@fieldwangai/agentflow 0.1.30 → 0.1.32

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 (34) hide show
  1. package/bin/lib/agent-runners.mjs +26 -2
  2. package/bin/lib/api-runner.mjs +26 -3
  3. package/bin/lib/apply.mjs +6 -5
  4. package/bin/lib/catalog-flows.mjs +30 -5
  5. package/bin/lib/composer-agent.mjs +2 -1
  6. package/bin/lib/locales/en.json +4 -0
  7. package/bin/lib/locales/zh.json +6 -2
  8. package/bin/lib/marketplace.mjs +124 -2
  9. package/bin/lib/node-execute.mjs +1 -1
  10. package/bin/lib/paths.mjs +5 -0
  11. package/bin/lib/scheduler.mjs +3 -2
  12. package/bin/lib/ui-server.mjs +639 -8
  13. package/bin/lib/user-env.mjs +83 -0
  14. package/bin/pipeline/get-env.mjs +5 -29
  15. package/bin/pipeline/pre-process-node.mjs +28 -6
  16. package/bin/pipeline/run-tool-nodejs.mjs +7 -0
  17. package/builtin/nodes/agent_subAgent.md +6 -3
  18. package/builtin/nodes/control_cd_workspace.md +8 -6
  19. package/builtin/nodes/control_load_skills.md +2 -0
  20. package/builtin/nodes/control_user_workspace.md +20 -0
  21. package/builtin/nodes/display_ascii.md +5 -0
  22. package/builtin/nodes/display_markdown.md +5 -0
  23. package/builtin/nodes/display_mermaid.md +5 -0
  24. package/builtin/nodes/tool_git_checkout.md +3 -0
  25. package/builtin/web-ui/dist/assets/index-D0Tkhqr6.css +1 -0
  26. package/builtin/web-ui/dist/assets/index-DyhW5chp.js +197 -0
  27. package/builtin/web-ui/dist/index.html +2 -2
  28. package/package.json +1 -1
  29. package/skills/agentflow-workspace-ascii/SKILL.md +42 -0
  30. package/skills/agentflow-workspace-graph/SKILL.md +67 -0
  31. package/skills/agentflow-workspace-markdown/SKILL.md +44 -0
  32. package/skills/agentflow-workspace-mermaid/SKILL.md +43 -0
  33. package/builtin/web-ui/dist/assets/index-NdVOJLL9.js +0 -196
  34. package/builtin/web-ui/dist/assets/index-naVI6LZj.css +0 -1
@@ -9,6 +9,8 @@ import { normalizeCursorModelForCli } from "./model-config.mjs";
9
9
  import { appendRunLogLine } from "./run-events.mjs";
10
10
  import { writeWithPrefix } from "./terminal.mjs";
11
11
  import { t } from "./i18n.mjs";
12
+ import { readUserEnvObject } from "./user-env.mjs";
13
+ import { outputNodeBasename } from "../pipeline/get-exec-id.mjs";
12
14
 
