@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,23 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 已移除对 control_if_true、control_if_false 的支持;分支判断仅由 control_if 写 branch,get-ready-nodes 按 branch 解锁后继。
4
+ * 本脚本保留为 no-op,避免主流程调用时报错。
5
+ * 用法:node post-process-control-if.mjs <workspaceRoot> <flowName> <uuid> <instanceId>
6
+ * 输出(stdout JSON):{ "ok": true }
7
+ */
8
+
9
+ function main() {
10
+ const args = process.argv.slice(2);
11
+ if (args.length < 4) {
12
+ console.error(
13
+ JSON.stringify({
14
+ ok: false,
15
+ error: "Usage: node post-process-control-if.mjs <workspaceRoot> <flowName> <uuid> <instanceId>",
16
+ }),
17
+ );
18
+ process.exit(1);
19
+ }
20
+ console.log(JSON.stringify({ ok: true }));
21
+ }
22
+
23
+ main();
@@ -0,0 +1,490 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 统一后处理入口:对每个已执行完的节点运行一次。
4
+ * 1) 若存在本轮的 result 文件(AI/subagent 所写),先规范化 frontmatter 与正文后写回;
5
+ * 2) 再根据 definitionId 分支:control_if 写 branch,control_if_true/false 写 condition_not_met,tool_user_check/waitForUser 写 pending 等;
6
+ * 3) 若为 tool_print 则生成 optionalPromptPath 供主 agent 执行;
7
+ * 4) 最后在 memory 中将该节点 execId +1。
8
+ * 用法:node post-process-node.mjs <workspaceRoot> <flowName> <uuid> <instanceId> [execId]
9
+ * 可选 execId:本轮执行的 execId(与 pre-process 输出一致),未传则从 memory 读取。**必须传入**才能正确命中本轮的 result 文件(否则第二轮起 user_check 等不生效)。
10
+ * 输出(stdout JSON):{ "ok": true } 或 { "ok": true, "optionalPromptPath": "..." }
11
+ */
12
+
13
+ import { spawnSync } from "child_process";
14
+ import fs from "fs";
15
+ import path from "path";
16
+ import { fileURLToPath } from "url";
17
+
18
+ import { writeResult } from "./write-result.mjs";
19
+ import { loadExecId, intermediateResultBasename, intermediateDirForNode } from "./get-exec-id.mjs";
20
+ import { getResolvedValues, getOutputPathForSlot } from "./get-resolved-values.mjs";
21
+ import { loadFlowDefinition } from "./parse-flow.mjs";
22
+ import { parseBool, getFirstBoolInputValue } from "./parse-bool.mjs";
23
+ import { logToRunTag } from "./run-log.mjs";
24
+ import { emitEvent } from "../lib/run-events.mjs";
25
+ import { getRunDir, PIPELINES_DIR } from "../lib/paths.mjs";
26
+ import { getFlowDir } from "../lib/workspace.mjs";
27
+
28
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
+ const SAVE_KEY = path.join(__dirname, "save-key.mjs");
30
+ const EXEC_ID_KEY_PREFIX = "execId_";
31
+
32
+ /**
33
+ * 将本 run 内指定节点「本轮已完成的 execId」写入 memory.md。
34
+ * 语义:memory 存「上一轮已完成的 execId」,pre-process 用 execId = (memory || 0) + 1。
35
+ * 因此 post 只需把当前 execId 原样写回,不再 +1(历史上此处错误地 +1 会让 execId 跳号 1,3,5…,
36
+ * 导致 snapshotPriorRoundIfNeeded 生成的 backup 文件稀疏、sidebar 历史也跟着错乱)。
37
+ * @param {string} workspaceRoot
38
+ * @param {string} uuid
39
+ * @param {string} instanceId
40
+ * @param {number} [currentExecId] 本轮执行的 execId;未传则从 memory 读取后原样写回(兼容)
41
+ * @returns {string} 写入后的 execId(字符串)
42
+ */
43
+ export function incrementExecIdInMemory(workspaceRoot, flowName, uuid, instanceId, currentExecId) {
44
+ const current = currentExecId ?? loadExecId(workspaceRoot, flowName, uuid, instanceId);
45
+ const toSave = String(current);
46
+ const save = spawnSync(
47
+ process.execPath,
48
+ [SAVE_KEY, path.resolve(workspaceRoot), flowName, uuid, EXEC_ID_KEY_PREFIX + instanceId, toSave],
49
+ { cwd: path.resolve(workspaceRoot), encoding: "utf-8" },
50
+ );
51
+ if (save.status !== 0) {
52
+ throw new Error(`save-key failed: ${save.stderr || save.stdout || "unknown"}`);
53
+ }
54
+ return toSave;
55
+ }
56
+
57
+ /** 从 result.md 中解析 frontmatter 与正文(简单正则,不依赖 YAML 库) */
58
+ function parseResultFrontmatter(filePath) {
59
+ if (!fs.existsSync(filePath)) return null;
60
+ const raw = fs.readFileSync(filePath, "utf-8");
61
+ const match = raw.match(/---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n([\s\S]*)$/);
62
+ if (!match) return null;
63
+ const [, fm, body] = match;
64
+ const fields = {};
65
+ for (const line of fm.split(/\r?\n/)) {
66
+ const colon = line.indexOf(":");
67
+ if (colon <= 0) continue;
68
+ const key = line.slice(0, colon).trim();
69
+ let val = line.slice(colon + 1).trim();
70
+ if (val.startsWith('"') && val.endsWith('"'))
71
+ val = val.slice(1, -1).replace(/\\n/g, "\n").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
72
+ else if (val.startsWith("'") && val.endsWith("'")) val = val.slice(1, -1);
73
+ fields[key] = val;
74
+ }
75
+ return { ...fields, _body: body };
76
+ }
77
+
78
+ /**
79
+ * @param {number | undefined} elapsedMsFromApply 由 apply/replay 测量的本节点执行耗时(毫秒),写入 result 供 UI 展示
80
+ */
81
+ function resolveElapsedMsForWrite(parsed, elapsedMsFromApply, status) {
82
+ if (status !== "success") return undefined;
83
+ if (elapsedMsFromApply != null && Number.isFinite(elapsedMsFromApply) && elapsedMsFromApply >= 0) {
84
+ return Math.round(elapsedMsFromApply);
85
+ }
86
+ if (parsed?.elapsedMs != null) {
87
+ const n = parseInt(String(parsed.elapsedMs).replace(/\D/g, ""), 10);
88
+ if (Number.isFinite(n) && n >= 0) return n;
89
+ }
90
+ return undefined;
91
+ }
92
+
93
+ /**
94
+ * 规范化本轮的 result 文件(Agent 退出后调用)。
95
+ * - 若文件不存在或状态仍为 "running"(Agent 未主动汇报):自动写 success(成功路径免调 write-result)。
96
+ * - 若 Agent 写成纯 Markdown 无 frontmatter:补写 status: success。
97
+ * - 其余情况:规范化 completed/done → success,保留 failed/pending 等真实状态。
98
+ */
99
+ function applyExecutorResultNormalize(workspaceRoot, flowName, uuid, instanceId, execId, elapsedMsFromApply) {
100
+ const e = execId ?? loadExecId(workspaceRoot, flowName, uuid, instanceId);
101
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
102
+ const resultPath = path.join(runDir, intermediateDirForNode(instanceId), intermediateResultBasename(instanceId, e));
103
+
104
+ if (!fs.existsSync(resultPath)) {
105
+ const elapsedMs = resolveElapsedMsForWrite(null, elapsedMsFromApply, "success");
106
+ writeResult(
107
+ workspaceRoot,
108
+ flowName,
109
+ uuid,
110
+ instanceId,
111
+ {
112
+ status: "success",
113
+ message: "已完成",
114
+ ...(elapsedMs != null ? { elapsedMs } : {}),
115
+ },
116
+ { execId: e },
117
+ );
118
+ return;
119
+ }
120
+
121
+ const parsed = parseResultFrontmatter(resultPath);
122
+ let body;
123
+ let status;
124
+ let message;
125
+ let finishedAt;
126
+ let outputPath;
127
+ let branch;
128
+
129
+ if (parsed) {
130
+ body = parsed._body ?? "";
131
+ if (parsed.status === "running" || parsed.status === "completed" || parsed.status === "done") {
132
+ status = "success";
133
+ message = parsed.status === "running" ? "已完成" : (parsed.message ?? "已完成");
134
+ } else {
135
+ status = parsed.status ?? "success";
136
+ message = parsed.message ?? "";
137
+ }
138
+ finishedAt = parsed.finishedAt ?? new Date().toISOString();
139
+ outputPath = parsed.outputPath;
140
+ branch = parsed.branch;
141
+ } else {
142
+ const raw = fs.readFileSync(resultPath, "utf-8");
143
+ body = raw.trim();
144
+ status = "success";
145
+ message = "已完成";
146
+ finishedAt = new Date().toISOString();
147
+ outputPath = undefined;
148
+ branch = undefined;
149
+ }
150
+
151
+ const elapsedMs = resolveElapsedMsForWrite(parsed, elapsedMsFromApply, status);
152
+
153
+ writeResult(workspaceRoot, flowName, uuid, instanceId, {
154
+ status,
155
+ message,
156
+ finishedAt,
157
+ outputPath: outputPath || undefined,
158
+ branch: branch || undefined,
159
+ ...(elapsedMs != null ? { elapsedMs } : {}),
160
+ }, { body, execId: e });
161
+ }
162
+
163
+ /** 从 result.md 中解析 branch(与 get-ready-nodes 约定一致) */
164
+ function parseResultBranch(filePath) {
165
+ try {
166
+ const raw = fs.readFileSync(filePath, "utf-8");
167
+ const m = raw.match(/^\s*branch:\s*["']?([^"'\s]+)["']?/m);
168
+ return m ? m[1] : null;
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * 从 flow.json 读取 inputSlotTypes[instanceId]。
176
+ * @returns {Record<string, string>|null}
177
+ */
178
+ function readInputSlotTypes(workspaceRoot, flowName, uuid, instanceId) {
179
+ const flowJsonPath = path.join(getRunDir(workspaceRoot, flowName, uuid), "intermediate", "flow.json");
180
+ if (!fs.existsSync(flowJsonPath)) return null;
181
+ try {
182
+ const flow = JSON.parse(fs.readFileSync(flowJsonPath, "utf-8"));
183
+ const inTypes = flow?.inputSlotTypes?.[instanceId];
184
+ return inTypes && typeof inTypes === "object" ? inTypes : null;
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * control_if 后处理:必须写入 branch。
192
+ * 优先用 result 里已有的 branch;否则从 resolvedInputs 中第一个 type 为 bool 的槽位取值(名称不限)。
193
+ * 值可能是:1) 路径(如 output/xxx/...),需读文件内容再解析;2) 已是布尔内容(如 "true"/"false")。
194
+ */
195
+ function applyControlIfLogic(workspaceRoot, flowName, uuid, instanceId, definitionId, execId) {
196
+ if (definitionId !== "control_if") return;
197
+
198
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
199
+ const e = execId ?? loadExecId(workspaceRoot, flowName, uuid, instanceId);
200
+ const resultPath = path.join(runDir, intermediateDirForNode(instanceId), intermediateResultBasename(instanceId, e));
201
+
202
+ let branch = fs.existsSync(resultPath) ? parseResultBranch(resultPath) : null;
203
+ if (!branch) {
204
+ const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
205
+ if (!data.ok || !data.resolvedInputs) return;
206
+ const inputSlotTypes = readInputSlotTypes(workspaceRoot, flowName, uuid, instanceId);
207
+ const rawVal = getFirstBoolInputValue(data.resolvedInputs, inputSlotTypes);
208
+ if (rawVal == null) return;
209
+ let boolValue;
210
+ let filePath = null;
211
+ if (rawVal.startsWith("output/") || rawVal.startsWith("intermediate/")) {
212
+ filePath = path.join(runDir, rawVal);
213
+ } else if (path.isAbsolute(rawVal)) {
214
+ filePath = rawVal;
215
+ }
216
+ if (filePath) {
217
+ if (fs.existsSync(filePath)) {
218
+ boolValue = parseBool(fs.readFileSync(filePath, "utf-8").trim());
219
+ } else {
220
+ // 查找 snapshotPriorRoundIfNeeded 创建的 _N 备份文件
221
+ const dir = path.dirname(filePath);
222
+ const ext = path.extname(filePath);
223
+ const base = path.basename(filePath, ext);
224
+ let found = false;
225
+ try {
226
+ if (fs.existsSync(dir)) {
227
+ const candidates = fs.readdirSync(dir).filter(f =>
228
+ f.startsWith(base + "_") && f.endsWith(ext) &&
229
+ /^\d+$/.test(f.slice(base.length + 1, -ext.length))
230
+ );
231
+ if (candidates.length > 0) {
232
+ candidates.sort((a, b) => {
233
+ const na = parseInt(a.slice(base.length + 1, -ext.length), 10);
234
+ const nb = parseInt(b.slice(base.length + 1, -ext.length), 10);
235
+ return nb - na;
236
+ });
237
+ boolValue = parseBool(fs.readFileSync(path.join(dir, candidates[0]), "utf-8").trim());
238
+ found = true;
239
+ }
240
+ }
241
+ } catch (_) {}
242
+ if (!found) return;
243
+ }
244
+ } else {
245
+ boolValue = parseBool(rawVal);
246
+ }
247
+ branch = boolValue ? "true" : "false";
248
+ }
249
+
250
+ writeResult(workspaceRoot, flowName, uuid, instanceId, {
251
+ status: "success",
252
+ message: `分支 ${branch}`,
253
+ branch,
254
+ }, { preserveBody: true, execId: e });
255
+ }
256
+
257
+ /**
258
+ * 若为「待用户确认/选择」节点:将 result 的 status 覆写为 pending,流程暂停,等用户再次 apply 时续跑。
259
+ * 识别方式:definitionId === "tool_user_check" / "tool_user_ask" 或 flow.yaml instances[instanceId].waitForUser 为 true。
260
+ * user_check:读取 content 输入槽位内容,发送 user-check-content 事件。
261
+ * user_ask:读取 question 输入槽与 instance output 槽位(作为选项),发送 user-ask-prompt 事件。
262
+ */
263
+ function applyWaitForUserPending(workspaceRoot, flowName, uuid, instanceId, runDir, definitionId, execId, inst) {
264
+ const e = execId ?? loadExecId(workspaceRoot, flowName, uuid, instanceId);
265
+ const resultPath = path.join(runDir, intermediateDirForNode(instanceId), intermediateResultBasename(instanceId, e));
266
+ if (!fs.existsSync(resultPath)) return;
267
+
268
+ let waitForUser = false;
269
+ if (definitionId === "tool_user_check" || definitionId === "tool_user_ask") {
270
+ waitForUser = true;
271
+ } else if (inst != null && (inst.waitForUser === true || inst.waitForUser === "true" || inst.waitForUser === 1 || String(inst.waitForUser).toLowerCase() === "yes")) {
272
+ waitForUser = true;
273
+ }
274
+
275
+ if (!waitForUser) return;
276
+
277
+ const message = definitionId === "tool_user_ask" ? "等待用户选择" : "等待用户确认";
278
+ writeResult(workspaceRoot, flowName, uuid, instanceId, {
279
+ status: "pending",
280
+ message,
281
+ }, { preserveBody: true, execId: e });
282
+
283
+ if (definitionId === "tool_user_check") {
284
+ emitUserCheckContent(workspaceRoot, flowName, uuid, instanceId, runDir, e);
285
+ } else if (definitionId === "tool_user_ask") {
286
+ emitUserAskPrompt(workspaceRoot, flowName, uuid, instanceId, runDir, e, inst);
287
+ }
288
+ }
289
+
290
+ /**
291
+ * 发送 user-check-content 事件:读取 content 输入槽位内容,并告知 output 槽位路径(供编辑保存)。
292
+ */
293
+ function emitUserCheckContent(workspaceRoot, flowName, uuid, instanceId, runDir, execId) {
294
+ const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
295
+ if (!data.ok || !data.resolvedInputs) return;
296
+
297
+ const contentInputPath = data.resolvedInputs["content"];
298
+ let content = "";
299
+ let inputPath = null;
300
+
301
+ if (contentInputPath && typeof contentInputPath === "string") {
302
+ inputPath = contentInputPath;
303
+ if (fs.existsSync(contentInputPath)) {
304
+ try {
305
+ content = fs.readFileSync(contentInputPath, "utf-8");
306
+ } catch (_) {}
307
+ }
308
+ }
309
+
310
+ const outputPath = getOutputPathForSlot(instanceId, execId, "content");
311
+ const outputAbsPath = outputPath ? path.join(runDir, outputPath) : null;
312
+
313
+ if (outputAbsPath) {
314
+ try {
315
+ fs.mkdirSync(path.dirname(outputAbsPath), { recursive: true });
316
+ fs.writeFileSync(outputAbsPath, content, "utf-8");
317
+ } catch (_) {}
318
+ }
319
+
320
+ emitEvent(workspaceRoot, flowName, uuid, {
321
+ type: "user-check-content",
322
+ event: "user-check-content",
323
+ instanceId,
324
+ execId,
325
+ inputPath,
326
+ outputPath,
327
+ content,
328
+ });
329
+ }
330
+
331
+ /**
332
+ * 发送 user-ask-prompt 事件:读取 question 输入槽位内容,枚举 instance 的 output 槽位作为选项。
333
+ * 选项 label 取 output[i].description,兜底 output[i].value 或 name。
334
+ */
335
+ function emitUserAskPrompt(workspaceRoot, flowName, uuid, instanceId, runDir, execId, inst) {
336
+ // 读取 question 输入内容
337
+ let question = "";
338
+ let questionPath = null;
339
+ const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
340
+ if (data.ok && data.resolvedInputs) {
341
+ const questionInput = data.resolvedInputs["question"];
342
+ if (questionInput && typeof questionInput === "string") {
343
+ questionPath = questionInput;
344
+ if (fs.existsSync(questionInput)) {
345
+ try {
346
+ question = fs.readFileSync(questionInput, "utf-8");
347
+ } catch (_) {}
348
+ } else {
349
+ // 若不是路径,视作内联文本
350
+ question = questionInput;
351
+ }
352
+ }
353
+ }
354
+
355
+ // 枚举 instance output 槽位 → options
356
+ const options = [];
357
+ const outputs = Array.isArray(inst?.output) ? inst.output : [];
358
+ outputs.forEach((slot, idx) => {
359
+ const name = slot?.name != null ? String(slot.name) : `option_${idx}`;
360
+ const rawLabel = slot?.description || slot?.value || name;
361
+ const label = String(rawLabel || "").trim() || name;
362
+ options.push({ index: idx, name, label });
363
+ });
364
+
365
+ emitEvent(workspaceRoot, flowName, uuid, {
366
+ type: "user-ask-prompt",
367
+ event: "user-ask-prompt",
368
+ instanceId,
369
+ execId,
370
+ questionPath,
371
+ question,
372
+ options,
373
+ });
374
+ }
375
+
376
+ /**
377
+ * 若为 tool_print 节点,生成 prompt 文件并返回路径,供主 agent 执行(检查/修正 result 正文)。
378
+ * 返回 { optionalPromptPath } 相对 workspaceRoot,或 null。
379
+ */
380
+ function maybeEmitToolPrintPrompt(workspaceRoot, flowName, uuid, instanceId, runDir, definitionId, execId) {
381
+ if (definitionId !== "tool_print") return null;
382
+
383
+ const e = execId ?? loadExecId(workspaceRoot, flowName, uuid, instanceId);
384
+ const resultPathRel = `${intermediateDirForNode(instanceId)}/${intermediateResultBasename(instanceId, e)}`;
385
+ const promptContent = `请检查并必要时修正 result 文件:\`${resultPathRel}\`(相对 run 目录 \`${runDir}\`)。
386
+
387
+ 该节点为 **tool_print**。约定:输出内容必须写在 result 文档的**正文部分**(frontmatter 下方),正文直接写节点输出,不要加「醒目提醒」「内容:」等包装。若当前正文缺失或不符合,请从 output 移入或根据节点意图补充正文。
388
+ `;
389
+
390
+ const promptPath = path.join(runDir, intermediateDirForNode(instanceId), `${instanceId}_${e}.tool_print_prompt.md`);
391
+ try {
392
+ fs.mkdirSync(path.dirname(promptPath), { recursive: true });
393
+ fs.writeFileSync(promptPath, promptContent, "utf-8");
394
+ } catch (e) {
395
+ return null;
396
+ }
397
+
398
+ const relativePath = path.relative(workspaceRoot, promptPath);
399
+ return relativePath.replace(/\\/g, "/");
400
+ }
401
+
402
+ function main() {
403
+ const args = process.argv.slice(2);
404
+ if (args.length < 4) {
405
+ console.error(
406
+ JSON.stringify({
407
+ ok: false,
408
+ error:
409
+ "Usage: node post-process-node.mjs <workspaceRoot> <flowName> <uuid> <instanceId>",
410
+ }),
411
+ );
412
+ process.exit(1);
413
+ }
414
+
415
+ const [root, flowName, uuid, instanceId, execIdArg, elapsedMsArg] = args;
416
+ const workspaceRoot = path.resolve(root);
417
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
418
+ const execId = execIdArg != null && execIdArg !== ""
419
+ ? (parseInt(String(execIdArg), 10) || undefined)
420
+ : undefined;
421
+ const elapsedMsFromApply =
422
+ elapsedMsArg != null && elapsedMsArg !== ""
423
+ ? (() => {
424
+ const n = parseInt(String(elapsedMsArg), 10);
425
+ return Number.isFinite(n) && n >= 0 ? n : undefined;
426
+ })()
427
+ : undefined;
428
+
429
+ try {
430
+ let definitionId = null;
431
+ let flowData = null;
432
+ let flowDir = getFlowDir(workspaceRoot, flowName) || path.join(workspaceRoot, PIPELINES_DIR, flowName);
433
+ const flowJsonPath = path.join(runDir, "intermediate", "flow.json");
434
+ if (fs.existsSync(flowJsonPath)) {
435
+ try {
436
+ const flow = JSON.parse(fs.readFileSync(flowJsonPath, "utf-8"));
437
+ const node = flow?.nodes?.find((n) => n.id === instanceId);
438
+ if (node?.definitionId) definitionId = node.definitionId;
439
+ if (flow?.flowDir && typeof flow.flowDir === "string" && flow.flowDir.trim()) {
440
+ flowDir = path.isAbsolute(flow.flowDir) ? flow.flowDir : path.join(workspaceRoot, flow.flowDir);
441
+ }
442
+ } catch (_) {}
443
+ }
444
+ flowData = loadFlowDefinition(flowDir);
445
+ if (definitionId == null && flowData?.instances?.[instanceId]?.definitionId) {
446
+ definitionId = flowData.instances[instanceId].definitionId;
447
+ }
448
+ const inst = flowData?.instances?.[instanceId] ?? null;
449
+ logToRunTag(workspaceRoot, flowName, uuid, "post-process", {
450
+ event: "start",
451
+ instanceId,
452
+ definitionId,
453
+ execId: execId ?? loadExecId(workspaceRoot, flowName, uuid, instanceId),
454
+ });
455
+
456
+ applyExecutorResultNormalize(workspaceRoot, flowName, uuid, instanceId, execId, elapsedMsFromApply);
457
+ logToRunTag(workspaceRoot, flowName, uuid, "post-process", { event: "result-normalized", instanceId });
458
+ applyControlIfLogic(workspaceRoot, flowName, uuid, instanceId, definitionId, execId);
459
+ applyWaitForUserPending(workspaceRoot, flowName, uuid, instanceId, runDir, definitionId, execId, inst);
460
+
461
+ const optionalPromptPath = maybeEmitToolPrintPrompt(
462
+ workspaceRoot,
463
+ flowName,
464
+ uuid,
465
+ instanceId,
466
+ runDir,
467
+ definitionId,
468
+ execId,
469
+ );
470
+ const savedExecId = incrementExecIdInMemory(workspaceRoot, flowName, uuid, instanceId, execId);
471
+ logToRunTag(workspaceRoot, flowName, uuid, "post-process", {
472
+ event: "done",
473
+ instanceId,
474
+ execIdIncremented: savedExecId,
475
+ hasOptionalPrompt: !!optionalPromptPath,
476
+ });
477
+ const output = { ok: true };
478
+ if (optionalPromptPath) output.optionalPromptPath = optionalPromptPath;
479
+ console.log(JSON.stringify(output));
480
+ } catch (err) {
481
+ console.error(JSON.stringify({ ok: false, error: err.message }));
482
+ process.exit(1);
483
+ }
484
+ }
485
+
486
+ const isMain =
487
+ typeof process !== "undefined" &&
488
+ process.argv[1] &&
489
+ (process.argv[1].endsWith("post-process-node.mjs") || process.argv[1].endsWith("post-process-node.js"));
490
+ if (isMain) main();