@fieldwangai/agentflow 0.1.153 → 0.1.156

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 (193) 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 +277 -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 +303 -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 +1092 -18123
  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-preview.mjs +12 -2
  57. package/bin/lib/workspace-routes.mjs +3088 -0
  58. package/bin/lib/workspace-server.mjs +6199 -0
  59. package/bin/lib/workspace-state.mjs +331 -0
  60. package/bin/lib/workspace.mjs +2 -1
  61. package/builtin/nodes/agent_subAgent.md +1 -0
  62. package/builtin/nodes/control_agent_toBool.md +2 -0
  63. package/builtin/nodes/control_anyOne.md +2 -0
  64. package/builtin/nodes/control_cancelled.md +2 -0
  65. package/builtin/nodes/control_cd_workspace.md +5 -11
  66. package/builtin/nodes/control_delay.md +2 -0
  67. package/builtin/nodes/control_end.md +2 -0
  68. package/builtin/nodes/control_if.md +1 -0
  69. package/builtin/nodes/control_interval_loop.md +2 -0
  70. package/builtin/nodes/control_load_mcp.md +24 -0
  71. package/builtin/nodes/control_load_skills.md +8 -23
  72. package/builtin/nodes/control_start.md +2 -0
  73. package/builtin/nodes/control_toBool.md +2 -0
  74. package/builtin/nodes/control_user_workspace.md +2 -0
  75. package/builtin/nodes/control_wait_until.md +2 -0
  76. package/builtin/nodes/display_ascii.md +1 -0
  77. package/builtin/nodes/display_chart.md +1 -0
  78. package/builtin/nodes/display_html.md +1 -0
  79. package/builtin/nodes/display_image.md +1 -0
  80. package/builtin/nodes/display_markdown.md +1 -0
  81. package/builtin/nodes/display_mermaid.md +1 -0
  82. package/builtin/nodes/display_react_app.md +1 -0
  83. package/builtin/nodes/display_table.md +1 -0
  84. package/builtin/nodes/provide_bool.md +1 -0
  85. package/builtin/nodes/provide_file.md +1 -0
  86. package/builtin/nodes/provide_password.md +1 -0
  87. package/builtin/nodes/provide_str.md +1 -0
  88. package/builtin/nodes/tool_display_share_link.md +1 -0
  89. package/builtin/nodes/tool_get_env.md +2 -0
  90. package/builtin/nodes/tool_git_checkout.md +1 -0
  91. package/builtin/nodes/tool_git_worktree_load.md +1 -0
  92. package/builtin/nodes/tool_git_worktree_unload.md +1 -0
  93. package/builtin/nodes/tool_gitlab_create_mr.md +1 -0
  94. package/builtin/nodes/tool_jenkins_build.md +2 -0
  95. package/builtin/nodes/tool_load_key.md +2 -0
  96. package/builtin/nodes/tool_nodejs.md +17 -19
  97. package/builtin/nodes/tool_print.md +2 -0
  98. package/builtin/nodes/tool_save_key.md +2 -0
  99. package/builtin/nodes/tool_set_run_env.md +1 -0
  100. package/builtin/nodes/tool_user_ask.md +2 -0
  101. package/builtin/nodes/tool_user_check.md +2 -0
  102. package/builtin/nodes/tool_wecom_send_app_markdown.md +1 -0
  103. package/builtin/nodes/tool_wecom_send_group_markdown.md +1 -0
  104. package/builtin/nodes/workspace_one_click_task.md +44 -0
  105. package/builtin/nodes/workspace_run.md +16 -0
  106. package/builtin/nodes/workspace_scheduled_run.md +16 -0
  107. package/builtin/pipelines/module-migrate/scripts/gate.mjs +37 -0
  108. package/builtin/pipelines/module-migrate/scripts/static-check.mjs +82 -0
  109. package/builtin/pipelines/module-migrate/workspace.flow.js +172 -0
  110. package/builtin/pipelines/module-migrate/workspace.layout.json +134 -0
  111. package/builtin/pipelines/new/scripts/lint-flow.mjs +38 -0
  112. package/builtin/pipelines/new/workspace.flow.js +91 -0
  113. package/builtin/pipelines/new/workspace.layout.json +70 -0
  114. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-B9xslwI0.js → WorkflowAssistantThread-ClNxY2Wu.js} +1 -1
  115. package/builtin/web-ui/dist/assets/index-BJzMYRK3.js +870 -0
  116. package/builtin/web-ui/dist/assets/index-CEXmmwM2.css +1 -0
  117. package/builtin/web-ui/dist/index.html +2 -2
  118. package/package.json +3 -1
  119. package/reference/flow-control-capabilities.md +77 -158
  120. package/reference/flow-layout.md +1 -1
  121. package/reference/flow-prompt-handler-check.md +2 -2
  122. package/skills/agentflow-author-flow/SKILL.md +1 -1
  123. package/skills/agentflow-cli/SKILL.md +139 -24
  124. package/skills/agentflow-cli/agents/openai.yaml +2 -2
  125. package/skills/agentflow-cli/scripts/agentflow-cli.mjs +654 -20
  126. package/skills/agentflow-cli/scripts/agentflow-runtime.mjs +97 -0
  127. package/skills/agentflow-flow-add-instances/SKILL.md +1 -1
  128. package/skills/agentflow-flow-dsl/SKILL.md +206 -0
  129. package/skills/agentflow-flow-dsl/agents/openai.yaml +4 -0
  130. package/skills/agentflow-flow-dsl/references/node-calls.md +39 -0
  131. package/skills/agentflow-flow-edit-node-fields/SKILL.md +1 -1
  132. package/skills/agentflow-flow-recipes/SKILL.md +7 -4
  133. package/skills/agentflow-flow-recipes/references/recipes.md +97 -43
  134. package/skills/agentflow-flow-sync-ui/SKILL.md +1 -1
  135. package/skills/agentflow-node-dsl/SKILL.md +212 -0
  136. package/skills/agentflow-node-dsl/agents/openai.yaml +4 -0
  137. package/skills/agentflow-node-reference/SKILL.md +2 -2
  138. package/skills/agentflow-node-reference/references/builtin-nodes.md +166 -115
  139. package/skills/agentflow-runtime-reference/references/runtime.md +1 -1
  140. package/skills/agentflow-workspace-ascii/SKILL.md +9 -16
  141. package/skills/agentflow-workspace-graph/SKILL.md +62 -48
  142. package/skills/agentflow-workspace-html/SKILL.md +7 -2
  143. package/skills/agentflow-workspace-image/SKILL.md +6 -2
  144. package/skills/agentflow-workspace-markdown/SKILL.md +14 -21
  145. package/skills/agentflow-workspace-mermaid/SKILL.md +8 -16
  146. package/bin/lib/api-runner.mjs +0 -387
  147. package/bin/lib/apply.mjs +0 -903
  148. package/bin/lib/composer-flow-instances.mjs +0 -68
  149. package/bin/lib/composer-flow-skeleton.mjs +0 -334
  150. package/bin/lib/composer-flow-validate.mjs +0 -47
  151. package/bin/lib/composer-model-router.mjs +0 -185
  152. package/bin/lib/composer-node-schema.mjs +0 -303
  153. package/bin/lib/composer-planner.mjs +0 -751
  154. package/bin/lib/composer-script-ops.mjs +0 -233
  155. package/bin/lib/flow-static-preview.mjs +0 -104
  156. package/bin/lib/hub-login.mjs +0 -54
  157. package/bin/lib/hub-publish.mjs +0 -159
  158. package/bin/lib/hub-remote.mjs +0 -189
  159. package/bin/lib/hub.mjs +0 -299
  160. package/bin/lib/jenkins.mjs +0 -380
  161. package/bin/lib/node-execute.mjs +0 -539
  162. package/bin/lib/normalize-node-tool-command.mjs +0 -97
  163. package/bin/lib/runtime-context.mjs +0 -243
  164. package/bin/lib/scheduler.mjs +0 -601
  165. package/bin/lib/ui-print.mjs +0 -94
  166. package/bin/pipeline/build-node-prompt.mjs +0 -271
  167. package/bin/pipeline/check-cache.mjs +0 -191
  168. package/bin/pipeline/check-flow.mjs +0 -543
  169. package/bin/pipeline/collect-nodes.mjs +0 -212
  170. package/bin/pipeline/compute-cache-md5.mjs +0 -177
  171. package/bin/pipeline/ensure-run-dir.mjs +0 -71
  172. package/bin/pipeline/gc.mjs +0 -129
  173. package/bin/pipeline/get-env.mjs +0 -59
  174. package/bin/pipeline/get-resolved-values.mjs +0 -344
  175. package/bin/pipeline/load-key.mjs +0 -62
  176. package/bin/pipeline/parse-flow.mjs +0 -708
  177. package/bin/pipeline/post-process-control-if.mjs +0 -23
  178. package/bin/pipeline/post-process-node.mjs +0 -490
  179. package/bin/pipeline/pre-process-node.mjs +0 -1430
  180. package/bin/pipeline/resolve-inputs.mjs +0 -201
  181. package/bin/pipeline/run-tool-nodejs.mjs +0 -167
  182. package/bin/pipeline/save-key.mjs +0 -93
  183. package/bin/pipeline/snapshot-prior-round.mjs +0 -70
  184. package/bin/pipeline/validate-for-ui.mjs +0 -234
  185. package/bin/pipeline/validate-script-output.mjs +0 -130
  186. package/bin/pipeline/write-result.mjs +0 -182
  187. package/builtin/pipelines/module-migrate/flow.yaml +0 -819
  188. package/builtin/pipelines/new/flow.yaml +0 -545
  189. package/builtin/pipelines/new/scripts/check-flow.mjs +0 -9
  190. package/builtin/pipelines/new/scripts/collect-nodes.mjs +0 -211
  191. package/builtin/web-ui/dist/assets/index-DZ328oSo.css +0 -1
  192. package/builtin/web-ui/dist/assets/index-DmUV7ZCL.js +0 -888
  193. package/skills/agentflow-node-authoring/SKILL.md +0 -57
