@fieldwangai/agentflow 0.1.72 → 0.1.74

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.
@@ -81,7 +81,7 @@ import {
81
81
  readComposerSessionMeta,
82
82
  } from "./composer-log.mjs";
83
83
  import { runNodeScript } from "./pipeline-scripts.mjs";
84
- import { readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
84
+ import { computeNextRunAt, readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
85
85
  import { listScheduleStatuses } from "./scheduler.mjs";
86
86
  import {
87
87
  deleteMarketplaceFlowSnippetPackage,
@@ -91,6 +91,7 @@ import {
91
91
  listMarketplacePackages,
92
92
  publishFlowSnippet,
93
93
  publishNodeFromInstance,
94
+ resolveMarketplaceNodePackage,
94
95
  } from "./marketplace.mjs";
95
96
  import { buildGitContext, inferGitRepoRootFromWorktree, loadGitWorktree, normalizeGitContext, runGit, sanitizeWorktreeName, unloadGitWorktree } from "./git-worktree.mjs";
96
97
  import { createGitLabMergeRequest } from "./gitlab-mr.mjs";
@@ -2173,9 +2174,76 @@ function hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped = {}, gra
2173
2174
  return changed ? { ...next, instances } : next;
2174
2175
  }
2175
2176
 
2177
+ function workspaceRuntimeInterpreterForMarketplaceEntry(runtime, entry) {
2178
+ const language = String(runtime?.language || "").trim().toLowerCase();
2179
+ const entryLower = String(entry || "").trim().toLowerCase();
2180
+ if (language.includes("python") || entryLower.endsWith(".py")) return "python3";
2181
+ if (language.includes("shell") || language === "bash" || entryLower.endsWith(".sh") || entryLower.endsWith(".bash")) return "bash";
2182
+ return "node";
2183
+ }
2184
+
2185
+ function workspaceRuntimeArgForMarketplace(arg) {
2186
+ const text = String(arg ?? "").trim();
2187
+ if (!text) return "";
2188
+ if (text.includes("${")) return text;
2189
+ return workspaceShellQuote(text);
2190
+ }
2191
+
2192
+ function workspaceMarketplaceRuntimeCommand(resolved) {
2193
+ const runtime = resolved?.runtime && typeof resolved.runtime === "object" ? resolved.runtime : {};
2194
+ const entry = String(runtime.entry || "").trim().replace(/^\/+/, "");
2195
+ if (entry && resolved?.packageDir) {
2196
+ const entryAbs = path.resolve(resolved.packageDir, ...entry.split(/[\\/]+/).filter(Boolean));
2197
+ const packageRoot = path.resolve(resolved.packageDir);
2198
+ const packageRootWithSep = packageRoot.endsWith(path.sep) ? packageRoot : `${packageRoot}${path.sep}`;
2199
+ if (entryAbs === packageRoot || !entryAbs.startsWith(packageRootWithSep)) return "";
2200
+ const args = Array.isArray(runtime.args) ? runtime.args.map(workspaceRuntimeArgForMarketplace).filter(Boolean) : [];
2201
+ return [workspaceRuntimeInterpreterForMarketplaceEntry(runtime, entry), workspaceShellQuote(entryAbs), ...args].join(" ");
2202
+ }
2203
+ return String(runtime.command || "").trim();
2204
+ }
2205
+
2206
+ function hydrateWorkspaceMarketplaceToolNodejsRuntime(workspaceRoot, scoped = {}, graph = {}, userCtx = {}) {
2207
+ const next = normalizeWorkspaceGraphPayload(graph || {});
2208
+ const instances = { ...(next.instances || {}) };
2209
+ let changed = false;
2210
+ for (const [nodeId, instance] of Object.entries(instances)) {
2211
+ if (!instance || typeof instance !== "object") continue;
2212
+ const marketplaceDefId = String(instance.marketplaceRef || instance.definitionId || "").trim();
2213
+ if (!marketplaceDefId.startsWith("marketplace:")) continue;
2214
+ let resolved = null;
2215
+ try {
2216
+ resolved = resolveMarketplaceNodePackage(
2217
+ workspaceRoot,
2218
+ scoped.root || scoped.scopedRoot || workspaceRoot,
2219
+ marketplaceDefId,
2220
+ next,
2221
+ { userId: userCtx?.userId || "" },
2222
+ );
2223
+ } catch {
2224
+ resolved = null;
2225
+ }
2226
+ if (!resolved || String(resolved.baseDefinitionId || "").trim() !== "tool_nodejs") continue;
2227
+ const script = String(instance.script || "").trim();
2228
+ const scriptRef = String(instance.scriptRef || "").trim();
2229
+ const runtimeScript = script || scriptRef ? "" : workspaceMarketplaceRuntimeCommand(resolved);
2230
+ instances[nodeId] = {
2231
+ ...instance,
2232
+ definitionId: "tool_nodejs",
2233
+ marketplaceRef: resolved.resolvedDefinitionId || marketplaceDefId,
2234
+ marketplacePackageId: resolved.id,
2235
+ marketplaceVersion: resolved.version,
2236
+ ...(runtimeScript ? { script: runtimeScript } : {}),
2237
+ };
2238
+ changed = true;
2239
+ }
2240
+ return changed ? { ...next, instances } : next;
2241
+ }
2242
+
2176
2243
  function hydrateWorkspaceGraphForRuntime(workspaceRoot, scoped = {}, graph = {}, userCtx = {}) {
2177
2244
  const withRefs = hydrateWorkspaceNodeRefsFromFiles(scoped.root || scoped.scopedRoot || workspaceRoot, graph);
2178
- return hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped, withRefs, userCtx);
2245
+ const withMarketplaceRuntime = hydrateWorkspaceMarketplaceToolNodejsRuntime(workspaceRoot, scoped, withRefs, userCtx);
2246
+ return hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped, withMarketplaceRuntime, userCtx);
2179
2247
  }
