@fieldwangai/agentflow 0.1.29 → 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 (60) 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 +1 -1
  51. package/skills/agentflow-flow-recipes/SKILL.md +24 -0
  52. package/skills/agentflow-flow-recipes/references/recipes.md +63 -0
  53. package/skills/agentflow-node-reference/SKILL.md +25 -0
  54. package/skills/agentflow-node-reference/references/builtin-nodes.md +210 -0
  55. package/skills/agentflow-placeholder-reference/SKILL.md +24 -0
  56. package/skills/agentflow-placeholder-reference/references/placeholders.md +20 -0
  57. package/skills/agentflow-runtime-reference/SKILL.md +25 -0
  58. package/skills/agentflow-runtime-reference/references/runtime.md +64 -0
  59. package/builtin/web-ui/dist/assets/index-0vJxkTJz.css +0 -1
  60. package/builtin/web-ui/dist/assets/index-h69bpxLI.js +0 -190
@@ -221,13 +221,14 @@ function readFilesRecursive(dir, baseDir, maxDepth = 2, currentDepth = 0) {
221
221
  }
222
222
  }
223
223
 
224
- export function getPipelineFiles(workspaceRoot, flowId, flowSource, archived = false) {
224
+ export function getPipelineFiles(workspaceRoot, flowId, flowSource, archived = false, opts = {}) {
225
225
  const root = path.resolve(workspaceRoot);
226
226
  let pipelineDir = null;
227
+ const userPipelinesRoot = getUserPipelinesRoot(opts.userId);
227
228
 
228
229
  if (archived) {
229
230
  if (flowSource === "user") {
230
- pipelineDir = path.join(getUserPipelinesRoot(), ARCHIVED_PIPELINES_DIR_NAME, flowId);
231
+ pipelineDir = path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId);
231
232
  } else if (flowSource === "workspace") {
232
233
  pipelineDir = path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowId);
233
234
  if (!fs.existsSync(pipelineDir)) {
@@ -239,7 +240,7 @@ export function getPipelineFiles(workspaceRoot, flowId, flowSource, archived = f
239
240
  if (flowSource === "builtin") {
240
241
  pipelineDir = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId);
241
242
  } else if (flowSource === "user") {
242
- pipelineDir = path.join(getUserPipelinesRoot(), flowId);
243
+ pipelineDir = path.join(userPipelinesRoot, flowId);
243
244
  if (!fs.existsSync(pipelineDir)) {
244
245
  const alt = path.join(root, PIPELINES_DIR, flowId);
245
246
  if (fs.existsSync(alt)) pipelineDir = alt;
@@ -9,6 +9,7 @@ import {
9
9
  getReferenceRootAbs,
10
10
  getWorkspaceRunBuildRoot,
11
11
  getUserPipelinesRoot,
12
+ resolveUniqueUserPipelineDir,
12
13
  ARCHIVED_PIPELINES_DIR_NAME,
13
14
  } from "./paths.mjs";
14
15
 
@@ -23,7 +24,7 @@ export { getRunDir } from "./paths.mjs";
23
24
  * - legacyUserRoot: ~/agentflow/runBuild/<name>/<uuid>
24
25
  * 后两者仅作为向下兼容读取;新写入只走 user/workspace。
25
26
  */
26
- export function listAllRunDirs(workspaceRoot) {
27
+ export function listAllRunDirs(workspaceRoot, opts = {}) {
27
28
  const root = path.resolve(workspaceRoot);
28
29
  const out = [];
29
30
  const seen = new Set();
@@ -62,7 +63,7 @@ export function listAllRunDirs(workspaceRoot) {
62
63
  };
63
64
 
64
65
  // 新位置(优先)
65
- scanPipelinesDir(getUserPipelinesRoot(), "user");
66
+ scanPipelinesDir(getUserPipelinesRoot(opts.userId), "user");
66
67
  scanPipelinesDir(path.join(root, PIPELINES_DIR), "workspace");
67
68
 
68
69
  // 旧位置(兼容读)
@@ -138,25 +139,22 @@ export function listRunsWithLogs(workspaceRoot) {
138
139
  return list;
139
140
  }
140
141
 
141
- /** 解析 flow 目录:~/agentflow/pipelines.workspace/agentflow/pipelines.cursor/agentflow/pipelines(旧)→ builtin/pipelines */
142
- export function getFlowDir(workspaceRoot, flowName) {
142
+ /** 解析活跃 flow 目录:user → workspace → legacy workspace → builtin。归档 flow 必须走显式 archived resolver。 */
143
+ export function getFlowDir(workspaceRoot, flowName, opts = {}) {
143
144
  const root = path.resolve(workspaceRoot);
144
145
  const hasFlow = (dir) => fs.existsSync(dir) && fs.existsSync(path.join(dir, "flow.yaml"));
145
146
 
146
147
  // user pipelines
147
- const userRoot = getUserPipelinesRoot();
148
+ const userRoot = getUserPipelinesRoot(opts.userId);
148
149
  const userFlowDir = path.join(userRoot, flowName);
149
150
  if (hasFlow(userFlowDir)) return userFlowDir;
150
- // user archived
151
- const userArchivedDir = path.join(userRoot, ARCHIVED_PIPELINES_DIR_NAME, flowName);
152
- if (hasFlow(userArchivedDir)) return userArchivedDir;
151
+
152
+ const inferredUserFlowDir = resolveUniqueUserPipelineDir(flowName);
153
+ if (inferredUserFlowDir) return inferredUserFlowDir;
153
154
 
154
155
  // workspace pipelines
155
156
  const wsFlowDir = path.join(root, PIPELINES_DIR, flowName);
156
157
  if (hasFlow(wsFlowDir)) return wsFlowDir;
157
- // workspace archived
158
- const wsArchivedDir = path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowName);
159
- if (hasFlow(wsArchivedDir)) return wsArchivedDir;
160
158
 
161
159
  // legacy
162
160
  const legacyFlowDir = path.join(root, LEGACY_PIPELINES_DIR, flowName);
@@ -16,6 +16,7 @@ import { loadFlowDefinition } from "./parse-flow.mjs";
16
16
  import { getResolvedValues, getOutputPathForSlot } from "./get-resolved-values.mjs";
17
17
  import { loadExecId } from "./get-exec-id.mjs";
18
18
  import { intermediatePromptBasename, intermediateDirForNode } from "./get-exec-id.mjs";
19
+ import { normalizeSkillsContext, normalizeWorkspaceContext, renderSkillsContextForPrompt } from "../lib/runtime-context.mjs";
19
20
 
20
21
  function shellQuote(s) {
21
22
  if (s == null) return "''";
@@ -116,7 +117,7 @@ function marketplaceRuntimeCommand(marketplaceNode, resolvedInputs, resolvedOutp
116
117
  * @param {number} [execId] - 本轮 execId,缺省则从 memory 读取
117
118
  * @returns {{ ok: boolean, promptPath?: string, nodeContext?: string, taskBody?: string, error?: string }}
118
119
  */
119
- export function buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execId) {
120
+ export function buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execId, opts = {}) {
120
121
  const runDir = getRunDir(workspaceRoot, flowName, uuid);
121
122
  const flowJsonPath = path.join(runDir, "intermediate", "flow.json");
122
123
  let flowDir = getFlowDir(workspaceRoot, flowName) || path.join(workspaceRoot, PIPELINES_DIR, flowName);
@@ -154,7 +155,26 @@ export function buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execI
154
155
  : "";
155
156
 
156
157
  const { resolvedInputs = {}, resolvedOutputs = {}, systemPrompt = "" } = data;
157
- const resolveOpts = { instanceId, currentExecId: e, runDir, workspaceRoot };
158
+ const workspaceContext = normalizeWorkspaceContext(opts.workspaceContext || resolvedInputs.workspaceContext, workspaceRoot, flowName, { flowDir });
159
+ const skillsContext = normalizeSkillsContext(opts.skillsContext || resolvedInputs.skillsContext);
160
+ if (workspaceContext?.workspaceRoot) {
161
+ resolvedInputs.workspaceRoot = workspaceContext.workspaceRoot;
162
+ resolvedInputs.cwd = workspaceContext.cwd || workspaceContext.workspaceRoot;
163
+ resolvedInputs.pipelineWorkspace = workspaceContext.pipelineWorkspace || path.resolve(workspaceRoot);
164
+ }
165
+ const resolveOpts = {
166
+ instanceId,
167
+ currentExecId: e,
168
+ runDir,
169
+ workspaceRoot: workspaceContext?.workspaceRoot || workspaceRoot,
170
+ extra: {
171
+ workspaceRoot: workspaceContext?.workspaceRoot || path.resolve(workspaceRoot),
172
+ cwd: workspaceContext?.cwd || workspaceContext?.workspaceRoot || path.resolve(workspaceRoot),
173
+ pipelineWorkspace: workspaceContext?.pipelineWorkspace || path.resolve(workspaceRoot),
174
+ flowDir: path.resolve(flowDir),
175
+ runDir,
176
+ },
177
+ };
158
178
  const taskBody = resolvePlaceholdersInText(
159
179
  instanceBody,
160
180
  resolvedInputs,
@@ -168,9 +188,12 @@ export function buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execI
168
188
  ? marketplaceRuntimeCommand(marketplaceNode, resolvedInputs, resolvedOutputs, resolveOpts)
169
189
  : "";
170
190
 
191
+ const skillsPrompt = renderSkillsContextForPrompt(skillsContext);
192
+ const contextBlocks = [systemPrompt || "(无)", skillsPrompt].filter((x) => x && String(x).trim());
193
+
171
194
  const content = `## 节点上下文
172
195
 
173
- ${systemPrompt || "(无)"}
196
+ ${contextBlocks.join("\n\n")}
174
197
 
175
198
  ## 执行任务
176
199
 
@@ -191,9 +214,11 @@ ${taskBody || "(无)"}
191
214
  return {
192
215
  ok: true,
193
216
  promptPath: relativePath.replace(/\\/g, "/"),
194
- nodeContext: systemPrompt || "",
217
+ nodeContext: contextBlocks.join("\n\n") || "",
195
218
  taskBody: taskBody || "",
196
219
  script: resolvedScript || "",
220
+ workspaceContext,
221
+ skillsContext,
197
222
  };
198
223
  }
199
224
 
@@ -53,8 +53,8 @@ export function loadExecId(workspaceRoot, flowName, uuid, instanceId) {
53
53
  /**
54
54
  * 从 memory 读取所有 order 中节点的 execId。
55
55
  */
56
- export function loadAllExecIds(workspaceRoot, flowName, uuid, order) {
57
- const runDir = getRunDir(workspaceRoot, flowName, uuid);
56
+ export function loadAllExecIds(workspaceRoot, flowName, uuid, order, opts = {}) {
57
+ const runDir = getRunDir(workspaceRoot, flowName, uuid, opts);
58
58
  const memoryPath = path.join(runDir, MEMORY_FILENAME);
59
59
  const map = fs.existsSync(memoryPath)
60
60
  ? parseMemory(fs.readFileSync(memoryPath, "utf-8"))
@@ -233,6 +233,7 @@ export function getResolvedValues(workspaceRoot, flowName, uuid, instanceId) {
233
233
  // 运行时常量放在后面,确保不会被 input 槽位的空值覆盖
234
234
  const runtimeConstants = {
235
235
  workspaceRoot: path.resolve(workspaceRoot),
236
+ pipelineWorkspace: path.resolve(workspaceRoot),
236
237
  flowName,
237
238
  runDir: runDirRel,
238
239
  flowDir: path.resolve(flowDir),
@@ -36,7 +36,16 @@ import { parseBool, getFirstBoolInputValue } from "./parse-bool.mjs";
36
36
  import { writeResult } from "./write-result.mjs";
37
37
  import { intermediateResultBasename, intermediateCacheBasename, intermediateDirForNode, outputNodeBasename, outputDirForNode } from "./get-exec-id.mjs";
38
38
  import { logToRunTag } from "./run-log.mjs";
39
- import { getRunDir } from "../lib/paths.mjs";
39
+ import { getRunDir, sanitizeAgentflowUserId } from "../lib/paths.mjs";
40
+ import {
41
+ buildSkillsContext,
42
+ buildSkillsContextFromRegistry,
43
+ expandRuntimePlaceholders,
44
+ normalizeSkillsContext,
45
+ normalizeWorkspaceContext,
46
+ parseSkillKeyList,
47
+ resolveWorkspaceTarget,
48
+ } from "../lib/runtime-context.mjs";
40
49
 
41
50
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
42
51
 
@@ -124,6 +133,12 @@ function bashSingleQuote(s) {
124
133
  return "'" + String(s).replace(/'/g, "'\\''") + "'";
125
134
  }
126
135
 
136
+ function agentflowCommand() {
137
+ const userId = sanitizeAgentflowUserId(process.env.AGENTFLOW_USER_ID);
138
+ if (!userId) return "agentflow";
139
+ return `AGENTFLOW_USER_ID=${bashSingleQuote(userId)} agentflow`;
140
+ }
141
+
127
142
  function parseDurationMs(raw) {
128
143
  const text = String(raw || "").trim();
129
144
  if (!text) throw new Error("duration is required");
@@ -209,6 +224,256 @@ function writeOutputSlot(runDir, instanceId, execId, slotName, value) {
209
224
  fs.writeFileSync(p, String(value ?? "") + "\n", "utf-8");
210
225
  }
211
226
 
227
+ function readFlowJsonObject(workspaceRoot, flowName, uuid) {
228
+ const flowJsonPath = path.join(getRunDir(workspaceRoot, flowName, uuid), "intermediate", "flow.json");
229
+ if (!fs.existsSync(flowJsonPath)) return null;
230
+ try {
231
+ const flow = JSON.parse(fs.readFileSync(flowJsonPath, "utf-8"));
232
+ return flow && typeof flow === "object" ? flow : null;
233
+ } catch {
234
+ return null;
235
+ }
236
+ }
237
+
238
+ function resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId) {
239
+ const flowJson = readFlowJsonObject(workspaceRoot, flowName, uuid);
240
+ const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
241
+ const inputs = data.ok ? (data.resolvedInputs || {}) : {};
242
+ const workspaceContext = normalizeWorkspaceContext(inputs.workspaceContext, workspaceRoot, flowName, flowJson);
243
+ const skillsContext = normalizeSkillsContext(inputs.skillsContext);
244
+ return { inputs, workspaceContext, skillsContext, flowJson };
245
+ }
246
+
247
+ function sanitizeRepoDirName(repoUrl) {
248
+ const raw = String(repoUrl || "").trim().replace(/\.git$/i, "");
249
+ const last = raw.split(/[/:]/).filter(Boolean).pop() || "repo";
250
+ return last.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
251
+ }
252
+
253
+ function runGit(args, cwd) {
254
+ return spawnSync("git", args, {
255
+ cwd,
256
+ encoding: "utf-8",
257
+ stdio: ["ignore", "pipe", "pipe"],
258
+ });
259
+ }
260
+
261
+ function isTruthyInput(value) {
262
+ const text = String(value ?? "").trim().toLowerCase();
263
+ return text === "true" || text === "1" || text === "yes" || text === "y" || text === "on";
264
+ }
265
+
266
+ function emitGitCheckoutNode(workspaceRoot, flowName, uuid, instanceId, execId, resultPathRel) {
267
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
268
+ const { inputs, workspaceContext } = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
269
+ const repoUrl = String(inputs.repoUrl || inputs.url || "").trim();
270
+ if (!repoUrl) throw new Error("tool_git_checkout: repoUrl is required");
271
+ const branch = String(inputs.branch || "").trim();
272
+ const pullIfExists = String(inputs.pullIfExists ?? "true").trim().toLowerCase() !== "false";
273
+ const includeSubmodules = isTruthyInput(inputs.includeSubmodules ?? inputs.submodules ?? inputs.pullSubmodules);
274
+ const defaultDir = path.join(workspaceContext.pipelineWorkspace || workspaceRoot, ".workspace", "agentflow", "git-repos", sanitizeRepoDirName(repoUrl));
275
+ const targetRaw = String(inputs.targetDir || "").trim();
276
+ const targetDir = targetRaw
277
+ ? resolveWorkspaceTarget(targetRaw, workspaceContext, { repoName: sanitizeRepoDirName(repoUrl) })
278
+ : defaultDir;
279
+
280
+ fs.mkdirSync(path.dirname(targetDir), { recursive: true });
281
+ let changed = false;
282
+ let action = "clone";
283
+ if (fs.existsSync(path.join(targetDir, ".git"))) {
284
+ action = pullIfExists ? "pull" : "exists";
285
+ if (pullIfExists) {
286
+ const fetch = runGit(["fetch", "--all", "--prune"], targetDir);
287
+ if (fetch.status !== 0) throw new Error(`git fetch failed: ${fetch.stderr || fetch.stdout}`);
288
+ if (branch) {
289
+ const checkout = runGit(["checkout", branch], targetDir);
290
+ if (checkout.status !== 0) throw new Error(`git checkout failed: ${checkout.stderr || checkout.stdout}`);
291
+ }
292
+ const before = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
293
+ const pull = runGit(["pull", "--ff-only"], targetDir);
294
+ if (pull.status !== 0) throw new Error(`git pull failed: ${pull.stderr || pull.stdout}`);
295
+ const after = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
296
+ changed = before !== after;
297
+ }
298
+ } else {
299
+ const args = ["clone"];
300
+ if (includeSubmodules) args.push("--recurse-submodules");
301
+ if (branch) args.push("--branch", branch);
302
+ args.push(repoUrl, targetDir);
303
+ const clone = runGit(args, workspaceContext.cwd || workspaceRoot);
304
+ if (clone.status !== 0) throw new Error(`git clone failed: ${clone.stderr || clone.stdout}`);
305
+ changed = true;
306
+ }
307
+ if (includeSubmodules) {
308
+ const submodule = runGit(["submodule", "update", "--init", "--recursive"], targetDir);
309
+ if (submodule.status !== 0) throw new Error(`git submodule update failed: ${submodule.stderr || submodule.stdout}`);
310
+ }
311
+
312
+ const currentBranch = runGit(["rev-parse", "--abbrev-ref", "HEAD"], targetDir).stdout.trim();
313
+ const commit = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
314
+ const outWorkspaceContext = {
315
+ version: 1,
316
+ label: inputs.label || sanitizeRepoDirName(repoUrl),
317
+ cwd: path.resolve(targetDir),
318
+ workspaceRoot: path.resolve(targetDir),
319
+ pipelineWorkspace: workspaceContext.pipelineWorkspace || path.resolve(workspaceRoot),
320
+ flowDir: workspaceContext.flowDir,
321
+ previous: workspaceContext,
322
+ };
323
+ writeOutputSlot(runDir, instanceId, execId, "repoPath", targetDir);
324
+ writeOutputSlot(runDir, instanceId, execId, "branch", currentBranch);
325
+ writeOutputSlot(runDir, instanceId, execId, "commit", commit);
326
+ writeOutputSlot(runDir, instanceId, execId, "changed", changed ? "true" : "false");
327
+ writeOutputSlot(runDir, instanceId, execId, "workspaceContext", JSON.stringify(outWorkspaceContext));
328
+ writeResult(workspaceRoot, flowName, uuid, instanceId, { status: "success", message: `git ${action}: ${currentBranch}@${commit.slice(0, 8)}` }, { execId });
329
+ return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "git-checkout", `Git checkout completed: ${targetDir}\n`);
330
+ }
331
+
332
+ function emitCdWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId) {
333
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
334
+ const { inputs, workspaceContext } = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
335
+ const mode = String(inputs.mode || "set").trim().toLowerCase();
336
+ let next;
337
+ if (mode === "pop") {
338
+ next = normalizeWorkspaceContext(workspaceContext.previous, workspaceRoot, flowName);
339
+ } else {
340
+ const target = resolveWorkspaceTarget(inputs.target || inputs.path || inputs.repoPath || "", workspaceContext);
341
+ if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
342
+ throw new Error(`control_cd_workspace: target directory not found: ${target}`);
343
+ }
344
+ next = {
345
+ version: 1,
346
+ label: String(inputs.label || path.basename(target) || "workspace").trim(),
347
+ cwd: path.resolve(target),
348
+ workspaceRoot: path.resolve(target),
349
+ pipelineWorkspace: workspaceContext.pipelineWorkspace || path.resolve(workspaceRoot),
350
+ flowDir: workspaceContext.flowDir,
351
+ previous: mode === "push" ? workspaceContext : workspaceContext.previous || null,
352
+ };
353
+ }
354
+ writeOutputSlot(runDir, instanceId, execId, "workspaceContext", JSON.stringify(next));
355
+ writeOutputSlot(runDir, instanceId, execId, "cwd", next.cwd);
356
+ writeOutputSlot(runDir, instanceId, execId, "previous", next.previous ? JSON.stringify(next.previous) : "");
357
+ writeResult(workspaceRoot, flowName, uuid, instanceId, { status: "success", message: `cwd=${next.cwd}` }, { execId });
358
+ return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "cd-workspace", `Workspace context switched to: ${next.cwd}\n`);
359
+ }
360
+
361
+ function emitLoadSkillsNode(workspaceRoot, flowName, uuid, instanceId, execId) {
362
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
363
+ const { inputs, workspaceContext, skillsContext: existingSkills } = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
364
+ const mergeMode = String(inputs.mergeMode || "replace").trim();
365
+ const skillKeys = parseSkillKeyList(inputs.skillKeys || inputs.skills || inputs.keys || "");
366
+ let source = "public-registry";
367
+ let loaded;
368
+ if (skillKeys.length > 0) {
369
+ loaded = buildSkillsContextFromRegistry({ workspaceContext, skillKeys, mergeMode });
370
+ } else {
371
+ source = String(inputs.source || "current-workspace").trim();
372
+ const paths = String(inputs.paths || "")
373
+ .split(/\r?\n|,/)
374
+ .map((x) => expandRuntimePlaceholders(x, workspaceContext).trim())
375
+ .filter(Boolean);
376
+ const include = String(inputs.include || "").split(/[\s,]+/).map((x) => x.trim()).filter(Boolean);
377
+ const exclude = String(inputs.exclude || "").split(/[\s,]+/).map((x) => x.trim()).filter(Boolean);
378
+ loaded = buildSkillsContext({ workspaceContext, source, paths, include, exclude, mergeMode });
379
+ }
380
+ let next = loaded;
381
+ if (existingSkills && mergeMode !== "replace") {
382
+ const existingBodies = Array.isArray(existingSkills.skillBodies) ? existingSkills.skillBodies : [];
383
+ const loadedBodies = Array.isArray(loaded.skillBodies) ? loaded.skillBodies : [];
384
+ const skillBodies = mergeMode === "prepend" ? [...loadedBodies, ...existingBodies] : [...existingBodies, ...loadedBodies];
385
+ const seen = new Set();
386
+ next = {
387
+ ...loaded,
388
+ skillBodies: skillBodies.filter((s) => {
389
+ const key = s.key || s.name;
390
+ if (seen.has(key)) return false;
391
+ seen.add(key);
392
+ return true;
393
+ }),
394
+ };
395
+ next.skills = next.skillBodies.map(({ body, ...meta }) => meta);
396
+ next.skillKeys = next.skills.map((s) => s.key);
397
+ next.loadedCount = next.skills.length;
398
+ }
399
+ writeOutputSlot(runDir, instanceId, execId, "skillsContext", JSON.stringify(next));
400
+ writeOutputSlot(runDir, instanceId, execId, "loadedCount", String(next.loadedCount || 0));
401
+ writeOutputSlot(runDir, instanceId, execId, "summary", `${next.loadedCount || 0} skills loaded from ${source}`);
402
+ writeResult(workspaceRoot, flowName, uuid, instanceId, { status: "success", message: `加载 ${next.loadedCount || 0} 个 skills` }, { execId });
403
+ return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "load-skills", `Loaded ${next.loadedCount || 0} skills.\n`);
404
+ }
405
+
406
+ function readPrintableValue(value, runDir) {
407
+ const text = String(value ?? "").trim();
408
+ if (!text) return "";
409
+ const candidates = [];
410
+ if (path.isAbsolute(text)) candidates.push(text);
411
+ candidates.push(path.join(runDir, text));
412
+ for (const candidate of candidates) {
413
+ try {
414
+ if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) continue;
415
+ const stat = fs.statSync(candidate);
416
+ if (stat.size > 1024 * 1024) {
417
+ return fs.readFileSync(candidate, "utf-8").slice(0, 1024 * 1024) + "\n\n[内容超过 1MB,已截断]";
418
+ }
419
+ return fs.readFileSync(candidate, "utf-8").trim();
420
+ } catch (_) {}
421
+ }
422
+ return text;
423
+ }
424
+
425
+ function readResultBody(runDir, sourceInstanceId) {
426
+ const id = String(sourceInstanceId || "").trim();
427
+ if (!id) return "";
428
+ const resultPath = path.join(runDir, intermediateDirForNode(id), intermediateResultBasename(id, 1));
429
+ try {
430
+ if (!fs.existsSync(resultPath)) return "";
431
+ const raw = fs.readFileSync(resultPath, "utf-8");
432
+ const match = raw.match(/---\s*\r?\n[\s\S]*?\r?\n---\s*\r?\n([\s\S]*)$/);
433
+ return (match ? match[1] : raw).trim();
434
+ } catch (_) {
435
+ return "";
436
+ }
437
+ }
438
+
439
+ function emitToolPrintNode(workspaceRoot, flowName, uuid, instanceId, execId) {
440
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
441
+ const flowJson = readFlowJsonObject(workspaceRoot, flowName, uuid);
442
+ const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
443
+ const inputs = data.ok ? (data.resolvedInputs || {}) : {};
444
+ const skipNames = new Set(["prev", "next", "workspaceContext", "skillsContext", "workspaceRoot", "pipelineWorkspace", "flowName", "runDir", "flowDir", "cwd"]);
445
+
446
+ let content = readPrintableValue(inputs.content, runDir);
447
+ if (!content) {
448
+ const parts = [];
449
+ for (const [name, value] of Object.entries(inputs)) {
450
+ if (skipNames.has(name)) continue;
451
+ const v = readPrintableValue(value, runDir);
452
+ if (!v) continue;
453
+ parts.push(name === "content" ? v : `## ${name}\n\n${v}`);
454
+ }
455
+ content = parts.join("\n\n").trim();
456
+ }
457
+ if (!content && inputs.prev) {
458
+ content = readResultBody(runDir, inputs.prev);
459
+ }
460
+ if (!content && flowJson?.nodes) {
461
+ const node = flowJson.nodes.find((n) => n.id === instanceId);
462
+ content = String(node?.body || "").trim();
463
+ }
464
+ if (!content) content = "(tool_print 没有可展示内容:请填写 content 输入,或连接上游输出到 content。)";
465
+
466
+ writeResult(
467
+ workspaceRoot,
468
+ flowName,
469
+ uuid,
470
+ instanceId,
471
+ { status: "success", message: "Print 输出" },
472
+ { execId, preserveBody: false, body: content },
473
+ );
474
+ return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "tool-print", `Printed content for ${instanceId}.\n`);
475
+ }
476
+
212
477
  function writeWaitState(runDir, state) {
213
478
  const legacyPath = path.join(runDir, "wait-state.json");
214
479
  const registryPath = path.join(runDir, "wait-states.json");
@@ -300,16 +565,17 @@ function emitLoadSaveKeyOptionalPrompt(workspaceRoot, flowName, uuid, instanceId
300
565
  const rootArg = workspaceRoot;
301
566
  const q = bashSingleQuote;
302
567
  const keyQ = q(key);
568
+ const af = agentflowCommand();
303
569
  const directCommand =
304
570
  definitionId === "tool_get_env"
305
- ? `agentflow apply -ai get-env ${q(rootArg)} ${q(flowName)} ${q(uuid)} ${q(instanceId)} ${q(String(execId))} ${keyQ}`
571
+ ? `${af} apply -ai get-env ${q(rootArg)} ${q(flowName)} ${q(uuid)} ${q(instanceId)} ${q(String(execId))} ${keyQ}`
306
572
  : (() => {
307
573
  const scriptArgs =
308
574
  definitionId === "tool_load_key"
309
575
  ? `${q(rootArg)} ${q(flowName)} ${q(uuid)} ${keyQ}`
310
576
  : `${q(rootArg)} ${q(flowName)} ${q(uuid)} ${keyQ} ${q(value)}`;
311
577
  const scriptPath = path.join(__dirname, definitionId === "tool_load_key" ? "load-key.mjs" : "save-key.mjs");
312
- return `agentflow apply -ai run-tool-nodejs ${q(rootArg)} ${q(flowName)} ${q(uuid)} ${q(instanceId)} ${q(String(execId))} -- node ${q(scriptPath)} ${scriptArgs}`;
578
+ return `${af} apply -ai run-tool-nodejs ${q(rootArg)} ${q(flowName)} ${q(uuid)} ${q(instanceId)} ${q(String(execId))} -- node ${q(scriptPath)} ${scriptArgs}`;
313
579
  })();
314
580
  const content = `此节点不调用 subagent,请主 agent 在工作区根目录直接执行以下命令完成该节点。
315
581
 
@@ -342,7 +608,7 @@ function emitToolNodejsDirectCommand(workspaceRoot, flowName, uuid, instanceId,
342
608
  const promptPath = path.join(nodeIntermediateDir, promptFileName);
343
609
 
344
610
  const q = bashSingleQuote;
345
- const directCommand = `agentflow apply -ai run-tool-nodejs ${q(workspaceRoot)} ${q(flowName)} ${q(uuid)} ${q(instanceId)} ${q(String(execId))} -- ${resolvedScript}`;
611
+ const directCommand = `${agentflowCommand()} apply -ai run-tool-nodejs ${q(workspaceRoot)} ${q(flowName)} ${q(uuid)} ${q(instanceId)} ${q(String(execId))} -- ${resolvedScript}`;
346
612
  const content = `此节点为 tool_nodejs(直接执行模式),不调用 subagent,由流水线直接执行以下命令。
347
613
 
348
614
  \`\`\`bash
@@ -376,7 +642,7 @@ function emitAnyOneOptionalPrompt(workspaceRoot, flowName, uuid, instanceId, exe
376
642
  message: "任一前驱已就绪,直接通过",
377
643
  execId,
378
644
  });
379
- const directCommand = `agentflow apply -ai write-result ${bashSingleQuote(workspaceRoot)} ${bashSingleQuote(flowName)} ${bashSingleQuote(uuid)} ${bashSingleQuote(instanceId)} --json ${bashSingleQuote(jsonPayload)}`;
645
+ const directCommand = `${agentflowCommand()} apply -ai write-result ${bashSingleQuote(workspaceRoot)} ${bashSingleQuote(flowName)} ${bashSingleQuote(uuid)} ${bashSingleQuote(instanceId)} --json ${bashSingleQuote(jsonPayload)}`;
380
646
  const content = `此节点为 control_anyOne,不调用 subagent。请主 agent 在工作区根目录直接执行以下命令将该节点标记为 success。
381
647
 
382
648
  \`\`\`bash
@@ -715,7 +981,39 @@ function main() {
715
981
  return;
716
982
  }
717
983
 
718
- const data = buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execId);
984
+ if (definitionId === "tool_git_checkout" || definitionId === "control_cd_workspace" || definitionId === "control_load_skills" || definitionId === "tool_print") {
985
+ try {
986
+ const promptPath =
987
+ definitionId === "tool_git_checkout"
988
+ ? emitGitCheckoutNode(workspaceRoot, flowName, uuid, instanceId, execId, resultPathRel)
989
+ : definitionId === "control_cd_workspace"
990
+ ? emitCdWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId)
991
+ : definitionId === "control_load_skills"
992
+ ? emitLoadSkillsNode(workspaceRoot, flowName, uuid, instanceId, execId)
993
+ : emitToolPrintNode(workspaceRoot, flowName, uuid, instanceId, execId);
994
+ writeCacheJsonForNode(workspaceRoot, flowName, uuid, instanceId, execId);
995
+ logToRunTag(workspaceRoot, flowName, uuid, "pre-process", { event: "runtime-context-node", instanceId, definitionId });
996
+ console.log(JSON.stringify({
997
+ ok: true,
998
+ promptPath,
999
+ resultPath: resultPathRel,
1000
+ execId,
1001
+ subagent: "agentflow-node-executor",
1002
+ optionalPromptPath: promptPath,
1003
+ definitionId,
1004
+ }));
1005
+ return;
1006
+ } catch (e) {
1007
+ console.error(JSON.stringify({ ok: false, error: `${definitionId}: ${e.message || e}` }));
1008
+ process.exit(1);
1009
+ }
1010
+ }
1011
+
1012
+ const runtimeContexts = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
1013
+ const data = buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execId, {
1014
+ workspaceContext: runtimeContexts.workspaceContext,
1015
+ skillsContext: runtimeContexts.skillsContext,
1016
+ });
719
1017
  if (!data.ok) {
720
1018
  console.error(JSON.stringify({ ok: false, error: data.error || "build-node-prompt failed" }));
721
1019
  process.exit(1);
@@ -746,6 +1044,8 @@ function main() {
746
1044
  definitionId,
747
1045
  role,
748
1046
  };
1047
+ if (data.workspaceContext) output.workspaceContext = data.workspaceContext;
1048
+ if (data.skillsContext) output.skillsContext = data.skillsContext;
749
1049
  if (model) output.model = model;
750
1050
  if (data.optionalPromptPath) {
751
1051
  output.optionalPromptPath = data.optionalPromptPath;
@@ -17,6 +17,7 @@ import { getFlowDir } from "../lib/workspace.mjs";
17
17
  /** 槽位合法 type 集合(英文为 builtin/nodes 标准;中文为遗留兼容,新写代码统一英文) */
18
18
  const VALID_SLOT_TYPES = new Set(["node", "text", "file", "bool", "节点", "文本", "文件", "布尔"]);
19
19
  const CANONICAL_SLOT_TYPES = "node|text|file|bool";
20
+ const RUNTIME_CONTEXT_INPUT_NAMES = new Set(["workspaceContext", "skillsContext"]);
20
21
 
21
22
  /** 与前端 flowFormat.VALID_ROLES + 内置 id 一致 */
22
23
  const VALID_ROLE_KEYS = ["requirement", "planning", "code", "test", "normal"];
@@ -470,6 +471,7 @@ function checkFlowCore(nodes, edges, flowDir, nodeIdToSlots, getNodeBody, instan
470
471
  const slotName = (slot && slot.name != null ? String(slot.name).trim() : "");
471
472
  const slotType = normType((slot && slot.type != null ? String(slot.type).trim() : ""));
472
473
  if (!slotName || slotType === "node") continue;
474
+ if (RUNTIME_CONTEXT_INPUT_NAMES.has(slotName)) continue;
473
475
  if (!phSet.has(slotName) && !phSet.has(`input.${slotName}`)) {
474
476
  errors.push(
475
477
  `节点 "${n.id}"(${defId})script 未引用 input 引脚 "${slotName}"(type: ${slotType}),` +
@@ -1,11 +1,17 @@
1
1
  ---
2
2
  # 内置节点:SubAgent
3
- description: 利用SubAgent执行任务
3
+ description: 利用 SubAgent 执行任务;可接收 workspaceContext 切换执行工作区,并接收 skillsContext 注入已加载 skills。
4
4
  displayName: SubAgent
5
5
  input:
6
6
  - type: node
7
7
  name: prev
8
8
  default: ""
9
+ - type: text
10
+ name: workspaceContext
11
+ default: ""
12
+ - type: text
13
+ name: skillsContext
14
+ default: ""
9
15
  output:
10
16
  - type: node
11
17
  name: next
@@ -0,0 +1,43 @@
1
+ ---
2
+ # 内置节点:CD Workspace
3
+ description: |
4
+ Switch the runtime workspace context for downstream nodes without changing the AgentFlow pipeline workspace.
5
+
6
+ Modes:
7
+ - `set`: switch to target, keep previous stack unchanged.
8
+ - `push`: switch to target and save the incoming context as previous.
9
+ - `pop`: restore the previous context.
10
+
11
+ `target` supports `${workspaceRoot}`, `${pipelineWorkspace}`, `${flowDir}`, absolute paths, and paths relative to current workspace context.
12
+ displayName: CD Workspace
13
+ input:
14
+ - type: node
15
+ name: prev
16
+ default: ""
17
+ - type: text
18
+ name: target
19
+ default: "${pipelineWorkspace}"
20
+ - type: text
21
+ name: mode
22
+ default: "set"
23
+ - type: text
24
+ name: label
25
+ default: ""
26
+ - type: text
27
+ name: workspaceContext
28
+ default: ""
29
+ output:
30
+ - type: node
31
+ name: next
32
+ default: ""
33
+ - type: text
34
+ name: workspaceContext
35
+ default: ""
36
+ - type: file
37
+ name: cwd
38
+ default: ""
39
+ - type: text
40
+ name: previous
41
+ default: ""
42
+ ---
43
+ Switch downstream execution to `${target}` using mode `${mode}`.