-
v0.26.2(2026-09-22):修「catalog 标题用目录名推导」——codemirror-fullscreen-editing 的真名其实叫 $:/plugins/tiddlywiki/codemirror-fullscreen。bundledCatalog() 一直用 $:/plugins/tiddlywiki/${目录名} 拼标题,而 npm 包里这个插件目录名与 plugin.info 声明的 title 不一致(全包仅此一例,本机实测确认)。v0.26.0 引入的状态徽标按完整 tiddler 标题匹配,于是这个插件一旦在 TW 里被禁用,行内不会出现「TW 内已禁用」徽标(勾选不受影响——勾选按 tiddlywiki.info 的插件名)。修复:标题改为优先取 plugin.info 的 title,缺失/为空才回落目录名(plugin.info 才是 TW 真正加载的那个 tiddler,是权威来源)。守门:scripts/verify-plugin-runtime.mjs 新增 2 条——① 行为断言(fixture 里目录名与 title 不一致时必须采用 title,无 title 时回落目录名);② 不变量断言(遍历真实 npm 包的全部自带插件,catalog 标题必须逐条等于其 plugin.info 声明的 title,并单独锁定 codemirror-fullscreen 这一例),已反向验证(把优先逻辑去掉即红 2 条)。
-
v0.26.1(2026-09-22):修 P0:设置页插件/主题/语言的勾选框全部显示为「未勾选」,且误点「应用」会把启动清单整个清空。回归由 v0.25.0 的 CatalogPending 引入(commit 883196f):契约是「undefined = 用户还没碰过、完全跟随服务器」,而挂载时把它初始化成了 { plugins: new Set(), themes: new Set(), languages: new Set() }——desiredPlugins() 用 ?? 回退,而空 Set 不是 undefined,于是「没碰过」被读成「用户期望集合为空」:① 所有勾选框全空(用户看到「TW 里明明装着的插件,这里一个都没勾」——本次由作者实测报障);② 更严重的是,什么都没改就点「应用插件」会 POST 一个空数组,把 tiddlywiki.info 的 plugins 清单整个清空(主题清单、语言同理:一次误点等于卸载 codemirror/menubar/全部主题/中文语言,TW 重启后只剩内核 + 自动补装的 markdown)。修复:初始化改为空对象 {};并给三个「应用」按钮加闸门——pending.* === undefined(未碰过)时按钮禁用、点击直接返回,只有真正改过勾选才允许提交(「用户刻意取消全部勾选」仍是合法提交,host 的 no-op 守卫照旧兜底)。顺带把「wiki 内已装」徽标的判据从 desiredPlugins() 改为服务器集合 serverPlugins——徽标描述当前事实,不该随未应用的勾选抖动。守门:scripts/verify-plugin-runtime.mjs 新增 4 条(空对象初始化 + 三个闸门 + 三个 sync 函数必须在 change 回调里重算按钮 + 徽标判据),已反向验证(改回空 Set 即红)。
-
v0.26.0(2026-09-22):修「设置页插件管理与 TW 实际状态对不上」——两套真相源不再互相说谎。现象(作者本机实测):TW 控制面板里明明启用着的插件(TiddlyFlex、mermaid、pinboard 等经 TW 原生插件库装的),设置页勾选清单里根本不出现;反过来 menubar / comments / help 在 tiddlywiki.info 里(勾选着),但用户早在 TW 面板里禁用了它们。根因:设置页只读 tiddlywiki.info 的 plugins 数组(启动安装清单),而 TW 运行时集合 = tiddlywiki.info ∪ 以 tiddler 形式装进 wiki 的插件 − $:/config/Plugins/Disabled/<title>(正文为 yes,TW 核心自己的判定)——两个面板读的不是同一份真相,谁也没错但谁也不全。修复(方向「修真」):勾选语义保持「是否写入 tiddlywiki.info」不变(它只管引擎自带插件的离线启停,这是 TW 原生界面做不到的唯一能力),但把缺失的另一半真相显示出来——① 每个插件行加状态徽标:「TW 内已禁用」(勾选着但被 TW 禁用)/「wiki 内已装」(不在启动清单但 wiki 里有同名 tiddler 版本,TW 运行时已在用);② 新增只读小节「wiki 内插件(经 TW 原生安装)」,列出第三方插件 tiddler(名称/版本/描述/禁用状态),启停/卸载引导去 TW 控制面板;③ 区块说明文字写明两边分工。实现:GET /admin/state 新增 runtimePlugins 字段,由 scanWikiRuntimePlugins()(src/host/admin.ts)扫 tiddlers 目录得出——插件库安装的插件是 $__plugins_*.json + .json.meta(只读几十字节的 meta,不碰几 MB 的插件 payload;无 meta 的裸 .json 还可能是拖拽导入的多 tiddler 数组,一并解析);禁用标记 $__config_Plugins_Disabled_*.tid 按文件自身 title: 头还原插件标题(不做有损的文件名反解码)。扫描失败返回 null(读失败 ≠ 不存在),客户端隐藏徽标与小节而不是显示空的假象;dsh/*、dsh-tiddlywiki/* 命名空间(插件自己的种子 bundle)排除在外。产品哲学不变:插件对 wiki 的强制定制依旧只有 --init server 基线 + 自动补装 markdown + 3 个核心种子,官方自带插件之外的安装/管理全部留给 TW 原生界面。
-
v0.25.1(2026-09-21):修「回复流里的 wiki 链接点了打不开」——根因在 DSH web 的 markdown 渲染器把 [标题](/dsh-tiddlywiki/tw/#标题) 这类相对链接解析成绝对 http(s) URL,再按 protocol 判为「外链」(target="_blank" + 自己的 openExternalLink onClick)。旧 installWikiLinkInterceptor 只匹配相对路径正则 ^/dsh-tiddlywiki/tw/#(.+)$,绝对 href 匹配不上 → 拦截器不接管 → 点击落入 DSH 外链逻辑,在新标签页打开 TW 编辑器而不是中央面板(用户感知「打不开」)。修复:拦截器改用 new URL(href, window.location.origin) 归一化后判同源 + pathname === TW_PROXY_BASE + hash 非空(相对/绝对两种形态都接),在 capture 阶段 preventDefault + stopPropagation 抢在 DSH 外链 onClick 之前打开中央 TW 面板;scripts/verify-tool-views.mjs 新增源码级断言(必须用 new URL + 同源校验 + 代理 pathname,且旧相对正则已移除)。headless Chrome 实测:TW 面板 hash 导航(含 TiddlyFlex 的 $:/StoryList-1)本身正常,问题确在拦截器漏匹配绝对 href。
-
v0.25.0(2026-09-20):第五轮代码审计的修复版(审计那天被用户判定为「本地单机可接受、不修」的只有一条:POST /render 的 title 分支可以经被渲染条目正文里的 {{…}} 转写把插件配置读出来——本机实测复现,text 分支早已 403;不修的理由是单机自用、且真正的边界本就不在这条路由(任何笔记正文的 {{$:/plugins/dsh-tiddlywiki/config}} 在嵌入式 TW 面板里同样会被转写渲染)。其余全部修掉,按性质分四组。① 工具契约(数据安全):(a) tags: [] 现在真的清空标签——此前 normalizeTagArg() 把空数组归一成 undefined(=「未传 = 保留原标签」),而 tags 又是 RESERVED_TIDDLER_FIELDS 成员(fields.tags 会被拒),于是没有任何途径把一篇笔记的标签改成空:模型照 tiddlywiki_lint 的 junk-tags 建议去清标签、传 [],什么都不会发生,回执还把旧标签原样列出来(本机实测)。(b) tiddlywiki_append / tiddlywiki_rename / tiddlywiki_batch_put(逐条目 expectedModified)补上乐观并发令牌:这三条与 put/delete/attach 一样是「读全文 → 整篇回写」(append 读旧基底再加一段),而可传令牌的只有后三条 —— 注入提示词让模型回传 expectedModified,append 却没有这个参数,人类在 TW 编辑器里的并发修改会被旧基底静默回滚。(c) tiddlywiki_rename 更新引用时逐个重读引用者(列表只是快照),遍历期间被改动过的跳过并在回执里报出数量——跳过一条链接可以手工补,被覆盖掉的一段正文不可恢复。(d) tiddlywiki_attach 的 noteTitle 指向二进制附件时明确拒绝(会把 base64 写坏而回执看着正常)。(e) tiddlywiki_lint 的 checks 拼错不再谎报干净:本机实测 checks:["broken-link"](少了 s)在 2933 条笔记的知识库上一个检查都没跑,回执却是「扫描 2933 条文本笔记,发现 0 个问题 / 没有发现问题」;现在回执列出实际运行的检查、显式报出无法识别的名字,且「一个检查都没跑」不再等于「没问题」。(f) valid-until / review-after 的纯日期(YYYY-MM-DD)改按当地日终判过期,此前按 UTC 0 点算,东八区当天 08:00 就报「已过期」。② 设置项(都是「保存了却不变」):git.autoCommit/git.debounceMs/git.remote 保存后对运行中的插件立即生效(AutoCommitter 在构造时把 enabled/debounceMs 快照进不可变 options,remote 只在启动 bootstrap 时应用——此前关掉自动提交仍会 commit、换远端仍推老地址,直到重启 dsh web);「重启 TW」按钮的 15s 超时改为 120s(/admin/restart 要等 TW 就绪,软窗口默认 60s、大知识库自述 44s+,此前会弹「重启失败」而宿主其实成功);数值项(bridge.port / startup.readyTimeoutMs / ui.allArticles.pageSize / git.debounceMs)加 min/max 与字段旁红字,非法值拒绝保存(宿主对越界值是静默夹取/回落,提交只会继续制造「回显 ≠ 生效」);空补丁不再发请求、宿主在序列化文本逐字节相同时跳过 PUT(每次空点「保存配置」都会重写配置 tiddler 并刷新 .meta 的 modified → 白造 git diff、两台机器各点一次就冲突在 .meta 上,与 v0.24.2 刚修的 $__language.txt.meta 同类);保存被拒(409,如配置 tiddler 残留冲突标记)时错误持久显示在配置区而不是一闪而过的 toast;微信发布的 command/adapter/token/dsn 补齐 UI(token 密码型 + 👁 显隐切换,掩码回填语义照旧);插件/主题/语言的未应用勾选提升为页面级状态,能活过「同步 / 重启」触发的重刷;表单有未保存内容时若配置在别处被改过,保留你的输入并显式提示,而不是重建表单把输入清掉;首屏加「加载中…」、加载失败不再清空已填表单。宿主侧 /admin/config 的补丁现在必须过新的 normalizeConfigPatch():已知数值夹取到真实边界、已知布尔/字符串类型错误返回 400 并点名字段、__proto__/constructor/prototype 一律拒绝、未知键透传(此前任意 JSON 都会落进配置 tiddler——数组体变成 {"0":1,"1":2} 这种永久垃圾键,git.debounceMs: "abc" 会经 ?? 进 setTimeout 被当 0 → 每次写入都 commit)。③ 会话「知识库」Tab:NOTE_TOOL_NAMES 补 append(「增量写入」)/attach(「附件」)/delete/trash(restore 计产生),新增「🗑 删除 / 回收站」一节——此前用 append 写的笔记在这个 Tab 里完全不可见,而注入提示词恰好推荐「纯增量内容优先用 append」,于是一整轮工作会被显示成「本会话暂时没有产生、读取或检索过任何知识库笔记」;事件日志读不出来的会话在页面上明说(不再静默吞掉、把「读不出来」显示成「没碰过笔记」),counts.sessions 只统计成功读取的会话;读日志从串行改 4 路有界并发、探测整段加 12s 墙钟预算(最坏 ~375s → 有界,客户端 25s abort 之后不再继续压着 TW 打),生成加 3s 单飞复用;客户端自愈改为三态探测(在 / 被清 / 状态未知——未知既不计失败也不清零,此前 catch { return true } 把「探测失败」当「一切正常」),自愈 interval 与 phase 解耦(进 error 态后不再永久失去重试),并新增按 3 分钟节拍的静默刷新(tab 长期开着也能跟上会话,不闪 loading 不砸内容);escapeInline() 中和 {}(会话日志里出现的 {{…}} 会被 /render 转写展开)并改为先截断再转义(否则 & 会被截成 &am);scanRefs 不再把标题里的 )/] 一律截断;TitleState.snippet 从死字段改为真的渲染。④ 其余:检索卡片的范围与工具结果对齐(卡片把模型可见回执里读到的 workspace 回传 /search,v0.24.0 只改了工具侧——卡片会列出更多的结果且不显示范围);/status 的 git 冲突内容扫描加上限(200 个文件 / 2MB,批量导入期间不再每 30s 同步读几千个文件阻塞事件循环);tiddlywiki_delete 的描述写明 $:/ 系统条目实为永久删除;TiddlywikiConfig.note 补上漏掉的 workspaceMark(三处形状 lockstep);tiddlywiki_attach 有专属卡片。守门:新增 scripts/verify-config-patch.mjs(6 组纯函数断言:非对象拒绝 / 越界夹取 / 类型错误点名字段 / 危险键拒绝 / 未知键透传)与 scripts/verify-tool-views.mjs(5 条源码级断言:TOOL_VIEW_KEYS 与 host 工具名完全一致 / TOOL_LABELS 全覆盖 / 工作区前缀两处一致 / 搜索卡片仍回传 workspace),两者进 verify:unit;verify-tools.mjs +4 条真实 TW 断言(tags:[] 清空且「不传」仍保留 / append 令牌不匹配拒绝、匹配放行 / batch 逐条失败 / lint 未知检查名不得谎报);verify-write-policy.mjs +1 条(空数组 vs 不传的语义边界)。已反向验证的路径:把 normalizeTagArg 还原成「空数组 → undefined」、把 lint 的 checks 还原成自由集合、把 append 的令牌检查删掉,对应断言分别变红。
-
v0.24.2(2026-09-20):修「$:/language 每次 dsh web 启动都被无条件重写 → 两台机器每次 pull 都冲突」。现象:git pull 稳定卡在 tiddlers/$__language.txt.meta 上,而正文文件根本不冲突、diff 出来只有 created/modified 两行;本仓库的 git 历史里甚至已有一条专门清理该冲突残留的提交,而 2026-09-20 一次会话内又撞了两次。根因定位到行:启动自举应用 uiLanguage 时、以及设置页改动语言时,两处都无条件 PUT $:/language(正文每次都一样,但 TW 会给 .meta 盖新时间戳)——两台机器各自启动过就都改了同几行。现在抽出 pinLanguageTiddler()(src/host/admin.ts):先读,内容一致就不写,内容不同/条目不存在才写。⚠️ 关键边界:读失败绝不等于「内容相同」(铁律第三条)——client.get() 只有 404 返回 undefined、其余抛错,抛错时照写并记一条日志,否则用户的语言会永久卡在错的值上且重启也修不回来。守门:新增 scripts/verify-language-pin.mjs(进 verify:unit)——4 条桩 client 行为断言(一致不写 / 不存在要写 / 不同要写 / 读失败照写并报警)+ 源码级断言(index.ts 与 admin.ts 除原语自身外一处裸 PUT 都不许有,且两处都必须经 pinLanguageTiddler)。已反向验证:还原启动路径为无条件 PUT → 红 1 条;把读失败分支改成「直接 return false」→ 红 1 条。(附带一条方法论教训:第一次做这条反向验证时构建因我的改写而失败,而我把构建输出丢进了 $null,于是守门测的是旧 lib/、假绿了一轮;重做时把构建输出放回台面才抓到真红。反向验证必须确认「改动真的被编译进去了」。)
-
v0.24.1(2026-09-20):修「重启/停止 TW 的四个路径里有三个没排干 syncer 队列」——一类静默丢写入的缺陷。本仓库铁律第一条要求「重启 TW 前必须把 syncer 队列排干」,但那条规则只写在文档里:POST /restart、POST /admin/restart、POST /admin/info(改插件/主题后重启)与「知识库切换」的 stopServer 全都是直接杀 TW 子进程,而 TW 的 REST PUT 返回 204 时条目还在内存里、文件系统 syncer 要 ~250ms 后才落盘 —— 在那一秒内点「重启 TW」,刚写的笔记就随子进程一起消失(与 v0.19.0 丢 tw-web-host、v0.22.0 才发现 throttle 竞态同一类)。只有启动自举 / seed / /sync 三处走了 flushPendingWrites。根因不是谁写错了某一行,而是「规则」和「代码」之间没有强制关系 —— 所以这一版不是补四个调用点,而是把不变量变成原语:新增 drainThenStop()(src/host/seeds.ts,排干发生在它内部,签名是 {client, tiddlersDir, stop}),四个路径全部改为经它停/重启,调用方没法绕过;stop 的返回类型放宽为 Promise<unknown>(restart() 回状态视图、stop() 回 void,排干都不关心)。/restart 与 /admin/restart 的回包新增 drained,排干不完整时如实上报而不是静默。守门:新增 scripts/verify-restart-drain.mjs(进 verify:unit)——① 顺序断言:drainThenStop 内部「排干的 await」必须早于「stop 的 await」,且排干失败必须走显式分支后仍然 stop(best-effort:不能因为排不干就把用户锁死在起不来的 TW 上);② 源码级断言:routes.ts/admin.ts 里每个 deps.server.restart() 都必须作为 drainThenStop({stop: …}) 的参数出现(裸 restart 即红),index.ts 的 stopServer 必须包在它里面;③ 行为断言:用桩 client 真跑一遍,确认「写入真的落盘之后才 stop」「client 缺失(TW 没在跑)是安全 no-op」「排干失败也继续 stop 并把警告交出去」。已反向验证:分别把 /sync 路径、知识库切换还原成裸调用、并删掉 drainThenStop 里的 stop,三次都稳定变红。顺带把 /sync 与 seed 两条本来就在 flush 的路径也收敛到同一原语,从此「重启前排干」只有一个实现。
-
v0.24.0(2026-09-20):三处「Agent 读写 wiki 的规矩」按用户要求改进 + 一个 lint 误报修复。① 新建笔记自动标工作区:put/batch_put/append 新建条目时自动带上 ws/<项目名> 标签 + workspace 字段,项目名取自调用会话的工作目录(新 src/host/workspace.ts 纯函数:Windows/Unix 路径、尾斜杠、CJK、非法字符规范化;home/tmp/desktop 这类通用目录与驱动器根目录一律拒绝——后者是写这个模块的守门脚本抓出来的真缺陷:C:\ 会被算成 C:,在盘根开会话时全盘笔记都挂上 ws/C:)。标记是附加式(调用方标签在前)、仅新建(覆盖不追打,也不会改写已有 workspace 字段)、$:/ 豁免、note.workspaceMark: false 可关、调用方显式 fields.workspace 优先;exec 缺失 / 会话无 cwd / 未知会话一律安全降级为不标记(不猜、不抛错)。为什么标签必须带 ws/ 前缀:裸项目名与真实业务标签撞车——作者库里 dsh-tiddlywiki 已挂着 2510 篇导入的书籍章节。② 检索先窄后宽 + 多词 AND:tiddlywiki_search 先在工作区内查、区内 0 条才自动扩大到全库,回执明确写出「已在工作区 ws/X 内缩小范围」或「工作区 ws/X 内 0 条,已扩大到全库」(「工作区内 0 条」不等于库里没有,这是把「搜不到」错判成「不存在」的主要来源);query 从整串子串匹配改为按空白切词、全部词命中才算(旧行为连 alphaprobe betaprobe 这种自然的双词查询都永远匹配不到);调用方显式传 tag/field/value 时不自动收窄(显式范围不被二次猜测)。③ 过期内容淘汰策略(只提示,绝不自动删):阶段性笔记写 valid-until(硬过期)/ review-after(该复查),被取代时写 superseded-by + superseded 标签、保留旧笔记;tiddlywiki_lint 新增 stale 检查(默认开启、staleAfterDays 默认 180),报 stale-expired / stale-review 两类硬判定加一类 stale-candidates(带版本号或 done 标签且长期未改动,明确标注「候选,非判定」)。淘汰只由人决定——报告全程只读,不删不改不归档。④ 修 lint 死链误报(v0.23.6 的内容并入本版):死链正则 \{\{([^}]+)\}\} 会把 {{{ [tag[todo]count[]] }}} 这类三花括号过滤器表达式从第 1 个 { 开始捕获成「转写目标」并报死链(实测线上 480 条死链报告里 8 条属此类),现在用前后瞻 + 捕获内容守卫排除。明确不改的:≈440 条真实死链保留不修——多数是用户手工删掉的笔记,属预期状态(只读报告是对的,自动「修复」是错的)。守门:新增 scripts/verify-workspace.mjs(进 verify:unit,9 组纯函数断言,含「任何 cwd 算出的标签都不得是垃圾标签 / 含空白 / 丢前缀」这条与 isJunkTag() 的跨模块一致性检查,以及 ws/ 前缀不得被「简化」掉的回归),verify-tools.mjs +12 条真实 TW 断言(工作区标记六种形态 + 多词 AND + 先窄后宽 + fellBack + stale 三类),verify-prompt.mjs +1 条「三条新治理规则必须在场」并把 slim 长度预算从 1800 调至 2100(预算上调与新增的逐条断言绑定:删掉任何一条规则即红)。所有新守门都做过反向验证:分别还原旧行为(整串子串检索 / 关掉工作区收窄 / 关掉自动标记 / 关掉 stale 检查 / 去掉驱动器根目录判定 / 从规范化字符集里删掉 |)后,对应断言稳定变红。slim 正文实测 1918 字符(full 2722)。
-
v0.23.5(2026-09-18):修 15 处审计缺陷(第三轮全面审查列出的 18 条,逐条核实后修掉其中 15 条真缺陷;跳过 3 条纯理论性的信息泄露。完整审计报告见知识库笔记「dsh-tiddlywiki 第三轮代码审计」——审计报告是工作文档,不再进 git)。按性质分四组。① 数据安全(最重):tiddlywiki_delete 原先先删原条目、后读回收站索引,于是「快照已写 → 原条目已删 → 索引读失败」时报"已中止",但笔记其实已经没了、且没进索引——trash list 看不见、restore 找不到、empty 清不掉,成为永久孤儿(只能从 git 捞);现在读校验索引排在所有破坏性步骤之前,中止即等于"没删"。同一处还有两个洞:$:/ 系统条目会被软删除(删掉 trash-index 自己 → 下一次读 404 → 被当成"回收站为空" → 索引被重写成 1 条,丢掉全部记录),现在系统条目一律走永久删除分支,且删回收站索引被显式拒绝(要清空请用 trash action=empty)。tiddlywiki_put 不再把二进制附件写成文本(保留基底 type: image/png 会让 base64 解不出、图裂而回执说"类型没变";要转换得显式 fields.type)。git pull 不再无条件 rebase --abort——此前只要 pull 失败(含没配 remote、离线等与被 rebase 无关的原因),用户在 wiki 仓库里正在做的 rebase 与冲突解决就被清掉;现在只 abort「我们自己发起」的那个。乐观并发的两个令牌从 OR 改回 AND:revision 是 TW 的内存计数器(重启/拉取后复位),旧逻辑「modified 命中就放行、否则看 revision」会让跨重启的写入被判"一致"而静默覆盖人类改动,同时传两个令牌也并不更安全。配置 tiddler 的 modified 不再被钉死成 created(ensureTiddlerTimestamps 只补缺的那一半,用 now)。② 崩溃与资源泄漏:fs.watch 补上常驻 error 监听(Node 在无监听器时把 error 抛成未捕获异常 → 整个 dsh web 进程退出;Windows EPERM / Linux ENOSPC 都能触发);WikiServer.startOnce() 在 spawn 前复查 stopping(stop() 可在 await findFreePort() 期间跑完,此后 spawn 的子进程没人杀 = 孤儿 TW);知识库切换回滚失败也必须重装 extras(此前从 catch 直接返回、跳过 setupExtras(),此后所有 wiki 写入都不再被提交,而"迟到就绪"复探让 /status 看着一切正常——静默停摆);runSwitch 补 disposed 守卫(热重载/关闭期间的在途切换不再重新挂上 committer/watcher,也不在 teardown 之后再 spawn TW)。③ 行为瑕疵:escapePromptBraces 对连续花括号失效({{{name}}} 会残留 {{,而 DSH 对未知变量直接抛错、能炸掉整个系统提示词装配正是该函数存在的理由);?limit= 空值不再被夹成 1(Number('' ?? fallback) 是 0,/recent?limit= 曾只返回 1 条而不是默认 15);tiddlywiki_append 在 mode=prepend 时不再回执谎报「写进了某段落」(heading 只对 append 生效);POST /render 的超限 body 从 400 改为 413。④ 两处零成本的安全收口:代理路径判定不再只看字面量 /tiddlers/——TW core 的 get-tiddler-html 路由是单段(/^\/([^\/]+)$/),把 / 编码成 %2F 即可绕过旧守卫(本机实测:修复前 /tw/...%2Fconfig 与 /api/...%2Fconfig 都返回 200 + 715 字节配置 JSON);/render 的 text 分支也不再是旁路(TW 会解析 {{…}} 转写,实测修复前 {"text":"{{$:/plugins/dsh-tiddlywiki/config}}"} 返回 200 + 3123 字节配置正文,净化器只管标签、管不住正文),contextTitle 一并校验。守门:verify-write-policy +6 条(令牌 AND 语义 +「证据不足判冲突」+ onlyCreated 不再钉死 modified)、verify-prompt +1 条连续花括号、verify-frame-guards +4 条源码级生命周期断言(fs.watch error 监听 / spawn 前复查 stopping / 回滚 finally / runSwitch disposed)、verify-audit-fixes +4 条 E2E(删除顺序:索引读失败后原条目必须仍在、$:/ 与索引不得被软删、二进制附件拒写、?limit= 空值)、verify-conflict-config-guards +1 条真 git E2E(在真 rebase 中途 pull 失败,用户的 rebase 必须原样保留)、selftest +8 条(编码/双编码代理路径 403、/render 转写与 contextTitle 403、普通转写仍 200)。全部 15 条修复都做过反向验证:把修复还原后,新断言稳定变红。
-
v0.23.4(2026-09-18):三处硬化,全部来自 v0.23.3 上线当天暴露的真实事故。① 提交前拦冲突(数据安全):git pull --rebase --autostash 的 autostash 重新应用冲突后,rebase --abort 已无事可 abort,冲突标记留在工作树里,随后 60s 的 AutoCommitter 照常 git add -A && commit —— <<<<<<< Updated upstream 就这样被永久提交并推送进了 wiki 的配置 tiddler(0b19ca6,2026-09-17),插件随即解析不了自己的配置(微信发布一直是关的、prompt.extra 静默失效)。现在 GitFace.conflictState() 三重探测(进行中的 rebase / 未合并路径 / 工作树里残留的冲突块),commit() 直接抛 GitConflictStateError 拒绝提交;/sync 回 409 + 文件名、agent 工具返回结构化失败、AutoCommitter 去重上报一次(不去刷屏);pull() 在 abort 后复检(autostash 形态正是 abort 之后才暴露);/status 与设置页状态行显示「冲突未解决(N 个文件,已阻止提交)」。⚠️ MERGE_HEAD/CHERRY_PICK_HEAD 故意不硬拒——git commit 正是收尾它们的动作(真 git E2E 抓出了这个设计错误)。② 配置解析失败拒绝写(数据丢失):ConfigStore.set() 在存量 tiddler 解析失败时,原先拿空缓存合并 → 一次设置页保存就把配置写成只剩表单里那几个字段的"残版"(18 日 13:38 实测:prompt.extra/git.remote/ui.* 全丢)。现在抛 ConfigUnreadableError、不 PUT,并把原因经 /admin/state 的 configError 渲染成设置页顶部红色横幅;瞬时读失败仍走"合并到缓存"(回归断言守住)。③ adapter 版本校验:/wechat/ready 原先只数文件在不在,旧版 adapter(不认识 --title-file、标题仍是必填位置参数)也会报"就绪",点按钮才在 opencli 里报"缺少必填参数"。现在按定义判新旧(weixin-flow.js 必须 export function resolveNoteTitle(、两个入口必须声明 name: 'titleFile'——不是字符串出现,注释里提到不算),missing(不存在)与 stale(过旧)语义互斥;按钮预检与路由 503 分别说「版本过旧」和「缺少发布脚本」。bundle 升 0.2.0。守门:新增 scripts/verify-conflict-config-guards.mjs(23 条:伪 exec 纯逻辑 + 真起临时 git 仓库的冲突 E2E(真 merge 冲突 / 真残留标记,且解决后必须能提交)+ ConfigStore 拒绝写/回归/接线断言;已反向验证:拆掉 contentScan 或配置拒绝各红 2 条),verify-wechat-publish.mjs 17→24 条(+注释里提及新符号必须判旧、真源码符号对齐),bundle 守门 +3 条。
-
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」。
-
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 残留(踩过:把残留文案当实时状态误判账号被拦,实际账号正常)。
-
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 两行表格。
-
v0.23.0(2026-09-17):新增「发布到微信公众号」+ 发布元数据(tools/wechat/,可选功能,默认关闭,需额外安装,不动 host 代码)。把 wiki 笔记一键发到公众号草稿箱,可选点发表。
⚠️ 可选功能,默认关:真正干活的是仓库 tools/wechat/ 下的 opencli adapter + Browser Bridge 浏览器扩展——插件本体不含它,也不会替你装。开关 wechat.enabled 默认 false:关闭时不注入任何发布相关提示词、也不往 wiki 写「发布元数据规范」文档;打开后仅多这两项,不会安装任何东西、不启用任何后台服务。不装/不开它,插件其他功能完全不受影响。开启方式:设置 →「TiddlyWiki 知识库」→「可选功能:微信公众号发布」→ 勾选 → 保存配置。完整安装步骤(opencli、浏览器扩展、扫码登录)与排错见 docs/wechat-publish-setup.md。
为什么不用官方 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 驱动后台填表/写正文/传图/设封面/存草稿。
发布元数据(本轮追加,解决「哪些发过 / 哪些不能发」):每篇笔记用自定义字段记录对外发布状态——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 回写。
四个实测踩坑:① 必须带 --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 符号原样进库)。发表需管理员扫码——微信的账号安全机制,无法自动化,「一键」的真实含义是「脚本做完排版/上传/填表,你只扫一次码」;默认走发表(不推送粉丝、不占群发额度)而非群发。
命令: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 笔记「公众号发布插件调研」。
-
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 才落盘)。
-
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 条。
-
v0.22.8(2026-09-15):第六轮代码审计修复——host 与 client 各做一份只读审计,逐条核实后修 10 处真缺陷,并清掉一轮重复实现与死代码。① 数据丢失(P0):/edit 仍会把「调用方文本或笔记已保存正文」PUT 进刚复用的那个草稿,而那个草稿里装的是用户在 TW 原生编辑器里敲的未保存内容——触发路径极常见:打开快速笔记(标题精确到分钟)→ 写 → 关掉 → 同一分钟内再打开 → 草稿被覆盖。现在只有我们创建的草稿才由我们填充,复用的仅在调用方显式给了文本时才写。② 设置页超时(P0):fetchJson 展开 init 后又硬编码 signal,于是「知识库切换 / 恢复默认 / 重启 TW」的 120s 预算被 15s 掐断——宿主其实切成功了,用户却看到「切换失败」(大 wiki 冷启动实测 44s+),且重试会再走一遍停/启。③ 净化器崩服务:� 这类越界数字实体会让 String.fromCodePoint() 抛 RangeError,而净化器没有 try——一条笔记正文就能让 POST /render 502,打坏回复流工具卡与会话「知识库」Tab。④ 「幽灵草稿」:保存/「丢弃」后重置编辑器会触发防抖,把「空正文 + 新时间戳标题」又写成草稿,下次打开误报「已恢复未保存草稿」并跳过默认 tag。⑤ /recent 一直丢掉 since(工具与卡片都发它),于是模型看到过滤结果、人看到未过滤列表——与 v0.20.0 修过的 /search 同类漂移。⑥ 隐藏的 TW 面板会因为一个在途 /status 响应重新起一轮 30×1.5s 轮询。⑦ /tw 代理补齐显式方法白名单。⑧ /render 的 404 改为结构化异常(原先按错误消息文本正则,改词即把「不存在」变 502)。⑨ /get 改用共享的 isBlockedProxyTitle()(原为硬编码字面量副本)。⑩ 侧边栏显示名改完设置即时生效(原只挂载时读一次)。去重/死代码:新增 render-fetch.ts(唯一渲染调用)、describeSyncResult()(同步回执)、formatLocalMinute(),ui-config.ts 承担全部 ui.* 投影;删除生产零调用的 ensureTwWebHost()(与 tw-web-host seed 重复)与 listTags(),多个仅供模块内部使用的 export 与「算了却从不显示」的字段一并清除。守门:verify-render-sanitizer.mjs +1、verify-frame-guards.mjs +3、verify-audit-fixes.mjs +3(草稿覆盖那条已反向验证:还原修复即红)。
-
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)。
-
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 + 真实卡片结构对比修复前后的按钮排版。
-
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 的重启断言(窗口热改 + 夹取 + 仍判就绪)。
-
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 的失败属环境问题(手动复核过断言全过),与本版无关。
-
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() 走真实函数(前缀 / 裸名 / 缺失 / 读失败),静态断言会剥掉注释行以免匹配到自己的说明文字。
-
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:{...}} 对象,导致「发布包内容」检查假失败(与本次改动无关,但会卡住发布流程)。
-
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 已手工打上同样的两处修改)。
-
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 组内容哈希断言(新鲜 / 用户改过 / 内置更新 / 旧标记两种 / 重新初始化后刷新)。
-
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。
-
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+回收站恢复保类型)。
-
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。
-
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。
-
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 用例。
-
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。
-
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 两个坏检查、客户端泄漏与草稿回写等。
-
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——实测路由)。新增 **host 侧片段净化器**(src/host/sanitize.ts:丢 script/style/iframe/object/embed/form/svg/math…、丢on/srcdoc/srcset、URL 属性只放行 http(s)/mailto/tel/相对路径/栅格data:image/,并先做实体解码挡javascript:这类混淆),新增 **POST /dsh-tiddlywiki/render** 作为客户端唯一渲染入口(客户端不再直连 TW 的未净化/tw/render,因此**老 wiki 也立刻受保护**);配套 payload 回归scripts/verify-render-sanitizer.mjs(已进 CI)。**数据安全**:/note与/edit此前不读旧条目就整体 PUT,用同名标题保存会**清掉人类笔记的标签与自定义字段**——现在两条人类路径与 agent 工具共用新的 **src/host/write-policy.ts**(先读后写、保留字段、agent-written只给 agent 新建的条目);快速笔记卡片对「🕘 最近」载入的笔记回传 **expectedRevision**,人类在 TW 里的并发修改会让保存得到 **409** 而不是被覆盖;**重启 TW 前先排干 syncer**(/sync与工具git_sync此前漏了 flush 哨兵——刚保存的笔记可能被重启吞掉);WikiServer.restart()改为**内部单飞**(admin 路由不受 routes 层互斥保护,并发重启会留下孤儿 TW 进程);启动任务是 fire-and-forget,teardown 现在**先置disposed并等它收尾**(插件热更新/禁用时不再「disposer 先跑完、startup 后 spawn」留下孤儿进程与泄漏监听)。**工具层**:tiddlywiki_get的自定义字段此前渲染成fields=[object Object](单条 GET 的fields嵌套没摊平)——q/due/clip-url对模型完全不可见,已修;tiddlywiki_lint的死链检查把**每一条指向图片/附件的链接**都误报成死链(标题集合取自排除二进制的列表),missing-type则**永远不触发**(TW 服务端会给无 type 的条目补text/vnd.tiddlywiki,改用[!has[type]]过滤器判定),描述与checks里的large-binary从未实现,已删除;tw-api的瘦列表(/tags、lint 用)加短 TTL 缓存。**客户端**:TW 就绪重试的 40×150ms 定时器、sync-button卸载后的迟到回调、injectStyles的共享
-
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 后新会话生效;客户端改动需刷新页面。
-
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 不再默认打作者本机端口。
-
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 规避冲突。
-
v0.16.28(2026-09-10):修:拖拽版剪藏书签在知乎等真实页面报 SyntaxError: Unexpected token ';'。根因(真实 Chrome 实验证实):0.16.27 把书签 href 做 HTML 实体转义("/&/<),但浏览器的 a.href 不做实体还原——拖拽成书签后存的就是实体文本,执行时即解析失败。修复:href 改为 percent-encoding(CLIP_BRIDGE_DRAG_HREF,属性里只剩 %XX,天然无需任何属性转义);Chrome 执行 javascript: URL 前会 percent 解码(实测通过)。浏览器 E2E 升级为直接以编码 href 走 location.href 执行(完全等价拖拽书签的执行路径)并断言浮层弹出;静态验收改为 decodeURIComponent(href) === 代码常量 一致性校验。行为不变——已拖过旧版书签的用户需重新初始化 seed 后重新拖一次(旧书签仍然坏的,因为里面的代码是实体文本)。
-
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 异常)。
-
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 文档)修复。
-
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 项全过)。
-
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。
-
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 自动跳过。更新后刷新页面即可看到入口。
-
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])。
-
v0.16.21(2026-09-09):新:DSH 右侧边栏(rightbar)集成。注册 TiddlyWiki 为右侧栏 tab 类型——rightbar 首页出现「TiddlyWiki 知识库」入口盒,点击即在右侧栏以 tab 形式打开完整 TW 编辑器(同源代理 iframe,跟随 DSH 主题),与聊天并排;配置 ui.showRightbarTab(默认 true)可开关。更新后刷新页面即可看到入口。
-
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。
-
v0.16.19(2026-09-07):修:会话「知识库」Tab 把汇总当源码显示——TW 5.4.1 把 $:/temp/ 条目一律按代码块渲染;改为 /tw/render 原生片段管线(与回复流工具卡同链路),不再用 iframe/story view。
-
v0.16.18(2026-09-07):README 全面精简重写;seed 优化——首页 seed 跟随 wiki 现状、修「所有文章」回主页死链、doc-note 文案修正。
-
v0.16.17(2026-09-07):点击快速笔记可选直达 TW 原生编辑页(ui.quickNoteMode)+ 修「在 TW 中编辑」弹窗 ✕ 关不掉。
-
v0.16.16(2026-09-07):修会话知识库 Tab 误入编辑草稿显示源码——三层防误编辑(清残留草稿/吞草稿创建/禁 ✏️)。
-
v0.16.15(2026-09-07):put/batch_put 未指定类型自动默认 text/markdown;警告 fields.type 是内容类型保留字段。
-
v0.16.14(2026-09-07):修知识库 Tab「佚失条目」根因——浏览器端 TW 天生排除 $:/temp,改为 iframe 就绪后注入 + 原生 hash 导航。
-
v0.16.11(2026-09-06):新增会话顶部「知识库」Tab(本会话相关 wiki 汇总,volatile $:/temp)。
-
v0.16.0(2026-09-05):回复流原生 TW 工具卡片 + 可点击 wiki 链接(新增 render-route seed)。
-
v0.15.0(2026-09-05):初始化不强制 + 可反初始化(seed 拆核心/可选两层)。
-
v0.14.0(2026-09-05):「发送给 Agent」按钮独立图标 + 附加说明 + 权限选择。
-
v0.13.0(2026-09-04):menubar 顶栏主题自适应。
-
v0.12.0(2026-09-04):新默认主页 + 「所有文章」两列分页页。
-
v0.11.0(2026-09-04):「发送给 Agent」支持选择工作模式(Agent 预设)。
-
v0.10.0(2026-09-04):统一 seed 注册表 + 设置页「初始化」。
-
v0.8.0(2026-09-04):嵌入式 TW 跟随 DSH 深浅主题。
-
v0.7.0(2026-09-03):TW 一键把当前笔记发送到 dsh 会话。
-
v0.6.0(2026-09-03):同源代理(远程访问核心)。
-
v0.5.0(2026-09-03):Agent 工具扩充 + 快速笔记草稿自动保存 + 统一知识库 FAB。