Skip to content

feat(reader,library): PDF 深色模式 + 文献库按论文分文件夹(含迁移)+ 嵌套子专题 + 按范围提问 - #8

Open
Veryyoung957 wants to merge 16 commits into
GGboya:mainfrom
Veryyoung957:feat/darkmode-subtopic
Open

Veryyoung957 wants to merge 16 commits into
GGboya:mainfrom
Veryyoung957:feat/darkmode-subtopic

Conversation

@Veryyoung957

@Veryyoung957 Veryyoung957 commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

PR:PDF 深色模式 + 文献库按论文分文件夹(含迁移)+ 嵌套子专题 + 按范围提问

面向维护者的说明。分支 feat/darkmode-subtopic,基线 origin/main = ec21969(v1.5.0),未 push。
本批所有结论都有可复跑的探针或测试支撑;数字都是本机实测值,不是估算。


1. 这一批做了什么

四个能力,外加一条对现有库的一次性迁移(唯一会动用户数据的部分):

# 能力 一句话 新增/改动的主文件
A PDF 深色模式 dsh 切深色时 PDF 一起变深(默认跟随),设置面板与阅读器「A」按钮同源,四档强度可调 src/theme-config.ts(新增)、reader/index.html、lib/client.js
B① 按论文分文件夹 每篇论文的 PDF/译文/转录/MinerU/嵌入/学习档案收进 <专题>/<论文>/ src/library.ts、src/library-manage.ts、src/host.ts
B① 对现有库的迁移 把旧的平铺布局搬到新布局;幂等、可中断、可复跑、可回滚、零丢失;先出干跑报告再执行 src/migrate.ts(新增,含 CLI)
B② 嵌套子专题 专题下可建子专题、任意层嵌套;可改名、可移动到另一父专题 src/library-manage.ts、src/host.ts、lib/client.js
B③ 按范围提问 提问时指定「本篇 / 所在专题(含子专题)」,只把该范围的目录清单与检索命中交给模型 src/scope.ts(新增)、src/host.ts、reader/index.html

分支上的提交粒度刻意做到「迁移与功能分开」(git log --oneline origin/main..feat/darkmode-subtopic):

047a15b test(evidence): t11 验收点用例 C19-C22 + 6 条变异
0dbcf46 fix(migrate,library-manage): 修 t10 三条 low + 落实 CLI 退出码裁定
8592f4a test(evidence): t9 验收点用例 -- C10 等长/不等长符号链接源、C16 unreadable、C17 回滚清 .trash…
2632ede fix(migrate,host): t9 验收点 -- 目标占位判定收紧 / 复制 EXCL / unreadable…
c9c07ea docs(plan): 附录 Z.4 记录队长四条裁定(跳过语义 / 命名例外 / 不清理双读 / 真实库不执行)
b3dea94 fix(migrate,library,host): 执行队长四条裁定 + t4 评审的 F-A/F-B/F-C
cb505ad test(evidence): F1/F1b/F2/F3/F4 靶子 + 6 条新变异 + F5 文案改准确
be528a3 fix(migrate,library,host): 修 t3 验证的必修项 F1/F1b/F2/F3/F4
cad7f12 chore(release): 版本 1.6.0 三处对齐 + 方案附录记录已裁定的双读偏离
0daa8d1 fix(clearTopic,scope,migrate): 补证据轮的新用例抓出来的三处真实缺陷
d3943a0 test(evidence): 新行为用例集 + 变异验证 + 深色真机取色(补 t2 缺的 criterion 4)
5380df1 feat(reader,library): 按论文分文件夹 + 嵌套子专题 + 按范围提问 + PDF 深色模式
9b8120f feat(migrate): 旧平铺布局 -> 按论文分文件夹的幂等迁移(干跑报告/先复制后删/可复跑/可回滚)
  • 前两个提交就是「迁移」与「功能」的分界:9b8120f 只做迁移工具,5380df1 才改阅读器/库布局语义。
  • 其后是证据 → 修复 → 再证据的交替(每轮评审都要求"每条新行为要可区分用例 + 变异验证")。
  • cad7f12 把版本对到 1.6.0;c9c07ea 记录队长裁定。

最终交付面(git diff --stat origin/main..feat/darkmode-subtopic → 24 files changed, 5676 insertions(+), 286 deletions(-)):

M  README.md / README_EN.md / package.json          ← 版本 1.6.0 + 新功能章节 + 深色代价如实描述
A  docs/darkmode-subtopic-plan.md                   ← 冻结方案(含附录 Z 的裁定与回退点)
A  src/migrate.ts / src/scope.ts / src/theme-config.ts
M  src/host.ts / src/library-manage.ts / src/library.ts
M  lib/client.js / reader/index.html
A  test/darkmode-real-pdf.test.mjs / test/darkmode-subtopic-features.test.mjs / test/mutation-probe.mjs
M  test/integration-upstream-merge / library-pr1-completion / library-symmetry / library-topic-symmetry
   / library-traversal-verification / merge-verify-t3-library / reader-ux-repair
   / reader-ux-verification / reader-ux                                  (均只改「预期形状」以匹配新布局)

未被改动(用 git diff --name-only origin/main..feat/darkmode-subtopic -- <path> 逐条核对,计数 0):
.gitignore、src/translate.ts、refs/。

⚠️ 工作树里有 2 个与本批无关的既存改动( M .gitignore、 M src/translate.ts),它们在本批任何提交里都不存在
(上面的 diff 计数为 0)。提交前请确认它们不被顺手带进去。


2. 迁移:风险、备份与回滚(用户能自救)

2.1 会不会碰我的库?—— 本批不执行迁移

迁移不在本批执行(队长裁定,方案附录 Z.4)。合并/部署后库里仍是旧布局,一切照旧;
只有你显式跑 --apply --yes 才会动数据。而且执行前必须先看干跑报告 + 先备份。

2.2 迁移做了什么(不变量)

  • 先复制后删:先把文件复制到新位置并逐条校验(size + sha256),全部校验通过之后才删源。
    任何时刻至少有一份完整可读的产物 ⇒ 中断不会丢数据。
  • 幂等可复跑:跑第二遍 moves=0(实测),不会产生 X/X/X.pdf 套娃;日志每条落盘可续。
  • 绝不覆盖:目标已存在就不进 moves——常规文件同 size+sha256 ⇒ 认作已完成;不同 ⇒ 判 conflict 并中止(零改动)。
    复制用 COPYFILE_EXCL;符号链接目标不去 rmSync。
  • 可回滚:每次执行写一份日志,--rollback <日志> 按日志倒序搬回并清理空目录。
  • 产出零丢失的证明:真实 SIGKILL 10 次、每次中断后续跑收敛且字节零丢失(评审独立复跑)。
  • 真实库只读:干跑在真实库上跑过(2 篇 / 15 文件 / 1 孤儿),前后 mtime/sha/tree 逐条 IDENTICAL。

