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,667 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦞 sc — 多阶段流水线引擎 (ESM)
|
|
3
|
+
*
|
|
4
|
+
* 声明式多阶段任务编排,支持先后依赖和并行阶段。
|
|
5
|
+
*
|
|
6
|
+
* 设计:
|
|
7
|
+
* 1. DAG解析 — 自动解析 stages 依赖关系
|
|
8
|
+
* 2. 拓扑排序 — 按依赖拆分层级(同层可并行,异层串行)
|
|
9
|
+
* 3. 上下文传递 — 每阶段拿到上游所有输出
|
|
10
|
+
* 4. 容错 — 逐阶段熔断/独立超时/错误收集
|
|
11
|
+
*
|
|
12
|
+
* 使用 core_dispatch + shared-fs 作为基础设施。
|
|
13
|
+
*
|
|
14
|
+
* @module pipeline-engine
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
import { homedir } from "os";
|
|
19
|
+
import { writeFile, readFile, mkdir, unlink } from "fs/promises";
|
|
20
|
+
import crypto from "crypto";
|
|
21
|
+
|
|
22
|
+
// ====== 类型定义(JSDoc 注释) ======
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} StageDef
|
|
26
|
+
* @property {string} name - 阶段名称(唯一标识)
|
|
27
|
+
* @property {('exec'|'pool'|'fn')} [mode='pool'] - 执行模式
|
|
28
|
+
* @property {Object} task - 任务定义
|
|
29
|
+
* @property {string[]} [dependsOn] - 依赖的前置阶段名列表
|
|
30
|
+
* @property {boolean} [optional=false] - 可选阶段(失败不终止流水线)
|
|
31
|
+
* @property {number} [timeoutMs] - 本阶段超时(ms)
|
|
32
|
+
* @property {Object} [retry] - retry策略
|
|
33
|
+
* @property {number} [retry.max=0] - 最大retry次数
|
|
34
|
+
* @property {number} [retry.delayMs=1000] - retry间隔
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} PipelineDef
|
|
39
|
+
* @property {string} [name='unnamed'] - 流水线名称
|
|
40
|
+
* @property {StageDef[]} stages - 阶段定义列表
|
|
41
|
+
* @property {Object} [params] - 全局参数
|
|
42
|
+
* @property {string} [fallback='abort'] - 阶段失败策略: 'abort'|'skip-dependents'|'continue'
|
|
43
|
+
* @property {number} [stageTimeoutMs=120000] - 阶段默认超时
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} StageResult
|
|
48
|
+
* @property {string} name - 阶段名
|
|
49
|
+
* @property {'pending'|'running'|'success'|'failed'|'skipped'} status
|
|
50
|
+
* @property {*} [output] - 阶段输出
|
|
51
|
+
* @property {string} [error] - 错误信息(仅 failed)
|
|
52
|
+
* @property {number} durationMs - 执行耗时
|
|
53
|
+
* @property {boolean} [optional=false]
|
|
54
|
+
* @property {number} [attempts=1]- retry次数
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {Object} PipelineResult
|
|
59
|
+
* @property {string} name - 流水线名称
|
|
60
|
+
* @property {'running'|'completed'|'failed'|'aborted'} status
|
|
61
|
+
* @property {number} totalStages - 总阶段数
|
|
62
|
+
* @property {StageResult[]} stages - 各阶段结果
|
|
63
|
+
* @property {number} durationMs - 总耗时
|
|
64
|
+
* @property {string} [error] - 流水线级错误
|
|
65
|
+
* @property {Object} [finalContext] - 最终上下文
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
// ====== 内部常量 ======
|
|
69
|
+
const PIPELINE_DIR = join(homedir(), '.openclaw', 'workspace', 'memory', 'pipeline');
|
|
70
|
+
const PID_LENGTH = 8;
|
|
71
|
+
|
|
72
|
+
// ====== 工具函数 ======
|
|
73
|
+
|
|
74
|
+
/** 生成全局唯一的流水线实例 ID */
|
|
75
|
+
function genPipelineId() {
|
|
76
|
+
return crypto.randomUUID().slice(0, PID_LENGTH);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 写入阶段输出到persist文件(供后续阶段读取) */
|
|
80
|
+
async function writeStageOutput(pipeId, stageName, data) {
|
|
81
|
+
await mkdir(PIPELINE_DIR, { recursive: true });
|
|
82
|
+
const fp = join(PIPELINE_DIR, `${pipeId}__${stageName}.json`);
|
|
83
|
+
const tmp = fp + '.tmp.' + crypto.randomBytes(4).toString('hex');
|
|
84
|
+
await writeFile(tmp, JSON.stringify({ pipeId, stageName, data, ts: Date.now() }, null, 2), 'utf-8');
|
|
85
|
+
try {
|
|
86
|
+
const { rename } = await import('fs/promises');
|
|
87
|
+
await rename(tmp, fp);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// 原子重命名失败,尝试直接覆盖
|
|
90
|
+
await writeFile(fp, JSON.stringify({ pipeId, stageName, data, ts: Date.now() }, null, 2), 'utf-8');
|
|
91
|
+
await unlink(tmp).catch(() => {});
|
|
92
|
+
}
|
|
93
|
+
return fp;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 读取上游阶段的输出 */
|
|
97
|
+
async function readStageOutput(pipeId, stageName) {
|
|
98
|
+
const fp = join(PIPELINE_DIR, `${pipeId}__${stageName}.json`);
|
|
99
|
+
try {
|
|
100
|
+
const raw = await readFile(fp, 'utf-8');
|
|
101
|
+
return JSON.parse(raw).data;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 清理流水线产生的临时文件 */
|
|
108
|
+
async function cleanupPipeline(pipeId) {
|
|
109
|
+
const { readdir, unlink: ul } = await import('fs/promises');
|
|
110
|
+
try {
|
|
111
|
+
const files = await readdir(PIPELINE_DIR);
|
|
112
|
+
const toDelete = files.filter(f => f.startsWith(pipeId + '__'));
|
|
113
|
+
for (const f of toDelete) {
|
|
114
|
+
await ul(join(PIPELINE_DIR, f)).catch(() => {});
|
|
115
|
+
}
|
|
116
|
+
} catch {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 从清理时间 >1h 的旧流水线文件
|
|
121
|
+
*/
|
|
122
|
+
async function cleanupOldPipelines() {
|
|
123
|
+
const { readdir, stat, unlink: ul } = await import('fs/promises');
|
|
124
|
+
try {
|
|
125
|
+
if (!await mkdir(PIPELINE_DIR, { recursive: true }).then(() => true).catch(() => false)) return;
|
|
126
|
+
const files = await readdir(PIPELINE_DIR);
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
let count = 0;
|
|
129
|
+
for (const f of files) {
|
|
130
|
+
if (!f.includes('__')) continue;
|
|
131
|
+
const fp = join(PIPELINE_DIR, f);
|
|
132
|
+
try {
|
|
133
|
+
const st = await stat(fp);
|
|
134
|
+
if (now - st.mtimeMs > 3600000) {
|
|
135
|
+
await ul(fp);
|
|
136
|
+
count++;
|
|
137
|
+
}
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
140
|
+
if (count > 0) console.log(`[pipeline-engine] 🧹 清理了 ${count} 个旧流水线文件`);
|
|
141
|
+
} catch {}
|
|
142
|
+
}
|
|
143
|
+
// 每1小时自动清理一次
|
|
144
|
+
setInterval(cleanupOldPipelines, 3600000).unref();
|
|
145
|
+
|
|
146
|
+
// ====== DAG 解析 ======
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 解析阶段依赖关系,返回拓扑排序后的层级列表。
|
|
150
|
+
* 返回 [[stage1], [stage2, stage3], [stage4], ...]
|
|
151
|
+
* 同一层内可并行执行,不同层串行。
|
|
152
|
+
*
|
|
153
|
+
* @param {StageDef[]} stages
|
|
154
|
+
* @returns {{ levels: string[][], adjacency: Map<string, string[]>, error?: string }}
|
|
155
|
+
*/
|
|
156
|
+
export function buildDAG(stages) {
|
|
157
|
+
const names = new Set(stages.map(s => s.name));
|
|
158
|
+
const adjacency = new Map(); // name → [dependent names]
|
|
159
|
+
const reverseAdj = new Map(); // name → [dependency names]
|
|
160
|
+
|
|
161
|
+
// 初始化邻接表
|
|
162
|
+
for (const s of stages) {
|
|
163
|
+
adjacency.set(s.name, []);
|
|
164
|
+
reverseAdj.set(s.name, s.dependsOn || []);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 构建正向依赖
|
|
168
|
+
for (const s of stages) {
|
|
169
|
+
const deps = s.dependsOn || [];
|
|
170
|
+
for (const dep of deps) {
|
|
171
|
+
if (!names.has(dep)) {
|
|
172
|
+
return { levels: [], adjacency, error: `阶段 "${s.name}" 依赖不存在的阶段 "${dep}"` };
|
|
173
|
+
}
|
|
174
|
+
if (dep === s.name) {
|
|
175
|
+
return { levels: [], adjacency, error: `阶段 "${s.name}" 不能依赖自身` };
|
|
176
|
+
}
|
|
177
|
+
adjacency.get(dep).push(s.name);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 拓扑排序(Kahn 算法)
|
|
182
|
+
const inDegree = new Map();
|
|
183
|
+
for (const s of stages) {
|
|
184
|
+
inDegree.set(s.name, (s.dependsOn || []).length);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const queue = [];
|
|
188
|
+
for (const [name, degree] of inDegree) {
|
|
189
|
+
if (degree === 0) queue.push(name);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const sorted = [];
|
|
193
|
+
while (queue.length > 0) {
|
|
194
|
+
const levelSize = queue.length;
|
|
195
|
+
const level = [];
|
|
196
|
+
for (let i = 0; i < levelSize; i++) {
|
|
197
|
+
const node = queue.shift();
|
|
198
|
+
level.push(node);
|
|
199
|
+
sorted.push(node);
|
|
200
|
+
const neighbors = adjacency.get(node) || [];
|
|
201
|
+
for (const next of neighbors) {
|
|
202
|
+
const deg = inDegree.get(next) - 1;
|
|
203
|
+
inDegree.set(next, deg);
|
|
204
|
+
if (deg === 0) queue.push(next);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// 检查环:如果 queue 为空但还有未处理的nodes,说明有环
|
|
208
|
+
// 但在 Kahn 算法中,如果环存在,inDegree 永远不为 0 的nodes不会被加入
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (sorted.length !== stages.length) {
|
|
212
|
+
const unsorted = stages.filter(s => !sorted.includes(s.name)).map(s => s.name);
|
|
213
|
+
return { levels: [], adjacency, error: `检测到循环依赖: ${unsorted.join(', ')}` };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 重建分层(按依赖深度)
|
|
217
|
+
const depths = new Map();
|
|
218
|
+
for (const s of stages) {
|
|
219
|
+
depths.set(s.name, 0);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 计算每个nodes的深度=max(依赖深度)+1
|
|
223
|
+
let changed = true;
|
|
224
|
+
while (changed) {
|
|
225
|
+
changed = false;
|
|
226
|
+
for (const s of stages) {
|
|
227
|
+
const deps = s.dependsOn || [];
|
|
228
|
+
if (deps.length === 0) continue;
|
|
229
|
+
const maxDepDepth = Math.max(...deps.map(d => depths.get(d) || 0));
|
|
230
|
+
const newDepth = maxDepDepth + 1;
|
|
231
|
+
if (newDepth > (depths.get(s.name) || 0)) {
|
|
232
|
+
depths.set(s.name, newDepth);
|
|
233
|
+
changed = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 按深度分组
|
|
239
|
+
const maxDepth = Math.max(...depths.values(), 0);
|
|
240
|
+
const levels = [];
|
|
241
|
+
for (let d = 0; d <= maxDepth; d++) {
|
|
242
|
+
const level = stages.filter(s => depths.get(s.name) === d).map(s => s.name);
|
|
243
|
+
if (level.length > 0) levels.push(level);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { levels, adjacency, error: undefined };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ====== 阶段执行器 ======
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 执行单阶段
|
|
253
|
+
*
|
|
254
|
+
* @param {Object} ctx - 执行上下文
|
|
255
|
+
* @param {string} ctx.pipeId
|
|
256
|
+
* @param {StageDef} ctx.stageDef
|
|
257
|
+
* @param {Object} ctx.context - 上游阶段传递的上下文
|
|
258
|
+
* @param {Function} ctx.poolExec - pool.exec 函数引用
|
|
259
|
+
* @returns {Promise<StageResult>}
|
|
260
|
+
*/
|
|
261
|
+
async function runStage(ctx) {
|
|
262
|
+
const { pipeId, stageDef, context, poolExec } = ctx;
|
|
263
|
+
const startTime = Date.now();
|
|
264
|
+
const name = stageDef.name;
|
|
265
|
+
const maxAttempts = (stageDef.retry?.max || 0) + 1;
|
|
266
|
+
const stageTimeout = stageDef.timeoutMs || 120000;
|
|
267
|
+
|
|
268
|
+
let lastError = null;
|
|
269
|
+
|
|
270
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
271
|
+
try {
|
|
272
|
+
// ✅ 使用 AbortController + Promise.race 替代 setTimeout+throw
|
|
273
|
+
// 超时 reject 在 Promise 链中传播,可被外层 try-catch 安全捕获
|
|
274
|
+
const abortController = new AbortController();
|
|
275
|
+
|
|
276
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
277
|
+
const timer = setTimeout(() => {
|
|
278
|
+
reject(new Error(`阶段 "${name}" 超时 ${stageTimeout}ms (第${attempt}次尝试)`));
|
|
279
|
+
}, stageTimeout);
|
|
280
|
+
// AbortController 信号触发时清理定时器,防止内存泄漏
|
|
281
|
+
abortController.signal.addEventListener('abort', () => clearTimeout(timer));
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// 构建工作 Promise(按 mode 分支)
|
|
285
|
+
let workPromise;
|
|
286
|
+
|
|
287
|
+
switch (stageDef.mode || 'pool') {
|
|
288
|
+
case 'pool': {
|
|
289
|
+
const taskArgs = { ...stageDef.task };
|
|
290
|
+
if (context) taskArgs._pipelineContext = context;
|
|
291
|
+
workPromise = poolExec(taskArgs, 'high');
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
case 'exec': {
|
|
296
|
+
const taskArgs = {
|
|
297
|
+
type: 'exec',
|
|
298
|
+
command: stageDef.task.command || '',
|
|
299
|
+
cwd: stageDef.task.cwd,
|
|
300
|
+
env: stageDef.task.env,
|
|
301
|
+
_pipelineContext: context,
|
|
302
|
+
};
|
|
303
|
+
workPromise = poolExec(taskArgs, 'high');
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
case 'fn': {
|
|
308
|
+
if (typeof stageDef.task.fn === 'function') {
|
|
309
|
+
workPromise = stageDef.task.fn(context);
|
|
310
|
+
} else {
|
|
311
|
+
throw new Error('fn 模式需要提供可调用函数');
|
|
312
|
+
}
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
default:
|
|
317
|
+
throw new Error(`未知执行模式: ${stageDef.mode}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Promise.race: 工作 vs 超时,谁先完成谁胜出
|
|
321
|
+
const output = await Promise.race([workPromise, timeoutPromise]);
|
|
322
|
+
|
|
323
|
+
// 工作完成 → 取消超时定时器(通过 AbortController 清理,同时触发 clearTimeout)
|
|
324
|
+
abortController.abort();
|
|
325
|
+
|
|
326
|
+
// persist输出,供依赖阶段读取
|
|
327
|
+
await writeStageOutput(pipeId, name, output).catch(() => {});
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
name,
|
|
331
|
+
status: 'success',
|
|
332
|
+
output,
|
|
333
|
+
durationMs: Date.now() - startTime,
|
|
334
|
+
optional: stageDef.optional || false,
|
|
335
|
+
attempts: attempt,
|
|
336
|
+
};
|
|
337
|
+
} catch (err) {
|
|
338
|
+
// 超时 reject、workPromise reject、fn 同步 throw 均在此被统一捕获
|
|
339
|
+
lastError = err;
|
|
340
|
+
if (attempt < maxAttempts) {
|
|
341
|
+
const delay = stageDef.retry?.delayMs || 1000;
|
|
342
|
+
// TODO: 移除调试日志 console.log(`[pipeline] 阶段 "${name}" 第${attempt}次失败, ${delay}ms后retry: ${err.message}`);
|
|
343
|
+
await new Promise(r => setTimeout(r, delay));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 所有retry都用尽
|
|
349
|
+
return {
|
|
350
|
+
name,
|
|
351
|
+
status: 'failed',
|
|
352
|
+
error: lastError?.message || '未知错误',
|
|
353
|
+
durationMs: Date.now() - startTime,
|
|
354
|
+
optional: stageDef.optional || false,
|
|
355
|
+
attempts: maxAttempts,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ====== 流水线主入口 ======
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 执行多阶段流水线
|
|
363
|
+
*
|
|
364
|
+
* @param {PipelineDef} pipelineDef
|
|
365
|
+
* @param {Object} options
|
|
366
|
+
* @param {Function} options.poolExec - pool.exec 引用
|
|
367
|
+
* @param {boolean} [options.cleanup=true] - 完成后是否清理临时文件
|
|
368
|
+
* @param {AbortSignal} [options.signal] - 取消信号
|
|
369
|
+
* @returns {Promise<PipelineResult>}
|
|
370
|
+
*/
|
|
371
|
+
export async function runPipeline(pipelineDef, options = {}) {
|
|
372
|
+
const startTime = Date.now();
|
|
373
|
+
const pipeId = genPipelineId();
|
|
374
|
+
const poolExec = options.poolExec;
|
|
375
|
+
const cleanup = options.cleanup !== false;
|
|
376
|
+
const signal = options.signal;
|
|
377
|
+
|
|
378
|
+
if (!poolExec) {
|
|
379
|
+
throw new Error('[pipeline-engine] missing poolExec,无法执行流水线');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const stages = pipelineDef.stages || [];
|
|
383
|
+
if (stages.length === 0) {
|
|
384
|
+
return {
|
|
385
|
+
name: pipelineDef.name || 'unnamed',
|
|
386
|
+
status: 'completed',
|
|
387
|
+
totalStages: 0,
|
|
388
|
+
stages: [],
|
|
389
|
+
durationMs: 0,
|
|
390
|
+
finalContext: {},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// 确保临时目录存在
|
|
395
|
+
await mkdir(PIPELINE_DIR, { recursive: true });
|
|
396
|
+
|
|
397
|
+
// 1. 解析 DAG
|
|
398
|
+
const { levels, error: dagError } = buildDAG(stages);
|
|
399
|
+
|
|
400
|
+
if (dagError) {
|
|
401
|
+
return {
|
|
402
|
+
name: pipelineDef.name || 'unnamed',
|
|
403
|
+
status: 'failed',
|
|
404
|
+
totalStages: stages.length,
|
|
405
|
+
stages: [],
|
|
406
|
+
durationMs: Date.now() - startTime,
|
|
407
|
+
error: dagError,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// stageDef 的快速查找表
|
|
412
|
+
const stageDefMap = new Map(stages.map(s => [s.name, s]));
|
|
413
|
+
|
|
414
|
+
// 2. 逐层执行
|
|
415
|
+
const stageResults = [];
|
|
416
|
+
const stageOutputs = {}; // name → output(运行时上下文)
|
|
417
|
+
let pipelineFailed = false;
|
|
418
|
+
const fallback = pipelineDef.fallback || 'abort';
|
|
419
|
+
|
|
420
|
+
for (const level of levels) {
|
|
421
|
+
if (pipelineFailed && fallback === 'abort') {
|
|
422
|
+
// 跳过剩余所有阶段(break前已标记),这段不会再执行到
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (signal?.aborted) {
|
|
427
|
+
pipelineFailed = true;
|
|
428
|
+
for (const name of level) {
|
|
429
|
+
stageResults.push({
|
|
430
|
+
name,
|
|
431
|
+
status: 'skipped',
|
|
432
|
+
durationMs: 0,
|
|
433
|
+
optional: stageDefMap.get(name)?.optional || false,
|
|
434
|
+
error: '流水线被取消',
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
const remainingLevels = levels.slice(levels.indexOf(level) + 1);
|
|
438
|
+
for (const lvl of remainingLevels) {
|
|
439
|
+
for (const name of lvl) {
|
|
440
|
+
stageResults.push({
|
|
441
|
+
name,
|
|
442
|
+
status: 'skipped',
|
|
443
|
+
durationMs: 0,
|
|
444
|
+
optional: stageDefMap.get(name)?.optional || false,
|
|
445
|
+
error: '流水线被取消',
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 同层并行执行
|
|
453
|
+
const levelPromises = level.map(async (name) => {
|
|
454
|
+
const sd = stageDefMap.get(name);
|
|
455
|
+
if (!sd) {
|
|
456
|
+
return {
|
|
457
|
+
name,
|
|
458
|
+
status: 'failed',
|
|
459
|
+
error: `阶段定义丢失: ${name}`,
|
|
460
|
+
durationMs: 0,
|
|
461
|
+
optional: false,
|
|
462
|
+
attempts: 0,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// 检查是否有依赖失败(skip-dependents 模式)
|
|
467
|
+
if (fallback === 'skip-dependents' && sd.dependsOn) {
|
|
468
|
+
for (const dep of sd.dependsOn) {
|
|
469
|
+
const depResult = stageResults.find(r => r.name === dep);
|
|
470
|
+
if (depResult && depResult.status === 'failed') {
|
|
471
|
+
return {
|
|
472
|
+
name,
|
|
473
|
+
status: 'skipped',
|
|
474
|
+
error: `依赖阶段 "${dep}" 失败`,
|
|
475
|
+
durationMs: 0,
|
|
476
|
+
optional: sd.optional || false,
|
|
477
|
+
attempts: 0,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 构建本阶段上下文 = 上游所有依赖的输出
|
|
484
|
+
const context = { pipeline: { name: pipelineDef.name || 'unnamed', pipeId } };
|
|
485
|
+
if (sd.dependsOn && sd.dependsOn.length > 0) {
|
|
486
|
+
context.stageOutputs = {};
|
|
487
|
+
for (const dep of sd.dependsOn) {
|
|
488
|
+
const depOutput = stageOutputs[dep];
|
|
489
|
+
if (depOutput !== undefined) {
|
|
490
|
+
context.stageOutputs[dep] = depOutput;
|
|
491
|
+
} else {
|
|
492
|
+
// 尝试从文件读取persist输出
|
|
493
|
+
const fileOutput = await readStageOutput(pipeId, dep);
|
|
494
|
+
if (fileOutput !== undefined) {
|
|
495
|
+
context.stageOutputs[dep] = fileOutput;
|
|
496
|
+
stageOutputs[dep] = fileOutput; // 缓存
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
} else {
|
|
501
|
+
context.stageOutputs = {};
|
|
502
|
+
}
|
|
503
|
+
context.params = pipelineDef.params || {};
|
|
504
|
+
|
|
505
|
+
const result = await runStage({
|
|
506
|
+
pipeId,
|
|
507
|
+
stageDef: sd,
|
|
508
|
+
context,
|
|
509
|
+
poolExec,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// 记录输出
|
|
513
|
+
if (result.status === 'success' && result.output !== undefined) {
|
|
514
|
+
stageOutputs[name] = result.output;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// 检查是否应该终止流水线
|
|
518
|
+
if (result.status === 'failed' && !result.optional) {
|
|
519
|
+
pipelineFailed = true;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return result;
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const results = await Promise.allSettled(levelPromises);
|
|
526
|
+
for (const r of results) {
|
|
527
|
+
if (r.status === 'fulfilled') {
|
|
528
|
+
stageResults.push(r.value);
|
|
529
|
+
} else {
|
|
530
|
+
stageResults.push({
|
|
531
|
+
name: 'unknown',
|
|
532
|
+
status: 'failed',
|
|
533
|
+
error: `阶段执行异常: ${r.reason?.message || '未知'}`,
|
|
534
|
+
durationMs: 0,
|
|
535
|
+
optional: false,
|
|
536
|
+
attempts: 0,
|
|
537
|
+
});
|
|
538
|
+
pipelineFailed = true;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (pipelineFailed && fallback === 'abort') {
|
|
543
|
+
// 先标记剩余所有阶段为 skipped
|
|
544
|
+
const remainingLevels = levels.slice(levels.indexOf(level) + 1);
|
|
545
|
+
for (const lvl of remainingLevels) {
|
|
546
|
+
for (const name of lvl) {
|
|
547
|
+
stageResults.push({
|
|
548
|
+
name,
|
|
549
|
+
status: 'skipped',
|
|
550
|
+
durationMs: 0,
|
|
551
|
+
optional: stageDefMap.get(name)?.optional || false,
|
|
552
|
+
attempts: 0,
|
|
553
|
+
error: '流水线已终止',
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// 3. 计算结果
|
|
562
|
+
const totalStages = stages.length;
|
|
563
|
+
const completedCount = stageResults.filter(r => r.status === 'success').length;
|
|
564
|
+
const failedCount = stageResults.filter(r => r.status === 'failed').length;
|
|
565
|
+
const mandatoryFailed = stageResults.filter(r => r.status === 'failed' && !r.optional).length;
|
|
566
|
+
const skippedCount = stageResults.filter(r => r.status === 'skipped').length;
|
|
567
|
+
|
|
568
|
+
let pipelineStatus;
|
|
569
|
+
if (mandatoryFailed > 0 && fallback !== 'continue') {
|
|
570
|
+
pipelineStatus = 'failed';
|
|
571
|
+
} else if (pipelineFailed) {
|
|
572
|
+
pipelineStatus = 'failed';
|
|
573
|
+
} else {
|
|
574
|
+
pipelineStatus = 'completed';
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (signal?.aborted) {
|
|
578
|
+
pipelineStatus = 'aborted';
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// 4. 清理临时文件
|
|
582
|
+
if (cleanup) {
|
|
583
|
+
cleanupPipeline(pipeId).catch(() => {});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
name: pipelineDef.name || 'unnamed',
|
|
588
|
+
status: pipelineStatus,
|
|
589
|
+
pipelineId: pipeId,
|
|
590
|
+
totalStages,
|
|
591
|
+
completedStages: completedCount,
|
|
592
|
+
failedStages: failedCount,
|
|
593
|
+
skippedStages: skippedCount,
|
|
594
|
+
stages: stageResults,
|
|
595
|
+
durationMs: Date.now() - startTime,
|
|
596
|
+
error: pipelineFailed ? `${failedCount} 个阶段失败, ${skippedCount} 个阶段被跳过` : undefined,
|
|
597
|
+
finalContext: stageOutputs,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* 验证流水线定义的合法性(不执行)
|
|
603
|
+
* @param {PipelineDef} def
|
|
604
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[] }}
|
|
605
|
+
*/
|
|
606
|
+
export function validatePipeline(def) {
|
|
607
|
+
const errors = [];
|
|
608
|
+
const warnings = [];
|
|
609
|
+
|
|
610
|
+
if (!def) {
|
|
611
|
+
errors.push('流水线定义为空');
|
|
612
|
+
return { valid: false, errors, warnings };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const stages = def.stages || [];
|
|
616
|
+
if (stages.length === 0) {
|
|
617
|
+
errors.push('流水线至少需要 1 个阶段');
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// 检查名称唯一性
|
|
621
|
+
const names = new Set();
|
|
622
|
+
for (const s of stages) {
|
|
623
|
+
if (!s.name || typeof s.name !== 'string') {
|
|
624
|
+
errors.push('每个阶段必须提供 name');
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (names.has(s.name)) {
|
|
628
|
+
errors.push(`阶段名称重复: ${s.name}`);
|
|
629
|
+
}
|
|
630
|
+
names.add(s.name);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// 检查依赖合法性
|
|
634
|
+
for (const s of stages) {
|
|
635
|
+
const deps = s.dependsOn || [];
|
|
636
|
+
for (const d of deps) {
|
|
637
|
+
if (!names.has(d)) {
|
|
638
|
+
errors.push(`阶段 "${s.name}" 依赖不存在的阶段 "${d}"`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// 检查循环依赖
|
|
644
|
+
if (errors.length === 0) {
|
|
645
|
+
const { error: dagError } = buildDAG(stages);
|
|
646
|
+
if (dagError) errors.push(dagError);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// 警告
|
|
650
|
+
for (const s of stages) {
|
|
651
|
+
if (s.parallel !== undefined) {
|
|
652
|
+
warnings.push(`阶段 "${s.name}": parallel 参数已弃用,并行由 DAG 自动推断`);
|
|
653
|
+
}
|
|
654
|
+
if (s.retry?.max > 5) {
|
|
655
|
+
warnings.push(`阶段 "${s.name}": retry次数 (${s.retry.max}) 超过建议值 5`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export default {
|
|
663
|
+
runPipeline,
|
|
664
|
+
validatePipeline,
|
|
665
|
+
buildDAG,
|
|
666
|
+
cleanupPipeline,
|
|
667
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json, urllib.request, re, time
|
|
3
|
+
|
|
4
|
+
sse = urllib.request.urlopen('http://127.0.0.1:18790/sse')
|
|
5
|
+
sse.readline() # event: endpoint
|
|
6
|
+
data_line = sse.readline().decode()
|
|
7
|
+
session_id = re.search(r'sessionId=([^\s]+)', data_line).group(1)
|
|
8
|
+
print(f'Session: {session_id}')
|
|
9
|
+
|
|
10
|
+
msg_url = f'http://127.0.0.1:18790/messages?sessionId={session_id}'
|
|
11
|
+
req = urllib.request.Request(msg_url, data=json.dumps({
|
|
12
|
+
'jsonrpc': '2.0', 'id': 'list-all', 'method': 'tools/list', 'params': {}
|
|
13
|
+
}).encode(), headers={'Content-Type': 'application/json'})
|
|
14
|
+
urllib.request.urlopen(req)
|
|
15
|
+
|
|
16
|
+
for _ in range(60):
|
|
17
|
+
line = sse.readline().decode(errors='replace')
|
|
18
|
+
if 'list-all' in line:
|
|
19
|
+
data = json.loads(line[6:])
|
|
20
|
+
tools = data['result']['tools']
|
|
21
|
+
names = [t['name'] for t in tools]
|
|
22
|
+
dupes = [n for n in names if names.count(n) > 1]
|
|
23
|
+
print(f'Total: {len(tools)}, Unique: {len(set(names))}')
|
|
24
|
+
if dupes:
|
|
25
|
+
print(f'Duplicates ({len(set(dupes))}): {sorted(set(dupes))}')
|
|
26
|
+
else:
|
|
27
|
+
print('No duplicates!')
|
|
28
|
+
print('First 5 tools:')
|
|
29
|
+
for t in tools[:5]:
|
|
30
|
+
js = json.dumps(t.get('inputSchema', {}))[:100]
|
|
31
|
+
print(f' {t["name"]}: {js}')
|
|
32
|
+
break
|
|
33
|
+
time.sleep(0.3)
|
|
34
|
+
sse.close()
|