sage-mem 2.5.1 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +6 -6
- package/README.md +6 -6
- package/lib/client.js +862 -168
- package/lib/index.js +6 -3
- package/lib/settings.js +41 -9
- package/lib/typert.host.js +4 -4
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -2551,6 +2551,17 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2551
2551
|
".smem-modal-body{font-size:12.5px;line-height:1.65;color:var(--dsw-alias-label-secondary);display:flex;flex-direction:column;gap:8px;}",
|
|
2552
2552
|
".smem-modal-foot{display:flex;gap:8px;justify-content:flex-end;margin-top:14px;}",
|
|
2553
2553
|
"@media (prefers-reduced-motion:reduce){.smem-modal,.smem-modal-scrim{animation:none;}}",
|
|
2554
|
+
// ── 提示条(2.6.0):复制成功的回执,铺在视口下方中央 ──
|
|
2555
|
+
// **容器不吃点击**(pointer-events:none):回执不该拦住下一次操作;只有 × 收回
|
|
2556
|
+
// 点击,让用户能手动关掉。z-index 压在模态(50)之下、抽屉(40)同层之上。
|
|
2557
|
+
".smem-toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:41;pointer-events:none;display:flex;align-items:center;gap:10px;max-width:min(560px,92vw);padding:10px 10px 10px 16px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);box-shadow:0 12px 32px rgba(0,0,0,.38);font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary);animation:smem-pop 200ms cubic-bezier(.2,0,0,1);}",
|
|
2558
|
+
".smem-toast-check{flex:none;color:var(--dsw-alias-state-success-primary,#16a34a);font-weight:700;}",
|
|
2559
|
+
".smem-toast-text{min-width:0;overflow-wrap:anywhere;}",
|
|
2560
|
+
".smem-toast-x{pointer-events:auto;flex:none;width:22px;height:22px;padding:0;border-radius:6px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1;font-family:inherit;cursor:pointer;}",
|
|
2561
|
+
".smem-toast-x:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2);}",
|
|
2562
|
+
"@media (prefers-reduced-motion:reduce){.smem-toast{animation:none;}}",
|
|
2563
|
+
// 兜底模态里的提示词:等宽、尽量多显示几行。
|
|
2564
|
+
".smem-prompt-ta{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11.5px;line-height:1.5;height:38vh;min-height:160px;white-space:pre;resize:vertical;}",
|
|
2554
2565
|
// ── 编辑抽屉(2.5.0,D39):替掉 1.0 起的页面内联表单 `.smem-form` ──
|
|
2555
2566
|
// 抽屉从右侧滑入,遮罩自带淡入。两处都用 `animation` 而不是 transition:mount 即播,
|
|
2556
2567
|
// 不依赖二次渲染(页面不可见时 rAF 不触发)。
|
|
@@ -2577,7 +2588,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2577
2588
|
".smem-chips{display:flex;gap:6px;flex-wrap:wrap;}",
|
|
2578
2589
|
// ── 模块卡头(2.5.0,D39)────────────────────────────────────────────
|
|
2579
2590
|
// 一屏两张模块卡,就必须看得出「这张卡到哪儿为止」——
|
|
2580
|
-
// 否则下面那张会被读成「当前这一段的内容」(2026-11-08
|
|
2591
|
+
// 否则下面那张会被读成「当前这一段的内容」(2026-11-08 维护时一眼看到的正是这个)。
|
|
2581
2592
|
// 所以每张卡自己带边框;卡头(开关 + 标题 + 状态)在分段之外,底下一条线把它和内容分开。
|
|
2582
2593
|
".smem-module-card{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1);padding:14px 16px;}",
|
|
2583
2594
|
".smem-module-head{padding-bottom:10px;border-bottom:1px solid var(--dsw-alias-border-l1);margin-bottom:12px;}",
|
|
@@ -2675,19 +2686,25 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2675
2686
|
".smem-page-top{display:flex;align-items:center;}",
|
|
2676
2687
|
".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;}",
|
|
2677
2688
|
".smem-page-back:hover{color:var(--dsw-alias-label-primary,#1f2329);background:var(--dsw-alias-bg-layer-2);}",
|
|
2678
|
-
// ── 1.8.2
|
|
2689
|
+
// ── 1.8.2 / 2.7.0:插件详情页的「界面入口」折叠行 ──
|
|
2679
2690
|
// 融入宿主插件页:标题 13px/600、说明 12px 次要色,圆角与分隔线只用宿主 token。
|
|
2680
|
-
//
|
|
2691
|
+
// 2.7.0 起默认收起:收起时只露标题 + 当前状态,展开才是三选一。
|
|
2681
2692
|
".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;}",
|
|
2682
|
-
".smem-
|
|
2693
|
+
".smem-entry-summary{display:flex;align-items:center;gap:8px;width:100%;border:0;background:transparent;padding:0;margin:0;font-family:inherit;text-align:left;cursor:pointer;color:inherit;}",
|
|
2694
|
+
".smem-entry-summary:hover .smem-plugin-card-title{color:var(--dsw-alias-brand-primary,#4d76e6);}",
|
|
2695
|
+
".smem-entry-summary:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4d76e6);outline-offset:3px;border-radius:6px;}",
|
|
2696
|
+
".smem-entry-chevron{flex:none;margin-left:auto;color:var(--dsw-alias-label-tertiary,#8b93a1);transition:transform 180ms cubic-bezier(.2,0,0,1);}",
|
|
2697
|
+
".smem-entry-body{display:flex;flex-direction:column;gap:8px;padding-top:2px;}",
|
|
2698
|
+
".smem-entry-opts{display:flex;flex-direction:column;gap:6px;}",
|
|
2699
|
+
".smem-entry-opt{font-size:12.5px;line-height:1.5;padding:1px 0;}",
|
|
2700
|
+
".smem-entry-opt input:disabled{cursor:not-allowed;opacity:.55;}",
|
|
2683
2701
|
".smem-plugin-card-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#1f2430);line-height:1.4;}",
|
|
2684
2702
|
".smem-plugin-card-status{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);line-height:1.4;}",
|
|
2685
|
-
".smem-plugin-card-check{font-size:12.5px;}",
|
|
2686
2703
|
".smem-plugin-card-note{font-size:12px;line-height:1.55;color:var(--dsw-alias-label-secondary,#6b7280);}",
|
|
2687
2704
|
".smem-plugin-card-err{font-size:12px;line-height:1.5;color:var(--dsw-alias-state-error-primary,#ef4444);overflow-wrap:anywhere;}",
|
|
2688
2705
|
// ── 底部一行仓库链接(引流,不抢视线)──
|
|
2689
2706
|
// ── 求 Star 的角标(2.5.0,D39):不占正文位置,hover 才展开半句话 ──
|
|
2690
|
-
// 2026-11-09
|
|
2707
|
+
// 2026-11-09 调(原话:「太不显眼了」):既然它固定在右下角、不压任何内容,
|
|
2691
2708
|
// 就没必要靠 0.62 的透明度自我隐形 —— 那是给「怕打扰阅读」的按下去的做法,
|
|
2692
2709
|
// 而这一颗星本来就该被看见。现在:不透明、底色抬到 layer-2、字色从 tertiary 抬到 secondary、
|
|
2693
2710
|
// 加一层轻阴影让它离底;hover 才把「喜欢这个插件的话」展开(不常显,仍不占阅读视线)。
|
|
@@ -3476,6 +3493,27 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
3476
3493
|
]);
|
|
3477
3494
|
}
|
|
3478
3495
|
|
|
3496
|
+
/**
|
|
3497
|
+
* 一条**不挡操作**的提示条(2.6.0)。
|
|
3498
|
+
*
|
|
3499
|
+
* 为什么不用模态:复制成功是一次「做完了」的回执,不该拦着人继续点 ——
|
|
3500
|
+
* 所以它铺在视口下方中央、容器 `pointer-events:none`(点得穿),只有右上那颗
|
|
3501
|
+
* × 收 `pointer-events:auto`,让用户能手动关掉(`onClose`)。
|
|
3502
|
+
*
|
|
3503
|
+
* 自动消失的计时器由调用方(Section)持有:它才知道「换页签就该收」。
|
|
3504
|
+
*/
|
|
3505
|
+
function Toast(props) {
|
|
3506
|
+
return h("div", { className: "smem-toast", role: "status", "aria-live": "polite" }, [
|
|
3507
|
+
h("span", { className: "smem-toast-check", key: "c", "aria-hidden": "true" }, "✓"),
|
|
3508
|
+
h("span", { className: "smem-toast-text", key: "t" }, String(props.text == null ? "" : props.text)),
|
|
3509
|
+
h("button", {
|
|
3510
|
+
type: "button", key: "x", className: "smem-toast-x",
|
|
3511
|
+
title: props.closeTitle || "", "aria-label": props.closeTitle || "",
|
|
3512
|
+
onClick: props.onClose,
|
|
3513
|
+
}, "×"),
|
|
3514
|
+
]);
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3479
3517
|
function AuditPanel(props) {
|
|
3480
3518
|
const r = props.report || {};
|
|
3481
3519
|
/**
|
|
@@ -3493,6 +3531,10 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
3493
3531
|
])
|
|
3494
3532
|
: null);
|
|
3495
3533
|
const hints = r.archivedLinks || [];
|
|
3534
|
+
// 分节表(字段 / 标题 / 条目格式)与「复制修复提示词」共用同一份,定义在
|
|
3535
|
+
// `client/56-health-prompt.js` 的 `AUDIT_SECTIONS` —— 面板上看到的每一行,
|
|
3536
|
+
// 就是提示词里读到的那一行。这里只负责把它渲染成「标题(说明)」。
|
|
3537
|
+
const sectionTitle = (s) => (s.sub ? s.title + "(" + s.sub + ")" : s.title);
|
|
3496
3538
|
return h("div", { className: "smem-tool-panel" + (standalone ? " smem-tool-panel--full" : " smem-tool-panel--cap"), key: "audit" }, [
|
|
3497
3539
|
h("div", { className: "smem-tool-head", key: "head" }, [
|
|
3498
3540
|
h("span", { className: "smem-tool-title", key: "t" }, "体检"),
|
|
@@ -3506,19 +3548,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
3506
3548
|
standalone ? null : toolClose(props),
|
|
3507
3549
|
]),
|
|
3508
3550
|
r.problems
|
|
3509
|
-
? h("div", { key: "secs" },
|
|
3510
|
-
section(
|
|
3511
|
-
|
|
3512
|
-
section("type 与前缀对不上", r.typeMismatch, (x) => x.file + " · type=" + x.type + ",文件名前缀=" + x.prefix),
|
|
3513
|
-
section("行尾是 CRLF", r.crlf, (x) => x.file),
|
|
3514
|
-
section("双链断链", r.brokenLinks, (x) => x.file + " → [[" + x.target + "]]"),
|
|
3515
|
-
section("未加引号的「空格+#」", r.hashHazards, (x) => x.file + " · " + x.line),
|
|
3516
|
-
section("没有 frontmatter", r.noFrontmatter, (x) => x.file),
|
|
3517
|
-
section("读不出来", r.unreadable, (x) => x.file),
|
|
3518
|
-
// 归档中途崩溃留下的私有名残留:它不是一条记忆(扫描/注入都不认),
|
|
3519
|
-
// 但里面可能是用户唯一一份内容 —— 所以要在体检里露头,而不是隐形。
|
|
3520
|
-
section("归档残留(内容可能还在,请确认后改回 .md 或删除)", r.strays, (x) => x.file),
|
|
3521
|
-
])
|
|
3551
|
+
? h("div", { key: "secs" }, AUDIT_SECTIONS.map(function (s) {
|
|
3552
|
+
return section(sectionTitle(s), r[s.field], s.item);
|
|
3553
|
+
}))
|
|
3522
3554
|
: h("div", { className: "smem-empty smem-empty--ok", key: "ok" },
|
|
3523
3555
|
"没有发现问题 —— 索引、双链、frontmatter 与行尾都对得上。"),
|
|
3524
3556
|
hints.length
|
|
@@ -3526,7 +3558,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
3526
3558
|
h("div", { className: "smem-audit-h", key: "h" }, "指向已归档(提示,不计入问题)"),
|
|
3527
3559
|
h("div", { className: "smem-ar-hint", key: "n" },
|
|
3528
3560
|
"归档不是删除:这些链接的目标还在档案馆里躺着,恢复它链接就活了 —— 不用改。"),
|
|
3529
|
-
rows(hints,
|
|
3561
|
+
rows(hints, AUDIT_ARCHIVED_LINKS.item),
|
|
3530
3562
|
])
|
|
3531
3563
|
: null,
|
|
3532
3564
|
h("div", { className: "smem-ar-hint", key: "ro" }, "这里只报告、不修改 —— 要改哪条,用那条记忆自己的编辑入口。"),
|
|
@@ -4162,13 +4194,18 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4162
4194
|
"star.dismiss": "不再显示(下次打开 DSH 还会出现)",
|
|
4163
4195
|
"star.aria": "在 GitHub 上给 gezi-wen/sage-mem 点一颗星",
|
|
4164
4196
|
|
|
4165
|
-
"
|
|
4166
|
-
"
|
|
4167
|
-
"
|
|
4168
|
-
"
|
|
4169
|
-
"
|
|
4170
|
-
"
|
|
4171
|
-
"
|
|
4197
|
+
"entry.title": "界面入口",
|
|
4198
|
+
"entry.subtitle": "控制「记忆管理」在界面中的入口。至少保留一个。",
|
|
4199
|
+
"entry.opt.both": "两个都显示",
|
|
4200
|
+
"entry.opt.sidebar": "只显示侧栏",
|
|
4201
|
+
"entry.opt.settings": "只显示设置页",
|
|
4202
|
+
"entry.note": "改动立即生效,不需要重启。",
|
|
4203
|
+
"entry.sidebarUnsupported": "当前宿主不支持侧栏入口。",
|
|
4204
|
+
"entry.degraded": "当前宿主不支持侧栏入口,已临时按「只显示设置页」显示。",
|
|
4205
|
+
"entry.legacyHost": "宿主模块还是旧版本,重启 DSH 后可选「只显示侧栏」。",
|
|
4206
|
+
"entry.notice.saved": "界面入口已更新。",
|
|
4207
|
+
"entry.err.noSetSettings": "宿主侧还没有 setSettings 接口(插件版本较旧)",
|
|
4208
|
+
"entry.err.saveFailed": "保存失败(未知原因)",
|
|
4172
4209
|
|
|
4173
4210
|
"error.remote": "remote 调用失败",
|
|
4174
4211
|
"error.listFiles": "listFiles RPC 失败: {message}",
|
|
@@ -4363,11 +4400,6 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4363
4400
|
"ad.last.retried": " · 零正文重试 {count} 次后补齐",
|
|
4364
4401
|
"ad.last.continuations": " · 续写 {count} 轮",
|
|
4365
4402
|
"ad.last.report": " · 报告 {name}",
|
|
4366
|
-
"ad.sidebar.title": "侧栏入口",
|
|
4367
|
-
"ad.sidebar.on": "已开启",
|
|
4368
|
-
"ad.sidebar.off": "已关闭",
|
|
4369
|
-
"ad.sidebar.note": "在 DSH 侧栏显示「记忆管理」图标,点它把中央主区切到本页。默认开启;关闭后「设置 → 记忆管理」这个入口仍然保留。改动立即生效,不需要重启。",
|
|
4370
|
-
"ad.sidebar.label": "在侧栏显示「记忆管理」入口",
|
|
4371
4403
|
"ad.limits.title": "记忆注入参数",
|
|
4372
4404
|
"ad.limits.pending": "有改动待生效",
|
|
4373
4405
|
"ad.limits.restartWarn": "⚠ 这些值在 DSH 启动时就固化了 —— 要重启 DSH 才生效。下面每行都写清「当前 → 重启后」。",
|
|
@@ -4467,8 +4499,6 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4467
4499
|
"ad.err.notSaved": "没保存 —— {detail}",
|
|
4468
4500
|
"ad.err.saveRejected": "保存被拒绝",
|
|
4469
4501
|
"ad.notice.limitsSaved": "已保存 —— 需重启 DSH 才生效",
|
|
4470
|
-
"ad.notice.sidebarOn": "侧栏入口已开启(立即生效)",
|
|
4471
|
-
"ad.notice.sidebarOff": "侧栏入口已关闭(设置页入口仍在)",
|
|
4472
4502
|
"ad.err.reservedRejected": "名单被拒绝",
|
|
4473
4503
|
"ad.notice.reservedSaved": "保留名名单已保存 —— 需重启 DSH 才生效",
|
|
4474
4504
|
"ad.err.reservedNeedName": "先写一个文件名,例如 project_notes.md",
|
|
@@ -4517,6 +4547,16 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4517
4547
|
"archive.err.thresholdBad": "{label}的阈值要是 1~3650 之间的天数(现在是「{value}」)",
|
|
4518
4548
|
"archive.notice.thresholdsSaved": "归档条件已保存",
|
|
4519
4549
|
"archive.err.thresholdStill": "{label} 仍是 {value}",
|
|
4550
|
+
|
|
4551
|
+
// ── 库健康 · 复制修复提示词(2.6.0)──
|
|
4552
|
+
// 只有按钮 / title / 提示条 / 兜底模态走 i18n;提示词**正文**仍是中文
|
|
4553
|
+
// (体检卡与冲突卡本身还没 i18n,见 D31 / D40)。
|
|
4554
|
+
"header.copyFixPrompt": "复制修复提示词",
|
|
4555
|
+
"header.copyHealthTitle": "把这 {count} 件事整理成提示词 —— 粘给智能体(体检问题照修,冲突只给建议)",
|
|
4556
|
+
"toast.copied": "复制修复任务成功,请在会话中发送给智能体",
|
|
4557
|
+
"prompt.copyFailedTitle": "复制没成功 —— 手动复制这段提示词",
|
|
4558
|
+
"prompt.copyFailedHint": "点一下文本框全选,再按 Ctrl+C 复制",
|
|
4559
|
+
"prompt.close": "关闭",
|
|
4520
4560
|
};
|
|
4521
4561
|
|
|
4522
4562
|
const EN = {
|
|
@@ -4661,13 +4701,18 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4661
4701
|
"star.dismiss": "Hide until next launch",
|
|
4662
4702
|
"star.aria": "Star gezi-wen/sage-mem on GitHub",
|
|
4663
4703
|
|
|
4664
|
-
"
|
|
4665
|
-
"
|
|
4666
|
-
"
|
|
4667
|
-
"
|
|
4668
|
-
"
|
|
4669
|
-
"
|
|
4670
|
-
"
|
|
4704
|
+
"entry.title": "Interface entries",
|
|
4705
|
+
"entry.subtitle": "Choose where Memory management appears. At least one entry stays on.",
|
|
4706
|
+
"entry.opt.both": "Show both",
|
|
4707
|
+
"entry.opt.sidebar": "Sidebar only",
|
|
4708
|
+
"entry.opt.settings": "Settings only",
|
|
4709
|
+
"entry.note": "Changes take effect immediately; no restart needed.",
|
|
4710
|
+
"entry.sidebarUnsupported": "This host does not support the sidebar entry.",
|
|
4711
|
+
"entry.degraded": "The sidebar entry is unavailable on this host, so the Settings entry is shown instead.",
|
|
4712
|
+
"entry.legacyHost": "The host module is still the old version; restart DSH to choose Sidebar only.",
|
|
4713
|
+
"entry.notice.saved": "Interface entries updated.",
|
|
4714
|
+
"entry.err.noSetSettings": "The host has no setSettings API yet (older plugin version)",
|
|
4715
|
+
"entry.err.saveFailed": "Save failed (unknown reason)",
|
|
4671
4716
|
|
|
4672
4717
|
"error.remote": "remote call failed",
|
|
4673
4718
|
"error.listFiles": "listFiles RPC failed: {message}",
|
|
@@ -4860,11 +4905,6 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4860
4905
|
"ad.last.retried": " · completed after {count} zero-body retries",
|
|
4861
4906
|
"ad.last.continuations": " · {count} continuation rounds",
|
|
4862
4907
|
"ad.last.report": " · report {name}",
|
|
4863
|
-
"ad.sidebar.title": "Sidebar entry",
|
|
4864
|
-
"ad.sidebar.on": "On",
|
|
4865
|
-
"ad.sidebar.off": "Off",
|
|
4866
|
-
"ad.sidebar.note": "Show the “Memory management” icon in the DSH sidebar; clicking it switches the main area to this page. On by default; turning it off keeps the Settings → Memory management entry. Changes take effect immediately, no restart needed.",
|
|
4867
|
-
"ad.sidebar.label": "Show the “Memory management” entry in the sidebar",
|
|
4868
4908
|
"ad.limits.title": "Injection limits",
|
|
4869
4909
|
"ad.limits.pending": "changes pending",
|
|
4870
4910
|
"ad.limits.restartWarn": "⚠ These values are fixed at DSH startup — restart DSH for them to apply. Each row below shows “current → after restart”.",
|
|
@@ -4964,8 +5004,6 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4964
5004
|
"ad.err.notSaved": "Not saved — {detail}",
|
|
4965
5005
|
"ad.err.saveRejected": "Save rejected",
|
|
4966
5006
|
"ad.notice.limitsSaved": "Saved — restart DSH for it to take effect",
|
|
4967
|
-
"ad.notice.sidebarOn": "Sidebar entry enabled (takes effect immediately)",
|
|
4968
|
-
"ad.notice.sidebarOff": "Sidebar entry disabled (the Settings entry remains)",
|
|
4969
5007
|
"ad.err.reservedRejected": "Reserved-name list rejected",
|
|
4970
5008
|
"ad.notice.reservedSaved": "Reserved-name list saved — restart DSH for it to take effect",
|
|
4971
5009
|
"ad.err.reservedNeedName": "Enter a file name first, e.g. project_notes.md",
|
|
@@ -5014,6 +5052,14 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5014
5052
|
"archive.err.thresholdBad": "{label} threshold must be between 1 and 3650 days (currently “{value}”)",
|
|
5015
5053
|
"archive.notice.thresholdsSaved": "Archive conditions saved",
|
|
5016
5054
|
"archive.err.thresholdStill": "{label} is still {value}",
|
|
5055
|
+
|
|
5056
|
+
// ── Health · copy the fix prompt (2.6.0) ──
|
|
5057
|
+
"header.copyFixPrompt": "Copy fix prompt",
|
|
5058
|
+
"header.copyHealthTitle": "Turn these {count} items into a prompt you can paste to the agent (it fixes the audit problems and only advises on conflicts)",
|
|
5059
|
+
"toast.copied": "Fix task copied — paste it to the agent in the conversation",
|
|
5060
|
+
"prompt.copyFailedTitle": "Copy failed — copy this prompt by hand",
|
|
5061
|
+
"prompt.copyFailedHint": "Click the box to select it all, then press Ctrl+C",
|
|
5062
|
+
"prompt.close": "Close",
|
|
5017
5063
|
};
|
|
5018
5064
|
|
|
5019
5065
|
/**
|
|
@@ -5078,6 +5124,334 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5078
5124
|
else install();
|
|
5079
5125
|
return true;
|
|
5080
5126
|
}
|
|
5127
|
+
// ── 库健康:把「要处理的事」整理成能给智能体的提示词(2.6.0)──────────
|
|
5128
|
+
//
|
|
5129
|
+
// 为什么是「复制」而不是「一键修」:体检与冲突都是只读面 —— 改哪一条、标哪一方
|
|
5130
|
+
// 由人定。智能体该拿到的是一份**可执行的清单**,而不是让面板替人去改文件。
|
|
5131
|
+
//
|
|
5132
|
+
// 为什么生成放在客户端、不新增 remote:整段文本只由「界面已经拿到的体检 / 冲突
|
|
5133
|
+
// 数据」拼出来,宿主半身一个字段都不用加 —— 升级界面不需要重启宿主,也不牵动
|
|
5134
|
+
// typert 契约(那套 manifest 按包名缓存、进程内不失效,加 wire 参数最容易出事)。
|
|
5135
|
+
//
|
|
5136
|
+
// **一颗按钮、两段内容**(2026-11-10 定):界面只留一颗「复制修复提示词」,
|
|
5137
|
+
// 但提示词里必须把 A(体检问题:要修)与 B(待裁决的矛盾:只判断)**分开写清楚** ——
|
|
5138
|
+
// 两段的纪律是相反的,混成一段智能体很容易把「只判断」做成「顺手改了」。
|
|
5139
|
+
//
|
|
5140
|
+
// 提示词正文**暂只出中文**:体检卡与冲突卡本身仍是硬编码中文(D31 遗留),
|
|
5141
|
+
// 半翻译的提示词只会更怪。按钮文案 / title / 提示条走 i18n,正文等 i18n 批次
|
|
5142
|
+
// 连同那两卡一起翻(见 docs/decisions.md D40)。
|
|
5143
|
+
|
|
5144
|
+
/** 每类最多列多少条:再多就让智能体自己看面板,不把剪贴板撑爆。 */
|
|
5145
|
+
const AUDIT_PROMPT_MAX_ITEMS = 50;
|
|
5146
|
+
|
|
5147
|
+
/**
|
|
5148
|
+
* 体检的九类硬问题(**唯一一份**:体检面板的分节与提示词共用)。
|
|
5149
|
+
*
|
|
5150
|
+
* 面板(`client/50-shared-ui.js` 的 `AuditPanel`)用 `title` + `sub` 渲染成
|
|
5151
|
+
* 「索引悬空(索引里有、文件没了)」,提示词用 `title` + `fix` 渲染成
|
|
5152
|
+
* 「【索引悬空】…怎么改」。**`item` 是两处共用的一份**:面板上看到的每一行,
|
|
5153
|
+
* 就是粘给智能体的那一行 —— 改一处不会只改半边。
|
|
5154
|
+
*
|
|
5155
|
+
* 顺序 = `lib/autodream/audit.js` 里 problems 的计数顺序,别按字母重排。
|
|
5156
|
+
*/
|
|
5157
|
+
const AUDIT_SECTIONS = [
|
|
5158
|
+
{
|
|
5159
|
+
field: "dangling", title: "索引悬空", sub: "索引里有、文件没了",
|
|
5160
|
+
fix: "索引里有、文件不存在 —— 从索引删掉这一行;若是文件被误删,把文件补回来",
|
|
5161
|
+
item: function (x) { return String(x.file == null ? "" : x.file); },
|
|
5162
|
+
},
|
|
5163
|
+
{
|
|
5164
|
+
field: "unlisted", title: "漏索引", sub: "文件在、索引里没有",
|
|
5165
|
+
fix: "文件在、索引里没有 —— 在索引里补一行 `- [标题](文件.md) — 一句话`",
|
|
5166
|
+
item: function (x) { return String(x.file == null ? "" : x.file); },
|
|
5167
|
+
},
|
|
5168
|
+
{
|
|
5169
|
+
field: "typeMismatch", title: "type 与前缀对不上", sub: "",
|
|
5170
|
+
fix: "本库以前缀为准,改 frontmatter 的 type(确实该换类型的再改文件名)",
|
|
5171
|
+
item: function (x) {
|
|
5172
|
+
return String(x.file == null ? "" : x.file) +
|
|
5173
|
+
" · type=" + String(x.type == null ? "" : x.type) +
|
|
5174
|
+
",文件名前缀=" + String(x.prefix == null ? "" : x.prefix);
|
|
5175
|
+
},
|
|
5176
|
+
},
|
|
5177
|
+
{
|
|
5178
|
+
field: "crlf", title: "行尾是 CRLF", sub: "",
|
|
5179
|
+
fix: "转成 LF,正文一个字节都不要改",
|
|
5180
|
+
item: function (x) { return String(x.file == null ? "" : x.file); },
|
|
5181
|
+
},
|
|
5182
|
+
{
|
|
5183
|
+
field: "brokenLinks", title: "双链断链", sub: "",
|
|
5184
|
+
fix: "改指向真实存在的文件名,或去掉链接只留文字",
|
|
5185
|
+
item: function (x) {
|
|
5186
|
+
return String(x.file == null ? "" : x.file) + " → [[" + String(x.target == null ? "" : x.target) + "]]";
|
|
5187
|
+
},
|
|
5188
|
+
},
|
|
5189
|
+
{
|
|
5190
|
+
field: "hashHazards", title: "未加引号的「空格+#」", sub: "",
|
|
5191
|
+
fix: "这个值会被 YAML 当注释吞掉 —— 给它加引号",
|
|
5192
|
+
item: function (x) {
|
|
5193
|
+
return String(x.file == null ? "" : x.file) + " · " + String(x.line == null ? "" : x.line);
|
|
5194
|
+
},
|
|
5195
|
+
},
|
|
5196
|
+
{
|
|
5197
|
+
field: "noFrontmatter", title: "没有 frontmatter", sub: "",
|
|
5198
|
+
fix: "按该文件的类型补 name / description / metadata",
|
|
5199
|
+
item: function (x) { return String(x.file == null ? "" : x.file); },
|
|
5200
|
+
},
|
|
5201
|
+
{
|
|
5202
|
+
field: "unreadable", title: "读不出来", sub: "",
|
|
5203
|
+
fix: "先报给我,不要凭猜重写",
|
|
5204
|
+
item: function (x) { return String(x.file == null ? "" : x.file); },
|
|
5205
|
+
},
|
|
5206
|
+
{
|
|
5207
|
+
// 归档中途崩溃留下的私有名残留:它不是一条记忆(扫描 / 注入都不认),
|
|
5208
|
+
// 但里面可能是用户唯一一份内容 —— 所以要在体检里露头,而不是隐形。
|
|
5209
|
+
field: "strays", title: "归档残留", sub: "内容可能还在,请确认后改回 .md 或删除",
|
|
5210
|
+
fix: "归档中途崩溃留下的私有名,内容可能还在 —— 先问我,再决定改回 .md 还是删除",
|
|
5211
|
+
item: function (x) { return String(x.file == null ? "" : x.file); },
|
|
5212
|
+
},
|
|
5213
|
+
];
|
|
5214
|
+
|
|
5215
|
+
/** 「指向已归档」是提示、不计入 problems —— 面板与提示词各用一句自己的标题,条目格式共用。 */
|
|
5216
|
+
const AUDIT_ARCHIVED_LINKS = {
|
|
5217
|
+
field: "archivedLinks",
|
|
5218
|
+
item: function (x) {
|
|
5219
|
+
return String(x.file == null ? "" : x.file) + " → [[" + String(x.target == null ? "" : x.target) + "]]";
|
|
5220
|
+
},
|
|
5221
|
+
};
|
|
5222
|
+
|
|
5223
|
+
/** 提示词里那一节的标题与口径(面板上写的是「不计入问题」,提示词里写「不是错误」)。 */
|
|
5224
|
+
const AUDIT_PROMPT_HINT = {
|
|
5225
|
+
field: AUDIT_ARCHIVED_LINKS.field,
|
|
5226
|
+
title: "指向已归档(提示,不是错误)",
|
|
5227
|
+
fix: "归档不是删除,目标还在档案馆里 —— 能改成指向现有活动记忆就改;改不了就保持原样,不要为此恢复归档,也不要删掉链接所在的记忆",
|
|
5228
|
+
item: AUDIT_ARCHIVED_LINKS.item,
|
|
5229
|
+
};
|
|
5230
|
+
|
|
5231
|
+
/** 冲突类型 → 中文标签(提示词正文只出中文,所以不走界面那份 i18n 键)。 */
|
|
5232
|
+
const CONFLICT_KIND_ZH = {
|
|
5233
|
+
contradiction: "矛盾",
|
|
5234
|
+
stale_version: "版本冲突", staleVersion: "版本冲突",
|
|
5235
|
+
external_change: "外部改动", externalChange: "外部改动",
|
|
5236
|
+
unresolved_merge: "合并未完成", unresolvedMerge: "合并未完成",
|
|
5237
|
+
};
|
|
5238
|
+
|
|
5239
|
+
/** 统计行里的数字:缺字段写 `?`,别把它渲染成 0(0 是「查过、确实没有」)。 */
|
|
5240
|
+
function auditPromptNum(v) {
|
|
5241
|
+
return typeof v === "number" && isFinite(v) ? String(v) : "?";
|
|
5242
|
+
}
|
|
5243
|
+
|
|
5244
|
+
/**
|
|
5245
|
+
* 把一类条目写进 lines;返回这一波一共列了几条。
|
|
5246
|
+
*
|
|
5247
|
+
* 空类**整节不出现**(面板上不显示的分类,提示词里也不该占位),超过上限截断
|
|
5248
|
+
* 并留一句「还有 N 条」—— 悄悄截断会让智能体以为改完了。
|
|
5249
|
+
*/
|
|
5250
|
+
function appendPromptGroups(lines, groups, report) {
|
|
5251
|
+
let listed = 0;
|
|
5252
|
+
groups.forEach(function (g) {
|
|
5253
|
+
const raw = report && Array.isArray(report[g.field]) ? report[g.field] : [];
|
|
5254
|
+
const items = raw.filter(function (x) { return x && typeof x === "object"; });
|
|
5255
|
+
if (!items.length) return;
|
|
5256
|
+
lines.push("");
|
|
5257
|
+
lines.push("【" + g.title + "】" + g.fix + "(" + items.length + " 条)");
|
|
5258
|
+
items.slice(0, AUDIT_PROMPT_MAX_ITEMS).forEach(function (x) {
|
|
5259
|
+
lines.push("- " + g.item(x));
|
|
5260
|
+
});
|
|
5261
|
+
if (items.length > AUDIT_PROMPT_MAX_ITEMS) {
|
|
5262
|
+
lines.push("- …(还有 " + (items.length - AUDIT_PROMPT_MAX_ITEMS) + " 条,见面板)");
|
|
5263
|
+
}
|
|
5264
|
+
listed += items.length;
|
|
5265
|
+
});
|
|
5266
|
+
return listed;
|
|
5267
|
+
}
|
|
5268
|
+
|
|
5269
|
+
/**
|
|
5270
|
+
* A 段(体检问题)的正文行。
|
|
5271
|
+
*
|
|
5272
|
+
* `problems <= 0` 时返回 null:整段不出现,一颗空按钮也不该冒出来。
|
|
5273
|
+
* 返回的是**正文**(统计 + 口径 + 分类 + 收尾),不带自己的开场白 ——
|
|
5274
|
+
* 开场白由 `buildHealthFixPrompt` 统一写,免得合成一颗按钮之后出现两句「请修复」。
|
|
5275
|
+
*/
|
|
5276
|
+
function auditFixLines(report) {
|
|
5277
|
+
const r = report && typeof report === "object" ? report : null;
|
|
5278
|
+
if (!r) return null;
|
|
5279
|
+
const problems = typeof r.problems === "number" && isFinite(r.problems) ? r.problems : 0;
|
|
5280
|
+
if (problems <= 0) return null;
|
|
5281
|
+
|
|
5282
|
+
const lines = [];
|
|
5283
|
+
// 不在这里写「记忆目录」—— 那是整段提示词的开头(`buildHealthFixPrompt` 统一写一次),
|
|
5284
|
+
// 两段各写一遍会在合成后的文本里出现两次同样的路径。
|
|
5285
|
+
lines.push(
|
|
5286
|
+
"扫描 " + auditPromptNum(r.fileCount) + " 个记忆 · 索引 " + auditPromptNum(r.indexEntries) +
|
|
5287
|
+
" 条 · 档案馆 " + auditPromptNum(r.archivedCount) + " 条 · 问题合计 " + problems,
|
|
5288
|
+
);
|
|
5289
|
+
lines.push("");
|
|
5290
|
+
lines.push("修复口径:");
|
|
5291
|
+
// 工具名写成条件句:聊天会话里的智能体通常只有 search_memory / read_memory,
|
|
5292
|
+
// write_memory / archive_memory 属 autodream 工具面 —— 不能假设它一定拿得到。
|
|
5293
|
+
lines.push("1. 能直接改就改(有 sage-mem 的 write_memory / archive_memory 就用它们;没有就直接编辑文件),改完把每条的处理结果列给我。");
|
|
5294
|
+
lines.push("2. 只改清单里指出的那一点:正文与其它 frontmatter 字段不要顺手动,行尾保持 LF。");
|
|
5295
|
+
lines.push("3. 不删记忆:要下线用归档,不要删文件;「归档残留」这一类先问我再动。");
|
|
5296
|
+
lines.push("4. 拿不准的(读不出来、type 该怎么统一)先说不确定,不要猜着改。");
|
|
5297
|
+
|
|
5298
|
+
const listed = appendPromptGroups(lines, AUDIT_SECTIONS, r);
|
|
5299
|
+
const hinted = appendPromptGroups(lines, [AUDIT_PROMPT_HINT], r);
|
|
5300
|
+
lines.push("");
|
|
5301
|
+
lines.push("改完请自己再核对一遍(能跑 audit_memory 就跑),然后告诉我每条改了什么。");
|
|
5302
|
+
return { lines: lines, count: listed, hints: hinted };
|
|
5303
|
+
}
|
|
5304
|
+
|
|
5305
|
+
/**
|
|
5306
|
+
* B 段(待裁决的矛盾)的正文行。
|
|
5307
|
+
*
|
|
5308
|
+
* 只取**待裁决**的记录;一份都没有就返回 null。措辞刻意把裁决权留在人手里
|
|
5309
|
+
* (标失效只写一行 frontmatter,真正点按钮的是我)—— 提示词不是授权书。
|
|
5310
|
+
*/
|
|
5311
|
+
function conflictFixLines(data) {
|
|
5312
|
+
const d = data && typeof data === "object" ? data : null;
|
|
5313
|
+
const all = d && Array.isArray(d.items) ? d.items : [];
|
|
5314
|
+
const items = all.filter(function (x) { return x && typeof x === "object" && x.status === "open"; });
|
|
5315
|
+
if (!items.length) return null;
|
|
5316
|
+
|
|
5317
|
+
const lines = [];
|
|
5318
|
+
lines.push("请对每一条:读这两份正文,用一两句话说清「哪一份过时 / 两边说的其实是不同的事」,并给出你的建议 —— 标哪一方失效,还是两边都留着。拿不准就直说不确定。");
|
|
5319
|
+
lines.push("");
|
|
5320
|
+
items.slice(0, AUDIT_PROMPT_MAX_ITEMS).forEach(function (x, i) {
|
|
5321
|
+
const kind = CONFLICT_KIND_ZH[String(x.kind)] || String(x.kind || "矛盾");
|
|
5322
|
+
const pair = x.otherFile
|
|
5323
|
+
? String(x.file == null ? "" : x.file) + " ↔ " + String(x.otherFile)
|
|
5324
|
+
: String(x.file == null ? "" : x.file);
|
|
5325
|
+
lines.push((i + 1) + ". " + kind + " · " + pair + "(记录 " + String(x.id == null ? "" : x.id) + ")");
|
|
5326
|
+
if (x.note) lines.push(" 备注:" + String(x.note));
|
|
5327
|
+
});
|
|
5328
|
+
if (items.length > AUDIT_PROMPT_MAX_ITEMS) {
|
|
5329
|
+
lines.push("…(还有 " + (items.length - AUDIT_PROMPT_MAX_ITEMS) + " 条,见面板)");
|
|
5330
|
+
}
|
|
5331
|
+
return { lines: lines, count: items.length };
|
|
5332
|
+
}
|
|
5333
|
+
|
|
5334
|
+
/**
|
|
5335
|
+
* 「库健康」一屏 → 一段提示词(**全场唯一入口**,对应界面上那一颗按钮)。
|
|
5336
|
+
*
|
|
5337
|
+
* 两段**分开写**:A 是体检问题(要修),B 是待裁决的矛盾(只判断)。开头的
|
|
5338
|
+
* 一句话先说清这次有哪几段、没什么 —— 智能体不该靠猜「为什么 B 不见了」。
|
|
5339
|
+
* 两边都没有时返回 null:界面据此不渲染那颗按钮。
|
|
5340
|
+
*/
|
|
5341
|
+
function buildHealthFixPrompt(input) {
|
|
5342
|
+
const src = input && typeof input === "object" ? input : {};
|
|
5343
|
+
const audit = src.report;
|
|
5344
|
+
const a = auditFixLines(audit);
|
|
5345
|
+
const b = conflictFixLines(src.conflicts);
|
|
5346
|
+
if (!a && !b) return null;
|
|
5347
|
+
|
|
5348
|
+
const dir = audit && typeof audit.dir === "string" ? audit.dir : "";
|
|
5349
|
+
const lines = [];
|
|
5350
|
+
lines.push("sage-mem「库健康」这一屏要处理的事,分两部分;两部分的纪律不一样,别混。");
|
|
5351
|
+
lines.push("");
|
|
5352
|
+
if (a && b) {
|
|
5353
|
+
lines.push("这次两部分都有:A 是体检发现的结构问题(**要修**),B 是待裁决的矛盾(**只做判断,不要改盘上的文件**)。");
|
|
5354
|
+
} else if (a) {
|
|
5355
|
+
lines.push("这次只有 A:体检发现的结构问题(**要修**);当前没有待裁决的矛盾。");
|
|
5356
|
+
} else {
|
|
5357
|
+
lines.push("这次只有 B:待裁决的矛盾(**只做判断,不要改盘上的文件**);体检没有发现问题。");
|
|
5358
|
+
}
|
|
5359
|
+
if (dir) lines.push("记忆目录:" + dir);
|
|
5360
|
+
|
|
5361
|
+
if (a) {
|
|
5362
|
+
lines.push("");
|
|
5363
|
+
lines.push("════ A · 体检问题:按下面的清单修 ════");
|
|
5364
|
+
Array.prototype.push.apply(lines, a.lines);
|
|
5365
|
+
}
|
|
5366
|
+
if (b) {
|
|
5367
|
+
lines.push("");
|
|
5368
|
+
lines.push("════ B · 待裁决的矛盾:只给判断,不要改盘 ════");
|
|
5369
|
+
lines.push("这些要我来裁决 —— 你只做判断,不要改盘上的文件,也不要把任何一方标失效。面板里的「标失效」只往 frontmatter 加一行 invalid_at、不动正文;点哪颗按钮由我来。");
|
|
5370
|
+
Array.prototype.push.apply(lines, b.lines);
|
|
5371
|
+
}
|
|
5372
|
+
return {
|
|
5373
|
+
text: lines.join("\n"),
|
|
5374
|
+
count: (a ? a.count : 0) + (b ? b.count : 0),
|
|
5375
|
+
hasAudit: !!a,
|
|
5376
|
+
hasConflict: !!b,
|
|
5377
|
+
};
|
|
5378
|
+
}
|
|
5379
|
+
|
|
5380
|
+
/**
|
|
5381
|
+
* 写剪贴板:优先异步 Clipboard API,失败降级到隐藏 textarea + execCommand。
|
|
5382
|
+
*
|
|
5383
|
+
* 两条都不承诺「一定成功」—— 局域网 http 部署不是安全上下文,两条都可能被拒;
|
|
5384
|
+
* 调用方拿到 `false` 必须把文本摊出来让人手抄(`PromptFallbackModal`),
|
|
5385
|
+
* 绝不静默失败(D25:点了没反应是最坏的失败形态)。
|
|
5386
|
+
*/
|
|
5387
|
+
function copyPromptText(text) {
|
|
5388
|
+
const value = String(text == null ? "" : text);
|
|
5389
|
+
let clip = null;
|
|
5390
|
+
try {
|
|
5391
|
+
if (typeof navigator !== "undefined" && navigator && navigator.clipboard &&
|
|
5392
|
+
typeof navigator.clipboard.writeText === "function") {
|
|
5393
|
+
clip = navigator.clipboard;
|
|
5394
|
+
}
|
|
5395
|
+
} catch (e) { clip = null; }
|
|
5396
|
+
if (clip) {
|
|
5397
|
+
return Promise.resolve()
|
|
5398
|
+
.then(function () { return clip.writeText(value); })
|
|
5399
|
+
.then(function () { return true; })
|
|
5400
|
+
.catch(function () { return legacyCopyPrompt(value); });
|
|
5401
|
+
}
|
|
5402
|
+
return Promise.resolve(legacyCopyPrompt(value));
|
|
5403
|
+
}
|
|
5404
|
+
|
|
5405
|
+
/** 降级路径:老的 textarea + execCommand(已废弃但仍是 http 页面唯一的出路)。 */
|
|
5406
|
+
function legacyCopyPrompt(value) {
|
|
5407
|
+
try {
|
|
5408
|
+
if (typeof document === "undefined" || !document || !document.body) return false;
|
|
5409
|
+
const ta = document.createElement("textarea");
|
|
5410
|
+
ta.value = value;
|
|
5411
|
+
ta.setAttribute("readonly", "readonly");
|
|
5412
|
+
ta.style.position = "fixed";
|
|
5413
|
+
ta.style.left = "-9999px";
|
|
5414
|
+
ta.style.top = "0";
|
|
5415
|
+
document.body.appendChild(ta);
|
|
5416
|
+
if (typeof ta.select === "function") ta.select();
|
|
5417
|
+
const ok = typeof document.execCommand === "function" && document.execCommand("copy") === true;
|
|
5418
|
+
document.body.removeChild(ta);
|
|
5419
|
+
return ok;
|
|
5420
|
+
} catch (e) {
|
|
5421
|
+
return false;
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
5424
|
+
|
|
5425
|
+
/**
|
|
5426
|
+
* 复制失败时的兜底:把提示词摊在一只只读 textarea 里,点一下即全选。
|
|
5427
|
+
*
|
|
5428
|
+
* 用户手动关(× / 关闭 / 点遮罩)—— 与面板里其它模态同一套壳。
|
|
5429
|
+
*/
|
|
5430
|
+
function PromptFallbackModal(props) {
|
|
5431
|
+
return h("div", { className: "smem-modal-host" }, [
|
|
5432
|
+
h("div", { className: "smem-modal-scrim", key: "scrim", onClick: props.onClose }),
|
|
5433
|
+
h("div", {
|
|
5434
|
+
className: "smem-modal", key: "m",
|
|
5435
|
+
role: "dialog", "aria-modal": "true",
|
|
5436
|
+
"aria-label": t("prompt.copyFailedTitle"),
|
|
5437
|
+
}, [
|
|
5438
|
+
h("div", { className: "smem-modal-title", key: "t" }, t("prompt.copyFailedTitle")),
|
|
5439
|
+
h("div", { className: "smem-modal-body", key: "b" }, [
|
|
5440
|
+
h("textarea", {
|
|
5441
|
+
className: "smem-textarea smem-prompt-ta", key: "ta",
|
|
5442
|
+
readOnly: true, rows: 12, value: String(props.text == null ? "" : props.text),
|
|
5443
|
+
onFocus: function (e) {
|
|
5444
|
+
if (e && e.target && typeof e.target.select === "function") e.target.select();
|
|
5445
|
+
},
|
|
5446
|
+
}),
|
|
5447
|
+
h("div", { className: "smem-ar-hint", key: "h" }, t("prompt.copyFailedHint")),
|
|
5448
|
+
]),
|
|
5449
|
+
h("div", { className: "smem-modal-foot", key: "f" }, [
|
|
5450
|
+
h("button", { type: "button", className: "smem-btn", key: "c", onClick: props.onClose }, t("prompt.close")),
|
|
5451
|
+
]),
|
|
5452
|
+
]),
|
|
5453
|
+
]);
|
|
5454
|
+
}
|
|
5081
5455
|
/**
|
|
5082
5456
|
* 「自动做梦」面板 —— 让记忆自己整理一遍(引擎在 宿主侧 lib/autodream.js)。
|
|
5083
5457
|
*
|
|
@@ -5122,9 +5496,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5122
5496
|
const sLimitsDraft = react.useState(null); const limitsDraft = sLimitsDraft[0], setLimitsDraft = sLimitsDraft[1];
|
|
5123
5497
|
const sSaveBusy = react.useState(false); const saveBusy = sSaveBusy[0], setSaveBusy = sSaveBusy[1];
|
|
5124
5498
|
const sReservedNew = react.useState(""); const reservedNew = sReservedNew[0], setReservedNew = sReservedNew[1];
|
|
5125
|
-
// ──
|
|
5126
|
-
const
|
|
5127
|
-
const
|
|
5499
|
+
// ── 2.7.0 界面入口三选一(客户端行为:改完立刻生效,不需要重启)──
|
|
5500
|
+
const sEntryBusy = react.useState(false); const entryBusy = sEntryBusy[0], setEntryBusy = sEntryBusy[1];
|
|
5501
|
+
const sEntryErr = react.useState(""); const entryErr = sEntryErr[0], setEntryErr = sEntryErr[1];
|
|
5128
5502
|
|
|
5129
5503
|
// 派生量放在 effect 之前:下面几个 effect 的依赖数组要在渲染期就能取到值。
|
|
5130
5504
|
const c = cfg || {};
|
|
@@ -5139,8 +5513,11 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5139
5513
|
const fixedReserved = ((memSet && memSet.reserved) || []).filter(function (n) {
|
|
5140
5514
|
return extraLower.indexOf(String(n).toLowerCase()) < 0;
|
|
5141
5515
|
});
|
|
5142
|
-
//
|
|
5143
|
-
|
|
5516
|
+
// 入口模式:新字段优先,旧 sidebarEntry 兜底;坏值 / 旧宿主一律按「两个都显示」。
|
|
5517
|
+
// 存了 sidebar 但当前宿主不支持侧栏时,界面按 settings 显示并给一条降级提示。
|
|
5518
|
+
const entryModeValue = entryModeFromSettings(memSet);
|
|
5519
|
+
const entryShown = entryModeValue === "sidebar" && !sidebarEntryAvailable() ? "settings" : entryModeValue;
|
|
5520
|
+
const entryIsDegraded = entryModeValue === "sidebar" && !sidebarEntryAvailable();
|
|
5144
5521
|
|
|
5145
5522
|
react.useEffect(function () {
|
|
5146
5523
|
if (!remote) { setErr(t("ad.err.remoteUnavailable")); return undefined; }
|
|
@@ -5272,28 +5649,29 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5272
5649
|
}
|
|
5273
5650
|
|
|
5274
5651
|
/**
|
|
5275
|
-
*
|
|
5652
|
+
* 界面入口三选一(2.7.0)。
|
|
5276
5653
|
*
|
|
5277
|
-
*
|
|
5278
|
-
* 失败只报错、不改任何东西 ——
|
|
5279
|
-
*
|
|
5654
|
+
* 顺序仍是「先写设置、成功后再动槽位」:设置没落盘就不该让界面先变一个样。
|
|
5655
|
+
* 失败只报错、不改任何东西 —— 选项由 memSet 派生,于是自动回到原值。
|
|
5656
|
+
* 成功后 saveEntryMode 内部会调 applyEntryMode:**立即**增删槽位,不刷新、不重启。
|
|
5280
5657
|
*/
|
|
5281
|
-
function
|
|
5658
|
+
function saveEntry(next) {
|
|
5282
5659
|
if (!memRemote || typeof memRemote.setSettings !== "function") {
|
|
5283
|
-
|
|
5660
|
+
setEntryErr(t("entry.err.noSetSettings"));
|
|
5284
5661
|
return;
|
|
5285
5662
|
}
|
|
5286
|
-
if (
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5663
|
+
if (entryBusy) return;
|
|
5664
|
+
setEntryBusy(true); setEntryErr(""); setNotice("");
|
|
5665
|
+
saveEntryMode(memRemote, next).then(function (out) {
|
|
5666
|
+
setEntryBusy(false);
|
|
5667
|
+
if (!out || out.ok === false) {
|
|
5668
|
+
setEntryErr((out && out.error) || t("ad.err.saveRejected"));
|
|
5669
|
+
return;
|
|
5670
|
+
}
|
|
5671
|
+
if (out.settings) setMemSet(out.settings);
|
|
5293
5672
|
else loadMemSettings();
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
}, function (e) { setSidebarBusy(false); setSidebarErr(detail(e)); });
|
|
5673
|
+
setNotice(t("entry.notice.saved"));
|
|
5674
|
+
}, function (e) { setEntryBusy(false); setEntryErr(detail(e)); });
|
|
5297
5675
|
}
|
|
5298
5676
|
|
|
5299
5677
|
/**
|
|
@@ -5445,7 +5823,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5445
5823
|
// 模块卡头(2.5.0 修):开关 + 标题 + 状态**必须活在分段之外**。
|
|
5446
5824
|
// 它回答的是「这是哪个模块、现在是开是关」,和「这次看哪一段」是两个维度;
|
|
5447
5825
|
// 原先它长在「运行」段里,切到设置 / 记录就一起消失 —— 于是下面那张
|
|
5448
|
-
//
|
|
5826
|
+
// 自动归档的卡看起来像是当前这一段的内容(2026-11-08 一眼看到的就是这个)。
|
|
5449
5827
|
h("div", { className: "smem-autodream-sec smem-module-head", key: "top" }, [
|
|
5450
5828
|
h("div", { className: "smem-autodream-row", key: "r" }, [
|
|
5451
5829
|
h(Switch, {
|
|
@@ -5586,24 +5964,24 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5586
5964
|
]),
|
|
5587
5965
|
]) : null,
|
|
5588
5966
|
segOf === "settings" ? h("div", { key: "seg-set" }, [
|
|
5589
|
-
//
|
|
5590
|
-
//
|
|
5591
|
-
h("div", { className: "smem-autodream-sec", key: "
|
|
5967
|
+
// 2.7.0:界面入口三选一。至少保留一个(三选一从类型上就排除了「都不显示」)。
|
|
5968
|
+
// 这里与插件详情页共用同一个 EntryChooser 与 applyEntryMode。
|
|
5969
|
+
h("div", { className: "smem-autodream-sec", key: "entry" }, [
|
|
5592
5970
|
h("div", { className: "smem-autodream-row", key: "h" }, [
|
|
5593
|
-
h("span", { key: "t", style: { fontWeight: "600", fontSize: "12.5px" } }, t("
|
|
5594
|
-
h("span", { className: "smem-status", key: "s" },
|
|
5971
|
+
h("span", { key: "t", style: { fontWeight: "600", fontSize: "12.5px" } }, t("entry.title")),
|
|
5972
|
+
h("span", { className: "smem-status", key: "s" }, entryStatusLabel(entryShown)),
|
|
5595
5973
|
]),
|
|
5596
|
-
h("div", { className: "smem-autodream-note", key: "n" }, t("
|
|
5597
|
-
h(
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
}
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5974
|
+
h("div", { className: "smem-autodream-note", key: "n" }, t("entry.subtitle")),
|
|
5975
|
+
h(EntryChooser, {
|
|
5976
|
+
key: "opts", mode: entryShown, busy: entryBusy,
|
|
5977
|
+
sidebarOk: sidebarEntryAvailable(), modernApi: entryApiModern,
|
|
5978
|
+
onPick: saveEntry,
|
|
5979
|
+
}),
|
|
5980
|
+
(entryIsDegraded || entryApiModern === false)
|
|
5981
|
+
? h("div", { className: "smem-autodream-note smem-ad-warn", key: "deg" },
|
|
5982
|
+
t(entryApiModern === false ? "entry.legacyHost" : "entry.degraded"))
|
|
5983
|
+
: null,
|
|
5984
|
+
entryErr ? h("div", { className: "smem-autodream-note smem-ad-warn", key: "err" }, entryErr) : null,
|
|
5607
5985
|
]),
|
|
5608
5986
|
// 自动归档那一段(三档策略 + 三个阈值)**已挪到「自动归档」页签**(v0.9.9):
|
|
5609
5987
|
// 归档是确定性规则、不调模型,和这一页的「让模型整理」不是一件事;
|
|
@@ -5953,7 +6331,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5953
6331
|
// 根节点带一个稳定的类名(2.5.0):它现在与「自动做梦」同屏堆叠,
|
|
5954
6332
|
// 断言与样式都得能把这一张卡与旁边那张分开 —— 没有类名就只能按整棵树找文本。
|
|
5955
6333
|
//
|
|
5956
|
-
// 2.5.0
|
|
6334
|
+
// 2.5.0 补(2026-11-08 提的结构):这张卡也要有自己的「运行 / 设置」分段。
|
|
5957
6335
|
// 并屏之后两张卡形状必须一样 —— 否则「设置」那一段里出现阈值、档位、说明混在一起,
|
|
5958
6336
|
// 读起来像没分过类。分法照自动做梦那张卡:
|
|
5959
6337
|
// 运行 = 它做什么 + 现在什么档(要看状态、要改档位的人在这一段)
|
|
@@ -6349,6 +6727,11 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
6349
6727
|
const audit = sAudit[0], setAudit = sAudit[1];
|
|
6350
6728
|
const sAuditBusy = react.useState(false);
|
|
6351
6729
|
const auditBusy = sAuditBusy[0], setAuditBusy = sAuditBusy[1];
|
|
6730
|
+
// 2.6.0:复制修复提示词的回执(null = 不显示)与复制失败时的兜底模态。
|
|
6731
|
+
const sToast = react.useState(null);
|
|
6732
|
+
const toast = sToast[0], setToast = sToast[1];
|
|
6733
|
+
const sPromptFallback = react.useState(null);
|
|
6734
|
+
const promptFallback = sPromptFallback[0], setPromptFallback = sPromptFallback[1];
|
|
6352
6735
|
// 更新提示:只在**确实有新版本**时才被赋值(查不到/限流/网络错都不赋值)
|
|
6353
6736
|
const sUpdate = react.useState(null);
|
|
6354
6737
|
const updateInfo = sUpdate[0], setUpdateInfo = sUpdate[1];
|
|
@@ -6462,6 +6845,24 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
6462
6845
|
}
|
|
6463
6846
|
}, []);
|
|
6464
6847
|
|
|
6848
|
+
/**
|
|
6849
|
+
* 复制提示词的回执(2.6.0):10 秒后自动收掉。
|
|
6850
|
+
*
|
|
6851
|
+
* 计时器挂在 `toast` 上:再复制一次只是换文本 → 依赖变 → 旧计时器清掉、重新计时。
|
|
6852
|
+
* 卸载时 cleanup 清掉,不留悬挂定时器(面板关掉之后再去 setState 是纯净的噪音)。
|
|
6853
|
+
*/
|
|
6854
|
+
react.useEffect(function () {
|
|
6855
|
+
if (!toast) return undefined;
|
|
6856
|
+
const id = setTimeout(function () { setToast(null); }, 10000);
|
|
6857
|
+
return function () { clearTimeout(id); };
|
|
6858
|
+
}, [toast]);
|
|
6859
|
+
|
|
6860
|
+
/**
|
|
6861
|
+
* 换页签立刻收掉回执 —— 用 effect 盯 `tab`,而不是在每个 setTab 的调用点里塞
|
|
6862
|
+
* `setToast(null)`:入口只会越来越多(徽标、抽屉、侧栏),漏一个就是「切了还在」。
|
|
6863
|
+
*/
|
|
6864
|
+
react.useEffect(function () { setToast(null); }, [tab]);
|
|
6865
|
+
|
|
6465
6866
|
/**
|
|
6466
6867
|
* 抽屉「点外部 / Esc 关闭」。
|
|
6467
6868
|
*
|
|
@@ -6892,6 +7293,25 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
6892
7293
|
.then(function () { setAuditBusy(false); });
|
|
6893
7294
|
}
|
|
6894
7295
|
|
|
7296
|
+
/**
|
|
7297
|
+
* 把「库健康这一屏要处理的事」复制成一段提示词(2.6.0)。
|
|
7298
|
+
*
|
|
7299
|
+
* 界面上只有**一颗**按钮,提示词里却是**两段**(A 体检问题要修 / B 待裁决只判断)——
|
|
7300
|
+
* 分不分得清由 `buildHealthFixPrompt` 负责,这里只管复制与失败兜底。
|
|
7301
|
+
*
|
|
7302
|
+
* `built` 在两边都没事时是 null;按钮本就不会渲染,这里再挡一次,防将来有别的入口。
|
|
7303
|
+
* 复制失败**不静默**:把提示词摊进模态框让人手抄(局域网 http 不是安全上下文,
|
|
7304
|
+
* Clipboard API 与 execCommand 都可能被拒 —— 「点了没反应」是最坏的形态)。
|
|
7305
|
+
*/
|
|
7306
|
+
function copyHealthPrompt() {
|
|
7307
|
+
const built = buildHealthFixPrompt({ report: audit, conflicts: conf });
|
|
7308
|
+
if (!built) return;
|
|
7309
|
+
copyPromptText(built.text).then(function (ok) {
|
|
7310
|
+
if (ok) { setToast(t("toast.copied")); return; }
|
|
7311
|
+
setPromptFallback(built.text);
|
|
7312
|
+
});
|
|
7313
|
+
}
|
|
7314
|
+
|
|
6895
7315
|
function loadCandidates() {
|
|
6896
7316
|
if (typeof remote.archiveCandidates !== "function") { setError(t("error.noArchiveCandidates")); return; }
|
|
6897
7317
|
setCandBusy(true); setError(null);
|
|
@@ -7344,6 +7764,10 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
7344
7764
|
// 冲突徽标数的是**待裁决**(open)条数 —— 与冲突页签里的「N 条待裁决」同源。
|
|
7345
7765
|
// -1 = 还没取到队列(显示「未载」,不假装 0 条)。
|
|
7346
7766
|
const conflictOpen = conf && conf.counts && typeof conf.counts.open === "number" ? conf.counts.open : -1;
|
|
7767
|
+
// 「复制修复提示词」那颗按钮的分母:体检问题 + 待裁决冲突。两边都没有(=0)时按钮不渲染;
|
|
7768
|
+
// 冲突队列还没载入(-1)不算进分母 —— 免得按钮先冒出来又消失。
|
|
7769
|
+
const healthWork = (audit && typeof audit.problems === "number" && audit.problems > 0 ? audit.problems : 0)
|
|
7770
|
+
+ (conflictOpen > 0 ? conflictOpen : 0);
|
|
7347
7771
|
|
|
7348
7772
|
// 「文件列表」这一屏:状态筛选决定看哪一批,灰色卡片与留痕块自带「退役」语义。
|
|
7349
7773
|
// banner 只在**专门看归档**时出现,切到全部时不重复说一遍。
|
|
@@ -7407,6 +7831,17 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
7407
7831
|
: conflictOpen > 0 ? t("header.conflictOpen", { count: conflictOpen })
|
|
7408
7832
|
: t("header.conflictEmpty"))
|
|
7409
7833
|
: null,
|
|
7834
|
+
// 2.6.0:**一颗**按钮收在两组徽标之后(2026-11-10 定:保持界面简洁)。
|
|
7835
|
+
// 只在真有内容时出现(体检有问题 或 冲突待裁决);提示词里 A / B 两段分开写,
|
|
7836
|
+
// 所以一颗按钮不会把「要修」与「只判断」混成一件事。
|
|
7837
|
+
(healthWork > 0)
|
|
7838
|
+
? h("button", {
|
|
7839
|
+
type: "button", className: "smem-btn", key: "cp",
|
|
7840
|
+
title: t("header.copyHealthTitle", { count: healthWork }),
|
|
7841
|
+
"aria-label": t("header.copyHealthTitle", { count: healthWork }),
|
|
7842
|
+
onClick: copyHealthPrompt,
|
|
7843
|
+
}, t("header.copyFixPrompt"))
|
|
7844
|
+
: null,
|
|
7410
7845
|
restartPending
|
|
7411
7846
|
? h("button", {
|
|
7412
7847
|
type: "button", key: "rs", className: "smem-tool-badge smem-ad-badge--warn",
|
|
@@ -7640,6 +8075,12 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
7640
8075
|
h(AuditPanel, { key: "audit", report: audit, busy: auditBusy, onRun: runAudit, standalone: true }),
|
|
7641
8076
|
])
|
|
7642
8077
|
: h("div", { className: "smem-view", key: "files" }, filesView),
|
|
8078
|
+
// 2.6.0:复制成功的回执铺在视口下方中央(`position:fixed`,不吃点击,10s 自收,
|
|
8079
|
+
// 也能自己点 × 关)。渲染在 `.smem-root` 直属层 —— 别塞进带进场动画的
|
|
8080
|
+
// `.smem-view`(那个祖先有 transform,`fixed` 会被改写定位)。
|
|
8081
|
+
toast ? h(Toast, { key: "toast", text: toast, closeTitle: t("prompt.close"), onClose: function () { setToast(null); } }) : null,
|
|
8082
|
+
// 复制失败时把提示词摊出来手抄;用户手动关(× / 关闭 / 点遮罩)。
|
|
8083
|
+
promptFallback ? h(PromptFallbackModal, { key: "prompt-fallback", text: promptFallback, onClose: function () { setPromptFallback(null); } }) : null,
|
|
7643
8084
|
// 底部一行仓库链接(四个 tab 都在,不抢视线)
|
|
7644
8085
|
h(StarLine, { key: "star-line" }),
|
|
7645
8086
|
]);
|
|
@@ -7693,7 +8134,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
7693
8134
|
|
|
7694
8135
|
/** 卡片里的错误文案:remote 可能 reject 一个字符串 / 对象 / Error,尽量展开成人话。 */
|
|
7695
8136
|
function pluginCardErr(e) {
|
|
7696
|
-
if (!e) return t("
|
|
8137
|
+
if (!e) return t("entry.err.saveFailed");
|
|
7697
8138
|
if (e instanceof Error) return e.message || String(e);
|
|
7698
8139
|
try {
|
|
7699
8140
|
const s = JSON.stringify(e);
|
|
@@ -7719,20 +8160,16 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
7719
8160
|
const ctx = props && props.ctx;
|
|
7720
8161
|
// 必须走 stableRemote:真实宿主里 ctx.get() 每次返回新对象,进依赖数组会无限重跑。
|
|
7721
8162
|
const remote = stableRemote(ctx, "remote.memory");
|
|
7722
|
-
const
|
|
8163
|
+
const sOpen = react.useState(false); const open = sOpen[0], setOpen = sOpen[1];
|
|
8164
|
+
const sMode = react.useState("both"); const mode = sMode[0], setMode = sMode[1];
|
|
7723
8165
|
const sBusy = react.useState(false); const busy = sBusy[0], setBusy = sBusy[1];
|
|
7724
8166
|
const sErr = react.useState(""); const err = sErr[0], setErr = sErr[1];
|
|
7725
8167
|
|
|
7726
8168
|
react.useEffect(function () {
|
|
7727
|
-
if (!remote || typeof remote.getSettings !== "function") return undefined;
|
|
7728
8169
|
let alive = true;
|
|
7729
|
-
|
|
7730
|
-
if (!alive) return;
|
|
7731
|
-
|
|
7732
|
-
// 旧宿主没有 ui 字段 / 值不是布尔 → 保持默认「开」,不弹错、不阻塞界面。
|
|
7733
|
-
if (loaded.ui && typeof loaded.ui.sidebarEntry === "boolean") setOn(loaded.ui.sidebarEntry);
|
|
7734
|
-
}, function () {
|
|
7735
|
-
// 读设置失败也按「开」显示:这里只是插件页的一张卡片,不该因为它报红。
|
|
8170
|
+
readEntrySettings(remote).then(function (loaded) {
|
|
8171
|
+
if (!alive || !loaded) return;
|
|
8172
|
+
setMode(entryDisplayMode());
|
|
7736
8173
|
});
|
|
7737
8174
|
return function () { alive = false; };
|
|
7738
8175
|
}, [remote]);
|
|
@@ -7742,25 +8179,15 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
7742
8179
|
|
|
7743
8180
|
function toggle(next) {
|
|
7744
8181
|
if (busy) return;
|
|
7745
|
-
if (!remote || typeof remote.setSettings !== "function") {
|
|
7746
|
-
setErr(t("pluginCard.noSetSettings"));
|
|
7747
|
-
return;
|
|
7748
|
-
}
|
|
7749
8182
|
setBusy(true); setErr("");
|
|
7750
|
-
|
|
8183
|
+
// saveEntryMode 负责「先写设置、成功后再动槽位」;失败时界面由服务端返回值派生。
|
|
8184
|
+
saveEntryMode(remote, next).then(function (out) {
|
|
7751
8185
|
setBusy(false);
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
setErr(out.error || t("error.saveRejected"));
|
|
8186
|
+
if (!out || out.ok === false) {
|
|
8187
|
+
setErr((out && out.error) || t("entry.err.saveFailed"));
|
|
7755
8188
|
return;
|
|
7756
8189
|
}
|
|
7757
|
-
|
|
7758
|
-
const applied = settings && settings.ui && typeof settings.ui.sidebarEntry === "boolean"
|
|
7759
|
-
? settings.ui.sidebarEntry
|
|
7760
|
-
: !!next;
|
|
7761
|
-
setOn(applied);
|
|
7762
|
-
// 写盘成功之后才动槽位:设置没落盘时界面不该先变一个样。
|
|
7763
|
-
setSidebarEntry(!!next);
|
|
8190
|
+
setMode(entryDisplayMode());
|
|
7764
8191
|
}, function (e) {
|
|
7765
8192
|
setBusy(false);
|
|
7766
8193
|
setErr(pluginCardErr(e));
|
|
@@ -7768,22 +8195,36 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
7768
8195
|
}
|
|
7769
8196
|
|
|
7770
8197
|
return h("div", { className: "smem-plugin-card" }, [
|
|
7771
|
-
h("
|
|
7772
|
-
|
|
7773
|
-
|
|
8198
|
+
h("button", {
|
|
8199
|
+
type: "button", className: "smem-entry-summary", key: "sum",
|
|
8200
|
+
"aria-expanded": open ? "true" : "false",
|
|
8201
|
+
onClick: function () { setOpen(!open); },
|
|
8202
|
+
}, [
|
|
8203
|
+
h("span", { className: "smem-plugin-card-title", key: "t" }, t("entry.title")),
|
|
8204
|
+
h("span", { className: "smem-plugin-card-status", key: "s" }, entryStatusLabel(mode)),
|
|
8205
|
+
h("svg", {
|
|
8206
|
+
key: "i", className: "smem-entry-chevron", width: 14, height: 14,
|
|
8207
|
+
viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true",
|
|
8208
|
+
style: { transform: open ? "rotate(180deg)" : "rotate(0deg)" },
|
|
8209
|
+
}, h("path", {
|
|
8210
|
+
d: "M4 6.5 8 10.5 12 6.5", stroke: "currentColor", strokeWidth: "1.6",
|
|
8211
|
+
strokeLinecap: "round", strokeLinejoin: "round",
|
|
8212
|
+
})),
|
|
7774
8213
|
]),
|
|
7775
|
-
h("
|
|
7776
|
-
h("
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
8214
|
+
open ? h("div", { className: "smem-entry-body", key: "body" }, [
|
|
8215
|
+
h("div", { className: "smem-plugin-card-note", key: "sub" }, t("entry.subtitle")),
|
|
8216
|
+
h(EntryChooser, {
|
|
8217
|
+
key: "opts", mode: mode, busy: busy,
|
|
8218
|
+
sidebarOk: sidebarEntryAvailable(), modernApi: entryApiModern,
|
|
8219
|
+
onPick: toggle,
|
|
7781
8220
|
}),
|
|
7782
|
-
h("
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
8221
|
+
h("div", { className: "smem-plugin-card-note", key: "note" }, t("entry.note")),
|
|
8222
|
+
(entryDegraded() || entryApiModern === false)
|
|
8223
|
+
? h("div", { className: "smem-plugin-card-note smem-ad-warn", key: "deg" },
|
|
8224
|
+
t(entryApiModern === false ? "entry.legacyHost" : "entry.degraded"))
|
|
8225
|
+
: null,
|
|
8226
|
+
err ? h("div", { className: "smem-plugin-card-err", key: "e" }, err) : null,
|
|
8227
|
+
]) : null,
|
|
7787
8228
|
]);
|
|
7788
8229
|
}
|
|
7789
8230
|
|
|
@@ -7822,28 +8263,44 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
7822
8263
|
]);
|
|
7823
8264
|
}
|
|
7824
8265
|
|
|
7825
|
-
/**
|
|
8266
|
+
/**
|
|
8267
|
+
* 注册两个槽位(幂等:已经注册就什么都不做)。
|
|
8268
|
+
*
|
|
8269
|
+
* 2.7.0:返回布尔值 —— 侧栏是可选入口,旧宿主没有 sidebar.panellist 时
|
|
8270
|
+
* inject 会抛错。这里必须自己接住并**反序清理**已经注册成功的槽位,
|
|
8271
|
+
* 否则会留下「侧栏条目录入了、main 却没注册」的半状态。
|
|
8272
|
+
*/
|
|
7826
8273
|
function registerSidebarEntry() {
|
|
7827
|
-
if (sidebarDispose !== null
|
|
8274
|
+
if (sidebarDispose !== null) return true;
|
|
8275
|
+
if (!sidebarCtx) return false;
|
|
7828
8276
|
const ctx = sidebarCtx;
|
|
7829
8277
|
const handles = [];
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
8278
|
+
try {
|
|
8279
|
+
// 侧栏 list 槽:只给图标与 label,按钮由宿主自己画。
|
|
8280
|
+
handles.push(ctx.slots.inject("sidebar.panellist", function () {
|
|
8281
|
+
return ctx.slots.register({
|
|
8282
|
+
name: "sidebar.panellist",
|
|
8283
|
+
id: SIDEBAR_ENTRY_ID,
|
|
8284
|
+
order: 20,
|
|
8285
|
+
label: () => t("sidebar.label"),
|
|
8286
|
+
}, SidebarEntryIcon);
|
|
8287
|
+
}));
|
|
8288
|
+
// 中央主区 keyed 槽:key 与侧栏 id 相同;inject 面把「返回会话」交给页面组件。
|
|
8289
|
+
handles.push(ctx.slots.inject("main", function () {
|
|
8290
|
+
return ctx.slots.register({
|
|
8291
|
+
name: "main",
|
|
8292
|
+
key: SIDEBAR_ENTRY_ID,
|
|
8293
|
+
inject: function () { return { backToConversation: backToConversation }; },
|
|
8294
|
+
}, MemoryPanelPage);
|
|
8295
|
+
}));
|
|
8296
|
+
} catch (e) {
|
|
8297
|
+
for (let i = handles.length - 1; i >= 0; i--) {
|
|
8298
|
+
const dispose = handles[i];
|
|
8299
|
+
if (typeof dispose !== "function") continue;
|
|
8300
|
+
try { dispose(); } catch (inner) { /* 清理失败不掩盖原始错误 */ }
|
|
8301
|
+
}
|
|
8302
|
+
return false;
|
|
8303
|
+
}
|
|
7847
8304
|
sidebarDispose = handles;
|
|
7848
8305
|
return true;
|
|
7849
8306
|
}
|
|
@@ -7867,6 +8324,255 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
7867
8324
|
}
|
|
7868
8325
|
return true;
|
|
7869
8326
|
}
|
|
8327
|
+
// ── 界面入口三选一(2.7.0)──────────────────────────────────────────
|
|
8328
|
+
// 两个入口不是重复:设置页那个是所有宿主版本都有的保底,侧栏那个是可选、
|
|
8329
|
+
// 用户还能在插件详情页关掉。所以「只留一个」必须是**用户选择**,而不是
|
|
8330
|
+
// 默认删掉一个;三选一从类型上就保证不会出现「一个入口都没有」。
|
|
8331
|
+
//
|
|
8332
|
+
// 这一分片负责三件事:
|
|
8333
|
+
// 1. 把服务端的 ui.entry / 旧 ui.sidebarEntry 归一成一个模式;
|
|
8334
|
+
// 2. 按「宿主实际能力」解析出该注册哪一侧(侧栏槽 / 设置页 section);
|
|
8335
|
+
// 3. 提供两个控制面共用的 EntryChooser(面板 60 与插件页 76 都渲染它)。
|
|
8336
|
+
//
|
|
8337
|
+
// 旧接口(stale host:客户端已更新、宿主模块还是 2.6.0)没有 ui.entry,
|
|
8338
|
+
// 这时只能表达「侧栏开 / 关」,所以「只显示侧栏」置灰,保存退回 sidebarEntry。
|
|
8339
|
+
|
|
8340
|
+
/** 三个合法模式;顺序就是界面里的顺序。 */
|
|
8341
|
+
const ENTRY_MODES = ["both", "sidebar", "settings"];
|
|
8342
|
+
|
|
8343
|
+
/** 当前存储值(canonical);apply 时从 getSettings 读入。 */
|
|
8344
|
+
let entryMode = "both";
|
|
8345
|
+
/** 宿主是否认识 ui.entry;false = stale host,只能写 sidebarEntry。 */
|
|
8346
|
+
let entryApiModern = true;
|
|
8347
|
+
/** 侧栏槽能力:null = 还没探测,true / false = 探测结果。 */
|
|
8348
|
+
let sidebarSupported = null;
|
|
8349
|
+
/** 设置页 section 能力:null = 还没注册过,true / false = 结果。 */
|
|
8350
|
+
let settingsSupported = null;
|
|
8351
|
+
/** settings.section 的注销句柄(null = 当前没注册)。 */
|
|
8352
|
+
let settingsSectionDispose = null;
|
|
8353
|
+
|
|
8354
|
+
/** 合法模式原样返回,其余(含旧布尔)交给调用方回落。 */
|
|
8355
|
+
function normalizeEntryMode(value) {
|
|
8356
|
+
return ENTRY_MODES.indexOf(value) >= 0 ? value : null;
|
|
8357
|
+
}
|
|
8358
|
+
|
|
8359
|
+
/** 从 getSettings 返回值里取模式:新字段优先,旧字段按 true→both / false→settings。 */
|
|
8360
|
+
function entryModeFromSettings(settings) {
|
|
8361
|
+
const ui = settings && settings.ui;
|
|
8362
|
+
const next = normalizeEntryMode(ui && ui.entry);
|
|
8363
|
+
if (next) return next;
|
|
8364
|
+
if (ui && typeof ui.sidebarEntry === "boolean") return ui.sidebarEntry ? "both" : "settings";
|
|
8365
|
+
return "both";
|
|
8366
|
+
}
|
|
8367
|
+
|
|
8368
|
+
/** 宿主模块是不是 2.7.0:有 ui.entry 才算认识三选一。 */
|
|
8369
|
+
function hasModernEntryApi(settings) {
|
|
8370
|
+
return !!(settings && settings.ui && normalizeEntryMode(settings.ui.entry));
|
|
8371
|
+
}
|
|
8372
|
+
|
|
8373
|
+
/** 选项文案(三处共用:折叠行状态、面板状态、radio label)。 */
|
|
8374
|
+
function entryStatusLabel(mode) {
|
|
8375
|
+
return t(mode === "sidebar" ? "entry.opt.sidebar" : mode === "settings" ? "entry.opt.settings" : "entry.opt.both");
|
|
8376
|
+
}
|
|
8377
|
+
|
|
8378
|
+
/**
|
|
8379
|
+
* 某宿主上「实际会显示哪一个」。
|
|
8380
|
+
*
|
|
8381
|
+
* - 存 sidebar 但侧栏不可用 → 回落 settings(绝不两个都不显示);
|
|
8382
|
+
* - 存 settings 但设置页不可用 → 回落 sidebar;
|
|
8383
|
+
* - 两个都不可用(极旧宿主)→ 交给插件页卡片兜底,这里不抛错。
|
|
8384
|
+
*/
|
|
8385
|
+
function resolveEntryVisibility(mode, support) {
|
|
8386
|
+
const sidebarOk = support.sidebar !== false;
|
|
8387
|
+
const settingsOk = support.settings !== false;
|
|
8388
|
+
if (mode === "sidebar") {
|
|
8389
|
+
if (sidebarOk) return { sidebar: true, settings: false };
|
|
8390
|
+
return { sidebar: false, settings: settingsOk };
|
|
8391
|
+
}
|
|
8392
|
+
if (mode === "settings") {
|
|
8393
|
+
if (settingsOk) return { sidebar: false, settings: true };
|
|
8394
|
+
return { sidebar: sidebarOk, settings: false };
|
|
8395
|
+
}
|
|
8396
|
+
return { sidebar: sidebarOk, settings: settingsOk };
|
|
8397
|
+
}
|
|
8398
|
+
|
|
8399
|
+
/** 界面上展示的模式:降级时把 sidebar 显示成 settings,避免控件与实际不符。 */
|
|
8400
|
+
function entryDisplayMode() {
|
|
8401
|
+
if (entryMode === "sidebar" && (sidebarSupported === false || entryApiModern === false)) return "settings";
|
|
8402
|
+
return entryMode;
|
|
8403
|
+
}
|
|
8404
|
+
|
|
8405
|
+
/** 降级提示是否要出现(存了 sidebar、当前宿主却显示设置页)。 */
|
|
8406
|
+
function entryDegraded() {
|
|
8407
|
+
return entryMode === "sidebar" && (sidebarSupported === false || entryApiModern === false);
|
|
8408
|
+
}
|
|
8409
|
+
|
|
8410
|
+
/** 「只显示侧栏」这一项能不能选:宿主支持侧栏,且认识 ui.entry。 */
|
|
8411
|
+
function sidebarEntryAvailable() {
|
|
8412
|
+
return sidebarSupported !== false && entryApiModern !== false;
|
|
8413
|
+
}
|
|
8414
|
+
|
|
8415
|
+
/** 探测侧栏槽能力:真实注册一次,失败时 registerSidebarEntry 自己清干净半注册。 */
|
|
8416
|
+
function detectSidebarSupport() {
|
|
8417
|
+
if (sidebarSupported !== null) return sidebarSupported;
|
|
8418
|
+
sidebarSupported = !!registerSidebarEntry();
|
|
8419
|
+
return sidebarSupported;
|
|
8420
|
+
}
|
|
8421
|
+
|
|
8422
|
+
/** 注册设置页 section(幂等)。失败返回 false —— 旧外壳 / 没有设置面的宿主。 */
|
|
8423
|
+
function registerSettingsSection() {
|
|
8424
|
+
if (settingsSectionDispose !== null) return true;
|
|
8425
|
+
if (!sidebarCtx) return false;
|
|
8426
|
+
const ctx = sidebarCtx;
|
|
8427
|
+
try {
|
|
8428
|
+
settingsSectionDispose = ctx.slots.inject("settings.section", function () {
|
|
8429
|
+
return ctx.slots.register({
|
|
8430
|
+
name: "settings.section",
|
|
8431
|
+
id: "sage-mem",
|
|
8432
|
+
order: 20,
|
|
8433
|
+
label: function () { return t("section.label"); },
|
|
8434
|
+
// 声明「记忆星图」子槽(沿用 1.8.0 的契约):sage-starmap 之类的
|
|
8435
|
+
// 第三方插件向它注入视图。⚠️ 现状:Section 还没有调用 renderSlot,
|
|
8436
|
+
// 所以这个子槽目前只声明、不渲染(2.7.0 未改;渲染它是独立事项)。
|
|
8437
|
+
// 保留声明是为了不破坏第三方插件的注入面。
|
|
8438
|
+
children: { "settings.memory.starmap": { kind: "list", scope: "root" } },
|
|
8439
|
+
}, function (props) {
|
|
8440
|
+
return react.createElement(Section, { ...props, ctx: ctx });
|
|
8441
|
+
});
|
|
8442
|
+
});
|
|
8443
|
+
return true;
|
|
8444
|
+
} catch (e) {
|
|
8445
|
+
settingsSectionDispose = null;
|
|
8446
|
+
return false;
|
|
8447
|
+
}
|
|
8448
|
+
}
|
|
8449
|
+
|
|
8450
|
+
/** 注销设置页 section(反序不需要:只有一个 inject 句柄)。 */
|
|
8451
|
+
function disposeSettingsSection() {
|
|
8452
|
+
const stop = settingsSectionDispose;
|
|
8453
|
+
settingsSectionDispose = null;
|
|
8454
|
+
if (typeof stop !== "function") return;
|
|
8455
|
+
try { stop(); } catch (e) { /* 注销失败不能把设置卡死在「已注销」状态 */ }
|
|
8456
|
+
}
|
|
8457
|
+
|
|
8458
|
+
/**
|
|
8459
|
+
* 立即应用一个入口模式(改完槽位就变,不需要刷新)。
|
|
8460
|
+
*
|
|
8461
|
+
* 顺序:探测侧栏 → 解析可见性 → 按需注册 / 注销两侧。
|
|
8462
|
+
* 设置页注册失败而侧栏又被要求隐藏时,强制把侧栏放回来 —— 这是最后一道门。
|
|
8463
|
+
*/
|
|
8464
|
+
function applyEntryMode(mode) {
|
|
8465
|
+
entryMode = normalizeEntryMode(mode) || "both";
|
|
8466
|
+
const sidebarOk = detectSidebarSupport();
|
|
8467
|
+
const visible = resolveEntryVisibility(entryMode, { sidebar: sidebarOk, settings: settingsSupported });
|
|
8468
|
+
|
|
8469
|
+
if (visible.sidebar) {
|
|
8470
|
+
if (!registerSidebarEntry()) {
|
|
8471
|
+
sidebarSupported = false;
|
|
8472
|
+
visible.sidebar = false;
|
|
8473
|
+
}
|
|
8474
|
+
} else {
|
|
8475
|
+
setSidebarEntry(false);
|
|
8476
|
+
}
|
|
8477
|
+
|
|
8478
|
+
if (visible.settings) {
|
|
8479
|
+
settingsSupported = registerSettingsSection();
|
|
8480
|
+
if (!settingsSupported && !visible.sidebar) {
|
|
8481
|
+
if (registerSidebarEntry()) {
|
|
8482
|
+
sidebarSupported = true;
|
|
8483
|
+
visible.sidebar = true;
|
|
8484
|
+
}
|
|
8485
|
+
}
|
|
8486
|
+
} else {
|
|
8487
|
+
disposeSettingsSection();
|
|
8488
|
+
}
|
|
8489
|
+
|
|
8490
|
+
return {
|
|
8491
|
+
mode: entryMode,
|
|
8492
|
+
sidebar: visible.sidebar,
|
|
8493
|
+
settings: visible.settings,
|
|
8494
|
+
sidebarOk: sidebarSupported,
|
|
8495
|
+
settingsOk: settingsSupported,
|
|
8496
|
+
};
|
|
8497
|
+
}
|
|
8498
|
+
|
|
8499
|
+
/**
|
|
8500
|
+
* 保存一个入口模式:先写设置、成功后再动槽位。
|
|
8501
|
+
*
|
|
8502
|
+
* stale host 上没有 ui.entry,退回旧写法 sidebarEntry(true = 两个都显示,
|
|
8503
|
+
* false = 只显示设置页);若宿主真的拒绝了 entry,也自动重试一次旧写法。
|
|
8504
|
+
*/
|
|
8505
|
+
function saveEntryMode(remote, mode) {
|
|
8506
|
+
const next = normalizeEntryMode(mode) || "both";
|
|
8507
|
+
const writeModern = function () {
|
|
8508
|
+
return Promise.resolve(remote.setSettings({ ui: { entry: next } }));
|
|
8509
|
+
};
|
|
8510
|
+
const writeLegacy = function () {
|
|
8511
|
+
return Promise.resolve(remote.setSettings({ ui: { sidebarEntry: next !== "settings" } }));
|
|
8512
|
+
};
|
|
8513
|
+
const finish = function (out, retried) {
|
|
8514
|
+
if (out && out.ok === false) {
|
|
8515
|
+
// stale host 的报错形如 "unknown ui setting: entry":自动退回旧写法再试一次。
|
|
8516
|
+
if (!retried && entryApiModern !== false && /unknown ui setting/i.test(String(out.error || ""))) {
|
|
8517
|
+
entryApiModern = false;
|
|
8518
|
+
return writeLegacy().then(function (legacyOut) { return finish(legacyOut, true); });
|
|
8519
|
+
}
|
|
8520
|
+
return { ok: false, error: out.error || t("ad.err.saveRejected") };
|
|
8521
|
+
}
|
|
8522
|
+
const settings = out && out.settings ? out.settings : null;
|
|
8523
|
+
// 服务端回传的形状就是事实:带 ui.entry 才是新接口。
|
|
8524
|
+
if (settings) entryApiModern = hasModernEntryApi(settings);
|
|
8525
|
+
const applied = settings ? entryModeFromSettings(settings) : next;
|
|
8526
|
+
applyEntryMode(applied);
|
|
8527
|
+
return { ok: true, settings: settings, mode: applied };
|
|
8528
|
+
};
|
|
8529
|
+
return (entryApiModern === false ? writeLegacy() : writeModern()).then(function (out) {
|
|
8530
|
+
return finish(out, entryApiModern === false);
|
|
8531
|
+
});
|
|
8532
|
+
}
|
|
8533
|
+
|
|
8534
|
+
/** 读一次设置并同步全局模式;失败时返回 null(界面按默认渲染,不阻塞)。 */
|
|
8535
|
+
function readEntrySettings(remote) {
|
|
8536
|
+
if (!remote || typeof remote.getSettings !== "function") return Promise.resolve(null);
|
|
8537
|
+
return Promise.resolve(remote.getSettings()).then(function (out) {
|
|
8538
|
+
const settings = unwrap(out) || {};
|
|
8539
|
+
entryApiModern = hasModernEntryApi(settings);
|
|
8540
|
+
const mode = entryModeFromSettings(settings);
|
|
8541
|
+
applyEntryMode(mode);
|
|
8542
|
+
return { settings: settings, mode: mode };
|
|
8543
|
+
}, function () { return null; });
|
|
8544
|
+
}
|
|
8545
|
+
|
|
8546
|
+
/**
|
|
8547
|
+
* 三选一控件(受控)。
|
|
8548
|
+
*
|
|
8549
|
+
* 不自己读设置、不自己保存:两个控制面各自处理 busy / error,
|
|
8550
|
+
* 这样插件页卡片与面板里的交互文案可以不同,而选项本身只有一份。
|
|
8551
|
+
*/
|
|
8552
|
+
function EntryChooser(props) {
|
|
8553
|
+
const mode = props && normalizeEntryMode(props.mode) ? props.mode : entryDisplayMode();
|
|
8554
|
+
const busy = !!(props && props.busy);
|
|
8555
|
+
const sidebarOk = !(props && props.sidebarOk === false);
|
|
8556
|
+
const modernApi = !(props && props.modernApi === false);
|
|
8557
|
+
const onPick = props && typeof props.onPick === "function" ? props.onPick : function () {};
|
|
8558
|
+
const labels = { both: "entry.opt.both", sidebar: "entry.opt.sidebar", settings: "entry.opt.settings" };
|
|
8559
|
+
return h("div", { className: "smem-entry-opts" }, ENTRY_MODES.map(function (value) {
|
|
8560
|
+
const blocked = value === "sidebar" && (!sidebarOk || !modernApi);
|
|
8561
|
+
const disabled = busy || blocked;
|
|
8562
|
+
const blockedWhy = !modernApi ? t("entry.legacyHost") : t("entry.sidebarUnsupported");
|
|
8563
|
+
return h("label", {
|
|
8564
|
+
className: "smem-check smem-entry-opt", key: value,
|
|
8565
|
+
title: blocked ? blockedWhy : undefined,
|
|
8566
|
+
}, [
|
|
8567
|
+
h("input", {
|
|
8568
|
+
type: "radio", key: "r", name: "smem-entry-mode",
|
|
8569
|
+
value: value, checked: mode === value, disabled: disabled,
|
|
8570
|
+
onChange: function () { if (!disabled) onPick(value); },
|
|
8571
|
+
}),
|
|
8572
|
+
h("span", { key: "l" }, t(labels[value])),
|
|
8573
|
+
]);
|
|
8574
|
+
}));
|
|
8575
|
+
}
|
|
7870
8576
|
// ⚠️ 不把 `layout` 写进 inject:它是**可选**依赖(官方 inspect:optional access =
|
|
7871
8577
|
// `ctx.get("layout")` + 判空;hardDependency 才是 inject)。写进 inject 会让旧外壳
|
|
7872
8578
|
// 缺这个服务时整个 client 插件不 apply —— 连带丢掉设置页。返回会话优先用宿主给的
|
|
@@ -7889,38 +8595,26 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
7889
8595
|
document.head.appendChild(tag);
|
|
7890
8596
|
}
|
|
7891
8597
|
setupI18n(ctx);
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
order: 20,
|
|
7898
|
-
label: () => t("section.label"),
|
|
7899
|
-
// 声明「记忆星图」子槽:sage-starmap 向它注入星空视图,
|
|
7900
|
-
// 记忆管理页用 renderSlot 渲染。声明 children 后 shell 才会
|
|
7901
|
-
// 向本 section 组件传入 renderSlot。
|
|
7902
|
-
children: { "settings.memory.starmap": { kind: "list", scope: "root" } },
|
|
7903
|
-
},
|
|
7904
|
-
(props) => react.createElement(Section, { ...props, ctx }),
|
|
7905
|
-
),
|
|
7906
|
-
);
|
|
7907
|
-
// ── 1.8.0 侧栏入口:先记住 ctx,再读一次设置决定注不注册 ──────────────
|
|
7908
|
-
// 设置页 section **永远**保留(关闭侧栏入口只是少一个入口,不是少一个设置页)。
|
|
7909
|
-
// 读失败 / 旧宿主没有 ui 字段 / 值坏掉 → 一律按「开」处理,绝不阻塞界面。
|
|
8598
|
+
|
|
8599
|
+
// ── 2.7.0 界面入口三选一 ────────────────────────────────────────────
|
|
8600
|
+
// 先读一次设置,决定注册哪一侧;读失败 / 旧宿主没有 ui.entry / 值坏掉
|
|
8601
|
+
// → 一律按「两个都显示」处理,绝不阻塞界面。
|
|
8602
|
+
// 设置页 section 不再无条件注册:只显示侧栏时它会被注销(78-entry-visibility)。
|
|
7910
8603
|
sidebarCtx = ctx;
|
|
7911
|
-
let
|
|
8604
|
+
let initialMode = "both";
|
|
7912
8605
|
try {
|
|
7913
8606
|
const mem = typeof ctx.get === "function" ? ctx.get("remote.memory") : null;
|
|
7914
8607
|
if (mem && typeof mem.getSettings === "function") {
|
|
7915
8608
|
const loaded = unwrap(await mem.getSettings()) || {};
|
|
7916
|
-
|
|
8609
|
+
entryApiModern = hasModernEntryApi(loaded);
|
|
8610
|
+
initialMode = entryModeFromSettings(loaded);
|
|
7917
8611
|
}
|
|
7918
8612
|
} catch (e) {
|
|
7919
|
-
|
|
8613
|
+
initialMode = "both";
|
|
7920
8614
|
}
|
|
7921
|
-
|
|
8615
|
+
applyEntryMode(initialMode);
|
|
7922
8616
|
|
|
7923
|
-
// ── 1.8.2
|
|
8617
|
+
// ── 1.8.2 / 2.7.0 插件详情页的「界面入口」折叠行 ────────────────────
|
|
7924
8618
|
// `plugins.bundle.config` 是 keyed 槽,key = 本 bundle 的 **package name**(sage-mem)。
|
|
7925
8619
|
// 宿主在插件页的 description 与「包含的组件」行之间渲染它;ownerProps 形如
|
|
7926
8620
|
// `{ view: "summary" | "page" }`,卡片里自己再判一次(summary 返回 null)。
|