@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,227 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { getFlowYamlAbs } from "./catalog-flows.mjs";
4
+
5
+ export const SCHEDULE_CONFIG_FILENAME = "schedule.json";
6
+ export const SCHEDULE_STATE_FILENAME = "schedule-state.json";
7
+
8
+ const DEFAULT_SCHEDULE = {
9
+ enabled: false,
10
+ cron: "",
11
+ timezone: "Asia/Shanghai",
12
+ preset: "",
13
+ overlapPolicy: "skip",
14
+ misfirePolicy: "skip",
15
+ };
16
+
17
+ const CRON_FIELDS = [
18
+ { name: "minute", min: 0, max: 59 },
19
+ { name: "hour", min: 0, max: 23 },
20
+ { name: "dayOfMonth", min: 1, max: 31 },
21
+ { name: "month", min: 1, max: 12 },
22
+ { name: "dayOfWeek", min: 0, max: 7 },
23
+ ];
24
+
25
+ const VALID_OVERLAP_POLICIES = new Set(["skip"]);
26
+ const VALID_MISFIRE_POLICIES = new Set(["skip"]);
27
+
28
+ function normalizeTimezone(tz) {
29
+ const value = typeof tz === "string" && tz.trim() ? tz.trim() : DEFAULT_SCHEDULE.timezone;
30
+ try {
31
+ new Intl.DateTimeFormat("en-US", { timeZone: value }).format(new Date());
32
+ return value;
33
+ } catch {
34
+ throw new Error(`Invalid timezone: ${value}`);
35
+ }
36
+ }
37
+
38
+ function parseCronNumber(raw, field) {
39
+ if (!/^\d+$/.test(raw)) throw new Error(`Invalid ${field.name} value: ${raw}`);
40
+ const n = Number(raw);
41
+ const normalized = field.name === "dayOfWeek" && n === 7 ? 0 : n;
42
+ if (n < field.min || n > field.max) throw new Error(`Invalid ${field.name} value: ${raw}`);
43
+ return normalized;
44
+ }
45
+
46
+ function addRange(values, field, start, end, step) {
47
+ if (step <= 0) throw new Error(`Invalid ${field.name} step`);
48
+ if (start > end) throw new Error(`Invalid ${field.name} range`);
49
+ for (let n = start; n <= end; n += step) {
50
+ values.add(field.name === "dayOfWeek" && n === 7 ? 0 : n);
51
+ }
52
+ }
53
+
54
+ function parseCronField(raw, field) {
55
+ const text = String(raw || "").trim();
56
+ if (!text) throw new Error(`Missing ${field.name}`);
57
+ const values = new Set();
58
+ values.cronWildcard = text === "*";
59
+ for (const partRaw of text.split(",")) {
60
+ const part = partRaw.trim();
61
+ if (!part) throw new Error(`Invalid ${field.name} field`);
62
+ const [rangeRaw, stepRaw] = part.split("/");
63
+ if (part.split("/").length > 2) throw new Error(`Invalid ${field.name} step`);
64
+ const step = stepRaw == null ? 1 : parseCronNumber(stepRaw, { ...field, min: 1 });
65
+ if (rangeRaw === "*") {
66
+ addRange(values, field, field.min, field.max, step);
67
+ continue;
68
+ }
69
+ const dashIdx = rangeRaw.indexOf("-");
70
+ if (dashIdx >= 0) {
71
+ const start = parseCronNumber(rangeRaw.slice(0, dashIdx), field);
72
+ const end = parseCronNumber(rangeRaw.slice(dashIdx + 1), field);
73
+ addRange(values, field, start, end, step);
74
+ continue;
75
+ }
76
+ if (stepRaw != null) throw new Error(`Invalid ${field.name} step target`);
77
+ values.add(parseCronNumber(rangeRaw, field));
78
+ }
79
+ return values;
80
+ }
81
+
82
+ export function parseCronExpression(cron) {
83
+ const parts = String(cron || "").trim().split(/\s+/).filter(Boolean);
84
+ if (parts.length !== 5) {
85
+ throw new Error("Cron must contain 5 fields: minute hour day month weekday");
86
+ }
87
+ return CRON_FIELDS.map((field, i) => parseCronField(parts[i], field));
88
+ }
89
+
90
+ function zonedParts(date, timeZone) {
91
+ const dtf = new Intl.DateTimeFormat("en-US", {
92
+ timeZone,
93
+ year: "numeric",
94
+ month: "2-digit",
95
+ day: "2-digit",
96
+ hour: "2-digit",
97
+ minute: "2-digit",
98
+ second: "2-digit",
99
+ hour12: false,
100
+ });
101
+ const out = {};
102
+ for (const p of dtf.formatToParts(date)) {
103
+ if (p.type !== "literal") out[p.type] = Number(p.value);
104
+ }
105
+ if (out.hour === 24) out.hour = 0;
106
+ return out;
107
+ }
108
+
109
+ function cronMatches(parsed, parts) {
110
+ const [minutes, hours, days, months, weekdays] = parsed;
111
+ const weekday = new Date(Date.UTC(parts.year, parts.month - 1, parts.day)).getUTCDay();
112
+ const dayMatches =
113
+ days.cronWildcard || weekdays.cronWildcard
114
+ ? days.has(parts.day) && weekdays.has(weekday)
115
+ : days.has(parts.day) || weekdays.has(weekday);
116
+ return (
117
+ minutes.has(parts.minute) &&
118
+ hours.has(parts.hour) &&
119
+ months.has(parts.month) &&
120
+ dayMatches
121
+ );
122
+ }
123
+
124
+ export function computeNextRunAt(cron, timezone, fromDate = new Date()) {
125
+ const parsed = parseCronExpression(cron);
126
+ const tz = normalizeTimezone(timezone);
127
+ const startMs = Math.floor(fromDate.getTime() / 60000) * 60000 + 60000;
128
+ const maxMinutes = 366 * 24 * 60;
129
+ for (let i = 0; i < maxMinutes; i++) {
130
+ const candidate = new Date(startMs + i * 60000);
131
+ if (cronMatches(parsed, zonedParts(candidate, tz))) return candidate.toISOString();
132
+ }
133
+ throw new Error("Could not find next run within one year");
134
+ }
135
+
136
+ export function computeNextRunAtFromSchedule(schedule, fromDate = new Date()) {
137
+ const normalized = normalizeSchedule(schedule);
138
+ if (!normalized.cron) return null;
139
+ return computeNextRunAt(normalized.cron, normalized.timezone, fromDate);
140
+ }
141
+
142
+ export function normalizeSchedule(raw) {
143
+ const src = raw && typeof raw === "object" ? raw : {};
144
+ const enabled = Boolean(src.enabled);
145
+ const cron = typeof src.cron === "string" ? src.cron.trim() : "";
146
+ const timezone = normalizeTimezone(src.timezone);
147
+ const preset = typeof src.preset === "string" ? src.preset.trim() : "";
148
+ const overlapPolicy = VALID_OVERLAP_POLICIES.has(src.overlapPolicy) ? src.overlapPolicy : DEFAULT_SCHEDULE.overlapPolicy;
149
+ const misfirePolicy = VALID_MISFIRE_POLICIES.has(src.misfirePolicy) ? src.misfirePolicy : DEFAULT_SCHEDULE.misfirePolicy;
150
+ let nextRunAt = null;
151
+ if (cron) {
152
+ parseCronExpression(cron);
153
+ nextRunAt = computeNextRunAt(cron, timezone);
154
+ } else if (enabled) {
155
+ throw new Error("Cron is required when schedule is enabled");
156
+ }
157
+ return { enabled, cron, timezone, preset, overlapPolicy, misfirePolicy, nextRunAt };
158
+ }
159
+
160
+ function readJsonIfExists(filePath) {
161
+ if (!fs.existsSync(filePath)) return null;
162
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
163
+ }
164
+
165
+ export function readScheduleState(workspaceRoot, flowId, flowSource, opts = {}) {
166
+ const paths = getFlowSchedulePaths(workspaceRoot, flowId, flowSource, opts);
167
+ if (paths.error) return { success: false, error: paths.error };
168
+ try {
169
+ return { success: true, state: readJsonIfExists(paths.statePath) || {}, statePath: paths.statePath };
170
+ } catch (e) {
171
+ return { success: false, error: e && e.message ? e.message : String(e) };
172
+ }
173
+ }
174
+
175
+ export function writeScheduleState(workspaceRoot, flowId, flowSource, state, opts = {}) {
176
+ const paths = getFlowSchedulePaths(workspaceRoot, flowId, flowSource, opts);
177
+ if (paths.error) return { success: false, error: paths.error };
178
+ try {
179
+ fs.writeFileSync(paths.statePath, JSON.stringify(state && typeof state === "object" ? state : {}, null, 2) + "\n", "utf-8");
180
+ return { success: true, statePath: paths.statePath };
181
+ } catch (e) {
182
+ return { success: false, error: e && e.message ? e.message : String(e) };
183
+ }
184
+ }
185
+
186
+ export function getFlowSchedulePaths(workspaceRoot, flowId, flowSource, opts = {}) {
187
+ const yamlRes = getFlowYamlAbs(workspaceRoot, flowId, flowSource, opts);
188
+ if (yamlRes.error || !yamlRes.path) return { error: yamlRes.error || "Could not resolve flow.yaml" };
189
+ const flowDir = path.dirname(yamlRes.path);
190
+ return {
191
+ flowDir,
192
+ configPath: path.join(flowDir, SCHEDULE_CONFIG_FILENAME),
193
+ statePath: path.join(flowDir, SCHEDULE_STATE_FILENAME),
194
+ };
195
+ }
196
+
197
+ export function readFlowSchedule(workspaceRoot, flowId, flowSource, opts = {}) {
198
+ const paths = getFlowSchedulePaths(workspaceRoot, flowId, flowSource, opts);
199
+ if (paths.error) return { success: false, error: paths.error };
200
+ try {
201
+ const configRaw = readJsonIfExists(paths.configPath) || DEFAULT_SCHEDULE;
202
+ const state = readJsonIfExists(paths.statePath) || {};
203
+ return { success: true, schedule: normalizeSchedule(configRaw), state };
204
+ } catch (e) {
205
+ return { success: false, error: e && e.message ? e.message : String(e) };
206
+ }
207
+ }
208
+
209
+ export function writeFlowSchedule(workspaceRoot, flowId, flowSource, schedule, opts = {}) {
210
+ const paths = getFlowSchedulePaths(workspaceRoot, flowId, flowSource, opts);
211
+ if (paths.error) return { success: false, error: paths.error };
212
+ try {
213
+ const normalized = normalizeSchedule(schedule);
214
+ const toWrite = {
215
+ enabled: normalized.enabled,
216
+ cron: normalized.cron,
217
+ timezone: normalized.timezone,
218
+ preset: normalized.preset,
219
+ overlapPolicy: normalized.overlapPolicy,
220
+ misfirePolicy: normalized.misfirePolicy,
221
+ };
222
+ fs.writeFileSync(paths.configPath, JSON.stringify(toWrite, null, 2) + "\n", "utf-8");
223
+ return { success: true, schedule: normalized };
224
+ } catch (e) {
225
+ return { success: false, error: e && e.message ? e.message : String(e) };
226
+ }
227
+ }
@@ -0,0 +1,312 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { spawn } from "child_process";
4
+ import yaml from "js-yaml";
5
+ import { listFlowsJson } from "./catalog-flows.mjs";
6
+ import {
7
+ computeNextRunAtFromSchedule,
8
+ readFlowSchedule,
9
+ readScheduleState,
10
+ writeScheduleState,
11
+ } from "./schedule-config.mjs";
12
+ import { getRunDir, PACKAGE_ROOT } from "./paths.mjs";
13
+ import { isApplyProcessAlive } from "./run-apply-active-lock.mjs";
14
+ import { log } from "./log.mjs";
15
+
16
+ const DEFAULT_POLL_MS = 30_000;
17
+ const RUN_CONFIG_FILENAME = "run-config.json";
18
+
19
+ function sleep(ms) {
20
+ return new Promise((resolve) => setTimeout(resolve, ms));
21
+ }
22
+
23
+ function scheduleIdentity(schedule) {
24
+ return [
25
+ schedule.enabled ? "1" : "0",
26
+ schedule.cron || "",
27
+ schedule.timezone || "",
28
+ schedule.preset || "",
29
+ schedule.overlapPolicy || "skip",
30
+ schedule.misfirePolicy || "skip",
31
+ ].join("\t");
32
+ }
33
+
34
+ function readRunConfig(flowDir) {
35
+ const configPath = path.join(flowDir, RUN_CONFIG_FILENAME);
36
+ if (!fs.existsSync(configPath)) return { presets: {}, activePreset: null };
37
+ try {
38
+ const data = JSON.parse(fs.readFileSync(configPath, "utf-8"));
39
+ return {
40
+ presets: data.presets && typeof data.presets === "object" ? data.presets : {},
41
+ activePreset: typeof data.activePreset === "string" ? data.activePreset : null,
42
+ };
43
+ } catch {
44
+ return { presets: {}, activePreset: null };
45
+ }
46
+ }
47
+
48
+ function buildCliInputArgs(flowDir, presetName) {
49
+ const cfg = readRunConfig(flowDir);
50
+ const name = presetName || cfg.activePreset || "";
51
+ const preset = name && cfg.presets && typeof cfg.presets[name] === "object" ? cfg.presets[name] : null;
52
+ if (!preset) return [];
53
+ let flow;
54
+ try {
55
+ flow = yaml.load(fs.readFileSync(path.join(flowDir, "flow.yaml"), "utf-8"));
56
+ } catch {
57
+ flow = null;
58
+ }
59
+ const instances = flow && typeof flow === "object" && flow.instances && typeof flow.instances === "object" ? flow.instances : {};
60
+ const edges = flow && typeof flow === "object" && Array.isArray(flow.edges) ? flow.edges : [];
61
+ const provideToInputName = {};
62
+ for (const e of edges) {
63
+ if (!e || !e.source || !e.target) continue;
64
+ const source = String(e.source);
65
+ const target = String(e.target);
66
+ if (!preset.hasOwnProperty(source)) continue;
67
+ const sourceInst = instances[source] || {};
68
+ const sourceDef = String(sourceInst.definitionId || "");
69
+ if (!sourceDef.startsWith("provide_")) continue;
70
+ const m = /^input-(\d+)$/.exec(String(e.targetHandle || ""));
71
+ if (!m) continue;
72
+ const targetInst = instances[target] || {};
73
+ const inputs = Array.isArray(targetInst.input) ? targetInst.input : [];
74
+ const slot = inputs[parseInt(m[1], 10)];
75
+ if (slot && typeof slot.name === "string" && slot.name.trim()) {
76
+ provideToInputName[source] = {
77
+ name: slot.name.trim(),
78
+ isFile: sourceDef.startsWith("provide_file"),
79
+ };
80
+ }
81
+ }
82
+ const args = [];
83
+ for (const [inputName, value] of Object.entries(preset)) {
84
+ if (!inputName || typeof inputName !== "string") continue;
85
+ const mapped = provideToInputName[inputName];
86
+ if (!mapped) continue;
87
+ args.push("--input", `${mapped.name}=${mapped.isFile ? "file:" : ""}${String(value ?? "")}`);
88
+ }
89
+ return args;
90
+ }
91
+
92
+ function hasHigherPriorityDuplicate(workspaceRoot, flow) {
93
+ if ((flow.source || "user") !== "workspace") return false;
94
+ return listFlowsJson(workspaceRoot).some((f) => f.id === flow.id && !f.archived && (f.source || "user") === "user");
95
+ }
96
+
97
+ function getLatestRunUuidForFlow(workspaceRoot, flowId) {
98
+ const runRoot = path.dirname(getRunDir(workspaceRoot, flowId, "00000000000000"));
99
+ if (!fs.existsSync(runRoot)) return null;
100
+ try {
101
+ const dirs = fs.readdirSync(runRoot, { withFileTypes: true })
102
+ .filter((e) => e.isDirectory() && /^\d{14}$/.test(e.name))
103
+ .map((e) => e.name)
104
+ .sort();
105
+ return dirs[dirs.length - 1] || null;
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+
111
+ function isFlowCurrentlyRunning(workspaceRoot, flowId, state) {
112
+ const candidates = [];
113
+ if (state && typeof state.lastRunUuid === "string") candidates.push(state.lastRunUuid);
114
+ const latest = getLatestRunUuidForFlow(workspaceRoot, flowId);
115
+ if (latest) candidates.push(latest);
116
+ for (const uuid of candidates) {
117
+ const runDir = getRunDir(workspaceRoot, flowId, uuid);
118
+ if (isApplyProcessAlive(runDir)) return true;
119
+ }
120
+ return false;
121
+ }
122
+
123
+ function baseState(flow, schedule, previousState) {
124
+ return {
125
+ ...(previousState && typeof previousState === "object" ? previousState : {}),
126
+ flowId: flow.id,
127
+ flowSource: flow.source || "user",
128
+ scheduleIdentity: scheduleIdentity(schedule),
129
+ updatedAt: new Date().toISOString(),
130
+ };
131
+ }
132
+
133
+ function ensureNextRunAt(workspaceRoot, flow, schedule, state) {
134
+ const identity = scheduleIdentity(schedule);
135
+ if (state.scheduleIdentity === identity && state.nextRunAt) return state;
136
+ const nextRunAt = schedule.enabled && schedule.cron ? computeNextRunAtFromSchedule(schedule) : null;
137
+ const next = {
138
+ ...baseState(flow, schedule, state),
139
+ nextRunAt,
140
+ lastError: "",
141
+ };
142
+ writeScheduleState(workspaceRoot, flow.id, flow.source || "user", next);
143
+ return next;
144
+ }
145
+
146
+ function startScheduledRun(workspaceRoot, flow, schedule, state) {
147
+ const flowDir = flow.path || "";
148
+ const agentflowBin = path.join(PACKAGE_ROOT, "bin", "agentflow.mjs");
149
+ const args = [agentflowBin, "apply", flow.id, "--machine-readable", "--workspace-root", path.resolve(workspaceRoot), "--force"];
150
+ args.push(...buildCliInputArgs(flowDir, schedule.preset));
151
+ const child = spawn(process.execPath, args, {
152
+ cwd: path.resolve(workspaceRoot),
153
+ stdio: ["ignore", "pipe", "pipe"],
154
+ env: { ...process.env, FORCE_COLOR: "0" },
155
+ detached: true,
156
+ });
157
+
158
+ const startedAt = new Date().toISOString();
159
+ let lastRunUuid = null;
160
+ let stdoutBuf = "";
161
+ child.stdout.on("data", (chunk) => {
162
+ stdoutBuf += chunk.toString("utf8");
163
+ const lines = stdoutBuf.split("\n");
164
+ stdoutBuf = lines.pop() || "";
165
+ for (const line of lines) {
166
+ if (!line.trim()) continue;
167
+ try {
168
+ const evt = JSON.parse(line);
169
+ if (evt && evt.event === "apply-start" && typeof evt.uuid === "string") {
170
+ lastRunUuid = evt.uuid;
171
+ writeScheduleState(workspaceRoot, flow.id, flow.source || "user", {
172
+ ...baseState(flow, schedule, state),
173
+ nextRunAt: computeNextRunAtFromSchedule(schedule),
174
+ lastTriggeredAt: startedAt,
175
+ lastRunUuid,
176
+ lastPid: child.pid || null,
177
+ lastError: "",
178
+ });
179
+ }
180
+ } catch {
181
+ /* ignore non-json lines */
182
+ }
183
+ }
184
+ });
185
+
186
+ child.stderr.on("data", (chunk) => {
187
+ const text = chunk.toString("utf8").trim();
188
+ if (text) log.debug(`[scheduler] ${flow.id}: ${text.slice(0, 1000)}`);
189
+ });
190
+
191
+ child.on("exit", (code, signal) => {
192
+ const prev = readScheduleState(workspaceRoot, flow.id, flow.source || "user").state || state;
193
+ writeScheduleState(workspaceRoot, flow.id, flow.source || "user", {
194
+ ...baseState(flow, schedule, prev),
195
+ nextRunAt: prev.nextRunAt || computeNextRunAtFromSchedule(schedule),
196
+ lastTriggeredAt: prev.lastTriggeredAt || startedAt,
197
+ lastRunUuid: lastRunUuid || prev.lastRunUuid || null,
198
+ lastExitCode: code,
199
+ lastExitSignal: signal || "",
200
+ lastFinishedAt: new Date().toISOString(),
201
+ lastError: code === 0 ? "" : `scheduled run exited with code ${code}${signal ? ` signal ${signal}` : ""}`,
202
+ });
203
+ });
204
+
205
+ child.unref();
206
+ return child;
207
+ }
208
+
209
+ export function listScheduleStatuses(workspaceRoot) {
210
+ const rows = [];
211
+ for (const flow of listFlowsJson(workspaceRoot)) {
212
+ if (flow.archived || flow.source === "builtin") continue;
213
+ const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flow.source || "user");
214
+ if (!scheduleRes.success) {
215
+ rows.push({ flowId: flow.id, flowSource: flow.source || "user", enabled: false, error: scheduleRes.error });
216
+ continue;
217
+ }
218
+ const schedule = scheduleRes.schedule;
219
+ const stateRes = readScheduleState(workspaceRoot, flow.id, flow.source || "user");
220
+ const state = stateRes.success ? stateRes.state : {};
221
+ rows.push({
222
+ flowId: flow.id,
223
+ flowSource: flow.source || "user",
224
+ enabled: Boolean(schedule.enabled),
225
+ cron: schedule.cron || "",
226
+ timezone: schedule.timezone || "",
227
+ preset: schedule.preset || "",
228
+ nextRunAt: state.nextRunAt || schedule.nextRunAt || null,
229
+ lastTriggeredAt: state.lastTriggeredAt || null,
230
+ lastRunUuid: state.lastRunUuid || null,
231
+ lastError: hasHigherPriorityDuplicate(workspaceRoot, flow)
232
+ ? "workspace flow is shadowed by a user flow with the same id"
233
+ : state.lastError || "",
234
+ running: isFlowCurrentlyRunning(workspaceRoot, flow.id, state),
235
+ });
236
+ }
237
+ rows.sort((a, b) => {
238
+ const ea = a.enabled ? 0 : 1;
239
+ const eb = b.enabled ? 0 : 1;
240
+ return ea - eb || String(a.nextRunAt || "").localeCompare(String(b.nextRunAt || "")) || a.flowId.localeCompare(b.flowId);
241
+ });
242
+ return rows;
243
+ }
244
+
245
+ export async function startScheduler(workspaceRoot, opts = {}) {
246
+ const pollMs = Math.max(1000, Number(opts.pollMs) || DEFAULT_POLL_MS);
247
+ const once = Boolean(opts.once);
248
+ log.info(`AgentFlow scheduler started. workspace=${path.resolve(workspaceRoot)} poll=${pollMs}ms`);
249
+ while (true) {
250
+ const now = Date.now();
251
+ for (const flow of listFlowsJson(workspaceRoot)) {
252
+ if (flow.archived || flow.source === "builtin") continue;
253
+ const flowSource = flow.source || "user";
254
+ const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flowSource);
255
+ if (!scheduleRes.success) {
256
+ log.debug(`[scheduler] ${flow.id}: ${scheduleRes.error}`);
257
+ continue;
258
+ }
259
+ const schedule = scheduleRes.schedule;
260
+ if (!schedule.enabled || !schedule.cron) continue;
261
+ if (hasHigherPriorityDuplicate(workspaceRoot, flow)) {
262
+ const stateRes = readScheduleState(workspaceRoot, flow.id, flowSource);
263
+ writeScheduleState(workspaceRoot, flow.id, flowSource, {
264
+ ...baseState(flow, schedule, stateRes.success ? stateRes.state : {}),
265
+ nextRunAt: null,
266
+ lastError: "workspace flow is shadowed by a user flow with the same id; scheduled run skipped",
267
+ lastErrorAt: new Date().toISOString(),
268
+ });
269
+ continue;
270
+ }
271
+ const stateRes = readScheduleState(workspaceRoot, flow.id, flowSource);
272
+ let state = ensureNextRunAt(workspaceRoot, flow, schedule, stateRes.success ? stateRes.state : {});
273
+ if (!state.nextRunAt || Date.parse(state.nextRunAt) > now) continue;
274
+
275
+ if (isFlowCurrentlyRunning(workspaceRoot, flow.id, state)) {
276
+ const nextRunAt = computeNextRunAtFromSchedule(schedule);
277
+ writeScheduleState(workspaceRoot, flow.id, flowSource, {
278
+ ...baseState(flow, schedule, state),
279
+ nextRunAt,
280
+ lastSkippedAt: new Date().toISOString(),
281
+ lastSkipReason: "running",
282
+ });
283
+ log.info(`[scheduler] skip ${flow.id}: already running; next=${nextRunAt}`);
284
+ continue;
285
+ }
286
+
287
+ try {
288
+ const child = startScheduledRun(workspaceRoot, flow, schedule, state);
289
+ const nextRunAt = computeNextRunAtFromSchedule(schedule);
290
+ writeScheduleState(workspaceRoot, flow.id, flowSource, {
291
+ ...baseState(flow, schedule, state),
292
+ nextRunAt,
293
+ lastTriggeredAt: new Date().toISOString(),
294
+ lastPid: child.pid || null,
295
+ lastError: "",
296
+ });
297
+ log.info(`[scheduler] triggered ${flow.id}; pid=${child.pid || "?"}; next=${nextRunAt}`);
298
+ } catch (e) {
299
+ const nextRunAt = computeNextRunAtFromSchedule(schedule);
300
+ writeScheduleState(workspaceRoot, flow.id, flowSource, {
301
+ ...baseState(flow, schedule, state),
302
+ nextRunAt,
303
+ lastError: e && e.message ? e.message : String(e),
304
+ lastErrorAt: new Date().toISOString(),
305
+ });
306
+ log.info(`[scheduler] failed ${flow.id}: ${e && e.message ? e.message : String(e)}`);
307
+ }
308
+ }
309
+ if (once) return;
310
+ await sleep(pollMs);
311
+ }
312
+ }
@@ -0,0 +1,4 @@
1
+ import { createRequire } from "module";
2
+
3
+ const require = createRequire(import.meta.url);
4
+ export const Table = require("cli-table3");
@@ -0,0 +1,42 @@
1
+ /** 当前时间 hh:MM:ss(24 小时) */
2
+ export function formatTimeHHMMSS() {
3
+ const d = new Date();
4
+ const h = String(d.getHours()).padStart(2, "0");
5
+ const m = String(d.getMinutes()).padStart(2, "0");
6
+ const s = String(d.getSeconds()).padStart(2, "0");
7
+ return `${h}:${m}:${s}`;
8
+ }
9
+
10
+ /** 耗时展示:<1 分钟只展示秒,<1 小时展示分秒,>=1 小时展示时分秒 */
11
+ export function formatDuration(ms) {
12
+ if (ms < 0 || !Number.isFinite(ms)) return "0s";
13
+ const sec = Math.floor(ms / 1000) % 60;
14
+ const min = Math.floor(ms / 60000) % 60;
15
+ const hour = Math.floor(ms / 3600000);
16
+ if (hour > 0) return `${hour}h ${min}m ${sec}s`;
17
+ if (min > 0) return `${min}m ${sec}s`;
18
+ return `${sec}s`;
19
+ }
20
+
21
+ const SAVE_CURSOR = "\x1b[s";
22
+ const RESTORE_CURSOR = "\x1b[u";
23
+
24
+ /** 在终端右下角写入一行文字(需 TTY)。pos 为从右往左的字符数,默认 8(hh:MM:ss) */
25
+ export function writeBottomRight(stream, text, pos = 8) {
26
+ if (!stream.isTTY || stream.columns == null || stream.rows == null) return;
27
+ const cols = stream.columns || 80;
28
+ const rows = stream.rows || 24;
29
+ const col = Math.max(1, cols - pos);
30
+ stream.write(SAVE_CURSOR + `\x1b[${rows};${col}H` + text + RESTORE_CURSOR);
31
+ }
32
+
33
+ /** 对多行文本每行前加前缀后写入 stream */
34
+ export function writeWithPrefix(stream, text, prefix, contentColor = null) {
35
+ if (!text || !prefix) {
36
+ if (text) stream.write(contentColor ? contentColor(text) : text);
37
+ return;
38
+ }
39
+ const lines = text.split("\n");
40
+ const out = lines.map((line) => prefix + (contentColor ? contentColor(line) : line)).join("\n");
41
+ stream.write(out + (text.endsWith("\n") ? "" : "\n"));
42
+ }
@@ -0,0 +1,94 @@
1
+ import chalk from "chalk";
2
+ import { log } from "./log.mjs";
3
+ import { runNodeScript } from "./pipeline-scripts.mjs";
4
+ import { Table } from "./table.mjs";
5
+
6
+ /** 开始时:入口信息(仅流程名称与 uuid) */
7
+ export function printEntryAndFlowFiles(workspaceRoot, flowName, uuid) {
8
+ const entryTable = new Table({
9
+ head: [chalk.cyan("项目"), chalk.cyan("值")],
10
+ colWidths: [18, 24],
11
+ style: { head: [], border: ["grey"] },
12
+ });
13
+ entryTable.push(["流程名称", flowName], ["本次运行 uuid", uuid]);
14
+ log.info("\n" + chalk.bold("入口信息"));
15
+ log.info(entryTable.toString());
16
+ }
17
+
18
+ export function styleStatus(s) {
19
+ if (s === "success") return chalk.green("success");
20
+ if (s === "pending") return chalk.yellow("pending");
21
+ if (s === "running") return chalk.cyan("running");
22
+ if (s === "condition_not_met") return chalk.dim("condition_not_met");
23
+ return chalk.dim(s || "-");
24
+ }
25
+
26
+ /** 仅打印全量节点状态表(应用进入时展示一次用)。输出到 stderr。 */
27
+ export function printNodeStatusTable(instanceStatus, nodes, execIdMap = {}) {
28
+ const idToLabel = new Map();
29
+ const idToType = new Map();
30
+ if (Array.isArray(nodes)) {
31
+ for (const n of nodes) {
32
+ idToLabel.set(n.id, n.label || n.id);
33
+ idToType.set(n.id, n.type || "-");
34
+ }
35
+ }
36
+ const order = Array.isArray(nodes) ? nodes.map((n) => n.id) : Object.keys(instanceStatus || {});
37
+ if (order.length === 0) return;
38
+ const statusTable = new Table({
39
+ head: [chalk.cyan("标签"), chalk.cyan("类型"), chalk.cyan("状态"), chalk.cyan("execId")],
40
+ colWidths: [20, 10, 16, 8],
41
+ style: { head: [], border: ["grey"] },
42
+ });
43
+ for (const id of order) {
44
+ const label = idToLabel.get(id) || id;
45
+ const type = idToType.get(id) || "-";
46
+ const status = (instanceStatus && instanceStatus[id]) || "-";
47
+ const execId = execIdMap[id] != null ? String(execIdMap[id]) : "-";
48
+ statusTable.push([label, type, styleStatus(status), execId]);
49
+ }
50
+ process.stderr.write("\n" + chalk.bold("节点状态") + "\n");
51
+ process.stderr.write(statusTable.toString() + "\n");
52
+ }
53
+
54
+ /**
55
+ * apply 启动时执行 validate-flow(统一校验);errors 则退出,仅 warnings 则提示后继续。
56
+ */
57
+ export function runValidateFlowAndExitIfInvalid(workspaceRoot, flowName, flowDir) {
58
+ const result = runNodeScript(workspaceRoot, "validate-flow.mjs", [workspaceRoot, flowName, flowDir], {
59
+ captureStdout: true,
60
+ });
61
+ const stdout = (result.stdout || "").trim();
62
+ if (!stdout) return;
63
+ let data;
64
+ try {
65
+ data = JSON.parse(stdout);
66
+ } catch {
67
+ return;
68
+ }
69
+ const errors = Array.isArray(data.errors) ? data.errors : [];
70
+ const warnings = Array.isArray(data.warnings) ? data.warnings : [];
71
+
72
+ if (errors.length > 0) {
73
+ process.stderr.write("\n" + chalk.bold.red("流程校验未通过,请修正后再执行 apply:") + "\n\n");
74
+ for (const err of errors) {
75
+ process.stderr.write(" " + chalk.red("• ") + err + "\n");
76
+ }
77
+ if (warnings.length > 0) {
78
+ process.stderr.write("\n" + chalk.yellow("警告:") + "\n");
79
+ for (const w of warnings) {
80
+ process.stderr.write(" " + chalk.yellow("• ") + w + "\n");
81
+ }
82
+ }
83
+ process.stderr.write("\n" + chalk.dim("校验命令: agentflow validate " + flowName) + "\n");
84
+ process.exit(1);
85
+ }
86
+
87
+ if (warnings.length > 0) {
88
+ process.stderr.write("\n" + chalk.yellow("校验警告:") + "\n");
89
+ for (const w of warnings) {
90
+ process.stderr.write(" " + chalk.yellow("• ") + w + "\n");
91
+ }
92
+ process.stderr.write("\n");
93
+ }
94
+ }