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,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hippocampus-store.js — hippocampus存储
|
|
3
|
+
*
|
|
4
|
+
* 提供 recordEvent() 接口,自动记录工具调用事件到hippocampus永久存储。
|
|
5
|
+
*
|
|
6
|
+
* 存储架构:
|
|
7
|
+
* 主存储: ~/.openclaw/workspace/memory/hippocampus/events.jsonl
|
|
8
|
+
* 次级存储(高水位时自动启用归档)
|
|
9
|
+
*
|
|
10
|
+
* 每记录的 schema:
|
|
11
|
+
* {
|
|
12
|
+
* eventId: "hippo_<timestamp>_<random>",
|
|
13
|
+
* type: "tool_call",
|
|
14
|
+
* timestamp: "ISO8601",
|
|
15
|
+
* toolName: "cpu_xxx",
|
|
16
|
+
* params: { ... }, // 参数(自动截断)
|
|
17
|
+
* result: { ... }, // 返回结果(自动截断)
|
|
18
|
+
* duration: 123, // 耗时(ms)
|
|
19
|
+
* status: "success" | "error",
|
|
20
|
+
* sessionId: "xxx",
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* v1.0.3 — 2026-06-10
|
|
24
|
+
* - 修复空catch块加warn日志
|
|
25
|
+
* - queryRecentEvents JSON.parse 加 try-catch 防崩溃
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { appendFile, mkdir, writeFile, readFile, access } from 'fs/promises';
|
|
29
|
+
import { join, dirname } from 'path';
|
|
30
|
+
import { homedir } from 'os';
|
|
31
|
+
import { randomBytes } from 'crypto';
|
|
32
|
+
import { existsSync } from 'fs';
|
|
33
|
+
|
|
34
|
+
// ====== 常量 ======
|
|
35
|
+
const WORKSPACE_ROOT = join(homedir(), '.openclaw', 'workspace');
|
|
36
|
+
const EVENTS_FILE = join(WORKSPACE_ROOT, 'memory', 'hippocampus', 'events.jsonl');
|
|
37
|
+
const INDEX_FILE = join(WORKSPACE_ROOT, 'memory', 'hippocampus', 'index.json');
|
|
38
|
+
const MAX_PARAMS_LENGTH = 512; // 参数截断长度
|
|
39
|
+
const MAX_RESULT_LENGTH = 1024; // 结果截断长度
|
|
40
|
+
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB 自动归档
|
|
41
|
+
|
|
42
|
+
// ====== 内部状态 ======
|
|
43
|
+
let _initialized = false;
|
|
44
|
+
let _fallbackDir = null;
|
|
45
|
+
|
|
46
|
+
// ====== 路径工具 ======
|
|
47
|
+
function ensureDir(p) {
|
|
48
|
+
return mkdir(dirname(p), { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function generateEventId() {
|
|
52
|
+
const ts = Date.now();
|
|
53
|
+
const rand = randomBytes(4).readUInt32BE(0).toString(36);
|
|
54
|
+
return `hippo_${ts}_${rand}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function truncateObj(obj, maxLen) {
|
|
58
|
+
if (!obj) return obj;
|
|
59
|
+
// 先试试完整序列化是否超长
|
|
60
|
+
const str = JSON.stringify(obj);
|
|
61
|
+
if (str.length <= maxLen) return obj;
|
|
62
|
+
// 超长了:不截断JSON字符串(那样必然破坏结构),
|
|
63
|
+
// 而是在对象结构层面递归截断字符串值,保证输出永远是合法 JSON
|
|
64
|
+
const result = deepTruncate(obj, maxLen);
|
|
65
|
+
const finalStr = JSON.stringify(result);
|
|
66
|
+
// 如果递归截断后仍然超长(罕见),回退到标记截断
|
|
67
|
+
if (finalStr.length > maxLen) {
|
|
68
|
+
return { _truncated: true, _keys: typeof obj === 'object' ? Object.keys(obj).slice(0, 20) : undefined };
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 递归截断对象中的字符串值,保证 JSON 结构完整
|
|
75
|
+
* 每个字符串值按比例分配预算,确保整体不超过 maxLen
|
|
76
|
+
*/
|
|
77
|
+
function deepTruncate(value, budget) {
|
|
78
|
+
if (typeof value === 'string') {
|
|
79
|
+
if (value.length <= budget) return value;
|
|
80
|
+
// 留 '..."' 的余量(3字符),最多截到20字符
|
|
81
|
+
const keep = Math.max(20, budget - 3);
|
|
82
|
+
return value.substring(0, keep) + '...';
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
return value.map(item => deepTruncate(item, Math.floor(budget / Math.max(value.length, 1))));
|
|
86
|
+
}
|
|
87
|
+
if (value && typeof value === 'object') {
|
|
88
|
+
const keys = Object.keys(value);
|
|
89
|
+
const result = {};
|
|
90
|
+
for (const key of keys) {
|
|
91
|
+
result[key] = deepTruncate(value[key], Math.max(20, Math.floor(budget / Math.max(keys.length, 1))));
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ====== 初始化 ======
|
|
99
|
+
let _writeQueue = Promise.resolve();
|
|
100
|
+
|
|
101
|
+
async function initStore() {
|
|
102
|
+
if (_initialized) return true;
|
|
103
|
+
|
|
104
|
+
// 主存储路径
|
|
105
|
+
try {
|
|
106
|
+
await ensureDir(EVENTS_FILE);
|
|
107
|
+
// 确保文件存在
|
|
108
|
+
try { await access(EVENTS_FILE); } catch { await writeFile(EVENTS_FILE, '', 'utf-8'); }
|
|
109
|
+
// 确保索引文件存在
|
|
110
|
+
try { await access(INDEX_FILE); } catch {
|
|
111
|
+
await writeFile(INDEX_FILE, JSON.stringify({
|
|
112
|
+
version: 1,
|
|
113
|
+
created: new Date().toISOString(),
|
|
114
|
+
totalEvents: 0,
|
|
115
|
+
lastEventId: null,
|
|
116
|
+
}, null, 2), 'utf-8');
|
|
117
|
+
}
|
|
118
|
+
_initialized = true;
|
|
119
|
+
return true;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
// 主存储不可用,启用回退:写入临时目录
|
|
122
|
+
_fallbackDir = join(homedir(), '.openclaw', 'workspace', 'memory', 'hippocampus_fallback');
|
|
123
|
+
try {
|
|
124
|
+
const fbFile = join(_fallbackDir, 'events.jsonl');
|
|
125
|
+
await ensureDir(fbFile);
|
|
126
|
+
try { await access(fbFile); } catch { await writeFile(fbFile, '', 'utf-8'); }
|
|
127
|
+
console.warn('[hippocampus] ⚠️ 主存储不可用,切换到回退路径:', _fallbackDir);
|
|
128
|
+
_initialized = true;
|
|
129
|
+
return true;
|
|
130
|
+
} catch (fbErr) {
|
|
131
|
+
console.warn('[hippocampus] ❌ 存储init failed(主+回退均不可用):', fbErr.message);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ====== 核心 API 接口 ======
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 记录一个工具调用事件到hippocampus
|
|
141
|
+
*
|
|
142
|
+
* @param {Object} event - 事件数据
|
|
143
|
+
* @param {string} event.toolName - 工具名
|
|
144
|
+
* @param {Object} [event.params] - 调用参数
|
|
145
|
+
* @param {Object} [event.result] - 返回结果
|
|
146
|
+
* @param {number} event.duration - 耗时(ms)
|
|
147
|
+
* @param {string} event.status - "success" 或 "error"
|
|
148
|
+
* @param {string} [event.sessionId] - 会话ID
|
|
149
|
+
* @returns {Promise<Object>} { success: boolean, eventId?: string, error?: string }
|
|
150
|
+
*/
|
|
151
|
+
export async function recordEvent({ toolName, params, result, duration, status, sessionId }) {
|
|
152
|
+
try {
|
|
153
|
+
if (!toolName) {
|
|
154
|
+
return { success: false, error: 'missing toolName' };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const initialized = await initStore();
|
|
158
|
+
if (!initialized) {
|
|
159
|
+
return { success: false, error: 'hippocampus存储不可用' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const eventId = generateEventId();
|
|
163
|
+
const entry = {
|
|
164
|
+
eventId,
|
|
165
|
+
type: 'tool_call',
|
|
166
|
+
timestamp: new Date().toISOString(),
|
|
167
|
+
toolName,
|
|
168
|
+
params: truncateObj(params, MAX_PARAMS_LENGTH),
|
|
169
|
+
result: truncateObj(result, MAX_RESULT_LENGTH),
|
|
170
|
+
duration: typeof duration === 'number' ? Math.round(duration) : 0,
|
|
171
|
+
status: status || (result && !result.error ? 'success' : 'error'),
|
|
172
|
+
sessionId: sessionId || '',
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const line = JSON.stringify(entry) + '\n';
|
|
176
|
+
|
|
177
|
+
// 串行写入(防并发交错)
|
|
178
|
+
_writeQueue = _writeQueue.then(async () => {
|
|
179
|
+
const targetFile = _fallbackDir
|
|
180
|
+
? join(_fallbackDir, 'events.jsonl')
|
|
181
|
+
: EVENTS_FILE;
|
|
182
|
+
|
|
183
|
+
await appendFile(targetFile, line, 'utf-8');
|
|
184
|
+
|
|
185
|
+
// 更新索引stats
|
|
186
|
+
try {
|
|
187
|
+
const raw = await readFile(INDEX_FILE, 'utf-8');
|
|
188
|
+
const idx = JSON.parse(raw);
|
|
189
|
+
idx.totalEvents = (idx.totalEvents || 0) + 1;
|
|
190
|
+
idx.lastEventId = eventId;
|
|
191
|
+
idx.lastUpdated = new Date().toISOString();
|
|
192
|
+
await writeFile(INDEX_FILE, JSON.stringify(idx, null, 2), 'utf-8');
|
|
193
|
+
} catch (e) {
|
|
194
|
+
// 索引更新失败不影响主流程
|
|
195
|
+
console.warn('[hippocampus] ⚠️ 索引更新失败:', e.message);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 检查文件大小,触发归档
|
|
199
|
+
try {
|
|
200
|
+
const stats = await import('fs').then(fs => fs.promises.stat(targetFile));
|
|
201
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
202
|
+
await rotateLog(targetFile);
|
|
203
|
+
}
|
|
204
|
+
} catch (e) {
|
|
205
|
+
// 文件大小检查非关键
|
|
206
|
+
console.warn('[hippocampus] ⚠️ 文件大小检查失败:', e.message);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await _writeQueue;
|
|
211
|
+
return { success: true, eventId };
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.warn('[hippocampus] ❌ recordEvent 失败:', err.message);
|
|
214
|
+
return { success: false, error: err.message };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ====== 文件轮转归档 ======
|
|
219
|
+
async function rotateLog(filePath) {
|
|
220
|
+
try {
|
|
221
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
222
|
+
const archivePath = filePath.replace('.jsonl', `_${ts}.jsonl`);
|
|
223
|
+
const { rename } = await import('fs/promises');
|
|
224
|
+
await rename(filePath, archivePath);
|
|
225
|
+
await writeFile(filePath, '', 'utf-8');
|
|
226
|
+
// TODO: 移除调试日志 console.log(`[hippocampus] 📦 事件日志已归档: ${archivePath}`);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.warn('[hippocampus] ⚠️ archive failed:', err.message);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ====== 查询接口 ======
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 查询最近的事件(同步操作,不依赖索引)
|
|
236
|
+
*/
|
|
237
|
+
export async function queryRecentEvents(limit = 20) {
|
|
238
|
+
try {
|
|
239
|
+
const targetFile = _fallbackDir
|
|
240
|
+
? join(_fallbackDir, 'events.jsonl')
|
|
241
|
+
: EVENTS_FILE;
|
|
242
|
+
|
|
243
|
+
try { await access(targetFile); } catch { return []; }
|
|
244
|
+
|
|
245
|
+
const raw = await readFile(targetFile, 'utf-8');
|
|
246
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
247
|
+
const recent = lines.slice(-limit);
|
|
248
|
+
return recent.map(l => {
|
|
249
|
+
try {
|
|
250
|
+
return JSON.parse(l);
|
|
251
|
+
} catch (e) {
|
|
252
|
+
console.warn('[hippocampus] ⚠️ 跳过损坏的事件行:', e.message, l.substring(0, 60));
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}).filter(Boolean);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
console.warn('[hippocampus] ⚠️ queryRecentEvents 失败:', err.message);
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export default { recordEvent, queryRecentEvents };
|