@xcanwin/manyoyo 5.6.8 → 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/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 +499 -52
- 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) {
|
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
|
@@ -321,6 +321,7 @@ function hasAgentConversationHistory(history) {
|
|
|
321
321
|
for (const message of messages) {
|
|
322
322
|
if (!message || typeof message !== 'object') continue;
|
|
323
323
|
if (message.mode !== 'agent') continue;
|
|
324
|
+
if (message.streamTrace === true) continue;
|
|
324
325
|
if (message.role === 'user' || message.role === 'assistant') {
|
|
325
326
|
return true;
|
|
326
327
|
}
|
|
@@ -339,7 +340,12 @@ function clipAgentContextMessageText(text) {
|
|
|
339
340
|
function buildAgentPromptWithHistory(history, prompt) {
|
|
340
341
|
const sessionHistory = history && Array.isArray(history.messages) ? history.messages : [];
|
|
341
342
|
const relevantMessages = sessionHistory
|
|
342
|
-
.filter(message =>
|
|
343
|
+
.filter(message => (
|
|
344
|
+
message
|
|
345
|
+
&& message.mode === 'agent'
|
|
346
|
+
&& message.streamTrace !== true
|
|
347
|
+
&& (message.role === 'user' || message.role === 'assistant')
|
|
348
|
+
))
|
|
343
349
|
.slice(-WEB_AGENT_CONTEXT_MAX_MESSAGES);
|
|
344
350
|
if (!relevantMessages.length) {
|
|
345
351
|
return String(prompt || '');
|
|
@@ -370,6 +376,198 @@ function buildAgentPromptWithHistory(history, prompt) {
|
|
|
370
376
|
].join('\n');
|
|
371
377
|
}
|
|
372
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
|
+
|
|
373
571
|
function secureStringEqual(a, b) {
|
|
374
572
|
const aStr = String(a || '');
|
|
375
573
|
const bStr = String(b || '');
|
|
@@ -1177,6 +1375,145 @@ async function execCommandInWebContainer(ctx, containerName, command) {
|
|
|
1177
1375
|
});
|
|
1178
1376
|
}
|
|
1179
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
|
+
|
|
1180
1517
|
function readRequestBody(req) {
|
|
1181
1518
|
return new Promise((resolve, reject) => {
|
|
1182
1519
|
let body = '';
|
|
@@ -1213,6 +1550,24 @@ function sendJson(res, statusCode, payload, extraHeaders = {}) {
|
|
|
1213
1550
|
res.end(JSON.stringify(payload));
|
|
1214
1551
|
}
|
|
1215
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
|
+
|
|
1216
1571
|
function sendHtml(res, statusCode, html, extraHeaders = {}) {
|
|
1217
1572
|
res.writeHead(statusCode, {
|
|
1218
1573
|
'Content-Type': 'text/html; charset=utf-8',
|
|
@@ -1807,72 +2162,156 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
1807
2162
|
return;
|
|
1808
2163
|
}
|
|
1809
2164
|
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
}
|
|
1816
|
-
if (!isAgentPromptCommandEnabled(history.agentPromptCommand)) {
|
|
1817
|
-
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 执行准备失败' });
|
|
1818
2170
|
return;
|
|
1819
2171
|
}
|
|
1820
2172
|
|
|
1821
|
-
|
|
1822
|
-
const agentMeta = getAgentRuntimeMeta(history);
|
|
1823
|
-
const hasPriorConversation = hasAgentConversationHistory(history);
|
|
1824
|
-
let resumeAttempted = false;
|
|
1825
|
-
let resumeSucceeded = false;
|
|
1826
|
-
let resumeError = '';
|
|
1827
|
-
if (hasPriorConversation && agentMeta.resumeSupported && agentMeta.resumeCommand) {
|
|
1828
|
-
resumeAttempted = true;
|
|
1829
|
-
const resumeResult = await execCommandInWebContainer(ctx, containerName, agentMeta.resumeCommand);
|
|
1830
|
-
if (resumeResult.exitCode === 0) {
|
|
1831
|
-
resumeSucceeded = true;
|
|
1832
|
-
} else {
|
|
1833
|
-
resumeError = clipText(String(resumeResult.output || '(无输出)'), 1200);
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
const effectivePrompt = resumeSucceeded
|
|
1838
|
-
? prompt
|
|
1839
|
-
: buildAgentPromptWithHistory(history, prompt);
|
|
1840
|
-
const command = buildWebAgentExecCommand(history.agentPromptCommand, effectivePrompt, agentMeta.agentProgram);
|
|
1841
|
-
const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
|
|
2173
|
+
const { history, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
|
|
1842
2174
|
appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, {
|
|
1843
2175
|
mode: 'agent',
|
|
1844
2176
|
contextMode
|
|
1845
2177
|
});
|
|
1846
2178
|
const result = await execCommandInWebContainer(ctx, containerName, command);
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
exitCode: result.exitCode,
|
|
1854
|
-
mode: 'agent',
|
|
1855
|
-
contextMode,
|
|
1856
|
-
resumeAttempted,
|
|
1857
|
-
resumeSucceeded
|
|
1858
|
-
}
|
|
1859
|
-
);
|
|
1860
|
-
patchWebSessionAgentState(state.webHistoryDir, containerName, {
|
|
1861
|
-
agentProgram: agentMeta.agentProgram,
|
|
1862
|
-
resumeSupported: agentMeta.resumeSupported,
|
|
1863
|
-
lastResumeAt: resumeAttempted ? new Date().toISOString() : history.lastResumeAt || null,
|
|
1864
|
-
lastResumeOk: resumeAttempted ? resumeSucceeded : history.lastResumeOk,
|
|
1865
|
-
lastResumeError: resumeAttempted ? (resumeSucceeded ? '' : resumeError) : history.lastResumeError || ''
|
|
1866
|
-
});
|
|
2179
|
+
finalizeWebAgentExecution(state, containerName, history, agentMeta, {
|
|
2180
|
+
contextMode,
|
|
2181
|
+
resumeAttempted,
|
|
2182
|
+
resumeSucceeded,
|
|
2183
|
+
resumeError
|
|
2184
|
+
}, result);
|
|
1867
2185
|
sendJson(res, 200, {
|
|
1868
2186
|
exitCode: result.exitCode,
|
|
1869
2187
|
output: result.output,
|
|
1870
2188
|
contextMode,
|
|
1871
2189
|
resumeAttempted,
|
|
1872
|
-
resumeSucceeded
|
|
2190
|
+
resumeSucceeded,
|
|
2191
|
+
interrupted: result.interrupted === true
|
|
1873
2192
|
});
|
|
1874
2193
|
}
|
|
1875
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
|
|
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 });
|
|
2313
|
+
}
|
|
2314
|
+
},
|
|
1876
2315
|
{
|
|
1877
2316
|
method: 'POST',
|
|
1878
2317
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/remove$/),
|
|
@@ -1971,7 +2410,8 @@ async function startWebServer(options) {
|
|
|
1971
2410
|
webHistoryDir: options.webHistoryDir || path.join(os.homedir(), '.manyoyo', 'web-history'),
|
|
1972
2411
|
webConfigPath: options.webConfigPath || getDefaultWebConfigPath(),
|
|
1973
2412
|
authSessions: new Map(),
|
|
1974
|
-
terminalSessions: new Map()
|
|
2413
|
+
terminalSessions: new Map(),
|
|
2414
|
+
agentRuns: new Map()
|
|
1975
2415
|
};
|
|
1976
2416
|
|
|
1977
2417
|
ensureWebHistoryDir(state.webHistoryDir);
|
|
@@ -2197,6 +2637,13 @@ async function startWebServer(options) {
|
|
|
2197
2637
|
}
|
|
2198
2638
|
}
|
|
2199
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();
|
|
2200
2647
|
|
|
2201
2648
|
const closeHttp = () => {
|
|
2202
2649
|
if (!server.listening) {
|