@modusensus/dsh-mneme 0.6.6 → 0.6.7
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 +9 -3
- package/lib/api.js +90 -0
- package/lib/client.js +189 -88
- package/lib/settings.js +30 -0
- package/package.json +7 -2
- package/src/api.js +90 -0
- package/src/settings.js +30 -0
- package/test/api.test.js +45 -0
- package/test/settings.test.js +17 -0
package/README.md
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
8
|
-
[](https://github.com/modusensus/dsh-mneme)
|
|
9
|
+
[](https://github.com/modusensus/dsh-mneme/actions)
|
|
10
|
+
[](https://nodejs.org)
|
|
11
|
+
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
12
|
+
[](https://codecov.io/gh/modusensus/dsh-mneme)
|
|
9
13
|
|
|
10
14
|
> 给 DeepSeek Harness 的跨会话记忆插件:让 Agent 记住你、记住项目、自动整理记忆。**Mneme**(Μνήμη)——希腊记忆女神 Mnemosyne 之名,掌管记忆与梦境,正如 autoDream 在后台巩固记忆。
|
|
11
15
|
|
|
@@ -226,6 +230,8 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
226
230
|
|
|
227
231
|
| 版本 | 亮点 |
|
|
228
232
|
|------|------|
|
|
233
|
+
| **v0.6.7** | 记忆面板前端增强:记忆删除端点 + autoTag 手动开关 + 目录 VS Code 式文件树 + 面板卡片式布局(分类栏 + search/tree/detail 三卡);716 测试全绿 |
|
|
234
|
+
| **v0.6.6** | kimi-k3 复验 4 项修复:autoTag 跳过已打标记忆并合并、tag: 搜索召回统计门控(避免零召回拖垮 TopK);710 测试全绿 |
|
|
229
235
|
| **v0.6.5** | 整合 v0.6.2-0.6.4:Tag 系统 + 目录视图 + Tag 加权召回(全部 opt-in);709 测试全绿 |
|
|
230
236
|
| **v0.6.4** | Tag 加权召回:query/session tag 交集 boost(`tagBoostEnabled` 默认关) |
|
|
231
237
|
| **v0.6.3** | 目录视图:Tag 文件夹 + 无标签兜底 + 点击跳详情 |
|
|
@@ -425,7 +431,7 @@ src/
|
|
|
425
431
|
lib/
|
|
426
432
|
├── client.js # Web 面板(手写 ModuleLoader bundle)
|
|
427
433
|
└── *.js # src 的同步分发产物
|
|
428
|
-
test/ #
|
|
434
|
+
test/ # 716 个 node:test 测试(含审计与三轴线压测不变量)
|
|
429
435
|
scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · benchmark-recall.js 召回基准
|
|
430
436
|
```
|
|
431
437
|
|
|
@@ -434,7 +440,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
|
|
|
434
440
|
```bash
|
|
435
441
|
cd dsh-mneme
|
|
436
442
|
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
|
|
437
|
-
npm test # 运行
|
|
443
|
+
npm test # 运行 716 个测试
|
|
438
444
|
npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
|
|
439
445
|
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
|
|
440
446
|
```
|
package/lib/api.js
CHANGED
|
@@ -218,6 +218,96 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
218
218
|
}
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
+
// --- app config read/write (partial: pass only the fields to change) ---
|
|
222
|
+
register({
|
|
223
|
+
kind: "exact",
|
|
224
|
+
path: "/api/dsh-mneme/config",
|
|
225
|
+
handler: async (req, res) => {
|
|
226
|
+
try {
|
|
227
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
228
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
229
|
+
const body = parseBody(await readBody(req));
|
|
230
|
+
const patch = {};
|
|
231
|
+
const setIfBoolean = (key) => {
|
|
232
|
+
if (Object.prototype.hasOwnProperty.call(body, key)) {
|
|
233
|
+
if (typeof body[key] !== "boolean") {
|
|
234
|
+
sendJson(res, 400, { error: `${key} must be a boolean` });
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
patch[key] = body[key];
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
};
|
|
241
|
+
if (!setIfBoolean("autoTagEnabled") || !setIfBoolean("manualTagEnabled")) return;
|
|
242
|
+
if (Object.keys(patch).length === 0) {
|
|
243
|
+
sendJson(res, 400, { error: "no valid config field provided" });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const config = settings.setAutoTagConfig(patch);
|
|
247
|
+
sendJson(res, 200, { config });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
sendJson(res, 200, { config: settings.getAutoTagConfig() });
|
|
251
|
+
} catch {
|
|
252
|
+
sendJson(res, 500, { error: "internal" });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// --- delete a memory by exact id or best query match (mirrors memory_delete tool) ---
|
|
258
|
+
register({
|
|
259
|
+
kind: "exact",
|
|
260
|
+
path: "/api/dsh-mneme/memories",
|
|
261
|
+
handler: async (req, res) => {
|
|
262
|
+
try {
|
|
263
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
264
|
+
if (req.method !== "DELETE") {
|
|
265
|
+
sendJson(res, 405, { error: "method not allowed" });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const body = parseBody(await readBody(req));
|
|
269
|
+
const hasId = Object.prototype.hasOwnProperty.call(body, "id");
|
|
270
|
+
const hasQuery = Object.prototype.hasOwnProperty.call(body, "query");
|
|
271
|
+
if ((hasId && hasQuery) || (!hasId && !hasQuery)) {
|
|
272
|
+
sendJson(res, 400, { error: "provide exactly one of id or query" });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (hasId) {
|
|
276
|
+
if (typeof body.id !== "string" || !body.id.trim()) {
|
|
277
|
+
sendJson(res, 400, { error: "invalid id" });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const mem = service.getById(body.id);
|
|
281
|
+
if (!mem) {
|
|
282
|
+
sendJson(res, 200, { deleted: false });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
service.remove(body.id);
|
|
286
|
+
sendJson(res, 200, { deleted: true, id: body.id });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (typeof body.query !== "string" || !body.query.trim()) {
|
|
290
|
+
sendJson(res, 400, { error: "invalid query" });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const results = await service.searchMemories(body.query, { mode: "auto", topK: 1, useRerank: true });
|
|
294
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
295
|
+
sendJson(res, 200, { deleted: false });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const mem = results[0];
|
|
299
|
+
if (!mem || !mem.id) {
|
|
300
|
+
sendJson(res, 200, { deleted: false });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
service.remove(mem.id);
|
|
304
|
+
sendJson(res, 200, { deleted: true, id: mem.id });
|
|
305
|
+
} catch {
|
|
306
|
+
sendJson(res, 500, { error: "internal" });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
221
311
|
// --- vector re-index (backfill embeddings for rows missing them) ---
|
|
222
312
|
register({
|
|
223
313
|
kind: "exact",
|
package/lib/client.js
CHANGED
|
@@ -72,6 +72,11 @@ window.__ModuleLoader__.load({
|
|
|
72
72
|
"memory.settings.profileSaved": "画像已保存",
|
|
73
73
|
"memory.settings.rules": "规则",
|
|
74
74
|
"memory.settings.rulesHint": "Agent 必须遵守的行为规则,每轮注入",
|
|
75
|
+
"memory.settings.autoTagTitle": "自动打标签",
|
|
76
|
+
"memory.settings.autoTagHint": "开启后,新记忆由轻量模型自动打标签,目录页按标签自动分类(opt-in,默认关)",
|
|
77
|
+
"memory.settings.autoTagEnabled": "自动打标签",
|
|
78
|
+
"memory.settings.autoTagSave": "保存",
|
|
79
|
+
"memory.settings.autoTagSaved": "已保存",
|
|
75
80
|
"memory.settings.ruleAdd": "添加规则",
|
|
76
81
|
"memory.settings.rulePlaceholder": "例如:回答时总是先给结论",
|
|
77
82
|
"memory.settings.commands": "自定义指令",
|
|
@@ -115,6 +120,8 @@ window.__ModuleLoader__.load({
|
|
|
115
120
|
"memory.directory.untagged": "无标签",
|
|
116
121
|
"memory.directory.loading": "加载中…",
|
|
117
122
|
"memory.directory.empty": "暂无记忆条目",
|
|
123
|
+
"directory.editToggle": "编辑模式(显示删除)",
|
|
124
|
+
"directory.deleteConfirm": "确定删除这条记忆吗?",
|
|
118
125
|
"memory.card.open": "在主区记忆库中查看全文",
|
|
119
126
|
"memory.time.now": "刚刚",
|
|
120
127
|
"memory.time.seconds": "{n}秒前",
|
|
@@ -172,6 +179,11 @@ window.__ModuleLoader__.load({
|
|
|
172
179
|
"memory.settings.profileSaved": "Profile saved",
|
|
173
180
|
"memory.settings.rules": "Rules",
|
|
174
181
|
"memory.settings.rulesHint": "Behavior rules the agent must follow every turn",
|
|
182
|
+
"memory.settings.autoTagTitle": "Auto-Tag",
|
|
183
|
+
"memory.settings.autoTagHint": "When on, new memories are auto-tagged by a light model and the directory auto-grouped by tag (opt-in, off by default)",
|
|
184
|
+
"memory.settings.autoTagEnabled": "Auto-tagging",
|
|
185
|
+
"memory.settings.autoTagSave": "Save",
|
|
186
|
+
"memory.settings.autoTagSaved": "Saved",
|
|
175
187
|
"memory.settings.ruleAdd": "Add Rule",
|
|
176
188
|
"memory.settings.rulePlaceholder": "e.g. Always lead with a conclusion",
|
|
177
189
|
"memory.settings.commands": "Custom Commands",
|
|
@@ -215,6 +227,8 @@ window.__ModuleLoader__.load({
|
|
|
215
227
|
"memory.directory.untagged": "Untagged",
|
|
216
228
|
"memory.directory.loading": "Loading…",
|
|
217
229
|
"memory.directory.empty": "No memories yet",
|
|
230
|
+
"directory.editToggle": "Edit mode",
|
|
231
|
+
"directory.deleteConfirm": "Delete this memory?",
|
|
218
232
|
"memory.card.open": "Open full text in the Memory tab",
|
|
219
233
|
"memory.time.now": "just now",
|
|
220
234
|
"memory.time.seconds": "{n}s ago",
|
|
@@ -365,6 +379,13 @@ window.__ModuleLoader__.load({
|
|
|
365
379
|
// --- detail column ---
|
|
366
380
|
".mneme-xdetail{flex:none;max-height:44%;min-height:0;overflow-y:auto;padding:14px 20px 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
|
|
367
381
|
".mneme-xtree{flex:1;min-height:0;overflow-y:auto;padding:8px 10px 28px}",
|
|
382
|
+
".mneme-xmain{display:flex;flex-direction:column}",
|
|
383
|
+
".mneme-xfilter-bar{flex:none;height:48px;display:flex;align-items:center;gap:8px;padding:0 12px;border-bottom:1px solid var(--dsw-alias-border-l2,#ddd)}",
|
|
384
|
+
".mneme-xcards{flex:1;display:flex;flex-direction:row;gap:12px;padding:12px;overflow:hidden}",
|
|
385
|
+
".mneme-card{background:var(--dsw-alias-bg-base,#fafafa);border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,.06);padding:12px;display:flex;flex-direction:column;overflow:auto}",
|
|
386
|
+
".mneme-card--search{width:236px;flex:none}",
|
|
387
|
+
".mneme-card--tree{width:260px;flex:none}",
|
|
388
|
+
".mneme-card--detail{flex:1;min-width:0}",
|
|
368
389
|
".mneme-xdinner{max-width:720px}",
|
|
369
390
|
".mneme-xdtitle{font-size:16px;font-weight:600;line-height:24px;color:var(--dsw-alias-label-primary);margin-bottom:10px;word-break:break-word}",
|
|
370
391
|
".mneme-xdmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-bottom:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
@@ -408,6 +429,16 @@ window.__ModuleLoader__.load({
|
|
|
408
429
|
".mneme-gs-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
409
430
|
// --- directory sub-view (v0.6.3): tag folders + memory entries ---
|
|
410
431
|
".mneme-directory{flex:1;min-height:0;overflow-y:auto;padding:8px 10px 28px}",
|
|
432
|
+
".mneme-edit-toggle{margin:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;user-select:none}",
|
|
433
|
+
".mneme-dir-item{position:relative;display:flex;align-items:center;gap:6px;padding:2px 6px;border-radius:4px}",
|
|
434
|
+
".mneme-dir-item:hover{background:rgba(127,127,127,.18)}",
|
|
435
|
+
".mneme-tree-line{width:14px;height:1px;border-top:1px dashed rgba(127,127,127,.5);flex:none}",
|
|
436
|
+
".mneme-dir-item .mneme-dir-del{opacity:0;transition:opacity .12s}",
|
|
437
|
+
".mneme-dir-item:hover .mneme-dir-del{opacity:1}",
|
|
438
|
+
".mneme-dir-del{margin-left:auto;background:none;border:none;color:#e5484d;cursor:pointer;font-size:13px;line-height:1;padding:2px 4px}",
|
|
439
|
+
".mneme-dir-del:hover{filter:brightness(1.25)}",
|
|
440
|
+
".mneme-edit-on .mneme-dir-del{opacity:1}",
|
|
441
|
+
".mneme-dir-ititle{background:none;border:none;padding:0;font:inherit;color:inherit;cursor:pointer;flex:1;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
411
442
|
".mneme-dir-folder{margin-bottom:4px}",
|
|
412
443
|
".mneme-dir-folderhead{display:flex;align-items:center;gap:6px;width:100%;padding:6px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;font-size:13px;font-weight:500;line-height:18px;text-align:left}",
|
|
413
444
|
".mneme-dir-folderhead:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
@@ -832,6 +863,8 @@ window.__ModuleLoader__.load({
|
|
|
832
863
|
(typeof window !== "undefined" && window.localStorage) ? window.localStorage.getItem("dsh-mneme-api-token") || "" : ""
|
|
833
864
|
);
|
|
834
865
|
const [apiTokenSaved, setApiTokenSaved] = react.useState(false);
|
|
866
|
+
const [autoTag, setAutoTag] = react.useState(false);
|
|
867
|
+
const [autoTagSaved, setAutoTagSaved] = react.useState(false);
|
|
835
868
|
|
|
836
869
|
const load = react.useCallback(async () => {
|
|
837
870
|
try {
|
|
@@ -845,6 +878,11 @@ window.__ModuleLoader__.load({
|
|
|
845
878
|
setRules(Array.isArray(r.rules) ? r.rules : []);
|
|
846
879
|
setCommands(Array.isArray(c.commands) ? c.commands : []);
|
|
847
880
|
setVector(v.config || { enabled: false, baseUrl: "", apiKey: "", model: "" });
|
|
881
|
+
// autoTag loads independently so a config failure can't cascade-block the rest.
|
|
882
|
+
try {
|
|
883
|
+
const g = await apiFetch("/api/dsh-mneme/config").then((res) => res.json());
|
|
884
|
+
setAutoTag(g.config?.autoTagEnabled === true);
|
|
885
|
+
} catch { /* keep default off */ }
|
|
848
886
|
} catch { /* ignore */ }
|
|
849
887
|
}, []);
|
|
850
888
|
|
|
@@ -941,6 +979,18 @@ window.__ModuleLoader__.load({
|
|
|
941
979
|
} catch { /* ignore */ }
|
|
942
980
|
}
|
|
943
981
|
|
|
982
|
+
async function saveAutoTag() {
|
|
983
|
+
try {
|
|
984
|
+
await apiFetch("/api/dsh-mneme/config", {
|
|
985
|
+
method: "PUT",
|
|
986
|
+
headers: { "Content-Type": "application/json" },
|
|
987
|
+
body: JSON.stringify({ autoTagEnabled: autoTag })
|
|
988
|
+
});
|
|
989
|
+
setAutoTagSaved(true);
|
|
990
|
+
setTimeout(() => setAutoTagSaved(false), 1500);
|
|
991
|
+
} catch { /* ignore */ }
|
|
992
|
+
}
|
|
993
|
+
|
|
944
994
|
const inputStyle = { boxSizing: "border-box", width: "100%", height: 34, padding: "0 12px", borderRadius: 10, border: "1px solid var(--dsw-alias-border-l2, #ddd)", background: "var(--dsw-alias-bg-base, transparent)", color: "var(--dsw-alias-label-primary)", fontFamily: "inherit", fontSize: 13, outline: "none", marginBottom: 8 };
|
|
945
995
|
const labelStyle = { fontSize: 12, fontWeight: 600, margin: "12px 0 4px", color: "var(--dsw-alias-label-primary)" };
|
|
946
996
|
const hintStyle = { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)", marginBottom: 8 };
|
|
@@ -1007,6 +1057,17 @@ window.__ModuleLoader__.load({
|
|
|
1007
1057
|
h("button", { style: styles.footerButton, onClick: addCommand }, t("memory.settings.cmdAdd")),
|
|
1008
1058
|
cmdError && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-state-error, #c33)" } }, cmdError)
|
|
1009
1059
|
),
|
|
1060
|
+
// auto-tag switch
|
|
1061
|
+
h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.autoTagTitle")),
|
|
1062
|
+
h("div", { style: hintStyle }, t("memory.settings.autoTagHint")),
|
|
1063
|
+
h("label", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 8, fontSize: 13 } },
|
|
1064
|
+
h("input", { type: "checkbox", checked: autoTag, onChange: (e) => setAutoTag(e.target.checked) }),
|
|
1065
|
+
h("span", null, t("memory.settings.autoTagEnabled"))
|
|
1066
|
+
),
|
|
1067
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 6 } },
|
|
1068
|
+
h("button", { style: styles.footerButton, onClick: saveAutoTag }, t("memory.settings.autoTagSave")),
|
|
1069
|
+
autoTagSaved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.autoTagSaved"))
|
|
1070
|
+
),
|
|
1010
1071
|
// vector search
|
|
1011
1072
|
h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.vectorTitle")),
|
|
1012
1073
|
h("div", { style: hintStyle }, t("memory.settings.vectorHint")),
|
|
@@ -1120,6 +1181,7 @@ window.__ModuleLoader__.load({
|
|
|
1120
1181
|
function DirectoryPanel({ t, onJump, collapsed, setCollapsed }) {
|
|
1121
1182
|
const [dir, setDir] = useState(null); // { groups: [...], untagged: [...] }
|
|
1122
1183
|
const [status, setStatus] = useState("loading"); // loading | ready | error
|
|
1184
|
+
const [editMode, setEditMode] = useState(false); // manual tag/delete UI gate
|
|
1123
1185
|
|
|
1124
1186
|
useEffect(() => {
|
|
1125
1187
|
let cancelled = false;
|
|
@@ -1133,23 +1195,52 @@ window.__ModuleLoader__.load({
|
|
|
1133
1195
|
setStatus("ready");
|
|
1134
1196
|
})
|
|
1135
1197
|
.catch(() => { if (!cancelled) { setDir(null); setStatus("error"); } });
|
|
1198
|
+
apiFetch("/api/dsh-mneme/config")
|
|
1199
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
1200
|
+
.then((d) => { if (!cancelled && d?.config && typeof d.config.manualTagEnabled === "boolean") setEditMode(d.config.manualTagEnabled); })
|
|
1201
|
+
.catch(() => {});
|
|
1136
1202
|
return () => { cancelled = true; };
|
|
1137
1203
|
}, []);
|
|
1138
1204
|
|
|
1139
1205
|
const toggle = (key) => setCollapsed((c) => ({ ...c, [key]: !c[key] }));
|
|
1140
1206
|
|
|
1207
|
+
const toggleEdit = () => {
|
|
1208
|
+
const next = !editMode;
|
|
1209
|
+
apiFetch("/api/dsh-mneme/config", {
|
|
1210
|
+
method: "PUT",
|
|
1211
|
+
headers: { "Content-Type": "application/json" },
|
|
1212
|
+
body: JSON.stringify({ manualTagEnabled: next })
|
|
1213
|
+
}).then((res) => { if (res.ok) setEditMode(next); }).catch(() => {});
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
const handleDelete = (memId, e) => {
|
|
1217
|
+
e?.stopPropagation?.();
|
|
1218
|
+
if (typeof window === "undefined" || !window.confirm(t("directory.deleteConfirm"))) return;
|
|
1219
|
+
apiFetch("/api/dsh-mneme/memories", {
|
|
1220
|
+
method: "DELETE",
|
|
1221
|
+
headers: { "Content-Type": "application/json" },
|
|
1222
|
+
body: JSON.stringify({ id: memId })
|
|
1223
|
+
})
|
|
1224
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
1225
|
+
.then((d) => {
|
|
1226
|
+
if (!d || d.deleted !== true) return;
|
|
1227
|
+
setDir((prev) => ({
|
|
1228
|
+
groups: prev.groups.map((g) => ({ ...g, memories: g.memories.filter((m) => m.id !== memId) })),
|
|
1229
|
+
untagged: prev.untagged.filter((m) => m.id !== memId)
|
|
1230
|
+
}));
|
|
1231
|
+
})
|
|
1232
|
+
.catch(() => {});
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1141
1235
|
const renderFolder = (name, memories, extraKey, emptyLabel) => {
|
|
1142
1236
|
const folderKey = extraKey || name;
|
|
1143
1237
|
const open = !collapsed[folderKey];
|
|
1144
1238
|
const rows = memories.map((m) =>
|
|
1145
|
-
h("
|
|
1146
|
-
|
|
1147
|
-
type: "button",
|
|
1148
|
-
className: "mneme-dir-
|
|
1149
|
-
onClick: () =>
|
|
1150
|
-
},
|
|
1151
|
-
h("span", { className: "mneme-dir-ititle" }, m.title || m.content?.slice(0, 40)),
|
|
1152
|
-
h("span", { className: "mneme-dir-itime" }, formatDate(m.updated_at || m.created_at))
|
|
1239
|
+
h("div", { key: m.id, className: "mneme-dir-item" },
|
|
1240
|
+
h("span", { className: "mneme-tree-line" }),
|
|
1241
|
+
h("button", { type: "button", className: "mneme-dir-ititle", onClick: () => onJump(m) }, m.title || m.content?.slice(0, 40)),
|
|
1242
|
+
h("span", { className: "mneme-dir-itime" }, formatDate(m.updated_at || m.created_at)),
|
|
1243
|
+
editMode && h("button", { type: "button", className: "mneme-dir-del", "aria-label": "delete", onClick: (e) => handleDelete(m.id, e) }, "✕")
|
|
1153
1244
|
)
|
|
1154
1245
|
);
|
|
1155
1246
|
return h("div", {
|
|
@@ -1164,7 +1255,7 @@ window.__ModuleLoader__.load({
|
|
|
1164
1255
|
onClick: () => toggle(folderKey)
|
|
1165
1256
|
},
|
|
1166
1257
|
h("span", { className: "mneme-dir-caret" }, open ? "▾" : "▸"),
|
|
1167
|
-
h("span", { className: "mneme-dir-fname" }, name),
|
|
1258
|
+
h("span", { className: "mneme-dir-fname" }, `📁 ${name}`),
|
|
1168
1259
|
h("span", { className: "mneme-dir-fcount" }, String(memories.length))
|
|
1169
1260
|
),
|
|
1170
1261
|
open && h("div", { className: "mneme-dir-fbody" },
|
|
@@ -1180,7 +1271,11 @@ window.__ModuleLoader__.load({
|
|
|
1180
1271
|
if (status === "error" || !hasAny) {
|
|
1181
1272
|
return h("div", { className: "mneme-directory" }, h("div", { className: "mneme-dir-empty" }, t("memory.directory.empty")));
|
|
1182
1273
|
}
|
|
1183
|
-
return h("div", { className: "mneme-directory" },
|
|
1274
|
+
return h("div", { className: editMode ? "mneme-directory mneme-edit-on" : "mneme-directory" },
|
|
1275
|
+
h("label", { className: "mneme-edit-toggle" },
|
|
1276
|
+
h("input", { type: "checkbox", checked: editMode, onChange: toggleEdit }),
|
|
1277
|
+
t("directory.editToggle")
|
|
1278
|
+
),
|
|
1184
1279
|
dir.groups.map((g) => renderFolder(g.tag, g.memories)),
|
|
1185
1280
|
dir.untagged.length > 0 && renderFolder(t("memory.directory.untagged"), dir.untagged, "mneme-untagged")
|
|
1186
1281
|
);
|
|
@@ -1426,38 +1521,8 @@ window.__ModuleLoader__.load({
|
|
|
1426
1521
|
),
|
|
1427
1522
|
),
|
|
1428
1523
|
view === "memory" && h("div", { className: "mneme-xmain" },
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
h("input", {
|
|
1432
|
-
className: "mneme-search mneme-xsearch",
|
|
1433
|
-
placeholder: t("memory.explorer.search"),
|
|
1434
|
-
value: query,
|
|
1435
|
-
onChange: (e) => setQuery(e.target.value)
|
|
1436
|
-
}),
|
|
1437
|
-
entityQuery && h("button", {
|
|
1438
|
-
className: "mneme-entitychip",
|
|
1439
|
-
style: { textAlign: "left", justifyContent: "flex-start" },
|
|
1440
|
-
onClick: () => openGraphFor(entityQuery)
|
|
1441
|
-
}, `${t("memory.graph.viewInGraph")} “${entityQuery}”`),
|
|
1442
|
-
h("div", { className: "mneme-xrow" },
|
|
1443
|
-
vecEnabled && h("button", {
|
|
1444
|
-
className: semantic ? "mneme-chip mneme-active" : "mneme-chip",
|
|
1445
|
-
title: t("memory.settings.vectorTitle"),
|
|
1446
|
-
onClick: () => setSemantic(!semantic)
|
|
1447
|
-
}, t("memory.panel.semantic")),
|
|
1448
|
-
h("select", {
|
|
1449
|
-
className: "mneme-select mneme-xselect",
|
|
1450
|
-
value: searchTopK,
|
|
1451
|
-
onChange: (e) => setSearchTopK(Number(e.target.value)),
|
|
1452
|
-
title: t("memory.explorer.topK")
|
|
1453
|
-
}, [5, 10, 20, 50].map((n) => h("option", { key: n, value: n }, t("memory.explorer.topKOption").replace("{n}", String(n)))))
|
|
1454
|
-
),
|
|
1455
|
-
h("div", { className: "mneme-xrow" },
|
|
1456
|
-
h("span", { className: "mneme-xcount" }, t("memory.explorer.count").replace("{n}", String(visible.length))),
|
|
1457
|
-
h("button", { className: "mneme-footbtn", onClick: () => setReloadKey((k) => k + 1) }, t("memory.explorer.refresh"))
|
|
1458
|
-
)
|
|
1459
|
-
),
|
|
1460
|
-
h("div", { className: "mneme-xside mneme-xside--filter" },
|
|
1524
|
+
// 1. 中间分类栏
|
|
1525
|
+
h("div", { className: "mneme-xfilter-bar" },
|
|
1461
1526
|
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.types")),
|
|
1462
1527
|
h("button", {
|
|
1463
1528
|
className: type === "all" ? "mneme-xtype mneme-active" : "mneme-xtype",
|
|
@@ -1473,8 +1538,88 @@ window.__ModuleLoader__.load({
|
|
|
1473
1538
|
h("span", { className: "mneme-xcount2" }, String(counts[key]))
|
|
1474
1539
|
))
|
|
1475
1540
|
),
|
|
1476
|
-
|
|
1477
|
-
|
|
1541
|
+
// 2. 底部三卡片
|
|
1542
|
+
h("div", { className: "mneme-xcards" },
|
|
1543
|
+
// 左卡:搜索
|
|
1544
|
+
h("div", { className: "mneme-card mneme-card--search" },
|
|
1545
|
+
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.searchTitle")),
|
|
1546
|
+
h("input", {
|
|
1547
|
+
className: "mneme-search mneme-xsearch",
|
|
1548
|
+
placeholder: t("memory.explorer.search"),
|
|
1549
|
+
value: query,
|
|
1550
|
+
onChange: (e) => setQuery(e.target.value)
|
|
1551
|
+
}),
|
|
1552
|
+
entityQuery && h("button", {
|
|
1553
|
+
className: "mneme-entitychip",
|
|
1554
|
+
style: { textAlign: "left", justifyContent: "flex-start" },
|
|
1555
|
+
onClick: () => openGraphFor(entityQuery)
|
|
1556
|
+
}, `${t("memory.graph.viewInGraph")} “${entityQuery}”`),
|
|
1557
|
+
h("div", { className: "mneme-xrow" },
|
|
1558
|
+
vecEnabled && h("button", {
|
|
1559
|
+
className: semantic ? "mneme-chip mneme-active" : "mneme-chip",
|
|
1560
|
+
title: t("memory.settings.vectorTitle"),
|
|
1561
|
+
onClick: () => setSemantic(!semantic)
|
|
1562
|
+
}, t("memory.panel.semantic")),
|
|
1563
|
+
h("select", {
|
|
1564
|
+
className: "mneme-select mneme-xselect",
|
|
1565
|
+
value: searchTopK,
|
|
1566
|
+
onChange: (e) => setSearchTopK(Number(e.target.value)),
|
|
1567
|
+
title: t("memory.explorer.topK")
|
|
1568
|
+
}, [5, 10, 20, 50].map((n) => h("option", { key: n, value: n }, t("memory.explorer.topKOption").replace("{n}", String(n)))))
|
|
1569
|
+
),
|
|
1570
|
+
h("div", { className: "mneme-xrow" },
|
|
1571
|
+
h("span", { className: "mneme-xcount" }, t("memory.explorer.count").replace("{n}", String(visible.length))),
|
|
1572
|
+
h("button", { className: "mneme-footbtn", onClick: () => setReloadKey((k) => k + 1) }, t("memory.explorer.refresh"))
|
|
1573
|
+
)
|
|
1574
|
+
),
|
|
1575
|
+
// 中卡:时间树
|
|
1576
|
+
h("div", { className: "mneme-card mneme-card--tree" },
|
|
1577
|
+
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.timeline")),
|
|
1578
|
+
loading
|
|
1579
|
+
? h("div", { className: "mneme-xempty" }, "…")
|
|
1580
|
+
: months.length === 0
|
|
1581
|
+
? h("div", { className: "mneme-xempty" }, t("memory.explorer.empty"))
|
|
1582
|
+
: months.map((month) =>
|
|
1583
|
+
h("div", { key: month.key },
|
|
1584
|
+
h("button", {
|
|
1585
|
+
className: "mneme-xmonth",
|
|
1586
|
+
"aria-expanded": String(!collapsed[month.key]),
|
|
1587
|
+
onClick: () => setCollapsed((c) => ({ ...c, [month.key]: !c[month.key] }))
|
|
1588
|
+
},
|
|
1589
|
+
h("span", { className: "mneme-xcaret" }, collapsed[month.key] ? "▸" : "▾"),
|
|
1590
|
+
month.label
|
|
1591
|
+
),
|
|
1592
|
+
!collapsed[month.key] && month.days.map((day) =>
|
|
1593
|
+
h("div", { key: day.key },
|
|
1594
|
+
h("button", {
|
|
1595
|
+
className: "mneme-xday",
|
|
1596
|
+
"aria-expanded": String(!collapsed[day.key]),
|
|
1597
|
+
onClick: () => setCollapsed((c) => ({ ...c, [day.key]: !c[day.key] }))
|
|
1598
|
+
},
|
|
1599
|
+
h("span", { className: "mneme-xcaret" }, collapsed[day.key] ? "▸" : "▾"),
|
|
1600
|
+
day.label
|
|
1601
|
+
),
|
|
1602
|
+
!collapsed[day.key] && day.items.map((m) => {
|
|
1603
|
+
const d = new Date(m.updated_at || m.created_at || 0);
|
|
1604
|
+
const time = Number.isNaN(d.getTime())
|
|
1605
|
+
? ""
|
|
1606
|
+
: d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
|
|
1607
|
+
return h("button", {
|
|
1608
|
+
key: m.id,
|
|
1609
|
+
ref: (el) => { if (el) itemRefs.current.set(m.id, el); else itemRefs.current.delete(m.id); },
|
|
1610
|
+
className: m.id === selectedId ? "mneme-xitem mneme-active" : "mneme-xitem",
|
|
1611
|
+
onClick: () => setSelectedId(m.id)
|
|
1612
|
+
},
|
|
1613
|
+
h("span", { className: "mneme-xtime" }, time),
|
|
1614
|
+
h("span", { className: "mneme-xname" }, m.title || m.content?.slice(0, 40))
|
|
1615
|
+
);
|
|
1616
|
+
})
|
|
1617
|
+
)
|
|
1618
|
+
)
|
|
1619
|
+
)
|
|
1620
|
+
)),
|
|
1621
|
+
// 右卡:详情
|
|
1622
|
+
h("div", { className: "mneme-card mneme-card--detail" },
|
|
1478
1623
|
selected
|
|
1479
1624
|
? h(react.Fragment, { key: selected.id },
|
|
1480
1625
|
h("div", { className: "mneme-xdinner" },
|
|
@@ -1535,50 +1680,6 @@ window.__ModuleLoader__.load({
|
|
|
1535
1680
|
)
|
|
1536
1681
|
)
|
|
1537
1682
|
: h("div", { className: "mneme-xempty" }, t("memory.explorer.emptyDetail"))
|
|
1538
|
-
),
|
|
1539
|
-
h("div", { className: "mneme-xtree" },
|
|
1540
|
-
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.timeline")),
|
|
1541
|
-
loading
|
|
1542
|
-
? h("div", { className: "mneme-xempty" }, "…")
|
|
1543
|
-
: months.length === 0
|
|
1544
|
-
? h("div", { className: "mneme-xempty" }, t("memory.explorer.empty"))
|
|
1545
|
-
: months.map((month) =>
|
|
1546
|
-
h("div", { key: month.key },
|
|
1547
|
-
h("button", {
|
|
1548
|
-
className: "mneme-xmonth",
|
|
1549
|
-
"aria-expanded": String(!collapsed[month.key]),
|
|
1550
|
-
onClick: () => setCollapsed((c) => ({ ...c, [month.key]: !c[month.key] }))
|
|
1551
|
-
},
|
|
1552
|
-
h("span", { className: "mneme-xcaret" }, collapsed[month.key] ? "▸" : "▾"),
|
|
1553
|
-
month.label
|
|
1554
|
-
),
|
|
1555
|
-
!collapsed[month.key] && month.days.map((day) =>
|
|
1556
|
-
h("div", { key: day.key },
|
|
1557
|
-
h("button", {
|
|
1558
|
-
className: "mneme-xday",
|
|
1559
|
-
"aria-expanded": String(!collapsed[day.key]),
|
|
1560
|
-
onClick: () => setCollapsed((c) => ({ ...c, [day.key]: !c[day.key] }))
|
|
1561
|
-
},
|
|
1562
|
-
h("span", { className: "mneme-xcaret" }, collapsed[day.key] ? "▸" : "▾"),
|
|
1563
|
-
day.label
|
|
1564
|
-
),
|
|
1565
|
-
!collapsed[day.key] && day.items.map((m) => {
|
|
1566
|
-
const d = new Date(m.updated_at || m.created_at || 0);
|
|
1567
|
-
const time = Number.isNaN(d.getTime())
|
|
1568
|
-
? ""
|
|
1569
|
-
: d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
|
|
1570
|
-
return h("button", {
|
|
1571
|
-
key: m.id,
|
|
1572
|
-
ref: (el) => { if (el) itemRefs.current.set(m.id, el); else itemRefs.current.delete(m.id); },
|
|
1573
|
-
className: m.id === selectedId ? "mneme-xitem mneme-active" : "mneme-xitem",
|
|
1574
|
-
onClick: () => setSelectedId(m.id)
|
|
1575
|
-
},
|
|
1576
|
-
h("span", { className: "mneme-xtime" }, time),
|
|
1577
|
-
h("span", { className: "mneme-xname" }, m.title || m.content?.slice(0, 40))
|
|
1578
|
-
);
|
|
1579
|
-
})
|
|
1580
|
-
))
|
|
1581
|
-
))
|
|
1582
1683
|
)
|
|
1583
1684
|
)
|
|
1584
1685
|
),
|
package/lib/settings.js
CHANGED
|
@@ -137,6 +137,36 @@ export function createSettings(db) {
|
|
|
137
137
|
};
|
|
138
138
|
setSetting("vector", JSON.stringify(cfg));
|
|
139
139
|
return cfg;
|
|
140
|
+
},
|
|
141
|
+
/** Tagging switches (autoTag opt-in LLM pass + manual tag editing gate). */
|
|
142
|
+
getAutoTagConfig() {
|
|
143
|
+
const raw = getSetting("autoTag");
|
|
144
|
+
let stored = {};
|
|
145
|
+
if (raw) {
|
|
146
|
+
try {
|
|
147
|
+
const j = JSON.parse(raw);
|
|
148
|
+
if (j && typeof j === "object") stored = j;
|
|
149
|
+
} catch { /* fall through to defaults */ }
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
autoTagEnabled: stored.autoTagEnabled === true,
|
|
153
|
+
manualTagEnabled: stored.manualTagEnabled === true
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
setAutoTagConfig(partial) {
|
|
157
|
+
const cur = (() => {
|
|
158
|
+
const raw = getSetting("autoTag");
|
|
159
|
+
try {
|
|
160
|
+
const j = JSON.parse(raw);
|
|
161
|
+
return j && typeof j === "object" ? j : {};
|
|
162
|
+
} catch { return {}; }
|
|
163
|
+
})();
|
|
164
|
+
const cfg = {
|
|
165
|
+
autoTagEnabled: partial.autoTagEnabled === undefined ? cur.autoTagEnabled === true : partial.autoTagEnabled === true,
|
|
166
|
+
manualTagEnabled: partial.manualTagEnabled === undefined ? cur.manualTagEnabled === true : partial.manualTagEnabled === true
|
|
167
|
+
};
|
|
168
|
+
setSetting("autoTag", JSON.stringify(cfg));
|
|
169
|
+
return cfg;
|
|
140
170
|
}
|
|
141
171
|
};
|
|
142
172
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
3
|
"description": "Cross-session memory plugin for DeepSeek Harness with autoDream consolidation: SQLite store, Markdown mirrors, 7 model tools, automatic injection, session summarization, user profile/rules, custom slash commands, vector (semantic) search, and a Web GUI panel",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.7",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -57,12 +57,14 @@
|
|
|
57
57
|
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
58
58
|
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
59
59
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
60
|
-
"@deepseek-ai/schemastery": "^3.18.1"
|
|
60
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
61
|
+
"c8": "^12.0.0"
|
|
61
62
|
},
|
|
62
63
|
"scripts": {
|
|
63
64
|
"sync": "node scripts/sync-lib.js",
|
|
64
65
|
"prepack": "npm run sync",
|
|
65
66
|
"test": "node --test test/*.test.js",
|
|
67
|
+
"test:coverage": "c8 node --test test/*.test.js",
|
|
66
68
|
"e2e": "node scripts/e2e-dsh.js",
|
|
67
69
|
"stress": "node scripts/stress-dsh.js"
|
|
68
70
|
},
|
|
@@ -71,5 +73,8 @@
|
|
|
71
73
|
},
|
|
72
74
|
"overrides": {
|
|
73
75
|
"adm-zip": "0.6.0"
|
|
76
|
+
},
|
|
77
|
+
"c8": {
|
|
78
|
+
"reporter": ["text", "lcov"]
|
|
74
79
|
}
|
|
75
80
|
}
|
package/src/api.js
CHANGED
|
@@ -218,6 +218,96 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
218
218
|
}
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
+
// --- app config read/write (partial: pass only the fields to change) ---
|
|
222
|
+
register({
|
|
223
|
+
kind: "exact",
|
|
224
|
+
path: "/api/dsh-mneme/config",
|
|
225
|
+
handler: async (req, res) => {
|
|
226
|
+
try {
|
|
227
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
228
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
229
|
+
const body = parseBody(await readBody(req));
|
|
230
|
+
const patch = {};
|
|
231
|
+
const setIfBoolean = (key) => {
|
|
232
|
+
if (Object.prototype.hasOwnProperty.call(body, key)) {
|
|
233
|
+
if (typeof body[key] !== "boolean") {
|
|
234
|
+
sendJson(res, 400, { error: `${key} must be a boolean` });
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
patch[key] = body[key];
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
};
|
|
241
|
+
if (!setIfBoolean("autoTagEnabled") || !setIfBoolean("manualTagEnabled")) return;
|
|
242
|
+
if (Object.keys(patch).length === 0) {
|
|
243
|
+
sendJson(res, 400, { error: "no valid config field provided" });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const config = settings.setAutoTagConfig(patch);
|
|
247
|
+
sendJson(res, 200, { config });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
sendJson(res, 200, { config: settings.getAutoTagConfig() });
|
|
251
|
+
} catch {
|
|
252
|
+
sendJson(res, 500, { error: "internal" });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// --- delete a memory by exact id or best query match (mirrors memory_delete tool) ---
|
|
258
|
+
register({
|
|
259
|
+
kind: "exact",
|
|
260
|
+
path: "/api/dsh-mneme/memories",
|
|
261
|
+
handler: async (req, res) => {
|
|
262
|
+
try {
|
|
263
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
264
|
+
if (req.method !== "DELETE") {
|
|
265
|
+
sendJson(res, 405, { error: "method not allowed" });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const body = parseBody(await readBody(req));
|
|
269
|
+
const hasId = Object.prototype.hasOwnProperty.call(body, "id");
|
|
270
|
+
const hasQuery = Object.prototype.hasOwnProperty.call(body, "query");
|
|
271
|
+
if ((hasId && hasQuery) || (!hasId && !hasQuery)) {
|
|
272
|
+
sendJson(res, 400, { error: "provide exactly one of id or query" });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (hasId) {
|
|
276
|
+
if (typeof body.id !== "string" || !body.id.trim()) {
|
|
277
|
+
sendJson(res, 400, { error: "invalid id" });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const mem = service.getById(body.id);
|
|
281
|
+
if (!mem) {
|
|
282
|
+
sendJson(res, 200, { deleted: false });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
service.remove(body.id);
|
|
286
|
+
sendJson(res, 200, { deleted: true, id: body.id });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (typeof body.query !== "string" || !body.query.trim()) {
|
|
290
|
+
sendJson(res, 400, { error: "invalid query" });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const results = await service.searchMemories(body.query, { mode: "auto", topK: 1, useRerank: true });
|
|
294
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
295
|
+
sendJson(res, 200, { deleted: false });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const mem = results[0];
|
|
299
|
+
if (!mem || !mem.id) {
|
|
300
|
+
sendJson(res, 200, { deleted: false });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
service.remove(mem.id);
|
|
304
|
+
sendJson(res, 200, { deleted: true, id: mem.id });
|
|
305
|
+
} catch {
|
|
306
|
+
sendJson(res, 500, { error: "internal" });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
221
311
|
// --- vector re-index (backfill embeddings for rows missing them) ---
|
|
222
312
|
register({
|
|
223
313
|
kind: "exact",
|
package/src/settings.js
CHANGED
|
@@ -137,6 +137,36 @@ export function createSettings(db) {
|
|
|
137
137
|
};
|
|
138
138
|
setSetting("vector", JSON.stringify(cfg));
|
|
139
139
|
return cfg;
|
|
140
|
+
},
|
|
141
|
+
/** Tagging switches (autoTag opt-in LLM pass + manual tag editing gate). */
|
|
142
|
+
getAutoTagConfig() {
|
|
143
|
+
const raw = getSetting("autoTag");
|
|
144
|
+
let stored = {};
|
|
145
|
+
if (raw) {
|
|
146
|
+
try {
|
|
147
|
+
const j = JSON.parse(raw);
|
|
148
|
+
if (j && typeof j === "object") stored = j;
|
|
149
|
+
} catch { /* fall through to defaults */ }
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
autoTagEnabled: stored.autoTagEnabled === true,
|
|
153
|
+
manualTagEnabled: stored.manualTagEnabled === true
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
setAutoTagConfig(partial) {
|
|
157
|
+
const cur = (() => {
|
|
158
|
+
const raw = getSetting("autoTag");
|
|
159
|
+
try {
|
|
160
|
+
const j = JSON.parse(raw);
|
|
161
|
+
return j && typeof j === "object" ? j : {};
|
|
162
|
+
} catch { return {}; }
|
|
163
|
+
})();
|
|
164
|
+
const cfg = {
|
|
165
|
+
autoTagEnabled: partial.autoTagEnabled === undefined ? cur.autoTagEnabled === true : partial.autoTagEnabled === true,
|
|
166
|
+
manualTagEnabled: partial.manualTagEnabled === undefined ? cur.manualTagEnabled === true : partial.manualTagEnabled === true
|
|
167
|
+
};
|
|
168
|
+
setSetting("autoTag", JSON.stringify(cfg));
|
|
169
|
+
return cfg;
|
|
140
170
|
}
|
|
141
171
|
};
|
|
142
172
|
}
|
package/test/api.test.js
CHANGED
|
@@ -547,3 +547,48 @@ test("GET /api/dsh-mneme/directory reports untagged memories", async () => {
|
|
|
547
547
|
const data = JSON.parse(res.body);
|
|
548
548
|
assert.deepEqual(data.untagged.map((m) => m.id), [bare.id]);
|
|
549
549
|
});
|
|
550
|
+
|
|
551
|
+
test("GET/PUT /api/dsh-mneme/config round-trips autoTag switches", async () => {
|
|
552
|
+
const { routes } = setup();
|
|
553
|
+
const route = routes.find((r) => r.path === "/api/dsh-mneme/config");
|
|
554
|
+
const res1 = new FakeRes();
|
|
555
|
+
await route.handler(req("/api/dsh-mneme/config"), res1);
|
|
556
|
+
assert.equal(res1.statusCode, 200);
|
|
557
|
+
assert.deepEqual(JSON.parse(res1.body).config, { autoTagEnabled: false, manualTagEnabled: false });
|
|
558
|
+
const res2 = new FakeRes();
|
|
559
|
+
await route.handler(req("/api/dsh-mneme/config", "PUT", { autoTagEnabled: true, manualTagEnabled: false }), res2);
|
|
560
|
+
assert.equal(res2.statusCode, 200);
|
|
561
|
+
assert.deepEqual(JSON.parse(res2.body).config, { autoTagEnabled: true, manualTagEnabled: false });
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("DELETE /api/dsh-mneme/memories removes by id and reports misses", async () => {
|
|
565
|
+
const { routes, service } = setup();
|
|
566
|
+
service.saveWithDedupe({ type: "preference", title: "语言", content: "中文" });
|
|
567
|
+
const target = service.list().find((m) => m.title === "语言");
|
|
568
|
+
const id = target.id;
|
|
569
|
+
const route = routes.find((r) => r.path === "/api/dsh-mneme/memories");
|
|
570
|
+
const res = new FakeRes();
|
|
571
|
+
await route.handler(req("/api/dsh-mneme/memories", "DELETE", { id }), res);
|
|
572
|
+
assert.equal(res.statusCode, 200);
|
|
573
|
+
assert.equal(JSON.parse(res.body).deleted, true);
|
|
574
|
+
const res2 = new FakeRes();
|
|
575
|
+
await route.handler(req("/api/dsh-mneme/memories", "DELETE", { id }), res2);
|
|
576
|
+
assert.equal(JSON.parse(res2.body).deleted, false);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test("DELETE /api/dsh-mneme/memories rejects id+query together", async () => {
|
|
580
|
+
const { routes } = setup();
|
|
581
|
+
const route = routes.find((r) => r.path === "/api/dsh-mneme/memories");
|
|
582
|
+
const res = new FakeRes();
|
|
583
|
+
await route.handler(req("/api/dsh-mneme/memories", "DELETE", { id: "x", query: "y" }), res);
|
|
584
|
+
assert.equal(res.statusCode, 400);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
test("PUT /api/dsh-mneme/config supports partial update", async () => {
|
|
588
|
+
const { routes } = setup();
|
|
589
|
+
const route = routes.find((r) => r.path === "/api/dsh-mneme/config");
|
|
590
|
+
const res = new FakeRes();
|
|
591
|
+
await route.handler(req("/api/dsh-mneme/config", "PUT", { autoTagEnabled: true }), res);
|
|
592
|
+
assert.equal(res.statusCode, 200);
|
|
593
|
+
assert.deepEqual(JSON.parse(res.body).config, { autoTagEnabled: true, manualTagEnabled: false });
|
|
594
|
+
});
|
package/test/settings.test.js
CHANGED
|
@@ -99,3 +99,20 @@ test("vector config disabled value is stored as false", () => {
|
|
|
99
99
|
assert.equal(settings.getVectorConfig().enabled, false);
|
|
100
100
|
store.close();
|
|
101
101
|
});
|
|
102
|
+
|
|
103
|
+
test("autoTag config defaults to disabled and round-trips", () => {
|
|
104
|
+
const { store, settings } = setup();
|
|
105
|
+
assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: false, manualTagEnabled: false });
|
|
106
|
+
settings.setAutoTagConfig({ autoTagEnabled: true, manualTagEnabled: true });
|
|
107
|
+
assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: true, manualTagEnabled: true });
|
|
108
|
+
settings.setAutoTagConfig({ autoTagEnabled: false });
|
|
109
|
+
assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: false, manualTagEnabled: true });
|
|
110
|
+
store.close();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("autoTag config coerces non-boolean to false", () => {
|
|
114
|
+
const { store, settings } = setup();
|
|
115
|
+
settings.setAutoTagConfig({ autoTagEnabled: "yes", manualTagEnabled: 1 });
|
|
116
|
+
assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: false, manualTagEnabled: false });
|
|
117
|
+
store.close();
|
|
118
|
+
});
|