Commit Graph

774 Commits

Author SHA1 Message Date
wangjia b255fdfb4e chore(ci): CI 校验迁本地(ci/check-local.sh),删 pipeline ci.yml
ci.yml 跑在自建 gitea 的 mac-pangolin-2 runner 上,该 runner 不稳:重 job
(go/flutter/golden/integration/e2e)频繁整体超时/失败(run 233、236 复现:所有
runs-on=mac 的 job 全挂、runs-on=ubuntu 的 nas 快扫描全过,证明是 mac runner
掉线而非代码)。既然开发就在 mac 上、原生工具齐全,把校验迁回本地。

- 新增 ci/check-local.sh:与 ci.yml 各 job 一一对应,dev mac 原生跑(仅 golden
  因 Linux 权威基线走 docker)。分档:默认=静态闸+go build/test+flutter analyze/test
  (各带覆盖率闸 Go30%/Flutter28%);--full 加 golden/go-integration/e2e;
  --only <名> 单跑;--list 列项。缺工具标 SKIP 不算失败。shellcheck-clean。
- 删 .gitea/workflows/ci.yml(部署流水线 deploy-server/client/site 保留,发版
  仍过 go test)。
- .githooks/pre-commit:秒级快闸不变,指引改指 ci/check-local.sh(原指 CI)。
- CLAUDE.md CI/CD 段重写 + ds-flow 闸表「CI」触发点改「check-local」。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-08-01 10:20:18 +08:00
wangjia 9aafb63461 chore(todo): 同步最新看板到 main(#24-#27 + #23→done)
Deploy Server / deploy-server (push) Successful in 4m7s
主 checkout 停在陈旧 feat/private-dest-acl,其工作区 todo 看板反而领先 main
4 条(#24 UI连接态脱钩 / #25 私有服务在家直连 / #26 MySQL Promo 并发防线 /
#27 分支审核 Minor 硬化)且 #23 已推进到 done。工作区是 main 的干净超集
(main 无任何独有条目),取工作区版覆盖回 main,零丢失。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
server-v1.1.1
2026-08-01 02:12:34 +08:00
wangjia 6f232d4043 fix(routing): 堵住 ip_cidr direct 旁路系统层 + 客户端保存失败可见提示
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 25s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 19s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 18s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Failing after 13m6s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 13m16s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 13m25s
ci-pangolin / Go — build + test (push) Failing after 13m35s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Failing after 13m45s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Failing after 13m54s
ci-pangolin / Flutter — analyze + test (push) Failing after 14m5s
ci-pangolin / OpenAPI Sync Check (push) Failing after 14m16s
ci-pangolin / Lint — shellcheck (push) Failing after 14m27s
分支审核发现两处 Important,合并前修复。

① [安全] direct 的 ip_cidr 用户规则可自伤式旁路整条隧道:
   Validate 原先只校 CIDR 语法。用户提交 ip_cidr=0.0.0.0/0 action=direct
   (或 172.16.0.0/12,含隧道 DNS 172.19.0.2)会并入 TUN 入站
   route_exclude_address(OS/auto_route 层,位于系统强制层之下),被排除的
   流量根本不进 sing-box → hijack-dns 与整条隧道被静默旁路,违反「系统层
   用户不可越」铁律。
   - Validate: direct 的 ip_cidr 拒绝 catch-all(/0)及与保留段 172.16.0.0/12
     重叠(写入闸)。
   - clientconfig 渲染层:新增 routing.SafeToExclude 守卫,只有安全的 direct
     ip_cidr 才并入 route_exclude_address(纵深防护,兜底写入闸之前的历史坏行)。
   - 测试 TestValidateDirectIPCIDRReservedGuard 钉死:拒 catch-all/隧道段重叠、
     放行 proxy catch-all 与不重叠 direct。

② [健壮性] 客户端保存失败静默回滚 + 抛未捕获异步异常 + 对话框无字段校验:
   _persist 失败会 rethrow(约定调用方 catch),但屏幕层所有回调
   (setMode/setBuiltin/addRule/removeRule/reorder/resetToDefault)均未 catch,
   规则闪现即消失、无提示,且 rethrow 变 zone 未处理异常。
   - 新增 _guardSave 守卫:await + 失败弹 SnackBar(AuthApiException 显服务端
     双语文案含校验错,其余回退通用「保存失败」),包裹全部变更类回调。
   - 添加规则对话框:_valueError 字段级预校验(ip_cidr 用 InternetAddress
     校验、geo 白名单仅 cn),非法即禁用保存并内联红字提示;语义级(保留段)
     仍由服务端权威判定经 SnackBar 呈现。
   - l10n 单源新增 routingSaveFailed / routingRuleValueInvalid(6 语),regen。

go test ./... 全绿;flutter analyze 无 error;flutter test 265 全过无 golden 回归;
codegen 幂等、原型 i18n 无漂移。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-08-01 00:32:52 +08:00
wangjia e157b16c11 fix(server): 走隧道/拒绝的域名规则也开 reverse_mapping
用户规则(可配置分流)里 action==proxy/reject 的域名规则,此前不触发
dns.reverse_mapping(只有 direct 域名规则触发)。而应用自行解析域名后按 IP
发起连接,路由层只剩 IP,无反向映射则 domain 规则永不命中——走隧道/拒绝的
域名规则会静默失效。

translateUserRules 把 hasDomainDirect 扩成 hasDomainRule:任意 action 的
域名类规则(domain/domain_suffix/domain_keyword)都置真、都开 reverse_mapping。
direct 专属的 ip_cidr→route_exclude_address(extraExclude)那条线不变。

这也是把私有服务分流(PANGOLIN_PRIVATE_SPLIT_DOMAINS)改用用户规则表达的
前置修复——否则一条"走隧道"用户规则替代 private-split 会连不上。

回归测试 TestBuildConfigProxyDomainEnablesReverseMapping:唯一一条走隧道/拒绝
域名规则(privateSplit 关、无 direct 域名规则)必须开 reverse_mapping。go test ./... 全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-31 01:15:34 +08:00
wangjia ce485a1b62 fix(ui): SegSwitch 用 FittedBox(scaleDown) 防窄容器截断文字
添加规则弹层的动作段选(Direct/Tunnel/Reject)在窄弹层里被 icon+文字挤到 ellipsis 截断
成「Dire…/Tun…/Reje…」。改:段内容包 FittedBox(scaleDown)——放得下原样(宽段选/goldens
不变),放不下整体等比缩放而非截断;padding 16→12 留余量。flutter test 265/265 无 golden 回归。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-29 08:42:27 +08:00
wangjia 49d0c5d2df docs(claude): 记 iOS 真机装机(local_test.sh ipad,老忘)+ CI 发版(tag触发+私有依赖鉴权)+ 可配置分流功能
- 移动端段加真机装机:API_URL=... local_test.sh ipad <设备>(公司分发证书/签名核验/xcurl 装机);
  iOS libbox gitignore 产物从主仓拷免重建;新设备 -allowProvisioningDeviceRegistration。
- CI/CD 段更新:tag 触发发版(server-v*/client-v*/site-v* → compile→test→release→deploy pangolin1
  备份+migrate+回滚+healthz),取代过时的「仅校验无部署」;私有依赖 github.com/wangjia/codes→gitea
  鉴权(GOPRIVATE+insteadOf,冷缓存 runner 必踩);发版排障(gitea actions API + rbw gitea 读写key)。
- server/ 加可配置分流小节:routing_profiles/GET-POST me/routing/BuildClientConfig 翻译(层级/三模式/
  IP直连 route_exclude 在 tunIn/域名直连 reverse_mapping/nil 逐字节不变)/system_locked_domains。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-29 08:23:42 +08:00
wangjia cd0ba919d1 fix(ci): deploy-server 编译前给私有依赖 github.com/wangjia/codes 配 gitea 鉴权
Deploy Server / deploy-server (push) Successful in 4m17s
冷缓存 runner 上 go build 拉私有 codes 依赖时,GOPROXY=goproxy.cn 对私有仓 404 →
回退 direct git 到 github.com → 无凭证失败(server-v1.1.0 首发在此挂,44s Compile 步红)。
加一步:GOPRIVATE + git insteadOf 把 github.com/wangjia/codes 重写到自建 gitea 并注入
FORGEJO_TOKEN(已验证 oauth2:<token> basic-auth 可 ls-remote 到 pin 的 commit)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
server-v1.1.0
2026-07-29 07:50:35 +08:00
wangjia a307c6ca2c fix(test): codes-lib 迁移往返测试改用 m.Migrate(21) 显式定位版本
Deploy Server / deploy-server (push) Failing after 41s
TestCodesLibMigrateRoundTrip 硬编码 6 次 Steps(-1),假设 000027 是迁移栈顶;本功能
加了 000028_routing_profiles 后 6 步落在版本 22、跳过了 000022 down,导致断言失败。
根因不是迁移 SQL(000022 up/down 正确、codes-lib 表无 FK——推翻了 FK 假设),而是
测试的脆弱步数计数被新增迁移打乱。改用 m.Migrate(21) 显式降到 000022 down 之后的
边界,不受栈顶新增迁移影响。全仓 go test ./... 恢复 0 FAIL。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-29 07:08:56 +08:00
wangjia 2930d76cf3 feat(routing): 域名级系统锁冲突提示 + 重置默认按钮 + 客户端 follow-up 清理
承接 FT-A(GET /v1/me/routing 含只读 system_locked_domains)。

- RoutingProfile 加只读 systemLockedDomains(fromJson 读/toJson 不输出);
  routing_screen 冲突检测扩展到域名类规则(domain/domain_suffix/domain_keyword
  命中锁定域名 → systemLocked),ip_cidr 私网启发式保留。
- RoutingProfile.defaults() + RoutingProfileNotifier.resetToDefault()(复用
  _persist:乐观更新/失败回滚/存成功后自动重连,保留只读 systemLockedDomains
  不丢)+ routing_screen 加「重置默认」按钮与二次确认弹层(新增 3 个 l10n 键)。
- RoutingRule.copyWith 用哨兵支持 note 显式清空为 null;RoutingRule/Builtin/
  RoutingProfile 加值相等 operator==/hashCode。
- T8 smartRouteSub 清理:grep 全仓发现 design/prototype/i18n/alias.json →
  gen_proto_i18n.mjs(CI 漂移闸)仍有活引用,按计划口径不删,详见
  .superpowers/sdd/task-FTB-report.md。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 06:57:58 +08:00
wangjia 3159dd75c1 refactor(routing): 暴露 system_locked_domains + 服务端 follow-up 清理(FK/Validate/dedup/exclude单源/共用Store/Builtin保留) 2026-07-29 06:38:52 +08:00
wangjia 52912268d0 docs(routing): 可配置分流 spec 状态 → Phase 1 已实现
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-28 09:34:35 +08:00
wangjia 4c76c2a701 feat(routing): 存档案后连接态自动重连使新规则生效
Task 9: RoutingProfileNotifier._persist() 保存成功后,若 connectionProvider.phase
== on,触发 ConnectionController.reapplyRoutingProfile()(disconnect→connect,
不改选中节点)重连,让服务端按新档案渲染的规则生效;off 态不触发。

新增 l10n 键 routingRulesReconnecting(单源 design/i18n/strings.json → codegen),
「规则已更新,正在重连…」区别于既有 nodeReconnecting(弱网抖动语义)。

reapplyRoutingProfile 内先置 _userDisconnect=true 再 disconnect,避免其触发的
kernel off 事件被 _onKernelStatus 误判「意外掉线」、再抢跑一次 watchdog 自动重连。
2026-07-28 09:17:32 +08:00
wangjia 86e5b6d451 feat(routing): 设置入口改分流规则下钻行 + 双形态导航
- settings_page.dart: smartRoute 布尔开关行 → 分流规则下钻行(标题 + 当前模式 pill
  + chevron),点击 desktop/tablet 切 navViewProvider 内容区下钻,mobile 全屏 push
  RoutingScreen(与 account_page.dart 的 open() 双形态导航同一范式)。
- navigation_provider.dart: 新增 NavView.routing + kSettingsSubViews 集合(登记
  settings 的下钻子页,供 shell backTarget 判断返回 settings 而非 account)。
- desktop_shell.dart/tablet_shell.dart: 内容区 switch 挂 RoutingScreen(embedded:
  true),backTarget 优先判 kSettingsSubViews → 回 settings。
- settings_ia_test.dart: 追加下钻行断言(标题+模式pill+点击导航到 RoutingScreen)。
- 重录 desktop_settings golden(唯一因本刀布局变化受影响的 golden)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 09:04:46 +08:00
wangjia d73b6da110 feat(routing): 分流规则子屏 UI(模式/内置/我的规则/添加/冲突提示)
- 新增 RoutingScreen(ConsumerWidget):代理模式段选(全局/智能/直连)→setMode、
  内置规则卡(国内直连开关 + LAN 强制锁定行)、我的规则 ReorderableListView
  (拖动排序→reorder、删除→removeRule)、添加规则弹层(类型 chip + 目标输入 +
  动作段选→addRule)、FINAL 兜底行。非智能分流模式「我的规则」区灰化+忽略提示。
- 冲突提示:①同 type+value 被更靠前规则遮蔽 →「已被上面规则覆盖」(纯本地计算);
  ②自定义 ip_cidr 规则落在私网/回环地址段 → 系统锁定提示(启发式,详见文件头注释
  ——真正的私有服务域名清单未经 API 下发给客户端,是已知缺口)。
- l10n 新增 9 个 key(routingTypeDomain/DomainKeyword/IpCidr/Geoip、
  routingRuleShadowed、routingModeNote、routingRuleValueHint、routingNoRules),
  经 design/i18n/strings.json 单源 + gen_l10n_dart.mjs 生成六语。
- 补生成 pangolin_icons.dart 的 plus 图标(design/prototype/icons.js 已有,
  codegen 之前未跑过)。
- 新增 widget 测试 test/widget/routing_screen_test.dart:模式段选/规则行/添加
  按钮可见、非智能模式灰化、重复规则遮蔽提示,共 3 例。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 08:51:40 +08:00
wangjia 057c11eee2 feat(routing): 客户端 model + AccountApi + AsyncNotifier provider
- RoutingProfile/RoutingRule/Builtin 手写 fromJson/toJson/copyWith,对齐服务端
  GET/POST /v1/me/routing 契约;enabled 缺省容错默认 true,toJson 恒输出该字段
- AccountApi.routingProfile()/saveRoutingProfile() 封装两端点
- RoutingProfileNotifier(AsyncNotifier):addRule/removeRule/updateRule/reorder/
  setMode/setBuiltin 均本地乐观更新后落盘;save() 失败整体回退到变更前 state
  并 rethrow(不静默吞,交调用方处理)——Riverpod asyncTransition 的 seamless
  copyWithPrevious 会用当前已存 state 覆盖手动挂的错误值,故不用 AsyncError
  路径,改走显式回退
2026-07-28 08:27:43 +08:00
wangjia 82b3988974 feat(routing): connect 读 per-user 档案传入渲染(fail-safe 回退默认) 2026-07-28 08:11:48 +08:00
wangjia f76aa56929 feat(routing): BuildClientConfig 翻译用户规则(层级/IP直连/域名直连/三模式)
ClientConfigOpts 加 Profile *routing.Profile(nil = 逐字节回退旧行为)。
translateUserRules 把 profile.Rules 翻译成 route.rules,插在系统层3
(私有域名)之后、国内分流之前:action→outbound、type→字段映射;IP 直连
并入 TUN 入站的 route_exclude_address(真正生效的字段,而非顶层 route
对象);域名直连开 dns.reverse_mapping;三模式(rule/global/direct)决定
route.final 与是否跳过用户规则层/国内分流层。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 08:01:10 +08:00
wangjia 49f62d9b14 feat(routing): GET/POST /v1/me/routing 端点 + openapi 2026-07-28 07:49:03 +08:00
wangjia 3a2c48541c docs(routing): 计划澄清 enabled 缺省语义(必填/客户端恒发/服务端不默认)
Task 2 复核发现 plan 内部不一致(接口注释 vs 代码块)。裁决:enabled 必填,Task 6 model
恒输出 enabled=true、fromJson 容错默认 true;服务端不修正缺省(缺失=禁用属 fail-safe)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-28 07:39:51 +08:00
wangjia b98aa2335b feat(routing): 档案校验 + 规范化(白名单/上限/去重) 2026-07-28 07:31:55 +08:00
wangjia 04b02da78d feat(routing): routing_profiles 表 + Profile 类型 + store(双方言) 2026-07-28 07:24:01 +08:00
wangjia c99a9b1106 Merge branch 'main' into feat/configurable-proxy
# Conflicts:
#	.gitignore
#	client/lib/l10n/strings_es.dart
#	client/lib/l10n/strings_ja.dart
#	client/lib/l10n/strings_ko.dart
#	client/lib/l10n/strings_ru.dart
#	docs/index.html
#	scripts/local_test.sh
2026-07-28 07:17:34 +08:00
wangjia 7776fc4d6c docs(routing): 可配置分流 Phase 1 实现计划(.md 执行版 + HTML 阅读版 + 登记索引)
Phase 1 计划:Task 0 合并 main(前置)+ 10 任务 TDD(服务端 routing_profiles 表/校验/
API/BuildClientConfig 翻译/connect 读档案;客户端 model+API+provider/规则子屏 UI/设置入口/
自动重连)。执行真相源 docs/superpowers/plans/2026-07-27-configurable-proxy-phase1.md,
阅读版 docs/configurable-proxy-plan.html 已登记 docs/index.html「实现计划」。待用户确认后执行。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-27 12:06:21 +08:00
wangjia fae73ff656 feat(proxy): 设置行模式 pill 反映当前分流模式(两端,随模式+语言联动)
设置页「Routing rules」行的模式 pill 之前静态显「智能分流/Smart routing」,不随实际
所选模式变。改为动态:
- pill 加 id=routeModePill,renderRouting 按 state.routeMode 设其 data-i18n(rr.modeGlobal/
  Smart/Direct)+ textContent(经 _ri 从 I18N 单源取)。切模式(seg 点击→renderRouting)即时更新。
- data-i18n 保留:切语言时 applyLang/applyI18n 用 pill 当前模式 key 重译 → 模式+语言双联动。
- pill 文案与 seg 一致(全局代理·Global / 智能分流·Smart / 全部直连·Direct)。

验收:两端语法通过、check-proto 全 resolve(rr.mode* 三态)、原型 --check 无漂移。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-27 11:37:12 +08:00
wangjia 64b1ec10c2 feat(i18n): 分流规则页静态标签接单源 + 修 mobile 设置行模式 pill 语言
路由页此前静态标签硬编码中文,EN 模式混语。全部接进 strings.json 单源:
- 新增 26 个 routing* 产品 getter(6 语):代理模式/内置规则/国内直连/我的规则/
  添加规则/从文本导入/重置默认/最终/直连·走隧道·拒绝/域名后缀/GeoSite 规则集/返回设置…
  (复用 smartRoute;seg 三项 EN 取短词 Global/Smart/Direct 适配段宽)。
- app_text.dart +26 抽象声明;重生成 6 份 strings_*.dart;alias.json desktop +24 / mobile +19。
- markup 加 data-i18n;JS pill(直连/走隧道/拒绝、FINAL)统一改从生成的 I18N 单源取(_ri),
  与静态 pill 同源。JS-owned 的模式说明 rtModeNote 保留 _t 双语(不加 data-i18n)。
- desktop LAN 行(此前未接)补接 rr.lan/rr.lanSub + 新 routingForcedPill。
- 修 mobile 设置行「Routing rules」的模式 pill:缺 data-i18n → EN 显中文,补 setSmartRoute
  (en=Smart routing,与 desktop 一致)。

验收:l10n 重生成+analyze 0 issue、原型 0 unresolved、生成器零 diff、244 测试全绿、无红线词。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-27 11:09:03 +08:00
wangjia a78364ebec fix(proxy): 添加规则「规则类型」改 chip 选择器(替原生 select)
原生 <select> 展开的是 OS 原生下拉(深色蓝高亮),浏览器不让改样式、与暖色设计
系统冲突(铁律 §14 禁原生默认外观)。改为设计一致的可选 chip 组:
域名后缀 / 域名 / IP-CIDR / GeoSite / GeoIP,选中=accent-subtle 底 + clay 字。两端同步。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-27 10:44:28 +08:00
wangjia 4a912b5af3 feat(proxy): 分流规则页交互 — 代理模式切换 + 添加/导入/重置(两端)
原型路由页此前段选与三按钮无事件、不重画。补齐(desktop + mobile 同步):
- 代理模式段选(全局代理/智能分流/全部直连)点击切换 + renderRouting 重画:
  非智能分流时「国内直连 + 我的规则」灰掉禁点、FINAL 兜底按模式变(全局/智能→走隧道、
  全部直连→直连)、模式说明随之更新。
- 添加规则→弹层表单(规则类型/目标/动作段选),提交插入规则行(首命中置顶)。
- 从文本导入→textarea,按「目标,动作」逐行解析插入。
- 重置默认→确认弹层,还原出厂规则表。
- 弹层内容 JS 双语渲染(state/S.lang),避免再造「EN 显中文」;复用现有 .dl-overlay/
  .rt-sheet + segswitch 范式;图标全取 icons.js 单源(refresh-cw/x/plus/download)。

注:路由页静态标签(段选/内置/FINAL 等)的整体 i18n 仍是独立后续(subagent 已标记的
19 个未接 rr.* 词汇 key)。本次聚焦交互,未改 I18N 生成块(--check 绿)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-27 10:35:49 +08:00
wangjia 75d23023e3 feat(i18n): ③ 原型接单源 — I18N 块由 strings.json 生成,消除重复翻译
原型 design/prototype/screens/{ui-desktop,ui-mobile}.html 的 I18N 数据块不再手写,
改由 strings.json 生成(marker 包裹、生成器唯一写):
- alias.json:protoKey → flutterKey(产品串取单源 zh/en,单源赢);desktop 130 / mobile 143。
- overlay.json:原型专属(开发挡板 + app 没有的串,zh/en);desktop 98 / mobile 104。
- new-strings.json:4 个新产品串(分流规则/自定义/局域网直连/强制),6 语,已并入
  strings.json + app_text.dart 抽象声明 + 重生成 6 份 strings_*.dart。
- gen_proto_i18n.mjs:strings.json+alias+overlay → 两份 HTML 的 I18N 块;--check 只比对
  marker 块(unrelated HTML 编辑不误报)。
- check-proto-i18n.mjs:两份 HTML 所有 data-i18n key 的 zh/en 必须 resolve。
- ci/check-codegen-drift.sh:加原型 i18n 段(--check 漂移 + resolution 验收)。

同带入之前路由原型 WIP 的 i18n 收口:私有服务内置行→局域网/私网直连行、rr.* 接 data-i18n
(rr.lan→routingLanDirect 等)。路由页其余 UI(段选/添加规则页)仍为独立后续。

验收(自复核全绿):生成器跑通、data-i18n 0 unresolved、两生成器重生成零 diff、
flutter analyze lib/l10n 0 issue、flutter test 244 全绿、new-strings 无红线词。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-27 09:59:18 +08:00
wangjia f46191ef4d feat(i18n): ③ 底座 — serve.mjs 暴露 canonical strings.json 给原型
原型与 Flutter app 共用同一份 design/i18n/strings.json(不在原型内复制):
- serve.mjs 加路由 /i18n/strings.json(及别名 /strings.json)→ 直读 ../i18n/strings.json。
- 原型 JS 可 fetch 同源翻译;完整 data-i18n key remap(3 套 key 空间对齐)为后续工作。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-27 08:57:47 +08:00
wangjia 13e0ba6d47 feat(i18n): ④ l10n 漂移闸 + codegen 改纯 node 确定性输出
- gen_l10n_dart.mjs 去掉 dart format:其输出依赖包解析状态、跨环境不稳(首次 4 空格、
  重跑 8 空格),会让漂移闸误报。改为确定性终态格式(list 单行 const [])。
  analyze 不校验格式,不影响;gen 现零 dart 依赖、纯 node 确定。
- ci/check-codegen-drift.sh 增 l10n 段:重生成 strings_*.dart → git diff 非空即 fail
  (与 token 段同法,纯 node)。
- 验证:往返无损、重生成零 diff(闸稳定)、flutter analyze 0 issue、244 测试全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-27 08:55:21 +08:00
wangjia f76e3cdffd feat(i18n): l10n codegen — strings.json 单源生成 6 份 strings_*.dart
② 单源 codegen 落地:
- strings.json 补全 lists 段(days7/featsFree/featsPro/featsTeam),
  之前 extraction 漏了 4 个 List<String> getter;现 246 getter+4 list+5 method×6 语。
- design/codegen/gen_l10n_dart.mjs:strings.json → strings_{zh,en,ja,ko,ru,es}.dart,
  getter 转义 \$、method 保留 $var 插值,dart format 定型(保证漂移闸零 diff)。
- app_text.dart 保持手写(enum/abstract 声明/AppLangMisc 逻辑),编译器强制 concrete⊆abstract 兜底。
- 往返无损:生成的 Dart 重新抽取 == strings.json 逐字节一致;flutter analyze 0 issue、flutter test 244 全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-27 08:47:44 +08:00
wangjia 9ab550d398 feat(i18n): l10n 单源地基 — 6 份手写 Dart 抽成 design/i18n/strings.json
i18n 单源第一步(用户要求:原型与实际程序必须一份数据)。
- 246 getter + 5 带参方法 × 6 语(zh/en/ja/ko/ru/es),抽取校验 0 缺失
- 抓到真漂移:strings_en.dart 原缺 quotaExhaustedNotice/openAlipayFailed
  两个 key(双引号+EN漏维护),单源后六语强制对齐
- migrate_l10n_to_json.py 一次性迁移脚本(单/双引号 + getter/带参方法)

下一步:codegen(JSON→app_text.dart+strings_*.dart,替换手写)+ 原型读同源 + CI 漂移闸。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 02:48:39 +08:00
wangjia f1292b757c design(configurable-proxy): 修图标(补 plus/私有服务用 shield)+ 桌面补齐 + 模式语义
- icons.js 补 'plus'(原缺失致「添加规则」+ 空);私有服务图标 server(不存在)→ shield
- 桌面 routing 视图:内置规则加图标框(与移动一致)+ 从文本导入/重置默认已在
- 移动 routing 子屏:补「从文本导入/重置默认」
- 两端加模式说明:用户规则仅「智能分流」生效;全局/直连忽略(系统层仍强制)
- 说明文档加三模式语义表 + 系统层解释

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 02:26:48 +08:00
wangjia 27b9f0965a design(prototype): 分流规则深度嵌入真实 ui-kit(桌面+移动屏)+ 登记设计系统
不再用单独页,直接嵌进真实交互原型:
- ui-desktop: Smart routing 开关行→分流规则入口→ data-view=routing 子视图(setView 接线+i18n)
- ui-mobile: 智能分流行→分流规则入口→ data-sub=routing 子屏(subOpen 接线+i18n+返回回设置)
- 复用现有 .pill 语义(直连 is-success/走隧道 is-warning/拒绝 is-danger),不造新原子
- index.html 登记「分流动作」胶囊场景
- 删单独 routing-rules.html;搬入配置说明文档
全走 v2 真原子,设计系统闸绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 02:14:36 +08:00
wangjia f10e887c2a design(configurable-proxy): Settings页衔接(Smart routing→分流规则入口)+ 默认/自定义规则冲突处理
- Settings 的 Smart routing 二元开关演进为「分流规则」入口行(显示模式+chevron)
- 冲突处理:系统层永远赢>用户规则(有序首命中)>智能分流>FINAL;
  UI 主动标记①被遮蔽永不命中②撞锁定系统目标 两类冲突
原型加 before/after 对照 + 冲突可视化例;说明文档加对应章节。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 01:57:38 +08:00
wangjia 51317e9f43 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>
2026-07-27 01:48:22 +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
client-v1.0.75
2026-07-26 12:44:36 +00:00
wangjia caaed88298 ci(android): gradle 发行版 + maven 依赖走国内镜像(修 #215 22min 挂)
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 20s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 17s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 16s
ci-pangolin / Lint — shellcheck (pull_request) Failing after 7s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 12m2s
ci-pangolin / Flutter — analyze + test (pull_request) Successful in 4m19s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Failing after 11m12s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Failing after 11m22s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 11m32s
ci-pangolin / Go — build + test (pull_request) Failing after 11m42s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Failing after 11m51s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Failing after 11m59s
#215 android 跑到 flutter build apk 的 Gradle 阶段挂了 ~17min:
"Gradle threw an error while downloading artifacts from the network"
"java.io.EOFException: SSL peer shut down incorrectly"(org.gradle.wrapper.Download)
——这是 sing-box 之外的第二个 GFW 网络坑:Gradle 要下载发行版和 maven 依赖,
走了被墙的 services.gradle.org / dl.google.com / maven central,SSL 被重置。

env 里配了 GOPROXY/PUB/FLUTTER 镜像,唯独 Gradle+Maven 没配。改:
- gradle-wrapper.properties: distributionUrl → 腾讯云 gradle 镜像(同一份 zip)。
- settings.gradle / build.gradle: repositories 前置阿里云 google/public/gradle-plugin
  镜像,原 google()/mavenCentral()/gradlePluginPortal() 保留作 fallback。

镜像已逐个验证:腾讯 gradle-8.7-bin.zip(206)、阿里云 AGP 8.6.0 pom(200)、
kotlin.android 2.2.0(200)均可取。全球通用,墙内不再依赖被墙源。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-26 11:15:14 +08:00
wangjia fc1160ecb3 ci(client): sing-box 源码统一走公网镜像 git.51yanmei.com(android/ios/macos)
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 24s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 20s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 18s
ci-pangolin / Lint — shellcheck (pull_request) Successful in 3m17s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 9m23s
ci-pangolin / Flutter — analyze + test (pull_request) Successful in 1m20s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 5m29s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 54s
ci-pangolin / Go — build + test (pull_request) Failing after 1m20s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 17m49s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Failing after 5m24s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Successful in 1m16s
android/ios/macos 三端都内嵌 libbox,编译期都要 git clone sing-box 源码。
此前只有 build-android 注入了 SINGBOX_GIT(且指向本地 relay 127.0.0.1:13000,
依赖 tailscale/LAN,Mac runner 出外网即失效),ios/macos 完全没注入 → 直接
clone github.com → 被 GFW/DNS 污染打挂(SSL_ERROR_SYSCALL,exit 128)。

统一改用稳定公网域名 git.51yanmei.com(ali frps ← 家里 NAS frpc,nginx 443
反代 gitea)——外网/内网都通,不依赖本地 relay/tailscale。已用真实 clone 命令
从外网验证 v1.13.13 可拉。ios/macos 同时补 GOPROXY(gomobile go install)。
build-windows 保持 LAN 镜像不动(Windows 台式机固定在家,不移动)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-26 10:36:08 +08:00
wangjia ee16cd3266 ci(android): libbox 从 NAS gitea 镜像取 sing-box 源码,绕开 github 不可达
Deploy Client / build-windows (push) Successful in 1m46s
Deploy Client / build-android (push) Failing after 22m36s
Deploy Client / release-deploy (push) Has been skipped
Deploy Client / build-macos (push) Failing after 3m40s
Deploy Client / build-ios (push) Failing after 1m16s
build-android 失败根因:build-libbox.sh 第一步 git clone github.com/SagerNet/
sing-box,而 mac runner(经 pangolin 隧道)连不上 github(SSL_ERROR_SYSCALL,
exit 128)。windows 成功是因为它从 NAS 镜像取 sing-box.exe、不碰 github。

修:build-libbox.sh clone 改用 $SINGBOX_GIT(默认仍 github,本地开发不受影响);
deploy-client.yml 的 build-android 设 SINGBOX_GIT=http://127.0.0.1:13000/wangjia/
sing-box.git,经 runner 的 gitea relay 取 NAS 上的 sing-box 镜像(已用 gitea
migrate 从 github 拉好,含 v1.13.13)。与 windows 的 DESKTOP_BIN_MIRROR 同思路,
CI 不再依赖 github。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
client-v1.0.74
2026-07-26 08:24:03 +08:00
wangjia e1e43adc80 fix(client-tun): macOS/iOS 应用 excludedRoutes,私有 LAN 真直连
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 26s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 20s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 21s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Successful in 2m5s
ci-pangolin / Go — build + test (push) Failing after 2m37s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 3m47s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Failing after 11m31s
ci-pangolin / Flutter — analyze + test (push) Failing after 11m39s
ci-pangolin / OpenAPI Sync Check (push) Failing after 11m50s
ci-pangolin / Lint — shellcheck (push) Failing after 12m1s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 6m57s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Failing after 10m29s
Deploy Client / build-windows (push) Successful in 1m51s
Deploy Client / build-android (push) Failing after 3m7s
Deploy Client / release-deploy (push) Has been skipped
Deploy Client / build-macos (push) Failing after 27s
Deploy Client / build-ios (push) Failing after 5m17s
上一版加了服务端 route_exclude_address,但 macOS/iOS 路由由 Swift 的
NEPacketTunnelNetworkSettings 控制,而 openTun 只设了 includedRoutes、
从不设 excludedRoutes → libbox 解析出的排除网段被丢弃,strict_route 仍把
LAN(192.168/16、10/8)抓进隧道,VPN 开着连不上局域网/NAS/家里机器/内网 gitea。

修:openTun 里 ipv4/ipv6 各补 excludedRoutes = excludedRoutes4/6(options),
读 libbox 的 getInet4/6RouteExcludeAddress(sing-box config 的 route_exclude_address
经此暴露)。镜像已有的 includedRoutes4/6 写法。

生效:客户端重新构建发版后,私有 LAN 直连恢复 → CI runner 不再需要经 ali 的
中继隧道(relay),从 Mac 也能直连家里所有内网机器。
Android(VpnService.excludeRoute,API33+ 分版本)作后续。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 07:17:09 +08:00
wangjia a3ad50aa75 fix(client-config): TUN 加 route_exclude_address 让私有 LAN 直连
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 27s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 20s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 20s
ci-pangolin / Flutter — analyze + test (push) Failing after 10m20s
ci-pangolin / OpenAPI Sync Check (push) Failing after 10m29s
ci-pangolin / Lint — shellcheck (push) Failing after 10m41s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 14m28s
ci-pangolin / Go — build + test (push) Failing after 14m37s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Failing after 14m49s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Failing after 14m57s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 19m19s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Failing after 12m37s
隧道开着时连不上局域网/NAS/家里机器(SSH/gitea 全 reset)。根因:TUN 入站
auto_route+strict_route 在 OS 层把所有流量(含 LAN)强抓进隧道,而 route.rules
里的 ip_cidr(192.168/16…)→direct 在 macOS 被 strict_route 抵消(direct 出站的
包又被捕回隧道)。

修法:TUN 入站加 route_exclude_address=[192.168.0.0/16, 10.0.0.0/8],在 auto_route
层就把这些网段排除出隧道,LAN 走系统直连。**刻意不含 172.16.0.0/12**——隧道自身
地址与 DNS(172.19.x)在此段,排除会断 DNS。route_exclude_address 为 sing-box
v1.13 合法 TUN 字段(option/tun.go)。

影响:VPN 不再黑洞局域网——直连 NAS/家里机器/内网 gitea 恢复,CI runner 也不再
需要经 ali 的中继隧道(relay)。客户端需完整重连拉新配置生效。

测试:TestBuildClientConfigLANExclude 断言 tun 含 192.168/16+10/8、不含 172.16/12;
go test ./internal/httpapi 全绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 01:53:35 +08:00
wangjia 5bc5ed1e62 ci: pangolin 的 runs-on 从 nas 统一改为 mac
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 29s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 21s
ci-pangolin / Lint — shellcheck (push) Successful in 49s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 22s
ci-pangolin / OpenAPI Sync Check (push) Successful in 10m53s
ci-pangolin / Flutter — analyze + test (push) Failing after 12s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 28s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Successful in 24s
ci-pangolin / Go — build + test (push) Failing after 46s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 36s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 5m15s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Successful in 1m3s
runner 命名/标签统一:mac-pangolin-2 是跑在 Mac 上的个人级 runner,原来挂
`nas` 标签(历史上按"理想归宿是 NAS Linux runner"预留),命名与实体不符、
易混。现把 runner 标签改为 `mac`(个人级不变,仍服务名下所有仓库),并把
pangolin 全部 workflow 的 `runs-on: nas` → `runs-on: mac` 对齐(ci.yml 9 个
job + 解释注释、deploy-client.yml 4 个 job)。与 jiu 的 `runs-on: mac` 统一,
一个个人级 mac runner 即可服务两边。ci-runner.md 标签引用同步更新。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 00:55:26 +08:00
wangjia 398e6e61cb Revert "ci(site): web 发版从 ubuntu-latest 迁到 nas runner"
This reverts commit 7e357ce.

原判断有误:以为 ubuntu-latest 无对应 runner。管理后台确认 gitea 有一个
**全局**在线 runner `nas-runner`(id 2, label ubuntu-latest),它服务所有仓库
(含 pangolin)——web 发版本来就有 runner、并未卡住。恢复 runs-on: ubuntu-latest,
用回这个专供 web/node 的全局 runner,而非把 web 挪到 Mac host runner。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 02:48:11 +08:00
wangjia 7e357ced0a ci(site): web 发版从 ubuntu-latest 迁到 nas runner
pangolin 只注册了一个 runner(mac-pangolin-2, label nas, host 模式);
deploy-site.yml 原 runs-on: ubuntu-latest 无对应 runner → site-v* 发版
永久排队,官网/用户中心走 CI 发不出来。

改 runs-on: nas,直接在宿主 Mac 上 host 跑(Mac 自带 node v25/npm)。
内存无忧:Astro 轻,Next.js next build 峰值 ~1-2GB,Mac 足够——刻意不放
内存紧的 pangolin1(960MB)或 NAS-Linux。wrangler pages deploy 需网络 +
CF token(CLOUDFLARE_API_TOKEN/ACCOUNT_ID secret 已配)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9G7E3wmAYL9KeYCVZVsqu
2026-07-25 00:44:42 +08:00
wangjia ea1a875607 Merge feat/private-dest-acl: 节点侧私有目的地 ACL(按 dp_uuid 放行家庭内网服务)
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 28s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 19s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 18s
ci-pangolin / OpenAPI Sync Check (push) Successful in 1m24s
ci-pangolin / Flutter — analyze + test (push) Successful in 1m0s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 27s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Successful in 29s
ci-pangolin / Go — build + test (push) Failing after 49s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 38s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 5m3s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Successful in 43s
ci-pangolin / Lint — shellcheck (push) Failing after 12m23s
pangolin 出口 IP 是家庭内网服务(brain/nas/git/win.51yanmei.com)在 ali 侧的
唯一准入凭据;单用户时=「只有我」,现在 5 个账号退化成「所有 pangolin 用户」。
本功能在节点 sing-box route 上按 dp_uuid 补一道 fail-closed 白名单闸,把语义
拉回「只有我」。改动收敛在 internal/agentd/,零 migration,不动控制面/客户端/DB。

合并动因:该分支代码已在 pangolin1 生产运行(线上 agent 二进制 vcs.revision=
e4b527b + auth_user 修复,昨晚经 SIGHUP 热渲染 auth_user 闸生效),但 main 缺失
——从 main 重建部署 agent 会静默移除这道安全闸。合并后 main 与生产对齐,并把
线上「脏树构建(vcs.modified=true)」规整成干净可追溯来源。

含 21 个提交:设计/计划文档、ACL 配置类型与 fail-closed 加载、渲染放行+拒绝
规则对、last-good 内存+落盘跨重启兜底、SIGHUP 热重载不踢人、ip_cidr/port 校验、
resolve 堵域名绕过、auth_user(VLESS 运行时只认 auth_user)、29 个单测 + 真机验收。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9G7E3wmAYL9KeYCVZVsqu
2026-07-25 00:20:53 +08:00
wangjia 1d154bd627 feat(admin): 后台登录支持「记住此设备」(免二次验证 + 保持登录)
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 25s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 20s
ci-pangolin / Lint — shellcheck (push) Successful in 51s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 23s
ci-pangolin / OpenAPI Sync Check (push) Successful in 1m10s
ci-pangolin / Flutter — analyze + test (push) Successful in 3m44s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 1m7s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Successful in 24s
ci-pangolin / Go — build + test (push) Failing after 1m0s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 40s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 6m9s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Successful in 42s
常用设备(已在 mTLS 白名单内)每次都要输 TOTP + 30 分钟就掉线,体验差。
新增登录页「记住此设备」勾选:

- 勾选并成功登录(需完整 密码+TOTP)后,签发 30 天设备信任令牌(HttpOnly/
  Secure/SameSite=Strict cookie,Redis 存储绑定 admin ID),并把会话延到 30 天
  (持久 cookie + 服务端 TTL,滑动续期按会话自身 TTL)。
- 之后该设备重登只需 用户名+密码,**跳过 TOTP**;会话在有效期内保持登录。

安全不变量(均有测试覆盖):
- 密码永远必验——即便持有效信任令牌,密码错一律拒(只跳过第二因子,不跳过密码);
- 信任令牌绑定 admin,alice 的令牌不能给 bob 免 TOTP;
- 无令牌 + 空 TOTP 一律拒(未记住设备仍强制二次验证);
- 令牌过期/Redis 清空/未知令牌全部 fail-closed 回退到「要 TOTP」;
- TrustedDeviceTTL=0 关闭整功能(勾选无效)。

实现:新增 TrustedStore(Redis, trusted.go);Authenticator.LoginDevice
(旧 Login 保持签名,委托新方法,零行为变化);SessionStore.CreateWithTTL +
Session.TTLSeconds 支持持久会话按自身 TTL 滑动;handler 读 cookie/勾选、
按 Persistent 设长短会话 cookie、下发信任 cookie;登录页加勾选、TOTP 去
required。配置项 ADMIN_TRUSTED_DEVICE_TTL(默认 720h)。

测试:trusted_test(签发/校验/绑定/吊销/过期/禁用)、login_device_test
(跳过TOTP/仍需密码/绑定admin/无令牌需TOTP)、login_device_handler_test
(端到端 勾选→双cookie→凭信任cookie免TOTP、无信任空TOTP 401);
go test ./internal/admin 全绿,go vet 净。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9G7E3wmAYL9KeYCVZVsqu
2026-07-24 09:52:56 +08:00
wangjia f819a77d83 fix(admin): 登录审计记录真实来源 IP(经本机反代时取 XFF 末跳)
管理后台经 caddy mTLS 网关(同机 loopback 反代 127.0.0.1:9444)对外后,
LoginSubmit 里 hostOnly(r.RemoteAddr) 恒为 127.0.0.1,admin_login_ok/fail
的 ip 字段失去溯源价值。

新增 realIP():仅当 TCP 对端是 loopback(请求确实来自本机可信反代)才信
X-Forwarded-For,且取**最后一跳**(Caddy 把真实 TCP 对端追加在末位,更早的
段可被客户端伪造预置);直连、XFF 缺失或非法值一律回退 TCP 对端。

IPAllow 白名单中间件刻意不变——安全闸继续只看 RemoteAddr,不受任何代理头
影响(维持原注释声明的边界)。

测试:TestRealIP 七例(直连/伪造头/单跳/多跳/缺失/非法/IPv6);
go test ./internal/admin 全绿。(internal/store 迁移彩排 3 例失败为 HEAD
既有——主仓干净树复现,与本次无关。)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9G7E3wmAYL9KeYCVZVsqu
2026-07-24 08:34:57 +08:00
wangjia 344dfe9a82 docs(agent): 记录 auth_user 血泪教训 + 客户端隧道前置 + 验收须真连接
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 07:39:39 +08:00
wangjia 2abe4d0816 fix(agent): ACL 放行用 auth_user 而非 user(VLESS 运行时只认 auth_user)
生产验证发现:sing-box 1.13 的 route rule 里 user 字段对 VLESS/REALITY
入站运行时不匹配(仅 sing-box check 语法通过),导致放行规则永不命中、
白名单用户也被兜底拒绝。本地真 VLESS 连接实测确认 auth_user 生效
(good→通/bad→block),node 端 git 一进一出白名单验证 per-user 生效。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:36:02 +08:00