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,896 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦞 sc — L5+ 任务自动分解(任务递归分解 + DAG 流水线)
|
|
3
|
+
*
|
|
4
|
+
* Worker E(route-decompose):在双树评估的大树阶段运行。
|
|
5
|
+
*
|
|
6
|
+
* 设计理念:
|
|
7
|
+
* 1. 分解原本:L5+ 复杂任务自动拆解为子步骤链
|
|
8
|
+
* 2. 工具推断:每步根据 action/描述自动匹配最佳工具
|
|
9
|
+
* 3. DAG 构建:建立依赖edges,L5串行/L6并行发现/L7全DAG
|
|
10
|
+
* 4. Pipeline 输出:输出可直接喂入 stagePipeline 的格式
|
|
11
|
+
* 5. 独立熔断:每步独立超时、独立结果文件、独立retry
|
|
12
|
+
* 6. 共享消费:结果写入 shared/decomposed/,Worker 并行处理
|
|
13
|
+
*
|
|
14
|
+
* v2 — 2026-05-31 重新实现(修复:步骤文件写入、toolInfo 传递、DAG 工具感知)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import crypto from "crypto";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import { mkdir, writeFile, rename, readFile, unlink } from "fs/promises";
|
|
20
|
+
import { SHARED_DIR } from './constants.js';
|
|
21
|
+
|
|
22
|
+
// ====== 常量 ======
|
|
23
|
+
|
|
24
|
+
const DECOMPOSED_DIR = join(SHARED_DIR, 'decomposed');
|
|
25
|
+
|
|
26
|
+
// ====== 步骤 → 工具推断映射 ======
|
|
27
|
+
// 根据 action 标签或描述关键词推断最佳推荐工具
|
|
28
|
+
// 返回 { tool: string, mode: 'pool'|'exec', timeoutMs: number, retryMax: number }
|
|
29
|
+
// 🧠 设计决策:ACTION_TOOL_MAP 的超时(timeoutMs)和retry(retryMax)值不是随意定的。
|
|
30
|
+
// 搜索/获取类(timeoutMs=30000, retryMax=2):网络I/O最不确定,给30s+2次retry够
|
|
31
|
+
// 应对 DNS 超时、TCP 重传、API 限速等瞬态故障。retry2次而不是3次:第三次之前换路。
|
|
32
|
+
// 分析/编排类(timeoutMs=60000, retryMax=1):core_orchestrate 要排子任务,
|
|
33
|
+
// 典型耗时 15-45s,60s是合理超时。retryMax=1 是因为编排不是幂等的,重复执行
|
|
34
|
+
// 可能产生不同结果,retry1次足够覆盖临时问题的同时避免结果不一致。
|
|
35
|
+
// 编码/修复类(timeoutMs=60000-90000, retryMax=2):代码编辑需要模型调用+
|
|
36
|
+
// 文件写入+语法验证,典型 20-40 s。retryMax=2 是因为这种操作失败后retry通常成功
|
|
37
|
+
// (临时文件锁释放、Ollama 负载下降后retry生效)。
|
|
38
|
+
// 系统运维类(timeoutMs=120000, retryMax=1):安装/部署最慢(下载文件、等待安装完成),
|
|
39
|
+
// 给2min足够。retryMax=1 是因为系统操作失败通常不可恢复(网络不行、磁盘满了),
|
|
40
|
+
// retry多次无意义。
|
|
41
|
+
// 兜底 spawn_subagent(timeoutMs=120000, retryMax=0):子 agent 有独立超时和熔断,
|
|
42
|
+
// 父步骤不需要retry它。
|
|
43
|
+
// 超时 = 工具平均耗时 × 2(缓冲区) + 网络/序列化开销(5-10s)
|
|
44
|
+
// retry = 幂等性越高可retry越多:搜索类(2) > 编码类(2) > 编排类(1) > 系统类(1) > 子agent(0)
|
|
45
|
+
|
|
46
|
+
const ACTION_TOOL_MAP = {
|
|
47
|
+
// ★ 搜索/调研类
|
|
48
|
+
'搜索': { tool: 'tavily_search', mode: 'pool', timeoutMs: 30000, retryMax: 2 },
|
|
49
|
+
'调研': { tool: 'core_research', mode: 'pool', timeoutMs: 60000, retryMax: 1 },
|
|
50
|
+
'研究': { tool: 'core_research', mode: 'pool', timeoutMs: 60000, retryMax: 1 },
|
|
51
|
+
'查找': { tool: 'core_search', mode: 'pool', timeoutMs: 30000, retryMax: 2 },
|
|
52
|
+
|
|
53
|
+
// ★ 获取/提取类
|
|
54
|
+
'获取': { tool: 'web_fetch', mode: 'pool', timeoutMs: 30000, retryMax: 2 },
|
|
55
|
+
'提取': { tool: 'tavily_extract', mode: 'pool', timeoutMs: 30000, retryMax: 2 },
|
|
56
|
+
'爬取': { tool: 'web_fetch', mode: 'pool', timeoutMs: 45000, retryMax: 2 },
|
|
57
|
+
|
|
58
|
+
// ★ 分析/评估/对比类
|
|
59
|
+
'分析': { tool: 'core_orchestrate', mode: 'pool', timeoutMs: 60000, retryMax: 1 },
|
|
60
|
+
'评估': { tool: 'core_orchestrate', mode: 'pool', timeoutMs: 60000, retryMax: 1 },
|
|
61
|
+
'对比': { tool: 'core_diff', mode: 'pool', timeoutMs: 30000, retryMax: 1 },
|
|
62
|
+
'比较': { tool: 'core_orchestrate', mode: 'pool', timeoutMs: 45000, retryMax: 1 },
|
|
63
|
+
|
|
64
|
+
// ★ 设计/规划类
|
|
65
|
+
'设计': { tool: 'core_orchestrate', mode: 'pool', timeoutMs: 60000, retryMax: 1 },
|
|
66
|
+
'规划': { tool: 'core_orchestrate', mode: 'pool', timeoutMs: 60000, retryMax: 1 },
|
|
67
|
+
'方案': { tool: 'core_orchestrate', mode: 'pool', timeoutMs: 60000, retryMax: 1 },
|
|
68
|
+
|
|
69
|
+
// ★ 编码/实现类
|
|
70
|
+
'实现': { tool: 'core_codeEdit', mode: 'pool', timeoutMs: 60000, retryMax: 2 },
|
|
71
|
+
'编写': { tool: 'core_codeEdit', mode: 'pool', timeoutMs: 60000, retryMax: 2 },
|
|
72
|
+
'编码': { tool: 'core_codeEdit', mode: 'pool', timeoutMs: 60000, retryMax: 2 },
|
|
73
|
+
'修改': { tool: 'core_bugFix', mode: 'pool', timeoutMs: 60000, retryMax: 2 },
|
|
74
|
+
'修复': { tool: 'core_bugFix', mode: 'pool', timeoutMs: 60000, retryMax: 2 },
|
|
75
|
+
'重构': { tool: 'core_codeEdit', mode: 'pool', timeoutMs: 90000, retryMax: 2 },
|
|
76
|
+
|
|
77
|
+
// ★ 测试/验证类
|
|
78
|
+
'测试': { tool: 'core_bugFix', mode: 'pool', timeoutMs: 60000, retryMax: 2 },
|
|
79
|
+
'验证': { tool: 'core_orchestrate', mode: 'pool', timeoutMs: 45000, retryMax: 1 },
|
|
80
|
+
|
|
81
|
+
// ★ 整理/汇总/生成类
|
|
82
|
+
'整理': { tool: 'core_orchestrate', mode: 'pool', timeoutMs: 45000, retryMax: 1 },
|
|
83
|
+
'汇总': { tool: 'core_orchestrate', mode: 'pool', timeoutMs: 45000, retryMax: 1 },
|
|
84
|
+
'生成': { tool: 'core_orchestrate', mode: 'pool', timeoutMs: 60000, retryMax: 1 },
|
|
85
|
+
|
|
86
|
+
// ★ 部署/系统类
|
|
87
|
+
'部署': { tool: 'core_systemRun', mode: 'pool', timeoutMs: 120000, retryMax: 1 },
|
|
88
|
+
'安装': { tool: 'core_systemRun', mode: 'pool', timeoutMs: 120000, retryMax: 1 },
|
|
89
|
+
'同步': { tool: 'core_workspaceSync', mode: 'pool', timeoutMs: 60000, retryMax: 2 },
|
|
90
|
+
|
|
91
|
+
// ★ 日记/记忆类
|
|
92
|
+
'回顾': { tool: 'core_dialogRecall', mode: 'pool', timeoutMs: 30000, retryMax: 2 },
|
|
93
|
+
'回忆': { tool: 'core_dialogRecall', mode: 'pool', timeoutMs: 30000, retryMax: 2 },
|
|
94
|
+
'检索': { tool: 'core_semanticSearch', mode: 'pool', timeoutMs: 45000, retryMax: 1 },
|
|
95
|
+
|
|
96
|
+
// ★ 兜底
|
|
97
|
+
'完整': { tool: 'spawn_subagent', mode: 'pool', timeoutMs: 120000, retryMax: 0 },
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// 描述关键词 → 工具的映射(当 action 不匹配时作为兜底)
|
|
101
|
+
const DESCRIPTION_KEYWORD_MAP = [
|
|
102
|
+
{ pattern: /搜索|查找|搜素|查.*资料|查.*信息/i, tool: 'tavily_search', mode: 'pool', timeoutMs: 30000 },
|
|
103
|
+
{ pattern: /打开.*网页|访问.*网站|fetch|爬取/i, tool: 'web_fetch', mode: 'pool', timeoutMs: 30000 },
|
|
104
|
+
{ pattern: /提取|抽取|解析.*内容/i, tool: 'tavily_extract', mode: 'pool', timeoutMs: 30000 },
|
|
105
|
+
{ pattern: /分析|评估|判断|评价/i, tool: 'core_orchestrate', mode: 'pool', timeoutMs: 60000 },
|
|
106
|
+
{ pattern: /对比|比较|diff|差异|区别/i, tool: 'core_diff', mode: 'pool', timeoutMs: 30000 },
|
|
107
|
+
{ pattern: /设计|方案|架构|规划/i, tool: 'core_orchestrate', mode: 'pool', timeoutMs: 60000 },
|
|
108
|
+
{ pattern: /编码|编写|写代码|实现|开发|修改|编辑/i, tool: 'core_codeEdit', mode: 'pool', timeoutMs: 60000 },
|
|
109
|
+
{ pattern: /修复|修|改bug|bug|fix/i, tool: 'core_bugFix', mode: 'pool', timeoutMs: 60000 },
|
|
110
|
+
{ pattern: /测试|验证|检查.*结果|确认/i, tool: 'core_orchestrate', mode: 'pool', timeoutMs: 45000 },
|
|
111
|
+
{ pattern: /整理|汇总|总结|输出|报告|生成/i, tool: 'core_orchestrate', mode: 'pool', timeoutMs: 45000 },
|
|
112
|
+
{ pattern: /部署|发布|安装|下载|同步/i, tool: 'core_systemRun', mode: 'pool', timeoutMs: 120000 },
|
|
113
|
+
{ pattern: /日记|回忆|历史|之前|回顾/i, tool: 'core_dialogRecall', mode: 'pool', timeoutMs: 30000 },
|
|
114
|
+
{ pattern: /调研|研究|调查|了解|打听/i, tool: 'core_research', mode: 'pool', timeoutMs: 60000 },
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 根据步骤的 action 和描述推断最佳工具
|
|
119
|
+
* @param {string} description - 步骤描述
|
|
120
|
+
* @param {string} [action] - 步骤 action 标签
|
|
121
|
+
* @returns {{ tool: string, mode: string, timeoutMs: number, retryMax: number }}
|
|
122
|
+
*/
|
|
123
|
+
function inferStepTool(description, action) {
|
|
124
|
+
// 1. 优先按 action 标签匹配
|
|
125
|
+
if (action && ACTION_TOOL_MAP[action]) {
|
|
126
|
+
return { ...ACTION_TOOL_MAP[action] };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 2. 按描述关键词匹配
|
|
130
|
+
for (const rule of DESCRIPTION_KEYWORD_MAP) {
|
|
131
|
+
if (rule.pattern.test(description)) {
|
|
132
|
+
return { tool: rule.tool, mode: rule.mode, timeoutMs: rule.timeoutMs, retryMax: 1 };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 3. 兜底:spawn subagent
|
|
137
|
+
return { tool: 'spawn_subagent', mode: 'pool', timeoutMs: 120000, retryMax: 0 };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 判断两个步骤能否并行执行(工具感知版)
|
|
142
|
+
*
|
|
143
|
+
* v2: 使用 toolInfo 中的工具名做更精确的并行判断
|
|
144
|
+
*
|
|
145
|
+
* 规则:
|
|
146
|
+
* 1. 两个搜索类工具可并行
|
|
147
|
+
* 2. 两个分析类工具(主题不同)可并行
|
|
148
|
+
* 3. 搜索+分析可部分并行
|
|
149
|
+
* 4. 生成/实现在搜索之前不能并行
|
|
150
|
+
* 5. L5 不做并行检测(严格串行),L6 开始支持
|
|
151
|
+
*
|
|
152
|
+
* @param {object} stepA - 前一步(必须有 toolInfo 字段)
|
|
153
|
+
* @param {object} stepB - 后一步(必须有 toolInfo 字段)
|
|
154
|
+
* @returns {boolean} 可并行
|
|
155
|
+
*/
|
|
156
|
+
function canParallel(stepA, stepB) {
|
|
157
|
+
// 获取工具名
|
|
158
|
+
const toolA = stepA.toolInfo?.tool || stepA.tool || 'spawn_subagent';
|
|
159
|
+
const toolB = stepB.toolInfo?.tool || stepB.tool || 'spawn_subagent';
|
|
160
|
+
|
|
161
|
+
// 搜索类工具(只读,互不依赖)
|
|
162
|
+
const searchTools = ['tavily_search', 'core_search', 'core_research', 'core_dialogRecall', 'core_semanticSearch'];
|
|
163
|
+
// 提取类工具(只读,需要独立 URL/文件)
|
|
164
|
+
const fetchTools = ['web_fetch', 'tavily_extract'];
|
|
165
|
+
// 分析类工具(中等开销)
|
|
166
|
+
const analyticTools = ['core_orchestrate', 'core_diff', 'core_codeReview', 'core_diagnose'];
|
|
167
|
+
// 生产类工具(写入,需要前置依赖)
|
|
168
|
+
const productiveTools = ['core_codeEdit', 'core_bugFix', 'core_workspaceSync', 'core_systemRun'];
|
|
169
|
+
|
|
170
|
+
// 两个搜索类 → 可并行
|
|
171
|
+
if (searchTools.includes(toolA) && searchTools.includes(toolB)) return true;
|
|
172
|
+
|
|
173
|
+
// 搜索+提取 → 可并行(互不依赖)
|
|
174
|
+
if (searchTools.includes(toolA) && fetchTools.includes(toolB)) return true;
|
|
175
|
+
if (fetchTools.includes(toolA) && searchTools.includes(toolB)) return true;
|
|
176
|
+
|
|
177
|
+
// 两个分析类(主题不同)→ 可并行
|
|
178
|
+
if (analyticTools.includes(toolA) && analyticTools.includes(toolB)) {
|
|
179
|
+
// 检查描述是否有大量重叠关键词
|
|
180
|
+
const descA = (stepA.description || '').substring(0, 30);
|
|
181
|
+
const descB = (stepB.description || '').substring(0, 30);
|
|
182
|
+
const aTokens = new Set(descA.split(/[\s,,。、]+/).filter(t => t.length > 2));
|
|
183
|
+
const bTokens = new Set(descB.split(/[\s,,。、]+/).filter(t => t.length > 2));
|
|
184
|
+
const overlap = [...aTokens].filter(t => bTokens.has(t)).length;
|
|
185
|
+
return overlap <= 1; // 重合度低 → 不同主题 → 可并行
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 搜索+分析 → 不可并行(分析依赖搜索结果做输入)
|
|
189
|
+
if (searchTools.includes(toolA) && analyticTools.includes(toolB)) return false;
|
|
190
|
+
if (fetchTools.includes(toolA) && analyticTools.includes(toolB)) return false;
|
|
191
|
+
|
|
192
|
+
// 前一个是生产类 → 后一个也生产类 → 不可并行(串行编码)
|
|
193
|
+
if (productiveTools.includes(toolA) && productiveTools.includes(toolB)) return false;
|
|
194
|
+
|
|
195
|
+
// 最后一个是验证/汇总 → 依赖前面所有 → 不可并行
|
|
196
|
+
if (toolB === 'core_orchestrate' && (stepB.action === '验证' || stepB.action === '汇总')) return false;
|
|
197
|
+
|
|
198
|
+
// 兜底
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 从扁平步骤构建 DAG(依赖图)
|
|
204
|
+
*
|
|
205
|
+
* v2: 使用 toolInfo 做工具感知的并行判断
|
|
206
|
+
*
|
|
207
|
+
* @param {Array<{step: number, description: string, action?: string, toolInfo?: object}>} steps - 已富化的步骤
|
|
208
|
+
* @param {string} level - 'L5'|'L6'|'L7'
|
|
209
|
+
* @returns {{ stages: Array<{name: string, dependsOn: string[]}>, adjacency: Map<string, string[]> }}
|
|
210
|
+
*/
|
|
211
|
+
function buildDAGFromSteps(steps, level) {
|
|
212
|
+
if (steps.length <= 1) {
|
|
213
|
+
return {
|
|
214
|
+
stages: steps.map(s => ({
|
|
215
|
+
name: `step-${s.step}`,
|
|
216
|
+
dependsOn: [],
|
|
217
|
+
})),
|
|
218
|
+
adjacency: new Map(),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const stageDefs = [];
|
|
223
|
+
const adjacency = new Map();
|
|
224
|
+
|
|
225
|
+
if (level === 'L5') {
|
|
226
|
+
// L5:严格串行——每步依赖前一步
|
|
227
|
+
for (let i = 0; i < steps.length; i++) {
|
|
228
|
+
const stageName = `step-${steps[i].step}`;
|
|
229
|
+
adjacency.set(stageName, []);
|
|
230
|
+
stageDefs.push({
|
|
231
|
+
name: stageName,
|
|
232
|
+
dependsOn: i === 0 ? [] : [`step-${steps[i - 1].step}`],
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
} else if (level === 'L6') {
|
|
236
|
+
// L6:智能并行——检测可并行的步骤
|
|
237
|
+
stageDefs.push({ name: `step-${steps[0].step}`, dependsOn: [] });
|
|
238
|
+
adjacency.set(`step-${steps[0].step}`, []);
|
|
239
|
+
|
|
240
|
+
for (let i = 1; i < steps.length; i++) {
|
|
241
|
+
const prev = steps[i - 1];
|
|
242
|
+
const curr = steps[i];
|
|
243
|
+
const prevName = `step-${prev.step}`;
|
|
244
|
+
const currName = `step-${curr.step}`;
|
|
245
|
+
|
|
246
|
+
if (canParallel(prev, curr)) {
|
|
247
|
+
// 可并行:依赖前一步的同级(前前一步,如果存在)
|
|
248
|
+
const prevPrev = i >= 2 ? `step-${steps[i - 2].step}` : null;
|
|
249
|
+
stageDefs.push({
|
|
250
|
+
name: currName,
|
|
251
|
+
dependsOn: prevPrev ? [prevPrev] : [],
|
|
252
|
+
});
|
|
253
|
+
if (prevPrev) {
|
|
254
|
+
const deps = adjacency.get(prevPrev) || [];
|
|
255
|
+
deps.push(currName);
|
|
256
|
+
adjacency.set(prevPrev, deps);
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
// 不可并行:正常串行依赖
|
|
260
|
+
stageDefs.push({
|
|
261
|
+
name: currName,
|
|
262
|
+
dependsOn: [prevName],
|
|
263
|
+
});
|
|
264
|
+
const deps = adjacency.get(prevName) || [];
|
|
265
|
+
deps.push(currName);
|
|
266
|
+
adjacency.set(prevName, deps);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
// L7:全 DAG 模式
|
|
271
|
+
for (let i = 0; i < steps.length; i++) {
|
|
272
|
+
const stageName = `step-${steps[i].step}`;
|
|
273
|
+
adjacency.set(stageName, []);
|
|
274
|
+
|
|
275
|
+
if (i === 0) {
|
|
276
|
+
stageDefs.push({ name: stageName, dependsOn: [] });
|
|
277
|
+
} else if (i === steps.length - 1) {
|
|
278
|
+
// 最后一步 → 依赖前面所有步骤(汇总/验证)
|
|
279
|
+
const allPrev = steps.slice(0, -1).map(s => `step-${s.step}`);
|
|
280
|
+
stageDefs.push({ name: stageName, dependsOn: allPrev });
|
|
281
|
+
for (const prev of allPrev) {
|
|
282
|
+
const deps = adjacency.get(prev) || [];
|
|
283
|
+
deps.push(stageName);
|
|
284
|
+
adjacency.set(prev, deps);
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
const prev = steps[i - 1];
|
|
288
|
+
const curr = steps[i];
|
|
289
|
+
const prevName = `step-${prev.step}`;
|
|
290
|
+
const currName = `step-${curr.step}`;
|
|
291
|
+
|
|
292
|
+
if (canParallel(prev, curr)) {
|
|
293
|
+
const prevPrev = i >= 2 ? `step-${steps[i - 2].step}` : null;
|
|
294
|
+
stageDefs.push({
|
|
295
|
+
name: currName,
|
|
296
|
+
dependsOn: prevPrev ? [prevPrev] : [],
|
|
297
|
+
});
|
|
298
|
+
if (prevPrev) {
|
|
299
|
+
const deps = adjacency.get(prevPrev) || [];
|
|
300
|
+
deps.push(currName);
|
|
301
|
+
adjacency.set(prevPrev, deps);
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
stageDefs.push({
|
|
305
|
+
name: currName,
|
|
306
|
+
dependsOn: [prevName],
|
|
307
|
+
});
|
|
308
|
+
const deps = adjacency.get(prevName) || [];
|
|
309
|
+
deps.push(currName);
|
|
310
|
+
adjacency.set(prevName, deps);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return { stages: stageDefs, adjacency };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 构建分步结果文件路径列表
|
|
321
|
+
* 每个步骤对应一个独立的结果文件,支持独立熔断
|
|
322
|
+
*
|
|
323
|
+
* @param {string} taskId
|
|
324
|
+
* @param {number} totalSteps
|
|
325
|
+
* @returns {string[]} 各步骤的文件路径
|
|
326
|
+
*/
|
|
327
|
+
function buildStepResultPaths(taskId, totalSteps) {
|
|
328
|
+
const paths = [];
|
|
329
|
+
for (let i = 1; i <= totalSteps; i++) {
|
|
330
|
+
paths.push(join(DECOMPOSED_DIR, `${taskId}-step${i}.result.json`));
|
|
331
|
+
}
|
|
332
|
+
return paths;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 将分解结果转换为 stagePipeline 兼容的流水线定义
|
|
337
|
+
*
|
|
338
|
+
* v2: 使用已富化的 steps(含 toolInfo),每步带独立超时、结果文件、熔断配置。
|
|
339
|
+
* pipeline 阶段使用 exec 模式,命令格式为:
|
|
340
|
+
* node <decomposed-runner> <decomposedDir> <taskId> <stepN>
|
|
341
|
+
* 每个阶段读取 shared/decomposed/{taskId}-step{N}.json 中的步骤文件并执行。
|
|
342
|
+
*
|
|
343
|
+
* @param {string} taskText - 原始任务文本
|
|
344
|
+
* @param {string} level - 'L5'|'L6'|'L7'
|
|
345
|
+
* @param {Array<{step: number, description: string, action?: string, toolInfo?: object}>} steps - 已富化的步骤列表
|
|
346
|
+
* @returns {object} pipelineDef — 可直接喂入 runPipeline 或 core_stagePipeline
|
|
347
|
+
*/
|
|
348
|
+
function stepsToPipeline(taskText, level, steps) {
|
|
349
|
+
// 步骤必须有 toolInfo,若缺失则补充
|
|
350
|
+
const enrichedSteps = steps.map(s => ({
|
|
351
|
+
...s,
|
|
352
|
+
toolInfo: s.toolInfo || inferStepTool(s.description, s.action),
|
|
353
|
+
}));
|
|
354
|
+
|
|
355
|
+
const { stages: dagStages } = buildDAGFromSteps(enrichedSteps, level);
|
|
356
|
+
const taskId = genTaskId(taskText);
|
|
357
|
+
const resultPaths = buildStepResultPaths(taskId, enrichedSteps.length);
|
|
358
|
+
|
|
359
|
+
// 给每步分配 pipeline 阶段定义
|
|
360
|
+
const pipelineStages = dagStages.map((ds, idx) => {
|
|
361
|
+
const stepDef = enrichedSteps.find(s => `step-${s.step}` === ds.name);
|
|
362
|
+
if (!stepDef) return null;
|
|
363
|
+
|
|
364
|
+
const toolInfo = stepDef.toolInfo || inferStepTool(stepDef.description, stepDef.action);
|
|
365
|
+
const stepResultPath = resultPaths[idx] || join(DECOMPOSED_DIR, `${taskId}-step${stepDef.step}.result.json`);
|
|
366
|
+
|
|
367
|
+
// 构建 stage 定义(使用 pool 模式,type = decomposed-step)
|
|
368
|
+
// 每个阶段带独立超时、结果文件路径、熔断retry
|
|
369
|
+
return {
|
|
370
|
+
name: ds.name,
|
|
371
|
+
mode: 'pool',
|
|
372
|
+
dependsOn: ds.dependsOn,
|
|
373
|
+
optional: idx < enrichedSteps.length - 1, // 最后一步强制,其余可选
|
|
374
|
+
timeoutMs: (toolInfo.timeoutMs || 60000) + 20000, // 工具超时 + 20s 缓冲
|
|
375
|
+
retry: { max: toolInfo.retryMax || 1, delayMs: 2000 },
|
|
376
|
+
task: {
|
|
377
|
+
type: 'decomposed-exec', // Worker 需要处理此类型(或在 index.js 中扩展)
|
|
378
|
+
decomposeTaskId: taskId,
|
|
379
|
+
step: stepDef.step,
|
|
380
|
+
totalSteps: enrichedSteps.length,
|
|
381
|
+
description: stepDef.description.substring(0, 200),
|
|
382
|
+
action: stepDef.action || `step-${stepDef.step}`,
|
|
383
|
+
targetTool: toolInfo.tool,
|
|
384
|
+
stepFile: join(DECOMPOSED_DIR, `${taskId}-step${stepDef.step}.json`),
|
|
385
|
+
resultFile: stepResultPath,
|
|
386
|
+
circuitBreaker: { maxRetries: toolInfo.retryMax || 1, delayMs: 2000 },
|
|
387
|
+
timeoutMs: toolInfo.timeoutMs || 60000,
|
|
388
|
+
outputDir: DECOMPOSED_DIR,
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}).filter(Boolean);
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
taskId,
|
|
395
|
+
pipelineDef: {
|
|
396
|
+
name: `任务分解-${taskId}`,
|
|
397
|
+
stages: pipelineStages,
|
|
398
|
+
fallback: level === 'L7' ? 'skip-dependents' : level === 'L6' ? 'skip-dependents' : 'abort',
|
|
399
|
+
params: {
|
|
400
|
+
decomposeTaskId: taskId,
|
|
401
|
+
sourceLevel: level,
|
|
402
|
+
totalSteps: enrichedSteps.length,
|
|
403
|
+
originalTask: taskText.substring(0, 300),
|
|
404
|
+
decomposedDir: DECOMPOSED_DIR,
|
|
405
|
+
resultFilePattern: `${taskId}-step{N}.result.json`,
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
dagStages,
|
|
409
|
+
resultPaths,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Worker E(route-decompose)核心函数
|
|
415
|
+
*
|
|
416
|
+
* 在双树评估的大树阶段运行,对 L5+ 任务进行深度分解。
|
|
417
|
+
* 输出可直接写入 shared/decomposed/ 并由 Worker 并行消费。
|
|
418
|
+
*
|
|
419
|
+
* v2: 1) enrichedSteps 携带 toolInfo 传递到下游
|
|
420
|
+
* 2) 每步写入独立步骤文件(含 tool 和 timeout 信息)
|
|
421
|
+
* 3) 每步结果文件独立(支持独立熔断)
|
|
422
|
+
* 4) pipeline 阶段使用 tool-aware timeout
|
|
423
|
+
*
|
|
424
|
+
* @param {string} taskText - 原始任务文本
|
|
425
|
+
* @returns {Promise<object>} 分解结果
|
|
426
|
+
* - taskId: string
|
|
427
|
+
* - decomposed: boolean
|
|
428
|
+
* - steps: Array<{step, description, action, tool, timeoutMs, retryMax}>
|
|
429
|
+
* - pipeline: object|null
|
|
430
|
+
* - filePaths: string[]
|
|
431
|
+
* - recommendedConcurrency: number
|
|
432
|
+
* - note: string
|
|
433
|
+
*/
|
|
434
|
+
async function routeDecompose(taskText) {
|
|
435
|
+
if (!taskText || taskText.length < 5) {
|
|
436
|
+
return {
|
|
437
|
+
taskId: genTaskId(taskText || 'empty'),
|
|
438
|
+
decomposed: false,
|
|
439
|
+
steps: [{ step: 1, description: taskText || '无描述', action: '完整', tool: 'spawn_subagent', timeoutMs: 120000, retryMax: 0 }],
|
|
440
|
+
pipeline: null,
|
|
441
|
+
filePaths: [],
|
|
442
|
+
recommendedConcurrency: 1,
|
|
443
|
+
note: '任务描述过短,无法分解',
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 1. 快路径:检测任务复杂度(L0-L4 直接跳过分解)
|
|
448
|
+
const complexityPatterns = [
|
|
449
|
+
{ min: 5, pattern: /步骤\s*\d|Step\s*\d|第.*步|首先.*然后.*最后|1[.、].*\n\s*2[.、]/i, hint: '显式步骤' },
|
|
450
|
+
{ min: 4, pattern: /分析|设计|实现|测试|部署|优化|重构/g, hint: '多行动动词' },
|
|
451
|
+
{ min: 3, pattern: /搜索|查找|调研|比较|对比|生成|编写|整理/g, hint: '复合操作' },
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
let complexity = 0;
|
|
455
|
+
for (const rule of complexityPatterns) {
|
|
456
|
+
const matches = taskText.match(rule.pattern);
|
|
457
|
+
if (matches) {
|
|
458
|
+
complexity = Math.max(complexity, rule.min);
|
|
459
|
+
if (Array.isArray(matches)) complexity = Math.max(complexity, matches.length + 2);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 简单任务(L0-L4)→ 不分解
|
|
464
|
+
if (complexity < 3) {
|
|
465
|
+
return {
|
|
466
|
+
taskId: genTaskId(taskText),
|
|
467
|
+
decomposed: false,
|
|
468
|
+
steps: [{ step: 1, description: taskText.substring(0, 200), action: '完整', tool: 'spawn_subagent', timeoutMs: 120000, retryMax: 0 }],
|
|
469
|
+
pipeline: null,
|
|
470
|
+
filePaths: [],
|
|
471
|
+
recommendedConcurrency: 1,
|
|
472
|
+
note: '任务复杂度较低(L0-L4),无需分解',
|
|
473
|
+
complexity,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// 2. 复杂任务 → 完整分解
|
|
478
|
+
const level = complexity >= 7 ? 'L7' : complexity >= 5 ? 'L6' : 'L5';
|
|
479
|
+
const { taskId, steps } = decomposeTask(taskText, level);
|
|
480
|
+
|
|
481
|
+
if (steps.length <= 1) {
|
|
482
|
+
return {
|
|
483
|
+
taskId,
|
|
484
|
+
decomposed: false,
|
|
485
|
+
steps: [{ step: 1, description: taskText.substring(0, 200), action: '完整', tool: 'spawn_subagent', timeoutMs: 120000, retryMax: 0 }],
|
|
486
|
+
pipeline: null,
|
|
487
|
+
filePaths: [],
|
|
488
|
+
recommendedConcurrency: 1,
|
|
489
|
+
note: '分解后仅单步骤,不值得',
|
|
490
|
+
complexity,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// 3. 为每步分配工具信息(enrich with toolInfo)
|
|
495
|
+
const enrichedSteps = steps.map(s => {
|
|
496
|
+
const toolInfo = inferStepTool(s.description, s.action);
|
|
497
|
+
return {
|
|
498
|
+
step: s.step,
|
|
499
|
+
description: s.description,
|
|
500
|
+
action: s.action || `step-${s.step}`,
|
|
501
|
+
toolInfo, // ★ 传递 toolInfo 到下游
|
|
502
|
+
tool: toolInfo.tool,
|
|
503
|
+
mode: toolInfo.mode,
|
|
504
|
+
timeoutMs: toolInfo.timeoutMs,
|
|
505
|
+
retryMax: toolInfo.retryMax,
|
|
506
|
+
};
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// 4. 写入 shared/decomposed/
|
|
510
|
+
let filePaths = [];
|
|
511
|
+
try {
|
|
512
|
+
filePaths = await writeDecomposedSteps(taskId, enrichedSteps, taskText);
|
|
513
|
+
} catch (err) {
|
|
514
|
+
// write failed不阻塞
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// 5. 构建 pipeline DAG(使用 enrichedSteps 传递 toolInfo)
|
|
518
|
+
const pipeline = stepsToPipeline(taskText, level, enrichedSteps);
|
|
519
|
+
|
|
520
|
+
// 6. 计算推荐并发度
|
|
521
|
+
const maxParallel = level === 'L7' ? Math.min(steps.length, 5)
|
|
522
|
+
: level === 'L6' ? Math.min(steps.length, 3)
|
|
523
|
+
: 1;
|
|
524
|
+
|
|
525
|
+
// 7. 构建结果步骤列表(不含 toolInfo — 轻量返回给routing system)
|
|
526
|
+
const resultSteps = enrichedSteps.map(s => ({
|
|
527
|
+
step: s.step,
|
|
528
|
+
description: s.description.substring(0, 120),
|
|
529
|
+
action: s.action,
|
|
530
|
+
tool: s.tool,
|
|
531
|
+
timeoutMs: s.timeoutMs,
|
|
532
|
+
retryMax: s.retryMax,
|
|
533
|
+
}));
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
taskId,
|
|
537
|
+
decomposed: true,
|
|
538
|
+
steps: resultSteps,
|
|
539
|
+
pipeline,
|
|
540
|
+
filePaths,
|
|
541
|
+
recommendedConcurrency: maxParallel,
|
|
542
|
+
complexity,
|
|
543
|
+
level,
|
|
544
|
+
note: `✅ 任务分解成功: ${taskId} → ${steps.length} 步骤 (${level}, 复杂度=${complexity})`,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ====== 原有函数(保留) ======
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* 确保 decomposed 目录存在
|
|
552
|
+
*/
|
|
553
|
+
async function ensureDecomposedDir() {
|
|
554
|
+
try { await mkdir(DECOMPOSED_DIR, { recursive: true }); } catch {}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* 从任务文本生成稳定 taskId
|
|
559
|
+
*/
|
|
560
|
+
function genTaskId(text) {
|
|
561
|
+
const hash = crypto.createHash('sha256').update(text, 'utf-8').digest('hex');
|
|
562
|
+
return hash.substring(0, 12);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* 拆解任务描述为子步骤
|
|
567
|
+
*
|
|
568
|
+
* 策略(优先级递减):
|
|
569
|
+
* A. 步骤N: / Step N: / 第N步: / N:(行首或前有空格)
|
|
570
|
+
* B. 编号列表 1.xxx / 1、xxx
|
|
571
|
+
* C. 首先…然后…接着…最后
|
|
572
|
+
* D. 行动动词(行首或分隔符后)
|
|
573
|
+
* E. 自然段落换行(≥3行)
|
|
574
|
+
* F. 兜底
|
|
575
|
+
*/
|
|
576
|
+
function decomposeTask(taskText, level) {
|
|
577
|
+
const steps = [];
|
|
578
|
+
|
|
579
|
+
// ------ 策略 A:步骤N / Step N / 第N步 / 编号+冒号 ------
|
|
580
|
+
// A1: 步骤N:内容 / Step N:内容 / 第N步:内容
|
|
581
|
+
let matches = [];
|
|
582
|
+
let m;
|
|
583
|
+
const a1 = /(?:步骤|step|第)[\s ]*(\d+)[\s ]*[::]\s*([^\n]+?)(?=\s*(?:步骤|\d{1,2}\s*[::..])|\s*$)/gi;
|
|
584
|
+
while ((m = a1.exec(taskText)) !== null) {
|
|
585
|
+
const desc = m[2].trim();
|
|
586
|
+
if (desc.length > 2) matches.push({ step: parseInt(m[1]), description: desc });
|
|
587
|
+
}
|
|
588
|
+
if (matches.length >= 2) { steps.push(...matches); }
|
|
589
|
+
|
|
590
|
+
// A2: 内联编号列表
|
|
591
|
+
if (steps.length < 2) {
|
|
592
|
+
matches = [];
|
|
593
|
+
const a2 = /(?:^|[\s ,,;:;])(\d{1,2})\s*[::..]\s*([^\n]+?)(?=\s*\d{1,2}\s*[::..]|\n|\s*$)/g;
|
|
594
|
+
while ((m = a2.exec(taskText)) !== null) {
|
|
595
|
+
const desc = m[2].trim();
|
|
596
|
+
if (desc.length > 3) matches.push({ step: parseInt(m[1]), description: desc });
|
|
597
|
+
}
|
|
598
|
+
if (matches.length >= 2) steps.push(...matches);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ------ 策略 B:编号列表(行首 1.xxx) ------
|
|
602
|
+
if (steps.length < 2) {
|
|
603
|
+
matches = [];
|
|
604
|
+
const b1 = /(?:^|\n)[\s ]*(\d+)[.、.]\s*([^\n]{4,})/g;
|
|
605
|
+
while ((m = b1.exec(taskText)) !== null) {
|
|
606
|
+
const desc = m[2].trim();
|
|
607
|
+
if (desc.length > 3 && !/^\d/.test(desc)) {
|
|
608
|
+
matches.push({ step: parseInt(m[1]), description: desc });
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (matches.length >= 2) steps.push(...matches);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ------ 策略 C:首先…然后…接着…最后 ------
|
|
615
|
+
if (steps.length < 2) {
|
|
616
|
+
const seqPat = /(?:首先|最先|先(?!生))[::,,\s ]*([^。\n]{4,})/i;
|
|
617
|
+
const thenPat = /(?:然后|接着|之后|随后|其次|接下来|下一步|再(?!次))[::,,\s ]*([^。\n]{4,})/gi;
|
|
618
|
+
const finallyPat = /(?:最后|最终|末尾|收尾)[::,,\s ]*([^。\n]{4,})/i;
|
|
619
|
+
|
|
620
|
+
const firstMatch = seqPat.exec(taskText);
|
|
621
|
+
const thenMatches = [];
|
|
622
|
+
const finallyMatch = finallyPat.exec(taskText);
|
|
623
|
+
|
|
624
|
+
let tm;
|
|
625
|
+
while ((tm = thenPat.exec(taskText)) !== null) {
|
|
626
|
+
const desc = tm[1].trim();
|
|
627
|
+
if (desc.length > 3) thenMatches.push(desc);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
let stepCounter = 0;
|
|
631
|
+
if (firstMatch && firstMatch[1].trim().length > 3) {
|
|
632
|
+
steps.push({ step: ++stepCounter, description: firstMatch[1].trim(), action: '先' });
|
|
633
|
+
}
|
|
634
|
+
for (const desc of thenMatches) {
|
|
635
|
+
if (stepCounter < 10) steps.push({ step: ++stepCounter, description: desc, action: '然后' });
|
|
636
|
+
}
|
|
637
|
+
if (finallyMatch && finallyMatch[1].trim().length > 3) {
|
|
638
|
+
steps.push({ step: ++stepCounter, description: finallyMatch[1].trim(), action: '最后' });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ------ 策略 D:行动动词 ------
|
|
643
|
+
if (steps.length < 2) {
|
|
644
|
+
const actionPatterns = [
|
|
645
|
+
/(?:^|[\s ,,;:;。])(分析)[::,,\s ]*([^\n,,;;。]{4,})/g,
|
|
646
|
+
/(?:^|[\s ,,;:;。])(评估)[::,,\s ]*([^\n,,;;。]{4,})/g,
|
|
647
|
+
/(?:^|[\s ,,;:;。])(设计)[::,,\s ]*([^\n,,;;。]{4,})/g,
|
|
648
|
+
/(?:^|[\s ,,;:;。])(实现)[::,,\s ]*([^\n,,;;。]{4,})/g,
|
|
649
|
+
/(?:^|[\s ,,;:;。])(测试)[::,,\s ]*([^\n,,;;。]{4,})/g,
|
|
650
|
+
/(?:^|[\s ,,;:;。])(部署)[::,,\s ]*([^\n,,;;。]{4,})/g,
|
|
651
|
+
/(?:^|[\s ,,;:;。])(优化)[::,,\s ]*([^\n,,;;。]{4,})/g,
|
|
652
|
+
/(?:^|[\s ,,;:;。])(重构)[::,,\s ]*([^\n,,;;。]{4,})/g,
|
|
653
|
+
/(?:^|[\s ,,;:;。])(编写)[::,,\s ]*([^\n,,;;。]{4,})/g,
|
|
654
|
+
/(?:^|[\s ,,;:;。])(生成)[::,,\s ]*([^\n,,;;。]{4,})/g,
|
|
655
|
+
];
|
|
656
|
+
|
|
657
|
+
const actionMatches = new Map();
|
|
658
|
+
let order = 0;
|
|
659
|
+
for (const pat of actionPatterns) {
|
|
660
|
+
while ((m = pat.exec(taskText)) !== null) {
|
|
661
|
+
const action = m[1];
|
|
662
|
+
const desc = m[2].trim();
|
|
663
|
+
if (desc.length > 3 && !actionMatches.has(desc)) {
|
|
664
|
+
actionMatches.set(desc, { order: order++, description: desc, action });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (actionMatches.size >= 2) {
|
|
670
|
+
const sorted = [...actionMatches.values()].sort((a, b) => a.order - b.order);
|
|
671
|
+
sorted.forEach((item, idx) => {
|
|
672
|
+
steps.push({ step: idx + 1, description: item.description, action: item.action });
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// ------ 策略 E:按自然段落换行(≥3行) ------
|
|
678
|
+
if (steps.length < 2) {
|
|
679
|
+
const lines = taskText.split('\n')
|
|
680
|
+
.map(l => l.trim())
|
|
681
|
+
.filter(l => {
|
|
682
|
+
if (l.length < 6) return false;
|
|
683
|
+
if (/^(?:🏃|思考|模式|⚡|#|---)/.test(l)) return false;
|
|
684
|
+
if (/^(?:任务|步骤|输出|验收|结果)[::]/.test(l)) return false;
|
|
685
|
+
return true;
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
if (lines.length >= 3) {
|
|
689
|
+
lines.slice(0, 10).forEach((line, idx) => {
|
|
690
|
+
steps.push({ step: idx + 1, description: line.substring(0, 200) });
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ------ 策略 F:兜底 ------
|
|
696
|
+
if (steps.length === 0) {
|
|
697
|
+
const clean = taskText
|
|
698
|
+
.replace(/^.*?任务[::]/s, '')
|
|
699
|
+
.replace(/\n⚡.*/s, '')
|
|
700
|
+
.replace(/\n---.*/s, '')
|
|
701
|
+
.trim()
|
|
702
|
+
.substring(0, 300);
|
|
703
|
+
steps.push({ step: 1, description: clean || '执行完整任务', action: '完整' });
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const finalSteps = steps.slice(0, 10);
|
|
707
|
+
if (level === 'L7' && finalSteps.length < 3) {
|
|
708
|
+
finalSteps.push({ step: finalSteps.length + 1, description: '验证整体结果并输出最终报告', action: '验证' });
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (finalSteps.length >= 2) {
|
|
712
|
+
for (const s of finalSteps) {
|
|
713
|
+
if (!s.action) {
|
|
714
|
+
const stepAction = s.description.match(/^(搜索|查找|调研|获取|提取|爬取|分析|评估|对比|比较|设计|规划|方案|实现|编写|编码|修改|修复|重构|测试|验证|部署|安装|同步|回顾|回忆|检索|整理|汇总|生成|研究)/);
|
|
715
|
+
s.action = stepAction ? stepAction[1] : `step-${s.step}`;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return { taskId: genTaskId(taskText), steps: finalSteps };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* 将分解后的步骤写入 shared/decomposed/ 目录
|
|
725
|
+
*
|
|
726
|
+
* v2: 步骤文件包含完整的 toolInfo、timeout、resultFile 字段
|
|
727
|
+
* 供 Worker 或子 agent 读取后直接执行
|
|
728
|
+
*
|
|
729
|
+
* @param {string} taskId
|
|
730
|
+
* @param {Array<{step: number, description: string, action?: string, toolInfo?: object, tool?: string, timeoutMs?: number, retryMax?: number}>} steps - 已富化的步骤
|
|
731
|
+
* @param {string} sourceTask
|
|
732
|
+
* @returns {Promise<string[]>} 写入的文件路径列表
|
|
733
|
+
*/
|
|
734
|
+
async function writeDecomposedSteps(taskId, steps, sourceTask) {
|
|
735
|
+
await ensureDecomposedDir();
|
|
736
|
+
const written = [];
|
|
737
|
+
|
|
738
|
+
for (const s of steps) {
|
|
739
|
+
const fileName = `${taskId}-step${s.step}.json`;
|
|
740
|
+
const resultFileName = `${taskId}-step${s.step}.result.json`;
|
|
741
|
+
const filePath = join(DECOMPOSED_DIR, fileName);
|
|
742
|
+
const resultFilePath = join(DECOMPOSED_DIR, resultFileName);
|
|
743
|
+
|
|
744
|
+
// 从 toolInfo 提取工具的详细配置
|
|
745
|
+
const toolInfo = s.toolInfo || inferStepTool(s.description, s.action);
|
|
746
|
+
|
|
747
|
+
// ★ 完整的步骤定义:包含独立超时、结果文件、熔断配置
|
|
748
|
+
const payload = {
|
|
749
|
+
decomposedTaskId: taskId,
|
|
750
|
+
step: s.step,
|
|
751
|
+
totalSteps: steps.length,
|
|
752
|
+
description: s.description,
|
|
753
|
+
action: s.action || `step-${s.step}`,
|
|
754
|
+
parentTask: sourceTask.substring(0, 100),
|
|
755
|
+
timestamp: Date.now(),
|
|
756
|
+
|
|
757
|
+
// ★ 新增:独立执行配置
|
|
758
|
+
tool: toolInfo.tool,
|
|
759
|
+
mode: toolInfo.mode,
|
|
760
|
+
timeoutMs: toolInfo.timeoutMs || 60000,
|
|
761
|
+
retryMax: toolInfo.retryMax || 1,
|
|
762
|
+
resultFile: resultFilePath,
|
|
763
|
+
circuitBreaker: {
|
|
764
|
+
maxRetries: toolInfo.retryMax || 1,
|
|
765
|
+
delayMs: 2000,
|
|
766
|
+
},
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
const tmpPath = filePath + '.tmp';
|
|
770
|
+
await writeFile(tmpPath, JSON.stringify(payload, null, 2), 'utf-8');
|
|
771
|
+
await rename(tmpPath, filePath);
|
|
772
|
+
|
|
773
|
+
written.push(filePath);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return written;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// ====== 新增:读取已分解的步骤文件 ======
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* 读取 shared/decomposed/ 目录中的已分解步骤文件
|
|
783
|
+
* 供 Worker 或子 agent 读取并执行
|
|
784
|
+
*
|
|
785
|
+
* @param {string} taskId
|
|
786
|
+
* @param {number} step
|
|
787
|
+
* @returns {Promise<object|null>} 步骤定义或 null
|
|
788
|
+
*/
|
|
789
|
+
async function readDecomposedStep(taskId, step) {
|
|
790
|
+
const fileName = `${taskId}-step${step}.json`;
|
|
791
|
+
const filePath = join(DECOMPOSED_DIR, fileName);
|
|
792
|
+
try {
|
|
793
|
+
const content = await readFile(filePath, 'utf-8');
|
|
794
|
+
return JSON.parse(content);
|
|
795
|
+
} catch {
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* 列出指定 taskId 的所有分解步骤
|
|
802
|
+
*
|
|
803
|
+
* @param {string} taskId
|
|
804
|
+
* @returns {Promise<Array<{step: number, description: string, tool: string, timeoutMs: number}>>}
|
|
805
|
+
*/
|
|
806
|
+
async function listDecomposedSteps(taskId) {
|
|
807
|
+
try {
|
|
808
|
+
const files = await import('fs/promises').then(fs => fs.readdir(DECOMPOSED_DIR));
|
|
809
|
+
const stepFiles = files
|
|
810
|
+
.filter(f => f.startsWith(taskId) && f.endsWith('.json') && !f.endsWith('.result.json'))
|
|
811
|
+
.sort();
|
|
812
|
+
const steps = [];
|
|
813
|
+
for (const f of stepFiles) {
|
|
814
|
+
try {
|
|
815
|
+
const content = await readFile(join(DECOMPOSED_DIR, f), 'utf-8');
|
|
816
|
+
const data = JSON.parse(content);
|
|
817
|
+
steps.push({
|
|
818
|
+
step: data.step,
|
|
819
|
+
description: data.description,
|
|
820
|
+
tool: data.tool,
|
|
821
|
+
timeoutMs: data.timeoutMs,
|
|
822
|
+
action: data.action,
|
|
823
|
+
resultFile: data.resultFile,
|
|
824
|
+
});
|
|
825
|
+
} catch {}
|
|
826
|
+
}
|
|
827
|
+
return steps.sort((a, b) => a.step - b.step);
|
|
828
|
+
} catch {
|
|
829
|
+
return [];
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* 写入某步骤的执行结果
|
|
835
|
+
* 每步独立结果文件,支持独立熔断
|
|
836
|
+
*
|
|
837
|
+
* @param {string} taskId
|
|
838
|
+
* @param {number} step
|
|
839
|
+
* @param {object} result - { status: 'success'|'error'|'timeout', output?, error?, durationMs }
|
|
840
|
+
* @returns {Promise<string|null>}
|
|
841
|
+
*/
|
|
842
|
+
async function writeStepResult(taskId, step, result) {
|
|
843
|
+
await ensureDecomposedDir();
|
|
844
|
+
const fileName = `${taskId}-step${step}.result.json`;
|
|
845
|
+
const filePath = join(DECOMPOSED_DIR, fileName);
|
|
846
|
+
const payload = {
|
|
847
|
+
decomposedTaskId: taskId,
|
|
848
|
+
step,
|
|
849
|
+
result,
|
|
850
|
+
timestamp: Date.now(),
|
|
851
|
+
};
|
|
852
|
+
const tmpPath = filePath + '.tmp';
|
|
853
|
+
try {
|
|
854
|
+
await writeFile(tmpPath, JSON.stringify(payload, null, 2), 'utf-8');
|
|
855
|
+
await rename(tmpPath, filePath);
|
|
856
|
+
return filePath;
|
|
857
|
+
} catch {
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* 读取某步骤的执行结果
|
|
864
|
+
*
|
|
865
|
+
* @param {string} taskId
|
|
866
|
+
* @param {number} step
|
|
867
|
+
* @returns {Promise<object|null>}
|
|
868
|
+
*/
|
|
869
|
+
async function readStepResult(taskId, step) {
|
|
870
|
+
const fileName = `${taskId}-step${step}.result.json`;
|
|
871
|
+
const filePath = join(DECOMPOSED_DIR, fileName);
|
|
872
|
+
try {
|
|
873
|
+
const content = await readFile(filePath, 'utf-8');
|
|
874
|
+
return JSON.parse(content);
|
|
875
|
+
} catch {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// ====== 导出 ======
|
|
881
|
+
|
|
882
|
+
export {
|
|
883
|
+
decomposeTask,
|
|
884
|
+
writeDecomposedSteps,
|
|
885
|
+
DECOMPOSED_DIR,
|
|
886
|
+
ensureDecomposedDir,
|
|
887
|
+
genTaskId,
|
|
888
|
+
inferStepTool,
|
|
889
|
+
buildDAGFromSteps,
|
|
890
|
+
stepsToPipeline,
|
|
891
|
+
routeDecompose,
|
|
892
|
+
readDecomposedStep,
|
|
893
|
+
listDecomposedSteps,
|
|
894
|
+
writeStepResult,
|
|
895
|
+
readStepResult,
|
|
896
|
+
};
|