@modusensus/dsh-mneme 0.6.0 → 0.6.5
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 +25 -2
- package/lib/api.js +178 -1
- package/lib/client.js +363 -9
- package/lib/config.js +27 -0
- package/lib/dream/tag-extractor.js +149 -0
- package/lib/dream.js +20 -0
- package/lib/mirror.js +9 -0
- package/lib/parser/tag.js +59 -0
- package/lib/parser/wiki-link.js +38 -0
- package/lib/search/tag-boost.js +61 -0
- package/lib/service.js +211 -4
- package/lib/store.js +242 -2
- package/package.json +1 -1
- package/src/api.js +178 -1
- package/src/config.js +27 -0
- package/src/dream/tag-extractor.js +149 -0
- package/src/dream.js +20 -0
- package/src/mirror.js +9 -0
- package/src/parser/tag.js +59 -0
- package/src/parser/wiki-link.js +38 -0
- package/src/search/tag-boost.js +61 -0
- package/src/service.js +211 -4
- package/src/store.js +242 -2
- package/test/api.test.js +80 -0
- package/test/boundary-v0625.test.js +82 -0
- package/test/client.test.js +134 -0
- package/test/directory.test.js +134 -0
- package/test/tag-boost.test.js +125 -0
- package/test/tag.test.js +294 -0
- package/test/wiki-link.test.js +332 -0
package/lib/client.js
CHANGED
|
@@ -88,6 +88,7 @@ window.__ModuleLoader__.load({
|
|
|
88
88
|
"memory.overlay.close": "关闭",
|
|
89
89
|
"memory.explorer.tabMemory": "记忆",
|
|
90
90
|
"memory.explorer.tabGraph": "图谱",
|
|
91
|
+
"memory.explorer.tabDirectory": "目录",
|
|
91
92
|
"memory.explorer.tabSettings": "设置",
|
|
92
93
|
"memory.explorer.search": "搜索标题或内容…",
|
|
93
94
|
"memory.explorer.searchTitle": "语义检索",
|
|
@@ -104,9 +105,16 @@ window.__ModuleLoader__.load({
|
|
|
104
105
|
"memory.explorer.created": "创建",
|
|
105
106
|
"memory.explorer.updated": "更新",
|
|
106
107
|
"memory.explorer.tags": "标签",
|
|
108
|
+
"memory.explorer.tagAdd": "添加标签",
|
|
109
|
+
"memory.explorer.tagRemove": "移除标签",
|
|
110
|
+
"memory.explorer.tagPlaceholder": "输入标签,回车添加",
|
|
111
|
+
"memory.explorer.tagsEmpty": "暂无标签",
|
|
107
112
|
"memory.explorer.importance": "重要性",
|
|
108
113
|
"memory.explorer.topK": "返回数量",
|
|
109
114
|
"memory.explorer.topKOption": "返回 {n} 条",
|
|
115
|
+
"memory.directory.untagged": "无标签",
|
|
116
|
+
"memory.directory.loading": "加载中…",
|
|
117
|
+
"memory.directory.empty": "暂无记忆条目",
|
|
110
118
|
"memory.card.open": "在主区记忆库中查看全文",
|
|
111
119
|
"memory.time.now": "刚刚",
|
|
112
120
|
"memory.time.seconds": "{n}秒前",
|
|
@@ -142,7 +150,12 @@ window.__ModuleLoader__.load({
|
|
|
142
150
|
"memory.settings.apiTokenHint": "设置后,写操作与密钥接口(画像/规则/命令/向量配置)需携带 Authorization: Bearer <token>;面板只读操作不受影响。清空并保存可关闭鉴权。",
|
|
143
151
|
"memory.settings.apiTokenPlaceholder": "留空 = 不鉴权(默认)",
|
|
144
152
|
"memory.settings.apiTokenSave": "保存 Token",
|
|
145
|
-
"memory.settings.apiTokenSaved": "Token 已保存"
|
|
153
|
+
"memory.settings.apiTokenSaved": "Token 已保存",
|
|
154
|
+
"memory.wikilink.backlinks": "链接到此记忆",
|
|
155
|
+
"memory.wikilink.forward": "此记忆链接到",
|
|
156
|
+
"memory.wikilink.empty": "暂无关联记忆",
|
|
157
|
+
"memory.wikilink.loading": "加载中…",
|
|
158
|
+
"memory.wikilink.unresolved": "目标记忆不存在"
|
|
146
159
|
},
|
|
147
160
|
en: {
|
|
148
161
|
"memory.panel.empty": "No memories yet",
|
|
@@ -175,6 +188,7 @@ window.__ModuleLoader__.load({
|
|
|
175
188
|
"memory.overlay.close": "Close",
|
|
176
189
|
"memory.explorer.tabMemory": "Memories",
|
|
177
190
|
"memory.explorer.tabGraph": "Graph",
|
|
191
|
+
"memory.explorer.tabDirectory": "Directory",
|
|
178
192
|
"memory.explorer.tabSettings": "Settings",
|
|
179
193
|
"memory.explorer.search": "Search title or content…",
|
|
180
194
|
"memory.explorer.searchTitle": "Semantic Search",
|
|
@@ -191,9 +205,16 @@ window.__ModuleLoader__.load({
|
|
|
191
205
|
"memory.explorer.created": "Created",
|
|
192
206
|
"memory.explorer.updated": "Updated",
|
|
193
207
|
"memory.explorer.tags": "Tags",
|
|
208
|
+
"memory.explorer.tagAdd": "Add tag",
|
|
209
|
+
"memory.explorer.tagRemove": "Remove tag",
|
|
210
|
+
"memory.explorer.tagPlaceholder": "Type a tag, press Enter",
|
|
211
|
+
"memory.explorer.tagsEmpty": "No tags",
|
|
194
212
|
"memory.explorer.importance": "Importance",
|
|
195
213
|
"memory.explorer.topK": "Results limit",
|
|
196
214
|
"memory.explorer.topKOption": "Return {n}",
|
|
215
|
+
"memory.directory.untagged": "Untagged",
|
|
216
|
+
"memory.directory.loading": "Loading…",
|
|
217
|
+
"memory.directory.empty": "No memories yet",
|
|
197
218
|
"memory.card.open": "Open full text in the Memory tab",
|
|
198
219
|
"memory.time.now": "just now",
|
|
199
220
|
"memory.time.seconds": "{n}s ago",
|
|
@@ -229,7 +250,12 @@ window.__ModuleLoader__.load({
|
|
|
229
250
|
"memory.settings.apiTokenHint": "When set, write operations and secret endpoints (profile/rules/commands/vector config) require Authorization: Bearer <token>. Read-only panel calls stay open. Save empty to disable.",
|
|
230
251
|
"memory.settings.apiTokenPlaceholder": "Empty = no auth (default)",
|
|
231
252
|
"memory.settings.apiTokenSave": "Save Token",
|
|
232
|
-
"memory.settings.apiTokenSaved": "Token saved"
|
|
253
|
+
"memory.settings.apiTokenSaved": "Token saved",
|
|
254
|
+
"memory.wikilink.backlinks": "Links to this memory",
|
|
255
|
+
"memory.wikilink.forward": "This memory links to",
|
|
256
|
+
"memory.wikilink.empty": "No linked memories",
|
|
257
|
+
"memory.wikilink.loading": "Loading…",
|
|
258
|
+
"memory.wikilink.unresolved": "Target memory not found"
|
|
233
259
|
}
|
|
234
260
|
};
|
|
235
261
|
|
|
@@ -289,6 +315,17 @@ window.__ModuleLoader__.load({
|
|
|
289
315
|
".mneme-hint{color:var(--dsw-alias-label-tertiary);padding:24px 0;text-align:center;font-size:13px}",
|
|
290
316
|
".mneme-entitychip{flex:none;height:26px;padding:0 10px;border-radius:8px;border:none;background:none;color:var(--dsw-alias-state-business-primary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center}",
|
|
291
317
|
".mneme-entitychip:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
|
|
318
|
+
// --- tag chips in the detail meta (v0.6.2) ---
|
|
319
|
+
".mneme-tagrow{display:flex;flex-wrap:wrap;gap:6px;align-items:center}",
|
|
320
|
+
".mneme-tagchip{display:inline-flex;align-items:center;gap:4px;max-width:100%;height:22px;padding:0 4px 0 8px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-state-business-primary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px}",
|
|
321
|
+
".mneme-tagchip:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
|
|
322
|
+
".mneme-tagremove{flex:none;display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border:none;border-radius:4px;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-family:inherit;font-size:12px;line-height:1;padding:0}",
|
|
323
|
+
".mneme-tagremove:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
324
|
+
".mneme-tagadd{border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;padding:2px 8px;border-radius:6px;flex:none}",
|
|
325
|
+
".mneme-tagadd:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
326
|
+
".mneme-taginput{box-sizing:border-box;height:22px;padding:0 8px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:12px;outline:none;width:150px}",
|
|
327
|
+
".mneme-taginput:focus{border-color:var(--dsw-alias-state-business-primary)}",
|
|
328
|
+
".mneme-tagempty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}",
|
|
292
329
|
// --- main-area memory library page ---
|
|
293
330
|
".mneme-x{flex:1;min-height:0;height:100%;width:100%;box-sizing:border-box;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2)}",
|
|
294
331
|
".mneme-xbar{flex:none;display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding:0 16px}",
|
|
@@ -333,6 +370,21 @@ window.__ModuleLoader__.load({
|
|
|
333
370
|
".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)}",
|
|
334
371
|
".mneme-xdcontent{margin-top:14px;font-size:14px;line-height:1.75;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word}",
|
|
335
372
|
".mneme-xdactions{display:flex;gap:8px;margin-top:18px}",
|
|
373
|
+
// --- wiki-link backlinks panel (detail pane footer) ---
|
|
374
|
+
".mneme-backlinks{margin-top:18px;padding-top:14px;border-top:1px solid var(--dsw-alias-border-l1);display:flex;flex-direction:column;gap:12px}",
|
|
375
|
+
".mneme-bl-block{display:flex;flex-direction:column;gap:4px}",
|
|
376
|
+
".mneme-bl-head{font-size:12px;font-weight:500;color:var(--dsw-alias-label-tertiary)}",
|
|
377
|
+
".mneme-bl-list{display:flex;flex-wrap:wrap;gap:6px}",
|
|
378
|
+
".mneme-bl-link{display:inline-flex;align-items:center;gap:6px;max-width:100%;border:none;background:none;color:var(--dsw-alias-state-business-primary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;padding:2px 8px;border-radius:6px;text-align:left}",
|
|
379
|
+
".mneme-bl-link:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
|
|
380
|
+
".mneme-bl-link.mneme-bl-link--dim{color:var(--dsw-alias-label-tertiary);cursor:default}",
|
|
381
|
+
".mneme-bl-link.mneme-bl-link--dim:hover{background:none}",
|
|
382
|
+
".mneme-bl-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
383
|
+
".mneme-bl-meta{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary)}",
|
|
384
|
+
".mneme-bl-empty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
|
|
385
|
+
// --- inline [[wiki-link]] links inside the detail content ---
|
|
386
|
+
".mneme-wikilink{display:inline;padding:0 2px;border:none;background:none;color:var(--dsw-alias-state-business-primary);cursor:pointer;font-family:inherit;font-size:inherit;line-height:inherit;text-decoration:underline;text-underline-offset:2px;border-radius:3px}",
|
|
387
|
+
".mneme-wikilink:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 8%,transparent)}",
|
|
336
388
|
// --- graph sub-view (fills the content area under the tabs) ---
|
|
337
389
|
".mneme-graph{flex:1;min-height:0;width:100%;display:flex;flex-direction:column;padding:12px 16px 16px;box-sizing:border-box}",
|
|
338
390
|
".mneme-graphbar{display:flex;gap:8px;align-items:center;flex:none;margin-bottom:10px}",
|
|
@@ -354,6 +406,21 @@ window.__ModuleLoader__.load({
|
|
|
354
406
|
".mneme-gs-attrval{color:var(--dsw-alias-label-secondary);word-break:break-word;font-size:13px}",
|
|
355
407
|
".mneme-gs-link{display:block;width:100%;text-align:left;border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;padding:3px 6px;border-radius:6px;word-break:break-word}",
|
|
356
408
|
".mneme-gs-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
409
|
+
// --- directory sub-view (v0.6.3): tag folders + memory entries ---
|
|
410
|
+
".mneme-directory{flex:1;min-height:0;overflow-y:auto;padding:8px 10px 28px}",
|
|
411
|
+
".mneme-dir-folder{margin-bottom:4px}",
|
|
412
|
+
".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
|
+
".mneme-dir-folderhead:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
414
|
+
".mneme-dir-caret{flex:none;width:10px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px}",
|
|
415
|
+
".mneme-dir-fname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
416
|
+
".mneme-dir-fcount{flex:none;margin-left:auto;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
|
|
417
|
+
".mneme-dir-fbody{margin:2px 0 4px 22px;padding-left:8px;border-left:1px solid var(--dsw-alias-border-l2)}",
|
|
418
|
+
".mneme-dir-item{display:flex;gap:8px;align-items:baseline;width:100%;padding:4px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;text-align:left}",
|
|
419
|
+
".mneme-dir-item:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
420
|
+
".mneme-dir-item.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
|
|
421
|
+
".mneme-dir-ititle{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
422
|
+
".mneme-dir-itime{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
|
|
423
|
+
".mneme-dir-empty{color:var(--dsw-alias-label-tertiary);padding:24px 0;text-align:center;font-size:13px}",
|
|
357
424
|
// --- settings sub-view ---
|
|
358
425
|
".mneme-xsettings{flex:1;min-height:0;overflow-y:auto;padding:24px 24px 48px}",
|
|
359
426
|
".mneme-xsettings-inner{max-width:640px}",
|
|
@@ -409,6 +476,92 @@ window.__ModuleLoader__.load({
|
|
|
409
476
|
});
|
|
410
477
|
}
|
|
411
478
|
|
|
479
|
+
// --- wiki-link (v0.6.1) -------------------------------------------------
|
|
480
|
+
// Splits detail content into literal text and clickable [[target]] /
|
|
481
|
+
// [[display|target]] links. Mirrors src/parser/wiki-link.js: single pipe
|
|
482
|
+
// only, empty / multi-pipe / unclosed markers stay literal text.
|
|
483
|
+
function wikilinkSegments(text, onClick) {
|
|
484
|
+
if (typeof text !== "string" || text.length === 0) return text;
|
|
485
|
+
const out = [];
|
|
486
|
+
let last = 0;
|
|
487
|
+
let key = 0;
|
|
488
|
+
const re = /\[\[([^\[\]]*)\]\]/g;
|
|
489
|
+
let m;
|
|
490
|
+
while ((m = re.exec(text)) !== null) {
|
|
491
|
+
const parts = m[1].split("|");
|
|
492
|
+
if (parts.length > 2) continue; // 多管道 → 非法,保留字面文本
|
|
493
|
+
const target = (parts.length === 2 ? parts[1] : parts[0]).trim();
|
|
494
|
+
if (!target) continue; // 空目标 → 非法
|
|
495
|
+
if (m.index > last) out.push(text.slice(last, m.index));
|
|
496
|
+
out.push(h("button", {
|
|
497
|
+
key: `w${key++}`,
|
|
498
|
+
type: "button",
|
|
499
|
+
className: "mneme-wikilink",
|
|
500
|
+
title: target,
|
|
501
|
+
onClick: () => onClick(target)
|
|
502
|
+
}, parts[0].trim() || target));
|
|
503
|
+
last = m.index + m[0].length;
|
|
504
|
+
}
|
|
505
|
+
if (last < text.length) out.push(text.slice(last));
|
|
506
|
+
return out.length ? out : text;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Backlinks panel: the two wiki-link relation blocks under the detail pane.
|
|
510
|
+
// Backlinks = memories whose content links to this one; forward = memories
|
|
511
|
+
// this one links to (unresolved targets surface as dim, non-clickable).
|
|
512
|
+
// Mounted per-selection (the surrounding Fragment keys on selected.id), so
|
|
513
|
+
// state resets and the fetches re-run on every switch.
|
|
514
|
+
function BacklinksPanel({ memory, t, onJump }) {
|
|
515
|
+
const [back, setBack] = useState(null); // null = loading
|
|
516
|
+
const [forward, setForward] = useState(null);
|
|
517
|
+
|
|
518
|
+
useEffect(() => {
|
|
519
|
+
let cancelled = false;
|
|
520
|
+
if (!memory || !memory.id) return;
|
|
521
|
+
apiFetch(`/api/dsh-mneme/wikilinks/backlinks?id=${encodeURIComponent(memory.id)}`)
|
|
522
|
+
.then((r) => (r.ok ? r.json() : { backlinks: [] }))
|
|
523
|
+
.then((j) => { if (!cancelled) setBack(Array.isArray(j.backlinks) ? j.backlinks : []); })
|
|
524
|
+
.catch(() => { if (!cancelled) setBack([]); });
|
|
525
|
+
apiFetch(`/api/dsh-mneme/wikilinks/forward?id=${encodeURIComponent(memory.id)}`)
|
|
526
|
+
.then((r) => (r.ok ? r.json() : { links: [] }))
|
|
527
|
+
.then((j) => { if (!cancelled) setForward(Array.isArray(j.links) ? j.links : []); })
|
|
528
|
+
.catch(() => { if (!cancelled) setForward([]); });
|
|
529
|
+
return () => { cancelled = true; };
|
|
530
|
+
}, [memory && memory.id]);
|
|
531
|
+
|
|
532
|
+
if (!memory || !memory.id) return null;
|
|
533
|
+
|
|
534
|
+
const loading = back === null || forward === null;
|
|
535
|
+
const row = (link, i) => {
|
|
536
|
+
const canJump = !!link.id;
|
|
537
|
+
return h("button", {
|
|
538
|
+
key: link.id ?? `unresolved-${i}`,
|
|
539
|
+
type: "button",
|
|
540
|
+
className: canJump ? "mneme-bl-link" : "mneme-bl-link mneme-bl-link--dim",
|
|
541
|
+
title: canJump ? t("memory.card.open") : t("memory.wikilink.unresolved"),
|
|
542
|
+
disabled: !canJump,
|
|
543
|
+
onClick: () => onJump && onJump({ id: link.id })
|
|
544
|
+
},
|
|
545
|
+
h("span", { className: "mneme-bl-title" }, link.title || "—"),
|
|
546
|
+
link.type && h("span", { className: "mneme-bl-meta" }, typeLabel(t, link.type))
|
|
547
|
+
);
|
|
548
|
+
};
|
|
549
|
+
const section = (label, items) =>
|
|
550
|
+
h("div", { className: "mneme-bl-block" },
|
|
551
|
+
h("div", { className: "mneme-bl-head" }, label),
|
|
552
|
+
loading
|
|
553
|
+
? h("div", { className: "mneme-bl-empty" }, t("memory.wikilink.loading"))
|
|
554
|
+
: items.length === 0
|
|
555
|
+
? h("div", { className: "mneme-bl-empty" }, t("memory.wikilink.empty"))
|
|
556
|
+
: h("div", { className: "mneme-bl-list" }, items.map(row))
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
return h("div", { className: "mneme-backlinks" },
|
|
560
|
+
section(t("memory.wikilink.backlinks"), back || []),
|
|
561
|
+
section(t("memory.wikilink.forward"), forward || [])
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
412
565
|
// --- Graph view: ego-graph of one entity, zero-dependency SVG force layout ---
|
|
413
566
|
// The DSH client module table only whitelists platform modules, so a graph
|
|
414
567
|
// library like vis-network cannot be required from a plugin. A hand-rolled
|
|
@@ -957,10 +1110,86 @@ window.__ModuleLoader__.load({
|
|
|
957
1110
|
return tree;
|
|
958
1111
|
}
|
|
959
1112
|
|
|
1113
|
+
// --- directory sub-view (v0.6.3) ---
|
|
1114
|
+
// A tag folder tree over the live memory set. One-level folders keyed by
|
|
1115
|
+
// tag (handled on the server, getDirectory), each with a 二级 memory
|
|
1116
|
+
// entry list (title + updated time); memories without a live tag land in
|
|
1117
|
+
// the "untagged" folder. Collapse state lives in MemoryExplorer (lifted via
|
|
1118
|
+
// `collapsed`), so switching tabs does not drop it. Clicking an entry jumps
|
|
1119
|
+
// back into the browser with that memory selected (onJump = jumpToMemory).
|
|
1120
|
+
function DirectoryPanel({ t, onJump, collapsed, setCollapsed }) {
|
|
1121
|
+
const [dir, setDir] = useState(null); // { groups: [...], untagged: [...] }
|
|
1122
|
+
const [status, setStatus] = useState("loading"); // loading | ready | error
|
|
1123
|
+
|
|
1124
|
+
useEffect(() => {
|
|
1125
|
+
let cancelled = false;
|
|
1126
|
+
setStatus("loading");
|
|
1127
|
+
apiFetch("/api/dsh-mneme/directory")
|
|
1128
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
1129
|
+
.then((d) => {
|
|
1130
|
+
if (cancelled) return;
|
|
1131
|
+
if (!d) { setDir(null); setStatus("error"); return; }
|
|
1132
|
+
setDir({ groups: Array.isArray(d.groups) ? d.groups : [], untagged: Array.isArray(d.untagged) ? d.untagged : [] });
|
|
1133
|
+
setStatus("ready");
|
|
1134
|
+
})
|
|
1135
|
+
.catch(() => { if (!cancelled) { setDir(null); setStatus("error"); } });
|
|
1136
|
+
return () => { cancelled = true; };
|
|
1137
|
+
}, []);
|
|
1138
|
+
|
|
1139
|
+
const toggle = (key) => setCollapsed((c) => ({ ...c, [key]: !c[key] }));
|
|
1140
|
+
|
|
1141
|
+
const renderFolder = (name, memories, extraKey, emptyLabel) => {
|
|
1142
|
+
const folderKey = extraKey || name;
|
|
1143
|
+
const open = !collapsed[folderKey];
|
|
1144
|
+
const rows = memories.map((m) =>
|
|
1145
|
+
h("button", {
|
|
1146
|
+
key: m.id,
|
|
1147
|
+
type: "button",
|
|
1148
|
+
className: "mneme-dir-item",
|
|
1149
|
+
onClick: () => onJump(m)
|
|
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))
|
|
1153
|
+
)
|
|
1154
|
+
);
|
|
1155
|
+
return h("div", {
|
|
1156
|
+
key: folderKey,
|
|
1157
|
+
className: "mneme-dir-folder",
|
|
1158
|
+
"data-expanded": String(open)
|
|
1159
|
+
},
|
|
1160
|
+
h("button", {
|
|
1161
|
+
type: "button",
|
|
1162
|
+
className: "mneme-dir-folderhead",
|
|
1163
|
+
"aria-expanded": String(open),
|
|
1164
|
+
onClick: () => toggle(folderKey)
|
|
1165
|
+
},
|
|
1166
|
+
h("span", { className: "mneme-dir-caret" }, open ? "▾" : "▸"),
|
|
1167
|
+
h("span", { className: "mneme-dir-fname" }, name),
|
|
1168
|
+
h("span", { className: "mneme-dir-fcount" }, String(memories.length))
|
|
1169
|
+
),
|
|
1170
|
+
open && h("div", { className: "mneme-dir-fbody" },
|
|
1171
|
+
rows.length > 0 ? rows : h("div", { className: "mneme-dir-empty" }, emptyLabel || t("memory.directory.empty"))
|
|
1172
|
+
)
|
|
1173
|
+
);
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
if (status === "loading") {
|
|
1177
|
+
return h("div", { className: "mneme-directory" }, h("div", { className: "mneme-dir-empty" }, t("memory.directory.loading")));
|
|
1178
|
+
}
|
|
1179
|
+
const hasAny = dir && (dir.groups.length > 0 || dir.untagged.length > 0);
|
|
1180
|
+
if (status === "error" || !hasAny) {
|
|
1181
|
+
return h("div", { className: "mneme-directory" }, h("div", { className: "mneme-dir-empty" }, t("memory.directory.empty")));
|
|
1182
|
+
}
|
|
1183
|
+
return h("div", { className: "mneme-directory" },
|
|
1184
|
+
dir.groups.map((g) => renderFolder(g.tag, g.memories)),
|
|
1185
|
+
dir.untagged.length > 0 && renderFolder(t("memory.directory.untagged"), dir.untagged, "mneme-untagged")
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
960
1189
|
const EXPLORER_TYPES = ["preference", "project", "decision", "summary", "history"];
|
|
961
1190
|
|
|
962
1191
|
function MemoryExplorer({ t }) {
|
|
963
|
-
const [view, setView] = useState("memory"); // memory | graph | settings
|
|
1192
|
+
const [view, setView] = useState("memory"); // memory | directory | graph | settings
|
|
964
1193
|
const [items, setItems] = useState([]);
|
|
965
1194
|
const [loading, setLoading] = useState(true);
|
|
966
1195
|
const [type, setType] = useState("all");
|
|
@@ -971,9 +1200,20 @@ window.__ModuleLoader__.load({
|
|
|
971
1200
|
const [remoteItems, setRemoteItems] = useState(null);
|
|
972
1201
|
const [selectedId, setSelectedId] = useState(null);
|
|
973
1202
|
const [collapsed, setCollapsed] = useState({});
|
|
1203
|
+
// v0.6.3 directory sub-view: its own folder-collapse state, kept in
|
|
1204
|
+
// MemoryExplorer so switching tabs does not lose it and jumpToMemory's
|
|
1205
|
+
// time-tree reset (setCollapsed({})) cannot clobber it.
|
|
1206
|
+
const [dirCollapsed, setDirCollapsed] = useState({});
|
|
974
1207
|
const [copied, setCopied] = useState(false);
|
|
975
1208
|
const [reloadKey, setReloadKey] = useState(0);
|
|
976
1209
|
const [graphFocus, setGraphFocus] = useState("");
|
|
1210
|
+
// v0.6.2 tag editing state for the detail pane. The authoritative tag set
|
|
1211
|
+
// lives server-side (entity_attrs), so it is fetched per selected memory
|
|
1212
|
+
// and refreshed after every write instead of trusting the list snapshot.
|
|
1213
|
+
const [tagTags, setTagTags] = useState([]);
|
|
1214
|
+
const [tagManual, setTagManual] = useState(true);
|
|
1215
|
+
const [tagInput, setTagInput] = useState("");
|
|
1216
|
+
const [tagAdding, setTagAdding] = useState(false);
|
|
977
1217
|
const itemRefs = useRef(new Map());
|
|
978
1218
|
|
|
979
1219
|
useEffect(() => {
|
|
@@ -994,10 +1234,12 @@ window.__ModuleLoader__.load({
|
|
|
994
1234
|
}, [reloadKey]);
|
|
995
1235
|
|
|
996
1236
|
// Semantic search: server-side ranking replaces the client filter
|
|
997
|
-
// while enabled and a query is present (debounced).
|
|
1237
|
+
// while enabled and a query is present (debounced). tag: queries always
|
|
1238
|
+
// go server-side (searchMemories resolves them without vector support),
|
|
1239
|
+
// so they bypass the semantic toggle and use the same search endpoint.
|
|
998
1240
|
useEffect(() => {
|
|
999
1241
|
const q = query.trim();
|
|
1000
|
-
if (!semantic || !q || q.startsWith("entity:")) { setRemoteItems(null); return; }
|
|
1242
|
+
if ((!semantic && !q.startsWith("tag:")) || !q || q.startsWith("entity:")) { setRemoteItems(null); return; }
|
|
1001
1243
|
let cancelled = false;
|
|
1002
1244
|
const timer = setTimeout(() => {
|
|
1003
1245
|
apiFetch(`/api/dsh-mneme/search?q=${encodeURIComponent(q)}&mode=vector&topK=${searchTopK}`)
|
|
@@ -1013,6 +1255,30 @@ window.__ModuleLoader__.load({
|
|
|
1013
1255
|
itemRefs.current.get(selectedId)?.scrollIntoView({ block: "nearest" });
|
|
1014
1256
|
}, [selectedId]);
|
|
1015
1257
|
|
|
1258
|
+
// Load the authoritative tag set for the selected memory (entity_attrs-
|
|
1259
|
+
// backed) plus the manualTagEnabled gate that hides editing when off.
|
|
1260
|
+
const loadTags = (id) => {
|
|
1261
|
+
if (!id) { setTagTags([]); setTagManual(true); setTagInput(""); return; }
|
|
1262
|
+
apiFetch(`/api/dsh-mneme/memory/tags?id=${encodeURIComponent(id)}`)
|
|
1263
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
1264
|
+
.then((j) => { if (j) { setTagTags(Array.isArray(j.tags) ? j.tags : []); setTagManual(j.manualTagEnabled !== false); } })
|
|
1265
|
+
.catch(() => {});
|
|
1266
|
+
};
|
|
1267
|
+
useEffect(() => {
|
|
1268
|
+
let cancelled = false;
|
|
1269
|
+
if (selectedId) {
|
|
1270
|
+
apiFetch(`/api/dsh-mneme/memory/tags?id=${encodeURIComponent(selectedId)}`)
|
|
1271
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
1272
|
+
.then((j) => { if (!cancelled && j) { setTagTags(Array.isArray(j.tags) ? j.tags : []); setTagManual(j.manualTagEnabled !== false); } })
|
|
1273
|
+
.catch(() => {});
|
|
1274
|
+
} else {
|
|
1275
|
+
setTagTags([]);
|
|
1276
|
+
setTagManual(true);
|
|
1277
|
+
}
|
|
1278
|
+
setTagAdding(false);
|
|
1279
|
+
return () => { cancelled = true; };
|
|
1280
|
+
}, [selectedId]);
|
|
1281
|
+
|
|
1016
1282
|
const q = query.trim().toLowerCase();
|
|
1017
1283
|
// "entity:" is the graph entry grammar: typing it means the user wants
|
|
1018
1284
|
// the entity's neighborhood, not a memory list. Offer the jump instead
|
|
@@ -1072,6 +1338,42 @@ window.__ModuleLoader__.load({
|
|
|
1072
1338
|
);
|
|
1073
1339
|
};
|
|
1074
1340
|
|
|
1341
|
+
// v0.6.2 tag editing: a chip click jumps to a tag:xxx search (reusing
|
|
1342
|
+
// the query-driven search flow), the + entry and per-chip × post the new
|
|
1343
|
+
// tag set through the tags endpoint then re-fetch. A 409 from the server
|
|
1344
|
+
// means manualTagEnabled turned off — flip the gate so editing hides.
|
|
1345
|
+
const submitTag = () => {
|
|
1346
|
+
const tag = tagInput.trim();
|
|
1347
|
+
if (!tag || !selectedId) return;
|
|
1348
|
+
apiFetch("/api/dsh-mneme/memory/tags", {
|
|
1349
|
+
method: "POST",
|
|
1350
|
+
headers: { "Content-Type": "application/json" },
|
|
1351
|
+
body: JSON.stringify({ id: selectedId, tags: [...tagTags, tag] })
|
|
1352
|
+
}).then((res) => {
|
|
1353
|
+
if (res.status === 409) { setTagManual(false); setTagAdding(false); setTagInput(""); return; }
|
|
1354
|
+
return res.json();
|
|
1355
|
+
}).then((j) => {
|
|
1356
|
+
if (j?.ok) { setTagAdding(false); setTagInput(""); loadTags(selectedId); }
|
|
1357
|
+
}).catch(() => {});
|
|
1358
|
+
};
|
|
1359
|
+
const removeTag = (tag) => {
|
|
1360
|
+
if (!selectedId) return;
|
|
1361
|
+
apiFetch("/api/dsh-mneme/memory/tags", {
|
|
1362
|
+
method: "POST",
|
|
1363
|
+
headers: { "Content-Type": "application/json" },
|
|
1364
|
+
body: JSON.stringify({ id: selectedId, tags: tagTags.filter((x) => x !== tag) })
|
|
1365
|
+
}).then((res) => {
|
|
1366
|
+
if (res.status === 409) { setTagManual(false); return; }
|
|
1367
|
+
return res.json();
|
|
1368
|
+
}).then((j) => {
|
|
1369
|
+
if (j?.ok) loadTags(selectedId);
|
|
1370
|
+
}).catch(() => {});
|
|
1371
|
+
};
|
|
1372
|
+
const onTagClick = (tag) => {
|
|
1373
|
+
setType("all");
|
|
1374
|
+
setQuery(`tag:${tag}`);
|
|
1375
|
+
};
|
|
1376
|
+
|
|
1075
1377
|
// Graph → memory jump: land on the browser tab with filters reset so
|
|
1076
1378
|
// the target row is visible, selected and scrolled into view.
|
|
1077
1379
|
const jumpToMemory = (target) => {
|
|
@@ -1083,6 +1385,17 @@ window.__ModuleLoader__.load({
|
|
|
1083
1385
|
setSelectedId(target.id);
|
|
1084
1386
|
};
|
|
1085
1387
|
|
|
1388
|
+
// [[wiki-link]] click in the detail content: resolve the title to a
|
|
1389
|
+
// memory (case-insensitive exact match) then jump to it. Unresolvable
|
|
1390
|
+
// titles silently no-op — the link stays as display text.
|
|
1391
|
+
const onWikilinkClick = (target) => {
|
|
1392
|
+
if (!target) return;
|
|
1393
|
+
apiFetch(`/api/dsh-mneme/wikilinks/resolve?title=${encodeURIComponent(target)}`)
|
|
1394
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
1395
|
+
.then((j) => { if (j?.memory?.id) jumpToMemory(j.memory); })
|
|
1396
|
+
.catch(() => {});
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1086
1399
|
const openGraphFor = (name) => {
|
|
1087
1400
|
setGraphFocus(name || "");
|
|
1088
1401
|
setView("graph");
|
|
@@ -1090,6 +1403,7 @@ window.__ModuleLoader__.load({
|
|
|
1090
1403
|
|
|
1091
1404
|
const subviews = [
|
|
1092
1405
|
{ key: "memory", label: t("memory.explorer.tabMemory") },
|
|
1406
|
+
{ key: "directory", label: t("memory.explorer.tabDirectory") },
|
|
1093
1407
|
{ key: "graph", label: t("memory.explorer.tabGraph") },
|
|
1094
1408
|
{ key: "settings", label: t("memory.explorer.tabSettings") }
|
|
1095
1409
|
];
|
|
@@ -1171,14 +1485,53 @@ window.__ModuleLoader__.load({
|
|
|
1171
1485
|
h("span", { title: formatDate(selected.created_at) }, `${t("memory.explorer.created")}: ${formatDate(selected.created_at)}`),
|
|
1172
1486
|
h("span", { title: formatDate(selected.updated_at) }, `${t("memory.explorer.updated")}: ${formatDate(selected.updated_at)}`)
|
|
1173
1487
|
),
|
|
1174
|
-
|
|
1175
|
-
h("span", null,
|
|
1488
|
+
h("div", { className: "mneme-xdmeta" },
|
|
1489
|
+
h("span", null, t("memory.explorer.tags")),
|
|
1490
|
+
h("div", { className: "mneme-tagrow" },
|
|
1491
|
+
tagTags.length > 0
|
|
1492
|
+
? tagTags.map((tag) =>
|
|
1493
|
+
h("span", {
|
|
1494
|
+
key: tag,
|
|
1495
|
+
className: "mneme-tagchip",
|
|
1496
|
+
title: `tag:${tag}`,
|
|
1497
|
+
onClick: () => onTagClick(tag)
|
|
1498
|
+
},
|
|
1499
|
+
tag,
|
|
1500
|
+
tagManual && h("button", {
|
|
1501
|
+
type: "button",
|
|
1502
|
+
className: "mneme-tagremove",
|
|
1503
|
+
"aria-label": `${t("memory.explorer.tagRemove")}: ${tag}`,
|
|
1504
|
+
onClick: (e) => { e.stopPropagation(); removeTag(tag); }
|
|
1505
|
+
}, "×")
|
|
1506
|
+
)
|
|
1507
|
+
)
|
|
1508
|
+
: h("span", { className: "mneme-tagempty" }, t("memory.explorer.tagsEmpty")),
|
|
1509
|
+
tagManual && (tagAdding
|
|
1510
|
+
? h("input", {
|
|
1511
|
+
key: selectedId,
|
|
1512
|
+
className: "mneme-taginput",
|
|
1513
|
+
autoFocus: true,
|
|
1514
|
+
value: tagInput,
|
|
1515
|
+
placeholder: t("memory.explorer.tagPlaceholder"),
|
|
1516
|
+
onChange: (e) => setTagInput(e.target.value),
|
|
1517
|
+
onKeyDown: (e) => { if (e.key === "Enter") submitTag(); },
|
|
1518
|
+
onBlur: () => { setTagAdding(false); setTagInput(""); }
|
|
1519
|
+
})
|
|
1520
|
+
: h("button", {
|
|
1521
|
+
type: "button",
|
|
1522
|
+
className: "mneme-tagadd",
|
|
1523
|
+
onClick: () => setTagAdding(true)
|
|
1524
|
+
}, `+ ${t("memory.explorer.tagAdd")}`))
|
|
1525
|
+
)
|
|
1526
|
+
),
|
|
1527
|
+
h("div", { className: "mneme-xdcontent" },
|
|
1528
|
+
selected.content ? wikilinkSegments(selected.content, onWikilinkClick) : selected.content
|
|
1176
1529
|
),
|
|
1177
|
-
h("div", { className: "mneme-xdcontent" }, selected.content),
|
|
1178
1530
|
h("div", { className: "mneme-xdactions" },
|
|
1179
1531
|
h("button", { className: "mneme-footbtn", onClick: copyContent },
|
|
1180
1532
|
copied ? t("memory.explorer.copied") : t("memory.explorer.copy"))
|
|
1181
|
-
)
|
|
1533
|
+
),
|
|
1534
|
+
h(BacklinksPanel, { memory: selected, t, onJump: jumpToMemory })
|
|
1182
1535
|
)
|
|
1183
1536
|
)
|
|
1184
1537
|
: h("div", { className: "mneme-xempty" }, t("memory.explorer.emptyDetail"))
|
|
@@ -1229,6 +1582,7 @@ window.__ModuleLoader__.load({
|
|
|
1229
1582
|
)
|
|
1230
1583
|
)
|
|
1231
1584
|
),
|
|
1585
|
+
view === "directory" && h(DirectoryPanel, { t, onJump: jumpToMemory, collapsed: dirCollapsed, setCollapsed: setDirCollapsed }),
|
|
1232
1586
|
view === "graph" && h(GraphPanel, { t, focusEntity: graphFocus, onJumpMemory: jumpToMemory }),
|
|
1233
1587
|
view === "settings" && h("div", { className: "mneme-xsettings" },
|
|
1234
1588
|
h("div", { className: "mneme-xsettings-inner" }, h(SettingsContent, { t }))
|
package/lib/config.js
CHANGED
|
@@ -166,6 +166,33 @@ export const Config = z.object({
|
|
|
166
166
|
// Prefix/semantic search over entity names (used by recall).
|
|
167
167
|
entitySearchEnabled: z.boolean().default(true),
|
|
168
168
|
|
|
169
|
+
// --- wiki-link: explicit cross-memory [[links]] (v0.6.1) ----------------
|
|
170
|
+
// Opt-in, off by default. When enabled, saveWithDedupe/update fire-and-forget
|
|
171
|
+
// a wiki-link resolution pass: [[target]] / [[显示|target]] markers in a
|
|
172
|
+
// memory's content become links_to relations in entity_relations (idempotent,
|
|
173
|
+
// deduped by the unique relation index). The storage layer + read APIs
|
|
174
|
+
// (getBacklinks/getForwardLinks/resolveWikiLink) are always available
|
|
175
|
+
// regardless of this flag.
|
|
176
|
+
wikiLinkEnabled: z.boolean().default(false),
|
|
177
|
+
|
|
178
|
+
// --- tag system (v0.6.2) ---------------------------------------------------
|
|
179
|
+
// Opt-in: when autoTagEnabled is true, a light LLM pass runs after each
|
|
180
|
+
// autoDream consolidation and extracts 1-3 tags per retained memory
|
|
181
|
+
// (autoTagMaxPerRun bounds how many memories are tagged per run). The tag
|
|
182
|
+
// storage layer (store.setMemoryTags/getMemoryTags + tag: search + mirror
|
|
183
|
+
// `#tag` line) is always available regardless of this flag.
|
|
184
|
+
autoTagEnabled: z.boolean().default(false),
|
|
185
|
+
autoTagMaxPerRun: z.natural().min(1).max(100).default(10),
|
|
186
|
+
// Manual tagging (service.setMemoryTags / memory tools) is on by default;
|
|
187
|
+
// set false to disable the manual write path too.
|
|
188
|
+
manualTagEnabled: z.boolean().default(true),
|
|
189
|
+
|
|
190
|
+
// --- tag-weighted re-rank (v0.6.4) -------------------------------------
|
|
191
|
+
// Opt-in: boost candidates whose tags overlap the query/session tags.
|
|
192
|
+
tagBoostEnabled: z.boolean().default(false),
|
|
193
|
+
tagBoostFactor: z.number().min(1).max(2).default(1.15),
|
|
194
|
+
sessionTagBoostFactor: z.number().min(1).max(2).default(1.08),
|
|
195
|
+
|
|
169
196
|
// --- sleep mode: idle-triggered deep maintenance (v0.4.0) ---------------
|
|
170
197
|
// Opt-in, off by default. Unlike autoDream (threshold-triggered, lightweight)
|
|
171
198
|
// sleep fires when the store has been quiet for sleepIdleMinutes and deep-
|