并 main:Apple 账号迁美国 + 签名单源/多账号 CI + 支付/连接/续期修复 #12

Merged
wangjia merged 12 commits from worktree-fix-sysext-network-ent into main 2026-09-07 00:40:19 +00:00
Owner

把 worktree 分支(18 commit,含历次发版切点)并回 main,让 main 追上、以后以 main 为发版线。

签名 / Apple 账号迁移(中国→美国 Yanmei AI LLC)

  • signing.env shell 单源 + gen-signing.mjs codegen 回填原生标识符 + drift 闸(baaf21d/3fb645e)
  • Team 44WULXM6SV / com.yanmeiai.pangolin;macOS 本地 build→签→公证 Accepted、iOS 装机均验证,零中国痕迹
  • CI 多账号 Phase 1:SIGNING_ACCOUNT 开关 + SIGN_US_* 账号密钥集(fea6f82)
  • 迁移 Runbook + 多账号 CI 设计文档(47c1e33/7be0eb4/acb9d80/5513377)

支付

  • 客户端:点购买即时跳转 + 首拍立即查 + 回前台补查 + 轮询 3s→2s(55f6b1a)
  • 服务端:查单对账开通 reconcile-on-read,付完不干等 webhook,幂等兜底(4a76134)

连接 / 服务端

  • 付费会话活跃即续期,根治 macOS 常驻隧道一天多掉线(721371d)
  • 断开连接前二次确认框(1cc5207) · l10n 补进 strings.json 单源(6618929)
  • 控制面 8080 收口默认绑 loopback(def3bb3) · 语言跟随系统+SSO basePath(bb031a0)
  • 品牌文案统一英文 Pangolin(e7e59a7) · macOS sysext entitlements 补网络权限(0ddcc5e)
把 worktree 分支(18 commit,含历次发版切点)并回 main,让 main 追上、以后以 main 为发版线。 ## 签名 / Apple 账号迁移(中国→美国 Yanmei AI LLC) - signing.env shell 单源 + gen-signing.mjs codegen 回填原生标识符 + drift 闸(baaf21d/3fb645e) - Team 44WULXM6SV / com.yanmeiai.pangolin;macOS 本地 build→签→公证 Accepted、iOS 装机均验证,零中国痕迹 - CI 多账号 Phase 1:SIGNING_ACCOUNT 开关 + SIGN_US_* 账号密钥集(fea6f82) - 迁移 Runbook + 多账号 CI 设计文档(47c1e33/7be0eb4/acb9d80/5513377) ## 支付 - 客户端:点购买即时跳转 + 首拍立即查 + 回前台补查 + 轮询 3s→2s(55f6b1a) - 服务端:查单对账开通 reconcile-on-read,付完不干等 webhook,幂等兜底(4a76134) ## 连接 / 服务端 - 付费会话活跃即续期,根治 macOS 常驻隧道一天多掉线(721371d) - 断开连接前二次确认框(1cc5207) · l10n 补进 strings.json 单源(6618929) - 控制面 8080 收口默认绑 loopback(def3bb3) · 语言跟随系统+SSO basePath(bb031a0) - 品牌文案统一英文 Pangolin(e7e59a7) · macOS sysext entitlements 补网络权限(0ddcc5e)
wangjia added 12 commits 2026-09-07 00:08:04 +00:00
连接键在 on 态点击不再直接断开:先弹危险操作确认框(告知连接已加密,
确认才 toggle 断开);off/connecting 态行为不变直接 toggle。
新增 l10n disconnectTitle/Body/Action(en/zh/es/ja/ko/ru 六语)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEVUXAbFT6bF1Qw27RHWoD
根因:付费连接凭证硬编码 24h TTL(paidCredentialTTL),而客户端只在 _connect()
(用户/看门狗前台重连)才重签,服务端从不因流量续期。macOS sysext(root)隧道独立于
GUI app 常驻,用户关窗后 Dart 看门狗根本不运行 → 凭证 24h 到期、下次 agent 重注册
用「未过期」快照整表覆盖并重渲染 sing-box → REALITY 会话被剔除、永久黑洞。该逻辑
四端共用同一份 Dart,故为全端共性(macOS 最易现形)。

