@fieldwangai/agentflow 0.1.28 → 0.1.30

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 (63) hide show
  1. package/agents/agentflow-node-executor-code.md +3 -2
  2. package/agents/agentflow-node-executor-planning.md +3 -2
  3. package/agents/agentflow-node-executor-requirement.md +3 -2
  4. package/agents/agentflow-node-executor-test.md +3 -2
  5. package/agents/agentflow-node-executor-ui.md +3 -2
  6. package/agents/agentflow-node-executor.md +3 -2
  7. package/agents/en/agentflow-node-executor.md +3 -2
  8. package/agents/zh/agentflow-node-executor.md +3 -2
  9. package/bin/lib/agent-runners.mjs +38 -13
  10. package/bin/lib/api-runner.mjs +6 -3
  11. package/bin/lib/auth.mjs +240 -0
  12. package/bin/lib/catalog-agents.mjs +2 -2
  13. package/bin/lib/catalog-flows.mjs +192 -16
  14. package/bin/lib/composer-agent.mjs +21 -1
  15. package/bin/lib/composer-skill-router.mjs +10 -78
  16. package/bin/lib/flow-import.mjs +2 -2
  17. package/bin/lib/flow-write.mjs +20 -20
  18. package/bin/lib/help.mjs +2 -2
  19. package/bin/lib/locales/en.json +25 -1
  20. package/bin/lib/locales/zh.json +25 -1
  21. package/bin/lib/main.mjs +6 -1
  22. package/bin/lib/node-exec-context.mjs +5 -5
  23. package/bin/lib/node-execute.mjs +14 -9
  24. package/bin/lib/paths.mjs +64 -13
  25. package/bin/lib/recent-runs.mjs +2 -2
  26. package/bin/lib/run-node-statuses-from-disk.mjs +3 -3
  27. package/bin/lib/runtime-context.mjs +225 -0
  28. package/bin/lib/scheduler.mjs +41 -38
  29. package/bin/lib/skill-registry.mjs +145 -0
  30. package/bin/lib/ui-server.mjs +902 -57
  31. package/bin/lib/workspace-tree.mjs +4 -3
  32. package/bin/lib/workspace.mjs +9 -11
  33. package/bin/pipeline/build-node-prompt.mjs +29 -4
  34. package/bin/pipeline/get-exec-id.mjs +2 -2
  35. package/bin/pipeline/get-resolved-values.mjs +1 -0
  36. package/bin/pipeline/pre-process-node.mjs +306 -6
  37. package/bin/pipeline/validate-flow.mjs +2 -0
  38. package/builtin/nodes/agent_subAgent.md +7 -1
  39. package/builtin/nodes/control_cd_workspace.md +43 -0
  40. package/builtin/nodes/control_load_skills.md +48 -0
  41. package/builtin/nodes/display_ascii.md +17 -0
  42. package/builtin/nodes/display_markdown.md +17 -0
  43. package/builtin/nodes/display_mermaid.md +17 -0
  44. package/builtin/nodes/tool_git_checkout.md +54 -0
  45. package/builtin/nodes/tool_nodejs.md +8 -1
  46. package/builtin/nodes/tool_print.md +4 -1
  47. package/builtin/web-ui/dist/assets/index-NdVOJLL9.js +196 -0
  48. package/builtin/web-ui/dist/assets/index-naVI6LZj.css +1 -0
  49. package/builtin/web-ui/dist/index.html +2 -2
  50. package/package.json +2 -1
  51. package/skills/agentflow-flow-add-instances/SKILL.md +257 -0
  52. package/skills/agentflow-flow-edit-node-fields/SKILL.md +79 -0
  53. package/skills/agentflow-flow-recipes/SKILL.md +24 -0
  54. package/skills/agentflow-flow-recipes/references/recipes.md +63 -0
  55. package/skills/agentflow-flow-sync-ui/SKILL.md +59 -0
  56. package/skills/agentflow-node-reference/SKILL.md +25 -0
  57. package/skills/agentflow-node-reference/references/builtin-nodes.md +210 -0
  58. package/skills/agentflow-placeholder-reference/SKILL.md +24 -0
  59. package/skills/agentflow-placeholder-reference/references/placeholders.md +20 -0
  60. package/skills/agentflow-runtime-reference/SKILL.md +25 -0
  61. package/skills/agentflow-runtime-reference/references/runtime.md +64 -0
  62. package/builtin/web-ui/dist/assets/index-BeUBxIj1.js +0 -190
  63. package/builtin/web-ui/dist/assets/index-BzhdjOzb.css +0 -1
@@ -11,7 +11,8 @@ readonly: true
11
11
 
12
12
  执行时**只引用本节的变量**,勿自行推导或拼接路径:
