dsh-prime-memory 0.16.1 → 0.16.4

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/cordis.patch.yml +6 -0
  3. package/dist/client.js +1333 -168
  4. package/dist/config.d.ts +37 -0
  5. package/dist/config.js +7 -0
  6. package/dist/contract.d.ts +155 -1
  7. package/dist/hooks/capture-recovery.d.ts +64 -0
  8. package/dist/hooks/capture-recovery.js +105 -0
  9. package/dist/hooks/capture.js +123 -1
  10. package/dist/hooks/recall-ack.d.ts +48 -0
  11. package/dist/hooks/recall-ack.js +79 -0
  12. package/dist/hooks/recall.d.ts +23 -1
  13. package/dist/hooks/recall.js +121 -6
  14. package/dist/index.d.ts +22 -0
  15. package/dist/index.js +18 -4
  16. package/dist/metadata-validators.d.ts +5 -0
  17. package/dist/metadata-validators.js +11 -1
  18. package/dist/pipeline/l1.d.ts +4 -0
  19. package/dist/pipeline/l1.js +9 -2
  20. package/dist/pipeline/reconcile.js +4 -0
  21. package/dist/pipeline/ruminate.d.ts +11 -1
  22. package/dist/pipeline/ruminate.js +18 -1
  23. package/dist/pipeline/runner.js +62 -0
  24. package/dist/prompts/boundary.d.ts +17 -0
  25. package/dist/prompts/boundary.js +20 -0
  26. package/dist/prompts/graph-projection.js +4 -1
  27. package/dist/prompts/l1-dedup.d.ts +3 -3
  28. package/dist/prompts/l1-dedup.js +9 -1
  29. package/dist/prompts/l1-extraction.d.ts +3 -3
  30. package/dist/prompts/l1-extraction.js +12 -0
  31. package/dist/prompts/persona.js +6 -0
  32. package/dist/prompts/scene.js +7 -1
  33. package/dist/room-review.d.ts +196 -0
  34. package/dist/room-review.js +281 -0
  35. package/dist/stats.d.ts +26 -2
  36. package/dist/stats.js +270 -13
  37. package/dist/store/l0.d.ts +4 -0
  38. package/dist/store/l0.js +8 -0
  39. package/dist/store/l1.d.ts +34 -0
  40. package/dist/store/l1.js +68 -1
  41. package/dist/store/recall-usage.d.ts +43 -0
  42. package/dist/store/recall-usage.js +149 -0
  43. package/dist/store/rooms-registry.d.ts +66 -0
  44. package/dist/store/rooms-registry.js +250 -0
  45. package/dist/store/sqlite.d.ts +16 -0
  46. package/dist/store/sqlite.js +57 -0
  47. package/dist/store/trace.d.ts +39 -0
  48. package/dist/store/trace.js +149 -0
  49. package/dist/tools/index.d.ts +4 -1
  50. package/dist/tools/index.js +249 -2
  51. package/dist/types.d.ts +4 -0
  52. package/dist/util/redact.d.ts +27 -0
  53. package/dist/util/redact.js +228 -0
  54. package/dsh.plugin.json +1 -1
  55. package/package.json +10 -10
  56. package/dist/hall-backfill.d.ts +0 -33
  57. package/dist/hall-backfill.js +0 -95
  58. package/dist/hall-migrate.d.ts +0 -32
  59. package/dist/hall-migrate.js +0 -88
package/dist/client.js CHANGED
@@ -32,7 +32,7 @@ var __defProp = Object.defineProperty;
32
32
  module.exports = __toCommonJS(entry_exports);
33
33
 
34
34
  // client/src/panel.tsx
35
- var import_react17 = require("react");
35
+ var import_react19 = require("react");
36
36
 
37
37
  // client/src/sidebar-icon.ts
38
38
  var BOOK_ICON_SVG = '<svg data-mem-icon="1" viewBox="0 0 16 16" width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0"><path d="M8 3.4C6.6 2.5 4.6 2.4 2.9 3.1v9.3c1.7-.7 3.7-.6 5.1.3 1.4-.9 3.4-1 5.1-.3V3.1C11.4 2.4 9.4 2.5 8 3.4Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/><path d="M8 3.4v9.3" stroke="currentColor" stroke-width="1.2"/></svg>';
@@ -1178,20 +1178,46 @@ var __defProp = Object.defineProperty;
1178
1178
  // client/src/tabs/LogTab.tsx
1179
1179
  var import_react4 = require("react");
1180
1180
  var import_jsx_runtime4 = require("react/jsx-runtime");
1181
+ var SOURCES = [
1182
+ { id: "", label: "系统日志" },
1183
+ { id: "recall_turn", label: "召回追踪" },
1184
+ { id: "distill_run", label: "蒸馏追踪" }
1185
+ ];
1186
+ function fmtEvent(ev) {
1187
+ if (ev.kind === "recall_turn") {
1188
+ const outcome = String(ev.outcome ?? "?");
1189
+ const hits = ev.hitIds?.length ?? 0;
1190
+ const injected = ev.injectedIds?.length ?? 0;
1191
+ const q = ev.queryText ? ` query="${String(ev.queryText).slice(0, 40)}"` : ` sha=${String(ev.querySha ?? "")}`;
1192
+ return `[${new Date(Number(ev.ts)).toLocaleTimeString()}] recall ${outcome}${q} hits=${hits} injected=${injected} suppressed=${String(ev.suppressedCount ?? 0)} ${String(ev.durationMs ?? 0)}ms`;
1193
+ }
1194
+ if (ev.kind === "distill_run") {
1195
+ const by = ev.byKind ? " byKind=" + JSON.stringify(ev.byKind) : "";
1196
+ const err = ev.errorKind ? " error=" + String(ev.errorKind) : "";
1197
+ return `[${new Date(Number(ev.ts)).toLocaleTimeString()}] distill ${String(ev.layer)}${ev.runId ? " run=" + String(ev.runId) : ""} ok=${String(ev.ok)} in=${String(ev.inputChars ?? 0)}ch new=${String(ev.newRecords ?? 0)} ${String(ev.durationMs ?? 0)}ms${by}${err}`;
1198
+ }
1199
+ return JSON.stringify(ev);
1200
+ }
1181
1201
  function LogTab(props) {
1182
1202
  const rpc = props.rpc;
1203
+ const [source, setSource] = (0, import_react4.useState)("");
1183
1204
  const [lines, setLines] = (0, import_react4.useState)(null);
1184
1205
  const [error, setError] = (0, import_react4.useState)(null);
1185
1206
  const preRef = (0, import_react4.useRef)(null);
1186
1207
  const load = (0, import_react4.useCallback)(() => {
1187
1208
  setError(null);
1188
- rpc("dsh-memory/log-tail", { lines: 200 }).then((r) => {
1189
- if (r && r.ok) setLines(r.value.lines);
1209
+ if (source === "") {
1210
+ rpc("dsh-memory/log-tail", { lines: 200 }).then((r) => {
1211
+ if (r && r.ok) setLines(r.value.lines);
1212
+ else setError(r && r.error ? r.error.message : "RPC error");
1213
+ }).catch((e) => setError(String(e && e.message || e)));
1214
+ return;
1215
+ }
1216
+ rpc("dsh-memory/trace-tail", { lines: 200, kind: source }).then((r) => {
1217
+ if (r && r.ok) setLines(r.value.events.map(fmtEvent));
1190
1218
  else setError(r && r.error ? r.error.message : "RPC error");
1191
- }).catch((e) => {
1192
- setError(String(e && e.message || e));
1193
- });
1194
- }, [rpc]);
1219
+ }).catch((e) => setError(String(e && e.message || e)));
1220
+ }, [rpc, source]);
1195
1221
  (0, import_react4.useEffect)(() => {
1196
1222
  load();
1197
1223
  }, [load]);
@@ -1199,12 +1225,13 @@ var __defProp = Object.defineProperty;
1199
1225
  if (lines && preRef.current) preRef.current.scrollTop = preRef.current.scrollHeight;
1200
1226
  }, [lines]);
1201
1227
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
1202
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { ...S.flexRow, marginBottom: 10 }, children: [
1203
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: S.muted, children: error ? "加载失败" : lines === null ? "加载中…" : "最近 " + lines.length + " 行(memory.log)" }),
1228
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { ...S.flexRow, marginBottom: 10, gap: 8 }, children: [
1229
+ SOURCES.map((s) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NButton, { onClick: () => setSource(s.id), children: source === s.id ? "● " + s.label : s.label }, s.id)),
1204
1230
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: S.grow }),
1205
1231
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NButton, { onClick: load, children: "刷新" })
1206
1232
  ] }),
1207
- error ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { ...S.error, marginBottom: 10 }, children: "日志读取失败:" + error + "(点右上“刷新”重试)" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pre", { style: S.pre, ref: preRef, children: (lines || []).join("\n") || "(暂无日志)" })
1233
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { ...S.muted, marginBottom: 10 }, children: error ? "加载失败" : lines === null ? "加载中…" : `最近 ${lines.length} 条(${SOURCES.find((s) => s.id === source)?.label})` }),
1234
+ error ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { ...S.error, marginBottom: 10 }, children: "读取失败:" + error + "(点右上“刷新”重试)" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pre", { style: S.pre, ref: preRef, children: (lines || []).join("\n") || "(暂无记录)" })
1208
1235
  ] });
1209
1236
  }
1210
1237
 
@@ -1618,8 +1645,8 @@ var __defProp = Object.defineProperty;
1618
1645
  setOpen(false);
1619
1646
  setIdx(-1);
1620
1647
  if (refocus && wrapRef.current) {
1621
- const btn = wrapRef.current.querySelector("button");
1622
- if (btn && btn.focus) btn.focus();
1648
+ const btn3 = wrapRef.current.querySelector("button");
1649
+ if (btn3 && btn3.focus) btn3.focus();
1623
1650
  }
1624
1651
  };
1625
1652
  const pick = (id) => {
@@ -3260,10 +3287,11 @@ var __defProp = Object.defineProperty;
3260
3287
  const [sceneFilter, setSceneFilter] = (0, import_react15.useState)("");
3261
3288
  const [hallFilter, setHallFilter] = (0, import_react15.useState)([]);
3262
3289
  const [tagFilter, setTagFilter] = (0, import_react15.useState)("");
3290
+ const [untaggedOnly, setUntaggedOnly] = (0, import_react15.useState)(false);
3291
+ const [orphanCount, setOrphanCount] = (0, import_react15.useState)(0);
3263
3292
  const [retiredFilter, setRetiredFilter] = (0, import_react15.useState)("");
3264
- const [rooms, setRooms] = (0, import_react15.useState)([]);
3265
3293
  const [wingCatalog, setHallCatalog] = (0, import_react15.useState)(null);
3266
- const [last, setLast] = (0, import_react15.useState)({ query: "", type: "", scene: "", halls: [], tag: "", retired: "" });
3294
+ const [last, setLast] = (0, import_react15.useState)({ query: "", type: "", scene: "", halls: [], tag: "", untagged: false, retired: "" });
3267
3295
  const seqRef = (0, import_react15.useRef)(0);
3268
3296
  const fetchPage = (0, import_react15.useCallback)(
3269
3297
  (conds, offset, append) => {
@@ -3276,6 +3304,7 @@ var __defProp = Object.defineProperty;
3276
3304
  if (conds.scene) payload.scene = conds.scene;
3277
3305
  if (conds.halls.length > 0) payload.halls = conds.halls;
3278
3306
  if (conds.tag) payload.tag = conds.tag;
3307
+ if (conds.untagged) payload.untagged = true;
3279
3308
  const retiredSel = retiredSelOf(conds.retired);
3280
3309
  if (retiredSel !== void 0) payload.retired = retiredSel;
3281
3310
  rpc("dsh-memory/list-records", payload).then((r) => {
@@ -3287,6 +3316,7 @@ var __defProp = Object.defineProperty;
3287
3316
  }
3288
3317
  const v = r.value;
3289
3318
  setItems((prev) => append ? prev.concat(v.items) : v.items);
3319
+ if (v.usageSummary) setUsageSummary(v.usageSummary);
3290
3320
  if (!append) setSel(/* @__PURE__ */ new Set());
3291
3321
  setHasMore(!!v.hasMore);
3292
3322
  setTotal(v.total === void 0 || v.total === null ? null : v.total);
@@ -3302,24 +3332,24 @@ var __defProp = Object.defineProperty;
3302
3332
  [rpc]
3303
3333
  );
3304
3334
  const search = () => {
3305
- const conds = { query: query.trim(), type: typeFilter, scene: sceneFilter, halls: hallFilter, tag: tagFilter, retired: retiredFilter };
3335
+ const conds = { query: query.trim(), type: typeFilter, scene: sceneFilter, halls: hallFilter, tag: tagFilter, untagged: untaggedOnly, retired: retiredFilter };
3306
3336
  setLast(conds);
3307
3337
  fetchPage(conds, 0, false);
3308
3338
  };
3309
3339
  const applyRetiredFilter = (next) => {
3310
3340
  setRetiredFilter(next);
3311
- const conds = { query: query.trim(), type: typeFilter, scene: sceneFilter, halls: hallFilter, tag: tagFilter, retired: next };
3341
+ const conds = { query: query.trim(), type: typeFilter, scene: sceneFilter, halls: hallFilter, tag: tagFilter, untagged: untaggedOnly, retired: next };
3312
3342
  setLast(conds);
3313
3343
  fetchPage(conds, 0, false);
3314
3344
  };
3315
3345
  const loadRooms = (0, import_react15.useCallback)(() => {
3316
3346
  rpc("dsh-memory/rooms-get", {}).then((r) => {
3317
- if (r && r.ok) setRooms(r.value.rooms ?? []);
3347
+ if (r && r.ok) setOrphanCount(r.value.orphanCount ?? 0);
3318
3348
  }).catch(() => {
3319
3349
  });
3320
3350
  }, [rpc]);
3321
3351
  (0, import_react15.useEffect)(() => {
3322
- fetchPage({ query: "", type: "", scene: "", halls: [], tag: "", retired: "" }, 0, false);
3352
+ fetchPage({ query: "", type: "", scene: "", halls: [], tag: "", untagged: false, retired: "" }, 0, false);
3323
3353
  loadRooms();
3324
3354
  }, [fetchPage, loadRooms]);
3325
3355
  const loadHiPriv = (0, import_react15.useCallback)(() => {
@@ -3422,6 +3452,7 @@ var __defProp = Object.defineProperty;
3422
3452
  unknown: "已退场"
3423
3453
  };
3424
3454
  const countText = total !== null ? "共 " + total + " 条" : items.length + " 条" + (hasMore ? "+" : "");
3455
+ const [usageSummary, setUsageSummary] = (0, import_react15.useState)(null);
3425
3456
  const selCount = sel.size;
3426
3457
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { children: [
3427
3458
  /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: S.toolbar, children: [
@@ -3488,40 +3519,86 @@ var __defProp = Object.defineProperty;
3488
3519
  }
3489
3520
  )
3490
3521
  ] }),
3491
- rooms.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, alignItems: "center", marginBottom: 10 }, children: [
3492
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: "Room 分类" }),
3493
- rooms.slice(0, 40).map((r) => {
3494
- const on = tagFilter === r.room;
3495
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3522
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { marginBottom: 10, display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }, children: [
3523
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3524
+ "button",
3525
+ {
3526
+ type: "button",
3527
+ title: untaggedOnly ? "取消孤儿筛选" : "只看没有任何 Room 绑定的记忆",
3528
+ onClick: () => {
3529
+ const next = !untaggedOnly;
3530
+ setUntaggedOnly(next);
3531
+ if (next) setTagFilter("");
3532
+ const conds = { query: query.trim(), type: typeFilter, scene: sceneFilter, halls: hallFilter, tag: next ? "" : tagFilter, untagged: next, retired: retiredFilter };
3533
+ setLast(conds);
3534
+ fetchPage(conds, 0, false);
3535
+ },
3536
+ style: {
3537
+ cursor: "pointer",
3538
+ fontSize: 12,
3539
+ padding: "2px 8px",
3540
+ borderRadius: 999,
3541
+ border: untaggedOnly ? "1px solid var(--dsh-mem-accent)" : "1px solid var(--dsh-mem-border)",
3542
+ background: untaggedOnly ? "var(--dsh-mem-bg-inset)" : "transparent",
3543
+ color: untaggedOnly ? "var(--dsh-mem-accent)" : "var(--dsh-mem-text-2)"
3544
+ },
3545
+ children: "无绑定 · " + orphanCount
3546
+ }
3547
+ ),
3548
+ (tagFilter || untaggedOnly) && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { style: S.muted, children: [
3549
+ "筛选中:",
3550
+ untaggedOnly ? "无绑定" : tagFilter,
3551
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3496
3552
  "button",
3497
3553
  {
3498
3554
  type: "button",
3499
- title: on ? "取消按该 Room 筛选" : "按该 Room 筛选记录",
3555
+ title: "清除 Room/孤儿筛选",
3500
3556
  onClick: () => {
3501
- const next = on ? "" : r.room;
3502
- setTagFilter(next);
3503
- const conds = { query: query.trim(), type: typeFilter, scene: sceneFilter, halls: hallFilter, tag: next, retired: retiredFilter };
3557
+ setTagFilter("");
3558
+ setUntaggedOnly(false);
3559
+ const conds = { query: query.trim(), type: typeFilter, scene: sceneFilter, halls: hallFilter, tag: "", untagged: false, retired: retiredFilter };
3504
3560
  setLast(conds);
3505
3561
  fetchPage(conds, 0, false);
3506
3562
  },
3507
- style: {
3508
- cursor: "pointer",
3509
- fontSize: 12,
3510
- padding: "2px 8px",
3511
- borderRadius: 999,
3512
- border: on ? "1px solid var(--dsh-mem-accent)" : "1px solid var(--dsh-mem-border)",
3513
- background: on ? "var(--dsh-mem-bg-inset)" : "transparent",
3514
- color: on ? "var(--dsh-mem-accent)" : "var(--dsh-mem-text-2)"
3515
- },
3516
- children: r.room + " · " + r.count
3563
+ style: { cursor: "pointer", marginLeft: 6, background: "transparent", border: "none", color: "var(--dsh-mem-accent)", fontSize: 12 },
3564
+ children: "×清除"
3565
+ }
3566
+ )
3567
+ ] }),
3568
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3569
+ "button",
3570
+ {
3571
+ type: "button",
3572
+ title: "导出无 Room 绑定的孤儿清单 CSV(含候选)",
3573
+ onClick: () => {
3574
+ rpc("dsh-memory/rooms-export", { kind: "orphans" }).then((r) => {
3575
+ if (!r || !r.ok || !r.value?.csv) return;
3576
+ const blob = new Blob([r.value.csv], { type: "text/csv;charset=utf-8" });
3577
+ const url = URL.createObjectURL(blob);
3578
+ const a = document.createElement("a");
3579
+ a.href = url;
3580
+ a.download = "dsh-memory-room-orphans.csv";
3581
+ a.click();
3582
+ URL.revokeObjectURL(url);
3583
+ }).catch(() => void 0);
3517
3584
  },
3518
- r.room
3519
- );
3520
- }),
3521
- rooms.length > 40 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: "(仅显示前 40 / 共 " + rooms.length + ")" }) : null
3522
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: { ...S.muted, marginBottom: 10 }, children: "Room 分类:暂无(由反刍涌现的标签自动生成,无需手工建立)" }),
3585
+ style: {
3586
+ cursor: "pointer",
3587
+ fontSize: 12,
3588
+ padding: "2px 8px",
3589
+ borderRadius: 6,
3590
+ border: "1px solid var(--dsh-mem-border)",
3591
+ background: "transparent",
3592
+ color: "var(--dsh-mem-text-2)"
3593
+ },
3594
+ children: "导出孤儿"
3595
+ }
3596
+ ),
3597
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: "Room 标签/分类管理:见「Hall」「Room」标签页。" })
3598
+ ] }),
3523
3599
  /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { ...S.flexRow, marginBottom: 10 }, children: [
3524
3600
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: loading ? "加载中…" : countText }),
3601
+ usageSummary ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, title: "老化权重效果口径:参与召回尝试的记忆中,进 topN(被注入上下文)的比例", children: "召回使用 " + usageSummary.used + "/" + usageSummary.attempted + "(" + usageSummary.rate + "%)" }) : null,
3525
3602
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: "退场筛查" }),
3526
3603
  [["", "全部"], ["active", "仅活跃"], ["retired", "仅退场"]].map(([val, label]) => {
3527
3604
  const on = retiredFilter === val;
@@ -3625,6 +3702,29 @@ var __defProp = Object.defineProperty;
3625
3702
  }
3626
3703
  ) : null,
3627
3704
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: "优先级 " + m.priority }),
3705
+ m.agingWeight != null ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3706
+ "span",
3707
+ {
3708
+ style: {
3709
+ fontSize: 11,
3710
+ padding: "1px 6px",
3711
+ borderRadius: 999,
3712
+ whiteSpace: "nowrap",
3713
+ border: "1px solid " + (m.agingWeight <= 0.6 ? "var(--dsh-mem-danger)" : "var(--dsh-mem-border)"),
3714
+ color: m.agingWeight <= 0.6 ? "var(--dsh-mem-danger)" : "var(--dsh-mem-text-2)"
3715
+ },
3716
+ title: "老化权重 " + Math.round(m.agingWeight * 100) + "%(读路径衰减系数 max(0.5, 0.5^(Δ天/半衰期)));越低越老",
3717
+ children: "老化 " + Math.round(m.agingWeight * 100) + "%"
3718
+ }
3719
+ ) : null,
3720
+ m.usage ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3721
+ "span",
3722
+ {
3723
+ style: S.muted,
3724
+ title: "召回使用 " + m.usage.used + "/" + m.usage.attempts + "(进 topN 才算 used)" + (m.usage.lastUsedAt ? ";最近使用 " + fmtTime(new Date(m.usage.lastUsedAt).toISOString()) : "") + (m.usage.lastAttemptAt ? ";最近尝试 " + fmtTime(new Date(m.usage.lastAttemptAt).toISOString()) : ""),
3725
+ children: "用 " + m.usage.used + "/" + m.usage.attempts
3726
+ }
3727
+ ) : null,
3628
3728
  m.score !== null && m.score !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: "相关度 " + Number(m.score).toFixed(2) }) : null,
3629
3729
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: S.grow }),
3630
3730
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: fmtTime(m.updatedAt) }),
@@ -3747,14 +3847,1075 @@ var __defProp = Object.defineProperty;
3747
3847
  ] });
3748
3848
  }
3749
3849
 
3750
- // client/src/tabs/ScenesTab.tsx
3850
+ // client/src/tabs/RoomsTab.tsx
3751
3851
  var import_react16 = require("react");
3752
3852
  var import_jsx_runtime16 = require("react/jsx-runtime");
3853
+ function majorOf(slug) {
3854
+ const i = slug.indexOf("/");
3855
+ return i === -1 ? slug : slug.slice(0, i);
3856
+ }
3857
+ function suffixOf(slug) {
3858
+ const i = slug.indexOf("/");
3859
+ return i === -1 ? slug : slug.slice(i + 1);
3860
+ }
3861
+ function downloadCsv(csv, name) {
3862
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
3863
+ const url = URL.createObjectURL(blob);
3864
+ const a = document.createElement("a");
3865
+ a.href = url;
3866
+ a.download = name;
3867
+ a.click();
3868
+ URL.revokeObjectURL(url);
3869
+ }
3870
+ var btn = {
3871
+ cursor: "pointer",
3872
+ fontSize: 12,
3873
+ padding: "2px 8px",
3874
+ borderRadius: 6,
3875
+ border: "1px solid var(--dsh-mem-border)",
3876
+ background: "transparent",
3877
+ color: "var(--dsh-mem-text-2)"
3878
+ };
3879
+ function RoomsTab(props) {
3880
+ const rpc = props.rpc;
3881
+ const [registry, setRegistry] = (0, import_react16.useState)([]);
3882
+ const [rooms, setRooms] = (0, import_react16.useState)([]);
3883
+ const [orphanCount, setOrphanCount] = (0, import_react16.useState)(0);
3884
+ const [busy, setBusy] = (0, import_react16.useState)(false);
3885
+ const [msg, setMsg] = (0, import_react16.useState)(null);
3886
+ const [error, setError] = (0, import_react16.useState)(null);
3887
+ const [hiPriv, setHiPriv] = (0, import_react16.useState)(false);
3888
+ const [hiPrivBusy, setHiPrivBusy] = (0, import_react16.useState)(false);
3889
+ const [regSlug, setRegSlug] = (0, import_react16.useState)("");
3890
+ const [regLabel, setRegLabel] = (0, import_react16.useState)("");
3891
+ const [regDesc, setRegDesc] = (0, import_react16.useState)("");
3892
+ const [form, setForm] = (0, import_react16.useState)(null);
3893
+ const [preview, setPreview] = (0, import_react16.useState)(null);
3894
+ const [grownOpen, setGrownOpen] = (0, import_react16.useState)(false);
3895
+ const [dragOverHall, setDragOverHall] = (0, import_react16.useState)(null);
3896
+ const [picking, setPicking] = (0, import_react16.useState)(null);
3897
+ const [editing, setEditing] = (0, import_react16.useState)(null);
3898
+ const [expandedRooms, setExpandedRooms] = (0, import_react16.useState)(/* @__PURE__ */ new Set());
3899
+ const [roomRecords, setRoomRecords] = (0, import_react16.useState)({});
3900
+ const [expandedHalls, setExpandedHalls] = (0, import_react16.useState)(() => {
3901
+ try {
3902
+ const v = window.localStorage.getItem("dsh.memory.halls.open");
3903
+ return v ? new Set(JSON.parse(v)) : /* @__PURE__ */ new Set();
3904
+ } catch {
3905
+ return /* @__PURE__ */ new Set();
3906
+ }
3907
+ });
3908
+ const [filter, setFilter] = (0, import_react16.useState)("");
3909
+ const dragRef = (0, import_react16.useRef)(null);
3910
+ const [ghost, setGhost] = (0, import_react16.useState)(null);
3911
+ const onDragLabelDown = (ev, slug) => {
3912
+ if (!hiPriv || busy || ev.button !== 0) return;
3913
+ ev.currentTarget.setPointerCapture(ev.pointerId);
3914
+ dragRef.current = { from: slug, x: ev.clientX, y: ev.clientY, active: false };
3915
+ };
3916
+ const onDragLabelMove = (ev) => {
3917
+ const d = dragRef.current;
3918
+ if (!d) return;
3919
+ if (!d.active && Math.hypot(ev.clientX - d.x, ev.clientY - d.y) > 6) d.active = true;
3920
+ if (!d.active) return;
3921
+ setGhost({ x: ev.clientX, y: ev.clientY, text: d.from });
3922
+ const hall = document.elementFromPoint(ev.clientX, ev.clientY)?.closest?.("[data-hall]")?.getAttribute("data-hall") ?? null;
3923
+ setDragOverHall(hall);
3924
+ };
3925
+ const onDragLabelUp = (ev) => {
3926
+ const d = dragRef.current;
3927
+ dragRef.current = null;
3928
+ setGhost(null);
3929
+ if (!d) return;
3930
+ if (!d.active) return;
3931
+ const hall = document.elementFromPoint(ev.clientX, ev.clientY)?.closest?.("[data-hall]")?.getAttribute("data-hall");
3932
+ setDragOverHall(null);
3933
+ if (hall) beginReclass(d.from, hall);
3934
+ };
3935
+ const loadAll = (0, import_react16.useCallback)(() => {
3936
+ rpc("dsh-memory/rooms-get", {}).then((r) => {
3937
+ if (r && r.ok) {
3938
+ setRooms(r.value.rooms ?? []);
3939
+ setRegistry(r.value.registry ?? []);
3940
+ setOrphanCount(r.value.orphanCount ?? 0);
3941
+ }
3942
+ }).catch(() => void 0);
3943
+ }, [rpc]);
3944
+ const loadHiPriv = (0, import_react16.useCallback)(() => {
3945
+ rpc("dsh-memory/settings-get", {}).then((r) => {
3946
+ if (r && r.ok && r.value) setHiPriv(!!r.value.settings.memoryMutate);
3947
+ }).catch(() => {
3948
+ });
3949
+ }, [rpc]);
3950
+ (0, import_react16.useEffect)(() => {
3951
+ loadAll();
3952
+ loadHiPriv();
3953
+ }, [loadAll, loadHiPriv]);
3954
+ const toggleHiPriv = () => {
3955
+ const next = !hiPriv;
3956
+ if (next && !window.confirm("开启高权限模式:模型获得写入/删除记忆工具,并解锁 Hall 页的分类管理操作。确定开启?")) return;
3957
+ setHiPrivBusy(true);
3958
+ rpc("dsh-memory/settings-set", { memoryMutate: next }).then((r) => {
3959
+ if (r && r.ok) setHiPriv(next);
3960
+ else if (r) setError(r.error ? r.error.message : "切换高权限模式失败");
3961
+ setHiPrivBusy(false);
3962
+ loadHiPriv();
3963
+ }).catch((e) => {
3964
+ setHiPrivBusy(false);
3965
+ setError(String(e && e.message || e));
3966
+ });
3967
+ };
3968
+ const run = (fn) => {
3969
+ if (busy) return;
3970
+ setBusy(true);
3971
+ setError(null);
3972
+ setMsg(null);
3973
+ fn().finally(() => setBusy(false));
3974
+ };
3975
+ const register = () => run(async () => {
3976
+ const r = await rpc("dsh-memory/room-register", {
3977
+ slug: regSlug.trim(),
3978
+ label: regLabel.trim() || void 0,
3979
+ description: regDesc.trim() || void 0
3980
+ });
3981
+ if (r && r.ok) {
3982
+ setMsg(r.value?.notice ?? `已注册:${regSlug.trim()}`);
3983
+ setRegSlug("");
3984
+ setRegLabel("");
3985
+ setRegDesc("");
3986
+ loadAll();
3987
+ } else setError(r && r.error ? r.error.message : "注册失败");
3988
+ });
3989
+ const adopt = (slug) => run(async () => {
3990
+ const r = await rpc("dsh-memory/room-register", { slug, source: "grown" });
3991
+ if (r && r.ok) {
3992
+ setMsg(`已收编:${slug}(source=grown)`);
3993
+ loadAll();
3994
+ } else setError(r && r.error ? r.error.message : "收编失败");
3995
+ });
3996
+ const previewMerge = (override) => run(async () => {
3997
+ const fm = override ?? form;
3998
+ if (!fm) return;
3999
+ const to = fm.to.trim();
4000
+ if (!to) {
4001
+ setError("目标 slug 不能为空");
4002
+ return;
4003
+ }
4004
+ if (to === fm.from) {
4005
+ setError("目标与源相同");
4006
+ return;
4007
+ }
4008
+ const r = await rpc("dsh-memory/room-admin", { action: fm.action, from: fm.from, to });
4009
+ if (r && r.ok) {
4010
+ setPreview({ action: fm.action, from: fm.from, to, notice: String(r.value?.notice ?? "") });
4011
+ } else setError(r && r.error ? r.error.message : "预览失败");
4012
+ });
4013
+ const beginReclass = (from, hall) => {
4014
+ if (!busy) {
4015
+ setMsg(null);
4016
+ setError(null);
4017
+ }
4018
+ if (!hall) {
4019
+ setError("尚无 hall:先注册一个(如 dsh-plugin),再归类。");
4020
+ return;
4021
+ }
4022
+ if (majorOf(from) === hall) {
4023
+ if (registry.some((e) => e.slug === from)) {
4024
+ setMsg(`${from} 已在 ${hall} 下。`);
4025
+ return;
4026
+ }
4027
+ setPicking(null);
4028
+ void adopt(from);
4029
+ return;
4030
+ }
4031
+ const f = { action: "rename", from, to: `${hall}/${suffixOf(from)}` };
4032
+ setForm(f);
4033
+ setPreview(null);
4034
+ void previewMerge(f);
4035
+ };
4036
+ const execMerge = () => run(async () => {
4037
+ if (!preview) return;
4038
+ if (preview.action === "rename" && !registry.some((e) => e.slug === preview.from)) {
4039
+ const rr = await rpc("dsh-memory/room-register", { slug: preview.from, source: "grown" });
4040
+ if (!rr || !rr.ok) {
4041
+ setError(rr && rr.error ? rr.error.message : "收编源失败");
4042
+ return;
4043
+ }
4044
+ }
4045
+ const r = await rpc("dsh-memory/room-admin", { action: preview.action, from: preview.from, to: preview.to, dryRun: false });
4046
+ if (r && r.ok) {
4047
+ setMsg(String(r.value?.notice ?? "完成"));
4048
+ setPreview(null);
4049
+ setForm(null);
4050
+ loadAll();
4051
+ } else setError(r && r.error ? r.error.message : "执行失败");
4052
+ });
4053
+ const retire = (slug, active, withRecords = false) => run(async () => {
4054
+ if (!active) {
4055
+ const tip = withRecords ? `退役 Room:${slug} 并退场其下全部记忆?(记录为软删,可在记忆页「已退场」区逐条恢复)` : `退役 Room:${slug}?(存量记录的 tags 不动,仅词表不再推荐;可随时恢复)`;
4056
+ if (!window.confirm(tip)) return;
4057
+ }
4058
+ const r = await rpc("dsh-memory/room-admin", { action: "retire", slug, active, withRecords: withRecords || void 0 });
4059
+ if (r && r.ok) {
4060
+ setMsg(String(r.value?.notice ?? "完成"));
4061
+ loadAll();
4062
+ if (expandedRooms.has(slug)) {
4063
+ const rr = await rpc("dsh-memory/list-records", { tag: slug, limit: 10, retired: false });
4064
+ if (rr && rr.ok) setRoomRecords((m) => ({ ...m, [slug]: { items: rr.value.items, total: rr.value.total ?? null } }));
4065
+ }
4066
+ } else setError(r && r.error ? r.error.message : "操作失败");
4067
+ });
4068
+ const saveEditing = () => run(async () => {
4069
+ if (!editing) return;
4070
+ const r = await rpc("dsh-memory/room-admin", { action: "update", slug: editing.slug, label: editing.label, description: editing.description });
4071
+ if (r && r.ok) {
4072
+ setMsg(String(r.value?.notice ?? "已更新"));
4073
+ setEditing(null);
4074
+ loadAll();
4075
+ } else setError(r && r.error ? r.error.message : "更新失败");
4076
+ });
4077
+ const toggleRoomExpand = (slug) => run(async () => {
4078
+ if (expandedRooms.has(slug)) {
4079
+ const next = new Set(expandedRooms);
4080
+ next.delete(slug);
4081
+ setExpandedRooms(next);
4082
+ return;
4083
+ }
4084
+ const r = await rpc("dsh-memory/list-records", { tag: slug, limit: 10, retired: false });
4085
+ if (r && r.ok) {
4086
+ const next = new Set(expandedRooms);
4087
+ next.add(slug);
4088
+ setExpandedRooms(next);
4089
+ setRoomRecords((m) => ({ ...m, [slug]: { items: r.value.items, total: r.value.total ?? null } }));
4090
+ } else setError(r && r.error ? r.error.message : "加载失败");
4091
+ });
4092
+ const exportRecords = (slug) => run(async () => {
4093
+ const r = await rpc("dsh-memory/rooms-export", { kind: "records", tag: slug });
4094
+ if (!r.ok) {
4095
+ setError(r.error.message);
4096
+ return;
4097
+ }
4098
+ if (r.value.csv) downloadCsv(r.value.csv, `dsh-memory-room-${slug.replace(/\//g, "-")}-records.csv`);
4099
+ else setError("导出为空");
4100
+ });
4101
+ const groups = (0, import_react16.useMemo)(() => {
4102
+ const countOf = new Map(rooms.map((r) => [r.room, r.count]));
4103
+ const byHall = /* @__PURE__ */ new Map();
4104
+ for (const e of registry) {
4105
+ if (e.status !== "active") continue;
4106
+ const hall = majorOf(e.slug);
4107
+ const list = byHall.get(hall);
4108
+ if (list) list.push(e);
4109
+ else byHall.set(hall, [e]);
4110
+ }
4111
+ return [...byHall.entries()].map(([hall, entries]) => ({
4112
+ hall,
4113
+ label: registry.find((e) => e.slug === hall)?.label,
4114
+ entries: [...entries].sort((a, b) => a.slug.localeCompare(b.slug)),
4115
+ total: entries.reduce((s2, e) => s2 + (countOf.get(e.slug) ?? 0), 0)
4116
+ })).sort((a, b) => a.hall.localeCompare(b.hall));
4117
+ }, [registry, rooms]);
4118
+ const grownOnly = (0, import_react16.useMemo)(() => rooms.filter((r) => !registry.some((e) => e.slug === r.room)), [rooms, registry]);
4119
+ const persistHalls = (next) => {
4120
+ try {
4121
+ window.localStorage.setItem("dsh.memory.halls.open", JSON.stringify([...next]));
4122
+ } catch {
4123
+ }
4124
+ };
4125
+ const toggleHall = (hall) => setExpandedHalls((cur) => {
4126
+ const next = new Set(cur);
4127
+ if (next.has(hall)) next.delete(hall);
4128
+ else next.add(hall);
4129
+ persistHalls(next);
4130
+ return next;
4131
+ });
4132
+ const setAllHalls = (open) => {
4133
+ const next = open ? new Set(groups.map((g) => g.hall)) : /* @__PURE__ */ new Set();
4134
+ setExpandedHalls(next);
4135
+ persistHalls(next);
4136
+ };
4137
+ const normFilter = filter.trim().toLowerCase();
4138
+ const visibleGroups = (0, import_react16.useMemo)(() => {
4139
+ if (!normFilter) return groups;
4140
+ return groups.map((g) => {
4141
+ const hallHit = g.hall.toLowerCase().includes(normFilter) || (g.label ?? "").toLowerCase().includes(normFilter);
4142
+ const entries = hallHit ? g.entries : g.entries.filter(
4143
+ (e) => e.slug.toLowerCase().includes(normFilter) || (e.label ?? "").toLowerCase().includes(normFilter) || (e.aliases ?? []).some((a) => a.toLowerCase().includes(normFilter))
4144
+ );
4145
+ return { ...g, entries };
4146
+ }).filter((g) => g.entries.length > 0);
4147
+ }, [groups, normFilter]);
4148
+ const visibleGrown = (0, import_react16.useMemo)(
4149
+ () => normFilter ? grownOnly.filter((r) => r.room.toLowerCase().includes(normFilter)) : grownOnly,
4150
+ [grownOnly, normFilter]
4151
+ );
4152
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { children: [
4153
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { ...S.flexRow, marginBottom: 8 }, children: [
4154
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: S.muted, children: "Hall = Room 大类 · 大类上限 200,小类「大类/小类」细分不占额度 · 与 Wing(域)筛选无关" }),
4155
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: S.grow }),
4156
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4157
+ "button",
4158
+ {
4159
+ type: "button",
4160
+ title: "导出分类数据 CSV(Room 词表+计数)",
4161
+ onClick: () => run(async () => {
4162
+ const r = await rpc("dsh-memory/rooms-export", { kind: "rooms" });
4163
+ if (!r.ok) {
4164
+ setError(r.error.message);
4165
+ return;
4166
+ }
4167
+ if (r.value.csv) downloadCsv(r.value.csv, "dsh-memory-rooms.csv");
4168
+ }),
4169
+ style: btn,
4170
+ children: "导出词表"
4171
+ }
4172
+ ),
4173
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4174
+ "button",
4175
+ {
4176
+ type: "button",
4177
+ title: "导出无 Room 绑定的孤儿清单(含候选)",
4178
+ onClick: () => run(async () => {
4179
+ const r = await rpc("dsh-memory/rooms-export", { kind: "orphans" });
4180
+ if (!r.ok) {
4181
+ setError(r.error.message);
4182
+ return;
4183
+ }
4184
+ if (r.value.csv) downloadCsv(r.value.csv, "dsh-memory-room-orphans.csv");
4185
+ }),
4186
+ style: btn,
4187
+ children: [
4188
+ "导出孤儿 · ",
4189
+ orphanCount
4190
+ ]
4191
+ }
4192
+ ),
4193
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4194
+ "button",
4195
+ {
4196
+ type: "button",
4197
+ style: {
4198
+ ...btn,
4199
+ color: hiPriv ? "var(--dsh-mem-danger)" : void 0,
4200
+ border: hiPriv ? "1px solid var(--dsh-mem-danger)" : btn.border
4201
+ },
4202
+ disabled: hiPrivBusy,
4203
+ title: hiPriv ? "关闭高权限模式(收回模型写删工具与管理操作)" : "开启高权限模式以解锁分类管理操作",
4204
+ onClick: toggleHiPriv,
4205
+ children: hiPriv ? "高权限:开" : "高权限:关"
4206
+ }
4207
+ ),
4208
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", style: btn, onClick: loadAll, title: "重拉注册表与计数", children: "刷新" })
4209
+ ] }),
4210
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { display: "flex", gap: 6, alignItems: "center", marginBottom: 8, flexWrap: "wrap" }, children: [
4211
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4212
+ "input",
4213
+ {
4214
+ value: filter,
4215
+ onChange: (e) => setFilter(e.target.value),
4216
+ placeholder: "搜索 hall/room(slug、显示名、别名)",
4217
+ style: { ...S.input, width: 250, fontSize: 12 }
4218
+ }
4219
+ ),
4220
+ filter ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", style: btn, onClick: () => setFilter(""), children: "清除" }) : null,
4221
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: S.muted, children: `匹配 ${visibleGroups.reduce((s2, g) => s2 + g.entries.length, 0)} room / ${visibleGroups.length} hall` }),
4222
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: S.grow }),
4223
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", style: btn, title: "展开全部 hall 的子 room", onClick: () => setAllHalls(true), children: "展开全部" }),
4224
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", style: btn, title: "收起全部 hall(只留 hall 头)", onClick: () => setAllHalls(false), children: "收起全部" })
4225
+ ] }),
4226
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, alignItems: "center", marginBottom: 10 }, children: [
4227
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4228
+ "input",
4229
+ {
4230
+ value: regSlug,
4231
+ onChange: (e) => setRegSlug(e.target.value),
4232
+ placeholder: "slug(如 dsh-plugin 或 dsh-plugin/merge)",
4233
+ disabled: !hiPriv || busy,
4234
+ style: { ...S.input, width: 210, fontSize: 12 }
4235
+ }
4236
+ ),
4237
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4238
+ "input",
4239
+ {
4240
+ value: regLabel,
4241
+ onChange: (e) => setRegLabel(e.target.value),
4242
+ placeholder: "名称(可中文)",
4243
+ disabled: !hiPriv || busy,
4244
+ style: { ...S.input, width: 110, fontSize: 12 }
4245
+ }
4246
+ ),
4247
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4248
+ "input",
4249
+ {
4250
+ value: regDesc,
4251
+ onChange: (e) => setRegDesc(e.target.value),
4252
+ placeholder: "归类说明(喂标注器)",
4253
+ disabled: !hiPriv || busy,
4254
+ style: { ...S.input, width: 170, fontSize: 12 }
4255
+ }
4256
+ ),
4257
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4258
+ "button",
4259
+ {
4260
+ type: "button",
4261
+ disabled: !hiPriv || busy || !regSlug.trim(),
4262
+ onClick: register,
4263
+ style: { ...btn, color: "var(--dsh-mem-accent)", borderColor: "var(--dsh-mem-accent)", opacity: !hiPriv || busy || !regSlug.trim() ? 0.5 : 1 },
4264
+ children: "注册"
4265
+ }
4266
+ ),
4267
+ !hiPriv && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: S.muted, children: "管理操作需先开启高权限模式" })
4268
+ ] }),
4269
+ editing ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { position: "sticky", top: 0, zIndex: 5, background: "var(--dsh-mem-bg-inset)", display: "flex", alignItems: "center", gap: 6, marginBottom: 8, flexWrap: "wrap", padding: "6px 8px", border: "1px solid var(--dsh-mem-accent)", borderRadius: 8 }, children: [
4270
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { style: S.muted, children: [
4271
+ "编辑 ",
4272
+ editing.slug
4273
+ ] }),
4274
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("input", { value: editing.label, onChange: (ev) => setEditing({ ...editing, label: ev.target.value }), placeholder: "显示名(可中文)", disabled: busy, style: { ...S.input, width: 140, fontSize: 12 } }),
4275
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("input", { value: editing.description, onChange: (ev) => setEditing({ ...editing, description: ev.target.value }), placeholder: "归类说明(喂标注器)", disabled: busy, style: { ...S.input, width: 200, fontSize: 12 } }),
4276
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", disabled: busy, onClick: saveEditing, style: { ...btn, color: "var(--dsh-mem-accent)", borderColor: "var(--dsh-mem-accent)" }, children: "保存" }),
4277
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", disabled: busy, onClick: () => setEditing(null), style: btn, children: "取消" })
4278
+ ] }) : null,
4279
+ form ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { position: "sticky", top: 0, zIndex: 5, background: "var(--dsh-mem-bg-inset)", display: "flex", alignItems: "center", gap: 6, marginBottom: 8, flexWrap: "wrap", padding: "6px 8px", border: "1px solid var(--dsh-mem-accent)", borderRadius: 8 }, children: [
4280
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { style: S.muted, children: [
4281
+ form.action === "merge" ? "合并" : "归类/改名",
4282
+ " ",
4283
+ form.from,
4284
+ " →"
4285
+ ] }),
4286
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4287
+ "input",
4288
+ {
4289
+ value: form.to,
4290
+ onChange: (ev) => setForm({ ...form, to: ev.target.value }),
4291
+ placeholder: "目标 slug",
4292
+ disabled: busy,
4293
+ style: { ...S.input, width: 190, fontSize: 12 }
4294
+ }
4295
+ ),
4296
+ form.action === "rename" ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: { display: "inline-flex", gap: 4, flexWrap: "wrap" }, children: groups.filter((gg) => gg.hall !== majorOf(form.from)).map((gg) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4297
+ "button",
4298
+ {
4299
+ type: "button",
4300
+ title: `归入 ${gg.hall} 下(${gg.hall}/${suffixOf(form.from)})`,
4301
+ onClick: () => setForm({ ...form, to: `${gg.hall}/${suffixOf(form.from)}` }),
4302
+ style: { ...btn, borderRadius: 999, padding: "1px 8px" },
4303
+ children: gg.hall
4304
+ },
4305
+ gg.hall
4306
+ )) }) : null,
4307
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", disabled: busy, onClick: () => previewMerge(), style: { ...btn, color: "var(--dsh-mem-accent)", borderColor: "var(--dsh-mem-accent)" }, children: "预览" }),
4308
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", disabled: busy, onClick: () => {
4309
+ setForm(null);
4310
+ setPreview(null);
4311
+ setPicking(null);
4312
+ }, style: btn, children: "取消" }),
4313
+ picking ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: S.muted, children: "挑选中:点击目标 hall 头完成归类" }) : null,
4314
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: S.muted, children: "与反刍并发会互相覆盖,请在反刍空闲时执行。" })
4315
+ ] }) : null,
4316
+ preview ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { position: "sticky", top: 0, zIndex: 5, background: "var(--dsh-mem-bg-inset)", marginBottom: 8, padding: "6px 8px", border: "1px solid var(--dsh-mem-danger)", borderRadius: 8, fontSize: 12 }, children: [
4317
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { children: preview.notice }),
4318
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { display: "flex", gap: 6, marginTop: 4 }, children: [
4319
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("button", { type: "button", disabled: busy, onClick: execMerge, style: { ...btn, color: "var(--dsh-mem-danger)", borderColor: "var(--dsh-mem-danger)" }, children: [
4320
+ "确认执行",
4321
+ preview.action === "merge" ? "合并" : "归类/改名"
4322
+ ] }),
4323
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", disabled: busy, onClick: () => setPreview(null), style: btn, children: "再改改" })
4324
+ ] })
4325
+ ] }) : null,
4326
+ msg ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: S.hint, children: msg }) : null,
4327
+ error ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: S.error, children: error }) : null,
4328
+ ghost ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: { position: "fixed", left: ghost.x + 12, top: ghost.y + 12, zIndex: 9999, pointerEvents: "none", fontSize: 12, padding: "2px 10px", borderRadius: 999, border: "1px solid var(--dsh-mem-accent)", background: "var(--dsh-mem-bg-inset)", color: "var(--dsh-mem-accent)" }, children: ghost.text }) : null,
4329
+ visibleGroups.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { style: S.intro, children: normFilter ? "无匹配的 hall/room。" : "注册表为空。在上方注册 Hall/Room(如 dsh-plugin),或把自生长 slug「收编」进词表 —— 之后候选标注器会优先把相关记忆挂到这些 slug 上。" }) : visibleGroups.map((g) => {
4330
+ const hallOpen = !!normFilter || expandedHalls.has(g.hall);
4331
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { marginBottom: 10, border: "1px solid var(--dsh-mem-border)", borderRadius: 8, overflow: "hidden" }, children: [
4332
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4333
+ "div",
4334
+ {
4335
+ "data-hall": g.hall,
4336
+ onDragOver: (ev) => {
4337
+ if (!hiPriv || busy) return;
4338
+ ev.preventDefault();
4339
+ ev.dataTransfer.dropEffect = "move";
4340
+ setDragOverHall(g.hall);
4341
+ },
4342
+ onDragLeave: () => setDragOverHall((cur) => cur === g.hall ? null : cur),
4343
+ onDrop: (ev) => {
4344
+ ev.preventDefault();
4345
+ setDragOverHall(null);
4346
+ if (!hiPriv || busy) return;
4347
+ const from = ev.dataTransfer.getData("text/dsh-room");
4348
+ if (from) beginReclass(from, g.hall);
4349
+ },
4350
+ onClick: () => {
4351
+ if (picking) {
4352
+ if (!busy) {
4353
+ const from = picking;
4354
+ setPicking(null);
4355
+ beginReclass(from, g.hall);
4356
+ }
4357
+ return;
4358
+ }
4359
+ toggleHall(g.hall);
4360
+ },
4361
+ style: {
4362
+ display: "flex",
4363
+ alignItems: "center",
4364
+ gap: 8,
4365
+ padding: "5px 10px",
4366
+ background: "var(--dsh-mem-bg-inset)",
4367
+ outline: dragOverHall === g.hall || picking !== null ? "2px dashed var(--dsh-mem-accent)" : void 0,
4368
+ outlineOffset: dragOverHall === g.hall || picking !== null ? "-2px" : void 0,
4369
+ cursor: "pointer"
4370
+ },
4371
+ title: picking ? "点击此 hall:把「" + picking + "」归入 " + g.hall + " 下(major/minor,预览后确认)" : hiPriv ? "拖入 room 归类到该 hall 下(major/minor;记录随迁,旧名进别名,预览后确认)" : void 0,
4372
+ children: [
4373
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4374
+ "button",
4375
+ {
4376
+ type: "button",
4377
+ title: hallOpen ? "收起子 room" : "展开子 room(默认折叠)",
4378
+ onClick: (ev) => {
4379
+ ev.stopPropagation();
4380
+ toggleHall(g.hall);
4381
+ },
4382
+ style: { ...btn, borderRadius: 999, padding: "1px 7px", flexShrink: 0 },
4383
+ children: hallOpen ? "▾" : "▸"
4384
+ }
4385
+ ),
4386
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { style: { fontSize: 12, fontWeight: 600 }, children: [
4387
+ g.hall,
4388
+ g.label ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: S.muted, children: " (" + g.label + ")" }) : null
4389
+ ] }),
4390
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: S.muted, children: `合计 ${g.total} 条 · ${g.entries.length} 个 room` }),
4391
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: S.grow }),
4392
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4393
+ "button",
4394
+ {
4395
+ type: "button",
4396
+ disabled: !hiPriv || busy,
4397
+ title: `预填注册表单为 ${g.hall}/…(注册该 hall 下的小类)`,
4398
+ onClick: (ev) => {
4399
+ ev.stopPropagation();
4400
+ setRegSlug(g.hall + "/");
4401
+ },
4402
+ style: btn,
4403
+ children: "+小类"
4404
+ }
4405
+ )
4406
+ ]
4407
+ }
4408
+ ),
4409
+ hallOpen ? g.entries.map((e) => {
4410
+ const standalone = e.slug === g.hall;
4411
+ const count = rooms.find((r) => r.room === e.slug)?.count ?? 0;
4412
+ const retired = e.status === "retired";
4413
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { padding: "4px 10px", borderTop: "1px solid var(--dsh-mem-border)", opacity: retired ? 0.55 : 1 }, children: [
4414
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
4415
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { flex: 1, minWidth: 0, display: "flex", flexWrap: "wrap", gap: "2px 8px", alignItems: "baseline" }, children: [
4416
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4417
+ "button",
4418
+ {
4419
+ type: "button",
4420
+ title: expandedRooms.has(e.slug) ? "收起内容" : "展开查看该 Room 的记录内容(默认收起)",
4421
+ onClick: () => void toggleRoomExpand(e.slug),
4422
+ style: { ...btn, borderRadius: 999, padding: "1px 7px", flexShrink: 0 },
4423
+ children: expandedRooms.has(e.slug) ? "▾" : "▸"
4424
+ }
4425
+ ),
4426
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4427
+ "span",
4428
+ {
4429
+ style: { fontSize: 12, cursor: hiPriv && !busy ? "grab" : "default", touchAction: "none" },
4430
+ onPointerDown: (ev) => onDragLabelDown(ev, e.slug),
4431
+ onPointerMove: onDragLabelMove,
4432
+ onPointerUp: onDragLabelUp,
4433
+ title: hiPriv && !busy ? "按住拖到某个 hall 头上归类(major/minor;记录随迁,旧名进别名)" : e.description ? `${e.slug} — ${e.description}` : e.slug,
4434
+ children: [
4435
+ standalone ? e.slug : "· " + e.slug.slice(g.hall.length + 1),
4436
+ e.source === "pre-registered" ? " ⭐" : ""
4437
+ ]
4438
+ }
4439
+ ),
4440
+ e.label ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: S.muted, children: e.label }) : null,
4441
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: S.muted, children: count + " 条" }),
4442
+ retired ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: { ...S.muted, color: "var(--dsh-mem-danger)" }, children: "已退役" }) : null,
4443
+ (e.aliases ?? []).length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: { ...S.muted, wordBreak: "break-all" }, children: "别名: " + (e.aliases ?? []).join(", ") }) : null
4444
+ ] }),
4445
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { display: "flex", gap: 6, flexShrink: 0, alignItems: "center" }, children: [
4446
+ !hiPriv ? null : /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
4447
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", disabled: busy, title: "编辑显示名/归类说明", onClick: () => {
4448
+ setEditing({ slug: e.slug, label: e.label ?? "", description: e.description ?? "" });
4449
+ setForm(null);
4450
+ setPreview(null);
4451
+ setPicking(null);
4452
+ }, style: btn, children: "编辑" }),
4453
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4454
+ "button",
4455
+ {
4456
+ type: "button",
4457
+ disabled: busy,
4458
+ title: picking === e.slug ? "再点一次取消挑选;然后点击目标 hall 头完成归类" : "归类到已有 hall 下:点击后选目标 hall(记录随迁,旧名进别名)",
4459
+ onClick: () => {
4460
+ if (picking === e.slug) {
4461
+ setPicking(null);
4462
+ return;
4463
+ }
4464
+ setForm({ action: "rename", from: e.slug, to: "" });
4465
+ setPreview(null);
4466
+ setPicking(e.slug);
4467
+ setMsg(null);
4468
+ setError(null);
4469
+ },
4470
+ style: { ...btn, ...picking === e.slug ? { color: "var(--dsh-mem-accent)", borderColor: "var(--dsh-mem-accent)" } : {} },
4471
+ children: picking === e.slug ? "选 hall…" : "归类"
4472
+ }
4473
+ ),
4474
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", disabled: busy, title: `改名(= merge 1:1 + 注册表改名)`, onClick: () => {
4475
+ setForm({ action: "rename", from: e.slug, to: "" });
4476
+ setPreview(null);
4477
+ }, style: btn, children: "改名" }),
4478
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", disabled: busy, title: "合并到另一个 Room(dryRun 预览→确认实跑)", onClick: () => {
4479
+ setForm({ action: "merge", from: e.slug, to: "" });
4480
+ setPreview(null);
4481
+ }, style: btn, children: "合并" }),
4482
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", disabled: busy, title: retired ? "恢复该 Room 到词表" : "退役该 Room(存量 tags 不动)", onClick: () => retire(e.slug, retired), style: { ...btn, ...retired ? { color: "var(--dsh-mem-accent)", borderColor: "var(--dsh-mem-accent)" } : { color: "var(--dsh-mem-danger)", borderColor: "var(--dsh-mem-danger)" } }, children: retired ? "恢复" : "退役" }),
4483
+ !retired ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", disabled: busy, title: "退役并退场该 Room 下全部记忆(软删,可在「已退场」区恢复)", onClick: () => retire(e.slug, false, true), style: { ...btn, color: "var(--dsh-mem-danger)", borderColor: "var(--dsh-mem-danger)" }, children: "退场" }) : null
4484
+ ] }),
4485
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", disabled: busy, title: "导出该 Room 的记录清单 CSV", onClick: () => exportRecords(e.slug), style: btn, children: "导出" })
4486
+ ] })
4487
+ ] }),
4488
+ expandedRooms.has(e.slug) ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { margin: "4px 0 2px", padding: "4px 8px", border: "1px dashed var(--dsh-mem-border)", borderRadius: 6 }, children: [
4489
+ (roomRecords[e.slug]?.items ?? []).map((it) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { fontSize: 11, padding: "3px 0", borderBottom: "1px solid var(--dsh-mem-border)" }, children: [
4490
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "dsh-mem-tag dsh-mem-tag-" + it.type, children: TYPE_LABELS[it.type] || it.type }),
4491
+ " ",
4492
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: S.muted, children: it.updatedAt ? fmtTime(it.updatedAt) : "" }),
4493
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: { marginTop: 2, wordBreak: "break-all", color: "var(--dsh-mem-text-2)" }, children: it.content.length > 120 ? it.content.slice(0, 120) + "…" : it.content })
4494
+ ] }, it.id)),
4495
+ (roomRecords[e.slug]?.items.length ?? 0) === 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: S.muted, children: "该 Room 暂无记录。" }) : null,
4496
+ (roomRecords[e.slug]?.total ?? 0) > (roomRecords[e.slug]?.items.length ?? 0) ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: { ...S.muted, fontSize: 11 }, children: "共 " + roomRecords[e.slug].total + " 条,仅显示前 " + roomRecords[e.slug].items.length + " 条" }) : null
4497
+ ] }) : null
4498
+ ] }, e.slug);
4499
+ }) : null
4500
+ ] }, g.hall);
4501
+ }),
4502
+ visibleGrown.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { marginTop: 4 }, children: [
4503
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("button", { type: "button", onClick: () => setGrownOpen(!grownOpen), style: btn, title: grownOpen ? "收起未注册自生长 Room" : "展开未注册自生长 Room", children: [
4504
+ grownOpen ? "▾" : "▸",
4505
+ " 自生长未注册 · 共 ",
4506
+ visibleGrown.length
4507
+ ] }),
4508
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: S.muted, children: "(只在 tags 里涌现,不在词表 —— 收编后标注器才会优先挂靠)" }),
4509
+ grownOpen ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, alignItems: "center", marginTop: 6 }, children: visibleGrown.map((r) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { style: { display: "inline-flex", alignItems: "center", gap: 4 }, children: [
4510
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4511
+ "span",
4512
+ {
4513
+ style: { ...btn, cursor: hiPriv && !busy ? "grab" : "default", borderRadius: 999, padding: "2px 8px", touchAction: "none" },
4514
+ onPointerDown: (ev) => onDragLabelDown(ev, r.room),
4515
+ onPointerMove: onDragLabelMove,
4516
+ onPointerUp: onDragLabelUp,
4517
+ title: hiPriv && !busy ? "自生长 slug(未注册)——按住拖到某个 hall 头上 = 收编并归为其子类" : "自生长 slug(未注册)",
4518
+ children: r.room + " · " + r.count
4519
+ }
4520
+ ),
4521
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", disabled: !hiPriv || busy, title: "收编进注册表(source=grown,原地)", onClick: () => adopt(r.room), style: { ...btn, color: "var(--dsh-mem-accent)", borderColor: "var(--dsh-mem-accent)", opacity: !hiPriv || busy ? 0.5 : 1 }, children: "收编" }),
4522
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4523
+ "button",
4524
+ {
4525
+ type: "button",
4526
+ disabled: !hiPriv || busy,
4527
+ title: "归类到已有 hall 下:点击后选目标 hall(收编+子类一步,记录随迁)",
4528
+ onClick: () => {
4529
+ if (picking === r.room) {
4530
+ setPicking(null);
4531
+ return;
4532
+ }
4533
+ setForm({ action: "rename", from: r.room, to: "" });
4534
+ setPreview(null);
4535
+ setPicking(r.room);
4536
+ setMsg(null);
4537
+ setError(null);
4538
+ },
4539
+ style: { ...btn, opacity: !hiPriv || busy ? 0.5 : 1, ...picking === r.room ? { color: "var(--dsh-mem-accent)", borderColor: "var(--dsh-mem-accent)" } : {} },
4540
+ children: picking === r.room ? "选 hall…" : "归"
4541
+ }
4542
+ )
4543
+ ] }, r.room)) }) : null
4544
+ ] }) : null
4545
+ ] });
4546
+ }
4547
+
4548
+ // client/src/tabs/RoomTab.tsx
4549
+ var import_react17 = require("react");
4550
+ var import_jsx_runtime17 = require("react/jsx-runtime");
4551
+ var btn2 = {
4552
+ cursor: "pointer",
4553
+ fontSize: 12,
4554
+ padding: "2px 8px",
4555
+ borderRadius: 6,
4556
+ border: "1px solid var(--dsh-mem-border)",
4557
+ background: "transparent",
4558
+ color: "var(--dsh-mem-text-2)"
4559
+ };
4560
+ function downloadCsv2(csv, name) {
4561
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
4562
+ const url = URL.createObjectURL(blob);
4563
+ const a = document.createElement("a");
4564
+ a.href = url;
4565
+ a.download = name;
4566
+ a.click();
4567
+ URL.revokeObjectURL(url);
4568
+ }
4569
+ function RoomTab(props) {
4570
+ const rpc = props.rpc;
4571
+ const [registry, setRegistry] = (0, import_react17.useState)([]);
4572
+ const [counts, setCounts] = (0, import_react17.useState)([]);
4573
+ const [orphanCount, setOrphanCount] = (0, import_react17.useState)(0);
4574
+ const [busy, setBusy] = (0, import_react17.useState)(false);
4575
+ const [msg, setMsg] = (0, import_react17.useState)(null);
4576
+ const [error, setError] = (0, import_react17.useState)(null);
4577
+ const [hiPriv, setHiPriv] = (0, import_react17.useState)(false);
4578
+ const [hiPrivBusy, setHiPrivBusy] = (0, import_react17.useState)(false);
4579
+ const [reg, setReg] = (0, import_react17.useState)({ slug: "", label: "", description: "" });
4580
+ const [editing, setEditing] = (0, import_react17.useState)(null);
4581
+ const [renaming, setRenaming] = (0, import_react17.useState)(null);
4582
+ const [expanded, setExpanded] = (0, import_react17.useState)(/* @__PURE__ */ new Set());
4583
+ const [retiredFilter, setRetiredFilter] = (0, import_react17.useState)("");
4584
+ const [filter, setFilter] = (0, import_react17.useState)("");
4585
+ const [records, setRecords] = (0, import_react17.useState)({});
4586
+ const loadAll = (0, import_react17.useCallback)(() => {
4587
+ rpc("dsh-memory/rooms-get", {}).then((r) => {
4588
+ if (r && r.ok) {
4589
+ setCounts(r.value.rooms ?? []);
4590
+ setRegistry(r.value.registry ?? []);
4591
+ setOrphanCount(r.value.orphanCount ?? 0);
4592
+ }
4593
+ }).catch(() => void 0);
4594
+ }, [rpc]);
4595
+ const loadHiPriv = (0, import_react17.useCallback)(() => {
4596
+ rpc("dsh-memory/settings-get", {}).then((r) => {
4597
+ if (r && r.ok && r.value) setHiPriv(!!r.value.settings.memoryMutate);
4598
+ }).catch(() => {
4599
+ });
4600
+ }, [rpc]);
4601
+ (0, import_react17.useEffect)(() => {
4602
+ loadAll();
4603
+ loadHiPriv();
4604
+ }, [loadAll, loadHiPriv]);
4605
+ const toggleHiPriv = () => {
4606
+ const next = !hiPriv;
4607
+ if (next && !window.confirm("开启高权限模式:模型获得写入/删除记忆工具,并解锁 Room 管理操作。确定开启?")) return;
4608
+ rpc("dsh-memory/settings-set", { memoryMutate: next }).then((r) => {
4609
+ if (r && r.ok) setHiPriv(next);
4610
+ setHiPrivBusy(false);
4611
+ loadHiPriv();
4612
+ }).catch(() => setHiPrivBusy(false));
4613
+ };
4614
+ const run = (fn) => {
4615
+ if (busy) return;
4616
+ setBusy(true);
4617
+ setError(null);
4618
+ setMsg(null);
4619
+ fn().finally(() => setBusy(false));
4620
+ };
4621
+ const register = () => run(async () => {
4622
+ const r = await rpc("dsh-memory/room-register", { slug: reg.slug.trim(), label: reg.label.trim() || void 0, description: reg.description.trim() || void 0 });
4623
+ if (r && r.ok) {
4624
+ setMsg(r.value?.notice ?? `已注册:${reg.slug.trim()}`);
4625
+ setReg({ slug: "", label: "", description: "" });
4626
+ loadAll();
4627
+ } else setError(r && r.error ? r.error.message : "注册失败");
4628
+ });
4629
+ const adopt = (slug) => run(async () => {
4630
+ const r = await rpc("dsh-memory/room-register", { slug, source: "grown" });
4631
+ if (r && r.ok) {
4632
+ setMsg(`已收编:${slug}(source=grown)`);
4633
+ loadAll();
4634
+ } else setError(r && r.error ? r.error.message : "收编失败");
4635
+ });
4636
+ const previewRename = () => run(async () => {
4637
+ if (!renaming) return;
4638
+ const to = renaming.to.trim();
4639
+ if (!to) {
4640
+ setError("目标 slug 不能为空");
4641
+ return;
4642
+ }
4643
+ if (to === renaming.slug) {
4644
+ setError("目标与源相同");
4645
+ return;
4646
+ }
4647
+ if (!registry.some((e) => e.slug === renaming.slug)) {
4648
+ const rr = await rpc("dsh-memory/room-register", { slug: renaming.slug, source: "grown" });
4649
+ if (!rr || !rr.ok) {
4650
+ setError(rr && rr.error ? rr.error.message : "收编源失败");
4651
+ return;
4652
+ }
4653
+ loadAll();
4654
+ }
4655
+ const r = await rpc("dsh-memory/room-admin", { action: "rename", from: renaming.slug, to });
4656
+ if (r && r.ok) setMsg(String(r.value?.notice ?? ""));
4657
+ else setError(r && r.error ? r.error.message : "预览失败");
4658
+ });
4659
+ const execRename = () => run(async () => {
4660
+ if (!renaming) return;
4661
+ if (!registry.some((e) => e.slug === renaming.slug)) {
4662
+ await rpc("dsh-memory/room-register", { slug: renaming.slug, source: "grown" });
4663
+ }
4664
+ const r = await rpc("dsh-memory/room-admin", { action: "rename", from: renaming.slug, to: renaming.to.trim(), dryRun: false });
4665
+ if (r && r.ok) {
4666
+ setMsg(String(r.value?.notice ?? "完成"));
4667
+ setRenaming(null);
4668
+ loadAll();
4669
+ } else setError(r && r.error ? r.error.message : "执行失败");
4670
+ });
4671
+ const saveEditing = () => run(async () => {
4672
+ if (!editing) return;
4673
+ const r = await rpc("dsh-memory/room-admin", { action: "update", slug: editing.slug, label: editing.label, description: editing.description });
4674
+ if (r && r.ok) {
4675
+ setMsg(String(r.value?.notice ?? "已更新"));
4676
+ setEditing(null);
4677
+ loadAll();
4678
+ } else setError(r && r.error ? r.error.message : "更新失败");
4679
+ });
4680
+ const retire = (slug, active, withRecords = false) => run(async () => {
4681
+ if (!active) {
4682
+ const tip = withRecords ? `退役 Room:${slug} 并退场其下全部记忆?(记录为软删,可在记忆页「已退场」区逐条恢复)` : `退役 Room:${slug}?(存量记录的 tags 不动,仅词表不再推荐;可随时恢复)`;
4683
+ if (!window.confirm(tip)) return;
4684
+ }
4685
+ const r = await rpc("dsh-memory/room-admin", { action: "retire", slug, active, withRecords: withRecords || void 0 });
4686
+ if (r && r.ok) {
4687
+ setMsg(String(r.value?.notice ?? "完成"));
4688
+ loadAll();
4689
+ if (expanded.has(slug)) {
4690
+ const rr = await rpc("dsh-memory/list-records", { tag: slug, limit: 10, retired: false });
4691
+ if (rr && rr.ok) setRecords((m) => ({ ...m, [slug]: { items: rr.value.items, total: rr.value.total ?? null } }));
4692
+ }
4693
+ } else setError(r && r.error ? r.error.message : "操作失败");
4694
+ });
4695
+ const exportRecords = (slug) => run(async () => {
4696
+ const r = await rpc("dsh-memory/rooms-export", { kind: "records", tag: slug });
4697
+ if (!r.ok) {
4698
+ setError(r.error.message);
4699
+ return;
4700
+ }
4701
+ if (r.value.csv) {
4702
+ const blob = new Blob([r.value.csv], { type: "text/csv;charset=utf-8" });
4703
+ const url = URL.createObjectURL(blob);
4704
+ const a = document.createElement("a");
4705
+ a.href = url;
4706
+ a.download = `dsh-memory-room-${slug.replace(/\//g, "-")}-records.csv`;
4707
+ a.click();
4708
+ URL.revokeObjectURL(url);
4709
+ }
4710
+ });
4711
+ const toggleExpand = (slug) => run(async () => {
4712
+ if (expanded.has(slug)) {
4713
+ const next = new Set(expanded);
4714
+ next.delete(slug);
4715
+ setExpanded(next);
4716
+ return;
4717
+ }
4718
+ const r = await rpc("dsh-memory/list-records", { tag: slug, limit: 10, retired: false });
4719
+ if (r && r.ok) {
4720
+ const next = new Set(expanded);
4721
+ next.add(slug);
4722
+ setExpanded(next);
4723
+ setRecords((m) => ({ ...m, [slug]: { items: r.value.items, total: r.value.total ?? null } }));
4724
+ } else setError(r && r.error ? r.error.message : "加载失败");
4725
+ });
4726
+ const countOf = (slug) => counts.find((c) => c.room === slug)?.count ?? 0;
4727
+ const rows = [
4728
+ ...registry.map((e) => ({ slug: e.slug, count: countOf(e.slug), entry: e })),
4729
+ ...counts.filter((c) => !registry.some((e) => e.slug === c.room)).map((c) => ({ slug: c.room, count: c.count }))
4730
+ ].filter((row2) => {
4731
+ if (retiredFilter === "active") return row2.entry?.status !== "retired";
4732
+ if (retiredFilter === "retired") return row2.entry?.status === "retired";
4733
+ return true;
4734
+ });
4735
+ const normFilter = filter.trim().toLowerCase();
4736
+ const visibleRows = normFilter ? rows.filter(
4737
+ (row2) => row2.slug.toLowerCase().includes(normFilter) || (row2.entry?.label ?? "").toLowerCase().includes(normFilter) || (row2.entry?.aliases ?? []).some((a) => a.toLowerCase().includes(normFilter))
4738
+ ) : rows;
4739
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
4740
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { style: { ...S.flexRow, marginBottom: 8, flexWrap: "wrap" }, children: [
4741
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { style: S.muted, children: "Room 级管理:存在(注册/收编/退役)· 名称(显示名/说明/改名)· 条目(展开浏览) · 归类到 hall 请到「Hall」页" }),
4742
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: S.grow }),
4743
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", style: btn2, title: "导出无 Room 绑定的孤儿清单(含候选)", onClick: () => run(async () => {
4744
+ const r = await rpc("dsh-memory/rooms-export", { kind: "orphans" });
4745
+ if (!r.ok) {
4746
+ setError(r.error.message);
4747
+ return;
4748
+ }
4749
+ if (r.value.csv) downloadCsv2(r.value.csv, "dsh-memory-room-orphans.csv");
4750
+ }), children: [
4751
+ "导出孤儿 · ",
4752
+ orphanCount
4753
+ ] }),
4754
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4755
+ "button",
4756
+ {
4757
+ type: "button",
4758
+ style: { ...btn2, color: hiPriv ? "var(--dsh-mem-danger)" : void 0, border: hiPriv ? "1px solid var(--dsh-mem-danger)" : btn2.border },
4759
+ disabled: hiPrivBusy,
4760
+ title: hiPriv ? "关闭高权限模式" : "开启高权限模式以解锁 Room 管理操作",
4761
+ onClick: toggleHiPriv,
4762
+ children: hiPriv ? "高权限:开" : "高权限:关"
4763
+ }
4764
+ ),
4765
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", style: btn2, onClick: loadAll, children: "刷新" })
4766
+ ] }),
4767
+ editing ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { style: { position: "sticky", top: 0, zIndex: 5, background: "var(--dsh-mem-bg-inset)", display: "flex", alignItems: "center", gap: 6, marginBottom: 8, flexWrap: "wrap", padding: "6px 8px", border: "1px solid var(--dsh-mem-accent)", borderRadius: 8 }, children: [
4768
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { style: S.muted, children: [
4769
+ "编辑 ",
4770
+ editing.slug
4771
+ ] }),
4772
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("input", { value: editing.label, onChange: (e) => setEditing({ ...editing, label: e.target.value }), placeholder: "显示名(可中文)", disabled: busy, style: { ...S.input, width: 140, fontSize: 12 } }),
4773
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("input", { value: editing.description, onChange: (e) => setEditing({ ...editing, description: e.target.value }), placeholder: "归类说明(喂标注器)", disabled: busy, style: { ...S.input, width: 200, fontSize: 12 } }),
4774
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", disabled: busy, onClick: saveEditing, style: { ...btn2, color: "var(--dsh-mem-accent)", borderColor: "var(--dsh-mem-accent)" }, children: "保存" }),
4775
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", disabled: busy, onClick: () => setEditing(null), style: btn2, children: "取消" })
4776
+ ] }) : null,
4777
+ renaming ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { style: { position: "sticky", top: 0, zIndex: 5, background: "var(--dsh-mem-bg-inset)", display: "flex", alignItems: "center", gap: 6, marginBottom: 8, flexWrap: "wrap", padding: "6px 8px", border: "1px solid var(--dsh-mem-accent)", borderRadius: 8 }, children: [
4778
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { style: S.muted, children: [
4779
+ "改名 ",
4780
+ renaming.slug,
4781
+ " →"
4782
+ ] }),
4783
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("input", { value: renaming.to, onChange: (e) => setRenaming({ ...renaming, to: e.target.value }), placeholder: "新 slug(可两级:hall/子类)", disabled: busy, style: { ...S.input, width: 200, fontSize: 12 } }),
4784
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", disabled: busy, onClick: previewRename, style: { ...btn2, color: "var(--dsh-mem-accent)", borderColor: "var(--dsh-mem-accent)" }, children: "预览" }),
4785
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", disabled: busy, onClick: execRename, style: { ...btn2, color: "var(--dsh-mem-danger)", borderColor: "var(--dsh-mem-danger)" }, children: "确认执行" }),
4786
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", disabled: busy, onClick: () => setRenaming(null), style: btn2, children: "取消" }),
4787
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { style: S.muted, children: "记录随迁;与反刍并发会互相覆盖,请在反刍空闲时执行。" })
4788
+ ] }) : null,
4789
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 8, flexWrap: "wrap" }, children: [
4790
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { style: S.muted, children: "退场筛选" }),
4791
+ [["", "全部"], ["active", "活跃"], ["retired", "已退役"]].map(([val, label]) => {
4792
+ const on = retiredFilter === val;
4793
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4794
+ "button",
4795
+ {
4796
+ type: "button",
4797
+ title: val === "" ? "全部 room" : val === "active" ? "只看词表未退役的 room" : "只看已退役的 room",
4798
+ onClick: () => setRetiredFilter(val),
4799
+ style: {
4800
+ cursor: "pointer",
4801
+ fontSize: 12,
4802
+ padding: "2px 10px",
4803
+ borderRadius: 999,
4804
+ border: on ? "1px solid var(--dsh-mem-accent)" : "1px solid var(--dsh-mem-border)",
4805
+ background: on ? "var(--dsh-mem-bg-inset)" : "transparent",
4806
+ color: on ? "var(--dsh-mem-accent)" : "var(--dsh-mem-text-2)"
4807
+ },
4808
+ children: label
4809
+ },
4810
+ val
4811
+ );
4812
+ }),
4813
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: S.grow }),
4814
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4815
+ "input",
4816
+ {
4817
+ value: filter,
4818
+ onChange: (e) => setFilter(e.target.value),
4819
+ placeholder: "搜索 room(slug、显示名、别名)",
4820
+ style: { ...S.input, width: 230, fontSize: 12 }
4821
+ }
4822
+ ),
4823
+ filter ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", style: btn2, onClick: () => setFilter(""), children: "清除" }) : null
4824
+ ] }),
4825
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, alignItems: "center", marginBottom: 10 }, children: [
4826
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("input", { value: reg.slug, onChange: (e) => setReg({ ...reg, slug: e.target.value }), placeholder: "slug(如 dsh-plugin 或 dsh-plugin/merge)", disabled: !hiPriv || busy, style: { ...S.input, width: 210, fontSize: 12 } }),
4827
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("input", { value: reg.label, onChange: (e) => setReg({ ...reg, label: e.target.value }), placeholder: "名称(可中文)", disabled: !hiPriv || busy, style: { ...S.input, width: 110, fontSize: 12 } }),
4828
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("input", { value: reg.description, onChange: (e) => setReg({ ...reg, description: e.target.value }), placeholder: "归类说明(喂标注器)", disabled: !hiPriv || busy, style: { ...S.input, width: 170, fontSize: 12 } }),
4829
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", disabled: !hiPriv || busy || !reg.slug.trim(), onClick: register, style: { ...btn2, color: "var(--dsh-mem-accent)", borderColor: "var(--dsh-mem-accent)", opacity: !hiPriv || busy || !reg.slug.trim() ? 0.5 : 1 }, children: "注册" }),
4830
+ !hiPriv && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { style: S.muted, children: "管理操作需先开启高权限模式" })
4831
+ ] }),
4832
+ msg ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: S.hint, children: msg }) : null,
4833
+ error ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: S.error, children: error }) : null,
4834
+ visibleRows.length === 0 ? normFilter ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { style: S.intro, children: "无匹配的 Room。" }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { style: S.intro, children: "暂无 Room:对话/反刍会从标签涌现,或在上方注册。" }) : visibleRows.map((row2) => {
4835
+ const e = row2.entry;
4836
+ const retired = e?.status === "retired";
4837
+ const grownOnly = !e;
4838
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { style: { marginBottom: 6, border: "1px solid var(--dsh-mem-border)", borderRadius: 8 }, children: [
4839
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "4px 10px" }, children: [
4840
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4841
+ "button",
4842
+ {
4843
+ type: "button",
4844
+ title: expanded.has(row2.slug) ? "收起记忆条目" : "展开查看该 Room 的记忆条目(默认收起)",
4845
+ onClick: () => void toggleExpand(row2.slug),
4846
+ style: { ...btn2, borderRadius: 999, padding: "1px 7px", flexShrink: 0 },
4847
+ children: expanded.has(row2.slug) ? "▾" : "▸"
4848
+ }
4849
+ ),
4850
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { style: { flex: 1, minWidth: 0, display: "flex", flexWrap: "wrap", gap: "2px 8px", alignItems: "baseline" }, children: [
4851
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { style: { fontSize: 12, opacity: retired ? 0.6 : 1 }, children: [
4852
+ row2.slug,
4853
+ e?.source === "pre-registered" ? " ⭐" : ""
4854
+ ] }),
4855
+ e?.label ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { style: S.muted, children: e.label }) : null,
4856
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { style: S.muted, children: row2.count + " 条" }),
4857
+ grownOnly ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { style: S.muted, children: "(自生长未注册)" }) : null,
4858
+ retired ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { style: { ...S.muted, color: "var(--dsh-mem-danger)" }, children: "已退役" }) : null,
4859
+ (e?.aliases ?? []).length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { style: { ...S.muted, wordBreak: "break-all" }, children: "别名: " + (e?.aliases ?? []).join(", ") }) : null
4860
+ ] }),
4861
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { style: { display: "flex", gap: 6, flexShrink: 0, alignItems: "center" }, children: [
4862
+ !hiPriv || grownOnly ? null : /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
4863
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", disabled: busy, title: "编辑显示名/归类说明", onClick: () => {
4864
+ setEditing({ slug: row2.slug, label: e?.label ?? "", description: e?.description ?? "" });
4865
+ setRenaming(null);
4866
+ }, style: btn2, children: "编辑" }),
4867
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", disabled: busy, title: "改名(可两级 slug;记录随迁,旧名进别名)", onClick: () => {
4868
+ setRenaming({ slug: row2.slug, to: "" });
4869
+ setEditing(null);
4870
+ }, style: btn2, children: "改名" }),
4871
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4872
+ "button",
4873
+ {
4874
+ type: "button",
4875
+ disabled: busy,
4876
+ title: retired ? "恢复该 Room 到词表" : "退役该 Room(存量 tags 不动)",
4877
+ onClick: () => retire(row2.slug, retired),
4878
+ style: { ...btn2, ...retired ? { color: "var(--dsh-mem-accent)", borderColor: "var(--dsh-mem-accent)" } : { color: "var(--dsh-mem-danger)", borderColor: "var(--dsh-mem-danger)" } },
4879
+ children: retired ? "恢复" : "退役"
4880
+ }
4881
+ ),
4882
+ !retired ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", disabled: busy, title: "退役并退场该 Room 下全部记忆(软删,可在「已退场」区恢复)", onClick: () => retire(row2.slug, false, true), style: { ...btn2, color: "var(--dsh-mem-danger)", borderColor: "var(--dsh-mem-danger)" }, children: "退场" }) : null
4883
+ ] }),
4884
+ grownOnly ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
4885
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", disabled: !hiPriv || busy, title: "收编进注册表(source=grown)", onClick: () => adopt(row2.slug), style: { ...btn2, color: "var(--dsh-mem-accent)", borderColor: "var(--dsh-mem-accent)", opacity: !hiPriv || busy ? 0.5 : 1 }, children: "收编" }),
4886
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", disabled: !hiPriv || busy, title: "退场该 Room 下全部记忆(软删,可在「已退场」区恢复)", onClick: () => {
4887
+ setRenaming(null);
4888
+ retire(row2.slug, false, true);
4889
+ }, style: { ...btn2, color: "var(--dsh-mem-danger)", borderColor: "var(--dsh-mem-danger)", opacity: !hiPriv || busy ? 0.5 : 1 }, children: "退场" })
4890
+ ] }) : null,
4891
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", disabled: busy, title: "导出该 Room 的记录清单 CSV", onClick: () => exportRecords(row2.slug), style: btn2, children: "导出" })
4892
+ ] })
4893
+ ] }),
4894
+ expanded.has(row2.slug) ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { style: { margin: "0 10px 6px", padding: "4px 8px", border: "1px dashed var(--dsh-mem-border)", borderRadius: 6 }, children: [
4895
+ (records[row2.slug]?.items ?? []).map((it) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { style: { fontSize: 11, padding: "3px 0", borderBottom: "1px solid var(--dsh-mem-border)" }, children: [
4896
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "dsh-mem-tag dsh-mem-tag-" + it.type, children: TYPE_LABELS[it.type] || it.type }),
4897
+ " ",
4898
+ it.retired ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { style: { ...S.muted, color: "var(--dsh-mem-danger)" }, children: "[已退场]" }) : null,
4899
+ " ",
4900
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { style: S.muted, children: it.updatedAt ? fmtTime(it.updatedAt) : "" }),
4901
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: { marginTop: 2, wordBreak: "break-all", color: "var(--dsh-mem-text-2)", opacity: it.retired ? 0.55 : 1 }, children: it.content.length > 140 ? it.content.slice(0, 140) + "…" : it.content })
4902
+ ] }, it.id)),
4903
+ (records[row2.slug]?.items.length ?? 0) === 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: S.muted, children: "该 Room 暂无记忆条目。" }) : null,
4904
+ (records[row2.slug]?.total ?? 0) > (records[row2.slug]?.items.length ?? 0) ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: { ...S.muted, fontSize: 11 }, children: "共 " + records[row2.slug].total + " 条,仅显示前 " + records[row2.slug].items.length + " 条" }) : null
4905
+ ] }) : null
4906
+ ] }, row2.slug);
4907
+ })
4908
+ ] });
4909
+ }
4910
+
4911
+ // client/src/tabs/ScenesTab.tsx
4912
+ var import_react18 = require("react");
4913
+ var import_jsx_runtime18 = require("react/jsx-runtime");
3753
4914
  function SceneCard(props) {
3754
4915
  const s = props.s;
3755
- const [open, setOpen] = (0, import_react16.useState)(false);
3756
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "dsh-mem-card dsh-mem-card-hover", style: S.card, children: [
3757
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4916
+ const [open, setOpen] = (0, import_react18.useState)(false);
4917
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dsh-mem-card dsh-mem-card-hover", style: S.card, children: [
4918
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
3758
4919
  "div",
3759
4920
  {
3760
4921
  style: { ...S.sceneHead, cursor: "pointer", userSelect: "none" },
@@ -3762,23 +4923,23 @@ var __defProp = Object.defineProperty;
3762
4923
  setOpen(!open);
3763
4924
  },
3764
4925
  children: [
3765
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "dsh-mem-scene-chev", style: { transform: open ? "rotate(90deg)" : "none" }, children: "▸" }),
3766
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: S.sceneTitle, children: s.path }),
3767
- s.heat ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: S.muted, children: "热度 " + s.heat }) : null,
3768
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: S.grow }),
3769
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: S.muted, children: "更新 " + fmtTime(s.updated) })
4926
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "dsh-mem-scene-chev", style: { transform: open ? "rotate(90deg)" : "none" }, children: "▸" }),
4927
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { style: S.sceneTitle, children: s.path }),
4928
+ s.heat ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { style: S.muted, children: "热度 " + s.heat }) : null,
4929
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: S.grow }),
4930
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { style: S.muted, children: "更新 " + fmtTime(s.updated) })
3770
4931
  ]
3771
4932
  }
3772
4933
  ),
3773
- s.summary ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: { ...S.muted, marginBottom: 6 }, children: s.summary }) : null,
3774
- open ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("pre", { style: S.pre, children: s.content || "(空)" }) : null
4934
+ s.summary ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { ...S.muted, marginBottom: 6 }, children: s.summary }) : null,
4935
+ open ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("pre", { style: S.pre, children: s.content || "(空)" }) : null
3775
4936
  ] });
3776
4937
  }
3777
4938
  function ScenesTab(props) {
3778
4939
  const rpc = props.rpc;
3779
- const [items, setItems] = (0, import_react16.useState)(null);
3780
- const [error, setError] = (0, import_react16.useState)(null);
3781
- const load = (0, import_react16.useCallback)(() => {
4940
+ const [items, setItems] = (0, import_react18.useState)(null);
4941
+ const [error, setError] = (0, import_react18.useState)(null);
4942
+ const load = (0, import_react18.useCallback)(() => {
3782
4943
  rpc("dsh-memory/scenes", {}).then((r) => {
3783
4944
  if (r && r.ok) {
3784
4945
  setItems(r.value.items);
@@ -3788,54 +4949,58 @@ var __defProp = Object.defineProperty;
3788
4949
  setError(String(e && e.message || e));
3789
4950
  });
3790
4951
  }, [rpc]);
3791
- (0, import_react16.useEffect)(() => {
4952
+ (0, import_react18.useEffect)(() => {
3792
4953
  load();
3793
4954
  }, [load]);
3794
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { children: [
3795
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { ...S.flexRow, marginBottom: 10 }, children: [
3796
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: S.muted, children: items ? items.length + " 个场景块" : "加载中…" }),
3797
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: S.grow }),
3798
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(NButton, { onClick: load, children: "刷新" })
4955
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
4956
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { ...S.flexRow, marginBottom: 10 }, children: [
4957
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { style: S.muted, children: items ? items.length + " 个场景块" : "加载中…" }),
4958
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: S.grow }),
4959
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(NButton, { onClick: load, children: "刷新" })
3799
4960
  ] }),
3800
- error ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: S.error, children: error }) : null,
3801
- items && items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { style: S.intro, children: "暂无场景块。累计 5 条新记忆后 L2 会自动整合出第一个场景。" }) : null,
4961
+ error ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: S.error, children: error }) : null,
4962
+ items && items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { style: S.intro, children: "暂无场景块。累计 5 条新记忆后 L2 会自动整合出第一个场景。" }) : null,
3802
4963
  (items || []).map((s) => {
3803
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(SceneCard, { s }, s.path);
4964
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SceneCard, { s }, s.path);
3804
4965
  })
3805
4966
  ] });
3806
4967
  }
3807
4968
 
3808
4969
  // client/src/panel.tsx
3809
- var import_jsx_runtime17 = require("react/jsx-runtime");
4970
+ var import_jsx_runtime19 = require("react/jsx-runtime");
3810
4971
  var TABS = [
3811
4972
  ["overview", "概览"],
3812
4973
  ["records", "记忆"],
3813
4974
  ["conflicts", "冲突"],
3814
4975
  ["scenes", "场景"],
4976
+ ["rooms", "Hall"],
4977
+ ["room", "Room"],
3815
4978
  ["persona", "画像"],
3816
4979
  ["cost", "成本"],
3817
4980
  ["log", "日志"]
3818
4981
  ];
3819
4982
  function MemoryPanel(props) {
3820
4983
  const rpc = props.rpc;
3821
- const [tab, setTab] = (0, import_react17.useState)("overview");
4984
+ const [tab, setTab] = (0, import_react19.useState)("overview");
3822
4985
  ensureThemeStyle();
3823
- (0, import_react17.useEffect)(() => {
4986
+ (0, import_react19.useEffect)(() => {
3824
4987
  watchSidebarIcon();
3825
4988
  }, []);
3826
4989
  let body;
3827
- if (tab === "overview") body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(OverviewTab, { rpc });
3828
- else if (tab === "records") body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(RecordsTab, { rpc });
3829
- else if (tab === "conflicts") body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ConflictsTab, { rpc });
3830
- else if (tab === "scenes") body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ScenesTab, { rpc });
3831
- else if (tab === "persona") body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(PersonaTab, { rpc });
3832
- else if (tab === "cost") body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(CostTab, { rpc });
3833
- else body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(LogTab, { rpc });
3834
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "dsh-mem-root", style: S.section, children: [
3835
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h2", { style: S.heading, children: "记忆 (Memory)" }),
3836
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { style: S.intro, children: "L0~L3 分层蒸馏记忆:浏览被记住的内容,控制记忆模式开关。" }),
3837
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: S.tabbar, children: TABS.map((t) => {
3838
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4990
+ if (tab === "overview") body = /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(OverviewTab, { rpc });
4991
+ else if (tab === "records") body = /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(RecordsTab, { rpc });
4992
+ else if (tab === "conflicts") body = /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ConflictsTab, { rpc });
4993
+ else if (tab === "scenes") body = /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ScenesTab, { rpc });
4994
+ else if (tab === "rooms") body = /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(RoomsTab, { rpc });
4995
+ else if (tab === "room") body = /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(RoomTab, { rpc });
4996
+ else if (tab === "persona") body = /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(PersonaTab, { rpc });
4997
+ else if (tab === "cost") body = /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(CostTab, { rpc });
4998
+ else body = /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(LogTab, { rpc });
4999
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "dsh-mem-root", style: S.section, children: [
5000
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h2", { style: S.heading, children: "记忆 (Memory)" }),
5001
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { style: S.intro, children: "L0~L3 分层蒸馏记忆:浏览被记住的内容,控制记忆模式开关。" }),
5002
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { style: S.tabbar, children: TABS.map((t) => {
5003
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3839
5004
  "button",
3840
5005
  {
3841
5006
  className: tab === t[0] ? "dsh-mem-tab dsh-mem-tab-on" : "dsh-mem-tab",
@@ -3852,7 +5017,7 @@ var __defProp = Object.defineProperty;
3852
5017
  }
3853
5018
 
3854
5019
  // client/src/pill/MemoryModePill.tsx
3855
- var import_react20 = require("react");
5020
+ var import_react22 = require("react");
3856
5021
 
3857
5022
  // src/util/context-occupancy.ts
3858
5023
  var CONTEXT_METER_CIRCUMFERENCE = 34.55751918948772;
@@ -4168,23 +5333,23 @@ var __defProp = Object.defineProperty;
4168
5333
  }
4169
5334
 
4170
5335
  // client/src/pill/WingWheel.tsx
4171
- var import_react19 = require("react");
5336
+ var import_react21 = require("react");
4172
5337
 
4173
5338
  // client/src/pill/SessionInfoArea.tsx
4174
- var import_react18 = require("react");
4175
- var import_jsx_runtime18 = require("react/jsx-runtime");
5339
+ var import_react20 = require("react");
5340
+ var import_jsx_runtime20 = require("react/jsx-runtime");
4176
5341
  function sinfoCell(val, label, title) {
4177
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { title: title || void 0, children: [
4178
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dsh-mem-sinfo-val", children: val }),
4179
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dsh-mem-sinfo-label", children: label })
5342
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { title: title || void 0, children: [
5343
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "dsh-mem-sinfo-val", children: val }),
5344
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "dsh-mem-sinfo-label", children: label })
4180
5345
  ] });
4181
5346
  }
4182
5347
  function SessionInfoArea(props) {
4183
5348
  const rpc = props.rpc;
4184
5349
  const sessionId = props.sessionId;
4185
- const [stats, setStats] = (0, import_react18.useState)(void 0);
4186
- const busyRef = (0, import_react18.useRef)(false);
4187
- (0, import_react18.useEffect)(() => {
5350
+ const [stats, setStats] = (0, import_react20.useState)(void 0);
5351
+ const busyRef = (0, import_react20.useRef)(false);
5352
+ (0, import_react20.useEffect)(() => {
4188
5353
  if (!rpc || !sessionId) return void 0;
4189
5354
  let alive = true;
4190
5355
  let timer = null;
@@ -4217,7 +5382,7 @@ var __defProp = Object.defineProperty;
4217
5382
  }, [rpc, sessionId]);
4218
5383
  if (stats === null) return null;
4219
5384
  if (stats === void 0) {
4220
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dsh-mem-sinfo", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dsh-mem-sinfo-grid", children: [
5385
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "dsh-mem-sinfo", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "dsh-mem-sinfo-grid", children: [
4221
5386
  sinfoCell("…", "召回命中"),
4222
5387
  sinfoCell("…", "攒批进度"),
4223
5388
  sinfoCell("…", "本会话记忆"),
@@ -4266,21 +5431,21 @@ var __defProp = Object.defineProperty;
4266
5431
  else if (stats.retrieval === "none") note = "检索不可用(FTS 与向量均失效)";
4267
5432
  }
4268
5433
  const ago = fmtAgo(gl.lastExtractAt);
4269
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dsh-mem-sinfo", children: [
4270
- warn ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dsh-mem-sinfo-warn", children: warn }) : null,
4271
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dsh-mem-sinfo-grid", children: [
5434
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "dsh-mem-sinfo", children: [
5435
+ warn ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "dsh-mem-sinfo-warn", children: warn }) : null,
5436
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "dsh-mem-sinfo-grid", children: [
4272
5437
  sinfoCell(rcVal, rcLabel, rcTitle),
4273
5438
  sinfoCell(dVal, dLabel, dTitle),
4274
5439
  sinfoCell(String(di.producedRecords || 0), "本会话记忆", pTitle),
4275
5440
  sinfoCell(stats.l0Count != null ? String(stats.l0Count) : "…", "会话消息")
4276
5441
  ] }),
4277
- note ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dsh-mem-sinfo-note", children: note }) : null,
4278
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dsh-mem-sinfo-sum", children: "待蒸馏 " + (gl.pendingTotal || 0) + " · 上次蒸馏 " + (ago || "尚未蒸馏") })
5442
+ note ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "dsh-mem-sinfo-note", children: note }) : null,
5443
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "dsh-mem-sinfo-sum", children: "待蒸馏 " + (gl.pendingTotal || 0) + " · 上次蒸馏 " + (ago || "尚未蒸馏") })
4279
5444
  ] });
4280
5445
  }
4281
5446
 
4282
5447
  // client/src/pill/WingWheel.tsx
4283
- var import_jsx_runtime19 = require("react/jsx-runtime");
5448
+ var import_jsx_runtime21 = require("react/jsx-runtime");
4284
5449
  var SIZE = 196;
4285
5450
  var CENTER = SIZE / 2;
4286
5451
  var R = 64;
@@ -4323,17 +5488,17 @@ var __defProp = Object.defineProperty;
4323
5488
  function WingWheel(props) {
4324
5489
  ensureThemeStyle();
4325
5490
  const isOff = props.mode === "off";
4326
- const [overview, setOverview] = (0, import_react19.useState)(null);
4327
- const [backfillBusy, setBackfillBusy] = (0, import_react19.useState)(false);
4328
- const [localError, setLocalError] = (0, import_react19.useState)(null);
4329
- const timersRef = (0, import_react19.useRef)([]);
4330
- (0, import_react19.useEffect)(
5491
+ const [overview, setOverview] = (0, import_react21.useState)(null);
5492
+ const [backfillBusy, setBackfillBusy] = (0, import_react21.useState)(false);
5493
+ const [localError, setLocalError] = (0, import_react21.useState)(null);
5494
+ const timersRef = (0, import_react21.useRef)([]);
5495
+ (0, import_react21.useEffect)(
4331
5496
  () => () => {
4332
5497
  for (const t of timersRef.current) window.clearTimeout(t);
4333
5498
  },
4334
5499
  []
4335
5500
  );
4336
- (0, import_react19.useEffect)(() => {
5501
+ (0, import_react21.useEffect)(() => {
4337
5502
  let alive = true;
4338
5503
  props.rpc("dsh-memory/wing-overview", {}).then((r) => {
4339
5504
  if (!alive) return;
@@ -4379,28 +5544,28 @@ var __defProp = Object.defineProperty;
4379
5544
  finish();
4380
5545
  });
4381
5546
  };
4382
- const boxRef = (0, import_react19.useRef)(null);
4383
- const startRef = (0, import_react19.useRef)(null);
4384
- const movedRef = (0, import_react19.useRef)(false);
4385
- const downIndexRef = (0, import_react19.useRef)(null);
4386
- const dragTargetRef = (0, import_react19.useRef)(null);
4387
- const [drag, setDrag] = (0, import_react19.useState)(null);
5547
+ const boxRef = (0, import_react21.useRef)(null);
5548
+ const startRef = (0, import_react21.useRef)(null);
5549
+ const movedRef = (0, import_react21.useRef)(false);
5550
+ const downIndexRef = (0, import_react21.useRef)(null);
5551
+ const dragTargetRef = (0, import_react21.useRef)(null);
5552
+ const [drag, setDrag] = (0, import_react21.useState)(null);
4388
5553
  const lockedIndex = (() => {
4389
5554
  if (props.halls.length !== 1 || !overview) return null;
4390
5555
  const i = overview.corners.findIndex((c) => c.id === props.halls[0]);
4391
5556
  return i >= 0 ? i : null;
4392
5557
  })();
4393
5558
  const activeCorner = drag ? drag.index : lockedIndex;
4394
- const blockPosRef = (0, import_react19.useRef)(
5559
+ const blockPosRef = (0, import_react21.useRef)(
4395
5560
  activeCorner != null ? cornerXY(activeCorner) : { x: CENTER, y: CENTER }
4396
5561
  );
4397
- const blockTargetRef = (0, import_react19.useRef)(blockPosRef.current);
4398
- const [blockPos, setBlockPos] = (0, import_react19.useState)(blockPosRef.current);
4399
- (0, import_react19.useEffect)(() => {
5562
+ const blockTargetRef = (0, import_react21.useRef)(blockPosRef.current);
5563
+ const [blockPos, setBlockPos] = (0, import_react21.useState)(blockPosRef.current);
5564
+ (0, import_react21.useEffect)(() => {
4400
5565
  blockTargetRef.current = activeCorner != null ? cornerXY(activeCorner) : { x: CENTER, y: CENTER };
4401
5566
  }, [activeCorner]);
4402
- const reducedMotionRef = (0, import_react19.useRef)(false);
4403
- (0, import_react19.useEffect)(() => {
5567
+ const reducedMotionRef = (0, import_react21.useRef)(false);
5568
+ (0, import_react21.useEffect)(() => {
4404
5569
  const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
4405
5570
  const apply2 = () => {
4406
5571
  reducedMotionRef.current = mq.matches;
@@ -4409,7 +5574,7 @@ var __defProp = Object.defineProperty;
4409
5574
  mq.addEventListener("change", apply2);
4410
5575
  return () => mq.removeEventListener("change", apply2);
4411
5576
  }, []);
4412
- (0, import_react19.useEffect)(() => {
5577
+ (0, import_react21.useEffect)(() => {
4413
5578
  let raf = 0;
4414
5579
  const loop = () => {
4415
5580
  const cur = blockPosRef.current;
@@ -4498,8 +5663,8 @@ var __defProp = Object.defineProperty;
4498
5663
  }).join(" ");
4499
5664
  const grayStyle = isOff ? { opacity: 0.45, pointerEvents: "none" } : {};
4500
5665
  const boundariesDisabled = props.halls.length === 0 || isOff;
4501
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { style: { width: SIZE }, children: [
4502
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
5666
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { style: { width: SIZE }, children: [
5667
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
4503
5668
  "div",
4504
5669
  {
4505
5670
  ref: boxRef,
@@ -4509,7 +5674,7 @@ var __defProp = Object.defineProperty;
4509
5674
  onPointerUp: onBoxUp,
4510
5675
  onPointerCancel: onBoxUp,
4511
5676
  children: [
4512
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
5677
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
4513
5678
  "svg",
4514
5679
  {
4515
5680
  width: SIZE,
@@ -4517,7 +5682,7 @@ var __defProp = Object.defineProperty;
4517
5682
  style: { position: "absolute", inset: 0, pointerEvents: "none" },
4518
5683
  "aria-hidden": "true",
4519
5684
  children: [
4520
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5685
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4521
5686
  "polygon",
4522
5687
  {
4523
5688
  points: polyPoints,
@@ -4528,7 +5693,7 @@ var __defProp = Object.defineProperty;
4528
5693
  opacity: 0.45
4529
5694
  }
4530
5695
  ),
4531
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5696
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4532
5697
  "polygon",
4533
5698
  {
4534
5699
  points: polyPoints,
@@ -4539,7 +5704,7 @@ var __defProp = Object.defineProperty;
4539
5704
  ),
4540
5705
  [0, 1, 2, 3, 4, 5, 6, 7].map((i) => {
4541
5706
  const p = cornerPos(i);
4542
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5707
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4543
5708
  "line",
4544
5709
  {
4545
5710
  x1: CENTER,
@@ -4555,7 +5720,7 @@ var __defProp = Object.defineProperty;
4555
5720
  ]
4556
5721
  }
4557
5722
  ),
4558
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5723
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4559
5724
  "div",
4560
5725
  {
4561
5726
  "aria-hidden": "true",
@@ -4574,7 +5739,7 @@ var __defProp = Object.defineProperty;
4574
5739
  }
4575
5740
  }
4576
5741
  ),
4577
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5742
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4578
5743
  "button",
4579
5744
  {
4580
5745
  type: "button",
@@ -4608,7 +5773,7 @@ var __defProp = Object.defineProperty;
4608
5773
  const dim = activeCorner != null && !active;
4609
5774
  const empty = count === 0;
4610
5775
  const p = cornerPos(i);
4611
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
5776
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
4612
5777
  "div",
4613
5778
  {
4614
5779
  title: id ? `「${label}」${count ?? 0} 条
@@ -4637,8 +5802,8 @@ var __defProp = Object.defineProperty;
4637
5802
  zIndex: 2
4638
5803
  },
4639
5804
  children: [
4640
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { children: label }),
4641
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { style: { fontSize: 8, opacity: 0.75, fontVariantNumeric: "tabular-nums" }, children: count === null ? " " : empty ? "空角" : `${count} 条` })
5805
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: label }),
5806
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { style: { fontSize: 8, opacity: 0.75, fontVariantNumeric: "tabular-nums" }, children: count === null ? " " : empty ? "空角" : `${count} 条` })
4642
5807
  ]
4643
5808
  },
4644
5809
  id ?? i
@@ -4647,7 +5812,7 @@ var __defProp = Object.defineProperty;
4647
5812
  ]
4648
5813
  }
4649
5814
  ),
4650
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
5815
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
4651
5816
  "div",
4652
5817
  {
4653
5818
  title: props.halls.length === 0 ? "选定某个角(单 Wing)后,这两项才生效" : "锁定 Wing 时两类无角记忆是否参与召回",
@@ -4661,9 +5826,9 @@ var __defProp = Object.defineProperty;
4661
5826
  pointerEvents: boundariesDisabled ? "none" : void 0
4662
5827
  },
4663
5828
  children: [
4664
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: overview ? `另有 ${overview.unlabeled} 条未打标` : "未打标" }),
4665
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
4666
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5829
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: overview ? `另有 ${overview.unlabeled} 条未打标` : "未打标" }),
5830
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
5831
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4667
5832
  Segmented,
4668
5833
  {
4669
5834
  value: props.hallIncludeUnlabeled ? "in" : "ex",
@@ -4674,7 +5839,7 @@ var __defProp = Object.defineProperty;
4674
5839
  onChange: (key) => props.onCommitWingBoundaries({ includeUnlabeled: key === "in" })
4675
5840
  }
4676
5841
  ),
4677
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5842
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4678
5843
  Segmented,
4679
5844
  {
4680
5845
  value: props.hallIncludeGeneral ? "in" : "ex",
@@ -4689,7 +5854,7 @@ var __defProp = Object.defineProperty;
4689
5854
  ]
4690
5855
  }
4691
5856
  ),
4692
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Row, { label: "会话", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5857
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Row, { label: "会话", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4693
5858
  Segmented,
4694
5859
  {
4695
5860
  value: isOff ? "off" : "on",
@@ -4704,7 +5869,7 @@ var __defProp = Object.defineProperty;
4704
5869
  onChange: (key) => props.onCommit(key === "off" ? "off" : "auto")
4705
5870
  }
4706
5871
  ) }),
4707
- props.recall !== void 0 && props.onCommitRecall ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Row, { label: "注入", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5872
+ props.recall !== void 0 && props.onCommitRecall ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Row, { label: "注入", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4708
5873
  Segmented,
4709
5874
  {
4710
5875
  value: props.recall === null ? "follow" : props.recall ? "on" : "off",
@@ -4717,7 +5882,7 @@ var __defProp = Object.defineProperty;
4717
5882
  onChange: (key) => props.onCommitRecall(key === "on" ? true : key === "off" ? false : null)
4718
5883
  }
4719
5884
  ) }) : null,
4720
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
5885
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
4721
5886
  "div",
4722
5887
  {
4723
5888
  style: {
@@ -4728,8 +5893,8 @@ var __defProp = Object.defineProperty;
4728
5893
  marginTop: 10
4729
5894
  },
4730
5895
  children: [
4731
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: overview ? `未打标 ${overview.unlabeled} 条` : "未打标计数加载中" }),
4732
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5896
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: overview ? `未打标 ${overview.unlabeled} 条` : "未打标计数加载中" }),
5897
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4733
5898
  ActionButton,
4734
5899
  {
4735
5900
  label: backfillBusy ? "回填中…" : "回填",
@@ -4741,12 +5906,12 @@ var __defProp = Object.defineProperty;
4741
5906
  ]
4742
5907
  }
4743
5908
  ),
4744
- props.error || localError ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { style: { fontSize: 11, color: "var(--dsh-mem-danger)", marginTop: 8 }, children: props.error || localError }) : null,
4745
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(SessionInfoArea, { rpc: props.rpc, sessionId: props.sessionId })
5909
+ props.error || localError ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { style: { fontSize: 11, color: "var(--dsh-mem-danger)", marginTop: 8 }, children: props.error || localError }) : null,
5910
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(SessionInfoArea, { rpc: props.rpc, sessionId: props.sessionId })
4746
5911
  ] });
4747
5912
  }
4748
5913
  function Row(props) {
4749
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
5914
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
4750
5915
  "div",
4751
5916
  {
4752
5917
  style: {
@@ -4756,7 +5921,7 @@ var __defProp = Object.defineProperty;
4756
5921
  marginTop: 10
4757
5922
  },
4758
5923
  children: [
4759
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { style: { fontSize: 12, color: "var(--dsh-mem-text-3)" }, children: props.label }),
5924
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { style: { fontSize: 12, color: "var(--dsh-mem-text-3)" }, children: props.label }),
4760
5925
  props.children
4761
5926
  ]
4762
5927
  }
@@ -4764,9 +5929,9 @@ var __defProp = Object.defineProperty;
4764
5929
  }
4765
5930
  var LABEL_FALLBACK = ["工作", "人际", "学习", "创作娱乐", "居家", "健康", "财务", "出行"];
4766
5931
  function useViewportClamp(popRef) {
4767
- const shiftRef = (0, import_react19.useRef)(0);
4768
- const [shiftX, setShiftX] = (0, import_react19.useState)(0);
4769
- (0, import_react19.useEffect)(() => {
5932
+ const shiftRef = (0, import_react21.useRef)(0);
5933
+ const [shiftX, setShiftX] = (0, import_react21.useState)(0);
5934
+ (0, import_react21.useEffect)(() => {
4770
5935
  const clamp = () => {
4771
5936
  const el = popRef.current;
4772
5937
  if (!el) return;
@@ -4796,24 +5961,24 @@ var __defProp = Object.defineProperty;
4796
5961
  }
4797
5962
 
4798
5963
  // client/src/pill/MemoryModePill.tsx
4799
- var import_jsx_runtime20 = require("react/jsx-runtime");
5964
+ var import_jsx_runtime22 = require("react/jsx-runtime");
4800
5965
  function MemoryModePill(props) {
4801
5966
  const rpc = props.rpc;
4802
5967
  const sessionId = props.sessionId || props.session && props.session.sessionId;
4803
- const [mode, setMode] = (0, import_react20.useState)(null);
4804
- const [recall, setRecall] = (0, import_react20.useState)(null);
4805
- const [recallResolved, setRecallResolved] = (0, import_react20.useState)(true);
4806
- const [halls, setWings] = (0, import_react20.useState)([]);
4807
- const [hallIncludeUnlabeled, setWingIncludeUnlabeled] = (0, import_react20.useState)(true);
4808
- const [hallIncludeGeneral, setWingIncludeGeneral] = (0, import_react20.useState)(false);
4809
- const [wingLabels, setHallLabels] = (0, import_react20.useState)({});
4810
- const [error, setError] = (0, import_react20.useState)(null);
4811
- const [open, setOpen] = (0, import_react20.useState)(false);
4812
- const wrapRef = (0, import_react20.useRef)(null);
4813
- const popRef = (0, import_react20.useRef)(null);
5968
+ const [mode, setMode] = (0, import_react22.useState)(null);
5969
+ const [recall, setRecall] = (0, import_react22.useState)(null);
5970
+ const [recallResolved, setRecallResolved] = (0, import_react22.useState)(true);
5971
+ const [halls, setWings] = (0, import_react22.useState)([]);
5972
+ const [hallIncludeUnlabeled, setWingIncludeUnlabeled] = (0, import_react22.useState)(true);
5973
+ const [hallIncludeGeneral, setWingIncludeGeneral] = (0, import_react22.useState)(false);
5974
+ const [wingLabels, setHallLabels] = (0, import_react22.useState)({});
5975
+ const [error, setError] = (0, import_react22.useState)(null);
5976
+ const [open, setOpen] = (0, import_react22.useState)(false);
5977
+ const wrapRef = (0, import_react22.useRef)(null);
5978
+ const popRef = (0, import_react22.useRef)(null);
4814
5979
  const shiftX = useViewportClamp(popRef);
4815
- const seqRef = (0, import_react20.useRef)(0);
4816
- const load = (0, import_react20.useCallback)(() => {
5980
+ const seqRef = (0, import_react22.useRef)(0);
5981
+ const load = (0, import_react22.useCallback)(() => {
4817
5982
  if (!sessionId || !rpc) return;
4818
5983
  const token = ++seqRef.current;
4819
5984
  setError(null);
@@ -4838,13 +6003,13 @@ var __defProp = Object.defineProperty;
4838
6003
  }).catch(() => {
4839
6004
  });
4840
6005
  }, [sessionId, rpc]);
4841
- (0, import_react20.useEffect)(() => {
6006
+ (0, import_react22.useEffect)(() => {
4842
6007
  load();
4843
6008
  }, [load]);
4844
- (0, import_react20.useEffect)(() => {
6009
+ (0, import_react22.useEffect)(() => {
4845
6010
  watchSidebarIcon();
4846
6011
  }, []);
4847
- (0, import_react20.useEffect)(() => {
6012
+ (0, import_react22.useEffect)(() => {
4848
6013
  initOccupancyIndicator(
4849
6014
  (endpoint, payload) => rpc(endpoint, payload)
4850
6015
  );
@@ -4852,7 +6017,7 @@ var __defProp = Object.defineProperty;
4852
6017
  watchContextMeter();
4853
6018
  noteOccupancySession(sessionId ?? null);
4854
6019
  }, [sessionId, rpc]);
4855
- (0, import_react20.useEffect)(() => {
6020
+ (0, import_react22.useEffect)(() => {
4856
6021
  if (!open) return;
4857
6022
  const onDown = (e) => {
4858
6023
  if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
@@ -4992,8 +6157,8 @@ var __defProp = Object.defineProperty;
4992
6157
  pillStyle.boxShadow = "0 0 12px color-mix(in srgb, " + info.color + " 30%, transparent)";
4993
6158
  pillStyle["--dsh-mem-pill-tint"] = info.color;
4994
6159
  }
4995
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { ref: wrapRef, style: { position: "relative", display: "inline-flex" }, children: [
4996
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
6160
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { ref: wrapRef, style: { position: "relative", display: "inline-flex" }, children: [
6161
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
4997
6162
  "button",
4998
6163
  {
4999
6164
  type: "button",
@@ -5006,11 +6171,11 @@ var __defProp = Object.defineProperty;
5006
6171
  style: pillStyle,
5007
6172
  children: [
5008
6173
  "记忆 · ",
5009
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { children: faceLabel })
6174
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { children: faceLabel })
5010
6175
  ]
5011
6176
  }
5012
6177
  ),
5013
- open ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
6178
+ open ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
5014
6179
  "div",
5015
6180
  {
5016
6181
  ref: popRef,
@@ -5021,12 +6186,12 @@ var __defProp = Object.defineProperty;
5021
6186
  transform: "translateX(calc(-50% + " + shiftX + "px))",
5022
6187
  zIndex: 1e3
5023
6188
  },
5024
- children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
6189
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
5025
6190
  "div",
5026
6191
  {
5027
6192
  className: "dsh-mem-popover",
5028
6193
  style: { position: "relative", padding: "10px 12px" },
5029
- children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
6194
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
5030
6195
  WingWheel,
5031
6196
  {
5032
6197
  mode: mode || "auto",