dsh-tiddlywiki 0.20.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,8 +23,11 @@
23
23
 
24
24
  | 能力 | 说明 |
25
25
  |---|---|
26
- | 🏠 **文档中心起步包** | 首次安装自动 seed:插件说明 + 「示例与文档」(主题汇总模板 / 教程 / 三个示例主题页),首页「📚 插件文档」栏一键查阅;**同名 tiddler 已存在一律安全跳过,绝不覆盖你的数据**(v0.16.22) |
26
+ | 🏠 **文档中心起步包** | 首次安装自动 seed:插件说明 + 「示例与文档」(主题汇总模板 / 教程 / 三个示例主题页),首页「📚 插件文档」栏一键查阅;**同名 tiddler 已存在一律安全跳过,绝不覆盖你的数据**(v0.16.22);插件说明里的**工具清单由工具注册表实时生成**,不会再写着「10 个工具」却已经 15 个(v0.22.0) |
27
+ | 📍 **知识库位置可切换** | 设置页「知识库位置」可把插件切换到**任意本地文件夹**(不用改 cordis 配置、不用重装):就地停/起 TW 子进程,目标目录没有 `tiddlywiki.info` 时自动 `--init server` 建一个全新知识库;选择记在 `$DSH_HOME/dsh-tiddlywiki/location.json`——一个**在 wiki 之外**的指针文件(所以切到新 wiki 后仍记得「我用的是哪个」),「恢复为配置默认」一键清除;**切换失败自动回滚**到原知识库并如实告知(v0.22.0) |
28
+ | 🔄 **seed 更新检测** | seed 标记记录**内容哈希**:内置内容在本 wiki 预置之后更新过 → 设置页显示「⬆ 有更新」;被你改过 → 显示「✏️ 本地已修改」,且「重新初始化」前**二次确认**;两者都基于哈希判定,绝不猜(旧格式标记会明确显示「尚未启用更新检测」,不会误报为「你改过」,v0.22.0) |
27
29
  | 🎨 **自定义样式** | 「自定义样式」seed:编辑器美化 / 窄屏侧栏隐藏 / menubar 加高 / 批注弹窗等 5 张通用样式表,新 wiki 也能一键初始化(可选,v0.16.22) |
30
+ | 📝 **可配置的注入提示词** | 插件注入每个会话的「TiddlyWiki 持久知识库」提示词可在设置页配置:**默认精简版**(~1.7KB,只保留工具 schema 表达不了的约定——同步纪律 / 标签约定 / 链接格式),可选**完整版**(额外附一份**由工具注册表实时生成**的参数索引,不会再过期);`extra` 追加自定义规范、`override` 整段接管、可整体停用;**保存后无需重启 dsh web**(section 即时重注册,当前会话下一步即生效),设置页可**预览**即将注入的全文(v0.21.0) |
28
31
  | 🤖 **Agent 工具** | 15 个 `tiddlywiki_*` 工具:检索(**相关度排序 + 命中处片段 + 字段过滤**)、读写、**增量追加**、批量、重命名、**软删除/回收站**、**反向链接**、**附件入库**、**知识库体检**、git 同步与冲突解决(v0.19.0;检索/最近仍在**服务端**排除二进制附件,大 wiki 上从 515MB/17s 降到 ~0.4s) |
29
32
  | 🛡️ **不会被覆盖的写入** | 所有写入路径(agent 工具 **与** 快速笔记/编辑器路由)都**先读后写**:不传 tags 就保留原有标签、自定义字段与**内容类型**(`text/css`/wikitext 等不会被重置成 Markdown,v0.20.1);`tiddlywiki_put(..., expectedModified/expectedRevision)` 与 `tiddlywiki_delete(..., expectedModified/expectedRevision)` 乐观并发——读取后若有人(在 TW 编辑器里)改过,写入/删除被拒绝(HTTP 409)而不是静默覆盖或丢进回收站;`tiddlywiki_attach` 的同名标题**默认拒绝**(要覆盖必须 `force: true`);`tiddlywiki_delete` 默认**软删除进回收站**,`tiddlywiki_trash` 可恢复(v0.19.0 / v0.19.1 / v0.19.5) |
