design(prototype): 分流规则屏用 v2 真 ui-kit 重建 + 搬入配置说明文档(Task 3)

用 v2 设计系统真原子(.segswitch/.setting-row/.toggle/.card/.pill/.btn)建
screens/routing-rules.html(移动子屏 + 桌面设置窗口),替代独立 docs mockup。
配置说明文档搬入本分支 docs/。设计-first,待确认 4 取舍后出实现计划。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-27 01:48:22 +08:00
parent 4a98bccedc
commit 51317e9f43
2 changed files with 319 additions and 0 deletions
+163
View File
@@ -0,0 +1,163 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>分流规则 · 可配置代理原型(真 ui-kit)</title>
<link rel="stylesheet" href="../tokens.css">
<link rel="stylesheet" href="../atoms.css">
<script src="../icons.js"></script>
<style>
body{ margin:0; background:var(--bg); color:var(--fg1); font-family:var(--font-sans); -webkit-font-smoothing:antialiased; }
.wrap{ max-width:1120px; margin:0 auto; padding:var(--space-8) var(--space-6) var(--space-16); }
h1{ font-family:var(--font-display); font-size:var(--text-h2); margin:0 0 var(--space-2); }
.lead{ font-size:var(--text-sm); color:var(--fg2); line-height:var(--leading-relaxed); max-width:720px; margin:0 0 var(--space-8); }
.lead code{ font-family:var(--font-mono); font-size:.85em; color:var(--accent); background:var(--accent-subtle); padding:1px 6px; border-radius:var(--radius-sm); }
h2.sec{ font-size:var(--text-body); color:var(--accent); border-bottom:1px solid var(--border); padding-bottom:var(--space-2); margin:var(--space-10) 0 var(--space-5); }
.stage{ display:flex; flex-wrap:wrap; gap:var(--space-10); align-items:flex-start; }
/* 手机框(精简 chrome,内容全走真 atoms) */
.phone{ width:360px; border-radius:var(--radius-2xl); overflow:hidden; background:var(--bg); border:1px solid var(--border); box-shadow:var(--shadow-lg); flex:0 0 auto; }
.sbar{ height:30px; display:flex; align-items:center; justify-content:space-between; padding:0 var(--space-5); font-size:var(--text-caption); color:var(--fg3); font-weight:600; }
.navb{ display:flex; align-items:center; gap:var(--space-3); padding:var(--space-2) var(--space-5) var(--space-4); border-bottom:1px solid var(--border); }
.navb .bk{ color:var(--accent); font-size:20px; }
.navb .ti{ font-family:var(--font-display); font-weight:700; font-size:var(--text-body); }
.navb .pl{ margin-left:auto; color:var(--accent); }
.pbody{ padding:var(--space-4) var(--space-4) var(--space-6); display:flex; flex-direction:column; gap:var(--space-2); max-height:660px; overflow-y:auto; }
.sec-label{ margin-top:var(--space-4); }
/* 规则行动作 pill 语义色(原型内联,实现时收进 atoms) */
.pill.act-direct{ color:var(--success); border-color:var(--success); background:color-mix(in srgb,var(--success) 14%,transparent); }
.pill.act-proxy{ color:var(--accent); border-color:var(--accent); background:var(--accent-subtle); }
.pill.act-reject{ color:var(--danger); border-color:var(--danger); background:color-mix(in srgb,var(--danger) 14%,transparent); }
.rtype{ font-family:var(--font-mono); font-size:var(--text-caption); color:var(--fg3); }
.rval{ font-family:var(--font-mono); font-size:var(--text-sm); }
.grip{ color:var(--fg3); opacity:.55; cursor:grab; }
/* 桌面窗口 */
.win{ flex:1; min-width:520px; border:1px solid var(--border); border-radius:var(--radius-xl); overflow:hidden; box-shadow:var(--shadow-lg); }
.wt{ height:38px; background:var(--surface); border-bottom:1px solid var(--border); display:flex; align-items:center; padding:0 var(--space-4); gap:var(--space-4); }
.wt .dt{ display:flex; gap:6px; } .wt .dt i{ width:11px; height:11px; border-radius:50%; background:var(--border-strong); } .wt .dt i:nth-child(1){ background:var(--danger); } .wt .dt i:nth-child(3){ background:var(--success); }
.wt .wn{ font-size:var(--text-sm); color:var(--fg3); font-weight:600; }
.wbody{ display:flex; min-height:480px; }
.wside{ width:150px; background:var(--surface); border-right:1px solid var(--border); padding:var(--space-3) var(--space-2); }
.wside .si{ padding:var(--space-2) var(--space-3); border-radius:var(--radius-md); font-size:var(--text-sm); color:var(--fg2); margin-bottom:2px; }
.wside .si.on{ background:var(--accent); color:var(--fg-on-accent); font-weight:600; }
.wmain{ flex:1; padding:var(--space-5) var(--space-6); overflow-y:auto; }
.wtable{ width:100%; border-collapse:collapse; font-size:var(--text-sm); }
.wtable th{ text-align:left; font-size:var(--text-caption); text-transform:uppercase; letter-spacing:var(--tracking-caps); color:var(--accent-2,var(--fg3)); padding:var(--space-2) var(--space-3); border-bottom:1px solid var(--border); }
.wtable td{ padding:var(--space-3); border-bottom:1px solid var(--border); vertical-align:middle; }
.note{ display:flex; gap:var(--space-8); flex-wrap:wrap; margin-top:var(--space-6); }
.note>div{ flex:1; min-width:260px; }
.note h3{ font-size:var(--text-sm); color:var(--accent); margin:0 0 var(--space-2); }
.note p{ font-size:var(--text-sm); color:var(--fg2); margin:var(--space-2) 0; line-height:var(--leading-relaxed); }
</style>
</head>
<body>
<div class="wrap">
<h1>分流规则 · 可配置代理</h1>
<p class="lead">Task 3 原型(<b>用 v2 真实设计系统重建</b>:全走 <code>atoms.css</code> / <code>tokens.css</code> / <code>icons.js</code>,非独立 mockup)。让用户像 Shadowrocket 那样自定义"直连 / 走隧道 / 拒绝"。规则模型与实现见 <a href="../../../docs/configurable-proxy-spec.html" style="color:var(--accent)">配置说明文档</a></p>
<h2 class="sec">移动端 · 分流规则子屏</h2>
<div class="stage">
<div class="phone">
<div class="sbar"><span>9:41</span><span>5G ▮▮▮</span></div>
<div class="navb"><span class="bk"></span><span class="ti">分流规则</span><span class="pl"><svg style="width:20px;height:20px"><use href="#i-plus"/></svg></span></div>
<div class="pbody">
<div class="sec-label">代理模式</div>
<div class="segswitch">
<button class="segswitch-opt"><span>全局代理</span></button>
<button class="segswitch-opt is-active"><span>智能分流</span></button>
<button class="segswitch-opt"><span>全部直连</span></button>
</div>
<div class="sec-label">内置规则</div>
<div class="card card-flush">
<div class="setting-row"><span class="icon-box"><svg><use href="#i-globe"/></svg></span><div class="st-body"><div class="st-title">国内直连</div><div class="st-sub">GeoIP / GeoSite CN</div></div><button class="toggle is-on"><i></i></button></div>
<div class="setting-row"><span class="icon-box"><svg><use href="#i-home"/></svg></span><div class="st-body"><div class="st-title">局域网 / 私网直连</div><div class="st-sub">强制,不可关</div></div><span class="pill pill-outline">🔒</span></div>
<div class="setting-row"><span class="icon-box"><svg><use href="#i-server"/></svg></span><div class="st-body"><div class="st-title">私有服务走隧道</div><div class="st-sub">服务端下发</div></div><span class="pill pill-outline">🔒</span></div>
</div>
<div class="sec-label">我的规则 · 首命中生效</div>
<div class="card card-flush">
<div class="setting-row"><span class="grip"></span><div class="st-body"><div class="st-title rval">git.51yanmei.com</div><div class="st-sub rtype">域名后缀</div></div><span class="pill act-direct">直连</span></div>
<div class="setting-row"><span class="grip"></span><div class="st-body"><div class="st-title rval">aliyun.com</div><div class="st-sub rtype">域名后缀</div></div><span class="pill act-direct">直连</span></div>
<div class="setting-row"><span class="grip"></span><div class="st-body"><div class="st-title rval">github.com</div><div class="st-sub rtype">域名后缀</div></div><span class="pill act-proxy">走隧道</span></div>
<div class="setting-row"><span class="grip"></span><div class="st-body"><div class="st-title rval">category-ads</div><div class="st-sub rtype">GeoSite 规则集</div></div><span class="pill act-reject">拒绝</span></div>
<div class="setting-row"><span class="grip"></span><div class="st-body"><div class="st-title rval">35.190.0.0/16</div><div class="st-sub rtype">IP-CIDR</div></div><span class="pill act-proxy">走隧道</span></div>
</div>
<button class="btn btn-subtle btn-block" style="margin-top:var(--space-2)"><svg><use href="#i-plus"/></svg>添加规则</button>
<div class="sec-label">最终(未命中)</div>
<div class="card card-flush">
<div class="setting-row"><div class="st-body"><div class="st-title">FINAL 兜底</div></div><span class="pill act-proxy">走隧道</span></div>
</div>
</div>
</div>
<!-- 添加规则子屏(精简) -->
<div class="phone">
<div class="sbar"><span>9:41</span><span>5G ▮▮▮</span></div>
<div class="navb"><span class="bk">取消</span><span class="ti">添加规则</span><span class="pl" style="font-weight:700;font-size:var(--text-sm)">保存</span></div>
<div class="pbody">
<div class="sec-label">类型</div>
<div class="card"><div style="display:flex;flex-wrap:wrap;gap:var(--space-2)">
<span class="pill pill-outline">域名</span><span class="pill pill-accent">域名后缀</span><span class="pill pill-outline">关键字</span>
<span class="pill pill-outline">IP-CIDR</span><span class="pill pill-outline">GeoIP</span><span class="pill pill-outline">GeoSite</span>
</div></div>
<div class="sec-label"></div>
<div class="field"><input class="input" value="git.51yanmei.com" readonly></div>
<div class="sec-label">动作</div>
<div class="card"><div style="display:flex;gap:var(--space-2)">
<span class="pill act-direct">直连 DIRECT</span><span class="pill pill-outline">走隧道 PROXY</span><span class="pill pill-outline">拒绝 REJECT</span>
</div></div>
<div class="sec-label">预览 · 翻译为 sing-box 规则</div>
<div class="card"><pre style="margin:0;font-family:var(--font-mono);font-size:var(--text-caption);color:var(--fg2);white-space:pre-wrap">{ "domain_suffix":
["git.51yanmei.com"],
"outbound": "direct" }</pre></div>
</div>
</div>
</div>
<h2 class="sec">桌面端 · 设置窗口(表格 + 实时预览)</h2>
<div class="win">
<div class="wt"><span class="dt"><i></i><i></i><i></i></span><span class="wn">Pangolin — 设置 · 分流规则</span></div>
<div class="wbody">
<div class="wside">
<div class="si">通用</div><div class="si">节点</div><div class="si on">分流规则</div><div class="si">账户</div><div class="si">统计</div>
</div>
<div class="wmain">
<div style="display:flex;align-items:center;gap:var(--space-4);margin-bottom:var(--space-4)">
<div style="font-family:var(--font-display);font-weight:700;font-size:var(--text-h3,var(--text-body))">分流规则</div>
<div class="segswitch" style="margin-left:auto">
<button class="segswitch-opt"><span>全局</span></button><button class="segswitch-opt is-active"><span>智能分流</span></button><button class="segswitch-opt"><span>直连</span></button>
</div>
</div>
<div class="card" style="display:flex;gap:var(--space-6);flex-wrap:wrap;align-items:center;margin-bottom:var(--space-4);padding:var(--space-3) var(--space-4)">
<label style="display:flex;align-items:center;gap:var(--space-2);font-size:var(--text-sm)">国内直连 <button class="toggle is-on"><i></i></button></label>
<span style="font-size:var(--text-sm);color:var(--fg3)">LAN 直连 🔒</span>
<span style="font-size:var(--text-sm);color:var(--fg3)">私有服务走隧道 🔒</span>
</div>
<table class="wtable">
<tr><th style="width:24px"></th><th style="width:120px">类型</th><th></th><th style="width:92px">动作</th><th style="width:130px">说明</th><th style="width:44px">启用</th></tr>
<tr><td class="grip"></td><td class="rtype">域名后缀</td><td class="rval">git.51yanmei.com</td><td><span class="pill act-direct">直连</span></td><td style="color:var(--fg3)">CI 源</td><td><button class="toggle is-on"><i></i></button></td></tr>
<tr><td class="grip"></td><td class="rtype">域名后缀</td><td class="rval">aliyun.com</td><td><span class="pill act-direct">直连</span></td><td style="color:var(--fg3)">镜像</td><td><button class="toggle is-on"><i></i></button></td></tr>
<tr><td class="grip"></td><td class="rtype">域名后缀</td><td class="rval">github.com</td><td><span class="pill act-proxy">走隧道</span></td><td style="color:var(--fg3)"></td><td><button class="toggle is-on"><i></i></button></td></tr>
<tr><td class="grip"></td><td class="rtype">GeoSite</td><td class="rval">category-ads</td><td><span class="pill act-reject">拒绝</span></td><td style="color:var(--fg3)">广告</td><td><button class="toggle is-on"><i></i></button></td></tr>
<tr><td class="grip"></td><td class="rtype">IP-CIDR</td><td class="rval">35.190.0.0/16</td><td><span class="pill act-proxy">走隧道</span></td><td style="color:var(--fg3)"></td><td><button class="toggle"><i></i></button></td></tr>
</table>
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-4);align-items:center">
<button class="btn btn-subtle"><svg><use href="#i-plus"/></svg>添加规则</button>
<button class="btn btn-ghost">从文本导入</button>
<button class="btn btn-ghost">重置默认</button>
<span style="margin-left:auto;font-size:var(--text-sm);color:var(--fg3)">FINAL:<span class="pill act-proxy">走隧道</span></span>
</div>
</div>
</div>
</div>
<div class="note">
<div><h3>为什么用 v2 真 ui-kit</h3><p>本屏所有组件(段选 <code>.segswitch</code>、设置行 <code>.setting-row</code>、开关 <code>.toggle</code>、卡片 <code>.card</code>、胶囊 <code>.pill</code>、按钮 <code>.btn</code>)都来自 v2 设计系统真原子,颜色全走 token,与线上 App 一致——不是独立 mockup。动作语义色(直连绿/走隧道橙/拒绝红)是原型内联,实现时收进 atoms。</p></div>
<div><h3>待你确认(设计层)</h3><p>①规则粒度够不够 ②要不要分应用代理(per-app,建议后置)③「从文本导入」Phase 1 还是 2 ④档案 per-user 还是 per-device。确认后出 Phase 1 实现计划。</p></div>
</div>
</div>
</body>
</html>
+156
View File
@@ -0,0 +1,156 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pangolin 可配置分流 · 配置说明</title>
<style>
:root{
--bg:#0f1117; --panel:#171a22; --panel2:#1d2129; --fg:#e6e8ee; --fg2:#a8afbd;
--accent:#e0884f; --accent2:#5fb0c9; --ok:#5ec27a; --bad:#e06a6a; --warn:#e0b84f;
--border:#262b36; --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;
--mono:"SF Mono",ui-monospace,Menlo,Consolas,monospace;
}
@media (prefers-color-scheme: light){:root{--bg:#f4f5f8;--panel:#fff;--panel2:#eef0f4;--fg:#1a1d24;--fg2:#5a6172;--border:#dde0e7}}
:root[data-theme="dark"]{--bg:#0f1117;--panel:#171a22;--panel2:#1d2129;--fg:#e6e8ee;--fg2:#a8afbd;--border:#262b36}
:root[data-theme="light"]{--bg:#f4f5f8;--panel:#fff;--panel2:#eef0f4;--fg:#1a1d24;--fg2:#5a6172;--border:#dde0e7}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.7;font-size:15px}
.wrap{max-width:860px;margin:0 auto;padding:34px 22px 90px}
h1{font-size:25px;margin:0 0 4px}
.lead{color:var(--fg2);margin:0 0 8px;font-size:14.5px}
h2{font-size:18px;color:var(--accent);border-bottom:1px solid var(--border);padding-bottom:6px;margin:40px 0 14px}
h3{font-size:15.5px;color:var(--accent2);margin:24px 0 8px}
p{margin:9px 0}
code{font-family:var(--mono);font-size:.87em;background:var(--panel2);padding:1px 6px;border-radius:5px;color:#f0d9c4}
pre{background:#0a0c11;border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;font-family:var(--mono);font-size:12.5px;line-height:1.55;color:#cdd3df}
table{border-collapse:collapse;width:100%;font-size:13.5px;margin:12px 0;display:block;overflow-x:auto}
th,td{border:1px solid var(--border);padding:8px 11px;text-align:left;vertical-align:top}
th{background:var(--panel2);color:var(--accent2);font-weight:600;white-space:nowrap}
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:4px 18px;margin:14px 0}
.tag{display:inline-block;font-size:11px;padding:1px 8px;border-radius:20px;font-family:var(--mono)}
.t-direct{background:rgba(94,194,122,.16);color:var(--ok)} .t-proxy{background:rgba(224,136,79,.18);color:var(--accent)} .t-reject{background:rgba(224,106,106,.16);color:var(--bad)}
.box{border-left:3px solid var(--accent);background:var(--panel);border-radius:0 10px 10px 0;padding:12px 16px;margin:14px 0;font-size:14px}
.box.warn{border-color:var(--warn)} .box.q{border-color:var(--accent2)}
ul{padding-left:22px} li{margin:5px 0}
.step{display:flex;gap:10px;align-items:baseline;margin:6px 0}
.step b{color:var(--accent2);font-family:var(--mono);font-size:13px}
a{color:var(--accent2)}
</style>
</head>
<body>
<div class="wrap">
<h1>Pangolin 可配置分流 · 配置说明文档</h1>
<p class="lead">Task 3 设计 · 让用户像 Shadowrocket 那样自定义路由规则(哪些直连 / 走隧道 / 拒绝)。</p>
<p class="lead">配套视觉原型:<a href="configurable-proxy-prototype.html">交互原型 →</a> · 状态:<b style="color:var(--warn)">设计待确认(未开发)</b></p>
<h2>1. 目标与背景</h2>
<p>当前 pangolin 客户端的分流是<b>服务端固定渲染</b>的:LAN 直连、DNS 劫持、可选国内分流(geoip/geosite-cn)、私有域名走隧道,用户无法自定义。实际需求(如 CI 编译源、公司内网、某些国内站要直连;某些站要强制走隧道;广告要拒绝)无处配置——这次暴露的「CI 流量被灌进隧道拖垮小节点」就是典型。</p>
<p>本设计给用户一套<b>可配置路由规则</b>,心智模型对齐 Shadowrocket 的 <code>[Rule]</code> 段:<b>有序规则表,首命中生效</b>,动作三选一(直连 / 走隧道 / 拒绝)。</p>
<h2>2. 架构铁律:客户端不拼配置</h2>
<div class="box warn"><code>ARCHITECTURE.md §3.1</code>:Dart/Flutter 客户端<b>不得</b>自行拼装或修改 sing-box 配置——配置一律服务端渲染、原样下发。所以"客户端可配置"不能变成"客户端本地改 config"。</div>
<p>解法:<b>用户在 App 编规则 → 存为服务端 per-user「路由档案(routing profile)」→ connect 时服务端把档案翻译进渲染的配置</b>。客户端只负责编辑 UI + 存/取档案,永不碰 sing-box JSON。</p>
<pre>App 规则编辑器 ──PUT /v1/routing-profile──▶ 服务端存 per-user 档案(DB)
App 点连接 ──POST /v1/nodes/{id}/connect──▶ BuildClientConfig(读档案→翻译成 route.rules)
App ◀────────── 完整 sing-box 配置(含用户规则)─────┘ 原样喂内核</pre>
<h2>3. 规则模型</h2>
<p>一条规则 = <code>{ type, value, action, note?, enabled }</code>。整个档案:</p>
<pre>{
"mode": "rule", // global | rule | direct(对齐 shadowrocket 三模式)
"builtin": {
"china_direct": true, // 国内分流(geoip-cn/geosite-cn → 直连)开关
"lan_direct": true, // LAN/私网直连(强制,不可关)
"private_via_tunnel": true // 私有服务域名走隧道(服务端下发,不可关)
},
"rules": [ // 用户自定义,有序,首命中生效
{ "type": "domain_suffix", "value": "git.51yanmei.com", "action": "direct", "note": "CI 源", "enabled": true },
{ "type": "geosite", "value": "category-ads", "action": "reject" },
{ "type": "ip_cidr", "value": "35.190.0.0/16", "action": "proxy" }
],
"final": "proxy" // 兜底:未命中任何规则的动作
}</pre>
<h3>类型 type</h3>
<table>
<tr><th>type</th><th>值示例</th><th>sing-box 字段</th></tr>
<tr><td><code>domain</code></td><td>example.com</td><td><code>domain</code>(精确)</td></tr>
<tr><td><code>domain_suffix</code></td><td>aliyun.com</td><td><code>domain_suffix</code>(含子域)</td></tr>
<tr><td><code>domain_keyword</code></td><td>google</td><td><code>domain_keyword</code></td></tr>
<tr><td><code>ip_cidr</code></td><td>35.190.0.0/16 · ::/0</td><td><code>ip_cidr</code>(v4/v6)</td></tr>
<tr><td><code>geoip</code></td><td>CN · US</td><td><code>rule_set</code>(自托管 geoip-*.srs)</td></tr>
<tr><td><code>geosite</code></td><td>cn · netflix · category-ads</td><td><code>rule_set</code>(自托管 geosite-*.srs)</td></tr>
</table>
<h3>动作 action → sing-box outbound</h3>
<table>
<tr><th>action</th><th>outbound</th><th>说明</th></tr>
<tr><td><span class="tag t-direct">direct</span></td><td><code>direct</code></td><td>物理网卡直连(配合 route_exclude/reverse_mapping 真直连)</td></tr>
<tr><td><span class="tag t-proxy">proxy</span></td><td><code>auto</code></td><td>经节点(REALITY/Hy2 urltest 择优)</td></tr>
<tr><td><span class="tag t-reject">reject</span></td><td><code>block</code></td><td>阻断</td></tr>
</table>
<h2>4. 优先级(渲染顺序)</h2>
<p>服务端把规则按固定层级拼进 <code>route.rules</code>,<b>自上而下首命中</b>:</p>
<div class="card">
<div class="step"><b>1</b><span>DNS 劫持(port 53 → hijack-dns)· <b>系统强制</b></span></div>
<div class="step"><b>2</b><span>LAN / 私网直连(10/8·192.168/16·127/8)· <b>系统强制</b></span></div>
<div class="step"><b>3</b><span>私有服务域名 → 走隧道(<code>PANGOLIN_PRIVATE_SPLIT_DOMAINS</code><b>服务端</b></span></div>
<div class="step"><b>4</b><span><b>用户规则</b>(档案 <code>rules[]</code> 顺序展开)← 新增</span></div>
<div class="step"><b>5</b><span>国内分流 geoip-cn/geosite-cn → 直连(<code>china_direct</code> 开时)</span></div>
<div class="step"><b>6</b><span>FINAL 兜底(<code>final</code>:proxy / direct)</span></div>
</div>
<div class="box">用户规则排在国内分流<b>之前</b>——这样"我要 github 走隧道""我要某国内站走隧道"能压过 geoip-cn 的直连默认。系统层(1-3)永远在用户规则之上,防止用户误配把 DNS/LAN/私有服务弄坏。</div>
<h2>5. 直连是否真"不走 VPN"</h2>
<p><code>direct</code> outbound 让 sing-box 从物理网卡直接出连接。但 TUN <code>strict_route</code> 会把包重新捕回隧道——已有两个机制解决,用户规则复用:</p>
<ul>
<li><b>IP 段直连</b>:靠 <code>route_exclude_address</code>(在 auto_route 层排除,已用于 LAN)。用户加的 <code>ip_cidr → direct</code> 需同步进 route_exclude,否则被 strict_route 抓回。</li>
<li><b>域名直连</b>:靠 <code>dns.reverse_mapping</code>(记 IP↔域名,按 IP 连接时补回域名元数据让 domain 规则命中)+ local DNS(223.5.5.5,国内解析)。已用于私有域名分流,机制现成。</li>
</ul>
<div class="box warn">这是<b>关键实现难点</b>:域名类直连规则要真生效,必须开 reverse_mapping 且用 local DNS 解析该域名;IP 类直连规则要真生效,值必须同时进 route_exclude_address。这两点在实现计划里逐条落。</div>
<h2>6. 存储与传输(API)</h2>
<table>
<tr><th>端点</th><th>作用</th></tr>
<tr><td><code>GET /v1/routing-profile</code></td><td>拉当前用户档案(App 编辑器初始化;无则返回内置默认)</td></tr>
<tr><td><code>PUT /v1/routing-profile</code></td><td>保存档案(服务端校验:CIDR 合法、type 合法、条数上限、去重)</td></tr>
<tr><td><code>POST …/connect</code>(现有)</td><td>渲染时读该用户档案,翻译进 <code>route.rules</code></td></tr>
</table>
<p>档案存 DB(<code>routing_profiles</code> 表:<code>user_id · profile_json · updated_at</code>),不走 connect body(与现有 <code>split_cn</code> 走 query 的约束一致——大规则集不塞 query/body)。客户端本地也缓存一份(离线可看/编,连接时以服务端为准)。</p>
<h2>7. 校验与兜底(fail-safe)</h2>
<ul>
<li>PUT 时服务端校验每条规则:<code>ip_cidr</code> 可解析、<code>type</code> 在白名单、<code>geosite/geoip</code> 值在自托管规则集清单内、总条数 ≤ 上限(如 200)。非法整体拒绝(4xx + 逐条错误),不半保存。</li>
<li>渲染时档案损坏/为空 → 回退内置默认(等于现在的行为),绝不产出坏配置(参考 agent ACL 的 fail-closed/last-good 思路)。</li>
<li>系统层(DNS/LAN/私有服务)永远强制,用户规则无法覆盖或删除。</li>
</ul>
<h2>8. 与现有机制的关系</h2>
<table>
<tr><th>现有</th><th>本设计如何吸收</th></tr>
<tr><td>SplitCN(geoip/geosite-cn 直连,#5)</td><td>降为档案里的 <code>builtin.china_direct</code> 开关(层级 5)</td></tr>
<tr><td>PrivateSplitDomains(走隧道)</td><td>保留为系统层 3(服务端 env,用户不可动)</td></tr>
<tr><td>route_exclude_address(LAN)</td><td>保留 + 扩展:用户 ip_cidr→direct 规则动态并入</td></tr>
<tr><td>reverse_mapping / local DNS</td><td>复用:域名直连规则靠它命中</td></tr>
</table>
<p>换句话说,本设计是把三个散落的分流机制<b>统一收进一个用户可见、可配的规则模型</b>,底层复用已验证的 sing-box 手法。</p>
<h2>9. 待你确认的取舍</h2>
<div class="box q"><b>Q1 规则粒度</b>:先做"域名/IP/GeoIP/GeoSite + 三动作"的规则表(本设计)?还是要更细(URL 正则、UA、进程名)?建议先前者,后者 shadowrocket 也少人用。</div>
<div class="box q"><b>Q2 分应用代理(per-app)</b>:按 App 分流(仅 Android/桌面可行,iOS 系统扩展做不到)。较重,<b>建议后置</b>为独立任务。</div>
<div class="box q"><b>Q3「从文本导入」</b>:粘贴 Shadowrocket/Clash 规则批量建(原型第三屏)。锦上添花,<b>可 Phase 2</b></div>
<div class="box q"><b>Q4 档案作用域</b>:per-user(跨设备同步,推荐)还是 per-device(每台独立)?推荐 per-user + 单档案起步。</div>
<h2>10. 分期建议</h2>
<ul>
<li><b>Phase 1(核心)</b>:routing_profiles 表 + API + BuildClientConfig 翻译(域名/IP/GeoIP/GeoSite × 直连/走隧道/拒绝)+ 移动端规则表 UI(增删改排序)+ 校验/兜底 + reverse_mapping/route_exclude 打通。</li>
<li><b>Phase 2</b>:从文本导入、内置规则模板(常用国内直连包/广告拒绝包一键加)、桌面端 UI。</li>
<li><b>Phase 3(可选)</b>:分应用代理、规则订阅 URL。</li>
</ul>
<p style="margin-top:24px;color:var(--fg2)">确认这份设计后,我用 <code>writing-plans</code> 出 Phase 1 的实现计划(TDD、多端、服务端翻译逐条测),再开发。</p>
</div>
</body>
</html>