13
13
 
14
- - workspaceRoot:${workspaceRoot}(工作区根目录)
14
+ - workspaceRoot:${workspaceRoot}(当前执行工作区根目录,可能由 CD Workspace 节点切换)
15
+ - pipelineWorkspace:${pipelineWorkspace}(流水线所在工作区,写 AgentFlow 结果时使用)
15
16
  - flowName:${flowName}
16
17
  - uuid:${uuid}
17
18
  - instanceId:${instanceId}
@@ -28,5 +29,5 @@ ${taskBody}
28
29
 
29
30
  以代码实现为核心:理清需求与接口,编写或修改代码,确保可运行、通过类型检查与项目规范,节点中如有写入文件的操作可以执行。任务完成后直接退出,结果由系统自动标记成功。**仅当任务明确失败时**,执行以下命令报告失败(`agentflow` 是可直接在终端运行的 CLI 命令):
30
31
  ```bash
31
- agentflow apply -ai write-result ${workspaceRoot} ${flowName} ${uuid} ${instanceId} --json '{"status":"failed","message":"失败原因"}'
32
+ agentflow apply -ai write-result ${pipelineWorkspace} ${flowName} ${uuid} ${instanceId} --json '{"status":"failed","message":"失败原因"}'
32
33
  ```
@@ -11,7 +11,8 @@ readonly: true
11
11
 
12
12
  执行时**只引用本节的变量**,勿自行推导或拼接路径:
13
13
 
14
- - workspaceRoot:${workspaceRoot}(工作区根目录)
14
+ - workspaceRoot:${workspaceRoot}(当前执行工作区根目录,可能由 CD Workspace 节点切换)
15
+ - pipelineWorkspace:${pipelineWorkspace}(流水线所在工作区,写 AgentFlow 结果时使用)
15
16
  - flowName:${flowName}
16
17
  - uuid:${uuid}
17
18
  - instanceId:${instanceId}
@@ -28,5 +29,5 @@ ${taskBody}
28
29
 
29
30
  先做规划(目标拆解、步骤与依赖、可选方案),再按规划执行并产出结果,节点中如有写入文件的操作可以执行。任务完成后直接退出,结果由系统自动标记成功。**仅当任务明确失败时**,执行以下命令报告失败(`agentflow` 是可直接在终端运行的 CLI 命令):
30
31
  ```bash
31
- agentflow apply -ai write-result ${workspaceRoot} ${flowName} ${uuid} ${instanceId} --json '{"status":"failed","message":"失败原因"}'
32
+ agentflow apply -ai write-result ${pipelineWorkspace} ${flowName} ${uuid} ${instanceId} --json '{"status":"failed","message":"失败原因"}'
32
33
  ```
@@ -11,7 +11,8 @@ readonly: true
11
11
 
12
12
  执行时**只引用本节的变量**,勿自行推导或拼接路径:
13
13
 
14
- - workspaceRoot:${workspaceRoot}(工作区根目录)
14
+ - workspaceRoot:${workspaceRoot}(当前执行工作区根目录,可能由 CD Workspace 节点切换)
15
+ - pipelineWorkspace:${pipelineWorkspace}(流水线所在工作区,写 AgentFlow 结果时使用)
15
16
  - flowName:${flowName}
16
17
  - uuid:${uuid}
17
18
  - instanceId:${instanceId}
@@ -28,5 +29,5 @@ ${taskBody}
28
29
 
29
30
  侧重需求理解与拆解,按上述任务完成执行,节点中如有写入文件的操作可以执行。任务完成后直接退出,结果由系统自动标记成功。**仅当任务明确失败时**,执行以下命令报告失败(`agentflow` 是可直接在终端运行的 CLI 命令):
30
31
  ```bash
31
- agentflow apply -ai write-result ${workspaceRoot} ${flowName} ${uuid} ${instanceId} --json '{"status":"failed","message":"失败原因"}'
32
+ agentflow apply -ai write-result ${pipelineWorkspace} ${flowName} ${uuid} ${instanceId} --json '{"status":"failed","message":"失败原因"}'
32
33
  ```
@@ -11,7 +11,8 @@ readonly: true
11
11
 
12
12
  执行时**只引用本节的变量**,勿自行推导或拼接路径:
13
13
 
14
- - workspaceRoot:${workspaceRoot}(工作区根目录)
14
+ - workspaceRoot:${workspaceRoot}(当前执行工作区根目录,可能由 CD Workspace 节点切换)
15
+ - pipelineWorkspace:${pipelineWorkspace}(流水线所在工作区,写 AgentFlow 结果时使用)
15
16
  - flowName:${flowName}
16
17
  - uuid:${uuid}
17
18
  - instanceId:${instanceId}
