@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.
@@ -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}`);
@@ -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
- const workspaceRoot = path.resolve(flowDir, "..", "..", "..", "..");
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
- const { nodes, edges } = readFlowFromYaml(flowDir);
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)) : [];