@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.
- package/bin/lib/agent-runners.mjs +26 -2
- package/bin/lib/api-runner.mjs +26 -3
- package/bin/lib/apply.mjs +6 -5
- package/bin/lib/catalog-flows.mjs +30 -5
- package/bin/lib/composer-agent.mjs +2 -1
- package/bin/lib/locales/en.json +4 -0
- package/bin/lib/locales/zh.json +6 -2
- package/bin/lib/marketplace.mjs +124 -2
- package/bin/lib/node-execute.mjs +1 -1
- package/bin/lib/paths.mjs +5 -0
- package/bin/lib/scheduler.mjs +3 -2
- package/bin/lib/ui-server.mjs +639 -8
- package/bin/lib/user-env.mjs +83 -0
- package/bin/pipeline/get-env.mjs +5 -29
- package/bin/pipeline/pre-process-node.mjs +28 -6
- package/bin/pipeline/run-tool-nodejs.mjs +7 -0
- package/builtin/nodes/agent_subAgent.md +6 -3
- package/builtin/nodes/control_cd_workspace.md +8 -6
- package/builtin/nodes/control_load_skills.md +2 -0
- package/builtin/nodes/control_user_workspace.md +20 -0
- package/builtin/nodes/display_ascii.md +5 -0
- package/builtin/nodes/display_markdown.md +5 -0
- package/builtin/nodes/display_mermaid.md +5 -0
- package/builtin/nodes/tool_git_checkout.md +3 -0
- package/builtin/web-ui/dist/assets/index-D0Tkhqr6.css +1 -0
- package/builtin/web-ui/dist/assets/index-DyhW5chp.js +197 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- 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-NdVOJLL9.js +0 -196
- 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
|
-
|
|
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
|
-
|
|
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
|
});
|
package/bin/lib/api-runner.mjs
CHANGED
|
@@ -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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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 内容注入辅助 ─────────────────────────────────────────────────
|
package/bin/lib/locales/en.json
CHANGED
|
@@ -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"
|
package/bin/lib/locales/zh.json
CHANGED
|
@@ -218,8 +218,8 @@
|
|
|
218
218
|
"description": "AgentFlow 的结束节点,执行完成后流程结束"
|
|
219
219
|
},
|
|
220
220
|
"agent_subAgent": {
|
|
221
|
-
"displayName": "
|
|
222
|
-
"description": "
|
|
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"
|
package/bin/lib/marketplace.mjs
CHANGED
|
@@ -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 {
|
|
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 = {};
|
package/bin/lib/node-execute.mjs
CHANGED
|
@@ -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",
|
package/bin/lib/scheduler.mjs
CHANGED
|
@@ -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", () => {});
|