@fieldwangai/agentflow 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/README.zh-CN.md +201 -0
  4. package/agents/agentflow-node-executor-code.md +32 -0
  5. package/agents/agentflow-node-executor-planning.md +32 -0
  6. package/agents/agentflow-node-executor-requirement.md +32 -0
  7. package/agents/agentflow-node-executor-test.md +32 -0
  8. package/agents/agentflow-node-executor-ui.md +32 -0
  9. package/agents/agentflow-node-executor.md +32 -0
  10. package/agents/agents.json +8 -0
  11. package/agents/en/agentflow-node-executor.md +32 -0
  12. package/agents/zh/agentflow-node-executor.md +32 -0
  13. package/bin/agentflow.mjs +52 -0
  14. package/bin/ensure-workspace-reference.mjs +35 -0
  15. package/bin/lib/agent-runners.mjs +1199 -0
  16. package/bin/lib/agents-path.mjs +61 -0
  17. package/bin/lib/api-runner.mjs +361 -0
  18. package/bin/lib/apply.mjs +852 -0
  19. package/bin/lib/catalog-agents.mjs +300 -0
  20. package/bin/lib/catalog-flows.mjs +532 -0
  21. package/bin/lib/composer-agent.mjs +884 -0
  22. package/bin/lib/composer-flow-instances.mjs +68 -0
  23. package/bin/lib/composer-flow-skeleton.mjs +334 -0
  24. package/bin/lib/composer-flow-validate.mjs +47 -0
  25. package/bin/lib/composer-log.mjs +197 -0
  26. package/bin/lib/composer-model-router.mjs +160 -0
  27. package/bin/lib/composer-node-schema.mjs +299 -0
  28. package/bin/lib/composer-planner.mjs +749 -0
  29. package/bin/lib/composer-script-ops.mjs +233 -0
  30. package/bin/lib/composer-skill-router.mjs +384 -0
  31. package/bin/lib/flow-import.mjs +305 -0
  32. package/bin/lib/flow-normalize.mjs +71 -0
  33. package/bin/lib/flow-write.mjs +395 -0
  34. package/bin/lib/help.mjs +139 -0
  35. package/bin/lib/hub-login.mjs +54 -0
  36. package/bin/lib/hub-publish.mjs +159 -0
  37. package/bin/lib/hub-remote.mjs +189 -0
  38. package/bin/lib/hub.mjs +299 -0
  39. package/bin/lib/i18n.mjs +233 -0
  40. package/bin/lib/locales/en.json +344 -0
  41. package/bin/lib/locales/zh.json +344 -0
  42. package/bin/lib/log.mjs +37 -0
  43. package/bin/lib/main.mjs +611 -0
  44. package/bin/lib/model-config.mjs +118 -0
  45. package/bin/lib/model-lists.mjs +188 -0
  46. package/bin/lib/node-exec-context.mjs +336 -0
  47. package/bin/lib/node-execute.mjs +513 -0
  48. package/bin/lib/normalize-node-tool-command.mjs +97 -0
  49. package/bin/lib/paths.mjs +216 -0
  50. package/bin/lib/pipeline-scripts.mjs +41 -0
  51. package/bin/lib/recent-runs.mjs +173 -0
  52. package/bin/lib/run-apply-active-lock.mjs +82 -0
  53. package/bin/lib/run-events.mjs +85 -0
  54. package/bin/lib/run-node-statuses-from-disk.mjs +85 -0
  55. package/bin/lib/schedule-config.mjs +227 -0
  56. package/bin/lib/scheduler.mjs +312 -0
  57. package/bin/lib/table.mjs +4 -0
  58. package/bin/lib/terminal.mjs +42 -0
  59. package/bin/lib/ui-print.mjs +94 -0
  60. package/bin/lib/ui-server.mjs +2113 -0
  61. package/bin/lib/workspace-tree.mjs +266 -0
  62. package/bin/lib/workspace.mjs +180 -0
  63. package/bin/pipeline/build-node-prompt.mjs +179 -0
  64. package/bin/pipeline/check-cache.mjs +191 -0
  65. package/bin/pipeline/check-flow.mjs +543 -0
  66. package/bin/pipeline/collect-nodes.mjs +212 -0
  67. package/bin/pipeline/compute-cache-md5.mjs +177 -0
  68. package/bin/pipeline/ensure-run-dir.mjs +71 -0
  69. package/bin/pipeline/extract-thinking.mjs +308 -0
  70. package/bin/pipeline/gc.mjs +129 -0
  71. package/bin/pipeline/get-env.mjs +83 -0
  72. package/bin/pipeline/get-exec-id.mjs +145 -0
  73. package/bin/pipeline/get-ready-nodes.mjs +435 -0
  74. package/bin/pipeline/get-resolved-values.mjs +337 -0
  75. package/bin/pipeline/load-key.mjs +62 -0
  76. package/bin/pipeline/parse-bool.mjs +33 -0
  77. package/bin/pipeline/parse-flow.mjs +698 -0
  78. package/bin/pipeline/post-process-control-if.mjs +23 -0
  79. package/bin/pipeline/post-process-node.mjs +490 -0
  80. package/bin/pipeline/pre-process-node.mjs +449 -0
  81. package/bin/pipeline/resolve-inputs.mjs +201 -0
  82. package/bin/pipeline/run-log.mjs +34 -0
  83. package/bin/pipeline/run-tool-nodejs.mjs +160 -0
  84. package/bin/pipeline/save-key.mjs +93 -0
  85. package/bin/pipeline/snapshot-prior-round.mjs +70 -0
  86. package/bin/pipeline/validate-flow.mjs +825 -0
  87. package/bin/pipeline/validate-for-ui.mjs +226 -0
  88. package/bin/pipeline/validate-script-output.mjs +130 -0
  89. package/bin/pipeline/write-result.mjs +182 -0
  90. package/builtin/nodes/agent_subAgent.md +14 -0
  91. package/builtin/nodes/control_agent_toBool.md +20 -0
  92. package/builtin/nodes/control_anyOne.md +17 -0
  93. package/builtin/nodes/control_end.md +11 -0
  94. package/builtin/nodes/control_if.md +20 -0
  95. package/builtin/nodes/control_start.md +11 -0
  96. package/builtin/nodes/control_toBool.md +21 -0
  97. package/builtin/nodes/provide_file.md +11 -0
  98. package/builtin/nodes/provide_str.md +11 -0
  99. package/builtin/nodes/tool_get_env.md +14 -0
  100. package/builtin/nodes/tool_load_key.md +20 -0
  101. package/builtin/nodes/tool_nodejs.md +40 -0
  102. package/builtin/nodes/tool_print.md +14 -0
  103. package/builtin/nodes/tool_save_key.md +20 -0
  104. package/builtin/nodes/tool_user_ask.md +23 -0
  105. package/builtin/nodes/tool_user_check.md +22 -0
  106. package/builtin/pipelines/module-migrate/flow.yaml +819 -0
  107. package/builtin/pipelines/module-migrate/scripts/check_imports.mjs +700 -0
  108. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Makefile +362 -0
  109. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/node_modules/node-addon-api/node_addon_api_except.stamp.d +1 -0
  110. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/bindings/node/binding.o.d +17 -0
  111. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o.d +5 -0
  112. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o.d +8 -0
  113. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/tree_sitter_kotlin_binding.node.d +1 -0
  114. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/node_modules/node-addon-api/node_addon_api_except.stamp +0 -0
  115. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/bindings/node/binding.o +0 -0
  116. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o +0 -0
  117. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o +0 -0
  118. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/tree_sitter_kotlin_binding.node +0 -0
  119. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/binding.Makefile +6 -0
  120. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/gyp-mac-tool +768 -0
  121. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.Makefile +6 -0
  122. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.target.mk +122 -0
  123. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api_except.target.mk +126 -0
  124. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api_maybe.target.mk +122 -0
  125. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/tree_sitter_kotlin_binding.target.mk +203 -0
  126. package/builtin/pipelines/new/flow.yaml +545 -0
  127. package/builtin/pipelines/new/scripts/check-flow.mjs +9 -0
  128. package/builtin/pipelines/new/scripts/collect-nodes.mjs +211 -0
  129. package/builtin/pipelines/scripts/adjust-node-positions.mjs +113 -0
  130. package/builtin/web-ui/dist/agentflow-icon.svg +23 -0
  131. package/builtin/web-ui/dist/assets/index-CZkUPcXE.css +1 -0
  132. package/builtin/web-ui/dist/assets/index-DkkhNESc.js +190 -0
  133. package/builtin/web-ui/dist/index.html +24 -0
  134. package/package.json +67 -0
  135. package/reference/flow-control-capabilities.md +274 -0
  136. package/reference/flow-layout.md +84 -0
  137. package/reference/flow-prompt-handler-check.md +12 -0
  138. package/reference/flow-result-semantics.md +14 -0