2180
2248
 
2181
2249
  function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
@@ -3451,6 +3519,7 @@ function workspaceMaterializeImplementationReference(instance, scopedRoot, runPa
3451
3519
  }
3452
3520
 
3453
3521
  function workspaceImplementationBlock(instance, scopedRoot, runPackage = {}) {
3522
+ if (!WORKSPACE_IMPLEMENTATION_REFERENCE_ENABLED) return "";
3454
3523
  const ref = workspaceMaterializeImplementationReference(instance, scopedRoot, runPackage);
3455
3524
  const inline = workspaceImplementationInlineText(instance);
3456
3525
  if (!ref && !inline) return "";
@@ -3478,6 +3547,46 @@ function workspaceDefaultImplementationRef(nodeId) {
3478
3547
  return `nodes/${workspaceSafeNodeFileName(nodeId)}/implementation.md`;
3479
3548
  }
3480
3549
 
3550
+ function workspaceDefaultHistoryRef(nodeId) {
3551
+ return `nodes/${workspaceSafeNodeFileName(nodeId)}/history.md`;
3552
+ }
3553
+
3554
+ function workspaceMaterializeNodeHistoryReference(nodeId, scopedRoot, runPackage = {}) {
3555
+ const historyRef = workspaceDefaultHistoryRef(nodeId);
3556
+ const abs = workspaceResolveFlowFile(scopedRoot, historyRef, "historyRef");
3557
+ const exists = fs.existsSync(abs) && fs.statSync(abs).isFile();
3558
+ const nodeRunDir = String(runPackage?.nodeRunDir || "").trim();
3559
+ let mounted = "";
3560
+ if (exists && nodeRunDir) {
3561
+ try {
3562
+ const mountedRel = path.join("references", "history.md");
3563
+ const dest = path.resolve(nodeRunDir, mountedRel);
3564
+ const nodeRunWithSep = nodeRunDir.endsWith(path.sep) ? nodeRunDir : `${nodeRunDir}${path.sep}`;
3565
+ if (dest === nodeRunDir || dest.startsWith(nodeRunWithSep)) {
3566
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
3567
+ fs.copyFileSync(abs, dest);
3568
+ mounted = mountedRel.split(path.sep).join(path.posix.sep);
3569
+ }
3570
+ } catch {
3571
+ mounted = "";
3572
+ }
3573
+ }
3574
+ return { historyRef, exists, mounted };
3575
+ }
3576
+
3577
+ function workspaceNodeHistoryBlock(nodeId, scopedRoot, runPackage = {}) {
3578
+ const ref = workspaceMaterializeNodeHistoryReference(nodeId, scopedRoot, runPackage);
3579
+ if (!ref?.exists) return "";
3580
+ return [
3581
+ "## 历史参考",
3582
+ "",
3583
+ `- 历史记录文件:\`${ref.mounted || ref.historyRef}\``,
3584
+ ref.mounted ? `- 原始流水线路径:\`${ref.historyRef}\`` : "",
3585
+ "",
3586
+ "该文件只记录之前运行时的 thinking 摘要与最终结论,用作轻量参考;不要把历史当成硬约束。若历史与当前任务、输入或输出要求冲突,以当前任务为准。",
3587
+ ].filter((line) => line !== "").join("\n");
3588
+ }
3589
+
3481
3590
  function workspaceImplementationModeForInstance(instance) {
3482
3591
  const explicit = String(instance?.implementationMode || "").trim();
3483
3592
  if (explicit) return explicit;
@@ -3669,11 +3778,96 @@ async function workspaceGenerateImplementationMarkdown({
3669
3778
  return markdown.replace(/^```(?:markdown|md)?\s*/i, "").replace(/```\s*$/i, "").trim();
3670
3779
  }
3671
3780
 
3781
+ function workspaceHistoryTextFromEvents(events = [], kind, maxItems = 24, maxChars = 12000) {
3782
+ const parts = [];
3783
+ for (const ev of Array.isArray(events) ? events : []) {
3784
+ if (!ev || ev.kind !== kind) continue;
3785
+ const text = String(ev.text || "").trim();
3786
+ if (!text) continue;
3787
+ parts.push(text);
3788
+ if (parts.length >= maxItems) break;
3789
+ }
3790
+ return workspaceClipImplementationText(parts.join("\n"), maxChars);
3791
+ }
3792
+
3793
+ function workspaceBuildNodeHistoryEntry(instance, nodeId, opts = {}) {
3794
+ const inputValues = opts.inputValues || {};
3795
+ const structured = opts.structured && typeof opts.structured === "object" ? opts.structured : {};
3796
+ const result = String(opts.resultContent || structured.result || "").trim();
3797
+ const task = workspaceResolveBodyPlaceholders(instance?.body || "", inputValues).trim();
3798
+ const thinking = workspaceHistoryTextFromEvents(opts.historyEvents || [], "thinking", 40, 16000);
3799
+ const assistant = workspaceHistoryTextFromEvents(opts.historyEvents || [], "assistant", 8, 8000);
3800
+ const resultEvent = workspaceHistoryTextFromEvents(opts.historyEvents || [], "result", 4, 12000);
3801
+ const conclusion = workspaceClipImplementationText(resultEvent || result || assistant, 20000);
3802
+ return [
3803
+ `## ${new Date().toISOString()} · ${nodeId}`,
3804
+ "",
3805
+ `label: ${String(instance?.label || nodeId || "node")}`,
3806
+ `definitionId: ${String(instance?.definitionId || "(unknown)")}`,
3807
+ "",
3808
+ "### 任务",
3809
+ "",
3810
+ workspaceClipImplementationText(task || instance?.body || instance?.scriptRef || instance?.script || "(无显式任务)", 6000),
3811
+ "",
3812
+ Object.keys(inputValues || {}).length ? "### 输入摘要" : "",
3813
+ Object.keys(inputValues || {}).length ? "" : "",
3814
+ Object.keys(inputValues || {}).length ? workspaceClipImplementationText(JSON.stringify(inputValues, null, 2), 8000) : "",
3815
+ Object.keys(inputValues || {}).length ? "" : "",
3816
+ thinking ? "### Thinking 摘要" : "",
3817
+ thinking ? "" : "",
3818
+ thinking || "",
3819
+ thinking ? "" : "",
3820
+ "### 结论",
3821
+ "",
3822
+ conclusion || "(无结论内容)",
3823
+ "",
3824
+ "### 输出协议",
3825
+ "",
3826
+ JSON.stringify({
3827
+ result: workspaceClipImplementationText(structured.result || result, 4000),
3828
+ resultFile: structured.resultFile || "",
3829
+ outParams: structured.outParams || {},
3830
+ }, null, 2),
3831
+ ].filter((line) => line !== "").join("\n");
3832
+ }
3833
+
3834
+ function workspaceClipNodeHistory(value, maxChars = WORKSPACE_NODE_HISTORY_MAX_CHARS) {
3835
+ const text = String(value || "").trim();
3836
+ if (text.length <= maxChars) return text;
3837
+ return [
3838
+ "# Workspace Node History",
3839
+ "",
3840
+ "> Older history was truncated to keep this reference lightweight.",
3841
+ "",
3842
+ text.slice(-maxChars),
3843
+ ].join("\n").trim();
3844
+ }
3845
+
3846
+ function workspacePersistNodeHistory(scopedRoot, graph, nodeId, opts = {}) {
3847
+ const current = graph?.instances?.[nodeId];
3848
+ if (!current || String(current.definitionId || "") === "workspace_run" || String(current.definitionId || "") === "workspace_scheduled_run") {
3849
+ return { changed: false, wrote: false, instance: current };
3850
+ }
3851
+ const historyRef = workspaceDefaultHistoryRef(nodeId);
3852
+ const abs = workspaceResolveFlowFile(scopedRoot, historyRef, "historyRef");
3853
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
3854
+ const previous = fs.existsSync(abs) && fs.statSync(abs).isFile()
3855
+ ? workspaceReadTextFileIfExists(abs, WORKSPACE_NODE_HISTORY_MAX_CHARS + 20000)
3856
+ : "# Workspace Node History\n";
3857
+ const entry = workspaceBuildNodeHistoryEntry(current, nodeId, opts);
3858
+ const nextText = workspaceClipNodeHistory(`${previous.trimEnd()}\n\n${entry}\n`);
3859
+ fs.writeFileSync(abs, `${nextText.trimEnd()}\n`, "utf-8");
3860
+ return { changed: false, wrote: true, instance: current, historyRef };
3861
+ }
3862
+
3672
3863
  async function workspacePersistNodeImplementation(scopedRoot, graph, nodeId, opts = {}) {
3673
3864
  const current = graph?.instances?.[nodeId];
3674
3865
  if (!current || String(current.definitionId || "") === "workspace_run" || String(current.definitionId || "") === "workspace_scheduled_run") {
3675
3866
  return { changed: false, wrote: false, instance: current };
3676
3867
  }
3868
+ if (!WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED) {
3869
+ return workspacePersistNodeHistory(scopedRoot, graph, nodeId, opts);
3870
+ }
3677
3871
  const existingRef = String(current.implementationRef || "").trim();
3678
3872
  const implementationRef = existingRef || workspaceDefaultImplementationRef(nodeId);
3679
3873
  const abs = workspaceResolveFlowFile(scopedRoot, implementationRef, "implementationRef");
@@ -3715,7 +3909,9 @@ async function workspaceTryPersistNodeImplementation(scopedRoot, graph, nodeId,
3715
3909
  opts.emit?.({
3716
3910
  type: "natural",
3717
3911
  kind: "warning",
3718
- text: `实现方案未更新:${e?.message || String(e)}`,
3912
+ text: WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED
3913
+ ? `实现方案未更新:${e?.message || String(e)}`
3914
+ : `历史记录未更新:${e?.message || String(e)}`,
3719
3915
  });
3720
3916
  return { changed: false, wrote: false, instance: graph?.instances?.[nodeId] };
3721
3917
  }
@@ -4747,8 +4943,8 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4747
4943
  } catch {
4748
4944
  // Best-effort debug artifact only.
4749
4945
  }
4750
- const implementationBlock = workspaceImplementationBlock(instance, scopedRoot, runPackage);
4751
- const prompt = workspaceNodePrompt(graph, nodeId, promptUpstreamText, promptSkillsBlock, promptMcpBlock, runtimeInputValues, runPackage, implementationBlock);
4946
+ const historyBlock = workspaceNodeHistoryBlock(nodeId, scopedRoot, runPackage);
4947
+ const prompt = workspaceNodePrompt(graph, nodeId, promptUpstreamText, promptSkillsBlock, promptMcpBlock, runtimeInputValues, runPackage, historyBlock);
4752
4948
  try {
4753
4949
  fs.writeFileSync(path.join(runPackage.nodeRunDir, "prompt.md"), prompt.trimEnd() + "\n", "utf-8");
4754
4950
  } catch {
@@ -4763,6 +4959,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4763
4959
  });
4764
4960
  emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
4765
4961
  let content = "";
4962
+ const runHistoryEvents = [];
4766
4963
  const maxAttempts = 3;
4767
4964
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4768
4965
  let attemptContent = "";
@@ -4792,6 +4989,12 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4792
4989
  ? { ...ev, text: workspaceCanonicalAgentOutput(ev.text), nodeId }
4793
4990
  : { ...ev, nodeId };
4794
4991
  emit(eventToEmit);
4992
+ if (ev?.type === "natural" && typeof ev.text === "string") {
4993
+ const kind = String(ev.kind || "");
4994
+ if (kind === "thinking" || kind === "assistant" || kind === "result") {
4995
+ runHistoryEvents.push({ kind, text: ev.text });
4996
+ }
4997
+ }
4795
4998
  if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
4796
4999
  attemptLastAssistantContent = ev.text;
4797
5000
  attemptContent += (attemptContent ? "\n" : "") + ev.text;
@@ -4841,6 +5044,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4841
5044
  runPackage,
4842
5045
  modelKey,
4843
5046
  userCtx,
5047
+ historyEvents: runHistoryEvents,
4844
5048
  emit: (event) => emit({ ...event, nodeId }),
4845
5049
  onActiveChild: opts.onActiveChild,
4846
5050
  });
