@fieldwangai/agentflow 0.1.70 → 0.1.71
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/catalog-flows.mjs +1 -0
- package/bin/lib/locales/en.json +8 -0
- package/bin/lib/locales/zh.json +8 -0
- package/bin/lib/marketplace.mjs +1 -0
- package/bin/lib/paths.mjs +2 -0
- package/bin/lib/ui-server.mjs +505 -28
- package/bin/lib/wecom.mjs +102 -0
- package/bin/pipeline/pre-process-node.mjs +40 -8
- package/builtin/nodes/tool_wecom_send_app_markdown.md +56 -0
- package/builtin/nodes/tool_wecom_send_group_markdown.md +44 -0
- package/builtin/web-ui/dist/assets/{index-ByGcCRCN.css → index-B3Wr-I3p.css} +1 -1
- package/builtin/web-ui/dist/assets/index-C8JMDUZb.js +296 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-BdgCU6QK.js +0 -296
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -92,6 +92,7 @@ import {
|
|
|
92
92
|
} from "./marketplace.mjs";
|
|
93
93
|
import { buildGitContext, inferGitRepoRootFromWorktree, loadGitWorktree, normalizeGitContext, runGit, sanitizeWorktreeName, unloadGitWorktree } from "./git-worktree.mjs";
|
|
94
94
|
import { createGitLabMergeRequest } from "./gitlab-mr.mjs";
|
|
95
|
+
import { sendWecomAppMarkdown, sendWecomGroupMarkdown } from "./wecom.mjs";
|
|
95
96
|
import {
|
|
96
97
|
authSetupRequired,
|
|
97
98
|
buildClearSessionCookie,
|
|
@@ -2106,6 +2107,57 @@ function hydrateWorkspaceNodeRefsFromFiles(scopedRoot, graph) {
|
|
|
2106
2107
|
return changed ? { ...next, instances } : next;
|
|
2107
2108
|
}
|
|
2108
2109
|
|
|
2110
|
+
function workspaceMergeSlotsWithDefinitionMeta(slots, definitionSlots) {
|
|
2111
|
+
const current = Array.isArray(slots) ? slots : [];
|
|
2112
|
+
const defs = Array.isArray(definitionSlots) ? definitionSlots : [];
|
|
2113
|
+
const byName = new Map(defs.map((slot) => [String(slot?.name || ""), slot]));
|
|
2114
|
+
return current.map((slot, index) => {
|
|
2115
|
+
const def = byName.get(String(slot?.name || "")) || defs[index] || null;
|
|
2116
|
+
if (!def) return slot;
|
|
2117
|
+
const next = { ...slot };
|
|
2118
|
+
for (const key of ["type", "name", "description", "required", "showOnNode"]) {
|
|
2119
|
+
if (next[key] == null || String(next[key]).trim?.() === "") {
|
|
2120
|
+
if (def[key] != null) next[key] = def[key];
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
return next;
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
function hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped = {}, graph = {}, userCtx = {}) {
|
|
2128
|
+
const next = normalizeWorkspaceGraphPayload(graph || {});
|
|
2129
|
+
let definitions = [];
|
|
2130
|
+
try {
|
|
2131
|
+
definitions = listNodesJson(workspaceRoot, scoped.flowId || "", scoped.flowSource || "user", {
|
|
2132
|
+
archived: scoped.archived === true,
|
|
2133
|
+
userId: userCtx?.userId || "",
|
|
2134
|
+
}).nodes || [];
|
|
2135
|
+
} catch {
|
|
2136
|
+
definitions = [];
|
|
2137
|
+
}
|
|
2138
|
+
if (!definitions.length) return next;
|
|
2139
|
+
const defById = new Map(definitions.map((def) => [String(def?.id || ""), def]));
|
|
2140
|
+
const instances = { ...(next.instances || {}) };
|
|
2141
|
+
let changed = false;
|
|
2142
|
+
for (const [nodeId, instance] of Object.entries(instances)) {
|
|
2143
|
+
if (!instance || typeof instance !== "object") continue;
|
|
2144
|
+
const def = defById.get(String(instance.definitionId || ""));
|
|
2145
|
+
if (!def) continue;
|
|
2146
|
+
const input = workspaceMergeSlotsWithDefinitionMeta(instance.input, def.inputs);
|
|
2147
|
+
const output = workspaceMergeSlotsWithDefinitionMeta(instance.output, def.outputs);
|
|
2148
|
+
if (JSON.stringify(input) !== JSON.stringify(instance.input || []) || JSON.stringify(output) !== JSON.stringify(instance.output || [])) {
|
|
2149
|
+
instances[nodeId] = { ...instance, input, output };
|
|
2150
|
+
changed = true;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
return changed ? { ...next, instances } : next;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
function hydrateWorkspaceGraphForRuntime(workspaceRoot, scoped = {}, graph = {}, userCtx = {}) {
|
|
2157
|
+
const withRefs = hydrateWorkspaceNodeRefsFromFiles(scoped.root || scoped.scopedRoot || workspaceRoot, graph);
|
|
2158
|
+
return hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped, withRefs, userCtx);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2109
2161
|
function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
|
|
2110
2162
|
const flowId = params.flowId != null ? String(params.flowId).trim() : "";
|
|
2111
2163
|
if (!flowId) return { root: path.resolve(workspaceRoot), flowId: "", flowSource: "", archived: false };
|
|
@@ -2290,22 +2342,47 @@ function isWorkspaceSemanticOutputSlot(slot) {
|
|
|
2290
2342
|
return type === "node" || name === "prev" || name === "next";
|
|
2291
2343
|
}
|
|
2292
2344
|
|
|
2293
|
-
function
|
|
2345
|
+
function workspaceLinkedOutputShouldStayPath(slot) {
|
|
2346
|
+
const rawName = String(slot?.name || "").trim();
|
|
2347
|
+
const name = rawName.toLowerCase();
|
|
2348
|
+
const type = String(slot?.type || "").trim().toLowerCase();
|
|
2349
|
+
if (["file", "image", "audio", "video", "binary", "directory", "dir"].includes(type)) return true;
|
|
2350
|
+
if (["file", "filepath", "file_path", "path", "url", "uri", "ref"].includes(name)) return true;
|
|
2351
|
+
return /(?:^|[_-])(file|path|url|uri|ref)$/i.test(rawName) || /(?:File|Path|Url|URL|Uri|URI|Ref)$/.test(rawName);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
function workspaceResolveLinkedOutputForTarget(value, targetSlot, scopedRoot = "") {
|
|
2355
|
+
const text = String(value ?? "");
|
|
2356
|
+
if (!text.trim() || workspaceLinkedOutputShouldStayPath(targetSlot)) return text;
|
|
2357
|
+
const outputRel = workspaceSafeNodeOutputRelPath(text);
|
|
2358
|
+
if (!outputRel || !String(scopedRoot || "").trim()) return text;
|
|
2359
|
+
try {
|
|
2360
|
+
const abs = workspaceResolveFlowFile(scopedRoot, outputRel, "linked output");
|
|
2361
|
+
const content = workspaceReadTextFileIfExists(abs, 120000);
|
|
2362
|
+
return content || text;
|
|
2363
|
+
} catch {
|
|
2364
|
+
return text;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
function workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot = "") {
|
|
2294
2369
|
const sourceId = String(edge?.source || "");
|
|
2295
2370
|
const slot = workspaceSourceSlotForEdge(graph, edge);
|
|
2296
2371
|
if (isWorkspaceSemanticOutputSlot(slot)) return "";
|
|
2372
|
+
const targetSlot = workspaceTargetSlotForEdge(graph, edge);
|
|
2373
|
+
const resolveValue = (value) => workspaceResolveLinkedOutputForTarget(value, targetSlot, scopedRoot);
|
|
2297
2374
|
const out = outputs.get(sourceId);
|
|
2298
2375
|
const sourceIndex = workspaceHandleIndex(edge?.sourceHandle, "output");
|
|
2299
2376
|
const slotName = String(slot?.name || "").trim();
|
|
2300
2377
|
const isPrimaryOutput = !slot || slotName === "result" || slotName === "content" || sourceIndex === 0;
|
|
2301
|
-
if (isPrimaryOutput && out != null && String(out).trim()) return
|
|
2378
|
+
if (isPrimaryOutput && out != null && String(out).trim()) return resolveValue(out);
|
|
2302
2379
|
if (slot && String(slot?.type || "") !== "node") {
|
|
2303
2380
|
const value = workspaceSlotValue(slot);
|
|
2304
|
-
if (value.trim()) return value;
|
|
2381
|
+
if (value.trim()) return resolveValue(value);
|
|
2305
2382
|
}
|
|
2306
|
-
if (out != null && String(out).trim()) return
|
|
2383
|
+
if (out != null && String(out).trim()) return resolveValue(out);
|
|
2307
2384
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
2308
|
-
return workspaceInstanceText(instances[sourceId]);
|
|
2385
|
+
return resolveValue(workspaceInstanceText(instances[sourceId]));
|
|
2309
2386
|
}
|
|
2310
2387
|
|
|
2311
2388
|
function workspaceParseJsonObjectFromText(text) {
|
|
@@ -2931,10 +3008,47 @@ function workspaceRelevantInputValues(body, inputValues = {}) {
|
|
|
2931
3008
|
return { values, placeholders };
|
|
2932
3009
|
}
|
|
2933
3010
|
|
|
3011
|
+
function workspaceDownstreamSlotKind(slot) {
|
|
3012
|
+
const name = String(slot?.name || "").trim().toLowerCase();
|
|
3013
|
+
const type = String(slot?.type || "").trim().toLowerCase();
|
|
3014
|
+
if (type === "markdown" || name === "markdown" || name.endsWith("markdown")) return "markdown";
|
|
3015
|
+
if (type === "html" || name === "html" || name.endsWith("html")) return "html";
|
|
3016
|
+
if (type === "mermaid" || name === "mermaid" || name.endsWith("mermaid")) return "mermaid";
|
|
3017
|
+
if (type === "ascii" || name === "ascii") return "ascii";
|
|
3018
|
+
if (type === "chart" || name === "chart" || name.endsWith("chart")) return "chart";
|
|
3019
|
+
if (type === "table" || name === "table" || name.endsWith("table")) return "table";
|
|
3020
|
+
return "";
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
function workspaceDownstreamOutputKindForField(graph, nodeId, field) {
|
|
3024
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
3025
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
3026
|
+
const source = instances[String(nodeId || "")] || {};
|
|
3027
|
+
const outputSlots = Array.isArray(source.output) ? source.output : [];
|
|
3028
|
+
for (const edge of edges) {
|
|
3029
|
+
if (String(edge?.source || "") !== String(nodeId)) continue;
|
|
3030
|
+
const sourceIndex = workspaceHandleIndex(edge?.sourceHandle, "output");
|
|
3031
|
+
const sourceSlot = outputSlots[sourceIndex] || null;
|
|
3032
|
+
if (workspaceOutputFieldForSlot(sourceSlot, sourceIndex) !== field) continue;
|
|
3033
|
+
const targetSlot = workspaceTargetSlotForEdge(graph, edge);
|
|
3034
|
+
if (!targetSlot || isWorkspaceSemanticInputSlot(targetSlot)) continue;
|
|
3035
|
+
const kind = workspaceDownstreamSlotKind(targetSlot);
|
|
3036
|
+
if (kind) return kind;
|
|
3037
|
+
}
|
|
3038
|
+
return "";
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
function workspaceDownstreamInputDescription(target, slot) {
|
|
3042
|
+
const description = String(slot?.description || "").trim();
|
|
3043
|
+
if (description) return description;
|
|
3044
|
+
return "";
|
|
3045
|
+
}
|
|
3046
|
+
|
|
2934
3047
|
function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
2935
3048
|
const instance = graph?.instances?.[nodeId] || {};
|
|
2936
3049
|
const outputSlots = Array.isArray(instance.output) ? instance.output : [];
|
|
2937
3050
|
const displayBindings = workspaceDownstreamOutputDisplayBindings(graph, nodeId);
|
|
3051
|
+
const downstreamInputRequirements = workspaceDownstreamInputRequirements(graph, nodeId);
|
|
2938
3052
|
const displayByField = new Map();
|
|
2939
3053
|
for (const binding of displayBindings) {
|
|
2940
3054
|
if (!displayByField.has(binding.field)) displayByField.set(binding.field, binding.kind);
|
|
@@ -2946,7 +3060,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
2946
3060
|
return name && type !== "node" && name !== "next" && name !== "result" && name !== "content";
|
|
2947
3061
|
})
|
|
2948
3062
|
.map((slot) => String(slot.name).trim());
|
|
2949
|
-
const resultKind = displayByField.get("result") || "";
|
|
3063
|
+
const resultKind = displayByField.get("result") || workspaceDownstreamOutputKindForField(graph, nodeId, "result") || "";
|
|
2950
3064
|
const resultExtByKind = {
|
|
2951
3065
|
html: "html",
|
|
2952
3066
|
markdown: "md",
|
|
@@ -2982,6 +3096,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
2982
3096
|
"## 输出",
|
|
2983
3097
|
"",
|
|
2984
3098
|
`请把${resultKindText}结果写入 \`${resultFile}\`。${resultGuidance}`,
|
|
3099
|
+
downstreamInputRequirements ? `\n${downstreamInputRequirements}` : "",
|
|
2985
3100
|
...(slots.length ? [`额外输出:${slots.map((name) => `\`${name}\``).join("、")}。短值可写在 \`outParams\`,文件值写成 \`outParams.<name>File\`。`] : []),
|
|
2986
3101
|
"最终只输出下面的 agentflow envelope,不要输出解释、进度或其它文字:",
|
|
2987
3102
|
"",
|
|
@@ -2989,6 +3104,42 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
2989
3104
|
].join("\n");
|
|
2990
3105
|
}
|
|
2991
3106
|
|
|
3107
|
+
function workspaceDownstreamInputRequirements(graph, nodeId) {
|
|
3108
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
3109
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
3110
|
+
const source = instances[String(nodeId || "")] || {};
|
|
3111
|
+
const outputSlots = Array.isArray(source.output) ? source.output : [];
|
|
3112
|
+
const rows = [];
|
|
3113
|
+
const seen = new Set();
|
|
3114
|
+
for (const edge of edges) {
|
|
3115
|
+
if (String(edge?.source || "") !== String(nodeId)) continue;
|
|
3116
|
+
const target = instances[String(edge?.target || "")];
|
|
3117
|
+
if (!target) continue;
|
|
3118
|
+
const targetSlot = workspaceTargetSlotForEdge(graph, edge);
|
|
3119
|
+
if (!targetSlot || isWorkspaceSemanticInputSlot(targetSlot)) continue;
|
|
3120
|
+
const targetName = String(targetSlot.name || "").trim();
|
|
3121
|
+
const targetType = String(targetSlot.type || "text").trim();
|
|
3122
|
+
const description = workspaceDownstreamInputDescription(target, targetSlot);
|
|
3123
|
+
if (!targetName && !description) continue;
|
|
3124
|
+
const sourceIndex = workspaceHandleIndex(edge?.sourceHandle, "output");
|
|
3125
|
+
const sourceSlot = outputSlots[sourceIndex] || null;
|
|
3126
|
+
const sourceField = workspaceOutputFieldForSlot(sourceSlot, sourceIndex);
|
|
3127
|
+
const targetLabel = String(target.label || target.definitionId || edge.target || "").trim();
|
|
3128
|
+
const key = `${sourceField}->${edge.target}:${targetName}:${description}`;
|
|
3129
|
+
if (seen.has(key)) continue;
|
|
3130
|
+
seen.add(key);
|
|
3131
|
+
rows.push([
|
|
3132
|
+
`- 输出 \`${sourceField}\` 会连接到下游 \`${targetLabel}\` 的输入 \`${targetName || "input"}\`(type=${targetType})。`,
|
|
3133
|
+
description ? ` 要求:${description}` : "",
|
|
3134
|
+
].filter(Boolean).join("\n"));
|
|
3135
|
+
}
|
|
3136
|
+
if (!rows.length) return "";
|
|
3137
|
+
return [
|
|
3138
|
+
"下游输入格式要求:",
|
|
3139
|
+
...rows,
|
|
3140
|
+
].join("\n");
|
|
3141
|
+
}
|
|
3142
|
+
|
|
2992
3143
|
function workspaceRunPlan(graph, runNodeId, scopedRoot = "") {
|
|
2993
3144
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
2994
3145
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
@@ -3121,14 +3272,14 @@ function workspaceCachedOutputValueExists(value, scopedRoot = "") {
|
|
|
3121
3272
|
return fs.existsSync(abs) && fs.statSync(abs).isFile();
|
|
3122
3273
|
}
|
|
3123
3274
|
|
|
3124
|
-
function workspaceUpstreamText(graph, nodeId, outputs) {
|
|
3275
|
+
function workspaceUpstreamText(graph, nodeId, outputs, scopedRoot = "") {
|
|
3125
3276
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
3126
3277
|
const incoming = edges
|
|
3127
3278
|
.filter((edge) => String(edge?.target || "") === String(nodeId))
|
|
3128
3279
|
.filter((edge) => !isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge)));
|
|
3129
3280
|
const contentEdge = incoming.find((edge) => String(edge?.targetHandle || "") === "input-1") || incoming[0];
|
|
3130
3281
|
if (!contentEdge) return "";
|
|
3131
|
-
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
|
|
3282
|
+
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge, scopedRoot);
|
|
3132
3283
|
}
|
|
3133
3284
|
|
|
3134
3285
|
function workspaceHandleIndex(handle, prefix) {
|
|
@@ -3181,7 +3332,7 @@ function workspaceNodeFileBoundaryBlock(runPackage = {}) {
|
|
|
3181
3332
|
].filter(Boolean).join("\n");
|
|
3182
3333
|
}
|
|
3183
3334
|
|
|
3184
|
-
function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames = null) {
|
|
3335
|
+
function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames = null, scopedRoot = "") {
|
|
3185
3336
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
3186
3337
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
3187
3338
|
const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
|
|
@@ -3194,10 +3345,10 @@ function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames =
|
|
|
3194
3345
|
}
|
|
3195
3346
|
const contentEdge = contentEdges.find((edge) => String(edge?.targetHandle || "") === "input-1") || contentEdges[0];
|
|
3196
3347
|
if (!contentEdge) return "";
|
|
3197
|
-
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
|
|
3348
|
+
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge, scopedRoot);
|
|
3198
3349
|
}
|
|
3199
3350
|
|
|
3200
|
-
function workspaceInputValues(graph, nodeId, outputs) {
|
|
3351
|
+
function workspaceInputValues(graph, nodeId, outputs, scopedRoot = "") {
|
|
3201
3352
|
const values = {};
|
|
3202
3353
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
3203
3354
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
@@ -3209,7 +3360,7 @@ function workspaceInputValues(graph, nodeId, outputs) {
|
|
|
3209
3360
|
const slot = inputSlots[index] || null;
|
|
3210
3361
|
const name = String(slot?.name || "").trim();
|
|
3211
3362
|
if (!name || isWorkspaceSemanticInputSlot(slot)) continue;
|
|
3212
|
-
const value = workspaceOutputSlotValueForEdge(graph, outputs, edge);
|
|
3363
|
+
const value = workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot);
|
|
3213
3364
|
if (String(value || "").trim()) values[name] = String(value);
|
|
3214
3365
|
}
|
|
3215
3366
|
for (const slot of inputSlots) {
|
|
@@ -3706,7 +3857,7 @@ function workspaceUnwrapOutputEnvelopeForDisplay(content) {
|
|
|
3706
3857
|
return structured.structured ? String(structured.result || "") : raw;
|
|
3707
3858
|
}
|
|
3708
3859
|
|
|
3709
|
-
function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null) {
|
|
3860
|
+
function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null, scopedRoot = "") {
|
|
3710
3861
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
3711
3862
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
3712
3863
|
const updated = [];
|
|
@@ -3716,7 +3867,7 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null)
|
|
|
3716
3867
|
const targetId = String(edge?.target || "");
|
|
3717
3868
|
const target = instances[targetId];
|
|
3718
3869
|
if (!target || !workspaceDisplayKind(target.definitionId)) continue;
|
|
3719
|
-
const value = outputs ? workspaceOutputSlotValueForEdge(graph, outputs, edge) : String(content || "");
|
|
3870
|
+
const value = outputs ? workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot) : String(content || "");
|
|
3720
3871
|
instances[targetId] = workspaceWriteDisplayContent(target, value || content);
|
|
3721
3872
|
updated.push(targetId);
|
|
3722
3873
|
}
|
|
@@ -4110,7 +4261,12 @@ async function workspaceRunToolNodejsScript({
|
|
|
4110
4261
|
}
|
|
4111
4262
|
|
|
4112
4263
|
async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
|
|
4113
|
-
const graph =
|
|
4264
|
+
const graph = hydrateWorkspaceGraphForRuntime(root, {
|
|
4265
|
+
root: scopedRoot,
|
|
4266
|
+
flowId: payload.flowId || "",
|
|
4267
|
+
flowSource: payload.flowSource || "user",
|
|
4268
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
4269
|
+
}, payload.graph || {}, userCtx);
|
|
4114
4270
|
const runNodeId = String(payload?.runNodeId || "").trim();
|
|
4115
4271
|
const { order, pauseNodeIds } = workspaceRunPlan(graph, runNodeId, scopedRoot);
|
|
4116
4272
|
const signal = opts.signal || null;
|
|
@@ -4199,7 +4355,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4199
4355
|
)),
|
|
4200
4356
|
};
|
|
4201
4357
|
outputs.set(nodeId, skillsBlock);
|
|
4202
|
-
workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock, outputs);
|
|
4358
|
+
workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock, outputs, scopedRoot);
|
|
4203
4359
|
emit({ type: "graph", nodeId, graph });
|
|
4204
4360
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4205
4361
|
continue;
|
|
@@ -4219,14 +4375,14 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4219
4375
|
)),
|
|
4220
4376
|
};
|
|
4221
4377
|
outputs.set(nodeId, mcpBlock);
|
|
4222
|
-
workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock, outputs);
|
|
4378
|
+
workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock, outputs, scopedRoot);
|
|
4223
4379
|
emit({ type: "graph", nodeId, graph });
|
|
4224
4380
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4225
4381
|
continue;
|
|
4226
4382
|
}
|
|
4227
4383
|
|
|
4228
4384
|
if (workspaceDisplayKind(defId)) {
|
|
4229
|
-
const content = workspaceUpstreamText(graph, nodeId, outputs);
|
|
4385
|
+
const content = workspaceUpstreamText(graph, nodeId, outputs, scopedRoot);
|
|
4230
4386
|
graph.instances[nodeId] = workspaceWriteDisplayContent(instance, content);
|
|
4231
4387
|
outputs.set(nodeId, content);
|
|
4232
4388
|
emit({ type: "graph", nodeId, graph });
|
|
@@ -4262,7 +4418,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4262
4418
|
}
|
|
4263
4419
|
|
|
4264
4420
|
if (defId === "control_cd_workspace") {
|
|
4265
|
-
const inputText = workspaceUpstreamText(graph, nodeId, outputs);
|
|
4421
|
+
const inputText = workspaceUpstreamText(graph, nodeId, outputs, scopedRoot);
|
|
4266
4422
|
const inputSlots = Array.isArray(instance.input) ? instance.input : [];
|
|
4267
4423
|
const pathSlot = inputSlots.find((slot) => String(slot?.name || "") === "path") ||
|
|
4268
4424
|
inputSlots.find((slot) => String(slot?.name || "") === "target");
|
|
@@ -4473,9 +4629,36 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4473
4629
|
continue;
|
|
4474
4630
|
}
|
|
4475
4631
|
|
|
4632
|
+
if (defId === "tool_wecom_send_group_markdown" || defId === "tool_wecom_send_app_markdown") {
|
|
4633
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
4634
|
+
const markdown = String(inputValues.markdown || inputValues.content || workspaceSlotValue(workspaceSlotByName(instance, "markdown")) || workspaceUpstreamText(graph, nodeId, outputs, scopedRoot) || "");
|
|
4635
|
+
const result = defId === "tool_wecom_send_app_markdown"
|
|
4636
|
+
? await sendWecomAppMarkdown({
|
|
4637
|
+
markdown,
|
|
4638
|
+
toUser: inputValues.toUser || workspaceSlotValue(workspaceSlotByName(instance, "toUser")),
|
|
4639
|
+
corpId: inputValues.corpId || workspaceSlotValue(workspaceSlotByName(instance, "corpId")),
|
|
4640
|
+
corpSecret: inputValues.corpSecret || workspaceSlotValue(workspaceSlotByName(instance, "corpSecret")),
|
|
4641
|
+
agentId: inputValues.agentId || workspaceSlotValue(workspaceSlotByName(instance, "agentId")),
|
|
4642
|
+
accessToken: inputValues.accessToken || workspaceSlotValue(workspaceSlotByName(instance, "accessToken")),
|
|
4643
|
+
}, runtimeEnvForUser(userCtx))
|
|
4644
|
+
: await sendWecomGroupMarkdown({
|
|
4645
|
+
markdown,
|
|
4646
|
+
webhookUrl: inputValues.webhookUrl || workspaceSlotValue(workspaceSlotByName(instance, "webhookUrl")),
|
|
4647
|
+
webhookKey: inputValues.webhookKey || workspaceSlotValue(workspaceSlotByName(instance, "webhookKey")),
|
|
4648
|
+
}, runtimeEnvForUser(userCtx));
|
|
4649
|
+
let nextInstance = workspaceSetOutputSlot(instance, "sent", "true");
|
|
4650
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "message", result.message);
|
|
4651
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "response", JSON.stringify(result.response || {}));
|
|
4652
|
+
graph.instances[nodeId] = nextInstance;
|
|
4653
|
+
outputs.set(nodeId, result.message);
|
|
4654
|
+
emit({ type: "graph", nodeId, graph });
|
|
4655
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4656
|
+
continue;
|
|
4657
|
+
}
|
|
4658
|
+
|
|
4476
4659
|
if (defId === "tool_nodejs") {
|
|
4477
4660
|
const prepareStartedAt = Date.now();
|
|
4478
|
-
const inputValues = workspaceInputValues(graph, nodeId, outputs);
|
|
4661
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
4479
4662
|
const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
|
|
4480
4663
|
scopedRoot,
|
|
4481
4664
|
cwd,
|
|
@@ -4512,16 +4695,16 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4512
4695
|
onActiveChild: opts.onActiveChild,
|
|
4513
4696
|
});
|
|
4514
4697
|
if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
|
|
4515
|
-
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
|
|
4698
|
+
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs, scopedRoot);
|
|
4516
4699
|
if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
4517
4700
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4518
4701
|
continue;
|
|
4519
4702
|
}
|
|
4520
4703
|
|
|
4521
4704
|
const prepareStartedAt = Date.now();
|
|
4522
|
-
const inputValues = workspaceInputValues(graph, nodeId, outputs);
|
|
4705
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
4523
4706
|
const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
|
|
4524
|
-
const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders);
|
|
4707
|
+
const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders, scopedRoot);
|
|
4525
4708
|
const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
|
|
4526
4709
|
const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks);
|
|
4527
4710
|
const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
|
|
@@ -4642,7 +4825,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4642
4825
|
onActiveChild: opts.onActiveChild,
|
|
4643
4826
|
});
|
|
4644
4827
|
if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
|
|
4645
|
-
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
|
|
4828
|
+
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs, scopedRoot);
|
|
4646
4829
|
if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
4647
4830
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4648
4831
|
}
|
|
@@ -4839,11 +5022,267 @@ function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userI
|
|
|
4839
5022
|
const activeFlowRuns = new Map();
|
|
4840
5023
|
/** 正在执行的 Workspace 临时 run(flowId → { controller, child, runNodeId, startedAt });同一 flow 只允许一个 run */
|
|
4841
5024
|
const activeWorkspaceRuns = new Map();
|
|
5025
|
+
const WORKSPACE_SCHEDULES_FILENAME = "workspace-schedules.json";
|
|
5026
|
+
const WORKSPACE_SCHEDULE_POLL_MS = 30_000;
|
|
4842
5027
|
|
|
4843
5028
|
function workspaceRunKey(userCtx, flowSource, flowId) {
|
|
4844
5029
|
return `${userCtx?.userId || ""}:${flowSource || "user"}:${flowId}`;
|
|
4845
5030
|
}
|
|
4846
5031
|
|
|
5032
|
+
function normalizeWorkspaceScheduledRunConfig(raw) {
|
|
5033
|
+
let parsed = {};
|
|
5034
|
+
const text = String(raw || "").trim();
|
|
5035
|
+
if (text) {
|
|
5036
|
+
try {
|
|
5037
|
+
parsed = JSON.parse(text);
|
|
5038
|
+
} catch {
|
|
5039
|
+
parsed = {};
|
|
5040
|
+
}
|
|
5041
|
+
}
|
|
5042
|
+
const intervalMinutes = Number(parsed.intervalMinutes);
|
|
5043
|
+
return {
|
|
5044
|
+
enabled: parsed.enabled === true,
|
|
5045
|
+
intervalMinutes: Number.isFinite(intervalMinutes) && intervalMinutes > 0
|
|
5046
|
+
? Math.min(Math.max(Math.round(intervalMinutes), 1), 1440)
|
|
5047
|
+
: 60,
|
|
5048
|
+
};
|
|
5049
|
+
}
|
|
5050
|
+
|
|
5051
|
+
function workspaceSchedulesPath() {
|
|
5052
|
+
return path.join(getAgentflowDataRoot(), WORKSPACE_SCHEDULES_FILENAME);
|
|
5053
|
+
}
|
|
5054
|
+
|
|
5055
|
+
function readWorkspaceScheduleRegistry() {
|
|
5056
|
+
const filePath = workspaceSchedulesPath();
|
|
5057
|
+
if (!fs.existsSync(filePath)) return { version: 1, schedules: {} };
|
|
5058
|
+
try {
|
|
5059
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
5060
|
+
return {
|
|
5061
|
+
version: 1,
|
|
5062
|
+
schedules: parsed?.schedules && typeof parsed.schedules === "object" && !Array.isArray(parsed.schedules)
|
|
5063
|
+
? parsed.schedules
|
|
5064
|
+
: {},
|
|
5065
|
+
};
|
|
5066
|
+
} catch {
|
|
5067
|
+
return { version: 1, schedules: {} };
|
|
5068
|
+
}
|
|
5069
|
+
}
|
|
5070
|
+
|
|
5071
|
+
function writeWorkspaceScheduleRegistry(registry) {
|
|
5072
|
+
const filePath = workspaceSchedulesPath();
|
|
5073
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
5074
|
+
fs.writeFileSync(filePath, JSON.stringify({
|
|
5075
|
+
version: 1,
|
|
5076
|
+
updatedAt: new Date().toISOString(),
|
|
5077
|
+
schedules: registry?.schedules && typeof registry.schedules === "object" ? registry.schedules : {},
|
|
5078
|
+
}, null, 2) + "\n", "utf-8");
|
|
5079
|
+
}
|
|
5080
|
+
|
|
5081
|
+
function workspaceScheduleKey(userId, flowSource, flowId, runNodeId) {
|
|
5082
|
+
return [
|
|
5083
|
+
String(userId || ""),
|
|
5084
|
+
String(flowSource || "user"),
|
|
5085
|
+
String(flowId || ""),
|
|
5086
|
+
String(runNodeId || ""),
|
|
5087
|
+
].join(":");
|
|
5088
|
+
}
|
|
5089
|
+
|
|
5090
|
+
function listWorkspaceScheduleStatusesForFlow(userCtx = {}, flowSource = "user", flowId = "") {
|
|
5091
|
+
const registry = readWorkspaceScheduleRegistry();
|
|
5092
|
+
const userId = String(userCtx.userId || "");
|
|
5093
|
+
return Object.values(registry.schedules || {})
|
|
5094
|
+
.filter((entry) => (
|
|
5095
|
+
String(entry?.userId || "") === userId &&
|
|
5096
|
+
String(entry?.flowSource || "user") === String(flowSource || "user") &&
|
|
5097
|
+
String(entry?.flowId || "") === String(flowId || "")
|
|
5098
|
+
))
|
|
5099
|
+
.sort((a, b) => String(a.runNodeId || "").localeCompare(String(b.runNodeId || "")));
|
|
5100
|
+
}
|
|
5101
|
+
|
|
5102
|
+
function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx = {}) {
|
|
5103
|
+
const flowId = String(scoped?.flowId || "").trim();
|
|
5104
|
+
const flowSource = String(scoped?.flowSource || "user");
|
|
5105
|
+
const userId = String(userCtx.userId || authUser?.userId || "");
|
|
5106
|
+
if (!flowId || !userId) return [];
|
|
5107
|
+
const now = Date.now();
|
|
5108
|
+
const nowIso = new Date(now).toISOString();
|
|
5109
|
+
const registry = readWorkspaceScheduleRegistry();
|
|
5110
|
+
const schedules = { ...(registry.schedules || {}) };
|
|
5111
|
+
const prefix = `${userId}:${flowSource}:${flowId}:`;
|
|
5112
|
+
for (const key of Object.keys(schedules)) {
|
|
5113
|
+
if (key.startsWith(prefix)) delete schedules[key];
|
|
5114
|
+
}
|
|
5115
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
5116
|
+
for (const [runNodeId, instance] of Object.entries(instances)) {
|
|
5117
|
+
if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
|
|
5118
|
+
const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
|
|
5119
|
+
if (!config.enabled) continue;
|
|
5120
|
+
const key = workspaceScheduleKey(userId, flowSource, flowId, runNodeId);
|
|
5121
|
+
const intervalMs = config.intervalMinutes * 60 * 1000;
|
|
5122
|
+
const previous = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
|
|
5123
|
+
const previousNext = Number(previous.nextRunAt || 0);
|
|
5124
|
+
schedules[key] = {
|
|
5125
|
+
...previous,
|
|
5126
|
+
key,
|
|
5127
|
+
enabled: true,
|
|
5128
|
+
userId,
|
|
5129
|
+
username: String(authUser?.username || previous.username || userId),
|
|
5130
|
+
flowId,
|
|
5131
|
+
flowSource,
|
|
5132
|
+
runNodeId,
|
|
5133
|
+
label: String(instance.label || "Scheduled Run"),
|
|
5134
|
+
intervalMinutes: config.intervalMinutes,
|
|
5135
|
+
nextRunAt: Number.isFinite(previousNext) && previousNext > now ? previousNext : now + intervalMs,
|
|
5136
|
+
lastStatus: previous.lastStatus || "armed",
|
|
5137
|
+
updatedAt: nowIso,
|
|
5138
|
+
};
|
|
5139
|
+
}
|
|
5140
|
+
const nextRegistry = { version: 1, schedules };
|
|
5141
|
+
writeWorkspaceScheduleRegistry(nextRegistry);
|
|
5142
|
+
return listWorkspaceScheduleStatusesForFlow(userCtx, flowSource, flowId);
|
|
5143
|
+
}
|
|
5144
|
+
|
|
5145
|
+
function updateWorkspaceScheduleEntry(key, patch) {
|
|
5146
|
+
const registry = readWorkspaceScheduleRegistry();
|
|
5147
|
+
const current = registry.schedules?.[key];
|
|
5148
|
+
if (!current) return null;
|
|
5149
|
+
const next = {
|
|
5150
|
+
...current,
|
|
5151
|
+
...(patch && typeof patch === "object" ? patch : {}),
|
|
5152
|
+
updatedAt: new Date().toISOString(),
|
|
5153
|
+
};
|
|
5154
|
+
registry.schedules[key] = next;
|
|
5155
|
+
writeWorkspaceScheduleRegistry(registry);
|
|
5156
|
+
return next;
|
|
5157
|
+
}
|
|
5158
|
+
|
|
5159
|
+
async function runWorkspaceScheduledEntry(root, entry) {
|
|
5160
|
+
const userCtx = { userId: String(entry.userId || "") };
|
|
5161
|
+
const runKey = workspaceRunKey(userCtx, entry.flowSource || "user", entry.flowId || "");
|
|
5162
|
+
const intervalMs = Math.max(1, Number(entry.intervalMinutes || 60)) * 60 * 1000;
|
|
5163
|
+
const nextRunAt = Date.now() + intervalMs;
|
|
5164
|
+
if (activeWorkspaceRuns.has(runKey)) {
|
|
5165
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5166
|
+
nextRunAt,
|
|
5167
|
+
lastSkippedAt: Date.now(),
|
|
5168
|
+
lastStatus: "skipped: busy",
|
|
5169
|
+
});
|
|
5170
|
+
return;
|
|
5171
|
+
}
|
|
5172
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
5173
|
+
flowId: entry.flowId || "",
|
|
5174
|
+
flowSource: entry.flowSource || "user",
|
|
5175
|
+
}, userCtx);
|
|
5176
|
+
if (scoped.error || scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
5177
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5178
|
+
nextRunAt,
|
|
5179
|
+
lastStatus: "error",
|
|
5180
|
+
lastError: scoped.error || "Workspace schedule target is not writable",
|
|
5181
|
+
lastErrorAt: Date.now(),
|
|
5182
|
+
});
|
|
5183
|
+
return;
|
|
5184
|
+
}
|
|
5185
|
+
const graphPath = workspaceGraphPath(scoped.root);
|
|
5186
|
+
const graph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root).graph, userCtx);
|
|
5187
|
+
const instance = graph.instances?.[entry.runNodeId];
|
|
5188
|
+
const config = normalizeWorkspaceScheduledRunConfig(instance?.body || "");
|
|
5189
|
+
if (!instance || String(instance.definitionId || "") !== "workspace_scheduled_run" || !config.enabled) {
|
|
5190
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5191
|
+
enabled: false,
|
|
5192
|
+
nextRunAt: null,
|
|
5193
|
+
lastStatus: "disabled",
|
|
5194
|
+
});
|
|
5195
|
+
return;
|
|
5196
|
+
}
|
|
5197
|
+
|
|
5198
|
+
const controller = new AbortController();
|
|
5199
|
+
const authUsers = readAuthUsers();
|
|
5200
|
+
const authUser = authUsers[userCtx.userId] || {};
|
|
5201
|
+
const runEntry = {
|
|
5202
|
+
controller,
|
|
5203
|
+
child: null,
|
|
5204
|
+
runId: runLedgerId("workspace"),
|
|
5205
|
+
userId: userCtx.userId,
|
|
5206
|
+
username: String(authUser.username || entry.username || userCtx.userId),
|
|
5207
|
+
runNodeId: String(entry.runNodeId || ""),
|
|
5208
|
+
flowId: String(entry.flowId || ""),
|
|
5209
|
+
flowSource: String(entry.flowSource || "user"),
|
|
5210
|
+
startedAt: Date.now(),
|
|
5211
|
+
scheduled: true,
|
|
5212
|
+
stopChild() {
|
|
5213
|
+
if (this.child && !this.child.killed) {
|
|
5214
|
+
try { this.child.kill("SIGTERM"); } catch (_) {}
|
|
5215
|
+
}
|
|
5216
|
+
},
|
|
5217
|
+
};
|
|
5218
|
+
activeWorkspaceRuns.set(runKey, runEntry);
|
|
5219
|
+
appendWorkspaceRunStarted(runEntry);
|
|
5220
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5221
|
+
lastStatus: "running",
|
|
5222
|
+
lastTriggeredAt: runEntry.startedAt,
|
|
5223
|
+
lastRunId: runEntry.runId,
|
|
5224
|
+
lastError: "",
|
|
5225
|
+
});
|
|
5226
|
+
const setActiveChild = (child) => {
|
|
5227
|
+
runEntry.child = child || null;
|
|
5228
|
+
if (controller.signal.aborted) runEntry.stopChild();
|
|
5229
|
+
};
|
|
5230
|
+
try {
|
|
5231
|
+
const result = await runWorkspaceGraph(root, scoped.root, {
|
|
5232
|
+
flowId: entry.flowId,
|
|
5233
|
+
flowSource: entry.flowSource || "user",
|
|
5234
|
+
runNodeId: entry.runNodeId,
|
|
5235
|
+
graph,
|
|
5236
|
+
}, userCtx, {
|
|
5237
|
+
signal: controller.signal,
|
|
5238
|
+
onActiveChild: setActiveChild,
|
|
5239
|
+
});
|
|
5240
|
+
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
5241
|
+
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
5242
|
+
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
5243
|
+
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
5244
|
+
const endedAt = Date.now();
|
|
5245
|
+
appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "success");
|
|
5246
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5247
|
+
nextRunAt: Date.now() + intervalMs,
|
|
5248
|
+
lastFinishedAt: endedAt,
|
|
5249
|
+
lastStatus: "success",
|
|
5250
|
+
lastError: "",
|
|
5251
|
+
});
|
|
5252
|
+
} catch (e) {
|
|
5253
|
+
const endedAt = Date.now();
|
|
5254
|
+
appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "failed");
|
|
5255
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5256
|
+
nextRunAt: Date.now() + intervalMs,
|
|
5257
|
+
lastFinishedAt: endedAt,
|
|
5258
|
+
lastStatus: "failed",
|
|
5259
|
+
lastError: (e && e.message) || String(e),
|
|
5260
|
+
lastErrorAt: endedAt,
|
|
5261
|
+
});
|
|
5262
|
+
log.info(`[workspace-scheduler] failed ${entry.flowId}/${entry.runNodeId}: ${(e && e.message) || String(e)}`);
|
|
5263
|
+
} finally {
|
|
5264
|
+
if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
|
|
5268
|
+
function pollWorkspaceSchedules(root) {
|
|
5269
|
+
const now = Date.now();
|
|
5270
|
+
const registry = readWorkspaceScheduleRegistry();
|
|
5271
|
+
for (const entry of Object.values(registry.schedules || {})) {
|
|
5272
|
+
if (!entry || entry.enabled !== true) continue;
|
|
5273
|
+
const nextRunAt = Number(entry.nextRunAt || 0);
|
|
5274
|
+
if (!Number.isFinite(nextRunAt) || nextRunAt <= 0) {
|
|
5275
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5276
|
+
nextRunAt: now + Math.max(1, Number(entry.intervalMinutes || 60)) * 60 * 1000,
|
|
5277
|
+
lastStatus: entry.lastStatus || "armed",
|
|
5278
|
+
});
|
|
5279
|
+
continue;
|
|
5280
|
+
}
|
|
5281
|
+
if (nextRunAt > now) continue;
|
|
5282
|
+
void runWorkspaceScheduledEntry(root, entry);
|
|
5283
|
+
}
|
|
5284
|
+
}
|
|
5285
|
+
|
|
4847
5286
|
/** Cursor/OpenCode 执行目录统一使用当前 UI 启动 workspace。 */
|
|
4848
5287
|
function composerCliWorkspaceForFlowDir(workspaceRoot, _flowDir) {
|
|
4849
5288
|
return path.resolve(workspaceRoot);
|
|
@@ -5498,7 +5937,7 @@ export function startUiServer({
|
|
|
5498
5937
|
return;
|
|
5499
5938
|
}
|
|
5500
5939
|
const { path: graphPath, graph } = readWorkspaceGraph(scoped.root);
|
|
5501
|
-
const hydratedGraph =
|
|
5940
|
+
const hydratedGraph = hydrateWorkspaceGraphForRuntime(root, scoped, graph, userCtx);
|
|
5502
5941
|
json(res, 200, {
|
|
5503
5942
|
ok: true,
|
|
5504
5943
|
graph: hydratedGraph,
|
|
@@ -5508,6 +5947,7 @@ export function startUiServer({
|
|
|
5508
5947
|
flowSource: scoped.flowSource,
|
|
5509
5948
|
archived: scoped.archived,
|
|
5510
5949
|
writable: !(scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)),
|
|
5950
|
+
workspaceSchedules: listWorkspaceScheduleStatusesForFlow(userCtx, scoped.flowSource || "user", scoped.flowId || ""),
|
|
5511
5951
|
});
|
|
5512
5952
|
} catch (e) {
|
|
5513
5953
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
@@ -5588,12 +6028,30 @@ export function startUiServer({
|
|
|
5588
6028
|
json(res, 400, { error: "Cannot write workspace graph for builtin or archived pipeline" });
|
|
5589
6029
|
return;
|
|
5590
6030
|
}
|
|
5591
|
-
const submittedGraph =
|
|
6031
|
+
const submittedGraph = hydrateWorkspaceGraphForRuntime(root, scoped, payload.graph || payload, userCtx);
|
|
5592
6032
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
5593
|
-
const currentGraph =
|
|
6033
|
+
const currentGraph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root).graph, userCtx);
|
|
5594
6034
|
const graph = mergeWorkspacePersistentNodeRefs(submittedGraph, currentGraph);
|
|
5595
6035
|
fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2) + "\n", "utf-8");
|
|
5596
|
-
|
|
6036
|
+
const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
|
|
6037
|
+
json(res, 200, { ok: true, path: graphPath, graph, workspaceSchedules });
|
|
6038
|
+
} catch (e) {
|
|
6039
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
6040
|
+
}
|
|
6041
|
+
return;
|
|
6042
|
+
}
|
|
6043
|
+
|
|
6044
|
+
if (req.method === "GET" && url.pathname === "/api/workspace/schedules") {
|
|
6045
|
+
try {
|
|
6046
|
+
const flowId = url.searchParams.get("flowId") || "";
|
|
6047
|
+
const flowSource = url.searchParams.get("flowSource") || "user";
|
|
6048
|
+
if (!flowId) {
|
|
6049
|
+
json(res, 400, { error: "Missing flowId" });
|
|
6050
|
+
return;
|
|
6051
|
+
}
|
|
6052
|
+
json(res, 200, {
|
|
6053
|
+
schedules: listWorkspaceScheduleStatusesForFlow(userCtx, flowSource, flowId),
|
|
6054
|
+
});
|
|
5597
6055
|
} catch (e) {
|
|
5598
6056
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
5599
6057
|
}
|
|
@@ -8154,6 +8612,25 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
8154
8612
|
res.end(data);
|
|
8155
8613
|
});
|
|
8156
8614
|
|
|
8615
|
+
const workspaceScheduleTimer = setInterval(() => {
|
|
8616
|
+
try {
|
|
8617
|
+
pollWorkspaceSchedules(root);
|
|
8618
|
+
} catch (e) {
|
|
8619
|
+
log.debug(`[workspace-scheduler] poll failed: ${(e && e.message) || String(e)}`);
|
|
8620
|
+
}
|
|
8621
|
+
}, WORKSPACE_SCHEDULE_POLL_MS);
|
|
8622
|
+
try {
|
|
8623
|
+
workspaceScheduleTimer.unref?.();
|
|
8624
|
+
} catch (_) {}
|
|
8625
|
+
server.on("close", () => clearInterval(workspaceScheduleTimer));
|
|
8626
|
+
setTimeout(() => {
|
|
8627
|
+
try {
|
|
8628
|
+
pollWorkspaceSchedules(root);
|
|
8629
|
+
} catch (e) {
|
|
8630
|
+
log.debug(`[workspace-scheduler] initial poll failed: ${(e && e.message) || String(e)}`);
|
|
8631
|
+
}
|
|
8632
|
+
}, 1000).unref?.();
|
|
8633
|
+
|
|
8157
8634
|
return new Promise((resolve, reject) => {
|
|
8158
8635
|
server.once("error", reject);
|
|
8159
8636
|
server.listen(port, host, () => {
|