openclaw-sc 5.38.0

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.
Files changed (48) hide show
  1. package/.env.example +13 -0
  2. package/INSTALL.md +13 -0
  3. package/LICENSE +233 -0
  4. package/README.md +123 -0
  5. package/SECURITY.md +27 -0
  6. package/index.js +3479 -0
  7. package/lib/code-review-shared.js +164 -0
  8. package/lib/config.js +164 -0
  9. package/lib/constants.js +438 -0
  10. package/lib/decomposer.js +896 -0
  11. package/lib/dialog-recall.js +389 -0
  12. package/lib/env.js +59 -0
  13. package/lib/level-rules.js +72 -0
  14. package/lib/log-manager.js +258 -0
  15. package/lib/preemption.js +278 -0
  16. package/lib/priority-calc.js +134 -0
  17. package/lib/prompt-injection.js +748 -0
  18. package/lib/route-evidence.js +701 -0
  19. package/lib/shared-fs.js +244 -0
  20. package/lib/steward-rules.js +648 -0
  21. package/lib/task-center.js +602 -0
  22. package/lib/task-chain.js +713 -0
  23. package/lib/task-checker.js +317 -0
  24. package/lib/task-profiles.js +255 -0
  25. package/lib/tcell.js +411 -0
  26. package/lib/tool-auto-discover.js +522 -0
  27. package/lib/tool-enrich-subagent.js +375 -0
  28. package/lib/tool-handlers.js +178 -0
  29. package/lib/tool-selector.js +459 -0
  30. package/openclaw.plugin.json +55 -0
  31. package/package.json +63 -0
  32. package/security.js +141 -0
  33. package/tools/bridge.js +3288 -0
  34. package/tools/dashboard/tk-dashboard.py +262 -0
  35. package/tools/hippocampus-multi-search.js +1038 -0
  36. package/tools/hippocampus-sqlite.js +738 -0
  37. package/tools/hippocampus-store.js +262 -0
  38. package/tools/mcp-tools.config.json +653 -0
  39. package/tools/pipeline-engine.js +667 -0
  40. package/tools/sidecar/_check_tools.py +34 -0
  41. package/tools/sidecar/sidecar-server.cjs +1360 -0
  42. package/tools/sidecar/subagent-runner.cjs +1471 -0
  43. package/tools/system-tools.js +619 -0
  44. package/vector/README.md +100 -0
  45. package/vector/usearch-bridge.js +639 -0
  46. package/vector/usearch-http.js +74 -0
  47. package/vector/usearch-serve.js +156 -0
  48. package/workers/worker.js +1419 -0
