dsh-tiddlywiki 0.23.0 → 0.23.3

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
@@ -1,491 +1,508 @@
1
- # dsh-tiddlywiki
2
-
3
- > 把 **TiddlyWiki 5** 变成你和 Agent 之间的**文档中心**:一个嵌在 DSH 里、人手一个的私人知识库——Agent 用 `tiddlywiki_*` 工具读写,你在界面里用完整 TW 编辑器记录,一切通过 **git** 自动同步备份。
4
-
5
- [![npm](https://img.shields.io/npm/v/dsh-tiddlywiki)](https://www.npmjs.com/package/dsh-tiddlywiki)
6
- [![license](https://img.shields.io/npm/l/dsh-tiddlywiki)](https://github.com/bbqisbbq/dsh-tiddlywiki/blob/main/LICENSE)
7
- [![GitHub](https://img.shields.io/github/stars/bbqisbbq/dsh-tiddlywiki)](https://github.com/bbqisbbq/dsh-tiddlywiki)
8
-
9
- ---
10
-
11
- ## 🗂 它是什么:你的「文档 × Agent」一站式文档中心
12
-
13
- 这个插件**首先不是一个"AI 记忆工具"**(虽然它顺带就是)。它把一件事做顺了——**文档和人、文档和 Agent 之间的协作**:
14
-
15
- - **TiddlyWiki 天生适合当文档中心**:卡片化组织(标签 / 双向链接 / 一键成文 / 一个文件夹就是整个 wiki)、纯文本 git 仓库天然可迁移可备份。它能是你的**私人笔记本**、**GTD 工具**、**Agent 的记忆库**,甚至让 AI 帮你把电子书导入 TW 来阅读批注——**TW + DSH 能做到的唯一限制,只是人类的想象力**。
16
- - **人会参与**:完整 TW 5 编辑器内嵌在 DSH 里,随时翻、随时改、随时批注。文档不是 Agent 写给你的黑盒,而是**方便人类参与的文档中心**。
17
- - **Agent 也能进**:Agent 用 10 个 `tiddlywiki_*` 工具读写;TW 里「发送给 Agent」把笔记一键注入某个会话;每个会话顶部自动汇总它读写过的 wiki 笔记。**切换会话、方案终结、多轮合作,成果最终都回到文档里**——文档成为跨会话、跨 Agent 的公共底座。
18
- - **AI 填平 TiddlyWiki 的门槛**:TW 功能强大但上手有门槛,这正是 AI 擅长的——让 Agent 按你的习惯定制首页、标签体系、样式与工作流。曾经要折腾 Obsidian / Logseq 插件体系才能搭出来的个人知识库,现在**一句话就能初始化一整套开箱即用的文档中心**。
19
-
20
- > Obsidian、Logseq 也能做到其中一部分,但**从没有像这样一站式地顺畅**:编辑器、工具、会话、同步、主题全部在同一个界面里闭环。
21
-
22
- ## ✨ 特性一览
23
-
24
- | 能力 | 说明 |
25
- |---|---|
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) |
29
- | 🎨 **自定义样式** | 「自定义样式」seed:编辑器美化 / 窄屏侧栏隐藏 / menubar 加高 / 批注弹窗等 5 张通用样式表,新 wiki 也能一键初始化(可选,v0.16.22) |
30
- | 📝 **可配置的注入提示词** | 插件注入每个会话的「TiddlyWiki 持久知识库」提示词可在设置页配置:**默认精简版**(~1.7KB,只保留工具 schema 表达不了的约定——同步纪律 / 标签约定 / 链接格式),可选**完整版**(额外附一份**由工具注册表实时生成**的参数索引,不会再过期);`extra` 追加自定义规范、`override` 整段接管、可整体停用;**保存后无需重启 dsh web**(section 即时重注册,当前会话下一步即生效),设置页可**按表单当前值预览**即将注入的全文(未保存的形态切换也立刻可见,v0.21.0 / v0.22.7) |
31
- | 🤖 **Agent 工具** | 15 个 `tiddlywiki_*` 工具:检索(**相关度排序 + 命中处片段 + 字段过滤**)、读写、**增量追加**、批量、重命名、**软删除/回收站**、**反向链接**、**附件入库**、**知识库体检**、git 同步与冲突解决(v0.19.0;检索/最近仍在**服务端**排除二进制附件,大 wiki 上从 515MB/17s 降到 ~0.4s) |
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) |
33
- | 🕒 **时间戳不再丢** | 插件写入的每条笔记都会带上 TW 的 `created`/`modified`(17 位紧凑 UTC,与 TW 编辑器逐字节一致):新建 = 两者都取当前时刻,覆盖 = **保留原 `created`、刷新 `modified`**。此前这两个字段被当成「TW 服务端会补」而丢弃,而服务端**从不补**——缺 `modified` 的条目会被 TW 的 `sortTiddlers` 当空串,在 `+[!sort[modified]]` 页面上**直接沉到最后一名**(表现为「新日记没被收录」)。`TiddlyWebClient.put()` 还有一道兜底,任何写路径都不会写出无时间戳的条目(v0.22.5) |
34
- | 🧼 **渲染片段净化** | 回复流卡片与会话汇总注入的 TW 片段先经 **host 白名单净化**(丢 `iframe`/`script`/`svg`/`on*`/`javascript:`/`data:text/html` 等)——TW 自己的解析器只剥 `on*`,`<iframe src="javascript:…">` 会原样通过并在 DSH 页面里执行(v0.19.1 修复的存储型 XSS) |
35
- | 🔒 **写路由方法校验** | 每个写路由只接受自己的 HTTP 方法:跨站 `GET /sync`、`GET /restart`、`GET /upload` 一律 405 且无副作用(v0.19.0 修复了「任意网页一张 `<img>` 即可触发 pull/commit/push」的 CSRF 面) |
36
- | 🔑 **密钥不外泄** | `bridge.token` / `ui.sendToAgent.token` / **`auth.password`**(v0.20.0)在 `/admin/state`、`/admin/config` 的回包里是 `********`(设置页原样保存 ≠ 覆盖,清空即删除),`git.remote` 里的 PAT 打码;**`/tw`、`/api` 与 `POST /render` 都拒绝 `$:/plugins/dsh-tiddlywiki/` 命名空间**——此前 `/render` 能把配置 tiddler 连 token 与 PAT 一起渲染出来(v0.19.3 / v0.20.0) |
37
- | 🧯 **路由不会拖垮进程** | 所有路由经 `guardHandler` 包装:任何 rejection(含代理里 try 之外的 `new URL()`)都变成 413/500 响应,而不是宿主未处理的 promise rejection(那会**直接结束 dsh web 进程**并挂死请求);剪藏桥 listen 后保留常驻 `error` 监听(v0.19.3) |
38
- | 📊 **回复流卡片** | 工具结果显示原生 TW 卡片(**按笔记自己的内容类型渲染**:Markdown 笔记就是 Markdown,v0.18.0),检索/最近列表带**命中处摘要**(v0.22.8);`[标题](/dsh-tiddlywiki/tw/#标题)` 点击直达 TW 面板 |
39
- | 📤 **发送给 Agent** | TW 笔记工具栏一键把当前笔记注入所选 dsh 会话(可选工作模式/权限/附加说明);成功/失败会弹出提示(v0.20.0 修复:此前提示把自由文本当 tiddler 标题传给 TW notifier,全部静默) |
40
- | 📮 **发布到微信公众号**(**可选,默认关**) | 把笔记一键发到公众号**草稿箱**(可选点发表):TW 渲染 → 补内联样式 → 浏览器自动化复用你已登录的后台会话。**绕开官方 API 权限封锁**(2025-07 起个人主体账号的发布接口被回收),个人号可用;发表需管理员扫一次码。**需额外安装**(opencli + 浏览器扩展),插件不替你装;**关闭时完全不打扰**(不注入提示词、不写文档)。**带发布元数据**(`pub-state`/`pub-platform`/`pub-wechat-*` + `no-publish` 标签)避免重发或误发。见 [docs/wechat-publish-setup.md](docs/wechat-publish-setup.md) |
41
- | 🧭 **内嵌编辑器** | 中央列内嵌完整 TW 5 编辑器(同源代理,Tailscale/内网/域名/HTTPS 均可) |
42
- | 🗂️ **右侧边栏 Tab** | DSH 新右侧栏(rightbar):首页「TiddlyWiki 知识库」入口一键打开,与聊天并排;链接点击可直达(v0.16.21) |
43
- | 🧪 **审计守门** | 第四轮审计(v0.20.0)把 CI 与 `npm run verify:*` 合成一份清单,并补上 `verify-constants`(filter 长度预算)、`/render` 403、auth 打码、notify 与草稿避让回归 |
44
- | 🧩 **Better Sidebar 共存** | 与 dsh-better-sidebar 侧边栏共存(其展开/收起按钮浮在 TW 面板之上);**不再向该侧边栏注册 TW tab**(v0.17.0 移除了 tab 注册,避免 tab kind 冲突) |
45
- | 📚 **会话知识库 Tab** | 每个会话顶部汇总本会话读写过的 wiki 笔记,TW 原生渲染(host `/render` 片段管线 + 白名单净化,v0.16.19 / v0.19.1) |
46
- | 📝 **快速笔记** | 输入框上方快捷按钮或右下角「知识库」FAB;原生编辑页或 Markdown 卡片两种模式;首页内置「快速记笔记(完整编辑器)」 |
47
- | 📌 **本地剪藏桥** | 可选「剪藏桥 + 书签小工具」:DSH 监听 127.0.0.1 端口接收剪藏请求(Host 校验防 rebinding),点书签弹出浮层——可改标题/编辑选中文字/**勾选图片**,一键写入知识库;文字成笔记(默认 `clip` 标签),图片由桥下载存为**二进制附件**(`type: image/*` + base64,笔记内 `[img[标题]]` 内嵌),随 wiki 自动进 git;图片下载有 **SSRF 守卫**(仅公网 http(s)、逐跳校验重定向,v0.18.0) |
48
- | 🔐 **写操作防跨站** | 所有写路由(笔记/上传/同步/后台/代理)拒绝跨站请求(403),仅同源的 GUI 与内嵌 TW 可写;跨站 GET 与浏览器地址栏直达不受影响(v0.18.0) |
49
- | ✅ **待办四象限** | 首页看板:任务打 `todo` 标签即收录,拖动即可分类/完成(正文附 `q` 字段),逾期/今日到期自动统计 |
50
- | 🌗 **跟随主题** | 内嵌 TW 自适应 DSH 深浅主题(纯内存切换,不进 git) |
51
- | 🔄 **一键同步** | FAB「同步」一键 pull→commit→push,状态点实时反映 git 状态 |
52
- | 💾 **数据即备份** | wiki 文件夹本身就是 git 仓库,自动 commit(60s 防抖) |
53
-
54
- ---
55
-
56
- ## 📦 安装
57
-
58
- ```bash
59
- # ① npm 发布包(推荐)
60
- dsh plugin --profile web add dsh-tiddlywiki
61
-
62
- # ② 直接从 GitHub 安装
63
- dsh plugin --profile web add github:bbqisbbq/dsh-tiddlywiki
64
-
65
- # ③ 本地开发(link 方式,改完 src 后 npm run build 即生效)
66
- dsh plugin --profile web add link:/path/to/dsh-tiddlywiki
67
- ```
68
-
69
- 装完**重启 dsh web** 生效。首次启动自动完成:初始化 wiki 目录、`git init` 并提交基线、写入**功能必需的 seed**(发送给 Agent 按钮 + 原生渲染路由 + 同源代理基址)与**起步文档**(插件说明 + 示例与文档);首页/所有文章/自定义样式等为可选项,需要时在设置页「初始化」手动写入。
70
-
71
- ---
72
-
73
- ## 🚀 快速开始
74
-
75
- 1. **安装并重启** dsh web。
76
- 2. 侧边栏「**TiddlyWiki**」→ 中央打开完整 TW 编辑器。
77
- 3. **随手记**:点输入框上方「📝」或右下角「知识库」→「快速笔记」;或在首页直接用「✍️ 快速记笔记」区块,写完保存即成为 tiddler 并自动进 git。
78
- 4. **看文档**:新 wiki 首页(初始化 home-index 后)有「**📚 插件文档**」栏——插件说明、汇总教程、模板都在里面,可自由删改。
79
- 5. **收工同步**:「知识库」→「🔁 同步」一键 push 备份。
80
- 6. **让 Agent 参与**:直接说「把刚才的会议纪要存进知识库」——Agent 用 `tiddlywiki_*` 工具读写。
81
- 7. **发笔记给 Agent**:TW 里打开笔记 → 工具栏「发送给 Agent」→ 选会话 → 作为消息注入。
82
-
83
- ---
84
-
85
- ## 📖 使用指南
86
-
87
- ### 🤖 Agent 工具(15 个)
88
-
89
- | 工具 | 说明 |
90
- |---|---|
91
- | `tiddlywiki_search` | 检索(`query` + 可选 `tags[]/tag/since/type/field/value/limit`),**按相关度排序**(标题命中 > 标签 > 正文命中次数),摘要取自**命中处上下文**而不是正文开头 |
92
- | `tiddlywiki_recent` | 最近修改的笔记(倒序,支持 `limit`/`since`),开工快速了解动态 |
93
- | `tiddlywiki_list_tags` | 现有非系统 tag 及计数(已排除只挂在二进制附件上的 tag);默认列使用最多的 200 个(`limit` 可调、上限 1000),截断时返回 `total`/`truncated` |
94
- | `tiddlywiki_get` | 读单个 tiddler 全文(`modified` 以 ISO 返回,可直接用作 `expectedModified`) |
95
- | `tiddlywiki_put` | 写/覆盖;**新建**未指定类型时默认 `text/markdown`(`$:/` 系统条目除外),**覆盖既有条目时保留原 `type`/tags/自定义字段**(v0.20.1,改类型要显式 `fields.type`);`expectedModified` + `force` 提供乐观并发保护 |
96
- | `tiddlywiki_batch_put` | 批量写入(`overwrite=false` 跳过已存在;单条失败不影响其余、逐条报错;4 路并发但结果保持入参顺序) |
97
- | `tiddlywiki_append` | **增量追加**:`mode=append\|prepend`、`heading=某标题` 定位段落,写日志/批注无需读全文;与 `put` 同一套写策略(保留原 `type`/tags/自定义字段,新条目才默认 markdown),支持 `fields` 显式覆盖 |
98
- | `tiddlywiki_rename` | 重命名 + 尽量同步其他条目里的引用;旧标题删除失败时如实回报「两份副本都在」 |
99
- | `tiddlywiki_delete` | 删除(幂等)。默认**软删除**进 `$:/dsh-tiddlywiki/trash/`,`permanent=true` 才真删;支持 `expectedModified`/`expectedRevision`/`force` 乐观并发(读后被改动则拒绝,不把人类的新改动丢进回收站) |
100
- | `tiddlywiki_trash` | 回收站:`action=list\|restore\|empty`(索引读不到或损坏时显式报错,绝不把回收站当空的重建) |
101
- | `tiddlywiki_backlinks` | 反向链接:谁用 `[[标题]]`/`{{标题}}` 引用了它、谁把它当标签 |
102
- | `tiddlywiki_attach` | 把**本机文件或公网 http(s) 地址**存成二进制附件(图片/PDF/…),可嵌入某篇笔记;URL 走 SSRF 守卫;**同名 tiddler 已存在时默认拒绝**(避免静默覆盖笔记),确认覆盖要传 `force: true`(tags/自定义字段仍保留) |
103
- | `tiddlywiki_lint` | 知识库体检:垃圾标签 / 死链 / 空笔记 / 缺内容类型的类 Markdown 笔记 |
104
- | `tiddlywiki_git_sync` | `action: pull\|push\|sync` |
105
- | `tiddlywiki_git_resolve` | pull 冲突后按 tiddler 二选一(`keep-local\|keep-remote`) |
106
-
107
- **知识库同步纪律**:
108
- 1. 开工先 `tiddlywiki_git_sync action=pull`(rebase + autostash,真冲突会 abort 并报文件)。
109
- 2. 冲突后用 `tiddlywiki_git_resolve` 二选一解决,再重新 sync(**绝不自动覆盖**)。
110
- 3. 收工 `tiddlywiki_git_sync action=sync`(pull → commit → push)。
111
-
112
- > 🔍 **检索/最近跳过二进制附件**(v0.16.20):`search`/`recent` 在**服务端**只取文本 tiddler(无 `type` 字段或 `text/*`),图片/音频/视频/PDF/zip 等二进制 tiddler(base64 正文)不参与检索、不出现在结果里——大 wiki(如数千张书籍扫描页图)从此从 515MB/17s 降到 ~0.4s,也不会被同名书页图刷屏。`get` 对二进制 tiddler 只返回元数据(`binary=true` + 类型/大小 + 链接),不返回 base64 正文。
113
-
114
- > ⚠️ `fields.type` 是 TW 的**内容类型保留字段**(`text/markdown` 等),业务分类请放 `tags`,别写进 `fields.type`。
115
-
116
- ### 🧠 注入的系统提示词(可配置)
117
-
118
- 插件会往每个会话的系统提示词里注入一段「TiddlyWiki 持久知识库」约定(**设置页 → 系统提示词**):
119
-
120
- | 配置 | 作用 |
121
- |---|---|
122
- | `prompt.enabled` | 关掉后本插件不注入任何文本 |
123
- | `prompt.mode` | `slim`(**默认**):只保留工具 schema 表达不了的约定(写入/并发纪律、同步纪律、标签约定、可点击链接格式);`full`:额外附一份**参数索引**,由工具注册表在运行时生成,因此永远不会与真实工具脱节 |
124
- | `prompt.extra` | 追加在末尾的自定义规范(团队 / 个人偏好),始终生效 |
125
- | `prompt.override` | 非空时整段取代内置文本(`extra` 仍会追加)——想完全自写提示词时用 |
126
-
127
- 保存后**不用重启 dsh web**:host 会即时重新注册 prompt section,当前会话从**下一步**起就使用新文本(DSH 的 `system-prompt/change` 会更新历史里的系统消息)。设置页的「预览注入文本(按表单当前值)」按钮会把**表单里此刻的值**(含还没保存的形态切换 / extra / override / 停用开关)发给 host 实时拼成全文——所见即「保存后会注入的内容」,顶部还会标注「含未保存的修改」;预览是只读的,不会替你保存(v0.22.7 之前它只读**已保存**的配置,所以切换形态后不点保存直接预览会看到逐字节相同的旧文本,容易被读成「两种形态没差别」)。
128
-
129
- > 为什么默认精简: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 的每个工具与每个参数都必须在场 / 两种形态都必须保留治理约定块)。
130
-
131
- ### 🧑‍💻 界面操作
132
-
133
- - **📤 发送给 Agent**:TW 工具栏按钮(首次启动自动写入 wiki,ONE-SHOT)。弹层可选**附加说明**(位于消息末尾)、**工作模式**(Agent 预设)、**权限**(权限预设),按工作区分组选会话或新建。消息自带待办说明。
134
- - **🧭 中央列编辑器**:侧边栏「TiddlyWiki」开关(显示名可改 `ui.sidebarLabel`,保存后即时生效,v0.22.8)。
135
- - **🗂️ 右侧边栏 Tab**(v0.16.21):DSH 新右侧栏展开后,首页会出现「**TiddlyWiki 知识库**」入口盒,点击即在右侧栏以 tab 形式打开完整 TW 编辑器——**与聊天并排**,适合边聊边查/边记。由 `ui.showRightbarTab` 控制(默认开);老版本 DSH(无右侧栏)自动跳过。
136
- - **🧩 Better Sidebar 共存**:装了 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 时,插件只做 UI 共存(中央 TW 面板的 z-index 自动压在其侧边栏展开/收起按钮之下,按钮始终可点)。**v0.17.0 起不再向 dsh-better-sidebar 注册「TiddlyWiki 知识库」tab**(旧版可用 `ui.showBetterSidebarTab` 关闭)——该 tab 的 kind 会与其它注册方冲突报 `tab kind "dsh-tiddlywiki" is already registered`。右侧边栏入口请用上面的 **右侧边栏 Tab**(DSH 原生 rightbar)。
137
- - **📝 快速笔记**:输入框上方快捷按钮(`ui.showQuickNoteDock`)或 FAB;`ui.quickNoteMode` 选打开方式——**native**(默认,直达 TW 原生编辑页,草稿自动续写)或 **card**(CodeMirror 6 Markdown 高亮、文件上传、多选 tag、草稿自动保存、「🕘 最近」载入、Ctrl+Enter 保存)。**v0.22.6**:原生编辑弹窗里用 TW 的「🗑 删除」把笔记删掉后不会再变成打不开的白板——再点一次「快速笔记」就会重新载入编辑器;card 模式底部操作条改成「按钮文字永不折行、放不下时整组换行」,窄卡片里不再把「✏️ 在 TW 中编辑」压成两行。
138
- - **📌 本地剪藏桥**(可选,v0.16.24+/v0.16.25 支持图片):DSH 设置 → 常规配置 → 勾选「**启用本地剪藏桥**」(保存后立即生效)——DSH 即监听 `127.0.0.1:8618` 接收剪藏请求。浏览器书签栏新建书签,把知识库文档里的 JS 代码粘贴为地址,点一下弹出**浮层**:确认/修改标题与划线文字、勾选页面图片(封面自动标出)→「剪藏」即写入 wiki。文字成笔记(默认 `clip` 标签、正文含来源与选中文字、重名自动 `(2)` 去重);**图片由 DSH 本机下载字节存为二进制附件**(`clip-url`/`clip-note` 溯源字段,笔记内 `[img[标题]]` 内嵌展示;某张下载失败自动降级为链接),全部随 wiki 自动 git commit。「📚 插件文档」栏的 `本地剪藏桥(书签小工具)` seed 文档含完整步骤、书签代码、安全说明与 curl 用法。安全:桥只监听 127.0.0.1 + Host 白名单防 DNS rebinding;**强烈建议设 `bridge.token`**(非空时校验书签的 `x-clip-token` 头,防止任意网页往 wiki 里塞内容)。图片附件按设计不参与 `search`/`recent`(避免刷屏),`get` 只回元数据。
139
- - **🏠 首页(初始化 home-index 后)**:待办四象限(`todo` 标签 + `q` 字段拖放分类)+ 快速记笔记(完整编辑器,勾选「同时加入待办」即建任务)+「所有标签 / 所有文章」入口 +「📚 插件文档」栏(自动收录所有带 `dsh-docs` 标签的 seed 文档)。
140
- - **📚 会话知识库 Tab**:会话顶部 Tab(`ui.tabLabel` 改名、`ui.showSessionTab` 关闭),自动汇总本会话读写过的 wiki 笔记(写入 volatile `$:/temp`,不落盘不进 git),**TW 原生渲染**(v0.16.19 起 `/tw/render` 片段管线,与回复流工具卡同链路),链接点击直达中央 TW 面板,不可编辑。
141
- - **🌗 跟随 DSH 主题**:内嵌 TW 随 DSH 深浅切换 palette,纯内存不写回 wiki(`ui.followDshTheme`/`ui.darkPalette`)。
142
- - **🔧 知识库 FAB**:统一入口(TW 面板开关/重载、快速笔记、同步、TW 服务状态悬停 tip)。同步拉取到新内容会自动重启 TW(同端口)。
143
- - **⚙️ 设置页**:DSH 设置 →「TiddlyWiki 知识库」:状态/重启、**知识库位置(可切换)**、常规配置、插件/主题/语言管理、**初始化**(seed 状态与重新初始化)。配置写入 `$:/plugins/dsh-tiddlywiki/config` tiddler,覆盖 cordis `config:` 块(tiddler 优先)。
144
-
145
- ### 📍 知识库位置(可切换,v0.22.0)
146
-
147
- 设置页顶部「**知识库位置(可切换)**」区块显示当前实际服务的文件夹、这个位置**是怎么决定的**(指针文件 / cordis 配置 / 默认值)、指针文件路径,以及**同目录下其它看起来像 wiki 的文件夹**(含 `tiddlywiki.info`)的快捷填入按钮。
148
-
149
- - **换一个位置**:填根目录(绝对路径,支持 `$DSH_HOME` / `${VAR}` / `%VAR%`)+ 文件夹名 → 「切换到这个位置」。host 会停掉 TW → 释放自动提交与监听 → 指向新目录 → 起 TW → 重载新 wiki 的配置 tiddler → 跑核心 seed(markdown 插件 / 发送给 Agent / 渲染路由 / 同源代理 / 语言)→ 重新武装自动提交;**选中即被记住**(写进 `$DSH_HOME/dsh-tiddlywiki/location.json`),重启 dsh web 后仍在新知识库。
150
- - **新建一个知识库**:目标文件夹若还没有 `tiddlywiki.info`,插件会照常自动 `--init server` 建一个全新的(**别填已有的普通笔记目录**——它会被初始化)。
151
- - **恢复为配置默认**:删除指针文件并(必要时)切回 `config.wikiRoot` / `config.wiki`。
152
- - **安全语义**:指针文件在任何 wiki **之外**(若存在 wiki 自己的配置 tiddler 里,切到新 wiki 就会把这个选择一起丢掉——鸡生蛋);指针文件损坏/非法时**明确报告并回退到配置默认**,不会静默乱跑;切换**失败会回滚**到原来的知识库,并告诉你是「已回滚」还是「回滚也失败了」;同刻只允许一个切换(并发请求直接拒绝)。
153
- - **注意**:切换期间正在跑的 Agent 工具调用会失败(TW 在重启),界面上按钮会禁用并显示「切换中…(重启 TW)」。
154
-
155
- `wikiRoot` / `wiki` 现在只是**默认值**——运行中的实际位置以指针文件优先。这也是为什么它能不改 cordis、不重装就切。
156
-
157
- ### 📮 发布到微信公众号(**可选功能,默认关闭**,2026-09-17)
158
-
159
- > **这是可选功能**:需要额外安装 opencli + Browser Bridge 浏览器扩展,**插件不替你装**。
160
- > 开关 `wechat.enabled` **默认 `false`**——关闭时不注入任何发布相关提示词、也不往 wiki 写
161
- > 「发布元数据规范」。开启:DSH 设置 →「TiddlyWiki 知识库」→「可选功能:微信公众号发布」。
162
- > 完整安装步骤与排错见 [docs/wechat-publish-setup.md](docs/wechat-publish-setup.md)。
163
-
164
- 把 wiki 里的任意笔记**一键发到微信公众号草稿箱**(可选直接发表)。整套能力放在 `tools/wechat/`,**不依赖公众号服务端 API**——因为 2025-07 起官方已回收个人主体账号的「发布能力」接口权限;本方案改用**浏览器自动化复用你已登录的后台会话**,所以个人号也能用。
165
-
166
- ```bash
167
- # ① 先按 docs/wechat-publish-setup.md 装好 opencli + 浏览器扩展,并登录公众号
168
- # ② 一次性:装 adapter 到本机 opencli(幂等,会自检扩展/登录状态)
169
- node tools/wechat/install-wechat-adapters.mjs
170
-
171
- # ③ 发布(注意必须带 --trace retain-on-failure,原因见下)
172
- opencli weixin publish-note "笔记标题" --trace retain-on-failure -f json
173
- opencli weixin publish-note "笔记标题" --cover ./cover.png -f json # 带封面
174
- opencli weixin publish-note "笔记标题" --preview ./out -f json # 先导出排版预览
175
- opencli weixin publish-note "笔记标题" --publish -f json # 直接发表(需管理员扫码)
176
-
177
- # ④ 可选:回填存量「已发布」状态(默认 dry-run)
178
- node tools/wechat/backfill-publish-state.mjs # 看
179
- node tools/wechat/backfill-publish-state.mjs --write # 写
180
- ```
181
-
182
- **流程**:笔记标题 → DSH 的 `/render`(TW 自己渲染成语义 HTML,含代码高亮)→ `wechat-html.js` 补**内联样式**(微信会剥 `<style>` 和 class,只认内联)→ opencli 驱动后台填表/写正文/传图/设封面/存草稿 →(可选)点发表。
183
-
184
- **三个要点**:
185
-
186
- 1. **必须带 `--trace retain-on-failure`**——不带会对 `mp.weixin.qq.com` 稳定报 `Navigation rejected`(实测 trace 开 5/5 成功、关 8/8 失败;这是 opencli 1.8.7 的 bug,`--site-session ephemeral` 等绕法均无效)。
187
- 2. **发表必须管理员扫码**——后台点「发表」后微信要求管理员微信扫码确认,无法自动化。「一键」的真实含义是「脚本做到填表/排版/上传,你只需扫一次码」。默认走**发表**(不推送粉丝、不占群发额度),群发请自行在后台操作。
188
- 3. **图片上传用 DataTransfer 注入**,不用 `page.setFileInput`——后者依赖 CDP `Page.fileChooserOpened`,本机扩展版本组合下稳定失败;改用页面上下文直接塞 `input.files`,实测图片真进 `mmbiz.qpic.cn`。代价是单图 **8MB** 上限(字节要以 base64 穿过 evaluate)。
189
-
190
- **换机器还原**见 [docs/wechat-publish-setup.md](docs/wechat-publish-setup.md)(含 opencli / Browser Bridge 扩展安装、扫码登录、排错表)。设计依据与全部实测细节见 [docs/plans/2026-09-17-wechat-publish-design.md](docs/plans/2026-09-17-wechat-publish-design.md)。
191
-
192
- ### 🧩 初始化(一次性预置 seed):哪些「必备」,哪些「可有可无」
193
-
194
- seed 是把「wiki 里预置内容」随插件分发的机制:**ONE-SHOT(只写缺失)+ 安全跳过(同名 tiddler 已存在绝不覆盖你的数据)**,需要时可「重新初始化」恢复、可「反初始化」移除。详细见 [docs/seed-initialization.md](docs/seed-initialization.md)。
195
-
196
- | 层级 | seed | 说明 | 首次安装 |
197
- |---|---|---|---|
198
- | 🔒 **核心**(功能必需,不可移除) | `send-to-agent` | TW 工具栏「发送给 Agent」按钮插件 | 自动写 |
199
- | | `render-route` | 原生渲染路由(回复流卡片 / 链接直达依赖) | 自动写 |
200
- | | `tw-web-host` | TW 前端 API 基址 → 同源代理(内嵌编辑器前提) | 自动写 |
201
- | 📖 **起步**(默认写、可移除,**想要完整体验建档案**) | `doc-note` | 「dsh-tiddlywiki 插件说明」笔记 | 自动写 |
202
- | | `starter-docs` | 「示例与文档」:主题汇总页·模板 + 教程 + 三个示例主题页(日志/决策记录/排障) | 自动写 |
203
- | 🎀 **可选**(默认不写、设置页手动、可移除,**可有可无**) | `home-index` | 首页(四象限待办 + 快速记笔记 + 所有标签/所有文章 + 📚 文档栏)——文档中心的「门面」 | 手动 |
204
- | | `all-articles` | 「所有文章」两列分页总览(🤖 Agent / 👤 人工) | 手动 |
205
- | | `ui-styles` | 自定义样式 5 张(编辑器美化 / 窄屏侧栏 / menubar 加高 / 批注弹窗等) | 手动 |
206
- | | `menubar-theme` | menubar 顶栏跟随 DSH 主题换色 | 手动 |
207
- | | `clip-bridge` | 「本地剪藏桥(书签小工具)」使用说明——含书签代码 / 启用步骤 / 安全说明(真功能在插件运行时代码里,此 seed 只预置文档) | 手动 |
208
-
209
- - **想获得完整插件体验**:核心 3 项首次安装就有;再补 `home-index`(首页)+ `starter-docs`(示例文档)即是一个开箱即用的文档中心。
210
- - **一个可选项都不想要**:完全不影响功能——设置页「反初始化」即可,核心项受保护不可移除。
211
- - **文档怎么扩散到更多**:以后插件新增的任何说明 / 教程 / 模板类内容都走 seed 并带 **`dsh-docs`** 标签——首页「📚 插件文档」栏自动收录,你无需任何配置。
212
- - **内置内容更新了怎么办**(v0.22.0):seed 标记里记着**内容哈希**,设置页据此显示两个提示 chip——「**⬆ 有更新**」(内置内容比你 wiki 里预置的更新,可点「更新到内置版本」取用)与「**✏️ 本地已修改**」(这篇是你的内容,重新初始化会覆盖它,会先二次确认)。升级插件后旧 wiki 的标记没有哈希,会显示「更新检测尚未启用(重新初始化一次即可)」,**不会**把老标记误判成「你改过」;重新初始化一次即升级标记。**绝不自动改写你的 wiki**——更新只在你点的时候发生。
213
-
214
- ---
215
-
216
- ## 🛠 配置
217
-
218
- 插件行默认配置(缺省即默认,无需手动配置):
219
-
220
- ```yaml
221
- - id: dsh-tiddlywiki
222
- config:
223
- wikiRoot: "$DSH_HOME/tiddlywiki" # 默认位置;运行中可被设置页「知识库位置」覆盖(指针文件优先,v0.22.0)
224
- wiki: "main" # 文件夹名("." = 直接用 wikiRoot 这个目录)
225
- port: 0 # 0 = 自动探测空闲端口
226
- git:
227
- autoCommit: true
228
- debounceMs: 60000
229
- remote: "" # 空 = 仅本地 commit;填了才 push
230
- branch: "main"
231
- note:
232
- tag: "inbox" # 快速笔记默认 tag
233
- startup:
234
- readyTimeoutMs: 60000 # TW 启动就绪窗口(v0.22.5,5s–600s;超出只警告并继续等,硬上限 = 3×,仍未就绪才判失败)
235
- prompt:
236
- enabled: true # false = 本插件不注入任何提示词
237
- mode: "slim" # slim(默认:只留约定)/ full(+ 由工具注册表实时生成的参数索引)
238
- extra: "" # 追加在提示词末尾的自定义规范(团队/个人偏好)
239
- override: "" # 非空时整段取代内置文本(extra 仍会追加)
240
- bridge:
241
- enabled: false # 本地剪藏桥(书签小工具);保存后立即生效
242
- port: 8618 # 监听端口(127.0.0.1;改后需重启 dsh web 才绑定新端口)
243
- token: "" # 共享口令;非空时校验书签的 x-clip-token 头(强烈建议设置)
244
- tag: "clip" # 剪藏笔记默认 tag
245
- ui:
246
- showQuickNote: true # FAB 里显示快速笔记入口
247
- showQuickNoteDock: true # 输入框上方快捷按钮
248
- quickNoteMode: "native" # native=TW 原生编辑页 / card=Markdown 卡片
249
- sidebarLabel: "TiddlyWiki" # 侧边栏入口显示名
250
- showPanelStatus: true
251
- showSyncButton: true
252
- followDshTheme: true # 跟随 DSH 深浅主题
253
- darkPalette: "$:/palettes/CupertinoDark"
254
- tabLabel: "知识库" # 会话 Tab 名
255
- showSessionTab: true
256
- showRightbarTab: true # DSH 右侧边栏提供 TiddlyWiki 入口/Tab
257
- sendToAgent: { enabled: true }
258
- uiLanguage: "" # 留空不干预;"zh-Hans" 自动启用简体
259
- auth:
260
- username: "" # 默认 loopback 匿名;暴露到非回环才需要
261
- password: "" # 非空时插件内置客户端/就绪探测/浏览器代理都带 Basic 认证(v0.18.0 起真正可用)
262
- ```
263
-
264
- > **运行时配置**:设置页写入的 `$:/plugins/dsh-tiddlywiki/config` tiddler 是 `config:` 块之上的覆盖层(tiddler 优先、随 wiki git 同步),改 note tag / git / ui 开关 / **注入提示词(`prompt.*`)** 都无需动 cordis;**提示词改动保存后立即生效**(section 即时重注册,当前会话下一步生效),剪藏桥端口改动仍需重启 dsh web 重新绑定监听;**`startup.readyTimeoutMs` 对下一次 TW 启动/重启生效**(v0.22.5)。
265
- >
266
- > **知识库位置是三层**(v0.22.0):指针文件 `$DSH_HOME/dsh-tiddlywiki/location.json` > `config:` 块的 `wikiRoot`/`wiki` > 内置默认(`$DSH_HOME/tiddlywiki` + `main`)。`wikiRoot`/`wiki` 因此是「默认位置」而不是「唯一位置」——设置页切过的位置存在指针文件里(一个在 wiki 之外的文件),所以要换回配置值就点「恢复为配置默认」。
267
-
268
- ---
269
-
270
- ## 🌐 远程访问
271
-
272
- TW 子进程只监听 **127.0.0.1 回环**;Agent 工具/快速笔记/同步都走 DSH 宿主进程→回环 TW,不受访问入口影响。浏览器里的 TW 编辑器 iframe 经**同源代理** `<DSH origin>/dsh-tiddlywiki/tw/` 访问(v0.6.0 起),所以通过 Tailscale / 内网 / 域名 / HTTPS 访问 DSH 时编辑器照常工作,且 TW 重启不打断编辑中的内容。
273
-
274
- ---
275
-
276
- ## 👨‍💻 开发
277
-
278
- 需要 **Node.js ≥ 22**。
279
-
280
- ```bash
281
- npm install
282
- npm run typecheck # tsc --noEmit
283
- npm run build # clean + host tsdown + client tsdown + wrap(wrap 会校验 id 与体积 <900KB)
284
- npm run selftest # headless:spawn TW → REST 读写 → git → 退出回收
285
- npm run smoke:client # client bundle 的 module-loader 形状冒烟
286
- npm run verify # = verify:static + verify:unit + verify:e2e(本地一键;CI 同款分档)
287
- npm run verify:large # 3000+ 条目大 wiki:检索耗时 / 二进制零出现 / 真跑 commit(约 1 分钟)
288
- node scripts/verify-send-to-agent-bundle.mjs # bundle 字段 + 源件逐字一致
289
- node scripts/verify-seed-send-to-agent.mjs # 全新 wiki 上的 seed E2E
290
- node scripts/verify-clip-bridge.mjs # 剪藏桥 headless 验收(含 SSRF 守卫)
291
- node scripts/verify-seeds-admin.mjs # /admin/seeds 状态与 run 的 E2E
292
- node scripts/verify-prompt.mjs # 注入提示词守门(slim 无参数清单 / full 与工具注册表逐项一致 / 治理约定不丢)
293
- node scripts/verify-wiki-switch.mjs # 运行时切换知识库 E2E(真起 TW:切换 / 回滚 / 指针文件 / 非法输入,v0.22.0)
294
- ```
295
-
296
- > 📦 从 **npm 包**安装的用户只有 `lib/` + `src/` + `docs/`(`scripts/` 不在发布包里,避免把构建脚本塞进依赖树)——想跑上面的验收脚本请用 git 仓库:`git clone https://github.com/bbqisbbq/dsh-tiddlywiki && npm install`。
297
-
298
- **改 bundle/seed 的再生成流水线**(不要手改 `seed-*.ts` 里的生成常量;bundle 版本号只在 `scripts/bundle/versions.mjs` 定义一处):
299
-
300
- ```bash
301
- # 发送给 Agent 按钮(改 scripts/bundle/send-to-agent/ 后)
302
- node scripts/build-send-to-agent-bundle.mjs
303
- node scripts/gen-seed-send-to-agent.mjs scripts/bundle/send-to-agent.bundle.json src/host/seed-send-to-agent.ts
304
- node scripts/verify-send-to-agent-bundle.mjs
305
-
306
- # 渲染路由(改 scripts/bundle/render/server-routes/render.js 后;版本在 versions.mjs)
307
- node scripts/build-render-bundle.mjs
308
- node scripts/gen-seed-render.mjs scripts/bundle/render.bundle.json src/host/seed-render.ts
309
-
310
- # 首页(改 wiki 的 🏠 主页/所有标签/标签笔记 .tid 后;默认就剥离作者私有人口 +
311
- # 注入「📚 插件文档」栏,确要保留私有内容才加 --keep-private)
312
- node scripts/gen-seed-home.mjs '<wiki>/tiddlers/🏠 主页.tid' '<wiki>/tiddlers/所有标签.tid' '<wiki>/tiddlers/标签笔记.tid' src/host/seed-home.ts
313
-
314
- # 自定义样式(改 wiki 的样式 .css + .meta 后;tag 自动收窄为 $:/tags/Stylesheet)
315
- node scripts/gen-seed-ui-styles.mjs '<wiki>/tiddlers/<样式.css>' … src/host/seed-ui-styles.ts
316
-
317
- # 示例与文档(starter-docs)/ menubar 主题:直接维护 src/host/seed-starter-docs.ts / seed-menubar-theme.ts
318
-
319
- # 随后 npm run build
320
- ```
321
-
322
- **产物约定**:`lib/` 内**零** `@deepseek-ai` 运行时 import;client 必须 minify(否则 >1MB 被注册表校验拒绝);`react`/`tiddlywiki` 不打包(运行时解析)。
323
-
324
- ### 路由参考(开发者)
325
-
326
- | 路由 | 方法 | 用途 |
327
- |---|---|---|
328
- | `/dsh-tiddlywiki/status` | GET | 面板健康(service/url/git/tag/ui) |
329
- | `/dsh-tiddlywiki/note` `/edit` `/tags` `/recent` `/get` `/search` | POST/GET | 快速笔记、打开编辑器、tag/最近/单个/搜索(列表路由都支持 `limit`;`/tags` 另支持 `sort=alpha\|count` 与 `total`/`truncated`,v0.19.4) |
330
- | `/dsh-tiddlywiki/render` | POST | TW 片段渲染(host 净化后返回,回复流卡片/会话汇总用,v0.19.1) |
331
- | `/dsh-tiddlywiki/sync` `/upload` `/restart` | POST | 一键同步、文件上传、重启 TW |
332
- | `/dsh-tiddlywiki/session/summary` | POST | 会话「知识库」Tab 汇总 |
333
- | `/dsh-tiddlywiki/agent/sessions` `/modes` `/send` `/create` | GET/POST | TW「发送给 Agent」:会话/模式/发送/新建 |
334
- | `/dsh-tiddlywiki/api/*` | any | 透传 TW 服务(JSON) |
335
- | `/dsh-tiddlywiki/tw/*` | any | 同源 TW 代理(远程访问核心) |
336
- | `/dsh-tiddlywiki/admin/seeds` `/run` `/remove` | GET/POST | seed 状态(v0.22.0 起含内容哈希的「有更新 / 本地已修改」)/ 运行 / 反初始化 |
337
- | `/dsh-tiddlywiki/admin/prompt` | GET/POST | 当前注入提示词全文(GET = 已保存的有效配置,v0.21.0);POST 带 `{enabled,mode,extra,override}` 则按**草稿**渲染、写入零副作用,供设置页预览未保存的表单值(v0.22.7) |
338
- | `/dsh-tiddlywiki/admin/wiki/location` | GET | 当前知识库位置 + 来源(指针/配置/默认)+ 指针文件路径 + 同目录候选 wiki(v0.22.0) |
339
- | `/dsh-tiddlywiki/admin/wiki/switch` `/reset` | POST | 运行时切换知识库 / 恢复为配置默认(失败自动回滚并报告,v0.22.0) |
340
-
341
- ### 项目结构
342
-
343
- ```
344
- src/
345
- ├── index.ts # host 入口:装配 WikiServer/路由/工具/提示词/自动 commit
346
- ├── sdk.ts # 自包含 defineTool + dshHomePath(零 @deepseek-ai 运行时依赖)
347
- ├── host/
348
- │ ├── wiki.ts # WikiServer:spawn/kill/自愈/端口探测/就绪轮询(策略见 ready-policy.ts)+ 迟到就绪复探;TW_PROXY_PATH 同源代理
349
- │ ├── ready-policy.ts # TW 启动就绪策略(v0.22.5):软窗口 60s(可配)/ 硬上限 3× / awaitReady 纯策略(注入时钟,可单测)
350
- │ ├── tw-api.ts # TiddlyWeb REST 客户端
351
- │ ├── git.ts # git init/commit/pull/push/sync/status + AutoCommitter
352
- │ ├── routes.ts # 全部 DSH 路由 + agent-send/create/modes + session/summary
353
- │ ├── http.ts # 共用 HTTP 助手(readBody/json)
354
- │ ├── clip-bridge.ts # 本地剪藏桥(v0.16.24):127.0.0.1 监听 + POST /clip 写 wiki(Host 校验/token/CORS preflight)
355
- │ ├── session-summary.ts # 会话「知识库」Tab 后端
356
- │ ├── admin.ts # 设置页后台:tiddlywiki.info 读写 + /admin/*
357
- │ ├── config.ts # ConfigStore:cordis config 基底 + 配置 tiddler 覆盖层
358
- │ ├── seeds.ts # 统一 seed 注册表(10 项,三层:核心/起步/可选)
359
- │ ├── prompt.ts # 系统提示词(v0.21.0):slim/full 两种形态 + extra/override,full 的目录由工具注册表实时生成;v0.22.7 起草稿预览(normalizePromptPreview/describePrompt,与保存路径同一份拼装)
360
- │ ├── seed-*.ts # 各 seed 实现(bundle/首页/ui-styles 常量脚本生成;starter-docs/menubar/clip-bridge 手工维护)
361
- │ └── tools.ts # 15 个 tiddlywiki_* 工具(列表式注册)
362
- └── client/ # 浏览器半部
363
- ├── index.ts # client 入口(inject ['slots'],纯 DOM,永不 throw)
364
- ├── endpoints.ts # 客户端同源端点常量 + describeSyncResult(同步回执唯一实现,v0.22.8)
365
- ├── status-cache.ts # 共享 /status 读取器(2s TTL + 在途合并)
366
- ├── ui-config.ts # 共享 ui.* 投影 + invalidate/subscribeUiConfig(v0.22.8 扩到全部 FAB 开关)
367
- ├── render-fetch.ts # **唯一**的 POST /render 调用(v0.22.8)
368
- ├── knowledge-fab.ts / quick-note-dock.ts / note-widget.ts / editor-popup.ts
369
- │ # 知识库 FAB / 快捷按钮 / 快速笔记 / 原生编辑弹窗
370
- ├── markdown-editor.ts # CodeMirror 6 Markdown 编辑器
371
- ├── session-summary.ts # 会话「知识库」Tab(conversation.view 槽位)
372
- ├── tool-views.ts # 回复流工具卡片(tool.call.toolview)
373
- ├── theme-sync.ts / sidebar-entry.ts / sync-button.ts
374
- │ # 主题同步 / 侧边栏入口 / 同步按钮
375
- ├── tw-frame.ts # **TW frame 内核(v0.22.4 起唯一实现)**:createTwFrameSurface(skin) 管 lazy-load / status 轮询 /
376
- │ # 错误与启动态 / 主题同步 / FAB 重载 / hash 导航 / dispose;中央面板与右侧栏共用同一份
377
- └── panel.ts / rightbar-tab.ts / settings-page.ts / state.ts / styles.ts / toast.ts
378
-
379
- > ⚠️ v0.22.8 起 `endpoints.ts` 的 `describeSyncResult()`、`render-fetch.ts`、`ui-config.ts` 各是**唯一实现**:
380
- > 同步回执、渲染调用、`ui.*` 投影都不要再在客户端面里各写一份(此前已因此漂移)。
381
- scripts/ # 构建/校验/再生成脚本
382
- docs/seed-initialization.md # seed 机制详解(权威)
383
- lib/ # 预构建产物(发布含 lib/**,提交入库;零 @deepseek-ai 运行时 import)
384
- ```
385
-
386
- ---
387
-
388
- ## 🕘 版本记录
389
-
390
- > 最近几个主要版本的一句话记录(完整变更见 [Releases](https://github.com/bbqisbbq/dsh-tiddlywiki/releases) / git log)。
391
-
392
- - **v0.23.0**(2026-09-17):**新增「发布到微信公众号」+ 发布元数据**(`tools/wechat/`,**可选功能,默认关闭,需额外安装**,不动 host 代码)。把 wiki 笔记一键发到公众号**草稿箱**,可选点发表。
393
-
394
- **⚠️ 可选功能,默认关**:真正干活的是仓库 `tools/wechat/` 下的 opencli adapter + Browser Bridge 浏览器扩展——**插件本体不含它,也不会替你装**。开关 `wechat.enabled` 默认 `false`:**关闭时**不注入任何发布相关提示词、也不往 wiki 写「发布元数据规范」文档;**打开后**仅多这两项,不会安装任何东西、不启用任何后台服务。不装/不开它,插件其他功能完全不受影响。开启方式:设置 →「TiddlyWiki 知识库」→「可选功能:微信公众号发布」→ 勾选 → 保存配置。完整安装步骤(opencli、浏览器扩展、扫码登录)与排错见 [docs/wechat-publish-setup.md](docs/wechat-publish-setup.md)。
395
-
396
- **为什么不用官方 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` 驱动后台填表/写正文/传图/设封面/存草稿。
397
-
398
- **发布元数据**(本轮追加,解决「哪些发过 / 哪些不能发」):每篇笔记用自定义字段记录对外发布状态——`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 回写。
399
-
400
- **四个实测踩坑**:① **必须带 `--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 符号原样进库)。**发表需管理员扫码**——微信的账号安全机制,无法自动化,「一键」的真实含义是「脚本做完排版/上传/填表,你只扫一次码」;默认走**发表**(不推送粉丝、不占群发额度)而非群发。
401
-
402
- 命令:`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 笔记「公众号发布插件调研」。
403
-
404
- - **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` 才落盘)。
405
-
406
- - **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 条。
407
-
408
- - **v0.22.8**(2026-09-15):**第六轮代码审计修复**——host 与 client 各做一份只读审计,逐条核实后修 10 处真缺陷,并清掉一轮重复实现与死代码。① **数据丢失(P0)**:`/edit` 仍会把「调用方文本或笔记已保存正文」PUT 进**刚复用**的那个草稿,而那个草稿里装的是用户在 TW 原生编辑器里敲的**未保存内容**——触发路径极常见:打开快速笔记(标题精确到分钟)→ 写 → 关掉 → 同一分钟内再打开 → 草稿被覆盖。现在只有**我们创建的**草稿才由我们填充,复用的仅在调用方显式给了文本时才写。② **设置页超时(P0)**:`fetchJson` 展开 `init` 后又硬编码 `signal`,于是「知识库切换 / 恢复默认 / 重启 TW」的 120s 预算被 15s 掐断——宿主其实切成功了,用户却看到「切换失败」(大 wiki 冷启动实测 44s+),且重试会再走一遍停/启。③ **净化器崩服务**:`&#1114112;` 这类越界数字实体会让 `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(草稿覆盖那条**已反向验证**:还原修复即红)。
409
-
410
- - **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)。
411
-
412
- - **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 + 真实卡片结构对比修复前后的按钮排版。
413
-
414
- - **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 的重启断言(窗口热改 + 夹取 + 仍判就绪)。
415
-
416
- - **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` 的失败属环境问题(手动复核过断言全过),与本版无关。
417
-
418
- - **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()` 走真实函数(前缀 / 裸名 / 缺失 / 读失败),静态断言会剥掉注释行以免匹配到自己的说明文字。
419
-
420
- - **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:{...}}` 对象,导致「发布包内容」检查假失败(与本次改动无关,但会卡住发布流程)。
421
-
422
- - **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 已手工打上同样的两处修改)。
423
-
424
- - **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 组内容哈希断言(新鲜 / 用户改过 / 内置更新 / 旧标记两种 / 重新初始化后刷新)。
425
-
426
- - **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`。
427
-
428
- - **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+回收站恢复保类型)。
429
-
430
- - **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。
431
-
432
- - **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`。
433
-
434
- - **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 用例。
435
-
436
- - **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。
437
-
438
- - **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` 两个坏检查、客户端泄漏与草稿回写等。
439
- - **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/*`,并先做实体解码挡 `&#106;avascript:` 这类混淆),新增 **`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 描述同步。
440
-
441
- - **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 后新会话生效;客户端改动需刷新页面。
442
- - **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` 不再默认打作者本机端口。
443
- - **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` 规避冲突。
444
- - **v0.16.28**(2026-09-10):**修:拖拽版剪藏书签在知乎等真实页面报 `SyntaxError: Unexpected token ';'`**。根因(真实 Chrome 实验证实):0.16.27 把书签 href 做 **HTML 实体转义**(`&quot;`/`&amp;`/`&lt;`),但浏览器的 `a.href` **不做实体还原**——拖拽成书签后存的就是实体文本,执行时即解析失败。修复:href 改为 **percent-encoding**(`CLIP_BRIDGE_DRAG_HREF`,属性里只剩 `%XX`,天然无需任何属性转义);Chrome 执行 `javascript:` URL 前会 percent 解码(实测通过)。浏览器 E2E 升级为**直接以编码 href 走 `location.href` 执行**(完全等价拖拽书签的执行路径)并断言浮层弹出;静态验收改为 `decodeURIComponent(href) === 代码常量` 一致性校验。行为不变——**已拖过旧版书签的用户需重新初始化 seed 后重新拖一次**(旧书签仍然坏的,因为里面的代码是实体文本)。
445
- - **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 异常)。
446
- - **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 文档)修复。
447
- - **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 项全过)。
448
-
449
- - **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。
450
-
451
- - **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 自动跳过。**更新后刷新页面**即可看到入口。
452
- - **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]`)。
453
- - **v0.16.21**(2026-09-09):**新:DSH 右侧边栏(rightbar)集成**。注册 TiddlyWiki 为右侧栏 tab 类型——rightbar 首页出现「**TiddlyWiki 知识库**」入口盒,点击即在右侧栏以 tab 形式打开完整 TW 编辑器(同源代理 iframe,跟随 DSH 主题),**与聊天并排**;配置 `ui.showRightbarTab`(默认 true)可开关。**更新后刷新页面**即可看到入口。
454
- - **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**。
455
- - **v0.16.19**(2026-09-07):**修:会话「知识库」Tab 把汇总当源码显示**——TW 5.4.1 把 `$:/temp/` 条目一律按代码块渲染;改为 `/tw/render` 原生片段管线(与回复流工具卡同链路),不再用 iframe/story view。
456
- - **v0.16.18**(2026-09-07):README 全面精简重写;seed 优化——首页 seed 跟随 wiki 现状、修「所有文章」回主页死链、doc-note 文案修正。
457
- - **v0.16.17**(2026-09-07):点击快速笔记可选直达 TW 原生编辑页(`ui.quickNoteMode`)+ 修「在 TW 中编辑」弹窗 ✕ 关不掉。
458
- - **v0.16.16**(2026-09-07):修会话知识库 Tab 误入编辑草稿显示源码——三层防误编辑(清残留草稿/吞草稿创建/禁 ✏️)。
459
- - **v0.16.15**(2026-09-07):`put`/`batch_put` 未指定类型自动默认 `text/markdown`;警告 `fields.type` 是内容类型保留字段。
460
- - **v0.16.14**(2026-09-07):修知识库 Tab「佚失条目」根因——浏览器端 TW 天生排除 `$:/temp`,改为 iframe 就绪后注入 + 原生 hash 导航。
461
- - **v0.16.11**(2026-09-06):新增会话顶部「知识库」Tab(本会话相关 wiki 汇总,volatile `$:/temp`)。
462
- - **v0.16.0**(2026-09-05):回复流原生 TW 工具卡片 + 可点击 wiki 链接(新增 `render-route` seed)。
463
- - **v0.15.0**(2026-09-05):初始化不强制 + 可反初始化(seed 拆核心/可选两层)。
464
- - **v0.14.0**(2026-09-05):「发送给 Agent」按钮独立图标 + 附加说明 + 权限选择。
465
- - **v0.13.0**(2026-09-04):menubar 顶栏主题自适应。
466
- - **v0.12.0**(2026-09-04):新默认主页 + 「所有文章」两列分页页。
467
- - **v0.11.0**(2026-09-04):「发送给 Agent」支持选择工作模式(Agent 预设)。
468
- - **v0.10.0**(2026-09-04):统一 seed 注册表 + 设置页「初始化」。
469
- - **v0.8.0**(2026-09-04):嵌入式 TW 跟随 DSH 深浅主题。
470
- - **v0.7.0**(2026-09-03):TW 一键把当前笔记发送到 dsh 会话。
471
- - **v0.6.0**(2026-09-03):同源代理(远程访问核心)。
472
- - **v0.5.0**(2026-09-03):Agent 工具扩充 + 快速笔记草稿自动保存 + 统一知识库 FAB。
473
-
474
- ---
475
-
476
- ## 📦 发布
477
-
478
- ```bash
479
- npm publish # 版本号在 package.json;文件白名单见 files 字段
480
- ```
481
-
482
- `tiddlywiki` 依赖体较大(含全部语言包/插件)。
483
-
484
- ---
485
-
486
- ## 🔗 仓库与发布元数据
487
-
488
- - **GitHub**:https://github.com/bbqisbbq/dsh-tiddlywiki
489
- - **npm**:`dsh-tiddlywiki`(https://www.npmjs.com/package/dsh-tiddlywiki)
490
- - 说明笔记、首页、示例文档、样式等预置内容由 seed 机制写入 wiki(见 [🧩 初始化](#-初始化一次性预置-seed哪些必备哪些可有可无))
491
- - MIT;Node ≥ 22;GitHub topics:`dsh` `dsh-plugin` `tiddlywiki` `knowledge-base` `note-taking` `git-sync` `agent-tools` 等
1
+ # dsh-tiddlywiki
2
+
3
+ > 把 **TiddlyWiki 5** 变成你和 Agent 之间的**文档中心**:一个嵌在 DSH 里、人手一个的私人知识库——Agent 用 `tiddlywiki_*` 工具读写,你在界面里用完整 TW 编辑器记录,一切通过 **git** 自动同步备份。
4
+
5
+ [![npm](https://img.shields.io/npm/v/dsh-tiddlywiki)](https://www.npmjs.com/package/dsh-tiddlywiki)
6
+ [![license](https://img.shields.io/npm/l/dsh-tiddlywiki)](https://github.com/bbqisbbq/dsh-tiddlywiki/blob/main/LICENSE)
7
+ [![GitHub](https://img.shields.io/github/stars/bbqisbbq/dsh-tiddlywiki)](https://github.com/bbqisbbq/dsh-tiddlywiki)
8
+
9
+ ---
10
+
11
+ ## 🗂 它是什么:你的「文档 × Agent」一站式文档中心
12
+
13
+ 这个插件**首先不是一个"AI 记忆工具"**(虽然它顺带就是)。它把一件事做顺了——**文档和人、文档和 Agent 之间的协作**:
14
+
15
+ - **TiddlyWiki 天生适合当文档中心**:卡片化组织(标签 / 双向链接 / 一键成文 / 一个文件夹就是整个 wiki)、纯文本 git 仓库天然可迁移可备份。它能是你的**私人笔记本**、**GTD 工具**、**Agent 的记忆库**,甚至让 AI 帮你把电子书导入 TW 来阅读批注——**TW + DSH 能做到的唯一限制,只是人类的想象力**。
16
+ - **人会参与**:完整 TW 5 编辑器内嵌在 DSH 里,随时翻、随时改、随时批注。文档不是 Agent 写给你的黑盒,而是**方便人类参与的文档中心**。
17
+ - **Agent 也能进**:Agent 用 10 个 `tiddlywiki_*` 工具读写;TW 里「发送给 Agent」把笔记一键注入某个会话;每个会话顶部自动汇总它读写过的 wiki 笔记。**切换会话、方案终结、多轮合作,成果最终都回到文档里**——文档成为跨会话、跨 Agent 的公共底座。
18
+ - **AI 填平 TiddlyWiki 的门槛**:TW 功能强大但上手有门槛,这正是 AI 擅长的——让 Agent 按你的习惯定制首页、标签体系、样式与工作流。曾经要折腾 Obsidian / Logseq 插件体系才能搭出来的个人知识库,现在**一句话就能初始化一整套开箱即用的文档中心**。
19
+
20
+ > Obsidian、Logseq 也能做到其中一部分,但**从没有像这样一站式地顺畅**:编辑器、工具、会话、同步、主题全部在同一个界面里闭环。
21
+
22
+ ## ✨ 特性一览
23
+
24
+ | 能力 | 说明 |
25
+ |---|---|
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) |
29
+ | 🎨 **自定义样式** | 「自定义样式」seed:编辑器美化 / 窄屏侧栏隐藏 / menubar 加高 / 批注弹窗等 5 张通用样式表,新 wiki 也能一键初始化(可选,v0.16.22) |
30
+ | 📝 **可配置的注入提示词** | 插件注入每个会话的「TiddlyWiki 持久知识库」提示词可在设置页配置:**默认精简版**(~1.7KB,只保留工具 schema 表达不了的约定——同步纪律 / 标签约定 / 链接格式),可选**完整版**(额外附一份**由工具注册表实时生成**的参数索引,不会再过期);`extra` 追加自定义规范、`override` 整段接管、可整体停用;**保存后无需重启 dsh web**(section 即时重注册,当前会话下一步即生效),设置页可**按表单当前值预览**即将注入的全文(未保存的形态切换也立刻可见,v0.21.0 / v0.22.7) |
31
+ | 🤖 **Agent 工具** | 15 个 `tiddlywiki_*` 工具:检索(**相关度排序 + 命中处片段 + 字段过滤**)、读写、**增量追加**、批量、重命名、**软删除/回收站**、**反向链接**、**附件入库**、**知识库体检**、git 同步与冲突解决(v0.19.0;检索/最近仍在**服务端**排除二进制附件,大 wiki 上从 515MB/17s 降到 ~0.4s) |
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) |
33
+ | 🕒 **时间戳不再丢** | 插件写入的每条笔记都会带上 TW 的 `created`/`modified`(17 位紧凑 UTC,与 TW 编辑器逐字节一致):新建 = 两者都取当前时刻,覆盖 = **保留原 `created`、刷新 `modified`**。此前这两个字段被当成「TW 服务端会补」而丢弃,而服务端**从不补**——缺 `modified` 的条目会被 TW 的 `sortTiddlers` 当空串,在 `+[!sort[modified]]` 页面上**直接沉到最后一名**(表现为「新日记没被收录」)。`TiddlyWebClient.put()` 还有一道兜底,任何写路径都不会写出无时间戳的条目(v0.22.5) |
34
+ | 🧼 **渲染片段净化** | 回复流卡片与会话汇总注入的 TW 片段先经 **host 白名单净化**(丢 `iframe`/`script`/`svg`/`on*`/`javascript:`/`data:text/html` 等)——TW 自己的解析器只剥 `on*`,`<iframe src="javascript:…">` 会原样通过并在 DSH 页面里执行(v0.19.1 修复的存储型 XSS) |
35
+ | 🔒 **写路由方法校验** | 每个写路由只接受自己的 HTTP 方法:跨站 `GET /sync`、`GET /restart`、`GET /upload` 一律 405 且无副作用(v0.19.0 修复了「任意网页一张 `<img>` 即可触发 pull/commit/push」的 CSRF 面) |
36
+ | 🔑 **密钥不外泄** | `bridge.token` / `ui.sendToAgent.token` / **`auth.password`**(v0.20.0)在 `/admin/state`、`/admin/config` 的回包里是 `********`(设置页原样保存 ≠ 覆盖,清空即删除),`git.remote` 里的 PAT 打码;**`/tw`、`/api` 与 `POST /render` 都拒绝 `$:/plugins/dsh-tiddlywiki/` 命名空间**——此前 `/render` 能把配置 tiddler 连 token 与 PAT 一起渲染出来(v0.19.3 / v0.20.0) |
37
+ | 🧯 **路由不会拖垮进程** | 所有路由经 `guardHandler` 包装:任何 rejection(含代理里 try 之外的 `new URL()`)都变成 413/500 响应,而不是宿主未处理的 promise rejection(那会**直接结束 dsh web 进程**并挂死请求);剪藏桥 listen 后保留常驻 `error` 监听(v0.19.3) |
38
+ | 📊 **回复流卡片** | 工具结果显示原生 TW 卡片(**按笔记自己的内容类型渲染**:Markdown 笔记就是 Markdown,v0.18.0),检索/最近列表带**命中处摘要**(v0.22.8);`[标题](/dsh-tiddlywiki/tw/#标题)` 点击直达 TW 面板 |
39
+ | 📤 **发送给 Agent** | TW 笔记工具栏一键把当前笔记注入所选 dsh 会话(可选工作模式/权限/附加说明);成功/失败会弹出提示(v0.20.0 修复:此前提示把自由文本当 tiddler 标题传给 TW notifier,全部静默) |
40
+ | 📮 **发布到微信公众号**(**可选,默认关**) | 把笔记一键发到公众号**草稿箱**(可选点发表):TW 渲染 → 补内联样式 → 浏览器自动化复用你已登录的后台会话。**绕开官方 API 权限封锁**(2025-07 起个人主体账号的发布接口被回收),个人号可用;发表需管理员扫一次码。**需额外安装**(opencli + 浏览器扩展),插件不替你装;**关闭时完全不打扰**(不注入提示词、不写文档)。**带发布元数据**(`pub-state`/`pub-platform`/`pub-wechat-*` + `no-publish` 标签)避免重发或误发。**v0.23.3 起 TW 工具栏有「发布到公众号」按钮**(预检 + 进度轮询,只到草稿箱),也可继续用 CLI。见 [docs/wechat-publish-setup.md](docs/wechat-publish-setup.md) |
41
+ | 🧭 **内嵌编辑器** | 中央列内嵌完整 TW 5 编辑器(同源代理,Tailscale/内网/域名/HTTPS 均可) |
42
+ | 🗂️ **右侧边栏 Tab** | DSH 新右侧栏(rightbar):首页「TiddlyWiki 知识库」入口一键打开,与聊天并排;链接点击可直达(v0.16.21) |
43
+ | 🧪 **审计守门** | 第四轮审计(v0.20.0)把 CI 与 `npm run verify:*` 合成一份清单,并补上 `verify-constants`(filter 长度预算)、`/render` 403、auth 打码、notify 与草稿避让回归 |
44
+ | 🧩 **Better Sidebar 共存** | 与 dsh-better-sidebar 侧边栏共存(其展开/收起按钮浮在 TW 面板之上);**不再向该侧边栏注册 TW tab**(v0.17.0 移除了 tab 注册,避免 tab kind 冲突) |
45
+ | 📚 **会话知识库 Tab** | 每个会话顶部汇总本会话读写过的 wiki 笔记,TW 原生渲染(host `/render` 片段管线 + 白名单净化,v0.16.19 / v0.19.1) |
46
+ | 📝 **快速笔记** | 输入框上方快捷按钮或右下角「知识库」FAB;原生编辑页或 Markdown 卡片两种模式;首页内置「快速记笔记(完整编辑器)」 |
47
+ | 📌 **本地剪藏桥** | 可选「剪藏桥 + 书签小工具」:DSH 监听 127.0.0.1 端口接收剪藏请求(Host 校验防 rebinding),点书签弹出浮层——可改标题/编辑选中文字/**勾选图片**,一键写入知识库;文字成笔记(默认 `clip` 标签),图片由桥下载存为**二进制附件**(`type: image/*` + base64,笔记内 `[img[标题]]` 内嵌),随 wiki 自动进 git;图片下载有 **SSRF 守卫**(仅公网 http(s)、逐跳校验重定向,v0.18.0) |
48
+ | 🔐 **写操作防跨站** | 所有写路由(笔记/上传/同步/后台/代理)拒绝跨站请求(403),仅同源的 GUI 与内嵌 TW 可写;跨站 GET 与浏览器地址栏直达不受影响(v0.18.0) |
49
+ | ✅ **待办四象限** | 首页看板:任务打 `todo` 标签即收录,拖动即可分类/完成(正文附 `q` 字段),逾期/今日到期自动统计 |
50
+ | 🌗 **跟随主题** | 内嵌 TW 自适应 DSH 深浅主题(纯内存切换,不进 git) |
51
+ | 🔄 **一键同步** | FAB「同步」一键 pull→commit→push,状态点实时反映 git 状态 |
52
+ | 💾 **数据即备份** | wiki 文件夹本身就是 git 仓库,自动 commit(60s 防抖) |
53
+
54
+ ---
55
+
56
+ ## 📦 安装
57
+
58
+ ```bash
59
+ # ① npm 发布包(推荐)
60
+ dsh plugin --profile web add dsh-tiddlywiki
61
+
62
+ # ② 直接从 GitHub 安装
63
+ dsh plugin --profile web add github:bbqisbbq/dsh-tiddlywiki
64
+
65
+ # ③ 本地开发(link 方式,改完 src 后 npm run build 即生效)
66
+ dsh plugin --profile web add link:/path/to/dsh-tiddlywiki
67
+ ```
68
+
69
+ 装完**重启 dsh web** 生效。首次启动自动完成:初始化 wiki 目录、`git init` 并提交基线、写入**功能必需的 seed**(发送给 Agent 按钮 + 原生渲染路由 + 同源代理基址)与**起步文档**(插件说明 + 示例与文档);首页/所有文章/自定义样式等为可选项,需要时在设置页「初始化」手动写入。
70
+
71
+ ---
72
+
73
+ ## 🚀 快速开始
74
+
75
+ 1. **安装并重启** dsh web。
76
+ 2. 侧边栏「**TiddlyWiki**」→ 中央打开完整 TW 编辑器。
77
+ 3. **随手记**:点输入框上方「📝」或右下角「知识库」→「快速笔记」;或在首页直接用「✍️ 快速记笔记」区块,写完保存即成为 tiddler 并自动进 git。
78
+ 4. **看文档**:新 wiki 首页(初始化 home-index 后)有「**📚 插件文档**」栏——插件说明、汇总教程、模板都在里面,可自由删改。
79
+ 5. **收工同步**:「知识库」→「🔁 同步」一键 push 备份。
80
+ 6. **让 Agent 参与**:直接说「把刚才的会议纪要存进知识库」——Agent 用 `tiddlywiki_*` 工具读写。
81
+ 7. **发笔记给 Agent**:TW 里打开笔记 → 工具栏「发送给 Agent」→ 选会话 → 作为消息注入。
82
+
83
+ ---
84
+
85
+ ## 📖 使用指南
86
+
87
+ ### 🤖 Agent 工具(15 个)
88
+
89
+ | 工具 | 说明 |
90
+ |---|---|
91
+ | `tiddlywiki_search` | 检索(`query` + 可选 `tags[]/tag/since/type/field/value/limit`),**按相关度排序**(标题命中 > 标签 > 正文命中次数),摘要取自**命中处上下文**而不是正文开头 |
92
+ | `tiddlywiki_recent` | 最近修改的笔记(倒序,支持 `limit`/`since`),开工快速了解动态 |
93
+ | `tiddlywiki_list_tags` | 现有非系统 tag 及计数(已排除只挂在二进制附件上的 tag);默认列使用最多的 200 个(`limit` 可调、上限 1000),截断时返回 `total`/`truncated` |
94
+ | `tiddlywiki_get` | 读单个 tiddler 全文(`modified` 以 ISO 返回,可直接用作 `expectedModified`) |
95
+ | `tiddlywiki_put` | 写/覆盖;**新建**未指定类型时默认 `text/markdown`(`$:/` 系统条目除外),**覆盖既有条目时保留原 `type`/tags/自定义字段**(v0.20.1,改类型要显式 `fields.type`);`expectedModified` + `force` 提供乐观并发保护 |
96
+ | `tiddlywiki_batch_put` | 批量写入(`overwrite=false` 跳过已存在;单条失败不影响其余、逐条报错;4 路并发但结果保持入参顺序) |
97
+ | `tiddlywiki_append` | **增量追加**:`mode=append\|prepend`、`heading=某标题` 定位段落,写日志/批注无需读全文;与 `put` 同一套写策略(保留原 `type`/tags/自定义字段,新条目才默认 markdown),支持 `fields` 显式覆盖 |
98
+ | `tiddlywiki_rename` | 重命名 + 尽量同步其他条目里的引用;旧标题删除失败时如实回报「两份副本都在」 |
99
+ | `tiddlywiki_delete` | 删除(幂等)。默认**软删除**进 `$:/dsh-tiddlywiki/trash/`,`permanent=true` 才真删;支持 `expectedModified`/`expectedRevision`/`force` 乐观并发(读后被改动则拒绝,不把人类的新改动丢进回收站) |
100
+ | `tiddlywiki_trash` | 回收站:`action=list\|restore\|empty`(索引读不到或损坏时显式报错,绝不把回收站当空的重建) |
101
+ | `tiddlywiki_backlinks` | 反向链接:谁用 `[[标题]]`/`{{标题}}` 引用了它、谁把它当标签 |
102
+ | `tiddlywiki_attach` | 把**本机文件或公网 http(s) 地址**存成二进制附件(图片/PDF/…),可嵌入某篇笔记;URL 走 SSRF 守卫;**同名 tiddler 已存在时默认拒绝**(避免静默覆盖笔记),确认覆盖要传 `force: true`(tags/自定义字段仍保留) |
103
+ | `tiddlywiki_lint` | 知识库体检:垃圾标签 / 死链 / 空笔记 / 缺内容类型的类 Markdown 笔记 |
104
+ | `tiddlywiki_git_sync` | `action: pull\|push\|sync` |
105
+ | `tiddlywiki_git_resolve` | pull 冲突后按 tiddler 二选一(`keep-local\|keep-remote`) |
106
+
107
+ **知识库同步纪律**:
108
+ 1. 开工先 `tiddlywiki_git_sync action=pull`(rebase + autostash,真冲突会 abort 并报文件)。
109
+ 2. 冲突后用 `tiddlywiki_git_resolve` 二选一解决,再重新 sync(**绝不自动覆盖**)。
110
+ 3. 收工 `tiddlywiki_git_sync action=sync`(pull → commit → push)。
111
+
112
+ > 🔍 **检索/最近跳过二进制附件**(v0.16.20):`search`/`recent` 在**服务端**只取文本 tiddler(无 `type` 字段或 `text/*`),图片/音频/视频/PDF/zip 等二进制 tiddler(base64 正文)不参与检索、不出现在结果里——大 wiki(如数千张书籍扫描页图)从此从 515MB/17s 降到 ~0.4s,也不会被同名书页图刷屏。`get` 对二进制 tiddler 只返回元数据(`binary=true` + 类型/大小 + 链接),不返回 base64 正文。
113
+
114
+ > ⚠️ `fields.type` 是 TW 的**内容类型保留字段**(`text/markdown` 等),业务分类请放 `tags`,别写进 `fields.type`。
115
+
116
+ ### 🧠 注入的系统提示词(可配置)
117
+
118
+ 插件会往每个会话的系统提示词里注入一段「TiddlyWiki 持久知识库」约定(**设置页 → 系统提示词**):
119
+
120
+ | 配置 | 作用 |
121
+ |---|---|
122
+ | `prompt.enabled` | 关掉后本插件不注入任何文本 |
123
+ | `prompt.mode` | `slim`(**默认**):只保留工具 schema 表达不了的约定(写入/并发纪律、同步纪律、标签约定、可点击链接格式);`full`:额外附一份**参数索引**,由工具注册表在运行时生成,因此永远不会与真实工具脱节 |
124
+ | `prompt.extra` | 追加在末尾的自定义规范(团队 / 个人偏好),始终生效 |
125
+ | `prompt.override` | 非空时整段取代内置文本(`extra` 仍会追加)——想完全自写提示词时用 |
126
+
127
+ 保存后**不用重启 dsh web**:host 会即时重新注册 prompt section,当前会话从**下一步**起就使用新文本(DSH 的 `system-prompt/change` 会更新历史里的系统消息)。设置页的「预览注入文本(按表单当前值)」按钮会把**表单里此刻的值**(含还没保存的形态切换 / extra / override / 停用开关)发给 host 实时拼成全文——所见即「保存后会注入的内容」,顶部还会标注「含未保存的修改」;预览是只读的,不会替你保存(v0.22.7 之前它只读**已保存**的配置,所以切换形态后不点保存直接预览会看到逐字节相同的旧文本,容易被读成「两种形态没差别」)。
128
+
129
+ > 为什么默认精简: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 的每个工具与每个参数都必须在场 / 两种形态都必须保留治理约定块)。
130
+
131
+ ### 🧑‍💻 界面操作
132
+
133
+ - **📤 发送给 Agent**:TW 工具栏按钮(首次启动自动写入 wiki,ONE-SHOT)。弹层可选**附加说明**(位于消息末尾)、**工作模式**(Agent 预设)、**权限**(权限预设),按工作区分组选会话或新建。消息自带待办说明。
134
+ - **🧭 中央列编辑器**:侧边栏「TiddlyWiki」开关(显示名可改 `ui.sidebarLabel`,保存后即时生效,v0.22.8)。
135
+ - **🗂️ 右侧边栏 Tab**(v0.16.21):DSH 新右侧栏展开后,首页会出现「**TiddlyWiki 知识库**」入口盒,点击即在右侧栏以 tab 形式打开完整 TW 编辑器——**与聊天并排**,适合边聊边查/边记。由 `ui.showRightbarTab` 控制(默认开);老版本 DSH(无右侧栏)自动跳过。
136
+ - **🧩 Better Sidebar 共存**:装了 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 时,插件只做 UI 共存(中央 TW 面板的 z-index 自动压在其侧边栏展开/收起按钮之下,按钮始终可点)。**v0.17.0 起不再向 dsh-better-sidebar 注册「TiddlyWiki 知识库」tab**(旧版可用 `ui.showBetterSidebarTab` 关闭)——该 tab 的 kind 会与其它注册方冲突报 `tab kind "dsh-tiddlywiki" is already registered`。右侧边栏入口请用上面的 **右侧边栏 Tab**(DSH 原生 rightbar)。
137
+ - **📝 快速笔记**:输入框上方快捷按钮(`ui.showQuickNoteDock`)或 FAB;`ui.quickNoteMode` 选打开方式——**native**(默认,直达 TW 原生编辑页,草稿自动续写)或 **card**(CodeMirror 6 Markdown 高亮、文件上传、多选 tag、草稿自动保存、「🕘 最近」载入、Ctrl+Enter 保存)。**v0.22.6**:原生编辑弹窗里用 TW 的「🗑 删除」把笔记删掉后不会再变成打不开的白板——再点一次「快速笔记」就会重新载入编辑器;card 模式底部操作条改成「按钮文字永不折行、放不下时整组换行」,窄卡片里不再把「✏️ 在 TW 中编辑」压成两行。
138
+ - **📌 本地剪藏桥**(可选,v0.16.24+/v0.16.25 支持图片):DSH 设置 → 常规配置 → 勾选「**启用本地剪藏桥**」(保存后立即生效)——DSH 即监听 `127.0.0.1:8618` 接收剪藏请求。浏览器书签栏新建书签,把知识库文档里的 JS 代码粘贴为地址,点一下弹出**浮层**:确认/修改标题与划线文字、勾选页面图片(封面自动标出)→「剪藏」即写入 wiki。文字成笔记(默认 `clip` 标签、正文含来源与选中文字、重名自动 `(2)` 去重);**图片由 DSH 本机下载字节存为二进制附件**(`clip-url`/`clip-note` 溯源字段,笔记内 `[img[标题]]` 内嵌展示;某张下载失败自动降级为链接),全部随 wiki 自动 git commit。「📚 插件文档」栏的 `本地剪藏桥(书签小工具)` seed 文档含完整步骤、书签代码、安全说明与 curl 用法。安全:桥只监听 127.0.0.1 + Host 白名单防 DNS rebinding;**强烈建议设 `bridge.token`**(非空时校验书签的 `x-clip-token` 头,防止任意网页往 wiki 里塞内容)。图片附件按设计不参与 `search`/`recent`(避免刷屏),`get` 只回元数据。
139
+ - **🏠 首页(初始化 home-index 后)**:待办四象限(`todo` 标签 + `q` 字段拖放分类)+ 快速记笔记(完整编辑器,勾选「同时加入待办」即建任务)+「所有标签 / 所有文章」入口 +「📚 插件文档」栏(自动收录所有带 `dsh-docs` 标签的 seed 文档)。
140
+ - **📚 会话知识库 Tab**:会话顶部 Tab(`ui.tabLabel` 改名、`ui.showSessionTab` 关闭),自动汇总本会话读写过的 wiki 笔记(写入 volatile `$:/temp`,不落盘不进 git),**TW 原生渲染**(v0.16.19 起 `/tw/render` 片段管线,与回复流工具卡同链路),链接点击直达中央 TW 面板,不可编辑。
141
+ - **🌗 跟随 DSH 主题**:内嵌 TW 随 DSH 深浅切换 palette,纯内存不写回 wiki(`ui.followDshTheme`/`ui.darkPalette`)。
142
+ - **🔧 知识库 FAB**:统一入口(TW 面板开关/重载、快速笔记、同步、TW 服务状态悬停 tip)。同步拉取到新内容会自动重启 TW(同端口)。
143
+ - **⚙️ 设置页**:DSH 设置 →「TiddlyWiki 知识库」:状态/重启、**知识库位置(可切换)**、常规配置、插件/主题/语言管理、**初始化**(seed 状态与重新初始化)。配置写入 `$:/plugins/dsh-tiddlywiki/config` tiddler,覆盖 cordis `config:` 块(tiddler 优先)。
144
+
145
+ ### 📍 知识库位置(可切换,v0.22.0)
146
+
147
+ 设置页顶部「**知识库位置(可切换)**」区块显示当前实际服务的文件夹、这个位置**是怎么决定的**(指针文件 / cordis 配置 / 默认值)、指针文件路径,以及**同目录下其它看起来像 wiki 的文件夹**(含 `tiddlywiki.info`)的快捷填入按钮。
148
+
149
+ - **换一个位置**:填根目录(绝对路径,支持 `$DSH_HOME` / `${VAR}` / `%VAR%`)+ 文件夹名 → 「切换到这个位置」。host 会停掉 TW → 释放自动提交与监听 → 指向新目录 → 起 TW → 重载新 wiki 的配置 tiddler → 跑核心 seed(markdown 插件 / 发送给 Agent / 渲染路由 / 同源代理 / 语言)→ 重新武装自动提交;**选中即被记住**(写进 `$DSH_HOME/dsh-tiddlywiki/location.json`),重启 dsh web 后仍在新知识库。
150
+ - **新建一个知识库**:目标文件夹若还没有 `tiddlywiki.info`,插件会照常自动 `--init server` 建一个全新的(**别填已有的普通笔记目录**——它会被初始化)。
151
+ - **恢复为配置默认**:删除指针文件并(必要时)切回 `config.wikiRoot` / `config.wiki`。
152
+ - **安全语义**:指针文件在任何 wiki **之外**(若存在 wiki 自己的配置 tiddler 里,切到新 wiki 就会把这个选择一起丢掉——鸡生蛋);指针文件损坏/非法时**明确报告并回退到配置默认**,不会静默乱跑;切换**失败会回滚**到原来的知识库,并告诉你是「已回滚」还是「回滚也失败了」;同刻只允许一个切换(并发请求直接拒绝)。
153
+ - **注意**:切换期间正在跑的 Agent 工具调用会失败(TW 在重启),界面上按钮会禁用并显示「切换中…(重启 TW)」。
154
+
155
+ `wikiRoot` / `wiki` 现在只是**默认值**——运行中的实际位置以指针文件优先。这也是为什么它能不改 cordis、不重装就切。
156
+
157
+ ### 📮 发布到微信公众号(**可选功能,默认关闭**,2026-09-17)
158
+
159
+ > **这是可选功能**:需要额外安装 opencli + Browser Bridge 浏览器扩展,**插件不替你装**。
160
+ > 开关 `wechat.enabled` **默认 `false`**——关闭时不注入任何发布相关提示词、也不往 wiki 写
161
+ > 「发布元数据规范」。开启:DSH 设置 →「TiddlyWiki 知识库」→「可选功能:微信公众号发布」。
162
+ > 完整安装步骤与排错见 [docs/wechat-publish-setup.md](docs/wechat-publish-setup.md)。
163
+
164
+ 把 wiki 里的任意笔记**一键发到微信公众号草稿箱**(可选直接发表)。整套能力放在 `tools/wechat/`,**不依赖公众号服务端 API**——因为 2025-07 起官方已回收个人主体账号的「发布能力」接口权限;本方案改用**浏览器自动化复用你已登录的后台会话**,所以个人号也能用。
165
+
166
+ **两条路,随便挑一条**:
167
+
168
+ - **点按钮(v0.23.3,日常推荐)**:开启可选功能后,笔记工具栏出现「**发布到公众号**」。点它 → 先**预检**(opencli 在不在、adapter 缺不缺)→ 弹确认框(自动列出 `no-publish` / `pub-state` 警告)→「开始存草稿」→ 覆盖层每 2 秒显示进度与日志。**只到草稿箱为止**,发表请到后台点(需扫码)。背后是宿主进程起一个单并发的后台任务(`POST /dsh-tiddlywiki/wechat/publish` + 轮询 `…/status`),标题经 **UTF-8 文件**(`--title-file`)传给 adapter——不进 argv,避免 Windows `cmd.exe` shim 把 `&` 当命令分隔符、把中文解成乱码。
169
+ - **敲命令**(等价,适合批量 / 脚本化):
170
+
171
+ ```bash
172
+ # ① 先按 docs/wechat-publish-setup.md 装好 opencli + 浏览器扩展,并登录公众号
173
+ # ② 一次性:装 adapter 到本机 opencli(幂等,会自检扩展/登录状态)
174
+ node tools/wechat/install-wechat-adapters.mjs
175
+
176
+ # ③ 发布(注意必须带 --trace retain-on-failure,原因见下)
177
+ opencli weixin publish-note "笔记标题" --trace retain-on-failure -f json
178
+ opencli weixin publish-note "笔记标题" --cover ./cover.png -f json # 带封面
179
+ opencli weixin publish-note "笔记标题" --preview ./out -f json # 先导出排版预览
180
+ opencli weixin publish-note "笔记标题" --publish -f json # 直接发表(需管理员扫码)
181
+
182
+ # ④ 可选:回填存量「已发布」状态(默认 dry-run)
183
+ node tools/wechat/backfill-publish-state.mjs # 看
184
+ node tools/wechat/backfill-publish-state.mjs --write # 写
185
+ ```
186
+
187
+ **流程**:笔记标题 → DSH 的 `/render`(TW 自己渲染成语义 HTML,含代码高亮)→ `wechat-html.js` 补**内联样式**(微信会剥 `<style>` 和 class,只认内联)→ opencli 驱动后台填表/写正文/传图/设封面/存草稿 →(可选)点发表。
188
+
189
+ **三个要点**:
190
+
191
+ 1. **必须带 `--trace retain-on-failure`**——不带会对 `mp.weixin.qq.com` 稳定报 `Navigation rejected`(实测 trace 开 5/5 成功、关 8/8 失败;这是 opencli 1.8.7 的 bug,`--site-session ephemeral` 等绕法均无效)。
192
+ 2. **发表必须管理员扫码**——后台点「发表」后微信要求管理员微信扫码确认,无法自动化。「一键」的真实含义是「脚本做到填表/排版/上传,你只需扫一次码」。默认走**发表**(不推送粉丝、不占群发额度),群发请自行在后台操作。
193
+ 3. **图片上传用 DataTransfer 注入**,不用 `page.setFileInput`——后者依赖 CDP `Page.fileChooserOpened`,本机扩展版本组合下稳定失败;改用页面上下文直接塞 `input.files`,实测图片真进 `mmbiz.qpic.cn`。代价是单图 **8MB** 上限(字节要以 base64 穿过 evaluate)。
194
+
195
+ **换机器还原**见 [docs/wechat-publish-setup.md](docs/wechat-publish-setup.md)(含 opencli / Browser Bridge 扩展安装、扫码登录、排错表)。设计依据与全部实测细节见 [docs/plans/2026-09-17-wechat-publish-design.md](docs/plans/2026-09-17-wechat-publish-design.md)。
196
+
197
+ ### 🧩 初始化(一次性预置 seed):哪些「必备」,哪些「可有可无」
198
+
199
+ seed 是把「wiki 里预置内容」随插件分发的机制:**ONE-SHOT(只写缺失)+ 安全跳过(同名 tiddler 已存在绝不覆盖你的数据)**,需要时可「重新初始化」恢复、可「反初始化」移除。详细见 [docs/seed-initialization.md](docs/seed-initialization.md)。
200
+
201
+ | 层级 | seed | 说明 | 首次安装 |
202
+ |---|---|---|---|
203
+ | 🔒 **核心**(功能必需,不可移除) | `send-to-agent` | TW 工具栏「发送给 Agent」按钮插件 | 自动写 |
204
+ | | `render-route` | 原生渲染路由(回复流卡片 / 链接直达依赖) | 自动写 |
205
+ | | `tw-web-host` | TW 前端 API 基址 → 同源代理(内嵌编辑器前提) | 自动写 |
206
+ | 📖 **起步**(默认写、可移除,**想要完整体验建档案**) | `doc-note` | 「dsh-tiddlywiki 插件说明」笔记 | 自动写 |
207
+ | | `starter-docs` | 「示例与文档」:主题汇总页·模板 + 教程 + 三个示例主题页(日志/决策记录/排障) | 自动写 |
208
+ | | `publish-spec` / `wechat-setup` / `wechat-publish`(**带 gate**) | 只有开启「可选功能:微信公众号发布」才写:发布元数据规范 + 换机还原指南 + 「**发布到公众号**」工具栏按钮插件(v0.23.3) | 开启后自动写 |
209
+ | 🎀 **可选**(默认不写、设置页手动、可移除,**可有可无**) | `home-index` | 首页(四象限待办 + 快速记笔记 + 所有标签/所有文章 + 📚 文档栏)——文档中心的「门面」 | 手动 |
210
+ | | `all-articles` | 「所有文章」两列分页总览(🤖 Agent / 👤 人工) | 手动 |
211
+ | | `ui-styles` | 自定义样式 5 张(编辑器美化 / 窄屏侧栏 / menubar 加高 / 批注弹窗等) | 手动 |
212
+ | | `menubar-theme` | menubar 顶栏跟随 DSH 主题换色 | 手动 |
213
+ | | `clip-bridge` | 「本地剪藏桥(书签小工具)」使用说明——含书签代码 / 启用步骤 / 安全说明(真功能在插件运行时代码里,此 seed 只预置文档) | 手动 |
214
+
215
+ - **想获得完整插件体验**:核心 3 项首次安装就有;再补 `home-index`(首页)+ `starter-docs`(示例文档)即是一个开箱即用的文档中心。
216
+ - **一个可选项都不想要**:完全不影响功能——设置页「反初始化」即可,核心项受保护不可移除。
217
+ - **文档怎么扩散到更多**:以后插件新增的任何说明 / 教程 / 模板类内容都走 seed 并带 **`dsh-docs`** 标签——首页「📚 插件文档」栏自动收录,你无需任何配置。
218
+ - **内置内容更新了怎么办**(v0.22.0):seed 标记里记着**内容哈希**,设置页据此显示两个提示 chip——「**⬆ 有更新**」(内置内容比你 wiki 里预置的更新,可点「更新到内置版本」取用)与「**✏️ 本地已修改**」(这篇是你的内容,重新初始化会覆盖它,会先二次确认)。升级插件后旧 wiki 的标记没有哈希,会显示「更新检测尚未启用(重新初始化一次即可)」,**不会**把老标记误判成「你改过」;重新初始化一次即升级标记。**绝不自动改写你的 wiki**——更新只在你点的时候发生。
219
+
220
+ ---
221
+
222
+ ## 🛠 配置
223
+
224
+ 插件行默认配置(缺省即默认,无需手动配置):
225
+
226
+ ```yaml
227
+ - id: dsh-tiddlywiki
228
+ config:
229
+ wikiRoot: "$DSH_HOME/tiddlywiki" # 默认位置;运行中可被设置页「知识库位置」覆盖(指针文件优先,v0.22.0)
230
+ wiki: "main" # 文件夹名("." = 直接用 wikiRoot 这个目录)
231
+ port: 0 # 0 = 自动探测空闲端口
232
+ git:
233
+ autoCommit: true
234
+ debounceMs: 60000
235
+ remote: "" # 空 = 仅本地 commit;填了才 push
236
+ branch: "main"
237
+ note:
238
+ tag: "inbox" # 快速笔记默认 tag
239
+ startup:
240
+ readyTimeoutMs: 60000 # TW 启动就绪窗口(v0.22.5,5s–600s;超出只警告并继续等,硬上限 = 3×,仍未就绪才判失败)
241
+ prompt:
242
+ enabled: true # false = 本插件不注入任何提示词
243
+ mode: "slim" # slim(默认:只留约定)/ full(+ 由工具注册表实时生成的参数索引)
244
+ extra: "" # 追加在提示词末尾的自定义规范(团队/个人偏好)
245
+ override: "" # 非空时整段取代内置文本(extra 仍会追加)
246
+ bridge:
247
+ enabled: false # 本地剪藏桥(书签小工具);保存后立即生效
248
+ port: 8618 # 监听端口(127.0.0.1;改后需重启 dsh web 才绑定新端口)
249
+ token: "" # 共享口令;非空时校验书签的 x-clip-token 头(强烈建议设置)
250
+ tag: "clip" # 剪藏笔记默认 tag
251
+ ui:
252
+ showQuickNote: true # FAB 里显示快速笔记入口
253
+ showQuickNoteDock: true # 输入框上方快捷按钮
254
+ quickNoteMode: "native" # native=TW 原生编辑页 / card=Markdown 卡片
255
+ sidebarLabel: "TiddlyWiki" # 侧边栏入口显示名
256
+ showPanelStatus: true
257
+ showSyncButton: true
258
+ followDshTheme: true # 跟随 DSH 深浅主题
259
+ darkPalette: "$:/palettes/CupertinoDark"
260
+ tabLabel: "知识库" # 会话 Tab 名
261
+ showSessionTab: true
262
+ showRightbarTab: true # DSH 右侧边栏提供 TiddlyWiki 入口/Tab
263
+ sendToAgent: { enabled: true }
264
+ wechat: # 可选功能:微信公众号发布(默认关;三条 /wechat/* 路由在关时一律 403)
265
+ enabled: false # 开:注入发布约定 + 启动写 3 个 gated seed(元数据规范/换机指南/工具栏按钮)
266
+ adapter: "publish-note" # publish-note-imgs = 正文内嵌多图版(需该 adapter 已装)
267
+ command: "opencli" # CLI 路径(不在 PATH / 用了别名时填绝对路径)
268
+ token: "" # 非空时 /wechat/* 要求请求头 x-wechat-publish-token(按钮自动带)
269
+ dsn: "" # adapter 回连 DSH 的基址;空 = 按请求端口推导 loopback
270
+ uiLanguage: "" # 留空不干预;"zh-Hans" 自动启用简体
271
+ auth:
272
+ username: "" # 默认 loopback 匿名;暴露到非回环才需要
273
+ password: "" # 非空时插件内置客户端/就绪探测/浏览器代理都带 Basic 认证(v0.18.0 起真正可用)
274
+ ```
275
+
276
+ > **运行时配置**:设置页写入的 `$:/plugins/dsh-tiddlywiki/config` tiddler 是 `config:` 块之上的覆盖层(tiddler 优先、随 wiki git 同步),改 note tag / git / ui 开关 / **注入提示词(`prompt.*`)** 都无需动 cordis;**提示词改动保存后立即生效**(section 即时重注册,当前会话下一步生效),剪藏桥端口改动仍需重启 dsh web 重新绑定监听;**`startup.readyTimeoutMs` 对下一次 TW 启动/重启生效**(v0.22.5)。
277
+ >
278
+ > **知识库位置是三层**(v0.22.0):指针文件 `$DSH_HOME/dsh-tiddlywiki/location.json` > `config:` 块的 `wikiRoot`/`wiki` > 内置默认(`$DSH_HOME/tiddlywiki` + `main`)。`wikiRoot`/`wiki` 因此是「默认位置」而不是「唯一位置」——设置页切过的位置存在指针文件里(一个在 wiki 之外的文件),所以要换回配置值就点「恢复为配置默认」。
279
+
280
+ ---
281
+
282
+ ## 🌐 远程访问
283
+
284
+ TW 子进程只监听 **127.0.0.1 回环**;Agent 工具/快速笔记/同步都走 DSH 宿主进程→回环 TW,不受访问入口影响。浏览器里的 TW 编辑器 iframe 经**同源代理** `<DSH origin>/dsh-tiddlywiki/tw/` 访问(v0.6.0 起),所以通过 Tailscale / 内网 / 域名 / HTTPS 访问 DSH 时编辑器照常工作,且 TW 重启不打断编辑中的内容。
285
+
286
+ ---
287
+
288
+ ## 👨‍💻 开发
289
+
290
+ 需要 **Node.js ≥ 22**。
291
+
292
+ ```bash
293
+ npm install
294
+ npm run typecheck # tsc --noEmit
295
+ npm run build # clean + host tsdown + client tsdown + wrap(wrap 会校验 id 与体积 <900KB)
296
+ npm run selftest # headless:spawn TW → REST 读写 → git → 退出回收
297
+ npm run smoke:client # client bundle 的 module-loader 形状冒烟
298
+ npm run verify # = verify:static + verify:unit + verify:e2e(本地一键;CI 同款分档)
299
+ npm run verify:large # 3000+ 条目大 wiki:检索耗时 / 二进制零出现 / 真跑 commit(约 1 分钟)
300
+ node scripts/verify-send-to-agent-bundle.mjs # bundle 字段 + 源件逐字一致
301
+ node scripts/verify-seed-send-to-agent.mjs # 全新 wiki 上的 seed E2E
302
+ node scripts/verify-clip-bridge.mjs # 剪藏桥 headless 验收(含 SSRF 守卫)
303
+ node scripts/verify-seeds-admin.mjs # /admin/seeds 状态与 run 的 E2E
304
+ node scripts/verify-prompt.mjs # 注入提示词守门(slim 无参数清单 / full 与工具注册表逐项一致 / 治理约定不丢)
305
+ node scripts/verify-wiki-switch.mjs # 运行时切换知识库 E2E(真起 TW:切换 / 回滚 / 指针文件 / 非法输入,v0.22.0)
306
+ ```
307
+
308
+ > 📦 从 **npm 包**安装的用户只有 `lib/` + `src/` + `docs/`(`scripts/` 不在发布包里,避免把构建脚本塞进依赖树)——想跑上面的验收脚本请用 git 仓库:`git clone https://github.com/bbqisbbq/dsh-tiddlywiki && npm install`。
309
+
310
+ **改 bundle/seed 的再生成流水线**(不要手改 `seed-*.ts` 里的生成常量;bundle 版本号只在 `scripts/bundle/versions.mjs` 定义一处):
311
+
312
+ ```bash
313
+ # 发送给 Agent 按钮(改 scripts/bundle/send-to-agent/ 后)
314
+ node scripts/build-send-to-agent-bundle.mjs
315
+ node scripts/gen-seed-send-to-agent.mjs scripts/bundle/send-to-agent.bundle.json src/host/seed-send-to-agent.ts
316
+ node scripts/verify-send-to-agent-bundle.mjs
317
+
318
+ # 渲染路由(改 scripts/bundle/render/server-routes/render.js 后;版本在 versions.mjs)
319
+ node scripts/build-render-bundle.mjs
320
+ node scripts/gen-seed-render.mjs scripts/bundle/render.bundle.json src/host/seed-render.ts
321
+
322
+ # 首页(改 wiki 的 🏠 主页/所有标签/标签笔记 .tid 后;默认就剥离作者私有人口 +
323
+ # 注入「📚 插件文档」栏,确要保留私有内容才加 --keep-private)
324
+ node scripts/gen-seed-home.mjs '<wiki>/tiddlers/🏠 主页.tid' '<wiki>/tiddlers/所有标签.tid' '<wiki>/tiddlers/标签笔记.tid' src/host/seed-home.ts
325
+
326
+ # 自定义样式(改 wiki 的样式 .css + .meta 后;tag 自动收窄为 $:/tags/Stylesheet)
327
+ node scripts/gen-seed-ui-styles.mjs '<wiki>/tiddlers/<样式.css>' … src/host/seed-ui-styles.ts
328
+
329
+ # 示例与文档(starter-docs)/ menubar 主题:直接维护 src/host/seed-starter-docs.ts / seed-menubar-theme.ts
330
+
331
+ # 随后 npm run build
332
+ ```
333
+
334
+ **产物约定**:`lib/` 内**零** `@deepseek-ai` 运行时 import;client 必须 minify(否则 >1MB 被注册表校验拒绝);`react`/`tiddlywiki` 不打包(运行时解析)。
335
+
336
+ ### 路由参考(开发者)
337
+
338
+ | 路由 | 方法 | 用途 |
339
+ |---|---|---|
340
+ | `/dsh-tiddlywiki/status` | GET | 面板健康(service/url/git/tag/ui) |
341
+ | `/dsh-tiddlywiki/note` `/edit` `/tags` `/recent` `/get` `/search` | POST/GET | 快速笔记、打开编辑器、tag/最近/单个/搜索(列表路由都支持 `limit`;`/tags` 另支持 `sort=alpha\|count` 与 `total`/`truncated`,v0.19.4) |
342
+ | `/dsh-tiddlywiki/render` | POST | TW 片段渲染(host 净化后返回,回复流卡片/会话汇总用,v0.19.1) |
343
+ | `/dsh-tiddlywiki/sync` `/upload` `/restart` | POST | 一键同步、文件上传、重启 TW |
344
+ | `/dsh-tiddlywiki/session/summary` | POST | 会话「知识库」Tab 汇总 |
345
+ | `/dsh-tiddlywiki/agent/sessions` `/modes` `/send` `/create` | GET/POST | TW「发送给 Agent」:会话/模式/发送/新建 |
346
+ | `/dsh-tiddlywiki/api/*` | any | 透传 TW 服务(JSON) |
347
+ | `/dsh-tiddlywiki/tw/*` | any | 同源 TW 代理(远程访问核心) |
348
+ | `/dsh-tiddlywiki/admin/seeds` `/run` `/remove` | GET/POST | seed 状态(v0.22.0 起含内容哈希的「有更新 / 本地已修改」)/ 运行 / 反初始化 |
349
+ | `/dsh-tiddlywiki/admin/prompt` | GET/POST | 当前注入提示词全文(GET = 已保存的有效配置,v0.21.0);POST 带 `{enabled,mode,extra,override}` 则按**草稿**渲染、写入零副作用,供设置页预览未保存的表单值(v0.22.7) |
350
+ | `/dsh-tiddlywiki/admin/wiki/location` | GET | 当前知识库位置 + 来源(指针/配置/默认)+ 指针文件路径 + 同目录候选 wiki(v0.22.0) |
351
+ | `/dsh-tiddlywiki/admin/wiki/switch` `/reset` | POST | 运行时切换知识库 / 恢复为配置默认(失败自动回滚并报告,v0.22.0) |
352
+
353
+ ### 项目结构
354
+
355
+ ```
356
+ src/
357
+ ├── index.ts # host 入口:装配 WikiServer/路由/工具/提示词/自动 commit
358
+ ├── sdk.ts # 自包含 defineTool + dshHomePath(零 @deepseek-ai 运行时依赖)
359
+ ├── host/
360
+ │ ├── wiki.ts # WikiServer:spawn/kill/自愈/端口探测/就绪轮询(策略见 ready-policy.ts)+ 迟到就绪复探;TW_PROXY_PATH 同源代理
361
+ │ ├── ready-policy.ts # TW 启动就绪策略(v0.22.5):软窗口 60s(可配)/ 硬上限 3× / awaitReady 纯策略(注入时钟,可单测)
362
+ │ ├── tw-api.ts # TiddlyWeb REST 客户端
363
+ │ ├── git.ts # git init/commit/pull/push/sync/status + AutoCommitter
364
+ │ ├── routes.ts # 全部 DSH 路由 + agent-send/create/modes + session/summary
365
+ │ ├── http.ts # 共用 HTTP 助手(readBody/json)
366
+ │ ├── clip-bridge.ts # 本地剪藏桥(v0.16.24):127.0.0.1 监听 + POST /clip 写 wiki(Host 校验/token/CORS preflight)
367
+ │ ├── session-summary.ts # 会话「知识库」Tab 后端
368
+ │ ├── admin.ts # 设置页后台:tiddlywiki.info 读写 + /admin/*
369
+ │ ├── config.ts # ConfigStore:cordis config 基底 + 配置 tiddler 覆盖层
370
+ │ ├── seeds.ts # 统一 seed 注册表(10 项,三层:核心/起步/可选)
371
+ │ ├── prompt.ts # 系统提示词(v0.21.0):slim/full 两种形态 + extra/override,full 的目录由工具注册表实时生成;v0.22.7 起草稿预览(normalizePromptPreview/describePrompt,与保存路径同一份拼装)
372
+ │ ├── seed-*.ts # 各 seed 实现(bundle/首页/ui-styles 常量脚本生成;starter-docs/menubar/clip-bridge 手工维护)
373
+ │ └── tools.ts # 15 个 tiddlywiki_* 工具(列表式注册)
374
+ └── client/ # 浏览器半部
375
+ ├── index.ts # client 入口(inject ['slots'],纯 DOM,永不 throw)
376
+ ├── endpoints.ts # 客户端同源端点常量 + describeSyncResult(同步回执唯一实现,v0.22.8)
377
+ ├── status-cache.ts # 共享 /status 读取器(2s TTL + 在途合并)
378
+ ├── ui-config.ts # 共享 ui.* 投影 + invalidate/subscribeUiConfig(v0.22.8 扩到全部 FAB 开关)
379
+ ├── render-fetch.ts # **唯一**的 POST /render 调用(v0.22.8)
380
+ ├── knowledge-fab.ts / quick-note-dock.ts / note-widget.ts / editor-popup.ts
381
+ │ # 知识库 FAB / 快捷按钮 / 快速笔记 / 原生编辑弹窗
382
+ ├── markdown-editor.ts # CodeMirror 6 Markdown 编辑器
383
+ ├── session-summary.ts # 会话「知识库」Tab(conversation.view 槽位)
384
+ ├── tool-views.ts # 回复流工具卡片(tool.call.toolview)
385
+ ├── theme-sync.ts / sidebar-entry.ts / sync-button.ts
386
+ │ # 主题同步 / 侧边栏入口 / 同步按钮
387
+ ├── tw-frame.ts # **TW frame 内核(v0.22.4 起唯一实现)**:createTwFrameSurface(skin) 管 lazy-load / status 轮询 /
388
+ │ # 错误与启动态 / 主题同步 / FAB 重载 / hash 导航 / dispose;中央面板与右侧栏共用同一份
389
+ └── panel.ts / rightbar-tab.ts / settings-page.ts / state.ts / styles.ts / toast.ts
390
+
391
+ > ⚠️ v0.22.8 起 `endpoints.ts` 的 `describeSyncResult()`、`render-fetch.ts`、`ui-config.ts` 各是**唯一实现**:
392
+ > 同步回执、渲染调用、`ui.*` 投影都不要再在客户端面里各写一份(此前已因此漂移)。
393
+ scripts/ # 构建/校验/再生成脚本
394
+ docs/seed-initialization.md # seed 机制详解(权威)
395
+ lib/ # 预构建产物(发布含 lib/**,提交入库;零 @deepseek-ai 运行时 import)
396
+ ```
397
+
398
+ ---
399
+
400
+ ## 🕘 版本记录
401
+
402
+ > 最近几个主要版本的一句话记录(完整变更见 [Releases](https://github.com/bbqisbbq/dsh-tiddlywiki/releases) / git log)。
403
+
404
+ - **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`」。
405
+ - **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 残留**(踩过:把残留文案当实时状态误判账号被拦,实际账号正常)。
406
+
407
+ - **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 两行表格。
408
+
409
+ - **v0.23.0**(2026-09-17):**新增「发布到微信公众号」+ 发布元数据**(`tools/wechat/`,**可选功能,默认关闭,需额外安装**,不动 host 代码)。把 wiki 笔记一键发到公众号**草稿箱**,可选点发表。
410
+
411
+ **⚠️ 可选功能,默认关**:真正干活的是仓库 `tools/wechat/` 下的 opencli adapter + Browser Bridge 浏览器扩展——**插件本体不含它,也不会替你装**。开关 `wechat.enabled` 默认 `false`:**关闭时**不注入任何发布相关提示词、也不往 wiki 写「发布元数据规范」文档;**打开后**仅多这两项,不会安装任何东西、不启用任何后台服务。不装/不开它,插件其他功能完全不受影响。开启方式:设置 →「TiddlyWiki 知识库」→「可选功能:微信公众号发布」→ 勾选 → 保存配置。完整安装步骤(opencli、浏览器扩展、扫码登录)与排错见 [docs/wechat-publish-setup.md](docs/wechat-publish-setup.md)。
412
+
413
+ **为什么不用官方 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` 驱动后台填表/写正文/传图/设封面/存草稿。
414
+
415
+ **发布元数据**(本轮追加,解决「哪些发过 / 哪些不能发」):每篇笔记用自定义字段记录对外发布状态——`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 回写。
416
+
417
+ **四个实测踩坑**:① **必须带 `--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 符号原样进库)。**发表需管理员扫码**——微信的账号安全机制,无法自动化,「一键」的真实含义是「脚本做完排版/上传/填表,你只扫一次码」;默认走**发表**(不推送粉丝、不占群发额度)而非群发。
418
+
419
+ 命令:`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 笔记「公众号发布插件调研」。
420
+
421
+ - **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` 才落盘)。
422
+
423
+ - **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 条。
424
+
425
+ - **v0.22.8**(2026-09-15):**第六轮代码审计修复**——host 与 client 各做一份只读审计,逐条核实后修 10 处真缺陷,并清掉一轮重复实现与死代码。① **数据丢失(P0)**:`/edit` 仍会把「调用方文本或笔记已保存正文」PUT 进**刚复用**的那个草稿,而那个草稿里装的是用户在 TW 原生编辑器里敲的**未保存内容**——触发路径极常见:打开快速笔记(标题精确到分钟)→ 写 → 关掉 → 同一分钟内再打开 → 草稿被覆盖。现在只有**我们创建的**草稿才由我们填充,复用的仅在调用方显式给了文本时才写。② **设置页超时(P0)**:`fetchJson` 展开 `init` 后又硬编码 `signal`,于是「知识库切换 / 恢复默认 / 重启 TW」的 120s 预算被 15s 掐断——宿主其实切成功了,用户却看到「切换失败」(大 wiki 冷启动实测 44s+),且重试会再走一遍停/启。③ **净化器崩服务**:`&#1114112;` 这类越界数字实体会让 `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(草稿覆盖那条**已反向验证**:还原修复即红)。
426
+
427
+ - **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)。
428
+
429
+ - **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 + 真实卡片结构对比修复前后的按钮排版。
430
+
431
+ - **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 的重启断言(窗口热改 + 夹取 + 仍判就绪)。
432
+
433
+ - **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` 的失败属环境问题(手动复核过断言全过),与本版无关。
434
+
435
+ - **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()` 走真实函数(前缀 / 裸名 / 缺失 / 读失败),静态断言会剥掉注释行以免匹配到自己的说明文字。
436
+
437
+ - **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:{...}}` 对象,导致「发布包内容」检查假失败(与本次改动无关,但会卡住发布流程)。
438
+
439
+ - **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 已手工打上同样的两处修改)。
440
+
441
+ - **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 组内容哈希断言(新鲜 / 用户改过 / 内置更新 / 旧标记两种 / 重新初始化后刷新)。
442
+
443
+ - **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`。
444
+
445
+ - **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+回收站恢复保类型)。
446
+
447
+ - **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。
448
+
449
+ - **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`。
450
+
451
+ - **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 用例。
452
+
453
+ - **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。
454
+
455
+ - **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` 两个坏检查、客户端泄漏与草稿回写等。
456
+ - **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/*`,并先做实体解码挡 `&#106;avascript:` 这类混淆),新增 **`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 描述同步。
457
+
458
+ - **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 后新会话生效;客户端改动需刷新页面。
459
+ - **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` 不再默认打作者本机端口。
460
+ - **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` 规避冲突。
461
+ - **v0.16.28**(2026-09-10):**修:拖拽版剪藏书签在知乎等真实页面报 `SyntaxError: Unexpected token ';'`**。根因(真实 Chrome 实验证实):0.16.27 把书签 href 做 **HTML 实体转义**(`&quot;`/`&amp;`/`&lt;`),但浏览器的 `a.href` **不做实体还原**——拖拽成书签后存的就是实体文本,执行时即解析失败。修复:href 改为 **percent-encoding**(`CLIP_BRIDGE_DRAG_HREF`,属性里只剩 `%XX`,天然无需任何属性转义);Chrome 执行 `javascript:` URL 前会 percent 解码(实测通过)。浏览器 E2E 升级为**直接以编码 href 走 `location.href` 执行**(完全等价拖拽书签的执行路径)并断言浮层弹出;静态验收改为 `decodeURIComponent(href) === 代码常量` 一致性校验。行为不变——**已拖过旧版书签的用户需重新初始化 seed 后重新拖一次**(旧书签仍然坏的,因为里面的代码是实体文本)。
462
+ - **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 异常)。
463
+ - **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 文档)修复。
464
+ - **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 项全过)。
465
+
466
+ - **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。
467
+
468
+ - **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 自动跳过。**更新后刷新页面**即可看到入口。
469
+ - **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]`)。
470
+ - **v0.16.21**(2026-09-09):**新:DSH 右侧边栏(rightbar)集成**。注册 TiddlyWiki 为右侧栏 tab 类型——rightbar 首页出现「**TiddlyWiki 知识库**」入口盒,点击即在右侧栏以 tab 形式打开完整 TW 编辑器(同源代理 iframe,跟随 DSH 主题),**与聊天并排**;配置 `ui.showRightbarTab`(默认 true)可开关。**更新后刷新页面**即可看到入口。
471
+ - **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**。
472
+ - **v0.16.19**(2026-09-07):**修:会话「知识库」Tab 把汇总当源码显示**——TW 5.4.1 把 `$:/temp/` 条目一律按代码块渲染;改为 `/tw/render` 原生片段管线(与回复流工具卡同链路),不再用 iframe/story view。
473
+ - **v0.16.18**(2026-09-07):README 全面精简重写;seed 优化——首页 seed 跟随 wiki 现状、修「所有文章」回主页死链、doc-note 文案修正。
474
+ - **v0.16.17**(2026-09-07):点击快速笔记可选直达 TW 原生编辑页(`ui.quickNoteMode`)+ 修「在 TW 中编辑」弹窗 ✕ 关不掉。
475
+ - **v0.16.16**(2026-09-07):修会话知识库 Tab 误入编辑草稿显示源码——三层防误编辑(清残留草稿/吞草稿创建/禁 ✏️)。
476
+ - **v0.16.15**(2026-09-07):`put`/`batch_put` 未指定类型自动默认 `text/markdown`;警告 `fields.type` 是内容类型保留字段。
477
+ - **v0.16.14**(2026-09-07):修知识库 Tab「佚失条目」根因——浏览器端 TW 天生排除 `$:/temp`,改为 iframe 就绪后注入 + 原生 hash 导航。
478
+ - **v0.16.11**(2026-09-06):新增会话顶部「知识库」Tab(本会话相关 wiki 汇总,volatile `$:/temp`)。
479
+ - **v0.16.0**(2026-09-05):回复流原生 TW 工具卡片 + 可点击 wiki 链接(新增 `render-route` seed)。
480
+ - **v0.15.0**(2026-09-05):初始化不强制 + 可反初始化(seed 拆核心/可选两层)。
481
+ - **v0.14.0**(2026-09-05):「发送给 Agent」按钮独立图标 + 附加说明 + 权限选择。
482
+ - **v0.13.0**(2026-09-04):menubar 顶栏主题自适应。
483
+ - **v0.12.0**(2026-09-04):新默认主页 + 「所有文章」两列分页页。
484
+ - **v0.11.0**(2026-09-04):「发送给 Agent」支持选择工作模式(Agent 预设)。
485
+ - **v0.10.0**(2026-09-04):统一 seed 注册表 + 设置页「初始化」。
486
+ - **v0.8.0**(2026-09-04):嵌入式 TW 跟随 DSH 深浅主题。
487
+ - **v0.7.0**(2026-09-03):TW 一键把当前笔记发送到 dsh 会话。
488
+ - **v0.6.0**(2026-09-03):同源代理(远程访问核心)。
489
+ - **v0.5.0**(2026-09-03):Agent 工具扩充 + 快速笔记草稿自动保存 + 统一知识库 FAB。
490
+
491
+ ---
492
+
493
+ ## 📦 发布
494
+
495
+ ```bash
496
+ npm publish # 版本号在 package.json;文件白名单见 files 字段
497
+ ```
498
+
499
+ `tiddlywiki` 依赖体较大(含全部语言包/插件)。
500
+
501
+ ---
502
+
503
+ ## 🔗 仓库与发布元数据
504
+
505
+ - **GitHub**:https://github.com/bbqisbbq/dsh-tiddlywiki
506
+ - **npm**:`dsh-tiddlywiki`(https://www.npmjs.com/package/dsh-tiddlywiki)
507
+ - 说明笔记、首页、示例文档、样式等预置内容由 seed 机制写入 wiki(见 [🧩 初始化](#-初始化一次性预置-seed哪些必备哪些可有可无))
508
+ - MIT;Node ≥ 22;GitHub topics:`dsh` `dsh-plugin` `tiddlywiki` `knowledge-base` `note-taking` `git-sync` `agent-tools` 等