dsh-tiddlywiki 0.22.5 → 0.22.7
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 +11 -6
- package/lib/client.js +37 -22
- package/lib/index.js +100 -22
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/editor-popup.ts +39 -1
- package/src/client/note-widget.ts +6 -2
- package/src/client/quick-note-dock.ts +5 -2
- package/src/client/settings-page.ts +27 -11
- package/src/client/styles.ts +18 -3
- package/src/host/admin.ts +44 -9
- package/src/host/prompt.ts +60 -0
- package/src/index.ts +15 -19
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
| 📍 **知识库位置可切换** | 设置页「知识库位置」可把插件切换到**任意本地文件夹**(不用改 cordis 配置、不用重装):就地停/起 TW 子进程,目标目录没有 `tiddlywiki.info` 时自动 `--init server` 建一个全新知识库;选择记在 `$DSH_HOME/dsh-tiddlywiki/location.json`——一个**在 wiki 之外**的指针文件(所以切到新 wiki 后仍记得「我用的是哪个」),「恢复为配置默认」一键清除;**切换失败自动回滚**到原知识库并如实告知(v0.22.0) |
|
|
28
28
|
| 🔄 **seed 更新检测** | seed 标记记录**内容哈希**:内置内容在本 wiki 预置之后更新过 → 设置页显示「⬆ 有更新」;被你改过 → 显示「✏️ 本地已修改」,且「重新初始化」前**二次确认**;两者都基于哈希判定,绝不猜(旧格式标记会明确显示「尚未启用更新检测」,不会误报为「你改过」,v0.22.0) |
|
|
29
29
|
| 🎨 **自定义样式** | 「自定义样式」seed:编辑器美化 / 窄屏侧栏隐藏 / menubar 加高 / 批注弹窗等 5 张通用样式表,新 wiki 也能一键初始化(可选,v0.16.22) |
|
|
30
|
-
| 📝 **可配置的注入提示词** | 插件注入每个会话的「TiddlyWiki 持久知识库」提示词可在设置页配置:**默认精简版**(~1.7KB,只保留工具 schema 表达不了的约定——同步纪律 / 标签约定 / 链接格式),可选**完整版**(额外附一份**由工具注册表实时生成**的参数索引,不会再过期);`extra` 追加自定义规范、`override` 整段接管、可整体停用;**保存后无需重启 dsh web**(section
|
|
30
|
+
| 📝 **可配置的注入提示词** | 插件注入每个会话的「TiddlyWiki 持久知识库」提示词可在设置页配置:**默认精简版**(~1.7KB,只保留工具 schema 表达不了的约定——同步纪律 / 标签约定 / 链接格式),可选**完整版**(额外附一份**由工具注册表实时生成**的参数索引,不会再过期);`extra` 追加自定义规范、`override` 整段接管、可整体停用;**保存后无需重启 dsh web**(section 即时重注册,当前会话下一步即生效),设置页可**按表单当前值预览**即将注入的全文(未保存的形态切换也立刻可见,v0.21.0 / v0.22.7) |
|
|
31
31
|
| 🤖 **Agent 工具** | 15 个 `tiddlywiki_*` 工具:检索(**相关度排序 + 命中处片段 + 字段过滤**)、读写、**增量追加**、批量、重命名、**软删除/回收站**、**反向链接**、**附件入库**、**知识库体检**、git 同步与冲突解决(v0.19.0;检索/最近仍在**服务端**排除二进制附件,大 wiki 上从 515MB/17s 降到 ~0.4s) |
|
|
32
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
33
|
| 🧼 **渲染片段净化** | 回复流卡片与会话汇总注入的 TW 片段先经 **host 白名单净化**(丢 `iframe`/`script`/`svg`/`on*`/`javascript:`/`data:text/html` 等)——TW 自己的解析器只剥 `on*`,`<iframe src="javascript:…">` 会原样通过并在 DSH 页面里执行(v0.19.1 修复的存储型 XSS) |
|
|
@@ -122,7 +122,7 @@ dsh plugin --profile web add link:/path/to/dsh-tiddlywiki
|
|
|
122
122
|
| `prompt.extra` | 追加在末尾的自定义规范(团队 / 个人偏好),始终生效 |
|
|
123
123
|
| `prompt.override` | 非空时整段取代内置文本(`extra` 仍会追加)——想完全自写提示词时用 |
|
|
124
124
|
|
|
125
|
-
保存后**不用重启 dsh web**:host 会即时重新注册 prompt section,当前会话从**下一步**起就使用新文本(DSH 的 `system-prompt/change`
|
|
125
|
+
保存后**不用重启 dsh web**:host 会即时重新注册 prompt section,当前会话从**下一步**起就使用新文本(DSH 的 `system-prompt/change` 会更新历史里的系统消息)。设置页的「预览注入文本(按表单当前值)」按钮会把**表单里此刻的值**(含还没保存的形态切换 / extra / override / 停用开关)发给 host 实时拼成全文——所见即「保存后会注入的内容」,顶部还会标注「含未保存的修改」;预览是只读的,不会替你保存(v0.22.7 之前它只读**已保存**的配置,所以切换形态后不点保存直接预览会看到逐字节相同的旧文本,容易被读成「两种形态没差别」)。
|
|
126
126
|
|
|
127
127
|
> 为什么默认精简:v0.20.1 之前这段提示词里手抄了一份**工具参数清单**,最后一次同步停在 v0.19.0,而 v0.19.4 / v0.19.5 / v0.20.1 都改过工具参数——模型因此看到 6 处过期签名(`delete` 缺并发令牌、`append` 缺 `fields`、`attach` 缺覆盖保护、`batch_put` 缺 `overwrite`、`trash` 缺 `title`/`limit`、`list_tags` 缺 `limit`)。v0.21.0 起:默认形态不再复述参数(工具 description 才是唯一事实),`full` 形态的目录改为**运行时生成**,并新增 `scripts/verify-prompt.mjs` 守门(slim 不得出现参数清单 / full 的每个工具与每个参数都必须在场 / 两种形态都必须保留治理约定块)。
|
|
128
128
|
|
|
@@ -132,7 +132,7 @@ dsh plugin --profile web add link:/path/to/dsh-tiddlywiki
|
|
|
132
132
|
- **🧭 中央列编辑器**:侧边栏「TiddlyWiki」开关(显示名可改 `ui.sidebarLabel`)。
|
|
133
133
|
- **🗂️ 右侧边栏 Tab**(v0.16.21):DSH 新右侧栏展开后,首页会出现「**TiddlyWiki 知识库**」入口盒,点击即在右侧栏以 tab 形式打开完整 TW 编辑器——**与聊天并排**,适合边聊边查/边记。由 `ui.showRightbarTab` 控制(默认开);老版本 DSH(无右侧栏)自动跳过。
|
|
134
134
|
- **🧩 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)。
|
|
135
|
-
- **📝 快速笔记**:输入框上方快捷按钮(`ui.showQuickNoteDock`)或 FAB;`ui.quickNoteMode` 选打开方式——**native**(默认,直达 TW 原生编辑页,草稿自动续写)或 **card**(CodeMirror 6 Markdown 高亮、文件上传、多选 tag、草稿自动保存、「🕘 最近」载入、Ctrl+Enter
|
|
135
|
+
- **📝 快速笔记**:输入框上方快捷按钮(`ui.showQuickNoteDock`)或 FAB;`ui.quickNoteMode` 选打开方式——**native**(默认,直达 TW 原生编辑页,草稿自动续写)或 **card**(CodeMirror 6 Markdown 高亮、文件上传、多选 tag、草稿自动保存、「🕘 最近」载入、Ctrl+Enter 保存)。**v0.22.6**:原生编辑弹窗里用 TW 的「🗑 删除」把笔记删掉后不会再变成打不开的白板——再点一次「快速笔记」就会重新载入编辑器;card 模式底部操作条改成「按钮文字永不折行、放不下时整组换行」,窄卡片里不再把「✏️ 在 TW 中编辑」压成两行。
|
|
136
136
|
- **📌 本地剪藏桥**(可选,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` 只回元数据。
|
|
137
137
|
- **🏠 首页(初始化 home-index 后)**:待办四象限(`todo` 标签 + `q` 字段拖放分类)+ 快速记笔记(完整编辑器,勾选「同时加入待办」即建任务)+「所有标签 / 所有文章」入口 +「📚 插件文档」栏(自动收录所有带 `dsh-docs` 标签的 seed 文档)。
|
|
138
138
|
- **📚 会话知识库 Tab**:会话顶部 Tab(`ui.tabLabel` 改名、`ui.showSessionTab` 关闭),自动汇总本会话读写过的 wiki 笔记(写入 volatile `$:/temp`,不落盘不进 git),**TW 原生渲染**(v0.16.19 起 `/tw/render` 片段管线,与回复流工具卡同链路),链接点击直达中央 TW 面板,不可编辑。
|
|
@@ -297,7 +297,7 @@ node scripts/gen-seed-ui-styles.mjs '<wiki>/tiddlers/<样式.css>' … src/host/
|
|
|
297
297
|
| `/dsh-tiddlywiki/api/*` | any | 透传 TW 服务(JSON) |
|
|
298
298
|
| `/dsh-tiddlywiki/tw/*` | any | 同源 TW 代理(远程访问核心) |
|
|
299
299
|
| `/dsh-tiddlywiki/admin/seeds` `/run` `/remove` | GET/POST | seed 状态(v0.22.0 起含内容哈希的「有更新 / 本地已修改」)/ 运行 / 反初始化 |
|
|
300
|
-
| `/dsh-tiddlywiki/admin/prompt` | GET |
|
|
300
|
+
| `/dsh-tiddlywiki/admin/prompt` | GET/POST | 当前注入提示词全文(GET = 已保存的有效配置,v0.21.0);POST 带 `{enabled,mode,extra,override}` 则按**草稿**渲染、写入零副作用,供设置页预览未保存的表单值(v0.22.7) |
|
|
301
301
|
| `/dsh-tiddlywiki/admin/wiki/location` | GET | 当前知识库位置 + 来源(指针/配置/默认)+ 指针文件路径 + 同目录候选 wiki(v0.22.0) |
|
|
302
302
|
| `/dsh-tiddlywiki/admin/wiki/switch` `/reset` | POST | 运行时切换知识库 / 恢复为配置默认(失败自动回滚并报告,v0.22.0) |
|
|
303
303
|
|
|
@@ -308,7 +308,8 @@ src/
|
|
|
308
308
|
├── index.ts # host 入口:装配 WikiServer/路由/工具/提示词/自动 commit
|
|
309
309
|
├── sdk.ts # 自包含 defineTool + dshHomePath(零 @deepseek-ai 运行时依赖)
|
|
310
310
|
├── host/
|
|
311
|
-
│ ├── wiki.ts # WikiServer:spawn/kill
|
|
311
|
+
│ ├── wiki.ts # WikiServer:spawn/kill/自愈/端口探测/就绪轮询(策略见 ready-policy.ts)+ 迟到就绪复探;TW_PROXY_PATH 同源代理
|
|
312
|
+
│ ├── ready-policy.ts # TW 启动就绪策略(v0.22.5):软窗口 60s(可配)/ 硬上限 3× / awaitReady 纯策略(注入时钟,可单测)
|
|
312
313
|
│ ├── tw-api.ts # TiddlyWeb REST 客户端
|
|
313
314
|
│ ├── git.ts # git init/commit/pull/push/sync/status + AutoCommitter
|
|
314
315
|
│ ├── routes.ts # 全部 DSH 路由 + agent-send/create/modes + session/summary
|
|
@@ -318,7 +319,7 @@ src/
|
|
|
318
319
|
│ ├── admin.ts # 设置页后台:tiddlywiki.info 读写 + /admin/*
|
|
319
320
|
│ ├── config.ts # ConfigStore:cordis config 基底 + 配置 tiddler 覆盖层
|
|
320
321
|
│ ├── seeds.ts # 统一 seed 注册表(10 项,三层:核心/起步/可选)
|
|
321
|
-
│ ├── prompt.ts # 系统提示词(v0.21.0):slim/full 两种形态 + extra/override,full
|
|
322
|
+
│ ├── prompt.ts # 系统提示词(v0.21.0):slim/full 两种形态 + extra/override,full 的目录由工具注册表实时生成;v0.22.7 起草稿预览(normalizePromptPreview/describePrompt,与保存路径同一份拼装)
|
|
322
323
|
│ ├── seed-*.ts # 各 seed 实现(bundle/首页/ui-styles 常量脚本生成;starter-docs/menubar/clip-bridge 手工维护)
|
|
323
324
|
│ └── tools.ts # 15 个 tiddlywiki_* 工具(列表式注册)
|
|
324
325
|
└── client/ # 浏览器半部
|
|
@@ -345,6 +346,10 @@ lib/ # 预构建产物(发布含 lib/**,提交入库;
|
|
|
345
346
|
|
|
346
347
|
> 最近几个主要版本的一句话记录(完整变更见 [Releases](https://github.com/bbqisbbq/dsh-tiddlywiki/releases) / git log)。
|
|
347
348
|
|
|
349
|
+
- **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)。
|
|
350
|
+
|
|
351
|
+
- **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 + 真实卡片结构对比修复前后的按钮排版。
|
|
352
|
+
|
|
348
353
|
- **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 的重启断言(窗口热改 + 夹取 + 仍判就绪)。
|
|
349
354
|
|
|
350
355
|
- **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` 的失败属环境问题(手动复核过断言全过),与本版无关。
|