@@ -8,7 +8,16 @@ import {
8
8
  MARKETPLACE_PACKAGES_DIR,
9
9
  PIPELINES_DIR,
10
10
  getUserPipelinesRoot,
11
+ isFlowDir,
11
12
  } from "./paths.mjs";
13
+ import { NODE_PACKAGE_ENTRY, isNodePackageDir, readNodePackageManifest } from "./node-package-manifest.mjs";
14
+ import {
15
+ NODE_PACKAGE_METADATA_FILENAME,
16
+ createNodePackageArchive,
17
+ inspectNodePackageArchive,
18
+ inspectNodePackageDirectory,
19
+ writeNodePackageFiles,
20
+ } from "./node-package-archive.mjs";
12
21
 
13
22
  const NODE_MANIFEST = "node.yaml";
14
23
  const COLLECTION_MANIFEST = "collection.yaml";
@@ -80,6 +89,21 @@ function normalizeSlotList(value) {
80
89
  });
81
90
  }
82
91
 
92
+ /**
93
+ * 读取节点包清单:`index.mjs` 的静态声明优先,回退 `node.yaml`。
94
+ * 声明写得不合法时不静默吞掉——按目录名报错,否则节点会莫名从面板消失。
95
+ */
96
+ function readNodeManifestRaw(dir) {
97
+ try {
98
+ const manifest = readNodePackageManifest(dir, readYamlObject);
99
+ const metadata = readJsonObject(path.join(dir, NODE_PACKAGE_METADATA_FILENAME));
100
+ return manifest && metadata ? { ...manifest, ...metadata } : manifest;
101
+ } catch (e) {
102
+ console.warn(`[agentflow] 节点包 ${path.basename(dir)} 清单无效:${(e && e.message) || e}`);
103
+ return null;
104
+ }
105
+ }
106
+
83
107
  function normalizeManifest(raw, packageDir, source = "workspace") {
84
108
  if (!raw || typeof raw !== "object") return null;
85
109
  const id = raw.id != null ? String(raw.id).trim() : path.basename(packageDir);
@@ -187,7 +211,7 @@ function collectFlowDirs(rootDir, source, archived = false) {
187
211
  for (const entry of entries) {
188
212
  if (!entry.isDirectory() || entry.name === ARCHIVED_PIPELINES_DIR_NAME) continue;
189
213
  const dir = path.join(rootDir, entry.name);
190
- if (!fs.existsSync(path.join(dir, "flow.yaml"))) continue;
214
+ if (!isFlowDir(dir)) continue;
191
215
  out.push({ flowId: entry.name, flowSource: source, archived, flowDir: dir });
192
216
  }
193
217
  return out;
@@ -263,11 +287,11 @@ function findNodePackageDir(workspaceRoot, id, version) {
263
287
  const nodeBase = path.join(root, "nodes", id);
264
288
  if (version) {
265
289
  const direct = path.join(nodeBase, version);
266
- if (fs.existsSync(path.join(direct, NODE_MANIFEST))) return direct;
290
+ if (isNodePackageDir(direct)) return direct;
267
291
  } else {
268
292
  for (const v of sortVersionsDesc(listVersionDirs(nodeBase))) {
269
293
  const direct = path.join(nodeBase, v);
270
- if (fs.existsSync(path.join(direct, NODE_MANIFEST))) return direct;
294
+ if (isNodePackageDir(direct)) return direct;
271
295
  }
272
296
  }
273
297
  return null;
@@ -289,13 +313,13 @@ function iterCollectionNodeDirs(workspaceRoot, collectionDeps = []) {
289
313
  for (const entry of fs.readdirSync(nodesRoot, { withFileTypes: true })) {
290
314
  if (!entry.isDirectory()) continue;
291
315
  const direct = path.join(nodesRoot, entry.name);
292
- if (fs.existsSync(path.join(direct, NODE_MANIFEST))) {
316
+ if (isNodePackageDir(direct)) {
293
317
  out.push(direct);
294
318
  continue;
295
319
  }
296
320
  for (const nodeVersion of sortVersionsDesc(listVersionDirs(direct))) {
297
321
  const versioned = path.join(direct, nodeVersion);
298
- if (fs.existsSync(path.join(versioned, NODE_MANIFEST))) out.push(versioned);
322
+ if (isNodePackageDir(versioned)) out.push(versioned);
299
323
  }
300
324
  }
301
325
  }
@@ -317,6 +341,13 @@ function dependencyVersion(flowData, id) {
317
341
  return null;
318
342
  }
319
343
 
344
+ /**
345
+ * 只读,没有写入方——写 `agentflow.lock.json` 的是已删除的 `install-node`。
346
+ *
347
+ * 留着是因为磁盘上仍可能有它当年写下的锁文件,那些流程的版本钉应该继续生效。代码化流程
348
+ * 不依赖它:版本钉在实例的 `marketplaceRef`(`marketplace:<id>@<version>`)上,解析时
349
+ * `parsed.version` 先命中,这条兜底根本走不到。
350
+ */
320
351
  function lockVersion(flowDir, id) {
321
352
  const lock = readJsonObject(path.join(flowDir, LOCK_FILENAME));
322
353
  const entry = lock && lock.nodes && lock.nodes[id];
@@ -330,18 +361,58 @@ function collectionDeps(flowData) {
330
361
  return deps && Array.isArray(deps.collections) ? deps.collections : [];
331
362
  }
332
363
 
364
+ /**
365
+ * flow 自带的代码节点包:`<flowDir>/nodes/<dirName>/`,不带版本子目录。
366
+ *
367
+ * 这类包跟着 flow 走——AI 生成流程时可以顺手把节点实现写在流程目录里,不必先发布到
368
+ * marketplace。id 以包声明为准(目录名只是回退),所以目录名和 id 可以不一致。
369
+ */
370
+ function findFlowLocalNodePackageDir(flowDir, id, requestedVersion) {
371
+ const nodesRoot = flowDir ? path.join(path.resolve(flowDir), "nodes") : "";
372
+ if (!nodesRoot || !fs.existsSync(nodesRoot) || !fs.statSync(nodesRoot).isDirectory()) return null;
373
+ for (const entry of fs.readdirSync(nodesRoot, { withFileTypes: true })) {
374
+ if (!entry.isDirectory()) continue;
375
+ const dir = path.join(nodesRoot, entry.name);
376
+ if (!isNodePackageDir(dir)) continue;
377
+ const manifest = normalizeManifest(readNodeManifestRaw(dir), dir, "flow");
378
+ if (!manifest || manifest.id !== id) continue;
379
+ if (requestedVersion && manifest.version !== requestedVersion) continue;
380
+ return dir;
381
+ }
382
+ return null;
383
+ }
384
+
385
+ /**
386
+ * 列出一个 `nodes/` 目录下的所有代码节点包(子目录形式,不带版本层)。
387
+ * 供节点目录把 flow 自带的节点一起摆进面板。
388
+ */
389
+ export function listNodePackagesInDir(nodesRoot) {
390
+ const out = [];
391
+ const root = String(nodesRoot || "");
392
+ if (!root || !fs.existsSync(root) || !fs.statSync(root).isDirectory()) return out;
393
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
394
+ if (!entry.isDirectory()) continue;
395
+ const dir = path.join(root, entry.name);
396
+ if (!isNodePackageDir(dir)) continue;
397
+ const manifest = normalizeManifest(readNodeManifestRaw(dir), dir, "flow");
398
+ if (manifest) out.push(manifest);
399
+ }
400
+ return out.sort((a, b) => a.id.localeCompare(b.id));
401
+ }
402
+
333
403
  export function resolveMarketplaceNodePackage(workspaceRoot, flowDir, definitionId, flowData = null, opts = {}) {
334
404
  const parsed = parseMarketplaceDefinitionId(definitionId);
335
405
  if (!parsed) return null;
336
406
  const id = parsed.id;
337
407
  const requestedVersion = parsed.version || dependencyVersion(flowData, id) || lockVersion(flowDir, id);
338
- let packageDir = findNodePackageDir(workspaceRoot, id, requestedVersion);
339
- let packageSource = "marketplace";
408
+ // flow 自带的包优先:流程目录里的实现就是这个流程要用的那份,不该被同名的已发布包顶掉
409
+ let packageDir = findFlowLocalNodePackageDir(flowDir, id, requestedVersion);
410
+ let packageSource = packageDir ? "flow" : "marketplace";
411
+ if (!packageDir) packageDir = findNodePackageDir(workspaceRoot, id, requestedVersion);
340
412
 
341
413
  if (!packageDir) {
342
414
  for (const dir of iterCollectionNodeDirs(workspaceRoot, collectionDeps(flowData))) {
343
- const raw = readYamlObject(path.join(dir, NODE_MANIFEST));
344
- const manifest = normalizeManifest(raw, dir, "collection");
415
+ const manifest = normalizeManifest(readNodeManifestRaw(dir), dir, "collection");
345
416
  if (!manifest || manifest.id !== id) continue;
346
417
  if (requestedVersion && manifest.version !== requestedVersion) continue;
347
418
  packageDir = dir;
@@ -351,7 +422,7 @@ export function resolveMarketplaceNodePackage(workspaceRoot, flowDir, definition
351
422
  }
352
423
 
353
424
  if (!packageDir) return null;
354
- const manifest = normalizeManifest(readYamlObject(path.join(packageDir, NODE_MANIFEST)), packageDir, packageSource);
425
+ const manifest = normalizeManifest(readNodeManifestRaw(packageDir), packageDir, packageSource);
355
426
  if (!manifest) return null;
356
427
  if (!canAccessMarketplaceNode(manifest, opts)) return null;
357
428
  return {
@@ -366,7 +437,7 @@ export function listMarketplaceNodes(workspaceRoot, flowData = null, opts = {})
366
437
  const out = [];
367
438
  const seen = new Set();
368
439
  const addManifest = (dir, source = "marketplace") => {
369
- const manifest = normalizeManifest(readYamlObject(path.join(dir, NODE_MANIFEST)), dir, source);
440
+ const manifest = normalizeManifest(readNodeManifestRaw(dir), dir, source);
370
441
  if (!manifest) return;
371
442
  if (!canAccessMarketplaceNode(manifest, opts)) return;
372
443
  const key = `${manifest.id}@${manifest.version}`;
@@ -404,6 +475,15 @@ export function listMarketplacePackages(workspaceRoot, opts = {}) {
404
475
  inputs: n.input,
405
476
  outputs: n.output,
406
477
  packagedFiles: Array.isArray(n.packagedFiles) ? n.packagedFiles : [],
478
+ fileList: Array.isArray(n.fileList) ? n.fileList : [],
479
+ fileCount: Number(n.fileCount) || 0,
480
+ totalBytes: Number(n.totalBytes) || 0,
481
+ contentSha256: String(n.contentSha256 || ""),
482
+ archiveSha256: String(n.archiveSha256 || ""),
483
+ installedFrom: String(n.installedFrom || ""),
484
+ installedAt: String(n.installedAt || ""),
485
+ ownerUserId: n.ownerUserId || n.createdBy || "",
486
+ createdBy: n.createdBy || n.ownerUserId || "",
407
487
  packageDir: n.packageDir,
408
488
  usage: listMarketplaceNodeUsages(workspaceRoot, n.id, n.version, opts),
409
489
  }));
@@ -519,42 +599,126 @@ export function deleteMarketplaceFlowSnippetPackage(workspaceRoot, id, version,
519
599
  return { ok: true, id, version, packageDir };
520
600
  }
521
601
 
522
- export function writeFlowMarketplaceLock(workspaceRoot, flowDir, flowData, opts = {}) {
523
- if (!flowData || !flowData.instances || typeof flowData.instances !== "object") return null;
524
- const nodes = {};
525
- for (const inst of Object.values(flowData.instances)) {
526
- const defId = inst && (inst.marketplaceRef || inst.definitionId);
527
- const resolved = resolveMarketplaceNodePackage(workspaceRoot, flowDir, defId, flowData, opts);
528
- if (!resolved) continue;
529
- nodes[resolved.id] = {
530
- version: resolved.version,
531
- resolved: path.relative(flowDir, resolved.packageDir).replace(/\\/g, "/"),
532
- definitionId: resolved.resolvedDefinitionId,
533
- };
534
- }
535
- const lockPath = path.join(flowDir, LOCK_FILENAME);
536
- if (Object.keys(nodes).length === 0) return null;
537
- const lock = readJsonObject(lockPath) || {};
538
- const next = {
539
- ...lock,
540
- version: 1,
541
- updatedAt: new Date().toISOString(),
542
- nodes: { ...(lock.nodes && typeof lock.nodes === "object" ? lock.nodes : {}), ...nodes },
543
- };
544
- fs.writeFileSync(lockPath, JSON.stringify(next, null, 2) + "\n", "utf-8");
545
- return next;
546
- }
547
602
 
548
- export function publishNodePackage(workspaceRoot, sourceDir) {
603
+ export function publishNodePackage(workspaceRoot, sourceDir, opts = {}) {
549
604
  const src = path.resolve(sourceDir);
550
- const manifestPath = path.join(src, NODE_MANIFEST);
551
- const manifest = normalizeManifest(readYamlObject(manifestPath), src);
552
- if (!manifest) return { ok: false, error: `Invalid node package manifest: ${manifestPath}` };
553
- const dest = path.join(workspacePackageRoot(workspaceRoot), "nodes", manifest.id, manifest.version);
605
+ // 走和读取同一条路径:`index.mjs` 的静态声明优先,回落 `node.yaml`。只认后者的话,
606
+ // 一个目录扫描、面板、运行时都跑得通的 index.mjs 包偏偏发布不出去。
607
+ const inspected = inspectNodePackageDirectory(src, { allowLegacyManifest: true });
608
+ if (!inspected.ok) return inspected;
609
+ const manifest = normalizeManifest(inspected.manifest, src);
610
+ if (!manifest) return { ok: false, error: `Invalid node package manifest: ${src}` };
611
+ const dest = resolveWorkspaceNodePackageDir(workspaceRoot, manifest.id, manifest.version);
612
+ if (!dest) return { ok: false, error: "Invalid marketplace node id or version" };
613
+ const existing = fs.existsSync(dest) ? inspectNodePackageDirectory(dest, { allowLegacyManifest: true }) : null;
614
+ if (existing?.ok && existing.contentSha256 === inspected.contentSha256) {
615
+ return {
616
+ ok: true,
617
+ alreadyExists: true,
618
+ id: manifest.id,
619
+ version: manifest.version,
620
+ packageDir: dest,
621
+ definitionId: manifest.definitionId,
622
+ contentSha256: inspected.contentSha256,
623
+ fileList: inspected.fileList,
624
+ };
625
+ }
626
+ if (existing?.ok && opts.immutable === true) {
627
+ return { ok: false, conflict: true, error: `${manifest.id}@${manifest.version} 已存在且内容不同,请提升版本号` };
628
+ }
554
629
  fs.mkdirSync(path.dirname(dest), { recursive: true });
555
630
  fs.rmSync(dest, { recursive: true, force: true });
556
631
  fs.cpSync(src, dest, { recursive: true });
557
- return { ok: true, id: manifest.id, version: manifest.version, packageDir: dest, definitionId: manifest.definitionId };
632
+ const ownerUserId = String(opts.ownerUserId || opts.userId || manifest.ownerUserId || manifest.createdBy || "").trim();
633
+ fs.writeFileSync(path.join(dest, NODE_PACKAGE_METADATA_FILENAME), `${JSON.stringify({
634
+ ...(ownerUserId ? { ownerUserId, createdBy: ownerUserId } : {}),
635
+ contentSha256: inspected.contentSha256,
636
+ fileList: inspected.fileList,
637
+ fileCount: inspected.fileCount,
638
+ totalBytes: inspected.totalBytes,
639
+ publishedAt: new Date().toISOString(),
640
+ }, null, 2)}\n`, "utf-8");
641
+ return {
642
+ ok: true,
643
+ id: manifest.id,
644
+ version: manifest.version,
645
+ packageDir: dest,
646
+ definitionId: manifest.definitionId,
647
+ contentSha256: inspected.contentSha256,
648
+ fileList: inspected.fileList,
649
+ };
650
+ }
651
+
652
+ export function publishNodePackageArchive(workspaceRoot, archiveInput, opts = {}) {
653
+ const inspected = inspectNodePackageArchive(archiveInput);
654
+ if (!inspected.ok) return inspected;
655
+ const manifest = normalizeManifest(inspected.manifest, "");
656
+ if (!manifest) return { ok: false, error: "Invalid node package manifest" };
657
+ const dest = resolveWorkspaceNodePackageDir(workspaceRoot, manifest.id, manifest.version);
658
+ if (!dest) return { ok: false, error: "Invalid marketplace node id or version" };
659
+ const existing = fs.existsSync(dest) ? inspectNodePackageDirectory(dest) : null;
660
+ if (existing?.ok) {
661
+ if (existing.contentSha256 !== inspected.contentSha256) {
662
+ return { ok: false, conflict: true, error: `${manifest.id}@${manifest.version} 已存在且内容不同,请提升版本号` };
663
+ }
664
+ return {
665
+ ok: true,
666
+ alreadyExists: true,
667
+ id: manifest.id,
668
+ version: manifest.version,
669
+ definitionId: manifest.definitionId,
670
+ packageDir: dest,
671
+ contentSha256: inspected.contentSha256,
672
+ archiveSha256: inspected.archiveSha256,
673
+ fileList: inspected.fileList,
674
+ };
675
+ }
676
+ const parent = path.dirname(dest);
677
+ fs.mkdirSync(parent, { recursive: true });
678
+ // 先在同一父目录完整写好再 rename。这样进程中断或磁盘异常时不会留下一个被目录扫描到、
679
+ // 但脚本只写了一半的版本。
680
+ const staging = fs.mkdtempSync(path.join(parent, `.${manifest.version}.installing-`));
681
+ const ownerUserId = String(opts.ownerUserId || opts.userId || "").trim();
682
+ const installedFrom = String(opts.installedFrom || "").trim();
683
+ const installedAt = String(opts.installedAt || "").trim();
684
+ try {
685
+ const written = writeNodePackageFiles(staging, inspected.files);
686
+ if (!written.ok) return written;
687
+ fs.writeFileSync(path.join(staging, NODE_PACKAGE_METADATA_FILENAME), `${JSON.stringify({
688
+ ...(ownerUserId ? { ownerUserId, createdBy: ownerUserId } : {}),
689
+ contentSha256: inspected.contentSha256,
690
+ archiveSha256: inspected.archiveSha256,
691
+ fileList: inspected.fileList,
692
+ fileCount: inspected.fileCount,
693
+ totalBytes: inspected.totalBytes,
694
+ publishedAt: new Date().toISOString(),
695
+ ...(installedFrom ? { installedFrom } : {}),
696
+ ...(installedAt ? { installedAt } : {}),
697
+ }, null, 2)}\n`, "utf-8");
698
+ fs.renameSync(staging, dest);
699
+ } catch (error) {
700
+ return { ok: false, error: error?.message || String(error) };
701
+ } finally {
702
+ if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true });
703
+ }
704
+ return {
705
+ ok: true,
706
+ id: manifest.id,
707
+ version: manifest.version,
708
+ definitionId: manifest.definitionId,
709
+ packageDir: dest,
710
+ contentSha256: inspected.contentSha256,
711
+ archiveSha256: inspected.archiveSha256,
712
+ fileList: inspected.fileList,
713
+ };
714
+ }
715
+
716
+ export function nodePackageArchive(workspaceRoot, id, version, opts = {}) {
717
+ const packageDir = resolveWorkspaceNodePackageDir(workspaceRoot, id, version);
718
+ if (!packageDir || !fs.existsSync(packageDir)) return { ok: false, error: `Node package not found: ${id}@${version}` };
719
+ const manifest = normalizeManifest(readNodeManifestRaw(packageDir), packageDir, "marketplace");
720
+ if (!manifest || !canAccessMarketplaceNode(manifest, opts)) return { ok: false, error: "Node package permission denied" };
721
+ return createNodePackageArchive(packageDir);
558
722
  }
559
723
 
560
724
  function safePackageId(raw) {
@@ -907,22 +1071,3 @@ export function publishFlowSnippet(workspaceRoot, payload = {}, opts = {}) {
907
1071
  );
908
1072
  return { ok: true, id, version, packageDir: dest, snippet: manifest.snippet };
909
1073
  }
910
-
911
- export function installFlowDependency(workspaceRoot, flowDir, spec, opts = {}) {
912
- const parsed = parseMarketplaceDefinitionId(spec.startsWith("marketplace:") ? spec : `marketplace:${spec}`);
913
- if (!parsed) return { ok: false, error: `Invalid marketplace node spec: ${spec}` };
914
- const resolved = resolveMarketplaceNodePackage(workspaceRoot, flowDir, `marketplace:${parsed.id}${parsed.version ? `@${parsed.version}` : ""}`, { dependencies: {} }, opts);
915
- if (!resolved) return { ok: false, error: `Marketplace node not found: ${spec}` };
916
-
917
- const flowYamlPath = path.join(flowDir, "flow.yaml");
918
- const data = readYamlObject(flowYamlPath);
919
- if (!data) return { ok: false, error: `Invalid flow.yaml: ${flowYamlPath}` };
920
- const deps = data.dependencies && typeof data.dependencies === "object" ? data.dependencies : {};
921
- const nodes = Array.isArray(deps.nodes) ? deps.nodes : [];
922
- const exists = nodes.some((item) => (typeof item === "string" ? item === resolved.id : item && item.id === resolved.id));
923
- if (!exists) nodes.push({ id: resolved.id, version: resolved.version });
924
- data.dependencies = { ...deps, nodes };
925
- fs.writeFileSync(flowYamlPath, yaml.dump(data, { lineWidth: -1 }), "utf-8");
926
- writeFlowMarketplaceLock(workspaceRoot, flowDir, data, opts);
927
- return { ok: true, id: resolved.id, version: resolved.version, definitionId: `marketplace:${resolved.id}@${resolved.version}` };
928
- }
@@ -150,3 +150,13 @@ export function resolveCliAndModel(workspaceRoot, nodeModel, agentModelOverride)
150
150
  label: model === "Auto" ? "cursor: Auto" : `cursor: ${model}`,
151
151
  };
152
152
  }
153
+
154
+ /** 解析 `api:<provider>/<model>` 形式的模型串;无 provider 时默认 openai。 */
155
+ export function parseApiModel(str) {
156
+ const s = String(str ?? "").replace(/^api:/, "");
157
+ const slash = s.indexOf("/");
158
+ if (slash < 0) return { provider: "openai", model: s };
159
+ const provider = s.slice(0, slash).toLowerCase();
160
+ const model = s.slice(slash + 1);
161
+ return { provider, model };
162
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Portable node-package archives.
3
+ *
4
+ * On disk a node package is a directory. ZIP is only the transport form. The
5
+ * archive always has `index.mjs` at its root and may contain relative scripts,
6
+ * templates, and assets. We validate paths and size limits before either
7
+ * packing or installing so an uploaded package cannot escape its destination.
8
+ */
9
+ import crypto from "node:crypto";
10
+ import fs from "node:fs";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+
14
+ import { unzipSync, zipSync } from "fflate";
15
+ import yaml from "js-yaml";
16
+
17
+ import {
18
+ NODE_PACKAGE_ENTRY,
19
+ nodePackageExportsRun,
20
+ readNodePackageManifest,
21
+ } from "./node-package-manifest.mjs";
22
+
23
+ export const NODE_PACKAGE_MAX_UNCOMPRESSED_BYTES = 8 * 1024 * 1024;
24
+ export const NODE_PACKAGE_MAX_FILE_ENTRIES = 500;
25
+ export const NODE_PACKAGE_MAX_ARCHIVE_BYTES = 10 * 1024 * 1024;
26
+ export const NODE_PACKAGE_METADATA_FILENAME = ".agentflow-package.json";
27
+
28
+ const FORBIDDEN_SEGMENTS = new Set([
29
+ ".git",
30
+ ".ssh",
31
+ "__MACOSX",
32
+ "node_modules",
33
+ ]);
34
+ const FORBIDDEN_FILES = new Set([
35
+ ".env",
36
+ ".env.local",
37
+ ".npmrc",
38
+ ".DS_Store",
39
+ NODE_PACKAGE_METADATA_FILENAME,
40
+ "id_rsa",
41
+ "id_ed25519",
42
+ ]);
43
+
44
+ export function sanitizeNodePackageEntryPath(raw) {
45
+ if (typeof raw !== "string") return null;
46
+ // ZIP 规范只使用 `/`。不能把绝对路径或反斜杠悄悄“修正”为相对路径,否则两个不同的
47
+ // archive entry 可能在安装时落到同一个文件,路径穿越也会从拒绝变成接受。
48
+ const value = raw.replace(/^\uFEFF/, "");
49
+ if (!value || value.includes("\0") || value.includes("\\") || value.startsWith("/") || /^[A-Za-z]:/.test(value)) return null;
50
+ const parts = value.split("/").filter(Boolean);
51
+ if (!parts.length || parts.some((part) => part === "." || part === "..")) return null;
52
+ if (parts.some((part) => FORBIDDEN_SEGMENTS.has(part))) return null;
53
+ const base = parts.at(-1);
54
+ if (FORBIDDEN_FILES.has(base) || /^\.env\./.test(base) || /\.(?:pem|key|p12|pfx)$/i.test(base)) return null;
55
+ if (parts.some((part) => part.startsWith("._"))) return null;
56
+ return parts.join("/");
57
+ }
58
+
59
+ function stableContentHash(files) {
60
+ const hash = crypto.createHash("sha256");
61
+ for (const [rel, content] of [...files.entries()].sort(([a], [b]) => a.localeCompare(b))) {
62
+ hash.update(rel);
63
+ hash.update("\0");
64
+ hash.update(content);
65
+ hash.update("\0");
66
+ }
67
+ return hash.digest("hex");
68
+ }
69
+
70
+ function validateFilesMap(files, opts = {}) {
71
+ if (!(files instanceof Map) || files.size === 0) {
72
+ return { ok: false, error: "节点包内没有文件" };
73
+ }
74
+ if (files.size > NODE_PACKAGE_MAX_FILE_ENTRIES) {
75
+ return { ok: false, error: `节点包文件数量超过限制(最多 ${NODE_PACKAGE_MAX_FILE_ENTRIES} 个)` };
76
+ }
77
+ let totalBytes = 0;
78
+ for (const [rel, content] of files) {
79
+ const safe = sanitizeNodePackageEntryPath(rel);
80
+ if (!safe || safe !== rel) return { ok: false, error: `节点包包含非法或敏感路径:${rel}` };
81
+ totalBytes += content.length;
82
+ if (totalBytes > NODE_PACKAGE_MAX_UNCOMPRESSED_BYTES) {
83
+ return { ok: false, error: "节点包解压后总大小超过限制(8MB)" };
84
+ }
85
+ }
86
+ if (!files.has(NODE_PACKAGE_ENTRY) && !(opts.allowLegacyManifest && files.has("node.yaml"))) {
87
+ return { ok: false, error: `节点包根目录缺少 ${NODE_PACKAGE_ENTRY}(旧包可使用 node.yaml)` };
88
+ }
89
+ return { ok: true, totalBytes };
90
+ }
91
+
92
+ function collectDirectoryFiles(packageDir) {
93
+ const root = path.resolve(packageDir);
94
+ const files = new Map();
95
+ const walk = (dir) => {
96
+ const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
97
+ for (const entry of entries) {
98
+ if (entry.name === NODE_PACKAGE_METADATA_FILENAME) continue;
99
+ const abs = path.join(dir, entry.name);
100
+ const stat = fs.lstatSync(abs);
101
+ if (stat.isSymbolicLink()) throw new Error(`节点包不允许符号链接:${path.relative(root, abs)}`);
102
+ const rel = path.relative(root, abs).replace(/\\/g, "/");
103
+ const safe = sanitizeNodePackageEntryPath(rel);
104
+ if (!safe || safe !== rel) throw new Error(`节点包包含非法或敏感路径:${rel}`);
105
+ if (stat.isDirectory()) {
106
+ walk(abs);
107
+ } else if (stat.isFile()) {
108
+ files.set(rel, fs.readFileSync(abs));
109
+ }
110
+ }
111
+ };
112
+ walk(root);
113
+ return files;
114
+ }
115
+
116
+ function manifestFromTemporaryFiles(files, opts = {}) {
117
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "agentflow-node-package-"));
118
+ try {
119
+ for (const [rel, content] of files) {
120
+ const abs = path.join(tempRoot, ...rel.split("/"));
121
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
122
+ fs.writeFileSync(abs, content);
123
+ }
124
+ const manifest = readNodePackageManifest(tempRoot, (yamlPath) => {
125
+ if (!opts.allowLegacyManifest || !fs.existsSync(yamlPath)) return null;
126
+ const parsed = yaml.load(fs.readFileSync(yamlPath, "utf-8"));
127
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
128
+ });
129
+ if (!manifest) return { ok: false, error: `${NODE_PACKAGE_ENTRY} 缺少有效的 export default 声明,node.yaml 也无有效清单` };
130
+ if (files.has(NODE_PACKAGE_ENTRY) && !nodePackageExportsRun(path.join(tempRoot, NODE_PACKAGE_ENTRY))) {
131
+ return { ok: false, error: `${NODE_PACKAGE_ENTRY} 缺少 export function run` };
132
+ }
133
+ return { ok: true, manifest };
134
+ } catch (error) {
135
+ return { ok: false, error: error?.message || String(error) };
136
+ } finally {
137
+ fs.rmSync(tempRoot, { recursive: true, force: true });
138
+ }
139
+ }
140
+
141
+ export function inspectNodePackageDirectory(packageDir, opts = {}) {
142
+ try {
143
+ const root = path.resolve(packageDir);
144
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
145
+ return { ok: false, error: `节点包目录不存在:${root}` };
146
+ }
147
+ const files = collectDirectoryFiles(root);
148
+ const checked = validateFilesMap(files, opts);
149
+ if (!checked.ok) return checked;
150
+ const parsed = manifestFromTemporaryFiles(files, opts);
151
+ if (!parsed.ok) return parsed;
152
+ return {
153
+ ok: true,
154
+ packageDir: root,
155
+ manifest: parsed.manifest,
156
+ files,
157
+ fileList: [...files.keys()].sort(),
158
+ fileCount: files.size,
159
+ totalBytes: checked.totalBytes,
160
+ contentSha256: stableContentHash(files),
161
+ };
162
+ } catch (error) {
163
+ return { ok: false, error: error?.message || String(error) };
164
+ }
165
+ }
166
+
167
+ export function createNodePackageArchive(packageDir) {
168
+ const inspected = inspectNodePackageDirectory(packageDir);
169
+ if (!inspected.ok) return inspected;
170
+ const entries = Object.fromEntries([...inspected.files].map(([rel, content]) => [rel, new Uint8Array(content)]));
171
+ const archive = Buffer.from(zipSync(entries, { level: 6 }));
172
+ if (archive.length > NODE_PACKAGE_MAX_ARCHIVE_BYTES) {
173
+ return { ok: false, error: "节点包 ZIP 超过限制(10MB)" };
174
+ }
175
+ return {
176
+ ...inspected,
177
+ archive,
178
+ archiveSha256: crypto.createHash("sha256").update(archive).digest("hex"),
179
+ };
180
+ }
181
+
182
+ function stripOptionalRoot(files) {
183
+ const roots = new Set([...files.keys()].map((rel) => rel.split("/")[0]));
184
+ if (roots.size !== 1 || files.has(NODE_PACKAGE_ENTRY)) return files;
185
+ const [root] = [...roots];
186
+ const prefix = `${root}/`;
187
+ if (!files.has(`${prefix}${NODE_PACKAGE_ENTRY}`)) return files;
188
+ return new Map([...files].map(([rel, content]) => [rel.slice(prefix.length), content]));
189
+ }
190
+
191
+ export function inspectNodePackageArchive(input) {
192
+ try {
193
+ const archive = Buffer.from(input || []);
194
+ if (!archive.length) return { ok: false, error: "节点包 ZIP 为空" };
195
+ if (archive.length > NODE_PACKAGE_MAX_ARCHIVE_BYTES) return { ok: false, error: "节点包 ZIP 超过限制(10MB)" };
196
+ const raw = unzipSync(new Uint8Array(archive));
197
+ let files = new Map();
198
+ for (const [rawRel, value] of Object.entries(raw)) {
199
+ if (rawRel.endsWith("/")) continue;
200
+ const rel = sanitizeNodePackageEntryPath(rawRel);
201
+ if (!rel) return { ok: false, error: `节点包包含非法或敏感路径:${rawRel}` };
202
+ if (files.has(rel)) return { ok: false, error: `节点包包含重复路径:${rawRel}` };
203
+ files.set(rel, Buffer.from(value));
204
+ }
205
+ files = stripOptionalRoot(files);
206
+ const checked = validateFilesMap(files);
207
+ if (!checked.ok) return checked;
208
+ const parsed = manifestFromTemporaryFiles(files);
209
+ if (!parsed.ok) return parsed;
210
+ return {
211
+ ok: true,
212
+ manifest: parsed.manifest,
213
+ files,
214
+ fileList: [...files.keys()].sort(),
215
+ fileCount: files.size,
216
+ totalBytes: checked.totalBytes,
217
+ contentSha256: stableContentHash(files),
218
+ archiveSha256: crypto.createHash("sha256").update(archive).digest("hex"),
219
+ archive,
220
+ };
221
+ } catch (error) {
222
+ return { ok: false, error: `节点包 ZIP 解析失败:${error?.message || String(error)}` };
223
+ }
224
+ }
225
+
226
+ export function writeNodePackageFiles(targetDir, files) {
227
+ const target = path.resolve(targetDir);
228
+ const checked = validateFilesMap(files);
229
+ if (!checked.ok) return checked;
230
+ fs.mkdirSync(target, { recursive: true });
231
+ for (const [rel, content] of files) {
232
+ const abs = path.resolve(target, ...rel.split("/"));
233
+ if (abs !== target && !abs.startsWith(`${target}${path.sep}`)) {
234
+ return { ok: false, error: `节点包路径越界:${rel}` };
235
+ }
236
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
237
+ fs.writeFileSync(abs, content);
238
+ }
239
+ return { ok: true, totalBytes: checked.totalBytes };
240
+ }