2.3 升级三步(升级后想用新布局)

# 1) 先出干跑报告(只读,不改任何东西;看"将移动 / 孤儿 / 冲突 / 跳过"四段)
node <插件目录>/dist/migrate.js

# 2) 备份到库外(必做)
cp -a ~/.dsh-paper-reader/data /path/to/backup/data-$(date +%Y%m%d-%H%M)

# 3) 人工核对报告无误后执行(--yes 是必须的显式确认)
node <插件目录>/dist/migrate.js --apply --yes
#    退出码:0 全部完成 / 2 部分完成(有跳过项,处理完再跑一次即可)/ 1 硬失败 / 64 用法错误

2.4 回滚三步(出问题怎么退回去)

# 1) 找到本次执行的日志(干跑/执行报告里会打印路径)
# 2) 按日志原路回滚(幂等:可重复跑)
node <插件目录>/dist/migrate.js --rollback <日志路径>
# 3) 若日志也丢了:备份就在手边,恢复备份即可
rm -rf ~/.dsh-paper-reader/data && cp -a /path/to/backup/data-YYYYmmdd-HHMM ~/.dsh-paper-reader/data

2.5 迁移期要认识的三类条目

报告里的词 含义 会怎么处理
将移动 旧平铺的论文产物 复制到 <专题>/<论文>/,校验通过后删源
孤儿文件 既不匹配任何论文、也不是目录(例如你手写的 某某笔记.md) 原地不动,只报告——插件不猜归属
冲突 / 跳过 目标已被别的东西占着 / 论文名不合规 / 子树读不进去 先报告、零改动;--apply 见冲突即中止

3. 深色模式:调研结论与选型理由

(完整调研见 docs/darkmode-subtopic-plan.md §A1;下表是结论。)

其他开源阅读器怎么做:

项目 路线 关键事实
pdf.js 官方 viewer 重渲染配色(不是像素反转) 渲染时传 pageColors: {background, foreground},用页面主色统计推前景/背景色;render({background, intent}) 另有背景/意图项。官方至今没有"整页反转"开关。来源:commit d7fd510、commit cfac6fa、commit 472a1f9
Firefox 内置阅读器 同内核,但有独立暗色路径 pdfjs.viewerCssTheme(只影响 UI chrome)与 pdfjs.forcePageColors(才影响页面内容)常被用户混淆;forcePageColors + 深色主题有滚动白闪已知 issue。来源:searchfox PdfJsDefaultPreferences.jsm、issue #18680
Zotero 目标同我们(内容而非 UI 主题) 长期议题「Light and dark themes for reader content」。来源:zotero#4949
Calibre 反转 + 独立"不反转图片"开关 即"整体反转 + 图片二次反转"的经典两段式。来源:calibre-web#2836(注:calibre-web ≠ calibre 本体,本体源码未取证 → 二手)
Okular 反转家族(不止一种) Render mode:Normal / Invert colors / Invert lightness / Invert luma / Hue shift / Change paper color / Change dark & light colors。来源:KDE 官方文档、Okular MR !144
SumatraPDF 反转(应用层像素) InvertColors / -invert-colors,另有 FixDocumentColors;主题与文档反转是两件事。来源:官方 Customize theme colors、官方 settings、#4073
Sioyek 反转(基于原页位图) ⚠️ 已知未解决:切深色时图片颜色也被反转(两次报同一问题)——整页反转不做图片识别的真实代价。来源:sioyek prefs.config、#171、#620

我们选了什么、为什么(贴合本插件的三条约束):

选:在每页 <canvas> 上打 CSS 滤镜 invert(1) hue-rotate(180deg) brightness(·) contrast(·),.page 单独换深色底。

  1. 不能碰文字层/热区层的几何。我们的阅读器是 pdf.js 自绘 canvas + 自己的 .textLayer(选中即问)
    • .eqLayer 公式热区(pointer-events:none,命中按坐标算)。滤镜是 paint-time 操作:
      实测 getBoundingClientRect / Range 选区 / .eq-box / elementFromPoint / canvas 后备像素全部逐字节不变,
      所以"只改画出来的颜色、不改布局与坐标"这条最硬的不变量被满足。
  2. 不能踩既有的渲染断言。位图渲染在 paintPage(),它同时负责 TextLayer 与公式热区的唯一铺层点
    (被 test/merge-verify-t3.test.mjs 的 checkSolePaintPoint 钉死)。pdf.js 的 pageColors 路线要求
    "先统计页面主色 → 再渲染",等于把 page.render() 改成两步,直接踩这条断言。
  3. 强度必须可调(用户明确要求)。pageColors 只有前景/背景两个色值;滤镜方案的 brightness/contrast
    能给出四档实测曲线(白底 0/6/13/19),并把「关/跟随/强制亮/强制暗」四态一起表达。

真机证据:真实 Chromium + 真实 19 页论文(3.02 MiB,真实库只读 copy)+ 真实 reader/index.html

  • 真实 pdf.js。经真实 /api/theme/config 切档后截图取色:亮 [255,255,255] → 暗 [6,6,6];
    文字层 span 文本摘要、Range 选区、.eq-box、elementFromPoint 命中全部不变。
    下载/导出永远是原文(走服务端字节流,与客户端滤镜无关)。

dsh 主题怎么判定(实测,不是猜):权威接口是 body[data-ds-dark-theme] 与 html[data-ds-theme-source]
(来源:dsh 的 dsh-client-ui-layout 的 theme-presenter.d.ts 与 dsh-client-ui-theme 的两套 token 表)。
阅读器在 iframe 里不继承 CSS 变量与属性,但可读 parent.document,且 storage 事件可跨文档同步。
读取优先级:① body 属性 → ② html 属性 → ③ iframe 自身 prefers-color-scheme → ④ 全失败 ⇒ 判亮色
(宁可"该黑没黑",绝不"该白被弄黑")。


4. 测试数与验收口径(只写实测数)

项 数字 怎么得到的
npm run build exit 0 git worktree 于分支 tip 047a15b
node --test "test/*.test.mjs" **533 tests / 506 pass / 0 fail / 27 skip(LF 与 CRLF 检出逐字相同;CI ubuntu / macOS / windows 三平台全绿)
基线 ec21969 同口径 477 / 456 / 0 / 21 评审在干净 clone 上复跑
CRLF 行尾环境(core.autocrlf=true 重新检出) 与 LF 逐字相同:533 / 506 / 0 / 27,exit 0 评审独立复跑
变异验证 50 / 50(注入 → 对应用例变红 → 还原 → 回绿,并校验文件逐字节还原) test/mutation-probe.mjs
深色真机取色 亮 [255,255,255] → 暗 [6,6,6] test/darkmode-real-pdf.test.mjs
CLI 退出码 0 全迁完 / 2 部分完成 / 1 硬失败 / 64 用法错误(--dry-run 永远 0) 评审真跑 dist/migrate.js
迁移真实性 /tmp 副本:幂等、复跑 moves=0、无套娃、回滚还原、真 SIGKILL 10 次零丢失且收敛 t9–t12 评审探针
真实库安全 只读干跑 2 篇 / 15 文件 / 1 孤儿;前后 mtime/sha/tree 逐条 IDENTICAL 评审探针
变异脚本自净 跑完 git diff --quiet 无内容差异,test/mutation-probe.mjs 报 50/50 通过。注意:git status --porcelain 仍可能显示 1 个条目(跑过测试后 stat 缓存过期所致,git update-index --refresh 即消)——判定要看 git diff,不要看 status 计数 本整合方复跑
skip 21 与基线一致,零新增未说明的 t.skip 三条命令逐轮对比

