@fieldwangai/agentflow 0.1.71 → 0.1.73
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/composer-skill-router.mjs +9 -1
- package/bin/lib/paths.mjs +28 -0
- package/bin/lib/skill-registry.mjs +20 -7
- package/bin/lib/ui-server.mjs +539 -63
- package/builtin/web-ui/dist/assets/index-DKw0bt1j.js +297 -0
- package/builtin/web-ui/dist/assets/index-t1Xrjvhr.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-B3Wr-I3p.css +0 -1
- package/builtin/web-ui/dist/assets/index-C8JMDUZb.js +0 -296
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 {
|
|
@@ -79,7 +81,7 @@ import {
|
|
|
79
81
|
readComposerSessionMeta,
|
|
80
82
|
} from "./composer-log.mjs";
|
|
81
83
|
import { runNodeScript } from "./pipeline-scripts.mjs";
|
|
82
|
-
import { readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
|
|
84
|
+
import { computeNextRunAt, readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
|
|
83
85
|
import { listScheduleStatuses } from "./scheduler.mjs";
|
|
84
86
|
import {
|
|
85
87
|
deleteMarketplaceFlowSnippetPackage,
|
|
@@ -89,6 +91,7 @@ import {
|
|
|
89
91
|
listMarketplacePackages,
|
|
90
92
|
publishFlowSnippet,
|
|
91
93
|
publishNodeFromInstance,
|
|
94
|
+
resolveMarketplaceNodePackage,
|
|
92
95
|
} from "./marketplace.mjs";
|
|
93
96
|
import { buildGitContext, inferGitRepoRootFromWorktree, loadGitWorktree, normalizeGitContext, runGit, sanitizeWorktreeName, unloadGitWorktree } from "./git-worktree.mjs";
|
|
94
97
|
import { createGitLabMergeRequest } from "./gitlab-mr.mjs";
|
|
@@ -1134,6 +1137,22 @@ function normalizeSkillhubListPayload(raw) {
|
|
|
1134
1137
|
}).filter((x) => x.name);
|
|
1135
1138
|
}
|
|
1136
1139
|
|
|
1140
|
+
function skillhubManagedSkillsDir() {
|
|
1141
|
+
const dir = getAgentflowSkillsRoot();
|
|
1142
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1143
|
+
return dir;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function skillhubListArgs(target, agent) {
|
|
1147
|
+
const normalizedTarget = String(target || "agentflow").trim();
|
|
1148
|
+
const normalizedAgent = String(agent || "codex").trim();
|
|
1149
|
+
const args = ["list", "--json"];
|
|
1150
|
+
if (normalizedTarget === "all") args.push("--all");
|
|
1151
|
+
else if (normalizedTarget === "legacy-global") args.push("--global", "--agent", normalizedAgent);
|
|
1152
|
+
else args.push("--dir", skillhubManagedSkillsDir());
|
|
1153
|
+
return args;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1137
1156
|
function skillhubInstallArgs(payload, { uninstall = false } = {}) {
|
|
1138
1157
|
const slug = String(payload?.slug || payload?.name || "").trim();
|
|
1139
1158
|
if (!slug && !payload?.collection) return null;
|
|
@@ -1144,10 +1163,12 @@ function skillhubInstallArgs(payload, { uninstall = false } = {}) {
|
|
|
1144
1163
|
args.push(slug);
|
|
1145
1164
|
}
|
|
1146
1165
|
if (payload?.skillId) args.push("--skill-id", String(payload.skillId).trim());
|
|
1147
|
-
const target = String(payload?.target || "
|
|
1166
|
+
const target = String(payload?.target || "agentflow").trim();
|
|
1148
1167
|
const agent = String(payload?.agent || "codex").trim();
|
|
1149
|
-
if (target === "global") {
|
|
1168
|
+
if (target === "global" || target === "legacy-global") {
|
|
1150
1169
|
args.push("--global", "--agent", agent);
|
|
1170
|
+
} else if (target === "agentflow") {
|
|
1171
|
+
args.push("--dir", skillhubManagedSkillsDir());
|
|
1151
1172
|
} else if (payload?.dir) {
|
|
1152
1173
|
args.push("--dir", String(payload.dir).trim());
|
|
1153
1174
|
}
|
|
@@ -2153,9 +2174,76 @@ function hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped = {}, gra
|
|
|
2153
2174
|
return changed ? { ...next, instances } : next;
|
|
2154
2175
|
}
|
|
2155
2176
|
|
|
2177
|
+
function workspaceRuntimeInterpreterForMarketplaceEntry(runtime, entry) {
|
|
2178
|
+
const language = String(runtime?.language || "").trim().toLowerCase();
|
|
2179
|
+
const entryLower = String(entry || "").trim().toLowerCase();
|
|
2180
|
+
if (language.includes("python") || entryLower.endsWith(".py")) return "python3";
|
|
2181
|
+
if (language.includes("shell") || language === "bash" || entryLower.endsWith(".sh") || entryLower.endsWith(".bash")) return "bash";
|
|
2182
|
+
return "node";
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
function workspaceRuntimeArgForMarketplace(arg) {
|
|
2186
|
+
const text = String(arg ?? "").trim();
|
|
2187
|
+
if (!text) return "";
|
|
2188
|
+
if (text.includes("${")) return text;
|
|
2189
|
+
return workspaceShellQuote(text);
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
function workspaceMarketplaceRuntimeCommand(resolved) {
|
|
2193
|
+
const runtime = resolved?.runtime && typeof resolved.runtime === "object" ? resolved.runtime : {};
|
|
2194
|
+
const entry = String(runtime.entry || "").trim().replace(/^\/+/, "");
|
|
2195
|
+
if (entry && resolved?.packageDir) {
|
|
2196
|
+
const entryAbs = path.resolve(resolved.packageDir, ...entry.split(/[\\/]+/).filter(Boolean));
|
|
2197
|
+
const packageRoot = path.resolve(resolved.packageDir);
|
|
2198
|
+
const packageRootWithSep = packageRoot.endsWith(path.sep) ? packageRoot : `${packageRoot}${path.sep}`;
|
|
2199
|
+
if (entryAbs === packageRoot || !entryAbs.startsWith(packageRootWithSep)) return "";
|
|
2200
|
+
const args = Array.isArray(runtime.args) ? runtime.args.map(workspaceRuntimeArgForMarketplace).filter(Boolean) : [];
|
|
2201
|
+
return [workspaceRuntimeInterpreterForMarketplaceEntry(runtime, entry), workspaceShellQuote(entryAbs), ...args].join(" ");
|
|
2202
|
+
}
|
|
2203
|
+
return String(runtime.command || "").trim();
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
function hydrateWorkspaceMarketplaceToolNodejsRuntime(workspaceRoot, scoped = {}, graph = {}, userCtx = {}) {
|
|
2207
|
+
const next = normalizeWorkspaceGraphPayload(graph || {});
|
|
2208
|
+
const instances = { ...(next.instances || {}) };
|
|
2209
|
+
let changed = false;
|
|
2210
|
+
for (const [nodeId, instance] of Object.entries(instances)) {
|
|
2211
|
+
if (!instance || typeof instance !== "object") continue;
|
|
2212
|
+
const marketplaceDefId = String(instance.marketplaceRef || instance.definitionId || "").trim();
|
|
2213
|
+
if (!marketplaceDefId.startsWith("marketplace:")) continue;
|
|
2214
|
+
let resolved = null;
|
|
2215
|
+
try {
|
|
2216
|
+
resolved = resolveMarketplaceNodePackage(
|
|
2217
|
+
workspaceRoot,
|
|
2218
|
+
scoped.root || scoped.scopedRoot || workspaceRoot,
|
|
2219
|
+
marketplaceDefId,
|
|
2220
|
+
next,
|
|
2221
|
+
{ userId: userCtx?.userId || "" },
|
|
2222
|
+
);
|
|
2223
|
+
} catch {
|
|
2224
|
+
resolved = null;
|
|
2225
|
+
}
|
|
2226
|
+
if (!resolved || String(resolved.baseDefinitionId || "").trim() !== "tool_nodejs") continue;
|
|
2227
|
+
const script = String(instance.script || "").trim();
|
|
2228
|
+
const scriptRef = String(instance.scriptRef || "").trim();
|
|
2229
|
+
const runtimeScript = script || scriptRef ? "" : workspaceMarketplaceRuntimeCommand(resolved);
|
|
2230
|
+
instances[nodeId] = {
|
|
2231
|
+
...instance,
|
|
2232
|
+
definitionId: "tool_nodejs",
|
|
2233
|
+
marketplaceRef: resolved.resolvedDefinitionId || marketplaceDefId,
|
|
2234
|
+
marketplacePackageId: resolved.id,
|
|
2235
|
+
marketplaceVersion: resolved.version,
|
|
2236
|
+
...(runtimeScript ? { script: runtimeScript } : {}),
|
|
2237
|
+
};
|
|
2238
|
+
changed = true;
|
|
2239
|
+
}
|
|
2240
|
+
return changed ? { ...next, instances } : next;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2156
2243
|
function hydrateWorkspaceGraphForRuntime(workspaceRoot, scoped = {}, graph = {}, userCtx = {}) {
|
|
2157
2244
|
const withRefs = hydrateWorkspaceNodeRefsFromFiles(scoped.root || scoped.scopedRoot || workspaceRoot, graph);
|
|
2158
|
-
|
|
2245
|
+
const withMarketplaceRuntime = hydrateWorkspaceMarketplaceToolNodejsRuntime(workspaceRoot, scoped, withRefs, userCtx);
|
|
2246
|
+
return hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped, withMarketplaceRuntime, userCtx);
|
|
2159
2247
|
}
|
|
2160
2248
|
|
|
2161
2249
|
function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
|
|
@@ -3431,6 +3519,7 @@ function workspaceMaterializeImplementationReference(instance, scopedRoot, runPa
|
|
|
3431
3519
|
}
|
|
3432
3520
|
|
|
3433
3521
|
function workspaceImplementationBlock(instance, scopedRoot, runPackage = {}) {
|
|
3522
|
+
if (!WORKSPACE_IMPLEMENTATION_REFERENCE_ENABLED) return "";
|
|
3434
3523
|
const ref = workspaceMaterializeImplementationReference(instance, scopedRoot, runPackage);
|
|
3435
3524
|
const inline = workspaceImplementationInlineText(instance);
|
|
3436
3525
|
if (!ref && !inline) return "";
|
|
@@ -3458,6 +3547,46 @@ function workspaceDefaultImplementationRef(nodeId) {
|
|
|
3458
3547
|
return `nodes/${workspaceSafeNodeFileName(nodeId)}/implementation.md`;
|
|
3459
3548
|
}
|
|
3460
3549
|
|
|
3550
|
+
function workspaceDefaultHistoryRef(nodeId) {
|
|
3551
|
+
return `nodes/${workspaceSafeNodeFileName(nodeId)}/history.md`;
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
function workspaceMaterializeNodeHistoryReference(nodeId, scopedRoot, runPackage = {}) {
|
|
3555
|
+
const historyRef = workspaceDefaultHistoryRef(nodeId);
|
|
3556
|
+
const abs = workspaceResolveFlowFile(scopedRoot, historyRef, "historyRef");
|
|
3557
|
+
const exists = fs.existsSync(abs) && fs.statSync(abs).isFile();
|
|
3558
|
+
const nodeRunDir = String(runPackage?.nodeRunDir || "").trim();
|
|
3559
|
+
let mounted = "";
|
|
3560
|
+
if (exists && nodeRunDir) {
|
|
3561
|
+
try {
|
|
3562
|
+
const mountedRel = path.join("references", "history.md");
|
|
3563
|
+
const dest = path.resolve(nodeRunDir, mountedRel);
|
|
3564
|
+
const nodeRunWithSep = nodeRunDir.endsWith(path.sep) ? nodeRunDir : `${nodeRunDir}${path.sep}`;
|
|
3565
|
+
if (dest === nodeRunDir || dest.startsWith(nodeRunWithSep)) {
|
|
3566
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
3567
|
+
fs.copyFileSync(abs, dest);
|
|
3568
|
+
mounted = mountedRel.split(path.sep).join(path.posix.sep);
|
|
3569
|
+
}
|
|
3570
|
+
} catch {
|
|
3571
|
+
mounted = "";
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
return { historyRef, exists, mounted };
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
function workspaceNodeHistoryBlock(nodeId, scopedRoot, runPackage = {}) {
|
|
3578
|
+
const ref = workspaceMaterializeNodeHistoryReference(nodeId, scopedRoot, runPackage);
|
|
3579
|
+
if (!ref?.exists) return "";
|
|
3580
|
+
return [
|
|
3581
|
+
"## 历史参考",
|
|
3582
|
+
"",
|
|
3583
|
+
`- 历史记录文件:\`${ref.mounted || ref.historyRef}\``,
|
|
3584
|
+
ref.mounted ? `- 原始流水线路径:\`${ref.historyRef}\`` : "",
|
|
3585
|
+
"",
|
|
3586
|
+
"该文件只记录之前运行时的 thinking 摘要与最终结论,用作轻量参考;不要把历史当成硬约束。若历史与当前任务、输入或输出要求冲突,以当前任务为准。",
|
|
3587
|
+
].filter((line) => line !== "").join("\n");
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3461
3590
|
function workspaceImplementationModeForInstance(instance) {
|
|
3462
3591
|
const explicit = String(instance?.implementationMode || "").trim();
|
|
3463
3592
|
if (explicit) return explicit;
|
|
@@ -3649,11 +3778,96 @@ async function workspaceGenerateImplementationMarkdown({
|
|
|
3649
3778
|
return markdown.replace(/^```(?:markdown|md)?\s*/i, "").replace(/```\s*$/i, "").trim();
|
|
3650
3779
|
}
|
|
3651
3780
|
|
|
3781
|
+
function workspaceHistoryTextFromEvents(events = [], kind, maxItems = 24, maxChars = 12000) {
|
|
3782
|
+
const parts = [];
|
|
3783
|
+
for (const ev of Array.isArray(events) ? events : []) {
|
|
3784
|
+
if (!ev || ev.kind !== kind) continue;
|
|
3785
|
+
const text = String(ev.text || "").trim();
|
|
3786
|
+
if (!text) continue;
|
|
3787
|
+
parts.push(text);
|
|
3788
|
+
if (parts.length >= maxItems) break;
|
|
3789
|
+
}
|
|
3790
|
+
return workspaceClipImplementationText(parts.join("\n"), maxChars);
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
function workspaceBuildNodeHistoryEntry(instance, nodeId, opts = {}) {
|
|
3794
|
+
const inputValues = opts.inputValues || {};
|
|
3795
|
+
const structured = opts.structured && typeof opts.structured === "object" ? opts.structured : {};
|
|
3796
|
+
const result = String(opts.resultContent || structured.result || "").trim();
|
|
3797
|
+
const task = workspaceResolveBodyPlaceholders(instance?.body || "", inputValues).trim();
|
|
3798
|
+
const thinking = workspaceHistoryTextFromEvents(opts.historyEvents || [], "thinking", 40, 16000);
|
|
3799
|
+
const assistant = workspaceHistoryTextFromEvents(opts.historyEvents || [], "assistant", 8, 8000);
|
|
3800
|
+
const resultEvent = workspaceHistoryTextFromEvents(opts.historyEvents || [], "result", 4, 12000);
|
|
3801
|
+
const conclusion = workspaceClipImplementationText(resultEvent || result || assistant, 20000);
|
|
3802
|
+
return [
|
|
3803
|
+
`## ${new Date().toISOString()} · ${nodeId}`,
|
|
3804
|
+
"",
|
|
3805
|
+
`label: ${String(instance?.label || nodeId || "node")}`,
|
|
3806
|
+
`definitionId: ${String(instance?.definitionId || "(unknown)")}`,
|
|
3807
|
+
"",
|
|
3808
|
+
"### 任务",
|
|
3809
|
+
"",
|
|
3810
|
+
workspaceClipImplementationText(task || instance?.body || instance?.scriptRef || instance?.script || "(无显式任务)", 6000),
|
|
3811
|
+
"",
|
|
3812
|
+
Object.keys(inputValues || {}).length ? "### 输入摘要" : "",
|
|
3813
|
+
Object.keys(inputValues || {}).length ? "" : "",
|
|
3814
|
+
Object.keys(inputValues || {}).length ? workspaceClipImplementationText(JSON.stringify(inputValues, null, 2), 8000) : "",
|
|
3815
|
+
Object.keys(inputValues || {}).length ? "" : "",
|
|
3816
|
+
thinking ? "### Thinking 摘要" : "",
|
|
3817
|
+
thinking ? "" : "",
|
|
3818
|
+
thinking || "",
|
|
3819
|
+
thinking ? "" : "",
|
|
3820
|
+
"### 结论",
|
|
3821
|
+
"",
|
|
3822
|
+
conclusion || "(无结论内容)",
|
|
3823
|
+
"",
|
|
3824
|
+
"### 输出协议",
|
|
3825
|
+
"",
|
|
3826
|
+
JSON.stringify({
|
|
3827
|
+
result: workspaceClipImplementationText(structured.result || result, 4000),
|
|
3828
|
+
resultFile: structured.resultFile || "",
|
|
3829
|
+
outParams: structured.outParams || {},
|
|
3830
|
+
}, null, 2),
|
|
3831
|
+
].filter((line) => line !== "").join("\n");
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
function workspaceClipNodeHistory(value, maxChars = WORKSPACE_NODE_HISTORY_MAX_CHARS) {
|
|
3835
|
+
const text = String(value || "").trim();
|
|
3836
|
+
if (text.length <= maxChars) return text;
|
|
3837
|
+
return [
|
|
3838
|
+
"# Workspace Node History",
|
|
3839
|
+
"",
|
|
3840
|
+
"> Older history was truncated to keep this reference lightweight.",
|
|
3841
|
+
"",
|
|
3842
|
+
text.slice(-maxChars),
|
|
3843
|
+
].join("\n").trim();
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3846
|
+
function workspacePersistNodeHistory(scopedRoot, graph, nodeId, opts = {}) {
|
|
3847
|
+
const current = graph?.instances?.[nodeId];
|
|
3848
|
+
if (!current || String(current.definitionId || "") === "workspace_run" || String(current.definitionId || "") === "workspace_scheduled_run") {
|
|
3849
|
+
return { changed: false, wrote: false, instance: current };
|
|
3850
|
+
}
|
|
3851
|
+
const historyRef = workspaceDefaultHistoryRef(nodeId);
|
|
3852
|
+
const abs = workspaceResolveFlowFile(scopedRoot, historyRef, "historyRef");
|
|
3853
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
3854
|
+
const previous = fs.existsSync(abs) && fs.statSync(abs).isFile()
|
|
3855
|
+
? workspaceReadTextFileIfExists(abs, WORKSPACE_NODE_HISTORY_MAX_CHARS + 20000)
|
|
3856
|
+
: "# Workspace Node History\n";
|
|
3857
|
+
const entry = workspaceBuildNodeHistoryEntry(current, nodeId, opts);
|
|
3858
|
+
const nextText = workspaceClipNodeHistory(`${previous.trimEnd()}\n\n${entry}\n`);
|
|
3859
|
+
fs.writeFileSync(abs, `${nextText.trimEnd()}\n`, "utf-8");
|
|
3860
|
+
return { changed: false, wrote: true, instance: current, historyRef };
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3652
3863
|
async function workspacePersistNodeImplementation(scopedRoot, graph, nodeId, opts = {}) {
|
|
3653
3864
|
const current = graph?.instances?.[nodeId];
|
|
3654
3865
|
if (!current || String(current.definitionId || "") === "workspace_run" || String(current.definitionId || "") === "workspace_scheduled_run") {
|
|
3655
3866
|
return { changed: false, wrote: false, instance: current };
|
|
3656
3867
|
}
|
|
3868
|
+
if (!WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED) {
|
|
3869
|
+
return workspacePersistNodeHistory(scopedRoot, graph, nodeId, opts);
|
|
3870
|
+
}
|
|
3657
3871
|
const existingRef = String(current.implementationRef || "").trim();
|
|
3658
3872
|
const implementationRef = existingRef || workspaceDefaultImplementationRef(nodeId);
|
|
3659
3873
|
const abs = workspaceResolveFlowFile(scopedRoot, implementationRef, "implementationRef");
|
|
@@ -3695,7 +3909,9 @@ async function workspaceTryPersistNodeImplementation(scopedRoot, graph, nodeId,
|
|
|
3695
3909
|
opts.emit?.({
|
|
3696
3910
|
type: "natural",
|
|
3697
3911
|
kind: "warning",
|
|
3698
|
-
text:
|
|
3912
|
+
text: WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED
|
|
3913
|
+
? `实现方案未更新:${e?.message || String(e)}`
|
|
3914
|
+
: `历史记录未更新:${e?.message || String(e)}`,
|
|
3699
3915
|
});
|
|
3700
3916
|
return { changed: false, wrote: false, instance: graph?.instances?.[nodeId] };
|
|
3701
3917
|
}
|
|
@@ -4727,8 +4943,8 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4727
4943
|
} catch {
|
|
4728
4944
|
// Best-effort debug artifact only.
|
|
4729
4945
|
}
|
|
4730
|
-
const
|
|
4731
|
-
const prompt = workspaceNodePrompt(graph, nodeId, promptUpstreamText, promptSkillsBlock, promptMcpBlock, runtimeInputValues, runPackage,
|
|
4946
|
+
const historyBlock = workspaceNodeHistoryBlock(nodeId, scopedRoot, runPackage);
|
|
4947
|
+
const prompt = workspaceNodePrompt(graph, nodeId, promptUpstreamText, promptSkillsBlock, promptMcpBlock, runtimeInputValues, runPackage, historyBlock);
|
|
4732
4948
|
try {
|
|
4733
4949
|
fs.writeFileSync(path.join(runPackage.nodeRunDir, "prompt.md"), prompt.trimEnd() + "\n", "utf-8");
|
|
4734
4950
|
} catch {
|
|
@@ -4743,6 +4959,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4743
4959
|
});
|
|
4744
4960
|
emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
|
|
4745
4961
|
let content = "";
|
|
4962
|
+
const runHistoryEvents = [];
|
|
4746
4963
|
const maxAttempts = 3;
|
|
4747
4964
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4748
4965
|
let attemptContent = "";
|
|
@@ -4772,6 +4989,12 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4772
4989
|
? { ...ev, text: workspaceCanonicalAgentOutput(ev.text), nodeId }
|
|
4773
4990
|
: { ...ev, nodeId };
|
|
4774
4991
|
emit(eventToEmit);
|
|
4992
|
+
if (ev?.type === "natural" && typeof ev.text === "string") {
|
|
4993
|
+
const kind = String(ev.kind || "");
|
|
4994
|
+
if (kind === "thinking" || kind === "assistant" || kind === "result") {
|
|
4995
|
+
runHistoryEvents.push({ kind, text: ev.text });
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4775
4998
|
if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
|
|
4776
4999
|
attemptLastAssistantContent = ev.text;
|
|
4777
5000
|
attemptContent += (attemptContent ? "\n" : "") + ev.text;
|
|
@@ -4821,6 +5044,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4821
5044
|
runPackage,
|
|
4822
5045
|
modelKey,
|
|
4823
5046
|
userCtx,
|
|
5047
|
+
historyEvents: runHistoryEvents,
|
|
4824
5048
|
emit: (event) => emit({ ...event, nodeId }),
|
|
4825
5049
|
onActiveChild: opts.onActiveChild,
|
|
4826
5050
|
});
|
|
@@ -5020,15 +5244,61 @@ function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userI
|
|
|
5020
5244
|
|
|
5021
5245
|
/** 正在执行的 flow run(flowId → { child, runUuid });同一 flow 只允许一个 run */
|
|
5022
5246
|
const activeFlowRuns = new Map();
|
|
5023
|
-
/** 正在执行的 Workspace 临时 run(
|
|
5247
|
+
/** 正在执行的 Workspace 临时 run(runId/sessionId → { controller, child, runNodeId, startedAt, plannedNodeIds }) */
|
|
5024
5248
|
const activeWorkspaceRuns = new Map();
|
|
5025
5249
|
const WORKSPACE_SCHEDULES_FILENAME = "workspace-schedules.json";
|
|
5026
5250
|
const WORKSPACE_SCHEDULE_POLL_MS = 30_000;
|
|
5251
|
+
const WORKSPACE_IMPLEMENTATION_REFERENCE_ENABLED = false;
|
|
5252
|
+
const WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED = false;
|
|
5253
|
+
const WORKSPACE_NODE_HISTORY_MAX_CHARS = 80000;
|
|
5254
|
+
|
|
5255
|
+
function workspaceIntervalMinutesToCron(intervalMinutes) {
|
|
5256
|
+
const n = Number(intervalMinutes);
|
|
5257
|
+
if (!Number.isFinite(n) || n <= 0) return "0 9 * * *";
|
|
5258
|
+
const minutes = Math.max(1, Math.min(1440, Math.round(n)));
|
|
5259
|
+
if (minutes < 60) return `*/${minutes} * * * *`;
|
|
5260
|
+
if (minutes === 60) return "0 * * * *";
|
|
5261
|
+
if (minutes < 1440 && minutes % 60 === 0) return `0 */${minutes / 60} * * *`;
|
|
5262
|
+
return "0 9 * * *";
|
|
5263
|
+
}
|
|
5027
5264
|
|
|
5028
5265
|
function workspaceRunKey(userCtx, flowSource, flowId) {
|
|
5029
5266
|
return `${userCtx?.userId || ""}:${flowSource || "user"}:${flowId}`;
|
|
5030
5267
|
}
|
|
5031
5268
|
|
|
5269
|
+
function workspaceRunEntryKey(scopeKey, runId) {
|
|
5270
|
+
return `${scopeKey}:${String(runId || "").trim() || runLedgerId("workspace")}`;
|
|
5271
|
+
}
|
|
5272
|
+
|
|
5273
|
+
function workspaceActiveRunsForScope(scopeKey) {
|
|
5274
|
+
const key = String(scopeKey || "");
|
|
5275
|
+
return Array.from(activeWorkspaceRuns.entries())
|
|
5276
|
+
.filter(([, entry]) => String(entry?.scopeKey || "") === key);
|
|
5277
|
+
}
|
|
5278
|
+
|
|
5279
|
+
function workspaceRunPlanNodeIds(runNodeId, plan) {
|
|
5280
|
+
return Array.from(new Set([
|
|
5281
|
+
String(runNodeId || "").trim(),
|
|
5282
|
+
...(Array.isArray(plan?.order) ? plan.order : []),
|
|
5283
|
+
...(Array.isArray(plan?.pauseNodeIds) ? plan.pauseNodeIds : []),
|
|
5284
|
+
].map((id) => String(id || "").trim()).filter(Boolean)));
|
|
5285
|
+
}
|
|
5286
|
+
|
|
5287
|
+
function workspaceFindActiveRunConflict(scopeKey, plannedNodeIds) {
|
|
5288
|
+
const planned = new Set((plannedNodeIds || []).map((id) => String(id || "").trim()).filter(Boolean));
|
|
5289
|
+
for (const [key, entry] of workspaceActiveRunsForScope(scopeKey)) {
|
|
5290
|
+
const activeIds = Array.isArray(entry?.plannedNodeIds) ? entry.plannedNodeIds : [];
|
|
5291
|
+
if (!activeIds.length) {
|
|
5292
|
+
return { key, entry, conflictNodeIds: [] };
|
|
5293
|
+
}
|
|
5294
|
+
const conflictNodeIds = activeIds
|
|
5295
|
+
.map((id) => String(id || "").trim())
|
|
5296
|
+
.filter((id) => id && planned.has(id));
|
|
5297
|
+
if (conflictNodeIds.length) return { key, entry, conflictNodeIds };
|
|
5298
|
+
}
|
|
5299
|
+
return null;
|
|
5300
|
+
}
|
|
5301
|
+
|
|
5032
5302
|
function normalizeWorkspaceScheduledRunConfig(raw) {
|
|
5033
5303
|
let parsed = {};
|
|
5034
5304
|
const text = String(raw || "").trim();
|
|
@@ -5040,14 +5310,29 @@ function normalizeWorkspaceScheduledRunConfig(raw) {
|
|
|
5040
5310
|
}
|
|
5041
5311
|
}
|
|
5042
5312
|
const intervalMinutes = Number(parsed.intervalMinutes);
|
|
5313
|
+
const migratedCron = workspaceIntervalMinutesToCron(intervalMinutes);
|
|
5314
|
+
const cron = typeof parsed.cron === "string" && parsed.cron.trim()
|
|
5315
|
+
? parsed.cron.trim()
|
|
5316
|
+
: migratedCron;
|
|
5317
|
+
const timezone = typeof parsed.timezone === "string" && parsed.timezone.trim()
|
|
5318
|
+
? parsed.timezone.trim()
|
|
5319
|
+
: "Asia/Shanghai";
|
|
5320
|
+
const targetRunNodeId = typeof parsed.targetRunNodeId === "string" ? parsed.targetRunNodeId.trim() : "";
|
|
5321
|
+
const overlapPolicy = parsed.overlapPolicy === "skip" ? "skip" : "skip";
|
|
5043
5322
|
return {
|
|
5044
5323
|
enabled: parsed.enabled === true,
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5324
|
+
cron,
|
|
5325
|
+
timezone,
|
|
5326
|
+
targetRunNodeId,
|
|
5327
|
+
overlapPolicy,
|
|
5048
5328
|
};
|
|
5049
5329
|
}
|
|
5050
5330
|
|
|
5331
|
+
function workspaceScheduleNextRunAt(config, fromDate = new Date()) {
|
|
5332
|
+
if (!config?.enabled || !config?.cron) return null;
|
|
5333
|
+
return Date.parse(computeNextRunAt(config.cron, config.timezone || "Asia/Shanghai", fromDate));
|
|
5334
|
+
}
|
|
5335
|
+
|
|
5051
5336
|
function workspaceSchedulesPath() {
|
|
5052
5337
|
return path.join(getAgentflowDataRoot(), WORKSPACE_SCHEDULES_FILENAME);
|
|
5053
5338
|
}
|
|
@@ -5078,12 +5363,12 @@ function writeWorkspaceScheduleRegistry(registry) {
|
|
|
5078
5363
|
}, null, 2) + "\n", "utf-8");
|
|
5079
5364
|
}
|
|
5080
5365
|
|
|
5081
|
-
function workspaceScheduleKey(userId, flowSource, flowId,
|
|
5366
|
+
function workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId) {
|
|
5082
5367
|
return [
|
|
5083
5368
|
String(userId || ""),
|
|
5084
5369
|
String(flowSource || "user"),
|
|
5085
5370
|
String(flowId || ""),
|
|
5086
|
-
String(
|
|
5371
|
+
String(scheduleNodeId || ""),
|
|
5087
5372
|
].join(":");
|
|
5088
5373
|
}
|
|
5089
5374
|
|
|
@@ -5096,7 +5381,23 @@ function listWorkspaceScheduleStatusesForFlow(userCtx = {}, flowSource = "user",
|
|
|
5096
5381
|
String(entry?.flowSource || "user") === String(flowSource || "user") &&
|
|
5097
5382
|
String(entry?.flowId || "") === String(flowId || "")
|
|
5098
5383
|
))
|
|
5099
|
-
.sort((a, b) => String(a.runNodeId || "").localeCompare(String(b.runNodeId || "")));
|
|
5384
|
+
.sort((a, b) => String(a.scheduleNodeId || a.runNodeId || "").localeCompare(String(b.scheduleNodeId || b.runNodeId || "")));
|
|
5385
|
+
}
|
|
5386
|
+
|
|
5387
|
+
function workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config = {}) {
|
|
5388
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
5389
|
+
const explicit = String(config.targetRunNodeId || "").trim();
|
|
5390
|
+
if (explicit && String(instances[explicit]?.definitionId || "") === "workspace_run") return explicit;
|
|
5391
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
5392
|
+
for (const edge of edges) {
|
|
5393
|
+
const source = String(edge?.source || "");
|
|
5394
|
+
const target = String(edge?.target || "");
|
|
5395
|
+
if (source !== String(scheduleNodeId || "") || !target) continue;
|
|
5396
|
+
if (!workspaceIsControlEdge(graph, edge)) continue;
|
|
5397
|
+
if (String(instances[target]?.definitionId || "") === "workspace_run") return target;
|
|
5398
|
+
}
|
|
5399
|
+
const firstRun = Object.entries(instances).find(([, instance]) => String(instance?.definitionId || "") === "workspace_run");
|
|
5400
|
+
return firstRun ? firstRun[0] : "";
|
|
5100
5401
|
}
|
|
5101
5402
|
|
|
5102
5403
|
function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx = {}) {
|
|
@@ -5113,14 +5414,35 @@ function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx =
|
|
|
5113
5414
|
if (key.startsWith(prefix)) delete schedules[key];
|
|
5114
5415
|
}
|
|
5115
5416
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
5116
|
-
for (const [
|
|
5417
|
+
for (const [scheduleNodeId, instance] of Object.entries(instances)) {
|
|
5117
5418
|
if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
|
|
5118
5419
|
const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
|
|
5119
5420
|
if (!config.enabled) continue;
|
|
5120
|
-
const
|
|
5121
|
-
const
|
|
5421
|
+
const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
|
|
5422
|
+
const key = workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId);
|
|
5122
5423
|
const previous = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
|
|
5123
5424
|
const previousNext = Number(previous.nextRunAt || 0);
|
|
5425
|
+
const previousMatches = (
|
|
5426
|
+
String(previous.cron || "") === config.cron &&
|
|
5427
|
+
String(previous.timezone || "") === config.timezone &&
|
|
5428
|
+
String(previous.targetRunNodeId || previous.runNodeId || "") === targetRunNodeId
|
|
5429
|
+
);
|
|
5430
|
+
let nextRunAt = null;
|
|
5431
|
+
let lastStatus = previous.lastStatus || "armed";
|
|
5432
|
+
let lastError = previous.lastError || "";
|
|
5433
|
+
try {
|
|
5434
|
+
nextRunAt = previousMatches && Number.isFinite(previousNext) && previousNext > now
|
|
5435
|
+
? previousNext
|
|
5436
|
+
: workspaceScheduleNextRunAt(config, new Date(now));
|
|
5437
|
+
if (!targetRunNodeId) {
|
|
5438
|
+
lastStatus = "invalid";
|
|
5439
|
+
lastError = "No target Run node selected or connected";
|
|
5440
|
+
}
|
|
5441
|
+
} catch (e) {
|
|
5442
|
+
lastStatus = "invalid";
|
|
5443
|
+
lastError = (e && e.message) || String(e);
|
|
5444
|
+
nextRunAt = null;
|
|
5445
|
+
}
|
|
5124
5446
|
schedules[key] = {
|
|
5125
5447
|
...previous,
|
|
5126
5448
|
key,
|
|
@@ -5129,11 +5451,16 @@ function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx =
|
|
|
5129
5451
|
username: String(authUser?.username || previous.username || userId),
|
|
5130
5452
|
flowId,
|
|
5131
5453
|
flowSource,
|
|
5132
|
-
|
|
5454
|
+
scheduleNodeId,
|
|
5455
|
+
runNodeId: targetRunNodeId,
|
|
5456
|
+
targetRunNodeId,
|
|
5133
5457
|
label: String(instance.label || "Scheduled Run"),
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5458
|
+
cron: config.cron,
|
|
5459
|
+
timezone: config.timezone,
|
|
5460
|
+
overlapPolicy: config.overlapPolicy,
|
|
5461
|
+
nextRunAt,
|
|
5462
|
+
lastStatus,
|
|
5463
|
+
lastError,
|
|
5137
5464
|
updatedAt: nowIso,
|
|
5138
5465
|
};
|
|
5139
5466
|
}
|
|
@@ -5158,17 +5485,22 @@ function updateWorkspaceScheduleEntry(key, patch) {
|
|
|
5158
5485
|
|
|
5159
5486
|
async function runWorkspaceScheduledEntry(root, entry) {
|
|
5160
5487
|
const userCtx = { userId: String(entry.userId || "") };
|
|
5161
|
-
const
|
|
5162
|
-
const
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5488
|
+
const scopeKey = workspaceRunKey(userCtx, entry.flowSource || "user", entry.flowId || "");
|
|
5489
|
+
const fallbackConfig = {
|
|
5490
|
+
enabled: true,
|
|
5491
|
+
cron: String(entry.cron || "0 9 * * *"),
|
|
5492
|
+
timezone: String(entry.timezone || "Asia/Shanghai"),
|
|
5493
|
+
targetRunNodeId: String(entry.targetRunNodeId || entry.runNodeId || ""),
|
|
5494
|
+
overlapPolicy: "skip",
|
|
5495
|
+
};
|
|
5496
|
+
const computeNext = (config = fallbackConfig) => {
|
|
5497
|
+
try {
|
|
5498
|
+
return workspaceScheduleNextRunAt(config, new Date());
|
|
5499
|
+
} catch {
|
|
5500
|
+
return null;
|
|
5501
|
+
}
|
|
5502
|
+
};
|
|
5503
|
+
let nextRunAt = computeNext(fallbackConfig);
|
|
5172
5504
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
5173
5505
|
flowId: entry.flowId || "",
|
|
5174
5506
|
flowSource: entry.flowSource || "user",
|
|
@@ -5184,8 +5516,10 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5184
5516
|
}
|
|
5185
5517
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
5186
5518
|
const graph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root).graph, userCtx);
|
|
5187
|
-
const
|
|
5519
|
+
const scheduleNodeId = String(entry.scheduleNodeId || entry.key?.split(":").pop() || "");
|
|
5520
|
+
const instance = graph.instances?.[scheduleNodeId];
|
|
5188
5521
|
const config = normalizeWorkspaceScheduledRunConfig(instance?.body || "");
|
|
5522
|
+
nextRunAt = computeNext(config);
|
|
5189
5523
|
if (!instance || String(instance.definitionId || "") !== "workspace_scheduled_run" || !config.enabled) {
|
|
5190
5524
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5191
5525
|
enabled: false,
|
|
@@ -5194,19 +5528,56 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5194
5528
|
});
|
|
5195
5529
|
return;
|
|
5196
5530
|
}
|
|
5531
|
+
const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
|
|
5532
|
+
if (!targetRunNodeId) {
|
|
5533
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5534
|
+
nextRunAt,
|
|
5535
|
+
lastStatus: "invalid",
|
|
5536
|
+
lastError: "No target Run node selected or connected",
|
|
5537
|
+
lastErrorAt: Date.now(),
|
|
5538
|
+
});
|
|
5539
|
+
return;
|
|
5540
|
+
}
|
|
5541
|
+
let plan;
|
|
5542
|
+
try {
|
|
5543
|
+
plan = workspaceRunPlan(graph, targetRunNodeId, scoped.root);
|
|
5544
|
+
} catch (e) {
|
|
5545
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5546
|
+
nextRunAt,
|
|
5547
|
+
lastStatus: "failed",
|
|
5548
|
+
lastError: (e && e.message) || String(e),
|
|
5549
|
+
lastErrorAt: Date.now(),
|
|
5550
|
+
});
|
|
5551
|
+
return;
|
|
5552
|
+
}
|
|
5553
|
+
const plannedNodeIds = workspaceRunPlanNodeIds(targetRunNodeId, plan);
|
|
5554
|
+
const conflict = workspaceFindActiveRunConflict(scopeKey, plannedNodeIds);
|
|
5555
|
+
if (conflict) {
|
|
5556
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5557
|
+
nextRunAt,
|
|
5558
|
+
lastSkippedAt: Date.now(),
|
|
5559
|
+
lastStatus: "skipped: busy",
|
|
5560
|
+
lastError: "",
|
|
5561
|
+
});
|
|
5562
|
+
return;
|
|
5563
|
+
}
|
|
5197
5564
|
|
|
5198
5565
|
const controller = new AbortController();
|
|
5199
5566
|
const authUsers = readAuthUsers();
|
|
5200
5567
|
const authUser = authUsers[userCtx.userId] || {};
|
|
5568
|
+
const runId = runLedgerId("workspace");
|
|
5569
|
+
const runKey = workspaceRunEntryKey(scopeKey, runId);
|
|
5201
5570
|
const runEntry = {
|
|
5571
|
+
scopeKey,
|
|
5202
5572
|
controller,
|
|
5203
5573
|
child: null,
|
|
5204
|
-
runId
|
|
5574
|
+
runId,
|
|
5205
5575
|
userId: userCtx.userId,
|
|
5206
5576
|
username: String(authUser.username || entry.username || userCtx.userId),
|
|
5207
|
-
runNodeId:
|
|
5577
|
+
runNodeId: targetRunNodeId,
|
|
5208
5578
|
flowId: String(entry.flowId || ""),
|
|
5209
5579
|
flowSource: String(entry.flowSource || "user"),
|
|
5580
|
+
plannedNodeIds,
|
|
5210
5581
|
startedAt: Date.now(),
|
|
5211
5582
|
scheduled: true,
|
|
5212
5583
|
stopChild() {
|
|
@@ -5221,6 +5592,10 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5221
5592
|
lastStatus: "running",
|
|
5222
5593
|
lastTriggeredAt: runEntry.startedAt,
|
|
5223
5594
|
lastRunId: runEntry.runId,
|
|
5595
|
+
runNodeId: targetRunNodeId,
|
|
5596
|
+
targetRunNodeId,
|
|
5597
|
+
cron: config.cron,
|
|
5598
|
+
timezone: config.timezone,
|
|
5224
5599
|
lastError: "",
|
|
5225
5600
|
});
|
|
5226
5601
|
const setActiveChild = (child) => {
|
|
@@ -5231,7 +5606,7 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5231
5606
|
const result = await runWorkspaceGraph(root, scoped.root, {
|
|
5232
5607
|
flowId: entry.flowId,
|
|
5233
5608
|
flowSource: entry.flowSource || "user",
|
|
5234
|
-
runNodeId:
|
|
5609
|
+
runNodeId: targetRunNodeId,
|
|
5235
5610
|
graph,
|
|
5236
5611
|
}, userCtx, {
|
|
5237
5612
|
signal: controller.signal,
|
|
@@ -5244,7 +5619,7 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5244
5619
|
const endedAt = Date.now();
|
|
5245
5620
|
appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "success");
|
|
5246
5621
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5247
|
-
nextRunAt:
|
|
5622
|
+
nextRunAt: computeNext(config),
|
|
5248
5623
|
lastFinishedAt: endedAt,
|
|
5249
5624
|
lastStatus: "success",
|
|
5250
5625
|
lastError: "",
|
|
@@ -5253,13 +5628,13 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5253
5628
|
const endedAt = Date.now();
|
|
5254
5629
|
appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "failed");
|
|
5255
5630
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5256
|
-
nextRunAt:
|
|
5631
|
+
nextRunAt: computeNext(config),
|
|
5257
5632
|
lastFinishedAt: endedAt,
|
|
5258
5633
|
lastStatus: "failed",
|
|
5259
5634
|
lastError: (e && e.message) || String(e),
|
|
5260
5635
|
lastErrorAt: endedAt,
|
|
5261
5636
|
});
|
|
5262
|
-
log.info(`[workspace-scheduler] failed ${entry.flowId}/${
|
|
5637
|
+
log.info(`[workspace-scheduler] failed ${entry.flowId}/${targetRunNodeId}: ${(e && e.message) || String(e)}`);
|
|
5263
5638
|
} finally {
|
|
5264
5639
|
if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
|
|
5265
5640
|
}
|
|
@@ -5272,8 +5647,25 @@ function pollWorkspaceSchedules(root) {
|
|
|
5272
5647
|
if (!entry || entry.enabled !== true) continue;
|
|
5273
5648
|
const nextRunAt = Number(entry.nextRunAt || 0);
|
|
5274
5649
|
if (!Number.isFinite(nextRunAt) || nextRunAt <= 0) {
|
|
5650
|
+
const config = {
|
|
5651
|
+
enabled: true,
|
|
5652
|
+
cron: String(entry.cron || "0 9 * * *"),
|
|
5653
|
+
timezone: String(entry.timezone || "Asia/Shanghai"),
|
|
5654
|
+
};
|
|
5655
|
+
let computedNext = null;
|
|
5656
|
+
try {
|
|
5657
|
+
computedNext = workspaceScheduleNextRunAt(config, new Date(now));
|
|
5658
|
+
} catch (e) {
|
|
5659
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5660
|
+
nextRunAt: null,
|
|
5661
|
+
lastStatus: "invalid",
|
|
5662
|
+
lastError: (e && e.message) || String(e),
|
|
5663
|
+
lastErrorAt: now,
|
|
5664
|
+
});
|
|
5665
|
+
continue;
|
|
5666
|
+
}
|
|
5275
5667
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5276
|
-
nextRunAt:
|
|
5668
|
+
nextRunAt: computedNext,
|
|
5277
5669
|
lastStatus: entry.lastStatus || "armed",
|
|
5278
5670
|
});
|
|
5279
5671
|
continue;
|
|
@@ -5620,6 +6012,7 @@ export function startUiServer({
|
|
|
5620
6012
|
}
|
|
5621
6013
|
try {
|
|
5622
6014
|
const config = writeAdminStorageConfig(payload?.config || payload || {});
|
|
6015
|
+
clearSkillRegistryCache();
|
|
5623
6016
|
json(res, 200, { ok: true, config });
|
|
5624
6017
|
} catch (e) {
|
|
5625
6018
|
json(res, 400, { error: (e && e.message) || String(e) });
|
|
@@ -6058,6 +6451,53 @@ export function startUiServer({
|
|
|
6058
6451
|
return;
|
|
6059
6452
|
}
|
|
6060
6453
|
|
|
6454
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/run/plan") {
|
|
6455
|
+
let payload;
|
|
6456
|
+
try {
|
|
6457
|
+
payload = JSON.parse(await readBody(req));
|
|
6458
|
+
} catch {
|
|
6459
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
6460
|
+
return;
|
|
6461
|
+
}
|
|
6462
|
+
try {
|
|
6463
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
6464
|
+
flowId: payload.flowId || "",
|
|
6465
|
+
flowSource: payload.flowSource || "user",
|
|
6466
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
6467
|
+
}, userCtx);
|
|
6468
|
+
if (scoped.error) {
|
|
6469
|
+
json(res, 400, { error: scoped.error });
|
|
6470
|
+
return;
|
|
6471
|
+
}
|
|
6472
|
+
const flowId = String(payload.flowId || "").trim();
|
|
6473
|
+
if (!flowId) {
|
|
6474
|
+
json(res, 400, { error: "Missing flowId" });
|
|
6475
|
+
return;
|
|
6476
|
+
}
|
|
6477
|
+
const graph = hydrateWorkspaceGraphForRuntime(root, scoped, payload.graph || {}, userCtx);
|
|
6478
|
+
const runNodeId = String(payload.runNodeId || "").trim();
|
|
6479
|
+
const plan = workspaceRunPlan(graph, runNodeId, scoped.root);
|
|
6480
|
+
const plannedNodeIds = workspaceRunPlanNodeIds(runNodeId, plan);
|
|
6481
|
+
const scopeKey = workspaceRunKey(userCtx, scoped.flowSource || payload.flowSource || "user", flowId);
|
|
6482
|
+
const conflict = workspaceFindActiveRunConflict(scopeKey, plannedNodeIds);
|
|
6483
|
+
json(res, 200, {
|
|
6484
|
+
ok: true,
|
|
6485
|
+
runNodeId,
|
|
6486
|
+
order: plan.order,
|
|
6487
|
+
pauseNodeIds: plan.pauseNodeIds,
|
|
6488
|
+
plannedNodeIds,
|
|
6489
|
+
conflict: conflict ? {
|
|
6490
|
+
runId: conflict.entry?.runId || "",
|
|
6491
|
+
runNodeId: conflict.entry?.runNodeId || "",
|
|
6492
|
+
conflictNodeIds: conflict.conflictNodeIds,
|
|
6493
|
+
} : null,
|
|
6494
|
+
});
|
|
6495
|
+
} catch (e) {
|
|
6496
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
6497
|
+
}
|
|
6498
|
+
return;
|
|
6499
|
+
}
|
|
6500
|
+
|
|
6061
6501
|
if (req.method === "POST" && url.pathname === "/api/workspace/run") {
|
|
6062
6502
|
let payload;
|
|
6063
6503
|
try {
|
|
@@ -6086,21 +6526,35 @@ export function startUiServer({
|
|
|
6086
6526
|
json(res, 400, { error: "Missing flowId" });
|
|
6087
6527
|
return;
|
|
6088
6528
|
}
|
|
6089
|
-
const
|
|
6090
|
-
|
|
6091
|
-
|
|
6529
|
+
const runtimeGraph = hydrateWorkspaceGraphForRuntime(root, scoped, payload.graph || {}, userCtx);
|
|
6530
|
+
const runNodeId = String(payload.runNodeId || "").trim();
|
|
6531
|
+
const plan = workspaceRunPlan(runtimeGraph, runNodeId, scoped.root);
|
|
6532
|
+
const plannedNodeIds = workspaceRunPlanNodeIds(runNodeId, plan);
|
|
6533
|
+
const scopeKey = workspaceRunKey(userCtx, scoped.flowSource || payload.flowSource || "user", flowId);
|
|
6534
|
+
const conflict = workspaceFindActiveRunConflict(scopeKey, plannedNodeIds);
|
|
6535
|
+
if (conflict) {
|
|
6536
|
+
json(res, 409, {
|
|
6537
|
+
error: "该 Run 与正在执行的 Run 共享节点",
|
|
6538
|
+
runNodeId: conflict.entry?.runNodeId || "",
|
|
6539
|
+
runId: conflict.entry?.runId || "",
|
|
6540
|
+
conflictNodeIds: conflict.conflictNodeIds,
|
|
6541
|
+
});
|
|
6092
6542
|
return;
|
|
6093
6543
|
}
|
|
6094
6544
|
const controller = new AbortController();
|
|
6545
|
+
const runId = String(payload.runSessionId || payload.runId || "").trim() || runLedgerId("workspace");
|
|
6546
|
+
const runKey = workspaceRunEntryKey(scopeKey, runId);
|
|
6095
6547
|
const runEntry = {
|
|
6548
|
+
scopeKey,
|
|
6096
6549
|
controller,
|
|
6097
6550
|
child: null,
|
|
6098
|
-
runId
|
|
6551
|
+
runId,
|
|
6099
6552
|
userId: String(userCtx.userId || ""),
|
|
6100
6553
|
username: String(authUser?.username || userCtx.userId || ""),
|
|
6101
|
-
runNodeId
|
|
6554
|
+
runNodeId,
|
|
6102
6555
|
flowId,
|
|
6103
6556
|
flowSource: scoped.flowSource || payload.flowSource || "user",
|
|
6557
|
+
plannedNodeIds,
|
|
6104
6558
|
startedAt: Date.now(),
|
|
6105
6559
|
stopChild() {
|
|
6106
6560
|
if (this.child && !this.child.killed) {
|
|
@@ -6214,14 +6668,22 @@ export function startUiServer({
|
|
|
6214
6668
|
return;
|
|
6215
6669
|
}
|
|
6216
6670
|
const flowSource = url.searchParams.get("flowSource") || "user";
|
|
6217
|
-
const
|
|
6218
|
-
const
|
|
6671
|
+
const scopeKey = workspaceRunKey(userCtx, flowSource, flowId);
|
|
6672
|
+
const entries = workspaceActiveRunsForScope(scopeKey).map(([, entry]) => entry);
|
|
6673
|
+
const entry = entries[0] || null;
|
|
6219
6674
|
json(res, 200, {
|
|
6220
|
-
running:
|
|
6675
|
+
running: entries.length > 0,
|
|
6221
6676
|
flowId,
|
|
6222
6677
|
flowSource,
|
|
6223
6678
|
runNodeId: entry?.runNodeId || "",
|
|
6224
6679
|
startedAt: entry?.startedAt || null,
|
|
6680
|
+
runs: entries.map((item) => ({
|
|
6681
|
+
runId: item?.runId || "",
|
|
6682
|
+
runNodeId: item?.runNodeId || "",
|
|
6683
|
+
startedAt: item?.startedAt || null,
|
|
6684
|
+
plannedNodeIds: Array.isArray(item?.plannedNodeIds) ? item.plannedNodeIds : [],
|
|
6685
|
+
scheduled: item?.scheduled === true,
|
|
6686
|
+
})),
|
|
6225
6687
|
});
|
|
6226
6688
|
return;
|
|
6227
6689
|
}
|
|
@@ -6239,8 +6701,14 @@ export function startUiServer({
|
|
|
6239
6701
|
json(res, 400, { error: "Missing flowId" });
|
|
6240
6702
|
return;
|
|
6241
6703
|
}
|
|
6242
|
-
const
|
|
6243
|
-
const
|
|
6704
|
+
const scopeKey = workspaceRunKey(userCtx, payload.flowSource || "user", flowId);
|
|
6705
|
+
const runId = String(payload.runId || payload.runSessionId || "").trim();
|
|
6706
|
+
const runNodeId = String(payload.runNodeId || "").trim();
|
|
6707
|
+
const entries = workspaceActiveRunsForScope(scopeKey);
|
|
6708
|
+
const match = entries.find(([, item]) => runId && String(item?.runId || "") === runId)
|
|
6709
|
+
|| entries.find(([, item]) => runNodeId && String(item?.runNodeId || "") === runNodeId)
|
|
6710
|
+
|| (!runId && !runNodeId && entries.length === 1 ? entries[0] : null);
|
|
6711
|
+
const entry = match?.[1] || null;
|
|
6244
6712
|
if (!entry) {
|
|
6245
6713
|
json(res, 404, { error: "该 Workspace 未在运行" });
|
|
6246
6714
|
return;
|
|
@@ -7058,17 +7526,19 @@ export function startUiServer({
|
|
|
7058
7526
|
}
|
|
7059
7527
|
|
|
7060
7528
|
if (req.method === "GET" && url.pathname === "/api/skillhub/list") {
|
|
7061
|
-
const target = url.searchParams.get("target") || "
|
|
7529
|
+
const target = url.searchParams.get("target") || "agentflow";
|
|
7062
7530
|
const agent = url.searchParams.get("agent") || "codex";
|
|
7063
|
-
const args =
|
|
7064
|
-
if (target === "all") args.push("--all");
|
|
7065
|
-
else if (target === "global") args.push("--global", "--agent", agent);
|
|
7531
|
+
const args = skillhubListArgs(target, agent);
|
|
7066
7532
|
const result = await runSkillhub(args, { cwd: root });
|
|
7067
7533
|
if (!result.ok) {
|
|
7068
7534
|
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
7069
7535
|
return;
|
|
7070
7536
|
}
|
|
7071
|
-
json(res, 200, {
|
|
7537
|
+
json(res, 200, {
|
|
7538
|
+
skills: normalizeSkillhubListPayload(parseJsonText(result.stdout, [])),
|
|
7539
|
+
target,
|
|
7540
|
+
skillsRoot: target === "agentflow" ? getAgentflowSkillsRoot() : "",
|
|
7541
|
+
});
|
|
7072
7542
|
return;
|
|
7073
7543
|
}
|
|
7074
7544
|
|
|
@@ -7113,6 +7583,10 @@ export function startUiServer({
|
|
|
7113
7583
|
}
|
|
7114
7584
|
|
|
7115
7585
|
if (req.method === "POST" && url.pathname === "/api/skillhub/install") {
|
|
7586
|
+
if (!authUser?.isAdmin) {
|
|
7587
|
+
json(res, 403, { error: "Admin required" });
|
|
7588
|
+
return;
|
|
7589
|
+
}
|
|
7116
7590
|
let payload;
|
|
7117
7591
|
try {
|
|
7118
7592
|
payload = JSON.parse(await readBody(req));
|
|
@@ -7125,16 +7599,13 @@ export function startUiServer({
|
|
|
7125
7599
|
json(res, 400, { error: "Missing skill slug or collection" });
|
|
7126
7600
|
return;
|
|
7127
7601
|
}
|
|
7128
|
-
if (payload?.collection && !authUser?.isAdmin) {
|
|
7129
|
-
json(res, 403, { error: "Admin required" });
|
|
7130
|
-
return;
|
|
7131
|
-
}
|
|
7132
7602
|
const beforeSkills = payload?.collection ? listComposerSkills(PACKAGE_ROOT, root) : [];
|
|
7133
7603
|
const result = await runSkillhub(args, { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
|
|
7134
7604
|
if (!result.ok) {
|
|
7135
7605
|
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
7136
7606
|
return;
|
|
7137
7607
|
}
|
|
7608
|
+
clearSkillRegistryCache();
|
|
7138
7609
|
let skillCollections = null;
|
|
7139
7610
|
if (payload?.collection) {
|
|
7140
7611
|
const afterSkills = listComposerSkills(PACKAGE_ROOT, root);
|
|
@@ -7146,6 +7617,10 @@ export function startUiServer({
|
|
|
7146
7617
|
}
|
|
7147
7618
|
|
|
7148
7619
|
if (req.method === "POST" && url.pathname === "/api/skillhub/uninstall") {
|
|
7620
|
+
if (!authUser?.isAdmin) {
|
|
7621
|
+
json(res, 403, { error: "Admin required" });
|
|
7622
|
+
return;
|
|
7623
|
+
}
|
|
7149
7624
|
let payload;
|
|
7150
7625
|
try {
|
|
7151
7626
|
payload = JSON.parse(await readBody(req));
|
|
@@ -7158,21 +7633,22 @@ export function startUiServer({
|
|
|
7158
7633
|
json(res, 400, { error: "Missing skill slug or collection" });
|
|
7159
7634
|
return;
|
|
7160
7635
|
}
|
|
7161
|
-
if (payload?.collection && !authUser?.isAdmin) {
|
|
7162
|
-
json(res, 403, { error: "Admin required" });
|
|
7163
|
-
return;
|
|
7164
|
-
}
|
|
7165
7636
|
const result = await runSkillhub(args, { cwd: root, timeoutMs: 120_000, maxBuffer: 4 * 1024 * 1024 });
|
|
7166
7637
|
if (!result.ok) {
|
|
7167
7638
|
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
7168
7639
|
return;
|
|
7169
7640
|
}
|
|
7641
|
+
clearSkillRegistryCache();
|
|
7170
7642
|
const skillCollections = payload?.collection ? removeSkillhubCollectionGroup(userCtx, payload.collection, root) : null;
|
|
7171
7643
|
json(res, 200, { ok: true, stdout: result.stdout, skillCollections });
|
|
7172
7644
|
return;
|
|
7173
7645
|
}
|
|
7174
7646
|
|
|
7175
7647
|
if (req.method === "POST" && url.pathname === "/api/skillhub/update") {
|
|
7648
|
+
if (!authUser?.isAdmin) {
|
|
7649
|
+
json(res, 403, { error: "Admin required" });
|
|
7650
|
+
return;
|
|
7651
|
+
}
|
|
7176
7652
|
const result = await runSkillhub(["update"], { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
|
|
7177
7653
|
if (!result.ok) {
|
|
7178
7654
|
json(res, 500, { error: result.error, stdout: result.stdout });
|