cortico-world-memory 0.1.9 → 0.1.10
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/package.json +1 -1
- package/src/world.ts +241 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortico-world-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Cortico World 扩展:五层记忆(事实库/知识库/反思/人格/档案)+ 三级记忆底座 + Hybrid Recall 向量混合检索(移植自 E:/qq_bot 的记忆系统与 memory_server 向量 sidecar,qq_bot 源码零改动)。",
|
|
6
6
|
"license": "MIT",
|
package/src/world.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, statSync } from 'node:fs';
|
|
1
|
+
import { readFileSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import type { World, WorldHost, ToolDef, ToolTag, EventEnvelope } from 'cortico/core/types.ts';
|
|
4
4
|
import type { WorldContext } from 'cortico/world.ts';
|
|
@@ -17,6 +17,119 @@ import { buildEventText, buildView, promptVars } from './observe.ts';
|
|
|
17
17
|
|
|
18
18
|
const ENV_PROMPT_FILE = fileURLToPath(new URL('./ENV_PROMPT.md', import.meta.url));
|
|
19
19
|
|
|
20
|
+
type _Dict = Record<string, any>;
|
|
21
|
+
|
|
22
|
+
/** 仅保留对象中指定 key 的条目(用于按 scope 过滤)。 */
|
|
23
|
+
function pickScope(obj: _Dict, key: string): _Dict {
|
|
24
|
+
if (obj && typeof obj === 'object' && key in obj) return { [key]: obj[key] };
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 把导出 bundle 渲染为易读 Markdown。 */
|
|
29
|
+
function renderMemoryMarkdown(b: _Dict): string {
|
|
30
|
+
const L: string[] = [];
|
|
31
|
+
const profiles = (b.profiles ?? {}) as _Dict;
|
|
32
|
+
const profKeys = Object.keys(profiles);
|
|
33
|
+
const store = (b.memory?.store ?? {}) as _Dict;
|
|
34
|
+
const storeKeys = Object.keys(store);
|
|
35
|
+
const summary = (b.memory?.summary ?? {}) as _Dict;
|
|
36
|
+
const topic = (b.memory?.topic ?? {}) as _Dict;
|
|
37
|
+
const notes = (b.memory?.notes ?? {}) as _Dict;
|
|
38
|
+
const noteKeys = Object.keys(notes);
|
|
39
|
+
const factScopes = (b.facts?.scopes ?? {}) as _Dict;
|
|
40
|
+
const factKeys = Object.keys(factScopes);
|
|
41
|
+
const knowledge = (b.knowledge?.entries ?? []) as any[];
|
|
42
|
+
const reflections = (b.reflection?.reflections ?? []) as any[];
|
|
43
|
+
const rules = (b.reflection?.rules ?? []) as any[];
|
|
44
|
+
const personas = (b.persona?.personas ?? {}) as _Dict;
|
|
45
|
+
const personaKeys = Object.keys(personas);
|
|
46
|
+
const vectors = (b.vectors?.vectors ?? []) as any[];
|
|
47
|
+
|
|
48
|
+
L.push('# 记忆导出');
|
|
49
|
+
L.push('');
|
|
50
|
+
L.push(`- 导出时间:${b.exportedAt ?? ''}`);
|
|
51
|
+
L.push(`- 作用域:${b.scope ?? 'ALL'}`);
|
|
52
|
+
L.push(`- 存储后端:${b.backend ?? 'unknown'}`);
|
|
53
|
+
L.push('');
|
|
54
|
+
|
|
55
|
+
if (profKeys.length) {
|
|
56
|
+
L.push(`## 人物档案 (${profKeys.length})`);
|
|
57
|
+
for (const u of profKeys) {
|
|
58
|
+
L.push(`### ${u}`);
|
|
59
|
+
for (const f of profiles[u]?.facts ?? []) L.push(`- ${f}`);
|
|
60
|
+
}
|
|
61
|
+
L.push('');
|
|
62
|
+
}
|
|
63
|
+
if (storeKeys.length) {
|
|
64
|
+
L.push(`## 短期记忆 (${storeKeys.length} scope)`);
|
|
65
|
+
for (const s of storeKeys) {
|
|
66
|
+
L.push(`### scope: ${s}`);
|
|
67
|
+
for (const it of store[s] ?? []) L.push(`- [${it.role ?? ''}] ${it.content ?? ''}`);
|
|
68
|
+
}
|
|
69
|
+
L.push('');
|
|
70
|
+
}
|
|
71
|
+
const sumKeys = Object.keys(summary);
|
|
72
|
+
if (sumKeys.length || Object.keys(topic).length) {
|
|
73
|
+
L.push('## 总结与主题');
|
|
74
|
+
for (const s of sumKeys) { L.push(`### ${s} 总结`); L.push(summary[s] ?? ''); }
|
|
75
|
+
for (const s of Object.keys(topic)) { L.push(`### ${s} 主题`); L.push(topic[s] ?? ''); }
|
|
76
|
+
L.push('');
|
|
77
|
+
}
|
|
78
|
+
if (noteKeys.length) {
|
|
79
|
+
L.push(`## 笔记 (${noteKeys.length})`);
|
|
80
|
+
for (const s of noteKeys) {
|
|
81
|
+
L.push(`### ${s}`);
|
|
82
|
+
for (const n of notes[s] ?? []) L.push(`- ${n.text ?? ''}`);
|
|
83
|
+
}
|
|
84
|
+
L.push('');
|
|
85
|
+
}
|
|
86
|
+
if (factKeys.length) {
|
|
87
|
+
L.push(`## 事实库 (${factKeys.length} scope)`);
|
|
88
|
+
for (const s of factKeys) {
|
|
89
|
+
L.push(`### ${s}`);
|
|
90
|
+
for (const f of factScopes[s] ?? []) L.push(`- ${f.text ?? ''}`);
|
|
91
|
+
}
|
|
92
|
+
L.push('');
|
|
93
|
+
}
|
|
94
|
+
if (knowledge.length) {
|
|
95
|
+
L.push(`## 知识库 (${knowledge.length})`);
|
|
96
|
+
for (const e of knowledge) {
|
|
97
|
+
L.push(`### ${e.topic ?? ''}`);
|
|
98
|
+
if (e.keywords?.length) L.push(`关键词:${e.keywords.join(', ')}`);
|
|
99
|
+
for (const f of e.facts ?? []) L.push(`- ${f}`);
|
|
100
|
+
}
|
|
101
|
+
L.push('');
|
|
102
|
+
}
|
|
103
|
+
if (reflections.length || rules.length) {
|
|
104
|
+
L.push(`## 反思与交互规则 (${reflections.length}/${rules.length})`);
|
|
105
|
+
for (const r of reflections) L.push(`- [${r.type}] (${r.userId}) ${r.content ?? ''}`);
|
|
106
|
+
if (rules.length) {
|
|
107
|
+
L.push('### 交互规则');
|
|
108
|
+
for (const r of rules) L.push(`- (${r.userId}) ${r.rule ?? ''}`);
|
|
109
|
+
}
|
|
110
|
+
L.push('');
|
|
111
|
+
}
|
|
112
|
+
if (personaKeys.length || b.persona?.globalStyle) {
|
|
113
|
+
L.push(`## 人格记忆 (${personaKeys.length})`);
|
|
114
|
+
if (b.persona?.globalStyle) L.push(`全局风格:${b.persona.globalStyle}`);
|
|
115
|
+
for (const u of personaKeys) {
|
|
116
|
+
L.push(`### ${u}`);
|
|
117
|
+
L.push(`风格:${personas[u]?.style ?? ''}`);
|
|
118
|
+
for (const t of personas[u]?.traits ?? []) L.push(`- ${t}`);
|
|
119
|
+
}
|
|
120
|
+
L.push('');
|
|
121
|
+
}
|
|
122
|
+
if (vectors.length) {
|
|
123
|
+
L.push(`## 向量记忆 (${vectors.length}, 模型:${b.vectors?.model ?? '未知'})`);
|
|
124
|
+
for (const v of vectors) {
|
|
125
|
+
const txt = typeof v.text === 'string' ? v.text : JSON.stringify(v.text);
|
|
126
|
+
L.push(`- [${v.scope}] ${txt.slice(0, 200)}`);
|
|
127
|
+
}
|
|
128
|
+
L.push('');
|
|
129
|
+
}
|
|
130
|
+
return L.join('\n');
|
|
131
|
+
}
|
|
132
|
+
|
|
20
133
|
/** 清理自动捕获的入站文本:去掉 QQ 的「【QQ群 ...】」前缀(对其它 world 无前缀,无副作用),压缩空白。 */
|
|
21
134
|
function cleanCaptureText(t: string): string {
|
|
22
135
|
return (t ?? '')
|
|
@@ -91,6 +204,9 @@ export class MemoryWorld implements World {
|
|
|
91
204
|
envPromptVars(): Record<string, string> | null {
|
|
92
205
|
if (!this.mem || !this.vectors) return null; // 未启动 = 本 World 这段不进前缀
|
|
93
206
|
const view = buildView(this.mem, this.vectors, 'main', this.cfg.notesInPrompt, this.profile);
|
|
207
|
+
// 合并 QQ 笔记本('qq' scope)的要点,让智能体也能常驻记住——qq_note_* 走记忆插件时落在此域。
|
|
208
|
+
const qqNotes = this.mem.getNotes('qq').slice(-this.cfg.notesInPrompt);
|
|
209
|
+
if (qqNotes.length) view.notes = [...view.notes, ...qqNotes].slice(-this.cfg.notesInPrompt);
|
|
94
210
|
return promptVars(view);
|
|
95
211
|
}
|
|
96
212
|
|
|
@@ -161,10 +277,19 @@ export class MemoryWorld implements World {
|
|
|
161
277
|
// 自动捕获:从当前事件尾开始(不回灌历史),周期扫描外部入站消息沉淀进记忆。
|
|
162
278
|
try { this.lastCursor = (host.store?.latestCursor?.() ?? 0) || 0; } catch { this.lastCursor = 0; }
|
|
163
279
|
if (this.cfg.autoCapture) this.startCapture();
|
|
280
|
+
// QQ 插件笔记本桥:启用时让 qq_note_* 直接写入本插件笔记库('qq' scope),复用常驻环境提示词与混合检索。
|
|
281
|
+
(globalThis as Record<string, unknown>)['cortico:memory:notebook'] = {
|
|
282
|
+
kind: 'memory' as const,
|
|
283
|
+
save: (text: string) => this.mem!.addNote('qq', text),
|
|
284
|
+
list: () => this.mem!.getNotes('qq'),
|
|
285
|
+
get: (index: number) => this.mem!.getNotes('qq')[index] ?? null,
|
|
286
|
+
forget: (index: number) => this.mem!.removeNote('qq', index),
|
|
287
|
+
};
|
|
164
288
|
}
|
|
165
289
|
|
|
166
290
|
/** stop 必须 async 返回 Promise(框架 withDeadline 直接 work.then)。 */
|
|
167
291
|
async stop(): Promise<void> {
|
|
292
|
+
delete (globalThis as Record<string, unknown>)['cortico:memory:notebook'];
|
|
168
293
|
if (this.timer) clearInterval(this.timer);
|
|
169
294
|
this.timer = null;
|
|
170
295
|
if (this.captureTimer) { clearInterval(this.captureTimer); this.captureTimer = null; }
|
|
@@ -178,6 +303,103 @@ export class MemoryWorld implements World {
|
|
|
178
303
|
this.backend?.flush(); // 最后统一关闭 SQLite(每个 store 的 flush 只写行,不关库)
|
|
179
304
|
}
|
|
180
305
|
|
|
306
|
+
/**
|
|
307
|
+
* 导出全部记忆为 JSON / Markdown 文件(便于备份、迁移、阅读)。
|
|
308
|
+
* - format: 'json' | 'markdown' | 'both'
|
|
309
|
+
* - scope: 可选,按作用域/用户过滤(短期/事实/向量按 scope;反思/人格/档案按 userId;知识库为全局始终包含)
|
|
310
|
+
* - outDir: 导出目录,默认 <dataDir>/exports
|
|
311
|
+
*/
|
|
312
|
+
private async exportMemory(format: string, scope: string, outDir: string): Promise<{ text: string }> {
|
|
313
|
+
const wantJson = !format || format === 'both' || format === 'json';
|
|
314
|
+
const wantMd = !format || format === 'both' || format === 'markdown';
|
|
315
|
+
const dir = (outDir && outDir.trim()) || path.join(this.dataDir, 'exports');
|
|
316
|
+
mkdirSync(dir, { recursive: true });
|
|
317
|
+
|
|
318
|
+
const read = async (stem: string): Promise<_Dict> => {
|
|
319
|
+
try { return (await this.backend!.readJson(stem)) as _Dict; } catch { return {}; }
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const memRaw = (await read('memory-data')) ?? {};
|
|
323
|
+
const factRaw = (await read('fact-store')) ?? {};
|
|
324
|
+
const vectorRaw = (await read('vector-memory')) ?? {};
|
|
325
|
+
const kbRaw = (await read('knowledge-base')) ?? {};
|
|
326
|
+
const reflRaw = (await read('reflection-data')) ?? {};
|
|
327
|
+
const personaRaw = (await read('persona-data')) ?? {};
|
|
328
|
+
const profileRaw = (await read('user-profiles')) ?? {};
|
|
329
|
+
|
|
330
|
+
let store = (memRaw.store ?? {}) as _Dict;
|
|
331
|
+
let notes = (memRaw.notes ?? {}) as _Dict;
|
|
332
|
+
let summary = (memRaw.summary ?? {}) as _Dict;
|
|
333
|
+
let topic = (memRaw.topic ?? {}) as _Dict;
|
|
334
|
+
let factScopes = (factRaw.scopes ?? {}) as _Dict;
|
|
335
|
+
let vectors = (vectorRaw.vectors ?? []) as any[];
|
|
336
|
+
let reflections = (reflRaw.reflections ?? []) as any[];
|
|
337
|
+
let rules = (reflRaw.interactionRules ?? []) as any[];
|
|
338
|
+
let personas = (personaRaw.personas ?? {}) as _Dict;
|
|
339
|
+
let profiles = (profileRaw.profiles ?? {}) as _Dict;
|
|
340
|
+
const globalStyle = (personaRaw.globalStyle ?? '') as string;
|
|
341
|
+
const knowledge = (kbRaw.entries ?? []) as any[];
|
|
342
|
+
|
|
343
|
+
const filterScope = (scope || '').trim();
|
|
344
|
+
if (filterScope) {
|
|
345
|
+
store = pickScope(store, filterScope);
|
|
346
|
+
notes = pickScope(notes, filterScope);
|
|
347
|
+
summary = pickScope(summary, filterScope);
|
|
348
|
+
topic = pickScope(topic, filterScope);
|
|
349
|
+
factScopes = pickScope(factScopes, filterScope);
|
|
350
|
+
vectors = vectors.filter((v: _Dict) => v.scope === filterScope);
|
|
351
|
+
reflections = reflections.filter((r: _Dict) => String(r.userId) === filterScope);
|
|
352
|
+
rules = rules.filter((r: _Dict) => String(r.userId) === filterScope);
|
|
353
|
+
personas = pickScope(personas, filterScope);
|
|
354
|
+
profiles = pickScope(profiles, filterScope);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const bundle: _Dict = {
|
|
358
|
+
exportedAt: new Date().toISOString(),
|
|
359
|
+
plugin: 'cortico-world-memory',
|
|
360
|
+
scope: filterScope || 'ALL',
|
|
361
|
+
backend: (this.backend as unknown as { constructor?: { name?: string } })?.constructor?.name ?? 'unknown',
|
|
362
|
+
memory: { store, summary, topic, notes },
|
|
363
|
+
facts: { scopes: factScopes },
|
|
364
|
+
vectors: { model: vectorRaw.model_name ?? null, vectors },
|
|
365
|
+
knowledge: { entries: knowledge },
|
|
366
|
+
reflection: { reflections, rules, stats: reflRaw.stats ?? {} },
|
|
367
|
+
persona: { personas, globalStyle },
|
|
368
|
+
profiles,
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19);
|
|
372
|
+
const base = `memory-export-${stamp}`;
|
|
373
|
+
const written: string[] = [];
|
|
374
|
+
|
|
375
|
+
if (wantJson) {
|
|
376
|
+
const p = path.join(dir, `${base}.json`);
|
|
377
|
+
writeFileSync(p, JSON.stringify(bundle, null, 2), 'utf8');
|
|
378
|
+
written.push(p);
|
|
379
|
+
}
|
|
380
|
+
if (wantMd) {
|
|
381
|
+
const p = path.join(dir, `${base}.md`);
|
|
382
|
+
writeFileSync(p, renderMemoryMarkdown(bundle), 'utf8');
|
|
383
|
+
written.push(p);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const factCount = Object.values(factScopes).reduce((s: number, l: any) => s + (Array.isArray(l) ? l.length : 0), 0);
|
|
387
|
+
const counts = [
|
|
388
|
+
`短期记忆 ${Object.keys(store).length} scope`,
|
|
389
|
+
`笔记 ${Object.keys(notes).length}`,
|
|
390
|
+
`事实 ${factCount} 条`,
|
|
391
|
+
`向量 ${vectors.length}`,
|
|
392
|
+
`知识 ${knowledge.length} 条`,
|
|
393
|
+
`反思 ${reflections.length} / 规则 ${rules.length}`,
|
|
394
|
+
`人格 ${Object.keys(personas).length} 用户`,
|
|
395
|
+
`档案 ${Object.keys(profiles).length} 用户`,
|
|
396
|
+
].join(' | ');
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
text: `记忆导出完成(${filterScope ? 'scope=' + filterScope : '全部'})\n${counts}\n文件:\n` + written.map((w) => '- ' + w).join('\n'),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
181
403
|
/** 启动自动捕获轮询(尊重暂停开关)。 */
|
|
182
404
|
private startCapture(): void {
|
|
183
405
|
const ms = Math.max(5000, Number(this.cfg.captureIntervalMs) || 20000);
|
|
@@ -448,6 +670,24 @@ export class MemoryWorld implements World {
|
|
|
448
670
|
}),
|
|
449
671
|
mem_flush: mk('mem_flush', '把记忆立即落盘(平时自动防抖落盘,一般不用调)。', {}, [], ['act'],
|
|
450
672
|
async () => { this.mem?.flush(); this.vectors?.flush(); this.facts?.flush(); this.kb?.flush(); this.reflect?.flush(); this.persona?.flush(); this.profile?.flush(); return 'OK 已落盘'; }),
|
|
673
|
+
mem_export: mk(
|
|
674
|
+
'mem_export',
|
|
675
|
+
'把全部记忆导出为文件(JSON/Markdown),便于备份、迁移、阅读。知识库为全局始终包含;其他可按作用域/用户过滤。',
|
|
676
|
+
{
|
|
677
|
+
format: { type: 'string', optional: true, title: '格式', description: "json | markdown | both(默认 both)。" },
|
|
678
|
+
scope: { type: 'string', optional: true, title: '作用域过滤', description: '可选,按作用域/用户过滤(短期/事实/向量按 scope;反思/人格/档案按 userId)。留空导出全部。' },
|
|
679
|
+
outDir: { type: 'string', optional: true, title: '导出目录', description: '可选,文件保存目录,默认 <数据目录>/exports。' },
|
|
680
|
+
},
|
|
681
|
+
[],
|
|
682
|
+
['act'],
|
|
683
|
+
async (args: _Dict) => {
|
|
684
|
+
const format = typeof args.format === 'string' ? args.format.trim().toLowerCase() : 'both';
|
|
685
|
+
const scope = typeof args.scope === 'string' ? args.scope.trim() : '';
|
|
686
|
+
const outDir = typeof args.outDir === 'string' ? args.outDir.trim() : '';
|
|
687
|
+
if (!['json', 'markdown', 'both'].includes(format)) return { text: '错误:format 只能是 json / markdown / both。' };
|
|
688
|
+
return await this.exportMemory(format, scope, outDir);
|
|
689
|
+
},
|
|
690
|
+
),
|
|
451
691
|
};
|
|
452
692
|
}
|
|
453
693
|
}
|