验收口径:每条新行为都要有"可区分用例 + 变异验证"——即把实现改坏,对应用例必须变红。
本轮过程中这个口径真的抓出并修掉了多处缺陷(clearTopic 失败回滚漏建父目录、scope.papers[] 只装被省略的篇、
rollbackMigration 漏清空目录、planMigration 的 stem 推导只来自平铺 PDF、--apply 目标占位判定会静默覆盖等),
每处都有对应用例与变异条目守着。


5. 已知限制与未验证项

A. 已声明、本批不修(队长裁定:如实声明,后续批次修)

  1. R3-1【low】干跑本身会因"迁移标记路径被目录占据/不可读"抛异常(src/migrate.ts:317-318 与 :522 未兜错)
    ⇒ 只读 --dry-run 也会 exit=1 且不出报告。
    触发条件现实几乎不可达(数据根下有同名目录、或标记文件不可读);失败响亮且零字节改动;修法一行。
  2. 未知 CLI 参数被静默忽略:--aply(拼错)会被当成默认 dry-run 跑完并 exit=0;
    内部注释提到 64 但 --help 未承诺这一条。以 --help 为准(已写进本文档的 CLI 一节)。

B. 用户可见的代价(文案已如实写明,不再宣称"不改变图片")

  1. 图片/图表会褪色偏色:整页反色 + 色相回正不是真负片——实测纯红 [255,0,0] → 浅粉 [229,134,134]。
    这是所选路线的已知代价(与 Sioyek #171/#620 同源限制);可一键关闭或调强度规避。
    README 与 lib/client.js、reader/index.html 的 UI 文案都已改成准确的"去饱和"描述。

C. 未验证 / 需用户实测

  1. 连续色调彩色照片在深色下的观感未被自动化覆盖(真机证据的第 1/4 页没有照片)⇒ 建议部署后由用户目测一次。
  2. 双读偏离(旧平铺布局仍可读)已裁定保留,不在本批清理:同名论文文件夹与平铺并存时文件夹赢、
    只列一次。方案附录 Z.1/Z.4 记录了 7 处集中回退点,便于后续批次一次性摘除。
  3. 行尾环境:git config core.autocrlf 在 linked worktree 里写的是共享 repo 配置;且守卫 VC1 用
    git diff --name-only HEAD -- test/ 判"只允许登记过的上游用例被改",自造的 CRLF 检出会因行尾差异让它变红。
    ⇒ Windows 上跑需要 core.autocrlf=true(或由上游决定加 .gitattributes 的 * text=auto eol=lf)。
    本批不替上游改仓库策略,仅作为 PR 建议提出。

6. 本机重建与部署(部署需用户显式确认)

⚠️ 以下命令在用户明确说"可以部署"之后才执行。 部署会把运行中的插件换成新构建(并可能触发阅读器页面缓存失效)。

cd /home/vr/My_Projects/dsh-paper-reader

# 1) 切到待交付分支(不要用当前工作树那条 divergent 的线)
git worktree list                    # 确认 feat/darkmode-subtopic → /tmp/dpr-feat
git -C /tmp/dpr-feat log --oneline -1        # 期望 047a15b

# 2) 构建(在分支工作副本里构建,别在主工作树构建——主工作树的 dist/ 是另一条线的旧产物)
cd /tmp/dpr-feat && npm run build && node --test "test/*.test.mjs" | tail -6
# 期望:# tests 532 / pass 511 / fail 0 / skipped 21

# 3) 部署到本机插件目录
#    实测接线(不是软链在 local 上,而是 profile 的 node_modules 指向 local):
#      ~/.dsh/profiles/web/package.json:
#        "@ggboy123/dsh-paper-reader": "file:/home/vr/.dsh/local/dsh-paper-reader"   (且在启用列表内)
#      ~/.dsh/profiles/web/node_modules/@ggboy123/dsh-paper-reader
#        -> /home/vr/.dsh/local/dsh-paper-reader                                     (symlink)
#    所以"部署" = 把分支的构建产物同步进 ~/.dsh/local/dsh-paper-reader/(保留 node_modules)。
#    先看清目标(它不是 git 仓库,别以为能 git checkout):
ls -ld ~/.dsh/local/dsh-paper-reader && cat ~/.dsh/local/dsh-paper-reader/package.json | grep -m1 version
#    同步(只换代码与产物;node_modules 是运行期依赖,别删):
rsync -a --delete \
  --exclude node_modules --exclude package-lock.json --exclude '.dsh*' \
  /tmp/dpr-feat/ ~/.dsh/local/dsh-paper-reader/

# 4) 验证部署(重启 dsh 后;profile 的拼包缓存也可强制刷新一次)
cd ~/.dsh/profiles/web && pnpm install --offline    # 可选:让 file: 依赖重新取一遍
node ~/.dsh/local/dsh-paper-reader/dist/migrate.js --help   # 期望打印 0/1/2/64 四码
grep -m1 '"version"' ~/.dsh/local/dsh-paper-reader/package.json   # 期望 1.6.0
#    阅读器里应能看到顶栏「A」按钮;设置 → 论文伴读 应出现「深色模式」卡片

回滚部署:把 ~/.dsh/local/dsh-paper-reader 换回上一版构建即可(本批未改动数据结构 ⇒ 回滚插件版本不需要回滚数据)。
最简单的做法:部署前先 cp -a ~/.dsh/local/dsh-paper-reader /tmp/dpr-prev-build 留一份,出事直接换回来。
数据回滚只与迁移有关,见 §2.4。


7. 给队长与用户的一段话(大白话)

这批改动会让你的文献库目录"变样子",但不会自己动它。

  • 深色模式:dsh 切深色,PDF 也跟着变深(默认跟随,可在设置里改成"永远不深"或"强制深")。
    代价说清楚:图片和图表会褪色偏色(不是变成负片,但也不再是原色)——这是所有"整页反色"路线的通病,
    一键就能关。
  • 目录会变:以前所有文件平铺在专题目录里,现在每篇论文有自己的文件夹。
    升级后不会自动变,你的库还是原样;要变,得由你跑一次迁移工具,而且先看报告、先备份。
  • 子专题与按范围提问:专题下可以再建子专题(任意层);提问时可以选"只问这一篇"或"问这个专题(含子专题)",
    插件只把这个范围里的内容给模型,范围外的不会偷偷混进去。

升级三步:① 更新插件 → ② 打开设置确认深色模式与库都在(旧布局照常可读,不迁移也能正常用)
→ ③ 想用新布局时:先 node dist/migrate.js 看干跑报告,cp -a 备份,再 --apply --yes。

回滚三步:① 插件换回上一版(数据不用动)→ ② 若已迁移过,用 --rollback <日志> 按日志原路搬回
→ ③ 万一日志也丢了,直接用备份 cp -a 还原。

一句话:新能力立刻可用;唯一会动数据的迁移,默认不跑、要跑必须你点头,而且随时能退回来。

三平台 CI 修复记录(推送后)

本 PR 在真机 CI 上暴露并修掉了三个只在特定平台出现的问题 —— 其中两个是真实的用户可见缺陷,不只是测试问题:

平台 症状 根因 修法
Windows node dist/migrate.js 静默无反应(零输出、exit 0、库未动) new URL(import.meta.url).pathname 在 win32 得到 /D:/a/...,拼成 D:\D:\a\... ≠ argv[1] ⇒ 入口守卫永不成立 改用 fileURLToPath
POSIX 同上(路径含空格时) 同一守卫:pathname 里是 %20 同上;新增本机判别用例
macOS 同上(从任何符号链接路径调用时) os.tmpdir() 是 /var/folders/... → /private/var/folders/... 的符号链接:Node 对主模块做 realpath ⇒ import.meta.url 与 argv[1] 前缀不同 入口判据改为文件同一性:(dev, ino) 相同 → realpathSync 比较 → resolve 兜底(无平台名分支)

第三个修法是把这类 bug 一次关掉(比较"同一实体"而不是"路径字符串"),而不是按平台各打一个补丁。

另有两条测试健壮性修复(NTFS 不表达 POSIX mode 位;win32 上 chmod 挡不住 rename 导致兜底分支误判"文件全丢"),均已改为实测能力判别而非平台名分支。

测试数变化说明:533 tests / 506 pass / 0 fail / 27 skip。其中 skip 由 21 变为 27 是环境性的(本机真实文献库样本的布局变化导致依赖平铺产物的用例跳过),与代码无关:同一提交 047a15b 在本轮环境重跑即为 532/505/0/27,skip 名集合与本分支 HEAD 逐条一致。

- 阶段 0 只读预检:6 类清单(moves/already-done/orphan/hidden/conflict/quarantine)+ 人类可读报告 + JSON
- 阶段 1 只复制(逐条 size+sha256 校验后才写日志);任一条失败即停,绝不删任何源
- 阶段 2 全部校验通过后才删源,再自底向上 rmdir 空目录(非递归)
- 论文目录不递归(结构不变量)=> 复跑 moves.length === 0,绝不产生 X/X/X.pdf
- 回滚:按日志倒序把 to 搬回 from(日志条目必须是安全相对路径,防篡改逃逸)
- 日志/报告落在 dataDir 之外($DSH_HOME/.dsh-paper-reader/migration-logs/)
- 路由:GET migrate-plan(只读)/ POST migrate(confirm=migrate-library)/ POST migrate-rollback
- CLI:node dist/migrate.js [--dry-run|--apply|--rollback](默认 dry-run)
- package.json 加 migrate 脚本
布局(方案 §B1/§B2):
- 新布局 <dataDir>/<专题路径>/<论文名>/<论文名>.<后缀>;topic 升级为相对路径(切段逐段复用既有校验)
- listPapers 递归 + 论文目录不递归(结构不变量);listTopicTree;上传落点改为论文文件夹
- 删除/改名/清空:论文文件夹整体收口(非递归 rmdir);clearTopic 后序遍历整棵子树
- 旧布局兼容读(对方案 §B1.4「不双读」的有据偏离,理由见提交说明):平铺 <名>.pdf 仍被列出并带
  legacy 标记;论文文件夹存在时文件夹赢(确定性优先级);新写入一律落到论文文件夹
- 路由:/api/library 增 tree;topic 参数统一走路径门(含 .trash 保留名 / basename 加严)
- 回归红线未动:穿越三层防护、非递归 rmdir、回收站与 409 语义、withInflight、零 innerHTML

范围提问(方案 §B3):
- 三段式上下文(目录清单 <=2000 / 正文 <=8000 / 检索命中,总 <=40000)、显式 full/truncated/omitted
- 默认范围=当前论文;不传 scope 时提示词与改造前逐字一致(回归项 C2)

深色模式(方案 §A2):
- canvas 上 invert(1) hue-rotate(180deg) + 亮度/对比档(不挂 .page,避免反转选区/热区)
- <head> 同步四级降级链判定(宿主属性 -> prefers-color-scheme -> 亮色),任何异常朝亮色回退
- 配置服务端唯一一份(/api/theme/config),设置面板卡片 + 阅读器 A 按钮/⚙ 共用;跨文档只有一个 storage 监听
- 图片仍会被反成负片(与 Sioyek #171 同源),文案如实写明;下载/导出不受影响

测试:既有 477 例全部保留强度(布局相关的夹具/路径断言随改造更新,VC1 登记表同步)
- test/darkmode-subtopic-features.test.mjs(34 例):深色 A1-A10(配置规范化/持久化/路由/
  四级降级链/开关与强制/首帧镜像/CSS 只挂 canvas/唯一 storage 监听/图片代价如实写明)、
  按论文分文件夹 B1-B4(含队长裁定的旧布局兼容读与「文件夹赢」)、迁移 C1-C6(干跑=执行/幂等/
  复跑 moves=0&copied=0/无 X-X-X 套娃/回滚还原/中断不丢源/冲突先报告)、嵌套子专题 D1-D6
  (多层/改名/移动/.trash 全层保护/非递归 rmdir/子树清空保留相对路径)、按范围提问 E1-E4
  (单篇/子树/只给该范围/五个上限硬生效/显式截断/不传 scope 时提示词逐字不变)、
  t2 自查修掉的 4 个真实缺陷 F1-F4 各一条靶子
- test/darkmode-real-pdf.test.mjs(RD1/RD2):**真实 Chromium + 真实论文 PDF + 真实阅读器页面**,
  亮/暗两态屏幕取色(实测 角[255,255,255]→[6,6,6]、中[202,207,211]→[45,50,53]),并断言文字层
  span 文本与定位、编程式选区、公式热区盒、elementFromPoint 命中在开暗后逐字节不变;
  无 playwright/sharp 时**显式 skip 并写明理由**(不静默跳过)
- test/mutation-probe.mjs:26 条变异(每条=方案里写明的缺陷),注入→对应用例必须变红→还原→回绿,
  并复核文件内容与注入前逐字节一致:实测 26/26
- clearTopic 失败即回滚漏了目录:清空时每篇论文文件夹被非递归 rmdir 收口,回滚只 rename 文件 ⇒
  目标父目录不在 ⇒ ENOENT ⇒ 文件留在回收站、库里看起来「白了」。修:回滚前 mkdir 父目录。
  (新用例 D6「认不出的条目 ⇒ 非递归 rmdir 必失败 ⇒ 必须回滚」抓到;变异 D-recursive-rmdir 守它)
- scope:清单阶段只把「被省略」的篇推进 papers[](coverage/统计不覆盖全量);且 {kind:'paper'}
  不带 topic/name 时抛「需要 path 或 name」而不是回落当前论文。修:范围内每篇都进 papers[];
  {kind:'paper'} 缺省 = 当前论文。(E1/E2/E3 抓到)
- migrate.rollbackMigration:只把「源」的父目录列为 rmdir 候选 ⇒ 迁移时新建的论文文件夹在搬回后
  留成空目录(回滚后形状与迁移前不一致)。修:源与副本的父目录都进候选。(C4 抓到)
- package.json 1.5.0 → 1.6.0;两份 README 的版本标签同步(merge-readme-parity 与
  library-clear-and-busy 的 C3 版本守卫保持绿),并各加 4 条 v1.6.0 特性 bullet(深色模式写明
  「图片会变负片」的已知代价、按论文分文件夹/迁移、嵌套子专题、按范围提问)
- docs/darkmode-subtopic-plan.md 收录进库并追加「附录 Z」:Z.1 记录 §B1.4「不双读」被有据推翻
  与**队长裁定保留**的规则表(平铺可读可删 / 文件夹赢 / 新写入落文件夹 / 迁移收口)与**集中回退点**;
  Z.2 记录补证据轮修掉的 3 处实现缺陷;Z.3 列出每条验收项对应的用例与真机证据落点
F1/F1b【high】迁移阶段 2 中断后不可复跑收敛:
- 预检的论文 stem 现在同时来自「已存在的论文文件夹 <topic>/<stem>/」——阶段 2 若在「某篇的
  平铺 <名>.pdf 已删、同篇其余产物还在」处中断,那些产物不再被判 orphan(永远留在旧布局=对插件消失)
- MigrationAbort 新增 unlinked(阶段 2 已删的源),复跑/排障不再只有阶段 1 视角的 remaining

F2【medium】同名并存且内容不同时被静默覆盖(与裁定的「文件夹赢」相反):
- 「已完成」判定从「size 相同」升级为「size + sha256 都相同」;同 size 不同内容 ⇒ 判 conflict,
  先报告不执行(runMigration 见 conflicts 即中止,零改动),绝不覆盖、绝不删源

F3【low】单篇删除的回收站未保留论文名层级:
- 产物落到 <bundle>/<论文名>/<文件>(与 clearTopic 的 <bundle>/<标签>/<文件> 口径一致);
  manifest 同时写 bundle 根与论文层级,新增 trashSubdir 线索;响应里的 trashRel 仍是 bundle 根;
  失败回滚时收掉空 bundle(不留空的回收站记录)

F4【high】路由层挡死「专题移动到另一父」(UI 必然 400):
- rename-topic 的 to 从单段 validateEntryName 改成路径类门 badTopicPath(与 topic 创建/清空同一条门),
  并把规范化后的值传给 renameTopic
- 新增「编码变形」负向防护(encodedEscapeError):%2E%2E%2F 这类解码后才是分隔符/上跳的输入一律拒;
  名字里合法含 % (如 50% off)不受影响
- 新增 C7(阶段 2 中断的确定性崩溃现场:同篇残留必须收进论文夹、不许留 orphan)
- 新增 C7b(failBeforeUnlink 逐点扫 1..4:源零丢失、unlinked 如实上报、复跑收敛成同一形状)
- 新增 C8(F2:同名并存且内容不同 ⇒ 报冲突、零改动、文件夹版与平铺源都不许动;反向对照同内容=已完成;
  并补「等长但内容不同」子例 —— 只比 size 的实现会在这里静默丢数据)
- 新增 C9(F3:回收站保留论文名层级 + manifest 双写 + 失败不留空 bundle)
- 新增 D7(F4:路由级「移动到另一父」必须 200;.. / 绝对路径 / A/%2E%2E%2Fx / 17 段超深 / NUL / .hidden
  一律 4xx 且盘上零变化;移入自身子树 409)
- 变异验证 33/33(新增 7 条:F1 stem 来源、F1b unlinked、F2 只比 size、F3 平铺 bundle、F4 单段 to、
  F4 编码变形、F5 文案回退;并把因 F2 改写而失锚的 C-already-done 同步到新代码形状)
- F5 文案:README×2 / lib/client.js i18n / 阅读器 ⚙ 提示 从「图片变负片」改为准确的
  「明显褪色偏色(实测纯红 [255,0,0] → 浅粉 [229,134,134],不是真负片)」;A10 断言随之加强
- 既有用例随 F3 的新口径更新回收站路径(存在性/内容强度不降:G1/G4/Q2/V3/T9/VB1/VB2/VB5/C5/E5)
裁定 1(quarantine/冲突 => 跳过该篇、继续迁移;绝不假成功):
- runMigration 不再因 conflicts/quarantine 整库中止;跳过项进 skippedPapers(code+reason)
- 有跳过项时只写 .migration-v1.partial.json(含 partial/skipped 清单),绝不写 .done;
  干跑报告与 CLI 输出都显著列出跳过项并明说「不会写全部完成标记」;rollback 同时清两种标记

裁定 2(隐藏论文纳入迁移、保留原名):
- planMigration 的 stem 来源含前导点的平铺 <名>.pdf 与隐藏论文目录;匹配到的隐藏产物随篇迁移
- paperDirNameError 不再拒前导点 / .trash(迁移是搬运已有数据,不是重新命名),
  只拒过长 / 分隔符 / NUL / "." / ".."
- listTopicTree 与 listPapers 同口径:.hidden/ 这类论文目录与隐藏平铺论文都要在树里可见

F-A(符号链接源绕过冲突判定 => 阶段 1 把论文夹里的常规文件覆盖成链接):
- 目标已存在(常规文件或链接)一律走冲突判定;链接到链接只在指向同一目标时算「已完成」,
  否则判 conflict(不覆盖、不删源)

F-B(同名平铺论文被写进已有子专题目录 => 该目录同时是专题与论文):
- 目标已存在且是非论文目录 => 判 conflict(干跑即报警);子专题内论文不再从列表/树里消失,
  clearTopic 也不会永久 409

F-C(/api/library 的 pdfPath 写死新布局 => 未迁移旧库指向不存在的路径):
- 改用 paperPdfPath()(按该篇实际布局推导),未迁移库与新库都给出真实存在的路径

顺带:把「论文目录不递归」的结构不变量收敛到唯一判定点(原先 head 处是可达不到的冗余守卫,
让不变量没有单一变异点),并在子级分支补 paperDirs,报告字段不空。
…trash / clear-plan 入参门

1) 目标占位判定(F-A 的彻底修法):只要目标**已存在**就绝不进 moves --
   - 目标是常规文件:size+sha256 相同 => alreadyDone;不同 => conflict(先报告、零改动、源不动)
   - 目标是符号链接:与源是同一份链接 => alreadyDone;否则 => conflict
   - 目标既不是常规文件也不是链接(目录/设备)=> conflict
   只有目标不存在才 moves;**禁止** rmSync(force) 覆盖:符号链接分支删掉了「先删后建」,
   常规文件复制改用 COPYFILE_EXCL(内核层兜底,判定层回归也不会覆盖)
2) unreadable:读不进去的子树单列 {dir,cause},报告里显著提示「本次未处理」,并计入
   partial/skippedPapers(code=migrate-unreadable),绝不假装全迁完;成功响应带 unreadable 字段
