cortico-world-memory 0.1.7 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortico-world-memory",
3
- "version": "0.1.7",
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
@@ -97,8 +97,8 @@ export const MEMORY_CONFIG_GROUP: ConfigGroup = {
97
97
  'worlds.memory.embeddingSidecarUrl': { type: 'string', title: 'Sidecar 地址', description: 'sidecar 模式用,qq_bot memory_server(默认 http://127.0.0.1:8766)。' },
98
98
  'worlds.memory.embeddingTimeoutMs': { type: 'number', title: 'Sidecar 超时(ms)', description: 'qq_bot 向量模型首载慢,默认 3 分钟。' },
99
99
  'worlds.memory.storageBackend': {
100
- type: 'string', title: '存储后端', enum: ['json', 'sqlite'],
101
- description: 'json=原生 JSON 文件(默认);sqlite=单库 KV(自动 better-sqlite3→node:sqlite 回退)。切 sqlite 首次自动迁移旧 JSON。',
100
+ type: 'string', title: '存储后端', enum: ['json', 'sqlite'], 'x-hot': false,
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,8 +1,9 @@
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';
5
5
  import type { MemoryConfig } from './config.ts';
6
+ import { MEMORY_CONFIG_GROUP } from './config.ts';
6
7
  import { MemoryBase } from './memory.ts';
7
8
  import { EmbeddingClient } from './embedding.ts';
8
9
  import { VectorStore } from './vector-store.ts';
@@ -11,7 +12,7 @@ import { KnowledgeStore } from './knowledge-store.ts';
11
12
  import { ReflectionStore } from './reflection-store.ts';
12
13
  import { PersonaStore } from './persona-store.ts';
13
14
  import { ProfileStore } from './profile-store.ts';
14
- import { createBackend, StoreBackend, ALL_STEMS } from './backend.ts';
15
+ import { createBackend, StoreBackend, ALL_STEMS, backfillSqliteToJson } from './backend.ts';
15
16
  import { buildEventText, buildView, promptVars } from './observe.ts';
16
17
 
17
18
  const ENV_PROMPT_FILE = fileURLToPath(new URL('./ENV_PROMPT.md', import.meta.url));
@@ -103,7 +104,7 @@ export class MemoryWorld implements World {
103
104
 
104
105
  console() {
105
106
  return {
106
- config: [], // 旋钮由部署的 worlds/memory.json 提供,此处不重复
107
+ config: [MEMORY_CONFIG_GROUP], // 在「记忆系统」分组下渲染全部旋钮(含存储后端切换)
107
108
  promptDocs: [
108
109
  {
109
110
  key: 'worlds.memory.envPrompt',
@@ -125,8 +126,13 @@ export class MemoryWorld implements World {
125
126
  this.host = host;
126
127
  const ctxData = this.ctx.dataDir;
127
128
  const dataDir = typeof ctxData === 'string' && ctxData ? ctxData : '.';
128
- this.backend = await createBackend(this.cfg.storageBackend ?? 'json', dataDir, (m) => host.log.info(m));
129
- this.backend.migrateFromJson(ALL_STEMS); // 首次切 sqlite 自动迁移旧 JSON
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
+ }
130
136
  this.mem = new MemoryBase(dataDir, this.cfg.maxHistory, this.backend);
131
137
  this.vectors = new VectorStore(dataDir, this.backend);
132
138
  this.facts = new FactStore(dataDir, this.backend);