dsh-memory-core 0.0.0-stage → 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - 首版:按 preset 隔离的记忆包(与 dsh-memory-shield 配套)。
6
+ - 五轨记忆(memory / user / project / key / daily),lite 三轨 + full 五轨。
7
+ - `memory_core` 工具:add / list / replace / remove / archive。
8
+ - `memory-core:snapshot` 注入段(常驻记忆 + 引导 + 写看门狗提醒)。
9
+ - `/api/memory-core` 路由:status / entries / entry / manage。
10
+ - `manage.html` 管理页:浏览、编辑、删除、归档、待确认队列。
11
+ - 写看门狗:连续 N 个空回合提醒写记忆,写入后清零。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sacredbluedragon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,104 @@
1
- # Temporary Holding Version
1
+ # dsh-memory-core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 遮蔽会话的 companion 记忆包。与 [dsh-memory-shield (shield)](../dsh-memory-shield/) 配对使用:
4
+
5
+ - **shield 包**:挡住其他记忆插件(dsh-memory-evolve 等)的注入和工具,让指定 preset 的会话拥有干净的上下文。
6
+ - **companion 包**:给被遮蔽的会话加一套隔离的记忆存储——独立数据目录、独立工具、独立面板,与普通会话的 dsh-memory-evolve 完全隔离。
7
+
8
+ ## 安装
9
+
10
+ ```bash
11
+ dsh plugin --profile web add dsh-memory-core
12
+ ```
13
+
14
+ 装完还要**手动启用一次**:包默认是关闭的(`cordis.patch.yml` 里 `disabled: true`),不启用的话重启后没有工具、没有记忆注入、面板也是空的,而且不报错。
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,再新建会话。
19
+
20
+ 前提:已安装 `dsh-memory-shield`(shield 包)。两包配对时建议只把 companion 挂进需要独立记忆的那个 preset,不要放进全局 `dsh.profile.bundles`。
21
+
22
+ ## 配置
23
+
24
+ | 配置项 | 默认值 | 说明 |
25
+ |---|---|---|
26
+ | `memoryTracks` | `"lite"` | 记忆轨模式:`lite` = 三轨 memory/user/key;`full` = 五轨 memory/user/project/key/daily |
27
+ | `presetName` | `"default"` | preset 名称(影响数据目录落点) |
28
+ | `memoryDir` | `""` | 记忆数据目录(留空自动解析到 `~/.dsh-plugin-data/<presetName>/memory/`) |
29
+ | `writeGuard` | `true` | key 轨写入走待确认队列(防误写) |
30
+ | `writeGuardThreshold` | `2` | 连续 N 轮没写记忆就提醒 |
31
+ | `snapshotOrder` | `500` | memory-core:snapshot 注入顺序 |
32
+
33
+ ## 记忆轨
34
+
35
+ ### lite 模式(默认,三轨)
36
+
37
+ | 轨 | 文件 | 用途 |
38
+ |---|---|---|
39
+ | `memory` | `memory.md` | 长期记忆(跨会话复用) |
40
+ | `user` | `user.md` | 用户偏好与画像 |
41
+ | `key` | `key.md` | 关键事实(走待确认队列) |
42
+
43
+ ### full 模式(五轨)
44
+
45
+ 在 lite 三轨基础上加:
46
+
47
+ | 轨 | 文件 | 用途 |
48
+ |---|---|---|
49
+ | `project` | `projects/<hash>/memory.md` | 项目记忆(按 cwd 隔离) |
50
+ | `daily` | `daily/<date>.md` | 每日日志(编年史) |
51
+
52
+ 切 full 模式:把配置里 `memoryTracks` 改成 `"full"`,重启会话即可。
53
+
54
+ ## 工具
55
+
56
+ 注册了一个 `memory_core` 工具,agent 可在遮蔽会话里调用:
57
+
58
+ ```
59
+ memory_core action=add target=memory content="用户喜欢用中文回复"
60
+ memory_core action=list target=user
61
+ memory_core action=replace target=key match="旧内容" content="新内容"
62
+ memory_core action=remove target=memory match="要删的子串"
63
+ memory_core action=archive target=user match="要归档的子串"
64
+ ```
65
+
66
+ ## 面板
67
+
68
+ 插件详情页里有一个小面板:
69
+
70
+ - 显示当前模式(lite/full)
71
+ - 各轨记忆条数
72
+ - 待确认队列条数
73
+ - 「打开记忆管理面板」按钮——在独立窗口打开 `manage.html`,可查看/修改/归档各轨记忆
74
+
75
+ ## 与 dsh-memory-evolve 的关系
76
+
77
+ - 本包的存储层(`lib/store.js`)**参考并借鉴了 [dsh-memory-evolve](https://github.com/csyangwen/dsh-memory-evolve)**(作者 csyangwen)的实现,做了隔离适配:数据目录独立、工具名独立、注入 section 同名(`memory-core:snapshot`)但内容是自己的。
78
+ - 普通会话的 dsh-memory-evolve 面板看不到遮蔽会话的记忆,反之亦然。
79
+ - 如果用户想要完整的五轨记忆 + 待确认审查 + 合并梳理,可以在普通会话里装 dsh-memory-evolve,本包只负责给遮蔽会话提供最小可用的记忆能力。
80
+
81
+ ## 目录结构
82
+
83
+ ```
84
+ packages/dsh-memory-core/
85
+ ├── index.js # 插件入口(memory_core 工具 + snapshot 注入 + webServer API)
86
+ ├── client.js # 插件详情页小面板
87
+ ├── manage.html # 独立窗口记忆管理页
88
+ ├── package.json
89
+ ├── README.md
90
+ └── lib/
91
+ ├── constants.js # PLUGIN_NAME
92
+ ├── defaults.js # DEFAULTS + resolveMemoryDir
93
+ ├── schema.js # schemastery config schema
94
+ ├── tracks.js # 轨道定义(lite/full)
95
+ ├── store.js # MemoryStore + SuggestionQueue + ArchiveStore(抄自 evolve)
96
+ ├── render.js # 注入渲染(预算分配 + 看门狗提醒)
97
+ ├── watchdog.js # 写入看门狗(连续 N 轮没写就提醒)
98
+ ├── budgets.js # 注入预算与检索参数
99
+ └── entryid.js # 条目身份证
100
+ ```
101
+
102
+ ## License
103
+
104
+ MIT
package/client.js ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * dsh-memory-core —— 浏览器半端:插件详情页的状态面板。
3
+ *
4
+ * 显示当前模式、各轨记忆条数、待确认队列,以及打开独立管理页 manage.html 的按钮。
5
+ * 挂载点:plugins.bundle.config(key = 包名)。
6
+ */
7
+ window.__ModuleLoader__.load({
8
+ id: "dsh-memory-core",
9
+ factory: (require) => {
10
+ const React = require("react");
11
+ const h = React.createElement;
12
+
13
+ const PKG = "dsh-memory-core";
14
+ const API = "/api/memory-core";
15
+ const jget = async (p) => (await fetch(API + p, { credentials: "same-origin" })).json();
16
+
17
+ const CSS = `
18
+ [data-pmc-page]{display:flex;flex-direction:column;gap:10px;padding:4px 2px 10px;max-width:640px;font-size:13px;}
19
+ [data-pmc-head]{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;}
20
+ [data-pmc-title]{font-size:14px;font-weight:600;}
21
+ [data-pmc-sub]{font-size:12px;color:var(--dsw-alias-label-tertiary,#8a9099);}
22
+ [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;}
23
+ [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;}
25
+ [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;}
27
+ [data-pmc-note]{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#8a9099);}
28
+ [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
+ [data-pmc-chip="ok"]{background:#e6f5ec;color:#1a7f4b;}
30
+ [data-pmc-chip="warn"]{background:#fdf3e3;color:#b26a00;}
31
+ `;
32
+
33
+ const TRACKS_LITE = ["memory", "user", "key"];
34
+ const TRACKS_FULL = ["memory", "user", "project", "key", "daily"];
35
+ const TRACK_LABELS = { memory: "长期记忆", user: "用户档案", project: "项目记忆", key: "关键事实", daily: "每日日志" };
36
+
37
+ function Panel() {
38
+ const [st, setSt] = React.useState(null);
39
+ const [err, setErr] = React.useState("");
40
+
41
+ const load = React.useCallback(async () => {
42
+ 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 });
55
+ setErr("");
56
+ } catch (e) {
57
+ setErr(String(e?.message || e));
58
+ }
59
+ }, []);
60
+
61
+ React.useEffect(() => {
62
+ load();
63
+ const t = setInterval(load, 10000);
64
+ return () => clearInterval(t);
65
+ }, [load]);
66
+
67
+ if (err) return h("div", { "data-pmc-page": "" }, h("div", { "data-pmc-note": "" }, "状态读取失败:" + err));
68
+ if (!st) return h("div", { "data-pmc-page": "" }, h("div", { "data-pmc-note": "" }, "读取中…"));
69
+
70
+ return h("div", { "data-pmc-page": "" },
71
+ h("div", { "data-pmc-head": "" },
72
+ h("span", { "data-pmc-title": "" }, "遮蔽会话记忆"),
73
+ h("span", { "data-pmc-sub": "" }, "companion 模式 · 数据与普通会话完全隔离")),
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)),
83
+ 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} 条`),
88
+ ),
89
+ )),
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。")),
96
+ );
97
+ }
98
+
99
+ return {
100
+ inject: ["slots"],
101
+ apply(ctx) {
102
+ const style = document.createElement("style");
103
+ style.setAttribute("data-plugin-css", PKG);
104
+ style.textContent = CSS;
105
+ document.head.appendChild(style);
106
+ if (typeof ctx.effect === "function") ctx.effect(() => () => style.remove(), `${PKG}: 面板样式`);
107
+
108
+ const slots = (typeof ctx.get === "function" ? ctx.get("slots") : undefined) || ctx.slots;
109
+ if (!slots) return;
110
+ slots.inject("plugins.bundle.config", () => slots.register(
111
+ { name: "plugins.bundle.config", key: PKG },
112
+ Panel,
113
+ ));
114
+ },
115
+ };
116
+ },
117
+ });
@@ -0,0 +1,8 @@
1
+ # dsh bundle patch: 插入本插件,但默认关闭。
2
+ # 本包一旦装进 profile 就会被所有会话加载(注册 memory_core 工具 + memory-core:snapshot 注入),
3
+ # 因此默认 disabled: true,只在目标 preset 里显式启用。
4
+ # 要用于某个 preset:在该 preset 的 plugins 里显式挂载本包,并去掉 disabled(或写 disabled: false)。
5
+ - insert:
6
+ - id: memory-core
7
+ name: 'dsh-memory-core'
8
+ disabled: true
package/index.js ADDED
@@ -0,0 +1,291 @@
1
+ /**
2
+ * dsh-memory-core —— 遮蔽会话的 companion 记忆包。
3
+ *
4
+ * 与 shield 包配对使用:shield 挡住其他记忆插件的注入,本包给遮蔽会话加一套
5
+ * 隔离的记忆存储(独立数据目录,与普通会话的 dsh-memory-evolve 完全隔离)。
6
+ *
7
+ * 默认 lite 模式(三轨 memory/user/key);切 full 模式(五轨)由用户在设置里改。
8
+ */
9
+ import { PLUGIN_NAME } from "./lib/constants.js";
10
+ import { DEFAULTS, resolveMemoryDir } from "./lib/defaults.js";
11
+ import { MemoryStore, SuggestionQueue, ArchiveStore } from "./lib/store.js";
12
+ import { renderSnapshot } from "./lib/render.js";
13
+ import { writeGapCounter } from "./lib/watchdog.js";
14
+ import { tracksOf, TRACK_FILES, PENDING_FILE } from "./lib/tracks.js";
15
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
16
+ import { join, dirname } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const name = PLUGIN_NAME;
20
+ const inject = ["systemPrompt", "tools", "webServer"];
21
+
22
+ const LOOPBACK = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
23
+
24
+ /** 同源守卫:挡 DNS 重绑定与跨站请求;返回空串表示放行 */
25
+ function sameOriginBlocked(req) {
26
+ const host = String(req.headers?.host ?? "").split(":")[0];
27
+ const origin = req.headers?.origin;
28
+ if (origin) {
29
+ let oh = "";
30
+ try {
31
+ oh = new URL(origin).hostname;
32
+ } catch {
33
+ return "非法 Origin 头";
34
+ }
35
+ if (oh !== host && !(LOOPBACK.has(host) && LOOPBACK.has(oh))) return "跨源访问被拒绝";
36
+ }
37
+ const site = req.headers?.["sec-fetch-site"];
38
+ if (!origin && site && site !== "same-origin" && site !== "none") return "跨站请求被拒绝";
39
+ return "";
40
+ }
41
+
42
+ function apply(ctx, config) {
43
+ const s = { ...DEFAULTS, ...(config ?? {}) };
44
+ const memoryDir = resolveMemoryDir(s);
45
+ if (!existsSync(memoryDir)) mkdirSync(memoryDir, { recursive: true });
46
+
47
+ const store = new MemoryStore(memoryDir, { injectionScan: true, memoryTracks: s.memoryTracks });
48
+ const pending = new SuggestionQueue(join(memoryDir, PENDING_FILE));
49
+ const archive = new ArchiveStore(memoryDir);
50
+
51
+ // ── memory_core 工具 ────────────────────────────────────────
52
+ const writeGap = writeGapCounter(ctx, () => s.writeGuard === true);
53
+ /** 写入成功后清 gap,否则看门狗每轮都会提醒「没写记忆」 */
54
+ const markWrote = (agent) => {
55
+ try {
56
+ writeGap?.noteWrite?.(agent);
57
+ } catch {}
58
+ };
59
+
60
+ const memoryTool = {
61
+ name: "memory_core",
62
+ description:
63
+ "读写遮蔽会话的隔离记忆(与全局 memory 工具互不影响)。target 可选:memory(长期记忆)、user(用户偏好)、key(关键事实,需确认)、project(项目记忆,需 full 模式)、daily(每日日志,需 full 模式)。",
64
+ parameters: {
65
+ type: "object",
66
+ properties: {
67
+ action: { type: "string", enum: ["add", "list", "replace", "remove", "archive"], description: "操作" },
68
+ target: { type: "string", enum: ["memory", "user", "key", "project", "daily"], description: "记忆轨" },
69
+ content: { type: "string", description: "新内容(add/replace 时必填)" },
70
+ match: { type: "string", description: "匹配子串(replace/remove/archive 时必填)" },
71
+ },
72
+ required: ["action", "target"],
73
+ },
74
+ output: {
75
+ schema: {
76
+ type: "object",
77
+ additionalProperties: false,
78
+ properties: {
79
+ ok: { type: "boolean" },
80
+ message: { type: "string" },
81
+ target: { type: "string" },
82
+ entries: { type: "array", items: { type: "string" } },
83
+ count: { type: "integer" },
84
+ queued: { type: "boolean" },
85
+ },
86
+ required: ["ok", "message"],
87
+ },
88
+ render: (_args, value) => [{ type: "text", text: value.message || (value.ok ? "ok" : "failed") }],
89
+ },
90
+ async execute({ action, target, content, match }, exec) {
91
+ try {
92
+ if (action === "list") {
93
+ const entries = store.entriesOf(target, exec?.agent);
94
+ return { ok: true, message: `${target} 轨 ${entries.length} 条`, target, entries, count: entries.length };
95
+ }
96
+ if (action === "add") {
97
+ if (!content) return { ok: false, message: "add 需要 content", target };
98
+ // key 轨走待确认队列
99
+ if (target === "key" && s.writeGuard) {
100
+ pending.append({ target, content, at: new Date().toISOString() });
101
+ return { ok: true, message: "已加入待确认队列,用户确认后生效。", target, queued: true };
102
+ }
103
+ const r = store.add(target, content, exec?.agent);
104
+ if (r?.ok !== false) markWrote(exec?.agent);
105
+ return { ok: r?.ok !== false, message: r?.message || `已写入 ${target}`, target };
106
+ }
107
+ if (action === "replace") {
108
+ if (!match || !content) return { ok: false, message: "replace 需要 match 和 content", target };
109
+ const r = store.replace(target, match, content, exec?.agent);
110
+ if (r?.ok !== false) markWrote(exec?.agent);
111
+ return { ok: r?.ok !== false, message: r?.message || `已替换 ${target}`, target };
112
+ }
113
+ if (action === "remove") {
114
+ if (!match) return { ok: false, message: "remove 需要 match", target };
115
+ const r = store.remove(target, match, exec?.agent);
116
+ return { ok: r?.ok !== false, message: r?.message || `已删除 ${target}`, target };
117
+ }
118
+ if (action === "archive") {
119
+ if (!match) return { ok: false, message: "archive 需要 match", target };
120
+ // 先写归档、再删主轨:两步之间断掉也不会丢内容
121
+ const peek = store.peek(target, match, exec?.agent);
122
+ if (peek?.ok === false) return { ok: false, message: peek.message || "找不到要归档的条目", target };
123
+ archive.append(target, peek.entry, exec?.agent?.session?.header?.cwd);
124
+ const r = store.remove(target, match, exec?.agent);
125
+ return { ok: r?.ok !== false, message: r?.message || `已归档 ${target}`, target };
126
+ }
127
+ return { ok: false, message: `未知 action: ${action}`, target };
128
+ } catch (e) {
129
+ return { ok: false, message: e?.message ?? String(e), target };
130
+ }
131
+ },
132
+ };
133
+ ctx.effect(() => ctx.tools.register(memoryTool), `${name}: memory tool`);
134
+
135
+ // ── memory-core:snapshot 注入 ──────────────────────────────
136
+ ctx.effect(
137
+ () =>
138
+ ctx.systemPrompt.context({
139
+ name: "memory-core:snapshot",
140
+ order: s.snapshotOrder,
141
+ text: () => renderSnapshot(store, s, writeGap, { agent: ctx.agent }),
142
+ }),
143
+ `${name}: memory-core:snapshot inject`,
144
+ );
145
+
146
+ // ── webServer API ───────────────────────────────────────────
147
+ ctx.effect(
148
+ () =>
149
+ ctx.webServer.register({
150
+ kind: "prefix",
151
+ path: "/api/memory-core",
152
+ handler: async (req, res) => {
153
+ const url = new URL(req.url, "http://127.0.0.1");
154
+ const json = (code, data) => {
155
+ res.writeHead(code, { "content-type": "application/json" });
156
+ res.end(JSON.stringify(data));
157
+ };
158
+ const denied = sameOriginBlocked(req);
159
+ if (denied) {
160
+ json(403, { ok: false, why: denied });
161
+ return;
162
+ }
163
+ const cwd = ctx.agent?.session?.header?.cwd;
164
+
165
+ // GET /manage —— 返回 manage.html 页面
166
+ if (req.method === "GET" && url.pathname === "/api/memory-core/manage") {
167
+ try {
168
+ const htmlPath = join(dirname(fileURLToPath(import.meta.url)), "manage.html");
169
+ const html = readFileSync(htmlPath, "utf8");
170
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
171
+ res.end(html);
172
+ } catch (e) {
173
+ json(500, { ok: false, why: e?.message ?? String(e) });
174
+ }
175
+ return;
176
+ }
177
+
178
+ // GET /status —— 面板状态
179
+ if (req.method === "GET" && url.pathname === "/api/memory-core/status") {
180
+ json(200, {
181
+ ok: true,
182
+ data: {
183
+ memoryEnabled: true,
184
+ memoryTracks: s.memoryTracks,
185
+ presetName: s.presetName,
186
+ memoryDir,
187
+ tracks: tracksOf(s.memoryTracks),
188
+ },
189
+ });
190
+ return;
191
+ }
192
+
193
+ // GET /entries?track=memory —— 列出某轨正式条目 + 待确认 + 归档
194
+ if (req.method === "GET" && url.pathname === "/api/memory-core/entries") {
195
+ const track = url.searchParams.get("track") || "memory";
196
+ try {
197
+ const entries = store.entriesOf(track, ctx.agent);
198
+ const pendingItems = pending.read().filter((p) => p.target === track);
199
+ const archived = archive.entriesOf(track, cwd);
200
+ json(200, { ok: true, data: { track, entries, pending: pendingItems, archived, count: entries.length } });
201
+ } catch (e) {
202
+ json(500, { ok: false, why: e?.message ?? String(e) });
203
+ }
204
+ return;
205
+ }
206
+
207
+ // POST /entry —— 所有写操作
208
+ if (req.method === "POST" && url.pathname === "/api/memory-core/entry") {
209
+ try {
210
+ const body = await new Promise((resolve) => {
211
+ let raw = "";
212
+ req.on("data", (c) => (raw += c));
213
+ req.on("end", () => {
214
+ try { resolve(JSON.parse(raw || "{}")); } catch { resolve({}); }
215
+ });
216
+ });
217
+ const { action, track, target, content, entry, match, index } = body;
218
+ const t = track || target || "memory";
219
+
220
+ if (action === "add") {
221
+ if (t === "key" && s.writeGuard) {
222
+ pending.append({ target: t, content, at: new Date().toISOString() });
223
+ } else {
224
+ const r = store.add(t, content, ctx.agent);
225
+ if (r?.ok !== false) markWrote(ctx.agent);
226
+ }
227
+ } else if (action === "replace" || action === "edit" || action === "saveEdit") {
228
+ const r = store.replace(t, match || entry, content, ctx.agent);
229
+ if (r?.ok !== false) markWrote(ctx.agent);
230
+ } else if (action === "delete" || action === "remove") {
231
+ store.remove(t, match || entry, ctx.agent);
232
+ } else if (action === "archive") {
233
+ // 先写归档、再删主轨(两步之间断掉也不丢内容)
234
+ archive.append(t, entry || match, cwd);
235
+ store.remove(t, match || entry, ctx.agent);
236
+ } else if (action === "confirm") {
237
+ // 待确认 → 正式写入;按内容匹配,不用前端索引(前端列表是过滤后的)
238
+ const items = pending.read();
239
+ const want = String(entry ?? "").trim();
240
+ const idx = items.findIndex((p) => String(p.content ?? "").trim() === want);
241
+ if (idx >= 0) {
242
+ const r = store.add(items[idx].target, items[idx].content, ctx.agent);
243
+ if (r?.ok !== false) {
244
+ items.splice(idx, 1);
245
+ pending.write(items);
246
+ markWrote(ctx.agent);
247
+ }
248
+ }
249
+ } else if (action === "reject") {
250
+ // 待确认 → 删除;同样按内容匹配
251
+ const items = pending.read();
252
+ const want = String(entry ?? "").trim();
253
+ const idx = items.findIndex(
254
+ (p) => p.target === t && String(p.content ?? "").trim() === want,
255
+ );
256
+ if (idx >= 0) {
257
+ items.splice(idx, 1);
258
+ pending.write(items);
259
+ }
260
+ } else if (action === "promote") {
261
+ // 归档 → 移回主轨:先写主轨,写成功才删归档
262
+ const r = store.add(t, entry, ctx.agent);
263
+ if (r?.ok !== false) {
264
+ archive.removeExact(t, entry, cwd);
265
+ markWrote(ctx.agent);
266
+ }
267
+ } else if (action === "removeArchive") {
268
+ archive.removeExact(t, entry, cwd);
269
+ } else {
270
+ return json(400, { ok: false, why: "unknown action: " + action });
271
+ }
272
+ json(200, { ok: true, data: { action, target: t } });
273
+ } catch (e) {
274
+ json(500, { ok: false, why: e?.message ?? String(e) });
275
+ }
276
+ return;
277
+ }
278
+
279
+ json(404, { ok: false, why: "not found" });
280
+ },
281
+ }),
282
+ `${name}: webServer API`,
283
+ );
284
+
285
+ ctx.logger.info(
286
+ `${name}: companion active — mode=${s.memoryTracks}, dir=${memoryDir}, tracks=${tracksOf(s.memoryTracks).join("/")}`,
287
+ );
288
+ }
289
+
290
+ export { apply, inject, name };
291
+ export { Config } from "./lib/schema.js";
package/lib/budgets.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 注入预算与检索参数。
3
+ */
4
+ export const BUDGETS = { memory: 1000, user: 600, key: 400 };
5
+ export const TOTAL_BUDGET = 2000;
6
+ export const KEY_TOP_N = 5;
7
+ export const SEARCH_TOP_N = 5;
8
+ export const KEY_SCAN_N = 8;
9
+ export const KEY_EXTRA_CAP = 600;
10
+ // key 轨加权挑条目时用的「重要词」:命中越多排越前;留空数组 = 不加权(原样按最近排序)
11
+ export const KEY_HOT_WORDS = ['不要', '必须', '注意', '重要', '偏好', '禁止', '始终', '永远'];
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 不变的常量。
3
+ */
4
+ export const PLUGIN_NAME = "dsh-memory-core";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 默认配置(companion 版)。
3
+ */
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+
7
+ export const DEFAULTS = {
8
+ memoryTracks: "lite", // "lite" = 三轨 memory/user/key;"full" = 五轨 memory/user/project/key/daily
9
+ presetName: "default",
10
+ memoryDir: "",
11
+ writeGuard: true,
12
+ writeGuardThreshold: 2,
13
+ snapshotOrder: 500,
14
+ };
15
+
16
+ /** 解析 memoryDir 落点。 */
17
+ export function resolveMemoryDir(s) {
18
+ if (s.memoryDir) return s.memoryDir;
19
+ return process.env.DSH_MEMORY_CORE_DIR || path.join(
20
+ process.env.DSH_HOME || path.join(os.homedir(), ".dsh"),
21
+ "..", ".dsh-plugin-data", s.presetName, "memory",
22
+ );
23
+ }