@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.
- package/bin/lib/flow-dsl/codegen.mjs +33 -22
- package/bin/lib/flow-dsl/parser.mjs +24 -4
- package/bin/lib/jenkins.mjs +368 -0
- package/bin/lib/legacy-flow-execution.mjs +0 -1
- package/bin/lib/locales/en.json +4 -0
- package/bin/lib/locales/zh.json +4 -0
- package/bin/lib/mcp-server.mjs +1 -0
- package/bin/lib/ui-server.mjs +72 -0
- package/bin/lib/workspace-routes.mjs +113 -9
- package/bin/lib/workspace-server.mjs +416 -9
- package/builtin/nodes/display_code.md +39 -0
- package/builtin/nodes/tool_jenkins_build.md +5 -4
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-ClNxY2Wu.js → WorkflowAssistantThread-CY19DsYq.js} +1 -1
- package/builtin/web-ui/dist/assets/index-B9ppXv7e.css +1 -0
- package/builtin/web-ui/dist/assets/index-CdEQWRrp.js +872 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-flow-dsl/SKILL.md +14 -1
- package/skills/agentflow-flow-dsl/references/node-calls.md +2 -0
- package/skills/agentflow-node-dsl/SKILL.md +27 -3
- package/skills/agentflow-node-reference/references/builtin-nodes.md +16 -0
- package/builtin/web-ui/dist/assets/index-BJzMYRK3.js +0 -870
- package/builtin/web-ui/dist/assets/index-CEXmmwM2.css +0 -1
|
@@ -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
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
9
|
-
|
|
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.
|