@@ -5040,15 +5244,61 @@ function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userI
5040
5244
 
5041
5245
  /** 正在执行的 flow run(flowId → { child, runUuid });同一 flow 只允许一个 run */
5042
5246
  const activeFlowRuns = new Map();
5043
- /** 正在执行的 Workspace 临时 run(flowId → { controller, child, runNodeId, startedAt });同一 flow 只允许一个 run */
5247
+ /** 正在执行的 Workspace 临时 run(runId/sessionId → { controller, child, runNodeId, startedAt, plannedNodeIds } */
5044
5248
  const activeWorkspaceRuns = new Map();
5045
5249
  const WORKSPACE_SCHEDULES_FILENAME = "workspace-schedules.json";
5046
5250
  const WORKSPACE_SCHEDULE_POLL_MS = 30_000;
5251
+ const WORKSPACE_IMPLEMENTATION_REFERENCE_ENABLED = false;
5252
+ const WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED = false;
5253
+ const WORKSPACE_NODE_HISTORY_MAX_CHARS = 80000;
5254
+
5255
+ function workspaceIntervalMinutesToCron(intervalMinutes) {
5256
+ const n = Number(intervalMinutes);
5257
+ if (!Number.isFinite(n) || n <= 0) return "0 9 * * *";
5258
+ const minutes = Math.max(1, Math.min(1440, Math.round(n)));
5259
+ if (minutes < 60) return `*/${minutes} * * * *`;
5260
+ if (minutes === 60) return "0 * * * *";
5261
+ if (minutes < 1440 && minutes % 60 === 0) return `0 */${minutes / 60} * * *`;
5262
+ return "0 9 * * *";
5263
+ }
5047
5264
 
