dsh-tiddlywiki 0.28.14 → 0.30.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/docs/CHANGELOG.md +48 -0
  2. package/docs/development.md +46 -18
  3. package/docs/features.md +4 -3
  4. package/docs/seed-initialization.md +4 -3
  5. package/docs/usage.md +7 -6
  6. package/docs/wechat-publish-setup.md +1 -1
  7. package/lib/client.js +46 -34
  8. package/lib/index.js +5920 -5115
  9. package/lib/index.js.map +1 -1
  10. package/package.json +3 -3
  11. package/src/client/dock-align.ts +6 -2
  12. package/src/client/editor-popup.ts +13 -9
  13. package/src/client/endpoints.ts +4 -2
  14. package/src/client/format.ts +14 -0
  15. package/src/client/i18n-card.ts +132 -0
  16. package/src/client/i18n-catalog.ts +32 -0
  17. package/src/client/i18n-chrome.ts +103 -0
  18. package/src/client/i18n-config.ts +173 -0
  19. package/src/client/i18n-frame.ts +64 -0
  20. package/src/client/i18n-note.ts +125 -0
  21. package/src/client/i18n-plugins.ts +13 -0
  22. package/src/client/i18n-settings.ts +475 -0
  23. package/src/client/i18n.ts +96 -0
  24. package/src/client/index.ts +3 -2
  25. package/src/client/knowledge-fab.ts +72 -29
  26. package/src/client/note-widget-draft.ts +13 -11
  27. package/src/client/note-widget-recent.ts +189 -0
  28. package/src/client/note-widget-scope.ts +152 -0
  29. package/src/client/note-widget-tags.ts +4 -3
  30. package/src/client/note-widget-upload.ts +5 -4
  31. package/src/client/note-widget.ts +111 -210
  32. package/src/client/quick-note-dock.ts +4 -3
  33. package/src/client/rightbar-tab.ts +8 -3
  34. package/src/client/scope-seat.ts +3 -1
  35. package/src/client/session-summary.ts +35 -16
  36. package/src/client/settings-icon-picker.ts +62 -54
  37. package/src/client/settings-page-catalog.ts +83 -72
  38. package/src/client/settings-page-config.ts +87 -80
  39. package/src/client/settings-page-runtime.ts +1 -6
  40. package/src/client/settings-page-wikis.ts +95 -78
  41. package/src/client/settings-page.ts +118 -44
  42. package/src/client/sidebar-entry.ts +17 -6
  43. package/src/client/status-cache.ts +11 -1
  44. package/src/client/styles-css.ts +914 -0
  45. package/src/client/styles.ts +1 -904
  46. package/src/client/sync-button.ts +15 -17
  47. package/src/client/tool-views-body.ts +143 -0
  48. package/src/client/tool-views-fetch.ts +139 -0
  49. package/src/client/tool-views-links.ts +112 -0
  50. package/src/client/tool-views-misc.ts +92 -0
  51. package/src/client/tool-views-search.ts +130 -0
  52. package/src/client/tool-views-shell.ts +164 -0
  53. package/src/client/tool-views-types.ts +86 -0
  54. package/src/client/tool-views.ts +28 -764
  55. package/src/client/tw-frame-api.ts +107 -0
  56. package/src/client/tw-frame-hash.ts +106 -0
  57. package/src/client/tw-frame.ts +55 -151
  58. package/src/client/ui-config.ts +4 -2
  59. package/src/client/wiki-scope-dock.ts +11 -7
  60. package/src/client/wiki-scope.ts +33 -8
  61. package/src/host/admin-catalog.ts +8 -3
  62. package/src/host/admin-routes-config.ts +131 -0
  63. package/src/host/admin-routes-info.ts +224 -0
  64. package/src/host/admin-routes-seeds.ts +142 -0
  65. package/src/host/admin-routes-wikis.ts +145 -0
  66. package/src/host/admin-routes.ts +96 -494
  67. package/src/host/admin-secrets.ts +23 -2
  68. package/src/host/clip-bridge-limits.ts +17 -0
  69. package/src/host/clip-bridge-payload.ts +212 -0
  70. package/src/host/clip-bridge-ssrf.ts +315 -0
  71. package/src/host/clip-bridge.ts +54 -475
  72. package/src/host/config.ts +54 -7
  73. package/src/host/git.ts +42 -4
  74. package/src/host/mutation-lock.ts +51 -0
  75. package/src/host/prompt.ts +47 -19
  76. package/src/host/proxy-guard.ts +73 -0
  77. package/src/host/routes-helpers.ts +271 -0
  78. package/src/host/routes-note.ts +386 -0
  79. package/src/host/routes-session-agent.ts +336 -0
  80. package/src/host/routes-session-summary.ts +103 -0
  81. package/src/host/routes-session.ts +19 -362
  82. package/src/host/routes-tw-proxy.ts +18 -60
  83. package/src/host/routes-wechat.ts +230 -0
  84. package/src/host/routes.ts +134 -776
  85. package/src/host/seed-clip-bridge.ts +3 -3
  86. package/src/host/seed-wechat-docs.ts +1 -1
  87. package/src/host/seeds-flush.ts +198 -0
  88. package/src/host/seeds-registry.ts +422 -0
  89. package/src/host/seeds.ts +17 -579
  90. package/src/host/session-scope.ts +39 -3
  91. package/src/host/session-summary-scan.ts +239 -0
  92. package/src/host/session-summary.ts +1 -218
  93. package/src/host/tools-attach.ts +31 -4
  94. package/src/host/tools-git.ts +238 -91
  95. package/src/host/tools-notes-lifecycle.ts +179 -0
  96. package/src/host/tools-notes-structure.ts +244 -0
  97. package/src/host/tools-notes-write.ts +266 -0
  98. package/src/host/tools-notes.ts +9 -664
  99. package/src/host/tools-support.ts +81 -4
  100. package/src/host/wechat-publish-contract.ts +275 -0
  101. package/src/host/wechat-publish.ts +5 -245
  102. package/src/host/wiki-farm.ts +73 -11
  103. package/src/host/wiki-instance.ts +36 -9
  104. package/src/host/write-policy.ts +25 -0
  105. package/src/index-config.ts +127 -0
  106. package/src/index-git.ts +34 -2
  107. package/src/index-wikis.ts +5 -2
  108. package/src/index.ts +59 -105
package/docs/CHANGELOG.md CHANGED
@@ -4,6 +4,54 @@
4
4
 