@@ -28,5 +29,5 @@ ${taskBody}
28
29
 
29
30
  侧重测试与验证,执行并产出结果,节点中如有写入文件的操作可以执行。任务完成后直接退出,结果由系统自动标记成功。**仅当任务明确失败时**,执行以下命令报告失败(`agentflow` 是可直接在终端运行的 CLI 命令):
30
31
  ```bash
31
- agentflow apply -ai write-result ${workspaceRoot} ${flowName} ${uuid} ${instanceId} --json '{"status":"failed","message":"失败原因"}'
32
+ agentflow apply -ai write-result ${pipelineWorkspace} ${flowName} ${uuid} ${instanceId} --json '{"status":"failed","message":"失败原因"}'
32
33
  ```
@@ -11,7 +11,8 @@ readonly: true
11
11
 
12
12
  执行时**只引用本节的变量**,勿自行推导或拼接路径:
13
13
 
14
- - workspaceRoot:${workspaceRoot}(工作区根目录)
14
+ - workspaceRoot:${workspaceRoot}(当前执行工作区根目录,可能由 CD Workspace 节点切换)
15
+ - pipelineWorkspace:${pipelineWorkspace}(流水线所在工作区,写 AgentFlow 结果时使用)
15
16
  - flowName:${flowName}
16
17
  - uuid:${uuid}
17
18
  - instanceId:${instanceId}
@@ -28,5 +29,5 @@ ${taskBody}
28
29
 
29
30
  理解设计稿或规格(Figma、标注、描述),实现或调整组件与样式,保证布局、间距、层级、断点与 RTL 等与设计一致;必要时做走查与修正,节点中如有写入文件的操作可以执行。任务完成后直接退出,结果由系统自动标记成功。**仅当任务明确失败时**,执行以下命令报告失败(`agentflow` 是可直接在终端运行的 CLI 命令):
30
31
  ```bash
31
- agentflow apply -ai write-result ${workspaceRoot} ${flowName} ${uuid} ${instanceId} --json '{"status":"failed","message":"失败原因"}'
32
+ agentflow apply -ai write-result ${pipelineWorkspace} ${flowName} ${uuid} ${instanceId} --json '{"status":"failed","message":"失败原因"}'
32
33
  ```
@@ -11,7 +11,8 @@ readonly: true
11
11
 
12
12
  执行时**只引用本节的变量**,勿自行推导或拼接路径:
13
13
 
14
- - workspaceRoot:${workspaceRoot}(工作区根目录)
14
+ - workspaceRoot:${workspaceRoot}(当前执行工作区根目录,可能由 CD Workspace 节点切换)
15
+ - pipelineWorkspace:${pipelineWorkspace}(流水线所在工作区,写 AgentFlow 结果时使用)
15
16
  - flowName:${flowName}
16
17
  - uuid:${uuid}
17
18
  - instanceId:${instanceId}
@@ -28,5 +29,5 @@ ${taskBody}
28
29
 
29
30
  按上述任务完成执行,节点中如有写入文件的操作可以执行。任务完成后直接退出,结果由系统自动标记成功。**仅当任务明确失败时**,执行以下命令报告失败(`agentflow` 是可直接在终端运行的 CLI 命令):
30
31
  ```bash
31
- agentflow apply -ai write-result ${workspaceRoot} ${flowName} ${uuid} ${instanceId} --json '{"status":"failed","message":"失败原因"}'
32
+ agentflow apply -ai write-result ${pipelineWorkspace} ${flowName} ${uuid} ${instanceId} --json '{"status":"failed","message":"失败原因"}'
32
33
  ```
@@ -11,7 +11,8 @@ You are a flow node executor. Complete the work described in the node context an
11
11
 
12
12
  **Only reference the variables in this section** during execution. Do not derive or concatenate paths on your own:
13
13
 
14
- - workspaceRoot: ${workspaceRoot} (workspace root directory)
14
+ - workspaceRoot: ${workspaceRoot} (current execution workspace root; may be switched by a CD Workspace node)
15
+ - pipelineWorkspace: ${pipelineWorkspace} (pipeline workspace; use this when writing AgentFlow results)
15
16
  - flowName: ${flowName}
16
17
  - uuid: ${uuid}
17
18
  - instanceId: ${instanceId}
@@ -28,5 +29,5 @@ ${taskBody}
28
29
 
29
30
  Complete the task as described above. If the node involves file writing operations, they can be executed. Exit when done — the system automatically marks the result as success. **Only if the task explicitly fails**, run the following command to report failure (`agentflow` is a CLI command available directly in the terminal):
