@fieldwangai/agentflow 0.1.71 → 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.
@@ -45,6 +45,7 @@ import {
45
45
  PACKAGE_ROOT,
46
46
  ARCHIVED_PIPELINES_DIR_NAME,
47
47
  getAgentflowDataRoot,
48
+ getAgentflowSkillsRoot,
48
49
  getAgentflowUserConfigAbs,
49
50
  getAgentflowUserDataRoot,
50
51
  getUserPipelinesRoot,
@@ -62,6 +63,7 @@ import {
62
63
  buildSkillInjectionBlock,
63
64
  buildSkillCompactInjectionBlock,
64
65
  } from "./composer-skill-router.mjs";
66
+ import { clearSkillRegistryCache } from "./skill-registry.mjs";
65
67
  import { COMPOSER_NODE_SPEC_FILENAME } from "./composer-planner.mjs";
66
68
  import { listRecentRunsFromDisk } from "./recent-runs.mjs";
67
69
  import {
@@ -79,7 +81,7 @@ import {
79
81
  readComposerSessionMeta,
80
82
  } from "./composer-log.mjs";
81
83
  import { runNodeScript } from "./pipeline-scripts.mjs";
82
- import { readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
84
+ import { computeNextRunAt, readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
83
85
  import { listScheduleStatuses } from "./scheduler.mjs";
84
86
  import {
85
87
  deleteMarketplaceFlowSnippetPackage,
@@ -89,6 +91,7 @@ import {
89
91
  listMarketplacePackages,
90
92
  publishFlowSnippet,
91
93
  publishNodeFromInstance,
94
+ resolveMarketplaceNodePackage,
92
95
  } from "./marketplace.mjs";
93
96
  import { buildGitContext, inferGitRepoRootFromWorktree, loadGitWorktree, normalizeGitContext, runGit, sanitizeWorktreeName, unloadGitWorktree } from "./git-worktree.mjs";
94
97
  import { createGitLabMergeRequest } from "./gitlab-mr.mjs";
@@ -1134,6 +1137,22 @@ function normalizeSkillhubListPayload(raw) {
1134
1137
  }).filter((x) => x.name);
1135
1138
  }
1136
1139
 
1140
+ function skillhubManagedSkillsDir() {
1141
+ const dir = getAgentflowSkillsRoot();
1142
+ fs.mkdirSync(dir, { recursive: true });
1143
+ return dir;
1144
+ }
1145
+
1146
+ function skillhubListArgs(target, agent) {
1147
+ const normalizedTarget = String(target || "agentflow").trim();
1148
+ const normalizedAgent = String(agent || "codex").trim();
1149
+ const args = ["list", "--json"];
1150
+ if (normalizedTarget === "all") args.push("--all");
1151
+ else if (normalizedTarget === "legacy-global") args.push("--global", "--agent", normalizedAgent);
1152
+ else args.push("--dir", skillhubManagedSkillsDir());
1153
+ return args;
1154
+ }
1155
+
1137
1156
  function skillhubInstallArgs(payload, { uninstall = false } = {}) {
1138
1157
  const slug = String(payload?.slug || payload?.name || "").trim();
1139
1158
  if (!slug && !payload?.collection) return null;
@@ -1144,10 +1163,12 @@ function skillhubInstallArgs(payload, { uninstall = false } = {}) {
1144
1163
  args.push(slug);
1145
1164
  }
1146
1165
  if (payload?.skillId) args.push("--skill-id", String(payload.skillId).trim());
1147
- const target = String(payload?.target || "project").trim();
1166
+ const target = String(payload?.target || "agentflow").trim();
1148
1167
  const agent = String(payload?.agent || "codex").trim();
1149
- if (target === "global") {
1168
+ if (target === "global" || target === "legacy-global") {
1150
1169
  args.push("--global", "--agent", agent);
1170
+ } else if (target === "agentflow") {
1171
+ args.push("--dir", skillhubManagedSkillsDir());
1151
1172
  } else if (payload?.dir) {
1152
1173
  args.push("--dir", String(payload.dir).trim());
1153
1174
  }
@@ -2153,9 +2174,76 @@ function hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped = {}, gra
2153
2174
  return changed ? { ...next, instances } : next;
2154
2175
  }
2155
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
+
2156
2243
  function hydrateWorkspaceGraphForRuntime(workspaceRoot, scoped = {}, graph = {}, userCtx = {}) {
2157
2244
  const withRefs = hydrateWorkspaceNodeRefsFromFiles(scoped.root || scoped.scopedRoot || workspaceRoot, graph);
2158
- return hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped, withRefs, userCtx);
2245
+ const withMarketplaceRuntime = hydrateWorkspaceMarketplaceToolNodejsRuntime(workspaceRoot, scoped, withRefs, userCtx);
2246
+ return hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped, withMarketplaceRuntime, userCtx);
2159
2247
  }
2160
2248
 
2161
2249
  function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
@@ -3431,6 +3519,7 @@ function workspaceMaterializeImplementationReference(instance, scopedRoot, runPa
3431
3519
  }
3432
3520
 
3433
3521
  function workspaceImplementationBlock(instance, scopedRoot, runPackage = {}) {
3522
+ if (!WORKSPACE_IMPLEMENTATION_REFERENCE_ENABLED) return "";
3434
3523
  const ref = workspaceMaterializeImplementationReference(instance, scopedRoot, runPackage);
3435
3524
  const inline = workspaceImplementationInlineText(instance);
3436
3525
  if (!ref && !inline) return "";
@@ -3458,6 +3547,46 @@ function workspaceDefaultImplementationRef(nodeId) {
3458
3547
  return `nodes/${workspaceSafeNodeFileName(nodeId)}/implementation.md`;
3459
3548
  }
3460
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
+
3461
3590
  function workspaceImplementationModeForInstance(instance) {
3462
3591
  const explicit = String(instance?.implementationMode || "").trim();
3463
3592
  if (explicit) return explicit;
@@ -3649,11 +3778,96 @@ async function workspaceGenerateImplementationMarkdown({
3649
3778
  return markdown.replace(/^```(?:markdown|md)?\s*/i, "").replace(/```\s*$/i, "").trim();
3650
3779
  }
3651
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
+
3652
3863
  async function workspacePersistNodeImplementation(scopedRoot, graph, nodeId, opts = {}) {
3653
3864
  const current = graph?.instances?.[nodeId];
3654
3865
  if (!current || String(current.definitionId || "") === "workspace_run" || String(current.definitionId || "") === "workspace_scheduled_run") {
3655
3866
  return { changed: false, wrote: false, instance: current };
3656
3867
  }
3868
+ if (!WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED) {
3869
+ return workspacePersistNodeHistory(scopedRoot, graph, nodeId, opts);
3870
+ }
3657
3871
  const existingRef = String(current.implementationRef || "").trim();
3658
3872
  const implementationRef = existingRef || workspaceDefaultImplementationRef(nodeId);
3659
3873
  const abs = workspaceResolveFlowFile(scopedRoot, implementationRef, "implementationRef");
@@ -3695,7 +3909,9 @@ async function workspaceTryPersistNodeImplementation(scopedRoot, graph, nodeId,
3695
3909
  opts.emit?.({
3696
3910
  type: "natural",
3697
3911
  kind: "warning",
3698
- text: `实现方案未更新:${e?.message || String(e)}`,
3912
+ text: WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED
3913
+ ? `实现方案未更新:${e?.message || String(e)}`
3914
+ : `历史记录未更新:${e?.message || String(e)}`,
3699
3915
  });
3700
3916
  return { changed: false, wrote: false, instance: graph?.instances?.[nodeId] };
3701
3917
  }
@@ -4727,8 +4943,8 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4727
4943
  } catch {
4728
4944
  // Best-effort debug artifact only.
4729
4945
  }
4730
- const implementationBlock = workspaceImplementationBlock(instance, scopedRoot, runPackage);
4731
- 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);
4732
4948
  try {
4733
4949
  fs.writeFileSync(path.join(runPackage.nodeRunDir, "prompt.md"), prompt.trimEnd() + "\n", "utf-8");
4734
4950
  } catch {
@@ -4743,6 +4959,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4743
4959
  });
4744
4960
  emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
4745
4961
  let content = "";
4962
+ const runHistoryEvents = [];
4746
4963
  const maxAttempts = 3;
4747
4964
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4748
4965
  let attemptContent = "";
@@ -4772,6 +4989,12 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4772
4989
  ? { ...ev, text: workspaceCanonicalAgentOutput(ev.text), nodeId }
4773
4990
  : { ...ev, nodeId };
4774
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
+ }
4775
4998
  if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
4776
4999
  attemptLastAssistantContent = ev.text;
4777
5000
  attemptContent += (attemptContent ? "\n" : "") + ev.text;
@@ -4821,6 +5044,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4821
5044
  runPackage,
4822
5045
  modelKey,
4823
5046
  userCtx,
5047
+ historyEvents: runHistoryEvents,
4824
5048
  emit: (event) => emit({ ...event, nodeId }),
4825
5049
  onActiveChild: opts.onActiveChild,
4826
5050
  });
