@fieldwangai/agentflow 0.1.25 → 0.1.27
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-agent.mjs +20 -84
- package/bin/lib/composer-skill-router.mjs +0 -18
- 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/node-exec-context.mjs +39 -14
- package/bin/lib/paths.mjs +7 -0
- package/bin/lib/scheduler.mjs +246 -0
- package/bin/lib/ui-server.mjs +88 -69
- 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-C3PT7ICx.js +190 -0
- package/builtin/web-ui/dist/assets/index-CFuFD_86.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-DkkhNESc.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
|
@@ -29,8 +29,8 @@ import {
|
|
|
29
29
|
shouldUseMultiStep,
|
|
30
30
|
runComposerPostFlowValidationAndRepair,
|
|
31
31
|
buildScriptContentBlockForInstances,
|
|
32
|
-
buildQueryContextBlock,
|
|
33
32
|
} from "./composer-agent.mjs";
|
|
33
|
+
import { buildNodeSchemaCompactSection } from "./composer-node-schema.mjs";
|
|
34
34
|
import { t } from "./i18n.mjs";
|
|
35
35
|
import {
|
|
36
36
|
PACKAGE_ROOT,
|
|
@@ -41,7 +41,6 @@ import {
|
|
|
41
41
|
import { RUN_INTERRUPTED_FILENAME } from "./recent-runs.mjs";
|
|
42
42
|
import {
|
|
43
43
|
detectIntents,
|
|
44
|
-
classifyIntentCategory,
|
|
45
44
|
loadResourcesForIntents,
|
|
46
45
|
buildSkillInjectionBlock,
|
|
47
46
|
buildSkillCompactInjectionBlock,
|
|
@@ -65,6 +64,7 @@ import {
|
|
|
65
64
|
import { runNodeScript } from "./pipeline-scripts.mjs";
|
|
66
65
|
import { readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
|
|
67
66
|
import { listScheduleStatuses } from "./scheduler.mjs";
|
|
67
|
+
import { installFlowDependency, listMarketplacePackages, publishNodeFromInstance } from "./marketplace.mjs";
|
|
68
68
|
|
|
69
69
|
const MIME = {
|
|
70
70
|
".html": "text/html; charset=utf-8",
|
|
@@ -274,33 +274,7 @@ function formatThreadHistory(thread) {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
function buildComposerPromptWithFlowContext(p) {
|
|
277
|
-
const intentCategory = p.intentCategory || "generic";
|
|
278
277
|
const flowDirAbs = path.dirname(p.flowYamlAbs);
|
|
279
|
-
|
|
280
|
-
// ── query 轻量路径:只注入节点上下文 + 问题,跳过全部编辑规则 ──
|
|
281
|
-
if (intentCategory === "query") {
|
|
282
|
-
const queryCtx = buildQueryContextBlock(p.flowYamlAbs, p.instanceIds);
|
|
283
|
-
const parts = [
|
|
284
|
-
"## AgentFlow 问答上下文",
|
|
285
|
-
`- 流水线(flowId=${p.flowId}):${flowDirAbs}`,
|
|
286
|
-
`- 图定义文件:${p.flowYamlAbs}`,
|
|
287
|
-
"",
|
|
288
|
-
];
|
|
289
|
-
if (queryCtx) {
|
|
290
|
-
parts.push(queryCtx, "");
|
|
291
|
-
}
|
|
292
|
-
parts.push(
|
|
293
|
-
"请基于上方注入的节点 YAML 与脚本内容回答用户的问题。**不要修改任何文件。**",
|
|
294
|
-
""
|
|
295
|
-
);
|
|
296
|
-
if (p.thread && p.thread.length > 0) {
|
|
297
|
-
parts.push(formatThreadHistory(p.thread), "");
|
|
298
|
-
}
|
|
299
|
-
parts.push("## 用户说明", "", p.userPrompt.trim());
|
|
300
|
-
return parts.join("\n");
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// ── 编辑路径(edit-node / add-node / add-flow / edit-flow / generic) ──
|
|
304
278
|
const idsLine =
|
|
305
279
|
p.instanceIds.length > 0 ? p.instanceIds.map(String).join(", ") : "(无,可能为全局修改或新增节点)";
|
|
306
280
|
const syncFs = p.editorSyncFlowSource ?? p.flowSource;
|
|
@@ -330,48 +304,29 @@ function buildComposerPromptWithFlowContext(p) {
|
|
|
330
304
|
"- 仅改已有实例文案/占位等:遵循 `skills/agentflow-flow-edit-node-fields/SKILL.md`,勿改 definitionId、instanceId、IO 结构与边拓扑。",
|
|
331
305
|
];
|
|
332
306
|
|
|
333
|
-
|
|
334
|
-
const needsNodeTypeRules = intentCategory !== "edit-node";
|
|
307
|
+
const nodeSchemaSection = buildNodeSchemaCompactSection();
|
|
335
308
|
|
|
336
309
|
const prefix = [
|
|
337
|
-
"## AgentFlow
|
|
310
|
+
"## AgentFlow Composer 上下文",
|
|
338
311
|
`- 流水线目录(flowId=${p.flowId}):${flowDirAbs}`,
|
|
339
|
-
`-
|
|
312
|
+
`- 图定义文件:${p.flowYamlAbs}`,
|
|
340
313
|
`- flowId:${p.flowId}`,
|
|
341
314
|
`- flowSource:${p.flowSource}`,
|
|
342
315
|
...builtinExtra,
|
|
343
316
|
`- 当前关联的节点实例 ID(顺序:画布选中优先,再输入框 @提及):${idsLine}`,
|
|
317
|
+
"- 请根据用户需求自行判断:如果是在问问题,只回答;如果是在要求新增、修改、完善或修复流程,请直接修改对应文件。",
|
|
318
|
+
"- 一旦修改 flow.yaml、脚本或相关文件,必须按下方方式刷新 Web 画布。",
|
|
344
319
|
...skillPathHints,
|
|
345
320
|
"",
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
"| 代码翻译/生成、源码/文本理解、多步决策、创意写作 | **agent_subAgent** |",
|
|
356
|
-
"**反例**:「Android 转 RN/TS」「分析代码生成测试」「代码 review」必须 agent——做成 tool_nodejs 必然失败。",
|
|
357
|
-
"",
|
|
358
|
-
"tool_nodejs + script 示例(打印文本):",
|
|
359
|
-
"```yaml",
|
|
360
|
-
"print_hello:",
|
|
361
|
-
" definitionId: tool_nodejs",
|
|
362
|
-
" label: 打印Hello",
|
|
363
|
-
' script: node -e "console.log(${value})"',
|
|
364
|
-
" input:",
|
|
365
|
-
" - { type: 节点, name: prev, value: '' }",
|
|
366
|
-
" - { type: 文本, name: value, value: '' }",
|
|
367
|
-
" output:",
|
|
368
|
-
" - { type: 节点, name: next, value: '' }",
|
|
369
|
-
" - { type: 文本, name: result, value: '' }",
|
|
370
|
-
"```",
|
|
371
|
-
"script 成败以 exit code 为准(0=success),stdout 直接作为 result 槽位内容(纯文本即可,如 console.log)。",
|
|
372
|
-
"常见误用:用 agent_subAgent 做「打印一段文字」「执行已有脚本」→ 应改用 tool_nodejs + script 或 tool_print。",
|
|
373
|
-
"",
|
|
374
|
-
] : []),
|
|
321
|
+
"### 节点能力选择",
|
|
322
|
+
"**判据**:确定性任务优先 `tool_nodejs`;需要语义理解、生成、判断或多步推理时使用 `agent_subAgent`。",
|
|
323
|
+
"- **确定性**:相同输入永远产出相同输出,可用普通代码完整描述(CLI/npm 调用、读写文件、JSON/路径转换、调现成 API 解析固定格式、跑脚手架等)。",
|
|
324
|
+
"- **非确定性**:需要语义理解或创造(代码翻译/生成、源码/文本解析改写、多步推理决策、创意写作)。",
|
|
325
|
+
"- 分支/循环使用 `control_toBool` / `control_agent_toBool` + `control_if` + `control_anyOne` 组合。",
|
|
326
|
+
"- 常量输入使用 `provide_str` / `provide_file`;读取环境变量使用 `tool_get_env`;终端展示使用 `tool_print`。",
|
|
327
|
+
"",
|
|
328
|
+
nodeSchemaSection,
|
|
329
|
+
"",
|
|
375
330
|
"### tool_nodejs 的 script 与 body 关键区分",
|
|
376
331
|
"- **`script` 字段**:实际执行的命令代码,流水线直接 spawn 执行;**tool_nodejs 必须写 script**",
|
|
377
332
|
"- **`body` 字段**:纯文档注释,有 script 时完全不执行;**禁止在 body 写期望执行的逻辑**",
|
|
@@ -931,6 +886,77 @@ export function startUiServer({
|
|
|
931
886
|
return;
|
|
932
887
|
}
|
|
933
888
|
|
|
889
|
+
if (req.method === "GET" && url.pathname === "/api/marketplace/nodes") {
|
|
890
|
+
try {
|
|
891
|
+
json(res, 200, listMarketplacePackages(root));
|
|
892
|
+
} catch (e) {
|
|
893
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
894
|
+
}
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (req.method === "POST" && url.pathname === "/api/marketplace/install-node") {
|
|
899
|
+
let payload;
|
|
900
|
+
try {
|
|
901
|
+
payload = JSON.parse(await readBody(req));
|
|
902
|
+
} catch {
|
|
903
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const flowId = payload?.flowId;
|
|
907
|
+
const flowSource = payload?.flowSource || "user";
|
|
908
|
+
const flowArchived = payload?.archived === true;
|
|
909
|
+
const nodeSpec = payload?.nodeSpec || payload?.definitionId || payload?.id;
|
|
910
|
+
if (!flowId) {
|
|
911
|
+
json(res, 400, { error: "Missing flowId" });
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
if (!nodeSpec) {
|
|
915
|
+
json(res, 400, { error: "Missing nodeSpec" });
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
if (flowArchived || !isValidFlowSourceWrite(flowSource)) {
|
|
919
|
+
json(res, 400, { error: "Cannot install marketplace nodes into builtin or archived flow" });
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
try {
|
|
923
|
+
const resolved = resolveFlowDirForWrite(root, flowId, flowSource);
|
|
924
|
+
if (resolved.error || !resolved.flowDir) {
|
|
925
|
+
json(res, 400, { error: resolved.error || "Could not resolve flow directory" });
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const result = installFlowDependency(root, resolved.flowDir, nodeSpec);
|
|
929
|
+
json(res, result.ok ? 200 : 400, result);
|
|
930
|
+
} catch (e) {
|
|
931
|
+
json(res, 500, { ok: false, error: (e && e.message) || String(e) });
|
|
932
|
+
}
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (req.method === "POST" && url.pathname === "/api/marketplace/publish-node-from-instance") {
|
|
937
|
+
let payload;
|
|
938
|
+
try {
|
|
939
|
+
payload = JSON.parse(await readBody(req));
|
|
940
|
+
} catch {
|
|
941
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
try {
|
|
945
|
+
const flowId = payload?.flowId;
|
|
946
|
+
const flowSource = payload?.flowSource || "user";
|
|
947
|
+
let flowDir = "";
|
|
948
|
+
if (flowId && isValidFlowSourceWrite(flowSource)) {
|
|
949
|
+
const resolved = resolveFlowDirForWrite(root, flowId, flowSource);
|
|
950
|
+
if (!resolved.error && resolved.flowDir) flowDir = resolved.flowDir;
|
|
951
|
+
}
|
|
952
|
+
const result = publishNodeFromInstance(root, payload || {}, { flowDir });
|
|
953
|
+
json(res, result.ok ? 200 : 400, result);
|
|
954
|
+
} catch (e) {
|
|
955
|
+
json(res, 500, { ok: false, error: (e && e.message) || String(e) });
|
|
956
|
+
}
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
|
|
934
960
|
if (req.method === "GET" && url.pathname === "/api/flow") {
|
|
935
961
|
const flowId = url.searchParams.get("flowId");
|
|
936
962
|
const flowSource = url.searchParams.get("flowSource") || "user";
|
|
@@ -1761,7 +1787,6 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1761
1787
|
|
|
1762
1788
|
// 基于用户意图动态加载 skill 上下文
|
|
1763
1789
|
const multiStepIntents = detectIntents(prompt);
|
|
1764
|
-
const intentCategory = classifyIntentCategory(multiStepIntents);
|
|
1765
1790
|
const multiStepResources = loadResourcesForIntents(multiStepIntents, PACKAGE_ROOT);
|
|
1766
1791
|
const flowPipelineDir = flowYamlAbs ? path.dirname(flowYamlAbs) : "";
|
|
1767
1792
|
|
|
@@ -1770,7 +1795,6 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1770
1795
|
flowId,
|
|
1771
1796
|
flowSource,
|
|
1772
1797
|
intents: multiStepIntents,
|
|
1773
|
-
intentCategory,
|
|
1774
1798
|
canvasInstanceIds: instanceIds,
|
|
1775
1799
|
skillsHint: multiStepResources.skillsHint,
|
|
1776
1800
|
skillInjectionBlock: multiStepResources.hasContext
|
|
@@ -1794,7 +1818,6 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1794
1818
|
flowArchived,
|
|
1795
1819
|
thread,
|
|
1796
1820
|
scriptContentBlock,
|
|
1797
|
-
intentCategory,
|
|
1798
1821
|
});
|
|
1799
1822
|
cliWorkspace = composerCliWorkspaceForFlowDir(root, flowDirForCli);
|
|
1800
1823
|
}
|
|
@@ -1887,13 +1910,9 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1887
1910
|
log.debug(`[ui] composer-agent: flowId=${flowId || "(none)"} model=${model || "default"} promptLen=${finalPrompt.length}`);
|
|
1888
1911
|
|
|
1889
1912
|
const hasPhaseContext = payload.phaseContext && typeof payload.phaseContext === "object" && typeof payload.phaseContext.phaseIndex === "number";
|
|
1890
|
-
// query 意图:跳过 planner,直接走单步轻量路径
|
|
1891
|
-
const resolvedIntentCategory = flowContextForMultiStep?.intentCategory || "generic";
|
|
1892
1913
|
let useMultiStep;
|
|
1893
1914
|
try {
|
|
1894
|
-
useMultiStep =
|
|
1895
|
-
? false
|
|
1896
|
-
: (hasPhaseContext || ((await shouldUseMultiStep({ flowYamlAbs, userPrompt: prompt.trim(), cliWorkspace })) && !payload.singleStep));
|
|
1915
|
+
useMultiStep = hasPhaseContext || ((await shouldUseMultiStep({ flowYamlAbs, userPrompt: prompt.trim(), cliWorkspace })) && !payload.singleStep);
|
|
1897
1916
|
} catch (classifyErr) {
|
|
1898
1917
|
log.debug(`[ui] composer classify error: ${classifyErr.message}`);
|
|
1899
1918
|
logComposerEvent(composerLogPath, "composer-done", {
|
|
@@ -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;
|
|
@@ -10,6 +10,7 @@ import fs from "fs";
|
|
|
10
10
|
import path from "path";
|
|
11
11
|
|
|
12
12
|
import { getRunDir, LEGACY_NODES_DIR, PIPELINES_DIR, PROJECT_NODES_DIR } from "../lib/paths.mjs";
|
|
13
|
+
import { isMarketplaceDefinitionId, resolveMarketplaceNodePackage } from "../lib/marketplace.mjs";
|
|
13
14
|
import { getFlowDir } from "../lib/workspace.mjs";
|
|
14
15
|
import { fileURLToPath } from "url";
|
|
15
16
|
|
|
@@ -102,6 +103,11 @@ function extractDescriptionFromFrontmatter(frontmatter) {
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
function readNodeDescription(workspaceRoot, flowDir, definitionId) {
|
|
106
|
+
if (isMarketplaceDefinitionId(definitionId)) {
|
|
107
|
+
const flowData = loadFlowDefinition(flowDir);
|
|
108
|
+
const resolved = resolveMarketplaceNodePackage(workspaceRoot, flowDir, definitionId, flowData);
|
|
109
|
+
if (resolved?.description) return resolved.description;
|
|
110
|
+
}
|
|
105
111
|
const fileName = definitionId.endsWith(".md") ? definitionId : `${definitionId}.md`;
|
|
106
112
|
const flowNodesPath = path.join(flowDir, "nodes", fileName);
|
|
107
113
|
const projectNodesNew = path.join(workspaceRoot, PROJECT_NODES_DIR, fileName);
|