dsh-tiddlywiki 0.4.0 → 0.6.0
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 +55 -25
- package/lib/client.bundle.js +903 -386
- package/lib/client.js +903 -386
- package/lib/index.js +796 -32
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/index.ts +10 -4
- package/src/client/knowledge-fab.ts +232 -0
- package/src/client/markdown-editor.ts +5 -0
- package/src/client/note-widget.ts +725 -475
- package/src/client/panel.ts +27 -62
- package/src/client/settings-page.ts +3 -3
- package/src/client/styles.ts +110 -0
- package/src/client/sync-button.ts +191 -188
- package/src/host/git.ts +21 -0
- package/src/host/routes.ts +145 -8
- package/src/host/seed-notes.ts +7 -5
- package/src/host/tools.ts +589 -281
- package/src/host/tw-api.ts +80 -6
- package/src/host/wiki.ts +12 -0
- package/src/index.ts +46 -6
- package/src/sdk.ts +8 -2
package/lib/index.js
CHANGED
|
@@ -275,6 +275,47 @@ var GitFace = class {
|
|
|
275
275
|
message: (r.stderr.trim() || r.stdout.trim()).slice(0, 500)
|
|
276
276
|
};
|
|
277
277
|
}
|
|
278
|
+
/** `git fetch` (no remote configured → failure reported by the caller). */
|
|
279
|
+
async fetch(dir) {
|
|
280
|
+
const r = await this.exec(["fetch"], {
|
|
281
|
+
cwd: dir,
|
|
282
|
+
timeout: HEAVY_TIMEOUT_MS
|
|
283
|
+
});
|
|
284
|
+
return r.ok ? {
|
|
285
|
+
ok: true,
|
|
286
|
+
message: r.stdout.trim() || "fetch ok"
|
|
287
|
+
} : {
|
|
288
|
+
ok: false,
|
|
289
|
+
message: (r.stderr.trim() || r.stdout.trim()).slice(0, 500)
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Restore the given files from the freshly fetched remote HEAD (FETCH_HEAD)
|
|
294
|
+
* into the working tree + index — the "keep remote version" half of
|
|
295
|
+
* tiddler-granular conflict resolution. Callers must `git fetch` first.
|
|
296
|
+
*/
|
|
297
|
+
async checkoutFetchHead(dir, files) {
|
|
298
|
+
if (files.length === 0) return {
|
|
299
|
+
ok: true,
|
|
300
|
+
message: "no files given"
|
|
301
|
+
};
|
|
302
|
+
const r = await this.exec([
|
|
303
|
+
"checkout",
|
|
304
|
+
"FETCH_HEAD",
|
|
305
|
+
"--",
|
|
306
|
+
...files
|
|
307
|
+
], {
|
|
308
|
+
cwd: dir,
|
|
309
|
+
timeout: HEAVY_TIMEOUT_MS
|
|
310
|
+
});
|
|
311
|
+
return r.ok ? {
|
|
312
|
+
ok: true,
|
|
313
|
+
message: `已从远端检出 ${files.length} 个文件`
|
|
314
|
+
} : {
|
|
315
|
+
ok: false,
|
|
316
|
+
message: (r.stderr.trim() || r.stdout.trim()).slice(0, 500)
|
|
317
|
+
};
|
|
318
|
+
}
|
|
278
319
|
/** Ensure `origin` points at `url` (add or set-url). */
|
|
279
320
|
async ensureRemote(dir, url) {
|
|
280
321
|
const cur = await this.exec([
|
|
@@ -410,6 +451,16 @@ var AutoCommitter = class {
|
|
|
410
451
|
*/
|
|
411
452
|
/** The DSH webserver route prefix (NOT a TW path-prefix; see module header). */
|
|
412
453
|
const PATH_PREFIX = "/dsh-tiddlywiki";
|
|
454
|
+
/**
|
|
455
|
+
* Same-origin TW proxy route on the DSH webserver (remote-access mode, R1).
|
|
456
|
+
* The browser only ever talks to the DSH origin — which it already reaches
|
|
457
|
+
* over loopback, LAN, Tailscale, a domain or HTTPS — and DSH proxies to the
|
|
458
|
+
* loopback TW child. TW's frontend is pointed at this prefix via the
|
|
459
|
+
* `$:/config/tiddlyweb/host` tiddler so every API call stays same-origin.
|
|
460
|
+
*/
|
|
461
|
+
const TW_PROXY_PREFIX = `${PATH_PREFIX}/tw`;
|
|
462
|
+
/** The proxy base path (trailing slash) handed to browsers / TW's frontend. */
|
|
463
|
+
const TW_PROXY_PATH = `${TW_PROXY_PREFIX}/`;
|
|
413
464
|
/** How long to wait for the wiki to answer /status. */
|
|
414
465
|
const READY_TIMEOUT_MS = 2e4;
|
|
415
466
|
/** Poll cadence while waiting for readiness. */
|
|
@@ -679,6 +730,29 @@ function readBodyBuffer(req, limit = MAX_UPLOAD_BYTES) {
|
|
|
679
730
|
req.on("error", rejectP);
|
|
680
731
|
});
|
|
681
732
|
}
|
|
733
|
+
/** Header names forwarded to the upstream TW service by the proxy routes. */
|
|
734
|
+
const FORWARD_HEADER_NAMES = [
|
|
735
|
+
"accept",
|
|
736
|
+
"accept-encoding",
|
|
737
|
+
"content-type",
|
|
738
|
+
"cookie",
|
|
739
|
+
"authorization",
|
|
740
|
+
"if-none-match",
|
|
741
|
+
"if-modified-since",
|
|
742
|
+
"origin",
|
|
743
|
+
"referer",
|
|
744
|
+
"user-agent"
|
|
745
|
+
];
|
|
746
|
+
/** Copy a safe, string-valued subset of the request headers upstream. */
|
|
747
|
+
function forwardHeaders(headers) {
|
|
748
|
+
const out = {};
|
|
749
|
+
for (const name of FORWARD_HEADER_NAMES) {
|
|
750
|
+
const value = headers[name];
|
|
751
|
+
if (typeof value === "string") out[name] = value;
|
|
752
|
+
else if (Array.isArray(value) && value.length > 0) out[name] = value.join(", ");
|
|
753
|
+
}
|
|
754
|
+
return out;
|
|
755
|
+
}
|
|
682
756
|
/**
|
|
683
757
|
* Sanitize an uploaded filename into a safe bare name (no path separators,
|
|
684
758
|
* no `..`, no control characters). Returns '' when nothing usable remains.
|
|
@@ -701,6 +775,11 @@ function json$1(res, payload, status = 200) {
|
|
|
701
775
|
function pad(n) {
|
|
702
776
|
return n < 10 ? `0${n}` : String(n);
|
|
703
777
|
}
|
|
778
|
+
/** Flat one-line snippet for the recent-notes picker. */
|
|
779
|
+
function snippetOf$1(text, max = 120) {
|
|
780
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
781
|
+
return flat.length <= max ? flat : `${flat.slice(0, max)}…`;
|
|
782
|
+
}
|
|
704
783
|
/** Default note title: `YYYY-MM-DD HH:mm` (design doc D6). */
|
|
705
784
|
function timestampTitle(date = /* @__PURE__ */ new Date()) {
|
|
706
785
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
@@ -767,6 +846,7 @@ function registerRoutes(ctx, deps) {
|
|
|
767
846
|
json$1(res, {
|
|
768
847
|
ok: true,
|
|
769
848
|
...view,
|
|
849
|
+
twProxy: TW_PROXY_PATH,
|
|
770
850
|
git: gitSummary,
|
|
771
851
|
note: { tag: deps.noteDefaults().tag },
|
|
772
852
|
ui: deps.uiDefaults()
|
|
@@ -833,7 +913,7 @@ function registerRoutes(ctx, deps) {
|
|
|
833
913
|
json$1(res, {
|
|
834
914
|
ok: true,
|
|
835
915
|
...result,
|
|
836
|
-
twUrl:
|
|
916
|
+
twUrl: TW_PROXY_PATH
|
|
837
917
|
});
|
|
838
918
|
} catch (err) {
|
|
839
919
|
json$1(res, {
|
|
@@ -867,6 +947,85 @@ function registerRoutes(ctx, deps) {
|
|
|
867
947
|
}, 500);
|
|
868
948
|
}
|
|
869
949
|
};
|
|
950
|
+
/** Recent non-system tiddlers for the quick-note "最近" picker (newest first). */
|
|
951
|
+
const handleRecent = async (req, res) => {
|
|
952
|
+
const client = deps.getClient();
|
|
953
|
+
if (client === void 0) {
|
|
954
|
+
json$1(res, {
|
|
955
|
+
ok: false,
|
|
956
|
+
error: "wiki service is not running"
|
|
957
|
+
}, 503);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
try {
|
|
961
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
962
|
+
const limitRaw = Number(url.searchParams.get("limit") ?? 15);
|
|
963
|
+
const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(Math.floor(limitRaw), 200)) : 15;
|
|
964
|
+
json$1(res, {
|
|
965
|
+
ok: true,
|
|
966
|
+
limit,
|
|
967
|
+
items: (await client.recent(limit)).map((t) => ({
|
|
968
|
+
title: t.title,
|
|
969
|
+
tags: t.tags ?? [],
|
|
970
|
+
modified: typeof t.modified === "string" ? t.modified : null,
|
|
971
|
+
snippet: snippetOf$1(t.text ?? "")
|
|
972
|
+
}))
|
|
973
|
+
});
|
|
974
|
+
} catch (err) {
|
|
975
|
+
json$1(res, {
|
|
976
|
+
ok: false,
|
|
977
|
+
error: err instanceof Error ? err.message : String(err)
|
|
978
|
+
}, 500);
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
/** Full tiddler for the quick-note "最近" picker (load into the editor). */
|
|
982
|
+
const handleGet = async (req, res) => {
|
|
983
|
+
const client = deps.getClient();
|
|
984
|
+
if (client === void 0) {
|
|
985
|
+
json$1(res, {
|
|
986
|
+
ok: false,
|
|
987
|
+
error: "wiki service is not running"
|
|
988
|
+
}, 503);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
try {
|
|
992
|
+
const title = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("title") ?? "";
|
|
993
|
+
if (title.length === 0) {
|
|
994
|
+
json$1(res, {
|
|
995
|
+
ok: false,
|
|
996
|
+
error: "missing title"
|
|
997
|
+
}, 400);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const t = await client.get(title);
|
|
1001
|
+
if (t === void 0) {
|
|
1002
|
+
json$1(res, {
|
|
1003
|
+
ok: false,
|
|
1004
|
+
notFound: true,
|
|
1005
|
+
title
|
|
1006
|
+
}, 404);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const fields = {};
|
|
1010
|
+
for (const [k, v] of Object.entries(t)) {
|
|
1011
|
+
if (k === "title" || k === "text" || k === "tags") continue;
|
|
1012
|
+
fields[k] = v;
|
|
1013
|
+
}
|
|
1014
|
+
json$1(res, {
|
|
1015
|
+
ok: true,
|
|
1016
|
+
title: t.title,
|
|
1017
|
+
text: t.text ?? "",
|
|
1018
|
+
tags: t.tags ?? [],
|
|
1019
|
+
type: t.type ?? "text/vnd.tiddlywiki",
|
|
1020
|
+
fields
|
|
1021
|
+
});
|
|
1022
|
+
} catch (err) {
|
|
1023
|
+
json$1(res, {
|
|
1024
|
+
ok: false,
|
|
1025
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1026
|
+
}, 500);
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
870
1029
|
const handleRestart = async (_req, res) => {
|
|
871
1030
|
try {
|
|
872
1031
|
await deps.server.restart();
|
|
@@ -984,7 +1143,7 @@ function registerRoutes(ctx, deps) {
|
|
|
984
1143
|
ok: true,
|
|
985
1144
|
name: candidate,
|
|
986
1145
|
path: `files/${candidate}`,
|
|
987
|
-
url:
|
|
1146
|
+
url: `${TW_PROXY_PATH}files/${encodeURIComponent(candidate)}`,
|
|
988
1147
|
size: buf.length,
|
|
989
1148
|
type: req.headers["content-type"] ?? "application/octet-stream"
|
|
990
1149
|
});
|
|
@@ -1032,6 +1191,62 @@ function registerRoutes(ctx, deps) {
|
|
|
1032
1191
|
}, 502);
|
|
1033
1192
|
}
|
|
1034
1193
|
};
|
|
1194
|
+
/**
|
|
1195
|
+
* SAME-ORIGIN proxy /dsh-tiddlywiki/tw/<rest> → TW root /<rest>. Serves the
|
|
1196
|
+
* ENTIRE TW frontend (index HTML, /files/*, the TiddlyWeb API) to the
|
|
1197
|
+
* browser through the DSH origin, so the embedded editor works from any
|
|
1198
|
+
* host/domain the user reaches DSH on (loopback, LAN, Tailscale, domain,
|
|
1199
|
+
* HTTPS). The browser never talks to the loopback TW child directly; DSH
|
|
1200
|
+
* does, on the same machine. Binary responses are buffered losslessly
|
|
1201
|
+
* (arrayBuffer) — unlike the /api JSON proxy, this route must never .text().
|
|
1202
|
+
*/
|
|
1203
|
+
const handleTwProxy = async (req, res) => {
|
|
1204
|
+
if (deps.getClient() === void 0) {
|
|
1205
|
+
json$1(res, {
|
|
1206
|
+
ok: false,
|
|
1207
|
+
error: "wiki service is not running"
|
|
1208
|
+
}, 503);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1212
|
+
const rest = url.pathname.replace(new RegExp(`^${TW_PROXY_PREFIX}(?=/|$)`), "") || "/";
|
|
1213
|
+
try {
|
|
1214
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
1215
|
+
const headers = forwardHeaders(req.headers);
|
|
1216
|
+
if (method === "PUT" || method === "DELETE" || method === "POST") headers["x-requested-with"] = "TiddlyWiki";
|
|
1217
|
+
const init = {
|
|
1218
|
+
method,
|
|
1219
|
+
headers,
|
|
1220
|
+
signal: AbortSignal.timeout(3e4)
|
|
1221
|
+
};
|
|
1222
|
+
if (method === "PUT" || method === "POST") init.body = await readBodyBuffer(req, MAX_UPLOAD_BYTES);
|
|
1223
|
+
const upstream = await fetch(`${deps.server.url}${rest}${url.search}`, init);
|
|
1224
|
+
const data = Buffer.from(await upstream.arrayBuffer());
|
|
1225
|
+
const responseHeaders = {
|
|
1226
|
+
"content-type": upstream.headers.get("content-type") ?? "application/octet-stream",
|
|
1227
|
+
"cache-control": upstream.headers.get("cache-control") ?? "no-store"
|
|
1228
|
+
};
|
|
1229
|
+
for (const name of [
|
|
1230
|
+
"etag",
|
|
1231
|
+
"last-modified",
|
|
1232
|
+
"content-disposition"
|
|
1233
|
+
]) {
|
|
1234
|
+
const value = upstream.headers.get(name);
|
|
1235
|
+
if (value !== null) responseHeaders[name] = value;
|
|
1236
|
+
}
|
|
1237
|
+
res.writeHead(upstream.status, responseHeaders);
|
|
1238
|
+
res.end(data);
|
|
1239
|
+
} catch (err) {
|
|
1240
|
+
if (res.headersSent) {
|
|
1241
|
+
res.end();
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
json$1(res, {
|
|
1245
|
+
ok: false,
|
|
1246
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1247
|
+
}, 502);
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1035
1250
|
const disposers = [
|
|
1036
1251
|
ctx.webServer.register({
|
|
1037
1252
|
kind: "exact",
|
|
@@ -1061,6 +1276,20 @@ function registerRoutes(ctx, deps) {
|
|
|
1061
1276
|
handleTags(req, res);
|
|
1062
1277
|
}
|
|
1063
1278
|
}),
|
|
1279
|
+
ctx.webServer.register({
|
|
1280
|
+
kind: "exact",
|
|
1281
|
+
path: `${ROUTE_PREFIX}/recent`,
|
|
1282
|
+
handler: (req, res) => {
|
|
1283
|
+
handleRecent(req, res);
|
|
1284
|
+
}
|
|
1285
|
+
}),
|
|
1286
|
+
ctx.webServer.register({
|
|
1287
|
+
kind: "exact",
|
|
1288
|
+
path: `${ROUTE_PREFIX}/get`,
|
|
1289
|
+
handler: (req, res) => {
|
|
1290
|
+
handleGet(req, res);
|
|
1291
|
+
}
|
|
1292
|
+
}),
|
|
1064
1293
|
ctx.webServer.register({
|
|
1065
1294
|
kind: "exact",
|
|
1066
1295
|
path: `${ROUTE_PREFIX}/sync`,
|
|
@@ -1088,6 +1317,13 @@ function registerRoutes(ctx, deps) {
|
|
|
1088
1317
|
handler: (req, res) => {
|
|
1089
1318
|
handleApiProxy(req, res);
|
|
1090
1319
|
}
|
|
1320
|
+
}),
|
|
1321
|
+
ctx.webServer.register({
|
|
1322
|
+
kind: "prefix",
|
|
1323
|
+
path: `${TW_PROXY_PREFIX}`,
|
|
1324
|
+
handler: (req, res) => {
|
|
1325
|
+
handleTwProxy(req, res);
|
|
1326
|
+
}
|
|
1091
1327
|
})
|
|
1092
1328
|
];
|
|
1093
1329
|
return () => {
|
|
@@ -1542,17 +1778,19 @@ const DOC_NOTE_TEXT = `! dsh-tiddlywiki 插件说明
|
|
|
1542
1778
|
|
|
1543
1779
|
!! 它能做什么
|
|
1544
1780
|
|
|
1545
|
-
* **
|
|
1781
|
+
* **10 个 agent 工具**:\`tiddlywiki_search\`(检索,支持 tags/since/type/limit 过滤)/ \`tiddlywiki_get\`(读)/ \`tiddlywiki_put\`(写)/ \`tiddlywiki_batch_put\`(批量写)/ \`tiddlywiki_rename\`(重命名+同步引用)/ \`tiddlywiki_delete\`(删)/ \`tiddlywiki_recent\`(最近修改)/ \`tiddlywiki_list_tags\`(标签清单)/ \`tiddlywiki_git_sync\`(git 同步)/ \`tiddlywiki_git_resolve\`(冲突按 tiddler 二选一)。
|
|
1546
1782
|
* **TW 编辑器面板**:侧边栏「TiddlyWiki」按钮 → 在界面中央打开完整版 TW 编辑器。
|
|
1547
|
-
*
|
|
1783
|
+
* **快速笔记**:右下角「知识库」悬浮按钮 → 「📝 快速笔记」写随手记(Markdown 高亮、文件上传、多选/自动补全 tag,草稿自动保存到本地),\`Ctrl+Enter\` 保存;「🕘 最近」可一键载入旧笔记继续编辑;「✏️ 在 TW 中编辑」会弹出独立小窗用 TW 原生编辑器编辑。
|
|
1784
|
+
* **一键同步**:「知识库」按钮 → 「🔁 同步」一键 pull → commit → push,按钮上的状态点实时反映 git 状态。
|
|
1548
1785
|
* **git 同步**:写入自动防抖 commit(默认 60 秒);手动 \`tiddlywiki_git_sync action=sync\` 做 pull → commit → push。
|
|
1549
|
-
* **设置页**:DSH 设置 → 「TiddlyWiki
|
|
1786
|
+
* **设置页**:DSH 设置 → 「TiddlyWiki 知识库」管理插件/主题/语言与运行配置(含「知识库」按钮相关显示开关)。
|
|
1550
1787
|
|
|
1551
1788
|
!! 知识库纪律(三条)
|
|
1552
1789
|
|
|
1553
1790
|
1. 开工先 \`tiddlywiki_git_sync action=pull\`(rebase + autostash,真冲突会自动 abort 并报文件)。
|
|
1554
|
-
2.
|
|
1555
|
-
3.
|
|
1791
|
+
2. 冲突后:\`tiddlywiki_git_resolve files=[冲突文件] strategy=keep-local|keep-remote\` 按 tiddler 二选一解决,再重新 sync。
|
|
1792
|
+
3. 收工 \`tiddlywiki_git_sync action=sync\`。
|
|
1793
|
+
4. 插件自动 commit 兜底,手动 sync 用于需要主动推送的场合。
|
|
1556
1794
|
|
|
1557
1795
|
!! 主题与语言
|
|
1558
1796
|
|
|
@@ -1689,20 +1927,82 @@ var TiddlyWebClient = class {
|
|
|
1689
1927
|
/**
|
|
1690
1928
|
* Search non-system tiddlers: one request (default listing with text) plus
|
|
1691
1929
|
* local case-insensitive substring matching on title + text, optional exact
|
|
1692
|
-
*
|
|
1930
|
+
* tags (AND), a `since` modified-time floor, an exact `type`, capped at
|
|
1931
|
+
* `limit`. Robust against the server's external-filter 403.
|
|
1693
1932
|
*/
|
|
1694
|
-
async search(query,
|
|
1933
|
+
async search(query, options = {}) {
|
|
1695
1934
|
const items = await this.list(void 0, true);
|
|
1696
1935
|
const needle = query.toLowerCase();
|
|
1697
|
-
|
|
1936
|
+
const sinceTime = parseSince(options.since);
|
|
1937
|
+
const wantedTags = [...options.tags ?? [], options.tag].filter((t) => typeof t === "string" && t.trim().length > 0);
|
|
1938
|
+
const limit = options.limit ?? 30;
|
|
1939
|
+
const matched = items.filter((t) => {
|
|
1940
|
+
if (t.title.startsWith("$:/")) return false;
|
|
1698
1941
|
if (!t.title.toLowerCase().includes(needle) && !(t.text ?? "").toLowerCase().includes(needle)) return false;
|
|
1699
|
-
if (
|
|
1700
|
-
|
|
1942
|
+
if (sinceTime !== void 0) {
|
|
1943
|
+
const modified = typeof t.modified === "string" ? new Date(t.modified).getTime() : NaN;
|
|
1944
|
+
if (Number.isNaN(modified) || modified < sinceTime) return false;
|
|
1945
|
+
}
|
|
1946
|
+
if (options.type !== void 0 && options.type.length > 0 && (t.type ?? "text/vnd.tiddlywiki") !== options.type) return false;
|
|
1947
|
+
if (wantedTags.length > 0) {
|
|
1948
|
+
const tags = (t.tags ?? []).map((tag) => tag.toLowerCase());
|
|
1949
|
+
if (!wantedTags.every((w) => tags.includes(w.toLowerCase()))) return false;
|
|
1950
|
+
}
|
|
1951
|
+
return true;
|
|
1952
|
+
});
|
|
1953
|
+
return {
|
|
1954
|
+
items: matched.slice(0, limit),
|
|
1955
|
+
total: matched.length
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* List the most recently modified NON-SYSTEM tiddlers, newest first
|
|
1960
|
+
* (missing/modified-less tiddlers sort to the tail). `since` keeps only
|
|
1961
|
+
* tiddlers modified at/after that instant.
|
|
1962
|
+
*/
|
|
1963
|
+
async recent(limit = 15, since) {
|
|
1964
|
+
const items = await this.list(void 0, true);
|
|
1965
|
+
const sinceTime = parseSince(since);
|
|
1966
|
+
const filtered = items.filter((t) => {
|
|
1967
|
+
if (t.title.startsWith("$:/")) return false;
|
|
1968
|
+
if (sinceTime !== void 0) {
|
|
1969
|
+
const modified = typeof t.modified === "string" ? new Date(t.modified).getTime() : NaN;
|
|
1970
|
+
if (Number.isNaN(modified) || modified < sinceTime) return false;
|
|
1701
1971
|
}
|
|
1702
1972
|
return true;
|
|
1703
|
-
})
|
|
1973
|
+
});
|
|
1974
|
+
filtered.sort((a, b) => {
|
|
1975
|
+
const am = typeof a.modified === "string" ? new Date(a.modified).getTime() : 0;
|
|
1976
|
+
return (typeof b.modified === "string" ? new Date(b.modified).getTime() : 0) - am;
|
|
1977
|
+
});
|
|
1978
|
+
return filtered.slice(0, Math.max(1, Math.min(limit, 200)));
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Distinct non-system tags with their tiddler counts, most-used first then
|
|
1982
|
+
* zh-locale. One skinny listing request (no text payloads).
|
|
1983
|
+
*/
|
|
1984
|
+
async listTags() {
|
|
1985
|
+
const items = await this.list(void 0, false);
|
|
1986
|
+
const map = /* @__PURE__ */ new Map();
|
|
1987
|
+
for (const t of items) {
|
|
1988
|
+
if (t.title.startsWith("$:/")) continue;
|
|
1989
|
+
for (const tag of t.tags ?? []) {
|
|
1990
|
+
if (tag.startsWith("$:/")) continue;
|
|
1991
|
+
map.set(tag, (map.get(tag) ?? 0) + 1);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
return [...map.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], "zh")).map(([tag, count]) => ({
|
|
1995
|
+
tag,
|
|
1996
|
+
count
|
|
1997
|
+
}));
|
|
1704
1998
|
}
|
|
1705
1999
|
};
|
|
2000
|
+
/** Parse a `since` value into an epoch ms, or undefined when absent/invalid. */
|
|
2001
|
+
function parseSince(since) {
|
|
2002
|
+
if (typeof since !== "string" || since.trim().length === 0) return void 0;
|
|
2003
|
+
const ms = new Date(since.trim()).getTime();
|
|
2004
|
+
return Number.isNaN(ms) ? void 0 : ms;
|
|
2005
|
+
}
|
|
1706
2006
|
//#endregion
|
|
1707
2007
|
//#region src/sdk.ts
|
|
1708
2008
|
/**
|
|
@@ -1738,7 +2038,11 @@ function compileValue(spec) {
|
|
|
1738
2038
|
const objectSpec = spec;
|
|
1739
2039
|
node.type = "object";
|
|
1740
2040
|
node.additionalProperties = objectSpec.additionalProperties;
|
|
1741
|
-
if (objectSpec.properties !== void 0)
|
|
2041
|
+
if (objectSpec.properties !== void 0) {
|
|
2042
|
+
const compiled = compilePropertyMap(objectSpec.properties);
|
|
2043
|
+
node.properties = compiled.properties;
|
|
2044
|
+
if (compiled.required !== void 0) node.required = compiled.required;
|
|
2045
|
+
}
|
|
1742
2046
|
return node;
|
|
1743
2047
|
}
|
|
1744
2048
|
if (type === "array") {
|
|
@@ -1842,10 +2146,14 @@ function defineTool(options) {
|
|
|
1842
2146
|
//#endregion
|
|
1843
2147
|
//#region src/host/tools.ts
|
|
1844
2148
|
/**
|
|
1845
|
-
* The
|
|
2149
|
+
* The `tiddlywiki_*` agent tools (design doc §11, D8) plus the extension
|
|
1846
2150
|
* point: `registerTiddlywikiTools(ctx, deps)` registers tools list-style, so a
|
|
1847
2151
|
* new tool is just one more `defineTool` in the array — index.ts never changes.
|
|
1848
2152
|
*
|
|
2153
|
+
* Toolset (v0.5):
|
|
2154
|
+
* search / get / put / batch_put / rename / delete / recent / list_tags /
|
|
2155
|
+
* git_sync / git_resolve
|
|
2156
|
+
*
|
|
1849
2157
|
* RENDER CONTRACT (design doc §4.3): the registry feeds `output.render(args,
|
|
1850
2158
|
* value)` into the loop — the model sees ONLY the rendered text, never the raw
|
|
1851
2159
|
* JSON `value`. Every render must carry the complete facts an agent needs to
|
|
@@ -1866,6 +2174,42 @@ function pickFields(t) {
|
|
|
1866
2174
|
}
|
|
1867
2175
|
return out;
|
|
1868
2176
|
}
|
|
2177
|
+
/** A put-ready copy of a tiddler (no created/modified, tags as array). */
|
|
2178
|
+
function cleanTiddler(t) {
|
|
2179
|
+
const out = {
|
|
2180
|
+
title: t.title,
|
|
2181
|
+
text: t.text ?? "",
|
|
2182
|
+
tags: t.tags ?? []
|
|
2183
|
+
};
|
|
2184
|
+
if (typeof t.type === "string") out.type = t.type;
|
|
2185
|
+
for (const [k, v] of Object.entries(t)) {
|
|
2186
|
+
if (k === "title" || k === "text" || k === "tags" || k === "type" || k === "created" || k === "modified" || k === "fields") continue;
|
|
2187
|
+
out[k] = v;
|
|
2188
|
+
}
|
|
2189
|
+
return out;
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Rewrite TiddlyWiki references to a title inside wiki text: `[[Title]]`,
|
|
2193
|
+
* `[[display|Title]]`, `{{Title}}` → the new title. Best-effort link/text
|
|
2194
|
+
* migration for tiddlywiki_rename; returns the rewritten text + hit count.
|
|
2195
|
+
*/
|
|
2196
|
+
function rewriteRefs(text, oldTitle, newTitle) {
|
|
2197
|
+
if (oldTitle.length === 0) return {
|
|
2198
|
+
text,
|
|
2199
|
+
count: 0
|
|
2200
|
+
};
|
|
2201
|
+
const escaped = oldTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2202
|
+
const re = new RegExp(`(\\[\\[[^\\]|]*\\|)${escaped}(\\]\\])|(\\[\\[)${escaped}(\\]\\])|(\\{\\{)${escaped}(\\}\\})`, "g");
|
|
2203
|
+
let count = 0;
|
|
2204
|
+
return {
|
|
2205
|
+
text: text.replace(re, (...args) => {
|
|
2206
|
+
const p = args;
|
|
2207
|
+
count++;
|
|
2208
|
+
return `${p[1] ?? p[3] ?? p[5] ?? ""}${newTitle}${p[2] ?? p[4] ?? p[6] ?? ""}`;
|
|
2209
|
+
}),
|
|
2210
|
+
count
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
1869
2213
|
function registerTiddlywikiTools(ctx, deps) {
|
|
1870
2214
|
const disposers = [];
|
|
1871
2215
|
const register = (tool) => {
|
|
@@ -1873,29 +2217,51 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
1873
2217
|
};
|
|
1874
2218
|
register(defineTool({
|
|
1875
2219
|
name: "tiddlywiki_search",
|
|
1876
|
-
description: "检索 TiddlyWiki 持久知识库:按关键词(可选
|
|
2220
|
+
description: "检索 TiddlyWiki 持久知识库:按关键词(可选 tags 数组 / since 修改时间 / type / limit)搜索非系统 tiddler,返回标题、标签、修改时间与摘要片段。",
|
|
1877
2221
|
parameters: {
|
|
1878
2222
|
query: {
|
|
1879
2223
|
type: "string",
|
|
1880
2224
|
description: "搜索关键词(大小写不敏感,子串匹配)",
|
|
1881
2225
|
required: true
|
|
1882
2226
|
},
|
|
2227
|
+
tags: {
|
|
2228
|
+
type: "array",
|
|
2229
|
+
items: { type: "string" },
|
|
2230
|
+
description: "可选:要求同时包含的标签(AND)"
|
|
2231
|
+
},
|
|
1883
2232
|
tag: {
|
|
1884
2233
|
type: "string",
|
|
1885
|
-
description: "
|
|
2234
|
+
description: "可选:单个精确标签(与 tags 同为 AND)"
|
|
2235
|
+
},
|
|
2236
|
+
since: {
|
|
2237
|
+
type: "string",
|
|
2238
|
+
description: "可选:ISO 时间(如 2026-09-01 或 2026-09-01T00:00:00Z),只返回修改时间不早于它的 tiddler"
|
|
2239
|
+
},
|
|
2240
|
+
type: {
|
|
2241
|
+
type: "string",
|
|
2242
|
+
description: "可选:精确 tiddler 类型(默认 text/vnd.tiddlywiki)"
|
|
2243
|
+
},
|
|
2244
|
+
limit: {
|
|
2245
|
+
type: "integer",
|
|
2246
|
+
description: "可选:返回条数上限(默认 30,最大 200)"
|
|
1886
2247
|
}
|
|
1887
2248
|
},
|
|
1888
2249
|
output: {
|
|
1889
2250
|
schema: { type: "json" },
|
|
1890
2251
|
render: (_args, value) => {
|
|
1891
|
-
const
|
|
2252
|
+
const filters = [];
|
|
2253
|
+
if (value.tags.length > 0) filters.push(`tags=${value.tags.join(",")}`);
|
|
2254
|
+
if (value.since !== null) filters.push(`since=${value.since}`);
|
|
2255
|
+
if (value.type !== null) filters.push(`type=${value.type}`);
|
|
2256
|
+
const lines = [`TiddlyWiki 搜索「${value.query}」${filters.length > 0 ? ` (${filters.join(" · ")})` : ""}:命中 ${value.total} 条。`];
|
|
1892
2257
|
if (value.results.length === 0) lines.push("没有匹配的 tiddler。");
|
|
1893
2258
|
for (const r of value.results) {
|
|
1894
2259
|
const tags = r.tags.length > 0 ? ` [${r.tags.join(", ")}]` : "";
|
|
1895
|
-
|
|
2260
|
+
const modified = r.modified !== null ? ` (${r.modified})` : "";
|
|
2261
|
+
lines.push(`- ${r.title}${tags}${modified}`);
|
|
1896
2262
|
if (r.snippet.length > 0) lines.push(` ${r.snippet}`);
|
|
1897
2263
|
}
|
|
1898
|
-
if (value.
|
|
2264
|
+
if (value.total > value.results.length) lines.push(`(另有 ${value.total - value.results.length} 条未展开,可用 tiddlywiki_get 读取具体标题,或提高 limit)`);
|
|
1899
2265
|
return [{
|
|
1900
2266
|
type: "text",
|
|
1901
2267
|
text: lines.join("\n")
|
|
@@ -1905,19 +2271,101 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
1905
2271
|
execute: async (args) => {
|
|
1906
2272
|
const wiki = deps.wiki();
|
|
1907
2273
|
if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
|
|
1908
|
-
const
|
|
2274
|
+
const { items, total } = await wiki.search(args.query, {
|
|
2275
|
+
tags: args.tags,
|
|
2276
|
+
tag: args.tag,
|
|
2277
|
+
since: args.since,
|
|
2278
|
+
type: args.type,
|
|
2279
|
+
limit: args.limit
|
|
2280
|
+
});
|
|
1909
2281
|
return {
|
|
1910
2282
|
query: args.query,
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
2283
|
+
tags: args.tags ?? [],
|
|
2284
|
+
since: args.since ?? null,
|
|
2285
|
+
type: args.type ?? null,
|
|
2286
|
+
total,
|
|
2287
|
+
results: items.map((t) => ({
|
|
2288
|
+
title: t.title,
|
|
2289
|
+
tags: t.tags ?? [],
|
|
2290
|
+
modified: typeof t.modified === "string" ? t.modified : null,
|
|
2291
|
+
snippet: snippetOf(t.text ?? "")
|
|
2292
|
+
}))
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
}));
|
|
2296
|
+
register(defineTool({
|
|
2297
|
+
name: "tiddlywiki_recent",
|
|
2298
|
+
description: "查看 TiddlyWiki 知识库最近修改的笔记(按修改时间倒序,排除系统 tiddler),返回标题、标签、修改时间与摘要。适合开工时快速了解近期动态。",
|
|
2299
|
+
parameters: {
|
|
2300
|
+
limit: {
|
|
2301
|
+
type: "integer",
|
|
2302
|
+
description: "可选:返回条数(默认 15,最大 200)"
|
|
2303
|
+
},
|
|
2304
|
+
since: {
|
|
2305
|
+
type: "string",
|
|
2306
|
+
description: "可选:只返回修改时间不早于该 ISO 时间的 tiddler"
|
|
2307
|
+
}
|
|
2308
|
+
},
|
|
2309
|
+
output: {
|
|
2310
|
+
schema: { type: "json" },
|
|
2311
|
+
render: (_args, value) => {
|
|
2312
|
+
const lines = [`TiddlyWiki 最近修改(最近 ${value.results.length} 条${value.since !== null ? `,since=${value.since}` : ""}):`];
|
|
2313
|
+
if (value.results.length === 0) lines.push("暂无笔记。");
|
|
2314
|
+
for (const r of value.results) {
|
|
2315
|
+
const tags = r.tags.length > 0 ? ` [${r.tags.join(", ")}]` : "";
|
|
2316
|
+
lines.push(`- ${r.title}${tags} (${r.modified ?? "?"})`);
|
|
2317
|
+
if (r.snippet.length > 0) lines.push(` ${r.snippet}`);
|
|
2318
|
+
}
|
|
2319
|
+
return [{
|
|
2320
|
+
type: "text",
|
|
2321
|
+
text: lines.join("\n")
|
|
2322
|
+
}];
|
|
2323
|
+
}
|
|
2324
|
+
},
|
|
2325
|
+
execute: async (args) => {
|
|
2326
|
+
const wiki = deps.wiki();
|
|
2327
|
+
if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
|
|
2328
|
+
const items = await wiki.recent(args.limit ?? 15, args.since);
|
|
2329
|
+
return {
|
|
2330
|
+
since: args.since ?? null,
|
|
2331
|
+
results: items.map((t) => ({
|
|
1914
2332
|
title: t.title,
|
|
1915
2333
|
tags: t.tags ?? [],
|
|
2334
|
+
modified: typeof t.modified === "string" ? t.modified : null,
|
|
1916
2335
|
snippet: snippetOf(t.text ?? "")
|
|
1917
2336
|
}))
|
|
1918
2337
|
};
|
|
1919
2338
|
}
|
|
1920
2339
|
}));
|
|
2340
|
+
register(defineTool({
|
|
2341
|
+
name: "tiddlywiki_list_tags",
|
|
2342
|
+
description: "列出 TiddlyWiki 知识库现有的非系统标签及各自计数(按使用次数降序),方便决定给笔记打什么 tag。",
|
|
2343
|
+
parameters: {},
|
|
2344
|
+
output: {
|
|
2345
|
+
schema: { type: "json" },
|
|
2346
|
+
render: (_args, value) => {
|
|
2347
|
+
if (value.tags.length === 0) return [{
|
|
2348
|
+
type: "text",
|
|
2349
|
+
text: "知识库暂无标签。"
|
|
2350
|
+
}];
|
|
2351
|
+
const lines = [`现有标签(${value.tags.length} 个,按使用次数降序):`];
|
|
2352
|
+
for (const t of value.tags) lines.push(`- ${t.tag} × ${t.count}`);
|
|
2353
|
+
return [{
|
|
2354
|
+
type: "text",
|
|
2355
|
+
text: lines.join("\n")
|
|
2356
|
+
}];
|
|
2357
|
+
}
|
|
2358
|
+
},
|
|
2359
|
+
execute: async () => {
|
|
2360
|
+
const wiki = deps.wiki();
|
|
2361
|
+
if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
|
|
2362
|
+
const tags = await wiki.listTags();
|
|
2363
|
+
return {
|
|
2364
|
+
count: tags.length,
|
|
2365
|
+
tags
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
}));
|
|
1921
2369
|
register(defineTool({
|
|
1922
2370
|
name: "tiddlywiki_get",
|
|
1923
2371
|
description: "读取一个 TiddlyWiki tiddler 的完整内容(标题、全文、标签、自定义字段)。",
|
|
@@ -1935,6 +2383,7 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
1935
2383
|
}];
|
|
1936
2384
|
const lines = [`tiddler「${value.title}」`];
|
|
1937
2385
|
if (value.tags.length > 0) lines.push(`标签: ${value.tags.join(", ")}`);
|
|
2386
|
+
if (value.modified !== null) lines.push(`修改: ${value.modified}`);
|
|
1938
2387
|
const fields = Object.entries(value.fields);
|
|
1939
2388
|
if (fields.length > 0) lines.push(`字段: ${fields.map(([k, v]) => `${k}=${String(v)}`).join(", ")}`);
|
|
1940
2389
|
lines.push("--- 全文 ---");
|
|
@@ -1954,14 +2403,16 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
1954
2403
|
title: args.title,
|
|
1955
2404
|
text: "",
|
|
1956
2405
|
tags: [],
|
|
1957
|
-
fields: {}
|
|
2406
|
+
fields: {},
|
|
2407
|
+
modified: null
|
|
1958
2408
|
};
|
|
1959
2409
|
return {
|
|
1960
2410
|
notFound: false,
|
|
1961
2411
|
title: t.title,
|
|
1962
2412
|
text: t.text ?? "",
|
|
1963
2413
|
tags: t.tags ?? [],
|
|
1964
|
-
fields: pickFields(t)
|
|
2414
|
+
fields: pickFields(t),
|
|
2415
|
+
modified: typeof t.modified === "string" ? t.modified : null
|
|
1965
2416
|
};
|
|
1966
2417
|
}
|
|
1967
2418
|
}));
|
|
@@ -2023,6 +2474,186 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
2023
2474
|
};
|
|
2024
2475
|
}
|
|
2025
2476
|
}));
|
|
2477
|
+
register(defineTool({
|
|
2478
|
+
name: "tiddlywiki_batch_put",
|
|
2479
|
+
description: "批量写入/覆盖多个 TiddlyWiki tiddler(一次工具调用)。overwrite=false 时跳过已存在的标题;返回逐条结果。写入后触发自动 commit。",
|
|
2480
|
+
parameters: {
|
|
2481
|
+
items: {
|
|
2482
|
+
type: "array",
|
|
2483
|
+
items: {
|
|
2484
|
+
type: "object",
|
|
2485
|
+
additionalProperties: true,
|
|
2486
|
+
description: "要写入的 tiddler 数组",
|
|
2487
|
+
properties: {
|
|
2488
|
+
title: {
|
|
2489
|
+
type: "string",
|
|
2490
|
+
description: "标题(精确匹配,覆盖同名)",
|
|
2491
|
+
required: true
|
|
2492
|
+
},
|
|
2493
|
+
text: {
|
|
2494
|
+
type: "string",
|
|
2495
|
+
description: "全文(wiki 文本)",
|
|
2496
|
+
required: true
|
|
2497
|
+
},
|
|
2498
|
+
tags: {
|
|
2499
|
+
type: "array",
|
|
2500
|
+
items: { type: "string" },
|
|
2501
|
+
description: "标签数组(可选)"
|
|
2502
|
+
},
|
|
2503
|
+
fields: {
|
|
2504
|
+
type: "json",
|
|
2505
|
+
description: "附加自定义字段(可选)"
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
},
|
|
2510
|
+
overwrite: {
|
|
2511
|
+
type: "boolean",
|
|
2512
|
+
description: "可选:true=覆盖同名(默认),false=跳过已存在的标题"
|
|
2513
|
+
}
|
|
2514
|
+
},
|
|
2515
|
+
output: {
|
|
2516
|
+
schema: { type: "json" },
|
|
2517
|
+
render: (_args, value) => {
|
|
2518
|
+
const lines = [`批量写入完成:成功 ${value.written},跳过 ${value.skipped},共 ${value.items.length} 条。`];
|
|
2519
|
+
for (const r of value.items) lines.push(`- ${r.title}:${r.written ? "已写入" : "已跳过(存在)"}`);
|
|
2520
|
+
return [{
|
|
2521
|
+
type: "text",
|
|
2522
|
+
text: lines.join("\n")
|
|
2523
|
+
}];
|
|
2524
|
+
}
|
|
2525
|
+
},
|
|
2526
|
+
execute: async (args) => {
|
|
2527
|
+
const wiki = deps.wiki();
|
|
2528
|
+
if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
|
|
2529
|
+
const list = Array.isArray(args.items) ? args.items : [];
|
|
2530
|
+
if (list.length === 0) return {
|
|
2531
|
+
ok: true,
|
|
2532
|
+
written: 0,
|
|
2533
|
+
skipped: 0,
|
|
2534
|
+
items: []
|
|
2535
|
+
};
|
|
2536
|
+
const overwrite = args.overwrite !== false;
|
|
2537
|
+
const results = [];
|
|
2538
|
+
let written = 0;
|
|
2539
|
+
let skipped = 0;
|
|
2540
|
+
for (const item of list) {
|
|
2541
|
+
if (typeof item.title !== "string" || item.title.length === 0) throw new Error("batch_put: 每条 items 都需要非空 title");
|
|
2542
|
+
if (typeof item.text !== "string") throw new Error(`batch_put: items「${item.title}」缺少 text`);
|
|
2543
|
+
if (!overwrite) {
|
|
2544
|
+
if (await wiki.get(item.title).catch(() => void 0) !== void 0) {
|
|
2545
|
+
skipped++;
|
|
2546
|
+
results.push({
|
|
2547
|
+
title: item.title,
|
|
2548
|
+
written: false,
|
|
2549
|
+
skipped: true
|
|
2550
|
+
});
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
const tiddler = {
|
|
2555
|
+
title: item.title,
|
|
2556
|
+
text: item.text
|
|
2557
|
+
};
|
|
2558
|
+
if (Array.isArray(item.tags) && item.tags.length > 0) tiddler.tags = item.tags;
|
|
2559
|
+
if (item.fields !== void 0 && typeof item.fields === "object" && item.fields !== null) Object.assign(tiddler, item.fields);
|
|
2560
|
+
await wiki.put(tiddler);
|
|
2561
|
+
written++;
|
|
2562
|
+
results.push({
|
|
2563
|
+
title: item.title,
|
|
2564
|
+
written: true,
|
|
2565
|
+
skipped: false
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
deps.autoCommit();
|
|
2569
|
+
return {
|
|
2570
|
+
ok: true,
|
|
2571
|
+
written,
|
|
2572
|
+
skipped,
|
|
2573
|
+
items: results
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
}));
|
|
2577
|
+
register(defineTool({
|
|
2578
|
+
name: "tiddlywiki_rename",
|
|
2579
|
+
description: "重命名一个 TiddlyWiki tiddler:把旧标题的内容复制到新标题、删除旧标题,并可选地更新其他 tiddler 里的 [[旧标题]] / {{旧标题}} 引用(最佳努力)。",
|
|
2580
|
+
parameters: {
|
|
2581
|
+
oldTitle: {
|
|
2582
|
+
type: "string",
|
|
2583
|
+
description: "当前标题",
|
|
2584
|
+
required: true
|
|
2585
|
+
},
|
|
2586
|
+
newTitle: {
|
|
2587
|
+
type: "string",
|
|
2588
|
+
description: "新标题",
|
|
2589
|
+
required: true
|
|
2590
|
+
},
|
|
2591
|
+
updateRefs: {
|
|
2592
|
+
type: "boolean",
|
|
2593
|
+
description: "可选:是否同步更新其他 tiddler 里的引用(默认 true)"
|
|
2594
|
+
}
|
|
2595
|
+
},
|
|
2596
|
+
output: {
|
|
2597
|
+
schema: { type: "json" },
|
|
2598
|
+
render: (_args, value) => {
|
|
2599
|
+
const lines = [`已重命名「${value.from}」→「${value.to}」`];
|
|
2600
|
+
lines.push(`更新了 ${value.refsUpdated} 处引用(${value.refsTiddlers} 个 tiddler)`);
|
|
2601
|
+
if (value.warning !== void 0) lines.push(`注意: ${value.warning}`);
|
|
2602
|
+
return [{
|
|
2603
|
+
type: "text",
|
|
2604
|
+
text: lines.join("\n")
|
|
2605
|
+
}];
|
|
2606
|
+
}
|
|
2607
|
+
},
|
|
2608
|
+
execute: async (args) => {
|
|
2609
|
+
const wiki = deps.wiki();
|
|
2610
|
+
if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
|
|
2611
|
+
const { oldTitle, newTitle } = args;
|
|
2612
|
+
if (oldTitle === newTitle) return {
|
|
2613
|
+
ok: true,
|
|
2614
|
+
from: oldTitle,
|
|
2615
|
+
to: newTitle,
|
|
2616
|
+
refsUpdated: 0,
|
|
2617
|
+
refsTiddlers: 0
|
|
2618
|
+
};
|
|
2619
|
+
const existing = await wiki.get(oldTitle);
|
|
2620
|
+
if (existing === void 0) throw new Error(`tiddler「${oldTitle}」不存在`);
|
|
2621
|
+
if (await wiki.get(newTitle) !== void 0) throw new Error(`新标题「${newTitle}」已存在(可先用 tiddlywiki_delete 删除)`);
|
|
2622
|
+
let refsUpdated = 0;
|
|
2623
|
+
let refsTiddlers = 0;
|
|
2624
|
+
let warning;
|
|
2625
|
+
if (args.updateRefs !== false) {
|
|
2626
|
+
const all = await wiki.list(void 0, true);
|
|
2627
|
+
for (const t of all) {
|
|
2628
|
+
if (t.title === oldTitle || t.title === newTitle) continue;
|
|
2629
|
+
if (t.title.startsWith("$:/")) continue;
|
|
2630
|
+
const text = t.text ?? "";
|
|
2631
|
+
if (text.length === 0) continue;
|
|
2632
|
+
const rewritten = rewriteRefs(text, oldTitle, newTitle);
|
|
2633
|
+
if (rewritten.count > 0) {
|
|
2634
|
+
await wiki.put({
|
|
2635
|
+
...cleanTiddler(t),
|
|
2636
|
+
text: rewritten.text
|
|
2637
|
+
});
|
|
2638
|
+
refsUpdated += rewritten.count;
|
|
2639
|
+
refsTiddlers++;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
await wiki.put(cleanTiddler(existing));
|
|
2644
|
+
await wiki.delete(oldTitle);
|
|
2645
|
+
if (refsTiddlers === 0) warning = "未找到任何其他 tiddler 引用旧标题;如确实需要,可手动补充链接。";
|
|
2646
|
+
deps.autoCommit();
|
|
2647
|
+
return {
|
|
2648
|
+
ok: true,
|
|
2649
|
+
from: oldTitle,
|
|
2650
|
+
to: newTitle,
|
|
2651
|
+
refsUpdated,
|
|
2652
|
+
refsTiddlers,
|
|
2653
|
+
...warning !== void 0 ? { warning } : {}
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
}));
|
|
2026
2657
|
register(defineTool({
|
|
2027
2658
|
name: "tiddlywiki_delete",
|
|
2028
2659
|
description: "删除一个 TiddlyWiki tiddler(不存在时是幂等空操作)。删除后触发自动 commit。",
|
|
@@ -2132,6 +2763,98 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
2132
2763
|
}
|
|
2133
2764
|
}
|
|
2134
2765
|
}));
|
|
2766
|
+
register(defineTool({
|
|
2767
|
+
name: "tiddlywiki_git_resolve",
|
|
2768
|
+
description: "在 tiddlywiki_git_sync action=pull 冲突(已 abort)后,按 tiddler 二选一解决:keep-local 保留本地版本;keep-remote 用 git fetch 拉取远端并检出远端版本(需已配置 git.remote);list 仅报告当前状态。解决后建议重新 pull/sync 整合其余改动。",
|
|
2769
|
+
parameters: {
|
|
2770
|
+
strategy: {
|
|
2771
|
+
type: "string",
|
|
2772
|
+
enum: [
|
|
2773
|
+
"keep-local",
|
|
2774
|
+
"keep-remote",
|
|
2775
|
+
"list"
|
|
2776
|
+
],
|
|
2777
|
+
description: "keep-local=保留本地;keep-remote=改用远端版本;list=仅报告当前 git 状态",
|
|
2778
|
+
required: true
|
|
2779
|
+
},
|
|
2780
|
+
files: {
|
|
2781
|
+
type: "array",
|
|
2782
|
+
items: { type: "string" },
|
|
2783
|
+
description: "冲突文件名数组(来自 pull 返回的 conflictFiles;list 时忽略)"
|
|
2784
|
+
}
|
|
2785
|
+
},
|
|
2786
|
+
output: {
|
|
2787
|
+
schema: { type: "json" },
|
|
2788
|
+
render: (_args, value) => {
|
|
2789
|
+
const lines = [`git resolve ${value.action}: ${value.ok ? "成功" : "失败"}`];
|
|
2790
|
+
lines.push(` ${value.message}`);
|
|
2791
|
+
if (value.files !== void 0 && value.files.length > 0) lines.push(`涉及文件: ${value.files.join(", ")}`);
|
|
2792
|
+
if (value.commit !== void 0) lines.push(`本地 commit: ${value.commit}`);
|
|
2793
|
+
if (value.status !== void 0) {
|
|
2794
|
+
const s = value.status;
|
|
2795
|
+
const bits = [`分支 ${s.branch}`];
|
|
2796
|
+
if (s.ahead !== void 0) bits.push(`领先 ${s.ahead}`);
|
|
2797
|
+
if (s.behind !== void 0) bits.push(`落后 ${s.behind}`);
|
|
2798
|
+
if (s.dirty) bits.push(`工作区有 ${s.dirtyFiles.length} 个未提交改动`);
|
|
2799
|
+
if (s.lastCommit !== void 0) bits.push(`最近提交 ${s.lastCommit}`);
|
|
2800
|
+
lines.push(`状态: ${bits.join(" · ")}`);
|
|
2801
|
+
}
|
|
2802
|
+
return [{
|
|
2803
|
+
type: "text",
|
|
2804
|
+
text: lines.join("\n")
|
|
2805
|
+
}];
|
|
2806
|
+
}
|
|
2807
|
+
},
|
|
2808
|
+
execute: async (args) => {
|
|
2809
|
+
const dir = deps.wikiPath();
|
|
2810
|
+
if (args.strategy === "list") return {
|
|
2811
|
+
ok: true,
|
|
2812
|
+
action: "list",
|
|
2813
|
+
message: "当前仓库状态(pull 冲突已 abort,工作区即本地版本,不会残留未合并状态)",
|
|
2814
|
+
status: await deps.git.status(dir)
|
|
2815
|
+
};
|
|
2816
|
+
const files = (args.files ?? []).filter((f) => typeof f === "string" && f.length > 0);
|
|
2817
|
+
if (files.length === 0) return {
|
|
2818
|
+
ok: false,
|
|
2819
|
+
action: args.strategy,
|
|
2820
|
+
message: "请提供 conflictFiles(来自 pull 返回)",
|
|
2821
|
+
hint: "可先执行 tiddlywiki_git_sync action=pull 查看冲突文件"
|
|
2822
|
+
};
|
|
2823
|
+
if (args.strategy === "keep-local") {
|
|
2824
|
+
const status = await deps.git.status(dir);
|
|
2825
|
+
return {
|
|
2826
|
+
ok: true,
|
|
2827
|
+
action: "keep-local",
|
|
2828
|
+
message: `已保留本地版本(${files.length} 个文件;abort 后本地即为工作区内容)。建议重新 tiddlywiki_git_sync action=sync 整合远端其余改动。`,
|
|
2829
|
+
files,
|
|
2830
|
+
status
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
const fetched = await deps.git.fetch(dir);
|
|
2834
|
+
if (!fetched.ok) return {
|
|
2835
|
+
ok: false,
|
|
2836
|
+
action: "keep-remote",
|
|
2837
|
+
message: `fetch 失败(可能未配置 git.remote):${fetched.message}`
|
|
2838
|
+
};
|
|
2839
|
+
const checked = await deps.git.checkoutFetchHead(dir, files);
|
|
2840
|
+
if (!checked.ok) return {
|
|
2841
|
+
ok: false,
|
|
2842
|
+
action: "keep-remote",
|
|
2843
|
+
message: `从远端检出失败:${checked.message}`
|
|
2844
|
+
};
|
|
2845
|
+
const committed = await deps.git.commit(dir, `resolve conflict (keep remote) ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
2846
|
+
deps.autoCommit();
|
|
2847
|
+
const status = await deps.git.status(dir);
|
|
2848
|
+
return {
|
|
2849
|
+
ok: true,
|
|
2850
|
+
action: "keep-remote",
|
|
2851
|
+
message: `已把 ${files.length} 个冲突文件改为远端版本并提交。建议继续 tiddlywiki_git_sync action=sync 完成整合与推送。`,
|
|
2852
|
+
files,
|
|
2853
|
+
commit: committed.message,
|
|
2854
|
+
status
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
}));
|
|
2135
2858
|
return disposers;
|
|
2136
2859
|
}
|
|
2137
2860
|
function renderSync(value) {
|
|
@@ -2140,7 +2863,7 @@ function renderSync(value) {
|
|
|
2140
2863
|
if (value.conflictFiles !== void 0 && value.conflictFiles.length > 0) {
|
|
2141
2864
|
lines.push(`冲突文件(rebase 已 abort,勿自动覆盖):`);
|
|
2142
2865
|
for (const f of value.conflictFiles) lines.push(` - ${f}`);
|
|
2143
|
-
lines.push("
|
|
2866
|
+
lines.push("处理方式:用 tiddlywiki_git_resolve files=[以上文件] strategy=keep-local|keep-remote 按 tiddler 二选一解决,再重新 sync;也可以直接让用户处理。");
|
|
2144
2867
|
}
|
|
2145
2868
|
if (value.commit !== void 0) lines.push(`本地 commit: ${value.commit}`);
|
|
2146
2869
|
if (value.push !== void 0) lines.push(`远端 push: ${value.push}`);
|
|
@@ -2208,6 +2931,34 @@ const DEFAULTS = {
|
|
|
2208
2931
|
password: ""
|
|
2209
2932
|
}
|
|
2210
2933
|
};
|
|
2934
|
+
/** Config tiddler steering TW's frontend API base (tiddlywebadaptor). */
|
|
2935
|
+
const TW_WEB_HOST_TIDDLER = "$:/config/tiddlyweb/host";
|
|
2936
|
+
/** The legacy default value this plugin replaces with the same-origin proxy. */
|
|
2937
|
+
const TW_WEB_HOST_DEFAULT = "$protocol$//$host$/";
|
|
2938
|
+
/**
|
|
2939
|
+
* Point TW's frontend at the same-origin DSH proxy (remote-access mode, R1).
|
|
2940
|
+
* The tiddlywebadaptor builds every API URL from $:/config/tiddlyweb/host; its
|
|
2941
|
+
* default `$protocol$//$host$/` resolves to the iframe's origin ROOT, which
|
|
2942
|
+
* 404s whenever the browser is not on the same machine as TW. Written only
|
|
2943
|
+
* when the tiddler is missing or still the legacy default — a user override is
|
|
2944
|
+
* honored (e.g. someone who really does expose TW on a dedicated origin).
|
|
2945
|
+
*/
|
|
2946
|
+
async function ensureTwWebHost(client) {
|
|
2947
|
+
if (client === void 0) return;
|
|
2948
|
+
let current;
|
|
2949
|
+
try {
|
|
2950
|
+
current = (await client.get(TW_WEB_HOST_TIDDLER))?.text;
|
|
2951
|
+
} catch {
|
|
2952
|
+
current = void 0;
|
|
2953
|
+
}
|
|
2954
|
+
if (current !== void 0 && current.trim() !== TW_WEB_HOST_DEFAULT) return;
|
|
2955
|
+
await client.put({
|
|
2956
|
+
title: TW_WEB_HOST_TIDDLER,
|
|
2957
|
+
text: TW_PROXY_PATH,
|
|
2958
|
+
type: "text/plain",
|
|
2959
|
+
tags: []
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2211
2962
|
/** Expand $VAR / ${VAR} / %VAR% from process.env (config uses $DSH_HOME). */
|
|
2212
2963
|
function expandEnvPath(input) {
|
|
2213
2964
|
return input.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, k) => process.env[k] ?? "").replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, k) => process.env[k] ?? "").replace(/%([A-Za-z_][A-Za-z0-9_]*%)/g, (_, k) => process.env[k.slice(0, -1)] ?? "");
|
|
@@ -2248,14 +2999,17 @@ const PROMPT_TEXT = `## TiddlyWiki 持久知识库
|
|
|
2248
2999
|
|
|
2249
3000
|
本机有一个 TiddlyWiki 5 持久知识库(wiki 文件夹即 git 仓库)。你可以用工具读写 tiddler:
|
|
2250
3001
|
|
|
2251
|
-
- \`tiddlywiki_search\`(query
|
|
2252
|
-
- \`
|
|
3002
|
+
- \`tiddlywiki_search\`(query 必填;可选 tags[]/tag、since 修改时间、type、limit)检索;\`tiddlywiki_get\`(title)读全文;\`tiddlywiki_put\`(title, text, tags?, fields?)写/覆盖;\`tiddlywiki_batch_put\`(items[])批量写;\`tiddlywiki_rename\`(oldTitle, newTitle, updateRefs?)重命名并尽量同步引用;\`tiddlywiki_delete\`(title)删除。
|
|
3003
|
+
- \`tiddlywiki_recent\`(limit?, since?)看最近修改的笔记;\`tiddlywiki_list_tags\` 看现有 tag 及计数。
|
|
3004
|
+
- \`tiddlywiki_git_sync\`(pull|push|sync)做 git 同步;\`tiddlywiki_git_resolve\`(files, strategy=keep-local|keep-remote|list)在 pull 冲突后按 tiddler 二选一解决。
|
|
2253
3005
|
|
|
2254
3006
|
知识库同步纪律(三条):
|
|
2255
3007
|
1. 开工先 pull:\`tiddlywiki_git_sync action=pull\`(rebase + autostash;真冲突会自动 abort 并报冲突文件)。
|
|
2256
3008
|
2. 收工 commit + push:\`tiddlywiki_git_sync action=sync\`(pull → commit → push)。
|
|
2257
3009
|
3. 插件会自动防抖 commit(默认 60s),手动同步用上面的工具。
|
|
2258
3010
|
|
|
3011
|
+
pull 冲突后:先 \`tiddlywiki_git_resolve files=[冲突文件] strategy=keep-local\`(保留本地)或 \`strategy=keep-remote\`(改用远端版本),再重新 pull/sync 整合其余改动。
|
|
3012
|
+
|
|
2259
3013
|
把 wiki 当作长期记忆与知识沉淀的地方:会议纪要、决策记录、调研笔记、随手的想法都可存成独立 tiddler(tag 建议用 inbox/meeting/decision 等便于检索)。
|
|
2260
3014
|
|
|
2261
3015
|
用本插件自动创建笔记时,除了业务性 tag 外,请把「当前工作区(项目)的名字」也作为标签之一加上去(例如 \`tiddlywiki_put\` 的 tags 里带上当前 workspace 名),这样笔记能按项目归集、检索。`;
|
|
@@ -2377,6 +3131,11 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2377
3131
|
try {
|
|
2378
3132
|
await server.start();
|
|
2379
3133
|
await configStore.load(client());
|
|
3134
|
+
try {
|
|
3135
|
+
await ensureTwWebHost(client());
|
|
3136
|
+
} catch (err) {
|
|
3137
|
+
console.warn("[dsh-tiddlywiki] ensuring $:/config/tiddlyweb/host:", err);
|
|
3138
|
+
}
|
|
2380
3139
|
try {
|
|
2381
3140
|
const seedClient = client();
|
|
2382
3141
|
if (seedClient !== void 0) await seedDocNote(seedClient);
|
|
@@ -2427,11 +3186,16 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2427
3186
|
};
|
|
2428
3187
|
});
|
|
2429
3188
|
ctx.effect(() => () => {
|
|
2430
|
-
|
|
2431
|
-
|
|
3189
|
+
(async () => {
|
|
3190
|
+
try {
|
|
3191
|
+
await committer?.flush();
|
|
3192
|
+
} catch {}
|
|
3193
|
+
disposeAll();
|
|
3194
|
+
await server.stop();
|
|
3195
|
+
})();
|
|
2432
3196
|
}, "dsh-tiddlywiki: host teardown");
|
|
2433
3197
|
}
|
|
2434
3198
|
//#endregion
|
|
2435
|
-
export { AutoCommitter, ConfigStore, DOC_NOTE_TAG, DOC_NOTE_TEXT, DOC_NOTE_TITLE, GitFace, PATH_PREFIX, TiddlyWebClient, WikiServer, apply, bundledCatalog, deepMerge, defineTool, dshHomePath, ensureLanguage, inject, name, normalizeThemes, openInTwEditor, readWikiInfo, registerAdminRoutes, registerRoutes, resolveTwRoot, seedDocNote, writeWikiInfo };
|
|
3199
|
+
export { AutoCommitter, ConfigStore, DOC_NOTE_TAG, DOC_NOTE_TEXT, DOC_NOTE_TITLE, GitFace, PATH_PREFIX, TW_PROXY_PATH, TW_PROXY_PREFIX, TW_WEB_HOST_TIDDLER, TiddlyWebClient, WikiServer, apply, bundledCatalog, deepMerge, defineTool, dshHomePath, ensureLanguage, ensureTwWebHost, inject, name, normalizeThemes, openInTwEditor, readWikiInfo, registerAdminRoutes, registerRoutes, resolveTwRoot, seedDocNote, writeWikiInfo };
|
|
2436
3200
|
|
|
2437
3201
|
//# sourceMappingURL=index.js.map
|