@@ -5020,15 +5244,61 @@ function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userI
5020
5244
 
5021
5245
  /** 正在执行的 flow run(flowId → { child, runUuid });同一 flow 只允许一个 run */
5022
5246
  const activeFlowRuns = new Map();
5023
- /** 正在执行的 Workspace 临时 run(flowId → { controller, child, runNodeId, startedAt });同一 flow 只允许一个 run */
5247
+ /** 正在执行的 Workspace 临时 run(runId/sessionId → { controller, child, runNodeId, startedAt, plannedNodeIds } */
5024
5248
  const activeWorkspaceRuns = new Map();
5025
5249
  const WORKSPACE_SCHEDULES_FILENAME = "workspace-schedules.json";
5026
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
+ }
5027
5264
 
5028
5265
  function workspaceRunKey(userCtx, flowSource, flowId) {
5029
5266
  return `${userCtx?.userId || ""}:${flowSource || "user"}:${flowId}`;
5030
5267
  }
5031
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
+
5032
5302
  function normalizeWorkspaceScheduledRunConfig(raw) {
5033
5303
  let parsed = {};
5034
5304
  const text = String(raw || "").trim();
@@ -5040,14 +5310,29 @@ function normalizeWorkspaceScheduledRunConfig(raw) {
5040
5310
  }
5041
5311
  }
