@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,513 @@
1
+ import { spawnSync } from "child_process";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { outputNodeBasename, outputDirForNode } from "../pipeline/get-exec-id.mjs";
5
+ import { writeResult } from "../pipeline/write-result.mjs";
6
+ import {
7
+ runClaudeCodeAgentForNode,
8
+ runClaudeCodeAgentWithPrompt,
9
+ runCursorAgentForNode,
10
+ runCursorAgentWithPrompt,
11
+ runOpenCodeAgentForNode,
12
+ runOpenCodeAgentWithPrompt,
13
+ } from "./agent-runners.mjs";
14
+ import { runApiAgentForNode } from "./api-runner.mjs";
15
+ import { log } from "./log.mjs";
16
+ import { LOCAL_ONLY_DEFINITION_IDS, LOCAL_ONLY_TERMINAL_SUCCESS_IDS } from "./paths.mjs";
17
+ import { resolveCliAndModel } from "./model-config.mjs";
18
+ import { parseJsonStdout, runNodeScript } from "./pipeline-scripts.mjs";
19
+ import { emitEvent } from "./run-events.mjs";
20
+ import { getRunDir } from "./workspace.mjs";
21
+ import { nodeToolCommandToArgv } from "./normalize-node-tool-command.mjs";
22
+ import { buildPipelineScriptPathHint } from "./flow-normalize.mjs";
23
+ import { validateAndParse } from "../pipeline/validate-script-output.mjs";
24
+
25
+ const TOOL_NODEJS_MAX_RETRIES = 3;
26
+ const TOOL_NODEJS_RETRY_DELAY_MS = 1000;
27
+ const AI_HEAL_ENABLED = !(
28
+ process.env.AGENTFLOW_TOOL_NODEJS_AI_HEAL === "0" ||
29
+ process.env.AGENTFLOW_TOOL_NODEJS_AI_HEAL === "false"
30
+ );
31
+
32
+ // ─── AI 自愈:失败后调用 Cursor/OpenCode CLI 修复脚本再重试 ──────────────────
33
+
34
+ /**
35
+ * @returns {{ path: string | null, parsed: string | null, reason: "none" | "unparsable" | "not-found" }}
36
+ */
37
+ function extractScriptPath(resolvedScript) {
38
+ const { argv } = nodeToolCommandToArgv(resolvedScript);
39
+ if (argv.length === 0) return { path: null, parsed: null, reason: "unparsable" };
40
+ const candidate = argv[0];
41
+ if (!candidate) return { path: null, parsed: null, reason: "unparsable" };
42
+ const abs = path.isAbsolute(candidate) ? candidate : path.resolve(candidate);
43
+ if (fs.existsSync(abs)) return { path: abs, parsed: abs, reason: "none" };
44
+ return { path: null, parsed: abs, reason: "not-found" };
45
+ }
46
+
47
+
48
+ function buildHealPrompt(scriptPath, command, errorInfo, scriptContent) {
49
+ const stderrSlice = (errorInfo.stderr || "").trim().slice(0, 4000) || "(无)";
50
+ const stdoutSlice = (errorInfo.stdout || "").trim().slice(0, 2000) || "(无)";
51
+ return [
52
+ "你是脚本调试助手。以下 Node.js 脚本执行失败,请直接修复脚本文件中的错误。",
53
+ "",
54
+ `## 脚本路径\n${scriptPath}`,
55
+ `## 执行命令\n${command}`,
56
+ `## 退出码\n${errorInfo.exitCode}`,
57
+ `## 标准错误 (stderr)\n${stderrSlice}`,
58
+ `## 标准输出 (stdout)\n${stdoutSlice}`,
59
+ "## 当前脚本内容",
60
+ "```javascript",
61
+ scriptContent.slice(0, 30000),
62
+ "```",
63
+ "",
64
+ "## 修复要求",
65
+ `1. 直接编辑脚本文件 \`${scriptPath}\` 修复错误`,
66
+ "2. 保持脚本的输入输出格式不变(stdout:JSON `{\"err_code\":0,\"message\":{...}}` 中 message 的键对应各输出槽位,或纯文本→写入 result)",
67
+ "3. 只修复导致失败的问题,不做无关改动",
68
+ "4. 不要创建新文件、不要修改其他文件",
69
+ ].join("\n");
70
+ }
71
+
72
+ /**
73
+ * 调用 Cursor/OpenCode CLI 让 AI 分析 stderr 并修复脚本文件,然后由调用方重新执行。
74
+ * @returns {Promise<boolean>} 是否成功完成修复(AI 调用无报错即视为成功,实际修复效果由重试验证)
75
+ */
76
+ async function healToolNodejsWithAI(workspaceRoot, flowName, uuid, instanceId, resolvedScript, errorInfo, cli, model) {
77
+ const ext = extractScriptPath(resolvedScript);
78
+ if (!ext.path) {
79
+ if (ext.reason === "not-found") {
80
+ log.warn(
81
+ `[tool_nodejs AI 自愈] 脚本文件不存在,跳过(这是 flow.yaml 模板路径错,非脚本内容错):` +
82
+ `${ext.parsed}。建议把 flow.yaml 里 tool_nodejs.script 的 ` +
83
+ `\`\${workspaceRoot}/.workspace/agentflow/pipelines/\${flowName}/scripts/...\` ` +
84
+ `改为 \`\${flowDir}/scripts/xxx.mjs\`(兼容 user/workspace/builtin 安装)`,
85
+ );
86
+ } else {
87
+ log.warn(`[tool_nodejs AI 自愈] 无法从命令提取脚本路径,跳过: ${resolvedScript.slice(0, 120)}`);
88
+ }
89
+ return false;
90
+ }
91
+ const scriptPath = ext.path;
92
+
93
+ let scriptContent;
94
+ try {
95
+ scriptContent = fs.readFileSync(scriptPath, "utf-8");
96
+ } catch {
97
+ log.warn(`[tool_nodejs AI 自愈] 无法读取脚本文件,跳过: ${scriptPath}`);
98
+ return false;
99
+ }
100
+
101
+ const prompt = buildHealPrompt(scriptPath, resolvedScript, errorInfo, scriptContent);
102
+ const healCli =
103
+ cli === "opencode" ? "opencode" : cli === "claude-code" ? "claude-code" : "cursor";
104
+
105
+ log.info(`[tool_nodejs AI 自愈] ${instanceId} 调用 ${healCli}${model ? ` (${model})` : ""} 修复 ${path.basename(scriptPath)}`);
106
+ emitEvent(workspaceRoot, flowName, uuid, {
107
+ event: "tool-nodejs-ai-heal-start",
108
+ instanceId,
109
+ scriptPath,
110
+ cli: healCli,
111
+ model: model ?? null,
112
+ });
113
+
114
+ try {
115
+ if (healCli === "opencode") {
116
+ const { finished } = runOpenCodeAgentWithPrompt(workspaceRoot, prompt, { model: model || undefined, force: true });
117
+ await finished;
118
+ } else if (healCli === "claude-code") {
119
+ const { finished } = runClaudeCodeAgentWithPrompt(workspaceRoot, prompt, { model: model || undefined });
120
+ await finished;
121
+ } else {
122
+ const { finished } = runCursorAgentWithPrompt(workspaceRoot, prompt, { model: model || undefined });
123
+ await finished;
124
+ }
125
+ log.info(`[tool_nodejs AI 自愈] ${instanceId} AI 修复完成,即将重试脚本`);
126
+ emitEvent(workspaceRoot, flowName, uuid, {
127
+ event: "tool-nodejs-ai-heal-done",
128
+ instanceId,
129
+ scriptPath,
130
+ });
131
+ return true;
132
+ } catch (healErr) {
133
+ log.warn(`[tool_nodejs AI 自愈] ${instanceId} AI 修复失败: ${healErr.message?.slice(0, 200) || healErr}`);
134
+ emitEvent(workspaceRoot, flowName, uuid, {
135
+ event: "tool-nodejs-ai-heal-failed",
136
+ instanceId,
137
+ scriptPath,
138
+ error: healErr.message?.slice(0, 300) || String(healErr),
139
+ });
140
+ return false;
141
+ }
142
+ }
143
+
144
+ // ─── 内联执行 + 重试 ────────────────────────────────────────────────────────
145
+
146
+ /**
147
+ * tool_nodejs + script 内联执行:直接跑脚本、写 result,无中间进程。
148
+ * 失败时可通过 AI(Cursor/OpenCode CLI)自动分析 stderr 修复脚本再重试,
149
+ * 最多重试 TOOL_NODEJS_MAX_RETRIES 次。
150
+ * 设置 AGENTFLOW_TOOL_NODEJS_AI_HEAL=0 可关闭 AI 自愈回退为简单重试。
151
+ *
152
+ * 协议:与 run-tool-nodejs.mjs 一致——validate-script-output 解析 stdout;
153
+ * JSON 时 message 的每个键写入同名 output 槽位(含 result);纯文本则等价于 message.result。
154
+ */
155
+ async function executeToolNodejsInline(workspaceRoot, flowName, uuid, instanceId, resolvedScript, execId, healOptions) {
156
+ let lastError;
157
+ for (let attempt = 1; attempt <= TOOL_NODEJS_MAX_RETRIES + 1; attempt++) {
158
+ try {
159
+ executeToolNodejsOnce(workspaceRoot, flowName, uuid, instanceId, resolvedScript, execId);
160
+ return;
161
+ } catch (err) {
162
+ lastError = err;
163
+ if (attempt <= TOOL_NODEJS_MAX_RETRIES) {
164
+ const tag = `[tool_nodejs 自愈] ${instanceId} 第 ${attempt}/${TOOL_NODEJS_MAX_RETRIES} 次重试`;
165
+ log.warn(`${tag}:${err.message?.slice(0, 200) || err}`);
166
+ emitEvent(workspaceRoot, flowName, uuid, {
167
+ event: "tool-nodejs-retry",
168
+ instanceId,
169
+ attempt,
170
+ maxRetries: TOOL_NODEJS_MAX_RETRIES,
171
+ error: err.message?.slice(0, 300) || String(err),
172
+ });
173
+
174
+ if (AI_HEAL_ENABLED && healOptions) {
175
+ const errorInfo = {
176
+ stdout: err.scriptStdout || "",
177
+ stderr: err.scriptStderr || "",
178
+ exitCode: err.scriptExitCode ?? 1,
179
+ };
180
+ await healToolNodejsWithAI(
181
+ workspaceRoot, flowName, uuid, instanceId, resolvedScript,
182
+ errorInfo, healOptions.cli, healOptions.model,
183
+ );
184
+ }
185
+
186
+ if (TOOL_NODEJS_RETRY_DELAY_MS > 0) {
187
+ spawnSync("sleep", [String(TOOL_NODEJS_RETRY_DELAY_MS / 1000)], { stdio: "ignore" });
188
+ }
189
+ }
190
+ }
191
+ }
192
+ throw lastError;
193
+ }
194
+
195
+ function persistToolNodejsStderr(outputDir, instanceId, execId, stderr) {
196
+ if (!stderr) return;
197
+ try {
198
+ fs.mkdirSync(outputDir, { recursive: true });
199
+ fs.writeFileSync(path.join(outputDir, outputNodeBasename(instanceId, execId, "stderr")), stderr, "utf-8");
200
+ } catch (_) {}
201
+ }
202
+
203
+ function executeToolNodejsOnce(workspaceRoot, flowName, uuid, instanceId, resolvedScript, execId) {
204
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
205
+ const outputDir = path.join(runDir, outputDirForNode(instanceId));
206
+
207
+ const { argv, commandLine: normalized } = nodeToolCommandToArgv(resolvedScript);
208
+ let child;
209
+ if (/^node\s/i.test(String(normalized).trim()) && argv.length >= 1) {
210
+ child = spawnSync(process.execPath, argv, {
211
+ cwd: workspaceRoot,
212
+ shell: false,
213
+ stdio: ["inherit", "pipe", "pipe"],
214
+ });
215
+ } else {
216
+ child = spawnSync(normalized, [], {
217
+ cwd: workspaceRoot,
218
+ shell: true,
219
+ stdio: ["inherit", "pipe", "pipe"],
220
+ });
221
+ }
222
+
223
+ const stdout = child.stdout?.toString("utf-8") ?? "";
224
+ const stderr = child.stderr?.toString("utf-8") ?? "";
225
+ const exitCode = child.status ?? 1;
226
+
227
+ fs.mkdirSync(outputDir, { recursive: true });
228
+
229
+ // 直接写文件模式:stdout 为空 + exit 0 → 脚本已自行写入 output 文件,无需解析
230
+ if (!stdout.trim() && exitCode === 0) {
231
+ persistToolNodejsStderr(outputDir, instanceId, execId, stderr);
232
+ writeResult(workspaceRoot, flowName, uuid, instanceId,
233
+ { status: "success", message: "执行完成" },
234
+ { preserveBody: true, execId });
235
+ return;
236
+ }
237
+
238
+ // 非零退出码 + 无 stdout → 直接失败
239
+ if (!stdout.trim() && exitCode !== 0) {
240
+ const baseDetail = `脚本退出码 ${exitCode}` + (stderr.trim() ? `:${stderr.trim().slice(0, 200)}` : "");
241
+ const detail = baseDetail + buildPipelineScriptPathHint(stderr);
242
+ persistToolNodejsStderr(outputDir, instanceId, execId, stderr);
243
+ writeResult(workspaceRoot, flowName, uuid, instanceId,
244
+ { status: "failed", message: detail },
245
+ { preserveBody: true, execId });
246
+ const err = new Error(`Script failed: ${detail}`);
247
+ err.scriptStdout = stdout;
248
+ err.scriptStderr = stderr;
249
+ err.scriptExitCode = exitCode;
250
+ throw err;
251
+ }
252
+
253
+ // JSON stdout 模式(兼容旧脚本)
254
+ const { ok, errors, payload } = validateAndParse(stdout);
255
+
256
+ if (!ok) {
257
+ const baseDetail =
258
+ exitCode !== 0
259
+ ? `脚本退出码 ${exitCode}` + (stderr.trim() ? `:${stderr.trim().slice(0, 200)}` : "")
260
+ : errors.length
261
+ ? errors.join("; ")
262
+ : "脚本无输出";
263
+ const detail = baseDetail + buildPipelineScriptPathHint(stderr);
264
+ persistToolNodejsStderr(outputDir, instanceId, execId, stderr);
265
+ writeResult(workspaceRoot, flowName, uuid, instanceId,
266
+ { status: "failed", message: detail },
267
+ { preserveBody: true, execId });
268
+ const err = new Error(`Script failed: ${detail}`);
269
+ err.scriptStdout = stdout;
270
+ err.scriptStderr = stderr;
271
+ err.scriptExitCode = exitCode;
272
+ throw err;
273
+ }
274
+
275
+ const isSynthetic = Boolean(payload._synthetic);
276
+ const success = isSynthetic ? exitCode === 0 : payload.err_code === 0;
277
+ const message = payload.message;
278
+ // 备份由 snapshotPriorRoundIfNeeded 在 pre-process 入口统一处理,此处只写新 output。
279
+ for (const slot of Object.keys(message)) {
280
+ if (slot === "_synthetic") continue;
281
+ const content = message[slot];
282
+ if (content == null) continue;
283
+ fs.writeFileSync(
284
+ path.join(outputDir, outputNodeBasename(instanceId, execId, slot)),
285
+ String(content),
286
+ "utf-8",
287
+ );
288
+ }
289
+ persistToolNodejsStderr(outputDir, instanceId, execId, stderr);
290
+
291
+ writeResult(workspaceRoot, flowName, uuid, instanceId,
292
+ { status: success ? "success" : "failed",
293
+ message: success ? "执行完成" : "执行未通过" },
294
+ { preserveBody: true, execId });
295
+
296
+ if (!success) {
297
+ const hint = stderr.trim() ? ` ${stderr.trim().slice(0, 280)}` : "";
298
+ const err = new Error(`Script failed (exit ${exitCode}): ${String(normalized).slice(0, 120)}${hint}`);
299
+ err.scriptStdout = stdout;
300
+ err.scriptStderr = stderr;
301
+ err.scriptExitCode = exitCode;
302
+ throw err;
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Execute one node: 按 definitionId / resolvedScript / directCommand 决定执行方式。
308
+ */
309
+ export async function executeNode(workspaceRoot, flowName, uuid, instanceId, preOutput, options = {}) {
310
+ const { definitionId, directCommand, resolvedScript, promptPath, nodeContext, taskBody, resultPath, subagent } = preOutput;
311
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
312
+ const intermediatePath = runDir;
313
+
314
+ if (definitionId && LOCAL_ONLY_DEFINITION_IDS.has(definitionId)) {
315
+ return;
316
+ }
317
+
318
+ if (resolvedScript) {
319
+ const execId = preOutput.execId ?? 1;
320
+ let healOptions = null;
321
+ if (AI_HEAL_ENABLED) {
322
+ const { cli, model } = resolveCliAndModel(workspaceRoot, preOutput.model ?? null, options.model ?? null);
323
+ healOptions = {
324
+ cli: cli === "api" ? "cursor" : cli,
325
+ model,
326
+ };
327
+ }
328
+ emitEvent(workspaceRoot, flowName, uuid, {
329
+ event: "direct-command-start",
330
+ instanceId,
331
+ directCommand: resolvedScript,
332
+ });
333
+ try {
334
+ await executeToolNodejsInline(workspaceRoot, flowName, uuid, instanceId, resolvedScript, execId, healOptions);
335
+ emitEvent(workspaceRoot, flowName, uuid, {
336
+ event: "direct-command-done",
337
+ instanceId,
338
+ directCommand: resolvedScript,
339
+ });
340
+ } catch (err) {
341
+ emitEvent(workspaceRoot, flowName, uuid, {
342
+ event: "direct-command-failed",
343
+ instanceId,
344
+ directCommand: resolvedScript,
345
+ error: err.message,
346
+ });
347
+ throw err;
348
+ }
349
+ return;
350
+ }
351
+
352
+ if (directCommand) {
353
+ emitEvent(workspaceRoot, flowName, uuid, {
354
+ event: "direct-command-start",
355
+ instanceId,
356
+ directCommand,
357
+ });
358
+ try {
359
+ const result = spawnSync(directCommand, [], { cwd: workspaceRoot, shell: true, stdio: "inherit" });
360
+ if (result.status !== 0) {
361
+ emitEvent(workspaceRoot, flowName, uuid, {
362
+ event: "direct-command-failed",
363
+ instanceId,
364
+ directCommand,
365
+ exitCode: result.status,
366
+ });
367
+ throw new Error(`Direct command failed: ${directCommand}`);
368
+ }
369
+ emitEvent(workspaceRoot, flowName, uuid, {
370
+ event: "direct-command-done",
371
+ instanceId,
372
+ directCommand,
373
+ });
374
+ } catch (err) {
375
+ if (err.message && !err.message.includes("Direct command failed")) {
376
+ emitEvent(workspaceRoot, flowName, uuid, {
377
+ event: "direct-command-failed",
378
+ instanceId,
379
+ directCommand,
380
+ error: err.message,
381
+ });
382
+ }
383
+ throw err;
384
+ }
385
+ return;
386
+ }
387
+
388
+ const { cli, model } = resolveCliAndModel(workspaceRoot, preOutput.model ?? null, options.model ?? null);
389
+
390
+ const execId = preOutput.execId ?? 1;
391
+ // 备份由 snapshotPriorRoundIfNeeded 在 pre-process 入口统一处理,此处直接进入 agent 执行。
392
+
393
+ emitEvent(workspaceRoot, flowName, uuid, {
394
+ event: "agent-invoke-start",
395
+ instanceId,
396
+ subagent: subagent ?? null,
397
+ promptPath: promptPath ?? null,
398
+ resultPathRel: resultPath ?? null,
399
+ modelCli: cli,
400
+ model: model ?? null,
401
+ });
402
+ try {
403
+ if (cli === "api") {
404
+ await runApiAgentForNode(
405
+ workspaceRoot,
406
+ { promptPath, nodeContext: nodeContext ?? "", taskBody: taskBody ?? "", subagent, instanceId },
407
+ {
408
+ model,
409
+ onToolCall: options.onToolCall,
410
+ flowName,
411
+ uuid,
412
+ },
413
+ );
414
+ } else if (cli === "opencode") {
415
+ await runOpenCodeAgentForNode(
416
+ workspaceRoot,
417
+ { promptPath, nodeContext: nodeContext ?? "", taskBody: taskBody ?? "", intermediatePath, resultPathRel: resultPath, subagent, instanceId },
418
+ {
419
+ model,
420
+ stderrBuffer: options.stderrBuffer,
421
+ force: options.force,
422
+ outputPrefix: options.outputPrefix,
423
+ prefixColor: options.prefixColor,
424
+ onToolCall: options.onToolCall,
425
+ flowName,
426
+ uuid,
427
+ },
428
+ );
429
+ } else if (cli === "claude-code") {
430
+ await runClaudeCodeAgentForNode(
431
+ workspaceRoot,
432
+ { promptPath, nodeContext: nodeContext ?? "", taskBody: taskBody ?? "", intermediatePath, resultPathRel: resultPath, subagent, instanceId },
433
+ {
434
+ model,
435
+ stderrBuffer: options.stderrBuffer,
436
+ force: options.force,
437
+ outputPrefix: options.outputPrefix,
438
+ prefixColor: options.prefixColor,
439
+ onToolCall: options.onToolCall,
440
+ flowName,
441
+ uuid,
442
+ },
443
+ );
444
+ } else {
445
+ await runCursorAgentForNode(
446
+ workspaceRoot,
447
+ { promptPath, nodeContext: nodeContext ?? "", taskBody: taskBody ?? "", intermediatePath, resultPathRel: resultPath, subagent, instanceId },
448
+ {
449
+ model,
450
+ stderrBuffer: options.stderrBuffer,
451
+ force: options.force,
452
+ outputPrefix: options.outputPrefix,
453
+ prefixColor: options.prefixColor,
454
+ onToolCall: options.onToolCall,
455
+ flowName,
456
+ uuid,
457
+ },
458
+ );
459
+ }
460
+ emitEvent(workspaceRoot, flowName, uuid, {
461
+ event: "agent-invoke-done",
462
+ instanceId,
463
+ subagent: subagent ?? null,
464
+ modelCli: cli,
465
+ model: model ?? null,
466
+ });
467
+ } catch (err) {
468
+ const payload = {
469
+ event: "agent-invoke-failed",
470
+ instanceId,
471
+ subagent: subagent ?? null,
472
+ modelCli: cli,
473
+ model: model ?? null,
474
+ error: err && err.message ? String(err.message) : String(err),
475
+ };
476
+ if (err && err.cursorStderrTail) payload.cursorStderrTail = err.cursorStderrTail;
477
+ emitEvent(workspaceRoot, flowName, uuid, payload);
478
+ throw err;
479
+ }
480
+ }
481
+
482
+ /**
483
+ * @param {number | undefined} [opts.elapsedMs] 本节点执行耗时(毫秒),写入 result.elapsedMs 供 UI 展示
484
+ */
485
+ export function runPostProcess(workspaceRoot, flowName, uuid, instanceId, execId, opts = {}) {
486
+ const args = [workspaceRoot, flowName, uuid, instanceId, String(execId)];
487
+ const { elapsedMs } = opts;
488
+ if (elapsedMs != null && Number.isFinite(elapsedMs) && elapsedMs >= 0) {
489
+ args.push(String(Math.round(elapsedMs)));
490
+ }
491
+ const result = runNodeScript(workspaceRoot, "post-process-node.mjs", args, {
492
+ captureStdout: true,
493
+ });
494
+ parseJsonStdout(result);
495
+ }
496
+
497
+ export function ensureLocalNodeTerminalSuccess(workspaceRoot, flowName, uuid, instanceId, preOutput) {
498
+ if (!preOutput.definitionId || !LOCAL_ONLY_TERMINAL_SUCCESS_IDS.has(preOutput.definitionId)) return;
499
+ const payload = {
500
+ status: "success",
501
+ message: "已通过",
502
+ execId: preOutput.execId ?? 1,
503
+ };
504
+ const result = runNodeScript(
505
+ workspaceRoot,
506
+ "write-result.mjs",
507
+ [workspaceRoot, flowName, uuid, instanceId, "--json", JSON.stringify(payload)],
508
+ { captureStdout: true },
509
+ );
510
+ if (result.status !== 0) {
511
+ log.warn(`[agentflow] ensureLocalNodeTerminalSuccess write-result failed for ${instanceId}: ${result.stdout || result.stderr || result.status}`);
512
+ }
513
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * 规范化 tool_nodejs 的 script 命令行,修复误写 `node "${workspaceRoot}/..."` 等导致的错误路径。
3
+ * 供 node-execute(内联执行)与 run-tool-nodejs(子进程)共用。
4
+ */
5
+
6
+ /**
7
+ * 合并 bash 风格「'片段'/后续路径」拼接,得到真实文件系统路径。
8
+ * 同时处理中间嵌入的 `/'T'/` 形态(脚本模板里对变量加单引号,
9
+ * 但这些单引号对 spawn/非 shell 调用是字面量,需剥掉)。
10
+ */
11
+ export function normalizeConcatenatedSingleQuotedPath(s) {
12
+ let out = String(s).trim();
13
+ for (let n = 0; n < 64; n++) {
14
+ // 开头:'x'/y 或 'x'.y → xy
15
+ let next = out.replace(/^'([^']*)'(\/|\.)/, "$1$2");
16
+ // 中间:/'x'/ 或 /'x'. → /x/ 或 /x. (保留前后分隔符,避免误吃尾随空格后的独立 '…' 参数)
17
+ next = next.replace(/(\/)'([^'\s]*)'(\/|\.)/g, "$1$2$3");
18
+ if (next === out) break;
19
+ out = next;
20
+ }
21
+ return out;
22
+ }
23
+
24
+ /**
25
+ * 去掉首段外层双引号(含 `node "'/a'/.b" --flags`:首段结束后还有参数)。
26
+ */
27
+ export function stripOuterDoubleQuotes(rest) {
28
+ const t = rest.trim();
29
+ if (!t.startsWith('"')) return t;
30
+ for (let i = 1; i < t.length; i++) {
31
+ if (t[i] === '"' && (i + 1 >= t.length || /\s/.test(t[i + 1]))) {
32
+ return (t.slice(1, i) + t.slice(i + 1)).trim();
33
+ }
34
+ }
35
+ if (t.endsWith('"')) return t.slice(1, -1).trim();
36
+ return t.slice(1).trim();
37
+ }
38
+
39
+ /**
40
+ * 极简 shell 式分词:未加引号片段、单引号块、双引号块。
41
+ */
42
+ export function parseShellLikeArgs(input) {
43
+ const args = [];
44
+ const str = String(input);
45
+ let i = 0;
46
+ const len = str.length;
47
+ while (i < len) {
48
+ while (i < len && /\s/.test(str[i])) i++;
49
+ if (i >= len) break;
50
+ let arg = "";
51
+ if (str[i] === "'") {
52
+ i++;
53
+ while (i < len && str[i] !== "'") arg += str[i++];
54
+ if (str[i] === "'") i++;
55
+ } else if (str[i] === '"') {
56
+ i++;
57
+ while (i < len && str[i] !== '"') {
58
+ if (str[i] === "\\" && i + 1 < len) arg += str[++i];
59
+ else arg += str[i];
60
+ i++;
61
+ }
62
+ if (str[i] === '"') i++;
63
+ } else {
64
+ while (i < len && !/\s/.test(str[i])) arg += str[i++];
65
+ }
66
+ args.push(arg);
67
+ }
68
+ return args;
69
+ }
70
+
71
+ /**
72
+ * @param {string} resolvedScript - 如 node "'/root'/.workspace/.../x.mjs" 或 node "${workspaceRoot}/..." 解析后的错误形态
73
+ * @returns {string}
74
+ */
75
+ export function normalizeNodeToolCommandLine(resolvedScript) {
76
+ const t = String(resolvedScript).trim();
77
+ const m = t.match(/^node\s+/i);
78
+ if (!m) return resolvedScript;
79
+ let rest = t.slice(m[0].length).trim();
80
+ rest = stripOuterDoubleQuotes(rest);
81
+ rest = normalizeConcatenatedSingleQuotedPath(rest);
82
+ return `node ${rest}`;
83
+ }
84
+
85
+ /**
86
+ * @returns {{ argv: string[], commandLine: string }}
87
+ */
88
+ export function nodeToolCommandToArgv(commandLine) {
89
+ const normalized = normalizeNodeToolCommandLine(commandLine);
90
+ const nodeLead = normalized.match(/^node\s+/i);
91
+ if (!nodeLead) {
92
+ return { argv: [], commandLine: normalized };
93
+ }
94
+ const rest = normalized.slice(nodeLead[0].length).trim();
95
+ const argv = parseShellLikeArgs(rest);
96
+ return { argv, commandLine: normalized };
97
+ }