dsh-tiddlywiki 0.19.5 → 0.20.1

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
@@ -26,17 +26,18 @@
26
26
  | 🏠 **文档中心起步包** | 首次安装自动 seed:插件说明 + 「示例与文档」(主题汇总模板 / 教程 / 三个示例主题页),首页「📚 插件文档」栏一键查阅;**同名 tiddler 已存在一律安全跳过,绝不覆盖你的数据**(v0.16.22) |
27
27
  | 🎨 **自定义样式** | 「自定义样式」seed:编辑器美化 / 窄屏侧栏隐藏 / menubar 加高 / 批注弹窗等 5 张通用样式表,新 wiki 也能一键初始化(可选,v0.16.22) |
28
28
  | 🤖 **Agent 工具** | 15 个 `tiddlywiki_*` 工具:检索(**相关度排序 + 命中处片段 + 字段过滤**)、读写、**增量追加**、批量、重命名、**软删除/回收站**、**反向链接**、**附件入库**、**知识库体检**、git 同步与冲突解决(v0.19.0;检索/最近仍在**服务端**排除二进制附件,大 wiki 上从 515MB/17s 降到 ~0.4s) |
29
- | 🛡️ **不会被覆盖的写入** | 所有写入路径(agent 工具 **与** 快速笔记/编辑器路由)都**先读后写**:不传 tags 就保留原有标签与自定义字段;`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) |
29
+ | 🛡️ **不会被覆盖的写入** | 所有写入路径(agent 工具 **与** 快速笔记/编辑器路由)都**先读后写**:不传 tags 就保留原有标签、自定义字段与**内容类型**(`text/css`/wikitext 等不会被重置成 Markdown,v0.20.1);`tiddlywiki_put(..., expectedModified/expectedRevision)` 与 `tiddlywiki_delete(..., expectedModified/expectedRevision)` 乐观并发——读取后若有人(在 TW 编辑器里)改过,写入/删除被拒绝(HTTP 409)而不是静默覆盖或丢进回收站;`tiddlywiki_attach` 的同名标题**默认拒绝**(要覆盖必须 `force: true`);`tiddlywiki_delete` 默认**软删除进回收站**,`tiddlywiki_trash` 可恢复(v0.19.0 / v0.19.1 / v0.19.5) |
30
30
  | 🧼 **渲染片段净化** | 回复流卡片与会话汇总注入的 TW 片段先经 **host 白名单净化**(丢 `iframe`/`script`/`svg`/`on*`/`javascript:`/`data:text/html` 等)——TW 自己的解析器只剥 `on*`,`<iframe src="javascript:…">` 会原样通过并在 DSH 页面里执行(v0.19.1 修复的存储型 XSS) |
31
31
  | 🔒 **写路由方法校验** | 每个写路由只接受自己的 HTTP 方法:跨站 `GET /sync`、`GET /restart`、`GET /upload` 一律 405 且无副作用(v0.19.0 修复了「任意网页一张 `<img>` 即可触发 pull/commit/push」的 CSRF 面) |
32
- | 🔑 **密钥不外泄** | `bridge.token` / `ui.sendToAgent.token` 在 `/admin/state`、`/admin/config` 的回包里是 `********`(设置页原样保存 ≠ 覆盖,清空即删除),`git.remote` 里的 PAT 打码;**`/tw` 与 `/api` 代理拒绝 `$:/plugins/dsh-tiddlywiki/` 命名空间**——此前可绕过 `/get` 的限制直接经代理读到配置 tiddler(v0.19.3) |
32
+ | 🔑 **密钥不外泄** | `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) |
33
33
  | 🧯 **路由不会拖垮进程** | 所有路由经 `guardHandler` 包装:任何 rejection(含代理里 try 之外的 `new URL()`)都变成 413/500 响应,而不是宿主未处理的 promise rejection(那会**直接结束 dsh web 进程**并挂死请求);剪藏桥 listen 后保留常驻 `error` 监听(v0.19.3) |