5
5
  > 最近几个主要版本的一句话记录(完整变更见 [Releases](https://github.com/bbqisbbq/dsh-tiddlywiki/releases) / git log)。
6
6
 
7
+ - **v0.30.42**(2026-09-29):**`index.ts` 拆分第一轮 —— 先切「配置常量」这半边,1036 → 955 行**(交接卡第 6 条 B 步:**先区分「阶段产物」与「配置常量」,再按生命周期阶段抽**)。新版 `src/index-config.ts`(127 行)拿走五样东西:`TiddlywikiConfig`(`config:` 块形状)· `ResolvedConfig`(合并后形状)· `CLIP_BRIDGE_DEFAULT_PORT` · `DEFAULTS`(那张默认值表)· `resolveConfig(rawConfig)`(合并函数)。① **为什么这半边能离开入口**:它是**纯**的 —— 不吃 `ctx`、无副作用、**对装配顺序零依赖**;而 `apply()` 里剩下的东西(farm 的建立 → 变更锁 → 路由 → 工具)恰恰相反,**顺序本身就是语义**(第 3 条裁定:整插件只建一把锁、drain 只走一条路径),所以先切纯的这半边,正是 B 步「先分类再动手」的意义。② **搬迁 ≠ 顺手扩公共面**:只把**本来就是导出**的 `TiddlywikiConfig`(type)+ `CLIP_BRIDGE_DEFAULT_PORT` 再导出;`DEFAULTS` / `ResolvedConfig` / `resolveConfig` 以前**都不是** `export`(`DEFAULTS` 连 `const` 都没带 export),所以**不**加进 barrel —— 公共面按原样搬,不借机变宽。③ `index-wikis.ts` 里那条 `import type { TiddlywikiConfig } from './index.ts'` 改指 `./index-config.ts`(形状的新家):运行期图仍是**单向**的 `index.ts → index-config.ts → index-wikis.ts`(回来的那条只有 `import type`,编译后不产生运行期边)。④ **编译器又一次当免费的协调器**:`noUnusedLocals` 精确点名搬迁后**只剩两条** import 变孤儿(`DARK_PALETTE_DEFAULT`、`READY_TIMEOUT_DEFAULT_MS`),逐条删掉即可,没有一处靠猜。⑤ ⚠️ **本仓库的一条新教训(已写进 wiki 常见坑):barrel 里"再导出"的丢失,`tsc` 与守门脚本都**看不见**。** 搬迁过程中我误删了同一区块内的 `export { MARKDOWN_PLUGIN } from './host/wiki-instance.ts'` —— `tsc` 全绿、`verify:unit` 42 套全绿,因为**全仓库没有任何东西 import 它**(`grep MARKDOWN_PLUGIN` 只有定义处 + 一条守门脚本里都没提),它是给 `lib/index.js` 的**外部**消费者用的公共面。**抓它的是 `git diff` 逐行复核**,不是自动化。⇒ 凡动 `index.ts` 的 barrel,`git diff` 必须逐行看,别只看编译器脸色(这与 v0.30.28「钉 import 字面量的断言改成钉语义」是同一族问题的两面:**公共面的存在性没有守门**)。⑥ **守门无需改动**:`verify-config-ranges.mjs` 的 `read('src/index.ts')` 本来就是 `readFamily`(`index-config.ts` 属 `src/index-*` 族 ⇒ `DEFAULTS` 仍被找到、`CLIP_BRIDGE_DEFAULT_PORT` 仍被 `export const [A-Z0-9_]+ = [\d_]+` 收集到);另外九个按族读 `src/index` 的守门(`verify-restart-drain` / `verify-language-pin` / `verify-frame-guards` / `verify-mutation-lock` / `verify-skill-install` / `verify-wechat-docs-seed` / `verify-wechat-publish` / `verify-wiki-focus` / `verify-ready-policy`)逐个复跑全绿;`verify-package-contents.mjs` 逐字列了 `'src/index.ts'`,但只判文件存在,不受影响。**验证**:`typecheck` / `build`(client bundle 769,598 字节,仍 minify)/ `verify:unit`(42 套,退出码 0)/ 上述九个族读守门单跑 / 全量 `npm run verify`(static + unit + e2e + large)退出码 0 / `verify-line-endings` / `verify-version-consistency` 全绿。
8
+ - **v0.30.41**(2026-09-29):**`note-widget.ts` 拆分第一轮 —— 抽出两片,993 → 817 行**(第 5 条裁定的收尾,接着 v0.30.40 的 `tw-frame` 样板做)。① **`note-widget-recent.ts`(「🕘 最近」picker)**:这是 774 行 `createNoteWidget()` 闭包里**唯一一组状态完全私有**的东西 —— `recentOpen` 只被 `toggleRecent`/`closeRecent`/`loadNote` 三个函数碰到,于是**状态直接跟着搬走**,卡片里**一处都不用改名**(`tw-frame` 那条「优先抽取、少改名」的结论又一次成立)。它从卡片拿的两样东西是**会写**的(`loadedToken` / `persistedSignature`),用 setter 回调传进去 —— **卡片仍然是这两个值的主人**,模块只负责喂。⚠️ 每个请求都走 `wikiQuery` 依赖:漏了这条在多库下「最近」会去默认库拿列表(正是 `verify-wiki-focus` 钉的那个缺陷)。② **`note-widget-scope.ts`(这张卡片作用于哪个库)**:把 `targetWiki`/`targetPicked`/`rosterMode`/`roster`/`defaultWikiId`/`focusOff`/`targetPromise` 七个状态与 `ensureTarget()` / `stoppedTarget()` / `wikiQuery()` 一起搬出,**但 DOM 那一半刻意留下** —— 画 `<select>` 是卡片的职责,卡片通过 `scope.setSyncDom()` 把重画函数注册回去。这条分界线同时保住了 `verify-wiki-focus.mjs` 里那条**按精确路径读 `note-widget.ts`** 的断言(「默认库只加身份标记」的那行 `const mark = …` 仍在卡片里),所以**不需要**把那个 `readFileSync` 改成族读。③ **守门同步三处(都是"钉语义不钉排版")**:`twProxyFor(rosterMode, targetWiki, …)` 的两条断言(`verify-tw-origin` / `verify-wiki-focus`)放宽成 `twProxyFor\([^,]+, [^,]+, payload\.twUrl, payload\.twUrlAbsolute\)` —— 钉的是「前两个参数来自**这张卡片的目标库**」,不是那两个变量名;native 路径的顺序断言从 `indexOf('await ensureTarget()')` 改成 `search(/await (?:scope\.)?ensureTarget\(\)/)`(钉的是**先解析、再写**这个顺序);`const mark = item.id === default(Wiki)?Id ? …` 改成 `… === [^?]*\? t\('note\.defaultMark'\)`(钉的是「默认库那一项拿到目录里的身份标记」)。④ `verify-frame-guards.mjs` 那份「生命周期函数各一份」的清单**不用改** —— 它本来就是 `readFamily`,`const ensureTarget = (): Promise<void> => {` 的写法原样保留在新模块里。**验证**:`typecheck` / `build`(client bundle 769.2 kB,仍 minify)/ `verify:unit`(42 套,退出码 0)/ 五个客户端守门(`verify-frame-guards` / `verify-frame-surface` / `verify-tw-origin` / `verify-wiki-focus` / `verify-client-i18n`)/ `verify-line-endings` 全绿。
9
+ - **v0.30.40**(2026-09-29):**`tw-frame.ts` 内核抽出 `tw-frame-hash.ts`**(第 5 条裁定继续,**纯搬迁**):把「待落的 hash 怎么落到 iframe 上」整段(`applyPendingHash` + `tryOnce` 的 40×150ms 等待链 + 跨源兜底 `fallbackLoad`,62 行)抽成 `createHashNavigator(deps)`。base **652 → 615 行**,新模块 106 行。① **关键设计:只读状态用访问器传,不改名**。这段代码对 `frame` / `disposed` **只读**(`frame.src = …` 是对**对象属性**赋值,不是重绑变量),所以用 `getFrame()` / `isDisposed()` 传进去即可 —— 而这一步是**必须**的:`frame` 在同一文件里**同时**是接口属性(`frame: string`)与对象字面量的键(`frame: 'dsh-tw-rightbar-frame'`),任何文件级改名都会误伤它们(v0.30.38 实测被 tsc 拦下、已回滚)。**抽取只要求集中「写入点」,改名才要求改对所有「出现点」** ⇒ 名字超载时**优先抽取、少改名**;真正会被写的 `pendingHash` / `hashWaitTimers` 留在 `surfaceState` 里按引用传入。② **守门同步**(两处,都是"钉语义不钉排版"):`verify-wiki-focus.mjs` 里 `wikiSwitchPending` 的两条断言放宽为 `[Ww]ikiSwitchPending` / `/if \(.*[Ww]ikiSwitchPending.*\) return/`(现在只剩访问器形式 `deps.isWikiSwitchPending()`);同文件的 `const frame = readFileSync(…'src/client/tw-frame.ts')` 改成**再拼一个族成员** —— 它是**按精确路径**读的,抽走后那半段断言会看不见代码(这正是本仓库"守门按模块族读"那条规矩的由来)。③ 顺带确认:`verify-frame-guards.mjs` **本来就是 `readFamily`**,所以它那份「生命周期函数各一份」的清单**不需要改**(`applyPendingHash`/`fallbackLoad` 仍然各一份,只是换了文件)。**验证**:`typecheck` / `build` / `verify:unit`(42 套,退出码 0)/ 五个客户端守门(`verify-frame-guards` / `verify-frame-surface` / `verify-tw-origin` / `verify-wiki-focus` / `verify-client-i18n`)/ `verify-line-endings` / `verify-version-consistency` 全绿。
10
+ - **v0.30.39**(2026-09-29):**`tw-frame.ts` 状态合并第二片**(第 5 条裁定继续,**行为零变化**):`pendingHash`(10 处引用)与 `wikiSwitchPending`(5 处)也进 `surfaceState`。① 这两个是上一轮**特意留下**的——它们被 6 处源码级守门提到,动手前逐条核实了**到底哪些真的在钉名字**:`verify-frame-guards:182` 是一份**函数名显示清单**、`verify-frame-surface:355` 是**注释**、`verify-tw-origin:177` 是**测试名**(都不受影响),真正结构性的一条只有 `verify-wiki-focus:736` 的 `/if \(wikiSwitchPending\) return/`。② 于是把那条断言的判据**从「钉变量名」改成「钉语义」**:`/if \((?:\w+\.)?wikiSwitchPending\) return/` —— `(?:\w+\.)?` 让它只要求「换库未完成时必须 return」这件事成立,**不再锁定这份状态住在哪**(这正是本仓库拆分守门的通用改法:钉意图,不钉排版;此前 `verify-tw-origin` 那条钉 import 字面量的断言也是这么改的)。③ 至此 `tw-frame` 内核的**第一阶段(状态合并)**已覆盖 4 个成员(`hashWaitTimers`/`frameLoaded`/`pendingHash`/`wikiSwitchPending`),剩下的 `frame`/`disposed` 同类处理即可;**状态全部集中后**才做第二阶段的模块抽取(`tw-frame-hash.ts`,必须用 `tw-frame-` 前缀,否则 `readFamily` 读不到、守门静默失效)。**验证**:`typecheck` / `build` / `verify:unit`(42 套,退出码 0)/ 五个客户端守门(`verify-frame-guards` / `verify-frame-surface` / `verify-tw-origin` / `verify-wiki-focus` / `verify-client-i18n`)/ `verify-line-endings` 全绿。
11
+ - **v0.30.38**(2026-09-29):**`tw-frame.ts` 内核:第一步「状态合并」起手**(第 5 条裁定继续,**行为零变化**)。① **为什么不是"直接抽取"**(v0.30.37 读了源码后的结论):`createTwFrameSurface()` 里最像独立模块的那一段(`applyPendingHash` + `tryOnce` + `fallbackLoad`,约 60 行)引用了 **6 个闭包内的可变状态**;硬抽成模块就得把它们做成 7 个访问器(含 setter)—— 那不是"职责清晰",而是**把内部细节摊成一个又宽又漏的接口**,比原样更难读。**抽取不是目的,清晰才是**,所以先做第一步:**把状态收成对象**(此后抽取只需传一个 `surfaceState`)。② **先量爆炸半径再动手**(`grep scripts/*.mjs`):`pendingHash` 被 **4** 处源码级守门按文本钉着、`wikiSwitchPending` 被 **2** 处,而 **`hashWaitTimers` 与 `frameLoaded` 是 0 处** —— 于是本版**只合并这两个零风险的状态**(`const surfaceState = { hashWaitTimers, frameLoaded }`,7 处引用改名),把"改名撞上按文本匹配的守门"这类**事后排错**换成**事前排序**。③ 下一步(已写进交接笔记):再动 `pendingHash`/`wikiSwitchPending`,届时把那 4+2 处断言的判据从**「钉变量名」改成「钉语义」**;状态全部集中后,才做真正的模块抽取。**验证**:`typecheck` / `build`(client bundle 768,231 → 768,362 字节,仍 minify、远低于 1MB)/ `verify:unit`(42 套,退出码 0)/ 五个客户端守门(`verify-frame-guards` / `verify-frame-surface` / `verify-tw-origin` / `verify-wiki-focus` / `verify-client-i18n`)/ `verify-line-endings` 全绿。
12
+ - **v0.30.37**(2026-09-29):**给 v0.30.36 补上缺的那条行为守门**(本会话第 5 次「规则必须有人执行」)。v0.30.36 修好了「未知变化 ⇒ 保守重启该仓库下全部库」,但那一支当时**只有类型系统间接约束、没有直接断言**。本版在 `verify-wiki-farm.mjs` 既有的 `affectedBy` 用例里补一条:`ids(undefined)` ⇒ **该仓库下全部库**,与紧邻的 `ids([])` ⇒ `[]`(没有改动就不重启)**成对**钉住 —— 两条并排才说明「未知」与「没有改动」是**可区分**的两种状态,而不只是各自成立。**反向验证(带 build)**:把 `undefined` 分支改成 `return []` ⇒ `WIKI FARM CHECKS FAILED`;恢复 ⇒ 绿。② 顺带记一条纪律:这一支的守门是**行为级**的(import 编译后的 `lib/`),所以反向验证必须「注入 → `npm run build` → 跑守门」——v0.30.35 时我漏了 build,得到过一次「看起来通过了」的假验证(详见 wiki 常见坑)。**验证**:`typecheck` / `build` / `verify:unit`(42 套,退出码 0)/ `verify-wiki-farm` / `verify-line-endings` 全绿。
13
+ - **v0.30.36**(2026-09-29):**补完 v0.30.35 的另一半:未知变化 ⇒ 保守重启该仓库下的全部库**(同一个 `pull` 缺陷的收尾)。① v0.30.35 只做到「不再沉默」(回执警告),本版把**行为**也修正:`affectedBy(repoRoot, changedFiles)` 的第三参改为 `readonly string[] | undefined`,**`undefined`(diff 算不出来 ⇒ 不知道哪些文件变了)⇒ 重启该仓库下的全部 runtime**。宁愿多重启一个(代价是一次 TW 重启),也不能静默陈旧 —— 用户面前的 TW 是内存副本,磁盘被 pull 改过而没重启就会一直显示旧内容。② ⚠️ **谓词方向是个真陷阱,已写进代码注释**:受影响 = 文件落在库目录**里面**,所以正常分支是 `isInsidePath(runtime.path, filePath)`(第一个参数是容器);"全部库"必须用**反方向**的 `isInsidePath(repoRoot, runtime.path)`。**不能**拿一个假文件名(例如 `.`)去套正常分支 —— 那样问的是「repoRoot 是否在某个库目录里」,方向反了,**同仓多库里更深的那些库不会重启**(正是本版要覆盖的场景)。③ 签名链 5 处(`tools-support.ts` 的 `RestartAffected` 类型 · `routes.ts` 的路由依赖类型与调用点 · `tools-git.ts` 的局部类型与调用点 · `index.ts` 的两处 lambda · `index-wikis.ts` 的共享 helper · `wiki-farm.affectedBy`)由**编译器逐个点名**改齐,无一处靠猜;两个调用点统一写成 `pulled.changedFilesUnknown === true ? undefined : pulled.changedFiles ?? []`。④ **仍未加行为守门**:`undefined ⇒ 全部库` 这一支目前由**类型系统**(调用点必须处理 `| undefined`)与 `verify-repo-keying`(生产者三态)间接约束,**没有**直接断言"未知时重启了全部库"的行为测试 —— 要造一个多库同仓的 farm 实例,留给下一步。**验证**:`typecheck` / `build` / `verify:unit`(42 套,退出码 0)/ `verify-repo-keying` / `verify-git-multi-sync` / `verify-git-resolve` / `verify-line-endings` 全绿。
14
+ - **v0.30.35**(2026-09-29):**修「`pull` 的 diff 失败被当成『没有变化』⇒ 不重启任何库」**(第 4 条裁定追到底的一处**真缺陷**,不是假阳性)。① **缺陷链**:`git.ts` 的 `filesChangedBetween()` 里 `if (!r.ok) return []` —— **「diff 算不出来」与「真的没有变化」返回同一个值**;`pullUnlocked` 只在 `changedFiles.length > 0` 时带该键、却仍返回 `changed: true`(HEAD 确实动了);两个调用点(`routes.ts` / `tools-git.ts`)又当 `?? []`,`wiki-farm.affectedBy([])` 返回 `[]` ⇒ **一个库都不重启**。后果:`pull` 成功把 HEAD 从 A 移到 B(磁盘内容**真的变了**),但 TW 子进程仍在内存里渲染**旧内容**,**而回执写着同步成功** —— 内容变了、服务没跟上、回执不说。② **本版修「不再沉默」那一半**(单文件可完成、无签名扩散):`filesChangedBetween` 返回类型改为 `Promise<string[] | undefined>`(**`undefined` = 答不出来**,`[]` = 确实没变化),`pullUnlocked` 据此带出 `changedFilesUnknown: true`,并**在回执 message 里显式警告**「已拉取到新提交,但无法确定哪些文件变化 —— 没有自动重启任何知识库,请手动重载 TW 面板」。③ **保守重启(未知 ⇒ 该仓库下全部库)需要动 `restartAffected` 的签名链**(`tools-support`/`routes`/`tools-git`/`wiki-farm` 四处 + 新守门):`affectedBy` 的谓词是 `isInsidePath(runtime.path, join(repoRoot, file))`,所以"全部库"**不能**用一个假文件名糊过去 —— 完整修法写在 wiki 缺陷笔记里,下一步执行。④ **守门升级为三态**(`verify-repo-keying.mjs`):`''`/同一 ref ⇒ `[]`;**无法解析的 ref ⇒ `undefined`**,并加 `assert.notDeepEqual(…, [])` 把「必须与『没有变化』可区分」钉死。⑤ ⚠️ **反向验证过程中又踩到同一族坑**:该守门是**行为级**的(import 编译后的 `lib/`),只改 TS 源码**不会**让它变红 —— 第一次注入因此"看起来通过了反向验证",是我的检查脚本自己抛错才拦住(否则就会记下一条假的"已验证")。正确姿势是**注入后先 `build`**。**反向验证(带 build)**:改回 `return []` ⇒ `REPO KEYING CHECKS FAILED`;恢复 ⇒ 绿。**验证**:`typecheck` / `build` / `verify:unit`(42 套,退出码 0)/ `verify-line-endings` 全绿。
15
+ - **v0.30.34**(2026-09-29):**把三个「手动浏览器守门」写进文档**(第 4 条裁定的收尾,**行为零变化**)。核查「守门是否存在孤儿」时发现:`scripts/verify-*.mjs` 共 **56 个**,其中 **53 个**接在四条链(`verify:static` / `verify:unit` / `verify:e2e` / `verify:large`)里,剩下 3 个 **`verify-theme-browser.mjs` · `verify-clip-bridge-browser.mjs` · `verify-menubar-theme.mjs`** 是**有意**的手动浏览器 E2E(各自头部都写明「OPTIONAL dev tool / NOT part of CI:CI 上只会打 SKIP」)—— **不是死脚本**。但它们**在任何文档里都搜不到**,于是成了「规则存在、没人知道它存在」的一类:真出问题时不会有人想到去跑它。本版把三条命令连同**前置条件**(本机 Chrome/Edge + `puppeteer-core`;`verify-menubar-theme.mjs` 需要 `TW_URL` 指向**你自己**运行中的 wiki,且**故意没有默认值**)写进 `docs/development.md` 的命令清单,并标明「不在任何链里」。② 同一次核查还确认了两件事:**没有悬空的守门引用**;`docs/development.md` **早已写明** `npm run verify` 这个总闸(= static + unit + e2e + large)—— 也就是说「逐条手动拼链」是我的疏忽而非文档缺失(已把「发布前跑总闸,别自己拼」记进 wiki 交接笔记)。**验证**:`verify:static`(含文档链接 + 行尾)/ `verify:version-consistency` 全绿。
16
+ - **v0.30.33**(2026-09-29):**`ConfigStore` 的写路径:核实后决定「不改」,并把理由写具体**(第 4 条裁定的收尾,**行为零变化**)。v0.30.30/31/32 之后剩下的唯一一处手拼 body 是 `config.ts` 的配置 tiddler,本版**读完全文再判**:它**已经先读后写**,且**按文本**做幂等跳过(`storedText === nextText`,v0.25.0 为「每次点保存都产出一条 git diff、两台机器因此冲突」而加)。改成 `buildWriteTiddler` 的唯一收益是「保留自定义字段」—— 而这在配置 tiddler 上**没有这个场景**;代价却是**若写策略对正文做过任何规范化,幂等判据就会与实际落盘的文本不一致 ⇒ 每次保存都重新 PUT ⇒ 那条 git-diff 回归复活**。**收益小、风险确切 ⇒ 不动。** 于是把守门里那句含糊的「改前需确认」换成这段具体理由 —— 否则下一个人(或下一次审计)会再问一遍同样的问题,甚至"顺手修好"它。**验证**:`verify-write-call-sites` / `verify:unit`(42 套)/ `verify:version-consistency` / `verify:line-endings` 全绿。
17
+ - **v0.30.32**(2026-09-29):**给「共享写策略」补上调用点守门**(第 4 条裁定的收尾,把 v0.30.31 写下的规则变成可执行断言)。新增 `scripts/verify-write-call-sites.mjs`(进 `verify:unit`):钉的形态只有一种 —— **`client.put(` 后面直接跟对象字面量 `{`**(=手拼 body,TW 的整体替换会静默丢自定义字段);`const { tiddler } = buildWriteTiddler(…)` 之后 `client.put(tiddler)` **不算**(那就是共享写策略本身 —— 审计曾把它误算成「绕过」)。② 三条断言:**(a)** 手拼 body 的调用点必须全部落在**例外清单**里;**(b)** 清单**自清洁**(列了却已合规 ⇒ 红);**(c)** 清单每项必须写明 `why`。③ 守门**第一次运行就抓到两件事**:`src/host/routes-helpers.ts` 有一处我没数到的手拼 body(TW **草稿**写入 —— 草稿必须显式构造、**不能**以原条目为基底,否则会把笔记的 tags/自定义字段复制进草稿;已作为「特例」连同理由入清单),以及**我自己的 `index-clip.ts` 清单条目是过期的**(那里根本没有手拼 body ⇒ 自清洁断言当场把我抓出来)。④ 现状:手拼 body 共 **18 个文件** —— 7 个 seed 家族(内置内容)+ flush 探针 / `$:/temp` 汇总 / 配置 tiddler / 草稿,**全部有书面理由**。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(42 套,含新守门)全绿。
18
+ - **v0.30.31**(2026-09-29):**把「共享写策略」的例外清单写进代码与铁律**(第 4 条裁定的收尾,**行为零变化**)。① `write-policy.ts` 模块头新增一节「本策略的例外清单」:先澄清一个**曾被误报的口径** —— `const { tiddler } = buildWriteTiddler(…)` 之后跟 `client.put(tiddler)` **就是本策略本身**(规则要求的就是走 `buildWriteTiddler`),第二轮审计把它算成了绕过,于是「21 个文件绕过共享写策略」这个数字**大半是假的**;再列出真正允许手拼 body 的**两类**:**(1) 我们完全拥有内容**的条目(seed marker / `$:/temp` 会话汇总 / 剪藏写入 / seed 的内置内容)—— 读一遍没有信息量;**(2) flush 探针**(唯一用途是给一次写入计时并轮询落盘,改读-改-写会引入额外往返、**改变时序语义** —— 这条不是偷懒,是必须)。② `AGENTS.md` 铁律 #2 同步这一句,并写明判据:**这次 PUT 的 body 是不是基于某条已存在的用户条目改写的**(是 ⇒ 必须先 `get` 再 `buildWriteTiddler()`),**不是**文件名或行数。③ 为什么值得单独一版:这条规则此前是「绝对规则 + 隐性例外」,于是**每一次审计都会重新发现同一个「问题」**(本会话就重报了两次)。把例外写成可读的清单后,下次审计的结论只剩「清单里没有的新调用点」——那才是真缺陷。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(41 套)全绿(纯注释与文档,`lib/` 行为不变)。
19
+ - **v0.30.30**(2026-09-29):**`$:/language` 的条件写入改走共享写策略**(第 4 条裁定「写路径例外清单」分类后的**第一处真修复**)。`pinLanguageTiddler()` 此前**已经** `get` 过那条目(v0.24.2 为「内容没变就别 PUT」加的),却仍手拼 body:`client.put({ title: '$:/language', text: desired, type: 'text/plain', tags: [] })` —— 手拼就是铁律 #2 说的那件事:**整体替换会丢掉这条目的自定义字段**(`tags: []` 还会顺手清空标签)。改为 `buildWriteTiddler('$:/language', desired, { existing, fields: { type: 'text/plain' } })`:以已读到的条目为基底、显式保住 `text/plain`、自定义字段原样保留;`$:/language` 是系统条目,`buildWriteTiddler` 不会给它补 `agent-written`(`finalTagsForWrite` 对 `$:/` 前缀直接跳过)。**行为不变**:`verify-language-pin.mjs`(行为级,断言「内容相同只读不写」「内容不同才写」的调用序列)照旧全绿。② 顺带记录本轮的分类结论(见 wiki 待办笔记):先前「21 个文件绕过 `buildWriteTiddler`」的**口径是错的** —— `const { tiddler } = buildWriteTiddler(...)` 之后跟一句 `client.put(tiddler)` **就是**共享写策略本身,被我算成了绕过。按「body 是天生的还是手拼的」重新分类后:**不是违规 2 处、有意例外约 17 处(seed marker / flush 探针 / `$:/temp` 会话汇总 / 剪藏写入 / seed 家族)、真候选只剩 2 处** —— 本版修掉其中一处(另一处是 `config.ts` 的配置 tiddler,`ConfigStore` 有自己的合并逻辑,改前需确认语义不冲突)。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(41 套)/ `verify-language-pin` 全绿。
20
+ - **v0.30.29**(2026-09-29):**`wechat-publish.ts` 拆出「契约层」**(第 5 条裁定继续,**纯搬迁**)—— 也就是上一轮记为「会成运行期环、需先定方案」的那个文件的落地。① 新增 `wechat-publish-contract.ts`(275 行):适配器清单与各自的产物文件、DSN/Cli 值的合法性判定、配置形状与归一化、命令行调用怎么拼、输出怎么判成败、任务视图类型。`wechat-publish.ts` **700 → 450 行**,只剩有状态的 `WechatPublishRunner`(子进程、任务表、轮询)。② **先核实再动手**:`scripts/verify-wechat-publish.mjs` **按精确文件**读 `wechat-publish.ts` 并断言两条**赋值语句**的字面量(`export const WECHAT_ADAPTER_MARKER = 'resolveNoteTitle'`、`const WECHAT_TITLE_FILE_ARG = 'titleFile'` —— 它们是「入口版本哨兵」,真入口声明的参数名/函数名必须与之对齐)。核实后发现这两个符号**只被 base 里的 `WECHAT_FRESHNESS_MATCHERS` 用到**,且搬走的那段**一次都没引用**它们 ⇒ 把它们连同那条匹配器留在 base 即可,**既不动守门、也不造环**。这正是上一轮结论里 (a) 那一支:**一条 grep 就把「环」的担心消掉了**。③ 搬迁脚本这轮又暴露两个真实的生成器缺陷(都值得记):**(a) 别名 import** —— `import { spawn as nodeSpawn }` 的 specifier 整体是 `spawn as nodeSpawn`,而代码里出现的是**局部名** `nodeSpawn`,于是被「按名字过滤」滤掉了;判据要取 ` as ` **之后**的名字。**(b) 非 export 的声明** —— 原文件里 `const WECHAT_FLOW_FILE`、`const JOB_HISTORY_LIMIT`、`interface JobRecord` 本来不是导出的,搬进新文件后需要变成 `export`(否则新文件里「声明未使用」= 红),而 base 的 import 清单是**按 `export ...` 正则**扫出来的 ⇒ 漏掉这三个。修法是**先让新文件成稿,再从它的「实际导出」反推 base 的 import/re-export 清单**(而不是只信一次性的预扫描)。④ 另一处必须**排除**的范围:`WechatPublishRunner` 的 doc 注释(`/** … */`)属于类、不属于被搬的代码 —— 按行硬切会把它劈成两半,症状是 `'*/' expected` 与 `Unterminated regular expression literal` 这种「看起来像语法错、实则是范围错」的报错。⑤ 证据:逐行 `Compare-Object` ⇒ **零代码缺失**(唯一三条「只在旧文件里」的行就是被有意加上 `export` 的那三行);`WECHAT PUBLISH OK` + `WECHAT ADAPTER CHECKS OK`。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(41 套)/ `verify:e2e` 全绿。
21
+ - **v0.30.28**(2026-09-29):**`tw-frame.ts` 拆出「公共 API 层」**(第 5 条裁定继续,**纯搬迁**)。① 新增 `src/client/tw-frame-api.ts`(107 行):别的模块要用 tw-frame 的那一套 —— 两个事件名(`ACTIVATE_EVENT`/`PANEL_RELOAD_EVENT`)、`reloadTwSurfaces()`、标签文字(`setTabLabel`/`getTabLabel`)、`requestRestart()`、`TwTabIcon`、以及「这份 dataset 能不能直接当 iframe src 用」的 `loadableFrameUrl()` + `FrameLoadedState`。`tw-frame.ts` **736 → 652 行**,并把这些名字**原样再导出**(外部导入路径一个不变)。② **先证明了无环才动手**:脚本扫过这段代码引用的每个标识符,确认它**只引用自己人**、不引用文件后半部分的任何东西(`TwFrameSkin`/`createTwFrameSurface`/`liveFrames` 一个都没出现)—— 与 v0.30.27 那次相反,这里**不需要**让 base 反向 import,所以搬得干净。③ **两处 `tabLabel` 直接读改成 `getTabLabel()`**:内核里原来写的是 `setTabLabel(payload.ui.tabLabel ?? tabLabel)`,直接摸模块级变量;搬走后 base 不能再碰它,改用公开的 accessor(同一份状态、语义完全等价)。这与 v0.30.27 的教训是同一类:**搬迁会逼出「谁有权碰这份状态」这个问题**,答案是 accessor 而不是把状态留在原地。④ 守门同步:`scripts/verify-tw-origin.mjs` 原本钉死字面量 `import { RESTART_ENDPOINT, resolveTwUrl, twProxyFor } from './endpoints.ts'`,而 `RESTART_ENDPOINT` 跟着 `requestRestart` 搬走了 —— 判据改成「族里**从 endpoints.ts 取到了**这两个必需符号」+ 单独钉住 `RESTART_ENDPOINT` 仍在族内(意图不变:**该拿到的符号一个都不能少**,但不该锁定某一行的写法)。⑤ 证据:逐行 `Compare-Object` ⇒ 只有 3 行差异且**全是有意改动**(2 处 `?? tabLabel` → `?? getTabLabel()` + 那条 import);客户端 bundle 768227 → 768231 字节(仍在 1MB 限制内、仍 minify)。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(41 套)/ `verify:e2e` 全绿;客户端四个守门(`verify-frame-guards` / `verify-frame-surface` / `verify-tw-origin` / `verify-wiki-focus` / `verify-client-i18n`)亦绿。
22
+ - **v0.30.27**(2026-09-29):**`seeds.ts` 拆出「注册表装配层」**(第 5 条裁定继续,**纯搬迁**)。① 新增 `seeds-registry.ts`(422 行):`SEED_DEFS` 那张表 + 装配它的全部东西(`defineSeed()`、`inspectSeedContent`、`refreshSeedMarker`、`SeedMeta`/`SeedWriter`/`SeedContent` 类型,以及三个小判定 `presentOf`/`removedDetail`/`isOurProxyBase`)。`seeds.ts` **657 → 262 行**,只剩**编排**(`checkAllSeeds` / `runSeedById` / `runAllSeeds` / `removeSeedById` + 接口 + 排干原语的再导出)。② **分界线是「策略 vs 编排」**:此前想回答「哪些 seed 会被自动写、各自的 gate 是什么」得先翻过几百行实现;现在一张表一个文件。③ **依赖方向单向**:base 从注册表取 `SEED_DEFS`,注册表用 `import type` 拿 base 的三个类型(`SeedContext`/`SeedDef`/`SeedRunResult`/`SeedStatus`)——类型期闭环、运行期无环。注册表的值依赖全部来自**其它**模块(`seed-util` / `config` / `wiki` / 12 个 `seed-*`),所以 `isOurProxyBase` 必须**跟着搬**(它被 `defineSeed` 的实现用到),否则会造出「注册表 → base → 注册表」的真环。`seeds.ts` 原样再导出 `SEED_DEFS` 与 `isOurProxyBase`,**外部导入路径一个都不变**。④ **搬迁手法升级为「编译器驱动」**:脚本解析 base 自己的 import 语句、按「这段代码里到底用了哪些名字」过滤,逐个模块重生成注册表的 import 行;base 侧则由 `tsc` 的 `noUnusedLocals` 点名后**只删「确实是 import 声明」的行**(`TS6192/TS6133` 也可能指向未用变量,所以删除前逐行核对前缀)——15 行一次删净,零猜测。⑤ 顺手把 `scripts/verify-wechat-docs-seed.mjs` 从「按精确文件名读 `seeds.ts`」改成 `readFamily(repoRoot, 'src/host/seeds')`(拆分后它读不到注册表 ⇒ 稳定失败在那个点,正是本仓库「守门按模块族读」的由来)。⑥ 证据:逐行 `Compare-Object` ⇒ **零代码缺失**(唯一两条「只在旧文件里」的行就是被有意加上 `export` 前缀的那两行)。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(41 套)/ `verify:e2e` 全绿,`verify-seed-error-policy` / `verify-restart-drain` / `verify-mutation-lock` 亦绿。
23
+ - **v0.30.26**(2026-09-29):**补上 v0.30.22 漏掉的用户文档**(纯文档,无代码改动)。`docs/usage.md` 的工具表仍写着 `tiddlywiki_attach` 存「二进制附件」,而 v0.30.22 之后文本类(`text/*`、`application/json`)存的是**纯文本正文**。这一行的措辞直接决定用户(和模型)对「我 attach 一个 .md 之后能不能搜到它」的预期,所以按实际行为改写(二进制 → base64;文本 → 纯文本 ⇒ 进检索)。顺带核对 `docs/features.md` / `usage.md` 其余关于附件与 base64 的说法:剪藏桥的图片、以及「检索跳过二进制附件」那两条描述**依然准确**(文本附件现在正确地参与检索,正是 v0.30.22 的意图)。**验证**:`verify:static`(含 `verify-doc-links`)全绿。
24
+ - **v0.30.25**(2026-09-29):**`admin-routes.ts` 拆出最后两条业务路由**(`config` + `prompt`),本文件终于成为**纯组装点**(第 5 条裁定的继续,**纯搬迁** + 工厂化)。① 新增 `admin-routes-config.ts`(131 行):`POST /admin/config`(保存配置补丁)与 `GET|POST /admin/prompt`(提示词预览,POST 带草稿且不落盘)收进 `createAdminConfigRoutes(deps, helpers)`。base **358 → 272 行**(本文件累计 **746 → 272**),现在只剩 `AdminDeps` 与名册接口、两个闭包 helper、四个工厂调用、注册表 —— 与 `routes.ts` 同一种「组装点」形态。② **依赖面这次是「按代码生成」而不是手抄**:脚本在切出的两段代码里扫 `deps.X`,据此拼出 `Pick<AdminDeps,…>` 与调用点成员(结果是 `config` / `getClient` / `getPrompt` / `onConfigChanged` 四项)。③ **两次自我纠错值得记下来**:**(a)** 第一次切 `prompt` 时我把范围切宽了,**把相邻的 `createAdminWikiRoutes(...)` 工厂调用一起卷进了新文件** —— 症状是新文件里出现 `Cannot find name 'createAdminWikiRoutes'`,而 base 静默少了那次调用。根因是我按「`handlePrompt` 之后的下一条注释」猜终点,而中间还夹着两次工厂调用;改成**按内容找终点**(找到 `handlePrompt` 的收尾 `}`,并断言它确实是 ` }`)后一次通过。**(b)** 重做前我漏了 `git checkout` 恢复 base,于是脚本在**上一轮的破损状态**上又切了一刀(base 511 行、新文件 349 行全是垃圾)。补上「行数必须是 358、终点行必须是 ` }`」这类**前置断言**之后,坏状态再也无法静默产出垃圾 —— **搬迁脚本必须自带前置断言**,这是本轮最实在的一条教训。④ 证据:逐行 `Compare-Object` ⇒ **零代码缺失**,唯一三条「只在旧文件里」的行就是**被有意改写的那三条 import**。⑤ `verify-restart-drain` / `verify-mutation-lock` / `verify-config-patch` / `selftest` / `verify:e2e` 全绿(config 那一组会拿变更锁并热应用 git 配置)。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(41 套)/ `verify:e2e` 全绿。
25
+ - **v0.30.24**(2026-09-29):**`admin-routes.ts` 再拆出「状态 / 插件·主题·语言」那一组**(第 5 条裁定的继续,**纯搬迁** + 工厂化)。① 新增 `admin-routes-info.ts`(224 行):`GET /admin/state`(设置页总览)与 `POST /admin/info`(写 `tiddlywiki.info`,必要时重启 TW)收进 `createAdminInfoRoutes(deps, helpers)` —— 依赖面 `Pick<AdminDeps,'config'|'getClient'|'getWikiPath'|'server'|'targetProblem'|'twRoot'>` 加 base 的闭包 helper `refuseStoppedTarget`。base **535 → 358 行**(本文件累计 746 → 358)。② 搬迁过程中 **tsc 替我发现两件值得记的事**:**(a)** 这一组里的 `handleInfo` **也**调 `drainThenStop` —— 也就是说「会 drain + 重启子进程的 admin 路由」**不止 seeds 那一组**,新文件同样落在铁律 #1 的守门范围内(`verify-restart-drain.mjs` / `verify-mutation-lock.mjs` 都按模块族读 admin,搬迁后照旧绿,但我把这条写进了新文件的模块头);**(b)** 顺着 `noUnusedLocals` 精确清掉了三条**只被这一组使用**的 import(`redactLogLines` / `redactRemoteUrl` 从 routes.ts、整条 `admin-catalog` 导入)与一个多余的 helper 参数 —— 这次**没有靠猜**:先搬、再让编译器点名、再删,删错会立刻红。③ 证据:HEAD 全文非空行 vs「base + 新模块」逐行 `Compare-Object` ⇒ **零代码缺失**(唯一两条「只在旧文件里」的行就是**被有意改写的那两条 import**)。④ 行为侧由 `selftest`(真实打 `/admin/state`)与 `verify:e2e` 覆盖。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(41 套)/ `verify:e2e` 全绿。
26
+ - **v0.30.23**(2026-09-29):**把一条审计假阳性就地钉死**(第 4 条裁定的核对环节,**行为零变化**)。第二轮审计把 `session-scope.ts` 的两条正则记成「sessionId 正则 ×2(冗余实现)」,核实后是**假阳性**:`isSafeSessionId` 校验的是 **DSH 生成的**会话 id(实测允许大写与 `:`,上限 120),`isSafeWikiId` 校验的是**我们自己造的**库 slug(只允许小写,上限 64)—— 两条看着像、语义不同。合并的两种后果都是真故障:按 wiki 的规则收紧会**拒掉合法会话 id**(作用域静默丢失),按会话的规则放宽会**放行非法库 id**(报错点离现场很远,得等 registry 兜底)。所以本轮不动代码行为,只在两处就地写明「故意不同、别合并」并注明这条审计是假阳性 —— **判据要落在下一次会读到它的地方**,而不是只写在审计笔记里(否则同一个假警报会周期性回来)。**验证**:`typecheck` / `build` / `verify-session-scope` / `verify-line-endings` 全绿。
27
+ - **v0.30.22**(2026-09-29):**修 `tiddlywiki_attach` 把文本文件存成 base64**(第二轮审计的遗留项,与 v0.23.5 那条「别把二进制交给 `put` 写文本」正好互为镜像)。旧实现无条件 `buffer.toString('base64')`,于是 attach 一个 `.md` / `.txt` / `.json` 会得到 `type: text/markdown` + **一整段 base64**:TW 把那段 base64 当**正文**渲染(满屏乱码),而 `isBinaryType('text/markdown')` 又是 `false`,所以它还会被算成「文本条目」进检索与 lint —— 搜出来同样是乱码。现在按 TW 自己的约定分流:`text/*` 与 `application/json` 存**纯文本正文**(且只接受能**原样往返**的 UTF-8;不能往返的例如 latin-1 `.txt` 退回 base64),其余照旧 base64。`AttachResult` 新增 `encoding: 'text' | 'base64'`、`chars` 改为「实际存进 tiddler 的正文长度」,回执据此如实写「纯文本 N 字符 / base64 约 N 字符」;工具 schema 的 description 也同步说明(原文里「同名已有条目默认拒绝写入」那句一字未动 —— `verify-prompt.mjs` 会断言它)。**已反向验证**:把分流条件写死为 false(=旧行为)⇒ `selftest` 稳定失败在「文本附件按纯文本存(text/markdown/base64)」;恢复 ⇒ 通过。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(41 套,含 `verify-prompt` 1011 字符 slim)/ `verify:e2e` 全绿。
28
+ - **v0.30.21**(2026-09-29):**修掉剪藏桥「同一秒内连剪两次会撞名」**(第二轮审计的遗留项)。`resolveClipTitle` 的去重顺序是 `base` → `base(2)…(20)` → **时间戳兜底**,而兜底只精确到**秒**(`toISOString().slice(0, 19)`)。走到兜底意味着这一页已经剪过 **20+ 次** —— 也就是用户正在**批量重剪**,此时「同一秒里剪两次」(连点书签、或浏览器把书签跑了两遍)不是边角情况而是常态:两次算出**同一个标题**,第二次直接撞上刚写下的那篇。现在兜底带**毫秒**,且对每个候选名仍然**真的问一次 `exists`**(同一毫秒内继续加 `-<n>` 序号)—— 不再因为「时间戳不同了」就假设名字空着;`now` 做成可注入参数,让回归测试能钉住同一毫秒、仍然观察到两个不同的标题。**已反向验证**:把毫秒改回秒 + 去掉候选探测 ⇒ 新用例稳定变红;恢复 ⇒ 绿。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(41 套,含 `verify-clip-bridge`)/ `verify:e2e` 全绿。
29
+ - **v0.30.20**(2026-09-29):**`admin-routes.ts` 再拆出「一次性预置(seed)」那一组**(第 5 条裁定的继续,**纯搬迁** + 工厂化)。① 新增 `admin-routes-seeds.ts`(142 行):`GET /admin/seeds`、`POST /admin/seeds/run`、`POST /admin/seeds/remove` 收进 `createAdminSeedRoutes(deps, helpers)` —— 依赖面 `Pick<AdminDeps,'getClient'|'getWikiPath'|'mutationLock'|'seeds'|'server'>` 加 base 的闭包 helper `notRunning`,与 `createNoteRoutes(deps, helpers)` 同形状。base **632 → 535 行**(本轮累计 746 → 535)。② **为什么这一组值得单独出去**:`/seeds/run` 是全仓库**唯一**「写完就 drain + 重启 TW」的 admin 路由(seed 里的 server-route 插件只在 TW 启动时加载),于是它同时牵着两条铁律 —— 排干必须经 `drainThenStop`(铁律 #1)、这一段必须持有共享变更锁(v0.30.12)。单独成文件后,「谁能在 admin 侧停子进程」只有一处可读。③ 顺带清掉三条因此变成**未使用**的 import(`waitForFileWrite` / `needsRestartAfterSeeds` / `RENDER_PLUGIN_FILE` 只在这一组里用)—— `noUnusedLocals` 会替我们确认没删错:`drainThenStop` 仍被 base 的另外两处用着,所以保留。④ 证据:HEAD 全文非空行 vs「base + 新模块」逐行 `Compare-Object` ⇒ **零缺失**(唯一「只在旧文件里」的行是那条被有意拆成两条的 import)。⑤ 这一组的两条铁律守门 `verify-mutation-lock.mjs` / `verify-restart-drain.mjs` 都按**模块族**读 admin(`admin-*.ts`),搬迁后照旧绿;行为侧由 `selftest`(seeds 的 run/remove 断言)与 `verify:e2e` 的 `verify-seeds-admin.mjs` 覆盖。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(41 套)/ `verify:e2e` 全绿。
30
+ - **v0.30.19**(2026-09-29):**`admin-routes.ts`(746 行)拆出「知识库列表 / 位置」那一组**(第 5 条裁定的继续,**纯搬迁** + 工厂化)。① 新增 `admin-routes-wikis.ts`(145 行):`/admin/wikis`(GET 读控制文件、POST 增删改 / 设默认 / 切模式)与 `/admin/wiki/{location,switch,reset}` 四条 handler 收进 `createAdminWikiRoutes(deps)`,依赖面用 `Pick<AdminDeps,'server'|'wiki'|'wikis'>` 写明 —— **一个闭包 helper 都不需要**。base **746 → 632 行**,注册表(那 12 行 `webServer.register`)一行未改。② 为什么按这一组切:它们回答的是「插件现在服务哪个目录 / 控制文件里有什么」,与 `/admin/{state,info,config,seeds}` 那组(改**当前库**的配置)是两件事;而且它们**不吃 `?wiki=`** —— 请求问的就是「哪个库」本身,所以这一组也不需要 `refuseStoppedTarget`(搬迁时顺手把这条写进注释,免得下一个人以为漏了)。③ **「什么都没丢」的证据**:HEAD 版全文的非空行 vs「base + 新模块」的非空行逐行 `Compare-Object` ⇒ **零缺失**,多出的 28 行全部是模块头注释、3 行 import、工厂签名与类型、调用点、返回语句。④ 被搬走的路由由 `verify:e2e` 真跑覆盖(`WIKI FARM BOOT` 打 `/admin/wikis`、`WIKI SWITCH` 打 `/admin/wiki/{switch,reset}`),均绿。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(41 套)/ `verify:e2e` 全绿。
31
+ - **v0.30.18**(2026-09-29):**`src/host/session-summary.ts`(633 行)拆出扫描层**(第 5 条裁定的继续,**纯搬迁**)。① 新增 `session-summary-scan.ts`(239 行):`NOTE_TOOL_NAMES` + 两个扫描上限(`MAX_SESSIONS` / `MAX_SEARCH_RECORDS`)+ 六个只与「读事件日志」有关的函数(`collectDescendantIds` / `isPlausibleTitle` / `record` / `trimTrailingClosers` / `scanRefs` / `scanSnapshot`)。base **633 → 416 行**,只剩编排(`writeSessionSummary`)与探测 / 格式化 / wikitext 三段。② **顺手纠正速查表里的一个错**:那条「仍未拆」清单里的 `session-summary.ts`(633) 指的是**宿主**这一个,客户端有个**同名**文件只有 430 行、是另一件事(会话「知识库」Tab 的视图)—— 本轮把宿主这个拆完,并顺带按实测更新清单里的行数(`tw-frame.ts` 718 → **736**)。③ **搬迁纪律**:类型用 `import type` 回 base(类型期闭环、运行期无环,本仓库既有约定),而**值**(两个上限常量)**搬进扫描层**、让 base 反过来 import —— 依赖只有一个方向,不制造运行期环。④ **"什么都没丢"的证据**:把 HEAD 版全文的非空行与「base + 新模块」的非空行(剥掉 `export ` 前缀后)**逐行比对**,结果只差新加的模块头注释与两行 import、**零缺失**(`Compare-Object` 输出 19 行全是我新加的);行为证据是 `selftest` 里真的跑了一遍 `/session/summary`(`produced:5, read:3, searches:2, sessions:3`)照旧。⑤ 过程里踩了自己的坑:`[IO.File]::WriteAllLines` 在 Windows 上写 **CRLF**,被刚落地一天的行尾守门当场抓住(先红后归一)—— 那条新守门第一次真的拦住了东西。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(41 套)/ `verify:e2e` 全绿。
32
+ - **v0.30.17**(2026-09-29):**公众号三条路由补上「已登记但没在跑 → 503 点名」+ 铁律「行尾必须 LF」终于有了执行者**。① `/wechat/{ready,publish,publish/status}` 此前是**唯一**不吃 `?wiki=` 目标检查的路由,而 `wechatConfig(req)` / `wechatReady(req)` 都是**按请求上那个库**取值的 —— 于是设置页写着「正在配置 books」、公众号的就绪状态与配置却来自**默认库**,而且看起来一切正常。现在三条都在 `guardWechat` **之后**补上 `refuseStoppedTarget`(先认证/开关、再判目标;这个顺序由 `verify-wechat-publish.mjs` 钉死的「先判方法+同源、再判开关/token」相邻性决定),`createWechatRoutes` 与 `createNoteRoutes` 一样收到闭包 helper。② **过程中揪出一处静默的仓库不一致**:`src/host/routes-wechat.ts` 的**工作区**副本是 CRLF,而 git blob 是 LF —— `.gitattributes` 会在 commit 时把 CRLF 归一成 LF,于是 `git status` / `git diff` **完全看不到**这个状态。它当天的实际后果是:我用 `[IO.File]::WriteAllText` 做的一次**反向验证注入没匹配上、变成了空操作**,守门照样绿 —— 差一点被读成「断言不生效」而去改一条本来正确的断言。③ 所以顺手把这条铁律变成**有执行者**的规则:新增 `scripts/verify-line-endings.mjs`(进 `verify:static`),扫 `src/` `scripts/` `tools/` `docs/` `lib/` 与根目录共 **244** 个文本文件,任何 CRLF 直接红;并带一条反向保险「扫到的文件数 < 50 就报错」,免得目录名/白名单写错时它「零文件、零违规、全绿」(本仓库管这叫守门在空转)。**已反向验证**:给一个源文件注入 CRLF → 红;恢复 → 绿。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(41 套)/ `verify:e2e` 全绿。
33
+ - **v0.30.16**(2026-09-29):**数值配置项的「同一组数字」收成一条通用守门**(第 4 条裁定的继续,接 v0.30.15)。v0.30.15 只钉住了 `startup.readyTimeoutMs` 一处;这次发现**每个**数值配置项都患同一个病:数字散在**两到三处**、各自决定不同的事 —— 宿主 `config.ts` 的 `NUMBER_CONFIG_RANGES`(保存时**夹取**到哪)、客户端设置页表单的 `min`/`max`(**允许填**什么)、以及表单在「还没配置过」时回显的默认值。只改一处就会重现本仓库反复记录的那个症状「**设置页显示的值 ≠ 真正生效的值**」,而且**没有任何报错**(夹取是静默的、回显是照抄的)。新增 `scripts/verify-config-ranges.mjs`(进 `verify:unit`),刻意只做**数字比对**:① 两侧**键集合必须完全相同**(新加一个可夹取路径却没给表单字段,或反过来);② 每个键的 `[min, max]` 数值相同;③ 默认回显 == 宿主 `DEFAULTS` 的默认值。宿主侧允许写**导出常量**(`READY_TIMEOUT_MIN_MS`、`CLIP_BRIDGE_DEFAULT_PORT`),守门会把常量解析成数值 —— 它鼓励「只有一处定义」,而不是把常量逼回字面量。解析上刻意避开两个坑:取 `DEFAULTS` 的体用**花括号配对**(同名的**类型声明** `port: number` 会被普通正则先匹配到,于是拿「类型」当「默认值」比、还比得过),以及强断言解析出的字段数 ≥ 4(解析器坏了必须**响亮**,否则就是「以为在守、其实没守」)。**已反向验证三种注入**:表单区间漂移 → 红;表单默认回显漂移 → 红;宿主夹取表漂移 → 红;恢复 → 绿。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(41 套)全绿。
34
+ - **v0.30.15**(2026-09-29):**就绪窗口的上下界收成「唯一来源」+ 一条反向验证过的守门**(第 4 条裁定「该清理的就清理」的继续)。`startup.readyTimeoutMs` 的 `5000–600000` 此前在**三处**各写一遍:`ready-policy.ts` 的导出常量(权威)、`config.ts` 的 `NUMBER_CONFIG_RANGES` 夹取表、客户端设置页表单的 `min/max`(外加一份默认回显 `60000`)。三者分别决定「宿主怎么夹」「表单允许填什么」,只改一处就会重演本仓库反复记录的那个症状——**设置页显示的值 ≠ 真正生效的值**。现在宿主夹取表**直接引用** `READY_TIMEOUT_MIN_MS` / `READY_TIMEOUT_MAX_MS`(宿主侧从此不可能漂,且 `config.ts → ready-policy.ts` 无环);客户端进不了 host 模块(两份 bundle 分开构建),所以改成**钉数值**:`scripts/verify-ready-policy.mjs` 新增一条断言,把表单的 `min` / `max` / 默认回显分别与宿主三个常量**数值比较**,并断言宿主夹取表必须引用常量而不是写死数字。**已反向验证**:把表单的 `max` 改成 `600_001` → 红;恢复 → 绿。**验证**:`typecheck` / `build` / `selftest` / `verify:unit`(40 套)全绿。
35
+ - **v0.30.14**(2026-09-29):**客户端 i18n 收官 + `tool-views.ts` 拆分 + 设置页重启后顺手重载面板**(第 3/5 条裁定的收束,外加作者当天追加的一条)。① **i18n 全量落地**:本轮转完最后三个文件并补齐接线 —— 工具卡 `tool-views.ts`(49 条 `card.*` 键进此前空着的 `i18n-card.ts`)、设置页插件/主题/语言/初始化 `settings-page-catalog.ts`(60 条 `settings.*` 键)、常规配置 `settings-page-config.ts`(73 条 `config.*` 键)。目录 **283 → 467 键 / 7 个分片**,守门的 `PENDING_CONVERSION` **清空** —— `src/client/` 下不再有未转换的用户可见中文(机制保留,防止下一处新界面又直接写中文)。工具卡的 15 个徽标改成**惰性取值**(`TOOL_LABELS` 存 `() => t('card.toolX')`),模块级常量不再把 bundle 加载那一刻的语言永久冻住。② **`tool-views.ts` 940 行 → 7 个模块 + 组装口**:`tool-views-{types,fetch,shell,body,search,misc,links}.ts`(类型与参数解码 / 取数·`useAsync`·正文 LRU 缓存 / 卡片外壳与知识库作用域 / 原生渲染正文卡与列表原语 / 检索·最近·批量卡 / 附件·标签·git·删除卡 / TW 代理路径判定与链接拦截器),`tool-views.ts` 只剩调度器 + `TOOL_VIEW_KEYS` 注册并原样 re-export 公共面(`index.ts` 的 import 一字未改)。③ **设置页「重启 TW」成功后顺手重载面板**(作者 2026-09-29 追加要求):新增 `tw-frame.ts` 的 `reloadTwSurfaces()`,把此前只有 FAB 菜单在用的 `PANEL_RELOAD_EVENT` 收成一个具名出口;四条**会就地重启 TW** 的路径(状态行「重启 TW」+ 插件/主题/语言三个「应用…(重启 TW)」)在成功回执后调用它,并 `invalidateStatus()` 丢掉旧的 `/status` 探针缓存 —— 此前面板会一直显示重启前那份文档,而「重新载入」只藏在 FAB 菜单里。④ **守门**:三个**按精确文件名**读源码的守门改读模块族(`verify-tool-views` 的措辞断言、`verify-render-sanitizer` 的两条、`verify-wiki-focus` 的卡片断言),措辞类断言统一改成仓库既有的「目录里有该文案 + 代码里有 `t()` 调用点」形状(`verify-plugin-runtime.mjs` 同办),并新增一条把「`tool-views-fetch.ts` 必须经 `render-fetch.ts` 取渲染片段」钉死 —— 补上模块族带出的那条新缝隙。⑤ **修掉一个守门自身的解析 bug(当天真踩了两次)**:`verify-client-i18n.mjs` 的 `stripComments()` 先剥块注释、后剥行注释,于是**行注释里**出现「块注释起始符」(例如 `/wechat/` 后面跟一个 `*`)会一路吞到下一个结束符之前的**真实代码** —— 14 个正常键被误判成「从未使用」;现在**先剥行注释**,并把这条坑写进函数头(第二次是给这个函数写说明时,说明文字里又出现了结束符,直接把脚本写成 SyntaxError)。⑥ 顺带把 `ui.tabLabel` 的空值兜底改成 `t('frame.tabLabelDefault')`(zh 逐字不变,en 下显示 "Knowledge base")。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(含 i18n 守门 467/467)/ `verify:e2e` 全绿。
36
+ - **v0.30.13**(2026-09-29):**客户端 i18n 收尾(第 3 条裁定,283 键 / 20 个文件已转,只剩 3 个)**。本轮按「串行手工、每步守门全绿」的纪律一次转完 8 个文件:**note-widget.ts**(卡片与对话框的全部文案:标题/占位符/「写入」目标/最近列表/上传/保存与冲突提示,共 40 条新键)· `note-widget-draft.ts`(相对时间)· `note-widget-upload.ts`(上传 toast)· `editor-popup.ts`(原生编辑器浮层:标题、关闭、拖拽提示、aria-label)· `knowledge-fab.ts`(FAB 菜单:状态行 / 悬停 tip / 多库分组 / 打开·收起面板 / 重载 / 同步 / 快速笔记)· `quick-note-dock.ts`(按钮标签与三种悬停 tip)· `sidebar-entry.ts`(每行的 aria-label)· `tw-frame.ts`(中央面板与右栏的 TW 内核状态行:不可用 / 启动中 / 重试 / 重启中 / 「正在载入知识库『X』…」/ `/status` 不可达 / 状态码)· `wiki-scope-dock.ts`(会话选择器:标签、aria、悬停说明、「(默认)」「(未运行,选中会启动)」身份标记、切换失败)· `session-summary.ts`(会话「知识库」Tab 的空/错/降级态与刷新按钮)。① **顺手修掉三处语言相关的真缺陷**:**(a) 最近列表的日期兜底**硬编码 `toLocaleDateString('zh-CN')` ⇒ 英文界面下最近列表出现中英混排,现在跟随 `getLang()`;**(b) `tw-frame.ts` 的 `tabLabel`** 是模块级 `let tabLabel = '知识库'` —— 若照搬 `t()` 到模块作用域,会把「bundle 加载那一刻缓存的语言」永久冻住(换语言再也不生效);现在默认值**延迟**到 `getTabLabel()` 里求值(`tabLabel ?? t('frame.tabLabelDefault')`,`setTabLabel` 接受 `undefined`),`ui-config.ts` 那处模块级 `t()` 也一并复核过;**(c) `sidebar-entry.ts` 的「新建会话」选择器**是 DSH shell 自己按钮的 aria-label 匹配(两种语言都要匹配),属于**协议不是文案** —— 这是本轮唯一新增的 ALLOWLIST 条目,同时把 v0.30.6 留下的 `session-summary.ts` 那条(转换后已无用)删掉,避免白名单慢慢变成「什么都放行」。② **守门同步**:`verify-wiki-focus.mjs` 里「换库提示必须写清是哪个库」原来匹配 `tw-frame.ts` 里的模板串,文案搬进目录后假红 —— 改成仓库既有的「目录里有带 `{label}` 的模板 + 代码里有 `t('frame.loadingWiki', { label })` 调用点」形状(这条断言在本轮**真的红过一次**,不是预防性修改)。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(32 套,含 i18n 守门 283/283)/ `verify:e2e` 全绿。
37
+ - **v0.30.12**(2026-09-29):**两个真缺陷 + 三条新守门**。① **`tiddlywiki_append` 的 `heading` 会把 Markdown 图片当成标题**:wikitext 标题正则 `/^!{1,6}\s*(.*)$/` 同样匹配 Markdown 图片语法(叹号紧跟一个方括号开头的行),于是该行被认成「一级标题,标题文本就是那串图片语法」。后果两层:模型可能被回执骗着 append 到一个"图片标题"上;更实际的是**定位与切段**——`heading: '工作日志'` 扫「下一个标题」时会在图片行提前截断,内容落进图片与正文之间。现在加负向先行断言 `(?![[!])`:叹号紧跟方括号的图片行不再算标题,而 `! 章节`、`!章节`、`!! 小节` 照旧(TW 两种写法都合法)。② **变更锁只覆盖了一半**:`/restart` 与 `/sync` 互相排斥,但**各持一把私有标志**(`routes.ts` 里的 `mutationInFlight`),`admin-routes.ts` **一把都没有** ⇒ 设置页能在 `/sync` 正排干 syncer 队列时按「重启」(`/admin/restart`),或在与重启重叠时跑初始化(`/admin/seeds/run`)——两者都会停 TW 子进程并抢同一个队列,正是这把锁当初要防的事。现在抽出 `host/mutation-lock.ts`,由 `index.ts` 建**唯一**实例并同时注入 `RouteDeps.mutationLock` 与 `AdminDeps.mutationLock`;`/admin/restart` 与 seeds-run 的重启段都拿锁、拿不到回 **429 并报出持有者**、`finally` 释放。③ **守门**:新增 `scripts/verify-mutation-lock.mjs`(进 `verify:unit`)——行为级(独占 / 释放后可再取 / idempotent / 两个实例互不影响,后者正是缺陷当年的形态)+ 接线级(`createMutationLock()` 在 `index.ts` 只能调**一次**、两个 deps 都拿到、别处不得手写同一套标志)。另在 `verify-write-policy.mjs` 追加三条 `insertIntoSection` 用例(图片不算标题 / 真 wikitext 标题仍认 / CRLF 不退化),并把 `insertIntoSection` 从 `index.ts` 导出给 headless 守门(此前它的唯一回归测试在 e2e 里,要 spawn TW,太慢)。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(39 套)/ `verify:e2e` 全绿。
38
+ - **v0.30.11**(2026-09-29):**客户端 i18n 继续推进(第 3 条裁定)+ 守门解析器加固**。① 本轮把 5 个小文件全部转完并补进目录(共 181 键):`scope-seat.ts`(选择器座位标签)· `endpoints.ts`(同步回执后缀 `,TW 已重启` / `,TW 未自动重启` —— 它会拼进设置页与同步按钮的 toast,是最容易漏的一处用户可见文案)· `rightbar-tab.ts`(右侧边栏 Tab 的标题与说明)· `client/index.ts`(dock「快速笔记」与设置页分区名)· `note-widget-tags.ts`(标签输入框占位符 + 添加/移除标签的 aria-label,后者用 `{tag}` 占位符)。② **守门解析器加固**:`verify-client-i18n.mjs` 的 `chunkKeys()` 原来靠「`},` 是否单独成行」切 zh/en 区——某个分片把收尾括号写在最后一条同一行时,zh 区会吞掉整个 en 块(22 条、11 条重复),报成「键集合不一致」这种**看不懂的假红**。现在按 `zh: {` / `en: {` 两个语言头切分,对任何括号排版都稳。③ 执行方式调整(重要教训):**i18n 逐文件转换改为串行手工做**——本会话早些时候的并行子代理方案里,12 个子代理有 10 个静默失败并留下「文件改了、目录没写」的半成品,比不做更糟;串行做一轮 5 个文件、每步 `typecheck` + 守门全绿,可控得多。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(38 套,含 i18n 守门 181/181)全绿。
39
+ - **v0.30.10**(2026-09-29):**`routes.ts` 拆分第 3 步(笔记组)+ 修掉 `/upload` 的多库 URL 缺陷**。① `routes-note.ts`(373 行):`/note` `/tags` `/recent` `/get` `/search` `/upload` 六个 handler 抽成 `createNoteRoutes(deps, helpers)`——依赖面用 `Pick<RouteDeps,'getClient'|'getWikiPath'|'server'|'serverById'|'wikiIds'|'wikiSummaries'|'noteDefaults'|'autoCommit'>` 写明,闭包 helper(`refuseStoppedTarget`/`notRunning`/`invalidateGitStatus`)由参数传入;`routes.ts` 1373→**667**(`handleStatus`/`handleEdit`/`handleRestart`/`handleSync` 与 `twProxyAbsoluteBase` 按守门要求留在原地,注册的 kind/path/guardHandler 包法一字未改)。② **顺手修的真缺陷**:`/upload` 回给客户端的 `url` 原来是 `${TW_PROXY_PATH}files/<name>` ——**裸路径,等于默认库**。多库安装里往「书籍库」上传图片,拿到的链接会指到**默认库**的 `/files/`(图裂,或更糟:指向另一个库里同名文件)。现在走新增的 `uploadBase(req)`:多库给 `/tw/<id>/`(`proxyBaseFor(mode, id)`,id 取请求上那个在册的 `?wiki=`,否则默认库),**单库保持原来的裸路径**——老安装一个字节都不变。③ 执行过程中被 `tsc` 与守门各抓到一次:批量删 import 的正则把仍在用的 `timestampTitle`/`resolveTags`/`conflictTokens` 也删了(`TS2304` 点名救回),以及 `,\s+\}` → ` }` 的压缩把 `twProxyAbsoluteBase,` 的尾逗号吃掉、让 `verify-tw-origin.mjs` 的「必须注入会话路由」断言变红。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(38 套)/ `verify:e2e` 全绿。
40
+ - **v0.30.9**(2026-09-29):**`routes.ts` 拆分(第 5 条裁定)—— 两步纯搬迁/工厂抽取,行为零变化**。① `routes-helpers.ts`(256 行):十来个**模块级纯 helper**(上传文件名净化 `sanitizeUploadName` + 危险扩展名/Windows 保留名、`redactRemoteUrl`/`redactLogLines` 脱敏、`MAX_TAGS_LIMIT`/`readLimit`/`readOptionalLimit` 夹取、`timestampTitle`、`resolveTags`/`conflictTokens` 解析)以及 `openInTwEditor` 一起搬走。**`routes.ts` 原样 re-export 了公开的三个**(`redactRemoteUrl`/`redactLogLines`/`openInTwEditor`)——`admin-secrets.ts` 从这里 import、`index.ts` 从这里再导出、selftest 与 `verify-audit-fixes.mjs` 直接用它们,**既有 import 路径一个都没改**。`NOTE_TYPE` 随 `openInTwEditor` 一起搬(它属于那套写策略),由新模块导出。② `routes-wechat.ts`(210 行):`guardWechat` + `/wechat/{ready,publish,publish/status}` 三条路由抽成 `createWechatRoutes(deps)`。它们原来直接住在 `registerRoutes()` 的闭包里,抽取时**量过并写明了真正用到的依赖面**(`Pick<RouteDeps,'getClient'|'wechatConfig'|'wechatReady'|'wechatRunner'|'wikiSummaries'>`)——`import type` 只在类型期存在,运行时无环;`ROUTE_PREFIX` 改从源头 `wiki.ts` 的 `PATH_PREFIX` 导入并起同名别名,函数体一字未改。③ **守门铺路的第三处**:`verify-wechat-publish.mjs` 原本按精确文件名读 `routes.ts` 断言三条路由的注册与「先判方法+同源、再判开关/token」,搬迁后假红——改成读 `readFamily('src/host/routes')`,断言回到「规则」而不是「文件路径」。**验证**:`typecheck` / `build` / `selftest` / `verify:static` / `verify:unit`(38 套)/ `verify:e2e` 全绿。
41
+ - **v0.30.8**(2026-09-29):**给 `routes.ts` 的拆分铺路:两处守门改成按模块族读**(行为零变化)。① `verify-render-sanitizer.mjs` 原本读「`routes.ts` + `routes-tw-proxy.ts`」这个**写死的两文件列表**,一旦把 `/render` 再搬进更细的模块就会假红(v0.28.8 已经踩过一次,当时的补法是往列表里加一个文件名——治标);现在读 `readFamily('src/host/routes')`,断言的是规则本身(宿主侧存在 `sanitizeTwFragment(` 调用),搬家不再需要改守门。② `verify-restart-drain.mjs` 原本遍历 `['routes.ts','admin.ts']` 两个**具体文件**并要求「每个文件里 restart 数 > 0」——把 `/sync` 或 `/restart` 的 handler 搬进新模块,`routes.ts` 的 restart 数会变成 0 而**静默丢掉覆盖**(甚至搬到别的文件后连「0 处 restart」那声错都不会响);现在遍历 `['src/host/routes','src/host/admin']` 两个**模块族**,规则(区域里不得有裸 restart、`drainUntilStop` 数量不少于 restart)对整片区域成立。**教训**:改守门脚本时改了调用处却忘了补 `import { readFamily }`,会以「函数未定义 → 测试抛错 → FAIL」的形式暴露(本轮正是这样被抓到的),所以守门改完必须**立刻单跑一次**。
42
+ - **v0.30.7**(2026-09-29):**大文件拆分(第 5 条裁定的继续)** —— 纯搬迁 + 工厂抽取,行为零变化。① `src/host/tools-notes.ts`(683 行)→ **14 行的 barrel**:七个工具工厂按职责搬进 `tools-notes-write.ts`(`get`/`put`/`batch_put`)· `tools-notes-structure.ts`(`rename`/`append`)· `tools-notes-lifecycle.ts`(`delete`/`trash`);barrel 保持原导出面,`tools.ts` 的导入与**注册顺序**(`full` 模式参数索引依赖它)一字未动,每个分片的 `import` 按「该片真的用到」自动裁剪(`typecheck` 校验)。**守门依赖按模块族生效**(`readFamily('src/host/tools-notes')` / `'src/host/routes'` 会把新文件一起读进来),所以拆分没有削弱任何断言——`verify:static` / `verify:unit`(38 套)/ `selftest` / `e2e` 全绿。
43
+ - **v0.30.6**(2026-09-29):**客户端 i18n 层落地**(作者:「客户端零 i18n 层,这个需要补齐」)。此前 `uiLanguage` 只驱动 TW 子进程,插件自己的界面(FAB / 侧边栏入口 / 输入框上方那一行 / 快速笔记卡片与编辑弹窗 / 工具卡片 / 会话「知识库」Tab / 设置页 / 全部 toast)**全是硬编码中文**,设置页里甚至写着「界面语言暂不支持」的自认。本次:① **唯一入口** `src/client/i18n.ts` 的 `t(key, vars?)`(`{name}` 插值,未知键回落默认语言再回落键本身——转换期间缺条目会**显形**而不是静默空串);② **目录按区域分片**成 `i18n-{chrome,note,card,settings,config,plugins,frame}.ts`,同一批表面的人只碰自己那一小份;③ **语言来源是同一个 `uiLanguage`**:host 在 `/status` 回 `lang`(`RouteDeps.langOf` ← `WikiInstance.uiLanguage()`),客户端 `fetchStatus()` 调 `setLang()` 并写 `localStorage` 缓存 ⇒ **刷新后首屏即为该语言**;④ **顺手修掉设置页作用域条的真问题**(v0.28.12 定过规则却漏了这一处):它把**库 id** 当名字显示、默认态写死「正在配置:默认知识库」——现在读同一份 `/admin/wikis` 名册取**显示名**(拿不到就中性兜底),界面上不再出现「默认库」;⑤ **8 处守门断言改用「目录里有该文案 + 代码里有 `t()` 调用点」的形状**(`verify-wiki-focus.mjs`),因为文案搬进目录后,再去匹配源码里的中文字面量就是假红——其中「切单库必须直呼默认库之名」改成断言目录里有 `{name}` 占位符 + 调用点传了 `name`。**守门**:新增 `scripts/verify-client-i18n.mjs`(进 `verify:unit`)——分片 zh/en 键集合一致、每个 `t('…')` 的键都存在、没有死键、**客户端源码里不得残留中文文案**(剥注释后扫描;`t()` 第一参数与 ALLOWLIST 里的协议串除外);未转换的文件列在 `PENDING_CONVERSION` 里,清单**自清洁**(转完了还留着 = 红),所以它只会缩短。本次实到:设置核心 4 个文件(`settings-page.ts` / `settings-page-wikis.ts` / `settings-icon-picker.ts` + 160 条键、含 42 个图标名)已转完,其余 20 个仍未转(清单在脚本里,逐文件转换即可)。
44
+ - **v0.30.5**(2026-09-29):**`tiddlywiki_git_sync` 改为同步「所有开启了 git 同步的库」**(作者裁定:「同步所有开启了git,并且有同步库的wiki库」)。此前它只作用于**会话作用域那一个库**(再往前是"作用在默认库、回执却标注会话的库",v0.29.0 只补了注释没补实现),于是多库安装**没法用 Agent 同步其余库**。现在:① **凡是有效 `git.remote` 非空的已登记库都在同步范围内**(`autoCommit` 关掉不算理由 —— 手动同步本来就是手动);② **按仓库去重**:多个库共用一个工作树时 pull/commit/push 各只跑一遍(跑 N 遍会互踩同一个 index),回执按仓库合并成一项并列出库名与**库 id**;③ **逐仓库独立成败**:一个仓库冲突/失败不再吞掉其余仓库的结果,总 `ok` 反映整体、总 `message` 点名是哪个库失败;④ 没配 `git.remote` 的库出现在 `skipped` 里("同步完成"不允许被读成"全同步了"),一个可同步的库都没有时 `ok:false`;⑤ 冲突回执给出**带 `wiki=<库 id>` 的解决指引**,`tiddlywiki_git_resolve` 新增可选 `wiki` 参数(多库各有独立仓库时**必须**传,否则会改错仓库);⑥ 排干(`flushPendingWrites`)仍只针对调用方刚写过的那个库(它是唯一够得着的队列、也是重启风险所在),失败照样在回执里显形。实现走 `ToolsDeps.gitTargets()`:由 `index-git.ts` 从**注册表**(不是运行态)枚举每库目录 + `repos.repoRootOf()` 缓存出的仓库根 + 每库有效 `git.remote`,所以**停止的库也同步**(同步是磁盘操作,不需要 TW 子进程)。**守门**:新增 `scripts/verify-git-multi-sync.mjs`(进 `verify:unit`,**行为级**、不 spawn TW):用假 deps 驱动注册出来的真工具,断言"按仓库去重/逐库标注/skipped/失败隔离/冲突带 wiki= 指引/排干失败显形/无目标 ok:false/`git_resolve wiki=` 选对仓库并拒绝未知 id" 九条。**验证**:`typecheck` / `build` / `verify:static` / `verify:unit`(含新守门)/ `verify:e2e` / `verify:large` / `selftest` 全绿。
45
+ - **v0.30.4**(2026-09-29):**清理 + 守门加固 + 作者对剪藏桥的姿态落地**(无行为变化)。① **作者裁定「剪藏桥不做认证」并写入代码与文档**:桥保持对任意页面开放 —— 「任意网页能给你的 wiki 建笔记」是**许可的**,不允许的只有**覆盖系统项**(`$:/…` 标题一律拒收,v0.30.0 起生效)。模块注释里那句「随机网页不知道 token,所以污染不了 wiki」是对安全模型的误述(token 默认空、且设计上不打算要求它),已改为如实陈述;`bridge.token` 降级为**可选的额外收紧**,同步更新了会被写进用户 wiki 的《本地剪藏桥》文档(`seed-clip-bridge.ts`)、`docs/usage.md` 与 `AGENTS.md` §2。② **守门加固:`readFamily()` 读空/缺 `<base>.ts` 现在直接抛错**。此前族文本会静默退化成空串 ⇒ **反向断言与 `assertNo` 全部自动成立**(本仓库最危险的一种假绿)。③ **`verify-prompt.mjs` 的多库预算不再是孤立魔数**:手写的 `1800` 在 v0.29.0 把 slim 瘦到 1011 之后就再没跟上(等于一个永远成立的数假装在守门),改成 `SLIM_BUDGET + 200`。④ **清理**:删除被 `gen-wiki-icons.mjs` 取代的开发助手 `scripts/extract-dsh-icons.mjs`;`pad()` 的两份逐字相同实现合流到新的 `client/format.ts`;`tool-views.ts` 里与 `settings-page-runtime.ts` **同名但语义相反**的 `fetchJson` 更名 `fetchJsonOrNull`;删掉从未被读的 `RightbarTabBodyProps.sessionId`;`seeds.ts` 的 `SeedMeta.gate` 是**死字段**(`defineSeed` 只读第二个参数里的 gate,写在 meta 里会被静默忽略)——删掉后放错位置会变成编译错误。⑤ **一处「差点清错」值得记**:`wiki-scope-dock.ts` 的 `wrapRef` 看起来是没人读的调试锚点,实际是 `verify-wiki-focus.mjs` 明确断言的契约(「选择器的根元素必须挂 ref」——对齐按行测量,元素要有可被持有的身份)。删掉它守门当场变红,已恢复并加注释说明,免得下一个人再当死代码清掉。**验证**:`typecheck` / `build` / `verify:static` / `verify:unit`(29 套)全绿。
46
+ - **v0.30.3**(2026-09-29):**剪藏桥第三刀**(纯搬迁,行为零变化)——`clip-bridge.ts` 在 v0.30.1 拆出 SSRF 层之后仍有 574 行,里面混着「payload ↔ tiddler 翻译」与「loopback 服务器」两件事。现在 payload 层独立成 `clip-bridge-payload.ts`(189 行:标题去重规则 `resolveClipTitle`、三个 tiddler 构造器、mime/扩展名映射、以及那个带 `$:/` 系统命名空间拒绝的 payload 校验器),`clip-bridge.ts` **574 → 414 行**,只剩 loopback 服务器 + CORS + 公开面再导出。三个新文件与 base 之间:SSRF 层无反向依赖、payload 层只 `import type` base 的 `ClipPayload`/tw-api 的 `Tiddler`(类型侧闭环,运行期无环),所以搬迁零改写。**验证**:`typecheck` / `build` / `verify:unit`(含 `verify-clip-bridge` 从 `lib/` 取 12 个符号的断言)全绿。
47
+ - **v0.30.2**(2026-09-29):**拆掉仓库里最大的单个函数**(纯搬迁,行为零变化)——`createSessionRoutes()` 曾是一个 **432 行**的函数,里面混着三族互不相干的 handler。现在 `src/host/routes-session.ts` **632 → 274 行**,只剩「会话知识库作用域」三条(GET/POST 同路径的那组)+装配;新增 `routes-session-summary.ts`(103 行:`/session/summary` 与它那个"一次要读 41 份会话日志、探测 300 篇 tiddler"的 promise 缓存——缓存只有一个消费者,就该和它住在一起)与 `routes-session-agent.ts`(336 行:`/agent/{sessions,modes,send,create}` 四条 + 共享 token 守卫 `guardSendToAgent`,这个守卫只有这一族用)。装配收成两行工厂调用(`createSessionSummaryHandler(deps)` / `createAgentHandlers(deps)`):两个工厂拿到的都是同一个 `deps`,所以**函数体逐字未改**,搬迁零改写;两个新文件与 base 之间只有 `import type`(类型侧闭环,运行期无环)。**守门铺路先行**(前一提交):`scripts/verify-tw-origin.mjs` 里「数两处 `twUrlAbsolute`」原本**精确读** `routes-session.ts`,这次搬迁会让它假红——已改成按**模块族**读(`routes-session.ts` + `routes-session-*.ts`)并补一条「族里真的找到了 `createSessionRoutes`」的非空断言(防止基名写错时族读读空、断言静默全过);**规则没变、计数仍是 2**。这条顺序就是本仓库自己的教训:**大重构的第一笔提交是给守门铺路**。**验证**:`typecheck` / `build` / `selftest` / `smoke:client` / `verify:static` / `verify:unit` / `verify:e2e` / `verify:large` 全绿。
48
+
49
+ - **v0.30.1**(2026-09-29):**纯搬迁的拆分版(行为零变化)**——v0.30.0 的修复逐字不变,本版只把三个「一个文件混了几件事」的大文件按职责拆开,为审计待办里剩下的拆分打样(每个都先确认守门是**按模块族**读还是精确读单个文件,再决定搬迁边界)。① `src/client/styles.ts` **945 → 47 行**:900 行 CSS 正文移入 `styles-css.ts`,`styles.ts` 只剩 `STYLE_ID` + `injectStyles()`;7 处守门改按 `readFamily('src/client/styles')` 读,而"CSS 模板串没被正文里的反引号提前截断"那条改读 CSS 所在文件(它才是模板串的家)。② `src/host/clip-bridge.ts` **872 → 574 行**:SSRF 姿态(URL 校验 / DNS pinning / 逐跳重定向复检 / 限字节流式下载)移入 `clip-bridge-ssrf.ts`(315 行),四个尺寸上限移入 `clip-bridge-limits.ts`(17 行)——两者之间没有反向依赖,公开面在 `clip-bridge.ts` 原样再导出(`src/index.ts` 那条 12 个符号的 re-export 与守门从 `lib/` 取的名单逐字不变)。把"代表网页去公网取数据"的那一半单独成文件,也让这条风险更容易被审读。③ `src/host/seeds.ts` **824 → 658 行**:排干/重启原语(`waitForFileWrite` / `flushPendingWrites` / `drainThenStop` / `needsRestartAfterSeeds` + 探针常量)移入 `seeds-flush.ts`(198 行)——它们是关于 TW syncer 的(铁律 #1 的唯一实现),不是关于 seed 内容的;`scripts/verify-restart-drain.mjs` 按模块族读源码(`seeds.ts` + `seeds-*.ts`),对这次搬迁免疫(原语整段留在同一文件、其后不再跟以 `\n}` 收尾的函数,符合该守门的解析前提)。**验证**:`typecheck` / `build` / `selftest` / `smoke:client` / `verify:static` / `verify:unit` / `verify:e2e` / `verify:large` 全绿。
50
+
51
+ - **v0.30.0**(2026-09-29):**第二轮全面审计(五路只读审计:路由 / 数据层 / 工具与可选功能 / 客户端 / 守门与文档)之后的修复 + 清理版**。门禁当时全绿,问题同样全在门禁看不到的地方;本轮另查出**三条守门自身是空转的**(断言恒真)。① **`tiddlywiki_get` 漏了密钥命名空间守卫**(铁律 #6 的第四条路径):`/get`、`/tw`、`/api`、`/render` 都挡了 `$:/plugins/dsh-tiddlywiki/`,唯独 agent 工具直接 `wiki.get(title)` —— 模型可以原样读出 `$:/plugins/dsh-tiddlywiki/config`(`bridge.token` / `ui.sendToAgent.token` / `wechat.token` / 带 PAT 的 `git.remote`),即那些路由存在的理由被整条绕过。守卫抽成共享的 `host/proxy-guard.ts`(工具层不该为一条安全原语去 import 路由模块),`routes-tw-proxy.ts` 原样再导出保持既有调用面。② **剪藏桥可以写系统命名空间**:桥**故意**对任意网页开放(bookmarklet 跑在页面源里,加 Origin 检查会直接废掉功能)、`bridge.token` 默认空,而标题原样收下 → 恶意页面可 `PUT $:/plugins/dsh-tiddlywiki/config`(`ConfigStore` 随后读它、`wechat.command` 会被当 CLI 起、`git.remote` 会用于下次同步),或覆盖回收站索引等插件状态。现在 `parseClipPayload()` 拒绝 `$:/` 前缀(含守门用例);`bridge.token` 仍是可选的加固手段。③ **`bridge.wiki`(v0.28.8 的多库剪藏目标)在运行期恒被丢掉**:`WikiInstance.bridgeConfig()` 是手写对象字面量,重建时只带 4 个键,`wiki` 从未传给 `clipTarget()` —— 于是「剪藏写进哪个库」这个设置项在多库下**永远回落默认库且不打印回落日志**(守门只断言 `clipTarget` 源码,抓不到这条接线)。④ **`tiddlywiki_git_sync` 的排干结果是空守护**:`flushPendingWrites()` **从不 reject**(它 resolve `false`),而调用点写的是 `.catch(() => undefined)` —— 既是死代码,又把 `false`(超时/探测 PUT 失败)丢掉后照常重启,正是铁律 #1 要防的"重启杀掉未落盘写入"。策略仍与 `drainThenStop` 一致(排干失败也要能重启,否则卡死的 TW 无法自救),但结果现在**写进回执**(`drainFailed` + 一行醒目警告,让模型提示用户核对)。⑤ **`/admin/restart` 对"已登记但没在跑"的库假成功**:它没有兄弟路由 `/restart` 的 `refuseStoppedTarget`,于是 `?wiki=<停止的库>` 会走 `client: undefined` + `getWikiPath` 的默认库回落,然后回 `{ok:true, drained:true, status:undefined}` —— 设置页作用域写着那个库、却什么也没重启。现在 503 点名。⑥ **多库 + 微信公众号发布 = 会发错库的内容**:发布链路把**一个**基址(`--dsn`)交给 opencli adapter,adapter 再自行加 `/render`、`/get?title=`,所以库 id **只能走路径**、`?wiki=` 到不了那两次调用;多库下任务会拿 B 库的标题去渲染**默认库**的同名笔记并发出去(公开平台、且回执不说)。本轮**响亮拒绝**(400 + 说清原因与出路),单库安装逐字不变;彻底修法(`/w/<id>/` 作用域别名前缀)记在审计笔记里。⑦ **配置保存会把打码的 remote 写回库**:`/admin/config` 用**内存缓存**比对 `git.remote`,而 `ConfigStore.set` 合并的是**刚读到的** tiddler;两者不一致时(有人在 TW 编辑器里改过 remote)判据失效,`https://***@github.com/…` 被当成真 remote 落库 —— 判据改为**结构性**(字符串里带 `//***@` 就丢弃,不依赖任何缓存),并补了"缓存过期"回归用例。⑧ **会话作用域文件坏掉会清空所有会话**:`setSessionScope` 是"读-改-写",而坏 JSON 被当成"空状态" → 整份重写后**其它会话的库作用域一起没了**(铁律 #3 的同类,读路径可以软失败,写路径必须中止)。现在中止并保留原文件。⑨ **微信任务输出截断方向反了**:`capOutput` 保留**前** 64k,而判据(`-f json` 的 JSON 行、stderr 末行)都在**末尾** → 超长运行的判据退化成"未解析到结构化回执"= ok。改为保留尾部。⑩ 其余:`rename(updateRefs:false)` 不再谎报"未找到任何引用"(旧判据恒真)、FAB 重建时退订旧的焦点订阅(每次名册变化泄漏一个监听器 + 旧闭包)、快速笔记 `ensureTarget()` 不再把失败的 `/status` 记忆化(一次抖动曾把目标库永久钉在默认库)、草稿恢复后清空正文不再写出"空草稿"(v0.22.8 症状的另一条入口)、`/session/summary` 的并发缓存不再无界增长。**⑪ 三条守门空转(这是本轮最该记的部分)**:`verify-wechat-adapters` 的"必须清掉 class"写成 `/(坏正则)/.test(src) || /class/.test(src)` —— 右边匹配任意 `class` 字样(注释也算)⇒ 恒真;`verify-tool-views` 的旧拦截器反向断言 `^` 无 `m` 标志又作用在折行后的整份文本上 ⇒ 恒真;`verify-prompt` 的反向 needle 写成了全仓不存在的措辞 ⇒ 恒真。三处都换成**行为级/同源 needle**断言并补了回归(②的 `$:/` 拒绝、⑦的缓存过期、⑧的坏 JSON 中止、⑥的多库拒绝、`styles` 家族的 `readFamily` 读取)。**⑫ 拆大文件**:`styles.ts` 945 → **47 行**(900 行 CSS 正文移入 `styles-css.ts`,7 处守门改按 `readFamily('src/client/styles')` 读、模板串截断守门改读 CSS 所在文件)。**⑬ 死代码清理**:`WikiInstanceState`(零引用类型)、`DOCK_ALIGN_MIN_INPUT_WIDTH` 的多余导出(注释还写着"导出给守门断言"——已过期)、唯一无引用的 CSS 规则 `.dsh-tw-settings-icon-select`、`scripts/lib` 的 `familyFiles()`(零调用点)与 `matchRoute`/`CHROME_CANDIDATES` 的多余导出、`sanitize.ts` 里因更早的白名单判定而永不执行的 `action`/`formaction` 分支。**验证**:`typecheck` / `build`(client 708953 字节)/ `verify:static` / `verify:unit`(29 套)/ `verify:e2e` / `verify:large` 全绿。**未做完的拆分**(审计给了逐文件方案与守门约束,记在 [[dsh-tiddlywiki 第二轮全面审计(2026-09-29)]]):`routes.ts`(1387) / `routes-session.ts`(625,单函数 432 行) / `note-widget.ts`(986,`build()` 384 行) / `index.ts`(1021,`apply()` 630 行) / `clip-bridge.ts`(860) / `seeds.ts`(824) / `tool-views.ts`(940)。
52
+
53
+ - **v0.29.0**(2026-09-29):**一轮只读审计(门禁全绿、0 个 open issue,问题全在门禁看不到的地方)之后的修复版**——三个 P1 都属"静默作用到错误的知识库",两条守门还是**假绿**。① **快速笔记 native 路径从不解析目标库**(默认 `ui.quickNoteMode=native`):名册与目标库的解析整段写在 `build()` 里,而 native 路径**从不建卡片** —— 于是 `targetWiki` 恒为 `undefined`、`rosterMode` 恒为 `'single'`,在 B 库写作时点「快速笔记」,笔记落进**默认库**、编辑器也是默认库的,而那个本该拦住它的「写入」下拉永远不会出现。修法:解析抽成 `ensureTarget()`(build 之外,两个入口都调),native 路径**先解析、目标库没在跑就拒绝**、再写。守门 `verify-wiki-focus.mjs` 只做源码字符串匹配(匹配的还是 helper 里的实现)→ 新增**调用点/顺序**断言(已反向验证:删掉 `await ensureTarget()` 或漏传 `wikiQuery` 都稳定变红)。② **`?wiki=<已登记但没在跑>` 静默回落默认库**(写路由也是):`targetRuntimeFor()` 注释只论证过"未知 id 回落是为了老书签",却把"已登记但没运行"一起回落了 —— 设置页写着「正在配置:books」却把配置写进默认库、`/admin/info` 改的是默认库的 `tiddlywiki.info`、`/sync`/`/restart` 动的是默认库。现在**未知 id 仍回落**(老书签不能 404),**已登记但没在跑一律 503 并点名那个库**(与 `/tw/<id>/` 代理早已给出的 503 语义一致);判断只有一份实现(`stoppedWikiFromRequest()` / `stoppedWikiMessage()`),`admin-routes` 的 state/info/config/prompt/seeds 先查 `targetProblem`,routes 侧 status/note/edit/sync/restart/upload 同样。③ **启动自举有两处裸 `server.restart()`**(`pluginAdded` 与 `uiLanguage` 分支)绕过 `drainThenStop()` —— 而守门用 `indexOf` 只看**第一处**,数不到它们。现在自举(含 seed 分支与 `start()` 的卸载中止)全部走 `instance.restart()` / `drainStop()`,`verify-restart-drain.mjs` 改成**枚举全部调用点**:每一个 `server.restart()/stop()` 都必须落在 `drainThenStop` 的 stop 回调里(`dispose()` 是唯一例外)。④ **注入提示词去重瘦身(1918 → 1011 字符,−47%)**:删掉与工具 description 逐字重复的句子(写入/并发 4 条 → 1 条;工作区标记、先窄后宽、多词 AND 交还 `put`/`search` 的 schema;intro 里手写的工具类别清单也删了,那是唯一还会漂移的副本),预算 2100 → 1200。**安全网换成 schema 侧断言**:`verify-prompt.mjs` 新增"委派的规则必须在工具 description 里",删一处可以、**两处一起删立刻红**(`attach` 的同名拒绝原本只写在提示词里,已补进它的 description)。⑤ **其余 P2**:卡片正文缓存失效时补上 wikiId(删除/批量写入后最多 5 分钟还能读到旧正文)、FAB 名册在打开菜单前对账(新增/移除/改名/启停库后不再显示过期清单)、会话「知识库」Tab 订阅作用域变化(切库后不再挂 3 分钟旧库内容)、`data-dsh-tw-wiki` 表达"host 实际服务的那个库"(多库下无显式作用域时不再跟随 GUI 焦点 → v0.28.11 报障的后半)、`catalogPending` 与配置表单重建键带上库作用域(A 库的勾选/脏补丁不再应用到 B)、侧边栏高亮按**完整名册**解析(焦点库没在跑时不再高亮错行/点成切换)、右侧栏 tab 创建即应用可见性、剪藏桥写入串行化(连点两次不再互相覆盖)、`session-scope`/`ConfigStore.set` 的读失败不再被当成"没有"(前者会丢其它会话作用域、后者会把配置写成只剩本次改动——铁律 #3)。⑥ **文档**:`usage.md` 删掉已删除的"2 秒回落"(它在教一个修掉的回归)、seed 表补 `wechat-publish`(13 项)、路由表补 `/admin/wikis` `/session/wiki` `/wechat/*` `/tw/<id>/` 并修正只读路由的方法、项目结构补 v0.28.8 拆出的模块、"重启只有一条路"按新实现改写。
54
+
7
55
  - **v0.28.14**(2026-09-29):**修「点击切换知识库感觉会卡那么一下」**(作者报障,接在 v0.28.13 之后)。① **先量再改**(不然只是猜):`/status` 每次要 host 跑最多 5 个 git 进程(本机实测冷缓存 **300–400 ms**);而换库后的 TW 文档本身是 **9.9 MB / 29.9 MB / 28 MB**(三个库实测),并且 TW 自己回 `Cache-Control: no-store`、没有 `ETag` —— **既缓存不了也预取不了**。② **两段来源分开处理**:内核的焦点订阅原来只调 `doRefresh()`,而它**先等一次 `/status` 往返**才动 iframe —— "点下去"到"真的开始换"之间于是空了 0.3–0.4 秒,紧接着才是那份大文档的下载与解析。现在新增 `switchFrameNow()`:焦点一变就**同步**用上一次 `/status` 的 payload(`mode` + 代理基址 + 名册)把 iframe 指到新库,`doRefresh()` 退为**后台对账**(不再挡在点击与界面反应之间);拿不到可用 payload 时它什么都不做,行为与 v0.28.13 逐字一致。③ **侧边栏不再重复打 `/status`**:焦点变化原来会重跑一遍 `applyRoster()`(再打一次 `/status` + 重建所有行 + 重走放置逻辑),可名册根本没变、变的只是哪一行亮着;现在收成 `paintFocus()`,只重画高亮。④ **给可见说明**:换文档期间在 TW 界面底部挂一条 `正在载入知识库「个人」…`(`.dsh-tw-loading`),新文档 `load` 到达即收起,并留 **15s 兜底**(`load` 不来时不能把"正在载入"永久挂在界面上);`showError` / `showStarting` / `setVisible(false)` 三条路径也都会收掉它。提示条是 view 的**最后一个孩子**,刻意**不碰** `frame` 的显隐规则(那条"未载入 TW 地址就绝不显示 frame"是 v0.22.3 的血债)。⑤ **剩下还能不能更快**:不能 —— 剩下的时间全是 TW 自己的文档大小(10–30 MB、`no-store`),不是这套代码能缩短的;面板与右侧栏共用同一条提示。**守门**:`verify-frame-surface.mjs` 新增 2 条**行为级**单测(把 `/status` 打桩成**永不返回**后切库:`frame.src` 必须**已经**指到新库,且提示条可见、写明目标库名、`load` 后收起、15s 兜底收起);`verify-wiki-focus.mjs` 新增 1 条源码级(`switchFrameNow()` 必须早于 `doRefresh()`,订阅里不许 `await`;侧边栏焦点变化不许重跑名册 —— 反断言跑在**剥掉注释**的代码上;提示条必须**真有 CSS**,v0.28.9 的教训)。**已反向验证**四种注入(退回"只 doRefresh" / `load` 后不收提示 / 焦点重跑名册 / 提示条没有 CSS)都稳定变红,恢复即绿。单库安装行为逐字不变。
8
56
 
9
57
  - **v0.28.13**(2026-09-29):**修「左侧多 wiki 入口有时候要点两次才能切到对应的库」**(作者报障)。① **症状**:面板正开着「工作」时点侧边栏「个人」那一行,得点两次才切过去。② **根因**:入口行的点击**一律**走 `state.toggle()` —— `setFocusWiki()` 确实把焦点换过去了(内核也开始换库),但**同一击**里的 toggle 又把面板关掉;用户看到的只是"面板没了",于是再点一次。开着面板时点**另一个**库,用户要的是切换,不是关面板。③ **修法**:判定收成一个纯函数 `resolveEntryClick()`(面板关着 → `open`;开着且就是当前显示的那个库 → `close`;开着但是别的库 → `switch`),副作用收进 `applyEntryClick()`;`switch` 只切焦点、面板保持打开,换库仍由焦点订阅驱动内核重载(`tw-frame.ts`)。**两条顺序/同源要求**:(a) 判定必须在 `setFocusWiki()` **之前**求值 —— 反了的话刚切过去的那个库会被当成"面板此刻显示的就是它"→ 判成 `close`,等于没修;(b) 行**高亮**与点击判定改用同一个解析 `shownWiki()` —— 原来高亮那一路把 `defaultId` 传成了 `undefined`,焦点还没显式设过时会点亮**第一行**而不是默认库;高亮与点击语义不一致时,"点看起来没亮的那一行"就会变成关面板(同一个报障的第二个来源)。④ **守门**:`verify-wiki-focus.mjs` 新增两条 —— 判定表(7 个用例,含单库那一行)与**行为级**用例(真 `PanelState` + 真 `setFocusWiki`,直接断言本次报障的原始场景:"面板开着时点另一个库,面板必须仍开着且焦点已切过去"),另加源码级反向断言(入口行里不得再出现 `toggle()`、判定必须在切焦点之前求值)。**已反向验证**:注入旧行为(切换判成关闭 / 退回无条件 toggle / 把判定挪到切焦点之后)三种改法都稳定变红。**单库安装行为逐字不变**(那一行没有 `wikiId`,仍是纯粹的开关)。⑤ **顺带修好一个长期红着没人看的守门**:`scripts/smoke-client.mjs`(CI 里那条「client bundle module-loader 形状」检查)的 React 替身缺 `createContext`,而 `src/client/tool-views.ts` 自 v0.28.8 起在**模块作用域**调用 `React.createContext(...)` —— 这个脚本于是每次都抛 `c.createContext is not a function`(已在 HEAD 的 `lib/client.js` 上复现确认,与本次改动无关),等于**从那时起就在 CI 里空转**。补全替身(`createContext`/`useContext`/`useMemo`/`useCallback`/`useLayoutEffect`/`forwardRef`/`memo`)并在注释里写清「stub 漏一个 API 就是漏一次检查」。
@@ -10,7 +10,7 @@ npm run typecheck # tsc --noEmit
10
10
  npm run build # clean + host tsdown + client tsdown + wrap(wrap 会校验 id 与体积 <900KB)
11
11
  npm run selftest # headless:spawn TW → REST 读写 → git → 退出回收
12
12
  npm run smoke:client # client bundle 的 module-loader 形状冒烟
13
- npm run verify # = verify:static + verify:unit + verify:e2e(本地一键;CI 同款分档)
13
+ npm run verify # = verify:static + verify:unit + verify:e2e + verify:large(本地一键;CI 同款分档)
14
14
  npm run verify:large # 3000+ 条目大 wiki:检索耗时 / 二进制零出现 / 真跑 commit(约 1 分钟)
15
15
  node scripts/verify-send-to-agent-bundle.mjs # bundle 字段 + 源件逐字一致
16
16
  node scripts/verify-seed-send-to-agent.mjs # 全新 wiki 上的 seed E2E
@@ -18,9 +18,14 @@ node scripts/verify-clip-bridge.mjs # 剪藏桥 headless 验收(含 SSRF 守
18
18
  node scripts/verify-seeds-admin.mjs # /admin/seeds 状态与 run 的 E2E
19
19
  node scripts/verify-prompt.mjs # 注入提示词守门(slim 无参数清单 / full 与工具注册表逐项一致 / 治理约定不丢)
20
20
  node scripts/verify-wiki-switch.mjs # 运行时切换知识库 E2E(真起 TW:切换 / 回滚 / 指针文件 / 非法输入,v0.22.0)
21
+
22
+ # ── 手动(浏览器)守门:需要本机 Chrome/Edge + puppeteer-core,**不在任何链里**(CI 上只会打 SKIP,故不进 verify)──
23
+ node scripts/verify-theme-browser.mjs # DSH 主题 → TW palette 适配(临时 wiki + headless;含 racy boot 窗口的同步器守卫)
24
+ node scripts/verify-clip-bridge-browser.mjs # 剪藏桥 seed 文档里的**可拖拽书签锚点**是否挺过 TW 渲染(href 未被改写)
25
+ node scripts/verify-menubar-theme.mjs # menubar 跟随激活调色板;⚠️ 需 `TW_URL` 指向**你自己**运行中的 wiki(**无默认值** —— 内置作者地址会改到别人的库)
21
26
  ```
22
27
 
23
- > 📦 从 **npm 包**安装的用户只有 `lib/` + `src/` + `docs/`(`scripts/` 不在发布包里,避免把构建脚本塞进依赖树)——想跑上面的验收脚本请用 git 仓库:`git clone https://github.com/bbqisbbq/dsh-tiddlywiki && npm install`。
28
+ > 📦 从 **npm 包**安装的用户拿到的是 `lib/` + `src/` + `docs/` + `skills/` + `tools/` + `cordis.patch.yml` + README/LICENSE(`scripts/` 不在发布包里,避免把构建脚本塞进依赖树)——想跑上面的验收脚本请用 git 仓库:`git clone https://github.com/bbqisbbq/dsh-tiddlywiki && npm install`。
24
29
 
25
30
  **改 bundle/seed 的再生成流水线**(不要手改 `seed-*.ts` 里的生成常量;bundle 版本号只在 `scripts/bundle/versions.mjs` 定义一处):
26
31
 
@@ -52,56 +57,79 @@ node scripts/gen-seed-ui-styles.mjs '<wiki>/tiddlers/<样式.css>' … src/host/
52
57
 
53
58
  | 路由 | 方法 | 用途 |
54
59
  |---|---|---|
55
- | `/dsh-tiddlywiki/status` | GET | 面板健康(service/url/git/tag/ui) |
56
- | `/dsh-tiddlywiki/note` `/edit` `/tags` `/recent` `/get` `/search` | POST/GET | 快速笔记、打开编辑器、tag/最近/单个/搜索(列表路由都支持 `limit`;`/tags` 另支持 `sort=alpha\|count` 与 `total`/`truncated`,v0.19.4) |
60
+ | `/dsh-tiddlywiki/status` | GET | 面板健康(service/url/git/tag/ui + 知识库名册 `mode`/`defaultId`/`wikis[]`) |
61
+ | `/dsh-tiddlywiki/note` `/edit` | POST | 快速笔记、打开编辑器 |
62
+ | `/dsh-tiddlywiki/tags` `/recent` `/get` `/search` | **GET** | tag / 最近 / 单个 / 搜索(只读:`rejectNonRead`,POST 会 405。列表路由都支持 `limit`;`/tags` 另支持 `sort=alpha\|count` 与 `total`/`truncated`,v0.19.4) |
57
63
  | `/dsh-tiddlywiki/render` | POST | TW 片段渲染(host 净化后返回,回复流卡片/会话汇总用,v0.19.1) |
58
64
  | `/dsh-tiddlywiki/sync` `/upload` `/restart` | POST | 一键同步、文件上传、重启 TW |
59
65
  | `/dsh-tiddlywiki/session/summary` | POST | 会话「知识库」Tab 汇总 |
66
+ | `/dsh-tiddlywiki/session/wiki` | GET/POST | 会话级知识库作用域:读 = 当前选择 + 解析结果(`scope`/`resolved`/`reason`/`mode`,v0.29.0 起带 `mode`);写 = 选择或清空(v0.28.0) |
60
67
  | `/dsh-tiddlywiki/agent/sessions` `/modes` `/send` `/create` | GET/POST | TW「发送给 Agent」:会话/模式/发送/新建 |
68
+ | `/dsh-tiddlywiki/wechat/ready` `/publish` `/publish/status` | GET/POST | 公众号发布(可选功能,`wechat.enabled` 关闭时一律 403,v0.23.3) |
61
69
  | `/dsh-tiddlywiki/api/*` | any | 透传 TW 服务(JSON) |
62
- | `/dsh-tiddlywiki/tw/*` | any | 同源 TW 代理(远程访问核心) |
70
+ | `/dsh-tiddlywiki/tw/*` | any | 同源 TW 代理(远程访问核心);`/tw/<id>/…` 按库定向,裸 `/tw/` = 默认库别名(v0.28.0) |
71
+ | `/dsh-tiddlywiki/admin/state` `/info` `/config` `/restart` | GET/POST | 设置页:状态快照 / 插件·主题·语言 / 配置读写 / 重启(v0.28.0 起全部按 `?wiki=` 定向) |
63
72
  | `/dsh-tiddlywiki/admin/seeds` `/run` `/remove` | GET/POST | seed 状态(v0.22.0 起含内容哈希的「有更新 / 本地已修改」)/ 运行 / 反初始化 |
64
73
  | `/dsh-tiddlywiki/admin/prompt` | GET/POST | 当前注入提示词全文(GET = 已保存的有效配置,v0.21.0);POST 带 `{enabled,mode,extra,override}` 则按**草稿**渲染、写入零副作用,供设置页预览未保存的表单值(v0.22.7) |
74
+ | `/dsh-tiddlywiki/admin/wikis` | GET/POST | 多库清单读写:`add`/`update`/`remove`/`set-default`/`set-mode` + 运行态 `start`/`stop`(v0.28.0) |
65
75
  | `/dsh-tiddlywiki/admin/wiki/location` | GET | 当前知识库位置 + 来源(指针/配置/默认)+ 指针文件路径 + 同目录候选 wiki(v0.22.0) |
66
76
  | `/dsh-tiddlywiki/admin/wiki/switch` `/reset` | POST | 运行时切换知识库 / 恢复为配置默认(失败自动回滚并报告,v0.22.0) |
67
77
 
78
+ > ⚠️ **`?wiki=<id>` 的三种语义**(v0.29.0 定稿):命名的库在跑 → 就是它;**已登记但没在跑 → 一律 503 并点名那个库**(绝不静默落到默认库:设置页、快速笔记、`/admin/info` 都曾因此改错库);**未知 id → 回落默认库**(老书签不能 404)。三种判断只有一份实现:`host/wiki-farm.ts` 的 `targetRuntimeFor()` / `stoppedWikiFromRequest()`。
79
+
68
80
  ### 项目结构
69
81
 
70
82
  ```
71
83
  src/
72
- ├── index.ts # host 入口:装配 WikiServer/路由/工具/提示词/自动 commit
84
+ ├── index.ts # host 入口:装配农场(多库)/路由/工具/提示词/自动 commit
85
+ ├── index-wikis.ts # 知识库视图与切换(v0.28.8 拆出)
86
+ ├── index-git.ts # git 层装配(按仓库去重的提交者)
87
+ ├── index-prompt.ts # 提示词 section 的注册与按会话求值(v0.28.8 拆出)
88
+ ├── index-clip.ts # 剪藏桥装配
73
89
  ├── sdk.ts # 自包含 defineTool + dshHomePath(零 @deepseek-ai 运行时依赖)
74
90
  ├── host/
75
91
  │ ├── wiki.ts # WikiServer:spawn/kill/自愈/端口探测/就绪轮询(策略见 ready-policy.ts)+ 迟到就绪复探;TW_PROXY_PATH 同源代理
92
+ │ ├── wiki-instance.ts / wiki-farm.ts / wiki-registry.ts / wiki-switch.ts / wiki-location.ts
93
+ │ │ # 多库(v0.28.0):一个库的运行时 / 谁该在跑与会话作用域 / 清单与动作规则(纯函数)/ 单库切换 / 位置解析
76
94
  │ ├── ready-policy.ts # TW 启动就绪策略(v0.22.5):软窗口 60s(可配)/ 硬上限 3× / awaitReady 纯策略(注入时钟,可单测)
77
95
  │ ├── tw-api.ts # TiddlyWeb REST 客户端
78
96
  │ ├── git.ts # git init/commit/pull/push/sync/status + AutoCommitter
79
- │ ├── routes.ts # 全部 DSH 路由 + agent-send/create/modes + session/summary
80
- │ ├── http.ts # 共用 HTTP 助手(readBody/json)
81
- │ ├── clip-bridge.ts # 本地剪藏桥(v0.16.24):127.0.0.1 监听 + POST /clip 写 wiki(Host 校验/token/CORS preflight)
97
+ │ ├── repo-committers.ts # 按**仓库**去重的自动提交者(多库可共用仓库,v0.28.0)
98
+ │ ├── routes.ts # 路由装配(barrel)+ 非代理路由;实现分在下面三个同族文件里
99
+ │ ├── routes-session.ts / routes-tw-proxy.ts
100
+ │ │ # 会话/agent-send/session-summary(前者)/ `/tw` 与 `/api` 代理(后者)
101
+ │ ├── http.ts # 共用 HTTP 助手(readBody/json/guardHandler/同源守卫)
102
+ │ ├── clip-bridge.ts # 本地剪藏桥(v0.16.24):127.0.0.1 监听 + POST /clip 写 wiki(Host 校验/token/CORS preflight;v0.29.0 起写入串行化)
82
103
  │ ├── session-summary.ts # 会话「知识库」Tab 后端
83
- │ ├── admin.ts # 设置页后台:tiddlywiki.info 读写 + /admin/*
104
+ │ ├── session-scope.ts # 会话级作用域持久化($DSH_HOME/dsh-tiddlywiki/sessions.json)
105
+ │ ├── admin.ts # 设置页后台装配(barrel);实现分 admin-{catalog,secrets,routes}.ts
84
106
  │ ├── config.ts # ConfigStore:cordis config 基底 + 配置 tiddler 覆盖层
85
- │ ├── seeds.ts # 统一 seed 注册表(10 项,三层:核心/起步/可选)
86
- │ ├── prompt.ts # 系统提示词(v0.21.0):slim/full 两种形态 + extra/override,full 的目录由工具注册表实时生成;v0.22.7 起草稿预览(normalizePromptPreview/describePrompt,与保存路径同一份拼装)
107
+ │ ├── seeds.ts # 统一 seed 注册表(13 项,三层:核心/起步/可选)
108
+ │ ├── prompt.ts # 系统提示词(v0.21.0):slim/full 两种形态 + extra/override,full 的目录由工具注册表实时生成;v0.22.7 起草稿预览;v0.29.0 slim 去重到 ~1.0KB
87
109
  │ ├── seed-*.ts # 各 seed 实现(bundle/首页/ui-styles 常量脚本生成;starter-docs/menubar/clip-bridge 手工维护)
88
- │ └── tools.ts # 15 个 tiddlywiki_* 工具(列表式注册)
110
+ │ └── tools.ts # 15 个 tiddlywiki_* 工具的**装配与注册顺序**;实现分 tools-{support,read,notes,attach,git}.ts(v0.28.8)
89
111
  └── client/ # 浏览器半部
90
112
  ├── index.ts # client 入口(inject ['slots'],纯 DOM,永不 throw)
91
113
  ├── endpoints.ts # 客户端同源端点常量 + describeSyncResult(同步回执唯一实现,v0.22.8)
92
114
  ├── status-cache.ts # 共享 /status 读取器(2s TTL + 在途合并)
93
115
  ├── ui-config.ts # 共享 ui.* 投影 + invalidate/subscribeUiConfig(v0.22.8 扩到全部 FAB 开关)
94
116
  ├── render-fetch.ts # **唯一**的 POST /render 调用(v0.22.8)
95
- ├── knowledge-fab.ts / quick-note-dock.ts / note-widget.ts / editor-popup.ts
96
- │ # 知识库 FAB / 快捷按钮 / 快速笔记 / 原生编辑弹窗
117
+ ├── wiki-focus.ts / wiki-scope.ts / wiki-scope-dock.ts / scope-seat.ts / sidebar-entry.ts
118
+ │ # 焦点库记忆 / 会话作用域读取(含 v0.29.0 的订阅)/ 选择器 / 空白会话座位 / 侧边栏入口
119
+ ├── knowledge-fab.ts / quick-note-dock.ts / note-widget{,-draft,-tags,-upload}.ts / editor-popup.ts
120
+ │ # 知识库 FAB / 快捷按钮 / 快速笔记(v0.28.8 拆分)/ 原生编辑弹窗
97
121
  ├── markdown-editor.ts # CodeMirror 6 Markdown 编辑器
98
122
  ├── session-summary.ts # 会话「知识库」Tab(conversation.view 槽位)
99
123
  ├── tool-views.ts # 回复流工具卡片(tool.call.toolview)
100
- ├── theme-sync.ts / sidebar-entry.ts / sync-button.ts
101
- │ # 主题同步 / 侧边栏入口 / 同步按钮
124
+ ├── theme-sync.ts / sync-button.ts / settings-page{,-runtime,-wikis,-config,-catalog}.ts
125
+ │ # 主题同步 / 同步按钮 / 设置页(v0.28.8 拆成装配 + 4 个 section)
102
126
  ├── tw-frame.ts # **TW frame 内核(v0.22.4 起唯一实现)**:createTwFrameSurface(skin) 管 lazy-load / status 轮询 /
103
127
  │ # 错误与启动态 / 主题同步 / FAB 重载 / hash 导航 / dispose;中央面板与右侧栏共用同一份
104
- └── panel.ts / rightbar-tab.ts / settings-page.ts / state.ts / styles.ts / toast.ts
128
+ └── panel.ts / rightbar-tab.ts / state.ts / styles.ts / toast.ts
129
+
130
+ > ⚠️ v0.28.8 起大文件按**模块族**拆分:`<base>.ts` 一律保留为装配/barrel,实现放 `<base>-xxx.ts`。
131
+ > 守门脚本用 `scripts/lib/source-family.mjs` 的 `readFamily()` 把一族拼成一份文本 ——
132
+ > **新增拆分文件必须用 `<base>-xxx.ts` 命名**,否则守门读不到、断言会静默失效。
105
133
 
106
134
  > ⚠️ v0.22.8 起 `endpoints.ts` 的 `describeSyncResult()`、`render-fetch.ts`、`ui-config.ts` 各是**唯一实现**:
107
135
  > 同步回执、渲染调用、`ui.*` 投影都不要再在客户端面里各写一份(此前已因此漂移)。
package/docs/features.md CHANGED
@@ -16,12 +16,13 @@
16
16
  | 📍 **知识库位置可切换** | 设置页「知识库位置」可把插件切换到**任意本地文件夹**(不用改 cordis 配置、不用重装):就地停/起 TW 子进程,目标目录没有 `tiddlywiki.info` 时自动 `--init server` 建一个全新知识库;选择记在 `$DSH_HOME/dsh-tiddlywiki/location.json`——一个**在 wiki 之外**的指针文件(所以切到新 wiki 后仍记得「我用的是哪个」),「恢复为配置默认」一键清除;**切换失败自动回滚**到原知识库并如实告知(v0.22.0)。⚠️ 它一次只有一个库在线;要**多库同时在线**见上面的「多知识库」(v0.28.0) |
17
17
  | 🔄 **seed 更新检测** | seed 标记记录**内容哈希**:内置内容在本 wiki 预置之后更新过 → 设置页显示「⬆ 有更新」;被你改过 → 显示「✏️ 本地已修改」,且「重新初始化」前**二次确认**;两者都基于哈希判定,绝不猜(旧格式标记会明确显示「尚未启用更新检测」,不会误报为「你改过」,v0.22.0) |
18
18
  | 🎨 **自定义样式** | 「自定义样式」seed:编辑器美化 / 窄屏侧栏隐藏 / menubar 加高 / 批注弹窗等 5 张通用样式表,新 wiki 也能一键初始化(可选,v0.16.22) |
19
- | 📝 **可配置的注入提示词** | 插件注入每个会话的「TiddlyWiki 持久知识库」提示词可在设置页配置:**默认精简版**(v0.24.0 起 ~1.9KB,只保留工具 schema 表达不了的约定——同步纪律 / 工作区标记 / 先窄后宽检索 / 时效标注 / 链接格式),可选**完整版**(额外附一份**由工具注册表实时生成**的参数索引,不会再过期);`extra` 追加自定义规范、`override` 整段接管、可整体停用;**保存后无需重启 dsh web**(section 即时重注册,当前会话下一步即生效),设置页可**按表单当前值预览**即将注入的全文(未保存的形态切换也立刻可见,v0.21.0 / v0.22.7) |
20
- | 🤖 **Agent 工具** | 15 个 `tiddlywiki_*` 工具:检索(**相关度排序 + 命中处片段 + 字段过滤**)、读写、**增量追加**、批量、重命名、**软删除/回收站**、**反向链接**、**附件入库**、**知识库体检**、git 同步与冲突解决(v0.19.0;检索/最近仍在**服务端**排除二进制附件,大 wiki 上从 515MB/17s 降到 ~0.4s) |
19
+ | 📝 **可配置的注入提示词** | 插件注入每个会话的「TiddlyWiki 持久知识库」提示词可在设置页配置:**默认精简版**(v0.29.0 起 ~1.0KB,只保留工具 schema **表达不了**的约定——同步时机 / 写入被拒后重读而不用 `force` / 时效标注 / 淘汰权归人 / `human-edited` / 链接格式 / todo 约定),可选**完整版**(额外附一份**由工具注册表实时生成**的参数索引,不会再过期);`extra` 追加自定义规范、`override` 整段接管、可整体停用;**保存后无需重启 dsh web**(section 即时重注册,当前会话下一步即生效),设置页可**按表单当前值预览**即将注入的全文(未保存的形态切换也立刻可见,v0.21.0 / v0.22.7)。**v0.29.0 去重**:与工具 description 逐字重复的句子(工作区标记 / 先窄后宽 / 多词 AND / tags·type 保留规则)从提示词里删掉、**只留 schema 一份**,`verify-prompt.mjs` 改为在 schema 侧逐条断言「从提示词删 ≠ 丢了」 |
20
+ | 🌐 **中英双语界面**(v0.30.6) | 插件自己的界面(FAB / 侧边栏 / 快速笔记 / 工具卡片 / 设置页 / toast)与 TW 子进程**共用同一个 `uiLanguage` 设置**;文案按区域分片成 `i18n-*.ts` 的 `zh`/`en` 目录,守门保证两侧键集合一致、且**客户端代码里不再有硬编码中文文案** |
21
+ | 🤖 **Agent 工具** | 15 个 `tiddlywiki_*` 工具:检索(**相关度排序 + 命中处片段 + 字段过滤**)、读写、**增量追加**、批量、重命名、**软删除/回收站**、**反向链接**、**附件入库**、**知识库体检**、git 同步与冲突解决(v0.30.5 起 `git_sync` **一次同步所有配了 `git.remote` 的库**、按仓库去重、逐仓库独立成败;v0.19.0;检索/最近仍在**服务端**排除二进制附件,大 wiki 上从 515MB/17s 降到 ~0.4s) |
21
22
  | 🛡️ **不会被覆盖的写入** | 所有写入路径(agent 工具 **与** 快速笔记/编辑器路由)都**先读后写**:不传 tags 就保留原有标签、自定义字段与**内容类型**(`text/css`/wikitext 等不会被重置成 Markdown,v0.20.1);`tiddlywiki_put(..., expectedModified/expectedRevision)` 与 `tiddlywiki_delete(..., expectedModified/expectedRevision)` 乐观并发——读取后若有人(在 TW 编辑器里)改过,写入/删除被拒绝(HTTP 409)而不是静默覆盖或丢进回收站;**给了哪个令牌就必须匹配哪个**(v0.23.5:两个令牌是 AND 不是 OR——`revision` 是 TW 的内存计数器、重启会复位,旧逻辑会因此静默放行跨重启的覆盖);`tiddlywiki_attach` 的同名标题**默认拒绝**(要覆盖必须 `force: true`);`tiddlywiki_delete` 默认**软删除进回收站**,`tiddlywiki_trash` 可恢复(v0.19.0 / v0.19.1 / v0.19.5 / v0.23.5)。**v0.25.0**:`tiddlywiki_append` / `tiddlywiki_rename` / `tiddlywiki_batch_put`(逐条目)也支持并发令牌——它们同样是「读全文 → 整篇回写」,此前**没有令牌可传**,而注入的提示词恰恰让模型回传 `expectedModified`;`tags: []` 现在表示**清空全部标签**(此前被当成「未传」而静默保留,模型按 lint 的 junk-tags 建议去清标签时什么都不会发生);`tiddlywiki_rename` 更新引用时若某个引用者在遍历期间被改动过,**跳过它**并在回执里报出数量,而不是覆盖它 |
22
23
  | 🏷 **工作区感知的读写** | **新建**笔记自动带 `ws/<项目名>` 标签 + `workspace` 字段(项目名取自会话工作目录,`note.workspaceMark` 可关);`tiddlywiki_search` 因此默认**先在本项目里找、区内 0 条才自动扩大到全库**并在回执里写明实际范围,`query` 按空白切词、**全部词命中**才算(AND)。工作区只是**附加**标记,绝不改写笔记原有的标签/字段,也**只在新建时**打(v0.24.0) |
23
24
  | 🕰 **时效性内容只提示不删** | 阶段性笔记(版本记录 / 部署步骤 / 排期 / 临时方案)可声明 `valid-until`(硬过期)或 `review-after`(该复查),被取代时写 `superseded-by` + `superseded` 标签并**保留旧笔记**;`tiddlywiki_lint` 的 `stale` 检查**只报告**(含「版本号或 done 标签 + 长期未改动」的**候选**,明确标注非判定)——**淘汰永远由人决定**,插件不会自动删除或归档任何笔记(v0.24.0) |
24
- | 🚰 **重启不丢写入** | 停/重启 TW 只有一条路:`drainThenStop()`——**排干 syncer 队列之后才停子进程**,`/restart`、`/admin/restart`、插件/主题变更后的重启、知识库切换、`/sync`、seed 全部经它(此前其中三条直接杀进程,在一秒内点「重启 TW」会静默丢掉刚写的笔记);排干不完整会在回包里如实报 `drained:false`,而不是假装没事(v0.24.1) |
25
+ | 🚰 **重启不丢写入** | 停/重启 TW 只有一条路:`drainThenStop()`——**排干 syncer 队列之后才停子进程**,`/restart`、`/admin/restart`、插件/主题变更后的重启、知识库切换、`/sync`、seed、**以及启动自举路径**(v0.29.0:`pluginAdded` 与 `uiLanguage` 两处曾是裸 `server.restart()`,现在全部走 `instance.restart()`)都经它;排干不完整会在回包里如实报 `drained:false`,而不是假装没事(v0.24.1)。唯一的例外是 `dispose()`(卸载路径按设计不排干,且它只从会先排干的 `stopEntry` 到达)|
25
26
  | 🕒 **时间戳不再丢** | 插件写入的每条笔记都会带上 TW 的 `created`/`modified`(17 位紧凑 UTC,与 TW 编辑器逐字节一致):新建 = 两者都取当前时刻,覆盖 = **保留原 `created`、刷新 `modified`**。此前这两个字段被当成「TW 服务端会补」而丢弃,而服务端**从不补**——缺 `modified` 的条目会被 TW 的 `sortTiddlers` 当空串,在 `+[!sort[modified]]` 页面上**直接沉到最后一名**(表现为「新日记没被收录」)。`TiddlyWebClient.put()` 还有一道兜底,任何写路径都不会写出无时间戳的条目(v0.22.5) |
26
27
  | 🧼 **渲染片段净化** | 回复流卡片与会话汇总注入的 TW 片段先经 **host 白名单净化**(丢 `iframe`/`script`/`svg`/`on*`/`javascript:`/`data:text/html` 等)——TW 自己的解析器只剥 `on*`,`<iframe src="javascript:…">` 会原样通过并在 DSH 页面里执行(v0.19.1 修复的存储型 XSS) |
27
28
  | 🔒 **写路由方法校验** | 每个写路由只接受自己的 HTTP 方法:跨站 `GET /sync`、`GET /restart`、`GET /upload` 一律 405 且无副作用(v0.19.0 修复了「任意网页一张 `<img>` 即可触发 pull/commit/push」的 CSRF 面) |
@@ -60,13 +60,14 @@ interface SeedDef {
60
60
  | `clip-bridge` | `seed-clip-bridge.ts` → `seedClipBridge` / `unseedClipBridge` | 「本地剪藏桥 + 书签小工具」使用说明(Markdown 文档,带书签代码 / 启用步骤 / 安全说明,tag `dsh-docs`)——真功能在 `clip-bridge.ts` 运行时代码里 | `$:/plugins/dsh-tiddlywiki/seed-clip-bridge` | 可选 |
61
61
  | `publish-spec` | `seed-publish-spec.ts` → `seedPublishSpec` / `unseedPublishSpec` | 「发布元数据规范」(Markdown,tag `dsh-docs`)——pub-state / pub-* 字段与 no-publish 标签约定,供微信发布流程判断「发过没有 / 能不能发」 | `$:/dsh-tiddlywiki/publish-spec-seeded` | **起步(gated)** |
62
62
  | `wechat-setup` | `seed-wechat-docs.ts` → `seedWechatDocs` / `unseedWechatDocs` | 「微信公众号发布指南」(Markdown,tag `dsh-docs`)——**逐字节等于 `docs/wechat-publish-setup.md`**(由 `scripts/gen-seed-wechat-docs.mjs` 生成,勿手改常量):opencli / 扩展 / 登录 / adapter 安装与换机还原、三个坑、排错表 | `$:/plugins/dsh-tiddlywiki/seed-wechat-docs` | **起步(gated)** |
63
+ | `wechat-publish` | `seed-wechat-publish.ts` → `seedWechatPublish` / `unseedWechatPublish` | `$:/plugins/dsh/wechat-publish` 工具栏按钮 bundle(`application/json`)——TW 笔记工具栏的「发布到公众号」按钮 | `$:/plugins/dsh-tiddlywiki/seed-wechat-publish` | **起步(gated)** |
63
64
  | `tw-web-host` | `seeds.ts` 内联 | `$:/config/tiddlyweb/host` → `/dsh-tiddlywiki/tw/` | 无 marker(ensure 型,见 §4) | **核心** |
64
65
 
65
66
  > ℹ️ `home-index` 的 seed 版首页是**通用版**:生成脚本**默认**剥离作者 wiki 里的个人元素(主题页 tabs、书籍书架入口等,仅 `--keep-private` 才原样嵌入),并内置「📚 插件文档」tabs 栏(`[tag[dsh-docs]!is[system]]`,默认展开插件说明)。作者自己的 wiki 首页不受影响(seed 是 ONE-SHOT,不会覆盖)。
66
67
  >
67
68
  > ℹ️ **v0.22.0 起 marker 记内容哈希**:marker tiddler(`$:/plugins/dsh-tiddlywiki/seed-*`)的正文从一行 `seeded-once` 升级为 JSON `{ version, hashes: { <标题>: <sha256 前 16 位> }, at }`——哈希记录的是**我们写下的内置正文**,据此可区分「内置内容更新了」与「用户自己改过」(见 §3.1)。旧 marker 仍可读,按文本比对,并在下一次重新初始化时升级。
68
69
  >
69
- > ℹ️ **v0.23.0 起 gated seed**:`publish-spec` / `wechat-setup`(v0.23.1)虽是起步层(`startup: true`),但带 `gate: (ctx) => ctx.wechat === true`——**只在设置页开启「微信公众号发布」(`wechat.enabled`)时**才参与启动写入;不开该功能的用户 wiki 里不会出现发布相关文档(提示词里也没有指针,死链风险只存在于开启侧)。`gate` 只作用于启动路径 `runAllSeeds`;设置页手动「初始化」走 `runSeedById` 不受它约束(显式请求)。`checkAllSeeds` 照常报告两项「缺失」,属预期。
70
+ > ℹ️ **v0.23.0 起 gated seed**:`publish-spec` / `wechat-setup`(v0.23.1)/ `wechat-publish`(v0.23.3)虽是起步层(`startup: true`),但带 `gate: (ctx) => ctx.wechat === true`——**只在设置页开启「微信公众号发布」(`wechat.enabled`)时**才参与启动写入;不开该功能的用户 wiki 里不会出现发布相关文档(提示词里也没有指针,死链风险只存在于开启侧)。`gate` 只作用于启动路径 `runAllSeeds`;设置页手动「初始化」走 `runSeedById` 不受它约束(显式请求)。`checkAllSeeds` 照常报告三项「缺失」,属预期。
70
71
  >
71
72
  > ℹ️ **v0.22.0 起 `doc-note` 正文是生成的**:工具清单来自 `tiddlywikiToolSummary()`(`docNoteText(tools)`),不再手抄「N 个 agent 工具」。无注册表的 headless 调用会退化成一句指针,绝不写出过期数量。`wechat-setup` 的正文同理由 `docs/wechat-publish-setup.md` 生成(`scripts/gen-seed-wechat-docs.mjs`),`scripts/verify-wechat-docs-seed.mjs` 守逐字节一致。
72
73
 
@@ -289,8 +290,8 @@ npm run typecheck && npm run build && npm run selftest
289
290
  ## 8. 验证
290
291
 
291
292
  - `npm run typecheck` / `npm run build`:编译与打包;
292
- - `npm run selftest`:5d 段覆盖注册表清单(10 项,含 `core` / `removable` / `startup`)、**启动只 seed 核心 + 起步项(5 项)且不碰可选项**、手动全跑只写缺失、单跑幂等、force 重写、unknown id、tw-web-host 三分支(custom 保留 / force 写回 / legacy 修复)、**反初始化**(单移除 / 核心拒绝 / unknown / 全部移除保留核心)——另含 starter-docs 安全跳过、ui-styles 仅功能 tag、seed 版首页剥离私有人口 + 文档栏等断言;
293
- - `node scripts/verify-seeds-admin.mjs`:全新 wiki + 真实 HTTP,端到端验证 `GET /admin/seeds` 状态流转(10 项全缺失 → 启动后核心+起步就绪、可选项仍缺失)与 `POST /admin/seeds/run`(force 单跑恢复被改坏的首页、非 force 不覆盖用户内容、force-all 恢复代理基址、unknown id 400)+ `POST /admin/seeds/remove`(移除非核心、核心 400、remove-all 仅剩核心)。
293
+ - `npm run selftest`:5d 段覆盖注册表清单(**13 项**,含 `core` / `removable` / `startup`)、**启动只 seed 核心 + 起步项(wechat 关时 5 项)且不碰可选项**、手动全跑只写缺失、单跑幂等、force 重写、unknown id、tw-web-host 三分支(custom 保留 / force 写回 / legacy 修复)、**反初始化**(单移除 / 核心拒绝 / unknown / 全部移除保留核心)——另含 starter-docs 安全跳过、ui-styles 仅功能 tag、seed 版首页剥离私有人口 + 文档栏等断言;
294
+ - `node scripts/verify-seeds-admin.mjs`:全新 wiki + 真实 HTTP,端到端验证 `GET /admin/seeds` 状态流转(**13 项全缺失** → 启动后核心+起步就绪、可选项仍缺失)与 `POST /admin/seeds/run`(force 单跑恢复被改坏的首页、非 force 不覆盖用户内容、force-all 恢复代理基址、unknown id 400)+ `POST /admin/seeds/remove`(移除非核心、核心 400、remove-all 仅剩核心)。
294
295
 
295
296
  ---
296
297
 
package/docs/usage.md CHANGED
@@ -17,15 +17,15 @@
17
17
  | `tiddlywiki_delete` | 删除(幂等)。默认**软删除**进 `$:/dsh-tiddlywiki/trash/`,`permanent=true` 才真删;支持 `expectedModified`/`expectedRevision`/`force` 乐观并发(读后被改动则拒绝,不把人类的新改动丢进回收站) |
18
18
  | `tiddlywiki_trash` | 回收站:`action=list\|restore\|empty`(索引读不到或损坏时显式报错,绝不把回收站当空的重建) |
19
19
  | `tiddlywiki_backlinks` | 反向链接:谁用 `[[标题]]`/`{{标题}}` 引用了它、谁把它当标签 |
20
- | `tiddlywiki_attach` | 把**本机文件或公网 http(s) 地址**存成二进制附件(图片/PDF/…),可嵌入某篇笔记;URL 走 SSRF 守卫;**同名 tiddler 已存在时默认拒绝**(避免静默覆盖笔记),确认覆盖要传 `force: true`(tags/自定义字段仍保留) |
20
+ | `tiddlywiki_attach` | 把**本机文件或公网 http(s) 地址**存成附件(图片/PDF/… 等二进制存 **`type` + base64 正文**;`text/markdown`·`text/plain`·`application/json` 等**文本**存 `type` + **纯文本正文**,这样 TW 才渲染得出来、也才搜得到 —— v0.30.22),可嵌入某篇笔记;URL 走 SSRF 守卫;**同名 tiddler 已存在时默认拒绝**(避免静默覆盖笔记),确认覆盖要传 `force: true`(tags/自定义字段仍保留) |
21
21
  | `tiddlywiki_lint` | 知识库体检(**只读**):垃圾标签 / 死链 / 空笔记 / 缺内容类型的类 Markdown 笔记 / **时效性内容**(`valid-until`·`review-after` 过期,以及「版本号或 done 标签 + 长期未改动」的**候选**,`staleAfterDays` 可调) |
22
- | `tiddlywiki_git_sync` | `action: pull\|push\|sync` |
22
+ | `tiddlywiki_git_sync` | `action: pull\|push\|sync` —— **作用于所有配了 `git.remote` 的库**(v0.30.5):多库各自独立仓库时逐仓库同步、**每个仓库只跑一次**(多库共用一个工作树时不会重复 pull/commit/push),一个仓库冲突不影响其余;回执逐仓库标注「哪个库 @ 哪个仓库」,没配 remote 的库被列出来而不是静默略过 |
23
23
  | `tiddlywiki_git_resolve` | pull 冲突后按 tiddler 二选一(`keep-local\|keep-remote`) |
24
24
 
25
25
  **知识库同步纪律**:
26
26
  1. 开工先 `tiddlywiki_git_sync action=pull`(rebase + autostash,真冲突会 abort 并报文件)。
27
27
  2. 冲突后用 `tiddlywiki_git_resolve` 二选一解决,再重新 sync(**绝不自动覆盖**)。
28
- 3. 收工 `tiddlywiki_git_sync action=sync`(pull → commit → push)。
28
+ 3. 收工 `tiddlywiki_git_sync action=sync`(pull → commit → push)。**它与「同步」按钮的区别**(v0.30.5):工具作用于**所有**配了 `git.remote` 的库(一次一个仓库,各自独立成败),而设置页/FAB 的「同步」按钮只作用于**当前那个库**(它带着 `?wiki=`)。多库各有仓库时,冲突回执会写清是哪个库,解决时把那个库的 id 传给 `tiddlywiki_git_resolve wiki=<id>`。
29
29
 
30
30
  > 🛡 **带着冲突绝不提交**(v0.23.4):`commit` 之前会探测「进行中的 rebase / 未合并路径 / **工作树里残留的冲突标记**」,命中就**拒绝提交**(`/sync` 返回 409 + 文件名、自动提交只上报一次、`/status` 与设置页状态行显示「冲突未解决(N 个文件,已阻止提交)」)。这条守卫来自真实事故:autostash 重新应用冲突后 `rebase --abort` 已无事可 abort,冲突标记留在工作树里被自动提交**永久写进了 git 历史**(连带插件自己解析不了配置)。**删冲突标记的方式**:解决后 `tiddlywiki_git_resolve`(或手动编辑掉 `<<<<<<<`/`=======`/`>>>>>>>` 三行)再 sync。
31
31
  >
@@ -48,7 +48,7 @@
48
48
  | 配置 | 作用 |
49
49
  |---|---|
50
50
  | `prompt.enabled` | 关掉后本插件不注入任何文本 |
51
- | `prompt.mode` | `slim`(**默认**,v0.24.0 起约 1.9KB):只保留工具 schema 表达不了的约定(写入/并发纪律、同步纪律、工作区标记、先窄后宽检索、时效标注、可点击链接格式);`full`:额外附一份**参数索引**,由工具注册表在运行时生成,因此永远不会与真实工具脱节 |
51
+ | `prompt.mode` | `slim`(**默认**,v0.29.0 起约 1.0KB):只保留工具 schema 表达不了的约定(**同步时机**、写入被拒后重读而不用 `force`、时效标注 `valid-until`/`review-after`/`superseded-by`、淘汰权归人、`human-edited`、可点击链接格式、todo 约定)。与工具 description 逐字重复的句子**不再复述**(工作区标记、先窄后宽检索、多词 AND、tags/type 保留规则都在对应工具的 schema 里,`verify-prompt.mjs` 会在 schema 侧逐条守住"从提示词删 ≠ 丢了");`full`:额外附一份**参数索引**,由工具注册表在运行时生成,因此永远不会与真实工具脱节 |
52
52
  | `prompt.extra` | 追加在末尾的自定义规范(团队 / 个人偏好),始终生效 |
53
53
  | `prompt.override` | 非空时整段取代内置文本(`extra` 仍会追加)——想完全自写提示词时用 |
54
54
 
@@ -104,8 +104,9 @@
104
104
  - **左侧边栏每个库一行**(v0.28.3 起,点击语义见 v0.28.13):每个**在运行**的库一个入口行,用自己的显示名与图标(在「知识库列表」里改),**当前面板显示的那个库高亮**。**点另一个库 = 立刻切过去**(面板保持打开,不会先关掉让你再点一次);点**当前那个库** = 收起面板 —— 入口同时也是开关。没在运行的库不在这一列(它们仍可从右下角「知识库」菜单启动)。
105
105
  - **切库为什么需要等一下**(v0.28.14):切换 = 在同一个 iframe 里**换成另一个库的整份 TW 文档**(一个 iframe 只能装一个编辑器)。本机实测这份文档是 **9.9 MB / 29.9 MB / 28 MB**,而且 TW 自己回 `Cache-Control: no-store`、没有 ETag —— **缓存不了也预取不了**,所以每次切换都要真下载解析一遍。这一版做了三件事让它不"看起来卡住":① 点击**立即**开始换(不再先等一次 `/status`,那次往返在这台机器上要 300–400 ms,因为 host 每次都要跑最多 5 个 git 进程);② 侧边栏的选中高亮即时跟上,且不再重复请求 `/status`;③ TW 界面底部显示 **`正在载入知识库「个人」…`**,新文档到达即收起(另有 15 秒兜底)。**剩下的等待是 TW 文档大小决定的**,不是可以再优化的延迟。
106
106
  - **「移出列表」不删目录**:只是让插件不再管理它,内容原样保留。
107
+ - **界面语言(v0.30.6)**:设置页的 `uiLanguage`(`zh` 默认 / `en`)现在**同时驱动两半** —— TW 子进程的界面**和插件自己的界面**(右下角「知识库」FAB、侧边栏入口、输入框上方那一行、快速笔记卡片与编辑器弹窗、工具卡片、会话「知识库」Tab、设置页、全部 toast)。改完保存:TW 面板内的界面随子进程重启生效,插件自己的界面**刷新页面**即生效(客户端在 `localStorage` 缓存了语言,刷新后首屏就是对的)。
107
108
  - **每个库一个图标**(v0.28.8):知识库列表每行的图标是个**弹出式网格**——里面是 **DSH 官方的 36 个图标**(与侧边栏/工具栏那套同源,深浅主题都对)、本插件自绘的 8 个(书籍 / 公文包 / 首页 / 笔记本 / 烧瓶 / 地球 / 星标 / 归档),外加一组 emoji 快捷选择;图标多了按 **48 个一页**翻页。选中即写入 `wikis.json` 的 `icon` 字段。当前值若不在候选里(比如你手写的短 emoji)会原样保留,不会被候选列表抹掉。**v0.28.8 同时修了一个真 bug**:此前选了图标界面会立刻变回默认 —— `/admin/wikis` 的读取视图漏了 `icon` 字段,于是"写进去了、读回来是 undefined",看起来就是没生效(现在有守门钉死这个字段,反向验证过)。
108
- - **还没发过消息的新会话也能选库**(v0.28.8):选择器此前只长在输入框上方;新会话(空白页,输入框还没出现)时它挂在**预设/模式选择旁边**,位置由声明决定、声明不到就 2 秒后回落,两种布局不会同时出现两个。
109
+ - **还没发过消息的新会话也能选库**(v0.28.8,v0.28.11 修):选择器此前只长在输入框上方;新会话(空白页,输入框还没出现)时它会挂到**预设/模式选择旁边**那一格(`conversation.input.selector.context`)。实现是**声明感知**的:DSH 声明了那一格才注入,**声明不到就一直留在 dock 里那一行**——不会再"2 秒后回落到 dock"另注册一个(那个回落会在新会话里同时出现两个选择器,v0.28.11 已删除)。
109
110
  - **会话「知识库」Tab 在多库下按会话作用域取数**(v0.28.8):汇总只统计**该会话作用域那个库**里的笔记;不这样做就会出现"A 库的笔记标题在 B 库里渲染成空白卡片"。
110
111
  - **桌面版(DSH Desktop)**(v0.28.8):TW 子进程需要真正的 `node`。以前直接拿 `process.execPath` —— 在桌面版里那是 Electron 可执行文件,TW 根本起不来。现在先按 `DSH_TIDDLYWIKI_NODE` 环境变量、再 `where node` / `which node`、最后才回落 `process.execPath`;解析结果缓存,并在启动日志里写明用了哪个。
111
112
  - **清单文件**:`$DSH_HOME/dsh-tiddlywiki/wikis.json`(在每本 wiki 之外,所以它不会随某个库被搬走)。
@@ -217,7 +218,7 @@ seed 是把「wiki 里预置内容」随插件分发的机制:**ONE-SHOT(只
217
218
  bridge:
218
219
  enabled: false # 本地剪藏桥(书签小工具);保存后立即生效
219
220
  port: 8618 # 监听端口(127.0.0.1;改后需重启 dsh web 才绑定新端口)
220
- token: "" # 共享口令;非空时校验书签的 x-clip-token 头(强烈建议设置)
221
+ token: "" # 可选加固:非空时校验书签的 x-clip-token 头(默认不设——任意页面可建笔记,系统项拒收)
221
222
  tag: "clip" # 剪藏笔记默认 tag
222
223
  wiki: "" # 剪藏写进哪个库(v0.28.8):空 = 知识库列表里带 ★ 的那个库;多库时可钉死到某个 id
223
224
  ui:
@@ -205,7 +205,7 @@ wiki 里既有**早已发布**的文章,也有**明确不能发**的内容。
205
205
 
206
206
  **规范全文在 wiki 里**:笔记「**发布元数据规范**」(随插件 seed 分发,`dsh-docs` 标签,
207
207
  开机自动写入;也可手动跑 `npx tsx tools/wechat/seed-publish-spec-now.mts` 立即写入)。
208
- 注入给 agent 的系统提示词里只有一句指针(slim 预算只剩几十字符,放不下全文)。
208
+ 注入给 agent 的系统提示词里**只有一句指针**(规范全文太长,不适合放进每个会话都要付的常驻预算 —— v0.29.0 把 slim 从 ~1.9KB 瘦到 ~1.0KB 之后依然如此,指针就够了:需要时 Agent 会去读那篇笔记)。
209
209
 
210
210
  ### 字段(自定义字段,不是标签)
211
211