@fieldwangai/agentflow 0.1.83 → 0.1.85
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/ui-server.mjs +857 -32
- package/bin/lib/workspace-run-logs.mjs +181 -0
- package/builtin/nodes/tool_git_worktree_load.md +2 -1
- package/builtin/web-ui/dist/assets/index-Lb9rIMqs.js +250 -0
- package/builtin/web-ui/dist/assets/index-aqphFZlG.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-CVdxKCZm.css +0 -1
- package/builtin/web-ui/dist/assets/index-DkW7IrCF.js +0 -297
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -120,6 +120,13 @@ import {
|
|
|
120
120
|
readRunLedgerEvents,
|
|
121
121
|
runLedgerId,
|
|
122
122
|
} from "./run-ledger.mjs";
|
|
123
|
+
import {
|
|
124
|
+
appendWorkspaceRunLogEvent,
|
|
125
|
+
createWorkspaceRunLogSession,
|
|
126
|
+
finishWorkspaceRunLogSession,
|
|
127
|
+
listWorkspaceRunLogs,
|
|
128
|
+
readWorkspaceRunLogEvents,
|
|
129
|
+
} from "./workspace-run-logs.mjs";
|
|
123
130
|
|
|
124
131
|
const MIME = {
|
|
125
132
|
".html": "text/html; charset=utf-8",
|
|
@@ -1482,6 +1489,7 @@ function readWorkspaceGraph(workspaceRoot) {
|
|
|
1482
1489
|
|
|
1483
1490
|
const DISPLAY_SHARE_FILENAME = "display-shares.json";
|
|
1484
1491
|
const DISPLAY_SHARE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
1492
|
+
const DISPLAY_SHARE_ALLOWED_EXPIRY_DAYS = new Set([1, 7, 30, 90, 365]);
|
|
1485
1493
|
|
|
1486
1494
|
function displaySharesPath() {
|
|
1487
1495
|
return path.join(getAgentflowDataRoot(), DISPLAY_SHARE_FILENAME);
|
|
@@ -1931,9 +1939,30 @@ function createDisplayShareId() {
|
|
|
1931
1939
|
return crypto.randomBytes(12).toString("base64url");
|
|
1932
1940
|
}
|
|
1933
1941
|
|
|
1934
|
-
function
|
|
1942
|
+
function normalizeDisplayShareExpiry(input = {}, now = new Date()) {
|
|
1943
|
+
const mode = String(input?.expiresMode || input?.expiryMode || "").trim().toLowerCase();
|
|
1944
|
+
const rawDays = Number(input?.expiresInDays ?? input?.expiryDays ?? input?.ttlDays);
|
|
1945
|
+
if (
|
|
1946
|
+
mode === "permanent" ||
|
|
1947
|
+
mode === "forever" ||
|
|
1948
|
+
input?.permanent === true ||
|
|
1949
|
+
input?.expiresAt === null ||
|
|
1950
|
+
String(input?.expiresAt || "").trim().toLowerCase() === "permanent"
|
|
1951
|
+
) {
|
|
1952
|
+
return { expiresAt: "", expiresMode: "permanent", expiresInDays: null };
|
|
1953
|
+
}
|
|
1954
|
+
let days = Number.isFinite(rawDays) ? Math.round(rawDays) : 30;
|
|
1955
|
+
if (!DISPLAY_SHARE_ALLOWED_EXPIRY_DAYS.has(days)) days = 30;
|
|
1935
1956
|
const time = now instanceof Date ? now.getTime() : Date.now();
|
|
1936
|
-
return
|
|
1957
|
+
return {
|
|
1958
|
+
expiresAt: new Date(time + days * 24 * 60 * 60 * 1000).toISOString(),
|
|
1959
|
+
expiresMode: "days",
|
|
1960
|
+
expiresInDays: days,
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
function displayShareExpiresAt(now = new Date()) {
|
|
1965
|
+
return normalizeDisplayShareExpiry({ expiresInDays: 30 }, now).expiresAt;
|
|
1937
1966
|
}
|
|
1938
1967
|
|
|
1939
1968
|
function isDisplayShareExpired(share) {
|
|
@@ -1971,12 +2000,13 @@ function normalizeDisplayShareLayout(layout, fallback = "canvas") {
|
|
|
1971
2000
|
return ["canvas", "gallery", "slides", "document", "single"].includes(text) ? text : fallback;
|
|
1972
2001
|
}
|
|
1973
2002
|
|
|
1974
|
-
function createDisplayShareRecord({ userId, flowId, flowSource, archived, title, layout, nodeIds }) {
|
|
2003
|
+
function createDisplayShareRecord({ userId, flowId, flowSource, archived, title, layout, nodeIds, expiresMode, expiresInDays, permanent, expiresAt }) {
|
|
1975
2004
|
const shares = readDisplayShares();
|
|
1976
2005
|
let id = createDisplayShareId();
|
|
1977
2006
|
while (shares[id]) id = createDisplayShareId();
|
|
1978
2007
|
const nowDate = new Date();
|
|
1979
2008
|
const now = nowDate.toISOString();
|
|
2009
|
+
const expiry = normalizeDisplayShareExpiry({ expiresMode, expiresInDays, permanent, expiresAt }, nowDate);
|
|
1980
2010
|
const share = {
|
|
1981
2011
|
id,
|
|
1982
2012
|
userId: String(userId || ""),
|
|
@@ -1988,13 +2018,84 @@ function createDisplayShareRecord({ userId, flowId, flowSource, archived, title,
|
|
|
1988
2018
|
nodeIds: Array.isArray(nodeIds) ? nodeIds : [],
|
|
1989
2019
|
createdAt: now,
|
|
1990
2020
|
updatedAt: now,
|
|
1991
|
-
expiresAt:
|
|
2021
|
+
expiresAt: expiry.expiresAt,
|
|
2022
|
+
expiresMode: expiry.expiresMode,
|
|
2023
|
+
expiresInDays: expiry.expiresInDays,
|
|
1992
2024
|
};
|
|
1993
2025
|
shares[id] = share;
|
|
1994
2026
|
writeDisplayShares(shares);
|
|
1995
2027
|
return share;
|
|
1996
2028
|
}
|
|
1997
2029
|
|
|
2030
|
+
function displayShareSummary(share, baseUrl = "") {
|
|
2031
|
+
return {
|
|
2032
|
+
id: String(share?.id || ""),
|
|
2033
|
+
userId: String(share?.userId || ""),
|
|
2034
|
+
flowId: String(share?.flowId || ""),
|
|
2035
|
+
flowSource: String(share?.flowSource || "user"),
|
|
2036
|
+
archived: share?.archived === true,
|
|
2037
|
+
title: String(share?.title || "AgentFlow Display"),
|
|
2038
|
+
layout: String(share?.layout || "gallery"),
|
|
2039
|
+
nodeIds: Array.isArray(share?.nodeIds) ? share.nodeIds : [],
|
|
2040
|
+
createdAt: String(share?.createdAt || ""),
|
|
2041
|
+
updatedAt: String(share?.updatedAt || ""),
|
|
2042
|
+
expiresAt: String(share?.expiresAt || ""),
|
|
2043
|
+
expiresMode: String(share?.expiresMode || (share?.expiresAt ? "days" : "permanent")),
|
|
2044
|
+
expiresInDays: share?.expiresInDays == null ? null : Number(share.expiresInDays),
|
|
2045
|
+
url: displayShareOutputUrl(share?.id || "", baseUrl),
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
function listDisplaySharesForUser(userCtx = {}, baseUrl = "") {
|
|
2050
|
+
const userId = String(userCtx?.userId || "");
|
|
2051
|
+
const isAdmin = userCtx?.isAdmin === true;
|
|
2052
|
+
const shares = readDisplayShares();
|
|
2053
|
+
let changed = false;
|
|
2054
|
+
const rows = [];
|
|
2055
|
+
for (const [id, share] of Object.entries(shares)) {
|
|
2056
|
+
if (isDisplayShareExpired(share)) {
|
|
2057
|
+
delete shares[id];
|
|
2058
|
+
changed = true;
|
|
2059
|
+
continue;
|
|
2060
|
+
}
|
|
2061
|
+
if (!isAdmin && String(share?.userId || "") !== userId) continue;
|
|
2062
|
+
rows.push(displayShareSummary(share, baseUrl));
|
|
2063
|
+
}
|
|
2064
|
+
if (changed) writeDisplayShares(shares);
|
|
2065
|
+
rows.sort((a, b) => Date.parse(b.createdAt || "") - Date.parse(a.createdAt || ""));
|
|
2066
|
+
return rows;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function updateDisplayShareExpiryForUser(id, userCtx = {}, patch = {}) {
|
|
2070
|
+
const shares = readDisplayShares();
|
|
2071
|
+
const share = shares[id];
|
|
2072
|
+
if (!share) return { status: 404, error: "Display share not found" };
|
|
2073
|
+
const userId = String(userCtx?.userId || "");
|
|
2074
|
+
if (userCtx?.isAdmin !== true && String(share.userId || "") !== userId) return { status: 403, error: "Forbidden" };
|
|
2075
|
+
const expiry = normalizeDisplayShareExpiry(patch, new Date());
|
|
2076
|
+
const updated = {
|
|
2077
|
+
...share,
|
|
2078
|
+
expiresAt: expiry.expiresAt,
|
|
2079
|
+
expiresMode: expiry.expiresMode,
|
|
2080
|
+
expiresInDays: expiry.expiresInDays,
|
|
2081
|
+
updatedAt: new Date().toISOString(),
|
|
2082
|
+
};
|
|
2083
|
+
shares[id] = updated;
|
|
2084
|
+
writeDisplayShares(shares);
|
|
2085
|
+
return { status: 200, share: updated };
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function deleteDisplayShareForUser(id, userCtx = {}) {
|
|
2089
|
+
const shares = readDisplayShares();
|
|
2090
|
+
const share = shares[id];
|
|
2091
|
+
if (!share) return { status: 404, error: "Display share not found" };
|
|
2092
|
+
const userId = String(userCtx?.userId || "");
|
|
2093
|
+
if (userCtx?.isAdmin !== true && String(share.userId || "") !== userId) return { status: 403, error: "Forbidden" };
|
|
2094
|
+
delete shares[id];
|
|
2095
|
+
writeDisplayShares(shares);
|
|
2096
|
+
return { status: 200 };
|
|
2097
|
+
}
|
|
2098
|
+
|
|
1998
2099
|
function parseDisplayShareNodeIdInput(value) {
|
|
1999
2100
|
return String(value || "")
|
|
2000
2101
|
.split(/[\s,,]+/g)
|
|
@@ -2146,6 +2247,8 @@ function publicDisplayPayloadFromShare(root, share) {
|
|
|
2146
2247
|
createdAt: share.createdAt || "",
|
|
2147
2248
|
updatedAt: share.updatedAt || "",
|
|
2148
2249
|
expiresAt: share.expiresAt || "",
|
|
2250
|
+
expiresMode: share.expiresMode || (share.expiresAt ? "days" : "permanent"),
|
|
2251
|
+
expiresInDays: share.expiresInDays == null ? null : Number(share.expiresInDays),
|
|
2149
2252
|
},
|
|
2150
2253
|
nodes,
|
|
2151
2254
|
};
|
|
@@ -3832,6 +3935,135 @@ function workspaceBuildImplementationPrompt(instance, nodeId, opts = {}) {
|
|
|
3832
3935
|
].filter((line) => line !== "").join("\n");
|
|
3833
3936
|
}
|
|
3834
3937
|
|
|
3938
|
+
function workspaceImplementationPlanNeighbors(graph, nodeId) {
|
|
3939
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
3940
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
3941
|
+
const incoming = [];
|
|
3942
|
+
const outgoing = [];
|
|
3943
|
+
for (const edge of edges) {
|
|
3944
|
+
if (String(edge?.target || "") === String(nodeId)) {
|
|
3945
|
+
const sourceId = String(edge?.source || "");
|
|
3946
|
+
if (sourceId) incoming.push(`${sourceId} (${instances[sourceId]?.label || instances[sourceId]?.definitionId || "node"})`);
|
|
3947
|
+
}
|
|
3948
|
+
if (String(edge?.source || "") === String(nodeId)) {
|
|
3949
|
+
const targetId = String(edge?.target || "");
|
|
3950
|
+
if (targetId) outgoing.push(`${targetId} (${instances[targetId]?.label || instances[targetId]?.definitionId || "node"})`);
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
return {
|
|
3954
|
+
incoming: workspaceUniqueImplementationList(incoming, 20),
|
|
3955
|
+
outgoing: workspaceUniqueImplementationList(outgoing, 20),
|
|
3956
|
+
};
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
function workspaceBuildPlannedImplementationPrompt(graph, nodeId, opts = {}) {
|
|
3960
|
+
const instance = graph?.instances?.[nodeId] || {};
|
|
3961
|
+
const defId = String(instance?.definitionId || "").trim();
|
|
3962
|
+
const label = String(instance?.label || nodeId || "node").trim();
|
|
3963
|
+
const mode = workspaceImplementationModeForInstance(instance);
|
|
3964
|
+
const inputValues = opts.inputValues || {};
|
|
3965
|
+
const task = workspaceResolveBodyPlaceholders(instance?.body || "", inputValues).trim();
|
|
3966
|
+
const scriptRef = String(instance?.scriptRef || "").trim();
|
|
3967
|
+
const inlineScript = String(instance?.script || "").trim();
|
|
3968
|
+
const previousImplementation = opts.previousImplementation
|
|
3969
|
+
? workspaceClipImplementationText(opts.previousImplementation, 12000)
|
|
3970
|
+
: "";
|
|
3971
|
+
const neighbors = workspaceImplementationPlanNeighbors(graph, nodeId);
|
|
3972
|
+
return [
|
|
3973
|
+
"你要为 AgentFlow Workspace 的一个重复执行节点提前写“实现方案”Markdown。",
|
|
3974
|
+
"这个 implementation.md 会在后续 Scheduled Run 执行同一节点前作为参考上下文,目标是减少重复推理、提前固定执行路径和脚本约定。",
|
|
3975
|
+
"",
|
|
3976
|
+
"要求:",
|
|
3977
|
+
"- 基于当前节点任务、输入槽、上下游关系,写一份可复用的执行方案。",
|
|
3978
|
+
"- 写清楚下次运行应优先采用的步骤、文件路径、输入输出协议、错误处理和可复用约定。",
|
|
3979
|
+
"- 如果是脚本类节点,重点写清楚脚本入口、环境变量、输入 JSON/输出文件协议、幂等性和失败重试策略。",
|
|
3980
|
+
"- 不要假装已经执行过;这是执行前优化计划,不要引用不存在的实际结果。",
|
|
3981
|
+
"- 只输出 Markdown 正文,不要输出代码围栏包裹整篇。",
|
|
3982
|
+
"",
|
|
3983
|
+
"## 节点信息",
|
|
3984
|
+
"",
|
|
3985
|
+
`nodeId: ${nodeId}`,
|
|
3986
|
+
`label: ${label}`,
|
|
3987
|
+
`definitionId: ${defId || "(unknown)"}`,
|
|
3988
|
+
`mode: ${mode}`,
|
|
3989
|
+
scriptRef ? `scriptRef: ${scriptRef}` : "",
|
|
3990
|
+
inlineScript ? `inlineScript: ${workspaceClipImplementationText(inlineScript, 2400)}` : "",
|
|
3991
|
+
"",
|
|
3992
|
+
"## 当前任务",
|
|
3993
|
+
"",
|
|
3994
|
+
workspaceClipImplementationText(task || instance?.body || scriptRef || inlineScript || "(无显式任务)", 8000),
|
|
3995
|
+
"",
|
|
3996
|
+
"## 可见输入",
|
|
3997
|
+
"",
|
|
3998
|
+
JSON.stringify(inputValues || {}, null, 2),
|
|
3999
|
+
"",
|
|
4000
|
+
"## 上下游",
|
|
4001
|
+
"",
|
|
4002
|
+
`incoming: ${neighbors.incoming.length ? neighbors.incoming.join(", ") : "(none)"}`,
|
|
4003
|
+
`outgoing: ${neighbors.outgoing.length ? neighbors.outgoing.join(", ") : "(none)"}`,
|
|
4004
|
+
"",
|
|
4005
|
+
previousImplementation ? "## 上一版实现方案" : "",
|
|
4006
|
+
previousImplementation || "",
|
|
4007
|
+
].filter((line) => line !== "").join("\n");
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
async function workspaceGeneratePlannedImplementationMarkdown({
|
|
4011
|
+
scopedRoot,
|
|
4012
|
+
graph,
|
|
4013
|
+
nodeId,
|
|
4014
|
+
inputValues,
|
|
4015
|
+
implementationPath,
|
|
4016
|
+
previousImplementation,
|
|
4017
|
+
runPackage,
|
|
4018
|
+
modelKey,
|
|
4019
|
+
userCtx,
|
|
4020
|
+
emit,
|
|
4021
|
+
onActiveChild,
|
|
4022
|
+
}) {
|
|
4023
|
+
const prompt = workspaceBuildPlannedImplementationPrompt(graph, nodeId, {
|
|
4024
|
+
inputValues,
|
|
4025
|
+
previousImplementation,
|
|
4026
|
+
});
|
|
4027
|
+
let content = "";
|
|
4028
|
+
let lastAssistant = "";
|
|
4029
|
+
let resultText = "";
|
|
4030
|
+
emit?.({ type: "status", nodeId, line: `Generate implementation plan: ${nodeId}` });
|
|
4031
|
+
const handle = startComposerAgent({
|
|
4032
|
+
uiWorkspaceRoot: scopedRoot,
|
|
4033
|
+
cliWorkspace: runPackage?.nodeRunDir || scopedRoot,
|
|
4034
|
+
prompt,
|
|
4035
|
+
modelKey,
|
|
4036
|
+
agentflowUserId: userCtx?.userId || "",
|
|
4037
|
+
extraEnv: runtimeEnvForUser(userCtx, {
|
|
4038
|
+
AGENTFLOW_IMPLEMENTATION_REF: implementationPath || "",
|
|
4039
|
+
AGENTFLOW_NODE_RUN_DIR: runPackage?.nodeRunDir || "",
|
|
4040
|
+
AGENTFLOW_NODE_TMP_DIR: runPackage?.nodeTmpDir || "",
|
|
4041
|
+
AGENTFLOW_OUTPUTS_DIR: runPackage?.outputsDir || "",
|
|
4042
|
+
}),
|
|
4043
|
+
onStreamEvent: (ev) => {
|
|
4044
|
+
if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
|
|
4045
|
+
lastAssistant = ev.text;
|
|
4046
|
+
content += (content ? "\n" : "") + ev.text;
|
|
4047
|
+
} else if (ev?.type === "natural" && ev.kind === "result" && typeof ev.text === "string") {
|
|
4048
|
+
resultText = ev.text;
|
|
4049
|
+
}
|
|
4050
|
+
},
|
|
4051
|
+
onToolCall: (subtype, toolName) => {
|
|
4052
|
+
const sub = subtype ? String(subtype) : "";
|
|
4053
|
+
const tool = toolName ? String(toolName) : "";
|
|
4054
|
+
emit?.({ type: "status", nodeId, line: `优化工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
|
|
4055
|
+
},
|
|
4056
|
+
});
|
|
4057
|
+
if (typeof onActiveChild === "function") onActiveChild(handle.child || null);
|
|
4058
|
+
try {
|
|
4059
|
+
await handle.finished;
|
|
4060
|
+
} finally {
|
|
4061
|
+
if (typeof onActiveChild === "function") onActiveChild(null);
|
|
4062
|
+
}
|
|
4063
|
+
const markdown = String(resultText || lastAssistant || content || "").trim();
|
|
4064
|
+
return markdown.replace(/^```(?:markdown|md)?\s*/i, "").replace(/```\s*$/i, "").trim();
|
|
4065
|
+
}
|
|
4066
|
+
|
|
3835
4067
|
async function workspaceGenerateImplementationMarkdown({
|
|
3836
4068
|
scopedRoot,
|
|
3837
4069
|
nodeId,
|
|
@@ -4033,6 +4265,88 @@ async function workspaceTryPersistNodeImplementation(scopedRoot, graph, nodeId,
|
|
|
4033
4265
|
}
|
|
4034
4266
|
}
|
|
4035
4267
|
|
|
4268
|
+
function workspaceShouldOptimizeNodeImplementation(instance) {
|
|
4269
|
+
const defId = String(instance?.definitionId || "").trim();
|
|
4270
|
+
if (!defId) return false;
|
|
4271
|
+
if (defId === "workspace_run" || defId === "workspace_scheduled_run") return false;
|
|
4272
|
+
if (defId.startsWith("display_") || defId.startsWith("provide_") || defId.startsWith("control_")) return false;
|
|
4273
|
+
return defId === "agent_subAgent" || defId === "tool_nodejs" || defId.startsWith("tool_");
|
|
4274
|
+
}
|
|
4275
|
+
|
|
4276
|
+
async function workspaceOptimizeNodeImplementation(scopedRoot, graph, nodeId, opts = {}) {
|
|
4277
|
+
const current = graph?.instances?.[nodeId];
|
|
4278
|
+
if (!current || !workspaceShouldOptimizeNodeImplementation(current)) {
|
|
4279
|
+
return { optimized: false, skipped: true, nodeId, reason: "not optimizable" };
|
|
4280
|
+
}
|
|
4281
|
+
const implementationRef = String(current.implementationRef || "").trim() || workspaceDefaultImplementationRef(nodeId);
|
|
4282
|
+
const abs = workspaceResolveFlowFile(scopedRoot, implementationRef, "implementationRef");
|
|
4283
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
4284
|
+
const previousImplementation = fs.existsSync(abs) && fs.statSync(abs).isFile()
|
|
4285
|
+
? workspaceReadTextFileIfExists(abs, 60000)
|
|
4286
|
+
: "";
|
|
4287
|
+
const inputValues = workspaceInputValues(graph, nodeId, new Map(), scopedRoot);
|
|
4288
|
+
const runPackage = workspaceCreateNodeRunPackage(opts.runTmpRoot || workspaceCreateRunTmpRoot(scopedRoot, "optimize"), nodeId, {
|
|
4289
|
+
scopedRoot,
|
|
4290
|
+
cwd: scopedRoot,
|
|
4291
|
+
task: workspaceResolveBodyPlaceholders(current.body || current.script || current.scriptRef || "", inputValues),
|
|
4292
|
+
inputValues,
|
|
4293
|
+
});
|
|
4294
|
+
const markdown = await workspaceGeneratePlannedImplementationMarkdown({
|
|
4295
|
+
scopedRoot,
|
|
4296
|
+
graph,
|
|
4297
|
+
nodeId,
|
|
4298
|
+
inputValues: { ...inputValues, ...(runPackage.inputValues || {}) },
|
|
4299
|
+
implementationPath: implementationRef,
|
|
4300
|
+
previousImplementation,
|
|
4301
|
+
runPackage,
|
|
4302
|
+
modelKey: opts.modelKey || "",
|
|
4303
|
+
userCtx: opts.userCtx || {},
|
|
4304
|
+
emit: opts.emit,
|
|
4305
|
+
onActiveChild: opts.onActiveChild,
|
|
4306
|
+
});
|
|
4307
|
+
if (!markdown.trim()) throw new Error(`Implementation plan is empty for node ${nodeId}`);
|
|
4308
|
+
fs.writeFileSync(abs, markdown.trimEnd() + "\n", "utf-8");
|
|
4309
|
+
const explicitMode = String(current.implementationMode || "").trim();
|
|
4310
|
+
graph.instances[nodeId] = {
|
|
4311
|
+
...current,
|
|
4312
|
+
implementationRef,
|
|
4313
|
+
implementationMode: explicitMode || workspaceImplementationModeForInstance(current),
|
|
4314
|
+
};
|
|
4315
|
+
return { optimized: true, nodeId, implementationRef };
|
|
4316
|
+
}
|
|
4317
|
+
|
|
4318
|
+
async function workspaceOptimizeRunImplementations(root, scopedRoot, payload, userCtx = {}, opts = {}) {
|
|
4319
|
+
const graph = hydrateWorkspaceGraphForRuntime(root, {
|
|
4320
|
+
root: scopedRoot,
|
|
4321
|
+
flowId: payload.flowId || "",
|
|
4322
|
+
flowSource: payload.flowSource || "user",
|
|
4323
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
4324
|
+
}, payload.graph || {}, userCtx);
|
|
4325
|
+
const runNodeId = String(payload?.runNodeId || "").trim();
|
|
4326
|
+
const plan = workspaceRunPlan(graph, runNodeId, scopedRoot);
|
|
4327
|
+
const runTmpRoot = workspaceCreateRunTmpRoot(scopedRoot, `${runNodeId || "run"}-optimize`);
|
|
4328
|
+
const optimized = [];
|
|
4329
|
+
const skipped = [];
|
|
4330
|
+
for (const nodeId of plan.order) {
|
|
4331
|
+
const instance = graph.instances?.[nodeId];
|
|
4332
|
+
if (!workspaceShouldOptimizeNodeImplementation(instance)) {
|
|
4333
|
+
skipped.push({ nodeId, reason: "not optimizable" });
|
|
4334
|
+
continue;
|
|
4335
|
+
}
|
|
4336
|
+
opts.emit?.({ type: "node-start", nodeId, definitionId: instance.definitionId, phase: "optimize" });
|
|
4337
|
+
const result = await workspaceOptimizeNodeImplementation(scopedRoot, graph, nodeId, {
|
|
4338
|
+
runTmpRoot,
|
|
4339
|
+
modelKey: payload.model || "",
|
|
4340
|
+
userCtx,
|
|
4341
|
+
emit: opts.emit,
|
|
4342
|
+
onActiveChild: opts.onActiveChild,
|
|
4343
|
+
});
|
|
4344
|
+
optimized.push(result);
|
|
4345
|
+
opts.emit?.({ type: "node-done", nodeId, definitionId: instance.definitionId, phase: "optimize", implementationRef: result.implementationRef });
|
|
4346
|
+
}
|
|
4347
|
+
return { ok: true, graph, order: plan.order, optimized, skipped };
|
|
4348
|
+
}
|
|
4349
|
+
|
|
4036
4350
|
function parseWorkspaceSkillKeys(raw) {
|
|
4037
4351
|
const text = String(raw || "").trim();
|
|
4038
4352
|
if (!text) return [];
|
|
@@ -4329,8 +4643,11 @@ function workspaceDefaultWorktreePath(runTmpRoot, nodeId, repoPath, branch = "")
|
|
|
4329
4643
|
);
|
|
4330
4644
|
}
|
|
4331
4645
|
|
|
4332
|
-
function workspaceShouldAutoCleanupWorktree(result) {
|
|
4333
|
-
|
|
4646
|
+
function workspaceShouldAutoCleanupWorktree(result, scopedRoot = "") {
|
|
4647
|
+
const worktreePath = String(result?.worktreePath || "").trim();
|
|
4648
|
+
if (!worktreePath) return false;
|
|
4649
|
+
if (result.created === true) return true;
|
|
4650
|
+
return scopedRoot ? workspacePathInside(scopedRoot, worktreePath) : false;
|
|
4334
4651
|
}
|
|
4335
4652
|
|
|
4336
4653
|
function workspaceTrackAutoCleanupWorktree(list, item) {
|
|
@@ -5010,7 +5327,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5010
5327
|
const pruneMissingRaw = workspaceSlotValue(workspaceSlotByName(instance, "pruneMissing")).trim().toLowerCase();
|
|
5011
5328
|
const pruneMissing = pruneMissingRaw !== "false";
|
|
5012
5329
|
const result = loadGitWorktree({ repoPath, branch, worktreePath, pipelineWorkspace: scopedRoot, force, pruneMissing });
|
|
5013
|
-
if (workspaceShouldAutoCleanupWorktree(result)) {
|
|
5330
|
+
if (workspaceShouldAutoCleanupWorktree(result, scopedRoot)) {
|
|
5014
5331
|
workspaceTrackAutoCleanupWorktree(autoCleanupWorktrees, {
|
|
5015
5332
|
nodeId,
|
|
5016
5333
|
repoPath: result.repoRoot,
|
|
@@ -5177,6 +5494,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5177
5494
|
title,
|
|
5178
5495
|
layout,
|
|
5179
5496
|
nodeIds,
|
|
5497
|
+
expiresMode: payload.expiresMode,
|
|
5498
|
+
expiresInDays: payload.expiresInDays,
|
|
5499
|
+
permanent: payload.permanent,
|
|
5500
|
+
expiresAt: payload.expiresAt,
|
|
5180
5501
|
});
|
|
5181
5502
|
const url = displayShareOutputUrl(share.id, baseUrl);
|
|
5182
5503
|
let nextInstance = workspaceSetOutputSlot(instance, "url", url);
|
|
@@ -5567,7 +5888,7 @@ const activeFlowRuns = new Map();
|
|
|
5567
5888
|
const activeWorkspaceRuns = new Map();
|
|
5568
5889
|
const WORKSPACE_SCHEDULES_FILENAME = "workspace-schedules.json";
|
|
5569
5890
|
const WORKSPACE_SCHEDULE_POLL_MS = 30_000;
|
|
5570
|
-
const WORKSPACE_IMPLEMENTATION_REFERENCE_ENABLED =
|
|
5891
|
+
const WORKSPACE_IMPLEMENTATION_REFERENCE_ENABLED = true;
|
|
5571
5892
|
const WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED = false;
|
|
5572
5893
|
const WORKSPACE_NODE_HISTORY_MAX_CHARS = 80000;
|
|
5573
5894
|
|
|
@@ -5703,6 +6024,77 @@ function listWorkspaceScheduleStatusesForFlow(userCtx = {}, flowSource = "user",
|
|
|
5703
6024
|
.sort((a, b) => String(a.scheduleNodeId || a.runNodeId || "").localeCompare(String(b.scheduleNodeId || b.runNodeId || "")));
|
|
5704
6025
|
}
|
|
5705
6026
|
|
|
6027
|
+
function listWorkspaceScheduleStatuses(root, userCtx = {}) {
|
|
6028
|
+
const registry = readWorkspaceScheduleRegistry();
|
|
6029
|
+
const userId = String(userCtx.userId || "");
|
|
6030
|
+
const flows = listFlowsJson(root, { ...userCtx, includeWorkspaceFlows: true })
|
|
6031
|
+
.filter((flow) => !flow.archived && !isReadonlyBuiltinFlowSource(flow.source || "user"));
|
|
6032
|
+
const rows = [];
|
|
6033
|
+
for (const flow of flows) {
|
|
6034
|
+
const flowId = String(flow.id || "");
|
|
6035
|
+
const flowSource = String(flow.source || "user");
|
|
6036
|
+
const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource }, userCtx);
|
|
6037
|
+
if (scoped.error || !scoped.root) continue;
|
|
6038
|
+
let graph;
|
|
6039
|
+
try {
|
|
6040
|
+
graph = readWorkspaceGraph(scoped.root).graph;
|
|
6041
|
+
} catch {
|
|
6042
|
+
continue;
|
|
6043
|
+
}
|
|
6044
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
6045
|
+
for (const [scheduleNodeId, instance] of Object.entries(instances)) {
|
|
6046
|
+
if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
|
|
6047
|
+
const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
|
|
6048
|
+
const key = workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId);
|
|
6049
|
+
const current = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
|
|
6050
|
+
const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
|
|
6051
|
+
const scopeKey = workspaceRunKey(userCtx, flowSource, flowId);
|
|
6052
|
+
const running = workspaceActiveRunsForScope(scopeKey).some(([, active]) => (
|
|
6053
|
+
active?.scheduled === true &&
|
|
6054
|
+
String(active?.runNodeId || "") === String(targetRunNodeId || scheduleNodeId)
|
|
6055
|
+
));
|
|
6056
|
+
let nextRunAt = current.nextRunAt || null;
|
|
6057
|
+
let lastStatus = current.lastStatus || (config.enabled ? "armed" : "disabled");
|
|
6058
|
+
let lastError = current.lastError || "";
|
|
6059
|
+
if (config.enabled && !nextRunAt) {
|
|
6060
|
+
try {
|
|
6061
|
+
nextRunAt = workspaceScheduleNextRunAt(config, new Date());
|
|
6062
|
+
} catch (e) {
|
|
6063
|
+
lastStatus = "invalid";
|
|
6064
|
+
lastError = (e && e.message) || String(e);
|
|
6065
|
+
}
|
|
6066
|
+
}
|
|
6067
|
+
rows.push({
|
|
6068
|
+
kind: "workspace",
|
|
6069
|
+
key,
|
|
6070
|
+
flowId,
|
|
6071
|
+
flowSource,
|
|
6072
|
+
scheduleNodeId,
|
|
6073
|
+
runNodeId: targetRunNodeId,
|
|
6074
|
+
label: String(instance.label || "Scheduled Run"),
|
|
6075
|
+
enabled: config.enabled,
|
|
6076
|
+
cron: config.cron,
|
|
6077
|
+
timezone: config.timezone,
|
|
6078
|
+
preset: "",
|
|
6079
|
+
nextRunAt,
|
|
6080
|
+
lastTriggeredAt: current.lastTriggeredAt || null,
|
|
6081
|
+
lastFinishedAt: current.lastFinishedAt || null,
|
|
6082
|
+
lastRunId: current.lastRunId || "",
|
|
6083
|
+
lastStatus,
|
|
6084
|
+
lastError,
|
|
6085
|
+
running,
|
|
6086
|
+
waiting: 0,
|
|
6087
|
+
});
|
|
6088
|
+
}
|
|
6089
|
+
}
|
|
6090
|
+
rows.sort((a, b) => {
|
|
6091
|
+
const ea = a.enabled ? 0 : 1;
|
|
6092
|
+
const eb = b.enabled ? 0 : 1;
|
|
6093
|
+
return ea - eb || String(a.nextRunAt || "").localeCompare(String(b.nextRunAt || "")) || a.flowId.localeCompare(b.flowId);
|
|
6094
|
+
});
|
|
6095
|
+
return rows;
|
|
6096
|
+
}
|
|
6097
|
+
|
|
5706
6098
|
function workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config = {}) {
|
|
5707
6099
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
5708
6100
|
return String(instances[scheduleNodeId]?.definitionId || "") === "workspace_scheduled_run"
|
|
@@ -5710,6 +6102,35 @@ function workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config = {
|
|
|
5710
6102
|
: "";
|
|
5711
6103
|
}
|
|
5712
6104
|
|
|
6105
|
+
function setWorkspaceScheduleEnabled(root, payload = {}, authUser = {}, userCtx = {}) {
|
|
6106
|
+
const flowId = String(payload.flowId || "").trim();
|
|
6107
|
+
const flowSource = String(payload.flowSource || "user").trim() || "user";
|
|
6108
|
+
const scheduleNodeId = String(payload.scheduleNodeId || "").trim();
|
|
6109
|
+
if (!flowId) return { success: false, error: "Missing flowId" };
|
|
6110
|
+
if (!scheduleNodeId) return { success: false, error: "Missing scheduleNodeId" };
|
|
6111
|
+
if (!isValidFlowSourceWrite(flowSource)) return { success: false, error: "Cannot update readonly workspace schedule" };
|
|
6112
|
+
const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource }, userCtx);
|
|
6113
|
+
if (scoped.error) return { success: false, error: scoped.error };
|
|
6114
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
6115
|
+
return { success: false, error: "Cannot update schedule for builtin or archived workspace" };
|
|
6116
|
+
}
|
|
6117
|
+
const { graph } = readWorkspaceGraph(scoped.root);
|
|
6118
|
+
const instance = graph.instances?.[scheduleNodeId];
|
|
6119
|
+
if (!instance || String(instance.definitionId || "") !== "workspace_scheduled_run") {
|
|
6120
|
+
return { success: false, error: "Workspace schedule node not found" };
|
|
6121
|
+
}
|
|
6122
|
+
const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
|
|
6123
|
+
const nextConfig = { ...config, enabled: payload.enabled === true };
|
|
6124
|
+
graph.instances = { ...(graph.instances || {}) };
|
|
6125
|
+
graph.instances[scheduleNodeId] = {
|
|
6126
|
+
...instance,
|
|
6127
|
+
body: JSON.stringify(nextConfig),
|
|
6128
|
+
};
|
|
6129
|
+
fs.writeFileSync(workspaceGraphPath(scoped.root), JSON.stringify(graph, null, 2) + "\n", "utf-8");
|
|
6130
|
+
const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
|
|
6131
|
+
return { success: true, workspaceSchedules };
|
|
6132
|
+
}
|
|
6133
|
+
|
|
5713
6134
|
function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx = {}) {
|
|
5714
6135
|
const flowId = String(scoped?.flowId || "").trim();
|
|
5715
6136
|
const flowSource = String(scoped?.flowSource || "user");
|
|
@@ -5727,7 +6148,6 @@ function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx =
|
|
|
5727
6148
|
for (const [scheduleNodeId, instance] of Object.entries(instances)) {
|
|
5728
6149
|
if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
|
|
5729
6150
|
const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
|
|
5730
|
-
if (!config.enabled) continue;
|
|
5731
6151
|
const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
|
|
5732
6152
|
const key = workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId);
|
|
5733
6153
|
const previous = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
|
|
@@ -5741,9 +6161,15 @@ function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx =
|
|
|
5741
6161
|
let lastStatus = previous.lastStatus || "armed";
|
|
5742
6162
|
let lastError = previous.lastError || "";
|
|
5743
6163
|
try {
|
|
5744
|
-
nextRunAt =
|
|
6164
|
+
nextRunAt = !config.enabled
|
|
6165
|
+
? null
|
|
6166
|
+
: previousMatches && Number.isFinite(previousNext) && previousNext > now
|
|
5745
6167
|
? previousNext
|
|
5746
6168
|
: workspaceScheduleNextRunAt(config, new Date(now));
|
|
6169
|
+
if (!config.enabled) {
|
|
6170
|
+
lastStatus = "disabled";
|
|
6171
|
+
lastError = "";
|
|
6172
|
+
}
|
|
5747
6173
|
if (!targetRunNodeId) {
|
|
5748
6174
|
lastStatus = "invalid";
|
|
5749
6175
|
lastError = "Scheduled Run node is missing";
|
|
@@ -5756,7 +6182,7 @@ function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx =
|
|
|
5756
6182
|
schedules[key] = {
|
|
5757
6183
|
...previous,
|
|
5758
6184
|
key,
|
|
5759
|
-
enabled:
|
|
6185
|
+
enabled: config.enabled,
|
|
5760
6186
|
userId,
|
|
5761
6187
|
username: String(authUser?.username || previous.username || userId),
|
|
5762
6188
|
flowId,
|
|
@@ -5796,6 +6222,21 @@ function updateWorkspaceScheduleEntry(key, patch) {
|
|
|
5796
6222
|
async function runWorkspaceScheduledEntry(root, entry) {
|
|
5797
6223
|
const userCtx = { userId: String(entry.userId || "") };
|
|
5798
6224
|
const scopeKey = workspaceRunKey(userCtx, entry.flowSource || "user", entry.flowId || "");
|
|
6225
|
+
const authUsers = readAuthUsers();
|
|
6226
|
+
const authUser = authUsers[userCtx.userId] || {};
|
|
6227
|
+
const runId = runLedgerId("workspace");
|
|
6228
|
+
const runLog = createWorkspaceRunLogSession({
|
|
6229
|
+
runId,
|
|
6230
|
+
userId: userCtx.userId,
|
|
6231
|
+
username: String(authUser.username || entry.username || userCtx.userId),
|
|
6232
|
+
flowId: String(entry.flowId || ""),
|
|
6233
|
+
flowSource: String(entry.flowSource || "user"),
|
|
6234
|
+
scheduleNodeId: String(entry.scheduleNodeId || entry.key?.split(":").pop() || ""),
|
|
6235
|
+
runNodeId: String(entry.targetRunNodeId || entry.runNodeId || ""),
|
|
6236
|
+
scheduled: true,
|
|
6237
|
+
trigger: "scheduled",
|
|
6238
|
+
label: String(entry.label || "Scheduled Run"),
|
|
6239
|
+
});
|
|
5799
6240
|
const fallbackConfig = {
|
|
5800
6241
|
enabled: true,
|
|
5801
6242
|
cron: String(entry.cron || "0 9 * * *"),
|
|
@@ -5816,10 +6257,14 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5816
6257
|
flowSource: entry.flowSource || "user",
|
|
5817
6258
|
}, userCtx);
|
|
5818
6259
|
if (scoped.error || scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
6260
|
+
const error = scoped.error || "Workspace schedule target is not writable";
|
|
6261
|
+
appendWorkspaceRunLogEvent(runLog.runId, { type: "error", error });
|
|
6262
|
+
finishWorkspaceRunLogSession(runLog.runId, "failed", { error });
|
|
5819
6263
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5820
6264
|
nextRunAt,
|
|
5821
6265
|
lastStatus: "error",
|
|
5822
|
-
|
|
6266
|
+
lastRunId: runLog.runId,
|
|
6267
|
+
lastError: error,
|
|
5823
6268
|
lastErrorAt: Date.now(),
|
|
5824
6269
|
});
|
|
5825
6270
|
return;
|
|
@@ -5831,31 +6276,43 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5831
6276
|
const config = normalizeWorkspaceScheduledRunConfig(instance?.body || "");
|
|
5832
6277
|
nextRunAt = computeNext(config);
|
|
5833
6278
|
if (!instance || String(instance.definitionId || "") !== "workspace_scheduled_run" || !config.enabled) {
|
|
6279
|
+
appendWorkspaceRunLogEvent(runLog.runId, { type: "disabled", scheduleNodeId });
|
|
6280
|
+
finishWorkspaceRunLogSession(runLog.runId, "disabled");
|
|
5834
6281
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5835
6282
|
enabled: false,
|
|
5836
6283
|
nextRunAt: null,
|
|
5837
6284
|
lastStatus: "disabled",
|
|
6285
|
+
lastRunId: runLog.runId,
|
|
5838
6286
|
});
|
|
5839
6287
|
return;
|
|
5840
6288
|
}
|
|
5841
6289
|
const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
|
|
5842
6290
|
if (!targetRunNodeId) {
|
|
6291
|
+
const error = "Scheduled Run node is missing";
|
|
6292
|
+
appendWorkspaceRunLogEvent(runLog.runId, { type: "invalid", error, scheduleNodeId });
|
|
6293
|
+
finishWorkspaceRunLogSession(runLog.runId, "failed", { error });
|
|
5843
6294
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5844
6295
|
nextRunAt,
|
|
5845
6296
|
lastStatus: "invalid",
|
|
5846
|
-
|
|
6297
|
+
lastRunId: runLog.runId,
|
|
6298
|
+
lastError: error,
|
|
5847
6299
|
lastErrorAt: Date.now(),
|
|
5848
6300
|
});
|
|
5849
6301
|
return;
|
|
5850
6302
|
}
|
|
6303
|
+
appendWorkspaceRunLogEvent(runLog.runId, { type: "scheduler-triggered", scheduleNodeId, runNodeId: targetRunNodeId, cron: config.cron, timezone: config.timezone });
|
|
5851
6304
|
let plan;
|
|
5852
6305
|
try {
|
|
5853
6306
|
plan = workspaceRunPlan(graph, targetRunNodeId, scoped.root);
|
|
5854
6307
|
} catch (e) {
|
|
6308
|
+
const error = (e && e.message) || String(e);
|
|
6309
|
+
appendWorkspaceRunLogEvent(runLog.runId, { type: "error", error });
|
|
6310
|
+
finishWorkspaceRunLogSession(runLog.runId, "failed", { error, runNodeId: targetRunNodeId });
|
|
5855
6311
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5856
6312
|
nextRunAt,
|
|
5857
6313
|
lastStatus: "failed",
|
|
5858
|
-
|
|
6314
|
+
lastRunId: runLog.runId,
|
|
6315
|
+
lastError: error,
|
|
5859
6316
|
lastErrorAt: Date.now(),
|
|
5860
6317
|
});
|
|
5861
6318
|
return;
|
|
@@ -5863,19 +6320,25 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5863
6320
|
const plannedNodeIds = workspaceRunPlanNodeIds(targetRunNodeId, plan);
|
|
5864
6321
|
const conflict = workspaceFindActiveRunConflict(scopeKey, plannedNodeIds);
|
|
5865
6322
|
if (conflict) {
|
|
6323
|
+
appendWorkspaceRunLogEvent(runLog.runId, {
|
|
6324
|
+
type: "skipped",
|
|
6325
|
+
reason: "busy",
|
|
6326
|
+
runNodeId: targetRunNodeId,
|
|
6327
|
+
conflictRunId: conflict.entry?.runId || "",
|
|
6328
|
+
conflictNodeIds: conflict.conflictNodeIds,
|
|
6329
|
+
});
|
|
6330
|
+
finishWorkspaceRunLogSession(runLog.runId, "skipped", { runNodeId: targetRunNodeId, error: "" });
|
|
5866
6331
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5867
6332
|
nextRunAt,
|
|
5868
6333
|
lastSkippedAt: Date.now(),
|
|
5869
6334
|
lastStatus: "skipped: busy",
|
|
6335
|
+
lastRunId: runLog.runId,
|
|
5870
6336
|
lastError: "",
|
|
5871
6337
|
});
|
|
5872
6338
|
return;
|
|
5873
6339
|
}
|
|
5874
6340
|
|
|
5875
6341
|
const controller = new AbortController();
|
|
5876
|
-
const authUsers = readAuthUsers();
|
|
5877
|
-
const authUser = authUsers[userCtx.userId] || {};
|
|
5878
|
-
const runId = runLedgerId("workspace");
|
|
5879
6342
|
const runKey = workspaceRunEntryKey(scopeKey, runId);
|
|
5880
6343
|
const runEntry = {
|
|
5881
6344
|
scopeKey,
|
|
@@ -5921,6 +6384,7 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5921
6384
|
}, userCtx, {
|
|
5922
6385
|
signal: controller.signal,
|
|
5923
6386
|
onActiveChild: setActiveChild,
|
|
6387
|
+
onEvent: (event) => appendWorkspaceRunLogEvent(runLog.runId, event),
|
|
5924
6388
|
});
|
|
5925
6389
|
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
5926
6390
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
@@ -5928,6 +6392,11 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5928
6392
|
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
5929
6393
|
const endedAt = Date.now();
|
|
5930
6394
|
appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "success");
|
|
6395
|
+
finishWorkspaceRunLogSession(runLog.runId, "success", {
|
|
6396
|
+
endedAt,
|
|
6397
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
6398
|
+
runNodeId: targetRunNodeId,
|
|
6399
|
+
});
|
|
5931
6400
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5932
6401
|
nextRunAt: computeNext(config),
|
|
5933
6402
|
lastFinishedAt: endedAt,
|
|
@@ -5936,15 +6405,23 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5936
6405
|
});
|
|
5937
6406
|
} catch (e) {
|
|
5938
6407
|
const endedAt = Date.now();
|
|
6408
|
+
const error = (e && e.message) || String(e);
|
|
5939
6409
|
appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "failed");
|
|
6410
|
+
appendWorkspaceRunLogEvent(runLog.runId, { type: "error", error, ts: endedAt });
|
|
6411
|
+
finishWorkspaceRunLogSession(runLog.runId, "failed", {
|
|
6412
|
+
endedAt,
|
|
6413
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
6414
|
+
runNodeId: targetRunNodeId,
|
|
6415
|
+
error,
|
|
6416
|
+
});
|
|
5940
6417
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5941
6418
|
nextRunAt: computeNext(config),
|
|
5942
6419
|
lastFinishedAt: endedAt,
|
|
5943
6420
|
lastStatus: "failed",
|
|
5944
|
-
lastError:
|
|
6421
|
+
lastError: error,
|
|
5945
6422
|
lastErrorAt: endedAt,
|
|
5946
6423
|
});
|
|
5947
|
-
log.info(`[workspace-scheduler] failed ${entry.flowId}/${targetRunNodeId}: ${
|
|
6424
|
+
log.info(`[workspace-scheduler] failed ${entry.flowId}/${targetRunNodeId}: ${error}`);
|
|
5948
6425
|
} finally {
|
|
5949
6426
|
if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
|
|
5950
6427
|
}
|
|
@@ -6168,6 +6645,21 @@ export function startUiServer({
|
|
|
6168
6645
|
|
|
6169
6646
|
const authUser = getAuthUserFromRequest(req);
|
|
6170
6647
|
const userCtx = authUser ? { userId: authUser.userId, isAdmin: Boolean(authUser.isAdmin) } : {};
|
|
6648
|
+
if (req.method === "GET" && url.pathname === "/api/display/shares") {
|
|
6649
|
+
try {
|
|
6650
|
+
if (!authUser?.userId) {
|
|
6651
|
+
json(res, 401, { error: "Unauthorized" });
|
|
6652
|
+
return;
|
|
6653
|
+
}
|
|
6654
|
+
json(res, 200, {
|
|
6655
|
+
shares: listDisplaySharesForUser(userCtx, requestPublicBaseUrl(req)),
|
|
6656
|
+
});
|
|
6657
|
+
} catch (e) {
|
|
6658
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
6659
|
+
}
|
|
6660
|
+
return;
|
|
6661
|
+
}
|
|
6662
|
+
|
|
6171
6663
|
if (req.method === "GET" && url.pathname === "/api/display/share") {
|
|
6172
6664
|
try {
|
|
6173
6665
|
const id = String(url.searchParams.get("id") || "").trim();
|
|
@@ -6192,6 +6684,62 @@ export function startUiServer({
|
|
|
6192
6684
|
return;
|
|
6193
6685
|
}
|
|
6194
6686
|
|
|
6687
|
+
if (req.method === "PATCH" && url.pathname === "/api/display/share") {
|
|
6688
|
+
let payload;
|
|
6689
|
+
try {
|
|
6690
|
+
payload = JSON.parse(await readBody(req));
|
|
6691
|
+
} catch {
|
|
6692
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
6693
|
+
return;
|
|
6694
|
+
}
|
|
6695
|
+
try {
|
|
6696
|
+
if (!authUser?.userId) {
|
|
6697
|
+
json(res, 401, { error: "Unauthorized" });
|
|
6698
|
+
return;
|
|
6699
|
+
}
|
|
6700
|
+
const id = String(payload?.id || url.searchParams.get("id") || "").trim();
|
|
6701
|
+
if (!id) {
|
|
6702
|
+
json(res, 400, { error: "Missing display share id" });
|
|
6703
|
+
return;
|
|
6704
|
+
}
|
|
6705
|
+
const result = updateDisplayShareExpiryForUser(id, userCtx, payload);
|
|
6706
|
+
if (result.error) {
|
|
6707
|
+
json(res, result.status || 400, { error: result.error });
|
|
6708
|
+
return;
|
|
6709
|
+
}
|
|
6710
|
+
json(res, 200, {
|
|
6711
|
+
ok: true,
|
|
6712
|
+
share: displayShareSummary(result.share, requestPublicBaseUrl(req)),
|
|
6713
|
+
});
|
|
6714
|
+
} catch (e) {
|
|
6715
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
6716
|
+
}
|
|
6717
|
+
return;
|
|
6718
|
+
}
|
|
6719
|
+
|
|
6720
|
+
if (req.method === "DELETE" && url.pathname === "/api/display/share") {
|
|
6721
|
+
try {
|
|
6722
|
+
if (!authUser?.userId) {
|
|
6723
|
+
json(res, 401, { error: "Unauthorized" });
|
|
6724
|
+
return;
|
|
6725
|
+
}
|
|
6726
|
+
const id = String(url.searchParams.get("id") || "").trim();
|
|
6727
|
+
if (!id) {
|
|
6728
|
+
json(res, 400, { error: "Missing display share id" });
|
|
6729
|
+
return;
|
|
6730
|
+
}
|
|
6731
|
+
const result = deleteDisplayShareForUser(id, userCtx);
|
|
6732
|
+
if (result.error) {
|
|
6733
|
+
json(res, result.status || 400, { error: result.error });
|
|
6734
|
+
return;
|
|
6735
|
+
}
|
|
6736
|
+
json(res, 200, { ok: true });
|
|
6737
|
+
} catch (e) {
|
|
6738
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
6739
|
+
}
|
|
6740
|
+
return;
|
|
6741
|
+
}
|
|
6742
|
+
|
|
6195
6743
|
if (req.method === "GET" && url.pathname === "/api/display/file/raw") {
|
|
6196
6744
|
try {
|
|
6197
6745
|
const id = String(url.searchParams.get("id") || "").trim();
|
|
@@ -6690,6 +7238,10 @@ export function startUiServer({
|
|
|
6690
7238
|
title: payload.title,
|
|
6691
7239
|
layout: payload.layout,
|
|
6692
7240
|
nodeIds,
|
|
7241
|
+
expiresMode: payload.expiresMode,
|
|
7242
|
+
expiresInDays: payload.expiresInDays,
|
|
7243
|
+
permanent: payload.permanent,
|
|
7244
|
+
expiresAt: payload.expiresAt,
|
|
6693
7245
|
});
|
|
6694
7246
|
json(res, 200, { ok: true, share, url: `/display/${encodeURIComponent(share.id)}` });
|
|
6695
7247
|
} catch (e) {
|
|
@@ -6797,6 +7349,57 @@ export function startUiServer({
|
|
|
6797
7349
|
return;
|
|
6798
7350
|
}
|
|
6799
7351
|
|
|
7352
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/run/optimize") {
|
|
7353
|
+
let payload;
|
|
7354
|
+
try {
|
|
7355
|
+
payload = JSON.parse(await readBody(req));
|
|
7356
|
+
} catch {
|
|
7357
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
7358
|
+
return;
|
|
7359
|
+
}
|
|
7360
|
+
try {
|
|
7361
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
7362
|
+
flowId: payload.flowId || "",
|
|
7363
|
+
flowSource: payload.flowSource || "user",
|
|
7364
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
7365
|
+
}, userCtx);
|
|
7366
|
+
if (scoped.error) {
|
|
7367
|
+
json(res, 400, { error: scoped.error });
|
|
7368
|
+
return;
|
|
7369
|
+
}
|
|
7370
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
7371
|
+
json(res, 400, { error: "Cannot optimize workspace graph for builtin or archived pipeline" });
|
|
7372
|
+
return;
|
|
7373
|
+
}
|
|
7374
|
+
const flowId = String(payload.flowId || "").trim();
|
|
7375
|
+
if (!flowId) {
|
|
7376
|
+
json(res, 400, { error: "Missing flowId" });
|
|
7377
|
+
return;
|
|
7378
|
+
}
|
|
7379
|
+
const graphPath = workspaceGraphPath(scoped.root);
|
|
7380
|
+
const result = await workspaceOptimizeRunImplementations(root, scoped.root, payload, userCtx, {
|
|
7381
|
+
emit: () => {},
|
|
7382
|
+
});
|
|
7383
|
+
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
7384
|
+
const touchedIds = new Set((result.optimized || []).map((item) => item.nodeId).filter(Boolean));
|
|
7385
|
+
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
7386
|
+
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
7387
|
+
const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, mergedGraph, authUser, userCtx);
|
|
7388
|
+
json(res, 200, {
|
|
7389
|
+
ok: true,
|
|
7390
|
+
path: graphPath,
|
|
7391
|
+
graph: mergedGraph,
|
|
7392
|
+
order: result.order,
|
|
7393
|
+
optimized: result.optimized,
|
|
7394
|
+
skipped: result.skipped,
|
|
7395
|
+
workspaceSchedules,
|
|
7396
|
+
});
|
|
7397
|
+
} catch (e) {
|
|
7398
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
7399
|
+
}
|
|
7400
|
+
return;
|
|
7401
|
+
}
|
|
7402
|
+
|
|
6800
7403
|
if (req.method === "POST" && url.pathname === "/api/workspace/run") {
|
|
6801
7404
|
let payload;
|
|
6802
7405
|
try {
|
|
@@ -6861,6 +7464,19 @@ export function startUiServer({
|
|
|
6861
7464
|
}
|
|
6862
7465
|
},
|
|
6863
7466
|
};
|
|
7467
|
+
const runLog = createWorkspaceRunLogSession({
|
|
7468
|
+
runId,
|
|
7469
|
+
userId: runEntry.userId,
|
|
7470
|
+
username: runEntry.username,
|
|
7471
|
+
flowId: runEntry.flowId,
|
|
7472
|
+
flowSource: runEntry.flowSource,
|
|
7473
|
+
scheduleNodeId: String(runtimeGraph.instances?.[runNodeId]?.definitionId || "") === "workspace_scheduled_run" ? runNodeId : "",
|
|
7474
|
+
runNodeId,
|
|
7475
|
+
scheduled: false,
|
|
7476
|
+
trigger: "manual",
|
|
7477
|
+
label: String(runtimeGraph.instances?.[runNodeId]?.label || "Workspace Run"),
|
|
7478
|
+
startedAt: runEntry.startedAt,
|
|
7479
|
+
});
|
|
6864
7480
|
activeWorkspaceRuns.set(runKey, runEntry);
|
|
6865
7481
|
appendWorkspaceRunStarted(runEntry);
|
|
6866
7482
|
const setActiveChild = (child) => {
|
|
@@ -6879,6 +7495,7 @@ export function startUiServer({
|
|
|
6879
7495
|
"X-Accel-Buffering": "no",
|
|
6880
7496
|
});
|
|
6881
7497
|
const writeEvent = (event) => {
|
|
7498
|
+
appendWorkspaceRunLogEvent(runLog.runId, event);
|
|
6882
7499
|
try { res.write(JSON.stringify(event) + "\n"); } catch (_) {}
|
|
6883
7500
|
};
|
|
6884
7501
|
try {
|
|
@@ -6891,28 +7508,47 @@ export function startUiServer({
|
|
|
6891
7508
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
6892
7509
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
6893
7510
|
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
7511
|
+
const endedAt = Date.now();
|
|
6894
7512
|
appendWorkspaceRunFinished({
|
|
6895
7513
|
...runEntry,
|
|
6896
|
-
endedAt
|
|
6897
|
-
durationMs:
|
|
7514
|
+
endedAt,
|
|
7515
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
6898
7516
|
}, "success");
|
|
7517
|
+
finishWorkspaceRunLogSession(runLog.runId, "success", {
|
|
7518
|
+
endedAt,
|
|
7519
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
7520
|
+
runNodeId,
|
|
7521
|
+
});
|
|
6899
7522
|
writeEvent({ type: "done", ok: true, path: graphPath, graph: mergedGraph, order: result.order, touchedNodeIds: Array.from(touchedIds), pauseNodeIds: result.pauseNodeIds || [] });
|
|
6900
7523
|
res.end();
|
|
6901
7524
|
} catch (e) {
|
|
7525
|
+
const endedAt = Date.now();
|
|
6902
7526
|
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
6903
7527
|
appendWorkspaceRunFinished({
|
|
6904
7528
|
...runEntry,
|
|
6905
|
-
endedAt
|
|
6906
|
-
durationMs:
|
|
7529
|
+
endedAt,
|
|
7530
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
6907
7531
|
}, "stopped");
|
|
7532
|
+
finishWorkspaceRunLogSession(runLog.runId, "stopped", {
|
|
7533
|
+
endedAt,
|
|
7534
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
7535
|
+
runNodeId,
|
|
7536
|
+
});
|
|
6908
7537
|
writeEvent({ type: "stopped", ok: false, stopped: true, message: "Workspace run stopped" });
|
|
6909
7538
|
} else {
|
|
7539
|
+
const error = (e && e.message) || String(e);
|
|
6910
7540
|
appendWorkspaceRunFinished({
|
|
6911
7541
|
...runEntry,
|
|
6912
|
-
endedAt
|
|
6913
|
-
durationMs:
|
|
7542
|
+
endedAt,
|
|
7543
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
6914
7544
|
}, "failed");
|
|
6915
|
-
|
|
7545
|
+
finishWorkspaceRunLogSession(runLog.runId, "failed", {
|
|
7546
|
+
endedAt,
|
|
7547
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
7548
|
+
runNodeId,
|
|
7549
|
+
error,
|
|
7550
|
+
});
|
|
7551
|
+
writeEvent({ type: "error", error });
|
|
6916
7552
|
}
|
|
6917
7553
|
res.end();
|
|
6918
7554
|
} finally {
|
|
@@ -6924,32 +7560,53 @@ export function startUiServer({
|
|
|
6924
7560
|
const result = await runWorkspaceGraph(root, scoped.root, { ...payload, requestBaseUrl: requestPublicBaseUrl(req) }, userCtx, {
|
|
6925
7561
|
signal: controller.signal,
|
|
6926
7562
|
onActiveChild: setActiveChild,
|
|
7563
|
+
onEvent: (event) => appendWorkspaceRunLogEvent(runLog.runId, event),
|
|
6927
7564
|
});
|
|
6928
7565
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
6929
7566
|
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
6930
7567
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
6931
7568
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
6932
7569
|
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
7570
|
+
const endedAt = Date.now();
|
|
6933
7571
|
appendWorkspaceRunFinished({
|
|
6934
7572
|
...runEntry,
|
|
6935
|
-
endedAt
|
|
6936
|
-
durationMs:
|
|
7573
|
+
endedAt,
|
|
7574
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
6937
7575
|
}, "success");
|
|
7576
|
+
finishWorkspaceRunLogSession(runLog.runId, "success", {
|
|
7577
|
+
endedAt,
|
|
7578
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
7579
|
+
runNodeId,
|
|
7580
|
+
});
|
|
6938
7581
|
json(res, 200, { ok: true, path: graphPath, ...result, graph: mergedGraph, touchedNodeIds: Array.from(touchedIds) });
|
|
6939
7582
|
} catch (e) {
|
|
7583
|
+
const endedAt = Date.now();
|
|
6940
7584
|
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
6941
7585
|
appendWorkspaceRunFinished({
|
|
6942
7586
|
...runEntry,
|
|
6943
|
-
endedAt
|
|
6944
|
-
durationMs:
|
|
7587
|
+
endedAt,
|
|
7588
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
6945
7589
|
}, "stopped");
|
|
7590
|
+
finishWorkspaceRunLogSession(runLog.runId, "stopped", {
|
|
7591
|
+
endedAt,
|
|
7592
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
7593
|
+
runNodeId,
|
|
7594
|
+
});
|
|
6946
7595
|
json(res, 200, { ok: false, stopped: true, message: "Workspace run stopped" });
|
|
6947
7596
|
} else {
|
|
7597
|
+
const error = (e && e.message) || String(e);
|
|
6948
7598
|
appendWorkspaceRunFinished({
|
|
6949
7599
|
...runEntry,
|
|
6950
|
-
endedAt
|
|
6951
|
-
durationMs:
|
|
7600
|
+
endedAt,
|
|
7601
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
6952
7602
|
}, "failed");
|
|
7603
|
+
appendWorkspaceRunLogEvent(runLog.runId, { type: "error", error, ts: endedAt });
|
|
7604
|
+
finishWorkspaceRunLogSession(runLog.runId, "failed", {
|
|
7605
|
+
endedAt,
|
|
7606
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
7607
|
+
runNodeId,
|
|
7608
|
+
error,
|
|
7609
|
+
});
|
|
6953
7610
|
throw e;
|
|
6954
7611
|
}
|
|
6955
7612
|
} finally {
|
|
@@ -6961,6 +7618,52 @@ export function startUiServer({
|
|
|
6961
7618
|
return;
|
|
6962
7619
|
}
|
|
6963
7620
|
|
|
7621
|
+
if (req.method === "GET" && url.pathname === "/api/workspace/run-logs") {
|
|
7622
|
+
try {
|
|
7623
|
+
const flowId = url.searchParams.get("flowId") || "";
|
|
7624
|
+
const flowSource = url.searchParams.get("flowSource") || "";
|
|
7625
|
+
const scheduleNodeId = url.searchParams.get("scheduleNodeId") || "";
|
|
7626
|
+
const runNodeId = url.searchParams.get("runNodeId") || "";
|
|
7627
|
+
const limit = Number(url.searchParams.get("limit") || 50);
|
|
7628
|
+
json(res, 200, {
|
|
7629
|
+
runs: listWorkspaceRunLogs({
|
|
7630
|
+
userId: userCtx.userId || "",
|
|
7631
|
+
flowId,
|
|
7632
|
+
flowSource,
|
|
7633
|
+
scheduleNodeId,
|
|
7634
|
+
runNodeId,
|
|
7635
|
+
limit,
|
|
7636
|
+
}),
|
|
7637
|
+
});
|
|
7638
|
+
} catch (e) {
|
|
7639
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
7640
|
+
}
|
|
7641
|
+
return;
|
|
7642
|
+
}
|
|
7643
|
+
|
|
7644
|
+
if (req.method === "GET" && url.pathname.startsWith("/api/workspace/run-logs/")) {
|
|
7645
|
+
try {
|
|
7646
|
+
const runId = decodeURIComponent(url.pathname.slice("/api/workspace/run-logs/".length));
|
|
7647
|
+
if (!runId) {
|
|
7648
|
+
json(res, 400, { error: "Missing runId" });
|
|
7649
|
+
return;
|
|
7650
|
+
}
|
|
7651
|
+
const run = listWorkspaceRunLogs({ userId: userCtx.userId || "", limit: 200 })
|
|
7652
|
+
.find((item) => String(item.runId || "") === runId);
|
|
7653
|
+
if (!run) {
|
|
7654
|
+
json(res, 404, { error: "Run log not found" });
|
|
7655
|
+
return;
|
|
7656
|
+
}
|
|
7657
|
+
json(res, 200, {
|
|
7658
|
+
run,
|
|
7659
|
+
events: readWorkspaceRunLogEvents(runId),
|
|
7660
|
+
});
|
|
7661
|
+
} catch (e) {
|
|
7662
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
7663
|
+
}
|
|
7664
|
+
return;
|
|
7665
|
+
}
|
|
7666
|
+
|
|
6964
7667
|
if (req.method === "GET" && url.pathname === "/api/workspace/run/status") {
|
|
6965
7668
|
const flowId = typeof url.searchParams.get("flowId") === "string" ? url.searchParams.get("flowId").trim() : "";
|
|
6966
7669
|
if (!flowId) {
|
|
@@ -8708,6 +9411,128 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
8708
9411
|
return;
|
|
8709
9412
|
}
|
|
8710
9413
|
|
|
9414
|
+
if (req.method === "GET" && url.pathname === "/api/flow/schedules") {
|
|
9415
|
+
try {
|
|
9416
|
+
json(res, 200, { schedules: listScheduleStatuses(root, userCtx) });
|
|
9417
|
+
} catch (e) {
|
|
9418
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
9419
|
+
}
|
|
9420
|
+
return;
|
|
9421
|
+
}
|
|
9422
|
+
|
|
9423
|
+
if (req.method === "GET" && url.pathname === "/api/schedules") {
|
|
9424
|
+
try {
|
|
9425
|
+
const pipelineSchedules = listScheduleStatuses(root, userCtx)
|
|
9426
|
+
.filter((schedule) => (
|
|
9427
|
+
schedule.enabled ||
|
|
9428
|
+
schedule.cron ||
|
|
9429
|
+
schedule.nextRunAt ||
|
|
9430
|
+
schedule.lastTriggeredAt ||
|
|
9431
|
+
schedule.lastRunUuid ||
|
|
9432
|
+
schedule.lastError ||
|
|
9433
|
+
schedule.running ||
|
|
9434
|
+
schedule.waiting
|
|
9435
|
+
))
|
|
9436
|
+
.map((schedule) => ({
|
|
9437
|
+
kind: "pipeline",
|
|
9438
|
+
...schedule,
|
|
9439
|
+
}));
|
|
9440
|
+
const workspaceSchedules = listWorkspaceScheduleStatuses(root, userCtx);
|
|
9441
|
+
json(res, 200, {
|
|
9442
|
+
schedules: [...workspaceSchedules, ...pipelineSchedules].sort((a, b) => {
|
|
9443
|
+
const ea = a.enabled ? 0 : 1;
|
|
9444
|
+
const eb = b.enabled ? 0 : 1;
|
|
9445
|
+
return ea - eb || String(a.nextRunAt || "").localeCompare(String(b.nextRunAt || "")) || String(a.flowId || "").localeCompare(String(b.flowId || ""));
|
|
9446
|
+
}),
|
|
9447
|
+
});
|
|
9448
|
+
} catch (e) {
|
|
9449
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
9450
|
+
}
|
|
9451
|
+
return;
|
|
9452
|
+
}
|
|
9453
|
+
|
|
9454
|
+
if (req.method === "POST" && url.pathname === "/api/schedule/toggle") {
|
|
9455
|
+
let payload;
|
|
9456
|
+
try {
|
|
9457
|
+
payload = JSON.parse(await readBody(req));
|
|
9458
|
+
} catch {
|
|
9459
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
9460
|
+
return;
|
|
9461
|
+
}
|
|
9462
|
+
const kind = String(payload.kind || "").trim();
|
|
9463
|
+
if (kind === "workspace") {
|
|
9464
|
+
try {
|
|
9465
|
+
const result = setWorkspaceScheduleEnabled(root, payload, authUser, userCtx);
|
|
9466
|
+
if (!result.success) {
|
|
9467
|
+
json(res, 400, { error: result.error || "Could not update workspace schedule" });
|
|
9468
|
+
return;
|
|
9469
|
+
}
|
|
9470
|
+
json(res, 200, { success: true });
|
|
9471
|
+
} catch (e) {
|
|
9472
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
9473
|
+
}
|
|
9474
|
+
return;
|
|
9475
|
+
}
|
|
9476
|
+
if (kind === "pipeline") {
|
|
9477
|
+
const flowId = String(payload.flowId || "").trim();
|
|
9478
|
+
const flowSource = String(payload.flowSource || "user").trim() || "user";
|
|
9479
|
+
if (!flowId) {
|
|
9480
|
+
json(res, 400, { error: "Missing flowId" });
|
|
9481
|
+
return;
|
|
9482
|
+
}
|
|
9483
|
+
if (!isValidFlowSourceWrite(flowSource)) {
|
|
9484
|
+
json(res, 400, { error: "Cannot update schedule for builtin or readonly flow" });
|
|
9485
|
+
return;
|
|
9486
|
+
}
|
|
9487
|
+
const current = readFlowSchedule(root, flowId, flowSource, userCtx);
|
|
9488
|
+
if (!current.success) {
|
|
9489
|
+
json(res, 400, { error: current.error || "Could not read schedule" });
|
|
9490
|
+
return;
|
|
9491
|
+
}
|
|
9492
|
+
const result = writeFlowSchedule(root, flowId, flowSource, { ...current.schedule, enabled: payload.enabled === true }, userCtx);
|
|
9493
|
+
if (!result.success) {
|
|
9494
|
+
json(res, 400, { error: result.error || "Could not update schedule" });
|
|
9495
|
+
return;
|
|
9496
|
+
}
|
|
9497
|
+
json(res, 200, { success: true, schedule: result.schedule });
|
|
9498
|
+
return;
|
|
9499
|
+
}
|
|
9500
|
+
json(res, 400, { error: "Invalid schedule kind" });
|
|
9501
|
+
return;
|
|
9502
|
+
}
|
|
9503
|
+
|
|
9504
|
+
if (req.method === "POST" && url.pathname === "/api/flow/schedule/disable") {
|
|
9505
|
+
let payload;
|
|
9506
|
+
try {
|
|
9507
|
+
payload = JSON.parse(await readBody(req));
|
|
9508
|
+
} catch {
|
|
9509
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
9510
|
+
return;
|
|
9511
|
+
}
|
|
9512
|
+
const flowId = String(payload.flowId || "").trim();
|
|
9513
|
+
const flowSource = String(payload.flowSource || "user").trim() || "user";
|
|
9514
|
+
if (!flowId) {
|
|
9515
|
+
json(res, 400, { error: "Missing flowId" });
|
|
9516
|
+
return;
|
|
9517
|
+
}
|
|
9518
|
+
if (!isValidFlowSourceWrite(flowSource)) {
|
|
9519
|
+
json(res, 400, { error: "Cannot disable schedule for builtin or readonly flow" });
|
|
9520
|
+
return;
|
|
9521
|
+
}
|
|
9522
|
+
const current = readFlowSchedule(root, flowId, flowSource, userCtx);
|
|
9523
|
+
if (!current.success) {
|
|
9524
|
+
json(res, 400, { error: current.error || "Could not read schedule" });
|
|
9525
|
+
return;
|
|
9526
|
+
}
|
|
9527
|
+
const result = writeFlowSchedule(root, flowId, flowSource, { ...current.schedule, enabled: false }, userCtx);
|
|
9528
|
+
if (!result.success) {
|
|
9529
|
+
json(res, 400, { error: result.error || "Could not disable schedule" });
|
|
9530
|
+
return;
|
|
9531
|
+
}
|
|
9532
|
+
json(res, 200, { success: true, schedule: result.schedule });
|
|
9533
|
+
return;
|
|
9534
|
+
}
|
|
9535
|
+
|
|
8711
9536
|
if (req.method === "POST" && url.pathname === "/api/flow/run") {
|
|
8712
9537
|
let payload;
|
|
8713
9538
|
try {
|