@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.
@@ -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}`);
@@ -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
- // edit-node: 不需要重型节点类型选择规则和 tool_nodejs 区分
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
- `- 图定义文件(必读/必改此文件):${p.flowYamlAbs}`,
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
- ...(needsNodeTypeRules ? [
347
- "### 节点类型选择(必须遵守)",
348
- "**判据**:**确定性任务 `tool_nodejs`;非确定性任务 `agent_subAgent`**。",
349
- "- **确定性**:相同输入永远产出相同输出,可用普通代码完整描述(CLI/npm 调用、读写文件、JSON/路径转换、调现成 API 解析固定格式、跑脚手架等)",
350
- "- **非确定性**:需要语义理解或创造(代码翻译/生成、源码/文本解析改写、多步推理决策、创意写作)",
351
- "| 场景 | 推荐节点 |",
352
- "|------|----------|",
353
- "| 确定性逻辑(跑命令、读写文件、转换格式、调 API) | **tool_nodejs** + `script` |",
354
- "| 醒目输出 | **tool_print** |",
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 = resolvedIntentCategory === "query"
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);