主题
Backlog — 持久化未来任务池
永不归档——本文件是活跃 backlog,区别于其他一次性 plan。 用户口头提到的"以后做 / 后续 sprint"必须立即追加到这里,避免会话异常丢失。 启动新会话时若问"接下来做什么",先 Read 本文件。
一旦某个任务进入实施,把它从这里移到独立
<feature>-plan.md,并从本文件删除条目。 创建:2026-05-21🔄 归档节律:条目做完标
## ✅后,攒够一批就移进backlog-archive.md(保留裁决推理,别删——CHANGELOG 只记 as-built, 不记"为什么否掉另一个方案")。2026-08-15 首批移出 20 条 ✅,本文件从 242KB 降到 198KB; 在那之前 ✅ 条目占了近 19% 的篇幅,把真正的待办埋在中间。📦 2026-08-30 分片(
post-merge-repo-optimization-plan.mdT4-3): 本文件从 255 KB 降到约 120 KB。搬走的三类是 ① 标 ✅/❌ 的 8 条 →backlog-archive.md; ② 长成 plan 形状的「短语自动高亮 B 方案」(24 KB)→phrase-highlight-followups-plan.md; ③ RVH 执行的 11 条 →rvh/docs/plans/backlog.md(见下 §跨端条目落点)。 一个字都没删 —— 全部逐字换了位置,裁决推理照旧可 grep。🔀 跨端条目落点 = 「谁执行谁持有」(同日定):一条跨端任务的正文只存一份, 落在将来真正动手那一端的 backlog,另一端只留一行指针。 RB 要做的(改 Rust/TS、动 Supabase DDL、改预装库 pipeline)写这里; RVH 要做的(改 Dart / 跑
flutter test/ 动 RVH schema)写rvh/docs/plans/backlog.md。 两端都要动的(契约类)落本文件 —— 契约的稳定归属本来就在根CLAUDE.md§4/§9。 ⚠️ 判据是执行端不是发现端。📍 结构化优先级另见
product-iteration-roadmap-2026h2.md(2026-07-21 产品评估定下的四梯队迭代方向:发行信任还债 → 打磨包 → 推荐/收敛 → 遥测/商业化决策; 与本文件互补不重复登记)。
🔴 P0 知识库迁到 Cloudflare Pages + Access(Vercel Hobby 挡不住)
为什么必须换平台:Vercel Hobby 的 Standard Protection 不保护 production 域, 而项目别名 lampio-kb.vercel.app 本身就是 production 域(判据与「是不是自定义域」无关)。 能覆盖它的 All Deployments 是 Pro / $150 月。⇒ Vercel 侧无解,不是配置问题。 完整推翻过程见 knowledge-base-plan.md §4.2。
为什么是 Cloudflare:它是本方案 §4.2 的原备选(当时因「Vercel 够用」降级,那个前提已倒)。 ✅ 两个前提实测都已满足:lampio.app 的 NS 已经在 Cloudflare (angelina/salvador.ns.cloudflare.com)⇒ Access 要求的「域名托在 Cloudflare」成立、 kb.lampio.app 不用迁 DNS;产物 1241 文件 / 最大单文件 10.9 MiB, 两条硬限制(20,000 文件 / 25 MiB)都不紧。
仓库侧已备好(02596685 之后一批):wrangler.toml 的 pages_build_output_dir · .node-version = 22.14.0(Pages 默认 Node 偏旧)· pnpm-lock.yaml 让 Pages 自动选 pnpm。
剩下的都要账号授权,得人来做:
wrangler login(开浏览器 OAuth)—— 或直接在 dashboard 做完 2-4。- Pages 项目连 GitHub
ttfishnet/lampio:构建命令pnpm run docs:build· 产物目录docs/.vitepress/dist· 根目录/。 - 自定义域
kb.lampio.app(DNS 记录会自动建,因为 zone 就在 Cloudflare)。 - Zero Trust → Access → Applications 加一个 self-hosted 应用覆盖
kb.lampio.app, 策略 = 允许你自己的邮箱,登录方式 One-time PIN。免费档 50 人。
🔴 验收方式与 Vercel 那次逐字相同,别看设置页:
bash
curl -sS -o /dev/null -w '%{http_code}\n' https://kb.lampio.app/非 200(Access 会 302 到 *.cloudflareaccess.com)才算过;再跑一次带 | head -c 400 确认响应体不是站点内容。上一次就是靠这条判据才发现「设置页说保护、实际 200」。 ⚠️ 同样别预设 Cloudflare 的语义是对的 —— 我对 Vercel 的模型就错过一次。
Vercel 侧收尾(Cloudflare 验收通过之后再做,别提前): 删掉 project lampio-kb → 删 vercel.json / .vercelignore → 同时改 scripts/check-kb.mjs 的 A9(它现在断言 vercel.json 必须存在, 删文件不改断言 = 造一道恒红的闸门)。
🟡 P3 CHANGELOG.md 不在任何反引号路径闸门的范围里
pnpm run check:readme-paths 只取 tracked 的 README.md,CHANGELOG.md 与 docs/plans/*.md 都不在内。实测 CHANGELOG.md 已积 17 处失效反引号路径 (scripts/sync-rvh-vocabulary.sh、一批已归档的 plan 等)。 🔑 它们全是历史条目,按「历史记述不改」不该去修 —— 所以这不是「清存量」任务, 真正要定的是:闸门要不要覆盖历史记述区?若要,得先有一套「历史条目豁免」的表达方式, 否则接进去就是恒红。
✅ 发版版本号的第四处没人守 —— 已修(2026-09-01),Cargo.lock 进了写入器与断言
scripts/set-version.mjs 写三处(src-tauri/tauri.conf.json / src-tauri/Cargo.toml / package.json),pnpm run check:version-sync 也只断言这三处。而 src-tauri/Cargo.lock 里 [[package]] name = "app" 的 version 是第四处 —— 它只有本机跑过 cargo 才会跟着变,没有任何东西强制它跟上。
实测证据(v0.1.0-dev.11 发版当场撞到):
| tag | Cargo.toml | Cargo.lock |
|---|---|---|
v0.1.0-dev.10 | dev.10 | dev.10 ✅ |
v0.1.0-dev.11 | dev.11 | dev.10 ❌ |
dev.10 那次是一致的 —— 说明以往靠的是「bump 之后碰巧跑过一次 cargo」,不是靠任何断言。 dev.11 这次 bump 完立刻打 tag,缺口就露出来了。
影响:有限但真实。CI 的发版构建不带 --locked,所以三平台照常编译通过(已实测); 但从该 tag 做可复现构建(cargo build --locked)会失败。也就是说这个缺陷的失效面 恰好落在「平时不会碰、真要复现历史构建时才发现」的位置 —— 与本仓反复治的那类 「错着但什么都不会红」同形。
该做的两件(一起做,缺一无效):
set-version.mjs写版本时一并改Cargo.lock的app包版本 (只改[[package]]块里name = "app"那一条,别碰依赖树)。--check模式断言第四处,让check:version-sync名副其实 —— 只做 1 不做 2,下次仍会靠运气。
🔴 只做 1 是不够的:真正的病是「三处一致」这句话本身在说谎,而说谎的是断言的覆盖面, 不是写入逻辑。没有 2,这条 backlog 明年还会以另一种形式复发。
为什么当时没顺手修:set-version.mjs 正被在飞的 dev.11 发版流程依赖 (见 docs/plans/archive/release-v0.1.0-dev.11-handoff.md),当场改它会干扰按交接单接手的新会话。 触发时机 = dev.11 发完、该交接单归档之后。 ✅ 该条件 2026-09-01 已满足(dev.11 两仓已 publish + promote-updater 已跑 + 交接单已归档), 当天即修完:
set-version.mjs写入器 + 读取器 +--check断言都补上了第四处,正则锚定在[[package]] name = "app"那个块上(Cargo.lock 里有 720 行version =,裸正则会改到依赖上)。- 两条反向注入实证:① 让 lock 的 app 版本漂移 →
--check退出 1 并指名道姓报「从该 tag 跑cargo build --locked会失败」;② 写一个新版本 → Cargo.lock 恰好 1 行变更(1+/1-),四处同步。 - 顺带验掉真实失效面:
cargo check --locked从当前树通过。 - 连带订正两处过期表述:
docs/verification/release-chain.md的 R1 行(三处→四处)+ 根CLAUDE.md§8 那句「package.json的version是死字段(长期0.0.0)」—— 实测它是0.1.0-dev.11,死的是消费方不是值。
🟢 pnpm/action-setup@v4 仍在 Node 20 运行时(2026-08-31)
2026-08-31 把 actions/* 五个全部升离 Node 20(checkout@v7 · setup-node@v7 · cache@v6 · upload-artifact@v7 · download-artifact@v8,各自的跨版本风险已逐条核过并实跑验证)。 剩下的唯一一个是 pnpm/action-setup@v4 —— 它不是 actions/* 官方仓,当时没纳入那一轮清单, 是 CI 日志里的弃用警告自己露出来的。
用在三处:ci.yml · ci-admin.yml · release.yml。
现状只是警告(被强制跑在 Node 24 上),不阻断。做的时候照 actions/* 那轮的做法: 先用 gh api repos/pnpm/action-setup/releases 逐个大版本核破坏性变更,再改、再 dispatch 实跑验证。 ⚠️ release.yml 那处不发版就跑不到,与 setup-node 同一个盲区。
🔀 跨端条目落点 —— RVH 执行的 11 条已迁往 rvh/docs/plans/backlog.md(2026-08-30)
按头部那条「谁执行谁持有」原则一次性迁出。这里只留索引,正文在 rvh/docs/plans/backlog.md (该文件的 §从根 docs/plans/backlog.md 迁入 一节)。要动手就去那边读,别照下表的一句话开工。
| 迁出的条目 | 原优先级 |
|---|---|
| RVH 的 Dart 格式没有机械守卫 —— 要不要全仓 tall-style 重排版 | 🟡 |
| RVH 有两套 DI 装配写法并存 | 🟢 |
| RVH OCR 配置校验与实际引擎集合不一致 | 🟡 |
| cloze 验证 C 项:RVH 删扫描页后 cloze 行是否还在 | 🟡 |
RVH 侧核对:写 known_words 是否漏归一(红线 #9 对称问题) | 🟡 |
| 知会 RVH:隐私政策已改(默认开启口径) | 🟢 |
| RVH 接 Azure TTS —— 两端发音统一 | 🟢 |
reading_notes.source_platform 跨端漂移(已诊断,修复在 RVH) | 🟠 |
| RVH 构建预装短语库(phrasal verb / idiom) | 🟣 |
| RVH 短语/习语归一噪声治理(lemmatizer 资产 + idiom 展示形) | 🟠 |
| Supabase 统一维护到 RB —— 阶段 2 RVH 侧 | 🟡 |
另有 1 条是真重复,已合并不是迁移:「RVH push learning_entries 95% 失败根因调查」 与 rvh/docs/plans/backlog.md 的「RVH push 75→4 漏推根因调查」是同一件事(同日、同 user id)。 保留了 RVH 那份(它算出缺口是 63 而非表面的 71,且已把三个诊断分支收敛到 clean=75 那支), 本文件那份唯一的独有信息(RB 端 sync 路径已验证正常)已并进去。
⚫ 2026-08-31 状态更新:那条已结案(证据灭失·不复现),不要再当待办开工。 样本两端都没了(RVH 设备 2026-08-26 重装、旧账号
95d7104d-…已从auth.users删除 ⇒ 10 张同步表按ON DELETE CASCADE全清),当前 build 双端逐词对账本地独有 0 行。 同一次调查在 pull 侧查实了一条新缺陷(跳过的行会被 watermark 永久跳过,实例pellet), 已在 RVH 那份里另立条目并于当天修完、真机验证通过(收尾行数对账 + 全量自愈 pull; 顺带修掉真正的根因 ——cefr_inferred被当num?转,导致 backfill 抛异常、 该词永久拉不回来。RB 侧无同款问题:supabase.rs是Option<bool>,类型化反序列化天然正确)。 执行端是 RVH,正文不迁来本文件。
留在本文件的跨端条目(两端都要动或 RB 执行,故不迁): 百度 OCR 弃用收尾 · 预装词数口径(RB 是产源方)· roadmap ④ 个性化例句跨端可见性 · 跨端联调开关 · 跨端整合尾项 4 项。
🟠 P2 —「本地墓碑 + 云端活行」两端永久分歧 —— ✅ 已结案(2026-08-31):两端落地 + 真机验收 + 存量清零
契约类,两端都要动 ⇒ 正文落根侧(rvh/docs/plans/backlog.md 只留一行指针)。 🔴 动手前必读根 CLAUDE.md §4 的 RB #6d,以及 rvh/lib/features/sync/CLAUDE.md 的 RVH #6f / #6g。
✅ 裁决已出:
docs/cross-end/47-rb-tombstone-vs-remote-alive-adjudication.md(2026-08-31,纯文档,零代码改动)。 判据 = 「push 会不会把这条墓碑送出去」 —— 会 → 照旧 skip;不会 → 接受远端、复活本地行。 全程不比较任何时间戳(绕开 RB #6d 的跨时钟禁令),判据 SQL 必须由 push 的脏检查常量拼出、禁止手搓。 影响面不止 cloze:分支② 在 RB 10 个pull_*里逐字都在,8 张表有复活写路径。 🔴 顺带查出 RVH_pullKnownWords没有分支② 且把synced_at刷成本端now—— 今天就在静默丢删除,必须先修。下面的现场记录保留原样(裁决单不复述它)。还没做的是实施:
- [x]
RB 实施会话—— ✅ 已完成(2026-08-31):红线 #6e 落地(根CLAUDE.md§4),10 处分支② 全改,新增 10 个用例/守卫,九处反向注入全部按预期变红;as-built 与两处「守卫自己错了」记在裁决单 §10。cargo test251/251- [x]
RVH 实施会话—— ✅ 已完成(2026-08-31):新立 RVH #6i(rvh/lib/features/sync/CLAUDE.md),RVH-0…RVH-4 全做,flutter test1401/1401(新增 20 例),11 处反向注入全部按预期变红。🔴 顺带查出并修掉一个独立的真缺陷:word_page_links那份 push 脏检查整支updated_at条件都不在 ⇒ 删词 Undo 复活出的 link 永不上行、且任何后续操作都修不了(详见回执 §2)- [x]
存量 1 行—— ✅ 已处理(2026-08-31):按 §8 选 B 做了单 id no-op touch(行快照存backups/),下一轮 pull 即命中 ②b,设备上该行deleted_at清空、updated_at = synced_at = 远端 updated_at逐字相符 ⇒ T4/T5 真机各实证一次- [x]
真机验收—— ✅ 差额归零:word_cloze_contexts119/120 → 120/120,本地独有全 0(push 侧无回归)。唯一剩下的word_page_links dd052755已查证是 #6f 的存量孤儿(父词条云端也不存在),属另一条线- [ ] 🟡 建议 RB 自查(回执 §4.5):②b 落到「不计数的下游分支」时会不会卡住 watermark —— RVH 侧真机撞到了(空转三轮,靠行数对账自愈顶过去),已修成 ②b 显式
total++;RB 的 cloze 双活分支是同一形状- [ ] 🟡 待 RB 定:分支② 排在父行三态守卫之前(两端同形)——「clean 子墓碑 + 远端子行活 + 本地父行仍是墓碑」会先复活子行、再被守卫 skip ⇒ 本地留一条挂在墓碑父下的隐身孤儿。常见路径够不到,RVH 侧刻意没有单方面改序(回执 §5)
- [x]
两端各回一份回执—— RB 的 as-built 在裁决单 §10;RVH 回执 =docs/cross-end/49-rvh-tombstone-vs-remote-alive-confirmation.md(含对裁决单三处判断的订正 + 两处欠的前置)
现场
word_cloze_contexts 3a50ad2b-f876-4250-905c-2dc25798292e(cough up):
| 本地(RVH) | 云端 | |
|---|---|---|
| 状态 | 墓碑 deleted_at=2026-08-26T09:23:02 | 活行 deleted_at=null |
updated_at | 2026-08-26T09:23:02 | 2026-08-27T03:10:02 |
synced_at | 2026-08-26T09:23:02 = deleted_at ⇒ 已 clean | — |
🔑 关键事实一:没有任何一端做错(2026-08-31 逐条读码核实)
| RB | RVH | |
|---|---|---|
| 写侧遇到同句已软删行 | crud.rs::insert_cloze_context:SET deleted_at = NULL 复活,注释写明「红线 #7 精神,避免同句产生『新行 + 旧墓碑』两条记录」 | cloze_pool_writer.dart:同样复活,注释同款 |
| pull 侧「本地已删 + remote 活」 | pull/vocab.rs:→ skip(本地墓碑靠 push 传播,不复活) | sync_repository_impl.dart:一字不差的同一条 |
⇒ 复活是刻意设计(用户又遇到同一句 → 复用旧行而非造重复行),两端写侧对称、 pull 侧也对称。这不是某一端的 bug,单方面改哪边都会立刻制造真正的不对称。
🔑 关键事实二:缺口在那句注释的前提,而它从没被校验过
本地墓碑靠 push 传播 —— 这句话默认「本地墓碑还是脏的」。而按 RB #6d 规则 W (软删时 deleted_at 与 updated_at 绑同一个参数)+ push 成功后的 _markSynced, 墓碑推完就恒不脏。于是:
- pull 说「不复活,靠 push 去传播」
- push 说「这行不脏,没什么可推」
前提落空,规则悬空,两端各说各话地停住。 无异常、无告警。 属本仓反复在抓的形状:注释里写着的假设,代码里没人校验。
🔴 难点不是「谁赢」,是「怎么判断谁更新」
直觉是「远端 08-27 晚于本地 08-26 ⇒ 远端赢」。但那是跨时钟比较 —— 正是 RB #6d 明令禁止的那类(该条记着 RVH 真机反算的 27 秒漂移门槛)。日级差异看着安全, 秒级不是。
而只看「本地墓碑已 clean」不够:远端变活有两种成因,在本端看起来完全一样 ——
- 对端刻意复活(用户又遇到同句)→ 远端该赢
- 对端还没拉到墓碑,就把自己那份陈旧的活行推了上去(红线 #6b:push payload 传本地真值 ⇒
deleted_at=null覆盖掉墓碑)→ 远端不该赢
⇒ 光靠现有列区分不了。这才是本条的真正难点。
可能的出路(⚠️ 已被裁决单 §4 逐条处置 —— 三条全部否决,此处仅留原始记录)
- 加一列显式意图(如
revived_at):复活时写,pull 侧据它判「这是刻意复活」而非 陈旧覆盖。代价 = 动 Supabase DDL + 两端 schema,属正经跨端契约变更。 - 墓碑不进业务键 upsert 的冲突面:让复活走「新 id 新行」而不是复用旧行 —— 但那正是 两端注释里说要避免的「新行 + 旧墓碑」,且会撞 cap-5 池语义。
- 接受分歧、只保证可观测:不修语义,但让
sync_reconcile.sh与引擎内对账把这类 差额单独归一类(而不是混在「疑似丢行」里)。成本最低,但用户仍会看到两端内容不一致。
影响面与自愈
目前 1 行(一条语境句)。后果:RB 上看得到这条例句、RVH 上看不到,两端池子内容分歧。 用户操作能自愈 —— 在 RVH 上再遇到这句,写侧会把本地行复活。 ⚠️ 它还会让行数对账在这张表上留一个永久差额(按设计退到「20 轮试一次」, 见上面那条已完成的条目)—— 那不是对账的 bug,是它如实报出了这个分歧。
会话怎么开
先开一个「契约裁决」会话 + 两个实施会话 —— 全部完成(2026-08-31:doc 47 裁决 · RB 侧 as-built 在 47 §10 · RVH 侧回执 doc 49)。 剩下的不是编码会话:存量那 1 行是一次 🔴 高危档的 Supabase 单 id 写操作 (先备份 + 用户确认,时机 = RVH 装机之后),真机验收跟在它后面。
🟢 EPUB B8「改名/移动」—— 9 条缺陷里唯一没做的(2026-08-07 记录,2026-08-30 拆出)
全文(9 条逐条 as-built + 4 份 handoff 索引 + 三次对账进度)已移进
backlog-archive.md§📦 第二批 —— 那块 47.7 KB 里 8 条已闭环, 留在活跃区的只有本条。动手前去那边读第 8 条的原文,这里不复述。
为什么单独留一条:整块进归档区会让这条唯一的活跃缺陷一起消失; 留全块在活跃区则每个读 backlog 的会话都要吃 47.7 KB 的 as-built 流水。
🟢 低频,无阻塞,随时可做。
📋 外部 backlog 两轮裁定留痕 —— 已全部结案,正文在归档区(2026-08-12 / 08-13)
第二轮(发现模块 × 学习统计,5 条建议 + 2 条设计原则)与第三轮(四模块视觉优化,约 24 条建议) 的逐条裁定已于 2026-08-30 移进 backlog-archive.md §📦 第二批。
⚠️ 它们的作用没变、只是换了位置:这两块存在的理由是「防止同一批建议被反复评估」。 外部又送来同类建议时,先 grep 归档区那两节,多半当初已逐条对着代码核过并给了结论 (尤其第三轮 §0「已否决、别重新论证」那张清单)。
✅ 清 src/ 的 eslint 存量并把 pnpm lint 入闸 —— 已完成(2026-09-01)
来源:原「文档治理尾项」①(ci.yml 不跑 eslint)。那条已收口 —— 作用域缺陷已修、 裁定已写进 .github/workflows/ci.yml 头注释,这里只剩「清存量」这件真活。
为什么原来看不清尺寸:根 eslint.config.js 只 ignore 了 dist,于是 eslint . 走进 admin/ landing/ supabase/ rvh/ —— 而那四个各有自己的工具链与 lint。 旧记录说「507 errors,多在 supabase/functions/ 的 Deno 代码」,两点都不对: 实测 243,其中 156 个(64%)在那四个目录里,把桌面端自己的问题埋在噪声里。 作用域限定到桌面端本体后,真实存量是 44 个,全部在 src/:
| 规则 | 条数 | 性质 |
|---|---|---|
react-hooks/set-state-in-effect | 25 | 在 useEffect 里 setState —— 逐个要判是不是真的需要同步派生状态,是重构不是格式修正 |
react-refresh/only-export-components | 7 | 组件文件里混着非组件导出,影响 Vite HMR 粒度。多半是把常量/类型挪走即可 |
@typescript-eslint/no-empty-object-type · no-empty-pattern · no-unused-expressions | 各 2 | 机械可修 |
react-hooks/{immutability,purity,preserve-manual-memoization,refs} | 共 5 | 逐条判 |
@typescript-eslint/no-unused-vars | 1 | 机械可修 |
分布:src/components 39 · src/hooks 5。热点文件:AddressBar.tsx 4 · vocab/WordSenses.tsx 4 · SentenceWorkbench.tsx 3 · StatsPanel.tsx 3。
做法:先做机械可修的 10 条(→ 34),再逐个判 set-state-in-effect 那 25 条 —— 它们多半分成「真的需要派生状态(改成 useMemo / 渲染期计算)」与「确实需要副作用同步」两类, 后者要 eslint-disable 加一行理由,不是删规则。清到 0 之后再把 pnpm lint 加进 ci.yml。
⚠️ 别为了入闸而降级规则(把 set-state-in-effect 调成 warn)—— 那是让闸门变绿而不是让代码变对。 ⚠️ 入闸前必须真的到 0:本仓 2026-08-18 因一道恒红闸门让 main 的 CI 连红 6 天, 纯文档 commit 也挂,而 Vercel 不等 Actions —— 红着的那 6 天部署照常进行。
✅ 结果(2026-09-01):0 error 入闸,但计划的两点都被实测改了
① 尺寸不是 44,是 51;而且作用域缺陷还剩一半没修。 复测 eslint . 报 135 条,其中 84 条来自 .claude/worktrees/project-docs-knowledge-base-ae3bb6/ —— 那是本仓自己的 git worktree(Agent isolation 留下的),里面完整一份 src/+admin/+landing/+supabase/。 2026-08-30 那轮加的 ignore 写的是 admin/** 而不是 **/admin/**,一个都挡不住它。 补 .claude/** 之后真实存量 = 51(不是 44 —— 44 是 error 数,还有 7 条 warning)。
② 「set-state-in-effect 分两类」的划分不成立,实测是三类(最小实验钉的判据):
| 形状 | 例 | 结果 |
|---|---|---|
| effect 体里同步 setState | if (!word) { setItems([]); return; } | 报 ✅ 真阳性 |
effect 调一个会 setState 的 useCallback | useEffect(() => { load(); }, [load]) | 报 ❌ 假阳性 —— load 是 async,setState 在 await 之后,规则看不穿 callback 边界 |
| 内联 async IIFE | useEffect(() => { (async()=>{…setData(r)})() }) | 不报 —— 同样语义,换写法就不报 |
而且逐条 eslint-disable 追不上:给某处加了 disable,规则立刻改报同一个 effect 里的 另一个 setState(实测从 setMemory(null) 跳到 .catch 里那个),且每加一条注释报告行都移动 ——它是整函数分析。⇒ 计划里「后者要 eslint-disable 加一行理由」这条路在这条规则上做不到。
清掉的 29 条全部是真修(不是压制):7 条 react-refresh(3 个纯函数搬进 lib/ + 1 个取消导出
- 2 个死 hook 删除,
useAnnotationCounts/useVocabStats注释都写着「给某某用」而实测零调用方)· 2 条 immutability(真缺陷:EmphasizedText改的是调用方useMemo缓存的那个/g正则的lastIndex,两处同时渲染会互踩游标漏高亮)· 1 条 refs(渲染期写 ref)· 5 条机械 · 3 条 set-state-in-effect 真重构(1 处改渲染期派生 + 2 处改 React 官方「渲染期调整」模式)· 顺带把三个组件各抄一份的LOW_SCORE_THRESHOLD = 80收成单点。
剩余 22 条走祖父条款(用户 2026-09-01 拍板):eslint.config.js 里按文件白名单关掉 这一条,规则本身保持开启(新文件照样受约束)。守卫 scripts/check-lint-grandfather.mjs 逐文件复验「是否仍需要豁免」,不需要了就必须删——名单只能变短,两条反向注入实证过 (塞一个已修好的文件进去→红;把注释条数改错→告警)。已接进 ci.yml。
🟢 set-state-in-effect 祖父条款名单的消化(2026-09-01 立项 · 同日消化掉最便宜的一批)
起点 22 条 / 17 文件 → 现在 15 条 / 12 文件。 已消化的是「规则假阳性」那 7 处 (effect 直接调 async useCallback):改成 void (async () => { await load(); })() 即可, 语义等价而规则不再误判。
剩下 15 条不建议照此办理 —— 它们是 DOM 测量(必须在 paint 之后)、定时器/动画状态机、 异步加载前先清空(有意的级联,防上一条数据串台)、换 props 重置、外部 store 信号五类, 包 IIFE 对它们是掩盖不是修复。🔑 判据:setState 是不是真在 await 之后。 反例就在这批里 —— StatsPanel.loadStatic 在第一个 await 前 setIsLoading(true), 那是真同步级联(查下来那句还是纯冗余,已删)。
⇒ 后续按「碰到那个文件时顺手判一处」推进即可,不必专门排期;守卫会在某文件不再需要豁免时 提醒你把它从名单里删掉。
✅ 3 条 react-hooks/exhaustive-deps warning —— 已修(2026-09-01 同日),门槛已收紧到 --max-warnings 0
三条各自的病根都不是「漏写依赖」:
BrowserPage.tsx:218缺getContainerRect—— 补进去是纯形式:它是useCallback(…, [])恒稳,而 deps 里已有的getWebviewRect又依赖它 ⇒ 两者身份同生同灭,重跑时机一个字没变。AddressBar.tsx:257缺activeTab?.filePath—— 真相是那里有两个几乎逐字重复的 effect (一个 deps[activeTab?.id]还挂着eslint-disable-line,一个 deps[url]),算同一个 siteUrl、 写同一个 state,换 tab 时两条异步竞写isSavedState。合并成一个 + 补竞态守卫, 顺带把祖父条款里 AddressBar 的条数从 4 降到 3。RssPanel.tsx:128缺handleRefresh/isRefreshing—— 病根是在setItems的 updater 回调里调副作用,拿状态更新器当「读当前 items」的手段;React 要求 updater 纯,StrictMode 下 会跑两次 ⇒ 自动刷新可能触发两遍。改成用loadItems的返回值判断(返回本次真正加载到的 条数,比读提交态更准),handleRefresh走 latest-ref;isRefreshing那个条件删掉不是漏掉 —— 守卫已在被调方handleRefresh第一行。
⇒ pnpm lint 已收紧为 eslint . --max-warnings 0,反向注入验过(去掉一个依赖 → rc=1)。
🟡 百度 OCR 弃用收尾 —— 控制台删应用 + 清历史里那对凭据(2026-08-28)
来源:RVH 合仓预检 P0-3(gitleaks git ~/reading_vocab_helper --log-opts="--all")。 详细取证见 rvh-merge-plan.md §2「P0-3 结果」。
事实:RVH 历史 commit e3e236dd(2026-01-02)的 .env.staging 与 assets/.env.staging 里有一对真实的 BAIDU_OCR_API_KEY / BAIDU_OCR_SECRET_KEY (熵 4.42)。两个文件早已从 HEAD 移除(bb5dc7f / 60ac1e1),只剩在历史里。
严重性 = 卫生问题,不是事故(2026-08-28 用户确认后定级):
- gitee
ttfishnet/reading_vocab_helper是私有仓 → 未对外暴露。 - 百度 OCR 在 RVH 已弃用:
.env里没有这两个键,.env.example里是注释掉的,ocr_engine_providers.dart:36的 provider 返回null, 优先级Google Vision > Baidu OCR > ML Kit直接跳过。 - 但 AK/SK 不会自己过期 —— 只要百度控制台里那个「应用」还在, 它就还能认证、还能被计费。风险不在 RVH 用不用它,在百度账号那边它还认不认。
进度(2026-08-29 复核):
[x] ② 清历史里那两个文件 —— ✅ 已完成(随
archive/rvh-merge-plan.mdT3-2 的 filter-repo 一并做掉, 边际成本确实 ≈ 0)。本仓复核实证:git log --all -- 'rvh/.env.staging'/'rvh/assets/.env.staging'/'*.env.staging'三条均为 0 个 commit; 全历史git grep BAIDU_OCR_*只剩 5 个文件,全是占位符 (rvh/.env.example的your-api-key-here)、变量名(app_config.dart/check_api_config.sh)与描述本问题的文档本身 —— 没有任何真实键值。 交叉验证:同日对本仓全历史跑gitleaks git . --log-opts="--all"(1379 commits / 215 MB / 9m40s) 共 14 条命中,百度 AK/SK 一条都不在里面 —— 与上面的git log/git grep相互独立地佐证了同一结论。 那 14 条的分类见下面「本仓首次全历史 secret scan」。 ⚠️ 只清的是本仓的历史。老 gitee 仓reading_vocab_helper里那两个文件照旧在 (2026-08-29 已置「关闭」= 只读,且一直是私有仓)—— 这正是 ① 不能省的原因。[ ] ① 百度智能云控制台删掉那个应用 —— 仍待用户操作,且是这条唯一的实质风险。 AK/SK 不会自己过期;只要控制台里那个「应用」还在,它就还能认证、还能被计费, 与代码用不用它无关。控制台没有「重新生成密钥」按钮,作废 = 删应用。 对号入座用这个 API Key 前后缀:
8rEyau…………1OJc(SK 不需要 —— 控制台不按 SK 检索;要看全值:cd ~/reading_vocab_helper && git show e3e236dd:.env.staging | grep BAIDU_OCR, 老仓虽已归档为只读,本机克隆照常可读)。 删完 RVH 不需要任何代码改动(引擎本来就没启用)。[ ] ③ 删掉 RVH 里的百度 OCR 死代码 —— 已拆成下面独立一条(与 ①② 无依赖,属 RVH 会话的活)。
⇒ 本条的收尾判据 = ① 一件事。 ② 已闭环,③ 已另立条目。①做完即可整条归档。
🟡 预装词数口径 —— 主项已改(2026-08-28),剩通知 RVH 镜像 + 一条未复核的数
来源:RVH 在 cloze relay 第 3 棒交接单 §9 里提出。RB 侧已核实,数字属实:
sqlite3 src-tauri/assets/lampio_dict.db \
"SELECT COUNT(*), SUM(word NOT LIKE '% %'), SUM(word LIKE '% %') FROM vocabulary;"
18916 | 12172 单词 | 6744 短语12,291 在任何口径下都对不上。
它主要不是口径问题,是陈旧问题。 CLAUDE.md:336 那行括注写着「2026-05-14 v10 切换」—— 它冻在 v10,此后多轮 reseed 都没回写。本仓自己已经记着正确的数: 本文件 v28 那条清理记录写的就是「词数 18,925→18,916」、 VOCABULARY_SEED_VERSION = rb-18916-v28-poscefr-2026-08-26。 所以「口径」只剩一个小问题:这个数该叫什么(「词汇」还是「词条」、含不含 6744 条短语)。
🔴 前置依赖搞反了,别照着等。交接单写的是「等 RVH 另开会话定口径并发交接过来」, 但 RB 才是这个资产的单一产源(红线 #10:pipeline 2026-07-19 迁入 RB,RVH 已降级为纯消费方)。 口径由消费方定、产源方等着,是反的 —— 这么等下去两边都不会动。 正解:RB 定口径 + 改自己这行,然后按 §9 流程通知 RVH 镜像。
✅ 已做(2026-08-28):CLAUDE.md 那行改为不写绝对数
选了「指向 VOCABULARY_SEED_VERSION」而不是「换成 18,916」—— 换数字只是把过期时间往后推一轮。选那个常量的理由不是它更权威,而是 它不会静默过期:reseed 必须 bump 它,不 bump 就不会重新灌库 (helpers.rs:198 比对相等即无条件 return),所以「常量没变」= 「库也没变」。 行内附了可复跑的 SQL(总数 / 单词 / 短语),并写明「别再往这行填绝对数」及理由。
⏳ 剩下两件
- 通知 RVH 镜像同一处措辞(走 §9 流程)。RVH 那边不必为此另开会话定口径 —— 口径已由产源侧(RB)定完,它照抄即可。
- 🔴 idiomaticity 三档的数没有复核:RVH 交接单说实测是
2776 / 3421 / 413, 而两仓 CLAUDE.md 都写2776 / 3451 / 383(合计同为 6610,疑似 30 条 context→literal 的 retag 没回写文档)。本会话未验,数字来自对方交接单。 改前先翻cross-end/14、15找这次 retag 的出处 —— 同一个形状的陈旧, 但这次两仓都写着同一个可能过期的数,属双份副本同时漂移。
⚠️ 同批还发现 idiomaticity 三档实测是 2776 / 3421 / 413, 而两仓 CLAUDE.md 都写 2776 / 3451 / 383(合计同为 6610, 是 30 条 context→literal 的 retag 没回写文档)。改前先翻 cross-end/14、15 找这次 retag 的出处。 这一条本会话未复核,数字来自 RVH 交接单。
🟡 cloze 语境池:满池时复活一条被淘汰的句子会谎报「✓ 语境 +1」(2026-08-28 实测)
来源:跨端 cloze 池验证 relay 的顺带发现,完整实测数据在 docs/verification/cross-end-cloze-pool-2026-08.md §2 1-g。 这里只登记要动手的那部分。
症状(用户视角)
池子已满 5 条时,重新双击一句曾被淘汰过的句子 → 弹窗亮出「✓ 语境 +1 · 撤销」→ 60 秒后那条语境悄悄消失。 用户收到的是成功反馈,拿到的是零。
根因是两处叠加,缺一不会出事
vocabulary/crud.rs::insert_cloze_context的复活分支在封顶之前return—— 命中已软删的同句行就deleted_at = NULL然后直接返回, 下面那条封顶 UPDATE 根本没跑 ⇒ 活跃行变 6,CLOZE_POOL_CAP = 5被突破。 且复活不刷新created_at⇒ 它回到池里立刻又是最旧的那条。popup.js::checkContextIsNew把墓碑行判成「新语境」 —— 它走get_cloze_sense,那条查询带deleted_at IS NULL,对墓碑行返回None⇒ 判为新句 ⇒ 渲染「✓ 语境 +1」徽标。
第 6 条会在下一轮 sync 被 reconcile_cloze_pool 砍掉(push 在 pull 之前, 同一轮把复活推上去再拉回来,touched_words 含该词 ⇒ 触发 reconcile ⇒ 它作为最旧的当场再次淘汰)。离线时不自愈,6 条会一直挂到该词下次插入。
优先级判断:要修的是第 2 处,不是第 1 处
封顶越界是瞬态且在线自愈的,没有数据损坏; 谎报成功不自愈 —— 用户会以为自己收录了一条语境,而它根本不在池里。
三条修法(未裁定,倾向 B)
| 做法 | 代价 | |
|---|---|---|
| A | 复活分支也过封顶 | 复活的行本身就是最旧的 ⇒ 会被当场砍掉 ⇒ 等于「复活无效」,UI 反而更该说「没收录」。治标不治本 |
| B | 复活时刷新 created_at(当作一次新入池) | 🟢 与既定意图一致 —— insert_cloze_context 的注释原话就是「语境池滚动更新,倾向保留近期读到的句」。刷新之后它是最新的、不会被立刻淘汰,「语境 +1」也就诚实了。代价:created_at 语义从「首次遇见」变成「最近遇见」 |
| C | 只改 UI:checkContextIsNew 把墓碑行也算「已在池」 | 最小改动,但用户仍然拿不到那条语境,只是不再被骗 |
⚠️ 选 B 要顺带确认一件事:created_at 是两端 reconcile 的排序键 (红线 #6d 相邻条款 / doc 23 §C3),刷新它等于改变跨端淘汰顺序的输入。 本端改完要想清楚对端会不会因此产生「同一条语境两端存活状态不一致」的窗口。
触发时机
不紧急(要同时满足「池满」+「重读旧句」才撞上)。 下次动 cloze 采集侧代码时顺手做掉;若先做了 B,记得在 docs/cross-end/ 开一条交接给 RVH(它的采集侧有同形的封顶逻辑)。
🟡 学习循环专题验收捞出的两条(2026-08-27 记录,只报未修)
来源:建 docs/verification/learning-loop.md 那轮。 判断层与「什么条件下重新处理」在该清单 §4 K2 / K6,这里只登记要动手的那部分。
本轮抓到的主缺陷(
next_review_date写本地时钟)不在这里 —— 它的修法在 RVH 侧,走交接单cross-end/40。
✅ 黄金向量的 RVH 副本靠人 cp 同步(K2)—— 已消解(2026-08-29,rvh-merge-plan T4-3)
docs/cross-end/sm2-golden-vectors.json 是三端 SM-2 行为的真相源, 而 RVH 曾读它仓里 test/fixtures/ 下的手工副本 (当时理由成立:RVH 的 CI 不能假设 RB 仓在同机并列存在)。 learning-loop-verify.sh L1 只能在事后报出漂移。
🔴 不是按上面写的修法修的,是前提没了:合仓(2026-08-28)之后两份文件同属一个 checkout,「不能假设 RB 仓在同机并列存在」自动失效。T4-3 删掉副本,Dart 测试改读 ../docs/cross-end/sm2-golden-vectors.json(cwd=rvh/),三端消费同一个物理文件。 本条原来开的方子(把 cp 接进改 JSON 那一步)没有采纳 —— 它解决的是「怎么同步得更可靠」, 而现在没有第二份可同步了。
2026-08-27 当天就制造过一次漂移(只改了两个说明字段、未动任何
expected), L1 立刻红 —— 机制按设计工作,但也当场说明了「靠人记得 cp」的成本。遗留的义务变成「别让副本长回来」:L1 已反转成负向断言(全仓不许出现第二份同名文件), L2 收紧成按字面量断言 Dart 读的是根那一份;四处反向注入全部实测变红。
🟡 单词归一的 Layer 3/4 没有跨端断言(K6)
红线 #9 的归一分四层:Layer 1/2 是数据(lampio_dict.db 的 lemma_* 表, 红线 #10 byte-equal 已覆盖,是绝大多数流量),Layer 3/4 是两端各一份算法代码。 目前只有短语归一有跨端回归集(rvh-phrase-normalize.csv), 单词归一的 Layer 3/4 分叉不会有任何东西变红。
修法:与短语那条同构 —— 造一份跨端 CSV 回归集,两端各自消费。 触发时机 = 出现第一例「同一个词在两端归一成不同 PK」时 (症状:sync 端 backfill skip + log::warn)。
🟡 隐私与鉴权专题验收捞出的三条(2026-08-27 记录,只报未修)
来源:建 docs/verification/privacy-auth.md 那轮的实测。 判断层与「什么条件下重新处理」都在该清单的 §4 已知未修,这里只登记要动手的那部分。
① ✅ get_user_count / get_registration_trend 对 anon 开放 EXECUTE —— 已修(2026-09-01)
get_user_count / get_registration_trend 对 anon 开放 EXECUTE两个 SECURITY DEFINER 函数读 auth.users,而 supabase/sql/admin-analytics-rpcs.sql 首行写着「仅供 admin service_role 调用」——声明与部署态不符。
2026-08-27 实测(带公开 anon key 直打 PostgREST): get_user_count → HTTP 200 + 注册总数;get_registration_trend → HTTP 200 + 逐日趋势。 anon key 随桌面端二进制分发,是公开值,所以这等于公网可查。
泄漏的是业务指标(注册数与趋势),不是用户 PII —— 没有邮箱 / user_id / 学习内容, 故未当场停下来改。但它是「DDL 注释里的意图从未被机械核对过」的实例,值得修。
修法(照抄同文件里 bump_quota 的写法):
sql
revoke execute on function get_user_count() from public, anon, authenticated;
revoke execute on function get_registration_trend(integer) from public, anon, authenticated;⚠️ 修完必须同步收紧 scripts/privacy-verify.sh 的 P5 基线(把那两个名字从 BASELINE 删掉), 否则 P5 会因为「集合不再等于基线」而变红。那条断言刻意做成集合相等而非「集合为空」, 正是为了让「修好了」也必须回来改一次账 —— 见清单 §4 K1。 改完顺手确认 admin 侧仍能调(它走 service_role,不受影响)。
✅ 2026-09-01 执行完毕(生产 DDL,用户批准):
| 改前 | 改后 | |
|---|---|---|
has_function_privilege('anon', …) | 两个都 t | 两个都 f(service_role 仍 t) |
| 公开 anon key 直打 PostgREST | HTTP 200 [{"count":7}] | HTTP 401 42501 permission denied for function |
| service_role(admin 走这条) | 200 | 200,未受影响 |
三处一起改的,缺任一条都会回退或变红:
- 生产库执行两条
revoke execute … from public, anon, authenticated(事务内先看权限表再 commit); supabase/sql/admin-analytics-rpcs.sql末尾补上同样两条 ——CREATE OR REPLACE FUNCTION不保留后来的 revoke,只改库不改文件的话,下次重跑那个文件就把洞还回去了;privacy-verify.sh的 P5BASELINE清空 + 注明「空基线≠空断言」(n_def守卫钉住 public 里确实解析到了 definer 函数)。反向注入实证:把任一名字塞回基线 → P5 立刻 FAIL=1。
④ ✅ 生产库连接串是 IPv6-only 主机 —— 已解决(2026-08-27 当日)
2026-08-27 实测:db.jdtbyteiwnciqnfppztz.supabase.co 只有 AAAA、没有 A 记录, 本机无 IPv6 出口时 getaddrinfo 直接失败(could not translate host name)。 同一次会话里先能连、一小时后连不上 —— 说明那条 IPv6 路径时通时不通。
受影响的不止验证脚本(都读同一个 Keychain lampio-supabase-db):
scripts/ops-verify.sh · scripts/sync-verify.sh · scripts/privacy-verify.sh
scripts/db-audit.sh · scripts/backup-supabase.sh ← 备份也在里面🔴 backup-supabase.sh 在列,这条才是重点。 备份是 dead man's switch, 而运维看板的「本机备份心跳」正是靠它推进 —— 一段 IPv4-only 的网络期 = 备份静默失败。 (心跳陈旧会被 ops 巡检抓到,所以不是无人看守,但根因一直没被指认。)
已修:Keychain lampio-supabase-db 换成 postgresql://postgres.<project-ref>@aws-1-<region>.pooler.supabase.com:5432/postgres (session mode,IPv4 可达)。区域不用去 Dashboard 抄 —— supabase/.temp/pooler-url 里本来就有,CLI link 时写下的。原直连串备份在 Keychain service lampio-supabase-db-direct。
切换前逐项验过 pooler 支持全部用法(pg_policies / storage.* / pg_proc / cron.job / pg_dump -Fc --schema=public 备份同形);切换后 privacy 16/16 · sync 14/14 · ops 24/24(M4 从 SKIP 恢复)· db-audit 全绿 · backup-supabase.sh 真跑一次成功。
⚠️ security add-generic-password -U 会更新备注却不更新密文 —— 实测踩过一次 (icmt 变了、密码没变,看起来像没执行)。可靠做法是 delete 再 add,且写完要回读验证。
🔴 不要走「SOCKS 代理 + 本地 TCP 转发」那条路 —— 已实测是死路。127.0.0.1:7897 对 db.<ref>.supabase.co:5432 的 SOCKS5 CONNECT 返回 rep=0(成功), 绑定地址却是代理自己的 127.0.0.1:7897;紧接着发一个 PostgreSQL SSLRequest (int32 len=8, int32 code=80877103)过去,一个字节都收不回来,直接 EOF。 也就是说那个 rep=0 是乐观应答,隧道根本没建起来(代理自己多半也没有 IPv6 出口)。
⚠️ 这条本身就是本轮的一个教学样本,与 K7 同源: 「拿到了成功返回码」不等于「真的通了」。 只看 rep=0 就会得出「代理能到, 加个 TCP 转发即可」的结论,然后在一条死路上耗时间。要验通不通,得让数据真的走一趟 (这里就是那个 SSLRequest 往返)。
(docs/verification/ops-monitoring.md 2026-08-27 那行台账里写着「经 SOCKS5 实测能 够到该主机:5432」—— 那是同一个乐观 rep=0 造成的误判,以本节为准。)
⚠️ 改完把 5 个脚本各跑一遍确认,尤其 backup-supabase.sh(它是唯一会写的那个)。
② 🟢 加 server-only 包做构建期硬阻断
客户端误引服务端模块(supabase-server 持 service_role),当前只有两道事后网: privacy-invariants.spec.ts 扫源码意图、no-secret-leak.spec.ts 扫下发字节。 两道都做过反向验证、都有效,但都不是「根本编不过」。
import "server-only" 放进 lib/supabase-server.ts 等模块头部即可。 不急:下次动 lib/supabase-server.ts 时顺手加。
③ 🟢 admin/CLAUDE.md §3 红线编号缺 #8
编号从 7 直接跳到 9。红线是唯一真相源,一个洞会让读的人怀疑自己漏读了一条。 下次编辑该节时确认:是补一条回去,还是重排编号并在 CHANGELOG 留一句。
🟡 预装库把两个仓撑到 70%/78% —— 不上 LFS,盯 100 MB 硬限(2026-08-26 分析)
src-tauri/assets/lampio_dict.db 每轮 reseed 整包重提交(红线 #10 要求它进仓)。实测:
| RB | RVH | |
|---|---|---|
.git 总大小 | 209 MB | 418 MB |
| 其中该资产的历史 | 146 MB(70%) | 326 MB(78%) |
| 版本数 | 20 | 29 |
| 当前单版本 | 62.7 MB 工作区 / 21.5 MB 打包 | 同一份 |
SQLite 版本间几乎不能 delta(939 MB 解包 → 146 MB 打包 ≈ 纯 zlib 的 2.9×), 每个新版本永久 +21.5 MB,不随时间摊薄。
裁定:不上 LFS
四条理由,后两条是本项目特有的:
- LFS 不缩已有历史。 「从今往后用」= 146/326 MB 原地不动;要回收只能
git lfs migrate --everything,那会重写全部 commit SHA —— 仓里 10 个 tag、release.yml正是靠v*tag 触发且已发过 Releases,双远端都要 force, cross-end 文档里引用的 RB SHA 全部失真。不可接受。 - 成本会从「一次性磁盘」变成「每月带宽」。 GitHub 免费 LFS = 1 GB 存储 + 1 GB/月带宽。现有 20 版 × 62.7 MB ≈ 1.25 GB,迁移一上来就超存储配额; 带宽更糟 ——
ci.yml+ci-rust.yml每次 push 各拉一份,release.yml还是多 OS matrix,一次发版约 250 MB,免费额度撑不到 5 次。超了就是 $5/月/50GB 订阅。 - 现在这 146 MB 几乎不产生运营成本。 工作流里没有任何
fetch-depth覆盖,actions/checkout默认fetch-depth=1—— CI 从不下载历史,只取单个 commit 的树。 历史体积只有「全新完整 clone」才付,对单机开发是罕见的一次性成本。 这一条基本抽掉了紧迫性。 - 🔴 LFS 会开一个静默假绿的口子 —— 2026-08-29 已补上守卫,但要看清补的是哪一段。 没装 LFS(或 smudge 失败)的 clone 拿到的是 ~130 字节 pointer 文本。原文列的两条假绿路径 现在状态不同:
「这条已经不存在了:红线 #10 由「两端字节相等」升级为「只有一份物理文件」 (T4-5),sync-rvh-vocabulary.sh照样 cp + 比 SHA,两边都是 pointer 时报 byte-equal ✅」cross-end-check.sh§B 换成结构断言,那个脚本也不再 cp。- 「Tauri 打包把 pointer 塞进安装包 → 发出去的 app 没有词典,全程零报错」—— 这条曾经真的没人挡(
release.yml至今没有任何资产校验)。现由scripts/check-vocab-asset.sh覆盖:它用sqlite3读 4 表白名单 +lemma_*行数,pointer 文本会当场炸。两个执行点:ci-cross-end.yml(改动命中 paths 时)与/releaseskill 的 Step 1.6 / 护栏 #9(发版必经,硬 gate)。 ⇒ 这一条不再是「上 LFS 的阻断理由」,但仍是「上 LFS 之前必须先确认守卫在场」的前置。 ⚠️ 顺带记:同型的失败还有一种,与 LFS 无关 —— Windowscore.symlinks=false的 clone 会把rvh/assets/databases/lampio_dict.db落成 40 字节路径文本(红线 #10 的 Windows 段)。 今天无破坏面(RVH 不在 Windows 构建),但它和 pointer 是同一个形状:一个看起来在、 实际不是那个东西的文件。 而git lfs本机现在根本没装。
该做的第一件事不是 LFS,是别一轮提交多个版本
2026-08-26 一天提交了 3 个 db 版本(e9b53f3 / a5ac7d9 / 5df7cf8),逻辑上是 一轮改库,打包成本 3 × 21.5 = 64.5 MB;合成一个 commit 就是 21.5 MB。 3 倍差距,零基础设施改动。 2026-06 那 13 次同理,大多是 pipeline 迭代的中间态。
⇒ ✅ 已写进 /vocab-reseed 的护栏 #4 + Step 9 收尾(2026-08-26)。并行会话共用工作树时 --amend 有风险,那种情况改用「全部载荷落完再一次性提交 db」。
两条 trigger(满足任一才重开这个话题)
| # | 触发条件 | 到时候该做什么 |
|---|---|---|
| T1 | 单版本逼近 GitHub 单文件硬限 100 MB(今天 62.7 MB;轨迹 8 → 52.7 → 62.7,照这个走还剩约 2–4 轮大扩容) | 这才是真正的倒计时。GH001 那个 50 MB 只是警告、可以无视;100 MB 是 push 直接被拒 |
| T2 | 需要多机/多人协作,或全新 clone 的耗时真的开始碍事 | 重新评估,但仍不是 LFS —— 见下 |
真撞到 T1 时,对症的也不是 LFS,而是把资产挪出 git(独立 asset 仓,或 Release 附件 + 构建期下载)。那样同时避开配额与 pointer 陷阱,代价是要给红线 #10 的 byte-equal 校验换一个不依赖工作区文件的实现(比如两端各自校验对同一个已发布 artifact 的哈希)。
不放进 /admin/ops「容量」卡(2026-08-26 裁定)
一度考虑把这个倒计时做成运维面的一张卡,否掉了,四条理由:
- 失效形状不对。 那个面板存在的理由是抓「取不到 ≠ 正常」这类静默假绿 (
ops-verify.sh头注释自己列了四处假绿)。而超 100 MB 是git push当场被拒、 报错清楚——最响亮、最阻断的那种失败,恰恰是最不需要看板的。 - 延迟不对。 这个数只在我跑 reseed 时才变。该看的时机是改库那一刻, 不是某人偶尔打开一个页面的时候。
- 读者不对。 admin 部署在 Vercel 上给运营看,根本碰不到 git 仓 —— 要拿这个数就得新接 GitHub API + 一份凭据,为的是一个只有跑 reseed 的人才在意的数字。
- 会稀释那一页。
ops/page.tsx自己的注释就写着,它与/observability分开正是 为了不让「推荐池够不够健康」和「备份是不是停了」互相稀释。「git 仓里某文件多大」 离「备份是不是停了」更远。
⇒ 正确的家是那个每轮 reseed 必跑的脚本:它已经在打印 SHA 和行数,加一行 「超过 80 MB 就警告」即可 —— 正是 ops-verify.sh 头注释主张的 「散文里的数字必然漂移,断言不会」。 ⇒ ✅ 已落(2026-08-26):每次跑都打印体积与剩余余量,≥80 MB 告警 · ≥100 MB 判死。两档都做了注入式反向验证。 ⇒ 🔴 2026-08-29 换了家(rvh-merge-plan T4-6):那个脚本原名 sync-rvh-vocabulary.sh, 它「必跑」是因为要 cp 给 RVH;T4-5 把 RVH 那份改成 symlink 之后 cp 没了,「必跑」也就没了。 脚本 git mv 成 scripts/check-vocab-asset.sh(只留体积闸 + 卫生断言), 并接进 .github/workflows/ci-cross-end.yml 换一条真正的必经之路。 ⚠️ CI 挡不住这一条:≥100 MB 时 push 本身就被 GitHub 拒,CI 在 push 之后才跑 ⇒ /vocab-reseed Step 3 那次提交前的人工执行仍是主闸。
🟡 校准上限 B2 解封 —— 闸门 1 已清,只卡闸门 2 了(2026-08-26 更新)
round30 把 vocab_scope 排除集从 584 做到 769,CALIBRATION_MAX_LEVEL 仍是 B2。两个闸门:
闸门 1(小,可枚举)—— ✅ 已清(2026-08-26):C1/C2 且 cefr_inferred=1 的可学池里 那 9 个露骨/网络垃圾词(blowjob bukkake creampie cunt deepthroat footjob handjob milf trackback)已随上一条那轮改库一并删掉,可学池 857 → 848。 calibration.ts:36 注释里「清池子先于解封」指的就是它们。 📌 补一句 2026-08-31 才看清的机制:那一轮同时还从资产里删了 53 个词(round30 主 delete_words 载荷),而 seed 合并是 UPSERT(只增改不删)⇒ 存量用户一行没少。 闸门 1 的 9 个当时补了 helpers.rs 的 v27 prune 才真的送达;主载荷那 53 个漏了, 2026-08-31 补 v29 prune(含 bangbus/hentai/porno/sexo/vaginas 等 8 个露骨词)。 细节与守卫见 proper-noun-governance-plan.md §4.5「② 真因与 v29 prune」。
闸门 2(大,判据形状)—— 仍在:C2 首屏 20 词里仍有 10 个人名 (ajax austin barney barr barry beck benedict benny 等)。它们在库里带 verb/adj/prep 词性 (to beck=招手、to barney=争吵),被 vocab_scope 判据的第二半放行 —— 而那一半正是 fox/march/turkey 不被误杀的安全网,模块注释早把这类列为「刻意接受的假阴性」。 继续补标无效,瓶颈在判据形状。
🔴 2026-08-31 订正:此前这里写「对症的是 L-D 的 token 级信号」—— L-D 已落地,但它解不了闸门 2。 L-D 的 B 类原计划里那半「非句首 + 首字母大写 ⇒ 该处不高亮」被实测推翻(会抑制中位 12.1% 的高亮实例,其中只有 9.7% 落在真专名上 = 在 token 级重演 fox 那个误杀), 落地的是非破坏性提示:只在查词弹窗上标「这里可能是专名」。 它作用在 content-script 的 surface 上,跟快速分级的候选池(Rust 侧 SQL)不相交 —— is_pure_proper_noun 全仓唯一调用方是 dictionary.rs 的查词路径。 ⇒ 那 10 个人名一个都没少,闸门 2 原样还在。
⇒ 解封 B2 只卡闸门 2,而闸门 2 的钥匙在 L-C 那边,不是 L-D。 在 L-C 落地之前不要动 CALIBRATION_MAX_LEVEL。
🔴 2026-08-31 再订正一次:闸门 2 也不只是「那 10 个人名」,补标同样解不了 C2。 实测 C2 档 519 个词权威分级 = 0 个(全部 cefr_inferred=1),噪声率 B2 ~10% · C1 ~30% · C2 ~73%。清掉人名之后 C2 剩下的还是词频反推的 alprazolam / batterie / phentermine / clon / outgo 这类。⇒ 解封 C2 需要的是给这一档引进权威词源,不是清理; 而 C1 只要补标 + 判据到位就能干净(549 个权威 C1 词实测探针是 abortion amid casualty configuration critique diplomatic encompass franchise induce justification)。 ⚠️ 「把主动推送池收紧到 cefr_inferred=0」这条路已被实测否掉(会砍掉 A2–B2 里 cellphone/clipboard/enzyme/reboot 这类权威表覆盖不到的现代高频词), 详见 plan §0.7 第 5 条 —— 别再提这个方案。
🟡 专有名词治理 —— 已立项,内容全量迁至 plan(2026-08-25)
📄 唯一真相源 =
proper-noun-governance-plan.md。 病根(vocabulary一张表同时当「可查 / 可学 / 计难度」三个角色用)、四类症状的实测证据、 已排除的三种错误解法、四层方案,全在那里。本条只留指针,不要在这里再抄一份。
已完成:L-A(运行时判据统一)· v29 prune(2026-08-31,round30 删除载荷送达存量用户)· L-D 两半(2026-08-31 —— ① 过滤器退役:reference_words 黑名单停止参与任何查询过滤;② 产品出口:查词弹窗对专名「止损折叠 + 按需补背景」, B 类收窄为只提示、不动页面高亮)。L-B 实施中推翻,并入 L-D。纯 RB,无 RVH 交接。 实机验收 2026-08-30/31 补完(此前只有对真库跑 SQL 的机械层):§4.3 四条 + §4.5 ③ + ①④
- A/B 类九词矩阵 9/9。⇒ 只剩 L-C 未开工。
⚠️ 「命中 584 词、误杀 0」这句今后要带两个作用域(2026-08-30 实机订正): ① 584 是快照相关量 —— 本机 dev 库实测 770(标签覆盖 1058 vs 871)。引用方向,别引用数字。 ② 「误杀 0」原本只在判据层成立 —— 用户在页面上看到的误杀曾不是 0:fox passes-predicate,却仍被 reference_words 黑名单挡住。✅ 2026-08-31 已修(黑名单退役), 实机 DOM 里 fox 现在是 rb-discover-b2、存进生词本后是 rb-highlight-b2。 详见 plan §2.4 裁定 + §4.5「①④ 结案」。
留在 backlog 视野里的原因 —— plan §6 只剩 L-C 未排期,且它卡着别的事:
| 后续阶段 | 卡着什么 | 触发条件 |
|---|---|---|
L-C pipeline 收词闸门(保留 name POS 证据 + 清露骨/垃圾词) | 校准 B2 封顶解不开;双击 Virginia 仍给粗俗释义 | 下次整库 reseed 时顺手(别为它单独重跑)。⚠️ 成本模型 2026-08-31 订正:不需要整库重跑(译文那 $5-15 用不上),真正的成本是改资产 = 一个 65MB blob(pack 约 21.5MB);且 plan §6 L-C 的第 4 条形状已改(C2 无权威词源,清理解不开它) |
| — | ✅ 2026-08-31 全部交付,见上「已完成」。双击 Kyiv 现在能拿到「乌克兰首都…」(按需,点了才调);March/Polish 同形碰撞走提示而非硬过滤。⚠️ 别再把「误杀未修」「Kyiv 给词典释义」当作任何条目的前提 |
⚠️ L-A 完成 ≠ 校准可解封:L-A 依赖 proper_noun 标签,总有一批专名没被标,钥匙在 L-C。 📌 但那串具体的词不要再照抄(2026-08-30 实机订正):本机 dev 库里 virginia/manchester/todd/claire 是带标签的、已被滤掉,8 个典型样本只漏 peter/york 两个。标签覆盖率随库快照走,结论方向不变,样本清单会过期。
实机验收另捞出两条(正文在 plan §4.5,这里只留指针):
- 🟡
bangbus(成人网站品牌)出现在快速分级 C2 首屏 20 词里 —— §0.5 症状 4 的 第一个实机实例。不是专名判据能碰的东西,对症的是 L-C 的 pipeline 收词闸门, 下次整库 reseed 时一并处理。 🟢 改造点 #7(—— 已结案(2026-08-31 补跑)。 真因是赶上两批推荐之间的空窗(上游analyze_article_bodies)实机未被覆盖expires_at全过期 + 本地先DELETE再插 ⇒ 清成空表), 不是本机配置。补跑后路径确认活了,但改前/改后档位分布完全相同 —— A1 用户读 A2–C1 新闻时超纲率远高于 5% 阈值,档位对本改动饱和。 ⇒ 修正了「各档一致下修」的读法:那说的是比值,不是档位。 详见 plan §4.5 ③。
🟡 lemmatizer v26 资产修正的三条尾巴(2026-08-18 记录,主体已完成)
主体见 docs/cross-end/24-rb-lemmatizer-asset-v26-reseed-handoff.md 与 CHANGELOG。 留下三条明确边界内的尾项:
- 存量数据清理 —— 不落本地迁移(2026-08-18 裁定:内测用户会重装),只剩云端一次性清理。 ⚠️ 重装清不掉这批脏数据:
rath/alway这些行同时在 Supabase 的user_*表里, 重装后重新登录,sync pull 会原样拉回来;而且拉得进来——本轮没动vocabulary,rath仍是预装库 headword。(桌面端「重装」也未必清本地:macOS 换掉.app不会删~/Library/Application Support/<identifier>/lampio.db。) ⇒ 待做的只有一件:按白名单docs/cross-end/24-affected-lemmas.tsv在 Supabase 上 软删user_learning_entries/user_known_words/user_word_page_links/user_word_cloze_contexts的对应行(内部账号少,成本很低)。 🔒 用软删不用硬删:墓碑不是给云端看的,是给对端设备看的——硬删后,任何还没重装的 设备本地那行没有墓碑,下次 push 会把它重新上传回云端。详见交接 §9.1b。 - 预装库
vocabulary表的 91 个死 headword(alway/rath/lat/ye/opus…) 与缺失的正确词条(always/rather/during…暂走缓冲池兜底)。下次整库 reseed 时自然收敛——届时 wordlist 会用新资产归一。别为它单独重跑 pipeline:output/中间产物已丢失,整库重跑 = 为 ~12k 词重新付费 LLM 翻译(约 $5-15)。 - 7 条 POS 歧义映射(
bit→bite、ground→grind、clad→clothe等)。两读都成立, 定这类需要上下文,而 Layer 1 是无上下文查表——改成任一边都会在另一边制造等量的错。 真要治得引入 POS 标注层,属独立设计问题,不是本轮的遗漏。
🟡 预装库 C1/C2 档被同形异义词污染 → 校准上限被迫封在 B2(2026-08-12 实机发现)
📍 2026-08-25 定性升级:本条是
proper-noun-governance-plan.md§0.4 的症状 3, 不是独立问题。那里还查出同一池子里有露骨词与网络垃圾(症状 4), 使「解开 B2 上限」从精度问题变成必须先清池子的硬前置。先读那条再动这条。
触发条件:想解开水平校准的 B2 上限时;或下次跑 tools/vocabulary_builder_v3/ pipeline 时顺手。
现象:按 frequency_rank 取 C1/C2 档的头部,拿到的几乎全是垃圾:
C2: peter · pmid · todd · claire · mariah · caput · cote · diazepam · tyne · facesitting
C1: abortion · charter · decision-making · escalate · insertion · devel · manchester · situate · trustee · cant根因:frequency_rank 是词形级的,primary_cefr_level 是词义级的,二者对同形异义词 必然打架 —— paul 在语料里高频是因为人名,而被标 C2 的是「古意大利银币」那个义项 (实查 pos_definitions 确认:paul=古意大利银币 / sharon=UK 贬义俚语 / virginia=粗俗语)。 word_tags 的 proper_noun 只盖住 127/483(C2)、118/1261(C1),剩下的靠标签抓不到。
当下的处理:校准流上限封在 B2(lib/calibration.ts::CALIBRATION_MAX_LEVEL,常量注释里写了 完整推导)。不封的话:B2 用户上跳 C1 看到 manchester、照实答「认识」→ 再上跳 C2 看到 peter → 又全「认识」→ 被判 C2,推荐从此全给他 C2 文章。封顶的代价是真 C1/C2 用户被判 B2 (推荐略简单一档),远小于反向错误。
根治(pipeline 侧,不在本仓运行时):给 C1/C2 档的词补 proper_noun 标注,或对 「高频词形 + 低频义项」的组合降权/剔除。做完后把 CALIBRATION_MAX_LEVEL 放开到 C2 并复验探针词。 ⚠️ 预装库是双端 byte-equal 资产(红线 #10),改它要走 /vocab-reseed 全流程。
🟡 发现页问候语下的个人化状态行(2026-08-14 议题 B,方向 ✅ / 原文案 ❌)
触发条件:真觉得发现页开场冷、要暖场时;或做「推荐流个性化 V2 画像」时顺手(两者共用信号)。 纯 RB,零 RVH。 完整推导见 §2.2〈2026-08-14 二次复评〉B 节,此处只留落地形状。
要点(三条,缺一就会做出一句失真的文案):
- 不能用
top_tags:它来自rss_feeds.tag= 「你订阅了什么」,不是「你读了什么」 —— 订了没读的用户会被告知「你常读科技类」。页头是断言,不是行内猜测,容不下这个失真。 - 必须加时间窗:
top_domains/top_tags现在都是全量累计,说「最近」就是假的。 最小改动 = 给get_reading_interest_signals加天数参数,走reading_log.created_at >= now-30d。 - slogan 处置 = 条件替换:无信号 →
hpGreetingSub(2026-07-24 主叙事对齐特意定的品牌叙事); 有信号 → 换成状态行。不并列两行(会把品牌叙事稀释成装饰)。
最小可诚实版本 = 30 天窗 + top_domains + 条件替换,文案退到「你最近常读 X 和 Y」。 想说「主题」就得先给读过的内容打标(不是给订阅打标)= V2 画像的活,不是一句文案的活。
🟢 App 内加「服务条款 / 隐私政策」入口(2026-08-11 记录,与既有条目合并)
与本文件既有条目 §隐私政策收尾残留 #2 是同一件事,此处补充技术前提,实施时合并。
landing 侧 /[lang]/terms 与 /[lang]/privacy 已上线,缺的只是 app 内入口。
⚠️ 技术前提:src-tauri/Cargo.toml 里既无 tauri-plugin-opener 也无 tauri-plugin-shell ——当前 app 没有"用系统浏览器打开外部链接"的能力。两个选择:
| 做法 | 评价 | |
|---|---|---|
| (a) | 加 tauri-plugin-opener → 系统默认浏览器打开 https://lampio.app/zh/terms | 推荐。法务页的正确归宿(可保存/打印/分享)。但要动 Cargo.toml + capabilities(CLAUDE.md §6「环境配置」类,需确认) |
| (b) | 零新依赖:app 内开普通 content tab 指向该 URL | 会被 CEFR 高亮 / 查词全套增强层处理——在法务文本上双击查词,观感很怪 |
落点建议:Settings「应用」小节(版本号旁)+ AuthPanel 注册表单附近 (注册时同意条款是常规做法,法务上也更该出现在那里)。
⚠️ 与 popup-and-entry-points-plan.md T5a 的联动:AI 语境助手改默认开后, 隐私政策文案必须同步修改(现文写的是"当你主动使用")。若两件事同期做, 一并部署 landing。
🟢 设置里加「清除本机缓存」显式入口(2026-08-06 记录)
原「快照只上传不下载 / library 未按用户隔离」两条的 P3 残留 —— 两条本体已于 2026-08-06 落地(docs/plans/archive/snapshot-restore-and-user-scoped-library-plan.md)。
真有隐私顾虑的用户应该能自己决定清掉本机缓存的正文,而不是由我们替他删 (换用户时删文件已明确否决:正文是唯一副本,切回来会变成「行在、内容没了」)。 clear_web_cache 命令已经按用户隔离、只清当前用户的 web/,缺的只是设置页的入口
- 说清「云端还有副本,下次打开会重新取回」这层预期。
🟡 待发现词 Top-N 排序 v2(roadmap 3-2 落地后的遗留,2026-08-04)
v1 as-built:
docs/plans/archive/discovery-topn-ranking-plan.md(migration v31word_encounters+lib/discoveryRank.ts+DiscoveryWordChannel.tsx+ CTR 遥测三事件)。以下四条 v1 明确未做。
- 词族联系作第五维:
vocabulary.word_family已填 4408 词但没进打分。可表达 「你已经会 X,这个是它的变体」——对"值得先看"是很强的信号,且零新数据。 - 精确跨页去重:v31 是聚合表 +
last_page_url单值去重闸,A→B→A回访会重复计数 (已声明接受)。实机实测来回切几次就把共现词刷到×5,涨得比理想快。若日后虚高到 影响排序观感,再考虑 rolling bloom / 近期 N 页 URL 环。 注意:换精确去重就要付明细表 ~1M 行/年的代价,别轻易翻这个决定。 - 候选池噪声(实机新发现):Latin 页 Top-9 里进了
ad("AD 800" 日期)/cite("does not cite any sources" 引用模板)/york(专名)。不是排序 bug —— 恰是权重在如实工作 (这些词页内频次高、且frequency_rank常用 →general维度奖励它们)。病根在候选池卫生: 专名该被滤掉、Wikipedia 模板文字该排除。别用调权重去压 —— 会连带压掉 真正有用的常用词。 ⚠️ 也别往reference_words里加词(本条原文的写法,2026-08-31 订正):那张黑名单 已退役,实测 193 行里 17 行是误杀。专名的对症手段是db/vocab_scope.rs的运行时谓词 (已落地)+ L-C 的预装库补标(未开工)。 📍 2026-08-25:候选池卫生已单独立项 —— 见proper-noun-governance-plan.md(症状 1/2)。 那里给出的 L-A 判据(proper_noun标签 + POS 只有 noun)实测命中 584 词、误杀 0, 正是这条要的「候选池卫生」,落地后回来复验ad/cite/york是否还在 Top-9。 - admin 聚合卡:CTR 三事件(
discovery_list_shown/_word_click/_words_saved) 目前只能在 admin observability 的原始事件列表里看,没有聚合视图。攒够数据后补。 - 权重调参:
WEIGHTS(pageFreq 2 / encounters 1.5 / general 1 / heading 0.5)与封顶值 5 是凭产品直觉给的初值,没有数据支撑。攒够遥测后应回看——这正是当初埋 CTR 的目的。
🟢 三档阅读净度 —— 实测 3 条(2026-08-04 记录 → 同日排查收口)
来源:commit
737e4e8(reader 进出 vestigial 可变间接层清理)的交互实测会话顺带捞出。 三条都与该 commit 无关 —— 737e4e8 已验证零行为变化。⚠️ 2026-08-04 二次会话结论:原记录的 3 条里有 2 条的归因是错的。 下面已按实测证据改写;原始(错误的)表述见 git history。
✅ 三条已全部收口(#1 修复 · #2 判定非缺陷 · #3 修复 + 4 站回归通过)。 排查过程、as-built 修法、工具链配方(含「osascript 驱动本 app 不可用」的更正、 「dev app 会自行干净退出」的踩坑)见
~/reading-browser/docs/plans/archive/read-tier-followup-handoff.md。
1. ✅ 已修复:档位指示器在换页后说谎(原记录归因为「CSR 冲掉 calm 标记」,错)
真因(与 CSR 重渲染无关):usePanelStore 的 readerActiveLabels / calmActiveLabels 以 webview label 为键,且只由 content-script 的 report 事件驱动。 换页会重建 content-script(两个 flag 归 false),却不会发出任何 exit 事件—— exitReaderMode / exitCalmMode 开头都是 if (!flag) return;,新页面上压根执行不到那句 report。于是旧页的档位标记永久粘在该 label 上。
实测复现(deterministic,2026-08-04):calm 页(en.wiktionary.org)→ 同标签页点跨域 链接到无 pref 的 wikipedia.org → 新页 body.rb-calm = false、[data-rb-calm] = 0, 但工具栏图标仍是 lucide-feather 且 aria-pressed="true"。原记录里「三个信号都说 calm 开着、页面却零标记」的现象由此而来,不需要任何 CSR 重渲染参与。
原记录的两处错误:
- ❌「
enterCalmMode幂等守卫让再选一次 calm 变 no-op、卡死」——不成立。新页面上isCalmMode本就是 false,守卫不拦;实测再选 calm 会正常打出[RB-CALM] enter: hid=0 host=example.com并加回body.rb-calm。用户感到「点了没反应」 大概率是因为 calm 在这些页上只折叠了 2 个空广告位(hid=2 (empty=2))=几乎不可见。 - ❌「全程无 late-scan 日志 ⇒ observer 失效」——observer 压根没启动过(
startCalmObserver只在enterCalmMode内调用,而新页面从未进过 calm)。
修复:content-script 每页 ready 时无条件重报真实档位 (calm.js::reportReadModeState,index.js 在 applyDomainReadMode 之前调用), 沿 detectHeavyPage「两态都报、每页自愈」既有先例。已实测验证:跨域换页后图标正确回落 layout-template / aria-pressed=false;且 reload 有 pref 的页面时 「先报 false、auto-enter 再报 true」终态仍正确,无回归。
残留(未处理,低频):SPA 前端路由换页不会重建 content-script,档位标记与
data-rb-calm都会跟着旧 DOM 走。本次实测未复现(Vox 站内链接实际走的是硬导航)。
2. ✅ 不是缺陷:⌘⌥R 实测两条入口都通(原记录归因错误)
原记录称「events.js:134 是 ⌘⌥R 的唯一处理器,React 侧没有 r 绑定(已 grep 确认)」—— 该 grep 结论是错的。React 侧兜底完整存在且已接线: useKeyboardShortcuts.ts:82(e.altKey && e.code === 'KeyR')→ useNavigation.ts:222onToggleReader → toggleReaderMode(webviewLabel) → reading.rs toggle_reader_mode。
实测(2026-08-04):从原生菜单进 reader 后直接按 ⌘⌥R —— 成功退出;auto-enter 进 reader 后按 ⌘⌥R 同样成功。两条入口都通。
events.js:128-133 的注释也是准确的:它说的是「当 content webview 持焦时 快捷键到不了 React」——解释为什么 content 侧需要第二个处理器,并没有声称 React 侧没有处理器。 原记录把它读成了「暗示有兜底但实际没有」。注释不用改,代码不用改。
3. ✅ 已修复:Readability 把页头 cruft 收进正文(stripPageChrome)
取样(修前,2026-08-04,4 站全部本人实测 reader 实际渲染结果):Wikipedia + Vox 命中 (页级 banner 区被收进正文:skip 链接 / 站点 logo / 与 h1 重复的标题 / 栏目导航), The Verge + AP News 干净。2/4 —— 不是个案,值得治;也不是通病,动手要保守。
修法:reader.js:110-155 stripPageChrome,在 Readability 打分前作用于克隆树, 两条独立规则对应两种形态 —— R1「无 <p> 的 <header> 一律删」(治 Wikipedia 的 main > header.vector-page-titlebar;带 <p> 的 article > header 保留 = 安全闸)、 R2「从 skip 链接向上爬到 banner 容器、止步于会吞掉 main/article 的那一层」(治 Vox —— 它没有任何 <header>/[role="banner"])。两条提取路径都挂(:207 / :479)。
与原「建议修法」的偏差:R1 没有按原建议「限
article/main之外」——vector-page-titlebar恰恰在main之内,按位置过滤反而治不了它;改用「有无<p>」 作安全闸,既保住 dek/byline 又能治 Wikipedia。
回归验收(2026-08-04,4 站全过):Wikipedia 225 段正文 / Vox 6 / Verge 7 / AP 26, 四站 <header> = <nav> = 0,文本级 jump to content / skip to main / vector-page-titlebar / site-header 等全 0 命中;Verge/AP 保持「干净」→ R2 没误伤。
实现落在 commit
43bf0d1,但那条 commit message 只写了第 1 条(reportReadModeState) —— 代提时只 diff 了calm.js就下笔。按 message 查这 52 行会查不到。
已知残留(2026-08-05 复测补记,未处理):Vox 的提取结果与提取时机有关,上面那张 回归表测的是「页面刚加载 → auto-enter 进 reader」这条路径(首节点 = hero <figure>,干净)。 但在已开很久的页面上退出 reader 再重进,Vox 会稳定残留 Vox logo · <重复标题> · Future Perfect ·(3/3 复现;Skip to main content 已被 R2 清掉,即 R2 生效,只是 logo / 重复标题 / 栏目链接与 skip 链接不在同一个容器里,R2 向上爬会先停下)。 同一 URL 刚加载时提取则完全干净 —— 说明是页面随时间变化(懒加载 / 付费墙模块注入) 改变了 Readability 的容器打分,不是 stripPageChrome 规则本身失效。 影响面小(多出 3 个短条目,正文与 dek 完好),要治得再加一条「正文首个 <p> 之前的 leading chrome 裁剪」,风险高于收益,故只记录不动手。
🟡 原生焦点会卡在 content webview → 主侧快捷键集体哑火(2026-08-04 记录,本次刻意不修)
现象:原生焦点一旦落到 content webview,就会一直卡在那里(实测:切到 Discover 模块、 content webview 已隐藏之后仍卡着)。此时只有 React 侧处理器的快捷键—— ⌘L / ⌘F / ⌘T / ⌘W / ⌘[ / ⌘] / ⌘1-9 ——全部失效,必须真鼠标点一下主 UI 才恢复 (合成点击 rb_click 不改变原生焦点,无效)。
⌘⌥R 和 ⌘+/-/0 之所以没事,正因为它们在 content-script 侧也有处理器 (events.js:116 / events.js:134)。这大概率就是上一轮误报「⌘⌥R 失效」的真身。
佐证:commands/webview.rs 里没有任何 focus 相关代码(grep focus 零命中)—— webview 焦点归还从来没被显式管理过,全靠 OS 默认行为。
修法方向(未实施):切 tab / 切模块 / 原生菜单关闭后,显式把原生焦点交还目标 webview (Tauri WebviewWindow::set_focus / Webview 级 focus)。改动面比看上去大:要定义 「哪一档场景焦点该归主、哪一档该归内容」,且两端都有 keydown 处理器时要防双触发。
用户决定(2026-08-04):本次只记录,不动 —— 留待专门会话。
🟢 观察后退役 Free Dictionary 备源(2026-08-03 记录)
背景:
lookup-or-fetch-word的上游主源当日从api.dictionaryapi.dev换成 Wiktionary(Wikimedia)。换源理由见docs/plans/archive/llm-server-unification-plan.md§上游硬化。Free Dictionary 保留为备源,仅在 Wiktionary 抖动时触发。
退役条件:观察一段时间,若备源基本没被用到就整块删掉。
怎么数(无需埋新点,现有日志够用):Supabase Dashboard → Edge Functions → lookup-or-fetch-word → Logs,或走 Management API 查 function_logs。两个标记:
[via=wiktionary]—— 主源直接命中(正常路径)[via=fallback-rescued]—— Wiktionary 抖动且 Free Dictionary 救回, 这是备源唯一产生价值的情形
sql
-- 走 https://api.supabase.com/v1/projects/<ref>/analytics/endpoints/logs.all
select event_message from function_logs
where event_message like '%[via=%' order by timestamp desc limit 500fallback-rescued 占比长期为 0(或极低)→ 删除依据成立。
删除范围(届时):fetchJsonWithRetry 对 dictionaryapi.dev 的调用、 apiData / firstEntry 那条富数据解析分支(含 synonyms/antonyms/examples 映射)、 fallbackVerdict 相关分支。删后 authoritativeMiss 判定塌缩成「Wiktionary 说没有」一种情形, 逻辑会显著变简单。
⚠️ 删之前确认一件事:同义/反义词只有 Free Dictionary 给(Wiktionary REST 不分字段)。 删掉 = 此后新回填的长尾词永远没有 syn/ant。预装库那 12,291 词不受影响(pipeline 另出)。
🟢 验证:摘录喂 LLM vs 全文喂 LLM 的 CEFR 评级偏差(roadmap 3-1 Phase 2 附属,2026-07-29 记录)
背景:3-1 Phase 2 设计定稿(
article-decision-card-plan.mdPhase 2 段)里,Layer 1 的 LLM 语体 CEFR 评级选定「批量单调用 + 每篇放宽摘录 ~2,000 词」而非全文进 LLM(理由:全文会撞便宜模型上下文上限 + 拉高延迟
- 批量 all-or-nothing 风险,且语体判断对摘录边际递减)。用户希望实测确认摘录方案与全文方案的 CEFR 结果 是否有大偏差,作独立小任务、Phase 2 实施前后择时做。
动作:取一批真实文章(覆盖 A2–C1 各档、含长文),分别用 ①~2,000 词摘录 ②全文 喂同一低成本 LLM 打 cefr_level, 比对两者档位差异分布(多少篇一致 / 差 1 档 / 差 ≥2 档)+ 差异是否集中在长文/前松后紧的文章。
- 若绝大多数一致或仅偶发差 1 档 → 摘录方案坐实,维持决策 4。
- 若长文系统性偏低(摘录漏后文难句)→ 考虑对长文改头+中+尾多段采样,或长文单独 per-article 全文。 成本:一次性抽样脚本,几十次 LLM 调用,可忽略。非阻塞——Phase 2 可先按摘录实施,此验证平行做。
🟢 macOS CI 签名/公证 + 发版 —— dev.10 已发 + 真机全绿(roadmap 1-1,2026-08-01 了结)
✅ 2026-08-01 了结:8/1 月度额度重置后,用全新
v0.1.0-dev.10tag(含 a0428fa 版本显示修复 + 遥测看板 + Phase 2 文章卡)走 GitHub CI,四 job 全绿(mac aarch64 7m37s / mac x64 9m51s / Windows 23m46s / publish 29s),公证这次一次过(Notarizing /Lampio.app → status Accepted,无 瞬时失败)。成本缓解 ① universal binary 暂缓——用户选「直接发双 arch」(额度已恢复,本次不折腾)。 两仓 publish + promote-updater 完成,endpoint latest.json = 0.1.0-dev.10(三平台齐)。 真机验证全绿:① updater dev.8/9→dev.10 检出+下载+应用 ✅;② 干净机下载首开 = 良性「downloaded from Internet → Open」框(非 Gatekeeper 拦截,"Apple checked … none detected" = 公证生效)✅; ③ 麦克风授权:reset-dev-data.sh不清系统 TCC → 旧 dev 构建的过期 mic 决定残留致「不弹」;tccutil reset Microphone com.lampio.dev+ 重启后授权弹窗正常(app 侧 NSMicrophoneUsageDescription
- audio-input entitlement + hardened runtime 逐项验齐,非构建缺陷)✅。 landing 文案已撤(本次):
dictionaries.ts/download.tsx的「未签名/xattr 绕行」块删除、chipHelp 去「未做代码签名」尾巴(保留芯片选择句)、mac 卡 note 改「首次打开点 Open 即可」、注释「mac 为未签名」→ 「已签名+公证」。releaseBody 早已「已签名+公证」。剩:Windows 仍未签名(SmartScreen 首拦,保留文案)。 遗留观察:verify-notarized脚本验错对象(见本文件顶部条目);成本缓解 ① 下次若嫌 macOS ×10 贵可再评估。以下为踩坑历史(保留作审计)。
背景:roadmap 1-1 macOS 签名/公证接线代码已验证正确(dev.9 attempt 1 日志:证书导入 ✅ /
Developer ID Application: xiangliang li (SW87SQBZ66)identity 派生 ✅ / Lampio.app 签名 ✅ / 公证已提交拿到 submission id ✅)。但两处踩坑:
踩坑 1 — 公证网络失败(transient):attempt 1 两个 macOS job 公证提交后卡 ~40 分钟,最终 -1009 No network route 打 appstoreconnect.apple.com 失败(Apple 侧慢 + runner 网络掉线)。单次重试大概率过。
踩坑 2 — 仓库 workflow 权限被降 read → Windows CREATE release 403(已修):ttfishnet/lampio 的 default_workflow_permissions 不知何时被改成 read,导致全新 tag 首个去 CREATE draft release 的 job 撞 Resource not accessible by integration。dev.8 靠「release 已存在只追加」掩盖。已 gh api PUT … default_workflow_permissions=write 修复(2026-07-28)。
踩坑 3(结构性,未解)— macOS ×10 计费太贵 → 额度耗尽:私有仓 macOS runner ×10 倍率,attempt 1 两个 mac job 各等 40 分钟公证 = ~800 计费分钟一次性烧光月度额度 → 之后所有 rerun 4 秒零 step 启动失败 (provision 被 spending limit 拦)。当前 dev.9 卡在此——等月度额度重置或用户抬 spending limit 才能继续。
成本缓解选项(下次发版前定,均未实施):
- ① 改 universal binary(倾向):macOS 从 2 job(aarch64 + x86_64)合成 1 个
universal-apple-darwinjob,一次编译产 fat binary + 一次公证覆盖双 arch。砍掉一半 macOS runner 开销。代价:gen-latest-json.mjs+ updater latest.json 的darwin-aarch64/darwin-x86_64键需改为都指向同一 universal 制品(或darwin-universal)—— 动到 updater 制品命名,需一并验证 updater。 - ② 本机 Mac 本地签名/公证发 macOS:用户 Mac 有 Developer ID cert,
pnpm tauri build本地签名+公证(免费), 只用 GitHub 跑 Windows。破坏全自动,但零 ×10 开销。 - ③ 验证期只留 Apple Silicon 一个 mac job:证明管线用,减半;正式再加回 Intel。
- ④ 镜像仓设 public(Actions 免费):与「私有镜像」设计 + git 历史含旧 anon key 相悖,不推荐。
未撤文案:landing ✅ 已撤(2026-08-01,见顶部了结横幅)。 release.yml releaseBody 已改「已签名+公证」(受 Step 5.5 闸门保护,安全)。download.tsx/dictionaries.ts 的「未签名/xattr 绕行」文案仍留着
✅ 本地端到端验证通过(2026-07-28):GitHub 额度耗尽后改本机 Mac 本地 pnpm tauri build (Developer ID cert 在钥匙串、首次 codesign 需 GUI 点「始终允许」——非交互会话会 errSecInternalComponent)。 结果:Developer ID 签名 ✅ + flags=0x10000(runtime) hardened runtime ✅ + entitlements 嵌入 com.apple.security.device.audio-input=true ✅(最关键那颗 mic key 坐实) + 公证 status: Accepted ✅
- staple ✅ +
spctl: accepted / source=Notarized Developer ID✅。配置正确性已证,无需再靠 GitHub 证。
- 公证网络:notarytool 上传不走系统代理/HTTPS_PROXY(tauri build 内嵌那次 connectTimeout);但直接
xcrun notarytool submit --wait重试即通(<1 分钟 Accepted)——中国本地公证可行,靠重试抗瞬时。 - 已签名+公证+stapled 的产物:
src-tauri/target/release/bundle/macos/Lampio.app(本机可直接开验 Gatekeeper+mic)。 - 本地凭证文件
src-tauri/.env.notarize.local(gitignored,含 APPLE_ID/APPLE_PASSWORD,可复用或删)。
剩余(原清单,均已了结 2026-08-01):
- ✅ 本地人工验证(2026-07-28):本地 Lampio.app 首开无拦 + 麦克风 TCC 弹窗 + 录音出分。
- ✅ GitHub CI 出可分发 release + updater 制品(dev.10,2026-08-01 额度恢复后)。
- ✅ updater 真机验证(dev.8/9→dev.10,2026-08-01)。
- ✅ 撤 landing 未签名文案(2026-08-01,见顶部了结横幅);releaseBody 早已改。 真相源 plan
~/.claude/plans/concurrent-inventing-panda.md。
🟢 版本命名切换姿势(-dev.N → beta/preview)—— 记录,切换时用(2026-07-28)
触发条件:功能冻结、准备放给外部测试者、想把工程味的
-dev.N换成beta/preview/rc时。 纯 RB(版本真相源src-tauri/tauri.conf.jsonversion)。关联 memoryproject_updater_and_version_scheme。
🔑 semver + updater 的坑(必须记牢):Tauri updater 用 semver 比较判「是否有更新」,而 semver 的 prerelease 段是按 ASCII 字母序比的。同一基版本 0.1.0- 下:
0.1.0-alpha.N < 0.1.0-beta.N < 0.1.0-dev.N < 0.1.0-preview.N < 0.1.0-rc.Ndev 字母序 大于 beta/alpha!所以若从 0.1.0-dev.9 直接切 0.1.0-beta.1,semver 认为 beta.1 比 dev.9 旧 → 已装 dev.9 的用户收不到这个"更新",升级链静默断掉。
安全姿势 = 换标签的同时抬基版本号,让数字段压过字母序:
0.1.0-dev.N→ 要公开测试时 →0.2.0-beta.1或1.0.0-beta.1(0.2.0/1.0.0>0.1.0主导比较,beta.1 稳稳 > 任何0.1.0-dev.N)。- 禁止在同一
0.1.0基版本内把dev换成beta/alpha。 - 现有路线(CLAUDE.md §10)=
dev.N→1.0.0-rc.N→1.0.0;rc字母序 >dev且通常同时抬到1.0.0,天然安全。要插beta,就在一次基版本 bump 上插。
建议:内测阶段继续 dev.N(现状);功能冻结要外测时一次性跳 0.2.0-beta.N / 1.0.0-beta.N。
🟢 隐私政策 / 服务条款(roadmap 1-6)收尾残留 —— 3 条(2026-07-28 记录)
1-6 已落地(landing
/[lang]/privacy/[lang]/terms+ footer 链接,as-builtprivacy-terms-plan.md)。以下为尾巴。
1. ✅ 部署后 Vercel 上线复验(2026-07-28 完成)
- 域名
lampio.app已注册 + Vercel 绑定 +NEXT_PUBLIC_SITE_URL已设。 - curl 实测通过:
/zh|en/privacy/terms渲染正常 + 无前缀/privacy→307→/zh+ OG/metadataBase 基址均https://lampio.app(预览图不裂)。非阻塞小注:未显式输出<link rel="canonical">(可选 SEO 增强)。
2. RB Settings 内加「隐私政策 / 服务条款」入口链接(纯 RB 侧,小)
- 现状:仅 landing footer 有链接,app 内无入口。
- 动作:RB
SettingsPanel「关于/应用」小节加指向https://lampio.app/privacy(+/terms)的外链(走src/lib/strings/i18n + 现有 open-external 方式)。RVH 侧同理(RVH 新会话)。
3. ✅ lampio.app 域名 + canonical 生效(2026-07-28,landing 侧完成)
- 域名注册 + Vercel 绑定 +
NEXT_PUBLIC_SITE_URL+ 线上 OG/metadataBase 复验全部通过(见上条)。launch-todo A1 部署项 + A5 域名项已勾。 - 唯一剩余 = RVH 上架填 URL(RVH 新会话,未发版先挂账):iOS/Android 配置填隐私政策
https://lampio.app/privacy(线上已可达)。见 launch-todo A1 第 4 项。
🟢 品牌改名(Lampio)收尾残留 —— 2 条(2026-07-26 记录)
品牌 ReadBrowser → Lampio 改名大工程已基本完成(桌面/移动/后端/landing/仓库/admin/预装库 全 Lampio 化,v0.1.0-dev.7 首个 Lampio 安装包已发布)。真相源
docs/plans/rename-plan.md+ memoryproject_brand_rename。以下 2 条为剩余尾巴。
1. Logo 精修 → 重新生成图标(用户 Figma,等通知开新会话)
- 现状(2026-07-26):已用初稿母图
docs/brand/lampio-icon.svg经rsvg-convert → pnpm tauri icon生成src-tauri/icons/*(桌面端已是 Lampio 初稿图标,commit7ebac72)。下次构建/发版生效(dev.7 及更早的包仍是旧图标)。 - 触发:用户在 Figma 精修母图 → 导出 1024 PNG → 在新会话通知 Claude「logo 好了」。
- Claude 侧动作:
rsvg-convert/直接用精修 PNG →pnpm tauri icon <1024.png>重生成src-tauri/icons/*(删 tauri 顺带生的 android/ios,桌面用不到)→ 下次发版带上。RVH 侧同理用同一母图(flutter_launcher_icons)。 - 纯机械,几分钟。
- 实证补记(2026-07-27):v0.1.0-dev.8 的
.app已带 Lampio 绿/琥珀图标——下载包内icon.icns与仓库src-tauri/icons/icon.icns字节相同(SHA-2560f5c053c…)。用户在 Finder/DMG 看到旧的黄青 spinner = macOS Icon Services 缓存串味(之前装过同 bundle idcom.lampio.dev的旧图标版;dev/prod 共用 id 加剧,见条 2)。非构建/发版 bug——清缓存或重启即恢复。 - favicon 卫生:① app
public/favicon.svg原是紫色闪电模板遗留(#863bff),2026-07-27 已换成docs/brand/lampio-icon.svg(本会话修);② landinglanding/app/favicon.ico仍待 Lampio 化——Figma 终稿出后连同 icons 一起用 Lampio mark 重生成(当前是旧/模板 .ico)。
2. dev/prod identifier 拆分(正式 1.0 时做)
- 现状:
tauri.conf.jsonidentifier =com.lampio.dev,dev/prod 共用(改名时只换名、未拆分)。 - 触发:发正式 1.0(无
-devtag)前——/releaseskill 的正式版 identifier 硬闸会拦(要求com.lampio,非.dev)。 - 动作:prod identifier 改
com.lampio+ dev 经 Cargo feature/build script 覆盖回com.lampio.dev(数据隔离);与 schema.sql 冻结点、下载页确认一起在 1.0 那次做。详见 memoryproject_release_prep(已更新为 com.lampio)。
🟡 测试线收尾(roadmap 5-x,2026-07-22 记录)
5-2 RVH 镜像✅ 已完成(2026-07-22):RVHtest/sm2_golden_test.dart+21 全过、三端无漂移 (被测对象实为lib/core/algorithms/sm2_algorithm.dart)。RB 改 JSON 时须重新复制到 RVH fixture。5-5 冒烟 runbook 写驱动固化✅ 已完成(2026-08-07,搭 roadmap 3-5 EPUB 验收的实机会话顺手做掉):- 复习入口选择器已实测点通 →
nav[aria-label] button:nth-of-type(3)(位置型、不吃 locale, 绕开原「title 中英混用不可靠」的障碍);另固化 TOC 条目 / 切标签 /.rb-annotation计数, 并确认rb_click对主 webview 的 React onClick 有效。 - 写驱动步骤改按能力边界写死而非继续挂「待定」:打开本地 .epub(macOS 原生文件面板) 与 双击存词/选区划线(需真实 Selection + dblclick)合成事件永远碰不到,固定标 🙋,别再试。
- 顺带修掉一条假断言:原 Flow 4「位置恢复」断言的是产品不存在的行为(3-5 证明书/章/章内偏移 三层全不恢复)——一条永远挂红的断言比没有更糟。已重写为「打开→章节导航→划线跨会话持久」, 位置恢复待本文件 §🔴EPUB B4 修复后加回。
- 剩一个小尾巴:Flow 3 的 Hard/Easy 评分按钮选择器已从源码推导 (
button[class*="flex-1"][class*="font-semibold"],文案硬编码英文不吃 locale), 本轮无到期卡未能实跑,首次实跑时验证并把 runbook 那行改 ✓。 - runbook 可以挂
/releaseStep 1 前了。
- 复习入口选择器已实测点通 →
- 5-6 用户测试:挂在 roadmap 1-3 Sentry 之后(被动崩溃反馈 = 最重要的用户测试)。第五梯队只差这一条。
🟢 本仓首次全历史 secret scan —— 基线已建,无 service_role 泄漏(2026-08-29)
这是合仓后本仓第一次被完整扫过。 此前只有 P0-3 扫过老 RVH 仓(8 条命中); RB 自己的历史从来没扫过。本次 gitleaks git . --log-opts="--all": 1379 commits / 215.04 MB / 9m40s / 14 条命中。
分类(14 = 10 噪声 + 4 真实,且那 4 条是同一个 key):
| 类 | 数 | 内容 |
|---|---|---|
| 刻意的测试夹具 | 4 | .github/workflows/ci-admin.yml(iss: e2e-dummy,注释明写"刻意做成合法 JWT 形状好让阳性对照看得见")· admin/tests/unit/sentry-scrub.spec.ts 的 FAKE_SERVICE_ROLE_JWT + 两个 sb_* 假 key(同文件注释自证)。都自带说明,不是疏忽 |
YOUR_... 占位符 | 6 | 两份 Edge Function 部署文档 × 各自的 RB / RVH 副本;其中 4 条所在文件已不在 HEAD |
| 真实 anon key | 4 | 全部是同一个:role=anon · ref=jdtbyteiwnciqnfppztz · iss=supabase · exp 到 2036。位置:supabase/sql/cron-setup.sql ×2(仍在 HEAD)· src-tauri/assets/sql/init_data.sql · src-tauri/src/commands/init.rs(后两处已被构建期注入清空,只在历史里) |
🔒 最重要的一条结论:没有任何真实的 service_role key 进过本仓。 唯一带 role=service_role 的命中是那个自证的测试假 JWT(无 ref、无 iss —— 真 Supabase key 必有)。 service_role 才是绕过 RLS 的那把钥匙;anon key 按 Supabase 设计就是发给客户端的 (它本来就随桌面端二进制与移动端 APK 分发,RLS 才是保护层)—— 这与上面「轮换 anon key」条目自己的定级一致,不是事故。
做了什么 / 没做什么:只做了扫描与分类,没有改代码、没有加 .gitleaksignore、 没有把 gitleaks 接进 CI。理由:14 条里 10 条是噪声、4 条是已知且已有条目跟踪的 anon key, 接 CI 的收益此刻低于"再造一道需要维护基线的闸门"的成本。 真要接,前置是先写 .gitleaksignore 把那 10 条钉死,否则它一上来就是恒红。
这次扫描的实际产出是两条:① 独立佐证了百度 AK/SK 已清干净(见上面那条); ② 查出「轮换 anon key」条目漏了 cron-setup.sql 这一处,且漏掉它的后果是定时任务静默 401(见下条 ⑥)。
🔴 轮换 Supabase anon key(roadmap 1-5 的后续——真正作废旧 key,2026-07-23 记录)
背景:roadmap 1-5「anon key 改构建期注入」已落地(as-built
anon-key-build-injection-plan.md)—— 工作树不再携带项目身份。但注入基建 ≠ 旧 key 作废:旧 anon key 字面量仍留在 ① git 历史(git log -p可翻出,未做 history rewrite)② 已发版的 2026-07-19+ 二进制里 ③ 🔴 以及supabase/sql/cron-setup.sql—— 它现在(HEAD 工作树)就带着 2 处真 key (2026-08-29 gitleaks 扫出,见下面那条基线)。原文只写了 ①②,漏了这一处;src-tauri/那两处(init.rs/assets/sql/init_data.sql)确实已被构建期注入清空、现为 0 处。 真正让旧 key 失效只能靠在 Supabase 轮换 anon key。本条 = 那个后续动作。
触发条件:决定真正作废旧 key 时(例如判断 fork/泄露风险升级,或例行安全轮换)。非紧急——anon key 公开 by design(RLS 保数据),泄露本身不直接危害数据。
⚠️ 依赖方向已更正(2026-08-02,第二轮讨论):早先记的「启动『LLM 调用统一到服务端』后本条升级为前置条件」 反了。该 plan 的 WP3 阶段 2 落地后,光有 anon key(无 JWT
sub)打不出 LLM 调用, 它就不再是「烧钱凭证」——攻击者得注册真账号,而那被 per-user 配额封顶且可封号。 真正的准入控制是 per-user 配额,不是 key 保密;本条因此回落到「例行安全卫生」级别,不是前置条件。
为什么是跨端协调任务(不是单 RB 会话):
- RVH(本仓
rvh/)共用同一 Supabase 项目(init.rs注释原文 "shared with RVH"),几乎必然也硬编码了同一 anon key → 轮换须 RVH 同步改注入 + 重新发版(RVH 新会话,CLAUDE.md §9)。 - admin(
admin/)也连同项目,需核对其 key 来源(env 还是硬编码)。 - 已分发客户端会失效:旧 key 轮换后,所有装了旧二进制(含 2026-07-19 野外 Windows 版)的用户 auth/sync 会 401 → 必须先有 roadmap 1-2 Tauri Updater 把用户升到含新 key 的版本,否则强制轮换 = 老用户集体掉线。故本条实际上被 1-2 阻塞。
动作清单(重启时):① Supabase Dashboard 轮换 anon key;② RB 更新 .env.local + GitHub Secrets RB_SUPABASE_ANON_KEY + 重新发版;③ RVH 新会话同步注入 + 发版;④ admin 核对;⑤ 评估已分发老客户端的迁移路径(依赖 1-2 updater); ⑥ 🔴 改 supabase/sql/cron-setup.sql 的 2 处字面量并重新执行它(2026-08-29 补)—— 那两处是 pg_cron 调 Edge Function 时的 Authorization: Bearer <anon>。 已安装的 cron job 里存的是安装当时的 key,轮换后不重装 = 推荐系统的每日/每周定时任务 开始 401 且静默失败(cron 失败不会有人看见)。漏掉这一步,轮换的代价不是"没生效", 是"推荐池悄悄停止更新"。
🟡 快速标记熟词(triage)第二轮:分档进度条 + 完成页成果图(2026-07-04 记录)
第一轮(文案熟词域统一 / 认识-还不熟按钮对齐复习 emerald-amber 语义色 / 计数大数字化+脉冲 / 卡片砍 IPA+CEFR 徽标 / gloss 按预装库源序选 2 条中文义项)已落地。本条为当时评审方案里 明确「单独一轮」的剩余两项,均需后端 per-level 统计支撑:
- 分档进度条:CEFR 分档选择条(QuickTriageSurface 顶部 segmented picker)升级为段内填充 进度——每段按「该档已表态(notebook + excluded)/ 该档总词数」显示填充比例,标记时当前段 实时微增,轻量版所见即所得。后端扩展
get_triage_pending_count或新命令返回 per-level(pending, total)。 - 完成页成果图:completed EmptyState 增加 StatsPanel 同款
SimpleBarChart横向条,展示 「本轮各档标记数」或熟词累计分布,构成庆祝时刻。注意口径:StatsPanel 现有 CEFR 分布图统计的 是生词本,triage 写的是 known_words,不能直接搬——需要独立查询。 - 顺带候选(低优):ConfirmDialog 增加中性(非 warning)variant,跳档确认弹窗去掉琥珀警告 图标(非破坏性操作);或干脆去确认直接跳。
🟢 词族(word_family)关联词 chips —— 静态展示已落地(2026-06-15),剩可跳转导航
已落地:⑦ 段二顺手做了 word_family 静态 chips(与词源同区"来历+家族",
EtymologyAction.tsx内)—— 呈现构词网络。vocabulary.word_family列 4,408 词(~23%)的干净派生关系已露出。 剩余触发条件:用户说「词族 chips 可点跳转 / 关联词导航」即启动可跳转增强。纯 RB、零 LLM。
剩余 = 点击 chip 跳查该派生词:需跨 VocabPanel(3 处 WordDetailView 挂点,且派生词可能不在生词本→需 re-lookup)
- 复习卡(无导航目标)的导航布线,故 v1 留静态展示。价值 = 构词网络导航(不是辨析——词族是 POS 不同、 各有其义的派生词,非可替换同义选项)。是「网状关系」最 philosophy-safe 的第一步(就地关系露出,非全局图谱)。 注意辨析 vs 关系的边界:⑤ 辨析只配 synonyms(该用哪个);词形屈折(
word_forms)不做展示(cloze 内部已用、噪声大、价值低); 词族走导航 chips。三者别混。
⬜(远期·明确 gate)全局词网可视化图谱
触发条件:用户心智验证后明确要求「做词网图谱 / 关联可视化页面」才启动。默认不做。
把同义/近义/词形/词族建网做可视化(用户长期想法)。暂缓理由:① 产品身份风险——独立"词网探索页"是 背单词工具/教育产品特征(vocabulary.com 式),是"去某页探索"的目的地,违反三道筛(阅读自然路径 / 放大读他想读的 / 竞品是 Kindle 非 Duolingo)。 ② 数据撑不住——synonyms 77 条噪声 dump、word_family 仅 23%、word_forms 噪声大,画图成 hairball。③ ROI 在专门学习 App 里都有争议、工程量大。 真价值由就地增量交付:⑤ 辨析 + 已有 antonyms + 词族 chips = "网在阅读时一跳一跳浮现",优于全局静态大图。呼应 roadmap §0 + layer-5"验证心智再碰"。
roadmap ⑤ 近义词辨析/搭配已落地(2026-06-14,段一 RVH 生成 + 段二 RB 渲染;实机视觉验收待用户冷重启)。 详
docs/plans/archive/synonym-differentiation-plan.md+docs/cross-end/11-rb-synonym-differentiation-reseed-confirmation.md。原段一/段二 backlog 条目已消。
⬜ roadmap ④ 个性化例句——跨端可见性(必须新会话,CLAUDE.md §9)
触发条件:启动 ⑥ cloze 跨端 v2(RVH 新会话)时一并处理。
RB v1(2026-06-14 落地,personalized-example-plan.md)的个性化例句缓存 word_personalized_examples 是 local-only,RVH 看不到。这是有意为之——跟随 ⑥ cloze 整体"跨端 v2 需 RVH 新会话"的边界,不为 ④ 单开同步。跨端 cloze v2 启动时,评估是否把个性化例句一并下发到 RVH 复习卡(需 Supabase schema + Dart 端对齐)。 注意:个性化锚定单用户但仍是同一用户跨端(RVH = 同用户另一端),与"跨不同用户摊薄"是两个轴——同用户跨端 同步是可做的,只是 v1 未做。
🟡 叙事统领式仪表盘(narrative-led dashboard)—— ⑪ 的延伸,纯 RB 侧
进度(2026-06-08):B-lite + 模块精简 + Round 2/3/4/5 打磨已落地。Round 4 含 Rust:阅读统计加内容页过滤(
word_count≥200滤导航页噪声)+ 显示文章数、砍每日时长折线、「记得牢吗?」→「记牢了多少?」、range picker 进两趋势板块表头。Round 5:文章数+活跃天数合并成一句、两 picker 拆独立 range 去联动 + 改低调纯文字样式。StatsPanel 冷名词标题全换第二人称问句(温和陪伴语气),按定位(阅读工具非背单词 App / 无打卡)叙事精简:9 块 → 5 块——砍本周卡、本月句并入顶部 AI 周回顾卡(常驻补充行 + 模糊小时)、合并等级+CEFR、Streak/活跃天数并入「你读了多少?」去打卡感、低词量水平转鼓励句 + CEFR 白话注解、「练得最多」空态隐藏、SRS 正确率换真实掌握进展(cards_in_review)、range picker 下移只管两张趋势图(删 StatsToolbar)、词数措辞改诚实「浏览」。零额外 LLM、图表全保留。计划见docs/plans/archive/narrative-dashboard-blite-plan.md。本条降级为 A 档可选增强的占位(见下)。
剩余可选(A 档):各块动态 LLM 旁白。看完 B-lite 实际效果后再决定,且只叠在真正需要点评的板块(SRS 记忆曲线 / 水平进展),而非每块都加。触发条件:用户说「给某板块加动态旁白 / 做叙事仪表盘 A 档」即启动。纯 RB 侧,无需 RVH。
想法来源:用户观察——「本周回顾」(⑪)的叙事形式让人感觉「像有个好导师/好伴侣陪在身边」,而其余统计板块是简洁数字、各有优点;能否用「本周回顾」这种风格把整个「学习统计」串起来,作为一种新型报告形态。
定位论证(为什么成立):ReadBrowser 定位「学习自然发生、不施压」。理想的「学习统计」不该是 dashboard(工具/SaaS 语言),而该是 companion's notes(陪伴者语言)。现有纯数字板块是「为统计而统计」的残留,与产品气质有轻微错位。叙事化是有产品哲学支撑的差异化,不是换皮。命名:叙事统领式仪表盘——叙事负责「意义与情感」,图表负责「精确与趋势」,分工而非互斥。
核心平衡原则(不可一刀切全叙事化):
| 维度 | 叙事强 | 数字强 |
|---|---|---|
| 情感/动机 | ✅ 被看见、被鼓励 | ❌ 冷 |
| 趋势/对比 | ❌ 文字说不清涨跌 | ✅ 折线/柱状一眼看穿 |
| 扫读效率 | ❌ 要读一段 | ✅ 一瞥 get |
| 成本/实时 | ❌ 调 LLM、有延迟 | ✅ 即时免费 |
| 精确性 | ❌ LLM 可能 154→153 | ✅ 准 |
| → 结论:叙事统领数字,不是叙事取代数字。 |
三档方案(讨论过的取舍):
- A. 各 section 加 LLM 旁白:每个数字板块顶上一句动态人话。隐患:① 和顶部「本周回顾」内容重复打架(都讲"读得稳")② 每块烧一次 LLM。边际价值低。→ 不作首选,降级为「可选增强」。
- B. 一份贯通叙事 + 数字作证据:整页重构成「周记」,数字内联高亮 + 可展开图表。体验最佳但结构性重构、重,且强依赖缓存否则每次进页烧 LLM。
- B-lite(★ 推荐首做):B 的骨架 + 静态人话串联。叙事灵魂由顶部 LLM 周回顾承担(已实现,⑪);各板块只把冷名词标题换成第二人称问句(零额外 LLM 成本、零冗余),图表原样保留:
- 「阅读活跃」→「你读了多久」
- 「词汇增长」→「你记住了什么」
- 「英语等级 + CEFR分布」→「你现在的水平」
- 「连续天数」→「你的节奏」
- 「SRS表现」→「记得牢吗」 整页气质从「仪表盘」变「导师的观察笔记」,而精确性/扫读效率/零成本全保留。投入产出比最高的一步(基本只动 StatsPanel 文案 + strings,已在 ⑪ Part 2 做了 section 重排打底)。
推荐渐进路径:B-lite 先行(低风险、复用已有周回顾)→ 看实际效果 → 若还想要更「贴身」,再把 A(动态旁白)作为 opt-in 增强只叠在真正需要点评的板块(如 SRS 记忆曲线、水平进展),而非每块都加。不建议「A、B 各做整版去对比」——成本不对等、B 做完即已花掉大部分成本。
关联:roadmap ⑪(docs/plans/archive/ai-weekly-report-plan.md);落地点 src/components/StatsPanel.tsx(⑪ Part 2 已重排 section 顺序,B-lite 接着换标题即可)+ src/lib/strings/{panels,locales}/report.ts;可记入 llm-product-upgrade-roadmap.md 层四作为 ⑪ 的延伸。
推荐流个性化 V2:⑨ 理由 LLM 润色(暂缓,纯 RB 侧)
触发条件:观察到用户真的在用推荐理由再启动——真实使用中表现出对理由的关注/依赖,或主动说 v1 模板理由「太机械、想要更懂我」。当前暂缓,先让 v1 模板版(commit c01431d)跑一阵接受真实验证。
为什么暂缓:v1(⑧难度档+⑨模板理由+⑩结构化兴趣,0 LLM,2026-06-09 落地)的设计逻辑就是「先零成本模板验证用户是否真看理由,再决定要不要花 LLM 钱润色」。v1 刚落地未经真实使用验证,先上 V2 LLM 等于跳过这个验证关卡。
scope(重启时):第一步只做 ⑨ 理由 LLM 润色。LLM 拿画像(等级+近期 reading_log 标题+近期生词+这篇 title/summary)一步生成「懂我」理由,天然含主题理解 → 吸收了 ⑩,不单独做 ⑩ 语义主题层。站点/源个性化最后。
已探索好的设计要点(重启时直接用,无需重探,2026-06-09 探索):
- opt-in:仿
context_disambiguation——GeneralSettings.tsx(Switch+ConfirmDialog 开启前弹隐私确认) +init_data.sqlseedpersonalized_reasons='false'+settings.ts5 getter + zh/en locale。 - 缓存:仿
WeeklyReportCache(report.rs)——settings KVpersonalized_reasons_cache存{reasons:{url→reason}, level, cached_at},按 user_level 变化或 TTL 失效。不进 recommended_articles 表列(fetch_recommended_articles全表 DELETE 替换会冲掉)。 - EF:新建独立
generate-recommendation-reason(项目惯例每功能一 EF);secret 复用全局LLM_API_KEY/MODEL/PROVIDER;project-refjdtbyteiwnciqnfppztz;部署走/edge-deploy。 - 成本纪律(待重启时与用户确认):打开首页对整个推荐列表(~20)一次批处理 LLM(仿 analyze-articles 一次 20 篇)→ 缓存 url→reason;
buildReason优先级改 LLM 缓存 → 回落 v1 模板;opt-in 默认关。 - 隐私:只发 title/summary(公开 RSS)+ 画像,不发原文——与 reading-report 同口径。
关联:v1 计划 docs/plans/archive/recommend-personalization-v1-plan.md;roadmap llm-product-upgrade-roadmap.md ⑨。纯 RB 侧,无需 RVH。
AI 周报:生成前 flush 活跃 tab 阅读会话(P3,纯 RB 侧)
触发条件:用户反馈「刚读完一篇想立刻看周报,但这篇没算进去」/ 觉得周报即时性是痛点时启动。
现象/根因:reading_log 只在阅读会话结束时落库(save_reading_log,前提 readTime>=5s),三个触发点 = 同 tab 导航新网址(__RB_PAGE_CHANGING)/ 关闭 tab(close_content_webview → __RB_SAVE_READING_SESSION__)/ 退出 app(lib.rs 退出钩子)。tab 间 show/hide 切换只累加时长、不写表。所以用户停在某篇文章页(tab 没关)时,这篇不在 reading_log 里 → 周报(generate_reading_report 滚动 7 天 + title IS NOT NULL)看不到它。这是 reading_log 的既有设计,不是周报 bug。
做法(若启动):在 Stats 点「生成回顾」前,先对所有活跃 content webview 各 eval 一遍 __RB_SAVE_READING_SESSION__()(已存在,见 webview.rs:170 close 路径复用同一函数)+ sleep 一小段等 SQLite INSERT 落地,再调 generate_reading_report。需要一个新 Rust 命令 flush_all_reading_sessions(遍历 content-* webview eval)或在前端生成前先调。注意 __RB_SAVE_READING_SESSION__ 内部把 isPageVisible=false,重复调要确认不会丢后续计时(读一遍 tracking.js:108-129 的状态机)。
为什么 P3:周报语义是「本周回顾」看累积,不强求最新一篇即时入库;正常用户读完会切走/关 tab 自然落库。仅当即时性确成痛点再做。
关联:docs/plans/archive/ai-weekly-report-plan.md;落库机制 src-tauri/src/content-script/features/tracking.js(saveAndResetReading / __RB_SAVE_READING_SESSION__)+ webview.rs close/navigate 路径。
跨端联调开关:在 RVH/RB 会话同时加载对端 MCP server(P3,按需启用)
触发条件:前置 = RVH-debug-mcp 已落地;信号 = 用户首次说「在两个项目反复切窗口跑诊断查询烦了」/「跨端 sync drift 想一次看完两边」/「打开联调」等
为什么不默认开:每会话 system prompt 多吃 ~2-3K token(6 个额外工具 schema)。RVH 自调 / RB 自调占场景多数,默认不开避免无效消耗。
怎么开(项目级,推荐起手):
🔴 2026-08-28 合仓后本条的前提变了:曾经要在两个仓各维护一份
.mcp.json(老仓那份指向本机绝对路径,T3-4 已随合仓删除)。现在只有根.mcp.json一份, 且rb-debug与rvh-debug两个条目都已经在里面了(T3-4 加的,用绝对路径)。 ⇒ 「怎么开」这一步已经不需要做;本条剩下的价值是下面「运行时前提」与工具清单。
当前根 .mcp.json 的形状(两端并存,无需再改):
json
{
"mcpServers": {
"rb-debug": { "command": "node", "args": ["/Users/larry/reading-browser/rb-debug-mcp/dist/index.js"] },
"rvh-debug": { "command": "node", "args": ["/Users/larry/reading-browser/rvh/rvh-debug-mcp/dist/index.js"] }
}
}改完重启 Claude Code 即时生效。
运行时前提:用 rb_* 工具时 RB Tauri dev app 必须跑着(pnpm tauri dev);用 rvh_* 工具时 RVH 真机/emulator 必须连着 adb。两端跑着时两套工具完全独立。
升级到用户级(高频时再做):若联调真用了 2-3 次发现常用,把两条 server 配置搬到 ~/.claude/settings.json 的 mcpServers,所有会话默认看到 12 工具(只在确实高频时这么做,否则 token 浪费)。
严格边界(CLAUDE.md §9 不变):
- 联调 = 只读跨端诊断,不是跨端实施
- RVH 会话看到
rb_*工具只能用于查 RB 状态;改 RB 代码仍必须 RB 新会话 - 反之亦然
典型使用场景:用户报"我在 RB 加的词在 RVH 看不到" → 同会话 rb_db_query learning_entries WHERE word=X + rvh_db_query同 → Claude diff 出是 push 没出还是 pull 没进 → 报告给用户 → 用户切到对应项目新会话执行修复。30 分钟诊断 → 3 分钟。
关联:
- 设计背景
docs/cross-end/03-rvh-debug-mcp-design.md§8「跨端联调后续」 - 双端 MCP 工具列表:RB 6 个(
rb_snapshot/rb_snapshot_tab/rb_logs/rb_state/rb_dom/rb_db_query)+ RVH 6 个(rvh_snapshot/rvh_logs/rvh_state/rvh_widget_tree/rvh_db_query/rvh_navigate_log)
rb-debug-mcp content webview IPC postMessage fallback 噪声(P3)
触发条件:调试 content webview 时 console 噪声成为困扰 / 想知道 fallback 是否影响性能
现象:content webview 反复打 IPC custom protocol failed → postMessage fallback warn。每次 invoke() 走子 webview(rb_dom 注入 / content-script invoke 等)都会触发一条。功能不受影响——所有 invoke 实测都跑通了。
已知:
- 不影响 rb-debug-mcp 任何工具(2026-05-26 6/6 全过)
- 不影响生产 content-script 的 IPC 调用(双击查词 / 高亮 / TTS 等仍工作)
- Tauri 2 的子 webview 在某些情况下走 postMessage 路径而非
ipc://custom protocol,这是 wry 的已知 fallback 设计
推测原因(未验证):
- content webview 创建时 custom protocol 没完整注册到该 WKWebView
- 或某些远端站点的 CSP 阻断
ipc://协议,Tauri 自动降级
可能的排查方向(按 ROI):
- A. 看 wry 源码中
ipc://协议注册路径,确认add_child(WebviewBuilder)是否漏注册 - B. 在
create_content_webview调用处显式with_initialization_script设置 custom protocol handler - C. 接受 fallback,改
tauri-plugin-logfilter 把这条 warn 静音(治症不治根)
触达提醒:遇到 invoke 慢 / content-script IPC 偶发失败时同步追根因;否则纯噪声不动。
Practice 跨端同步(P2)
触发条件:用户开始跨设备使用 Practice 功能 + 反馈跟读历史看不到
估算:1-1.5d(含 Supabase Dashboard DDL + RVH 镜像)
简短描述:pronunciation_attempts 当前是 local-only MVP(schema 注释明确)。加入跨端同步矩阵需要:
- 加
deleted_at+synced_at列(migration) - Supabase 端新建
user_pronunciation_attempts表 + RLS - sync/push.rs + sync/pull/ 加 13 张同步表第 10 张(pronunciation 类)
- 音频字节如何同步:直传 BLOB(PostgREST bytea) vs Supabase Storage(参考 reading-snapshots bucket 模式)
注意:audio_blob 字节量大,建议走 Storage 路径而非 PostgREST。
关联:依赖 Practice P0 + Practice P1(先让本地有出口再考虑跨端)
NotesPanel / VocabPanel 抽 ListWithDetail organism
触发条件:自然触达时(修改 NotesPanel 或 VocabPanel 任一时顺手)
估算:1d
简短描述:两面板有大段相似的"列表+搜索+批量操作+详情滑入"模式,可抽 organism 减少 ~200 行重复代码。
关联:docs/plans/project-refactoring-plan.md 长尾
HistoryPanel / MySitesPanel / RssPanel 抽 useListPanel hook
触发条件:自然触达时
估算:1d
简短描述:三个面板共有"列表+搜索+分页+drill-down"重复模式,可抽 hook。
关联:docs/plans/project-refactoring-plan.md 长尾
reading.rs 拆 3 块(T1)
触发条件:下次修改 reading 相关功能时顺手拆
估算:1d
简短描述:1300+ 行,拆 text-analysis / settings / stats 三块。
关联:docs/plans/project-refactoring-plan.md T1
105 处直连 invoke() 收敛到 hooks/api/*(T3)
触发条件:仅在新增命令时要求走封装;存量不动
估算:渐进,无单次时间盒
关联:docs/plans/project-refactoring-plan.md T3
Content webview navigate gap 黑闪(macOS dark system)
触发条件:用户再次反馈 / 重启切换体验打磨阶段
估算:未知(已耗 ~1 个会话探索三条路径未中根因,再开需要先做更深的层级诊断)
现象:
- macOS 系统是 Dark 主题(应用 light/dark 主题都重现,与 React/Tailwind 主题无关)
- Review prev/next 切换源、Read 模式打开新网页加载前那一帧都是黑色
- 用户原始描述:"短暂的黑底白字的页面"
已排除的路径(避免下次重复试错):
WebviewBuilder::background_color(Color(255,255,255,255))—— wry 源码 (wry-0.54.3/src/wkwebview/mod.rs:368-443) 在 builder 设 background_color 时会强制drawsBackground=false(让 webview 透明)+setUnderPageBackgroundColor兜底。dark mode 下还闪。WindowBuilder::background_color—— tao 路径在 macOS overlay style 下对 NSWindow 不生效。[WKWebView setBackgroundColor: whiteColor](NSView 继承的,私有用法)—— 没解决黑闪,但确实改变了 webview 内部状态。[WKWebView setUnderPageBackgroundColor: white](macOS 12+ 公开 API,不动 opaque/drawsBackground)—— 红色诊断验证:那一帧根本不显示 underPageBackgroundColor 设的颜色。[NSWindow.contentView setWantsLayer:YES; layer.backgroundColor = red CGColor]—— 红色诊断验证:那一帧也不是 contentView layer 透出。
核心诊断结论:那一帧的黑色既不来自 WKWebView 自身的 backgroundColor / underPageBackgroundColor,也不来自 NSWindow.contentView 的 layer。它在更深的层级,可能是 wry 的 NSView wrapper、WKWebView 内部的某个未公开 sublayer、或者整个 WebContent process 还没接管时的某个 AppKit 默认 layer。
下一步候选方向(按 ROI 排):
- A. 给 wry 的 webview superview(WKWebView.superview,是 wry 包的 NSView 容器)设
wantsLayer:true + layer.backgroundColor = white CGColor,看能否拦住那一帧 - B. 用
webview.with_webview后遍历 WKWebView 所有 sublayer,逐层着色诊断 - C. 绕过根因走症状治:Rust 端 navigate_to 监听
didCommitNavigationevent,navigate 前把 webview move 到 -10000,-10000,commit 后再 move 回来——隐藏过渡帧。复杂度高 - D. 升级 Tauri / wry 版本,确认是否有 upstream 修复(当前 tauri 2.10.3 / wry 0.54.3)
关联文件:
src-tauri/src/commands/webview.rs:48-79—create_content_webviewsrc-tauri/src/lib.rs:206-218— main window setup- wry 源码
~/.cargo/registry/.../wry-0.54.3/src/wkwebview/mod.rs:368-468— background_color 处理
🖼️ 词汇插图(Word Illustration)OpenMoji 离线层 —— ✅ Phase 0-3 全部落地(2026-06-06)
已完成全链路:RVH 单点产出 vocabulary.emoji(v53,505 词/489 图)→ RB byte-equal reseed(migration v14 ALTER ADD emoji + helpers UPSERT,VOCABULARY_SEED_VERSION v16)→ dictionary.rs/srs.rs 读 illustration_hexcode → WordIllustration 组件接 WordDetailView(标题 lg)+ ReviewCardFace 正面(lg)→ public/openmoji/ 489 SVG(2.4M)+ Settings 署名。cargo check + pnpm build + reseed UPSERT 模拟(保留 audio_local_path 用户态)全过。配图粒度 = word 级 + 歧义黑名单(不做 sense 级)。
唯一遗留验证:live render 自测需用户跑 pnpm tauri dev(启动触发 migration v14 + reseed),在 Library 详情 / Review 卡肉眼看图 + rb_snapshot。代码侧已验证,只差实机一眼。
v2 延后(独立触发):在线兜底层(Openverse/Wikimedia 补具象漏网词 airport/classroom 等 → canvas 压缩 → BLOB 缓存,复用 notes.rs::save_note_cover + lib/coverImage.ts 管线,独立 word_illustrations 缓存表 local-only)。触发条件:有数据证明 B1 段配图是真需求。 sense 级延后:OpenMoji 覆盖度显著提升 + 消歧链路稳定后再评估。
🟢 LLM 产品升级路线 —— 已成文,分批推进(roadmap,非一次性任务)
触发条件:用户说「推进 LLM 升级 / 做下一个创新点 / 短语高亮」等即触发;启动相关会话先 Read docs/plans/llm-product-upgrade-roadmap.md。
性质:这是活跃 roadmap(15 个 LLM 创新点分 5 层 + 优先级 + 状态台账),不是单条任务。真相在 roadmap 文件,本条仅作 backlog 入口指针。
首发:P0 ① 短语动词/习语高亮(复用度最高、竞品最空白)。实施时拆 docs/plans/archive/phrasal-idiom-highlight-plan.md,并回填 roadmap §4 台账。
接地线:复用已落地的 disambiguate-sense Edge Function 骨架(deepseek temperature 0 + prompt cache + opt-in 自动触发)+ content-script 双击/选中链路 + vocabulary 缓冲池。
跨端注意:层四⑫跨端学习画像、产出层 RVH 镜像等涉及 RVH 的点必须新会话(CLAUDE.md §9)。
🟢 复习卡 cloze v2 候选(⑥ 延伸,纯 RB 侧)
触发条件:用户说「cloze 优化 / 多句轮换 / 给挖空加图提示」等即讨论。纯 RB 侧、无 RVH。优先级低(v1 已交付核心价值)。v1 落地见
docs/plans/archive/review-cloze-plan.md+ roadmap ⑥。
已在 v1 内解决(不再是 v2 项):① 挖空占位虚线;② 同根词泄露——多形态挖空(句中该词所有 surface+word_forms 都挖)已修;③「先回忆再揭晓」——cloze 与
cloze_review开关绑定(升级「复习先回忆」模式),遮蔽未翻面时经useReviewStore.clozeMasked抑制右侧来源原文的答案自动高亮,翻面才揭晓;④ 跨 inline 节点残缺句(如 "hyalotype" 弯引号 span 打断文本节点 → 句子被截成 ", patented in 1850, ..." 缺主语)——getSentenceAround改为总是拿单节点结果(一定是完整句子串)去块级整段文本里向两侧扩成完整句,更完整则用;⑤ 首句自愈升级——落库改ON CONFLICT(user_id,word) DO UPDATE ... WHERE length(new)>length(old),且add_word_page_link(已存词再读路径)也捕获语境,老词重读到更完整句时自动补全残缺行。定位拍板:cloze = 鲜活语境唤起+轻量自问,不是闭卷考,故「答案触手可及」(右侧原文/同根词)不追求滴水不漏,仅做「未翻面先不主动剧透」。
v1 边界(已知降级,留 v2):
一词一句:✅ 2026-07-02 已解(migration v21 多语境池:去 UNIQUE 一词多行、去重+封顶 5、查词即自动收录、复习轮换+切换。「自愈升级 longer-wins」逻辑随多行池删除——长短句共存各是不同 occurrence。详见docs/plans/archive/multi-context-cloze-plan.md)。- 句子边界粗糙:
getSentenceAround/sentenceContaining按.!?切句,遇缩写(W. & F. / P. irritans)会误切。v2 可引入更稳的句子分割。 - 插图作弱提示:v1 cloze 正面隐藏 WordIllustration 防泄底。v2 可探索"具象词配图作弱提示"的 opt-in(如先只露图轮廓/部分),但需防直接泄答案(dog→🐶)。
- 跨端 cloze:local-only,RVH 复习看不到 RB 捕获的句。跨端属未来,需 RVH 新会话(CLAUDE.md §9)——需评估
word_cloze_contexts是否值得进同步矩阵(句子字节量 vs 价值)。
/cross-end-check 提示:word_cloze_contexts 是 RB-local 预期差异(同 reading_pages.cached_file_path / pronunciation_attempts),不是三端漂移 bug——跑核对时归为合法 local-only,勿误报。
🟡 选区工具栏入口过载优化(理解层三件套铺满后顺手收敛)
触发条件:用户说「优化工具栏 / 工具栏太挤 / 收纳选区按钮」即启动。纯 RB 侧、无 RVH。
现状:随 roadmap ③ 指代/背景落地,选区工具栏已达 7 个按钮——翻译 / 朗读 / 拆解 / 指代·背景 / 短语 / 标记 / 跟读。用户面对一句话要在 7 个动作里选,决策瘫痪 + 认知负担,侵蚀「阅读工具该克制」的产品调性。
已拍板(2026-06-10,③ 落地时):③ 保留双入口(选区平级动作 + 拆解卡升级),顶层入口收敛留本专项、③ 本次不动。
可选方向(实施时再定):
- 高频动作(翻译/朗读/拆解)留顶层,低频(指代·背景 / 短语 / 跟读 / 标记)收进「更多…」溢出菜单。
- 或按"理解(翻译/拆解/指代背景/短语)vs 行动(朗读/跟读/标记)"分组。
- 注意:content-script 工具栏是 DOM 手搓(
toolbar.jsshowSelToolbar),溢出菜单需新增浮层组件 + i18n.js 文案,动工量中等。
「短语」按钮可能整个删除(2026-06-18 用户提出,倾向删):B 方案后自动高亮已覆盖 always 桶(高价值习语);选区「短语」按钮捞回的是低价值 context/literal,且交互成本高(选文本→点按钮→等匹配→看高亮→再点短语看释义,多步),用户基本不主动用。§9 深度短语模式一旦落地,context 桶会被 reader 全文自动救回 → 选区按钮彻底冗余。故长期倾向删(早前"选区是 context/literal 唯一捞回入口"的论证已收回——捞回低价值内容且无人用)。删涉及 toolbar.js(去 phrasesBtn)+ highlightPhrasesInSelection/handleSelectionPhrases + i18n toolbar.phrases。先不删——与本条工具栏收敛 + §9 决策一起评估(数据上它不污染 gate-A,已用 data-rb-phrase-auto 隔离,故无紧迫性)。
关联:docs/plans/archive/context-resolution-plan.md(③,新增第 7 个按钮的那次)。
🟢 指代高亮小遗憾:同句代词不在原文高亮(③ 增强 B 的已知降级,未来再议)
触发条件:用户说「做同句代词高亮 / 协调包裹」即讨论。纯 RB 侧、无 RVH。优先级低(仅小遗憾)。
现象:hover 指代行时,原文会绿色高亮先行词 + 句内代词。但当代词与先行词在同一文本节点(同句回指,如 them/worms 在 "shocking the worms while exposing them")时,只高亮先行词、代词被跳过。跨句场景(代词与先行词在不同段/节点,如 His/McConnell)两个都高亮,无此问题。panel 内代词始终绿(不受影响)。
根因(2026-06-11 落地时确认):
wrapTextNodePortion(annotations.js)用parent.replaceChild(frag, node)销毁原文本节点 → 同节点连包两个 mark 会让第二个 Range 失效。isNodeAllowed的p.closest('mark.rb-annotation') return false→ 已 mark 文本被二次搜索排除 → "包一个再搜另一个"在同句也失败(句子串出现缺口、anchor 找不到)。- 当前护栏:
highlightReference仅当代词与先行词startContainer不同才两个都包;同节点只包先行词(优雅降级,规避破坏 DOM/Range)。
未来可行方案:写一个「同节点协调包裹」helper——把落在同一文本节点内的多个 portion(代词 + 先行词)在一次 replaceChild 里用一个 fragment 包完([before][mark_a][between][mark_b][after]),按 node 分组所有 range。预计 ~30 行 + 边界处理(portion 排序/重叠/跨节点混合)。属"复杂/有坑"范畴,故本次未做。
关联:docs/plans/archive/context-resolution-plan.md(③);本条是「指代消解高亮增强 B」同族的细化遗留。
🟡 消歧缓存分层:review 缓存页同词同句重调 LLM(⑥+ 后续优化,2026-06-13 记录)
触发条件:用户说「消歧缓存优化 / disambig cache」或 RVH 消歧立项时讨论。L1 纯 RB 侧;L3 涉跨端。
现象(2026-06-13 实测确认):复习「复习中」缓存页(独立 content-review webview,rb-cache:// 快照)双击已存词, 即使与当初存词时同词同句,仍重调 LLM(~3s 等待 + 花钱)。根因:disambigCache 是 per-webview 内存缓存, 新 webview 注入即空。语义键 (word, sentence) 本身正确——同页不同位置(不同句)重调是 by design(上下文不同)。
缓存/持久化全景(现状):
| 层 | 范围 | 状态 |
|---|---|---|
| L0 内存 disambigCache | 同 webview 同词同句 | ✅ 已有 |
| L1 sense_gloss 回读 | 已存词 + 句子匹配 → 弹窗免 LLM | ✅ 已做(2026-06-13:get_cloze_sense 命令 + popup.js 三级取数 L0→L1→LLM,relocateGloss 文本重定位,失配回落 LLM) |
| L2 本地通用缓存表 | 同设备任意 (word,sentence) | ❌ 未做(价值低,未存词重现率低) |
| L3 服务端缓存(Edge + Supabase 表) | 跨端跨用户 | ❌ 未做(RVH 立项时一起做) |
L1 推荐方案(恰好覆盖 review 缓存页场景——被高亮的词就是已存词、句子就是存的句): 弹窗打开时查 word_cloze_contexts 该词行,若 (sentence 匹配当前句 && sense_gloss 非空) → 按 gloss 文本在 entries 里重定位出 pos/sense_index → 直接 applyDisambiguation + 种入 disambigCache,零 LLM 零等待。 持久层兼作缓存,无新表。
不能缓存/不应持久化的边界(已成立的设计,勿破坏):
- null/低置信结果:不入缓存不持久化(已如此——避免把瞬时故障钉死成"无匹配")。
- L2/L3 若做:key 必须含词库版本/候选 glosses 指纹,否则 reseed 后 index 漂移重蹈"自信标错";值只存 hash→(pos,sense_index),不存原句(隐私面不扩大,跨用户共享安全)。
- 短语 explain_phrase(生成式 explanation):持久化更谨慎,暂不做。
RVH 跨端消歧评估:
- 易:
disambiguate-senseEdge Function 端无关(HTTP + anon key),Flutter 一个 POST;byte-equal 预装库 (红线 #10)保证 candidates 跨端逐字节一致 → L3 服务端缓存天然跨端命中,这是 byte-equal 的额外红利。 - 难(产品决策非技术):
word_cloze_contexts目前 local-only(v16)。RVH 无阅读功能,语境句只能来自 RB—— RVH 要显示 cloze 句 + sense 高亮,几乎必然要把这张表纳入同步矩阵(Supabase DDL + RLS + 双端 sync + 红线 5b/5c 配套,按 CLAUDE.md §9 协议走,RVH 侧新会话)。Dart 侧另需 gloss 文本重定位渲染的等价实现。
顺带观察(独立小问题,自然触达时处理):
- review 缓存页 console 反复
[RB] effectiveSourceUrl: rb-cache:// without review/epub metadata警告。 - rb-debug-mcp rust 日志通道疑似失效:消歧成功时
supabase.rs的info: hit未出现在 rb_logs(console 正常), 连续两个会话观察到"rust": []。
🟡 复习缓存页"答案词高亮/定位"重构(新会话,2026-06-13 记录)
触发条件:用户说「复习缓存页高亮 / recall 答案泄露 / 原型匹配高亮」即启动。纯 RB 侧。
复习时右侧缓存页给复习词高亮+定位,三个纠缠问题需一次想清:① recall 阶段提前泄露答案(cloze 先回忆再揭晓) ② Find 精确匹配标不全屈折形(send↔sent、cat↔cats)③ Find 面板默认展开占空间。探索中发现可见 amber 是旧 hero-word 系统(review.js highlightHeroWord,index.js onPageReady 触发)而非 Find——且 MainApp.tsx 注释本意是"Find 取代 hero"但没删干净。完整分析 + 候选方案(统一到 Find 多词形 OR 匹配 / 复活 hero 加 masked / masked 透明高亮思路)+ 文件锚点见 docs/plans/archive/review-recall-source-highlight-handoff.md。本会话已回退探索代码,留干净基线。
🟢 推荐发现算法优化 —— Phase 1+2 已落地,剩余 tier-aware 排序等未来项(2026-06-15 更新)
真相在
docs/plans/archive/recommendation-discovery-optimization-plan.md(含完成记录 + 剩余项)。纯 RB 域。
已完成:
- Phase 1(双档 targeting prompt + 格式过滤 + 第一层机械校验 + analyze 跨源 round-robin/新鲜度 + 发现链独立 reasoner
DISCOVERY_LLM_MODEL)+ Phase 2 grounded 审核(真实样本喂快 chat 判 keep + 修正, 优雅降级)(2026-06-15,commits 962e370 / 158e5d2)。三端一致性已核对干净。 - ROTATION grounded 健康检查(
healthCheckGrounded:真实抓老站 URL+feed+样本判 still_active, 替代凭记忆瞎猜;重试抗抖动 + 保守降级 + 死站按 rss_url 关 feed +?mode/?dry验证钩子)- publisher 级去重(analyze round-robin 分组键 feed URL→可注册域名,同发行方多 feed 坍缩) (2026-06-15,dry-run + 实机 invoke 验证通过)。
剩余未来项:
- ✅ tier-aware 排序(Phase 0-5 全部完成,2026-06-15):完整设计稿 + §进度 + 验证记录见
docs/plans/archive/tier-aware-recommendation-sorting-plan.md。范围 = 站点/Feed 加cefr_band(persist discover 审核已算值 + 一次性 grounded 回填)按水平分桶排序 + 文章侧兑现 V1-V2 排序合流(前端recommendRank.ts:CEFR i+1 甜区 × 兴趣命中 × 在学词)。不做难度过滤 UI、不碰周回顾 next_step。Schema v19 ALTER-ADD。 状态:已部署 + 端到端验证通过(Dashboard ALTER → deploy discover-favorite_sites → backfill-bands 写 62 站+35 feed → 本地镜像/文章 ranking/站点桶序 rb-debug 全验过)。站点/Feed 侧已上线生效。 🔴 文章侧 gate 仍在:A1+A2 供给薄(实测 4),文章侧排序短期效果弱(门控安全无害),须等下方「推荐分级供给侧补强」线补强后才显效。 - 推荐分级供给侧补强(tier-aware 文章侧的真正前提,独立线):2026-06-15 实测根因不止"缺 feed"—— 种了 News in Levels 仍产出 0,真因在
analyze-articles自身两处。完整实施 + 验证见docs/plans/archive/recommend-graded-supply-side-plan.md。- ① seed feed(完成):候选实测后只 News in Levels 真缺+活+按 level 拆页(最佳 A1/A2 源); BNE/News for Kids(A2)/SNS 已在池、VOA 全 zone 2025-03 冻结(停摆)弃。seed 文件
supabase/sql/recommended-graded-feeds-seed.sql(band=A2),用户已 Dashboard 跑。 - ② 修 analyze-articles(完成,已 deploy):根因 1 = round-robin 位置饿死(新 feed 永不被采样)→ 改 band 优先 + rest 时间轮换;根因 2 = CEFR 只看标题打分偏高(News for Kids 被评 B1)→ feed band 作 先验 + A1/A2 锚点 prompt。实测两 fix 生效:News in Levels level-1→A2、News for Kids B1→A2、 A1+A2 即时 3→5,7 天 TTL 窗口内随 News in Levels 日更累积爬向 gate ~12-15。
- 剩余未来项:A2 仍偏少 → 找更多活的分级 feed(生态稀少);或为无 RSS 分级站建非 RSS 抓取路径 (另立 plan,工作量大);analyze 不抓正文 → CEFR 仍非完美,正文抓取留 refinement。 触发:用户说「补分级供给 / 找更多分级 feed / 修 A2 供给」。纯 RB、无 RVH。
- ① seed feed(完成):候选实测后只 News in Levels 真缺+活+按 level 拆页(最佳 A1/A2 源); BNE/News for Kids(A2)/SNS 已在池、VOA 全 zone 2025-03 冻结(停摆)弃。seed 文件
- 🚀 推荐系统 agent 化演进(总设计已定稿,多会话工程):把推荐系统从"脚本管线"演进成 「可观测/有记忆/可治理的半 agent 策展系统」。总设计 =
docs/plans/archive/recommend-agent-evolution-plan.md。 优先级=质量+稳定(不贪大已淡化);现有 cap+churn 模型与稳定错配 → 改稳定核心+保守增补。 横跨三仓库(RB edge/schema + readVocab-admin 后台 + 客户端基本不动),分三阶段、各一会话:- ✅ Phase 1 共享 LLM/审计层(完成+验证,2026-06-16):
_shared/llm.ts统一 callLLM + 记账; 三张 log 表(observability-tables.sql,用户已 Dashboard 跑);discover-favorite_sites + analyze-articles 已接入并验通 (llm_call_log/edge_run_log 落行、cost 算对)。8 个 LLM function 已全迁共享层(2026-06-16),成本全可见。剩预算闸(低优)。 - ✅ Phase 2 池模型改造(完成+验证,2026-06-16):废 POOL_CAP churn → 稳定核心(health 永远跑)+ below-target 保守增补;白名单(
is_pinned)/黑名单(recommended_blocklistauto_reject+eliminated_dead)/最小存活期(admitted_at); 双源发现(LLM 记忆 ∥ 种子外链一跳分类,防幻觉 guard);多维审核(reading_value/lang/form/ad_noise)+cleanReadingScore启发式;recommend_config配置化 + 预算闸;recommend_audit_log全 populate。共享_shared/url.ts+governance.ts。 客户端零改动。commits 3b1435b/7bb8dda/5b727b1。实测:health 保守降级 + 双源高门槛 + 真跑 seed_link 入 Brain Pickings 全通。 详docs/plans/archive/recommend-agent-evolution-phase2-plan.md。剩余手动验证项(逻辑已上线,等自然/手动触发):预算闸(需临时调recommend_config.monthly_llm_budget_usd)、blocklist 写入(需出现硬拒/死站)。 - Phase 3 运营后台:readVocab-admin(
~/readVocab-admin,Next.js)加监控/配置/图表/复核队列/预算闸 UI。 必须新会话(跨仓库,栈 Next.js/pnpm/Vercel)。启动上下文包(新会话先读)=docs/plans/recommend-agent-evolution-phase3-admin-startup.md(含表精确列定义 + admin 现状勘察 + C1-C4 落点映射 + service_role 坑)。 明确非目标:完整自治 agent、Playwright/代理/ES/Redis/RL、编排搬出 Supabase。 触发:用户说「推进 recommend 演进 / 做共享 LLM 层 / 改池模型 / 做 admin 运营界面」。
- ✅ Phase 1 共享 LLM/审计层(完成+验证,2026-06-16):
- 深度议题(未启动):跨源语义去重(同事件多篇)、CEFR 强制分布补齐、互动反馈闭环 (注意 memory
project_recommend_stream_strategic红线:优化不砍)。
跨端整合尾项(2026-08-14 自 docs/archive/09-双端整合实施计划.md 转入)
09 那份实施计划已归档:61/65 项完成,阶段 0/1/2 全绿。剩下这 4 项原是它的"阶段 3 体验闭环", 但它们是四个互不相干的独立功能,不再构成一份整合计划——放这里按普通 backlog 条目排期即可。 原文的背景推理(工作量估算、MVP/进阶分拆理由、非目标)仍在归档件里,动手前值得回看。
⚠️ 原文用 2026-07 改名前的表名,下面已换成现名。
1. RVH 复习推送通知(3-2)
- MVP(RVH 侧,约半天):主页/复习页加"学习汇总条",拉 Supabase 显示"今日新增 N 词 / 待复习 M 词"。 纯应用内提示,不碰系统级推送。
- 进阶(3-5 天 + 基础设施调研):APNs / FCM 接入 Supabase Edge Function trigger,需证书管理 + device token 登记。分拆理由:MVP 用现有同步栈即可出效果,进阶要引入新依赖和发版流程。
- 归属:RVH 新会话(CLAUDE.md §9 会话隔离规则)。
2. 跨端学习报告(3-3)
- 零新增 schema,数据源全是现成表:
learning_entries(新增/到期/掌握度分布)·reading_pages(近期阅读源)·word_page_links(哪篇文章贡献最多生词)。 - RB 侧已有 Stats 模块可挂;RVH 需新建入口。
- 非目标:不做跨用户排行、不做趋势预测。第一版能看数即可。
3. 跨端引导入口(3-4)
- RB → RVH:设置页"在手机上继续学习"二维码(deep link
rvh://auth/bootstrap?email={mask})+ 商店链接。 - RVH → RB:设置页"桌面端下载"链接到 Releases / 官网。
- 🔒 Auth 不跨端携带 token:引导只做到"方便找到入口 + 预填用户名",密码仍需手输。
- Deep link 注册只需 RVH 侧(Flutter iOS/Android 清单);Tauri 桌面端不接收 deep link。
4. 账号删除 / 数据导出(3-13,合规向)
- 触发:GDPR 类"删除我的账号 + 所有数据"请求。对外发版后这条的优先级会上升。
- 实现:Supabase Edge Function 聚合删除(客户端 anon_key 删不了
auth.users)+ 两端设置页入口。 - 数据导出(按 user_id 导出 JSON 压缩包)优先级低于删除。
- 关联:
docs/plans/product-launch-todo.md的合规条目。