3) 回滚收尾 best-effort rmdirSync 空的 .trash 根(非空一律不动 -- 那是用户可恢复的数据)
4) clear-plan 的 topic 也过 badTopicPath(topic 类路由统一入参门:逐段词法 + 编码变形 + 深度/长度上限)
…rash、C18 结构护栏、V12b 路由门分工

- C2 增结构不变量:moves 的目标必须不存在(已存在的目标只能走 alreadyDone/conflict)
- C10 重写:符号链接平铺源 x 论文夹同名常规文件,等长/不等长**两种**都判 conflict,
  断言零改动、源链接不动、库外目标不动、不写完成标记(等长那种专打「只比 size 的实现」)
- C11 增「冲突篇零改动」递归快照断言
- C14 扩到三种布局(平铺 / 论文夹 / 同名并存文件夹赢),断言 pdfPath 全部真实存在
- C16 unreadable:chmod 000 造读不进去的子树,断言单列、报告提示、partial + skippedPapers 记账、
  不写完成标记、其它子树照常迁移(无法构造时显式 skip 并写明理由,不是静默通过)
- C17 回滚收尾:空的 .trash 根被清掉;非空的一个字都不动
- C18 结构护栏(只看代码、剥掉注释):默认复制必须 COPYFILE_EXCL;绝不 rmSync(toAbs);
  符号链接分支不得先删后建