30
31
  ```bash
31
- agentflow apply -ai write-result ${workspaceRoot} ${flowName} ${uuid} ${instanceId} --json '{"status":"failed","message":"reason for failure"}'
32
+ agentflow apply -ai write-result ${pipelineWorkspace} ${flowName} ${uuid} ${instanceId} --json '{"status":"failed","message":"reason for failure"}'
32
33
  ```
@@ -11,7 +11,8 @@ readonly: true
11
11
 
12
12
  执行时**只引用本节的变量**,勿自行推导或拼接路径:
13
13
 
14
- - workspaceRoot:${workspaceRoot}(工作区根目录)
14
+ - workspaceRoot:${workspaceRoot}(当前执行工作区根目录,可能由 CD Workspace 节点切换)
15
+ - pipelineWorkspace:${pipelineWorkspace}(流水线所在工作区,写 AgentFlow 结果时使用)
15
16
  - flowName:${flowName}
16
17
  - uuid:${uuid}
17
18
  - instanceId:${instanceId}
@@ -28,5 +29,5 @@ ${taskBody}
28
29
 
29
30
  按上述任务完成执行,节点中如有写入文件的操作可以执行。任务完成后直接退出,结果由系统自动标记成功。**仅当任务明确失败时**,执行以下命令报告失败(`agentflow` 是可直接在终端运行的 CLI 命令):
30
31
  ```bash
31
- agentflow apply -ai write-result ${workspaceRoot} ${flowName} ${uuid} ${instanceId} --json '{"status":"failed","message":"失败原因"}'
32
+ agentflow apply -ai write-result ${pipelineWorkspace} ${flowName} ${uuid} ${instanceId} --json '{"status":"failed","message":"失败原因"}'
32
33
  ```
@@ -10,6 +10,16 @@ import { appendRunLogLine } from "./run-events.mjs";
10
10
  import { writeWithPrefix } from "./terminal.mjs";
11
11
  import { t } from "./i18n.mjs";
12
12
 
13
+ function shouldPassCursorModelArg(model) {
14
+ const text = String(model || "").trim();
15
+ return text !== "" && !/^auto$/i.test(text);
16
+ }
17
+
18
+ function childEnv(options = {}, extra = {}) {
19
+ const optEnv = options && options.env && typeof options.env === "object" ? options.env : {};
20
+ return { ...process.env, ...optEnv, ...extra };
21
+ }
22
+
13
23
  /**
14
24
  * Run Cursor CLI with stream-json, forward events to stdout, return success/failure.
15
25
  */
