@fieldwangai/agentflow 0.1.72 → 0.1.73

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,23 @@ 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
+ const explicit = String(config.targetRunNodeId || "").trim();
5390
+ if (explicit && String(instances[explicit]?.definitionId || "") === "workspace_run") return explicit;
5391
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
5392
+ for (const edge of edges) {
5393
+ const source = String(edge?.source || "");
5394
+ const target = String(edge?.target || "");
5395
+ if (source !== String(scheduleNodeId || "") || !target) continue;
5396
+ if (!workspaceIsControlEdge(graph, edge)) continue;
5397
+ if (String(instances[target]?.definitionId || "") === "workspace_run") return target;
5398
+ }
5399
+ const firstRun = Object.entries(instances).find(([, instance]) => String(instance?.definitionId || "") === "workspace_run");
5400
+ return firstRun ? firstRun[0] : "";
5120
5401
  }
5121
5402
 
5122
5403
  function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx = {}) {
@@ -5133,14 +5414,35 @@ function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx =
5133
5414
  if (key.startsWith(prefix)) delete schedules[key];
5134
5415
  }
5135
5416
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
5136
- for (const [runNodeId, instance] of Object.entries(instances)) {
5417
+ for (const [scheduleNodeId, instance] of Object.entries(instances)) {
5137
5418
  if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
5138
5419
  const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
5139
5420
  if (!config.enabled) continue;
5140
- const key = workspaceScheduleKey(userId, flowSource, flowId, runNodeId);
5141
- const intervalMs = config.intervalMinutes * 60 * 1000;
5421
+ const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
5422
+ const key = workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId);
5142
5423
  const previous = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
5143
5424
  const previousNext = Number(previous.nextRunAt || 0);
5425
+ const previousMatches = (
5426
+ String(previous.cron || "") === config.cron &&
5427
+ String(previous.timezone || "") === config.timezone &&
5428
+ String(previous.targetRunNodeId || previous.runNodeId || "") === targetRunNodeId
5429
+ );
5430
+ let nextRunAt = null;
5431
+ let lastStatus = previous.lastStatus || "armed";
5432
+ let lastError = previous.lastError || "";
5433
+ try {
5434
+ nextRunAt = previousMatches && Number.isFinite(previousNext) && previousNext > now
5435
+ ? previousNext
5436
+ : workspaceScheduleNextRunAt(config, new Date(now));
5437
+ if (!targetRunNodeId) {
5438
+ lastStatus = "invalid";
5439
+ lastError = "No target Run node selected or connected";
5440
+ }
5441
+ } catch (e) {
5442
+ lastStatus = "invalid";
5443
+ lastError = (e && e.message) || String(e);
5444
+ nextRunAt = null;
5445
+ }
5144
5446
  schedules[key] = {
5145
5447
  ...previous,
5146
5448
  key,
@@ -5149,11 +5451,16 @@ function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx =
5149
5451
  username: String(authUser?.username || previous.username || userId),
5150
5452
  flowId,
5151
5453
  flowSource,
5152
- runNodeId,
5454
+ scheduleNodeId,
5455
+ runNodeId: targetRunNodeId,
5456
+ targetRunNodeId,
5153
5457
  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",
5458
+ cron: config.cron,
5459
+ timezone: config.timezone,
5460
+ overlapPolicy: config.overlapPolicy,
5461
+ nextRunAt,
5462
+ lastStatus,
5463
+ lastError,
5157
5464
  updatedAt: nowIso,
5158
5465
  };
5159
5466
  }
@@ -5178,17 +5485,22 @@ function updateWorkspaceScheduleEntry(key, patch) {
5178
5485
 
5179
5486
  async function runWorkspaceScheduledEntry(root, entry) {
5180
5487
  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
- }
5488
+ const scopeKey = workspaceRunKey(userCtx, entry.flowSource || "user", entry.flowId || "");
5489
+ const fallbackConfig = {
5490
+ enabled: true,
5491
+ cron: String(entry.cron || "0 9 * * *"),
5492
+ timezone: String(entry.timezone || "Asia/Shanghai"),
5493
+ targetRunNodeId: String(entry.targetRunNodeId || entry.runNodeId || ""),
5494
+ overlapPolicy: "skip",
5495
+ };
5496
+ const computeNext = (config = fallbackConfig) => {
5497
+ try {
5498
+ return workspaceScheduleNextRunAt(config, new Date());
5499
+ } catch {
5500
+ return null;
5501
+ }
5502
+ };
5503
+ let nextRunAt = computeNext(fallbackConfig);
5192
5504
  const scoped = resolveWorkspaceScopeRoot(root, {
5193
5505
  flowId: entry.flowId || "",
5194
5506
  flowSource: entry.flowSource || "user",
@@ -5204,8 +5516,10 @@ async function runWorkspaceScheduledEntry(root, entry) {
5204
5516
  }
5205
5517
  const graphPath = workspaceGraphPath(scoped.root);
5206
5518
  const graph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root).graph, userCtx);
5207
- const instance = graph.instances?.[entry.runNodeId];
5519
+ const scheduleNodeId = String(entry.scheduleNodeId || entry.key?.split(":").pop() || "");
5520
+ const instance = graph.instances?.[scheduleNodeId];
5208
5521
  const config = normalizeWorkspaceScheduledRunConfig(instance?.body || "");
5522
+ nextRunAt = computeNext(config);
5209
5523
  if (!instance || String(instance.definitionId || "") !== "workspace_scheduled_run" || !config.enabled) {
5210
5524
  updateWorkspaceScheduleEntry(entry.key, {
5211
5525
  enabled: false,
@@ -5214,19 +5528,56 @@ async function runWorkspaceScheduledEntry(root, entry) {
5214
5528
  });
5215
5529
  return;
5216
5530
  }
5531
+ const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
5532
+ if (!targetRunNodeId) {
5533
+ updateWorkspaceScheduleEntry(entry.key, {
5534
+ nextRunAt,
5535
+ lastStatus: "invalid",
5536
+ lastError: "No target Run node selected or connected",
5537
+ lastErrorAt: Date.now(),
5538
+ });
5539
+ return;
5540
+ }
5541
+ let plan;
5542
+ try {
5543
+ plan = workspaceRunPlan(graph, targetRunNodeId, scoped.root);
5544
+ } catch (e) {
5545
+ updateWorkspaceScheduleEntry(entry.key, {
5546
+ nextRunAt,
5547
+ lastStatus: "failed",
5548
+ lastError: (e && e.message) || String(e),
5549
+ lastErrorAt: Date.now(),
5550
+ });
5551
+ return;
5552
+ }
5553
+ const plannedNodeIds = workspaceRunPlanNodeIds(targetRunNodeId, plan);
5554
+ const conflict = workspaceFindActiveRunConflict(scopeKey, plannedNodeIds);
5555
+ if (conflict) {
5556
+ updateWorkspaceScheduleEntry(entry.key, {
5557
+ nextRunAt,
5558
+ lastSkippedAt: Date.now(),
5559
+ lastStatus: "skipped: busy",
5560
+ lastError: "",
5561
+ });
5562
+ return;
5563
+ }
5217
5564
 
5218
5565
  const controller = new AbortController();
5219
5566
  const authUsers = readAuthUsers();
5220
5567
  const authUser = authUsers[userCtx.userId] || {};
5568
+ const runId = runLedgerId("workspace");
5569
+ const runKey = workspaceRunEntryKey(scopeKey, runId);
5221
5570
  const runEntry = {
5571
+ scopeKey,
5222
5572
  controller,
5223
5573
  child: null,
5224
- runId: runLedgerId("workspace"),
5574
+ runId,
5225
5575
  userId: userCtx.userId,
5226
5576
  username: String(authUser.username || entry.username || userCtx.userId),
5227
- runNodeId: String(entry.runNodeId || ""),
5577
+ runNodeId: targetRunNodeId,
5228
5578
  flowId: String(entry.flowId || ""),
5229
5579
  flowSource: String(entry.flowSource || "user"),
5580
+ plannedNodeIds,
5230
5581
  startedAt: Date.now(),
5231
5582
  scheduled: true,
5232
5583
  stopChild() {
@@ -5241,6 +5592,10 @@ async function runWorkspaceScheduledEntry(root, entry) {
5241
5592
  lastStatus: "running",
5242
5593
  lastTriggeredAt: runEntry.startedAt,
5243
5594
  lastRunId: runEntry.runId,
5595
+ runNodeId: targetRunNodeId,
5596
+ targetRunNodeId,
5597
+ cron: config.cron,
5598
+ timezone: config.timezone,
5244
5599
  lastError: "",
5245
5600
  });
5246
5601
  const setActiveChild = (child) => {
@@ -5251,7 +5606,7 @@ async function runWorkspaceScheduledEntry(root, entry) {
5251
5606
  const result = await runWorkspaceGraph(root, scoped.root, {
5252
5607
  flowId: entry.flowId,
5253
5608
  flowSource: entry.flowSource || "user",
5254
- runNodeId: entry.runNodeId,
5609
+ runNodeId: targetRunNodeId,
5255
5610
  graph,
5256
5611
  }, userCtx, {
5257
5612
  signal: controller.signal,
@@ -5264,7 +5619,7 @@ async function runWorkspaceScheduledEntry(root, entry) {
5264
5619
  const endedAt = Date.now();
5265
5620
  appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "success");
5266
5621
  updateWorkspaceScheduleEntry(entry.key, {
5267
- nextRunAt: Date.now() + intervalMs,
5622
+ nextRunAt: computeNext(config),
5268
5623
  lastFinishedAt: endedAt,
5269
5624
  lastStatus: "success",
5270
5625
  lastError: "",
@@ -5273,13 +5628,13 @@ async function runWorkspaceScheduledEntry(root, entry) {
5273
5628
  const endedAt = Date.now();
5274
5629
  appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "failed");
5275
5630
  updateWorkspaceScheduleEntry(entry.key, {
5276
- nextRunAt: Date.now() + intervalMs,
5631
+ nextRunAt: computeNext(config),
5277
5632
  lastFinishedAt: endedAt,
5278
5633
  lastStatus: "failed",
5279
5634
  lastError: (e && e.message) || String(e),
5280
5635
  lastErrorAt: endedAt,
5281
5636
  });
5282
- log.info(`[workspace-scheduler] failed ${entry.flowId}/${entry.runNodeId}: ${(e && e.message) || String(e)}`);
5637
+ log.info(`[workspace-scheduler] failed ${entry.flowId}/${targetRunNodeId}: ${(e && e.message) || String(e)}`);
5283
5638
  } finally {
5284
5639
  if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
5285
5640
  }
@@ -5292,8 +5647,25 @@ function pollWorkspaceSchedules(root) {
5292
5647
  if (!entry || entry.enabled !== true) continue;
5293
5648
  const nextRunAt = Number(entry.nextRunAt || 0);
5294
5649
  if (!Number.isFinite(nextRunAt) || nextRunAt <= 0) {
5650
+ const config = {
5651
+ enabled: true,
5652
+ cron: String(entry.cron || "0 9 * * *"),
5653
+ timezone: String(entry.timezone || "Asia/Shanghai"),
5654
+ };
5655
+ let computedNext = null;
5656
+ try {
5657
+ computedNext = workspaceScheduleNextRunAt(config, new Date(now));
5658
+ } catch (e) {
5659
+ updateWorkspaceScheduleEntry(entry.key, {
5660
+ nextRunAt: null,
5661
+ lastStatus: "invalid",
5662
+ lastError: (e && e.message) || String(e),
5663
+ lastErrorAt: now,
5664
+ });
5665
+ continue;
5666
+ }
5295
5667
  updateWorkspaceScheduleEntry(entry.key, {
5296
- nextRunAt: now + Math.max(1, Number(entry.intervalMinutes || 60)) * 60 * 1000,
5668
+ nextRunAt: computedNext,
5297
5669
  lastStatus: entry.lastStatus || "armed",
5298
5670
  });
5299
5671
  continue;
@@ -6079,6 +6451,53 @@ export function startUiServer({
6079
6451
  return;
6080
6452
  }
6081
6453
 
6454
+ if (req.method === "POST" && url.pathname === "/api/workspace/run/plan") {
6455
+ let payload;
6456
+ try {
6457
+ payload = JSON.parse(await readBody(req));
6458
+ } catch {
6459
+ json(res, 400, { error: "Invalid JSON body" });
6460
+ return;
6461
+ }
6462
+ try {
6463
+ const scoped = resolveWorkspaceScopeRoot(root, {
6464
+ flowId: payload.flowId || "",
6465
+ flowSource: payload.flowSource || "user",
6466
+ archived: payload.archived === true || payload.flowArchived === true,
6467
+ }, userCtx);
6468
+ if (scoped.error) {
6469
+ json(res, 400, { error: scoped.error });
6470
+ return;
6471
+ }
6472
+ const flowId = String(payload.flowId || "").trim();
6473
+ if (!flowId) {
6474
+ json(res, 400, { error: "Missing flowId" });
6475
+ return;
6476
+ }
6477
+ const graph = hydrateWorkspaceGraphForRuntime(root, scoped, payload.graph || {}, userCtx);
6478
+ const runNodeId = String(payload.runNodeId || "").trim();
6479
+ const plan = workspaceRunPlan(graph, runNodeId, scoped.root);
6480
+ const plannedNodeIds = workspaceRunPlanNodeIds(runNodeId, plan);
6481
+ const scopeKey = workspaceRunKey(userCtx, scoped.flowSource || payload.flowSource || "user", flowId);
6482
+ const conflict = workspaceFindActiveRunConflict(scopeKey, plannedNodeIds);
6483
+ json(res, 200, {
6484
+ ok: true,
6485
+ runNodeId,
6486
+ order: plan.order,
6487
+ pauseNodeIds: plan.pauseNodeIds,
6488
+ plannedNodeIds,
6489
+ conflict: conflict ? {
6490
+ runId: conflict.entry?.runId || "",
6491
+ runNodeId: conflict.entry?.runNodeId || "",
6492
+ conflictNodeIds: conflict.conflictNodeIds,
6493
+ } : null,
6494
+ });
6495
+ } catch (e) {
6496
+ json(res, 500, { error: (e && e.message) || String(e) });
6497
+ }
6498
+ return;
6499
+ }
6500
+
6082
6501
  if (req.method === "POST" && url.pathname === "/api/workspace/run") {
6083
6502
  let payload;
6084
6503
  try {
@@ -6107,21 +6526,35 @@ export function startUiServer({
6107
6526
  json(res, 400, { error: "Missing flowId" });
6108
6527
  return;
6109
6528
  }
6110
- const runKey = workspaceRunKey(userCtx, scoped.flowSource || payload.flowSource || "user", flowId);
6111
- if (activeWorkspaceRuns.has(runKey)) {
6112
- json(res, 409, { error: "该 Workspace 正在运行" });
6529
+ const runtimeGraph = hydrateWorkspaceGraphForRuntime(root, scoped, payload.graph || {}, userCtx);
6530
+ const runNodeId = String(payload.runNodeId || "").trim();
6531
+ const plan = workspaceRunPlan(runtimeGraph, runNodeId, scoped.root);
6532
+ const plannedNodeIds = workspaceRunPlanNodeIds(runNodeId, plan);
6533
+ const scopeKey = workspaceRunKey(userCtx, scoped.flowSource || payload.flowSource || "user", flowId);
6534
+ const conflict = workspaceFindActiveRunConflict(scopeKey, plannedNodeIds);
6535
+ if (conflict) {
6536
+ json(res, 409, {
6537
+ error: "该 Run 与正在执行的 Run 共享节点",
6538
+ runNodeId: conflict.entry?.runNodeId || "",
6539
+ runId: conflict.entry?.runId || "",
6540
+ conflictNodeIds: conflict.conflictNodeIds,
6541
+ });
6113
6542
  return;
6114
6543
  }
6115
6544
  const controller = new AbortController();
6545
+ const runId = String(payload.runSessionId || payload.runId || "").trim() || runLedgerId("workspace");
6546
+ const runKey = workspaceRunEntryKey(scopeKey, runId);
6116
6547
  const runEntry = {
6548
+ scopeKey,
6117
6549
  controller,
6118
6550
  child: null,
6119
- runId: runLedgerId("workspace"),
6551
+ runId,
6120
6552
  userId: String(userCtx.userId || ""),
6121
6553
  username: String(authUser?.username || userCtx.userId || ""),
6122
- runNodeId: String(payload.runNodeId || "").trim(),
6554
+ runNodeId,
6123
6555
  flowId,
6124
6556
  flowSource: scoped.flowSource || payload.flowSource || "user",
6557
+ plannedNodeIds,
6125
6558
  startedAt: Date.now(),
6126
6559
  stopChild() {
6127
6560
  if (this.child && !this.child.killed) {
@@ -6235,14 +6668,22 @@ export function startUiServer({
6235
6668
  return;
6236
6669
  }
6237
6670
  const flowSource = url.searchParams.get("flowSource") || "user";
6238
- const runKey = workspaceRunKey(userCtx, flowSource, flowId);
6239
- const entry = activeWorkspaceRuns.get(runKey);
6671
+ const scopeKey = workspaceRunKey(userCtx, flowSource, flowId);
6672
+ const entries = workspaceActiveRunsForScope(scopeKey).map(([, entry]) => entry);
6673
+ const entry = entries[0] || null;
6240
6674
  json(res, 200, {
6241
- running: Boolean(entry),
6675
+ running: entries.length > 0,
6242
6676
  flowId,
6243
6677
  flowSource,
6244
6678
  runNodeId: entry?.runNodeId || "",
6245
6679
  startedAt: entry?.startedAt || null,
6680
+ runs: entries.map((item) => ({
6681
+ runId: item?.runId || "",
6682
+ runNodeId: item?.runNodeId || "",
6683
+ startedAt: item?.startedAt || null,
6684
+ plannedNodeIds: Array.isArray(item?.plannedNodeIds) ? item.plannedNodeIds : [],
6685
+ scheduled: item?.scheduled === true,
6686
+ })),
6246
6687
  });
6247
6688
  return;
6248
6689
  }
@@ -6260,8 +6701,14 @@ export function startUiServer({
6260
6701
  json(res, 400, { error: "Missing flowId" });
6261
6702
  return;
6262
6703
  }
6263
- const runKey = workspaceRunKey(userCtx, payload.flowSource || "user", flowId);
6264
- const entry = activeWorkspaceRuns.get(runKey);
6704
+ const scopeKey = workspaceRunKey(userCtx, payload.flowSource || "user", flowId);
6705
+ const runId = String(payload.runId || payload.runSessionId || "").trim();
6706
+ const runNodeId = String(payload.runNodeId || "").trim();
6707
+ const entries = workspaceActiveRunsForScope(scopeKey);
6708
+ const match = entries.find(([, item]) => runId && String(item?.runId || "") === runId)
6709
+ || entries.find(([, item]) => runNodeId && String(item?.runNodeId || "") === runNodeId)
6710
+ || (!runId && !runNodeId && entries.length === 1 ? entries[0] : null);
6711
+ const entry = match?.[1] || null;
6265
6712
  if (!entry) {
6266
6713
  json(res, 404, { error: "该 Workspace 未在运行" });
6267
6714
  return;