34
34
  | 📊 **回复流卡片** | 工具结果显示原生 TW 卡片(**按笔记自己的内容类型渲染**:Markdown 笔记就是 Markdown,v0.18.0);`[标题](/dsh-tiddlywiki/tw/#标题)` 点击直达 TW 面板 |
35
- | 📤 **发送给 Agent** | TW 笔记工具栏一键把当前笔记注入所选 dsh 会话(可选工作模式/权限/附加说明) |
35
+ | 📤 **发送给 Agent** | TW 笔记工具栏一键把当前笔记注入所选 dsh 会话(可选工作模式/权限/附加说明);成功/失败会弹出提示(v0.20.0 修复:此前提示把自由文本当 tiddler 标题传给 TW notifier,全部静默) |
36
36
  | 🧭 **内嵌编辑器** | 中央列内嵌完整 TW 5 编辑器(同源代理,Tailscale/内网/域名/HTTPS 均可) |
37
37
  | 🗂️ **右侧边栏 Tab** | DSH 新右侧栏(rightbar):首页「TiddlyWiki 知识库」入口一键打开,与聊天并排;链接点击可直达(v0.16.21) |
38
+ | 🧪 **审计守门** | 第四轮审计(v0.20.0)把 CI 与 `npm run verify:*` 合成一份清单,并补上 `verify-constants`(filter 长度预算)、`/render` 403、auth 打码、notify 与草稿避让回归 |
38
39
  | 🧩 **Better Sidebar 共存** | 与 dsh-better-sidebar 侧边栏共存(其展开/收起按钮浮在 TW 面板之上);**不再向该侧边栏注册 TW tab**(v0.17.0 移除了 tab 注册,避免 tab kind 冲突) |
39
- | 📚 **会话知识库 Tab** | 每个会话顶部汇总本会话读写过的 wiki 笔记,TW 原生渲染(`/tw/render` 片段管线,v0.16.19) |
40
+ | 📚 **会话知识库 Tab** | 每个会话顶部汇总本会话读写过的 wiki 笔记,TW 原生渲染(host `/render` 片段管线 + 白名单净化,v0.16.19 / v0.19.1) |
40
41
  | 📝 **快速笔记** | 输入框上方快捷按钮或右下角「知识库」FAB;原生编辑页或 Markdown 卡片两种模式;首页内置「快速记笔记(完整编辑器)」 |
