dsh-memory-core 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4
4
+
5
+ - 默认改为 **full 五轨**(memory / user / project / key / daily)。原先默认是 lite 三轨,装完只有三轨,得手改配置才能拿到完整记忆;老配置里显式写 `memoryTracks: lite` 的仍按三轨跑。
6
+ - 记忆管理页 `manage.html` 按 dsh-memory-evolve 的记忆页形制重做:顶部页签(五轨 + 三份归档,带条数)、文件条(名称 / 条数 / 完整路径 / 复制路径)、美观视图与纯文本视图切换、搜索框、条目卡片(编辑 / 归档 / 删除)、待确认队列(允许并写入 / 拒绝)、归档区(移回主轨 / 删除)、新增条目框。
7
+ - 新增 `GET /api/memory-core/files`:一次返回每轨路径、是否存在、条数、归档数、待确认数,以及当前模式、数据目录、preset 名。面板与页签都用它,不再按前端硬编码的三轨逐个查。
8
+ - `GET /api/memory-core/entries` 与 `POST /api/memory-core/entry` 支持 `?cwd=` / `body.cwd`:管理页在别的会话里打开时也能读写项目记忆与关键事实(这两轨按会话工作目录隔离)。
9
+ - 插件详情页面板改用 `/files`,模式、条数、归档数按实际值显示;「打开记忆管理页」改成按钮样式并提到顶部。
10
+ - 管理页细节:条目改成**最新在前**(记忆文件是追加写,原先按文件顺序渲染会把最旧的排最上面);每页 20 条分页;没有 `?cwd=` 时顶部给输入框,填一次记在浏览器里;「项目日志」页签不再报 `无效的归档轨 "project"`。
11
+ - 工具面补齐:`list` 支持 `filter` / `since` / `until` / `limit`(默认 50)/ `recent`,返回总条数与文件路径;`add` 支持 `entries` 批量写(仅 daily/project);`replace` 保留原时间戳,不再重盖;子代理只读,不能往常驻轨写。
12
+ - 误伤防护:`archive` / `promote` / `removeArchive` / `replace` / `remove` 都接受整条原文或唯一子串,子串命中多条时明确报「命中 N 条,无法确定」,不做模糊删除;`archive` 只支持 memory/user/key 三轨。
13
+ - 待确认队列带 `cwd` / `reason` / 命中次数并去重;`confirm` / `reject` 按「轨 + 内容」定位,未传轨时退回只按内容匹配。
14
+ - 看门狗在 full 模式下只认 `daily` / `project` 的写入(lite 模式退回任意轨),子代理不提醒;面板的写操作返回真实结果,`promote` 会剥掉归档时附加的理由行。
15
+ - 项目轨 / 关键事实在取不到会话工作目录时,`list` 直接报错说明原因,不再静默返回 0 条让人以为没有记忆。
16
+
17
+ ## 0.1.3
18
+
19
+ - 修复:路由与注入段直接访问 `ctx.agent`。Cordis 的 `ctx.<服务>` 在插件未声明 `inject` 时会抛 `cannot get property "agent" without inject`,被 webserver 兜底成 **400 空体**:`/api/memory-core/status`、`/manage`、`/entries` 全部打不开,`memory-core:snapshot` 注入同时失效,日志里也没有堆栈。改为 `agentOf(ctx)`,只经 `ctx.get(name, false)` 探测(取不到返回 undefined;lite 三轨不依赖 agent)。
20
+ - `_dryrun.mjs`:stub ctx 补 `get(name)`(模拟真实服务读取),并加 8d2 回归断言——`index.js` 里出现 `ctx.agent` 即失败。
21
+
3
22
  ## 0.1.2
4
23
 
5
24
  - 修复:`lib/schema.js` 用 `z.enum(...)`(zod 的写法,`@deepseek-ai/schemastery` 没有这个方法),包一 import 就抛 `TypeError: z.enum is not a function`。内核 Loader 的 `_init()` 捕获 import 错误后不建 fiber,于是 preset 里这一行只显示「never started」,日志里不落错。改为 `z.union([z.const("lite"), z.const("full")])`。
package/README.md CHANGED
@@ -11,11 +11,27 @@
11
11
  dsh plugin --profile web add dsh-memory-core