5048
5265
  function workspaceRunKey(userCtx, flowSource, flowId) {
5049
5266
  return `${userCtx?.userId || ""}:${flowSource || "user"}:${flowId}`;
5050
5267
  }
5051
5268
 
5269
+ function workspaceRunEntryKey(scopeKey, runId) {
5270
+ return `${scopeKey}:${String(runId || "").trim() || runLedgerId("workspace")}`;
5271
+ }
5272
+
5273
+ function workspaceActiveRunsForScope(scopeKey) {
5274
+ const key = String(scopeKey || "");
5275
+ return Array.from(activeWorkspaceRuns.entries())
5276
+ .filter(([, entry]) => String(entry?.scopeKey || "") === key);
5277
+ }
5278
+
5279
+ function workspaceRunPlanNodeIds(runNodeId, plan) {
5280
+ return Array.from(new Set([
5281
+ String(runNodeId || "").trim(),
5282
+ ...(Array.isArray(plan?.order) ? plan.order : []),
5283
+ ...(Array.isArray(plan?.pauseNodeIds) ? plan.pauseNodeIds : []),
5284
+ ].map((id) => String(id || "").trim()).filter(Boolean)));
5285
+ }
5286
+
5287
+ function workspaceFindActiveRunConflict(scopeKey, plannedNodeIds) {
5288
+ const planned = new Set((plannedNodeIds || []).map((id) => String(id || "").trim()).filter(Boolean));
5289
+ for (const [key, entry] of workspaceActiveRunsForScope(scopeKey)) {
5290
+ const activeIds = Array.isArray(entry?.plannedNodeIds) ? entry.plannedNodeIds : [];
5291
+ if (!activeIds.length) {
5292
+ return { key, entry, conflictNodeIds: [] };
5293
+ }
5294
+ const conflictNodeIds = activeIds
5295
+ .map((id) => String(id || "").trim())
5296
+ .filter((id) => id && planned.has(id));
5297
+ if (conflictNodeIds.length) return { key, entry, conflictNodeIds };
5298
+ }
5299
+ return null;
5300
+ }
5301
+
5052
5302
  function normalizeWorkspaceScheduledRunConfig(raw) {
5053
5303
  let parsed = {};
5054
5304
  const text = String(raw || "").trim();
@@ -5060,14 +5310,29 @@ function normalizeWorkspaceScheduledRunConfig(raw) {
5060
5310
  }
5061
5311
  }
5062
5312
  const intervalMinutes = Number(parsed.intervalMinutes);
