@fieldwangai/agentflow 0.1.26 → 0.1.28
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/apply.mjs +18 -1
- package/bin/lib/catalog-flows.mjs +65 -0
- package/bin/lib/composer-skill-router.mjs +117 -0
- package/bin/lib/help.mjs +8 -0
- package/bin/lib/locales/en.json +21 -1
- package/bin/lib/locales/zh.json +21 -1
- package/bin/lib/main.mjs +56 -3
- package/bin/lib/marketplace.mjs +542 -0
- package/bin/lib/paths.mjs +7 -0
- package/bin/lib/scheduler.mjs +246 -0
- package/bin/lib/ui-server.mjs +117 -55
- package/bin/pipeline/build-node-prompt.mjs +43 -2
- package/bin/pipeline/get-ready-nodes.mjs +2 -2
- package/bin/pipeline/get-resolved-values.mjs +6 -0
- package/bin/pipeline/parse-flow.mjs +15 -5
- package/bin/pipeline/pre-process-node.mjs +336 -1
- package/builtin/nodes/control_cancelled.md +20 -0
- package/builtin/nodes/control_deadline.md +32 -0
- package/builtin/nodes/control_delay.md +20 -0
- package/builtin/nodes/control_interval_loop.md +53 -0
- package/builtin/nodes/control_wait_until.md +23 -0
- package/builtin/web-ui/dist/assets/index-BeUBxIj1.js +190 -0
- package/builtin/web-ui/dist/assets/index-BzhdjOzb.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-CZkUPcXE.css +0 -1
- package/builtin/web-ui/dist/assets/index-D4949pHe.js +0 -190
package/bin/lib/scheduler.mjs
CHANGED
|
@@ -12,9 +12,12 @@ import {
|
|
|
12
12
|
import { 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 { writeResult } from "../pipeline/write-result.mjs";
|
|
15
16
|
|
|
16
17
|
const DEFAULT_POLL_MS = 30_000;
|
|
17
18
|
const RUN_CONFIG_FILENAME = "run-config.json";
|
|
19
|
+
const WAIT_STATE_FILENAME = "wait-state.json";
|
|
20
|
+
const WAIT_STATES_FILENAME = "wait-states.json";
|
|
18
21
|
|
|
19
22
|
function sleep(ms) {
|
|
20
23
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -108,6 +111,104 @@ function getLatestRunUuidForFlow(workspaceRoot, flowId) {
|
|
|
108
111
|
}
|
|
109
112
|
}
|
|
110
113
|
|
|
114
|
+
function listRunDirsForFlow(flow) {
|
|
115
|
+
const flowDir = flow.path || "";
|
|
116
|
+
const runRoot = flowDir ? path.join(flowDir, "runBuild") : "";
|
|
117
|
+
if (!runRoot || !fs.existsSync(runRoot)) return [];
|
|
118
|
+
try {
|
|
119
|
+
return fs.readdirSync(runRoot, { withFileTypes: true })
|
|
120
|
+
.filter((e) => e.isDirectory() && /^\d{14}$/.test(e.name))
|
|
121
|
+
.map((e) => ({ uuid: e.name, runDir: path.join(runRoot, e.name) }));
|
|
122
|
+
} catch {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readJsonObject(filePath) {
|
|
128
|
+
if (!fs.existsSync(filePath)) return null;
|
|
129
|
+
try {
|
|
130
|
+
const state = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
131
|
+
return state && typeof state === "object" ? state : null;
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function waitStateKey(state) {
|
|
138
|
+
return String((state && (state.id || state.instanceId)) || "");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function persistedWaitState(state) {
|
|
142
|
+
const {
|
|
143
|
+
waitPath: _waitPath,
|
|
144
|
+
legacyPath: _legacyPath,
|
|
145
|
+
registryPath: _registryPath,
|
|
146
|
+
runDir: _runDir,
|
|
147
|
+
...persisted
|
|
148
|
+
} = state && typeof state === "object" ? state : {};
|
|
149
|
+
return persisted;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readWaitStates(runDir) {
|
|
153
|
+
const legacyPath = path.join(runDir, WAIT_STATE_FILENAME);
|
|
154
|
+
const registryPath = path.join(runDir, WAIT_STATES_FILENAME);
|
|
155
|
+
const states = [];
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
const registry = readJsonObject(registryPath);
|
|
158
|
+
if (registry && Array.isArray(registry.waits)) {
|
|
159
|
+
for (const raw of registry.waits) {
|
|
160
|
+
if (!raw || typeof raw !== "object") continue;
|
|
161
|
+
const state = { ...raw, runDir, legacyPath, registryPath };
|
|
162
|
+
const key = waitStateKey(state);
|
|
163
|
+
if (!key || seen.has(key)) continue;
|
|
164
|
+
seen.add(key);
|
|
165
|
+
states.push(state);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const legacy = readJsonObject(legacyPath);
|
|
169
|
+
if (legacy) {
|
|
170
|
+
const state = { ...legacy, runDir, waitPath: legacyPath, legacyPath, registryPath: fs.existsSync(registryPath) ? registryPath : null };
|
|
171
|
+
const key = waitStateKey(state);
|
|
172
|
+
if (key && !seen.has(key)) states.push(state);
|
|
173
|
+
}
|
|
174
|
+
return states;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function writeWaitState(waitState, patch = {}) {
|
|
178
|
+
const next = {
|
|
179
|
+
...(waitState && typeof waitState === "object" ? waitState : {}),
|
|
180
|
+
...(patch && typeof patch === "object" ? patch : {}),
|
|
181
|
+
updatedAt: new Date().toISOString(),
|
|
182
|
+
};
|
|
183
|
+
const runDir = next.runDir || (next.waitPath ? path.dirname(next.waitPath) : "");
|
|
184
|
+
if (!runDir) return;
|
|
185
|
+
|
|
186
|
+
const legacyPath = next.legacyPath || next.waitPath || path.join(runDir, WAIT_STATE_FILENAME);
|
|
187
|
+
const registryPath = next.registryPath || path.join(runDir, WAIT_STATES_FILENAME);
|
|
188
|
+
const persisted = persistedWaitState(next);
|
|
189
|
+
const key = waitStateKey(persisted);
|
|
190
|
+
|
|
191
|
+
const registry = readJsonObject(registryPath);
|
|
192
|
+
if (registry && Array.isArray(registry.waits)) {
|
|
193
|
+
const waits = registry.waits.filter((w) => waitStateKey(w) !== key);
|
|
194
|
+
waits.push(persisted);
|
|
195
|
+
fs.writeFileSync(registryPath, JSON.stringify({ ...registry, updatedAt: new Date().toISOString(), waits }, null, 2) + "\n", "utf-8");
|
|
196
|
+
}
|
|
197
|
+
fs.writeFileSync(legacyPath, JSON.stringify(persisted, null, 2) + "\n", "utf-8");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function readNodeResultStatus(runDir, instanceId) {
|
|
201
|
+
const resultPath = path.join(runDir, "intermediate", instanceId, `${instanceId}.result.md`);
|
|
202
|
+
if (!fs.existsSync(resultPath)) return null;
|
|
203
|
+
try {
|
|
204
|
+
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
205
|
+
const m = raw.match(/^\s*status:\s*["']?([^"'\s]+)["']?/m);
|
|
206
|
+
return m ? m[1] : null;
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
111
212
|
function isFlowCurrentlyRunning(workspaceRoot, flowId, state) {
|
|
112
213
|
const candidates = [];
|
|
113
214
|
if (state && typeof state.lastRunUuid === "string") candidates.push(state.lastRunUuid);
|
|
@@ -206,6 +307,110 @@ function startScheduledRun(workspaceRoot, flow, schedule, state) {
|
|
|
206
307
|
return child;
|
|
207
308
|
}
|
|
208
309
|
|
|
310
|
+
function startWaitingRunResume(workspaceRoot, flow, waitState) {
|
|
311
|
+
const agentflowBin = path.join(PACKAGE_ROOT, "bin", "agentflow.mjs");
|
|
312
|
+
const uuid = String(waitState.uuid || "");
|
|
313
|
+
const instanceId = String(waitState.instanceId || "");
|
|
314
|
+
const args = [
|
|
315
|
+
agentflowBin,
|
|
316
|
+
"resume",
|
|
317
|
+
flow.id,
|
|
318
|
+
uuid,
|
|
319
|
+
instanceId,
|
|
320
|
+
"--machine-readable",
|
|
321
|
+
"--workspace-root",
|
|
322
|
+
path.resolve(workspaceRoot),
|
|
323
|
+
"--force",
|
|
324
|
+
];
|
|
325
|
+
const child = spawn(process.execPath, args, {
|
|
326
|
+
cwd: path.resolve(workspaceRoot),
|
|
327
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
328
|
+
env: { ...process.env, FORCE_COLOR: "0" },
|
|
329
|
+
detached: true,
|
|
330
|
+
});
|
|
331
|
+
child.stdout.on("data", () => {});
|
|
332
|
+
child.stderr.on("data", (chunk) => {
|
|
333
|
+
const text = chunk.toString("utf8").trim();
|
|
334
|
+
if (text) log.debug(`[scheduler] resume ${flow.id}/${uuid}: ${text.slice(0, 1000)}`);
|
|
335
|
+
});
|
|
336
|
+
child.on("exit", (code, signal) => {
|
|
337
|
+
const runDir = waitState.runDir || (waitState.waitPath ? path.dirname(waitState.waitPath) : "");
|
|
338
|
+
if (!runDir) return;
|
|
339
|
+
const latest = readWaitStates(runDir).find((s) => waitStateKey(s) === waitStateKey(waitState));
|
|
340
|
+
if (!latest || latest.wakeAt !== waitState.wakeAt || latest.instanceId !== waitState.instanceId) return;
|
|
341
|
+
writeWaitState(latest, {
|
|
342
|
+
status: code === 0 ? "resumed" : "waiting",
|
|
343
|
+
lastResumeExitCode: code,
|
|
344
|
+
lastResumeExitSignal: signal || "",
|
|
345
|
+
lastResumeFinishedAt: new Date().toISOString(),
|
|
346
|
+
...(code === 0 ? {} : { lastError: `resume exited with code ${code}${signal ? ` signal ${signal}` : ""}` }),
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
child.unref();
|
|
350
|
+
return child;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function countActiveWaitsForFlow(flow) {
|
|
354
|
+
let count = 0;
|
|
355
|
+
for (const run of listRunDirsForFlow(flow)) {
|
|
356
|
+
for (const waitState of readWaitStates(run.runDir)) {
|
|
357
|
+
if (waitState && (waitState.status === "waiting" || waitState.status === "resuming")) count += 1;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return count;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function hasNodeBranchEdge(runDir, instanceId, branchName) {
|
|
364
|
+
const flowJsonPath = path.join(runDir, "intermediate", "flow.json");
|
|
365
|
+
const flow = readJsonObject(flowJsonPath);
|
|
366
|
+
if (!flow || !Array.isArray(flow.edges)) return false;
|
|
367
|
+
const outputSlotTypes = flow.outputSlotTypes && flow.outputSlotTypes[instanceId];
|
|
368
|
+
if (!outputSlotTypes || typeof outputSlotTypes !== "object") return false;
|
|
369
|
+
const idx = Object.keys(outputSlotTypes).indexOf(branchName);
|
|
370
|
+
if (idx < 0) return false;
|
|
371
|
+
const sourceHandle = `output-${idx}`;
|
|
372
|
+
return flow.edges.some((e) => e && e.source === instanceId && (e.sourceHandle || "output-0") === sourceHandle);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function cancelScheduledRun(workspaceRoot, flowId, uuid) {
|
|
376
|
+
const flow = listFlowsJson(workspaceRoot).find((f) => f.id === flowId && !f.archived && f.source !== "builtin");
|
|
377
|
+
if (!flow) return { ok: false, error: `flow not found: ${flowId}` };
|
|
378
|
+
const runDir = getRunDir(workspaceRoot, flow.id, uuid);
|
|
379
|
+
if (!fs.existsSync(runDir)) return { ok: false, error: `run not found: ${flowId}/${uuid}` };
|
|
380
|
+
const cancelledAt = new Date().toISOString();
|
|
381
|
+
fs.writeFileSync(path.join(runDir, "cancelled.json"), JSON.stringify({ cancelled: true, cancelledAt }, null, 2) + "\n", "utf-8");
|
|
382
|
+
let updated = 0;
|
|
383
|
+
let propagated = 0;
|
|
384
|
+
let resumePid = null;
|
|
385
|
+
for (const waitState of readWaitStates(runDir)) {
|
|
386
|
+
if (waitState.status !== "waiting" && waitState.status !== "resuming") continue;
|
|
387
|
+
const instanceId = String(waitState.instanceId || "");
|
|
388
|
+
const canPropagate =
|
|
389
|
+
waitState.reason === "control_interval_loop" &&
|
|
390
|
+
instanceId &&
|
|
391
|
+
hasNodeBranchEdge(runDir, instanceId, "cancelled") &&
|
|
392
|
+
!resumePid;
|
|
393
|
+
if (canPropagate) {
|
|
394
|
+
writeResult(
|
|
395
|
+
workspaceRoot,
|
|
396
|
+
flow.id,
|
|
397
|
+
uuid,
|
|
398
|
+
instanceId,
|
|
399
|
+
{ status: "success", message: "已取消", branch: "cancelled" },
|
|
400
|
+
{ execId: Number(waitState.execId) || undefined, preserveBody: false },
|
|
401
|
+
);
|
|
402
|
+
const child = startWaitingRunResume(workspaceRoot, flow, { ...waitState, uuid, runDir, branch: "cancelled" });
|
|
403
|
+
resumePid = child.pid || null;
|
|
404
|
+
writeWaitState(waitState, { status: "resuming", branch: "cancelled", cancelledAt, resumePid, resumeStartedAt: cancelledAt });
|
|
405
|
+
propagated += 1;
|
|
406
|
+
} else {
|
|
407
|
+
writeWaitState(waitState, { status: "cancelled", cancelledAt });
|
|
408
|
+
}
|
|
409
|
+
updated += 1;
|
|
410
|
+
}
|
|
411
|
+
return { ok: true, flowId, uuid, cancelledAt, updatedWaits: updated, propagatedWaits: propagated, resumePid };
|
|
412
|
+
}
|
|
413
|
+
|
|
209
414
|
export function listScheduleStatuses(workspaceRoot) {
|
|
210
415
|
const rows = [];
|
|
211
416
|
for (const flow of listFlowsJson(workspaceRoot)) {
|
|
@@ -232,6 +437,7 @@ export function listScheduleStatuses(workspaceRoot) {
|
|
|
232
437
|
? "workspace flow is shadowed by a user flow with the same id"
|
|
233
438
|
: state.lastError || "",
|
|
234
439
|
running: isFlowCurrentlyRunning(workspaceRoot, flow.id, state),
|
|
440
|
+
waiting: countActiveWaitsForFlow(flow),
|
|
235
441
|
});
|
|
236
442
|
}
|
|
237
443
|
rows.sort((a, b) => {
|
|
@@ -251,6 +457,46 @@ export async function startScheduler(workspaceRoot, opts = {}) {
|
|
|
251
457
|
for (const flow of listFlowsJson(workspaceRoot)) {
|
|
252
458
|
if (flow.archived || flow.source === "builtin") continue;
|
|
253
459
|
const flowSource = flow.source || "user";
|
|
460
|
+
let resumedWaitingRun = false;
|
|
461
|
+
for (const run of listRunDirsForFlow(flow)) {
|
|
462
|
+
if (resumedWaitingRun) break;
|
|
463
|
+
for (const waitState of readWaitStates(run.runDir)) {
|
|
464
|
+
if (!waitState || !waitState.wakeAt || !waitState.instanceId) continue;
|
|
465
|
+
if (waitState.status === "resuming" && !isFlowCurrentlyRunning(workspaceRoot, flow.id, { lastRunUuid: run.uuid })) {
|
|
466
|
+
const nodeStatus = readNodeResultStatus(run.runDir, String(waitState.instanceId));
|
|
467
|
+
writeWaitState(waitState, {
|
|
468
|
+
status: nodeStatus === "pending" ? "waiting" : "resumed",
|
|
469
|
+
reconciledAt: new Date().toISOString(),
|
|
470
|
+
});
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (waitState.status !== "waiting") continue;
|
|
474
|
+
if (Date.parse(waitState.wakeAt) > now) continue;
|
|
475
|
+
if (isFlowCurrentlyRunning(workspaceRoot, flow.id, { lastRunUuid: run.uuid })) continue;
|
|
476
|
+
const nextState = {
|
|
477
|
+
...waitState,
|
|
478
|
+
status: "resuming",
|
|
479
|
+
resumePid: null,
|
|
480
|
+
resumeStartedAt: new Date().toISOString(),
|
|
481
|
+
};
|
|
482
|
+
try {
|
|
483
|
+
const child = startWaitingRunResume(workspaceRoot, flow, { ...waitState, uuid: run.uuid, runDir: run.runDir });
|
|
484
|
+
nextState.resumePid = child.pid || null;
|
|
485
|
+
writeWaitState(waitState, nextState);
|
|
486
|
+
resumedWaitingRun = true;
|
|
487
|
+
log.info(`[scheduler] resume ${flow.id}/${run.uuid} at ${waitState.instanceId}; pid=${child.pid || "?"}`);
|
|
488
|
+
break;
|
|
489
|
+
} catch (e) {
|
|
490
|
+
writeWaitState(waitState, {
|
|
491
|
+
status: "waiting",
|
|
492
|
+
lastError: e && e.message ? e.message : String(e),
|
|
493
|
+
lastErrorAt: new Date().toISOString(),
|
|
494
|
+
});
|
|
495
|
+
log.info(`[scheduler] resume failed ${flow.id}/${run.uuid}: ${e && e.message ? e.message : String(e)}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
254
500
|
const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flowSource);
|
|
255
501
|
if (!scheduleRes.success) {
|
|
256
502
|
log.debug(`[scheduler] ${flow.id}: ${scheduleRes.error}`);
|
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -26,11 +26,9 @@ import { updateModelLists } from "./model-lists.mjs";
|
|
|
26
26
|
import {
|
|
27
27
|
startComposerAgent,
|
|
28
28
|
startComposerMultiStep,
|
|
29
|
-
shouldUseMultiStep,
|
|
30
29
|
runComposerPostFlowValidationAndRepair,
|
|
31
30
|
buildScriptContentBlockForInstances,
|
|
32
31
|
} from "./composer-agent.mjs";
|
|
33
|
-
import { buildNodeSchemaCompactSection } from "./composer-node-schema.mjs";
|
|
34
32
|
import { t } from "./i18n.mjs";
|
|
35
33
|
import {
|
|
36
34
|
PACKAGE_ROOT,
|
|
@@ -42,6 +40,8 @@ import { RUN_INTERRUPTED_FILENAME } from "./recent-runs.mjs";
|
|
|
42
40
|
import {
|
|
43
41
|
detectIntents,
|
|
44
42
|
loadResourcesForIntents,
|
|
43
|
+
loadResourcesForSkillKeys,
|
|
44
|
+
listComposerSkills,
|
|
45
45
|
buildSkillInjectionBlock,
|
|
46
46
|
buildSkillCompactInjectionBlock,
|
|
47
47
|
} from "./composer-skill-router.mjs";
|
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
import { runNodeScript } from "./pipeline-scripts.mjs";
|
|
65
65
|
import { readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
|
|
66
66
|
import { listScheduleStatuses } from "./scheduler.mjs";
|
|
67
|
+
import { installFlowDependency, listMarketplacePackages, publishNodeFromInstance } from "./marketplace.mjs";
|
|
67
68
|
|
|
68
69
|
const MIME = {
|
|
69
70
|
".html": "text/html; charset=utf-8",
|
|
@@ -276,10 +277,6 @@ function buildComposerPromptWithFlowContext(p) {
|
|
|
276
277
|
const flowDirAbs = path.dirname(p.flowYamlAbs);
|
|
277
278
|
const idsLine =
|
|
278
279
|
p.instanceIds.length > 0 ? p.instanceIds.map(String).join(", ") : "(无,可能为全局修改或新增节点)";
|
|
279
|
-
const syncFs = p.editorSyncFlowSource ?? p.flowSource;
|
|
280
|
-
const syncBody = { flowId: p.flowId, flowSource: syncFs };
|
|
281
|
-
if (p.flowArchived) syncBody.flowArchived = true;
|
|
282
|
-
const syncJsonArg = JSON.stringify(JSON.stringify(syncBody));
|
|
283
280
|
const builtinExtra =
|
|
284
281
|
p.flowSource === "builtin" && p.workspaceWriteDirAbs
|
|
285
282
|
? [
|
|
@@ -288,23 +285,6 @@ function buildComposerPromptWithFlowContext(p) {
|
|
|
288
285
|
]
|
|
289
286
|
: [];
|
|
290
287
|
|
|
291
|
-
// 基于用户意图动态注入 skill 和 reference 内容
|
|
292
|
-
const intents = detectIntents(p.userPrompt);
|
|
293
|
-
const resources = loadResourcesForIntents(intents, PACKAGE_ROOT);
|
|
294
|
-
const skillBlock = resources.hasContext
|
|
295
|
-
? buildSkillInjectionBlock(resources.skills, resources.references)
|
|
296
|
-
: "";
|
|
297
|
-
|
|
298
|
-
// 无意图匹配时使用通用 skill 路径引用作为兜底
|
|
299
|
-
const skillPathHints = resources.hasContext
|
|
300
|
-
? []
|
|
301
|
-
: [
|
|
302
|
-
"- 新增实例与边:遵循 skill `skills/agentflow-flow-add-instances/SKILL.md`(或 `.cursor/skills/.../SKILL.md`)。",
|
|
303
|
-
"- 仅改已有实例文案/占位等:遵循 `skills/agentflow-flow-edit-node-fields/SKILL.md`,勿改 definitionId、instanceId、IO 结构与边拓扑。",
|
|
304
|
-
];
|
|
305
|
-
|
|
306
|
-
const nodeSchemaSection = buildNodeSchemaCompactSection();
|
|
307
|
-
|
|
308
288
|
const prefix = [
|
|
309
289
|
"## AgentFlow Composer 上下文",
|
|
310
290
|
`- 流水线目录(flowId=${p.flowId}):${flowDirAbs}`,
|
|
@@ -313,33 +293,15 @@ function buildComposerPromptWithFlowContext(p) {
|
|
|
313
293
|
`- flowSource:${p.flowSource}`,
|
|
314
294
|
...builtinExtra,
|
|
315
295
|
`- 当前关联的节点实例 ID(顺序:画布选中优先,再输入框 @提及):${idsLine}`,
|
|
316
|
-
"-
|
|
317
|
-
"-
|
|
318
|
-
|
|
319
|
-
"",
|
|
320
|
-
"
|
|
321
|
-
"
|
|
322
|
-
"-
|
|
323
|
-
"- **非确定性**:需要语义理解或创造(代码翻译/生成、源码/文本解析改写、多步推理决策、创意写作)。",
|
|
324
|
-
"- 分支/循环使用 `control_toBool` / `control_agent_toBool` + `control_if` + `control_anyOne` 组合。",
|
|
325
|
-
"- 常量输入使用 `provide_str` / `provide_file`;读取环境变量使用 `tool_get_env`;终端展示使用 `tool_print`。",
|
|
326
|
-
"",
|
|
327
|
-
nodeSchemaSection,
|
|
328
|
-
"",
|
|
329
|
-
"### tool_nodejs 的 script 与 body 关键区分",
|
|
330
|
-
"- **`script` 字段**:实际执行的命令代码,流水线直接 spawn 执行;**tool_nodejs 必须写 script**",
|
|
331
|
-
"- **`body` 字段**:纯文档注释,有 script 时完全不执行;**禁止在 body 写期望执行的逻辑**",
|
|
332
|
-
"- 如果无法写出完整可执行的 script(需要 AI 理解/判断),**必须改用 agent_subAgent**,不要用 tool_nodejs",
|
|
333
|
-
"- script 支持多行(YAML `|`)和管道,可写复杂的 curl + node 组合",
|
|
334
|
-
"- **禁止**:tool_nodejs 只有 body 没有 script(body 中的自然语言不会被执行,节点会失败)",
|
|
335
|
-
"",
|
|
336
|
-
// 动态注入的 skill 和 reference 内容
|
|
337
|
-
...(skillBlock ? [skillBlock, ""] : []),
|
|
338
|
-
"- **保存 flow.yaml 后必须刷新 Web 画布**:遵循 `skills/agentflow-flow-sync-ui/SKILL.md`;在终端执行(将 JSON 与上方 flowId、flowSource" +
|
|
339
|
-
(p.flowArchived ? "、flowArchived" : "") +
|
|
340
|
-
" 保持一致):",
|
|
341
|
-
` curl -sS -X POST http://127.0.0.1:${p.uiPort}/api/flow-editor-sync -H 'Content-Type: application/json' -d ${syncJsonArg}`,
|
|
296
|
+
"- 像普通 agent 请求一样处理用户说明:可能只是问问题,也可能要求编辑文件。不要因为存在 flowId 就默认修改 flow.yaml。",
|
|
297
|
+
"- 按需使用当前环境可用的 skills;如果用户点名某个 skill,遵循该 skill 的 SKILL.md。",
|
|
298
|
+
"- 如果你判断需要编辑 AgentFlow 流程,可按需读取这些本地 skills:",
|
|
299
|
+
" - `skills/agentflow-flow-add-instances/SKILL.md`:新增实例、边和布局",
|
|
300
|
+
" - `skills/agentflow-flow-edit-node-fields/SKILL.md`:只改已有节点字段",
|
|
301
|
+
" - `skills/agentflow-flow-sync-ui/SKILL.md`:保存 flow.yaml 后刷新画布",
|
|
302
|
+
"- 如果只是回答问题,不要修改文件。",
|
|
342
303
|
"",
|
|
304
|
+
...(p.selectedSkillBlock ? [p.selectedSkillBlock, ""] : []),
|
|
343
305
|
...(p.thread && p.thread.length > 0
|
|
344
306
|
? [formatThreadHistory(p.thread), ""]
|
|
345
307
|
: []),
|
|
@@ -351,6 +313,15 @@ function buildComposerPromptWithFlowContext(p) {
|
|
|
351
313
|
return prefix;
|
|
352
314
|
}
|
|
353
315
|
|
|
316
|
+
function flowYamlChangedSince(flowYamlAbs, beforeText) {
|
|
317
|
+
if (!flowYamlAbs || beforeText == null) return false;
|
|
318
|
+
try {
|
|
319
|
+
return fs.readFileSync(flowYamlAbs, "utf-8") !== beforeText;
|
|
320
|
+
} catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
354
325
|
function normalizeContextInstanceIds(raw) {
|
|
355
326
|
if (raw == null) return [];
|
|
356
327
|
if (!Array.isArray(raw)) return [];
|
|
@@ -885,6 +856,77 @@ export function startUiServer({
|
|
|
885
856
|
return;
|
|
886
857
|
}
|
|
887
858
|
|
|
859
|
+
if (req.method === "GET" && url.pathname === "/api/marketplace/nodes") {
|
|
860
|
+
try {
|
|
861
|
+
json(res, 200, listMarketplacePackages(root));
|
|
862
|
+
} catch (e) {
|
|
863
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
864
|
+
}
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (req.method === "POST" && url.pathname === "/api/marketplace/install-node") {
|
|
869
|
+
let payload;
|
|
870
|
+
try {
|
|
871
|
+
payload = JSON.parse(await readBody(req));
|
|
872
|
+
} catch {
|
|
873
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const flowId = payload?.flowId;
|
|
877
|
+
const flowSource = payload?.flowSource || "user";
|
|
878
|
+
const flowArchived = payload?.archived === true;
|
|
879
|
+
const nodeSpec = payload?.nodeSpec || payload?.definitionId || payload?.id;
|
|
880
|
+
if (!flowId) {
|
|
881
|
+
json(res, 400, { error: "Missing flowId" });
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (!nodeSpec) {
|
|
885
|
+
json(res, 400, { error: "Missing nodeSpec" });
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (flowArchived || !isValidFlowSourceWrite(flowSource)) {
|
|
889
|
+
json(res, 400, { error: "Cannot install marketplace nodes into builtin or archived flow" });
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
const resolved = resolveFlowDirForWrite(root, flowId, flowSource);
|
|
894
|
+
if (resolved.error || !resolved.flowDir) {
|
|
895
|
+
json(res, 400, { error: resolved.error || "Could not resolve flow directory" });
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const result = installFlowDependency(root, resolved.flowDir, nodeSpec);
|
|
899
|
+
json(res, result.ok ? 200 : 400, result);
|
|
900
|
+
} catch (e) {
|
|
901
|
+
json(res, 500, { ok: false, error: (e && e.message) || String(e) });
|
|
902
|
+
}
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (req.method === "POST" && url.pathname === "/api/marketplace/publish-node-from-instance") {
|
|
907
|
+
let payload;
|
|
908
|
+
try {
|
|
909
|
+
payload = JSON.parse(await readBody(req));
|
|
910
|
+
} catch {
|
|
911
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
try {
|
|
915
|
+
const flowId = payload?.flowId;
|
|
916
|
+
const flowSource = payload?.flowSource || "user";
|
|
917
|
+
let flowDir = "";
|
|
918
|
+
if (flowId && isValidFlowSourceWrite(flowSource)) {
|
|
919
|
+
const resolved = resolveFlowDirForWrite(root, flowId, flowSource);
|
|
920
|
+
if (!resolved.error && resolved.flowDir) flowDir = resolved.flowDir;
|
|
921
|
+
}
|
|
922
|
+
const result = publishNodeFromInstance(root, payload || {}, { flowDir });
|
|
923
|
+
json(res, result.ok ? 200 : 400, result);
|
|
924
|
+
} catch (e) {
|
|
925
|
+
json(res, 500, { ok: false, error: (e && e.message) || String(e) });
|
|
926
|
+
}
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
888
930
|
if (req.method === "GET" && url.pathname === "/api/flow") {
|
|
889
931
|
const flowId = url.searchParams.get("flowId");
|
|
890
932
|
const flowSource = url.searchParams.get("flowSource") || "user";
|
|
@@ -1637,6 +1679,11 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1637
1679
|
return;
|
|
1638
1680
|
}
|
|
1639
1681
|
|
|
1682
|
+
if (req.method === "GET" && url.pathname === "/api/skills") {
|
|
1683
|
+
json(res, 200, { skills: listComposerSkills(PACKAGE_ROOT, root) });
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1640
1687
|
if (req.method === "POST" && url.pathname === "/api/composer-agent") {
|
|
1641
1688
|
let payload;
|
|
1642
1689
|
try {
|
|
@@ -1656,6 +1703,9 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1656
1703
|
json(res, 400, { error: "Invalid model" });
|
|
1657
1704
|
return;
|
|
1658
1705
|
}
|
|
1706
|
+
const selectedSkillKeys = Array.isArray(payload.selectedSkills)
|
|
1707
|
+
? payload.selectedSkills.map((x) => String(x || "").trim()).filter(Boolean).slice(0, 20)
|
|
1708
|
+
: [];
|
|
1659
1709
|
|
|
1660
1710
|
const flowIdRaw = payload.flowId;
|
|
1661
1711
|
const flowSourceRaw = payload.flowSource;
|
|
@@ -1678,6 +1728,8 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1678
1728
|
let flowSource = null;
|
|
1679
1729
|
let instanceIds = [];
|
|
1680
1730
|
let flowContextForMultiStep = null;
|
|
1731
|
+
let flowYamlBefore = null;
|
|
1732
|
+
const hasPhaseContext = payload.phaseContext && typeof payload.phaseContext === "object" && typeof payload.phaseContext.phaseIndex === "number";
|
|
1681
1733
|
|
|
1682
1734
|
if (hasFlowId) {
|
|
1683
1735
|
flowId = String(flowIdRaw).trim();
|
|
@@ -1693,6 +1745,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1693
1745
|
return;
|
|
1694
1746
|
}
|
|
1695
1747
|
flowYamlAbs = yamlRes.path;
|
|
1748
|
+
try { flowYamlBefore = fs.readFileSync(flowYamlAbs, "utf-8"); } catch { flowYamlBefore = null; }
|
|
1696
1749
|
let workspaceWriteDirAbs;
|
|
1697
1750
|
let editorSyncFlowSource = flowSource;
|
|
1698
1751
|
let flowDirForCli = path.dirname(flowYamlAbs);
|
|
@@ -1713,10 +1766,18 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1713
1766
|
if (flowArchived) syncBody.flowArchived = true;
|
|
1714
1767
|
const syncJsonArg = JSON.stringify(JSON.stringify(syncBody));
|
|
1715
1768
|
|
|
1716
|
-
//
|
|
1769
|
+
// 多步分阶段仍需要技能上下文;普通 Composer 请求直接交给 agent + skills 自行判断。
|
|
1717
1770
|
const multiStepIntents = detectIntents(prompt);
|
|
1718
|
-
const
|
|
1771
|
+
const selectedSkillResources = selectedSkillKeys.length > 0
|
|
1772
|
+
? loadResourcesForSkillKeys(selectedSkillKeys, PACKAGE_ROOT, root)
|
|
1773
|
+
: { skills: [], references: [], skillsHint: "", hasContext: false };
|
|
1774
|
+
const multiStepResources = selectedSkillResources.hasContext
|
|
1775
|
+
? selectedSkillResources
|
|
1776
|
+
: loadResourcesForIntents(multiStepIntents, PACKAGE_ROOT);
|
|
1719
1777
|
const flowPipelineDir = flowYamlAbs ? path.dirname(flowYamlAbs) : "";
|
|
1778
|
+
const selectedSkillBlock = selectedSkillResources.hasContext
|
|
1779
|
+
? buildSkillInjectionBlock(selectedSkillResources.skills, selectedSkillResources.references)
|
|
1780
|
+
: "";
|
|
1720
1781
|
|
|
1721
1782
|
flowContextForMultiStep = {
|
|
1722
1783
|
flowYamlAbs,
|
|
@@ -1746,6 +1807,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1746
1807
|
flowArchived,
|
|
1747
1808
|
thread,
|
|
1748
1809
|
scriptContentBlock,
|
|
1810
|
+
selectedSkillBlock,
|
|
1749
1811
|
});
|
|
1750
1812
|
cliWorkspace = composerCliWorkspaceForFlowDir(root, flowDirForCli);
|
|
1751
1813
|
}
|
|
@@ -1837,10 +1899,9 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1837
1899
|
onStreamEvent({ type: "status", line: t("composer.analyzing_task") });
|
|
1838
1900
|
log.debug(`[ui] composer-agent: flowId=${flowId || "(none)"} model=${model || "default"} promptLen=${finalPrompt.length}`);
|
|
1839
1901
|
|
|
1840
|
-
const hasPhaseContext = payload.phaseContext && typeof payload.phaseContext === "object" && typeof payload.phaseContext.phaseIndex === "number";
|
|
1841
1902
|
let useMultiStep;
|
|
1842
1903
|
try {
|
|
1843
|
-
useMultiStep = hasPhaseContext
|
|
1904
|
+
useMultiStep = hasPhaseContext && !payload.singleStep;
|
|
1844
1905
|
} catch (classifyErr) {
|
|
1845
1906
|
log.debug(`[ui] composer classify error: ${classifyErr.message}`);
|
|
1846
1907
|
logComposerEvent(composerLogPath, "composer-done", {
|
|
@@ -1937,7 +1998,8 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1937
1998
|
endSafe();
|
|
1938
1999
|
return;
|
|
1939
2000
|
}
|
|
1940
|
-
|
|
2001
|
+
const flowYamlChanged = flowYamlChangedSince(flowYamlAbs, flowYamlBefore);
|
|
2002
|
+
if (flowYamlChanged && flowYamlAbs && flowContextForMultiStep) {
|
|
1941
2003
|
try {
|
|
1942
2004
|
await runComposerPostFlowValidationAndRepair({
|
|
1943
2005
|
uiWorkspaceRoot: root,
|
|
@@ -1966,7 +2028,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1966
2028
|
flowId: flowId || null,
|
|
1967
2029
|
flowSource: flowSource || null,
|
|
1968
2030
|
});
|
|
1969
|
-
if (flowId && flowSource) {
|
|
2031
|
+
if (flowYamlChanged && flowId && flowSource) {
|
|
1970
2032
|
broadcastFlowEditorSync(flowId, flowSource, Boolean(payload.flowArchived));
|
|
1971
2033
|
}
|
|
1972
2034
|
try { res.write(JSON.stringify({ type: "done" }) + "\n"); } catch (_) {}
|
|
@@ -11,6 +11,7 @@ import { fileURLToPath } from "url";
|
|
|
11
11
|
|
|
12
12
|
import { getRunDir, PIPELINES_DIR } from "../lib/paths.mjs";
|
|
13
13
|
import { getFlowDir } from "../lib/workspace.mjs";
|
|
14
|
+
import { isMarketplaceDefinitionId, resolveMarketplaceNodePackage } from "../lib/marketplace.mjs";
|
|
14
15
|
import { loadFlowDefinition } from "./parse-flow.mjs";
|
|
15
16
|
import { getResolvedValues, getOutputPathForSlot } from "./get-resolved-values.mjs";
|
|
16
17
|
import { loadExecId } from "./get-exec-id.mjs";
|
|
@@ -22,7 +23,8 @@ function shellQuote(s) {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
function resolvePlaceholder(k, resolvedInputs, resolvedOutputs, opts) {
|
|
25
|
-
const { instanceId, currentExecId, runDir } = opts;
|
|
26
|
+
const { instanceId, currentExecId, runDir, extra = {} } = opts;
|
|
27
|
+
if (Object.prototype.hasOwnProperty.call(extra, k)) return extra[k];
|
|
26
28
|
const execId = currentExecId ?? 1;
|
|
27
29
|
const toAbs = (rel) => (runDir && rel ? path.join(runDir, rel) : rel);
|
|
28
30
|
if (k.startsWith("input.")) {
|
|
@@ -76,6 +78,39 @@ function resolveScriptCommand(
|
|
|
76
78
|
});
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
function marketplaceRuntimeCommand(marketplaceNode, resolvedInputs, resolvedOutputs, opts) {
|
|
82
|
+
if (!marketplaceNode || !marketplaceNode.runtime) return "";
|
|
83
|
+
const runtime = marketplaceNode.runtime;
|
|
84
|
+
const packageDir = marketplaceNode.packageDir;
|
|
85
|
+
const entry = runtime.entry ? path.join(packageDir, String(runtime.entry)) : "";
|
|
86
|
+
const language = String(runtime.language || "").trim();
|
|
87
|
+
const runner =
|
|
88
|
+
language === "python"
|
|
89
|
+
? "python3"
|
|
90
|
+
: language === "bash"
|
|
91
|
+
? "bash"
|
|
92
|
+
: language === "nodejs" || entry
|
|
93
|
+
? "node"
|
|
94
|
+
: "";
|
|
95
|
+
const args = Array.isArray(runtime.args) ? runtime.args.map((x) => String(x)).join(" ") : "";
|
|
96
|
+
const command = runtime.command
|
|
97
|
+
? String(runtime.command)
|
|
98
|
+
: entry
|
|
99
|
+
? [runner, "${entry}", args].filter(Boolean).join(" ")
|
|
100
|
+
: "";
|
|
101
|
+
if (!command) return "";
|
|
102
|
+
return resolveScriptCommand(command, resolvedInputs, resolvedOutputs, {
|
|
103
|
+
...opts,
|
|
104
|
+
extra: {
|
|
105
|
+
...(opts.extra || {}),
|
|
106
|
+
entry,
|
|
107
|
+
packageDir,
|
|
108
|
+
workspaceRoot: opts.workspaceRoot || "",
|
|
109
|
+
runDir: opts.runDir || "",
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
79
114
|
/**
|
|
80
115
|
* 执行占位符替换,组装 prompt 并写入 intermediate 文件(文件名带 _execId)。
|
|
81
116
|
* @param {number} [execId] - 本轮 execId,缺省则从 memory 读取
|
|
@@ -105,6 +140,10 @@ export function buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execI
|
|
|
105
140
|
|
|
106
141
|
const flowData = loadFlowDefinition(flowDir);
|
|
107
142
|
const inst = flowData?.instances?.[instanceId];
|
|
143
|
+
const marketplaceNode =
|
|
144
|
+
inst?.definitionId && isMarketplaceDefinitionId(inst.definitionId)
|
|
145
|
+
? resolveMarketplaceNodePackage(workspaceRoot, flowDir, inst.definitionId, flowData)
|
|
146
|
+
: null;
|
|
108
147
|
const instanceBody =
|
|
109
148
|
inst?.body != null
|
|
110
149
|
? String(inst.body || "").trim()
|
|
@@ -115,7 +154,7 @@ export function buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execI
|
|
|
115
154
|
: "";
|
|
116
155
|
|
|
117
156
|
const { resolvedInputs = {}, resolvedOutputs = {}, systemPrompt = "" } = data;
|
|
118
|
-
const resolveOpts = { instanceId, currentExecId: e, runDir };
|
|
157
|
+
const resolveOpts = { instanceId, currentExecId: e, runDir, workspaceRoot };
|
|
119
158
|
const taskBody = resolvePlaceholdersInText(
|
|
120
159
|
instanceBody,
|
|
121
160
|
resolvedInputs,
|
|
@@ -125,6 +164,8 @@ export function buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execI
|
|
|
125
164
|
|
|
126
165
|
const resolvedScript = instanceScript
|
|
127
166
|
? resolveScriptCommand(instanceScript, resolvedInputs, resolvedOutputs, resolveOpts)
|
|
167
|
+
: marketplaceNode
|
|
168
|
+
? marketplaceRuntimeCommand(marketplaceNode, resolvedInputs, resolvedOutputs, resolveOpts)
|
|
128
169
|
: "";
|
|
129
170
|
|
|
130
171
|
const content = `## 节点上下文
|
|
@@ -248,7 +248,7 @@ function main() {
|
|
|
248
248
|
for (const id of pendingInstances) {
|
|
249
249
|
if (instanceStatus[id] !== "success") continue;
|
|
250
250
|
nextPending.delete(id);
|
|
251
|
-
if (nodeDefinitions[id] === "control_if" || nodeDefinitions[id] === "tool_user_ask") {
|
|
251
|
+
if (nodeDefinitions[id] === "control_if" || nodeDefinitions[id] === "tool_user_ask" || nodeDefinitions[id] === "control_interval_loop") {
|
|
252
252
|
const predLatestE = latestResultExecId(execIdMap[id] ?? 1);
|
|
253
253
|
const resultPath = predLatestE
|
|
254
254
|
? path.join(intermediateDir, id, intermediateResultBasename(id, predLatestE))
|
|
@@ -309,7 +309,7 @@ function main() {
|
|
|
309
309
|
/** 判断 predecessor P 对 target N 是否算「就绪」:普通节点看 status;control_if / tool_user_ask 看 branch 与出边 sourceHandle */
|
|
310
310
|
const isPredecessorReadyFor = (predSource, predDefId, targetId) => {
|
|
311
311
|
if (instanceStatus[predSource] !== "success") return false;
|
|
312
|
-
if (predDefId !== "control_if" && predDefId !== "tool_user_ask") return true;
|
|
312
|
+
if (predDefId !== "control_if" && predDefId !== "tool_user_ask" && predDefId !== "control_interval_loop") return true;
|
|
313
313
|
const inEdges = inEdgesByTarget[targetId] || [];
|
|
314
314
|
const edge = inEdges.find((ie) => ie.source === predSource);
|
|
315
315
|
if (!edge) return true;
|