12
12
  ```
13
13
 
14
- 装完还要**手动启用一次**:包默认是关闭的(`cordis.patch.yml` 里 `disabled: true`),不启用的话重启后没有工具、没有记忆注入、面板也是空的,而且不报错。
14
+ 装完默认是关闭的(包内 `cordis.patch.yml` 写了 `disabled: true`),插件列表里会显示「已关闭」——这是预期行为,不是装错了:装上只是把代码放好,不会给任何会话加记忆。
15
15
 
16
- 1. 打开 `%USERPROFILE%\.dsh\profiles\web\cordis.patch.yml`(macOS/Linux 下是 `~/.dsh/profiles/web/cordis.patch.yml`)。
17
- 2. 找到 `id: memory-core` 那一段,把 `disabled: true` 改成 `disabled: false`,或直接删掉这一行。
18
- 3. 完全退出并重启 DSH,再新建会话。
16
+ 启用有两条路,推荐第一条:
17
+
18
+ 1. **挂进目标 preset(推荐)**:在该 preset 的 `plugins:` 里加两项(见下面「preset 挂载」),重启后新建会话即生效。数据按 preset 名分目录,不影响其他会话。
19
+ 2. 全局启用(不推荐):把 `cordis.patch.yml` 里 `id: memory-core` 那段的 `disabled: true` 改成 `false`,重启。这样所有会话都会多一套记忆工具与注入,只在单会话环境里合适。
20
+
21
+ 判断它是否真的在跑,看插件详情页有没有条数,或 `GET /api/memory-core/status` 的 `memoryTracks` 是不是五轨;不要看插件列表那颗开关,它管的是 profile 层。
22
+
23
+ > **别同时开两处**:如果你已经用 preset 挂载了本包,就不要再打开插件列表上的开关——两个实例会抢同一个 `/api/memory-core` 路由,后挂载的那个 preset 会报 `duplicate prefix route` 并整体加载失败。真遇到了:把列表开关关掉、重启,数据不会丢。
24
+
25
+ ## preset 挂载
26
+
27
+ ```yaml
28
+ - id: memory-core
29
+ name: dsh-memory-core
30
+ config:
31
+ presetName: <preset 名>
32
+ ```
33
+
34
+ `presetName` 决定数据落点 `~/.dsh-plugin-data/<preset 名>/memory/`,建议与 preset 的 `id` 一致。
19
35
 
20
36
  前提:已安装 `dsh-memory-shield`(shield 包)。两包配对时建议只把 `dsh-memory-core` 挂进需要独立记忆的那个 preset,不要放进全局 `dsh.profile.bundles`。
21
37
 
@@ -23,33 +39,30 @@ dsh plugin --profile web add dsh-memory-core
23
39
 
24
40
  | 配置项 | 默认值 | 说明 |
25
41
  |---|---|---|
26
- | `memoryTracks` | `"lite"` | 记忆轨模式:`lite` = 三轨 memory/user/key;`full` = 五轨 memory/user/project/key/daily |
42
+ | `memoryTracks` | `"full"` | 记忆轨模式:`full` = 五轨 memory/user/project/key/daily(默认);`lite` = 三轨 memory/user/key |
27
43
  | `presetName` | `"default"` | preset 名称(影响数据目录落点) |
28
44
  | `memoryDir` | `""` | 记忆数据目录(留空自动解析到 `~/.dsh-plugin-data/<presetName>/memory/`) |
29
45
  | `writeGuard` | `true` | key 轨写入走待确认队列(防误写) |
30
46
  | `writeGuardThreshold` | `2` | 连续 N 轮没写记忆就提醒 |
31
47
  | `snapshotOrder` | `500` | memory-core:snapshot 注入顺序 |
32
48
 
49
+ 上面几项都可以在插件详情页的「配置」页签里改,落点是一份覆盖层 `~/.dsh-plugin-data/<presetName>/memory-core.config.json`,每轮重读,改完立即生效(`snapshotOrder` 这种注册期属性除外,要重启)。
50
+
33
51
  ## 记忆轨
34
52
 
35
- ### lite 模式(默认,三轨)
53
+ ### full 模式(默认,五轨)
36
54
 
37
55
  | 轨 | 文件 | 用途 |
38
56
  |---|---|---|
39
57
  | `memory` | `memory.md` | 长期记忆(跨会话复用) |
40
58
  | `user` | `user.md` | 用户偏好与画像 |
41
- | `key` | `key.md` | 关键事实(走待确认队列) |
42
-
43
- ### full 模式(五轨)
44
-
45
- 在 lite 三轨基础上加:
46
-
47
- | 轨 | 文件 | 用途 |
48
- |---|---|---|
59
+ | `key` | `projects/<hash>/key.md` | 关键事实(走待确认队列,按 cwd 隔离) |
49
60
  | `project` | `projects/<hash>/memory.md` | 项目记忆(按 cwd 隔离) |
50
61
  | `daily` | `daily/<date>.md` | 每日日志(编年史) |
51
62
 
52
- 切 full 模式:把配置里 `memoryTracks` 改成 `"full"`,重启会话即可。
63
+ ### lite 模式(三轨)
64
+
65
+ 只保留 `memory` / `user` / `key`(`key` 落在根目录 `key.md`,不按项目分)。切 lite:在设置页「配置」里把记忆模式改成 lite 即可。
53
66
 
54
67
  ## 工具
55
68
 
package/client.js CHANGED
@@ -1,7 +1,9 @@
1
1
  /**
2
- * dsh-memory-core —— 浏览器半端:插件详情页的状态面板。
2
+ * dsh-memory-core —— 浏览器半端:插件详情页的设置面板。
3
3
  *
4
- * 显示当前模式、各轨记忆条数、待确认队列,以及打开独立管理页 manage.html 的按钮。
4
+ * 形制对齐 dsh-memory-evolve 的设置页:三个页签(指南 / 配置 / 版本),
5
+ * 配置项改完立即 POST 到覆盖层并生效(不需要重启),同时展示记忆状态与
6
+ * 独立管理页入口。
5
7
  * 挂载点:plugins.bundle.config(key = 包名)。
6
8
  */
7
9
  window.__ModuleLoader__.load({
@@ -13,45 +15,216 @@ window.__ModuleLoader__.load({
13
15
  const PKG = "dsh-memory-core";
14
16
  const API = "/api/memory-core";
15
17
  const jget = async (p) => (await fetch(API + p, { credentials: "same-origin" })).json();
18
+ const jpost = async (p, body) =>
19
+ (await fetch(API + p, {
20
+ method: "POST",
21
+ credentials: "same-origin",
22
+ headers: { "content-type": "application/json" },
23
+ body: JSON.stringify(body ?? {}),
24
+ })).json();
16
25
 
17
26
  const CSS = `
