dsh-tiddlywiki 0.16.11 → 0.16.13

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/lib/index.js CHANGED
@@ -6,18 +6,6 @@ import { execFile, spawn } from "node:child_process";
6
6
  import { promisify } from "node:util";
7
7
  import { createServer } from "node:net";
8
8
  import { homedir } from "node:os";
9
- //#region \0rolldown/runtime.js
10
- var __defProp = Object.defineProperty;
11
- var __exportAll = (all, no_symbols) => {
12
- let target = {};
13
- for (var name in all) __defProp(target, name, {
14
- get: all[name],
15
- enumerable: true
16
- });
17
- if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
18
- return target;
19
- };
20
- //#endregion
21
9
  //#region src/host/git.ts
22
10
  /**
23
11
  * Git face (design doc §7, D11) — the ONLY place dsh-tiddlywiki shells out to
@@ -36,10 +24,6 @@ var __exportAll = (all, no_symbols) => {
36
24
  *
37
25
  * @module dsh-tiddlywiki/host/git
38
26
  */
39
- var git_exports = /* @__PURE__ */ __exportAll({
40
- AutoCommitter: () => AutoCommitter,
41
- GitFace: () => GitFace
42
- });
43
27
  const execFileP = promisify(execFile);
44
28
  /** Timeout for quick read-only queries. */
45
29
  const QUICK_TIMEOUT_MS = 5e3;
@@ -706,9 +690,23 @@ function collectDescendantIds(nodes, out) {
706
690
  }
707
691
  }
708
692
  }
