可配置分流 · Phase 1 实现计划

让用户像 Shadowrocket 那样自定义路由规则(域名 / IP / GeoIP / GeoSite × 直连 / 走隧道 / 拒绝),存服务端 per-user 档案,连接时服务端翻译进渲染的 sing-box 配置。

执行真相源(含 - [ ] checkbox):docs/superpowers/plans/2026-07-27-configurable-proxy-phase1.md · 配套设计:configurable-proxy-spec.html · 视觉原型 design/prototype/screens/ui-mobile.html

Goal / Architecture

目标:一套用户可见、可配的有序规则表(首命中生效),动作三选一。

架构铁律:客户端做规则编辑 UI + 存/取档案,绝不拼 sing-box 配置。用户编规则 → POST /v1/me/routing 存 per-user 档案(DB)→ connect 时 BuildClientConfig 读档案翻译成 route.rules,插在系统强制层之后、国内分流之前。IP 直连并入 route_exclude_address、域名直连靠 reverse_mapping+local DNS 真生效(复用 main 已验证的手法)。

:服务端 Go(chi + 裸 SQL + internal/db 方言层 + golang-migrate 双方言);客户端 Flutter + Riverpod(StateNotifier/AsyncNotifier)+ SharedPreferences。

规则模型(server ↔ client 共用契约)

// 档案 JSON
{
  "mode": "rule",                    // rule | global | direct
  "builtin": {
    "china_direct": true,           // 国内分流(geoip-cn/geosite-cn → 直连)开关
    "lan_direct": true,             // LAN/私网直连(强制,恒 true)
    "private_via_tunnel": true      // 私有服务域名走隧道(服务端 env,恒 true)
  },
  "rules": [                          // 有序,首命中生效,上限 200
    { "type":"domain_suffix", "value":"git.51yanmei.com", "action":"direct", "note":"CI 源", "enabled":true }
  ],
  "final": "proxy"                    // proxy | direct(未命中兜底)
}
type示例→ sing-box 字段
domainexample.comdomain(精确)
domain_suffixaliyun.comdomain_suffix(含子域)
domain_keywordgoogledomain_keyword
ip_cidr35.190.0.0/16ip_cidr(v4/v6)
geoip / geositecnrule_set(自托管 .srs;Phase 1 仅 cn)
action→ outbound说明
directdirect物理网卡直连
proxyauto经节点(REALITY/Hy2 urltest 择优)
rejectblock阻断

渲染层级(route.rules 自上而下,首命中)

三模式语义:rule(智能分流,默认)完整链;global(全局代理)跳过④⑤、final=auto、系统层仍在;direct(全部直连)跳过④⑤、final=direct、系统层仍在。用户规则排在国内分流之前——「我要 github 走隧道」能压过 geoip-cn 直连默认。系统层任何模式强制生效,用户不可覆盖。

Global Constraints(每个任务隐含)

任务总览(Task 0 前置 + 10 任务 TDD)

#任务关键产物 / 验证
0合并 main(前置)拿到私有分流/reverse_mapping/route_exclude;解 7 冲突(3 非 l10n 并集 + 4 l10n 归一 strings.json 重生成);go/flutter/漂移闸全绿
1routing_profiles 表 + Profile 类型 + Store双方言 000028;Store.Get/Upsert;sqlite 实库测试
2档案校验 / 规范化Validate()[]FieldError(type/CIDR/geo白名单/上限200)+ Normalize()(去重/trim)
3GET/POST /v1/me/routingRoutingAPI;无档案返回 Default;非法 400+逐条;openapi 两份;httptest
4核心:BuildClientConfig 翻译用户规则层;IP直连→route_exclude;域名直连→reverse_mapping;三模式;nil-profile 逐字节不变
5connect 读档案 → 传入渲染uid 已在手;fail-safe 回退默认;保留 ?split_cn 兜底
6客户端 model + API + providerRoutingProfile(fromJson/toJson);AccountApi.routingProfile/saveRoutingProfile;AsyncNotifier
7分流规则子屏 UI模式段选 / 内置 / 我的规则增删排序 / 添加弹层 / 冲突提示;对照原型
8设置入口下钻行 + 导航smartRoute 开关 → 「分流规则」行(当前模式 pill + chevron);双形态
9存档案后自动重连连接态 on 时 save→onNodeChanged 重连使新规则生效
10端到端联调 + 收尾go/flutter 全量 + 真机冒烟 + 文档登记 + draft PR

File Structure

服务端(Go)

客户端(Flutter)

关键任务细节(核心代码)

Task 0 · 合并 main(前置)

当前分支落后 main 37 commit,spec 复用的 PrivateSplitDomains(系统层3)/reverse_mapping/route_exclude_address 都在 main 的 74d8c85、不在此分支。必须先合并。冲突 7 个:.gitignore/docs/index.html/scripts/local_test.sh(取并集)+ strings_{es,ja,ko,ru}.dart(main 手写 pay-v2 getter vs 我的 codegen 重排 → 把 main 新增并进 strings.json 单源后 gen_l10n_dart.mjs 重生成)。验证:go build/test + flutter analyze/test + 三段漂移闸全绿。

Task 1 · Profile 类型 + Store

type Profile struct {
  Mode    string  // rule | global | direct
  Builtin Builtin // ChinaDirect / LanDirect / PrivateViaTunnel
  Rules   []Rule  // Type/Value/Action/Note/Enabled
  Final   string  // proxy | direct
}
// Default() 是 fail-safe 兜底,等价当前行为(智能分流+国内直连+无用户规则)
func (s *Store) Get(ctx, userID) (*Profile, error)   // 无档案 → nil,nil
func (s *Store) Upsert(ctx, userID, p) error          // dialect.Upsert(["user_id"], ...)

Task 4 · BuildClientConfig 翻译(核心)

在 main 版 route.rules 顺序基础上,私有域名(层3)之后、geoip-cn(层5)之前插入用户规则;提取纯函数便于测试:

func translateUserRules(p *routing.Profile) (
    rules []any, extraExclude []string, hasDomainDirect bool, geoSets []string)

Task 7 · 分流规则子屏 UI

对照视觉真源 design/prototype/screens/ui-mobile.html(data-sub="routing"):代理模式段选 → setMode;内置规则卡(国内直连开关 + LAN 锁定行);我的规则 ReorderableListView(拖动排序 + 动作 pill + 删除 + enabled 开关);「添加规则」弹层(类型 chip + 目标输入 + 动作段选,仿 account_screens.dart:209 _renameDialog);FINAL 行。冲突提示:被前面规则遮蔽的行灰化 +「已被上面规则覆盖」;命中系统锁定目标标「系统强制走隧道,此规则不生效」。桌面/移动双形态复用 SubScaffold + account_page.dart:47 open()

不在本轮(Phase 2/3)

执行方式(确认计划后二选一):A. Subagent 驱动(推荐)——每 Task 派新 subagent 实现 + 两段复核,任务间可叫停;B. 本会话内批量执行 + 检查点。