@@ -0,0 +1,884 @@
1
+ /**
2
+ * Composer 执行器:支持单步执行(兼容旧逻辑)和多步编排执行。
3
+ *
4
+ * 多步模式流程:
5
+ * 用户 prompt → planner 分解 → [script 直执 | agent 子调用(按复杂度选模型)] → sync UI
6
+ */
7
+ import fs from "fs";
8
+ import path from "path";
9
+ import { getAgentflowDataRoot } from "./paths.mjs";
10
+ import { resolveCliAndModel } from "./model-config.mjs";
11
+ import { runClaudeCodeAgentWithPrompt, runCursorAgentWithPrompt, runOpenCodeAgentWithPrompt } from "./agent-runners.mjs";
12
+ import { planComposerTasks, hasPlannerApiAvailable, shouldUsePhased, classifyComplexity, classifyTaskComplexity, PHASED_DEFINITIONS } from "./composer-planner.mjs";
13
+ import { executeScriptOp, isSupportedScriptOp } from "./composer-script-ops.mjs";
14
+ import { routeModel } from "./composer-model-router.mjs";
15
+ import { validateComposerFlowYaml, formatValidationErrorsBlock } from "./composer-flow-validate.mjs";
16
+ import { parseInstanceRoleModelMap } from "./composer-flow-instances.mjs";
17
+ import { buildNodeSchemaPromptSection, buildNodeSchemaCompactSection, getBuiltinNodeSchemas, EXTENSIBLE_DEFINITIONS } from "./composer-node-schema.mjs";
18
+ import { ensurePhase1Skeletons, applyPlannedSlotsFromSpec } from "./composer-flow-skeleton.mjs";
19
+ import yaml from "js-yaml";
20
+ import { t } from "./i18n.mjs";
21
+
22
+ const MAX_PROMPT_CHARS = 500_000;
23
+ const MAX_COMPOSER_VALIDATION_REPAIR = 5;
24
+ const MAX_SCRIPT_INJECT_BYTES = 30_000;
25
+
26
+ // ─── script 内容注入辅助 ─────────────────────────────────────────────────
27
+
28
+ /**
29
+ * 从文本中提取 .mjs 脚本文件名(去重)。
30
+ */
31
+ function extractScriptFilenames(text) {
32
+ if (!text) return [];
33
+ const matches = text.match(/[\w\-.]+\.mjs/g);
34
+ return matches ? [...new Set(matches)] : [];
35
+ }
36
+
37
+ /**
38
+ * 读取 scripts/ 目录下指定 .mjs 文件内容,拼为 prompt 注入块。
39
+ */
40
+ function readPipelineScriptContents(scriptsDirAbs, filenames, nodeHint) {
41
+ if (!scriptsDirAbs || !filenames.length) return "";
42
+ const blocks = [];
43
+ let totalBytes = 0;
44
+ for (const fn of filenames) {
45
+ try {
46
+ const content = fs.readFileSync(path.join(scriptsDirAbs, fn), "utf-8");
47
+ if (totalBytes + content.length > MAX_SCRIPT_INJECT_BYTES) break;
48
+ totalBytes += content.length;
49
+ const header = nodeHint ? `### ${fn}(节点 \`${nodeHint}\`)` : `### ${fn}`;
50
+ blocks.push(`${header}\n\`\`\`javascript\n${content.trimEnd()}\n\`\`\``);
51
+ } catch { /* file not found — skip */ }
52
+ }
53
+ return blocks.length
54
+ ? `## 关联脚本文件内容(scripts/ 目录)\n\n${blocks.join("\n\n")}`
55
+ : "";
56
+ }
57
+
58
+ /**
59
+ * 为选中的 tool_nodejs 实例构建脚本内容注入块(供单步 Composer 使用)。
60
+ */
61
+ export function buildScriptContentBlockForInstances(flowYamlAbs, instanceIds) {
62
+ if (!flowYamlAbs || !instanceIds?.length) return "";
63
+ try {
64
+ const flowDir = path.dirname(flowYamlAbs);
65
+ const scriptsDirAbs = path.join(flowDir, "scripts");
66
+ if (!fs.existsSync(scriptsDirAbs)) return "";
67
+ const flowRaw = fs.readFileSync(flowYamlAbs, "utf-8");
68
+ const flowDoc = yaml.load(flowRaw);
69
+ const instances = flowDoc?.instances || {};
70
+ const blocks = [];
71
+ let totalBytes = 0;
72
+ for (const id of instanceIds) {
73
+ const inst = instances[id];
74
+ if (!inst || inst.definitionId !== "tool_nodejs" || !inst.script) continue;
75
+ const filenames = extractScriptFilenames(String(inst.script));
76
+ for (const fn of filenames) {
77
+ try {
78
+ const content = fs.readFileSync(path.join(scriptsDirAbs, fn), "utf-8");
79
+ if (totalBytes + content.length > MAX_SCRIPT_INJECT_BYTES) break;
80
+ totalBytes += content.length;
81
+ blocks.push(`### ${fn}(节点 \`${id}\`)\n\`\`\`javascript\n${content.trimEnd()}\n\`\`\``);
82
+ } catch { /* skip */ }
83
+ }
84
+ }
85
+ if (!blocks.length) return "";
86
+ return `## 关联脚本文件内容(scripts/ 目录)\n\n已选中节点引用的脚本实际代码:\n\n${blocks.join("\n\n")}`;
87
+ } catch {
88
+ return "";
89
+ }
90
+ }
91
+
92
+ /**
93
+ * 为 query 意图构建选中节点的完整上下文块(YAML excerpt + 脚本内容)。
94
+ * 供单步轻量 prompt 使用,不注入编辑规则。
95
+ */
96
+ export function buildQueryContextBlock(flowYamlAbs, instanceIds) {
97
+ if (!flowYamlAbs || !instanceIds?.length) return "";
98
+ try {
99
+ const flowDir = path.dirname(flowYamlAbs);
100
+ const scriptsDirAbs = path.join(flowDir, "scripts");
101
+ const flowRaw = fs.readFileSync(flowYamlAbs, "utf-8");
102
+ const flowDoc = yaml.load(flowRaw);
103
+ const instances = flowDoc?.instances || {};
104
+ const parts = [];
105
+ let totalBytes = 0;
106
+ for (const id of instanceIds) {
107
+ const inst = instances[id];
108
+ if (!inst) continue;
109
+ // YAML excerpt
110
+ const instYaml = yaml.dump({ [id]: inst }, { lineWidth: -1 });
111
+ if (totalBytes + instYaml.length > MAX_SCRIPT_INJECT_BYTES) break;
112
+ totalBytes += instYaml.length;
113
+ parts.push(`### 节点 \`${id}\`(${inst.definitionId || "unknown"})\n\`\`\`yaml\n${instYaml.trimEnd()}\n\`\`\``);
114
+
115
+ // Script content for tool_nodejs
116
+ if (inst.definitionId === "tool_nodejs" && inst.script && fs.existsSync(scriptsDirAbs)) {
117
+ const filenames = extractScriptFilenames(String(inst.script));
118
+ for (const fn of filenames) {
119
+ try {
120
+ const content = fs.readFileSync(path.join(scriptsDirAbs, fn), "utf-8");
121
+ if (totalBytes + content.length > MAX_SCRIPT_INJECT_BYTES) break;
122
+ totalBytes += content.length;
123
+ parts.push(`### 脚本 \`${fn}\`\n\`\`\`javascript\n${content.trimEnd()}\n\`\`\``);
124
+ } catch { /* skip */ }
125
+ }
126
+ }
127
+ }
128
+ if (!parts.length) return "";
129
+ return `## 选中节点上下文\n\n${parts.join("\n\n")}`;
130
+ } catch {
131
+ return "";
132
+ }
133
+ }
134
+
135
+ // ─── 单步模式(向后兼容) ──────────────────────────────────────────────────
136
+
137
+ /**
138
+ * 旧版单步执行:将整个 prompt 一次性发给 Cursor / OpenCode。
139
+ * @param {object} opts
140
+ * @param {string} opts.uiWorkspaceRoot
141
+ * @param {string} [opts.cliWorkspace]
142
+ * @param {string} opts.prompt
143
+ * @param {string} [opts.modelKey]
144
+ * @param {boolean} [opts.force]
145
+ * @param {(ev: object) => void} [opts.onStreamEvent]
146
+ * @returns {{ child: import('child_process').ChildProcess, finished: Promise<void> }}
147
+ */
148
+ export function startComposerAgent(opts) {
149
+ const uiRoot = opts.uiWorkspaceRoot && String(opts.uiWorkspaceRoot).trim();
150
+ if (!uiRoot) throw new Error("Missing uiWorkspaceRoot");
151
+
152
+ const prompt = opts.prompt != null ? String(opts.prompt) : "";
153
+ if (!prompt.trim()) throw new Error("Empty prompt");
154
+ if (prompt.length > MAX_PROMPT_CHARS) throw new Error(`Prompt exceeds ${MAX_PROMPT_CHARS} characters`);
155
+
156
+ const cliWs = opts.cliWorkspace ? String(opts.cliWorkspace) : getAgentflowDataRoot();
157
+ const modelKey = opts.modelKey != null ? String(opts.modelKey).trim() : "";
158
+ const { cli, model } = resolveCliAndModel(uiRoot, modelKey || null, null);
159
+
160
+ const common = {
161
+ onStreamEvent: opts.onStreamEvent,
162
+ force: Boolean(opts.force),
163
+ };
164
+
165
+ if (cli === "opencode") {
166
+ return runOpenCodeAgentWithPrompt(cliWs, prompt, {
167
+ ...common,
168
+ model: model || undefined,
169
+ });
170
+ }
171
+
172
+ if (cli === "claude-code") {
173
+ return runClaudeCodeAgentWithPrompt(cliWs, prompt, {
174
+ ...common,
175
+ model: model || undefined,
176
+ });
177
+ }
178
+
179
+ return runCursorAgentWithPrompt(cliWs, prompt, {
180
+ ...common,
181
+ model: model || undefined,
182
+ });
183
+ }
184
+
185
+ // ─── 为单个 agent 步骤构建 prompt ──────────────────────────────────────────
186
+
187
+ /**
188
+ * 从 flow.yaml 中提取指定 instance 的 YAML 片段,供子 agent 上下文使用,
189
+ * 避免子 agent 重新 Read 整份 flow.yaml 仅为定位一个节点。
190
+ * @param {string} flowYamlAbs
191
+ * @param {string} instanceId
192
+ * @returns {string} 该 instance 的 YAML 文本(缩进保留),找不到返回空串
193
+ */
194
+ function extractInstanceYamlExcerpt(flowYamlAbs, instanceId) {
195
+ if (!flowYamlAbs || !instanceId) return "";
196
+ try {
197
+ const raw = fs.readFileSync(flowYamlAbs, "utf-8");
198
+ const data = yaml.load(raw);
199
+ const inst = data?.instances?.[instanceId];
200
+ if (!inst || typeof inst !== "object") return "";
201
+ return yaml.dump({ [instanceId]: inst }, { lineWidth: 120, noRefs: true });
202
+ } catch {
203
+ return "";
204
+ }
205
+ }
206
+
207
+ function buildAgentStepPrompt(step, flowContext) {
208
+ const parts = [];
209
+ const intentCategory = flowContext?.intentCategory || "generic";
210
+ const isQuery = intentCategory === "query";
211
+
212
+ const nodeRole = step.nodeRole != null ? String(step.nodeRole).trim() : "";
213
+ if (nodeRole) {
214
+ parts.push(`## ${t("composer.task_title").replace("## ", "")}\n${nodeRole}`);
215
+ parts.push("");
216
+ }
217
+
218
+ // 编辑上下文(query 模式跳过编辑规则/skill/sync)
219
+ if (flowContext && !isQuery) {
220
+ parts.push(t("composer.edit_context"));
221
+ if (flowContext.flowYamlAbs) {
222
+ parts.push(`- 图定义文件:${flowContext.flowYamlAbs}`);
223
+ }
224
+ if (flowContext.composerSpecAbs) {
225
+ parts.push(`- 节点规格书:${flowContext.composerSpecAbs}`);
226
+ }
227
+ if (flowContext.pipelineScriptsDirAbs) {
228
+ parts.push(`- 流水线 scripts 目录(tool_nodejs 可执行脚本放此):${flowContext.pipelineScriptsDirAbs}`);
229
+ }
230
+ if (flowContext.flowId) {
231
+ parts.push(`- flowId:${flowContext.flowId}`);
232
+ parts.push(`- flowSource:${flowContext.flowSource || "user"}`);
233
+ }
234
+ if (flowContext.skillsHint) {
235
+ parts.push(flowContext.skillsHint);
236
+ }
237
+ if (flowContext.syncCurlHint) {
238
+ parts.push(`- 保存后执行:${flowContext.syncCurlHint}`);
239
+ }
240
+ parts.push("");
241
+
242
+ if (flowContext.skillInjectionBlock) {
243
+ parts.push(flowContext.skillInjectionBlock);
244
+ parts.push("");
245
+ }
246
+ } else if (flowContext && isQuery) {
247
+ parts.push("## AgentFlow 问答上下文");
248
+ if (flowContext.flowYamlAbs) parts.push(`- 图定义文件:${flowContext.flowYamlAbs}`);
249
+ parts.push("");
250
+ }
251
+
252
+ const sid = step.instanceId != null ? String(step.instanceId).trim() : "";
253
+ const instMap = flowContext?._instanceMap;
254
+ const targetInst = sid && instMap && instMap[sid];
255
+ if (!isQuery && targetInst && targetInst.definitionId === "tool_nodejs") {
256
+ parts.push(t("composer.tool_nodejs_rules_title"));
257
+ parts.push(t("composer.tool_nodejs_rules_body"));
258
+ parts.push("");
259
+ }
260
+
261
+ parts.push(t("composer.task_title"));
262
+ parts.push(step.prompt || step.description || "");
263
+ parts.push("");
264
+
265
+ // 节点 schema 与目标 instance 上下文
266
+ // query 模式:只注入 instance excerpt + script,跳过 schema
267
+ try {
268
+ if (!isQuery) {
269
+ const targetIsExtensible = targetInst && EXTENSIBLE_DEFINITIONS.has(targetInst.definitionId);
270
+ const promptText = String(step.prompt || step.description || "");
271
+ const promptMentionsSlots = /input\s*:|output\s*:|追加|扩展槽|business\s*slot|业务槽/i.test(promptText);
272
+ const useFullSchema = Boolean(targetIsExtensible || promptMentionsSlots);
273
+ const schemaSection = useFullSchema
274
+ ? buildNodeSchemaPromptSection()
275
+ : buildNodeSchemaCompactSection();
276
+ if (schemaSection) {
277
+ parts.push(schemaSection);
278
+ parts.push("");
279
+ }
280
+ }
281
+ // Inject YAML excerpt + script content for target instance (or canvas fallback)
282
+ const idsToInject = sid ? [sid] : (flowContext?.canvasInstanceIds || []);
283
+ if (idsToInject.length > 0 && flowContext?.flowYamlAbs) {
284
+ for (const iid of idsToInject) {
285
+ const excerpt = extractInstanceYamlExcerpt(flowContext.flowYamlAbs, iid);
286
+ if (!excerpt) continue;
287
+ const iInst = iid === sid ? targetInst : (instMap && instMap[iid]);
288
+ const defId = (iInst && iInst.definitionId) || "";
289
+ const headerLabel = sid ? "目标 instance" : "关联 instance(画布选中)";
290
+ parts.push(`## ${headerLabel}(${iid}${defId ? ` · ${defId}` : ""})当前 YAML`);
291
+ parts.push("```yaml");
292
+ parts.push(excerpt.trimEnd());
293
+ parts.push("```");
294
+ parts.push("");
295
+
296
+ // tool_nodejs: inject referenced .mjs script file contents
297
+ if (defId === "tool_nodejs" && flowContext?.pipelineScriptsDirAbs) {
298
+ const scriptFns = extractScriptFilenames(excerpt);
299
+ const scriptBlock = readPipelineScriptContents(flowContext.pipelineScriptsDirAbs, scriptFns, iid);
300
+ if (scriptBlock) {
301
+ parts.push(scriptBlock);
302
+ parts.push("");
303
+ }
304
+ }
305
+ }
306
+ }
307
+ if (isQuery) {
308
+ parts.push(
309
+ "## 上下文已就绪\n" +
310
+ "- 节点 YAML 与脚本内容已附上。请基于上方信息回答用户的问题,**不要修改任何文件**。\n" +
311
+ "- 如需查看整份 flow.yaml 可读取一次。"
312
+ );
313
+ } else {
314
+ parts.push(
315
+ "## 上下文已就绪(禁止 forage)\n" +
316
+ "- 节点定义见上方 schema 表,**禁止** Glob/Read `builtin/nodes/`、`.workspace/agentflow/nodes/`、历史 `runBuild/` 来推断节点结构。\n" +
317
+ "- 目标 instance 的当前 YAML 已附上(若 instanceId 已知);tool_nodejs 节点引用的 .mjs 脚本内容也已附上(若存在)。\n" +
318
+ "- 如需查看整份 flow,仅在确实需要时读取一次。"
319
+ );
320
+ }
321
+ parts.push("");
322
+ } catch {
323
+ /* schema 注入失败不影响主流程 */
324
+ }
325
+
326
+ if (isQuery) {
327
+ parts.push("请基于上方注入的节点 YAML 与脚本内容回答用户的问题。**不要修改任何文件。**");
328
+ } else {
329
+ parts.push(t("composer.task_instruction"));
330
+ }
331
+ return parts.join("\n");
332
+ }
333
+
334
+ /**
335
+ * @param {object} step
336
+ * @param {Record<string, { role: string, model?: string, label: string }>} instMap
337
+ * @param {string} [globalModelKey]
338
+ * @returns {{ nodeRole: string, preferredModel: string }}
339
+ */
340
+ function resolveAgentStepRoleAndModel(step, instMap, globalModelKey) {
341
+ const sid = step.instanceId != null ? String(step.instanceId).trim() : "";
342
+ const fromFlow = sid && instMap[sid];
343
+ const plannerRole = step.nodeRole != null ? String(step.nodeRole).trim() : "";
344
+ const nodeRole = plannerRole || (fromFlow && fromFlow.role) || "";
345
+ let preferredModel = "";
346
+ const execM = step.executorModel != null ? String(step.executorModel).trim() : "";
347
+ if (execM && execM !== "default") preferredModel = execM;
348
+ else if (fromFlow && fromFlow.model && String(fromFlow.model).trim() && String(fromFlow.model).trim() !== "default") {
349
+ preferredModel = String(fromFlow.model).trim();
350
+ } else if (globalModelKey && String(globalModelKey).trim()) {
351
+ preferredModel = String(globalModelKey).trim();
352
+ }
353
+ return { nodeRole, preferredModel };
354
+ }
355
+
356
+ /**
357
+ * @param {object} step
358
+ * @param {number} index
359
+ * @param {Record<string, { role: string, model?: string, label: string }>} instMap
360
+ */
361
+ function summarizePlanStepForUi(step, index, instMap) {
362
+ const sid = step.instanceId != null ? String(step.instanceId).trim() : "";
363
+ const fromFlow = sid && instMap[sid];
364
+ const plannerRole = step.nodeRole != null ? String(step.nodeRole).trim() : "";
365
+ const nodeRole = plannerRole || (fromFlow && fromFlow.role) || "";
366
+ let modelHint = "";
367
+ const execM = step.executorModel != null ? String(step.executorModel).trim() : "";
368
+ if (execM && execM !== "default") modelHint = execM;
369
+ else if (fromFlow && fromFlow.model && String(fromFlow.model).trim() && String(fromFlow.model).trim() !== "default") {
370
+ modelHint = String(fromFlow.model).trim();
371
+ }
372
+ return {
373
+ index,
374
+ type: step.type,
375
+ description: step.description,
376
+ op: step.op,
377
+ instanceId: sid || undefined,
378
+ nodeRole: nodeRole || undefined,
379
+ executorModel: modelHint || undefined,
380
+ complexity: step.complexity,
381
+ };
382
+ }
383
+
384
+ // ─── 编辑后校验与自动修复 ─────────────────────────────────────────────────
385
+
386
+ /**
387
+ * Composer 改动 flow.yaml 之后:运行与 CLI 一致的 validate-flow;若有 errors 则循环调用 agent 修复直至通过或达到上限。
388
+ *
389
+ * @param {object} opts
390
+ * @param {string} opts.uiWorkspaceRoot
391
+ * @param {string} [opts.cliWorkspace]
392
+ * @param {string} opts.flowYamlAbs
393
+ * @param {object} [opts.flowContext] 与多步相同的上下文(含 syncCurlHint、skillsHint 等)
394
+ * @param {string} [opts.modelKey]
395
+ * @param {boolean} [opts.force]
396
+ * @param {(ev: object) => void} [opts.onStreamEvent]
397
+ * @param {() => boolean} [opts.getAborted]
398
+ * @param {(c: import('child_process').ChildProcess | null) => void} [opts.setCurrentChild] 便于外部 abort 杀子进程
399
+ * @param {number} [opts.maxRepairAttempts] 默认 5,最大 10
400
+ * @returns {Promise<{ ok: boolean, result?: object, repairAttempts?: number, aborted?: boolean, repairError?: string }>}
401
+ */
402
+ export async function runComposerPostFlowValidationAndRepair(opts) {
403
+ const emit = typeof opts.onStreamEvent === "function" ? opts.onStreamEvent : () => {};
404
+ const getAborted = typeof opts.getAborted === "function" ? opts.getAborted : () => false;
405
+ const setChild = typeof opts.setCurrentChild === "function" ? opts.setCurrentChild : () => {};
406
+
407
+ const uiRoot = String(opts.uiWorkspaceRoot || "").trim();
408
+ const flowYamlAbs = String(opts.flowYamlAbs || "").trim();
409
+ const cliWs = opts.cliWorkspace ? String(opts.cliWorkspace) : getAgentflowDataRoot();
410
+ const maxRepair = Math.max(1, Math.min(10, Number(opts.maxRepairAttempts) || MAX_COMPOSER_VALIDATION_REPAIR));
411
+
412
+ if (!uiRoot || !flowYamlAbs) {
413
+ return { ok: true, result: { skipped: true } };
414
+ }
415
+
416
+ let last = validateComposerFlowYaml(flowYamlAbs, uiRoot);
417
+ if (last.ok) {
418
+ emit({ type: "status", line: t("composer.validation_passed") });
419
+ emit({ type: "natural", kind: "assistant", text: t("composer.validation_passed_detail") });
420
+ return { ok: true, result: last };
421
+ }
422
+
423
+ emit({ type: "status", line: t("composer.validation_failed") });
424
+ emit({
425
+ type: "natural",
426
+ kind: "assistant",
427
+ text: `⚠ flow 校验未通过(${(last.errors && last.errors.length) || 0} 条错误),将调用 agent 修复…\n${formatValidationErrorsBlock(last)}`,
428
+ });
429
+
430
+ for (let attempt = 1; attempt <= maxRepair; attempt++) {
431
+ if (getAborted()) {
432
+ setChild(null);
433
+ return { ok: false, result: last, aborted: true };
434
+ }
435
+
436
+ const repairStep = {
437
+ type: "agent",
438
+ complexity: "complex",
439
+ description: `自动修复校验错误(第 ${attempt}/${maxRepair} 次)`,
440
+ prompt: [
441
+ t("composer.fix_task_title"),
442
+ "",
443
+ t("composer.fix_errors_intro"),
444
+ "",
445
+ formatValidationErrorsBlock(last),
446
+ "",
447
+ t("composer.fix_constraints_title"),
448
+ t("composer.fix_constraints_body"),
449
+ "- 完成后**保存文件**,并执行上下文中的同步 Web 画布命令(curl)。",
450
+ ].join("\n"),
451
+ };
452
+
453
+ const agentPrompt = buildAgentStepPrompt(repairStep, opts.flowContext);
454
+ const modelKey = opts.modelKey != null ? String(opts.modelKey).trim() : "";
455
+ const routed = routeModel("complex", { userPreferredModel: modelKey || null });
456
+ const { cli, model } = resolveCliAndModel(uiRoot, (routed.model || modelKey) || null, null);
457
+
458
+ emit({ type: "ai-log", tag: "repair-prompt", text: agentPrompt, meta: { attempt, max: maxRepair, cli, model: model || null, errorCount: (last?.errors || []).length } });
459
+ emit({ type: "status", line: t("composer.validation_repair", { attempt, max: maxRepair }) });
460
+ emit({ type: "natural", kind: "assistant", text: t("composer.validation_repair_start", { attempt, max: maxRepair }) });
461
+
462
+ const stepEmit = (ev) => {
463
+ emit({ ...ev, stepIndex: -1, stepTotal: 0, phase: "validation-repair" });
464
+ };
465
+
466
+ try {
467
+ if (cli === "opencode") {
468
+ const handle = runOpenCodeAgentWithPrompt(cliWs, agentPrompt, {
469
+ onStreamEvent: stepEmit,
470
+ model: model || undefined,
471
+ force: Boolean(opts.force),
472
+ });
473
+ setChild(handle.child);
474
+ await handle.finished;
475
+ } else if (cli === "claude-code") {
476
+ const handle = runClaudeCodeAgentWithPrompt(cliWs, agentPrompt, {
477
+ onStreamEvent: stepEmit,
478
+ model: model || undefined,
479
+ force: Boolean(opts.force),
480
+ });
481
+ setChild(handle.child);
482
+ await handle.finished;
483
+ } else {
484
+ const handle = runCursorAgentWithPrompt(cliWs, agentPrompt, {
485
+ onStreamEvent: stepEmit,
486
+ model: model || undefined,
487
+ force: Boolean(opts.force),
488
+ });
489
+ setChild(handle.child);
490
+ await handle.finished;
491
+ }
492
+ } catch (e) {
493
+ setChild(null);
494
+ emit({ type: "natural", kind: "error", text: `校验修复 agent 失败: ${e.message}` });
495
+ return { ok: false, result: last, repairError: e.message, repairAttempts: attempt };
496
+ }
497
+ setChild(null);
498
+
499
+ if (getAborted()) {
500
+ return { ok: false, result: last, aborted: true };
501
+ }
502
+
503
+ last = validateComposerFlowYaml(flowYamlAbs, uiRoot);
504
+ if (last.ok) {
505
+ emit({ type: "status", line: t("composer.validation_passed") + t("composer.validation_repair_auto_success") });
506
+ emit({ type: "natural", kind: "assistant", text: "✓ 校验修复后 flow.yaml 已通过 validate-flow" });
507
+ return { ok: true, result: last, repairAttempts: attempt };
508
+ }
509
+
510
+ emit({
511
+ type: "natural",
512
+ kind: "assistant",
513
+ text: `⚠ 第 ${attempt} 次修复后仍未通过:\n${formatValidationErrorsBlock(last)}`,
514
+ });
515
+ }
516
+
517
+ emit({
518
+ type: "natural",
519
+ kind: "error",
520
+ text: `flow 校验在 ${maxRepair} 次自动修复后仍未通过,请根据上方错误列表手动修改 flow.yaml。`,
521
+ });
522
+ return { ok: false, result: last, repairAttempts: maxRepair };
523
+ }
524
+
525
+ // ─── 多步编排执行 ──────────────────────────────────────────────────────────
526
+
527
+ /**
528
+ * 多步 Composer:规划 → 分步执行 → 流式推送进度。
529
+ * 支持分阶段模式:大任务按「流转规划 → 节点补充 → 流程完善」三阶段逐轮生成,
530
+ * 每阶段完成后 emit phase-complete,由前端决定是否继续下一阶段。
531
+ *
532
+ * @param {object} opts
533
+ * @param {string} opts.uiWorkspaceRoot
534
+ * @param {string} [opts.cliWorkspace]
535
+ * @param {string} opts.userPrompt 用户原始输入
536
+ * @param {string} [opts.fullPrompt] 含上下文的完整 prompt(兼容旧逻辑,若多步不使用此字段)
537
+ * @param {string} [opts.modelKey] 用户选择的模型
538
+ * @param {string} [opts.flowYamlAbs] flow.yaml 绝对路径
539
+ * @param {string} [opts.flowId]
540
+ * @param {string} [opts.flowSource]
541
+ * @param {string[]} [opts.instanceIds]
542
+ * @param {object} [opts.flowContext] { skillsHint, syncCurlHint } 等上下文
543
+ * @param {Array<{ role: string, text: string }>} [opts.thread] 对话历史
544
+ * @param {object} [opts.phaseContext] 分阶段上下文 { phaseIndex, phases, userPromptOriginal }
545
+ * @param {string} [opts.phaseRole] 用户为本阶段指定的默认节点角色
546
+ * @param {boolean} [opts.force]
547
+ * @param {(ev: object) => void} [opts.onStreamEvent]
548
+ * @returns {{ finished: Promise<void>, abort: () => void }}
549
+ */
550
+ export function startComposerMultiStep(opts) {
551
+ const uiRoot = opts.uiWorkspaceRoot && String(opts.uiWorkspaceRoot).trim();
552
+ if (!uiRoot) throw new Error("Missing uiWorkspaceRoot");
553
+
554
+ const emit = typeof opts.onStreamEvent === "function" ? opts.onStreamEvent : () => {};
555
+ let aborted = false;
556
+ let currentChild = null;
557
+
558
+ const abort = () => {
559
+ aborted = true;
560
+ if (currentChild && !currentChild.killed) {
561
+ try { currentChild.kill("SIGTERM"); } catch { /* ignore */ }
562
+ }
563
+ };
564
+
565
+ const finished = (async () => {
566
+ try {
567
+ // ── 1. 规划 ──────────────────────────────────────────────────────
568
+ emit({ type: "status", line: t("composer.analyzing_task") });
569
+
570
+ let flowYaml = "";
571
+ if (opts.flowYamlAbs) {
572
+ try { flowYaml = fs.readFileSync(opts.flowYamlAbs, "utf-8"); } catch { /* ignore */ }
573
+ }
574
+ const instMap = parseInstanceRoleModelMap(flowYaml);
575
+
576
+ if (!opts.flowContext) opts.flowContext = {};
577
+ opts.flowContext._instanceMap = instMap;
578
+
579
+ const planResult = await planComposerTasks({
580
+ userPrompt: opts.userPrompt,
581
+ flowYaml,
582
+ flowYamlAbs: opts.flowYamlAbs,
583
+ instanceIds: opts.instanceIds,
584
+ thread: opts.thread,
585
+ intents: opts.flowContext?.intents,
586
+ phaseContext: opts.phaseContext,
587
+ phaseRole: opts.phaseRole,
588
+ onEvent: emit,
589
+ });
590
+
591
+ if (aborted) return;
592
+
593
+ const steps = planResult.steps;
594
+ const isPhased = Boolean(planResult.phased);
595
+ const phases = planResult.phases || PHASED_DEFINITIONS;
596
+ const currentPhase = planResult.currentPhase ?? 0;
597
+
598
+ if (isPhased) {
599
+ const phaseDef = phases[currentPhase];
600
+ emit({
601
+ type: "phase-plan",
602
+ phases: phases.map((p, i) => ({
603
+ ...p,
604
+ status: i < currentPhase ? "done" : i === currentPhase ? "running" : "pending",
605
+ })),
606
+ currentPhase,
607
+ phaseTotal: phases.length,
608
+ phaseName: phaseDef?.label || `阶段 ${currentPhase + 1}`,
609
+ });
610
+ }
611
+
612
+ const totalSteps = steps.length;
613
+ emit({
614
+ type: "plan",
615
+ steps: steps.map((s, i) => summarizePlanStepForUi(s, i, instMap)),
616
+ total: totalSteps,
617
+ });
618
+
619
+ // ── 1.5 多步前置:脚本预生成 flow.yaml 与 spec.md skeleton ──────
620
+ // 让 AI 只做"插入节点 + 填充 section",省下重复 YAML 与样板模板字符串。
621
+ // 触发条件(任一):
622
+ // - 分阶段模式的第 0 阶段(流转规划),始终尝试
623
+ // - 非分阶段多步:只要 flow.yaml 还空,也尝试(用户 prompt 没命中 phased
624
+ // 正则,但实质是「新建」场景)
625
+ // skeleton 内部幂等:已有 instances 或 spec.md 已存在则自动跳过。
626
+ const shouldTrySkeleton = opts.flowYamlAbs && (
627
+ (isPhased && currentPhase === 0) ||
628
+ (!isPhased)
629
+ );
630
+ if (shouldTrySkeleton) {
631
+ // composerSpecAbs 兜底:flowContext 没传时按 flow.yaml 同目录推
632
+ let specAbs = opts.flowContext?.composerSpecAbs || "";
633
+ if (!specAbs && opts.flowYamlAbs) {
634
+ specAbs = path.join(path.dirname(opts.flowYamlAbs), "composer-node-spec.md");
635
+ }
636
+ try {
637
+ const skel = ensurePhase1Skeletons({
638
+ flowYamlAbs: opts.flowYamlAbs,
639
+ composerSpecAbs: specAbs,
640
+ flowId: opts.flowId,
641
+ userRequest: opts.phaseContext?.userPromptOriginal || opts.userPrompt,
642
+ });
643
+ if (skel.flow.created) {
644
+ emit({ type: "natural", kind: "assistant", text: `✓ 预生成 flow.yaml skeleton (start + end + 主链 edge)` });
645
+ }
646
+ if (skel.spec.created) {
647
+ emit({ type: "natural", kind: "assistant", text: `✓ 预生成 composer-node-spec.md 模板:${specAbs}` });
648
+ }
649
+ } catch (e) {
650
+ emit({ type: "natural", kind: "error", text: `skeleton 预生成失败: ${e.message}` });
651
+ }
652
+ }
653
+
654
+ // ── 2. 执行步骤 ────────────────────────────────────────────────
655
+ let stepIndex = 0;
656
+ while (stepIndex < steps.length && !aborted) {
657
+ const step = steps[stepIndex];
658
+
659
+ if (step.type === "script" && isSupportedScriptOp(step.op)) {
660
+ const scriptBatch = [step];
661
+ let nextIdx = stepIndex + 1;
662
+ while (nextIdx < steps.length && steps[nextIdx].type === "script" && isSupportedScriptOp(steps[nextIdx].op)) {
663
+ scriptBatch.push(steps[nextIdx]);
664
+ nextIdx++;
665
+ }
666
+
667
+ const scriptFirstId = scriptBatch[0]?.params?.instanceId;
668
+ emit({
669
+ type: "step-start",
670
+ index: stepIndex,
671
+ total: totalSteps,
672
+ stepType: "script",
673
+ description: `执行 ${scriptBatch.length} 个脚本操作`,
674
+ count: scriptBatch.length,
675
+ instanceId: scriptFirstId != null ? String(scriptFirstId) : undefined,
676
+ });
677
+
678
+ for (let i = 0; i < scriptBatch.length; i++) {
679
+ const s = scriptBatch[i];
680
+ if (aborted) break;
681
+ const globalIdx = stepIndex + i;
682
+ emit({ type: "step-progress", index: globalIdx, total: totalSteps, description: s.description || s.op });
683
+
684
+ if (!opts.flowYamlAbs) {
685
+ emit({ type: "natural", kind: "error", text: `脚本操作需要 flow.yaml 路径,但未提供` });
686
+ continue;
687
+ }
688
+
689
+ const result = executeScriptOp(opts.flowYamlAbs, s);
690
+ if (result.success) {
691
+ emit({ type: "natural", kind: "assistant", text: `✓ ${result.message}` });
692
+ } else {
693
+ emit({ type: "natural", kind: "error", text: `✗ ${s.op}: ${result.message}` });
694
+ }
695
+ emit({ type: "step-done", index: globalIdx, total: totalSteps, success: result.success });
696
+ }
697
+
698
+ stepIndex = nextIdx;
699
+ continue;
700
+ }
701
+
702
+ if (step.type === "agent") {
703
+ const complexity = step.complexity || "medium";
704
+ const { nodeRole, preferredModel } = resolveAgentStepRoleAndModel(step, instMap, opts.modelKey);
705
+ const routed = routeModel(complexity, { userPreferredModel: preferredModel || null });
706
+ const sid = step.instanceId != null ? String(step.instanceId).trim() : "";
707
+ const fromFlow = sid && instMap[sid];
708
+
709
+ emit({
710
+ type: "step-start",
711
+ index: stepIndex,
712
+ total: totalSteps,
713
+ stepType: "agent",
714
+ description: step.description,
715
+ model: routed.model,
716
+ tier: routed.tier,
717
+ complexity,
718
+ nodeRole: nodeRole || undefined,
719
+ instanceId: sid || undefined,
720
+ instanceLabel: fromFlow?.label,
721
+ });
722
+
723
+ const stepForPrompt = nodeRole ? { ...step, nodeRole } : { ...step };
724
+ const agentPrompt = buildAgentStepPrompt(stepForPrompt, opts.flowContext);
725
+ const cliWs = opts.cliWorkspace ? String(opts.cliWorkspace) : getAgentflowDataRoot();
726
+ const modelKey = routed.model || preferredModel || opts.modelKey || "";
727
+ const { cli, model } = resolveCliAndModel(uiRoot, modelKey || null, null);
728
+
729
+ emit({ type: "ai-log", tag: "agent-step-prompt", text: agentPrompt, meta: { stepIndex, total: totalSteps, instanceId: sid || null, nodeRole: nodeRole || null, complexity, cli, model: model || null, description: step.description } });
730
+
731
+ const stepEmit = (ev) => {
732
+ emit({ ...ev, stepIndex, stepTotal: totalSteps });
733
+ };
734
+
735
+ try {
736
+ if (cli === "opencode") {
737
+ const handle = runOpenCodeAgentWithPrompt(cliWs, agentPrompt, {
738
+ onStreamEvent: stepEmit,
739
+ model: model || undefined,
740
+ force: Boolean(opts.force),
741
+ });
742
+ currentChild = handle.child;
743
+ await handle.finished;
744
+ } else if (cli === "claude-code") {
745
+ const handle = runClaudeCodeAgentWithPrompt(cliWs, agentPrompt, {
746
+ onStreamEvent: stepEmit,
747
+ model: model || undefined,
748
+ force: Boolean(opts.force),
749
+ });
750
+ currentChild = handle.child;
751
+ await handle.finished;
752
+ } else {
753
+ const handle = runCursorAgentWithPrompt(cliWs, agentPrompt, {
754
+ onStreamEvent: stepEmit,
755
+ model: model || undefined,
756
+ force: Boolean(opts.force),
757
+ });
758
+ currentChild = handle.child;
759
+ await handle.finished;
760
+ }
761
+ currentChild = null;
762
+ emit({ type: "step-done", index: stepIndex, total: totalSteps, success: true });
763
+ } catch (e) {
764
+ currentChild = null;
765
+ emit({ type: "step-done", index: stepIndex, total: totalSteps, success: false, error: e.message });
766
+ emit({ type: "natural", kind: "error", text: `步骤 ${stepIndex + 1} 失败: ${e.message}` });
767
+ }
768
+
769
+ stepIndex++;
770
+ continue;
771
+ }
772
+
773
+ emit({ type: "step-done", index: stepIndex, total: totalSteps, success: false, error: `未知步骤类型: ${step.type}` });
774
+ stepIndex++;
775
+ }
776
+
777
+ // ── 2.5 阶段一/非分阶段后置:把 spec.md 计划数据槽合并到 flow.yaml ──
778
+ // 幂等:同 name 槽位跳过;非 ★ 节点跳过。AI 已在 yaml 写过的不动。
779
+ const shouldApplyPlannedSlots = !aborted && opts.flowYamlAbs && (
780
+ (isPhased && currentPhase === 0) ||
781
+ (!isPhased)
782
+ );
783
+ if (shouldApplyPlannedSlots) {
784
+ let specAbs = opts.flowContext?.composerSpecAbs || "";
785
+ if (!specAbs && opts.flowYamlAbs) {
786
+ specAbs = path.join(path.dirname(opts.flowYamlAbs), "composer-node-spec.md");
787
+ }
788
+ if (specAbs && fs.existsSync(specAbs)) {
789
+ try {
790
+ const r = applyPlannedSlotsFromSpec(opts.flowYamlAbs, specAbs);
791
+ if (r.ok && r.applied.length > 0) {
792
+ const summary = r.applied.map((a) => {
793
+ const parts = [];
794
+ if (a.addedInputs.length) parts.push(`input += [${a.addedInputs.join(", ")}]`);
795
+ if (a.addedOutputs.length) parts.push(`output += [${a.addedOutputs.join(", ")}]`);
796
+ return ` ${a.instanceId}: ${parts.join(" / ")}`;
797
+ }).join("\n");
798
+ emit({
799
+ type: "natural",
800
+ kind: "assistant",
801
+ text: `✓ 脚本合并 spec.md 计划数据槽到 flow.yaml(已存在的跳过):\n${summary}`,
802
+ });
803
+ } else if (r.ok && r.applied.length === 0) {
804
+ emit({ type: "status", line: "spec.md 计划数据槽已全部落到 flow.yaml" });
805
+ } else if (!r.ok) {
806
+ emit({ type: "natural", kind: "error", text: `合并计划数据槽失败:${r.error}` });
807
+ }
808
+ } catch (e) {
809
+ emit({ type: "natural", kind: "error", text: `合并计划数据槽异常:${e.message}` });
810
+ }
811
+ }
812
+ }
813
+
814
+ // ── 3. 校验(分阶段仅在最后阶段统一校验;非分阶段每次都校验) ──
815
+ const shouldRunValidation = !isPhased || currentPhase >= phases.length - 1;
816
+ if (!aborted && opts.flowYamlAbs && shouldRunValidation) {
817
+ await runComposerPostFlowValidationAndRepair({
818
+ uiWorkspaceRoot: uiRoot,
819
+ cliWorkspace: opts.cliWorkspace ? String(opts.cliWorkspace) : getAgentflowDataRoot(),
820
+ flowYamlAbs: opts.flowYamlAbs,
821
+ flowContext: opts.flowContext,
822
+ modelKey: opts.modelKey,
823
+ force: Boolean(opts.force),
824
+ onStreamEvent: emit,
825
+ getAborted: () => aborted,
826
+ setCurrentChild: (c) => {
827
+ currentChild = c;
828
+ },
829
+ });
830
+ } else if (!aborted && opts.flowYamlAbs && isPhased) {
831
+ emit({ type: "status", line: t("composer.skip_validation_for_phase") });
832
+ }
833
+
834
+ // ── 4. 分阶段完成通知 ──────────────────────────────────────────
835
+ if (!aborted && isPhased) {
836
+ const isLastPhase = currentPhase >= phases.length - 1;
837
+ const phaseDef = phases[currentPhase];
838
+ const nextPhaseDef = !isLastPhase ? phases[currentPhase + 1] : null;
839
+ const userPromptOriginal = opts.phaseContext?.userPromptOriginal || opts.userPrompt;
840
+
841
+ emit({
842
+ type: "phase-complete",
843
+ phaseIndex: currentPhase,
844
+ phaseTotal: phases.length,
845
+ phaseName: phaseDef?.label || t("composer.current_phase", { index: currentPhase + 1 }),
846
+ nextPhase: nextPhaseDef ? { index: currentPhase + 1, name: nextPhaseDef.name, label: nextPhaseDef.label } : null,
847
+ isLastPhase,
848
+ phases: phases.map((p, i) => ({
849
+ ...p,
850
+ status: i <= currentPhase ? "done" : "pending",
851
+ })),
852
+ userPromptOriginal,
853
+ });
854
+
855
+ if (isLastPhase) {
856
+ emit({ type: "status", line: t("composer.all_phases_complete") });
857
+ } else {
858
+ emit({ type: "status", line: t("composer.phase_complete_waiting", { label: phaseDef?.label || t("composer.current_phase", { index: currentPhase + 1 }) }) });
859
+ }
860
+ } else if (!aborted) {
861
+ emit({ type: "status", line: t("composer.all_steps_complete") });
862
+ }
863
+ } catch (e) {
864
+ emit({ type: "natural", kind: "error", text: t("composer.multi_step_failed", { message: e.message }) });
865
+ throw e;
866
+ }
867
+ })();
868
+
869
+ return { finished, abort };
870
+ }
871
+
872
+ /**
873
+ * 根据任务复杂度判断是否走多步模式(async:用 Cursor/OpenCode CLI AI 判断,降级正则)。
874
+ * - "multi":新建流程、重构、复杂多节点操作 → 多步
875
+ * - "single":改标签、连一条边、小改动 → 单步
876
+ */
877
+ export async function shouldUseMultiStep(opts) {
878
+ if (!opts.flowYamlAbs) return false;
879
+ const prompt = (opts.userPrompt || "").trim();
880
+ if (!prompt) return false;
881
+ const cliWs = opts.cliWorkspace || (opts.flowYamlAbs ? path.dirname(opts.flowYamlAbs) : undefined);
882
+ const result = await classifyTaskComplexity(prompt, cliWs);
883
+ return result === "multi";
884
+ }