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,1038 @@
1
+
2
+ /**
3
+ * 🧠 hippocampus-multi-search.js — 多路径跨区域搜索引擎
4
+ *
5
+ * sc 五路并行记忆搜索:
6
+ * A: 主关键词搜索 — 现有 FTS5/keyword 搜索 + 同义词展开
7
+ * B: 同义词展开搜索 — SYNONYM_MAP 自动展开查询词
8
+ * C: 失败经验搜索 — error/crash/timeout/报错 等模式
9
+ * D: 跨区域关联 — 实体→决策→时间线 JOIN 遍历
10
+ * E: 时间线回溯 — 按时间窗口前后聚类
11
+ *
12
+ * 用法:
13
+ * import { multiPathSearch } from './hippocampus-multi-search.js';
14
+ * const result = await multiPathSearch({ query: "数据库配置", pool }, { timeRange: "1w" });
15
+ *
16
+ * v1.0.0 — 2026-06-03
17
+ */
18
+
19
+ import { readFile, readdir } from 'fs/promises';
20
+ import { join, dirname } from 'path';
21
+ import { homedir } from 'os';
22
+ import { existsSync } from 'fs';
23
+
24
+ // ====== 路径常量 ======
25
+ const WORKSPACE_DIR = join(homedir(), '.openclaw', 'workspace');
26
+ const DIALOG_DIR = join(WORKSPACE_DIR, 'memory', 'dialog');
27
+ const HIPPOCAMPUS_DIR = join(WORKSPACE_DIR, 'memory', 'hippocampus');
28
+ const TASK_STATES_DIR = join(WORKSPACE_DIR, 'memory', 'task-states');
29
+ const REVIEWS_DIR = join(WORKSPACE_DIR, 'memory', 'reviews');
30
+
31
+ // ====== ─── SYNONYM_MAP(同义词映射表,覆盖日常 90% 场景)─── ======
32
+ const SYNONYM_MAP = {
33
+ 'config': 'config configure configuration setting setup 配置 设置 参数 settings',
34
+ 'database': 'database db 数据库 数据源 datasource postgres mysql sqlite mongo',
35
+ 'error': 'error failure crash timeout 错误 失败 异常 exception 报错 卡死 fatal bug',
36
+ 'search': 'search query find 搜索 查找 查询 find recall grep lookup',
37
+ 'install': 'install setup 安装 部署 setup installation 配置环境',
38
+ 'network': 'network 网络 连接 proxy vpn 代理 connection 端口 port',
39
+ 'build': 'build compile 构建 编译 打包 npm node pod install bundle',
40
+ 'test': 'test 测试 单元测试 unittest jest mocha spec 验证 verify',
41
+ 'docker': 'docker container 容器 镜像 image compose yml',
42
+ 'deploy': 'deploy deployment 部署 发布 release rollback 上线',
43
+ 'memory': 'memory 记忆 知识 知识库 remember cache 缓存 回忆 recall',
44
+ 'tool': 'tool 工具 skill plugin 插件 函数 function 功能',
45
+ 'security': 'security 安全 权限 auth authz ssl 防火墙 firewall',
46
+ 'code': 'code 代码 file 文件 修改 edit 编辑 重构 refactor 调试 debug',
47
+ 'performance': 'performance 性能 优化 速度 内存 延迟 latency speed',
48
+ 'backup': 'backup 备份 恢复 restore 同步 迁移 migrate',
49
+ 'monitor': 'monitor 监控 日志 log 警报 alert dashboard 观察 watch',
50
+ 'project': 'project 项目 workspace 工作空间 workspace 工程 repo',
51
+ 'update': 'update 更新 升级 upgrade version 版本 新版本 release',
52
+ 'remove': 'remove delete 删除 卸载 uninstall clean 清理 清除 rm purge',
53
+ 'create': 'create 创建 新建 new 生成 generate 构建 build 初始化 init',
54
+ 'fix': 'fix 修复 解决 修复解决 bugfix repair patch 补丁 hotfix',
55
+ 'path': 'path 路径 directory 目录 folder 文件夹 目录结构 filepath',
56
+ 'window': 'window 窗口 panel 面板 界面 ui 界面 view 视图',
57
+ 'deploy': 'deploy deployment 部署 发布 release rollback 上线 cicd',
58
+ 'git': 'git github 版本控制 vcs 仓库 repo branch commit merge push pull',
59
+ 'python': 'python pip 虚拟环境 virtualenv conda 脚本 script',
60
+ 'node': 'node nodejs npm 包 package 模块 module 依赖 dependency',
61
+ 'ai': 'ai 人工智能 llm 模型 model 大模型 agent agent 智能体',
62
+ // 中文专用同义词
63
+ '配置': 'config configure configuration setting setup 配置 设置 参数 settings',
64
+ '错误': 'error failure crash timeout 错误 失败 异常 exception 报错 卡死 fatal',
65
+ '搜索': 'search query find 搜索 查找 查询 find recall grep',
66
+ '安装': 'install setup 安装 部署 setup installation',
67
+ '网络': 'network 网络 连接 proxy vpn 代理 端口 port',
68
+ '测试': 'test 测试 单元测试 验证 verify spec',
69
+ '部署': 'deploy deployment 部署 发布 release 上线 rollback',
70
+ '记忆': 'memory 记忆 知识 知识库 remember cache 回忆 recall',
71
+ '工具': 'tool 工具 skill plugin 插件 函数 function',
72
+ '安全': 'security 安全 权限 auth 防火墙 firewall',
73
+ '代码': 'code 代码 file 文件 修改 edit 重构 refactor',
74
+ '性能': 'performance 性能 优化 速度 内存 延迟 latency',
75
+ '备份': 'backup 备份 恢复 restore 同步 sync',
76
+ '监控': 'monitor 监控 日志 log 警报 alert 观察',
77
+ '项目': 'project 项目 workspace workspace repo',
78
+ '更新': 'update 更新 升级 upgrade version version',
79
+ '删除': 'remove delete 删除 卸载 uninstall clean 清除 清理 rm',
80
+ '创建': 'create 创建 新建 new 生成 generate 初始化 init build',
81
+ '修复': 'fix 修复 解决 bugfix repair patch 补丁 hotfix 修正',
82
+ '路径': 'path 路径 directory 目录 folder 目录结构',
83
+ };
84
+
85
+ // ====== ─── 失败关键词集合(路径 C 专用)─── ======
86
+ const FAILURE_TERMS = [
87
+ 'error', 'failure', 'crash', 'timeout', '报错', '错误', '失败',
88
+ '异常', 'exception', 'fatal', 'bug', '死锁', '卡死', '崩溃',
89
+ 'ECONNREFUSED', 'ENOENT', 'EACCES', 'EPERM', 'ETIMEDOUT',
90
+ 'reject', 'unhandled', 'rejection', '报错', '不行', '搞不定',
91
+ '死循环', '内存泄漏', 'leak', '堆积', 'queue',
92
+ 'cancel', 'abort', '熔断', '降级', '降级',
93
+ '修复', 'fixed', 'bugfix', 'hotfix', '补丁', 'workaround',
94
+ '踩坑', '坑', '教训', 'warning',
95
+ ];
96
+
97
+ // ====== 工具函数 ======
98
+
99
+ /**
100
+ * 展开查询词为同义词集合
101
+ * @param {string} query - 原始查询
102
+ * @returns {string[]} 展开后的搜索词数组
103
+ */
104
+ function expandQueryTerms(query) {
105
+ const terms = query.split(/[\s,,、]+/).filter(Boolean);
106
+ const expandedSet = new Set();
107
+
108
+ for (const term of terms) {
109
+ const lower = term.toLowerCase();
110
+ expandedSet.add(term);
111
+ expandedSet.add(lower);
112
+
113
+ // 查同义词映射
114
+ const synonyms = SYNONYM_MAP[term] || SYNONYM_MAP[lower];
115
+ if (synonyms) {
116
+ const synTerms = synonyms.split(/\s+/);
117
+ for (const st of synTerms) {
118
+ if (st.length > 1) expandedSet.add(st);
119
+ }
120
+ }
121
+
122
+ // 也查反向映射(同义词→原词)
123
+ for (const [key, val] of Object.entries(SYNONYM_MAP)) {
124
+ const valTerms = val.split(/\s+/);
125
+ if (valTerms.includes(lower) || valTerms.includes(term)) {
126
+ expandedSet.add(key);
127
+ expandedSet.add(key.toLowerCase());
128
+ }
129
+ }
130
+ }
131
+
132
+ return [...expandedSet].filter(Boolean);
133
+ }
134
+
135
+ /**
136
+ * 构建搜索关键词的 OR 查询串
137
+ * @param {string} query - 原始查询
138
+ * @returns {string} 展开后的 OR 查询串
139
+ */
140
+ function buildSynonymQuery(query) {
141
+ const terms = expandQueryTerms(query);
142
+ return terms.join(' ');
143
+ }
144
+
145
+ /**
146
+ * 构建失败模式搜索串
147
+ * @param {string} query - 原始查询
148
+ * @returns {string} 失败模式组合搜索串
149
+ */
150
+ function buildFailureQuery(query) {
151
+ const originalTerms = query.split(/[\s,,、]+/).filter(Boolean);
152
+ const failTerms = FAILURE_TERMS.slice(0, 15).join(' ');
153
+ // 组合:原始词 + 失败词 → 同时匹配
154
+ return `${originalTerms.join(' ')} ${failTerms}`;
155
+ }
156
+
157
+ /**
158
+ * 扫描目录获取 .md 文件列表(含子目录)
159
+ */
160
+ async function scanMarkdownFiles(dir, maxFiles = 500, depth = 0) {
161
+ if (depth > 8 || !existsSync(dir)) return [];
162
+ const results = [];
163
+ let entries;
164
+ try {
165
+ entries = await readdir(dir, { withFileTypes: true });
166
+ } catch {
167
+ return [];
168
+ }
169
+ for (const entry of entries) {
170
+ if (results.length >= maxFiles) break;
171
+ const fullPath = join(dir, entry.name);
172
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
173
+ const sub = await scanMarkdownFiles(fullPath, maxFiles - results.length, depth + 1);
174
+ results.push(...sub);
175
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
176
+ results.push(fullPath);
177
+ }
178
+ }
179
+ return results;
180
+ }
181
+
182
+ // ====== ─── 路径 A:主关键词搜索 ─── ======
183
+
184
+ /**
185
+ * A 路径 — 主关键词搜索
186
+ * 使用 Worker pool 的 search-text 类型在 dialog + reviews 中搜索
187
+ *
188
+ * @param {string} query - 原始查询词
189
+ * @param {object} pool - CpuWorkerPool 实例
190
+ * @param {object} options - { timeRange, mode, limit }
191
+ * @returns {Promise<{ results: Array, elapsed: number, count: number }>}
192
+ */
193
+ async function pathA_MainSearch(query, pool, options = {}) {
194
+ const startTime = Date.now();
195
+ const expandedQuery = buildSynonymQuery(query);
196
+ const limit = options.limit || 10;
197
+
198
+ // 收集搜索路径
199
+ const searchPaths = [];
200
+
201
+ // 1. 对话日记目录
202
+ if (existsSync(DIALOG_DIR)) {
203
+ searchPaths.push(DIALOG_DIR);
204
+ }
205
+
206
+ // 2. reviews 目录(设计文档/方案)
207
+ if (existsSync(REVIEWS_DIR)) {
208
+ searchPaths.push(REVIEWS_DIR);
209
+ }
210
+
211
+ // 3. 核心文档目录
212
+ const memoryDir = join(WORKSPACE_DIR, 'memory');
213
+ if (existsSync(memoryDir)) {
214
+ searchPaths.push(memoryDir);
215
+ }
216
+
217
+ // 扫描所有 .md 文件
218
+ const allFiles = [];
219
+ for (const sp of searchPaths) {
220
+ const files = await scanMarkdownFiles(sp, 200);
221
+ allFiles.push(...files);
222
+ }
223
+
224
+ if (allFiles.length === 0) {
225
+ return { results: [], elapsed: Date.now() - startTime, count: 0, expandedQuery, note: '未找到搜索文件' };
226
+ }
227
+
228
+ // 分块并发搜索
229
+ const CHUNK_SIZE = 50;
230
+ const chunks = [];
231
+ for (let i = 0; i < allFiles.length; i += CHUNK_SIZE) {
232
+ chunks.push(allFiles.slice(i, i + CHUNK_SIZE));
233
+ }
234
+
235
+ const workerTasks = chunks.map(fileList =>
236
+ pool.exec({
237
+ type: 'search-text',
238
+ keyword: expandedQuery,
239
+ files: fileList,
240
+ }, 'high')
241
+ );
242
+
243
+ // 也搜索原始词(保底,避免扩展得太散)
244
+ const originalChunks = [];
245
+ if (expandedQuery !== query) {
246
+ for (let i = 0; i < allFiles.length; i += CHUNK_SIZE) {
247
+ originalChunks.push(allFiles.slice(i, i + CHUNK_SIZE));
248
+ }
249
+ workerTasks.push(...originalChunks.map(fileList =>
250
+ pool.exec({
251
+ type: 'search-text',
252
+ keyword: query,
253
+ files: fileList,
254
+ }, 'high')
255
+ ));
256
+ }
257
+
258
+ const rawResults = await Promise.allSettled(workerTasks);
259
+
260
+ // 合并去重
261
+ const seen = new Set();
262
+ const merged = [];
263
+ for (const r of rawResults) {
264
+ if (r.status === 'fulfilled' && r.value && r.value.results) {
265
+ for (const item of r.value.results) {
266
+ if (!seen.has(item.file)) {
267
+ seen.add(item.file);
268
+ merged.push(item);
269
+ } else {
270
+ // 合并匹配计数
271
+ const existing = merged.find(m => m.file === item.file);
272
+ if (existing && item.matchCount) {
273
+ existing.matchCount = (existing.matchCount || 0) + item.matchCount;
274
+ if (item.matches) {
275
+ (existing.matches = existing.matches || []).push(...item.matches);
276
+ }
277
+ }
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ // 按匹配数排序
284
+ merged.sort((a, b) => (b.matchCount || 0) - (a.matchCount || 0));
285
+
286
+ const elapsed = Date.now() - startTime;
287
+ return {
288
+ results: merged.slice(0, limit),
289
+ elapsed,
290
+ count: merged.length,
291
+ expandedQuery,
292
+ totalFilesSearched: allFiles.length,
293
+ };
294
+ }
295
+
296
+ // ====== ─── 路径 B:同义词/关联词展开搜索 ─── ======
297
+
298
+ /**
299
+ * B 路径 — 同义词/关联词展开搜索
300
+ * 将 query 中每个词拆开,分别走语义搜索或各同义词分支搜索
301
+ *
302
+ * @param {string} query - 原始查询词
303
+ * @param {object} pool - CpuWorkerPool 实例
304
+ * @param {object} options
305
+ * @returns {Promise<{ results: Array, branches: Array, elapsed: number }>}
306
+ */
307
+ async function pathB_SynonymSearch(query, pool, options = {}) {
308
+ const startTime = Date.now();
309
+ const terms = query.split(/[\s,,、]+/).filter(Boolean);
310
+ const limit = options.limit || 8;
311
+
312
+ // 对每个 term 分别展开同义词(如果该词有同义词映射)
313
+ const branchQueries = [];
314
+ for (const term of terms) {
315
+ const lower = term.toLowerCase();
316
+ const synonyms = SYNONYM_MAP[lower] || SYNONYM_MAP[term];
317
+ if (synonyms) {
318
+ const synTerms = synonyms.split(/\s+/).filter(s => s.length > 1);
319
+ // 取前 3 个最有代表性的同义词
320
+ const topSyns = synTerms.slice(0, 3);
321
+ for (const syn of topSyns) {
322
+ if (syn !== lower && syn !== term) {
323
+ branchQueries.push(syn);
324
+ }
325
+ }
326
+ }
327
+ }
328
+
329
+ // 去重
330
+ const uniqueQueries = [...new Set(branchQueries)].slice(0, 10);
331
+ if (uniqueQueries.length === 0) {
332
+ return { results: [], branches: [], elapsed: Date.now() - startTime, note: '无需同义词展开' };
333
+ }
334
+
335
+ // 并行搜索所有分支(走语义搜索,也可以走 search-text)
336
+ const searchTasks = uniqueQueries.map(bq =>
337
+ pool.exec({
338
+ type: 'semantic-search',
339
+ query: bq,
340
+ rootDir: WORKSPACE_DIR,
341
+ maxResults: 5,
342
+ }, 'high').catch(() => null)
343
+ );
344
+
345
+ // 也为原始 query 做一个语义搜索
346
+ searchTasks.push(
347
+ pool.exec({
348
+ type: 'semantic-search',
349
+ query,
350
+ rootDir: WORKSPACE_DIR,
351
+ maxResults: limit,
352
+ }, 'high').catch(() => null)
353
+ );
354
+
355
+ const rawResults = await Promise.allSettled(searchTasks);
356
+
357
+ // 合并结果
358
+ const seen = new Set();
359
+ const fused = [];
360
+ const branches = [];
361
+
362
+ for (let i = 0; i < uniqueQueries.length; i++) {
363
+ const r = rawResults[i];
364
+ const bq = uniqueQueries[i];
365
+ if (r.status === 'fulfilled' && r.value && r.value.results) {
366
+ const branchResults = r.value.results.slice(0, 3);
367
+ branches.push({ term: bq, count: branchResults.length });
368
+ for (const item of branchResults) {
369
+ const key = item.file || `${item.path}_${item.line}`;
370
+ if (!seen.has(key)) {
371
+ seen.add(key);
372
+ fused.push({ ...item, _synonymBranch: bq });
373
+ }
374
+ }
375
+ }
376
+ }
377
+
378
+ // 也要原始语义结果中的额外结果
379
+ const origResult = rawResults[uniqueQueries.length];
380
+ if (origResult?.status === 'fulfilled' && origResult.value?.results) {
381
+ for (const item of origResult.value.results) {
382
+ const key = item.file || `${item.path}_${item.line}`;
383
+ if (!seen.has(key)) {
384
+ seen.add(key);
385
+ fused.push({ ...item, _synonymBranch: 'original' });
386
+ }
387
+ }
388
+ }
389
+
390
+ fused.sort((a, b) => (b.score || 0) - (a.score || 0));
391
+
392
+ return {
393
+ results: fused.slice(0, limit),
394
+ branches: branches.filter(b => b.count > 0),
395
+ elapsed: Date.now() - startTime,
396
+ totalSynonymBranches: uniqueQueries.length,
397
+ };
398
+ }
399
+
400
+ // ====== ─── 路径 C:失败经验搜索 ─── ======
401
+
402
+ /**
403
+ * C 路径 — 失败经验搜索
404
+ * 搜索所有包含失败/报错/踩坑模式的记录
405
+ *
406
+ * @param {string} query - 原始查询词
407
+ * @param {object} pool - CpuWorkerPool 实例
408
+ * @param {object} options
409
+ * @returns {Promise<{ results: Array, elapsed: number, count: number }>}
410
+ */
411
+ async function pathC_FailureSearch(query, pool, options = {}) {
412
+ const startTime = Date.now();
413
+ const failureQuery = buildFailureQuery(query);
414
+ const limit = options.limit || 8;
415
+
416
+ // 搜索路径:dialog + reviews + memory
417
+ const searchPaths = [];
418
+ if (existsSync(DIALOG_DIR)) searchPaths.push(DIALOG_DIR);
419
+ if (existsSync(REVIEWS_DIR)) searchPaths.push(REVIEWS_DIR);
420
+ const memoryDir = join(WORKSPACE_DIR, 'memory');
421
+ if (existsSync(memoryDir)) searchPaths.push(memoryDir);
422
+
423
+ const allFiles = [];
424
+ for (const sp of searchPaths) {
425
+ const files = await scanMarkdownFiles(sp, 300);
426
+ allFiles.push(...files);
427
+ }
428
+
429
+ if (allFiles.length === 0) {
430
+ return { results: [], elapsed: Date.now() - startTime, count: 0 };
431
+ }
432
+
433
+ // 1. Worker pool 搜索失败模式
434
+ const CHUNK_SIZE = 50;
435
+ const chunks = [];
436
+ for (let i = 0; i < allFiles.length; i += CHUNK_SIZE) {
437
+ chunks.push(allFiles.slice(i, i + CHUNK_SIZE));
438
+ }
439
+
440
+ const workerTasks = chunks.map(fileList =>
441
+ pool.exec({
442
+ type: 'search-text',
443
+ keyword: failureQuery,
444
+ files: fileList,
445
+ }, 'high')
446
+ );
447
+
448
+ const workerResults = await Promise.allSettled(workerTasks);
449
+
450
+ // 合并
451
+ const seen = new Set();
452
+ const merged = [];
453
+ for (const r of workerResults) {
454
+ if (r.status === 'fulfilled' && r.value?.results) {
455
+ for (const item of r.value.results) {
456
+ if (!seen.has(item.file)) {
457
+ seen.add(item.file);
458
+ merged.push(item);
459
+ }
460
+ }
461
+ }
462
+ }
463
+
464
+ // 2. 搜索 task-states 文件(失败状态的任务)
465
+ let taskFailures = [];
466
+ if (existsSync(TASK_STATES_DIR)) {
467
+ try {
468
+ const taskFiles = await readdir(TASK_STATES_DIR);
469
+ for (const tf of taskFiles) {
470
+ if (!tf.endsWith('.json')) continue;
471
+ try {
472
+ const content = await readFile(join(TASK_STATES_DIR, tf), 'utf-8');
473
+ const data = JSON.parse(content);
474
+ if (data.status === 'error' || data.error || data.errors) {
475
+ taskFailures.push({
476
+ file: join(TASK_STATES_DIR, tf),
477
+ taskId: tf.replace('.json', ''),
478
+ status: 'error',
479
+ errorDetail: data.error || data.errors?.slice(0, 2).join(', ') || data.errorDetail || '未知错误',
480
+ matchCount: 1,
481
+ });
482
+ }
483
+ } catch {}
484
+ }
485
+ } catch {}
486
+ }
487
+
488
+ // 3. 搜索 hippocampus events 中的失败事件
489
+ let eventFailures = [];
490
+ const eventsJsonl = join(HIPPOCAMPUS_DIR, 'events.jsonl');
491
+ if (existsSync(eventsJsonl)) {
492
+ try {
493
+ const content = await readFile(eventsJsonl, 'utf-8');
494
+ const lines = content.trim().split('\n').slice(-50); // 只看最近 50
495
+ for (const line of lines) {
496
+ try {
497
+ const evt = JSON.parse(line);
498
+ if (evt.status === 'error' || evt.status === 'failure') {
499
+ const queryTerms = query.split(/[\s,,、]+/).filter(Boolean);
500
+ const evtText = JSON.stringify(evt).toLowerCase();
501
+ const matchesQuery = queryTerms.some(t => evtText.includes(t.toLowerCase()));
502
+ if (matchesQuery) {
503
+ eventFailures.push({
504
+ file: eventsJsonl,
505
+ eventId: evt.eventId || evt.id || 'unknown',
506
+ type: evt.type || 'unknown',
507
+ status: evt.status,
508
+ errorMessage: evt.result || evt.error || evt.params || '无详细',
509
+ matchCount: 1,
510
+ timestamp: evt.timestamp,
511
+ });
512
+ }
513
+ }
514
+ } catch {}
515
+ }
516
+ } catch {}
517
+ }
518
+
519
+ // 排序:worker结果按匹配数,task-states和events按时间
520
+ merged.sort((a, b) => (b.matchCount || 0) - (a.matchCount || 0));
521
+
522
+ const failures = [
523
+ ...merged.slice(0, limit),
524
+ ...taskFailures.slice(0, 3),
525
+ ...eventFailures.slice(0, 2),
526
+ ];
527
+
528
+ return {
529
+ results: failures.slice(0, limit),
530
+ elapsed: Date.now() - startTime,
531
+ count: failures.length,
532
+ totalFilesSearched: allFiles.length,
533
+ taskFailureCount: taskFailures.length,
534
+ eventFailureCount: eventFailures.length,
535
+ };
536
+ }
537
+
538
+ // ====== ─── 路径 D:跨区域关联搜索 ─── ======
539
+
540
+ /**
541
+ * D 路径 — 跨区域关联搜索
542
+ * 遍历 hippocampus 的 entities.json → decisions.json → timeline.json
543
+ * 建立跨区域关联
544
+ *
545
+ * @param {string} query - 原始查询词
546
+ * @param {object} pool - CpuWorkerPool 实例(保留用于扩展)
547
+ * @param {object} options
548
+ * @returns {Promise<{ results: Array, elapsed: number }>}
549
+ */
550
+ async function pathD_CrossRegionSearch(query, options = {}) {
551
+ const startTime = Date.now();
552
+ const limit = options.limit || 10;
553
+ const queryLower = query.toLowerCase();
554
+ const queryTerms = queryLower.split(/[\s,,、]+/).filter(Boolean);
555
+
556
+ const hippocampusFiles = [
557
+ { name: 'entities.json', type: 'entity', path: join(HIPPOCAMPUS_DIR, 'entities.json') },
558
+ { name: 'decisions.json', type: 'decision', path: join(HIPPOCAMPUS_DIR, 'decisions.json') },
559
+ { name: 'timeline.json', type: 'timeline', path: join(HIPPOCAMPUS_DIR, 'timeline.json') },
560
+ ];
561
+
562
+ const seen = new Set();
563
+ const results = [];
564
+
565
+ for (const { name, type, path: fPath } of hippocampusFiles) {
566
+ if (!existsSync(fPath)) continue;
567
+ try {
568
+ const raw = await readFile(fPath, 'utf-8');
569
+ const data = JSON.parse(raw);
570
+
571
+ // 统一遍历逻辑
572
+ const entries = Array.isArray(data) ? data : (data.records || data.items || data.entries || [data]);
573
+
574
+ for (const entry of entries) {
575
+ const entryStr = JSON.stringify(entry).toLowerCase();
576
+ const matches = queryTerms.some(t => entryStr.includes(t));
577
+ if (!matches) continue;
578
+
579
+ let id = entry.id || entry.entity_id || entry.decision_id || entry.event_id || '';
580
+ let summary = entry.summary || entry.name || entry.description || entry.title || '';
581
+ let recordTime = entry.time || entry.timestamp || entry.date || '';
582
+
583
+ // 去重
584
+ const dedupKey = `${type}:${id || summary}`;
585
+ if (seen.has(dedupKey)) continue;
586
+ seen.add(dedupKey);
587
+
588
+ const resultItem = {
589
+ _region: type,
590
+ id: id,
591
+ summary: typeof summary === 'string' ? summary.substring(0, 300) : JSON.stringify(summary).substring(0, 300),
592
+ time: recordTime,
593
+ sourceFile: name,
594
+ };
595
+
596
+ // 实体特定的额外字段
597
+ if (type === 'entity') {
598
+ resultItem.entityName = entry.name || entry.label || '';
599
+ resultItem.entityType = entry.type || entry.entity_type || '';
600
+ // 提取关联的决策/事件
601
+ const relatedDecisions = entry.decisions || entry.related_decisions || [];
602
+ if (Array.isArray(relatedDecisions) && relatedDecisions.length > 0) {
603
+ resultItem.relatedDecisions = relatedDecisions.slice(0, 5);
604
+ }
605
+ const relatedEvents = entry.events || entry.related_events || [];
606
+ if (Array.isArray(relatedEvents) && relatedEvents.length > 0) {
607
+ resultItem.relatedEvents = relatedEvents.slice(0, 5);
608
+ }
609
+ }
610
+
611
+ // 决策特定的额外字段
612
+ if (type === 'decision') {
613
+ resultItem.context = entry.context || entry.reason || '';
614
+ resultItem.decision = entry.decision || entry.outcome || entry.conclusion || '';
615
+ }
616
+
617
+ // 时间线特定的额外字段
618
+ if (type === 'timeline') {
619
+ resultItem.eventType = entry.type || entry.event_type || '';
620
+ resultItem.detail = entry.detail || entry.content || '';
621
+ }
622
+
623
+ results.push(resultItem);
624
+ }
625
+ } catch (err) {
626
+ results.push({ _region: type, error: `读取 ${name} 失败: ${err.message}` });
627
+ }
628
+ }
629
+
630
+ // 按时间排序(如有)
631
+ results.sort((a, b) => {
632
+ // 有时间戳的先排,没有的沉底
633
+ if (a.time && b.time) return String(b.time).localeCompare(String(a.time));
634
+ if (a.time) return -1;
635
+ if (b.time) return 1;
636
+ return 0;
637
+ });
638
+
639
+ return {
640
+ results: results.slice(0, limit),
641
+ elapsed: Date.now() - startTime,
642
+ count: results.length,
643
+ regions: {
644
+ entities: results.filter(r => r._region === 'entity').length,
645
+ decisions: results.filter(r => r._region === 'decision').length,
646
+ timeline: results.filter(r => r._region === 'timeline').length,
647
+ },
648
+ };
649
+ }
650
+
651
+ // ====== ─── 路径 E:时间线回溯搜索 ─── ======
652
+
653
+ /**
654
+ * E 路径 — 时间线回溯搜索
655
+ * 基于时间窗口的搜索:搜索 query 相关事件后,扩展时间窗口
656
+ *
657
+ * @param {string} query - 原始查询词
658
+ * @param {object} pool - CpuWorkerPool 实例
659
+ * @param {object} options - { timeRange, hoursBefore, hoursAfter }
660
+ * @returns {Promise<{ results: Array, elapsed: number }>}
661
+ */
662
+ async function pathE_TimelineBacktrack(query, pool, options = {}) {
663
+ const startTime = Date.now();
664
+ const limit = options.limit || 8;
665
+ const queryLower = query.toLowerCase();
666
+ const queryTerms = queryLower.split(/[\s,,、]+/).filter(Boolean);
667
+ const hoursBefore = options.hoursBefore || 48; // 默认往前48小时
668
+ const hoursAfter = options.hoursAfter || 24; // 默认往后24小时
669
+
670
+ // 1. 先用搜索找相关事件的时间点
671
+ const dialogDir = DIALOG_DIR;
672
+ let referenceTimes = [];
673
+
674
+ // 从对话日记文件名中提取时间(文件名 YYYY-MM-DD.md)
675
+ if (existsSync(dialogDir)) {
676
+ try {
677
+ const entries = await readdir(dialogDir);
678
+ for (const entry of entries) {
679
+ if (!entry.endsWith('.md')) continue;
680
+ // 尝试搜索匹配
681
+ const filePath = join(dialogDir, entry);
682
+ try {
683
+ const content = await readFile(filePath, 'utf-8');
684
+ const contentLower = content.toLowerCase();
685
+ const matches = queryTerms.some(t => contentLower.includes(t));
686
+ if (matches) {
687
+ const dateMatch = entry.match(/(\d{4}-\d{2}-\d{2})/);
688
+ if (dateMatch) {
689
+ referenceTimes.push({
690
+ file: entry,
691
+ date: dateMatch[1],
692
+ content: content.substring(0, 200),
693
+ });
694
+ }
695
+ }
696
+ } catch {}
697
+ }
698
+ } catch {}
699
+ }
700
+
701
+ // 2. 如果有参考时间点,扩展时间窗口搜索
702
+ let expandedResults = [];
703
+ if (referenceTimes.length > 0) {
704
+ const CHUNK_SIZE = 50;
705
+
706
+ // 收集所有 dialog 文件并按时间排序
707
+ const allDialogFiles = [];
708
+ if (existsSync(dialogDir)) {
709
+ const entries = await readdir(dialogDir);
710
+ for (const entry of entries) {
711
+ if (!entry.endsWith('.md')) continue;
712
+ const dateMatch = entry.match(/(\d{4}-\d{2}-\d{2})/);
713
+ if (dateMatch) {
714
+ allDialogFiles.push({ file: entry, date: dateMatch[1], fullPath: join(dialogDir, entry) });
715
+ }
716
+ }
717
+ }
718
+
719
+ // 对每个参考时间点,收集前后窗口内的文件
720
+ const candidateFiles = new Map();
721
+ for (const rt of referenceTimes) {
722
+ const refDate = new Date(rt.date);
723
+ for (const df of allDialogFiles) {
724
+ const fileDate = new Date(df.date);
725
+ const diffMs = fileDate.getTime() - refDate.getTime();
726
+ const diffHours = diffMs / (1000 * 60 * 60);
727
+ if (diffHours >= -hoursBefore && diffHours <= hoursAfter) {
728
+ candidateFiles.set(df.fullPath, df.date);
729
+ }
730
+ }
731
+ }
732
+
733
+ expandedResults = [...candidateFiles.keys()].map(fp => ({
734
+ file: fp,
735
+ date: candidateFiles.get(fp),
736
+ matchCount: 1,
737
+ }));
738
+ }
739
+
740
+ // 3. 结合 timeline.json 中的时间事件
741
+ const timelinePath = join(HIPPOCAMPUS_DIR, 'timeline.json');
742
+ let timelineEvents = [];
743
+ if (existsSync(timelinePath)) {
744
+ try {
745
+ const raw = await readFile(timelinePath, 'utf-8');
746
+ const data = JSON.parse(raw);
747
+ const entries = Array.isArray(data) ? data : (data.records || data.items || data.entries || [data]);
748
+ for (const entry of entries) {
749
+ const entryStr = JSON.stringify(entry).toLowerCase();
750
+ const matches = queryTerms.some(t => entryStr.includes(t));
751
+ if (matches) {
752
+ timelineEvents.push({
753
+ file: 'timeline.json',
754
+ eventType: entry.type || entry.event_type || '',
755
+ summary: entry.summary || entry.description || entry.detail || '',
756
+ time: entry.time || entry.timestamp || entry.date || '',
757
+ matchCount: 1,
758
+ });
759
+ }
760
+ }
761
+ } catch {}
762
+ }
763
+
764
+ // 4. 合并:时间窗口文件 + timeline事件
765
+ const results = [
766
+ ...expandedResults.slice(0, limit),
767
+ ...timelineEvents.slice(0, Math.max(3, limit - expandedResults.length)),
768
+ ];
769
+
770
+ return {
771
+ results: results.slice(0, limit),
772
+ elapsed: Date.now() - startTime,
773
+ count: results.length,
774
+ referenceDates: referenceTimes.map(r => r.date),
775
+ timelineEventCount: timelineEvents.length,
776
+ timeWindowHours: { before: hoursBefore, after: hoursAfter },
777
+ };
778
+ }
779
+
780
+ // ====== ─── 结果融合与重排序 ─── ======
781
+
782
+ /**
783
+ * 五路搜索结果融合
784
+ * 去重 → 交叉插入 → 权重排序
785
+ *
786
+ * @param {object} paths - { pathA, pathB, pathC, pathD, pathE }
787
+ * @param {string} query - 原始查询
788
+ * @returns {object} 融合后的结果
789
+ */
790
+ function fuseResults(paths, query) {
791
+ const seen = new Map(); // key → result item
792
+ const ordered = []; // 有序列表,保持 source 标签
793
+
794
+ function addResults(items, tag) {
795
+ if (!items || items.length === 0) return;
796
+ for (const item of items) {
797
+ const key = item.file || item.id || `${item.summary?.substring(0, 50)}`;
798
+ if (!seen.has(key)) {
799
+ seen.set(key, { ...item, _tag: tag, _order: ordered.length });
800
+ ordered.push(key);
801
+ }
802
+ }
803
+ }
804
+
805
+ // 交叉插入(主结果优先)
806
+ const pathA = paths.pathA?.results || [];
807
+ const pathB = paths.pathB?.results || [];
808
+ const pathC = paths.pathC?.results || [];
809
+ const pathD = paths.pathD?.results || [];
810
+ const pathE = paths.pathE?.results || [];
811
+
812
+ const maxLen = Math.max(pathA.length, pathB.length, pathC.length, pathD.length, pathE.length);
813
+
814
+ for (let i = 0; i < maxLen * 3; i++) {
815
+ const idx = Math.floor(i / 3);
816
+ const sub = i % 3;
817
+
818
+ if (sub === 0 && pathA[idx]) addResults([pathA[idx]], '🎯 主结果');
819
+ if (sub === 1 && pathB[idx]) addResults([pathB[idx]], '🔗 语义扩展');
820
+ if (sub === 2 && pathC[idx]) addResults([pathC[idx]], '⚠️ 失败经验');
821
+
822
+ if (sub === 0 && idx < 3) {
823
+ if (pathD[idx]) addResults([pathD[idx]], '🌐 跨区域');
824
+ if (pathE[idx]) addResults([pathE[idx]], '⏰ 时间线');
825
+ }
826
+ }
827
+
828
+ // 再补上未添加的 D/E 路径结果
829
+ if (pathD.length > 3) addResults(pathD.slice(3), '🌐 跨区域');
830
+ if (pathE.length > 3) addResults(pathE.slice(3), '⏰ 时间线');
831
+
832
+ const finalResults = ordered.map(k => seen.get(k));
833
+
834
+ return {
835
+ query,
836
+ results: finalResults.slice(0, 30),
837
+ totalResults: finalResults.length,
838
+ pathStats: {
839
+ pathA: { count: paths.pathA?.count || 0, elapsed: paths.pathA?.elapsed || 0 },
840
+ pathB: { count: paths.pathB?.results?.length || 0, elapsed: paths.pathB?.elapsed || 0 },
841
+ pathC: { count: paths.pathC?.count || 0, elapsed: paths.pathC?.elapsed || 0 },
842
+ pathD: { count: paths.pathD?.count || 0, elapsed: paths.pathD?.elapsed || 0 },
843
+ pathE: { count: paths.pathE?.count || 0, elapsed: paths.pathE?.elapsed || 0 },
844
+ },
845
+ totalElapsed: Object.values(paths).reduce((sum, p) => sum + (p?.elapsed || 0), 0),
846
+ };
847
+ }
848
+
849
+ // ====== ─── 主入口:五路并行搜索 ─── ======
850
+
851
+ /**
852
+ * 多路径并行搜索入口
853
+ *
854
+ * 同时执行 A/B/C/D/E 五路搜索,融合结果
855
+ *
856
+ * @param {object} params
857
+ * @param {string} params.query - 搜索关键词(必填)
858
+ * @param {string} [params.timeRange] - 时间范围: 3d|1w|1m|all
859
+ * @param {number} [params.limit] - 每路径结果上限(默认10)
860
+ * @param {boolean} [params.enablePathB] - 启用语义同义词展开(默认true)
861
+ * @param {boolean} [params.enablePathC] - 启用失败经验搜索(默认true)
862
+ * @param {boolean} [params.enablePathD] - 启用跨区域搜索(默认true)
863
+ * @param {boolean} [params.enablePathE] - 启用时间线回溯(默认true)
864
+ * @param {boolean} [params.fuseOnly] - 仅融合已传入的paths(不执行搜索)
865
+ * @param {object} [params.paths] - 当 fuseOnly=true 时传入
866
+ * @param {object} pool - CpuWorkerPool 实例
867
+ * @returns {Promise<object>} 融合后的搜索结果
868
+ */
869
+ export async function multiPathSearch(params, pool) {
870
+ const query = params.query;
871
+ if (!query) return { status: 'error', error: 'missing query 参数' };
872
+
873
+ const timeRange = params.timeRange || 'all';
874
+ const limit = Math.min(params.limit || 10, 30);
875
+ const enablePathB = params.enablePathB !== false;
876
+ const enablePathC = params.enablePathC !== false;
877
+ const enablePathD = params.enablePathD !== false;
878
+ const enablePathE = params.enablePathE !== false;
879
+
880
+ // 快速路径:仅融合已有结果
881
+ if (params.fuseOnly && params.paths) {
882
+ return {
883
+ status: 'success',
884
+ ...fuseResults(params.paths, query),
885
+ mode: 'fuse_only',
886
+ };
887
+ }
888
+
889
+ const startTime = Date.now();
890
+
891
+ // 并行执行五路搜索(不互相依赖,可全并行)
892
+ const tasks = [
893
+ pathA_MainSearch(query, pool, { limit: limit + 5, timeRange }).catch(err => ({
894
+ results: [], elapsed: 0, count: 0, error: err.message,
895
+ })),
896
+ ];
897
+
898
+ if (enablePathB) {
899
+ tasks.push(
900
+ pathB_SynonymSearch(query, pool, { limit: limit + 3 }).catch(err => ({
901
+ results: [], branches: [], elapsed: 0, error: err.message,
902
+ }))
903
+ );
904
+ }
905
+
906
+ if (enablePathC) {
907
+ tasks.push(
908
+ pathC_FailureSearch(query, pool, { limit: limit + 3, timeRange }).catch(err => ({
909
+ results: [], elapsed: 0, count: 0, error: err.message,
910
+ }))
911
+ );
912
+ }
913
+
914
+ if (enablePathD) {
915
+ tasks.push(
916
+ pathD_CrossRegionSearch(query, { limit: limit + 3 }).catch(err => ({
917
+ results: [], elapsed: 0, count: 0, error: err.message,
918
+ }))
919
+ );
920
+ }
921
+
922
+ if (enablePathE) {
923
+ tasks.push(
924
+ pathE_TimelineBacktrack(query, pool, { limit: limit + 3, timeRange }).catch(err => ({
925
+ results: [], elapsed: 0, count: 0, error: err.message,
926
+ }))
927
+ );
928
+ }
929
+
930
+ const settled = await Promise.allSettled(tasks);
931
+
932
+ const paths = {};
933
+ const pathResults = settled.map((s, i) => {
934
+ const names = ['pathA', 'pathB', 'pathC', 'pathD', 'pathE'];
935
+ return { name: names[i], result: s.status === 'fulfilled' ? s.value : { error: s.reason?.message, elapsed: 0 } };
936
+ });
937
+
938
+ for (const pr of pathResults) {
939
+ paths[pr.name] = pr.result;
940
+ }
941
+
942
+ // 融合
943
+ const fused = fuseResults(paths, query);
944
+ fused.totalElapsed = Date.now() - startTime;
945
+
946
+ return {
947
+ status: 'success',
948
+ query,
949
+ timeRange,
950
+ ...fused,
951
+ mode: 'full_multi_path',
952
+ };
953
+ }
954
+
955
+ // ====== ─── CLI 入口 ─── ======
956
+
957
+ async function main() {
958
+ const args = process.argv.slice(2);
959
+ if (args.length === 0 || args[0] === '--help') {
960
+ // TODO: 移除调试日志 console.log(`
961
+ // 🧠 hippocampus-multi-search.js — 多路径跨区域搜索引擎
962
+ // // 用法:
963
+ // node hippocampus-multi-search.js <query> [options]
964
+ // // 选项:
965
+ // --timeRange 3d|1w|1m|all 时间范围 (默认 all)
966
+ // --limit <n> 每路径结果上限 (默认 10)
967
+ // --disableB 禁用同义词展开
968
+ // --disableC 禁用失败经验搜索
969
+ // --disableD 禁用跨区域搜索
970
+ // --disableE 禁用时间线回溯
971
+ // --dry-run 只输出路径规划, 不执行搜索
972
+ // // 示例:
973
+ // node hippocampus-multi-search.js "数据库配置"
974
+ // node hippocampus-multi-search.js "error timeout" --timeRange 1w
975
+ // node hippocampus-multi-search.js "sc" --limit 20
976
+ // `);
977
+ return;
978
+ }
979
+
980
+ const query = args[0];
981
+ const options = {
982
+ query,
983
+ timeRange: 'all',
984
+ limit: 10,
985
+ enablePathB: !args.includes('--disableB'),
986
+ enablePathC: !args.includes('--disableC'),
987
+ enablePathD: !args.includes('--disableD'),
988
+ enablePathE: !args.includes('--disableE'),
989
+ };
990
+
991
+ const trIdx = args.indexOf('--timeRange');
992
+ if (trIdx >= 0 && args[trIdx + 1]) options.timeRange = args[trIdx + 1];
993
+
994
+ const liIdx = args.indexOf('--limit');
995
+ if (liIdx >= 0 && args[liIdx + 1]) options.limit = parseInt(args[liIdx + 1], 10);
996
+
997
+ if (args.includes('--dry-run')) {
998
+ // TODO: 移除调试日志 console.log(JSON.stringify({
999
+ // status: 'dry_run',
1000
+ // query: options.query,
1001
+ // paths: {
1002
+ // pathA: '主关键词搜索',
1003
+ // pathB: options.enablePathB ? '同义词展开搜索' : '已禁用',
1004
+ // pathC: options.enablePathC ? '失败经验搜索' : '已禁用',
1005
+ // pathD: options.enablePathD ? '跨区域关联搜索' : '已禁用',
1006
+ // pathE: options.enablePathE ? '时间线回溯搜索' : '已禁用',
1007
+ // },
1008
+ // }, null, 2));
1009
+ return;
1010
+ }
1011
+
1012
+ // CLI 模式需要 pool 参数,这里提示
1013
+ // TODO: 移除调试日志 console.log(JSON.stringify({
1014
+ // status: 'info',
1015
+ // message: 'CLI 模式需要 Worker pool 实例,请在sc环境下通过 MCP 调用 multi_search action',
1016
+ // usage: '通过 agent_memory_retriever(action: "multi_search", query: "...") 调用',
1017
+ // }, null, 2));
1018
+ }
1019
+
1020
+ // 直接运行
1021
+ if (process.argv[1] && (process.argv[1].endsWith('hippocampus-multi-search.js'))) {
1022
+ main().catch(err => {
1023
+ console.error(JSON.stringify({ status: 'error', error: err.message }, null, 2));
1024
+ process.exit(1);
1025
+ });
1026
+ }
1027
+
1028
+ export default {
1029
+ multiPathSearch,
1030
+ pathA_MainSearch,
1031
+ pathB_SynonymSearch,
1032
+ pathC_FailureSearch,
1033
+ pathD_CrossRegionSearch,
1034
+ pathE_TimelineBacktrack,
1035
+ fuseResults,
1036
+ expandQueryTerms,
1037
+ SYNONYM_MAP,
1038
+ };