- V12b:把 F4 之后被放宽的路由门断言补回并**分工** -- topic 类必须走路径门
  (badTopicPath/validateTopicPath),论文名类必须走 badEntryBoundary,且路由层不得直接用单段原语
- 变异 45/45(新增 6 条:占位判定降级 / 链接源旁路 / 去 EXCL / rmSync 目标回归 / unreadable 静默 /
  回滚不清 .trash / clear-plan 与论文名门分工)
R2-1【必修】全量成功后**清掉旧 .partial**(与写 .done 成对):此前 .partial 只写不清,
跳过项修好后重跑会写 .done 但旧 .partial 仍在 ⇒ 之后每次干跑报告永久谎报「上次有跳过项」,
而那份报告是用户点头迁移前唯一要读的东西(谎报=欺骗)。顺序刻意是「先清 .partial 再写 .done」:
中途死掉时两个标记都不在 ⇒ 报告只会保守地说「没有完成标记」,绝不出现 .done 与 .partial 同时在场的谎报。

R2-2【必修】`unreadable` 收进**数据根**:chmod 300(可写可进不可读)时 readdir 失败,
此前只记 `rel`(根为 '')会漏掉 ⇒ moves=0 / unreadable=[] / partial=false ⇒ 写 .done = **假成功**
(论文其实全在旧布局)。根用 '.' 表示;有 unreadable 就绝不写 .done(与裁定 1 同一条规矩)。

