@fieldwangai/agentflow 0.1.156 → 0.1.159

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.
@@ -18,6 +18,7 @@ import { buildGitContext, inferGitRepoRootFromWorktree, loadGitWorktree, normali
18
18
  import { createGitLabMergeRequest } from "./gitlab-mr.mjs";
19
19
  import { json } from "./http-util.mjs";
20
20
  import { t } from "./i18n.mjs";
21
+ import { advanceJenkinsBuild, createJenkinsHttpInvoker, jenkinsBuildStatePath, normalizeJenkinsBuildConfig, readJenkinsBuildState, writeJenkinsBuildState } from "./jenkins.mjs";
21
22
  import { log } from "./log.mjs";
22
23
  import { resolveMarketplaceNodePackage } from "./marketplace.mjs";
23
24
  import { PACKAGE_ROOT, getAgentflowDataRoot, getAgentflowUserDataRoot, listAgentflowUserIds } from "./paths.mjs";
@@ -1127,9 +1128,13 @@ function normalizeWorkspaceGraphPayload(payload) {
1127
1128
 
1128
1129
  export function workspaceRunTouchedNodeIds(result) {
1129
1130
  const ids = new Set();
1130
- for (const id of Array.isArray(result?.order) ? result.order : []) {
1131
- const text = String(id || "").trim();
1132
- if (text) ids.add(text);
1131
+ // A deferred run has only executed the prefix ending at the waiting node. Merging the
1132
+ // complete plan here would overwrite unrelated edits made while Jenkins is running.
1133
+ if (!result?.deferred) {
1134
+ for (const id of Array.isArray(result?.order) ? result.order : []) {
1135
+ const text = String(id || "").trim();
1136
+ if (text) ids.add(text);
1137
+ }
1133
1138
  }
1134
1139
  for (const event of Array.isArray(result?.events) ? result.events : []) {
1135
1140
  const nodeId = String(event?.nodeId || "").trim();
@@ -2205,6 +2210,7 @@ function workspaceInstanceText(instance) {
2205
2210
  function workspaceDisplayKind(definitionId) {
2206
2211
  const id = String(definitionId || "");
2207
2212
  if (id === "display_markdown") return "markdown";
2213
+ if (id === "display_code") return "code";
2208
2214
  if (id === "display_mermaid") return "mermaid";
2209
2215
  if (id === "display_ascii") return "ascii";
2210
2216
  if (id === "display_html") return "html";
@@ -2235,6 +2241,7 @@ export function workspaceDisplayTextFilePath(value, kind = "") {
2235
2241
  html: new Set(["html", "htm"]),
2236
2242
  react: new Set(["json", "jsx", "tsx", "js", "txt"]),
2237
2243
  markdown: new Set(["md", "markdown", "txt"]),
2244
+ code: new Set(["txt", "js", "jsx", "mjs", "cjs", "ts", "tsx", "py", "kt", "kts", "java", "go", "rs", "sh", "bash", "zsh", "json", "yaml", "yml", "xml", "html", "htm", "css", "scss", "sql", "md"]),
2238
2245
  mermaid: new Set(["mmd", "mermaid", "txt"]),
2239
2246
  ascii: new Set(["txt", "log"]),
2240
2247
  chart: new Set(["json"]),
@@ -2596,6 +2603,7 @@ function workspaceDownstreamSlotKind(slot) {
2596
2603
  const name = String(slot?.name || "").trim().toLowerCase();
2597
2604
  const type = String(slot?.type || "").trim().toLowerCase();
2598
2605
  if (type === "markdown" || name === "markdown" || name.endsWith("markdown")) return "markdown";
2606
+ if (type === "code" || name === "code" || name.endsWith("code")) return "code";
2599
2607
  if (type === "html" || name === "html" || name.endsWith("html")) return "html";
2600
2608
  if (type === "mermaid" || name === "mermaid" || name.endsWith("mermaid")) return "mermaid";
2601
2609
  if (type === "ascii" || name === "ascii") return "ascii";
@@ -2637,6 +2645,7 @@ function workspaceResultOutputSpec(graph, nodeId) {
2637
2645
  html: "html",
2638
2646
  react: "json",
2639
2647
  markdown: "md",
2648
+ code: "txt",
2640
2649
  mermaid: "mmd",
2641
2650
  ascii: "txt",
2642
2651
  chart: "json",
@@ -2709,6 +2718,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
2709
2718
  html: "内容必须是可直接放入 iframe 渲染的 HTML;不要使用 Markdown 代码围栏。",
2710
2719
  react: "内容必须是 React 工程 JSON,包含 title、entry、files;files 至少包含 src/App.jsx,可包含 CSS 文件。",
2711
2720
  markdown: "内容必须是 Markdown 正文;除非正文确实需要代码块,否则不要额外包裹代码围栏。",
2721
+ code: "内容必须是原始代码文本;不要使用 Markdown 代码围栏,也不要附加解释。",
2712
2722
  mermaid: "内容必须是 Mermaid 图表代码,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏。",
2713
2723
  ascii: "内容必须是纯文本/ASCII 图或表格;不要输出 HTML 或 Markdown 装饰。",
2714
2724
  image: "内容必须是可作为 img src 使用的图片地址、data URL 或 base64 data URL;不要输出 Markdown 图片语法。",
@@ -4102,7 +4112,7 @@ function isWorkspaceOneClickTaskDefinitionId(definitionId) {
4102
4112
 
4103
4113
  function workspaceContextRunDisplayKind(instance) {
4104
4114
  const raw = workspaceSlotValue(workspaceSlotByName(instance, "displayType")).trim().toLowerCase();
4105
- if (["markdown", "html", "react", "table", "chart", "ascii", "mermaid"].includes(raw)) return raw;
4115
+ if (["markdown", "code", "html", "react", "table", "chart", "ascii", "mermaid"].includes(raw)) return raw;
4106
4116
  return "markdown";
4107
4117
  }
4108
4118
 
@@ -4175,7 +4185,7 @@ function buildWorkspaceMcpManifestBlock(results, servers = [], selectedNames = [
4175
4185
  ].join("\n");
4176
4186
  }
4177
4187
 
4178
- function workspaceWriteDisplayContent(instance, content) {
4188
+ export function workspaceWriteDisplayContent(instance, content) {
4179
4189
  const next = { ...(instance || {}) };
4180
4190
  const kind = workspaceDisplayKind(next.definitionId);
4181
4191
  const unwrapped = workspaceUnwrapOutputEnvelopeForDisplay(content);
@@ -4184,12 +4194,12 @@ function workspaceWriteDisplayContent(instance, content) {
4184
4194
  next.displayReloadKey = `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
4185
4195
  next.body = text;
4186
4196
  next.input = (Array.isArray(next.input) ? next.input : []).map((slot) => (
4187
- String(slot?.name || "") === primaryName || String(slot?.type || "") === "text"
4197
+ String(slot?.name || "") === primaryName
4188
4198
  ? { ...slot, default: text, value: text }
4189
4199
  : slot
4190
4200
  ));
4191
4201
  next.output = (Array.isArray(next.output) ? next.output : []).map((slot) => (
4192
- String(slot?.name || "") === primaryName || String(slot?.type || "") === "text"
4202
+ String(slot?.name || "") === primaryName
4193
4203
  ? { ...slot, default: text, value: text }
4194
4204
  : slot
4195
4205
  ));
@@ -4825,6 +4835,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
4825
4835
  const runTmpRoot = workspaceCreateRunTmpRoot(scopedRoot, runNodeId);
4826
4836
  const controlBranches = new Map();
4827
4837
  const skippedNodes = new Set();
4838
+ let deferred = null;
4828
4839
  const incomingControlEdgesByTarget = new Map();
4829
4840
  for (const edge of Array.isArray(graph?.edges) ? graph.edges : []) {
4830
4841
  const target = String(edge?.target || "");
@@ -5072,6 +5083,77 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5072
5083
  continue;
5073
5084
  }
5074
5085
 
5086
+ if (defId === "tool_jenkins_build") {
5087
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5088
+ const config = normalizeJenkinsBuildConfig({
5089
+ job: inputValues.job || workspaceSlotValue(workspaceSlotByName(instance, "job")),
5090
+ parameters: inputValues.parameters || workspaceSlotValue(workspaceSlotByName(instance, "parameters")),
5091
+ credentialRef: inputValues.credentialRef || workspaceSlotValue(workspaceSlotByName(instance, "credentialRef")),
5092
+ pollInterval: inputValues.pollInterval || workspaceSlotValue(workspaceSlotByName(instance, "pollInterval")),
5093
+ timeout: inputValues.timeout || workspaceSlotValue(workspaceSlotByName(instance, "timeout")),
5094
+ });
5095
+ const statePath = jenkinsBuildStatePath(scopedRoot, nodeId);
5096
+ let state = readJenkinsBuildState(statePath);
5097
+ // A completed checkpoint belongs to an earlier execution. An unfinished one is always
5098
+ // resumed, even when the server restarted and the browser created a new run id.
5099
+ if (state?.phase === "complete") state = null;
5100
+ const invoke = createJenkinsHttpInvoker({
5101
+ credentialRef: config.credentialRef,
5102
+ env: runtimeEnv(),
5103
+ fetchImpl: opts.jenkinsFetch || globalThis.fetch,
5104
+ signal,
5105
+ });
5106
+ throwIfAborted();
5107
+ const result = await advanceJenkinsBuild({
5108
+ state,
5109
+ config,
5110
+ invoke,
5111
+ persistState: (checkpoint) => writeJenkinsBuildState(statePath, checkpoint),
5112
+ cancelled: signal?.aborted === true,
5113
+ runId: opts.runId || payload.runId || payload.runSessionId || "",
5114
+ });
5115
+ throwIfAborted();
5116
+ state = result.state;
5117
+ writeJenkinsBuildState(statePath, state);
5118
+ let nextInstance = workspaceSetOutputSlot(graph.instances[nodeId], "status", result.outputs?.status || state.status || "");
5119
+ nextInstance = workspaceSetOutputSlot(nextInstance, "url", result.outputs?.url || state.url || state.buildUrl || "");
5120
+ nextInstance = workspaceSetOutputSlot(nextInstance, "qrUrl", result.outputs?.qrUrl || state.qrUrl || "");
5121
+ graph.instances[nodeId] = nextInstance;
5122
+ emit({
5123
+ type: "status",
5124
+ nodeId,
5125
+ line: result.message || state.message || "Jenkins Build",
5126
+ phase: state.phase || "",
5127
+ jenkinsStatus: state.status || "",
5128
+ buildNumber: state.buildNumber || "",
5129
+ url: state.url || state.buildUrl || "",
5130
+ qrUrl: state.qrUrl || "",
5131
+ wakeAt: state.wakeAt || "",
5132
+ });
5133
+ emit({ type: "graph", nodeId, graph });
5134
+ if (result.kind === "waiting") {
5135
+ deferred = {
5136
+ kind: "jenkins",
5137
+ nodeId,
5138
+ phase: String(state.phase || ""),
5139
+ status: String(state.status || ""),
5140
+ message: String(result.message || state.message || "Jenkins Build"),
5141
+ buildNumber: String(state.buildNumber || ""),
5142
+ url: String(state.url || state.buildUrl || ""),
5143
+ qrUrl: String(state.qrUrl || ""),
5144
+ wakeAt: String(result.wakeAt || state.wakeAt || new Date(Date.now() + config.pollIntervalMs).toISOString()),
5145
+ };
5146
+ emit({ type: "node-waiting", nodeId, definitionId: defId, ...deferred });
5147
+ break;
5148
+ }
5149
+ if (result.kind === "failed") throw new Error(result.message || "Jenkins build node failed");
5150
+ const finalStatus = result.outputs?.status || state.status || "ERROR";
5151
+ const updatedDisplays = publishNodeOutput(nodeId, finalStatus);
5152
+ emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
5153
+ emit({ type: "node-done", nodeId, definitionId: defId, jenkinsStatus: finalStatus });
5154
+ continue;
5155
+ }
5156
+
5075
5157
  if (defId === "tool_git_checkout") {
5076
5158
  const repoUrl = workspaceSlotValue(workspaceSlotByName(instance, "repoUrl")).trim();
5077
5159
  if (!repoUrl) throw new Error("Git Checkout requires repoUrl");
@@ -5569,11 +5651,11 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5569
5651
  workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit);
5570
5652
  workspaceCleanupTmpRoot(runTmpRoot, userCtx, emit);
5571
5653
  }
5572
- if (pauseNodeIds.length > 0) {
5654
+ if (!deferred && pauseNodeIds.length > 0) {
5573
5655
  emit({ type: "paused", nodeIds: pauseNodeIds, message: `Workspace run paused at ${pauseNodeIds.join(", ")}` });
5574
5656
  }
5575
5657
  graph.updatedAt = new Date().toISOString();
5576
- return { graph, events, order, pauseNodeIds };
5658
+ return { graph, events, order, pauseNodeIds, deferred };
5577
5659
  }
5578
5660
 
5579
5661
  export function isWorkspaceRunAbortError(err) {
@@ -5625,10 +5707,34 @@ export const workspaceCollaborationSubscribers = new Map();
5625
5707
 
5626
5708
  export const workspaceCollaborationSequences = new Map();
5627
5709
 
5710
+ function emitWorkspaceCollaborationEvent(userCtx, flowSource, flowId, archived, event = {}) {
5711
+ const key = workspaceCollaborationEventKey(userCtx, flowSource, flowId, archived);
5712
+ const seq = (workspaceCollaborationSequences.get(key) || 0) + 1;
5713
+ workspaceCollaborationSequences.set(key, seq);
5714
+ const payload = JSON.stringify({ seq, at: new Date().toISOString(), ...event });
5715
+ const subscribers = workspaceCollaborationSubscribers.get(key);
5716
+ if (!subscribers?.size) return seq;
5717
+ const chunk = `id: ${seq}\ndata: ${payload}\n\n`;
5718
+ for (const clientRes of subscribers) {
5719
+ try { clientRes.write(chunk); } catch (_) {}
5720
+ }
5721
+ return seq;
5722
+ }
5723
+
5628
5724
  const WORKSPACE_SCHEDULES_FILENAME = "workspace-schedules.json";
5629
5725
 
5726
+ const WORKSPACE_DEFERRED_RUNS_FILENAME = "workspace-deferred-runs.json";
5727
+
5630
5728
  export const WORKSPACE_SCHEDULE_POLL_MS = 30_000;
5631
5729
 
5730
+ export const WORKSPACE_DEFERRED_RUN_POLL_MS = 1_000;
5731
+
5732
+ const WORKSPACE_DEFERRED_LEASE_MS = 60_000;
5733
+
5734
+ const workspaceDeferredLeaseOwner = `${process.pid}-${crypto.randomBytes(8).toString("hex")}`;
5735
+
5736
+ const activeWorkspaceDeferredRuns = new Set();
5737
+
5632
5738
  const WORKSPACE_IMPLEMENTATION_REFERENCE_ENABLED = true;
5633
5739
 
5634
5740
  const WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED = false;
@@ -5694,6 +5800,123 @@ export function workspaceActiveRunsForScope(scopeKey) {
5694
5800
  .filter(([, entry]) => String(entry?.scopeKey || "") === key);
5695
5801
  }
5696
5802
 
5803
+ function workspaceDeferredRunsPath() {
5804
+ return path.join(getAgentflowDataRoot(), WORKSPACE_DEFERRED_RUNS_FILENAME);
5805
+ }
5806
+
5807
+ export function readWorkspaceDeferredRunRegistry() {
5808
+ const filePath = workspaceDeferredRunsPath();
5809
+ if (!fs.existsSync(filePath)) return { version: 1, runs: {} };
5810
+ try {
5811
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
5812
+ return {
5813
+ version: 1,
5814
+ runs: parsed?.runs && typeof parsed.runs === "object" && !Array.isArray(parsed.runs)
5815
+ ? parsed.runs
5816
+ : {},
5817
+ };
5818
+ } catch {
5819
+ return { version: 1, runs: {} };
5820
+ }
5821
+ }
5822
+
5823
+ function writeWorkspaceDeferredRunRegistry(registry) {
5824
+ const filePath = workspaceDeferredRunsPath();
5825
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
5826
+ const tempPath = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
5827
+ fs.writeFileSync(tempPath, JSON.stringify({
5828
+ version: 1,
5829
+ updatedAt: new Date().toISOString(),
5830
+ runs: registry?.runs && typeof registry.runs === "object" ? registry.runs : {},
5831
+ }, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
5832
+ fs.renameSync(tempPath, filePath);
5833
+ }
5834
+
5835
+ function workspaceDeferredRunKey(meta = {}, deferred = {}) {
5836
+ return crypto.createHash("sha256").update([
5837
+ String(meta.scopeKey || ""),
5838
+ String(meta.runId || ""),
5839
+ String(deferred.nodeId || meta.nodeId || ""),
5840
+ ].join("\n")).digest("hex").slice(0, 32);
5841
+ }
5842
+
5843
+ export function upsertWorkspaceDeferredRun(meta = {}, deferred = {}) {
5844
+ const registry = readWorkspaceDeferredRunRegistry();
5845
+ const key = String(meta.deferredKey || "").trim() || workspaceDeferredRunKey(meta, deferred);
5846
+ const previous = registry.runs?.[key] && typeof registry.runs[key] === "object" ? registry.runs[key] : {};
5847
+ const now = Date.now();
5848
+ const next = {
5849
+ ...previous,
5850
+ key,
5851
+ kind: String(deferred.kind || previous.kind || "jenkins"),
5852
+ status: "waiting",
5853
+ scopeKey: String(meta.scopeKey || previous.scopeKey || ""),
5854
+ runId: String(meta.runId || previous.runId || ""),
5855
+ userId: String(meta.userId || previous.userId || ""),
5856
+ username: String(meta.username || previous.username || meta.userId || ""),
5857
+ flowId: String(meta.flowId || previous.flowId || ""),
5858
+ flowSource: String(meta.flowSource || previous.flowSource || "user"),
5859
+ archived: meta.archived === true || previous.archived === true,
5860
+ runNodeId: String(meta.runNodeId || previous.runNodeId || ""),
5861
+ nodeId: String(deferred.nodeId || meta.nodeId || previous.nodeId || ""),
5862
+ label: String(meta.label || previous.label || "Workspace Run"),
5863
+ plannedNodeIds: Array.isArray(meta.plannedNodeIds) ? meta.plannedNodeIds.map(String) : (previous.plannedNodeIds || []),
5864
+ startedAt: Number(meta.startedAt || previous.startedAt || now),
5865
+ scheduled: meta.scheduled === true || previous.scheduled === true,
5866
+ scheduleKey: String(meta.scheduleKey || previous.scheduleKey || ""),
5867
+ scheduleNodeId: String(meta.scheduleNodeId || previous.scheduleNodeId || ""),
5868
+ wakeAt: String(deferred.wakeAt || previous.wakeAt || new Date(now + WORKSPACE_DEFERRED_RUN_POLL_MS).toISOString()),
5869
+ phase: String(deferred.phase || previous.phase || ""),
5870
+ jenkinsStatus: String(deferred.status || previous.jenkinsStatus || ""),
5871
+ message: String(deferred.message || previous.message || ""),
5872
+ buildNumber: String(deferred.buildNumber || previous.buildNumber || ""),
5873
+ url: String(deferred.url || previous.url || ""),
5874
+ qrUrl: String(deferred.qrUrl || previous.qrUrl || ""),
5875
+ createdAt: String(previous.createdAt || new Date(now).toISOString()),
5876
+ updatedAt: new Date(now).toISOString(),
5877
+ leaseOwner: "",
5878
+ leaseUntil: 0,
5879
+ };
5880
+ registry.runs[key] = next;
5881
+ writeWorkspaceDeferredRunRegistry(registry);
5882
+ return next;
5883
+ }
5884
+
5885
+ export function removeWorkspaceDeferredRun(key) {
5886
+ const id = String(key || "").trim();
5887
+ if (!id) return null;
5888
+ const registry = readWorkspaceDeferredRunRegistry();
5889
+ const current = registry.runs?.[id] || null;
5890
+ if (!current) return null;
5891
+ delete registry.runs[id];
5892
+ writeWorkspaceDeferredRunRegistry(registry);
5893
+ return current;
5894
+ }
5895
+
5896
+ export function workspaceDeferredRunsForScope(scopeKey) {
5897
+ const key = String(scopeKey || "");
5898
+ return Object.values(readWorkspaceDeferredRunRegistry().runs || {})
5899
+ .filter((entry) => String(entry?.scopeKey || "") === key);
5900
+ }
5901
+
5902
+ function claimWorkspaceDeferredRun(key, now = Date.now()) {
5903
+ const registry = readWorkspaceDeferredRunRegistry();
5904
+ const entry = registry.runs?.[key];
5905
+ if (!entry) return null;
5906
+ const leaseUntil = Number(entry.leaseUntil || 0);
5907
+ if (leaseUntil > now && String(entry.leaseOwner || "") !== workspaceDeferredLeaseOwner) return null;
5908
+ const claimed = {
5909
+ ...entry,
5910
+ status: "polling",
5911
+ leaseOwner: workspaceDeferredLeaseOwner,
5912
+ leaseUntil: now + WORKSPACE_DEFERRED_LEASE_MS,
5913
+ updatedAt: new Date(now).toISOString(),
5914
+ };
5915
+ registry.runs[key] = claimed;
5916
+ writeWorkspaceDeferredRunRegistry(registry);
5917
+ return claimed;
5918
+ }
5919
+
5697
5920
  export function workspaceRunPlanNodeIds(runNodeId, plan) {
5698
5921
  return Array.from(new Set([
5699
5922
  String(runNodeId || "").trim(),
@@ -5714,6 +5937,14 @@ export function workspaceFindActiveRunConflict(scopeKey, plannedNodeIds) {
5714
5937
  .filter((id) => id && planned.has(id));
5715
5938
  if (conflictNodeIds.length) return { key, entry, conflictNodeIds };
5716
5939
  }
5940
+ for (const entry of workspaceDeferredRunsForScope(scopeKey)) {
5941
+ const waitingIds = Array.isArray(entry?.plannedNodeIds) ? entry.plannedNodeIds : [];
5942
+ if (!waitingIds.length) return { key: entry.key, entry, conflictNodeIds: [] };
5943
+ const conflictNodeIds = waitingIds
5944
+ .map((id) => String(id || "").trim())
5945
+ .filter((id) => id && planned.has(id));
5946
+ if (conflictNodeIds.length) return { key: entry.key, entry, conflictNodeIds };
5947
+ }
5717
5948
  return null;
5718
5949
  }
5719
5950
 
@@ -6151,11 +6382,33 @@ export async function runWorkspaceScheduledEntry(root, entry) {
6151
6382
  signal: controller.signal,
6152
6383
  onActiveChild: setActiveChild,
6153
6384
  onEvent: (event) => appendWorkspaceRunLogEvent(runLog.runId, event),
6385
+ runId,
6154
6386
  });
6155
6387
  const currentGraph = readWorkspaceGraph(scoped.root, root).graph;
6156
6388
  const touchedIds = workspaceRunTouchedNodeIds(result);
6157
6389
  const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
6158
6390
  writeWorkspaceGraph(scoped.root, mergedGraph, root);
6391
+ if (result.deferred) {
6392
+ const waiting = upsertWorkspaceDeferredRun({
6393
+ ...runEntry,
6394
+ scheduleKey: entry.key,
6395
+ scheduleNodeId,
6396
+ }, result.deferred);
6397
+ appendWorkspaceRunLogEvent(runLog.runId, {
6398
+ type: "run-waiting",
6399
+ nodeId: waiting.nodeId,
6400
+ wakeAt: waiting.wakeAt,
6401
+ phase: waiting.phase,
6402
+ jenkinsStatus: waiting.jenkinsStatus,
6403
+ ts: Date.now(),
6404
+ });
6405
+ updateWorkspaceScheduleEntry(entry.key, {
6406
+ nextRunAt: computeNext(config),
6407
+ lastStatus: "waiting",
6408
+ lastError: "",
6409
+ });
6410
+ return;
6411
+ }
6159
6412
  const endedAt = Date.now();
6160
6413
  appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "success");
6161
6414
  finishWorkspaceRunLogSession(runLog.runId, "success", {
@@ -6197,3 +6450,157 @@ export async function runWorkspaceScheduledEntry(root, entry) {
6197
6450
  if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
6198
6451
  }
6199
6452
  }
6453
+
6454
+ function finishWorkspaceDeferredRun(entry, status, patch = {}) {
6455
+ const endedAt = Number(patch.endedAt || Date.now());
6456
+ appendWorkspaceRunFinished({
6457
+ ...entry,
6458
+ endedAt,
6459
+ durationMs: Math.max(0, endedAt - Number(entry.startedAt || endedAt)),
6460
+ }, status);
6461
+ finishWorkspaceRunLogSession(entry.runId, status, {
6462
+ endedAt,
6463
+ durationMs: Math.max(0, endedAt - Number(entry.startedAt || endedAt)),
6464
+ runNodeId: entry.runNodeId || "",
6465
+ error: String(patch.error || ""),
6466
+ });
6467
+ if (entry.scheduleKey) {
6468
+ updateWorkspaceScheduleEntry(entry.scheduleKey, {
6469
+ lastFinishedAt: endedAt,
6470
+ lastStatus: status,
6471
+ lastError: String(patch.error || ""),
6472
+ ...(patch.error ? { lastErrorAt: endedAt } : {}),
6473
+ });
6474
+ }
6475
+ }
6476
+
6477
+ async function runWorkspaceDeferredEntry(root, claimed) {
6478
+ const userCtx = { userId: String(claimed.userId || "") };
6479
+ const scoped = resolveWorkspaceScopeRoot(root, {
6480
+ flowId: claimed.flowId || "",
6481
+ flowSource: claimed.flowSource || "user",
6482
+ archived: claimed.archived === true,
6483
+ }, userCtx);
6484
+ if (scoped.error || scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
6485
+ const error = scoped.error || "Deferred Workspace target is not writable";
6486
+ removeWorkspaceDeferredRun(claimed.key);
6487
+ appendWorkspaceRunLogEvent(claimed.runId, { type: "error", error, ts: Date.now() });
6488
+ finishWorkspaceDeferredRun(claimed, "failed", { error });
6489
+ return;
6490
+ }
6491
+
6492
+ const controller = new AbortController();
6493
+ const runControl = workspaceRunControl(controller);
6494
+ const runKey = workspaceRunEntryKey(claimed.scopeKey, claimed.runId);
6495
+ const runEntry = {
6496
+ ...claimed,
6497
+ controller,
6498
+ runControl,
6499
+ plannedNodeIds: Array.isArray(claimed.plannedNodeIds) ? claimed.plannedNodeIds : [],
6500
+ };
6501
+ activeWorkspaceRuns.set(runKey, runEntry);
6502
+ let activeReleased = false;
6503
+ const releaseActive = (status = "finished") => {
6504
+ if (activeReleased) return;
6505
+ activeReleased = true;
6506
+ runControl.finish(status);
6507
+ if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
6508
+ };
6509
+ try {
6510
+ const graph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root, root).graph, userCtx);
6511
+ const result = await runWorkspaceGraph(root, scoped.root, {
6512
+ flowId: claimed.flowId,
6513
+ flowSource: claimed.flowSource || "user",
6514
+ runNodeId: claimed.runNodeId,
6515
+ graph,
6516
+ }, userCtx, {
6517
+ signal: controller.signal,
6518
+ onActiveChild: (child, options = {}) => runControl.setChild(child, options),
6519
+ onEvent: (event) => appendWorkspaceRunLogEvent(claimed.runId, event),
6520
+ runId: claimed.runId,
6521
+ });
6522
+ const currentGraph = readWorkspaceGraph(scoped.root, root).graph;
6523
+ const touchedIds = workspaceRunTouchedNodeIds(result);
6524
+ const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
6525
+ writeWorkspaceGraph(scoped.root, mergedGraph, root);
6526
+ if (result.deferred) {
6527
+ const waiting = upsertWorkspaceDeferredRun({ ...claimed, deferredKey: claimed.key }, result.deferred);
6528
+ appendWorkspaceRunLogEvent(claimed.runId, {
6529
+ type: "run-waiting",
6530
+ nodeId: waiting.nodeId,
6531
+ wakeAt: waiting.wakeAt,
6532
+ phase: waiting.phase,
6533
+ jenkinsStatus: waiting.jenkinsStatus,
6534
+ ts: Date.now(),
6535
+ });
6536
+ if (claimed.scheduleKey) updateWorkspaceScheduleEntry(claimed.scheduleKey, { lastStatus: "waiting" });
6537
+ releaseActive("waiting");
6538
+ emitWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
6539
+ type: "runtime.committed",
6540
+ runId: claimed.runId,
6541
+ runNodeId: claimed.runNodeId,
6542
+ actorId: userCtx.userId || "",
6543
+ source: "deferred-run",
6544
+ });
6545
+ emitWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
6546
+ type: "run.waiting",
6547
+ status: "waiting",
6548
+ runId: claimed.runId,
6549
+ runNodeId: claimed.runNodeId,
6550
+ actorId: userCtx.userId || "",
6551
+ });
6552
+ return;
6553
+ }
6554
+
6555
+ removeWorkspaceDeferredRun(claimed.key);
6556
+ finishWorkspaceDeferredRun(claimed, "success");
6557
+ releaseActive("finished");
6558
+ emitWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
6559
+ type: "runtime.committed",
6560
+ runId: claimed.runId,
6561
+ runNodeId: claimed.runNodeId,
6562
+ actorId: userCtx.userId || "",
6563
+ source: "deferred-run",
6564
+ });
6565
+ emitWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
6566
+ type: "run.finished",
6567
+ status: "success",
6568
+ runId: claimed.runId,
6569
+ runNodeId: claimed.runNodeId,
6570
+ actorId: userCtx.userId || "",
6571
+ });
6572
+ } catch (e) {
6573
+ const error = (e && e.message) || String(e);
6574
+ const stopped = isWorkspaceRunAbortError(e) || controller.signal.aborted;
6575
+ removeWorkspaceDeferredRun(claimed.key);
6576
+ appendWorkspaceRunLogEvent(claimed.runId, stopped
6577
+ ? { type: "stopped", message: "Workspace run stopped", ts: Date.now() }
6578
+ : { type: "error", error, ts: Date.now() });
6579
+ finishWorkspaceDeferredRun(claimed, stopped ? "stopped" : "failed", { error: stopped ? "" : error });
6580
+ releaseActive(stopped ? "stopped" : "failed");
6581
+ emitWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
6582
+ type: "run.finished",
6583
+ status: stopped ? "stopped" : "failed",
6584
+ runId: claimed.runId,
6585
+ runNodeId: claimed.runNodeId,
6586
+ actorId: userCtx.userId || "",
6587
+ });
6588
+ if (!stopped) log.info(`[workspace-deferred] failed ${claimed.flowId}/${claimed.runNodeId}: ${error}`);
6589
+ } finally {
6590
+ releaseActive(controller.signal.aborted ? "stopped" : "finished");
6591
+ }
6592
+ }
6593
+
6594
+ export function pollWorkspaceDeferredRuns(root, now = Date.now()) {
6595
+ const registry = readWorkspaceDeferredRunRegistry();
6596
+ for (const entry of Object.values(registry.runs || {})) {
6597
+ const key = String(entry?.key || "").trim();
6598
+ if (!key || activeWorkspaceDeferredRuns.has(key)) continue;
6599
+ const wakeAt = Date.parse(String(entry.wakeAt || ""));
6600
+ if (Number.isFinite(wakeAt) && wakeAt > now) continue;
6601
+ const claimed = claimWorkspaceDeferredRun(key, now);
6602
+ if (!claimed) continue;
6603
+ activeWorkspaceDeferredRuns.add(key);
6604
+ void runWorkspaceDeferredEntry(root, claimed).finally(() => activeWorkspaceDeferredRuns.delete(key));
6605
+ }
6606
+ }
@@ -0,0 +1,39 @@
1
+ ---
2
+ # 内置节点:代码展示
3
+ runtime: native
4
+ description: Display source code with language highlighting, line numbers, copy, wrap, and download controls; passes content downstream as text
5
+ displayName: Code Display
6
+ input:
7
+ - type: node
8
+ name: prev
9
+ default: ""
10
+ - type: text
11
+ name: content
12
+ default: ""
13
+ required: true
14
+ showOnNode: true
15
+ - type: text
16
+ name: language
17
+ default: ""
18
+ description: Syntax language such as javascript, typescript, python, kotlin, java, shell, json, yaml, html, css, or sql
19
+ showOnNode: false
20
+ - type: text
21
+ name: fileName
22
+ default: ""
23
+ description: Optional file name used when downloading
24
+ showOnNode: false
25
+ - type: bool
26
+ name: wrap
27
+ default: "false"
28
+ description: Wrap long source lines by default
29
+ showOnNode: false
30
+ output:
31
+ - type: text
32
+ name: content
33
+ default: ""
34
+ showOnNode: true
35
+ - type: node
36
+ name: next
37
+ default: ""
38
+ ---
39
+ ${content}
@@ -1,12 +1,13 @@
1
1
  ---
2
2
  # Built-in node: durable Jenkins build
3
- runtime: none
4
- palette: hidden
3
+ runtime: native
5
4
  description: |
6
5
  Trigger one Jenkins job and durably monitor it until completion.
7
6
 
8
- The node persists queue/build checkpoints and never keeps a process blocked while waiting.
9
- Scheduler re-enters the same node at `pollInterval`; an existing queueId/buildNumber is reused,
7
+ The node persists queue/build checkpoints and uses server-side deferred polling at `pollInterval`:
8
+ the Workspace request returns in a waiting state instead of keeping an HTTP request or worker asleep.
9
+ The AgentFlow server registry wakes the flow again and continues downstream after completion.
10
+ An existing queueId/buildNumber is reused after a run or AgentFlow server interruption,
10
11
  so a resumed run does not trigger the job again. Jenkins FAILURE/ABORTED/TIMEOUT are business
11
12
  outcomes and continue to downstream notification nodes. Authentication, configuration, and
12
13
  repeated platform request errors fail the AgentFlow node.