@fieldwangai/agentflow 0.1.153 → 0.1.154

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 (192) hide show
  1. package/README.md +16 -34
  2. package/README.zh-CN.md +14 -34
  3. package/agents/agentflow-node-executor-code.md +13 -3
  4. package/agents/agentflow-node-executor-planning.md +13 -3
  5. package/agents/agentflow-node-executor-requirement.md +13 -3
  6. package/agents/agentflow-node-executor-test.md +13 -3
  7. package/agents/agentflow-node-executor-ui.md +13 -3
  8. package/agents/agentflow-node-executor.md +13 -3
  9. package/agents/en/agentflow-node-executor.md +13 -3
  10. package/agents/zh/agentflow-node-executor.md +13 -3
  11. package/bin/agentflow.mjs +3 -3
  12. package/bin/lib/admin-builtin-pipelines.mjs +3 -2
  13. package/bin/lib/agent-runners.mjs +0 -929
  14. package/bin/lib/auth.mjs +2 -1
  15. package/bin/lib/catalog-agents.mjs +2 -1
  16. package/bin/lib/catalog-flows.mjs +165 -90
  17. package/bin/lib/composer-agent.mjs +3 -793
  18. package/bin/lib/composer-skill-router.mjs +3 -323
  19. package/bin/lib/exec-buffered.mjs +32 -0
  20. package/bin/lib/flow-dsl/cli.mjs +276 -0
  21. package/bin/lib/flow-dsl/codegen.mjs +358 -0
  22. package/bin/lib/flow-dsl/defs.mjs +103 -0
  23. package/bin/lib/flow-dsl/index.mjs +77 -0
  24. package/bin/lib/flow-dsl/ir.mjs +305 -0
  25. package/bin/lib/flow-dsl/layout.mjs +115 -0
  26. package/bin/lib/flow-dsl/legacy-yaml.mjs +268 -0
  27. package/bin/lib/flow-dsl/lint.mjs +241 -0
  28. package/bin/lib/flow-dsl/packages.mjs +251 -0
  29. package/bin/lib/flow-dsl/parser.mjs +444 -0
  30. package/bin/lib/flow-import.mjs +96 -26
  31. package/bin/lib/flow-write.mjs +48 -12
  32. package/bin/lib/help.mjs +8 -28
  33. package/bin/lib/html-escape.mjs +19 -0
  34. package/bin/lib/http-util.mjs +49 -0
  35. package/bin/lib/legacy-flow-execution.mjs +37 -0
  36. package/bin/lib/locales/en.json +1 -1
  37. package/bin/lib/locales/zh.json +1 -1
  38. package/bin/lib/main.mjs +152 -286
  39. package/bin/lib/marketplace.mjs +206 -61
  40. package/bin/lib/model-config.mjs +10 -0
  41. package/bin/lib/node-package-archive.mjs +240 -0
  42. package/bin/lib/node-package-bootstrap.mjs +72 -0
  43. package/bin/lib/node-package-manifest.mjs +174 -0
  44. package/bin/lib/paths.mjs +42 -19
  45. package/bin/lib/prd-workflow-routes.mjs +2949 -0
  46. package/bin/lib/prd-workflow-server.mjs +4884 -0
  47. package/bin/lib/run-events.mjs +0 -66
  48. package/bin/lib/schedule-config.mjs +4 -4
  49. package/bin/lib/skill-runtime.mjs +35 -0
  50. package/bin/lib/startup-storage-migrations.mjs +274 -0
  51. package/bin/lib/ui-server.mjs +1087 -18122
  52. package/bin/lib/user-env.mjs +16 -0
  53. package/bin/lib/workspace-auto-layout.mjs +234 -0
  54. package/bin/lib/workspace-flow-store.mjs +478 -0
  55. package/bin/lib/workspace-graph-merge.mjs +41 -34
  56. package/bin/lib/workspace-routes.mjs +3088 -0
  57. package/bin/lib/workspace-server.mjs +6199 -0
  58. package/bin/lib/workspace-state.mjs +331 -0
  59. package/bin/lib/workspace.mjs +2 -1
  60. package/builtin/nodes/agent_subAgent.md +1 -0
  61. package/builtin/nodes/control_agent_toBool.md +2 -0
  62. package/builtin/nodes/control_anyOne.md +2 -0
  63. package/builtin/nodes/control_cancelled.md +2 -0
  64. package/builtin/nodes/control_cd_workspace.md +5 -11
  65. package/builtin/nodes/control_delay.md +2 -0
  66. package/builtin/nodes/control_end.md +2 -0
  67. package/builtin/nodes/control_if.md +1 -0
  68. package/builtin/nodes/control_interval_loop.md +2 -0
  69. package/builtin/nodes/control_load_mcp.md +24 -0
  70. package/builtin/nodes/control_load_skills.md +8 -23
  71. package/builtin/nodes/control_start.md +2 -0
  72. package/builtin/nodes/control_toBool.md +2 -0
  73. package/builtin/nodes/control_user_workspace.md +2 -0
  74. package/builtin/nodes/control_wait_until.md +2 -0
  75. package/builtin/nodes/display_ascii.md +1 -0
  76. package/builtin/nodes/display_chart.md +1 -0
  77. package/builtin/nodes/display_html.md +1 -0
  78. package/builtin/nodes/display_image.md +1 -0
  79. package/builtin/nodes/display_markdown.md +1 -0
  80. package/builtin/nodes/display_mermaid.md +1 -0
  81. package/builtin/nodes/display_react_app.md +1 -0
  82. package/builtin/nodes/display_table.md +1 -0
  83. package/builtin/nodes/provide_bool.md +1 -0
  84. package/builtin/nodes/provide_file.md +1 -0
  85. package/builtin/nodes/provide_password.md +1 -0
  86. package/builtin/nodes/provide_str.md +1 -0
  87. package/builtin/nodes/tool_display_share_link.md +1 -0
  88. package/builtin/nodes/tool_get_env.md +2 -0
  89. package/builtin/nodes/tool_git_checkout.md +1 -0
  90. package/builtin/nodes/tool_git_worktree_load.md +1 -0
  91. package/builtin/nodes/tool_git_worktree_unload.md +1 -0
  92. package/builtin/nodes/tool_gitlab_create_mr.md +1 -0
  93. package/builtin/nodes/tool_jenkins_build.md +2 -0
  94. package/builtin/nodes/tool_load_key.md +2 -0
  95. package/builtin/nodes/tool_nodejs.md +17 -19
  96. package/builtin/nodes/tool_print.md +2 -0
  97. package/builtin/nodes/tool_save_key.md +2 -0
  98. package/builtin/nodes/tool_set_run_env.md +1 -0
  99. package/builtin/nodes/tool_user_ask.md +2 -0
  100. package/builtin/nodes/tool_user_check.md +2 -0
  101. package/builtin/nodes/tool_wecom_send_app_markdown.md +1 -0
  102. package/builtin/nodes/tool_wecom_send_group_markdown.md +1 -0
  103. package/builtin/nodes/workspace_one_click_task.md +44 -0
  104. package/builtin/nodes/workspace_run.md +16 -0
  105. package/builtin/nodes/workspace_scheduled_run.md +16 -0
  106. package/builtin/pipelines/module-migrate/scripts/gate.mjs +37 -0
  107. package/builtin/pipelines/module-migrate/scripts/static-check.mjs +82 -0
  108. package/builtin/pipelines/module-migrate/workspace.flow.js +172 -0
  109. package/builtin/pipelines/module-migrate/workspace.layout.json +134 -0
  110. package/builtin/pipelines/new/scripts/lint-flow.mjs +38 -0
  111. package/builtin/pipelines/new/workspace.flow.js +91 -0
  112. package/builtin/pipelines/new/workspace.layout.json +70 -0
  113. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-B9xslwI0.js → WorkflowAssistantThread-CKClwj96.js} +1 -1
  114. package/builtin/web-ui/dist/assets/index-BZ5KqLur.js +870 -0
  115. package/builtin/web-ui/dist/assets/index-CEXmmwM2.css +1 -0
  116. package/builtin/web-ui/dist/index.html +2 -2
  117. package/package.json +2 -1
  118. package/reference/flow-control-capabilities.md +77 -158
  119. package/reference/flow-layout.md +1 -1
  120. package/reference/flow-prompt-handler-check.md +2 -2
  121. package/skills/agentflow-author-flow/SKILL.md +1 -1
  122. package/skills/agentflow-cli/SKILL.md +139 -24
  123. package/skills/agentflow-cli/agents/openai.yaml +2 -2
  124. package/skills/agentflow-cli/scripts/agentflow-cli.mjs +654 -20
  125. package/skills/agentflow-cli/scripts/agentflow-runtime.mjs +97 -0
  126. package/skills/agentflow-flow-add-instances/SKILL.md +1 -1
  127. package/skills/agentflow-flow-dsl/SKILL.md +206 -0
  128. package/skills/agentflow-flow-dsl/agents/openai.yaml +4 -0
  129. package/skills/agentflow-flow-dsl/references/node-calls.md +39 -0
  130. package/skills/agentflow-flow-edit-node-fields/SKILL.md +1 -1
  131. package/skills/agentflow-flow-recipes/SKILL.md +7 -4
  132. package/skills/agentflow-flow-recipes/references/recipes.md +97 -43
  133. package/skills/agentflow-flow-sync-ui/SKILL.md +1 -1
  134. package/skills/agentflow-node-dsl/SKILL.md +212 -0
  135. package/skills/agentflow-node-dsl/agents/openai.yaml +4 -0
  136. package/skills/agentflow-node-reference/SKILL.md +2 -2
  137. package/skills/agentflow-node-reference/references/builtin-nodes.md +166 -115
  138. package/skills/agentflow-runtime-reference/references/runtime.md +1 -1
  139. package/skills/agentflow-workspace-ascii/SKILL.md +9 -16
  140. package/skills/agentflow-workspace-graph/SKILL.md +62 -48
  141. package/skills/agentflow-workspace-html/SKILL.md +7 -2
  142. package/skills/agentflow-workspace-image/SKILL.md +6 -2
  143. package/skills/agentflow-workspace-markdown/SKILL.md +14 -21
  144. package/skills/agentflow-workspace-mermaid/SKILL.md +8 -16
  145. package/bin/lib/api-runner.mjs +0 -387
  146. package/bin/lib/apply.mjs +0 -903
  147. package/bin/lib/composer-flow-instances.mjs +0 -68
  148. package/bin/lib/composer-flow-skeleton.mjs +0 -334
  149. package/bin/lib/composer-flow-validate.mjs +0 -47
  150. package/bin/lib/composer-model-router.mjs +0 -185
  151. package/bin/lib/composer-node-schema.mjs +0 -303
  152. package/bin/lib/composer-planner.mjs +0 -751
  153. package/bin/lib/composer-script-ops.mjs +0 -233
  154. package/bin/lib/flow-static-preview.mjs +0 -104
  155. package/bin/lib/hub-login.mjs +0 -54
  156. package/bin/lib/hub-publish.mjs +0 -159
  157. package/bin/lib/hub-remote.mjs +0 -189
  158. package/bin/lib/hub.mjs +0 -299
  159. package/bin/lib/jenkins.mjs +0 -380
  160. package/bin/lib/node-execute.mjs +0 -539
  161. package/bin/lib/normalize-node-tool-command.mjs +0 -97
  162. package/bin/lib/runtime-context.mjs +0 -243
  163. package/bin/lib/scheduler.mjs +0 -601
  164. package/bin/lib/ui-print.mjs +0 -94
  165. package/bin/pipeline/build-node-prompt.mjs +0 -271
  166. package/bin/pipeline/check-cache.mjs +0 -191
  167. package/bin/pipeline/check-flow.mjs +0 -543
  168. package/bin/pipeline/collect-nodes.mjs +0 -212
  169. package/bin/pipeline/compute-cache-md5.mjs +0 -177
  170. package/bin/pipeline/ensure-run-dir.mjs +0 -71
  171. package/bin/pipeline/gc.mjs +0 -129
  172. package/bin/pipeline/get-env.mjs +0 -59
  173. package/bin/pipeline/get-resolved-values.mjs +0 -344
  174. package/bin/pipeline/load-key.mjs +0 -62
  175. package/bin/pipeline/parse-flow.mjs +0 -708
  176. package/bin/pipeline/post-process-control-if.mjs +0 -23
  177. package/bin/pipeline/post-process-node.mjs +0 -490
  178. package/bin/pipeline/pre-process-node.mjs +0 -1430
  179. package/bin/pipeline/resolve-inputs.mjs +0 -201
  180. package/bin/pipeline/run-tool-nodejs.mjs +0 -167
  181. package/bin/pipeline/save-key.mjs +0 -93
  182. package/bin/pipeline/snapshot-prior-round.mjs +0 -70
  183. package/bin/pipeline/validate-for-ui.mjs +0 -234
  184. package/bin/pipeline/validate-script-output.mjs +0 -130
  185. package/bin/pipeline/write-result.mjs +0 -182
  186. package/builtin/pipelines/module-migrate/flow.yaml +0 -819
  187. package/builtin/pipelines/new/flow.yaml +0 -545
  188. package/builtin/pipelines/new/scripts/check-flow.mjs +0 -9
  189. package/builtin/pipelines/new/scripts/collect-nodes.mjs +0 -211
  190. package/builtin/web-ui/dist/assets/index-DZ328oSo.css +0 -1
  191. package/builtin/web-ui/dist/assets/index-DmUV7ZCL.js +0 -888
  192. package/skills/agentflow-node-authoring/SKILL.md +0 -57