13
15
  function shouldPassCursorModelArg(model) {
14
16
  const text = String(model || "").trim();
@@ -17,7 +19,19 @@ function shouldPassCursorModelArg(model) {
17
19
 
18
20
  function childEnv(options = {}, extra = {}) {
19
21
  const optEnv = options && options.env && typeof options.env === "object" ? options.env : {};
20
- return { ...process.env, ...optEnv, ...extra };
22
+ const userId = optEnv.AGENTFLOW_USER_ID || process.env.AGENTFLOW_USER_ID || "";
23
+ return { ...process.env, ...readUserEnvObject(userId), ...optEnv, ...extra };
24
+ }
25
+
26
+ function writeAgentTextArtifacts(absResultPath, absRunDir, instanceId, text) {
27
+ const body = String(text ?? "").trim();
28
+ if (!body) return;
29
+ fs.mkdirSync(path.dirname(absResultPath), { recursive: true });
30
+ fs.writeFileSync(absResultPath, body + "\n", "utf-8");
31
+ if (!instanceId) return;
32
+ const slotPath = path.join(absRunDir, "output", instanceId, outputNodeBasename(instanceId, 1, "result"));
33
+ fs.mkdirSync(path.dirname(slotPath), { recursive: true });
34
+ fs.writeFileSync(slotPath, body + "\n", "utf-8");
21
35
  }
22
36
 
23
37
  /**
@@ -102,6 +116,7 @@ export function runCursorAgentForNode(
102
116
 
103
117
  let lastResult = null;
104
118
  let hadError = false;
119
+ const assistantTextChunks = [];
105
120
  const STDERR_CAP_BYTES = 1024 * 1024;
106
121
  const stderrChunks = [];
107
122
  let stderrTotalBytes = 0;
@@ -186,6 +201,7 @@ export function runCursorAgentForNode(
186
201
  .join("");
187
202
  if (text) {
188
203
  text = text.replace(/\\n/g, "\n").replace(/\\t/g, "\t");
204
+ assistantTextChunks.push(text);
189
205
  const out = mdStreamer.push(text);
190
206
  if (out) writeStdout(out);
191
207
  }
@@ -280,6 +296,7 @@ export function runCursorAgentForNode(
280
296
  reject(new Error(lastResult?.result || "Agent reported error."));
281
297
  return;
282
298
  }
299
+ writeAgentTextArtifacts(absResultPath, absRunDir, instanceId, assistantTextChunks.join("") || lastResult?.result || "");
283
300
  resolve();
284
301
  });
285
302
  });
@@ -364,6 +381,7 @@ export function runOpenCodeAgentForNode(
364
381
 
365
382
  let stdoutLogBuf = "";
366
383
  let stderrLogBuf = "";
384
+ let stdoutCaptured = "";
367
385
 
368
386
  function drainLogBuf(buf, tag) {
369
387
  let idx;
@@ -390,7 +408,9 @@ export function runOpenCodeAgentForNode(
390
408
  child.stdout.on("data", (chunk) => {
391
409
  if (coloredPrefix) writeWithPrefix(process.stdout, chunk, coloredPrefix, agentContentColor);
392
410
  else process.stdout.write(agentContentColor(chunk));
393
- stdoutLogBuf += String(chunk).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
411
+ const normalizedChunk = String(chunk).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
412
+ stdoutCaptured += normalizedChunk;
413
+ stdoutLogBuf += normalizedChunk;
394
414
  stdoutLogBuf = drainLogBuf(stdoutLogBuf, "opencode-stdout");
395
415
  });
396
416
 
@@ -422,6 +442,7 @@ export function runOpenCodeAgentForNode(
422
442
  reject(new Error(`OpenCode CLI exited ${code}.`));
423
443
  return;
424
444
  }
445
+ writeAgentTextArtifacts(absResultPath, absRunDir, instanceId, stripAnsi(stdoutCaptured));
425
446
  resolve();
426
447
  });
427
448
  });
@@ -520,6 +541,7 @@ export function runClaudeCodeAgentForNode(
520
541
  let lastResult = null;
521
542
  let hadError = false;
522
543
  let sessionId = null;
544
+ const assistantTextChunks = [];
523
545
  const STDERR_CAP_BYTES = 1024 * 1024;
524
546
  const stderrChunks = [];
525
547
  let stderrTotalBytes = 0;
@@ -596,6 +618,7 @@ export function runClaudeCodeAgentForNode(
596
618
  if (!block || typeof block !== "object") continue;
597
619
  if (block.type === "text" && block.text) {
598
620
  const text = normalizeStreamTextChunk(block.text);
621
+ assistantTextChunks.push(text);
599
622
  const out = mdStreamer.push(text);
600
623
  if (out) writeStdout(out);
601
624
  } else if (block.type === "thinking") {
@@ -664,6 +687,7 @@ export function runClaudeCodeAgentForNode(
664
687
  reject(new Error(String(msg)));
665
688
  return;
666
689
  }
690
+ writeAgentTextArtifacts(absResultPath, absRunDir, instanceId, assistantTextChunks.join("") || lastResult?.result || "");
667
691
  resolve();
668
692
  });
669
693
  });
@@ -19,11 +19,23 @@ import { spawnSync } from "child_process";
19
19
 
20
20
  import { loadAgentPromptWithReplacements, stripYamlFrontmatter } from "./agents-path.mjs";
21
21
  import { appendRunLogLine } from "./run-events.mjs";
22
+ import { outputNodeBasename } from "../pipeline/get-exec-id.mjs";
22
23
 
23
24
  const DEFAULT_OPENAI_BASE = "https://api.openai.com/v1";
24
25
  const MAX_TOOL_ROUNDS = parseInt(process.env.AGENTFLOW_API_MAX_ROUNDS ?? "30", 10) || 30;
25
26
  const MAX_TOKENS = parseInt(process.env.AGENTFLOW_API_MAX_TOKENS ?? "8192", 10) || 8192;
26
27
 
28
+ function writeAgentTextArtifacts(absResultPath, absRunDir, instanceId, text) {
29
+ const body = String(text ?? "").trim();
30
+ if (!body || !absResultPath || !absRunDir) return;
31
+ fs.mkdirSync(path.dirname(absResultPath), { recursive: true });
32
+ fs.writeFileSync(absResultPath, body + "\n", "utf-8");
33
+ if (!instanceId) return;
34
+ const slotPath = path.join(absRunDir, "output", instanceId, outputNodeBasename(instanceId, 1, "result"));
35
+ fs.mkdirSync(path.dirname(slotPath), { recursive: true });
36
+ fs.writeFileSync(slotPath, body + "\n", "utf-8");
37
+ }
38
+
27
39
  // ─── 工具定义 ────────────────────────────────────────────────────────────────
28
40
 
29
41
  const TOOL_DEFS = [
@@ -203,6 +215,7 @@ async function runOpenAiLoop(apiKey, baseUrl, model, systemPrompt, userContent,
203
215
  ...(systemPrompt ? [{ role: "system", content: systemPrompt }] : []),
204
216
  { role: "user", content: userContent },
205
217
  ];
218
+ let finalText = "";
206
219
 
207
220
  for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
208
221
  log(`[api/openai] round ${round + 1}`);
@@ -218,6 +231,7 @@ async function runOpenAiLoop(apiKey, baseUrl, model, systemPrompt, userContent,
218
231
  const txt = typeof msg.content === "string" ? msg.content : "";
219
232
  if (txt.trim()) options.onToolCall("assistant", txt.slice(0, 200));
220
233
  }
234
+ if (typeof msg.content === "string" && msg.content.trim()) finalText = msg.content;
221
235
 
222
236
  if (choice.finish_reason === "stop" || choice.finish_reason === "end_turn" || !msg.tool_calls?.length) {
223
237
  log(`[api/openai] finished (${choice.finish_reason ?? "no-tool-calls"})`);
@@ -240,10 +254,12 @@ async function runOpenAiLoop(apiKey, baseUrl, model, systemPrompt, userContent,
240
254
  }
241
255
  messages.push(...toolResults);
242
256
  }
257
+ return finalText;
243
258
  }
244
259
 
245
260
  async function runAnthropicLoop(apiKey, model, systemPrompt, userContent, workspaceRoot, log, options) {
246
261
  const messages = [{ role: "user", content: userContent }];
262
+ let finalText = "";
247
263
 
248
264
  for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
249
265
  log(`[api/anthropic] round ${round + 1}`);
@@ -256,6 +272,8 @@ async function runAnthropicLoop(apiKey, model, systemPrompt, userContent, worksp
256
272
  const textBlock = resp.content?.find((b) => b.type === "text");
257
273
  if (textBlock?.text) options.onToolCall("assistant", textBlock.text.slice(0, 200));
258
274
  }
275
+ const textBlock = resp.content?.find((b) => b.type === "text");
276
+ if (textBlock?.text?.trim()) finalText = textBlock.text;
259
277
 
260
278
  if (resp.stop_reason === "end_turn" || resp.stop_reason === "stop_sequence") {
261
279
  log(`[api/anthropic] finished (${resp.stop_reason})`);
@@ -280,6 +298,7 @@ async function runAnthropicLoop(apiKey, model, systemPrompt, userContent, worksp
280
298
  }
281
299
  messages.push({ role: "user", content: toolResults });
282
300
  }
301
+ return finalText;
283
302
  }
284
303
 
285
304
  // ─── 公共解析函数 ─────────────────────────────────────────────────────────────
@@ -313,9 +332,11 @@ export function parseApiModel(str) {
313
332
  * uuid — 用于日志
314
333
  * onToolCall — (subtype: string, name: string) => void 供 spinner 展示
315
334
  */
316
- export async function runApiAgentForNode(workspaceRoot, { promptPath, nodeContext, taskBody, subagent, instanceId }, options = {}) {
335
+ export async function runApiAgentForNode(workspaceRoot, { promptPath, nodeContext, taskBody, intermediatePath, resultPathRel, subagent, instanceId }, options = {}) {
317
336
  const absRoot = path.resolve(workspaceRoot);
318
337
  const execRoot = path.resolve(options.execWorkspaceRoot || workspaceRoot);
338
+ const absRunDir = intermediatePath ? path.resolve(workspaceRoot, intermediatePath) : "";
339
+ const absResultPath = absRunDir && resultPathRel ? path.join(absRunDir, resultPathRel) : "";
319
340
  const flowName = options.flowName ?? null;
320
341
  const uuid = options.uuid ?? null;
321
342
 
@@ -352,12 +373,14 @@ export async function runApiAgentForNode(workspaceRoot, { promptPath, nodeContex
352
373
  if (provider === "anthropic") {
353
374
  const key = process.env.ANTHROPIC_API_KEY;
354
375
  if (!key) throw new Error("[api-runner] ANTHROPIC_API_KEY is required for api:anthropic/* models");
355
- await runAnthropicLoop(key, model, systemPrompt, userContent, execRoot, log, options);
376
+ const finalText = await runAnthropicLoop(key, model, systemPrompt, userContent, execRoot, log, options);
377
+ writeAgentTextArtifacts(absResultPath, absRunDir, instanceId, finalText);
356
378
  } else {
357
379
  const key = process.env.OPENAI_API_KEY;
358
380
  if (!key) throw new Error("[api-runner] OPENAI_API_KEY is required for api:openai/* models");
359
381
  const baseUrl = (process.env.OPENAI_BASE_URL ?? DEFAULT_OPENAI_BASE).trim();
360
- await runOpenAiLoop(key, baseUrl, model, systemPrompt, userContent, execRoot, log, options);
382
+ const finalText = await runOpenAiLoop(key, baseUrl, model, systemPrompt, userContent, execRoot, log, options);
383
+ writeAgentTextArtifacts(absResultPath, absRunDir, instanceId, finalText);
361
384
  }
362
385
 
363
386
  log(`done instanceId=${instanceId ?? "-"}`);
package/bin/lib/apply.mjs CHANGED
@@ -23,6 +23,7 @@ import { formatDuration } from "./terminal.mjs";
23
23
  import { printEntryAndFlowFiles, printNodeStatusTable, runValidateFlowAndExitIfInvalid } from "./ui-print.mjs";
24
24
  import { clearApplyActiveLock, writeApplyActiveLock } from "./run-apply-active-lock.mjs";
25
25
  import { ensureReference, findFlowNameByUuid, getFlowDir, getRunDir } from "./workspace.mjs";
26
+ import { readUserEnvObject } from "./user-env.mjs";
26
27
 
27
28
  const PARALLEL_PREFIX_COLORS = [
28
29
  (s) => chalk.cyan(s),
@@ -344,11 +345,11 @@ ${currentContent}
344
345
  fs.mkdirSync(path.dirname(tmpPromptFile), { recursive: true });
345
346
  fs.writeFileSync(tmpPromptFile, fullPrompt, "utf-8");
346
347
 
347
- const result = spawnSync(opencodeCmd, ["--prompt-file", tmpPromptFile, "--print"], {
348
- cwd: workspaceRoot,
349
- env: { ...process.env, OPENCODE_NON_INTERACTIVE: "1" },
350
- stdio: ["ignore", "pipe", "pipe"],
351
- });
348
+ const result = spawnSync(opencodeCmd, ["--prompt-file", tmpPromptFile, "--print"], {
349
+ cwd: workspaceRoot,
350
+ env: { ...process.env, ...readUserEnvObject(process.env.AGENTFLOW_USER_ID || ""), OPENCODE_NON_INTERACTIVE: "1" },
351
+ stdio: ["ignore", "pipe", "pipe"],
352
+ });
352
353
 
353
354
  try { fs.unlinkSync(tmpPromptFile); } catch (_) {}
354
355
 
@@ -129,7 +129,10 @@ function normalizeFrontmatterSlots(arr) {
129
129
  let def = item.default !== undefined && item.default !== null ? item.default : item.value;
130
130
  if (def === undefined || def === null) def = "";
131
131
  else if (typeof def !== "string") def = String(def);
132
- return { type, name, default: def };
132
+ const slot = { type, name, default: def };
133
+ if (item.required != null) slot.required = Boolean(item.required);
134
+ if (item.showOnNode != null) slot.showOnNode = Boolean(item.showOnNode);
135
+ return slot;
133
136
  });
134
137
  }
135
138
 
@@ -653,16 +656,38 @@ function resolveMarkdownNodeFile(workspaceRoot, nodeId, flowId, flowSource, opts
653
656
 
654
657
  function readNodeUsage(workspaceRoot, nodeId, opts = {}) {
655
658
  const usage = [];
659
+ const marketSpec = parseMarketplaceDefinitionId(nodeId);
656
660
  for (const flow of listFlowsJson(workspaceRoot, opts)) {
657
661
  const flowPath = getFlowYamlAbs(workspaceRoot, flow.id, flow.source || "user", { archived: Boolean(flow.archived), userId: opts.userId });
658
662
  if (!flowPath.path) continue;
659
663
  try {
660
664
  const data = yaml.load(fs.readFileSync(flowPath.path, "utf-8"));
661
665
  const instances = data && typeof data === "object" ? data.instances : null;
662
- if (!instances || typeof instances !== "object") continue;
663
- const hits = Object.entries(instances)
664
- .filter(([, inst]) => inst && inst.definitionId === nodeId)
665
- .map(([instanceId, inst]) => ({ instanceId, label: inst.label || instanceId }));
666
+ const hits = [];
667
+ if (marketSpec) {
668
+ const deps = data && typeof data === "object" && data.dependencies && typeof data.dependencies === "object" ? data.dependencies : {};
669
+ const nodeDeps = Array.isArray(deps.nodes) ? deps.nodes : [];
670
+ if (nodeDeps.some((dep) => {
671
+ const parsed = typeof dep === "string"
672
+ ? parseMarketplaceDefinitionId(dep.startsWith("marketplace:") ? dep : `marketplace:${dep}`)
673
+ : dep && typeof dep === "object"
674
+ ? { id: dep.id, version: dep.version != null ? String(dep.version) : null }
675
+ : null;
676
+ return parsed && parsed.id === marketSpec.id && (!parsed.version || !marketSpec.version || parsed.version === marketSpec.version);
677
+ })) {
678
+ hits.push({ instanceId: "dependencies.nodes", label: "dependency" });
679
+ }
680
+ }
681
+ if (instances && typeof instances === "object") {
682
+ hits.push(...Object.entries(instances)
683
+ .filter(([, inst]) => {
684
+ if (!inst) return false;
685
+ if (!marketSpec) return inst.definitionId === nodeId;
686
+ const parsed = parseMarketplaceDefinitionId(inst.definitionId);
687
+ return parsed && parsed.id === marketSpec.id && (!parsed.version || !marketSpec.version || parsed.version === marketSpec.version);
688
+ })
689
+ .map(([instanceId, inst]) => ({ instanceId, label: inst.label || instanceId })));
690
+ }
666
691
  if (hits.length > 0) {
667
692
  usage.push({ flowId: flow.id, flowSource: flow.source || "user", archived: Boolean(flow.archived), instances: hits });
668
693
  }
@@ -7,6 +7,7 @@
7
7
  import fs from "fs";
8
8
  import path from "path";
9
9
  import { getAgentflowDataRoot, sanitizeAgentflowUserId } from "./paths.mjs";
10
+ import { readUserEnvObject } from "./user-env.mjs";
10
11
  import { resolveCliAndModel } from "./model-config.mjs";
11
12
  import { runClaudeCodeAgentWithPrompt, runCursorAgentWithPrompt, runOpenCodeAgentWithPrompt } from "./agent-runners.mjs";
12
13
  import { planComposerTasks, hasPlannerApiAvailable, shouldUsePhased, classifyComplexity, classifyTaskComplexity, PHASED_DEFINITIONS } from "./composer-planner.mjs";
@@ -25,7 +26,7 @@ const MAX_SCRIPT_INJECT_BYTES = 30_000;
25
26
 
26
27
  function agentflowUserEnv(userId) {
27
28
  const safe = sanitizeAgentflowUserId(userId);
28
- return safe ? { AGENTFLOW_USER_ID: safe } : {};
29
+ return safe ? { ...readUserEnvObject(safe), AGENTFLOW_USER_ID: safe } : {};
29
30
  }
30
31
 
31
32
  // ─── script 内容注入辅助 ─────────────────────────────────────────────────
@@ -269,6 +269,10 @@
269
269
  "displayName": "CD Workspace",
270
270
  "description": "Switch downstream execution to another workspace context without changing the pipeline workspace"
271
271
  },
272
+ "control_user_workspace": {
273
+ "displayName": "User Workspace",
274
+ "description": "Output a workspace context pointing to the current user's home directory"
275
+ },
272
276
  "control_load_skills": {
273
277
  "displayName": "Load Skills",
274
278
  "description": "Load SKILL.md files from the current workspace, pipeline workspace, explicit paths, or both"
@@ -218,8 +218,8 @@
218
218
  "description": "AgentFlow 的结束节点,执行完成后流程结束"
219
219
  },
220
220
  "agent_subAgent": {
221
- "displayName": "SubAgent",
222
- "description": "利用 SubAgent 执行任务"
221
+ "displayName": "子 Agent",
222
+ "description": "利用子 Agent 执行任务"
223
223
  },
224
224
  "tool_user_ask": {
225
225
  "displayName": "用户选择",
@@ -269,6 +269,10 @@
269
269
  "displayName": "CD 工作区",
270
270
  "description": "切换下游节点的执行工作区上下文,不改变流水线所在工作区"
271
271
  },
272
+ "control_user_workspace": {
273
+ "displayName": "用户工作区",
274
+ "description": "输出指向当前用户 Home 目录的工作区上下文"
275
+ },
272
276
  "control_load_skills": {
273
277
  "displayName": "加载 Skills",
274
278
  "description": "从当前工作区、流水线工作区、显式路径或组合来源加载 SKILL.md"
@@ -2,7 +2,13 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import yaml from "js-yaml";
4
4
 
5
- import { MARKETPLACE_PACKAGES_DIR } from "./paths.mjs";
5
+ import {
6
+ ARCHIVED_PIPELINES_DIR_NAME,
7
+ LEGACY_PIPELINES_DIR,
8
+ MARKETPLACE_PACKAGES_DIR,
9
+ PIPELINES_DIR,
10
+ getUserPipelinesRoot,
11
+ } from "./paths.mjs";
6
12
 
7
13
  const NODE_MANIFEST = "node.yaml";
8
14
  const COLLECTION_MANIFEST = "collection.yaml";
@@ -100,6 +106,99 @@ function listVersionDirs(baseDir) {
100
106
  .filter(Boolean);
101
107
  }
102
108
 
109
+ function isSafePathSegment(value) {
110
+ const text = String(value || "").trim();
111
+ return Boolean(text) && !text.includes("\0") && !path.isAbsolute(text) && !text.split(/[\\/]+/).includes("..");
112
+ }
113
+
114
+ function resolveWorkspaceNodePackageDir(workspaceRoot, id, version) {
115
+ if (!isSafePathSegment(id) || !isSafePathSegment(version)) return null;
116
+ const base = path.resolve(workspacePackageRoot(workspaceRoot), "nodes");
117
+ const target = path.resolve(base, id, version);
118
+ if (target !== base && !target.startsWith(base + path.sep)) return null;
119
+ return target;
120
+ }
121
+
122
+ function collectFlowDirs(rootDir, source, archived = false) {
123
+ const out = [];
124
+ if (!fs.existsSync(rootDir)) return out;
125
+ let entries = [];
126
+ try {
127
+ entries = fs.readdirSync(rootDir, { withFileTypes: true });
128
+ } catch {
129
+ return out;
130
+ }
131
+ for (const entry of entries) {
132
+ if (!entry.isDirectory() || entry.name === ARCHIVED_PIPELINES_DIR_NAME) continue;
133
+ const dir = path.join(rootDir, entry.name);
134
+ if (!fs.existsSync(path.join(dir, "flow.yaml"))) continue;
135
+ out.push({ flowId: entry.name, flowSource: source, archived, flowDir: dir });
136
+ }
137
+ return out;
138
+ }
139
+
140
+ function listWritableFlowDirs(workspaceRoot, opts = {}) {
141
+ const root = path.resolve(workspaceRoot);
142
+ const userRoot = getUserPipelinesRoot(opts.userId);
143
+ const wsRoot = path.join(root, PIPELINES_DIR);
144
+ const legacyRoot = path.join(root, LEGACY_PIPELINES_DIR);
145
+ return [
146
+ ...collectFlowDirs(userRoot, "user", false),
147
+ ...collectFlowDirs(path.join(userRoot, ARCHIVED_PIPELINES_DIR_NAME), "user", true),
148
+ ...collectFlowDirs(wsRoot, "workspace", false),
149
+ ...collectFlowDirs(path.join(wsRoot, ARCHIVED_PIPELINES_DIR_NAME), "workspace", true),
150
+ ...collectFlowDirs(legacyRoot, "workspace", false),
151
+ ...collectFlowDirs(path.join(legacyRoot, ARCHIVED_PIPELINES_DIR_NAME), "workspace", true),
152
+ ];
153
+ }
154
+
155
+ function depMatchesNode(dep, id, version) {
156
+ if (typeof dep === "string") {
157
+ const parsed = parseMarketplaceDefinitionId(dep.startsWith("marketplace:") ? dep : `marketplace:${dep}`);
158
+ return Boolean(parsed && parsed.id === id && (!parsed.version || parsed.version === version));
159
+ }
160
+ if (!dep || typeof dep !== "object") return false;
161
+ return dep.id === id && (dep.version == null || String(dep.version) === version);
162
+ }
163
+
164
+ function instanceMatchesNode(inst, id, version) {
165
+ const parsed = parseMarketplaceDefinitionId(inst?.definitionId);
166
+ return Boolean(parsed && parsed.id === id && (!parsed.version || parsed.version === version));
167
+ }
168
+
169
+ export function listMarketplaceNodeUsages(workspaceRoot, id, version, opts = {}) {
170
+ const usages = [];
171
+ if (!id || !version) return usages;
172
+ for (const flow of listWritableFlowDirs(workspaceRoot, opts)) {
173
+ const flowYamlPath = path.join(flow.flowDir, "flow.yaml");
174
+ const data = readYamlObject(flowYamlPath);
175
+ if (!data) continue;
176
+ const hits = [];
177
+ const deps = data.dependencies && typeof data.dependencies === "object" ? data.dependencies : {};
178
+ const nodeDeps = Array.isArray(deps.nodes) ? deps.nodes : [];
179
+ for (const dep of nodeDeps) {
180
+ if (depMatchesNode(dep, id, version)) {
181
+ hits.push({ instanceId: "dependencies.nodes", label: "dependency" });
182
+ }
183
+ }
184
+ const instances = data.instances && typeof data.instances === "object" ? data.instances : {};
185
+ for (const [instanceId, inst] of Object.entries(instances)) {
186
+ if (instanceMatchesNode(inst, id, version)) {
187
+ hits.push({ instanceId, label: inst?.label || instanceId });
188
+ }
189
+ }
190
+ if (hits.length > 0) {
191
+ usages.push({
192
+ flowId: flow.flowId,
193
+ flowSource: flow.flowSource,
194
+ archived: flow.archived,
195
+ instances: hits,
196
+ });
197
+ }
198
+ }
199
+ return usages;
200
+ }
201
+
103
202
  function findNodePackageDir(workspaceRoot, id, version) {
104
203
  const root = workspacePackageRoot(workspaceRoot);
105
204
  const nodeBase = path.join(root, "nodes", id);
@@ -230,7 +329,7 @@ export function listMarketplaceNodes(workspaceRoot, flowData = null) {
230
329
  return out.sort((a, b) => a.id.localeCompare(b.id) || a.version.localeCompare(b.version));
231
330
  }
232
331
 
233
- export function listMarketplacePackages(workspaceRoot) {
332
+ export function listMarketplacePackages(workspaceRoot, opts = {}) {
234
333
  const root = workspacePackageRoot(workspaceRoot);
235
334
  const nodes = listMarketplaceNodes(workspaceRoot).map((n) => ({
236
335
  id: n.id,
@@ -242,6 +341,7 @@ export function listMarketplacePackages(workspaceRoot) {
242
341
  outputs: n.output,
243
342
  packagedFiles: Array.isArray(n.packagedFiles) ? n.packagedFiles : [],
244
343
  packageDir: n.packageDir,
344
+ usage: listMarketplaceNodeUsages(workspaceRoot, n.id, n.version, opts),
245
345
  }));
246
346
  const collections = [];
247
347
  const collectionsRoot = path.join(root, "collections");
@@ -265,6 +365,28 @@ export function listMarketplacePackages(workspaceRoot) {
265
365
  return { nodes, collections };
266
366
  }
267
367
 
368
+ export function deleteMarketplaceNodePackage(workspaceRoot, id, version, opts = {}) {
369
+ const packageDir = resolveWorkspaceNodePackageDir(workspaceRoot, id, version);
370
+ if (!packageDir) return { ok: false, error: "Invalid marketplace node id or version" };
371
+ if (!fs.existsSync(path.join(packageDir, NODE_MANIFEST))) {
372
+ return { ok: false, error: `Marketplace node package not found: ${id}@${version}` };
373
+ }
374
+ const usage = listMarketplaceNodeUsages(workspaceRoot, id, version, opts);
375
+ if (usage.length > 0) {
376
+ return { ok: false, error: "Marketplace node is still used by flows", usage };
377
+ }
378
+ fs.rmSync(packageDir, { recursive: true, force: true });
379
+ const versionRoot = path.dirname(packageDir);
380
+ try {
381
+ if (fs.existsSync(versionRoot) && fs.readdirSync(versionRoot).length === 0) {
382
+ fs.rmdirSync(versionRoot);
383
+ }
384
+ } catch {
385
+ /* keep non-empty or unreadable parent */
386
+ }
387
+ return { ok: true, id, version, packageDir };
388
+ }
389
+
268
390
  export function writeFlowMarketplaceLock(workspaceRoot, flowDir, flowData) {
269
391
  if (!flowData || !flowData.instances || typeof flowData.instances !== "object") return null;
270
392
  const nodes = {};
@@ -404,7 +404,7 @@ export async function executeNode(workspaceRoot, flowName, uuid, instanceId, pre
404
404
  if (cli === "api") {
405
405
  await runApiAgentForNode(
406
406
  workspaceRoot,
407
- { promptPath, nodeContext: nodeContext ?? "", taskBody: taskBody ?? "", subagent, instanceId },
407
+ { promptPath, nodeContext: nodeContext ?? "", taskBody: taskBody ?? "", intermediatePath, resultPathRel: resultPath, subagent, instanceId },
408
408
  {
409
409
  model,
410
410
  onToolCall: options.onToolCall,
package/bin/lib/paths.mjs CHANGED
@@ -182,6 +182,10 @@ export function getAgentflowUserConfigAbs() {
182
182
  return path.join(getAgentflowDataRoot(), "config.json");
183
183
  }
184
184
 
185
+ export function getAgentflowUserEnvAbs(userId) {
186
+ return path.join(getAgentflowUserDataRoot(userId), "env.json");
187
+ }
188
+
185
189
  /** CLI / UI 文案用 */
186
190
  export const USER_AGENTFLOW_DIR_LABEL = "~/agentflow";
187
191
  export const USER_AGENTFLOW_PIPELINES_LABEL = "~/agentflow/pipelines";
@@ -253,6 +257,7 @@ export const LOCAL_ONLY_DEFINITION_IDS = new Set([
253
257
  "control_cancelled",
254
258
  "control_interval_loop",
255
259
  "control_cd_workspace",
260
+ "control_user_workspace",
256
261
  "control_load_skills",
257
262
  "control_start",
258
263
  "control_end",
@@ -12,6 +12,7 @@ import {
12
12
  import { getAgentflowUserContexts, getRunDir, PACKAGE_ROOT } from "./paths.mjs";
13
13
  import { isApplyProcessAlive } from "./run-apply-active-lock.mjs";
14
14
  import { log } from "./log.mjs";
15
+ import { readUserEnvObject } from "./user-env.mjs";
15
16
  import { writeResult } from "../pipeline/write-result.mjs";
16
17
 
17
18
  const DEFAULT_POLL_MS = 30_000;
@@ -252,7 +253,7 @@ function startScheduledRun(workspaceRoot, flow, schedule, state, opts = {}) {
252
253
  const child = spawn(process.execPath, args, {
253
254
  cwd: path.resolve(workspaceRoot),
254
255
  stdio: ["ignore", "pipe", "pipe"],
255
- env: { ...process.env, FORCE_COLOR: "0", AGENTFLOW_USER_ID: opts.userId || "" },
256
+ env: { ...process.env, ...readUserEnvObject(opts.userId), FORCE_COLOR: "0", AGENTFLOW_USER_ID: opts.userId || "" },
256
257
  detached: true,
257
258
  });
258
259
 
@@ -325,7 +326,7 @@ function startWaitingRunResume(workspaceRoot, flow, waitState, opts = {}) {
325
326
  const child = spawn(process.execPath, args, {
326
327
  cwd: path.resolve(workspaceRoot),
327
328
  stdio: ["ignore", "pipe", "pipe"],
328
- env: { ...process.env, FORCE_COLOR: "0", AGENTFLOW_USER_ID: opts.userId || "" },
329
+ env: { ...process.env, ...readUserEnvObject(opts.userId), FORCE_COLOR: "0", AGENTFLOW_USER_ID: opts.userId || "" },
329
330
  detached: true,
330
331
  });
331
332
  child.stdout.on("data", () => {});