dsh-tiddlywiki 0.26.4 → 0.26.6
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/LICENSE +21 -21
- package/README.md +6 -1
- package/cordis.patch.yml +13 -13
- package/lib/index.js +148 -27
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/markdown-editor.ts +128 -128
- package/src/client/state.ts +39 -39
- package/src/client/toast.ts +22 -22
- package/src/host/tools.ts +90 -18
- package/src/host/write-policy.ts +72 -2
- package/src/index.ts +1 -0
- package/src/sdk.ts +14 -2
- package/docs/code-review-2026-09-17.md +0 -151
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 dsh-tiddlywiki contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-tiddlywiki contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
| 💾 **数据即备份** | wiki 文件夹本身就是 git 仓库,自动 commit(60s 防抖) |
|
|
56
56
|
| ⚙️ **设置页不再「假装生效」** | **v0.25.0** 把一批「保存了却不变」的项收敛掉:`git.autoCommit`/`git.debounceMs`/`git.remote` 保存后**对运行中的插件立即生效**(重建防抖提交器 + 指向新 remote;`git.branch` 只对首次初始化的仓库有意义,标签里已写明,状态行显示的是真实分支);数值项(端口 / 就绪窗口 / 分页条数 / 防抖毫秒)带 `min`/`max` 与字段旁红字,非法值**拒绝保存**而不是静默夹取;保存被拒(409,例如配置 tiddler 残留冲突标记)时错误**持久显示**而不是一闪而过的 toast;「没有改动」不再发请求、宿主也不重写配置 tiddler(每次空点保存都给 git 造 diff / 两台机器在 `.meta` 上冲突);插件/主题/语言的未应用勾选能活过「同步 / 重启」触发的重刷;表单有未保存内容时若配置在别处被改过,**保留你的输入**并给出显式提示而不是重建表单;微信发布的 `command`/`adapter`/`token`/`dsn` 补齐 UI(token 是密码型 + 显隐切换,掩码回填照旧) |
|
|
57
57
|
|
|
58
|
+
| 🧾 **`fields` 是对象,不是字符串** | **v0.26.5**:三个写工具的 `fields` 参数 schema 从 `{type:'json'}`(在 DSH 的 schema 里是**纯注解**、编译后连 `type` 都没有,模型只能猜)改成真正的 `object`;工具层 `normalizeFieldsArg()` 还会把模型发来的 **JSON 字符串解析成对象**,数组 / 解析不出的字符串则**明确报错**——不再出现「传了 `fields` 却写出 `0={`、`1="`、`2=r`… 几十个单字符垃圾字段,真正的字段一个没落」(用户实测复现两次) |
|
|
58
59
|
| 🔍 **插件状态不说谎** | **v0.26.0**:设置页「插件管理」的勾选只代表 `tiddlywiki.info` 启动清单,而 TW 运行时集合还要并上**经 TW 原生插件库装进 wiki 的插件 tiddler**、减去**在 TW 面板里被禁用的**。两边不一致时(TW 里启用着、设置页没有——或反过来),行内会显示「**wiki 内已装**」「**TW 内已禁用**」徽标,并单列只读小节「**wiki 内插件(经 TW 原生安装)**」(名称/版本/描述),启停与卸载引导回 TW 控制面板。第三方插件的安装与管理仍然完全交给 TW 原生界面 |
|
|
59
60
|
|
|
60
61
|
---
|
|
@@ -100,7 +101,7 @@ dsh plugin --profile web add link:/path/to/dsh-tiddlywiki
|
|
|
100
101
|
| `tiddlywiki_get` | 读单个 tiddler 全文(`modified` 以 ISO 返回,可直接用作 `expectedModified`) |
|
|
101
102
|
| `tiddlywiki_put` | 写/覆盖;**新建**未指定类型时默认 `text/markdown`(`$:/` 系统条目除外),**覆盖既有条目时保留原 `type`/tags/自定义字段**(v0.20.1,改类型要显式 `fields.type`);`expectedModified` + `force` 提供乐观并发保护 |
|
|
102
103
|
| `tiddlywiki_batch_put` | 批量写入(`overwrite=false` 跳过已存在;单条失败不影响其余、逐条报错;4 路并发但结果保持入参顺序) |
|
|
103
|
-
| `tiddlywiki_append` | **增量追加**:`mode=append\|prepend`、`heading=某标题`
|
|
104
|
+
| `tiddlywiki_append` | **增量追加**:`mode=append\|prepend`、`heading=某标题` 定位写入(落点 = 该标题之后、**下一个任意级别标题之前**;CRLF 行尾的笔记同样有效),写日志/批注无需读全文;标题不存在时**不报错但也不再装成功**——追加到文末并返回 `headingMatched=false` + 回执明说;与 `put` 同一套写策略(保留原 `type`/tags/自定义字段,新条目才默认 markdown),支持 `fields` 显式覆盖 |
|
|
104
105
|
| `tiddlywiki_rename` | 重命名 + 尽量同步其他条目里的引用;旧标题删除失败时如实回报「两份副本都在」 |
|
|
105
106
|
| `tiddlywiki_delete` | 删除(幂等)。默认**软删除**进 `$:/dsh-tiddlywiki/trash/`,`permanent=true` 才真删;支持 `expectedModified`/`expectedRevision`/`force` 乐观并发(读后被改动则拒绝,不把人类的新改动丢进回收站) |
|
|
106
107
|
| `tiddlywiki_trash` | 回收站:`action=list\|restore\|empty`(索引读不到或损坏时显式报错,绝不把回收站当空的重建) |
|
|
@@ -122,6 +123,8 @@ dsh plugin --profile web add link:/path/to/dsh-tiddlywiki
|
|
|
122
123
|
> 🔍 **检索/最近跳过二进制附件**(v0.16.20):`search`/`recent` 在**服务端**只取文本 tiddler(无 `type` 字段或 `text/*`),图片/音频/视频/PDF/zip 等二进制 tiddler(base64 正文)不参与检索、不出现在结果里——大 wiki(如数千张书籍扫描页图)从此从 515MB/17s 降到 ~0.4s,也不会被同名书页图刷屏。`get` 对二进制 tiddler 只返回元数据(`binary=true` + 类型/大小 + 链接),不返回 base64 正文。
|
|
123
124
|
|
|
124
125
|
> ⚠️ `fields.type` 是 TW 的**内容类型保留字段**(`text/markdown` 等),业务分类请放 `tags`,别写进 `fields.type`。
|
|
126
|
+
>
|
|
127
|
+
> 🧾 **`fields` 必须是对象**(v0.26.5):三个写工具的 `fields` 参数是 `object`(如 `{"review-after":"2026-12-22"}`),**不要传字符串**。工具层会宽容地把 JSON 字符串解析成对象(兼容旧行为),但数组 / 解析不出对象的字符串会**明确报错**——早期版本曾把字符串按字符拆成 `0={`、`1="`… 几十个垃圾字段写进条目,真正的字段反而没写进去;碰到这种历史条目只能重建(`fields` 只能覆盖、不能删除单个字段)。
|
|
125
128
|
|
|
126
129
|
> 🕰 **过期内容怎么淘汰**(v0.24.0):很多笔记是**阶段性**的(版本记录、部署步骤、排期、临时方案、一次性口令),过一段时间就不适用了。策略是**声明时效 + 人工决定,插件绝不自动删**:写这类笔记时带 `valid-until: YYYY-MM-DD`(硬过期)或 `review-after: YYYY-MM-DD`(该复查);被取代时写 `superseded-by: [[新笔记]]` 并打 `superseded` 标签,**旧笔记保留**(wiki 是 git 仓库,留着的成本几乎为零,删错的成本很高)。`tiddlywiki_lint` 的 `stale` 检查会**只报告**三类:`stale-expired`(`valid-until` 已过)、`stale-review`(`review-after` 已到)、`stale-candidates`(带版本号或 `done` 标签且长期未改动——**明确标注是候选,不是判定**)。要清理就自己用 `tiddlywiki_delete`(默认进回收站,可恢复)。
|
|
127
130
|
|
|
@@ -416,6 +419,8 @@ lib/ # 预构建产物(发布含 lib/**,提交入库;
|
|
|
416
419
|
|
|
417
420
|
> 最近几个主要版本的一句话记录(完整变更见 [Releases](https://github.com/bbqisbbq/dsh-tiddlywiki/releases) / git log)。
|
|
418
421
|
|
|
422
|
+
- **v0.26.6**(2026-09-26):**修 `tiddlywiki_append` 的 `heading` 在 CRLF 笔记上完全失效、且静默回退到文末**(作者本机知识库的一篇 25KB / 454 行长笔记上连续踩两次后定位到源码)。现象:对这篇笔记连做两次带 `heading` 的 append,内容**全部**落到了文末,而回执照样打印「段落「X」」——看起来完全成功。**根因**:`insertIntoSection` 用 `base.split('\n')`,而 CRLF 正文的**每一行结尾都留着 `\r`**;JS 里 `(.*)` **不匹配 `\r`**、`$` 也**不匹配 `\r` 之前的位置**,于是 `/^#{1,6}\s+(.*)$/` 对**每一个**标题都匹配失败 ⇒ `start < 0` ⇒ 走「追加到文末」的回退分支。**只在 CRLF 笔记上复现**(纯 LF 笔记一切正常),所以它看起来像偶发;落盘文件的「**376 个 CRLF + 78 个裸 LF**」混合行尾正是走过该分支的签名(`lines.join('\n')` 保留每行原有 `\r`、只把新块用 `\n` 接上)。**修复三处**:① 标题匹配前**只**剥掉行尾 `\r`(原文行尾一个字节都不动;**没有**改成 `split(/\r?\n/)`——那会让每次 append 都把整篇重写成 LF,炸出全文件 diff);② 新块按**文档自己的行尾**写入(`base.includes('\r\n')` 判定),CRLF 笔记不再越写越乱;③ `insertIntoSection` 改为返回 `{ text, matched }`,`tiddlywiki_append` 把它作为 **`headingMatched`** 一并返回,未命中时回执改为「⚠️ 未找到标题「X」,已改为追加到文末」——**不再把「定位失败」伪装成「写进了那一节」**(Agent 因此完全无法自检,这是本 bug 最坑的一点)。顺带把 `heading` 的真实落点语义写进 schema:实为「该标题之后、**下一个任意级别标题之前**」,标题后紧跟子标题时落在两者之间(=章节开头),并非 schema 原先暗示的「整节末尾」。守门:`verify-tools` +4 条 E2E(CRLF 正文的笔记必须 `headingMatched=true` 且标记落在目标段内、段内不得混入裸 LF;未命中必须 `headingMatched=false` 且回执含「未找到标题」、不得再宣称写进该段落;命中时的反向保护;wikitext `!` 标题同样可定位),**已反向验证**(撤掉 lib 里的行尾 CR 剥离即红 1 条)。
|
|
423
|
+
- **v0.26.5**(2026-09-23):**修 P0:`fields` 参数被按字符拆成几十个垃圾字段**(用户实测复现两次后报障)。现象:`tiddlywiki_put` / `batch_put` / `append` 传 `fields: {"review-after":"2026-12-22"}` 后,条目上出现 `0={`、`1="`、`2=r`… 几十个**单字符字段**,真正的自定义字段**一个都没写进去**;又因为 `fields.type` 是**改内容类型的唯一入口**,这期间也无法把条目写成 wikitext / `text/css`。**根因两层**:① 参数 schema 声明为 `{type:'json'}`,而 DSH 的 schema 里 `type:'json'` 是**纯注解**——编译后连 `type` 都没有(`src/sdk.ts` 的 `compileValue`),模型收不到「这是个对象」的信息,于是把 JSON 当**字符串**发来;② 新建笔记 + 自动工作区标记走 `withWorkspaceMark()` 的 `{ ...(fields ?? {}) }`,而**展开字符串是按字符展开**的(`{...'{"r"'}` → `{0:'{',1:'"',2:'r'…}`),这些索引键随后被当成自定义字段写入;旧 `applyCustomFields()` 对非对象又是**静默 return**,所以全程没有任何报错。**修复(三处)**:① 三个写工具的 `fields` schema 改成真正的 `object` + `additionalProperties: true`(模型能直接看到类型);② 新增 `normalizeFieldsArg()`(`write-policy.ts`)——对象原样返回、`undefined`/`null`/空串视同未给(保留基底)、**JSON 字符串解析成对象**(兼容仍按字符串发参的模型)、数组/数字/解析不出对象的字符串**明确抛错**;`defineTool` 相应新增 `normalizeArgs` 钩子,让归一化跑在 schema 预校验**之前**(放到之后会被「must be an object」先拦掉,兼容层就没机会生效);③ `applyCustomFields()` 遇到非对象**抛错**而不是静默忽略,`withWorkspaceMark()` 只为已归一化的对象做展开。守门:`verify-write-policy` +5 条纯函数(字符串解析 / 对象原样 / 三种坏输入拒绝 / 字符串在 `buildWriteTiddler` 处被拦 / 归一化后无索引键),`verify-tools` +4 条 E2E(三个工具的 schema 必须 `type:object`、按字符串发参仍写对字段且不产生 `0=` 垃圾、坏输入明确报错且不留半成品条目、`append`/`batch_put` 走同一套归一化)。**注意**:本次修复只影响写入端;已被写进条目的垃圾字段需要重建该条目(`fields` 只能覆盖、不能删除单个字段),本机知识库已扫描确认无残留。
|
|
419
424
|
- **v0.26.4**(2026-09-23):**修「发送给 Agent」因配置被浏览器自动填充而 404 + 设置页文本框防自动填充**。实测:设置页「TW 端请求基址」(`ui.sendToAgent.endpoint`)被浏览器自动完成当成 URL/账号字段填进 `/dsh-tiddlywiki/tw/root`,于是 send-to-agent 把请求拼成 `/dsh-tiddlywiki/tw/root/agent/sessions`,命中 TW 代理子路径 → 404「获取会话列表失败」。**两层加固**:① 设置页 `textField` 一律 `autocomplete=off`(`tokenField` 早已如此,URL 类文本框 `endpoint`/`git.remote`/`wechat.dsn` 等不再被自动填充污染);② send-to-agent bundle `baseEndpoint()` 校验自定义 endpoint——一旦含 `/tw/`、`/api/`、`/recipes/`、`/files/` 等 TW 子路径或非 origin/根绝对路径(判为浏览器误填),**回退到 `location.origin + "/dsh-tiddlywiki"` 自动推导**,按钮不再因误配 endpoint 而 404。另修 `scripts/gen-seed-send-to-agent.mjs` 模板字符串里一处未转义反引号(`ok:false`)导致再生成 seed 必崩的 bug。bundle send-to-agent 0.3.5→0.3.6。
|
|
420
425
|
- **v0.26.3**(2026-09-23):**修正 starter-docs 教程里一处技术性错误说法**:「方法一」原来写「点标签后 TW 会自动生成该标签的页面,列出所有带此标签的笔记」——这不准确。实测(TiddlyWiki 5.4.1 内核,已核对 `$:/config/ViewTemplateBodyFilters.multids` 与 `$:/core/ui/TagTemplate`):内核**没有**"标签页"这种自动页面,点标签丸/侧边栏标签弹出的是**下拉列表**(`list-tagged-draggable`,全量不分页),直接打开以标签命名的条目只会看到条目本身(缺失则提示),不会自动列出成员笔记。教程现在如实描述弹层行为与它的局限(不分页、不可分享链接),并指出想要「一个页面 + 可翻页」的标签列表应自行写汇总页(笔记多时套分页)。纯 seed 文案修正,不影响任何运行时行为;新装 wiki 的「示例与文档」seed 应用新文案,已有 wiki 不受影响(seed 是 ONE-SHOT)。
|
|
421
426
|
- **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 条)。
|
package/cordis.patch.yml
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# dsh-tiddlywiki bundle patch: inserts the plugin row into the web profile
|
|
2
|
-
# roster. Applied as a profile bundle layer (the `dsh.bundle.patch` manifest
|
|
3
|
-
# field) over dsh-base; activate with
|
|
4
|
-
# `dsh plugin --profile web add link:<path-to-this-package>` and restart dsh web.
|
|
5
|
-
#
|
|
6
|
-
# The row is a bare plugin by package name: the node half (exports ".") runs
|
|
7
|
-
# in the host process (WikiServer, routes, tiddlywiki_* tools, prompt section,
|
|
8
|
-
# auto-commit), and the `dsh.client` declaration in package.json makes the
|
|
9
|
-
# browser half (exports "./client", served at /plugins/dsh-tiddlywiki/client.js)
|
|
10
|
-
# load in the web GUI.
|
|
11
|
-
- insert:
|
|
12
|
-
- id: dsh-tiddlywiki
|
|
13
|
-
name: dsh-tiddlywiki
|
|
1
|
+
# dsh-tiddlywiki bundle patch: inserts the plugin row into the web profile
|
|
2
|
+
# roster. Applied as a profile bundle layer (the `dsh.bundle.patch` manifest
|
|
3
|
+
# field) over dsh-base; activate with
|
|
4
|
+
# `dsh plugin --profile web add link:<path-to-this-package>` and restart dsh web.
|
|
5
|
+
#
|
|
6
|
+
# The row is a bare plugin by package name: the node half (exports ".") runs
|
|
7
|
+
# in the host process (WikiServer, routes, tiddlywiki_* tools, prompt section,
|
|
8
|
+
# auto-commit), and the `dsh.client` declaration in package.json makes the
|
|
9
|
+
# browser half (exports "./client", served at /plugins/dsh-tiddlywiki/client.js)
|
|
10
|
+
# load in the web GUI.
|
|
11
|
+
- insert:
|
|
12
|
+
- id: dsh-tiddlywiki
|
|
13
|
+
name: dsh-tiddlywiki
|
package/lib/index.js
CHANGED
|
@@ -4345,7 +4345,7 @@ const TW_WEB_HOST_DEFAULT = "$protocol$//$host$/";
|
|
|
4345
4345
|
* (mirrored in src/client/theme-sync.ts — the two bundles cannot share code).
|
|
4346
4346
|
*/
|
|
4347
4347
|
const DARK_PALETTE_DEFAULT = "$:/palettes/CupertinoDark";
|
|
4348
|
-
function isPlainObject(value) {
|
|
4348
|
+
function isPlainObject$1(value) {
|
|
4349
4349
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4350
4350
|
}
|
|
4351
4351
|
/** Deep-merge: `over` wins; nested plain objects merge recursively. */
|
|
@@ -4353,7 +4353,7 @@ function deepMerge(base, over) {
|
|
|
4353
4353
|
const out = { ...base };
|
|
4354
4354
|
for (const [key, value] of Object.entries(over)) {
|
|
4355
4355
|
if (value === void 0) continue;
|
|
4356
|
-
if (isPlainObject(value) && isPlainObject(out[key])) out[key] = deepMerge(out[key], value);
|
|
4356
|
+
if (isPlainObject$1(value) && isPlainObject$1(out[key])) out[key] = deepMerge(out[key], value);
|
|
4357
4357
|
else out[key] = value;
|
|
4358
4358
|
}
|
|
4359
4359
|
return out;
|
|
@@ -4468,13 +4468,13 @@ const FORBIDDEN_PATCH_KEYS = /* @__PURE__ */ new Set([
|
|
|
4468
4468
|
* - `__proto__` / `constructor` / `prototype` anywhere → rejected.
|
|
4469
4469
|
*/
|
|
4470
4470
|
function normalizeConfigPatch(input) {
|
|
4471
|
-
if (!isPlainObject(input)) throw new ConfigPatchError("配置补丁必须是一个 JSON 对象(例如 {\"note\":{\"tag\":\"inbox\"}})");
|
|
4471
|
+
if (!isPlainObject$1(input)) throw new ConfigPatchError("配置补丁必须是一个 JSON 对象(例如 {\"note\":{\"tag\":\"inbox\"}})");
|
|
4472
4472
|
const out = {};
|
|
4473
4473
|
const walk = (source, target, prefix) => {
|
|
4474
4474
|
for (const [key, value] of Object.entries(source)) {
|
|
4475
4475
|
if (FORBIDDEN_PATCH_KEYS.has(key)) throw new ConfigPatchError(`配置补丁里不允许出现键「${key}」`);
|
|
4476
4476
|
const path = prefix.length > 0 ? `${prefix}.${key}` : key;
|
|
4477
|
-
if (isPlainObject(value)) {
|
|
4477
|
+
if (isPlainObject$1(value)) {
|
|
4478
4478
|
const child = {};
|
|
4479
4479
|
target[key] = child;
|
|
4480
4480
|
walk(value, child, path);
|
|
@@ -4561,7 +4561,7 @@ var ConfigStore = class {
|
|
|
4561
4561
|
const raw = typeof tiddler.text === "string" ? tiddler.text : "";
|
|
4562
4562
|
try {
|
|
4563
4563
|
const parsed = JSON.parse(raw);
|
|
4564
|
-
if (!isPlainObject(parsed)) throw new Error("config tiddler is not a JSON object");
|
|
4564
|
+
if (!isPlainObject$1(parsed)) throw new Error("config tiddler is not a JSON object");
|
|
4565
4565
|
this.overrides = parsed;
|
|
4566
4566
|
this.lastParseError = void 0;
|
|
4567
4567
|
} catch {
|
|
@@ -4588,7 +4588,7 @@ var ConfigStore = class {
|
|
|
4588
4588
|
if (tiddler !== void 0 && typeof tiddler.text === "string") {
|
|
4589
4589
|
storedText = tiddler.text;
|
|
4590
4590
|
const parsed = JSON.parse(tiddler.text);
|
|
4591
|
-
if (!isPlainObject(parsed)) throw new Error("config tiddler is not a JSON object");
|
|
4591
|
+
if (!isPlainObject$1(parsed)) throw new Error("config tiddler is not a JSON object");
|
|
4592
4592
|
stored = parsed;
|
|
4593
4593
|
}
|
|
4594
4594
|
if (tiddler !== void 0 && typeof tiddler.created === "string" && tiddler.created.trim().length > 0) existingCreated = tiddler.created;
|
|
@@ -5406,15 +5406,71 @@ function cleanTiddler(t) {
|
|
|
5406
5406
|
}
|
|
5407
5407
|
return out;
|
|
5408
5408
|
}
|
|
5409
|
+
/** 是不是一个「平铺对象」(`fields` 唯一合法的形状;数组不算)。 */
|
|
5410
|
+
function isPlainObject(value) {
|
|
5411
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5412
|
+
}
|
|
5413
|
+
/** 错误信息里的短样本(不把整段正文塞进回执)。 */
|
|
5414
|
+
function excerpt(value, max = 60) {
|
|
5415
|
+
const flat = value.replace(/\s+/g, " ").trim();
|
|
5416
|
+
return flat.length > max ? `${flat.slice(0, max)}…` : flat;
|
|
5417
|
+
}
|
|
5418
|
+
/**
|
|
5419
|
+
* 归一化调用方给的 `fields`(v0.26.5)。
|
|
5420
|
+
*
|
|
5421
|
+
* 背景(真实事故):`tiddlywiki_put`/`batch_put`/`append` 的参数 schema 曾把它声明为
|
|
5422
|
+
* `{ type: 'json' }`,而 DSH 的 schema 里 `type:'json'` 是**纯注解**——编译出来的
|
|
5423
|
+
* 节点连 `type` 都没有(见 `src/sdk.ts` 的 `compileValue`)。模型于是不知道它是对象,
|
|
5424
|
+
* 把 `{"review-after":"2026-12-22"}` 当**字符串**发来;字符串走到
|
|
5425
|
+
* `withWorkspaceMark` 的 `{ ...(fields ?? {}) }` 被按字符展开成
|
|
5426
|
+
* `{0:'{', 1:'"', 2:'r', …}`,再被当成几十个单字符字段写进条目,真正的自定义字段
|
|
5427
|
+
* 一个也没落(用户 2026-09-23 实测复现两次)。
|
|
5428
|
+
*
|
|
5429
|
+
* 这条修复同时钉两头:工具 schema 改成真正的 object(`additionalProperties: true`),
|
|
5430
|
+
* 以及在任何展开/遍历之前先在这里归一化。语义:
|
|
5431
|
+
*
|
|
5432
|
+
* | 输入 | 结果 |
|
|
5433
|
+
* |---|---|
|
|
5434
|
+
* | `undefined` / `null` / 空串 | `undefined`(保留基底,不动任何字段) |
|
|
5435
|
+
* | 平铺对象 | 原样返回 |
|
|
5436
|
+
* | JSON 字符串(对象) | 解析后使用——兼容仍按字符串发参的模型 |
|
|
5437
|
+
* | 数组 / 数字 / 布尔 / 解析不出对象的字符串 | **抛错** |
|
|
5438
|
+
*
|
|
5439
|
+
* 最后一条是刻意的:宁可给模型一条可读的报错让它重发,也**绝不**静默丢字段或把
|
|
5440
|
+
* 字符串按字符拆成垃圾字段(那正是这次事故的形态)。
|
|
5441
|
+
*/
|
|
5442
|
+
function normalizeFieldsArg(fields) {
|
|
5443
|
+
if (fields === void 0 || fields === null) return void 0;
|
|
5444
|
+
if (typeof fields === "string") {
|
|
5445
|
+
const trimmed = fields.trim();
|
|
5446
|
+
if (trimmed.length === 0) return void 0;
|
|
5447
|
+
let parsed;
|
|
5448
|
+
try {
|
|
5449
|
+
parsed = JSON.parse(trimmed);
|
|
5450
|
+
} catch {
|
|
5451
|
+
parsed = void 0;
|
|
5452
|
+
}
|
|
5453
|
+
if (isPlainObject(parsed)) return parsed;
|
|
5454
|
+
throw new Error(`fields 必须是一个对象(如 {"review-after":"2026-12-22"}),收到无法解析成对象的字符串:${excerpt(fields)}`);
|
|
5455
|
+
}
|
|
5456
|
+
if (isPlainObject(fields)) return fields;
|
|
5457
|
+
throw new Error(`fields 必须是一个对象(如 {"review-after":"2026-12-22"}),收到 ${Array.isArray(fields) ? "数组" : typeof fields}`);
|
|
5458
|
+
}
|
|
5409
5459
|
/**
|
|
5410
5460
|
* 合并调用方显式提供的自定义字段(跳过保留字段与 undefined)。
|
|
5411
5461
|
*
|
|
5412
5462
|
* 模块私有(v0.22.8):它只被 buildWriteTiddler 调用,导出会让「写策略只有一个
|
|
5413
5463
|
* 入口」这条约定出现第二个可绕过的门。`verify-write-policy.mjs` 断的是
|
|
5414
5464
|
* `buildWriteTiddler`/`cleanTiddler` 的对外行为,不需要这个内部步骤。
|
|
5465
|
+
*
|
|
5466
|
+
* v0.26.5:非对象一律**抛错**,不再静默 return。旧实现的静默 return 让模型看到
|
|
5467
|
+
* 「字段没写进去」却收不到任何错误(事故的第二个症状);配合 `withWorkspaceMark`
|
|
5468
|
+
* 的字符串展开,才有了「几十个单字符字段」那一步。现在任何绕过
|
|
5469
|
+
* `normalizeFieldsArg` 的调用方都会在这里被拦住,而不是往人类笔记上写垃圾。
|
|
5415
5470
|
*/
|
|
5416
5471
|
function applyCustomFields(tiddler, fields) {
|
|
5417
|
-
if (fields === void 0 || fields === null
|
|
5472
|
+
if (fields === void 0 || fields === null) return;
|
|
5473
|
+
if (!isPlainObject(fields)) throw new Error(`内部错误:fields 必须是对象,收到 ${Array.isArray(fields) ? "数组" : typeof fields}`);
|
|
5418
5474
|
for (const [key, value] of Object.entries(fields)) {
|
|
5419
5475
|
if (RESERVED_TIDDLER_FIELDS.has(key)) continue;
|
|
5420
5476
|
if (value === void 0) continue;
|
|
@@ -5700,9 +5756,10 @@ function defineTool(options) {
|
|
|
5700
5756
|
}
|
|
5701
5757
|
},
|
|
5702
5758
|
async execute(args, exec) {
|
|
5703
|
-
const
|
|
5759
|
+
const normalized = options.normalizeArgs !== void 0 ? options.normalizeArgs(args) : args;
|
|
5760
|
+
const violations = validateValue(parameters, normalized, "arguments");
|
|
5704
5761
|
if (violations.length > 0) throw new Error(`Error: invalid arguments: ${violations.join("; ")}`);
|
|
5705
|
-
return userExecute(
|
|
5762
|
+
return userExecute(normalized, exec);
|
|
5706
5763
|
}
|
|
5707
5764
|
};
|
|
5708
5765
|
}
|
|
@@ -9949,11 +10006,35 @@ function workspaceMarkFor(deps, exec) {
|
|
|
9949
10006
|
return workspaceMarkFromCwd(deps.workspaceName(sessionId));
|
|
9950
10007
|
}
|
|
9951
10008
|
/**
|
|
10009
|
+
* 工具参数的 `fields` 归一化(v0.26.5,见 `normalizeFieldsArg`)。
|
|
10010
|
+
*
|
|
10011
|
+
* 历史:`fields` 的参数 schema 曾是 `{ type: 'json' }`——编译后是**纯注解**,
|
|
10012
|
+
* 模型收不到「这是个对象」的信息,于是把 JSON 当字符串发来,被
|
|
10013
|
+
* `withWorkspaceMark` 的 `{ ...fields }` 拆成单字符字段。schema 已改成 object;
|
|
10014
|
+
* 这里再兜一层,字符串能解析成对象就直接用,不必让模型重发一次。
|
|
10015
|
+
* 解析不出对象 → 抛错(绝不静默丢字段,也不按字符展开)。
|
|
10016
|
+
*/
|
|
10017
|
+
function normalizeFieldsInArgs(args) {
|
|
10018
|
+
if (!("fields" in args)) return args;
|
|
10019
|
+
return {
|
|
10020
|
+
...args,
|
|
10021
|
+
fields: normalizeFieldsArg(args.fields)
|
|
10022
|
+
};
|
|
10023
|
+
}
|
|
10024
|
+
/** batch_put 专用:逐条目归一化 `fields`(非对象条目原样留给逐条校验报错)。 */
|
|
10025
|
+
function normalizeBatchItemsFields(items) {
|
|
10026
|
+
if (!Array.isArray(items)) return items;
|
|
10027
|
+
return items.map((item) => item !== null && typeof item === "object" && !Array.isArray(item) ? normalizeFieldsInArgs(item) : item);
|
|
10028
|
+
}
|
|
10029
|
+
/**
|
|
9952
10030
|
* Merge the workspace marker into a NEW note's explicit tags and fields.
|
|
9953
10031
|
*
|
|
9954
10032
|
* ADDITIVE, never replacing: the caller's tags are kept in front and the caller's
|
|
9955
10033
|
* own `workspace` field (if it set one) wins, so an explicit value is never
|
|
9956
10034
|
* silently overwritten. Returns the inputs untouched when there is no marker.
|
|
10035
|
+
*
|
|
10036
|
+
* ⚠️ `fields` 必须已经过 `normalizeFieldsArg()`(v0.26.5):下面这行 `{ ...fields }`
|
|
10037
|
+
* 对字符串会按字符展开成 `{0:'{',1:'"',…}`——那正是 fields bug 的现场。
|
|
9957
10038
|
*/
|
|
9958
10039
|
function withWorkspaceMark(mark, tags, fields) {
|
|
9959
10040
|
if (mark === void 0) return {
|
|
@@ -10235,22 +10316,44 @@ function rewriteRefs(text, oldTitle, newTitle) {
|
|
|
10235
10316
|
};
|
|
10236
10317
|
}
|
|
10237
10318
|
/**
|
|
10238
|
-
* Insert `addition`
|
|
10239
|
-
*
|
|
10240
|
-
*
|
|
10241
|
-
*
|
|
10319
|
+
* Insert `addition` into the section introduced by `heading` (Markdown `#`/`##`…
|
|
10320
|
+
* or wikitext `!`/`!!`…). Used by tiddlywiki_append so an agent can add to one
|
|
10321
|
+
* section of a long note without rewriting the whole file.
|
|
10322
|
+
*
|
|
10323
|
+
* **落点语义**:插在「该标题之后、**下一个任意级别的标题**之前」。所以当标题后面
|
|
10324
|
+
* 紧跟子标题时(`## 4. 定时任务` + `### 槽位表`),内容落在两者**之间**,也就是
|
|
10325
|
+
* 「章节开头」而不是「整节末尾」——这一点以前只在 schema 里写成「段落的末尾」,
|
|
10326
|
+
* 容易让调用方误会(v0.26.6 起描述与回执都写清楚了)。
|
|
10327
|
+
*
|
|
10328
|
+
* **返回 `matched`**:`false` = 没找到该标题,文本被追加到**文末**。调用方**必须**
|
|
10329
|
+
* 把它透出去(v0.26.6 之前这个回退是静默的,回执照样打「段落「X」」,
|
|
10330
|
+
* 于是一次定位失败看起来像成功——Agent 没有任何办法自检)。
|
|
10331
|
+
*
|
|
10332
|
+
* **CRLF(v0.26.6 修复)**:tiddler 正文可能是 CRLF(Windows 上写的/同步来的笔记),
|
|
10333
|
+
* 而 `split('\n')` 会让每行结尾都留着 `\r`。JS 里 `(.*)` 不匹配 `\r`、`$` 也不匹配
|
|
10334
|
+
* `\r` 之前的位置,于是 `/^#{1,6}\s+(.*)$/` 对**每一个**标题都匹配失败 ⇒ 所有带
|
|
10335
|
+
* `heading` 的 append 都静默落到了文末。**只在 CRLF 笔记上复现**(纯 LF 笔记一切正常),
|
|
10336
|
+
* 这就是它像「偶发」的原因。现在只在**匹配时**剥掉行尾 CR,原文行尾一个字节都不动;
|
|
10337
|
+
* 并且新块用**文档自己的行尾**写入,避免 CRLF 笔记被越写越乱(混入 LF 块)。
|
|
10242
10338
|
*/
|
|
10243
10339
|
function insertIntoSection(base, heading, addition) {
|
|
10244
10340
|
const lines = base.split("\n");
|
|
10245
10341
|
const headingText = (line) => {
|
|
10246
|
-
const
|
|
10342
|
+
const s = line.replace(/\r$/, "");
|
|
10343
|
+
const md = /^#{1,6}\s+(.*)$/.exec(s);
|
|
10247
10344
|
if (md !== null) return (md[1] ?? "").trim();
|
|
10248
|
-
const tw = /^!{1,6}\s*(.*)$/.exec(
|
|
10345
|
+
const tw = /^!{1,6}\s*(.*)$/.exec(s);
|
|
10249
10346
|
if (tw !== null) return (tw[1] ?? "").trim();
|
|
10250
10347
|
return null;
|
|
10251
10348
|
};
|
|
10252
10349
|
const start = lines.findIndex((line) => headingText(line) === heading);
|
|
10253
|
-
|
|
10350
|
+
const eol = base.includes("\r\n") ? "\r\n" : "\n";
|
|
10351
|
+
const adapt = (s) => eol === "\n" ? s.replace(/\r\n/g, "\n") : s.replace(/\r\n/g, "\n").split("\n").join("\r\n");
|
|
10352
|
+
const block = adapt(addition);
|
|
10353
|
+
if (start < 0) return {
|
|
10354
|
+
text: base.trim().length === 0 ? block : `${base.replace(/\s+$/, "")}${eol}${eol}${block}`,
|
|
10355
|
+
matched: false
|
|
10356
|
+
};
|
|
10254
10357
|
let end = lines.length;
|
|
10255
10358
|
for (let i = start + 1; i < lines.length; i++) if (headingText(lines[i] ?? "") !== null) {
|
|
10256
10359
|
end = i;
|
|
@@ -10258,8 +10361,11 @@ function insertIntoSection(base, heading, addition) {
|
|
|
10258
10361
|
}
|
|
10259
10362
|
const before = lines.slice(0, end).join("\n").replace(/\s+$/, "");
|
|
10260
10363
|
const after = lines.slice(end).join("\n");
|
|
10261
|
-
const head = `${before}
|
|
10262
|
-
return
|
|
10364
|
+
const head = `${before}${eol}${eol}${block}`;
|
|
10365
|
+
return {
|
|
10366
|
+
text: after.trim().length === 0 ? head : `${head}${eol}${eol}${after.replace(/^\s+/, "")}`,
|
|
10367
|
+
matched: true
|
|
10368
|
+
};
|
|
10263
10369
|
}
|
|
10264
10370
|
function registerTiddlywikiTools(ctx, deps) {
|
|
10265
10371
|
const disposers = [];
|
|
@@ -10538,7 +10644,8 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
10538
10644
|
description: "标签数组(可选)。不传 = 保留既有条目的原标签;传空数组 [] = 清空全部标签;有内容 = 整体替换"
|
|
10539
10645
|
},
|
|
10540
10646
|
fields: {
|
|
10541
|
-
type: "
|
|
10647
|
+
type: "object",
|
|
10648
|
+
additionalProperties: true,
|
|
10542
10649
|
description: "附加自定义字段,如 {\"date\":\"2026-09-02\"}(可选)。fields.type 是**改内容类型的正规入口**(如 {\"type\":\"text/css\"}):新建条目未指定时默认 text/markdown,覆盖既有条目时保留原类型。注意不要把业务分类值(如 \"meeting\")写进 type——业务分类请放 tags"
|
|
10543
10650
|
},
|
|
10544
10651
|
expectedModified: {
|
|
@@ -10554,6 +10661,7 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
10554
10661
|
description: "可选:true 时忽略 expectedModified/expectedRevision 强制覆盖(默认 false)"
|
|
10555
10662
|
}
|
|
10556
10663
|
},
|
|
10664
|
+
normalizeArgs: normalizeFieldsInArgs,
|
|
10557
10665
|
output: { render: (_args, value) => {
|
|
10558
10666
|
const lines = [`已写入 tiddler「${value.title}」`];
|
|
10559
10667
|
if (value.workspace !== void 0) lines.push(`已自动标记工作区: ws/${value.workspace}(字段 ${WORKSPACE_FIELD})`);
|
|
@@ -10631,7 +10739,8 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
10631
10739
|
description: "标签数组(可选)。不传 = 保留原标签;传空数组 [] = 清空全部标签"
|
|
10632
10740
|
},
|
|
10633
10741
|
fields: {
|
|
10634
|
-
type: "
|
|
10742
|
+
type: "object",
|
|
10743
|
+
additionalProperties: true,
|
|
10635
10744
|
description: "附加自定义字段(可选)。注意:fields.type 是 TW 内容类型(保留字段,默认已自动补 text/markdown),不要写业务分类值"
|
|
10636
10745
|
},
|
|
10637
10746
|
expectedModified: {
|
|
@@ -10646,6 +10755,10 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
10646
10755
|
description: "可选:true=覆盖同名(默认),false=跳过已存在的标题"
|
|
10647
10756
|
}
|
|
10648
10757
|
},
|
|
10758
|
+
normalizeArgs: (args) => ({
|
|
10759
|
+
...args,
|
|
10760
|
+
items: normalizeBatchItemsFields(args.items)
|
|
10761
|
+
}),
|
|
10649
10762
|
output: { render: (_args, value) => {
|
|
10650
10763
|
const lines = [`批量写入完成:成功 ${value.written},跳过 ${value.skipped},失败 ${value.failed},共 ${value.items.length} 条。`];
|
|
10651
10764
|
for (const r of value.items) {
|
|
@@ -11022,7 +11135,7 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
11022
11135
|
}));
|
|
11023
11136
|
register(defineTool({
|
|
11024
11137
|
name: "tiddlywiki_append",
|
|
11025
|
-
description: "向已有 tiddler 追加/前插文本,或写入指定标题段落的末尾(无需先读全文、不会整篇覆盖)——适合日志、批注、清单的增量写入。条目不存在时默认新建(createIfMissing=false 则报错)。**写入既有条目走与 tiddlywiki_put 同一套写策略**:原有 tags、自定义字段与**内容类型**全部保留(不会把 Markdown 笔记改成 wikitext、也不会把 CSS 改成 Markdown);可用 fields 显式覆盖字段/类型。新建条目未指定内容类型时才默认 text/markdown(并像 tiddlywiki_put 一样自动补 agent-written 与 `ws/<项目名>` 工作区标记)。⚠️ 本工具内部是「读全文 → 整篇回写」,属于覆盖写入:改前请先 `tiddlywiki_get`,并把读到的 `modified` 作为 `expectedModified` 传回(v0.25.0 起支持),否则人类在 TW
|
|
11138
|
+
description: "向已有 tiddler 追加/前插文本,或写入指定标题段落的末尾(无需先读全文、不会整篇覆盖)——适合日志、批注、清单的增量写入。条目不存在时默认新建(createIfMissing=false 则报错)。**写入既有条目走与 tiddlywiki_put 同一套写策略**:原有 tags、自定义字段与**内容类型**全部保留(不会把 Markdown 笔记改成 wikitext、也不会把 CSS 改成 Markdown);可用 fields 显式覆盖字段/类型。新建条目未指定内容类型时才默认 text/markdown(并像 tiddlywiki_put 一样自动补 agent-written 与 `ws/<项目名>` 工作区标记)。⚠️ 本工具内部是「读全文 → 整篇回写」,属于覆盖写入:改前请先 `tiddlywiki_get`,并把读到的 `modified` 作为 `expectedModified` 传回(v0.25.0 起支持),否则人类在 TW 编辑器里的并发修改会被静默回滚。带 `heading` 时返回 `headingMatched`:`false` = 没找到该标题、文本已追加到**文末**(CRLF 行尾的笔记也能正确定位)。",
|
|
11026
11139
|
parameters: {
|
|
11027
11140
|
title: {
|
|
11028
11141
|
type: "string",
|
|
@@ -11041,7 +11154,7 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
11041
11154
|
},
|
|
11042
11155
|
heading: {
|
|
11043
11156
|
type: "string",
|
|
11044
|
-
description: "可选:append
|
|
11157
|
+
description: "可选:append 时改为写入指定标题的位置(Markdown # 或 wikitext ! 标题,按标题文本匹配、忽略标题级别)。**落点 = 该标题之后、下一个任意级别标题之前**:若该标题后紧跟子标题,内容插在两者之间(即章节开头),**不是整节末尾**。标题不存在时不报错,改为追加到文末,并在回执与返回的 headingMatched=false 里明确说明(别把「没命中」当「写进去了」)"
|
|
11045
11158
|
},
|
|
11046
11159
|
createIfMissing: {
|
|
11047
11160
|
type: "boolean",
|
|
@@ -11053,7 +11166,8 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
11053
11166
|
description: "可选:标签(不传则保留既有条目的原标签,传空数组 [] 表示清空全部标签;新建条目会额外自动补 agent-written)"
|
|
11054
11167
|
},
|
|
11055
11168
|
fields: {
|
|
11056
|
-
type: "
|
|
11169
|
+
type: "object",
|
|
11170
|
+
additionalProperties: true,
|
|
11057
11171
|
description: "可选:显式覆盖的自定义字段(如 {\"type\":\"text/css\"})。不传则保留既有条目的原字段与内容类型"
|
|
11058
11172
|
},
|
|
11059
11173
|
expectedModified: {
|
|
@@ -11069,8 +11183,10 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
11069
11183
|
description: "可选:true 时忽略 expectedModified/expectedRevision 强制写入(默认 false)"
|
|
11070
11184
|
}
|
|
11071
11185
|
},
|
|
11186
|
+
normalizeArgs: normalizeFieldsInArgs,
|
|
11072
11187
|
output: { render: (_args, value) => {
|
|
11073
|
-
const
|
|
11188
|
+
const headingNote = value.heading === null ? "" : value.headingMatched === false ? ` · ⚠️ 未找到标题「${value.heading}」,已改为追加到文末` : ` · 段落「${value.heading}」`;
|
|
11189
|
+
const lines = [`${value.created ? "已新建并写入" : "已增量写入"} tiddler「${value.title}」(${value.mode}${headingNote}):新增 ${value.added} 字符,现共 ${value.total} 字符。`];
|
|
11074
11190
|
if (value.type !== null) lines.push(`类型: ${value.type}${value.typeDefaulted === true ? "(新建且未指定,已默认 markdown)" : ""}`);
|
|
11075
11191
|
if (value.typeChanged !== void 0) lines.push(`⚠️ 内容类型已从 ${value.typeChanged.from} 改为 ${value.typeChanged.to}`);
|
|
11076
11192
|
if (value.workspace !== void 0) lines.push(`已自动标记工作区: ws/${value.workspace}(字段 ${WORKSPACE_FIELD})`);
|
|
@@ -11094,8 +11210,12 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
11094
11210
|
const base = existing?.text ?? "";
|
|
11095
11211
|
const addition = args.text;
|
|
11096
11212
|
let next;
|
|
11097
|
-
|
|
11098
|
-
|
|
11213
|
+
let headingMatched;
|
|
11214
|
+
if (mode === "append" && typeof args.heading === "string" && args.heading.trim().length > 0) {
|
|
11215
|
+
const placed = insertIntoSection(base, args.heading.trim(), addition);
|
|
11216
|
+
next = placed.text;
|
|
11217
|
+
headingMatched = placed.matched;
|
|
11218
|
+
} else if (mode === "prepend") next = base.trim().length === 0 ? addition : `${addition}\n\n${base}`;
|
|
11099
11219
|
else next = base.trim().length === 0 ? addition : `${base.replace(/\s+$/, "")}\n\n${addition}`;
|
|
11100
11220
|
const appendMarked = existing === void 0 && !title.startsWith("$:/") ? withWorkspaceMark(workspaceMarkFor(deps, exec), normalizeTagArg(args.tags), args.fields) : {
|
|
11101
11221
|
tags: normalizeTagArg(args.tags),
|
|
@@ -11113,6 +11233,7 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
11113
11233
|
title,
|
|
11114
11234
|
mode,
|
|
11115
11235
|
heading: mode === "append" && typeof args.heading === "string" && args.heading.trim().length > 0 ? args.heading.trim() : null,
|
|
11236
|
+
...headingMatched !== void 0 ? { headingMatched } : {},
|
|
11116
11237
|
created: existing === void 0,
|
|
11117
11238
|
added: addition.length,
|
|
11118
11239
|
total: next.length,
|
|
@@ -12698,6 +12819,6 @@ function apply(ctx, rawConfig = {}) {
|
|
|
12698
12819
|
}, "dsh-tiddlywiki: host teardown");
|
|
12699
12820
|
}
|
|
12700
12821
|
//#endregion
|
|
12701
|
-
export { AGENT_WRITTEN_TAG, ALL_ARTICLES_MARKER_TITLE, ALL_ARTICLES_TEXT, ALL_ARTICLES_TITLE, ANON_USERNAME, AutoCommitter, CLIP_BRIDGE_BOOKMARKLET, CLIP_BRIDGE_DEFAULT_PORT, CLIP_BRIDGE_DOC_TEXT, CLIP_BRIDGE_DOC_TITLE, CLIP_BRIDGE_DRAG_HREF, CLIP_BRIDGE_MARKER_TITLE, ClipBridge, ConfigPatchError, ConfigStore, ConfigUnreadableError, DEFAULT_NOTE_TYPE, DEFAULT_PROMPT_MODE, DEFAULT_WECHAT_ADAPTER, DEFAULT_WECHAT_COMMAND, DOC_NOTE_TAG, DOC_NOTE_TEXT, DOC_NOTE_TITLE, DSH_DOCS_TAG, GitConflictStateError, GitFace, HOME_DEFAULT_TIDDLERS, HOME_INDEX_ITEMS, HOME_INDEX_MARKER_TITLE, HUMAN_EDITED_TAG, LOCATION_STATE_VERSION, MARKDOWN_PLUGIN, MASKED_SECRET, MENUBAR_THEME_MARKER_TITLE, MENUBAR_THEME_TEXT, MENUBAR_THEME_TIDDLER, MISSING_TYPE_FILTER, PATH_PREFIX, PROMPT_GOVERNANCE_BLOCKS, PROMPT_MODES, PROMPT_SECTION_NAME, PROMPT_SECTION_ORDER, READY_HARD_FACTOR, READY_POLL_MS, READY_SLOW_POLL_MS, READY_TIMEOUT_DEFAULT_MS, READY_TIMEOUT_MAX_MS, READY_TIMEOUT_MIN_MS, RENDER_BUNDLE_TEXT, RENDER_MARKER_TITLE, RENDER_PLUGIN_FILE, RENDER_PLUGIN_TITLE, SEED_DEFS, SEED_MARKER_VERSION, SEND_TO_AGENT_BUNDLE_TEXT, SEND_TO_AGENT_MARKER_TITLE, SEND_TO_AGENT_PLUGIN_TITLE, SESSION_SUMMARY_PREFIX, STARTER_DOCS_ITEMS, STARTER_DOCS_MARKER_TITLE, TEXT_LIST_FILTER, TRASH_INDEX_TITLE, TRASH_PREFIX, TW_PROXY_PATH, TW_PROXY_PREFIX, TW_WEB_HOST_DEFAULT, TW_WEB_HOST_TIDDLER, TiddlyWebClient, TrashIndexUnavailableError, UI_STYLES_MARKER_TITLE, UI_STYLE_ITEMS, WECHAT_ADAPTERS, WECHAT_ADAPTER_FILES, WECHAT_ADAPTER_MARKER, WECHAT_DOCS_MARKER_TITLE, WECHAT_DOCS_TEXT, WECHAT_DOCS_TITLE, WECHAT_PUBLISH_BUNDLE_TEXT, WECHAT_PUBLISH_MARKER_TITLE, WECHAT_PUBLISH_PLUGIN_TITLE, WORKSPACE_FIELD, WORKSPACE_TAG_PREFIX, WechatPublishRunner, WikiServer, WriteConflictError, apply, assertNoConflict, assertPublicImageUrl, awaitReady, buildBinaryTiddler, buildClipTiddler, buildImageNoteTiddler, buildPromptText, buildPublishInvocation, buildVersionInvocation, buildWriteTiddler, bundledCatalog, capOutput, checkAllSeeds, checkWechatReady, cleanTiddler, clearLocationState, deepMerge, defaultAdaptersDir, defaultLocationStateFile, defaultWechatJobDir, defineTool, describeConflict, describePrompt, describeUnreadableConfig, docNoteText, downloadClipImage, drainThenStop, dshHomePath, ensureLanguage, ensurePlugin, ensureTiddlerTimestamps, escapeInline, escapePromptBraces, expandEnvPath, flattenTiddlerFields, flushPendingWrites, formatTiddlerDate, hashText, hostAllowed, imageExtensionForMime, inject, interpretPublishOutcome, isBinaryType, isJunkTag, isPrivateAddress, isSafeCliValue, isSafeDsn, isSafeUrl, listWikiCandidates, locationPath, maskConfigSecrets, name, needsRestartAfterSeeds, normalizeConfigPatch, normalizeLocation, normalizePromptMode, normalizePromptPreview, normalizeReadyTimeoutMs, normalizeThemes, normalizeWechatConfig, normalizeWorkspaceName, openInTwEditor, parseClipPayload, parseSeedMarker, parseTiddlerDate, pickImageMime, pinLanguageTiddler, readActiveThemeName, readLocationState, readSeedMarker, readWikiInfo, readyHardTimeoutMs, registerAdminRoutes, registerRoutes, registerTiddlywikiTools, removeSeedById, resolveClipTitle, resolveTwRoot, runAllSeeds, runSeedById, sanitizeTwFragment, scanAdapterDir, seedAllArticles, seedClipBridge, seedDocNote, seedHomeIndex, seedMenubarTheme, seedRenderRoute, seedSendToAgent, seedStarterDocs, seedUiStyles, seedWechatDocs, seedWechatPublish, stripMaskedSecrets, switchWiki, tailOf, tiddlywikiToolSummary, toIsoDateString, toolSignatureLines, unseedClipBridge, unseedWechatDocs, unseedWechatPublish, waitForFileWrite, workspaceMarkFromCwd, workspaceNameFromCwd, workspaceTagName, writeLocationState, writeSeedMarker, writeSessionSummary, writeWikiInfo };
|
|
12822
|
+
export { AGENT_WRITTEN_TAG, ALL_ARTICLES_MARKER_TITLE, ALL_ARTICLES_TEXT, ALL_ARTICLES_TITLE, ANON_USERNAME, AutoCommitter, CLIP_BRIDGE_BOOKMARKLET, CLIP_BRIDGE_DEFAULT_PORT, CLIP_BRIDGE_DOC_TEXT, CLIP_BRIDGE_DOC_TITLE, CLIP_BRIDGE_DRAG_HREF, CLIP_BRIDGE_MARKER_TITLE, ClipBridge, ConfigPatchError, ConfigStore, ConfigUnreadableError, DEFAULT_NOTE_TYPE, DEFAULT_PROMPT_MODE, DEFAULT_WECHAT_ADAPTER, DEFAULT_WECHAT_COMMAND, DOC_NOTE_TAG, DOC_NOTE_TEXT, DOC_NOTE_TITLE, DSH_DOCS_TAG, GitConflictStateError, GitFace, HOME_DEFAULT_TIDDLERS, HOME_INDEX_ITEMS, HOME_INDEX_MARKER_TITLE, HUMAN_EDITED_TAG, LOCATION_STATE_VERSION, MARKDOWN_PLUGIN, MASKED_SECRET, MENUBAR_THEME_MARKER_TITLE, MENUBAR_THEME_TEXT, MENUBAR_THEME_TIDDLER, MISSING_TYPE_FILTER, PATH_PREFIX, PROMPT_GOVERNANCE_BLOCKS, PROMPT_MODES, PROMPT_SECTION_NAME, PROMPT_SECTION_ORDER, READY_HARD_FACTOR, READY_POLL_MS, READY_SLOW_POLL_MS, READY_TIMEOUT_DEFAULT_MS, READY_TIMEOUT_MAX_MS, READY_TIMEOUT_MIN_MS, RENDER_BUNDLE_TEXT, RENDER_MARKER_TITLE, RENDER_PLUGIN_FILE, RENDER_PLUGIN_TITLE, SEED_DEFS, SEED_MARKER_VERSION, SEND_TO_AGENT_BUNDLE_TEXT, SEND_TO_AGENT_MARKER_TITLE, SEND_TO_AGENT_PLUGIN_TITLE, SESSION_SUMMARY_PREFIX, STARTER_DOCS_ITEMS, STARTER_DOCS_MARKER_TITLE, TEXT_LIST_FILTER, TRASH_INDEX_TITLE, TRASH_PREFIX, TW_PROXY_PATH, TW_PROXY_PREFIX, TW_WEB_HOST_DEFAULT, TW_WEB_HOST_TIDDLER, TiddlyWebClient, TrashIndexUnavailableError, UI_STYLES_MARKER_TITLE, UI_STYLE_ITEMS, WECHAT_ADAPTERS, WECHAT_ADAPTER_FILES, WECHAT_ADAPTER_MARKER, WECHAT_DOCS_MARKER_TITLE, WECHAT_DOCS_TEXT, WECHAT_DOCS_TITLE, WECHAT_PUBLISH_BUNDLE_TEXT, WECHAT_PUBLISH_MARKER_TITLE, WECHAT_PUBLISH_PLUGIN_TITLE, WORKSPACE_FIELD, WORKSPACE_TAG_PREFIX, WechatPublishRunner, WikiServer, WriteConflictError, apply, assertNoConflict, assertPublicImageUrl, awaitReady, buildBinaryTiddler, buildClipTiddler, buildImageNoteTiddler, buildPromptText, buildPublishInvocation, buildVersionInvocation, buildWriteTiddler, bundledCatalog, capOutput, checkAllSeeds, checkWechatReady, cleanTiddler, clearLocationState, deepMerge, defaultAdaptersDir, defaultLocationStateFile, defaultWechatJobDir, defineTool, describeConflict, describePrompt, describeUnreadableConfig, docNoteText, downloadClipImage, drainThenStop, dshHomePath, ensureLanguage, ensurePlugin, ensureTiddlerTimestamps, escapeInline, escapePromptBraces, expandEnvPath, flattenTiddlerFields, flushPendingWrites, formatTiddlerDate, hashText, hostAllowed, imageExtensionForMime, inject, interpretPublishOutcome, isBinaryType, isJunkTag, isPrivateAddress, isSafeCliValue, isSafeDsn, isSafeUrl, listWikiCandidates, locationPath, maskConfigSecrets, name, needsRestartAfterSeeds, normalizeConfigPatch, normalizeFieldsArg, normalizeLocation, normalizePromptMode, normalizePromptPreview, normalizeReadyTimeoutMs, normalizeThemes, normalizeWechatConfig, normalizeWorkspaceName, openInTwEditor, parseClipPayload, parseSeedMarker, parseTiddlerDate, pickImageMime, pinLanguageTiddler, readActiveThemeName, readLocationState, readSeedMarker, readWikiInfo, readyHardTimeoutMs, registerAdminRoutes, registerRoutes, registerTiddlywikiTools, removeSeedById, resolveClipTitle, resolveTwRoot, runAllSeeds, runSeedById, sanitizeTwFragment, scanAdapterDir, seedAllArticles, seedClipBridge, seedDocNote, seedHomeIndex, seedMenubarTheme, seedRenderRoute, seedSendToAgent, seedStarterDocs, seedUiStyles, seedWechatDocs, seedWechatPublish, stripMaskedSecrets, switchWiki, tailOf, tiddlywikiToolSummary, toIsoDateString, toolSignatureLines, unseedClipBridge, unseedWechatDocs, unseedWechatPublish, waitForFileWrite, workspaceMarkFromCwd, workspaceNameFromCwd, workspaceTagName, writeLocationState, writeSeedMarker, writeSessionSummary, writeWikiInfo };
|
|
12702
12823
|
|
|
12703
12824
|
//# sourceMappingURL=index.js.map
|