cortico-world-memory 0.1.8 → 0.1.9
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/backend.ts +67 -1
- package/src/config.ts +1 -1
- package/src/world.ts +9 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortico-world-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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/backend.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* SQLite 驱动优先级:先试 `better-sqlite3`(原生,若已安装),缺失则回退 Node 内置
|
|
11
11
|
* `node:sqlite`(v24+ 自带,零安装)。两者 API 兼容(.prepare().get()/.run()、.exec())。
|
|
12
12
|
*/
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
14
14
|
import { dirname, join } from 'node:path';
|
|
15
15
|
|
|
16
16
|
export type BackendKind = 'json' | 'sqlite';
|
|
@@ -164,3 +164,69 @@ export async function createBackend(kind: BackendKind, dataDir: string, log?: Lo
|
|
|
164
164
|
log?.('[memory] 存储后端=json');
|
|
165
165
|
return new JsonBackend(dataDir);
|
|
166
166
|
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 只读打开 memory.db,导出 KV 行为 Map<stem, obj>。供「切回 JSON」时回填用。
|
|
170
|
+
* 优先 better-sqlite3,缺失则回退 Node 内置 node:sqlite(本环境走这条)。
|
|
171
|
+
*/
|
|
172
|
+
export async function readSqliteData(dataDir: string, stems: readonly string[]): Promise<Map<string, unknown>> {
|
|
173
|
+
const dbFile = join(dataDir, 'memory.db');
|
|
174
|
+
if (!existsSync(dbFile)) return new Map();
|
|
175
|
+
let db: any;
|
|
176
|
+
try {
|
|
177
|
+
const mod: any = await import('better-sqlite3');
|
|
178
|
+
const Database = mod.default ?? mod;
|
|
179
|
+
db = new Database(dbFile, { readonly: true });
|
|
180
|
+
} catch {
|
|
181
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
182
|
+
db = new DatabaseSync(dbFile, { readOnly: true });
|
|
183
|
+
}
|
|
184
|
+
const out = new Map<string, unknown>();
|
|
185
|
+
try {
|
|
186
|
+
let rows: any[];
|
|
187
|
+
if (stems.length) {
|
|
188
|
+
const ph = stems.map(() => '?').join(',');
|
|
189
|
+
rows = db.prepare(`SELECT name, data FROM kv WHERE name IN (${ph})`).all(...stems);
|
|
190
|
+
} else {
|
|
191
|
+
rows = db.prepare('SELECT name, data FROM kv').all();
|
|
192
|
+
}
|
|
193
|
+
for (const r of rows) {
|
|
194
|
+
try { out.set(r.name, JSON.parse(r.data)); } catch { /* 跳过损坏行 */ }
|
|
195
|
+
}
|
|
196
|
+
} finally {
|
|
197
|
+
try { db.close(); } catch { /* ignore */ }
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 切回 JSON 时,若 memory.db 存在且其中数据比对应 JSON 文件更新,回填到 JSON 文件,
|
|
204
|
+
* 消除 json↔sqlite 切换的数据不对称(此前 sqlite→json 会丢 SQLite 期间的写入)。
|
|
205
|
+
* 返回回填的 store 数量。仅当 JSON 缺文件或比 DB 更旧时才覆盖,避免冲掉用户在 JSON 上的后续手动编辑。
|
|
206
|
+
*/
|
|
207
|
+
export async function backfillSqliteToJson(dataDir: string, log?: LogFn): Promise<number> {
|
|
208
|
+
const dbFile = join(dataDir, 'memory.db');
|
|
209
|
+
if (!existsSync(dbFile)) return 0;
|
|
210
|
+
let fromSqlite: Map<string, unknown>;
|
|
211
|
+
try {
|
|
212
|
+
fromSqlite = await readSqliteData(dataDir, ALL_STEMS);
|
|
213
|
+
} catch (e: any) {
|
|
214
|
+
log?.(`[memory] 读 SQLite 失败,跳过回填: ${e?.message ?? e}`);
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
if (!fromSqlite.size) return 0;
|
|
218
|
+
const json = new JsonBackend(dataDir);
|
|
219
|
+
let filled = 0;
|
|
220
|
+
const dbMtime = statSync(dbFile).mtimeMs;
|
|
221
|
+
for (const stem of ALL_STEMS) {
|
|
222
|
+
if (!fromSqlite.has(stem)) continue;
|
|
223
|
+
const jsonFile = join(dataDir, stem + '.json');
|
|
224
|
+
const jsonMtime = existsSync(jsonFile) ? statSync(jsonFile).mtimeMs : 0;
|
|
225
|
+
if (jsonMtime === 0 || dbMtime > jsonMtime) {
|
|
226
|
+
json.writeJson(stem, fromSqlite.get(stem));
|
|
227
|
+
filled++;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (filled) log?.(`[memory] 已从 SQLite 回填 ${filled} 个 store 到 JSON(不丢切换期间写入)`);
|
|
231
|
+
return filled;
|
|
232
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -98,7 +98,7 @@ export const MEMORY_CONFIG_GROUP: ConfigGroup = {
|
|
|
98
98
|
'worlds.memory.embeddingTimeoutMs': { type: 'number', title: 'Sidecar 超时(ms)', description: 'qq_bot 向量模型首载慢,默认 3 分钟。' },
|
|
99
99
|
'worlds.memory.storageBackend': {
|
|
100
100
|
type: 'string', title: '存储后端', enum: ['json', 'sqlite'], 'x-hot': false,
|
|
101
|
-
description: 'json=原生 JSON 文件(默认);sqlite=单库 KV(自动 better-sqlite3→node:sqlite 回退)。切 sqlite 首次自动迁移旧 JSON
|
|
101
|
+
description: 'json=原生 JSON 文件(默认);sqlite=单库 KV(自动 better-sqlite3→node:sqlite 回退)。切 sqlite 首次自动迁移旧 JSON;切回 json 若 memory.db 数据更新会自动回填,不丢切换期间写入。改后须点「重启」生效。',
|
|
102
102
|
},
|
|
103
103
|
},
|
|
104
104
|
},
|
package/src/world.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { readFileSync, statSync } 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';
|
|
@@ -12,7 +12,7 @@ import { KnowledgeStore } from './knowledge-store.ts';
|
|
|
12
12
|
import { ReflectionStore } from './reflection-store.ts';
|
|
13
13
|
import { PersonaStore } from './persona-store.ts';
|
|
14
14
|
import { ProfileStore } from './profile-store.ts';
|
|
15
|
-
import { createBackend, StoreBackend, ALL_STEMS } from './backend.ts';
|
|
15
|
+
import { createBackend, StoreBackend, ALL_STEMS, backfillSqliteToJson } from './backend.ts';
|
|
16
16
|
import { buildEventText, buildView, promptVars } from './observe.ts';
|
|
17
17
|
|
|
18
18
|
const ENV_PROMPT_FILE = fileURLToPath(new URL('./ENV_PROMPT.md', import.meta.url));
|
|
@@ -126,8 +126,13 @@ export class MemoryWorld implements World {
|
|
|
126
126
|
this.host = host;
|
|
127
127
|
const ctxData = this.ctx.dataDir;
|
|
128
128
|
const dataDir = typeof ctxData === 'string' && ctxData ? ctxData : '.';
|
|
129
|
-
|
|
130
|
-
this.backend
|
|
129
|
+
const kind = this.cfg.storageBackend ?? 'json';
|
|
130
|
+
this.backend = await createBackend(kind, dataDir, (m) => host.log.info(m));
|
|
131
|
+
if (kind === 'sqlite') {
|
|
132
|
+
this.backend.migrateFromJson(ALL_STEMS); // json→sqlite:首次自动迁移旧 JSON
|
|
133
|
+
} else {
|
|
134
|
+
await backfillSqliteToJson(dataDir, (m) => host.log.info(m)); // sqlite→json:回填较新数据,消除切换丢写
|
|
135
|
+
}
|
|
131
136
|
this.mem = new MemoryBase(dataDir, this.cfg.maxHistory, this.backend);
|
|
132
137
|
this.vectors = new VectorStore(dataDir, this.backend);
|
|
133
138
|
this.facts = new FactStore(dataDir, this.backend);
|