sage-mem 1.7.2 → 1.8.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.md +5 -1
- package/README.zh.md +5 -1
- package/lib/client.js +200 -1
- package/lib/index.js +21 -7
- package/lib/settings.js +33 -0
- package/lib/typert.host.js +8 -0
- package/package.json +1 -1
package/README.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: `1.
|
|
15
|
+
**Current release: `1.8.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.
|
|
@@ -80,6 +80,10 @@ Most memory schemes take the "SQLite + a long-running worker" route: once memori
|
|
|
80
80
|
|
|
81
81
|
The UI also carries four more capabilities (all of them optional): the memory management page, type and tag filtering, the memory star map, and audit & archiving.
|
|
82
82
|
|
|
83
|
+
**Since 1.8.0, memory management is also reachable from the sidebar**: a 「记忆管理」(memory management) icon appears in the sidebar and switches the central panel to this page (with a "back to conversation" control at the top).
|
|
84
|
+
It can be turned off in the settings page under 「记忆管理 → 自动做梦 → 设置」(on by default; applies immediately, no restart);
|
|
85
|
+
**turning it off only removes the sidebar icon — the 「设置 → 记忆管理」 entry always stays**.
|
|
86
|
+
|
|
83
87
|
The memory management page's file list also has an **Open memory folder** button: it opens the
|
|
84
88
|
memory directory in the system file manager (Explorer / Finder / the Linux file manager) through a
|
|
85
89
|
detached host-side process and returns without waiting for the window.
|
package/README.zh.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
|
-
**当前发布版本:`1.
|
|
17
|
+
**当前发布版本:`1.8.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 是最直接的反馈。
|
|
@@ -82,6 +82,10 @@ sage-mem 补上这一层,做法是:**把该记住的事写成一张张卡片
|
|
|
82
82
|
|
|
83
83
|
界面上还有四块能力(都可以不用):记忆管理页、类型与标签筛选、记忆星图、体检与归档。
|
|
84
84
|
|
|
85
|
+
**1.8.0 起,记忆管理还能从侧栏直接进**:侧栏多一个「记忆管理」图标,点它把中央主区切到本页(页顶有「返回会话」)。
|
|
86
|
+
设置页「记忆管理 → 自动做梦 → 设置」里可以关掉这个入口(默认开,立即生效、不用重启);
|
|
87
|
+
**关掉只影响侧栏图标,「设置 → 记忆管理」这个入口永远保留**。
|
|
88
|
+
|
|
85
89
|
记忆管理页的「文件列表」头部还有一个「打开记忆文件夹」按钮:用系统文件管理器打开当前记忆目录
|
|
86
90
|
(Windows Explorer / macOS Finder / Linux 文件管理器)。host 侧起一个 detached 进程就返回,不等窗口。
|
|
87
91
|
|
package/lib/client.js
CHANGED
|
@@ -1772,6 +1772,9 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1772
1772
|
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1773
1773
|
},
|
|
1774
1774
|
{
|
|
1775
|
+
// 1.8.0:结果里多了 `ui.sidebarEntry`(侧栏入口开关)。客户端这份 codec 是
|
|
1776
|
+
// **顶层严格、内部宽松**的 objCodec —— host 加字段不需要改这里(与 audit /
|
|
1777
|
+
// autodream 那批同一套理由);host 侧真正的形状约束在 typert.host.js 的 zod schema。
|
|
1775
1778
|
id: "sage-mem#memory/getSettings",
|
|
1776
1779
|
service: "memory",
|
|
1777
1780
|
namespace: "memory",
|
|
@@ -1782,6 +1785,8 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1782
1785
|
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1783
1786
|
},
|
|
1784
1787
|
{
|
|
1788
|
+
// 1.8.0:patch 可带 `{ ui: { sidebarEntry: boolean } }`。objCodec 只保证顶层是对象,
|
|
1789
|
+
// 字段级严格校验在 host 的 setSettings(非布尔 / 未知键都回 {ok:false})。
|
|
1785
1790
|
id: "sage-mem#memory/setSettings",
|
|
1786
1791
|
service: "memory",
|
|
1787
1792
|
namespace: "memory",
|
|
@@ -2219,6 +2224,13 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2219
2224
|
".smem-tabs-sep{width:1px;align-self:stretch;margin:6px 2px;background:var(--dsw-alias-border-l2);}",
|
|
2220
2225
|
// 窄窗口(≤420px):头部带与筛选条各让一行,卡片动作允许换行、分隔符隐藏。
|
|
2221
2226
|
"@media (max-width:420px){.smem-root{gap:10px;}.smem-head{gap:6px;}.smem-head-actions{flex-wrap:wrap;}.smem-card-title{min-width:0;word-break:break-word;}.smem-actions{flex-wrap:wrap;}.smem-actions-sep{display:none;}.smem-num{width:56px;}.smem-search{min-width:100px;}}",
|
|
2227
|
+
// ── 1.8.0:侧栏入口(设置开关 + 中央主区整页)──
|
|
2228
|
+
".smem-check{display:flex;align-items:center;gap:7px;font-size:12.5px;cursor:pointer;user-select:none;}",
|
|
2229
|
+
".smem-check input{margin:0;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#4d76e6);}",
|
|
2230
|
+
".smem-page{display:flex;flex-direction:column;gap:10px;padding:14px 16px 18px;box-sizing:border-box;min-height:0;overflow:auto;}",
|
|
2231
|
+
".smem-page-top{display:flex;align-items:center;}",
|
|
2232
|
+
".smem-page-back{display:inline-flex;align-items:center;gap:5px;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:12.5px;line-height:1.4;padding:3px 6px 3px 2px;border-radius:6px;cursor:pointer;}",
|
|
2233
|
+
".smem-page-back:hover{color:var(--dsw-alias-label-primary,#1f2329);background:var(--dsw-alias-bg-layer-2);}",
|
|
2222
2234
|
// ── 底部一行仓库链接(引流,不抢视线)──
|
|
2223
2235
|
".smem-star{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-tertiary,#8b93a1);padding:2px 2px 0;}",
|
|
2224
2236
|
".smem-star-link{color:var(--dsw-alias-brand-primary,#4d76e6);text-decoration:underline;}",
|
|
@@ -3086,6 +3098,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
3086
3098
|
const sLimitsDraft = react.useState(null); const limitsDraft = sLimitsDraft[0], setLimitsDraft = sLimitsDraft[1];
|
|
3087
3099
|
const sSaveBusy = react.useState(false); const saveBusy = sSaveBusy[0], setSaveBusy = sSaveBusy[1];
|
|
3088
3100
|
const sReservedNew = react.useState(""); const reservedNew = sReservedNew[0], setReservedNew = sReservedNew[1];
|
|
3101
|
+
// ── 1.8.0 侧栏入口开关(客户端行为:改完立刻生效,不需要重启)──
|
|
3102
|
+
const sSidebarBusy = react.useState(false); const sidebarBusy = sSidebarBusy[0], setSidebarBusy = sSidebarBusy[1];
|
|
3103
|
+
const sSidebarErr = react.useState(""); const sidebarErr = sSidebarErr[0], setSidebarErr = sSidebarErr[1];
|
|
3089
3104
|
|
|
3090
3105
|
// 派生量放在 effect 之前:下面几个 effect 的依赖数组要在渲染期就能取到值。
|
|
3091
3106
|
const c = cfg || {};
|
|
@@ -3100,6 +3115,8 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
3100
3115
|
const fixedReserved = ((memSet && memSet.reserved) || []).filter(function (n) {
|
|
3101
3116
|
return extraLower.indexOf(String(n).toLowerCase()) < 0;
|
|
3102
3117
|
});
|
|
3118
|
+
// 默认开:宿主没回 ui 字段(旧版本)或值不是布尔,都按「开」显示。
|
|
3119
|
+
const sidebarEntryOn = !(memSet && memSet.ui && memSet.ui.sidebarEntry === false);
|
|
3103
3120
|
|
|
3104
3121
|
react.useEffect(function () {
|
|
3105
3122
|
if (!remote) { setErr("remote.autodream 不可用 —— 插件的 宿主侧可能没加载成功,看 DSH 启动日志"); return undefined; }
|
|
@@ -3210,6 +3227,31 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
3210
3227
|
}, function (e) { setSaveBusy(false); setErr(detail(e)); });
|
|
3211
3228
|
}
|
|
3212
3229
|
|
|
3230
|
+
/**
|
|
3231
|
+
* 侧栏入口开关(1.8.0)。
|
|
3232
|
+
*
|
|
3233
|
+
* 顺序是「先写设置、成功后再动槽位」:设置没落盘就不该让界面先变一个样。
|
|
3234
|
+
* 失败只报错、不改任何东西 —— checkbox 的勾选状态由 memSet 派生,于是自动回到原值。
|
|
3235
|
+
* 成功后调 `setSidebarEntry`:**立即**增删侧栏槽位,不刷新、不重启。
|
|
3236
|
+
*/
|
|
3237
|
+
function saveSidebarEntry(next) {
|
|
3238
|
+
if (!memRemote || typeof memRemote.setSettings !== "function") {
|
|
3239
|
+
setSidebarErr("宿主侧还没有 setSettings 接口(插件版本较旧)");
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
if (sidebarBusy) return;
|
|
3243
|
+
setSidebarBusy(true); setSidebarErr(""); setNotice("");
|
|
3244
|
+
Promise.resolve(memRemote.setSettings({ ui: { sidebarEntry: !!next } })).then(function (out) {
|
|
3245
|
+
setSidebarBusy(false);
|
|
3246
|
+
// 与 saveLimits 同款:setSettings 是 {ok, settings, restartRequired} 形状,不要过 unwrap。
|
|
3247
|
+
if (out && out.ok === false) { setSidebarErr(out.error || "保存被拒绝"); return; }
|
|
3248
|
+
if (out && out.settings) setMemSet(out.settings);
|
|
3249
|
+
else loadMemSettings();
|
|
3250
|
+
setSidebarEntry(!!next);
|
|
3251
|
+
setNotice(!!next ? "侧栏入口已开启(立即生效)" : "侧栏入口已关闭(设置页入口仍在)");
|
|
3252
|
+
}, function (e) { setSidebarBusy(false); setSidebarErr(detail(e)); });
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3213
3255
|
/**
|
|
3214
3256
|
* 保存保留名名单(用户声明的那部分)。
|
|
3215
3257
|
*
|
|
@@ -3481,6 +3523,26 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
3481
3523
|
]),
|
|
3482
3524
|
]) : null,
|
|
3483
3525
|
segOf === "settings" ? h("div", { key: "seg-set" }, [
|
|
3526
|
+
// 1.8.0:侧栏入口开关。默认开;关掉只影响侧栏那个图标,
|
|
3527
|
+
// 「设置 → 记忆管理」这个入口永远在(它注册在 80-apply,和这个开关无关)。
|
|
3528
|
+
h("div", { className: "smem-autodream-sec", key: "sidebar-entry" }, [
|
|
3529
|
+
h("div", { className: "smem-autodream-row", key: "h" }, [
|
|
3530
|
+
h("span", { key: "t", style: { fontWeight: "600", fontSize: "12.5px" } }, "侧栏入口"),
|
|
3531
|
+
h("span", { className: "smem-status", key: "s" }, sidebarEntryOn ? "已开启" : "已关闭"),
|
|
3532
|
+
]),
|
|
3533
|
+
h("div", { className: "smem-autodream-note", key: "n" },
|
|
3534
|
+
"在 DSH 侧栏显示「记忆管理」图标,点它把中央主区切到本页。默认开启;关闭后「设置 → 记忆管理」这个入口仍然保留。改动立即生效,不需要重启。"),
|
|
3535
|
+
h("label", { className: "smem-check", key: "row" }, [
|
|
3536
|
+
h("input", {
|
|
3537
|
+
type: "checkbox", key: "c",
|
|
3538
|
+
checked: sidebarEntryOn,
|
|
3539
|
+
disabled: sidebarBusy,
|
|
3540
|
+
onChange: function (e) { saveSidebarEntry(!!(e.target && e.target.checked)); },
|
|
3541
|
+
}),
|
|
3542
|
+
h("span", { key: "l" }, "在侧栏显示「记忆管理」入口"),
|
|
3543
|
+
]),
|
|
3544
|
+
sidebarErr ? h("div", { className: "smem-autodream-note smem-ad-warn", key: "err" }, sidebarErr) : null,
|
|
3545
|
+
]),
|
|
3484
3546
|
// 自动归档那一段(三档策略 + 三个阈值)**已挪到「自动归档」页签**(v0.9.9):
|
|
3485
3547
|
// 归档是确定性规则、不调模型,和这一页的「让模型整理」不是一件事;
|
|
3486
3548
|
// 同一件事只在一处设定,这里不再重复摆一份。
|
|
@@ -5057,11 +5119,133 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5057
5119
|
]);
|
|
5058
5120
|
}
|
|
5059
5121
|
|
|
5122
|
+
// ── 侧栏入口(1.8.0)─────────────────────────────────────────────
|
|
5123
|
+
// 宿主侧栏的官方契约是两个槽位:
|
|
5124
|
+
// `sidebar.panellist`(list 槽):只画一个 size 见方的图标,行标题由宿主从 label 投影;
|
|
5125
|
+
// `main`(keyed 槽):key **必须**与侧栏条目 id 相同,选中缺失 key 宿主会直接抛错。
|
|
5126
|
+
// 开关(settings.ui.sidebarEntry)是**客户端行为**:这里持有两个 inject 的清理句柄,
|
|
5127
|
+
// 关 → 调清理器(宿主会连带注销 register 进去的条目),开 → 重新 inject/register。
|
|
5128
|
+
const SIDEBAR_ENTRY_ID = "sage-mem";
|
|
5129
|
+
/** 插件 ctx(80-apply 在 apply 时挂进来);setSidebarEntry 之后要拿它重注册。 */
|
|
5130
|
+
let sidebarCtx = null;
|
|
5131
|
+
/** 该槽位的清理句柄数组(null = 当前没注册)。 */
|
|
5132
|
+
let sidebarDispose = null;
|
|
5133
|
+
|
|
5134
|
+
/** 侧栏图标:内联 SVG、currentColor、细线 —— 不引外部图片/字体,跟随宿主明暗主题。 */
|
|
5135
|
+
function SidebarEntryIcon(props) {
|
|
5136
|
+
const edge = typeof props.size === "number" && props.size > 0 ? props.size : 16;
|
|
5137
|
+
return h("svg", {
|
|
5138
|
+
width: edge, height: edge, viewBox: "0 0 16 16", fill: "none",
|
|
5139
|
+
"aria-hidden": "true", focusable: "false",
|
|
5140
|
+
}, [
|
|
5141
|
+
h("rect", {
|
|
5142
|
+
key: "b", x: "2", y: "2.5", width: "12", height: "11", rx: "2.2",
|
|
5143
|
+
stroke: "currentColor", strokeWidth: "1.2",
|
|
5144
|
+
}),
|
|
5145
|
+
h("path", {
|
|
5146
|
+
key: "l1", d: "M5.2 6.2h5.6",
|
|
5147
|
+
stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round",
|
|
5148
|
+
}),
|
|
5149
|
+
h("path", {
|
|
5150
|
+
key: "l2", d: "M5.2 8.4h5.6",
|
|
5151
|
+
stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round",
|
|
5152
|
+
}),
|
|
5153
|
+
h("path", {
|
|
5154
|
+
key: "l3", d: "M5.2 10.6h3.2",
|
|
5155
|
+
stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round",
|
|
5156
|
+
}),
|
|
5157
|
+
]);
|
|
5158
|
+
}
|
|
5159
|
+
|
|
5160
|
+
/**
|
|
5161
|
+
* 返回会话:宿主 layout 服务的 `selectPanel(null)` 就是把中央主区还给 Conversation。
|
|
5162
|
+
* 取不到 layout(旧外壳 / 测试环境)时是**空操作**,不影响页面其它部分。
|
|
5163
|
+
*/
|
|
5164
|
+
function backToConversation() {
|
|
5165
|
+
try {
|
|
5166
|
+
const layout = sidebarCtx && typeof sidebarCtx.get === "function" ? sidebarCtx.get("layout") : null;
|
|
5167
|
+
if (layout && typeof layout.selectPanel === "function") layout.selectPanel(null);
|
|
5168
|
+
} catch (e) {
|
|
5169
|
+
// 返回按钮不能因为宿主差异把整页搞崩
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
/** 中央主区的整页:页顶「返回会话」+ 复用设置页那份 Section。 */
|
|
5174
|
+
function MemoryPanelPage(props) {
|
|
5175
|
+
const onBack = typeof props.backToConversation === "function" ? props.backToConversation : backToConversation;
|
|
5176
|
+
return h("div", { className: "smem-page" }, [
|
|
5177
|
+
h("div", { className: "smem-page-top", key: "top" }, [
|
|
5178
|
+
h("button", {
|
|
5179
|
+
type: "button", className: "smem-page-back", key: "back",
|
|
5180
|
+
title: "返回会话", onClick: onBack,
|
|
5181
|
+
}, [
|
|
5182
|
+
h("svg", {
|
|
5183
|
+
key: "i", width: 14, height: 14, viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true",
|
|
5184
|
+
}, h("path", {
|
|
5185
|
+
d: "M10 3.5 5.5 8l4.5 4.5", stroke: "currentColor", strokeWidth: "1.6",
|
|
5186
|
+
strokeLinecap: "round", strokeLinejoin: "round",
|
|
5187
|
+
})),
|
|
5188
|
+
h("span", { key: "l" }, "返回会话"),
|
|
5189
|
+
]),
|
|
5190
|
+
]),
|
|
5191
|
+
h(Section, { key: "sec", ...props, ctx: sidebarCtx }),
|
|
5192
|
+
]);
|
|
5193
|
+
}
|
|
5194
|
+
|
|
5195
|
+
/** 注册两个槽位(幂等:已经注册就什么都不做)。 */
|
|
5196
|
+
function registerSidebarEntry() {
|
|
5197
|
+
if (sidebarDispose !== null || !sidebarCtx) return true;
|
|
5198
|
+
const ctx = sidebarCtx;
|
|
5199
|
+
const handles = [];
|
|
5200
|
+
// 侧栏 list 槽:只给图标与 label,按钮由宿主自己画。
|
|
5201
|
+
handles.push(ctx.slots.inject("sidebar.panellist", function () {
|
|
5202
|
+
return ctx.slots.register({
|
|
5203
|
+
name: "sidebar.panellist",
|
|
5204
|
+
id: SIDEBAR_ENTRY_ID,
|
|
5205
|
+
order: 20,
|
|
5206
|
+
label: "记忆管理",
|
|
5207
|
+
}, SidebarEntryIcon);
|
|
5208
|
+
}));
|
|
5209
|
+
// 中央主区 keyed 槽:key 与侧栏 id 相同;inject 面把「返回会话」交给页面组件。
|
|
5210
|
+
handles.push(ctx.slots.inject("main", function () {
|
|
5211
|
+
return ctx.slots.register({
|
|
5212
|
+
name: "main",
|
|
5213
|
+
key: SIDEBAR_ENTRY_ID,
|
|
5214
|
+
inject: function () { return { backToConversation: backToConversation }; },
|
|
5215
|
+
}, MemoryPanelPage);
|
|
5216
|
+
}));
|
|
5217
|
+
sidebarDispose = handles;
|
|
5218
|
+
return true;
|
|
5219
|
+
}
|
|
5220
|
+
|
|
5221
|
+
/**
|
|
5222
|
+
* 开 / 关侧栏入口(立即生效,不改设置文件 —— 写盘由设置页负责)。
|
|
5223
|
+
*
|
|
5224
|
+
* 关闭顺序是**反序**:先摘 main、再摘 sidebar,避免宿主在「侧栏条目还在、main 已没了」
|
|
5225
|
+
* 的中间态里去解析一个不存在的 key(官方契约里那会抛错)。
|
|
5226
|
+
*/
|
|
5227
|
+
function setSidebarEntry(enabled) {
|
|
5228
|
+
if (enabled) return registerSidebarEntry();
|
|
5229
|
+
const handles = sidebarDispose;
|
|
5230
|
+
sidebarDispose = null;
|
|
5231
|
+
if (Array.isArray(handles)) {
|
|
5232
|
+
for (let i = handles.length - 1; i >= 0; i--) {
|
|
5233
|
+
const dispose = handles[i];
|
|
5234
|
+
if (typeof dispose !== "function") continue;
|
|
5235
|
+
try { dispose(); } catch (e) { /* 注销失败也不该把开关卡死在「已注册」状态 */ }
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
return true;
|
|
5239
|
+
}
|
|
5240
|
+
// ⚠️ 不把 `layout` 写进 inject:它是**可选**依赖(官方 inspect:optional access =
|
|
5241
|
+
// `ctx.get("layout")` + 判空;hardDependency 才是 inject)。写进 inject 会让旧外壳
|
|
5242
|
+
// 缺这个服务时整个 client 插件不 apply —— 连带丢掉设置页。返回会话优先用宿主给的
|
|
5243
|
+
// `props.backToConversation`,layout 只是兜底。
|
|
5060
5244
|
const inject = ["slots", "remote"];
|
|
5061
5245
|
|
|
5062
5246
|
/**
|
|
5063
5247
|
* Client 插件:$mount strict remote contribution(第三方 remote 不在
|
|
5064
|
-
* 宿主的固定 remote
|
|
5248
|
+
* 宿主的固定 remote 清单里,必须自己挂),再注册设置页签 / 侧栏入口。
|
|
5065
5249
|
*/
|
|
5066
5250
|
async function apply(ctx) {
|
|
5067
5251
|
await ctx.remote.$mount(TYPERT_REMOTE);
|
|
@@ -5089,6 +5273,21 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5089
5273
|
(props) => react.createElement(Section, { ...props, ctx }),
|
|
5090
5274
|
),
|
|
5091
5275
|
);
|
|
5276
|
+
// ── 1.8.0 侧栏入口:先记住 ctx,再读一次设置决定注不注册 ──────────────
|
|
5277
|
+
// 设置页 section **永远**保留(关闭侧栏入口只是少一个入口,不是少一个设置页)。
|
|
5278
|
+
// 读失败 / 旧宿主没有 ui 字段 / 值坏掉 → 一律按「开」处理,绝不阻塞界面。
|
|
5279
|
+
sidebarCtx = ctx;
|
|
5280
|
+
let sidebarEnabled = true;
|
|
5281
|
+
try {
|
|
5282
|
+
const mem = typeof ctx.get === "function" ? ctx.get("remote.memory") : null;
|
|
5283
|
+
if (mem && typeof mem.getSettings === "function") {
|
|
5284
|
+
const loaded = unwrap(await mem.getSettings()) || {};
|
|
5285
|
+
if (loaded.ui && loaded.ui.sidebarEntry === false) sidebarEnabled = false;
|
|
5286
|
+
}
|
|
5287
|
+
} catch (e) {
|
|
5288
|
+
sidebarEnabled = true;
|
|
5289
|
+
}
|
|
5290
|
+
setSidebarEntry(sidebarEnabled);
|
|
5092
5291
|
}
|
|
5093
5292
|
|
|
5094
5293
|
exports.apply = apply;
|
package/lib/index.js
CHANGED
|
@@ -32,7 +32,7 @@ import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
|
|
32
32
|
import { mkdir, unlink, writeFile } from 'node:fs/promises'
|
|
33
33
|
import { join } from 'node:path'
|
|
34
34
|
import { checkUpdate as checkUpdateImpl } from './update.js'
|
|
35
|
-
import { autodreamConfigSnapshot, clampInt, MAX_RAW_BYTES, pendingLimits, rawName, readReservedFile, readSettingsFile, RESERVED_FILE, SETTINGS_FILE } from './settings.js'
|
|
35
|
+
import { autodreamConfigSnapshot, clampInt, MAX_RAW_BYTES, parseUiPatch, pendingLimits, pendingUi, rawName, readReservedFile, readSettingsFile, RESERVED_FILE, SETTINGS_FILE } from './settings.js'
|
|
36
36
|
import { createRecallInjector, createSessionInjectState, readRecallDebug } from './recall/index.js'
|
|
37
37
|
import { mountAutodream, mountMemoryTools, mountObserve, mountWriteGate } from './assembly/index.js'
|
|
38
38
|
|
|
@@ -451,6 +451,8 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
451
451
|
limits,
|
|
452
452
|
pendingLimits: pending,
|
|
453
453
|
restartRequired,
|
|
454
|
+
// 1.8.0:客户端行为(侧栏入口开关)。**不参与** restartRequired —— 改完立刻生效。
|
|
455
|
+
ui: pendingUi(file),
|
|
454
456
|
// 生效的保护名单(含两个内置名)+ 用户声明的那部分原文(面板编辑的是后者)
|
|
455
457
|
reserved: [...new Set([INDEX_FILE_KEY, SESSION_LOG_KEY, ...reservedExtra.map((s) => s.toLowerCase())])],
|
|
456
458
|
reservedExtra,
|
|
@@ -495,19 +497,31 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
495
497
|
}
|
|
496
498
|
nextReserved = cleaned
|
|
497
499
|
}
|
|
498
|
-
|
|
500
|
+
// 1.8.0:ui 与 limits / reserved 同一套「先全验、后一起写」—— 任何一个非法,整份 patch 不落盘。
|
|
501
|
+
const uiCheck = parseUiPatch(p.ui)
|
|
502
|
+
if (!uiCheck.ok) return { ok: false, error: uiCheck.error }
|
|
503
|
+
const uiPatch = uiCheck.patch
|
|
504
|
+
if (!limitsPatch && nextReserved === null && uiPatch === null) return { ok: false, error: 'sage-mem: empty patch' }
|
|
499
505
|
|
|
500
506
|
await mkdir(STATE_ROOT, { recursive: true })
|
|
501
|
-
if (limitsPatch) {
|
|
507
|
+
if (limitsPatch || uiPatch !== null) {
|
|
502
508
|
const current = await readSettingsFile()
|
|
503
|
-
const
|
|
504
|
-
|
|
509
|
+
const mergedLimits = { ...(current?.limits ?? {}), ...nextLimits }
|
|
510
|
+
// ui 只覆盖这次给出的键;没给就沿用文件里的有效值(坏值 / 缺失一律按默认「开」)
|
|
511
|
+
const mergedUi = { ...pendingUi(current), ...(uiPatch ?? {}) }
|
|
512
|
+
await writeFileAtomic(SETTINGS_FILE, JSON.stringify({ limits: mergedLimits, ui: mergedUi, updatedAt: Date.now() }, null, 2))
|
|
505
513
|
}
|
|
506
514
|
if (nextReserved !== null) {
|
|
507
515
|
await writeFileAtomic(RESERVED_FILE, `${JSON.stringify(nextReserved, null, 2)}\n`)
|
|
508
516
|
}
|
|
509
|
-
//
|
|
510
|
-
|
|
517
|
+
// 上限 / 保护名单在模块加载时就固化了,写盘即提示重启;ui 是**客户端行为**,改完立刻生效,
|
|
518
|
+
// 所以只有前两者会强制 true,纯 ui 补丁照实回传(通常就是 false)。
|
|
519
|
+
const settings = await this.getSettings()
|
|
520
|
+
return {
|
|
521
|
+
ok: true,
|
|
522
|
+
settings,
|
|
523
|
+
restartRequired: limitsPatch || nextReserved !== null ? true : settings.restartRequired,
|
|
524
|
+
}
|
|
511
525
|
}
|
|
512
526
|
|
|
513
527
|
/**
|
package/lib/settings.js
CHANGED
|
@@ -20,6 +20,39 @@ export const RESERVED_FILE = join(STATE_ROOT, 'reserved.json')
|
|
|
20
20
|
/** 整数收敛(与 config.js 的 envInt 同一套语义):非有限数回落默认,越界夹到边界。 */
|
|
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
|
+
/**
|
|
24
|
+
* 面板 UI 设置(1.8.0)。
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ 与 `limits` **不是一类**:上限在模块加载时就固化进常量,改完必须重启;
|
|
27
|
+
* `ui.sidebarEntry` 是**客户端行为** —— 界面读到值就决定注不注册侧栏槽位,
|
|
28
|
+
* 改完立刻生效。所以它不进 `restartRequired` 的计算。
|
|
29
|
+
*/
|
|
30
|
+
export const DEFAULT_UI = Object.freeze({ sidebarEntry: true })
|
|
31
|
+
|
|
32
|
+
/** 读出 `ui` 的**有效值**:文件里没有 / 坏对象 / 不是布尔 → 一律回落默认(开)。 */
|
|
33
|
+
export function pendingUi(fileObj) {
|
|
34
|
+
const saved = fileObj?.ui && typeof fileObj.ui === 'object' && !Array.isArray(fileObj.ui) ? fileObj.ui : {}
|
|
35
|
+
return { sidebarEntry: typeof saved.sidebarEntry === 'boolean' ? saved.sidebarEntry : DEFAULT_UI.sidebarEntry }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 校验 `setSettings({ ui })` 的那一段:**只认 `sidebarEntry` 一个键**,值必须是布尔。
|
|
40
|
+
*
|
|
41
|
+
* 返回 `{ ok:true, patch }`(patch 为 null = 调用方没给 ui,或给的是空对象);`{ ok:false, error }`。
|
|
42
|
+
* 与 limits 同一套严格语义:绝不静默丢掉不认识的键。
|
|
43
|
+
*/
|
|
44
|
+
export function parseUiPatch(ui) {
|
|
45
|
+
if (ui === undefined) return { ok: true, patch: null }
|
|
46
|
+
if (!ui || typeof ui !== 'object' || Array.isArray(ui)) return { ok: false, error: 'sage-mem: ui must be an object' }
|
|
47
|
+
const patch = {}
|
|
48
|
+
for (const [key, value] of Object.entries(ui)) {
|
|
49
|
+
if (key !== 'sidebarEntry') return { ok: false, error: `sage-mem: unknown ui setting: ${key}` }
|
|
50
|
+
if (typeof value !== 'boolean') return { ok: false, error: `sage-mem: ui.sidebarEntry must be a boolean, got ${typeof value}` }
|
|
51
|
+
patch.sidebarEntry = value
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, patch: Object.keys(patch).length ? patch : null }
|
|
54
|
+
}
|
|
55
|
+
|
|
23
56
|
/** 读 `<状态根>/settings.json`;缺失、坏 JSON、不是对象都当「没设置」。 */
|
|
24
57
|
export async function readSettingsFile() {
|
|
25
58
|
try {
|
package/lib/typert.host.js
CHANGED
|
@@ -222,10 +222,15 @@ const settingsLimitsSchema = z.object({
|
|
|
222
222
|
'maxSessionBytes': z.number().readonly(),
|
|
223
223
|
'staleDays': z.number().readonly(),
|
|
224
224
|
}).readonly()
|
|
225
|
+
// 1.8.0:面板 UI 设置。`sidebarEntry` 是**客户端行为**(改完立刻生效),不进 restartRequired。
|
|
226
|
+
const settingsUiSchema = z.object({
|
|
227
|
+
'sidebarEntry': z.boolean().readonly(),
|
|
228
|
+
}).readonly()
|
|
225
229
|
const settingsSchema = z.object({
|
|
226
230
|
'limits': settingsLimitsSchema,
|
|
227
231
|
'pendingLimits': settingsLimitsSchema,
|
|
228
232
|
'restartRequired': z.boolean().readonly(),
|
|
233
|
+
'ui': settingsUiSchema,
|
|
229
234
|
'reserved': z.array(z.string()).readonly(),
|
|
230
235
|
'reservedExtra': z.array(z.string()).readonly(),
|
|
231
236
|
}).readonly()
|
|
@@ -237,6 +242,9 @@ const settingsPatchSchema = z.object({
|
|
|
237
242
|
'maxSessionBytes': z.number().optional(),
|
|
238
243
|
'staleDays': z.number().optional(),
|
|
239
244
|
}).readonly().optional(),
|
|
245
|
+
'ui': z.object({
|
|
246
|
+
'sidebarEntry': z.boolean().optional(),
|
|
247
|
+
}).readonly().optional(),
|
|
240
248
|
'reserved': z.array(z.string()).optional(),
|
|
241
249
|
}).readonly()
|
|
242
250
|
// 成功回 { ok, settings, restartRequired },失败回 { ok:false, error } —— 两条分支并集。
|
package/package.json
CHANGED