@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,852 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import chalk from "chalk";
4
+ import ora from "ora";
5
+ import readline from "readline";
6
+ import { spawn, spawnSync } from "child_process";
7
+ import { executeNode, ensureLocalNodeTerminalSuccess, runPostProcess } from "./node-execute.mjs";
8
+ import { log } from "./log.mjs";
9
+ import { resolveCliAndModel } from "./model-config.mjs";
10
+ import { t } from "./i18n.mjs";
11
+ import {
12
+ LOCAL_ONLY_DEFINITION_IDS,
13
+ MAX_LOOP_ROUNDS,
14
+ NODE_SEP,
15
+ LEGACY_PIPELINES_DIR,
16
+ PIPELINES_DIR,
17
+ USER_AGENTFLOW_PIPELINES_LABEL,
18
+ } from "./paths.mjs";
19
+ import { parseJsonStdout, runNodeScript } from "./pipeline-scripts.mjs";
20
+ import { loadFlowDefinition } from "../pipeline/parse-flow.mjs";
21
+ import { appendRunLogLine, emitEvent, ensureRunStartTime, readTotalExecutedMs, saveTotalExecutedMs } from "./run-events.mjs";
22
+ import { formatDuration } from "./terminal.mjs";
23
+ import { printEntryAndFlowFiles, printNodeStatusTable, runValidateFlowAndExitIfInvalid } from "./ui-print.mjs";
24
+ import { clearApplyActiveLock, writeApplyActiveLock } from "./run-apply-active-lock.mjs";
25
+ import { ensureReference, findFlowNameByUuid, getFlowDir, getRunDir } from "./workspace.mjs";
26
+
27
+ const PARALLEL_PREFIX_COLORS = [
28
+ (s) => chalk.cyan(s),
29
+ (s) => chalk.green(s),
30
+ (s) => chalk.yellow(s),
31
+ (s) => chalk.magenta(s),
32
+ (s) => chalk.blue(s),
33
+ ];
34
+
35
+ /** parallel 默认 false */
36
+ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel = null, force = true, parallel = false, cliInputs = {}) {
37
+ ensureReference(workspaceRoot);
38
+ const flowDir = getFlowDir(workspaceRoot, flowName);
39
+ if (!flowDir) {
40
+ throw new Error(
41
+ `Flow not found: ${flowName} (no flow.yaml under ${USER_AGENTFLOW_PIPELINES_LABEL}/${flowName}, ${PIPELINES_DIR}/${flowName}, ${LEGACY_PIPELINES_DIR}/${flowName}, or builtin)`,
42
+ );
43
+ }
44
+ runValidateFlowAndExitIfInvalid(workspaceRoot, flowName, flowDir);
45
+ const ensureArgs = [workspaceRoot, uuidArg ?? "", flowName];
46
+ const ensureResult = runNodeScript(workspaceRoot, "ensure-run-dir.mjs", ensureArgs, { captureStdout: true });
47
+ const { uuid } = parseJsonStdout(ensureResult);
48
+
49
+ const parseArgs = [workspaceRoot, flowName, uuid, flowDir];
50
+ if (Object.keys(cliInputs).length > 0) {
51
+ parseArgs.push("--cli-inputs", JSON.stringify(cliInputs));
52
+ }
53
+ const parseResult = runNodeScript(workspaceRoot, "parse-flow.mjs", parseArgs, { captureStdout: true });
54
+ const parseOut = parseJsonStdout(parseResult);
55
+ if (!parseOut.ok) throw new Error(parseOut.error || "parse-flow failed");
56
+
57
+ printEntryAndFlowFiles(workspaceRoot, flowName, uuid);
58
+ const priorTotalExecutedMs = readTotalExecutedMs(workspaceRoot, flowName, uuid) ?? 0;
59
+ // 提前 ensure:apply-start 携带原始 run 起始时间,UI 计时器可按「这个 uuid 从头到现在的总时长」显示,
60
+ // 避免每次 resume 仅从 totalExecutedMs 累加看起来像从 resume 才开始计时。
61
+ const priorRunStartTime = ensureRunStartTime(workspaceRoot, flowName, uuid);
62
+ emitEvent(workspaceRoot, flowName, uuid, {
63
+ event: "apply-start",
64
+ flowName,
65
+ uuid,
66
+ runDir: getRunDir(workspaceRoot, flowName, uuid),
67
+ dryRun: Boolean(dryRun),
68
+ parallel: Boolean(parallel),
69
+ totalExecutedMs: priorTotalExecutedMs,
70
+ runStartTime: priorRunStartTime,
71
+ });
72
+ writeApplyActiveLock(workspaceRoot, flowName, uuid);
73
+
74
+ try {
75
+ let runStartTime = priorRunStartTime;
76
+ let totalExecutedMs = priorTotalExecutedMs;
77
+ let round = 0;
78
+ while (round < MAX_LOOP_ROUNDS) {
79
+ round++;
80
+ const readyResult = runNodeScript(workspaceRoot, "get-ready-nodes.mjs", [workspaceRoot, flowName, uuid], {
81
+ captureStdout: true,
82
+ });
83
+ const { readyNodes = [], allDone, pendingNodes = [], instanceStatus = {}, execIdMap = {} } = parseJsonStdout(readyResult);
84
+
85
+ if (round === 1) printNodeStatusTable(instanceStatus, parseOut.nodes, execIdMap);
86
+
87
+ if (readyNodes.length === 0) {
88
+ if (allDone) {
89
+ saveTotalExecutedMs(workspaceRoot, flowName, uuid, totalExecutedMs);
90
+ const totalElapsed = formatDuration(totalExecutedMs);
91
+ emitEvent(workspaceRoot, flowName, uuid, {
92
+ event: "apply-done",
93
+ flowName,
94
+ uuid,
95
+ runDir: getRunDir(workspaceRoot, flowName, uuid),
96
+ totalElapsed,
97
+ });
98
+ log.info(`\n${t("apply.done")}. uuid=${uuid} runDir=${getRunDir(workspaceRoot, flowName, uuid)} ${chalk.dim(t("common.total") + " " + totalElapsed)}`);
99
+ return;
100
+ }
101
+ if (pendingNodes.length > 0) {
102
+ saveTotalExecutedMs(workspaceRoot, flowName, uuid, totalExecutedMs);
103
+ const totalElapsed = formatDuration(totalExecutedMs);
104
+ const resumeExample =
105
+ pendingNodes.length === 1
106
+ ? `agentflow resume ${flowName} ${uuid} ${pendingNodes[0]}`
107
+ : `agentflow resume ${flowName} ${uuid}`;
108
+ emitEvent(workspaceRoot, flowName, uuid, {
109
+ event: "apply-paused",
110
+ flowName,
111
+ uuid,
112
+ pendingNodes,
113
+ totalElapsed,
114
+ resumeExample,
115
+ });
116
+ log.info(`\n${t("apply.paused")}: uuid=${uuid} pendingNodes=${pendingNodes.join(", ")} ${chalk.dim(t("common.total") + " " + totalElapsed)}`);
117
+
118
+ let userConfirmed = false;
119
+ for (const pendId of pendingNodes) {
120
+ const pendNode = parseOut.nodes?.find((n) => n.id === pendId);
121
+ if (pendNode?.definitionId === "tool_user_ask") {
122
+ const pendExecId = execIdMap[pendId] ?? 1;
123
+ // 读取问题内容与选项
124
+ let question = "";
125
+ try {
126
+ const resolvedResult = runNodeScript(workspaceRoot, "get-resolved-values.mjs", [workspaceRoot, flowName, uuid, pendId], { captureStdout: true });
127
+ const resolvedData = parseJsonStdout(resolvedResult);
128
+ const qInput = resolvedData?.resolvedInputs?.question;
129
+ if (qInput && typeof qInput === "string") {
130
+ if (fs.existsSync(qInput)) {
131
+ try { question = fs.readFileSync(qInput, "utf-8"); } catch (_) {}
132
+ } else {
133
+ question = qInput;
134
+ }
135
+ }
136
+ } catch (_) {}
137
+ let pendInst = null;
138
+ try {
139
+ const flowDef = loadFlowDefinition(flowDir);
140
+ pendInst = flowDef?.instances?.[pendId] || null;
141
+ } catch (_) {}
142
+ const outputSlots = Array.isArray(pendInst?.output) ? pendInst.output : [];
143
+ const options = outputSlots.map((slot, idx) => {
144
+ const name = slot?.name != null ? String(slot.name) : `option_${idx}`;
145
+ const rawLabel = slot?.description || slot?.value || name;
146
+ const label = String(rawLabel || "").trim() || name;
147
+ return { index: idx, name, label };
148
+ });
149
+
150
+ if (options.length === 0) {
151
+ log.info(chalk.yellow(`节点 ${pendId} 未配置任何选项(output 槽位),无法选择。请先在编辑器中添加 output 槽位。`));
152
+ log.info(chalk.bold.yellow("→ " + t("flow.resume_hint") + " ") + resumeExample);
153
+ return;
154
+ }
155
+
156
+ if (!process.stdin.isTTY) {
157
+ log.info(chalk.bold.cyan(`\n━━━ 用户选择 (${pendId}) ━━━`));
158
+ if (question.trim()) {
159
+ for (const line of question.split("\n").slice(0, 30)) process.stderr.write(" " + line + "\n");
160
+ process.stderr.write("\n");
161
+ }
162
+ for (const opt of options) {
163
+ process.stderr.write(" " + chalk.green(`[${opt.index}]`) + " " + opt.label + "\n");
164
+ }
165
+ process.stderr.write(chalk.bold.cyan("━━━━━━━━━━━━━━━━━━━━━━━━━\n"));
166
+ log.info(chalk.bold.yellow("→ " + t("flow.resume_hint") + " ") + resumeExample);
167
+ return;
168
+ }
169
+
170
+ console.log("");
171
+ console.log(chalk.bold.cyan(`╔════════════════════════════════════════════════════════════╗`));
172
+ console.log(chalk.bold.cyan(`║ 用户选择节点: ${pendId}`) + " ".repeat(Math.max(0, 40 - pendId.length)) + "║");
173
+ console.log(chalk.bold.cyan(`╚════════════════════════════════════════════════════════════╝`));
174
+ console.log("");
175
+ if (question.trim()) {
176
+ console.log(chalk.dim("─".repeat(60)));
177
+ const qLines = question.split("\n").slice(0, 30);
178
+ for (const line of qLines) console.log(" " + line);
179
+ console.log(chalk.dim("─".repeat(60)));
180
+ console.log("");
181
+ }
182
+
183
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
184
+ try {
185
+ while (true) {
186
+ console.log(chalk.bold("请选择选项:"));
187
+ for (const opt of options) {
188
+ console.log(" " + chalk.green(`[${opt.index}]`) + " " + opt.label);
189
+ }
190
+ console.log(" " + chalk.red("q") + " - 取消并退出");
191
+ console.log("");
192
+ const answer = await new Promise((resolve) => {
193
+ rl.question(chalk.bold(`请输入编号 [0-${options.length - 1}]: `), resolve);
194
+ });
195
+ const trimmed = answer.trim().toLowerCase();
196
+ if (trimmed === "q") {
197
+ rl.close();
198
+ log.info(chalk.yellow("用户取消,流程暂停。") + chalk.dim(` 恢复命令: ${resumeExample}`));
199
+ return;
200
+ }
201
+ const idx = parseInt(trimmed, 10);
202
+ if (!Number.isFinite(idx) || idx < 0 || idx >= options.length) {
203
+ console.log(chalk.red(`无效编号:${answer}`));
204
+ continue;
205
+ }
206
+ const picked = options[idx];
207
+ const resultPayload = {
208
+ status: "success",
209
+ message: `用户选择 ${picked.name}`,
210
+ branch: picked.name,
211
+ execId: pendExecId,
212
+ };
213
+ runNodeScript(workspaceRoot, "write-result.mjs", [workspaceRoot, flowName, uuid, pendId, "--json", JSON.stringify(resultPayload)], { captureStdout: true });
214
+ log.info(chalk.dim(`用户选择 [${picked.index}] ${picked.label},继续执行...`));
215
+ userConfirmed = true;
216
+ rl.close();
217
+ break;
218
+ }
219
+ } catch (err) {
220
+ try { rl.close(); } catch (_) {}
221
+ throw err;
222
+ }
223
+ continue;
224
+ }
225
+ if (pendNode?.definitionId === "tool_user_check") {
226
+ const pendExecId = execIdMap[pendId] ?? 1;
227
+ const contentPath = path.join(getRunDir(workspaceRoot, flowName, uuid), `output/${pendId}/node_${pendId}_content.md`);
228
+ if (fs.existsSync(contentPath)) {
229
+ const checkContent = fs.readFileSync(contentPath, "utf-8");
230
+
231
+ // CLI 交互式确认
232
+ if (!process.stdin.isTTY) {
233
+ log.info(chalk.bold.cyan(`\n━━━ 用户确认内容 (${pendId}) ━━━`));
234
+ const contentLines = checkContent.split("\n").slice(0, 50);
235
+ for (const line of contentLines) {
236
+ process.stderr.write(" " + line + "\n");
237
+ }
238
+ process.stderr.write(chalk.bold.cyan("━━━━━━━━━━━━━━━━━━━━━━━━━\n"));
239
+ log.info(chalk.bold.yellow("→ " + t("flow.resume_hint") + " ") + resumeExample);
240
+ return;
241
+ }
242
+
243
+ // 显示内容和交互菜单
244
+ console.log("");
245
+ console.log(chalk.bold.cyan(`╔════════════════════════════════════════════════════════════╗`));
246
+ console.log(chalk.bold.cyan(`║ 用户确认节点: ${pendId}`) + " ".repeat(40 - pendId.length) + "║");
247
+ console.log(chalk.bold.cyan(`╠════════════════════════════════════════════════════════════╣`));
248
+ console.log(chalk.bold.cyan(`║ 文件路径: ${chalk.dim(contentPath)}`));
249
+ console.log(chalk.bold.cyan(`╚════════════════════════════════════════════════════════════╝`));
250
+ console.log("");
251
+
252
+ // 显示内容预览
253
+ const contentLines = checkContent.split("\n");
254
+ const previewLines = contentLines.slice(0, 30);
255
+ console.log(chalk.dim("─".repeat(60)));
256
+ for (const line of previewLines) {
257
+ console.log(" " + line);
258
+ }
259
+ if (contentLines.length > 30) {
260
+ console.log(chalk.dim(` ... (${contentLines.length - 30} 行已截断)`));
261
+ }
262
+ console.log(chalk.dim("─".repeat(60)));
263
+ console.log("");
264
+
265
+ // 交互菜单
266
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
267
+
268
+ while (true) {
269
+ console.log(chalk.bold("操作选项:"));
270
+ console.log(" " + chalk.green("c") + " - 确认并继续");
271
+ console.log(" " + chalk.blue("e") + " - 编辑内容(使用外部编辑器)");
272
+ console.log(" " + chalk.magenta("a") + " - AI 修改(输入指令)");
273
+ console.log(" " + chalk.yellow("v") + " - 查看完整内容");
274
+ console.log(" " + chalk.red("q") + " - 取消并退出");
275
+ console.log("");
276
+
277
+ const answer = await new Promise((resolve) => {
278
+ rl.question(chalk.bold("请选择操作 [c/e/a/v/q]: "), resolve);
279
+ });
280
+
281
+ if (answer.trim().toLowerCase() === "c" || answer.trim() === "") {
282
+ // 确认继续
283
+ log.info(chalk.dim("用户确认,继续执行..."));
284
+ const resultPayload = { status: "success", message: "用户确认通过", execId: pendExecId };
285
+ runNodeScript(workspaceRoot, "write-result.mjs", [workspaceRoot, flowName, uuid, pendId, "--json", JSON.stringify(resultPayload)], { captureStdout: true });
286
+ userConfirmed = true;
287
+ rl.close();
288
+ break;
289
+ } else if (answer.trim().toLowerCase() === "e") {
290
+ // 使用外部编辑器
291
+ const editor = process.env.EDITOR || process.env.VISUAL || "vim";
292
+ console.log(chalk.dim(`正在打开编辑器: ${editor} ${contentPath}`));
293
+ rl.pause();
294
+ const { status } = spawnSync(editor, [contentPath], { stdio: "inherit" });
295
+ rl.resume();
296
+ if (status === 0) {
297
+ console.log(chalk.green("编辑完成,内容已保存"));
298
+ // 重新读取内容
299
+ const editedContent = fs.readFileSync(contentPath, "utf-8");
300
+ console.log(chalk.dim("─".repeat(60)));
301
+ const newLines = editedContent.split("\n").slice(0, 10);
302
+ for (const line of newLines) {
303
+ console.log(" " + line);
304
+ }
305
+ console.log(chalk.dim("─".repeat(60)));
306
+ } else {
307
+ console.log(chalk.yellow("编辑器退出异常"));
308
+ }
309
+ } else if (answer.trim().toLowerCase() === "a") {
310
+ // AI 修改
311
+ const aiPrompt = await new Promise((resolve) => {
312
+ rl.question(chalk.magenta("输入 AI 修改指令: "), resolve);
313
+ });
314
+ if (aiPrompt.trim()) {
315
+ console.log(chalk.dim("AI 正在修改..."));
316
+ const currentContent = fs.readFileSync(contentPath, "utf-8");
317
+
318
+ // 调用 OpenCode
319
+ const opencodeCmd = process.env.OPENCODE_CMD || "opencode";
320
+ const tmpPromptFile = path.join(getRunDir(workspaceRoot, flowName, uuid), `intermediate/${pendId}_ai_prompt.txt`);
321
+ const fullPrompt = `请根据以下指令修改内容。直接输出修改后的完整内容,不要解释。
322
+
323
+ 原始内容:
324
+ ---
325
+ ${currentContent}
326
+ ---
327
+
328
+ 修改指令:${aiPrompt}
329
+
330
+ 请直接输出修改后的完整内容(保持原有格式):`;
331
+
332
+ fs.mkdirSync(path.dirname(tmpPromptFile), { recursive: true });
333
+ fs.writeFileSync(tmpPromptFile, fullPrompt, "utf-8");
334
+
335
+ const result = spawnSync(opencodeCmd, ["--prompt-file", tmpPromptFile, "--print"], {
336
+ cwd: workspaceRoot,
337
+ env: { ...process.env, OPENCODE_NON_INTERACTIVE: "1" },
338
+ stdio: ["ignore", "pipe", "pipe"],
339
+ });
340
+
341
+ try { fs.unlinkSync(tmpPromptFile); } catch (_) {}
342
+
343
+ if (result.status === 0 && result.stdout) {
344
+ const editedContent = result.stdout.toString().trim();
345
+ fs.writeFileSync(contentPath, editedContent, "utf-8");
346
+ console.log(chalk.green("AI 修改完成"));
347
+ console.log(chalk.dim("─".repeat(60)));
348
+ const newLines = editedContent.split("\n").slice(0, 10);
349
+ for (const line of newLines) {
350
+ console.log(" " + line);
351
+ }
352
+ console.log(chalk.dim("─".repeat(60)));
353
+ } else {
354
+ console.log(chalk.red("AI 修改失败: " + (result.stderr?.toString() || "未知错误")));
355
+ }
356
+ }
357
+ } else if (answer.trim().toLowerCase() === "v") {
358
+ // 查看完整内容
359
+ console.log("");
360
+ console.log(chalk.dim("─".repeat(60)));
361
+ const fullContent = fs.readFileSync(contentPath, "utf-8");
362
+ for (const line of fullContent.split("\n")) {
363
+ console.log(" " + line);
364
+ }
365
+ console.log(chalk.dim("─".repeat(60)));
366
+ console.log("");
367
+ } else if (answer.trim().toLowerCase() === "q") {
368
+ rl.close();
369
+ log.info(chalk.yellow("用户取消,流程暂停。") + chalk.dim(` 恢复命令: ${resumeExample}`));
370
+ return;
371
+ }
372
+ }
373
+ }
374
+ }
375
+ }
376
+
377
+ if (userConfirmed) {
378
+ // 用户确认后继续循环
379
+ continue;
380
+ }
381
+
382
+ log.info(chalk.bold.yellow("→ " + t("flow.resume_hint") + " ") + resumeExample);
383
+ return;
384
+ }
385
+ const endNodeIds = Array.isArray(parseOut.nodes)
386
+ ? parseOut.nodes.filter((n) => n.definitionId === "control_end").map((n) => n.id)
387
+ : [];
388
+ const endReached = endNodeIds.some((id) => instanceStatus[id] === "success");
389
+ if (endReached) {
390
+ saveTotalExecutedMs(workspaceRoot, flowName, uuid, totalExecutedMs);
391
+ const totalElapsed = formatDuration(totalExecutedMs);
392
+ emitEvent(workspaceRoot, flowName, uuid, {
393
+ event: "apply-done",
394
+ flowName,
395
+ uuid,
396
+ runDir: getRunDir(workspaceRoot, flowName, uuid),
397
+ totalElapsed,
398
+ });
399
+ log.info(`\n${t("apply.done")}. uuid=${uuid} runDir=${getRunDir(workspaceRoot, flowName, uuid)} ${chalk.dim(t("common.total") + " " + totalElapsed)}`);
400
+ return;
401
+ }
402
+ const totalElapsed = formatDuration(totalExecutedMs);
403
+ const stuckErr = new Error(t("flow.stuck_error") + " " + t("common.total") + " " + totalElapsed);
404
+ stuckErr.flowName = flowName;
405
+ stuckErr.uuid = uuid;
406
+ throw stuckErr;
407
+ }
408
+
409
+ if (dryRun) {
410
+ const totalElapsed = formatDuration(totalExecutedMs);
411
+ log.info(`\n${t("flow.dry_run_nodes", { nodes: readyNodes.join(", ") })} ${chalk.dim(t("common.total") + " " + totalElapsed)}`);
412
+ return;
413
+ }
414
+
415
+ if (runStartTime === null) {
416
+ runStartTime = ensureRunStartTime(workspaceRoot, flowName, uuid);
417
+ totalExecutedMs = readTotalExecutedMs(workspaceRoot, flowName, uuid);
418
+ }
419
+
420
+ const idToLabel = new Map();
421
+ if (Array.isArray(parseOut.nodes)) for (const n of parseOut.nodes) idToLabel.set(n.id, n.label || n.id);
422
+
423
+ const preOutputs = [];
424
+ for (const instanceId of readyNodes) {
425
+ log.debug(`[agentflow] 进入节点 flowName=${flowName} uuid=${uuid} instanceId=${instanceId} round=${round}`);
426
+ const preResult = runNodeScript(workspaceRoot, "pre-process-node.mjs", [workspaceRoot, flowName, uuid, instanceId], {
427
+ captureStdout: true,
428
+ });
429
+ const preOutput = parseJsonStdout(preResult);
430
+ preOutputs.push({ instanceId, label: idToLabel.get(instanceId) || instanceId, preOutput });
431
+ log.debug(
432
+ `[agentflow] 执行节点 instanceId=${instanceId} definitionId=${preOutput.definitionId ?? "-"} promptPath=${preOutput.promptPath} resultPath=${preOutput.resultPath} subagent=${preOutput.subagent} role=${preOutput.role ?? "-"} model=${preOutput.model ?? "-"} execId=${preOutput.execId} directCommand=${preOutput.directCommand ? "yes" : "-"}`,
433
+ );
434
+ }
435
+
436
+ const runOne = async ({ instanceId, label, preOutput, outputPrefix, prefixColor }, isParallel) => {
437
+ let elapsedMsForPost = undefined;
438
+ if (!isParallel) {
439
+ const isLocalOnly = preOutput.definitionId && LOCAL_ONLY_DEFINITION_IDS.has(preOutput.definitionId);
440
+ const { label: resolvedLabel } = resolveCliAndModel(workspaceRoot, preOutput.model ?? null, agentModel ?? null);
441
+ const modelLabel = isLocalOnly ? `(${t("common.local")})` : resolvedLabel;
442
+ const promptAbs = path.resolve(workspaceRoot, preOutput.promptPath);
443
+ const cliResolved = resolveCliAndModel(workspaceRoot, preOutput.model ?? null, agentModel ?? null);
444
+ emitEvent(workspaceRoot, flowName, uuid, {
445
+ event: "node-start",
446
+ instanceId,
447
+ label,
448
+ definitionId: preOutput.definitionId ?? null,
449
+ modelCli: isLocalOnly ? null : cliResolved.cli,
450
+ model: modelLabel,
451
+ execId: preOutput.execId ?? null,
452
+ promptPathRel: preOutput.promptPath ?? null,
453
+ promptPathAbs: promptAbs,
454
+ resultPathRel: preOutput.resultPath ?? null,
455
+ subagent: preOutput.subagent ?? null,
456
+ directCommand: preOutput.directCommand ? String(preOutput.directCommand) : null,
457
+ });
458
+ appendRunLogLine(workspaceRoot, flowName, uuid, "cli-raw", `${t("node.start")} ${instanceId} (${label}) model: ${modelLabel}`);
459
+ appendRunLogLine(workspaceRoot, flowName, uuid, "cli-raw", `Prompt: ${promptAbs}`);
460
+ process.stderr.write("\n" + NODE_SEP + "\n");
461
+ process.stderr.write(
462
+ chalk.bold.cyan(t("node.start") + " ") + instanceId + chalk.dim(" (" + label + ")") + " " + chalk.dim(t("node.model_label") + ": ") + chalk.yellow(modelLabel) + "\n",
463
+ );
464
+ log.info(chalk.dim("Prompt: ") + promptAbs);
465
+ process.stderr.write(NODE_SEP + "\n\n");
466
+ const startTime = Date.now();
467
+ const getTotalStr = () => formatDuration(totalExecutedMs + (Date.now() - startTime));
468
+ const initialRunningText = `Running: ${instanceId} (${label}) ${formatDuration(0) + " / " + getTotalStr()}`;
469
+ appendRunLogLine(workspaceRoot, flowName, uuid, "cli-raw", initialRunningText);
470
+ const spinner = ora({
471
+ text: `Running: ${instanceId} ${chalk.dim("(" + label + ")")} ${chalk.dim(formatDuration(0) + " / " + getTotalStr())}`,
472
+ stream: process.stderr,
473
+ discardStdin: false,
474
+ }).start();
475
+ let lastToolCallText = "";
476
+ const updateSpinnerText = () => {
477
+ const duration = formatDuration(Date.now() - startTime) + " / " + getTotalStr();
478
+ spinner.text =
479
+ `Running: ${instanceId} ${chalk.dim("(" + label + ")")} ${chalk.dim(duration)}` +
480
+ (lastToolCallText ? " " + chalk.dim("| " + lastToolCallText) : "");
481
+ };
482
+ const timeTick = setInterval(updateSpinnerText, 1000);
483
+ const stderrBuffer = [];
484
+ let elapsedStr = "";
485
+ try {
486
+ await executeNode(workspaceRoot, flowName, uuid, instanceId, preOutput, {
487
+ model: agentModel,
488
+ stderrBuffer,
489
+ force,
490
+ outputPrefix: instanceId,
491
+ prefixColor: (s) => chalk.cyan(s),
492
+ onToolCall(subtype, toolName) {
493
+ lastToolCallText = subtype === "thinking" ? "thinking" : `${toolName} ${subtype}`;
494
+ updateSpinnerText();
495
+ },
496
+ });
497
+ clearInterval(timeTick);
498
+ const nodeMs = Date.now() - startTime;
499
+ elapsedMsForPost = nodeMs;
500
+ totalExecutedMs += nodeMs;
501
+ saveTotalExecutedMs(workspaceRoot, flowName, uuid, totalExecutedMs);
502
+ elapsedStr = formatDuration(nodeMs);
503
+ const totalStr = formatDuration(totalExecutedMs);
504
+ spinner.succeed(
505
+ chalk.green(`${t("node.done_label")}: ${instanceId}`) + chalk.dim(" (" + label + ")") + " " + chalk.dim(elapsedStr) + " " + chalk.dim(t("common.total") + " " + totalStr),
506
+ );
507
+ emitEvent(workspaceRoot, flowName, uuid, {
508
+ event: "node-done",
509
+ instanceId,
510
+ label,
511
+ elapsed: elapsedStr,
512
+ total: totalStr,
513
+ });
514
+ } catch (err) {
515
+ clearInterval(timeTick);
516
+ const nodeMs = Date.now() - startTime;
517
+ totalExecutedMs += nodeMs;
518
+ saveTotalExecutedMs(workspaceRoot, flowName, uuid, totalExecutedMs);
519
+ elapsedStr = formatDuration(nodeMs);
520
+ const totalStr = formatDuration(totalExecutedMs);
521
+ spinner.fail(
522
+ chalk.red(`${t("node.failed_label")}: ${instanceId}`) + chalk.dim(" (" + label + ")") + " " + chalk.dim(elapsedStr) + " " + chalk.dim(t("common.total") + " " + totalStr),
523
+ );
524
+ emitEvent(workspaceRoot, flowName, uuid, {
525
+ event: "node-failed",
526
+ instanceId,
527
+ label,
528
+ elapsed: elapsedStr,
529
+ total: totalStr,
530
+ error: err && err.message ? String(err.message) : String(err),
531
+ });
532
+ if (stderrBuffer.length > 0) process.stderr.write(Buffer.concat(stderrBuffer));
533
+ err.flowName = flowName;
534
+ err.uuid = uuid;
535
+ throw err;
536
+ }
537
+ if (stderrBuffer.length > 0) process.stderr.write(Buffer.concat(stderrBuffer));
538
+ process.stderr.write("\n" + NODE_SEP + "\n");
539
+ process.stderr.write(
540
+ chalk.bold.cyan(t("node.end") + " ") +
541
+ instanceId +
542
+ chalk.dim(" (" + label + ")") +
543
+ (elapsedStr ? " " + chalk.dim(elapsedStr) : "") +
544
+ " " +
545
+ chalk.dim(t("common.total") + " " + formatDuration(totalExecutedMs)) +
546
+ "\n",
547
+ );
548
+ process.stderr.write(NODE_SEP + "\n");
549
+ } else {
550
+ const startTime = Date.now();
551
+ await executeNode(workspaceRoot, flowName, uuid, instanceId, preOutput, {
552
+ model: agentModel,
553
+ stderrBuffer: [],
554
+ force,
555
+ outputPrefix,
556
+ prefixColor,
557
+ });
558
+ elapsedMsForPost = Date.now() - startTime;
559
+ }
560
+ runPostProcess(workspaceRoot, flowName, uuid, instanceId, preOutput.execId, { elapsedMs: elapsedMsForPost });
561
+ ensureLocalNodeTerminalSuccess(workspaceRoot, flowName, uuid, instanceId, preOutput);
562
+
563
+ // user_check 节点:在主流程中发送事件,因为 post-process 的 emitEvent 被子进程捕获
564
+ if (preOutput.definitionId === "tool_user_check") {
565
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
566
+ const execId = preOutput.execId ?? 1;
567
+ const outputPath = path.join(runDir, `output/${instanceId}/node_${instanceId}_content.md`);
568
+
569
+ // 获取 resolvedInputs
570
+ const resolvedResult = runNodeScript(workspaceRoot, "get-resolved-values.mjs", [workspaceRoot, flowName, uuid, instanceId], { captureStdout: true });
571
+ const resolvedData = parseJsonStdout(resolvedResult);
572
+
573
+ // 读取 content 输入槽位的内容
574
+ let content = "";
575
+ let contentInputPath = null;
576
+ if (resolvedData.ok && resolvedData.resolvedInputs?.content) {
577
+ contentInputPath = resolvedData.resolvedInputs.content;
578
+ if (contentInputPath && fs.existsSync(contentInputPath)) {
579
+ try {
580
+ content = fs.readFileSync(contentInputPath, "utf-8");
581
+ } catch (_) {}
582
+ }
583
+ }
584
+
585
+ // 确保 output 文件存在
586
+ try {
587
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
588
+ fs.writeFileSync(outputPath, content, "utf-8");
589
+ } catch (_) {}
590
+
591
+ emitEvent(workspaceRoot, flowName, uuid, {
592
+ type: "user-check-content",
593
+ event: "user-check-content",
594
+ instanceId,
595
+ execId,
596
+ inputPath: contentInputPath,
597
+ outputPath: `output/${instanceId}/node_${instanceId}_content.md`,
598
+ content,
599
+ });
600
+ }
601
+
602
+ // user_ask 节点:在主流程中发送事件(post-process 的 emitEvent 在子进程中不会到 SSE 流)
603
+ if (preOutput.definitionId === "tool_user_ask") {
604
+ const execId = preOutput.execId ?? 1;
605
+
606
+ let question = "";
607
+ try {
608
+ const resolvedResult = runNodeScript(workspaceRoot, "get-resolved-values.mjs", [workspaceRoot, flowName, uuid, instanceId], { captureStdout: true });
609
+ const resolvedData = parseJsonStdout(resolvedResult);
610
+ const qInput = resolvedData?.resolvedInputs?.question;
611
+ if (qInput && typeof qInput === "string") {
612
+ if (fs.existsSync(qInput)) {
613
+ try { question = fs.readFileSync(qInput, "utf-8"); } catch (_) {}
614
+ } else {
615
+ question = qInput;
616
+ }
617
+ }
618
+ } catch (_) {}
619
+
620
+ let inst = null;
621
+ try {
622
+ const flowDef = loadFlowDefinition(flowDir);
623
+ inst = flowDef?.instances?.[instanceId] || null;
624
+ } catch (_) {}
625
+ const outputSlots = Array.isArray(inst?.output) ? inst.output : [];
626
+ const options = outputSlots.map((slot, idx) => {
627
+ const name = slot?.name != null ? String(slot.name) : `option_${idx}`;
628
+ const rawLabel = slot?.description || slot?.value || name;
629
+ const label = String(rawLabel || "").trim() || name;
630
+ return { index: idx, name, label };
631
+ });
632
+
633
+ emitEvent(workspaceRoot, flowName, uuid, {
634
+ type: "user-ask-prompt",
635
+ event: "user-ask-prompt",
636
+ instanceId,
637
+ execId,
638
+ question,
639
+ options,
640
+ });
641
+ }
642
+
643
+ // tool_print 节点:CLI 框框展示 + 发送事件供 Web UI 显示
644
+ if (preOutput.definitionId === "tool_print") {
645
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
646
+ const execId = preOutput.execId ?? 1;
647
+ const resultPath = path.join(runDir, `intermediate/${instanceId}/${instanceId}.result.md`);
648
+
649
+ if (fs.existsSync(resultPath)) {
650
+ const raw = fs.readFileSync(resultPath, "utf-8");
651
+ const bodyMatch = raw.match(/---\s*\r?\n[\s\S]*?\r?\n---\s*\r?\n([\s\S]*)$/);
652
+ const content = bodyMatch ? bodyMatch[1].trim() : raw.trim();
653
+
654
+ if (content) {
655
+ // CLI 框框样式展示(参考 user_check)
656
+ console.log("");
657
+ console.log(chalk.bold.cyan(`╔════════════════════════════════════════════════════════════╗`));
658
+ const titleLine = `║ Print 输出: ${instanceId}`;
659
+ const padding = 58 - titleLine.length;
660
+ console.log(chalk.bold.cyan(titleLine + " ".repeat(Math.max(0, padding)) + "║"));
661
+ console.log(chalk.bold.cyan(`╚════════════════════════════════════════════════════════════╝`));
662
+ console.log("");
663
+
664
+ const contentLines = content.split("\n");
665
+ const previewLines = contentLines.slice(0, 30);
666
+ console.log(chalk.dim("─".repeat(60)));
667
+ for (const line of previewLines) {
668
+ console.log(" " + line);
669
+ }
670
+ if (contentLines.length > 30) {
671
+ console.log(chalk.dim(` ... (${contentLines.length - 30} 行已截断)`));
672
+ }
673
+ console.log(chalk.dim("─".repeat(60)));
674
+ console.log("");
675
+
676
+ // 发送事件供 Web UI 显示右下角通知卡片
677
+ emitEvent(workspaceRoot, flowName, uuid, {
678
+ type: "tool-print-content",
679
+ event: "tool-print-content",
680
+ instanceId,
681
+ execId,
682
+ content,
683
+ });
684
+ }
685
+ }
686
+ }
687
+
688
+ log.debug(`[agentflow] 退出节点 instanceId=${instanceId} execId=${preOutput.execId}`);
689
+ };
690
+
691
+ const useParallel = parallel && preOutputs.length > 1;
692
+ if (useParallel) {
693
+ preOutputs.forEach((item, i) => {
694
+ item.outputPrefix = item.instanceId;
695
+ item.prefixColor = PARALLEL_PREFIX_COLORS[i % PARALLEL_PREFIX_COLORS.length];
696
+ });
697
+ emitEvent(workspaceRoot, flowName, uuid, {
698
+ event: "parallel-start",
699
+ size: preOutputs.length,
700
+ nodes: preOutputs.map((p) => ({
701
+ instanceId: p.instanceId,
702
+ label: p.label,
703
+ definitionId: p.preOutput?.definitionId ?? null,
704
+ promptPathRel: p.preOutput?.promptPath ?? null,
705
+ })),
706
+ });
707
+ appendRunLogLine(
708
+ workspaceRoot,
709
+ flowName,
710
+ uuid,
711
+ "cli-raw",
712
+ `${t("node.start_parallel")} ${preOutputs.length} 个节点: ${preOutputs.map((p) => p.instanceId).join(", ")}`,
713
+ );
714
+ preOutputs.forEach((item) => {
715
+ const abs = path.resolve(workspaceRoot, item.preOutput.promptPath);
716
+ appendRunLogLine(workspaceRoot, flowName, uuid, "cli-raw", `Prompt: ${abs}`);
717
+ });
718
+ appendRunLogLine(
719
+ workspaceRoot,
720
+ flowName,
721
+ uuid,
722
+ "cli-raw",
723
+ `Running ${preOutputs.length} nodes in parallel: ${preOutputs.map((p) => p.instanceId).join(", ")}`,
724
+ );
725
+ process.stderr.write("\n" + NODE_SEP + "\n");
726
+ process.stderr.write(chalk.bold.cyan(t("node.start_parallel") + " ") + preOutputs.length + " 个节点: " + preOutputs.map((p) => p.instanceId).join(", ") + "\n");
727
+ for (const item of preOutputs) log.info(chalk.dim("Prompt: ") + path.resolve(workspaceRoot, item.preOutput.promptPath));
728
+ process.stderr.write(NODE_SEP + "\n\n");
729
+ log.info(chalk.cyan(`Running ${preOutputs.length} nodes in parallel: ${preOutputs.map((p) => p.instanceId).join(", ")}`));
730
+ const parallelBatchStart = Date.now();
731
+ await Promise.all(preOutputs.map((item) => runOne(item, true)));
732
+ totalExecutedMs += Date.now() - parallelBatchStart;
733
+ saveTotalExecutedMs(workspaceRoot, flowName, uuid, totalExecutedMs);
734
+ const totalStrPar = formatDuration(totalExecutedMs);
735
+ process.stderr.write("\n" + NODE_SEP + "\n");
736
+ process.stderr.write(chalk.bold.cyan(t("node.end_parallel") + " ") + chalk.dim(t("common.total") + " " + totalStrPar) + "\n");
737
+ process.stderr.write(NODE_SEP + "\n");
738
+ emitEvent(workspaceRoot, flowName, uuid, {
739
+ event: "parallel-done",
740
+ size: preOutputs.length,
741
+ total: totalStrPar,
742
+ });
743
+ } else {
744
+ for (const item of preOutputs) await runOne(item, false);
745
+ }
746
+ }
747
+ const totalElapsed = formatDuration(totalExecutedMs);
748
+ const maxErr = new Error(`Max rounds (${MAX_LOOP_ROUNDS}) reached. ${t("common.total")} ${totalElapsed}`);
749
+ maxErr.flowName = flowName;
750
+ maxErr.uuid = uuid;
751
+ throw maxErr;
752
+ } finally {
753
+ clearApplyActiveLock(workspaceRoot, flowName, uuid);
754
+ }
755
+ }
756
+
757
+ export async function resume(workspaceRoot, flowName, uuid, instanceIdOptional, agentModel = null, force = true, parallel = false) {
758
+ let nodesToResume = [];
759
+ if (instanceIdOptional) {
760
+ nodesToResume = [instanceIdOptional];
761
+ } else {
762
+ const readyResult = runNodeScript(workspaceRoot, "get-ready-nodes.mjs", [workspaceRoot, flowName, uuid], { captureStdout: true });
763
+ const { pendingNodes = [], instanceStatus = {} } = parseJsonStdout(readyResult);
764
+ const failedNodes = Object.keys(instanceStatus).filter((id) => instanceStatus[id] === "failed");
765
+ nodesToResume = [...new Set([...pendingNodes, ...failedNodes])];
766
+ }
767
+ const payload = JSON.stringify({ status: "success", message: t("apply.user_confirmed") });
768
+ for (const instanceId of nodesToResume) {
769
+ const wr = runNodeScript(
770
+ workspaceRoot,
771
+ "write-result.mjs",
772
+ [workspaceRoot, flowName, uuid, instanceId, "--json", payload],
773
+ { captureStdout: true },
774
+ );
775
+ if (wr.status !== 0) {
776
+ const err = (wr.stdout || "").trim() || wr.stderr || "write-result failed";
777
+ throw new Error(`resume: write result for ${instanceId} failed: ${err}`);
778
+ }
779
+ log.info(chalk.dim(`Resumed node: ${instanceId}`));
780
+ }
781
+ await apply(workspaceRoot, flowName, uuid, false, agentModel, force, parallel, {});
782
+ }
783
+
784
+ export async function replay(workspaceRoot, flowNameOrUuid, uuidOrInstanceId, instanceIdArg, agentModel = null, force = true) {
785
+ let flowName, uuid, instanceId;
786
+ const flowJsonPathFor = (f, u) => path.join(getRunDir(workspaceRoot, f, u), "intermediate", "flow.json");
787
+
788
+ if (instanceIdArg !== undefined) {
789
+ flowName = flowNameOrUuid;
790
+ uuid = uuidOrInstanceId;
791
+ instanceId = instanceIdArg;
792
+ } else {
793
+ uuid = flowNameOrUuid;
794
+ instanceId = uuidOrInstanceId;
795
+ flowName = findFlowNameByUuid(workspaceRoot, uuid);
796
+ if (!flowName) {
797
+ throw new Error("No run found for uuid " + uuid + ". Run apply first or use: agentflow replay <flowName> <uuid> <instanceId>");
798
+ }
799
+ const flowJsonPath = flowJsonPathFor(flowName, uuid);
800
+ if (!fs.existsSync(flowJsonPath)) {
801
+ throw new Error("flow.json not found. Run apply first or use: agentflow replay <flowName> <uuid> <instanceId>");
802
+ }
803
+ const flow = JSON.parse(fs.readFileSync(flowJsonPath, "utf-8"));
804
+ if (!flow.flowName && !flow.name) {
805
+ throw new Error("flow.json missing flowName. Use: agentflow replay <flowName> <uuid> <instanceId>");
806
+ }
807
+ }
808
+
809
+ log.debug(`[agentflow] 进入节点 flowName=${flowName} uuid=${uuid} instanceId=${instanceId}`);
810
+ const preResult = runNodeScript(workspaceRoot, "pre-process-node.mjs", [workspaceRoot, flowName, uuid, instanceId], { captureStdout: true });
811
+ const preOutput = parseJsonStdout(preResult);
812
+ log.debug(
813
+ `[agentflow] 执行节点 instanceId=${instanceId} definitionId=${preOutput.definitionId ?? "-"} promptPath=${preOutput.promptPath} resultPath=${preOutput.resultPath} subagent=${preOutput.subagent} modelType=${preOutput.modelType ?? "-"} execId=${preOutput.execId} directCommand=${preOutput.directCommand ? "yes" : "-"}`,
814
+ );
815
+ const promptAbs = path.resolve(workspaceRoot, preOutput.promptPath);
816
+ const isLocalOnlyReplay = preOutput.definitionId && LOCAL_ONLY_DEFINITION_IDS.has(preOutput.definitionId);
817
+ const { label: modelLabelReplay } = resolveCliAndModel(workspaceRoot, preOutput.model ?? null, agentModel ?? null);
818
+ const modelLabelDisplay = isLocalOnlyReplay ? `(${t("common.local")})` : modelLabelReplay;
819
+ emitEvent(workspaceRoot, flowName, uuid, {
820
+ event: "replay-start",
821
+ flowName,
822
+ uuid,
823
+ instanceId,
824
+ definitionId: preOutput.definitionId ?? null,
825
+ model: modelLabelDisplay,
826
+ execId: preOutput.execId ?? null,
827
+ promptPathRel: preOutput.promptPath ?? null,
828
+ promptPathAbs: promptAbs,
829
+ resultPathRel: preOutput.resultPath ?? null,
830
+ });
831
+ appendRunLogLine(workspaceRoot, flowName, uuid, "cli-raw", `${t("node.start")} replay 节点 ${instanceId} model: ${modelLabelDisplay}`);
832
+ appendRunLogLine(workspaceRoot, flowName, uuid, "cli-raw", `Prompt: ${promptAbs}`);
833
+ process.stderr.write("\n" + NODE_SEP + "\n");
834
+ process.stderr.write(chalk.bold.cyan(t("node.start") + " ") + instanceId + " " + chalk.dim(t("node.model_label") + ": ") + chalk.yellow(modelLabelDisplay) + "\n");
835
+ log.info(chalk.dim("Prompt: ") + promptAbs);
836
+ process.stderr.write(NODE_SEP + "\n\n");
837
+ const replayStart = Date.now();
838
+ await executeNode(workspaceRoot, flowName, uuid, instanceId, preOutput, { model: agentModel, force });
839
+ runPostProcess(workspaceRoot, flowName, uuid, instanceId, preOutput.execId, { elapsedMs: Date.now() - replayStart });
840
+ ensureLocalNodeTerminalSuccess(workspaceRoot, flowName, uuid, instanceId, preOutput);
841
+ log.debug(`[agentflow] 退出节点 instanceId=${instanceId} execId=${preOutput.execId}`);
842
+ process.stderr.write("\n" + NODE_SEP + "\n");
843
+ process.stderr.write(chalk.bold.cyan(t("node.end") + " ") + instanceId + "\n");
844
+ process.stderr.write(NODE_SEP + "\n");
845
+ emitEvent(workspaceRoot, flowName, uuid, {
846
+ event: "replay-done",
847
+ flowName,
848
+ uuid,
849
+ instanceId,
850
+ });
851
+ log.info(`\nReplay done. ${instanceId} uuid=${uuid}`);
852
+ }