@fieldwangai/agentflow 0.1.29 → 0.1.31
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.
- package/agents/agentflow-node-executor-code.md +3 -2
- package/agents/agentflow-node-executor-planning.md +3 -2
- package/agents/agentflow-node-executor-requirement.md +3 -2
- package/agents/agentflow-node-executor-test.md +3 -2
- package/agents/agentflow-node-executor-ui.md +3 -2
- package/agents/agentflow-node-executor.md +3 -2
- package/agents/en/agentflow-node-executor.md +3 -2
- package/agents/zh/agentflow-node-executor.md +3 -2
- package/bin/lib/agent-runners.mjs +63 -14
- package/bin/lib/api-runner.mjs +30 -4
- package/bin/lib/apply.mjs +6 -5
- package/bin/lib/auth.mjs +240 -0
- package/bin/lib/catalog-agents.mjs +2 -2
- package/bin/lib/catalog-flows.mjs +196 -17
- package/bin/lib/composer-agent.mjs +22 -1
- package/bin/lib/composer-skill-router.mjs +10 -78
- package/bin/lib/flow-import.mjs +2 -2
- package/bin/lib/flow-write.mjs +20 -20
- package/bin/lib/help.mjs +2 -2
- package/bin/lib/locales/en.json +29 -1
- package/bin/lib/locales/zh.json +31 -3
- package/bin/lib/main.mjs +6 -1
- package/bin/lib/node-exec-context.mjs +5 -5
- package/bin/lib/node-execute.mjs +15 -10
- package/bin/lib/paths.mjs +69 -13
- package/bin/lib/recent-runs.mjs +2 -2
- package/bin/lib/run-node-statuses-from-disk.mjs +3 -3
- package/bin/lib/runtime-context.mjs +225 -0
- package/bin/lib/scheduler.mjs +42 -38
- package/bin/lib/skill-registry.mjs +145 -0
- package/bin/lib/ui-server.mjs +1517 -57
- package/bin/lib/user-env.mjs +83 -0
- package/bin/lib/workspace-tree.mjs +4 -3
- package/bin/lib/workspace.mjs +9 -11
- package/bin/pipeline/build-node-prompt.mjs +29 -4
- package/bin/pipeline/get-env.mjs +5 -29
- package/bin/pipeline/get-exec-id.mjs +2 -2
- package/bin/pipeline/get-resolved-values.mjs +1 -0
- package/bin/pipeline/pre-process-node.mjs +328 -6
- package/bin/pipeline/run-tool-nodejs.mjs +7 -0
- package/bin/pipeline/validate-flow.mjs +2 -0
- package/builtin/nodes/agent_subAgent.md +12 -3
- package/builtin/nodes/control_cd_workspace.md +45 -0
- package/builtin/nodes/control_load_skills.md +50 -0
- package/builtin/nodes/control_user_workspace.md +20 -0
- package/builtin/nodes/display_ascii.md +22 -0
- package/builtin/nodes/display_markdown.md +22 -0
- package/builtin/nodes/display_mermaid.md +22 -0
- package/builtin/nodes/tool_git_checkout.md +57 -0
- package/builtin/nodes/tool_nodejs.md +8 -1
- package/builtin/nodes/tool_print.md +4 -1
- package/builtin/web-ui/dist/assets/index-BVWwQpvg.css +1 -0
- package/builtin/web-ui/dist/assets/index-CvNy1n3f.js +197 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-flow-recipes/SKILL.md +24 -0
- package/skills/agentflow-flow-recipes/references/recipes.md +63 -0
- package/skills/agentflow-node-reference/SKILL.md +25 -0
- package/skills/agentflow-node-reference/references/builtin-nodes.md +210 -0
- package/skills/agentflow-placeholder-reference/SKILL.md +24 -0
- package/skills/agentflow-placeholder-reference/references/placeholders.md +20 -0
- package/skills/agentflow-runtime-reference/SKILL.md +25 -0
- package/skills/agentflow-runtime-reference/references/runtime.md +64 -0
- package/skills/agentflow-workspace-ascii/SKILL.md +42 -0
- package/skills/agentflow-workspace-graph/SKILL.md +67 -0
- package/skills/agentflow-workspace-markdown/SKILL.md +44 -0
- package/skills/agentflow-workspace-mermaid/SKILL.md +43 -0
- package/builtin/web-ui/dist/assets/index-0vJxkTJz.css +0 -1
- package/builtin/web-ui/dist/assets/index-h69bpxLI.js +0 -190
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { spawnSync } from "child_process";
|
|
26
26
|
import fs from "fs";
|
|
27
|
+
import os from "os";
|
|
27
28
|
import path from "path";
|
|
28
29
|
import { fileURLToPath } from "url";
|
|
29
30
|
|
|
@@ -36,7 +37,16 @@ import { parseBool, getFirstBoolInputValue } from "./parse-bool.mjs";
|
|
|
36
37
|
import { writeResult } from "./write-result.mjs";
|
|
37
38
|
import { intermediateResultBasename, intermediateCacheBasename, intermediateDirForNode, outputNodeBasename, outputDirForNode } from "./get-exec-id.mjs";
|
|
38
39
|
import { logToRunTag } from "./run-log.mjs";
|
|
39
|
-
import { getRunDir } from "../lib/paths.mjs";
|
|
40
|
+
import { getRunDir, sanitizeAgentflowUserId } from "../lib/paths.mjs";
|
|
41
|
+
import {
|
|
42
|
+
buildSkillsContext,
|
|
43
|
+
buildSkillsContextFromRegistry,
|
|
44
|
+
expandRuntimePlaceholders,
|
|
45
|
+
normalizeSkillsContext,
|
|
46
|
+
normalizeWorkspaceContext,
|
|
47
|
+
parseSkillKeyList,
|
|
48
|
+
resolveWorkspaceTarget,
|
|
49
|
+
} from "../lib/runtime-context.mjs";
|
|
40
50
|
|
|
41
51
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
42
52
|
|
|
@@ -124,6 +134,12 @@ function bashSingleQuote(s) {
|
|
|
124
134
|
return "'" + String(s).replace(/'/g, "'\\''") + "'";
|
|
125
135
|
}
|
|
126
136
|
|
|
137
|
+
function agentflowCommand() {
|
|
138
|
+
const userId = sanitizeAgentflowUserId(process.env.AGENTFLOW_USER_ID);
|
|
139
|
+
if (!userId) return "agentflow";
|
|
140
|
+
return `AGENTFLOW_USER_ID=${bashSingleQuote(userId)} agentflow`;
|
|
141
|
+
}
|
|
142
|
+
|
|
127
143
|
function parseDurationMs(raw) {
|
|
128
144
|
const text = String(raw || "").trim();
|
|
129
145
|
if (!text) throw new Error("duration is required");
|
|
@@ -209,6 +225,275 @@ function writeOutputSlot(runDir, instanceId, execId, slotName, value) {
|
|
|
209
225
|
fs.writeFileSync(p, String(value ?? "") + "\n", "utf-8");
|
|
210
226
|
}
|
|
211
227
|
|
|
228
|
+
function readFlowJsonObject(workspaceRoot, flowName, uuid) {
|
|
229
|
+
const flowJsonPath = path.join(getRunDir(workspaceRoot, flowName, uuid), "intermediate", "flow.json");
|
|
230
|
+
if (!fs.existsSync(flowJsonPath)) return null;
|
|
231
|
+
try {
|
|
232
|
+
const flow = JSON.parse(fs.readFileSync(flowJsonPath, "utf-8"));
|
|
233
|
+
return flow && typeof flow === "object" ? flow : null;
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId) {
|
|
240
|
+
const flowJson = readFlowJsonObject(workspaceRoot, flowName, uuid);
|
|
241
|
+
const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
|
|
242
|
+
const inputs = data.ok ? (data.resolvedInputs || {}) : {};
|
|
243
|
+
const workspaceContext = normalizeWorkspaceContext(inputs.workspaceContext, workspaceRoot, flowName, flowJson);
|
|
244
|
+
const skillsContext = normalizeSkillsContext(inputs.skillsContext);
|
|
245
|
+
return { inputs, workspaceContext, skillsContext, flowJson };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function sanitizeRepoDirName(repoUrl) {
|
|
249
|
+
const raw = String(repoUrl || "").trim().replace(/\.git$/i, "");
|
|
250
|
+
const last = raw.split(/[/:]/).filter(Boolean).pop() || "repo";
|
|
251
|
+
return last.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function runGit(args, cwd) {
|
|
255
|
+
return spawnSync("git", args, {
|
|
256
|
+
cwd,
|
|
257
|
+
encoding: "utf-8",
|
|
258
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isTruthyInput(value) {
|
|
263
|
+
const text = String(value ?? "").trim().toLowerCase();
|
|
264
|
+
return text === "true" || text === "1" || text === "yes" || text === "y" || text === "on";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function emitGitCheckoutNode(workspaceRoot, flowName, uuid, instanceId, execId, resultPathRel) {
|
|
268
|
+
const runDir = getRunDir(workspaceRoot, flowName, uuid);
|
|
269
|
+
const { inputs, workspaceContext } = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
|
|
270
|
+
const repoUrl = String(inputs.repoUrl || inputs.url || "").trim();
|
|
271
|
+
if (!repoUrl) throw new Error("tool_git_checkout: repoUrl is required");
|
|
272
|
+
const branch = String(inputs.branch || "").trim();
|
|
273
|
+
const pullIfExists = String(inputs.pullIfExists ?? "true").trim().toLowerCase() !== "false";
|
|
274
|
+
const includeSubmodules = isTruthyInput(inputs.includeSubmodules ?? inputs.submodules ?? inputs.pullSubmodules);
|
|
275
|
+
const defaultDir = path.join(workspaceContext.pipelineWorkspace || workspaceRoot, ".workspace", "agentflow", "git-repos", sanitizeRepoDirName(repoUrl));
|
|
276
|
+
const targetRaw = String(inputs.targetDir || "").trim();
|
|
277
|
+
const targetDir = targetRaw
|
|
278
|
+
? resolveWorkspaceTarget(targetRaw, workspaceContext, { repoName: sanitizeRepoDirName(repoUrl) })
|
|
279
|
+
: defaultDir;
|
|
280
|
+
|
|
281
|
+
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
282
|
+
let changed = false;
|
|
283
|
+
let action = "clone";
|
|
284
|
+
if (fs.existsSync(path.join(targetDir, ".git"))) {
|
|
285
|
+
action = pullIfExists ? "pull" : "exists";
|
|
286
|
+
if (pullIfExists) {
|
|
287
|
+
const fetch = runGit(["fetch", "--all", "--prune"], targetDir);
|
|
288
|
+
if (fetch.status !== 0) throw new Error(`git fetch failed: ${fetch.stderr || fetch.stdout}`);
|
|
289
|
+
if (branch) {
|
|
290
|
+
const checkout = runGit(["checkout", branch], targetDir);
|
|
291
|
+
if (checkout.status !== 0) throw new Error(`git checkout failed: ${checkout.stderr || checkout.stdout}`);
|
|
292
|
+
}
|
|
293
|
+
const before = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
|
|
294
|
+
const pull = runGit(["pull", "--ff-only"], targetDir);
|
|
295
|
+
if (pull.status !== 0) throw new Error(`git pull failed: ${pull.stderr || pull.stdout}`);
|
|
296
|
+
const after = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
|
|
297
|
+
changed = before !== after;
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
const args = ["clone"];
|
|
301
|
+
if (includeSubmodules) args.push("--recurse-submodules");
|
|
302
|
+
if (branch) args.push("--branch", branch);
|
|
303
|
+
args.push(repoUrl, targetDir);
|
|
304
|
+
const clone = runGit(args, workspaceContext.cwd || workspaceRoot);
|
|
305
|
+
if (clone.status !== 0) throw new Error(`git clone failed: ${clone.stderr || clone.stdout}`);
|
|
306
|
+
changed = true;
|
|
307
|
+
}
|
|
308
|
+
if (includeSubmodules) {
|
|
309
|
+
const submodule = runGit(["submodule", "update", "--init", "--recursive"], targetDir);
|
|
310
|
+
if (submodule.status !== 0) throw new Error(`git submodule update failed: ${submodule.stderr || submodule.stdout}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const currentBranch = runGit(["rev-parse", "--abbrev-ref", "HEAD"], targetDir).stdout.trim();
|
|
314
|
+
const commit = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
|
|
315
|
+
const outWorkspaceContext = {
|
|
316
|
+
version: 1,
|
|
317
|
+
label: inputs.label || sanitizeRepoDirName(repoUrl),
|
|
318
|
+
cwd: path.resolve(targetDir),
|
|
319
|
+
workspaceRoot: path.resolve(targetDir),
|
|
320
|
+
pipelineWorkspace: workspaceContext.pipelineWorkspace || path.resolve(workspaceRoot),
|
|
321
|
+
flowDir: workspaceContext.flowDir,
|
|
322
|
+
previous: workspaceContext,
|
|
323
|
+
};
|
|
324
|
+
writeOutputSlot(runDir, instanceId, execId, "repoPath", targetDir);
|
|
325
|
+
writeOutputSlot(runDir, instanceId, execId, "branch", currentBranch);
|
|
326
|
+
writeOutputSlot(runDir, instanceId, execId, "commit", commit);
|
|
327
|
+
writeOutputSlot(runDir, instanceId, execId, "changed", changed ? "true" : "false");
|
|
328
|
+
writeOutputSlot(runDir, instanceId, execId, "workspaceContext", JSON.stringify(outWorkspaceContext));
|
|
329
|
+
writeResult(workspaceRoot, flowName, uuid, instanceId, { status: "success", message: `git ${action}: ${currentBranch}@${commit.slice(0, 8)}` }, { execId });
|
|
330
|
+
return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "git-checkout", `Git checkout completed: ${targetDir}\n`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function emitCdWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId) {
|
|
334
|
+
const runDir = getRunDir(workspaceRoot, flowName, uuid);
|
|
335
|
+
const { inputs, workspaceContext } = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
|
|
336
|
+
const mode = String(inputs.mode || "set").trim().toLowerCase();
|
|
337
|
+
let next;
|
|
338
|
+
if (mode === "pop") {
|
|
339
|
+
next = normalizeWorkspaceContext(workspaceContext.previous, workspaceRoot, flowName);
|
|
340
|
+
} else {
|
|
341
|
+
const target = resolveWorkspaceTarget(inputs.path || inputs.target || inputs.repoPath || "", workspaceContext);
|
|
342
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
|
|
343
|
+
throw new Error(`control_cd_workspace: path directory not found: ${target}`);
|
|
344
|
+
}
|
|
345
|
+
next = {
|
|
346
|
+
version: 1,
|
|
347
|
+
label: String(inputs.label || path.basename(target) || "workspace").trim(),
|
|
348
|
+
cwd: path.resolve(target),
|
|
349
|
+
workspaceRoot: path.resolve(target),
|
|
350
|
+
pipelineWorkspace: workspaceContext.pipelineWorkspace || path.resolve(workspaceRoot),
|
|
351
|
+
flowDir: workspaceContext.flowDir,
|
|
352
|
+
previous: mode === "push" ? workspaceContext : workspaceContext.previous || null,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
writeOutputSlot(runDir, instanceId, execId, "workspaceContext", JSON.stringify(next));
|
|
356
|
+
writeOutputSlot(runDir, instanceId, execId, "cwd", next.cwd);
|
|
357
|
+
writeOutputSlot(runDir, instanceId, execId, "previous", next.previous ? JSON.stringify(next.previous) : "");
|
|
358
|
+
writeResult(workspaceRoot, flowName, uuid, instanceId, { status: "success", message: `cwd=${next.cwd}` }, { execId });
|
|
359
|
+
return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "cd-workspace", `Workspace context switched to: ${next.cwd}\n`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function emitUserWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId) {
|
|
363
|
+
const runDir = getRunDir(workspaceRoot, flowName, uuid);
|
|
364
|
+
const { workspaceContext } = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
|
|
365
|
+
const homeDir = path.resolve(os.homedir());
|
|
366
|
+
const next = {
|
|
367
|
+
version: 1,
|
|
368
|
+
label: "home",
|
|
369
|
+
cwd: homeDir,
|
|
370
|
+
workspaceRoot: homeDir,
|
|
371
|
+
pipelineWorkspace: workspaceContext.pipelineWorkspace || path.resolve(workspaceRoot),
|
|
372
|
+
flowDir: workspaceContext.flowDir,
|
|
373
|
+
previous: workspaceContext,
|
|
374
|
+
};
|
|
375
|
+
writeOutputSlot(runDir, instanceId, execId, "workspaceContext", JSON.stringify(next));
|
|
376
|
+
writeOutputSlot(runDir, instanceId, execId, "cwd", next.cwd);
|
|
377
|
+
writeResult(workspaceRoot, flowName, uuid, instanceId, { status: "success", message: `user workspace: ${homeDir}` }, { execId });
|
|
378
|
+
return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "user-workspace", `Workspace context switched to user home: ${homeDir}\n`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function emitLoadSkillsNode(workspaceRoot, flowName, uuid, instanceId, execId) {
|
|
382
|
+
const runDir = getRunDir(workspaceRoot, flowName, uuid);
|
|
383
|
+
const { inputs, workspaceContext, skillsContext: existingSkills } = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
|
|
384
|
+
const mergeMode = String(inputs.mergeMode || "replace").trim();
|
|
385
|
+
const skillKeys = parseSkillKeyList(inputs.skillKeys || inputs.skills || inputs.keys || "");
|
|
386
|
+
let source = "public-registry";
|
|
387
|
+
let loaded;
|
|
388
|
+
if (skillKeys.length > 0) {
|
|
389
|
+
loaded = buildSkillsContextFromRegistry({ workspaceContext, skillKeys, mergeMode });
|
|
390
|
+
} else {
|
|
391
|
+
source = String(inputs.source || "current-workspace").trim();
|
|
392
|
+
const paths = String(inputs.paths || "")
|
|
393
|
+
.split(/\r?\n|,/)
|
|
394
|
+
.map((x) => expandRuntimePlaceholders(x, workspaceContext).trim())
|
|
395
|
+
.filter(Boolean);
|
|
396
|
+
const include = String(inputs.include || "").split(/[\s,]+/).map((x) => x.trim()).filter(Boolean);
|
|
397
|
+
const exclude = String(inputs.exclude || "").split(/[\s,]+/).map((x) => x.trim()).filter(Boolean);
|
|
398
|
+
loaded = buildSkillsContext({ workspaceContext, source, paths, include, exclude, mergeMode });
|
|
399
|
+
}
|
|
400
|
+
let next = loaded;
|
|
401
|
+
if (existingSkills && mergeMode !== "replace") {
|
|
402
|
+
const existingBodies = Array.isArray(existingSkills.skillBodies) ? existingSkills.skillBodies : [];
|
|
403
|
+
const loadedBodies = Array.isArray(loaded.skillBodies) ? loaded.skillBodies : [];
|
|
404
|
+
const skillBodies = mergeMode === "prepend" ? [...loadedBodies, ...existingBodies] : [...existingBodies, ...loadedBodies];
|
|
405
|
+
const seen = new Set();
|
|
406
|
+
next = {
|
|
407
|
+
...loaded,
|
|
408
|
+
skillBodies: skillBodies.filter((s) => {
|
|
409
|
+
const key = s.key || s.name;
|
|
410
|
+
if (seen.has(key)) return false;
|
|
411
|
+
seen.add(key);
|
|
412
|
+
return true;
|
|
413
|
+
}),
|
|
414
|
+
};
|
|
415
|
+
next.skills = next.skillBodies.map(({ body, ...meta }) => meta);
|
|
416
|
+
next.skillKeys = next.skills.map((s) => s.key);
|
|
417
|
+
next.loadedCount = next.skills.length;
|
|
418
|
+
}
|
|
419
|
+
writeOutputSlot(runDir, instanceId, execId, "skillsContext", JSON.stringify(next));
|
|
420
|
+
writeOutputSlot(runDir, instanceId, execId, "loadedCount", String(next.loadedCount || 0));
|
|
421
|
+
writeOutputSlot(runDir, instanceId, execId, "summary", `${next.loadedCount || 0} skills loaded from ${source}`);
|
|
422
|
+
writeResult(workspaceRoot, flowName, uuid, instanceId, { status: "success", message: `加载 ${next.loadedCount || 0} 个 skills` }, { execId });
|
|
423
|
+
return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "load-skills", `Loaded ${next.loadedCount || 0} skills.\n`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function readPrintableValue(value, runDir) {
|
|
427
|
+
const text = String(value ?? "").trim();
|
|
428
|
+
if (!text) return "";
|
|
429
|
+
const candidates = [];
|
|
430
|
+
if (path.isAbsolute(text)) candidates.push(text);
|
|
431
|
+
candidates.push(path.join(runDir, text));
|
|
432
|
+
for (const candidate of candidates) {
|
|
433
|
+
try {
|
|
434
|
+
if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) continue;
|
|
435
|
+
const stat = fs.statSync(candidate);
|
|
436
|
+
if (stat.size > 1024 * 1024) {
|
|
437
|
+
return fs.readFileSync(candidate, "utf-8").slice(0, 1024 * 1024) + "\n\n[内容超过 1MB,已截断]";
|
|
438
|
+
}
|
|
439
|
+
return fs.readFileSync(candidate, "utf-8").trim();
|
|
440
|
+
} catch (_) {}
|
|
441
|
+
}
|
|
442
|
+
return text;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function readResultBody(runDir, sourceInstanceId) {
|
|
446
|
+
const id = String(sourceInstanceId || "").trim();
|
|
447
|
+
if (!id) return "";
|
|
448
|
+
const resultPath = path.join(runDir, intermediateDirForNode(id), intermediateResultBasename(id, 1));
|
|
449
|
+
try {
|
|
450
|
+
if (!fs.existsSync(resultPath)) return "";
|
|
451
|
+
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
452
|
+
const match = raw.match(/---\s*\r?\n[\s\S]*?\r?\n---\s*\r?\n([\s\S]*)$/);
|
|
453
|
+
return (match ? match[1] : raw).trim();
|
|
454
|
+
} catch (_) {
|
|
455
|
+
return "";
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function emitToolPrintNode(workspaceRoot, flowName, uuid, instanceId, execId) {
|
|
460
|
+
const runDir = getRunDir(workspaceRoot, flowName, uuid);
|
|
461
|
+
const flowJson = readFlowJsonObject(workspaceRoot, flowName, uuid);
|
|
462
|
+
const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
|
|
463
|
+
const inputs = data.ok ? (data.resolvedInputs || {}) : {};
|
|
464
|
+
const skipNames = new Set(["prev", "next", "workspaceContext", "skillsContext", "workspaceRoot", "pipelineWorkspace", "flowName", "runDir", "flowDir", "cwd"]);
|
|
465
|
+
|
|
466
|
+
let content = readPrintableValue(inputs.content, runDir);
|
|
467
|
+
if (!content) {
|
|
468
|
+
const parts = [];
|
|
469
|
+
for (const [name, value] of Object.entries(inputs)) {
|
|
470
|
+
if (skipNames.has(name)) continue;
|
|
471
|
+
const v = readPrintableValue(value, runDir);
|
|
472
|
+
if (!v) continue;
|
|
473
|
+
parts.push(name === "content" ? v : `## ${name}\n\n${v}`);
|
|
474
|
+
}
|
|
475
|
+
content = parts.join("\n\n").trim();
|
|
476
|
+
}
|
|
477
|
+
if (!content && inputs.prev) {
|
|
478
|
+
content = readResultBody(runDir, inputs.prev);
|
|
479
|
+
}
|
|
480
|
+
if (!content && flowJson?.nodes) {
|
|
481
|
+
const node = flowJson.nodes.find((n) => n.id === instanceId);
|
|
482
|
+
content = String(node?.body || "").trim();
|
|
483
|
+
}
|
|
484
|
+
if (!content) content = "(tool_print 没有可展示内容:请填写 content 输入,或连接上游输出到 content。)";
|
|
485
|
+
|
|
486
|
+
writeResult(
|
|
487
|
+
workspaceRoot,
|
|
488
|
+
flowName,
|
|
489
|
+
uuid,
|
|
490
|
+
instanceId,
|
|
491
|
+
{ status: "success", message: "Print 输出" },
|
|
492
|
+
{ execId, preserveBody: false, body: content },
|
|
493
|
+
);
|
|
494
|
+
return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "tool-print", `Printed content for ${instanceId}.\n`);
|
|
495
|
+
}
|
|
496
|
+
|
|
212
497
|
function writeWaitState(runDir, state) {
|
|
213
498
|
const legacyPath = path.join(runDir, "wait-state.json");
|
|
214
499
|
const registryPath = path.join(runDir, "wait-states.json");
|
|
@@ -300,16 +585,17 @@ function emitLoadSaveKeyOptionalPrompt(workspaceRoot, flowName, uuid, instanceId
|
|
|
300
585
|
const rootArg = workspaceRoot;
|
|
301
586
|
const q = bashSingleQuote;
|
|
302
587
|
const keyQ = q(key);
|
|
588
|
+
const af = agentflowCommand();
|
|
303
589
|
const directCommand =
|
|
304
590
|
definitionId === "tool_get_env"
|
|
305
|
-
?
|
|
591
|
+
? `${af} apply -ai get-env ${q(rootArg)} ${q(flowName)} ${q(uuid)} ${q(instanceId)} ${q(String(execId))} ${keyQ}`
|
|
306
592
|
: (() => {
|
|
307
593
|
const scriptArgs =
|
|
308
594
|
definitionId === "tool_load_key"
|
|
309
595
|
? `${q(rootArg)} ${q(flowName)} ${q(uuid)} ${keyQ}`
|
|
310
596
|
: `${q(rootArg)} ${q(flowName)} ${q(uuid)} ${keyQ} ${q(value)}`;
|
|
311
597
|
const scriptPath = path.join(__dirname, definitionId === "tool_load_key" ? "load-key.mjs" : "save-key.mjs");
|
|
312
|
-
return
|
|
598
|
+
return `${af} apply -ai run-tool-nodejs ${q(rootArg)} ${q(flowName)} ${q(uuid)} ${q(instanceId)} ${q(String(execId))} -- node ${q(scriptPath)} ${scriptArgs}`;
|
|
313
599
|
})();
|
|
314
600
|
const content = `此节点不调用 subagent,请主 agent 在工作区根目录直接执行以下命令完成该节点。
|
|
315
601
|
|
|
@@ -342,7 +628,7 @@ function emitToolNodejsDirectCommand(workspaceRoot, flowName, uuid, instanceId,
|
|
|
342
628
|
const promptPath = path.join(nodeIntermediateDir, promptFileName);
|
|
343
629
|
|
|
344
630
|
const q = bashSingleQuote;
|
|
345
|
-
const directCommand =
|
|
631
|
+
const directCommand = `${agentflowCommand()} apply -ai run-tool-nodejs ${q(workspaceRoot)} ${q(flowName)} ${q(uuid)} ${q(instanceId)} ${q(String(execId))} -- ${resolvedScript}`;
|
|
346
632
|
const content = `此节点为 tool_nodejs(直接执行模式),不调用 subagent,由流水线直接执行以下命令。
|
|
347
633
|
|
|
348
634
|
\`\`\`bash
|
|
@@ -376,7 +662,7 @@ function emitAnyOneOptionalPrompt(workspaceRoot, flowName, uuid, instanceId, exe
|
|
|
376
662
|
message: "任一前驱已就绪,直接通过",
|
|
377
663
|
execId,
|
|
378
664
|
});
|
|
379
|
-
const directCommand =
|
|
665
|
+
const directCommand = `${agentflowCommand()} apply -ai write-result ${bashSingleQuote(workspaceRoot)} ${bashSingleQuote(flowName)} ${bashSingleQuote(uuid)} ${bashSingleQuote(instanceId)} --json ${bashSingleQuote(jsonPayload)}`;
|
|
380
666
|
const content = `此节点为 control_anyOne,不调用 subagent。请主 agent 在工作区根目录直接执行以下命令将该节点标记为 success。
|
|
381
667
|
|
|
382
668
|
\`\`\`bash
|
|
@@ -715,7 +1001,41 @@ function main() {
|
|
|
715
1001
|
return;
|
|
716
1002
|
}
|
|
717
1003
|
|
|
718
|
-
|
|
1004
|
+
if (definitionId === "tool_git_checkout" || definitionId === "control_cd_workspace" || definitionId === "control_user_workspace" || definitionId === "control_load_skills" || definitionId === "tool_print") {
|
|
1005
|
+
try {
|
|
1006
|
+
const promptPath =
|
|
1007
|
+
definitionId === "tool_git_checkout"
|
|
1008
|
+
? emitGitCheckoutNode(workspaceRoot, flowName, uuid, instanceId, execId, resultPathRel)
|
|
1009
|
+
: definitionId === "control_cd_workspace"
|
|
1010
|
+
? emitCdWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId)
|
|
1011
|
+
: definitionId === "control_user_workspace"
|
|
1012
|
+
? emitUserWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId)
|
|
1013
|
+
: definitionId === "control_load_skills"
|
|
1014
|
+
? emitLoadSkillsNode(workspaceRoot, flowName, uuid, instanceId, execId)
|
|
1015
|
+
: emitToolPrintNode(workspaceRoot, flowName, uuid, instanceId, execId);
|
|
1016
|
+
writeCacheJsonForNode(workspaceRoot, flowName, uuid, instanceId, execId);
|
|
1017
|
+
logToRunTag(workspaceRoot, flowName, uuid, "pre-process", { event: "runtime-context-node", instanceId, definitionId });
|
|
1018
|
+
console.log(JSON.stringify({
|
|
1019
|
+
ok: true,
|
|
1020
|
+
promptPath,
|
|
1021
|
+
resultPath: resultPathRel,
|
|
1022
|
+
execId,
|
|
1023
|
+
subagent: "agentflow-node-executor",
|
|
1024
|
+
optionalPromptPath: promptPath,
|
|
1025
|
+
definitionId,
|
|
1026
|
+
}));
|
|
1027
|
+
return;
|
|
1028
|
+
} catch (e) {
|
|
1029
|
+
console.error(JSON.stringify({ ok: false, error: `${definitionId}: ${e.message || e}` }));
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const runtimeContexts = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
|
|
1035
|
+
const data = buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execId, {
|
|
1036
|
+
workspaceContext: runtimeContexts.workspaceContext,
|
|
1037
|
+
skillsContext: runtimeContexts.skillsContext,
|
|
1038
|
+
});
|
|
719
1039
|
if (!data.ok) {
|
|
720
1040
|
console.error(JSON.stringify({ ok: false, error: data.error || "build-node-prompt failed" }));
|
|
721
1041
|
process.exit(1);
|
|
@@ -746,6 +1066,8 @@ function main() {
|
|
|
746
1066
|
definitionId,
|
|
747
1067
|
role,
|
|
748
1068
|
};
|
|
1069
|
+
if (data.workspaceContext) output.workspaceContext = data.workspaceContext;
|
|
1070
|
+
if (data.skillsContext) output.skillsContext = data.skillsContext;
|
|
749
1071
|
if (model) output.model = model;
|
|
750
1072
|
if (data.optionalPromptPath) {
|
|
751
1073
|
output.optionalPromptPath = data.optionalPromptPath;
|
|
@@ -19,6 +19,7 @@ import path from "path";
|
|
|
19
19
|
import { fileURLToPath } from "url";
|
|
20
20
|
|
|
21
21
|
import { getRunDir } from "../lib/paths.mjs";
|
|
22
|
+
import { readUserEnvObject } from "../lib/user-env.mjs";
|
|
22
23
|
import { validateAndParse } from "./validate-script-output.mjs";
|
|
23
24
|
import { writeResult } from "./write-result.mjs";
|
|
24
25
|
import { loadExecId, outputNodeBasename, outputDirForNode } from "./get-exec-id.mjs";
|
|
@@ -29,6 +30,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
29
30
|
const MAX_RETRIES = 3;
|
|
30
31
|
const RETRY_DELAY_MS = 1000;
|
|
31
32
|
|
|
33
|
+
function runtimeEnv() {
|
|
34
|
+
return { ...process.env, ...readUserEnvObject(process.env.AGENTFLOW_USER_ID || "") };
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
function runOnce(workspaceRoot, flowName, uuid, instanceId, execId, scriptArgs) {
|
|
33
38
|
const runDir = getRunDir(workspaceRoot, flowName, uuid);
|
|
34
39
|
const outputDir = path.join(runDir, outputDirForNode(instanceId));
|
|
@@ -41,11 +46,13 @@ function runOnce(workspaceRoot, flowName, uuid, instanceId, execId, scriptArgs)
|
|
|
41
46
|
cwd: workspaceRoot,
|
|
42
47
|
shell: false,
|
|
43
48
|
stdio: ["inherit", "pipe", "pipe"],
|
|
49
|
+
env: runtimeEnv(),
|
|
44
50
|
})
|
|
45
51
|
: spawnSync(normalizedCmd, [], {
|
|
46
52
|
cwd: workspaceRoot,
|
|
47
53
|
shell: true,
|
|
48
54
|
stdio: ["inherit", "pipe", "pipe"],
|
|
55
|
+
env: runtimeEnv(),
|
|
49
56
|
});
|
|
50
57
|
|
|
51
58
|
const stdout = child.stdout?.toString("utf-8") ?? "";
|
|
@@ -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,14 +1,23 @@
|
|
|
1
1
|
---
|
|
2
|
-
#
|
|
3
|
-
description:
|
|
4
|
-
displayName:
|
|
2
|
+
# 内置节点:子 Agent
|
|
3
|
+
description: 利用子 Agent 执行任务;可接收 workspaceContext 切换执行工作区,并接收 skillsContext 注入已加载 skills。
|
|
4
|
+
displayName: 子 Agent
|
|
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
|
|
12
18
|
default: ""
|
|
19
|
+
- type: text
|
|
20
|
+
name: result
|
|
21
|
+
default: ""
|
|
13
22
|
---
|
|
14
23
|
${USER_PROMPT}
|
|
@@ -0,0 +1,45 @@
|
|
|
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 path, keep previous stack unchanged.
|
|
8
|
+
- `push`: switch to path and save the incoming context as previous.
|
|
9
|
+
- `pop`: restore the previous context.
|
|
10
|
+
|
|
11
|
+
`path` supports `${workspaceRoot}`, `${pipelineWorkspace}`, `${flowDir}`, absolute paths, and paths relative to the input workspace context.
|
|
12
|
+
displayName: CD Workspace
|
|
13
|
+
input:
|
|
14
|
+
- type: node
|
|
15
|
+
name: prev
|
|
16
|
+
default: ""
|
|
17
|
+
- type: text
|
|
18
|
+
name: path
|
|
19
|
+
default: ""
|
|
20
|
+
required: true
|
|
21
|
+
showOnNode: true
|
|
22
|
+
- type: text
|
|
23
|
+
name: mode
|
|
24
|
+
default: "set"
|
|
25
|
+
- type: text
|
|
26
|
+
name: label
|
|
27
|
+
default: ""
|
|
28
|
+
- type: text
|
|
29
|
+
name: workspaceContext
|
|
30
|
+
default: ""
|
|
31
|
+
output:
|
|
32
|
+
- type: node
|
|
33
|
+
name: next
|
|
34
|
+
default: ""
|
|
35
|
+
- type: text
|
|
36
|
+
name: workspaceContext
|
|
37
|
+
default: ""
|
|
38
|
+
- type: file
|
|
39
|
+
name: cwd
|
|
40
|
+
default: ""
|
|
41
|
+
- type: text
|
|
42
|
+
name: previous
|
|
43
|
+
default: ""
|
|
44
|
+
---
|
|
45
|
+
Switch downstream execution to `${path}` using mode `${mode}`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
# 内置节点:Load Skills
|
|
3
|
+
description: |
|
|
4
|
+
Load selected public Skills from the AgentFlow skill registry into the current workspace context.
|
|
5
|
+
|
|
6
|
+
Connect `workspaceContext` from CD Workspace, set `skillKeys` to skill names or registry keys, then connect `skillsContext` to downstream agent/tool nodes. Loaded skills are injected into the node prompt under "已加载 Skills" while downstream execution still uses the CD Workspace context.
|
|
7
|
+
|
|
8
|
+
Skill key examples:
|
|
9
|
+
- `agentflow-flow-add-instances`
|
|
10
|
+
- `workspace-agents:agentflow-flow-edit-node-fields`
|
|
11
|
+
- `global-codex:some-skill`
|
|
12
|
+
|
|
13
|
+
Merge modes:
|
|
14
|
+
- `replace`
|
|
15
|
+
- `append`
|
|
16
|
+
- `prepend`
|
|
17
|
+
displayName: Load Skills
|
|
18
|
+
input:
|
|
19
|
+
- type: node
|
|
20
|
+
name: prev
|
|
21
|
+
default: ""
|
|
22
|
+
- type: text
|
|
23
|
+
name: skillKeys
|
|
24
|
+
default: ""
|
|
25
|
+
required: true
|
|
26
|
+
showOnNode: true
|
|
27
|
+
- type: text
|
|
28
|
+
name: mergeMode
|
|
29
|
+
default: "replace"
|
|
30
|
+
- type: text
|
|
31
|
+
name: workspaceContext
|
|
32
|
+
default: ""
|
|
33
|
+
- type: text
|
|
34
|
+
name: skillsContext
|
|
35
|
+
default: ""
|
|
36
|
+
output:
|
|
37
|
+
- type: node
|
|
38
|
+
name: next
|
|
39
|
+
default: ""
|
|
40
|
+
- type: text
|
|
41
|
+
name: skillsContext
|
|
42
|
+
default: ""
|
|
43
|
+
- type: text
|
|
44
|
+
name: loadedCount
|
|
45
|
+
default: ""
|
|
46
|
+
- type: text
|
|
47
|
+
name: summary
|
|
48
|
+
default: ""
|
|
49
|
+
---
|
|
50
|
+
Load public skills `${skillKeys}` into `${workspaceContext}` and pass them to downstream nodes.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Built-in node: User Workspace
|
|
3
|
+
description: Output a workspace context pointing to the current user's home directory.
|
|
4
|
+
displayName: User Workspace
|
|
5
|
+
input:
|
|
6
|
+
- type: node
|
|
7
|
+
name: prev
|
|
8
|
+
default: ""
|
|
9
|
+
output:
|
|
10
|
+
- type: node
|
|
11
|
+
name: next
|
|
12
|
+
default: ""
|
|
13
|
+
- type: text
|
|
14
|
+
name: workspaceContext
|
|
15
|
+
default: ""
|
|
16
|
+
- type: file
|
|
17
|
+
name: cwd
|
|
18
|
+
default: ""
|
|
19
|
+
---
|
|
20
|
+
Use the current user's home directory as downstream workspace context.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
# 内置节点:ASCII 图展示
|
|
3
|
+
description: Display ASCII diagram content in workspace canvas; passes diagram text downstream as text
|
|
4
|
+
displayName: ASCII Display
|
|
5
|
+
input:
|
|
6
|
+
- type: node
|
|
7
|
+
name: prev
|
|
8
|
+
default: ""
|
|
9
|
+
- type: text
|
|
10
|
+
name: content
|
|
11
|
+
default: ""
|
|
12
|
+
required: true
|
|
13
|
+
showOnNode: true
|
|
14
|
+
output:
|
|
15
|
+
- type: text
|
|
16
|
+
name: content
|
|
17
|
+
default: ""
|
|
18
|
+
- type: node
|
|
19
|
+
name: next
|
|
20
|
+
default: ""
|
|
21
|
+
---
|
|
22
|
+
${content}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
# 内置节点:Markdown 展示
|
|
3
|
+
description: Display Markdown content in workspace canvas; passes content downstream as text
|
|
4
|
+
displayName: Markdown Display
|
|
5
|
+
input:
|
|
6
|
+
- type: node
|
|
7
|
+
name: prev
|
|
8
|
+
default: ""
|
|
9
|
+
- type: text
|
|
10
|
+
name: content
|
|
11
|
+
default: ""
|
|
12
|
+
required: true
|
|
13
|
+
showOnNode: true
|
|
14
|
+
output:
|
|
15
|
+
- type: text
|
|
16
|
+
name: content
|
|
17
|
+
default: ""
|
|
18
|
+
- type: node
|
|
19
|
+
name: next
|
|
20
|
+
default: ""
|
|
21
|
+
---
|
|
22
|
+
${content}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
# 内置节点:Mermaid 展示
|
|
3
|
+
description: Display Mermaid diagram source in workspace canvas; passes diagram source downstream as text
|
|
4
|
+
displayName: Mermaid Display
|
|
5
|
+
input:
|
|
6
|
+
- type: node
|
|
7
|
+
name: prev
|
|
8
|
+
default: ""
|
|
9
|
+
- type: text
|
|
10
|
+
name: content
|
|
11
|
+
default: ""
|
|
12
|
+
required: true
|
|
13
|
+
showOnNode: true
|
|
14
|
+
output:
|
|
15
|
+
- type: text
|
|
16
|
+
name: content
|
|
17
|
+
default: ""
|
|
18
|
+
- type: node
|
|
19
|
+
name: next
|
|
20
|
+
default: ""
|
|
21
|
+
---
|
|
22
|
+
${content}
|