@xcanwin/manyoyo 5.6.7 → 5.6.9
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/lib/agent-resume.js +10 -1
- package/lib/codex-output.js +25 -0
- package/lib/dev-release.js +2 -20
- package/lib/web/frontend/app.css +10 -0
- package/lib/web/frontend/app.html +4 -1
- package/lib/web/frontend/app.js +310 -26
- package/lib/web/server.js +513 -85
- package/package.json +1 -1
package/lib/agent-resume.js
CHANGED
|
@@ -16,6 +16,8 @@ const AGENT_PROMPT_TEMPLATE_MAP = {
|
|
|
16
16
|
opencode: 'opencode run {prompt}'
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
const CODEX_DANGEROUS_FLAG = '--dangerously-bypass-approvals-and-sandbox';
|
|
20
|
+
|
|
19
21
|
function stripLeadingAssignments(commandText) {
|
|
20
22
|
let rest = String(commandText || '').trim();
|
|
21
23
|
const assignmentPattern = /^(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)(?:\s+|$)/;
|
|
@@ -75,8 +77,15 @@ function resolveAgentResumeArg(commandText) {
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
function resolveAgentPromptCommandTemplate(commandText) {
|
|
80
|
+
const normalizedCommand = String(commandText || '').trim();
|
|
78
81
|
const program = resolveAgentProgram(commandText);
|
|
79
|
-
|
|
82
|
+
const template = AGENT_PROMPT_TEMPLATE_MAP[program] || '';
|
|
83
|
+
if (program === 'codex' && template) {
|
|
84
|
+
if (normalizedCommand.includes(CODEX_DANGEROUS_FLAG)) {
|
|
85
|
+
return `codex exec ${CODEX_DANGEROUS_FLAG} --skip-git-repo-check {prompt}`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return template;
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
function buildAgentResumeCommand(commandText) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function extractAgentMessageFromCodexJsonl(text) {
|
|
4
|
+
let lastMessage = '';
|
|
5
|
+
for (const rawLine of String(text || '').split('\n')) {
|
|
6
|
+
const line = rawLine.trim();
|
|
7
|
+
if (!line) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
let payload;
|
|
11
|
+
try {
|
|
12
|
+
payload = JSON.parse(line);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (payload && payload.type === 'item.completed' && payload.item && payload.item.type === 'agent_message') {
|
|
17
|
+
lastMessage = String(payload.item.text || '');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return lastMessage.trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
extractAgentMessageFromCodexJsonl
|
|
25
|
+
};
|
package/lib/dev-release.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { extractAgentMessageFromCodexJsonl } = require('./codex-output');
|
|
2
|
+
|
|
1
3
|
function parseReleaseVersion(version) {
|
|
2
4
|
const match = String(version || '').trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
3
5
|
if (!match) {
|
|
@@ -123,26 +125,6 @@ function normalizeCommitMessage(text) {
|
|
|
123
125
|
return lines.slice(start, end).join('\n').trim();
|
|
124
126
|
}
|
|
125
127
|
|
|
126
|
-
function extractAgentMessageFromCodexJsonl(text) {
|
|
127
|
-
let lastMessage = '';
|
|
128
|
-
for (const rawLine of String(text || '').split('\n')) {
|
|
129
|
-
const line = rawLine.trim();
|
|
130
|
-
if (!line) {
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
let payload;
|
|
134
|
-
try {
|
|
135
|
-
payload = JSON.parse(line);
|
|
136
|
-
} catch (error) {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
if (payload && payload.type === 'item.completed' && payload.item && payload.item.type === 'agent_message') {
|
|
140
|
-
lastMessage = String(payload.item.text || '');
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return lastMessage.trim();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
128
|
module.exports = {
|
|
147
129
|
parseReleaseVersion,
|
|
148
130
|
compareReleaseVersions,
|
package/lib/web/frontend/app.css
CHANGED
|
@@ -1079,6 +1079,12 @@ body.command-mode .msg.origin-agent .bubble {
|
|
|
1079
1079
|
gap: 10px;
|
|
1080
1080
|
}
|
|
1081
1081
|
|
|
1082
|
+
.composer-actions {
|
|
1083
|
+
display: flex;
|
|
1084
|
+
flex-direction: column;
|
|
1085
|
+
gap: 8px;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1082
1088
|
#commandInput {
|
|
1083
1089
|
width: 100%;
|
|
1084
1090
|
min-height: 116px;
|
|
@@ -1098,6 +1104,10 @@ body.command-mode .msg.origin-agent .bubble {
|
|
|
1098
1104
|
min-width: 92px;
|
|
1099
1105
|
}
|
|
1100
1106
|
|
|
1107
|
+
#stopBtn {
|
|
1108
|
+
min-width: 92px;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1101
1111
|
.composer-foot {
|
|
1102
1112
|
margin-top: 8px;
|
|
1103
1113
|
display: flex;
|
|
@@ -108,7 +108,10 @@
|
|
|
108
108
|
</div>
|
|
109
109
|
<div class="composer-inner">
|
|
110
110
|
<textarea id="commandInput" placeholder="输入容器命令,例如: ls -la"></textarea>
|
|
111
|
-
<
|
|
111
|
+
<div class="composer-actions">
|
|
112
|
+
<button type="submit" id="sendBtn">发送</button>
|
|
113
|
+
<button type="button" id="stopBtn" class="danger-outline">停止</button>
|
|
114
|
+
</div>
|
|
112
115
|
</div>
|
|
113
116
|
<div class="composer-foot">
|
|
114
117
|
<span id="composerHint">Enter 发送 · Shift/Alt + Enter 换行</span>
|
package/lib/web/frontend/app.js
CHANGED
|
@@ -64,6 +64,13 @@
|
|
|
64
64
|
sessionNodeMap: new Map(),
|
|
65
65
|
sessionRenderMode: 'empty',
|
|
66
66
|
messageRequestId: 0,
|
|
67
|
+
agentRun: {
|
|
68
|
+
active: false,
|
|
69
|
+
sessionName: '',
|
|
70
|
+
stopping: false,
|
|
71
|
+
controller: null,
|
|
72
|
+
traceMessageId: ''
|
|
73
|
+
},
|
|
67
74
|
terminal: {
|
|
68
75
|
term: null,
|
|
69
76
|
fitAddon: null,
|
|
@@ -142,6 +149,7 @@
|
|
|
142
149
|
const composerHint = document.getElementById('composerHint');
|
|
143
150
|
const sendState = document.getElementById('sendState');
|
|
144
151
|
const sendBtn = document.getElementById('sendBtn');
|
|
152
|
+
const stopBtn = document.getElementById('stopBtn');
|
|
145
153
|
const refreshBtn = document.getElementById('refreshBtn');
|
|
146
154
|
const removeBtn = document.getElementById('removeBtn');
|
|
147
155
|
const removeAllBtn = document.getElementById('removeAllBtn');
|
|
@@ -185,6 +193,9 @@
|
|
|
185
193
|
function roleName(role, message) {
|
|
186
194
|
if (role === 'user') return '我';
|
|
187
195
|
if (role === 'assistant') {
|
|
196
|
+
if (message && message.streamTrace) {
|
|
197
|
+
return 'AGENT 过程';
|
|
198
|
+
}
|
|
188
199
|
if (message && message.mode === 'agent') {
|
|
189
200
|
return 'AGENT 回复';
|
|
190
201
|
}
|
|
@@ -357,7 +368,11 @@
|
|
|
357
368
|
|
|
358
369
|
function resolveAgentPromptTemplate(commandText) {
|
|
359
370
|
const program = resolveAgentProgram(commandText);
|
|
360
|
-
|
|
371
|
+
const template = AGENT_PROMPT_TEMPLATE_MAP[program] || '';
|
|
372
|
+
if (program === 'codex' && String(commandText || '').includes('--dangerously-bypass-approvals-and-sandbox')) {
|
|
373
|
+
return 'codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check {prompt}';
|
|
374
|
+
}
|
|
375
|
+
return template;
|
|
361
376
|
}
|
|
362
377
|
|
|
363
378
|
function inferCreateAgentPromptCommand() {
|
|
@@ -541,6 +556,15 @@
|
|
|
541
556
|
}) || null;
|
|
542
557
|
}
|
|
543
558
|
|
|
559
|
+
function isAgentRunActiveForSession(sessionName) {
|
|
560
|
+
return Boolean(
|
|
561
|
+
state.agentRun
|
|
562
|
+
&& state.agentRun.active
|
|
563
|
+
&& state.agentRun.sessionName
|
|
564
|
+
&& state.agentRun.sessionName === String(sessionName || '').trim()
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
544
568
|
function isActiveSessionHistoryOnly() {
|
|
545
569
|
const session = getActiveSession();
|
|
546
570
|
return sessionStatusInfo(session && session.status).tone === 'history';
|
|
@@ -1187,11 +1211,15 @@
|
|
|
1187
1211
|
scheduleTerminalFit(false);
|
|
1188
1212
|
}
|
|
1189
1213
|
|
|
1214
|
+
const activeAgentRunning = isAgentRunActiveForSession(state.active);
|
|
1190
1215
|
const busy = state.loadingSessions || state.loadingMessages || state.sending;
|
|
1191
1216
|
refreshBtn.disabled = busy;
|
|
1192
1217
|
removeBtn.disabled = !state.active || busy;
|
|
1193
1218
|
removeAllBtn.disabled = !state.active || busy;
|
|
1194
1219
|
sendBtn.disabled = !activityTab || !state.active || busy || (agentMode && !agentEnabled);
|
|
1220
|
+
if (stopBtn) {
|
|
1221
|
+
stopBtn.disabled = !activityTab || !agentMode || !activeAgentRunning || state.agentRun.stopping;
|
|
1222
|
+
}
|
|
1195
1223
|
commandInput.disabled = !activityTab || !state.active || (agentMode && !agentEnabled);
|
|
1196
1224
|
if (commandInput) {
|
|
1197
1225
|
commandInput.placeholder = agentMode
|
|
@@ -1200,7 +1228,7 @@
|
|
|
1200
1228
|
}
|
|
1201
1229
|
if (composerHint) {
|
|
1202
1230
|
composerHint.textContent = agentMode
|
|
1203
|
-
? 'Enter 发送提示词 · Shift/Alt + Enter 换行'
|
|
1231
|
+
? 'Enter 发送提示词 · Shift/Alt + Enter 换行 · 执行中可停止'
|
|
1204
1232
|
: 'Enter 发送 · Shift/Alt + Enter 换行';
|
|
1205
1233
|
}
|
|
1206
1234
|
if (openCreateBtn) {
|
|
@@ -1227,6 +1255,21 @@
|
|
|
1227
1255
|
if (createCancelBtn) {
|
|
1228
1256
|
createCancelBtn.disabled = state.createSubmitting;
|
|
1229
1257
|
}
|
|
1258
|
+
if (sendState) {
|
|
1259
|
+
if (!state.active) {
|
|
1260
|
+
sendState.textContent = '未选择会话';
|
|
1261
|
+
sendState.classList.remove('is-active');
|
|
1262
|
+
} else if (activeAgentRunning && agentMode) {
|
|
1263
|
+
sendState.textContent = state.agentRun.stopping ? '正在停止 Agent…' : 'Agent 执行中';
|
|
1264
|
+
sendState.classList.add('is-active');
|
|
1265
|
+
} else if (busy) {
|
|
1266
|
+
sendState.textContent = '处理中';
|
|
1267
|
+
sendState.classList.add('is-active');
|
|
1268
|
+
} else {
|
|
1269
|
+
sendState.textContent = agentMode ? 'Agent 就绪' : '命令就绪';
|
|
1270
|
+
sendState.classList.remove('is-active');
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1230
1273
|
if (configModal) {
|
|
1231
1274
|
configModal.hidden = !state.configModalOpen;
|
|
1232
1275
|
}
|
|
@@ -1276,6 +1319,80 @@
|
|
|
1276
1319
|
return data;
|
|
1277
1320
|
}
|
|
1278
1321
|
|
|
1322
|
+
async function apiStream(url, options, handlers) {
|
|
1323
|
+
const requestOptions = Object.assign(
|
|
1324
|
+
{ headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } },
|
|
1325
|
+
options || {}
|
|
1326
|
+
);
|
|
1327
|
+
const streamHandlers = handlers && typeof handlers === 'object' ? handlers : {};
|
|
1328
|
+
const response = await fetch(url, requestOptions);
|
|
1329
|
+
if (response.status === 401) {
|
|
1330
|
+
window.location.href = '/';
|
|
1331
|
+
throw new Error('未登录或登录已过期');
|
|
1332
|
+
}
|
|
1333
|
+
if (!response.ok) {
|
|
1334
|
+
let errorText = '请求失败';
|
|
1335
|
+
try {
|
|
1336
|
+
const data = await response.json();
|
|
1337
|
+
errorText = data && data.detail ? `${data.error || '请求失败'}: ${data.detail}` : (data.error || '请求失败');
|
|
1338
|
+
} catch (e) {
|
|
1339
|
+
errorText = '请求失败';
|
|
1340
|
+
}
|
|
1341
|
+
throw new Error(errorText);
|
|
1342
|
+
}
|
|
1343
|
+
if (!response.body || typeof response.body.getReader !== 'function') {
|
|
1344
|
+
throw new Error('当前浏览器不支持流式读取');
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
const decoder = new window.TextDecoder();
|
|
1348
|
+
const reader = response.body.getReader();
|
|
1349
|
+
let pending = '';
|
|
1350
|
+
|
|
1351
|
+
while (true) {
|
|
1352
|
+
const result = await reader.read();
|
|
1353
|
+
if (result.done) {
|
|
1354
|
+
break;
|
|
1355
|
+
}
|
|
1356
|
+
pending += decoder.decode(result.value, { stream: true });
|
|
1357
|
+
const lines = pending.split('\n');
|
|
1358
|
+
pending = lines.pop() || '';
|
|
1359
|
+
lines.forEach(function (line) {
|
|
1360
|
+
const text = String(line || '').trim();
|
|
1361
|
+
if (!text) {
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
let payload = null;
|
|
1365
|
+
try {
|
|
1366
|
+
payload = JSON.parse(text);
|
|
1367
|
+
} catch (e) {
|
|
1368
|
+
payload = null;
|
|
1369
|
+
}
|
|
1370
|
+
if (!payload) {
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
if (typeof streamHandlers.onEvent === 'function') {
|
|
1374
|
+
streamHandlers.onEvent(payload);
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
const rest = decoder.decode();
|
|
1380
|
+
if (rest) {
|
|
1381
|
+
pending += rest;
|
|
1382
|
+
}
|
|
1383
|
+
const finalText = String(pending || '').trim();
|
|
1384
|
+
if (finalText) {
|
|
1385
|
+
try {
|
|
1386
|
+
const payload = JSON.parse(finalText);
|
|
1387
|
+
if (typeof streamHandlers.onEvent === 'function') {
|
|
1388
|
+
streamHandlers.onEvent(payload);
|
|
1389
|
+
}
|
|
1390
|
+
} catch (e) {
|
|
1391
|
+
// ignore trailing non-json fragments
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1279
1396
|
async function fetchConfigSnapshot() {
|
|
1280
1397
|
const snapshot = await api('/api/config');
|
|
1281
1398
|
state.configSnapshot = snapshot;
|
|
@@ -1595,6 +1712,11 @@
|
|
|
1595
1712
|
}
|
|
1596
1713
|
|
|
1597
1714
|
function getMessageRenderKey(msg, index) {
|
|
1715
|
+
if (msg && msg.id && msg.streamTrace) {
|
|
1716
|
+
const content = msg.content ? String(msg.content) : '';
|
|
1717
|
+
const timestamp = msg.timestamp ? String(msg.timestamp) : '';
|
|
1718
|
+
return `id:${msg.id}|trace|${timestamp}|${content}`;
|
|
1719
|
+
}
|
|
1598
1720
|
if (msg && msg.id) {
|
|
1599
1721
|
return `id:${msg.id}`;
|
|
1600
1722
|
}
|
|
@@ -1641,7 +1763,7 @@
|
|
|
1641
1763
|
const bubble = document.createElement('div');
|
|
1642
1764
|
bubble.className = 'bubble';
|
|
1643
1765
|
|
|
1644
|
-
const shouldRenderMarkdown = Boolean(markdownRenderer && markdownRenderer.shouldRenderMessage(msg));
|
|
1766
|
+
const shouldRenderMarkdown = Boolean(!msg.streamTrace && markdownRenderer && markdownRenderer.shouldRenderMessage(msg));
|
|
1645
1767
|
if (shouldRenderMarkdown) {
|
|
1646
1768
|
const markdownNode = document.createElement('div');
|
|
1647
1769
|
markdownNode.className = 'md-content';
|
|
@@ -1941,6 +2063,46 @@
|
|
|
1941
2063
|
return -1;
|
|
1942
2064
|
}
|
|
1943
2065
|
|
|
2066
|
+
function appendAgentTraceMessageLocal(sessionName) {
|
|
2067
|
+
const traceMessage = {
|
|
2068
|
+
id: createLocalMessageId('local-agent-trace'),
|
|
2069
|
+
role: 'assistant',
|
|
2070
|
+
content: '[执行过程]\n等待 Agent 启动…',
|
|
2071
|
+
timestamp: new Date().toISOString(),
|
|
2072
|
+
mode: 'agent',
|
|
2073
|
+
streamTrace: true
|
|
2074
|
+
};
|
|
2075
|
+
if (state.active === sessionName) {
|
|
2076
|
+
state.messages.push(traceMessage);
|
|
2077
|
+
}
|
|
2078
|
+
return traceMessage.id;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
function updateAgentTraceMessageLocal(sessionName, traceMessageId, content) {
|
|
2082
|
+
if (state.active !== sessionName) {
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
for (let i = state.messages.length - 1; i >= 0; i -= 1) {
|
|
2086
|
+
const message = state.messages[i];
|
|
2087
|
+
if (!message || String(message.id || '') !== String(traceMessageId || '')) {
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
message.content = String(content || '');
|
|
2091
|
+
message.timestamp = new Date().toISOString();
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
function finalizeAgentRunState() {
|
|
2097
|
+
if (state.agentRun && state.agentRun.controller) {
|
|
2098
|
+
state.agentRun.controller = null;
|
|
2099
|
+
}
|
|
2100
|
+
state.agentRun.active = false;
|
|
2101
|
+
state.agentRun.stopping = false;
|
|
2102
|
+
state.agentRun.sessionName = '';
|
|
2103
|
+
state.agentRun.traceMessageId = '';
|
|
2104
|
+
}
|
|
2105
|
+
|
|
1944
2106
|
function appendAssistantMessageLocal(sessionName, result, mode) {
|
|
1945
2107
|
if (state.active !== sessionName) {
|
|
1946
2108
|
return;
|
|
@@ -1957,6 +2119,109 @@
|
|
|
1957
2119
|
});
|
|
1958
2120
|
}
|
|
1959
2121
|
|
|
2122
|
+
async function sendAgentPromptStream(sessionName, inputText, pendingMessage) {
|
|
2123
|
+
const traceMessageId = appendAgentTraceMessageLocal(sessionName);
|
|
2124
|
+
const traceLines = ['[执行过程]', '等待 Agent 启动…'];
|
|
2125
|
+
let finalResult = null;
|
|
2126
|
+
let streamError = null;
|
|
2127
|
+
|
|
2128
|
+
state.agentRun.active = true;
|
|
2129
|
+
state.agentRun.sessionName = sessionName;
|
|
2130
|
+
state.agentRun.stopping = false;
|
|
2131
|
+
state.agentRun.controller = new window.AbortController();
|
|
2132
|
+
state.agentRun.traceMessageId = traceMessageId;
|
|
2133
|
+
renderMessages(state.messages, { stickToBottom: true });
|
|
2134
|
+
syncUi();
|
|
2135
|
+
|
|
2136
|
+
function pushTraceLine(text) {
|
|
2137
|
+
const line = String(text || '').trim();
|
|
2138
|
+
if (!line) {
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
if (traceLines[traceLines.length - 1] === line) {
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
traceLines.push(line);
|
|
2145
|
+
updateAgentTraceMessageLocal(sessionName, traceMessageId, traceLines.join('\n'));
|
|
2146
|
+
if (state.active === sessionName) {
|
|
2147
|
+
renderMessages(state.messages, { stickToBottom: true });
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
try {
|
|
2152
|
+
await apiStream('/api/sessions/' + encodeURIComponent(sessionName) + '/agent/stream', {
|
|
2153
|
+
method: 'POST',
|
|
2154
|
+
body: JSON.stringify({ prompt: inputText }),
|
|
2155
|
+
signal: state.agentRun.controller.signal
|
|
2156
|
+
}, {
|
|
2157
|
+
onEvent: function (event) {
|
|
2158
|
+
if (!event || typeof event !== 'object') {
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
if (event.type === 'meta') {
|
|
2162
|
+
const contextMode = String(event.contextMode || '').trim();
|
|
2163
|
+
const modeLabel = contextMode ? '上下文模式: ' + contextMode : '';
|
|
2164
|
+
if (modeLabel) {
|
|
2165
|
+
pushTraceLine(modeLabel);
|
|
2166
|
+
}
|
|
2167
|
+
if (event.resumeAttempted) {
|
|
2168
|
+
pushTraceLine(event.resumeSucceeded ? '会话恢复成功' : '会话恢复失败,已回退到历史注入');
|
|
2169
|
+
}
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
if (event.type === 'trace') {
|
|
2173
|
+
pushTraceLine(event.text || '');
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
if (event.type === 'result') {
|
|
2177
|
+
finalResult = event;
|
|
2178
|
+
if (event.interrupted) {
|
|
2179
|
+
pushTraceLine('[任务] 已停止');
|
|
2180
|
+
} else {
|
|
2181
|
+
pushTraceLine('[任务] 已完成');
|
|
2182
|
+
}
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
if (event.type === 'error') {
|
|
2186
|
+
streamError = new Error(event.error || 'Agent 执行失败');
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
});
|
|
2190
|
+
} catch (e) {
|
|
2191
|
+
if (!(e && e.name === 'AbortError')) {
|
|
2192
|
+
streamError = e;
|
|
2193
|
+
}
|
|
2194
|
+
} finally {
|
|
2195
|
+
const pendingIndex = confirmPendingUserMessage(sessionName, pendingMessage.id);
|
|
2196
|
+
if (pendingIndex >= 0 && pendingIndex < state.messageRenderKeys.length) {
|
|
2197
|
+
if (pendingIndex < messagesNode.children.length) {
|
|
2198
|
+
const pendingRow = messagesNode.children[pendingIndex];
|
|
2199
|
+
if (pendingRow && pendingRow.classList.contains('pending')) {
|
|
2200
|
+
pendingRow.classList.remove('pending');
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
finalizeAgentRunState();
|
|
2205
|
+
syncUi();
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
if (streamError) {
|
|
2209
|
+
throw streamError;
|
|
2210
|
+
}
|
|
2211
|
+
if (!finalResult) {
|
|
2212
|
+
throw new Error('Agent 流式响应未返回结果');
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
appendAssistantMessageLocal(sessionName, finalResult, 'agent');
|
|
2216
|
+
if (state.active === sessionName) {
|
|
2217
|
+
renderMessages(state.messages, { stickToBottom: true });
|
|
2218
|
+
}
|
|
2219
|
+
bumpSessionMetaAfterSend(sessionName);
|
|
2220
|
+
refreshSessionsSilent({ preferredName: sessionName }).catch(function () {
|
|
2221
|
+
// 静默同步失败不打断当前交互
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
|
|
1960
2225
|
if (openConfigBtn) {
|
|
1961
2226
|
openConfigBtn.addEventListener('click', function () {
|
|
1962
2227
|
openConfigModal();
|
|
@@ -2092,32 +2357,31 @@
|
|
|
2092
2357
|
try {
|
|
2093
2358
|
commandInput.value = '';
|
|
2094
2359
|
commandInput.focus();
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
)
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
pendingRow.classList.remove('pending');
|
|
2360
|
+
if (mode === 'agent') {
|
|
2361
|
+
await sendAgentPromptStream(submitSession, inputText, pendingMessage);
|
|
2362
|
+
} else {
|
|
2363
|
+
const runResult = await api('/api/sessions/' + encodeURIComponent(submitSession) + '/run', {
|
|
2364
|
+
method: 'POST',
|
|
2365
|
+
body: JSON.stringify({ command: inputText })
|
|
2366
|
+
});
|
|
2367
|
+
const pendingIndex = confirmPendingUserMessage(submitSession, pendingMessage.id);
|
|
2368
|
+
if (pendingIndex >= 0 && pendingIndex < state.messageRenderKeys.length) {
|
|
2369
|
+
if (pendingIndex < messagesNode.children.length) {
|
|
2370
|
+
const pendingRow = messagesNode.children[pendingIndex];
|
|
2371
|
+
if (pendingRow && pendingRow.classList.contains('pending')) {
|
|
2372
|
+
pendingRow.classList.remove('pending');
|
|
2373
|
+
}
|
|
2110
2374
|
}
|
|
2111
2375
|
}
|
|
2376
|
+
appendAssistantMessageLocal(submitSession, runResult, mode);
|
|
2377
|
+
if (state.active === submitSession) {
|
|
2378
|
+
renderMessages(state.messages, { stickToBottom: true });
|
|
2379
|
+
}
|
|
2380
|
+
bumpSessionMetaAfterSend(submitSession);
|
|
2381
|
+
refreshSessionsSilent({ preferredName: submitSession }).catch(function () {
|
|
2382
|
+
// 静默同步失败不打断当前交互
|
|
2383
|
+
});
|
|
2112
2384
|
}
|
|
2113
|
-
appendAssistantMessageLocal(submitSession, runResult, mode);
|
|
2114
|
-
if (state.active === submitSession) {
|
|
2115
|
-
renderMessages(state.messages, { stickToBottom: true });
|
|
2116
|
-
}
|
|
2117
|
-
bumpSessionMetaAfterSend(submitSession);
|
|
2118
|
-
refreshSessionsSilent({ preferredName: submitSession }).catch(function () {
|
|
2119
|
-
// 静默同步失败不打断当前交互
|
|
2120
|
-
});
|
|
2121
2385
|
} catch (e) {
|
|
2122
2386
|
if (state.active === submitSession) {
|
|
2123
2387
|
state.messages = state.messages.filter(function (message) {
|
|
@@ -2133,6 +2397,26 @@
|
|
|
2133
2397
|
}
|
|
2134
2398
|
});
|
|
2135
2399
|
|
|
2400
|
+
if (stopBtn) {
|
|
2401
|
+
stopBtn.addEventListener('click', async function () {
|
|
2402
|
+
if (!state.active || !isAgentRunActiveForSession(state.active) || state.agentRun.stopping) {
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
state.agentRun.stopping = true;
|
|
2406
|
+
syncUi();
|
|
2407
|
+
try {
|
|
2408
|
+
await api('/api/sessions/' + encodeURIComponent(state.active) + '/agent/stop', {
|
|
2409
|
+
method: 'POST',
|
|
2410
|
+
body: JSON.stringify({})
|
|
2411
|
+
});
|
|
2412
|
+
} catch (e) {
|
|
2413
|
+
alert(e.message);
|
|
2414
|
+
state.agentRun.stopping = false;
|
|
2415
|
+
syncUi();
|
|
2416
|
+
}
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2136
2420
|
commandInput.addEventListener('keydown', function (event) {
|
|
2137
2421
|
if (event.key !== 'Enter' || event.isComposing) {
|
|
2138
2422
|
return;
|
package/lib/web/server.js
CHANGED
|
@@ -9,6 +9,7 @@ const http = require('http');
|
|
|
9
9
|
const WebSocket = require('ws');
|
|
10
10
|
const JSON5 = require('json5');
|
|
11
11
|
const { buildContainerRunArgs } = require('../container-run');
|
|
12
|
+
const { extractAgentMessageFromCodexJsonl } = require('../codex-output');
|
|
12
13
|
const {
|
|
13
14
|
resolveAgentProgram,
|
|
14
15
|
resolveAgentPromptCommandTemplate,
|
|
@@ -26,8 +27,6 @@ const WEB_TERMINAL_MIN_ROWS = 12;
|
|
|
26
27
|
const WEB_AGENT_CONTEXT_MAX_MESSAGES = 24;
|
|
27
28
|
const WEB_AGENT_CONTEXT_MAX_CHARS = 6000;
|
|
28
29
|
const WEB_AGENT_CONTEXT_PER_MESSAGE_MAX_CHARS = 600;
|
|
29
|
-
const WEB_AGENT_LAST_MESSAGE_BEGIN_MARKER = '__MANYOYO_LAST_MESSAGE_BEGIN__';
|
|
30
|
-
const WEB_AGENT_LAST_MESSAGE_END_MARKER = '__MANYOYO_LAST_MESSAGE_END__';
|
|
31
30
|
const WEB_AUTH_COOKIE_NAME = 'manyoyo_web_auth';
|
|
32
31
|
const WEB_AUTH_TTL_SECONDS = 12 * 60 * 60;
|
|
33
32
|
const FRONTEND_DIR = path.join(__dirname, 'frontend');
|
|
@@ -282,23 +281,20 @@ function renderAgentPromptCommand(template, prompt) {
|
|
|
282
281
|
|
|
283
282
|
function buildCodexAgentExecCommand(template, prompt) {
|
|
284
283
|
const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
const codexTemplate = templateText.replace(
|
|
288
|
-
/^((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)codex\s+exec\b/,
|
|
289
|
-
`$1codex exec --output-last-message ${quotedOutputFile}`
|
|
284
|
+
const execMatch = templateText.match(
|
|
285
|
+
/^((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)codex\s+exec\b/
|
|
290
286
|
);
|
|
291
|
-
|
|
287
|
+
let codexTemplate = templateText;
|
|
288
|
+
if (execMatch) {
|
|
289
|
+
const prefix = execMatch[1] || '';
|
|
290
|
+
const suffix = templateText.slice(execMatch[0].length);
|
|
291
|
+
const hasJson = /(?:^|\s)--json(?:\s|$)/.test(suffix);
|
|
292
|
+
const injectedFlags = hasJson ? '' : ' --json';
|
|
293
|
+
codexTemplate = `${prefix}codex exec${injectedFlags}${suffix}`;
|
|
294
|
+
}
|
|
295
|
+
return codexTemplate === templateText
|
|
292
296
|
? renderAgentPromptCommand(templateText, prompt)
|
|
293
297
|
: renderAgentPromptCommand(codexTemplate, prompt);
|
|
294
|
-
return [
|
|
295
|
-
`rm -f ${quotedOutputFile}`,
|
|
296
|
-
command,
|
|
297
|
-
'__manyoyo_agent_exit=$?',
|
|
298
|
-
`if [ -f ${quotedOutputFile} ]; then printf '\\n${WEB_AGENT_LAST_MESSAGE_BEGIN_MARKER}\\n'; cat ${quotedOutputFile}; printf '\\n${WEB_AGENT_LAST_MESSAGE_END_MARKER}\\n'; fi`,
|
|
299
|
-
`rm -f ${quotedOutputFile}`,
|
|
300
|
-
'exit $__manyoyo_agent_exit'
|
|
301
|
-
].join('; ');
|
|
302
298
|
}
|
|
303
299
|
|
|
304
300
|
function buildWebAgentExecCommand(template, prompt, agentProgram) {
|
|
@@ -308,21 +304,6 @@ function buildWebAgentExecCommand(template, prompt, agentProgram) {
|
|
|
308
304
|
return renderAgentPromptCommand(template, prompt);
|
|
309
305
|
}
|
|
310
306
|
|
|
311
|
-
function extractLastMessageOutput(text) {
|
|
312
|
-
const raw = String(text || '');
|
|
313
|
-
const pattern = new RegExp(
|
|
314
|
-
`(?:^|\\r?\\n)${WEB_AGENT_LAST_MESSAGE_BEGIN_MARKER}\\r?\\n([\\s\\S]*?)(?:\\r?\\n)${WEB_AGENT_LAST_MESSAGE_END_MARKER}(?:\\r?\\n|$)`,
|
|
315
|
-
'g'
|
|
316
|
-
);
|
|
317
|
-
let lastMatch = null;
|
|
318
|
-
let matched = pattern.exec(raw);
|
|
319
|
-
while (matched) {
|
|
320
|
-
lastMatch = matched[1];
|
|
321
|
-
matched = pattern.exec(raw);
|
|
322
|
-
}
|
|
323
|
-
return String(lastMatch || '').trim();
|
|
324
|
-
}
|
|
325
|
-
|
|
326
307
|
function getAgentRuntimeMeta(history) {
|
|
327
308
|
const sessionHistory = history && typeof history === 'object' ? history : {};
|
|
328
309
|
const template = normalizeAgentPromptCommandTemplate(sessionHistory.agentPromptCommand, 'agentPromptCommand');
|
|
@@ -340,6 +321,7 @@ function hasAgentConversationHistory(history) {
|
|
|
340
321
|
for (const message of messages) {
|
|
341
322
|
if (!message || typeof message !== 'object') continue;
|
|
342
323
|
if (message.mode !== 'agent') continue;
|
|
324
|
+
if (message.streamTrace === true) continue;
|
|
343
325
|
if (message.role === 'user' || message.role === 'assistant') {
|
|
344
326
|
return true;
|
|
345
327
|
}
|
|
@@ -358,7 +340,12 @@ function clipAgentContextMessageText(text) {
|
|
|
358
340
|
function buildAgentPromptWithHistory(history, prompt) {
|
|
359
341
|
const sessionHistory = history && Array.isArray(history.messages) ? history.messages : [];
|
|
360
342
|
const relevantMessages = sessionHistory
|
|
361
|
-
.filter(message =>
|
|
343
|
+
.filter(message => (
|
|
344
|
+
message
|
|
345
|
+
&& message.mode === 'agent'
|
|
346
|
+
&& message.streamTrace !== true
|
|
347
|
+
&& (message.role === 'user' || message.role === 'assistant')
|
|
348
|
+
))
|
|
362
349
|
.slice(-WEB_AGENT_CONTEXT_MAX_MESSAGES);
|
|
363
350
|
if (!relevantMessages.length) {
|
|
364
351
|
return String(prompt || '');
|
|
@@ -389,6 +376,198 @@ function buildAgentPromptWithHistory(history, prompt) {
|
|
|
389
376
|
].join('\n');
|
|
390
377
|
}
|
|
391
378
|
|
|
379
|
+
function prepareCodexTraceDisplayLine(payload) {
|
|
380
|
+
if (!payload || typeof payload !== 'object') {
|
|
381
|
+
return '';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const eventType = typeof payload.type === 'string' ? payload.type : '';
|
|
385
|
+
const item = payload.item && typeof payload.item === 'object' && !Array.isArray(payload.item)
|
|
386
|
+
? payload.item
|
|
387
|
+
: {};
|
|
388
|
+
const itemType = typeof item.type === 'string' ? item.type : '';
|
|
389
|
+
const text = pickFirstString(
|
|
390
|
+
item.title,
|
|
391
|
+
item.summary,
|
|
392
|
+
item.text,
|
|
393
|
+
item.name,
|
|
394
|
+
item.command,
|
|
395
|
+
payload.message,
|
|
396
|
+
payload.text
|
|
397
|
+
);
|
|
398
|
+
const toolName = pickFirstString(
|
|
399
|
+
item.name,
|
|
400
|
+
item.tool_name,
|
|
401
|
+
item.tool,
|
|
402
|
+
item.command
|
|
403
|
+
);
|
|
404
|
+
const commandText = pickFirstString(item.command);
|
|
405
|
+
const mcpServer = pickFirstString(item.server);
|
|
406
|
+
const mcpTool = pickFirstString(item.tool);
|
|
407
|
+
const itemStatus = pickFirstString(item.status);
|
|
408
|
+
|
|
409
|
+
function shortenText(value, maxChars = 140) {
|
|
410
|
+
const raw = clipText(stripAnsi(String(value || '')).replace(/\s+/g, ' ').trim(), maxChars);
|
|
411
|
+
return raw.trim();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function summarizeArguments(args) {
|
|
415
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
416
|
+
return '';
|
|
417
|
+
}
|
|
418
|
+
const parts = [];
|
|
419
|
+
for (const [key, value] of Object.entries(args)) {
|
|
420
|
+
if (value === undefined || value === null) continue;
|
|
421
|
+
if (typeof value === 'string') {
|
|
422
|
+
const textValue = value.trim();
|
|
423
|
+
if (!textValue) continue;
|
|
424
|
+
parts.push(`${key}=${shortenText(textValue, 80)}`);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
428
|
+
parts.push(`${key}=${String(value)}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return parts.slice(0, 3).join(', ');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (eventType === 'thread.started') {
|
|
435
|
+
return '[会话] Codex 已开始处理';
|
|
436
|
+
}
|
|
437
|
+
if (eventType === 'thread.completed') {
|
|
438
|
+
return '[会话] Codex 已完成当前任务';
|
|
439
|
+
}
|
|
440
|
+
if (eventType === 'turn.started') {
|
|
441
|
+
return '[回合] 开始生成响应';
|
|
442
|
+
}
|
|
443
|
+
if (eventType === 'turn.completed') {
|
|
444
|
+
return '[回合] 响应完成';
|
|
445
|
+
}
|
|
446
|
+
if (eventType === 'item.started') {
|
|
447
|
+
if (itemType === 'tool_call') {
|
|
448
|
+
return `[工具开始] ${toolName || 'tool_call'}`;
|
|
449
|
+
}
|
|
450
|
+
if (itemType === 'command_execution') {
|
|
451
|
+
return `[命令开始] ${commandText || 'command_execution'}`;
|
|
452
|
+
}
|
|
453
|
+
if (itemType === 'mcp_tool_call') {
|
|
454
|
+
const summary = summarizeArguments(item.arguments);
|
|
455
|
+
return summary
|
|
456
|
+
? `[MCP开始] ${mcpServer || 'mcp'}.${mcpTool || 'tool'} (${summary})`
|
|
457
|
+
: `[MCP开始] ${mcpServer || 'mcp'}.${mcpTool || 'tool'}`;
|
|
458
|
+
}
|
|
459
|
+
if (itemType === 'reasoning') {
|
|
460
|
+
return text ? `[状态] ${text}` : '[状态] Codex 正在分析';
|
|
461
|
+
}
|
|
462
|
+
if (itemType === 'agent_message') {
|
|
463
|
+
return text ? `[说明] ${text}` : '[回复] 正在生成最终答复';
|
|
464
|
+
}
|
|
465
|
+
return text ? `[事件开始] ${text}` : `[事件开始] ${itemType || eventType}`;
|
|
466
|
+
}
|
|
467
|
+
if (eventType === 'item.completed') {
|
|
468
|
+
if (itemType === 'tool_call') {
|
|
469
|
+
return `[工具完成] ${toolName || 'tool_call'}`;
|
|
470
|
+
}
|
|
471
|
+
if (itemType === 'command_execution') {
|
|
472
|
+
const suffix = itemStatus || (typeof item.exit_code === 'number' ? `exit=${item.exit_code}` : 'completed');
|
|
473
|
+
return `[命令完成] ${commandText || 'command_execution'} (${suffix})`;
|
|
474
|
+
}
|
|
475
|
+
if (itemType === 'mcp_tool_call') {
|
|
476
|
+
const summary = summarizeArguments(item.arguments);
|
|
477
|
+
return summary
|
|
478
|
+
? `[MCP完成] ${mcpServer || 'mcp'}.${mcpTool || 'tool'} (${summary})`
|
|
479
|
+
: `[MCP完成] ${mcpServer || 'mcp'}.${mcpTool || 'tool'}`;
|
|
480
|
+
}
|
|
481
|
+
if (itemType === 'reasoning') {
|
|
482
|
+
return text ? `[状态] ${text}` : '';
|
|
483
|
+
}
|
|
484
|
+
if (itemType === 'agent_message') {
|
|
485
|
+
return text ? `[说明] ${text}` : '[回复] 已生成';
|
|
486
|
+
}
|
|
487
|
+
return text ? `[事件完成] ${text}` : `[事件完成] ${itemType || eventType}`;
|
|
488
|
+
}
|
|
489
|
+
if (eventType === 'error') {
|
|
490
|
+
return text ? `[错误] ${text}` : '[错误] Codex 返回了错误事件';
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return `[事件] ${eventType}`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function prepareWebAgentExecution(ctx, state, containerName, prompt) {
|
|
497
|
+
const history = loadWebSessionHistory(state.webHistoryDir, containerName);
|
|
498
|
+
const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
|
|
499
|
+
if (normalizedTemplate !== history.agentPromptCommand) {
|
|
500
|
+
history.agentPromptCommand = normalizedTemplate;
|
|
501
|
+
saveWebSessionHistory(state.webHistoryDir, containerName, history);
|
|
502
|
+
}
|
|
503
|
+
if (!isAgentPromptCommandEnabled(history.agentPromptCommand)) {
|
|
504
|
+
throw new Error('当前会话未配置 agentPromptCommand');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
await ensureWebContainer(ctx, state, containerName);
|
|
508
|
+
const agentMeta = getAgentRuntimeMeta(history);
|
|
509
|
+
const hasPriorConversation = hasAgentConversationHistory(history);
|
|
510
|
+
let resumeAttempted = false;
|
|
511
|
+
let resumeSucceeded = false;
|
|
512
|
+
let resumeError = '';
|
|
513
|
+
|
|
514
|
+
if (hasPriorConversation && agentMeta.resumeSupported && agentMeta.resumeCommand) {
|
|
515
|
+
resumeAttempted = true;
|
|
516
|
+
const resumeResult = await execCommandInWebContainer(ctx, containerName, agentMeta.resumeCommand);
|
|
517
|
+
if (resumeResult.exitCode === 0) {
|
|
518
|
+
resumeSucceeded = true;
|
|
519
|
+
} else {
|
|
520
|
+
resumeError = clipText(String(resumeResult.output || '(无输出)'), 1200);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const effectivePrompt = resumeSucceeded
|
|
525
|
+
? prompt
|
|
526
|
+
: buildAgentPromptWithHistory(history, prompt);
|
|
527
|
+
const command = buildWebAgentExecCommand(history.agentPromptCommand, effectivePrompt, agentMeta.agentProgram);
|
|
528
|
+
const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
history,
|
|
532
|
+
agentMeta,
|
|
533
|
+
command,
|
|
534
|
+
contextMode,
|
|
535
|
+
resumeAttempted,
|
|
536
|
+
resumeSucceeded,
|
|
537
|
+
resumeError
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function finalizeWebAgentExecution(state, containerName, history, agentMeta, meta, result) {
|
|
542
|
+
appendWebSessionMessage(state.webHistoryDir, containerName, 'assistant', result.output, {
|
|
543
|
+
exitCode: result.exitCode,
|
|
544
|
+
mode: 'agent',
|
|
545
|
+
contextMode: meta.contextMode,
|
|
546
|
+
resumeAttempted: meta.resumeAttempted,
|
|
547
|
+
resumeSucceeded: meta.resumeSucceeded,
|
|
548
|
+
interrupted: result.interrupted === true
|
|
549
|
+
});
|
|
550
|
+
patchWebSessionAgentState(state.webHistoryDir, containerName, {
|
|
551
|
+
agentProgram: agentMeta.agentProgram,
|
|
552
|
+
resumeSupported: agentMeta.resumeSupported,
|
|
553
|
+
lastResumeAt: meta.resumeAttempted ? new Date().toISOString() : history.lastResumeAt || null,
|
|
554
|
+
lastResumeOk: meta.resumeAttempted ? meta.resumeSucceeded : history.lastResumeOk,
|
|
555
|
+
lastResumeError: meta.resumeAttempted ? (meta.resumeSucceeded ? '' : meta.resumeError) : history.lastResumeError || ''
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function appendWebAgentTraceMessage(webHistoryDir, containerName, content, extra = {}) {
|
|
560
|
+
const text = String(content || '').trim();
|
|
561
|
+
if (!text) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
appendWebSessionMessage(webHistoryDir, containerName, 'assistant', text, {
|
|
565
|
+
mode: 'agent',
|
|
566
|
+
streamTrace: true,
|
|
567
|
+
...extra
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
392
571
|
function secureStringEqual(a, b) {
|
|
393
572
|
const aStr = String(a || '');
|
|
394
573
|
const bStr = String(b || '');
|
|
@@ -1188,14 +1367,153 @@ async function execCommandInWebContainer(ctx, containerName, command) {
|
|
|
1188
1367
|
const clippedStdout = stdoutTruncated ? `${stdoutOutput}\n...[stdout-truncated]` : stdoutOutput;
|
|
1189
1368
|
const clippedStderr = stderrTruncated ? `${stderrOutput}\n...[stderr-truncated]` : stderrOutput;
|
|
1190
1369
|
const clippedRaw = `${clippedStdout}${clippedStdout && clippedStderr ? '\n' : ''}${clippedStderr}`;
|
|
1191
|
-
const
|
|
1192
|
-
const cleanOutputSource =
|
|
1370
|
+
const extractedJsonAgentMessage = extractAgentMessageFromCodexJsonl(clippedStdout);
|
|
1371
|
+
const cleanOutputSource = extractedJsonAgentMessage || clippedRaw;
|
|
1193
1372
|
const output = clipText(stripAnsi(cleanOutputSource).trim() || '(无输出)');
|
|
1194
1373
|
resolve({ exitCode, output });
|
|
1195
1374
|
});
|
|
1196
1375
|
});
|
|
1197
1376
|
}
|
|
1198
1377
|
|
|
1378
|
+
async function execAgentInWebContainerStream(ctx, state, containerName, command, options = {}) {
|
|
1379
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
1380
|
+
const agentProgram = typeof opts.agentProgram === 'string' ? opts.agentProgram : '';
|
|
1381
|
+
const onEvent = typeof opts.onEvent === 'function' ? opts.onEvent : () => {};
|
|
1382
|
+
const process = spawn(
|
|
1383
|
+
ctx.dockerCmd,
|
|
1384
|
+
['exec', containerName, '/bin/bash', '-lc', command],
|
|
1385
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] }
|
|
1386
|
+
);
|
|
1387
|
+
|
|
1388
|
+
const runState = {
|
|
1389
|
+
containerName,
|
|
1390
|
+
process,
|
|
1391
|
+
command,
|
|
1392
|
+
startedAt: new Date().toISOString(),
|
|
1393
|
+
stopping: false
|
|
1394
|
+
};
|
|
1395
|
+
state.agentRuns.set(containerName, runState);
|
|
1396
|
+
|
|
1397
|
+
return await new Promise((resolve, reject) => {
|
|
1398
|
+
const MAX_RAW_OUTPUT_CHARS = 32 * 1024 * 1024;
|
|
1399
|
+
let stdoutOutput = '';
|
|
1400
|
+
let stderrOutput = '';
|
|
1401
|
+
let stdoutTruncated = false;
|
|
1402
|
+
let stderrTruncated = false;
|
|
1403
|
+
let stdoutPending = '';
|
|
1404
|
+
let stderrPending = '';
|
|
1405
|
+
function appendChunk(chunk, target) {
|
|
1406
|
+
if (!chunk) return;
|
|
1407
|
+
const text = chunk.toString('utf-8');
|
|
1408
|
+
if (!text) return;
|
|
1409
|
+
if (target.value.length >= MAX_RAW_OUTPUT_CHARS) {
|
|
1410
|
+
target.truncated = true;
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
const remain = MAX_RAW_OUTPUT_CHARS - target.value.length;
|
|
1414
|
+
if (text.length > remain) {
|
|
1415
|
+
target.value += text.slice(0, remain);
|
|
1416
|
+
target.truncated = true;
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
target.value += text;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function emitStdoutTraceLine(line) {
|
|
1423
|
+
const rawLine = String(line || '').trim();
|
|
1424
|
+
if (!rawLine) {
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
if (agentProgram === 'codex') {
|
|
1428
|
+
let payload = null;
|
|
1429
|
+
try {
|
|
1430
|
+
payload = JSON.parse(rawLine);
|
|
1431
|
+
} catch (e) {
|
|
1432
|
+
payload = null;
|
|
1433
|
+
}
|
|
1434
|
+
if (payload) {
|
|
1435
|
+
const display = prepareCodexTraceDisplayLine(payload);
|
|
1436
|
+
if (display) {
|
|
1437
|
+
onEvent({ type: 'trace', stream: 'stdout', text: display });
|
|
1438
|
+
}
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
if (/^OpenAI Codex\b/.test(rawLine) || /^tokens used\b/i.test(rawLine)) {
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
onEvent({ type: 'trace', stream: 'stdout', text: rawLine });
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function emitStderrTraceLine(line) {
|
|
1449
|
+
const rawLine = String(line || '').trim();
|
|
1450
|
+
if (!rawLine) {
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
onEvent({ type: 'trace', stream: 'stderr', text: `[stderr] ${rawLine}` });
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function drainLines(text, carry, handleLine) {
|
|
1457
|
+
let pending = carry + String(text || '');
|
|
1458
|
+
let newlineIndex = pending.indexOf('\n');
|
|
1459
|
+
while (newlineIndex !== -1) {
|
|
1460
|
+
const line = pending.slice(0, newlineIndex).replace(/\r$/, '');
|
|
1461
|
+
handleLine(line);
|
|
1462
|
+
pending = pending.slice(newlineIndex + 1);
|
|
1463
|
+
newlineIndex = pending.indexOf('\n');
|
|
1464
|
+
}
|
|
1465
|
+
return pending;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
process.stdout.on('data', chunk => {
|
|
1469
|
+
appendChunk(chunk, {
|
|
1470
|
+
get value() { return stdoutOutput; },
|
|
1471
|
+
set value(nextValue) { stdoutOutput = nextValue; },
|
|
1472
|
+
get truncated() { return stdoutTruncated; },
|
|
1473
|
+
set truncated(nextValue) { stdoutTruncated = nextValue; }
|
|
1474
|
+
});
|
|
1475
|
+
stdoutPending = drainLines(chunk.toString('utf-8'), stdoutPending, emitStdoutTraceLine);
|
|
1476
|
+
});
|
|
1477
|
+
process.stderr.on('data', chunk => {
|
|
1478
|
+
appendChunk(chunk, {
|
|
1479
|
+
get value() { return stderrOutput; },
|
|
1480
|
+
set value(nextValue) { stderrOutput = nextValue; },
|
|
1481
|
+
get truncated() { return stderrTruncated; },
|
|
1482
|
+
set truncated(nextValue) { stderrTruncated = nextValue; }
|
|
1483
|
+
});
|
|
1484
|
+
stderrPending = drainLines(chunk.toString('utf-8'), stderrPending, emitStderrTraceLine);
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
process.on('error', error => {
|
|
1488
|
+
state.agentRuns.delete(containerName);
|
|
1489
|
+
reject(error);
|
|
1490
|
+
});
|
|
1491
|
+
process.on('close', code => {
|
|
1492
|
+
state.agentRuns.delete(containerName);
|
|
1493
|
+
if (stdoutPending) {
|
|
1494
|
+
emitStdoutTraceLine(stdoutPending);
|
|
1495
|
+
stdoutPending = '';
|
|
1496
|
+
}
|
|
1497
|
+
if (stderrPending) {
|
|
1498
|
+
emitStderrTraceLine(stderrPending);
|
|
1499
|
+
stderrPending = '';
|
|
1500
|
+
}
|
|
1501
|
+
const exitCode = typeof code === 'number' ? code : 1;
|
|
1502
|
+
const clippedStdout = stdoutTruncated ? `${stdoutOutput}\n...[stdout-truncated]` : stdoutOutput;
|
|
1503
|
+
const clippedStderr = stderrTruncated ? `${stderrOutput}\n...[stderr-truncated]` : stderrOutput;
|
|
1504
|
+
const clippedRaw = `${clippedStdout}${clippedStdout && clippedStderr ? '\n' : ''}${clippedStderr}`;
|
|
1505
|
+
const extractedJsonAgentMessage = extractAgentMessageFromCodexJsonl(clippedStdout);
|
|
1506
|
+
const cleanOutputSource = extractedJsonAgentMessage || clippedRaw;
|
|
1507
|
+
const output = clipText(stripAnsi(cleanOutputSource).trim() || '(无输出)');
|
|
1508
|
+
resolve({
|
|
1509
|
+
exitCode,
|
|
1510
|
+
output,
|
|
1511
|
+
interrupted: exitCode !== 0 && runState.stopping === true
|
|
1512
|
+
});
|
|
1513
|
+
});
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1199
1517
|
function readRequestBody(req) {
|
|
1200
1518
|
return new Promise((resolve, reject) => {
|
|
1201
1519
|
let body = '';
|
|
@@ -1232,6 +1550,24 @@ function sendJson(res, statusCode, payload, extraHeaders = {}) {
|
|
|
1232
1550
|
res.end(JSON.stringify(payload));
|
|
1233
1551
|
}
|
|
1234
1552
|
|
|
1553
|
+
function sendNdjson(res, payload) {
|
|
1554
|
+
res.write(`${JSON.stringify(payload)}\n`);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function stopWebAgentRun(state, containerName) {
|
|
1558
|
+
const runState = state.agentRuns.get(containerName);
|
|
1559
|
+
if (!runState || !runState.process || runState.process.killed) {
|
|
1560
|
+
return false;
|
|
1561
|
+
}
|
|
1562
|
+
runState.stopping = true;
|
|
1563
|
+
try {
|
|
1564
|
+
runState.process.kill('SIGTERM');
|
|
1565
|
+
} catch (e) {
|
|
1566
|
+
return false;
|
|
1567
|
+
}
|
|
1568
|
+
return true;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1235
1571
|
function sendHtml(res, statusCode, html, extraHeaders = {}) {
|
|
1236
1572
|
res.writeHead(statusCode, {
|
|
1237
1573
|
'Content-Type': 'text/html; charset=utf-8',
|
|
@@ -1826,70 +2162,154 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
1826
2162
|
return;
|
|
1827
2163
|
}
|
|
1828
2164
|
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
}
|
|
1835
|
-
if (!isAgentPromptCommandEnabled(history.agentPromptCommand)) {
|
|
1836
|
-
sendJson(res, 400, { error: '当前会话未配置 agentPromptCommand' });
|
|
2165
|
+
let prepared = null;
|
|
2166
|
+
try {
|
|
2167
|
+
prepared = await prepareWebAgentExecution(ctx, state, containerName, prompt);
|
|
2168
|
+
} catch (e) {
|
|
2169
|
+
sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
|
|
1837
2170
|
return;
|
|
1838
2171
|
}
|
|
1839
2172
|
|
|
1840
|
-
|
|
1841
|
-
const agentMeta = getAgentRuntimeMeta(history);
|
|
1842
|
-
const hasPriorConversation = hasAgentConversationHistory(history);
|
|
1843
|
-
let resumeAttempted = false;
|
|
1844
|
-
let resumeSucceeded = false;
|
|
1845
|
-
let resumeError = '';
|
|
1846
|
-
if (hasPriorConversation && agentMeta.resumeSupported && agentMeta.resumeCommand) {
|
|
1847
|
-
resumeAttempted = true;
|
|
1848
|
-
const resumeResult = await execCommandInWebContainer(ctx, containerName, agentMeta.resumeCommand);
|
|
1849
|
-
if (resumeResult.exitCode === 0) {
|
|
1850
|
-
resumeSucceeded = true;
|
|
1851
|
-
} else {
|
|
1852
|
-
resumeError = clipText(String(resumeResult.output || '(无输出)'), 1200);
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
const effectivePrompt = resumeSucceeded
|
|
1857
|
-
? prompt
|
|
1858
|
-
: buildAgentPromptWithHistory(history, prompt);
|
|
1859
|
-
const command = buildWebAgentExecCommand(history.agentPromptCommand, effectivePrompt, agentMeta.agentProgram);
|
|
1860
|
-
const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
|
|
2173
|
+
const { history, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
|
|
1861
2174
|
appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, {
|
|
1862
2175
|
mode: 'agent',
|
|
1863
2176
|
contextMode
|
|
1864
2177
|
});
|
|
1865
2178
|
const result = await execCommandInWebContainer(ctx, containerName, command);
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
exitCode: result.exitCode,
|
|
1873
|
-
mode: 'agent',
|
|
1874
|
-
contextMode,
|
|
1875
|
-
resumeAttempted,
|
|
1876
|
-
resumeSucceeded
|
|
1877
|
-
}
|
|
1878
|
-
);
|
|
1879
|
-
patchWebSessionAgentState(state.webHistoryDir, containerName, {
|
|
1880
|
-
agentProgram: agentMeta.agentProgram,
|
|
1881
|
-
resumeSupported: agentMeta.resumeSupported,
|
|
1882
|
-
lastResumeAt: resumeAttempted ? new Date().toISOString() : history.lastResumeAt || null,
|
|
1883
|
-
lastResumeOk: resumeAttempted ? resumeSucceeded : history.lastResumeOk,
|
|
1884
|
-
lastResumeError: resumeAttempted ? (resumeSucceeded ? '' : resumeError) : history.lastResumeError || ''
|
|
1885
|
-
});
|
|
2179
|
+
finalizeWebAgentExecution(state, containerName, history, agentMeta, {
|
|
2180
|
+
contextMode,
|
|
2181
|
+
resumeAttempted,
|
|
2182
|
+
resumeSucceeded,
|
|
2183
|
+
resumeError
|
|
2184
|
+
}, result);
|
|
1886
2185
|
sendJson(res, 200, {
|
|
1887
2186
|
exitCode: result.exitCode,
|
|
1888
2187
|
output: result.output,
|
|
1889
2188
|
contextMode,
|
|
1890
2189
|
resumeAttempted,
|
|
1891
|
-
resumeSucceeded
|
|
2190
|
+
resumeSucceeded,
|
|
2191
|
+
interrupted: result.interrupted === true
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
},
|
|
2195
|
+
{
|
|
2196
|
+
method: 'POST',
|
|
2197
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent\/stream$/),
|
|
2198
|
+
handler: async match => {
|
|
2199
|
+
const containerName = getValidSessionName(ctx, res, match[1]);
|
|
2200
|
+
if (!containerName) {
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
const payload = await readJsonBody(req);
|
|
2205
|
+
const prompt = (payload.prompt || '').trim();
|
|
2206
|
+
if (!prompt) {
|
|
2207
|
+
sendJson(res, 400, { error: 'prompt 不能为空' });
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
if (state.agentRuns.has(containerName)) {
|
|
2211
|
+
sendJson(res, 409, { error: '当前会话已有运行中的 agent 任务' });
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
let prepared = null;
|
|
2216
|
+
try {
|
|
2217
|
+
prepared = await prepareWebAgentExecution(ctx, state, containerName, prompt);
|
|
2218
|
+
} catch (e) {
|
|
2219
|
+
sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
const { history, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
|
|
2224
|
+
const traceLines = ['[执行过程]'];
|
|
2225
|
+
appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, {
|
|
2226
|
+
mode: 'agent',
|
|
2227
|
+
contextMode
|
|
2228
|
+
});
|
|
2229
|
+
|
|
2230
|
+
res.writeHead(200, {
|
|
2231
|
+
'Content-Type': 'application/x-ndjson; charset=utf-8',
|
|
2232
|
+
'Cache-Control': 'no-store',
|
|
2233
|
+
'X-Accel-Buffering': 'no'
|
|
2234
|
+
});
|
|
2235
|
+
sendNdjson(res, {
|
|
2236
|
+
type: 'meta',
|
|
2237
|
+
containerName,
|
|
2238
|
+
contextMode,
|
|
2239
|
+
resumeAttempted,
|
|
2240
|
+
resumeSucceeded,
|
|
2241
|
+
agentProgram: agentMeta.agentProgram
|
|
1892
2242
|
});
|
|
2243
|
+
if (contextMode) {
|
|
2244
|
+
traceLines.push(`上下文模式: ${contextMode}`);
|
|
2245
|
+
}
|
|
2246
|
+
if (resumeAttempted) {
|
|
2247
|
+
traceLines.push(resumeSucceeded ? '会话恢复成功' : '会话恢复失败,已回退到历史注入');
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
try {
|
|
2251
|
+
const result = await execAgentInWebContainerStream(ctx, state, containerName, command, {
|
|
2252
|
+
agentProgram: agentMeta.agentProgram,
|
|
2253
|
+
onEvent: event => {
|
|
2254
|
+
if (event && event.type === 'trace' && event.text) {
|
|
2255
|
+
traceLines.push(String(event.text));
|
|
2256
|
+
}
|
|
2257
|
+
sendNdjson(res, event);
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
2260
|
+
traceLines.push(result.interrupted === true ? '[任务] 已停止' : '[任务] 已完成');
|
|
2261
|
+
appendWebAgentTraceMessage(state.webHistoryDir, containerName, traceLines.join('\n'), {
|
|
2262
|
+
contextMode,
|
|
2263
|
+
resumeAttempted,
|
|
2264
|
+
resumeSucceeded,
|
|
2265
|
+
interrupted: result.interrupted === true
|
|
2266
|
+
});
|
|
2267
|
+
finalizeWebAgentExecution(state, containerName, history, agentMeta, {
|
|
2268
|
+
contextMode,
|
|
2269
|
+
resumeAttempted,
|
|
2270
|
+
resumeSucceeded,
|
|
2271
|
+
resumeError
|
|
2272
|
+
}, result);
|
|
2273
|
+
sendNdjson(res, {
|
|
2274
|
+
type: 'result',
|
|
2275
|
+
exitCode: result.exitCode,
|
|
2276
|
+
output: result.output,
|
|
2277
|
+
contextMode,
|
|
2278
|
+
resumeAttempted,
|
|
2279
|
+
resumeSucceeded,
|
|
2280
|
+
interrupted: result.interrupted === true
|
|
2281
|
+
});
|
|
2282
|
+
} catch (e) {
|
|
2283
|
+
traceLines.push(`[错误] ${e && e.message ? e.message : 'Agent 执行失败'}`);
|
|
2284
|
+
appendWebAgentTraceMessage(state.webHistoryDir, containerName, traceLines.join('\n'), {
|
|
2285
|
+
contextMode,
|
|
2286
|
+
resumeAttempted,
|
|
2287
|
+
resumeSucceeded,
|
|
2288
|
+
interrupted: true
|
|
2289
|
+
});
|
|
2290
|
+
sendNdjson(res, {
|
|
2291
|
+
type: 'error',
|
|
2292
|
+
error: e && e.message ? e.message : 'Agent 执行失败'
|
|
2293
|
+
});
|
|
2294
|
+
} finally {
|
|
2295
|
+
res.end();
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
},
|
|
2299
|
+
{
|
|
2300
|
+
method: 'POST',
|
|
2301
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent\/stop$/),
|
|
2302
|
+
handler: async match => {
|
|
2303
|
+
const containerName = getValidSessionName(ctx, res, match[1]);
|
|
2304
|
+
if (!containerName) {
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
const stopped = stopWebAgentRun(state, containerName);
|
|
2308
|
+
if (!stopped) {
|
|
2309
|
+
sendJson(res, 404, { error: '当前会话没有运行中的 agent 任务' });
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
sendJson(res, 200, { ok: true, stopping: true });
|
|
1893
2313
|
}
|
|
1894
2314
|
},
|
|
1895
2315
|
{
|
|
@@ -1990,7 +2410,8 @@ async function startWebServer(options) {
|
|
|
1990
2410
|
webHistoryDir: options.webHistoryDir || path.join(os.homedir(), '.manyoyo', 'web-history'),
|
|
1991
2411
|
webConfigPath: options.webConfigPath || getDefaultWebConfigPath(),
|
|
1992
2412
|
authSessions: new Map(),
|
|
1993
|
-
terminalSessions: new Map()
|
|
2413
|
+
terminalSessions: new Map(),
|
|
2414
|
+
agentRuns: new Map()
|
|
1994
2415
|
};
|
|
1995
2416
|
|
|
1996
2417
|
ensureWebHistoryDir(state.webHistoryDir);
|
|
@@ -2216,6 +2637,13 @@ async function startWebServer(options) {
|
|
|
2216
2637
|
}
|
|
2217
2638
|
}
|
|
2218
2639
|
state.terminalSessions.clear();
|
|
2640
|
+
for (const runState of state.agentRuns.values()) {
|
|
2641
|
+
const child = runState && runState.process;
|
|
2642
|
+
if (child && !child.killed) {
|
|
2643
|
+
try { child.kill('SIGTERM'); } catch (e) {}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
state.agentRuns.clear();
|
|
2219
2647
|
|
|
2220
2648
|
const closeHttp = () => {
|
|
2221
2649
|
if (!server.listening) {
|