693
+ /**
694
+ * 汇总条目会直接写进 `[[标题]]` wikitext——标题既要能安全渲染、又不能制造死链。
695
+ * 会话日志里常混入模型的示例/残缺写法(`#…`、`#标题`、带反引号的截断链接、
696
+ * 控制字符等),这类收进来会被 TW 渲染成「佚失条目」;一律过滤。
697
+ */
698
+ function isPlausibleTitle(title) {
699
+ if (title.length === 0 || title.length > 300) return false;
700
+ if (title.trim().length === 0) return false;
701
+ if (/[\u0000-\u001f\u007f]/.test(title)) return false;
702
+ if (/[\]|`#]/.test(title)) return false;
703
+ if (title.startsWith("…") || title === "标题") return false;
704
+ return true;
705
+ }
709
706
  /** 记录一篇笔记的触达;同标题合并(时间取最新、detail 取最新、subagent 取 AND)。 */
710
707
  function record(map, entry) {
711
708
  if (entry.title.startsWith("$:/")) return;
709
+ if (!isPlausibleTitle(entry.title)) return;
712
710
  const prev = map.get(entry.title);
713
711
  if (prev !== void 0) {
714
712
  prev.time = Math.max(prev.time, entry.time);
@@ -720,7 +718,7 @@ function record(map, entry) {
720
718
  }
721
719
  /** 扫助手回复文本里的 `/dsh-tiddlywiki/tw/#标题` 引用链接 → 读取。 */
722
720
  function scanRefs(text, time, subagent, collected) {
723
- const re = /\/dsh-tiddlywiki\/tw\/#([^)\s\]]+)/g;
721
+ const re = /\/dsh-tiddlywiki\/tw\/#([^#)\s\]]+)/g;
724
722
  let m;
725
723
  while ((m = re.exec(text)) !== null) {
726
724
  const raw = m[1] ?? "";
@@ -829,29 +827,33 @@ function scanSnapshot(snap, subagent, collected) {
829
827
  }
830
828
  }
831
829
  }
832
- /** 逐个查询每篇笔记的当前状态(404 → 不存在/已删除)。 */
830
+ /** 并发(8 路)查询每篇笔记的当前状态(404 → 不存在/已删除)。 */
833
831
  async function enrichTitles(client, titles) {
834
832
  const out = /* @__PURE__ */ new Map();
835
- for (const title of titles) try {
836
- const t = await client.get(title);
837
- if (t === void 0) out.set(title, {
838
- exists: false,
839
- tags: []
840
- });
841
- else {
833
+ const enquire = async (title) => {
834
+ try {
835
+ const t = await client.get(title);
836
+ if (t === void 0) return [title, {
837
+ exists: false,
838
+ tags: []
839
+ }];
842
840
  const flat = typeof t.text === "string" ? t.text.replace(/\s+/g, " ").trim() : "";
843
- out.set(title, {
841
+ return [title, {
844
842
  exists: true,
845
843
  tags: t.tags ?? [],
846
844
  modified: typeof t.modified === "string" ? t.modified : void 0,
847
845
  snippet: flat.length > 48 ? `${flat.slice(0, 48)}…` : flat
848
- });
846
+ }];
847
+ } catch {
848
+ return [title, {
849
+ exists: false,
850
+ tags: []
851
+ }];
849
852
  }
850
- } catch {
851
- out.set(title, {
852
- exists: false,
853
- tags: []
854
- });
853
+ };
854
+ for (let i = 0; i < titles.length; i += 8) {
855
+ const batch = titles.slice(i, i + 8);
856
+ for (const [title, state] of await Promise.all(batch.map(enquire))) out.set(title, state);
855
857
  }
856
858
  return out;
857
859
  }
@@ -978,19 +980,14 @@ async function writeSessionSummary(client, sq, sessionId) {
978
980
  };
979
981
  }
980
982
  //#endregion
981
- //#region src/host/routes.ts
982
- const ROUTE_PREFIX = PATH_PREFIX;
983
- /** Max JSON body for note/restart. */
984
- const MAX_BODY_BYTES = 2 * 1024 * 1024;
985
- /** Max passthrough body (tiddler content can be large). */
983
+ //#region src/host/http.ts
984
+ /** Default cap for small JSON bodies (note/restart/config…). */
985
+ const MAX_JSON_BODY_BYTES = 2 * 1024 * 1024;
986
+ /** Cap for the /api passthrough body (tiddler content can be large). */
986
987
  const MAX_PROXY_BODY_BYTES = 16 * 1024 * 1024;
987
- /** Max uploaded file body. */
988
+ /** Cap for uploaded file bodies (/tw proxy, /upload). */
988
989
  const MAX_UPLOAD_BYTES = 64 * 1024 * 1024;
989
- /** Tiddler type for quick-notes: Markdown, so the uploaded images/links and
990
- * any Markdown in the note actually render in TW (a type-less tiddler is
991
- * treated as plain wiki text and shows raw `![..]`/`[..]` instead). */
992
- const NOTE_TYPE = "text/markdown";
993
- function readBody$1(req, limit = MAX_BODY_BYTES) {
990
+ function readBodyStream(req, limit, encoding) {
994
991
  return new Promise((resolveP, rejectP) => {
995
992
  let size = 0;
996
993
  const chunks = [];
@@ -1003,28 +1000,36 @@ function readBody$1(req, limit = MAX_BODY_BYTES) {
1003
1000
  }
1004
1001
  chunks.push(chunk);
1005
1002
  });
1006
- req.on("end", () => resolveP(Buffer.concat(chunks).toString("utf8")));
1003
+ req.on("end", () => {
1004
+ const buf = Buffer.concat(chunks);
1005
+ resolveP(encoding ? buf.toString("utf8") : buf);
1006
+ });
1007
1007
  req.on("error", rejectP);
1008
1008
  });
1009
1009
  }
1010
- /** Read a raw (binary-safe) request body up to `limit` bytes. */
1010
+ /** Read a JSON (utf8) request body up to `limit` bytes (default MAX_JSON_BODY_BYTES). */
1011
+ function readBody(req, limit = MAX_JSON_BODY_BYTES) {
1012
+ return readBodyStream(req, limit, true);
1013
+ }
1014
+ /** Read a raw (binary-safe) request body up to `limit` bytes (default MAX_UPLOAD_BYTES). */
1011
1015
  function readBodyBuffer(req, limit = MAX_UPLOAD_BYTES) {
1012
- return new Promise((resolveP, rejectP) => {
1013
- let size = 0;
1014
- const chunks = [];
1015
- req.on("data", (chunk) => {
1016
- size += chunk.length;
1017
- if (size > limit) {
1018
- rejectP(/* @__PURE__ */ new Error("file too large"));
1019
- req.destroy();
1020
- return;
1021
- }
1022
- chunks.push(chunk);
1023
- });
1024
- req.on("end", () => resolveP(Buffer.concat(chunks)));
1025
- req.on("error", rejectP);
1016
+ return readBodyStream(req, limit, false);
1017
+ }
1018
+ /** Write a JSON response (no-store, utf-8). */
1019
+ function json(res, payload, status = 200) {
1020
+ res.writeHead(status, {
1021
+ "content-type": "application/json; charset=utf-8",
1022
+ "cache-control": "no-store"
1026
1023
  });
1024
+ res.end(JSON.stringify(payload));
1027
1025
  }
1026
+ //#endregion
1027
+ //#region src/host/routes.ts
1028
+ const ROUTE_PREFIX = PATH_PREFIX;
1029
+ /** Tiddler type for quick-notes: Markdown, so the uploaded images/links and
1030
+ * any Markdown in the note actually render in TW (a type-less tiddler is
1031
+ * treated as plain wiki text and shows raw `![..]`/`[..]` instead). */
1032
+ const NOTE_TYPE = "text/markdown";
1028
1033
  /** Header names forwarded to the upstream TW service by the proxy routes. */
1029
1034
  const FORWARD_HEADER_NAMES = [
1030
1035
  "accept",
@@ -1059,14 +1064,6 @@ function sanitizeUploadName(input) {
1059
1064
  if (name.length > 160) return name.slice(0, 160);
1060
1065
  return name;
1061
1066
  }
1062
- function json$1(res, payload, status = 200) {
1063
- const body = JSON.stringify(payload);
1064
- res.writeHead(status, {
1065
- "content-type": "application/json; charset=utf-8",
1066
- "cache-control": "no-store"
1067
- });
1068
- res.end(body);
1069
- }
1070
1067
  function pad(n) {
1071
1068
  return n < 10 ? `0${n}` : String(n);
1072
1069
  }
@@ -1138,7 +1135,7 @@ function registerRoutes(ctx, deps) {
1138
1135
  } catch {
1139
1136
  gitSummary = null;
1140
1137
  }
1141
- json$1(res, {
1138
+ json(res, {
1142
1139
  ok: true,
1143
1140
  ...view,
1144
1141
  twProxy: TW_PROXY_PATH,
@@ -1158,11 +1155,11 @@ function registerRoutes(ctx, deps) {
1158
1155
  try {
1159
1156
  let body = {};
1160
1157
  try {
1161
- body = JSON.parse(await readBody$1(req));
1158
+ body = JSON.parse(await readBody(req));
1162
1159
  } catch {}
1163
1160
  const session = typeof body.session === "string" && body.session.trim().length > 0 ? body.session.trim() : "";
1164
1161
  if (session.length === 0) {
1165
- json$1(res, {
1162
+ json(res, {
1166
1163
  ok: false,
1167
1164
  error: "session is required"
1168
1165
  }, 400);
@@ -1170,7 +1167,7 @@ function registerRoutes(ctx, deps) {
1170
1167
  }
1171
1168
  const client = deps.getClient();
1172
1169
  if (client === void 0) {
1173
- json$1(res, {
1170
+ json(res, {
1174
1171
  ok: false,
1175
1172
  error: "wiki service is not running"
1176
1173
  }, 503);
@@ -1178,19 +1175,19 @@ function registerRoutes(ctx, deps) {
1178
1175
  }
1179
1176
  const sq = deps.getSessionQuery();
1180
1177
  if (sq === void 0) {
1181
- json$1(res, {
1178
+ json(res, {
1182
1179
  ok: false,
1183
1180
  error: "session query service unavailable"
1184
1181
  }, 503);
1185
1182
  return;
1186
1183
  }
1187
- json$1(res, {
1184
+ json(res, {
1188
1185
  ok: true,
1189
1186
  ...await writeSessionSummary(client, sq, session),
1190
1187
  twUrl: TW_PROXY_PATH
1191
1188
  });
1192
1189
  } catch (err) {
1193
- json$1(res, {
1190
+ json(res, {
1194
1191
  ok: false,
1195
1192
  error: err instanceof Error ? err.message : String(err)
1196
1193
  }, 500);
@@ -1207,7 +1204,7 @@ function registerRoutes(ctx, deps) {
1207
1204
  try {
1208
1205
  const sc = deps.getSessionController();
1209
1206
  if (sc === void 0) {
1210
- json$1(res, {
1207
+ json(res, {
1211
1208
  ok: false,
1212
1209
  error: "session service unavailable"
1213
1210
  }, 503);
@@ -1220,7 +1217,7 @@ function registerRoutes(ctx, deps) {
1220
1217
  const headers = await pers.list(AbortSignal.timeout(5e3));
1221
1218
  for (const h of headers) if (typeof h.agentPreset === "string" && h.agentPreset.length > 0) presetById[h.id] = h.agentPreset;
1222
1219
  } catch {}
1223
- json$1(res, {
1220
+ json(res, {
1224
1221
  ok: true,
1225
1222
  items: (list.items ?? []).filter((s) => s.parentSessionId === void 0).map((s) => ({
1226
1223
  sessionId: s.sessionId,
@@ -1232,40 +1229,47 @@ function registerRoutes(ctx, deps) {
1232
1229
  })).sort((a, b) => b.updatedAt - a.updatedAt)
1233
1230
  });
1234
1231
  } catch (err) {
1235
- json$1(res, {
1232
+ json(res, {
1236
1233
  ok: false,
1237
1234
  error: err instanceof Error ? err.message : String(err)
1238
1235
  }, 500);
1239
1236
  }
1240
1237
  };
1241
1238
  /**
1239
+ * Shared gate for the TW-side send-to-agent routes: feature switch
1240
+ * (`ui.sendToAgent.enabled`) then, when a shared token is configured, the
1241
+ * `x-send-to-agent-token` header must match. Returns false after writing the
1242
+ * error response — the caller just does `if (!guard(...)) return`.
1243
+ */
1244
+ const guardSendToAgent = (req, res) => {
1245
+ if (!deps.sendToAgentEnabled()) {
1246
+ json(res, {
1247
+ ok: false,
1248
+ error: "send-to-agent is disabled"
1249
+ }, 403);
1250
+ return false;
1251
+ }
1252
+ const token = deps.sendToAgentToken().trim();
1253
+ if (token.length === 0) return true;
1254
+ const got = req.headers["x-send-to-agent-token"];
1255
+ if ((typeof got === "string" ? got : Array.isArray(got) ? got[0] ?? "" : "") === token) return true;
1256
+ json(res, {
1257
+ ok: false,
1258
+ error: "unauthorized"
1259
+ }, 401);
1260
+ return false;
1261
+ };
1262
+ /**
1242
1263
  * GET /dsh-tiddlywiki/agent/modes — available "工作模式" (Agent presets) for
1243
1264
  * the TW picker: id/name/description per preset plus the deployment default.
1244
1265
  * Guards mirror the other agent routes (feature switch + optional token).
1245
1266
  */
1246
1267
  const handleAgentModes = async (req, res) => {
1247
1268
  try {
1248
- if (!deps.sendToAgentEnabled()) {
1249
- json$1(res, {
1250
- ok: false,
1251
- error: "send-to-agent is disabled"
1252
- }, 403);
1253
- return;
1254
- }
1255
- const token = deps.sendToAgentToken().trim();
1256
- if (token.length > 0) {
1257
- const got = req.headers["x-send-to-agent-token"];
1258
- if ((typeof got === "string" ? got : Array.isArray(got) ? got[0] ?? "" : "") !== token) {
1259
- json$1(res, {
1260
- ok: false,
1261
- error: "unauthorized"
1262
- }, 401);
1263
- return;
1264
- }
1265
- }
1269
+ if (!guardSendToAgent(req, res)) return;
1266
1270
  const ap = deps.getAgentPresets();
1267
1271
  if (ap === void 0) {
1268
- json$1(res, {
1272
+ json(res, {
1269
1273
  ok: false,
1270
1274
  error: "agent presets service unavailable"
1271
1275
  }, 503);
@@ -1288,7 +1292,7 @@ function registerRoutes(ctx, deps) {
1288
1292
  } catch {
1289
1293
  permissions = null;
1290
1294
  }
1291
- json$1(res, {
1295
+ json(res, {
1292
1296
  ok: true,
1293
1297
  defaultId: defaultId ?? null,
1294
1298
  items: presets.map((p) => ({
@@ -1302,7 +1306,7 @@ function registerRoutes(ctx, deps) {
1302
1306
  permissions
1303
1307
  });
1304
1308
  } catch (err) {
1305
- json$1(res, {
1309
+ json(res, {
1306
1310
  ok: false,
1307
1311
  error: err instanceof Error ? err.message : String(err)
1308
1312
  }, 500);
@@ -1315,29 +1319,12 @@ function registerRoutes(ctx, deps) {
1315
1319
  */
1316
1320
  const handleAgentSend = async (req, res) => {
1317
1321
  try {
1318
- if (!deps.sendToAgentEnabled()) {
1319
- json$1(res, {
1320
- ok: false,
1321
- error: "send-to-agent is disabled"
1322
- }, 403);
1323
- return;
1324
- }
1325
- const token = deps.sendToAgentToken().trim();
1326
- if (token.length > 0) {
1327
- const got = req.headers["x-send-to-agent-token"];
1328
- if ((typeof got === "string" ? got : Array.isArray(got) ? got[0] ?? "" : "") !== token) {
1329
- json$1(res, {
1330
- ok: false,
1331
- error: "unauthorized"
1332
- }, 401);
1333
- return;
1334
- }
1335
- }
1336
- const body = JSON.parse(await readBody$1(req));
1322
+ if (!guardSendToAgent(req, res)) return;
1323
+ const body = JSON.parse(await readBody(req));
1337
1324
  const sessionId = typeof body.sessionId === "string" && body.sessionId.trim().length > 0 ? body.sessionId.trim() : "";
1338
1325
  const text = typeof body.text === "string" && body.text.trim().length > 0 ? body.text.trim() : "";
1339
1326
  if (sessionId.length === 0 || text.length === 0) {
1340
- json$1(res, {
1327
+ json(res, {
1341
1328
  ok: false,
1342
1329
  error: "sessionId and text are required"
1343
1330
  }, 400);
@@ -1345,7 +1332,7 @@ function registerRoutes(ctx, deps) {
1345
1332
  }
1346
1333
  const sc = deps.getSessionController();
1347
1334
  if (sc === void 0) {
1348
- json$1(res, {
1335
+ json(res, {
1349
1336
  ok: false,
1350
1337
  error: "session service unavailable"
1351
1338
  }, 503);
@@ -1361,13 +1348,13 @@ function registerRoutes(ctx, deps) {
1361
1348
  text
1362
1349
  }]
1363
1350
  }, AbortSignal.timeout(2e4));
1364
- json$1(res, {
1351
+ json(res, {
1365
1352
  ok: true,
1366
1353
  requestId,
1367
1354
  sessionId
1368
1355
  });
1369
1356
  } catch (err) {
1370
- json$1(res, {
1357
+ json(res, {
1371
1358
  ok: false,
1372
1359
  error: err instanceof Error ? err.message : String(err)
1373
1360
  }, 500);
@@ -1395,39 +1382,22 @@ function registerRoutes(ctx, deps) {
1395
1382
  */
1396
1383
  const handleAgentCreate = async (req, res) => {
1397
1384
  try {
1398
- if (!deps.sendToAgentEnabled()) {
1399
- json$1(res, {
1400
- ok: false,
1401
- error: "send-to-agent is disabled"
1402
- }, 403);
1403
- return;
1404
- }
1405
- const token = deps.sendToAgentToken().trim();
1406
- if (token.length > 0) {
1407
- const got = req.headers["x-send-to-agent-token"];
1408
- if ((typeof got === "string" ? got : Array.isArray(got) ? got[0] ?? "" : "") !== token) {
1409
- json$1(res, {
1410
- ok: false,
1411
- error: "unauthorized"
1412
- }, 401);
1413
- return;
1414
- }
1415
- }
1416
- const body = JSON.parse(await readBody$1(req));
1385
+ if (!guardSendToAgent(req, res)) return;
1386
+ const body = JSON.parse(await readBody(req));
1417
1387
  const cwd = typeof body.cwd === "string" ? body.cwd.trim() : "";
1418
1388
  const mode = typeof body.mode === "string" && body.mode.trim().length > 0 ? body.mode.trim() : void 0;
1419
1389
  const permission = typeof body.permission === "string" && body.permission.trim().length > 0 ? body.permission.trim() : void 0;
1420
1390
  const pp = deps.getPermissionPresets();
1421
1391
  if (permission !== void 0) {
1422
1392
  if (pp === void 0) {
1423
- json$1(res, {
1393
+ json(res, {
1424
1394
  ok: false,
1425
1395
  error: "permission selected but the permission-presets service is unavailable"
1426
1396
  }, 503);
1427
1397
  return;
1428
1398
  }
1429
1399
  if (!pp.names.includes(permission)) {
1430
- json$1(res, {
1400
+ json(res, {
1431
1401
  ok: false,
1432
1402
  error: `unknown permission preset "${permission}" (available: ${pp.names.join(", ")})`
1433
1403
  }, 400);
@@ -1436,7 +1406,7 @@ function registerRoutes(ctx, deps) {
1436
1406
  }
1437
1407
  const sc = deps.getSessionController();
1438
1408
  if (sc === void 0) {
1439
- json$1(res, {
1409
+ json(res, {
1440
1410
  ok: false,
1441
1411
  error: "session service unavailable"
1442
1412
  }, 503);
@@ -1467,7 +1437,7 @@ function registerRoutes(ctx, deps) {
1467
1437
  }
1468
1438
  } catch {}
1469
1439
  }
1470
- json$1(res, {
1440
+ json(res, {
1471
1441
  ok: true,
1472
1442
  sessionId: created.sessionId,
1473
1443
  cwd: cwd || null,
@@ -1477,7 +1447,7 @@ function registerRoutes(ctx, deps) {
1477
1447
  permissionApplied
1478
1448
  });
1479
1449
  } catch (err) {
1480
- json$1(res, {
1450
+ json(res, {
1481
1451
  ok: false,
1482
1452
  error: err instanceof Error ? err.message : String(err)
1483
1453
  }, 500);
@@ -1485,10 +1455,10 @@ function registerRoutes(ctx, deps) {
1485
1455
  };
1486
1456
  const handleNote = async (req, res) => {
1487
1457
  try {
1488
- const body = JSON.parse(await readBody$1(req));
1458
+ const body = JSON.parse(await readBody(req));
1489
1459
  const text = typeof body.text === "string" && body.text.trim().length > 0 ? body.text.trim() : null;
1490
1460
  if (text === null) {
1491
- json$1(res, {
1461
+ json(res, {
1492
1462
  ok: false,
1493
1463
  error: "text is required"
1494
1464
  }, 400);
@@ -1496,7 +1466,7 @@ function registerRoutes(ctx, deps) {
1496
1466
  }
1497
1467
  const client = deps.getClient();
1498
1468
  if (client === void 0) {
1499
- json$1(res, {
1469
+ json(res, {
1500
1470
  ok: false,
1501
1471
  error: "wiki service is not running"
1502
1472
  }, 503);
@@ -1511,7 +1481,7 @@ function registerRoutes(ctx, deps) {
1511
1481
  type: NOTE_TYPE
1512
1482
  });
1513
1483
  deps.autoCommit();
1514
- json$1(res, {
1484
+ json(res, {
1515
1485
  ok: true,
1516
1486
  title,
1517
1487
  tag: tags.join(" "),
@@ -1520,7 +1490,7 @@ function registerRoutes(ctx, deps) {
1520
1490
  type: NOTE_TYPE
1521
1491
  });
1522
1492
  } catch (err) {
1523
- json$1(res, {
1493
+ json(res, {
1524
1494
  ok: false,
1525
1495
  error: err instanceof Error ? err.message : String(err)
1526
1496
  }, 500);
@@ -1528,10 +1498,10 @@ function registerRoutes(ctx, deps) {
1528
1498
  };
1529
1499
  const handleEdit = async (req, res) => {
1530
1500
  try {
1531
- const body = JSON.parse(await readBody$1(req));
1501
+ const body = JSON.parse(await readBody(req));
1532
1502
  const client = deps.getClient();
1533
1503
  if (client === void 0) {
1534
- json$1(res, {
1504
+ json(res, {
1535
1505
  ok: false,
1536
1506
  error: "wiki service is not running"
1537
1507
  }, 503);
@@ -1541,13 +1511,13 @@ function registerRoutes(ctx, deps) {
1541
1511
  const tags = resolveTags(body, deps.noteDefaults().tag);
1542
1512
  const result = await openInTwEditor(client, title, typeof body.text === "string" ? body.text : "", tags);
1543
1513
  deps.autoCommit();
1544
- json$1(res, {
1514
+ json(res, {
1545
1515
  ok: true,
1546
1516
  ...result,
1547
1517
  twUrl: TW_PROXY_PATH
1548
1518
  });
1549
1519
  } catch (err) {
1550
- json$1(res, {
1520
+ json(res, {
1551
1521
  ok: false,
1552
1522
  error: err instanceof Error ? err.message : String(err)
1553
1523
  }, 500);
@@ -1560,7 +1530,7 @@ function registerRoutes(ctx, deps) {
1560
1530
  const handleTags = async (_req, res) => {
1561
1531
  const client = deps.getClient();
1562
1532
  if (client === void 0) {
1563
- json$1(res, {
1533
+ json(res, {
1564
1534
  ok: false,
1565
1535
  error: "wiki service is not running"
1566
1536
  }, 503);
@@ -1571,7 +1541,7 @@ function registerRoutes(ctx, deps) {
1571
1541
  const counts = /* @__PURE__ */ new Map();
1572
1542
  for (const item of items) for (const tag of item.tags ?? []) if (tag.length > 0 && !tag.startsWith("$:/")) counts.set(tag, (counts.get(tag) ?? 0) + 1);
1573
1543
  const tags = [...counts.keys()].sort((a, b) => a.localeCompare(b, "zh"));
1574
- json$1(res, {
1544
+ json(res, {
1575
1545
  ok: true,
1576
1546
  tags,
1577
1547
  items: tags.map((tag) => ({
@@ -1580,7 +1550,7 @@ function registerRoutes(ctx, deps) {
1580
1550
  }))
1581
1551
  });
1582
1552
  } catch (err) {
1583
- json$1(res, {
1553
+ json(res, {
1584
1554
  ok: false,
1585
1555
  error: err instanceof Error ? err.message : String(err)
1586
1556
  }, 500);
@@ -1590,7 +1560,7 @@ function registerRoutes(ctx, deps) {
1590
1560
  const handleRecent = async (req, res) => {
1591
1561
  const client = deps.getClient();
1592
1562
  if (client === void 0) {
1593
- json$1(res, {
1563
+ json(res, {
1594
1564
  ok: false,
1595
1565
  error: "wiki service is not running"
1596
1566
  }, 503);
@@ -1600,7 +1570,7 @@ function registerRoutes(ctx, deps) {
1600
1570
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
1601
1571
  const limitRaw = Number(url.searchParams.get("limit") ?? 15);
1602
1572
  const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(Math.floor(limitRaw), 200)) : 15;
1603
- json$1(res, {
1573
+ json(res, {
1604
1574
  ok: true,
1605
1575
  limit,
1606
1576
  items: (await client.recent(limit)).map((t) => ({
@@ -1611,7 +1581,7 @@ function registerRoutes(ctx, deps) {
1611
1581
  }))
1612
1582
  });
1613
1583
  } catch (err) {
1614
- json$1(res, {
1584
+ json(res, {
1615
1585
  ok: false,
1616
1586
  error: err instanceof Error ? err.message : String(err)
1617
1587
  }, 500);
@@ -1621,7 +1591,7 @@ function registerRoutes(ctx, deps) {
1621
1591
  const handleGet = async (req, res) => {
1622
1592
  const client = deps.getClient();
1623
1593
  if (client === void 0) {
1624
- json$1(res, {
1594
+ json(res, {
1625
1595
  ok: false,
1626
1596
  error: "wiki service is not running"
1627
1597
  }, 503);
@@ -1630,7 +1600,7 @@ function registerRoutes(ctx, deps) {
1630
1600
  try {
1631
1601
  const title = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("title") ?? "";
1632
1602
  if (title.length === 0) {
1633
- json$1(res, {
1603
+ json(res, {
1634
1604
  ok: false,
1635
1605
  error: "missing title"
1636
1606
  }, 400);
@@ -1638,7 +1608,7 @@ function registerRoutes(ctx, deps) {
1638
1608
  }
1639
1609
  const t = await client.get(title);
1640
1610
  if (t === void 0) {
1641
- json$1(res, {
1611
+ json(res, {
1642
1612
  ok: false,
1643
1613
  notFound: true,
1644
1614
  title
@@ -1650,7 +1620,7 @@ function registerRoutes(ctx, deps) {
1650
1620
  if (k === "title" || k === "text" || k === "tags") continue;
1651
1621
  fields[k] = v;
1652
1622
  }
1653
- json$1(res, {
1623
+ json(res, {
1654
1624
  ok: true,
1655
1625
  title: t.title,
1656
1626
  text: t.text ?? "",
@@ -1660,7 +1630,7 @@ function registerRoutes(ctx, deps) {
1660
1630
  fields
1661
1631
  });
1662
1632
  } catch (err) {
1663
- json$1(res, {
1633
+ json(res, {
1664
1634
  ok: false,
1665
1635
  error: err instanceof Error ? err.message : String(err)
1666
1636
  }, 500);
@@ -1675,7 +1645,7 @@ function registerRoutes(ctx, deps) {
1675
1645
  const handleSearch = async (req, res) => {
1676
1646
  const client = deps.getClient();
1677
1647
  if (client === void 0) {
1678
- json$1(res, {
1648
+ json(res, {
1679
1649
  ok: false,
1680
1650
  error: "wiki service is not running"
1681
1651
  }, 503);
@@ -1697,7 +1667,7 @@ function registerRoutes(ctx, deps) {
1697
1667
  type,
1698
1668
  limit
1699
1669
  });
1700
- json$1(res, {
1670
+ json(res, {
1701
1671
  ok: true,
1702
1672
  query,
1703
1673
  tags,
@@ -1714,7 +1684,7 @@ function registerRoutes(ctx, deps) {
1714
1684
  }))
1715
1685
  });
1716
1686
  } catch (err) {
1717
- json$1(res, {
1687
+ json(res, {
1718
1688
  ok: false,
1719
1689
  error: err instanceof Error ? err.message : String(err)
1720
1690
  }, 500);
@@ -1723,12 +1693,12 @@ function registerRoutes(ctx, deps) {
1723
1693
  const handleRestart = async (_req, res) => {
1724
1694
  try {
1725
1695
  await deps.server.restart();
1726
- json$1(res, {
1696
+ json(res, {
1727
1697
  ok: true,
1728
1698
  status: deps.server.status().status
1729
1699
  });
1730
1700
  } catch (err) {
1731
- json$1(res, {
1701
+ json(res, {
1732
1702
  ok: false,
1733
1703
  error: err instanceof Error ? err.message : String(err)
1734
1704
  }, 500);
@@ -1752,7 +1722,7 @@ function registerRoutes(ctx, deps) {
1752
1722
  try {
1753
1723
  const pulled = await deps.git.pull(dir);
1754
1724
  if (!pulled.ok) {
1755
- json$1(res, {
1725
+ json(res, {
1756
1726
  ok: false,
1757
1727
  action: "sync",
1758
1728
  message: pulled.message,
@@ -1772,7 +1742,7 @@ function registerRoutes(ctx, deps) {
1772
1742
  const committed = await deps.git.commit(dir, `sync ${(/* @__PURE__ */ new Date()).toISOString()}`);
1773
1743
  const pushed = await deps.git.push(dir);
1774
1744
  const fresh = await status();
1775
- json$1(res, {
1745
+ json(res, {
1776
1746
  ok: pushed.ok,
1777
1747
  action: "sync",
1778
1748
  message: pushed.ok ? "同步完成" : pushed.message,
@@ -1786,7 +1756,7 @@ function registerRoutes(ctx, deps) {
1786
1756
  lastSync: (/* @__PURE__ */ new Date()).toISOString()
1787
1757
  }, pushed.ok ? 200 : 502);
1788
1758
  } catch (err) {
1789
- json$1(res, {
1759
+ json(res, {
1790
1760
  ok: false,
1791
1761
  error: err instanceof Error ? err.message : String(err)
1792
1762
  }, 500);
@@ -1812,7 +1782,7 @@ function registerRoutes(ctx, deps) {
1812
1782
  name = sanitizeUploadName(decoded);
1813
1783
  }
1814
1784
  if (name.length === 0) {
1815
- json$1(res, {
1785
+ json(res, {
1816
1786
  ok: false,
1817
1787
  error: "missing or invalid filename"
1818
1788
  }, 400);
@@ -1833,7 +1803,7 @@ function registerRoutes(ctx, deps) {
1833
1803
  }
1834
1804
  await writeFile(join(filesDir, candidate), buf);
1835
1805
  deps.autoCommit();
1836
- json$1(res, {
1806
+ json(res, {
1837
1807
  ok: true,
1838
1808
  name: candidate,
1839
1809
  path: `files/${candidate}`,
@@ -1842,7 +1812,7 @@ function registerRoutes(ctx, deps) {
1842
1812
  type: req.headers["content-type"] ?? "application/octet-stream"
1843
1813
  });
1844
1814
  } catch (err) {
1845
- json$1(res, {
1815
+ json(res, {
1846
1816
  ok: false,
1847
1817
  error: err instanceof Error ? err.message : String(err)
1848
1818
  }, err instanceof Error && /too large/.test(err.message) ? 413 : 500);
@@ -1851,7 +1821,7 @@ function registerRoutes(ctx, deps) {
1851
1821
  /** Passthrough /dsh-tiddlywiki/api/<rest> → TW root /<rest>. */
1852
1822
  const handleApiProxy = async (req, res) => {
1853
1823
  if (deps.getClient() === void 0) {
1854
- json$1(res, {
1824
+ json(res, {
1855
1825
  ok: false,
1856
1826
  error: "wiki service is not running"
1857
1827
  }, 503);
@@ -1870,7 +1840,7 @@ function registerRoutes(ctx, deps) {
1870
1840
  headers,
1871
1841
  signal: AbortSignal.timeout(15e3)
1872
1842
  };
1873
- if (method === "PUT" || method === "POST") init.body = await readBody$1(req, MAX_PROXY_BODY_BYTES);
1843
+ if (method === "PUT" || method === "POST") init.body = await readBody(req, MAX_PROXY_BODY_BYTES);
1874
1844
  const upstream = await fetch(`${deps.server.url}${rest}${url.search}`, init);
1875
1845
  const data = await upstream.text();
1876
1846
  res.writeHead(upstream.status, {
@@ -1879,7 +1849,7 @@ function registerRoutes(ctx, deps) {
1879
1849
  });
1880
1850
  res.end(data);
1881
1851
  } catch (err) {
1882
- json$1(res, {
1852
+ json(res, {
1883
1853
  ok: false,
1884
1854
  error: err instanceof Error ? err.message : String(err)
1885
1855
  }, 502);
@@ -1896,7 +1866,7 @@ function registerRoutes(ctx, deps) {
1896
1866
  */
1897
1867
  const handleTwProxy = async (req, res) => {
1898
1868
  if (deps.getClient() === void 0) {
1899
- json$1(res, {
1869
+ json(res, {
1900
1870
  ok: false,
1901
1871
  error: "wiki service is not running"
1902
1872
  }, 503);
@@ -1935,7 +1905,7 @@ function registerRoutes(ctx, deps) {
1935
1905
  res.end();
1936
1906
  return;
1937
1907
  }
1938
- json$1(res, {
1908
+ json(res, {
1939
1909
  ok: false,
1940
1910
  error: err instanceof Error ? err.message : String(err)
1941
1911
  }, 502);
@@ -2296,30 +2266,6 @@ async function ensureLanguage(wikiPath, twRoot, lang) {
2296
2266
  await writeWikiInfo(wikiPath, info);
2297
2267
  return true;
2298
2268
  }
2299
- function json(res, payload, status = 200) {
2300
- res.writeHead(status, {
2301
- "content-type": "application/json; charset=utf-8",
2302
- "cache-control": "no-store"
2303
- });
2304
- res.end(JSON.stringify(payload));
2305
- }
2306
- async function readBody(req, limit = 1024 * 1024) {
2307
- return new Promise((resolveP, rejectP) => {
2308
- let size = 0;
2309
- const chunks = [];
2310
- req.on("data", (chunk) => {
2311
- size += chunk.length;
2312
- if (size > limit) {
2313
- rejectP(/* @__PURE__ */ new Error("body too large"));
2314
- req.destroy();
2315
- return;
2316
- }
2317
- chunks.push(chunk);
2318
- });
2319
- req.on("end", () => resolveP(Buffer.concat(chunks).toString("utf8")));
2320
- req.on("error", rejectP);
2321
- });
2322
- }
2323
2269
  function registerAdminRoutes(ctx, deps) {
2324
2270
  const handleState = async (_req, res) => {
2325
2271
  try {
@@ -2327,7 +2273,6 @@ function registerAdminRoutes(ctx, deps) {
2327
2273
  const [info, catalog] = await Promise.all([readWikiInfo(wikiPath), bundledCatalog(deps.twRoot())]);
2328
2274
  let git = null;
2329
2275
  try {
2330
- const { GitFace } = await Promise.resolve().then(() => git_exports);
2331
2276
  git = await new GitFace().status(wikiPath);
2332
2277
  } catch {
2333
2278
  git = null;
@@ -3154,130 +3099,107 @@ const removedDetail = (id, removed) => ({
3154
3099
  wrote: false,
3155
3100
  detail: removed.length > 0 ? `已移除:${removed.join("、")}` : "本就不存在,无需移除"
3156
3101
  });
3102
+ /**
3103
+ * Define a seed from a small spec, killing the id/title/description
3104
+ * triplication and the try/catch boilerplate every registry entry used to
3105
+ * repeat by hand:
3106
+ * - `presentTitle` → default check: is that tiddler present?
3107
+ * - `check` → custom presence check (overrides presentTitle).
3108
+ * - `write` → built-in content writer; true = (re)written this call.
3109
+ * - `run` → custom runner (overrides the standard write wrapper;
3110
+ * e.g. tw-web-host honors a user-chosen host value).
3111
+ * - `unseed` → remove the seeded tiddlers + markers (removable seeds).
3112
+ */
3113
+ function defineSeed(meta, impl) {
3114
+ const { id, title, description, core } = meta;
3115
+ const removable = !core;
3116
+ const check = impl.check ?? (async (ctx) => {
3117
+ const present = await presentOf(ctx, impl.presentTitle ?? "");
3118
+ return {
3119
+ id,
3120
+ title,
3121
+ description,
3122
+ present,
3123
+ removable,
3124
+ detail: present ? "已存在" : "缺失"
3125
+ };
3126
+ });
3127
+ const run = impl.run ?? (async (ctx, force) => {
3128
+ try {
3129
+ const wrote = await impl.write(ctx.client, { force });
3130
+ return {
3131
+ id,
3132
+ ok: true,
3133
+ wrote,
3134
+ detail: wrote ? force ? "已重新初始化" : "已写入" : force ? "内容已是最新(未重写)" : "已存在,跳过"
3135
+ };
3136
+ } catch (err) {
3137
+ return {
3138
+ id,
3139
+ ok: false,
3140
+ wrote: false,
3141
+ error: err instanceof Error ? err.message : String(err)
3142
+ };
3143
+ }
3144
+ });
3145
+ const remove = impl.unseed === void 0 ? void 0 : async (ctx) => {
3146
+ try {
3147
+ return removedDetail(id, (await impl.unseed(ctx.client)).removed);
3148
+ } catch (err) {
3149
+ return {
3150
+ id,
3151
+ ok: false,
3152
+ wrote: false,
3153
+ error: err instanceof Error ? err.message : String(err)
3154
+ };
3155
+ }
3156
+ };
3157
+ return {
3158
+ id,
3159
+ title,
3160
+ description,
3161
+ core,
3162
+ check,
3163
+ run,
3164
+ ...remove === void 0 ? {} : { remove }
3165
+ };
3166
+ }
3157
3167
  /** The full registry, in display order. */
3158
3168
  const SEED_DEFS = [
3159
- {
3169
+ defineSeed({
3160
3170
  id: "doc-note",
3161
3171
  title: "插件说明笔记",
3162
3172
  description: "「dsh-tiddlywiki 插件说明」——新 wiki 首启自动写入的入门说明(ONE-SHOT,用户可改可删)。",
3163
- core: false,
3164
- check: async (ctx) => {
3165
- const present = await presentOf(ctx, DOC_NOTE_TITLE);
3166
- return {
3167
- id: "doc-note",
3168
- title: "插件说明笔记",
3169
- description: "「dsh-tiddlywiki 插件说明」——新 wiki 首启自动写入的入门说明(ONE-SHOT,用户可改可删)。",
3170
- present,
3171
- removable: true,
3172
- detail: present ? "已存在" : "缺失"
3173
- };
3174
- },
3175
- run: async (ctx, force) => {
3176
- try {
3177
- const wrote = await seedDocNote(ctx.client, { force });
3178
- return {
3179
- id: "doc-note",
3180
- ok: true,
3181
- wrote,
3182
- detail: wrote ? force ? "已重新初始化" : "已写入" : force ? "内容已是最新(未重写)" : "已存在,跳过"
3183
- };
3184
- } catch (err) {
3185
- return {
3186
- id: "doc-note",
3187
- ok: false,
3188
- wrote: false,
3189
- error: err instanceof Error ? err.message : String(err)
3190
- };
3191
- }
3192
- },
3193
- remove: async (ctx) => {
3194
- try {
3195
- return removedDetail("doc-note", (await unseedDocNote(ctx.client)).removed);
3196
- } catch (err) {
3197
- return {
3198
- id: "doc-note",
3199
- ok: false,
3200
- wrote: false,
3201
- error: err instanceof Error ? err.message : String(err)
3202
- };
3203
- }
3204
- }
3205
- },
3206
- {
3173
+ core: false
3174
+ }, {
3175
+ presentTitle: DOC_NOTE_TITLE,
3176
+ write: seedDocNote,
3177
+ unseed: unseedDocNote
3178
+ }),
3179
+ defineSeed({
3207
3180
  id: "send-to-agent",
3208
3181
  title: "「发送给 Agent」按钮",
3209
3182
  description: "TW 笔记工具栏「发送给 Agent」按钮插件($:/plugins/dsh/send-to-agent)——把笔记一键注入 DSH 会话。",
3210
- core: true,
3211
- check: async (ctx) => {
3212
- const present = await presentOf(ctx, SEND_TO_AGENT_PLUGIN_TITLE);
3213
- return {
3214
- id: "send-to-agent",
3215
- title: "「发送给 Agent」按钮",
3216
- description: "TW 笔记工具栏「发送给 Agent」按钮插件($:/plugins/dsh/send-to-agent)——把笔记一键注入 DSH 会话。",
3217
- present,
3218
- removable: false,
3219
- detail: present ? "已存在" : "缺失"
3220
- };
3221
- },
3222
- run: async (ctx, force) => {
3223
- try {
3224
- const wrote = await seedSendToAgent(ctx.client, { force });
3225
- return {
3226
- id: "send-to-agent",
3227
- ok: true,
3228
- wrote,
3229
- detail: wrote ? force ? "已重新初始化" : "已写入" : force ? "内容已是最新(未重写)" : "已存在,跳过"
3230
- };
3231
- } catch (err) {
3232
- return {
3233
- id: "send-to-agent",
3234
- ok: false,
3235
- wrote: false,
3236
- error: err instanceof Error ? err.message : String(err)
3237
- };
3238
- }
3239
- }
3240
- },
3241
- {
3183
+ core: true
3184
+ }, {
3185
+ presentTitle: SEND_TO_AGENT_PLUGIN_TITLE,
3186
+ write: seedSendToAgent
3187
+ }),
3188
+ defineSeed({
3242
3189
  id: "render-route",
3243
3190
  title: "原生渲染路由(/render)",
3244
3191
  description: "TW 服务端路由插件($:/plugins/dsh/render,server-routes/render.js)——把 wiki 文本在运行中的 TW 里原生渲染成 HTML 片段,回复流工具卡与 wiki 链接跳转依赖它。seed 写入后需重启 TW 使路由生效。",
3245
- core: true,
3246
- check: async (ctx) => {
3247
- const present = await presentOf(ctx, RENDER_PLUGIN_TITLE);
3248
- return {
3249
- id: "render-route",
3250
- title: "原生渲染路由(/render)",
3251
- description: "TW 服务端路由插件($:/plugins/dsh/render,server-routes/render.js)——把 wiki 文本在运行中的 TW 里原生渲染成 HTML 片段,回复流工具卡与 wiki 链接跳转依赖它。seed 写入后需重启 TW 使路由生效。",
3252
- present,
3253
- removable: false,
3254
- detail: present ? "已存在" : "缺失"
3255
- };
3256
- },
3257
- run: async (ctx, force) => {
3258
- try {
3259
- const wrote = await seedRenderRoute(ctx.client, { force });
3260
- return {
3261
- id: "render-route",
3262
- ok: true,
3263
- wrote,
3264
- detail: wrote ? force ? "已重新初始化" : "已写入" : force ? "内容已是最新(未重写)" : "已存在,跳过"
3265
- };
3266
- } catch (err) {
3267
- return {
3268
- id: "render-route",
3269
- ok: false,
3270
- wrote: false,
3271
- error: err instanceof Error ? err.message : String(err)
3272
- };
3273
- }
3274
- }
3275
- },
3276
- {
3192
+ core: true
3193
+ }, {
3194
+ presentTitle: RENDER_PLUGIN_TITLE,
3195
+ write: seedRenderRoute
3196
+ }),
3197
+ defineSeed({
3277
3198
  id: "home-index",
3278
3199
  title: "首页(主页 / 所有标签 / 标签笔记)",
3279
3200
  description: "默认主页:四象限待办 + 「所有标签」「所有文章」入口;所有标签:标签统计 + Agent 区块(纯 Agent / Agent+人工);标签笔记:按标签浏览。系统提示承诺的首页由这里 seed,主页同时写入 $:/DefaultTiddlers。",
3280
- core: false,
3201
+ core: false
3202
+ }, {
3281
3203
  check: async (ctx) => {
3282
3204
  const missing = [];
3283
3205
  for (const item of HOME_INDEX_ITEMS) if (!await presentOf(ctx, item.title)) missing.push(item.title);
@@ -3290,136 +3212,35 @@ const SEED_DEFS = [
3290
3212
  detail: missing.length === 0 ? "已存在" : `缺失:${missing.join("、")}`
3291
3213
  };
3292
3214
  },
3293
- run: async (ctx, force) => {
3294
- try {
3295
- const wrote = await seedHomeIndex(ctx.client, { force });
3296
- return {
3297
- id: "home-index",
3298
- ok: true,
3299
- wrote,
3300
- detail: wrote ? force ? "已重新初始化" : "已写入" : force ? "内容已是最新(未重写)" : "已存在,跳过"
3301
- };
3302
- } catch (err) {
3303
- return {
3304
- id: "home-index",
3305
- ok: false,
3306
- wrote: false,
3307
- error: err instanceof Error ? err.message : String(err)
3308
- };
3309
- }
3310
- },
3311
- remove: async (ctx) => {
3312
- try {
3313
- return removedDetail("home-index", (await unseedHomeIndex(ctx.client)).removed);
3314
- } catch (err) {
3315
- return {
3316
- id: "home-index",
3317
- ok: false,
3318
- wrote: false,
3319
- error: err instanceof Error ? err.message : String(err)
3320
- };
3321
- }
3322
- }
3323
- },
3324
- {
3215
+ write: seedHomeIndex,
3216
+ unseed: unseedHomeIndex
3217
+ }),
3218
+ defineSeed({
3325
3219
  id: "all-articles",
3326
3220
  title: "所有文章(两列分页总览)",
3327
3221
  description: "「所有文章」——全部条目分两列(🤖 Agent 撰写 / 👤 人工·人类)各自分页展示。每页条数取插件设置 ui.allArticles.pageSize(默认 10)。",
3328
- core: false,
3329
- check: async (ctx) => {
3330
- const present = await presentOf(ctx, ALL_ARTICLES_TITLE);
3331
- return {
3332
- id: "all-articles",
3333
- title: "所有文章(两列分页总览)",
3334
- description: "「所有文章」——全部条目分两列(🤖 Agent 撰写 / 👤 人工·人类)各自分页展示。每页条数取插件设置 ui.allArticles.pageSize(默认 10)。",
3335
- present,
3336
- removable: true,
3337
- detail: present ? "已存在" : "缺失"
3338
- };
3339
- },
3340
- run: async (ctx, force) => {
3341
- try {
3342
- const wrote = await seedAllArticles(ctx.client, { force });
3343
- return {
3344
- id: "all-articles",
3345
- ok: true,
3346
- wrote,
3347
- detail: wrote ? force ? "已重新初始化" : "已写入" : force ? "内容已是最新(未重写)" : "已存在,跳过"
3348
- };
3349
- } catch (err) {
3350
- return {
3351
- id: "all-articles",
3352
- ok: false,
3353
- wrote: false,
3354
- error: err instanceof Error ? err.message : String(err)
3355
- };
3356
- }
3357
- },
3358
- remove: async (ctx) => {
3359
- try {
3360
- return removedDetail("all-articles", (await unseedAllArticles(ctx.client)).removed);
3361
- } catch (err) {
3362
- return {
3363
- id: "all-articles",
3364
- ok: false,
3365
- wrote: false,
3366
- error: err instanceof Error ? err.message : String(err)
3367
- };
3368
- }
3369
- }
3370
- },
3371
- {
3222
+ core: false
3223
+ }, {
3224
+ presentTitle: ALL_ARTICLES_TITLE,
3225
+ write: seedAllArticles,
3226
+ unseed: unseedAllArticles
3227
+ }),
3228
+ defineSeed({
3372
3229
  id: "menubar-theme",
3373
3230
  title: "menubar 顶栏主题自适应",
3374
3231
  description: "样式表覆盖($:/plugins/dsh-tiddlywiki/menubar-theme,tag $:/tags/Stylesheet)——把 tiddlywiki/menubar 顶栏从「默认色映射的蓝色」改为跟随当前 palette 的 background/foreground,随 DSH 主题切换($:/palette 翻转)自动换色。",
3375
- core: false,
3376
- check: async (ctx) => {
3377
- const present = await presentOf(ctx, MENUBAR_THEME_TIDDLER);
3378
- return {
3379
- id: "menubar-theme",
3380
- title: "menubar 顶栏主题自适应",
3381
- description: "样式表覆盖($:/plugins/dsh-tiddlywiki/menubar-theme,tag $:/tags/Stylesheet)——把 tiddlywiki/menubar 顶栏从「默认色映射的蓝色」改为跟随当前 palette 的 background/foreground,随 DSH 主题切换($:/palette 翻转)自动换色。",
3382
- present,
3383
- removable: true,
3384
- detail: present ? "已存在" : "缺失"
3385
- };
3386
- },
3387
- run: async (ctx, force) => {
3388
- try {
3389
- const wrote = await seedMenubarTheme(ctx.client, { force });
3390
- return {
3391
- id: "menubar-theme",
3392
- ok: true,
3393
- wrote,
3394
- detail: wrote ? force ? "已重新初始化" : "已写入" : force ? "内容已是最新(未重写)" : "已存在,跳过"
3395
- };
3396
- } catch (err) {
3397
- return {
3398
- id: "menubar-theme",
3399
- ok: false,
3400
- wrote: false,
3401
- error: err instanceof Error ? err.message : String(err)
3402
- };
3403
- }
3404
- },
3405
- remove: async (ctx) => {
3406
- try {
3407
- return removedDetail("menubar-theme", (await unseedMenubarTheme(ctx.client)).removed);
3408
- } catch (err) {
3409
- return {
3410
- id: "menubar-theme",
3411
- ok: false,
3412
- wrote: false,
3413
- error: err instanceof Error ? err.message : String(err)
3414
- };
3415
- }
3416
- }
3417
- },
3418
- {
3232
+ core: false
3233
+ }, {
3234
+ presentTitle: MENUBAR_THEME_TIDDLER,
3235
+ write: seedMenubarTheme,
3236
+ unseed: unseedMenubarTheme
3237
+ }),
3238
+ defineSeed({
3419
3239
  id: "tw-web-host",
3420
3240
  title: "TW 前端 API 基址(同源代理)",
3421
3241
  description: "把 $:/config/tiddlyweb/host 指向 DSH 同源代理,嵌入式 TW 才能经 DSH origin 访问(远程访问模式的前提)。",
3422
- core: true,
3242
+ core: true
3243
+ }, {
3423
3244
  check: async (ctx) => {
3424
3245
  let current;
3425
3246
  try {
@@ -3472,7 +3293,7 @@ const SEED_DEFS = [
3472
3293
  };
3473
3294
  }
3474
3295
  }
3475
- }
3296
+ })
3476
3297
  ];
3477
3298
  /** Check every seed, returning statuses in registry order. */
3478
3299
  async function checkAllSeeds(ctx) {
@@ -3965,6 +3786,13 @@ function registerTiddlywikiTools(ctx, deps) {
3965
3786
  const register = (tool) => {
3966
3787
  disposers.push(ctx.tools.register(tool));
3967
3788
  };
3789
+ /** Every read/write tool needs a live TW client — shared guard for the 8
3790
+ * wiki-facing tools (git tools operate on the repo path instead). */
3791
+ const requireWiki = () => {
3792
+ const wiki = deps.wiki();
3793
+ if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
3794
+ return wiki;
3795
+ };
3968
3796
  register(defineTool({
3969
3797
  name: "tiddlywiki_search",
3970
3798
  description: "检索 TiddlyWiki 持久知识库:按关键词(可选 tags 数组 / since 修改时间 / type / limit)搜索非系统 tiddler,返回标题、标签、修改时间与摘要片段。",
@@ -4019,9 +3847,7 @@ function registerTiddlywikiTools(ctx, deps) {
4019
3847
  }
4020
3848
  },
4021
3849
  execute: async (args) => {
4022
- const wiki = deps.wiki();
4023
- if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
4024
- const { items, total } = await wiki.search(args.query, {
3850
+ const { items, total } = await requireWiki().search(args.query, {
4025
3851
  tags: args.tags,
4026
3852
  tag: args.tag,
4027
3853
  since: args.since,
@@ -4073,9 +3899,7 @@ function registerTiddlywikiTools(ctx, deps) {
4073
3899
  }
4074
3900
  },
4075
3901
  execute: async (args) => {
4076
- const wiki = deps.wiki();
4077
- if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
4078
- const items = await wiki.recent(args.limit ?? 15, args.since);
3902
+ const items = await requireWiki().recent(args.limit ?? 15, args.since);
4079
3903
  return {
4080
3904
  since: args.since ?? null,
4081
3905
  results: items.map((t) => ({
@@ -4107,9 +3931,7 @@ function registerTiddlywikiTools(ctx, deps) {
4107
3931
  }
4108
3932
  },
4109
3933
  execute: async () => {
4110
- const wiki = deps.wiki();
4111
- if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
4112
- const tags = await wiki.listTags();
3934
+ const tags = await requireWiki().listTags();
4113
3935
  return {
4114
3936
  count: tags.length,
4115
3937
  tags
@@ -4145,9 +3967,7 @@ function registerTiddlywikiTools(ctx, deps) {
4145
3967
  }
4146
3968
  },
4147
3969
  execute: async (args) => {
4148
- const wiki = deps.wiki();
4149
- if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
4150
- const t = await wiki.get(args.title);
3970
+ const t = await requireWiki().get(args.title);
4151
3971
  if (t === void 0) return {
4152
3972
  notFound: true,
4153
3973
  title: args.title,
@@ -4206,8 +4026,7 @@ function registerTiddlywikiTools(ctx, deps) {
4206
4026
  }
4207
4027
  },
4208
4028
  execute: async (args) => {
4209
- const wiki = deps.wiki();
4210
- if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
4029
+ const wiki = requireWiki();
4211
4030
  const existing = await wiki.get(args.title).catch(() => void 0);
4212
4031
  const tags = finalTagsForWrite(args.title, existing, Array.isArray(args.tags) ? args.tags.filter((t) => typeof t === "string" && t.trim().length > 0) : []);
4213
4032
  const tiddler = {
@@ -4276,8 +4095,7 @@ function registerTiddlywikiTools(ctx, deps) {
4276
4095
  }
4277
4096
  },
4278
4097
  execute: async (args) => {
4279
- const wiki = deps.wiki();
4280
- if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
4098
+ const wiki = requireWiki();
4281
4099
  const list = Array.isArray(args.items) ? args.items : [];
4282
4100
  if (list.length === 0) return {
4283
4101
  ok: true,
@@ -4358,8 +4176,7 @@ function registerTiddlywikiTools(ctx, deps) {
4358
4176
  }
4359
4177
  },
4360
4178
  execute: async (args) => {
4361
- const wiki = deps.wiki();
4362
- if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
4179
+ const wiki = requireWiki();
4363
4180
  const { oldTitle, newTitle } = args;
4364
4181
  if (oldTitle === newTitle) return {
4365
4182
  ok: true,
@@ -4425,9 +4242,7 @@ function registerTiddlywikiTools(ctx, deps) {
4425
4242
  }]
4426
4243
  },
4427
4244
  execute: async (args) => {
4428
- const wiki = deps.wiki();
4429
- if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
4430
- await wiki.delete(args.title);
4245
+ await requireWiki().delete(args.title);
4431
4246
  deps.autoCommit();
4432
4247
  return {
4433
4248
  ok: true,
@@ -4545,15 +4360,7 @@ function registerTiddlywikiTools(ctx, deps) {
4545
4360
  lines.push(` ${value.message}`);
4546
4361
  if (value.files !== void 0 && value.files.length > 0) lines.push(`涉及文件: ${value.files.join(", ")}`);
4547
4362
  if (value.commit !== void 0) lines.push(`本地 commit: ${value.commit}`);
4548
- if (value.status !== void 0) {
4549
- const s = value.status;
4550
- const bits = [`分支 ${s.branch}`];
4551
- if (s.ahead !== void 0) bits.push(`领先 ${s.ahead}`);
4552
- if (s.behind !== void 0) bits.push(`落后 ${s.behind}`);
4553
- if (s.dirty) bits.push(`工作区有 ${s.dirtyFiles.length} 个未提交改动`);
4554
- if (s.lastCommit !== void 0) bits.push(`最近提交 ${s.lastCommit}`);
4555
- lines.push(`状态: ${bits.join(" · ")}`);
4556
- }
4363
+ if (value.status !== void 0) lines.push(`状态: ${gitStatusBits(value.status)}`);
4557
4364
  return [{
4558
4365
  type: "text",
4559
4366
  text: lines.join("\n")
@@ -4612,6 +4419,15 @@ function registerTiddlywikiTools(ctx, deps) {
4612
4419
  }));
4613
4420
  return disposers;
4614
4421
  }
4422
+ /** One-line 状态 summary: 分支 … 领先 … 落后 … 工作区未提交 … 最近提交. */
4423
+ function gitStatusBits(s) {
4424
+ const bits = [`分支 ${s.branch}`];
4425
+ if (s.ahead !== void 0) bits.push(`领先 ${s.ahead}`);
4426
+ if (s.behind !== void 0) bits.push(`落后 ${s.behind}`);
4427
+ if (s.dirty) bits.push(`工作区有 ${s.dirtyFiles.length} 个未提交改动`);
4428
+ if (s.lastCommit !== void 0) bits.push(`最近提交 ${s.lastCommit}`);
4429
+ return bits.join(" · ");
4430
+ }
4615
4431
  function renderSync(value) {
4616
4432
  const lines = [`git ${value.action}: ${value.ok ? "成功" : "失败"}`];
4617
4433
  lines.push(` ${value.message}`);
@@ -4625,13 +4441,8 @@ function renderSync(value) {
4625
4441
  if (value.changed === true) lines.push(value.restarted === true ? "本次 pull 拉取了新内容,TW 服务已自动重启(同端口),读取/搜索均为最新快照。" : "本次 pull 拉取了新内容,但 TW 服务未能自动重启(如需最新快照,请手动重启 TW)。");
4626
4442
  if (value.restartError !== void 0) lines.push(`TW 重启失败: ${value.restartError}`);
4627
4443
  if (value.status !== void 0) {
4444
+ lines.push(`状态: ${gitStatusBits(value.status)}`);
4628
4445
  const s = value.status;
4629
- const bits = [`分支 ${s.branch}`];
4630
- if (s.ahead !== void 0) bits.push(`领先 ${s.ahead}`);
4631
- if (s.behind !== void 0) bits.push(`落后 ${s.behind}`);
4632
- if (s.dirty) bits.push(`工作区有 ${s.dirtyFiles.length} 个未提交改动`);
4633
- if (s.lastCommit !== void 0) bits.push(`最近提交 ${s.lastCommit}`);
4634
- lines.push(`状态: ${bits.join(" · ")}`);
4635
4446
  if (s.dirty && s.dirtyFiles.length > 0) lines.push(` 未提交: ${s.dirtyFiles.join(", ")}`);
4636
4447
  }
4637
4448
  return [{
@@ -4686,8 +4497,10 @@ const DEFAULTS = {
4686
4497
  darkPalette: DARK_PALETTE_DEFAULT,
4687
4498
  tabLabel: "知识库",
4688
4499
  showSessionTab: true,
4689
- sendToAgent: { enabled: true }
4500
+ sendToAgent: { enabled: true },
4501
+ allArticles: { pageSize: 10 }
4690
4502
  },
4503
+ uiLanguage: "",
4691
4504
  auth: {
4692
4505
  username: "",
4693
4506
  password: ""
@@ -4815,8 +4628,17 @@ function apply(ctx, rawConfig = {}) {
4815
4628
  },
4816
4629
  ui: {
4817
4630
  ...DEFAULTS.ui,
4818
- ...rawConfig.ui ?? {}
4631
+ ...rawConfig.ui ?? {},
4632
+ sendToAgent: {
4633
+ ...DEFAULTS.ui.sendToAgent,
4634
+ ...rawConfig.ui?.sendToAgent ?? {}
4635
+ },
4636
+ allArticles: {
4637
+ ...DEFAULTS.ui.allArticles,
4638
+ ...rawConfig.ui?.allArticles ?? {}
4639
+ }
4819
4640
  },
4641
+ uiLanguage: typeof rawConfig.uiLanguage === "string" ? rawConfig.uiLanguage.trim() : DEFAULTS.uiLanguage,
4820
4642
  auth: {
4821
4643
  ...DEFAULTS.auth,
4822
4644
  ...rawConfig.auth ?? {}
@@ -4827,7 +4649,8 @@ function apply(ctx, rawConfig = {}) {
4827
4649
  const configStore = new ConfigStore({
4828
4650
  note: config.note,
4829
4651
  git: config.git,
4830
- ui: config.ui
4652
+ ui: config.ui,
4653
+ uiLanguage: config.uiLanguage
4831
4654
  });
4832
4655
  const eff = () => configStore.get();
4833
4656
  const effectiveNoteTag = () => {