5042
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";
5043
5322
  return {
5044
5323
  enabled: parsed.enabled === true,
5045
- intervalMinutes: Number.isFinite(intervalMinutes) && intervalMinutes > 0
5046
- ? Math.min(Math.max(Math.round(intervalMinutes), 1), 1440)
5047
- : 60,
5324
+ cron,
5325
+ timezone,
5326
+ targetRunNodeId,
5327
+ overlapPolicy,
5048
5328
  };
5049
5329
  }
5050
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
+
5051
5336
  function workspaceSchedulesPath() {
5052
5337
  return path.join(getAgentflowDataRoot(), WORKSPACE_SCHEDULES_FILENAME);
5053
5338
  }
@@ -5078,12 +5363,12 @@ function writeWorkspaceScheduleRegistry(registry) {
5078
5363
  }, null, 2) + "\n", "utf-8");
5079
5364
  }
5080
5365
 
5081
- function workspaceScheduleKey(userId, flowSource, flowId, runNodeId) {
5366
+ function workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId) {
5082
5367
  return [
5083
5368
  String(userId || ""),
5084
5369
  String(flowSource || "user"),
5085
5370
  String(flowId || ""),
5086
- String(runNodeId || ""),
5371
+ String(scheduleNodeId || ""),
5087
5372
  ].join(":");
5088
5373
  }
5089
5374
 