@@ -0,0 +1,738 @@
1
+ /**
2
+ * hippocampus-sqlite.js — hippocampus SQLite 存储引擎
3
+ *
4
+ * 提供基于 SQLite + FTS5 的persist存储,作为 hippocampus-store.js 的增强层。
5
+ *
6
+ * 架构:
7
+ * 主存储: SQLite (WAL模式) — workspace/data/hippocampus.db
8
+ * 双写兼容: 可选的 JSONL 双写(兼容现有记录)
9
+ *
10
+ * 表结构:
11
+ * - events: 工具调用事件
12
+ * - decisions: 推理决策记录
13
+ * - entities: 实体发现表
14
+ * - file_changes: 文件变更追踪
15
+ * - system_logs: 系统日志
16
+ * 每个表对应一个 FTS5 全文搜索虚拟表
17
+ */
18
+
19
+ import Database from 'better-sqlite3';
20
+ import { join, dirname } from 'path';
21
+ import { homedir } from 'os';
22
+ import { existsSync, mkdirSync, statSync } from 'fs';
23
+ import { randomBytes } from 'crypto';
24
+
25
+ // ====== 常量 ======
26
+ const WORKSPACE_ROOT = join(homedir(), '.openclaw', 'workspace');
27
+ const DB_PATH = join(WORKSPACE_ROOT, 'data', 'hippocampus.db');
28
+ const EVENTS_FILE = join(WORKSPACE_ROOT, 'memory', 'hippocampus', 'events.jsonl');
29
+
30
+ const MAX_STRING_LENGTH = 4096; // 单个字段截断长度
31
+
32
+ // ====== 内部状态 ======
33
+ let _db = null;
34
+ let _stmtCache = {}; // 预编译语句缓存
35
+ let _initialized = false;
36
+ let _jsonlWriter = null; // 可选的 JSONL 双写引用
37
+
38
+ // ====== 工具函数 ======
39
+ function generateId(prefix) {
40
+ const ts = Date.now();
41
+ const rand = randomBytes(4).readUInt32BE(0).toString(36);
42
+ return `${prefix}_${ts}_${rand}`;
43
+ }
44
+
45
+ function truncateStr(str, maxLen = MAX_STRING_LENGTH) {
46
+ if (!str) return '';
47
+ const s = typeof str === 'string' ? str : JSON.stringify(str);
48
+ return s.length > maxLen ? s.substring(0, maxLen) + '...' : s;
49
+ }
50
+
51
+ function serialize(v) {
52
+ if (v === null || v === undefined) return null;
53
+ if (typeof v === 'string') return v;
54
+ return JSON.stringify(v);
55
+ }
56
+
57
+ // ====== 重试工具(多进程 SQLITE_BUSY 容错) ======
58
+
59
+ /**
60
+ * 在 SQLITE_BUSY/SQLITE_LOCKED 时自动重试,最多3次,指数退避
61
+ * @param {Function} fn - 需要重试的同步操作
62
+ * @returns {*} fn 的返回值
63
+ */
64
+ function withRetry(fn) {
65
+ const MAX_RETRIES = 3;
66
+ let lastErr;
67
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
68
+ try {
69
+ return fn();
70
+ } catch (err) {
71
+ lastErr = err;
72
+ // 仅 SQLITE_BUSY (errno=5) 或 SQLITE_LOCKED (errno=6) 重试
73
+ const isLockError = (err.code === 'SQLITE_BUSY' || err.code === 'SQLITE_LOCKED' || err.errno === 5 || err.errno === 6);
74
+ if (isLockError && attempt < MAX_RETRIES - 1) {
75
+ const delayMs = Math.min(50 * Math.pow(2, attempt), 200); // 50ms, 100ms, 200ms
76
+ console.warn(`[hippocampus-SQLite] ⚠️ ${err.code || '数据库锁定'}, 第${attempt + 1}次重试 (${delayMs}ms)...`);
77
+ // 同步阻塞等待(better-sqlite3 是同步 API)
78
+ const deadline = Date.now() + delayMs;
79
+ while (Date.now() < deadline) { /* busy wait */ }
80
+ } else {
81
+ throw err; // 非锁定错误或已达最大重试次数,直接抛出
82
+ }
83
+ }
84
+ }
85
+ throw lastErr;
86
+ }
87
+
88
+ // ====== 数据库初始化 ======
89
+
90
+ /**
91
+ * 初始化 SQLite 数据库,创建所有表和 FTS5 索引
92
+ */
93
+ export function initDB() {
94
+ if (_initialized && _db) return { success: true };
95
+
96
+ try {
97
+ // 确保目录存在
98
+ const dbDir = dirname(DB_PATH);
99
+ if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
100
+
101
+ // 打开数据库
102
+ _db = new Database(DB_PATH);
103
+
104
+ // 启用 WAL 模式 — 读写并发友好
105
+ _db.pragma('journal_mode = WAL');
106
+
107
+ // 启用外键
108
+ _db.pragma('foreign_keys = ON');
109
+
110
+ // 设置 busy_timeout 为 5000ms(better-sqlite3 内置重试)
111
+ _db.pragma('busy_timeout = 5000');
112
+
113
+ // 创建基础表 + FTS5 索引
114
+ _db.exec(`
115
+ -- ====== 事件表 ======
116
+ CREATE TABLE IF NOT EXISTS events (
117
+ eventId TEXT PRIMARY KEY,
118
+ type TEXT NOT NULL DEFAULT 'tool_call',
119
+ timestamp TEXT NOT NULL,
120
+ toolName TEXT,
121
+ params TEXT,
122
+ result TEXT,
123
+ duration INTEGER DEFAULT 0,
124
+ status TEXT DEFAULT 'success',
125
+ sessionId TEXT DEFAULT ''
126
+ );
127
+
128
+ CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
129
+ eventId UNINDEXED,
130
+ toolName,
131
+ params,
132
+ result,
133
+ status,
134
+ content='events',
135
+ content_rowid='rowid'
136
+ );
137
+
138
+ -- 触发器:保持 FTS 与基础表同步
139
+ CREATE TRIGGER IF NOT EXISTS events_ai AFTER INSERT ON events BEGIN
140
+ INSERT INTO events_fts(rowid, eventId, toolName, params, result, status)
141
+ VALUES (new.rowid, new.eventId, new.toolName, new.params, new.result, new.status);
142
+ END;
143
+
144
+ CREATE TRIGGER IF NOT EXISTS events_ad AFTER DELETE ON events BEGIN
145
+ INSERT INTO events_fts(events_fts, rowid, eventId, toolName, params, result, status)
146
+ VALUES ('delete', old.rowid, old.eventId, old.toolName, old.params, old.result, old.status);
147
+ END;
148
+
149
+ CREATE TRIGGER IF NOT EXISTS events_au AFTER UPDATE ON events BEGIN
150
+ INSERT INTO events_fts(events_fts, rowid, eventId, toolName, params, result, status)
151
+ VALUES ('delete', old.rowid, old.eventId, old.toolName, old.params, old.result, old.status);
152
+ INSERT INTO events_fts(rowid, eventId, toolName, params, result, status)
153
+ VALUES (new.rowid, new.eventId, new.toolName, new.params, new.result, new.status);
154
+ END;
155
+
156
+ -- ====== 决策表 ======
157
+ CREATE TABLE IF NOT EXISTS decisions (
158
+ decisionId TEXT PRIMARY KEY,
159
+ timestamp TEXT NOT NULL,
160
+ context TEXT,
161
+ decision TEXT,
162
+ reasoning TEXT,
163
+ outcome TEXT
164
+ );
165
+
166
+ CREATE VIRTUAL TABLE IF NOT EXISTS decisions_fts USING fts5(
167
+ decisionId UNINDEXED,
168
+ context,
169
+ decision,
170
+ reasoning,
171
+ outcome,
172
+ content='decisions',
173
+ content_rowid='rowid'
174
+ );
175
+
176
+ CREATE TRIGGER IF NOT EXISTS decisions_ai AFTER INSERT ON decisions BEGIN
177
+ INSERT INTO decisions_fts(rowid, decisionId, context, decision, reasoning, outcome)
178
+ VALUES (new.rowid, new.decisionId, new.context, new.decision, new.reasoning, new.outcome);
179
+ END;
180
+
181
+ CREATE TRIGGER IF NOT EXISTS decisions_ad AFTER DELETE ON decisions BEGIN
182
+ INSERT INTO decisions_fts(decisions_fts, rowid, decisionId, context, decision, reasoning, outcome)
183
+ VALUES ('delete', old.rowid, old.decisionId, old.context, old.decision, old.reasoning, old.outcome);
184
+ END;
185
+
186
+ CREATE TRIGGER IF NOT EXISTS decisions_au AFTER UPDATE ON decisions BEGIN
187
+ INSERT INTO decisions_fts(decisions_fts, rowid, decisionId, context, decision, reasoning, outcome)
188
+ VALUES ('delete', old.rowid, old.decisionId, old.context, old.decision, old.reasoning, old.outcome);
189
+ INSERT INTO decisions_fts(rowid, decisionId, context, decision, reasoning, outcome)
190
+ VALUES (new.rowid, new.decisionId, new.context, new.decision, new.reasoning, new.outcome);
191
+ END;
192
+
193
+ -- ====== 实体表 ======
194
+ CREATE TABLE IF NOT EXISTS entities (
195
+ entityId TEXT PRIMARY KEY,
196
+ name TEXT NOT NULL,
197
+ type TEXT DEFAULT 'unknown',
198
+ properties TEXT,
199
+ timestamp TEXT NOT NULL
200
+ );
201
+
202
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
203
+ entityId UNINDEXED,
204
+ name,
205
+ type,
206
+ properties,
207
+ content='entities',
208
+ content_rowid='rowid'
209
+ );
210
+
211
+ CREATE TRIGGER IF NOT EXISTS entities_ai AFTER INSERT ON entities BEGIN
212
+ INSERT INTO entities_fts(rowid, entityId, name, type, properties)
213
+ VALUES (new.rowid, new.entityId, new.name, new.type, new.properties);
214
+ END;
215
+
216
+ CREATE TRIGGER IF NOT EXISTS entities_ad AFTER DELETE ON entities BEGIN
217
+ INSERT INTO entities_fts(entities_fts, rowid, entityId, name, type, properties)
218
+ VALUES ('delete', old.rowid, old.entityId, old.name, old.type, old.properties);
219
+ END;
220
+
221
+ CREATE TRIGGER IF NOT EXISTS entities_au AFTER UPDATE ON entities BEGIN
222
+ INSERT INTO entities_fts(entities_fts, rowid, entityId, name, type, properties)
223
+ VALUES ('delete', old.rowid, old.entityId, old.name, old.type, old.properties);
224
+ INSERT INTO entities_fts(rowid, entityId, name, type, properties)
225
+ VALUES (new.rowid, new.entityId, new.name, new.type, new.properties);
226
+ END;
227
+
228
+ -- ====== 文件变更表 ======
229
+ CREATE TABLE IF NOT EXISTS file_changes (
230
+ changeId TEXT PRIMARY KEY,
231
+ timestamp TEXT NOT NULL,
232
+ filePath TEXT NOT NULL,
233
+ action TEXT NOT NULL,
234
+ content TEXT,
235
+ previousHash TEXT
236
+ );
237
+
238
+ CREATE VIRTUAL TABLE IF NOT EXISTS file_changes_fts USING fts5(
239
+ changeId UNINDEXED,
240
+ filePath,
241
+ action,
242
+ content,
243
+ previousHash,
244
+ content='file_changes',
245
+ content_rowid='rowid'
246
+ );
247
+
248
+ CREATE TRIGGER IF NOT EXISTS file_changes_ai AFTER INSERT ON file_changes BEGIN
249
+ INSERT INTO file_changes_fts(rowid, changeId, filePath, action, content, previousHash)
250
+ VALUES (new.rowid, new.changeId, new.filePath, new.action, new.content, new.previousHash);
251
+ END;
252
+
253
+ CREATE TRIGGER IF NOT EXISTS file_changes_ad AFTER DELETE ON file_changes BEGIN
254
+ INSERT INTO file_changes_fts(file_changes_fts, rowid, changeId, filePath, action, content, previousHash)
255
+ VALUES ('delete', old.rowid, old.changeId, old.filePath, old.action, old.content, old.previousHash);
256
+ END;
257
+
258
+ CREATE TRIGGER IF NOT EXISTS file_changes_au AFTER UPDATE ON file_changes BEGIN
259
+ INSERT INTO file_changes_fts(file_changes_fts, rowid, changeId, filePath, action, content, previousHash)
260
+ VALUES ('delete', old.rowid, old.changeId, old.filePath, old.action, old.content, old.previousHash);
261
+ INSERT INTO file_changes_fts(rowid, changeId, filePath, action, content, previousHash)
262
+ VALUES (new.rowid, new.changeId, new.filePath, new.action, new.content, new.previousHash);
263
+ END;
264
+
265
+ -- ====== 系统日志表 ======
266
+ CREATE TABLE IF NOT EXISTS system_logs (
267
+ logId TEXT PRIMARY KEY,
268
+ timestamp TEXT NOT NULL,
269
+ level TEXT NOT NULL DEFAULT 'info',
270
+ module TEXT,
271
+ message TEXT,
272
+ metadata TEXT
273
+ );
274
+
275
+ CREATE VIRTUAL TABLE IF NOT EXISTS system_logs_fts USING fts5(
276
+ logId UNINDEXED,
277
+ level,
278
+ module,
279
+ message,
280
+ metadata,
281
+ content='system_logs',
282
+ content_rowid='rowid'
283
+ );
284
+
285
+ CREATE TRIGGER IF NOT EXISTS system_logs_ai AFTER INSERT ON system_logs BEGIN
286
+ INSERT INTO system_logs_fts(rowid, logId, level, module, message, metadata)
287
+ VALUES (new.rowid, new.logId, new.level, new.module, new.message, new.metadata);
288
+ END;
289
+
290
+ CREATE TRIGGER IF NOT EXISTS system_logs_ad AFTER DELETE ON system_logs BEGIN
291
+ INSERT INTO system_logs_fts(system_logs_fts, rowid, logId, level, module, message, metadata)
292
+ VALUES ('delete', old.rowid, old.logId, old.level, old.module, old.message, old.metadata);
293
+ END;
294
+
295
+ CREATE TRIGGER IF NOT EXISTS system_logs_au AFTER UPDATE ON system_logs BEGIN
296
+ INSERT INTO system_logs_fts(system_logs_fts, rowid, logId, level, module, message, metadata)
297
+ VALUES ('delete', old.rowid, old.logId, old.level, old.module, old.message, old.metadata);
298
+ INSERT INTO system_logs_fts(rowid, logId, level, module, message, metadata)
299
+ VALUES (new.rowid, new.logId, new.level, new.module, new.message, new.metadata);
300
+ END;
301
+ `);
302
+
303
+ // 预编译常用语句
304
+ _stmtCache = {
305
+ insertEvent: _db.prepare(`
306
+ INSERT OR REPLACE INTO events (eventId, type, timestamp, toolName, params, result, duration, status, sessionId)
307
+ VALUES (@eventId, @type, @timestamp, @toolName, @params, @result, @duration, @status, @sessionId)
308
+ `),
309
+ insertDecision: _db.prepare(`
310
+ INSERT OR REPLACE INTO decisions (decisionId, timestamp, context, decision, reasoning, outcome)
311
+ VALUES (@decisionId, @timestamp, @context, @decision, @reasoning, @outcome)
312
+ `),
313
+ insertEntity: _db.prepare(`
314
+ INSERT OR REPLACE INTO entities (entityId, name, type, properties, timestamp)
315
+ VALUES (@entityId, @name, @type, @properties, @timestamp)
316
+ `),
317
+ insertFileChange: _db.prepare(`
318
+ INSERT OR REPLACE INTO file_changes (changeId, timestamp, filePath, action, content, previousHash)
319
+ VALUES (@changeId, @timestamp, @filePath, @action, @content, @previousHash)
320
+ `),
321
+ insertSystemLog: _db.prepare(`
322
+ INSERT OR REPLACE INTO system_logs (logId, timestamp, level, module, message, metadata)
323
+ VALUES (@logId, @timestamp, @level, @module, @message, @metadata)
324
+ `),
325
+ // 按时间查询事件
326
+ queryEventsByTime: _db.prepare(`
327
+ SELECT * FROM events
328
+ WHERE timestamp >= @since AND timestamp <= @until
329
+ ORDER BY timestamp DESC
330
+ LIMIT @limit
331
+ `),
332
+ // 按工具名查询事件
333
+ queryEventsByTool: _db.prepare(`
334
+ SELECT * FROM events
335
+ WHERE toolName = @toolName
336
+ ORDER BY timestamp DESC
337
+ LIMIT @limit
338
+ `),
339
+ };
340
+
341
+ _initialized = true;
342
+ // TODO: 移除调试日志 console.log(`[hippocampus-SQLite] ✅ 存储引擎已初始化: ${DB_PATH}`);
343
+ return { success: true, dbPath: DB_PATH };
344
+ } catch (err) {
345
+ console.error('[hippocampus-SQLite] ❌ init failed:', err.message);
346
+ _initialized = false;
347
+ _db = null;
348
+ return { success: false, error: err.message };
349
+ }
350
+ }
351
+
352
+ // ====== 设置 JSONL 双写兼容 ======
353
+
354
+ /**
355
+ * 启用 JSONL 双写(向后兼容 hippocampus-store.js)
356
+ * 传入已有的 recordEvent 函数引用
357
+ */
358
+ export function enableJsonlDualWrite(recordEventFn) {
359
+ _jsonlWriter = recordEventFn;
360
+ // TODO: 移除调试日志 console.log('[hippocampus-SQLite] 🔗 JSONL 双写已启用');
361
+ }
362
+
363
+ // ====== 事件操作 ======
364
+
365
+ /**
366
+ * 插入一工具调用事件(同时支持双写到 JSONL)
367
+ */
368
+ export function insertEvent({ toolName, params, result, duration, status, sessionId, type = 'tool_call' }) {
369
+ if (!_initialized || !_db) {
370
+ const initResult = initDB();
371
+ if (!initResult.success) return { success: false, error: '数据库未初始化', dbError: initResult.error };
372
+ }
373
+
374
+ try {
375
+ const eventId = generateId('hippo');
376
+ const timestamp = new Date().toISOString();
377
+
378
+ withRetry(() => {
379
+ _stmtCache.insertEvent.run({
380
+ eventId,
381
+ type,
382
+ timestamp,
383
+ toolName: truncateStr(toolName),
384
+ params: serialize(params),
385
+ result: serialize(result),
386
+ duration: typeof duration === 'number' ? Math.round(duration) : 0,
387
+ status: status || (result && !result.error ? 'success' : 'error'),
388
+ sessionId: truncateStr(sessionId || ''),
389
+ });
390
+ });
391
+
392
+ // JSONL 双写(如果启用了)
393
+ if (_jsonlWriter && typeof _jsonlWriter === 'function') {
394
+ _jsonlWriter({ toolName, params, result, duration, status, sessionId })
395
+ .catch(err => console.warn('[hippocampus-SQLite] ⚠️ JSONL dual write failed:', err.message));
396
+ }
397
+
398
+ return { success: true, eventId };
399
+ } catch (err) {
400
+ console.error('[hippocampus-SQLite] ❌ insertEvent 失败:', err.message);
401
+ return { success: false, error: err.message };
402
+ }
403
+ }
404
+
405
+ /**
406
+ * 查询事件 — 支持多种过滤方式
407
+ *
408
+ * @param {Object} opts
409
+ * @param {string} [opts.toolName] - 按工具名过滤
410
+ * @param {string} [opts.since] - 起始时间 (ISO8601)
411
+ * @param {string} [opts.until] - 结束时间 (ISO8601)
412
+ * @param {number} [opts.limit=50] - 最大返回数
413
+ * @param {string} [opts.status] - 按状态过滤 success/error
414
+ * @returns {Array}
415
+ */
416
+ export function queryEvents({ toolName, since, until, limit = 50, status } = {}) {
417
+ if (!_initialized || !_db) {
418
+ const initResult = initDB();
419
+ if (!initResult.success) return [];
420
+ }
421
+
422
+ try {
423
+ let sql = 'SELECT * FROM events WHERE 1=1';
424
+ const params = {};
425
+
426
+ if (toolName) {
427
+ sql += ' AND toolName = @toolName';
428
+ params.toolName = toolName;
429
+ }
430
+ if (since) {
431
+ sql += ' AND timestamp >= @since';
432
+ params.since = since;
433
+ }
434
+ if (until) {
435
+ sql += ' AND timestamp <= @until';
436
+ params.until = until;
437
+ }
438
+ if (status) {
439
+ sql += ' AND status = @status';
440
+ params.status = status;
441
+ }
442
+
443
+ sql += ' ORDER BY timestamp DESC LIMIT @limit';
444
+ params.limit = Math.min(limit, 1000);
445
+
446
+ const stmt = _db.prepare(sql);
447
+ const rows = stmt.all(params);
448
+
449
+ // 反序列化 JSON 字段
450
+ return rows.map(r => ({
451
+ ...r,
452
+ params: r.params ? tryParse(r.params) : null,
453
+ result: r.result ? tryParse(r.result) : null,
454
+ }));
455
+ } catch (err) {
456
+ console.error('[hippocampus-SQLite] ❌ queryEvents 失败:', err.message);
457
+ return [];
458
+ }
459
+ }
460
+
461
+ // ====== 全文搜索 ======
462
+
463
+ /**
464
+ * 跨表全文搜索(FTS5)
465
+ *
466
+ * @param {string} query - 搜索关键词(FTS5 查询语法)
467
+ * @param {Object} [opts]
468
+ * @param {string} [opts.table] - 限定搜索的表: events|decisions|entities|file_changes|system_logs
469
+ * @param {number} [opts.limit=20] - 最大返回数
470
+ * @param {string} [opts.since] - 起始时间过滤
471
+ * @param {string} [opts.until] - 结束时间过滤
472
+ * @returns {Array<{table, rowid, rank, snippet, row: Object}>}
473
+ */
474
+ export function searchFTS(query, { table, limit = 20, since, until } = {}) {
475
+ if (!_initialized || !_db) {
476
+ const initResult = initDB();
477
+ if (!initResult.success) return [];
478
+ }
479
+
480
+ if (!query || !query.trim()) return [];
481
+
482
+ const results = [];
483
+ const tables = table
484
+ ? [{ name: table, pk: `${table.slice(0, -1)}Id`, tsCol: 'timestamp' }]
485
+ : [
486
+ { name: 'events', pk: 'eventId', tsCol: 'timestamp' },
487
+ { name: 'decisions', pk: 'decisionId', tsCol: 'timestamp' },
488
+ { name: 'entities', pk: 'entityId', tsCol: 'timestamp' },
489
+ { name: 'file_changes', pk: 'changeId', tsCol: 'timestamp' },
490
+ { name: 'system_logs', pk: 'logId', tsCol: 'timestamp' },
491
+ ];
492
+
493
+ for (const t of tables) {
494
+ try {
495
+ const ftsTable = `${t.name}_fts`;
496
+ const sanitizedQuery = query.replace(/['\"]/g, '').trim();
497
+
498
+ let sql;
499
+ const ftsParams = { query: sanitizedQuery, limit: Math.min(limit, 200) };
500
+
501
+ // 先查 FTS5 获取 rowid 和 rank
502
+ sql = `
503
+ SELECT rowid, rank
504
+ FROM ${ftsTable}
505
+ WHERE ${ftsTable} MATCH @query
506
+ ORDER BY rank
507
+ LIMIT @limit
508
+ `;
509
+
510
+ const ftsRows = _db.prepare(sql).all(ftsParams);
511
+
512
+ if (ftsRows.length === 0) continue;
513
+
514
+ // 再取原始数据
515
+ const rowids = ftsRows.map(r => r.rowid);
516
+ // 使用命名参数 @id0,@id1,... 避免混合 ? 和 @named 参数(better-sqlite3 不支持混合风格)
517
+ const idParams = {};
518
+ const placeholders = rowids.map((id, i) => {
519
+ idParams[`id${i}`] = id;
520
+ return `@id${i}`;
521
+ }).join(',');
522
+
523
+ // 合并所有参数为单一对象
524
+ const allParams = { ...idParams };
525
+
526
+ // 时间范围过滤(统一用命名参数)
527
+ let dataSql = `SELECT rowid, * FROM ${t.name} WHERE rowid IN (${placeholders})`;
528
+ if (since) { dataSql += ` AND ${t.tsCol} >= @since`; allParams.since = since; }
529
+ if (until) { dataSql += ` AND ${t.tsCol} <= @until`; allParams.until = until; }
530
+
531
+ const dataStmt = _db.prepare(dataSql);
532
+ const dataRows = dataStmt.all(allParams);
533
+
534
+ // 合并结果
535
+ for (const ftsRow of ftsRows) {
536
+ const dataRow = dataRows.find(dr => dr.rowid === ftsRow.rowid);
537
+ if (dataRow) {
538
+ results.push({
539
+ table: t.name,
540
+ rowid: ftsRow.rowid,
541
+ rank: ftsRow.rank,
542
+ row: dataRow,
543
+ });
544
+ }
545
+ }
546
+ } catch (err) {
547
+ console.warn(`[hippocampus-SQLite] ⚠️ 搜索 ${t.name} 时出错:`, err.message);
548
+ }
549
+ }
550
+
551
+ // 按 rank 排序(FTS5 相关性分数)
552
+ results.sort((a, b) => a.rank - b.rank);
553
+ return results.slice(0, limit);
554
+ }
555
+
556
+ // ====== 其他表的插入接口 ======
557
+
558
+ export function insertDecision({ context, decision, reasoning, outcome }) {
559
+ if (!_initialized || !_db) {
560
+ const initResult = initDB();
561
+ if (!initResult.success) return { success: false, error: '数据库未初始化' };
562
+ }
563
+
564
+ try {
565
+ const decisionId = generateId('deci');
566
+ const timestamp = new Date().toISOString();
567
+
568
+ withRetry(() => {
569
+ _stmtCache.insertDecision.run({
570
+ decisionId,
571
+ timestamp,
572
+ context: truncateStr(context),
573
+ decision: truncateStr(decision),
574
+ reasoning: truncateStr(reasoning),
575
+ outcome: truncateStr(outcome),
576
+ });
577
+ });
578
+
579
+ return { success: true, decisionId };
580
+ } catch (err) {
581
+ console.error('[hippocampus-SQLite] ❌ insertDecision 失败:', err.message);
582
+ return { success: false, error: err.message };
583
+ }
584
+ }
585
+
586
+ export function insertEntity({ name, type = 'unknown', properties }) {
587
+ if (!_initialized || !_db) {
588
+ const initResult = initDB();
589
+ if (!initResult.success) return { success: false, error: '数据库未初始化' };
590
+ }
591
+
592
+ try {
593
+ const entityId = generateId('enti');
594
+ const timestamp = new Date().toISOString();
595
+
596
+ withRetry(() => {
597
+ _stmtCache.insertEntity.run({
598
+ entityId,
599
+ name: truncateStr(name),
600
+ type: truncateStr(type),
601
+ properties: serialize(properties),
602
+ timestamp,
603
+ });
604
+ });
605
+
606
+ return { success: true, entityId };
607
+ } catch (err) {
608
+ console.error('[hippocampus-SQLite] ❌ insertEntity 失败:', err.message);
609
+ return { success: false, error: err.message };
610
+ }
611
+ }
612
+
613
+ export function insertFileChange({ filePath, action, content, previousHash }) {
614
+ if (!_initialized || !_db) {
615
+ const initResult = initDB();
616
+ if (!initResult.success) return { success: false, error: '数据库未初始化' };
617
+ }
618
+
619
+ try {
620
+ const changeId = generateId('file');
621
+ const timestamp = new Date().toISOString();
622
+
623
+ withRetry(() => {
624
+ _stmtCache.insertFileChange.run({
625
+ changeId,
626
+ timestamp,
627
+ filePath: truncateStr(filePath),
628
+ action: truncateStr(action),
629
+ content: truncateStr(content, 8192),
630
+ previousHash: truncateStr(previousHash),
631
+ });
632
+ });
633
+
634
+ return { success: true, changeId };
635
+ } catch (err) {
636
+ console.error('[hippocampus-SQLite] ❌ insertFileChange 失败:', err.message);
637
+ return { success: false, error: err.message };
638
+ }
639
+ }
640
+
641
+ export function insertSystemLog({ level = 'info', module, message, metadata }) {
642
+ if (!_initialized || !_db) {
643
+ const initResult = initDB();
644
+ if (!initResult.success) return { success: false, error: '数据库未初始化' };
645
+ }
646
+
647
+ try {
648
+ const logId = generateId('log');
649
+ const timestamp = new Date().toISOString();
650
+
651
+ withRetry(() => {
652
+ _stmtCache.insertSystemLog.run({
653
+ logId,
654
+ timestamp,
655
+ level: truncateStr(level),
656
+ module: truncateStr(module),
657
+ message: truncateStr(message),
658
+ metadata: serialize(metadata),
659
+ });
660
+ });
661
+
662
+ return { success: true, logId };
663
+ } catch (err) {
664
+ console.error('[hippocampus-SQLite] ❌ insertSystemLog 失败:', err.message);
665
+ return { success: false, error: err.message };
666
+ }
667
+ }
668
+
669
+ // ====== 数据库stats ======
670
+
671
+ /**
672
+ * 获取各表记录数stats
673
+ */
674
+ export function getStats() {
675
+ if (!_initialized || !_db) {
676
+ return { success: false, error: '数据库未初始化' };
677
+ }
678
+
679
+ try {
680
+ const tables = ['events', 'decisions', 'entities', 'file_changes', 'system_logs'];
681
+ const stats = {};
682
+
683
+ for (const table of tables) {
684
+ const row = _db.prepare(`SELECT COUNT(*) as count FROM ${table}`).get();
685
+ stats[table] = row.count;
686
+ }
687
+
688
+ // 数据库文件大小
689
+ stats.dbPath = DB_PATH;
690
+ stats.dbSizeBytes = existsSync(DB_PATH) ? statSync(DB_PATH).size : 0;
691
+
692
+ return { success: true, ...stats };
693
+ } catch (err) {
694
+ return { success: false, error: err.message };
695
+ }
696
+ }
697
+
698
+ // ====== 关闭 ======
699
+
700
+ /**
701
+ * 关闭数据库连接
702
+ */
703
+ export function close() {
704
+ if (_db) {
705
+ try {
706
+ _db.close();
707
+ } catch (err) {
708
+ console.warn('[hippocampus-SQLite] ⚠️ 关闭连接时出错:', err.message);
709
+ }
710
+ _db = null;
711
+ _initialized = false;
712
+ _stmtCache = {};
713
+ }
714
+ }
715
+
716
+ // ====== 内部工具 ======
717
+
718
+ function tryParse(str) {
719
+ if (!str || str === 'null' || str === 'undefined') return null;
720
+ try { return JSON.parse(str); } catch { return str; }
721
+ }
722
+
723
+ // ====== 自动初始化(模块加载时) ======
724
+ initDB();
725
+
726
+ export default {
727
+ initDB,
728
+ insertEvent,
729
+ insertDecision,
730
+ insertEntity,
731
+ insertFileChange,
732
+ insertSystemLog,
733
+ queryEvents,
734
+ searchFTS,
735
+ enableJsonlDualWrite,
736
+ getStats,
737
+ close,
738
+ };