修法(方案 A,服务端、与客户端生命周期无关,一改修四端):
- ReportUsage 收到某 dp_uuid 有流量,若属付费套餐(!AdGate)即把其凭证 expires_at
  顶到 now+PaidCredentialTTL。活跃会话永不过期;免费凭证 TTL 编码日额度、绝不续期
  (否则击穿日限)。每报按 user 缓存一次 entitlement 查询。
- 新增 NodeStore.RenewCredential(纯 UPDATE,WHERE expires_at>now 不复活已过期会话)。
- 24h 提为 nodes.PaidCredentialTTL 单一真相源,httpapi 引用它消除漂移。
- 纯 DB 续期,无需再 push agent(现有 REALITY 用户仍在,只要 DB 行不过期,下次
  重注册快照仍含它)。可移植 SQL(? 占位 + Go 端算时间,无 MySQL 专属构造)。

测试:handler 层付费续期/免费不续期(mock);store 层真 SQLite 续期/不复活已过期。
go test ./... 全绿、go vet 干净。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEVUXAbFT6bF1Qw27RHWoD
复盘反馈:①点「立即购买」有一点卡顿 ②付完刷新不及时、等了十几秒。

① 卡顿:_buy 原本 await createOrder 网络往返、返回才导航,期间购买页零反馈 →
   改为 start()(同步先置 creating)后立刻导航到支付页,用户点击即见页面切换+转圈
   (支付页已有 creating/failed/succeeded 全相位渲染);加 _buying 防连点(避免下双单)。

② 刷新慢(客户端可控部分):
   - _startPolling 首拍立即发(原 Timer.periodic 要干等一个周期才首查);
   - 新增 AppLifecycleListener,付款后从微信/支付宝返回 app 即刻补查一拍
     (等待付款期切后台,iOS 会挂起 Dart Timer,回前台首拍可能迟到十几秒——主因);
   - 轮询间隔 3s→2s。

测试:payment_flow 新增「start 立即补一拍→已到账不等周期直接 succeeded」,
共 7 个单测 + 支付页 widget 全绿;flutter analyze 干净。

注:十几秒还有服务端一半——GetOrder 的 activated 只认本地 row=paid(仅 webhook 落),
即便主动查 pay 网关已 paid 也不即时开通。reconcile-on-read(查单时网关已成功即幂等 settle)
是更大且触及结算逻辑的改动,另议。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEVUXAbFT6bF1Qw27RHWoD
「付完刷新等十几秒」的服务端主因:GetOrder 的 activated 只认本地 row=paid,而 row
只有 webhook 到账才翻;webhook 投递+退避重投正是那十几秒。GetOrder 其实早已调 pay
网关查单(st),却只当 pay_status 展示、没用来开通。

改法(仅 pangolin,pay 无需改):GetOrder 拿到网关 st 后,若 st.Status=="paid" 但本地
row 未 paid → 用 st 拼 webhookEvent 就地调 settle(复用 webhook 同一段幂等开通逻辑),
把「等 webhook」压成「付完下一拍查单即开通」。webhook 保留作兜底。

- Handler 加可选 settler(orderSettler 接口,*WebhookHandler 已满足)+ SetSettler;
  main.go 装配时 payHandler.SetSettler(payWebhook)。
- 幂等双保险:row.Status!="paid" 守卫 + settle 内锁行/已 paid 短路 → 重复轮询/并发查
  不二次开通、不二次发奖。settle 失败仅记日志,退回旧展示由下一拍或 webhook 兜。
- channel 查单不返回 → 回退 row.Method(仅影响渠道显示)。成功状态词 "paid" 已对齐
  pay 仓 model.OrderStatusV2,提为 payOrderPaid 常量并注释「改前对齐 pay 契约」。
- 信任模型一致:主动调 pay 自有接口(server→server,order_no 持有凭据),与现有把
  st.Status 当 pay_status 透传同源,非接收外部推送,不需 webhook 那样验签。

测试:新增端到端 sqlite——网关 paid→就地开通(activated + 台账翻 paid + 真授予订阅)
且重复轮询幂等(仍 1 条订阅);网关 pending→不开通。go test ./... 全绿、vet 干净。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEVUXAbFT6bF1Qw27RHWoD
完整迁移流程:把 Pangolin 的 Apple 签名从中国「岩美北京技术」(Team BYL4KQHMTN)
整体迁到美国「Yanmei AI LLC」,目标产物+仓库无中国账号痕迹。干净切割方案(全新
Bundle ID com.yanmeiai.* + 全新 Team/证书/描述文件/公证凭据,零依赖旧账号)。

含:命名总表、Apple 后台建资产逐步(App ID/App Group/两证书/四描述文件/ASC Key)、
本机钥匙串+公证凭据配置、仓库 Team ID/Bundle ID/App Group/实体名全量替换点清单、
gitea 12 个 Apple secret 换新、无痕验收清单、责任顺序表。已登记 docs/index.html。

代码替换待用户提供新 Team ID + 证书全名后单独一批执行。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEVUXAbFT6bF1Qw27RHWoD
迁移美国账号引出的诉求:Team ID/实体名/Bundle ID/App Group 原本散在三个签名脚本里
硬编码,迁一次账号要改多处易漏。抽成 scripts/signing.env 单一真相源,三脚本
(local_test.sh / ci/compile-macos.sh / ci/compile-ios.sh)source 它并派生各自常量。

signing.env(非机密,入 git)含:SIGNING_TEAM_ID / SIGNING_ENTITY / SIGNING_APP_BUNDLE_ID
/ SIGNING_EXT_BUNDLE_ID / SIGNING_{MACOS,IOS}_APP_GROUP + 派生的 Developer ID /
Apple Distribution 签名主体串。当前值已是美国 Yanmei AI LLC(Team 44WULXM6SV,
Bundle com.yanmeiai.pangolin);今后迁账号/改 Bundle ID 只改这一个文件。

bash -n 三脚本语法通过;source 冒烟各派生值正确。Xcode 侧单源(Signing.xcconfig)
另提交,由一致性闸比对防两处漂移。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEVUXAbFT6bF1Qw27RHWoD
断开确认功能(1cc5207)当时直接手改了 strings_*.dart(生成文件),没补进真源
design/i18n/strings.json。codegen drift 闸重生成时会按 strings.json 把这三键冲掉,
且 app_text.dart 仍声明抽象 getter → 生成物缺实现会编译失败。

补 disconnectTitle/Body/Action(en/zh/es/ja/ko/ru 六语)进 strings.json;重生成后
strings_*.dart 内容与已发布逐字一致(仅改由手改改成走单源)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEVUXAbFT6bF1Qw27RHWoD
承接 scripts/signing.env(shell 单源):Xcode/Swift 侧的 Team ID/Bundle ID/App Group
等无法 source 变量、只能字面量,故用 codegen 保持单源。

- 新增 scripts/gen-signing.mjs:从 signing.env 读 3 基值,用**结构化正则**(匹配
  `DEVELOPMENT_TEAM = X`/app group 形状等,不含任何旧字面值)回填 16 个原生文件的
  标识符;--check 为 drift 模式。因不含旧值,codegen 自身无痕、未来再迁账号仍生效。
- ci/check-codegen-drift.sh 追加签名 drift 闸(gen-signing.mjs --check)。
- 全量迁到美国身份:Team 44WULXM6SV / 实体 Yanmei AI LLC / Bundle com.yanmeiai.pangolin
  (+.PacketTunnel)/ App Group macOS 原生 44WULXM6SV.com.yanmeiai.pangolin、iOS
  group.com.yanmeiai.pangolin。清理注释/ORGANIZATIONNAME 等 Apple 侧残留痕迹。
- CLAUDE.md 同步新身份 + 指向 signing.env 单源。