30
33
  | 🧼 **渲染片段净化** | 回复流卡片与会话汇总注入的 TW 片段先经 **host 白名单净化**(丢 `iframe`/`script`/`svg`/`on*`/`javascript:`/`data:text/html` 等)——TW 自己的解析器只剥 `on*`,`<iframe src="javascript:…">` 会原样通过并在 DSH 页面里执行(v0.19.1 修复的存储型 XSS) |
@@ -108,6 +111,21 @@ dsh plugin --profile web add link:/path/to/dsh-tiddlywiki
108
111
 
109
112
  > ⚠️ `fields.type` 是 TW 的**内容类型保留字段**(`text/markdown` 等),业务分类请放 `tags`,别写进 `fields.type`。
110
113
 
114
+ ### 🧠 注入的系统提示词(可配置)
115
+
116
+ 插件会往每个会话的系统提示词里注入一段「TiddlyWiki 持久知识库」约定(**设置页 → 系统提示词**):
117
+
118
+ | 配置 | 作用 |
119
+ |---|---|
120
+ | `prompt.enabled` | 关掉后本插件不注入任何文本 |
121
+ | `prompt.mode` | `slim`(**默认**):只保留工具 schema 表达不了的约定(写入/并发纪律、同步纪律、标签约定、可点击链接格式);`full`:额外附一份**参数索引**,由工具注册表在运行时生成,因此永远不会与真实工具脱节 |
122
+ | `prompt.extra` | 追加在末尾的自定义规范(团队 / 个人偏好),始终生效 |
123
+ | `prompt.override` | 非空时整段取代内置文本(`extra` 仍会追加)——想完全自写提示词时用 |
124
+
125
+ 保存后**不用重启 dsh web**:host 会即时重新注册 prompt section,当前会话从**下一步**起就使用新文本(DSH 的 `system-prompt/change` 会更新历史里的系统消息)。设置页的「查看当前注入文本」按钮会读取 host 实时拼好的全文,所见即下一步实际注入的内容。
126
+
127
+ > 为什么默认精简:v0.20.1 之前这段提示词里手抄了一份**工具参数清单**,最后一次同步停在 v0.19.0,而 v0.19.4 / v0.19.5 / v0.20.1 都改过工具参数——模型因此看到 6 处过期签名(`delete` 缺并发令牌、`append` 缺 `fields`、`attach` 缺覆盖保护、`batch_put` 缺 `overwrite`、`trash` 缺 `title`/`limit`、`list_tags` 缺 `limit`)。v0.21.0 起:默认形态不再复述参数(工具 description 才是唯一事实),`full` 形态的目录改为**运行时生成**,并新增 `scripts/verify-prompt.mjs` 守门(slim 不得出现参数清单 / full 的每个工具与每个参数都必须在场 / 两种形态都必须保留治理约定块)。
128
+
111
129
  ### 🧑‍💻 界面操作
112
130
 
113
131
  - **📤 发送给 Agent**:TW 工具栏按钮(首次启动自动写入 wiki,ONE-SHOT)。弹层可选**附加说明**(位于消息末尾)、**工作模式**(Agent 预设)、**权限**(权限预设),按工作区分组选会话或新建。消息自带待办说明。
@@ -120,7 +138,19 @@ dsh plugin --profile web add link:/path/to/dsh-tiddlywiki
120
138
  - **📚 会话知识库 Tab**:会话顶部 Tab(`ui.tabLabel` 改名、`ui.showSessionTab` 关闭),自动汇总本会话读写过的 wiki 笔记(写入 volatile `$:/temp`,不落盘不进 git),**TW 原生渲染**(v0.16.19 起 `/tw/render` 片段管线,与回复流工具卡同链路),链接点击直达中央 TW 面板,不可编辑。
121
139
  - **🌗 跟随 DSH 主题**:内嵌 TW 随 DSH 深浅切换 palette,纯内存不写回 wiki(`ui.followDshTheme`/`ui.darkPalette`)。
122
140
  - **🔧 知识库 FAB**:统一入口(TW 面板开关/重载、快速笔记、同步、TW 服务状态悬停 tip)。同步拉取到新内容会自动重启 TW(同端口)。
