dsh-tiddlywiki 0.30.58 → 0.30.59
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/docs/CHANGELOG.md +1 -0
- package/lib/index.js +130 -15
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/host/tools-notes-lifecycle.ts +73 -16
package/docs/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
> 最近几个主要版本的一句话记录(完整变更见 [Releases](https://github.com/bbqisbbq/dsh-tiddlywiki/releases) / git log)。
|
|
6
6
|
|
|
7
|
+
- **v0.30.59**(2026-10-09):**软删除改为「跟着 wiki 已有的回收站机制走」;同时把一条会误导人的注释改成实测事实** —— 本机 work 库已装 [kookma/trashbin](https://github.com/kookma/TW-Trashbin)(v1.3.6,`$:/trashbin/<原标题>` + `$:/tags/trashbin` 标签),但 `tiddlywiki_delete` 此前**看不见它**:软删除一律写 `$:/dsh-tiddlywiki/trash/<时间戳>/<原名>` + JSON 索引,于是 agent 删掉的东西只有 agent 能列出/恢复,人在 TW 侧边栏里一条都看不到,两套回收站互不相通。现在:① `tools-support.ts` 新增 `hasTrashbin()` 探测(**只读**,不写探针 tiddler——那会在从不装 trashbin 的库里留垃圾,而它正是要检测的东西;判据是 `$:/tags/trashbin` / `$:/plugins/kookma/trashbin/sidebar-tab` **是否存在条目**,**不是查 `tiddlywiki.info`**——浏览器安装的 trashbin 压根不在 info 里(实测:服务器无 `plugin.info`、info 无登记,却照常工作),查 info 会在「恰恰装了它的库」上误报 false);② 命中时 `delete` 写 trashbin 格式副本(保留原 `created`/`modified`,另记 `trash-of`/`trash-at`),**不再写 trash-index**,同名二次删除显式拒绝而非静默覆盖;③ 未命中时走原路径(未装 trashbin 的库行为不变)。**⚠️ 顺带修正一条错误的注释**(`tools-support.ts` 原先写「`$:/` 系统标题无法被枚举」,v0.30.58 之前 agent 据此连续三次误判,包括否掉一个实际可行的方案):实测证明「不可枚举」**只对 HTTP 列举端点成立**(`get-tiddlers-json.js` 给任何 filter 追加 `+[!is[system]]`,实测 `tiddlers.json` 恒 138 条、系统条目恒 0),而**filter 引擎无此限制**(服务端渲染 `[tag[$:/tags/trashbin]]` 得 `COUNT=125`)、**启动期 `store.area.template.html` 内嵌整个 store** 使客户端一开始就持有全部 tiddler——**三条通道必须分开说**,这也是 trashbin 格式不需要索引的根本原因。
|
|
7
8
|
- **v0.30.58**(2026-10-09):**`tiddlywiki_git_sync` 不再要求显式配 `git.remote` —— 配置为空时回落到该库所在仓库自己的 `origin`**(作者 2026-10-09 裁定,方案 A)。起因是一个**同一个问题两个答案**的真实现场:设置页/FAB 的「同步」按钮一直是在目录里跑 `pull/commit/push`、按仓库 `origin` 推,而 Agent 工具把 `git.remote` 当成"开启同步"的开关 —— 同一个仓库,界面点得动、工具却回「没有任何知识库配置了 git.remote(同步远端)」并**整库跳过**(实测:本机 notes 仓库已推到远端、`git remote -v` 有 origin,工具仍报三个库皆未配置)。现在:① 判定改为「**`git.remote` 或仓库 `origin`**」,`git.remote` 降级为**覆盖**(也仍是 `ensureRemote` 写入的目标);② 回落**只在「没配 **且** 真在 git 仓库里」时发生** —— 不在仓库里就不白跑 `git`(新增 `GitFace.originOf()`,**只读、不建远端**);③ `GitSyncTarget` 新增 `remoteSource: 'config' | 'origin' | 'none'`,**回执逐仓库写出远端与来源**(「自动识别自仓库 origin」/「来自 git.remote 配置」),且 URL 里的**凭据先剥掉**(`git.remote` 可能带 PAT,`/admin/state` 对同一个值打码 —— 回执是新开的一条出口);④ `skipped` 分得清「未配 `git.remote`,仓库也没有 origin」与「不在 git 仓库里」,零目标时的文案同时给出两条出路;⑤ 设置页「git 远端」提示改为「空=自动用该库所在仓库的 origin」,`docs/features.md` / `docs/usage.md` / `AGENTS.md` 同步改写。**守门**:`scripts/verify-git-multi-sync.mjs` 新增「回落只在该回落时发生」(**假 farm + 假 GitFace 直接驱动 `createGitLayer().gitTargets()`**,断言 `originOf` 的**调用点集合**:有配置的不探、不在仓库的不探)、回执来源文案、凭据剥离、两种跳过原因;`scripts/verify-repo-keying.mjs` 用真临时仓库钉 `originOf()` 的三态(有 origin / 没有 / 不在仓库里)与「只读不建远端」。`createGitLayer` 因此进入公共面(`scripts/index-barrel-surface.json` 已更新)。**反向验证**:把回落那一行改回 `''` ⇒ 新守门当场点名 `personal` 的 `remoteSource`;恢复后逐字一致。**顺手修好一处既存红灯**:`AGENTS.md` §5 的开发文档索引还指着 2026-10-04 那次笔记整理里**已被合并删除**的 5 篇旧笔记(常见坑 / 架构事实 / 仓库布局 / 易变清单 / 发布流程),而 `scripts/verify-wiki-pointers.mjs` 正是为此存在 —— 指针已改指**整理后仍在的三篇**(《dsh-tiddlywiki 事实总表》《TiddlyWiki 使用与技巧事实总表》《拆库丢失的系统条目清单》)+ 仓库 `docs/CHANGELOG.md` / `docs/development.md`,§2 §4 §6 §7 里同样的旧名字一并改掉。**不恢复那些笔记**(删除是有意为之),`verify:sync` 因此恢复全绿。
|
|
8
9
|
- **v0.30.57**(2026-09-30):**给 `AGENTS.md` 的注入预算装上守门** —— 这条纪律的**存在理由就是它自己**。本文件顶部写着:宿主给 workspace instructions 的预算是 **65536 字节**,「过去它长到 ~120KB,**截断正好砍掉最后几节(发布流程 / 维护规则 / 常见坑)**」—— 也就是说**这份文件当初被拆成「规则 + 指针」的原因,就是它撞过预算上限**。而那条教训**只写在正文里,没有任何东西在量它**(`verify-doc-links` 管链接、`verify-wiki-pointers` 管指针,都不管篇幅),失效方式依然是沉默的:文件继续长到超限,宿主从末尾截断,被砍掉的正好是**最该被看到的规则**,**没有人会收到报错**。新增 `scripts/verify-agents-budget.mjs`(进 `verify:static`):**两级硬阈值** —— 硬上限 **65536**(宿主预算=事实边界)与软上限 **45000**(自我警戒线),**两级都红**(本仓库的教训是「只打印 warning 的守门等于没有守门」;要抬 SOFT 必须明确改常量=有意识的决定)。⚠️ 顺带守一件更隐蔽的事:**文件第 5 行对自身体积的自述不得漂移** —— 它当时写着「现在拆完约 **12KB**」,而实测已 **40.7KB**(**偏差 70%**,文件在对自己撒谎而无人被冒犯)。**这正是本守门第一次运行就抓到的**:`顶部自述「约 12KB」,实测 40.7KB`。已改为实测值并把守门写进那句旁边。**反向验证**:填充到 46920 字节(> 软上限)⇒ 红;填充到 66920 字节(> 硬上限)⇒ 红并点名「注入会被从末尾截断」;恢复后哈希逐字一致。当前实况:`41890 字节 / 65536 = 63.9%`。
|
|
9
10
|
- **v0.30.56**(2026-09-30):**`AGENTS.md` 的 wiki 指针现在有守门了** —— §5「开发文档索引」是这套文档体系的**承重墙**(细节搬进 wiki、注入文件只留指针),但指针断掉的后果是**沉默的**:新会话按它去读会读到空气,而 `tsc` / 任何现有守门都不会报错(`verify-doc-links.mjs` 只管仓库内的相对文件链接)。新增 `scripts/verify-wiki-pointers.mjs`(并进 `npm run verify:sync`):**从 `AGENTS.md` 的表格行里**取出 `[[…]]` 指针(**只取表格行** —— 正文里的 `[[标题]]` 是 TW 语法示例、不是指针,这个区分是判据的核心),再从 wiki 数据根解析出**全部条目标题**(meta 的 `title:` 优先、退回文件名),要求每个指针都命中。**多库模式也认**(优先读 `$DSH_HOME/dsh-tiddlywiki/wikis.json` 的 `defaultId`/全部库,退回单库的 `location.json`)—— 实测本机上插件跑在多库模式、`location.json` 根本不存在,只认单库的话这个守门会**永远 SKIP**。⚠️ **找不到 wiki 数据根时 SKIP 而不是 FAIL**(别人的机器 / CI 上 wiki 可能不存在,前提不成立),但 SKIP **必须明说**「本轮未验证」;也正因如此它**不进 `verify:static`**(CI 上必然 SKIP,等于给人「已守」的错觉),而与 `verify:lib-sync` 同挂在 `verify:sync`(本机环境相关、提交/发布前跑)。**反向验证**:把 §5 的一个指针改成不存在的笔记名(=笔记被改名后的真实形状)⇒ **红并点名**;`--self-test` 模式另行证明「不存在的指针名会被判为缺失」。当前实况:**5 个指针全部命中 · 3 个库 · 2041 条条目**。
|
package/lib/index.js
CHANGED
|
@@ -2582,11 +2582,64 @@ function snippetAround(text, query, max = 160) {
|
|
|
2582
2582
|
const suffix = end < flat.length ? "…" : "";
|
|
2583
2583
|
return `${prefix}${flat.slice(start, end)}${suffix}`;
|
|
2584
2584
|
}
|
|
2585
|
-
/**
|
|
2586
|
-
*
|
|
2587
|
-
*
|
|
2585
|
+
/**
|
|
2586
|
+
* Trash namespace for soft-deleted tiddlers (system titles: never listed by
|
|
2587
|
+
* the default recipe filter, so deleted notes stop appearing in search/recent
|
|
2588
|
+
* while staying recoverable).
|
|
2589
|
+
*
|
|
2590
|
+
* ⚠️ v0.30.58 — 「不可枚举」只对 **HTTP 列举端点**成立,不是全平台的性质。
|
|
2591
|
+
* 三条通道必须分开说,v0.30.58 之前这里只写了两条半,害人:
|
|
2592
|
+
* 1. HTTP 列举(`GET /recipes/default/tiddlers.json`,含浏览器 syncer 的
|
|
2593
|
+
* skinny 增量同步):`get-tiddlers-json.js` 会给**任何** filter 追加
|
|
2594
|
+
* `+[!is[system]]`,所以 `$:/` 条目永远不在返回里(实测:work 库
|
|
2595
|
+
* tiddlers.json 恒 138 条、系统条目恒 0 条)。
|
|
2596
|
+
* 2. **filter 引擎**(`wiki.filterTiddlers()`,服务端的 /render 与浏览器
|
|
2597
|
+
* 内的 wikitext 都走它):**没有任何 `is[system]` 判断**,`$:/` 条目照常
|
|
2598
|
+
* 命中。实测服务端渲染 `[tag[$:/tags/trashbin]]` 得 `COUNT=125`。
|
|
2599
|
+
* 3. **启动期 store 内嵌**(`store.area.template.html.tid` 把整个 wiki
|
|
2600
|
+
* store 塞进 index.html):客户端启动即拥有全部 tiddler,不依赖通道 1。
|
|
2601
|
+
* 正因为 2 和 3 成立,trashbin 格式的副本才既能被 TW 界面 filter 列出、又能
|
|
2602
|
+
* 跨浏览器存活 —— 它不需要 trash-index 索引。
|
|
2603
|
+
*/
|
|
2588
2604
|
const TRASH_PREFIX = "$:/dsh-tiddlywiki/trash/";
|
|
2589
2605
|
/**
|
|
2606
|
+
* kookma/trashbin 的**数据契约**(v0.30.58)。
|
|
2607
|
+
*
|
|
2608
|
+
* 这里刻意只依赖两个约定 —— 副本标题 `$:/trashbin/<原标题>` + 标签
|
|
2609
|
+
* `$:/tags/trashbin` —— 而**不依赖该插件被安装**:它的 35 个 tiddler 全是
|
|
2610
|
+
* `text/vnd.tiddlywiki` 模板(靠 `$:/tags/SideBar` / `$:/tags/ViewToolbar`
|
|
2611
|
+
* 驱动,没有一个 JS module),所以哪怕没登记进 tiddlywiki.info、哪怕只是
|
|
2612
|
+
* 浏览器里装的、哪怕压根没装,我们自己写的 wikitext 页面照样列得出、恢复得
|
|
2613
|
+
* 动。同理装了它的库则额外多一个原生侧边栏,两边说的是同一种格式。
|
|
2614
|
+
*/
|
|
2615
|
+
const TRASHBIN_PREFIX = "$:/trashbin/";
|
|
2616
|
+
const TRASHBIN_TAG = "$:/tags/trashbin";
|
|
2617
|
+
/** 副本上记录的溯源字段:原标题、删除时刻。trashbin 原生不写,但我们写。 */
|
|
2618
|
+
const TRASHBIN_OF_FIELD = "trash-of";
|
|
2619
|
+
const TRASHBIN_AT_FIELD = "trash-at";
|
|
2620
|
+
/** The trashbin-format copy title that holds one soft-deleted tiddler. */
|
|
2621
|
+
function trashbinTitleFor(title) {
|
|
2622
|
+
return `${TRASHBIN_PREFIX}${title}`;
|
|
2623
|
+
}
|
|
2624
|
+
/**
|
|
2625
|
+
* Does this wiki use the trashbin mechanism?
|
|
2626
|
+
*
|
|
2627
|
+
* Read-only probe (v0.30.58). NO probe tiddler is written — a marker would
|
|
2628
|
+
* litter every wiki that never installs trashbin, and it would be the very
|
|
2629
|
+
* thing this is trying to detect.
|
|
2630
|
+
*
|
|
2631
|
+
* Presence of the *sidebar tab template* is the signal, not `tiddlywiki.info`:
|
|
2632
|
+
* a browser-installed trashbin never appears in the info file (verified — the
|
|
2633
|
+
* user's copy has no `plugin.info` on the server and is absent from
|
|
2634
|
+
* tiddlywiki.info, yet works), so an info-based check reports false negatives
|
|
2635
|
+
* on exactly the wikis that HAVE it. The tab template is what the mechanism
|
|
2636
|
+
* actually needs, and it is what both the sidebar and our own page read.
|
|
2637
|
+
*/
|
|
2638
|
+
async function hasTrashbin(wiki) {
|
|
2639
|
+
if (await wiki.get("$:/tags/trashbin") !== void 0) return true;
|
|
2640
|
+
return await wiki.get("$:/plugins/kookma/trashbin/sidebar-tab") !== void 0;
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2590
2643
|
* Trash INDEX tiddler. A `$:/`-titled tiddler cannot be ENUMERATED through the
|
|
2591
2644
|
* recipe listing at all: a fresh wiki has `$:/config/SyncSystemTiddlersFromServer
|
|
2592
2645
|
* = "no"`, and `get-tiddlers-json.js` then appends `+[!is[system]]` to every
|
|
@@ -2665,6 +2718,36 @@ async function writeTrashIndex(wiki, entries) {
|
|
|
2665
2718
|
tags: []
|
|
2666
2719
|
});
|
|
2667
2720
|
}
|
|
2721
|
+
/**
|
|
2722
|
+
* Enumerate trashbin-format copies by asking the TW service to run the filter.
|
|
2723
|
+
*
|
|
2724
|
+
* Returns `undefined` when the wiki cannot answer (service down, /render not
|
|
2725
|
+
* seeded yet) so callers can fall back to the legacy index instead of
|
|
2726
|
+
* reporting an empty trash.
|
|
2727
|
+
*/
|
|
2728
|
+
async function listTrashbin(wiki, renderFilter) {
|
|
2729
|
+
const wikitext = `<$list filter="[tag[${TRASHBIN_TAG}]sort[title]]" variable="ignore"><$view field="title"/>\\n</$list>`;
|
|
2730
|
+
let html;
|
|
2731
|
+
try {
|
|
2732
|
+
html = await renderFilter(wikitext);
|
|
2733
|
+
} catch {
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
if (typeof html !== "string") return void 0;
|
|
2737
|
+
const titles = html.split("\n").map((line) => line.trim()).filter((line) => line.startsWith(TRASHBIN_PREFIX));
|
|
2738
|
+
const entries = [];
|
|
2739
|
+
for (const trash of titles) {
|
|
2740
|
+
const t = await wiki.get(trash);
|
|
2741
|
+
const of = typeof t?.fields?.["trash-of"] === "string" ? t.fields[TRASHBIN_OF_FIELD] : trash.slice(12);
|
|
2742
|
+
const at = typeof t?.fields?.["trash-at"] === "string" ? t.fields[TRASHBIN_AT_FIELD] : t?.modified ?? "";
|
|
2743
|
+
entries.push({
|
|
2744
|
+
trash,
|
|
2745
|
+
of,
|
|
2746
|
+
at
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
return entries;
|
|
2750
|
+
}
|
|
2668
2751
|
/** Raised when the trash index cannot be read: the operation must abort rather
|
|
2669
2752
|
* than rebuild the index from an empty base (data-loss guard, v0.19.5). */
|
|
2670
2753
|
var TrashIndexUnavailableError = class extends Error {
|
|
@@ -3962,7 +4045,7 @@ function deleteTool(env) {
|
|
|
3962
4045
|
const { deps, requireWiki } = env;
|
|
3963
4046
|
return defineTool({
|
|
3964
4047
|
name: "tiddlywiki_delete",
|
|
3965
|
-
description: "删除一个 TiddlyWiki tiddler
|
|
4048
|
+
description: "删除一个 TiddlyWiki tiddler(不存在时是幂等空操作)。默认是**软删除**:内容移入回收站,可用 tiddlywiki_trash 恢复;permanent=true 才真正永久删除。**用哪个回收站由 wiki 自己决定(v0.30.59)**:该库装有 trashbin 类插件(探测 `$:/tags/trashbin` / trashbin 侧边栏条目)时,副本写成用户界面能直接看到并一键恢复的格式(`$:/trashbin/<标题>` + 标签);否则用内置格式。两种情况下 `tiddlywiki_trash` 都能列出与恢复。删除后触发自动 commit。⚠️ 例外:`$:/` 系统条目不进回收站(删除它们等同于 permanent=true——只有 git 历史可回退)。",
|
|
3966
4049
|
parameters: {
|
|
3967
4050
|
title: {
|
|
3968
4051
|
type: "string",
|
|
@@ -3988,7 +4071,7 @@ function deleteTool(env) {
|
|
|
3988
4071
|
},
|
|
3989
4072
|
output: { render: (_args, value) => [{
|
|
3990
4073
|
type: "text",
|
|
3991
|
-
text: value.trashed === true ? `已把 tiddler「${value.title}
|
|
4074
|
+
text: value.trashed === true ? `已把 tiddler「${value.title}」移入回收站${value.trashTitle !== void 0 ? `(${value.trashTitle},可用 tiddlywiki_trash action=restore 恢复)` : "(可用 tiddlywiki_trash action=restore 恢复)"}。` : `已永久删除 tiddler「${value.title}」。`
|
|
3992
4075
|
}] },
|
|
3993
4076
|
execute: async (args, exec) => {
|
|
3994
4077
|
const wiki = requireWiki(sessionIdOf(exec));
|
|
@@ -3998,7 +4081,7 @@ function deleteTool(env) {
|
|
|
3998
4081
|
expectedRevision: args.expectedRevision,
|
|
3999
4082
|
force: args.force
|
|
4000
4083
|
});
|
|
4001
|
-
const canTrash = existing !== void 0 && args.permanent !== true && !args.title.startsWith("$:/dsh-tiddlywiki/trash/") && !args.title.startsWith("$:/");
|
|
4084
|
+
const canTrash = existing !== void 0 && args.permanent !== true && !args.title.startsWith("$:/dsh-tiddlywiki/trash/") && !args.title.startsWith("$:/trashbin/") && !args.title.startsWith("$:/");
|
|
4002
4085
|
if (args.title === "$:/dsh-tiddlywiki/trash-index" && existing !== void 0) throw new Error(`tiddlywiki_delete: 「${TRASH_INDEX_TITLE}」是回收站索引,删掉它会让回收站里的条目全部变成不可恢复的孤儿(列不出、恢复不了、也清不掉)。要清空回收站请用 tiddlywiki_trash action=empty;确实要丢弃索引请显式传 permanent: true。`);
|
|
4003
4086
|
if (!canTrash) {
|
|
4004
4087
|
await wiki.delete(args.title);
|
|
@@ -4009,8 +4092,27 @@ function deleteTool(env) {
|
|
|
4009
4092
|
trashed: false
|
|
4010
4093
|
};
|
|
4011
4094
|
}
|
|
4012
|
-
const trashTitle = trashTitleFor(args.title);
|
|
4013
4095
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4096
|
+
if (await hasTrashbin(wiki)) {
|
|
4097
|
+
const trashTitle = trashbinTitleFor(args.title);
|
|
4098
|
+
if (await wiki.get(trashTitle) !== void 0) throw new Error(`tiddlywiki_delete: 回收站里已经有「${trashTitle}」了(同名笔记被删过两次?)。请先在 TW 回收站侧边栏里清理它,或直接用 permanent: true 删除本条。`);
|
|
4099
|
+
const { tiddler: copy } = buildWriteTiddler(trashTitle, existing.text ?? "", { existing });
|
|
4100
|
+
copy.tags = [TRASHBIN_TAG];
|
|
4101
|
+
copy.created = existing.created ?? copy.created;
|
|
4102
|
+
copy.modified = existing.modified ?? copy.modified;
|
|
4103
|
+
copy[TRASHBIN_OF_FIELD] = args.title;
|
|
4104
|
+
copy[TRASHBIN_AT_FIELD] = at;
|
|
4105
|
+
await wiki.put(copy);
|
|
4106
|
+
await wiki.delete(args.title);
|
|
4107
|
+
deps.autoCommit();
|
|
4108
|
+
return {
|
|
4109
|
+
ok: true,
|
|
4110
|
+
title: args.title,
|
|
4111
|
+
trashed: true,
|
|
4112
|
+
trashTitle
|
|
4113
|
+
};
|
|
4114
|
+
}
|
|
4115
|
+
const trashTitle = trashTitleFor(args.title);
|
|
4014
4116
|
const index = await readTrashIndex(wiki);
|
|
4015
4117
|
if (!index.readOk || index.corrupted) throw new TrashIndexUnavailableError();
|
|
4016
4118
|
const { tiddler: trashTiddler } = buildWriteTiddler(trashTitle, existing.text ?? "", { existing });
|
|
@@ -4042,7 +4144,7 @@ function trashTool(env) {
|
|
|
4042
4144
|
const { deps, requireWiki } = env;
|
|
4043
4145
|
return defineTool({
|
|
4044
4146
|
name: "tiddlywiki_trash",
|
|
4045
|
-
description: "回收站(软删除的笔记):action=list 列出、action=restore 恢复某条、action=empty 清空。配合 tiddlywiki_delete
|
|
4147
|
+
description: "回收站(软删除的笔记):action=list 列出、action=restore 恢复某条、action=empty 清空。配合 tiddlywiki_delete(默认软删除)使用。**两种回收站格式都认**(v0.30.59):trashbin 插件格式与内置格式都能列出与恢复;如果 wiki 装了 trashbin,你在 TW 侧边栏里看到的也是同一批条目。",
|
|
4046
4148
|
parameters: {
|
|
4047
4149
|
action: {
|
|
4048
4150
|
type: "string",
|
|
@@ -4073,10 +4175,22 @@ function trashTool(env) {
|
|
|
4073
4175
|
} },
|
|
4074
4176
|
execute: async (args, exec) => {
|
|
4075
4177
|
const wiki = requireWiki(sessionIdOf(exec));
|
|
4076
|
-
const
|
|
4178
|
+
const trashbin = await listTrashbin(wiki, (wikitext) => wiki.render({
|
|
4179
|
+
text: wikitext,
|
|
4180
|
+
type: "text/vnd.tiddlywiki"
|
|
4181
|
+
}));
|
|
4182
|
+
const index = trashbin !== void 0 ? {
|
|
4183
|
+
readOk: true,
|
|
4184
|
+
corrupted: false,
|
|
4185
|
+
entries: []
|
|
4186
|
+
} : await readTrashIndex(wiki);
|
|
4077
4187
|
if (!index.readOk) throw new TrashIndexUnavailableError();
|
|
4078
4188
|
if (index.corrupted) throw new Error("回收站索引已损坏(JSON 解析失败),本次操作已中止以免误删记录;可手动检查 $:/dsh-tiddlywiki/trash-index 后重试。");
|
|
4079
|
-
const indexed =
|
|
4189
|
+
const indexed = [...(trashbin ?? []).map((e) => ({
|
|
4190
|
+
trash: e.trash,
|
|
4191
|
+
of: e.of,
|
|
4192
|
+
at: e.at
|
|
4193
|
+
})), ...index.entries];
|
|
4080
4194
|
if (args.action === "list") {
|
|
4081
4195
|
const limit = Math.max(1, Math.min(args.limit ?? 30, 200));
|
|
4082
4196
|
return {
|
|
@@ -4091,7 +4205,7 @@ function trashTool(env) {
|
|
|
4091
4205
|
}
|
|
4092
4206
|
if (args.action === "empty") {
|
|
4093
4207
|
for (const entry of indexed) await wiki.delete(entry.trash);
|
|
4094
|
-
await writeTrashIndex(wiki, []);
|
|
4208
|
+
if (index.entries.length > 0 || trashbin === void 0) await writeTrashIndex(wiki, []);
|
|
4095
4209
|
deps.autoCommit();
|
|
4096
4210
|
return {
|
|
4097
4211
|
action: "empty",
|
|
@@ -4104,18 +4218,19 @@ function trashTool(env) {
|
|
|
4104
4218
|
if (match === void 0) throw new Error(`回收站里没有「${wanted}」`);
|
|
4105
4219
|
const stored = await wiki.get(match.trash);
|
|
4106
4220
|
if (stored === void 0) {
|
|
4107
|
-
await writeTrashIndex(wiki,
|
|
4221
|
+
if (trashbin === void 0) await writeTrashIndex(wiki, index.entries.filter((entry) => entry.trash !== match.trash));
|
|
4108
4222
|
throw new Error(`回收站条目「${match.trash}」已不存在(索引已清理)`);
|
|
4109
4223
|
}
|
|
4110
4224
|
if (await wiki.get(match.of) !== void 0) throw new Error(`无法恢复:标题「${match.of}」已被占用,请先处理现有条目`);
|
|
4111
4225
|
const { tiddler: restored } = buildWriteTiddler(match.of, stored.text ?? "", { existing: stored });
|
|
4112
4226
|
restored.created = stored.created ?? restored.created;
|
|
4113
4227
|
restored.modified = stored.modified ?? restored.modified;
|
|
4114
|
-
delete restored[
|
|
4115
|
-
delete restored[
|
|
4228
|
+
delete restored[TRASHBIN_OF_FIELD];
|
|
4229
|
+
delete restored[TRASHBIN_AT_FIELD];
|
|
4230
|
+
if ((Array.isArray(stored.tags) ? stored.tags : []).includes("$:/tags/trashbin")) restored.tags = (Array.isArray(stored.fields?.tags) ? stored.fields.tags : []).filter((tag) => tag !== TRASHBIN_TAG);
|
|
4116
4231
|
await wiki.put(restored);
|
|
4117
4232
|
await wiki.delete(match.trash);
|
|
4118
|
-
await writeTrashIndex(wiki,
|
|
4233
|
+
if (trashbin === void 0) await writeTrashIndex(wiki, index.entries.filter((entry) => entry.trash !== match.trash));
|
|
4119
4234
|
deps.autoCommit();
|
|
4120
4235
|
return {
|
|
4121
4236
|
action: "restore",
|