Compare commits

...

3 Commits

Author SHA1 Message Date
wangjia 9772db7d12 docs(configurable-proxy): 加桌面端(macOS/Windows)设置窗口原型
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 17s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 16s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 17s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Failing after 11m38s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Failing after 11m49s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 11m58s
ci-pangolin / Go — build + test (pull_request) Failing after 12m8s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Failing after 12m20s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Failing after 12m30s
ci-pangolin / Flutter — analyze + test (pull_request) Failing after 12m39s
ci-pangolin / OpenAPI Sync Check (pull_request) Failing after 12m48s
ci-pangolin / Lint — shellcheck (pull_request) Failing after 12m56s
移动三屏之外补桌面端:左侧栏 + 规则表格(内联编辑/拖动排序)+ 右侧实时预览
生成的 sing-box route.rules。两端共用同一套服务端 per-user 路由档案。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-27 00:41:12 +08:00
wangjia 45158c8b99 docs: 客户端可配置分流(类 Shadowrocket)设计 + 交互原型(Task 3,待确认)
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 27s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 18s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 17s
ci-pangolin / Lint — shellcheck (pull_request) Successful in 32m31s
ci-pangolin / Flutter — analyze + test (pull_request) Successful in 5m41s
ci-pangolin / OpenAPI Sync Check (pull_request) Failing after 11m25s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Failing after 15m29s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 35m20s
ci-pangolin / Go — build + test (pull_request) Failing after 5m34s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Successful in 2m33s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Failing after 10m41s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 10m48s
让用户自定义路由规则(域名/域名后缀/关键字/IP-CIDR/GeoIP/GeoSite × 直连/走隧道/
拒绝),有序首命中。架构铁律(client 不拼配置)下走服务端 per-user 路由档案:
App 编规则 → PUT /v1/routing-profile 存档案 → connect 时 BuildClientConfig
翻译进 sing-box route.rules,原样下发。

产物:
- docs/configurable-proxy-prototype.html — 移动端三屏 mockup(规则表/加规则/文本导入)
- docs/configurable-proxy-spec.html — 规则模型 + 优先级层级 + 直连真生效
  (route_exclude/reverse_mapping)+ API + 校验兜底 + 与现有机制关系 + 分期 + 4 个待确认取舍
- docs/index.html 登记

