|
@@ -1,8 +1,18 @@
|
|
1
1
|
# 版本记录
|
|
2
2
|
|
|
3
|
-
> 逐版本变更。**版本历史的唯一所在地**——README 只放"是什么、怎么装、好在哪",不再重复这里(2026-09-29 起,此前 README 抄了最近 18 条,每次发布都要改两处)。
|
|
3
|
+
> **只记「这一版改了什么、为什么」,不写推演过程**。完整来龙去脉见 git log / [GitHub Releases](https://github.com/bbqisbbq/dsh-tiddlywiki/releases);当前状态与踩过的坑见 AGENTS.md §2 与 wiki「dsh-tiddlywiki 事实总表」。
|
|
4
4
|
|
|
5
|
-
> 最近几个主要版本的一句话记录(完整变更见 [Releases](https://github.com/bbqisbbq/dsh-tiddlywiki/releases) / git log)。
|
|
5
|
+
> **维护规则**:「最近版本」保持 8 条详细条目,新条目加在最上面;超过 8 条就把最老的一条压成历史表的一行。历史表一行只需说清「改了什么」,别把守门条数、行数对比这些过程数据抄进来。
|
|
6
|
+
|
|
7
|
+
## 最近版本(详细)
|
|
8
|
+
|
|
9
|
+
- **v0.33.4**(2026-10-26):**四路只读审计(host / 工具与数据路径 / 客户端 / 死代码)后的修复版**。
|
|
10
|
+
- **数据安全**:trashbin 软删除把副本标签整体替换成 `$:/tags/trashbin`,恢复时又从 `fields.tags` 找原文——而 TW 的 `tags` 是已知字段、永远不进 `fields`,于是装了 trashbin 的库删完再恢复**标签全丢**且静默(现在副本写 `[标记, ...原标签]`,恢复只剥标记);写工具(put / batch_put / delete / rename / append / replace / attach)**没有** `isBlockedProxyTitle()` 守卫,模型可直接改写 `$:/plugins/dsh-tiddlywiki/config`(`wechat.command` 会被 spawn、`git.remote` 下次同步生效)——读侧早就拦了,写侧一直敞着。
|
|
11
|
+
- **互斥与假成功**:`MutationLock.end()` 没有所有权校验,而 `/admin/seeds/run` 对**每条**响应都调它(最常见的「重新初始化但什么都没改」压根没 begin)⇒ 会把此刻正持锁的 `/sync` 释放掉,`/admin/restart` 随即拿锁、两条路径同时排干 + 停子进程;现在 `end(label)` 校验持有者。`/admin/info`(改插件/主题/语言也会 drain + 重启)一直没接这把锁;`/restart` 与 `/admin/restart` 在目标库没在跑时回 `{ok:true,drained:true}`(什么都没重启)。
|
|
12
|
+
- **多库与会话**:会话汇总的引用正则只认裸 `/tw/#标题`(多库注入的是 `/tw/<id>/#标题` ⇒ 「知识库」Tab 的读取列表系统性为空)、`NOTE_TOOL_NAMES` 漏 `tiddlywiki_replace`、汇总缓存不按库分键(3s 内串库)、工具层 `autoCommit()` 永远 touch 默认库的 committer、设置页跑 seed 丢了 `proxyBase`(多库下「重新初始化」把 `$:/config/tiddlyweb/host` 写回裸路径,那个库的编辑器此后所有 API 打到默认库)、`/admin/prompt` 预览漏掉 wechat 规则与多库链接格式。
|
|
13
|
+
- **其他确定缺陷**:`config.load(undefined)` 清缓存却把 `loaded` 留成 true(下一次 `set()` 会把配置抹成只剩本次补丁)、`stop()` 打断 `start()` 后 health 永远停在 `starting`、`/note` 的 `=== true` 把「默认开启」倒过来、剪藏兜底标题的最后一个候选从未校验、剪藏正文上限(20 万字符)大于 body 上限(256KB)、不带 heading 的 append 仍塞 LF 块(CRLF 笔记被写混)、attach 吞 `tags: []`、put 回执的 `字段:` 是「调用方传了什么」、wechat 启动 spawn 抛错仍回 `ok:true`、`/api` 代理不排空 DELETE body。
|
|
14
|
+
- **死代码 / 无效逻辑**:`delete` 的 `permanent: true` 逃生通道写在 `canTrash` 之前(文案指的路永远走不通)、trash list 的 `slice(-limit).reverse()` 把「字母序副本 + 时间序索引」混当「最新在前」、`empty` 在从没有索引的库上凭空建索引、`listTrashbin` 单条读取失败会掀掉整张列表、`库目录 === 仓库根` 的库在「diff 未知 ⇒ 保守重启」里被漏掉、`isPlausibleTitle` 放行 `[`、7 处被 `\t` 吃字/粘行的注释;`tiddlywiki_status`(不存在的工具名)又一次出现在错误文案里。
|
|
15
|
+
- **文档**:`docs/CHANGELOG.md` 从 292 KB 压到 ~32 KB —— 最近 8 条保持详细,其余 150 条压成一句话表(演进细节交给 git log / GitHub Releases)。
|
|
6
16
|
|
|
7
17
|
- **v0.33.3**(2026-10-26):**`.tid` 预览的第一眼改成看内容,而不是看元数据**。
|
|
8
18
|
v0.33.2 修好注册之后,预览能出图了,但顶部把**所有**字段原样铺开:`created`/`modified` 是 17 位 UTC 戳(`20260920070533227`),`revision`/`workspace`/`ws/<项目>`/`agent-written` 这类机械字段也一字不落地占版面 —— 打开一个笔记,前两屏全是元数据。
|
|
@@ -33,200 +43,157 @@ v0.33.1 修好了 `key` 配对,但用户实测仍是纯文本,且 devtools
|
|
33
43
|
|
|
34
44
|
- **v0.31.0**(2026-10-26):**对齐 DSH 0.2.1 的一轮改造 + 两条真实回归修复**。① **选库入口搬到官方座位**:`conversation.input.selector.context` 在 0.2.1 里**不存在**(v0.28.8 起的 `scope-seat.ts` 整个是死代码),选库改挂官方内联座位 `conversation.input.left`(`id: 'wiki-scope'`),`scope-seat.ts` 删除、dock 里那套 `scope` 内联渲染与自测量一并清掉;右侧栏 guide 条目补 `id` + `keepMounted: true`(0.2.1 的分页约定)。② **新增 `scripts/verify-dsh-contract.mjs`**:解析宿主 `app.asar` 里的 `dsh-cordis-client-runner`,把 103 条官方座位目录与我们源码里注册的 6 个座位逐一比对(存在性 / kind / `tool.call.toolview` 的 54 个已占 key 是否撞车),并断言 `engines.dsh` 已声明 —— 此前"座位名写错"只会静默不渲染。③ **`tiddlywiki_get` 支持分页**(`offset`/`limit`,默认 12000 字符):超长笔记一次读全会撞宿主的 12500 token 回执上限;回执明确写"正文 a–b / 共 N 字符"并给出下一步 `offset`。`search`/`lint` 的回执也加了同量级的体量预算(命中条数与计数结论**永不截断**,只截示例并说明省略了多少)。④ **16 个工具补 `output.schema`**:DSH 0.2.1 在每次调用后用 `validateJsonSchemaValue` 校验返回值,不过即抛 `ToolOutputError` —— 而 `sdk.ts` 的 `output.schema` 自 v0.20.0 起写死成 `{}`(当时注释断言"没人读它",**实测为假**)。现在按"只声明必定存在且类型稳定"的规则为 16 个工具各写一份,并加两条守门:每个工具必须声明非空 object schema;**用 E2E 里每一次真实返回值反查声明**(抓"schema 比现实更严"这种线上致命错误)。⑤ **skill 改用 `ctx.skills.register`**:拆库 skill 过去只能往 `$DSH_HOME/skills` 写文件,现在优先注册到宿主 skill 注册表(内存、随插件卸载消失、不碰用户目录),只有宿主没有该服务时才回落写文件;新增 `scripts/verify-embedded-skill.mjs`(解析/坏输入/disposer/注册表抛错/**启动顺序必须"先注册后回落"**)。⑥ **新增 `/note` 斜杠命令**:一句话记笔记,直接写进当前会话的知识库、不产生模型消息;同名笔记**已存在就拒绝**并指路("同一件事写多份互相矛盾"是最难收拾的失败形态),且写成 `inbox` + 工作站标记、**不打 `agent-written`**(那是人写的);守门 `scripts/verify-note-command.mjs`。⑦ **提示词两处修订**:新增「### 先查再答」(模型可见工具从 57 涨到 243 之后,新会话不再主动检索 —— 注入本身没坏,`system/message` 里逐字可查,坏在提示词只讲"怎么写"、从不讲"什么时候该来查")与「### 时效与去重」(作者原话:agent 爱写短命内容、同一件事写多份有差异的、正文里堆变更流水)—— 钉子:一件事只留一处权威表述(先搜、命中就**原地改那一节**)、每条笔记必须看得出"还有效到什么时候"(`valid-until`/`review-after`/"长期有效",说不清时效的别写)、**不写流水账**、**读到就先判时效**。slim 正文 1079 → **1390 字符**(预算 1000 → 1470,与逐条断言绑定),并把提示词正文的拼装从"硬编码数组 + 导出的块清单"两份实现收成 `governanceBlocks()` 单一来源 —— 加规则时 `verify-prompt` 当场抓到这个双份实现。
|
|
35
45
|
|
|
36
|
-
- **v0.30.65**(2026-10-09):**新增 `tiddlywiki_replace` 工具(笔记正文局部替换)**。Agent 想只改笔记里的几处文字(比如把 Markdown 圆括号内链批量改成双括号 `[[x]]`)此前没有对应工具:要么整篇 `tiddlywiki_put` 重写(易出错、费 token),要么绕过插件手写 HTTP PUT(会踩「`modified` 是 UTC、别写本地时间」这类坑)。新工具 `replace(title, old, new, all?, expectedModified?, expectedRevision?, force?)`:读全文 → 精确替换子串 → 整篇回写;默认只换第一处、`all:true` 换全部;`old` 未命中回执 `replaced:0` 且不报错(别把「没命中」当「改好了」);与 put/append 同一套乐观并发保护与写策略(保留 type/tags/字段、二进制附件拒绝)。工具数 15 → 16:`full` 提示词目录、`verify-prompt`/`verify-tool-views` 的工具数断言、AGENTS.md §2、package.json 描述、docs/development.md 同步;客户端新增「局部替换」工具卡与徽标(`tool-views` / `i18n-card`)。
|
|
37
|
-
|
|
38
|
-
- **v0.30.63**(2026-10-09):**默认提示词整理(「聊天引用」与「笔记内引用」分家)+「整段替换」配置说明勘误**。① **笔记约定的链接规则拆成两条**:旧版只有一条 `[标题](/dsh-tiddlywiki/tw/#标题)`——那是**聊天回复**专用写法,模型会照抄进笔记正文,于是 URL 一旦带未转义空格就被 markdown-it 整条拒绝渲染成纯文本,即便渲染成功也是外链、点开另起标签页(正是「笔记里的链接偶尔是 Markdown 格式、不是 `[[…]]`」的根因)。现在拆成「**笔记内引用**」写 `[[标题]]`(文字不同写 `[[标题|文字]]`,两种内容类型下都能正确渲染成站内链接、无需 URL 编码)与「**聊天里引用笔记**」写带作用域 base 的链接形式(多库时自动带库 id)。slim 正文 866 → 937 字符(仍 < 预算 1000,`verify-prompt.mjs` 预算/链接/多库断言全绿)。② **`config.prompt.override` 设置页说明勘误**(zh+en):原「非空时取代上面的内置文本」有歧义,改为「非空时替换内置提示词,附加说明仍会追加在末尾」。纯文案,无行为变化。
|
|
39
|
-
|
|
40
|
-
- **v0.30.62**(2026-10-09):**第二轮审查的三类修复一次落地(回收站格式合并 / 会话作用域目录 / 剪藏目标库口径 + 提示词 −14% + 清理)**。全部改动都有行为级守门,且都做了反向验证。① **回收站:两种格式真正合并**。v0.30.59 的注释写着「legacy index is still READ so a wiki that has both keeps working」,代码却是 `trashbin !== undefined ? {entries: []} : readTrashIndex()` —— 于是**装了 trashbin 的库反而看不到内置条目**:work 库当时回收站里明明有 1 条内置索引条目,`trash list` 报「共 0 条」(本次实测复现)。现在两种格式**都读、合并列出**,恢复/清空按「这条来自哪种格式」记账(`fromIndex`)。② **`|` 分隔符会截断标题**:v0.30.60 的注释断言「TW 把 `|` 转义成 `|`」——**实测为假**(`<$text text="a|b"/>` 原样渲染回 `a|b`;TW 的 htmlEncode 只转 `& < > "`),所以标题含 `|` 的副本会被拆成两段、列出**截断的标题**且恢复不了。改为用**自有标记 `<li>`** 分隔(标题里的 `<` 必被转义,不可能伪造标记),并把抽取逻辑拆成纯函数 `parseTrashbinFragment()`;顺手补上**实体还原**(`<`/`>`/`"`/`&`,`&` 放最后才是无损的)⇒ 标题含 `<`/`&`/`"` 的副本现在也能正确列出与恢复。③ **恢复不再把副本的溯源字段留在笔记上**:`TRASHBIN_AT_FIELD` 在 v0.30.60 从 `trash-at` 改名成 `trash-iso`,而恢复路径只删新名字 ⇒ 恢复**任何 v0.30.60 之前的副本**(或内置格式快照)都会让 `trash-at` 留在恢复出来的笔记上(正是改名要躲的那种「TW 会重排成本地日期」的字段)。现在旧名字有具名常量 `TRASHBIN_LEGACY_AT_FIELD`,**列出时读、恢复时删**,内置格式的写入也统一用新常量。④ **`git_resolve` / `git_sync` 的目录改从会话作用域读**(`ToolScope.dir`):两者原来都走 `ToolsDeps.wikiPath()`,而它解析成**默认库** —— 于是「排干哨兵写进会话库、却在默认库目录里等文件」(多库且作用域≠默认时**必然** 12s 超时 + 假 `drainFailed`,而真正要验的库一行没验),`git_resolve` 不传 `wiki` 时会在**错的仓库**里 checkout+commit(回执示例恰好就是不带 `wiki` 的写法)。`ToolsDeps.wikiPath` 已删除(它没有 session 参数、只会误导)。⑤ **剪藏目标库口径统一**:`bridge.wiki` 指向**已登记但没在跑**的库时不再静默回落默认库(那正是「剪到别的库」的失败形态,而书签小工具根本不看回执里的库名),改为 **503 + 点名那个库**(与 `?wiki=` 路由自 v0.29.0 起的规则一致,`targetProblem()` 把句子带进响应体);**未知 id** 仍回落(老书签不能 404);书签的成功提示现在显示回执里的库 id;`bridge.*` 是**插件级**设置(读取它的是默认库那份 config),设置页因此只在「未选中具体库」的作用域里露出这一组,选中某个库时给一句说明 —— 否则在别的库的配置里改它是静默无效的(v0.28.8 起就存在的陷阱);seed 文档的配置表补上 `bridge.wiki` 这一行。⑥ **注入提示词 1011 → 866 字符(−14%,预算 1200 → 1000)**:写入约定压成「被拒后重读、别用 `force`」一句(`expectedModified`/`force` 的语义逐字在 `put` 的 schema 里)、同步那条去掉与 schema 重复的自动 commit 说明、intro 去掉「本机有一个知识库」这个**多库下就是错的**说法;同时**多库时正文会自动换成带库 id 的链接格式** `/tw/<id>/#标题`(裸 `/tw/` 在助手正文里跟随**焦点库**、写进笔记则把中央 TW 的 iframe 导航到**默认库**——两种都不是「本会话那个库」)。⑦ **清理**:删掉死代码 `messageKeys()`(注释自称有消费者,实际全仓 1 处引用=声明自身)、修掉三处过时注释(「尚无 i18n 分支」/「index.ts effectiveBridge」/两条被粘成一行的注释)、`tiddlywiki_status` 这个**不存在的工具名**从错误文案里去掉、删掉两个零引用的一次性迁移脚本(`push-notes-to-wiki.mjs` / `split-agents-to-wiki.mjs`)、`docs/code-review-*.md` 不再随 npm 包发布(`files` 加否定式 + 守门断言,实测 171 → 170 项)、`wechat.endpoint` 补进 `ResolvedConfig`/`STRING_CONFIG_PATHS` 两处形状、AGENTS.md §2 的行数清单去掉会漂移的数字。**守门**:`verify-trashbin-probe.mjs` 11 → **15 条**(新增「两种格式合并列出」「从索引恢复要摘掉索引记录」「恢复不得残留 `trash-at`」「含 `|`/转义标题的解析」)、新增 `scripts/verify-clip-target.mjs`(运行中/已登记未运行/未知 id/清单未就绪 + 端到端 503 文案,**这是 AGENTS 早就声称存在、实际为 0 的守门**)、`verify-git-multi-sync.mjs` 新增两条(哨兵必须落在会话库目录、`git_resolve` 缺省目标必须是会话库)、`verify-prompt.mjs` 新增多库链接用例。**反向验证**:去掉 `delete restored[TRASHBIN_LEGACY_AT_FIELD]` ⇒ 15 条里精确红 1 条;恢复后逐字一致。**环境事实(今天第二次踩到)**:运行时的 `profiles/*/node_modules/dsh-tiddlywiki` 必须显式升级,源码仓库改了不会自动生效。
|
|
41
|
-
- **v0.30.61**(2026-10-09):**修 `tiddlywiki_trash list` 少报一条 —— TW 给块级片段的首元素套 `<p>`** —— v0.30.60 实跑时 `list` 对**126 条**回收站报「共 125 条」,少的那一条**恰好是排序后的第一条**(实测片段为 `<p>$:/trashbin/__验收测试_trashbin软删除__|…`,而 `startsWith('$:/trashbin/')` 对带 `<p>` 的首条不成立)。**「列表少一条」是最坏的失败形态之一** —— 少的那条可能正是用户最想找的那条,而工具报告成功、不报错。现在改为**先定位前缀再剥标签**(`indexOf` 取到前缀处、再 `replace(/<[^>]*>/g,'')`),与首条是否被段落包裹无关。② 同时兼容旧字段名:`trash-iso` 是 v0.30.60 起的名字,**此前写的副本只带 `trash-at`**,不兼容读它时回执里「删除于」那一栏**整列为空**(实测);现在两个字段都读,`trash-iso` 优先。**守门** `scripts/verify-trashbin-probe.mjs` 增至 **11 条**,新增两条钉住上述两点;**反向验证**:去掉剥标签逻辑 ⇒ 红、改回 `\n` 拆分 ⇒ 红,各自精确点名,恢复后逐字一致。
|
|
42
|
-
- **v0.30.60**(2026-10-09):**修 v0.30.59 的 `tiddlywiki_trash` 静默谎报「回收站是空的」** —— v0.30.59 发布后第一次实跑,`list` 就报「共 0 条」,而 wiki 里明明有 126 条 `$:/trashbin/` 副本。**两个独立缺陷叠在一起,缺一个都不会这么静默**:① `listTrashbin()` 的探针写成 `<$list … variable="ignore">` —— **`variable="ignore"` 会占用 `currentTiddler` 这个保留变量名**,于是循环体内的 `<$text text=<<currentTiddler>>/>` 全部绑到空上下文,实测返回 **126 个分隔符、零个标题**;② 分隔符原写成 `\n`,而片段是 **HTML**、换行会被折成空格,按 `\n` 拆会把整个列表当成**一行**,那行又过不了 `$:/trashbin/` 前缀检查。两者都产出 `[]`,而「空的回收站」是完全正常的答案 —— 于是工具**报告成功、同时撒谎**。③ 补一道自检:**有分隔符却零标题 = 探针坏了,必须显式报错**(不再回落成空列表)。④ 逐条 `wiki.get()` 改**有界并发(8)**:125 条串行就是 125 次 REST 往返。⑤ 溯源时间字段由 `trash-at` 改名 **`trash-iso`** —— TW 会把任何 `*-at`/`*-date` 字段当日期**重新序列化成本地格式**(实测 PUT 进去的 ISO 读回来是 `10/09/2026 05:11:54`),机器可读的值会丢。⑥ 配套发现并修正一处**流程事实**:`plugins/dsh-tiddlywiki`(源码仓库,0.30.59)与 `profiles/*/node_modules/dsh-tiddlywiki`(运行时实际加载,0.30.57)**是两份不同的副本** —— 改完源码 + 重启 DSH 不会生效,必须走发布流程(tag → npm publish → 升级运行时副本),这是 AGENTS.md §4 末尾第 8 步「同步线上 wiki」的真实含义。**守门**(新增 `scripts/verify-trashbin-probe.mjs`,进 `verify:unit`,8 条):钉住探针不含 `variable="ignore"`、必须输出 `<<currentTiddler>>`、必须按 `|` 拆而非换行、有分隔符零标题必须 throw、有界并发、字段名。**反向验证**:把 `variable="ignore"` 注回去 ⇒ 红;把 `|` 换回 `\n` ⇒ 红;恢复后逐字一致。
|
|
43
|
-
- **v0.30.59**(2026-10-09):**软删除改为「跟着 wiki 已有的回收站机制走」;同时把一条会误导人的注释改成实测事实** —— 本机 work 库已装 [kookma/trashbin](https://github.com/kookma/TW-Trashbin)(v1.3.6,`$:/trashbin/<原标题>` + `$:/tags/trashbin` 标签),但 `tiddlywiki_delete` 此前**看不见它**:软删除一律写 `$:/dsh-tiddlywiki/trash/<时间戳>/<原名>` + JSON 索引,于是 agent 删掉的东西只有 agent 能列出/恢复,人在 TW 侧边栏里一条都看不到,两套回收站互不相通。现在:① `tools-support.ts` 新增 `hasTrashbin()` 探测(**只读**,不写探针 tiddler——那会在从不装 trashbin 的库里留垃圾,而它正是要检测的东西;判据是 `$:/tags/trashbin` / `$:/plugins/kookma/trashbin/sidebar-tab` **是否存在条目**,**不是查 `tiddlywiki.info`**——浏览器安装的 trashbin 压根不在 info 里(实测:服务器无 `plugin.info`、info 无登记,却照常工作),查 info 会在「恰恰装了它的库」上误报 false);② 命中时 `delete` 写 trashbin 格式副本(保留原 `created`/`modified`,另记 `trash-of`/`trash-iso`),**不再写 trash-index**,同名二次删除显式拒绝而非静默覆盖;③ 未命中时走原路径(未装 trashbin 的库行为不变)。**⚠️ 顺带修正一条错误的注释**(`tools-support.ts` 原先写「`$:/` 系统标题无法被枚举」,v0.30.58 之前 agent 据此连续三次误判,包括否掉一个实际可行的方案):实测证明「不可枚举」**只对 HTTP 列举端点成立**(`get-tiddlers-json.js` 给任何 filter 追加 `+[!is[system]]`,实测 `tiddlers.json` 恒 138 条、系统条目恒 0),而**filter 引擎无此限制**(服务端渲染 `[tag[$:/tags/trashbin]]` 得 `COUNT=125`)、**启动期 `store.area.template.html` 内嵌整个 store** 使客户端一开始就持有全部 tiddler——**三条通道必须分开说**,这也是 trashbin 格式不需要索引的根本原因。
|
|
44
|
-
- **v0.30.58**(2026-10-09):**`tiddlywiki_git_sync` 不再要求显式配 `git.remote` —— 配置为空时回落到该库所在仓库自己的 `origin`**(作者 2026-10-09 裁定,方案 A)。起因是一个**同一个问题两个答案**的真实现场:设置页/FAB 的「同步」按钮一直是在目录里跑 `pull/commit/push`、按仓库 `origin` 推,而 Agent 工具把 `git.remote` 当成"开启同步"的开关 —— 同一个仓库,界面点得动、工具却回「没有任何知识库配置了 git.remote(同步远端)」并**整库跳过**(实测:本机 notes 仓库已推到远端、`git remote -v` 有 origin,工具仍报三个库皆未配置)。现在:① 判定改为「**`git.remote` 或仓库 `origin`**」,`git.remote` 降级为**覆盖**(也仍是 `ensureRemote` 写入的目标);② 回落**只在「没配 **且** 真在 git 仓库里」时发生** —— 不在仓库里就不白跑 `git`(新增 `GitFace.originOf()`,**只读、不建远端**);③ `GitSyncTarget` 新增 `remoteSource: 'config' | 'origin' | 'none'`,**回执逐仓库写出远端与来源**(「自动识别自仓库 origin」/「来自 git.remote 配置」),且 URL 里的**凭据先剥掉**(`git.remote` 可能带 PAT,`/admin/state` 对同一个值打码 —— 回执是新开的一条出口);④ `skipped` 分得清「未配 `git.remote`,仓库也没有 origin」与「不在 git 仓库里」,零目标时的文案同时给出两条出路;⑤ 设置页「git 远端」提示改为「空=自动用该库所在仓库的 origin」,`docs/features.md` / `docs/usage.md` / `AGENTS.md` 同步改写。**守门**:`scripts/verify-git-multi-sync.mjs` 新增「回落只在该回落时发生」(**假 farm + 假 GitFace 直接驱动 `createGitLayer().gitTargets()`**,断言 `originOf` 的**调用点集合**:有配置的不探、不在仓库的不探)、回执来源文案、凭据剥离、两种跳过原因;`scripts/verify-repo-keying.mjs` 用真临时仓库钉 `originOf()` 的三态(有 origin / 没有 / 不在仓库里)与「只读不建远端」。`createGitLayer` 因此进入公共面(`scripts/index-barrel-surface.json` 已更新)。**反向验证**:把回落那一行改回 `''` ⇒ 新守门当场点名 `personal` 的 `remoteSource`;恢复后逐字一致。**顺手修好一处既存红灯**:`AGENTS.md` §5 的开发文档索引还指着 2026-10-04 那次笔记整理里**已被合并删除**的 5 篇旧笔记(常见坑 / 架构事实 / 仓库布局 / 易变清单 / 发布流程),而 `scripts/verify-wiki-pointers.mjs` 正是为此存在 —— 指针已改指**整理后仍在的三篇**(《dsh-tiddlywiki 事实总表》《TiddlyWiki 使用与技巧事实总表》《拆库丢失的系统条目清单》)+ 仓库 `docs/CHANGELOG.md` / `docs/development.md`,§2 §4 §6 §7 里同样的旧名字一并改掉。**不恢复那些笔记**(删除是有意为之),`verify:sync` 因此恢复全绿。
|
|
45
|
-
- **v0.30.57**(2026-09-30):**给 `AGENTS.md` 的注入预算装上守门** —— 这条纪律的**存在理由就是它自己**。本文件顶部写着:宿主给 workspace instructions 的预算是 **65536 字节**,「过去它长到 ~120KB,**截断正好砍掉最后几节(发布流程 / 维护规则 / 常见坑)**」—— 也就是说**这份文件当初被拆成「规则 + 指针」的原因,就是它撞过预算上限**。而那条教训**只写在正文里,没有任何东西在量它**(`verify-doc-links` 管链接、`verify-wiki-pointers` 管指针,都不管篇幅),失效方式依然是沉默的:文件继续长到超限,宿主从末尾截断,被砍掉的正好是**最该被看到的规则**,**没有人会收到报错**。新增 `scripts/verify-agents-budget.mjs`(进 `verify:static`):**两级硬阈值** —— 硬上限 **65536**(宿主预算=事实边界)与软上限 **45000**(自我警戒线),**两级都红**(本仓库的教训是「只打印 warning 的守门等于没有守门」;要抬 SOFT 必须明确改常量=有意识的决定)。⚠️ 顺带守一件更隐蔽的事:**文件第 5 行对自身体积的自述不得漂移** —— 它当时写着「现在拆完约 **12KB**」,而实测已 **40.7KB**(**偏差 70%**,文件在对自己撒谎而无人被冒犯)。**这正是本守门第一次运行就抓到的**:`顶部自述「约 12KB」,实测 40.7KB`。已改为实测值并把守门写进那句旁边。**反向验证**:填充到 46920 字节(> 软上限)⇒ 红;填充到 66920 字节(> 硬上限)⇒ 红并点名「注入会被从末尾截断」;恢复后哈希逐字一致。当前实况:`41890 字节 / 65536 = 63.9%`。
|
|
46
|
-
- **v0.30.56**(2026-09-30):**`AGENTS.md` 的 wiki 指针现在有守门了** —— §5「开发文档索引」是这套文档体系的**承重墙**(细节搬进 wiki、注入文件只留指针),但指针断掉的后果是**沉默的**:新会话按它去读会读到空气,而 `tsc` / 任何现有守门都不会报错(`verify-doc-links.mjs` 只管仓库内的相对文件链接)。新增 `scripts/verify-wiki-pointers.mjs`(并进 `npm run verify:sync`):**从 `AGENTS.md` 的表格行里**取出 `[[…]]` 指针(**只取表格行** —— 正文里的 `[[标题]]` 是 TW 语法示例、不是指针,这个区分是判据的核心),再从 wiki 数据根解析出**全部条目标题**(meta 的 `title:` 优先、退回文件名),要求每个指针都命中。**多库模式也认**(优先读 `$DSH_HOME/dsh-tiddlywiki/wikis.json` 的 `defaultId`/全部库,退回单库的 `location.json`)—— 实测本机上插件跑在多库模式、`location.json` 根本不存在,只认单库的话这个守门会**永远 SKIP**。⚠️ **找不到 wiki 数据根时 SKIP 而不是 FAIL**(别人的机器 / CI 上 wiki 可能不存在,前提不成立),但 SKIP **必须明说**「本轮未验证」;也正因如此它**不进 `verify:static`**(CI 上必然 SKIP,等于给人「已守」的错觉),而与 `verify:lib-sync` 同挂在 `verify:sync`(本机环境相关、提交/发布前跑)。**反向验证**:把 §5 的一个指针改成不存在的笔记名(=笔记被改名后的真实形状)⇒ **红并点名**;`--self-test` 模式另行证明「不存在的指针名会被判为缺失」。当前实况:**5 个指针全部命中 · 3 个库 · 2041 条条目**。
|
|
47
|
-
- **v0.30.55**(2026-09-30):**把新守门接进 `prepublishOnly` + 修 `verify-all` 的收尾** —— ① `package.json` 的 `prepublishOnly` 此前只跑 `typecheck + build + verify:static + verify:unit`,**不含 `verify:e2e` / `verify:large` / `selftest` / `smoke:client`**,且**不跑 `verify:sync`** ⇒ 发布时构建产物可能**没被校验过与 `src/` 同步**(正是 v0.30.52 那个缺口的发布形态)。现在改为 **`npm run verify:all`**(= `verify:sync` + 8 步 `verify`),发布前的门与 CI 完全一致。② `verify-lock-sync.mjs`(v0.30.54 新增)此前只在 `verify:static` 里;本版确认它在 `prepublishOnly` 路径上也一定会跑(`verify:all` → `verify` → `verify:static` 的第一步就是它)。③ 顺手把交接卡与三篇 wiki 笔记更新到 v0.30.55。**发布前的门 = CI 的门 = 本地的门**,三处同源。
|
|
48
|
-
- **v0.30.54**(2026-09-30):**铁律 #12 终于有了执行者** —— `package-lock.json` 与 `package.json` 的同步,此前**只靠「记得手工跑一下 `npm ci --dry-run`」**(既慢又碰网络,于是没人会在提交前跑)。新增 `scripts/verify-lock-sync.mjs`(进 `verify:static` 的**第一步**,因为它是纯读两个 JSON、最快能给出结论的一道网):**零网络、零磁盘副作用**地查六件事 —— ① 版本**三处一致**(`package.json.version` / `package-lock.json.version` / `packages[""].version`,发版漏改一处 `npm ci` 就报 `lock file does not satisfy`);② `lockfileVersion >= 2`;③ 名字一致;④ 四个依赖字段的**集合**双向一致(漏了 / 已删);⑤ 每个声明的依赖在 lock 里有**实体条目**且 **locked 版本满足声明的 range**(用 semver;非 semver 范围如 `git`/`file:` 跳过并明说);⑥ **范围本身一致**(同一个依赖写出两个范围=手改过 lock)。⚠️ 它**不替代** `npm ci`(真正的「全量可解析性」需要网络,交给 CI),它拦的是「**改了 `package.json` 忘了重生成 lock**」这一类事故的完整形状。**反向验证三种注入**:改 range 不重生成 lock ⇒ 红(范围不一致);改 lock 里实体版本到不满足范围 ⇒ 红;只改 lock 顶层 version ⇒ 红。当前实况:`LOCK SYNC OK(版本三处一致 · lockfileVersion 3 · 14 个依赖全部对得上)`。
|
|
49
|
-
- **v0.30.53**(2026-09-30):**让「本地全绿 = 可以提交」这条等式真正成立** —— `npm run verify` 现在与 CI **覆盖面相同**,并新增 `scripts/verify-ci-parity.mjs` 防止两者**再次**漂移。这一轮发现的缺口比上一轮更大:CI 的 `selftest` job 跑 **`selftest` + `smoke:client`**,而本地 `verify` **两个都不跑**;CI 四步里还各跑 `typecheck` / `build`,本地 `verify` 也到不了(`AGENTS.md` §4 只写了「改核心路径跑 `npm run selftest`」—— 一句**没有执行者**的建议)。⇒ `verify` 改为 `typecheck && build && verify:static && verify:unit && verify:e2e && verify:large && selftest && smoke:client`(**8 步,与 CI 的 9 个 script 一一对应**),并新增 `verify:all` = `verify:sync && verify` 作为「提交前跑完整一遍」的唯一入口。守门 `verify-ci-parity.mjs`(进 `verify:static`):取出 `ci.yml` 里所有 `npm run <x>`,要求**每一个都能从本地 `verify` / `verify:all` 到达**(按**脚本名可达性**判断,而不是逐字比较命令 —— 本地串 34 个 verify-*.mjs、CI 直接调聚合脚本,形式不同而覆盖面相同,逐字比会天天误报);例外表 `CI_ONLY_ALLOWED` **当前为空,且刻意如此**。**反向验证**:往 CI 里加一步本地到不了的 `npm run verify:missing-demo` ⇒ 红并点名;写完本守门的**第一次运行就当场抓出** `typecheck` 与 `build` 两个缺口。
|
|
50
|
-
- **v0.30.52**(2026-09-30):**补上唯一一个「本地全绿、CI 才红」的类别** —— `lib/` 与 `src/` 的同步,此前**只由 CI** 的 `git diff --exit-code -- lib/` 把关,本地四档 `npm run verify` **一次都不查它**。新增 `scripts/verify-lib-sync.mjs` + `npm run verify:sync`(**故意不进 `verify`/`verify:static`** —— 它是唯一会**写 `lib/`** 的守门,只应在「准备提交」时跑,只读的那两处必须保持只读)。判据之所以可以这么硬:构建**逐字节可复现**(实测两次 `npm run build` 的 `lib/index.js` SHA256 相同)⇒ 直接「重建 → `git status --porcelain -- lib/` 必须为空」,不需要时间戳或哈希清单这类花招。顺手把 CI 里那条**同样只存在于 CI** 的检查(铁律 #8:`lib/*.js` 不得含真实的 `@deepseek-ai` 运行时 import,用 `from`/`require(` 前缀正则避免命中文案)搬到本地。**反向验证**:改一行 `src/index.ts` 而不 build ⇒ 红并点名 `lib/index.js.map`(=当年那次「差点带过期 lib 提交」的完整形状);给 `lib/index.js` 注入 `@deepseek-ai/sdk` ⇒ 命中数 0→1。
|
|
51
|
-
- **v0.30.51**(2026-09-30):**把「barrel 公共面」从人肉纪律变成机器棘轮** —— 补上 `verify-index-barrel` 之外的最后一道缺口。v0.30.49 新加的守门只覆盖「本仓库脚本用得到的」175 个导出;而 `src/index.ts` 一共导出 **294** 个,剩下 ~119 个**在仓库内零消费者**(它们的存在意义是 npm 下游/未来调用方)—— v0.30.42 丢掉 `MARKDOWN_PLUGIN` 就是这一类:**零消费者 ⇒ 零报错 ⇒ 只有人肉 `git diff` 能发现**。本版加**只增不减的棘轮**:把完整公共面快照进 `scripts/index-barrel-surface.json`(294 个名字),**少一个** ⇒ 红并点名(要拦的事故),**多一个** ⇒ 也红并提示更新快照(`公共面变大应当是一次有意识的决定`)。**反向验证**:删掉 `export { MARKDOWN_PLUGIN }` —— `tsc` 报 **0 个错误**,而棘轮精确点名它 ⇒ 这正是 v0.30.42 的真实事故形状;把快照人为缩小一项 ⇒ 「新增了 MARKDOWN_PLUGIN」也红。⚠️ 生成入口 `--update-surface` 必须在「快照存在性检查」**之前**处理,否则首次生成会被自己拦住(本版实测并修掉)。
|
|
52
|
-
- **v0.30.50**(2026-09-30):`src/index.ts` **724 → 674** —— 新模块 `index-startup.ts`(202):**自举阶段整片**(装拆库 skill → 读控制文件 → 读会话作用域 → 建 farm → `startAll` → `applyPrompt` → 绑剪藏桥,收进 `createStartupStage(deps)`)。`index.ts` 拆分第三轮:剩下的是 `apply()` 的其余装配(farm/锁/工具接线 + 拆卸 effect),读入口现在基本能一眼看完生命周期。**三条刻意不动**:① `disposed` 仍归入口(它是**拆卸 effect 与这个任务共同的主人**——任务每次 await 后检查它、发现卸载就自己停子进程,v0.19.1),以 `isDisposed` 传入;② `farm` 由这个阶段**创建**、但**全插件所有**(路由/工具/视图都读)⇒ 经 `setFarm` 交回入口,入口仍是可变状态的唯一主人;③ **顺序即语义**(尤其是「拆库 skill 必须在 wiki 之前装」,那是真实事故的产物)——已在模块头逐条写明。⚠️ 其中一个**必须留在 `index.ts` 的导入绑定**(`readLocationState` / `readRegistry` 这两个名字只被 barrel 再导出使用)暴露成一个模式,本版顺手改掉了根因:`export { X } from '…'` **不消费** import 绑定 ⇒ `noUnusedLocals` 报「声明未读」、而删掉 import 就让 `lib/index.js` **静默少一个导出**(v0.30.48 / v0.30.50 各踩一次,只有 e2e 能发现)。修法:这类名字改用**无 `from` 的 `export { X }`** —— 真的读一次绑定,导出照样存在,两个要求同时满足。⚠️ 守门同步:`verify-ready-policy` 那条「`base: config` 必须交给实例」的断言改读 **`src/index` 族**(阶段搬走后 `base: deps.config` 落在新模块里)—— 判据不变,只是不再钉文件名;`verify-index-barrel`(v0.30.49 新增)本轮**当场**抓出了被漏掉的两个导出。
|
|
53
|
-
- **v0.30.49**(2026-09-30):**修 v0.30.48 引入的真缺陷 + 补上「barrel 公共面」的守门**。① v0.30.48 用 `noUnusedLocals` 清理 import 时删掉了 `writeLocationState` 的 **import 行**,而同一文件里 `export { … } from './host/wiki-location.ts'` 是**独立语句**、不消费那个绑定 —— 于是 `lib/index.js` 不再导出它,`scripts/verify-wiki-switch.mjs` 在 **e2e** 阶段才崩(`does not provide an export named 'writeLocationState'`),而 `typecheck` / `verify:unit` / `verify:static` **全绿**。这正是 v0.30.42(误删 `MARKDOWN_PLUGIN`)那一类「**barrel 丢失对编译器和多数守门不可见**」的第二次事故。② 新增 `scripts/verify-index-barrel.mjs`(进 `verify:static`):**凡是本仓库脚本从 `../lib/index.js` 具名导入的符号,`src/index.ts` 必须真的导出它**(解析的是 `export { … }` 与 `export <decl>`,**不是**「出现过这个名字」—— 后者挡不住「barrel 行被降级成 import」这种形态)。带两条反空转保险(导出数 ≥100、需求数 ≥50),并**反向验证**过两种注入(删掉导出 / 降级成 import,都稳定变红)。③ 写这条守门时自己踩了同一个坑的**第三种方向**:解析 `export { … }` 列表时按 `,` 切分,而列表里恰好有一段解释性注释 —— 注释里的逗号把名字切碎、注释里的右花括号提前终止列表,**症状是「源码明明导出了,守门却报缺」**。修法同 v0.30.45/48:**先剥注释再解析**(顺序照旧先剥行注释)。⇒ 「对源码文本做结构断言先剥注释」这条纪律,本仓库已经交了**四次**学费(v0.28.12 假红、v0.30.45 假绿、v0.30.48 假红、v0.30.49 解析器自己出错)。
|
|
54
|
-
- **v0.30.48**(2026-09-30):`src/index.ts` **955 → 724** —— 新模块 `index-routes.ts`(381):**路由 / admin 装配阶段整片**(`RouteDeps` + `AdminDeps` 两个对象与两张路由表的注册,收进 `createRouteStage(deps)`)。**这是 `index.ts` 拆分第二轮**:v0.30.42 先切走了「纯」的配置面,这一轮切走的是**装配阶段里唯一自成一段**的部分 —— 读入口现在能一眼看到生命周期(farm → prompt/tools → 启动任务 → 路由 → 拆卸),而不再被 120 行 accessor 接线打断。三条刻意不动的东西:① **`disposed` 仍归入口**(它的主人是拆卸 effect 与启动任务),唯一需要它的那处守卫改为**传入谓词** `isDisposed()`;② **变更锁仍由入口建、并把同一个实例传进 stage**(v0.30.12:整插件一把锁)—— 顺手删掉一个**从未被读的依赖** `GitLayerDeps.isDisposed`,README 式的「按行删 import」交给 `noUnusedLocals`(一次点名 23 处,零猜);③ **注册时机一字未改**(`ctx.inject(['webServer'])` 仍在 wikiViews 之后、拆卸 effect 之前 —— 顺序没有重排)。⚠️ 守门同步一处:`verify-mutation-lock` 的「只建一把锁」计数改成**按模块族**统计,并给该判据**剥注释** —— 新模块的说明文字里写了一句 `createMutationLock()`,未剥注释时会被当成**第二个调用点**(v0.30.45 那种「断言被自己的文档满足」的镜像:这次是假红)。
|
|
55
|
-
- **v0.30.47**(2026-09-30):`src/client/note-widget.ts` **588 → 573** —— 新模块 `note-widget-foot.ts`(73):**底栏那一组**(左组:上传按钮 / 「🕘 最近」/ `Ctrl+Enter` 提示;右组:「✏️ 在 TW 中编辑」/「保存」)。这一片是**零卡片状态**的纯布局(与 v0.30.43 的 placement 片同类):依赖面只有「一个已经建好的上传按钮」+「`recent.toggle` 的函数引用」,把 `{ foot, footLeft, footRight, saveBtn, editBtn }` 交回卡片 —— 两个按钮的 `click` 处理器**仍绑在卡片里**(紧挨着实现它们的 `note-widget-save.ts`),因为「谁实现动作」与「谁画按钮」是两件事。`footLeft`/`footRight` 两个 class 一并交回,因为 `.dsh-tw-note-foot-right` 的 flex 规则(右贴边 + 装不下时整组换行,v0.22.6 的按钮压成两行事故)是**契约**,卡片还要往里 append。
|
|
56
|
-
- **v0.30.46**(2026-09-30):`src/client/note-widget.ts` **615 → 588** —— 新模块 `note-widget-upload-ui.ts`(82):上传的**接线**那一组(「上传」按钮 + 隐藏的 file input + 拖放到编辑器上,连同它自有的 `dragDepth`)。分层刻意分成两个文件:`note-widget-upload.ts` 是**动作**(`uploadInto`:校验大小 → POST → 在光标处插入 Markdown 行 → toast),`note-widget-upload-ui.ts` 是**接线**(File 从哪来、何时给编辑器挂 `.dsh-tw-note-drop` 高亮)。分界线:整组只问卡片要两样东西 —— 编辑器(拖放目标 + 插入点)与 `wikiQuery`(附件必须落在**这张卡片的目标库**里),卡片侧不再持有任何上传状态(`footLeft.append(upload.button, …)` 一行替代原 36 行)。
|
|
57
|
-
- **v0.30.45**(2026-09-29):**`note-widget.ts` 拆分第四轮 —— 抽出「保存 / 在 TW 中编辑」整组(677 → 615 行),并在同一轮**抓出一条被注释满足的假绿守门**。① 新模块 `src/client/note-widget-save.ts`(187 行)拿走 `saveDone` · `saving`(在途互斥)· `doSave` · `doEdit` —— 这四样是 `build()` 里唯一真的"说话"的一段(发请求、读回执、弹 toast、改按钮状态与文案)。**分界线(可复用)**:先把这段代码**读写到的闭包状态**列出来,再逐样决定怎么传 —— 它自带的 `saving` **跟着函数走**(同 v0.30.44);卡片**留在原地**的四样各有理由:`close()` 是卡片自己的可见性收尾、`postEditAndOpen()` **两条路径共用**(`doEdit` 与 `openNative`)所以必须住在两者都看得见的地方、`loadedToken` 的**主人是「🕘 最近」picker**(它写、这里只读)⇒ 按 getter/setter 传、`scope` 这里只读**一个**方法(`stoppedTarget()`)⇒ 传函数而不是整个模块。② `const doSave = async (): Promise<void> => {` 这一行**逐字保留**:`verify-wiki-focus.mjs` 用 `indexOf(那句签名)` 定位函数体、再断言其后 900 字符内出现 `stoppedTarget()`(=「卡片保存同样要拒绝未运行的目标库」,v0.29.0);它是**族读**(`readFamily('src/client/note-widget')`)⇒ 搬到新文件仍命中。③ ⚠️ **本版最重要的一条:那条判据在搬迁后变成了假绿,是反向验证抓出来的。** 第一次跑两种注入(**删掉整个 `stoppedTarget()` 拒绝分支** / **把 `doSave` 改名**)——`verify-wiki-focus` **两次都照样绿**。根因:它是拿**未剥注释**的族文本做 `indexOf`,而新模块的**模块头注释**逐字引用了那句签名、还在同一段里提到 `stoppedTarget()` ⇒ `indexOf` 先命中**注释**,900 字符窗口里出现的 `stoppedTarget()` 也来自注释,**断言被自己的文档满足**(这条注释本身是为了解释"为什么这一行不许改名",属于"越写越安全"的反面)。修法:`verify-wiki-focus.mjs` 新增 `stripComments()`(**先剥行注释、再剥块注释** —— 顺序承重,v0.30.14 在 `verify-client-i18n` 里踩过反序吞代码的坑),那条测试的五条断言全部改跑在剥注释的源码上。**加固后重跑同样的两种注入 ⇒ 两次都稳定 FAIL**(删分支 ⇒「卡片保存同样要拒绝未运行的目标库」;改名 ⇒「找不到 doSave」),还原后文件哈希与注入前**逐字节一致** ⇒ 绿。⇒ 结论:`grep`/`indexOf` 型判据在**拆分**这件事上尤其脆 —— 模块头注释几乎必然会引用被搬走的那句签名,所以凡"定位函数体 + 窗口内找符号"的断言,**必须剥注释**。④ **其余守门一条都不用改**(逐个核实,不是猜):`verify-frame-guards` 按模块族读并在测试里剥注释行,`dispose()` / `open()` / `openNative()` / `installCardDrag()` / `positionCard()` 都留在卡片里;`verify-wiki-focus:770` 那条**按精确路径**读卡片、钉 `const mark = …note.defaultMark…` 的断言同样不受影响(被断言的那行没离开卡片)。⑤ 一个**诚实的差异**写进模块头(不改变可观察行为):旧代码直接闭包引用 `editor`/`titleInput`/`tagEditor`/`saveBtn`/`editBtn`,新代码经 `deps.getUi()`,于是两个函数各多一条 `ui === undefined` 早退(实践中不可达 —— 两者都由 `build()` 建出来的按钮与编辑器键位触发,写出来只为类型收窄)。**验证**:`typecheck` / `build`(client bundle 770,645 字节,仍 minify)/ 客户端五守门(`verify-frame-guards` / `verify-wiki-focus` / `verify-tw-origin` / `verify-client-i18n` / `verify-frame-surface`)/ `verify-editor-popup` / `verify:unit`(退出码 0)/ 全量 `npm run verify` 四档退出码 0(E2E / WIKI SWITCH / WIKI FARM BOOT / LARGE WIKI CHECKS 全绿)/ `verify-line-endings`(260 个文本文件全 LF)/ `verify-version-consistency` 全绿。
|
|
58
|
-
- **v0.30.44**(2026-09-29):**`note-widget.ts` 拆分第三轮 —— 抽出「草稿生命周期」整族,连同它持有的两个状态一起搬走(750 → 677 行)**。① 新模块 `src/client/note-widget-autosave.ts` 拿走 `resetTitle` / `flushDraft` / `scheduleDraft` / `hideDraftBanner` / `resetForNewNote`,**以及它们闭包里的 `draftTimer` 与 `persistedSignature`** —— 这一轮与前两轮都不同:**状态跟着函数整片搬走**,卡片只经模块返回的句柄读写(`resetTitle` / `flush` / `schedule` / `hideBanner` / `resetForNewNote` / `clearTimer` / `setPersistedSignature` / `markPersisted`)。② **为什么这次状态必须走**(而不是像 v0.30.41 的 scope 片那样只传访问器):`verify-frame-guards.mjs` 把两条不变量**按名字**钉在 `resetForNewNote` 的函数体里(`/clearTimeout\(draftTimer\)/` + `/persistedSignature\s*=/`),它们编码的正是 v0.22.8 的「幽灵草稿」事故 —— 「保存 / 丢弃后重置编辑器」必须①取消防抖定时器、②把这段空内容记成"已定型",否则 500ms 后那次防抖会把 `{text:'', title:'<时间戳>'}` 写成草稿、下次打开对着空编辑器报「已恢复未保存草稿」。把变量与函数整片搬走 ⇒ 两处字面量**逐字保留**、**一个守门都没有改**;反过来「改名成 `draft.timer` 再放宽断言」会把判据从「钉不变量」降级成「钉新名字」,那才是真正的损失。③ 卡片侧保留 `flushDraft` / `scheduleDraft` / `hideDraftBanner` / `resetTitle` 四个**局部别名**(`const { flush: flushDraft, … } = autosave`):这些名字在 `build()` / `dispose()` / `open()` 里有十几处调用,改名只放大 diff、不增加信息(v0.30.38 那条「优先抽取、少改名」)。④ 顺带删掉一处**只写不读的死状态** `autoTitle`:自 v0.29.0 起 `flushDraft` 的判据已从「正文为空 **且** 标题是自动生成的那个」升级成「正文为空」(守门还专门断言它不得再引用 `autoTitle`),于是这个变量只剩赋值 —— 把一个只写不读的变量搬进新文件等于把债务搬进新家。⑤ 两个**诚实的差异**写进了模块头,都不改变可观察行为:`getUi()` 可能返回 `undefined` ⇒ 模块里多了早退(旧代码直接闭包引用 `editor`/`titleInput`/`tagEditor`,那时卡片必然已 `build()`,真为 undefined 会抛 TypeError);`opened` / `disposed` **刻意不进**新模块,按谓词传进去(它们属于卡片的可见性生命周期,与 `note-widget-recent.ts` 同款处理)。⑥ **反向验证**:删掉模块里 `resetForNewNote` 的 `clearTimeout(draftTimer)` ⇒ `verify-frame-guards` 稳定 FAIL 在「保存/丢弃后重置必须取消防抖并记录已定型签名」;还原后文件哈希与注入前**逐字节一致** ⇒ 绿。**验证**:`typecheck` / `build`(client bundle 770,128 字节,仍 minify)/ 客户端五守门(`verify-frame-guards` / `verify-wiki-focus` / `verify-tw-origin` / `verify-client-i18n` / `verify-frame-surface`)/ `verify:unit`(42 套,退出码 0)/ 全量 `npm run verify` 四档退出码 0 / `verify-line-endings` / `verify-version-consistency` 全绿。
|
|
59
|
-
- **v0.30.43**(2026-09-29):**`note-widget.ts` 拆分第二轮 —— 抽出「卡片摆在哪 + 怎么拖」,817 → 750 行;顺手修掉一批写错版本号的注释**。① 新模块 `src/client/note-widget-placement.ts`(109 行)拿走两件事:`positionCard(root, card, anchor?)`(有锚点就贴在输入框上方、否则右下角,视口夹取)与 `installCardDrag({ root, card, head, closeBtn })`(标题栏拖动,pointer events + `setPointerCapture`)。**分界线怎么定的**:问「这段代码需要卡片给它什么」—— 两半都**只碰 DOM 元素和自己私有的手势状态**(`dragState`),对 wiki 名册 / 草稿 / 保存 token **零依赖**,所以一个访问器、一个 setter 都不用传(与前两轮的 `note-widget-scope.ts` 恰好相反:那边七个状态必须用访问器过桥)。这轮也因此**没有一个守门需要放宽** —— 全仓库 `grep` 不到任何 `dragState`/`applyDrag`/`pointerdown` 的断言。② 两个「别顺手清理掉」的语义已写进模块头:**`positionCard()` 每次 open 都跑**,所以拖过的位置**刻意不跨 open 保留**(下一次打开重新贴锚点);拖动的监听器挂在 `dispose()` 会移除的 DOM 上,所以**故意没有卸载步骤**。③ **补了一条守门**(`verify-frame-guards.mjs`,「每次 open 都重新定位 + 拖动接线只有一份实现」):前半钉 `open()` 里那次调用必须带上锚点(谁改成"记住位置"就是改了行为),后半**按精确路径**单独读卡片本体、断言 `setPointerCapture`/`pointerdown`/`dragState` 都**不在**卡片里(族读会把模块拼进来、分不出谁实现的 —— 这里正是"族读"与"精确读"必须各用一次的场景)。**反向验证**:去掉锚点参数 ⇒ 红;把 `dragState` 塞回卡片 ⇒ 红;还原后文件哈希与原文件一致。⚠️ 写这条守门时踩到 `bodyOf()` 的一个坑:**签名里别带 `{`** —— `bodyOf` 会自己跳到函数体的第一个花括号,签名带上 `{` 它就从那里往后找**内层**块,取到的不是整个函数(第一次断言因此假失败,已把这条写进脚本注释)。④ **顺带修掉 7 处写错版本号的注释**:上一轮(v0.30.41)的 `note-widget-scope.ts` 拆分被注释成了「v0.30.42」——`src/client/note-widget-scope.ts:4`、`src/client/note-widget.ts:36/111`、`scripts/verify-tw-origin.mjs:169`、`scripts/verify-wiki-focus.mjs:314/345/791` 全部改回 **v0.30.41**(`git log -S` 确认它们确实随 `d2417c5` 落地)。**教训与 v0.30.42 的 barrel 那条同源**:注释里的版本号/文件名/公共面**没有任何自动化校验**,只能靠人复核(顺带说明:这类"注释说的版本 ≠ 实际落地版本"不会让任何关卡变红)。**验证**:`typecheck` / `build`(client bundle 769,654 字节,仍 minify)/ 客户端五守门(`verify-frame-guards` / `verify-wiki-focus` / `verify-tw-origin` / `verify-client-i18n` / `verify-frame-surface`)/ `verify:unit`(42 套,退出码 0)/ 全量 `npm run verify` 四档退出码 0 / `verify-line-endings` / `verify-version-consistency` 全绿。
|
|
60
|
-
- **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` 全绿。
|
|
61
|
-
- **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` 全绿。
|
|
62
|
-
- **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` 全绿。
|
|
63
|
-
- **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` 全绿。
|
|
64
|
-
- **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` 全绿。
|
|
65
|
-
- **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` 全绿。
|
|
66
|
-
- **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` 全绿。
|
|
67
|
-
- **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` 全绿。
|
|
68
|
-
- **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` 全绿。
|
|
69
|
-
- **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` 全绿。
|
|
70
|
-
- **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 套,含新守门)全绿。
|
|
71
|
-
- **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/` 行为不变)。
|
|
72
|
-
- **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` 全绿。
|
|
73
|
-
- **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` 全绿。
|
|
74
|
-
- **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`)亦绿。
|
|
75
|
-
- **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` 亦绿。
|
|
76
|
-
- **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`)全绿。
|
|
77
|
-
- **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` 全绿。
|
|
78
|
-
- **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` 全绿。
|
|
79
|
-
- **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` 全绿。
|
|
80
|
-
- **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` 全绿。
|
|
81
|
-
- **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` 全绿。
|
|
82
|
-
- **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` 全绿。
|
|
83
|
-
- **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` 全绿。
|
|
84
|
-
- **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` 全绿。
|
|
85
|
-
- **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` 全绿。
|
|
86
|
-
- **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 套)全绿。
|
|
87
|
-
- **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 套)全绿。
|
|
88
|
-
- **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` 全绿。
|
|
89
|
-
- **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` 全绿。
|
|
90
|
-
- **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` 全绿。
|
|
91
|
-
- **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)全绿。
|
|
92
|
-
- **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` 全绿。
|
|
93
|
-
- **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` 全绿。
|
|
94
|
-
- **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」的形式暴露(本轮正是这样被抓到的),所以守门改完必须**立刻单跑一次**。
|
|
95
|
-
- **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` 全绿。
|
|
96
|
-
- **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 个仍未转(清单在脚本里,逐文件转换即可)。
|
|
97
|
-
- **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` 全绿。
|
|
98
|
-
- **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 套)全绿。
|
|
99
|
-
- **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 个符号的断言)全绿。
|
|
100
|
-
- **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` 全绿。
|
|
101
|
-
|
|
102
|
-
- **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` 全绿。
|
|
103
|
-
|
|
104
|
-
- **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)。
|
|
105
|
-
|
|
106
|
-
- **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 拆出的模块、"重启只有一条路"按新实现改写。
|
|
107
|
-
|
|
108
|
-
- **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)都稳定变红,恢复即绿。单库安装行为逐字不变。
|
|
109
|
-
|
|
110
|
-
- **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 就是漏一次检查」。
|
|
111
|
-
|
|
112
|
-
- **v0.28.12**(2026-09-29):**「默认库」不再是一个名字:哪里都直接用被设为默认的那个库的真实显示名**(作者要求:「设置了默认库之后,在其他所有的地方不出现默认库的字样……开启会话时默认选中的就是被设置成默认库的库,而不是显示默认库这个选项,这个选项都不该在下拉列表中出现」)。① **会话选择器**(输入框上方那一行):删掉那个无名的 `value=''`「默认库」选项;默认那个库直接用自己的显示名排在最前,后缀「(默认)」只作**身份标记**;新会话打开时它已经是选中项 —— 靠 `/session/wiki` 返回的 `resolved{id,label}`(宿主解析出来的"这个会话实际用哪个库"),没显式选过就显示它。选中默认那一项 = 清空显式作用域(仍然跟随默认),所以以后改了默认库、没动过选择器的会话会跟着走(语义与旧的无名选项**逐字一致**,只是不再有那个名字)。② **快速笔记「写入」目标**:同样去掉名为「默认库」的占位项(清单里本来就有那个库本身),只给默认库加「(默认)」标记。③ **设置页剪藏目标**:空值那一项就地改名成默认库的显示名 +「(默认)」,并且**跳过**它自己那一行(否则同一个库在下拉里出现两次);读不到清单时给中性说法「(跟随默认的那个库)」,绝不退回「默认库」。④ **设置页「运行模式」**:`单库(只跑默认库)` → `单库(只跑 <那个库的名字>)`,切单库的确认文案同样直呼其名。⑤ 顺带清掉两处宿主日志里的「默认库」措辞(共用仓库的 git 冲突、剪藏回落),插件说明 seed 里那句也改成「运行模式、库清单、以及哪个库是默认的」。**守门**:`verify-wiki-focus.mjs` 新增一条——把四个界面模块的**注释剥掉**后断言代码里不存在「默认库」字样(注释当然要能讲这段历史),并逐条断言会话选择器读 `resolved`、带「(默认)」标记、默认选中、选中默认项发 null,快速笔记无占位项,剪藏下拉改名且不重复列默认库,单库模式文案直呼其名。单库安装行为逐字不变。
|
|
113
|
-
|
|
114
|
-
- **v0.28.11**(2026-09-29):**修两个多库界面的真机报障(卡片链接开错库、新会话两个选择器)+ README 不再放版本记录**(三条都是作者当天报的)。① **点卡片上的「在 TW 打开」跳到最后打开的那个库** —— 卡片把会话作用域(`wikiId`)塞进了 `openTiddler` 的事件 detail,但 `panel.ts` 的事件处理**只读了 title**;而面板与右栏都按 GUI 的**焦点库**加载 `/tw/<id>/`,链接的库被整个丢掉(`undefined` 当时还同时表示"默认库"和"未指定",根本区分不开)。现在 `TwWikiTarget` 把三者分开(`string` = 该库 / `null` = 明确默认库 / `undefined` = 未指定、跟随焦点库):卡片与列表行传 `null` 而不是省略;`panel.ts` 拿到 `wiki` 先 `setFocusWiki()` 再路由(两个 TW 界面都按焦点库加载),并把目标库透传给右栏 tab 与中央面板。内核补齐**换库时序**:`wikiSwitchPending` 期间不落 hash —— 否则 hash 会写进正在被替换的旧文档,随后整页重载把它连同 `pendingHash` 一起吞掉(表现为"停在库首页、笔记没打开")。全局链接拦截面同样不再把裸 `/tw/#标题` 一律当默认库:链接自带 `/tw/<id>/` 就用它,裸路径取**所在卡片 / 会话汇总面板**的 `data-dsh-tw-wiki`(工具卡与会话「知识库」Tab 的根节点新增该属性),两者都没有才跟随焦点库。② **新会话出现两个知识库选择器**(其中一个与快速笔记同一行)—— 根因是 v0.28.8 给 `conversation.input.selector.context` 配的**「2 秒没等到就回落到 dock」**:回落会往 `conversation.input.dock` **再注册一个 blank-only 条目**,而 dock 里本来就有同一个选择器(v0.28.7 起由快速笔记条目的 `scope` 参数渲染在同一行)——本机 shell 恰好不声明 `selector.context`(`app.asar` 里连这个字符串都没有),于是回落成了常态、两个选择器同时渲染。现在**删掉回落**(dock 里那个才是唯一常驻的家),并把"谁在用"做成显式协议:`scope-seat.ts` 用 `isBlankSeatMounted()` 公布 context 那一格是否已挂上,dock 里的选择器在**空白会话 + 那一格在挂着**时返回 null 让位(`useSessions` 随之从快速笔记条目透传给子组件,否则子组件认不出空白会话、让位不了)。③ **README 不再放版本记录**(作者要求:只放最新最重要的信息,版本历史单独一个文档)—— 删掉 README 那一节(内容本来就是 `docs/CHANGELOG.md` 的副本),版本一致性的第三个锚点从 README 改到 `docs/CHANGELOG.md` 顶部第一条,并**反向断言 README 里不得再出现逐版本条目**(防止它随每次发布慢慢长回去)。守门:`verify-frame-surface.mjs` 新增换库时序行为单测(旧文档不得收到 hash、新文档必须收到);`verify-wiki-focus.mjs` 重写选择器座位那条(不许再回落注册 dock 条目、必须有 blankSeat 让位协议)并新增「卡片链接必须带库」的源码级断言;`verify-version-consistency.mjs` 改锚点 + 新增 README 反向断言。单库安装行为逐字不变。
|
|
115
|
-
|
|
116
|
-
- **v0.28.10**(2026-09-28):**修「图标弹层出现在设置弹窗后面、被遮挡」**(作者 v0.28.9 重启后实测报障)。① **根因是层级用错了数**:设置页是 DSH 的 `Modal`,渲染进 portal 且根层就是 `position:fixed; z-index:1000`(`dsh-client-ui-primitives/Modal.module.css` 的 `.root`),而弹层写的是 `z-index:60`。**`z-index` 只在同一个层叠上下文里比较** —— 弹层虽然是 `document.body` 的直接子节点(自己也在根上下文),但 60 < 1000,所以整层被 Modal 盖住:弹是弹了,在设置弹窗**后面**。② **修法**:改为 **1100**。这不是随手挑的"大一点的数",而是 DSH 自己给「锚点在对话框内部的浮层」定的值 —— `Menu.module.css` 的 `.portal`,注释原话:*"Portaled lists must layer above modal overlays (z 1000) — an anchor inside a dialog still expects its menu on top"*。跟它对齐即高于 Modal(1000)、低于我们自己的 toast(10001)。③ **守门**:断言弹层 `z-index ≥ 1100`,并**解析规则体时先剥掉注释**(第一版按字符往后切 400 字符,结果切到了我新写的注释里那句「DSH Modal 的 1000」,解析出 1000 而误报)。已反向验证:改回 `z-index:60` → 红;恢复 1100 → 绿。④ **顺带补 `styles.ts` 模板串守门**:CSS 正文里的反引号会让 `CSS_TEXT` 提前结束,`typecheck` 报 TS1005、而 `lib/` 已被 `clean-lib` 删掉(构建到一半失败)。本仓库踩过两次(v0.25.0 注释里引 `.dsh-tw-settings`;v0.28.9 我在解释 z-index 时又写了一次)。⑤ **这条守门的第一版是错的,值得单独记**:它检查「第一个闭引号之前有没有反引号」—— 可一旦正文多了一个反引号,**那个反引号本身就变成了"闭引号"的位置**,body 在它之前就结束了,于是"body 里没有反引号"**永远成立**:注入反引号反而让断言更容易通过。**用出错的输入去验证出错的解析器,必然假绿。** 改成检查「模板串有没有被提前截断」——正文须以 CSS 注释开头、以 `}` 结尾,且闭引号之后必须紧跟正常模块代码(`/**` / `import` / `const` …)。再注入反引号即稳定变红。⑥ 复核:`typecheck` / `build`(`client.bundle.js` 704856 字节,并确认 `z-index:1100` 进了 bundle)/ `verify:static` / `verify:unit` / `verify:e2e` / `verify:large` 全绿。
|
|
117
|
-
- **v0.28.9**(2026-09-28):**修「点击图标没有正常弹出」**(作者重启后实测报障)。本版只有一个修复,但值得单独发一版:它暴露的是**守门的失效模式**,不是普通的漏写。① **现象**:设置页知识库列表里点「图标」,弹层没有出现。② **根因**:v0.28.8 的 `settings-icon-picker.ts` 生成了完整 DOM(弹层 / 网格 / 格子 / 分页器)、接线也对,**但 `styles.ts` 里一条 CSS 都没有** —— 弹层照样被 append 到 `document.body`,而它是个无宽高约束的裸块、压在页面最底部,视觉上就等于「点了没反应」。③ **修复**:补齐 12 条规则 —— `.dsh-tw-iconpicker`(含 `position:fixed`:弹层挂在 body 上,用 absolute 会被页面滚动带走、也受祖先 overflow 影响)、`min(420px, 100vw-24px)` 宽度与 `max-height` 上限、8 列网格、格子悬停与当前项高亮、名称省略(8 列必须对齐)、分页器分隔线,以及按钮的图标位与文字位。④ **守门为什么没拦住(本版最重要的部分)**:`verify-wiki-focus.mjs` 原本断言的是 `assert.match(settings, /dsh-tw-iconpicker/)` —— 检查「TS 源码里出现过这个字符串」。但**类名当然会出现在 TS 里**(它是 `make('div', 'dsh-tw-iconpicker')` 的参数),所以组件一条 CSS 都没有时这条断言照样绿。**通用教训:断言「某个类名存在」不等于断言「这个组件可用」。** 渲染型组件的守门至少要跨两个文件——一个说「我生成了这个 DOM」,一个说「这个 DOM 有样式」。⑤ **新守门(双向)**:弹层 / 网格 / 格子 / 分页器四个类必须真有 CSS 规则;并且从 picker 源码里**解析出所有 `make()` 生成的 `dsh-tw-*` 类名**,逐个要求它们在 `styles.ts` 里有下落 —— 这条通用网能拦住「下次再加子元素、又忘了写 CSS」。**已反向验证**:删掉那段 CSS → 红;恢复 → 绿。⑥ **新守门当场抓出第二处漏写**:按钮的文字位 `.dsh-tw-settings-iconbtn-label` 同样没有样式。⑦ 复核:`typecheck` / `build`(`client.bundle.js` 701305 → **704155 字节**,并确认 CSS 真进了 bundle)/ `verify:static` / `verify:unit` / `verify:e2e` / `verify:large` 全绿。
|
|
118
|
-
- **v0.28.8**(2026-09-28):**多库第五批反馈(12 条一次做完)+ 拆大文件**。① **图标选择器接上 DSH 官方图标系统**(反馈 1)—— 从 8 个手绘图标扩到 **36 个官方图标**:没有 `require` 上游的 React 图标组件(客户端是纯 DOM,那样会引入运行时依赖、还会让改名对不上),而是把**上游的 path 数据内联**进 `src/client/wiki-icon.generated.ts`(由 `scripts/gen-wiki-icons.mjs` 生成,`scripts/extract-dsh-icons.mjs` 是取图素材的开发助手);实心图标(data / plugin / settings / dark)与线性图标分两套渲染。弹出网格 **48 个一页**分页,另有 8 个自绘图标与一组 emoji。守门断言 host↔client 的图标名单**双向完全一致**(少一个即红)。② **修「选了图标立刻变回默认」**(反馈 2)—— 根因是 `/admin/wikis` 的**读取视图漏了 `icon` 字段**(`buildWikisView()` 逐字段手写,加字段时漏了一处):写进 `wikis.json` 了,读回来是 `undefined`,客户端拿它重渲染就回落默认图标 —— 症状"完全像没保存"。补上 `...(entry.icon !== undefined ? { icon: entry.icon } : {})`,并加**源码级守门**钉死该字段(去掉修复即红)。③ **多库下设置页按作用域分页**(反馈 3/4/11)—— 十几块面板挤一页,既分不清「改这个库」还是「改整个插件」,改完也找不到退出口。现在分「**总览 / 本库配置 / 全局**」三个 tab;「插件管理(自带官方插件)/ 主题管理 / 语言管理 / 初始化」这些 **per-库** 面板只在你点了某个库那一行的「配置」之后才出现(此前它们和全局项混排,看着像所有库共用一个设置);「配置」按钮变成**开关**(再点 = 退出配置),顶部「配置作用域」条一直显示正在改哪个库。**单库安装没有 tab 条**,就是原来那一页。④ **新会话也能选知识库**(反馈 9)—— 选择器此前只长在 `conversation.input.dock`(新会话还没有输入框)。现在挂在**预设/模式选择旁边**:先声明式探测 `conversation.input.selector.context`,2 秒内没等到就回落到 dock,两条路径带 `blankOnly` 互斥,不会同时出现两个。⑤ **剪藏可指定落哪个库**(反馈 8)—— 新增 `bridge.wiki` 配置与设置页下拉(留空 = 默认库)。剪藏来自浏览器书签、没有"当前会话"可以跟随,所以这里必须是**显式钉死**;填了不存在的 id 时 `/clip` 回执会带 `wiki` 字段说明实际落点,插件日志也提示一次。⑥ **回复流卡片与会话「知识库」Tab 按会话作用域取数**(反馈 7/10)—— 卡片取数 / 链接 / 缓存 key / 汇总的存在性探测与渲染全部带上会话作用域的那个库;不这样做会出现"A 库的笔记标题在 B 库里渲染成空白卡片"。工具卡片右上角还会显示库名徽标。⑦ **桌面版能起 TW**(反馈 12)—— TW 子进程需要真正的 `node`,而桌面版里 `process.execPath` 是 Electron。现在按 `DSH_TIDDLYWIKI_NODE` → `where node` / `which node` → `process.execPath` 的顺序解析并缓存,启动日志写明用了哪个;新增 `scripts/verify-desktop-node.mjs` 做**行为级**断言(进 `verify:unit`)。⑧ **复核后确认无需改动的两条**:注入提示词**本来就是按会话求值**的(`systemPrompt.section` 的 text 是函数,DSH 每次装配传入该会话的 agent,插件据此读作用域库的 `prompt.*`)—— 在输入框上方换库,**下一步**就生效,不需要重启或新开会话;15 个 `tiddlywiki_*` 工具在多库下全部正常(可见库 >1 时每条回执由一处包装统一标明库名)。单库安装两条都是逐字节不变。⑨ **代码清理**(反馈 5)—— `tools.ts` 1913→132、`admin.ts` 1265→24(barrel)、`index.ts` 1407→1001、`note-widget.ts` 1245→916、`settings-page.ts` 1815→400,**全部纯搬移**(15 个工具的注册顺序、`apply()` 的装配顺序、所有注释与字符串逐字保持)。配套新增 `scripts/lib/source-family.mjs`:`readFamily()` 把 `<base>.ts` 与 `<base>-*.ts` 拼成一份文本,12 个守门脚本改用它 —— **以后纯搬迁不再假红**(断言的是"规则"而不是"路径")。`verify-frame-guards` 的 runSwitch 断言跟着新形状更新(`disposed` → `deps.isDisposed()`),并顺手收紧成必须命中**那一条**消息(旁边 `repointDefault` 的「已取消修改」曾因前缀相同而误判通过),两条都做了反向验证。⑩ **文档**:`docs/usage.md` 补 `bridge.wiki` / 设置页分页 / 新会话选择器 / 每个库一个图标 / 桌面版 node;`docs/features.md` 补四条能力;README 顶部版本记录同步。**验证**:`npx tsc --noEmit` 全绿、`npm run build` OK(`client.bundle.js` 701305 字节 / 684.9 KB,远低于 900KB 闸门)、`verify:static` / `verify:unit`(26 个脚本)/ `verify:e2e` / `verify:large` 全绿(含 `WIKI FARM BOOT` 真起两个 TW 子进程、`LARGE WIKI` 3100 条 64s)。
|
|
119
|
-
- **v0.28.7**(2026-09-28):**会话知识库选择器改成与「快速笔记」同一行 + 单库专属配置项不再在多库下出现**(两条都是作者报障)。① **选择器"没和对话框对齐"** —— 上次的修法方向就错了:`conversation.input.dock` 是**竖向 flex 列,一个条目独占一整行**,选择器和快速笔记是**两个条目**时永远是上下两行,怎么调内边距都不可能并排。现在选择器不再是独立条目,而是由快速笔记条目通过 `scope` 参数渲染在**自己这一行里**(按钮前面),整行统一与输入框右缘对齐,一行只测一次。顺手修掉合并时的一处遗留:选择器组件里还留着上一版的 `alignDockEntry` 自测,并入同一行后会把右内边距补两遍——已移除(`wiki-scope` 也从客户端 slot 表里消失,回归由 `verify-wiki-focus.mjs` 断言"input.dock 只能有一个条目"守住)。② **「侧边栏 TW 入口显示名称」在多库下不该出现** —— 多库时侧边栏是每个库各占一行、各用自己 `wikis.json` 里的显示名,`ui.sidebarLabel` 只喂单库那一行,所以多库下它是个**改了不生效的假配置项**。现在该字段只在单库模式渲染(多库时换成一行说明"去知识库列表改各自的显示名");模式探测失败**回落成单库**(保守:宁可多给一个真能用的字段,也不要因为一次请求失败把设置项藏了),并反向断言不许写成 `!== 'single'`(那会把未知模式误判成多库)。两条守门断言都做了反向验证。
|
|
120
|
-
- **v0.28.6**(2026-09-28):**修两个多库界面的真机报障**。① **「插件管理 / 主题管理 / 语言包 / 初始化」看起来是混在一起管理的** —— 宿主侧一直是 per-request 解析目标库的(`deps.server(req)`/`config(req)`/`getClient(req)`),缺的是**客户端那一半**:设置页只有 `state`/`config`/`prompt` 三处带上了 `?wiki=`,而**插件信息与初始化 seed 的调用没带**,于是它们永远落在默认库上,「切了库、面板内容却没变」。现在 7 个 per-wiki 端点全部经 `withWiki()`,并有守门逐个断言(去掉任一处的作用域 → 红)。单库(未选库)时 `withWiki` 原样返回 URL,现有用户逐字不变。② **左侧入口点一个却三个都高亮** —— 我上一版把「面板是共享的」错解成「所有行一起高亮」,但面板一次只显示一个库(当前焦点库)。现在只有那一行点亮,并订阅焦点变化重算;单库默认行在面板打开时仍正常高亮。两条守门断言都做了反向验证。
|
|
121
|
-
- **v0.28.5**(2026-09-28):**修「多库模式下侧边栏多出一个没有库名的 `TiddlyWiki` 入口」**(作者报障)。根因是**同一个 CSS 陷阱踩了第 12 次**:`hidden` 属性在 CSS 里只是 `display:none`,而 `.dsh-tw-entry` 有显式 `display:flex` —— **显式 display 盖掉了 hidden**,于是那行让位给每库入口的默认行照样渲染。这次不再给它单独补一条规则,而是**收敃成一条全局兜底** `[hidden] { display: none !important; }`,并删掉本仓库历史上为此在不同元素上各补的 **12 条逐元素规则**(面板 iframe / 错误块 / 快速笔记 / FAB 菜单与提示 / 右侧栏 / 目标库下拉 / 草稿栏 / 最近列表…)。守门同时改为断言「全局兜底存在 + 不允许再出现逐元素版本」,并反向验证(把全局规则改成挂在类名下 → 红)。
|
|
122
|
-
- **v0.28.4**(2026-09-28):**每库自定义图标 + 修「右键菜单切库后选中项不更新」**。① **右侧「知识库」菜单的选中项停在旧值** —— 根因是选中标记(`●`/`○`)只在**菜单构建时**算了一次,之后无论怎么切都不重画;现在改为从当前焦点重算,并在 `setFocusWiki` 订阅里即时重画、每次打开菜单前再兜一次(切库也可能来自侧边栏入口或快速笔记卡片)。② **每个库的入口图标可自定义**:`wikis.json` 的条目新增可选 `icon` 字段,值可以是内置图形名(book / briefcase / home / notebook / flask / globe / star / archive,各是一枚继承 `currentColor` 的手写 16×16 线性 SVG,两套主题下都对)或**一个短 emoji**(📚 / 💼 / 🏠 …,用来一眼区分四五个库,不用下载任何资源)。设置页「知识库列表」每行多了图标下拉(当前值若不在候选里会保留,不会被抹掉)。校验口径:名字或 ≤8 字符的可打印短串,超长与控制字符一律拒绝;空值 = 清除自定义回落默认。**emoji 用 `textContent` 渲染而不是 `innerHTML`** —— 这个值来自用户可编辑的文件,绝不能变成注入点。缺省不设 = 内置默认图标,升级用户看到的一模一样。
|
|
123
|
-
- **v0.28.3**(2026-09-28):**修三个真机报障 + 插件说明补「多库 / 升级」两节**(作者拆库实测中发现)。① **单库模式下快速笔记露出一个没有选项的「写入」下拉** —— 根因是 **CSS 的 `hidden` 属性只是 `display:none`,任何显式 `display` 都能盖掉它**:我给 `.dsh-tw-note-wiki` 写了 `display:inline-flex`,于是 `hidden` 失效,那个空下拉照常渲染。修法两层:CSS 显式加 `[hidden]{display:none}`(且必须写在设 `display` 的那条**之后**,同优先级靠后者生效)+ 组件侧兜底(单库时 `disabled` 并直接 `display:none`)。② **会话级知识库选择器没有和输入框对齐** —— 根因是**规则只写在了一个使用者的内部**:找 composer 输入卡片、按差值补 `paddingRight` 的整套测量逻辑原本长在 `quick-note-dock.ts` 里,第二个 dock 条目加进来时没人知道要对齐。已抽成 `client/dock-align.ts`(`alignDockEntry`)两处共用,并守门断言「条目内不得再自带测量实现」。③ **左侧入口按库显示** —— 多库时每个**在运行**的库一个入口行、用各自的显示名;点该行**先切焦点库再开面板**(否则点 A 打开 B);当前焦点库有左侧标线。单库安装仍走原来那一行 + `ui.sidebarLabel`,逐字不变。④ **插件说明新增两节**:升级用户最大的困惑是"装完了不知道有这个东西",所以明确写了——升级后**不会自动变成多库**、行为与升级前一致;想用多库去哪点;`wikis.json` 在哪;旧 `location.json` 在哪些模式下还权威;每个库要有自己的 `tiddlywiki.info`。守门 `verify-wiki-focus` 扩到 19 条(`hidden` 那条已反向验证)。
|
|
124
|
-
- **v0.28.2**(2026-09-28):**设置页去掉重复的「知识库位置」**(作者反馈:多库配置做完后它重复且逻辑冲突)。这一块的语义是「把**唯一的那个库**换到别处」——写的是 wiki 之外的指针文件 `location.json`,措辞也是单库时代的("切换到这个位置" / "恢复为配置默认")。现在**按模式二选一**:**单库模式**保留它(那里它是唯一正确入口);**多库模式整块不渲染**,某个库换目录收到「知识库列表」那一行的**「改目录」**(默认库经 `/admin/wiki/switch` → 改清单 + farm 收敛 + 校验;非默认库直接 `update`)。宿主接口**没有**冲突也**没有**改动:`/admin/wiki/switch|reset` 从 v0.28.0 起就按模式分派。守门:`verify-wiki-focus` 新增"多库模式下不得渲染该块"的断言;farm-boot 补上 location 面接线(22 条),保证藏了界面之后**接口仍按模式正确分派**。
|
|
125
|
-
- **v0.28.1**(2026-09-28):**修「拆库 skill 在 wiki 启动慢 / 起不来时永远装不上」+ 快速笔记的目标库可切换(R7)**。① **真实事故**(作者升级 0.28.0 后重启宿主时发现,测试没拦住):技能安装写在了 `farm.startAll()` **之后**,而作者的知识库很大、TW 冷启动要几十秒 —— 于是"技能装没装"取决于"wiki 起得快不快";更糟的是 wiki 若因配置问题**永远起不来**,技能就永远不装,而它跟 wiki 毫无关系(只往 `$DSH_HOME/skills/` 写一个文件)。已挪到启动任务**最前面**,并新增**顺序守门**:`verify-skill-install` 断言 `installSplitSkill()` 出现在 `farm.startAll()` 之前(已反向验证:在安装前插一次 startAll → 该条红)。② **R7 快速笔记目标**:卡片新增「写入」目标库,**默认跟随 GUI 的焦点库**(你在看哪个库,快速笔记就进哪个库),卡片内可临时改且改过之后不再被焦点带走。设计的核心是**整张卡片一起换库** —— 标签建议(memo 以 URL 为键,换库自动重读)/ 最近列表 / 草稿读取 / 附件上传 / 保存 / 随后弹出的 TW 编辑器,六处全部经 `withWikiQuery()`(`?wiki=<id>`)与 `twProxyFor()` 定向;只改保存那一处会产出"标签列表来自 A、笔记写进 B",比不做还糟。目标库没在运行时卡片会**先讲清楚**("先在右下角「知识库」菜单里打开它"),而不是让用户写完一段再失败;这里刻意**不自动启动** —— 启动是「知识库」菜单的职责,保存路由顺手 spawn 进程不该由一次误点触发。单库安装不显示选择器、请求 URL 逐字不变。守门 `verify-wiki-focus` 扩到 16 条(含 `withWikiQuery` 的四条纯逻辑断言与"整张卡片同库"的源码级断言,已反向验证);`verify-tw-origin` 的弹窗接线断言跟着间接层更新(保留强度)。
|
|
126
|
-
- **v0.28.0**(2026-09-28):**多知识库**——同一进程内可同时运行多个 TiddlyWiki,每个库独立配置、独立 git,Agent 侧按**会话**选择作用域,且每个库可对 Agent **彻底隐身**。起因是作者自己的库:`书籍` 语料 2468 条与日常笔记挤在一个 800MB 仓库里,Agent 的检索 / lint / backlink 每天在约 **30 倍于工作集**的内容上跑(「先窄后宽」是缓解不是解法)。而拆库必须**同时在线**——只切换位置的话,你切到语料库翻资料时 Agent 也跟着切过去了。**① 三个模块各司其职**:`host/wiki-registry.ts`(清单 / 模式 / 动作规则,纯函数)、`host/wiki-instance.ts`(**一个**库的运行时:TW 子进程 + REST 客户端 + 配置覆盖层 + fs 监听 + 自身自举)、`host/wiki-farm.ts`(**谁该在跑**:reconcile 规则 + 会话作用域解析);`apply()` 从"抱着一个实例"变成"驱动一个农场"。**② 模式开关**:`wikis.json` 的 `mode: 'single' | 'multi'`,**缺省 `single`** —— 升级用户的行为逐字不变(位置仍由旧指针文件 > cordis 默认决定,registry 此时只记住模式与候选列表)。**③ 路由按库定向**:`/tw/<id>/…`,且每个库把自己那份同源代理基址 seed 进**各自的** `$:/config/tiddlyweb/host`(TW 用它拼**所有** API URL;不区分 id 就会出现"页面显示 A、每次读写落在 B");裸 `/tw/` 保留为默认库别名(旧链接 / 书签 / 已 seed 的值都不失效)。为此引入**保留 id 名单**(`status`/`files`/`recipes`/`render`… 与 TW 自己的根路径撞名时无法区分,一律拒绝注册)。非代理路由统一吃 `?wiki=<id>`。**④ git 按仓库重构**:多个库可以共用**一个**仓库(按文件夹物理划分)。`git add -A` 在子目录执行也作用于整个工作树,所以自动提交必须**按仓库**去重(`host/repo-committers.ts`,提交在仓库根执行;`dir → repo` 只缓存**肯定**结果——否定结果缓存会让全新安装的自动提交永久静默失效);`pull()` 回传改动路径,**只重启内容真的变了的库**("这次 pull 改了东西"与"这个库快照过期了"是两个问题)。**⑤ Agent 侧**:会话级作用域(`$DSH_HOME/dsh-tiddlywiki/sessions.json`,单选,软失败——读不到就只是"没选过")+ `agentVisible`(**彻底隐身**:不进选择器、不进提示词、工具不可达)。工具按会话路由,且**可见库多于一个时每条回执都标明库名**(一处包装实现,不是改 15 个 render);注入提示词改为**按会话求值**(`PromptSection.text` 支持函数,DSH 每次装配传入该会话的 agent)并读**作用域那个库**的 `prompt.*`——单库安装的 slim 仍是 1918 字符,逐字节不变。**两条刻意的拒绝**:作用域指向隐身库 → 忽略(隐身是硬边界,过期选择不能绕过它);指向可见但**没在跑**的库 → **报错并带上库名**,绝不静默落到别的库(静默换库 = 笔记进了用户没指定的知识库而且看起来一切正常,这是本功能唯一真正危险的失败方式)。**⑥ GUI**:`/status` 回传知识库名册;输入框上方的**会话选择器**(只在可见库多于一个时存在);右下角「知识库」菜单的知识库分组(点开一个没在跑的库会**显式 POST** 启动它——代理路由绝不在 GET 里 spawn 子进程);设置页新增「知识库列表」(模式切换、增删改、启停、设为默认、可见性、开局自启、新增;「移出列表」明确写着**不删目录**)与**显式配置作用域**(每个库的配置在它自己的 tiddler 里,不说清就会出现"改了但不生效")。**⑦ 拆库引导**:`docs/wiki-split.md`(人读 + 顶部一整段**可直接复制给 Agent** 的话)+ 随包 skill `skills/wiki-split/SKILL.md`(启动时装到 `$DSH_HOME/skills/`;**只覆盖带标记的文件**,用户自己写的 skill 一个字节都不碰)。**守门**:新增 6 个脚本——`verify-wiki-registry`(18 条)、`verify-wiki-farm`(12 条,注入假实例钉死 reconcile)、`verify-wiki-farm-boot`(**22 条,真起两个 TW 子进程**)、`verify-session-scope`(13 条)、`verify-repo-keying`(11 条,**真 git 仓库**)、`verify-skill-install`(7 条)、`verify-wiki-focus`(13 条,真读 client 源码),**全部反向验证过**;另有 5 个既有守门(drain / language-pin / frame-guards / ready-policy / tw-origin / conflict-config)跟着间接层更新,同样保留强度并反向验证。**未做**:per-wiki `auth.*`(仍是全局)、快速笔记目标切换(目前写默认库也显示默认库,一致但不是最终形态)。
|
|
127
|
-
- **v0.27.0**(2026-09-28):**重新定位:从「AI 记忆工具」改写成「你和 Agent 共用的本地文档中心」——纯元数据 + 文档发布,运行时行为零变化。** 触发原因:生态里「记忆 / 知识库」这一格已挤进 30+ 个插件(最大的 `MemOS` 已 11,603★),而本插件真正的差异从来不是「更自动的记忆」,而是**人机共用同一份纯文本文件**:人能读、能改、能审计、能随时带走,Agent 用同一套工具读写它。本次把这件事搬上门口那块招牌:① `package.json` 的 `description` 由英文功能清单改为双语定位,并明确写出**不是自动记忆插件、不做后台抓取**,`keywords` 补 `knowledge-management` / `local-first` / `journal` / `publishing` / `wechat`;② GitHub topics 同步补位,便于目录站归类;③ 修正 README 一处过时数字(「Agent 用 10 个工具」→ 15 个)。**无代码改动**——「装完空手能用」(自动建库 + 起步 seed + 快速开始 7 步)自 v0.22.0 起就已具备,本次只是把它讲清楚。
|
|
128
|
-
- **v0.26.7**(2026-09-26):**修「DSH 桌面版里 TW 面板没有『添加条目』按钮」——嵌入式 TW 被渲染在自定义 scheme 上,于是它根本没有同步器**。现象(用户报障,桌面版):TW 面板能看能搜,但页面控件里 **添加条目 / 添加日志 / 添加图片 / 导入 / 条目管理器** 整排不见,单条笔记的 **编辑 / 克隆 / 删除** 也不见,像装了个只读版 TW。**根因不在本插件,而在 TW 内核的一处协议判定 + 桌面版新增的渲染 scheme**:TiddlyWiki 自己的 TiddlyWeb sync adaptor 只在**文档协议以 `http` 开头**时才导出自己(`plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js`:`if($tw.browser && document.location.protocol.substr(0,4) === "http")`);而桌面版用自定义 scheme `dsh-app:` 渲染界面(已在本机运行中的 Electron 渲染进程命令行里核实 `--standard-schemes=dsh-app --secure-schemes=dsh-app …`),于是 iframe 里的 TW **没有 syncadaptor → 没有 `$tw.syncer`**:`$:/status/IsReadOnly` 永远没人写,TW 自己的只读样式表 `$:/plugins/tiddlywiki/tiddlyweb/readonly`(`<$reveal … default="yes">`,**状态未知即当只读**)就把那批按钮 `display:none` 掉;同时 saver 退化成本地 `dirtyTracking`,**在那儿做的编辑根本存不回 wiki**。实测复现(不依赖桌面版):把同一份 wiki 页面用 `file://` 打开(同样非 http),+ 同样消失——所以这不是「按钮坏了」,而是「这个文档里 TW 没有同步器」。**修复思路:让嵌入式 TW 跑在真正的 http origin 上**。① 宿主 `/status` / `/edit` / `/session/summary` 在原有相对字段旁,新增**绝对孪生字段** `twProxyAbsolute` / `twUrlAbsolute`(`absoluteHostBase()`,由请求 `Host` 头推导;脏头一律拒绝 `/^[A-Za-z0-9.\-[\]:]+$/`,避免把路径/凭据拼进回给浏览器的 URL);② 客户端新增 `resolveTwUrl()`(`endpoints.ts`)——**页面本身是 http(s) 时继续走相对路径**(loopback / 局域网 / Tailscale / 域名 / HTTPS 行为一字不变,绝不把 https 页面改源),**只有页面不是 http(s) 时**才改用宿主回环 http 基址,于是桌面版 iframe 里 TW 的协议变成 `http:`,sync adaptor 正常加载、同步器回来、按钮与保存一起复原;③ 随之而来的跨源:桌面版下 TW frame 与宿主页面不同源,`tw-frame.ts` 的 hash 导航把「读跨源 `$tw` 抛 SecurityError」包进 try/catch 并直接走整页兜底(不再冒泡、链接不丢),`theme-sync` 本就是 best-effort 跨源静默。守门:新增 `scripts/verify-tw-origin.mjs`(7 条:Host 头推导 / 脏头拒绝 / http(s) 忽略绝对基址 / `dsh-app:`+`file:` 采用绝对基址 / 老服务端不回归 / 两处接线断言 / 跨源兜底源码断言),`verify-frame-surface` 再增 2 条行为单测(`dsh-app:` 页面必须把 frame 指到宿主 http 基址、且 http 页面仍用同源相对地址;跨源 frame 读 `$tw` 抛错时必须整页兜底)。**注意**:这修的是「TW 被非 http(s) 文档嵌入」;「在浏览器里直连 TW 子端口(`http://127.0.0.1:<port>/`)」是另一回事——那样 TW 的 API 基址会被指到 DSH 代理前缀而 404,同样显示成只读,请一律用 DSH 同源入口(面板 / 右侧栏知识库 tab / `<DSH origin>/dsh-tiddlywiki/tw/`)。
|
|
129
|
-
- **v0.26.6**(2026-09-26):**修 `tiddlywiki_append` 的 `heading` 在 CRLF 笔记上完全失效、且静默回退到文末**(作者本机知识库的一篇 25KB / 454 行长笔记上连续踩两次后定位到源码)。现象:对这篇笔记连做两次带 `heading` 的 append,内容**全部**落到了文末,而回执照样打印「段落「X」」——看起来完全成功。**根因**:`insertIntoSection` 用 `base.split('\n')`,而 CRLF 正文的**每一行结尾都留着 `\r`**;JS 里 `(.*)` **不匹配 `\r`**、`$` 也**不匹配 `\r` 之前的位置**,于是 `/^#{1,6}\s+(.*)$/` 对**每一个**标题都匹配失败 ⇒ `start < 0` ⇒ 走「追加到文末」的回退分支。**只在 CRLF 笔记上复现**(纯 LF 笔记一切正常),所以它看起来像偶发;落盘文件的「**376 个 CRLF + 78 个裸 LF**」混合行尾正是走过该分支的签名(`lines.join('\n')` 保留每行原有 `\r`、只把新块用 `\n` 接上)。**修复三处**:① 标题匹配前**只**剥掉行尾 `\r`(原文行尾一个字节都不动;**没有**改成 `split(/\r?\n/)`——那会让每次 append 都把整篇重写成 LF,炸出全文件 diff);② 新块按**文档自己的行尾**写入(`base.includes('\r\n')` 判定),CRLF 笔记不再越写越乱;③ `insertIntoSection` 改为返回 `{ text, matched }`,`tiddlywiki_append` 把它作为 **`headingMatched`** 一并返回,未命中时回执改为「⚠️ 未找到标题「X」,已改为追加到文末」——**不再把「定位失败」伪装成「写进了那一节」**(Agent 因此完全无法自检,这是本 bug 最坑的一点)。顺带把 `heading` 的真实落点语义写进 schema:实为「该标题之后、**下一个任意级别标题之前**」,标题后紧跟子标题时落在两者之间(=章节开头),并非 schema 原先暗示的「整节末尾」。守门:`verify-tools` +4 条 E2E(CRLF 正文的笔记必须 `headingMatched=true` 且标记落在目标段内、段内不得混入裸 LF;未命中必须 `headingMatched=false` 且回执含「未找到标题」、不得再宣称写进该段落;命中时的反向保护;wikitext `!` 标题同样可定位),**已反向验证**(撤掉 lib 里的行尾 CR 剥离即红 1 条)。
|
|
130
|
-
- **v0.26.5**(2026-09-23):**修 P0:`fields` 参数被按字符拆成几十个垃圾字段**(用户实测复现两次后报障)。现象:`tiddlywiki_put` / `batch_put` / `append` 传 `fields: {"review-after":"2026-12-22"}` 后,条目上出现 `0={`、`1="`、`2=r`… 几十个**单字符字段**,真正的自定义字段**一个都没写进去**;又因为 `fields.type` 是**改内容类型的唯一入口**,这期间也无法把条目写成 wikitext / `text/css`。**根因两层**:① 参数 schema 声明为 `{type:'json'}`,而 DSH 的 schema 里 `type:'json'` 是**纯注解**——编译后连 `type` 都没有(`src/sdk.ts` 的 `compileValue`),模型收不到「这是个对象」的信息,于是把 JSON 当**字符串**发来;② 新建笔记 + 自动工作区标记走 `withWorkspaceMark()` 的 `{ ...(fields ?? {}) }`,而**展开字符串是按字符展开**的(`{...'{"r"'}` → `{0:'{',1:'"',2:'r'…}`),这些索引键随后被当成自定义字段写入;旧 `applyCustomFields()` 对非对象又是**静默 return**,所以全程没有任何报错。**修复(三处)**:① 三个写工具的 `fields` schema 改成真正的 `object` + `additionalProperties: true`(模型能直接看到类型);② 新增 `normalizeFieldsArg()`(`write-policy.ts`)——对象原样返回、`undefined`/`null`/空串视同未给(保留基底)、**JSON 字符串解析成对象**(兼容仍按字符串发参的模型)、数组/数字/解析不出对象的字符串**明确抛错**;`defineTool` 相应新增 `normalizeArgs` 钩子,让归一化跑在 schema 预校验**之前**(放到之后会被「must be an object」先拦掉,兼容层就没机会生效);③ `applyCustomFields()` 遇到非对象**抛错**而不是静默忽略,`withWorkspaceMark()` 只为已归一化的对象做展开。守门:`verify-write-policy` +5 条纯函数(字符串解析 / 对象原样 / 三种坏输入拒绝 / 字符串在 `buildWriteTiddler` 处被拦 / 归一化后无索引键),`verify-tools` +4 条 E2E(三个工具的 schema 必须 `type:object`、按字符串发参仍写对字段且不产生 `0=` 垃圾、坏输入明确报错且不留半成品条目、`append`/`batch_put` 走同一套归一化)。**注意**:本次修复只影响写入端;已被写进条目的垃圾字段需要重建该条目(`fields` 只能覆盖、不能删除单个字段),本机知识库已扫描确认无残留。
|
|
131
|
-
- **v0.26.4**(2026-09-23):**修「发送给 Agent」因配置被浏览器自动填充而 404 + 设置页文本框防自动填充**。实测:设置页「TW 端请求基址」(`ui.sendToAgent.endpoint`)被浏览器自动完成当成 URL/账号字段填进 `/dsh-tiddlywiki/tw/root`,于是 send-to-agent 把请求拼成 `/dsh-tiddlywiki/tw/root/agent/sessions`,命中 TW 代理子路径 → 404「获取会话列表失败」。**两层加固**:① 设置页 `textField` 一律 `autocomplete=off`(`tokenField` 早已如此,URL 类文本框 `endpoint`/`git.remote`/`wechat.dsn` 等不再被自动填充污染);② send-to-agent bundle `baseEndpoint()` 校验自定义 endpoint——一旦含 `/tw/`、`/api/`、`/recipes/`、`/files/` 等 TW 子路径或非 origin/根绝对路径(判为浏览器误填),**回退到 `location.origin + "/dsh-tiddlywiki"` 自动推导**,按钮不再因误配 endpoint 而 404。另修 `scripts/gen-seed-send-to-agent.mjs` 模板字符串里一处未转义反引号(`ok:false`)导致再生成 seed 必崩的 bug。bundle send-to-agent 0.3.5→0.3.6。
|
|
132
|
-
- **v0.26.3**(2026-09-23):**修正 starter-docs 教程里一处技术性错误说法**:「方法一」原来写「点标签后 TW 会自动生成该标签的页面,列出所有带此标签的笔记」——这不准确。实测(TiddlyWiki 5.4.1 内核,已核对 `$:/config/ViewTemplateBodyFilters.multids` 与 `$:/core/ui/TagTemplate`):内核**没有**"标签页"这种自动页面,点标签丸/侧边栏标签弹出的是**下拉列表**(`list-tagged-draggable`,全量不分页),直接打开以标签命名的条目只会看到条目本身(缺失则提示),不会自动列出成员笔记。教程现在如实描述弹层行为与它的局限(不分页、不可分享链接),并指出想要「一个页面 + 可翻页」的标签列表应自行写汇总页(笔记多时套分页)。纯 seed 文案修正,不影响任何运行时行为;新装 wiki 的「示例与文档」seed 应用新文案,已有 wiki 不受影响(seed 是 ONE-SHOT)。
|
|
133
|
-
- **v0.26.2**(2026-09-22):**修「catalog 标题用目录名推导」——`codemirror-fullscreen-editing` 的真名其实叫 `$:/plugins/tiddlywiki/codemirror-fullscreen`**。`bundledCatalog()` 一直用 `$:/plugins/tiddlywiki/${目录名}` 拼标题,而 npm 包里这个插件目录名与 plugin.info 声明的 `title` 不一致(全包仅此一例,本机实测确认)。v0.26.0 引入的状态徽标按**完整 tiddler 标题**匹配,于是这个插件一旦在 TW 里被禁用,行内不会出现「TW 内已禁用」徽标(勾选不受影响——勾选按 `tiddlywiki.info` 的插件名)。**修复**:标题改为**优先取 plugin.info 的 `title`**,缺失/为空才回落目录名(plugin.info 才是 TW 真正加载的那个 tiddler,是权威来源)。守门:`scripts/verify-plugin-runtime.mjs` 新增 2 条——① 行为断言(fixture 里目录名与 `title` 不一致时必须采用 `title`,无 `title` 时回落目录名);② **不变量断言**(遍历真实 npm 包的全部自带插件,catalog 标题必须逐条等于其 plugin.info 声明的 title,并单独锁定 codemirror-fullscreen 这一例),**已反向验证**(把优先逻辑去掉即红 2 条)。
|
|
134
|
-
- **v0.26.1**(2026-09-22):**修 P0:设置页插件/主题/语言的勾选框全部显示为「未勾选」,且误点「应用」会把启动清单整个清空**。回归由 v0.25.0 的 `CatalogPending` 引入(commit 883196f):契约是「`undefined` = 用户还没碰过、完全跟随服务器」,而挂载时把它初始化成了 `{ plugins: new Set(), themes: new Set(), languages: new Set() }`——`desiredPlugins()` 用 `??` 回退,而**空 Set 不是 `undefined`**,于是「没碰过」被读成「用户期望集合为空」:① 所有勾选框全空(用户看到「TW 里明明装着的插件,这里一个都没勾」——本次由作者实测报障);② 更严重的是,什么都没改就点「应用插件」会 POST 一个**空数组**,把 `tiddlywiki.info` 的 `plugins` 清单**整个清空**(主题清单、语言同理:一次误点等于卸载 codemirror/menubar/全部主题/中文语言,TW 重启后只剩内核 + 自动补装的 markdown)。**修复**:初始化改为空对象 `{}`;并给三个「应用」按钮加闸门——`pending.* === undefined`(未碰过)时按钮禁用、点击直接返回,只有真正改过勾选才允许提交(「用户刻意取消全部勾选」仍是合法提交,host 的 no-op 守卫照旧兜底)。顺带把「wiki 内已装」徽标的判据从 `desiredPlugins()` 改为服务器集合 `serverPlugins`——徽标描述当前事实,不该随未应用的勾选抖动。守门:`scripts/verify-plugin-runtime.mjs` 新增 4 条(空对象初始化 + 三个闸门 + 三个 sync 函数必须在 change 回调里重算按钮 + 徽标判据),**已反向验证**(改回空 Set 即红)。
|
|
135
|
-
- **v0.26.0**(2026-09-22):**修「设置页插件管理与 TW 实际状态对不上」——两套真相源不再互相说谎**。现象(作者本机实测):TW 控制面板里明明启用着的插件(TiddlyFlex、mermaid、pinboard 等经 TW 原生插件库装的),设置页勾选清单里**根本不出现**;反过来 menubar / comments / help 在 tiddlywiki.info 里(勾选着),但用户早在 TW 面板里禁用了它们。根因:**设置页只读 `tiddlywiki.info` 的 `plugins` 数组(启动安装清单),而 TW 运行时集合 = tiddlywiki.info ∪ 以 tiddler 形式装进 wiki 的插件 − `$:/config/Plugins/Disabled/<title>`(正文为 `yes`,TW 核心自己的判定)**——两个面板读的不是同一份真相,谁也没错但谁也不全。**修复(方向「修真」)**:勾选语义保持「是否写入 tiddlywiki.info」不变(它只管引擎自带插件的离线启停,这是 TW 原生界面做不到的唯一能力),但把缺失的另一半真相显示出来——① 每个插件行加状态徽标:「TW 内已禁用」(勾选着但被 TW 禁用)/「wiki 内已装」(不在启动清单但 wiki 里有同名 tiddler 版本,TW 运行时已在用);② 新增只读小节「**wiki 内插件(经 TW 原生安装)**」,列出第三方插件 tiddler(名称/版本/描述/禁用状态),启停/卸载引导去 TW 控制面板;③ 区块说明文字写明两边分工。**实现**:`GET /admin/state` 新增 `runtimePlugins` 字段,由 `scanWikiRuntimePlugins()`(`src/host/admin.ts`)扫 tiddlers 目录得出——插件库安装的插件是 `$__plugins_*.json` + `.json.meta`(只读几十字节的 meta,不碰几 MB 的插件 payload;无 meta 的裸 `.json` 还可能是拖拽导入的**多 tiddler 数组**,一并解析);禁用标记 `$__config_Plugins_Disabled_*.tid` 按文件自身 `title:` 头还原插件标题(不做有损的文件名反解码)。扫描失败返回 `null`(读失败 ≠ 不存在),客户端隐藏徽标与小节而不是显示空的假象;`dsh/*`、`dsh-tiddlywiki/*` 命名空间(插件自己的种子 bundle)排除在外。产品哲学不变:插件对 wiki 的强制定制依旧只有 `--init server` 基线 + 自动补装 markdown + 3 个核心种子,官方自带插件之外的安装/管理全部留给 TW 原生界面。
|
|
136
|
-
- **v0.25.1**(2026-09-21):**修「回复流里的 wiki 链接点了打不开」**——根因在 **DSH web 的 markdown 渲染器**把 `[标题](/dsh-tiddlywiki/tw/#标题)` 这类相对链接**解析成绝对 http(s) URL**,再按 protocol 判为「外链」(`target="_blank"` + 自己的 `openExternalLink` onClick)。旧 `installWikiLinkInterceptor` 只匹配相对路径正则 `^/dsh-tiddlywiki/tw/#(.+)$`,绝对 href 匹配不上 → 拦截器不接管 → 点击落入 DSH 外链逻辑,在新标签页打开 TW 编辑器而不是中央面板(用户感知「打不开」)。**修复**:拦截器改用 `new URL(href, window.location.origin)` 归一化后判**同源 + `pathname === TW_PROXY_BASE` + hash 非空**(相对/绝对两种形态都接),在 capture 阶段 `preventDefault + stopPropagation` 抢在 DSH 外链 onClick 之前打开中央 TW 面板;`scripts/verify-tool-views.mjs` 新增源码级断言(必须用 `new URL` + 同源校验 + 代理 pathname,且旧相对正则已移除)。headless Chrome 实测:TW 面板 hash 导航(含 TiddlyFlex 的 `$:/StoryList-1`)本身正常,问题确在拦截器漏匹配绝对 href。
|
|
137
|
-
- **v0.25.0**(2026-09-20):**第五轮代码审计的修复版**(审计那天被用户判定为「本地单机可接受、不修」的只有一条:`POST /render` 的 **title 分支**可以经被渲染条目正文里的 `{{…}}` 转写把插件配置读出来——本机实测复现,`text` 分支早已 403;不修的理由是单机自用、且真正的边界本就不在这条路由(任何笔记正文的 `{{$:/plugins/dsh-tiddlywiki/config}}` 在嵌入式 TW 面板里同样会被转写渲染)。其余全部修掉,按性质分四组。**① 工具契约(数据安全)**:(a) **`tags: []` 现在真的清空标签**——此前 `normalizeTagArg()` 把空数组归一成 `undefined`(=「未传 = 保留原标签」),而 `tags` 又是 `RESERVED_TIDDLER_FIELDS` 成员(`fields.tags` 会被拒),于是**没有任何途径**把一篇笔记的标签改成空:模型照 `tiddlywiki_lint` 的 junk-tags 建议去清标签、传 `[]`,什么都不会发生,回执还把旧标签原样列出来(本机实测)。(b) `tiddlywiki_append` / `tiddlywiki_rename` / `tiddlywiki_batch_put`(逐条目 `expectedModified`)补上**乐观并发令牌**:这三条与 put/delete/attach 一样是「读全文 → 整篇回写」(append 读旧基底再加一段),而可传令牌的只有后三条 —— 注入提示词让模型回传 `expectedModified`,append 却**没有这个参数**,人类在 TW 编辑器里的并发修改会被旧基底静默回滚。(c) `tiddlywiki_rename` 更新引用时**逐个重读引用者**(列表只是快照),遍历期间被改动过的跳过并在回执里报出数量——跳过一条链接可以手工补,被覆盖掉的一段正文不可恢复。(d) `tiddlywiki_attach` 的 `noteTitle` 指向二进制附件时明确拒绝(会把 base64 写坏而回执看着正常)。(e) `tiddlywiki_lint` 的 `checks` **拼错不再谎报干净**:本机实测 `checks:["broken-link"]`(少了 s)在 2933 条笔记的知识库上**一个检查都没跑**,回执却是「扫描 2933 条文本笔记,发现 0 个问题 / 没有发现问题」;现在回执列出**实际运行**的检查、显式报出无法识别的名字,且「一个检查都没跑」不再等于「没问题」。(f) `valid-until` / `review-after` 的纯日期(`YYYY-MM-DD`)改按**当地日终**判过期,此前按 UTC 0 点算,东八区当天 08:00 就报「已过期」。**② 设置项(都是「保存了却不变」)**:`git.autoCommit`/`git.debounceMs`/`git.remote` 保存后**对运行中的插件立即生效**(`AutoCommitter` 在构造时把 enabled/debounceMs 快照进不可变 options,remote 只在启动 bootstrap 时应用——此前关掉自动提交仍会 commit、换远端仍推老地址,直到重启 dsh web);「重启 TW」按钮的 15s 超时改为 120s(`/admin/restart` 要等 TW 就绪,软窗口默认 60s、大知识库自述 44s+,此前会弹「重启失败」而宿主其实成功);数值项(`bridge.port` / `startup.readyTimeoutMs` / `ui.allArticles.pageSize` / `git.debounceMs`)加 `min`/`max` 与字段旁红字,非法值**拒绝保存**(宿主对越界值是静默夹取/回落,提交只会继续制造「回显 ≠ 生效」);**空补丁不再发请求**、宿主在序列化文本逐字节相同时**跳过 PUT**(每次空点「保存配置」都会重写配置 tiddler 并刷新 `.meta` 的 `modified` → 白造 git diff、两台机器各点一次就冲突在 `.meta` 上,与 v0.24.2 刚修的 `$__language.txt.meta` 同类);保存被拒(409,如配置 tiddler 残留冲突标记)时错误**持久显示**在配置区而不是一闪而过的 toast;微信发布的 `command`/`adapter`/`token`/`dsn` 补齐 UI(token 密码型 + 👁 显隐切换,掩码回填语义照旧);插件/主题/语言的未应用勾选提升为页面级状态,能活过「同步 / 重启」触发的重刷;表单有未保存内容时若配置在**别处**被改过,保留你的输入并显式提示,而不是重建表单把输入清掉;首屏加「加载中…」、加载失败不再清空已填表单。宿主侧 `/admin/config` 的补丁现在必须过新的 `normalizeConfigPatch()`:**已知数值夹取到真实边界、已知布尔/字符串类型错误返回 400 并点名字段、`__proto__`/`constructor`/`prototype` 一律拒绝、未知键透传**(此前任意 JSON 都会落进配置 tiddler——数组体变成 `{"0":1,"1":2}` 这种永久垃圾键,`git.debounceMs: "abc"` 会经 `??` 进 `setTimeout` 被当 0 → 每次写入都 commit)。**③ 会话「知识库」Tab**:`NOTE_TOOL_NAMES` 补 `append`(「增量写入」)/`attach`(「附件」)/`delete`/`trash`(`restore` 计产生),新增「🗑 删除 / 回收站」一节——此前**用 append 写的笔记在这个 Tab 里完全不可见**,而注入提示词恰好推荐「纯增量内容优先用 append」,于是一整轮工作会被显示成「本会话暂时没有产生、读取或检索过任何知识库笔记」;事件日志读不出来的会话**在页面上明说**(不再静默吞掉、把「读不出来」显示成「没碰过笔记」),`counts.sessions` 只统计成功读取的会话;读日志从串行改 **4 路有界并发**、探测整段加 **12s 墙钟预算**(最坏 ~375s → 有界,客户端 25s abort 之后不再继续压着 TW 打),生成加 **3s 单飞复用**;客户端自愈改为**三态探测**(在 / 被清 / 状态未知——未知既不计失败也不清零,此前 `catch { return true }` 把「探测失败」当「一切正常」),自愈 interval 与 `phase` 解耦(进 error 态后不再永久失去重试),并新增**按 3 分钟节拍的静默刷新**(tab 长期开着也能跟上会话,不闪 loading 不砸内容);`escapeInline()` 中和 `{}`(会话日志里出现的 `{{…}}` 会被 `/render` 转写展开)并改为**先截断再转义**(否则 `&` 会被截成 `&am`);`scanRefs` 不再把标题里的 `)`/`]` 一律截断;`TitleState.snippet` 从死字段改为真的渲染。**④ 其余**:检索卡片的范围与工具结果对齐(卡片把模型可见回执里读到的 `workspace` 回传 `/search`,v0.24.0 只改了工具侧——卡片会列出**更多**的结果且不显示范围);`/status` 的 git 冲突内容扫描加上限(200 个文件 / 2MB,批量导入期间不再每 30s 同步读几千个文件阻塞事件循环);`tiddlywiki_delete` 的描述写明 `$:/` 系统条目实为永久删除;`TiddlywikiConfig.note` 补上漏掉的 `workspaceMark`(三处形状 lockstep);`tiddlywiki_attach` 有专属卡片。**守门**:新增 `scripts/verify-config-patch.mjs`(6 组纯函数断言:非对象拒绝 / 越界夹取 / 类型错误点名字段 / 危险键拒绝 / 未知键透传)与 `scripts/verify-tool-views.mjs`(5 条源码级断言:`TOOL_VIEW_KEYS` 与 host 工具名完全一致 / `TOOL_LABELS` 全覆盖 / 工作区前缀两处一致 / 搜索卡片仍回传 `workspace`),两者进 `verify:unit`;`verify-tools.mjs` +4 条真实 TW 断言(`tags:[]` 清空且「不传」仍保留 / append 令牌不匹配拒绝、匹配放行 / batch 逐条失败 / lint 未知检查名不得谎报);`verify-write-policy.mjs` +1 条(空数组 vs 不传的语义边界)。**已反向验证的路径**:把 `normalizeTagArg` 还原成「空数组 → undefined」、把 lint 的 `checks` 还原成自由集合、把 append 的令牌检查删掉,对应断言分别变红。
|
|
138
|
-
- **v0.24.2**(2026-09-20):**修「`$:/language` 每次 dsh web 启动都被无条件重写 → 两台机器每次 pull 都冲突」**。现象:`git pull` 稳定卡在 `tiddlers/$__language.txt.meta` 上,而**正文文件根本不冲突、diff 出来只有 `created`/`modified` 两行**;本仓库的 git 历史里甚至已有一条专门清理该冲突残留的提交,而 2026-09-20 一次会话内又撞了两次。根因定位到行:启动自举应用 `uiLanguage` 时、以及设置页改动语言时,两处都**无条件 PUT** `$:/language`(正文每次都一样,但 TW 会给 `.meta` 盖新时间戳)——两台机器各自启动过就都改了同几行。现在抽出 `pinLanguageTiddler()`(`src/host/admin.ts`):**先读,内容一致就不写**,内容不同/条目不存在才写。⚠️ 关键边界:**读失败绝不等于「内容相同」**(铁律第三条)——`client.get()` 只有 404 返回 undefined、其余抛错,抛错时**照写并记一条日志**,否则用户的语言会永久卡在错的值上且重启也修不回来。**守门**:新增 `scripts/verify-language-pin.mjs`(进 `verify:unit`)——4 条桩 client 行为断言(一致不写 / 不存在要写 / 不同要写 / 读失败照写并报警)+ 源码级断言(`index.ts` 与 `admin.ts` 除原语自身外**一处裸 PUT 都不许有**,且两处都必须经 `pinLanguageTiddler`)。**已反向验证**:还原启动路径为无条件 PUT → 红 1 条;把读失败分支改成「直接 return false」→ 红 1 条。(附带一条方法论教训:第一次做这条反向验证时**构建因我的改写而失败**,而我把构建输出丢进了 `$null`,于是守门测的是旧 `lib/`、假绿了一轮;重做时把构建输出放回台面才抓到真红。反向验证必须确认「改动真的被编译进去了」。)
|
|
139
|
-
- **v0.24.1**(2026-09-20):**修「重启/停止 TW 的四个路径里有三个没排干 syncer 队列」——一类静默丢写入的缺陷**。本仓库铁律第一条要求「重启 TW 前必须把 syncer 队列排干」,但那条规则**只写在文档里**:`POST /restart`、`POST /admin/restart`、`POST /admin/info`(改插件/主题后重启)与「知识库切换」的 `stopServer` 全都是**直接杀 TW 子进程**,而 TW 的 REST PUT 返回 204 时条目还在内存里、文件系统 syncer 要 ~250ms 后才落盘 —— 在那一秒内点「重启 TW」,刚写的笔记就随子进程一起消失(与 v0.19.0 丢 `tw-web-host`、v0.22.0 才发现 throttle 竞态同一类)。只有启动自举 / seed / `/sync` 三处走了 `flushPendingWrites`。**根因不是谁写错了某一行,而是「规则」和「代码」之间没有强制关系** —— 所以这一版不是补四个调用点,而是把不变量变成**原语**:新增 `drainThenStop()`(`src/host/seeds.ts`,**排干发生在它内部**,签名是 `{client, tiddlersDir, stop}`),四个路径全部改为经它停/重启,调用方**没法**绕过;`stop` 的返回类型放宽为 `Promise<unknown>`(`restart()` 回状态视图、`stop()` 回 void,排干都不关心)。`/restart` 与 `/admin/restart` 的回包新增 `drained`,排干不完整时如实上报而不是静默。**守门**:新增 `scripts/verify-restart-drain.mjs`(进 `verify:unit`)——① 顺序断言:`drainThenStop` 内部「排干的 await」必须早于「stop 的 await」,且排干失败必须走显式分支后**仍然 stop**(best-effort:不能因为排不干就把用户锁死在起不来的 TW 上);② 源码级断言:`routes.ts`/`admin.ts` 里**每个** `deps.server.restart()` 都必须作为 `drainThenStop({stop: …})` 的参数出现(裸 restart 即红),`index.ts` 的 `stopServer` 必须包在它里面;③ 行为断言:用桩 client 真跑一遍,确认「写入真的落盘之后才 stop」「client 缺失(TW 没在跑)是安全 no-op」「排干失败也继续 stop 并把警告交出去」。**已反向验证**:分别把 `/sync` 路径、知识库切换还原成裸调用、并删掉 `drainThenStop` 里的 `stop`,三次都稳定变红。顺带把 `/sync` 与 seed 两条**本来就在 flush** 的路径也收敛到同一原语,从此「重启前排干」只有一个实现。
|
|
140
|
-
- **v0.24.0**(2026-09-20):**三处「Agent 读写 wiki 的规矩」按用户要求改进 + 一个 lint 误报修复**。① **新建笔记自动标工作区**:`put`/`batch_put`/`append` 新建条目时自动带上 **`ws/<项目名>` 标签 + `workspace` 字段**,项目名取自调用会话的工作目录(新 `src/host/workspace.ts` 纯函数:Windows/Unix 路径、尾斜杠、CJK、非法字符规范化;`home`/`tmp`/`desktop` 这类通用目录与**驱动器根目录**一律拒绝——后者是写这个模块的守门脚本抓出来的真缺陷:`C:\` 会被算成 `C:`,在盘根开会话时全盘笔记都挂上 `ws/C:`)。标记是**附加式**(调用方标签在前)、**仅新建**(覆盖不追打,也不会改写已有 `workspace` 字段)、`$:/` 豁免、`note.workspaceMark: false` 可关、调用方显式 `fields.workspace` 优先;`exec` 缺失 / 会话无 `cwd` / 未知会话一律**安全降级为不标记**(不猜、不抛错)。为什么标签必须带 `ws/` 前缀:裸项目名与真实业务标签撞车——作者库里 `dsh-tiddlywiki` 已挂着 2510 篇导入的书籍章节。② **检索先窄后宽 + 多词 AND**:`tiddlywiki_search` 先在工作区内查、**区内 0 条才自动扩大到全库**,回执明确写出「已在工作区 ws/X 内缩小范围」或「工作区 ws/X 内 0 条,已扩大到全库」(**「工作区内 0 条」不等于库里没有**,这是把「搜不到」错判成「不存在」的主要来源);`query` 从**整串子串匹配**改为按空白切词、**全部词命中**才算(旧行为连 `alphaprobe betaprobe` 这种自然的双词查询都永远匹配不到);调用方显式传 `tag`/`field`/`value` 时**不自动收窄**(显式范围不被二次猜测)。③ **过期内容淘汰策略(只提示,绝不自动删)**:阶段性笔记写 `valid-until`(硬过期)/ `review-after`(该复查),被取代时写 `superseded-by` + `superseded` 标签、**保留旧笔记**;`tiddlywiki_lint` 新增 `stale` 检查(默认开启、`staleAfterDays` 默认 180),报 `stale-expired` / `stale-review` 两类**硬判定**加一类 `stale-candidates`(带版本号或 `done` 标签且长期未改动,**明确标注「候选,非判定」**)。**淘汰只由人决定**——报告全程只读,不删不改不归档。④ **修 lint 死链误报**(v0.23.6 的内容并入本版):死链正则 `\{\{([^}]+)\}\}` 会把 `{{{ [tag[todo]count[]] }}}` 这类**三花括号过滤器表达式**从第 1 个 `{` 开始捕获成「转写目标」并报死链(实测线上 480 条死链报告里 8 条属此类),现在用前后瞻 + 捕获内容守卫排除。**明确不改的**:≈440 条真实死链**保留不修**——多数是用户手工删掉的笔记,属预期状态(只读报告是对的,自动「修复」是错的)。**守门**:新增 `scripts/verify-workspace.mjs`(进 `verify:unit`,9 组纯函数断言,含「任何 cwd 算出的标签都不得是垃圾标签 / 含空白 / 丢前缀」这条与 `isJunkTag()` 的跨模块一致性检查,以及 `ws/` 前缀不得被「简化」掉的回归),`verify-tools.mjs` +12 条真实 TW 断言(工作区标记六种形态 + 多词 AND + 先窄后宽 + `fellBack` + `stale` 三类),`verify-prompt.mjs` +1 条「三条新治理规则必须在场」并把 slim 长度预算从 1800 调至 2100(**预算上调与新增的逐条断言绑定**:删掉任何一条规则即红)。**所有新守门都做过反向验证**:分别还原旧行为(整串子串检索 / 关掉工作区收窄 / 关掉自动标记 / 关掉 stale 检查 / 去掉驱动器根目录判定 / 从规范化字符集里删掉 `|`)后,对应断言稳定变红。slim 正文实测 1918 字符(full 2722)。
|
|
141
|
-
|
|
142
|
-
- **v0.23.5**(2026-09-18):**修 15 处审计缺陷**(第三轮全面审查列出的 18 条,逐条核实后修掉其中 15 条真缺陷;跳过 3 条纯理论性的信息泄露。完整审计报告见知识库笔记「dsh-tiddlywiki 第三轮代码审计」——审计报告是工作文档,不再进 git)。按性质分四组。**① 数据安全(最重)**:`tiddlywiki_delete` 原先**先删原条目、后读回收站索引**,于是「快照已写 → 原条目已删 → 索引读失败」时报"已中止",但笔记其实已经没了、且没进索引——`trash list` 看不见、`restore` 找不到、`empty` 清不掉,成为永久孤儿(只能从 git 捞);现在**读校验索引排在所有破坏性步骤之前**,中止即等于"没删"。同一处还有两个洞:`$:/` 系统条目会被**软删除**(删掉 `trash-index` 自己 → 下一次读 404 → 被当成"回收站为空" → 索引被重写成 1 条,丢掉全部记录),现在系统条目一律走永久删除分支,且**删回收站索引被显式拒绝**(要清空请用 `trash action=empty`)。`tiddlywiki_put` 不再把**二进制附件**写成文本(保留基底 `type: image/png` 会让 base64 解不出、图裂而回执说"类型没变";要转换得显式 `fields.type`)。`git pull` 不再**无条件 `rebase --abort`**——此前只要 pull 失败(含没配 remote、离线等与被 rebase 无关的原因),用户在 wiki 仓库里正在做的 rebase 与冲突解决就被清掉;现在只 abort「我们自己发起」的那个。乐观并发的两个令牌从 **OR 改回 AND**:`revision` 是 TW 的内存计数器(重启/拉取后复位),旧逻辑「`modified` 命中就放行、否则看 `revision`」会让跨重启的写入被判"一致"而**静默覆盖**人类改动,同时传两个令牌也**并不更安全**。配置 tiddler 的 `modified` 不再被钉死成 `created`(`ensureTiddlerTimestamps` 只补缺的那一半,用 now)。**② 崩溃与资源泄漏**:`fs.watch` 补上常驻 `error` 监听(Node 在无监听器时把 `error` 抛成未捕获异常 → 整个 `dsh web` 进程退出;Windows EPERM / Linux ENOSPC 都能触发);`WikiServer.startOnce()` 在 `spawn` 前**复查 `stopping`**(`stop()` 可在 `await findFreePort()` 期间跑完,此后 spawn 的子进程没人杀 = 孤儿 TW);知识库切换**回滚失败也必须重装 extras**(此前从 catch 直接返回、跳过 `setupExtras()`,此后所有 wiki 写入都不再被提交,而"迟到就绪"复探让 `/status` 看着一切正常——静默停摆);`runSwitch` 补 `disposed` 守卫(热重载/关闭期间的在途切换不再重新挂上 committer/watcher,也不在 teardown 之后再 spawn TW)。**③ 行为瑕疵**:`escapePromptBraces` 对**连续花括号**失效(`{{{name}}}` 会残留 `{{`,而 DSH 对未知变量直接抛错、能炸掉整个系统提示词装配正是该函数存在的理由);`?limit=` 空值不再被夹成 1(`Number('' ?? fallback)` 是 0,`/recent?limit=` 曾只返回 1 条而不是默认 15);`tiddlywiki_append` 在 `mode=prepend` 时不再回执谎报「写进了某段落」(`heading` 只对 append 生效);`POST /render` 的超限 body 从 400 改为 **413**。**④ 两处零成本的安全收口**:代理路径判定不再只看字面量 `/tiddlers/`——TW core 的 `get-tiddler-html` 路由是**单段**(`/^\/([^\/]+)$/`),把 `/` 编码成 `%2F` 即可绕过旧守卫(本机实测:修复前 `/tw/...%2Fconfig` 与 `/api/...%2Fconfig` 都返回 **200 + 715 字节配置 JSON**);`/render` 的 **`text` 分支**也不再是旁路(TW 会解析 `{{…}}` 转写,实测修复前 `{"text":"{{$:/plugins/dsh-tiddlywiki/config}}"}` 返回 200 + 3123 字节配置正文,净化器只管标签、管不住正文),`contextTitle` 一并校验。**守门**:`verify-write-policy` +6 条(令牌 AND 语义 +「证据不足判冲突」+ `onlyCreated` 不再钉死 modified)、`verify-prompt` +1 条连续花括号、`verify-frame-guards` +4 条源码级生命周期断言(`fs.watch` error 监听 / spawn 前复查 `stopping` / 回滚 finally / `runSwitch` disposed)、`verify-audit-fixes` +4 条 E2E(删除顺序:索引读失败后原条目必须仍在、`$:/` 与索引不得被软删、二进制附件拒写、`?limit=` 空值)、`verify-conflict-config-guards` +1 条**真 git E2E**(在真 rebase 中途 pull 失败,用户的 rebase 必须原样保留)、selftest +8 条(编码/双编码代理路径 403、`/render` 转写与 `contextTitle` 403、普通转写仍 200)。**全部 15 条修复都做过反向验证**:把修复还原后,新断言稳定变红。
|
|
143
|
-
- **v0.23.4**(2026-09-18):**三处硬化,全部来自 v0.23.3 上线当天暴露的真实事故**。① **提交前拦冲突(数据安全)**:`git pull --rebase --autostash` 的 **autostash 重新应用**冲突后,`rebase --abort` 已无事可 abort,冲突标记留在工作树里,随后 60s 的 AutoCommitter 照常 `git add -A && commit` —— `<<<<<<< Updated upstream` 就这样被**永久提交并推送**进了 wiki 的配置 tiddler(`0b19ca6`,2026-09-17),插件随即解析不了自己的配置(微信发布一直是关的、`prompt.extra` 静默失效)。现在 `GitFace.conflictState()` 三重探测(进行中的 rebase / 未合并路径 / **工作树里残留的冲突块**),`commit()` 直接抛 `GitConflictStateError` 拒绝提交;`/sync` 回 409 + 文件名、agent 工具返回结构化失败、AutoCommitter 去重上报一次(不去刷屏);`pull()` 在 abort 后**复检**(autostash 形态正是 abort 之后才暴露);`/status` 与设置页状态行显示「冲突未解决(N 个文件,已阻止提交)」。⚠️ `MERGE_HEAD`/`CHERRY_PICK_HEAD` 故意**不**硬拒——`git commit` 正是收尾它们的动作(真 git E2E 抓出了这个设计错误)。② **配置解析失败拒绝写(数据丢失)**:`ConfigStore.set()` 在存量 tiddler 解析失败时,原先拿**空缓存**合并 → 一次设置页保存就把配置写成只剩表单里那几个字段的"残版"(18 日 13:38 实测:`prompt.extra`/`git.remote`/`ui.*` 全丢)。现在抛 `ConfigUnreadableError`、不 PUT,并把原因经 `/admin/state` 的 `configError` 渲染成设置页顶部红色横幅;瞬时**读**失败仍走"合并到缓存"(回归断言守住)。③ **adapter 版本校验**:`/wechat/ready` 原先只数文件在不在,旧版 adapter(不认识 `--title-file`、标题仍是必填位置参数)也会报"就绪",点按钮才在 opencli 里报"缺少必填参数"。现在按**定义**判新旧(`weixin-flow.js` 必须 `export function resolveNoteTitle(`、两个入口必须声明 `name: 'titleFile'`——**不是**字符串出现,注释里提到不算),`missing`(不存在)与 `stale`(过旧)语义互斥;按钮预检与路由 503 分别说「版本过旧」和「缺少发布脚本」。bundle 升 `0.2.0`。守门:新增 `scripts/verify-conflict-config-guards.mjs`(23 条:伪 exec 纯逻辑 + **真起临时 git 仓库的冲突 E2E**(真 merge 冲突 / 真残留标记,且解决后必须能提交)+ ConfigStore 拒绝写/回归/接线断言;**已反向验证**:拆掉 `contentScan` 或配置拒绝各红 2 条),`verify-wechat-publish.mjs` 17→24 条(+注释里提及新符号必须判旧、真源码符号对齐),bundle 守门 +3 条。
|
|
144
|
-
- **v0.23.3**(2026-09-18):**TW 笔记工具栏新增「发布到公众号」按钮**(可选功能,与既有 CLI 等价,只是不用敲命令)。链路:TW 按钮 → `POST /dsh-tiddlywiki/wechat/publish` → 宿主 spawn `opencli weixin publish-note` → `GET …/wechat/publish/status` 轮询 → 覆盖层显示进度与日志尾部。**只到草稿箱为止**,发表仍由人工扫码(沿用 v0.23.2 定的策略)。三处硬骨头:① **标题不进 argv**——Windows 上 `opencli` 是 `.cmd` shim,必须经 cmd.exe 启动,于是标题里的 `&`/`|`/`^`/引号成了命令注入面、中文标题还会被 cmd 的代码页解成乱码;改成宿主写 UTF-8 文件 + adapter 新增 **`--title-file`**(位置参数照旧可用,两者都给时位置参数优先)。② **任务化而非同步请求**——命令要跑几十秒到几分钟,`POST` 立即返回 `jobId`,单并发(第二次 409)、15 分钟上限,超时用 `taskkill /T` 杀**整棵进程树**(只杀 cmd.exe 会留下孤儿 node 抓着 job 目录,实测 `EBUSY`)。③ **可选功能默认不打扰**——`wechat.enabled` 关着时三条 `/wechat/*` 路由一律 403、按钮插件不写进 wiki(新增 gated 起步 seed `wechat-publish`,与 `publish-spec`/`wechat-setup` 同进同退)。新增 `src/host/wechat-publish.ts`(命令组装纯函数 + job registry + 就绪探测)、三条路由(方法/同源守卫 + 可选 `x-wechat-publish-token`;`wechat.token` 与其它密钥一样在 `/admin/state` 打码)、TW bundle `$:/plugins/dsh/wechat-publish`(`scripts/bundle/wechat-publish/` + build/gen/verify 流水线,`wechat-publish.bundle.json` 逐字节守门)。守门:`scripts/verify-wechat-publish.mjs`(17 条:纯函数 + **真跑 spawn 的「假 opencli」E2E**(含标题不进 argv 的逐字断言)+ 真实 HTTP 路由守卫;**已反向验证**:拆掉 `guardWechat` 即红)+ `scripts/verify-wechat-publish-bundle.mjs`(进 `verify:static`)+ `verify-wechat-adapters.mjs` 新增「两个 publish adapter 都必须走 `resolveNoteTitle`」。
|
|
145
|
-
- **v0.23.2**(2026-09-17):**多图发布命令 `publish-note-imgs` + 发布策略定为「到草稿箱为止」**。TW 笔记内嵌 `[img[...]]` 经 `/render` 是 data URI,微信存草稿会过滤非 mmbiz 图——新命令先把正文全部图片经 DataTransfer 逐张上传 CDN(轮询计数到期望值),按 DOM 顺序重写正文 src 再 insertHTML,从正文第一张设封面;发布前同样读 `pub-state`/`no-publish`(只告警不阻断)。`selectCoverFromContent` 封面校验改轮询,修「实际成功却报失败」的假阴性(实测 `list_ex` 的 `cover` 已是 mmbiz 而流程报失败)。安装脚本 FILES 收编至 5 个 adapter,守门断言同步。**策略**:adapter 负责到草稿落盘为止,发表(含原创声明/作者/留言等确认弹窗 + 管理员扫码)由人工完成——`--publish` 保留但 best-effort,遇弹窗会等到超时。文档新增:发表确认弹窗说明、`stale page identity` 续作法、`list_ex`/`appmsgpublish` 核实命令、GitHub ext release 落后 Web Store 备注、npm allowScripts 拦 postinstall 无害备注、**eval 诊断后台必须过滤不可见 toast 残留**(踩过:把残留文案当实时状态误判账号被拦,实际账号正常)。
|
|
146
|
-
|
|
147
|
-
- **v0.23.1**(2026-09-17):**新增 `wechat-setup` seed——「微信公众号发布指南」也随插件分发**(补齐 v0.23.0 的缺口:当时只有「发布元数据规范」进了 seed,387 行的安装/换机还原指南只存在于仓库与 npm 包的 `docs/` 里,wiki 里那篇「换机还原清单」是手写指针笔记)。与 `publish-spec` 完全同模式:起步层(`startup: true`)+ `gate: (ctx) => ctx.wechat === true`——**只在设置页开启「微信公众号发布」时**启动写入(笔记「微信公众号发布指南」,`dsh-docs` 标签自动进首页插件文档栏),不开该功能的用户 wiki 不出现、设置页手动「初始化」不受 gate 约束。**内容单一来源**:新 gen 脚本 `scripts/gen-seed-wechat-docs.mjs` 从 `docs/wechat-publish-setup.md` 生成 `src/host/seed-wechat-docs.ts`(照 send-to-agent 的 gen 流水线,勿手改常量),新守门 `scripts/verify-wechat-docs-seed.mjs`(进 `verify:unit`)断言 seed 常量与文档**逐字节一致** + 注册表 gate 接线(2 个 gated seed = gate 谓词恰好 2 处)。顺带:`verify-package-contents.mjs` 的 `npm pack` 从管道捕获改为**文件重定向 + 临时缓存目录**(DSH 沙箱禁止命名管道 stdio → 原 `exec` 实现 EPERM;npm 默认缓存在沙箱外也要绕开),本地终于能跑这条守门。selftest / verify-seed-error-policy 的 gate 断言改为**从注册表派生** gated 清单(新增 gated seed 不再漏改硬编码);设置页与 config 注释同步「开启后写两篇文档」;docs/seed-initialization.md 补齐 publish-spec / wechat-setup 两行表格。
|
|
148
|
-
|
|
149
|
-
- **v0.23.0**(2026-09-17):**新增「发布到微信公众号」+ 发布元数据**(`tools/wechat/`,**可选功能,默认关闭,需额外安装**,不动 host 代码)。把 wiki 笔记一键发到公众号**草稿箱**,可选点发表。
|
|
150
|
-
|
|
151
|
-
**⚠️ 可选功能,默认关**:真正干活的是仓库 `tools/wechat/` 下的 opencli adapter + Browser Bridge 浏览器扩展——**插件本体不含它,也不会替你装**。开关 `wechat.enabled` 默认 `false`:**关闭时**不注入任何发布相关提示词、也不往 wiki 写「发布元数据规范」文档;**打开后**仅多这两项,不会安装任何东西、不启用任何后台服务。不装/不开它,插件其他功能完全不受影响。开启方式:设置 →「TiddlyWiki 知识库」→「可选功能:微信公众号发布」→ 勾选 → 保存配置。完整安装步骤(opencli、浏览器扩展、扫码登录)与排错见 [docs/wechat-publish-setup.md](wechat-publish-setup.md)。
|
|
152
|
-
|
|
153
|
-
**为什么不用官方 API**:2025-07 起官方回收了「发布能力」接口对个人主体/未认证账号的调用权限(`freepublish/submit` 不可用、`draft/add` 常回 48001);即便可用还要配 API IP 白名单、封面永久素材、正文图片必须走 `media/uploadimg`。**本方案改走浏览器自动化**,复用你**已登录**的公众号后台会话(opencli + Browser Bridge 扩展),个人号可用、零凭据落盘。数据流:笔记标题 → DSH `POST /render`(TW 自己渲染成语义 HTML,**代码高亮白蹭**)→ `wechat-html.js` 补**内联样式**(实测微信会剥 `<style>` 并删 class,**只认内联 style**;而 TW 输出零内联样式,所以必须有这一步)→ `weixin-flow.js` 驱动后台填表/写正文/传图/设封面/存草稿。
|
|
154
|
-
|
|
155
|
-
**发布元数据**(本轮追加,解决「哪些发过 / 哪些不能发」):每篇笔记用自定义字段记录对外发布状态——`pub-state`(`draft`/`published`/`excluded`)、`pub-platform`、`pub-wechat-at` / `pub-wechat-title` / `pub-wechat-url`(**按平台分字段**,将来 `pub-zhihu-*` 直接平铺)、`pub-note`;标签 `no-publish` 作为给人看的镜像。**规范全文放进 seed**(「发布元数据规范」,`dsh-docs` 标签),因为注入提示词 slim 形态实测只剩几十字符余量,只在其中放**一句指针**;该 seed 带 `gate`——**只有开启可选功能才会写**。⚠️ 三个名字**已被占用**故特意避开:`publish`/`publishyear` 是 Obsidian 导入书籍的「出版社/出版年」,`发布记录` 是本插件自己的版本说明。存量来源可辨的公众号文章(`source-path` 含「公众号」,本 wiki 实测 18 篇)用 `tools/wechat/backfill-publish-state.mjs` 回填(**默认 dry-run**;先 GET 完整 tiddler 再只添加字段整体写回;已有 `pub-state` 跳过;正文为空跳过;发表时间无法考证就留空)。发布前检查**只告警不阻断**(读 `pub-state`/`no-publish` 命中就打印警告并继续,`--force` 仅改措辞),回执里附**建议回写值**供 agent 回写。
|
|
156
|
-
|
|
157
|
-
**四个实测踩坑**:① **必须带 `--trace retain-on-failure`**——不带就对 `mp.weixin.qq.com` 稳定报 `Navigation rejected`(trace 开 5/5 成功、关 8/8 失败;`--site-session ephemeral` / `--keep-tab false` / 前后台窗口 / 重置标签页 / 重启 daemon 全部无效,是 opencli 1.8.7 的 bug);② **轮询里绝不能读 `document.body.innerText`**——微信编辑器 DOM 极大,读一次强制整页 layout,**实测单次 ≈17 秒**,8 次轮询把命令拖过 210s 超时而**草稿其实已保存成功**(假阴性);改成只查少量 toast 节点后 `saveDraft` 从 **137 秒降到 3.9 秒**;③ **图片上传必须用 DataTransfer 注入**,不能用 `page.setFileInput`(后者依赖 CDP `Page.fileChooserOpened`,本机扩展版本组合下稳定失败;DataTransfer 在页面上下文直接塞 `input.files`,实测图片真进 `mmbiz` CDN,代价是单图 8MB 上限);④ **正文必须用 `execCommand('insertHTML')`**,`insertText` 会把 HTML 当字面文本(opencli 内置 `weixin create-draft` 就是这样,实测 Markdown 符号原样进库)。**发表需管理员扫码**——微信的账号安全机制,无法自动化,「一键」的真实含义是「脚本做完排版/上传/填表,你只扫一次码」;默认走**发表**(不推送粉丝、不占群发额度)而非群发。
|
|
158
|
-
|
|
159
|
-
命令:`opencli weixin publish-note "标题" [--cover x.png] [--preview out] [--publish] [--force]`(另有 `create-article` 从本地 HTML 文件建草稿)。**换机器还原**:`node tools/wechat/install-wechat-adapters.mjs` 一条命令装好 adapter 并自检扩展/登录状态;完整步骤与排错见 `docs/wechat-publish-setup.md`。守门:`verify-package-contents.mjs` +5 断言(发布包必须含 publish-note / install / backfill / seed-now 与 setup 文档)、`package.json` 的 `files` 白名单补 `tools`,新增 **`scripts/verify-wechat-adapters.mjs`**(进 `verify:unit`,10 条源码级断言:文件齐全、**轮询不得出现 `body.innerText`**、**不得用 `setFileInput`**、装饰器注入内联样式、发布前检查存在且**只告警不抛错**、返回行 key 与 `columns` 一一对应、install 清单与磁盘一致、三个坑的注释在位;**已反向验证**:塞回 `body.innerText` 即红)。调研与本机实测全过程沉淀在 wiki 笔记「公众号发布插件调研」。
|
|
160
|
-
|
|
161
|
-
- **v0.22.10**(2026-09-17):**修「写入丢失 `created`/`modified`,新笔记在按修改时间排序的页面上沉到最后一名」**(用户实测报障:日记在「主题页·日志」排 175/175,看着像没被收录)。根因:插件把这两个字段列进 `CLEAN_SKIP_FIELDS`(假设「TW 服务端会补」),但 TW 服务端的 PUT 路由**从不补**时间戳——`getCreationFields()`/`getModificationFields()` 只在 TW 自己的浏览器 UI 里调用,于是插件写的条目落盘只有 tags/title/type;而 TW 的 `sortTiddlers` 对缺失字段取 `fields[sortField] || ""`,`!sort[modified]` 降序时空串直接沉底。**修复**:`buildWriteTiddler()` 统一负责写这两个字段,语义与 TW 编辑器一致——新建 = 都取当前时刻,覆盖 = 保留原 `created`、只刷新 `modified`(基底无 `created` 的迁移存量则补当前时刻);格式用新增 `formatTiddlerDate()`(TW 的 17 位紧凑 UTC,与 `$tw.utils.stringifyDate()` 逐字节一致,别用 ISO)。两者继续留在 `RESERVED_TIDDLER_FIELDS`(`fields` 不得覆盖),只是不再被 CLEAN_SKIP 丢弃。`TiddlyWebClient.put()` 另有 `ensureTiddlerTimestamps()` 兜底(**只补缺、绝不改写**已有值),覆盖剪藏桥 / seed / 配置 tiddler 等手拼 PUT 的路径。顺带统一:`rename`(含引用改写)与回收站恢复改走共享写策略(原先手拼 body 会把旧 `modified` 原样带过去);回收站快照与恢复保留原时间戳(恢复 = 恢复原状,`trash-at` 单独记删除时刻);`/edit` 生成的 TW 草稿携带原笔记的 `created`——TW 保存草稿时草稿字段会覆盖新生成的时间戳,草稿不带它就会把笔记的创建时刻重置成「刚才」。守门:`verify-write-policy` +7 条纯函数(格式往返 / 三种语义 / `$:/` / `fields` 不可覆盖 / 兜底只补不改写),`verify-tools` +5 条 E2E(含用 TW 真实 `!sort[modified]` 过滤器验证排序),selftest 增配置 `created` 保留断言。**存量数据不自动回填**:线上 wiki 1161 个 `.tid` 中 948 个缺 `modified`(多为历史迁移脚本所致),它们仍会在 `sort[modified]` 页面沉底,可用 wiki 根目录的 `backfill-timestamps.mjs` 一次性补齐(默认 dry-run,按 **git 历史**取首次出现/最后变更时间,`--write` 才落盘)。
|
|
162
|
-
|
|
163
|
-
- **v0.22.9**(2026-09-16):**修「TiddlyWiki 编辑者署名被静默清空」**(用户实测报障:控制面板 →「信息」→「基础」里的「编辑者署名」填完保存不了、刷新就没了)。根因是 DSH 启动 TW 子进程时(匿名 loopback 分支)**没传 `anon-username`**:`get-status.js:21` 回落到 `""`,而 TiddlyWeb adaptor 判定登录用的是 `isLoggedIn = json.username !== "GUEST"`(`tiddlywebadaptor.js:93`)——`"" !== "GUEST"` 为真,于是**匿名读者被当成已登录用户**;`syncer.js:281-283` 随即在**每次页面加载**时把 `$:/status/UserName` 覆写成那个空串。名字在当次会话里看着还在,一刷新就没了;而一旦 `$:/config/SyncFilter` 放行了 `UserName`(本仓库 wiki 的现状,有一篇专门的踩坑笔记记录),这个空串还会**同步落盘并进 git**,从「内存里丢了」升级成**真实、可追溯的数据丢失**。现在匿名分支显式传 `anon-username=GUEST`(`wiki.ts` 的 `ANON_USERNAME` 常量):`isLoggedIn` 变 false → syncer **根本不再写** `UserName` → 用户填的名字稳定保留。⚠️ `GUEST` 是**承重的魔法值**,不是随便取的名字:TW 判的就是 `!== "GUEST"` 这个哨兵,填任何人名(含你自己的名字)都仍然算「已登录」,只会把署名覆盖成人名而不是空串。**为什么拖到现在**:`src/host/wiki.ts` 从 v0.16 起就写着一句注释「Passing anon-username/readers/writers here was verified to 401 every request」——它把**两个独立参数**混为一谈,等于给后来每个维护者发了张「此路不通」的牌子。实测(TW 5.4.1)只有 `readers`/`writers` 会关掉匿名访问(`server.js:63-66` 里显式的 `readers` 会让 `(anon)` 不再是 principal,于是 `isAuthorized()` 为 false,`requestHandler` 用当时为 `undefined` 的 `authenticatedUsername` 拼出 `'undefined' is not authorized` 的 401);`anon-username` **单独**传完全安全(`/status` 200、GET 200、PUT 204)。那句注释已改正并写明真实机制。**副作用(外观级)**:`$:/status/IsLoggedIn` 恒为 `no`,TW 的 SyncerDropdown 会多出一个用不上的 Login 按钮;`wiki.js:1690` 的 `generateDraftTitle` 会从 `Draft/Title` 形态切到 `Draft/Attribution` 形态(内核既有行为,与「快速笔记」的 `/edit` 草稿路由无关——后者按 `draft.of` 字段匹配,不依赖标题字面量)。**影响面**:默认 SyncFilter 的用户**不会**丢盘(`$:/status/` 被默认过滤器整个排除,空串走不到落盘),实际受数据丢失影响的只有**已经放行 `UserName` 的人**——所以这一版主要是消除一颗静默的数据丢失地雷 + 拔掉那句误导注释。守门:新增 `scripts/verify-anon-username.mjs`(进 `verify:e2e`,真实 TW 子进程)——`ANON_USERNAME` 必须字面量 `GUEST`、`/status` 必须回该哨兵、落盘署名不得被覆盖、匿名模式仍可正常写条目、auth 模式不受影响且对匿名请求仍 401,外加源码级断言(旧误导注释必须已删、鉴权分支不得顺手传 `anon-username`);**已反向验证**:把 `anon-username` 那行去掉即红 3 条。
|
|
164
|
-
|
|
165
|
-
- **v0.22.8**(2026-09-15):**第六轮代码审计修复**——host 与 client 各做一份只读审计,逐条核实后修 10 处真缺陷,并清掉一轮重复实现与死代码。① **数据丢失(P0)**:`/edit` 仍会把「调用方文本或笔记已保存正文」PUT 进**刚复用**的那个草稿,而那个草稿里装的是用户在 TW 原生编辑器里敲的**未保存内容**——触发路径极常见:打开快速笔记(标题精确到分钟)→ 写 → 关掉 → 同一分钟内再打开 → 草稿被覆盖。现在只有**我们创建的**草稿才由我们填充,复用的仅在调用方显式给了文本时才写。② **设置页超时(P0)**:`fetchJson` 展开 `init` 后又硬编码 `signal`,于是「知识库切换 / 恢复默认 / 重启 TW」的 120s 预算被 15s 掐断——宿主其实切成功了,用户却看到「切换失败」(大 wiki 冷启动实测 44s+),且重试会再走一遍停/启。③ **净化器崩服务**:`�` 这类越界数字实体会让 `String.fromCodePoint()` 抛 `RangeError`,而净化器没有 try——一条笔记正文就能让 `POST /render` 502,打坏回复流工具卡与会话「知识库」Tab。④ **「幽灵草稿」**:保存/「丢弃」后重置编辑器会触发防抖,把「空正文 + 新时间戳标题」又写成草稿,下次打开误报「已恢复未保存草稿」并跳过默认 tag。⑤ `/recent` 一直丢掉 `since`(工具与卡片都发它),于是模型看到过滤结果、人看到未过滤列表——与 v0.20.0 修过的 `/search` 同类漂移。⑥ 隐藏的 TW 面板会因为一个在途 `/status` 响应重新起一轮 30×1.5s 轮询。⑦ `/tw` 代理补齐显式方法白名单。⑧ `/render` 的 404 改为结构化异常(原先按错误消息文本正则,改词即把「不存在」变 502)。⑨ `/get` 改用共享的 `isBlockedProxyTitle()`(原为硬编码字面量副本)。⑩ 侧边栏显示名改完设置即时生效(原只挂载时读一次)。**去重/死代码**:新增 `render-fetch.ts`(唯一渲染调用)、`describeSyncResult()`(同步回执)、`formatLocalMinute()`,`ui-config.ts` 承担全部 `ui.*` 投影;删除生产零调用的 `ensureTwWebHost()`(与 `tw-web-host` seed 重复)与 `listTags()`,多个仅供模块内部使用的 `export` 与「算了却从不显示」的字段一并清除。守门:`verify-render-sanitizer.mjs` +1、`verify-frame-guards.mjs` +3、`verify-audit-fixes.mjs` +3(草稿覆盖那条**已反向验证**:还原修复即红)。
|
|
166
|
-
|
|
167
|
-
- **v0.22.7**(2026-09-14):**设置页「预览注入文本」改为按表单当前值渲染**(用户实测提问「两种内置文本形态差别是什么?为什么选了不同形态后查看当前注入文本显示的东西一样?」)。两种形态的差别其实只在 intro 一段:`slim`(默认)是一句能力概览(实测 1735 字符),`full` 换成由工具注册表实时生成的 15 行参数索引(2504 字符),三块治理约定(写入与并发 / 同步纪律 / 笔记约定)完全相同。但预览按钮此前只发 `GET /admin/prompt`,读的是 **host 已保存的有效配置**,而下拉框的值只活在浏览器 DOM 里——于是「切换形态 → 不点保存 → 点预览」看到的是逐字节相同的旧文本,用户自然以为两种形态没差别。现在:① `prompt.ts` 新增 `normalizePromptPreview()`(把不可信的 body 白名单化成 4 个字段,未知键/错类型丢弃并回落内置默认)+ `describePrompt()`(`applyPrompt()` 与预览**共用同一份拼装**,预览不可能与保存后的注入不一致);② `POST /admin/prompt` 接收 `{enabled,mode,extra,override}` 草稿并渲染全文,**写入零副作用**(不落盘、不触发 `onConfigChanged`),同源守卫 + 畸形 JSON 400;GET 保留为「已保存/正在注入」的读取;③ 设置页按钮改名「预览注入文本(按表单当前值)」并 POST 草稿,标题栏在表单有未保存改动时标注「含未保存的修改,保存后才真正注入」。守门:`verify-prompt.mjs` 新增 3 条纯函数断言(白名单、`describePrompt` 与 `buildPromptText` 逐字节一致、两种形态在草稿里必须不同),`verify-seeds-admin.mjs` 第 8 段把「POST 必须 405」换成 7 条草稿断言(slim/full 草稿不同、extra 追加、`enabled:false` 为空、GET 仍是旧的已保存状态且 `onConfigChanged` 未触发、垃圾字段回落、畸形 JSON 400、跨站 403)。
|
|
168
|
-
|
|
169
|
-
- **v0.22.6**(2026-09-14):**修「快速笔记」两处用户实测报障**。① **原生编辑弹窗里删掉笔记后,弹窗变永久白板、再点「快速笔记」也回不来**。`ui.quickNoteMode=native` 的编辑器是**同一个 iframe 复用**的,而 `openEditorPopup()` 只在 **URL 变了**才给 iframe 赋 `src`(刻意如此:赋 `src` 即整页重载,会丢掉 TW 里未保存的草稿 / 滚动位置 / 撤销栈)。可 TW 编辑器的「删除」是 `tm-delete-tiddler`:它把**原 tiddler 与草稿一起删掉**、并用 `removeTitleFromStory` 把 story 清空——于是弹窗里剩一块白板;而此时主机重建的草稿标题往往与删除前**完全相同**(默认标题是分钟级时间戳 + canonical `Draft of "…"` 复用),URL 不变 → 不重载 → 白板永远摆在那儿,再点按钮也只是「收起/重新打开」同一个空 iframe。现在三处一起兜住:新增 `isEditorPopupBlank()`(同源读 iframe 文档里的 `.tc-story-river`,**一条 `.tc-tiddler-frame` 都没有**即判空;跨源 = TW 里点了外链、文档仍在 loading、读不到文档时**一律不判空**,绝不擅自重载一个可能正在编辑的编辑器),空掉的弹窗在 `openEditorPopup()` 里**强制重载**;`closeEditorPopup()` 同时清掉 `frame.dataset.loaded`,所以「关闭再打开」也一定是新的编辑器载入(关闭意味着丢掉上一次的 view 状态);输入框上方的按钮与 `openNative()` 的「已打开」幂等守卫都改成 `isEditorPopupOpen() && !isEditorPopupBlank()`,用户再点一次就能拿回编辑器。② **card 模式底部操作条把按钮文字压成两行**(用户截图反馈的美化问题)。340px 的卡片里 5 个控件本就放不下,旧 CSS 让 flex 默认收缩,于是「📎 上传」「🕘 最近」「✏️ 在 TW 中编辑」「保存」**每个按钮里的文字都折成了两行**(无头 Chrome 实测:每个按钮 `textRects=2`、高 46px)。现在按钮一律 `white-space: nowrap` + `flex: 0 0 auto`(文字永不折行、永不被压扁),装不下时**整个右组换行**并用 `margin-left: auto` 继续贴右缘,「Ctrl+Enter」提示允许收缩让位——实测全部按钮恢复单行(高 30px),左下是「上传 / 最近 / Ctrl+Enter」、右下是「在 TW 中编辑 / 保存」两行布局。守门:新增 **`scripts/verify-editor-popup.mjs`**(进 `verify:unit`,tsx 直跑源码 + 最小 DOM 打桩,11 条行为断言——同 URL 有内容不重载 / story 被删空必须重载 / 关闭清 `dataset.loaded` / 关闭后重开必重载 / 空白判据的四种保守边界;**反向验证过**:把两处修复改回去会红 3 条),`verify-frame-guards.mjs` 另加 2 条接线断言(空弹窗不得被当成「已打开」)。真机复核:无头 Chrome 里确认「同一个 URL 再赋一次 `src` 确实会重新加载」「清空 story 后重载能恢复内容」,并用真实 CSS + 真实卡片结构对比修复前后的按钮排版。
|
|
170
|
-
|
|
171
|
-
- **v0.22.5**(2026-09-14):**修「大知识库冷启动被误判为启动失败」**(用户实测报障:dsh 启动时 `[dsh-tiddlywiki] startup issue (self-healing is armed): Error: wiki server did not become ready in time`)。根因不是 TW 没起来,而是**就绪判定太急 + 判失败后再也没人复探**。证据来自 `/status` 的环形日志:`01:43:51 spawn … tiddlywiki.js … --listen` → `01:44:11 wiki server did not become ready in time` → `01:44:35 [out] Processing background action …` + `[out] Serving on http://127.0.0.1:52323`。也就是说这个 5099 个 tiddler / 26MB 的知识库在**冷文件缓存**下真的需要约 44 秒才打印 `Serving on`,而旧代码只有**一个 20 秒硬超时**,于是:① 抛错中断整条启动管线——配置 tiddler 没加载、核心 seed(发送给 Agent / 渲染路由 / 同源代理 / markdown 插件)没写、剪藏桥没起,全都会拖到下一次重启;② 写死 `health='failed'` + 一条粘性 error,而**超时后没有任何东西会再探测**,所以 TW 明明在正常服务,`/status`(面板 / FAB / 悬浮提示)却一直报故障。现在把「就绪」变成一条明确的三段策略(新的 `src/host/ready-policy.ts`,纯函数 + 注入时钟,可单测):**软窗口**(默认 60s,可用 `startup.readyTimeoutMs` 在设置页调,夹在 5s–600s)之内安静等待;超过它只写一条 `slow start: /status not ready after Ns — still loading` 并**继续等**(状态保持 `starting`,不再粘住 error);**硬上限 = 3× 软窗口**仍无响应才判失败——并且失败时**不杀子进程**,而是挂一个**有界(10 分钟)、`unref` 的「迟到就绪」后台探测**,TW 真起来的那一刻立刻把状态改回 `running` 并清掉那条旧错误(`ready (late): /status 200`),`stop()` 与新一次 `start()` 都会取消它。`startup.readyTimeoutMs` 走「cordis 配置 < 配置 tiddler」双层,设置页保存后对**下一次启动/重启**生效,无需重启 dsh web。守门:新增 `scripts/verify-ready-policy.mjs`(进 `verify:unit`,tsx 直跑源码 + 虚拟时钟)——归一化/夹取、**44s 冷启动必须判成功**(本次事故形状的回归断言)、慢启动只警告一次、子进程退出 → 立即 `exited`、始终不就绪 → 到硬上限才 `timeout`、轮询节奏有界(慢窗口降到 2s,不空转),外加源码级接线断言(`wiki.ts` 不得再出现 20s 硬编码、必须有 `awaitReady` / `armLateReadyWatch` / `clearLateReadyWatch`,config/index/client 三处链路都在);`verify-resilience.mjs` 增加一条真实 TW 的重启断言(窗口热改 + 夹取 + 仍判就绪)。
|
|
172
|
-
|
|
173
|
-
- **v0.22.4**(2026-09-13):**重构:把 TW frame 的生命周期收敛成唯一一份实现**(无功能变更,纯去重)。v0.22.3 那个「空 `src` 把 DSH 载进 iframe」的 P0 之所以发生,根因不是谁写错了某个判断,而是 `panel.ts`(中央列面板)与 `tw-frame.ts`(右侧栏 tab)**各自抄了一份** `showError` / `showStarting` / `showFrame` / `applyPendingHash` / `fallbackLoad` / `doRefresh`——修 bug 时守卫只补到 panel,tw-frame 那份照旧,同一个坑于是踩了两次(`panel.ts` 靠「`!hidden` ⇒ 已载入真实 twProxy」这个不成文不变量侥幸安全,tw-frame 连这个都没有)。现在这六个函数只在 `tw-frame.ts` 新增的 **`createTwFrameSurface(skin)`** 里各有一份,`createTwFrameController()` 退化成它的 rightbar 薄包装;一个 surface 只提供三样东西:**皮肤**(类名 + 可选内联样式)、**惰性构建点**(`build()` 返回 view,由调用方决定挂到哪里)、**可见性**(`setVisible()`)。`panel.ts` 因此从 469 行降到 280 行,只剩 rect 钉住 / 惰性构建 / 共存 chrome / 面板互斥。顺带三处行为改进(都是去重后的自然结果):① 面板打开链接改为**先 `state.openPanel()` 再 `surface.openTiddler()`**(内核以 `visible=false` 拒绝隐藏 surface,顺序反了链接会静默丢失,脚本里有守门断言);② `setVisible(false)` 现在会**隐藏 frame 并停掉有界轮询**(下次显示重新给满预算);FAB「重载面板」刻意**不受可见性限制**(唯一判据是 `dataset.loaded`)——显式重载请求要能覆盖**关掉的**面板,否则「关掉面板 → 点重载 → 再打开」会看到旧资源,而展示陈旧内容比多一次重载更糟;③ 内核的 `build()` 会记住「DOM 还没建就被要求显示」的情况并在建好后补一次 `/status` —— 此前中心列若在面板挂载之后才出现,面板会停在空壳上直到用户再切一次。守门:`verify-frame-guards.mjs` 加了**计数去重断言**(panel 里出现任何一个生命周期函数就直接判红,反向验证过:植入回来必失败),并新增 **`scripts/verify-frame-surface.mjs`** —— 用 tsx 直跑源码 + 最小 DOM 打桩,真正执行 `createTwFrameSurface()` 跑 16 条行为断言(空 `src` 的两条重载路径、只显示真的载入过 TW 的 frame、同一 URL 不重复赋 `src`、启动/错误/重试恢复、同源 hash 导航与跨源兜底、共享 chip 标签、dispose 摘监听且幂等),已进 `verify:unit`。测试环境提示:本机沙箱里 node 再 spawn 孙进程会崩,`npm run` 偶发段错误,`verify-package-contents` 与 `verify-git-resolve` 的失败属环境问题(手动复核过断言全过),与本版无关。
|
|
174
|
-
|
|
175
|
-
- **v0.22.3**(2026-09-12):**第五轮代码审计的客户端修复版**(host 侧零缺陷:CSRF 门禁 / 片段净化 / SSRF 守卫 / 密钥打码 / 写策略 type 保留 / 乐观并发 / git 互斥 / seed 内容哈希 / 切换回滚逐条对照都成立;`tsc` 与 8 个 `verify-*` 全绿)。① **P0:iframe 空 `src` 被当成合法地址使用**。`tw-frame.ts` 的 FAB「重载面板」处理是 `frame.src = frame.src`、hash 兜底加载是 `frame.src.split('#')[0]` 拼 hash——而 `iframe` 从未赋过 `src` 时,**读 `iframe.src` 得到的是宿主 DSH 页面自己的 URL**。`setVisible(true)` 会在首个 `/status` 回来之前就取消 `hidden`(切走再切回即可命中),于是这两种写法会把 **DSH 页面载进 iframe**:iframe 里再起一份 DSH(重复 FAB、重复全局监听、白屏)。现在两处「整页重载」路径都只认 `dataset.loaded`(只有 `showFrame` 写它)并抽出共享的 `loadableFrameUrl()`,`panel.ts` 原本靠「`!hidden` ⇒ 已载入真实 twProxy」这个不成文不变量侥幸安全,现也改成显式判据。② **P1:主题「活动」单选预选错误,且会影响写入**。`/admin/state` 只回 `info.themes`(**已加载**集合),设置页就拿它的**最后一项**冒充「活动主题」;用户显式激活过非末位主题时,打开设置页显示错误选中项、**不动任何单选直接点「应用主题」也会静默把活动主题改回末位那个**。现在 host 读 `$:/theme` 并在 `/admin/state` 回 `info.themeActive`(去掉 `$:/themes/` 前缀归一),设置页优先用它、猜末位只作兜底,并且只在 catalog 里存在时才认。③ **P1:`applyToFrame` 缺 try/catch**。`setThemeSyncConfig()` 是**同步**遍历调用各 frame 的 applier 的(`doRefresh`/`load` 都在 await 链里),而 `frame.contentWindow.$tw` 这次属性访问在 iframe 导航到跨源页面(TW 里点外链)时**自身**就抛 `SecurityError`(内层各助手的 try/catch 还没轮到),异常直接变成未处理 rejection 并中断那次刷新;现在整段包住。④ **P2 清理**:会话汇总的挂载 effect 补 `mountedRef` 卸载守卫(`genRef` 只认「被更新取代」、覆盖不到卸载,在途请求 resolve 后仍会 setState 并递归再发一轮)、快速笔记按钮的 `fetchUiConfig().then(setMode)` 补 alive 守卫、`note-widget` 卸载不再删除**设置页也在用**的页面级 toast 单例(原来会掐掉别人正在显示的提示);删掉三处无人消费的死标记/死字段(`view.dataset.visible`、`data-dsh-tw-rightbar`、`BuiltUi.recentBtn`、`ConfigField.input`),工具卡标签 key 补下标(同标签重复会撞 key),并把 `panel.ts` 里重复的 `requestRestart` 收敛为复用 `tw-frame.ts` 的那一份。守门:新增 `scripts/verify-frame-guards.mjs`(已进 `verify:unit`)——空 src 陷阱、活动主题优先级、跨源 try/catch、共享 toast、死标记共 13 条断言,其中 `readActiveThemeName()` 走真实函数(前缀 / 裸名 / 缺失 / 读失败),静态断言会剥掉注释行以免匹配到自己的说明文字。
|
|
176
|
-
|
|
177
|
-
- **v0.22.2**(2026-09-12):**修「主题汇总页·模板」seed 内容被渲染成几千遍重复**(用户实测报障)。模板里留了两个「可选」大段落,其中一个标题里有一个**裸 `<$list>`**(没有 `filter` 属性)——TW 渲染时把它当成「列出**全部** tiddler 的列表」,于是整段「可选:把本页收进『一页多主题』tabs」说明被跟着重复渲染几千次(实测渲染出 1.1MB、约 2941 遍重复)。现在模板精简为**一个列表 + 一行指向教程的说明**:删掉两个「可选」段落(自定义 `<$list>` 样式版 / tabs 收录法),自定义需求统一见 [[教程:按主题/标签做汇总页]];模板正文的提示也改为「把正文里的 `主题A` 全部替换」(原来写「两处」,实际有 3 处)。**已有 wiki 的模板不会自动更新**(seed 是 ONE-SHOT):本机线上 wiki 已手工覆盖为新模板并同步 seed 标记哈希;其余 wiki 可在设置页「初始化」里对 `starter-docs` 点「重新初始化」(会覆盖你对模板的改动,改过请先备份)。顺带修好一个校验脚本的兼容问题:`scripts/verify-package-contents.mjs` 假定 `npm pack --json` 返回数组,但 npm 12 返回 `{name:{...}}` 对象,导致「发布包内容」检查假失败(与本次改动无关,但会卡住发布流程)。
|
|
178
|
-
|
|
179
|
-
- **v0.22.1**(2026-09-11):**修首页「快速记笔记」创建出的条目日期时间错误**(用户实测报障)。首页快速笔记在标题留空时用 `<now "YYYY-MM-DD-HHmm">` 生成标题,而 TW 的日期格式 token **只有小写** `YYYY`/`0MM`/`0DD`/`0hh`/`0mm`—**`HH` 根本不是 token**,未知字符被**原样输出**,于是生成的是 `2026-9-11-HH20` 这种标题(还伴随月份/日不补零)。现在改为 `YYYY-0MM-0DD-0hh0mm`,并新增 `verify-constants.mjs` 守门(首页 seed 不得含 `HHmm`)。同一处还把一个反模式改成 TW 的正确命名空间:`$savetitle` 原本写 `$:/state/home-note/last-created`——`$:/state/*` 会被 syncer **落盘进 git**(设置页覆盖层之外的有一份状态污染),改用 **`$:/temp/*`**(tiddlyweb adaptor 显式排除,浏览器端临时状态不进 wiki)。顺带修好一个**一直跑不起来的构建脚本**:`scripts/gen-seed-home.mjs` 的生成模板里有两处反引号没转义(第 148/181 行的 `` `ok:false` ``),整个脚本**语法错误**——也就是说「改首页 → 重新生成 seed」这条路早就断了,本次同时修复。**已有 wiki 的首页不会自动更新**(seed 是 ONE-SHOT):设置页「初始化」里对 `home-index` 点一次「重新初始化」即可拿到修好的首页(会覆盖你对首页的改动,若你改过请先备份;本次作者的线上 wiki 已手工打上同样的两处修改)。
|
|
180
|
-
|
|
181
|
-
- **v0.22.0**(2026-09-11):**知识库位置可在设置页运行时切换 + seed 文档「过期」检测 + 一个隐藏很久的数据丢失竞态**。① **知识库位置可切换**:`wikiRoot`/`wiki` 之前只能写死在 cordis 配置里(改一次要动配置文件 + 重启),现在设置页顶部「知识库位置」显示当前实际目录、这个位置**怎么决定的**(指针文件 / cordis 配置 / 默认值)、指针文件路径与同目录的候选 wiki;填一个绝对路径即可**运行中切换**——停 TW → 释放自动提交与监听 → 改指 → 起 TW → 重载新 wiki 的配置 tiddler → 跑核心 seed(markdown 插件 / 发送给 Agent / 渲染路由 / 同源代理 / 语言)→ 重建 git → **最后**才写指针;目标目录没有 `tiddlywiki.info` 会自动 `--init server` 建一个全新知识库;**失败自动回滚**到原知识库并如实回报 `rolledBack`。选择存在 `$DSH_HOME/dsh-tiddlywiki/location.json`——一个**在 wiki 之外**的指针文件:设置页的覆盖层就存在 wiki 自己的配置 tiddler 里,若「用哪个 wiki」也存那里,切过去就会把这个选择一起丢掉(鸡生蛋);指针损坏只会报告并回退配置默认,绝不乱跑。② **seed 更新检测**:seed 标记从一行 `seeded-once` 升级为 `{version,hashes,at}`——记下**我们写下的内置正文的哈希**,于是设置页能区分「⬆ 内置内容有更新」(可一键「更新到内置版本」)与「✏️ 本地已修改」(覆盖前二次确认);旧标记(无哈希)会显示「更新检测尚未启用」并按文本比对判定归属,**不会**被误判成「用户改过」;`updateAvailable` 只是提示,**绝不自动改写你的 wiki**。③ **文档不再手抄易变事实**:插件说明笔记里的工具清单由**工具注册表生成**(`docNoteText(tools)`)——它此前写死「10 个 agent 工具」而实际已有 15 个。④ **修掉一个潜伏的数据丢失竞态(本次最重要的修复)**:`flushPendingWrites()`(「重启 TW 前把 syncer 队列排干」的哨兵)假设「哨兵落盘 ⇒ 队列已空」,但 TW 的 syncer 会**跳过最近 1 秒内保存过的标题**(`syncer.js` 的 `throttleInterval` / `chooseNextTask`)——哨兵是另一个标题、随时可写,于是它会**插队**先落盘,被 throttle 的写入仍留在队列里、随即被重启吞掉。这个竞态一直存在(AGENTS.md 记录过 v0.19.0「force-all 随机丢 `tw-web-host`」),v0.22.0 新增的 seed 标记写入改变了时序,使它变成**必现**(`verify-seeds-admin` 稳定复现)。现在改为**两段式**:写哨兵 A 等落盘 → 睡一个 throttle 窗口(从 `$:/config/SyncThrottleInterval` 读,默认 1s)→ 再写哨兵 B 等落盘;这期间所有被 throttle 的标题都会变可用并被写出,第二枚哨兵才是真正的队尾。守门:新增 `scripts/verify-wiki-switch.mjs`(真起两个 wiki:切换 / 回滚 / 指针文件 / 非法输入,已进 `verify:e2e`),`verify-seeds-admin.mjs` 增加 5 组内容哈希断言(新鲜 / 用户改过 / 内置更新 / 旧标记两种 / 重新初始化后刷新)。
|
|
182
|
-
|
|
183
|
-
- **v0.21.0**(2026-09-11):**注入提示词改为「默认精简 + 可配置 + 不再过期」**。旧提示词手抄了一份工具参数清单,最后一次同步停在 v0.19.0,此后 v0.19.4(`list_tags limit`)、v0.19.5(`delete`/`attach`/`batch_put` 并发令牌与 `attach` 默认拒绝同名覆盖)、v0.20.1(`append` 的 `fields`)都改过工具层——模型实际看到 **6 处过期签名**。现在:① 新增 `prompt.{enabled,mode,extra,override}` 配置(设置页「系统提示词」区块):默认 `slim`(~1.7KB,只保留工具 schema 表达不了的约定:写入/并发纪律、同步纪律、标签约定、可点击链接格式),`full` 形态额外附参数索引但**由 `tiddlywikiToolSummary()` 从注册表实时生成**,不可能再脱节;② **保存后无需重启 dsh web**——section 即时重新注册,当前会话从下一步起生效(DSH `system-prompt/change` 会更新历史里的系统消息),设置页还能一键**预览**即将注入的全文(`GET /admin/prompt`);③ 用户文本里的 `{{…}}` 会被转义(DSH 对未知变量直接抛错,会炸掉整个系统提示词装配);④ 守门:新增 `scripts/verify-prompt.mjs`(slim 不得出现参数清单 / full 的每个工具与每个参数都必须在场 / 两种形态都必须保留治理约定块 / 转义与 extra-override 语义),已进 `verify:unit`。
|
|
184
|
-
|
|
185
|
-
- **v0.20.1**(2026-09-11):**修复「覆盖已有条目时内容类型被静默重置」**(数据正确性)。`tiddlywiki_put` / `tiddlywiki_append` / `tiddlywiki_batch_put` 覆盖既有条目时会丢掉它的 `type`:`cleanTiddler()` 把 `type` 当成「构造 PUT body 时跳过的字段」,于是 `finalTypeForWrite()` 再补默认值——`put` 把 `text/css` 的样式条目改成 `text/markdown`(整篇 CSS 被当 Markdown 渲染、样式静默失效)、把 wikitext 笔记改成 Markdown;`append` 更彻底:它自己手写 PUT body,连 `type` 都不写,TW 于是回落 `text/vnd.tiddlywiki`(磁盘上 `.md` + `.meta` 变成 `.tid`,`##`/`**粗体**`/表格全按 wikitext 解析)。现在:**覆盖路径一律保留原 `type`**,Markdown 默认值只给**新建**条目(`$:/` 系统条目除外);`tiddlywiki_append` 与 `put` 共用同一套写策略(`buildWriteTiddler`),并补上了此前缺失的 `fields` 参数;`rename` 与回收站恢复同样不再丢类型。回执也补了诚实提示:新建默认时写「新建且未指定,已默认 markdown」,覆盖时若类型真的变了会写「⚠️ 内容类型已从 X 改为 Y」。守门:新增 `scripts/verify-write-policy.mjs`(7 条纯函数单测,已进 `verify:unit`)与 `verify-audit-fixes.mjs` 的 6 条 E2E(css 保持 css / wikitext 保持 wikitext / markdown 追加不变 / 新建仍默认 markdown / `fields.type` 是唯一改类型入口 / rename+回收站恢复保类型)。
|
|
186
|
-
|
|
187
|
-
- **v0.20.0**(2026-09-11):**第四轮代码审计的修复版**(安全 / 用户可见缺陷 / 工程守门 / 死代码清理)。**安全**:① `POST /dsh-tiddlywiki/render` 没有像 `/get`、`/tw`、`/api` 那样拦插件命名空间——TW 的 `/render` 对**任意标题**都渲染,于是 `{"title":"$:/plugins/dsh-tiddlywiki/config"}` 能把配置 tiddler 连 `bridge.token` 与带 PAT 的 `git.remote` 一起渲染出来(本机临时 wiki 实测复现;片段净化器只管标签、管不住正文)。现在抽出一份 `isBlockedProxyTitle()` 并覆盖 `/render`(selftest 断言 403)。② `maskConfigSecrets()` 此前**只遮 token 与 git remote**,而文档一直声称 `auth.password` 也被遮——实测原样返回;现在 `auth.password` 一并打码并附 `passwordSet` 展示位,回填掩码同样被 `stripMaskedSecrets()` 丢弃。**用户可见缺陷**:③ TW 的 `$tw.notifier.display()` 只认**已存在的 tiddler 标题**、不存在时**什么都不做**,而「发送给 Agent」的 `notify()` 传的是自由文本——**所有成功/失败提示都是空操作**(用户点完按钮毫无反馈);现在先写 `$:/temp/dsh/send-to-agent/notice` 再 display(bundle 0.3.4 → 0.3.5)。④ 快速笔记卡片丢掉了 `404 + {notFound:true}` 的响应体,`notFound` 分支永不成立,于是「条目不存在」被显示成「wiki 服务不可用」;`fetchJson` 现在保留 404 正文。⑤ `/edit` 的草稿查找在 v0.19.5 重构时把分支写反:规范草稿**已存在**时反而直接覆盖(丢用户未保存的编辑),扫描到的异构草稿被改名为新条目(留孤儿);恢复 `free ? canonical : 复用既有草稿 ?? 时间戳` 的避让语义,并补 E2E 断言。⑥ 会话「知识库」Tab 的「修改」时间用 `Date.parse()` 解析 TW 紧凑格式(`20260101000000000` → NaN)后原样回显成 17 位数字;改用 `parseTiddlerDate()`。⑦ 设置页点「同步 / 重启 TW」会 `refresh()` 重建整块配置表单,**静默丢弃未保存的输入**;现在只有服务端配置真的变了才重建。⑧ `tiddlywiki_search` 的 `field`/`value` 只在工具层生效,客户端卡片不发送、host 路由也不读——卡片列出的是**未过滤**结果;两侧已打通。⑨ 草稿自动保存调用了带副作用的 `getTags()`,会把 tag 框里没回车的半截词提交成标签;新增纯读的 `peekTags()`。**工程守门**:⑩ CI 之前各自维护一份脚本清单,与 `package.json` 双向漂移——`verify-status-cache` 与 `verify-audit-fixes`(v0.19.5 全部数据安全修复的唯一回归)**从未在 CI 跑过**;现在 CI 只调 `npm run verify:static|verify:unit|verify:e2e|verify:large`。⑪ `verify-render-bundle` 的 `module-type` 断言查的是源码注释而不是 tiddler 字段(假断言,字段丢了照样绿);`verify-package-contents` 不守 `src`/`docs` 也不禁 `scripts/`;均已修正。⑫ 三个浏览器脚本硬编码 `D:/npm-global/...puppeteer-core` 且 Chrome 候选表漂移(Linux 上恒 SKIP),抽出 `scripts/lib/browser-env.mjs`;`waitFor` 与迷你路由分发器各抽成 `scripts/lib/tw-harness.mjs`;`TEXT_LIST_FILTER` 长度预算移进新的 `verify-constants.mjs`(static 档,不再只在两个最重的 TW 套件里断言)。**死代码 / 重复**:⑬ 开启 `tsconfig.noUnusedLocals` 并清掉 16 处死 import/死类型;删 `FLUSH_PROBE_FILE`、`fetchUiConfig({force})`、`sidebar-entry` 的 `initialLabel`、`ToolCallOwnerProps` 未用字段、四个卡片的死 `text` prop、`headerTitle` 的 7 个空 case、`__twDebug`、`panel.ts` 的兼容 re-export、一行包装 `setSessionSummaryTabLabel`;`invalidateStatus()` 由 `invalidateUiConfig()` 一并调用(设置页保存后 FAB/侧栏立即读新值);`GitStatusView` 三份合成一份,`snippetOf`/`safeEqual`/`assertPublicImageUrl` 各合成一份;客户端端点字面量统一收进 `endpoints.ts`;`defineTool` 从不被读取的 `output.schema` 参数移除。**文档**:修掉过期注释与文档(客户端仍写 `/tw/render`、`tool-views` 头部仍称「HTML 来自本地 wiki 所以安全」、`host/routes.ts` 文件头只列 6 条路由、生成的 seed docstring 仍写 `Never throws.`)。守门:`verify-send-to-agent-bundle` 补外层 version 断言与 notify 回归,`verify-secret-masking` 补 auth 用例,`verify-status-cache` 补 invalidate 联动,selftest 补 `/render` 403。
|
|
188
|
-
|
|
189
|
-
- **v0.19.5**(2026-09-11):**第三轮代码审计的修复版**(数据安全 / 正确性 / 客户端网络卫生 / 工具性能)。**数据安全**:① `tiddlywiki_attach` 此前**完全不读旧条目**就 `PUT`——附件标题一旦撞上既有笔记(`会议纪要.png` 之类)会把那篇笔记连 tags/自定义字段/正文一起**静默替换成 base64**,是本轮最危险的一条。现在先读后写(004 = 新建,其余错误照常抛出),撞名时**默认拒绝**并要求 `force: true`(并发令牌只证明「读过」、不等于「同意覆盖」),覆盖时仍保留原 tags 与自定义字段,新建附件也会补 `agent-written`;嵌入笔记(`noteTitle`)同样走写策略。② `/edit`(快速笔记直达 TW 原生编辑器)此前把草稿写死 `text/markdown`,而 TW 保存草稿会把字段抄回原条目——**任何 wikitext 笔记被这样编辑一次就降级成 Markdown**,`!` 标题 / `<$list>` / `[[链接]]` 下次全渲染成源码。现在草稿类型跟随原条目,只有新笔记才回落 Markdown。③ 回收站索引读失败/JSON 损坏时,`delete` 此前把它当「空索引」再全量覆盖 → **之前所有回收站记录变成既查不到、也清不掉的孤儿**(体积还留在 git 里)。现在读失败即中止(新增 `TrashIndexUnavailableError`),损坏也不静默重建,`trash list/restore/empty` 一律显式报错。④ `rename` 先写新标题、再删旧标题,此前若删除失败会**抛裸错**让调用方以为整个重命名没发生;现在如实回报「两份副本都在,请手动删除旧标题」。⑤ `tiddlywiki_delete` 支持 `expectedModified`/`expectedRevision`/`force`——删除比覆盖更具破坏性,此前却没有任何乐观并发保护(`get` → 人类在 TW 里改动 → `delete` 会把新改动一起丢进回收站)。**正确性**:⑥ 会话汇总只探测前 300 篇,却把**所有**条目按探测结果渲染,超出上限的会被谎报成「⚠️ 已删除/不存在」;现在未探测的条目标「(未探测,超出单次查询上限)」。⑦ `/api` 透传补上方法白名单(宿主按 pathname 分发,此前 TRACE 之类的任意方法都会被转发给 TW)。⑧ `/tw` 代理遇到带 body 的 DELETE 不再把请求体留在 socket 上(此前会挂到 30s 超时)。⑨ `/admin/config` 不再把所有失败都当 400(服务不可用/超限走 413/500);`/upload` 的「超限」判定与 `errorStatus` 统一。⑩ `WikiServer` 成功启动时清掉上一次失败的 `error`(自愈成功后 `/status` 与面板提示不再长期显示过期故障)。**工具性能**:⑪ `batch_put` 从「顺序 2N 次 REST」改为 **4 路有界并发**(结果按入参下标回填,顺序与逐条容错完全不变),`autoCommit` 移出循环。⑫ 快速笔记找草稿从「拉整份 listing」改为**先单条 GET 规范草稿名**,listing 只作兜底。**客户端网络卫生**:⑬ 新增共享的 `/status` 读取器(2s TTL + **在途合并**)——此前中央面板、侧栏框架、同步按钮、快速笔记配置**四处各有一份 `fetchStatus`**,页面加载时会同时发多次请求,而 host 每处理一次 `/status` 要起最多 5 个 git 进程;⑭ `ui-config` 的缓存同样改为 promise(并发调用共享一次请求),失败不缓存。**其它**:`ToolsDeps.noteTag` 死代码删除;`put`/`batch_put` 描述改为「防抖自动 commit(默认 60s)」以免模型误以为写完即提交。守门:新增 `scripts/verify-audit-fixes.mjs`(10 条 E2E,真实 TW + 真实路由,每条都能复现旧缺陷)与 `scripts/verify-status-cache.mjs`(5 条单测,`tsx` 直跑源码),均已并入 `verify:unit` / `verify:e2e`。
|
|
190
|
-
|
|
191
|
-
- **v0.19.4**(2026-09-11):**标签列表全面「有界」——最后一个没有上限的列表接口**。`GET /dsh-tiddlywiki/tags` 此前会把**每一个**去重标签(大 wiki 上千个)连同计数全量回给浏览器,而回复流工具卡只是展示前 60 个;`tiddlywiki_list_tags` 更严重——同样全量灌进**模型上下文**。现在:① `/tags` 支持 `limit`(1–500,缺省仍是全量,快速笔记的标签自动补全保留完整词表)与 `sort=alpha|count`(默认 `alpha`,`count` 按使用次数降序),回包新增 `total`(去重后的真实总数)与 `truncated`;② 工具 `tiddlywiki_list_tags` 新增可选 `limit`(默认 200、上限 1000),截断时 render 明说「共 N 个,仅列出最多的 M 个」,不再让模型以为那就是全部;③ 客户端 `TagsCard` 改为直接请求 `?limit=60&sort=count`(不再下载上千条再丢掉),并用 `total` 显示「共 N 个 · …另有 M 个」;④ 顺手消掉重复实现:路由与工具现在共用 `TiddlyWebClient.tagStats()` 一份计数逻辑(此前路由自己遍历一遍,且不跳过 `$:/` 系统标题,两侧口径可能不一致),`/recent`、`/search`、`/tags` 的 limit 解析收进 `readLimit()`/`readOptionalLimit()` 两个助手。守门:selftest 增 6 条断言(截断/`total` 不丢分母/`tags` 与 `items` 同集同序/上限 500/`sort=count` 单调递减/无 limit 时 `truncated=false`),`verify-tools` 增 `list_tags` 的 limit 用例。
|
|
192
|
-
|
|
193
|
-
- **v0.19.3**(2026-09-11):**清掉审计清单里剩下的中优先级项**(安全面 / 健壮性 / 正确性)。**密钥不再外泄**:`bridge.token` / `ui.sendToAgent.token` 会被 `GET /admin/state` 与 `POST /admin/config` 的回包替换成 `********`(并给 `tokenSet` 标记),设置页原样保存不会把真 token 覆盖成掩码、清空即删除;`git.remote` 里的 PAT 同时打码,回填同值也被丢弃。更要紧的是**代理旁路**:`/get` 早先禁止读插件配置 tiddler,但 `/tw` 与 `/api` 会把任意路径转发给 TW 子进程,而它的 TiddlyWeb REST 对 `$:/…` 标题照答不误——`GET /dsh-tiddlywiki/tw/recipes/default/tiddlers/%24%3A%2Fplugins%2Fdsh-tiddlywiki%2Fconfig` 实测能拿到配置正文。现在两个代理都拦截 `$:/plugins/dsh-tiddlywiki/` 命名空间(selftest 有回归断言)。**健壮性**:所有路由统一经 `guardHandler` 包装——此前每个注册都是 `void handleX(req,res)`,而宿主没有 `unhandledRejection` 处理器,任何一次 rejection(含代理里 try 之外的 `new URL()`)都会**直接结束 dsh web 进程**、请求挂死;剪藏桥 listen 之后补上**常驻 `error` 监听**(此前移除唯一监听后,运行期 server error 就是未处理事件 → 崩进程),`stop()` 用 `closeAllConnections()` + 1s 上限,keep-alive 不再是卸载时的死等;`/tw` 代理在客户端断连时**中止上游 fetch 并销毁流**(此前会一直拉到 30s 超时)。**正确性**:会话汇总不再把查询失败(超时/5xx)报成「已删除/不存在」(只有 404 才算不存在),来自请求体/会话日志的 sessionId、rename 旧标题、检索词一律 HTML 转义(TW 会原样透传 HTML,汇总片段又会被注入 DSH 页面),sessionId 还加了字符集白名单;`/upload` 改**原子写**(`wx` + 仅对 EEXIST 走后缀链,修掉 TOCTOU 与「任何 access 错误都当名字可用」)、文件名末尾点/空格先规范化(`evil.html.` 此前能绕过可执行扩展名黑名单);SSRF 白名单补上多播 `224/4`、保留 `240/4`(含 `255.255.255.255`)、`192.0.2.0/24`、`198.51.100.0/24`、`203.0.113.0/24`、`192.88.99.0/24`;`readWikiInfo` 对顶层 `null`/数组给出可读错误而不是 `TypeError`;`/admin/info` 空操作不再重写 `tiddlywiki.info` + 重启 TW,校验失败 400 / 内部失败 500 分开;`/agent/create` 先校验工作模式再建目录(此前未知模式会留下孤儿目录);body 超限统一 413。
|
|
194
|
-
|
|
195
|
-
- **v0.19.2**(2026-09-11):**v0.19.1 的内容 + 让 flush 哨兵真正生效**。v0.19.1 在 npm 上只是被 **staged**(未确认),而那份 tarball 少了下面这个修复,所以补一个版本号(**请以 0.19.2 为准,0.19.1 可以取消掉**)。修复内容:`flushPendingWrites`(「重启 TW 前把 syncer 队列排干」的哨兵)**从来没成功过**——① 它写 `type: 'text/plain'` 却去等 `.tid` 文件,而 TW 的文件系统适配器按内容类型决定扩展名(`text/plain` 实际落 `.txt` + `.txt.meta`);② 它用 `mtime >= startedAt` 判定,而哨兵是紧接在等待之前写的,Windows 上新文件的 mtime 可能落在同一/前一个时钟刻,条件永不成立。两者叠加 → 每次调用都超时返回 false,而**所有调用方只把它当 warning**:`/sync`、工具 `git_sync`、seed 重启与启动路径的「排干 syncer」全部静默失效(启动路径还白等 8 秒)。现在按「文件名含 `dsh-tiddlywiki_flush-probe` + 文件内容含本次随机戳」判定(与扩展名/时钟无关),实测 <1.2s 返回;selftest 加了断言,另修掉一处「轮询结果被忽略就重启」的测试竞态(首轮 GitHub CI selftest 抓到的 404)。**其余同 v0.19.1**:CI 复活(`.gitattributes` LF + sourcemap 可复现)、渲染片段净化(存储型 XSS)、`/note` `/edit` 共享写策略(不再清掉人类笔记的标签/自定义字段)、重启单飞与启动/teardown 握手、`tiddlywiki_get` 字段摊平、`lint` 两个坏检查、客户端泄漏与草稿回写等。
|
|
196
|
-
- **v0.19.1**(2026-09-11):**第二轮代码审计的修复版 + 让 CI 重新变绿**。**顺带抓出一个"静默失效"的哨兵**:`flushPendingWrites` 一直用 `type: 'text/plain'` 写哨兵、却去等 `.tid` 文件(TW 的文件系统适配器按内容类型决定扩展名,`text/plain` 实际落 `.txt` + `.txt.meta`),叠加 `mtime >= startedAt` 的判定在新写入的同一时钟刻可能永不成立 → 每次调用都超时返回 false,而所有调用方只把它当 warning。也就是说 v0.19.0 声称的「重启前排干 syncer」**其实一直没生效**(`/sync`、工具 `git_sync`、seed 重启与启动路径全都受影响,启动路径还会白等 8 秒)。现在改成**按内容随机戳匹配**(不依赖扩展名、不依赖文件时间),实测 <1.2s 返回,并有 selftest 断言守门。**CI 其实是红的**:`lib/index.js.map` 内嵌源文件原文(sourcemap 的 `sourcesContent`),Windows 工作区是 CRLF、GitHub runner 检出是 LF,Linux 上重建出的 map 与提交版必然不同 → `static` 作业的 `git diff --exit-code -- lib/` 每次失败,而它一挂,另外三个 job(`needs: static`)**根本不会跑**(v0.19.0 声称的四档守门实际停摆)。修法:新增 **`.gitattributes`(`* text=auto eol=lf`)** 把工作区钉成 LF 并重建提交 lib/;顺带补上 **send-to-agent 的「bundle ↔ seed 逐字节」守门**(render 早就有,缺这条时「跑了 build 忘跑 gen」会 CI 全绿却发旧按钮)、`prepublishOnly` 预检、CI 失败时能看懂的错误提示。**安全(存储型 XSS)**:回复流卡片与会话汇总把 TW 片段 `dangerouslySetInnerHTML` 进 DSH 页面,而 TW 的解析器只剥 `on*`——实测 `<iframe src="javascript:…">`、`<a href="javascript:…">`、`<form action="javascript:…">` 全部原样通过(任何写进 wiki 的正文——agent 笔记、剪藏、导入——都能在 DSH 同源页面执行脚本并调用未鉴权的 `/dsh-tiddlywiki/*` 路由)。新增 **host 侧片段净化器**(`src/host/sanitize.ts`:丢 script/style/iframe/object/embed/form/svg/math…、丢 `on*`/`srcdoc`/`srcset`、URL 属性只放行 http(s)/mailto/tel/相对路径/栅格 `data:image/*`,并先做实体解码挡 `javascript:` 这类混淆),新增 **`POST /dsh-tiddlywiki/render`** 作为客户端唯一渲染入口(客户端不再直连 TW 的未净化 `/tw/render`,因此**老 wiki 也立刻受保护**);配套 payload 回归 `scripts/verify-render-sanitizer.mjs`(已进 CI)。**数据安全**:`/note` 与 `/edit` 此前不读旧条目就整体 PUT,用同名标题保存会**清掉人类笔记的标签与自定义字段**——现在两条人类路径与 agent 工具共用新的 **`src/host/write-policy.ts`**(先读后写、保留字段、`agent-written` 只给 agent 新建的条目);快速笔记卡片对「🕘 最近」载入的笔记回传 **`expectedRevision`**,人类在 TW 里的并发修改会让保存得到 **409** 而不是被覆盖;**重启 TW 前先排干 syncer**(`/sync` 与工具 `git_sync` 此前漏了 flush 哨兵——刚保存的笔记可能被重启吞掉);`WikiServer.restart()` 改为**内部单飞**(admin 路由不受 routes 层互斥保护,并发重启会留下孤儿 TW 进程);启动任务是 fire-and-forget,teardown 现在**先置 `disposed` 并等它收尾**(插件热更新/禁用时不再「disposer 先跑完、startup 后 spawn」留下孤儿进程与泄漏监听)。**工具层**:`tiddlywiki_get` 的自定义字段此前渲染成 `fields=[object Object]`(单条 GET 的 `fields` 嵌套没摊平)——`q`/`due`/`clip-url` 对模型完全不可见,已修;`tiddlywiki_lint` 的死链检查把**每一条指向图片/附件的链接**都误报成死链(标题集合取自排除二进制的列表),`missing-type` 则**永远不触发**(TW 服务端会给无 type 的条目补 `text/vnd.tiddlywiki`,改用 `[!has[type]]` 过滤器判定),描述与 `checks` 里的 `large-binary` 从未实现,已删除;`tw-api` 的瘦列表(`/tags`、lint 用)加短 TTL 缓存。**客户端**:TW 就绪重试的 40×150ms 定时器、`sync-button` 卸载后的迟到回调、`injectStyles` 的共享 `<style>` 被旧 disposer 删掉、渲染阶段改模块级缓存(改到 effect 阶段)、`tiddlywiki_list_tags` 卡片不截断(现最多 60 个 + 提示余量);快速笔记「在 TW 中编辑」成功后不再把同一份内容重新写成「未保存草稿」。**工程**:工具层 E2E 从 10 个工具补齐到 **15 个**(并断言一个都不能少)、selftest 新增「人类写路径保留字段 / 409 冲突」与「host /render 净化」端到端断言、`package-lock.json` 版本与 npm 描述同步。
|
|
197
|
-
|
|
198
|
-
- **v0.19.0**(2026-09-11):**按代码审计结论做的全量修复 + 一批新能力**(安全 / 数据安全 / 工具层 / 客户端 / 工程)。**安全**:① 每个路由现在**校验 HTTP 方法**(此前 `http.ts` 主动放行 GET、宿主按 pathname 分发,于是 `GET /dsh-tiddlywiki/sync` 会 pull+commit+**push**、`GET /restart` 重启 TW、`GET /upload` 落文件——任意网页一张 `<img>` 即可触发;实测复现,现已一律 405 且无副作用);② 剪藏桥 SSRF 守卫的 IPv6 判定改为**按字节解析**(此前 `::ffff:7f00:1`、`0:0:0:0:0:0:0:1`、`::0:1` 等回环写法全部放行——实测复现);图片下载改为**逐跳 pin 住已校验的 IP**(堵 DNS rebinding TOCTOU)、**流式限长**(不再先全量缓冲再判 15MB)、禁用透明解压;③ TW 子进程的 spawn 日志**不再写明文口令**(该日志经无需认证的 `GET /status` 返回),`/status` 还会脱敏 git remote 里的 token、剪藏桥健康接口不再回显 port/tag/tokenSet、`/get` 不再暴露插件配置 tiddler、`/agent/sessions` 补上 token 守卫、token 比较改常数时间、`/tw` 代理改为**流式转发**(大附件不再整包进内存)、上传拒绝 html/svg 等可在同源执行的文件、`/note`/`/edit` 拒绝 `$:/` 标题。**数据安全**:④ 新增 `tiddlywiki/markdown` **插件自举**——全新 wiki 是用 `--init server` 建的,不含 markdown 插件,而插件默认把每篇笔记写成 `text/markdown`,此前新用户开箱即见 Markdown 源码;⑤ `tiddlywiki_put` 覆盖已有笔记时**不再丢掉原有的标签与自定义字段**(PUT 是整体替换,旧实现只发 title+text;单条 GET 的自定义字段还嵌套在 `fields` 里,一并修掉);⑥ 新增**乐观并发**:`expectedRevision`/`expectedModified` + `force`,人类在 TW 编辑器里的改动不再被静默覆盖;⑦ `tiddlywiki_delete` 默认**软删除进回收站**(`tiddlywiki_trash` 可 list/restore/empty);⑧ `readWikiInfo` 只把 ENOENT 当「没有配置」(此前读失败=空配置,设置页保存会整文件覆盖 `tiddlywiki.info`),写入改为备份 + 原子替换;seed 的 `tw-web-host` check 不再吞错;重启 TW 前用**flush 哨兵**排出 syncer 队列(force 重新初始化曾随机丢掉 `tw-web-host`)。**工具层(10 → 15 个)**:新增 `tiddlywiki_append`(增量追加 / 段落定位)、`tiddlywiki_backlinks`、`tiddlywiki_attach`(本机文件或公网 URL → 二进制附件)、`tiddlywiki_lint`(垃圾标签 / 死链 / 空笔记 / 缺内容类型)、`tiddlywiki_trash`;`search` 改为**相关度排序 + 命中处上下文片段 + 字段过滤 + limit 上限 + TW 紧凑日期解析**(此前 `since` 过滤恒为空)、`list_tags` 不再统计只挂在二进制附件上的标签、`batch_put` 的单条失败不再被参数预校验整批打断、列表加短 TTL 缓存。**客户端**:草稿卸载/刷新前落盘、草稿按窗口分键(不再跨标签页互相覆盖)、`session-summary` 自愈循环与卸载后 setState 修复、中央面板 `hidden` 状态修正、三个常驻定时器加上停止条件、wiki 链接不再吞掉 Ctrl/中键、编辑器弹窗补 `role=dialog` 与焦点、列表项键盘可达、21 处 `:focus-visible`、移动端 `touch-action`、`numField` 的 `0` 不再被吞。**工程**:新增 **GitHub Actions CI**(静态检查 + 自测 + 单元 + E2E 四档,含 `git diff --exit-code -- lib/` 抓「改了 src 忘了 build」)、9 个新验收脚本(auth 模式此前零覆盖、工具层、seed 读失败反例、git 冲突、崩溃自愈、大 wiki、render bundle 逐字节、发布包内容、版本一致性)、`npm run verify:*` 方便本地一键跑、`lib/client.bundle.js` 不再入库、AGENTS 的 `grep @deepseek-ai` 检查修正为「真实 import」。**升级提示**:重启 dsh web 后新会话生效;客户端改动需刷新页面。
|
|
199
|
-
- **v0.18.0**(2026-09-10):**一轮全量代码审计后的修复版**(数据安全 / 安全面 / 前端稳定性 / 工程卫生,共 40+ 项)。**数据不再静默丢失**:① seed 只在**真的 404** 时才认为条目缺失(此前任何读取错误都被当「不存在」,瞬时故障会让非 force 的启动 seed 覆盖你改过的同名笔记、`$:/DefaultTiddlers`、send-to-agent / render bundle);② `wiki/.gitignore` 不再每次启动被整份重写(用户自定规则改前会被冲掉);③ 配置 tiddler 读失败不再清空内存覆盖层(随后在设置页保存一项配置会丢掉其余全部覆盖);④ 只有 **render-route 真的重写后**才重启 TW,且要等它落盘(否则重启会丢掉未 flush 的写入——这条是本版新引入又被测试抓出的回归)。**正确性**:`/render` 现在按 tiddler 自己的 `type` 渲染——**Markdown 笔记在回复流卡片/汇总里终于按 Markdown 渲染**(此前硬编码 wikitext,`## 现象` 会被当成 wikitext 列表,实测复现)。**安全**:所有写路由(`/admin/*`、`/note`、`/edit`、`/upload`、`/sync`、`/restart`、`/api`、`/tw` 的写方法)加**同源/CSRF 守卫**(跨站写请求 403,跨站 GET 不受影响);剪藏桥图片下载加 **SSRF 守卫**(仅公网 http(s),逐跳校验重定向,拒绝回环/内网/云元数据地址);`bridge.port` 设置页改动现在**真的生效**(此前只读 cordis 基座);`auth.username/password` 从此**可用**(内置 REST 客户端与就绪探测带 Basic 认证,代理转发 `WWW-Authenticate` 让浏览器弹登录框,此前配置了用户名会让插件 20s 后启动失败)。**前端**:编辑器弹窗不再因同草稿重开而整机重载、并纳入面板互斥;中央面板自愈列元素重建;右栏 Tab 重新可见会重新探测状态;侧边栏入口插入不再抛 `NotFoundError` 连带拖垮 FAB/面板;同步中不再被 30s 轮询覆盖状态;`injectStyles` 可卸载(热重载拿到新样式);每处 mount 独立 try/catch;面板的 DOM 观察/滚动测量合并到 requestAnimationFrame(流式输出时不再每帧强制样式重算)。**工具与仓库**:`put`/`batch_put` 不再把读取失败当新条目而误打 `agent-written`,`fields` 不能覆盖 `title/text/tags`,`batch_put` 逐条报告失败;git 的 commit 与 pull 串行化(不再争 `.git/index.lock`);`/status` 的 git 摘要加 2s 缓存(每次调用少跑 5 个 git 进程);请求中途断开不再挂住路由;自动端口被占时会重新探测;`tiddlywiki.info` 损坏不再让后台 500;会话汇总对后代会话/条目数设上限。**工程卫生**:bundle 版本单一来源(`scripts/bundle/versions.mjs`,渲染路由 0.2.0)、生成器从 bundle 读版本(外层 0.3.2 漂移修正为 0.3.4)、发布包不再带 `client.bundle.js` 中间产物、`verify-*` 假阳性修复(未 await 的用例、被当成断言的 console.log)、selftest 失败也会回收 TW 子进程、`gen-seed-home` 默认剥离私有人口、`verify-menubar-theme` 不再默认打作者本机端口。
|
|
200
|
-
- **v0.17.0**(2026-09-10):**移除:DSH Better Sidebar(dsh-better-sidebar)Tab 集成**。旧版通过 `ctx.betterSidebar.registerTab({id:'dsh-tiddlywiki'})` 注册的 tab kind 会与其它注册方冲突,浏览器控制台报 `sidebarRight: tab kind "dsh-tiddlywiki" is already registered (extension)`;本版**整体删除**该注册路径(删除 `better-sidebar-tab.ts`、client 入口的挂载块,以及 `ui.showBetterSidebarTab` 配置项/设置页开关),插件不再引用 `ctx.betterSidebar` 服务。与 dsh-better-sidebar 的**界面共存**(中央面板 z-index 让位于其侧边栏按钮)保留;右侧边栏入口请用 DSH 原生 rightbar(`ui.showRightbarTab`)。**更新后刷新页面**生效;旧版可临时用 `ui.showBetterSidebarTab=false` 规避冲突。
|
|
201
|
-
- **v0.16.28**(2026-09-10):**修:拖拽版剪藏书签在知乎等真实页面报 `SyntaxError: Unexpected token ';'`**。根因(真实 Chrome 实验证实):0.16.27 把书签 href 做 **HTML 实体转义**(`"`/`&`/`<`),但浏览器的 `a.href` **不做实体还原**——拖拽成书签后存的就是实体文本,执行时即解析失败。修复:href 改为 **percent-encoding**(`CLIP_BRIDGE_DRAG_HREF`,属性里只剩 `%XX`,天然无需任何属性转义);Chrome 执行 `javascript:` URL 前会 percent 解码(实测通过)。浏览器 E2E 升级为**直接以编码 href 走 `location.href` 执行**(完全等价拖拽书签的执行路径)并断言浮层弹出;静态验收改为 `decodeURIComponent(href) === 代码常量` 一致性校验。行为不变——**已拖过旧版书签的用户需重新初始化 seed 后重新拖一次**(旧书签仍然坏的,因为里面的代码是实体文本)。
|
|
202
|
-
- **v0.16.27**(2026-09-10):**剪藏书签支持「拖拽安装」**。seed 文档新增一个**可拖拽按钮**(markdown 内嵌原始 HTML 锚点,href 即书签代码)——按住拖到浏览器书签栏/地址栏松手即装好,免去复制粘贴;代码 fence 保留作兜底。书签代码抽成单一常量 `CLIP_BRIDGE_BOOKMARKLET`(锚点 href 做 HTML 实体转义),并新增**同步一致性校验**(`verify-clip-bridge.mjs`:href 解码后与代码逐字相等)与**浏览器 E2E**(`verify-clip-bridge-browser.mjs`:真实 TW + 无头 Chrome 渲染真实 seed 文档,断言 javascript: href 原样保留 / draggable / 解码一致 / 真浏览器里执行书签弹出浮层 / 无 JS 异常)。
|
|
203
|
-
- **v0.16.26**(2026-09-10):**修:剪藏书签在真实页面上崩溃**(`Cannot set properties of null (setting 'value')`)。根因:浮层字段在 `p` 尚未挂到 document 前就用 `document.getElementById` 取值(脱离文档树查不到 → null);同时 `cb_*` id 可能与页面自身元素冲突。修复:所有浮层字段改为 `p.querySelector` 作用域内查找 + 字段赋值挪到 `appendChild` 之后;并把书签放进 **jsdom 真实 DOM 端到端跑通**(浮层/填充/选图/提交载荷/结果态 12 项全过)作为验证,`verify-clip-bridge.mjs` 新增静态回归断言(禁未限定 getElementById + 挂载时序)。行为无变化,仅书签代码(seed 文档)修复。
|
|
204
|
-
- **v0.16.25**(2026-09-10):**剪藏桥支持图片剪藏(浮层选图)**。书签升级为**选择式浮层**:可改标题/编辑选中文字,浮层列出页面图片缩略图(自动跳过 <80px 小图标与 data:/blob: 占位、去重,`og:image` 标「封面」并默认勾选)勾选图片后一键剪藏。图片由桥(DSH 进程侧)**下载字节存为二进制附件 tiddler**(`type: image/*` + base64 正文,即 TW 原生附件形态;`clip-url`/`clip-note` 溯源),笔记改用 wikitext 用 `[img[标题]]` 内嵌展示;某张下载失败自动降级为链接、不阻塞整次剪藏;纯文字剪藏保持原 markdown 路径不变。安全不变(127.0.0.1 + Host 校验 + 可选 token;下载带 referer/UA 应对常见防盗链)。验收新增 image-flow 测试 + 书签语法检查(`verify-clip-bridge.mjs` 24 项全过)。
|
|
205
|
-
|
|
206
|
-
- **v0.16.24**(2026-09-09):**新:本地剪藏桥 + 书签小工具**。DSH 进程内新增只监听 **127.0.0.1** 的 HTTP 桥(配置组 `bridge.*`:`enabled` 默认关、`port` 默认 8618、`token` 可选共享口令、`tag` 默认 `clip`)——浏览器书签(JS 小工具)把当前页标题/URL/选中文字 POST 给它,经唯一写入通道落进 wiki(markdown、重名自动 `(2)` 去重、默认 `clip` 标签、随 wiki 自动 git commit)。安全:Host 头白名单防 DNS rebinding、CORS/PNA 预检放行(https 页面书签可用)、`bridge.token` 强烈建议设置。附**可选 seed** `clip-bridge`:「本地剪藏桥(书签小工具)」说明文档(含书签代码/启用步骤/安全说明/curl 用法,打 `dsh-docs` 标签进「📚 插件文档」栏,可反初始化);设置页新增 4 个配置字段。`enabled`/`token`/`tag` 设置页保存即生效,改端口需重启 dsh web。
|
|
207
|
-
|
|
208
|
-
- **v0.16.23**(2026-09-09):**新:DSH Better Sidebar(dsh-better-sidebar)集成**。插件通过 `ctx.betterSidebar` 服务注册 TiddlyWiki 为 Better Sidebar 侧边栏的 tab 类型——+ 菜单点击即以 tab 打开完整 TW 编辑器(同源代理 iframe,与聊天并排);TW iframe 机制抽成共享模块(rightbar/better-sidebar 复用:lazy-load / status 轮询 / 主题同步 / hash 导航 / 互斥协议),回复流 wiki 链接在同一处统一路由到任一可见的侧边 TW tab。配置 `ui.showBetterSidebarTab`(默认 true)可开关;未安装 Better Sidebar 自动跳过。**更新后刷新页面**即可看到入口。
|
|
209
|
-
- **v0.16.22**(2026-09-09):**seed 体系三层化 + 文档中心起步包**。① 分层:**核心**(自动、不可移除:发送按钮/渲染路由/代理基址)+ **起步**(首次安装默认写、可移除:插件说明 + 新增「示例与文档」seed——主题汇总模板/教程/三个示例主题页)+ **可选**(手动:首页/所有文章/新增「自定义样式」seed/顶栏主题);所有种子**安全跳过**(同名 tiddler 已存在绝不覆盖)。② 新增 **`dsh-docs`** 文档合集约定:所有 seed 文档打该标签,首页「📚 插件文档」tabs 栏自动收录。③ seed 版首页改为**通用版**(`gen-seed-home --strip-private`:剥离作者私有人口,注入文档栏)。④ **修**:首页快速记笔记的 tags 筛选器 bug(`then[[todo]]` 操作数双括号导致「筛选器错误」被拆成 6 个 tag → 改为 `then[todo]`)。
|
|
210
|
-
- **v0.16.21**(2026-09-09):**新:DSH 右侧边栏(rightbar)集成**。注册 TiddlyWiki 为右侧栏 tab 类型——rightbar 首页出现「**TiddlyWiki 知识库**」入口盒,点击即在右侧栏以 tab 形式打开完整 TW 编辑器(同源代理 iframe,跟随 DSH 主题),**与聊天并排**;配置 `ui.showRightbarTab`(默认 true)可开关。**更新后刷新页面**即可看到入口。
|
|
211
|
-
- **v0.16.20**(2026-09-09):**修:`search`/`recent` 在大 wiki 上超时**。根因:列表请求把全部 tiddler(含图片等二进制附件)的 base64 正文拉回来(2418 个 tiddler ≈ 515MB/17s)。修复:`search`/`recent`/`get` 在**服务端**只取文本 tiddler(无 `type` 或 `text/*`);二进制 tiddler 完全排除,`get` 对二进制只回元数据。实测 515MB/17s → **6.24MB/0.3s**。
|
|
212
|
-
- **v0.16.19**(2026-09-07):**修:会话「知识库」Tab 把汇总当源码显示**——TW 5.4.1 把 `$:/temp/` 条目一律按代码块渲染;改为 `/tw/render` 原生片段管线(与回复流工具卡同链路),不再用 iframe/story view。
|
|
213
|
-
- **v0.16.18**(2026-09-07):README 全面精简重写;seed 优化——首页 seed 跟随 wiki 现状、修「所有文章」回主页死链、doc-note 文案修正。
|
|
214
|
-
- **v0.16.17**(2026-09-07):点击快速笔记可选直达 TW 原生编辑页(`ui.quickNoteMode`)+ 修「在 TW 中编辑」弹窗 ✕ 关不掉。
|
|
215
|
-
- **v0.16.16**(2026-09-07):修会话知识库 Tab 误入编辑草稿显示源码——三层防误编辑(清残留草稿/吞草稿创建/禁 ✏️)。
|
|
216
|
-
- **v0.16.15**(2026-09-07):`put`/`batch_put` 未指定类型自动默认 `text/markdown`;警告 `fields.type` 是内容类型保留字段。
|
|
217
|
-
- **v0.16.14**(2026-09-07):修知识库 Tab「佚失条目」根因——浏览器端 TW 天生排除 `$:/temp`,改为 iframe 就绪后注入 + 原生 hash 导航。
|
|
218
|
-
- **v0.16.11**(2026-09-06):新增会话顶部「知识库」Tab(本会话相关 wiki 汇总,volatile `$:/temp`)。
|
|
219
|
-
- **v0.16.0**(2026-09-05):回复流原生 TW 工具卡片 + 可点击 wiki 链接(新增 `render-route` seed)。
|
|
220
|
-
- **v0.15.0**(2026-09-05):初始化不强制 + 可反初始化(seed 拆核心/可选两层)。
|
|
221
|
-
- **v0.14.0**(2026-09-05):「发送给 Agent」按钮独立图标 + 附加说明 + 权限选择。
|
|
222
|
-
- **v0.13.0**(2026-09-04):menubar 顶栏主题自适应。
|
|
223
|
-
- **v0.12.0**(2026-09-04):新默认主页 + 「所有文章」两列分页页。
|
|
224
|
-
- **v0.11.0**(2026-09-04):「发送给 Agent」支持选择工作模式(Agent 预设)。
|
|
225
|
-
- **v0.10.0**(2026-09-04):统一 seed 注册表 + 设置页「初始化」。
|
|
226
|
-
- **v0.8.0**(2026-09-04):嵌入式 TW 跟随 DSH 深浅主题。
|
|
227
|
-
- **v0.7.0**(2026-09-03):TW 一键把当前笔记发送到 dsh 会话。
|
|
228
|
-
- **v0.6.0**(2026-09-03):同源代理(远程访问核心)。
|
|
229
|
-
- **v0.5.0**(2026-09-03):Agent 工具扩充 + 快速笔记草稿自动保存 + 统一知识库 FAB。
|
|
230
|
-
|
|
231
|
-
---
|
|
232
|
-
|
|
46
|
+
## 历史版本(一句话,150 条,新的在上)
|
|
47
|
+
|
|
48
|
+
| 版本 | 日期 | 变更 |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| v0.30.65 | 2026-10-09 | 新增 tiddlywiki_replace 工具(笔记正文局部替换) |
|
|
51
|
+
| v0.30.63 | 2026-10-09 | 默认提示词把「笔记内引用」与「聊天引用」拆成两条链接规则;`config.prompt.override` 设置页说明勘误 |
|
|
52
|
+
| v0.30.62 | 2026-10-09 | 第二轮审查三类修复(回收站两种格式合并 / `git_sync`·`git_resolve` 改用会话作用域目录 / 剪藏目标库没在跑改 503 点名)+ 提示词 −14% |
|
|
53
|
+
| v0.30.61 | 2026-10-09 | 修 tiddlywiki_trash list 少报一条 —— TW 给块级片段的首元素套 <p> |
|
|
54
|
+
| v0.30.60 | 2026-10-09 | 修 v0.30.59 的 tiddlywiki_trash 静默谎报「回收站是空的」 |
|
|
55
|
+
| v0.30.59 | 2026-10-09 | 软删除改为「跟着 wiki 已有的回收站机制走」;同时把一条会误导人的注释改成实测事实 |
|
|
56
|
+
| v0.30.58 | 2026-10-09 | tiddlywiki_git_sync 不再要求显式配 git.remote —— 配置为空时回落到该库所在仓库自己的 origin |
|
|
57
|
+
| v0.30.57 | 2026-09-30 | 给 AGENTS.md 的注入预算装上守门 |
|
|
58
|
+
| v0.30.56 | 2026-09-30 | AGENTS.md 的 wiki 指针现在有守门了 |
|
|
59
|
+
| v0.30.55 | 2026-09-30 | 把新守门接进 prepublishOnly + 修 verify-all 的收尾 |
|
|
60
|
+
| v0.30.54 | 2026-09-30 | 铁律 #12 终于有了执行者 —— 范围本身一致 |
|
|
61
|
+
| v0.30.53 | 2026-09-30 | 让「本地全绿 = 可以提交」这条等式真正成立 |
|
|
62
|
+
| v0.30.52 | 2026-09-30 | 新增 `verify:sync` —— `lib/` 与 `src/` 的同步此前只有 CI 查(全仓唯一「本地全绿、CI 才红」的类别) |
|
|
63
|
+
| v0.30.51 | 2026-09-30 | 把「barrel 公共面」从人肉纪律变成机器棘轮 |
|
|
64
|
+
| v0.30.50 | 2026-09-30 | index.ts 724 → 674:拆出自举阶段 `index-startup.ts` |
|
|
65
|
+
| v0.30.49 | 2026-09-30 | 修 v0.30.48 引入的真缺陷 + 补「barrel 公共面」守门 |
|
|
66
|
+
| v0.30.48 | 2026-09-30 | index.ts 955 → 724:拆出路由 / admin 装配 `index-routes.ts` |
|
|
67
|
+
| v0.30.47 | 2026-09-30 | note-widget 588 → 573:拆出底栏 `note-widget-foot.ts` |
|
|
68
|
+
| v0.30.46 | 2026-09-30 | note-widget 615 → 588:拆出上传接线 `note-widget-upload-ui.ts` |
|
|
69
|
+
| v0.30.45 | 2026-09-29 | note-widget.ts 拆分第四轮 —— 抽出「保存 / 在 TW 中编辑」整组(677 → 615 行),并在同一轮 —— 其余守门一条都不用改 |
|
|
70
|
+
| v0.30.44 | 2026-09-29 | note-widget 750 → 677:拆出草稿生命周期整族(两个状态跟着函数走) |
|
|
71
|
+
| v0.30.43 | 2026-09-29 | note-widget 817 → 750:拆出卡片定位与拖动 `note-widget-placement.ts`;顺手修 7 处写错版本号的注释 |
|
|
72
|
+
| v0.30.42 | 2026-09-29 | index.ts 1036 → 955:拆出纯配置面 `index-config.ts`(先切纯的那半边) |
|
|
73
|
+
| v0.30.41 | 2026-09-29 | note-widget.ts 拆分第一轮 —— 抽出两片,993 → 817 行 —— 守门同步三处(都是"钉语义不钉排版") |
|
|
74
|
+
| v0.30.40 | 2026-09-29 | tw-frame.ts 内核抽出 tw-frame-hash.ts —— 关键设计:只读状态用访问器传,不改名;守门同步 |
|
|
75
|
+
| v0.30.39 | 2026-09-29 | tw-frame 状态合并第二片:`pendingHash` / `wikiSwitchPending` 进 `surfaceState`(行为零变化) |
|
|
76
|
+
| v0.30.38 | 2026-09-29 | tw-frame.ts 内核:第一步「状态合并」起手 —— 为什么不是"直接抽取";先量爆炸半径再动手 |
|
|
77
|
+
| v0.30.37 | 2026-09-29 | 给 v0.30.36 补上缺的那条行为守门 |
|
|
78
|
+
| v0.30.36 | 2026-09-29 | 补完 v0.30.35 的另一半:未知变化 ⇒ 保守重启该仓库下的全部库 —— 仍未加行为守门 |
|
|
79
|
+
| v0.30.35 | 2026-09-29 | `pull` 的 diff 失败不再被当成「没有变化 ⇒ 不重启任何库」(本版修「不再沉默」那一半) |
|
|
80
|
+
| v0.30.34 | 2026-09-29 | 把三个「手动浏览器守门」写进文档 |
|
|
81
|
+
| v0.30.33 | 2026-09-29 | ConfigStore 的写路径:核实后决定「不改」,并把理由写具体 |
|
|
82
|
+
| v0.30.32 | 2026-09-29 | 给「共享写策略」补上调用点守门 |
|
|
83
|
+
| v0.30.31 | 2026-09-29 | 把「共享写策略」的例外清单写进代码与铁律 |
|
|
84
|
+
| v0.30.30 | 2026-09-29 | $:/language 的条件写入改走共享写策略 |
|
|
85
|
+
| v0.30.29 | 2026-09-29 | wechat-publish.ts 拆出「契约层」 —— 先核实再动手 |
|
|
86
|
+
| v0.30.28 | 2026-09-29 | tw-frame.ts 拆出「公共 API 层」 —— 先证明了无环才动手;两处 tabLabel 直接读改成 getTabLabel() |
|
|
87
|
+
| v0.30.27 | 2026-09-29 | seeds.ts 拆出「注册表装配层」 —— 分界线是「策略 vs 编排」;依赖方向单向;搬迁手法升级为「编译器驱动」 |
|
|
88
|
+
| v0.30.26 | 2026-09-29 | 补上 v0.30.22 漏掉的用户文档 |
|
|
89
|
+
| v0.30.25 | 2026-09-29 | admin-routes.ts 拆出最后两条业务路由 —— 依赖面这次是「按代码生成」而不是手抄;两次自我纠错值得记下来 |
|
|
90
|
+
| v0.30.24 | 2026-09-29 | admin-routes.ts 再拆出「状态 / 插件·主题·语言」那一组 |
|
|
91
|
+
| v0.30.23 | 2026-09-29 | 核实后把一条审计假阳性(两条看似冗余的 id 正则)就地钉死 |
|
|
92
|
+
| v0.30.22 | 2026-09-29 | 修 tiddlywiki_attach 把文本文件存成 base64 |
|
|
93
|
+
| v0.30.21 | 2026-09-29 | 修掉剪藏桥「同一秒内连剪两次会撞名」 |
|
|
94
|
+
| v0.30.20 | 2026-09-29 | admin-routes.ts 再拆出「一次性预置(seed)」那一组 —— 为什么这一组值得单独出去 |
|
|
95
|
+
| v0.30.19 | 2026-09-29 | admin-routes.ts(746 行)拆出「知识库列表 / 位置」那一组 —— 「什么都没丢」的证据 |
|
|
96
|
+
| v0.30.18 | 2026-09-29 | src/host/session-summary.ts(633 行)拆出扫描层 —— 顺手纠正速查表里的一个错;搬迁纪律;"什么都没丢"的证据 |
|
|
97
|
+
| v0.30.17 | 2026-09-29 | 公众号三条路由补上「已登记但没在跑 → 503 点名」+ 铁律「行尾必须 LF」终于有了执行者 —— 过程中揪出一处静默的仓库不一致 |
|
|
98
|
+
| v0.30.16 | 2026-09-29 | 数值配置项的「同一组数字」收成一条通用守门 |
|
|
99
|
+
| v0.30.15 | 2026-09-29 | 就绪窗口的上下界收成「唯一来源」+ 一条反向验证过的守门 |
|
|
100
|
+
| v0.30.14 | 2026-09-29 | 客户端 i18n 全量落地 + `tool-views.ts` 拆成 7 个模块 + 设置页重启后顺手重载面板 |
|
|
101
|
+
| v0.30.13 | 2026-09-29 | 客户端 i18n 收尾(第 3 条裁定,283 键 / 20 个文件已转,只剩 3 个) —— 顺手修掉三处语言相关的真缺陷;守门同步 |
|
|
102
|
+
| v0.30.12 | 2026-09-29 | 两个真缺陷 + 三条新守门 —— tiddlywiki_append 的 heading 会把 Markdown 图片当成标题;变更锁只覆盖了一半 |
|
|
103
|
+
| v0.30.11 | 2026-09-29 | 客户端 i18n 继续推进(283 键 / 20 个文件已转)+ 守门解析器加固 |
|
|
104
|
+
| v0.30.10 | 2026-09-29 | routes.ts 拆分第 3 步(笔记组)+ 修掉 /upload 的多库 URL 缺陷 —— 顺手修的真缺陷 |
|
|
105
|
+
| v0.30.9 | 2026-09-29 | routes.ts 拆分(第 5 条裁定)—— 两步纯搬迁/工厂抽取,行为零变化 —— 守门铺路的第三处 |
|
|
106
|
+
| v0.30.8 | 2026-09-29 | 给 routes.ts 的拆分铺路:两处守门改成按模块族读 |
|
|
107
|
+
| v0.30.7 | 2026-09-29 | `tools-notes.ts` 683 → 14 行 barrel:七个工具工厂按职责搬进 `tools-notes-{write,structure,lifecycle}.ts` |
|
|
108
|
+
| v0.30.6 | 2026-09-29 | 客户端 i18n 层落地 —— 唯一入口;目录按区域分片;语言来源是同一个 uiLanguage |
|
|
109
|
+
| v0.30.5 | 2026-09-29 | `git_sync` 改为同步「所有可同步的库」:按仓库去重、逐仓库独立成败、回执逐仓库标注 |
|
|
110
|
+
| v0.30.4 | 2026-09-29 | 剪藏桥「不做认证、对任意页面开放」的姿态落地(只拒 `$:/` 系统命名空间)+ 守门加固 |
|
|
111
|
+
| v0.30.3 | 2026-09-29 | `clip-bridge.ts` 574 → 414:payload 层独立成 `clip-bridge-payload.ts` |
|
|
112
|
+
| v0.30.2 | 2026-09-29 | `routes-session.ts` 632 → 274:432 行的 `createSessionRoutes()` 按族拆成两条工厂 |
|
|
113
|
+
| v0.30.1 | 2026-09-29 | 纯搬迁:`styles.ts` 945 → 47 行(CSS 进 `styles-css.ts`)、剪藏桥出 SSRF 层、seeds 出排干层 |
|
|
114
|
+
| v0.30.0 | 2026-09-29 | 第二轮全面审计修复(`get` 漏密钥命名空间守卫 / 剪藏桥可写系统命名空间 / `bridge.wiki` 运行期恒被丢掉 / `git_sync` 排干是空守护) |
|
|
115
|
+
| v0.29.0 | 2026-09-29 | 只读审计修复(快速笔记 native 路径不解析目标库 / 已登记未运行的库静默回落默认库 / 两处裸 `server.restart()` / 提示词 −47%) |
|
|
116
|
+
| v0.28.14 | 2026-09-29 | 修「点击切换知识库感觉会卡那么一下」 —— 先量再改;两段来源分开处理;侧边栏不再重复打 /status |
|
|
117
|
+
| v0.28.13 | 2026-09-29 | 修「左侧多 wiki 入口有时候要点两次才能切到对应的库」 —— 顺带修好一个长期红着没人看的守门 |
|
|
118
|
+
| v0.28.12 | 2026-09-29 | 「默认库」不再是一个名字:哪里都直接用被设为默认的那个库的真实显示名 —— 会话选择器;快速笔记「写入」目标;设置页剪藏目标 |
|
|
119
|
+
| v0.28.11 | 2026-09-29 | 卡片「在 TW 打开」跳错库 + 新会话出现两个选择器;README 不再放版本记录 |
|
|
120
|
+
| v0.28.10 | 2026-09-28 | 修「图标弹层出现在设置弹窗后面、被遮挡」 —— 根因是层级用错了数;顺带补 styles.ts 模板串守门;这条守门的第一版是错的,值得单独记 |
|
|
121
|
+
| v0.28.9 | 2026-09-28 | 修「点击图标没有正常弹出」 —— 守门为什么没拦住(本版最重要的部分);新守门(双向);新守门当场抓出第二处漏写 |
|
|
122
|
+
| v0.28.8 | 2026-09-28 | 多库第五批反馈(12 条一次做完)+ 拆大文件 —— 图标选择器接上 DSH 官方图标系统;修「选了图标立刻变回默认」;多库下设置页按作用域分页 |
|
|
123
|
+
| v0.28.7 | 2026-09-28 | 会话知识库选择器改成与「快速笔记」同一行 + 单库专属配置项不再在多库下出现 —— 选择器"没和对话框对齐";「侧边栏 TW 入口显示名称」在多库下不该出现 |
|
|
124
|
+
| v0.28.6 | 2026-09-28 | 修两个多库界面的真机报障 —— 「插件管理 / 主题管理 / 语言包 / 初始化」看起来是混在一起管理的;左侧入口点一个却三个都高亮 |
|
|
125
|
+
| v0.28.5 | 2026-09-28 | 修「多库模式下侧边栏多出一个没有库名的 TiddlyWiki 入口」 |
|
|
126
|
+
| v0.28.4 | 2026-09-28 | 每库自定义图标 + 修「右键菜单切库后选中项不更新」 —— 右侧「知识库」菜单的选中项停在旧值;每个库的入口图标可自定义 |
|
|
127
|
+
| v0.28.3 | 2026-09-28 | 三个真机报障 + 插件说明补「多库 / 升级」两节 |
|
|
128
|
+
| v0.28.2 | 2026-09-28 | 设置页去掉重复的「知识库位置」 |
|
|
129
|
+
| v0.28.1 | 2026-09-28 | 修「拆库 skill 在 wiki 启动慢 / 起不来时永远装不上」+ 快速笔记的目标库可切换(R7) —— 真实事故;R7 快速笔记目标 |
|
|
130
|
+
| v0.28.0 | 2026-09-28 | 多知识库 |
|
|
131
|
+
| v0.27.0 | 2026-09-28 | 重新定位:从「AI 记忆工具」改写成「你和 Agent 共用的本地文档中心」——纯元数据 + 文档发布,运行时行为零变化 |
|
|
132
|
+
| v0.26.7 | 2026-09-26 | 修「DSH 桌面版里 TW 面板没有『添加条目』按钮」——嵌入式 TW 被渲染在自定义 scheme 上,于是它根本没有同步器 |
|
|
133
|
+
| v0.26.6 | 2026-09-26 | 修 tiddlywiki_append 的 heading 在 CRLF 笔记上完全失效、且静默回退到文末 |
|
|
134
|
+
| v0.26.5 | 2026-09-23 | 修 P0:fields 参数被按字符拆成几十个垃圾字段 |
|
|
135
|
+
| v0.26.4 | 2026-09-23 | 修「发送给 Agent」因配置被浏览器自动填充而 404 + 设置页文本框防自动填充 |
|
|
136
|
+
| v0.26.3 | 2026-09-23 | 修正 starter-docs 教程里一处技术性错误说法 |
|
|
137
|
+
| v0.26.2 | 2026-09-22 | catalog 标题不再用目录名推导(`codemirror-fullscreen-editing` 的真名不同) |
|
|
138
|
+
| v0.26.1 | 2026-09-22 | 修 P0:设置页插件/主题/语言的勾选框全部显示为「未勾选」,且误点「应用」会把启动清单整个清空 |
|
|
139
|
+
| v0.26.0 | 2026-09-22 | 修「设置页插件管理与 TW 实际状态对不上」——两套真相源不再互相说谎 |
|
|
140
|
+
| v0.25.1 | 2026-09-21 | 修「回复流里的 wiki 链接点了打不开」 |
|
|
141
|
+
| v0.25.0 | 2026-09-20 | 第五轮代码审计修复(只留 `/render` title 分支 `{{…}}` 转写一条,判定单机可接受) |
|
|
142
|
+
| v0.24.2 | 2026-09-20 | 修「$:/language 每次 dsh web 启动都被无条件重写 → 两台机器每次 pull 都冲突」 |
|
|
143
|
+
| v0.24.1 | 2026-09-20 | 修「重启/停止 TW 的四个路径里有三个没排干 syncer 队列」——一类静默丢写入的缺陷 |
|
|
144
|
+
| v0.24.0 | 2026-09-20 | 三处「Agent 读写 wiki 的规矩」按用户要求改进 + 一个 lint 误报修复 |
|
|
145
|
+
| v0.23.5 | 2026-09-18 | 修 15 处审计缺陷(最重:`delete` 先删条目后读回收站索引 ⇒ 未进索引的永久孤儿) |
|
|
146
|
+
| v0.23.4 | 2026-09-18 | 三处硬化,全部来自 v0.23.3 上线当天暴露的真实事故 —— 提交前拦冲突(数据安全);配置解析失败拒绝写(数据丢失);adapter 版本校验 |
|
|
147
|
+
| v0.23.3 | 2026-09-18 | TW 笔记工具栏新增「发布到公众号」按钮 —— 标题不进 argv;任务化而非同步请求;可选功能默认不打扰 |
|
|
148
|
+
| v0.23.2 | 2026-09-17 | 多图发布命令 publish-note-imgs + 发布策略定为「到草稿箱为止」 |
|
|
149
|
+
| v0.23.1 | 2026-09-17 | 新增 wechat-setup seed——「微信公众号发布指南」也随插件分发 |
|
|
150
|
+
| v0.23.0 | 2026-09-17 | 新增「发布到微信公众号」+ 发布元数据 |
|
|
151
|
+
| v0.22.10 | 2026-09-17 | 修「写入丢失 created/modified,新笔记在按修改时间排序的页面上沉到最后一名」 |
|
|
152
|
+
| v0.22.9 | 2026-09-16 | 修「TiddlyWiki 编辑者署名被静默清空」 |
|
|
153
|
+
| v0.22.8 | 2026-09-15 | 第六轮代码审计修复 —— 数据丢失(P0);设置页超时(P0);净化器崩服务 |
|
|
154
|
+
| v0.22.7 | 2026-09-14 | 设置页「预览注入文本」改为按表单当前值渲染 |
|
|
155
|
+
| v0.22.6 | 2026-09-14 | 修「快速笔记」两处用户实测报障 —— 原生编辑弹窗里删掉笔记后,弹窗变永久白板、再点「快速笔记」也回不来;card 模式底部操作条把按钮文字压成两行 |
|
|
156
|
+
| v0.22.5 | 2026-09-14 | 修「大知识库冷启动被误判为启动失败」 |
|
|
157
|
+
| v0.22.4 | 2026-09-13 | 重构:把 TW frame 的生命周期收敛成唯一一份实现 |
|
|
158
|
+
| v0.22.3 | 2026-09-12 | 客户端审计修复(`iframe` 空 `src` 被当成合法地址使用 / 主题「活动」单选等) |
|
|
159
|
+
| v0.22.2 | 2026-09-12 | 修「主题汇总页·模板」seed 内容被渲染成几千遍重复 |
|
|
160
|
+
| v0.22.1 | 2026-09-11 | 修首页「快速记笔记」创建出的条目日期时间错误 |
|
|
161
|
+
| v0.22.0 | 2026-09-11 | 设置页运行期切换知识库位置 + seed 文档「过期」检测 + 一个隐藏很久的数据丢失竞态 |
|
|
162
|
+
| v0.21.0 | 2026-09-11 | 注入提示词改为「默认精简 + 可配置 + 不再过期」 —— 保存后无需重启 dsh web |
|
|
163
|
+
| v0.20.1 | 2026-09-11 | 修复「覆盖已有条目时内容类型被静默重置」 |
|
|
164
|
+
| v0.20.0 | 2026-09-11 | 第四轮代码审计修复(`/render` 未拦插件命名空间 ⇒ 可读出配置与 token 等) |
|
|
165
|
+
| v0.19.5 | 2026-09-11 | 第三轮代码审计修复(`attach` 不读旧条目就 PUT ⇒ 撞名静默覆盖既有笔记 等) |
|
|
166
|
+
| v0.19.4 | 2026-09-11 | 标签列表全面「有界」——最后一个没有上限的列表接口 |
|
|
167
|
+
| v0.19.3 | 2026-09-11 | 清掉审计剩余中优先级项(`/admin/state` 与 `/admin/config` 回包把 token / PAT 打码 等) |
|
|
168
|
+
| v0.19.2 | 2026-09-11 | v0.19.1 的内容 + 让 flush 哨兵真正生效 |
|
|
169
|
+
| v0.19.1 | 2026-09-11 | 第二轮代码审计的修复版 + 让 CI 重新变绿 |
|
|
170
|
+
| v0.19.0 | 2026-09-11 | 按审计结论全量修复 + 新能力(每个路由校验 HTTP 方法:`GET /sync` 曾能 push 等) |
|
|
171
|
+
| v0.18.0 | 2026-09-10 | 全量审计后 40+ 项修复(重头:seed 只在真 404 时才认为条目缺失,瞬时故障曾会覆盖笔记) |
|
|
172
|
+
| v0.17.0 | 2026-09-10 | 移除:DSH Better Sidebar(dsh-better-sidebar)Tab 集成 |
|
|
173
|
+
| v0.16.28 | 2026-09-10 | 修:拖拽版剪藏书签在知乎等真实页面报 SyntaxError: Unexpected token ';' |
|
|
174
|
+
| v0.16.27 | 2026-09-10 | 剪藏书签支持「拖拽安装」 |
|
|
175
|
+
| v0.16.26 | 2026-09-10 | 修:剪藏书签在真实页面上崩溃 |
|
|
176
|
+
| v0.16.25 | 2026-09-10 | 剪藏桥支持图片剪藏(浮层选图) |
|
|
177
|
+
| v0.16.24 | 2026-09-09 | 新:本地剪藏桥 + 书签小工具 |
|
|
178
|
+
| v0.16.23 | 2026-09-09 | 新:DSH Better Sidebar(dsh-better-sidebar)集成 |
|
|
179
|
+
| v0.16.22 | 2026-09-09 | seed 体系三层化 + 文档中心起步包 |
|
|
180
|
+
| v0.16.21 | 2026-09-09 | 新:DSH 右侧边栏(rightbar)集成 |
|
|
181
|
+
| v0.16.20 | 2026-09-09 | 修 search / recent 在大 wiki 上超时 |
|
|
182
|
+
| v0.16.19 | 2026-09-07 | 修:会话「知识库」Tab 把汇总当源码显示 |
|
|
183
|
+
| v0.16.18 | 2026-09-07 | README 全面精简重写;seed 优化——首页 seed 跟随 wiki 现状、修「所有文章」回主页死链、doc-note 文案修正 |
|
|
184
|
+
| v0.16.17 | 2026-09-07 | 点击快速笔记可选直达 TW 原生编辑页(ui.quickNoteMode)+ 修「在 TW 中编辑」弹窗 ✕ 关不掉 |
|
|
185
|
+
| v0.16.16 | 2026-09-07 | 修会话知识库 Tab 误入编辑草稿显示源码——三层防误编辑(清残留草稿/吞草稿创建/禁 ✏️) |
|
|
186
|
+
| v0.16.15 | 2026-09-07 | put/batch_put 未指定类型自动默认 text/markdown;警告 fields.type 是内容类型保留字段 |
|
|
187
|
+
| v0.16.14 | 2026-09-07 | 修知识库 Tab「佚失条目」根因——浏览器端 TW 天生排除 $:/temp,改为 iframe 就绪后注入 + 原生 hash 导航 |
|
|
188
|
+
| v0.16.11 | 2026-09-06 | 新增会话顶部「知识库」Tab(本会话相关 wiki 汇总,volatile $:/temp) |
|
|
189
|
+
| v0.16.0 | 2026-09-05 | 回复流原生 TW 工具卡片 + 可点击 wiki 链接(新增 render-route seed) |
|
|
190
|
+
| v0.15.0 | 2026-09-05 | 初始化不强制 + 可反初始化(seed 拆核心/可选两层) |
|
|
191
|
+
| v0.14.0 | 2026-09-05 | 「发送给 Agent」按钮独立图标 + 附加说明 + 权限选择 |
|
|
192
|
+
| v0.13.0 | 2026-09-04 | menubar 顶栏主题自适应 |
|
|
193
|
+
| v0.12.0 | 2026-09-04 | 新默认主页 + 「所有文章」两列分页页 |
|
|
194
|
+
| v0.11.0 | 2026-09-04 | 「发送给 Agent」支持选择工作模式(Agent 预设) |
|
|
195
|
+
| v0.10.0 | 2026-09-04 | 统一 seed 注册表 + 设置页「初始化」 |
|
|
196
|
+
| v0.8.0 | 2026-09-04 | 嵌入式 TW 跟随 DSH 深浅主题 |
|
|
197
|
+
| v0.7.0 | 2026-09-03 | TW 一键把当前笔记发送到 dsh 会话 |
|
|
198
|
+
| v0.6.0 | 2026-09-03 | 同源代理(远程访问核心) |
|
|
199
|
+
| v0.5.0 | 2026-09-03 | Agent 工具扩充 + 快速笔记草稿自动保存 + 统一知识库 FAB |
|