@@ -25,8 +35,11 @@ export function runCursorAgentForNode(
25
35
  const outputDir = instanceId ? path.join(absRunDir, "output", instanceId) : path.join(absRunDir, "output");
26
36
  if (instanceId) fs.mkdirSync(outputDir, { recursive: true });
27
37
  const absWorkspaceRoot = path.resolve(workspaceRoot);
38
+ const execWorkspaceRoot = path.resolve(options.execWorkspaceRoot || workspaceRoot);
28
39
  const replacements = {
29
- workspaceRoot: absWorkspaceRoot,
40
+ workspaceRoot: execWorkspaceRoot,
41
+ executionWorkspaceRoot: execWorkspaceRoot,
42
+ pipelineWorkspace: absWorkspaceRoot,
30
43
  promptPath: absPromptPath,
31
44
  nodeContext: nodeContext ?? "",
32
45
  taskBody: taskBody ?? "",
@@ -61,11 +74,11 @@ export function runCursorAgentForNode(
61
74
 
62
75
  return new Promise((resolve, reject) => {
63
76
  const agentCmd = process.env.CURSOR_AGENT_CMD || "agent";
64
- const args = ["--print", "--output-format", "stream-json", "--trust", "--workspace", workspaceRoot];
77
+ const args = ["--print", "--output-format", "stream-json", "--trust", "--workspace", execWorkspaceRoot];
65
78
  const approveMcps = process.env.AGENTFLOW_CURSOR_APPROVE_MCPS !== "0" && process.env.AGENTFLOW_CURSOR_APPROVE_MCPS !== "false";
66
79
  if (approveMcps) args.push("--approve-mcps");
67
80
  if (options.force) args.push("--force");
68
- args.push("--model", model);
81
+ if (shouldPassCursorModelArg(model)) args.push("--model", model);
69
82
  args.push(promptText);
70
83
  if (options.flowName && options.uuid) {
71
84
  const argvLog = args.slice(0, -1).concat([`(prompt ${args[args.length - 1].length} chars)`]);
@@ -81,9 +94,10 @@ export function runCursorAgentForNode(
81
94
  }
82
95
  const useStderrInherit = process.env.AGENTFLOW_CURSOR_STDERR_INHERIT === "1" || process.env.AGENTFLOW_CURSOR_STDERR_INHERIT === "true";
83
96
  const child = spawn(agentCmd, args, {
84
- cwd: workspaceRoot,
97
+ cwd: execWorkspaceRoot,
85
98
  stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
86
99
  shell: false,
100
+ env: childEnv(options),
87
101
  });
88
102
 
89
103
  let lastResult = null;
@@ -286,8 +300,11 @@ export function runOpenCodeAgentForNode(
286
300
  const outputDir = instanceId ? path.join(absRunDir, "output", instanceId) : path.join(absRunDir, "output");
287
301
  if (instanceId) fs.mkdirSync(outputDir, { recursive: true });
288
302
  const absWorkspaceRoot = path.resolve(workspaceRoot);
303
+ const execWorkspaceRoot = path.resolve(options.execWorkspaceRoot || workspaceRoot);
289
304
  const replacements = {
290
- workspaceRoot: absWorkspaceRoot,
305
+ workspaceRoot: execWorkspaceRoot,
306
+ executionWorkspaceRoot: execWorkspaceRoot,
307
+ pipelineWorkspace: absWorkspaceRoot,
291
308
  promptPath: absPromptPath,
292
309
  nodeContext: nodeContext ?? "",
293
310
  taskBody: taskBody ?? "",
@@ -325,16 +342,17 @@ export function runOpenCodeAgentForNode(
325
342
  if (model) {
326
343
  args.push("--model", model);
327
344
  }
328
- args.push("--dir", workspaceRoot);
345
+ args.push("--dir", execWorkspaceRoot);
329
346
  args.push("--", promptText);
330
347
  const spawnOpts = {
331
- cwd: workspaceRoot,
348
+ cwd: execWorkspaceRoot,
332
349
  stdio: ["ignore", "pipe", "pipe"],
333
350
  shell: false,
351
+ env: childEnv(options),
334
352
  };
335
353
  if (options.force) {
336
354
  spawnOpts.env = {
337
- ...process.env,
355
+ ...spawnOpts.env,
338
356
  OPENCODE_CONFIG_CONTENT: JSON.stringify({
339
357
  permission: { external_directory: "allow" },
340
358
  }),
@@ -426,8 +444,11 @@ export function runClaudeCodeAgentForNode(
426
444
  const outputDir = instanceId ? path.join(absRunDir, "output", instanceId) : path.join(absRunDir, "output");
427
445
  if (instanceId) fs.mkdirSync(outputDir, { recursive: true });
428
446
  const absWorkspaceRoot = path.resolve(workspaceRoot);
447
+ const execWorkspaceRoot = path.resolve(options.execWorkspaceRoot || workspaceRoot);
429
448
  const replacements = {
430
- workspaceRoot: absWorkspaceRoot,
449
+ workspaceRoot: execWorkspaceRoot,
450
+ executionWorkspaceRoot: execWorkspaceRoot,
451
+ pipelineWorkspace: absWorkspaceRoot,
431
452
  promptPath: absPromptPath,
432
453
  nodeContext: nodeContext ?? "",
433
454
  taskBody: taskBody ?? "",
@@ -464,7 +485,7 @@ export function runClaudeCodeAgentForNode(
464
485
  const bypassPermissions =
465
486
  process.env.AGENTFLOW_CLAUDE_CODE_BYPASS_PERMISSIONS !== "0" &&
466
487
  process.env.AGENTFLOW_CLAUDE_CODE_BYPASS_PERMISSIONS !== "false";
467
- const args = ["-p", "--output-format", "stream-json", "--verbose", "--add-dir", workspaceRoot];
488
+ const args = ["-p", "--output-format", "stream-json", "--verbose", "--add-dir", execWorkspaceRoot, "--add-dir", absWorkspaceRoot];
468
489
  if (bypassPermissions) args.push("--dangerously-skip-permissions");
469
490
  if (model) args.push("--model", model);
470
491
  args.push(promptText);
@@ -490,9 +511,10 @@ export function runClaudeCodeAgentForNode(
490
511
  process.env.AGENTFLOW_CLAUDE_CODE_STDERR_INHERIT === "1" ||
491
512
  process.env.AGENTFLOW_CLAUDE_CODE_STDERR_INHERIT === "true";
492
513
  const child = spawn(claudeCmd, args, {
493
- cwd: workspaceRoot,
514
+ cwd: execWorkspaceRoot,
494
515
  stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
495
516
  shell: false,
517
+ env: childEnv(options),
496
518
  });
497
519
 
498
520
  let lastResult = null;
@@ -743,7 +765,7 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
743
765
  const approveMcps = process.env.AGENTFLOW_CURSOR_APPROVE_MCPS !== "0" && process.env.AGENTFLOW_CURSOR_APPROVE_MCPS !== "false";
744
766
  if (approveMcps) args.push("--approve-mcps");
745
767
  args.push("--force");
746
- args.push("--model", model);
768
+ if (shouldPassCursorModelArg(model)) args.push("--model", model);
747
769
  args.push(promptText);
748
770
 
749
771
  const useStderrInherit = process.env.AGENTFLOW_CURSOR_STDERR_INHERIT === "1" || process.env.AGENTFLOW_CURSOR_STDERR_INHERIT === "true";
@@ -751,6 +773,7 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
751
773
  cwd: ws,
752
774
  stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
753
775
  shell: false,
776
+ env: childEnv(options),
754
777
  });
755
778
 
756
779
  let lastResult = null;
@@ -939,10 +962,11 @@ export function runOpenCodeAgentWithPrompt(cliWorkspace, promptText, options = {
939
962
  cwd: ws,
940
963
  stdio: ["ignore", "pipe", "pipe"],
941
964
  shell: false,
965
+ env: childEnv(options),
942
966
  };
943
967
  if (options.force) {
944
968
  spawnOpts.env = {
945
- ...process.env,
969
+ ...spawnOpts.env,
946
970
  OPENCODE_CONFIG_CONTENT: JSON.stringify({
947
971
  permission: { external_directory: "allow" },
948
972
  }),
@@ -1047,6 +1071,7 @@ export function runClaudeCodeAgentWithPrompt(cliWorkspace, promptText, options =
1047
1071
  cwd: ws,
1048
1072
  stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
1049
1073
  shell: false,
1074
+ env: childEnv(options),
1050
1075
  });
1051
1076
 
1052
1077
  let lastResult = null;
@@ -315,6 +315,7 @@ export function parseApiModel(str) {
315
315
  */
316
316
  export async function runApiAgentForNode(workspaceRoot, { promptPath, nodeContext, taskBody, subagent, instanceId }, options = {}) {
317
317
  const absRoot = path.resolve(workspaceRoot);
318
+ const execRoot = path.resolve(options.execWorkspaceRoot || workspaceRoot);
318
319
  const flowName = options.flowName ?? null;
319
320
  const uuid = options.uuid ?? null;
320
321
 
@@ -329,7 +330,9 @@ export async function runApiAgentForNode(workspaceRoot, { promptPath, nodeContex
329
330
 
330
331
  // ── 读取 Agent 角色提示,注入 nodeContext/taskBody ────────────────────────
331
332
  const replacements = {
332
- workspaceRoot: absRoot,
333
+ workspaceRoot: execRoot,
334
+ executionWorkspaceRoot: execRoot,
335
+ pipelineWorkspace: absRoot,
333
336
  nodeContext: nodeContext ?? "",
334
337
  taskBody: taskBody ?? "",
335
338
  flowName: flowName ?? "",
@@ -349,12 +352,12 @@ export async function runApiAgentForNode(workspaceRoot, { promptPath, nodeContex
349
352
  if (provider === "anthropic") {
350
353
  const key = process.env.ANTHROPIC_API_KEY;
351
354
  if (!key) throw new Error("[api-runner] ANTHROPIC_API_KEY is required for api:anthropic/* models");
352
- await runAnthropicLoop(key, model, systemPrompt, userContent, absRoot, log, options);
355
+ await runAnthropicLoop(key, model, systemPrompt, userContent, execRoot, log, options);
353
356
  } else {
354
357
  const key = process.env.OPENAI_API_KEY;
355
358
  if (!key) throw new Error("[api-runner] OPENAI_API_KEY is required for api:openai/* models");
356
359
  const baseUrl = (process.env.OPENAI_BASE_URL ?? DEFAULT_OPENAI_BASE).trim();
357
- await runOpenAiLoop(key, baseUrl, model, systemPrompt, userContent, absRoot, log, options);
360
+ await runOpenAiLoop(key, baseUrl, model, systemPrompt, userContent, execRoot, log, options);
358
361
  }
359
362
 
360
363
  log(`done instanceId=${instanceId ?? "-"}`);
@@ -0,0 +1,240 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import {
5
+ ARCHIVED_PIPELINES_DIR_NAME,
6
+ getAgentflowDataRoot,
7
+ getUserPipelinesRoot,
8
+ sanitizeAgentflowUserId,
9
+ } from "./paths.mjs";
10
+
11
+ const SESSION_COOKIE = "af_session";
12
+ const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
13
+
14
+ function authRoot() {
15
+ return path.join(getAgentflowDataRoot(), "auth");
16
+ }
17
+
18
+ function usersPath() {
19
+ return path.join(authRoot(), "users.json");
20
+ }
21
+
22
+ function sessionsPath() {
23
+ return path.join(authRoot(), "sessions.json");
24
+ }
25
+
26
+ function readJsonObject(filePath) {
27
+ try {
28
+ if (!fs.existsSync(filePath)) return {};
29
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
30
+ return data && typeof data === "object" && !Array.isArray(data) ? data : {};
31
+ } catch {
32
+ return {};
33
+ }
34
+ }
35
+
36
+ function writeJsonObject(filePath, data) {
37
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
38
+ fs.writeFileSync(filePath, JSON.stringify(data && typeof data === "object" ? data : {}, null, 2) + "\n", "utf-8");
39
+ }
40
+
41
+ function hashPassword(password, salt = crypto.randomBytes(16).toString("hex")) {
42
+ const hash = crypto.scryptSync(String(password), salt, 64).toString("hex");
43
+ return { salt, hash };
44
+ }
45
+
46
+ function verifyPassword(password, record) {
47
+ if (!record || typeof record.salt !== "string" || typeof record.hash !== "string") return false;
48
+ const next = hashPassword(password, record.salt).hash;
49
+ try {
50
+ return crypto.timingSafeEqual(Buffer.from(next, "hex"), Buffer.from(record.hash, "hex"));
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ function hashToken(token) {
57
+ return crypto.createHash("sha256").update(String(token)).digest("hex");
58
+ }
59
+
60
+ function parseCookies(header) {
61
+ const out = {};
62
+ for (const part of String(header || "").split(";")) {
63
+ const idx = part.indexOf("=");
64
+ if (idx <= 0) continue;
65
+ const key = part.slice(0, idx).trim();
66
+ const value = part.slice(idx + 1).trim();
67
+ if (!key) continue;
68
+ try {
69
+ out[key] = decodeURIComponent(value);
70
+ } catch {
71
+ out[key] = value;
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+
77
+ export function readAuthUsers() {
78
+ return readJsonObject(usersPath());
79
+ }
80
+
81
+ export function authSetupRequired() {
82
+ return Object.keys(readAuthUsers()).length === 0;
83
+ }
84
+
85
+ function listFlowDirs(root) {
86
+ try {
87
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) return [];
88
+ return fs.readdirSync(root, { withFileTypes: true })
89
+ .filter((entry) => entry.isDirectory())
90
+ .filter((entry) => entry.name !== ARCHIVED_PIPELINES_DIR_NAME)
91
+ .filter((entry) => fs.existsSync(path.join(root, entry.name, "flow.yaml")))
92
+ .map((entry) => entry.name)
93
+ .sort((a, b) => a.localeCompare(b));
94
+ } catch {
95
+ return [];
96
+ }
97
+ }
98
+
99
+ function copyMissingFlowDirs(sourceRoot, targetRoot, relativeRoot = "") {
100
+ const fromRoot = path.join(sourceRoot, relativeRoot);
101
+ const toRoot = path.join(targetRoot, relativeRoot);
102
+ const copied = [];
103
+ const skipped = [];
104
+ for (const name of listFlowDirs(fromRoot)) {
105
+ const fromDir = path.join(fromRoot, name);
106
+ const toDir = path.join(toRoot, name);
107
+ if (fs.existsSync(toDir)) {
108
+ skipped.push(path.join(relativeRoot, name).replace(/\\/g, "/"));
109
+ continue;
110
+ }
111
+ fs.mkdirSync(path.dirname(toDir), { recursive: true });
112
+ fs.cpSync(fromDir, toDir, { recursive: true });
113
+ copied.push(path.join(relativeRoot, name).replace(/\\/g, "/"));
114
+ }
115
+ return { copied, skipped };
116
+ }
117
+
118
+ export function migrateLegacyPipelinesToAdminUser(userId) {
119
+ const safeUserId = sanitizeAgentflowUserId(userId);
120
+ if (!safeUserId) return { copied: [], skipped: [], source: "", target: "", error: "invalid userId" };
121
+
122
+ const source = getUserPipelinesRoot("");
123
+ const target = getUserPipelinesRoot(safeUserId);
124
+ if (path.resolve(source) === path.resolve(target)) {
125
+ return { copied: [], skipped: [], source, target };
126
+ }
127
+ if (!fs.existsSync(source)) {
128
+ return { copied: [], skipped: [], source, target };
129
+ }
130
+
131
+ const active = copyMissingFlowDirs(source, target);
132
+ const archived = copyMissingFlowDirs(source, target, ARCHIVED_PIPELINES_DIR_NAME);
133
+ return {
134
+ copied: [...active.copied, ...archived.copied],
135
+ skipped: [...active.skipped, ...archived.skipped],
136
+ source,
137
+ target,
138
+ };
139
+ }
140
+
141
+ export function getSessionCookieName() {
142
+ return SESSION_COOKIE;
143
+ }
144
+
145
+ export function buildSessionCookie(token) {
146
+ const attrs = [
147
+ `${SESSION_COOKIE}=${encodeURIComponent(token)}`,
148
+ "Path=/",
149
+ "HttpOnly",
150
+ "SameSite=Lax",
151
+ `Max-Age=${Math.floor(SESSION_TTL_MS / 1000)}`,
152
+ ];
153
+ return attrs.join("; ");
154
+ }
155
+
156
+ export function buildClearSessionCookie() {
157
+ return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
158
+ }
159
+
160
+ export function getAuthUserFromRequest(req) {
161
+ const token = parseCookies(req.headers.cookie || "")[SESSION_COOKIE];
162
+ if (!token) return null;
163
+ const sessions = readJsonObject(sessionsPath());
164
+ const key = hashToken(token);
165
+ const session = sessions[key];
166
+ if (!session || typeof session.userId !== "string") return null;
167
+ if (Number(session.expiresAt) <= Date.now()) {
168
+ delete sessions[key];
169
+ writeJsonObject(sessionsPath(), sessions);
170
+ return null;
171
+ }
172
+ const users = readAuthUsers();
173
+ const user = users[session.userId];
174
+ if (!user) return null;
175
+ return {
176
+ userId: session.userId,
177
+ username: user.username || session.userId,
178
+ isAdmin: Boolean(user.isAdmin),
179
+ };
180
+ }
181
+
182
+ export function loginOrCreateUser(username, password) {
183
+ const userId = sanitizeAgentflowUserId(username);
184
+ if (!userId) {
185
+ return { ok: false, error: "用户名须以字母开头,仅可使用字母、数字、下划线与连字符,最多 64 字符" };
186
+ }
187
+ const pwd = String(password || "");
188
+ if (pwd.length < 4) return { ok: false, error: "密码至少 4 位" };
189
+
190
+ const users = readAuthUsers();
191
+ const firstUser = Object.keys(users).length === 0;
192
+ let user = users[userId];
193
+ if (!user) {
194
+ const hashed = hashPassword(pwd);
195
+ user = {
196
+ userId,
197
+ username: String(username).trim(),
198
+ salt: hashed.salt,
199
+ hash: hashed.hash,
200
+ isAdmin: firstUser,
201
+ createdAt: new Date().toISOString(),
202
+ };
203
+ users[userId] = user;
204
+ writeJsonObject(usersPath(), users);
205
+ } else if (!verifyPassword(pwd, user)) {
206
+ return { ok: false, error: "用户名或密码错误" };
207
+ }
208
+
209
+ let migration = null;
210
+ if (Boolean(user.isAdmin)) {
211
+ try {
212
+ migration = migrateLegacyPipelinesToAdminUser(userId);
213
+ } catch (e) {
214
+ migration = { copied: [], skipped: [], error: (e && e.message) || String(e) };
215
+ }
216
+ }
217
+
218
+ const token = crypto.randomBytes(32).toString("base64url");
219
+ const sessions = readJsonObject(sessionsPath());
220
+ sessions[hashToken(token)] = {
221
+ userId,
222
+ createdAt: Date.now(),
223
+ expiresAt: Date.now() + SESSION_TTL_MS,
224
+ };
225
+ writeJsonObject(sessionsPath(), sessions);
226
+ return {
227
+ ok: true,
228
+ token,
229
+ user: { userId, username: user.username || userId, isAdmin: Boolean(user.isAdmin) },
230
+ migration,
231
+ };
232
+ }
233
+
234
+ export function logoutRequest(req) {
235
+ const token = parseCookies(req.headers.cookie || "")[SESSION_COOKIE];
236
+ if (!token) return;
237
+ const sessions = readJsonObject(sessionsPath());
238
+ delete sessions[hashToken(token)];
239
+ writeJsonObject(sessionsPath(), sessions);
240
+ }
@@ -278,10 +278,10 @@ description: ${description != null ? String(description).replace(/\n/g, " ") : "
278
278
  }
279
279
  }
280
280
 
281
- export function copyBuiltinJson(workspaceRoot, flowId, targetFlowId) {
281
+ export function copyBuiltinJson(workspaceRoot, flowId, targetFlowId, opts = {}) {
282
282
  const destId = (targetFlowId && targetFlowId.trim()) || flowId;
283
283
  const srcDir = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId);
284
- const pipelinesRoot = getUserPipelinesRoot();
284
+ const pipelinesRoot = getUserPipelinesRoot(opts.userId);
285
285
  const destDir = path.join(pipelinesRoot, destId);
286
286
  if (!fs.existsSync(srcDir) || !fs.existsSync(path.join(srcDir, "flow.yaml"))) {
287
287
  return { success: false, error: t("catalog.builtin_flow_not_found") };