123
- - **⚙️ 设置页**:DSH 设置 →「TiddlyWiki 知识库」:状态/重启、常规配置、插件/主题/语言管理、**初始化**(seed 状态与重新初始化)。配置写入 `$:/plugins/dsh-tiddlywiki/config` tiddler,覆盖 cordis `config:` 块(tiddler 优先)。
141
+ - **⚙️ 设置页**:DSH 设置 →「TiddlyWiki 知识库」:状态/重启、**知识库位置(可切换)**、常规配置、插件/主题/语言管理、**初始化**(seed 状态与重新初始化)。配置写入 `$:/plugins/dsh-tiddlywiki/config` tiddler,覆盖 cordis `config:` 块(tiddler 优先)。
142
+
143
+ ### 📍 知识库位置(可切换,v0.22.0)
144
+
145
+ 设置页顶部「**知识库位置(可切换)**」区块显示当前实际服务的文件夹、这个位置**是怎么决定的**(指针文件 / cordis 配置 / 默认值)、指针文件路径,以及**同目录下其它看起来像 wiki 的文件夹**(含 `tiddlywiki.info`)的快捷填入按钮。
146
+
147
+ - **换一个位置**:填根目录(绝对路径,支持 `$DSH_HOME` / `${VAR}` / `%VAR%`)+ 文件夹名 → 「切换到这个位置」。host 会停掉 TW → 释放自动提交与监听 → 指向新目录 → 起 TW → 重载新 wiki 的配置 tiddler → 跑核心 seed(markdown 插件 / 发送给 Agent / 渲染路由 / 同源代理 / 语言)→ 重新武装自动提交;**选中即被记住**(写进 `$DSH_HOME/dsh-tiddlywiki/location.json`),重启 dsh web 后仍在新知识库。
148
+ - **新建一个知识库**:目标文件夹若还没有 `tiddlywiki.info`,插件会照常自动 `--init server` 建一个全新的(**别填已有的普通笔记目录**——它会被初始化)。
149
+ - **恢复为配置默认**:删除指针文件并(必要时)切回 `config.wikiRoot` / `config.wiki`。
150
+ - **安全语义**:指针文件在任何 wiki **之外**(若存在 wiki 自己的配置 tiddler 里,切到新 wiki 就会把这个选择一起丢掉——鸡生蛋);指针文件损坏/非法时**明确报告并回退到配置默认**,不会静默乱跑;切换**失败会回滚**到原来的知识库,并告诉你是「已回滚」还是「回滚也失败了」;同刻只允许一个切换(并发请求直接拒绝)。
151
+ - **注意**:切换期间正在跑的 Agent 工具调用会失败(TW 在重启),界面上按钮会禁用并显示「切换中…(重启 TW)」。
152
+
153
+ `wikiRoot` / `wiki` 现在只是**默认值**——运行中的实际位置以指针文件优先。这也是为什么它能不改 cordis、不重装就切。
124
154
 
125
155
  ### 🧩 初始化(一次性预置 seed):哪些「必备」,哪些「可有可无」
126
156
 
