@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/README.md
CHANGED
|
@@ -112,7 +112,21 @@ dsh web
|
|
|
112
112
|
- **幂等 + 熔断**:dispose/restore 状态守卫幂等(重复调用 no-op);事件回调内部异常 catch 住,不抛进 DSH 会话清理流程
|
|
113
113
|
- **恢复**:整会话 `service.restoreBySession(sessionId)` 一键还原;`service.listBySession(sessionId, { includeDisposed: true })` 可查看当前隐藏了哪些(DTO 带 `disposed` 标记)
|
|
114
114
|
|
|
115
|
+
### Wiki-Link 双向链接 🔗(v0.6.1,opt-in)
|
|
115
116
|
|
|
117
|
+
**默认关闭**(`wikiLinkEnabled: false`)。开启后,记忆正文支持 `[[target]]` / `[[显示|target]]` 双括号链接语法:保存时自动解析并记录跨记忆 `links_to` 关系(partial 唯一索引只对 links_to 去重),记忆详情旁出现反向链接面板,点击可跳转来源记忆。提供只读 API:backlinks / forward-links / wikilink-resolve(输出脱敏)。
|
|
118
|
+
|
|
119
|
+
### Tag 系统 🏷️(v0.6.2,opt-in)
|
|
120
|
+
|
|
121
|
+
`autoTagEnabled` 默认关。开启后:正文 `#标签` 即时标记(规则 `[a-zA-Z0-9_一-龥-]+`,≤20 字符,非法/超长自动丢弃);autoDream 整理后 LLM 自动打 1-3 个标签(`autoTagMaxPerRun=10` 频控,fail-safe);tags 存 `entity_attrs`;`tag:` 搜索前缀可与关键词/`entity:`/`attr:` 组合;mirror 顶部渲染 `#tag` 行;记忆面板标签可点击(过滤)、添加、移除(`manualTagEnabled` 默认开)。存储与搜索能力始终可用,`autoTagEnabled` 只控制自动打标。
|
|
122
|
+
|
|
123
|
+
### 目录视图 📁(v0.6.3)
|
|
124
|
+
|
|
125
|
+
记忆面板新增「目录」视图:以 Tag 为轴心的一级手风琴文件夹,无标签记忆自动沉底到「无标签」兜底组;组内按重要性/时间双降序;点击条目直达详情页。数据由 `GET /api/dsh-mneme/directory` 提供(只读、排除 disposed/archived/forgotten)。
|
|
126
|
+
|
|
127
|
+
### Tag 加权召回 ⚖️(v0.6.4,opt-in)
|
|
128
|
+
|
|
129
|
+
`tagBoostEnabled` 默认关。开启后,三路召回合并出候选后做 tag 加权重排:候选记忆 tags 与 Query 提取 tags(`#xxx` + 已知 tag 列表)交集 → 分数 `×1.15`;与当前 Session 热记忆 tags 交集 → `×1.08`(可叠加,上限 1.0)。关闭时行为与旧版完全一致,可用 `evaluateRetrieval` 开关对比调优。
|
|
116
130
|
|
|
117
131
|
官方设置面板 → 「记忆库设置」→「记忆」标签:按类型浏览、全文搜索;启用向量搜索后可用「语义」切换做向量召回。
|
|
118
132
|
|
|
@@ -212,6 +226,11 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
212
226
|
|
|
213
227
|
| 版本 | 亮点 |
|
|
214
228
|
|------|------|
|
|
229
|
+
| **v0.6.5** | 整合 v0.6.2-0.6.4:Tag 系统 + 目录视图 + Tag 加权召回(全部 opt-in);709 测试全绿 |
|
|
230
|
+
| **v0.6.4** | Tag 加权召回:query/session tag 交集 boost(`tagBoostEnabled` 默认关) |
|
|
231
|
+
| **v0.6.3** | 目录视图:Tag 文件夹 + 无标签兜底 + 点击跳详情 |
|
|
232
|
+
| **v0.6.2** | Tag 系统:`#标签` + autoDream 自动打标 + `tag:` 搜索 + 面板 chips |
|
|
233
|
+
| **v0.6.1** | Wiki-Link 双向链接(笔记化记忆库第一步):[[笔记]] 跨记忆链接 + 反向链接面板 + links_to partial 唯一索引;654 测试全绿 |
|
|
215
234
|
| **v0.6.0** | 会话生命周期(把对话当存档点):`session_disposed_at` 独立字段软隐藏会话删除的记忆(与 `archived` 正交,可恢复)+ `memory_delete` 支持描述删除 + 事件订阅熔断;阿里云 kimi-k2.7-code 审查 4 项修复;628 测试全绿 |
|
|
216
235
|
| **v0.5.0** | 主区「记忆库」视图(取代侧边栏抽屉)+ 记忆图谱可视化(ego-graph API + 零依赖 SVG 力导向)+ BM25 三路召回融合 + 自适应阈值 + 会话热记忆 + 召回基准评测;593 测试全绿 |
|
|
217
236
|
| **v0.4.2** | autoSummarize 自定义模型:`summarizeProvider`/`summarizeModel` 配置项,可独立指定轻量模型(如 qwen3.6-plus)用于会话摘要,节省主模型 token;473 测试全绿 |
|
|
@@ -239,6 +258,10 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
239
258
|
| **v0.4.7** | ✅ 完成 | schema 迁移幂等化 | 并发打开同一 db 时 `PRAGMA table_info` 检查与 ALTER 非原子,可能重复 `ADD COLUMN` 报 duplicate column name;改用 `addColumn` helper 吞掉竞态(try/catch),12 处迁移统一收口 |
|
|
240
259
|
| **v0.5.0** | ✅ 完成 | 召回融合与记忆可视化 | 主区「记忆库」视图 + 记忆图谱(ego-graph API + 零依赖 SVG 力导向)+ BM25 三路召回融合 + 自适应阈值 + 会话热记忆 + 召回基准;593 测试全绿 |
|
|
241
260
|
| **v0.6.0** | ✅ 完成 | 会话生命周期 | 把对话当存档点:`session_disposed_at` 软隐藏会话删除的记忆(与 `archived` 正交、可恢复)+ `memory_delete` 描述删除 + 事件熔断;628 测试全绿 |
|
|
261
|
+
| **v0.6.1** | ✅ 完成 | Wiki-Link 双向链接 | 笔记化记忆库第一步:`[[target]]` 跨记忆链接 + 反向链接面板 + links_to partial 唯一索引;654 测试全绿 |
|
|
262
|
+
| **v0.6.2** | ✅ 完成 | Tag 系统 | `#标签` 解析 + autoDream 自动打标 + `tag:` 搜索 + 面板 chips + mirror `#tag` 行(全部 opt-in) |
|
|
263
|
+
| **v0.6.3** | ✅ 完成 | 目录视图 | Tag 文件夹手风琴 + 无标签兜底 + 点击跳详情 + `GET /api/dsh-mneme/directory` 端点 |
|
|
264
|
+
| **v0.6.4** | ✅ 完成 | Tag 加权召回 | query/session tag 交集 boost(×1.15 / ×1.08),`tagBoostEnabled` 默认关 |
|
|
242
265
|
| **v0.7.0+** | 🚀 远期 | 自进化记忆 | 兴趣漂移跟踪 + 跨 workspace 记忆共享(等 DSH 支持) |
|
|
243
266
|
|
|
244
267
|
> 新能力一律做成**可开关的功能**(配置启用/关闭),默认保守开启、不破坏现有行为。`failure_memories` 表与 autoDream 决策引擎已为后续反思性成长铺好路。
|
|
@@ -402,7 +425,7 @@ src/
|
|
|
402
425
|
lib/
|
|
403
426
|
├── client.js # Web 面板(手写 ModuleLoader bundle)
|
|
404
427
|
└── *.js # src 的同步分发产物
|
|
405
|
-
test/ #
|
|
428
|
+
test/ # 628 个 node:test 测试(含审计与三轴线压测不变量)
|
|
406
429
|
scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · benchmark-recall.js 召回基准
|
|
407
430
|
```
|
|
408
431
|
|
|
@@ -411,7 +434,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
|
|
|
411
434
|
```bash
|
|
412
435
|
cd dsh-mneme
|
|
413
436
|
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
|
|
414
|
-
npm test # 运行
|
|
437
|
+
npm test # 运行 628 个测试
|
|
415
438
|
npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
|
|
416
439
|
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
|
|
417
440
|
```
|
package/lib/api.js
CHANGED
|
@@ -422,6 +422,107 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
422
422
|
}
|
|
423
423
|
});
|
|
424
424
|
|
|
425
|
+
// --- wiki-link back links (v0.6.1) --------------------------------------
|
|
426
|
+
// Read-only like the graph endpoints, so it stays open when apiToken is set.
|
|
427
|
+
// GET /api/dsh-mneme/wikilinks/backlinks?id=<memoryId> → memories whose
|
|
428
|
+
// content carries a [[wiki-link]] resolving to the given memory.
|
|
429
|
+
register({
|
|
430
|
+
kind: "exact",
|
|
431
|
+
path: "/api/dsh-mneme/wikilinks/backlinks",
|
|
432
|
+
handler(req, res) {
|
|
433
|
+
try {
|
|
434
|
+
const url = new URL(req.url, "http://localhost");
|
|
435
|
+
const id = (url.searchParams.get("id") ?? "").trim();
|
|
436
|
+
if (!id) {
|
|
437
|
+
sendJson(res, 400, { error: "missing-id" });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const memory = service.getById?.(id) ?? null;
|
|
441
|
+
const backlinks = (service.getBacklinks?.(id) ?? []).map(({ source, relation }) => ({
|
|
442
|
+
id: source.id,
|
|
443
|
+
title: source.title,
|
|
444
|
+
type: source.type,
|
|
445
|
+
created_at: relation.created_at
|
|
446
|
+
}));
|
|
447
|
+
sendJson(res, 200, {
|
|
448
|
+
memoryId: id,
|
|
449
|
+
memory: memory ? { id: memory.id, title: memory.title, type: memory.type } : null,
|
|
450
|
+
backlinks
|
|
451
|
+
});
|
|
452
|
+
} catch {
|
|
453
|
+
sendJson(res, 500, { error: "internal" });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// --- wiki-link forward links (v0.6.1) -----------------------------------
|
|
459
|
+
// GET /api/dsh-mneme/wikilinks/forward?id=<memoryId> → memories the given
|
|
460
|
+
// memory explicitly links to. Unresolved target titles surface with id:null.
|
|
461
|
+
register({
|
|
462
|
+
kind: "exact",
|
|
463
|
+
path: "/api/dsh-mneme/wikilinks/forward",
|
|
464
|
+
handler(req, res) {
|
|
465
|
+
try {
|
|
466
|
+
const url = new URL(req.url, "http://localhost");
|
|
467
|
+
const id = (url.searchParams.get("id") ?? "").trim();
|
|
468
|
+
if (!id) {
|
|
469
|
+
sendJson(res, 400, { error: "missing-id" });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const memory = service.getById?.(id) ?? null;
|
|
473
|
+
const links = (service.getForwardLinks?.(id) ?? []).map(({ target, relation }) => ({
|
|
474
|
+
id: target?.id ?? null,
|
|
475
|
+
title: target?.title ?? relation.to_entity,
|
|
476
|
+
type: target?.type ?? null,
|
|
477
|
+
created_at: relation.created_at
|
|
478
|
+
}));
|
|
479
|
+
sendJson(res, 200, {
|
|
480
|
+
memoryId: id,
|
|
481
|
+
memory: memory ? { id: memory.id, title: memory.title, type: memory.type } : null,
|
|
482
|
+
links
|
|
483
|
+
});
|
|
484
|
+
} catch {
|
|
485
|
+
sendJson(res, 500, { error: "internal" });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// --- wiki-link resolve (v0.6.1) -----------------------------------------
|
|
491
|
+
// GET /api/dsh-mneme/wikilinks/resolve?title=<title> → case-insensitive exact
|
|
492
|
+
// title match against the memories table (the resolution used when writing
|
|
493
|
+
// links_to relations). 404 when no memory matches.
|
|
494
|
+
register({
|
|
495
|
+
kind: "exact",
|
|
496
|
+
path: "/api/dsh-mneme/wikilinks/resolve",
|
|
497
|
+
handler(req, res) {
|
|
498
|
+
try {
|
|
499
|
+
const url = new URL(req.url, "http://localhost");
|
|
500
|
+
const title = (url.searchParams.get("title") ?? "").trim();
|
|
501
|
+
if (!title) {
|
|
502
|
+
sendJson(res, 400, { error: "missing-title" });
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const memory = service.resolveWikiLink?.(title) ?? null;
|
|
506
|
+
if (!memory) {
|
|
507
|
+
sendJson(res, 404, { error: "memory-not-found", title });
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// Note: no `source` field — it may carry file paths/internal host info
|
|
511
|
+
// and this endpoint is read-only without auth when apiToken is set.
|
|
512
|
+
sendJson(res, 200, {
|
|
513
|
+
title,
|
|
514
|
+
memory: {
|
|
515
|
+
id: memory.id,
|
|
516
|
+
title: memory.title,
|
|
517
|
+
type: memory.type
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
} catch {
|
|
521
|
+
sendJson(res, 500, { error: "internal" });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
425
526
|
// --- health: mirror sync state (F-NEW-03 / v0.3.6) ---
|
|
426
527
|
// Auth-gated; only returns a sanitized error code (never raw last_error which
|
|
427
528
|
// may leak paths/token-like strings/internal hosts). On state read failure it
|
|
@@ -507,8 +608,84 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
507
608
|
}
|
|
508
609
|
});
|
|
509
610
|
|
|
611
|
+
// --- memory tags (v0.6.2) ------------------------------------------------
|
|
612
|
+
// GET /api/dsh-mneme/memory/tags?id=<memoryId> → live entity_attrs-backed
|
|
613
|
+
// tag set for one memory plus the manualTagEnabled gate (so the panel
|
|
614
|
+
// can hide tag editing when the manual path is off). Read-only, stays
|
|
615
|
+
// open when apiToken is set (like list/search/semantic).
|
|
616
|
+
// POST /api/dsh-mneme/memory/tags { id, tags } → overwrite the live tag set
|
|
617
|
+
// via service.setMemoryTags (manualTagEnabled gate); 409 when the gate
|
|
618
|
+
// is closed. Auth-gated like the other write endpoints.
|
|
619
|
+
register({
|
|
620
|
+
kind: "exact",
|
|
621
|
+
path: "/api/dsh-mneme/memory/tags",
|
|
622
|
+
handler(req, res) {
|
|
623
|
+
try {
|
|
624
|
+
if (req.method === "POST" || req.method === "PUT") {
|
|
625
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
626
|
+
return readBody(req).then((text) => {
|
|
627
|
+
const body = parseBody(text);
|
|
628
|
+
const id = typeof body.id === "string" ? body.id.trim() : "";
|
|
629
|
+
if (!id) {
|
|
630
|
+
sendJson(res, 400, { error: "missing-id" });
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const memory = service.getById?.(id) ?? null;
|
|
634
|
+
if (!memory) {
|
|
635
|
+
sendJson(res, 404, { error: "memory-not-found" });
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const result = service.setMemoryTags(id, Array.isArray(body.tags) ? body.tags : []);
|
|
639
|
+
if (result?.ok === false) {
|
|
640
|
+
sendJson(res, 409, { error: result.error || "tags-disabled" });
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
sendJson(res, 200, { ok: true, memoryId: id, tags: result?.tags ?? [] });
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
const url = new URL(req.url, "http://localhost");
|
|
647
|
+
const id = (url.searchParams.get("id") ?? "").trim();
|
|
648
|
+
if (!id) {
|
|
649
|
+
sendJson(res, 400, { error: "missing-id" });
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const memory = service.getById?.(id) ?? null;
|
|
653
|
+
if (!memory) {
|
|
654
|
+
sendJson(res, 404, { error: "memory-not-found" });
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const tags = service.getMemoryTags?.(id) ?? [];
|
|
658
|
+
sendJson(res, 200, {
|
|
659
|
+
memoryId: id,
|
|
660
|
+
tags: Array.isArray(tags) ? tags : [],
|
|
661
|
+
manualTagEnabled: service.manualTagEnabled?.() ?? true
|
|
662
|
+
});
|
|
663
|
+
} catch {
|
|
664
|
+
sendJson(res, 500, { error: "internal" });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// --- directory view (v0.6.3) ---------------------------------------------
|
|
670
|
+
// GET /api/dsh-mneme/directory → memories grouped by tag as
|
|
671
|
+
// { groups: [{ tag, memories: [...] }], untagged: [...] }. Live-only
|
|
672
|
+
// (forgotten/archived/session-disposed excluded), groups tag-sorted, members
|
|
673
|
+
// importance+updated DESC. Read-only, stays open when apiToken is set.
|
|
674
|
+
register({
|
|
675
|
+
kind: "exact",
|
|
676
|
+
path: "/api/dsh-mneme/directory",
|
|
677
|
+
handler(req, res) {
|
|
678
|
+
try {
|
|
679
|
+
const dir = service.getDirectory?.() ?? { groups: [], untagged: [] };
|
|
680
|
+
sendJson(res, 200, dir);
|
|
681
|
+
} catch {
|
|
682
|
+
sendJson(res, 500, { error: "internal" });
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
510
687
|
return {
|
|
511
|
-
routes:
|
|
688
|
+
routes: 19,
|
|
512
689
|
dispose: () => {
|
|
513
690
|
for (const dispose of disposers) dispose();
|
|
514
691
|
}
|