18
- [data-pmc-page]{display:flex;flex-direction:column;gap:10px;padding:4px 2px 10px;max-width:640px;font-size:13px;}
27
+ [data-pmc-page]{display:flex;flex-direction:column;gap:10px;padding:4px 2px 10px;max-width:680px;font-size:13px;}
19
28
  [data-pmc-head]{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;}
20
29
  [data-pmc-title]{font-size:14px;font-weight:600;}
21
30
  [data-pmc-sub]{font-size:12px;color:var(--dsw-alias-label-tertiary,#8a9099);}
31
+ [data-pmc-tabs]{display:flex;gap:4px;border-bottom:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.18));}
32
+ [data-pmc-tab]{appearance:none;border:0;background:transparent;font:inherit;font-size:13px;padding:7px 12px;cursor:pointer;color:var(--dsw-alias-label-secondary,#6b7280);border-bottom:2px solid transparent;margin-bottom:-1px;}
33
+ [data-pmc-tab][data-on]{color:var(--dsw-alias-brand-primary,#2f6fed);border-bottom-color:var(--dsw-alias-brand-primary,#2f6fed);font-weight:600;}
34
+ [data-pmc-card]{display:flex;flex-direction:column;gap:12px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.18));border-radius:10px;padding:14px 16px;background:var(--dsw-alias-bg-elevated,#fff);}
35
+ [data-pmc-lead]{font-size:12px;color:var(--dsw-alias-label-tertiary,#8a9099);line-height:18px;}
22
36
  [data-pmc-sec]{display:flex;flex-direction:column;gap:6px;border-top:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.18));padding-top:10px;}
37
+ [data-pmc-sec]:first-child{border-top:0;padding-top:0;}
38
+ [data-pmc-h]{font-size:13px;font-weight:600;}
23
39
  [data-pmc-row]{display:flex;align-items:center;gap:10px;flex-wrap:wrap;min-height:26px;}
24
- [data-pmc-name]{min-width:100px;flex:none;}
40
+ [data-pmc-name]{min-width:110px;flex:none;}
25
41
  [data-pmc-val]{flex:1;color:var(--dsw-alias-label-secondary,#6b7280);}
26
- [data-pmc-btn]{font-size:12px;min-height:26px;padding:2px 10px;border-radius:6px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2,rgba(255,255,255,.16));background:transparent;color:inherit;flex:none;}
42
+ [data-pmc-desc]{flex-basis:100%;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#8a9099);}
43
+ [data-pmc-btn]{appearance:none;font:inherit;font-size:12px;min-height:28px;padding:2px 12px;border-radius:6px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.35));background:transparent;color:inherit;}
44
+ [data-pmc-btn][data-on]{border-color:var(--dsw-alias-brand-primary,#2f6fed);color:var(--dsw-alias-brand-primary,#2f6fed);background:rgba(47,111,237,.08);}
45
+ [data-pmc-num]{width:88px;min-height:28px;padding:2px 8px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.35));background:transparent;color:inherit;font:inherit;}
46
+ [data-pmc-open]{display:inline-flex;align-items:center;gap:6px;font-size:12px;min-height:28px;padding:2px 12px;border-radius:6px;cursor:pointer;text-decoration:none;border:1px solid var(--dsw-alias-brand-primary,#2f6fed);color:var(--dsw-alias-brand-primary,#2f6fed);background:transparent;flex:none;}
47
+ [data-pmc-open]:hover{background:rgba(47,111,237,.08);}
27
48
  [data-pmc-note]{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#8a9099);}
28
49
  [data-pmc-chip]{display:inline-block;padding:1px 8px;border-radius:999px;font-size:11px;background:var(--dsw-alias-bg-weak,rgba(128,128,128,.1));}
29
50
  [data-pmc-chip="ok"]{background:#e6f5ec;color:#1a7f4b;}
30
51
  [data-pmc-chip="warn"]{background:#fdf3e3;color:#b26a00;}
52
+ [data-pmc-chip="muted"]{background:rgba(128,128,128,.14);color:inherit;opacity:.75;}
53
+ [data-pmc-mono]{font-family:ui-monospace,Consolas,monospace;font-size:12px;word-break:break-all;}
54
+ [data-pmc-list]{margin:0;padding-left:18px;line-height:20px;}
55
+ [data-pmc-banner]{position:fixed;top:0;left:0;right:0;z-index:2147483000;display:flex;align-items:flex-start;gap:10px;padding:9px 14px;font-size:12px;line-height:18px;box-shadow:0 1px 0 rgba(0,0,0,.08);}
56
+ [data-pmc-banner="warn"]{background:#fef3c7;color:#7c4a03;}
57
+ [data-pmc-banner="danger"]{background:#fee2e2;color:#991b1b;}
58
+ [data-pmc-bx]{margin-left:auto;flex:none;appearance:none;border:0;background:transparent;color:inherit;cursor:pointer;font:inherit;opacity:.65;}
59
+ [data-pmc-bx]:hover{opacity:1;}
31
60
  `;
32
61
 
33
- const TRACKS_LITE = ["memory", "user", "key"];
34
- const TRACKS_FULL = ["memory", "user", "project", "key", "daily"];
35
62
  const TRACK_LABELS = { memory: "长期记忆", user: "用户档案", project: "项目记忆", key: "关键事实", daily: "每日日志" };
63
+ const TABS = [["guide", "指南"], ["config", "配置"], ["version", "版本"]];
64
+
65
+ function TabBar({ tab, setTab }) {
66
+ return h("div", { "data-pmc-tabs": "" },
67
+ TABS.map(([id, label]) =>
68
+ h("button", {
69
+ key: id,
70
+ type: "button",
71
+ "data-pmc-tab": "",
72
+ ...(tab === id ? { "data-on": "" } : {}),
73
+ onClick: () => setTab(id),
74
+ }, label),
75
+ ));
76
+ }
77
+
78
+ /** 开关:用原生 checkbox 承载语义,视觉上是一枚滑块。 */
79
+ function Toggle({ on, disabled, onChange }) {
80
+ return h("button", {
81
+ type: "button",
82
+ role: "switch",
83
+ "aria-checked": on ? "true" : "false",
84
+ "data-pmc-btn": "",
85
+ ...(on ? { "data-on": "" } : {}),
86
+ style: { minWidth: "56px", opacity: disabled ? 0.5 : 1 },
87
+ disabled: !!disabled,
88
+ onClick: () => onChange(!on),
89
+ }, on ? "开" : "关");
90
+ }
91
+
92
+ function Guide() {
93
+ return h("div", { "data-pmc-card": "" },
94
+ h("div", { "data-pmc-sec": "" },
95
+ h("div", { "data-pmc-h": "" }, "这是什么"),
96
+ h("div", { "data-pmc-lead": "" },
97
+ "给被 dsh-memory-shield 遮蔽的会话提供一套隔离记忆:独立数据目录,与普通会话的 dsh-memory-evolve 互不影响。")),
98
+ h("div", { "data-pmc-sec": "" },
99
+ h("div", { "data-pmc-h": "" }, "五条记忆轨"),
100
+ h("ul", { "data-pmc-list": "" },
101
+ h("li", {}, "memory 长期记忆:跨项目通用事实,每轮注入。"),
102
+ h("li", {}, "user 用户档案:关于用户的稳定偏好,每轮注入。"),
103
+ h("li", {}, "project 项目日志:按工作目录哈希记录每回合进展,不注入、按需读取。"),
104
+ h("li", {}, "key 项目关键记忆:下次会话不知道就会做错的事实,注入摘要,写入需确认。"),
105
+ h("li", {}, "daily 今日日志:按天记录,不注入。"))),
106
+ h("div", { "data-pmc-sec": "" },
107
+ h("div", { "data-pmc-h": "" }, "怎么用"),
108
+ h("div", { "data-pmc-lead": "" },
109
+ "会话里用 memory_core 工具写入;管理页可新增、编辑、归档、删除,并处理待确认队列。")),
110
+ h("div", { "data-pmc-sec": "" },
111
+ h("div", { "data-pmc-h": "" }, "插件列表里为什么显示「已关闭」"),
112
+ h("div", { "data-pmc-lead": "" },
113
+ "包自带硬开关(patch 里 disabled: true):装进 profile 只是加载,默认不跑,避免给所有会话都塞一套记忆。" +
114
+ "本页显示的状态来自 preset 里的显式挂载——只要这里的条数与路径是活的,就说明它在工作。" +
115
+ "列表上的开关管的是 profile 层,打开等于让所有会话都用这套记忆,一般不需要。"),
116
+ h("div", { "data-pmc-desc": "" },
117
+ "注意:如果已经用 preset 挂载了本包,不要再打开列表开关——两个实例会抢同一个 /api/memory-core 路由," +
118
+ "后挂载的 preset 会报 duplicate prefix route 并整体加载失败。误开了就关掉开关、重启,数据不会丢。")),
119
+ h("div", { "data-pmc-sec": "" },
120
+ h("div", { "data-pmc-h": "" }, "致谢"),
121
+ h("div", { "data-pmc-lead": "" },
122
+ "存储层与注入预算参考 dsh-memory-evolve(作者 csyangwen,MIT)的实现,按隔离场景做了适配。")),
123
+ );
124
+ }
125
+
126
+ function ConfigView({ cfg, save, saving }) {
127
+ if (!cfg) return h("div", { "data-pmc-note": "" }, "配置读取中…");
128
+ const full = cfg.memoryTracks === "full";
129
+ const set = (patch) => save(patch);
130
+ return h("div", { "data-pmc-card": "" },
131
+ h("div", { "data-pmc-lead": "" }, "修改立即生效并持久化(写入本 preset 的覆盖层,不改 preset 本身)。"),
132
+ h("div", { "data-pmc-sec": "" },
133
+ h("div", { "data-pmc-h": "" }, "记忆模式"),
134
+ h("div", { "data-pmc-row": "" },
135
+ h("button", { type: "button", "data-pmc-btn": "", ...(full ? {} : { "data-on": "" }), disabled: saving, onClick: () => set({ memoryTracks: "lite" }) }, "lite · 三轨"),
136
+ h("button", { type: "button", "data-pmc-btn": "", ...(full ? { "data-on": "" } : {}), disabled: saving, onClick: () => set({ memoryTracks: "full" }) }, "full · 五轨"),
137
+ h("span", { "data-pmc-chip": "ok" }, full ? "五轨" : "三轨")),
138
+ h("div", { "data-pmc-desc": "" }, "lite 只开 memory / user / key;full 再加 project 与 daily(按会话工作目录隔离)。")),
139
+ h("div", { "data-pmc-sec": "" },
140
+ h("div", { "data-pmc-h": "" }, "记忆写入看门狗"),
141
+ h("div", { "data-pmc-row": "" },
142
+ h("span", { "data-pmc-val": "" }, "连续多轮没写记忆时在快照里提醒"),
143
+ h(Toggle, { on: cfg.writeGuard === true, disabled: saving, onChange: (v) => set({ writeGuard: v }) })),
144
+ h("div", { "data-pmc-desc": "" }, "关掉后 key 轨写入也不再进待确认队列,直接落盘。")),
145
+ h("div", { "data-pmc-sec": "" },
146
+ h("div", { "data-pmc-h": "" }, "看门狗阈值(轮)"),
147
+ h("div", { "data-pmc-row": "" },
148
+ h("input", {
149
+ type: "number",
150
+ min: 1,
151
+ step: 1,
152
+ "data-pmc-num": "",
153
+ defaultValue: cfg.writeGuardThreshold,
154
+ disabled: saving,
155
+ onBlur: (e) => {
156
+ const n = parseInt(e.target.value, 10);
157
+ if (Number.isInteger(n) && n >= 1 && n !== cfg.writeGuardThreshold) set({ writeGuardThreshold: n });
158
+ },
159
+ }),
160
+ h("span", { "data-pmc-val": "" }, "连续 N 轮未写入即提醒(2 = 容忍漏 1 轮)")),
161
+ h("div", { "data-pmc-desc": "" }, "失焦即保存。小于 1 的值会被忽略。")),
162
+ h("div", { "data-pmc-sec": "" },
163
+ h("div", { "data-pmc-h": "" }, "快照顺序"),
164
+ h("div", { "data-pmc-row": "" },
165
+ h("input", {
166
+ type: "number",
167
+ step: 10,
168
+ "data-pmc-num": "",
169
+ defaultValue: cfg.snapshotOrder,
170
+ disabled: saving,
171
+ onBlur: (e) => {
172
+ const n = parseInt(e.target.value, 10);
173
+ if (Number.isInteger(n) && n !== cfg.snapshotOrder) set({ snapshotOrder: n });
174
+ },
175
+ }),
176
+ h("span", { "data-pmc-val": "" }, "注入段在同名段里的排序")),
177
+ h("div", { "data-pmc-desc": "" }, "顺序改动要重启 DSH 才对已存在的会话生效(挂载时注册)。")),
178
+ h("div", { "data-pmc-sec": "" },
179
+ h("div", { "data-pmc-row": "" },
180
+ h("span", { "data-pmc-name": "" }, "覆盖层文件"),
181
+ h("span", { "data-pmc-val": "", "data-pmc-mono": "" }, cfg.livePath || "—"))),
182
+ saving ? h("div", { "data-pmc-note": "" }, "保存中…") : null,
183
+ );
184
+ }
185
+
186
+ function Version({ cfg }) {
187
+ return h("div", { "data-pmc-card": "" },
188
+ h("div", { "data-pmc-sec": "" },
189
+ h("div", { "data-pmc-h": "" }, "版本"),
190
+ h("div", { "data-pmc-row": "" }, h("span", { "data-pmc-val": "" }, "dsh-memory-core 0.1.4"))),
191
+ h("div", { "data-pmc-sec": "" },
192
+ h("div", { "data-pmc-h": "" }, "本版要点"),
193
+ h("ul", { "data-pmc-list": "" },
194
+ h("li", {}, "默认 full 五轨;数据落点仍是本 preset 的独立目录。"),
195
+ h("li", {}, "管理页按 memory-evolve 的形制重做:页签、条数、路径、美观/纯文本切换、搜索。"),
196
+ h("li", {}, "注入前净化 {{}},避免记忆正文被宿主当模板解析。"),
197
+ h("li", {}, "配置可在此页修改,立即生效。"))),
198
+ h("div", { "data-pmc-sec": "" },
199
+ h("div", { "data-pmc-row": "" },
200
+ h("a", { href: "/api/memory-core/manage", target: "_blank", rel: "noopener", "data-pmc-open": "" }, "打开记忆管理页"),
201
+ h("span", { "data-pmc-note": "" }, cfg?.presetName ? "preset " + cfg.presetName : ""))),
202
+ );
203
+ }
36
204
 
37
205
  function Panel() {
38
206
  const [st, setSt] = React.useState(null);
207
+ const [cfg, setCfg] = React.useState(null);
208
+ const [tab, setTab] = React.useState("guide");
209
+ const [saving, setSaving] = React.useState(false);
39
210
  const [err, setErr] = React.useState("");
40
211
 
41
212
  const load = React.useCallback(async () => {
42
213
  try {
43
- // 拉各轨条数 + 待确认(后端没有 /pending,pending 挂在 /entries 的返回里)
44
- const tracks = TRACKS_LITE; // 默认 lite,full 模式需要切配置
45
- const counts = {};
46
- let pendingCount = 0;
47
- for (const t of tracks) {
48
- try {
49
- const j = await jget(`/entries?track=${t}`);
50
- counts[t] = j.ok ? j.data.count : 0;
51
- if (j.ok && Array.isArray(j.data.pending)) pendingCount += j.data.pending.length;
52
- } catch { counts[t] = 0; }
53
- }
54
- setSt({ counts, pendingCount, tracks });
214
+ const [j, c] = await Promise.all([jget("/files"), jget("/config")]);
215
+ if (!j.ok) throw new Error(j.why || "状态读取失败");
216
+ const d = j.data;
217
+ const rows = Array.isArray(d.rows) ? d.rows : [];
218
+ setSt({
219
+ rows,
220
+ mode: d.memoryTracks,
221
+ preset: d.presetName,
222
+ dir: d.memoryDir,
223
+ pending: rows.reduce((a, r) => a + (r.pending || 0), 0),
224
+ archived: rows.reduce((a, r) => a + (r.archived || 0), 0),
225
+ needCwd: rows.some((r) => r.track === "project" || r.track === "key") && !d.cwd,
226
+ });
227
+ if (c && c.ok) setCfg(c.data);
55
228
  setErr("");
56
229
  } catch (e) {
57
230
  setErr(String(e?.message || e));
@@ -60,39 +233,109 @@ window.__ModuleLoader__.load({
60
233
 
61
234
  React.useEffect(() => {
62
235
  load();
63
- const t = setInterval(load, 10000);
64
- return () => clearInterval(t);
236
+ }, [load]);
237
+
238
+ const save = React.useCallback(async (patch) => {
239
+ setSaving(true);
240
+ try {
241
+ const j = await jpost("/config", { patch });
242
+ if (!j.ok) throw new Error(j.why || "保存失败");
243
+ setCfg(j.data);
244
+ await load();
245
+ } catch (e) {
246
+ setErr(String(e?.message || e));
247
+ } finally {
248
+ setSaving(false);
249
+ }
65
250
  }, [load]);
66
251
 
67
252
  if (err) return h("div", { "data-pmc-page": "" }, h("div", { "data-pmc-note": "" }, "状态读取失败:" + err));
68
253
  if (!st) return h("div", { "data-pmc-page": "" }, h("div", { "data-pmc-note": "" }, "读取中…"));
69
254
 
255
+ const full = st.mode === "full";
256
+
70
257
  return h("div", { "data-pmc-page": "" },
71
258
  h("div", { "data-pmc-head": "" },
72
259
  h("span", { "data-pmc-title": "" }, "遮蔽会话记忆"),
73
- h("span", { "data-pmc-sub": "" }, "记忆核心 · 数据与普通会话完全隔离")),
74
- h("div", { "data-pmc-sec": "" },
75
- h("div", { "data-pmc-row": "" },
76
- h("span", { "data-pmc-name": "" }, "当前模式"),
77
- h("span", { "data-pmc-val": "" }, "lite(三轨 memory/user/key)"),
78
- h("span", { "data-pmc-chip": "ok" }, "默认")),
79
- h("div", { "data-pmc-row": "" },
80
- h("span", { "data-pmc-name": "" }, "待确认记忆"),
81
- h("span", { "data-pmc-val": "" }, st.pendingCount > 0 ? `${st.pendingCount} 条待确认` : "无"),
82
- st.pendingCount > 0 ? h("span", { "data-pmc-chip": "warn" }, "需处理") : null)),
260
+ h("span", { "data-pmc-sub": "" }, "preset " + (st.preset || "—") + " · 与普通会话完全隔离"),
261
+ h("span", { "data-pmc-chip": "ok" }, full ? "五轨" : "三轨"),
262
+ st.pending > 0 ? h("span", { "data-pmc-chip": "warn" }, st.pending + " 条待确认") : null),
263
+ h(TabBar, { tab, setTab }),
264
+ tab === "guide" ? h(Guide) : null,
265
+ tab === "config" ? h(ConfigView, { cfg, save, saving }) : null,
266
+ tab === "version" ? h(Version, { cfg }) : null,
83
267
  h("div", { "data-pmc-sec": "" },
84
- ...st.tracks.map((t) =>
85
- h("div", { "data-pmc-row": "", key: t },
86
- h("span", { "data-pmc-name": "" }, TRACK_LABELS[t] || t),
87
- h("span", { "data-pmc-val": "" }, `${st.counts[t] || 0} 条`),
268
+ ...st.rows.map((r) =>
269
+ h("div", { "data-pmc-row": "", key: r.track },
270
+ h("span", { "data-pmc-name": "" }, TRACK_LABELS[r.track] || r.track),
271
+ h("span", { "data-pmc-val": "" }, r.available ? `${r.count || 0} 条 · ${r.path || ""}` : "需会话工作目录"),
272
+ r.pending > 0 ? h("span", { "data-pmc-chip": "warn" }, `${r.pending} 待确认`) : null,
273
+ r.archived > 0 ? h("span", { "data-pmc-chip": "muted" }, `归档 ${r.archived}`) : null,
88
274
  ),
89
275
  )),
90
- h("div", { "data-pmc-sec": "" },
91
- h("div", { "data-pmc-row": "" },
92
- h("a", { href: "/api/memory-core/manage", target: "_blank", rel: "noopener", "data-pmc-btn": "" }, "打开记忆管理面板"),
93
- ),
94
- h("div", { "data-pmc-note": "" },
95
- "管理面板在独立窗口打开,可查看/修改/归档各轨记忆。升级到 full 模式(五轨)需在配置里改 memoryTracks=full。")),
276
+ h("div", { "data-pmc-note": "" },
277
+ "数据目录:" + (st.dir || "—") + (st.needCwd
278
+ ? "。项目记忆与关键事实按会话工作目录隔离,管理页会自动带上当前 cwd。"
279
+ : "。")),
280
+ );
281
+ }
282
+
283
+ /**
284
+ * 顶部横幅:把「配置方式不对」的隐形代价变成看得见的告警。
285
+ *
286
+ * 两种危险状况各一档:
287
+ * - 红:遮蔽在 profile 层生效(插件列表开关),所有会话的记忆工具与注入都被挡掉——这是全局破坏;
288
+ * - 黄:记忆核在 profile 层生效(没在 preset 里挂),所有会话多一份隔离记忆并与 evolve 双写。
289
+ * 判定只读两个插件的公开状态接口,不改任何配置;用户关掉横幅只在当前页面生效(不持久化)。
290
+ */
291
+ function Banner() {
292
+ const [state, setState] = React.useState(null);
293
+ const [hidden, setHidden] = React.useState(false);
294
+ React.useEffect(() => {
295
+ let alive = true;
296
+ const tick = async () => {
297
+ try {
298
+ const [core, shield] = await Promise.all([
299
+ jget("/status").catch(() => null),
300
+ fetch("/api/memory-shield/health", { credentials: "same-origin" })
301
+ .then((r) => r.json())
302
+ .catch(() => null),
303
+ ]);
304
+ if (!alive) return;
305
+ const c = core?.data ?? {};
306
+ const sh = shield?.data ?? shield ?? {};
307
+ if (sh.layer === "profile" && sh.enabled === true && (sh.shadowTools || 0) > 0) {
308
+ setState({
309
+ kind: "danger",
310
+ text:
311
+ "记忆遮蔽正在全局生效:所有会话的记忆工具与上下文段都被挡掉,只剩 memory_core。" +
312
+ "这是全局破坏性配置,请立刻关掉插件列表里的 dsh-memory-shield 开关,改成只在目标 preset 里挂载并开 enabled。",
313
+ });
314
+ } else if (c.layer === "profile") {
315
+ setState({
316
+ kind: "warn",
317
+ text:
318
+ "dsh-memory-core 是被插件列表开关拉起来的(没在 preset 里挂):所有会话都会多一份隔离记忆," +
319
+ "并与记忆插件双写;数据落在 " + (c.memoryDir || "(未知目录)") + "。建议关掉列表开关,只在目标 preset 里挂载它。",
320
+ });
321
+ } else {
322
+ setState(null);
323
+ }
324
+ } catch {
325
+ if (alive) setState(null);
326
+ }
327
+ };
328
+ tick();
329
+ const id = setInterval(tick, 30000);
330
+ return () => {
331
+ alive = false;
332
+ clearInterval(id);
333
+ };
334
+ }, []);
335
+ if (!state || hidden) return null;
336
+ return h("div", { "data-pmc-banner": state.kind, role: "alert" },
337
+ h("span", null, (state.kind === "danger" ? "⛔ " : "⚠️ ") + state.text),
338
+ h("button", { "data-pmc-bx": "", onClick: () => setHidden(true) }, "关闭"),
96
339
  );
97
340
  }
98
341
 
@@ -111,6 +354,11 @@ window.__ModuleLoader__.load({
111
354
  { name: "plugins.bundle.config", key: PKG },
112
355
  Panel,
113
356
  ));
357
+ // 顶部横幅走 shell.overlay(list 型槽位,用 id)
358
+ slots.inject("shell.overlay", () => slots.register(
359
+ { name: "shell.overlay", id: "memory-core-banner" },
360
+ Banner,
361
+ ));
114
362
  },
115
363
  };
116
364
  },