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,713 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔗 任务链 — 任务链检测与chain scheduler
|
|
3
|
+
*
|
|
4
|
+
* 模仿任务因果链(因果链分析):
|
|
5
|
+
* 1. 检测 core_routeTask 中的任务链模式(如 cpu_search→cpu_batch→summarize)
|
|
6
|
+
* 2. 自动生成子任务链,用 core_dispatch 批量调度
|
|
7
|
+
* 3. 链中每一步独立存储,某一步失败不影响已成功的步骤
|
|
8
|
+
*
|
|
9
|
+
* 检测规则(built-in chain patterns):
|
|
10
|
+
* codeReview→bugFix→validate: 审查→修复→验证 ✅ 存活
|
|
11
|
+
* ⛔ search-batch-summarize, scan-batch-report, diagnose-report,
|
|
12
|
+
* research-crossCheck, dispatch-orchestrate, imageBatch-read:
|
|
13
|
+
* 依赖已删除工具,已禁用
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import crypto from "crypto";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
import { homedir } from "os";
|
|
19
|
+
import { readFile, writeFile, mkdir, readdir, stat, unlink } from "fs/promises";
|
|
20
|
+
|
|
21
|
+
// ====== 配置 ======
|
|
22
|
+
const CHAIN_DIR = join(homedir(), '.openclaw', 'workspace', 'plugins', 'sc', 'shared', 'chains');
|
|
23
|
+
const STEP_DIR = join(CHAIN_DIR, 'steps');
|
|
24
|
+
const CHAIN_LOG_RETENTION_MS = 24 * 60 * 60 * 1000; // 24小时
|
|
25
|
+
|
|
26
|
+
// ====== 内置链模式 ======
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 链模式定义:
|
|
30
|
+
* { match: [关键词数组], chain: [步骤定义], confidence: number }
|
|
31
|
+
* 每步:{ name, tool, description, dependsOn?, optional? }
|
|
32
|
+
*
|
|
33
|
+
* ⛔ 已禁用的死链(依赖已删除工具):
|
|
34
|
+
* search-batch-summarize → core_search / core_batch
|
|
35
|
+
* scan-batch-report → core_scan / core_batch
|
|
36
|
+
* diagnose-report → core_diagnose
|
|
37
|
+
* research-crossCheck → cpu_research / cpu_orchestrate
|
|
38
|
+
* dispatch-orchestrate → core_dispatch / cpu_orchestrate
|
|
39
|
+
* imageBatch-read → core_imageBatch / core_batch
|
|
40
|
+
*/
|
|
41
|
+
const CHAIN_PATTERNS = [
|
|
42
|
+
// DEAD_CHAIN
|
|
43
|
+
/* ⛔ 死链: search-batch-summarize — 依赖已删除工具(core_search/core_batch)
|
|
44
|
+
{
|
|
45
|
+
id: 'search-batch-summarize',
|
|
46
|
+
match: ['search', 'batch', '读', 'summarize', '搜'],
|
|
47
|
+
confidence: 0.85,
|
|
48
|
+
factory: (taskDesc) => ({
|
|
49
|
+
name: '搜索→批量读→汇总',
|
|
50
|
+
steps: [
|
|
51
|
+
{ name: 'search', tool: 'core_search', description: `搜索相关内容: ${taskDesc.substring(0, 80)}`, task: { type: 'search-text', keyword: taskDesc, priority: 'high' }, optional: false, outputKey: 'searchResults', dependsOn: [] },
|
|
52
|
+
{ name: 'batch', tool: 'core_batch', description: '批量读取搜索结果文件', task: { type: 'search-text', keyword: `${taskDesc} 读文件`, priority: 'high' }, optional: false, outputKey: 'batchResults', dependsOn: ['search'] },
|
|
53
|
+
{ name: 'summarize', tool: 'subagent', description: `汇总分析: ${taskDesc.substring(0, 80)}`, task: { type: 'ai-summarize', text: `${taskDesc} 汇总结果`, priority: 'normal' }, optional: true, outputKey: 'summary', dependsOn: ['batch'] },
|
|
54
|
+
],
|
|
55
|
+
}),
|
|
56
|
+
},
|
|
57
|
+
*/
|
|
58
|
+
// DEAD_CHAIN
|
|
59
|
+
/* ⛔ 死链: scan-batch-report — 依赖已删除工具(core_scan/core_batch)
|
|
60
|
+
{
|
|
61
|
+
id: 'scan-batch-report',
|
|
62
|
+
match: ['scan', '扫描', '目录', '查找', '搜', 'report'],
|
|
63
|
+
confidence: 0.80,
|
|
64
|
+
factory: (taskDesc) => ({
|
|
65
|
+
name: '扫描→读取→报告',
|
|
66
|
+
steps: [
|
|
67
|
+
{ name: 'scan', tool: 'core_scan', description: `扫描搜索: ${taskDesc.substring(0, 80)}`, task: { type: 'search-text', keyword: taskDesc, priority: 'high' }, optional: false, outputKey: 'scanResults', dependsOn: [] },
|
|
68
|
+
{ name: 'batchRead', tool: 'core_batch', description: '批量读取扫描结果', task: { type: 'search-text', keyword: `${taskDesc} 内容`, priority: 'high' }, optional: false, outputKey: 'readResults', dependsOn: ['scan'] },
|
|
69
|
+
],
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
*/
|
|
73
|
+
{
|
|
74
|
+
id: 'codeReview-bugFix-validate',
|
|
75
|
+
match: ['codeReview', 'bugFix', '审查', '修复', 'code', 'fix'],
|
|
76
|
+
confidence: 0.82,
|
|
77
|
+
factory: (taskDesc) => ({
|
|
78
|
+
name: '代码审查→修复→验证',
|
|
79
|
+
steps: [
|
|
80
|
+
{
|
|
81
|
+
name: 'review',
|
|
82
|
+
tool: 'core_codeEditor', // 🛠 修复: cpu_codeReview → core_codeEditor
|
|
83
|
+
description: `代码审查: ${taskDesc.substring(0, 80)}`,
|
|
84
|
+
task: { type: 'search-text', keyword: `${taskDesc} review`, priority: 'high' },
|
|
85
|
+
optional: false,
|
|
86
|
+
outputKey: 'reviewResults',
|
|
87
|
+
dependsOn: [],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'fix',
|
|
91
|
+
tool: 'core_codeEditor', // 🛠 修复: cpu_bugFix → core_codeEditor
|
|
92
|
+
description: `自动修复: ${taskDesc.substring(0, 80)}`,
|
|
93
|
+
task: { type: 'search-text', keyword: `${taskDesc} fix`, priority: 'high' },
|
|
94
|
+
optional: true,
|
|
95
|
+
outputKey: 'fixResults',
|
|
96
|
+
dependsOn: ['review'],
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
}),
|
|
100
|
+
},
|
|
101
|
+
// DEAD_CHAIN
|
|
102
|
+
/* ⛔ 死链: diagnose-report — 依赖已删除工具(core_diagnose)
|
|
103
|
+
{
|
|
104
|
+
id: 'diagnose-report',
|
|
105
|
+
match: ['diagnose', '诊断', '体检', 'health'],
|
|
106
|
+
confidence: 0.78,
|
|
107
|
+
factory: (taskDesc) => ({
|
|
108
|
+
name: '系统诊断→报告',
|
|
109
|
+
steps: [
|
|
110
|
+
{ name: 'diagnose', tool: 'core_diagnose', description: `系统诊断: ${taskDesc.substring(0, 80)}`, task: { type: 'diagnose', priority: 'high' }, optional: false, outputKey: 'diagnoseResults', dependsOn: [] },
|
|
111
|
+
{ name: 'report', tool: 'subagent', description: '生成诊断报告', task: { type: 'ai-summarize', text: `${taskDesc} 生成报告`, priority: 'normal' }, optional: true, outputKey: 'report', dependsOn: ['diagnose'] },
|
|
112
|
+
],
|
|
113
|
+
}),
|
|
114
|
+
},
|
|
115
|
+
*/
|
|
116
|
+
// DEAD_CHAIN
|
|
117
|
+
/* ⛔ 死链: research-crossCheck — 依赖已删除工具(cpu_research/cpu_orchestrate)
|
|
118
|
+
{
|
|
119
|
+
id: 'research-crossCheck',
|
|
120
|
+
match: ['research', '调研', '研究', 'investigate', '查', '分析'],
|
|
121
|
+
confidence: 0.75,
|
|
122
|
+
factory: (taskDesc) => ({
|
|
123
|
+
name: '多维调研→交叉验证',
|
|
124
|
+
steps: [
|
|
125
|
+
{ name: 'multiAngle', tool: 'cpu_research', // TODO: remap
|
|
126
|
+
description: `多角度调研: ${taskDesc.substring(0, 80)}`, task: { type: 'search-text', keyword: taskDesc, priority: 'high' }, optional: false, outputKey: 'researchResults', dependsOn: [] },
|
|
127
|
+
{ name: 'crossCheck', tool: 'cpu_orchestrate', // TODO: remap
|
|
128
|
+
description: '交叉验证结果', task: { type: 'search-text', keyword: `${taskDesc} cross check`, priority: 'normal' }, optional: true, outputKey: 'crossCheckResults', dependsOn: ['multiAngle'] },
|
|
129
|
+
],
|
|
130
|
+
}),
|
|
131
|
+
},
|
|
132
|
+
*/
|
|
133
|
+
// DEAD_CHAIN
|
|
134
|
+
/* ⛔ 死链: dispatch-orchestrate — 依赖已删除工具(core_dispatch/cpu_orchestrate)
|
|
135
|
+
{
|
|
136
|
+
id: 'dispatch-orchestrate',
|
|
137
|
+
match: ['dispatch', '编排', 'orchestrate', '批量', '并发'],
|
|
138
|
+
confidence: 0.72,
|
|
139
|
+
factory: (taskDesc) => ({
|
|
140
|
+
name: '批量派发→编排',
|
|
141
|
+
steps: [
|
|
142
|
+
{ name: 'dispatch', tool: 'core_dispatch', description: `批量派发: ${taskDesc.substring(0, 80)}`, task: { type: 'search-text', keyword: taskDesc, priority: 'high' }, optional: false, outputKey: 'dispatchResults', dependsOn: [] },
|
|
143
|
+
{ name: 'orchestrate', tool: 'cpu_orchestrate', // TODO: remap
|
|
144
|
+
description: '结果编排', task: { type: 'search-text', keyword: `${taskDesc} orchestrate`, priority: 'normal' }, optional: true, outputKey: 'orchestrateResults', dependsOn: ['dispatch'] },
|
|
145
|
+
],
|
|
146
|
+
}),
|
|
147
|
+
},
|
|
148
|
+
*/
|
|
149
|
+
// DEAD_CHAIN
|
|
150
|
+
/* ⛔ 死链: imageBatch-read — 依赖已删除工具(core_imageBatch/core_batch)
|
|
151
|
+
{
|
|
152
|
+
id: 'imageBatch-read',
|
|
153
|
+
match: ['image', '图片', 'vision', 'visual', '视觉', 'batch'],
|
|
154
|
+
confidence: 0.74,
|
|
155
|
+
factory: (taskDesc) => ({
|
|
156
|
+
name: '图片批量分析→结果读取',
|
|
157
|
+
steps: [
|
|
158
|
+
{ name: 'imageBatch', tool: 'core_imageBatch', description: `图片批量分析: ${taskDesc.substring(0, 80)}`, task: { type: 'image-batch', keyword: taskDesc, priority: 'high' }, optional: false, outputKey: 'imageResults', dependsOn: [] },
|
|
159
|
+
{ name: 'read', tool: 'core_batch', description: '批量读取分析结果', task: { type: 'search-text', keyword: `${taskDesc} 结果`, priority: 'normal' }, optional: true, outputKey: 'readResults', dependsOn: ['imageBatch'] },
|
|
160
|
+
],
|
|
161
|
+
}),
|
|
162
|
+
},
|
|
163
|
+
*/
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
// ====== 链检测 ======
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 检测任务描述是否匹配某个链模式
|
|
170
|
+
* @param {string} taskDesc - 原始任务描述
|
|
171
|
+
* @returns {{ matched: boolean, chain: object|null, confidence: number, chainId: string|null }}
|
|
172
|
+
*/
|
|
173
|
+
function detectChain(taskDesc) {
|
|
174
|
+
if (!taskDesc || typeof taskDesc !== 'string') {
|
|
175
|
+
return { matched: false, chain: null, confidence: 0, chainId: null };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const query = taskDesc.toLowerCase();
|
|
179
|
+
|
|
180
|
+
// 按置信度排序,选最佳匹配
|
|
181
|
+
let bestMatch = null;
|
|
182
|
+
let bestScore = 0;
|
|
183
|
+
|
|
184
|
+
for (const pattern of CHAIN_PATTERNS) {
|
|
185
|
+
let matchCount = 0;
|
|
186
|
+
for (const kw of pattern.match) {
|
|
187
|
+
if (query.includes(kw.toLowerCase())) matchCount++;
|
|
188
|
+
}
|
|
189
|
+
const score = (matchCount / pattern.match.length) * pattern.confidence;
|
|
190
|
+
if (score > bestScore && matchCount >= 1) {
|
|
191
|
+
bestScore = score;
|
|
192
|
+
bestMatch = pattern;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 🧠 设计决策:bestScore >= 0.2 为链检测触发阈值。
|
|
197
|
+
// 存活链仅剩 codeReview-bugFix-validate (0.82)
|
|
198
|
+
// 其余6条死链已注释(search-batch-summarize, scan-batch-report,
|
|
199
|
+
// diagnose-report, research-crossCheck, dispatch-orchestrate, imageBatch-read)
|
|
200
|
+
// 0.2 拦截过多链(噪声),低于 0.5 防漏掉真正有链模式的任务。
|
|
201
|
+
// 数学:0.2 = 1/5 模式匹配度,最低要求至少 1 个关键词命中且置信度 > 0.2
|
|
202
|
+
if (bestMatch && bestScore >= 0.2) {
|
|
203
|
+
const chain = bestMatch.factory(taskDesc);
|
|
204
|
+
return {
|
|
205
|
+
matched: true,
|
|
206
|
+
chain,
|
|
207
|
+
confidence: Math.round(bestScore * 100) / 100,
|
|
208
|
+
chainId: bestMatch.id,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { matched: false, chain: null, confidence: 0, chainId: null };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ====== 链步骤persist ======
|
|
216
|
+
|
|
217
|
+
async function ensureChainDirs() {
|
|
218
|
+
try { await mkdir(CHAIN_DIR, { recursive: true }); } catch {}
|
|
219
|
+
try { await mkdir(STEP_DIR, { recursive: true }); } catch {}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 生成全局唯一的链ID
|
|
224
|
+
*/
|
|
225
|
+
function genChainId() {
|
|
226
|
+
return `chain_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* persist一个链实例到 shared/chains/
|
|
231
|
+
* @param {object} chainDef - { chainId, name, steps, taskDesc, status }
|
|
232
|
+
* @returns {string} 链ID
|
|
233
|
+
*/
|
|
234
|
+
async function persistChain(chainDef) {
|
|
235
|
+
await ensureChainDirs();
|
|
236
|
+
const chainId = chainDef.chainId || genChainId();
|
|
237
|
+
const safeName = chainId.replace(/[^a-zA-Z0-9_\-]/g, '_');
|
|
238
|
+
const fp = join(CHAIN_DIR, `${safeName}.json`);
|
|
239
|
+
const entry = {
|
|
240
|
+
...chainDef,
|
|
241
|
+
chainId,
|
|
242
|
+
createdAt: Date.now(),
|
|
243
|
+
stepStatus: chainDef.stepStatus || chainDef.steps.map(s => ({
|
|
244
|
+
name: s.name,
|
|
245
|
+
status: 'pending',
|
|
246
|
+
result: null,
|
|
247
|
+
error: null,
|
|
248
|
+
startedAt: null,
|
|
249
|
+
completedAt: null,
|
|
250
|
+
})),
|
|
251
|
+
};
|
|
252
|
+
await writeFile(fp, JSON.stringify(entry, null, 2), 'utf-8');
|
|
253
|
+
return chainId;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 更新链实例中的某一步状态
|
|
258
|
+
* @param {string} chainId
|
|
259
|
+
* @param {string} stepName
|
|
260
|
+
* @param {string} status - pending|running|success|failed|skipped
|
|
261
|
+
* @param {object|null} result
|
|
262
|
+
* @param {string|null} error
|
|
263
|
+
*/
|
|
264
|
+
async function updateChainStepStatus(chainId, stepName, status, result = null, error = null) {
|
|
265
|
+
const safeName = chainId.replace(/[^a-zA-Z0-9_\-]/g, '_');
|
|
266
|
+
const fp = join(CHAIN_DIR, `${safeName}.json`);
|
|
267
|
+
try {
|
|
268
|
+
const raw = await readFile(fp, 'utf-8');
|
|
269
|
+
const chain = JSON.parse(raw);
|
|
270
|
+
const step = chain.stepStatus.find(s => s.name === stepName);
|
|
271
|
+
if (step) {
|
|
272
|
+
step.status = status;
|
|
273
|
+
if (status === 'running') step.startedAt = Date.now();
|
|
274
|
+
if (status === 'success' || status === 'failed') step.completedAt = Date.now();
|
|
275
|
+
if (result !== null) step.result = result;
|
|
276
|
+
if (error !== null) step.error = error;
|
|
277
|
+
chain.updatedAt = Date.now();
|
|
278
|
+
// 检查是否全部完成
|
|
279
|
+
const allDone = chain.stepStatus.every(s => s.status === 'success' || s.status === 'failed' || s.status === 'skipped');
|
|
280
|
+
if (allDone) chain.status = 'completed';
|
|
281
|
+
await writeFile(fp, JSON.stringify(chain, null, 2), 'utf-8');
|
|
282
|
+
}
|
|
283
|
+
return true;
|
|
284
|
+
} catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 读取链状态
|
|
291
|
+
*/
|
|
292
|
+
async function readChain(chainId) {
|
|
293
|
+
const safeName = chainId.replace(/[^a-zA-Z0-9_\-]/g, '_');
|
|
294
|
+
const fp = join(CHAIN_DIR, `${safeName}.json`);
|
|
295
|
+
try {
|
|
296
|
+
const raw = await readFile(fp, 'utf-8');
|
|
297
|
+
return JSON.parse(raw);
|
|
298
|
+
} catch {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 清理超过24小时的链日志
|
|
305
|
+
*/
|
|
306
|
+
async function cleanupChainLogs() {
|
|
307
|
+
try {
|
|
308
|
+
await ensureChainDirs();
|
|
309
|
+
const files = await readdir(CHAIN_DIR);
|
|
310
|
+
const now = Date.now();
|
|
311
|
+
let cleaned = 0;
|
|
312
|
+
for (const f of files) {
|
|
313
|
+
if (!f.endsWith('.json')) continue;
|
|
314
|
+
const fp = join(CHAIN_DIR, f);
|
|
315
|
+
try {
|
|
316
|
+
const st = await stat(fp);
|
|
317
|
+
if (now - st.mtimeMs > CHAIN_LOG_RETENTION_MS) {
|
|
318
|
+
await unlink(fp);
|
|
319
|
+
cleaned++;
|
|
320
|
+
}
|
|
321
|
+
} catch {}
|
|
322
|
+
}
|
|
323
|
+
// 清理 steps/ 子目录
|
|
324
|
+
const stepFiles = await readdir(STEP_DIR).catch(() => []);
|
|
325
|
+
for (const f of stepFiles) {
|
|
326
|
+
if (!f.endsWith('.json')) continue;
|
|
327
|
+
const fp = join(STEP_DIR, f);
|
|
328
|
+
try {
|
|
329
|
+
const st = await stat(fp);
|
|
330
|
+
if (now - st.mtimeMs > CHAIN_LOG_RETENTION_MS) {
|
|
331
|
+
await unlink(fp);
|
|
332
|
+
cleaned++;
|
|
333
|
+
}
|
|
334
|
+
} catch {}
|
|
335
|
+
}
|
|
336
|
+
if (cleaned > 0) console.log(`[chain scheduler] 🧹 清理了 ${cleaned} 个过期链日志`);
|
|
337
|
+
} catch {}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ====== 独立步骤执行 ======
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 执行链中的单一步骤(独立于其他步骤)
|
|
344
|
+
* 失败不影响已成功的步骤
|
|
345
|
+
* @param {object} step - 步骤定义
|
|
346
|
+
* @param {object} pool - Worker 池引用
|
|
347
|
+
* @returns {Promise<object>} { success, result, error }
|
|
348
|
+
*/
|
|
349
|
+
async function executeStep(step, pool) {
|
|
350
|
+
if (!step || !step.task) {
|
|
351
|
+
return { success: false, error: 'missing任务定义' };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const result = await pool.exec(step.task, step.task?.priority || 'normal');
|
|
356
|
+
return { success: true, result };
|
|
357
|
+
} catch (err) {
|
|
358
|
+
// 步骤独立存储失败,不抛出
|
|
359
|
+
console.warn(`[chain scheduler] ⚠️ 步骤 "${step.name}" 失败: ${err.message}`);
|
|
360
|
+
return { success: false, error: err.message };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* 执行完整的链(按依赖拓扑排序串行+并行)
|
|
366
|
+
* @param {object} chainDef - { steps, stepStatus }
|
|
367
|
+
* @param {object} pool - Worker 池引用
|
|
368
|
+
* @param {function} updateFn - (stepName, status, result, error) => void
|
|
369
|
+
* @returns {Array<object>} 每步的执行结果
|
|
370
|
+
*/
|
|
371
|
+
async function executeChain(chainDef, pool, updateFn) {
|
|
372
|
+
const steps = chainDef.steps;
|
|
373
|
+
const updateStatus = updateFn || ((name, status, result, err) =>
|
|
374
|
+
updateChainStepStatus(chainDef.chainId, name, status, result, err));
|
|
375
|
+
|
|
376
|
+
// 构建依赖图
|
|
377
|
+
const depGraph = new Map();
|
|
378
|
+
for (const step of steps) {
|
|
379
|
+
deps: for (const dep of (step.dependsOn || [])) {
|
|
380
|
+
if (steps.find(s => s.name === dep)) {
|
|
381
|
+
if (!depGraph.has(dep)) depGraph.set(dep, []);
|
|
382
|
+
depGraph.get(dep).push(step.name);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 拓扑排序:先找无依赖的,逐步移除
|
|
388
|
+
const executed = new Set();
|
|
389
|
+
const skipped = new Set();
|
|
390
|
+
const results = [];
|
|
391
|
+
|
|
392
|
+
// 找出入度为0的步骤(没有依赖的)
|
|
393
|
+
async function tryExecuteReadyStep() {
|
|
394
|
+
for (const step of steps) {
|
|
395
|
+
if (executed.has(step.name) || skipped.has(step.name)) continue;
|
|
396
|
+
|
|
397
|
+
const deps = step.dependsOn || [];
|
|
398
|
+
const allDepsDone = deps.every(d => executed.has(d));
|
|
399
|
+
const anyDepFailed = deps.some(d => {
|
|
400
|
+
const r = results.find(rr => rr.name === d);
|
|
401
|
+
return r && !r.success && !step.optional;
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (allDepsDone) {
|
|
405
|
+
// 如果有依赖失败了且此步骤不(可选/必须),跳过
|
|
406
|
+
if (anyDepFailed) {
|
|
407
|
+
skipped.add(step.name);
|
|
408
|
+
await updateStatus(step.name, 'skipped', null, '依赖失败,跳过');
|
|
409
|
+
results.push({ name: step.name, success: false, skipped: true, error: '依赖失败,跳过' });
|
|
410
|
+
continue; // 继续检查其他可执行的
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 执行此步骤
|
|
414
|
+
executed.add(step.name);
|
|
415
|
+
await updateStatus(step.name, 'running');
|
|
416
|
+
const stepResult = await executeStep(step, pool);
|
|
417
|
+
if (stepResult.success) {
|
|
418
|
+
await updateStatus(step.name, 'success', stepResult.result);
|
|
419
|
+
} else {
|
|
420
|
+
const status = step.optional ? 'skipped' : 'failed';
|
|
421
|
+
await updateStatus(step.name, status, null, stepResult.error);
|
|
422
|
+
}
|
|
423
|
+
results.push({ name: step.name, ...stepResult });
|
|
424
|
+
return true; // 执行了一个步骤,重新检查
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 检查是否有步骤永远无法执行(死锁或全部跳过)
|
|
429
|
+
const remaining = steps.filter(s => !executed.has(s.name) && !skipped.has(s.name));
|
|
430
|
+
for (const step of remaining) {
|
|
431
|
+
const deps = step.dependsOn || [];
|
|
432
|
+
const deadLock = deps.some(d => remaining.some(r => r.name === d) && !executed.has(d));
|
|
433
|
+
if (deadLock) {
|
|
434
|
+
skipped.add(step.name);
|
|
435
|
+
await updateStatus(step.name, 'skipped', null, '死锁依赖');
|
|
436
|
+
results.push({ name: step.name, success: false, skipped: true, error: '死锁依赖' });
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return false; // 没有可执行的步骤了
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 主循环:不断执行ready步骤直到全部完成
|
|
445
|
+
for (let i = 0; i < steps.length * 2; i++) {
|
|
446
|
+
const didRun = await tryExecuteReadyStep();
|
|
447
|
+
if (!didRun) break; // 没有ready步骤了
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return results;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ====== 集成到 core_routeTask 的路由结果 ======
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 检查任务是否匹配链模式,返回链式路由决策
|
|
457
|
+
* @param {string} taskDesc - 原始任务描述
|
|
458
|
+
* @param {object} quickResult - core_routeTask 的小树结果
|
|
459
|
+
* @returns {object|null} 链式路由结果或null
|
|
460
|
+
*/
|
|
461
|
+
function getChainRoute(taskDesc, quickResult) {
|
|
462
|
+
const chainMatch = detectChain(taskDesc);
|
|
463
|
+
|
|
464
|
+
if (!chainMatch.matched) {
|
|
465
|
+
// 检查 quickResult 中是否有链式线索
|
|
466
|
+
// 如 route-quick 返回了多个阶段的操作
|
|
467
|
+
if (quickResult?.chainHint) {
|
|
468
|
+
const hint = quickResult.chainHint;
|
|
469
|
+
// 尝试根据提示构建链
|
|
470
|
+
// TODO: 移除调试日志 console.log(`[chain scheduler] 🔗 route-quick 返回了链提示: ${hint}`);
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const chainId = genChainId();
|
|
476
|
+
const chainDef = {
|
|
477
|
+
chainId,
|
|
478
|
+
name: chainMatch.chain.name,
|
|
479
|
+
steps: chainMatch.chain.steps,
|
|
480
|
+
taskDesc: taskDesc.substring(0, 200),
|
|
481
|
+
status: 'created',
|
|
482
|
+
stepStatus: chainMatch.chain.steps.map(s => ({
|
|
483
|
+
name: s.name,
|
|
484
|
+
status: 'pending',
|
|
485
|
+
result: null,
|
|
486
|
+
error: null,
|
|
487
|
+
startedAt: null,
|
|
488
|
+
completedAt: null,
|
|
489
|
+
})),
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// 异步persist(不阻塞返回)
|
|
493
|
+
persistChain(chainDef).catch(err => {
|
|
494
|
+
console.warn(`[chain scheduler] ⚠️ 链persist失败: ${err.message}`);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
matched: true,
|
|
499
|
+
chainId,
|
|
500
|
+
confidence: chainMatch.confidence,
|
|
501
|
+
chain: {
|
|
502
|
+
name: chainMatch.chain.name,
|
|
503
|
+
totalSteps: chainMatch.chain.steps.length,
|
|
504
|
+
steps: chainMatch.chain.steps.map(s => ({
|
|
505
|
+
name: s.name,
|
|
506
|
+
tool: s.tool,
|
|
507
|
+
description: s.description,
|
|
508
|
+
dependsOn: s.dependsOn,
|
|
509
|
+
optional: s.optional,
|
|
510
|
+
})),
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* 执行已检测到的链
|
|
517
|
+
* @param {string} chainId - persist的链ID
|
|
518
|
+
* @param {object} pool - Worker 池引用
|
|
519
|
+
* @returns {Promise<object>} 执行结果
|
|
520
|
+
*/
|
|
521
|
+
async function executeDetectedChain(chainId, pool) {
|
|
522
|
+
const chain = await readChain(chainId);
|
|
523
|
+
if (!chain) {
|
|
524
|
+
return { success: false, error: `链 ${chainId} 未找到` };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const allSteps = chain.steps;
|
|
528
|
+
const depGraph = new Map();
|
|
529
|
+
for (const step of allSteps) {
|
|
530
|
+
const deps = step.dependsOn || [];
|
|
531
|
+
for (const dep of deps) {
|
|
532
|
+
if (!depGraph.has(dep)) depGraph.set(dep, []);
|
|
533
|
+
depGraph.get(dep).push(step.name);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// 获取ready的步骤(所有依赖已完成)
|
|
538
|
+
function getReadySteps(completed) {
|
|
539
|
+
return allSteps.filter(s => {
|
|
540
|
+
if (completed.has(s.name)) return false;
|
|
541
|
+
const deps = s.dependsOn || [];
|
|
542
|
+
return deps.every(d => completed.has(d));
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const completed = new Set();
|
|
547
|
+
const results = [];
|
|
548
|
+
|
|
549
|
+
while (completed.size < allSteps.length) {
|
|
550
|
+
const ready = getReadySteps(completed);
|
|
551
|
+
if (ready.length === 0) break;
|
|
552
|
+
|
|
553
|
+
// 并行执行ready步骤
|
|
554
|
+
const readyResults = await Promise.allSettled(
|
|
555
|
+
ready.map(async (step) => {
|
|
556
|
+
await updateChainStepStatus(chainId, step.name, 'running');
|
|
557
|
+
const result = await executeStep(step, pool);
|
|
558
|
+
if (result.success) {
|
|
559
|
+
await updateChainStepStatus(chainId, step.name, 'success', result.result);
|
|
560
|
+
completed.add(step.name);
|
|
561
|
+
} else if (step.optional) {
|
|
562
|
+
await updateChainStepStatus(chainId, step.name, 'skipped', null, result.error);
|
|
563
|
+
completed.add(step.name); // 可选步骤失败也算完成
|
|
564
|
+
} else {
|
|
565
|
+
await updateChainStepStatus(chainId, step.name, 'failed', null, result.error);
|
|
566
|
+
completed.add(step.name); // 🛠 修复: 非可选步骤失败也必须加入 completed,防止死循环
|
|
567
|
+
// 非可选步骤失败 → 标记其依赖链上的所有后序步骤为 skipped
|
|
568
|
+
const cascadeSkip = [];
|
|
569
|
+
function markCascade(name) {
|
|
570
|
+
const dependents = depGraph.get(name) || [];
|
|
571
|
+
for (const dep of dependents) {
|
|
572
|
+
if (!completed.has(dep)) {
|
|
573
|
+
cascadeSkip.push(dep);
|
|
574
|
+
completed.add(dep);
|
|
575
|
+
markCascade(dep);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
markCascade(step.name);
|
|
580
|
+
for (const skipName of cascadeSkip) {
|
|
581
|
+
await updateChainStepStatus(chainId, skipName, 'skipped', null, '因前置步骤失败跳过');
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return { name: step.name, ...result };
|
|
585
|
+
})
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
for (const r of readyResults) {
|
|
589
|
+
if (r.status === 'fulfilled') results.push(r.value);
|
|
590
|
+
else results.push({ name: 'unknown', success: false, error: r.reason?.message });
|
|
591
|
+
|
|
592
|
+
// 如果非可选步骤失败则停止调度新步骤
|
|
593
|
+
if (r.status === 'fulfilled' && !r.value.success && !ready.find(s => s.name === r.value.name)?.optional) {
|
|
594
|
+
// 已通过 cascadeSkip 处理
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return { success: true, chainId, results };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ====== core_routeTask 集成 ======
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* 在 core_routeTask 的 L5+ 路由中检测链模式
|
|
606
|
+
* 返回链信息并persist,可由调用方调 executeDetectedChain 执行
|
|
607
|
+
*/
|
|
608
|
+
function enrichRouteWithChain(routeResult, taskDesc) {
|
|
609
|
+
if (!taskDesc) return routeResult;
|
|
610
|
+
|
|
611
|
+
// 只对 subagent/路由类任务尝试链检测
|
|
612
|
+
// 🛠 修复: 移除已删除的 cpu_orchestrate
|
|
613
|
+
const candidates = ['subagent', 'core_dispatch'];
|
|
614
|
+
const target = routeResult.decision || routeResult.recommendedTool || routeResult.strategy;
|
|
615
|
+
if (!candidates.includes(target) && !routeResult.strategy?.includes('chain')) return routeResult;
|
|
616
|
+
|
|
617
|
+
const chainMatch = getChainRoute(taskDesc, routeResult);
|
|
618
|
+
if (!chainMatch) return routeResult;
|
|
619
|
+
|
|
620
|
+
return {
|
|
621
|
+
...routeResult,
|
|
622
|
+
chainMode: true,
|
|
623
|
+
chain: chainMatch,
|
|
624
|
+
note: `${routeResult.note || ''} | 🔗 任务链检测: ${chainMatch.chain.name} (${chainMatch.chain.totalSteps}步,链ID=${chainMatch.chainId})`,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ====== 公开 API 汇总 ======
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* 列出所有链,按更新时间倒序排列
|
|
632
|
+
*
|
|
633
|
+
* 性能说明(问题13已审):
|
|
634
|
+
* - limit=50 已到位,防止返回过多结果
|
|
635
|
+
* - stat() 在 readFile+JSON.parse 之前过滤过期文件,避免不必要的大文件读取
|
|
636
|
+
* - 24h 清理兜底:cleanupChainLogs 每小时执行一次,删除超24小时的文件
|
|
637
|
+
* - 因此目录中文件数有自然上限(最多24小时内的文件),stat→readFile+JSON.parse 性能可接受
|
|
638
|
+
* - 排序后 slice(limit) 确保返回结果受控
|
|
639
|
+
*
|
|
640
|
+
* @param {number} [limit=50] - 最多返回数
|
|
641
|
+
* @returns {Promise<Array<{chainId, name, status, stepCount, createdAt, updatedAt}>>}
|
|
642
|
+
*/
|
|
643
|
+
async function listChains(limit = 50) {
|
|
644
|
+
try {
|
|
645
|
+
await ensureChainDirs();
|
|
646
|
+
const files = await readdir(CHAIN_DIR);
|
|
647
|
+
const now = Date.now();
|
|
648
|
+
const chains = [];
|
|
649
|
+
|
|
650
|
+
for (const f of files) {
|
|
651
|
+
if (!f.endsWith('.json')) continue;
|
|
652
|
+
|
|
653
|
+
const fp = join(CHAIN_DIR, f);
|
|
654
|
+
try {
|
|
655
|
+
// 快速过滤:stat 检查 24h 过期,避免不必要的 readFile
|
|
656
|
+
const st = await stat(fp);
|
|
657
|
+
if (now - st.mtimeMs > CHAIN_LOG_RETENTION_MS) continue;
|
|
658
|
+
|
|
659
|
+
// 只读基本信息(不解析完整内容以节省性能)
|
|
660
|
+
const raw = await readFile(fp, 'utf-8');
|
|
661
|
+
const chain = JSON.parse(raw);
|
|
662
|
+
chains.push({
|
|
663
|
+
chainId: chain.chainId || f.replace('.json', ''),
|
|
664
|
+
name: chain.name || 'unnamed',
|
|
665
|
+
status: chain.status || 'unknown',
|
|
666
|
+
stepCount: (chain.steps || chain.stepStatus || []).length,
|
|
667
|
+
createdAt: chain.createdAt || null,
|
|
668
|
+
updatedAt: chain.updatedAt || chain.createdAt || st.mtimeMs,
|
|
669
|
+
});
|
|
670
|
+
} catch {
|
|
671
|
+
// 损坏的JSON跳过
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// 按 updatedAt 倒序
|
|
677
|
+
chains.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
678
|
+
|
|
679
|
+
return chains.slice(0, Math.max(1, limit));
|
|
680
|
+
} catch {
|
|
681
|
+
return [];
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
export {
|
|
686
|
+
detectChain,
|
|
687
|
+
getChainRoute,
|
|
688
|
+
enrichRouteWithChain,
|
|
689
|
+
executeDetectedChain,
|
|
690
|
+
executeChain,
|
|
691
|
+
executeStep,
|
|
692
|
+
persistChain,
|
|
693
|
+
readChain,
|
|
694
|
+
updateChainStepStatus,
|
|
695
|
+
cleanupChainLogs,
|
|
696
|
+
genChainId,
|
|
697
|
+
listChains,
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
export default {
|
|
701
|
+
detectChain,
|
|
702
|
+
getChainRoute,
|
|
703
|
+
enrichRouteWithChain,
|
|
704
|
+
executeDetectedChain,
|
|
705
|
+
executeChain,
|
|
706
|
+
executeStep,
|
|
707
|
+
persistChain,
|
|
708
|
+
readChain,
|
|
709
|
+
updateChainStepStatus,
|
|
710
|
+
cleanupChainLogs,
|
|
711
|
+
genChainId,
|
|
712
|
+
listChains,
|
|
713
|
+
};
|