5313
+ const migratedCron = workspaceIntervalMinutesToCron(intervalMinutes);
5314
+ const cron = typeof parsed.cron === "string" && parsed.cron.trim()
5315
+ ? parsed.cron.trim()
5316
+ : migratedCron;
5317
+ const timezone = typeof parsed.timezone === "string" && parsed.timezone.trim()
5318
+ ? parsed.timezone.trim()
5319
+ : "Asia/Shanghai";
5320
+ const targetRunNodeId = typeof parsed.targetRunNodeId === "string" ? parsed.targetRunNodeId.trim() : "";
5321
+ const overlapPolicy = parsed.overlapPolicy === "skip" ? "skip" : "skip";
5063
5322
  return {
5064
5323
  enabled: parsed.enabled === true,
5065
- intervalMinutes: Number.isFinite(intervalMinutes) && intervalMinutes > 0
5066
- ? Math.min(Math.max(Math.round(intervalMinutes), 1), 1440)
5067
- : 60,
5324
+ cron,
5325
+ timezone,
5326
+ targetRunNodeId,
5327
+ overlapPolicy,
5068
5328
  };
5069
5329
  }
5070
5330
 
5331
+ function workspaceScheduleNextRunAt(config, fromDate = new Date()) {
5332
+ if (!config?.enabled || !config?.cron) return null;
5333
+ return Date.parse(computeNextRunAt(config.cron, config.timezone || "Asia/Shanghai", fromDate));
5334
+ }
5335
+
5071
5336
  function workspaceSchedulesPath() {
5072
5337
  return path.join(getAgentflowDataRoot(), WORKSPACE_SCHEDULES_FILENAME);
5073
5338
  }
@@ -5098,12 +5363,12 @@ function writeWorkspaceScheduleRegistry(registry) {
5098
5363
  }, null, 2) + "\n", "utf-8");
5099
5364
  }
5100
5365
 
5101
- function workspaceScheduleKey(userId, flowSource, flowId, runNodeId) {
5366
+ function workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId) {
5102
5367
  return [
5103
5368
  String(userId || ""),
5104
5369
  String(flowSource || "user"),
5105
5370
  String(flowId || ""),
5106
- String(runNodeId || ""),
5371
+ String(scheduleNodeId || ""),
5107
5372
  ].join(":");
5108
5373
  }
5109
5374
 
@@ -5116,7 +5381,14 @@ function listWorkspaceScheduleStatusesForFlow(userCtx = {}, flowSource = "user",
5116
5381
  String(entry?.flowSource || "user") === String(flowSource || "user") &&
5117
5382
  String(entry?.flowId || "") === String(flowId || "")
5118
5383
  ))
5119
- .sort((a, b) => String(a.runNodeId || "").localeCompare(String(b.runNodeId || "")));
5384
+ .sort((a, b) => String(a.scheduleNodeId || a.runNodeId || "").localeCompare(String(b.scheduleNodeId || b.runNodeId || "")));
5385
+ }
5386
+
5387
+ function workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config = {}) {
5388
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
5389
+ return String(instances[scheduleNodeId]?.definitionId || "") === "workspace_scheduled_run"
5390
+ ? String(scheduleNodeId || "")
5391
+ : "";
5120
5392
  }
5121
5393
 
5122
5394
  function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx = {}) {
@@ -5133,14 +5405,35 @@ function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx =
5133
5405
  if (key.startsWith(prefix)) delete schedules[key];
5134
5406
  }
5135
5407
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
5136
- for (const [runNodeId, instance] of Object.entries(instances)) {
5408
+ for (const [scheduleNodeId, instance] of Object.entries(instances)) {
5137
5409
  if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
5138
5410
  const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
5139
5411
  if (!config.enabled) continue;
5140
- const key = workspaceScheduleKey(userId, flowSource, flowId, runNodeId);
5141
- const intervalMs = config.intervalMinutes * 60 * 1000;
5412
+ const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
5413
+ const key = workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId);
5142
5414
  const previous = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
5143
5415
  const previousNext = Number(previous.nextRunAt || 0);
5416
+ const previousMatches = (
5417
+ String(previous.cron || "") === config.cron &&
5418
+ String(previous.timezone || "") === config.timezone &&
5419
+ String(previous.targetRunNodeId || previous.runNodeId || "") === targetRunNodeId
5420
+ );
5421
+ let nextRunAt = null;
5422
+ let lastStatus = previous.lastStatus || "armed";
5423
+ let lastError = previous.lastError || "";
5424
+ try {
5425
+ nextRunAt = previousMatches && Number.isFinite(previousNext) && previousNext > now
5426
+ ? previousNext
5427
+ : workspaceScheduleNextRunAt(config, new Date(now));
5428
+ if (!targetRunNodeId) {
5429
+ lastStatus = "invalid";
5430
+ lastError = "Scheduled Run node is missing";
5431
+ }
5432
+ } catch (e) {
5433
+ lastStatus = "invalid";
5434
+ lastError = (e && e.message) || String(e);
5435
+ nextRunAt = null;
5436
+ }
5144
5437
  schedules[key] = {
5145
5438
  ...previous,
5146
5439
  key,
@@ -5149,11 +5442,16 @@ function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx =
5149
5442
  username: String(authUser?.username || previous.username || userId),
5150
5443
  flowId,
5151
5444
  flowSource,
5152
- runNodeId,
5445
+ scheduleNodeId,
5446
+ runNodeId: targetRunNodeId,
5447
+ targetRunNodeId,
5153
5448
  label: String(instance.label || "Scheduled Run"),
5154
- intervalMinutes: config.intervalMinutes,
5155
- nextRunAt: Number.isFinite(previousNext) && previousNext > now ? previousNext : now + intervalMs,
5156
- lastStatus: previous.lastStatus || "armed",
5449
+ cron: config.cron,
5450
+ timezone: config.timezone,
5451
+ overlapPolicy: config.overlapPolicy,
5452
+ nextRunAt,
5453
+ lastStatus,
5454
+ lastError,
5157
5455
  updatedAt: nowIso,
5158
5456
  };
5159
5457
  }
