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,375 @@
1
+ /**
2
+ * 🛡️ sc — 子agent任务话术enrichment layer (Subagent Task Enrichment)
3
+ *
4
+ * 功能:在 spawn 子 agent 前调用,给任务描述注入工具推荐。
5
+ * 子 agent 拿到任务后直接看见工具推荐,不用自己瞎猜用什么工具。
6
+ *
7
+ * 设计意图:
8
+ * 子 agent(尤其是低配模型 or thinking=off)经常不知道用什么工具,
9
+ * 靠猜容易选错工具,导致质量差、token浪费。
10
+ * enrichment layer方案 = 在 spawn 之前「包一层」工具推荐提示,
11
+ * 子 agent 看一眼就知道该用什么,不浪费推理资源。
12
+ *
13
+ * 架构三层:
14
+ * 1. Tcell 缓存层(独立实例,阈值 0.75)— 相同任务指纹命中即返回
15
+ * 2. 小树 routeQuick(Worker 关键词匹配)— 50ms 内快速识别工具
16
+ * 3. 降级层 — 任何异常返回原始 taskText,不阻塞 spawn
17
+ *
18
+ * @module tool-enrich-subagent
19
+ */
20
+
21
+ import { computeSignature, extractKeywords, createEnrichTcell } from './tcell.js';
22
+ import { CORE_TOOLS, TASK_TIMEOUT_MAP } from './constants.js';
23
+ import { registerToolTier } from './steward-rules.js';
24
+
25
+ // ====== 默认兜底候选列表(当小树匹配但置信度 < 0.9 时用)======
26
+ // 设计原因:小树匹配一个工具但置信度不够时,不能只给一个候选。
27
+ // 用兜底候选表补 2 个近似工具,让子 agent 有选择空间。
28
+ // 🔧 Fix-11: 修复已删工具映射,所有 value 指向存在的工具
29
+ // - cpu_orchestrate 已删除(原含禁调工具 core_taskPipeline)
30
+ const FALLBACK_CANDIDATES = {
31
+ 'core_search': [
32
+ { tool: 'web_search', confidence: 0.7, reason: '网络搜索兜底(本地搜不到时转向网络)' },
33
+ { tool: 'core_webSearch', confidence: 0.6, reason: '网络搜索兜底(cpu_orchestrate不存在,改用core_webSearch)' },
34
+ ],
35
+ 'web_search': [
36
+ { tool: 'tavily_search', confidence: 0.7, reason: '高级搜索引擎(精确检索)' },
37
+ { tool: 'web_fetch', confidence: 0.65, reason: '网页内容提取(打开指定链接)' },
38
+ ],
39
+ 'core_batch': [
40
+ { tool: 'core_spawnWorker', confidence: 0.7, reason: 'Worker池并行处理(批量任务分派)' },
41
+ { tool: 'core_search', confidence: 0.6, reason: '文件全文搜索(批量读不如精准搜)' },
42
+ ],
43
+ 'cpu_dialogRecall': [
44
+ { tool: 'core_memorySearch', confidence: 0.7, reason: '记忆语义搜索(讨论过的话题)' },
45
+ { tool: 'core_search', confidence: 0.55, reason: '文件全文搜索(日记文件内搜索)' },
46
+ ],
47
+ 'core_processLog': [
48
+ { tool: 'core_memorySearch', confidence: 0.65, reason: '记忆搜索(日志模式分析检索)' },
49
+ { tool: 'core_search', confidence: 0.55, reason: '文件全文搜索(日志文件内搜索)' },
50
+ ],
51
+ 'core_diagnose': [
52
+ { tool: 'core_batchVision', confidence: 0.65, reason: '批量看图诊断(视觉分析兜底)' },
53
+ { tool: 'core_search', confidence: 0.5, reason: '文件搜索(协助定位问题文件)' },
54
+ ],
55
+ 'default': [
56
+ { tool: 'core_search', confidence: 0.55, reason: '通用文件搜索(万能兜底)' },
57
+ { tool: 'core_webSearch', confidence: 0.5, reason: '网络搜索(万能兜底)' },
58
+ ],
59
+ };
60
+
61
+ /**
62
+ * 构建话术enrichment layer文本
63
+ * 将工具推荐以追加形式注入原始任务描述
64
+ *
65
+ * 设计意图:不修改原始任务文本结构,只在其后追加一段「enrichment layer推荐」。
66
+ * 子 agent 读到这段就知道该用什么工具、为什么用、备选方案是什么。
67
+ *
68
+ * @param {string} taskText - 原始任务描述
69
+ * @param {Array} recommendations - 推荐列表 [{tool, confidence, reason}]
70
+ * @param {boolean} fromTcell - 是否来自缓存命中
71
+ * @returns {string} 增强版任务描述(原始文本 + enrichment layer)
72
+ */
73
+ function buildEnrichedTask(taskText, recommendations, fromTcell) {
74
+ // 无推荐 → 原样返回,不追加任何内容
75
+ if (!recommendations || recommendations.length === 0 || !recommendations[0]) {
76
+ return taskText;
77
+ }
78
+
79
+ const top = recommendations[0]; // 主推荐(最高置信度的工具)
80
+
81
+ // 话术enrichment layer格式:\n\n 隔离让子 agent 知道这是系统注入的推荐
82
+ let shieldText = '\n\n【🛡️ 话术enrichment layer — 工具推荐】\n';
83
+
84
+ if (fromTcell) {
85
+ // Tcell 缓存命中:高置信度(0.95),直接推荐
86
+ shieldText += `推荐工具: ${top.tool}\n`;
87
+ shieldText += `置信度: 0.95 (缓存命中)\n`;
88
+ shieldText += `建议: 执行此任务时优先使用 ${top.tool}\n`;
89
+ } else {
90
+ // 小树匹配:带理由 + 置信度
91
+ shieldText += `推荐工具: ${top.tool}\n`;
92
+ shieldText += `置信度: ${typeof top.confidence === 'number' ? top.confidence.toFixed(2) : 'N/A'}\n`;
93
+ shieldText += `理由: ${top.reason || '小树关键词匹配'}\n`;
94
+
95
+ // 中低置信度时提供备用候选(子 agent 可自行选择)
96
+ if (recommendations.length > 1) {
97
+ shieldText += '\n备用候选:\n';
98
+ for (let i = 1; i < recommendations.length; i++) {
99
+ const r = recommendations[i];
100
+ shieldText += ` ${i}. ${r.tool} (置信度 ${typeof r.confidence === 'number' ? r.confidence.toFixed(2) : 'N/A'})\n`;
101
+ }
102
+ }
103
+ }
104
+
105
+ // 引导句:子 agent 看了就知道可以自由选工具
106
+ shieldText += '\n💡 你可以直接用推荐的工具,也可以根据任务自由选择。\n';
107
+
108
+ return taskText + shieldText;
109
+ }
110
+
111
+ /**
112
+ * 构建 Top-3 多候选列表
113
+ * 当小树匹配置信度 < 0.9 时,生成多候选项让子 agent 自行选择
114
+ *
115
+ * @param {object} quickResult - routeQuick 返回值 { tool, confidence, params }
116
+ * @returns {Array} 推荐列表(最多 3 个)
117
+ */
118
+ function buildTop3Candidates(quickResult) {
119
+ // 小树已匹配的工具作为第 1 候选
120
+ const candidates = [{
121
+ tool: quickResult.tool,
122
+ confidence: quickResult.confidence,
123
+ reason: `小树关键词匹配(置信度 ${quickResult.confidence.toFixed(2)})`,
124
+ }];
125
+
126
+ // 按工具类型补 2 个近似候选
127
+ const fallbacks = FALLBACK_CANDIDATES[quickResult.tool] || FALLBACK_CANDIDATES['default'];
128
+ for (const fb of fallbacks.slice(0, 2)) {
129
+ candidates.push(fb);
130
+ }
131
+
132
+ return candidates;
133
+ }
134
+
135
+ /**
136
+ * 注册 cpu_enrichSubagentTask 工具到 OpenClaw
137
+ *
138
+ * @param {object} ctx - OpenClaw 插件上下文
139
+ * @param {object} deps - 依赖注入 { pool }
140
+ * @param {object} deps.pool - sc Worker 池(用于派发 routeQuick)
141
+ */
142
+ export async function register(ctx, deps = {}) {
143
+ const { pool } = deps;
144
+
145
+ // 延迟初始化 enrichTcell(不阻塞 plugin activate)
146
+ // 放在工具第一次执行时懒加载
147
+ let enrichTcell = null;
148
+ let enrichTcellReady = false;
149
+ // 🧠 保存 init Promise 以便 execute 中等待初始化完成
150
+ // 设计原因:createEnrichTcell 在 register 阶段发起异步初始化,
151
+ // 如果 execute 在初始化完成前被调用,enrichTcell 为 null。
152
+ // 用 enrichTcellInitPromise 让 execute 主动 await 等待,避免第一批命中丢失。
153
+ let enrichTcellInitPromise = null;
154
+
155
+ // 异步初始化 enrichTcell
156
+ enrichTcellInitPromise = createEnrichTcell().then(tcell => {
157
+ enrichTcell = tcell;
158
+ enrichTcellReady = true;
159
+ return tcell; // 返回 tcell 以便 execute 中 await 获取
160
+ }).catch(err => {
161
+ // enrichTcell init failed → 降级为无缓存模式,不影响主功能
162
+ enrichTcellReady = false;
163
+ console.warn(`[sc] ⚠️ enrichment layerTcellinit failed(降级为无缓存): ${err.message}`);
164
+ return null;
165
+ });
166
+
167
+ /**
168
+ * 🛡️ cpu_enrichSubagentTask:子agent任务话术enrichment layer
169
+ *
170
+ * 主 agent 在 spawn 子 agent 前调用此工具,给任务描述注入工具推荐。
171
+ * 子 agent 拿到增强版任务描述后直接看到推荐工具,不自己猜。
172
+ *
173
+ * 输入:
174
+ * taskText - 原始子 agent 任务描述(必填)
175
+ * returnOnly - 可选,true 时仅返回结构化推荐不生成enrichment layer文本
176
+ *
177
+ * 输出:
178
+ * status - "success" | "degraded"
179
+ * enrichedTask - 增强版任务描述(returnOnly=true 时为 null)
180
+ * recommendations - 结构化推荐列表
181
+ * tcellHit - 是否 Tcell 缓存命中
182
+ * confidence - 综合置信度
183
+ * bigTreePending - 是否正在异步跑大树兜底
184
+ */
185
+ ctx.registerTool({
186
+ name: 'cpu_enrichSubagentTask',
187
+ description: '🛡️ 子agent任务话术enrichment layer — 在spawn子agent前调用此工具,给任务描述注入工具推荐。返回增强版任务描述和结构化推荐列表。子agent拿到enrichment layer后直接看到该用什么工具。',
188
+ parameters: {
189
+ type: 'object',
190
+ properties: {
191
+ taskText: {
192
+ type: 'string',
193
+ description: '原始子agent任务描述文本',
194
+ },
195
+ returnOnly: {
196
+ type: 'boolean',
197
+ description: '仅返回结构化推荐而不生成enrichedTask文本(默认false)',
198
+ },
199
+ },
200
+ required: ['taskText'],
201
+ },
202
+ async execute(_toolCallId, params) {
203
+ const { taskText, returnOnly } = params || {};
204
+
205
+ // edges界情况:空/非法 taskText → 直接返回原始文本
206
+ // 设计原因:enrichment layer不应因输入异常而阻塞 spawn 流程
207
+ if (!taskText || typeof taskText !== 'string') {
208
+ return {
209
+ status: 'success',
210
+ enrichedTask: taskText || '',
211
+ recommendations: [],
212
+ tcellHit: false,
213
+ confidence: 0,
214
+ note: 'taskText为空,跳过enrichment layer',
215
+ };
216
+ }
217
+
218
+ try {
219
+ // ====== 第一步:计算任务指纹 ======
220
+ // 使用 enrich_subagent 作为独立签名空间,与原 tcell 不冲突
221
+ const keywords = extractKeywords(taskText);
222
+ const sig = computeSignature('enrich_subagent', keywords);
223
+
224
+ // ====== 第二步:Tcell 缓存查询(独立实例,阈值 0.75)======
225
+ // 设计原因:相同任务文本多次 enrich 时,直接返回缓存结果
226
+ // 省去 routeQuick 的 Worker 调用。每次约省 50ms。
227
+ // Tcell 命中给 0.95 固定置信度(保留 5% 不确定性)
228
+ // 🔧 Bug 3 修复:如果 enrichTcell 尚未异步初始化完成,等待其初始化再查缓存。
229
+ // 设计原因:createEnrichTcell 在 register 阶段发起异步初始化,
230
+ // 如果 execute 在初始化完成前被调用,enrichTcell 为 null。
231
+ // 传统检查 enrichTcellReady 会跳过缓存层,导致前几次命中丢失。
232
+ // 主动 await initPromise 可让首次命中也能走缓存。
233
+ if (!enrichTcellReady && enrichTcellInitPromise) {
234
+ // 🧠 还在初始化中,等待完成
235
+ const t = await enrichTcellInitPromise;
236
+ if (t) {
237
+ enrichTcell = t;
238
+ enrichTcellReady = true;
239
+ }
240
+ }
241
+ if (enrichTcellReady && enrichTcell) {
242
+ const cachedResult = enrichTcell.lookup(sig);
243
+ if (cachedResult && cachedResult.hit && cachedResult.entry) {
244
+ // 从缓存中提取之前推荐的 tool 构建推荐列表
245
+ const cachedTool = cachedResult.entry.tool;
246
+ const rec = [{
247
+ tool: cachedTool,
248
+ confidence: 0.95,
249
+ reason: '话术enrichment layer缓存命中(任务指纹匹配)',
250
+ }];
251
+
252
+ // 缓存命中 → 直接返回,跳过 routeQuick
253
+ return {
254
+ status: 'success',
255
+ enrichedTask: returnOnly ? null : buildEnrichedTask(taskText, rec, true),
256
+ recommendations: rec,
257
+ tcellHit: true,
258
+ confidence: 0.95,
259
+ };
260
+ }
261
+ }
262
+
263
+ // ====== 第三步:小树 routeQuick 匹配(Worker 池并行)======
264
+ // 设计原因:Tcell 未命中时走小树,50ms 内完成关键词匹配
265
+ // 用 pool.exec 派到 Worker 线程执行,不阻塞主线程
266
+ let quickResult;
267
+ try {
268
+ quickResult = await pool.exec({ type: 'route-quick', text: taskText }, 'high');
269
+ } catch (poolErr) {
270
+ // pool.exec 失败(如队列满、Worker 挂起等)→ 降级返回原始文本
271
+ // 走兜底路径:两 default 候选
272
+ const fallbackRec = [
273
+ { tool: 'core_search', confidence: 0.55, reason: '通用文件搜索(兜底)' },
274
+ { tool: 'core_webSearch', confidence: 0.5, reason: '网络搜索(兜底)' },
275
+ ];
276
+ return {
277
+ status: 'degraded',
278
+ enrichedTask: returnOnly ? null : taskText,
279
+ recommendations: fallbackRec,
280
+ tcellHit: false,
281
+ confidence: 0.5,
282
+ bigTreePending: false,
283
+ note: `enrichment layer降级(pool.exec失败): ${poolErr.message}`,
284
+ };
285
+ }
286
+
287
+ // 小树完全不匹配
288
+ if (!quickResult || !quickResult.matched) {
289
+ return {
290
+ status: 'success',
291
+ enrichedTask: null, // 无推荐,不注入enrichment layer
292
+ recommendations: [], // 空推荐列表
293
+ tcellHit: false,
294
+ confidence: 0,
295
+ note: '小树无匹配,无工具推荐。子 agent 自行选择工具。',
296
+ };
297
+ }
298
+
299
+ // ====== 第四步:根据置信度组织推荐列表 ======
300
+ let recommendations;
301
+ let bigTreePending = false;
302
+
303
+ if (quickResult.confidence >= 0.9) {
304
+ // ✅ 高置信度 → 单推荐(最可能的工具)
305
+ recommendations = [{
306
+ tool: quickResult.tool,
307
+ confidence: quickResult.confidence,
308
+ reason: `小树关键词匹配(识别为 ${quickResult.params?.level || '通用'} 级任务)`,
309
+ }];
310
+ } else {
311
+ // ⚠️ 中低置信度 → Top-3 多候选,推荐仅供参考
312
+ recommendations = buildTop3Candidates(quickResult);
313
+ bigTreePending = true;
314
+ }
315
+
316
+ // 更新 Tcell 缓存(记录本次匹配结果以供下次使用)
317
+ if (enrichTcellReady && enrichTcell && recommendations.length > 0) {
318
+ enrichTcell.add(sig, recommendations[0].tool, 'enrich_subagent', taskText, {
319
+ confidence: quickResult.confidence,
320
+ });
321
+ }
322
+
323
+ // ====== 第五步:构建返回结果 ======
324
+ const enrichedTask = returnOnly
325
+ ? null
326
+ : buildEnrichedTask(taskText, recommendations, false);
327
+
328
+ return {
329
+ status: 'success',
330
+ enrichedTask,
331
+ recommendations,
332
+ tcellHit: false,
333
+ confidence: quickResult.confidence || 0.5,
334
+ bigTreePending,
335
+ note: bigTreePending
336
+ ? `中置信度匹配 (${quickResult.confidence.toFixed(2)}),推荐仅供参考,子agent自行判断`
337
+ : `小树匹配: ${quickResult.tool} (置信度 ${quickResult.confidence.toFixed(2)})`,
338
+ };
339
+ } catch (err) {
340
+ // ====== 异常熔断:任何错误降级返回原始 taskText ======
341
+ // 设计原因:enrichment layer是「锦上添花」功能,失败不应阻塞 spawn
342
+ // 主 agent 拿到的 enrichedTask 保险丝后仍是原始文本
343
+ console.error(`[sc] ❌ cpu_enrichSubagentTask 出错: ${err.message}`);
344
+ return {
345
+ status: 'degraded',
346
+ enrichedTask: taskText, // 降级返回原始文本
347
+ recommendations: [],
348
+ confidence: 0,
349
+ note: `enrichment layer降级: ${err.message}`,
350
+ };
351
+ }
352
+ },
353
+ });
354
+
355
+ // 🛡️ 三件套:白名单 + 超时 + 管家规则
356
+ // 确保子agent能正常调用此工具
357
+ try {
358
+ // 1. 追加到 CORE_TOOLS 白名单(让 before_tool_call 放行)
359
+ if (!CORE_TOOLS.includes('cpu_enrichSubagentTask')) {
360
+ CORE_TOOLS.push('cpu_enrichSubagentTask');
361
+ }
362
+
363
+ // 2. 超时配置(短期查询,无需长时间超时)
364
+ if (!TASK_TIMEOUT_MAP['cpu_enrichSubagentTask']) {
365
+ TASK_TIMEOUT_MAP['cpu_enrichSubagentTask'] = { warn: 10, kill: 30, label: '子agentenrichment layer' };
366
+ }
367
+
368
+ // 3. 注册管家规则(safe 等级,放行)
369
+ registerToolTier('cpu_enrichSubagentTask', 'safe');
370
+ } catch (err) {
371
+ console.warn(`[sc] ⚠️ enrichment layer三件套注册失败: ${err.message}`);
372
+ }
373
+
374
+ // TODO: 移除调试日志 console.log('[sc] 🛡️ cpu_enrichSubagentTask enrichment layer工具已注册');
375
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * 🦞 sc v5.37.0 — 共享工具处理器 (ESM)
3
+ *
4
+ * 从 index.js 和 tools/bridge.js 提取的公共工具执行逻辑,
5
+ * 减少双份维护、保证行为一致。
6
+ *
7
+ * 每个 handler 签名:
8
+ * async handleXxx(params, pool) → result
9
+ *
10
+ * params — 调用方透传的参数字典(对象)
11
+ * pool — CpuWorkerPool 实例(也可传入 mock/stub)
12
+ *
13
+ * 注:pool 必须提供
14
+ * .exec(task, priority) — 派任务给 Worker
15
+ * .getStats() — 返回池状态
16
+ *
17
+ * 🧹 已清理已删除工具对应的 handler(2026-06-10):
18
+ * 删除: handleCoreSearch, handleCoreProcessLog, handleCoreResolveModel,
19
+ * handleCoreDiff, handleCoreBatch, handleCoreDiagnose, handleCoreScan,
20
+ * handleCoreWorkspaceSync
21
+ * 保留: handleCoreStats(core_stats), handleCoreImageBatch(core_batchVision)
22
+ */
23
+
24
+ import { resolve, basename, join } from 'path';
25
+ import { homedir } from 'os';
26
+ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
27
+ import { validatePath } from '../security.js';
28
+
29
+ // ========================================================================
30
+ // 1. core_stats — Worker 池状态快照
31
+ // ========================================================================
32
+ /**
33
+ * @param {object} params - (unused, kept for uniform signature)
34
+ * @param {object} pool
35
+ */
36
+ export async function handleCoreStats(params, pool) {
37
+ return pool.getStats();
38
+ }
39
+
40
+ // ========================================================================
41
+ // 2. core_batchVision (cpu_imageBatch) — 多核图片批量分析
42
+ // ========================================================================
43
+ /**
44
+ * 多Worker并行压缩图片→调视觉API→合并结果
45
+ * @param {object} params - { files: string[], prompt?: string, model?: string, priority?: string }
46
+ * @param {object} pool
47
+ */
48
+ /**
49
+ * 从 openclaw.json 读取默认视觉模型配置
50
+ */
51
+ function getDefaultVisionModel() {
52
+ try {
53
+ const configPath = resolve(homedir(), '.openclaw', 'openclaw.json');
54
+ const raw = readFileSync(configPath, 'utf8');
55
+ const config = JSON.parse(raw);
56
+ const model = config?.tools?.media?.image?.models?.[0]?.model;
57
+ if (model) return model;
58
+ } catch (err) {
59
+ // 读取失败就返回默认值
60
+ }
61
+ return 'qwen3-vl:8b';
62
+ }
63
+
64
+ /**
65
+ * inbound 图片目录
66
+ */
67
+ const MEDIA_INBOUND_DIR = resolve(homedir(), '.openclaw', 'media', 'inbound');
68
+
69
+ /**
70
+ * 解析图片路径:先当绝对路径试,不行就去 inbound 目录按文件名找
71
+ */
72
+ function resolveImagePath(filePath) {
73
+ // 去掉协议头(media://、file:// 等)
74
+ let cleanPath = filePath;
75
+ if (filePath.includes('://')) {
76
+ cleanPath = filePath.split('://').slice(1).join('://');
77
+ }
78
+
79
+ // 先试绝对路径
80
+ let absPath = resolve(cleanPath);
81
+ if (existsSync(absPath)) return absPath;
82
+
83
+ // 再试相对于 workspace 的路径
84
+ const wsPath = resolve(homedir(), '.openclaw', 'workspace', cleanPath);
85
+ if (existsSync(wsPath)) return wsPath;
86
+
87
+ // 最后去 inbound 目录按文件名找
88
+ const fileName = basename(cleanPath);
89
+ const inboundPath = join(MEDIA_INBOUND_DIR, fileName);
90
+ if (existsSync(inboundPath)) return inboundPath;
91
+
92
+ // 真找不到就抛原路径,让下游报错
93
+ return absPath;
94
+ }
95
+
96
+ /**
97
+ * 获取 inbound 目录下最新的 N 个文件
98
+ */
99
+ function getLatestInboundFiles(count = 10) {
100
+ if (!existsSync(MEDIA_INBOUND_DIR)) return [];
101
+ const entries = readdirSync(MEDIA_INBOUND_DIR)
102
+ .filter(name => /\.(png|jpg|jpeg|gif|webp|bmp)$/i.test(name))
103
+ .map(name => ({
104
+ name,
105
+ path: join(MEDIA_INBOUND_DIR, name),
106
+ mtime: statSync(join(MEDIA_INBOUND_DIR, name)).mtimeMs,
107
+ }))
108
+ .sort((a, b) => b.mtime - a.mtime);
109
+ return entries.slice(0, count).map(e => e.path);
110
+ }
111
+
112
+ export async function handleCoreImageBatch(params, pool) {
113
+ let files = params.files;
114
+
115
+ // 支持 "latest" / "current" 语义:解析为 inbound 目录最新一张图
116
+ if (Array.isArray(files) && files.length > 0) {
117
+ const resolvedLatest = [];
118
+ for (const f of files) {
119
+ if (f === 'latest' || f === 'current') {
120
+ const latestFiles = getLatestInboundFiles(1);
121
+ if (latestFiles.length > 0) {
122
+ resolvedLatest.push(latestFiles[0]);
123
+ }
124
+ } else {
125
+ resolvedLatest.push(f);
126
+ }
127
+ }
128
+ files = resolvedLatest;
129
+ }
130
+
131
+ // 没传 files → 去 inbound 目录读最新 1 张(不是 10 张,避免 GPU 显存爆炸)
132
+ if (!files || !Array.isArray(files) || files.length === 0) {
133
+ files = getLatestInboundFiles(1);
134
+ if (files.length === 0) {
135
+ throw new Error('未传 files,且 media/inbound/ 目录为空或不存在。发送图片后调用此工具即可自动识别最新一张。');
136
+ }
137
+ }
138
+
139
+ // 路径解析:协议路径 → 真实磁盘路径
140
+ const resolvedFiles = files.map(f => resolveImagePath(f));
141
+ for (const f of resolvedFiles) await validatePath(f);
142
+
143
+ const model = params.model || getDefaultVisionModel();
144
+
145
+ // 🔁 串行处理(不是并行)—— 每张图依次走 Worker,防止多图并发砸 GPU 显存
146
+ const output = [];
147
+ for (let i = 0; i < resolvedFiles.length; i++) {
148
+ const filePath = resolvedFiles[i];
149
+ try {
150
+ const result = await pool.exec({
151
+ type: 'image-process',
152
+ imagePath: filePath,
153
+ prompt: params.prompt,
154
+ model: model,
155
+ }, params.priority || 'normal');
156
+ output.push({ file: files[i], ...result });
157
+ } catch (err) {
158
+ output.push({ file: files[i], success: false, error: err?.message || 'Worker 执行失败' });
159
+ }
160
+ }
161
+
162
+ return {
163
+ total: files.length,
164
+ successCount: output.filter(r => r.success).length,
165
+ failCount: output.filter(r => !r.success).length,
166
+ results: output,
167
+ };
168
+ }
169
+
170
+ // ========================================================================
171
+ // 📋 处理函数映射表 — 供 MCP / 路由使用
172
+ // ========================================================================
173
+ export const HANDLER_MAP = {
174
+ cpu_stats: handleCoreStats,
175
+ cpu_imageBatch: handleCoreImageBatch,
176
+ };
177
+
178
+ export default { HANDLER_MAP };