@fieldwangai/agentflow 0.1.99 → 0.1.100
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/agent-runners.mjs +19 -0
- package/bin/lib/composer-agent.mjs +2 -0
- package/bin/lib/ui-server.mjs +99 -38
- package/bin/lib/workspace-run-controller.mjs +125 -0
- package/builtin/web-ui/dist/assets/{index-DR6YAyoA.js → index-h35s4X2i.js} +52 -52
- package/builtin/web-ui/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -31,6 +31,17 @@ function childEnv(options = {}, extra = {}) {
|
|
|
31
31
|
return { ...process.env, ...readMergedEnvObject(userId), ...optEnv, ...extra };
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function notifyPromptAgentChild(options, child) {
|
|
35
|
+
if (typeof options?.onChild !== "function") return;
|
|
36
|
+
try {
|
|
37
|
+
options.onChild(child || null, {
|
|
38
|
+
processGroup: Boolean(options.detached) && process.platform !== "win32",
|
|
39
|
+
});
|
|
40
|
+
} catch {
|
|
41
|
+
// Child tracking must not interfere with the runner.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
function cursorAttemptOptions(options = {}) {
|
|
35
46
|
const baseEnv = childEnv(options);
|
|
36
47
|
const attempts = Array.isArray(options._agentflowCursorApiKeyAttempts)
|
|
@@ -1330,7 +1341,9 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
1330
1341
|
stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
|
|
1331
1342
|
shell: false,
|
|
1332
1343
|
env: childEnv(options, cursorApiKeyEnv(cursorSelection)),
|
|
1344
|
+
detached: Boolean(options.detached) && process.platform !== "win32",
|
|
1333
1345
|
});
|
|
1346
|
+
notifyPromptAgentChild(options, child);
|
|
1334
1347
|
|
|
1335
1348
|
let lastResult = null;
|
|
1336
1349
|
let hadError = false;
|
|
@@ -1551,6 +1564,7 @@ export function runOpenCodeAgentWithPrompt(cliWorkspace, promptText, options = {
|
|
|
1551
1564
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1552
1565
|
shell: false,
|
|
1553
1566
|
env: childEnv(options),
|
|
1567
|
+
detached: Boolean(options.detached) && process.platform !== "win32",
|
|
1554
1568
|
};
|
|
1555
1569
|
if (options.force) {
|
|
1556
1570
|
spawnOpts.env = {
|
|
@@ -1562,6 +1576,7 @@ export function runOpenCodeAgentWithPrompt(cliWorkspace, promptText, options = {
|
|
|
1562
1576
|
}
|
|
1563
1577
|
|
|
1564
1578
|
const child = spawn(opencodeCmd, args, spawnOpts);
|
|
1579
|
+
notifyPromptAgentChild(options, child);
|
|
1565
1580
|
|
|
1566
1581
|
const emit = (payload) => {
|
|
1567
1582
|
try {
|
|
@@ -1664,7 +1679,9 @@ export function runClaudeCodeAgentWithPrompt(cliWorkspace, promptText, options =
|
|
|
1664
1679
|
stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
|
|
1665
1680
|
shell: false,
|
|
1666
1681
|
env: childEnv(options),
|
|
1682
|
+
detached: Boolean(options.detached) && process.platform !== "win32",
|
|
1667
1683
|
});
|
|
1684
|
+
notifyPromptAgentChild(options, child);
|
|
1668
1685
|
|
|
1669
1686
|
let lastResult = null;
|
|
1670
1687
|
let hadError = false;
|
|
@@ -1845,7 +1862,9 @@ export function runCodexAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
1845
1862
|
stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
|
|
1846
1863
|
shell: false,
|
|
1847
1864
|
env: childEnv(options),
|
|
1865
|
+
detached: Boolean(options.detached) && process.platform !== "win32",
|
|
1848
1866
|
});
|
|
1867
|
+
notifyPromptAgentChild(options, child);
|
|
1849
1868
|
|
|
1850
1869
|
let hadError = false;
|
|
1851
1870
|
let emittedNatural = false;
|
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -128,6 +128,7 @@ import {
|
|
|
128
128
|
listWorkspaceRunLogs,
|
|
129
129
|
readWorkspaceRunLogEvents,
|
|
130
130
|
} from "./workspace-run-logs.mjs";
|
|
131
|
+
import { createWorkspaceRunController } from "./workspace-run-controller.mjs";
|
|
131
132
|
|
|
132
133
|
const MIME = {
|
|
133
134
|
".html": "text/html; charset=utf-8",
|
|
@@ -4947,6 +4948,8 @@ async function workspaceGeneratePlannedImplementationMarkdown({
|
|
|
4947
4948
|
prompt,
|
|
4948
4949
|
modelKey,
|
|
4949
4950
|
agentflowUserId: userCtx?.userId || "",
|
|
4951
|
+
detached: process.platform !== "win32",
|
|
4952
|
+
onChild: onActiveChild,
|
|
4950
4953
|
extraEnv: runtimeEnvForUser(userCtx, {
|
|
4951
4954
|
AGENTFLOW_IMPLEMENTATION_REF: implementationPath || "",
|
|
4952
4955
|
AGENTFLOW_NODE_RUN_DIR: runPackage?.nodeRunDir || "",
|
|
@@ -4967,7 +4970,6 @@ async function workspaceGeneratePlannedImplementationMarkdown({
|
|
|
4967
4970
|
emit?.({ type: "status", nodeId, line: `优化工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
|
|
4968
4971
|
},
|
|
4969
4972
|
});
|
|
4970
|
-
if (typeof onActiveChild === "function") onActiveChild(handle.child || null);
|
|
4971
4973
|
try {
|
|
4972
4974
|
await handle.finished;
|
|
4973
4975
|
} finally {
|
|
@@ -5009,6 +5011,8 @@ async function workspaceGenerateImplementationMarkdown({
|
|
|
5009
5011
|
prompt,
|
|
5010
5012
|
modelKey,
|
|
5011
5013
|
agentflowUserId: userCtx?.userId || "",
|
|
5014
|
+
detached: process.platform !== "win32",
|
|
5015
|
+
onChild: onActiveChild,
|
|
5012
5016
|
extraEnv: runtimeEnvForUser(userCtx, {
|
|
5013
5017
|
AGENTFLOW_IMPLEMENTATION_REF: implementationPath || "",
|
|
5014
5018
|
AGENTFLOW_NODE_RUN_DIR: runPackage?.nodeRunDir || "",
|
|
@@ -5029,7 +5033,6 @@ async function workspaceGenerateImplementationMarkdown({
|
|
|
5029
5033
|
emit?.({ type: "status", line: `总结方案工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
|
|
5030
5034
|
},
|
|
5031
5035
|
});
|
|
5032
|
-
if (typeof onActiveChild === "function") onActiveChild(handle.child || null);
|
|
5033
5036
|
try {
|
|
5034
5037
|
await handle.finished;
|
|
5035
5038
|
} finally {
|
|
@@ -5994,6 +5997,8 @@ async function workspaceRunToolNodejsScript({
|
|
|
5994
5997
|
userCtx,
|
|
5995
5998
|
envOverlay = {},
|
|
5996
5999
|
emit,
|
|
6000
|
+
signal,
|
|
6001
|
+
onActiveChild,
|
|
5997
6002
|
}) {
|
|
5998
6003
|
const scriptRef = String(instance?.scriptRef || "").trim();
|
|
5999
6004
|
const scriptAbs = scriptRef ? workspaceResolveFlowFile(scopedRoot, scriptRef, "scriptRef") : "";
|
|
@@ -6041,14 +6046,24 @@ async function workspaceRunToolNodejsScript({
|
|
|
6041
6046
|
|
|
6042
6047
|
const started = Date.now();
|
|
6043
6048
|
return await new Promise((resolve, reject) => {
|
|
6049
|
+
const processGroup = process.platform !== "win32";
|
|
6044
6050
|
const child = spawn(command, [], {
|
|
6045
6051
|
cwd: runPackage.nodeRunDir,
|
|
6046
6052
|
shell: true,
|
|
6047
6053
|
stdio: ["ignore", "pipe", "pipe"],
|
|
6048
6054
|
env,
|
|
6055
|
+
detached: processGroup,
|
|
6049
6056
|
});
|
|
6057
|
+
if (typeof onActiveChild === "function") onActiveChild(child, { processGroup });
|
|
6050
6058
|
let stdout = "";
|
|
6051
6059
|
let stderr = "";
|
|
6060
|
+
let settled = false;
|
|
6061
|
+
const finish = (callback) => {
|
|
6062
|
+
if (settled) return;
|
|
6063
|
+
settled = true;
|
|
6064
|
+
if (typeof onActiveChild === "function") onActiveChild(null);
|
|
6065
|
+
callback();
|
|
6066
|
+
};
|
|
6052
6067
|
child.stdout.setEncoding("utf-8");
|
|
6053
6068
|
child.stderr.setEncoding("utf-8");
|
|
6054
6069
|
child.stdout.on("data", (chunk) => {
|
|
@@ -6057,19 +6072,27 @@ async function workspaceRunToolNodejsScript({
|
|
|
6057
6072
|
child.stderr.on("data", (chunk) => {
|
|
6058
6073
|
stderr += String(chunk);
|
|
6059
6074
|
});
|
|
6060
|
-
child.on("error", reject);
|
|
6075
|
+
child.on("error", (error) => finish(() => reject(error)));
|
|
6061
6076
|
child.on("close", (code) => {
|
|
6077
|
+
if (signal?.aborted) {
|
|
6078
|
+
finish(() => {
|
|
6079
|
+
const error = new Error("Workspace run stopped");
|
|
6080
|
+
error.code = "WORKSPACE_RUN_ABORTED";
|
|
6081
|
+
reject(error);
|
|
6082
|
+
});
|
|
6083
|
+
return;
|
|
6084
|
+
}
|
|
6062
6085
|
if (stderr.trim()) {
|
|
6063
6086
|
emit?.({ type: "natural", kind: "warning", text: `[script stderr]\n${stderr.trim().slice(-4000)}` });
|
|
6064
6087
|
}
|
|
6065
6088
|
if (code !== 0) {
|
|
6066
|
-
reject(new Error(`tool_nodejs script exited ${code}${stderr.trim() ? `: ${stderr.trim().slice(-800)}` : ""}`));
|
|
6089
|
+
finish(() => reject(new Error(`tool_nodejs script exited ${code}${stderr.trim() ? `: ${stderr.trim().slice(-800)}` : ""}`)));
|
|
6067
6090
|
return;
|
|
6068
6091
|
}
|
|
6069
6092
|
const elapsedMs = Math.max(0, Date.now() - started);
|
|
6070
6093
|
emit?.({ type: "status", line: `Timing script: ${elapsedMs}ms`, timing: { label: "script", elapsedMs } });
|
|
6071
6094
|
const content = stdout.trim() || workspaceEnvelopeFromOutputFiles(outputRefs, runPackage.nodeRunDir);
|
|
6072
|
-
resolve(content);
|
|
6095
|
+
finish(() => resolve(content));
|
|
6073
6096
|
});
|
|
6074
6097
|
});
|
|
6075
6098
|
}
|
|
@@ -6691,6 +6714,8 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
6691
6714
|
userCtx,
|
|
6692
6715
|
envOverlay: runEnv,
|
|
6693
6716
|
emit: (event) => emit({ ...event, nodeId }),
|
|
6717
|
+
signal,
|
|
6718
|
+
onActiveChild: opts.onActiveChild,
|
|
6694
6719
|
});
|
|
6695
6720
|
const normalizedAgentOutput = workspacePublishAgentOutputFiles(workspaceStructuredAgentOutput(content), runPackage);
|
|
6696
6721
|
const resultContent = normalizedAgentOutput.result || content;
|
|
@@ -6787,6 +6812,8 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
6787
6812
|
prompt,
|
|
6788
6813
|
modelKey: nodeModelKey,
|
|
6789
6814
|
agentflowUserId: userCtx.userId || "",
|
|
6815
|
+
detached: process.platform !== "win32",
|
|
6816
|
+
onChild: opts.onActiveChild,
|
|
6790
6817
|
extraEnv: runtimeEnv({
|
|
6791
6818
|
AGENTFLOW_WORKSPACE_TMP_ROOT: runTmpRoot,
|
|
6792
6819
|
AGENTFLOW_NODE_RUN_DIR: runPackage.nodeRunDir,
|
|
@@ -6823,7 +6850,6 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
6823
6850
|
emit({ type: "status", nodeId, line: `工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
|
|
6824
6851
|
},
|
|
6825
6852
|
});
|
|
6826
|
-
if (typeof opts.onActiveChild === "function") opts.onActiveChild(handle.child || null);
|
|
6827
6853
|
emitTiming(nodeId, "spawn-agent", spawnStartedAt, { attempt });
|
|
6828
6854
|
try {
|
|
6829
6855
|
await handle.finished;
|
|
@@ -9026,6 +9052,14 @@ function workspaceRunEntryKey(scopeKey, runId) {
|
|
|
9026
9052
|
return `${scopeKey}:${String(runId || "").trim() || runLedgerId("workspace")}`;
|
|
9027
9053
|
}
|
|
9028
9054
|
|
|
9055
|
+
function workspaceRunControl(abortController) {
|
|
9056
|
+
return createWorkspaceRunController({
|
|
9057
|
+
abortController,
|
|
9058
|
+
gracefulTimeoutMs: 3_000,
|
|
9059
|
+
forceTimeoutMs: 1_500,
|
|
9060
|
+
});
|
|
9061
|
+
}
|
|
9062
|
+
|
|
9029
9063
|
function workspaceRuntimeNodeLabel(graph, nodeId, fallback = "Workspace Run") {
|
|
9030
9064
|
const id = String(nodeId || "").trim();
|
|
9031
9065
|
const instance = graph?.instances && typeof graph.instances === "object" ? graph.instances[id] : null;
|
|
@@ -9463,11 +9497,12 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
9463
9497
|
}
|
|
9464
9498
|
|
|
9465
9499
|
const controller = new AbortController();
|
|
9500
|
+
const runControl = workspaceRunControl(controller);
|
|
9466
9501
|
const runKey = workspaceRunEntryKey(scopeKey, runId);
|
|
9467
9502
|
const runEntry = {
|
|
9468
9503
|
scopeKey,
|
|
9469
9504
|
controller,
|
|
9470
|
-
|
|
9505
|
+
runControl,
|
|
9471
9506
|
runId,
|
|
9472
9507
|
userId: userCtx.userId,
|
|
9473
9508
|
username: String(authUser.username || entry.username || userCtx.userId),
|
|
@@ -9478,11 +9513,6 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
9478
9513
|
plannedNodeIds,
|
|
9479
9514
|
startedAt: Date.now(),
|
|
9480
9515
|
scheduled: true,
|
|
9481
|
-
stopChild() {
|
|
9482
|
-
if (this.child && !this.child.killed) {
|
|
9483
|
-
try { this.child.kill("SIGTERM"); } catch (_) {}
|
|
9484
|
-
}
|
|
9485
|
-
},
|
|
9486
9516
|
};
|
|
9487
9517
|
activeWorkspaceRuns.set(runKey, runEntry);
|
|
9488
9518
|
appendWorkspaceRunStarted(runEntry);
|
|
@@ -9496,9 +9526,8 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
9496
9526
|
timezone: config.timezone,
|
|
9497
9527
|
lastError: "",
|
|
9498
9528
|
});
|
|
9499
|
-
const setActiveChild = (child) => {
|
|
9500
|
-
|
|
9501
|
-
if (controller.signal.aborted) runEntry.stopChild();
|
|
9529
|
+
const setActiveChild = (child, childOptions = {}) => {
|
|
9530
|
+
runControl.setChild(child, childOptions);
|
|
9502
9531
|
};
|
|
9503
9532
|
try {
|
|
9504
9533
|
const result = await runWorkspaceGraph(root, scoped.root, {
|
|
@@ -9531,23 +9560,28 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
9531
9560
|
} catch (e) {
|
|
9532
9561
|
const endedAt = Date.now();
|
|
9533
9562
|
const error = (e && e.message) || String(e);
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9563
|
+
const stopped = isWorkspaceRunAbortError(e) || controller.signal.aborted;
|
|
9564
|
+
const finalStatus = stopped ? "stopped" : "failed";
|
|
9565
|
+
appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, finalStatus);
|
|
9566
|
+
appendWorkspaceRunLogEvent(runLog.runId, stopped
|
|
9567
|
+
? { type: "stopped", message: "Workspace run stopped", ts: endedAt }
|
|
9568
|
+
: { type: "error", error, ts: endedAt });
|
|
9569
|
+
finishWorkspaceRunLogSession(runLog.runId, finalStatus, {
|
|
9537
9570
|
endedAt,
|
|
9538
9571
|
durationMs: endedAt - runEntry.startedAt,
|
|
9539
9572
|
runNodeId: targetRunNodeId,
|
|
9540
|
-
error,
|
|
9573
|
+
error: stopped ? "" : error,
|
|
9541
9574
|
});
|
|
9542
9575
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
9543
9576
|
nextRunAt: computeNext(config),
|
|
9544
9577
|
lastFinishedAt: endedAt,
|
|
9545
|
-
lastStatus:
|
|
9546
|
-
lastError: error,
|
|
9547
|
-
lastErrorAt: endedAt,
|
|
9578
|
+
lastStatus: finalStatus,
|
|
9579
|
+
lastError: stopped ? "" : error,
|
|
9580
|
+
...(stopped ? {} : { lastErrorAt: endedAt }),
|
|
9548
9581
|
});
|
|
9549
|
-
log.info(`[workspace-scheduler] failed ${entry.flowId}/${targetRunNodeId}: ${error}`);
|
|
9582
|
+
if (!stopped) log.info(`[workspace-scheduler] failed ${entry.flowId}/${targetRunNodeId}: ${error}`);
|
|
9550
9583
|
} finally {
|
|
9584
|
+
runControl.finish(controller.signal.aborted ? "stopped" : "finished");
|
|
9551
9585
|
if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
|
|
9552
9586
|
}
|
|
9553
9587
|
}
|
|
@@ -11516,13 +11550,14 @@ export function startUiServer({
|
|
|
11516
11550
|
return;
|
|
11517
11551
|
}
|
|
11518
11552
|
const controller = new AbortController();
|
|
11553
|
+
const runControl = workspaceRunControl(controller);
|
|
11519
11554
|
const runId = String(payload.runSessionId || payload.runId || "").trim() || runLedgerId("workspace");
|
|
11520
11555
|
const runKey = workspaceRunEntryKey(scopeKey, runId);
|
|
11521
11556
|
const runAlias = String(payload.runAlias || "").trim() || workspaceRuntimeNodeLabel(runtimeGraph, runNodeId, "Workspace Run");
|
|
11522
11557
|
const runEntry = {
|
|
11523
11558
|
scopeKey,
|
|
11524
11559
|
controller,
|
|
11525
|
-
|
|
11560
|
+
runControl,
|
|
11526
11561
|
runId,
|
|
11527
11562
|
userId: String(userCtx.userId || ""),
|
|
11528
11563
|
username: String(authUser?.username || userCtx.userId || ""),
|
|
@@ -11532,11 +11567,6 @@ export function startUiServer({
|
|
|
11532
11567
|
flowSource: scoped.flowSource || payload.flowSource || "user",
|
|
11533
11568
|
plannedNodeIds,
|
|
11534
11569
|
startedAt: Date.now(),
|
|
11535
|
-
stopChild() {
|
|
11536
|
-
if (this.child && !this.child.killed) {
|
|
11537
|
-
try { this.child.kill("SIGTERM"); } catch (_) {}
|
|
11538
|
-
}
|
|
11539
|
-
},
|
|
11540
11570
|
};
|
|
11541
11571
|
const runLog = createWorkspaceRunLogSession({
|
|
11542
11572
|
runId,
|
|
@@ -11553,11 +11583,11 @@ export function startUiServer({
|
|
|
11553
11583
|
});
|
|
11554
11584
|
activeWorkspaceRuns.set(runKey, runEntry);
|
|
11555
11585
|
appendWorkspaceRunStarted(runEntry);
|
|
11556
|
-
const setActiveChild = (child) => {
|
|
11557
|
-
|
|
11558
|
-
if (controller.signal.aborted) runEntry.stopChild();
|
|
11586
|
+
const setActiveChild = (child, childOptions = {}) => {
|
|
11587
|
+
runControl.setChild(child, childOptions);
|
|
11559
11588
|
};
|
|
11560
|
-
const clearActiveRun = () => {
|
|
11589
|
+
const clearActiveRun = (status = "finished") => {
|
|
11590
|
+
runControl.finish(status);
|
|
11561
11591
|
if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
|
|
11562
11592
|
};
|
|
11563
11593
|
if (wantsStream) {
|
|
@@ -11626,7 +11656,7 @@ export function startUiServer({
|
|
|
11626
11656
|
}
|
|
11627
11657
|
res.end();
|
|
11628
11658
|
} finally {
|
|
11629
|
-
clearActiveRun();
|
|
11659
|
+
clearActiveRun(controller.signal.aborted ? "stopped" : "finished");
|
|
11630
11660
|
}
|
|
11631
11661
|
return;
|
|
11632
11662
|
}
|
|
@@ -11684,7 +11714,7 @@ export function startUiServer({
|
|
|
11684
11714
|
throw e;
|
|
11685
11715
|
}
|
|
11686
11716
|
} finally {
|
|
11687
|
-
clearActiveRun();
|
|
11717
|
+
clearActiveRun(controller.signal.aborted ? "stopped" : "finished");
|
|
11688
11718
|
}
|
|
11689
11719
|
} catch (e) {
|
|
11690
11720
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
@@ -11750,6 +11780,7 @@ export function startUiServer({
|
|
|
11750
11780
|
const entry = entries[0] || null;
|
|
11751
11781
|
json(res, 200, {
|
|
11752
11782
|
running: entries.length > 0,
|
|
11783
|
+
state: entry?.runControl?.state || (entries.length > 0 ? "running" : "idle"),
|
|
11753
11784
|
flowId,
|
|
11754
11785
|
flowSource,
|
|
11755
11786
|
runNodeId: entry?.runNodeId || "",
|
|
@@ -11762,6 +11793,7 @@ export function startUiServer({
|
|
|
11762
11793
|
startedAt: item?.startedAt || null,
|
|
11763
11794
|
plannedNodeIds: Array.isArray(item?.plannedNodeIds) ? item.plannedNodeIds : [],
|
|
11764
11795
|
scheduled: item?.scheduled === true,
|
|
11796
|
+
state: item?.runControl?.state || "running",
|
|
11765
11797
|
})),
|
|
11766
11798
|
});
|
|
11767
11799
|
return;
|
|
@@ -11792,9 +11824,38 @@ export function startUiServer({
|
|
|
11792
11824
|
json(res, 404, { error: "该 Workspace 未在运行" });
|
|
11793
11825
|
return;
|
|
11794
11826
|
}
|
|
11795
|
-
|
|
11796
|
-
|
|
11797
|
-
|
|
11827
|
+
appendWorkspaceRunLogEvent(entry.runId, {
|
|
11828
|
+
type: "stop-requested",
|
|
11829
|
+
runNodeId: entry.runNodeId || "",
|
|
11830
|
+
ts: Date.now(),
|
|
11831
|
+
});
|
|
11832
|
+
const result = await entry.runControl.stop();
|
|
11833
|
+
if (!result.stopped) {
|
|
11834
|
+
appendWorkspaceRunLogEvent(entry.runId, {
|
|
11835
|
+
type: "stop-failed",
|
|
11836
|
+
runNodeId: entry.runNodeId || "",
|
|
11837
|
+
reason: result.timedOut ? "timeout" : "unknown",
|
|
11838
|
+
ts: Date.now(),
|
|
11839
|
+
});
|
|
11840
|
+
json(res, 409, {
|
|
11841
|
+
error: "停止请求已发送,但运行进程未能退出",
|
|
11842
|
+
ok: false,
|
|
11843
|
+
stopped: false,
|
|
11844
|
+
state: entry.runControl.state,
|
|
11845
|
+
});
|
|
11846
|
+
return;
|
|
11847
|
+
}
|
|
11848
|
+
appendWorkspaceRunLogEvent(entry.runId, {
|
|
11849
|
+
type: "stop-completed",
|
|
11850
|
+
runNodeId: entry.runNodeId || "",
|
|
11851
|
+
forced: result.forced === true,
|
|
11852
|
+
ts: Date.now(),
|
|
11853
|
+
});
|
|
11854
|
+
json(res, 200, {
|
|
11855
|
+
ok: true,
|
|
11856
|
+
stopped: true,
|
|
11857
|
+
forced: result.forced === true,
|
|
11858
|
+
});
|
|
11798
11859
|
return;
|
|
11799
11860
|
}
|
|
11800
11861
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
function childHasExited(child) {
|
|
2
|
+
return !child || child.exitCode != null || child.signalCode != null;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function waitForPromise(promise, timeoutMs) {
|
|
6
|
+
const ms = Math.max(0, Number(timeoutMs) || 0);
|
|
7
|
+
if (ms === 0) return Promise.resolve({ timedOut: true });
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
let settled = false;
|
|
10
|
+
const timer = setTimeout(() => {
|
|
11
|
+
if (settled) return;
|
|
12
|
+
settled = true;
|
|
13
|
+
resolve({ timedOut: true });
|
|
14
|
+
}, ms);
|
|
15
|
+
Promise.resolve(promise).then((value) => {
|
|
16
|
+
if (settled) return;
|
|
17
|
+
settled = true;
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
resolve({ timedOut: false, value });
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function terminateWorkspaceChild(child, options = {}) {
|
|
25
|
+
if (childHasExited(child)) return false;
|
|
26
|
+
const signal = String(options.signal || "SIGTERM");
|
|
27
|
+
const processGroup = options.processGroup === true && process.platform !== "win32";
|
|
28
|
+
const pid = Number(child?.pid || 0);
|
|
29
|
+
if (processGroup && pid > 0) {
|
|
30
|
+
try {
|
|
31
|
+
process.kill(-pid, signal);
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
// The group may already be gone. Fall back to the direct child.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
return child.kill(signal) !== false;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createWorkspaceRunController(options = {}) {
|
|
45
|
+
const abortController = options.abortController || new AbortController();
|
|
46
|
+
const gracefulTimeoutMs = Math.max(0, Number(options.gracefulTimeoutMs ?? 3_000));
|
|
47
|
+
const forceTimeoutMs = Math.max(0, Number(options.forceTimeoutMs ?? 1_000));
|
|
48
|
+
let activeChild = null;
|
|
49
|
+
let activeChildProcessGroup = false;
|
|
50
|
+
let state = "running";
|
|
51
|
+
let finished = false;
|
|
52
|
+
let finishResult = null;
|
|
53
|
+
let resolveDone;
|
|
54
|
+
let stopPromise = null;
|
|
55
|
+
const done = new Promise((resolve) => {
|
|
56
|
+
resolveDone = resolve;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const setChild = (child, childOptions = {}) => {
|
|
60
|
+
activeChild = child || null;
|
|
61
|
+
activeChildProcessGroup = Boolean(child && childOptions.processGroup === true);
|
|
62
|
+
if (state === "stopping" && activeChild) {
|
|
63
|
+
terminateWorkspaceChild(activeChild, {
|
|
64
|
+
signal: "SIGTERM",
|
|
65
|
+
processGroup: activeChildProcessGroup,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const finish = (status = "finished") => {
|
|
71
|
+
if (finished) return finishResult;
|
|
72
|
+
finished = true;
|
|
73
|
+
state = state === "stopping" ? "stopped" : String(status || "finished");
|
|
74
|
+
finishResult = { status: state };
|
|
75
|
+
activeChild = null;
|
|
76
|
+
activeChildProcessGroup = false;
|
|
77
|
+
resolveDone(finishResult);
|
|
78
|
+
return finishResult;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const stop = () => {
|
|
82
|
+
if (stopPromise) return stopPromise;
|
|
83
|
+
stopPromise = (async () => {
|
|
84
|
+
if (finished) return { stopped: true, alreadyFinished: true, status: state };
|
|
85
|
+
state = "stopping";
|
|
86
|
+
try {
|
|
87
|
+
abortController.abort();
|
|
88
|
+
} catch {
|
|
89
|
+
// Best effort; process termination below is authoritative.
|
|
90
|
+
}
|
|
91
|
+
terminateWorkspaceChild(activeChild, {
|
|
92
|
+
signal: "SIGTERM",
|
|
93
|
+
processGroup: activeChildProcessGroup,
|
|
94
|
+
});
|
|
95
|
+
let waited = await waitForPromise(done, gracefulTimeoutMs);
|
|
96
|
+
if (!waited.timedOut) return { stopped: true, forced: false, status: waited.value?.status || state };
|
|
97
|
+
|
|
98
|
+
terminateWorkspaceChild(activeChild, {
|
|
99
|
+
signal: "SIGKILL",
|
|
100
|
+
processGroup: activeChildProcessGroup,
|
|
101
|
+
});
|
|
102
|
+
waited = await waitForPromise(done, forceTimeoutMs);
|
|
103
|
+
if (!waited.timedOut) return { stopped: true, forced: true, status: waited.value?.status || state };
|
|
104
|
+
return { stopped: false, forced: true, timedOut: true, status: state };
|
|
105
|
+
})();
|
|
106
|
+
return stopPromise;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
abortController,
|
|
111
|
+
done,
|
|
112
|
+
finish,
|
|
113
|
+
setChild,
|
|
114
|
+
stop,
|
|
115
|
+
get child() {
|
|
116
|
+
return activeChild;
|
|
117
|
+
},
|
|
118
|
+
get state() {
|
|
119
|
+
return state;
|
|
120
|
+
},
|
|
121
|
+
get finished() {
|
|
122
|
+
return finished;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|