@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,543 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 检查 flow.yaml 定义:校验 instances、edges、槽位、control_start/control_end、环与 control_anyOne 等。
4
+ * 用法:agentflow apply -ai check-flow <workspaceRoot> <flowName> [flowDir]
5
+ * 或:agentflow apply -ai check-flow <flowYamlPath>(单参时为 flow 目录或 flow.yaml 路径)
6
+ * 当传 3 个参数时,flowDir 为含 flow.yaml 的目录(相对 workspaceRoot 或绝对路径);不传时按 getFlowDir 解析(含 ~/agentflow/pipelines、.workspace/…、旧版 .cursor/…、builtin)。
7
+ * 输出(适配 agentflow apply -ai run-tool-nodejs):stdout 单行 JSON { "err_code": number, "message": { "result": "<全文>" } };err_code 恒为 0。
8
+ */
9
+
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import yaml from "js-yaml";
13
+
14
+ import { PIPELINES_DIR } from "../lib/paths.mjs";
15
+ import { getFlowDir } from "../lib/workspace.mjs";
16
+
17
+ /** 由 definitionId 推导 type(与 parse-flow 一致) */
18
+ function definitionIdToType(definitionId) {
19
+ const id = (definitionId || "").toLowerCase();
20
+ if (id.startsWith("control_")) return "control";
21
+ if (id.startsWith("agent_")) return "agent";
22
+ if (id.startsWith("provide_")) return "provide";
23
+ if (id.startsWith("tool_")) return "agent";
24
+ return "agent";
25
+ }
26
+
27
+ /** 从 flow.yaml 的 instance 条目的 input/output 数组解析槽位名与类型 */
28
+ function getSlotsFromInstance(inst) {
29
+ const result = { inputNames: [], outputNames: [], inputTypes: [], outputTypes: [] };
30
+ if (!inst || typeof inst !== "object") return result;
31
+ const inp = Array.isArray(inst.input) ? inst.input : [];
32
+ const out = Array.isArray(inst.output) ? inst.output : [];
33
+ for (const slot of inp) {
34
+ const name = slot && (slot.name != null) ? String(slot.name).trim() : "";
35
+ const type = slot && (slot.type != null) ? String(slot.type).trim() : "";
36
+ if (name) {
37
+ result.inputNames.push(name);
38
+ result.inputTypes.push(type);
39
+ }
40
+ }
41
+ for (const slot of out) {
42
+ const name = slot && (slot.name != null) ? String(slot.name).trim() : "";
43
+ const type = slot && (slot.type != null) ? String(slot.type).trim() : "";
44
+ if (name) {
45
+ result.outputNames.push(name);
46
+ result.outputTypes.push(type);
47
+ }
48
+ }
49
+ return result;
50
+ }
51
+
52
+ /**
53
+ * 从流程目录读取 flow.yaml,返回 { nodes, edges, instances };不存在或解析失败返回 null。
54
+ */
55
+ function loadFlowYaml(flowDir) {
56
+ const filePath = path.join(flowDir, "flow.yaml");
57
+ if (!fs.existsSync(filePath)) return null;
58
+ try {
59
+ const raw = fs.readFileSync(filePath, "utf-8");
60
+ const data = yaml.load(raw);
61
+ if (!data || typeof data !== "object") return null;
62
+ const instances = data.instances && typeof data.instances === "object" ? data.instances : {};
63
+ const edgesRaw = Array.isArray(data.edges) ? data.edges : [];
64
+ const nodeIds = new Set(Object.keys(instances));
65
+ for (const e of edgesRaw) {
66
+ if (e?.source) nodeIds.add(e.source);
67
+ if (e?.target) nodeIds.add(e.target);
68
+ }
69
+ const nodes = Array.from(nodeIds).map((id) => {
70
+ const inst = instances[id] || {};
71
+ const definitionId = inst.definitionId != null ? String(inst.definitionId) : id;
72
+ return {
73
+ id,
74
+ type: definitionIdToType(definitionId),
75
+ label: inst.label != null ? String(inst.label) : id,
76
+ definitionId,
77
+ modelType: inst.modelType != null ? String(inst.modelType) : "Auto",
78
+ };
79
+ });
80
+ const edges = edgesRaw
81
+ .filter((e) => e?.source && e?.target)
82
+ .map((e) => ({
83
+ source: String(e.source),
84
+ target: String(e.target),
85
+ sourceHandle: e.sourceHandle ?? null,
86
+ targetHandle: e.targetHandle ?? null,
87
+ }));
88
+ return { nodes, edges, instances };
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * 从正文中提取所有 ${...} 占位符,返回占位符内容列表(如 ["prev", "input.github", "output.next"])。
96
+ */
97
+ function extractPlaceholders(body) {
98
+ const list = [];
99
+ const re = /\$\{([^}]*)\}/g;
100
+ let match;
101
+ while ((match = re.exec(body)) !== null) {
102
+ const inner = match[1].trim();
103
+ if (inner) list.push(inner);
104
+ }
105
+ return list;
106
+ }
107
+
108
+ /** 转义字符串中的正则特殊字符,用于构造 RegExp。 */
109
+ function escapeRegex(s) {
110
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
111
+ }
112
+
113
+ /**
114
+ * 检查正文中槽位名是否被反引号或引号包裹(而非 ${}),若有则加入 warnings。
115
+ */
116
+ function warnSlotNameNotWrappedInDollarBraces(body, inputNames, outputNames, warnings) {
117
+ const allSlotNames = [...new Set([...inputNames, ...outputNames])];
118
+ if (allSlotNames.length === 0) return;
119
+ const bodyWithoutDollarBraces = body.replace(/\$\{[^}]*\}/g, "\x00\x00");
120
+ for (const name of allSlotNames) {
121
+ const escaped = escapeRegex(name);
122
+ if (new RegExp(`\`${escaped}\``).test(bodyWithoutDollarBraces)) {
123
+ warnings.push(`槽位名 "${name}" 被反引号包裹,应使用 \${${name}} 或 \${input.${name}}/\${output.${name}} 引用`);
124
+ }
125
+ if (new RegExp(`"${escaped}"`).test(bodyWithoutDollarBraces)) {
126
+ warnings.push(`槽位名 "${name}" 被双引号包裹,应使用 \${${name}} 或 \${input.${name}}/\${output.${name}} 引用`);
127
+ }
128
+ if (new RegExp(`'${escaped}'`).test(bodyWithoutDollarBraces)) {
129
+ warnings.push(`槽位名 "${name}" 被单引号包裹,应使用 \${${name}} 或 \${input.${name}}/\${output.${name}} 引用`);
130
+ }
131
+ }
132
+ }
133
+
134
+ /**
135
+ * 校验正文中对 input/output 槽位的引用:必须为 ${name}、${input.name} 或 ${output.name}。
136
+ * 返回 { errors: string[], warnings: string[] },errors 带前缀 "[节点Id] 文件路径: "。
137
+ */
138
+ function validatePlaceholdersInBody(nodeId, filePath, body, inputNames, outputNames) {
139
+ const errors = [];
140
+ const warnings = [];
141
+ const inputSet = new Set(inputNames);
142
+ const outputSet = new Set(outputNames);
143
+ const placeholders = extractPlaceholders(body);
144
+
145
+ for (const ph of placeholders) {
146
+ if (ph.startsWith("input.")) {
147
+ const name = ph.slice(6).trim();
148
+ if (!name) {
149
+ errors.push(`占位符 \${${ph}} 格式错误,应为 \${input.槽位名}`);
150
+ continue;
151
+ }
152
+ if (!inputSet.has(name)) {
153
+ if (outputSet.has(name)) {
154
+ errors.push(`占位符 \${${ph}}:槽位 "${name}" 为 output,应使用 \${output.${name}} 或 \${${name}}`);
155
+ } else {
156
+ errors.push(`占位符 \${${ph}}:未定义的 input 槽位 "${name}",应在 frontmatter 的 input 中声明`);
157
+ }
158
+ }
159
+ continue;
160
+ }
161
+ if (ph.startsWith("output.")) {
162
+ const name = ph.slice(7).trim();
163
+ if (!name) {
164
+ errors.push(`占位符 \${${ph}} 格式错误,应为 \${output.槽位名}`);
165
+ continue;
166
+ }
167
+ if (!outputSet.has(name)) {
168
+ if (inputSet.has(name)) {
169
+ errors.push(`占位符 \${${ph}}:槽位 "${name}" 为 input,应使用 \${input.${name}} 或 \${${name}}`);
170
+ } else {
171
+ errors.push(`占位符 \${${ph}}:未定义的 output 槽位 "${name}",应在 frontmatter 的 output 中声明`);
172
+ }
173
+ }
174
+ continue;
175
+ }
176
+ // ${name} 形式:name 必须是 input 或 output 槽位名
177
+ if (inputSet.has(ph) || outputSet.has(ph)) continue;
178
+ // 非槽位名(如 USER_PROMPT)不报错,可选 warning
179
+ if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(ph)) {
180
+ warnings.push(`占位符 \${${ph}} 含非常规标识符,请确认是否为槽位引用`);
181
+ }
182
+ }
183
+
184
+ // 若槽位名被 ` 或 " 或 ' 包裹而非 ${},给出 warning
185
+ warnSlotNameNotWrappedInDollarBraces(body, inputNames, outputNames, warnings);
186
+
187
+ const prefix = `[${nodeId}] ${filePath}: `;
188
+ return {
189
+ errors: errors.map((e) => prefix + e),
190
+ warnings: warnings.map((w) => prefix + w),
191
+ };
192
+ }
193
+
194
+ function topoSort(nodes, edges) {
195
+ const idToIndex = new Map(nodes.map((n, i) => [n.id, i]));
196
+ const n = nodes.length;
197
+ const outEdges = Array.from({ length: n }, () => []);
198
+ const inDeg = new Array(n).fill(0);
199
+ for (const e of edges) {
200
+ const u = idToIndex.get(e.source);
201
+ const v = idToIndex.get(e.target);
202
+ if (u == null || v == null) continue;
203
+ outEdges[u].push(v);
204
+ inDeg[v]++;
205
+ }
206
+ const queue = [];
207
+ for (let i = 0; i < n; i++) if (inDeg[i] === 0) queue.push(i);
208
+ const order = [];
209
+ let visited = 0;
210
+ while (queue.length) {
211
+ const u = queue.shift();
212
+ order.push(nodes[u].id);
213
+ visited++;
214
+ for (const v of outEdges[u]) {
215
+ inDeg[v]--;
216
+ if (inDeg[v] === 0) queue.push(v);
217
+ }
218
+ }
219
+ const hasCycle = visited !== n;
220
+ return { order, hasCycle };
221
+ }
222
+
223
+ /**
224
+ * 内部校验核心:接收 nodes、edges、flowDir、nodeIdToSlots 以及从 instances 取 body 的 getter(仅 flow.yaml)。
225
+ */
226
+ function checkFlowCore(nodes, edges, flowDir, nodeIdToSlots, getNodeBody) {
227
+ const errors = [];
228
+ const warnings = [];
229
+ const fixesApplied = [];
230
+ const nodeIds = new Set(nodes.map((n) => n.id));
231
+ const nodesDir = path.join(flowDir, "..", "nodes");
232
+
233
+ // 必须包含 control_start 与 control_end 各一
234
+ const definitionIds = nodes.map((n) => n.definitionId).filter(Boolean);
235
+ const hasStart = definitionIds.some((d) => d === "control_start");
236
+ const hasEnd = definitionIds.some((d) => d === "control_end");
237
+ if (!hasStart) errors.push("流程必须包含一个 definitionId 为 control_start 的节点");
238
+ if (!hasEnd) errors.push("流程必须包含一个 definitionId 为 control_end 的节点");
239
+
240
+ // 校验 edges:source、target 必须存在;sourceHandle/targetHandle 与槽位一致
241
+ const edgesToFix = [];
242
+ for (let i = 0; i < edges.length; i++) {
243
+ const e = edges[i];
244
+ if (!e.source) errors.push(`edge ${i + 1}: 缺少 source`);
245
+ else if (!nodeIds.has(e.source)) errors.push(`edge ${i + 1}: source "${e.source}" 不在 nodes 中`);
246
+ if (!e.target) errors.push(`edge ${i + 1}: 缺少 target`);
247
+ else if (!nodeIds.has(e.target)) errors.push(`edge ${i + 1}: target "${e.target}" 不在 nodes 中`);
248
+ if (!e.sourceHandle && e.source) {
249
+ warnings.push(`edge ${i + 1} (${e.source} -> ${e.target}): 缺少 sourceHandle,建议补全为 output-0`);
250
+ edgesToFix.push({ i, field: "sourceHandle", value: "output-0" });
251
+ }
252
+ if (!e.targetHandle && e.target) {
253
+ warnings.push(`edge ${i + 1} (${e.source} -> ${e.target}): 缺少 targetHandle,建议补全为 input-0`);
254
+ edgesToFix.push({ i, field: "targetHandle", value: "input-0" });
255
+ }
256
+
257
+ // sourceHandle 必须在源节点的 output 槽位范围内
258
+ if (e.source && nodeIds.has(e.source)) {
259
+ const outSlots = nodeIdToSlots[e.source]?.outputNames ?? [];
260
+ const maxOut = outSlots.length - 1;
261
+ if (e.sourceHandle) {
262
+ const outMatch = e.sourceHandle.match(/^output-(\d+)$/);
263
+ if (!outMatch) {
264
+ errors.push(`edge ${i + 1} (${e.source} -> ${e.target}): sourceHandle 应为 output-N 格式,当前为 "${e.sourceHandle}"`);
265
+ } else {
266
+ const idx = parseInt(outMatch[1], 10);
267
+ if (idx < 0 || idx > maxOut) {
268
+ const slotList = maxOut >= 0 ? outSlots.join(", ") : "(无 output 槽位)";
269
+ const range = maxOut < 0 ? "(无有效 output 槽位)" : maxOut === 0 ? "output-0" : `output-0 ~ output-${maxOut}`;
270
+ errors.push(
271
+ `edge ${i + 1} (${e.source} -> ${e.target}): sourceHandle "${e.sourceHandle}" 超出源节点 output 槽位 [${slotList}],有效范围: ${range}`
272
+ );
273
+ }
274
+ }
275
+ }
276
+ }
277
+
278
+ // targetHandle 必须在目标节点的 input 槽位范围内
279
+ if (e.target && nodeIds.has(e.target)) {
280
+ const inSlots = nodeIdToSlots[e.target]?.inputNames ?? [];
281
+ const maxIn = inSlots.length - 1;
282
+ if (e.targetHandle) {
283
+ const inMatch = e.targetHandle.match(/^input-(\d+)$/);
284
+ if (!inMatch) {
285
+ errors.push(`edge ${i + 1} (${e.source} -> ${e.target}): targetHandle 应为 input-N 格式,当前为 "${e.targetHandle}"`);
286
+ } else {
287
+ const idx = parseInt(inMatch[1], 10);
288
+ if (idx < 0 || idx > maxIn) {
289
+ const slotList = maxIn >= 0 ? inSlots.join(", ") : "(无 input 槽位)";
290
+ const range = maxIn < 0 ? "(无有效 input 槽位)" : maxIn === 0 ? "input-0" : `input-0 ~ input-${maxIn}`;
291
+ errors.push(
292
+ `edge ${i + 1} (${e.source} -> ${e.target}): targetHandle "${e.targetHandle}" 超出目标节点 input 槽位 [${slotList}],有效范围: ${range}`
293
+ );
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ // 边两端槽位类型必须一致(节点↔节点、文本↔文本、文件↔文件)
300
+ if (e.source && e.target && nodeIds.has(e.source) && nodeIds.has(e.target) && e.sourceHandle && e.targetHandle) {
301
+ const outMatch = e.sourceHandle.match(/^output-(\d+)$/);
302
+ const inMatch = e.targetHandle.match(/^input-(\d+)$/);
303
+ if (outMatch && inMatch) {
304
+ const outIdx = parseInt(outMatch[1], 10);
305
+ const inIdx = parseInt(inMatch[1], 10);
306
+ const srcSlots = nodeIdToSlots[e.source];
307
+ const tgtSlots = nodeIdToSlots[e.target];
308
+ const srcType = (srcSlots?.outputTypes?.[outIdx] ?? "").trim();
309
+ const tgtType = (tgtSlots?.inputTypes?.[inIdx] ?? "").trim();
310
+ if (srcType && tgtType && srcType !== tgtType) {
311
+ const srcName = srcSlots?.outputNames?.[outIdx] ?? e.sourceHandle;
312
+ const tgtName = tgtSlots?.inputNames?.[inIdx] ?? e.targetHandle;
313
+ errors.push(
314
+ `edge ${i + 1} (${e.source} -> ${e.target}): 槽位类型不一致,源 ${e.sourceHandle}(${srcName})为「${srcType}」,目标 ${e.targetHandle}(${tgtName})为「${tgtType}」,应同为 节点/文本/文件 之一`
315
+ );
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ // 若有 input 或 output 槽位,应有对应 edge 连接,否则 warning
322
+ const incomingByNode = new Map(); // nodeId -> Set<targetHandle>
323
+ const outgoingByNode = new Map(); // nodeId -> Set<sourceHandle>
324
+ for (const e of edges) {
325
+ if (e.target && nodeIds.has(e.target)) {
326
+ if (!incomingByNode.has(e.target)) incomingByNode.set(e.target, new Set());
327
+ if (e.targetHandle) incomingByNode.get(e.target).add(e.targetHandle);
328
+ }
329
+ if (e.source && nodeIds.has(e.source)) {
330
+ if (!outgoingByNode.has(e.source)) outgoingByNode.set(e.source, new Set());
331
+ if (e.sourceHandle) outgoingByNode.get(e.source).add(e.sourceHandle);
332
+ }
333
+ }
334
+ for (const n of nodes) {
335
+ const defId = n.definitionId || "";
336
+ const slots = nodeIdToSlots[n.id] || { inputNames: [], outputNames: [] };
337
+ const inCount = slots.inputNames.length;
338
+ const outCount = slots.outputNames.length;
339
+ // input 槽位:应有至少一条 edge 的 target 为该节点且 targetHandle 对应该槽位
340
+ const skipInputCheck = defId === "control_start" || defId.startsWith("provide_");
341
+ if (!skipInputCheck && inCount > 0) {
342
+ for (let i = 0; i < inCount; i++) {
343
+ const h = `input-${i}`;
344
+ const hasIn = incomingByNode.get(n.id)?.has(h);
345
+ if (!hasIn) {
346
+ const slotName = slots.inputNames[i] || h;
347
+ warnings.push(`节点 "${n.id}" 的 input 槽位 "${slotName}"(${h})无对应 edge 连接`);
348
+ }
349
+ }
350
+ }
351
+ // output 槽位:应有至少一条 edge 的 source 为该节点且 sourceHandle 对应该槽位
352
+ const skipOutputCheck = defId === "control_end";
353
+ if (!skipOutputCheck && outCount > 0) {
354
+ for (let i = 0; i < outCount; i++) {
355
+ const h = `output-${i}`;
356
+ const hasOut = outgoingByNode.get(n.id)?.has(h);
357
+ if (!hasOut) {
358
+ const slotName = slots.outputNames[i] || h;
359
+ warnings.push(`节点 "${n.id}" 的 output 槽位 "${slotName}"(${h})无对应 edge 连接`);
360
+ }
361
+ }
362
+ }
363
+ }
364
+
365
+ // 节点正文中 input/output 槽位引用必须为 ${name}、${input.name}、${output.name}
366
+ for (const n of nodes) {
367
+ const slots = nodeIdToSlots[n.id] || { inputNames: [], outputNames: [] };
368
+ const body = (getNodeBody(n) || "").trim();
369
+ if (!body) continue;
370
+ const fileLabel = `flow.yaml (${n.id})`;
371
+ const { errors: placeErrors, warnings: placeWarnings } = validatePlaceholdersInBody(
372
+ n.id,
373
+ fileLabel,
374
+ body,
375
+ slots.inputNames,
376
+ slots.outputNames
377
+ );
378
+ errors.push(...placeErrors);
379
+ warnings.push(...placeWarnings);
380
+ }
381
+
382
+ // 拓扑序与环检测(支持环:有环时 order 为部分拓扑序,不报错)
383
+ const { order, hasCycle } = topoSort(nodes, edges);
384
+ const cycleNodes = hasCycle ? nodes.filter((n) => !order.includes(n.id)).map((n) => n.id) : [];
385
+
386
+ // 节点可达性:仅沿「节点」类型边从 start 到 end 的链路;仅文件/文本边连接视为不可达
387
+ const startNodes = nodes.filter((n) => n.definitionId === "control_start").map((n) => n.id);
388
+ const endNodes = nodes.filter((n) => n.definitionId === "control_end").map((n) => n.id);
389
+ const nodeOnlyEdges = [];
390
+ for (const e of edges) {
391
+ if (!e.source || !e.target || !nodeIds.has(e.source) || !nodeIds.has(e.target)) continue;
392
+ const outMatch = e.sourceHandle?.match(/^output-(\d+)$/);
393
+ const inMatch = e.targetHandle?.match(/^input-(\d+)$/);
394
+ if (!outMatch || !inMatch) continue;
395
+ const outIdx = parseInt(outMatch[1], 10);
396
+ const inIdx = parseInt(inMatch[1], 10);
397
+ const srcType = (nodeIdToSlots[e.source]?.outputTypes?.[outIdx] ?? "").trim();
398
+ const tgtType = (nodeIdToSlots[e.target]?.inputTypes?.[inIdx] ?? "").trim();
399
+ if ((srcType === "节点" || srcType === "node") && (tgtType === "节点" || tgtType === "node")) {
400
+ nodeOnlyEdges.push({ source: e.source, target: e.target });
401
+ }
402
+ }
403
+ const nodeReachable = new Set();
404
+ const queue = [...startNodes];
405
+ for (const id of startNodes) nodeReachable.add(id);
406
+ while (queue.length) {
407
+ const cur = queue.shift();
408
+ for (const e of nodeOnlyEdges) {
409
+ if (e.source !== cur || nodeReachable.has(e.target)) continue;
410
+ nodeReachable.add(e.target);
411
+ queue.push(e.target);
412
+ }
413
+ }
414
+ if (endNodes.length > 0 && !endNodes.some((id) => nodeReachable.has(id))) {
415
+ errors.push(
416
+ "从 control_start 到 control_end 无纯「节点」边构成的路径(仅存在文件/文本边),图不可达。请为 start→end 中间节点补充节点类型(节点→节点)的 edge。"
417
+ );
418
+ }
419
+ const nodeReachableOptional = (defId) =>
420
+ defId === "control_start" || defId === "control_end" || defId.startsWith("provide_") || defId === "tool_load_key" || defId === "tool_save_key" || defId === "tool_get_env";
421
+ for (const n of nodes) {
422
+ const defId = n.definitionId || "";
423
+ if (nodeReachableOptional(defId)) continue;
424
+ if (!nodeReachable.has(n.id)) {
425
+ warnings.push(
426
+ `节点 "${n.id}"(${n.label || n.id})无节点边可达:仅通过文件/文本边连接,缺少从 start 到该节点的节点链路,执行顺序无法保证`
427
+ );
428
+ }
429
+ }
430
+
431
+ // 有环时:环的入口节点(从环外进入环内的合并点)必须是 control_anyOne
432
+ if (hasCycle && cycleNodes.length > 0) {
433
+ const cycleSet = new Set(cycleNodes);
434
+ const idToNode = new Map(nodes.map((n) => [n.id, n]));
435
+ for (const nodeId of cycleNodes) {
436
+ // 仅看「节点→节点」边:上游是 provide_file 等时不算环外入边,避免误判
437
+ const incomingFromOutside = nodeOnlyEdges.filter((e) => e.target === nodeId && !cycleSet.has(e.source));
438
+ if (incomingFromOutside.length > 0) {
439
+ const node = idToNode.get(nodeId);
440
+ const defId = node?.definitionId || "";
441
+ if (defId !== "control_anyOne") {
442
+ errors.push(
443
+ `拓扑存在环时,环的入口节点必须是 control_anyOne。节点 "${nodeId}"(definitionId: ${defId})有来自环外的入边,应改为 control_anyOne 节点。`
444
+ );
445
+ }
446
+ }
447
+ }
448
+ }
449
+
450
+ const unreachableNodeIds = nodes
451
+ .filter(
452
+ (n) =>
453
+ n.definitionId &&
454
+ !nodeReachableOptional(n.definitionId) &&
455
+ !nodeReachable.has(n.id)
456
+ )
457
+ .map((n) => n.id);
458
+
459
+ const report = {
460
+ nodesCount: nodes.length,
461
+ edgesCount: edges.length,
462
+ order,
463
+ hasCycle,
464
+ cycleNodes,
465
+ hasStart,
466
+ hasEnd,
467
+ nodeReachable: nodeReachable.size,
468
+ unreachableNodeIds,
469
+ };
470
+
471
+ return {
472
+ ok: errors.length === 0,
473
+ errors,
474
+ warnings,
475
+ fixesApplied,
476
+ report,
477
+ nodes,
478
+ edges,
479
+ edgesToFix,
480
+ };
481
+ }
482
+
483
+ function checkFlowFromYaml(flowDir) {
484
+ const loaded = loadFlowYaml(flowDir);
485
+ if (!loaded) {
486
+ return { ok: false, errors: [`未找到 flow.yaml:${path.join(flowDir, "flow.yaml")}`], warnings: [], fixesApplied: [] };
487
+ }
488
+ const { nodes, edges, instances } = loaded;
489
+ if (nodes.length === 0) {
490
+ return { ok: false, errors: ["flow.yaml 必须包含 instances 且至少一个节点"], warnings: [], fixesApplied: [] };
491
+ }
492
+ const nodeIdToSlots = {};
493
+ for (const n of nodes) {
494
+ nodeIdToSlots[n.id] = getSlotsFromInstance(instances[n.id]);
495
+ }
496
+ const getNodeBody = (n) => (instances[n.id] && instances[n.id].body != null ? String(instances[n.id].body) : "");
497
+ return checkFlowCore(nodes, edges, flowDir, nodeIdToSlots, getNodeBody);
498
+ }
499
+
500
+ function main() {
501
+ const args = process.argv.slice(2);
502
+ const rest = args.filter((a) => a !== "--fix");
503
+
504
+ let flowDir;
505
+ if (rest.length === 1) {
506
+ const arg = rest[0];
507
+ const p = path.resolve(arg);
508
+ const hasFlowYaml = (dir) => fs.existsSync(path.join(dir, "flow.yaml"));
509
+ if (path.basename(p) === "flow.yaml") {
510
+ flowDir = path.dirname(p);
511
+ } else if (hasFlowYaml(p)) {
512
+ flowDir = p;
513
+ } else {
514
+ const cwd = process.cwd();
515
+ flowDir = getFlowDir(cwd, arg) || path.join(cwd, PIPELINES_DIR, arg);
516
+ }
517
+ } else if (rest.length >= 2) {
518
+ const root = path.resolve(rest[0]);
519
+ const name = rest[1];
520
+ if (rest.length >= 3) {
521
+ flowDir = path.resolve(root, rest[2]);
522
+ } else {
523
+ flowDir = getFlowDir(root, name) || path.join(root, PIPELINES_DIR, name);
524
+ }
525
+ } else {
526
+ console.error(JSON.stringify({ ok: false, error: "Usage: agentflow apply -ai check-flow <workspaceRoot> <flowName> [flowDir] | agentflow apply -ai check-flow <flowYamlPath>" }));
527
+ process.exit(1);
528
+ }
529
+
530
+ const result = checkFlowFromYaml(flowDir);
531
+ if (result.fixesApplied === undefined) result.fixesApplied = [];
532
+ delete result.edgesToFix;
533
+
534
+ // 适配 agentflow apply -ai run-tool-nodejs:脚本执行成功则 err_code 恒为 0(有无 error/warning 不影响);process.exit 仍按 result.ok 供 CLI 使用
535
+ const resultPayload = {
536
+ err_code: 0,
537
+ message: { result: JSON.stringify(result, null, 2) },
538
+ };
539
+ console.log(JSON.stringify(resultPayload));
540
+ process.exit(result.ok ? 0 : 1);
541
+ }
542
+
543
+ main();