@fieldwangai/agentflow 0.1.26 → 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/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 +72 -0
- 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-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
|
@@ -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",
|
|
@@ -885,6 +886,77 @@ export function startUiServer({
|
|
|
885
886
|
return;
|
|
886
887
|
}
|
|
887
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
|
+
|
|
888
960
|
if (req.method === "GET" && url.pathname === "/api/flow") {
|
|
889
961
|
const flowId = url.searchParams.get("flowId");
|
|
890
962
|
const flowSource = url.searchParams.get("flowSource") || "user";
|
|
@@ -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);
|
|
@@ -14,6 +14,7 @@ import yaml from "js-yaml";
|
|
|
14
14
|
|
|
15
15
|
import { getRunDir, LEGACY_NODES_DIR, PIPELINES_DIR, PROJECT_NODES_DIR } from "../lib/paths.mjs";
|
|
16
16
|
import { getFlowDir } from "../lib/workspace.mjs";
|
|
17
|
+
import { isMarketplaceDefinitionId, resolveMarketplaceNodePackage, writeFlowMarketplaceLock } from "../lib/marketplace.mjs";
|
|
17
18
|
import { loadAllExecIds, latestResultExecId, intermediateResultBasename, intermediateDirForNode, outputDirForNode } from "./get-exec-id.mjs";
|
|
18
19
|
|
|
19
20
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -39,6 +40,7 @@ function loadFlowDefinition(flowDir) {
|
|
|
39
40
|
instances: data.instances && typeof data.instances === "object" ? data.instances : {},
|
|
40
41
|
edges,
|
|
41
42
|
ui: data.ui && typeof data.ui === "object" ? data.ui : {},
|
|
43
|
+
dependencies: data.dependencies && typeof data.dependencies === "object" ? data.dependencies : {},
|
|
42
44
|
};
|
|
43
45
|
} catch {
|
|
44
46
|
return null;
|
|
@@ -48,6 +50,7 @@ function loadFlowDefinition(flowDir) {
|
|
|
48
50
|
/** 由 definitionId 前缀推导 type */
|
|
49
51
|
function definitionIdToType(definitionId) {
|
|
50
52
|
const id = (definitionId || "").toLowerCase();
|
|
53
|
+
if (id.startsWith("marketplace:")) return "agent";
|
|
51
54
|
if (id.startsWith("control_")) return "control";
|
|
52
55
|
if (id.startsWith("agent_")) return "agent";
|
|
53
56
|
if (id.startsWith("provide_")) return "provide";
|
|
@@ -62,8 +65,14 @@ function definitionIdToType(definitionId) {
|
|
|
62
65
|
* @param {string} definitionName - 实例中引用的定义名(如 user_confirm_scope)
|
|
63
66
|
* @returns {{ definitionId: string, definitionName: string }}
|
|
64
67
|
*/
|
|
65
|
-
function resolveDefinitionIdFromNodeClass(flowDir, definitionName) {
|
|
66
|
-
|
|
68
|
+
function resolveDefinitionIdFromNodeClass(flowDir, definitionName, workspaceRoot, flowData) {
|
|
69
|
+
if (isMarketplaceDefinitionId(definitionName)) {
|
|
70
|
+
const resolved = resolveMarketplaceNodePackage(workspaceRoot, flowDir, definitionName, flowData);
|
|
71
|
+
return {
|
|
72
|
+
definitionId: resolved?.resolvedDefinitionId || definitionName,
|
|
73
|
+
definitionName,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
67
76
|
const fileName = definitionName.endsWith(".md") ? definitionName : `${definitionName}.md`;
|
|
68
77
|
const flowNodesPath = path.join(flowDir, "nodes", fileName);
|
|
69
78
|
const projectNodesNew = path.join(workspaceRoot, PROJECT_NODES_DIR, fileName);
|
|
@@ -85,7 +94,7 @@ function resolveDefinitionIdFromNodeClass(flowDir, definitionName) {
|
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
/** 从 loadFlowDefinition 结果得到 nodes 和 edges(与 readFlowMd 输出形状一致) */
|
|
88
|
-
function readFlowFromYaml(flowDir) {
|
|
97
|
+
function readFlowFromYaml(flowDir, workspaceRoot = path.resolve(flowDir, "..", "..", "..", "..")) {
|
|
89
98
|
const def = loadFlowDefinition(flowDir);
|
|
90
99
|
if (!def) return { nodes: [], edges: [] };
|
|
91
100
|
const instances = def.instances;
|
|
@@ -98,7 +107,7 @@ function readFlowFromYaml(flowDir) {
|
|
|
98
107
|
const nodes = Array.from(nodeIds).map((id) => {
|
|
99
108
|
const inst = instances[id] || {};
|
|
100
109
|
const definitionName = inst.definitionId ?? id;
|
|
101
|
-
const { definitionId } = resolveDefinitionIdFromNodeClass(flowDir, definitionName);
|
|
110
|
+
const { definitionId } = resolveDefinitionIdFromNodeClass(flowDir, definitionName, workspaceRoot, def);
|
|
102
111
|
const type = definitionIdToType(definitionId);
|
|
103
112
|
const label = inst.label != null ? String(inst.label) : id;
|
|
104
113
|
const role =
|
|
@@ -613,7 +622,8 @@ function main() {
|
|
|
613
622
|
process.exit(1);
|
|
614
623
|
}
|
|
615
624
|
try {
|
|
616
|
-
|
|
625
|
+
writeFlowMarketplaceLock(workspaceRoot || path.resolve(flowDir, "..", "..", "..", ".."), flowDir, flowData);
|
|
626
|
+
const { nodes, edges } = readFlowFromYaml(flowDir, workspaceRoot || path.resolve(flowDir, "..", "..", "..", ".."));
|
|
617
627
|
const { order: topoOrder, hasCycle } = topoSort(nodes, edges);
|
|
618
628
|
const order = hasCycle ? nodes.map((n) => n.id) : topoOrder;
|
|
619
629
|
const cycleNodes = hasCycle ? Array.from(findCycleNodes(nodes, edges)) : [];
|