@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,449 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 统一预处理入口:对当前节点做 ${} 替换并生成 prompt.md;读取 role / model 并计算 subagent。
4
+ * 用法:node pre-process-node.mjs <workspaceRoot> <flowName> <uuid> <instanceId>
5
+ * 输出(stdout JSON):{ "ok": true, "promptPath": "...", "optionalPromptPath"?: "...", "directCommand"?: "...", "subagent": "...", "definitionId": "...", "role"?: "...", "model"?: "..." }
6
+ * definitionId 供 CLI 做 LOCAL_ONLY 判断;directCommand 供 CLI 直接执行并跳过 agent(与 optionalPromptPath 语义一致,仅 CLI 使用)。
7
+ *
8
+ * 当前 pre-process 流程与 cache 的关系:
9
+ *
10
+ * 1) 公共开头:从 memory 加载 execId(+1 作为本轮),得到 runDir、resultPathRel;读 flow.json 得 definitionId。
11
+ *
12
+ * 2) 分支 A(definitionId === "control_if"):
13
+ * - 用 getResolvedValues + 第一个 bool 槽取值 → parseBool → branch;
14
+ * - writeResult(success, branch);
15
+ * - buildNodePrompt → writeCacheJsonForNode(统一写 .cache.json)→ 写 noop prompt,设 optionalPromptPath,return。
16
+ *
17
+ * 3) 分支 B(普通节点,含 control_toBool 走 tool_nodejs 同路径):
18
+ * - buildNodePrompt → writeResult("running") → writeCacheJsonForNode(统一写 .cache.json);
19
+ * - 若有 tool_load_key/tool_save_key/tool_get_env/control_anyOne 再设 optionalPromptPath,并视情况输出 directCommand 供 CLI 执行;
20
+ * - 返回 promptPath、resultPath、execId、subagent 等。
21
+ *
22
+ * cache.json 流程已统一:control_if 与普通节点均通过 writeCacheJsonForNode 在「prompt 已存在」的前提下执行 computeCacheMd5 并写入 intermediate/<instanceId>/<instanceId>.cache.json,结构一致(含 cacheMd5、cacheInputInfo、execId、inputHandlerExecIds、payload)。
23
+ */
24
+
25
+ import { spawnSync } from "child_process";
26
+ import fs from "fs";
27
+ import path from "path";
28
+ import { fileURLToPath } from "url";
29
+
30
+ import { loadFlowDefinition } from "./parse-flow.mjs";
31
+ import { buildNodePrompt } from "./build-node-prompt.mjs";
32
+ import { snapshotPriorRoundIfNeeded } from "./snapshot-prior-round.mjs";
33
+ import { computeCacheMd5 } from "./compute-cache-md5.mjs";
34
+ import { getResolvedValues } from "./get-resolved-values.mjs";
35
+ import { parseBool, getFirstBoolInputValue } from "./parse-bool.mjs";
36
+ import { writeResult } from "./write-result.mjs";
37
+ import { intermediateResultBasename, intermediateCacheBasename, intermediateDirForNode, outputNodeBasename, outputDirForNode } from "./get-exec-id.mjs";
38
+ import { logToRunTag } from "./run-log.mjs";
39
+ import { getRunDir } from "../lib/paths.mjs";
40
+
41
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
42
+
43
+ const ROLE_TO_SUBAGENT = {
44
+ requirement: "agentflow-node-executor-requirement",
45
+ planning: "agentflow-node-executor-planning",
46
+ code: "agentflow-node-executor-code",
47
+ test: "agentflow-node-executor-test",
48
+ normal: "agentflow-node-executor",
49
+ 求拆解: "agentflow-node-executor-requirement",
50
+ 技术规划: "agentflow-node-executor-planning",
51
+ 代码执行: "agentflow-node-executor-code",
52
+ 测试回归: "agentflow-node-executor-test",
53
+ 普通: "agentflow-node-executor",
54
+ };
55
+
56
+ function readFlowJson(workspaceRoot, flowName, uuid) {
57
+ const flowJsonPath = path.join(getRunDir(workspaceRoot, flowName, uuid), "intermediate", "flow.json");
58
+ if (!fs.existsSync(flowJsonPath)) return null;
59
+ try {
60
+ const flow = JSON.parse(fs.readFileSync(flowJsonPath, "utf-8"));
61
+ return flow?.ok && Array.isArray(flow.nodes) ? flow : null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function getRoleAndModelFromFlowJson(workspaceRoot, flowName, uuid, instanceId) {
68
+ const flow = readFlowJson(workspaceRoot, flowName, uuid);
69
+ if (!flow || !Array.isArray(flow.nodes)) {
70
+ return { role: "普通", model: null };
71
+ }
72
+ const node = flow.nodes.find((n) => n.id === instanceId) || null;
73
+ const roleRaw = node && node.role != null ? String(node.role).trim() : "";
74
+ const modelRaw = node && node.model != null ? String(node.model).trim() : "";
75
+ const role = roleRaw || "普通";
76
+ const model = modelRaw || null;
77
+ return { role, model };
78
+ }
79
+
80
+ /** 从 flow.json 读取节点 definitionId,优先 node.definitionId,回退 nodeDefinitions[instanceId](start/end 等可据此做本地跳过) */
81
+ function getDefinitionIdFromFlowJson(workspaceRoot, flowName, uuid, instanceId) {
82
+ const flow = readFlowJson(workspaceRoot, flowName, uuid);
83
+ if (!flow) return null;
84
+ const node = flow.nodes.find((n) => n.id === instanceId);
85
+ return node?.definitionId ?? flow.nodeDefinitions?.[instanceId] ?? null;
86
+ }
87
+
88
+ /**
89
+ * 统一:根据当前 prompt 文件 + resolvedInputs + 上游 cache 算 MD5 并写 intermediate/<instanceId>/<instanceId>.cache.json。
90
+ * 调用前需保证 buildNodePrompt 已执行(prompt 文件已存在)。control_if 与普通节点共用此流程。
91
+ */
92
+ function writeCacheJsonForNode(workspaceRoot, flowName, uuid, instanceId, execId) {
93
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
94
+ const cache = computeCacheMd5(workspaceRoot, flowName, uuid, instanceId, execId);
95
+ if (!cache.ok || (!cache.cacheMd5 && !cache.cacheInputInfo)) return;
96
+ const nodeIntermediateDir = path.join(runDir, intermediateDirForNode(instanceId));
97
+ fs.mkdirSync(nodeIntermediateDir, { recursive: true });
98
+ const cachePath = path.join(nodeIntermediateDir, intermediateCacheBasename(instanceId, execId));
99
+ const cacheObj = {
100
+ cacheMd5: cache.cacheMd5,
101
+ cacheInputInfo: cache.cacheInputInfo,
102
+ execId,
103
+ };
104
+ if (cache.inputHandlerExecIds != null && Object.keys(cache.inputHandlerExecIds).length > 0) {
105
+ cacheObj.inputHandlerExecIds = cache.inputHandlerExecIds;
106
+ }
107
+ if (cache.payload !== undefined) cacheObj.payload = cache.payload;
108
+ // 备份由 snapshotPriorRoundIfNeeded 统一在 pre-process 入口完成;此处只管写新 cache。
109
+ fs.writeFileSync(cachePath, JSON.stringify(cacheObj, null, 0), "utf-8");
110
+ logToRunTag(workspaceRoot, flowName, uuid, "pre-process", {
111
+ event: "cache-written",
112
+ instanceId,
113
+ cacheMd5: cache.cacheMd5,
114
+ cachePath: path.join(intermediateDirForNode(instanceId), intermediateCacheBasename(instanceId, execId)),
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Bash 单引号包裹任意参数:单引号内无命令替换/变量展开,避免 save_key 的 value 含反引号、`$()`、换行时把整段当 shell 执行。
120
+ * 用法:'it'\''s' 表示 it's
121
+ */
122
+ function bashSingleQuote(s) {
123
+ if (s == null) return "''";
124
+ return "'" + String(s).replace(/'/g, "'\\''") + "'";
125
+ }
126
+
127
+ /**
128
+ * 若为 tool_load_key / tool_save_key / tool_get_env,写入「直接执行 agentflow apply -ai run-tool-nodejs + 对应脚本」的 prompt,
129
+ * key/value 从 getResolvedValues 的 resolvedInputs 读取并拼入命令。
130
+ * 返回 { optionalPromptPath, directCommand },供 AI 用 optionalPromptPath、CLI 用 directCommand 执行。
131
+ * @param {number} execId - 本轮 execId,传入 run-tool-nodejs 以写对 result 文件(第二轮起必须)
132
+ */
133
+ function emitLoadSaveKeyOptionalPrompt(workspaceRoot, flowName, uuid, instanceId, definitionId, execId) {
134
+ if (definitionId !== "tool_load_key" && definitionId !== "tool_save_key" && definitionId !== "tool_get_env") return null;
135
+ const scriptName =
136
+ definitionId === "tool_load_key" ? "load-key.mjs"
137
+ : definitionId === "tool_save_key" ? "save-key.mjs"
138
+ : "get-env.mjs";
139
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
140
+ const nodeIntermediateDir = path.join(runDir, intermediateDirForNode(instanceId));
141
+ const promptFileName = `${instanceId}.run-key.prompt.md`;
142
+ const promptPath = path.join(nodeIntermediateDir, promptFileName);
143
+
144
+ let key = "";
145
+ let value = "";
146
+ const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
147
+ if (data.ok && data.resolvedInputs) {
148
+ const inputs = data.resolvedInputs;
149
+ key = inputs.key != null ? String(inputs.key).trim() : "";
150
+ value = inputs.value != null ? String(inputs.value).trim() : "";
151
+ }
152
+
153
+ const rootArg = workspaceRoot;
154
+ const q = bashSingleQuote;
155
+ const keyQ = q(key);
156
+ const directCommand =
157
+ definitionId === "tool_get_env"
158
+ ? `agentflow apply -ai get-env ${q(rootArg)} ${q(flowName)} ${q(uuid)} ${q(instanceId)} ${q(String(execId))} ${keyQ}`
159
+ : (() => {
160
+ const scriptArgs =
161
+ definitionId === "tool_load_key"
162
+ ? `${q(rootArg)} ${q(flowName)} ${q(uuid)} ${keyQ}`
163
+ : `${q(rootArg)} ${q(flowName)} ${q(uuid)} ${keyQ} ${q(value)}`;
164
+ const scriptPath = path.join(__dirname, definitionId === "tool_load_key" ? "load-key.mjs" : "save-key.mjs");
165
+ return `agentflow apply -ai run-tool-nodejs ${q(rootArg)} ${q(flowName)} ${q(uuid)} ${q(instanceId)} ${q(String(execId))} -- node ${q(scriptPath)} ${scriptArgs}`;
166
+ })();
167
+ const content = `此节点不调用 subagent,请主 agent 在工作区根目录直接执行以下命令完成该节点。
168
+
169
+ \`\`\`bash
170
+ ${directCommand}
171
+ \`\`\`
172
+ `;
173
+
174
+ try {
175
+ fs.mkdirSync(nodeIntermediateDir, { recursive: true });
176
+ // 备份由 snapshotPriorRoundIfNeeded 统一处理
177
+ fs.writeFileSync(promptPath, content, "utf-8");
178
+ } catch (e) {
179
+ return null;
180
+ }
181
+ const relativePath = path.relative(workspaceRoot, promptPath);
182
+ return { optionalPromptPath: relativePath.replace(/\\/g, "/"), directCommand };
183
+ }
184
+
185
+ /**
186
+ * 若为 tool_nodejs 且 buildNodePrompt 返回了非空 script(来自 flow.yaml instance.script 字段),
187
+ * 生成 directCommand 直接通过 run-tool-nodejs 执行脚本,不调用 subagent。
188
+ * @param {string} resolvedScript - 已解析占位符且各参数已 shell-quote 的命令(run-tool-nodejs -- 之后的部分)
189
+ * @returns {{ optionalPromptPath: string, directCommand: string } | null}
190
+ */
191
+ function emitToolNodejsDirectCommand(workspaceRoot, flowName, uuid, instanceId, resolvedScript, execId) {
192
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
193
+ const nodeIntermediateDir = path.join(runDir, intermediateDirForNode(instanceId));
194
+ const promptFileName = `${instanceId}.tool-nodejs-direct.prompt.md`;
195
+ const promptPath = path.join(nodeIntermediateDir, promptFileName);
196
+
197
+ const q = bashSingleQuote;
198
+ const directCommand = `agentflow apply -ai run-tool-nodejs ${q(workspaceRoot)} ${q(flowName)} ${q(uuid)} ${q(instanceId)} ${q(String(execId))} -- ${resolvedScript}`;
199
+ const content = `此节点为 tool_nodejs(直接执行模式),不调用 subagent,由流水线直接执行以下命令。
200
+
201
+ \`\`\`bash
202
+ ${directCommand}
203
+ \`\`\`
204
+ `;
205
+
206
+ try {
207
+ fs.mkdirSync(nodeIntermediateDir, { recursive: true });
208
+ // 备份由 snapshotPriorRoundIfNeeded 统一处理
209
+ fs.writeFileSync(promptPath, content, "utf-8");
210
+ } catch (e) {
211
+ return null;
212
+ }
213
+ const relativePath = path.relative(workspaceRoot, promptPath);
214
+ return { optionalPromptPath: relativePath.replace(/\\/g, "/"), directCommand };
215
+ }
216
+
217
+ /**
218
+ * 若为 control_anyOne,写入「直接执行 write-result 将该节点标为 success」的 prompt,不调用 subagent。
219
+ * 返回 { optionalPromptPath, directCommand },供 AI 用 optionalPromptPath、CLI 用 directCommand 执行。
220
+ */
221
+ function emitAnyOneOptionalPrompt(workspaceRoot, flowName, uuid, instanceId, execId) {
222
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
223
+ const nodeIntermediateDir = path.join(runDir, intermediateDirForNode(instanceId));
224
+ const promptFileName = `${instanceId}.anyOne.prompt.md`;
225
+ const promptPath = path.join(nodeIntermediateDir, promptFileName);
226
+
227
+ const jsonPayload = JSON.stringify({
228
+ status: "success",
229
+ message: "任一前驱已就绪,直接通过",
230
+ execId,
231
+ });
232
+ const directCommand = `agentflow apply -ai write-result ${bashSingleQuote(workspaceRoot)} ${bashSingleQuote(flowName)} ${bashSingleQuote(uuid)} ${bashSingleQuote(instanceId)} --json ${bashSingleQuote(jsonPayload)}`;
233
+ const content = `此节点为 control_anyOne,不调用 subagent。请主 agent 在工作区根目录直接执行以下命令将该节点标记为 success。
234
+
235
+ \`\`\`bash
236
+ ${directCommand}
237
+ \`\`\`
238
+ `;
239
+
240
+ try {
241
+ fs.mkdirSync(nodeIntermediateDir, { recursive: true });
242
+ // 备份由 snapshotPriorRoundIfNeeded 统一处理
243
+ fs.writeFileSync(promptPath, content, "utf-8");
244
+ } catch (e) {
245
+ return null;
246
+ }
247
+ const relativePath = path.relative(workspaceRoot, promptPath);
248
+ return { optionalPromptPath: relativePath.replace(/\\/g, "/"), directCommand };
249
+ }
250
+
251
+ function main() {
252
+ const args = process.argv.slice(2);
253
+ if (args.length < 4) {
254
+ console.error(
255
+ JSON.stringify({
256
+ ok: false,
257
+ error:
258
+ "Usage: node pre-process-node.mjs <workspaceRoot> <flowName> <uuid> <instanceId>",
259
+ }),
260
+ );
261
+ process.exit(1);
262
+ }
263
+
264
+ const [root, flowName, uuid, instanceId] = args;
265
+ const workspaceRoot = path.resolve(root);
266
+
267
+ let execId = 1;
268
+ let priorExecId = 0;
269
+ const loadKeyPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "load-key.mjs");
270
+ const execIdKey = "execId_" + instanceId;
271
+ // load-key.mjs 参数约定:<workspaceRoot> <flowName> <uuid> <key>。
272
+ // 缺 flowName 会导致 getRunDir 拼成错误路径,memory 永远读不到 → execId 每轮都回到 1,
273
+ // snapshotPriorRoundIfNeeded 也永远 no-op,loop 跑多少轮 sidebar 都只有 #1/#2。
274
+ const loadResult = spawnSync(process.execPath, [loadKeyPath, workspaceRoot, flowName, uuid, execIdKey], {
275
+ cwd: workspaceRoot,
276
+ encoding: "utf-8",
277
+ });
278
+ if (loadResult.stdout) {
279
+ try {
280
+ const out = JSON.parse(loadResult.stdout.trim());
281
+ const result = out?.message?.result;
282
+ if (result !== undefined && result !== "") {
283
+ const current = parseInt(String(result), 10) || 0;
284
+ priorExecId = current;
285
+ execId = current + 1;
286
+ }
287
+ } catch (_) {}
288
+ }
289
+
290
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
291
+
292
+ // 唯一备份入口:把上一轮的 intermediate/output 文件统一 rename 为 _<priorExecId>。
293
+ // 之后任何 writer(write-result / build-node-prompt / run-tool-nodejs / get-env 等)
294
+ // 都不再负责备份,只管对 current 路径写入新内容。
295
+ snapshotPriorRoundIfNeeded(runDir, instanceId, priorExecId);
296
+
297
+ const resultPathRel = `${intermediateDirForNode(instanceId)}/${intermediateResultBasename(instanceId, execId)}`;
298
+
299
+ /** control_if:不执行 subagent,根据第一个 bool 类型输入直接写 result 并返回 optionalPromptPath */
300
+ const definitionId = getDefinitionIdFromFlowJson(workspaceRoot, flowName, uuid, instanceId);
301
+ if (definitionId === "control_if") {
302
+ const flow = readFlowJson(workspaceRoot, flowName, uuid);
303
+ const inputSlotTypes = (flow?.inputSlotTypes && flow.inputSlotTypes[instanceId]) || null;
304
+ const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
305
+ if (!data.ok || !data.resolvedInputs) {
306
+ console.error(JSON.stringify({ ok: false, error: "control_if: getResolvedValues failed or no resolvedInputs" }));
307
+ process.exit(1);
308
+ }
309
+ const rawVal = getFirstBoolInputValue(data.resolvedInputs, inputSlotTypes);
310
+ if (rawVal == null) {
311
+ console.error(JSON.stringify({ ok: false, error: "control_if: no bool-type input slot found" }));
312
+ process.exit(1);
313
+ }
314
+ let boolValue;
315
+ let filePath = null;
316
+ if (rawVal.startsWith("output/") || rawVal.startsWith("intermediate/")) {
317
+ filePath = path.join(runDir, rawVal);
318
+ } else if (path.isAbsolute(rawVal)) {
319
+ filePath = rawVal;
320
+ }
321
+ if (filePath) {
322
+ if (fs.existsSync(filePath)) {
323
+ boolValue = parseBool(fs.readFileSync(filePath, "utf-8").trim());
324
+ } else {
325
+ // 查找 snapshotPriorRoundIfNeeded 创建的 _N 备份文件
326
+ const dir = path.dirname(filePath);
327
+ const ext = path.extname(filePath);
328
+ const base = path.basename(filePath, ext);
329
+ let found = false;
330
+ try {
331
+ if (fs.existsSync(dir)) {
332
+ const candidates = fs.readdirSync(dir).filter(f =>
333
+ f.startsWith(base + "_") && f.endsWith(ext) &&
334
+ /^\d+$/.test(f.slice(base.length + 1, -ext.length))
335
+ );
336
+ if (candidates.length > 0) {
337
+ candidates.sort((a, b) => {
338
+ const na = parseInt(a.slice(base.length + 1, -ext.length), 10);
339
+ const nb = parseInt(b.slice(base.length + 1, -ext.length), 10);
340
+ return nb - na;
341
+ });
342
+ boolValue = parseBool(fs.readFileSync(path.join(dir, candidates[0]), "utf-8").trim());
343
+ found = true;
344
+ }
345
+ }
346
+ } catch (_) {}
347
+ if (!found) {
348
+ console.error(JSON.stringify({ ok: false, error: `control_if: bool input file not found: ${rawVal}` }));
349
+ process.exit(1);
350
+ }
351
+ }
352
+ } else {
353
+ boolValue = parseBool(rawVal);
354
+ }
355
+ const branch = boolValue ? "true" : "false";
356
+ writeResult(workspaceRoot, flowName, uuid, instanceId, { status: "success", message: `分支 ${branch}`, branch }, { execId });
357
+ const nodeIntermediateDir = path.join(runDir, intermediateDirForNode(instanceId));
358
+ fs.mkdirSync(nodeIntermediateDir, { recursive: true });
359
+ const build = buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execId);
360
+ if (build.ok) writeCacheJsonForNode(workspaceRoot, flowName, uuid, instanceId, execId);
361
+ const noopPromptPath = path.join(nodeIntermediateDir, `${instanceId}.control_if_noop.prompt.md`);
362
+ // 备份由 snapshotPriorRoundIfNeeded 统一处理
363
+ fs.writeFileSync(
364
+ noopPromptPath,
365
+ "此节点为 **control_if**,已由预处理根据 bool 输入直接写入 result,无需执行任何操作。",
366
+ "utf-8",
367
+ );
368
+ const optionalPromptPath = path.relative(workspaceRoot, noopPromptPath).replace(/\\/g, "/");
369
+ const output = {
370
+ ok: true,
371
+ promptPath: optionalPromptPath,
372
+ resultPath: resultPathRel,
373
+ execId,
374
+ subagent: "agentflow-node-executor",
375
+ optionalPromptPath,
376
+ definitionId,
377
+ };
378
+ logToRunTag(workspaceRoot, flowName, uuid, "pre-process", { event: "control_if-direct-write", instanceId, branch });
379
+ console.log(JSON.stringify(output));
380
+ return;
381
+ }
382
+
383
+ const data = buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execId);
384
+ if (!data.ok) {
385
+ console.error(JSON.stringify({ ok: false, error: data.error || "build-node-prompt failed" }));
386
+ process.exit(1);
387
+ }
388
+
389
+ const { role, model } = getRoleAndModelFromFlowJson(workspaceRoot, flowName, uuid, instanceId);
390
+ const subagent = ROLE_TO_SUBAGENT[role] ?? (role && String(role).trim() ? String(role).trim() : ROLE_TO_SUBAGENT.普通);
391
+
392
+ const intermediateDir = path.join(runDir, "intermediate");
393
+
394
+ writeResult(workspaceRoot, flowName, uuid, instanceId, { status: "running", message: "执行中" }, { preserveBody: false, execId });
395
+ logToRunTag(workspaceRoot, flowName, uuid, "pre-process", {
396
+ event: "result-running",
397
+ instanceId,
398
+ resultPath: resultPathRel,
399
+ });
400
+
401
+ writeCacheJsonForNode(workspaceRoot, flowName, uuid, instanceId, execId);
402
+
403
+ const output = {
404
+ ok: true,
405
+ promptPath: data.promptPath,
406
+ nodeContext: data.nodeContext ?? "",
407
+ taskBody: data.taskBody ?? "",
408
+ resultPath: resultPathRel,
409
+ execId,
410
+ subagent,
411
+ definitionId,
412
+ role,
413
+ };
414
+ if (model) output.model = model;
415
+ if (data.optionalPromptPath) {
416
+ output.optionalPromptPath = data.optionalPromptPath;
417
+ }
418
+ const runKeyResult = emitLoadSaveKeyOptionalPrompt(workspaceRoot, flowName, uuid, instanceId, definitionId, execId);
419
+ if (runKeyResult) {
420
+ output.optionalPromptPath = runKeyResult.optionalPromptPath;
421
+ output.directCommand = runKeyResult.directCommand;
422
+ } else if (definitionId === "control_anyOne") {
423
+ const anyOneResult = emitAnyOneOptionalPrompt(workspaceRoot, flowName, uuid, instanceId, execId);
424
+ if (anyOneResult) {
425
+ output.optionalPromptPath = anyOneResult.optionalPromptPath;
426
+ output.directCommand = anyOneResult.directCommand;
427
+ }
428
+ } else if ((definitionId === "tool_nodejs" || definitionId === "control_toBool") && data.script) {
429
+ const toolNodejsResult = emitToolNodejsDirectCommand(workspaceRoot, flowName, uuid, instanceId, data.script, execId);
430
+ if (toolNodejsResult) {
431
+ output.optionalPromptPath = toolNodejsResult.optionalPromptPath;
432
+ output.directCommand = toolNodejsResult.directCommand;
433
+ output.resolvedScript = data.script;
434
+ }
435
+ }
436
+ logToRunTag(workspaceRoot, flowName, uuid, "pre-process", {
437
+ event: "done",
438
+ instanceId,
439
+ promptPath: data.promptPath,
440
+ resultPath: resultPathRel,
441
+ subagent,
442
+ definitionId,
443
+ hasOptionalPrompt: !!output.optionalPromptPath,
444
+ hasDirectCommand: !!output.directCommand,
445
+ });
446
+ console.log(JSON.stringify(output));
447
+ }
448
+
449
+ main();
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 按 flow 结构 + 中间文件为单个 instance 计算 resolvedInputs(不注入常量;上游 output 路径已带对应 input handler 的 execId)。
4
+ * 供 get-resolved-values 等用时现算,不依赖 flow.json 中的 resolvedInputs。
5
+ * 约定:上游节点的 output 槽路径使用该前驱的 latestResultExecId,即 output/<predId>/node_<predId>_<execId>_<slot>.md。
6
+ * 用法(模块):import { computeResolvedInputsForInstance } from "./resolve-inputs.mjs";
7
+ * @returns {{ ok: true, resolvedInputs: object }} 或 {{ ok: false, error: string }}
8
+ */
9
+
10
+ import fs from "fs";
11
+ import path from "path";
12
+
13
+ import { getRunDir, PIPELINES_DIR } from "../lib/paths.mjs";
14
+ import { getFlowDir } from "../lib/workspace.mjs";
15
+ import {
16
+ loadAllExecIds,
17
+ latestResultExecId,
18
+ intermediateResultBasename,
19
+ outputDirForNode,
20
+ outputNodeBasename,
21
+ } from "./get-exec-id.mjs";
22
+ import {
23
+ loadFlowDefinition,
24
+ parseResultOutputPath,
25
+ instanceEntryToSlots,
26
+ parseInstanceSlots,
27
+ readInstanceContent,
28
+ extractPlaceholders,
29
+ } from "./parse-flow.mjs";
30
+
31
+ /**
32
+ * 为指定 instanceId 计算 resolvedInputs(与 parse-flow resolvePlaceholders 单实例逻辑一致)。
33
+ */
34
+ export function computeResolvedInputsForInstance(workspaceRoot, flowName, uuid, instanceId) {
35
+ const root = path.resolve(workspaceRoot);
36
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
37
+ const flowJsonPath = path.join(runDir, "intermediate", "flow.json");
38
+ const intermediateDir = path.join(runDir, "intermediate");
39
+
40
+ if (!fs.existsSync(flowJsonPath)) {
41
+ return { ok: false, error: `flow.json not found: ${flowJsonPath}. Run parse-flow.mjs first.` };
42
+ }
43
+
44
+ let flow;
45
+ try {
46
+ flow = JSON.parse(fs.readFileSync(flowJsonPath, "utf-8"));
47
+ } catch (e) {
48
+ return { ok: false, error: e.message || "Invalid flow.json" };
49
+ }
50
+ if (!flow.ok) {
51
+ return { ok: false, error: flow.error || "flow.json indicates error" };
52
+ }
53
+
54
+ let flowDir = getFlowDir(root, flowName) || path.join(root, PIPELINES_DIR, flowName);
55
+ if (flow.flowDir && typeof flow.flowDir === "string" && flow.flowDir.trim()) {
56
+ flowDir = path.isAbsolute(flow.flowDir) ? flow.flowDir : path.join(root, flow.flowDir);
57
+ }
58
+ const instanceDir = path.join(flowDir, "instance");
59
+
60
+ const order = flow.order || [];
61
+ const edges = flow.edges || [];
62
+ if (!order.length) {
63
+ return { ok: false, error: "flow.json missing order" };
64
+ }
65
+
66
+ if (!order.includes(instanceId)) {
67
+ return { ok: false, error: `instanceId ${instanceId} not in flow order` };
68
+ }
69
+
70
+ const flowData = loadFlowDefinition(flowDir);
71
+ const instances = flowData?.instances && typeof flowData.instances === "object" ? flowData.instances : {};
72
+ const useYaml = Object.keys(instances).length > 0;
73
+
74
+ let execIds = {};
75
+ if (fs.existsSync(intermediateDir)) {
76
+ try {
77
+ execIds = loadAllExecIds(workspaceRoot, flowName, uuid, order);
78
+ } catch (_) {}
79
+ }
80
+
81
+ /** 入边:target -> [{ source, sourceHandle, targetHandle }] */
82
+ const predecessors = new Map();
83
+ for (const e of edges) {
84
+ if (!e.target) continue;
85
+ if (!predecessors.has(e.target)) predecessors.set(e.target, []);
86
+ predecessors.get(e.target).push({
87
+ source: e.source,
88
+ sourceHandle: e.sourceHandle || "output-0",
89
+ targetHandle: e.targetHandle,
90
+ });
91
+ }
92
+
93
+ const getSlotsFor = (id) => {
94
+ if (useYaml && instances[id]) return instanceEntryToSlots(instances[id]);
95
+ const instPath = path.join(instanceDir, `${id}.md`);
96
+ return fs.existsSync(instPath) ? parseInstanceSlots(instPath) : instanceEntryToSlots(null);
97
+ };
98
+
99
+ const getContentFor = (id) => {
100
+ if (useYaml && instances[id] && instances[id].body != null) return String(instances[id].body || "").trim();
101
+ return readInstanceContent(path.join(instanceDir, `${id}.md`));
102
+ };
103
+
104
+ const getNodeOutput = (id, sourceHandle) => {
105
+ const slots = getSlotsFor(id);
106
+ const outValues = Object.values(slots.output || {});
107
+ if (sourceHandle && /^output-(\d+)$/.test(sourceHandle)) {
108
+ const idx = parseInt(sourceHandle.replace("output-", ""), 10);
109
+ if (idx >= 0 && idx < outValues.length) return outValues[idx] ?? null;
110
+ }
111
+ return outValues[0] ?? null;
112
+ };
113
+
114
+ const getNodeOutputFromResult = (id) => {
115
+ const latestE = latestResultExecId(execIds[id] ?? 1);
116
+ if (!latestE) return null;
117
+ const resultPath = path.join(intermediateDir, id, intermediateResultBasename(id, latestE));
118
+ return parseResultOutputPath(resultPath);
119
+ };
120
+
121
+ const resolvedInputs = {};
122
+ const slots = getSlotsFor(instanceId);
123
+ const { inputs: inputRefs } = extractPlaceholders(getContentFor(instanceId));
124
+ const preds = predecessors.get(instanceId) || [];
125
+ const inputSlotNames = Object.keys(slots.input || {});
126
+
127
+ for (let i = 0; i < inputSlotNames.length; i++) {
128
+ const slotName = inputSlotNames[i];
129
+ const targetHandle = `input-${i}`;
130
+ const pred = preds.find((p) => p.targetHandle === targetHandle);
131
+ if (!pred) continue;
132
+
133
+ const fromResult = getNodeOutputFromResult(pred.source);
134
+ const fromInstance = getNodeOutput(pred.source, pred.sourceHandle);
135
+ let value = fromResult ?? fromInstance;
136
+
137
+ if (pred.sourceHandle && /^output-(\d+)$/.test(pred.sourceHandle)) {
138
+ const sourceSlots = getSlotsFor(pred.source);
139
+ const outSlotNames = Object.keys(sourceSlots.output || {});
140
+ if (outSlotNames.length > 0) {
141
+ const idx = parseInt(pred.sourceHandle.replace("output-", ""), 10);
142
+ if (idx >= 0 && idx < outSlotNames.length) {
143
+ const predExecId = latestResultExecId(execIds[pred.source] ?? 1);
144
+ const slotPath = `${outputDirForNode(pred.source)}/${outputNodeBasename(pred.source, predExecId, outSlotNames[idx])}`;
145
+ if (!value || value === "" || outSlotNames.length > 1) {
146
+ value = slotPath;
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ if (value != null) {
153
+ const slotType = (slots.inputTypes && slots.inputTypes[slotName]) || null;
154
+ if (slotType === "节点" || slotType === "node") {
155
+ value = pred.source;
156
+ }
157
+ resolvedInputs[slotName] = value;
158
+ }
159
+ }
160
+
161
+ for (const slotName of inputSlotNames) {
162
+ if (resolvedInputs[slotName] != null) continue;
163
+ const defaultVal = slots.input && slots.input[slotName];
164
+ if (defaultVal !== undefined && defaultVal !== "") {
165
+ resolvedInputs[slotName] = defaultVal;
166
+ }
167
+ }
168
+
169
+ // --input 覆盖:仅当对应上游是 provide_ 节点,且用户传了该 slotName 的值时生效。
170
+ // parse-flow 初始阶段也会写入 flow.json 的 cliInputsApplied,但此处在运行时重算 resolvedInputs,
171
+ // 必须在同一处再打一次 patch,否则 override 会丢失(provide 节点原 default 值会覆盖用户输入)。
172
+ if (flow.cliInputsApplied && typeof flow.cliInputsApplied === "object") {
173
+ for (let i = 0; i < inputSlotNames.length; i++) {
174
+ const slotName = inputSlotNames[i];
175
+ const cliVal = flow.cliInputsApplied[slotName];
176
+ if (!cliVal || typeof cliVal !== "object") continue;
177
+ const pred = preds.find((p) => p.targetHandle === `input-${i}`);
178
+ if (!pred?.source) continue;
179
+ const sourceInst = instances[pred.source];
180
+ if (!sourceInst?.definitionId?.startsWith?.("provide_")) continue;
181
+ const overrideValue = cliVal.type === "file" ? cliVal.path : cliVal.value;
182
+ if (overrideValue == null) continue;
183
+ resolvedInputs[slotName] = overrideValue;
184
+ }
185
+ }
186
+
187
+ if (inputRefs.length > 0 && preds.length > 0 && !Object.keys(resolvedInputs).length) {
188
+ const pred = preds[0];
189
+ const fromResult = getNodeOutputFromResult(pred.source);
190
+ const fromInstance = getNodeOutput(pred.source, pred.sourceHandle);
191
+ let v = fromResult ?? fromInstance;
192
+ if (v != null) {
193
+ const firstSlotName = inputSlotNames[0];
194
+ const firstType = (slots.inputTypes && slots.inputTypes[firstSlotName]) || null;
195
+ if (firstType === "节点" || firstType === "node") v = pred.source;
196
+ return { ok: true, resolvedInputs: { _: v } };
197
+ }
198
+ }
199
+
200
+ return { ok: true, resolvedInputs };
201
+ }
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 向 run 目录下的 logs/log.txt 追加日志(单文件、追加模式),便于定位 get-ready-nodes、cache、result 等问题。
4
+ * 用法(模块):import { logToRunTag } from "./run-log.mjs";
5
+ * logToRunTag(workspaceRoot, flowName, uuid, tag, message)
6
+ * 日志写入:<workspaceRoot>/.workspace/agentflow/runBuild/<flowName>/<uuid>/logs/log.txt,每行 [ISO8601] [tag] message
7
+ */
8
+
9
+ import fs from "fs";
10
+ import path from "path";
11
+
12
+ import { getRunDir } from "../lib/paths.mjs";
13
+
14
+ const LOG_FILE = "logs/log.txt";
15
+
16
+ /**
17
+ * 向 run 目录下的 logs/log.txt 追加一行日志(单文件、追加模式)。
18
+ * @param {string} workspaceRoot - 工作区根目录
19
+ * @param {string} flowName - 流程名
20
+ * @param {string} uuid - 本次 run 的 uuid
21
+ * @param {string} tag - 来源标识:get-ready-nodes | check-cache | pre-process | post-process | result
22
+ * @param {string|object} message - 文本或对象(对象会 JSON.stringify)
23
+ */
24
+ export function logToRunTag(workspaceRoot, flowName, uuid, tag, message) {
25
+ if (!workspaceRoot || !flowName || !uuid) return;
26
+ const text = typeof message === "string" ? message : JSON.stringify(message);
27
+ try {
28
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
29
+ const logPath = path.join(runDir, LOG_FILE);
30
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
31
+ const line = `[${new Date().toISOString()}] [${tag}] ${text}\n`;
32
+ fs.appendFileSync(logPath, line, "utf-8");
33
+ } catch (_) {}
34
+ }