@@ -142,6 +172,7 @@ seed 是把「wiki 里预置内容」随插件分发的机制:**ONE-SHOT(只
142
172
  - **想获得完整插件体验**:核心 3 项首次安装就有;再补 `home-index`(首页)+ `starter-docs`(示例文档)即是一个开箱即用的文档中心。
143
173
  - **一个可选项都不想要**:完全不影响功能——设置页「反初始化」即可,核心项受保护不可移除。
144
174
  - **文档怎么扩散到更多**:以后插件新增的任何说明 / 教程 / 模板类内容都走 seed 并带 **`dsh-docs`** 标签——首页「📚 插件文档」栏自动收录,你无需任何配置。
175
+ - **内置内容更新了怎么办**(v0.22.0):seed 标记里记着**内容哈希**,设置页据此显示两个提示 chip——「**⬆ 有更新**」(内置内容比你 wiki 里预置的更新,可点「更新到内置版本」取用)与「**✏️ 本地已修改**」(这篇是你的内容,重新初始化会覆盖它,会先二次确认)。升级插件后旧 wiki 的标记没有哈希,会显示「更新检测尚未启用(重新初始化一次即可)」,**不会**把老标记误判成「你改过」;重新初始化一次即升级标记。**绝不自动改写你的 wiki**——更新只在你点的时候发生。
145
176
 
146
177
  ---
147
178
 
@@ -152,8 +183,8 @@ seed 是把「wiki 里预置内容」随插件分发的机制:**ONE-SHOT(只
152
183
  ```yaml
153
184
  - id: dsh-tiddlywiki
154
185
  config:
155
- wikiRoot: "$DSH_HOME/tiddlywiki" # 缺省自动展开
156
- wiki: "main"
186
+ wikiRoot: "$DSH_HOME/tiddlywiki" # 默认位置;运行中可被设置页「知识库位置」覆盖(指针文件优先,v0.22.0)
187
+ wiki: "main" # 文件夹名("." = 直接用 wikiRoot 这个目录)
157
188
  port: 0 # 0 = 自动探测空闲端口
158
189
  git:
159
190
  autoCommit: true
@@ -162,6 +193,11 @@ seed 是把「wiki 里预置内容」随插件分发的机制:**ONE-SHOT(只
162
193
  branch: "main"
163
194
  note:
164
195
  tag: "inbox" # 快速笔记默认 tag
196
+ prompt:
197
+ enabled: true # false = 本插件不注入任何提示词
198
+ mode: "slim" # slim(默认:只留约定)/ full(+ 由工具注册表实时生成的参数索引)
199
+ extra: "" # 追加在提示词末尾的自定义规范(团队/个人偏好)
200
+ override: "" # 非空时整段取代内置文本(extra 仍会追加)
165
201
  bridge:
166
202
  enabled: false # 本地剪藏桥(书签小工具);保存后立即生效
167
203
  port: 8618 # 监听端口(127.0.0.1;改后需重启 dsh web 才绑定新端口)
@@ -186,7 +222,9 @@ seed 是把「wiki 里预置内容」随插件分发的机制:**ONE-SHOT(只
186
222
  password: "" # 非空时插件内置客户端/就绪探测/浏览器代理都带 Basic 认证(v0.18.0 起真正可用)
187
223
  ```
188
224
 
189
- > **运行时配置**:设置页写入的 `$:/plugins/dsh-tiddlywiki/config` tiddler 是 `config:` 块之上的覆盖层(tiddler 优先、随 wiki git 同步),改 note tag / git / ui 开关**以及剪藏桥端口**都无需动 cordis(端口改动仍需重启 dsh web 重新绑定监听)。
225
+ > **运行时配置**:设置页写入的 `$:/plugins/dsh-tiddlywiki/config` tiddler 是 `config:` 块之上的覆盖层(tiddler 优先、随 wiki git 同步),改 note tag / git / ui 开关 / **注入提示词(`prompt.*`)** 都无需动 cordis;**提示词改动保存后立即生效**(section 即时重注册,当前会话下一步生效),剪藏桥端口改动仍需重启 dsh web 重新绑定监听。
226
+ >
227
+ > **知识库位置是三层**(v0.22.0):指针文件 `$DSH_HOME/dsh-tiddlywiki/location.json` > `config:` 块的 `wikiRoot`/`wiki` > 内置默认(`$DSH_HOME/tiddlywiki` + `main`)。`wikiRoot`/`wiki` 因此是「默认位置」而不是「唯一位置」——设置页切过的位置存在指针文件里(一个在 wiki 之外的文件),所以要换回配置值就点「恢复为配置默认」。
190
228
 
191
229
  ---
192
230
 
@@ -212,6 +250,8 @@ node scripts/verify-send-to-agent-bundle.mjs # bundle 字段 + 源件逐字一
212
250
  node scripts/verify-seed-send-to-agent.mjs # 全新 wiki 上的 seed E2E
213
251
  node scripts/verify-clip-bridge.mjs # 剪藏桥 headless 验收(含 SSRF 守卫)
214
252
  node scripts/verify-seeds-admin.mjs # /admin/seeds 状态与 run 的 E2E
253
+ node scripts/verify-prompt.mjs # 注入提示词守门(slim 无参数清单 / full 与工具注册表逐项一致 / 治理约定不丢)
254
+ node scripts/verify-wiki-switch.mjs # 运行时切换知识库 E2E(真起 TW:切换 / 回滚 / 指针文件 / 非法输入,v0.22.0)
215
255
  ```
216
256
 
217
257
  > 📦 从 **npm 包**安装的用户只有 `lib/` + `src/` + `docs/`(`scripts/` 不在发布包里,避免把构建脚本塞进依赖树)——想跑上面的验收脚本请用 git 仓库:`git clone https://github.com/bbqisbbq/dsh-tiddlywiki && npm install`。
@@ -254,7 +294,10 @@ node scripts/gen-seed-ui-styles.mjs '<wiki>/tiddlers/<样式.css>' … src/host/
254
294
  | `/dsh-tiddlywiki/agent/sessions` `/modes` `/send` `/create` | GET/POST | TW「发送给 Agent」:会话/模式/发送/新建 |
255
295
  | `/dsh-tiddlywiki/api/*` | any | 透传 TW 服务(JSON) |
256
296
  | `/dsh-tiddlywiki/tw/*` | any | 同源 TW 代理(远程访问核心) |
257
- | `/dsh-tiddlywiki/admin/seeds` `/run` `/remove` | GET/POST | seed 状态 / 运行 / 反初始化 |
297
+ | `/dsh-tiddlywiki/admin/seeds` `/run` `/remove` | GET/POST | seed 状态(v0.22.0 起含内容哈希的「有更新 / 本地已修改」)/ 运行 / 反初始化 |
298
+ | `/dsh-tiddlywiki/admin/prompt` | GET | 当前注入提示词全文(设置页预览用,v0.21.0) |
299
+ | `/dsh-tiddlywiki/admin/wiki/location` | GET | 当前知识库位置 + 来源(指针/配置/默认)+ 指针文件路径 + 同目录候选 wiki(v0.22.0) |
300
+ | `/dsh-tiddlywiki/admin/wiki/switch` `/reset` | POST | 运行时切换知识库 / 恢复为配置默认(失败自动回滚并报告,v0.22.0) |
258
301
 
259
302
  ### 项目结构
260
303
 
@@ -273,6 +316,7 @@ src/
273
316
  │ ├── admin.ts # 设置页后台:tiddlywiki.info 读写 + /admin/*
274
317
  │ ├── config.ts # ConfigStore:cordis config 基底 + 配置 tiddler 覆盖层
275
318
  │ ├── seeds.ts # 统一 seed 注册表(10 项,三层:核心/起步/可选)
319
+ │ ├── prompt.ts # 系统提示词(v0.21.0):slim/full 两种形态 + extra/override,full 的目录由工具注册表实时生成
276
320
  │ ├── seed-*.ts # 各 seed 实现(bundle/首页/ui-styles 常量脚本生成;starter-docs/menubar/clip-bridge 手工维护)
277
321
  │ └── tools.ts # 15 个 tiddlywiki_* 工具(列表式注册)
278
322
  └── client/ # 浏览器半部
@@ -298,6 +342,10 @@ lib/ # 预构建产物(发布含 lib/**,提交入库;
298
342
 
299
343
  > 最近几个主要版本的一句话记录(完整变更见 [Releases](https://github.com/bbqisbbq/dsh-tiddlywiki/releases) / git log)。
300
344
 
345
+ - **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 组内容哈希断言(新鲜 / 用户改过 / 内置更新 / 旧标记两种 / 重新初始化后刷新)。
346
+
347
+ - **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`。
348
+
301
349
  - **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+回收站恢复保类型)。
302
350
 
303
351
  - **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。
@@ -61,6 +61,10 @@ interface SeedDef {
61
61
  | `tw-web-host` | `seeds.ts` 内联 | `$:/config/tiddlyweb/host` → `/dsh-tiddlywiki/tw/` | 无 marker(ensure 型,见 §4) | **核心** |
62
62
 
63
63
  > ℹ️ `home-index` 的 seed 版首页是**通用版**:生成脚本**默认**剥离作者 wiki 里的个人元素(主题页 tabs、书籍书架入口等,仅 `--keep-private` 才原样嵌入),并内置「📚 插件文档」tabs 栏(`[tag[dsh-docs]!is[system]]`,默认展开插件说明)。作者自己的 wiki 首页不受影响(seed 是 ONE-SHOT,不会覆盖)。
64
+ >
65
+ > ℹ️ **v0.22.0 起 marker 记内容哈希**:marker tiddler(`$:/plugins/dsh-tiddlywiki/seed-*`)的正文从一行 `seeded-once` 升级为 JSON `{ version, hashes: { <标题>: <sha256 前 16 位> }, at }`——哈希记录的是**我们写下的内置正文**,据此可区分「内置内容更新了」与「用户自己改过」(见 §3.1)。旧 marker 仍可读,按文本比对,并在下一次重新初始化时升级。
66
+ >
67
+ > ℹ️ **v0.22.0 起 `doc-note` 正文是生成的**:工具清单来自 `tiddlywikiToolSummary()`(`docNoteText(tools)`),不再手抄「N 个 agent 工具」。无注册表的 headless 调用会退化成一句指针,绝不写出过期数量。
64
68
 
65
69
  ### 统一入口(`src/index.ts` 导出)
66
70
 
@@ -84,6 +88,28 @@ interface SeedDef {
84
88
 
85
89
  ---
86
90
 
91
+ ## 3.1 内容哈希与「更新检测」(v0.22.0)
92
+
93
+ seed 是 ONE-SHOT,所以**插件升级后旧 wiki 的内容不会自动更新**——这正是「插件说明里写着 10 个工具、实际已有 15 个」这类问题的根源。v0.22.0 让检测变成可能,但**不改变 ONE-SHOT 的安全性**:
94
+
95
+ | 判定 | 条件 | 设置页表现 |
96
+ |---|---|---|
97
+ | 内置内容有更新 | marker 记录的哈希 ≠ 当前内置正文的哈希 | 「⬆ 有更新」chip,按钮变为「更新到内置版本」 |
98
+ | 用户改过 | 当前 tiddler 正文的哈希 ≠ marker 记录的哈希 | 「✏️ 本地已修改」chip;覆盖前**二次确认** |
99
+ | 两者都不是 | 哈希一致 | 无 chip |
100
+ | 旧 marker(无哈希) | marker 不是我们的 JSON | 「更新检测尚未启用(重新初始化一次即可)」 |
101
+ | 旧 marker + 文本已不同 | 无法判断是用户改的还是内置变了 | 报 `updateAvailable` 且 `userModified: undefined`(**无法确认**),UI 明说可能覆盖你的改动 |
102
+
103
+ 三条不容妥协的规则:
104
+
105
+ 1. **绝不猜**:没有哈希就不编造「用户改过」——否则升级后满屏误报;
106
+ 2. **绝不自动改写**:`updateAvailable` 只是提示,更新必须在设置页手动点(自动更新等于覆盖用户内容);
107
+ 3. **哈希只在「写入的确实是我们写的内容」时记录**:`refreshSeedMarker()` 只对「当前正文 === 内置正文」的 tiddler 记哈希,用户改过的副本不进账;已有哈希**合并而非清空**,一次跳过不会忘掉其余条目。
108
+
109
+ 实现:`src/host/seed-util.ts`(`hashText` / `parseSeedMarker` / `readSeedMarker` / `writeSeedMarker`)+ `src/host/seeds.ts` 的 `inspectSeedContent()` / `refreshSeedMarker()`(`SeedDef.markerTitle` + `SeedDef.content` 两个钩子启用检测)。回归在 `scripts/verify-seeds-admin.mjs` 第 9 段(5 组断言:新鲜 / 用户改过 / 内置更新 / 旧标记两种 / 重新初始化后刷新)。
110
+
111
+ ---
112
+
87
113
  ## 4. force 语义(手动「重新初始化」)
88
114
 
89
115
  设置页「初始化」区块的「重新初始化」按钮(以及后台 `POST /admin/seeds/run` 带 `force: true`)触发: