Skip to content

1.19.0:首屏三段式 + 听译/朗读真机修复 + 诊断 L1/L2 + 验证基建(172 提交) - #535

Merged
belliedmonkey merged 93 commits into
mainfrom
feat/firstrun-gates
Oct 4, 2026
Merged

belliedmonkey merged 93 commits into
mainfrom
feat/firstrun-gates

Conversation

@belliedmonkey

@belliedmonkey belliedmonkey commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

这是什么

1.19.0 的全部改动(首屏三段式改版 + 听译/朗读真机多轮修复 + 诊断 L1/L2 + 验证基建 + 部署)。
分支 feat/firstrun-gates,领先 main 172 个提交(287 文件,+32,174 / −5,818 行)。

为什么是一条 PR:这 172 条是交错的(按主题拆要 cherry-pick 且反复踩门禁)。分类见下,
每条都能独立回读。

分类(172 条)

主题 条数 代表
App 首屏 / 登录 / 资源包 32 feat(app) 14 · fix(app) 14 · fix(packs) · fix(auth)
文档 / 规约 39 docs(design) 8 · docs(regress) 5 · docs(interaction) · docs(firstrun)
验证记录(TestFlight 批次 / ASC 回读) 22 docs(verification)
听译 / 朗读(真机多轮修复) 13 fix(listen) 5 · fix(tts) 4 · feat(tts) 4 · fix(stt)
部署(中继 / 中国机) 5 fix(deploy) 3 · chore(deploy)
诊断 L1/L2 3 4f0c4bdf L1 · 36465e49 L2 · a72b9ee2 披露
验证矩阵 / 合并 main / 其它 58 feat(verify) · test(...) · chore(release) · refactor(...)

落点:test/ 55 · scripts/ 19 · app/ 18 · src/app/ 16 · store-assets/ 15 · src/shared/ 15 · extension/learn/ 12 · extension/content/ 11 · docs/ 11 · build/ 11

本 PR 里最近落的三件(2026-10-06)

  1. 诊断 L1 + L2:设备侧环形日志(六类事件、字段白名单在 push 处强制)+ 自动上报到自有后端 write-only 信箱 bt_diag_events(INSERT only);排障走 npm run diag:latest / diag:dump。
    • 中国库表已通并端到端自查(46 条事件 + 版本号落地);东京库本次补齐(anon INSERT 201 / SELECT 401 验证通过)。
  2. 首页修订(2026-10-02 真机评审):规约 J14–J19 入库 + 缺陷① 卡高改常量(height 非 min-height)+ 标题单行。剩余三块(CTA 带 / 账号键 / 扩展引导行)见 首页修订(2026-10-02 真机评审):CTA 带 / 账号键 44×44 / 扩展引导行 —— 剩余三块(J14–J19) #540。
  3. 诊断上报的披露(上架前:诊断 L2 自动上报 vs 公开承诺(中国版隐私页 / AGENTS.md 规则 4)—— 措辞已认可,待改三处 #569):AGENTS.md 规则 4 改写(中国版不再「一个字节都不发」)+ docs/telemetry-design.md 新增「诊断上报 = 独立通道」一节 + 两站隐私页同版本改写(belliedmonkey-com 5d6fe2d / belliedmonkey-cc ad06441)。

门禁

npm test 2410 passed / 0 failed;test:app 两 flavor 绿。

上架前还挂着(都已开 issue)

belliedmonkey and others added 12 commits September 30, 2026 12:54
… / providers.gen.js MT_VERSION)

1.19.0 的用户可见内容三条半:系统翻译弹层字号三档(#503)· 登录态两处一致 +
读失败守卫(#502)· 额度自动领取守卫修复 + 回执说真话(#515)· 实时字幕过时文案
清理(#505)。构建面另有 ITMS-90332 根治(#501)。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <noreply@zhipuai.cn>
gen-changelog 同时按设计写了站点仓库的 ~/belliedmonkey-cc/changelog.html —— 那半随发版
(两站与 App 同版上线,release-checklist 的跨仓义务),不在本 commit 里。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <noreply@zhipuai.cn>
问题:1.19.0 分支从 main 切出,而 1.17.0/1.18.0 的发布说明只存在于各自的 release
分支 ⇒ 生成的 CHANGELOG(以及官网 changelog.html)从 1.19.0 直接跳到 1.16.0,
把两版从公开页面上删掉。这一次差点真的发生(生成器写出的站点文件是 14 增 44 删,
已还原)。

修法沿用既有做法(1.18.0 分支当年就带入了三份 1.17.0 说明):把 1.17.0 的三份与
1.18.0 的一份从各自 release 分支 checkout 进 1.19.0 树,再生成 CHANGELOG。
顺序要紧:**先生成、后提交** —— 生成器按「说明首次进本树的提交日」取日期,提交后
再跑会给 1.17/1.18 补上今天的日期,而线上页面本来就没有它们的日期。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <noreply@zhipuai.cn>
scope 裁定(#528)加做四项,发布说明三节各补四条:字体(Figtree 正文 +
Caprasimo 标题,仅拉丁)、键盘焦点环、注入层圆角与勾选号颜色、引导去时长承诺。
CHANGELOG.md 由 node scripts/gen-changelog.js 重生成(30 个版本,本版紧邻 1.18.0
不跳号);站点那份 changelog.html 按发布清单**只在发版那刻提交**,已还原。

(#528) (#529)

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <noreply@zhipuai.cn>
1.19.0 TestFlight 的第一条真机反馈:未登录首页太杂(设置链接 + 三个用不了的
功能入口 + 一句与登录卡自相矛盾的话),而两个设备包是懒加载 —— 新手路径无法
保证「配好了」。用户裁定三段式:屏1 登录(硬门)→ 屏2 资源包(硬门,无跳过)
→ 屏3 引导(可跳过),就绪 = 登录 ∧ 引擎 ∧ 识别包 ∧ 朗读包(不含扩展)。

本提交只落设计来源(不改产品代码):
- design/firstrun-3step/:画布 · 可点击原型 · 一页规格 · 品牌说明 · 板数据与样式
- design/firstrun-3step/README.md:六条裁定 + 规格里六条红线 → 我们要落成的门禁对照表
- 品牌已核验:生成的 CSS 用我们的九个 token(terra/sage/奶油/正文/危险 +
  Caprasimo/Figtree),未另发明一套

后续:文档先行(AGENTS.md 规则 2/3 窄化修订 · interaction-spec 首屏一节 ·
domain-design §9.2/§10 · learning-design §7.2/§8.10.1/§9.8)→ 门禁先红 → 代码
→ 中国版额度翻转 → 全矩阵 → 重打包 120/62/70/62。

(#532) (#528)

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <noreply@zhipuai.cn>
代码之前先改规约(AGENTS.md 的治理条:动交互要画布先行,且领域设计改动先过评审)。
画布与判据见 design/firstrun-3step/(#533),跟踪 #532。

- AGENTS.md 规则 2/3:日期化窄化修订(保留原文)。承重的两条不动 ——「不付费仍完整」与
  「免费路径不依赖我们的服务端」;被削的只是「**不登录也完整**」,且**只对 App 成立**
  (扩展仍免登录、自带 key 可用,免费路径的家没搬)。代价如实写:App 的未登录用户不再
  有完整产品。
- docs/interaction-spec.md:新增「App 首屏三段式(2026-10-01,#532)」—— 屏 1 登录(硬门)
  → 屏 2 资源包(硬门,无跳过)→ 屏 3 引导(可跳过);就绪 = 登录 ∧ 引擎 ∧ 识别包 ∧ 朗读包
  (不含扩展);老用户直进 / 缺包只出补齐屏;唯一降级(该语种不支持识别)。附六条红线。
  另给旧节「首次运行引导(App 侧)」加 2026-10-01 注(`signin` 已移出为屏 1),并把
  「登录永远不是墙」限定为「只对浏览器扩展成立」。
- docs/learning-design.md:§8.10.1 标「2026-09-30 用户裁定:通过」并写明 1.19.0 含中国版
  额度翻转(境内后端已在线,缺账本同机 + 中继 + 翻 ready);§9.1.1 记前置入口(屏 2 是硬门,
  原四处懒加载入口仍用于模型被清掉/换语言)。

npm test 2317 绿。未动代码。

(#532) (#533)

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <noreply@zhipuai.cn>
按「能变成门禁的就别留在清单里」:把 design/firstrun-3step/firstrun-spec.html 里
那六条红线的「怎么判红」写成 test/app-firstrun.test.js。没有 jsdom(domain-design
§10),所以 R1/R2/R6 打结构、R3 打纯函数真值表、R4/R5 打源码与文案。

现在 8 条全红,正是这一步的意义(本地红、不推送 —— 仓库规矩:不许推红套件)。
本提交只是把「红」固化下来,实现与文案在同一个分支里随后补上,绿了再推。

(#532)

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <noreply@zhipuai.cn>
追 test/app-firstrun.test.js 那六条红线(本地先红的那批):
- 新增 src/app/firstrun.js:纯函数 readyInputs()/isReady()/step()/degradeAllowed()
  —— 不引 Registry、不看 flavor、不碰扩展(R3/R3b/R3c/R4 已绿)
- AppShell 的 #signed-out 砍到只剩登录:删除「继续设置」卡、gear2、未登录复习块、
  local-note、以及三个用不了的功能入口(含 sfx="2" 那套)(R1/R6 已绿)
- shell-model 同步清理:paintObResume 加 null 守卫;paintSignedOutReview 与其监听退役
- 文案:12 语种共删 48 个死键(app_local_note 与 app_so_review_* 三条)—— R5 已绿
- test/growth-386.test.js 的那节改写为「#386 退役」(方向反转),并给源码判据加 stripComments

剩 1 红:R2(屏 2 · 资源包还没实现)。按仓库规矩红套件不推。

(#532)

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
`eq` 比的是恒等,`deepEq` 才做深比较(harness 两者都导出,src-boundaries 用的是
后者)。我在 app-firstrun 与 growth-386 两处写成 eq(数组, []),于是内容再对也红 ——
这类误报比漏报更贵(会把人引去改代码),所以在注释里写明。

现在套件 2306 绿 / 1 红:只剩 R2(屏 2 · 资源包还没实现)。

(#532)

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
AppShell 新增 #firstrun-packs:两行(识别语言包 / 高质量朗读包)+ 网络前提 +
错误行 + 一个填色按钮;**没有跳过出口**(R2 的判据)。

**这一版只是骨架**:`paintFirstRunPacks` 的接线(LearnTTS.deviceStatus /
NativeSpeech.probe 探测 · 下载触发 · 进度 · 重试 · degradeAllowed 降级)与它
自己的 12 语种文案还没写 —— 也就是说这一屏现在 inert。门绿 ≠ 功能可用,
这条差别写在这里,免得下一个人看到绿灯就以为它能用。

npm test 2307 绿 / 0 红(六条红线:R1 首屏只有登录 · R2 屏序与跳过口径 ·
R3/R3b/R3c 就绪判据 16 组真值表 + 判定顺序 + 唯一降级 · R4 两版同构 ·
R5 旧承诺防复活 · R6 一屏一填色)。

(#532)

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
把上一提交那个 inert 的骨架接成真的:
- paintFirstRun(session):挂在 show() 里(登录态的唯一汇合点)—— probePacks() 探
  LearnTTS.deviceStatus / NativeSpeech.probe,算四个输入 → firstrun.step() → 落屏;
  未就绪才显示屏 2,并把 #signed-out/#signed-in 覆盖掉
- runFirstRunPacks():#packs-go → 下朗读包(LearnTTS.ensureDeviceReady,与复习 ▶/
  播客/对话/设置试听同一个入口,§9.1.1)+ 识别语言包(NativeSpeech.ensureAssets('stt'))
  → 重新探 → 前进;失败留在原地并说明原因;唯一能绕过的是「该语种不支持识别」
  (degradeAllowed)⇒ 只下朗读包
- 10 个新键 × 12 语种
- 引擎判据按 §8.10.3 改对:**不用** EngineState.needsSetup(只看 apiKey 非空,
  会把残值 key 判成已配 —— test/grant-one-implementation.test.js 的门禁当场抓到),
  改为「额度在用 ∨ provider 能解析且配了 key/地址」

npm test 2307 绿 / 0 红;node build.js 干净。**未做真机验证**(下一步矩阵)。

(#532)

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <noreply@zhipuai.cn>
@belliedmonkey belliedmonkey changed the title feat(app): 首屏三段式 —— 屏 1 只剩登录 + firstrun 判定 + #386 退役 + 屏 2 骨架(六条红线全绿) feat(app): 首屏三段式 —— 屏 1 只剩登录 · 屏 2 资源包(已接线)· #386 退役(六条红线全绿) Sep 30, 2026
@belliedmonkey

Copy link
Copy Markdown
Owner Author

补充:这一版已接线(不再 inert),但未做真机验证 —— 保持草稿直到矩阵跑完。

2026-10-01 模拟器实测:全新安装的第一屏是「Safari 扩展还没打开」那张横幅(三步教程
+ 插图占满一屏),登录卡被挤出屏幕 —— 而首屏必须是登录。R1 当时只查 #signed-out
**里面**,所以没拦住:判据要从「那一段里」扩到「屏幕上」。

- paintExtBanner 加 firstRunActive:未登录 ⇒ 屏 1;firstRunScreen==='packs' ⇒ 屏 2。
  **不看 firstRunScreen 等于 'login'** —— 横幅由原生状态推送先画,那时它还是空串
  (第一版就是这么写的,实测不生效)。
- 新门 R1b 钉住这条(判据扩到屏幕上)。

另外两条实测教训(都记在注释里):
- App 资源改了要 **rm -rf derivedData** 再构建 —— 增量构建不重拷 Script.js,否则
  你验的是上一版(前两次截图都是旧包)。
- 别把 src/ 模块往 build/app-bundle.js 的 MODULES 里加:那是 ROOT 相对(app/ 指的是
  仓库那个 app/),src/app/* 由 esbuild 跟着 import 自己收。

(#532)

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <noreply@zhipuai.cn>
@belliedmonkey

Copy link
Copy Markdown
Owner Author

模拟器首轮观察(Release 干净构建)

  • 首屏不是空白(App:干净重建后在模拟器上整屏空白(挡着所有 App 侧真机验证) #536 是 Debug 专属;Release 正常),但出的是引导欢迎屏而不是屏 1(登录) —— 实现里的排序 bug:paintFirstRun 只管住屏 2,引导覆盖层仍按老逻辑首启就显示。
  • 门禁要补「出场顺序」一条(现有 R2 只查段存在/跳过口径,所以漏了 —— 与 R1 漏掉横幅是同一类:判据不够宽)。
  • 另:欢迎屏仍显示旧文案「两步,约 30 秒」,而本支已改成「两步就好」⇒ App 侧文案来源要查(ExtCopy.json vs _locales 的重建链)。

@belliedmonkey

Copy link
Copy Markdown
Owner Author

白屏真因已结案 + 屏 1 真机验证通过(详见 #536)

  • 真因:#532 从 AppShell.jsx 删掉「继续设置」卡(#ob-resume 一族)与 #gear2 之后,src/app/shell-model.js 还留着无条件访问 ⇒ 初始化期 TypeError: Cannot read properties of null (reading 'addEventListener') ⇒ 整壳启动中断 ⇒ 奶油底空白(页面与样式本身正常)。
  • 修:退役元素改「先取局部变量再判空」;卡内文字改为从 card 往下找;删掉 local-note / gear2 的死赋值与死监听。
  • 新门(test/app-shell-dom.test.js,4 条):静态核对「shell-model 引用的 id 必须在 App 页面源码里」+「退役元素不得 $('id').属性 直接取」。反向验证:套在 HEAD 版上当场点出当时那 7 处 ⇒ 这条门当时就抓得到。
  • 套件:2313 passed / 0 failed。
  • 真机(iOS 模拟器 · Release · 干净构建 · 全新安装):屏 1 正常 —— 只有登录(Apple / Google / 邮箱),无设置/功能/复习入口、无扩展横幅、单一填色按钮。
  • 另:本轮先前的「首屏是引导欢迎屏」「文案还是旧句」以及「Debug 白屏 / Release 正常」都是陈旧包造成的误判,已随之澄清。

belliedmonkey and others added 2 commits October 1, 2026 07:26
#532 把「继续设置」卡(#ob-resume 一族)与未登录首页的设置入口(#gear2)从
AppShell.jsx 删了,而 src/app/shell-model.js 里的无条件访问留着:初始化期
`$('ob-resume-go').addEventListener` 在 null 上抛 TypeError,整个壳的启动就此
中断 ⇒ 装机后是一屏奶油色空白(页面与样式本身都正常,WKWebView 日志里
Main.html 与 Script.js 都是 allowed = 1)。

- 修:退役元素先取局部变量并判空;卡内文字改从 card 往下找(querySelector);
  删掉 local-note / gear2 的死赋值与死监听。
- 门(新):test/app-shell-dom.test.js —— 静态核对「shell-model 引用的 id 必须在
  App 页面源码里」+「退役元素不得 $('id').属性 直接取」。反向验证:同样的规则套在
  HEAD 版上当场点出当时那 7 处 ⇒ 这条门当时就抓得到(套件当时全绿,因为 npm test
  跑在无 DOM 环境,看不见「元素没了而代码还在引用」)。
- 门:R2b 出场顺序(冷启动必须先进屏 1,引导不许抢在前面)+ 屏 2 完成这条新的
  引导进场路补上 §3.9 的 dwell 计时(这条是遥测门当场抓出来的)。
- 装前判据(新):npm run verify:app-fresh —— 逐字节比 .app 里的资源与 dist-app/。
  本轮两个假结论(「Debug 白屏 / Release 正常」「首屏是引导欢迎屏」)都来自装了一份
  更早的包:app:sync 的 exit 1 被 >/dev/null 吞掉,&& 链在构建之前就断了,
  而分号后面的 simctl install 照跑。判据与教训写进 verification-spec §0 与
  regression-tests §9.0.1。
- 真机:iOS 模拟器 Release 干净构建、全新安装 ⇒ 屏 1 正常(只有登录;无设置、
  无功能入口、无复习入口、无扩展横幅;每屏至多一个填色按钮)。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <noreply@zhipuai.cn>
模拟器实测(Release、干净构建、装机前过 md5 新鲜度判据)抓到并修掉:

1. **出场顺序**:屏 2 在场上时 `#systrans-banner`(iOS「翻译」App 三步 +「我已设好」)
   仍挂在同一页上方 —— R1b 只收掉了 Safari 扩展横幅,漏了这张同族的。三屏是**独占**的。
   修:paintSysBanner 的 away 并入首屏状态。门:R1c。
2. **引擎不再决定屏序**:原来 step() 把「引擎不可解析」也判到 'login',理由是「同一屏」;
   实测不是 —— 带会话的人落在那一支时登录卡不适用,实际露出的是**首页**,于是
   **屏 2 的硬门被整个跳过**(两个包一个都没下),而首页上没有任何东西能用。修:引擎只进
   isReady(),不进 step()。门:R3b(真值表对应那一行改了口径与理由)。
3. **领取额度移到首屏判定之前**:额度令牌晚一步落地,就会把刚登录的人读成「引擎不通」。门:R2c。
4. 朗读包那行不再留孤零零的分隔点(实测读到「离线模型未下载 · · 」)。
5. zh_CN / zh_TW 的 `listen_pack_missing` 少一个末字(其余语言完整)。
6. docs/interaction-spec.md 的三屏表与说明同步(引擎不参与屏序 + 额度时序)。

真机证据:屏 1(登录,独占)✓;屏 2(登录后缺包 ⇒ 资源包硬门、无跳过、单一填色按钮)✓。
仍阻塞(要裁定):屏 2 的下载在模拟器上**不返回** —— 15 分钟、无错误、容器里连 `mt-speech`
都没有;硬门可能挂住而没有任何出口。见 #532 的评论。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <noreply@zhipuai.cn>
@belliedmonkey

Copy link
Copy Markdown
Owner Author

屏 2 实机:渲染与硬门通过(P1 两轮绿);下载动作在模拟器上不返回(P2 红,产品问题)

  • 通过:屏 2 干净独占(系统翻译横幅已让位)、两行包状态、无跳过、单一填色按钮;屏 1 独占 ✓。
  • 本轮修 3 处:① 屏 2 上方漏挂 #systrans-banner(三屏不独占,门 R1c)② 引擎不再决定屏序
    (原来「引擎不通」判到屏 1,实际露出首页 ⇒ 屏 2 硬门被跳过;门 R3b)③ 领取额度移到首屏判定之前(门 R2c)。
  • 另修:朗读包那行的「· ·」空档;zh_CN/zh_TW listen_pack_missing 缺末字。
  • 阻塞:点「下载并继续」后 15 分钟不返回、无错误、容器里没有 mt-speech ⇒ 卡在朗读包下载那一步。
    设置页同一条下载是好的 ⇒ 强烈像屏 2 这条调用链的 bug。需要一次裁定:加超时出口(新降级理由)
    还是先定位(我建议先定位)。详见 App 首屏改版:登录(硬门)→ 资源包(硬门)→ 引导(可跳过);App 前提改为登录 + 中国版额度翻转 #532。

belliedmonkey and others added 2 commits October 1, 2026 08:49
按「先定位再定」的裁定定位到,全部是我这条链上的:
1. **没把引擎传进 ensureDeviceReady** —— 设置页/听译两处都传,我这条只靠 configure();
2. **返回值没接** —— 设置页一直是 `if (!r.ok) … '离线模型下载失败'`,我丢了返回值 ⇒
   失败静默(容器里没有 mt-speech、屏上一句话也没有),还接着走下一条;
3. **系统包没有上限** —— NativeSpeech.ensureAssets('stt') 只等原生事件,无超时 ⇒ 永远挂住
   (实测 15 分钟)。改为 Promise.race 120 s + 具名文案(新键 firstrun_packs_stt_slow ×12 语种),
   不绕过硬门,只给重试。

实测:P1 ✓(屏 2 干净独占);P2 ✓ 且结局是**具名「没动静」+ 可重试**(截图在证据目录),
不再是 2026-10-01 那个无出口的挂住。门:R3d。仍未答:朗读包那步没真下(容器里没有 mt-speech),
下一步一次探针(打印 MT_TTS_ENGINES.length 与 deviceStatus())。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <noreply@zhipuai.cn>
用户裁定:App 里用实时字幕/听译都需要设备引擎 ⇒ 这两个包必选。落实为:**探测与下载都钉在
设备引擎上**(`DEVICE_TTS_ENGINE = 'device'`),不看当前默认选的是哪个引擎。

起因(屏 2 上的探针实测):登录即领额度会把 TTS 引擎写成 `grant_speech`
(`DBG engines=7 id="grant_speech" device=false reason=not_device`)⇒ `deviceStatus` 判
`not_device` ⇒ `ensureDeviceReady` 以 `{ok:true, skipped:true}` 返回(额度引擎本来就没有设备包
要下),而硬门仍要求设备包 ⇒ **屏 2 永远过不去**(每个新用户都会撞上:登录即领额度)。

实测(iOS 模拟器 · Release · 干净构建 · 装机前过 verify:app-fresh):
- `testP1PacksGate` ✓ 屏 2 干净独占(无跳过、单一填色按钮、无横幅同屏)
- `testP2PacksAdvance` ✓ 且朗读包**真的下来了**:容器里 `mt-speech` = 157 MB、
  `piper-en` 带 sha256 安装戳;屏上那行随即变「已就绪」
- 识别包(系统包)在模拟器上 120 s 内无进展 ⇒ 具名「系统语音包下载没动静」+ 可重试
  (**不再是**无出口的挂住);真机那一步留给矩阵
- 门:R3d ④(探与下都必须钉在设备引擎上)

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <noreply@zhipuai.cn>
@belliedmonkey

Copy link
Copy Markdown
Owner Author

屏 2 走通(模拟器范围内)+ 裁定落实

  • 裁定:两个设备包必选(App 的实时字幕/听译要用设备引擎)⇒ 探测与下载都钉在设备引擎上,
    不看当前默认引擎。这修掉了「登录即领额度 ⇒ 引擎变 grant_speech ⇒ 硬门永远过不去」的死结。
  • 实测:P1 ✓(屏 2 干净独占);P2 ✓ 且朗读包真的下来了(容器 mt-speech 157 MB、带 sha256
    安装戳,屏上变「已就绪」);识别包(系统包)在模拟器 120 s 内无进展 ⇒ 具名「没动静」+ 可重试
    (不再是挂住),真机那步留矩阵。
  • 本轮另修:屏 2 漏挂的系统翻译横幅(门 R1c)、朗读包行的空分隔点、zh 文案缺字、
    ensureDeviceReady 三处(引擎要显式传 / 返回值要接 / 系统包要有上限,门 R3d)。

belliedmonkey and others added 7 commits October 1, 2026 12:10
- 进度与「地址不可用,换一个重试…」画在朗读包那一行上(与设置页同一个做法):包现在是硬门,
  而 global 的默认地址(GitHub)在国内可能先失败再换备用 —— 那几分钟不能只有一句「正在下载…」。
  门:R3d ⑤。
- **生产库改动(Tokyo)**:`bt_model_sources` 里 global 两行的 `url_alt` 由 `huggingface.co`
  改为 `hf-mirror.com`(同一 zip、同一 sha256,客户端一行未改)。理由:包自 1.19.0 起是首启
  硬门,而真机实测 GitHub 拉不动(HEAD 超时 / 下载停在 1%)⇒ 备用若也不可达,国内用户过不了
  屏 2(以前只是朗读降级)。已用 anon 回读确认(就是 App 读到的那份)。
  仓库侧记录在 `deploy/china/model-sources.sql` 的注释里 —— 那张表的 global 行不在仓库,
  是当年直接在东京用 SQL 建的。

实测(iOS 模拟器 · Release · 干净构建 · 装机前过 verify:app-fresh):P1 ✓ P2 ✓,朗读包仍真装
(容器 mt-speech 157 MB)。进度文案已接上并入门(R3d ⑤),但**未**抓到下载中途的行上进度截图。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <noreply@zhipuai.cn>
**背景**:1.19.0 的范围含「中国版额度翻转」,但 `deploy/china-relay/README.md` 的前置表写得很清楚:
**运行处(境内地域的云函数或境内机器)、百炼 key、按实价填 GRP 价格、境内网络下回读 `/spec`**
—— 四件都是外部事,一件没好就不许翻 `grant.china.ready`。本轮把仓库侧缺的补齐:

1. **`deploy/china/grants.sql`(新)**:免费额度的台账(`bt_grants` / `bt_grant_usage` + 四个函数),
   形状照抄 `supabase/schema.sql`(客户端协议一个字节不改),含回读断言(2 表 + 4 函数,少一个就红)。
   此前境内部署包里**没有**这张表 —— 而中继的 `/check`、`/charge` 与领取都要落点,翻 ready 前必须先跑它。
2. **「账本在东京」的说法改真**(`bt-relay/index.ts` 注释 + `deploy/china-relay/README.md`):
   2026-09-22 的裁定是**账本与账号同库**;代码本来就是对的(`/claim` 代转打 `SUPABASE_URL`,
   境内部署时即境内那台),只有注释与 README 还停在旧设计上。env 示例同步改成境内地址 + 境内那把钥匙。
3. **`deploy/china/README.md`**:把 `grants.sql` 写进建库清单(与 `model-sources.sql` 同一个先例)。
4. **文案改真(12 语种)**:`grant_cn_why` 原写「那需要一台境内的服务器,还没就绪」——
   **境内服务器早已上线**,真实原因是代领中继还没开通。改为按事实说。

未验证(如实记):`grants.sql` 没有对着活的 Postgres 跑过(本机没有该库;文件末尾的回读断言会在
部署时当场判),中国版合规门已重跑 ✓。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <noreply@zhipuai.cn>
境内后端 2026-09-22 已上线(#405),README 里的占位换成实际地址;账本同库那条裁定的
env 示例现在可以直接照着填。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
境内后端此前只挂了 bt-delete-account 一个函数 ⇒ `/functions/v1/bt-grant` 与
`/functions/v1/bt-grant-ledger` 落不到任何实现(我先前探到的 401 是那个删号函数自己回的,
不是「额度端点已在位」—— 这个误判已在 #532 纠正)。翻 grant.china.ready 之前必须有它们:

- compose:新增 `grant`(领取,需 GRANT_KEK / GRANT_LIMIT_USD / GRANT_DAILY_CAP)与
  `ledger`(窄口,需 LEDGER_KEY),与 `fn` 同构(各挂一个函数目录、只听默认 8000)。
- Caddyfile:两条具名路由写在 `handle_path /functions/v1/*` 兜底**之前**(handle 按序匹配);
  用 `handle` 不用 `handle_path` —— 函数自己会把路径截到 `/bt-grant-ledger` 之后。
- README:第 6 节写清生成两个值的命令与回读判据(期望 [])。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
**境内侧(服务器 lhins-6amaoj8m,经 TAT 操作)**
- 新增 `grant` / `ledger` 两个 compose 服务(挂 supabase/functions/bt-grant、bt-grant-ledger);
  Caddyfile 加两条具名路由(写在兜底之前),并给**内部入口 :8081** 补 `handle_path /rest/v1/*`
  —— 原来它只挂 /auth/v1/*,两个额度函数打 RPC 会拿到空体 404。
- 把 `deploy/china/grants.sql` 应用到境内库(幂等,自带回读断言);补 `grant execute ... to service_role`
  —— 撤了 PUBLIC 之后漏这四行就是 403 permission denied(中继表现为 500)。
- `.env` 现生成 GRANT_KEK / LEDGER_KEY。

**云函数(腾讯云 Web 函数,ap-beijing,函数 URL)**
- 打包(build-scf.sh)→ 建函数 → 开函数 URL(**API 网关触发器已停止售卖**,改走函数 URL)
  → 环境变量:两个地址指境内后端、UPSTREAM=dashscope + 百炼 key、CLAIM_PROXY=1、
  GRANT_MODELS={"chat":"qwen-plus"}、GRANT_PRICES(百炼实价的混合价,待复核)。

**回读(契约 §2 四条 + 境内延迟)**
- 窄口 /check → `[]` 200 ✓;/charge → `null` 200 ✓;错钥匙 → 403 ✓
- /claim(无令牌)→ 401 session ✓;中继带假令牌 → **401 grant_invalid** ✓
- 既有链路未受影响:删号 401 ✓、登录预检 204 ✓
- 境内(北京那台)延迟:/spec 五次 0.07–0.16 s(判据 <1 s)✓、/claim 0.28 s、窄口一跳 0.085 s

**仓库侧**
- backend.config.js:`grant.china = { ready: true, relayUrl: <函数 URL>, vendor: 'dashscope' }`;
  中国版产物里 `MT_GRANT.claimUrl` 指向它、`grantOnly` 条目进中国版注册表(合规门绿)。
- 同步的旧口径:backend-config 的「主机名只写一处」判据改成按 flavor 分(中国版那条主机名
  同样只在 backend.config.js 写一次);性能台账加一行 `reachable`(中继 host,写明参数没扫过)。
- deploy/china/README.md §6 记下这五处坑;Caddyfile / grants.sql 各自带注释。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
中国版额度翻转上线后,把**现在真实在跑的东西**画成一张图:客户端三条出口、
两个 flavor 各自的额度链路(东京 vs 境内云函数 + 账本同库)、实际部署与回读判据
(境内 6 个容器 · 云函数四条回读 + 境内延迟 0.07–0.16 s)、App 首屏三段式与两个设备包的
来源、数据边界上的六条承诺。自包含 HTML,无外部依赖。

Open Design 侧:项目 app-firstrun-3step → 产物 architecture-2026-10-01.html。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
第一版把数据路径画清了,但部署拓扑没分家。这一版按用户要求重画:两块并列的部署图
—— 国际(东京 Supabase 项目:GoTrue/PostgREST/五个 Edge Function/七张表 + 额度上游 OpenRouter)
与中国(北京轻量服务器:Caddy + 六个容器 + 境内库同库账本;云函数 bt-relay → 百炼 qwen-plus),
中间一条「两侧没有任何运行时连接」的边界带(三条可验证判据)。

分发状态取自 gbrain 权威页(2026-09-30 实测:iOS 国际 b117 / 中国 b59 / macOS 中国 b59 已上架、
macOS 国际 b68 在审、CWS 与 AMO 1.18.0、两站 1.18.0)+ 当日现场复验(itunes lookup、AMO API)。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
belliedmonkey and others added 25 commits October 3, 2026 21:00
问题(用户反馈):播客模式的段落朗读「基本没有断句、朗读效果不好」。查下来不是模型的锅 ——
`driving-model.js` 把**整张卡**一次交给 `LearnTTS.speak(text, lang)`,而 `extension/learn/tts.js`
的 `speak()` 里没有任何切分。合成器没有句间停顿的依据,再好的声音读一条长文本也会是平的。

解决:
- 新增 `src/app/speech-chunks.js`(纯逻辑):`speechChunks(text, lang)` 复用 learn 层现成的
  `LearnModel.splitSentences`(Intl.Segmenter + 引注修补;卡片 / 对齐 / 文档同一套规则),
  拿不到它时回落标点正则(与 learn/doc-core.js 同形)。
- `driving-model.js`:播放(execSpeak)逐句念、句间留 `SPEECH_GAP_MS`(220 ms);
  **预热(warmCard)枚举同一批文本** —— 本文件原本就写着那条警告(「否则预热的是另一段话,
  缓存永远打不中,用户还多付一次钱」),所以切分只能有一个来源,两处都调它。

只动音频:屏上仍按现在的高亮走,没有新的屏上行为(「读到哪句高亮哪句」是另一种改动,另走设计稿)。

放 src/app/ 而不是 src/shared/:**播客模式是 app-only**,而 src/shared 有 §9.4 对账门要求
两宿主都挂(这一版先踩到了它)。与 firstrun.js 同类:纯逻辑、可单测。

回归:test/speech-chunks.test.js(8 条)—— 空输入不合成、单句原样、透传语言给 Segmenter、
无 LearnModel 时回落仍断句(英文与 CJK)、LearnModel 抛错/回空都不吞整段、**所有块拼起来
不许丢字**、句间停顿是正数。全量 2365 passed, 0 failed。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
用户裁定:「临时加个泰国语进来吧。所有多语言的地方都支持泰国语。」

加一门语言**没有捷径** —— 三道门把它钉死成全量:engine-fields 要求注册表里
每一门在磁盘上都有 messages.json;i18n-parity 要求键集与 en 逐字相同(空串=缺键);
i18n-quality 要求非拉丁语系有字形覆盖。⇒ 1193 条泰文,绕不过去。

注册表 build/ui-langs.config.js 加一门,四个消费面跟着走:
- extension/_locales/th/messages.json —— 1193 条(机翻,待人工润色)
- 两个界面语言选择器(options.jsx / settings-view.jsx)
- 「译成」:target-langs.config.js + options/popup/settings-view + translation-api.js 的 LANG_NAMES
- build/descriptions.china.js 的泰文中国版描述(china 构建缺一门直接失败;103 字符 < 112)
- scripts/sync-app-assets.js 的 PLIST_L10N 三句权限说明 + SERVICES_L10N 菜单名
- build/try-pages.config.js + quick-setup.js 的 TRY_LANGS(试翻页三处一致)
- README 的「_locales/ 12 → 13」;i18n-quality 的 SCRIPTS 加 th
- build.js:同步注入面基线按门禁自己的出口重钉 3381059 → 3756331(泰语 +336 KB:
  生成表里 th 段 168107 字节 × 同步清单里出现两次;加它之前 3420117 本在预算内)

没动的两面各有理由:学习层的「可学习语言」(学习域,改它要过 domain-design 复核)
与商店素材(营销面)。

全量 2366 passed, 0 failed;global/china/firefox 三个 flavor 构建全过。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
用户裁定「加」,把上一版**故意留着不动**的那一面补上:build/langs.config.js 加
{ code:'th', labelKey:'lang_th', label:'ไทย', scripts:['Thai'] },13 份 _locales
各补一条 lang_th(endonym 的既定豁免:各语种都是「ไทย」)。

本段的三件事值得记:
- 判定链的两端**早就认得泰语** —— learn-rules.js 有 ['Thai', /\p{Script=Thai}/u]、
  guessLang 会猜 'th';缺的一直只是白名单这一格(不列举 = 默认「不可学」)。
- 泰文**不与别的语言共享脚本**,所以 §4.1 那条「known looseness」(Han/Latin/Kana
  互相放行)在泰语这里不适用:有 th 就收、没有就不收,没有中间态。
- docs/learning-design.md §4.1 同版更新(治理规则要求:动学习层先更文档)。

全量 2366 passed, 0 failed;构建绿。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
#558 高质量语音(用户裁定「把新版高质量语音做完」)
- app/device-models.config.js:zh/en 两条共用同一个 dir 与同一个包(140 MB,一个引擎覆盖中英);
  新增 type/voices/dictDir/lexicon/kokoroLang。
- app/native/speech-bridge.swift:按 type 分支构造 Kokoro 配置;**引擎缓存封顶 1** ——
  int8 一个引擎峰值 RSS ≈460 MB,中英同时驻留 ≈0.9 GB 会被 jetsam 杀。vits 分支保留 ⇒ 回滚改一行。
- scripts/pack-device-models.js:kokoro 前缀打全套(含三本词典、三个中文 fst、dict/、LICENSE)。
- scripts/verify-model-urls.js(新):HEAD + 范围 GET 核对 url 与 size —— 包是首启硬门,下不动就卡屏 2。
- deploy/china/model-sources.sql:中国版两行 piper → 一行 kokoro。
- test/registry.test.js:Kokoro 四字段 + 中英共用 dir/包 + **Swift 真的按 type 分支**。

#557 iOS 装不上(真机上才发现)
- scripts/sync-app-assets.js:bundlePrefix 的 `.extension` strip 改大小写不敏感 ——
  新版 converter 的 target 叫 `Extension`(大写),前缀因此带上 `.Extension`,
  拼出 `…translator.Extension.MTTranslateExt`(父前缀之后带点 ⇒ iOS 拒装)。
- scripts/verify-ios-bundle.js:新增 checkExtensionIds(),读**装出来的包**守这条
  (safari-project*/ 是本机态,CI 里没有工程可查)。

托管:魔搭 + HuggingFace 均已上传(sha256 5bab4f62…、size 146767700),verify:model-urls 4/4 ✓。

验证:npm test 2367 passed 0 failed;test:layout 44/44;verify:model-urls ✓;
iOS 模拟器首启真下载该包 → 装载 → 进首页(截图在 .local/regress-1.19.0/thai/sim-iphone)。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
问题:把界面语言选成泰语后,options 页**整页泰文**(3155 个泰文字符),
但「一键设置」那张卡仍是成句中文(本机 Chrome 界面语言是中文 ⇒ chrome.i18n 的兜底)。

定位(在页面里量的,不是猜):
- `MT_I18N_MESSAGES` 表在、`th` 在、`MT_I18N_MESSAGES.th.qs_sub` 是泰文;`chrome.storage` 里 `uiLang='th'`;
- 而 `chrome.i18n.getMessage('qs_sub')` 是中文 ⇒ 那张卡的 `t()` **没走 bundled 表**。

根因:这批命令式孤岛的挂载点是无依赖数组的 `useLayoutEffect` + `quickMountedRef` 哨兵
⇒ **只画一次**,而且画在 `PageText.setUiLang` 落地之前。引擎字段那批孤岛的 deps 里
本来就有 `uiLangNow`(见「四槽挂载」注释:换语言全部重画),只有这张卡漏了。

修法:把哨兵从布尔改成**记住这次画用的是哪个界面语言**,语言一变就重画。
代价:换语言会清掉卡里未保存的输入 —— 低频动作,且与「重画」本来就是同一件事。

验证:`node .local/chrome-thai-check.js`(CDP 装未打包扩展 → uiLang=th → reload)
从「81 个可疑汉字」变 0 ✓,泰文字符 2872 → 3155。npm test 2367/0、test:layout 44/44。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
一次没跑完的矩阵,按 device-verification.md 的惯例逐行记结果与证据:
iPhone/iPad 模拟器、macOS host app、macOS Chrome 四行绿;
macOS Safari 未跑(Safari 实例被另一个会话占着,按规矩不抢);
Firefox 红(options 页空白,issue #560)。
另记两条未验证:试听(引擎装载+合成)与音质。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
之前那条"iOS 模拟器到不了设置页"是判断;现在从 xcresult 里读回了断言原文:
屏上停在首启包屏(朗读包 พร้อมแล้ว、识别包未下载)⇒ 没进设置页。
另记两条读证据的口径坑:run-ob.sh 里那句 xcresulttool 在新版 Xcode 下
"没报错但也没值"(被 2>/dev/null || echo 0 吞掉);zsh 不对未加引号的变量分词。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
上一条我写"run-ob.sh 里那句 xcresulttool 一直静默失败、回落到 0"——**是错的**:
那条命令语法在 Xcode 27 里没变,脚本是 bash 且显式传参,它工作正常。
真实原因是我在 zsh 里用未加引号的 `$v` 展开子命令:zsh 不做词分割 ⇒ 子命令被拼成
一个参数 ⇒ 报 `Unknown option '--path'`。结论按仓库的规矩就地更正,不留错的判断在文档里。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
#560 结论:不是产品缺陷。web-ext 先起浏览器后装扩展,--start-url 落在没人认领的
moz-extension:// URL 上 ⇒ 白屏且此后一直白(连静态 manifest.json 都取不到,极具误导性)。
正确做法写进 verification-spec §2.E:先 about:blank,等 Installed,再地址栏导航。
顺带记下 Firefox 的清单规矩:_locales/ 存在时 default_locale 必填。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
- Firefox:#560 关掉后重测 —— 等 Installed 再用地址栏导航,页面完全正常(含 13 个学习语言 chip 里的 ไทย)。
- macOS Safari:扩展已装但未启用(勾选 value=0),cua-driver 点击不生效 ⇒ 启用这一步没做成。
- 试听:macOS App 设置页里根本没有那一行(AX 369 元素里只有语音模式/自动朗读/每日新卡),滚动不改树。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
- 引擎装载+合成:**已验证**。macOS App 播客模式出现「การ์ดที่ 3 จาก 4」+
  「รอบที่ 1 · กำลังเล่นต้นฉบับ…」⇒ 引擎装载成功并真的出声(配置错只会显示 TTS 失败)。
  设置页那条路走不通:那一行在 macOS App 的设置页里不渲染(AX 369 元素里没有)。
- macOS Safari:扩展已装未启用,勾选 value=0;cua-driver 的 AX 点击被拒、坐标点击 unverifiable
  ⇒ 启用这一步我解不开,写清楚给下一位(或请用户手点一次)。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
飞行窗监督裁定(2026-10-03):
- macOS Safari 那一行需要人手勾选扩展(飞行窗内无人可点),而同一扩展在 Chrome 与 Firefox
  两行已绿、三个宿主面(iPhone/iPad/macOS App)亦绿 ⇒ 按**人工阻塞**记账,**不挡 TestFlight**。
- 音质 A/B 属主观听判 ⇒ 留给落地后;样本脚本与回滚路径(vits 分支仍在)都写在这里。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
(插入时把 '### E. Firefox desktop' 与它的 '(real Mac, throwaway profile) — ✅ verified' 分开了,
现在标题完整、引用块在其后。)

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
国际 iOS 133 / 国际 macOS 81 / 中国 iOS 76 / 中国 macOS 74,四条 ASC 回读均 VALID
(08:10 / 08:12 / 08:21 / 08:18)。号码在跑之前从 ASC 回读上一批确定。
全程只 altool 上传、未做任何 bind/submit。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
用户报「连不上服务器又出来了」,并且让我查中国数据库。上机复查(troubleshooting §1.10):
服务七个全 Up、磁盘 20%、db 连接 9/100、proxy 零 50x、auth 零 panic/5xx、7 天非 2xx 与 §1.4
记录完全一致 ⇒ **服务端没有可修的东西**;用户自己的 Apple 登录 24h 内 3 次全 200(最后一次
= 10-04 03:10 CST),「连不上服务器」按 §0 只能是设备侧(mt:diag 待真机在手)。

查库时挖出**我自己的漏动作**:bt_model_sources 仍是两行 piper 且 active,而 App 从 1.19.0 起
请求 kokoro-zh-en.zip —— 我改了 deploy/china/model-sources.sql 却从未应用;且那份脚本只插新行、
不停用旧行,自带的"恰好 1 行 active"断言会直接报错。

- 脚本补上 update ... set active=false(停用而非删除,包仍在托管上);
- 已应用到境内后端并回读:INSERT 0 1 + UPDATE 2 + 断言通过,kokoro active=t、两条 piper active=f;
- 两处托管实测可下:魔搭 206/146767700、hf-mirror 备用 206 且总长一致(#532 的安全网恢复);
- 老版本无回归:仍请求 piper 的旧构建回落到内置魔搭地址(实测 206)。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
真机(2026-10-04 03:11):最小化 + 断网 ⇒「离线模型下载失败:Error Domain=NSURLErrorDomain
Code=-1005…」,而 NSError 的 UserInfo 里带着 11862 字节的 NSURLSessionDownloadTaskResumeData
—— 既没用它、也没重试,停在失败态等人再点。

行为(不改界面):
- app/native/speech-bridge.swift:新增 MTBackgroundDownloader —— background 会话(挂起/被回收都
  不影响传输)+ waitsForConnectivity + 失败先留 resume data 再用 downloadTask(withResumeData:)
  按 2/5/15/60/120s → 每 5 分钟退避重试(最多 20 次 ≈100 分钟)。重试期间**不动界面**,
  进度停在原地 ⇒ 用户看到「还在下」而不是「失败了」。
- 不再送 String(describing: error):改送协议码 offline/http/sha/load,JS 侧 LearnTTS.reason
  新增三个 case 拼人话(模板未动)。
- 失败不再 removeItem 整个模型目录(旧代码会把同一次里已下好的文件一起抹掉)。
- 回归:test/tts-background-download.test.js(6 条结构门)+ 字符串白名单。
  npm test 2373 passed / 0 failed;macOS 构建 BUILD SUCCEEDED。

交互(已出 Pencil 稿,**未改界面**,待评审):三帧 —— 下载页自选语言对、
朗读包三种状态(下载中会自动续 / 已就绪 / 失败可续传)、听译页语言菜单含 ไทย。
听译页一边是事实约束:本机识别器不支持泰语,只有云端转写引擎才能让泰语可选。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
…561)

用户 2026-10-04 03:46/03:47 拍板:泰语**必须可选**(所有选语言的地方,不灰显、不写「当前引擎不支持」);
朗读用**我们自己打、服务器预制的包**;识别侧 DictationTranscriber 若已能识别泰语则保留。

识别(本机):
- speech-bridge.swift 新增 mtTranscriberFor(窄优先、不支持就落回 DictationTranscriber)+
  mtSupportedLocalesUnion(报两台之并)⇒ 泰语/俄语/阿拉伯语进入可选集合;
- 识别会话按语言选模块类型,且**一个会话只用一种**(两台 result 类型不同);dictation 走
  deliverDictation(无 audioTimeRange/置信度,代价写在那条注释里)。
- 既有门禁(locale 归一化)更新到新结构:三处仍都必须经 mtTranscriberFor,两台都不许直接吃短码。

朗读(新包,服务器预制):
- sherpa-onnx 官方有现成 vits-mms-tha(Meta MMS-TTS,VITS 形状)⇒ **无需自己转换**;
  实测 data_dir 传空即可合成泰语(16 kHz、RTF 0.29)。
- 打包 vits-mms-tha.zip 105,167,496 B / sha256 26ee310a…,上传 ModelScope + hf-mirror;
  device-models.config.js 增 lang:'th'(dataDir 空串,MMS 没有 espeak-ng-data);
  installed() 与打包脚本为此放行空 dataDir;中国版 bt_model_sources 增一行并已应用(2 行 active)。
- verify:model-urls 6/6 全绿。

⚠️ vits-mms-tha 权重为 CC-BY-NC 4.0(禁商用)——是否接受由用户拍板;在明确许可前,
不把这一条当成可上架默认包写进对外说明(注释已写进 config 与 deploy SQL)。

Pencil:删掉灰显泰语那帧,出「改稿C · 泰语可选(本机引擎)」(监督评审通过)。

npm test 2380 passed / 0 failed;macOS 构建 BUILD SUCCEEDED。未打 TestFlight、未重跑已绿矩阵。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
用户未对许可证作答,监督按保守处理裁定:vits-mms-tha(Meta MMS-TTS,权重 CC-BY-NC 4.0)
**不写成可上架的默认分发包、不进下一版构建**;泰语朗读仍要支持,但要等许可干净的自打包方案。

- app/device-models.config.js:删掉 lang:'th' 那条(默认包只剩 kokoro-zh-en,中英)。
  泰语朗读因此回落到**系统语音**(本来就有、许可干净),界面用 tts_device_lang_fallback 具名说明。
  原地留一段注释记录「打过、测过、因许可撤下」+ 商用候选查证结论 + 将来替换的步骤。
- deploy/china/model-sources.sql:删掉 vits-mms-tha 那一行并把回读断言改回 1;**已应用到境内后端**
  (回读 1 行 active、psql rc=0)。
- 两处托管**都已删除**该包:HF commit 9f94fcd4 + ModelScope delete_files(回读 404 / 500)。
- 保留:语言可选与 DictationTranscriber 回落(用户明确不许回退);MMS 形状的打包支持与
  installed() 对空 dataDir 的放行(通用修复,将来换许可干净的 MMS 形状模型直接可用)。

商用候选查证(2026-10-04):**没有** —— MMS / F5-Thonburian 均为 NC;MeloTTS 是 MIT 但不含泰语;
Piper 官方没有泰语声;sherpa-onnx #3028 仍把泰语列为缺失。

npm test 2380 passed / 0 failed;verify:model-urls 4/4(只剩可发的 kokoro 包)。
未打 TestFlight、未重跑已绿矩阵。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
用户 2026-10-04 拍板:「继续用这个 CC-BY-NC 4.0 的 vits-mms-tha 当正式分发的默认泰语朗读包。
不要再另找、不要再问许可证。」⇒ 撤销上一条 revert(8ac3214) 的摘除,恢复为可上架默认包。

- app/device-models.config.js:恢复 lang:'th'(MMS 形状:model.onnx + tokens.txt、dataDir 空串)。
  注释保留许可证事实,但写明**这是所有者知情的决定**,不是疏漏。
- 两处托管已重传:HF commit feb2f7e6、ModelScope Upload complete。
- deploy/china/model-sources.sql:恢复 vits-mms-tha 行、断言回到 2;已应用到境内后端
  (回读 2 行 active、psql rc=0)。
- 界面不动:泰语保持可选(DictationTranscriber 回落让 th-TH 进可选集合)。

verify:model-urls 6/6 全绿(魔搭默认 + hf-mirror 备用,105,167,496 B / sha256 26ee310a…);
npm test 2380 passed / 0 failed。未打 TestFlight、未重跑已绿矩阵。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
用户 2026-10-04 拍板:默认泰语朗读包换成 ModelScope 达摩院
iic/speech_sambert-hifigan_tts_waan_Thai_16k(SAMBERT+HiFi-GAN、waan、16k、Apache 2.0、
resource.zip 248 MB),不再把 CC-BY-NC 的 vits-mms-tha 当正式默认包。

**评估结论:今天换不了 —— 卡在三点(实测,不是推测)**:
1. 该仓库 199 个文件里**一个 .onnx 都没有**(.pth:sambert 162.8 MB + hifigan 865.6 MB、.scm 词典、
   yaml/conf、festival 资源 + resource.zip 248 MB)⇒ PyTorch 的 DAMO pipeline 模型,
   而设备端朗读走 sherpa-onnx(只吃 ONNX)。
2. sherpa-onnx 的 TTS 家族只有 vits/matcha/kokoro/kitten/supertonic/pocket/zipvoice,
   **没有 SAMBERT+HiFi-GAN**;它是「声学 + 声码器」两段式,不是端到端单图 ⇒ 就是导出了 ONNX,
   也得先在原生侧写一套两段式推理(或推给 sherpa-onnx 上游)—— 工程,不是配置。
3. 其 pipeline 包装是 Linux/Python(同族中文模型写明),本机跑不了。

因此**不假装已换成设备包**:
- app/device-models.config.js:撤掉 lang:'th'(默认只剩 kokoro 中英),并在原地写明目标模型、
  Apache 2.0 许可与上面三点卡点 + 真要上的落地步骤(ONNX 导出 + 两段式运行时)。
- deploy/china/model-sources.sql:删掉 vits-mms-tha 行、断言回到 1;**已应用**(回读 1 行 active、rc=0)。
- 两处托管**都已删除**该包(HF commit c1e7880e / ModelScope delete_files)。
- 泰语在语言列表里**仍然可选**(识别侧 DictationTranscriber 回落不受影响,未回退)。
⇒ 泰语朗读此刻回落系统语音(tts_device_lang_fallback 具名说明)。

verify:model-urls 4/4;npm test 2380 passed / 0 failed。未打 TestFlight、未重跑已绿矩阵。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
## 1. Apple 登录「连不上服务器」——定位(境内日志,实测)

真机 05:02 CST 报错的那一刻,境内后端记到的是一条**成功**的 Apple 登录:
`POST /token` **200**、`traits.provider=apple`、来源 `27.55.93.42`(泰国),时间
`2026-10-03T21:02:15Z` = **05:02:15 CST**;近 6h **非 2xx 为空**。
⇒ 服务端那一分钟是成功的,屏幕那句来自**更早的一跳**:换会话那发 POST 在**网络层**失败
(`code network|offline`,按 §0 服务端与遥测都无痕),而 `signInWithIdToken` **没有重试** ——
用户第二次手点就成功,与日志一致(§1.8 早记过这个形状)。
未能读 `mt:diag`:手里是 77 的那台(张大本事)`unavailable`;连着的那台只装了 china **66**(早于 #550,没有 diag)。

**改**:`extension/learn/auth.js` 的 `signInWithIdToken` 加**第一发自动重试**(3 次、仅网络类、
退避 400/1200ms)—— 同一 id_token 兑换对同一用户幂等,安全;4xx/5xx 不重试。
**未进包**(按裁定:不为这一句单独打版)。

## 2. 首启包屏没有语言选择——根因:**那一页从来没写过**

Pencil「稿 · 下载页 · 自选语言对」过了,但代码里**一直没有**这两个选择器 ——
上一轮我把它们**明确留下没做**(当时报告里写了)。用户手里的 77 因此看不到它,不是"新包才有"。

**改**:`src/app/AppShell.jsx` 包屏加「我的语言 / 对方的语言」两个 select;
`src/app/shell-model.js` 画它们(选项走 `AppListen.langOptions` —— 注册表全量、含泰语、
引擎不支持的灰显而不是拿掉)、**与听译页共用** `listenMyLang`/`listenOtherLang` 两个键;
`firstRunLocales` 改成**优先用选中的那一对**(没选过才回落到界面语言+目标语言,老用户行为不变)
⇒ 识别包按所选语言下,不再写死 zh。**未进包**。

## 3. 门禁修正(都是我这次改动暴露出来的)

- `test/auth-auto-register.test.js`:切片从"固定 300 字"改成"到下一个顶层 function" ——
  加重试循环后固定窗口会提前截断,症状是误报「没打这个端点」。
- `scripts/verify-app-bundle.js`:「译成」项数期望**从 target-langs 注册表推导** ——
  写死 13 在 #556 加泰语之后过期(它只在 test:app 里,npm test 看不到,所以一直没红)。

## 4. TestFlight 第四批记档

ASC 实读四条全 VALID:intl iOS **134**(04:50 CST)、intl macOS **82**(04:53)、
CN iOS **77**(04:55)、CN macOS **75**(04:57)。已写进 docs/device-verification.md。
只上传、没有 bind/submit。未重跑全矩阵(用户明确要求)。

npm test 2380 passed / 0 failed;test:app + test:app:china 均通过。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
国际 iOS 135 / 国际 macOS 83 / 中国 iOS 78 / 中国 macOS 76(1.19.0),ASC 四端全 VALID。
只上传 TestFlight,无 bind / 无 submit。

这一批补的是第四批没有、而用户手里的 CN iOS 77 缺的两处:
① signInWithIdToken 的第一发自动重试(仅网络类,3 次带退避)——定位依据是境内后端在用户报错的
   同一分钟记到的是成功 200(05:02:15 CST,provider=apple,泰国 IP),近 6h 非 2xx 为空;
② 首启语音包页的「我的语言 / 对方的语言」(与听译页共用键,含泰语,识别包按所选语言下)。
两处都在 dist-app / dist-app-china 的 Script.js 里回读确认(不是只看 git)。

另记一处措辞偏差留给评审:帧里第二个选择器写「目标语言」,实现沿用听译页既有的「对方的语言」
(这一页要的是被识别的语言,与「译成」是两个概念)。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
用户 2026-10-04 明确:保持「对方的语言」,不为这个用词改代码、不再打版;第五批
(国际 iOS 135 / macOS 83、中国 iOS 78 / macOS 76)已 ASC VALID,维持现状。

- docs/interaction-spec.md(首启屏那一节):记下这一页的语言对(两个下拉、与听译页共用
  listenMyLang/listenOtherLang、选项注册表全量含泰语、引擎不支持的灰显而不拿掉、识别包按所选
  语言对下、老用户回落不变),以及**用语决定与理由** —— 这一页要的是被识别的语言,与「译成」
  (targetLang)是两个概念,Pencil 帧上那句以产品内既有命名为准。
- docs/device-verification.md:把原先「留给评审的措辞偏差」改成已裁定的记录。

无代码改动、无新构建、未提审。

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: GLM 5.3 <zhipuai.cn>
## 问题(中国版 iOS build 78 / 1.19.0 实测,用户截图)

首启「先把两个语音包下好」(屏 2 · 资源包)上,「我的语言 / 对方的语言」两个下拉
只剩并排两个空小方框:没有标签、没有选项(用户 09:01 的截图:只有上下箭头)。
问题记录:#562;特性出处:#561。

根因:`paintFirstRun(session)` 里语言对那段写的是 `firstRunLocales(s)`,而作用域里只有
形参 `session`,没有 `s` —— 严格模式抛 `ReferenceError`,被紧外层 `catch (_) {}` 静默
吞掉,后面的 `wire(...)` 一次都没跑,两个 `<select>` 既没写标签也没 append 选项。
(同文件 `probePacks()` / `runFirstRunPacks()` 里的 `firstRunLocales(s)` 是对的 —— 那两处
确实有 `const s = await readObSettings()`。只有 `paintFirstRun` 这处错。)

## 方案

1. **不再让一处回落失败连控件一起消失**:默认语言对从 `readObSettings()` 读、单独 try/catch
   兜底;两个槽都保证有值(我的语言 = 界面语言,对方的语言 = 另一门,默认英文,我的语言
   是英文时回落中文)⇒ 不再出现空框,与 Pencil 稿 `稿 · 下载页 · 自选语言对`(h1KZdJ)
   的「简体中文 / English」一致。
2. **语言对提前到 `probePacks()` 之前确定** —— 原来在画完状态行之后,识别包那行只报界面
   语言一门(`· zh`)而下载却按整对;现在显示与下载一致(`· zh · en`)。
3. **选项 = 注册表全量**(用户 2026-10-04:「两个下拉框都应该是我们支持的所有语言的列表」),
   按**当前值** keep(与听译页同一条规则,正选中的那门不被灰掉;引擎不支持的仍灰显 —— 这是
   用户「按你意见来」保留的既有裁定)。
4. **布局按 Pencil 稿**(只用 Pencil,未用 OD):两个全宽选择器块、标签在上、纵向排开
   (原来行内 label+select 挤成一行、紧贴下面);两个包各自一张白卡(16px 圆角、包名加粗)。

## 验证

- `npm test` 2381 passed / 0 failed;新增静态门禁 R2g(包屏语言对必须真的画出来:不许裸传
  `firstRunLocales(s)`、两个下拉用定好的 myLang/otherLang 画、全宽堆叠 class 在位)。
- `npm run test:app`(global)与 `npm run test:app:china` 均绿。浏览器门禁新增断言:标签非空、
  选项数 = 注册表、含泰语、**两个下拉都有选中值**、宽度 >150px。
- **反证**:把 `firstRunLocales(s)` 放回去 ⇒ 门禁红,报「我的语言=「」对方的语言=「」,0 项,
  宽度 18px」—— 正是 build 78 截图里的两个小方框。
- 真实引擎渲染截图对照:`我的语言[中文▾]`、`对方的语言[English▾]`、两张白卡,与稿一致。

**未打 TestFlight、未提审。**

Co-Authored-By: opencode <noreply@opencode.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
main 只领先 2 笔(#533/#534,都是 OpenDesign 首屏三段式的文档落地,2026-10-01);
本分支领先 92 笔。唯一冲突在 `docs/interaction-spec.md` 的「App 首屏」两处:
main 是 2026-10-01 的**三段式**(登录 → 资源包 → 引导),本分支是 2026-10-03 用户裁定后的
**两段式**(撤掉引导屏)。两处都取本分支(更新的产品裁定)。

main 的两处非冲突新增已自动合入并保留:
- 「登录永远不是墙 —— 这一条只对浏览器扩展成立」的 #532 限定;
- 「首次运行引导(App 侧)」下的 2026-10-01 更新块。

目的:让 PR #535 与 main 可合并(此前 mergeStateStatus: DIRTY),从而触发 CI。
@belliedmonkey
belliedmonkey marked this pull request as ready for review October 4, 2026 04:44
@belliedmonkey
belliedmonkey merged commit 748e9b4 into main Oct 4, 2026
5 checks passed
@belliedmonkey belliedmonkey changed the title feat(app): 首屏三段式 —— 屏 1 只剩登录 · 屏 2 资源包(已接线)· #386 退役(六条红线全绿) 1.19.0:首屏三段式 + 听译/朗读真机修复 + 诊断 L1/L2 + 验证基建(172 提交) Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant