sage-mem 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -2231,6 +2231,16 @@ const StarmapRuntime = (function (require, _react) {
2231
2231
  ".smem-page-top{display:flex;align-items:center;}",
2232
2232
  ".smem-page-back{display:inline-flex;align-items:center;gap:5px;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:12.5px;line-height:1.4;padding:3px 6px 3px 2px;border-radius:6px;cursor:pointer;}",
2233
2233
  ".smem-page-back:hover{color:var(--dsw-alias-label-primary,#1f2329);background:var(--dsw-alias-bg-layer-2);}",
2234
+ // ── 1.8.2:插件详情页的「侧栏入口」卡片 ──
2235
+ // 融入宿主插件页:标题 13px/600、说明 12px 次要色,圆角与分隔线只用宿主 token。
2236
+ // 不抄设置页的大标题 —— 这一块是插件页里的一张紧凑卡片,不是另一个 settings section。
2237
+ ".smem-plugin-card{display:flex;flex-direction:column;gap:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-1,#fafbfc);box-sizing:border-box;}",
2238
+ ".smem-plugin-card-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
2239
+ ".smem-plugin-card-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#1f2430);line-height:1.4;}",
2240
+ ".smem-plugin-card-status{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);line-height:1.4;}",
2241
+ ".smem-plugin-card-check{font-size:12.5px;}",
2242
+ ".smem-plugin-card-note{font-size:12px;line-height:1.55;color:var(--dsw-alias-label-secondary,#6b7280);}",
2243
+ ".smem-plugin-card-err{font-size:12px;line-height:1.5;color:var(--dsw-alias-state-error-primary,#ef4444);overflow-wrap:anywhere;}",
2234
2244
  // ── 底部一行仓库链接(引流,不抢视线)──
2235
2245
  ".smem-star{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-tertiary,#8b93a1);padding:2px 2px 0;}",
2236
2246
  ".smem-star-link{color:var(--dsw-alias-brand-primary,#4d76e6);text-decoration:underline;}",
@@ -2499,7 +2509,7 @@ const StarmapRuntime = (function (require, _react) {
2499
2509
  const isOpen = !!props.expanded;
2500
2510
  return h("div", { className: "smem-card", key: item.file }, [
2501
2511
  h("div", { className: "smem-card-head", key: "head" }, [
2502
- h("span", { className: "smem-badge", key: "badge", title: "类型 " + String(item.type) }, meta[0] + " " + meta[1]),
2512
+ h("span", { className: "smem-badge", key: "badge", title: t("card.typeTitle", { type: String(item.type) }) }, meta[0] + " " + typeLabel(TYPE_META[item.type] ? item.type : "reference")),
2503
2513
  h("span", { className: "smem-card-title", key: "title" }, item.file),
2504
2514
  h("span", { className: "smem-muted", key: "size" }, fmtSize(item.size)),
2505
2515
  ]),
@@ -2514,37 +2524,36 @@ const StarmapRuntime = (function (require, _react) {
2514
2524
  "data-clamp": isOpen ? "0" : "1",
2515
2525
  key: "body",
2516
2526
  onClick: function () { props.onToggle(item.file); },
2517
- title: isOpen ? "点击收起" : "点击展开全文",
2527
+ title: isOpen ? t("card.collapse") : t("card.expand"),
2518
2528
  }, String(item.description))
2519
2529
  : null,
2520
2530
  h("div", { className: "smem-card-foot", key: "foot" },
2521
2531
  props.archiving
2522
2532
  ? h("div", { className: "smem-ar-confirm", key: "ar" }, [
2523
- h("div", { key: "what" }, "把「" + item.file + "」收进档案馆:它不再参与检索与星图,文件移进 archive/,随时可以恢复。"),
2533
+ h("div", { key: "what" }, t("card.archiveWhat", { file: item.file })),
2524
2534
  // 「可留空」必须跟输入框贴在一起、并且独立成行 —— 它是这一块最容易
2525
2535
  // 被误读成「必须填才能按」的地方。
2526
- h("label", { className: "smem-label", key: "lb" }, "归档理由(可留空)"),
2536
+ h("label", { className: "smem-label", key: "lb" }, t("card.archiveReason")),
2527
2537
  h("input", {
2528
2538
  className: "smem-input", key: "in",
2529
2539
  value: props.archiveReason || "",
2530
- placeholder: "例如:结论已被 project_beta 取代",
2540
+ placeholder: t("card.archiveReasonPlaceholder"),
2531
2541
  onChange: function (e) { props.onArchiveReason(e.target.value); },
2532
2542
  }),
2533
- h("div", { className: "smem-ar-hint", key: "hint" },
2534
- "写下当时的判断,以后翻到它时就知道这条还值不值得捞回来。"),
2543
+ h("div", { className: "smem-ar-hint", key: "hint" }, t("card.archiveHint")),
2535
2544
  h("div", { className: "smem-row", key: "btns" }, [
2536
- h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "ok", disabled: props.busy, onClick: function () { props.onConfirmArchive(item.file); } }, "确认归档"),
2537
- h("button", { type: "button", className: "smem-btn", key: "no", disabled: props.busy, onClick: props.onCancelArchive }, "取消"),
2545
+ h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "ok", disabled: props.busy, onClick: function () { props.onConfirmArchive(item.file); } }, t("card.confirmArchive")),
2546
+ h("button", { type: "button", className: "smem-btn", key: "no", disabled: props.busy, onClick: props.onCancelArchive }, t("card.cancel")),
2538
2547
  ]),
2539
2548
  ])
2540
2549
  : h("div", { className: "smem-actions", key: "acts" },
2541
2550
  props.deleting
2542
2551
  ? [
2543
- h("button", { type: "button", className: "smem-btn smem-btn-danger", key: "yes", onClick: function () { props.onConfirmDelete(item.file); } }, "确认删除"),
2544
- h("button", { type: "button", className: "smem-btn", key: "no", onClick: function () { props.onCancelDelete(); } }, "取消"),
2552
+ h("button", { type: "button", className: "smem-btn smem-btn-danger", key: "yes", onClick: function () { props.onConfirmDelete(item.file); } }, t("card.confirmDelete")),
2553
+ h("button", { type: "button", className: "smem-btn", key: "no", onClick: function () { props.onCancelDelete(); } }, t("card.cancel")),
2545
2554
  ]
2546
2555
  : [
2547
- h("button", { type: "button", className: "smem-btn", key: "edit", onClick: function () { props.onEdit(item.file); } }, "编辑"),
2556
+ h("button", { type: "button", className: "smem-btn", key: "edit", onClick: function () { props.onEdit(item.file); } }, t("card.edit")),
2548
2557
  // 语义分组:左边只放「编辑」(安全、可逆),右边放「锁定 / 归档 / 删除」(对这条记忆的退役/保护决定)。
2549
2558
  h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
2550
2559
  /**
@@ -2560,21 +2569,21 @@ const StarmapRuntime = (function (require, _react) {
2560
2569
  className: "smem-btn" + (item.locked ? " smem-btn-locked" : ""),
2561
2570
  key: "lock",
2562
2571
  title: item.locked
2563
- ? "已锁定:自动归档不会选它,手动归档也会被拒绝。点一下解锁"
2564
- : "锁定这条记忆:自动与手动归档都不再动它(重要的人设 / 偏好建议锁上)",
2572
+ ? t("card.lockedTitle")
2573
+ : t("card.unlockedTitle"),
2565
2574
  disabled: props.busy,
2566
2575
  onClick: function () { props.onToggleLock(item.file, !item.locked); },
2567
- }, item.locked ? "🔒 已锁定" : "🔓 锁定"),
2576
+ }, item.locked ? t("card.locked") : t("card.lock")),
2568
2577
  // 归档排在「锁定」和「删除」之间:比编辑轻、比删除温和,且随时可逆。
2569
2578
  h("button", {
2570
2579
  type: "button", className: "smem-btn", key: "arch",
2571
2580
  title: item.locked
2572
- ? "这条已锁定,归档会被拒绝(先解锁)"
2573
- : "收进档案馆:不再参与检索与星图,随时可以恢复",
2581
+ ? t("card.archiveLockedTitle")
2582
+ : t("card.archiveTitle"),
2574
2583
  disabled: props.busy || item.locked === true,
2575
2584
  onClick: function () { props.onAskArchive(item.file); },
2576
- }, "归档"),
2577
- h("button", { type: "button", className: "smem-btn smem-btn-danger-ghost", key: "del", onClick: function () { props.onAskDelete(item.file); } }, "删除"),
2585
+ }, t("card.archive")),
2586
+ h("button", { type: "button", className: "smem-btn smem-btn-danger-ghost", key: "del", onClick: function () { props.onAskDelete(item.file); } }, t("card.delete")),
2578
2587
  ])),
2579
2588
  ]);
2580
2589
  }
@@ -2602,9 +2611,9 @@ const StarmapRuntime = (function (require, _react) {
2602
2611
  const descOpen = sDescOpen[0], setDescOpen = sDescOpen[1];
2603
2612
  return h("div", { className: "smem-card smem-card--archived", key: item.file }, [
2604
2613
  h("div", { className: "smem-card-head", key: "head" }, [
2605
- h("span", { className: "smem-badge", key: "badge", title: "类型 " + String(item.type) }, meta[0] + " " + meta[1]),
2614
+ h("span", { className: "smem-badge", key: "badge", title: t("card.typeTitle", { type: String(item.type) }) }, meta[0] + " " + typeLabel(TYPE_META[item.type] ? item.type : "reference")),
2606
2615
  h("span", { className: "smem-card-title", key: "title" }, item.file),
2607
- h("span", { className: "smem-ar-meta", key: "at", title: "归档时间" }, "已归档 " + (item.archivedAt || "(时间未知)")),
2616
+ h("span", { className: "smem-ar-meta", key: "at", title: t("card.archivedAtTitle") }, t("card.archivedAt", { at: item.archivedAt || t("card.unknownTime") })),
2608
2617
  h("span", { className: "smem-muted", key: "size" }, fmtSize(item.size)),
2609
2618
  ]),
2610
2619
  (item.tags && item.tags.length)
@@ -2618,21 +2627,20 @@ const StarmapRuntime = (function (require, _react) {
2618
2627
  ? h("div", {
2619
2628
  className: "smem-narrative", key: "desc",
2620
2629
  "data-clamp": descOpen ? "0" : "1",
2621
- title: descOpen ? "点击收起" : "点击展开全文",
2630
+ title: descOpen ? t("card.collapse") : t("card.expand"),
2622
2631
  onClick: function () { setDescOpen(!descOpen); },
2623
2632
  }, String(item.description))
2624
2633
  : null,
2625
2634
  h("div", {
2626
2635
  className: "smem-ar-reason smem-clamp-2" + (descOpen ? " smem-clamp-2--open" : ""),
2627
- key: "why", title: "归档理由:" + (item.archivedReason || "(没写)"),
2636
+ key: "why", title: t("card.reason", { reason: item.archivedReason || t("card.noReason") }),
2628
2637
  onClick: function () { setDescOpen(!descOpen); },
2629
- }, "归档理由:" + (item.archivedReason || "(没写)")),
2638
+ }, t("card.reason", { reason: item.archivedReason || t("card.noReason") })),
2630
2639
  props.editing
2631
2640
  ? h("div", { className: "smem-ar-edit", key: "edit" }, [
2632
- h("div", { className: "smem-ar-hint", key: "note" },
2633
- "原样文本编辑:下面就是这份文件的全文(含 frontmatter 与归档留痕),保存时原样写回,不会被重新生成。"),
2641
+ h("div", { className: "smem-ar-hint", key: "note" }, t("card.rawEditNote")),
2634
2642
  props.loading
2635
- ? h("div", { className: "smem-status", key: "loading" }, "读取中…")
2643
+ ? h("div", { className: "smem-status", key: "loading" }, t("card.reading"))
2636
2644
  : h("textarea", {
2637
2645
  className: "smem-textarea smem-ar-editor", key: "ta",
2638
2646
  value: props.text || "",
@@ -2640,20 +2648,17 @@ const StarmapRuntime = (function (require, _react) {
2640
2648
  onChange: function (e) { props.onText(e.target.value); },
2641
2649
  }),
2642
2650
  h("div", { className: "smem-row", key: "btns" }, [
2643
- h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "save", disabled: props.busy || props.loading, onClick: function () { props.onSave(item.file); } }, props.saving ? "保存中…" : "保存"),
2644
- h("button", { type: "button", className: "smem-btn", key: "cancel", disabled: props.busy, onClick: props.onCancel }, "取消"),
2651
+ h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "save", disabled: props.busy || props.loading, onClick: function () { props.onSave(item.file); } }, props.saving ? t("card.saving") : t("card.save")),
2652
+ h("button", { type: "button", className: "smem-btn", key: "cancel", disabled: props.busy, onClick: props.onCancel }, t("card.cancel")),
2645
2653
  ]),
2646
2654
  ])
2647
2655
  : props.deleting
2648
2656
  ? h("div", { className: "smem-ar-confirm", key: "del" }, [
2649
- h("div", { key: "what" },
2650
- "彻底删除「" + item.file + "」:它会从 archive/ 里被真的删掉,不能恢复。" +
2651
- "要保留它就先点「恢复」,把它放回活动记忆。"),
2652
- h("div", { className: "smem-ar-hint", key: "hint" },
2653
- "这一步之后没有任何界面能把内容找回来 —— 只剩你自己在磁盘上留过备份。"),
2657
+ h("div", { key: "what" }, t("card.purgeWhat", { file: item.file })),
2658
+ h("div", { className: "smem-ar-hint", key: "hint" }, t("card.purgeHint")),
2654
2659
  h("div", { className: "smem-row", key: "btns" }, [
2655
- h("button", { type: "button", className: "smem-btn smem-btn-danger", key: "yes", disabled: props.busy, onClick: function () { props.onConfirmDelete(item.file); } }, "确认彻底删除"),
2656
- h("button", { type: "button", className: "smem-btn", key: "no", disabled: props.busy, onClick: props.onCancelDelete }, "取消"),
2660
+ h("button", { type: "button", className: "smem-btn smem-btn-danger", key: "yes", disabled: props.busy, onClick: function () { props.onConfirmDelete(item.file); } }, t("card.confirmPurge")),
2661
+ h("button", { type: "button", className: "smem-btn", key: "no", disabled: props.busy, onClick: props.onCancelDelete }, t("card.cancel")),
2657
2662
  ]),
2658
2663
  ])
2659
2664
  : h("div", { className: "smem-card-foot", key: "foot" },
@@ -2662,21 +2667,21 @@ const StarmapRuntime = (function (require, _react) {
2662
2667
  type: "button", className: "smem-btn", key: "edit",
2663
2668
  disabled: props.busy,
2664
2669
  onClick: function () { props.onEdit(item.file); },
2665
- }, "编辑"),
2670
+ }, t("card.edit")),
2666
2671
  h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
2667
2672
  h("button", {
2668
2673
  type: "button", className: "smem-btn smem-btn-primary", key: "restore",
2669
2674
  disabled: props.busy,
2670
2675
  onClick: function () { props.onRestore(item.file); },
2671
- }, props.restoring ? "恢复中…" : "恢复"),
2676
+ }, props.restoring ? t("card.restoring") : t("card.restore")),
2672
2677
  // 放在最后、且用 danger-ghost:它是这张卡上唯一不可逆的动作,
2673
2678
  // 不该跟「恢复」长得一样。
2674
2679
  h("button", {
2675
2680
  type: "button", className: "smem-btn smem-btn-danger-ghost", key: "purge",
2676
2681
  disabled: props.busy,
2677
- title: "从 archive/ 里彻底删掉,不可恢复(要留就先恢复)",
2682
+ title: t("card.purgeTitle"),
2678
2683
  onClick: function () { props.onAskDelete(item.file); },
2679
- }, "彻底删除"),
2684
+ }, t("card.purge")),
2680
2685
  ])),
2681
2686
  ]);
2682
2687
  }
@@ -2693,9 +2698,6 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
2693
2698
  };
2694
2699
  const autoArchiveOf = (v) => (v === "off" || v === "auto" ? v : "report");
2695
2700
 
2696
- /** 头部带上的「我在哪」:五个 tab 的中文名(与 .smem-tabs 上的字面一致)。 */
2697
- const TAB_LABEL = { files: "文件列表", star: "记忆星图", autodream: "自动做梦", archive: "自动归档", audit: "体检" };
2698
-
2699
2701
  /**
2700
2702
  * 工具面板头部的「收起」按钮:抽屉里开的工具要能一键收回去 ——
2701
2703
  * 面板不收,列表就还在下面被顶着。
@@ -2841,14 +2843,14 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
2841
2843
  */
2842
2844
  function StarLine() {
2843
2845
  return h("div", { className: "smem-star" }, [
2844
- h("span", { key: "ask" }, "⭐ 顺手留颗 Star,作者能高兴一整天"),
2846
+ h("span", { key: "ask" }, t("star.ask")),
2845
2847
  h("a", {
2846
2848
  key: "cta",
2847
2849
  href: "https://github.com/gezi-wen/sage-mem",
2848
2850
  target: "_blank",
2849
2851
  rel: "noreferrer",
2850
2852
  className: "smem-star-link",
2851
- }, "行,给你一颗 Star"),
2853
+ }, t("star.cta")),
2852
2854
  ]);
2853
2855
  }
2854
2856
 
@@ -3054,6 +3056,440 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3054
3056
  return { provider: "", model: "" };
3055
3057
  }
3056
3058
 
3059
+ // ── i18n pilot: settings.memory ─────────────────────────────────────
3060
+ // The `zh` values below are copied verbatim from the strings this shard
3061
+ // replaces; they must render byte-identically to the pre-i18n client.
3062
+ // The `en` draft is a complete key-for-key translation. Placeholders use
3063
+ // the host locale service's own syntax (`{name}`), so a bound `t` and the
3064
+ // local fallback below substitute the same way.
3065
+ const NS = "settings.memory";
3066
+
3067
+ const ZH = {
3068
+ "section.label": "记忆管理",
3069
+ "page.back": "返回会话",
3070
+ "sidebar.label": "记忆管理",
3071
+
3072
+ "tab.files": "文件列表",
3073
+ "tab.star": "记忆星图",
3074
+ "tab.autodream": "自动做梦",
3075
+ "tab.archive": "自动归档",
3076
+ "tab.audit": "体检",
3077
+
3078
+ "type.user": "用户",
3079
+ "type.feedback": "反馈",
3080
+ "type.project": "项目",
3081
+ "type.reference": "参考",
3082
+ "type.special": "特殊",
3083
+
3084
+ "header.dirReadable": "memory 目录可读",
3085
+ "header.dirUnreadable": "memory 目录不可读",
3086
+ "header.dirTitle": "{state} · {tab}",
3087
+ "header.counts": "活动中 {active} · 已归档 {archived}",
3088
+ "header.countsFiltered": "筛出 {shown} / {total} · 活动中 {active} · 已归档 {archived}",
3089
+ "header.auditSwitch": "切到体检看是哪几条",
3090
+ "header.auditClean": "体检没有发现问题",
3091
+ "header.auditRun": "跑一次体检(只读)",
3092
+ "header.auditProblems": "体检 {count} 个问题",
3093
+ "header.auditPass": "体检 通过",
3094
+ "header.auditNotRun": "体检 · 未跑",
3095
+ "header.restartTitle": "有改动要重启 DSH 才生效",
3096
+ "header.restartPending": "待重启生效",
3097
+ "header.restartPendingCount": "{count} 项待重启生效",
3098
+ "header.openFolderTitle": "在系统文件管理器里打开记忆目录",
3099
+ "header.openFolder": "打开记忆文件夹",
3100
+ "header.refresh": "刷新",
3101
+ "action.add": "+ 添加记忆",
3102
+
3103
+ "form.addTitle": "添加记忆",
3104
+ "form.editTitle": "编辑 {file}",
3105
+ "form.type": "类型:",
3106
+ "form.fileName": "文件名(.md,留空自动 {type}_note.md)",
3107
+ "form.fileNamePlaceholder": "例如 project_example.md",
3108
+ "form.desc": "描述(写进 frontmatter,检索与列表都靠它)",
3109
+ "form.descPlaceholder": "一句话说清这条记忆是什么",
3110
+ "form.tags": "自定义标签(可选,逗号分隔;会写进 frontmatter 的 tags,列表页可按它筛)",
3111
+ "form.tagsPlaceholder": "例如 部署, 笔记, dsh",
3112
+ "form.body": "正文(可选,frontmatter 自动生成)",
3113
+ "form.bodyPlaceholder": "要长期记住的细节",
3114
+
3115
+ "update.found": "发现新版本 v{latest}(当前 v{current})",
3116
+ "update.notes": "看更新内容",
3117
+ "update.dismissTitle": "这次不看了(下次进面板还会提示)",
3118
+
3119
+ "toolbar.search": "搜索文件名 / 描述 / 类型 / 标签…",
3120
+ "tools.button": "工具 ▾",
3121
+ "tools.title": "归档候选 / 保留名编辑",
3122
+ "tools.titleDebug": "归档候选 / 保留名编辑 / 注入调试",
3123
+ "tools.candidates": "归档候选",
3124
+ "tools.candidatesCount": "{count} 条",
3125
+ "tools.candidatesEmpty": "未取",
3126
+ "tools.raw": "保留名编辑",
3127
+ "tools.rawCount": "{count} 个",
3128
+ "tools.debug": "注入调试",
3129
+ "tools.debugCount": "{count} 次",
3130
+
3131
+ "filter.sumStatus": "状态:{value}",
3132
+ "filter.sumType": "类型:{value}",
3133
+ "filter.sumTag": "标签:#{value}",
3134
+ "filter.sumQuery": "搜索:{value}",
3135
+ "filter.clearAll": "清除全部",
3136
+ "filter.statusLabel": "状态",
3137
+ "filter.all": "全部",
3138
+ "filter.allTitle": "活动与已归档一起看(归档条走灰色)",
3139
+ "filter.active": "活动中",
3140
+ "filter.activeTitle": "正在参与检索与星图的记忆",
3141
+ "filter.archived": "已归档",
3142
+ "filter.archivedTitle": "已收进档案馆:不参与检索与星图,可编辑 / 恢复",
3143
+ "filter.typeLabel": "类型",
3144
+ "filter.tags": "标签",
3145
+ "filter.collapseTags": "收起标签",
3146
+ "filter.moreTagsTitle": "还有一些标签没显示",
3147
+ "filter.moreTags": "更多标签 ({count})",
3148
+ "filter.clear": "清除筛选",
3149
+ "filter.remove": "撤掉这项筛选",
3150
+
3151
+ "files.emptyTitle": "还没有记忆文件。对话中值得记住的事会写到这里,也可以手动写入。",
3152
+ "files.skeletonTitle": "写入一份 MEMORY.md 索引骨架(已有索引不会覆盖)",
3153
+ "files.skeleton": "建立索引骨架",
3154
+ "files.archiveEmpty": "档案馆是空的 —— 在活动记忆那张卡上点「归档」,它就会收到这里。",
3155
+ "files.backToFiles": "回文件列表",
3156
+ "files.activeEmpty": "活动记忆是空的 —— 状态筛到「已归档」还能捞出 {count} 条。",
3157
+ "files.goArchived": "切到已归档",
3158
+ "files.noMatch": "没有符合搜索或筛选条件的文件。",
3159
+ "files.archiveIntro": "档案馆里的记忆不参与检索与星图,原文仍在记忆目录的 archive/ 下 —— 恢复后它就回到活动列表;「编辑」是原样文本编辑,改完存回同一份文件;「彻底删除」会真的从 archive/ 里删掉它(不可恢复,删除前还要再确认一次)。",
3160
+
3161
+ "card.typeTitle": "类型 {type}",
3162
+ "card.collapse": "点击收起",
3163
+ "card.expand": "点击展开全文",
3164
+ "card.archiveWhat": "把「{file}」收进档案馆:它不再参与检索与星图,文件移进 archive/,随时可以恢复。",
3165
+ "card.archiveReason": "归档理由(可留空)",
3166
+ "card.archiveReasonPlaceholder": "例如:结论已被 project_beta 取代",
3167
+ "card.archiveHint": "写下当时的判断,以后翻到它时就知道这条还值不值得捞回来。",
3168
+ "card.confirmArchive": "确认归档",
3169
+ "card.cancel": "取消",
3170
+ "card.confirmDelete": "确认删除",
3171
+ "card.edit": "编辑",
3172
+ "card.lockedTitle": "已锁定:自动归档不会选它,手动归档也会被拒绝。点一下解锁",
3173
+ "card.unlockedTitle": "锁定这条记忆:自动与手动归档都不再动它(重要的人设 / 偏好建议锁上)",
3174
+ "card.locked": "🔒 已锁定",
3175
+ "card.lock": "🔓 锁定",
3176
+ "card.archiveLockedTitle": "这条已锁定,归档会被拒绝(先解锁)",
3177
+ "card.archiveTitle": "收进档案馆:不再参与检索与星图,随时可以恢复",
3178
+ "card.archive": "归档",
3179
+ "card.delete": "删除",
3180
+ "card.archivedAtTitle": "归档时间",
3181
+ "card.archivedAt": "已归档 {at}",
3182
+ "card.unknownTime": "(时间未知)",
3183
+ "card.reason": "归档理由:{reason}",
3184
+ "card.noReason": "(没写)",
3185
+ "card.rawEditNote": "原样文本编辑:下面就是这份文件的全文(含 frontmatter 与归档留痕),保存时原样写回,不会被重新生成。",
3186
+ "card.reading": "读取中…",
3187
+ "card.saving": "保存中…",
3188
+ "card.save": "保存",
3189
+ "card.purgeWhat": "彻底删除「{file}」:它会从 archive/ 里被真的删掉,不能恢复。要保留它就先点「恢复」,把它放回活动记忆。",
3190
+ "card.purgeHint": "这一步之后没有任何界面能把内容找回来 —— 只剩你自己在磁盘上留过备份。",
3191
+ "card.confirmPurge": "确认彻底删除",
3192
+ "card.restoring": "恢复中…",
3193
+ "card.restore": "恢复",
3194
+ "card.purgeTitle": "从 archive/ 里彻底删掉,不可恢复(要留就先恢复)",
3195
+ "card.purge": "彻底删除",
3196
+
3197
+ "star.ask": "⭐ 顺手留颗 Star,作者能高兴一整天",
3198
+ "star.cta": "行,给你一颗 Star",
3199
+
3200
+ "pluginCard.saveFailed": "保存失败(未知原因)",
3201
+ "pluginCard.noSetSettings": "宿主侧还没有 setSettings 接口(插件版本较旧)",
3202
+ "pluginCard.title": "侧栏入口",
3203
+ "pluginCard.on": "已开启",
3204
+ "pluginCard.off": "已关闭",
3205
+ "pluginCard.label": "在侧栏显示「记忆管理」入口",
3206
+ "pluginCard.note": "默认开启;改动立即生效,不需要重启。关闭后「设置 → 记忆管理」入口仍然保留。",
3207
+
3208
+ "error.remote": "remote 调用失败",
3209
+ "error.listFiles": "listFiles RPC 失败: {message}",
3210
+ "error.noRaw": "宿主侧还没有 readRaw / writeRaw 接口(插件版本较旧)",
3211
+ "error.noOpenDir": "宿主侧还没有 openMemoryDir 接口(插件版本较旧)",
3212
+ "error.noArchive": "宿主侧还没有 archive 接口(插件版本较旧)",
3213
+ "error.noLock": "宿主侧还没有 setLocked 接口(插件版本较旧)",
3214
+ "error.noRestore": "宿主侧还没有 restore 接口(插件版本较旧)",
3215
+ "error.noDeleteArchived": "宿主侧还没有 deleteArchived 接口(插件版本较旧)",
3216
+ "error.noReadArchived": "宿主侧还没有 readArchived 接口(插件版本较旧)",
3217
+ "error.noWriteArchived": "宿主侧还没有 writeArchived 接口(插件版本较旧)",
3218
+ "error.noAudit": "宿主侧还没有 audit 接口(插件版本较旧)",
3219
+ "error.noArchiveCandidates": "宿主侧还没有 archiveCandidates 接口(插件版本较旧)",
3220
+ "error.noGetSettings": "宿主侧还没有 getSettings 接口(插件版本较旧)",
3221
+ "error.noReadRaw": "宿主侧还没有 readRaw 接口(插件版本较旧)",
3222
+ "error.noWriteRaw": "宿主侧还没有 writeRaw 接口(插件版本较旧)",
3223
+ "error.readFailed": "读不到 {file}:{message}",
3224
+ "error.descEmpty": "描述不能为空",
3225
+ "error.writeRejected": "写入被拒绝",
3226
+ "error.deleteRejected": "删除被拒绝",
3227
+ "error.archiveRejected": "归档被拒绝",
3228
+ "error.lockRejected": "锁定操作被拒绝",
3229
+ "error.restoreRejected": "恢复被拒绝",
3230
+ "error.saveRejected": "保存被拒绝",
3231
+ "error.skeletonFailed": "建立索引骨架失败",
3232
+ "error.openDirFailed": "打开记忆目录失败",
3233
+ "notice.indexExists": "MEMORY.md 已经存在,没有改动它。",
3234
+ "notice.indexCreated": "已建立 MEMORY.md 索引骨架。",
3235
+ "notice.updated": "已更新 {file}",
3236
+ "notice.saved": "已保存 {file}",
3237
+ "notice.rawNew": "已新建 {file}",
3238
+ "notice.deleted": "已删除 {file}",
3239
+ "notice.archived": "已归档 {file} —— 它现在在「已归档」视图里,随时可以恢复",
3240
+ "notice.batchPartial": "已归档 {done} 条,{failed} 条没成功:{files}",
3241
+ "notice.batchDone": "已归档 {done} 条 —— 它们现在在档案馆里,随时可以恢复",
3242
+ "notice.locked": "已锁定 {file} —— 自动归档不会再选它,手动归档也会被拒绝",
3243
+ "notice.unlocked": "已解锁 {file}",
3244
+ "notice.restored": "已恢复 {file} —— 它回到了活动记忆里",
3245
+ "notice.restoredIndexed": "已恢复 {file} —— 它回到了活动记忆里,并补进了索引",
3246
+ "notice.purged": "已彻底删除 {file} —— 归档区里也没有它了,找不回来",
3247
+ };
3248
+
3249
+ const EN = {
3250
+ "section.label": "Memory management",
3251
+ "page.back": "Back to conversation",
3252
+ "sidebar.label": "Memory management",
3253
+
3254
+ "tab.files": "Files",
3255
+ "tab.star": "Star map",
3256
+ "tab.autodream": "Autodream",
3257
+ "tab.archive": "Auto-archive",
3258
+ "tab.audit": "Audit",
3259
+
3260
+ "type.user": "User",
3261
+ "type.feedback": "Feedback",
3262
+ "type.project": "Project",
3263
+ "type.reference": "Reference",
3264
+ "type.special": "Special",
3265
+
3266
+ "header.dirReadable": "memory directory readable",
3267
+ "header.dirUnreadable": "memory directory unreadable",
3268
+ "header.dirTitle": "{state} · {tab}",
3269
+ "header.counts": "Active {active} · Archived {archived}",
3270
+ "header.countsFiltered": "Showing {shown} / {total} · Active {active} · Archived {archived}",
3271
+ "header.auditSwitch": "Switch to Audit to see which entries",
3272
+ "header.auditClean": "Audit found no problems",
3273
+ "header.auditRun": "Run an audit (read-only)",
3274
+ "header.auditProblems": "Audit: {count} problems",
3275
+ "header.auditPass": "Audit passed",
3276
+ "header.auditNotRun": "Audit · not run",
3277
+ "header.restartTitle": "Changes take effect after restarting DSH",
3278
+ "header.restartPending": "restart to apply",
3279
+ "header.restartPendingCount": "{count} items pending restart",
3280
+ "header.openFolderTitle": "Open the memory directory in the system file manager",
3281
+ "header.openFolder": "Open memory folder",
3282
+ "header.refresh": "Refresh",
3283
+ "action.add": "+ Add memory",
3284
+
3285
+ "form.addTitle": "Add memory",
3286
+ "form.editTitle": "Edit {file}",
3287
+ "form.type": "Type:",
3288
+ "form.fileName": "File name (.md; leave blank to use {type}_note.md automatically)",
3289
+ "form.fileNamePlaceholder": "e.g. project_example.md",
3290
+ "form.desc": "Description (written to frontmatter; retrieval and the list both rely on it)",
3291
+ "form.descPlaceholder": "One sentence saying what this memory is",
3292
+ "form.tags": "Custom tags (optional, comma-separated; written to frontmatter tags, filterable in the list)",
3293
+ "form.tagsPlaceholder": "e.g. deploy, notes, dsh",
3294
+ "form.body": "Body (optional; frontmatter is generated automatically)",
3295
+ "form.bodyPlaceholder": "Details worth remembering long-term",
3296
+
3297
+ "update.found": "New version v{latest} available (current v{current})",
3298
+ "update.notes": "See what's new",
3299
+ "update.dismissTitle": "Skip this time (it will show again next time)",
3300
+
3301
+ "toolbar.search": "Search file name / description / type / tag…",
3302
+ "tools.button": "Tools ▾",
3303
+ "tools.title": "Archive candidates / Reserved-name editor",
3304
+ "tools.titleDebug": "Archive candidates / Reserved-name editor / Injection debug",
3305
+ "tools.candidates": "Archive candidates",
3306
+ "tools.candidatesCount": "{count}",
3307
+ "tools.candidatesEmpty": "not loaded",
3308
+ "tools.raw": "Reserved-name editor",
3309
+ "tools.rawCount": "{count}",
3310
+ "tools.debug": "Injection debug",
3311
+ "tools.debugCount": "{count}",
3312
+
3313
+ "filter.sumStatus": "Status: {value}",
3314
+ "filter.sumType": "Type: {value}",
3315
+ "filter.sumTag": "Tag: #{value}",
3316
+ "filter.sumQuery": "Search: {value}",
3317
+ "filter.clearAll": "Clear all",
3318
+ "filter.statusLabel": "Status",
3319
+ "filter.all": "All",
3320
+ "filter.allTitle": "See active and archived together (archived rows are grey)",
3321
+ "filter.active": "Active",
3322
+ "filter.activeTitle": "Memories currently participating in retrieval and the star map",
3323
+ "filter.archived": "Archived",
3324
+ "filter.archivedTitle": "Moved to the archive: out of retrieval and the star map; can be edited / restored",
3325
+ "filter.typeLabel": "Type",
3326
+ "filter.tags": "Tags",
3327
+ "filter.collapseTags": "Collapse tags",
3328
+ "filter.moreTagsTitle": "Some tags are hidden",
3329
+ "filter.moreTags": "More tags ({count})",
3330
+ "filter.clear": "Clear filters",
3331
+ "filter.remove": "Remove this filter",
3332
+
3333
+ "files.emptyTitle": "No memory files yet. Things worth remembering from a conversation are written here, and you can add them by hand.",
3334
+ "files.skeletonTitle": "Write a MEMORY.md index skeleton (an existing index is never overwritten)",
3335
+ "files.skeleton": "Create index skeleton",
3336
+ "files.archiveEmpty": "The archive is empty — press Archive on an active memory card and it will land here.",
3337
+ "files.backToFiles": "Back to files",
3338
+ "files.activeEmpty": "There are no active memories — switching the status filter to Archived still finds {count}.",
3339
+ "files.goArchived": "Switch to archived",
3340
+ "files.noMatch": "No files match the search or filters.",
3341
+ "files.archiveIntro": "Archived memories stay out of retrieval and the star map; the original text remains in archive/ under the memory directory — restoring returns it to the active list. Edit opens the raw text and saves back to the same file; Delete permanently really removes it from archive/ (irreversible, and asks once more before deleting).",
3342
+
3343
+ "card.typeTitle": "Type {type}",
3344
+ "card.collapse": "Click to collapse",
3345
+ "card.expand": "Click to expand",
3346
+ "card.archiveWhat": "Move “{file}” into the archive: it stops participating in retrieval and the star map, moves under archive/, and can be restored at any time.",
3347
+ "card.archiveReason": "Archive reason (optional)",
3348
+ "card.archiveReasonPlaceholder": "e.g. superseded by project_beta",
3349
+ "card.archiveHint": "Write down the judgment you are making now, so a future reader knows whether this is still worth recovering.",
3350
+ "card.confirmArchive": "Confirm archive",
3351
+ "card.cancel": "Cancel",
3352
+ "card.confirmDelete": "Confirm delete",
3353
+ "card.edit": "Edit",
3354
+ "card.lockedTitle": "Locked: auto-archive never selects it and a manual archive is refused. Click to unlock",
3355
+ "card.unlockedTitle": "Lock this memory: auto and manual archiving both leave it alone (lock important personas / preferences)",
3356
+ "card.locked": "🔒 Locked",
3357
+ "card.lock": "🔓 Lock",
3358
+ "card.archiveLockedTitle": "This memory is locked, so archiving will be refused (unlock first)",
3359
+ "card.archiveTitle": "Move into the archive: out of retrieval and the star map, restorable at any time",
3360
+ "card.archive": "Archive",
3361
+ "card.delete": "Delete",
3362
+ "card.archivedAtTitle": "Archived at",
3363
+ "card.archivedAt": "Archived {at}",
3364
+ "card.unknownTime": "(time unknown)",
3365
+ "card.reason": "Archive reason: {reason}",
3366
+ "card.noReason": "(not written)",
3367
+ "card.rawEditNote": "Raw text editing: what follows is the whole file (including frontmatter and archive traces). Saving writes it back unchanged, without regenerating anything.",
3368
+ "card.reading": "Reading…",
3369
+ "card.saving": "Saving…",
3370
+ "card.save": "Save",
3371
+ "card.purgeWhat": "Permanently delete “{file}”: it is really removed from archive/ and cannot be recovered. To keep it, press Restore first and put it back among the active memories.",
3372
+ "card.purgeHint": "After this step no screen can bring the content back — only a backup you kept on disk can.",
3373
+ "card.confirmPurge": "Confirm permanent delete",
3374
+ "card.restoring": "Restoring…",
3375
+ "card.restore": "Restore",
3376
+ "card.purgeTitle": "Remove permanently from archive/, irreversible (restore first to keep it)",
3377
+ "card.purge": "Delete permanently",
3378
+
3379
+ "star.ask": "⭐ Leave a star — it makes the author's day",
3380
+ "star.cta": "Sure, here's a star",
3381
+
3382
+ "pluginCard.saveFailed": "Save failed (unknown reason)",
3383
+ "pluginCard.noSetSettings": "The host has no setSettings API yet (older plugin version)",
3384
+ "pluginCard.title": "Sidebar entry",
3385
+ "pluginCard.on": "On",
3386
+ "pluginCard.off": "Off",
3387
+ "pluginCard.label": "Show the “Memory management” entry in the sidebar",
3388
+ "pluginCard.note": "On by default; changes take effect immediately, no restart needed. Turning it off keeps the Settings → Memory management entry.",
3389
+
3390
+ "error.remote": "remote call failed",
3391
+ "error.listFiles": "listFiles RPC failed: {message}",
3392
+ "error.noRaw": "The host has no readRaw / writeRaw API yet (older plugin version)",
3393
+ "error.noOpenDir": "The host has no openMemoryDir API yet (older plugin version)",
3394
+ "error.noArchive": "The host has no archive API yet (older plugin version)",
3395
+ "error.noLock": "The host has no setLocked API yet (older plugin version)",
3396
+ "error.noRestore": "The host has no restore API yet (older plugin version)",
3397
+ "error.noDeleteArchived": "The host has no deleteArchived API yet (older plugin version)",
3398
+ "error.noReadArchived": "The host has no readArchived API yet (older plugin version)",
3399
+ "error.noWriteArchived": "The host has no writeArchived API yet (older plugin version)",
3400
+ "error.noAudit": "The host has no audit API yet (older plugin version)",
3401
+ "error.noArchiveCandidates": "The host has no archiveCandidates API yet (older plugin version)",
3402
+ "error.noGetSettings": "The host has no getSettings API yet (older plugin version)",
3403
+ "error.noReadRaw": "The host has no readRaw API yet (older plugin version)",
3404
+ "error.noWriteRaw": "The host has no writeRaw API yet (older plugin version)",
3405
+ "error.readFailed": "Cannot read {file}: {message}",
3406
+ "error.descEmpty": "Description cannot be empty",
3407
+ "error.writeRejected": "Write rejected",
3408
+ "error.deleteRejected": "Delete rejected",
3409
+ "error.archiveRejected": "Archive rejected",
3410
+ "error.lockRejected": "Lock operation rejected",
3411
+ "error.restoreRejected": "Restore rejected",
3412
+ "error.saveRejected": "Save rejected",
3413
+ "error.skeletonFailed": "Failed to create the index skeleton",
3414
+ "error.openDirFailed": "Failed to open the memory directory",
3415
+ "notice.indexExists": "MEMORY.md already exists; it was left unchanged.",
3416
+ "notice.indexCreated": "MEMORY.md index skeleton created.",
3417
+ "notice.updated": "Updated {file}",
3418
+ "notice.saved": "Saved {file}",
3419
+ "notice.rawNew": "Created {file}",
3420
+ "notice.deleted": "Deleted {file}",
3421
+ "notice.archived": "Archived {file} — it is now under the Archived view and can be restored at any time",
3422
+ "notice.batchPartial": "Archived {done}, {failed} failed: {files}",
3423
+ "notice.batchDone": "Archived {done} — they are now in the archive and can be restored at any time",
3424
+ "notice.locked": "Locked {file} — auto-archive will not select it and a manual archive will be refused",
3425
+ "notice.unlocked": "Unlocked {file}",
3426
+ "notice.restored": "Restored {file} — it is back among the active memories",
3427
+ "notice.restoredIndexed": "Restored {file} — it is back among the active memories and was added to the index",
3428
+ "notice.purged": "Permanently deleted {file} — it is gone from the archive as well and cannot be recovered",
3429
+ };
3430
+
3431
+ /**
3432
+ * Local placeholder fill, used only when the host has no locale service.
3433
+ * Mirrors the locale runtime: `{name}` is replaced, unknown names stay.
3434
+ */
3435
+ function i18nFill(template, params) {
3436
+ if (!params) return template;
3437
+ return String(template).replace(/\{(\w+)\}/g, function (match, name) {
3438
+ return Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match;
3439
+ });
3440
+ }
3441
+
3442
+ /** The zh dictionary is the fallback: a host without locale renders today's UI. */
3443
+ function i18nZh(key, params) {
3444
+ return i18nFill(Object.prototype.hasOwnProperty.call(ZH, key) ? ZH[key] : key, params);
3445
+ }
3446
+
3447
+ // Bound in `apply` when (and only when) the host provides the locale service.
3448
+ let t = i18nZh;
3449
+
3450
+ /** Known type ids get translated; unknown frontmatter values stay visible. */
3451
+ function typeLabel(id) {
3452
+ const key = "type." + String(id);
3453
+ return Object.prototype.hasOwnProperty.call(ZH, key) ? t(key) : String(id);
3454
+ }
3455
+
3456
+ /** Same for the tab names used in the directory tooltip. */
3457
+ function tabLabel(id) {
3458
+ const key = "tab." + String(id);
3459
+ return Object.prototype.hasOwnProperty.call(ZH, key) ? t(key) : "";
3460
+ }
3461
+
3462
+ /**
3463
+ * Register the dictionaries and bind `t`, optionally.
3464
+ *
3465
+ * `locale` is NOT a hard dependency (it is absent in older hosts, the
3466
+ * render harness and the unit tests): without it everything above keeps
3467
+ * rendering from `ZH`. Registration goes through `ctx.effect`, so the
3468
+ * dictionary is withdrawn when this plugin is disposed.
3469
+ */
3470
+ function setupI18n(ctx) {
3471
+ let locale = null;
3472
+ try {
3473
+ if (ctx && typeof ctx.get === "function") locale = ctx.get("locale");
3474
+ if (!locale && ctx && ctx.locale) locale = ctx.locale;
3475
+ } catch (e) {
3476
+ locale = null;
3477
+ }
3478
+ if (!locale || typeof locale.bind !== "function") return false;
3479
+ const install = function () {
3480
+ let dispose = null;
3481
+ if (typeof locale.register === "function") {
3482
+ dispose = locale.register(NS, { zh: ZH, en: EN });
3483
+ } else {
3484
+ return null;
3485
+ }
3486
+ t = locale.bind(NS);
3487
+ return dispose;
3488
+ };
3489
+ if (typeof ctx.effect === "function") ctx.effect(install, "sage-mem: i18n " + NS);
3490
+ else install();
3491
+ return true;
3492
+ }
3057
3493
  /**
3058
3494
  * 「自动做梦」面板 —— 让记忆自己整理一遍(引擎在 宿主侧 lib/autodream.js)。
3059
3495
  *
@@ -3954,24 +4390,24 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3954
4390
  }));
3955
4391
  } else if (totalCount === 0) {
3956
4392
  filesView = h("div", { className: "smem-empty", key: "empty" }, [
3957
- h("div", { key: "t" }, "还没有记忆文件。对话中值得记住的事会写到这里,也可以手动写入。"),
3958
- h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "add", onClick: openAdd }, "+ 添加记忆"),
3959
- h("button", { type: "button", className: "smem-btn", key: "idx", title: "写入一份 MEMORY.md 索引骨架(已有索引不会覆盖)", onClick: createIndexSkeleton }, "建立索引骨架"),
4393
+ h("div", { key: "t" }, t("files.emptyTitle")),
4394
+ h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "add", onClick: openAdd }, t("action.add")),
4395
+ h("button", { type: "button", className: "smem-btn", key: "idx", title: t("files.skeletonTitle"), onClick: createIndexSkeleton }, t("files.skeleton")),
3960
4396
  ]);
3961
4397
  } else if (statusFilter === "archived" && archiveItems.length === 0) {
3962
4398
  filesView = h("div", { className: "smem-empty", key: "empty" }, [
3963
- h("div", { key: "t" }, "档案馆是空的 —— 在活动记忆那张卡上点「归档」,它就会收到这里。"),
3964
- h("button", { type: "button", className: "smem-btn", key: "back", onClick: function () { setStatusFilter("active"); } }, "回文件列表"),
4399
+ h("div", { key: "t" }, t("files.archiveEmpty")),
4400
+ h("button", { type: "button", className: "smem-btn", key: "back", onClick: function () { setStatusFilter("active"); } }, t("files.backToFiles")),
3965
4401
  ]);
3966
4402
  } else if (statusFilter === "active" && items.length === 0) {
3967
4403
  filesView = h("div", { className: "smem-empty", key: "empty" }, [
3968
- h("div", { key: "t" }, "活动记忆是空的 —— 状态筛到「已归档」还能捞出 " + archiveItems.length + " 条。"),
3969
- h("button", { type: "button", className: "smem-btn", key: "go", onClick: function () { setStatusFilter("archived"); } }, "切到已归档"),
4404
+ h("div", { key: "t" }, t("files.activeEmpty", { count: archiveItems.length })),
4405
+ h("button", { type: "button", className: "smem-btn", key: "go", onClick: function () { setStatusFilter("archived"); } }, t("files.goArchived")),
3970
4406
  ]);
3971
4407
  } else if (filtered.length === 0) {
3972
4408
  filesView = h("div", { className: "smem-empty", key: "empty" }, [
3973
- h("div", { key: "t" }, "没有符合搜索或筛选条件的文件。"),
3974
- h("button", { type: "button", className: "smem-btn", key: "clear", onClick: function () { setStatusFilter("all"); setTypeFilter([]); setTagFilter([]); setQuery(""); } }, "清除筛选"),
4409
+ h("div", { key: "t" }, t("files.noMatch")),
4410
+ h("button", { type: "button", className: "smem-btn", key: "clear", onClick: function () { setStatusFilter("all"); setTypeFilter([]); setTagFilter([]); setQuery(""); } }, t("filter.clear")),
3975
4411
  ]);
3976
4412
  } else {
3977
4413
  const list = h("div", { className: "smem-list", key: "list" }, filtered.map(function (it) {
@@ -4023,9 +4459,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4023
4459
  }));
4024
4460
  filesView = statusFilter === "archived"
4025
4461
  ? [
4026
- h("div", { className: "smem-ar-banner", key: "banner" },
4027
- "档案馆里的记忆不参与检索与星图,原文仍在记忆目录的 archive/ 下 —— 恢复后它就回到活动列表;" +
4028
- "「编辑」是原样文本编辑,改完存回同一份文件;「彻底删除」会真的从 archive/ 里删掉它(不可恢复,删除前还要再确认一次)。"),
4462
+ h("div", { className: "smem-ar-banner", key: "banner" }, t("files.archiveIntro")),
4029
4463
  list,
4030
4464
  ]
4031
4465
  : list;
@@ -4044,56 +4478,56 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4044
4478
  hasFilter
4045
4479
  ? h("div", { className: "smem-filter-summary", key: "sum" }, [
4046
4480
  statusFilter !== "all"
4047
- ? sumItem("状态:" + (statusFilter === "active" ? "活动中" : "已归档"), function () { setStatusFilter("all"); })
4481
+ ? sumItem(t("filter.sumStatus", { value: statusFilter === "active" ? t("filter.active") : t("filter.archived") }), function () { setStatusFilter("all"); })
4048
4482
  : null,
4049
- typeFilter.map(function (t) {
4050
- return sumItem("类型:" + (TYPE_LABEL[t] || t), function () { toggleIn(setTypeFilter, t); });
4483
+ typeFilter.map(function (typeId) {
4484
+ return sumItem(t("filter.sumType", { value: typeLabel(typeId) }), function () { toggleIn(setTypeFilter, typeId); });
4051
4485
  }),
4052
4486
  tagFilter.map(function (g) {
4053
- return sumItem("标签:#" + g, function () { toggleIn(setTagFilter, g); });
4487
+ return sumItem(t("filter.sumTag", { value: g }), function () { toggleIn(setTagFilter, g); });
4054
4488
  }),
4055
- query.trim() ? sumItem("搜索:" + query.trim(), function () { setQuery(""); }) : null,
4489
+ query.trim() ? sumItem(t("filter.sumQuery", { value: query.trim() }), function () { setQuery(""); }) : null,
4056
4490
  h("button", {
4057
4491
  type: "button", className: "smem-sum-clear", key: "all",
4058
4492
  onClick: function () { setStatusFilter("all"); setTypeFilter([]); setTagFilter([]); setQuery(""); },
4059
- }, "清除全部"),
4493
+ }, t("filter.clearAll")),
4060
4494
  ])
4061
4495
  : null,
4062
- h("span", { className: "smem-filters-label", key: "lst" }, "状态"),
4496
+ h("span", { className: "smem-filters-label", key: "lst" }, t("filter.statusLabel")),
4063
4497
  h("button", {
4064
4498
  type: "button", key: "st-all", className: "smem-chip",
4065
4499
  "data-on": statusFilter === "all" ? "1" : "0",
4066
- title: "活动与已归档一起看(归档条走灰色)",
4500
+ title: t("filter.allTitle"),
4067
4501
  onClick: function () { setStatusFilter("all"); },
4068
- }, ["全部", h("span", { className: "smem-chip-n", key: "n" }, String(totalCount))]),
4502
+ }, [t("filter.all"), h("span", { className: "smem-chip-n", key: "n" }, String(totalCount))]),
4069
4503
  h("button", {
4070
4504
  type: "button", key: "st-active", className: "smem-chip",
4071
4505
  "data-on": statusFilter === "active" ? "1" : "0",
4072
- title: "正在参与检索与星图的记忆",
4506
+ title: t("filter.activeTitle"),
4073
4507
  onClick: function () { setStatusFilter("active"); },
4074
- }, ["活动中", h("span", { className: "smem-chip-n", key: "n" }, String(items.length))]),
4508
+ }, [t("filter.active"), h("span", { className: "smem-chip-n", key: "n" }, String(items.length))]),
4075
4509
  h("button", {
4076
4510
  type: "button", key: "st-arch", className: "smem-chip",
4077
4511
  "data-on": statusFilter === "archived" ? "1" : "0",
4078
- title: "已收进档案馆:不参与检索与星图,可编辑 / 恢复",
4512
+ title: t("filter.archivedTitle"),
4079
4513
  onClick: function () { setStatusFilter("archived"); },
4080
- }, ["已归档", h("span", { className: "smem-chip-n", key: "n" }, String(archiveItems.length))]),
4514
+ }, [t("filter.archived"), h("span", { className: "smem-chip-n", key: "n" }, String(archiveItems.length))]),
4081
4515
  h("span", { className: "smem-sep", key: "sep0" }),
4082
- h("span", { className: "smem-filters-label", key: "lt" }, "类型"),
4516
+ h("span", { className: "smem-filters-label", key: "lt" }, t("filter.typeLabel")),
4083
4517
  h("button", {
4084
4518
  type: "button", key: "all", className: "smem-chip",
4085
4519
  "data-on": typeFilter.length ? "0" : "1",
4086
4520
  onClick: function () { setTypeFilter([]); },
4087
- }, ["全部", h("span", { className: "smem-chip-n", key: "n" }, String(statusItems.length))]),
4088
- typeList.map(function (t) {
4521
+ }, [t("filter.all"), h("span", { className: "smem-chip-n", key: "n" }, String(statusItems.length))]),
4522
+ typeList.map(function (typeId) {
4089
4523
  return h("button", {
4090
- type: "button", key: t, className: "smem-chip",
4091
- "data-on": typeFilter.indexOf(t) >= 0 ? "1" : "0",
4092
- onClick: function () { toggleIn(setTypeFilter, t); },
4093
- }, [TYPE_LABEL[t] || t, h("span", { className: "smem-chip-n", key: "n" }, String(typeCounts[t]))]);
4524
+ type: "button", key: typeId, className: "smem-chip",
4525
+ "data-on": typeFilter.indexOf(typeId) >= 0 ? "1" : "0",
4526
+ onClick: function () { toggleIn(setTypeFilter, typeId); },
4527
+ }, [typeLabel(typeId), h("span", { className: "smem-chip-n", key: "n" }, String(typeCounts[typeId]))]);
4094
4528
  }),
4095
4529
  tagList.length ? h("span", { className: "smem-sep", key: "sep" }) : null,
4096
- tagList.length ? h("span", { className: "smem-filters-label", key: "lgt" }, "标签") : null,
4530
+ tagList.length ? h("span", { className: "smem-filters-label", key: "lgt" }, t("filter.tags")) : null,
4097
4531
  tagShown.map(function (g) {
4098
4532
  return h("button", {
4099
4533
  type: "button", key: "tag-" + g, className: "smem-chip",
@@ -4104,15 +4538,15 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4104
4538
  tagList.length > TAG_VISIBLE_MAX
4105
4539
  ? h("button", {
4106
4540
  type: "button", key: "moretag", className: "smem-btn",
4107
- title: tagOpen ? "收起标签" : "还有一些标签没显示",
4541
+ title: tagOpen ? t("filter.collapseTags") : t("filter.moreTagsTitle"),
4108
4542
  onClick: function () { setTagOpen(!tagOpen); },
4109
- }, tagOpen ? "收起标签" : ("更多标签 (" + String(tagHiddenCount) + ")") )
4543
+ }, tagOpen ? t("filter.collapseTags") : t("filter.moreTags", { count: String(tagHiddenCount) }) )
4110
4544
  : null,
4111
4545
  hasFilter
4112
4546
  ? h("button", {
4113
4547
  type: "button", key: "clear", className: "smem-btn",
4114
4548
  onClick: function () { setStatusFilter("all"); setTypeFilter([]); setTagFilter([]); setQuery(""); },
4115
- }, "清除筛选")
4549
+ }, t("filter.clear"))
4116
4550
  : null,
4117
4551
 
4118
4552
  ]);
@@ -4260,8 +4694,8 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4260
4694
  remote.listFiles()
4261
4695
  .then(function (res) {
4262
4696
  if (res && typeof res === "object" && res.ok === false) {
4263
- const msg = res.error && res.error.message ? res.error.message : "remote 调用失败";
4264
- throw new Error("listFiles RPC 失败: " + msg);
4697
+ const msg = res.error && res.error.message ? res.error.message : t("error.remote");
4698
+ throw new Error(t("error.listFiles", { message: msg }));
4265
4699
  }
4266
4700
  const list = unwrap(res);
4267
4701
  setItems(Array.isArray(list) ? list : []);
@@ -4374,20 +4808,20 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4374
4808
  setError(null);
4375
4809
  setNotice("");
4376
4810
  if (!remote || typeof remote.readRaw !== "function" || typeof remote.writeRaw !== "function") {
4377
- setError("宿主侧还没有 readRaw / writeRaw 接口(插件版本较旧)");
4811
+ setError(t("error.noRaw"));
4378
4812
  return;
4379
4813
  }
4380
4814
  setBusy(true);
4381
4815
  // 先读再写:**已有索引绝不覆盖**。readRaw 对不存在的文件会 reject,正好当探针。
4382
4816
  remote.readRaw("MEMORY.md").then(function () {
4383
4817
  setBusy(false);
4384
- setNotice("MEMORY.md 已经存在,没有改动它。");
4818
+ setNotice(t("notice.indexExists"));
4385
4819
  }, function () {
4386
4820
  return remote.writeRaw("MEMORY.md", INDEX_SKELETON).then(function (res) {
4387
4821
  const out = unwrap(res);
4388
4822
  setBusy(false);
4389
- if (out && out.ok === false) { setError(out.error || "建立索引骨架失败"); return; }
4390
- setNotice("已建立 MEMORY.md 索引骨架。");
4823
+ if (out && out.ok === false) { setError(out.error || t("error.skeletonFailed")); return; }
4824
+ setNotice(t("notice.indexCreated"));
4391
4825
  load();
4392
4826
  });
4393
4827
  }).catch(function (e) {
@@ -4399,13 +4833,13 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4399
4833
  function openMemoryFolder() {
4400
4834
  setError(null);
4401
4835
  if (!remote || typeof remote.openMemoryDir !== "function") {
4402
- setError("宿主侧还没有 openMemoryDir 接口(插件版本较旧)");
4836
+ setError(t("error.noOpenDir"));
4403
4837
  return;
4404
4838
  }
4405
4839
  remote.openMemoryDir()
4406
4840
  .then(function (res) {
4407
4841
  const result = unwrap(res);
4408
- if (result && result.ok === false) setError(result.error || "打开记忆目录失败");
4842
+ if (result && result.ok === false) setError(result.error || t("error.openDirFailed"));
4409
4843
  })
4410
4844
  .catch(function (e) { setError(String((e && e.message) || e)); });
4411
4845
  }
@@ -4452,7 +4886,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4452
4886
  }
4453
4887
 
4454
4888
  function submit() {
4455
- if (!formDesc.trim()) { setError("描述不能为空"); return; }
4889
+ if (!formDesc.trim()) { setError(t("error.descEmpty")); return; }
4456
4890
  setBusy(true); setError(null); setNotice("");
4457
4891
  const type = formType;
4458
4892
  const name = (formName.trim() || (type + "_note.md"));
@@ -4477,9 +4911,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4477
4911
  remote.writeFile(safeName, content)
4478
4912
  .then(function (r) {
4479
4913
  // host 侧对保留名 / 非字符串内容 / 超限一律返回 { ok: false, error },不再抛异常。
4480
- if (r && r.ok === false) { setError(r.error || "写入被拒绝"); return; }
4914
+ if (r && r.ok === false) { setError(r.error || t("error.writeRejected")); return; }
4481
4915
  closeForm();
4482
- setNotice(editingFile ? "已更新 " + safeName : "已保存 " + safeName);
4916
+ setNotice(t(editingFile ? "notice.updated" : "notice.saved", { file: safeName }));
4483
4917
  load();
4484
4918
  })
4485
4919
  .catch(function (e) { setError(String((e && e.message) || e)); })
@@ -4490,9 +4924,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4490
4924
  setBusy(true); setError(null); setNotice("");
4491
4925
  remote.deleteFile(file)
4492
4926
  .then(function (r) {
4493
- if (r && r.ok === false) { setError(r.error || "删除被拒绝"); return; }
4927
+ if (r && r.ok === false) { setError(r.error || t("error.deleteRejected")); return; }
4494
4928
  setDeleting(null);
4495
- setNotice("已删除 " + file);
4929
+ setNotice(t("notice.deleted", { file: file }));
4496
4930
  load();
4497
4931
  })
4498
4932
  .catch(function (e) { setError(String((e && e.message) || e)); })
@@ -4507,16 +4941,16 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4507
4941
  */
4508
4942
  function confirmArchive(file) {
4509
4943
  if (typeof remote.archive !== "function") {
4510
- setError("宿主侧还没有 archive 接口(插件版本较旧)");
4944
+ setError(t("error.noArchive"));
4511
4945
  return;
4512
4946
  }
4513
4947
  setBusy(true); setError(null); setNotice("");
4514
4948
  remote.archive(file, archiveReason.trim())
4515
4949
  .then(function (r) {
4516
- if (r && r.ok === false) { setError(r.error || "归档被拒绝"); return; }
4950
+ if (r && r.ok === false) { setError(r.error || t("error.archiveRejected")); return; }
4517
4951
  setArchiving(null);
4518
4952
  setArchiveReason("");
4519
- setNotice("已归档 " + file + " —— 它现在在「已归档」视图里,随时可以恢复");
4953
+ setNotice(t("notice.archived", { file: file }));
4520
4954
  load();
4521
4955
  })
4522
4956
  .catch(function (e) { setError(String((e && e.message) || e)); })
@@ -4531,17 +4965,17 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4531
4965
  */
4532
4966
  function toggleLock(file, locked) {
4533
4967
  if (typeof remote.setLocked !== "function") {
4534
- setError("宿主侧还没有 setLocked 接口(插件版本较旧)");
4968
+ setError(t("error.noLock"));
4535
4969
  return;
4536
4970
  }
4537
4971
  setBusy(true); setError(null); setNotice("");
4538
4972
  remote.setLocked(file, locked)
4539
4973
  .then(function (r) {
4540
4974
  const d = unwrap(r);
4541
- if (d && d.ok === false) { setError(d.error || "锁定操作被拒绝"); return; }
4975
+ if (d && d.ok === false) { setError(d.error || t("error.lockRejected")); return; }
4542
4976
  setNotice(locked
4543
- ? ("已锁定 " + file + " —— 自动归档不会再选它,手动归档也会被拒绝")
4544
- : ("已解锁 " + file));
4977
+ ? t("notice.locked", { file: file })
4978
+ : t("notice.unlocked", { file: file }));
4545
4979
  load();
4546
4980
  })
4547
4981
  .catch(function (e) { setError(String((e && e.message) || e)); })
@@ -4550,7 +4984,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4550
4984
 
4551
4985
  /** 从档案馆捞回来:文件移回记忆目录,归档留痕被抹掉。 */
4552
4986
  function restoreFile(file) { if (typeof remote.restore !== "function") {
4553
- setError("宿主侧还没有 restore 接口(插件版本较旧)");
4987
+ setError(t("error.noRestore"));
4554
4988
  return;
4555
4989
  }
4556
4990
  setBusy(true); setError(null); setNotice("");
@@ -4558,9 +4992,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4558
4992
  setArchDelete(null);
4559
4993
  remote.restore(file)
4560
4994
  .then(function (r) {
4561
- if (r && r.ok === false) { setError(r.error || "恢复被拒绝"); return; }
4995
+ if (r && r.ok === false) { setError(r.error || t("error.restoreRejected")); return; }
4562
4996
  // 恢复会顺带把这条补进索引:说清楚,否则用户不知道「漏索引」是怎么好的
4563
- setNotice("已恢复 " + file + " —— 它回到了活动记忆里" + (r && r.indexed ? ",并补进了索引" : ""));
4997
+ setNotice(t(r && r.indexed ? "notice.restoredIndexed" : "notice.restored", { file: file }));
4564
4998
  load();
4565
4999
  })
4566
5000
  .catch(function (e) { setError(String((e && e.message) || e)); })
@@ -4575,7 +5009,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4575
5009
  */
4576
5010
  function deleteArchivedFile(file) {
4577
5011
  if (typeof remote.deleteArchived !== "function") {
4578
- setError("宿主侧还没有 deleteArchived 接口(插件版本较旧)");
5012
+ setError(t("error.noDeleteArchived"));
4579
5013
  return;
4580
5014
  }
4581
5015
  const item = archiveItems.filter(function (x) { return x.file === file; })[0];
@@ -4583,9 +5017,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4583
5017
  setBusy(true); setError(null); setNotice("");
4584
5018
  remote.deleteArchived(file, stamp)
4585
5019
  .then(function (r) {
4586
- if (r && r.ok === false) { setError(r.error || "删除被拒绝"); return; }
5020
+ if (r && r.ok === false) { setError(r.error || t("error.deleteRejected")); return; }
4587
5021
  setArchDelete(null);
4588
- setNotice("已彻底删除 " + file + " —— 归档区里也没有它了,找不回来");
5022
+ setNotice(t("notice.purged", { file: file }));
4589
5023
  load();
4590
5024
  })
4591
5025
  .catch(function (e) { setError(String((e && e.message) || e)); })
@@ -4602,7 +5036,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4602
5036
  */
4603
5037
  function openArchEdit(file) {
4604
5038
  if (typeof remote.readArchived !== "function") {
4605
- setError("宿主侧还没有 readArchived 接口(插件版本较旧)");
5039
+ setError(t("error.noReadArchived"));
4606
5040
  return;
4607
5041
  }
4608
5042
  setError(null); setNotice("");
@@ -4625,16 +5059,16 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4625
5059
 
4626
5060
  function saveArchEdit(file) {
4627
5061
  if (typeof remote.writeArchived !== "function") {
4628
- setError("宿主侧还没有 writeArchived 接口(插件版本较旧)");
5062
+ setError(t("error.noWriteArchived"));
4629
5063
  return;
4630
5064
  }
4631
5065
  setArchSaving(true); setError(null); setNotice("");
4632
5066
  // 原样交回:不 trim、不补 frontmatter、不做任何「智能」处理。
4633
5067
  remote.writeArchived(file, archText)
4634
5068
  .then(function (r) {
4635
- if (r && r.ok === false) { setError(r.error || "保存被拒绝"); return; }
5069
+ if (r && r.ok === false) { setError(r.error || t("error.saveRejected")); return; }
4636
5070
  setArchEdit(null);
4637
- setNotice("已保存 " + file);
5071
+ setNotice(t("notice.saved", { file: file }));
4638
5072
  load();
4639
5073
  })
4640
5074
  .catch(function (e) { setError(String((e && e.message) || e)); })
@@ -4646,7 +5080,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4646
5080
  // ── 工具面板的三个数据通道 ──────────────────────────────────────
4647
5081
 
4648
5082
  function runAudit() {
4649
- if (typeof remote.audit !== "function") { setError("宿主侧还没有 audit 接口(插件版本较旧)"); return; }
5083
+ if (typeof remote.audit !== "function") { setError(t("error.noAudit")); return; }
4650
5084
  setAuditBusy(true); setError(null);
4651
5085
  remote.audit()
4652
5086
  .then(function (r) { setAudit(unwrap(r) || null); })
@@ -4655,7 +5089,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4655
5089
  }
4656
5090
 
4657
5091
  function loadCandidates() {
4658
- if (typeof remote.archiveCandidates !== "function") { setError("宿主侧还没有 archiveCandidates 接口(插件版本较旧)"); return; }
5092
+ if (typeof remote.archiveCandidates !== "function") { setError(t("error.noArchiveCandidates")); return; }
4659
5093
  setCandBusy(true); setError(null);
4660
5094
  remote.archiveCandidates()
4661
5095
  .then(function (r) {
@@ -4672,7 +5106,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4672
5106
  }
4673
5107
 
4674
5108
  function loadSettings() {
4675
- if (typeof remote.getSettings !== "function") { setError("宿主侧还没有 getSettings 接口(插件版本较旧)"); return; }
5109
+ if (typeof remote.getSettings !== "function") { setError(t("error.noGetSettings")); return; }
4676
5110
  remote.getSettings()
4677
5111
  .then(function (r) { const d = unwrap(r) || {}; setSettings(d); })
4678
5112
  .catch(function (e) { setError(String((e && e.message) || e)); });
@@ -4740,7 +5174,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4740
5174
  */
4741
5175
  function archiveCandidates(list) {
4742
5176
  if (!list.length) return;
4743
- if (typeof remote.archive !== "function") { setError("宿主侧还没有 archive 接口(插件版本较旧)"); return; }
5177
+ if (typeof remote.archive !== "function") { setError(t("error.noArchive")); return; }
4744
5178
  setBusy(true); setError(null); setNotice("");
4745
5179
  const done = [];
4746
5180
  const failed = [];
@@ -4750,8 +5184,8 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4750
5184
  setCandConfirm(null);
4751
5185
  setPicked({});
4752
5186
  setNotice(failed.length
4753
- ? ("已归档 " + done.length + " 条," + failed.length + " 条没成功:" + failed.join("、"))
4754
- : ("已归档 " + done.length + " 条 —— 它们现在在档案馆里,随时可以恢复"));
5187
+ ? t("notice.batchPartial", { done: done.length, failed: failed.length, files: failed.join("、") })
5188
+ : t("notice.batchDone", { done: done.length }));
4755
5189
  load();
4756
5190
  loadCandidates();
4757
5191
  return;
@@ -4768,7 +5202,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4768
5202
  }
4769
5203
 
4770
5204
  function openRaw(name) {
4771
- if (typeof remote.readRaw !== "function") { setError("宿主侧还没有 readRaw 接口(插件版本较旧)"); return; }
5205
+ if (typeof remote.readRaw !== "function") { setError(t("error.noReadRaw")); return; }
4772
5206
  setError(null); setNotice("");
4773
5207
  setRawName(name);
4774
5208
  setRawText("");
@@ -4791,20 +5225,20 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4791
5225
  setRawLoading(false);
4792
5226
  return;
4793
5227
  }
4794
- setError("读不到 " + name + ":" + msg);
5228
+ setError(t("error.readFailed", { file: name, message: msg }));
4795
5229
  setRawLoading(false);
4796
5230
  setRawName(null);
4797
5231
  });
4798
5232
  }
4799
5233
 
4800
5234
  function saveRaw(name) {
4801
- if (typeof remote.writeRaw !== "function") { setError("宿主侧还没有 writeRaw 接口(插件版本较旧)"); return; }
5235
+ if (typeof remote.writeRaw !== "function") { setError(t("error.noWriteRaw")); return; }
4802
5236
  setRawBusy(true); setError(null); setNotice("");
4803
5237
  // 原样交回:不 trim、不补 frontmatter、不做任何「智能」处理。
4804
5238
  remote.writeRaw(name, rawText)
4805
5239
  .then(function (r) {
4806
- if (r && r.ok === false) { setError(r.error || "保存被拒绝"); return; }
4807
- setNotice((rawIsNew ? "已新建 " : "已保存 ") + name);
5240
+ if (r && r.ok === false) { setError(r.error || t("error.saveRejected")); return; }
5241
+ setNotice(t(rawIsNew ? "notice.rawNew" : "notice.saved", { file: name }));
4808
5242
  setRawIsNew(false);
4809
5243
  })
4810
5244
  .catch(function (e) { setError(String((e && e.message) || e)); })
@@ -4873,12 +5307,13 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4873
5307
  function sumItem(label, onRemove) {
4874
5308
  return h("span", { className: "smem-sum-item", key: "sum-" + label }, [
4875
5309
  label,
4876
- h("button", { type: "button", className: "smem-sum-x", key: "x", title: "撤掉这项筛选", onClick: onRemove }, "×"),
5310
+ h("button", { type: "button", className: "smem-sum-x", key: "x", title: t("filter.remove"), onClick: onRemove }, "×"),
4877
5311
  ]);
4878
5312
  }
4879
5313
  // 两个计数永远一起露脸:用户不必先筛状态才知道档案馆里有没有东西。
4880
- const headerCount = (hasFilter ? "筛出 " + String(filtered.length) + " / " + String(statusItems.length) + " · " : "") +
4881
- "活动中 " + String(items.length) + " · 已归档 " + String(archiveItems.length);
5314
+ const headerCount = hasFilter
5315
+ ? t("header.countsFiltered", { shown: filtered.length, total: statusItems.length, active: items.length, archived: archiveItems.length })
5316
+ : t("header.counts", { active: items.length, archived: archiveItems.length });
4882
5317
  // 头部带的「待重启生效」徽标:有多少项要重启才生效,从 limits / pendingLimits 的差算出来
4883
5318
  // (reserved 名单的变化 getSettings 只给 restartRequired 布尔值,数不出来就不写数)。
4884
5319
  const LIMIT_KEYS = ["maxResults", "maxChars", "maxBaseline", "maxSessionBytes", "staleDays"];
@@ -4909,39 +5344,39 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4909
5344
 
4910
5345
  return h("div", { className: "smem-root" }, [
4911
5346
  h("div", { className: "smem-tabs", key: "tabs", role: "tablist" }, [
4912
- h("button", { type: "button", className: "smem-tab" + (tab === "files" ? " smem-tab--on" : ""), key: "f", role: "tab", "aria-selected": tab === "files" ? "true" : "false", onClick: function () { setTab("files"); } }, "文件列表"),
4913
- h("button", { type: "button", className: "smem-tab" + (tab === "star" ? " smem-tab--on" : ""), key: "s", role: "tab", "aria-selected": tab === "star" ? "true" : "false", onClick: function () { setTab("star"); } }, "记忆星图"),
4914
- h("button", { type: "button", className: "smem-tab" + (tab === "autodream" ? " smem-tab--on" : ""), key: "d", role: "tab", "aria-selected": tab === "autodream" ? "true" : "false", onClick: function () { setTab("autodream"); } }, "自动做梦"),
5347
+ h("button", { type: "button", className: "smem-tab" + (tab === "files" ? " smem-tab--on" : ""), key: "f", role: "tab", "aria-selected": tab === "files" ? "true" : "false", onClick: function () { setTab("files"); } }, t("tab.files")),
5348
+ h("button", { type: "button", className: "smem-tab" + (tab === "star" ? " smem-tab--on" : ""), key: "s", role: "tab", "aria-selected": tab === "star" ? "true" : "false", onClick: function () { setTab("star"); } }, t("tab.star")),
5349
+ h("button", { type: "button", className: "smem-tab" + (tab === "autodream" ? " smem-tab--on" : ""), key: "d", role: "tab", "aria-selected": tab === "autodream" ? "true" : "false", onClick: function () { setTab("autodream"); } }, t("tab.autodream")),
4915
5350
  // 视觉分组:左边「看」(文件列表 / 星图),右边「设置 · 自动化」。
4916
5351
  h("span", { className: "smem-tabs-sep", key: "sep", "aria-hidden": "true" }),
4917
5352
  // 「自动归档」排在自动做梦之后:归档是**确定性规则**(按闲置天数算,不调模型),
4918
5353
  // 和「让模型整理记忆」是两件事。塞在自动做梦的设置里用户找不到
4919
5354
  // (2026-10-11 用户原话:「扔这里连我都找不到」)。
4920
- h("button", { type: "button", className: "smem-tab" + (tab === "archive" ? " smem-tab--on" : ""), key: "ar", role: "tab", "aria-selected": tab === "archive" ? "true" : "false", onClick: function () { setTab("archive"); } }, "自动归档"),
5355
+ h("button", { type: "button", className: "smem-tab" + (tab === "archive" ? " smem-tab--on" : ""), key: "ar", role: "tab", "aria-selected": tab === "archive" ? "true" : "false", onClick: function () { setTab("archive"); } }, t("tab.archive")),
4921
5356
  // 体检再排在「自动归档」之后、独立成一屏:它只报告不修改,和「文件列表」里的
4922
5357
  // 增删改是两件事,塞在工具抽屉里会让人以为它是个动作按钮。
4923
- h("button", { type: "button", className: "smem-tab" + (tab === "audit" ? " smem-tab--on" : ""), key: "a", role: "tab", "aria-selected": tab === "audit" ? "true" : "false", onClick: openAuditTab }, "体检"),
5358
+ h("button", { type: "button", className: "smem-tab" + (tab === "audit" ? " smem-tab--on" : ""), key: "a", role: "tab", "aria-selected": tab === "audit" ? "true" : "false", onClick: openAuditTab }, t("tab.audit")),
4924
5359
  ]),
4925
5360
  // 四个 tab 共用的一条头部带:我在哪 · 目录状态 · 活动/归档计数 · 体检徽标 · 待重启徽标 · 主操作。
4926
5361
  // 层级形状固定:导航=下划线(.smem-tabs)、筛选=chip、动作=按钮,三种不混用。
4927
5362
  h("div", { className: "smem-head", key: "head" }, [
4928
- h("span", { className: "smem-title", key: "t" }, "记忆管理"),
5363
+ h("span", { className: "smem-title", key: "t" }, t("section.label")),
4929
5364
  // 目录状态收进状态点的 title(不可读时下面另有一条 smem-err,错误本来就该显眼)。
4930
- h("span", { className: "smem-dot" + (dirOk ? "" : " smem-dot--bad"), key: "dot", title: (dirOk ? "memory 目录可读" : "memory 目录不可读") + " · " + String(TAB_LABEL[tab] || "") }),
5365
+ h("span", { className: "smem-dot" + (dirOk ? "" : " smem-dot--bad"), key: "dot", title: t("header.dirTitle", { state: dirOk ? t("header.dirReadable") : t("header.dirUnreadable"), tab: tabLabel(tab) }) }),
4931
5366
  h("span", { className: "smem-badge", key: "cnt" }, headerCount),
4932
5367
  // 体检徽标:**抽屉关着也一直看得见**,点它就切到「体检」那一屏。
4933
5368
  h("button", {
4934
5369
  type: "button", key: "aud",
4935
5370
  className: "smem-tool-badge" + (audit && audit.problems ? " smem-tool-badge--bad" : ""),
4936
- title: audit ? (audit.problems ? "切到体检看是哪几条" : "体检没有发现问题") : "跑一次体检(只读)",
5371
+ title: audit ? (audit.problems ? t("header.auditSwitch") : t("header.auditClean")) : t("header.auditRun"),
4937
5372
  onClick: openAuditTab,
4938
- }, audit ? (audit.problems ? ("体检 " + audit.problems + " 个问题") : "体检 通过") : "体检 · 未跑"),
5373
+ }, audit ? (audit.problems ? t("header.auditProblems", { count: audit.problems }) : t("header.auditPass")) : t("header.auditNotRun")),
4939
5374
  restartPending
4940
5375
  ? h("button", {
4941
5376
  type: "button", key: "rs", className: "smem-tool-badge smem-ad-badge--warn",
4942
- title: "有改动要重启 DSH 才生效",
5377
+ title: t("header.restartTitle"),
4943
5378
  onClick: function () { setTab("autodream"); setAdSeg("settings"); },
4944
- }, (pendingDiff ? (pendingDiff + " 项") : "") + "待重启生效")
5379
+ }, t(pendingDiff ? "header.restartPendingCount" : "header.restartPending", { count: pendingDiff }))
4945
5380
  : null,
4946
5381
  // 主操作:只放当前 tab 的那一个(星图/自动做梦的主操作在各自面板里,不重复摆一份)。
4947
5382
  // 弹簧:把主操作顶到右边。**换行时它留在上一行**,主操作于是落到下一行、
@@ -4949,30 +5384,30 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4949
5384
  // 也推到最右,看起来像"跑出格子了")。
4950
5385
  h("span", { className: "smem-head-spacer", key: "sp" }),
4951
5386
  h("span", { className: "smem-head-actions", key: "act" }, tab === "files" ? [
4952
- h("button", { type: "button", className: "smem-btn", key: "open", disabled: busy || !dirOk, title: "在系统文件管理器里打开记忆目录", onClick: openMemoryFolder }, "打开记忆文件夹"),
4953
- h("button", { type: "button", className: "smem-btn", key: "refresh", disabled: busy || loading, onClick: load }, "刷新"),
4954
- h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "add", onClick: openAdd }, "+ 添加记忆"),
5387
+ h("button", { type: "button", className: "smem-btn", key: "open", disabled: busy || !dirOk, title: t("header.openFolderTitle"), onClick: openMemoryFolder }, t("header.openFolder")),
5388
+ h("button", { type: "button", className: "smem-btn", key: "refresh", disabled: busy || loading, onClick: load }, t("header.refresh")),
5389
+ h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "add", onClick: openAdd }, t("action.add")),
4955
5390
  ] : null),
4956
5391
  ]),
4957
5392
  // 更新提示:四个 tab 共用一条,只在真有新版时出现;「×」只是这次不看
4958
5393
  updateInfo
4959
5394
  ? h("div", { className: "smem-update", key: "update" }, [
4960
- h("span", { key: "t" }, "发现新版本 v" + String(updateInfo.latest) + "(当前 v" + String(updateInfo.current) + ")"),
5395
+ h("span", { key: "t" }, t("update.found", { latest: updateInfo.latest, current: updateInfo.current })),
4961
5396
  h("a", {
4962
5397
  key: "l", className: "smem-update-link",
4963
5398
  href: updateInfo.releasesUrl || "https://github.com/gezi-wen/sage-mem/releases",
4964
5399
  target: "_blank", rel: "noreferrer",
4965
- }, "看更新内容"),
5400
+ }, t("update.notes")),
4966
5401
  h("button", {
4967
5402
  type: "button", key: "x", className: "smem-update-x",
4968
- title: "这次不看了(下次进面板还会提示)",
5403
+ title: t("update.dismissTitle"),
4969
5404
  onClick: function () { setUpdateInfo(null); },
4970
5405
  }, "×"),
4971
5406
  ])
4972
5407
  : null,
4973
5408
  // 工具栏只剩「搜索 + 工具 ▾」:刷新与添加已经收进头部带的主操作位。
4974
5409
  tab === "files" ? h("div", { className: "smem-toolbar", key: "bar" }, [
4975
- h("input", { className: "smem-search", key: "q", value: query, placeholder: "搜索文件名 / 描述 / 类型 / 标签…", onChange: function (e) { setQuery(e.target.value); } }),
5410
+ h("input", { className: "smem-search", key: "q", value: query, placeholder: t("toolbar.search"), onChange: function (e) { setQuery(e.target.value); } }),
4976
5411
  // 工具抽屉:关着时这里只有一个按钮,零占位;面板一次只开一个。
4977
5412
  // `data-smem-drawer` 是「点外部关闭」的排除标记:这一整块里的点击
4978
5413
  // 都算「抽屉内部」,包括开抽屉的那一次点击本身(见上面 effect 的长注释)。
@@ -4982,27 +5417,27 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4982
5417
  className: "smem-btn" + (drawer ? " smem-btn-primary" : ""),
4983
5418
  "aria-expanded": drawer ? "true" : "false",
4984
5419
  "aria-haspopup": "menu",
4985
- title: "归档候选 / 保留名编辑" + (dbgOn ? " / 注入调试" : ""),
5420
+ title: t(dbgOn ? "tools.titleDebug" : "tools.title"),
4986
5421
  onClick: function () { setDrawer(!drawer); },
4987
- }, "工具 ▾"),
5422
+ }, t("tools.button")),
4988
5423
  drawer ? h("div", { className: "smem-drawer-pop", key: "pop" }, [
4989
5424
  // 体检独立成 tab 之后,抽屉里只剩这两件「要动手」的事。
4990
5425
  h("button", {
4991
5426
  type: "button", className: "smem-drawer-item", key: "c",
4992
5427
  "data-on": tool === "candidates" ? "1" : "0",
4993
5428
  onClick: function () { toggleTool("candidates"); setDrawer(false); },
4994
- }, ["归档候选", h("span", { className: "smem-status", key: "s" }, cand ? (cand.count + " 条") : "未取")]),
5429
+ }, [t("tools.candidates"), h("span", { className: "smem-status", key: "s" }, cand ? t("tools.candidatesCount", { count: cand.count }) : t("tools.candidatesEmpty"))]),
4995
5430
  h("button", {
4996
5431
  type: "button", className: "smem-drawer-item", key: "r",
4997
5432
  "data-on": tool === "raw" ? "1" : "0",
4998
5433
  onClick: function () { toggleTool("raw"); setDrawer(false); },
4999
- }, ["保留名编辑", h("span", { className: "smem-status", key: "s" }, settings && settings.reserved ? (settings.reserved.length + " 个") : "")]),
5434
+ }, [t("tools.raw"), h("span", { className: "smem-status", key: "s" }, settings && settings.reserved ? t("tools.rawCount", { count: settings.reserved.length }) : "")]),
5000
5435
  // 1.7.0:只在调试开关打开时出现(关着时抽屉仍是两项,界面零变化)
5001
5436
  dbgOn ? h("button", {
5002
5437
  type: "button", className: "smem-drawer-item", key: "d",
5003
5438
  "data-on": tool === "debug" ? "1" : "0",
5004
5439
  onClick: function () { toggleTool("debug"); setDrawer(false); },
5005
- }, ["注入调试", h("span", { className: "smem-status", key: "s" }, dbg && Array.isArray(dbg.records) ? (dbg.records.length + " 次") : "")]) : null,
5440
+ }, [t("tools.debug"), h("span", { className: "smem-status", key: "s" }, dbg && Array.isArray(dbg.records) ? t("tools.debugCount", { count: dbg.records.length }) : "")]) : null,
5006
5441
  ]) : null,
5007
5442
  ]),
5008
5443
  ]) : null,
@@ -5080,25 +5515,25 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5080
5515
  notice && !error ? h("div", { className: "smem-ok", key: "ok", role: "status" }, "✓ " + notice) : null,
5081
5516
  formOpen
5082
5517
  ? h("div", { className: "smem-form", key: "form" }, [
5083
- h("div", { className: "smem-form-title", key: "ft" }, editingFile ? "编辑 " + editingFile : "添加记忆"),
5518
+ h("div", { className: "smem-form-title", key: "ft" }, editingFile ? t("form.editTitle", { file: editingFile }) : t("form.addTitle")),
5084
5519
  h("div", { className: "smem-row", key: "cat" }, [
5085
- h("span", { className: "smem-label", key: "l" }, "类型:"),
5086
- h("div", { className: "smem-chips", key: "chips" }, TYPE_ORDER.map(function (t) {
5087
- const meta = TYPE_META[t];
5088
- return h(Chip, { key: t, label: meta[0] + " " + meta[1], on: formType === t, onClick: function () { setFormType(t); } });
5520
+ h("span", { className: "smem-label", key: "l" }, t("form.type")),
5521
+ h("div", { className: "smem-chips", key: "chips" }, TYPE_ORDER.map(function (id) {
5522
+ const meta = TYPE_META[id];
5523
+ return h(Chip, { key: id, label: meta[0] + " " + typeLabel(id), on: formType === id, onClick: function () { setFormType(id); } });
5089
5524
  })),
5090
5525
  ]),
5091
- h("label", { className: "smem-label", key: "ln" }, "文件名(.md,留空自动 " + formType + "_note.md)"),
5092
- h("input", { className: "smem-input", key: "tn", value: formName, onChange: function (e) { setFormName(e.target.value); }, placeholder: "例如 project_example.md" }),
5093
- h("label", { className: "smem-label", key: "ld" }, "描述(写进 frontmatter,检索与列表都靠它)"),
5094
- h("input", { className: "smem-input", key: "td", value: formDesc, onChange: function (e) { setFormDesc(e.target.value); }, placeholder: "一句话说清这条记忆是什么" }),
5095
- h("label", { className: "smem-label", key: "lt" }, "自定义标签(可选,逗号分隔;会写进 frontmatter 的 tags,列表页可按它筛)"),
5096
- h("input", { className: "smem-input", key: "tt", value: formTags, onChange: function (e) { setFormTags(e.target.value); }, placeholder: "例如 部署, 笔记, dsh" }),
5097
- h("label", { className: "smem-label", key: "lb" }, "正文(可选,frontmatter 自动生成)"),
5098
- h("textarea", { className: "smem-textarea", key: "tb", rows: 6, value: formBody, onChange: function (e) { setFormBody(e.target.value); }, placeholder: "要长期记住的细节" }),
5526
+ h("label", { className: "smem-label", key: "ln" }, t("form.fileName", { type: formType })),
5527
+ h("input", { className: "smem-input", key: "tn", value: formName, onChange: function (e) { setFormName(e.target.value); }, placeholder: t("form.fileNamePlaceholder") }),
5528
+ h("label", { className: "smem-label", key: "ld" }, t("form.desc")),
5529
+ h("input", { className: "smem-input", key: "td", value: formDesc, onChange: function (e) { setFormDesc(e.target.value); }, placeholder: t("form.descPlaceholder") }),
5530
+ h("label", { className: "smem-label", key: "lt" }, t("form.tags")),
5531
+ h("input", { className: "smem-input", key: "tt", value: formTags, onChange: function (e) { setFormTags(e.target.value); }, placeholder: t("form.tagsPlaceholder") }),
5532
+ h("label", { className: "smem-label", key: "lb" }, t("form.body")),
5533
+ h("textarea", { className: "smem-textarea", key: "tb", rows: 6, value: formBody, onChange: function (e) { setFormBody(e.target.value); }, placeholder: t("form.bodyPlaceholder") }),
5099
5534
  h("div", { className: "smem-row", key: "btns" }, [
5100
- h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "save", disabled: busy, onClick: submit }, busy ? "保存中…" : "保存"),
5101
- h("button", { type: "button", className: "smem-btn", key: "cancel", disabled: busy, onClick: closeForm }, "取消"),
5535
+ h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "save", disabled: busy, onClick: submit }, busy ? t("card.saving") : t("card.save")),
5536
+ h("button", { type: "button", className: "smem-btn", key: "cancel", disabled: busy, onClick: closeForm }, t("card.cancel")),
5102
5537
  ]),
5103
5538
  ])