R2-3【顺手,第一轮 F-E】clearTopic 失败收尾时 best-effort 清掉**空的** .trash 根
(rmdir 非递归天然保证:非空一字不动)。

CLI 退出码(队长裁定,脚本化调用者据此分支;同时写进 --help 与 migrateMain 的文档注释):
  0  = 全部迁移完成    2 = 部分完成(有跳过项/未处理子树)或运行被中断
  1  = 硬失败          64 = 用法错误(缺 --yes / 回滚缺日志路径)
--dry-run(默认)永远返回 0(只打印报告,不改任何东西);新增 --help 打印用法与四个码。
- C19 R2-1:先造一次有跳过的运行(留 .partial)→ 处理掉那篇后重跑 ⇒ 断言 .done 已写、
  旧 .partial 已消失、plan.partialMarker=null、报告不再谎报「部分完成/会被跳过/跳过该篇」
- C20 R2-2:chmod 300 数据根(无法构造时显式 skip 并写明理由)⇒ plan.unreadable=[{dir:'.',cause:'EACCES'}]、
  报告列出根、run.partial=true、绝不写 .done、写 .partial
- C21 R2-3:clearTopic 失败后空的 .trash 根被收掉;非空(keepme/payload.pdf)一字不动
- C22 CLI:真跑 dist/migrate.js 断言 0/2/1/64 四个退出码(全迁完 / 有跳过项 / 悬空链接硬失败 / 缺 --yes),
  并断言 --help 写明四个码、硬失败后源文件仍在(数据零丢失)
- 变异 50/50(新增 6 条:不清 .partial / 根漏进 unreadable / 空 .trash 残留 / partial 退 0 /
  硬失败退 0 / 用法错误退 0;删掉被完全覆盖的重复项 I-unreadable-silent)
…位 / 新布局递归收集)

Windows CI(run 38533904376 / job 115752032053)三个失败,根因各不相同;ubuntu/macOS 全绿是因为
前两个只在 win32 暴露、第三个只在「产物在论文夹里」的布局下暴露。

① C22 拿到空 stdout(`match /退出码 0/` vs '')——**CLI 入口判定在 win32 下永不成立**:
   `new URL(import.meta.url).pathname` 在 win32 形如 `/D:/a/b/dist/migrate.js`,`path.resolve()` 会拼成
   `D:\D:\a\...`,与 `process.argv[1]` 永不相等 ⇒ `node dist/migrate.js` **静默什么都不做**。
   改用平台感知的 `fileURLToPath`;同一缺陷在 POSIX 上也能用「带空格的路径」复现(pathname 里是 %20),
   所以新增 C23 在本机做判别(同时留一条无空格路径的反向对照)。

