@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
@@ -68,11 +68,11 @@ export function buildEmptyUserFlowYaml(options = {}) {
68
68
  * @param {string} workspaceRoot
69
69
  * @param {FlowWriteSource} source
70
70
  */
71
- function getPipelinesRootByWriteSource(workspaceRoot, source) {
71
+ function getPipelinesRootByWriteSource(workspaceRoot, source, opts = {}) {
72
72
  if (source === "workspace") {
73
73
  return path.join(path.resolve(workspaceRoot), PIPELINES_DIR);
74
74
  }
75
- return getUserPipelinesRoot();
75
+ return getUserPipelinesRoot(opts.userId);
76
76
  }
77
77
 
78
78
  /**
@@ -94,7 +94,7 @@ function resolveExistingWorkspaceFlowDir(workspaceRoot, flowId) {
94
94
  * @param {FlowWriteSource} flowSource
95
95
  * @returns {{ flowDir: string, error?: string }}
96
96
  */
97
- export function resolveFlowDirForWrite(workspaceRoot, flowId, flowSource) {
97
+ export function resolveFlowDirForWrite(workspaceRoot, flowId, flowSource, opts = {}) {
98
98
  if (!workspaceRoot || !flowId) {
99
99
  return { flowDir: "", error: "workspaceRoot and flowId are required" };
100
100
  }
@@ -125,7 +125,7 @@ export function resolveFlowDirForWrite(workspaceRoot, flowId, flowSource) {
125
125
  }
126
126
  }
127
127
 
128
- const pipelinesRoot = getPipelinesRootByWriteSource(workspaceRoot, flowSource);
128
+ const pipelinesRoot = getPipelinesRootByWriteSource(workspaceRoot, flowSource, opts);
129
129
  const flowDir = path.join(pipelinesRoot, flowId);
130
130
  const resolvedFlowDir = path.resolve(flowDir);
131
131
  const baseWithSep = boundariesBase.endsWith(path.sep) ? boundariesBase : boundariesBase + path.sep;
@@ -148,7 +148,7 @@ export function resolveFlowDirForWrite(workspaceRoot, flowId, flowSource) {
148
148
  * @param {FlowWriteSource} flowSource
149
149
  * @returns {{ flowDir: string, error?: string }}
150
150
  */
151
- export function resolveArchivedFlowDirForWrite(workspaceRoot, flowId, flowSource) {
151
+ export function resolveArchivedFlowDirForWrite(workspaceRoot, flowId, flowSource, opts = {}) {
152
152
  if (!workspaceRoot || !flowId) {
153
153
  return { flowDir: "", error: "workspaceRoot and flowId are required" };
154
154
  }
@@ -176,7 +176,7 @@ export function resolveArchivedFlowDirForWrite(workspaceRoot, flowId, flowSource
176
176
  }
177
177
  }
178
178
 
179
- const pipelinesRoot = getPipelinesRootByWriteSource(workspaceRoot, flowSource);
179
+ const pipelinesRoot = getPipelinesRootByWriteSource(workspaceRoot, flowSource, opts);
180
180
  const flowDir = path.join(pipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId);
181
181
  const resolvedFlowDir = path.resolve(flowDir);
182
182
  const baseWithSep = boundariesBase.endsWith(path.sep) ? boundariesBase : boundariesBase + path.sep;
@@ -197,8 +197,8 @@ export function resolveArchivedFlowDirForWrite(workspaceRoot, flowId, flowSource
197
197
  export function writeFlowYaml(workspaceRoot, flowId, flowSource, flowYaml, opts = {}) {
198
198
  const archived = Boolean(opts.archived);
199
199
  const { flowDir, error } = archived
200
- ? resolveArchivedFlowDirForWrite(workspaceRoot, flowId, flowSource)
201
- : resolveFlowDirForWrite(workspaceRoot, flowId, flowSource);
200
+ ? resolveArchivedFlowDirForWrite(workspaceRoot, flowId, flowSource, opts)
201
+ : resolveFlowDirForWrite(workspaceRoot, flowId, flowSource, opts);
202
202
  if (error) return { success: false, error };
203
203
  try {
204
204
  fs.mkdirSync(flowDir, { recursive: true });
@@ -217,11 +217,11 @@ export function writeFlowYaml(workspaceRoot, flowId, flowSource, flowYaml, opts
217
217
  * @param {FlowWriteSource} flowSource
218
218
  * @returns {{ success: true } | { success: false, error: string }}
219
219
  */
220
- export function archiveFlowPipeline(workspaceRoot, flowId, flowSource) {
220
+ export function archiveFlowPipeline(workspaceRoot, flowId, flowSource, opts = {}) {
221
221
  if (flowSource !== "user" && flowSource !== "workspace") {
222
222
  return { success: false, error: "仅支持用户目录或工作区流水线归档" };
223
223
  }
224
- const yamlRes = getFlowYamlAbs(workspaceRoot, flowId, flowSource, { archived: false });
224
+ const yamlRes = getFlowYamlAbs(workspaceRoot, flowId, flowSource, { archived: false, userId: opts.userId });
225
225
  if (yamlRes.error || !yamlRes.path) {
226
226
  return { success: false, error: yamlRes.error || "找不到流水线" };
227
227
  }
@@ -230,7 +230,7 @@ export function archiveFlowPipeline(workspaceRoot, flowId, flowSource) {
230
230
  if (fromDir.split(sep).includes(ARCHIVED_PIPELINES_DIR_NAME)) {
231
231
  return { success: false, error: "该流水线已在归档目录中" };
232
232
  }
233
- const toRes = resolveArchivedFlowDirForWrite(workspaceRoot, flowId, flowSource);
233
+ const toRes = resolveArchivedFlowDirForWrite(workspaceRoot, flowId, flowSource, opts);
234
234
  if (toRes.error || !toRes.flowDir) {
235
235
  return { success: false, error: toRes.error || "无法解析归档路径" };
236
236
  }
@@ -255,7 +255,7 @@ export function archiveFlowPipeline(workspaceRoot, flowId, flowSource) {
255
255
  * @param {"user" | "workspace"} toSource
256
256
  * @returns {{ success: true, flowSource: "user" | "workspace" } | { success: false, error: string }}
257
257
  */
258
- export function moveFlowDirectory(workspaceRoot, flowId, fromSource, toSource) {
258
+ export function moveFlowDirectory(workspaceRoot, flowId, fromSource, toSource, opts = {}) {
259
259
  if (fromSource === toSource) {
260
260
  return { success: false, error: "fromSource and toSource must differ" };
261
261
  }
@@ -267,16 +267,16 @@ export function moveFlowDirectory(workspaceRoot, flowId, fromSource, toSource) {
267
267
  }
268
268
  let fromDir;
269
269
  if (fromSource === "workspace") {
270
- const w = resolveFlowDirForWrite(workspaceRoot, flowId, "workspace");
270
+ const w = resolveFlowDirForWrite(workspaceRoot, flowId, "workspace", opts);
271
271
  if (w.error || !w.flowDir) return { success: false, error: w.error || "invalid source path" };
272
272
  fromDir = resolveExistingWorkspaceFlowDir(workspaceRoot, flowId);
273
273
  if (!fromDir) return { success: false, error: "source flow not found" };
274
274
  } else {
275
- const fromRes = resolveFlowDirForWrite(workspaceRoot, flowId, fromSource);
275
+ const fromRes = resolveFlowDirForWrite(workspaceRoot, flowId, fromSource, opts);
276
276
  if (fromRes.error || !fromRes.flowDir) return { success: false, error: fromRes.error || "invalid source path" };
277
277
  fromDir = fromRes.flowDir;
278
278
  }
279
- const toRes = resolveFlowDirForWrite(workspaceRoot, flowId, toSource);
279
+ const toRes = resolveFlowDirForWrite(workspaceRoot, flowId, toSource, opts);
280
280
  if (toRes.error || !toRes.flowDir) return { success: false, error: toRes.error || "invalid target path" };
281
281
  const toDir = toRes.flowDir;
282
282
  if (!fs.existsSync(path.join(fromDir, FLOW_YAML_FILENAME))) {
@@ -301,7 +301,7 @@ export function moveFlowDirectory(workspaceRoot, flowId, fromSource, toSource) {
301
301
  * @param {string} flowId
302
302
  * @returns {{ ok: true } | { ok: false, error: string }}
303
303
  */
304
- function assertFlowDirIsSafeToDelete(flowDir, workspaceRoot, flowSource, flowId) {
304
+ function assertFlowDirIsSafeToDelete(flowDir, workspaceRoot, flowSource, flowId, opts = {}) {
305
305
  let realDir;
306
306
  try {
307
307
  realDir = fs.realpathSync(flowDir);
@@ -316,9 +316,9 @@ function assertFlowDirIsSafeToDelete(flowDir, workspaceRoot, flowSource, flowId)
316
316
  const allowedRoots = [];
317
317
  if (flowSource === "user") {
318
318
  try {
319
- allowedRoots.push(fs.realpathSync(getUserPipelinesRoot()));
319
+ allowedRoots.push(fs.realpathSync(getUserPipelinesRoot(opts.userId)));
320
320
  } catch {
321
- allowedRoots.push(path.resolve(getUserPipelinesRoot()));
321
+ allowedRoots.push(path.resolve(getUserPipelinesRoot(opts.userId)));
322
322
  }
323
323
  for (const rel of [PIPELINES_DIR, LEGACY_PIPELINES_DIR]) {
324
324
  const base = path.join(root, rel);
@@ -379,12 +379,12 @@ export function deleteFlowPipeline(workspaceRoot, flowId, flowSource, opts = {})
379
379
  return { success: false, error: "invalid flowId" };
380
380
  }
381
381
  const archived = Boolean(opts.archived);
382
- const yamlRes = getFlowYamlAbs(workspaceRoot, flowId, flowSource, { archived });
382
+ const yamlRes = getFlowYamlAbs(workspaceRoot, flowId, flowSource, { archived, userId: opts.userId });
383
383
  if (yamlRes.error || !yamlRes.path) {
384
384
  return { success: false, error: yamlRes.error || "找不到流水线" };
385
385
  }
386
386
  const flowDir = path.dirname(yamlRes.path);
387
- const guard = assertFlowDirIsSafeToDelete(flowDir, workspaceRoot, flowSource, flowId);
387
+ const guard = assertFlowDirIsSafeToDelete(flowDir, workspaceRoot, flowSource, flowId, opts);
388
388
  if (!guard.ok) return { success: false, error: guard.error };
389
389
  try {
390
390
  fs.rmSync(flowDir, { recursive: true, force: true });
package/bin/lib/help.mjs CHANGED
@@ -17,7 +17,7 @@ AgentFlow CLI — 使用 Cursor / OpenCode / Claude Code CLI 流式输出驱动
17
17
  agentflow list-remote [--search <q>] [--sort popular|trending] [--json] 浏览 Hub 上的流程
18
18
  agentflow download <slug|title> [--user|--workspace] [--as <id>] [--raw [--output <dir>]] 从 Hub 下载流程(默认 --user 安装到 ~/agentflow/pipelines/<id>;--workspace 安装到当前工程 .workspace/agentflow/pipelines/<id>;--raw 仅保留压缩包)
19
19
  agentflow list 列出所有流水线
20
- agentflow ui [--host <addr>] [--port <n>] [--scheduler] [--no-open] 本地 HTTP:流水线列表 + React Flow 节点流程图编辑保存(默认 127.0.0.1:8765;可用 AGENTFLOW_UI_HOST)
20
+ agentflow ui [--host <addr>] [--port <n>] [--scheduler] [--no-open] [--hide-community-links] 本地 HTTP:流水线列表 + React Flow 节点流程图编辑保存(默认 127.0.0.1:8765;可用 AGENTFLOW_UI_HOST)
21
21
  agentflow scheduler start [--poll-ms <ms>] 启动定时执行调度器(读取各流水线 schedule.json)
22
22
  agentflow scheduler status [--json] 查看定时执行配置与状态
23
23
  agentflow scheduler cancel <FlowName> <uuid> 取消某次等待中的 watch/run
@@ -86,7 +86,7 @@ Usage:
86
86
  agentflow list-remote [--search <q>] [--sort popular|trending] [--json] Browse flows on Hub
87
87
  agentflow download <slug|title> [--user|--workspace] [--as <id>] [--raw [--output <dir>]] Download flow (default --user → ~/agentflow/pipelines/<id>; --workspace → current project's .workspace/agentflow/pipelines/<id>; --raw keeps the archive)
88
88
  agentflow list List all pipelines
89
- agentflow ui [--host <addr>] [--port <n>] [--scheduler] [--no-open] Local HTTP: pipeline list + React Flow node diagram editor (default 127.0.0.1:8765; AGENTFLOW_UI_HOST supported)
89
+ agentflow ui [--host <addr>] [--port <n>] [--scheduler] [--no-open] [--hide-community-links] Local HTTP: pipeline list + React Flow node diagram editor (default 127.0.0.1:8765; AGENTFLOW_UI_HOST supported)
90
90
  agentflow scheduler start [--poll-ms <ms>] Start the scheduled-run scheduler (reads each pipeline schedule.json)
91
91
  agentflow scheduler status [--json] Show scheduled-run configuration and state
92
92
  agentflow scheduler cancel <FlowName> <uuid> Cancel a waiting watch/run
@@ -111,7 +111,7 @@
111
111
  "composer": {
112
112
  "edit_context": "## AgentFlow Edit Context",
113
113
  "tool_nodejs_rules_title": "## tool_nodejs Node Writing Rules (Must Follow)",
114
- "tool_nodejs_rules_body": "The core of `definitionId: tool_nodejs` is the **`script` field**—must write a complete executable shell/node command.\n- `script`: Pipeline spawns directly, stdout becomes result, exit code determines success/failure\n- `body`: When `script` exists, it serves only as human-readable comment, **will NOT be executed**\n- `script` supports `${}` placeholders referencing input slots and system variables (workspaceRoot, runDir, etc.), values are auto shell-quoted\n- **Do NOT** wrap `${workspaceRoot}` etc. with extra double quotes (e.g., `node \"${workspaceRoot}/...\"`); write `node ${workspaceRoot}/...` instead, otherwise path will contain extra quotes causing execution failure\n- **Forbidden** to only write natural language description in `body` without `script`—this leads to no executable code at runtime\n- If logic is too complex to write complete `script`, use `agent_subAgent` instead (also change definitionId)",
114
+ "tool_nodejs_rules_body": "The core of `definitionId: tool_nodejs` is the **`script` field**—must write a complete executable shell/node command.\n- `script`: Pipeline spawns directly, stdout becomes result, exit code determines success/failure\n- `body`: When `script` exists, it serves only as human-readable comment, **will NOT be executed**\n- `script` supports `${}` placeholders referencing input slots and system variables (workspaceRoot, pipelineWorkspace, runDir, etc.), values are auto shell-quoted; after CD Workspace, `${workspaceRoot}` is the current execution workspace and `${pipelineWorkspace}` is the pipeline workspace\n- **Do NOT** wrap `${workspaceRoot}` etc. with extra double quotes (e.g., `node \"${workspaceRoot}/...\"`); write `node ${workspaceRoot}/...` instead, otherwise path will contain extra quotes causing execution failure\n- **Forbidden** to only write natural language description in `body` without `script`—this leads to no executable code at runtime\n- If logic is too complex to write complete `script`, use `agent_subAgent` instead (also change definitionId)",
115
115
  "task_title": "## Task",
116
116
  "task_instruction": "Please complete only the above single task, no extra modifications. After completion, sync UI per context instructions.",
117
117
  "validation_passed": "flow validation passed",
@@ -265,6 +265,18 @@
265
265
  "displayName": "Agent ToBool",
266
266
  "description": "AI-powered boolean judgment for non-deterministic scenarios"
267
267
  },
268
+ "control_cd_workspace": {
269
+ "displayName": "CD Workspace",
270
+ "description": "Switch downstream execution to another workspace context without changing the pipeline workspace"
271
+ },
272
+ "control_load_skills": {
273
+ "displayName": "Load Skills",
274
+ "description": "Load SKILL.md files from the current workspace, pipeline workspace, explicit paths, or both"
275
+ },
276
+ "tool_git_checkout": {
277
+ "displayName": "Git Checkout",
278
+ "description": "Clone or update a Git repository and expose it as a workspace context"
279
+ },
268
280
  "tool_nodejs": {
269
281
  "displayName": "Node.js Script",
270
282
  "description": "Execute Node.js script, success determined by exit code, stdout as result"
@@ -285,6 +297,18 @@
285
297
  "displayName": "Load Key",
286
298
  "description": "Load key-value from global storage"
287
299
  },
300
+ "display_markdown": {
301
+ "displayName": "Markdown Display",
302
+ "description": "Render Markdown content on the Workspace canvas and pass the text downstream"
303
+ },
304
+ "display_mermaid": {
305
+ "displayName": "Mermaid Display",
306
+ "description": "Render Mermaid diagram source on the Workspace canvas and pass the source downstream"
307
+ },
308
+ "display_ascii": {
309
+ "displayName": "ASCII Display",
310
+ "description": "Render ASCII diagram text on the Workspace canvas and pass the text downstream"
311
+ },
288
312
  "provide_str": {
289
313
  "displayName": "Text",
290
314
  "description": "Provide a text value directly, value will be passed to downstream as-is"
@@ -111,7 +111,7 @@
111
111
  "composer": {
112
112
  "edit_context": "## AgentFlow 编辑上下文",
113
113
  "tool_nodejs_rules_title": "## tool_nodejs 节点编写规则(必须遵守)",
114
- "tool_nodejs_rules_body": "`definitionId: tool_nodejs` 的核心是 **`script` 字段**——必须写完整可执行的 shell/node 命令。\n- **类型选择自检**(写 script 前先问自己):这事**确定性**吗?相同输入永远产出相同输出、可用普通代码完整描述?是 → 继续写 script;否(需要语义理解/代码翻译/LLM 推理)→ **该节点选错了类型**,应改 `definitionId: agent_subAgent`,删 `script` 字段,任务描述写到 `body`。反例:『Android→RN 页面转换』『代码 review』『生成测试用例』必须 agent。\n- `script`:流水线直接 spawn 执行,stdout 作为 result,exit code 决定成败\n- `body`:有 `script` 时仅作人类可读注释,**不会被执行**\n- `script` 支持 `${}` 占位符引用 input 槽位和系统变量(workspaceRoot、runDir 等),值自动 shell-quote\n- **不要**再对 `${workspaceRoot}` 等占位符外包双引号(如 `node \"${workspaceRoot}/...\"`);应写 `node ${workspaceRoot}/...`,否则路径会含多余引号导致执行失败\n- **禁止**只在 `body` 中写自然语言描述而不写 `script`——这会导致节点运行时无代码可执行",
114
+ "tool_nodejs_rules_body": "`definitionId: tool_nodejs` 的核心是 **`script` 字段**——必须写完整可执行的 shell/node 命令。\n- **类型选择自检**(写 script 前先问自己):这事**确定性**吗?相同输入永远产出相同输出、可用普通代码完整描述?是 → 继续写 script;否(需要语义理解/代码翻译/LLM 推理)→ **该节点选错了类型**,应改 `definitionId: agent_subAgent`,删 `script` 字段,任务描述写到 `body`。反例:『Android→RN 页面转换』『代码 review』『生成测试用例』必须 agent。\n- `script`:流水线直接 spawn 执行,stdout 作为 result,exit code 决定成败\n- `body`:有 `script` 时仅作人类可读注释,**不会被执行**\n- `script` 支持 `${}` 占位符引用 input 槽位和系统变量(workspaceRoot、pipelineWorkspace、runDir 等),值自动 shell-quote;接了 CD Workspace 后 `${workspaceRoot}` 表示当前执行工作区,`${pipelineWorkspace}` 表示流水线工作区\n- **不要**再对 `${workspaceRoot}` 等占位符外包双引号(如 `node \"${workspaceRoot}/...\"`);应写 `node ${workspaceRoot}/...`,否则路径会含多余引号导致执行失败\n- **禁止**只在 `body` 中写自然语言描述而不写 `script`——这会导致节点运行时无代码可执行",
115
115
  "task_title": "## 任务",
116
116
  "task_instruction": "请只完成上述单一任务,不要做额外修改。完成后按上下文中的指引同步 UI。",
117
117
  "validation_passed": "flow 校验已通过",
@@ -265,6 +265,18 @@
265
265
  "displayName": "AI 转布尔",
266
266
  "description": "由 AI 判断输入内容的布尔含义,适用于不确定性场景"
267
267
  },
268
+ "control_cd_workspace": {
269
+ "displayName": "CD 工作区",
270
+ "description": "切换下游节点的执行工作区上下文,不改变流水线所在工作区"
271
+ },
272
+ "control_load_skills": {
273
+ "displayName": "加载 Skills",
274
+ "description": "从当前工作区、流水线工作区、显式路径或组合来源加载 SKILL.md"
275
+ },
276
+ "tool_git_checkout": {
277
+ "displayName": "Git 拉取",
278
+ "description": "克隆或更新 Git 仓库,并输出可供下游使用的工作区上下文"
279
+ },
268
280
  "tool_nodejs": {
269
281
  "displayName": "Node.js 脚本",
270
282
  "description": "执行 Node.js 脚本,以 exit code 判断成败,stdout 作为结果"
@@ -285,6 +297,18 @@
285
297
  "displayName": "加载键值",
286
298
  "description": "从全局存储加载键值"
287
299
  },
300
+ "display_markdown": {
301
+ "displayName": "Markdown 展示",
302
+ "description": "在 Workspace 画布中渲染 Markdown 内容,并将文本继续传给下游"
303
+ },
304
+ "display_mermaid": {
305
+ "displayName": "Mermaid 展示",
306
+ "description": "在 Workspace 画布中渲染 Mermaid 图,并将源码继续传给下游"
307
+ },
308
+ "display_ascii": {
309
+ "displayName": "ASCII 图展示",
310
+ "description": "在 Workspace 画布中渲染等宽 ASCII 图,并将文本继续传给下游"
311
+ },
288
312
  "provide_str": {
289
313
  "displayName": "文本",
290
314
  "description": "直接提供一段文本,value 会原样供下游引用"
package/bin/lib/main.mjs CHANGED
@@ -346,6 +346,7 @@ export async function main() {
346
346
  let host = process.env.AGENTFLOW_UI_HOST || "127.0.0.1";
347
347
  let schedulerEnabled = false;
348
348
  let schedulerPollMs;
349
+ let hideCommunityLinks = /^(1|true|yes|on)$/i.test(String(process.env.AGENTFLOW_HIDE_COMMUNITY_LINKS || ""));
349
350
  const portIdx = argv.indexOf("--port");
350
351
  if (portIdx >= 0 && argv[portIdx + 1]) {
351
352
  port = parseInt(argv[portIdx + 1], 10);
@@ -371,13 +372,17 @@ export async function main() {
371
372
  }
372
373
  const noOpen = argv.includes("--no-open");
373
374
  if (noOpen) argv.splice(argv.indexOf("--no-open"), 1);
375
+ if (argv.includes("--hide-community-links")) {
376
+ hideCommunityLinks = true;
377
+ argv.splice(argv.indexOf("--hide-community-links"), 1);
378
+ }
374
379
  if (Number.isNaN(port) || port <= 0 || port > 65535) {
375
380
  throw new Error("Invalid --port (use 1–65535)");
376
381
  }
377
382
  if (!host) {
378
383
  throw new Error("Invalid --host");
379
384
  }
380
- await startUiServer({ workspaceRoot, port, host });
385
+ await startUiServer({ workspaceRoot, port, host, hideCommunityLinks });
381
386
  if (schedulerEnabled) {
382
387
  startScheduler(workspaceRoot, { pollMs: schedulerPollMs }).catch((e) => {
383
388
  log.error("Scheduler failed: " + ((e && e.message) || String(e)));
@@ -18,13 +18,13 @@ import {
18
18
  * @param {string} [runId] - specific uuid; if empty, picks the latest run
19
19
  * @returns {{ ok: boolean, rounds: Array, runId?: string, error?: string }}
20
20
  */
21
- export function getNodeExecContext(workspaceRoot, flowId, instanceId, runId) {
21
+ export function getNodeExecContext(workspaceRoot, flowId, instanceId, runId, opts = {}) {
22
22
  let uuid = runId;
23
23
  let runDir = "";
24
24
  if (uuid) {
25
- runDir = getRunDirCandidates(workspaceRoot, flowId, uuid).find((p) => fs.existsSync(p)) || "";
25
+ runDir = getRunDirCandidates(workspaceRoot, flowId, uuid, opts).find((p) => fs.existsSync(p)) || "";
26
26
  } else {
27
- const latest = findLatestRunDir(workspaceRoot, flowId);
27
+ const latest = findLatestRunDir(workspaceRoot, flowId, opts);
28
28
  uuid = latest.uuid;
29
29
  runDir = latest.runDir;
30
30
  }
@@ -38,9 +38,9 @@ export function getNodeExecContext(workspaceRoot, flowId, instanceId, runId) {
38
38
  return { ok: true, rounds, runId: uuid };
39
39
  }
40
40
 
41
- function findLatestRunDir(workspaceRoot, flowId) {
41
+ function findLatestRunDir(workspaceRoot, flowId, opts = {}) {
42
42
  const roots = [
43
- path.join(getFlowRuntimeRoot(workspaceRoot, flowId), "runBuild"),
43
+ path.join(getFlowRuntimeRoot(workspaceRoot, flowId, opts), "runBuild"),
44
44
  path.join(getWorkspaceRunBuildRoot(workspaceRoot), flowId),
45
45
  path.join(getLegacyUserRunBuildRoot(), flowId),
46
46
  ];
@@ -152,11 +152,11 @@ async function healToolNodejsWithAI(workspaceRoot, flowName, uuid, instanceId, r
152
152
  * 协议:与 run-tool-nodejs.mjs 一致——validate-script-output 解析 stdout;
153
153
  * JSON 时 message 的每个键写入同名 output 槽位(含 result);纯文本则等价于 message.result。
154
154
  */
155
- async function executeToolNodejsInline(workspaceRoot, flowName, uuid, instanceId, resolvedScript, execId, healOptions) {
155
+ async function executeToolNodejsInline(workspaceRoot, flowName, uuid, instanceId, resolvedScript, execId, healOptions, execWorkspaceRoot = workspaceRoot) {
156
156
  let lastError;
157
157
  for (let attempt = 1; attempt <= TOOL_NODEJS_MAX_RETRIES + 1; attempt++) {
158
158
  try {
159
- executeToolNodejsOnce(workspaceRoot, flowName, uuid, instanceId, resolvedScript, execId);
159
+ executeToolNodejsOnce(workspaceRoot, flowName, uuid, instanceId, resolvedScript, execId, execWorkspaceRoot);
160
160
  return;
161
161
  } catch (err) {
162
162
  lastError = err;
@@ -200,21 +200,22 @@ function persistToolNodejsStderr(outputDir, instanceId, execId, stderr) {
200
200
  } catch (_) {}
201
201
  }
202
202
 
203
- function executeToolNodejsOnce(workspaceRoot, flowName, uuid, instanceId, resolvedScript, execId) {
203
+ function executeToolNodejsOnce(workspaceRoot, flowName, uuid, instanceId, resolvedScript, execId, execWorkspaceRoot = workspaceRoot) {
204
204
  const runDir = getRunDir(workspaceRoot, flowName, uuid);
205
205
  const outputDir = path.join(runDir, outputDirForNode(instanceId));
206
+ fs.mkdirSync(outputDir, { recursive: true });
206
207
 
207
208
  const { argv, commandLine: normalized } = nodeToolCommandToArgv(resolvedScript);
208
209
  let child;
209
210
  if (/^node\s/i.test(String(normalized).trim()) && argv.length >= 1) {
210
211
  child = spawnSync(process.execPath, argv, {
211
- cwd: workspaceRoot,
212
+ cwd: execWorkspaceRoot,
212
213
  shell: false,
213
214
  stdio: ["inherit", "pipe", "pipe"],
214
215
  });
215
216
  } else {
216
217
  child = spawnSync(normalized, [], {
217
- cwd: workspaceRoot,
218
+ cwd: execWorkspaceRoot,
218
219
  shell: true,
219
220
  stdio: ["inherit", "pipe", "pipe"],
220
221
  });
@@ -224,8 +225,6 @@ function executeToolNodejsOnce(workspaceRoot, flowName, uuid, instanceId, resolv
224
225
  const stderr = child.stderr?.toString("utf-8") ?? "";
225
226
  const exitCode = child.status ?? 1;
226
227
 
227
- fs.mkdirSync(outputDir, { recursive: true });
228
-
229
228
  // 直接写文件模式:stdout 为空 + exit 0 → 脚本已自行写入 output 文件,无需解析
230
229
  if (!stdout.trim() && exitCode === 0) {
231
230
  persistToolNodejsStderr(outputDir, instanceId, execId, stderr);
@@ -310,6 +309,7 @@ export async function executeNode(workspaceRoot, flowName, uuid, instanceId, pre
310
309
  const { definitionId, directCommand, resolvedScript, promptPath, nodeContext, taskBody, resultPath, subagent } = preOutput;
311
310
  const runDir = getRunDir(workspaceRoot, flowName, uuid);
312
311
  const intermediatePath = runDir;
312
+ const execWorkspaceRoot = path.resolve(preOutput.workspaceContext?.workspaceRoot || preOutput.workspaceContext?.cwd || workspaceRoot);
313
313
 
314
314
  if (definitionId && LOCAL_ONLY_DEFINITION_IDS.has(definitionId)) {
315
315
  return;
@@ -331,7 +331,7 @@ export async function executeNode(workspaceRoot, flowName, uuid, instanceId, pre
331
331
  directCommand: resolvedScript,
332
332
  });
333
333
  try {
334
- await executeToolNodejsInline(workspaceRoot, flowName, uuid, instanceId, resolvedScript, execId, healOptions);
334
+ await executeToolNodejsInline(workspaceRoot, flowName, uuid, instanceId, resolvedScript, execId, healOptions, execWorkspaceRoot);
335
335
  emitEvent(workspaceRoot, flowName, uuid, {
336
336
  event: "direct-command-done",
337
337
  instanceId,
@@ -356,7 +356,7 @@ export async function executeNode(workspaceRoot, flowName, uuid, instanceId, pre
356
356
  directCommand,
357
357
  });
358
358
  try {
359
- const result = spawnSync(directCommand, [], { cwd: workspaceRoot, shell: true, stdio: "inherit" });
359
+ const result = spawnSync(directCommand, [], { cwd: execWorkspaceRoot, shell: true, stdio: "inherit" });
360
360
  if (result.status !== 0) {
361
361
  emitEvent(workspaceRoot, flowName, uuid, {
362
362
  event: "direct-command-failed",
@@ -398,6 +398,7 @@ export async function executeNode(workspaceRoot, flowName, uuid, instanceId, pre
398
398
  resultPathRel: resultPath ?? null,
399
399
  modelCli: cli,
400
400
  model: model ?? null,
401
+ execWorkspaceRoot,
401
402
  });
402
403
  try {
403
404
  if (cli === "api") {
@@ -409,6 +410,7 @@ export async function executeNode(workspaceRoot, flowName, uuid, instanceId, pre
409
410
  onToolCall: options.onToolCall,
410
411
  flowName,
411
412
  uuid,
413
+ execWorkspaceRoot,
412
414
  },
413
415
  );
414
416
  } else if (cli === "opencode") {
@@ -424,6 +426,7 @@ export async function executeNode(workspaceRoot, flowName, uuid, instanceId, pre
424
426
  onToolCall: options.onToolCall,
425
427
  flowName,
426
428
  uuid,
429
+ execWorkspaceRoot,
427
430
  },
428
431
  );
429
432
  } else if (cli === "claude-code") {
@@ -439,6 +442,7 @@ export async function executeNode(workspaceRoot, flowName, uuid, instanceId, pre
439
442
  onToolCall: options.onToolCall,
440
443
  flowName,
441
444
  uuid,
445
+ execWorkspaceRoot,
442
446
  },
443
447
  );
444
448
  } else {
@@ -454,6 +458,7 @@ export async function executeNode(workspaceRoot, flowName, uuid, instanceId, pre
454
458
  onToolCall: options.onToolCall,
455
459
  flowName,
456
460
  uuid,
461
+ execWorkspaceRoot,
457
462
  },
458
463
  );
459
464
  }
package/bin/lib/paths.mjs CHANGED
@@ -34,6 +34,56 @@ export function getAgentflowDataRoot() {
34
34
  return path.join(os.homedir(), "agentflow");
35
35
  }
36
36
 
37
+ export const AGENTFLOW_DEFAULT_USER_ID = "";
38
+
39
+ export function sanitizeAgentflowUserId(userId) {
40
+ const raw = userId == null ? "" : String(userId).trim();
41
+ if (!raw) return AGENTFLOW_DEFAULT_USER_ID;
42
+ const normalized = raw.toLowerCase();
43
+ if (!/^[a-z][a-z0-9_-]{0,63}$/.test(normalized)) return AGENTFLOW_DEFAULT_USER_ID;
44
+ return normalized;
45
+ }
46
+
47
+ export function getAgentflowUserDataRoot(userId) {
48
+ const safe = sanitizeAgentflowUserId(userId ?? process.env.AGENTFLOW_USER_ID);
49
+ if (!safe) return getAgentflowDataRoot();
50
+ return path.join(getAgentflowDataRoot(), "users", safe);
51
+ }
52
+
53
+ export function listAgentflowUserIds() {
54
+ const usersRoot = path.join(getAgentflowDataRoot(), "users");
55
+ try {
56
+ if (!fs.existsSync(usersRoot) || !fs.statSync(usersRoot).isDirectory()) return [];
57
+ return fs.readdirSync(usersRoot, { withFileTypes: true })
58
+ .filter((entry) => entry.isDirectory())
59
+ .map((entry) => sanitizeAgentflowUserId(entry.name))
60
+ .filter(Boolean)
61
+ .sort((a, b) => a.localeCompare(b));
62
+ } catch {
63
+ return [];
64
+ }
65
+ }
66
+
67
+ export function getAgentflowUserContexts() {
68
+ const ids = listAgentflowUserIds();
69
+ return [{}, ...ids.map((userId) => ({ userId }))];
70
+ }
71
+
72
+ export function resolveUniqueUserPipelineDir(flowName) {
73
+ const name = flowName == null ? "" : String(flowName).trim();
74
+ if (!name) return null;
75
+ const matches = [];
76
+ for (const userId of listAgentflowUserIds()) {
77
+ const dir = path.join(getAgentflowDataRoot(), "users", userId, "pipelines", name);
78
+ try {
79
+ if (fs.existsSync(path.join(dir, "flow.yaml"))) matches.push(dir);
80
+ } catch {
81
+ /* ignore unreadable user dirs */
82
+ }
83
+ }
84
+ return matches.length === 1 ? matches[0] : null;
85
+ }
86
+
37
87
  /** 项目内 runBuild 根目录:`<workspaceRoot>/.workspace/agentflow/runBuild`(legacy:写入路径已迁至 `<flowDir>/runBuild`,仅用于兼容读取) */
38
88
  export function getWorkspaceRunBuildRoot(workspaceRoot) {
39
89
  const root =
@@ -52,23 +102,21 @@ export function getLegacyUserRunBuildRoot() {
52
102
  * 统一 runtime root:每个 flow 的 pipeline 源、scripts、runBuild 共用一个根目录。
53
103
  * - 若 `~/agentflow/pipelines/<name>/flow.yaml` 存在 → user-scope:`~/agentflow/pipelines/<name>`
54
104
  * - 若 `<ws>/.workspace/agentflow/pipelines/<name>/flow.yaml` 存在 → workspace-scope:`<ws>/.workspace/agentflow/pipelines/<name>`
55
- * - archived(`_archived/<name>`)按对应 scope 返回
56
105
  * - 其他(builtin 只读 / 不存在)→ 默认 user-scope 路径(首次 run 时自动创建,builtin 源仍从包内读取但 runBuild 落到用户目录)
106
+ * 归档 flow 不参与普通 runtime root 解析,避免同名 archived flow 被误当作活跃 flow。
57
107
  */
58
- export function getFlowRuntimeRoot(workspaceRoot, flowName) {
108
+ export function getFlowRuntimeRoot(workspaceRoot, flowName, opts = {}) {
59
109
  const root =
60
110
  workspaceRoot != null && String(workspaceRoot).trim() !== ""
61
111
  ? path.resolve(String(workspaceRoot))
62
112
  : process.cwd();
63
- const userRoot = getUserPipelinesRoot();
113
+ const userRoot = getUserPipelinesRoot(opts.userId);
64
114
  const userDir = path.join(userRoot, flowName);
65
115
  if (fs.existsSync(path.join(userDir, "flow.yaml"))) return userDir;
66
- const userArchived = path.join(userRoot, ARCHIVED_PIPELINES_DIR_NAME, flowName);
67
- if (fs.existsSync(path.join(userArchived, "flow.yaml"))) return userArchived;
116
+ const inferredUserDir = resolveUniqueUserPipelineDir(flowName);
117
+ if (inferredUserDir) return inferredUserDir;
68
118
  const wsDir = path.join(root, PIPELINES_DIR, flowName);
69
119
  if (fs.existsSync(path.join(wsDir, "flow.yaml"))) return wsDir;
70
- const wsArchived = path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowName);
71
- if (fs.existsSync(path.join(wsArchived, "flow.yaml"))) return wsArchived;
72
120
  // builtin / legacy / 尚未落盘 → 默认 user 目录,runBuild 首次写入时创建
73
121
  return userDir;
74
122
  }
@@ -78,8 +126,8 @@ export function getFlowRuntimeRoot(workspaceRoot, flowName) {
78
126
  * 新运行走 `<flowRuntimeRoot>/runBuild/<uuid>`;
79
127
  * 若该 uuid 在旧位置(legacy workspace/user runBuild 根)已存在,则返回旧位置,保留 resume 兼容。
80
128
  */
81
- export function getRunDir(workspaceRoot, flowName, uuid) {
82
- const candidates = getRunDirCandidates(workspaceRoot, flowName, uuid);
129
+ export function getRunDir(workspaceRoot, flowName, uuid, opts = {}) {
130
+ const candidates = getRunDirCandidates(workspaceRoot, flowName, uuid, opts);
83
131
  for (const c of candidates) {
84
132
  if (fs.existsSync(c)) return c;
85
133
  }
@@ -93,9 +141,9 @@ export function getRunDir(workspaceRoot, flowName, uuid) {
93
141
  * 2. 旧 workspace:<ws>/.workspace/agentflow/runBuild/<flow>/<uuid>
94
142
  * 3. 旧 user:~/agentflow/runBuild/<flow>/<uuid>
95
143
  */
96
- export function getRunDirCandidates(workspaceRoot, flowName, uuid) {
144
+ export function getRunDirCandidates(workspaceRoot, flowName, uuid, opts = {}) {
97
145
  const candidates = [
98
- path.join(getFlowRuntimeRoot(workspaceRoot, flowName), "runBuild", uuid),
146
+ path.join(getFlowRuntimeRoot(workspaceRoot, flowName, opts), "runBuild", uuid),
99
147
  path.join(getWorkspaceRunBuildRoot(workspaceRoot), flowName, uuid),
100
148
  path.join(getLegacyUserRunBuildRoot(), flowName, uuid),
101
149
  ];
@@ -110,8 +158,8 @@ export function getRunDirCandidates(workspaceRoot, flowName, uuid) {
110
158
  return out;
111
159
  }
112
160
 
113
- export function getUserPipelinesRoot() {
114
- return path.join(getAgentflowDataRoot(), "pipelines");
161
+ export function getUserPipelinesRoot(userId) {
162
+ return path.join(getAgentflowUserDataRoot(userId), "pipelines");
115
163
  }
116
164
 
117
165
  export function getReferenceRootAbs() {
@@ -204,8 +252,11 @@ export const LOCAL_ONLY_DEFINITION_IDS = new Set([
204
252
  "control_deadline",
205
253
  "control_cancelled",
206
254
  "control_interval_loop",
255
+ "control_cd_workspace",
256
+ "control_load_skills",
207
257
  "control_start",
208
258
  "control_end",
259
+ "tool_git_checkout",
209
260
  "tool_print",
210
261
  "tool_user_check",
211
262
  "tool_user_ask",
@@ -151,9 +151,9 @@ function mapFlowSource(src) {
151
151
  * @param {string} workspaceRoot
152
152
  * @returns {Array<{ flowId: string, flowSource: 'workspace'|'user', runId: string, at: number, durationMs: number, endedAt: number|null, status: 'success'|'failed'|'running'|'stopped'|'interrupted'|'unknown' }>}
153
153
  */
154
- export function listRecentRunsFromDisk(workspaceRoot) {
154
+ export function listRecentRunsFromDisk(workspaceRoot, opts = {}) {
155
155
  const out = [];
156
- for (const { flowName, uuid, runDir, source } of listAllRunDirs(workspaceRoot)) {
156
+ for (const { flowName, uuid, runDir, source } of listAllRunDirs(workspaceRoot, opts)) {
157
157
  let at = readKeyFromMemory(runDir, "runStartTime");
158
158
  if (at == null) {
159
159
  try {
@@ -36,8 +36,8 @@ function parseElapsedMsLine(filePath) {
36
36
  * @param {string} uuid
37
37
  * @returns {Record<string, { status: string, elapsed?: string }>}
38
38
  */
39
- export function getRunNodeStatusesFromDisk(workspaceRoot, flowName, uuid) {
40
- const runDir = getRunDir(workspaceRoot, flowName, uuid);
39
+ export function getRunNodeStatusesFromDisk(workspaceRoot, flowName, uuid, opts = {}) {
40
+ const runDir = getRunDir(workspaceRoot, flowName, uuid, opts);
41
41
  const flowJsonPath = path.join(runDir, "intermediate", "flow.json");
42
42
  if (!fs.existsSync(flowJsonPath)) return {};
43
43
 
@@ -51,7 +51,7 @@ export function getRunNodeStatusesFromDisk(workspaceRoot, flowName, uuid) {
51
51
 
52
52
  const order = Array.isArray(flow.order) ? flow.order : [];
53
53
  const nodeDefinitions = flow.nodeDefinitions && typeof flow.nodeDefinitions === "object" ? flow.nodeDefinitions : {};
54
- const execIdMap = loadAllExecIds(workspaceRoot, flowName, uuid, order);
54
+ const execIdMap = loadAllExecIds(workspaceRoot, flowName, uuid, order, opts);
55
55
  const intermediateDir = path.join(runDir, "intermediate");
56
56
  /** @type {Record<string, { status: string, elapsed?: string }>} */
57
57
  const out = {};