sage-mem 2.6.0 → 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 +5 -5
- package/README.md +5 -5
- package/lib/client.js +409 -150
- 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/README.en.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
**Cross-session memory for DeepSeek Harness — plain markdown files, no database, no worker.**
|
|
14
14
|
|
|
15
|
-
**Current release: `2.
|
|
15
|
+
**Current release: `2.7.0`** — the npm badge above is the dynamic source of truth. **Contract baseline: `1.0.0`** — the first stable release, whose public commitments, the capability boundaries and the list of things it explicitly cannot do are frozen in the
|
|
16
16
|
[frozen contract](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md).
|
|
17
17
|
|
|
18
18
|
If it is useful to you, a Star is the most direct feedback.
|
|
@@ -81,10 +81,10 @@ Most memory schemes take the "SQLite + a long-running worker" route: once memori
|
|
|
81
81
|
|
|
82
82
|
The UI also carries four more capabilities (all of them optional): the memory management page, type and tag filtering, the timeline (an entries list by default, with an optional star-map view), and audit & archiving.
|
|
83
83
|
|
|
84
|
-
**Since
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
**Since 2.7.0 the interface entry is a three-way choice**: show both (default), sidebar only, or settings only — **at least one entry always stays on**.
|
|
85
|
+
The sidebar entry (1.8.0) switches the central panel to this page with one click (with a "back to conversation" control at the top); the settings entry is 「设置 → 记忆管理」.
|
|
86
|
+
The switch lives in the 「界面入口」 disclosure on the plugin's detail page (「插件 → sage-mem」), and the same options are in the panel under 「自动整理 → 设置」; changes apply immediately, no restart.
|
|
87
|
+
When the host does not support the sidebar slot, "sidebar only" is disabled and the plugin falls back to the settings entry; old settings files migrate automatically (sidebar on → both, off → settings only).
|
|
88
88
|
Since 1.8.2 the plugin also ships its own icon (a golden stack of memory cards) instead of the generic square.
|
|
89
89
|
|
|
90
90
|
The memory management page's file list also has an **Open memory folder** button: it opens the
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
> File-based cross-session memory for DeepSeek Harness — every memory is a plain markdown file you can open, edit and delete. No database, no worker, no port.
|
|
16
16
|
|
|
17
|
-
**当前发布版本:`2.
|
|
17
|
+
**当前发布版本:`2.7.0`** —— 顶部 npm 徽章是动态权威。**契约基线:`1.0.0`** —— 第一个稳定版,公开承诺、能力边界与「明确做不到」的清单冻结在
|
|
18
18
|
[契约冻结](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md)。
|
|
19
19
|
|
|
20
20
|
如果它对你有用,给个 Star 是最直接的反馈。
|
|
@@ -83,10 +83,10 @@ sage-mem 补上这一层,做法是:**把该记住的事写成一张张卡片
|
|
|
83
83
|
|
|
84
84
|
界面上还有四块能力(都可以不用):记忆管理页、类型与标签筛选、时间线(默认是条目列表,可切星空视角)、体检与归档。
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
**从 2.7.0 起,记忆管理的界面入口是三选一**:两个都显示(默认)/ 只显示侧栏 / 只显示设置页 —— **至少保留一个**。
|
|
87
|
+
侧栏入口(1.8.0)点一下就把中央主区切到本页(页顶有「返回会话」);设置页入口是「设置 → 记忆管理」。
|
|
88
|
+
开关在「插件 → sage-mem」的「界面入口」折叠行里,面板「自动整理 → 设置」里也有同一组选项;改完立即生效、不用重启。
|
|
89
|
+
宿主不支持侧栏时「只显示侧栏」自动置灰并回落到设置页;旧设置文件里的「侧栏开 / 关」会按 true → 两个都显示、false → 只显示设置页迁移。
|
|
90
90
|
从 1.8.2 开始,sage-mem 在插件列表里有了自己的图标(金色记忆卡片),不再是默认的通用方图标。
|
|
91
91
|
|
|
92
92
|
记忆管理页的「文件列表」头部还有一个「打开记忆文件夹」按钮:用系统文件管理器打开当前记忆目录
|
package/lib/client.js
CHANGED
|
@@ -2686,14 +2686,20 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2686
2686
|
".smem-page-top{display:flex;align-items:center;}",
|
|
2687
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;}",
|
|
2688
2688
|
".smem-page-back:hover{color:var(--dsw-alias-label-primary,#1f2329);background:var(--dsw-alias-bg-layer-2);}",
|
|
2689
|
-
// ── 1.8.2
|
|
2689
|
+
// ── 1.8.2 / 2.7.0:插件详情页的「界面入口」折叠行 ──
|
|
2690
2690
|
// 融入宿主插件页:标题 13px/600、说明 12px 次要色,圆角与分隔线只用宿主 token。
|
|
2691
|
-
//
|
|
2691
|
+
// 2.7.0 起默认收起:收起时只露标题 + 当前状态,展开才是三选一。
|
|
2692
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;}",
|
|
2693
|
-
".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;}",
|
|
2694
2701
|
".smem-plugin-card-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#1f2430);line-height:1.4;}",
|
|
2695
2702
|
".smem-plugin-card-status{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);line-height:1.4;}",
|
|
2696
|
-
".smem-plugin-card-check{font-size:12.5px;}",
|
|
2697
2703
|
".smem-plugin-card-note{font-size:12px;line-height:1.55;color:var(--dsw-alias-label-secondary,#6b7280);}",
|
|
2698
2704
|
".smem-plugin-card-err{font-size:12px;line-height:1.5;color:var(--dsw-alias-state-error-primary,#ef4444);overflow-wrap:anywhere;}",
|
|
2699
2705
|
// ── 底部一行仓库链接(引流,不抢视线)──
|
|
@@ -4188,13 +4194,18 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4188
4194
|
"star.dismiss": "不再显示(下次打开 DSH 还会出现)",
|
|
4189
4195
|
"star.aria": "在 GitHub 上给 gezi-wen/sage-mem 点一颗星",
|
|
4190
4196
|
|
|
4191
|
-
"
|
|
4192
|
-
"
|
|
4193
|
-
"
|
|
4194
|
-
"
|
|
4195
|
-
"
|
|
4196
|
-
"
|
|
4197
|
-
"
|
|
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": "保存失败(未知原因)",
|
|
4198
4209
|
|
|
4199
4210
|
"error.remote": "remote 调用失败",
|
|
4200
4211
|
"error.listFiles": "listFiles RPC 失败: {message}",
|
|
@@ -4389,11 +4400,6 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4389
4400
|
"ad.last.retried": " · 零正文重试 {count} 次后补齐",
|
|
4390
4401
|
"ad.last.continuations": " · 续写 {count} 轮",
|
|
4391
4402
|
"ad.last.report": " · 报告 {name}",
|
|
4392
|
-
"ad.sidebar.title": "侧栏入口",
|
|
4393
|
-
"ad.sidebar.on": "已开启",
|
|
4394
|
-
"ad.sidebar.off": "已关闭",
|
|
4395
|
-
"ad.sidebar.note": "在 DSH 侧栏显示「记忆管理」图标,点它把中央主区切到本页。默认开启;关闭后「设置 → 记忆管理」这个入口仍然保留。改动立即生效,不需要重启。",
|
|
4396
|
-
"ad.sidebar.label": "在侧栏显示「记忆管理」入口",
|
|
4397
4403
|
"ad.limits.title": "记忆注入参数",
|
|
4398
4404
|
"ad.limits.pending": "有改动待生效",
|
|
4399
4405
|
"ad.limits.restartWarn": "⚠ 这些值在 DSH 启动时就固化了 —— 要重启 DSH 才生效。下面每行都写清「当前 → 重启后」。",
|
|
@@ -4493,8 +4499,6 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4493
4499
|
"ad.err.notSaved": "没保存 —— {detail}",
|
|
4494
4500
|
"ad.err.saveRejected": "保存被拒绝",
|
|
4495
4501
|
"ad.notice.limitsSaved": "已保存 —— 需重启 DSH 才生效",
|
|
4496
|
-
"ad.notice.sidebarOn": "侧栏入口已开启(立即生效)",
|
|
4497
|
-
"ad.notice.sidebarOff": "侧栏入口已关闭(设置页入口仍在)",
|
|
4498
4502
|
"ad.err.reservedRejected": "名单被拒绝",
|
|
4499
4503
|
"ad.notice.reservedSaved": "保留名名单已保存 —— 需重启 DSH 才生效",
|
|
4500
4504
|
"ad.err.reservedNeedName": "先写一个文件名,例如 project_notes.md",
|
|
@@ -4697,13 +4701,18 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4697
4701
|
"star.dismiss": "Hide until next launch",
|
|
4698
4702
|
"star.aria": "Star gezi-wen/sage-mem on GitHub",
|
|
4699
4703
|
|
|
4700
|
-
"
|
|
4701
|
-
"
|
|
4702
|
-
"
|
|
4703
|
-
"
|
|
4704
|
-
"
|
|
4705
|
-
"
|
|
4706
|
-
"
|
|
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)",
|
|
4707
4716
|
|
|
4708
4717
|
"error.remote": "remote call failed",
|
|
4709
4718
|
"error.listFiles": "listFiles RPC failed: {message}",
|
|
@@ -4896,11 +4905,6 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4896
4905
|
"ad.last.retried": " · completed after {count} zero-body retries",
|
|
4897
4906
|
"ad.last.continuations": " · {count} continuation rounds",
|
|
4898
4907
|
"ad.last.report": " · report {name}",
|
|
4899
|
-
"ad.sidebar.title": "Sidebar entry",
|
|
4900
|
-
"ad.sidebar.on": "On",
|
|
4901
|
-
"ad.sidebar.off": "Off",
|
|
4902
|
-
"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.",
|
|
4903
|
-
"ad.sidebar.label": "Show the “Memory management” entry in the sidebar",
|
|
4904
4908
|
"ad.limits.title": "Injection limits",
|
|
4905
4909
|
"ad.limits.pending": "changes pending",
|
|
4906
4910
|
"ad.limits.restartWarn": "⚠ These values are fixed at DSH startup — restart DSH for them to apply. Each row below shows “current → after restart”.",
|
|
@@ -5000,8 +5004,6 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5000
5004
|
"ad.err.notSaved": "Not saved — {detail}",
|
|
5001
5005
|
"ad.err.saveRejected": "Save rejected",
|
|
5002
5006
|
"ad.notice.limitsSaved": "Saved — restart DSH for it to take effect",
|
|
5003
|
-
"ad.notice.sidebarOn": "Sidebar entry enabled (takes effect immediately)",
|
|
5004
|
-
"ad.notice.sidebarOff": "Sidebar entry disabled (the Settings entry remains)",
|
|
5005
5007
|
"ad.err.reservedRejected": "Reserved-name list rejected",
|
|
5006
5008
|
"ad.notice.reservedSaved": "Reserved-name list saved — restart DSH for it to take effect",
|
|
5007
5009
|
"ad.err.reservedNeedName": "Enter a file name first, e.g. project_notes.md",
|
|
@@ -5494,9 +5496,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5494
5496
|
const sLimitsDraft = react.useState(null); const limitsDraft = sLimitsDraft[0], setLimitsDraft = sLimitsDraft[1];
|
|
5495
5497
|
const sSaveBusy = react.useState(false); const saveBusy = sSaveBusy[0], setSaveBusy = sSaveBusy[1];
|
|
5496
5498
|
const sReservedNew = react.useState(""); const reservedNew = sReservedNew[0], setReservedNew = sReservedNew[1];
|
|
5497
|
-
// ──
|
|
5498
|
-
const
|
|
5499
|
-
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];
|
|
5500
5502
|
|
|
5501
5503
|
// 派生量放在 effect 之前:下面几个 effect 的依赖数组要在渲染期就能取到值。
|
|
5502
5504
|
const c = cfg || {};
|
|
@@ -5511,8 +5513,11 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5511
5513
|
const fixedReserved = ((memSet && memSet.reserved) || []).filter(function (n) {
|
|
5512
5514
|
return extraLower.indexOf(String(n).toLowerCase()) < 0;
|
|
5513
5515
|
});
|
|
5514
|
-
//
|
|
5515
|
-
|
|
5516
|
+
// 入口模式:新字段优先,旧 sidebarEntry 兜底;坏值 / 旧宿主一律按「两个都显示」。
|
|
5517
|
+
// 存了 sidebar 但当前宿主不支持侧栏时,界面按 settings 显示并给一条降级提示。
|
|
5518
|
+
const entryModeValue = entryModeFromSettings(memSet);
|
|
5519
|
+
const entryShown = entryModeValue === "sidebar" && !sidebarEntryAvailable() ? "settings" : entryModeValue;
|
|
5520
|
+
const entryIsDegraded = entryModeValue === "sidebar" && !sidebarEntryAvailable();
|
|
5516
5521
|
|
|
5517
5522
|
react.useEffect(function () {
|
|
5518
5523
|
if (!remote) { setErr(t("ad.err.remoteUnavailable")); return undefined; }
|
|
@@ -5644,28 +5649,29 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5644
5649
|
}
|
|
5645
5650
|
|
|
5646
5651
|
/**
|
|
5647
|
-
*
|
|
5652
|
+
* 界面入口三选一(2.7.0)。
|
|
5648
5653
|
*
|
|
5649
|
-
*
|
|
5650
|
-
* 失败只报错、不改任何东西 ——
|
|
5651
|
-
*
|
|
5654
|
+
* 顺序仍是「先写设置、成功后再动槽位」:设置没落盘就不该让界面先变一个样。
|
|
5655
|
+
* 失败只报错、不改任何东西 —— 选项由 memSet 派生,于是自动回到原值。
|
|
5656
|
+
* 成功后 saveEntryMode 内部会调 applyEntryMode:**立即**增删槽位,不刷新、不重启。
|
|
5652
5657
|
*/
|
|
5653
|
-
function
|
|
5658
|
+
function saveEntry(next) {
|
|
5654
5659
|
if (!memRemote || typeof memRemote.setSettings !== "function") {
|
|
5655
|
-
|
|
5660
|
+
setEntryErr(t("entry.err.noSetSettings"));
|
|
5656
5661
|
return;
|
|
5657
5662
|
}
|
|
5658
|
-
if (
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
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);
|
|
5665
5672
|
else loadMemSettings();
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
}, function (e) { setSidebarBusy(false); setSidebarErr(detail(e)); });
|
|
5673
|
+
setNotice(t("entry.notice.saved"));
|
|
5674
|
+
}, function (e) { setEntryBusy(false); setEntryErr(detail(e)); });
|
|
5669
5675
|
}
|
|
5670
5676
|
|
|
5671
5677
|
/**
|
|
@@ -5958,24 +5964,24 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5958
5964
|
]),
|
|
5959
5965
|
]) : null,
|
|
5960
5966
|
segOf === "settings" ? h("div", { key: "seg-set" }, [
|
|
5961
|
-
//
|
|
5962
|
-
//
|
|
5963
|
-
h("div", { className: "smem-autodream-sec", key: "
|
|
5967
|
+
// 2.7.0:界面入口三选一。至少保留一个(三选一从类型上就排除了「都不显示」)。
|
|
5968
|
+
// 这里与插件详情页共用同一个 EntryChooser 与 applyEntryMode。
|
|
5969
|
+
h("div", { className: "smem-autodream-sec", key: "entry" }, [
|
|
5964
5970
|
h("div", { className: "smem-autodream-row", key: "h" }, [
|
|
5965
|
-
h("span", { key: "t", style: { fontWeight: "600", fontSize: "12.5px" } }, t("
|
|
5966
|
-
h("span", { className: "smem-status", key: "s" },
|
|
5967
|
-
]),
|
|
5968
|
-
h("div", { className: "smem-autodream-note", key: "n" }, t("ad.sidebar.note")),
|
|
5969
|
-
h("label", { className: "smem-check", key: "row" }, [
|
|
5970
|
-
h("input", {
|
|
5971
|
-
type: "checkbox", key: "c",
|
|
5972
|
-
checked: sidebarEntryOn,
|
|
5973
|
-
disabled: sidebarBusy,
|
|
5974
|
-
onChange: function (e) { saveSidebarEntry(!!(e.target && e.target.checked)); },
|
|
5975
|
-
}),
|
|
5976
|
-
h("span", { key: "l" }, t("ad.sidebar.label")),
|
|
5971
|
+
h("span", { key: "t", style: { fontWeight: "600", fontSize: "12.5px" } }, t("entry.title")),
|
|
5972
|
+
h("span", { className: "smem-status", key: "s" }, entryStatusLabel(entryShown)),
|
|
5977
5973
|
]),
|
|
5978
|
-
|
|
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,
|
|
5979
5985
|
]),
|
|
5980
5986
|
// 自动归档那一段(三档策略 + 三个阈值)**已挪到「自动归档」页签**(v0.9.9):
|
|
5981
5987
|
// 归档是确定性规则、不调模型,和这一页的「让模型整理」不是一件事;
|
|
@@ -8128,7 +8134,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
8128
8134
|
|
|
8129
8135
|
/** 卡片里的错误文案:remote 可能 reject 一个字符串 / 对象 / Error,尽量展开成人话。 */
|
|
8130
8136
|
function pluginCardErr(e) {
|
|
8131
|
-
if (!e) return t("
|
|
8137
|
+
if (!e) return t("entry.err.saveFailed");
|
|
8132
8138
|
if (e instanceof Error) return e.message || String(e);
|
|
8133
8139
|
try {
|
|
8134
8140
|
const s = JSON.stringify(e);
|
|
@@ -8154,20 +8160,16 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
8154
8160
|
const ctx = props && props.ctx;
|
|
8155
8161
|
// 必须走 stableRemote:真实宿主里 ctx.get() 每次返回新对象,进依赖数组会无限重跑。
|
|
8156
8162
|
const remote = stableRemote(ctx, "remote.memory");
|
|
8157
|
-
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];
|
|
8158
8165
|
const sBusy = react.useState(false); const busy = sBusy[0], setBusy = sBusy[1];
|
|
8159
8166
|
const sErr = react.useState(""); const err = sErr[0], setErr = sErr[1];
|
|
8160
8167
|
|
|
8161
8168
|
react.useEffect(function () {
|
|
8162
|
-
if (!remote || typeof remote.getSettings !== "function") return undefined;
|
|
8163
8169
|
let alive = true;
|
|
8164
|
-
|
|
8165
|
-
if (!alive) return;
|
|
8166
|
-
|
|
8167
|
-
// 旧宿主没有 ui 字段 / 值不是布尔 → 保持默认「开」,不弹错、不阻塞界面。
|
|
8168
|
-
if (loaded.ui && typeof loaded.ui.sidebarEntry === "boolean") setOn(loaded.ui.sidebarEntry);
|
|
8169
|
-
}, function () {
|
|
8170
|
-
// 读设置失败也按「开」显示:这里只是插件页的一张卡片,不该因为它报红。
|
|
8170
|
+
readEntrySettings(remote).then(function (loaded) {
|
|
8171
|
+
if (!alive || !loaded) return;
|
|
8172
|
+
setMode(entryDisplayMode());
|
|
8171
8173
|
});
|
|
8172
8174
|
return function () { alive = false; };
|
|
8173
8175
|
}, [remote]);
|
|
@@ -8177,25 +8179,15 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
8177
8179
|
|
|
8178
8180
|
function toggle(next) {
|
|
8179
8181
|
if (busy) return;
|
|
8180
|
-
if (!remote || typeof remote.setSettings !== "function") {
|
|
8181
|
-
setErr(t("pluginCard.noSetSettings"));
|
|
8182
|
-
return;
|
|
8183
|
-
}
|
|
8184
8182
|
setBusy(true); setErr("");
|
|
8185
|
-
|
|
8183
|
+
// saveEntryMode 负责「先写设置、成功后再动槽位」;失败时界面由服务端返回值派生。
|
|
8184
|
+
saveEntryMode(remote, next).then(function (out) {
|
|
8186
8185
|
setBusy(false);
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
setErr(out.error || t("error.saveRejected"));
|
|
8186
|
+
if (!out || out.ok === false) {
|
|
8187
|
+
setErr((out && out.error) || t("entry.err.saveFailed"));
|
|
8190
8188
|
return;
|
|
8191
8189
|
}
|
|
8192
|
-
|
|
8193
|
-
const applied = settings && settings.ui && typeof settings.ui.sidebarEntry === "boolean"
|
|
8194
|
-
? settings.ui.sidebarEntry
|
|
8195
|
-
: !!next;
|
|
8196
|
-
setOn(applied);
|
|
8197
|
-
// 写盘成功之后才动槽位:设置没落盘时界面不该先变一个样。
|
|
8198
|
-
setSidebarEntry(!!next);
|
|
8190
|
+
setMode(entryDisplayMode());
|
|
8199
8191
|
}, function (e) {
|
|
8200
8192
|
setBusy(false);
|
|
8201
8193
|
setErr(pluginCardErr(e));
|
|
@@ -8203,22 +8195,36 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
8203
8195
|
}
|
|
8204
8196
|
|
|
8205
8197
|
return h("div", { className: "smem-plugin-card" }, [
|
|
8206
|
-
h("
|
|
8207
|
-
|
|
8208
|
-
|
|
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
|
+
})),
|
|
8209
8213
|
]),
|
|
8210
|
-
h("
|
|
8211
|
-
h("
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
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,
|
|
8216
8220
|
}),
|
|
8217
|
-
h("
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
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,
|
|
8222
8228
|
]);
|
|
8223
8229
|
}
|
|
8224
8230
|
|
|
@@ -8257,28 +8263,44 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
8257
8263
|
]);
|
|
8258
8264
|
}
|
|
8259
8265
|
|
|
8260
|
-
/**
|
|
8266
|
+
/**
|
|
8267
|
+
* 注册两个槽位(幂等:已经注册就什么都不做)。
|
|
8268
|
+
*
|
|
8269
|
+
* 2.7.0:返回布尔值 —— 侧栏是可选入口,旧宿主没有 sidebar.panellist 时
|
|
8270
|
+
* inject 会抛错。这里必须自己接住并**反序清理**已经注册成功的槽位,
|
|
8271
|
+
* 否则会留下「侧栏条目录入了、main 却没注册」的半状态。
|
|
8272
|
+
*/
|
|
8261
8273
|
function registerSidebarEntry() {
|
|
8262
|
-
if (sidebarDispose !== null
|
|
8274
|
+
if (sidebarDispose !== null) return true;
|
|
8275
|
+
if (!sidebarCtx) return false;
|
|
8263
8276
|
const ctx = sidebarCtx;
|
|
8264
8277
|
const handles = [];
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
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
|
+
}
|
|
8282
8304
|
sidebarDispose = handles;
|
|
8283
8305
|
return true;
|
|
8284
8306
|
}
|
|
@@ -8302,6 +8324,255 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
8302
8324
|
}
|
|
8303
8325
|
return true;
|
|
8304
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
|
+
}
|
|
8305
8576
|
// ⚠️ 不把 `layout` 写进 inject:它是**可选**依赖(官方 inspect:optional access =
|
|
8306
8577
|
// `ctx.get("layout")` + 判空;hardDependency 才是 inject)。写进 inject 会让旧外壳
|
|
8307
8578
|
// 缺这个服务时整个 client 插件不 apply —— 连带丢掉设置页。返回会话优先用宿主给的
|
|
@@ -8324,38 +8595,26 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
8324
8595
|
document.head.appendChild(tag);
|
|
8325
8596
|
}
|
|
8326
8597
|
setupI18n(ctx);
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
order: 20,
|
|
8333
|
-
label: () => t("section.label"),
|
|
8334
|
-
// 声明「记忆星图」子槽:sage-starmap 向它注入星空视图,
|
|
8335
|
-
// 记忆管理页用 renderSlot 渲染。声明 children 后 shell 才会
|
|
8336
|
-
// 向本 section 组件传入 renderSlot。
|
|
8337
|
-
children: { "settings.memory.starmap": { kind: "list", scope: "root" } },
|
|
8338
|
-
},
|
|
8339
|
-
(props) => react.createElement(Section, { ...props, ctx }),
|
|
8340
|
-
),
|
|
8341
|
-
);
|
|
8342
|
-
// ── 1.8.0 侧栏入口:先记住 ctx,再读一次设置决定注不注册 ──────────────
|
|
8343
|
-
// 设置页 section **永远**保留(关闭侧栏入口只是少一个入口,不是少一个设置页)。
|
|
8344
|
-
// 读失败 / 旧宿主没有 ui 字段 / 值坏掉 → 一律按「开」处理,绝不阻塞界面。
|
|
8598
|
+
|
|
8599
|
+
// ── 2.7.0 界面入口三选一 ────────────────────────────────────────────
|
|
8600
|
+
// 先读一次设置,决定注册哪一侧;读失败 / 旧宿主没有 ui.entry / 值坏掉
|
|
8601
|
+
// → 一律按「两个都显示」处理,绝不阻塞界面。
|
|
8602
|
+
// 设置页 section 不再无条件注册:只显示侧栏时它会被注销(78-entry-visibility)。
|
|
8345
8603
|
sidebarCtx = ctx;
|
|
8346
|
-
let
|
|
8604
|
+
let initialMode = "both";
|
|
8347
8605
|
try {
|
|
8348
8606
|
const mem = typeof ctx.get === "function" ? ctx.get("remote.memory") : null;
|
|
8349
8607
|
if (mem && typeof mem.getSettings === "function") {
|
|
8350
8608
|
const loaded = unwrap(await mem.getSettings()) || {};
|
|
8351
|
-
|
|
8609
|
+
entryApiModern = hasModernEntryApi(loaded);
|
|
8610
|
+
initialMode = entryModeFromSettings(loaded);
|
|
8352
8611
|
}
|
|
8353
8612
|
} catch (e) {
|
|
8354
|
-
|
|
8613
|
+
initialMode = "both";
|
|
8355
8614
|
}
|
|
8356
|
-
|
|
8615
|
+
applyEntryMode(initialMode);
|
|
8357
8616
|
|
|
8358
|
-
// ── 1.8.2
|
|
8617
|
+
// ── 1.8.2 / 2.7.0 插件详情页的「界面入口」折叠行 ────────────────────
|
|
8359
8618
|
// `plugins.bundle.config` 是 keyed 槽,key = 本 bundle 的 **package name**(sage-mem)。
|
|
8360
8619
|
// 宿主在插件页的 description 与「包含的组件」行之间渲染它;ownerProps 形如
|
|
8361
8620
|
// `{ view: "summary" | "page" }`,卡片里自己再判一次(summary 返回 null)。
|
package/lib/index.js
CHANGED
|
@@ -430,14 +430,16 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
430
430
|
const norm = (list) => JSON.stringify([...list].map((s) => String(s).toLowerCase()).sort())
|
|
431
431
|
const restartRequired =
|
|
432
432
|
JSON.stringify(limits) !== JSON.stringify(pending.limits) || norm(activeExtra) !== norm(reservedExtra)
|
|
433
|
+
// 2.7.0:entry 是 canonical;sidebarEntry 只为 2.6.0 的客户端保留(派生值,不落盘)。
|
|
434
|
+
const ui = pendingUi(fileRead.data)
|
|
433
435
|
return {
|
|
434
436
|
limits,
|
|
435
437
|
pendingLimits: pending.limits,
|
|
436
438
|
restartRequired,
|
|
437
439
|
// Q04 的诊断出口:面板据此说明「你保存的值没生效,原因是 X」;宿主侧先如实给出,别让问题只能靠猜。
|
|
438
440
|
limitDiagnostics,
|
|
439
|
-
// 1.8.0
|
|
440
|
-
ui:
|
|
441
|
+
// 1.8.0 / 2.7.0:客户端行为(界面入口三选一)。**不参与** restartRequired —— 改完立刻生效。
|
|
442
|
+
ui: { ...ui, sidebarEntry: ui.entry !== 'settings' },
|
|
441
443
|
// 生效的保护名单(含两个内置名)+ 用户声明的那部分原文(面板编辑的是后者)
|
|
442
444
|
reserved: [...new Set([INDEX_FILE_KEY, SESSION_LOG_KEY, ...reservedExtra.map((s) => s.toLowerCase())])],
|
|
443
445
|
reservedExtra,
|
|
@@ -498,7 +500,8 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
498
500
|
}
|
|
499
501
|
const current = currentRead.data
|
|
500
502
|
const mergedLimits = { ...(current?.limits ?? {}), ...nextLimits }
|
|
501
|
-
// ui 只覆盖这次给出的键;没给就沿用文件里的有效值(坏值 /
|
|
503
|
+
// ui 只覆盖这次给出的键;没给就沿用文件里的有效值(坏值 / 缺失一律按默认「两个都显示」)。
|
|
504
|
+
// 2.7.0:落盘只写 canonical 的 entry,旧 sidebarEntry 由 pendingUi 迁移进来、不再写出。
|
|
502
505
|
const mergedUi = { ...pendingUi(current), ...(uiPatch ?? {}) }
|
|
503
506
|
await writeFileAtomic(SETTINGS_FILE, JSON.stringify({ limits: mergedLimits, ui: mergedUi, updatedAt: Date.now() }, null, 2))
|
|
504
507
|
}
|
package/lib/settings.js
CHANGED
|
@@ -21,22 +21,34 @@ export const RESERVED_FILE = join(STATE_ROOT, 'reserved.json')
|
|
|
21
21
|
export const clampInt = (v, spec) => (Number.isFinite(v) ? Math.max(spec.lo, Math.min(spec.hi, Math.round(v))) : spec.dflt)
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* 面板 UI 设置(1.8.0
|
|
24
|
+
* 面板 UI 设置(1.8.0;2.7.0 改成三选一)。
|
|
25
25
|
*
|
|
26
26
|
* ⚠️ 与 `limits` **不是一类**:上限在模块加载时就固化进常量,改完必须重启;
|
|
27
|
-
* `ui.
|
|
27
|
+
* `ui.entry` 是**客户端行为** —— 界面读到值就决定注不注册侧栏 / 设置页槽位,
|
|
28
28
|
* 改完立刻生效。所以它不进 `restartRequired` 的计算。
|
|
29
|
+
*
|
|
30
|
+
* 2.7.0:`entry` 取代 1.8.0 的布尔 `sidebarEntry`。三个值从类型上就保证
|
|
31
|
+
* 「至少保留一个入口」,不需要额外的互斥校验;旧字段只读不写。
|
|
29
32
|
*/
|
|
30
|
-
export const
|
|
33
|
+
export const ENTRY_MODES = Object.freeze(['both', 'sidebar', 'settings'])
|
|
34
|
+
export const DEFAULT_UI = Object.freeze({ entry: 'both' })
|
|
31
35
|
|
|
32
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* 读出 `ui` 的**有效值**:新字段优先,旧 `sidebarEntry` 按
|
|
38
|
+
* `true → both` / `false → settings` 迁移;缺失或坏值回落默认(两个都显示)。
|
|
39
|
+
*/
|
|
33
40
|
export function pendingUi(fileObj) {
|
|
34
41
|
const saved = fileObj?.ui && typeof fileObj.ui === 'object' && !Array.isArray(fileObj.ui) ? fileObj.ui : {}
|
|
35
|
-
|
|
42
|
+
if (ENTRY_MODES.includes(saved.entry)) return { entry: saved.entry }
|
|
43
|
+
if (typeof saved.sidebarEntry === 'boolean') return { entry: saved.sidebarEntry ? 'both' : 'settings' }
|
|
44
|
+
return { entry: DEFAULT_UI.entry }
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
/**
|
|
39
|
-
* 校验 `setSettings({ ui })`
|
|
48
|
+
* 校验 `setSettings({ ui })` 的那一段:认 `entry`(2.7.0)与旧 `sidebarEntry`(1.8.0)。
|
|
49
|
+
*
|
|
50
|
+
* 两个键同时给时要求语义一致(`sidebarEntry === (entry !== 'settings')`),否则报冲突 ——
|
|
51
|
+
* 旧客户端写 true 的语义是「侧栏显示、设置页也保留」,和 `entry:'sidebar'` 不是一回事。
|
|
40
52
|
*
|
|
41
53
|
* 返回 `{ ok:true, patch }`(patch 为 null = 调用方没给 ui,或给的是空对象);`{ ok:false, error }`。
|
|
42
54
|
* 与 limits 同一套严格语义:绝不静默丢掉不认识的键。
|
|
@@ -45,10 +57,30 @@ export function parseUiPatch(ui) {
|
|
|
45
57
|
if (ui === undefined) return { ok: true, patch: null }
|
|
46
58
|
if (!ui || typeof ui !== 'object' || Array.isArray(ui)) return { ok: false, error: 'sage-mem: ui must be an object' }
|
|
47
59
|
const patch = {}
|
|
60
|
+
let hasLegacy = false
|
|
61
|
+
let legacy = null
|
|
48
62
|
for (const [key, value] of Object.entries(ui)) {
|
|
49
|
-
if (key
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
if (key === 'entry') {
|
|
64
|
+
if (typeof value !== 'string' || !ENTRY_MODES.includes(value)) {
|
|
65
|
+
return { ok: false, error: `sage-mem: ui.entry must be one of ${ENTRY_MODES.join(' | ')}, got ${JSON.stringify(value)}` }
|
|
66
|
+
}
|
|
67
|
+
patch.entry = value
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
if (key === 'sidebarEntry') {
|
|
71
|
+
if (typeof value !== 'boolean') return { ok: false, error: `sage-mem: ui.sidebarEntry must be a boolean, got ${typeof value}` }
|
|
72
|
+
hasLegacy = true
|
|
73
|
+
legacy = value
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
return { ok: false, error: `sage-mem: unknown ui setting: ${key}` }
|
|
77
|
+
}
|
|
78
|
+
if (hasLegacy) {
|
|
79
|
+
const derived = legacy ? 'both' : 'settings'
|
|
80
|
+
if (patch.entry !== undefined && patch.entry !== derived) {
|
|
81
|
+
return { ok: false, error: `sage-mem: ui.entry=${patch.entry} conflicts with ui.sidebarEntry=${legacy}` }
|
|
82
|
+
}
|
|
83
|
+
patch.entry = derived
|
|
52
84
|
}
|
|
53
85
|
return { ok: true, patch: Object.keys(patch).length ? patch : null }
|
|
54
86
|
}
|
package/lib/typert.host.js
CHANGED
|
@@ -239,10 +239,9 @@ const settingsLimitsSchema = z.object({
|
|
|
239
239
|
'maxSessionBytes': z.number().readonly(),
|
|
240
240
|
'staleDays': z.number().readonly(),
|
|
241
241
|
}).readonly()
|
|
242
|
-
// 1.8.0:面板 UI 设置。`
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}).readonly()
|
|
242
|
+
// 1.8.0 / 2.7.0:面板 UI 设置。`entry` 是**客户端行为**(改完立刻生效),不进 restartRequired。
|
|
243
|
+
// `sidebarEntry` 是给 2.6.0 客户端的派生值(= entry !== 'settings'),永远随 `entry` 一起返回。
|
|
244
|
+
const settingsUiSchema = z.object({ 'entry': z.enum(['both', 'sidebar', 'settings']).readonly(), 'sidebarEntry': z.boolean().readonly() }).readonly()
|
|
246
245
|
const settingsSchema = z.object({
|
|
247
246
|
'limits': settingsLimitsSchema,
|
|
248
247
|
'pendingLimits': settingsLimitsSchema,
|
|
@@ -260,6 +259,7 @@ const settingsPatchSchema = z.object({
|
|
|
260
259
|
'staleDays': z.number().optional(),
|
|
261
260
|
}).readonly().optional(),
|
|
262
261
|
'ui': z.object({
|
|
262
|
+
'entry': z.enum(['both', 'sidebar', 'settings']).optional(),
|
|
263
263
|
'sidebarEntry': z.boolean().optional(),
|
|
264
264
|
}).readonly().optional(),
|
|
265
265
|
'reserved': z.array(z.string()).optional(),
|
package/package.json
CHANGED