@@ -5096,7 +5381,23 @@ function listWorkspaceScheduleStatusesForFlow(userCtx = {}, flowSource = "user",
5096
5381
  String(entry?.flowSource || "user") === String(flowSource || "user") &&
5097
5382
  String(entry?.flowId || "") === String(flowId || "")
5098
5383
  ))
5099
- .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] : "";
5100
5401
  }
5101
5402
 
5102
5403
  function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx = {}) {
@@ -5113,14 +5414,35 @@ function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx =
5113
5414
  if (key.startsWith(prefix)) delete schedules[key];
5114
5415
  }
5115
5416
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
5116
- for (const [runNodeId, instance] of Object.entries(instances)) {
5417
+ for (const [scheduleNodeId, instance] of Object.entries(instances)) {
5117
5418
  if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
5118
5419
  const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
5119
5420
  if (!config.enabled) continue;
5120
- const key = workspaceScheduleKey(userId, flowSource, flowId, runNodeId);
5121
- const intervalMs = config.intervalMinutes * 60 * 1000;
5421
+ const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
5422
+ const key = workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId);
5122
5423
  const previous = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
5123
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
+ }
5124
5446
  schedules[key] = {
5125
5447
  ...previous,
5126
5448
  key,
@@ -5129,11 +5451,16 @@ function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx =
5129
5451
  username: String(authUser?.username || previous.username || userId),
5130
5452
  flowId,
5131
5453
  flowSource,
5132
- runNodeId,
5454
+ scheduleNodeId,
5455
+ runNodeId: targetRunNodeId,
5456
+ targetRunNodeId,
5133
5457
  label: String(instance.label || "Scheduled Run"),
5134
- intervalMinutes: config.intervalMinutes,
5135
- nextRunAt: Number.isFinite(previousNext) && previousNext > now ? previousNext : now + intervalMs,
5136
- lastStatus: previous.lastStatus || "armed",
5458
+ cron: config.cron,
5459
+ timezone: config.timezone,
5460
+ overlapPolicy: config.overlapPolicy,
5461
+ nextRunAt,
5462
+ lastStatus,
5463
+ lastError,
5137
5464
  updatedAt: nowIso,
5138
5465
  };
5139
5466
  }