设计-first,待用户确认后出实现计划开发。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-27 00:19:34 +08:00
wangjia 4ca21f9ee9 ci: 修外网发版 GFW 网络坑(sing-box 公网镜像 + gradle/maven 国内镜像)(#5)
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 26s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 21s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 24s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Failing after 13m27s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 13m36s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 13m46s
ci-pangolin / Go — build + test (push) Failing after 13m56s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Failing after 14m7s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Failing after 14m17s
ci-pangolin / Flutter — analyze + test (push) Failing after 14m28s
ci-pangolin / OpenAPI Sync Check (push) Failing after 14m36s
ci-pangolin / Lint — shellcheck (push) Failing after 14m48s
Deploy Client / build-windows (push) Successful in 1m46s
Deploy Client / build-android (push) Failing after 44m39s
Deploy Client / release-deploy (push) Has been skipped
Deploy Client / build-macos (push) Failing after 2m5s
Deploy Client / build-ios (push) Successful in 10m6s
2026-07-26 12:44:36 +00:00
3 changed files with 480 additions and 0 deletions
+319
View File
@@ -0,0 +1,319 @@
<!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.6;font-size:15px}
.wrap{max-width:1180px;margin:0 auto;padding:32px 20px 80px}
h1{font-size:24px;margin:0 0 4px}
.lead{color:var(--fg2);margin:0 0 28px;font-size:14.5px}
h2{font-size:17px;color:var(--accent);border-bottom:1px solid var(--border);padding-bottom:6px;margin:38px 0 16px}
.cols{display:flex;gap:34px;flex-wrap:wrap;align-items:flex-start}
.note{flex:1;min-width:280px}
.note h3{font-size:15px;color:var(--accent2);margin:18px 0 6px}
.note p{color:var(--fg2);font-size:14px;margin:6px 0}
.tag{display:inline-block;font-size:11px;padding:1px 8px;border-radius:20px;font-family:var(--mono);vertical-align:middle}
.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)}
.t-sys{background:rgba(95,176,201,.14);color:var(--accent2)}
code{font-family:var(--mono);font-size:.87em;background:var(--panel2);padding:1px 6px;border-radius:5px;color:#f0d9c4}
/* phone frame */
.phones{display:flex;gap:26px;flex-wrap:wrap}
.phone{width:330px;background:#000;border-radius:38px;padding:11px;box-shadow:0 12px 40px rgba(0,0,0,.5);flex:0 0 auto}
.screen{background:var(--bg);border-radius:28px;overflow:hidden;height:686px;display:flex;flex-direction:column;border:1px solid var(--border)}
.statusbar{height:26px;display:flex;align-items:center;justify-content:space-between;padding:0 20px;font-size:12px;color:var(--fg2);font-weight:600}
.navbar{display:flex;align-items:center;gap:10px;padding:8px 16px 12px;border-bottom:1px solid var(--border)}
.navbar .back{color:var(--accent);font-size:20px}
.navbar .title{font-size:17px;font-weight:700}
.navbar .add{margin-left:auto;color:var(--accent);font-size:22px;font-weight:300}
.body{flex:1;overflow-y:auto;padding:14px 14px 20px}
.caption{font-size:11.5px;color:var(--fg2);text-transform:uppercase;letter-spacing:.5px;margin:16px 4px 7px}
.card{background:var(--panel);border:1px solid var(--border);border-radius:14px;overflow:hidden}
.segment{display:flex;background:var(--panel2);border-radius:11px;padding:3px;margin:2px}
.segment div{flex:1;text-align:center;font-size:13px;padding:7px 0;border-radius:8px;color:var(--fg2);font-weight:600}
.segment div.on{background:var(--accent);color:#1a1206}
.row{display:flex;align-items:center;gap:11px;padding:12px 14px;border-bottom:1px solid var(--border)}
.row:last-child{border-bottom:none}
.row .grip{color:var(--fg2);font-size:15px;opacity:.6}
.row .main{flex:1;min-width:0}
.row .rt{font-size:14px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.row .rsub{font-size:11.5px;color:var(--fg2);font-family:var(--mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.toggle{width:40px;height:24px;border-radius:20px;background:var(--accent);position:relative;flex:0 0 auto}
.toggle.off{background:var(--panel2)}
.toggle::after{content:"";position:absolute;top:2px;left:18px;width:20px;height:20px;border-radius:50%;background:#fff;transition:.2s}
.toggle.off::after{left:2px}
.rowlabel{flex:1;font-size:14px}
.rowval{font-size:13px;color:var(--fg2);font-family:var(--mono)}
.lock{font-size:11px;color:var(--fg2)}
.addbtn{text-align:center;color:var(--accent);font-size:14px;font-weight:600;padding:13px}
.hint{font-size:11.5px;color:var(--fg2);padding:8px 6px;line-height:1.5}
/* modal mock (add rule) */
.sheet{background:var(--panel);border:1px solid var(--border);border-radius:18px;overflow:hidden}
.sheet .sh{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center}
.sheet .sh .c{color:var(--fg2);font-size:14px}
.sheet .sh .t{flex:1;text-align:center;font-weight:700;font-size:15px}
.sheet .sh .s{color:var(--accent);font-size:14px;font-weight:700}
.field{padding:12px 16px;border-bottom:1px solid var(--border)}
.field .fl{font-size:11.5px;color:var(--fg2);margin-bottom:6px}
.field .fv{font-size:15px}
.chips{display:flex;gap:8px;flex-wrap:wrap}
.chip{font-size:12.5px;padding:6px 12px;border-radius:9px;background:var(--panel2);color:var(--fg2);border:1px solid transparent}
.chip.on{background:rgba(224,136,79,.16);color:var(--accent);border-color:var(--accent)}
.inp{background:var(--panel2);border:1px solid var(--border);border-radius:9px;padding:9px 11px;font-family:var(--mono);font-size:14px;color:var(--fg)}
table{border-collapse:collapse;width:100%;font-size:13.5px;margin:6px 0}
th,td{border:1px solid var(--border);padding:7px 10px;text-align:left;vertical-align:top}
th{background:var(--panel2);color:var(--accent2);font-weight:600}
td code{white-space:nowrap}
/* desktop window */
.win{border:1px solid var(--border);border-radius:12px;overflow:hidden;box-shadow:0 14px 44px rgba(0,0,0,.45);background:var(--bg);max-width:1000px}
.titlebar{height:38px;background:var(--panel2);display:flex;align-items:center;padding:0 14px;border-bottom:1px solid var(--border)}
.dots{display:flex;gap:8px} .dots i{width:12px;height:12px;border-radius:50%;background:#3a3f4b;display:block}
.dots i:nth-child(1){background:#e0655a}.dots i:nth-child(2){background:#e0b84f}.dots i:nth-child(3){background:#5ec27a}
.wt{flex:1;text-align:center;font-size:13px;color:var(--fg2);font-weight:600;margin-left:-40px}
.winbody{display:flex;height:560px}
.sidebar{width:172px;background:var(--panel);border-right:1px solid var(--border);padding:12px 8px;flex:0 0 auto}
.si{padding:9px 12px;border-radius:8px;font-size:13.5px;color:var(--fg2);margin-bottom:2px}
.si.on{background:var(--accent);color:#1a1206;font-weight:600}
.content{flex:1;min-width:0;padding:18px 20px;overflow-y:auto;display:flex;flex-direction:column}
.chead{display:flex;align-items:center;gap:16px;margin-bottom:14px}
.ctitle{font-size:18px;font-weight:700}
.modeseg{display:flex;background:var(--panel2);border-radius:9px;padding:3px;margin-left:auto}
.modeseg span{font-size:12.5px;padding:6px 14px;border-radius:7px;color:var(--fg2);font-weight:600}
.modeseg span.on{background:var(--accent);color:#1a1206}
.binline{display:flex;gap:18px;flex-wrap:wrap;background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:11px 14px;margin-bottom:14px;font-size:13px;color:var(--fg2)}
.binline label{display:flex;align-items:center;gap:7px}
.tg{width:34px;height:20px;border-radius:14px;background:var(--accent);position:relative;display:inline-block}
.tg.off{background:var(--panel2)} .tg::after{content:"";position:absolute;top:2px;left:16px;width:16px;height:16px;border-radius:50%;background:#fff}.tg.off::after{left:2px}
.ruletable{border-collapse:collapse;width:100%;font-size:13px;display:table;margin:0}
.ruletable th{background:var(--panel2);color:var(--accent2);font-weight:600;padding:8px 10px;text-align:left;font-size:11.5px;text-transform:uppercase;letter-spacing:.4px;border:1px solid var(--border)}
.ruletable td{padding:9px 10px;border:1px solid var(--border);vertical-align:middle}
.ruletable tr:hover td{background:rgba(95,176,201,.05)}
.ruletable .g{color:var(--fg2);cursor:grab;opacity:.55}
.ruletable .v{font-family:var(--mono);font-size:12.5px}
.ty{font-size:11px;padding:2px 8px;border-radius:6px;background:var(--panel2);color:var(--fg2);font-family:var(--mono)}
.tablefoot{display:flex;align-items:center;gap:16px;margin-top:12px;font-size:13px;color:var(--accent);flex-wrap:wrap}
.tablefoot .fin{margin-left:auto;color:var(--fg2)}
.preview{width:250px;flex:0 0 auto;border-left:1px solid var(--border);background:var(--panel);padding:14px}
.pvh{font-size:11.5px;color:var(--fg2);text-transform:uppercase;letter-spacing:.4px;margin-bottom:8px}
.preview pre{font-size:11px;line-height:1.5;padding:11px;margin:0}
@media (max-width:820px){.preview{display:none}}
</style>
</head>
<body>
<div class="wrap">
<h1>Pangolin 可配置分流 · 交互原型</h1>
<p class="lead">Task 3 设计稿 · 让用户像 Shadowrocket 那样自定义"哪些流量直连 / 走隧道 / 拒绝"。下含<b>移动端</b>三屏 + <b>桌面端(macOS/Windows)</b>设置窗口的视觉原型(mockup,非真实数据),两端共用同一套服务端路由档案。配套规则模型与实现见 <a href="configurable-proxy-spec.html" style="color:var(--accent2)">配置说明文档 →</a></p>
<div class="phones">
<!-- Screen 1: 分流规则主屏 -->
<div class="phone"><div class="screen">
<div class="statusbar"><span>9:41</span><span>5G ▮▮▮</span></div>
<div class="navbar"><span class="back"></span><span class="title">分流规则</span><span class="add"></span></div>
<div class="body">
<div class="caption">代理模式</div>
<div class="card"><div class="segment">
<div>全局代理</div><div class="on">智能分流</div><div>全部直连</div>
</div></div>
<div class="hint">智能分流:按下面规则表逐条匹配;命中「直连」不走隧道,「走隧道」经节点出网。</div>
<div class="caption">内置规则(系统)</div>
<div class="card">
<div class="row"><span class="rowlabel">国内直连(GeoIP/GeoSite CN)</span><span class="toggle"></span></div>
<div class="row"><span class="rowlabel">局域网直连(LAN/私网)</span><span class="lock">🔒 强制</span></div>
<div class="row"><span class="rowlabel">私有服务走隧道</span><span class="lock">🔒 服务端</span></div>
</div>
<div class="caption">我的规则 · 按顺序匹配(首命中生效)</div>
<div class="card">
<div class="row"><span class="grip"></span><div class="main"><div class="rt">git.51yanmei.com</div><div class="rsub">域名后缀 · DOMAIN-SUFFIX</div></div><span class="tag t-direct">直连</span></div>
<div class="row"><span class="grip"></span><div class="main"><div class="rt">*.aliyun.com</div><div class="rsub">域名后缀 · DOMAIN-SUFFIX</div></div><span class="tag t-direct">直连</span></div>
<div class="row"><span class="grip"></span><div class="main"><div class="rt">github.com</div><div class="rsub">域名后缀 · DOMAIN-SUFFIX</div></div><span class="tag t-proxy">走隧道</span></div>
<div class="row"><span class="grip"></span><div class="main"><div class="rt">广告 / 追踪(GeoSite: category-ads)</div><div class="rsub">规则集 · GEOSITE</div></div><span class="tag t-reject">拒绝</span></div>
<div class="row"><span class="grip"></span><div class="main"><div class="rt">35.190.0.0/16</div><div class="rsub">IP 段 · IP-CIDR</div></div><span class="tag t-proxy">走隧道</span></div>
</div>
<div class="card" style="margin-top:10px"><div class="addbtn"> 添加规则</div></div>
<div class="caption">最终(未命中任何规则)</div>
<div class="card"><div class="row"><span class="rowlabel">FINAL · 兜底动作</span><span class="tag t-proxy">走隧道</span></div></div>
<div class="hint">从文本导入(Shadowrocket / Clash 规则)· 重置为默认</div>
</div>
</div></div>
<!-- Screen 2: 添加/编辑规则 -->
<div class="phone"><div class="screen">
<div class="statusbar"><span>9:41</span><span>5G ▮▮▮</span></div>
<div class="navbar"><span class="back"></span><span class="title">添加规则</span></div>
<div class="body">
<div class="sheet">
<div class="sh"><span class="c">取消</span><span class="t">添加规则</span><span class="s">保存</span></div>
<div class="field"><div class="fl">类型</div>
<div class="chips">
<span class="chip">域名</span><span class="chip on">域名后缀</span><span class="chip">关键字</span>
<span class="chip">IP-CIDR</span><span class="chip">GeoIP 国家</span><span class="chip">GeoSite 类别</span>
</div>
</div>
<div class="field"><div class="fl"></div>
<input class="inp" style="width:100%" value="git.51yanmei.com" readonly>
</div>
<div class="field"><div class="fl">动作</div>
<div class="chips">
<span class="chip on" style="border-color:var(--ok);color:var(--ok);background:rgba(94,194,122,.14)">直连 DIRECT</span>
<span class="chip">走隧道 PROXY</span>
<span class="chip">拒绝 REJECT</span>
</div>
</div>
<div class="field" style="border-bottom:none"><div class="fl">说明(选填)</div>
<input class="inp" style="width:100%" value="CI 编译源,国内直连不占隧道" readonly>
</div>
</div>
<div class="hint">GeoIP/GeoSite 由服务端自托管规则集(.srs)支持,墙内可直连下载,无需梯子。IP-CIDR 支持 v4/v6。</div>
<div class="caption">预览:将翻译为 sing-box 路由规则</div>
<div class="card" style="padding:12px"><pre style="margin:0;font-size:11.5px;color:#cdd3df;font-family:var(--mono);white-space:pre-wrap">{ "domain_suffix": ["git.51yanmei.com"],
"outbound": "direct" }</pre></div>
</div>
</div></div>
<!-- Screen 3: 从文本导入 -->
<div class="phone"><div class="screen">
<div class="statusbar"><span>9:41</span><span>5G ▮▮▮</span></div>
<div class="navbar"><span class="back"></span><span class="title">从文本导入</span><span class="add" style="font-size:14px;font-weight:700">导入</span></div>
<div class="body">
<div class="caption">粘贴规则(Shadowrocket / Clash 风格)</div>
<div class="card" style="padding:12px"><pre style="margin:0;font-size:11.5px;color:#cdd3df;font-family:var(--mono);white-space:pre-wrap"># 一行一条:类型,值,动作
DOMAIN-SUFFIX,git.51yanmei.com,DIRECT
DOMAIN-SUFFIX,aliyun.com,DIRECT
DOMAIN-SUFFIX,tencent.com,DIRECT
DOMAIN-SUFFIX,flutter-io.cn,DIRECT
DOMAIN-SUFFIX,github.com,PROXY
GEOSITE,category-ads,REJECT
GEOIP,CN,DIRECT
FINAL,PROXY</pre></div>
<div class="hint">解析后逐条加入「我的规则」,可再拖动排序。非法行(错误 CIDR / 未知类型)高亮跳过并提示。</div>
<div class="caption">解析结果预览</div>
<div class="card">
<div class="row"><div class="main"><div class="rt">✓ 7 条有效</div><div class="rsub">4 直连 · 1 走隧道 · 1 拒绝 · FINAL</div></div><span class="tag t-sys">OK</span></div>
<div class="row"><div class="main"><div class="rt">⚠ 0 条跳过</div><div class="rsub">无非法行</div></div></div>
</div>
</div>
</div></div>
</div>
<h2>桌面端(macOS / Windows)· 设置窗口</h2>
<p class="lead" style="margin-bottom:16px">桌面有更大空间:左侧栏 + 规则<b>表格</b>(可内联编辑/拖动排序)+ 右侧<b>实时预览</b>生成的 sing-box 规则。同一套档案模型,与移动端共用服务端。</p>
<div class="win">
<div class="titlebar"><span class="dots"><i></i><i></i><i></i></span><span class="wt">Pangolin — 设置</span></div>
<div class="winbody">
<div class="sidebar">
<div class="si">通用</div>
<div class="si">节点</div>
<div class="si on">分流规则</div>
<div class="si">账户</div>
<div class="si">统计</div>
<div class="si">关于</div>
</div>
<div class="content">
<div class="chead">
<div class="ctitle">分流规则</div>
<div class="modeseg"><span>全局代理</span><span class="on">智能分流</span><span>全部直连</span></div>
</div>
<div class="binline">
<label>国内直连(GeoIP/GeoSite CN)<span class="tg"></span></label>
<label>LAN / 私网直连 <span class="lock">🔒 强制</span></label>
<label>私有服务走隧道 <span class="lock">🔒 服务端</span></label>
</div>
<table class="ruletable">
<tr><th style="width:26px"></th><th style="width:130px">类型</th><th></th><th style="width:96px">动作</th><th style="width:150px">说明</th><th style="width:52px">启用</th></tr>
<tr><td class="g"></td><td><span class="ty">域名后缀</span></td><td class="v">git.51yanmei.com</td><td><span class="tag t-direct">直连</span></td><td style="color:var(--fg2);font-size:12px">CI 编译源</td><td><span class="tg" style="transform:scale(.85)"></span></td></tr>
<tr><td class="g"></td><td><span class="ty">域名后缀</span></td><td class="v">aliyun.com</td><td><span class="tag t-direct">直连</span></td><td style="color:var(--fg2);font-size:12px">maven 镜像</td><td><span class="tg" style="transform:scale(.85)"></span></td></tr>
<tr><td class="g"></td><td><span class="ty">域名后缀</span></td><td class="v">github.com</td><td><span class="tag t-proxy">走隧道</span></td><td style="color:var(--fg2);font-size:12px"></td><td><span class="tg" style="transform:scale(.85)"></span></td></tr>
<tr><td class="g"></td><td><span class="ty">GeoSite</span></td><td class="v">category-ads</td><td><span class="tag t-reject">拒绝</span></td><td style="color:var(--fg2);font-size:12px">广告/追踪</td><td><span class="tg" style="transform:scale(.85)"></span></td></tr>
<tr><td class="g"></td><td><span class="ty">IP-CIDR</span></td><td class="v">35.190.0.0/16</td><td><span class="tag t-proxy">走隧道</span></td><td style="color:var(--fg2);font-size:12px"></td><td><span class="tg off" style="transform:scale(.85)"></span></td></tr>
</table>
<div class="tablefoot"><span> 添加规则</span><span>⇪ 从文本导入</span><span>↺ 重置默认</span><span class="fin">FINAL 兜底:<span class="tag t-proxy">走隧道</span></span></div>
</div>
<div class="preview">
<div class="pvh">实时预览 · route.rules</div>
<pre>[
{"action":"hijack-dns",
"port":[53]},
{"ip_cidr":["10.0.0.0/8",
"192.168.0.0/16",...],
"outbound":"direct"},
{"domain_suffix":
["git.51yanmei.com",
"aliyun.com"],
"outbound":"direct"},
{"domain_suffix":
["github.com"],
"outbound":"auto"},
{"rule_set":["geosite-ads"],
"outbound":"block"},
{"rule_set":
["geoip-cn","geosite-cn"],
"outbound":"direct"}
]
final: "auto"</pre>
</div>
</div>
</div>
<p class="lead" style="margin-top:14px">桌面端与移动端<b>共用同一份服务端路由档案</b>——在任一端改,另一端下次连接生效(per-user 同步)。桌面多了内联表格编辑与实时预览,适合高级用户。</p>
<h2>三块界面在讲什么</h2>
<div class="cols">
<div class="note">
<h3>① 分流规则主屏</h3>
<p><b>代理模式</b>三选一(全局代理 / 智能分流 / 全部直连),对应 Shadowrocket 的 Global/Rule/Direct。</p>
<p><b>内置规则</b>是系统/服务端强制层:LAN 直连恒开(🔒),私有服务走隧道由服务端下发(🔒),国内直连可开关。</p>
<p><b>我的规则</b>是用户自定义、可拖动排序的有序表,<b>首命中生效</b><b>FINAL</b> 是兜底动作。</p>
</div>
<div class="note">
<h3>② 添加 / 编辑规则</h3>
<p><b>类型</b>(域名/后缀/关键字/IP-CIDR/GeoIP/GeoSite)→ 填<b></b> → 选<b>动作</b>(直连/走隧道/拒绝)。</p>
<p>实时<b>预览</b>该规则会被翻译成的 sing-box 路由片段,让高级用户看得懂、放心。</p>
</div>
<div class="note">
<h3>③ 从文本导入</h3>
<p>粘贴 <code>类型,值,动作</code> 一行一条,兼容 Shadowrocket/Clash 直觉。批量建规则、迁移旧配置。</p>
<p>解析给出有效/跳过统计,非法行不静默吞。</p>
</div>
</div>
<h2>动作与类型对照(给你确认)</h2>
<table>
<tr><th>动作</th><th>含义</th><th>sing-box outbound</th></tr>
<tr><td><span class="tag t-direct">直连</span></td><td>不走隧道,本机物理网卡直接出网(国内站、CI 源、私网)</td><td><code>direct</code></td></tr>
<tr><td><span class="tag t-proxy">走隧道</span></td><td>经 pangolin 节点出网(需科学上网的站)</td><td><code>auto</code>(REALITY/Hy2 择优)</td></tr>
<tr><td><span class="tag t-reject">拒绝</span></td><td>直接阻断(广告/追踪/恶意域)</td><td><code>block</code></td></tr>
</table>
<table>
<tr><th>类型</th><th>值示例</th><th>匹配</th></tr>
<tr><td>域名</td><td><code>example.com</code></td><td>精确</td></tr>
<tr><td>域名后缀</td><td><code>aliyun.com</code></td><td>含子域(<code>*.aliyun.com</code>)</td></tr>
<tr><td>关键字</td><td><code>google</code></td><td>域名含该串</td></tr>
<tr><td>IP-CIDR</td><td><code>35.190.0.0/16</code></td><td>IP 段(v4/v6)</td></tr>
<tr><td>GeoIP 国家</td><td><code>CN</code></td><td>该国 IP 段(自托管规则集)</td></tr>
<tr><td>GeoSite 类别</td><td><code>category-ads</code> / <code>netflix</code></td><td>预置域名集</td></tr>
</table>
<p class="lead" style="margin-top:30px">需要你确认:①这套 UI/模型对不对路?②要不要「从文本导入」这一块(可后置)?③规则粒度够不够(要不要 per-app 分应用代理,shadowrocket 有,较重、建议后置)?确认后我出实现计划开发。设计取舍与实现细节见 <a href="configurable-proxy-spec.html" style="color:var(--accent2)">配置说明文档 →</a></p>
</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>
+5
View File
@@ -84,6 +84,11 @@
<div class="d">iOS 隧道从 PoC 桩推到真机连通 + TestFlight;五条工作线 + NE 50MB 内存闸门;iPad 横屏侧栏布局适配。</div>
<div class="path">docs/ios-ipad-support-design.html</div>
</a>
<a class="doc" href="configurable-proxy-spec.html">
<div class="t">客户端可配置分流(类 Shadowrocket)设计 <span class="tag html">HTML</span></div>
<div class="d">让用户自定义路由规则(域名/IP/GeoIP/GeoSite × 直连/走隧道/拒绝),有序首命中。架构铁律下走「服务端 per-user 路由档案」:App 编规则→存档案→connect 时 server 翻译进 sing-box route.rules。含规则模型、优先级层级、直连真生效(route_exclude/reverse_mapping)、API、校验兜底、分期。配套<a href="configurable-proxy-prototype.html">交互原型(移动端三屏 + 桌面端设置窗口)</a>。Task 3,待确认。</div>
<div class="path">docs/configurable-proxy-spec.html</div>
</a>
<a class="doc" href="superpowers/specs/2026-06-21-windows-client-design.md">
<div class="t">Windows 客户端设计 <span class="tag md">MD</span></div>
<div class="d">Windows 端 sing-box 子进程 + wintun 隧道客户端设计。</div>