@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,226 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 产出 PC 用校验结果:edgeTypeMismatch、nodeRoleMissing、nodeModelMissing。
4
+ * 用法:agentflow apply -ai validate-for-ui <workspaceRoot> <flowName> <flowDir> [uuid]
5
+ * 或由 agentflow validate <FlowName> [uuid] 调用;传 uuid 时写入 runDir/intermediate/validation.json。
6
+ * 边 id 约定:source__sourceHandle__target__targetHandle。
7
+ */
8
+
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import yaml from "js-yaml";
12
+
13
+ import { getModelListsAbs, getRunDir, getUserAgentsJsonAbs } from "../lib/paths.mjs";
14
+
15
+ /** 与前端 flowFormat.VALID_ROLES + 内置 id 一致:flow 中 role 可能为展示名或 id */
16
+ const VALID_ROLE_KEYS = ["requirement", "planning", "code", "test", "normal"];
17
+ const ROLE_ZH_TO_KEY = {
18
+ 需求拆解: "requirement",
19
+ 技术规划: "planning",
20
+ 代码执行: "code",
21
+ 测试回归: "test",
22
+ 普通: "normal",
23
+ };
24
+ const VALID_ROLES = new Set([
25
+ ...VALID_ROLE_KEYS,
26
+ ...Object.keys(ROLE_ZH_TO_KEY),
27
+ "前端/UI",
28
+ "agentflow-node-executor-requirement",
29
+ "agentflow-node-executor-planning",
30
+ "agentflow-node-executor-code",
31
+ "agentflow-node-executor-test",
32
+ "agentflow-node-executor",
33
+ "agentflow-node-executor-ui",
34
+ ]);
35
+
36
+ function getSlotsFromInstance(inst) {
37
+ const result = { inputNames: [], outputNames: [], inputTypes: [], outputTypes: [] };
38
+ if (!inst || typeof inst !== "object") return result;
39
+ const inp = Array.isArray(inst.input) ? inst.input : [];
40
+ const out = Array.isArray(inst.output) ? inst.output : [];
41
+ for (const slot of inp) {
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.inputNames.push(name);
46
+ result.inputTypes.push(type);
47
+ }
48
+ }
49
+ for (const slot of out) {
50
+ const name = slot && (slot.name != null) ? String(slot.name).trim() : "";
51
+ const type = slot && (slot.type != null) ? String(slot.type).trim() : "";
52
+ if (name) {
53
+ result.outputNames.push(name);
54
+ result.outputTypes.push(type);
55
+ }
56
+ }
57
+ return result;
58
+ }
59
+
60
+ function loadFlowYaml(flowDir) {
61
+ const filePath = path.join(flowDir, "flow.yaml");
62
+ if (!fs.existsSync(filePath)) return null;
63
+ try {
64
+ const raw = fs.readFileSync(filePath, "utf-8");
65
+ const data = yaml.load(raw);
66
+ if (!data || typeof data !== "object") return null;
67
+ const instances = data.instances && typeof data.instances === "object" ? data.instances : {};
68
+ const edgesRaw = Array.isArray(data.edges) ? data.edges : [];
69
+ const nodeIds = new Set(Object.keys(instances));
70
+ for (const e of edgesRaw) {
71
+ if (e?.source) nodeIds.add(e.source);
72
+ if (e?.target) nodeIds.add(e.target);
73
+ }
74
+ const nodes = Array.from(nodeIds).map((id) => {
75
+ const inst = instances[id] || {};
76
+ return { id, definitionId: inst.definitionId != null ? String(inst.definitionId) : id };
77
+ });
78
+ const edges = edgesRaw
79
+ .filter((e) => e?.source && e?.target)
80
+ .map((e) => ({
81
+ source: String(e.source),
82
+ target: String(e.target),
83
+ sourceHandle: e.sourceHandle ?? "output-0",
84
+ targetHandle: e.targetHandle ?? "input-0",
85
+ }));
86
+ return { nodes, edges, instances };
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ function loadModelLists(_workspaceRoot) {
93
+ const p = getModelListsAbs();
94
+ if (!fs.existsSync(p)) return { cursor: [], opencode: [] };
95
+ try {
96
+ const data = JSON.parse(fs.readFileSync(p, "utf-8"));
97
+ return {
98
+ cursor: Array.isArray(data.cursor) ? data.cursor : [],
99
+ opencode: Array.isArray(data.opencode) ? data.opencode : [],
100
+ };
101
+ } catch {
102
+ return { cursor: [], opencode: [] };
103
+ }
104
+ }
105
+
106
+ /** ~/agentflow/agents.json 仅存用户角色,无 source 字段,取所有条目的 id */
107
+ function loadCustomRoleIds(_workspaceRoot) {
108
+ const agentsPath = getUserAgentsJsonAbs();
109
+ if (!fs.existsSync(agentsPath)) return new Set();
110
+ try {
111
+ const data = JSON.parse(fs.readFileSync(agentsPath, "utf-8"));
112
+ const list = Array.isArray(data) ? data : (data.agents || []);
113
+ return new Set(list.filter((a) => a && typeof a.id === "string" && a.id.trim()).map((a) => a.id.trim()));
114
+ } catch {
115
+ return new Set();
116
+ }
117
+ }
118
+
119
+ function toEdgeId(e) {
120
+ return `${e.source}__${e.sourceHandle || "output-0"}__${e.target}__${e.targetHandle || "input-0"}`;
121
+ }
122
+
123
+ function computeValidation(flowDir, workspaceRoot) {
124
+ const loaded = loadFlowYaml(flowDir);
125
+ const edgeTypeMismatch = [];
126
+ const nodeRoleMissing = [];
127
+ const nodeModelMissing = [];
128
+
129
+ if (!loaded || loaded.nodes.length === 0) {
130
+ return { validation: { edgeTypeMismatch, nodeRoleMissing, nodeModelMissing }, ok: false };
131
+ }
132
+
133
+ const { nodes, edges, instances } = loaded;
134
+ const nodeIds = new Set(nodes.map((n) => n.id));
135
+ const nodeIdToSlots = {};
136
+ for (const n of nodes) {
137
+ nodeIdToSlots[n.id] = getSlotsFromInstance(instances[n.id]);
138
+ }
139
+
140
+ for (const e of edges) {
141
+ if (!nodeIds.has(e.source) || !nodeIds.has(e.target)) continue;
142
+ const outMatch = (e.sourceHandle || "output-0").match(/^output-(\d+)$/);
143
+ const inMatch = (e.targetHandle || "input-0").match(/^input-(\d+)$/);
144
+ if (!outMatch || !inMatch) continue;
145
+ const outIdx = parseInt(outMatch[1], 10);
146
+ const inIdx = parseInt(inMatch[1], 10);
147
+ const srcSlots = nodeIdToSlots[e.source];
148
+ const tgtSlots = nodeIdToSlots[e.target];
149
+ const srcType = (srcSlots?.outputTypes?.[outIdx] ?? "").trim();
150
+ const tgtType = (tgtSlots?.inputTypes?.[inIdx] ?? "").trim();
151
+ if (srcType && tgtType && srcType !== tgtType) {
152
+ edgeTypeMismatch.push(toEdgeId(e));
153
+ }
154
+ }
155
+
156
+ const validRoles = new Set(VALID_ROLES);
157
+ if (workspaceRoot) {
158
+ for (const id of loadCustomRoleIds(workspaceRoot)) validRoles.add(id);
159
+ }
160
+ for (const n of nodes) {
161
+ const role = (instances[n.id] && instances[n.id].role != null) ? String(instances[n.id].role).trim() : "";
162
+ if (role && !validRoles.has(role)) nodeRoleMissing.push(n.id);
163
+ }
164
+
165
+ const root = workspaceRoot ? path.resolve(workspaceRoot) : flowDir;
166
+ const { cursor: cursorList, opencode: opencodeList } = loadModelLists(root);
167
+ const opencodeSet = new Set((opencodeList || []).map((s) => String(s).trim()));
168
+ const cursorSet = new Set(
169
+ (cursorList || []).map((s) => {
170
+ const t = String(s).trim();
171
+ const first = t.split(/\s+-/)[0].trim();
172
+ return first || t;
173
+ })
174
+ );
175
+ for (const n of nodes) {
176
+ const model = (instances[n.id] && instances[n.id].model != null) ? String(instances[n.id].model).trim() : "";
177
+ if (!model) continue;
178
+ let valid = false;
179
+ if (model.startsWith("opencode:")) {
180
+ valid = opencodeSet.has(model.slice(9).trim());
181
+ } else {
182
+ const cursorId = model.startsWith("cursor:") ? model.slice(7).trim() : model;
183
+ valid = cursorSet.has(cursorId) || (cursorList || []).some((c) => String(c).trim() === model || String(c).trim().startsWith(cursorId + " "));
184
+ }
185
+ if (!valid) nodeModelMissing.push(n.id);
186
+ }
187
+
188
+ const ok = edgeTypeMismatch.length === 0 && nodeRoleMissing.length === 0 && nodeModelMissing.length === 0;
189
+ return { validation: { edgeTypeMismatch, nodeRoleMissing, nodeModelMissing }, ok };
190
+ }
191
+
192
+ function main() {
193
+ const argv = process.argv.slice(2);
194
+ if (argv.length < 3) {
195
+ console.error(JSON.stringify({ ok: false, error: "Usage: validate-for-ui.mjs <workspaceRoot> <flowName> <flowDir> [uuid]" }));
196
+ process.exit(1);
197
+ }
198
+ const workspaceRoot = path.resolve(argv[0]);
199
+ const flowName = argv[1];
200
+ const flowDir = path.resolve(argv[2]);
201
+ const uuid = argv.length >= 4 && /^\d{14}$/.test(String(argv[3]).trim()) ? String(argv[3]).trim() : null;
202
+
203
+ if (!fs.existsSync(path.join(flowDir, "flow.yaml"))) {
204
+ console.error(JSON.stringify({ ok: false, error: "flow.yaml not found in " + flowDir }));
205
+ process.exit(1);
206
+ }
207
+
208
+ const { validation, ok } = computeValidation(flowDir, workspaceRoot);
209
+
210
+ if (uuid && flowName) {
211
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
212
+ const intermediateDir = path.join(runDir, "intermediate");
213
+ try {
214
+ fs.mkdirSync(intermediateDir, { recursive: true });
215
+ fs.writeFileSync(path.join(intermediateDir, "validation.json"), JSON.stringify({ validation }, null, 2), "utf-8");
216
+ } catch (err) {
217
+ console.error(JSON.stringify({ ok: false, error: err.message }));
218
+ process.exit(1);
219
+ }
220
+ }
221
+
222
+ console.log(JSON.stringify({ ok, validation }));
223
+ process.exit(ok ? 0 : 1);
224
+ }
225
+
226
+ main();
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 解析 tool_nodejs 脚本 stdout 为结构化 payload。
4
+ *
5
+ * 两种模式:
6
+ * 1. JSON 模式:stdout 含 { "err_code": number, "message": { "result": "..." } }
7
+ * → 直接使用,err_code 可覆盖 exit code 语义(向后兼容)。
8
+ * 2. 纯文本模式:stdout 不含合法 JSON
9
+ * → 自动包装为 { err_code: -1, message: { result: <stdout> }, _synthetic: true }
10
+ * → 调用方应以进程 exit code 决定成败。
11
+ *
12
+ * 用法:
13
+ * node validate-script-output.mjs # 从 stdin 读取
14
+ * node validate-script-output.mjs <file> # 从文件读取
15
+ *
16
+ * 输出(stdout JSON):{ "ok": boolean, "errors": string[], "payload": object | null }
17
+ * 退出码:0 解析成功,1 失败
18
+ */
19
+
20
+ import fs from "fs";
21
+ import path from "path";
22
+ import { fileURLToPath } from "url";
23
+
24
+ /**
25
+ * 从文本中提取第一个完整 JSON 对象并解析。
26
+ * 支持:单行 JSON、多行(pretty-print)JSON、stdout 前有日志的情况。
27
+ */
28
+ function parsePayload(text) {
29
+ if (!text || typeof text !== "string") return null;
30
+ const raw = text.trimStart().replace(/^\uFEFF/, "");
31
+ const start = raw.indexOf("{");
32
+ if (start < 0) return null;
33
+ let depth = 0;
34
+ let inString = false;
35
+ let escape = false;
36
+ let end = -1;
37
+ for (let i = start; i < raw.length; i++) {
38
+ const c = raw[i];
39
+ if (escape) {
40
+ escape = false;
41
+ continue;
42
+ }
43
+ if (inString) {
44
+ if (c === "\\") escape = true;
45
+ else if (c === '"') inString = false;
46
+ continue;
47
+ }
48
+ if (c === '"') {
49
+ inString = true;
50
+ continue;
51
+ }
52
+ if (c === "{") depth++;
53
+ else if (c === "}") {
54
+ depth--;
55
+ if (depth === 0) {
56
+ end = i + 1;
57
+ break;
58
+ }
59
+ }
60
+ }
61
+ const slice = end > 0 ? raw.slice(start, end) : raw.slice(start).split("\n")[0];
62
+ try {
63
+ return JSON.parse(slice);
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * 解析脚本 stdout 为结构化 payload。
71
+ *
72
+ * @param {string} stdoutText
73
+ * @returns {{ ok: boolean, errors: string[], payload: { err_code: number, message: object, _synthetic?: boolean } | null }}
74
+ * - JSON 模式:payload 含脚本输出的 err_code + message
75
+ * - 纯文本模式:payload._synthetic = true,err_code = -1(由调用方按 exit code 判断)
76
+ * - stdout 为空:ok = false
77
+ */
78
+ export function validateAndParse(stdoutText) {
79
+ const errors = [];
80
+ const jsonPayload = parsePayload(stdoutText);
81
+
82
+ if (jsonPayload) {
83
+ if (typeof jsonPayload.err_code !== "number") {
84
+ errors.push("JSON 缺少 err_code(数字)");
85
+ }
86
+ if (!jsonPayload.message || typeof jsonPayload.message !== "object" || Array.isArray(jsonPayload.message)) {
87
+ errors.push("JSON 缺少 message(对象)");
88
+ }
89
+ if (errors.length > 0) {
90
+ return { ok: false, errors, payload: null };
91
+ }
92
+ return { ok: true, errors: [], payload: jsonPayload };
93
+ }
94
+
95
+ if (stdoutText && stdoutText.trim()) {
96
+ return {
97
+ ok: true,
98
+ errors: [],
99
+ payload: { err_code: -1, message: { result: stdoutText.trim() }, _synthetic: true },
100
+ };
101
+ }
102
+
103
+ errors.push("stdout 为空");
104
+ return { ok: false, errors, payload: null };
105
+ }
106
+
107
+ function main() {
108
+ const args = process.argv.slice(2);
109
+
110
+ let input = "";
111
+ if (args.length > 0 && !args[0].startsWith("--")) {
112
+ try {
113
+ input = fs.readFileSync(args[0], "utf-8");
114
+ } catch (e) {
115
+ console.log(JSON.stringify({ ok: false, errors: [e.message], payload: null }));
116
+ process.exit(1);
117
+ }
118
+ } else {
119
+ input = fs.readFileSync(0, "utf-8");
120
+ }
121
+
122
+ const { ok, errors, payload } = validateAndParse(input);
123
+ console.log(JSON.stringify({ ok, errors, payload }));
124
+ process.exit(ok ? 0 : 1);
125
+ }
126
+
127
+ const _url = fileURLToPath(import.meta.url);
128
+ const _dir = path.dirname(_url);
129
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(_dir, "validate-script-output.mjs");
130
+ if (isMain) main();
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 统一写入/更新 intermediate/<instanceId>.result.md,保证 frontmatter 格式一致。
4
+ * 供前处理(写入 running)、执行后规范化、后处理(覆写 status/branch 等)共同使用。
5
+ *
6
+ * 用法(CLI):
7
+ * node write-result.mjs <workspaceRoot> <flowName> <uuid> <instanceId> --json '<JSON>'
8
+ * JSON 字段:status(必), message(必), finishedAt?, outputPath?, branch?, elapsedMs?, body?(cacheMd5/cacheInputInfo 仅存 .cache.json,不写入 result)
9
+ * 若传入 body 则使用;否则若文件已存在且未传 body,则保留原正文(preserveBody)。
10
+ *
11
+ * 用法(模块):
12
+ * import { writeResult } from "./write-result.mjs";
13
+ * writeResult(workspaceRoot, flowName, uuid, instanceId, fields, options?)
14
+ * fields: { status, message, finishedAt?, outputPath?, branch?, cacheNotMetReason?, elapsedMs? }
15
+ * options: { preserveBody?: boolean, body?: string, execId?: number }
16
+ */
17
+
18
+ import fs from "fs";
19
+ import path from "path";
20
+
21
+ import { getRunDir } from "../lib/paths.mjs";
22
+ import { loadExecId } from "./get-exec-id.mjs";
23
+ import { intermediateResultBasename, intermediateDirForNode } from "./get-exec-id.mjs";
24
+ import { logToRunTag } from "./run-log.mjs";
25
+
26
+ /**
27
+ * 转义 YAML frontmatter 中的字符串值(可含冒号、换行时用双引号并转义)
28
+ */
29
+ function escapeYamlValue(val) {
30
+ if (val == null) return '""';
31
+ const s = String(val);
32
+ if (/[\n"\\:]/.test(s)) {
33
+ return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n") + '"';
34
+ }
35
+ return '"' + s + '"';
36
+ }
37
+
38
+ /**
39
+ * 从已有 result 文件中截取正文(frontmatter 之后的部分)
40
+ */
41
+ function getExistingBody(resultPath) {
42
+ if (!fs.existsSync(resultPath)) return "";
43
+ const raw = fs.readFileSync(resultPath, "utf-8");
44
+ const match = raw.match(/---\s*\r?\n[\s\S]*?\r?\n---\s*\r?\n([\s\S]*)$/);
45
+ return match ? match[1] : "";
46
+ }
47
+
48
+ /**
49
+ * 统一写入 result 文件。
50
+ * cacheMd5/cacheInputInfo 仅保存在 <instanceId>.cache.json,不写入 result。
51
+ * @param {string} workspaceRoot - 工作区根目录
52
+ * @param {string} flowName - 流程名
53
+ * @param {string} uuid - 本次 run 的 uuid
54
+ * @param {string} instanceId - 节点 instance id
55
+ * @param {{ status: string, message: string, finishedAt?: string, outputPath?: string, branch?: string, cacheNotMetReason?: string, elapsedMs?: number }} fields - 必填 status、message;可选其余(elapsedMs 为节点执行耗时毫秒,供 UI 展示)
56
+ * @param {{ preserveBody?: boolean, body?: string, execId?: number }} [options] - preserveBody:保留已有正文;body:指定正文内容;execId:本轮 execId,缺省则从 memory 读取
57
+ */
58
+ export function writeResult(workspaceRoot, flowName, uuid, instanceId, fields, options = {}) {
59
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
60
+ const execId = options.execId ?? loadExecId(workspaceRoot, flowName, uuid, instanceId);
61
+ const resultBasename = intermediateResultBasename(instanceId, execId);
62
+ const resultPath = path.join(runDir, intermediateDirForNode(instanceId), resultBasename);
63
+
64
+ /** 调度只认 success;Agent 可能传 completed/done,在此统一写成 success */
65
+ let status = fields.status ?? "success";
66
+ if (typeof status === "string" && (status.toLowerCase() === "completed" || status.toLowerCase() === "done")) {
67
+ status = "success";
68
+ }
69
+ const message = fields.message ?? "";
70
+ const finishedAt = fields.finishedAt ?? new Date().toISOString();
71
+ const outputPath = fields.outputPath;
72
+ const branch = fields.branch;
73
+ const cacheNotMetReason = fields.cacheNotMetReason;
74
+ const elapsedMs =
75
+ fields.elapsedMs != null && Number.isFinite(fields.elapsedMs) && fields.elapsedMs >= 0 ? Math.round(fields.elapsedMs) : undefined;
76
+
77
+ let body = options.body;
78
+ if (body === undefined && options.preserveBody !== false) {
79
+ body = getExistingBody(resultPath);
80
+ }
81
+ if (body === undefined) body = "";
82
+ // 备份由 snapshotPriorRoundIfNeeded 在 pre-process 入口统一处理,此处只覆盖写。
83
+
84
+ const lines = [
85
+ "---",
86
+ `status: ${escapeYamlValue(status)}`,
87
+ `execId: ${escapeYamlValue(execId)}`,
88
+ ];
89
+ if (branch !== undefined && branch !== null) {
90
+ lines.push(`branch: ${escapeYamlValue(branch)}`);
91
+ }
92
+ lines.push(
93
+ `message: ${escapeYamlValue(message)}`,
94
+ `finishedAt: ${escapeYamlValue(finishedAt)}`
95
+ );
96
+ if (outputPath !== undefined && outputPath !== null && outputPath !== "") {
97
+ lines.push(`outputPath: ${escapeYamlValue(outputPath)}`);
98
+ }
99
+ if (cacheNotMetReason !== undefined && cacheNotMetReason !== null && cacheNotMetReason !== "") {
100
+ lines.push(`cacheNotMetReason: ${escapeYamlValue(cacheNotMetReason)}`);
101
+ }
102
+ if (elapsedMs !== undefined) {
103
+ lines.push(`elapsedMs: ${elapsedMs}`);
104
+ }
105
+ lines.push("---");
106
+ const content = lines.join("\n") + "\n" + (body ? "\n" + body : "");
107
+
108
+ fs.mkdirSync(path.dirname(resultPath), { recursive: true });
109
+ fs.writeFileSync(resultPath, content, "utf-8");
110
+ logToRunTag(workspaceRoot, flowName, uuid, "result", {
111
+ instanceId,
112
+ status,
113
+ message: message || undefined,
114
+ branch: branch ?? undefined,
115
+ cacheNotMetReason: cacheNotMetReason ?? undefined,
116
+ resultPathRel: path.relative(runDir, resultPath),
117
+ });
118
+ }
119
+
120
+ function main() {
121
+ const args = process.argv.slice(2);
122
+ const jsonIdx = args.indexOf("--json");
123
+ if (args.length < 5 || jsonIdx === -1 || !args[jsonIdx + 1]) {
124
+ console.error(
125
+ JSON.stringify({
126
+ ok: false,
127
+ error:
128
+ "Usage: node write-result.mjs <workspaceRoot> <flowName> <uuid> <instanceId> --json '<JSON>'",
129
+ })
130
+ );
131
+ process.exit(1);
132
+ }
133
+
134
+ const [root, flowName, uuid, instanceId] = args.slice(0, jsonIdx);
135
+ const jsonStr = args[jsonIdx + 1];
136
+ const workspaceRoot = path.resolve(root);
137
+
138
+ let payload;
139
+ try {
140
+ payload = JSON.parse(jsonStr);
141
+ } catch (e) {
142
+ console.error(JSON.stringify({ ok: false, error: "Invalid JSON: " + e.message }));
143
+ process.exit(1);
144
+ }
145
+
146
+ if (!payload.status || !payload.message) {
147
+ console.error(
148
+ JSON.stringify({ ok: false, error: "JSON must include status and message" })
149
+ );
150
+ process.exit(1);
151
+ }
152
+
153
+ const fields = {
154
+ status: payload.status,
155
+ message: payload.message,
156
+ finishedAt: payload.finishedAt,
157
+ outputPath: payload.outputPath,
158
+ branch: payload.branch,
159
+ cacheNotMetReason: payload.cacheNotMetReason,
160
+ elapsedMs: payload.elapsedMs,
161
+ };
162
+ const options = {};
163
+ if (payload.body !== undefined) options.body = payload.body;
164
+ else options.preserveBody = true;
165
+ if (payload.execId !== undefined) options.execId = Number(payload.execId) || 1;
166
+
167
+ try {
168
+ writeResult(workspaceRoot, flowName, uuid, instanceId, fields, options);
169
+ console.log(JSON.stringify({ ok: true }));
170
+ } catch (err) {
171
+ console.error(JSON.stringify({ ok: false, error: err.message }));
172
+ process.exit(1);
173
+ }
174
+ }
175
+
176
+ const isMain =
177
+ typeof process !== "undefined" &&
178
+ process.argv[1] &&
179
+ (process.argv[1].endsWith("write-result.mjs") || process.argv[1].endsWith("write-result.js"));
180
+ if (isMain) {
181
+ main();
182
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ # 内置节点:SubAgent
3
+ description: 利用SubAgent执行任务
4
+ displayName: SubAgent
5
+ input:
6
+ - type: node
7
+ name: prev
8
+ default: ""
9
+ output:
10
+ - type: node
11
+ name: next
12
+ default: ""
13
+ ---
14
+ ${USER_PROMPT}
@@ -0,0 +1,20 @@
1
+ ---
2
+ # 内置节点:AI 转布尔(agent 执行)
3
+ description: "AI-powered boolean judgment: an agent evaluates the input value and writes true/false to prediction. Use for non-deterministic scenarios requiring semantic understanding."
4
+ displayName: Agent ToBool
5
+ input:
6
+ - type: node
7
+ name: prev
8
+ default: ""
9
+ - type: text
10
+ name: value
11
+ default: ""
12
+ output:
13
+ - type: node
14
+ name: next
15
+ default: ""
16
+ - type: bool
17
+ name: prediction
18
+ default: ""
19
+ ---
20
+ ${USER_PROMPT}
@@ -0,0 +1,17 @@
1
+ ---
2
+ # 内置节点:任一满足
3
+ description: Continues to next when any upstream input is ready
4
+ displayName: AnyOne
5
+ input:
6
+ - type: node
7
+ name: prev1
8
+ default: ""
9
+ - type: node
10
+ name: prev2
11
+ default: ""
12
+ output:
13
+ - type: node
14
+ name: next
15
+ default: ""
16
+ ---
17
+ ${USER_PROMPT}
@@ -0,0 +1,11 @@
1
+ ---
2
+ # 内置节点:结束
3
+ description: End point of AgentFlow, flow terminates after this node
4
+ displayName: End
5
+ input:
6
+ - type: node
7
+ name: prev
8
+ default: ""
9
+ output: []
10
+ ---
11
+ ${USER_PROMPT}
@@ -0,0 +1,20 @@
1
+ ---
2
+ # 内置节点:If 分支
3
+ description: Has exactly one bool type input. Continues to next1 if true, next2 if false
4
+ displayName: If
5
+ input:
6
+ - type: node
7
+ name: prev
8
+ default: ""
9
+ - type: bool
10
+ name: prediction
11
+ default: ""
12
+ output:
13
+ - type: node
14
+ name: next1
15
+ default: ""
16
+ - type: node
17
+ name: next2
18
+ default: ""
19
+ ---
20
+ ${USER_PROMPT}
@@ -0,0 +1,11 @@
1
+ ---
2
+ # 内置节点:开始
3
+ description: Entry point of AgentFlow, all flows should start from this node
4
+ displayName: Start
5
+ input: []
6
+ output:
7
+ - type: node
8
+ name: next
9
+ default: ""
10
+ ---
11
+ ${USER_PROMPT}
@@ -0,0 +1,21 @@
1
+ ---
2
+ # 内置节点:转布尔(本地脚本执行,★ 可扩展输入)
3
+ description: "Script-based boolean conversion: executes script to produce true/false prediction. Like tool_nodejs but enforces bool output. Must have script field."
4
+ displayName: ToBool
5
+ input:
6
+ - type: node
7
+ name: prev
8
+ default: ""
9
+ - type: text
10
+ name: value
11
+ default: ""
12
+ output:
13
+ - type: node
14
+ name: next
15
+ default: ""
16
+ - type: bool
17
+ name: prediction
18
+ default: ""
19
+ extensible: true
20
+ ---
21
+ ${USER_PROMPT}
@@ -0,0 +1,11 @@
1
+ ---
2
+ # 内置节点:直接提供文件
3
+ description: Provide file path or content directly, value will be passed to downstream as-is
4
+ displayName: File
5
+ input: []
6
+ output:
7
+ - type: file
8
+ name: value
9
+ default: ""
10
+ ---
11
+ ${USER_PROMPT}
@@ -0,0 +1,11 @@
1
+ ---
2
+ # 内置节点:直接提供文本
3
+ description: Provide a text value directly, value will be passed to downstream as-is
4
+ displayName: String
5
+ input: []
6
+ output:
7
+ - type: text
8
+ name: value
9
+ default: ""
10
+ ---
11
+ ${USER_PROMPT}