41
42
  | 📌 **本地剪藏桥** | 可选「剪藏桥 + 书签小工具」:DSH 监听 127.0.0.1 端口接收剪藏请求(Host 校验防 rebinding),点书签弹出浮层——可改标题/编辑选中文字/**勾选图片**,一键写入知识库;文字成笔记(默认 `clip` 标签),图片由桥下载存为**二进制附件**(`type: image/*` + base64,笔记内 `[img[标题]]` 内嵌),随 wiki 自动进 git;图片下载有 **SSRF 守卫**(仅公网 http(s)、逐跳校验重定向,v0.18.0) |
42
43
  | 🔐 **写操作防跨站** | 所有写路由(笔记/上传/同步/后台/代理)拒绝跨站请求(403),仅同源的 GUI 与内嵌 TW 可写;跨站 GET 与浏览器地址栏直达不受影响(v0.18.0) |
@@ -86,9 +87,9 @@ dsh plugin --profile web add link:/path/to/dsh-tiddlywiki
86
87
  | `tiddlywiki_recent` | 最近修改的笔记(倒序),开工快速了解动态 |
87
88
  | `tiddlywiki_list_tags` | 现有非系统 tag 及计数(已排除只挂在二进制附件上的 tag);默认列使用最多的 200 个(`limit` 可调、上限 1000),截断时返回 `total`/`truncated` |
88
89
  | `tiddlywiki_get` | 读单个 tiddler 全文(`modified` 以 ISO 返回,可直接用作 `expectedModified`) |
89
- | `tiddlywiki_put` | 写/覆盖;未指定类型时自动默认 `text/markdown`(`$:/` 系统条目除外);`expectedModified` + `force` 提供乐观并发保护 |
90
+ | `tiddlywiki_put` | 写/覆盖;**新建**未指定类型时默认 `text/markdown`(`$:/` 系统条目除外),**覆盖既有条目时保留原 `type`/tags/自定义字段**(v0.20.1,改类型要显式 `fields.type`);`expectedModified` + `force` 提供乐观并发保护 |
90
91
  | `tiddlywiki_batch_put` | 批量写入(`overwrite=false` 跳过已存在;单条失败不影响其余、逐条报错;4 路并发但结果保持入参顺序) |
91
- | `tiddlywiki_append` | **增量追加**:`mode=append\|prepend`、`heading=某标题` 定位段落,写日志/批注无需读全文 |
92
+ | `tiddlywiki_append` | **增量追加**:`mode=append\|prepend`、`heading=某标题` 定位段落,写日志/批注无需读全文;与 `put` 同一套写策略(保留原 `type`/tags/自定义字段,新条目才默认 markdown),支持 `fields` 显式覆盖 |
92
93
  | `tiddlywiki_rename` | 重命名 + 尽量同步其他条目里的引用;旧标题删除失败时如实回报「两份副本都在」 |
93
94
  | `tiddlywiki_delete` | 删除(幂等)。默认**软删除**进 `$:/dsh-tiddlywiki/trash/`,`permanent=true` 才真删;支持 `expectedModified`/`expectedRevision`/`force` 乐观并发(读后被改动则拒绝,不把人类的新改动丢进回收站) |
94
95
  | `tiddlywiki_trash` | 回收站:`action=list\|restore\|empty`(索引读不到或损坏时显式报错,绝不把回收站当空的重建) |
@@ -297,6 +298,10 @@ lib/ # 预构建产物(发布含 lib/**,提交入库;
297
298
 
298
299
  > 最近几个主要版本的一句话记录(完整变更见 [Releases](https://github.com/bbqisbbq/dsh-tiddlywiki/releases) / git log)。
299
300
 
301
+ - **v0.20.1**(2026-09-11):**修复「覆盖已有条目时内容类型被静默重置」**(数据正确性)。`tiddlywiki_put` / `tiddlywiki_append` / `tiddlywiki_batch_put` 覆盖既有条目时会丢掉它的 `type`:`cleanTiddler()` 把 `type` 当成「构造 PUT body 时跳过的字段」,于是 `finalTypeForWrite()` 再补默认值——`put` 把 `text/css` 的样式条目改成 `text/markdown`(整篇 CSS 被当 Markdown 渲染、样式静默失效)、把 wikitext 笔记改成 Markdown;`append` 更彻底:它自己手写 PUT body,连 `type` 都不写,TW 于是回落 `text/vnd.tiddlywiki`(磁盘上 `.md` + `.meta` 变成 `.tid`,`##`/`**粗体**`/表格全按 wikitext 解析)。现在:**覆盖路径一律保留原 `type`**,Markdown 默认值只给**新建**条目(`$:/` 系统条目除外);`tiddlywiki_append` 与 `put` 共用同一套写策略(`buildWriteTiddler`),并补上了此前缺失的 `fields` 参数;`rename` 与回收站恢复同样不再丢类型。回执也补了诚实提示:新建默认时写「新建且未指定,已默认 markdown」,覆盖时若类型真的变了会写「⚠️ 内容类型已从 X 改为 Y」。守门:新增 `scripts/verify-write-policy.mjs`(7 条纯函数单测,已进 `verify:unit`)与 `verify-audit-fixes.mjs` 的 6 条 E2E(css 保持 css / wikitext 保持 wikitext / markdown 追加不变 / 新建仍默认 markdown / `fields.type` 是唯一改类型入口 / rename+回收站恢复保类型)。
302
+
303
+ - **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。
304
+
300
305
  - **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`。
301
306
 
302
307
  - **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 用例。