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.
- package/.env.example +13 -0
- package/INSTALL.md +13 -0
- package/LICENSE +233 -0
- package/README.md +123 -0
- package/SECURITY.md +27 -0
- package/index.js +3479 -0
- package/lib/code-review-shared.js +164 -0
- package/lib/config.js +164 -0
- package/lib/constants.js +438 -0
- package/lib/decomposer.js +896 -0
- package/lib/dialog-recall.js +389 -0
- package/lib/env.js +59 -0
- package/lib/level-rules.js +72 -0
- package/lib/log-manager.js +258 -0
- package/lib/preemption.js +278 -0
- package/lib/priority-calc.js +134 -0
- package/lib/prompt-injection.js +748 -0
- package/lib/route-evidence.js +701 -0
- package/lib/shared-fs.js +244 -0
- package/lib/steward-rules.js +648 -0
- package/lib/task-center.js +602 -0
- package/lib/task-chain.js +713 -0
- package/lib/task-checker.js +317 -0
- package/lib/task-profiles.js +255 -0
- package/lib/tcell.js +411 -0
- package/lib/tool-auto-discover.js +522 -0
- package/lib/tool-enrich-subagent.js +375 -0
- package/lib/tool-handlers.js +178 -0
- package/lib/tool-selector.js +459 -0
- package/openclaw.plugin.json +55 -0
- package/package.json +63 -0
- package/security.js +141 -0
- package/tools/bridge.js +3288 -0
- package/tools/dashboard/tk-dashboard.py +262 -0
- package/tools/hippocampus-multi-search.js +1038 -0
- package/tools/hippocampus-sqlite.js +738 -0
- package/tools/hippocampus-store.js +262 -0
- package/tools/mcp-tools.config.json +653 -0
- package/tools/pipeline-engine.js +667 -0
- package/tools/sidecar/_check_tools.py +34 -0
- package/tools/sidecar/sidecar-server.cjs +1360 -0
- package/tools/sidecar/subagent-runner.cjs +1471 -0
- package/tools/system-tools.js +619 -0
- package/vector/README.md +100 -0
- package/vector/usearch-bridge.js +639 -0
- package/vector/usearch-http.js +74 -0
- package/vector/usearch-serve.js +156 -0
- 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
|
+
};
|