@@ -2,6 +2,11 @@
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import {
6
+ configureAgentFlowRuntime,
7
+ loadAgentFlowRuntime,
8
+ resolveAgentFlowPackageRoot,
9
+ } from "./agentflow-runtime.mjs";
5
10
  import { createWorkflowReportClient } from "./workflow-report-client.mjs";
6
11
 
7
12
  const DEFAULT_BASE_URL = "http://ai.mengma.bigo.inner/";
@@ -16,6 +21,24 @@ const DISPLAY_DEFINITION_KINDS = new Map([
16
21
  ["display_table", "table"],
17
22
  ]);
18
23
 
24
+ /**
25
+ * `--file` 可以是流程目录、workspace.flow.js,或历史的 workspace.graph.json。
26
+ * 前两种要静态解析代码才能拿到图,交给 agentflow 自己的存储层——不在这里重实现一遍。
27
+ */
28
+ async function readWorkspaceGraphArg(target, marketplaceRoot = process.cwd()) {
29
+ const file = String(target || "");
30
+ if (!file) throw new Error("--file is required");
31
+ const stat = fs.statSync(file);
32
+ const dir = stat.isDirectory() ? file : path.dirname(file);
33
+ if (!stat.isDirectory() && file.endsWith(".json")) return readJsonFile(file);
34
+ const runtime = await loadAgentFlowRuntime();
35
+ const design = runtime.readWorkspaceDesign(dir, { marketplaceRoot });
36
+ if (design.format === "empty") throw new Error(`No workspace graph in ${dir}`);
37
+ const statePath = path.join(dir, runtime.WORKSPACE_STATE_FILENAME);
38
+ const localState = fs.existsSync(statePath) ? readJsonFile(statePath) : null;
39
+ return runtime.mergeWorkspaceState(design.graph, localState);
40
+ }
41
+
19
42
  function usage() {
20
43
  return `AgentFlow direct API CLI
21
44
 
@@ -25,6 +48,7 @@ Usage:
25
48
  Config:
26
49
  --base-url <url> Override AGENTFLOW_BASE_URL
27
50
  --token <token> Override AGENTFLOW_TOKEN
51
+ --agentflow-package-root <dir> Override the local @fieldwangai/agentflow runtime
28
52
  AGENTFLOW_BASE_URL Defaults to ${DEFAULT_BASE_URL}
29
53
  AGENTFLOW_TOKEN Required unless AGENTFLOW_SESSION_TOKEN is set
30
54
  AGENTFLOW_ENV_FILE Optional dotenv file path
@@ -33,9 +57,17 @@ Commands:
33
57
  config
34
58
  list-workspace | list-workspaces
35
59
  list-flows
36
- publish-flow --flow-id <id> --file <flow.yaml> [--target-space personal|workspace|team] [--replace]
60
+ node-package-list
61
+ node-package-search --query <text> [--limit <n>]
62
+ node-package-publish --file <nodePackageDir>
63
+ node-package-install --node <id>@<version> [--workspace-root <dir>]
64
+ node-package-sync --flow <flowDir|workspace.flow.js> [--workspace-root <dir>]
65
+ pull-flow --flow-id <id> [--flow-source user] [--output <flowDir>] [--workspace-root <dir>] [--replace]
66
+ publish-flow --flow-id <id> --file <flowDir|workspace.flow.js|flow.yaml> [--target-space personal|workspace|team] [--with-dependencies] [--replace]
37
67
  get-graph --flow-id <id> [--flow-source user]
38
- workspace-preview --file <workspace.graph.json> [--preview-id <id>] [--ttl-seconds <n>]
68
+ migrate-flow --flow-id <id> [--flow-source user] [--archived] [--allow-loss]
69
+ migrate-all [--include-archived] [--allow-loss] [--dry-run]
70
+ workspace-preview --file <flowDir|workspace.flow.js|workspace.graph.json> [--preview-id <id>] [--ttl-seconds <n>]
39
71
  run --flow-id <id> [--flow-source user] [--run-node-id <id>] [--input k=v]
40
72
  status --flow-id <id> [--flow-source user]
41
73
  list-run-by-workspace | list-runs-by-workspace --workspace <flowId> [--limit 20]
@@ -202,6 +234,283 @@ async function httpMultipart(args, pathname, form) {
202
234
  return data;
203
235
  }
204
236
 
237
+ async function httpBuffer(args, pathname) {
238
+ const token = authToken(args);
239
+ const url = new URL(pathname, normalizedBaseUrl(args));
240
+ const headers = { Accept: "application/zip" };
241
+ if (token) {
242
+ headers.Authorization = `Bearer ${token}`;
243
+ headers.Cookie = `af_session=${encodeURIComponent(token)}`;
244
+ }
245
+ const response = await fetch(url, { headers });
246
+ if (!response.ok) {
247
+ const text = await response.text();
248
+ let message = text;
249
+ try { message = JSON.parse(text)?.error || text; } catch { /* keep text */ }
250
+ const error = new Error(`GET ${url.pathname} failed: ${message || `HTTP ${response.status}`}`);
251
+ error.status = response.status;
252
+ throw error;
253
+ }
254
+ return {
255
+ buffer: Buffer.from(await response.arrayBuffer()),
256
+ contentSha256: String(response.headers.get("x-agentflow-content-sha256") || "").trim(),
257
+ archiveSha256: String(response.headers.get("x-agentflow-archive-sha256") || "").trim(),
258
+ };
259
+ }
260
+
261
+ function parseNodePackageSpec(raw) {
262
+ const text = String(raw || "").replace(/^marketplace:/, "").trim();
263
+ const at = text.lastIndexOf("@");
264
+ if (at <= 0 || at === text.length - 1) throw new Error("Invalid --node. Expected <id>@<version>.");
265
+ return { id: text.slice(0, at), version: text.slice(at + 1) };
266
+ }
267
+
268
+ function readFlowDependencySource(target) {
269
+ const requested = String(target || "").trim();
270
+ if (!requested) throw new Error("Missing --flow <flowDir|workspace.flow.js>.");
271
+ let resolved = path.resolve(requested);
272
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
273
+ resolved = path.join(resolved, "workspace.flow.js");
274
+ }
275
+ if (!/\.m?js$/i.test(resolved)) {
276
+ throw new Error(`Node package sync only supports workspace.flow.js: ${resolved}`);
277
+ }
278
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
279
+ throw new Error(`Cannot read workspace.flow.js: ${resolved}`);
280
+ }
281
+ return { resolved, source: fs.readFileSync(resolved, "utf-8") };
282
+ }
283
+
284
+ async function flowNodePackageDependencies(source) {
285
+ const runtime = await loadAgentFlowRuntime();
286
+ const result = runtime.marketplaceDependenciesFromSource(source);
287
+ if (result.errors.length) throw new Error(result.errors.join("\n"));
288
+ return result.dependencies;
289
+ }
290
+
291
+ async function remoteNodePackageCatalog(args) {
292
+ const result = await httpJson(args, "/api/node-packages");
293
+ const nodes = Array.isArray(result?.nodes) ? result.nodes : [];
294
+ return new Map(nodes.map((node) => [`${node.id}@${node.version}`, node]));
295
+ }
296
+
297
+ function searchNodePackageCatalog(nodes, queryText, limitValue) {
298
+ const needle = String(queryText || "").trim().toLowerCase();
299
+ const requestedLimit = Number.parseInt(String(limitValue || "20"), 10);
300
+ const limit = Number.isFinite(requestedLimit) ? Math.min(100, Math.max(1, requestedLimit)) : 20;
301
+ const score = (node) => {
302
+ if (!needle) return 1;
303
+ const id = String(node?.id || "").toLowerCase();
304
+ const name = String(node?.displayName || node?.name || "").toLowerCase();
305
+ const description = String(node?.description || "").toLowerCase();
306
+ const slots = JSON.stringify({
307
+ inputs: node?.inputs || node?.input || [],
308
+ outputs: node?.outputs || node?.output || [],
309
+ }).toLowerCase();
310
+ if (id === needle) return 100;
311
+ if (id.startsWith(needle)) return 80;
312
+ if (name === needle) return 70;
313
+ if (name.includes(needle)) return 50;
314
+ if (id.includes(needle)) return 40;
315
+ if (description.includes(needle)) return 20;
316
+ if (slots.includes(needle)) return 10;
317
+ return 0;
318
+ };
319
+ return nodes
320
+ .map((node) => ({ node, score: score(node) }))
321
+ .filter((item) => item.score > 0)
322
+ .sort((a, b) => b.score - a.score
323
+ || String(a.node.id).localeCompare(String(b.node.id))
324
+ || String(b.node.version).localeCompare(String(a.node.version)))
325
+ .slice(0, limit)
326
+ .map(({ node }) => ({
327
+ id: node.id,
328
+ version: node.version,
329
+ specifier: `marketplace:${node.id}@${node.version}`,
330
+ displayName: node.displayName || node.name || node.id,
331
+ description: node.description || "",
332
+ baseDefinitionId: node.baseDefinitionId || node.runtime?.type || "tool_nodejs",
333
+ inputs: node.inputs || node.input || [],
334
+ outputs: node.outputs || node.output || [],
335
+ contentSha256: node.contentSha256 || "",
336
+ installCommand: `node-package-install --node ${node.id}@${node.version}`,
337
+ }));
338
+ }
339
+
340
+ async function inspectDownloadedNodePackage(args, dependency, remote) {
341
+ const downloaded = await httpBuffer(
342
+ args,
343
+ `/api/node-packages/${encodeURIComponent(dependency.id)}/${encodeURIComponent(dependency.version)}/archive`,
344
+ );
345
+ const runtime = await loadAgentFlowRuntime();
346
+ const inspected = runtime.inspectNodePackageArchive(downloaded.buffer);
347
+ if (!inspected.ok) throw new Error(`${dependency.specifier}: ${inspected.error || "invalid node package archive"}`);
348
+ if (inspected.manifest.id !== dependency.id || inspected.manifest.version !== dependency.version) {
349
+ throw new Error(
350
+ `${dependency.specifier}: 下载内容声明为 ${inspected.manifest.id}@${inspected.manifest.version},与请求不一致`,
351
+ );
352
+ }
353
+ for (const [label, expected, actual] of [
354
+ ["目录内容", remote?.contentSha256, inspected.contentSha256],
355
+ ["响应内容", downloaded.contentSha256, inspected.contentSha256],
356
+ ["ZIP", downloaded.archiveSha256, inspected.archiveSha256],
357
+ ]) {
358
+ if (expected && expected !== actual) {
359
+ throw new Error(`${dependency.specifier}: ${label} SHA256 校验失败(expected ${expected}, got ${actual})`);
360
+ }
361
+ }
362
+ return { dependency, remote, downloaded, inspected };
363
+ }
364
+
365
+ async function syncNodePackageDependencies(args, dependencies, workspaceRoot, context = {}) {
366
+ const remoteByKey = await remoteNodePackageCatalog(args);
367
+ const runtime = await loadAgentFlowRuntime();
368
+ const localByKey = new Map(
369
+ runtime.listMarketplacePackages(workspaceRoot).nodes.map((node) => [`${node.id}@${node.version}`, node]),
370
+ );
371
+ const result = {
372
+ ok: true,
373
+ ...context,
374
+ workspaceRoot,
375
+ installed: [],
376
+ unchanged: [],
377
+ missing: [],
378
+ conflicts: [],
379
+ };
380
+ const pending = [];
381
+ for (const dependency of dependencies) {
382
+ const key = `${dependency.id}@${dependency.version}`;
383
+ const remote = remoteByKey.get(key);
384
+ if (!remote) {
385
+ result.missing.push({ ...dependency, error: "远端不存在该精确版本" });
386
+ continue;
387
+ }
388
+ const local = localByKey.get(key);
389
+ if (local) {
390
+ const localInspection = runtime.inspectNodePackageDirectory(local.packageDir, { allowLegacyManifest: true });
391
+ const localSha = localInspection.ok ? localInspection.contentSha256 : String(local.contentSha256 || "");
392
+ if (remote.contentSha256 && localSha && remote.contentSha256 !== localSha) {
393
+ result.conflicts.push({
394
+ ...dependency,
395
+ localContentSha256: localSha,
396
+ remoteContentSha256: remote.contentSha256,
397
+ error: "本地同版本内容与远端不同",
398
+ });
399
+ } else {
400
+ result.unchanged.push({ ...dependency, contentSha256: localSha || remote.contentSha256 || "" });
401
+ }
402
+ continue;
403
+ }
404
+ pending.push({ dependency, remote });
405
+ }
406
+ if (result.missing.length || result.conflicts.length) {
407
+ result.ok = false;
408
+ return result;
409
+ }
410
+
411
+ // 所有 ZIP 先下载并校验,再开始落盘;远端有一个坏包时,本地不会只装上一半。
412
+ const prepared = [];
413
+ for (const item of pending) {
414
+ prepared.push(await inspectDownloadedNodePackage(args, item.dependency, item.remote));
415
+ }
416
+ const installedAt = new Date().toISOString();
417
+ for (const item of prepared) {
418
+ const installed = runtime.publishNodePackageArchive(workspaceRoot, item.downloaded.buffer, {
419
+ installedFrom: normalizedBaseUrl(args),
420
+ installedAt,
421
+ });
422
+ if (!installed.ok) throw new Error(`${item.dependency.specifier}: ${installed.error || "install failed"}`);
423
+ result.installed.push({
424
+ ...item.dependency,
425
+ contentSha256: installed.contentSha256,
426
+ archiveSha256: installed.archiveSha256,
427
+ installedFrom: normalizedBaseUrl(args),
428
+ });
429
+ }
430
+ return result;
431
+ }
432
+
433
+ async function syncFlowNodePackages(args, flowTarget, workspaceRoot) {
434
+ const flow = readFlowDependencySource(flowTarget);
435
+ const dependencies = await flowNodePackageDependencies(flow.source);
436
+ return syncNodePackageDependencies(args, dependencies, workspaceRoot, { flow: flow.resolved });
437
+ }
438
+
439
+ async function graphNodePackageDependencies(graph) {
440
+ const runtime = await loadAgentFlowRuntime();
441
+ const bySpecifier = new Map();
442
+ for (const instance of Object.values(graph?.instances || {})) {
443
+ const explicit = String(instance?.marketplaceRef || "").trim();
444
+ const fallback = String(instance?.definitionId || "").trim();
445
+ const specifier = explicit || (fallback.startsWith("marketplace:") ? fallback : "");
446
+ if (!specifier) continue;
447
+ const parsed = runtime.parseMarketplaceDefinitionId(specifier);
448
+ if (!parsed?.id || !parsed.version) {
449
+ throw new Error(`Flow contains an invalid or unpinned node package reference: ${specifier}`);
450
+ }
451
+ bySpecifier.set(specifier, { id: parsed.id, version: parsed.version, specifier, line: 0 });
452
+ }
453
+ return [...bySpecifier.values()].sort((a, b) => a.specifier.localeCompare(b.specifier));
454
+ }
455
+
456
+ function pullFlowOutputDir(args, flowId, workspaceRoot) {
457
+ const explicit = option(args, "output") || option(args, "file");
458
+ if (explicit) return path.resolve(explicit);
459
+ if (!/^[A-Za-z0-9._-]+$/.test(flowId) || flowId === "." || flowId === "..") {
460
+ throw new Error("--flow-id contains characters unsafe for a default local directory; pass --output explicitly.");
461
+ }
462
+ return path.join(workspaceRoot, ".workspace", "agentflow", "pipelines", flowId);
463
+ }
464
+
465
+ async function pullFlow(args) {
466
+ const flowId = requireFlowId(args);
467
+ const flowSource = option(args, "flow-source") || "user";
468
+ const workspaceRoot = path.resolve(option(args, "workspace-root") || process.cwd());
469
+ const outputDir = pullFlowOutputDir(args, flowId, workspaceRoot);
470
+ if (fs.existsSync(outputDir)) {
471
+ if (!fs.statSync(outputDir).isDirectory()) throw new Error(`Pull target is not a directory: ${outputDir}`);
472
+ const entries = fs.readdirSync(outputDir);
473
+ if (entries.length && args.replace !== true) {
474
+ throw new Error(`Pull target is not empty: ${outputDir}. Pass --replace to update its managed flow files.`);
475
+ }
476
+ }
477
+ const current = await httpJson(args, `/api/workspace/graph${query({ flowId, flowSource })}`);
478
+ const graph = current?.graph;
479
+ if (!graph || typeof graph !== "object") throw new Error(`Server returned no workspace graph for ${flowId}.`);
480
+ const dependencies = await graphNodePackageDependencies(graph);
481
+ const nodePackages = await syncNodePackageDependencies(args, dependencies, workspaceRoot, { flowId, flowSource });
482
+ if (!nodePackages.ok) return { ok: false, flowId, flowSource, outputDir, nodePackages };
483
+
484
+ const runtime = await loadAgentFlowRuntime();
485
+ const { design } = runtime.splitWorkspaceGraph(graph);
486
+ const written = runtime.writeWorkspaceGraphFiles(outputDir, design, { marketplaceRoot: workspaceRoot });
487
+ return {
488
+ ok: true,
489
+ flowId,
490
+ flowSource,
491
+ outputDir,
492
+ revision: current.revision || "",
493
+ format: written.format,
494
+ path: path.join(outputDir, written.format === "dsl" ? "workspace.flow.js" : "workspace.graph.json"),
495
+ nodePackages,
496
+ };
497
+ }
498
+
499
+ async function preflightRemoteFlowDependencies(args, source) {
500
+ if (!source.isCode) return [];
501
+ const dependencies = await flowNodePackageDependencies(source.flowYaml);
502
+ if (!dependencies.length) return [];
503
+ const remoteByKey = await remoteNodePackageCatalog(args);
504
+ const missing = dependencies.filter((dependency) => !remoteByKey.has(`${dependency.id}@${dependency.version}`));
505
+ if (missing.length) {
506
+ throw new Error(
507
+ `Cannot publish flow: server is missing node packages ${missing.map((dependency) => dependency.specifier).join(", ")}. `
508
+ + "Publish those exact versions first with node-package-publish.",
509
+ );
510
+ }
511
+ return dependencies;
512
+ }
513
+
205
514
  function asArray(value) {
206
515
  if (value === undefined) return [];
207
516
  return Array.isArray(value) ? value : [value];
@@ -308,18 +617,129 @@ function readJsonFile(filePath) {
308
617
  }
309
618
  }
310
619
 
311
- function readFlowYamlFile(filePath) {
620
+ // 代码化的流程目录里没有 flow.yaml,权威存储是 workspace.flow.js。两种都要能发布。
621
+ const FLOW_MARKERS = ["workspace.flow.js", "workspace.graph.json", "flow.yaml"];
622
+
623
+ /**
624
+ * 单文件上传只能带一个文件,所以流程目录里如果还有这些东西,它们发不上去。宁可当场报错,
625
+ * 也不要把一个残缺的流程静悄悄发布出去——画布上少了坐标还好说,少了 marketplaceRef 或者
626
+ * nodes/ 里的代码节点包,那流程根本跑不起来。
627
+ */
628
+ const UNSHIPPABLE = ["workspace.nodes.json", "nodes"];
629
+
630
+ function readFlowSourceFile(filePath, { allowPackageDependencies = false } = {}) {
312
631
  const requested = String(filePath || "").trim();
313
- if (!requested) throw new Error("Missing --file <flow.yaml>.");
314
- const resolved = path.resolve(requested);
315
- let flowYaml;
632
+ if (!requested) throw new Error(`Missing --file <flowDir|${FLOW_MARKERS.join("|")}>.`);
633
+ let resolved = path.resolve(requested);
634
+
635
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
636
+ const dir = resolved;
637
+ const marker = FLOW_MARKERS.find((name) => fs.existsSync(path.join(dir, name)));
638
+ if (!marker) {
639
+ throw new Error(`Not a flow directory (no ${FLOW_MARKERS.join(" / ")}): ${dir}`);
640
+ }
641
+ const extra = allowPackageDependencies ? [] : UNSHIPPABLE.filter((name) => fs.existsSync(path.join(dir, name)));
642
+ if (extra.length) {
643
+ throw new Error(
644
+ `Cannot publish ${dir}: it also contains ${extra.join(", ")}, which a single-file upload cannot carry. `
645
+ + "Publish a flow whose graph is self-contained, or package it by hand for now.",
646
+ );
647
+ }
648
+ resolved = path.join(dir, marker);
649
+ }
650
+
651
+ let flowSource;
316
652
  try {
317
- flowYaml = fs.readFileSync(resolved, "utf8");
653
+ flowSource = fs.readFileSync(resolved, "utf8");
318
654
  } catch (error) {
319
655
  throw new Error(`Cannot read flow file ${resolved}: ${error?.message || String(error)}`);
320
656
  }
321
- if (!flowYaml.trim()) throw new Error(`Flow file is empty: ${resolved}`);
322
- return { resolved, flowYaml };
657
+ if (!flowSource.trim()) throw new Error(`Flow file is empty: ${resolved}`);
658
+ return {
659
+ resolved,
660
+ flowDir: path.dirname(resolved),
661
+ flowYaml: flowSource,
662
+ isCode: /\.m?js$/i.test(resolved),
663
+ };
664
+ }
665
+
666
+ async function uploadPreparedNodePackage(args, packed) {
667
+ const form = new FormData();
668
+ form.set(
669
+ "file",
670
+ new Blob([packed.archive], { type: "application/zip" }),
671
+ `${packed.manifest.id}-${packed.manifest.version}.zip`,
672
+ );
673
+ return httpMultipart(args, "/api/node-packages", form);
674
+ }
675
+
676
+ function nodePackageDirectories(flowDir) {
677
+ const root = path.join(flowDir, "nodes");
678
+ if (!fs.existsSync(root)) return [];
679
+ if (!fs.statSync(root).isDirectory()) throw new Error(`Flow nodes path is not a directory: ${root}`);
680
+ return fs.readdirSync(root, { withFileTypes: true })
681
+ .filter((entry) => entry.isDirectory())
682
+ .map((entry) => path.join(root, entry.name))
683
+ .sort();
684
+ }
685
+
686
+ async function prepareFlowWithDependencies(args, filePath, existingSource = null) {
687
+ const source = existingSource || readFlowSourceFile(filePath, { allowPackageDependencies: true });
688
+ if (!source.isCode) {
689
+ throw new Error("--with-dependencies only supports code DSL flows (workspace.flow.js).");
690
+ }
691
+ const runtime = await loadAgentFlowRuntime();
692
+ // 全部本地包先验证并打包;任何一个坏包都不会触发远端写入。
693
+ const packedCandidates = nodePackageDirectories(source.flowDir).map((packageDir) => {
694
+ const item = runtime.createNodePackageArchive(packageDir);
695
+ if (!item.ok) throw new Error(`${packageDir}: ${item.error || "Cannot package node."}`);
696
+ return { ...item, packageDir };
697
+ });
698
+
699
+ const packedByKey = new Map();
700
+ for (const item of packedCandidates) {
701
+ const key = `${item.manifest.id}@${item.manifest.version}`;
702
+ const previous = packedByKey.get(key);
703
+ if (previous && previous.contentSha256 !== item.contentSha256) {
704
+ throw new Error(`Flow contains two different local packages with the same version ${key}.`);
705
+ }
706
+ if (!previous) packedByKey.set(key, item);
707
+ }
708
+ const packed = [...packedByKey.values()];
709
+
710
+ const available = runtime.scanFlowLocalPackages(source.flowDir);
711
+ const portable = runtime.rewriteFlowLocalPackageImports(source.flowYaml, available);
712
+
713
+ // 上传之前先比较所有已存在版本,避免发布一半后才发现后面的同版本冲突。
714
+ const remoteByKey = await remoteNodePackageCatalog(args);
715
+ for (const item of packed) {
716
+ const key = `${item.manifest.id}@${item.manifest.version}`;
717
+ const remote = remoteByKey.get(key);
718
+ if (remote?.contentSha256 && remote.contentSha256 !== item.contentSha256) {
719
+ throw new Error(
720
+ `Cannot publish ${key}: server already has the same version with different content. Bump the node package version.`,
721
+ );
722
+ }
723
+ }
724
+
725
+ const packageResults = [];
726
+ for (const item of packed) {
727
+ const uploaded = await uploadPreparedNodePackage(args, item);
728
+ packageResults.push({
729
+ id: item.manifest.id,
730
+ version: item.manifest.version,
731
+ specifier: `marketplace:${item.manifest.id}@${item.manifest.version}`,
732
+ contentSha256: item.contentSha256,
733
+ alreadyExists: Boolean(uploaded?.alreadyExists),
734
+ });
735
+ }
736
+
737
+ return {
738
+ ...source,
739
+ flowYaml: portable.source,
740
+ rewrittenImports: portable.rewritten,
741
+ publishedNodePackages: packageResults,
742
+ };
323
743
  }
324
744
 
325
745
  function targetDestinationFromArgs(args) {
@@ -330,11 +750,18 @@ function targetDestinationFromArgs(args) {
330
750
  throw new Error("Invalid --target-space. Use personal|workspace|team (alias: user).");
331
751
  }
332
752
 
753
+ function isMissingPublishedFlowError(error) {
754
+ if (error?.status === 404) return true;
755
+ return error?.status === 400 && /Pipeline directory not found/i.test(String(error?.message || ""));
756
+ }
757
+
333
758
  async function importFlow(args, { flowId, targetSpace, resolved, flowYaml }) {
334
759
  const form = new FormData();
335
760
  form.set("flowId", flowId);
336
761
  form.set("targetSpace", targetSpace);
337
- form.set("file", new Blob([flowYaml], { type: "application/yaml" }), path.basename(resolved));
762
+ const name = path.basename(resolved);
763
+ const mime = /\.m?js$/i.test(name) ? "application/javascript" : "application/yaml";
764
+ form.set("file", new Blob([flowYaml], { type: mime }), name);
338
765
  return httpMultipart(args, "/api/flows/import", form);
339
766
  }
340
767
 
@@ -359,6 +786,7 @@ async function sharePublishedFlowWithTeam(args, { flowId, flowSource, team }) {
359
786
  async function main() {
360
787
  loadEnvFiles();
361
788
  const args = parseArgv(process.argv.slice(2));
789
+ configureAgentFlowRuntime({ packageRoot: option(args, "agentflow-package-root") });
362
790
  const command = args._[0] || "help";
363
791
  if (command === "help" || command === "--help" || command === "-h") {
364
792
  process.stdout.write(usage());
@@ -366,10 +794,19 @@ async function main() {
366
794
  }
367
795
 
368
796
  if (command === "config") {
797
+ let localRuntime = { available: false, root: "", version: "" };
798
+ try {
799
+ const runtimeRoot = resolveAgentFlowPackageRoot();
800
+ const manifest = JSON.parse(fs.readFileSync(path.join(runtimeRoot, "package.json"), "utf-8"));
801
+ localRuntime = { available: true, root: runtimeRoot, version: String(manifest.version || "") };
802
+ } catch (error) {
803
+ localRuntime.error = error?.message || String(error);
804
+ }
369
805
  printJson({
370
806
  baseUrl: normalizedBaseUrl(args),
371
807
  hasToken: Boolean(authToken(args, false)),
372
808
  tokenSource: option(args, "token") ? "flag" : process.env.AGENTFLOW_TOKEN ? "AGENTFLOW_TOKEN" : process.env.AGENTFLOW_SESSION_TOKEN ? "AGENTFLOW_SESSION_TOKEN" : "",
809
+ localRuntime,
373
810
  });
374
811
  return;
375
812
  }
@@ -391,31 +828,118 @@ async function main() {
391
828
  return;
392
829
  }
393
830
 
831
+ if (command === "node-package-list") {
832
+ printJson(await httpJson(args, "/api/node-packages"));
833
+ return;
834
+ }
835
+
836
+ if (command === "node-package-search") {
837
+ const queryText = option(args, "query") || option(args, "q");
838
+ if (!queryText) throw new Error("Missing --query <text>.");
839
+ const catalog = await httpJson(args, "/api/node-packages");
840
+ const matches = searchNodePackageCatalog(Array.isArray(catalog?.nodes) ? catalog.nodes : [], queryText, option(args, "limit"));
841
+ printJson({ query: queryText, count: matches.length, nodes: matches });
842
+ return;
843
+ }
844
+
845
+ if (command === "node-package-publish") {
846
+ const packageDir = option(args, "file");
847
+ if (!packageDir) throw new Error("Missing --file <nodePackageDir>.");
848
+ const runtime = await loadAgentFlowRuntime();
849
+ const packed = runtime.createNodePackageArchive(packageDir);
850
+ if (!packed.ok) throw new Error(packed.error || "Cannot package node.");
851
+ printJson(await uploadPreparedNodePackage(args, packed));
852
+ return;
853
+ }
854
+
855
+ if (command === "node-package-install") {
856
+ const spec = parseNodePackageSpec(option(args, "node"));
857
+ const workspaceRoot = path.resolve(option(args, "workspace-root") || process.cwd());
858
+ const dependency = { ...spec, specifier: `marketplace:${spec.id}@${spec.version}`, line: 0 };
859
+ const remote = (await remoteNodePackageCatalog(args)).get(`${spec.id}@${spec.version}`);
860
+ if (!remote) throw new Error(`Node package not found on server: ${dependency.specifier}`);
861
+ const prepared = await inspectDownloadedNodePackage(args, dependency, remote);
862
+ const runtime = await loadAgentFlowRuntime();
863
+ const installed = runtime.publishNodePackageArchive(workspaceRoot, prepared.downloaded.buffer, {
864
+ installedFrom: normalizedBaseUrl(args),
865
+ installedAt: new Date().toISOString(),
866
+ });
867
+ if (!installed.ok) throw new Error(installed.error || "Cannot install node package.");
868
+ printJson({ ...installed, workspaceRoot });
869
+ return;
870
+ }
871
+
872
+ if (command === "node-package-sync") {
873
+ const flowTarget = option(args, "flow") || option(args, "file");
874
+ const workspaceRoot = path.resolve(option(args, "workspace-root") || process.cwd());
875
+ const result = await syncFlowNodePackages(args, flowTarget, workspaceRoot);
876
+ printJson(result);
877
+ if (!result.ok) process.exitCode = 2;
878
+ return;
879
+ }
880
+
881
+ if (command === "pull-flow") {
882
+ const result = await pullFlow(args);
883
+ printJson(result);
884
+ if (!result.ok) process.exitCode = 2;
885
+ return;
886
+ }
887
+
394
888
  if (command === "publish-flow") {
395
889
  const flowId = requireFlowId(args);
396
890
  const destination = targetDestinationFromArgs(args);
397
891
  const targetSpace = destination.flowSource;
398
- const source = readFlowYamlFile(option(args, "file"));
892
+ const withDependencies = args["with-dependencies"] === true;
893
+ const requestedFile = option(args, "file");
894
+ const rawSource = readFlowSourceFile(requestedFile, { allowPackageDependencies: withDependencies });
399
895
  const replace = args.replace === true;
400
896
  const team = await resolvePublishTeam(args, destination.shareWithTeam);
401
897
 
898
+ // 先读目标是否存在,再上传不可变节点版本。这样 create-only 的 409 不会在服务端留下
899
+ // 一个 Flow 没发布成功、节点包却已经出现的半次发布。
900
+ let current = null;
901
+ try {
902
+ current = rawSource.isCode
903
+ ? await httpJson(args, `/api/workspace/graph${query({ flowId, flowSource: targetSpace })}`)
904
+ : await httpJson(args, `/api/flow${query({ flowId, flowSource: targetSpace })}`);
905
+ } catch (error) {
906
+ if (!isMissingPublishedFlowError(error)) throw error;
907
+ }
908
+ if (current && !replace) {
909
+ throw new Error(`已存在同名流水线 ${flowId};确认更新后请显式传 --replace。`);
910
+ }
911
+
912
+ const source = withDependencies
913
+ ? await prepareFlowWithDependencies(args, requestedFile, rawSource)
914
+ : rawSource;
915
+ const dependencyPreflight = await preflightRemoteFlowDependencies(args, source);
916
+
402
917
  if (!replace) {
403
918
  const result = await importFlow(args, { flowId, targetSpace, ...source });
404
919
  const sharedTeam = await sharePublishedFlowWithTeam(args, { flowId, flowSource: targetSpace, team });
405
- printJson({ ...result, action: "created", targetSpace: team ? "team" : targetSpace, team: sharedTeam, file: source.resolved });
920
+ printJson({ ...result, action: "created", targetSpace: team ? "team" : targetSpace, team: sharedTeam, file: source.resolved, nodeDependencies: dependencyPreflight.map((item) => item.specifier), rewrittenImports: source.rewrittenImports || [], publishedNodePackages: source.publishedNodePackages || [] });
406
921
  return;
407
922
  }
408
923
 
409
- let current = null;
410
- try {
411
- current = await httpJson(args, `/api/flow${query({ flowId, flowSource: targetSpace })}`);
412
- } catch (error) {
413
- if (error?.status !== 404) throw error;
414
- }
924
+ // 更新走哪条路取决于存储格式:yaml 流程改 /api/flow,代码化流程改 Workspace 图。
925
+ // /api/flow 只认 flowYaml 字符串,代码化的流程发过去等于把图退回成 yaml。
415
926
  if (!current) {
416
927
  const result = await importFlow(args, { flowId, targetSpace, ...source });
417
928
  const sharedTeam = await sharePublishedFlowWithTeam(args, { flowId, flowSource: targetSpace, team });
418
- printJson({ ...result, action: "created", targetSpace: team ? "team" : targetSpace, team: sharedTeam, file: source.resolved });
929
+ printJson({ ...result, action: "created", targetSpace: team ? "team" : targetSpace, team: sharedTeam, file: source.resolved, nodeDependencies: dependencyPreflight.map((item) => item.specifier), rewrittenImports: source.rewrittenImports || [], publishedNodePackages: source.publishedNodePackages || [] });
930
+ return;
931
+ }
932
+ if (source.isCode) {
933
+ const graph = await readWorkspaceGraphArg(
934
+ option(args, "file"),
935
+ path.resolve(option(args, "workspace-root") || process.cwd()),
936
+ );
937
+ const updated = await httpJson(args, "/api/workspace/graph", {
938
+ method: "POST",
939
+ body: { flowId, flowSource: targetSpace, graph, baseRevision: current.revision },
940
+ });
941
+ const sharedTeam = await sharePublishedFlowWithTeam(args, { flowId, flowSource: targetSpace, team });
942
+ printJson({ ...updated, action: "replaced", targetSpace: team ? "team" : targetSpace, team: sharedTeam, file: source.resolved, nodeDependencies: dependencyPreflight.map((item) => item.specifier), rewrittenImports: source.rewrittenImports || [], publishedNodePackages: source.publishedNodePackages || [] });
419
943
  return;
420
944
  }
421
945
  const result = await httpJson(args, "/api/flow", {
@@ -439,8 +963,118 @@ async function main() {
439
963
  return;
440
964
  }
441
965
 
966
+ /**
967
+ * 一键把这个账号能看到的所有流程迁到当前的权威存储格式。
968
+ *
969
+ * 两类活儿,风险完全不同:
970
+ * graph.json -> 代码 换存储格式,词汇表不变,往返比对闸门保证图等价——无损,随便跑
971
+ * flow.yaml -> 代码 要换节点词汇表,可能丢东西——默认拒绝,把清单摆出来给人看
972
+ *
973
+ * 所以默认行为是「无损的全做掉,有损的一个不碰、列出来」。跑完看报告再决定要不要
974
+ * 对那几个加 --allow-loss。重复跑是幂等的:已经是代码形态的流程直接跳过。
975
+ */
976
+ if (command === "migrate-all") {
977
+ const dryRun = args["dry-run"] === true;
978
+ const allowLoss = args["allow-loss"] === true;
979
+ const includeArchived = args["include-archived"] === true;
980
+ const flowsRaw = await httpJson(args, "/api/flows");
981
+ const all = Array.isArray(flowsRaw) ? flowsRaw : flowsRaw.flows || flowsRaw.items || [];
982
+ // builtin 与 admin 是只读目录(服务端 isReadonlyBuiltinFlowSource),写不回去。
983
+ // 它们本来也不依赖 flow.yaml 哨兵,留在原格式没有代价——所以是「跳过」,不是「失败」。
984
+ const readonly = all.filter((f) => f.source === "builtin" || f.source === "admin");
985
+ const skippedArchived = includeArchived ? [] : all.filter((f) => f.archived && !readonly.includes(f));
986
+ const flows = all
987
+ .filter((f) => !readonly.includes(f))
988
+ .filter((f) => includeArchived || !f.archived);
989
+
990
+ const rows = [];
991
+ for (const flow of flows) {
992
+ const label = `${flow.id}${flow.archived ? " (归档)" : ""} [${flow.source}]`;
993
+ const body = {
994
+ flowId: flow.id,
995
+ flowSource: flow.source,
996
+ archived: Boolean(flow.archived),
997
+ allowArchived: Boolean(flow.archived),
998
+ allowLoss,
999
+ };
1000
+ if (dryRun) {
1001
+ // 干跑靠的是服务端「默认拒绝有损」那条闸门本身:不带 allowLoss 发过去,有损的
1002
+ // 会原样退回清单且不落盘。无损的会真的迁——所以干跑只对有损那批有意义,
1003
+ // 这里改成只读探一次格式,一个字节都不写。
1004
+ const graph = await httpJson(args, `/api/workspace/graph${query({
1005
+ flowId: flow.id, flowSource: flow.source, archived: flow.archived ? "1" : "",
1006
+ })}`).catch((e) => ({ error: String(e.message || e) }));
1007
+ const file = String(graph.path || "").split("/").pop() || "";
1008
+ const nodes = Object.keys(graph.graph?.instances || {}).length;
1009
+ rows.push({
1010
+ flow: label,
1011
+ store: graph.error ? `读不到: ${graph.error}`
1012
+ : nodes > 0 ? (file === "workspace.flow.js" ? "代码(已是最新)" : "graph.json(待迁)")
1013
+ : "空图(多半是仅 yaml)",
1014
+ nodes,
1015
+ });
1016
+ continue;
1017
+ }
1018
+ const r = await httpJson(args, "/api/workspace/migrate", { method: "POST", body })
1019
+ .catch((e) => ({ error: String(e.message || e) }));
1020
+ const lost = [...(r.dropped || []), ...(r.droppedEdges || [])].filter((x) => !x.benign);
1021
+ rows.push({
1022
+ flow: label,
1023
+ result: r.error ? `失败: ${r.error}`
1024
+ : r.migrated ? "→ 代码"
1025
+ : r.leftYaml ? "→ graph.json(DSL 装不下部分字段)"
1026
+ : r.format === "dsl" ? "已是代码,跳过"
1027
+ : r.format === "yaml" ? `拒绝:有损(${lost.length} 处)`
1028
+ : `未处理(${r.format})`,
1029
+ remapped: (r.remapped || []).map((x) => `${x.from}->${x.to}`),
1030
+ lost: lost.map((x) => x.id || `${x.source}->${x.target}`),
1031
+ caveats: (r.remapped || []).filter((x) => x.caveat).map((x) => `${x.id}: ${x.caveat}`),
1032
+ });
1033
+ }
1034
+
1035
+ const refused = rows.filter((r) => String(r.result || "").startsWith("拒绝"));
1036
+ const failed = rows.filter((r) => String(r.result || "").startsWith("失败"));
1037
+ printJson({
1038
+ total: rows.length,
1039
+ dryRun,
1040
+ rows,
1041
+ // 只读目录和归档要交代清楚跳过了什么,否则「跑完了」会被读成「全覆盖了」
1042
+ skipped: {
1043
+ readonly: readonly.map((f) => `${f.id} [${f.source}]`),
1044
+ archived: skippedArchived.map((f) => `${f.id} [${f.source}]`),
1045
+ },
1046
+ // 拒绝的单独拎出来:一键跑完之后要人做决定的就这些
1047
+ needsDecision: refused.map((r) => r.flow),
1048
+ hint: refused.length
1049
+ ? "这些流程有节点/边在 Workspace 里没有对等物。看过 lost 清单后,对单个流程跑 migrate-flow --allow-loss。"
1050
+ : undefined,
1051
+ });
1052
+ if (failed.length) process.exitCode = 1;
1053
+ return;
1054
+ }
1055
+
1056
+ // 平台上还停在 flow.yaml 的老流程:列在列表里、点开是空图、跑不了。这条命令是它们的出口。
1057
+ // 默认拒绝有损迁移并把清单打出来,看过之后再加 --allow-loss。
1058
+ if (command === "migrate-flow") {
1059
+ const flowId = requireFlowId(args);
1060
+ const result = await httpJson(args, "/api/workspace/migrate", {
1061
+ method: "POST",
1062
+ body: {
1063
+ flowId,
1064
+ flowSource: option(args, "flow-source") || "user",
1065
+ archived: args.archived === true,
1066
+ allowArchived: args.archived === true,
1067
+ allowLoss: args["allow-loss"] === true,
1068
+ },
1069
+ });
1070
+ printJson(result);
1071
+ // 只有「因为有损而拒绝」才算失败。已经是代码形态是空操作,不是错。
1072
+ if (result.format === "yaml") process.exitCode = 1;
1073
+ return;
1074
+ }
1075
+
442
1076
  if (command === "workspace-preview" || command === "preview-workspace") {
443
- const graph = readJsonFile(option(args, "file"));
1077
+ const graph = await readWorkspaceGraphArg(option(args, "file"));
444
1078
  const result = await httpJson(args, "/api/workspace/preview", {
445
1079
  method: "POST",
446
1080
  body: {