@fieldwangai/agentflow 0.1.70 → 0.1.72
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/admin-storage-config.mjs +90 -5
- package/bin/lib/catalog-flows.mjs +1 -0
- package/bin/lib/composer-skill-router.mjs +9 -1
- 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 +30 -0
- package/bin/lib/skill-registry.mjs +20 -7
- package/bin/lib/ui-server.mjs +549 -43
- 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-NH_rVAzF.css +1 -0
- package/builtin/web-ui/dist/assets/index-t_zkIiXD.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/builtin/web-ui/dist/assets/index-ByGcCRCN.css +0 -1
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
PACKAGE_ROOT,
|
|
46
46
|
ARCHIVED_PIPELINES_DIR_NAME,
|
|
47
47
|
getAgentflowDataRoot,
|
|
48
|
+
getAgentflowSkillsRoot,
|
|
48
49
|
getAgentflowUserConfigAbs,
|
|
49
50
|
getAgentflowUserDataRoot,
|
|
50
51
|
getUserPipelinesRoot,
|
|
@@ -62,6 +63,7 @@ import {
|
|
|
62
63
|
buildSkillInjectionBlock,
|
|
63
64
|
buildSkillCompactInjectionBlock,
|
|
64
65
|
} from "./composer-skill-router.mjs";
|
|
66
|
+
import { clearSkillRegistryCache } from "./skill-registry.mjs";
|
|
65
67
|
import { COMPOSER_NODE_SPEC_FILENAME } from "./composer-planner.mjs";
|
|
66
68
|
import { listRecentRunsFromDisk } from "./recent-runs.mjs";
|
|
67
69
|
import {
|
|
@@ -92,6 +94,7 @@ import {
|
|
|
92
94
|
} from "./marketplace.mjs";
|
|
93
95
|
import { buildGitContext, inferGitRepoRootFromWorktree, loadGitWorktree, normalizeGitContext, runGit, sanitizeWorktreeName, unloadGitWorktree } from "./git-worktree.mjs";
|
|
94
96
|
import { createGitLabMergeRequest } from "./gitlab-mr.mjs";
|
|
97
|
+
import { sendWecomAppMarkdown, sendWecomGroupMarkdown } from "./wecom.mjs";
|
|
95
98
|
import {
|
|
96
99
|
authSetupRequired,
|
|
97
100
|
buildClearSessionCookie,
|
|
@@ -1133,6 +1136,22 @@ function normalizeSkillhubListPayload(raw) {
|
|
|
1133
1136
|
}).filter((x) => x.name);
|
|
1134
1137
|
}
|
|
1135
1138
|
|
|
1139
|
+
function skillhubManagedSkillsDir() {
|
|
1140
|
+
const dir = getAgentflowSkillsRoot();
|
|
1141
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1142
|
+
return dir;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function skillhubListArgs(target, agent) {
|
|
1146
|
+
const normalizedTarget = String(target || "agentflow").trim();
|
|
1147
|
+
const normalizedAgent = String(agent || "codex").trim();
|
|
1148
|
+
const args = ["list", "--json"];
|
|
1149
|
+
if (normalizedTarget === "all") args.push("--all");
|
|
1150
|
+
else if (normalizedTarget === "legacy-global") args.push("--global", "--agent", normalizedAgent);
|
|
1151
|
+
else args.push("--dir", skillhubManagedSkillsDir());
|
|
1152
|
+
return args;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1136
1155
|
function skillhubInstallArgs(payload, { uninstall = false } = {}) {
|
|
1137
1156
|
const slug = String(payload?.slug || payload?.name || "").trim();
|
|
1138
1157
|
if (!slug && !payload?.collection) return null;
|
|
@@ -1143,10 +1162,12 @@ function skillhubInstallArgs(payload, { uninstall = false } = {}) {
|
|
|
1143
1162
|
args.push(slug);
|
|
1144
1163
|
}
|
|
1145
1164
|
if (payload?.skillId) args.push("--skill-id", String(payload.skillId).trim());
|
|
1146
|
-
const target = String(payload?.target || "
|
|
1165
|
+
const target = String(payload?.target || "agentflow").trim();
|
|
1147
1166
|
const agent = String(payload?.agent || "codex").trim();
|
|
1148
|
-
if (target === "global") {
|
|
1167
|
+
if (target === "global" || target === "legacy-global") {
|
|
1149
1168
|
args.push("--global", "--agent", agent);
|
|
1169
|
+
} else if (target === "agentflow") {
|
|
1170
|
+
args.push("--dir", skillhubManagedSkillsDir());
|
|
1150
1171
|
} else if (payload?.dir) {
|
|
1151
1172
|
args.push("--dir", String(payload.dir).trim());
|
|
1152
1173
|
}
|
|
@@ -2106,6 +2127,57 @@ function hydrateWorkspaceNodeRefsFromFiles(scopedRoot, graph) {
|
|
|
2106
2127
|
return changed ? { ...next, instances } : next;
|
|
2107
2128
|
}
|
|
2108
2129
|
|
|
2130
|
+
function workspaceMergeSlotsWithDefinitionMeta(slots, definitionSlots) {
|
|
2131
|
+
const current = Array.isArray(slots) ? slots : [];
|
|
2132
|
+
const defs = Array.isArray(definitionSlots) ? definitionSlots : [];
|
|
2133
|
+
const byName = new Map(defs.map((slot) => [String(slot?.name || ""), slot]));
|
|
2134
|
+
return current.map((slot, index) => {
|
|
2135
|
+
const def = byName.get(String(slot?.name || "")) || defs[index] || null;
|
|
2136
|
+
if (!def) return slot;
|
|
2137
|
+
const next = { ...slot };
|
|
2138
|
+
for (const key of ["type", "name", "description", "required", "showOnNode"]) {
|
|
2139
|
+
if (next[key] == null || String(next[key]).trim?.() === "") {
|
|
2140
|
+
if (def[key] != null) next[key] = def[key];
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
return next;
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
function hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped = {}, graph = {}, userCtx = {}) {
|
|
2148
|
+
const next = normalizeWorkspaceGraphPayload(graph || {});
|
|
2149
|
+
let definitions = [];
|
|
2150
|
+
try {
|
|
2151
|
+
definitions = listNodesJson(workspaceRoot, scoped.flowId || "", scoped.flowSource || "user", {
|
|
2152
|
+
archived: scoped.archived === true,
|
|
2153
|
+
userId: userCtx?.userId || "",
|
|
2154
|
+
}).nodes || [];
|
|
2155
|
+
} catch {
|
|
2156
|
+
definitions = [];
|
|
2157
|
+
}
|
|
2158
|
+
if (!definitions.length) return next;
|
|
2159
|
+
const defById = new Map(definitions.map((def) => [String(def?.id || ""), def]));
|
|
2160
|
+
const instances = { ...(next.instances || {}) };
|
|
2161
|
+
let changed = false;
|
|
2162
|
+
for (const [nodeId, instance] of Object.entries(instances)) {
|
|
2163
|
+
if (!instance || typeof instance !== "object") continue;
|
|
2164
|
+
const def = defById.get(String(instance.definitionId || ""));
|
|
2165
|
+
if (!def) continue;
|
|
2166
|
+
const input = workspaceMergeSlotsWithDefinitionMeta(instance.input, def.inputs);
|
|
2167
|
+
const output = workspaceMergeSlotsWithDefinitionMeta(instance.output, def.outputs);
|
|
2168
|
+
if (JSON.stringify(input) !== JSON.stringify(instance.input || []) || JSON.stringify(output) !== JSON.stringify(instance.output || [])) {
|
|
2169
|
+
instances[nodeId] = { ...instance, input, output };
|
|
2170
|
+
changed = true;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
return changed ? { ...next, instances } : next;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
function hydrateWorkspaceGraphForRuntime(workspaceRoot, scoped = {}, graph = {}, userCtx = {}) {
|
|
2177
|
+
const withRefs = hydrateWorkspaceNodeRefsFromFiles(scoped.root || scoped.scopedRoot || workspaceRoot, graph);
|
|
2178
|
+
return hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped, withRefs, userCtx);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2109
2181
|
function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
|
|
2110
2182
|
const flowId = params.flowId != null ? String(params.flowId).trim() : "";
|
|
2111
2183
|
if (!flowId) return { root: path.resolve(workspaceRoot), flowId: "", flowSource: "", archived: false };
|
|
@@ -2290,22 +2362,47 @@ function isWorkspaceSemanticOutputSlot(slot) {
|
|
|
2290
2362
|
return type === "node" || name === "prev" || name === "next";
|
|
2291
2363
|
}
|
|
2292
2364
|
|
|
2293
|
-
function
|
|
2365
|
+
function workspaceLinkedOutputShouldStayPath(slot) {
|
|
2366
|
+
const rawName = String(slot?.name || "").trim();
|
|
2367
|
+
const name = rawName.toLowerCase();
|
|
2368
|
+
const type = String(slot?.type || "").trim().toLowerCase();
|
|
2369
|
+
if (["file", "image", "audio", "video", "binary", "directory", "dir"].includes(type)) return true;
|
|
2370
|
+
if (["file", "filepath", "file_path", "path", "url", "uri", "ref"].includes(name)) return true;
|
|
2371
|
+
return /(?:^|[_-])(file|path|url|uri|ref)$/i.test(rawName) || /(?:File|Path|Url|URL|Uri|URI|Ref)$/.test(rawName);
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
function workspaceResolveLinkedOutputForTarget(value, targetSlot, scopedRoot = "") {
|
|
2375
|
+
const text = String(value ?? "");
|
|
2376
|
+
if (!text.trim() || workspaceLinkedOutputShouldStayPath(targetSlot)) return text;
|
|
2377
|
+
const outputRel = workspaceSafeNodeOutputRelPath(text);
|
|
2378
|
+
if (!outputRel || !String(scopedRoot || "").trim()) return text;
|
|
2379
|
+
try {
|
|
2380
|
+
const abs = workspaceResolveFlowFile(scopedRoot, outputRel, "linked output");
|
|
2381
|
+
const content = workspaceReadTextFileIfExists(abs, 120000);
|
|
2382
|
+
return content || text;
|
|
2383
|
+
} catch {
|
|
2384
|
+
return text;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot = "") {
|
|
2294
2389
|
const sourceId = String(edge?.source || "");
|
|
2295
2390
|
const slot = workspaceSourceSlotForEdge(graph, edge);
|
|
2296
2391
|
if (isWorkspaceSemanticOutputSlot(slot)) return "";
|
|
2392
|
+
const targetSlot = workspaceTargetSlotForEdge(graph, edge);
|
|
2393
|
+
const resolveValue = (value) => workspaceResolveLinkedOutputForTarget(value, targetSlot, scopedRoot);
|
|
2297
2394
|
const out = outputs.get(sourceId);
|
|
2298
2395
|
const sourceIndex = workspaceHandleIndex(edge?.sourceHandle, "output");
|
|
2299
2396
|
const slotName = String(slot?.name || "").trim();
|
|
2300
2397
|
const isPrimaryOutput = !slot || slotName === "result" || slotName === "content" || sourceIndex === 0;
|
|
2301
|
-
if (isPrimaryOutput && out != null && String(out).trim()) return
|
|
2398
|
+
if (isPrimaryOutput && out != null && String(out).trim()) return resolveValue(out);
|
|
2302
2399
|
if (slot && String(slot?.type || "") !== "node") {
|
|
2303
2400
|
const value = workspaceSlotValue(slot);
|
|
2304
|
-
if (value.trim()) return value;
|
|
2401
|
+
if (value.trim()) return resolveValue(value);
|
|
2305
2402
|
}
|
|
2306
|
-
if (out != null && String(out).trim()) return
|
|
2403
|
+
if (out != null && String(out).trim()) return resolveValue(out);
|
|
2307
2404
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
2308
|
-
return workspaceInstanceText(instances[sourceId]);
|
|
2405
|
+
return resolveValue(workspaceInstanceText(instances[sourceId]));
|
|
2309
2406
|
}
|
|
2310
2407
|
|
|
2311
2408
|
function workspaceParseJsonObjectFromText(text) {
|
|
@@ -2931,10 +3028,47 @@ function workspaceRelevantInputValues(body, inputValues = {}) {
|
|
|
2931
3028
|
return { values, placeholders };
|
|
2932
3029
|
}
|
|
2933
3030
|
|
|
3031
|
+
function workspaceDownstreamSlotKind(slot) {
|
|
3032
|
+
const name = String(slot?.name || "").trim().toLowerCase();
|
|
3033
|
+
const type = String(slot?.type || "").trim().toLowerCase();
|
|
3034
|
+
if (type === "markdown" || name === "markdown" || name.endsWith("markdown")) return "markdown";
|
|
3035
|
+
if (type === "html" || name === "html" || name.endsWith("html")) return "html";
|
|
3036
|
+
if (type === "mermaid" || name === "mermaid" || name.endsWith("mermaid")) return "mermaid";
|
|
3037
|
+
if (type === "ascii" || name === "ascii") return "ascii";
|
|
3038
|
+
if (type === "chart" || name === "chart" || name.endsWith("chart")) return "chart";
|
|
3039
|
+
if (type === "table" || name === "table" || name.endsWith("table")) return "table";
|
|
3040
|
+
return "";
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
function workspaceDownstreamOutputKindForField(graph, nodeId, field) {
|
|
3044
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
3045
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
3046
|
+
const source = instances[String(nodeId || "")] || {};
|
|
3047
|
+
const outputSlots = Array.isArray(source.output) ? source.output : [];
|
|
3048
|
+
for (const edge of edges) {
|
|
3049
|
+
if (String(edge?.source || "") !== String(nodeId)) continue;
|
|
3050
|
+
const sourceIndex = workspaceHandleIndex(edge?.sourceHandle, "output");
|
|
3051
|
+
const sourceSlot = outputSlots[sourceIndex] || null;
|
|
3052
|
+
if (workspaceOutputFieldForSlot(sourceSlot, sourceIndex) !== field) continue;
|
|
3053
|
+
const targetSlot = workspaceTargetSlotForEdge(graph, edge);
|
|
3054
|
+
if (!targetSlot || isWorkspaceSemanticInputSlot(targetSlot)) continue;
|
|
3055
|
+
const kind = workspaceDownstreamSlotKind(targetSlot);
|
|
3056
|
+
if (kind) return kind;
|
|
3057
|
+
}
|
|
3058
|
+
return "";
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
function workspaceDownstreamInputDescription(target, slot) {
|
|
3062
|
+
const description = String(slot?.description || "").trim();
|
|
3063
|
+
if (description) return description;
|
|
3064
|
+
return "";
|
|
3065
|
+
}
|
|
3066
|
+
|
|
2934
3067
|
function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
2935
3068
|
const instance = graph?.instances?.[nodeId] || {};
|
|
2936
3069
|
const outputSlots = Array.isArray(instance.output) ? instance.output : [];
|
|
2937
3070
|
const displayBindings = workspaceDownstreamOutputDisplayBindings(graph, nodeId);
|
|
3071
|
+
const downstreamInputRequirements = workspaceDownstreamInputRequirements(graph, nodeId);
|
|
2938
3072
|
const displayByField = new Map();
|
|
2939
3073
|
for (const binding of displayBindings) {
|
|
2940
3074
|
if (!displayByField.has(binding.field)) displayByField.set(binding.field, binding.kind);
|
|
@@ -2946,7 +3080,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
2946
3080
|
return name && type !== "node" && name !== "next" && name !== "result" && name !== "content";
|
|
2947
3081
|
})
|
|
2948
3082
|
.map((slot) => String(slot.name).trim());
|
|
2949
|
-
const resultKind = displayByField.get("result") || "";
|
|
3083
|
+
const resultKind = displayByField.get("result") || workspaceDownstreamOutputKindForField(graph, nodeId, "result") || "";
|
|
2950
3084
|
const resultExtByKind = {
|
|
2951
3085
|
html: "html",
|
|
2952
3086
|
markdown: "md",
|
|
@@ -2982,6 +3116,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
2982
3116
|
"## 输出",
|
|
2983
3117
|
"",
|
|
2984
3118
|
`请把${resultKindText}结果写入 \`${resultFile}\`。${resultGuidance}`,
|
|
3119
|
+
downstreamInputRequirements ? `\n${downstreamInputRequirements}` : "",
|
|
2985
3120
|
...(slots.length ? [`额外输出:${slots.map((name) => `\`${name}\``).join("、")}。短值可写在 \`outParams\`,文件值写成 \`outParams.<name>File\`。`] : []),
|
|
2986
3121
|
"最终只输出下面的 agentflow envelope,不要输出解释、进度或其它文字:",
|
|
2987
3122
|
"",
|
|
@@ -2989,6 +3124,42 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
2989
3124
|
].join("\n");
|
|
2990
3125
|
}
|
|
2991
3126
|
|
|
3127
|
+
function workspaceDownstreamInputRequirements(graph, nodeId) {
|
|
3128
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
3129
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
3130
|
+
const source = instances[String(nodeId || "")] || {};
|
|
3131
|
+
const outputSlots = Array.isArray(source.output) ? source.output : [];
|
|
3132
|
+
const rows = [];
|
|
3133
|
+
const seen = new Set();
|
|
3134
|
+
for (const edge of edges) {
|
|
3135
|
+
if (String(edge?.source || "") !== String(nodeId)) continue;
|
|
3136
|
+
const target = instances[String(edge?.target || "")];
|
|
3137
|
+
if (!target) continue;
|
|
3138
|
+
const targetSlot = workspaceTargetSlotForEdge(graph, edge);
|
|
3139
|
+
if (!targetSlot || isWorkspaceSemanticInputSlot(targetSlot)) continue;
|
|
3140
|
+
const targetName = String(targetSlot.name || "").trim();
|
|
3141
|
+
const targetType = String(targetSlot.type || "text").trim();
|
|
3142
|
+
const description = workspaceDownstreamInputDescription(target, targetSlot);
|
|
3143
|
+
if (!targetName && !description) continue;
|
|
3144
|
+
const sourceIndex = workspaceHandleIndex(edge?.sourceHandle, "output");
|
|
3145
|
+
const sourceSlot = outputSlots[sourceIndex] || null;
|
|
3146
|
+
const sourceField = workspaceOutputFieldForSlot(sourceSlot, sourceIndex);
|
|
3147
|
+
const targetLabel = String(target.label || target.definitionId || edge.target || "").trim();
|
|
3148
|
+
const key = `${sourceField}->${edge.target}:${targetName}:${description}`;
|
|
3149
|
+
if (seen.has(key)) continue;
|
|
3150
|
+
seen.add(key);
|
|
3151
|
+
rows.push([
|
|
3152
|
+
`- 输出 \`${sourceField}\` 会连接到下游 \`${targetLabel}\` 的输入 \`${targetName || "input"}\`(type=${targetType})。`,
|
|
3153
|
+
description ? ` 要求:${description}` : "",
|
|
3154
|
+
].filter(Boolean).join("\n"));
|
|
3155
|
+
}
|
|
3156
|
+
if (!rows.length) return "";
|
|
3157
|
+
return [
|
|
3158
|
+
"下游输入格式要求:",
|
|
3159
|
+
...rows,
|
|
3160
|
+
].join("\n");
|
|
3161
|
+
}
|
|
3162
|
+
|
|
2992
3163
|
function workspaceRunPlan(graph, runNodeId, scopedRoot = "") {
|
|
2993
3164
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
2994
3165
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
@@ -3121,14 +3292,14 @@ function workspaceCachedOutputValueExists(value, scopedRoot = "") {
|
|
|
3121
3292
|
return fs.existsSync(abs) && fs.statSync(abs).isFile();
|
|
3122
3293
|
}
|
|
3123
3294
|
|
|
3124
|
-
function workspaceUpstreamText(graph, nodeId, outputs) {
|
|
3295
|
+
function workspaceUpstreamText(graph, nodeId, outputs, scopedRoot = "") {
|
|
3125
3296
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
3126
3297
|
const incoming = edges
|
|
3127
3298
|
.filter((edge) => String(edge?.target || "") === String(nodeId))
|
|
3128
3299
|
.filter((edge) => !isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge)));
|
|
3129
3300
|
const contentEdge = incoming.find((edge) => String(edge?.targetHandle || "") === "input-1") || incoming[0];
|
|
3130
3301
|
if (!contentEdge) return "";
|
|
3131
|
-
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
|
|
3302
|
+
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge, scopedRoot);
|
|
3132
3303
|
}
|
|
3133
3304
|
|
|
3134
3305
|
function workspaceHandleIndex(handle, prefix) {
|
|
@@ -3181,7 +3352,7 @@ function workspaceNodeFileBoundaryBlock(runPackage = {}) {
|
|
|
3181
3352
|
].filter(Boolean).join("\n");
|
|
3182
3353
|
}
|
|
3183
3354
|
|
|
3184
|
-
function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames = null) {
|
|
3355
|
+
function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames = null, scopedRoot = "") {
|
|
3185
3356
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
3186
3357
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
3187
3358
|
const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
|
|
@@ -3194,10 +3365,10 @@ function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames =
|
|
|
3194
3365
|
}
|
|
3195
3366
|
const contentEdge = contentEdges.find((edge) => String(edge?.targetHandle || "") === "input-1") || contentEdges[0];
|
|
3196
3367
|
if (!contentEdge) return "";
|
|
3197
|
-
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
|
|
3368
|
+
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge, scopedRoot);
|
|
3198
3369
|
}
|
|
3199
3370
|
|
|
3200
|
-
function workspaceInputValues(graph, nodeId, outputs) {
|
|
3371
|
+
function workspaceInputValues(graph, nodeId, outputs, scopedRoot = "") {
|
|
3201
3372
|
const values = {};
|
|
3202
3373
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
3203
3374
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
@@ -3209,7 +3380,7 @@ function workspaceInputValues(graph, nodeId, outputs) {
|
|
|
3209
3380
|
const slot = inputSlots[index] || null;
|
|
3210
3381
|
const name = String(slot?.name || "").trim();
|
|
3211
3382
|
if (!name || isWorkspaceSemanticInputSlot(slot)) continue;
|
|
3212
|
-
const value = workspaceOutputSlotValueForEdge(graph, outputs, edge);
|
|
3383
|
+
const value = workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot);
|
|
3213
3384
|
if (String(value || "").trim()) values[name] = String(value);
|
|
3214
3385
|
}
|
|
3215
3386
|
for (const slot of inputSlots) {
|
|
@@ -3706,7 +3877,7 @@ function workspaceUnwrapOutputEnvelopeForDisplay(content) {
|
|
|
3706
3877
|
return structured.structured ? String(structured.result || "") : raw;
|
|
3707
3878
|
}
|
|
3708
3879
|
|
|
3709
|
-
function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null) {
|
|
3880
|
+
function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null, scopedRoot = "") {
|
|
3710
3881
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
3711
3882
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
3712
3883
|
const updated = [];
|
|
@@ -3716,7 +3887,7 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null)
|
|
|
3716
3887
|
const targetId = String(edge?.target || "");
|
|
3717
3888
|
const target = instances[targetId];
|
|
3718
3889
|
if (!target || !workspaceDisplayKind(target.definitionId)) continue;
|
|
3719
|
-
const value = outputs ? workspaceOutputSlotValueForEdge(graph, outputs, edge) : String(content || "");
|
|
3890
|
+
const value = outputs ? workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot) : String(content || "");
|
|
3720
3891
|
instances[targetId] = workspaceWriteDisplayContent(target, value || content);
|
|
3721
3892
|
updated.push(targetId);
|
|
3722
3893
|
}
|
|
@@ -4110,7 +4281,12 @@ async function workspaceRunToolNodejsScript({
|
|
|
4110
4281
|
}
|
|
4111
4282
|
|
|
4112
4283
|
async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
|
|
4113
|
-
const graph =
|
|
4284
|
+
const graph = hydrateWorkspaceGraphForRuntime(root, {
|
|
4285
|
+
root: scopedRoot,
|
|
4286
|
+
flowId: payload.flowId || "",
|
|
4287
|
+
flowSource: payload.flowSource || "user",
|
|
4288
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
4289
|
+
}, payload.graph || {}, userCtx);
|
|
4114
4290
|
const runNodeId = String(payload?.runNodeId || "").trim();
|
|
4115
4291
|
const { order, pauseNodeIds } = workspaceRunPlan(graph, runNodeId, scopedRoot);
|
|
4116
4292
|
const signal = opts.signal || null;
|
|
@@ -4199,7 +4375,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4199
4375
|
)),
|
|
4200
4376
|
};
|
|
4201
4377
|
outputs.set(nodeId, skillsBlock);
|
|
4202
|
-
workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock, outputs);
|
|
4378
|
+
workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock, outputs, scopedRoot);
|
|
4203
4379
|
emit({ type: "graph", nodeId, graph });
|
|
4204
4380
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4205
4381
|
continue;
|
|
@@ -4219,14 +4395,14 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4219
4395
|
)),
|
|
4220
4396
|
};
|
|
4221
4397
|
outputs.set(nodeId, mcpBlock);
|
|
4222
|
-
workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock, outputs);
|
|
4398
|
+
workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock, outputs, scopedRoot);
|
|
4223
4399
|
emit({ type: "graph", nodeId, graph });
|
|
4224
4400
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4225
4401
|
continue;
|
|
4226
4402
|
}
|
|
4227
4403
|
|
|
4228
4404
|
if (workspaceDisplayKind(defId)) {
|
|
4229
|
-
const content = workspaceUpstreamText(graph, nodeId, outputs);
|
|
4405
|
+
const content = workspaceUpstreamText(graph, nodeId, outputs, scopedRoot);
|
|
4230
4406
|
graph.instances[nodeId] = workspaceWriteDisplayContent(instance, content);
|
|
4231
4407
|
outputs.set(nodeId, content);
|
|
4232
4408
|
emit({ type: "graph", nodeId, graph });
|
|
@@ -4262,7 +4438,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4262
4438
|
}
|
|
4263
4439
|
|
|
4264
4440
|
if (defId === "control_cd_workspace") {
|
|
4265
|
-
const inputText = workspaceUpstreamText(graph, nodeId, outputs);
|
|
4441
|
+
const inputText = workspaceUpstreamText(graph, nodeId, outputs, scopedRoot);
|
|
4266
4442
|
const inputSlots = Array.isArray(instance.input) ? instance.input : [];
|
|
4267
4443
|
const pathSlot = inputSlots.find((slot) => String(slot?.name || "") === "path") ||
|
|
4268
4444
|
inputSlots.find((slot) => String(slot?.name || "") === "target");
|
|
@@ -4473,9 +4649,36 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4473
4649
|
continue;
|
|
4474
4650
|
}
|
|
4475
4651
|
|
|
4652
|
+
if (defId === "tool_wecom_send_group_markdown" || defId === "tool_wecom_send_app_markdown") {
|
|
4653
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
4654
|
+
const markdown = String(inputValues.markdown || inputValues.content || workspaceSlotValue(workspaceSlotByName(instance, "markdown")) || workspaceUpstreamText(graph, nodeId, outputs, scopedRoot) || "");
|
|
4655
|
+
const result = defId === "tool_wecom_send_app_markdown"
|
|
4656
|
+
? await sendWecomAppMarkdown({
|
|
4657
|
+
markdown,
|
|
4658
|
+
toUser: inputValues.toUser || workspaceSlotValue(workspaceSlotByName(instance, "toUser")),
|
|
4659
|
+
corpId: inputValues.corpId || workspaceSlotValue(workspaceSlotByName(instance, "corpId")),
|
|
4660
|
+
corpSecret: inputValues.corpSecret || workspaceSlotValue(workspaceSlotByName(instance, "corpSecret")),
|
|
4661
|
+
agentId: inputValues.agentId || workspaceSlotValue(workspaceSlotByName(instance, "agentId")),
|
|
4662
|
+
accessToken: inputValues.accessToken || workspaceSlotValue(workspaceSlotByName(instance, "accessToken")),
|
|
4663
|
+
}, runtimeEnvForUser(userCtx))
|
|
4664
|
+
: await sendWecomGroupMarkdown({
|
|
4665
|
+
markdown,
|
|
4666
|
+
webhookUrl: inputValues.webhookUrl || workspaceSlotValue(workspaceSlotByName(instance, "webhookUrl")),
|
|
4667
|
+
webhookKey: inputValues.webhookKey || workspaceSlotValue(workspaceSlotByName(instance, "webhookKey")),
|
|
4668
|
+
}, runtimeEnvForUser(userCtx));
|
|
4669
|
+
let nextInstance = workspaceSetOutputSlot(instance, "sent", "true");
|
|
4670
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "message", result.message);
|
|
4671
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "response", JSON.stringify(result.response || {}));
|
|
4672
|
+
graph.instances[nodeId] = nextInstance;
|
|
4673
|
+
outputs.set(nodeId, result.message);
|
|
4674
|
+
emit({ type: "graph", nodeId, graph });
|
|
4675
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4676
|
+
continue;
|
|
4677
|
+
}
|
|
4678
|
+
|
|
4476
4679
|
if (defId === "tool_nodejs") {
|
|
4477
4680
|
const prepareStartedAt = Date.now();
|
|
4478
|
-
const inputValues = workspaceInputValues(graph, nodeId, outputs);
|
|
4681
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
4479
4682
|
const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
|
|
4480
4683
|
scopedRoot,
|
|
4481
4684
|
cwd,
|
|
@@ -4512,16 +4715,16 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4512
4715
|
onActiveChild: opts.onActiveChild,
|
|
4513
4716
|
});
|
|
4514
4717
|
if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
|
|
4515
|
-
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
|
|
4718
|
+
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs, scopedRoot);
|
|
4516
4719
|
if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
4517
4720
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4518
4721
|
continue;
|
|
4519
4722
|
}
|
|
4520
4723
|
|
|
4521
4724
|
const prepareStartedAt = Date.now();
|
|
4522
|
-
const inputValues = workspaceInputValues(graph, nodeId, outputs);
|
|
4725
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
4523
4726
|
const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
|
|
4524
|
-
const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders);
|
|
4727
|
+
const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders, scopedRoot);
|
|
4525
4728
|
const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
|
|
4526
4729
|
const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks);
|
|
4527
4730
|
const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
|
|
@@ -4642,7 +4845,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4642
4845
|
onActiveChild: opts.onActiveChild,
|
|
4643
4846
|
});
|
|
4644
4847
|
if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
|
|
4645
|
-
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
|
|
4848
|
+
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs, scopedRoot);
|
|
4646
4849
|
if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
4647
4850
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4648
4851
|
}
|
|
@@ -4839,11 +5042,267 @@ function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userI
|
|
|
4839
5042
|
const activeFlowRuns = new Map();
|
|
4840
5043
|
/** 正在执行的 Workspace 临时 run(flowId → { controller, child, runNodeId, startedAt });同一 flow 只允许一个 run */
|
|
4841
5044
|
const activeWorkspaceRuns = new Map();
|
|
5045
|
+
const WORKSPACE_SCHEDULES_FILENAME = "workspace-schedules.json";
|
|
5046
|
+
const WORKSPACE_SCHEDULE_POLL_MS = 30_000;
|
|
4842
5047
|
|
|
4843
5048
|
function workspaceRunKey(userCtx, flowSource, flowId) {
|
|
4844
5049
|
return `${userCtx?.userId || ""}:${flowSource || "user"}:${flowId}`;
|
|
4845
5050
|
}
|
|
4846
5051
|
|
|
5052
|
+
function normalizeWorkspaceScheduledRunConfig(raw) {
|
|
5053
|
+
let parsed = {};
|
|
5054
|
+
const text = String(raw || "").trim();
|
|
5055
|
+
if (text) {
|
|
5056
|
+
try {
|
|
5057
|
+
parsed = JSON.parse(text);
|
|
5058
|
+
} catch {
|
|
5059
|
+
parsed = {};
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
const intervalMinutes = Number(parsed.intervalMinutes);
|
|
5063
|
+
return {
|
|
5064
|
+
enabled: parsed.enabled === true,
|
|
5065
|
+
intervalMinutes: Number.isFinite(intervalMinutes) && intervalMinutes > 0
|
|
5066
|
+
? Math.min(Math.max(Math.round(intervalMinutes), 1), 1440)
|
|
5067
|
+
: 60,
|
|
5068
|
+
};
|
|
5069
|
+
}
|
|
5070
|
+
|
|
5071
|
+
function workspaceSchedulesPath() {
|
|
5072
|
+
return path.join(getAgentflowDataRoot(), WORKSPACE_SCHEDULES_FILENAME);
|
|
5073
|
+
}
|
|
5074
|
+
|
|
5075
|
+
function readWorkspaceScheduleRegistry() {
|
|
5076
|
+
const filePath = workspaceSchedulesPath();
|
|
5077
|
+
if (!fs.existsSync(filePath)) return { version: 1, schedules: {} };
|
|
5078
|
+
try {
|
|
5079
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
5080
|
+
return {
|
|
5081
|
+
version: 1,
|
|
5082
|
+
schedules: parsed?.schedules && typeof parsed.schedules === "object" && !Array.isArray(parsed.schedules)
|
|
5083
|
+
? parsed.schedules
|
|
5084
|
+
: {},
|
|
5085
|
+
};
|
|
5086
|
+
} catch {
|
|
5087
|
+
return { version: 1, schedules: {} };
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
|
|
5091
|
+
function writeWorkspaceScheduleRegistry(registry) {
|
|
5092
|
+
const filePath = workspaceSchedulesPath();
|
|
5093
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
5094
|
+
fs.writeFileSync(filePath, JSON.stringify({
|
|
5095
|
+
version: 1,
|
|
5096
|
+
updatedAt: new Date().toISOString(),
|
|
5097
|
+
schedules: registry?.schedules && typeof registry.schedules === "object" ? registry.schedules : {},
|
|
5098
|
+
}, null, 2) + "\n", "utf-8");
|
|
5099
|
+
}
|
|
5100
|
+
|
|
5101
|
+
function workspaceScheduleKey(userId, flowSource, flowId, runNodeId) {
|
|
5102
|
+
return [
|
|
5103
|
+
String(userId || ""),
|
|
5104
|
+
String(flowSource || "user"),
|
|
5105
|
+
String(flowId || ""),
|
|
5106
|
+
String(runNodeId || ""),
|
|
5107
|
+
].join(":");
|
|
5108
|
+
}
|
|
5109
|
+
|
|
5110
|
+
function listWorkspaceScheduleStatusesForFlow(userCtx = {}, flowSource = "user", flowId = "") {
|
|
5111
|
+
const registry = readWorkspaceScheduleRegistry();
|
|
5112
|
+
const userId = String(userCtx.userId || "");
|
|
5113
|
+
return Object.values(registry.schedules || {})
|
|
5114
|
+
.filter((entry) => (
|
|
5115
|
+
String(entry?.userId || "") === userId &&
|
|
5116
|
+
String(entry?.flowSource || "user") === String(flowSource || "user") &&
|
|
5117
|
+
String(entry?.flowId || "") === String(flowId || "")
|
|
5118
|
+
))
|
|
5119
|
+
.sort((a, b) => String(a.runNodeId || "").localeCompare(String(b.runNodeId || "")));
|
|
5120
|
+
}
|
|
5121
|
+
|
|
5122
|
+
function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx = {}) {
|
|
5123
|
+
const flowId = String(scoped?.flowId || "").trim();
|
|
5124
|
+
const flowSource = String(scoped?.flowSource || "user");
|
|
5125
|
+
const userId = String(userCtx.userId || authUser?.userId || "");
|
|
5126
|
+
if (!flowId || !userId) return [];
|
|
5127
|
+
const now = Date.now();
|
|
5128
|
+
const nowIso = new Date(now).toISOString();
|
|
5129
|
+
const registry = readWorkspaceScheduleRegistry();
|
|
5130
|
+
const schedules = { ...(registry.schedules || {}) };
|
|
5131
|
+
const prefix = `${userId}:${flowSource}:${flowId}:`;
|
|
5132
|
+
for (const key of Object.keys(schedules)) {
|
|
5133
|
+
if (key.startsWith(prefix)) delete schedules[key];
|
|
5134
|
+
}
|
|
5135
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
5136
|
+
for (const [runNodeId, instance] of Object.entries(instances)) {
|
|
5137
|
+
if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
|
|
5138
|
+
const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
|
|
5139
|
+
if (!config.enabled) continue;
|
|
5140
|
+
const key = workspaceScheduleKey(userId, flowSource, flowId, runNodeId);
|
|
5141
|
+
const intervalMs = config.intervalMinutes * 60 * 1000;
|
|
5142
|
+
const previous = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
|
|
5143
|
+
const previousNext = Number(previous.nextRunAt || 0);
|
|
5144
|
+
schedules[key] = {
|
|
5145
|
+
...previous,
|
|
5146
|
+
key,
|
|
5147
|
+
enabled: true,
|
|
5148
|
+
userId,
|
|
5149
|
+
username: String(authUser?.username || previous.username || userId),
|
|
5150
|
+
flowId,
|
|
5151
|
+
flowSource,
|
|
5152
|
+
runNodeId,
|
|
5153
|
+
label: String(instance.label || "Scheduled Run"),
|
|
5154
|
+
intervalMinutes: config.intervalMinutes,
|
|
5155
|
+
nextRunAt: Number.isFinite(previousNext) && previousNext > now ? previousNext : now + intervalMs,
|
|
5156
|
+
lastStatus: previous.lastStatus || "armed",
|
|
5157
|
+
updatedAt: nowIso,
|
|
5158
|
+
};
|
|
5159
|
+
}
|
|
5160
|
+
const nextRegistry = { version: 1, schedules };
|
|
5161
|
+
writeWorkspaceScheduleRegistry(nextRegistry);
|
|
5162
|
+
return listWorkspaceScheduleStatusesForFlow(userCtx, flowSource, flowId);
|
|
5163
|
+
}
|
|
5164
|
+
|
|
5165
|
+
function updateWorkspaceScheduleEntry(key, patch) {
|
|
5166
|
+
const registry = readWorkspaceScheduleRegistry();
|
|
5167
|
+
const current = registry.schedules?.[key];
|
|
5168
|
+
if (!current) return null;
|
|
5169
|
+
const next = {
|
|
5170
|
+
...current,
|
|
5171
|
+
...(patch && typeof patch === "object" ? patch : {}),
|
|
5172
|
+
updatedAt: new Date().toISOString(),
|
|
5173
|
+
};
|
|
5174
|
+
registry.schedules[key] = next;
|
|
5175
|
+
writeWorkspaceScheduleRegistry(registry);
|
|
5176
|
+
return next;
|
|
5177
|
+
}
|
|
5178
|
+
|
|
5179
|
+
async function runWorkspaceScheduledEntry(root, entry) {
|
|
5180
|
+
const userCtx = { userId: String(entry.userId || "") };
|
|
5181
|
+
const runKey = workspaceRunKey(userCtx, entry.flowSource || "user", entry.flowId || "");
|
|
5182
|
+
const intervalMs = Math.max(1, Number(entry.intervalMinutes || 60)) * 60 * 1000;
|
|
5183
|
+
const nextRunAt = Date.now() + intervalMs;
|
|
5184
|
+
if (activeWorkspaceRuns.has(runKey)) {
|
|
5185
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5186
|
+
nextRunAt,
|
|
5187
|
+
lastSkippedAt: Date.now(),
|
|
5188
|
+
lastStatus: "skipped: busy",
|
|
5189
|
+
});
|
|
5190
|
+
return;
|
|
5191
|
+
}
|
|
5192
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
5193
|
+
flowId: entry.flowId || "",
|
|
5194
|
+
flowSource: entry.flowSource || "user",
|
|
5195
|
+
}, userCtx);
|
|
5196
|
+
if (scoped.error || scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
5197
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5198
|
+
nextRunAt,
|
|
5199
|
+
lastStatus: "error",
|
|
5200
|
+
lastError: scoped.error || "Workspace schedule target is not writable",
|
|
5201
|
+
lastErrorAt: Date.now(),
|
|
5202
|
+
});
|
|
5203
|
+
return;
|
|
5204
|
+
}
|
|
5205
|
+
const graphPath = workspaceGraphPath(scoped.root);
|
|
5206
|
+
const graph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root).graph, userCtx);
|
|
5207
|
+
const instance = graph.instances?.[entry.runNodeId];
|
|
5208
|
+
const config = normalizeWorkspaceScheduledRunConfig(instance?.body || "");
|
|
5209
|
+
if (!instance || String(instance.definitionId || "") !== "workspace_scheduled_run" || !config.enabled) {
|
|
5210
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5211
|
+
enabled: false,
|
|
5212
|
+
nextRunAt: null,
|
|
5213
|
+
lastStatus: "disabled",
|
|
5214
|
+
});
|
|
5215
|
+
return;
|
|
5216
|
+
}
|
|
5217
|
+
|
|
5218
|
+
const controller = new AbortController();
|
|
5219
|
+
const authUsers = readAuthUsers();
|
|
5220
|
+
const authUser = authUsers[userCtx.userId] || {};
|
|
5221
|
+
const runEntry = {
|
|
5222
|
+
controller,
|
|
5223
|
+
child: null,
|
|
5224
|
+
runId: runLedgerId("workspace"),
|
|
5225
|
+
userId: userCtx.userId,
|
|
5226
|
+
username: String(authUser.username || entry.username || userCtx.userId),
|
|
5227
|
+
runNodeId: String(entry.runNodeId || ""),
|
|
5228
|
+
flowId: String(entry.flowId || ""),
|
|
5229
|
+
flowSource: String(entry.flowSource || "user"),
|
|
5230
|
+
startedAt: Date.now(),
|
|
5231
|
+
scheduled: true,
|
|
5232
|
+
stopChild() {
|
|
5233
|
+
if (this.child && !this.child.killed) {
|
|
5234
|
+
try { this.child.kill("SIGTERM"); } catch (_) {}
|
|
5235
|
+
}
|
|
5236
|
+
},
|
|
5237
|
+
};
|
|
5238
|
+
activeWorkspaceRuns.set(runKey, runEntry);
|
|
5239
|
+
appendWorkspaceRunStarted(runEntry);
|
|
5240
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5241
|
+
lastStatus: "running",
|
|
5242
|
+
lastTriggeredAt: runEntry.startedAt,
|
|
5243
|
+
lastRunId: runEntry.runId,
|
|
5244
|
+
lastError: "",
|
|
5245
|
+
});
|
|
5246
|
+
const setActiveChild = (child) => {
|
|
5247
|
+
runEntry.child = child || null;
|
|
5248
|
+
if (controller.signal.aborted) runEntry.stopChild();
|
|
5249
|
+
};
|
|
5250
|
+
try {
|
|
5251
|
+
const result = await runWorkspaceGraph(root, scoped.root, {
|
|
5252
|
+
flowId: entry.flowId,
|
|
5253
|
+
flowSource: entry.flowSource || "user",
|
|
5254
|
+
runNodeId: entry.runNodeId,
|
|
5255
|
+
graph,
|
|
5256
|
+
}, userCtx, {
|
|
5257
|
+
signal: controller.signal,
|
|
5258
|
+
onActiveChild: setActiveChild,
|
|
5259
|
+
});
|
|
5260
|
+
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
5261
|
+
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
5262
|
+
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
5263
|
+
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
5264
|
+
const endedAt = Date.now();
|
|
5265
|
+
appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "success");
|
|
5266
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5267
|
+
nextRunAt: Date.now() + intervalMs,
|
|
5268
|
+
lastFinishedAt: endedAt,
|
|
5269
|
+
lastStatus: "success",
|
|
5270
|
+
lastError: "",
|
|
5271
|
+
});
|
|
5272
|
+
} catch (e) {
|
|
5273
|
+
const endedAt = Date.now();
|
|
5274
|
+
appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "failed");
|
|
5275
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5276
|
+
nextRunAt: Date.now() + intervalMs,
|
|
5277
|
+
lastFinishedAt: endedAt,
|
|
5278
|
+
lastStatus: "failed",
|
|
5279
|
+
lastError: (e && e.message) || String(e),
|
|
5280
|
+
lastErrorAt: endedAt,
|
|
5281
|
+
});
|
|
5282
|
+
log.info(`[workspace-scheduler] failed ${entry.flowId}/${entry.runNodeId}: ${(e && e.message) || String(e)}`);
|
|
5283
|
+
} finally {
|
|
5284
|
+
if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5287
|
+
|
|
5288
|
+
function pollWorkspaceSchedules(root) {
|
|
5289
|
+
const now = Date.now();
|
|
5290
|
+
const registry = readWorkspaceScheduleRegistry();
|
|
5291
|
+
for (const entry of Object.values(registry.schedules || {})) {
|
|
5292
|
+
if (!entry || entry.enabled !== true) continue;
|
|
5293
|
+
const nextRunAt = Number(entry.nextRunAt || 0);
|
|
5294
|
+
if (!Number.isFinite(nextRunAt) || nextRunAt <= 0) {
|
|
5295
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5296
|
+
nextRunAt: now + Math.max(1, Number(entry.intervalMinutes || 60)) * 60 * 1000,
|
|
5297
|
+
lastStatus: entry.lastStatus || "armed",
|
|
5298
|
+
});
|
|
5299
|
+
continue;
|
|
5300
|
+
}
|
|
5301
|
+
if (nextRunAt > now) continue;
|
|
5302
|
+
void runWorkspaceScheduledEntry(root, entry);
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
|
|
4847
5306
|
/** Cursor/OpenCode 执行目录统一使用当前 UI 启动 workspace。 */
|
|
4848
5307
|
function composerCliWorkspaceForFlowDir(workspaceRoot, _flowDir) {
|
|
4849
5308
|
return path.resolve(workspaceRoot);
|
|
@@ -5181,6 +5640,7 @@ export function startUiServer({
|
|
|
5181
5640
|
}
|
|
5182
5641
|
try {
|
|
5183
5642
|
const config = writeAdminStorageConfig(payload?.config || payload || {});
|
|
5643
|
+
clearSkillRegistryCache();
|
|
5184
5644
|
json(res, 200, { ok: true, config });
|
|
5185
5645
|
} catch (e) {
|
|
5186
5646
|
json(res, 400, { error: (e && e.message) || String(e) });
|
|
@@ -5498,7 +5958,7 @@ export function startUiServer({
|
|
|
5498
5958
|
return;
|
|
5499
5959
|
}
|
|
5500
5960
|
const { path: graphPath, graph } = readWorkspaceGraph(scoped.root);
|
|
5501
|
-
const hydratedGraph =
|
|
5961
|
+
const hydratedGraph = hydrateWorkspaceGraphForRuntime(root, scoped, graph, userCtx);
|
|
5502
5962
|
json(res, 200, {
|
|
5503
5963
|
ok: true,
|
|
5504
5964
|
graph: hydratedGraph,
|
|
@@ -5508,6 +5968,7 @@ export function startUiServer({
|
|
|
5508
5968
|
flowSource: scoped.flowSource,
|
|
5509
5969
|
archived: scoped.archived,
|
|
5510
5970
|
writable: !(scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)),
|
|
5971
|
+
workspaceSchedules: listWorkspaceScheduleStatusesForFlow(userCtx, scoped.flowSource || "user", scoped.flowId || ""),
|
|
5511
5972
|
});
|
|
5512
5973
|
} catch (e) {
|
|
5513
5974
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
@@ -5588,12 +6049,30 @@ export function startUiServer({
|
|
|
5588
6049
|
json(res, 400, { error: "Cannot write workspace graph for builtin or archived pipeline" });
|
|
5589
6050
|
return;
|
|
5590
6051
|
}
|
|
5591
|
-
const submittedGraph =
|
|
6052
|
+
const submittedGraph = hydrateWorkspaceGraphForRuntime(root, scoped, payload.graph || payload, userCtx);
|
|
5592
6053
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
5593
|
-
const currentGraph =
|
|
6054
|
+
const currentGraph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root).graph, userCtx);
|
|
5594
6055
|
const graph = mergeWorkspacePersistentNodeRefs(submittedGraph, currentGraph);
|
|
5595
6056
|
fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2) + "\n", "utf-8");
|
|
5596
|
-
|
|
6057
|
+
const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
|
|
6058
|
+
json(res, 200, { ok: true, path: graphPath, graph, workspaceSchedules });
|
|
6059
|
+
} catch (e) {
|
|
6060
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
6061
|
+
}
|
|
6062
|
+
return;
|
|
6063
|
+
}
|
|
6064
|
+
|
|
6065
|
+
if (req.method === "GET" && url.pathname === "/api/workspace/schedules") {
|
|
6066
|
+
try {
|
|
6067
|
+
const flowId = url.searchParams.get("flowId") || "";
|
|
6068
|
+
const flowSource = url.searchParams.get("flowSource") || "user";
|
|
6069
|
+
if (!flowId) {
|
|
6070
|
+
json(res, 400, { error: "Missing flowId" });
|
|
6071
|
+
return;
|
|
6072
|
+
}
|
|
6073
|
+
json(res, 200, {
|
|
6074
|
+
schedules: listWorkspaceScheduleStatusesForFlow(userCtx, flowSource, flowId),
|
|
6075
|
+
});
|
|
5597
6076
|
} catch (e) {
|
|
5598
6077
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
5599
6078
|
}
|
|
@@ -6600,17 +7079,19 @@ export function startUiServer({
|
|
|
6600
7079
|
}
|
|
6601
7080
|
|
|
6602
7081
|
if (req.method === "GET" && url.pathname === "/api/skillhub/list") {
|
|
6603
|
-
const target = url.searchParams.get("target") || "
|
|
7082
|
+
const target = url.searchParams.get("target") || "agentflow";
|
|
6604
7083
|
const agent = url.searchParams.get("agent") || "codex";
|
|
6605
|
-
const args =
|
|
6606
|
-
if (target === "all") args.push("--all");
|
|
6607
|
-
else if (target === "global") args.push("--global", "--agent", agent);
|
|
7084
|
+
const args = skillhubListArgs(target, agent);
|
|
6608
7085
|
const result = await runSkillhub(args, { cwd: root });
|
|
6609
7086
|
if (!result.ok) {
|
|
6610
7087
|
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
6611
7088
|
return;
|
|
6612
7089
|
}
|
|
6613
|
-
json(res, 200, {
|
|
7090
|
+
json(res, 200, {
|
|
7091
|
+
skills: normalizeSkillhubListPayload(parseJsonText(result.stdout, [])),
|
|
7092
|
+
target,
|
|
7093
|
+
skillsRoot: target === "agentflow" ? getAgentflowSkillsRoot() : "",
|
|
7094
|
+
});
|
|
6614
7095
|
return;
|
|
6615
7096
|
}
|
|
6616
7097
|
|
|
@@ -6655,6 +7136,10 @@ export function startUiServer({
|
|
|
6655
7136
|
}
|
|
6656
7137
|
|
|
6657
7138
|
if (req.method === "POST" && url.pathname === "/api/skillhub/install") {
|
|
7139
|
+
if (!authUser?.isAdmin) {
|
|
7140
|
+
json(res, 403, { error: "Admin required" });
|
|
7141
|
+
return;
|
|
7142
|
+
}
|
|
6658
7143
|
let payload;
|
|
6659
7144
|
try {
|
|
6660
7145
|
payload = JSON.parse(await readBody(req));
|
|
@@ -6667,16 +7152,13 @@ export function startUiServer({
|
|
|
6667
7152
|
json(res, 400, { error: "Missing skill slug or collection" });
|
|
6668
7153
|
return;
|
|
6669
7154
|
}
|
|
6670
|
-
if (payload?.collection && !authUser?.isAdmin) {
|
|
6671
|
-
json(res, 403, { error: "Admin required" });
|
|
6672
|
-
return;
|
|
6673
|
-
}
|
|
6674
7155
|
const beforeSkills = payload?.collection ? listComposerSkills(PACKAGE_ROOT, root) : [];
|
|
6675
7156
|
const result = await runSkillhub(args, { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
|
|
6676
7157
|
if (!result.ok) {
|
|
6677
7158
|
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
6678
7159
|
return;
|
|
6679
7160
|
}
|
|
7161
|
+
clearSkillRegistryCache();
|
|
6680
7162
|
let skillCollections = null;
|
|
6681
7163
|
if (payload?.collection) {
|
|
6682
7164
|
const afterSkills = listComposerSkills(PACKAGE_ROOT, root);
|
|
@@ -6688,6 +7170,10 @@ export function startUiServer({
|
|
|
6688
7170
|
}
|
|
6689
7171
|
|
|
6690
7172
|
if (req.method === "POST" && url.pathname === "/api/skillhub/uninstall") {
|
|
7173
|
+
if (!authUser?.isAdmin) {
|
|
7174
|
+
json(res, 403, { error: "Admin required" });
|
|
7175
|
+
return;
|
|
7176
|
+
}
|
|
6691
7177
|
let payload;
|
|
6692
7178
|
try {
|
|
6693
7179
|
payload = JSON.parse(await readBody(req));
|
|
@@ -6700,21 +7186,22 @@ export function startUiServer({
|
|
|
6700
7186
|
json(res, 400, { error: "Missing skill slug or collection" });
|
|
6701
7187
|
return;
|
|
6702
7188
|
}
|
|
6703
|
-
if (payload?.collection && !authUser?.isAdmin) {
|
|
6704
|
-
json(res, 403, { error: "Admin required" });
|
|
6705
|
-
return;
|
|
6706
|
-
}
|
|
6707
7189
|
const result = await runSkillhub(args, { cwd: root, timeoutMs: 120_000, maxBuffer: 4 * 1024 * 1024 });
|
|
6708
7190
|
if (!result.ok) {
|
|
6709
7191
|
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
6710
7192
|
return;
|
|
6711
7193
|
}
|
|
7194
|
+
clearSkillRegistryCache();
|
|
6712
7195
|
const skillCollections = payload?.collection ? removeSkillhubCollectionGroup(userCtx, payload.collection, root) : null;
|
|
6713
7196
|
json(res, 200, { ok: true, stdout: result.stdout, skillCollections });
|
|
6714
7197
|
return;
|
|
6715
7198
|
}
|
|
6716
7199
|
|
|
6717
7200
|
if (req.method === "POST" && url.pathname === "/api/skillhub/update") {
|
|
7201
|
+
if (!authUser?.isAdmin) {
|
|
7202
|
+
json(res, 403, { error: "Admin required" });
|
|
7203
|
+
return;
|
|
7204
|
+
}
|
|
6718
7205
|
const result = await runSkillhub(["update"], { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
|
|
6719
7206
|
if (!result.ok) {
|
|
6720
7207
|
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
@@ -8154,6 +8641,25 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
8154
8641
|
res.end(data);
|
|
8155
8642
|
});
|
|
8156
8643
|
|
|
8644
|
+
const workspaceScheduleTimer = setInterval(() => {
|
|
8645
|
+
try {
|
|
8646
|
+
pollWorkspaceSchedules(root);
|
|
8647
|
+
} catch (e) {
|
|
8648
|
+
log.debug(`[workspace-scheduler] poll failed: ${(e && e.message) || String(e)}`);
|
|
8649
|
+
}
|
|
8650
|
+
}, WORKSPACE_SCHEDULE_POLL_MS);
|
|
8651
|
+
try {
|
|
8652
|
+
workspaceScheduleTimer.unref?.();
|
|
8653
|
+
} catch (_) {}
|
|
8654
|
+
server.on("close", () => clearInterval(workspaceScheduleTimer));
|
|
8655
|
+
setTimeout(() => {
|
|
8656
|
+
try {
|
|
8657
|
+
pollWorkspaceSchedules(root);
|
|
8658
|
+
} catch (e) {
|
|
8659
|
+
log.debug(`[workspace-scheduler] initial poll failed: ${(e && e.message) || String(e)}`);
|
|
8660
|
+
}
|
|
8661
|
+
}, 1000).unref?.();
|
|
8662
|
+
|
|
8157
8663
|
return new Promise((resolve, reject) => {
|
|
8158
8664
|
server.once("error", reject);
|
|
8159
8665
|
server.listen(port, host, () => {
|