验证:plutil lint 6 份 entitlements/plist 全 OK;macOS/iOS pbxproj `xcodebuild -list`
可解析、Runner build settings 解析为新 Team/Bundle;gen-signing.mjs --check 幂等 0 drift;
完整 codegen drift 闸(token/l10n/proto-i18n/signing)全绿。

注:未动 Android 包名 com.pangolin.pangolin_vpn(Play 身份、自签、非 Apple 账号痕迹,
改动影响大另议)与 Windows/桌面少量元数据串。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEVUXAbFT6bF1Qw27RHWoD
Team ID 已知(44WULXM6SV),§3 代码替换已落地并收敛为单一真相源:改写 §3 为
「signing.env + gen-signing.mjs codegen + drift 闸」,占位 <NEW_TEAM> 全替为真值,
顶部进度卡与 §6 责任表标注代码侧完成。剩余为 Apple 后台建资产/本机公证/gitea/验证。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEVUXAbFT6bF1Qw27RHWoD
实操验证补正:§1.2 原写 4 个 App ID 各勾 App Groups+Network Extensions,漏了主 app
的 System Extension 能力——缺它 macOS 主 app 的 Developer ID profile 不含
com.apple.developer.system-extension.install,构建报「profile doesn't include the
System Extension capability」。改为 2 个唯一 App ID(iOS/macOS 共用同串):主 app 需
App Groups+Network Extensions+System Extension,扩展需 App Groups+Network Extensions;
并说明改能力后 profile 变 Invalid 须重新生成。

背景:macOS Developer ID 签名链已本地端到端验证通过(build→重签→codesign 核验:
Yanmei AI LLC / 44WULXM6SV / com.yanmeiai.pangolin,零中国痕迹)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEVUXAbFT6bF1Qw27RHWoD
方案:CI 签名拆成正交两维——account(用哪个 Apple 账号,Developer ID/Apple
Distribution 证书 + ASC Key 跨该账号所有 App 共享)× app(bundle 专属描述文件)。
App 在自己仓 scripts/signing.env 声明 SIGNING_ACCOUNT=us|cn 一行完成切换;工作流
读它、三元映射从 gitea 用户级取对应账号 SIGN_<ACCT>_* 密钥集,描述文件走仓库级 per-app。
证书按账号存一次共享 + 描述文件按 App 存本仓 → 避免多账号×多App 的密钥爆炸。

含三层配置模型、gitea 命名规范、workflow 切换机制(基于现有 secret→env 映射层)、
composite action 多 App 复用、新增账号/App/切换的操作手册、Phase 0(pangolin 仓库级
快速解锁)/Phase 1(完整多账号模型)分期、待核实的 gitea 能力。已登记 docs/index.html。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEVUXAbFT6bF1Qw27RHWoD
按 docs/ci-multi-account-signing-design.html 落地:
- signing.env 加 SIGNING_ACCOUNT=us|cn(CI 多账号切换的唯一开关)。
- deploy-client.yml 的 macOS/iOS job 各加 "Resolve signing account" 步(从 signing.env
  读),证书/ASC 的 env 映射改为按账号三元选:us→SIGN_US_*(用户级)、cn→现有旧通用名
  (用户级,不动,供其他 app 继续用)。描述文件是 app 级(绑 bundle id)→ 仓库级,与账号无关,不变。

效果:pangolin(SIGNING_ACCOUNT=us)CI 用美国证书/ASC;其他 app 的独立 workflow 不受影响;
切账号只改 signing.env 一行。注:若 SIGNING_ACCOUNT=us 但 SIGN_US_* 未配,三元会回退到
旧(中国)名 → compile 脚本按 signing.env 的美国 Team 校验会失败(fail-safe,不会误签),
故 US 账号发版前须先配齐 SIGN_US_* 用户级 secret + 仓库级美国描述文件。

YAML 校验通过;codegen 无 drift(SIGNING_ACCOUNT 不参与 gen-signing)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEVUXAbFT6bF1Qw27RHWoD
wangjia merged commit 0a029dd222 into main 2026-09-07 00:40:19 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: wangjia/pangolin#12