@@ -5158,17 +5485,22 @@ function updateWorkspaceScheduleEntry(key, patch) {
5158
5485
 
5159
5486
  async function runWorkspaceScheduledEntry(root, entry) {
5160
5487
  const userCtx = { userId: String(entry.userId || "") };
5161
- const runKey = workspaceRunKey(userCtx, entry.flowSource || "user", entry.flowId || "");
5162
- const intervalMs = Math.max(1, Number(entry.intervalMinutes || 60)) * 60 * 1000;
5163
- const nextRunAt = Date.now() + intervalMs;
5164
- if (activeWorkspaceRuns.has(runKey)) {
5165
- updateWorkspaceScheduleEntry(entry.key, {
5166
- nextRunAt,
5167
- lastSkippedAt: Date.now(),
5168
- lastStatus: "skipped: busy",
5169
- });
5170
- return;
5171
- }
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);
5172
5504
  const scoped = resolveWorkspaceScopeRoot(root, {
5173
5505
  flowId: entry.flowId || "",
5174
5506
  flowSource: entry.flowSource || "user",
@@ -5184,8 +5516,10 @@ async function runWorkspaceScheduledEntry(root, entry) {
5184
5516
  }
5185
5517
  const graphPath = workspaceGraphPath(scoped.root);
5186
5518
  const graph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root).graph, userCtx);
5187
- const instance = graph.instances?.[entry.runNodeId];
5519
+ const scheduleNodeId = String(entry.scheduleNodeId || entry.key?.split(":").pop() || "");
5520
+ const instance = graph.instances?.[scheduleNodeId];
5188
5521
  const config = normalizeWorkspaceScheduledRunConfig(instance?.body || "");
5522
+ nextRunAt = computeNext(config);
5189
5523
  if (!instance || String(instance.definitionId || "") !== "workspace_scheduled_run" || !config.enabled) {
5190
5524
  updateWorkspaceScheduleEntry(entry.key, {
5191
5525
  enabled: false,
@@ -5194,19 +5528,56 @@ async function runWorkspaceScheduledEntry(root, entry) {
5194
5528
  });
5195
5529
  return;
5196
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
+ }
5197
5564
 
5198
5565
  const controller = new AbortController();
5199
5566
  const authUsers = readAuthUsers();
5200
5567
  const authUser = authUsers[userCtx.userId] || {};
5568
+ const runId = runLedgerId("workspace");
5569
+ const runKey = workspaceRunEntryKey(scopeKey, runId);
5201
5570
  const runEntry = {
5571
+ scopeKey,
5202
5572
  controller,
5203
5573
  child: null,
5204
- runId: runLedgerId("workspace"),
5574
+ runId,
5205
5575
  userId: userCtx.userId,
5206
5576
  username: String(authUser.username || entry.username || userCtx.userId),
5207
- runNodeId: String(entry.runNodeId || ""),
5577
+ runNodeId: targetRunNodeId,
5208
5578
  flowId: String(entry.flowId || ""),
5209
5579
  flowSource: String(entry.flowSource || "user"),
5580
+ plannedNodeIds,
5210
5581
  startedAt: Date.now(),
5211
5582
  scheduled: true,
5212
5583
  stopChild() {
@@ -5221,6 +5592,10 @@ async function runWorkspaceScheduledEntry(root, entry) {
5221
5592
  lastStatus: "running",
5222
5593
  lastTriggeredAt: runEntry.startedAt,
5223
5594
  lastRunId: runEntry.runId,
5595
+ runNodeId: targetRunNodeId,
5596
+ targetRunNodeId,
5597
+ cron: config.cron,
5598
+ timezone: config.timezone,
5224
5599
  lastError: "",
5225
5600
  });
5226
5601
  const setActiveChild = (child) => {
@@ -5231,7 +5606,7 @@ async function runWorkspaceScheduledEntry(root, entry) {
5231
5606
  const result = await runWorkspaceGraph(root, scoped.root, {
5232
5607
  flowId: entry.flowId,
5233
5608
  flowSource: entry.flowSource || "user",
5234
- runNodeId: entry.runNodeId,
5609
+ runNodeId: targetRunNodeId,
5235
5610
  graph,
5236
5611
  }, userCtx, {
5237
5612
  signal: controller.signal,
@@ -5244,7 +5619,7 @@ async function runWorkspaceScheduledEntry(root, entry) {
5244
5619
  const endedAt = Date.now();
5245
5620
  appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "success");
5246
5621
  updateWorkspaceScheduleEntry(entry.key, {
5247
- nextRunAt: Date.now() + intervalMs,
5622
+ nextRunAt: computeNext(config),
5248
5623
  lastFinishedAt: endedAt,
5249
5624
  lastStatus: "success",
5250
5625
  lastError: "",
@@ -5253,13 +5628,13 @@ async function runWorkspaceScheduledEntry(root, entry) {
5253
5628
  const endedAt = Date.now();
5254
5629
  appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "failed");
5255
5630
  updateWorkspaceScheduleEntry(entry.key, {
5256
- nextRunAt: Date.now() + intervalMs,
5631
+ nextRunAt: computeNext(config),
5257
5632
  lastFinishedAt: endedAt,
5258
5633
  lastStatus: "failed",
5259
5634
  lastError: (e && e.message) || String(e),
5260
5635
  lastErrorAt: endedAt,
5261
5636
  });
5262
- 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)}`);
5263
5638
  } finally {
5264
5639
  if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
5265
5640
  }
@@ -5272,8 +5647,25 @@ function pollWorkspaceSchedules(root) {
5272
5647
  if (!entry || entry.enabled !== true) continue;
5273
5648
  const nextRunAt = Number(entry.nextRunAt || 0);
5274
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
+ }
5275
5667
  updateWorkspaceScheduleEntry(entry.key, {
5276
- nextRunAt: now + Math.max(1, Number(entry.intervalMinutes || 60)) * 60 * 1000,
5668
+ nextRunAt: computedNext,
5277
5669
  lastStatus: entry.lastStatus || "armed",
5278
5670
  });
5279
5671
  continue;
@@ -5620,6 +6012,7 @@ export function startUiServer({
5620
6012
  }
5621
6013
  try {
5622
6014
  const config = writeAdminStorageConfig(payload?.config || payload || {});
6015
+ clearSkillRegistryCache();
5623
6016
  json(res, 200, { ok: true, config });
5624
6017
  } catch (e) {
5625
6018
  json(res, 400, { error: (e && e.message) || String(e) });
@@ -6058,6 +6451,53 @@ export function startUiServer({
6058
6451
  return;
6059
6452
  }
6060
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
+
6061
6501
  if (req.method === "POST" && url.pathname === "/api/workspace/run") {
6062
6502
  let payload;
6063
6503
  try {
@@ -6086,21 +6526,35 @@ export function startUiServer({
6086
6526
  json(res, 400, { error: "Missing flowId" });
6087
6527
  return;
6088
6528
  }
6089
- const runKey = workspaceRunKey(userCtx, scoped.flowSource || payload.flowSource || "user", flowId);
6090
- if (activeWorkspaceRuns.has(runKey)) {
6091
- 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
+ });
6092
6542
  return;
6093
6543
  }
6094
6544
  const controller = new AbortController();
6545
+ const runId = String(payload.runSessionId || payload.runId || "").trim() || runLedgerId("workspace");
6546
+ const runKey = workspaceRunEntryKey(scopeKey, runId);
6095
6547
  const runEntry = {
6548
+ scopeKey,
6096
6549
  controller,
6097
6550
  child: null,
6098
- runId: runLedgerId("workspace"),
6551
+ runId,
6099
6552
  userId: String(userCtx.userId || ""),
6100
6553
  username: String(authUser?.username || userCtx.userId || ""),
6101
- runNodeId: String(payload.runNodeId || "").trim(),
6554
+ runNodeId,
6102
6555
  flowId,
6103
6556
  flowSource: scoped.flowSource || payload.flowSource || "user",
6557
+ plannedNodeIds,
6104
6558
  startedAt: Date.now(),
6105
6559
  stopChild() {
6106
6560
  if (this.child && !this.child.killed) {
@@ -6214,14 +6668,22 @@ export function startUiServer({
6214
6668
  return;
6215
6669
  }
6216
6670
  const flowSource = url.searchParams.get("flowSource") || "user";
6217
- const runKey = workspaceRunKey(userCtx, flowSource, flowId);
6218
- 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;
6219
6674
  json(res, 200, {
6220
- running: Boolean(entry),
6675
+ running: entries.length > 0,
6221
6676
  flowId,
6222
6677
  flowSource,
6223
6678
  runNodeId: entry?.runNodeId || "",
6224
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
+ })),
6225
6687
  });
6226
6688
  return;
6227
6689
  }
@@ -6239,8 +6701,14 @@ export function startUiServer({
6239
6701
  json(res, 400, { error: "Missing flowId" });
6240
6702
  return;
6241
6703
  }
6242
- const runKey = workspaceRunKey(userCtx, payload.flowSource || "user", flowId);
6243
- 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;
6244
6712
  if (!entry) {
6245
6713
  json(res, 404, { error: "该 Workspace 未在运行" });
6246
6714
  return;
@@ -7058,17 +7526,19 @@ export function startUiServer({
7058
7526
  }
7059
7527
 
7060
7528
  if (req.method === "GET" && url.pathname === "/api/skillhub/list") {
7061
- const target = url.searchParams.get("target") || "global";
7529
+ const target = url.searchParams.get("target") || "agentflow";
7062
7530
  const agent = url.searchParams.get("agent") || "codex";
7063
- const args = ["list", "--json"];
7064
- if (target === "all") args.push("--all");
7065
- else if (target === "global") args.push("--global", "--agent", agent);
7531
+ const args = skillhubListArgs(target, agent);
7066
7532
  const result = await runSkillhub(args, { cwd: root });
7067
7533
  if (!result.ok) {
7068
7534
  json(res, 500, { error: result.error, stdout: result.stdout });
7069
7535
  return;
7070
7536
  }
7071
- json(res, 200, { skills: normalizeSkillhubListPayload(parseJsonText(result.stdout, [])) });
7537
+ json(res, 200, {
7538
+ skills: normalizeSkillhubListPayload(parseJsonText(result.stdout, [])),
7539
+ target,
7540
+ skillsRoot: target === "agentflow" ? getAgentflowSkillsRoot() : "",
7541
+ });
7072
7542
  return;
7073
7543
  }
7074
7544
 
@@ -7113,6 +7583,10 @@ export function startUiServer({
7113
7583
  }
7114
7584
 
7115
7585
  if (req.method === "POST" && url.pathname === "/api/skillhub/install") {
7586
+ if (!authUser?.isAdmin) {
7587
+ json(res, 403, { error: "Admin required" });
7588
+ return;
7589
+ }
7116
7590
  let payload;
7117
7591
  try {
7118
7592
  payload = JSON.parse(await readBody(req));
@@ -7125,16 +7599,13 @@ export function startUiServer({
7125
7599
  json(res, 400, { error: "Missing skill slug or collection" });
7126
7600
  return;
7127
7601
  }
7128
- if (payload?.collection && !authUser?.isAdmin) {
7129
- json(res, 403, { error: "Admin required" });
7130
- return;
7131
- }
7132
7602
  const beforeSkills = payload?.collection ? listComposerSkills(PACKAGE_ROOT, root) : [];
7133
7603
  const result = await runSkillhub(args, { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
7134
7604
  if (!result.ok) {
7135
7605
  json(res, 500, { error: result.error, stdout: result.stdout });
7136
7606
  return;
7137
7607
  }
7608
+ clearSkillRegistryCache();
7138
7609
  let skillCollections = null;
7139
7610
  if (payload?.collection) {
7140
7611
  const afterSkills = listComposerSkills(PACKAGE_ROOT, root);
@@ -7146,6 +7617,10 @@ export function startUiServer({
7146
7617
  }
7147
7618
 
7148
7619
  if (req.method === "POST" && url.pathname === "/api/skillhub/uninstall") {
7620
+ if (!authUser?.isAdmin) {
7621
+ json(res, 403, { error: "Admin required" });
7622
+ return;
7623
+ }
7149
7624
  let payload;
7150
7625
  try {
7151
7626
  payload = JSON.parse(await readBody(req));
@@ -7158,21 +7633,22 @@ export function startUiServer({
7158
7633
  json(res, 400, { error: "Missing skill slug or collection" });
7159
7634
  return;
7160
7635
  }
7161
- if (payload?.collection && !authUser?.isAdmin) {
7162
- json(res, 403, { error: "Admin required" });
7163
- return;
7164
- }
7165
7636
  const result = await runSkillhub(args, { cwd: root, timeoutMs: 120_000, maxBuffer: 4 * 1024 * 1024 });
7166
7637
  if (!result.ok) {
7167
7638
  json(res, 500, { error: result.error, stdout: result.stdout });
7168
7639
  return;
7169
7640
  }
7641
+ clearSkillRegistryCache();
7170
7642
  const skillCollections = payload?.collection ? removeSkillhubCollectionGroup(userCtx, payload.collection, root) : null;
7171
7643
  json(res, 200, { ok: true, stdout: result.stdout, skillCollections });
7172
7644
  return;
7173
7645
  }
7174
7646
 
7175
7647
  if (req.method === "POST" && url.pathname === "/api/skillhub/update") {
7648
+ if (!authUser?.isAdmin) {
7649
+ json(res, 403, { error: "Admin required" });
7650
+ return;
7651
+ }
7176
7652
  const result = await runSkillhub(["update"], { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
7177
7653
  if (!result.ok) {
7178
7654
  json(res, 500, { error: result.error, stdout: result.stdout });