② A2 `配置文件必须 0600` 438 !== 384:384=0o600、438=0o666 —— 不是行尾问题,是 **NTFS 不表达 POSIX
   mode 位**(chmod 只映射只读位)。改成**实测能力分支**:先探测本机 chmod 是否真能读回 0o600;
   能 → 沿用强断言,并**加**一条「已存在的 0o666 文件必须被重写收紧回 0o600」(这条正是 writeFile
   的 mode 只在新建时生效、故必须显式 chmod 的理由);不能(win32)→ 断言平台无关的等价性质:
   文件确在预期路径 + 写入路径**确实请求** 0600(源码级断言,防止有人以「反正 Windows 不生效」为由
   删掉 chmod,那样 Unix 上就真的不收紧)+ 实测记录本平台 mode 恰为 0o666。
   禁用 t.skip / 放宽换绿:跳过的部分被**换成更严的断言**,不是删掉。

③ VB3[B5] `['p']` vs `['p-zh.pdf','p.pdf','p.txt']`:win32 上 chmod 挡不住 rename ⇒ 注入不生效、
   删除其实**成功了**(200),而用例的兜底分支用**非递归** readdir 收集(新布局产物在 `<topic>/<paper>/`
   里)⇒ 只看到论文目录名 `p`,把「文件都在论文夹里」误判成「文件全丢了」。
   改成:注入能力用**实测探针**(复用 test/fs-probe.mjs 的 chmodBlocksRename)而不是硬编码
   `process.platform === 'win32'`;收集改成**递归**;并新增 ②b 场景——平台无关地覆盖「移动成功」那一路
   的零销毁不变量(三个产物必须全在回收站、专题里零残留),因此本机(POSIX)也能判别递归收集这一修复。
macOS CI(run 38536203961 / job 115759573564)唯一失败是 **C23**("入口判定必须在带空格路径下成立",
actual `''` = CLI 零输出)。根因与我 t13 的怀疑**同形但换了个平台面具**,且这次连 fileURLToPath 也救不了:

· macOS 的 `os.tmpdir()` 是 `/var/folders/...`,它是 `/private/var/folders/...` 的**符号链接**;
· Node 默认对**主模块**做 realpath ⇒ `import.meta.url` 派生出 `/private/...`,
  而 `process.argv[1]` 仍是「经由符号链接」的 `/var/...` ⇒ 两边字符串永不相等 ⇒ **CLI 静默零输出**。

判据改成按**实测能力分层**(不按平台名分支):
 ① **同一实体**:`statSync` 的 (dev, ino) 相同 ⇒ 就是同一个文件 —— 符号链接、大小写、分隔符、
    8.3 短名都影响不到(`ino === 0` 的文件系统上自动跳过);
 ② 退化:两边各自 `realpathSync` 后再比(解开符号链接拿规范路径);
 ③ 再退化(目标不可达的最保守判断):`resolve` 后直接比。
顺带把这段逻辑抽成导出的 `isDirectRun(argv1, metaUrl)`,便于直接对照测试。

C23 扩成三种平台形态的本机等价判别:带空格路径(POSIX `%20` 面具)、**符号链接路径**
(macOS `/var`→`/private` 面具,用 fs.symlinkSync 在本机造同形现场)、无空格真实路径对照,
并直接对 `isDirectRun` 做四组对照(符号↔真实同一入口、真实=同一入口、不同文件不得算同一入口、
argv[1] 缺失不执行)。

变异 54/54:新增 **L1-guard-plain-resolve**(把分层判据退回朴素的 `resolve(a)===resolve(b)`)⇒ C23 红
(含空格那条仍绿 ⇒ 证明符号链接那条才是判别器);K1 重新锚到 `isDirectRun` 的 `fileURLToPath` 处
(win32 `%20` 面具)⇒ 同样红。
@GGboya

GGboya commented Oct 17, 2026

Copy link
Copy Markdown
Owner

评审结论:功能与验证工作都很扎实,但新布局语义与既有面的交互处有一批已实证复现的缺陷,需要修一轮再合并

先说结论之外的话:这批的四条能力设计合理(迁移的先复制后删/幂等/回滚机制我本地完整闭环跑通过:干跑→执行→复跑 moves=0→回滚还原,内容零丢失;变异探针 54/54;CI 三平台绿)。但按「每条新行为要可区分用例 + 变异验证」的口径过了一遍新布局与既有界面的交互面(上传/改名/删除/清空/会话绑定/迁移工具自身),发现以下问题——每条都在本机复现过,不是读代码猜的。

必修(数据不可见/不可删/静默丢失一类)

A. 会话 id 与嵌套专题不兼容

  • src/tools.ts:72 paperFromSessionId 按第一个 / 切 topic/name,但嵌套专题的 topic 本身含 / ⇒ 深度 ≥2 的论文(A/B/P)会话绑定全部失效:agent 不带 name 调 search_paper 等会报「找不到 "B/P"」。lib/client.js:593 的 parseDprSession 同病,ⓘ「对话还停在另一篇」按钮对子专题论文永远常亮。修法:lastIndexOf('/')(论文名不允许 /)。

B. 上传无碰撞检查(会吞数据)

  • src/host.ts:616 上传直接 mkdir <topic>/<stem>/ 写 PDF:往已有子专题同名目录上传会把子专题变成「论文目录」,其下论文从列表/树/范围提问里整体消失(实测 listPapers 只剩 {T,Sub});往旧平铺论文同名上传则文件夹优先规则永久遮蔽旧 .txt/.study.json/.embeddings.json(学习进度不可见、平铺产物从 API 不可删)。大小写不敏感盘上还会静默吞掉大小写变体。

C. 目录/文件互踩一类(造成 API 不可见且不可删的孤儿)

  • src/library-manage.ts:1226 renameTopic 只查 resolveTopicDir,不拒绝论文目录当父(host.ts:552 create-topic 祖先巡检、host.ts:601 上传 topic 解析同漏)⇒ 子专题树搬进论文目录后消失,且 clearTopic 之后永远 409 全回滚(实测,API 层面无解)。
  • src/library.ts:184 + host.ts:560 create-topic 不查与平铺文件同名 ⇒ 旧平铺论文被文件夹规则遮蔽成不可见 + planPaperDelete 404。
  • src/library-manage.ts:1167 renameLegacyPaper 冲突预检只查逐文件目标、不查同名目录存在 ⇒ 平铺改名撞上子专题名后同样不可见不可删。
  • src/library-manage.ts:1131 文件夹布局 renamePaper 只查目标目录、丢掉了旧的逐文件冲突预检 ⇒ 目录内同名残留文件被 renameSync 静默覆盖(实测无 409、无回收站记录)。

D. 清空/删除的占用检查只看一层

  • src/host.ts:915 /topic/clear 的会话/译文占用巡检只遍历根层 plan.papers,但 clearTopic 删的是整棵子树 ⇒ 子专题里有活跃会话或生成中译文的论文照样进回收站(实测 plan.papers=['P1'] 而 papersCount=2);清空确认对话框同样漏列子专题论文。