@@ -5178,17 +5476,22 @@ function updateWorkspaceScheduleEntry(key, patch) {
5178
5476
 
5179
5477
  async function runWorkspaceScheduledEntry(root, entry) {
5180
5478
  const userCtx = { userId: String(entry.userId || "") };
5181
- const runKey = workspaceRunKey(userCtx, entry.flowSource || "user", entry.flowId || "");
5182
- const intervalMs = Math.max(1, Number(entry.intervalMinutes || 60)) * 60 * 1000;
5183
- const nextRunAt = Date.now() + intervalMs;
5184
- if (activeWorkspaceRuns.has(runKey)) {
5185
- updateWorkspaceScheduleEntry(entry.key, {
5186
- nextRunAt,
5187
- lastSkippedAt: Date.now(),
5188
- lastStatus: "skipped: busy",
5189
- });
5190
- return;
5191
- }
5479
+ const scopeKey = workspaceRunKey(userCtx, entry.flowSource || "user", entry.flowId || "");
5480
+ const fallbackConfig = {
5481
+ enabled: true,
5482
+ cron: String(entry.cron || "0 9 * * *"),
5483
+ timezone: String(entry.timezone || "Asia/Shanghai"),
5484
+ targetRunNodeId: String(entry.targetRunNodeId || entry.runNodeId || ""),
5485
+ overlapPolicy: "skip",
5486
+ };
5487
+ const computeNext = (config = fallbackConfig) => {
5488
+ try {
5489
+ return workspaceScheduleNextRunAt(config, new Date());
5490
+ } catch {
5491
+ return null;
5492
+ }
5493
+ };
5494
+ let nextRunAt = computeNext(fallbackConfig);
5192
5495
  const scoped = resolveWorkspaceScopeRoot(root, {
5193
5496
  flowId: entry.flowId || "",
5194
5497
  flowSource: entry.flowSource || "user",
@@ -5204,8 +5507,10 @@ async function runWorkspaceScheduledEntry(root, entry) {
5204
5507
  }
5205
5508
  const graphPath = workspaceGraphPath(scoped.root);
5206
5509
  const graph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root).graph, userCtx);
5207
- const instance = graph.instances?.[entry.runNodeId];
5510
+ const scheduleNodeId = String(entry.scheduleNodeId || entry.key?.split(":").pop() || "");
5511
+ const instance = graph.instances?.[scheduleNodeId];
5208
5512
  const config = normalizeWorkspaceScheduledRunConfig(instance?.body || "");
5513
+ nextRunAt = computeNext(config);
5209
5514
  if (!instance || String(instance.definitionId || "") !== "workspace_scheduled_run" || !config.enabled) {
5210
5515
  updateWorkspaceScheduleEntry(entry.key, {
5211
5516
  enabled: false,
@@ -5214,19 +5519,56 @@ async function runWorkspaceScheduledEntry(root, entry) {
5214
5519
  });
5215
5520
  return;
5216
5521
  }
5522
+ const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
5523
+ if (!targetRunNodeId) {
5524
+ updateWorkspaceScheduleEntry(entry.key, {
5525
+ nextRunAt,
5526
+ lastStatus: "invalid",
5527
+ lastError: "Scheduled Run node is missing",
5528
+ lastErrorAt: Date.now(),
5529
+ });
5530
+ return;
5531
+ }
5532
+ let plan;
5533
+ try {
5534
+ plan = workspaceRunPlan(graph, targetRunNodeId, scoped.root);
5535
+ } catch (e) {
5536
+ updateWorkspaceScheduleEntry(entry.key, {
5537
+ nextRunAt,
5538
+ lastStatus: "failed",
5539
+ lastError: (e && e.message) || String(e),
5540
+ lastErrorAt: Date.now(),
5541
+ });
5542
+ return;
5543
+ }
5544
+ const plannedNodeIds = workspaceRunPlanNodeIds(targetRunNodeId, plan);
5545
+ const conflict = workspaceFindActiveRunConflict(scopeKey, plannedNodeIds);
5546
+ if (conflict) {
5547
+ updateWorkspaceScheduleEntry(entry.key, {
5548
+ nextRunAt,
5549
+ lastSkippedAt: Date.now(),
5550
+ lastStatus: "skipped: busy",
5551
+ lastError: "",
5552
+ });
5553
+ return;
5554
+ }
5217
5555
 
5218
5556
  const controller = new AbortController();
5219
5557
  const authUsers = readAuthUsers();
5220
5558
  const authUser = authUsers[userCtx.userId] || {};
5559
+ const runId = runLedgerId("workspace");
5560
+ const runKey = workspaceRunEntryKey(scopeKey, runId);
5221
5561
  const runEntry = {
5562
+ scopeKey,
5222
5563
  controller,
5223
5564
  child: null,
5224
- runId: runLedgerId("workspace"),
5565
+ runId,
5225
5566
  userId: userCtx.userId,
5226
5567
  username: String(authUser.username || entry.username || userCtx.userId),
5227
- runNodeId: String(entry.runNodeId || ""),
5568
+ runNodeId: targetRunNodeId,
5228
5569
  flowId: String(entry.flowId || ""),
5229
5570
  flowSource: String(entry.flowSource || "user"),
5571
+ plannedNodeIds,
5230
5572
  startedAt: Date.now(),
5231
5573
  scheduled: true,
5232
5574
  stopChild() {
@@ -5241,6 +5583,10 @@ async function runWorkspaceScheduledEntry(root, entry) {
5241
5583
  lastStatus: "running",
5242
5584
  lastTriggeredAt: runEntry.startedAt,
5243
5585
  lastRunId: runEntry.runId,
5586
+ runNodeId: targetRunNodeId,
5587
+ targetRunNodeId,
5588
+ cron: config.cron,
5589
+ timezone: config.timezone,
5244
5590
  lastError: "",
5245
5591
  });
5246
5592
  const setActiveChild = (child) => {
@@ -5251,7 +5597,7 @@ async function runWorkspaceScheduledEntry(root, entry) {
5251
5597
  const result = await runWorkspaceGraph(root, scoped.root, {
5252
5598
  flowId: entry.flowId,
5253
5599
  flowSource: entry.flowSource || "user",
5254
- runNodeId: entry.runNodeId,
5600
+ runNodeId: targetRunNodeId,
5255
5601
  graph,
5256
5602
  }, userCtx, {
5257
5603
  signal: controller.signal,
@@ -5264,7 +5610,7 @@ async function runWorkspaceScheduledEntry(root, entry) {
5264
5610
  const endedAt = Date.now();
5265
5611
  appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "success");
5266
5612
  updateWorkspaceScheduleEntry(entry.key, {
5267
- nextRunAt: Date.now() + intervalMs,
5613
+ nextRunAt: computeNext(config),
5268
5614
  lastFinishedAt: endedAt,
5269
5615
  lastStatus: "success",
5270
5616
  lastError: "",
@@ -5273,13 +5619,13 @@ async function runWorkspaceScheduledEntry(root, entry) {
5273
5619
  const endedAt = Date.now();
5274
5620
  appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "failed");
5275
5621
  updateWorkspaceScheduleEntry(entry.key, {
5276
- nextRunAt: Date.now() + intervalMs,
5622
+ nextRunAt: computeNext(config),
5277
5623
  lastFinishedAt: endedAt,
5278
5624
  lastStatus: "failed",
5279
5625
  lastError: (e && e.message) || String(e),
5280
5626
  lastErrorAt: endedAt,
5281
5627
  });
5282
- log.info(`[workspace-scheduler] failed ${entry.flowId}/${entry.runNodeId}: ${(e && e.message) || String(e)}`);
5628
+ log.info(`[workspace-scheduler] failed ${entry.flowId}/${targetRunNodeId}: ${(e && e.message) || String(e)}`);
5283
5629
  } finally {
5284
5630
  if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
5285
5631
  }
@@ -5292,8 +5638,25 @@ function pollWorkspaceSchedules(root) {
5292
5638
  if (!entry || entry.enabled !== true) continue;
5293
5639
  const nextRunAt = Number(entry.nextRunAt || 0);
5294
5640
  if (!Number.isFinite(nextRunAt) || nextRunAt <= 0) {
5641
+ const config = {
5642
+ enabled: true,
5643
+ cron: String(entry.cron || "0 9 * * *"),
5644
+ timezone: String(entry.timezone || "Asia/Shanghai"),
5645
+ };
5646
+ let computedNext = null;
5647
+ try {
5648
+ computedNext = workspaceScheduleNextRunAt(config, new Date(now));
5649
+ } catch (e) {
5650
+ updateWorkspaceScheduleEntry(entry.key, {
5651
+ nextRunAt: null,
5652
+ lastStatus: "invalid",
5653
+ lastError: (e && e.message) || String(e),
5654
+ lastErrorAt: now,
5655
+ });
5656
+ continue;
5657
+ }
5295
5658
  updateWorkspaceScheduleEntry(entry.key, {
5296
- nextRunAt: now + Math.max(1, Number(entry.intervalMinutes || 60)) * 60 * 1000,
5659
+ nextRunAt: computedNext,
5297
5660
  lastStatus: entry.lastStatus || "armed",
5298
5661
  });
5299
5662
  continue;
@@ -6079,6 +6442,53 @@ export function startUiServer({
6079
6442
  return;
6080
6443
  }
6081
6444
 
6445
+ if (req.method === "POST" && url.pathname === "/api/workspace/run/plan") {
6446
+ let payload;
6447
+ try {
6448
+ payload = JSON.parse(await readBody(req));
6449
+ } catch {
6450
+ json(res, 400, { error: "Invalid JSON body" });
6451
+ return;
6452
+ }
6453
+ try {
6454
+ const scoped = resolveWorkspaceScopeRoot(root, {
6455
+ flowId: payload.flowId || "",
6456
+ flowSource: payload.flowSource || "user",
6457
+ archived: payload.archived === true || payload.flowArchived === true,
6458
+ }, userCtx);
6459
+ if (scoped.error) {
6460
+ json(res, 400, { error: scoped.error });
6461
+ return;
6462
+ }
6463
+ const flowId = String(payload.flowId || "").trim();
6464
+ if (!flowId) {
6465
+ json(res, 400, { error: "Missing flowId" });
6466
+ return;
6467
+ }
6468
+ const graph = hydrateWorkspaceGraphForRuntime(root, scoped, payload.graph || {}, userCtx);
6469
+ const runNodeId = String(payload.runNodeId || "").trim();
6470
+ const plan = workspaceRunPlan(graph, runNodeId, scoped.root);
6471
+ const plannedNodeIds = workspaceRunPlanNodeIds(runNodeId, plan);
6472
+ const scopeKey = workspaceRunKey(userCtx, scoped.flowSource || payload.flowSource || "user", flowId);
6473
+ const conflict = workspaceFindActiveRunConflict(scopeKey, plannedNodeIds);
6474
+ json(res, 200, {
6475
+ ok: true,
6476
+ runNodeId,
6477
+ order: plan.order,
6478
+ pauseNodeIds: plan.pauseNodeIds,
6479
+ plannedNodeIds,
6480
+ conflict: conflict ? {
6481
+ runId: conflict.entry?.runId || "",
6482
+ runNodeId: conflict.entry?.runNodeId || "",
6483
+ conflictNodeIds: conflict.conflictNodeIds,
6484
+ } : null,
6485
+ });
6486
+ } catch (e) {
6487
+ json(res, 500, { error: (e && e.message) || String(e) });
6488
+ }
6489
+ return;
6490
+ }
6491
+
6082
6492
  if (req.method === "POST" && url.pathname === "/api/workspace/run") {
6083
6493
  let payload;
6084
6494
  try {
@@ -6107,21 +6517,35 @@ export function startUiServer({
6107
6517
  json(res, 400, { error: "Missing flowId" });
6108
6518
  return;
6109
6519
  }
6110
- const runKey = workspaceRunKey(userCtx, scoped.flowSource || payload.flowSource || "user", flowId);
6111
- if (activeWorkspaceRuns.has(runKey)) {
6112
- json(res, 409, { error: "该 Workspace 正在运行" });
6520
+ const runtimeGraph = hydrateWorkspaceGraphForRuntime(root, scoped, payload.graph || {}, userCtx);
6521
+ const runNodeId = String(payload.runNodeId || "").trim();
6522
+ const plan = workspaceRunPlan(runtimeGraph, runNodeId, scoped.root);
6523
+ const plannedNodeIds = workspaceRunPlanNodeIds(runNodeId, plan);
6524
+ const scopeKey = workspaceRunKey(userCtx, scoped.flowSource || payload.flowSource || "user", flowId);
6525
+ const conflict = workspaceFindActiveRunConflict(scopeKey, plannedNodeIds);
6526
+ if (conflict) {
6527
+ json(res, 409, {
6528
+ error: "该 Run 与正在执行的 Run 共享节点",
6529
+ runNodeId: conflict.entry?.runNodeId || "",
6530
+ runId: conflict.entry?.runId || "",
6531
+ conflictNodeIds: conflict.conflictNodeIds,
6532
+ });
6113
6533
  return;
6114
6534
  }
6115
6535
  const controller = new AbortController();
6536
+ const runId = String(payload.runSessionId || payload.runId || "").trim() || runLedgerId("workspace");
6537
+ const runKey = workspaceRunEntryKey(scopeKey, runId);
6116
6538
  const runEntry = {
6539
+ scopeKey,
6117
6540
  controller,
6118
6541
  child: null,
6119
- runId: runLedgerId("workspace"),
6542
+ runId,
6120
6543
  userId: String(userCtx.userId || ""),
6121
6544
  username: String(authUser?.username || userCtx.userId || ""),
6122
- runNodeId: String(payload.runNodeId || "").trim(),
6545
+ runNodeId,
6123
6546
  flowId,
6124
6547
  flowSource: scoped.flowSource || payload.flowSource || "user",
6548
+ plannedNodeIds,
6125
6549
  startedAt: Date.now(),
6126
6550
  stopChild() {
6127
6551
  if (this.child && !this.child.killed) {
@@ -6235,14 +6659,22 @@ export function startUiServer({
6235
6659
  return;
6236
6660
  }
6237
6661
  const flowSource = url.searchParams.get("flowSource") || "user";
6238
- const runKey = workspaceRunKey(userCtx, flowSource, flowId);
6239
- const entry = activeWorkspaceRuns.get(runKey);
6662
+ const scopeKey = workspaceRunKey(userCtx, flowSource, flowId);
6663
+ const entries = workspaceActiveRunsForScope(scopeKey).map(([, entry]) => entry);
6664
+ const entry = entries[0] || null;
6240
6665
  json(res, 200, {
6241
- running: Boolean(entry),
6666
+ running: entries.length > 0,
6242
6667
  flowId,
6243
6668
  flowSource,
6244
6669
  runNodeId: entry?.runNodeId || "",
6245
6670
  startedAt: entry?.startedAt || null,
6671
+ runs: entries.map((item) => ({
6672
+ runId: item?.runId || "",
6673
+ runNodeId: item?.runNodeId || "",
6674
+ startedAt: item?.startedAt || null,
6675
+ plannedNodeIds: Array.isArray(item?.plannedNodeIds) ? item.plannedNodeIds : [],
6676
+ scheduled: item?.scheduled === true,
6677
+ })),
6246
6678
  });
6247
6679
  return;
6248
6680
  }
@@ -6260,8 +6692,14 @@ export function startUiServer({
6260
6692
  json(res, 400, { error: "Missing flowId" });
6261
6693
  return;
6262
6694
  }
6263
- const runKey = workspaceRunKey(userCtx, payload.flowSource || "user", flowId);
6264
- const entry = activeWorkspaceRuns.get(runKey);
6695
+ const scopeKey = workspaceRunKey(userCtx, payload.flowSource || "user", flowId);
6696
+ const runId = String(payload.runId || payload.runSessionId || "").trim();
6697
+ const runNodeId = String(payload.runNodeId || "").trim();
6698
+ const entries = workspaceActiveRunsForScope(scopeKey);
6699
+ const match = entries.find(([, item]) => runId && String(item?.runId || "") === runId)
6700
+ || entries.find(([, item]) => runNodeId && String(item?.runNodeId || "") === runNodeId)
6701
+ || (!runId && !runNodeId && entries.length === 1 ? entries[0] : null);
6702
+ const entry = match?.[1] || null;
6265
6703
  if (!entry) {
6266
6704
  json(res, 404, { error: "该 Workspace 未在运行" });
6267
6705
  return;