@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,698 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 解析 flow.yaml:从 instances + edges(顶层)构建有向图并输出拓扑序,带环检测。
4
+ * 用法:node parse-flow.mjs <flowYamlPath>
5
+ * 或: node parse-flow.mjs <workspaceRoot> <flowName> [uuid] [flowDir]
6
+ * 当参数个数 ≥4 且第 4 个参数存在时,用 args[3] 作为 flowDir;否则按 getFlowDir(user → .workspace → .cursor 旧路径 → builtin)解析。
7
+ * 输出:order、nodes(含 role、model)、edges、hasCycle;若传 uuid 则含 instanceStatus 并写入 intermediate/flow.json(不写 resolvedInputs/resolvedOutputs,由 get-resolved-values 用时现算);若传 flowName 则 stdout 含 outputSlotTypes、inputSlotTypes,可选含 resolvedInputs、resolvedOutputs。
8
+ */
9
+
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import { fileURLToPath } from "url";
13
+ import yaml from "js-yaml";
14
+
15
+ import { getRunDir, LEGACY_NODES_DIR, PIPELINES_DIR, PROJECT_NODES_DIR } from "../lib/paths.mjs";
16
+ import { getFlowDir } from "../lib/workspace.mjs";
17
+ import { loadAllExecIds, latestResultExecId, intermediateResultBasename, intermediateDirForNode, outputDirForNode } from "./get-exec-id.mjs";
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const PACKAGE_BUILTIN_NODES_DIR = path.join(path.resolve(__dirname, "..", ".."), "builtin", "nodes");
21
+ import { logToRunTag } from "./run-log.mjs";
22
+
23
+ const FLOW_YAML_FILENAME = "flow.yaml";
24
+
25
+ /** 从流程目录读取 flow.yaml,返回 { instances, edges, ui };兼容旧版 flow.edges */
26
+ function loadFlowDefinition(flowDir) {
27
+ const filePath = path.join(flowDir, FLOW_YAML_FILENAME);
28
+ if (!fs.existsSync(filePath)) return null;
29
+ try {
30
+ const raw = fs.readFileSync(filePath, "utf-8");
31
+ const data = yaml.load(raw);
32
+ if (!data || typeof data !== "object") return null;
33
+ const edges = Array.isArray(data.edges)
34
+ ? data.edges
35
+ : Array.isArray(data.flow?.edges)
36
+ ? data.flow.edges
37
+ : [];
38
+ return {
39
+ instances: data.instances && typeof data.instances === "object" ? data.instances : {},
40
+ edges,
41
+ ui: data.ui && typeof data.ui === "object" ? data.ui : {},
42
+ };
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /** 由 definitionId 前缀推导 type */
49
+ function definitionIdToType(definitionId) {
50
+ const id = (definitionId || "").toLowerCase();
51
+ if (id.startsWith("control_")) return "control";
52
+ if (id.startsWith("agent_")) return "agent";
53
+ if (id.startsWith("provide_")) return "provide";
54
+ if (id.startsWith("tool_")) return "agent";
55
+ return "agent";
56
+ }
57
+
58
+ /**
59
+ * 按定义名查找节点类文件,读 frontmatter 中的 definitionId;若无则返回 definitionName。
60
+ * 查找顺序:flowDir/nodes、.workspace/agentflow/nodes、旧 .cursor/agentflow/nodes、包内 builtin/nodes。
61
+ * @param {string} flowDir - 流程目录 pipelines/<flowName>
62
+ * @param {string} definitionName - 实例中引用的定义名(如 user_confirm_scope)
63
+ * @returns {{ definitionId: string, definitionName: string }}
64
+ */
65
+ function resolveDefinitionIdFromNodeClass(flowDir, definitionName) {
66
+ const workspaceRoot = path.resolve(flowDir, "..", "..", "..", "..");
67
+ const fileName = definitionName.endsWith(".md") ? definitionName : `${definitionName}.md`;
68
+ const flowNodesPath = path.join(flowDir, "nodes", fileName);
69
+ const projectNodesNew = path.join(workspaceRoot, PROJECT_NODES_DIR, fileName);
70
+ const projectNodesLegacy = path.join(workspaceRoot, LEGACY_NODES_DIR, fileName);
71
+ const packageNodesPath = path.join(PACKAGE_BUILTIN_NODES_DIR, fileName);
72
+ for (const filePath of [flowNodesPath, projectNodesNew, projectNodesLegacy, packageNodesPath]) {
73
+ if (!fs.existsSync(filePath)) continue;
74
+ try {
75
+ const raw = fs.readFileSync(filePath, "utf-8");
76
+ const m = raw.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
77
+ if (!m) continue;
78
+ const fmMatch = m[1].match(/^\s*definitionId:\s*["']?([^"'\n]+)["']?/m);
79
+ const inner = fmMatch ? fmMatch[1].trim() : "";
80
+ const definitionId = inner || definitionName;
81
+ return { definitionId, definitionName };
82
+ } catch (_) {}
83
+ }
84
+ return { definitionId: definitionName, definitionName };
85
+ }
86
+
87
+ /** 从 loadFlowDefinition 结果得到 nodes 和 edges(与 readFlowMd 输出形状一致) */
88
+ function readFlowFromYaml(flowDir) {
89
+ const def = loadFlowDefinition(flowDir);
90
+ if (!def) return { nodes: [], edges: [] };
91
+ const instances = def.instances;
92
+ const edgesRaw = Array.isArray(def.edges) ? def.edges : [];
93
+ const nodeIds = new Set(Object.keys(instances));
94
+ for (const e of edgesRaw) {
95
+ if (e?.source) nodeIds.add(e.source);
96
+ if (e?.target) nodeIds.add(e.target);
97
+ }
98
+ const nodes = Array.from(nodeIds).map((id) => {
99
+ const inst = instances[id] || {};
100
+ const definitionName = inst.definitionId ?? id;
101
+ const { definitionId } = resolveDefinitionIdFromNodeClass(flowDir, definitionName);
102
+ const type = definitionIdToType(definitionId);
103
+ const label = inst.label != null ? String(inst.label) : id;
104
+ const role =
105
+ inst.role != null && String(inst.role).trim()
106
+ ? String(inst.role).trim()
107
+ : "普通";
108
+ const model =
109
+ inst.model != null && String(inst.model).trim()
110
+ ? String(inst.model).trim()
111
+ : null;
112
+ return { id, type, label, definitionId, role, model, definitionName };
113
+ });
114
+ const edges = edgesRaw
115
+ .filter((e) => e?.source && e?.target)
116
+ .map((e) => ({
117
+ source: String(e.source),
118
+ target: String(e.target),
119
+ sourceHandle: e.sourceHandle ?? null,
120
+ targetHandle: e.targetHandle ?? null,
121
+ }));
122
+ return { nodes, edges };
123
+ }
124
+
125
+ function readFlowMd(filePath) {
126
+ const raw = fs.readFileSync(filePath, "utf-8");
127
+ const nodes = [];
128
+ const edges = [];
129
+ let inNodes = false;
130
+ let inEdges = false;
131
+ const lines = raw.split(/\r?\n/);
132
+ for (const line of lines) {
133
+ const trimmed = line.trim();
134
+ if (trimmed === "nodes:") {
135
+ inNodes = true;
136
+ inEdges = false;
137
+ continue;
138
+ }
139
+ if (trimmed === "edges:") {
140
+ inEdges = true;
141
+ inNodes = false;
142
+ continue;
143
+ }
144
+ if (inNodes && /^\s*-\s+id:\s*(.+)$/.test(line)) {
145
+ const id = line.replace(/^\s*-\s+id:\s*/, "").trim().replace(/^["']|["']$/g, "");
146
+ nodes.push({ id });
147
+ continue;
148
+ }
149
+ if (inNodes && nodes.length && /^\s{2,}(type|label|definitionId|role|model):\s*(.+)$/.test(line)) {
150
+ const m = line.match(/^\s{2,}(type|label|definitionId|role|model):\s*(.+)$/);
151
+ if (m) {
152
+ const key = m[1];
153
+ const val = m[2].trim().replace(/^["']|["']$/g, "");
154
+ nodes[nodes.length - 1][key] = val;
155
+ }
156
+ continue;
157
+ }
158
+ if (inEdges && /^\s*-\s+source:\s*(.+)$/.test(line)) {
159
+ const source = line.replace(/^\s*-\s+source:\s*/, "").trim().replace(/^["']|["']$/g, "");
160
+ edges.push({ source, target: null, sourceHandle: null, targetHandle: null });
161
+ continue;
162
+ }
163
+ if (inEdges && edges.length && /^\s+target:\s*(.+)$/.test(line)) {
164
+ const target = line.replace(/^\s+target:\s*/, "").trim().replace(/^["']|["']$/g, "");
165
+ edges[edges.length - 1].target = target;
166
+ continue;
167
+ }
168
+ if (inEdges && edges.length && /^\s+sourceHandle:\s*(.+)$/.test(line)) {
169
+ const v = line.replace(/^\s+sourceHandle:\s*/, "").trim().replace(/^["']|["']$/g, "");
170
+ edges[edges.length - 1].sourceHandle = v === "null" ? null : v;
171
+ continue;
172
+ }
173
+ if (inEdges && edges.length && /^\s+targetHandle:\s*(.+)$/.test(line)) {
174
+ const v = line.replace(/^\s+targetHandle:\s*/, "").trim().replace(/^["']|["']$/g, "");
175
+ edges[edges.length - 1].targetHandle = v === "null" ? null : v;
176
+ continue;
177
+ }
178
+ if (trimmed === "" && (inNodes || inEdges)) {
179
+ inNodes = false;
180
+ inEdges = false;
181
+ }
182
+ }
183
+ return { nodes, edges };
184
+ }
185
+
186
+ function topoSort(nodes, edges) {
187
+ const idToIndex = new Map(nodes.map((n, i) => [n.id, i]));
188
+ const n = nodes.length;
189
+ const outEdges = Array.from({ length: n }, () => []);
190
+ const inDeg = new Array(n).fill(0);
191
+ for (const e of edges) {
192
+ const u = idToIndex.get(e.source);
193
+ const v = idToIndex.get(e.target);
194
+ if (u == null || v == null) continue;
195
+ outEdges[u].push(v);
196
+ inDeg[v]++;
197
+ }
198
+ const queue = [];
199
+ for (let i = 0; i < n; i++) if (inDeg[i] === 0) queue.push(i);
200
+ const order = [];
201
+ let visited = 0;
202
+ while (queue.length) {
203
+ const u = queue.shift();
204
+ order.push(nodes[u].id);
205
+ visited++;
206
+ for (const v of outEdges[u]) {
207
+ inDeg[v]--;
208
+ if (inDeg[v] === 0) queue.push(v);
209
+ }
210
+ }
211
+ const hasCycle = visited !== n;
212
+ return { order, hasCycle };
213
+ }
214
+
215
+ /**
216
+ * 找出所有参与至少一个环的节点(Tarjan SCC:大小 > 1 的强连通分量中的节点并集)。
217
+ * 返回 Set<nodeId>。
218
+ */
219
+ function findCycleNodes(nodes, edges) {
220
+ const idToIndex = new Map(nodes.map((n, i) => [n.id, i]));
221
+ const n = nodes.length;
222
+ const outEdges = Array.from({ length: n }, () => []);
223
+ for (const e of edges) {
224
+ const u = idToIndex.get(e.source);
225
+ const v = idToIndex.get(e.target);
226
+ if (u == null || v == null) continue;
227
+ outEdges[u].push(v);
228
+ }
229
+
230
+ let index = 0;
231
+ const stack = [];
232
+ const dfn = new Array(n).fill(-1);
233
+ const low = new Array(n).fill(-1);
234
+ const onStack = new Array(n).fill(false);
235
+ const cycleNodeIds = new Set();
236
+
237
+ function strongConnect(u) {
238
+ dfn[u] = low[u] = index++;
239
+ stack.push(u);
240
+ onStack[u] = true;
241
+ for (const v of outEdges[u]) {
242
+ if (dfn[v] === -1) {
243
+ strongConnect(v);
244
+ low[u] = Math.min(low[u], low[v]);
245
+ } else if (onStack[v]) {
246
+ low[u] = Math.min(low[u], dfn[v]);
247
+ }
248
+ }
249
+ if (dfn[u] === low[u]) {
250
+ const scc = [];
251
+ let w;
252
+ do {
253
+ w = stack.pop();
254
+ onStack[w] = false;
255
+ scc.push(w);
256
+ } while (w !== u);
257
+ if (scc.length > 1) {
258
+ for (const i of scc) cycleNodeIds.add(nodes[i].id);
259
+ }
260
+ }
261
+ }
262
+
263
+ for (let i = 0; i < n; i++) {
264
+ if (dfn[i] === -1) strongConnect(i);
265
+ }
266
+ return cycleNodeIds;
267
+ }
268
+
269
+ /** 从 result.md 中解析 status(匹配 YAML frontmatter 中的 status: xxx) */
270
+ function parseResultStatus(filePath) {
271
+ try {
272
+ const raw = fs.readFileSync(filePath, "utf-8");
273
+ const m = raw.match(/^\s*status:\s*["']?(\w+)["']?/m);
274
+ return m ? m[1] : null;
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
279
+
280
+ /** 从文件中提取 ${input}、${output}、${input.xxx}、${output.xxx} 占位符 */
281
+ function extractPlaceholders(text) {
282
+ if (!text || typeof text !== "string") return { inputs: [], outputs: [] };
283
+ const inputs = [];
284
+ const outputs = [];
285
+ const inputRe = /\$\{input(?:\.(\w+))?\}/g;
286
+ const outputRe = /\$\{output(?:\.(\w+))?\}/g;
287
+ let m;
288
+ while ((m = inputRe.exec(text)) !== null) {
289
+ inputs.push(m[1] || null);
290
+ }
291
+ while ((m = outputRe.exec(text)) !== null) {
292
+ outputs.push(m[1] || null);
293
+ }
294
+ return { inputs: [...new Set(inputs)], outputs: [...new Set(outputs)] };
295
+ }
296
+
297
+ /** 解析 instance 的 frontmatter,提取 input/output 槽位的 name、value 与 type */
298
+ function parseInstanceSlots(filePath) {
299
+ const result = { input: {}, output: {}, inputTypes: {}, outputTypes: {} };
300
+ try {
301
+ const raw = fs.readFileSync(filePath, "utf-8");
302
+ const fmMatch = raw.match(/^---\s*\n([\s\S]*?)\n---/);
303
+ if (!fmMatch) return result;
304
+ const fm = fmMatch[1];
305
+ let section = null;
306
+ let currentName = null;
307
+ let currentType = null;
308
+ const lines = fm.split(/\n/);
309
+ for (const line of lines) {
310
+ if (/^\s*input:\s*$/.test(line)) {
311
+ section = "input";
312
+ currentName = null;
313
+ currentType = null;
314
+ continue;
315
+ }
316
+ if (/^\s*output:\s*$/.test(line)) {
317
+ section = "output";
318
+ currentName = null;
319
+ currentType = null;
320
+ continue;
321
+ }
322
+ const typeMatch = line.match(/^\s*-\s+type:\s*["']?([^"'\n]*)["']?/);
323
+ if (typeMatch) {
324
+ currentType = typeMatch[1].trim() || null;
325
+ currentName = null;
326
+ continue;
327
+ }
328
+ const typeMatch2 = line.match(/^\s+type:\s*["']?([^"'\n]*)["']?/);
329
+ if (typeMatch2 && section) {
330
+ currentType = typeMatch2[1].trim() || null;
331
+ continue;
332
+ }
333
+ const nameMatch = line.match(/^\s+name:\s*["']?([^"'\n]*)["']?/);
334
+ if (nameMatch) {
335
+ currentName = nameMatch[1].trim() || "";
336
+ continue;
337
+ }
338
+ const valueMatch = line.match(/^\s+(?:value|default):\s*(.*)$/);
339
+ if (valueMatch && section) {
340
+ const v = valueMatch[1].replace(/^["']|["']$/g, "").trim();
341
+ const key = currentName != null ? currentName : "_";
342
+ result[section][key] = v;
343
+ if (currentType && section === "output") result.outputTypes[key] = currentType;
344
+ if (currentType && section === "input") result.inputTypes[key] = currentType;
345
+ currentName = null;
346
+ currentType = null;
347
+ }
348
+ }
349
+ } catch (_) {}
350
+ return result;
351
+ }
352
+
353
+ /** 读取 instance 文件内容(用于提取占位符) */
354
+ function readInstanceContent(filePath) {
355
+ try {
356
+ const raw = fs.readFileSync(filePath, "utf-8");
357
+ const m = raw.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/);
358
+ return m ? m[1] : raw;
359
+ } catch {
360
+ return "";
361
+ }
362
+ }
363
+
364
+ /** 从 result.md 解析 outputPath(上游节点执行后的实际输出路径) */
365
+ function parseResultOutputPath(filePath) {
366
+ try {
367
+ const raw = fs.readFileSync(filePath, "utf-8");
368
+ const m = raw.match(/^\s*outputPath:\s*["']?([^"'\s]+)["']?/m);
369
+ return m ? m[1] : null;
370
+ } catch {
371
+ return null;
372
+ }
373
+ }
374
+
375
+ /** 按约定生成输出槽的静态路径:output/<instanceId>/node_<instanceId>_<slotBase>.md */
376
+ function getOutputPathForSlot(instanceId, slotName) {
377
+ const base = slotName.replace(/\.(md|txt|json|html?)$/i, "") || slotName;
378
+ return `${outputDirForNode(instanceId)}/node_${instanceId}_${base}.md`;
379
+ }
380
+
381
+ /** 从 flow.yaml 的 instance 条目得到 slots 形状(与 parseInstanceSlots 一致) */
382
+ function instanceEntryToSlots(inst) {
383
+ const result = { input: {}, output: {}, inputTypes: {}, outputTypes: {} };
384
+ if (!inst || typeof inst !== "object") return result;
385
+ const toMap = (arr, typesTarget) => {
386
+ const map = {};
387
+ if (!Array.isArray(arr)) return map;
388
+ arr.forEach((item, i) => {
389
+ const name = item?.name != null ? String(item.name) : (i === 0 ? "_" : "");
390
+ const key = name || `_${i}`;
391
+ map[key] = item?.value != null ? String(item.value) : "";
392
+ if (item?.type && typesTarget) typesTarget[key] = item.type;
393
+ });
394
+ return map;
395
+ };
396
+ result.input = toMap(inst.input, result.inputTypes);
397
+ result.output = toMap(inst.output, result.outputTypes);
398
+ return result;
399
+ }
400
+
401
+ /** 解析 ${input}/${output} 占位符的实际值;需 workspaceRoot、flowName,可选 uuid;flowData 为 loadFlowDefinition 结果,有则从 instances 读。可选 flowDirIn:传入时作为流程目录(支持用户数据目录下 flow)。 */
402
+ function resolvePlaceholders(workspaceRoot, flowName, order, edges, uuid, flowData, flowDirIn) {
403
+ const flowDir =
404
+ flowDirIn != null && flowDirIn !== ""
405
+ ? path.resolve(flowDirIn)
406
+ : getFlowDir(workspaceRoot, flowName) || path.join(workspaceRoot, PIPELINES_DIR, flowName);
407
+ const instanceDir = path.join(flowDir, "instance");
408
+ const instances = flowData?.instances && typeof flowData.instances === "object" ? flowData.instances : {};
409
+ const useYaml = Object.keys(instances).length > 0;
410
+ const intermediateDir = uuid && flowName ? path.join(getRunDir(workspaceRoot, flowName, uuid), "intermediate") : null;
411
+ let execIds = {};
412
+ if (uuid && flowName && intermediateDir && fs.existsSync(intermediateDir)) {
413
+ try {
414
+ execIds = loadAllExecIds(workspaceRoot, flowName, uuid, order);
415
+ } catch (_) {}
416
+ }
417
+
418
+ const predecessors = new Map();
419
+ for (const e of edges) {
420
+ if (!e.target) continue;
421
+ if (!predecessors.has(e.target)) predecessors.set(e.target, []);
422
+ predecessors.get(e.target).push({ source: e.source, sourceHandle: e.sourceHandle, targetHandle: e.targetHandle });
423
+ }
424
+
425
+ const resolvedInputs = {};
426
+ const resolvedOutputs = {};
427
+ const outputSlotTypes = {};
428
+ const inputSlotTypes = {};
429
+
430
+ const getSlotsFor = (instanceId) => {
431
+ if (useYaml && instances[instanceId]) return instanceEntryToSlots(instances[instanceId]);
432
+ const instPath = path.join(instanceDir, `${instanceId}.md`);
433
+ return fs.existsSync(instPath) ? parseInstanceSlots(instPath) : instanceEntryToSlots(null);
434
+ };
435
+
436
+ const getContentFor = (instanceId) => {
437
+ if (useYaml && instances[instanceId] && instances[instanceId].body != null) return String(instances[instanceId].body || "").trim();
438
+ return readInstanceContent(path.join(instanceDir, `${instanceId}.md`));
439
+ };
440
+
441
+ /** 通过 sourceHandle (output-0, output-1...) 获取上游节点对应 output 槽位的值 */
442
+ const getNodeOutput = (instanceId, sourceHandle) => {
443
+ const slots = getSlotsFor(instanceId);
444
+ const out = slots.output;
445
+ const outValues = Object.values(out);
446
+ if (sourceHandle && /^output-(\d+)$/.test(sourceHandle)) {
447
+ const idx = parseInt(sourceHandle.replace("output-", ""), 10);
448
+ if (idx >= 0 && idx < outValues.length) return outValues[idx] ?? null;
449
+ }
450
+ return outValues[0] ?? null;
451
+ };
452
+
453
+ const getNodeOutputFromResult = (instanceId) => {
454
+ if (!intermediateDir) return null;
455
+ const latestE = latestResultExecId(execIds[instanceId] ?? 1);
456
+ if (!latestE) return null;
457
+ const resultPath = path.join(intermediateDir, instanceId, intermediateResultBasename(instanceId, latestE));
458
+ return parseResultOutputPath(resultPath);
459
+ };
460
+
461
+ for (const instanceId of order) {
462
+ const content = getContentFor(instanceId);
463
+ const slots = getSlotsFor(instanceId);
464
+ resolvedOutputs[instanceId] = { ...slots.output };
465
+ if (slots.outputTypes && Object.keys(slots.outputTypes).length > 0) {
466
+ outputSlotTypes[instanceId] = slots.outputTypes;
467
+ }
468
+ if (slots.inputTypes && Object.keys(slots.inputTypes).length > 0) {
469
+ inputSlotTypes[instanceId] = slots.inputTypes;
470
+ }
471
+ const { inputs: inputRefs } = extractPlaceholders(content);
472
+
473
+ const preds = predecessors.get(instanceId) || [];
474
+ const inputSlotNames = Object.keys(slots.input);
475
+
476
+ for (let i = 0; i < inputSlotNames.length; i++) {
477
+ const slotName = inputSlotNames[i];
478
+ const targetHandle = `input-${i}`;
479
+ const pred = preds.find((p) => p.targetHandle === targetHandle);
480
+ if (!pred) continue;
481
+
482
+ const fromResult = getNodeOutputFromResult(pred.source);
483
+ const fromInstance = getNodeOutput(pred.source, pred.sourceHandle);
484
+ let value = fromResult ?? fromInstance;
485
+ // 按上游 output 槽名生成路径:上游多槽时 result 只记一个 path,若直接用 fromResult 会令所有边得到同一路径,导致 pre/now 不一致;故有 sourceHandle 时优先用槽名约定路径
486
+ if (pred.sourceHandle && /^output-(\d+)$/.test(pred.sourceHandle)) {
487
+ const sourceSlots = getSlotsFor(pred.source);
488
+ if (Object.keys(sourceSlots.output).length > 0) {
489
+ const outSlotNames = Object.keys(sourceSlots.output);
490
+ const idx = parseInt(pred.sourceHandle.replace("output-", ""), 10);
491
+ if (idx >= 0 && idx < outSlotNames.length) {
492
+ const slotPath = getOutputPathForSlot(pred.source, outSlotNames[idx]);
493
+ // 无 result 或 result 未提供路径时用约定路径;有 result 且上游多槽时仍用约定路径,避免多边共用一个 outputPath
494
+ if (!value || value === "" || outSlotNames.length > 1) {
495
+ value = slotPath;
496
+ }
497
+ }
498
+ }
499
+ }
500
+ if (value != null) {
501
+ // 类型为「节点」的 input 槽:填上游节点 id,不填文件路径,避免多 output 槽解析歧义与 cache 漂移
502
+ const slotType = slots.inputTypes && slots.inputTypes[slotName];
503
+ if (slotType === "节点" || slotType === "node") {
504
+ value = pred.source;
505
+ }
506
+ if (!resolvedInputs[instanceId]) resolvedInputs[instanceId] = {};
507
+ resolvedInputs[instanceId][slotName] = value;
508
+ }
509
+ }
510
+ // 无入边的 input 槽位使用 instance 中该槽的 value 作为默认值(如 tool_save_key 的 key)
511
+ if (!resolvedInputs[instanceId]) resolvedInputs[instanceId] = {};
512
+ for (const slotName of inputSlotNames) {
513
+ if (resolvedInputs[instanceId][slotName] != null) continue;
514
+ const defaultVal = slots.input[slotName];
515
+ if (defaultVal !== undefined && defaultVal !== "") {
516
+ resolvedInputs[instanceId][slotName] = defaultVal;
517
+ }
518
+ }
519
+
520
+ if (inputRefs.length > 0 && preds.length > 0 && !Object.keys(resolvedInputs[instanceId]).length) {
521
+ const pred = preds[0];
522
+ const fromResult = getNodeOutputFromResult(pred.source);
523
+ const fromInstance = getNodeOutput(pred.source, pred.sourceHandle);
524
+ let v = fromResult ?? fromInstance;
525
+ if (v != null) {
526
+ // 占位符 _ 若对应节点类型,用上游节点 id
527
+ const firstSlotName = inputSlotNames[0];
528
+ const firstType = slots.inputTypes && slots.inputTypes[firstSlotName];
529
+ if (firstType === "节点" || firstType === "node") v = pred.source;
530
+ resolvedInputs[instanceId] = { _: v };
531
+ }
532
+ }
533
+ }
534
+ return { resolvedInputs, resolvedOutputs, outputSlotTypes, inputSlotTypes };
535
+ }
536
+
537
+ function applyCliInputs(order, edges, instances, cliInputs, resolvedInputs) {
538
+ if (!instances || typeof instances !== "object") return;
539
+ for (const instanceId of order) {
540
+ const inst = instances[instanceId];
541
+ if (!inst?.input) continue;
542
+ const inputSlots = inst.input;
543
+ for (let i = 0; i < inputSlots.length; i++) {
544
+ const slotName = inputSlots[i]?.name;
545
+ if (!slotName || !cliInputs[slotName]) continue;
546
+ const edge = edges.find(e => e.target === instanceId && e.targetHandle === `input-${i}`);
547
+ if (!edge?.source) continue;
548
+ const sourceInst = instances[edge.source];
549
+ if (sourceInst?.definitionId?.startsWith("provide_")) {
550
+ const cliVal = cliInputs[slotName];
551
+ const value = cliVal.type === "file" ? cliVal.path : cliVal.value;
552
+ if (!resolvedInputs[instanceId]) resolvedInputs[instanceId] = {};
553
+ resolvedInputs[instanceId][slotName] = value;
554
+ }
555
+ }
556
+ }
557
+ }
558
+
559
+ /** 读取 uuid 对应 run 目录下 intermediate/ 中各 instance 的状态(按 _execId 最新一轮 result 读) */
560
+ function readInstanceStatus(workspaceRoot, flowName, uuid, order) {
561
+ const intermediateDir = path.join(getRunDir(workspaceRoot, flowName, uuid), "intermediate");
562
+ const instanceStatus = {};
563
+ try {
564
+ if (!fs.existsSync(intermediateDir)) return instanceStatus;
565
+ const execIds = loadAllExecIds(workspaceRoot, flowName, uuid, order);
566
+ for (const instanceId of order) {
567
+ const latestE = latestResultExecId(execIds[instanceId] ?? 1);
568
+ if (!latestE) continue;
569
+ const resultPath = path.join(intermediateDir, instanceId, intermediateResultBasename(instanceId, latestE));
570
+ const status = parseResultStatus(resultPath);
571
+ if (status) instanceStatus[instanceId] = status;
572
+ }
573
+ } catch (_) {}
574
+ return instanceStatus;
575
+ }
576
+
577
+ function main() {
578
+ const args = process.argv.slice(2);
579
+ let flowDir;
580
+ let workspaceRoot = null;
581
+ let uuid = null;
582
+ let cliInputs = {};
583
+ const cliInputsIdx = args.indexOf("--cli-inputs");
584
+ if (cliInputsIdx >= 0 && args[cliInputsIdx + 1]) {
585
+ try {
586
+ cliInputs = JSON.parse(args[cliInputsIdx + 1]);
587
+ args.splice(cliInputsIdx, 2);
588
+ } catch (_) {}
589
+ }
590
+ if (args.length === 1) {
591
+ const p = path.resolve(args[0]);
592
+ flowDir = path.dirname(p);
593
+ if (!p.endsWith(FLOW_YAML_FILENAME)) {
594
+ console.error(JSON.stringify({ ok: false, error: `Path must be to ${FLOW_YAML_FILENAME}` }));
595
+ process.exit(1);
596
+ }
597
+ } else if (args.length >= 2) {
598
+ const [root, name] = args;
599
+ workspaceRoot = path.resolve(root);
600
+ if (args.length >= 4 && args[3]) {
601
+ flowDir = path.resolve(args[3]);
602
+ } else {
603
+ flowDir = getFlowDir(workspaceRoot, name) || path.join(workspaceRoot, PIPELINES_DIR, name);
604
+ }
605
+ if (args.length >= 3) uuid = args[2];
606
+ } else {
607
+ console.error(JSON.stringify({ ok: false, error: "Usage: node parse-flow.mjs <flowYamlPath> | node parse-flow.mjs <workspaceRoot> <flowName> [uuid] [flowDir] [--cli-inputs <json>]" }));
608
+ process.exit(1);
609
+ }
610
+ const flowData = loadFlowDefinition(flowDir);
611
+ if (!flowData) {
612
+ console.error(JSON.stringify({ ok: false, error: `File not found or invalid: ${path.join(flowDir, FLOW_YAML_FILENAME)}` }));
613
+ process.exit(1);
614
+ }
615
+ try {
616
+ const { nodes, edges } = readFlowFromYaml(flowDir);
617
+ const { order: topoOrder, hasCycle } = topoSort(nodes, edges);
618
+ const order = hasCycle ? nodes.map((n) => n.id) : topoOrder;
619
+ const cycleNodes = hasCycle ? Array.from(findCycleNodes(nodes, edges)) : [];
620
+
621
+ const predecessors = {};
622
+ for (const e of edges) {
623
+ if (!e.target) continue;
624
+ if (!predecessors[e.target]) predecessors[e.target] = [];
625
+ predecessors[e.target].push(e.source);
626
+ }
627
+
628
+ const nodeDefinitions = {};
629
+ for (const n of nodes) {
630
+ if (n.definitionId) nodeDefinitions[n.id] = n.definitionId;
631
+ }
632
+ const out = { ok: true, order, nodes, edges, predecessors, nodeDefinitions, hasCycle, cycleNodes };
633
+ const flowNameArg = args.length >= 2 ? args[1] : null;
634
+ if (uuid && workspaceRoot && flowNameArg) {
635
+ out.instanceStatus = readInstanceStatus(workspaceRoot, flowNameArg, uuid, order);
636
+ }
637
+ if (workspaceRoot && args.length >= 2) {
638
+ const flowName = args[1];
639
+ out.flowName = flowName;
640
+ const { resolvedInputs, resolvedOutputs, outputSlotTypes, inputSlotTypes } = resolvePlaceholders(workspaceRoot, flowName, order, edges, uuid || null, flowData, flowDir);
641
+ if (Object.keys(cliInputs).length > 0) {
642
+ applyCliInputs(order, edges, flowData.instances, cliInputs, resolvedInputs);
643
+ }
644
+ out.resolvedInputs = resolvedInputs;
645
+ out.resolvedOutputs = resolvedOutputs;
646
+ out.outputSlotTypes = outputSlotTypes;
647
+ out.inputSlotTypes = inputSlotTypes;
648
+ out.cliInputsApplied = Object.keys(cliInputs).length > 0 ? cliInputs : null;
649
+ }
650
+ // 传入 uuid 时自动写入 intermediate/flow.json(仅结构 + slotTypes,不含 resolvedInputs/resolvedOutputs)
651
+ if (uuid && workspaceRoot && flowNameArg) {
652
+ const intermediateDir = path.join(getRunDir(workspaceRoot, flowNameArg, uuid), "intermediate");
653
+ fs.mkdirSync(intermediateDir, { recursive: true });
654
+ const flowJsonPath = path.join(intermediateDir, "flow.json");
655
+ const starts = order.filter((id) => nodeDefinitions[id] === "control_start");
656
+ let pendingInstances = starts;
657
+ let preserved = false;
658
+ if (fs.existsSync(flowJsonPath)) {
659
+ try {
660
+ const old = JSON.parse(fs.readFileSync(flowJsonPath, "utf-8"));
661
+ if (Array.isArray(old.pendingInstances) && old.pendingInstances.length > 0) {
662
+ pendingInstances = old.pendingInstances;
663
+ preserved = true;
664
+ }
665
+ } catch (_) {}
666
+ }
667
+ out.pendingInstances = pendingInstances;
668
+ out.flowDir = path.resolve(flowDir);
669
+ logToRunTag(workspaceRoot, flowNameArg, uuid, "parse-flow", {
670
+ event: "pendingInstances",
671
+ pendingInstances,
672
+ preserved,
673
+ });
674
+ const { resolvedInputs: _ri, resolvedOutputs: _ro, ...flowForFile } = out;
675
+ fs.writeFileSync(flowJsonPath, JSON.stringify(flowForFile), "utf-8");
676
+ }
677
+ console.log(JSON.stringify(out));
678
+ } catch (err) {
679
+ console.error(JSON.stringify({ ok: false, error: err.message }));
680
+ process.exit(1);
681
+ }
682
+ }
683
+
684
+ /** 供其他脚本使用:从流程目录读取 flow.yaml;resolve-inputs 等需用到的 helpers */
685
+ export {
686
+ loadFlowDefinition,
687
+ readFlowFromYaml,
688
+ parseResultOutputPath,
689
+ getOutputPathForSlot,
690
+ instanceEntryToSlots,
691
+ parseInstanceSlots,
692
+ readInstanceContent,
693
+ extractPlaceholders,
694
+ applyCliInputs,
695
+ };
696
+
697
+ const isMain = typeof process !== "undefined" && process.argv[1] && process.argv[1].endsWith("parse-flow.mjs");
698
+ if (isMain) main();