E. 迁移工具自身的两个契约缺口

  • src/migrate.ts:391 readMigrationLog 整个 JSONL 一个 try/catch,任何一行损坏(SIGKILL 打断 append 的典型现场)⇒ 返回 [],--rollback 打印「已回滚 0 个文件」且 exit 0——静默空转的假成功,直接违反「降级必须如实提示」。修法:逐行解析、坏行显式报错。
  • src/migrate.ts:239 阶段 1 在 mkdir 论文目录后、.pdf 复制前被打断,残留目录在复跑时被判 dir-other 冲突 ⇒ 该论文每次复跑都被跳过(实测 moves=[] 永远),与「可中断可复跑」的文档承诺矛盾。需要阶段 1 也写 partial 标记或对「自己上次的半成品目录」做识别清理。
  • 次级:host.ts:956 /migrate 在事件循环上同步跑全量复制+双 sha256(大库冻结所有路由,含 PDF 流与 SSE 桥),且没有像 delete/rename 那样对在飞转录/译文加 busy 门。

F. 会话/主题路径的一个回归

  • src/library-manage.ts:182 validateTopicPath 在 boundaryOnly 模式(删除/改名-from/清空)也执行新上限 MAX_TOPIC_SEG=100,而上一版允许 120 ⇒ 已存在的长名专题从 API 删不掉也改不了名,违反「legacy 怪名字必须保持可删」的自定不变量(实测 110 字名 400)。

应修(行为正确性/如实提示)

G. 范围提问的预算与诚实性

  • src/scope.ts:263 专题范围的命中片段 push 进 hitParts 不计入 used 预算(pushWithLabel 恒以 hits=null 调用,计费分支是死代码),且与正文重复同一段文本 ⇒ 实测 12 篇×3 命中时 totalChars=72574,超 MAX_CONTEXT_CHARS=40000 近一倍;omitted 提示句也在 276/287 两处重复输出。
  • src/scope.ts:146 scopePapers 逐篇 resolvePaper 的 catch 静默吞掉「命中多篇」(同名论文在不同深度时直接从范围里消失,无 omitted 标注);kind:'topic' 不校验专题存在 ⇒ 已改名/删除的专题装配出「空的但看起来正常」的范围 + 200 响应(违反如实提示)。顺带:每问一次 N+1 递归子树扫描。
  • 次级:host.ts:432 scopeBusy 的分隔符 \u0000 vs / 不匹配 ⇒ paper 范围的 busy 提示永远不触发;host.ts:985 回滚日志路径前缀判断缺分隔符;短正文全覆盖时 coverage 误报 truncated(scope.ts:253)。

H. 客户端嵌套结构追踪

  • lib/client.js:1079/375/1194 祖先专题改名/删除只按精确相等更新 cur/dpr.last/closed 键 ⇒ 读 A/B 时改名 A→X/A,阅读器 iframe 还持 ?topic=A/B,提问/翻译 404、刷新后回落 papers[0](阅读位置丢);openDelete 的 clear-vs-delete 分流只数直接论文(1172)⇒ 论文全在子专题里的专题没有 UI 路径到 /topic/clear(doDeleteTopic 1268 的 clearEndpoint 重试是死代码——api() 没把 clearEndpoint 挂到 Error 上)。

我这边已经修好的(合并时会带上,供参考)

  1. scope.ts 的 5 处码元截断全部切开代理对(readText/命中片段 1200×2/正文 limit×2)——与 issue 导致对话400的issue #9 同类(孤立代理项进会话历史 ⇒ 整条会话 400)。已提炼 cutToCodePoint 原语(search.ts 导出)统一替换,附 S5/S6/E3b 回归用例并变异验证。你们改完上面的问题后,scope.ts 这 5 处也请走 cutToCodePoint(或等价回退),main 上已有该导出。
  2. docs/features.md / features_EN.md 的新功能章节(深色代价如实写明/迁移三步/退出码/范围上限)与 AGENTS.md 模块清单——按仓库「用户向细节归口 features.md」的约定补齐。

其它(不阻塞,记录在案)

重复 CSS 块(reader/index.html:143-147)、DARK_PRESETS 三处重复声明、theme-config.ts 是第 5 份手写 0600 配置样板、死导出(normalizeTopicPath 零调用)、回收站 manifest 漏隐藏文件(library-manage.ts:611)、大小写改名在 macOS/Windows 被拒(library-manage.ts:1228)。


结论:request changes。 上面 A–F 属必修(都有数据不可见/不可删/静默丢失风险),G/H 应修。修完 push 后我再跑一轮集成验证(main 上已有 issue #9 修复,会一起合入)。感谢这批工作——迁移工具的核心机制经得起拆,问题集中在「新语义没铺到既有面的每个角」,这也是为什么这轮评审值得。

@GGboya GGboya left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

详见上一条评审评论:A–F 必修(数据不可见/不可删/静默丢失),G/H 应修。每条均已本机复现。修完 push 后我再跑集成验证。

上游 PR GGboya#8(feat/darkmode-subtopic)维护者 @GGboya 的 CHANGES_REQUESTED 逐条修复:

A 会话 id 嵌套专题:paperFromSessionId / parseDprSession 按最后一个 '/' 切
B 上传碰撞:同名子专题 / 论文目录 / 旧平铺产物 / 大小写变体一律 409,预检先于消费 body
C 目录-文件互踩:create-topic 祖先与平铺、renameTopic 父级、renameLegacyPaper 撞目录、
  renamePaper 逐文件预检(sameFileForRename;ino/dev=0 平台保守判冲突)
D 清空占用与确认清单改为整棵子树(flattenClearPapers),在跑会话 409 且零改动
E 迁移契约:坏 JSONL 逐行报错(CLI 非 0 / 路由 409);阶段 1 半成品标记与复跑识别;
  /migrate 异步执行 + 在飞转录/译文 busy 门;回滚 logDir 用 relative 判定
F boundaryOnly 长度回归:删除 / 改名-from / 清空不再卡创建侧段长上限
G 范围预算与诚实性:命中计费、omitted 单次、专题不存在 404、同名如实 omitted、
  coverage 不再误报 truncated、5 处码点安全截断(本树等价回退 cutToCodePoint)
H 客户端前缀感知重写(cur / 折叠键 / dpr.last)、子树计数、clearEndpoint 透传到 Error

验证口径(基线 fdb45f2 对照 → 修复后):
- 基线全量 533 tests / 506 pass / 0 fail / 27 skip;修复后 675 tests / 648 pass / 0 fail / 27 skip
- 同一批缺陷用例在基线必红、修复后全绿;独立评审 R1/R2/R3 探针基线 4/15、1/16、1/10 红
- 变异 R1–R5 共 29 条:注入红 / 还原绿 / diff clean
- 逐条事实与复跑命令见 docs/evidence/;给维护者的回复见 docs/pr8-review-response.md

未 amend 既有提交、未 rebase、未 push。
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.

2 participants