@fieldwangai/agentflow 0.1.153 → 0.1.154

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