@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,216 @@
1
+ /**
2
+ * 包路径与 CLI 常量(供 bin/agentflow 与各 lib 模块使用)。
3
+ */
4
+ import fs from "fs";
5
+ import os from "os";
6
+ import path from "path";
7
+ import { fileURLToPath } from "url";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ /** bin/ 目录(含 pipeline 子目录) */
11
+ export const BIN_DIR = path.resolve(__dirname, "..");
12
+ export const PACKAGE_ROOT = path.resolve(BIN_DIR, "..");
13
+
14
+ /** 节点执行区域分割线(开始/结束标识用) */
15
+ export const NODE_SEP = "════════════════════════════════════════════════════════════════";
16
+
17
+ /** agentflow 包根目录(CLI 所在包的 node_modules 用于解析脚本依赖,如 js-yaml) */
18
+ export const PACKAGE_AGENTS_DIR = path.join(PACKAGE_ROOT, "agents");
19
+ /** 包内 agents 元数据 JSON */
20
+ export const PACKAGE_AGENTS_JSON = path.join(PACKAGE_AGENTS_DIR, "agents.json");
21
+
22
+ /**
23
+ * 用户级 AgentFlow 数据根目录:`AGENTFLOW_HOME` 或 `~/agentflow`。
24
+ * 与项目根 workspaceRoot 分离;run、pipelines、agents 等均落盘于此。
25
+ */
26
+ export function getAgentflowDataRoot() {
27
+ const env = process.env.AGENTFLOW_HOME;
28
+ if (env != null && String(env).trim() !== "") {
29
+ let raw = String(env).trim();
30
+ if (raw === "~") raw = os.homedir();
31
+ else if (raw.startsWith("~/")) raw = path.join(os.homedir(), raw.slice(2));
32
+ return path.resolve(raw);
33
+ }
34
+ return path.join(os.homedir(), "agentflow");
35
+ }
36
+
37
+ /** 项目内 runBuild 根目录:`<workspaceRoot>/.workspace/agentflow/runBuild`(legacy:写入路径已迁至 `<flowDir>/runBuild`,仅用于兼容读取) */
38
+ export function getWorkspaceRunBuildRoot(workspaceRoot) {
39
+ const root =
40
+ workspaceRoot != null && String(workspaceRoot).trim() !== ""
41
+ ? path.resolve(String(workspaceRoot))
42
+ : process.cwd();
43
+ return path.join(root, ".workspace/agentflow/runBuild");
44
+ }
45
+
46
+ /** 旧版用户目录 runBuild 根目录:`~/agentflow/runBuild`(legacy:仅用于历史兼容读取) */
47
+ export function getLegacyUserRunBuildRoot() {
48
+ return path.join(getAgentflowDataRoot(), "runBuild");
49
+ }
50
+
51
+ /**
52
+ * 统一 runtime root:每个 flow 的 pipeline 源、scripts、runBuild 共用一个根目录。
53
+ * - 若 `~/agentflow/pipelines/<name>/flow.yaml` 存在 → user-scope:`~/agentflow/pipelines/<name>`
54
+ * - 若 `<ws>/.workspace/agentflow/pipelines/<name>/flow.yaml` 存在 → workspace-scope:`<ws>/.workspace/agentflow/pipelines/<name>`
55
+ * - archived(`_archived/<name>`)按对应 scope 返回
56
+ * - 其他(builtin 只读 / 不存在)→ 默认 user-scope 路径(首次 run 时自动创建,builtin 源仍从包内读取但 runBuild 落到用户目录)
57
+ */
58
+ export function getFlowRuntimeRoot(workspaceRoot, flowName) {
59
+ const root =
60
+ workspaceRoot != null && String(workspaceRoot).trim() !== ""
61
+ ? path.resolve(String(workspaceRoot))
62
+ : process.cwd();
63
+ const userRoot = getUserPipelinesRoot();
64
+ const userDir = path.join(userRoot, flowName);
65
+ if (fs.existsSync(path.join(userDir, "flow.yaml"))) return userDir;
66
+ const userArchived = path.join(userRoot, ARCHIVED_PIPELINES_DIR_NAME, flowName);
67
+ if (fs.existsSync(path.join(userArchived, "flow.yaml"))) return userArchived;
68
+ const wsDir = path.join(root, PIPELINES_DIR, flowName);
69
+ if (fs.existsSync(path.join(wsDir, "flow.yaml"))) return wsDir;
70
+ const wsArchived = path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowName);
71
+ if (fs.existsSync(path.join(wsArchived, "flow.yaml"))) return wsArchived;
72
+ // builtin / legacy / 尚未落盘 → 默认 user 目录,runBuild 首次写入时创建
73
+ return userDir;
74
+ }
75
+
76
+ /**
77
+ * 单次 run 目录。
78
+ * 新运行走 `<flowRuntimeRoot>/runBuild/<uuid>`;
79
+ * 若该 uuid 在旧位置(legacy workspace/user runBuild 根)已存在,则返回旧位置,保留 resume 兼容。
80
+ */
81
+ export function getRunDir(workspaceRoot, flowName, uuid) {
82
+ const candidates = getRunDirCandidates(workspaceRoot, flowName, uuid);
83
+ for (const c of candidates) {
84
+ if (fs.existsSync(c)) return c;
85
+ }
86
+ return candidates[0];
87
+ }
88
+
89
+ /**
90
+ * 枚举候选 run 目录(新/旧位置)供 UI 读取历史 run。
91
+ * 返回数组按"最优先 → 最兜底"排序:
92
+ * 1. 新 per-flow:<flowRuntimeRoot>/runBuild/<uuid>
93
+ * 2. 旧 workspace:<ws>/.workspace/agentflow/runBuild/<flow>/<uuid>
94
+ * 3. 旧 user:~/agentflow/runBuild/<flow>/<uuid>
95
+ */
96
+ export function getRunDirCandidates(workspaceRoot, flowName, uuid) {
97
+ const candidates = [
98
+ path.join(getFlowRuntimeRoot(workspaceRoot, flowName), "runBuild", uuid),
99
+ path.join(getWorkspaceRunBuildRoot(workspaceRoot), flowName, uuid),
100
+ path.join(getLegacyUserRunBuildRoot(), flowName, uuid),
101
+ ];
102
+ const seen = new Set();
103
+ const out = [];
104
+ for (const c of candidates) {
105
+ const r = path.resolve(c);
106
+ if (seen.has(r)) continue;
107
+ seen.add(r);
108
+ out.push(r);
109
+ }
110
+ return out;
111
+ }
112
+
113
+ export function getUserPipelinesRoot() {
114
+ return path.join(getAgentflowDataRoot(), "pipelines");
115
+ }
116
+
117
+ export function getReferenceRootAbs() {
118
+ return path.join(getAgentflowDataRoot(), "reference");
119
+ }
120
+
121
+ export function getUserAgentsDirAbs() {
122
+ return path.join(getAgentflowDataRoot(), "agents");
123
+ }
124
+
125
+ export function getUserAgentsJsonAbs() {
126
+ return path.join(getAgentflowDataRoot(), "agents.json");
127
+ }
128
+
129
+ export function getModelListsAbs() {
130
+ return path.join(getAgentflowDataRoot(), "model-lists.json");
131
+ }
132
+
133
+ export function getAgentflowUserConfigAbs() {
134
+ return path.join(getAgentflowDataRoot(), "config.json");
135
+ }
136
+
137
+ /** CLI / UI 文案用 */
138
+ export const USER_AGENTFLOW_DIR_LABEL = "~/agentflow";
139
+ export const USER_AGENTFLOW_PIPELINES_LABEL = "~/agentflow/pipelines";
140
+ export const USER_AGENTFLOW_AGENTS_LABEL = "~/agentflow/agents";
141
+
142
+ /** agents.json 中 user 角色 filepath 展示前缀(相对数据根) */
143
+ export const USER_AGENTS_FILEPATH_PREFIX = "agentflow/agents";
144
+
145
+ /** apply/replay 流水线脚本目录(随包发布) */
146
+ export const PIPELINE_SCRIPTS_DIR = path.join(BIN_DIR, "pipeline");
147
+ /** apply -ai 允许调用的单步脚本名(不含 .mjs) */
148
+ export const APPLY_AI_STEPS = [
149
+ "ensure-run-dir",
150
+ "parse-flow",
151
+ "get-ready-nodes",
152
+ "pre-process-node",
153
+ "post-process-node",
154
+ "write-result",
155
+ "run-tool-nodejs",
156
+ "get-env",
157
+ "validate-flow",
158
+ "collect-nodes",
159
+ "gc",
160
+ "extract-thinking",
161
+ ];
162
+ /** 项目内流水线根目录(写入与主读取路径) */
163
+ export const PIPELINES_DIR = ".workspace/agentflow/pipelines";
164
+ /** 用户目录或工作区 pipelines 下存放已归档流水线的子目录名 */
165
+ export const ARCHIVED_PIPELINES_DIR_NAME = "_archived";
166
+ /** 旧版项目内路径;仅用于读取回退 */
167
+ export const LEGACY_PIPELINES_DIR = ".cursor/agentflow/pipelines";
168
+
169
+ /** 项目内 AgentFlow 根目录(相对 workspaceRoot) */
170
+ export const WORKSPACE_AGENTFLOW_ROOT = ".workspace/agentflow";
171
+ /** 项目内自定义节点 .md 目录(主路径;与包内 builtin/nodes 区分) */
172
+ export const PROJECT_NODES_DIR = ".workspace/agentflow/nodes";
173
+ /** 旧版项目内节点目录;仅用于读取回退 */
174
+ export const LEGACY_NODES_DIR = ".cursor/agentflow/nodes";
175
+ /** Web UI 模型映射等项目内配置(主路径) */
176
+ export const MODEL_CONFIG_REL = ".workspace/agentflow/models.json";
177
+ /** 旧版 models.json;仅用于读取回退 */
178
+ export const LEGACY_MODEL_CONFIG_REL = ".cursor/agentflow/models.json";
179
+
180
+ export const RUN_LOG_REL = "logs/log.txt";
181
+
182
+ /** 包内 reference 目录 */
183
+ export const PACKAGE_REFERENCE_DIR = path.join(PACKAGE_ROOT, "reference");
184
+ /** 包内内置节点与流水线 */
185
+ export const PACKAGE_BUILTIN_NODES_DIR = path.join(PACKAGE_ROOT, "builtin", "nodes");
186
+ export const PACKAGE_BUILTIN_PIPELINES_DIR = path.join(PACKAGE_ROOT, "builtin", "pipelines");
187
+
188
+ export const MAX_LOOP_ROUNDS = 10000;
189
+
190
+ /** 去掉 ANSI 转义码,便于解析 Cursor/OpenCode models 输出 */
191
+ export const CURSOR_NON_MODEL_PATTERNS = [
192
+ /^loading\s+models/i,
193
+ /^available\s+models$/i,
194
+ /^tip:\s*use\s+--model/i,
195
+ ];
196
+
197
+ /** 仅 pre+post、不执行任何命令的节点类型 */
198
+ export const LOCAL_ONLY_DEFINITION_IDS = new Set([
199
+ "control_if",
200
+ "control_start",
201
+ "control_end",
202
+ "tool_print",
203
+ "tool_user_check",
204
+ "tool_user_ask",
205
+ "provide_str",
206
+ "provide_file",
207
+ ]);
208
+
209
+ /** 仅 pre+post 且由 CLI 负责写终态的节点 */
210
+ export const LOCAL_ONLY_TERMINAL_SUCCESS_IDS = new Set([
211
+ "control_start",
212
+ "control_end",
213
+ "tool_print",
214
+ "provide_str",
215
+ "provide_file",
216
+ ]);
@@ -0,0 +1,41 @@
1
+ import { spawnSync } from "child_process";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { PIPELINE_SCRIPTS_DIR } from "./paths.mjs";
5
+
6
+ /** 脚本路径:从 agentflow 包内 bin/pipeline 目录加载 */
7
+ export function getScriptPath(_workspaceRoot, name) {
8
+ return path.join(PIPELINE_SCRIPTS_DIR, name);
9
+ }
10
+
11
+ export function runNodeScript(workspaceRoot, scriptName, args, options = {}) {
12
+ const scriptPath = getScriptPath(workspaceRoot, scriptName);
13
+ if (!fs.existsSync(scriptPath)) {
14
+ throw new Error(`Script not found: ${scriptPath}. Reinstall the agentflow package.`);
15
+ }
16
+ const result = spawnSync(process.execPath, [scriptPath, ...args], {
17
+ cwd: workspaceRoot,
18
+ encoding: "utf-8",
19
+ stdio: options.captureStdout ? ["inherit", "pipe", "inherit"] : ["inherit", "inherit", "inherit"],
20
+ ...options,
21
+ });
22
+ return result;
23
+ }
24
+
25
+ export function parseJsonStdout(result) {
26
+ if (result.status !== 0) {
27
+ const err = (result.stdout || "").trim() || result.stderr || "unknown";
28
+ throw new Error(`Script failed: ${err}`);
29
+ }
30
+ const out = (result.stdout || "").trim();
31
+ if (!out) throw new Error("Script produced no stdout");
32
+ try {
33
+ return JSON.parse(out);
34
+ } catch (e) {
35
+ throw new Error(`Invalid JSON from script: ${out.slice(0, 200)}`);
36
+ }
37
+ }
38
+
39
+ export function isValidUuid(value) {
40
+ return typeof value === "string" && /^\d{14}$/.test(value);
41
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * 扫描 runBuild 目录,供 UI 展示「最近运行」。
3
+ */
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import { listAllRunDirs } from "./workspace.mjs";
7
+ import { isApplyProcessAlive } from "./run-apply-active-lock.mjs";
8
+
9
+ /** Web UI 调用 /api/flow/run/stop 时写入,用于与「未跑完但未标记」区分 */
10
+ export const RUN_INTERRUPTED_FILENAME = "run-interrupted.json";
11
+
12
+ /** @param {string} filePath */
13
+ function parseResultStatusFromFile(filePath) {
14
+ try {
15
+ const raw = fs.readFileSync(filePath, "utf-8");
16
+ const m = raw.match(/^\s*status:\s*["']?(\w+)["']?/m);
17
+ return m ? m[1] : null;
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ /** 读取 result.md 里的 finishedAt(ISO 字符串),返回 ms 时间戳或 null */
24
+ function parseResultFinishedAtFromFile(filePath) {
25
+ try {
26
+ const raw = fs.readFileSync(filePath, "utf-8");
27
+ const m = raw.match(/^\s*finishedAt:\s*["']?([^"'\n]+?)["']?\s*$/m);
28
+ if (!m) return null;
29
+ const t = Date.parse(m[1]);
30
+ return Number.isFinite(t) ? t : null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /** 扫 run 目录所有 result.md,返回最大的 finishedAt。回退到 runDir mtime */
37
+ function computeEndedAt(runDir) {
38
+ const interRoot = path.join(runDir, "intermediate");
39
+ let maxAt = 0;
40
+ if (fs.existsSync(interRoot) && fs.statSync(interRoot).isDirectory()) {
41
+ try {
42
+ const dirs = fs.readdirSync(interRoot, { withFileTypes: true }).filter((e) => e.isDirectory());
43
+ for (const d of dirs) {
44
+ const rp = path.join(interRoot, d.name, `${d.name}.result.md`);
45
+ if (!fs.existsSync(rp)) continue;
46
+ const t = parseResultFinishedAtFromFile(rp);
47
+ if (t != null && t > maxAt) maxAt = t;
48
+ }
49
+ } catch {
50
+ /* ignore */
51
+ }
52
+ }
53
+ if (maxAt > 0) return maxAt;
54
+ try {
55
+ return fs.statSync(runDir).mtimeMs;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * 根据 intermediate 下各节点 result 与 control_end 推断单次运行状态。
63
+ * @returns {'success'|'failed'|'running'|'stopped'|'interrupted'|'unknown'}
64
+ */
65
+ function inferRunStatusFromRunDir(runDir) {
66
+ const interruptedPath = path.join(runDir, RUN_INTERRUPTED_FILENAME);
67
+ if (fs.existsSync(interruptedPath)) return "stopped";
68
+
69
+ const interRoot = path.join(runDir, "intermediate");
70
+ if (!fs.existsSync(interRoot) || !fs.statSync(interRoot).isDirectory()) return "unknown";
71
+
72
+ let anyFailed = false;
73
+ let anyResult = false;
74
+ try {
75
+ const dirs = fs.readdirSync(interRoot, { withFileTypes: true }).filter((e) => e.isDirectory());
76
+ for (const d of dirs) {
77
+ const instanceId = d.name;
78
+ const rp = path.join(interRoot, instanceId, `${instanceId}.result.md`);
79
+ if (!fs.existsSync(rp)) continue;
80
+ anyResult = true;
81
+ const st = parseResultStatusFromFile(rp);
82
+ if (st === "failed") anyFailed = true;
83
+ }
84
+ } catch {
85
+ return "unknown";
86
+ }
87
+
88
+ if (anyFailed) return "failed";
89
+
90
+ const flowJsonPath = path.join(interRoot, "flow.json");
91
+ let endId = null;
92
+ if (fs.existsSync(flowJsonPath)) {
93
+ try {
94
+ const flow = JSON.parse(fs.readFileSync(flowJsonPath, "utf-8"));
95
+ const nd = flow.nodeDefinitions || {};
96
+ for (const [iid, def] of Object.entries(nd)) {
97
+ if (def === "control_end") {
98
+ endId = iid;
99
+ break;
100
+ }
101
+ }
102
+ } catch {
103
+ /* ignore */
104
+ }
105
+ }
106
+
107
+ if (endId) {
108
+ const endPath = path.join(interRoot, endId, `${endId}.result.md`);
109
+ const endSt = parseResultStatusFromFile(endPath);
110
+ if (endSt === "success") return "success";
111
+ }
112
+
113
+ if (anyResult || fs.existsSync(flowJsonPath)) {
114
+ if (isApplyProcessAlive(runDir)) return "running";
115
+ return "interrupted";
116
+ }
117
+ return "unknown";
118
+ }
119
+
120
+ function readKeyFromMemory(runDir, key) {
121
+ const memoryPath = path.join(runDir, "memory.md");
122
+ if (!fs.existsSync(memoryPath)) return null;
123
+ try {
124
+ const content = fs.readFileSync(memoryPath, "utf-8");
125
+ for (const line of (content || "").split(/\r?\n/)) {
126
+ const idx = line.indexOf(": ");
127
+ if (idx <= 0) continue;
128
+ const k = line.slice(0, idx).trim();
129
+ if (k !== key) continue;
130
+ const v = line.slice(idx + 2).trim();
131
+ const n = parseInt(String(v), 10);
132
+ return Number.isFinite(n) && n >= 0 ? n : null;
133
+ }
134
+ } catch {
135
+ return null;
136
+ }
137
+ return null;
138
+ }
139
+
140
+ /** listAllRunDirs 的 source 映射到 UI 里的 flowSource 字段 */
141
+ function mapFlowSource(src) {
142
+ if (src === "user") return "user";
143
+ if (src === "workspace") return "workspace";
144
+ // legacy 位置仍归到它物理所在的 scope
145
+ if (src === "legacyWorkspaceRoot") return "workspace";
146
+ if (src === "legacyUserRoot") return "user";
147
+ return "workspace";
148
+ }
149
+
150
+ /**
151
+ * @param {string} workspaceRoot
152
+ * @returns {Array<{ flowId: string, flowSource: 'workspace'|'user', runId: string, at: number, durationMs: number, endedAt: number|null, status: 'success'|'failed'|'running'|'stopped'|'interrupted'|'unknown' }>}
153
+ */
154
+ export function listRecentRunsFromDisk(workspaceRoot) {
155
+ const out = [];
156
+ for (const { flowName, uuid, runDir, source } of listAllRunDirs(workspaceRoot)) {
157
+ let at = readKeyFromMemory(runDir, "runStartTime");
158
+ if (at == null) {
159
+ try {
160
+ at = fs.statSync(runDir).mtimeMs;
161
+ } catch {
162
+ continue;
163
+ }
164
+ }
165
+ const durationMs = readKeyFromMemory(runDir, "totalExecutedMs") ?? 0;
166
+ const status = inferRunStatusFromRunDir(runDir);
167
+ const endedAt = status === "running" ? null : computeEndedAt(runDir);
168
+ out.push({ flowId: flowName, flowSource: mapFlowSource(source), runId: uuid, at, durationMs, endedAt, status });
169
+ }
170
+
171
+ out.sort((a, b) => b.at - a.at);
172
+ return out;
173
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * apply 运行期间在 run 目录写入 PID 锁,供「最近运行」严格区分 running / interrupted。
3
+ */
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import { getRunDir } from "./paths.mjs";
7
+
8
+ export const RUN_APPLY_ACTIVE_FILENAME = "run-apply-active.json";
9
+
10
+ /**
11
+ * @param {string} workspaceRoot
12
+ * @param {string} flowName
13
+ * @param {string} uuid
14
+ */
15
+ export function writeApplyActiveLock(workspaceRoot, flowName, uuid) {
16
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
17
+ fs.mkdirSync(runDir, { recursive: true });
18
+ const payload = {
19
+ pid: process.pid,
20
+ flowName,
21
+ uuid,
22
+ workspaceRoot: path.resolve(String(workspaceRoot)),
23
+ startedAt: Date.now(),
24
+ };
25
+ fs.writeFileSync(path.join(runDir, RUN_APPLY_ACTIVE_FILENAME), JSON.stringify(payload, null, 2), "utf-8");
26
+ }
27
+
28
+ /**
29
+ * @param {string} workspaceRoot
30
+ * @param {string} flowName
31
+ * @param {string} uuid
32
+ */
33
+ export function clearApplyActiveLock(workspaceRoot, flowName, uuid) {
34
+ try {
35
+ const lockPath = path.join(getRunDir(workspaceRoot, flowName, uuid), RUN_APPLY_ACTIVE_FILENAME);
36
+ if (fs.existsSync(lockPath)) fs.unlinkSync(lockPath);
37
+ } catch {
38
+ /* ignore */
39
+ }
40
+ }
41
+
42
+ /**
43
+ * 锁存在且 PID 仍存活 → apply 主进程仍在跑。
44
+ * 锁存在但 PID 已死 → 删除陈旧锁并返回 false。
45
+ * @param {string} runDir
46
+ * @returns {boolean}
47
+ */
48
+ export function isApplyProcessAlive(runDir) {
49
+ const lockPath = path.join(runDir, RUN_APPLY_ACTIVE_FILENAME);
50
+ if (!fs.existsSync(lockPath)) return false;
51
+ let pid;
52
+ try {
53
+ const j = JSON.parse(fs.readFileSync(lockPath, "utf-8"));
54
+ pid = typeof j.pid === "number" ? j.pid : parseInt(String(j.pid), 10);
55
+ } catch {
56
+ try {
57
+ fs.unlinkSync(lockPath);
58
+ } catch {
59
+ /* ignore */
60
+ }
61
+ return false;
62
+ }
63
+ if (!Number.isFinite(pid) || pid <= 0) {
64
+ try {
65
+ fs.unlinkSync(lockPath);
66
+ } catch {
67
+ /* ignore */
68
+ }
69
+ return false;
70
+ }
71
+ try {
72
+ process.kill(pid, 0);
73
+ return true;
74
+ } catch {
75
+ try {
76
+ fs.unlinkSync(lockPath);
77
+ } catch {
78
+ /* ignore */
79
+ }
80
+ return false;
81
+ }
82
+ }
@@ -0,0 +1,85 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { machineReadable } from "./log.mjs";
4
+ import { RUN_LOG_REL } from "./paths.mjs";
5
+ import { runNodeScript } from "./pipeline-scripts.mjs";
6
+ import { getRunDir } from "./workspace.mjs";
7
+
8
+ /**
9
+ * 将 CLI 侧的关键信息也落盘到 run 目录的 logs/log.txt。
10
+ */
11
+ export function appendRunLogLine(workspaceRoot, flowName, uuid, tag, message) {
12
+ if (!workspaceRoot || !flowName || !uuid) return;
13
+ try {
14
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
15
+ const logPath = path.join(runDir, RUN_LOG_REL);
16
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
17
+ const text = typeof message === "string" ? message : JSON.stringify(message);
18
+ const line = `[${new Date().toISOString()}] [${tag}] ${text}\n`;
19
+ fs.appendFileSync(logPath, line, "utf-8");
20
+ } catch (_) {}
21
+ }
22
+
23
+ /** 发送 CLI 事件:写 run log,且 machineReadable 时向 stdout 输出一行 JSON */
24
+ export function emitEvent(workspaceRoot, flowName, uuid, payload) {
25
+ appendRunLogLine(workspaceRoot, flowName, uuid, "cli", payload);
26
+ if (machineReadable && workspaceRoot && flowName && uuid) {
27
+ const line = JSON.stringify({ ...payload, ts: new Date().toISOString() }) + "\n";
28
+ /* 同步写入,减少 pipe 缓冲导致 UI 长时间收不到首行 */
29
+ try {
30
+ fs.writeSync(1, Buffer.from(line, "utf8"));
31
+ } catch {
32
+ process.stdout.write(line);
33
+ }
34
+ }
35
+ }
36
+
37
+ /** 从 run 的 memory.md 读取 runStartTime */
38
+ export function readRunStartTime(workspaceRoot, flowName, uuid) {
39
+ const memoryPath = path.join(getRunDir(workspaceRoot, flowName, uuid), "memory.md");
40
+ if (!fs.existsSync(memoryPath)) return null;
41
+ const content = fs.readFileSync(memoryPath, "utf-8");
42
+ for (const line of (content || "").split(/\r?\n/)) {
43
+ const idx = line.indexOf(": ");
44
+ if (idx <= 0) continue;
45
+ const k = line.slice(0, idx).trim();
46
+ if (k !== "runStartTime") continue;
47
+ const v = line.slice(idx + 2).trim();
48
+ const n = parseInt(String(v), 10);
49
+ return Number.isFinite(n) && n > 0 ? n : null;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ /** 从 run 的 memory.md 读取 totalExecutedMs */
55
+ export function readTotalExecutedMs(workspaceRoot, flowName, uuid) {
56
+ const memoryPath = path.join(getRunDir(workspaceRoot, flowName, uuid), "memory.md");
57
+ if (!fs.existsSync(memoryPath)) return 0;
58
+ const content = fs.readFileSync(memoryPath, "utf-8");
59
+ for (const line of (content || "").split(/\r?\n/)) {
60
+ const idx = line.indexOf(": ");
61
+ if (idx <= 0) continue;
62
+ const k = line.slice(0, idx).trim();
63
+ if (k !== "totalExecutedMs") continue;
64
+ const v = line.slice(idx + 2).trim();
65
+ const n = parseInt(String(v), 10);
66
+ return Number.isFinite(n) && n >= 0 ? n : 0;
67
+ }
68
+ return 0;
69
+ }
70
+
71
+ export function saveTotalExecutedMs(workspaceRoot, flowName, uuid, totalExecutedMs) {
72
+ runNodeScript(workspaceRoot, "save-key.mjs", [workspaceRoot, flowName, uuid, "totalExecutedMs", String(totalExecutedMs)], {
73
+ captureStdout: true,
74
+ });
75
+ }
76
+
77
+ export function ensureRunStartTime(workspaceRoot, flowName, uuid) {
78
+ const existing = readRunStartTime(workspaceRoot, flowName, uuid);
79
+ if (existing != null) return existing;
80
+ const runStartTime = Date.now();
81
+ runNodeScript(workspaceRoot, "save-key.mjs", [workspaceRoot, flowName, uuid, "runStartTime", String(runStartTime)], {
82
+ captureStdout: true,
83
+ });
84
+ return runStartTime;
85
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * 从某次 run 的 intermediate 读取各节点最新 result 状态,供 UI 从历史进入时还原画布角标。
3
+ */
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import { getRunDir } from "./paths.mjs";
7
+ import { formatDuration } from "./terminal.mjs";
8
+ import { loadAllExecIds, latestResultExecId, intermediateResultBasename } from "../pipeline/get-exec-id.mjs";
9
+
10
+ function parseResultStatus(filePath) {
11
+ try {
12
+ const raw = fs.readFileSync(filePath, "utf-8");
13
+ const m = raw.match(/^\s*status:\s*["']?(\w+)["']?/m);
14
+ return m ? m[1] : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ /** @param {string} filePath @returns {number | null} */
21
+ function parseElapsedMsLine(filePath) {
22
+ try {
23
+ const raw = fs.readFileSync(filePath, "utf-8");
24
+ const m = raw.match(/^\s*elapsedMs:\s*(\d+)/m);
25
+ if (!m) return null;
26
+ const n = parseInt(m[1], 10);
27
+ return Number.isFinite(n) && n >= 0 ? n : null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * @param {string} workspaceRoot
35
+ * @param {string} flowName
36
+ * @param {string} uuid
37
+ * @returns {Record<string, { status: string, elapsed?: string }>}
38
+ */
39
+ export function getRunNodeStatusesFromDisk(workspaceRoot, flowName, uuid) {
40
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
41
+ const flowJsonPath = path.join(runDir, "intermediate", "flow.json");
42
+ if (!fs.existsSync(flowJsonPath)) return {};
43
+
44
+ let flow;
45
+ try {
46
+ flow = JSON.parse(fs.readFileSync(flowJsonPath, "utf-8"));
47
+ } catch {
48
+ return {};
49
+ }
50
+ if (!flow || !flow.ok) return {};
51
+
52
+ const order = Array.isArray(flow.order) ? flow.order : [];
53
+ const nodeDefinitions = flow.nodeDefinitions && typeof flow.nodeDefinitions === "object" ? flow.nodeDefinitions : {};
54
+ const execIdMap = loadAllExecIds(workspaceRoot, flowName, uuid, order);
55
+ const intermediateDir = path.join(runDir, "intermediate");
56
+ /** @type {Record<string, { status: string, elapsed?: string }>} */
57
+ const out = {};
58
+
59
+ for (const instanceId of order) {
60
+ const defId = nodeDefinitions[instanceId] || "";
61
+ const execId = execIdMap[instanceId] ?? 1;
62
+ const latestE = latestResultExecId(execId);
63
+ const resultPath = path.join(intermediateDir, instanceId, intermediateResultBasename(instanceId, latestE));
64
+ let status = fs.existsSync(resultPath) ? parseResultStatus(resultPath) : null;
65
+ if (!status && defId.startsWith("provide_")) status = "success";
66
+ if (!status && defId === "control_start") status = "success";
67
+ if (!status) continue;
68
+
69
+ let uiStatus = status;
70
+ const low = String(status).toLowerCase();
71
+ if (low === "completed" || low === "done") uiStatus = "success";
72
+
73
+ /** @type {{ status: string, elapsed?: string }} */
74
+ const row = { status: uiStatus };
75
+ if (uiStatus === "success" && fs.existsSync(resultPath)) {
76
+ const ms = parseElapsedMsLine(resultPath);
77
+ if (ms != null && ms > 0) {
78
+ row.elapsed = formatDuration(ms);
79
+ }
80
+ }
81
+ out[instanceId] = row;
82
+ }
83
+
84
+ return out;
85
+ }