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,1419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦞 sc v5.37.0 — Worker 线程 (ESM + 多核并行决策)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { parentPort, workerData } from "worker_threads";
|
|
6
|
+
import { readFile, stat, realpath, unlink, rmdir, mkdtemp, writeFile, readdir, appendFile } from "fs/promises";
|
|
7
|
+
import { createReadStream } from "fs";
|
|
8
|
+
import { execFile, spawn } from "child_process";
|
|
9
|
+
import { join, resolve, normalize, relative, isAbsolute, dirname } from "path";
|
|
10
|
+
import { homedir, tmpdir } from "os";
|
|
11
|
+
import readline from "readline";
|
|
12
|
+
import { LEVEL_RULES, LEVEL_TOOL_MAP } from "../lib/level-rules.js";
|
|
13
|
+
import { TASK_CATEGORY_MAP, TASK_CATEGORY } from "../lib/constants.js";
|
|
14
|
+
import { routeDecompose as decomposeTaskBody } from "../lib/decomposer.js";
|
|
15
|
+
|
|
16
|
+
// ====== Worker 独立日志系统:console 双写(文件 + 原始 console)======
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
const _wDir = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const WORKER_LOG_DIR = join(_wDir, '..', 'logs');
|
|
20
|
+
const WORKER_LOG_FILE = join(WORKER_LOG_DIR, 'worker-pool.log');
|
|
21
|
+
const WORKER_ERROR_FILE = join(WORKER_LOG_DIR, 'error.log');
|
|
22
|
+
|
|
23
|
+
// 异步写入函数(Worker 内部使用,不依赖主线程 logger)
|
|
24
|
+
const _logQueue = [];
|
|
25
|
+
let _logFlushing = false;
|
|
26
|
+
|
|
27
|
+
async function _flushLogQueue() {
|
|
28
|
+
if (_logFlushing) return;
|
|
29
|
+
_logFlushing = true;
|
|
30
|
+
while (_logQueue.length > 0) {
|
|
31
|
+
const { filePath, line } = _logQueue.shift();
|
|
32
|
+
try {
|
|
33
|
+
await appendFile(filePath, line + '\n', 'utf-8');
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
_logFlushing = false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function _enqueueLog(filePath, line) {
|
|
40
|
+
_logQueue.push({ filePath, line });
|
|
41
|
+
if (_logQueue.length === 1) {
|
|
42
|
+
_flushLogQueue().catch(() => {});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _workerTs() {
|
|
47
|
+
const now = new Date();
|
|
48
|
+
return now.toISOString().replace('T', ' ').substring(0, 19) + '.' +
|
|
49
|
+
String(now.getMilliseconds()).padStart(3, '0');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 异步确保日志目录存在(首次调用时执行)
|
|
53
|
+
let _logDirEnsured = false;
|
|
54
|
+
async function _ensureWorkerLogDir() {
|
|
55
|
+
if (_logDirEnsured) return;
|
|
56
|
+
try {
|
|
57
|
+
const { mkdir } = await import('fs/promises');
|
|
58
|
+
await mkdir(WORKER_LOG_DIR, { recursive: true });
|
|
59
|
+
_logDirEnsured = true;
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
_ensureWorkerLogDir().catch(() => {});
|
|
63
|
+
|
|
64
|
+
// 保存原始 console 方法
|
|
65
|
+
const _origLog = console.log.bind(console);
|
|
66
|
+
const _origWarn = console.warn.bind(console);
|
|
67
|
+
const _origError = console.error.bind(console);
|
|
68
|
+
|
|
69
|
+
// 替换 console.log:文件 + 原始
|
|
70
|
+
console.log = function(...args) {
|
|
71
|
+
const msg = args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ');
|
|
72
|
+
const line = `[${_workerTs()}] [Worker-${workerData.id}] [LOG] ${msg}`;
|
|
73
|
+
_enqueueLog(WORKER_LOG_FILE, line);
|
|
74
|
+
_origLog(...args);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// 替换 console.warn:worker-pool + error 双文件 + 原始
|
|
78
|
+
console.warn = function(...args) {
|
|
79
|
+
const msg = args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ');
|
|
80
|
+
const line = `[${_workerTs()}] [Worker-${workerData.id}] [WARN] ${msg}`;
|
|
81
|
+
_enqueueLog(WORKER_LOG_FILE, line);
|
|
82
|
+
_enqueueLog(WORKER_ERROR_FILE, line);
|
|
83
|
+
_origWarn(...args);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// 替换 console.error:worker-pool + error 双文件 + 原始
|
|
87
|
+
console.error = function(...args) {
|
|
88
|
+
const msg = args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ');
|
|
89
|
+
const line = `[${_workerTs()}] [Worker-${workerData.id}] [ERROR] ${msg}`;
|
|
90
|
+
_enqueueLog(WORKER_LOG_FILE, line);
|
|
91
|
+
_enqueueLog(WORKER_ERROR_FILE, line);
|
|
92
|
+
_origError(...args);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// ====== 任务分类器 + 探索型 partialResult 存储 ======
|
|
96
|
+
const partialResultStore = new Map(); // jobId → { progress, ... }
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 分类任务类型(图灵可判定终止保证)
|
|
100
|
+
* 有限型 → 严格超时 | 件型 → 幂等retry | 探索型 → checkpoint | 交互型 → 等待
|
|
101
|
+
*/
|
|
102
|
+
/**
|
|
103
|
+
* 分类任务类型(图灵可判定终止保证)
|
|
104
|
+
* 有限型 → 严格超时 | 件型 → 幂等retry | 探索型 → checkpoint | 交互型 → 等待
|
|
105
|
+
*
|
|
106
|
+
* 🧠 先查 TASK_CATEGORY_MAP(精确匹配),不在 map 中的工具走关键词兜底。
|
|
107
|
+
* 不在 map 中的工具不是忘记加——是有意不加(见 constants.js TASK_CATEGORY_MAP 注释)。
|
|
108
|
+
* 兜底关键词匹配确保新工具或非常用工具也能被分类。
|
|
109
|
+
* 兜底不到的一律归为 FINITE(最保守的安全侧)。
|
|
110
|
+
*/
|
|
111
|
+
function classifyTaskType(taskType) {
|
|
112
|
+
if (TASK_CATEGORY_MAP[taskType]) return TASK_CATEGORY_MAP[taskType];
|
|
113
|
+
const kw = (taskType || '').toLowerCase();
|
|
114
|
+
if (/research|orchestrate|pipeline|explor/i.test(kw)) return TASK_CATEGORY.EXPLORATORY;
|
|
115
|
+
if (/ping|resolve|route|dispatch/i.test(kw)) return TASK_CATEGORY.CONDITIONAL;
|
|
116
|
+
if (/interact|wait|pause/i.test(kw)) return TASK_CATEGORY.INTERACTIVE;
|
|
117
|
+
return TASK_CATEGORY.FINITE;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 存储探索型任务的中间进度(供请求-响应协议使用)
|
|
122
|
+
*/
|
|
123
|
+
function storePartial(jobId, data) {
|
|
124
|
+
const existing = partialResultStore.get(jobId) || {};
|
|
125
|
+
partialResultStore.set(jobId, { ...existing, ...data, updatedAt: Date.now() });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ====== Embedding 配置(从系统配置读取,不硬编码) ======
|
|
129
|
+
import { getEmbeddingConfig, getVisionConfig } from '../lib/config.js';
|
|
130
|
+
|
|
131
|
+
let _embedCfg = null;
|
|
132
|
+
let _visionCfg = null;
|
|
133
|
+
let _cfgLoadTime = 0;
|
|
134
|
+
const CACHED_CFG_TTL = 60000;
|
|
135
|
+
|
|
136
|
+
/** 懒加载 embedding 配置(缓存 60 s) */
|
|
137
|
+
async function getOllamaConfig() {
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
if (_embedCfg && now - _cfgLoadTime < CACHED_CFG_TTL) {
|
|
140
|
+
return { embed: _embedCfg, vision: _visionCfg };
|
|
141
|
+
}
|
|
142
|
+
_embedCfg = await getEmbeddingConfig();
|
|
143
|
+
_visionCfg = await getVisionConfig();
|
|
144
|
+
_cfgLoadTime = now;
|
|
145
|
+
return { embed: _embedCfg, vision: _visionCfg };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 获取 embedding 模型名,没有配置则报错 */
|
|
149
|
+
async function getEmbeddingModel() {
|
|
150
|
+
const { embed } = await getOllamaConfig();
|
|
151
|
+
if (!embed.embeddingModel) {
|
|
152
|
+
throw new Error('Embedding 模型未配置,请在 openclaw.json 的 models.providers.embedding 中配置向量模型');
|
|
153
|
+
}
|
|
154
|
+
return embed.embeddingModel;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 🧠 设计决策:EMBEDDING_TIMEOUT_MS=30000(30s超时)。
|
|
158
|
+
// Ollama embedding 请求本地(127.0.0.1:11434)平均 1-3 s,
|
|
159
|
+
// 加文件读取最多 5-10 s,30s足够扛过模型冷启动加载。
|
|
160
|
+
// 设太短(10s)会在模型冷启动时频繁retry,设太长(60s)则
|
|
161
|
+
// 排队累积时语义搜索会大量阻塞。
|
|
162
|
+
const EMBEDDING_TIMEOUT_MS = 30000;
|
|
163
|
+
// 🧠 设计决策:EMBEDDING_MAX_RETRIES=2(最多retry2次)。
|
|
164
|
+
// Ollama 本地服务偶尔超时(模型冷加载),retry1次不够,
|
|
165
|
+
// retry3次→缓存已冷启动了的模型已经能用了。2次≈足够覆盖
|
|
166
|
+
// 偶发热加载延迟,又不至于在Ollama宕机时反复撞墙。
|
|
167
|
+
const EMBEDDING_MAX_RETRIES = 2;
|
|
168
|
+
|
|
169
|
+
// ====== 语义搜索配置 ======
|
|
170
|
+
const SEMANTIC_SEARCH_CHUNK_CHARS = 2000; // 每个文件最多取前 N 字符做 embedding
|
|
171
|
+
const SEMANTIC_SEARCH_MAX_FILES = 100; // 单次最多搜索文件数
|
|
172
|
+
|
|
173
|
+
const workerId = workerData.id;
|
|
174
|
+
const ALLOWED_ROOTS = [
|
|
175
|
+
resolve(homedir(), ".openclaw"),
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
const rootsReady = (async () => {
|
|
179
|
+
try {
|
|
180
|
+
const base = join(homedir(), ".openclaw");
|
|
181
|
+
ALLOWED_ROOTS.length = 0;
|
|
182
|
+
ALLOWED_ROOTS.push(await realpath(base));
|
|
183
|
+
} catch {
|
|
184
|
+
ALLOWED_ROOTS.length = 0;
|
|
185
|
+
ALLOWED_ROOTS.push(resolve(homedir(), ".openclaw"));
|
|
186
|
+
}
|
|
187
|
+
})();
|
|
188
|
+
|
|
189
|
+
let portClosed = false;
|
|
190
|
+
|
|
191
|
+
const MAX_CONCURRENT = 3; // 每个Worker最多同时跑3个任务
|
|
192
|
+
const MAX_QUEUE_DEPTH = 50; // 排队队列最大深度,超过后新任务返回 QUEUE_FULL 错误
|
|
193
|
+
let activeCount = 0; // 当前活跃任务数
|
|
194
|
+
const pendingQueue = []; // 排队等待的任务
|
|
195
|
+
|
|
196
|
+
// 🧠 设计决策:CHAIN_TASK_TIMEOUT=120000(链式任务超时2min)。
|
|
197
|
+
// Worker里链式任务(如route-quick→route-system→route-intent链)
|
|
198
|
+
// 每次单个任务超时60s(TASK_TIMEOUT_MAP.default.kill),
|
|
199
|
+
// 2min够3个链式步骤各跑60s。设太短(60s)会在正常3步链
|
|
200
|
+
// 中频繁超时,设太长(5min)会在无限循环链中浪费资源。
|
|
201
|
+
const CHAIN_TASK_TIMEOUT = 120000;
|
|
202
|
+
|
|
203
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
204
|
+
return Promise.race([
|
|
205
|
+
promise,
|
|
206
|
+
new Promise((_, reject) =>
|
|
207
|
+
setTimeout(() => reject(new Error(`任务超时: ${label} (${timeoutMs}ms)`)), timeoutMs)
|
|
208
|
+
),
|
|
209
|
+
]);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
parentPort.on("close", () => { portClosed = true; });
|
|
213
|
+
|
|
214
|
+
function runTask(task) {
|
|
215
|
+
if (portClosed) return;
|
|
216
|
+
activeCount++;
|
|
217
|
+
const { jobId } = task;
|
|
218
|
+
(async () => {
|
|
219
|
+
try {
|
|
220
|
+
const effectiveTimeout = task.timeout || CHAIN_TASK_TIMEOUT;
|
|
221
|
+
const result = await withTimeout(executeTask(task), effectiveTimeout, task.type);
|
|
222
|
+
if (!portClosed) parentPort.postMessage({ jobId, type: "result", data: result });
|
|
223
|
+
} catch (err) {
|
|
224
|
+
if (!portClosed) parentPort.postMessage({
|
|
225
|
+
jobId, type: "error",
|
|
226
|
+
error: JSON.stringify({ code: err.code || "UNKNOWN", message: err.message }),
|
|
227
|
+
});
|
|
228
|
+
} finally {
|
|
229
|
+
activeCount--;
|
|
230
|
+
// 🧠 可判定终止: 任务完成后清理 partialResult 存储
|
|
231
|
+
partialResultStore.delete(jobId);
|
|
232
|
+
// 🧠 [设计决策] Worker内部链式串行:用户定的。子agent返回几千字报告可能会卡,
|
|
233
|
+
// 但Worker返回几十字节不会。串行不卡主线程,所以未改并行。
|
|
234
|
+
// (详见 memory/known-design-decisions.md — Worker池设计)
|
|
235
|
+
if (pendingQueue.length > 0) {
|
|
236
|
+
const next = pendingQueue.shift();
|
|
237
|
+
runTask(next);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
})().catch((err) => {
|
|
241
|
+
if (err?.message) console.error(`[Worker ${workerId}] 并发错误:`, err.message);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
parentPort.on("message", (msg) => {
|
|
246
|
+
if (msg.type === "ping") {
|
|
247
|
+
parentPort.postMessage({ jobId: msg.jobId, type: "result", data: { pong: true, worker: workerId, ts: Date.now() } });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// 🧠 可判定终止: 探索型任务 partialResult 请求-响应协议
|
|
251
|
+
if (msg.type === "requestPartialResult") {
|
|
252
|
+
const partial = partialResultStore.get(msg.jobId);
|
|
253
|
+
if (!portClosed) {
|
|
254
|
+
parentPort.postMessage({
|
|
255
|
+
jobId: msg.jobId,
|
|
256
|
+
type: "partialResult",
|
|
257
|
+
data: partial || { status: 'running', progress: null, workerId, ts: Date.now() }
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (activeCount < MAX_CONCURRENT) {
|
|
263
|
+
runTask(msg);
|
|
264
|
+
} else {
|
|
265
|
+
// 排队等待,不再自动派AI兵(杉哥2026-06-09:AI兵已不走Worker池,无意义)
|
|
266
|
+
pendingQueue.push(msg);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ====== 安全校验 ======
|
|
271
|
+
|
|
272
|
+
async function validatePath(filePath) {
|
|
273
|
+
await rootsReady;
|
|
274
|
+
if (!filePath) throw Object.assign(new Error("路径不能为空"), { code: "BAD_REQUEST" });
|
|
275
|
+
const isWin = process.platform === "win32";
|
|
276
|
+
const norm = p => isWin ? p.toLowerCase() : p;
|
|
277
|
+
|
|
278
|
+
let resolved;
|
|
279
|
+
try {
|
|
280
|
+
resolved = await realpath(filePath);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
if (err.code === "ENOENT") {
|
|
283
|
+
resolved = resolve(normalize(filePath));
|
|
284
|
+
} else {
|
|
285
|
+
throw Object.assign(new Error(`路径解析失败: ${err.message}`), { code: "ACCESS_DENIED" });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const allowed = ALLOWED_ROOTS.some(root => {
|
|
290
|
+
const normRoot = norm(root);
|
|
291
|
+
const normResolved = norm(resolved);
|
|
292
|
+
if (isWin) {
|
|
293
|
+
return normResolved.startsWith(normRoot + "\\") || normResolved === normRoot;
|
|
294
|
+
}
|
|
295
|
+
const rel = relative(root, resolved);
|
|
296
|
+
return !rel.startsWith("..") && !isAbsolute(rel);
|
|
297
|
+
});
|
|
298
|
+
if (!allowed) throw Object.assign(new Error(`路径不在允许范围内: ${filePath}`), { code: "ACCESS_DENIED" });
|
|
299
|
+
return resolved;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 🧠 设计决策:RETRY_DELAY_MS=200(200ms退避基础间隔)。
|
|
303
|
+
// Windows 上 EBUSY/EACCES 通常是短暂的文件锁,200ms足够
|
|
304
|
+
// 等锁释放,同时retry3次总延迟不到1s。不用指数退避是因为
|
|
305
|
+
// 文件锁通常是瞬态的,长退避反而加延迟。
|
|
306
|
+
const RETRY_DELAY_MS = 200;
|
|
307
|
+
// 🧠 设计决策:MAX_RETRIES=3(最多retry3次)。
|
|
308
|
+
// 每次退避间隔递增(200ms→400ms→600ms),3次总等待约1.2s。
|
|
309
|
+
// 1次不够(恰好撞锁),4次+消耗I/O时间。3次折中。
|
|
310
|
+
const MAX_RETRIES = 3;
|
|
311
|
+
|
|
312
|
+
async function readFileRetry(fn, maxRetries = MAX_RETRIES) {
|
|
313
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
314
|
+
try {
|
|
315
|
+
return await fn();
|
|
316
|
+
} catch (err) {
|
|
317
|
+
if ((err.code === 'EBUSY' || err.code === 'EACCES') && i < maxRetries - 1) {
|
|
318
|
+
await new Promise(r => setTimeout(r, RETRY_DELAY_MS * (i + 1)));
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function safeReadJson(path) {
|
|
327
|
+
try {
|
|
328
|
+
const content = await readFileRetry(() => readFile(path, "utf-8"));
|
|
329
|
+
return JSON.parse(content);
|
|
330
|
+
} catch { return null; }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let _configCache = null;
|
|
334
|
+
let _configCacheTime = 0;
|
|
335
|
+
const CONFIG_CACHE_TTL = 30000;
|
|
336
|
+
|
|
337
|
+
async function readModelConfig(provider, modelId) {
|
|
338
|
+
const now = Date.now();
|
|
339
|
+
if (!_configCache || now - _configCacheTime > CONFIG_CACHE_TTL) {
|
|
340
|
+
_configCache = await safeReadJson(join(homedir(), ".openclaw", "openclaw.json"));
|
|
341
|
+
_configCacheTime = now;
|
|
342
|
+
}
|
|
343
|
+
const cfg = _configCache;
|
|
344
|
+
if (!cfg) return null;
|
|
345
|
+
const cleanId = modelId.includes("/") ? modelId.split("/").pop() : modelId;
|
|
346
|
+
const models = cfg?.models?.providers?.[provider]?.models || [];
|
|
347
|
+
return models.find(m => m.id === cleanId) || null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ====== Embedding 功能 ======
|
|
351
|
+
|
|
352
|
+
// 延迟 import similarity(仅在语义搜索时加载)
|
|
353
|
+
// [removed: migrated to FAISS GPU]
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 调 Ollama /api/embeddings 获取文本向量
|
|
357
|
+
* 带retry机制和超时处理
|
|
358
|
+
*/
|
|
359
|
+
async function embedText(text) {
|
|
360
|
+
if (!text || text.trim().length === 0) {
|
|
361
|
+
return { error: 'embedding 文本不能为空' };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 截断超长文本(Ollama embedding 模型最大支持 ~8192 token,~6000 字符安全)
|
|
365
|
+
const safeText = text.substring(0, 6000);
|
|
366
|
+
|
|
367
|
+
// 从配置读取 Ollama baseUrl 和 embedding 模型
|
|
368
|
+
const { embed: ollamaCfg } = await getOllamaConfig();
|
|
369
|
+
const embeddingModel = await getEmbeddingModel();
|
|
370
|
+
const baseUrl = ollamaCfg.baseUrl; // 配置文件里的 baseUrl
|
|
371
|
+
|
|
372
|
+
if (!embeddingModel) {
|
|
373
|
+
return { error: 'embedding 模型未配置,请检查 openclaw.json models.providers.embedding' };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let lastError = null;
|
|
377
|
+
for (let attempt = 0; attempt <= EMBEDDING_MAX_RETRIES; attempt++) {
|
|
378
|
+
try {
|
|
379
|
+
const controller = new AbortController();
|
|
380
|
+
const timeoutId = setTimeout(() => controller.abort(), EMBEDDING_TIMEOUT_MS);
|
|
381
|
+
|
|
382
|
+
const response = await fetch(`${baseUrl}/api/embeddings`, {
|
|
383
|
+
method: 'POST',
|
|
384
|
+
headers: { 'Content-Type': 'application/json' },
|
|
385
|
+
body: JSON.stringify({
|
|
386
|
+
model: embeddingModel,
|
|
387
|
+
prompt: safeText,
|
|
388
|
+
}),
|
|
389
|
+
signal: controller.signal,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
clearTimeout(timeoutId);
|
|
393
|
+
|
|
394
|
+
if (!response.ok) {
|
|
395
|
+
const errBody = await response.text().catch(() => '');
|
|
396
|
+
throw new Error(`Embedding API 返回 ${response.status}: ${errBody.substring(0, 200)}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const data = await response.json();
|
|
400
|
+
|
|
401
|
+
if (!data.embedding || !Array.isArray(data.embedding)) {
|
|
402
|
+
throw new Error('Embedding API 返回格式异常:missing embedding 字段');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return { embedding: data.embedding };
|
|
406
|
+
} catch (err) {
|
|
407
|
+
lastError = err;
|
|
408
|
+
if (attempt < EMBEDDING_MAX_RETRIES) {
|
|
409
|
+
// 指数退避:200ms → 400ms
|
|
410
|
+
await new Promise(r => setTimeout(r, 200 * (attempt + 1)));
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return { error: `embedding 失败(retry ${EMBEDDING_MAX_RETRIES} 次后): ${lastError?.message || '未知错误'}` };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* 从文件内容中提取用于 embedding 的文本摘要
|
|
421
|
+
*/
|
|
422
|
+
function extractContentForEmbedding(filePath, content) {
|
|
423
|
+
// 取前 SEMANTIC_SEARCH_CHUNK_CHARS 字符
|
|
424
|
+
return content.substring(0, SEMANTIC_SEARCH_CHUNK_CHARS);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ====== 语义搜索(已由 bridge → usearch-bridge 统一管理)======
|
|
428
|
+
// Worker不再独立启动搜索进程,语义搜索统一走 usearch-bridge.js 单例
|
|
429
|
+
// ====== 流式搜索大文件 ======
|
|
430
|
+
|
|
431
|
+
async function streamSearch(filePath, keyword) {
|
|
432
|
+
const matches = [];
|
|
433
|
+
let lineNum = 0;
|
|
434
|
+
const kw = keyword.toLowerCase();
|
|
435
|
+
const MAX_MATCHES = 1000;
|
|
436
|
+
|
|
437
|
+
const stream = createReadStream(filePath, { encoding: "utf-8", highWaterMark: 65536 });
|
|
438
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
for await (const line of rl) {
|
|
442
|
+
lineNum++;
|
|
443
|
+
if (line.toLowerCase().includes(kw)) {
|
|
444
|
+
matches.push({ line: lineNum, text: line.trim().substring(0, 200) });
|
|
445
|
+
if (matches.length >= MAX_MATCHES) break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
} finally {
|
|
449
|
+
stream.destroy();
|
|
450
|
+
try { rl.close(); } catch {}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return matches;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ====== 子 agent 专用工具列表(模块级常量,统一引用) ======
|
|
457
|
+
// v5.37.0: 已清理,子agent路由由 steward-rules.js 的 getToolTiers 统一管理
|
|
458
|
+
const SUBAGENT_TOOLS = [];
|
|
459
|
+
|
|
460
|
+
// ====== 双树快速路由:小树关键词匹配 ======
|
|
461
|
+
// LEVEL_RULES 和 LEVEL_TOOL_MAP 定义在 lib/level-rules.js 中
|
|
462
|
+
// 修改后无需同步 worker.js 和 task-router.js(该文件已删除)
|
|
463
|
+
|
|
464
|
+
function routeQuick(text) {
|
|
465
|
+
const kw = (text || "").toLowerCase();
|
|
466
|
+
const result = { matched: false, tool: null, params: {}, confidence: 0 };
|
|
467
|
+
|
|
468
|
+
// ====== Step 1: L0-L7 复杂度分类(优先使用,解决了 quickClassify() 闲置缺陷) ======
|
|
469
|
+
let bestLevel = null;
|
|
470
|
+
let maxWeight = 0;
|
|
471
|
+
const matchedLevels = [];
|
|
472
|
+
|
|
473
|
+
for (const rule of LEVEL_RULES) {
|
|
474
|
+
for (const pattern of rule.patterns) {
|
|
475
|
+
if (pattern.test(kw)) {
|
|
476
|
+
matchedLevels.push(rule.level);
|
|
477
|
+
if (rule.weight > maxWeight) {
|
|
478
|
+
maxWeight = rule.weight;
|
|
479
|
+
bestLevel = rule.level;
|
|
480
|
+
}
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (bestLevel && maxWeight >= 0.5) {
|
|
487
|
+
const levelConfig = LEVEL_TOOL_MAP[bestLevel];
|
|
488
|
+
if (levelConfig) {
|
|
489
|
+
result.matched = true;
|
|
490
|
+
result.tool = levelConfig.tool;
|
|
491
|
+
result.confidence = levelConfig.conf;
|
|
492
|
+
result.params = {
|
|
493
|
+
...(levelConfig.params || {}),
|
|
494
|
+
level: bestLevel,
|
|
495
|
+
levelConfidence: maxWeight,
|
|
496
|
+
matchedLevels,
|
|
497
|
+
};
|
|
498
|
+
return result;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ====== Step 2: 搜索类型细分(记忆/网络/文件,按优先级排序) ======
|
|
503
|
+
const patterns = [
|
|
504
|
+
// 🥇 记忆/日记搜索 — 路由到混合搜索(子agent用memory_hybrid_search.py)
|
|
505
|
+
{ regex: /日记|记忆|历史对话|之前说|之前聊|聊过|说过|之前.*事|之前.*话|记不记得|还记得.*吗|recall|remember|过去说过|翻.*日记|查.*日记|搜.*日记|搜.*记忆|memory.*search|之前.*讨论|之前.*决定|之前.*说过|之前.*聊过|查.*历史|翻.*记录|对话记录|Q&A|qa.*log|dialog.*log/i, tool: "memory_search", confidence: 0.98 },
|
|
506
|
+
|
|
507
|
+
// 🥇 网络搜索 — 路由到Tavily(直连不需VPN)
|
|
508
|
+
{ regex: /网络|网上|网页|网上查|查资料|查.*信息|最新|新闻|今天.*消息|现在.*情况|tavily|search.*web|网上找|搜.*网络|web.*search|online.*search|internet.*search|网上搜|搜.*网上|找.*网上|web_fetch|fetch.*url|打开.*网页|访问.*网站/i, tool: "web_search", confidence: 0.96 },
|
|
509
|
+
|
|
510
|
+
// 🥇 通用文件搜索(兜底)
|
|
511
|
+
{ regex: /搜索|搜|查找|寻找|搜寻|search|find|query|locate|搜一下|找文件|搜索什么内容|看一看|看一下|找一找|查一查|帮我查查|帮我找找|你去查查|你去看看|帮我搜|帮我找|有没有|哪里能|哪里有|谁有|你知道|帮我看看|hunt ?for|track ?down|poke ?around|root ?around|scope ?out|look ?up|ferret ?out/i, tool: "core_search", confidence: 0.95 },
|
|
512
|
+
{ regex: /日志|log|错误|error|warn|报错|查看日志|check ?log|出错了|报错了|挂了|崩了|出啥事了|看看日志|check一下|查查日志|what went wrong|crash|fail/i, tool: "core_processLog", confidence: 0.92 },
|
|
513
|
+
{ regex: /改代码|修复|bug|replace|edit|改一下|修修|调一调|弄一弄|整整|帮我改|帮我修修|这个东西坏了|不对劲|不好使了|fix|broken|not working|something wrong|malfunction|glitch/i, tool: "core_bugFix", confidence: 0.9 },
|
|
514
|
+
{ regex: /审查|检查代码|review|lint|看看代码/i, tool: "cpu_codeReview", confidence: 0.9 },
|
|
515
|
+
{ regex: /系统|体检|诊断|状态|资源|健康检查|system|diagnose|health|看看状态|检查一下|有啥问题|正常运行不|正不正常|还好吗|what.*wrong|check up|what.*going on|is.*ok|health.*check/i, tool: "cpu_diagnose", confidence: 0.9 },
|
|
516
|
+
{ regex: /对比|差异|diff|变化|different|compare|区别/i, tool: "cpu_diff", confidence: 0.9 },
|
|
517
|
+
{ regex: /批量|多个|同时/i, tool: "core_dispatch", confidence: 0.85 },
|
|
518
|
+
{ regex: /调研|研究|调查|了解|查资料|研究一下|research|investigate|analyze|对比|对比分析|帮我了解一下|打听一下|问问|查查资料|看看有啥说法|什么情况|dig ?into|suss ?out|find ?out|look ?into|what.*people.*say|find.*more/i, tool: "cpu_research", confidence: 0.9 },
|
|
519
|
+
{ regex: /打开|查看|看下|显示|read|open|view|show|display|打开看看|打开瞅瞅|给我看看|我看看|念一下|读一下|瞅一眼|扫一眼|说说内容|have a look|take a look|glance at|let me see|show me/i, tool: "cpu_batch", confidence: 0.85 },
|
|
520
|
+
{ regex: /全盘|扫描|遍历|全局|scan|full.?scan|traverse|翻一翻|全盘翻翻|搜一遍|扫一遍|全部查查|所有文件|everything|every.*file|search.*all|all.*search/i, tool: "cpu_scan", confidence: 0.9 },
|
|
521
|
+
{ regex: /设计|方案|架构|规划|蓝图|策划|体系|design|plan|architecture|blueprint|framework|roadmap|proposal|白皮书/i, tool: "cpu_orchestrate", confidence: 0.9 },
|
|
522
|
+
{ regex: /监控.*子agent|子agent.*监控|子agent.*状态|子agent.*卡死|subagent.*monitor|monitor.*subagent|查看.*子agent|子agent.*列表|子agent.*快照/i, tool: "cpu_monitorSubagents", confidence: 0.92 },
|
|
523
|
+
{ regex: /日记|对话记录|聊过什么|说过什么|搜.*日记|查.*日记|翻.*日记|翻.*记录|查.*记录|回看.*对话|回顾.*对话|之前.*说过|之前.*聊过|之前.*讨论|之前.*决定|回忆|recall.*dialog|dialog.*search|dialog.*recall|对话检索|历史.*对话|对话.*历史/i, tool: "cpu_dialogRecall", confidence: 0.95 },
|
|
524
|
+
|
|
525
|
+
// ====== 🆕 扩展规则(2026-06-05 新增:日常口语/哲学/心理学/文学/逻辑)======
|
|
526
|
+
|
|
527
|
+
// 🆕 日常口语桥接 — 口语层→操作层映射(置信度0.45-0.88)
|
|
528
|
+
{ regex: /帮我查查|帮我搜一下|帮我查一下|帮我搜搜|帮我找找|帮我搜索/i, tool: "cpu_search", confidence: 0.88 },
|
|
529
|
+
{ regex: /你看看这个|帮我看看|给我看看|你看一下|你看一看|你瞅瞅/i, tool: "cpu_batch", confidence: 0.75 },
|
|
530
|
+
{ regex: /怎么回事|这是怎么回事|这是什么情况|什么情况|怎么啦|咋回事|什么鬼/i, tool: "core_memorySearch", confidence: 0.70 },
|
|
531
|
+
{ regex: /怎么办|咋办|怎么弄|怎么搞|如何处理|该怎么做/i, tool: "cpu_orchestrate", confidence: 0.80 },
|
|
532
|
+
{ regex: /帮我(弄|搞|处理|看看|查查|整整)/i, tool: "cpu_orchestrate", confidence: 0.65 },
|
|
533
|
+
{ regex: /能不能帮我|能不能.*帮我|可以帮我|可不可以帮我/i, tool: "cpu_orchestrate", confidence: 0.55 },
|
|
534
|
+
{ regex: /把.*(弄|整|处理|搞).*(一下|下)/i, tool: "cpu_orchestrate", confidence: 0.45 },
|
|
535
|
+
{ regex: /请问|请教|问一下|想问/i, tool: "core_memorySearch", confidence: 0.50 },
|
|
536
|
+
|
|
537
|
+
// 🆕 哲学思辨 — 概念澄清/假设推演/伦理判断(置信度0.35-0.85)
|
|
538
|
+
{ regex: /这么做(对吗|真的对吗|是不是对的|合理吗|正当吗|应该吗)/i, tool: "core_memorySearch", confidence: 0.85 },
|
|
539
|
+
{ regex: /意义.*(何在|在哪|是什么)|存在.*(意义|本质)/i, tool: "core_memorySearch", confidence: 0.80 },
|
|
540
|
+
{ regex: /先有.*后(有|是)|因果.*先后/i, tool: "core_memorySearch", confidence: 0.85 },
|
|
541
|
+
{ regex: /(到底|究竟)什么是.*(本质|终极|真正|真实)/i, tool: "core_memorySearch", confidence: 0.70 },
|
|
542
|
+
{ regex: /有没有可能(其实|只是|不过)|会不会其实|可不可能/i, tool: "core_memorySearch", confidence: 0.75 },
|
|
543
|
+
{ regex: /(从|换).*(角度|视角|维度|层面|立场).*(看|分析|理解|思考)/i, tool: "core_memorySearch", confidence: 0.70 },
|
|
544
|
+
{ regex: /选择.*(自由|被迫|自愿|可选)|自由意志/i, tool: "core_memorySearch", confidence: 0.78 },
|
|
545
|
+
{ regex: /(重来|再来|重新|当初).*(选择|决定|选)/i, tool: "core_memorySearch", confidence: 0.72 },
|
|
546
|
+
{ regex: /(本质|意味).*(是什么|到底|究竟)/i, tool: "core_memorySearch", confidence: 0.65 },
|
|
547
|
+
{ regex: /(怎么|如何).*(证明|证实|确认|验证)|怎么知道.*是真/i, tool: "core_memorySearch", confidence: 0.55 },
|
|
548
|
+
{ regex: /(凭什么|这样公平|这样合理|公平.*合理)/i, tool: "core_memorySearch", confidence: 0.50 },
|
|
549
|
+
{ regex: /(自相矛盾|逻辑不通|不合理|说不过去)/i, tool: "core_memorySearch", confidence: 0.50 },
|
|
550
|
+
{ regex: /(公平|公正|正义)/i, tool: "core_memorySearch", confidence: 0.35 },
|
|
551
|
+
{ regex: /(矛盾|悖论)/i, tool: "core_memorySearch", confidence: 0.40 },
|
|
552
|
+
|
|
553
|
+
// 🆕 心理学情绪 — 情绪表达/心理咨询/性格分析(置信度0.50-0.90)
|
|
554
|
+
{ regex: /(我好|我有点|我感觉|我觉得).*(焦虑|抑郁|难受|压抑|烦躁|emo)/i, tool: "core_memorySearch", confidence: 0.90 },
|
|
555
|
+
{ regex: /怎么.*(调整|改善|改变).*(情绪|心态|心理|状态)/i, tool: "core_memorySearch", confidence: 0.80 },
|
|
556
|
+
{ regex: /(梦境|梦到|潜意识|投射|防御|依恋)/i, tool: "core_memorySearch", confidence: 0.75 },
|
|
557
|
+
{ regex: /(讨好型|回避型|edges缘型|焦虑型|依恋型)人格/i, tool: "core_webSearch", confidence: 0.75 },
|
|
558
|
+
{ regex: /(焦虑|抑郁|压力大|emo|emo了|好烦|不开心|烦躁)/i, tool: "core_memorySearch", confidence: 0.70 },
|
|
559
|
+
{ regex: /(原生家庭|童年阴影|童年创伤|缺爱)/i, tool: "core_memorySearch", confidence: 0.70 },
|
|
560
|
+
{ regex: /(睡不着|失眠|入睡困难|睡眠.*不好|噩梦)/i, tool: "core_memorySearch", confidence: 0.70 },
|
|
561
|
+
{ regex: /(心理|情绪|人格|性格|认知|行为)/i, tool: "core_memorySearch", confidence: 0.60 },
|
|
562
|
+
{ regex: /(弗洛伊德|荣格|阿德勒|马斯洛|MBTI|INFJ|INTP)/i, tool: "core_webSearch", confidence: 0.50 },
|
|
563
|
+
{ regex: /(正念|冥想|认知行为|CBT|内观)/i, tool: "core_webSearch", confidence: 0.55 },
|
|
564
|
+
|
|
565
|
+
// 🆕 文学 — 阅读评论/写作手法/文艺分析(置信度0.70-0.80)
|
|
566
|
+
{ regex: /读后感|书评|书单|推荐.*书|读了.*(书|文章)/i, tool: "core_memorySearch", confidence: 0.80 },
|
|
567
|
+
{ regex: /(比喻|拟人|排比|修辞|写作手法)/i, tool: "core_codeEditor", confidence: 0.70 },
|
|
568
|
+
{ regex: /(意境|描写|散文|诗歌|小说|叙事|抒情)/i, tool: "core_memorySearch", confidence: 0.80 },
|
|
569
|
+
{ regex: /(人物|情节|场景|倒叙|线索|冲突)/i, tool: "core_memorySearch", confidence: 0.75 },
|
|
570
|
+
{ regex: /(赏析|评价|感受|体验).*(书|文章|作品|电影|诗)/i, tool: "core_memorySearch", confidence: 0.75 },
|
|
571
|
+
|
|
572
|
+
// 🆕 逻辑推理 — 件/因果/推导/审查(置信度0.15-0.50)
|
|
573
|
+
{ regex: /(由此可见|综上|综上所述|论据|论证|推导)/i, tool: "core_memorySearch", confidence: 0.50 },
|
|
574
|
+
{ regex: /(逻辑.*错|逻辑.*谬|谬误|偷换概念|循环论证)/i, tool: "core_memorySearch", confidence: 0.40 },
|
|
575
|
+
{ regex: /(因为.*导致|根本原因|根因分析|追溯|归因)/i, tool: "core_memorySearch", confidence: 0.20 },
|
|
576
|
+
{ regex: /(只有.*才(能|会)|除非.*否则|当且仅当)/i, tool: "core_memorySearch", confidence: 0.15 },
|
|
577
|
+
];
|
|
578
|
+
|
|
579
|
+
for (const p of patterns) {
|
|
580
|
+
if (p.regex.test(kw) && p.confidence > result.confidence) {
|
|
581
|
+
result.matched = true;
|
|
582
|
+
result.tool = p.tool;
|
|
583
|
+
result.confidence = p.confidence;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return result;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ====== 双树系统状态评估 ======
|
|
591
|
+
|
|
592
|
+
function routeSystem(text) {
|
|
593
|
+
// Worker 级轻量系统评估
|
|
594
|
+
const memUsage = process.memoryUsage();
|
|
595
|
+
const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
|
|
596
|
+
const rssMB = Math.round(memUsage.rss / 1024 / 1024);
|
|
597
|
+
|
|
598
|
+
let loadLevel = "green";
|
|
599
|
+
const issues = [];
|
|
600
|
+
|
|
601
|
+
if (rssMB > 1024) { loadLevel = "yellow"; issues.push(`RSS 内存高: ${rssMB}MB`); }
|
|
602
|
+
if (heapUsedMB > 512) { loadLevel = "yellow"; issues.push(`Heap 使用高: ${heapUsedMB}MB`); }
|
|
603
|
+
if (rssMB > 2048) { loadLevel = "red"; }
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
loadLevel,
|
|
607
|
+
details: {
|
|
608
|
+
rssMB,
|
|
609
|
+
heapUsedMB,
|
|
610
|
+
uptime: process.uptime(),
|
|
611
|
+
issues,
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ====== 双树任务意图解析 ======
|
|
617
|
+
|
|
618
|
+
function routeIntent(text) {
|
|
619
|
+
const kw = (text || "").toLowerCase();
|
|
620
|
+
|
|
621
|
+
let taskType = "查询类";
|
|
622
|
+
if (/搜索|查找|搜/.test(kw)) taskType = "搜索类";
|
|
623
|
+
else if (/修改|修复|bug|replace|edit|删除|写入|创建/.test(kw)) taskType = "修改类";
|
|
624
|
+
else if (/分析|stats|计算|聚合|比较/.test(kw)) taskType = "分析类";
|
|
625
|
+
else if (/调研|研究|调查|了解|对比/.test(kw)) taskType = "调研类";
|
|
626
|
+
else if (/打开|查看|看下|显示|访问/.test(kw)) taskType = "查询类";
|
|
627
|
+
|
|
628
|
+
let urgency = "normal";
|
|
629
|
+
if (/紧急|马上|立刻|尽快/.test(kw)) urgency = "high";
|
|
630
|
+
if (/不急|有空|稍后/.test(kw)) urgency = "low";
|
|
631
|
+
|
|
632
|
+
let danger = false;
|
|
633
|
+
if (/删除|清空|格式化|reset|shutdown|重启|关机|关闭|关掉/.test(kw)) danger = true;
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
taskType,
|
|
637
|
+
intent: kw.substring(0, 200),
|
|
638
|
+
scope: "local",
|
|
639
|
+
urgency,
|
|
640
|
+
dangerDetected: danger,
|
|
641
|
+
constraints: danger ? ["需要用户确认"] : [],
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// ====== 双树能力edges界扫描 ======
|
|
646
|
+
|
|
647
|
+
function routeCapability(text) {
|
|
648
|
+
// 子agent能干的关键词检测:代码修改/审查/编写/方案设计等 → 移除直接工具,强制走子agent
|
|
649
|
+
const subagentKeywords = /修改|改代码|修复|bug|审查|review|write|写|创建|新建|编辑|代码|编码|设计|方案|架构|写代码|开发|实现|重构|优化|整理|生成/i;
|
|
650
|
+
const isSubagentTask = subagentKeywords.test(text || "");
|
|
651
|
+
|
|
652
|
+
const baseDirectTools = [
|
|
653
|
+
"core_stats",
|
|
654
|
+
];
|
|
655
|
+
|
|
656
|
+
return {
|
|
657
|
+
directTools: baseDirectTools,
|
|
658
|
+
compoundTools: ["cpu_orchestrate"],
|
|
659
|
+
fallback: "subagent",
|
|
660
|
+
gaps: isSubagentTask
|
|
661
|
+
? ['该任务建议派新子agent执行(子agent上下文干净,不易幻觉,且可并行)']
|
|
662
|
+
: [],
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ====== 双树策略生成 ======
|
|
667
|
+
|
|
668
|
+
function routeStrategy(quick, system, intent, capability, decompose = { decomposed: false }) {
|
|
669
|
+
// 系统状态红 → 只汇报
|
|
670
|
+
if (system.loadLevel === "red") {
|
|
671
|
+
return {
|
|
672
|
+
tree: "big",
|
|
673
|
+
decision: null,
|
|
674
|
+
strategy: "block",
|
|
675
|
+
risk: "high",
|
|
676
|
+
message: `系统负载过高 (${system.details.issues?.join(", ") || "未知"}),仅限汇报操作`,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// 小树高置信度匹配 + 系统正常 → 判断是否走子agent
|
|
681
|
+
if (quick.matched && quick.confidence >= 0.9 && system.loadLevel !== "red") {
|
|
682
|
+
if (SUBAGENT_TOOLS.includes(quick.tool)) {
|
|
683
|
+
let subagentStrategy = "parallel";
|
|
684
|
+
if (system.loadLevel === "yellow") subagentStrategy = "serial";
|
|
685
|
+
return {
|
|
686
|
+
tree: "small",
|
|
687
|
+
decision: null,
|
|
688
|
+
strategy: "subagent",
|
|
689
|
+
recommendedTool: quick.tool,
|
|
690
|
+
risk: "low",
|
|
691
|
+
message: `该工具(${quick.tool})可通过子agent执行,派新子agent更高效`,
|
|
692
|
+
subagentStrategy,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
let strategy = "concurrent";
|
|
696
|
+
if (system.loadLevel === "yellow") strategy = "serial";
|
|
697
|
+
return {
|
|
698
|
+
tree: "big",
|
|
699
|
+
decision: quick.tool,
|
|
700
|
+
params: quick.params || {},
|
|
701
|
+
confidence: quick.confidence,
|
|
702
|
+
strategy,
|
|
703
|
+
risk: "low",
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// 小树中等置信度 → 判断是否走子agent
|
|
708
|
+
if (quick.matched && quick.confidence >= 0.6) {
|
|
709
|
+
if (SUBAGENT_TOOLS.includes(quick.tool)) {
|
|
710
|
+
return {
|
|
711
|
+
tree: "small",
|
|
712
|
+
decision: null,
|
|
713
|
+
strategy: "subagent",
|
|
714
|
+
recommendedTool: quick.tool,
|
|
715
|
+
risk: "medium",
|
|
716
|
+
message: `该工具(${quick.tool})可通过子agent执行,派新子agent`,
|
|
717
|
+
subagentStrategy: "serial",
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
tree: "big",
|
|
722
|
+
decision: quick.tool,
|
|
723
|
+
params: quick.params || {},
|
|
724
|
+
confidence: quick.confidence,
|
|
725
|
+
strategy: system.loadLevel === "yellow" ? "degraded" : "serial",
|
|
726
|
+
risk: "medium",
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// 危险操作
|
|
731
|
+
if (intent.dangerDetected) {
|
|
732
|
+
return {
|
|
733
|
+
tree: "big",
|
|
734
|
+
decision: null,
|
|
735
|
+
strategy: "block",
|
|
736
|
+
risk: "high",
|
|
737
|
+
message: "检测到危险操作,需要用户确认",
|
|
738
|
+
dangerType: intent.intent.substring(0, 100),
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// decompose 兜底:无工具匹配但分解结果有效 → 返回 decompose 策略
|
|
743
|
+
if (decompose?.decomposed === true && (decompose?.steps?.length || 0) >= 2) {
|
|
744
|
+
return {
|
|
745
|
+
tree: "big",
|
|
746
|
+
decision: null,
|
|
747
|
+
strategy: "decompose",
|
|
748
|
+
risk: "low",
|
|
749
|
+
message: `任务已自动分解为 ${decompose.steps.length} 个子步骤,建议派子 agent 分步执行`,
|
|
750
|
+
decompose: {
|
|
751
|
+
taskId: decompose.taskId,
|
|
752
|
+
steps: (decompose.steps || []).map(s => ({
|
|
753
|
+
step: s.step,
|
|
754
|
+
description: (s.description || '').substring(0, 120),
|
|
755
|
+
action: s.action || `step-${s.step}`,
|
|
756
|
+
tool: s.tool || 'spawn_subagent',
|
|
757
|
+
timeoutMs: s.timeoutMs || 60000,
|
|
758
|
+
})),
|
|
759
|
+
recommendedConcurrency: decompose.recommendedConcurrency || 2,
|
|
760
|
+
},
|
|
761
|
+
recommendedConcurrency: decompose.recommendedConcurrency || 2,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// 完全不匹配
|
|
766
|
+
return {
|
|
767
|
+
tree: "big",
|
|
768
|
+
decision: null,
|
|
769
|
+
strategy: "subagent",
|
|
770
|
+
risk: "medium",
|
|
771
|
+
message: "无直接工具匹配,建议派子 agent 兜底",
|
|
772
|
+
intentType: intent.taskType,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ====== Worker E: 笛卡尔分解律(任务分解层) ======
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Worker E:在双树评估的大树阶段对 L5+ 任务进行深度分解。
|
|
780
|
+
* 调用 lib/decomposer.js 的 routeDecompose 核心函数。
|
|
781
|
+
*
|
|
782
|
+
* @param {string} text - 原始任务文本
|
|
783
|
+
* @returns {Promise<object>} 分解结果(含 DAG + pipeline 定义)
|
|
784
|
+
*/
|
|
785
|
+
async function routeDecompose(text) {
|
|
786
|
+
try {
|
|
787
|
+
const result = await decomposeTaskBody(text || '');
|
|
788
|
+
// 添加 Worker 元数据
|
|
789
|
+
return {
|
|
790
|
+
...result,
|
|
791
|
+
workerId,
|
|
792
|
+
evaluatedAt: Date.now(),
|
|
793
|
+
};
|
|
794
|
+
} catch (err) {
|
|
795
|
+
// 解构失败 → 返回单步骤兜底
|
|
796
|
+
return {
|
|
797
|
+
taskId: (await import('crypto')).default.createHash('sha256').update(text || '', 'utf-8').digest('hex').substring(0, 12),
|
|
798
|
+
decomposed: false,
|
|
799
|
+
steps: [{ step: 1, description: (text || '').substring(0, 200), action: '完整', tool: 'spawn_subagent', timeoutMs: 120000, retryMax: 0 }],
|
|
800
|
+
pipeline: null,
|
|
801
|
+
filePaths: [],
|
|
802
|
+
recommendedConcurrency: 1,
|
|
803
|
+
note: `分解器异常: ${err.message},降级为单步骤`,
|
|
804
|
+
error: err.message,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// ====== Robocopy 辅助函数 ======
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* 运行 robocopy 同步命令
|
|
813
|
+
* @param {string} src - 源目录
|
|
814
|
+
* @param {string} dst - 目标目录
|
|
815
|
+
* @param {string[]} args - robocopy 参数列表
|
|
816
|
+
* @returns {Promise<{exitCode: number, stdout: string, stderr: string}>}
|
|
817
|
+
*/
|
|
818
|
+
// ★ 跨平台路径修复:用 SystemRoot 环境变量代替硬编码 C:\Windows
|
|
819
|
+
const isWin = process.platform === 'win32';
|
|
820
|
+
const robocopyExe = isWin
|
|
821
|
+
? join(process.env.SystemRoot || process.env.windir || 'C:\\Windows', 'System32', 'robocopy.exe')
|
|
822
|
+
: 'rsync';
|
|
823
|
+
|
|
824
|
+
async function runRobocopy(src, dst, args) {
|
|
825
|
+
return new Promise((resolve, reject) => {
|
|
826
|
+
const child = execFile(robocopyExe, [src, dst, ...args], {
|
|
827
|
+
timeout: 300000, // 5min超时
|
|
828
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
829
|
+
windowsHide: true,
|
|
830
|
+
}, (err, stdout, stderr) => {
|
|
831
|
+
// robocopy 用 exit code 表示状态:0-7 成功,8+ 错误
|
|
832
|
+
const exitCode = err?.code ?? (err ? -1 : 0);
|
|
833
|
+
if (err && exitCode >= 8) {
|
|
834
|
+
resolve({ exitCode, stdout: stdout || '', stderr: stderr || '', error: err.message });
|
|
835
|
+
} else {
|
|
836
|
+
resolve({ exitCode, stdout: stdout || '', stderr: stderr || '' });
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ====== 任务执行 ======
|
|
843
|
+
|
|
844
|
+
async function executeTask(task) {
|
|
845
|
+
const { type, provider, modelId, text, files, keyword, minResults, priority } = task;
|
|
846
|
+
|
|
847
|
+
switch (type) {
|
|
848
|
+
|
|
849
|
+
case "resolve-model": {
|
|
850
|
+
let ctxWindow = 131072, maxTokens = 16384;
|
|
851
|
+
const found = await readModelConfig(provider, modelId);
|
|
852
|
+
if (found) {
|
|
853
|
+
ctxWindow = found.contextWindow || 131072;
|
|
854
|
+
maxTokens = found.maxTokens || 16384;
|
|
855
|
+
}
|
|
856
|
+
const cleanId = modelId.includes("/") ? modelId.split("/").pop() : modelId;
|
|
857
|
+
return {
|
|
858
|
+
id: `${provider}/${cleanId}`, provider,
|
|
859
|
+
contextWindow: ctxWindow, maxTokens,
|
|
860
|
+
capabilities: { completion: true, chat: true, function_calling: true, streaming: true, vision: false },
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
case "search-text": {
|
|
865
|
+
const kw = (text ?? keyword);
|
|
866
|
+
if (!kw) throw Object.assign(new Error("missing搜索关键词"), { code: "BAD_REQUEST" });
|
|
867
|
+
const maxFiles = task.maxFiles ?? 50;
|
|
868
|
+
const paths = (files || []).slice(0, maxFiles);
|
|
869
|
+
const results = [];
|
|
870
|
+
const minHits = (typeof minResults === 'number' && minResults > 0) ? minResults : (task.maxMinResults || 5);
|
|
871
|
+
let totalMatches = 0;
|
|
872
|
+
let stoppedEarly = false;
|
|
873
|
+
|
|
874
|
+
for (const raw of paths) {
|
|
875
|
+
// 有限理性早停:搜索达到足够匹配数后不再继续搜剩余文件
|
|
876
|
+
if (totalMatches >= minHits) {
|
|
877
|
+
stoppedEarly = true;
|
|
878
|
+
break;
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
const fp = await validatePath(raw);
|
|
882
|
+
const matches = await streamSearch(fp, kw);
|
|
883
|
+
if (matches.length > 0) {
|
|
884
|
+
results.push({ file: fp, matchCount: matches.length, matches });
|
|
885
|
+
totalMatches += matches.length;
|
|
886
|
+
}
|
|
887
|
+
} catch (err) {
|
|
888
|
+
results.push({ file: raw, error: err.message });
|
|
889
|
+
}
|
|
890
|
+
// 🧠 可判定终止: 探索型任务每处理一个文件就存储中间进度
|
|
891
|
+
storePartial(task.jobId, {
|
|
892
|
+
progress: `${results.length}/${paths.length} 文件已搜索`,
|
|
893
|
+
partialMatches: totalMatches,
|
|
894
|
+
filesProcessed: results.length,
|
|
895
|
+
totalFiles: paths.length,
|
|
896
|
+
taskType: 'search-text',
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
return {
|
|
900
|
+
keyword: kw,
|
|
901
|
+
total: totalMatches,
|
|
902
|
+
minResults: minHits,
|
|
903
|
+
stoppedEarly,
|
|
904
|
+
results,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
case "dialog-search": {
|
|
909
|
+
const kw = (text ?? keyword);
|
|
910
|
+
if (!kw) throw Object.assign(new Error("missing搜索关键词"), { code: "BAD_REQUEST" });
|
|
911
|
+
const MAX_DIALOG_FILES = 50;
|
|
912
|
+
const paths = (files || []).slice(0, MAX_DIALOG_FILES);
|
|
913
|
+
const results = [];
|
|
914
|
+
|
|
915
|
+
for (const raw of paths) {
|
|
916
|
+
try {
|
|
917
|
+
const fp = await validatePath(raw);
|
|
918
|
+
// 逐行搜索并stats文件总行数
|
|
919
|
+
const matches = [];
|
|
920
|
+
let lineNum = 0;
|
|
921
|
+
let totalLines = 0;
|
|
922
|
+
let contextBuffer = [];
|
|
923
|
+
const CONTEXT_WINDOW = task.withContext ? 3 : 0; // 上下文行数
|
|
924
|
+
const MAX_DIALOG_MATCHES = 500;
|
|
925
|
+
const kwLower = kw.toLowerCase();
|
|
926
|
+
|
|
927
|
+
const stream = createReadStream(fp, { encoding: "utf-8", highWaterMark: 65536 });
|
|
928
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
929
|
+
let foundAny = false;
|
|
930
|
+
try {
|
|
931
|
+
for await (const line of rl) {
|
|
932
|
+
lineNum++;
|
|
933
|
+
totalLines++;
|
|
934
|
+
|
|
935
|
+
// 上下文缓存:保存最近 CONTEXT_WINDOW 行
|
|
936
|
+
if (task.withContext && CONTEXT_WINDOW > 0) {
|
|
937
|
+
contextBuffer.push({ lineNum, text: line });
|
|
938
|
+
if (contextBuffer.length > CONTEXT_WINDOW) contextBuffer.shift();
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (line.toLowerCase().includes(kwLower)) {
|
|
942
|
+
foundAny = true;
|
|
943
|
+
const matchEntry = { line: lineNum, text: line.trim().substring(0, 200) };
|
|
944
|
+
// 附带前文上下文
|
|
945
|
+
if (task.withContext && CONTEXT_WINDOW > 0 && contextBuffer.length > 1) {
|
|
946
|
+
matchEntry.before = contextBuffer
|
|
947
|
+
.slice(0, -1)
|
|
948
|
+
.map(c => ({ line: c.lineNum, text: c.text.trim().substring(0, 200) }));
|
|
949
|
+
}
|
|
950
|
+
matches.push(matchEntry);
|
|
951
|
+
if (matches.length >= MAX_DIALOG_MATCHES) break;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
} finally {
|
|
955
|
+
stream.destroy();
|
|
956
|
+
try { rl.close(); } catch {}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (foundAny) {
|
|
960
|
+
results.push({
|
|
961
|
+
file: fp,
|
|
962
|
+
matchCount: matches.length,
|
|
963
|
+
totalLines,
|
|
964
|
+
matches: task.withContext ? matches : matches.map(m => ({ line: m.line, text: m.text })),
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
} catch (err) {
|
|
968
|
+
results.push({ file: raw, error: err.message });
|
|
969
|
+
}
|
|
970
|
+
// 🧠 可判定终止: 每处理一个文件存储中间进度
|
|
971
|
+
storePartial(task.jobId, {
|
|
972
|
+
progress: `${results.length}/${MAX_DIALOG_FILES} 日记文件已搜索`,
|
|
973
|
+
partialMatches: results.reduce((a, b) => a + (b.matchCount || 0), 0),
|
|
974
|
+
filesProcessed: results.length,
|
|
975
|
+
totalFiles: MAX_DIALOG_FILES,
|
|
976
|
+
taskType: 'dialog-search',
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
return {
|
|
980
|
+
keyword: kw,
|
|
981
|
+
total: results.reduce((a, b) => a + (b.matchCount || 0), 0),
|
|
982
|
+
results,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
case "semantic-search": {
|
|
987
|
+
const query = task.query || keyword;
|
|
988
|
+
if (!query) throw Object.assign(new Error('missing搜索查询'), { code: 'BAD_REQUEST' });
|
|
989
|
+
// 语义搜索已移至 bridge → usearch-bridge.js 管理
|
|
990
|
+
// Worker不再直接调语义搜索,请通过 bridge.js 查询
|
|
991
|
+
throw Object.assign(new Error('语义搜索请走 bridge → usearch-bridge.js'), { code: 'SEARCH_MAIN_THREAD' });
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
case "process-log": {
|
|
995
|
+
if (!files || files.length === 0) throw Object.assign(new Error("未指定文件"), { code: "BAD_REQUEST" });
|
|
996
|
+
const stats = [];
|
|
997
|
+
const MAX_FILES = 20;
|
|
998
|
+
const MAX_LINES = 500000;
|
|
999
|
+
const paths = files.slice(0, MAX_FILES);
|
|
1000
|
+
for (const raw of paths) {
|
|
1001
|
+
try {
|
|
1002
|
+
const fp = await validatePath(raw);
|
|
1003
|
+
const { size } = await stat(fp);
|
|
1004
|
+
const sizeKB = Math.round(size / 1024);
|
|
1005
|
+
let totalLines = 0;
|
|
1006
|
+
const levels = { error: 0, warn: 0, info: 0, debug: 0 };
|
|
1007
|
+
let truncated = false;
|
|
1008
|
+
const stream = createReadStream(fp, { encoding: "utf-8", highWaterMark: 65536 });
|
|
1009
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
1010
|
+
try {
|
|
1011
|
+
for await (const line of rl) {
|
|
1012
|
+
if (totalLines >= MAX_LINES) { truncated = true; break; }
|
|
1013
|
+
totalLines++;
|
|
1014
|
+
if (/^\s*(?:ERROR|Error|error|FATAL|Fatal|fatal|CRITICAL|Critical|critical)\b/.test(line) || /\[\s*(?:ERROR|Error|error|FATAL|Fatal|fatal|CRITICAL|Critical|critical)\s*\]/.test(line)) levels.error++;
|
|
1015
|
+
if (/^\s*(?:WARN|Warn|warn|WARNING|Warning|warning)\b/.test(line) || /\[\s*(?:WARN|Warn|warn|WARNING|Warning|warning)\s*\]/.test(line)) levels.warn++;
|
|
1016
|
+
if (/^\s*(?:INFO|Info|info)\b/.test(line) || /\[\s*(?:INFO|Info|info)\s*\]/.test(line)) levels.info++;
|
|
1017
|
+
if (/^\s*(?:DEBUG|Debug|debug|TRACE|Trace|trace)\b/.test(line) || /\[\s*(?:DEBUG|Debug|debug|TRACE|Trace|trace)\s*\]/.test(line)) levels.debug++;
|
|
1018
|
+
}
|
|
1019
|
+
} finally {
|
|
1020
|
+
stream.destroy();
|
|
1021
|
+
try { rl.close(); } catch {}
|
|
1022
|
+
}
|
|
1023
|
+
stats.push({ file: fp, totalLines, levels, sizeKB, truncated });
|
|
1024
|
+
} catch (err) {
|
|
1025
|
+
stats.push({ file: raw, error: err.message });
|
|
1026
|
+
}
|
|
1027
|
+
// 🧠 可判定终止: 每处理一个日志文件存储中间进度
|
|
1028
|
+
storePartial(task.jobId, {
|
|
1029
|
+
progress: `${stats.length}/${paths.length} 日志文件已分析`,
|
|
1030
|
+
filesProcessed: stats.length,
|
|
1031
|
+
totalFiles: paths.length,
|
|
1032
|
+
taskType: 'process-log',
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
return { stats };
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
case "diff": {
|
|
1039
|
+
if (!files || files.length === 0) throw Object.assign(new Error("未指定文件"), { code: "BAD_REQUEST" });
|
|
1040
|
+
const MAX_FILES = 20;
|
|
1041
|
+
const paths = files.slice(0, MAX_FILES);
|
|
1042
|
+
const results = [];
|
|
1043
|
+
for (const raw of paths) {
|
|
1044
|
+
try {
|
|
1045
|
+
const fp = await validatePath(raw);
|
|
1046
|
+
const { size: fileSize } = await stat(fp);
|
|
1047
|
+
if (fileSize > 50 * 1024 * 1024) {
|
|
1048
|
+
results.push({ file: fp, error: `文件过大(${Math.round(fileSize / 1024 / 1024)}MB),超过50MB上限` });
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
const content = await readFile(fp, "utf-8");
|
|
1052
|
+
const lines = content.split("\n");
|
|
1053
|
+
let added = 0, removed = 0, changedFiles = 0;
|
|
1054
|
+
let currentFile = "";
|
|
1055
|
+
const fileChanges = [];
|
|
1056
|
+
for (const line of lines) {
|
|
1057
|
+
if (line.startsWith("diff --git")) {
|
|
1058
|
+
if (currentFile) fileChanges.push({ file: currentFile, added, removed });
|
|
1059
|
+
currentFile = line.split(" ").pop() || "";
|
|
1060
|
+
added = 0; removed = 0;
|
|
1061
|
+
changedFiles++;
|
|
1062
|
+
} else if (line.startsWith("+") && !line.startsWith("+++")) added++;
|
|
1063
|
+
else if (line.startsWith("-") && !line.startsWith("---")) removed++;
|
|
1064
|
+
}
|
|
1065
|
+
if (currentFile) fileChanges.push({ file: currentFile, added, removed });
|
|
1066
|
+
results.push({ file: fp, changedFiles, totalAdded: fileChanges.reduce((a, b) => a + b.added, 0), totalRemoved: fileChanges.reduce((a, b) => a + b.removed, 0), fileChanges });
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
results.push({ file: raw, error: err.message });
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return { totalFiles: results.length, results };
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// ====== 双树决策引擎任务 ======
|
|
1075
|
+
|
|
1076
|
+
case "route-quick": {
|
|
1077
|
+
// 小树快路径:关键词匹配,~50ms Worker 内纯计算
|
|
1078
|
+
return routeQuick(text || keyword || "");
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
case "route-system": {
|
|
1082
|
+
// 系统状态评估
|
|
1083
|
+
return routeSystem(text || "");
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
case "route-intent": {
|
|
1087
|
+
// 任务意图解析
|
|
1088
|
+
return routeIntent(text || keyword || "");
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
case "route-capability": {
|
|
1092
|
+
// 能力edges界扫描
|
|
1093
|
+
return routeCapability(text || "");
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
case "route-strategy": {
|
|
1097
|
+
// 策略生成:合并所有评估结果
|
|
1098
|
+
const { quick, system, intent, capability, decompose } = task;
|
|
1099
|
+
return routeStrategy(
|
|
1100
|
+
quick || { matched: false, tool: null, params: {}, confidence: 0 },
|
|
1101
|
+
system || { loadLevel: "green", details: {} },
|
|
1102
|
+
intent || { taskType: "查询类", intent: "", scope: "local", urgency: "normal", dangerDetected: false, constraints: [] },
|
|
1103
|
+
capability || { directTools: [], compoundTools: [], fallback: "subagent", gaps: [] },
|
|
1104
|
+
decompose || { decomposed: false, steps: [], note: '' },
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
case "route-decompose": {
|
|
1109
|
+
// Worker E: L5+ 笛卡尔分解(task + level 参数)
|
|
1110
|
+
return routeDecompose(task.text || task.keyword || "");
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
case "ping":
|
|
1114
|
+
return { pong: true, worker: workerId, ts: Date.now() };
|
|
1115
|
+
|
|
1116
|
+
// ====== 图片批量分析 ======
|
|
1117
|
+
|
|
1118
|
+
case "image-process": {
|
|
1119
|
+
const { imagePath, prompt: visionPrompt } = task;
|
|
1120
|
+
if (!imagePath) throw Object.assign(new Error("missing imagePath"), { code: "BAD_REQUEST" });
|
|
1121
|
+
|
|
1122
|
+
const fp = await validatePath(imagePath);
|
|
1123
|
+
|
|
1124
|
+
// 1. Compress with sharp (resize max 1024px, JPEG quality 75)
|
|
1125
|
+
const tmpDir = await mkdtemp(join(tmpdir(), 'sansan-img-'));
|
|
1126
|
+
const compressedPath = join(tmpDir, 'compressed.jpg');
|
|
1127
|
+
|
|
1128
|
+
try {
|
|
1129
|
+
const sharp = (await import('sharp')).default;
|
|
1130
|
+
await sharp(fp)
|
|
1131
|
+
.resize(1024, 1024, { fit: 'inside', withoutEnlargement: true })
|
|
1132
|
+
.jpeg({ quality: 75 })
|
|
1133
|
+
.toFile(compressedPath);
|
|
1134
|
+
} catch (compressErr) {
|
|
1135
|
+
// Cleanup on failure
|
|
1136
|
+
try { await unlink(compressedPath).catch(() => {}); } catch {}
|
|
1137
|
+
try { await rmdir(tmpDir).catch(() => {}); } catch {}
|
|
1138
|
+
throw Object.assign(new Error(`压缩失败: ${compressErr.message}`), { code: "COMPRESS_FAILED" });
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// 2. Read compressed image as base64
|
|
1142
|
+
const compressedBuffer = await readFile(compressedPath);
|
|
1143
|
+
const base64Image = compressedBuffer.toString('base64');
|
|
1144
|
+
|
|
1145
|
+
// 3. Cleanup temp files
|
|
1146
|
+
try { await unlink(compressedPath).catch(() => {}); } catch {}
|
|
1147
|
+
try { await rmdir(tmpDir).catch(() => {}); } catch {}
|
|
1148
|
+
|
|
1149
|
+
// 4. 获取视觉模型配置(从系统配置读取)
|
|
1150
|
+
const { embed: ollamaCfg, vision: visionCfg } = await getOllamaConfig();
|
|
1151
|
+
|
|
1152
|
+
// 优先使用用户传入的 model 参数,其次 vision 配置,回退到 embedding 地址
|
|
1153
|
+
let visionModel = task.model;
|
|
1154
|
+
let ollamaUrl = '';
|
|
1155
|
+
let isVisionConfigured = false;
|
|
1156
|
+
|
|
1157
|
+
if (visionCfg.configured) {
|
|
1158
|
+
// 有 vision 配置(可能是智谱 API 或 Ollama 视觉)
|
|
1159
|
+
// Ollama图片走原生 /api/chat(OpenAI /chat/completions 不支持image_url)
|
|
1160
|
+
ollamaUrl = visionCfg.baseUrl ? `${visionCfg.baseUrl.replace(/\/v\d+$/,'')}/api/chat` : `${ollamaCfg.baseUrl || 'http://127.0.0.1:11434'}/api/chat`;
|
|
1161
|
+
visionModel = visionModel || visionCfg.model || null;
|
|
1162
|
+
isVisionConfigured = true;
|
|
1163
|
+
} else if (ollamaCfg.baseUrl) {
|
|
1164
|
+
// 回退到 Ollama
|
|
1165
|
+
ollamaUrl = `${ollamaCfg.baseUrl}/api/chat`;
|
|
1166
|
+
visionModel = visionModel || null;
|
|
1167
|
+
isVisionConfigured = false;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (!ollamaUrl) {
|
|
1171
|
+
return { file: fp, success: false, error: '视觉模型未配置,请在 openclaw.json 中配置 models.providers.vision 或 models.providers.embedding' };
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// 如果 Ollama 也没指定模型,报错
|
|
1175
|
+
if (ollamaUrl.includes('api/chat') && !visionModel) {
|
|
1176
|
+
return { file: fp, success: false, error: 'Ollama 视觉模型未指定,请在 openclaw.json 中配置 models.providers.vision.models[0].id' };
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const prompt = visionPrompt || '请详细描述这张图片的内容,包括物体、颜色、文字、场景等。';
|
|
1180
|
+
const isOpenAICompat = ollamaUrl.includes('/chat/completions');
|
|
1181
|
+
|
|
1182
|
+
let description = '';
|
|
1183
|
+
let error = null;
|
|
1184
|
+
|
|
1185
|
+
const buildPayload = () => {
|
|
1186
|
+
if (isOpenAICompat) {
|
|
1187
|
+
return {
|
|
1188
|
+
model: visionModel,
|
|
1189
|
+
messages: [
|
|
1190
|
+
{
|
|
1191
|
+
role: 'user',
|
|
1192
|
+
content: [
|
|
1193
|
+
{ type: 'text', text: prompt },
|
|
1194
|
+
{ type: 'image_url', image_url: { url: `data:image/jpeg;base64,${base64Image}` } },
|
|
1195
|
+
],
|
|
1196
|
+
},
|
|
1197
|
+
],
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
return {
|
|
1201
|
+
model: visionModel,
|
|
1202
|
+
stream: false,
|
|
1203
|
+
messages: [
|
|
1204
|
+
{
|
|
1205
|
+
role: 'user',
|
|
1206
|
+
content: prompt,
|
|
1207
|
+
images: [base64Image],
|
|
1208
|
+
},
|
|
1209
|
+
],
|
|
1210
|
+
};
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
const buildHeaders = () => {
|
|
1214
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
1215
|
+
if (visionCfg.apiKey) {
|
|
1216
|
+
headers['Authorization'] = `Bearer ${visionCfg.apiKey}`;
|
|
1217
|
+
}
|
|
1218
|
+
return headers;
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
const parseResponse = (data) => {
|
|
1222
|
+
if (isOpenAICompat) {
|
|
1223
|
+
return data?.choices?.[0]?.message?.content || JSON.stringify(data);
|
|
1224
|
+
}
|
|
1225
|
+
return data?.message?.content || JSON.stringify(data);
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
try {
|
|
1229
|
+
const payload = buildPayload();
|
|
1230
|
+
const resp = await fetch(ollamaUrl, {
|
|
1231
|
+
method: 'POST',
|
|
1232
|
+
headers: buildHeaders(),
|
|
1233
|
+
body: JSON.stringify(payload),
|
|
1234
|
+
signal: AbortSignal.timeout(120000),
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
if (!resp.ok) {
|
|
1238
|
+
const errText = await resp.text().catch(() => '');
|
|
1239
|
+
error = `视觉 API 返回 ${resp.status}: ${errText.substring(0, 200)}`;
|
|
1240
|
+
} else {
|
|
1241
|
+
const data = await resp.json();
|
|
1242
|
+
description = parseResponse(data);
|
|
1243
|
+
}
|
|
1244
|
+
} catch (fetchErr) {
|
|
1245
|
+
// Try fallback for Ollama: base64 in content field (some Ollama versions)
|
|
1246
|
+
if (!isOpenAICompat) {
|
|
1247
|
+
try {
|
|
1248
|
+
const resp2 = await fetch(ollamaUrl, {
|
|
1249
|
+
method: 'POST',
|
|
1250
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1251
|
+
body: JSON.stringify({
|
|
1252
|
+
model: visionModel,
|
|
1253
|
+
stream: false,
|
|
1254
|
+
messages: [
|
|
1255
|
+
{
|
|
1256
|
+
role: 'user',
|
|
1257
|
+
content: [
|
|
1258
|
+
{ type: 'text', text: prompt },
|
|
1259
|
+
{ type: 'image_url', image_url: { url: `data:image/jpeg;base64,${base64Image}` } },
|
|
1260
|
+
],
|
|
1261
|
+
},
|
|
1262
|
+
],
|
|
1263
|
+
}),
|
|
1264
|
+
signal: AbortSignal.timeout(120000),
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
if (!resp2.ok) {
|
|
1268
|
+
error = `视觉 API 失败: ${fetchErr.message} (fallback: ${resp2.status})`;
|
|
1269
|
+
} else {
|
|
1270
|
+
const data2 = await resp2.json();
|
|
1271
|
+
description = data2?.message?.content || JSON.stringify(data2);
|
|
1272
|
+
}
|
|
1273
|
+
} catch (fallbackErr) {
|
|
1274
|
+
error = `视觉 API 调用失败: ${fetchErr.message}; fallback: ${fallbackErr.message}`;
|
|
1275
|
+
}
|
|
1276
|
+
} else {
|
|
1277
|
+
error = `视觉 API 调用失败: ${fetchErr.message}`;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return {
|
|
1282
|
+
file: fp,
|
|
1283
|
+
success: !error,
|
|
1284
|
+
description: description || undefined,
|
|
1285
|
+
error: error || undefined,
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
case "sync-archive": {
|
|
1290
|
+
const { target, quick } = task;
|
|
1291
|
+
if (!target || !['E', 'G'].includes(target)) {
|
|
1292
|
+
throw Object.assign(new Error(`无效同步目标: ${target}`), { code: "BAD_REQUEST" });
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const src = join(homedir(), ".openclaw");
|
|
1296
|
+
const dst = `${target}:\\\\.openclaw`;
|
|
1297
|
+
const roboBase = ['/ZB', '/R:5', '/W:5', '/NP', '/NDL', '/NJH', '/NJS', '/XJ'];
|
|
1298
|
+
|
|
1299
|
+
if (quick) {
|
|
1300
|
+
// 快速模式:只同步 dialog 目录 + 核心 .md
|
|
1301
|
+
const dialogSrc = join(src, 'workspace', 'memory', 'dialog');
|
|
1302
|
+
const dialogDst = join(dst, 'workspace', 'memory', 'dialog');
|
|
1303
|
+
const dialogResult = await runRobocopy(dialogSrc, dialogDst, [...roboBase, '/MIR']);
|
|
1304
|
+
|
|
1305
|
+
const wsSrc = join(src, 'workspace');
|
|
1306
|
+
const wsDst = join(dst, 'workspace');
|
|
1307
|
+
const wsResult = await runRobocopy(wsSrc, wsDst, [...roboBase, '/MIR', '/IF', '*.md', '/XD', 'node_modules', '.git']);
|
|
1308
|
+
|
|
1309
|
+
return {
|
|
1310
|
+
target,
|
|
1311
|
+
mode: 'quick',
|
|
1312
|
+
dialog: { exitCode: dialogResult.exitCode, error: dialogResult.error },
|
|
1313
|
+
workspace: { exitCode: wsResult.exitCode, error: wsResult.error },
|
|
1314
|
+
timestamp: new Date().toISOString(),
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// 完整模式:同步全部,只排 node_modules
|
|
1319
|
+
const fullResult = await runRobocopy(src, dst, [...roboBase, '/MIR', '/XD', 'node_modules']);
|
|
1320
|
+
|
|
1321
|
+
return {
|
|
1322
|
+
target,
|
|
1323
|
+
mode: 'full',
|
|
1324
|
+
exitCode: fullResult.exitCode,
|
|
1325
|
+
error: fullResult.error,
|
|
1326
|
+
timestamp: new Date().toISOString(),
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
case "dispatch-subagent": {
|
|
1331
|
+
const subPrompt = task.prompt || '';
|
|
1332
|
+
const subModel = task.model || 'deepseek/deepseek-v4-flash';
|
|
1333
|
+
const subTimeout = Number(task.timeout) || 300;
|
|
1334
|
+
if (!subPrompt) throw Object.assign(new Error('dispatch-subagent missing prompt'), { code: "BAD_REQUEST" });
|
|
1335
|
+
try {
|
|
1336
|
+
const resp = await fetch('http://localhost:18792/spawn_subagent', {
|
|
1337
|
+
method: 'POST',
|
|
1338
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1339
|
+
body: JSON.stringify({
|
|
1340
|
+
prompt: subPrompt,
|
|
1341
|
+
model: subModel,
|
|
1342
|
+
timeout: subTimeout,
|
|
1343
|
+
maxRounds: 100,
|
|
1344
|
+
taskName: task.taskName || task.name || task.description || 'dispatch-subagent',
|
|
1345
|
+
batchName: task.batchName || '',
|
|
1346
|
+
groupName: task.groupName || ''
|
|
1347
|
+
}),
|
|
1348
|
+
signal: AbortSignal.timeout((subTimeout + 10) * 1000)
|
|
1349
|
+
});
|
|
1350
|
+
if (!resp.ok) throw new Error(`Sidecar 返回 ${resp.status}`);
|
|
1351
|
+
|
|
1352
|
+
// 拿到子Agent ID和结果文件路径,每15s查一次进度
|
|
1353
|
+
const spawnResult = await resp.json();
|
|
1354
|
+
const subId = spawnResult.id || '';
|
|
1355
|
+
const outputPath = spawnResult.outputPath || '';
|
|
1356
|
+
|
|
1357
|
+
if (subId && outputPath) {
|
|
1358
|
+
// 异步轮询:每15s读一次结果文件,直到完成或超时
|
|
1359
|
+
const { readFile, stat } = await import('fs/promises');
|
|
1360
|
+
const { existsSync } = await import('fs');
|
|
1361
|
+
const pollStart = Date.now();
|
|
1362
|
+
const pollTimeout = (subTimeout + 5) * 1000;
|
|
1363
|
+
|
|
1364
|
+
while (Date.now() - pollStart < pollTimeout) {
|
|
1365
|
+
await new Promise(r => setTimeout(r, 15000)); // 15s间隔
|
|
1366
|
+
|
|
1367
|
+
if (existsSync(outputPath)) {
|
|
1368
|
+
try {
|
|
1369
|
+
const stateContent = await readFile(outputPath, 'utf8');
|
|
1370
|
+
const state = JSON.parse(stateContent);
|
|
1371
|
+
const s = state.status || '';
|
|
1372
|
+
// 只在状态变化时输出
|
|
1373
|
+
if (s === 'success' || s === 'completed') {
|
|
1374
|
+
spawnResult._status = s;
|
|
1375
|
+
spawnResult._result = state.output || state.data || {};
|
|
1376
|
+
break;
|
|
1377
|
+
}
|
|
1378
|
+
if (s === 'error' || s === 'timeout' || s === 'failed') {
|
|
1379
|
+
spawnResult._status = s;
|
|
1380
|
+
spawnResult._error = state.error || '子Agent异常结束';
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
1383
|
+
} catch {}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
spawnResult._polled = (Date.now() - pollStart) + 'ms';
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
return spawnResult;
|
|
1390
|
+
} catch (e) {
|
|
1391
|
+
// Sidecar挂了 -> Worker自己fork子进程派兵
|
|
1392
|
+
try {
|
|
1393
|
+
const runnerPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'tools', 'sidecar', 'subagent-runner.cjs');
|
|
1394
|
+
const subId = 'worker-' + Date.now() + '-' + Math.random().toString(36).slice(2,6);
|
|
1395
|
+
const diarDir = join(homedir(), '.openclaw', 'workspace', 'memory', 'dialog', 'subagent');
|
|
1396
|
+
const taskDir = join(homedir(), '.openclaw', 'workspace', 'memory', 'task-states');
|
|
1397
|
+
const outputPath = join(taskDir, subId + '.json');
|
|
1398
|
+
const diaryPath = join(diarDir, subId + '.md');
|
|
1399
|
+
const argsJson = JSON.stringify({ prompt: subPrompt, model: subModel, timeout: subTimeout, outputPath, diaryPath, maxRounds: 100 });
|
|
1400
|
+
const proc = spawn(process.execPath, [runnerPath, argsJson], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, timeout: (subTimeout + 10) * 1000 });
|
|
1401
|
+
const exitCode = await new Promise((resolve) => { proc.on('exit', resolve); proc.on('error', () => resolve(-1)); });
|
|
1402
|
+
if (exitCode !== 0) throw new Error('子Agent退出码: ' + exitCode);
|
|
1403
|
+
const { readFile } = await import('fs/promises');
|
|
1404
|
+
const { existsSync } = await import('fs');
|
|
1405
|
+
if (existsSync(outputPath)) {
|
|
1406
|
+
const r = JSON.parse(await readFile(outputPath, 'utf8'));
|
|
1407
|
+
return { id: subId, status: r.status || 'completed', outputPath, diaryPath, _fallback: 'worker-fork' };
|
|
1408
|
+
}
|
|
1409
|
+
return { id: subId, status: 'completed', _fallback: 'worker-fork' };
|
|
1410
|
+
} catch (fbErr) {
|
|
1411
|
+
throw Object.assign(new Error('派兵失败(Sidecar+Worker都挂了): ' + fbErr.message), { code: 'DISPATCH_FAIL_ALL' });
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
default:
|
|
1417
|
+
throw Object.assign(new Error(`未知任务类型: ${type}`), { code: "UNKNOWN_TASK" });
|
|
1418
|
+
}
|
|
1419
|
+
}
|