5104
5539
  : null,
@@ -5131,32 +5566,136 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5131
5566
  /** 该槽位的清理句柄数组(null = 当前没注册)。 */
5132
5567
  let sidebarDispose = null;
5133
5568
 
5134
- /** 侧栏图标:内联 SVG、currentColor、细线 —— 不引外部图片/字体,跟随宿主明暗主题。 */
5569
+ /**
5570
+ * 侧栏图标:内联 SVG、currentColor、细线 —— 不引外部图片/字体,跟随宿主明暗主题。
5571
+ *
5572
+ * 1.8.2 起字形与仓库根的 `icon.png` 同源:**叠层记忆卡片**(后面一张只描边、低透明,
5573
+ * 前面一张压在左下),不再是「文档三行」。单色而不是品牌金:侧栏图标必须跟随
5574
+ * 宿主的 currentColor,深色 / 浅色主题下才都可读。
5575
+ */
5135
5576
  function SidebarEntryIcon(props) {
5136
5577
  const edge = typeof props.size === "number" && props.size > 0 ? props.size : 16;
5137
5578
  return h("svg", {
5138
5579
  width: edge, height: edge, viewBox: "0 0 16 16", fill: "none",
5139
5580
  "aria-hidden": "true", focusable: "false",
5140
5581
  }, [
5582
+ // 后面那张卡:错开、只描边、低透明 —— 表达「一叠记忆」而不是两张孤立的卡。
5141
5583
  h("rect", {
5142
- key: "b", x: "2", y: "2.5", width: "12", height: "11", rx: "2.2",
5143
- stroke: "currentColor", strokeWidth: "1.2",
5584
+ key: "back", x: "4.4", y: "2.2", width: "9.4", height: "9", rx: "1.8",
5585
+ stroke: "currentColor", strokeWidth: "1.2", opacity: "0.45",
5144
5586
  }),
5145
- h("path", {
5146
- key: "l1", d: "M5.2 6.2h5.6",
5147
- stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round",
5587
+ // 前面那张卡:压在左下,实心描边。
5588
+ h("rect", {
5589
+ key: "front", x: "2.2", y: "4.8", width: "9.4", height: "9", rx: "1.8",
5590
+ stroke: "currentColor", strokeWidth: "1.2",
5148
5591
  }),
5149
5592
  h("path", {
5150
- key: "l2", d: "M5.2 8.4h5.6",
5593
+ key: "l1", d: "M4.7 8.2h4.4",
5151
5594
  stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round",
5152
5595
  }),
5153
5596
  h("path", {
5154
- key: "l3", d: "M5.2 10.6h3.2",
5597
+ key: "l2", d: "M4.7 10.6h3.1",
5155
5598
  stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round",
5156
5599
  }),
5157
5600
  ]);
5158
5601
  }
5159
5602
 
5603
+ /** 卡片里的错误文案:remote 可能 reject 一个字符串 / 对象 / Error,尽量展开成人话。 */
5604
+ function pluginCardErr(e) {
5605
+ if (!e) return t("pluginCard.saveFailed");
5606
+ if (e instanceof Error) return e.message || String(e);
5607
+ try {
5608
+ const s = JSON.stringify(e);
5609
+ return s && s !== "{}" ? s.slice(0, 200) : String(e);
5610
+ } catch (_) {
5611
+ return String(e);
5612
+ }
5613
+ }
5614
+
5615
+ /**
5616
+ * 插件详情页的「侧栏入口」卡片(1.8.2)。
5617
+ *
5618
+ * 槽位 `plugins.bundle.config` 是 keyed 槽(key = 包名),宿主在 sage-mem 的插件页里、
5619
+ * description 与「包含的组件」行之间渲染它,ownerProps 为 `{ view: "summary" | "page" }`:
5620
+ * - summary:标题下的一块摘要位 —— 这里返回 null,不在摘要位重复渲染;
5621
+ * - page:详情页本体(本卡片自己写设置,所以宿主给的是 page)。
5622
+ *
5623
+ * 数据与设置页同一个来源(remote.memory 的 getSettings / setSettings),切换成功后调
5624
+ * 模块级 `setSidebarEntry`:**立即**增删侧栏图标,不需要刷新 / 重启。
5625
+ * 勾选态一律由服务端回传的 settings 派生 —— 失败时什么都不改,于是自动回到原值。
5626
+ */
5627
+ function SidebarEntryConfigCard(props) {
5628
+ const ctx = props && props.ctx;
5629
+ // 必须走 stableRemote:真实宿主里 ctx.get() 每次返回新对象,进依赖数组会无限重跑。
5630
+ const remote = stableRemote(ctx, "remote.memory");
5631
+ const sOn = react.useState(true); const on = sOn[0], setOn = sOn[1];
5632
+ const sBusy = react.useState(false); const busy = sBusy[0], setBusy = sBusy[1];
5633
+ const sErr = react.useState(""); const err = sErr[0], setErr = sErr[1];
5634
+
5635
+ react.useEffect(function () {
5636
+ if (!remote || typeof remote.getSettings !== "function") return undefined;
5637
+ let alive = true;
5638
+ Promise.resolve(remote.getSettings()).then(function (out) {
5639
+ if (!alive) return;
5640
+ const loaded = unwrap(out) || {};
5641
+ // 旧宿主没有 ui 字段 / 值不是布尔 → 保持默认「开」,不弹错、不阻塞界面。
5642
+ if (loaded.ui && typeof loaded.ui.sidebarEntry === "boolean") setOn(loaded.ui.sidebarEntry);
5643
+ }, function () {
5644
+ // 读设置失败也按「开」显示:这里只是插件页的一张卡片,不该因为它报红。
5645
+ });
5646
+ return function () { alive = false; };
5647
+ }, [remote]);
5648
+
5649
+ // summary 位不渲染(hooks 必须在上方无条件调用完再返回,规则与顺序都不变)。
5650
+ if (props && props.view === "summary") return null;
5651
+
5652
+ function toggle(next) {
5653
+ if (busy) return;
5654
+ if (!remote || typeof remote.setSettings !== "function") {
5655
+ setErr(t("pluginCard.noSetSettings"));
5656
+ return;
5657
+ }
5658
+ setBusy(true); setErr("");
5659
+ Promise.resolve(remote.setSettings({ ui: { sidebarEntry: !!next } })).then(function (out) {
5660
+ setBusy(false);
5661
+ // setSettings 是 { ok, settings, restartRequired } 形状,不要过 unwrap。
5662
+ if (out && out.ok === false) {
5663
+ setErr(out.error || t("error.saveRejected"));
5664
+ return;
5665
+ }
5666
+ const settings = out && out.settings ? out.settings : null;
5667
+ const applied = settings && settings.ui && typeof settings.ui.sidebarEntry === "boolean"
5668
+ ? settings.ui.sidebarEntry
5669
+ : !!next;
5670
+ setOn(applied);
5671
+ // 写盘成功之后才动槽位:设置没落盘时界面不该先变一个样。
5672
+ setSidebarEntry(!!next);
5673
+ }, function (e) {
5674
+ setBusy(false);
5675
+ setErr(pluginCardErr(e));
5676
+ });
5677
+ }
5678
+
5679
+ return h("div", { className: "smem-plugin-card" }, [
5680
+ h("div", { className: "smem-plugin-card-head", key: "h" }, [
5681
+ h("span", { className: "smem-plugin-card-title", key: "t" }, t("pluginCard.title")),
5682
+ h("span", { className: "smem-plugin-card-status", key: "s" }, on ? t("pluginCard.on") : t("pluginCard.off")),
5683
+ ]),
5684
+ h("label", { className: "smem-check smem-plugin-card-check", key: "row" }, [
5685
+ h("input", {
5686
+ type: "checkbox", key: "c",
5687
+ checked: on,
5688
+ disabled: busy,
5689
+ onChange: function (e) { toggle(!!(e.target && e.target.checked)); },
5690
+ }),
5691
+ h("span", { key: "l" }, t("pluginCard.label")),
5692
+ ]),
5693
+ h("div", { className: "smem-plugin-card-note", key: "n" },
5694
+ t("pluginCard.note")),
5695
+ err ? h("div", { className: "smem-plugin-card-err", key: "e" }, err) : null,
5696
+ ]);
5697
+ }
5698
+
5160
5699
  /**
5161
5700
  * 返回会话:宿主 layout 服务的 `selectPanel(null)` 就是把中央主区还给 Conversation。
5162
5701
  * 取不到 layout(旧外壳 / 测试环境)时是**空操作**,不影响页面其它部分。
@@ -5177,7 +5716,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5177
5716
  h("div", { className: "smem-page-top", key: "top" }, [
5178
5717
  h("button", {
5179
5718
  type: "button", className: "smem-page-back", key: "back",
5180
- title: "返回会话", onClick: onBack,
5719
+ title: t("page.back"), onClick: onBack,
5181
5720
  }, [
5182
5721
  h("svg", {
5183
5722
  key: "i", width: 14, height: 14, viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true",
@@ -5185,7 +5724,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5185
5724
  d: "M10 3.5 5.5 8l4.5 4.5", stroke: "currentColor", strokeWidth: "1.6",
5186
5725
  strokeLinecap: "round", strokeLinejoin: "round",
5187
5726
  })),
5188
- h("span", { key: "l" }, "返回会话"),
5727
+ h("span", { key: "l" }, t("page.back")),
5189
5728
  ]),
5190
5729
  ]),
5191
5730
  h(Section, { key: "sec", ...props, ctx: sidebarCtx }),
@@ -5203,7 +5742,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5203
5742
  name: "sidebar.panellist",
5204
5743
  id: SIDEBAR_ENTRY_ID,
5205
5744
  order: 20,
5206
- label: "记忆管理",
5745
+ label: () => t("sidebar.label"),
5207
5746
  }, SidebarEntryIcon);
5208
5747
  }));
5209
5748
  // 中央主区 keyed 槽:key 与侧栏 id 相同;inject 面把「返回会话」交给页面组件。
@@ -5258,13 +5797,14 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5258
5797
  tag.textContent = CSS;
5259
5798
  document.head.appendChild(tag);
5260
5799
  }
5800
+ setupI18n(ctx);
5261
5801
  ctx.slots.inject("settings.section", () =>
5262
5802
  ctx.slots.register(
5263
5803
  {
5264
5804
  name: "settings.section",
5265
5805
  id: "sage-mem",
5266
5806
  order: 20,
5267
- label: "记忆管理",
5807
+ label: () => t("section.label"),
5268
5808
  // 声明「记忆星图」子槽:sage-starmap 向它注入星空视图,
5269
5809
  // 记忆管理页用 renderSlot 渲染。声明 children 后 shell 才会
5270
5810
  // 向本 section 组件传入 renderSlot。
@@ -5288,6 +5828,29 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5288
5828
  sidebarEnabled = true;
5289
5829
  }
5290
5830
  setSidebarEntry(sidebarEnabled);
5831
+
5832
+ // ── 1.8.2 插件详情页的「侧栏入口」卡片 ──────────────────────────────
5833
+ // `plugins.bundle.config` 是 keyed 槽,key = 本 bundle 的 **package name**(sage-mem)。
5834
+ // 宿主在插件页的 description 与「包含的组件」行之间渲染它;ownerProps 形如
5835
+ // `{ view: "summary" | "page" }`,卡片里自己再判一次(summary 返回 null)。
5836
+ //
5837
+ // ⚠️ 这个槽 0.1.7+ 才有。旧宿主上 inject 会**抛错**,所以必须整段 try/catch ——
5838
+ // 否则一个可选的插件页卡片会把设置页与侧栏入口一起拖垮(整个 client 插件不 apply)。
5839
+ // 放在 apply 最后:即使这里因宿主差异出问题,前面的界面已经挂上去了。
5840
+ try {
5841
+ if (ctx.slots && typeof ctx.slots.inject === "function") {
5842
+ ctx.slots.inject("plugins.bundle.config", function () {
5843
+ return ctx.slots.register({
5844
+ name: "plugins.bundle.config",
5845
+ key: "sage-mem",
5846
+ }, function (props) {
5847
+ return react.createElement(SidebarEntryConfigCard, { ...(props || {}), ctx });
5848
+ });
5849
+ });
5850
+ }
5851
+ } catch (e) {
5852
+ // 旧宿主没有这个槽 / 不接受注册:静默跳过,绝不影响其余界面。
5853
+ }
5291
5854
  }
5292
5855
 
5293
5856
  exports.apply = apply;