@xcanwin/manyoyo 5.7.3 → 5.7.6
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/web/frontend/app.css +262 -0
- package/lib/web/frontend/app.html +33 -3
- package/lib/web/frontend/app.js +818 -103
- package/lib/web/server.js +512 -161
- package/package.json +1 -1
package/lib/web/server.js
CHANGED
|
@@ -29,6 +29,9 @@ const WEB_AGENT_CONTEXT_MAX_CHARS = 6000;
|
|
|
29
29
|
const WEB_AGENT_CONTEXT_PER_MESSAGE_MAX_CHARS = 600;
|
|
30
30
|
const WEB_AUTH_COOKIE_NAME = 'manyoyo_web_auth';
|
|
31
31
|
const WEB_AUTH_TTL_SECONDS = 12 * 60 * 60;
|
|
32
|
+
const WEB_SESSION_KEY_SEPARATOR = '~';
|
|
33
|
+
const WEB_DEFAULT_AGENT_ID = 'default';
|
|
34
|
+
const WEB_DEFAULT_AGENT_NAME = 'AGENT 1';
|
|
32
35
|
const FRONTEND_DIR = path.join(__dirname, 'frontend');
|
|
33
36
|
const SAFE_CONTAINER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
34
37
|
const IMAGE_VERSION_TAG_PATTERN = /^(\d+\.\d+\.\d+)-([A-Za-z0-9][A-Za-z0-9_.-]*)$/;
|
|
@@ -115,62 +118,113 @@ function getWebHistoryFile(webHistoryDir, containerName) {
|
|
|
115
118
|
return path.join(webHistoryDir, `${containerName}.json`);
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
function normalizeWebAgentName(agentId, agentName) {
|
|
122
|
+
const name = typeof agentName === 'string' ? agentName.trim() : '';
|
|
123
|
+
if (name) {
|
|
124
|
+
return name;
|
|
125
|
+
}
|
|
126
|
+
if (agentId === WEB_DEFAULT_AGENT_ID) {
|
|
127
|
+
return WEB_DEFAULT_AGENT_NAME;
|
|
128
|
+
}
|
|
129
|
+
const matched = String(agentId || '').match(/^agent-(\d+)$/);
|
|
130
|
+
if (matched) {
|
|
131
|
+
return `AGENT ${matched[1]}`;
|
|
132
|
+
}
|
|
133
|
+
return String(agentId || '').trim() || WEB_DEFAULT_AGENT_NAME;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function createEmptyWebAgentSession(agentId, agentName) {
|
|
137
|
+
return {
|
|
138
|
+
agentId,
|
|
139
|
+
agentName: normalizeWebAgentName(agentId, agentName),
|
|
140
|
+
updatedAt: null,
|
|
141
|
+
messages: [],
|
|
142
|
+
lastResumeAt: null,
|
|
143
|
+
lastResumeOk: null,
|
|
144
|
+
lastResumeError: ''
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeWebAgentSessionRecord(agentId, rawAgent) {
|
|
149
|
+
const source = rawAgent && typeof rawAgent === 'object' && !Array.isArray(rawAgent) ? rawAgent : {};
|
|
150
|
+
return {
|
|
151
|
+
agentId,
|
|
152
|
+
agentName: normalizeWebAgentName(agentId, source.agentName),
|
|
153
|
+
updatedAt: typeof source.updatedAt === 'string' ? source.updatedAt : null,
|
|
154
|
+
messages: Array.isArray(source.messages) ? source.messages : [],
|
|
155
|
+
lastResumeAt: typeof source.lastResumeAt === 'string' ? source.lastResumeAt : null,
|
|
156
|
+
lastResumeOk: typeof source.lastResumeOk === 'boolean' ? source.lastResumeOk : null,
|
|
157
|
+
lastResumeError: typeof source.lastResumeError === 'string' ? source.lastResumeError : ''
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizeWebHistoryRecord(containerName, rawData) {
|
|
162
|
+
const data = rawData && typeof rawData === 'object' && !Array.isArray(rawData) ? rawData : {};
|
|
163
|
+
const history = {
|
|
164
|
+
containerName,
|
|
165
|
+
updatedAt: typeof data.updatedAt === 'string' ? data.updatedAt : null,
|
|
166
|
+
agentPromptCommand: typeof data.agentPromptCommand === 'string'
|
|
167
|
+
? data.agentPromptCommand
|
|
168
|
+
: '',
|
|
169
|
+
applied: data.applied && typeof data.applied === 'object' && !Array.isArray(data.applied)
|
|
170
|
+
? data.applied
|
|
171
|
+
: null,
|
|
172
|
+
agents: {}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
if (data.agents && typeof data.agents === 'object' && !Array.isArray(data.agents)) {
|
|
176
|
+
Object.keys(data.agents).forEach(agentId => {
|
|
177
|
+
if (!SAFE_CONTAINER_NAME_PATTERN.test(agentId)) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
history.agents[agentId] = normalizeWebAgentSessionRecord(agentId, data.agents[agentId]);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!Object.keys(history.agents).length && Array.isArray(data.messages)) {
|
|
185
|
+
history.agents[WEB_DEFAULT_AGENT_ID] = normalizeWebAgentSessionRecord(WEB_DEFAULT_AGENT_ID, {
|
|
186
|
+
agentName: data.agentName || WEB_DEFAULT_AGENT_NAME,
|
|
187
|
+
updatedAt: typeof data.updatedAt === 'string' ? data.updatedAt : null,
|
|
188
|
+
messages: data.messages,
|
|
189
|
+
lastResumeAt: typeof data.lastResumeAt === 'string' ? data.lastResumeAt : null,
|
|
190
|
+
lastResumeOk: typeof data.lastResumeOk === 'boolean' ? data.lastResumeOk : null,
|
|
191
|
+
lastResumeError: typeof data.lastResumeError === 'string' ? data.lastResumeError : ''
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return history;
|
|
196
|
+
}
|
|
197
|
+
|
|
118
198
|
function loadWebSessionHistory(webHistoryDir, containerName) {
|
|
119
199
|
ensureWebHistoryDir(webHistoryDir);
|
|
120
200
|
const filePath = getWebHistoryFile(webHistoryDir, containerName);
|
|
121
201
|
if (!fs.existsSync(filePath)) {
|
|
122
|
-
return {
|
|
123
|
-
containerName,
|
|
124
|
-
updatedAt: null,
|
|
125
|
-
messages: [],
|
|
126
|
-
agentPromptCommand: '',
|
|
127
|
-
agentProgram: '',
|
|
128
|
-
resumeSupported: false,
|
|
129
|
-
lastResumeAt: null,
|
|
130
|
-
lastResumeOk: null,
|
|
131
|
-
lastResumeError: '',
|
|
132
|
-
applied: null
|
|
133
|
-
};
|
|
202
|
+
return normalizeWebHistoryRecord(containerName, {});
|
|
134
203
|
}
|
|
135
204
|
|
|
136
205
|
try {
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
containerName,
|
|
140
|
-
updatedAt: data.updatedAt || null,
|
|
141
|
-
messages: Array.isArray(data.messages) ? data.messages : [],
|
|
142
|
-
agentPromptCommand: typeof data.agentPromptCommand === 'string'
|
|
143
|
-
? data.agentPromptCommand
|
|
144
|
-
: '',
|
|
145
|
-
agentProgram: typeof data.agentProgram === 'string' ? data.agentProgram : '',
|
|
146
|
-
resumeSupported: data.resumeSupported === true,
|
|
147
|
-
lastResumeAt: typeof data.lastResumeAt === 'string' ? data.lastResumeAt : null,
|
|
148
|
-
lastResumeOk: typeof data.lastResumeOk === 'boolean' ? data.lastResumeOk : null,
|
|
149
|
-
lastResumeError: typeof data.lastResumeError === 'string' ? data.lastResumeError : '',
|
|
150
|
-
applied: data.applied && typeof data.applied === 'object' && !Array.isArray(data.applied)
|
|
151
|
-
? data.applied
|
|
152
|
-
: null
|
|
153
|
-
};
|
|
206
|
+
return normalizeWebHistoryRecord(containerName, JSON.parse(fs.readFileSync(filePath, 'utf-8')));
|
|
154
207
|
} catch (e) {
|
|
155
|
-
return {
|
|
156
|
-
containerName,
|
|
157
|
-
updatedAt: null,
|
|
158
|
-
messages: [],
|
|
159
|
-
agentPromptCommand: '',
|
|
160
|
-
agentProgram: '',
|
|
161
|
-
resumeSupported: false,
|
|
162
|
-
lastResumeAt: null,
|
|
163
|
-
lastResumeOk: null,
|
|
164
|
-
lastResumeError: '',
|
|
165
|
-
applied: null
|
|
166
|
-
};
|
|
208
|
+
return normalizeWebHistoryRecord(containerName, {});
|
|
167
209
|
}
|
|
168
210
|
}
|
|
169
211
|
|
|
170
212
|
function saveWebSessionHistory(webHistoryDir, containerName, history) {
|
|
171
213
|
ensureWebHistoryDir(webHistoryDir);
|
|
172
214
|
const filePath = getWebHistoryFile(webHistoryDir, containerName);
|
|
173
|
-
|
|
215
|
+
const normalized = normalizeWebHistoryRecord(containerName, history);
|
|
216
|
+
const runtimeMeta = getAgentRuntimeMeta(normalized);
|
|
217
|
+
const defaultAgent = getWebAgentSession(normalized, WEB_DEFAULT_AGENT_ID) || createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID);
|
|
218
|
+
const legacyCompatible = {
|
|
219
|
+
...normalized,
|
|
220
|
+
messages: Array.isArray(defaultAgent.messages) ? defaultAgent.messages : [],
|
|
221
|
+
agentProgram: runtimeMeta.agentProgram || '',
|
|
222
|
+
resumeSupported: runtimeMeta.resumeSupported === true,
|
|
223
|
+
lastResumeAt: defaultAgent.lastResumeAt || null,
|
|
224
|
+
lastResumeOk: typeof defaultAgent.lastResumeOk === 'boolean' ? defaultAgent.lastResumeOk : null,
|
|
225
|
+
lastResumeError: defaultAgent.lastResumeError || ''
|
|
226
|
+
};
|
|
227
|
+
fs.writeFileSync(filePath, JSON.stringify(legacyCompatible, null, 4));
|
|
174
228
|
}
|
|
175
229
|
|
|
176
230
|
function removeWebSessionHistory(webHistoryDir, containerName) {
|
|
@@ -189,10 +243,84 @@ function listWebHistorySessionNames(webHistoryDir, isValidContainerName) {
|
|
|
189
243
|
.filter(name => isValidContainerName(name));
|
|
190
244
|
}
|
|
191
245
|
|
|
192
|
-
function
|
|
193
|
-
|
|
246
|
+
function buildWebSessionKey(containerName, agentId = WEB_DEFAULT_AGENT_ID) {
|
|
247
|
+
if (agentId === WEB_DEFAULT_AGENT_ID) {
|
|
248
|
+
return containerName;
|
|
249
|
+
}
|
|
250
|
+
return `${containerName}${WEB_SESSION_KEY_SEPARATOR}${agentId}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function parseWebSessionKey(sessionKey) {
|
|
254
|
+
const decoded = String(sessionKey || '').trim();
|
|
255
|
+
if (!decoded) {
|
|
256
|
+
return {
|
|
257
|
+
key: '',
|
|
258
|
+
containerName: '',
|
|
259
|
+
agentId: WEB_DEFAULT_AGENT_ID
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const separatorIndex = decoded.indexOf(WEB_SESSION_KEY_SEPARATOR);
|
|
263
|
+
if (separatorIndex === -1) {
|
|
264
|
+
return {
|
|
265
|
+
key: decoded,
|
|
266
|
+
containerName: decoded,
|
|
267
|
+
agentId: WEB_DEFAULT_AGENT_ID
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
key: decoded,
|
|
272
|
+
containerName: decoded.slice(0, separatorIndex),
|
|
273
|
+
agentId: decoded.slice(separatorIndex + 1) || WEB_DEFAULT_AGENT_ID
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function getWebAgentSession(history, agentId, options = {}) {
|
|
278
|
+
const sessionHistory = history && typeof history === 'object' ? history : { agents: {} };
|
|
279
|
+
if (!sessionHistory.agents || typeof sessionHistory.agents !== 'object' || Array.isArray(sessionHistory.agents)) {
|
|
280
|
+
sessionHistory.agents = {};
|
|
281
|
+
}
|
|
282
|
+
const requestedAgentId = String(agentId || WEB_DEFAULT_AGENT_ID).trim() || WEB_DEFAULT_AGENT_ID;
|
|
283
|
+
if (sessionHistory.agents[requestedAgentId]) {
|
|
284
|
+
return sessionHistory.agents[requestedAgentId];
|
|
285
|
+
}
|
|
286
|
+
if (options.create === true) {
|
|
287
|
+
const agentSession = createEmptyWebAgentSession(requestedAgentId);
|
|
288
|
+
sessionHistory.agents[requestedAgentId] = agentSession;
|
|
289
|
+
return agentSession;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function listWebAgentSessions(history, options = {}) {
|
|
295
|
+
const sessionHistory = history && typeof history === 'object' ? history : {};
|
|
296
|
+
const agents = sessionHistory.agents && typeof sessionHistory.agents === 'object' && !Array.isArray(sessionHistory.agents)
|
|
297
|
+
? sessionHistory.agents
|
|
298
|
+
: {};
|
|
299
|
+
const agentIds = Object.keys(agents);
|
|
300
|
+
if (!agentIds.length && options.includeSyntheticDefault === true) {
|
|
301
|
+
return [createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID)];
|
|
302
|
+
}
|
|
303
|
+
return agentIds
|
|
304
|
+
.map(agentId => agents[agentId])
|
|
305
|
+
.filter(Boolean)
|
|
306
|
+
.sort((a, b) => {
|
|
307
|
+
const orderA = a.agentId === WEB_DEFAULT_AGENT_ID ? 0 : 1;
|
|
308
|
+
const orderB = b.agentId === WEB_DEFAULT_AGENT_ID ? 0 : 1;
|
|
309
|
+
if (orderA !== orderB) {
|
|
310
|
+
return orderA - orderB;
|
|
311
|
+
}
|
|
312
|
+
return String(a.agentName || '').localeCompare(String(b.agentName || ''), 'zh-CN');
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function appendWebSessionMessage(webHistoryDir, sessionRefOrContainerName, role, content, extra = {}) {
|
|
317
|
+
const sessionRef = typeof sessionRefOrContainerName === 'string'
|
|
318
|
+
? { containerName: sessionRefOrContainerName, agentId: WEB_DEFAULT_AGENT_ID }
|
|
319
|
+
: sessionRefOrContainerName;
|
|
320
|
+
const history = loadWebSessionHistory(webHistoryDir, sessionRef.containerName);
|
|
321
|
+
const agentSession = getWebAgentSession(history, sessionRef.agentId, { create: true });
|
|
194
322
|
const timestamp = new Date().toISOString();
|
|
195
|
-
|
|
323
|
+
agentSession.messages.push({
|
|
196
324
|
id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
|
197
325
|
role,
|
|
198
326
|
content,
|
|
@@ -200,30 +328,22 @@ function appendWebSessionMessage(webHistoryDir, containerName, role, content, ex
|
|
|
200
328
|
...extra
|
|
201
329
|
});
|
|
202
330
|
|
|
203
|
-
if (
|
|
204
|
-
|
|
331
|
+
if (agentSession.messages.length > WEB_HISTORY_MAX_MESSAGES) {
|
|
332
|
+
agentSession.messages = agentSession.messages.slice(-WEB_HISTORY_MAX_MESSAGES);
|
|
205
333
|
}
|
|
206
334
|
|
|
335
|
+
agentSession.updatedAt = timestamp;
|
|
207
336
|
history.updatedAt = timestamp;
|
|
208
|
-
saveWebSessionHistory(webHistoryDir, containerName, history);
|
|
337
|
+
saveWebSessionHistory(webHistoryDir, sessionRef.containerName, history);
|
|
209
338
|
}
|
|
210
339
|
|
|
211
340
|
function setWebSessionAgentPromptCommand(webHistoryDir, containerName, agentPromptCommand) {
|
|
212
341
|
const history = loadWebSessionHistory(webHistoryDir, containerName);
|
|
213
342
|
history.agentPromptCommand = normalizeAgentPromptCommandTemplate(agentPromptCommand, 'agentPromptCommand');
|
|
214
|
-
const agentProgram = resolveAgentProgram(history.agentPromptCommand);
|
|
215
|
-
const resumeCommand = buildAgentResumeCommand(agentProgram);
|
|
216
|
-
history.agentProgram = agentProgram || '';
|
|
217
|
-
history.resumeSupported = Boolean(resumeCommand);
|
|
218
|
-
if (!history.resumeSupported) {
|
|
219
|
-
history.lastResumeAt = null;
|
|
220
|
-
history.lastResumeOk = null;
|
|
221
|
-
history.lastResumeError = '';
|
|
222
|
-
}
|
|
223
343
|
saveWebSessionHistory(webHistoryDir, containerName, history);
|
|
224
344
|
}
|
|
225
345
|
|
|
226
|
-
function
|
|
346
|
+
function patchWebSessionHistory(webHistoryDir, containerName, patch) {
|
|
227
347
|
const history = loadWebSessionHistory(webHistoryDir, containerName);
|
|
228
348
|
if (!patch || typeof patch !== 'object') {
|
|
229
349
|
return history;
|
|
@@ -235,6 +355,37 @@ function patchWebSessionAgentState(webHistoryDir, containerName, patch) {
|
|
|
235
355
|
return history;
|
|
236
356
|
}
|
|
237
357
|
|
|
358
|
+
function patchWebAgentSessionState(webHistoryDir, sessionRefOrContainerName, patch) {
|
|
359
|
+
const sessionRef = typeof sessionRefOrContainerName === 'string'
|
|
360
|
+
? { containerName: sessionRefOrContainerName, agentId: WEB_DEFAULT_AGENT_ID }
|
|
361
|
+
: sessionRefOrContainerName;
|
|
362
|
+
const history = loadWebSessionHistory(webHistoryDir, sessionRef.containerName);
|
|
363
|
+
const agentSession = getWebAgentSession(history, sessionRef.agentId, { create: true });
|
|
364
|
+
if (!patch || typeof patch !== 'object') {
|
|
365
|
+
return agentSession;
|
|
366
|
+
}
|
|
367
|
+
Object.keys(patch).forEach(key => {
|
|
368
|
+
agentSession[key] = patch[key];
|
|
369
|
+
});
|
|
370
|
+
saveWebSessionHistory(webHistoryDir, sessionRef.containerName, history);
|
|
371
|
+
return agentSession;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function createWebAgentSession(history) {
|
|
375
|
+
const sessionHistory = history && typeof history === 'object' ? history : { agents: {} };
|
|
376
|
+
if (!sessionHistory.agents || typeof sessionHistory.agents !== 'object' || Array.isArray(sessionHistory.agents)) {
|
|
377
|
+
sessionHistory.agents = {};
|
|
378
|
+
}
|
|
379
|
+
let agentIndex = 2;
|
|
380
|
+
while (sessionHistory.agents[`agent-${agentIndex}`]) {
|
|
381
|
+
agentIndex += 1;
|
|
382
|
+
}
|
|
383
|
+
const agentId = `agent-${agentIndex}`;
|
|
384
|
+
const agentSession = createEmptyWebAgentSession(agentId, `AGENT ${agentIndex}`);
|
|
385
|
+
sessionHistory.agents[agentId] = agentSession;
|
|
386
|
+
return agentSession;
|
|
387
|
+
}
|
|
388
|
+
|
|
238
389
|
function stripAnsi(text) {
|
|
239
390
|
if (typeof text !== 'string') return '';
|
|
240
391
|
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
@@ -902,6 +1053,48 @@ function prepareStructuredTraceEvents(agentProgram, payload, state) {
|
|
|
902
1053
|
return [];
|
|
903
1054
|
}
|
|
904
1055
|
|
|
1056
|
+
function extractContentDeltaFromPayload(agentProgram, payload) {
|
|
1057
|
+
if (!payload || typeof payload !== 'object') {
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
if (agentProgram === 'claude') {
|
|
1061
|
+
if (pickFirstString(payload.type) !== 'assistant') {
|
|
1062
|
+
return null;
|
|
1063
|
+
}
|
|
1064
|
+
const message = toPlainObject(payload.message);
|
|
1065
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
1066
|
+
const text = content
|
|
1067
|
+
.filter(item => item && typeof item === 'object' && item.type === 'text')
|
|
1068
|
+
.map(item => collectStructuredText(item))
|
|
1069
|
+
.filter(Boolean)
|
|
1070
|
+
.join('\n')
|
|
1071
|
+
.trim();
|
|
1072
|
+
if (!text) {
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
return { text, reset: true };
|
|
1076
|
+
}
|
|
1077
|
+
if (agentProgram === 'gemini' || agentProgram === 'opencode') {
|
|
1078
|
+
const eventType = pickFirstString(payload.type);
|
|
1079
|
+
if (eventType !== 'message') {
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
const role = pickFirstString(payload.role);
|
|
1083
|
+
if (role !== 'assistant') {
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
const text = collectStructuredText(payload.content);
|
|
1087
|
+
if (!text) {
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
if (payload.delta === true) {
|
|
1091
|
+
return { text, reset: false };
|
|
1092
|
+
}
|
|
1093
|
+
return { text, reset: true };
|
|
1094
|
+
}
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
905
1098
|
function prepareCodexTraceEvent(payload) {
|
|
906
1099
|
if (!payload || typeof payload !== 'object') {
|
|
907
1100
|
return null;
|
|
@@ -1126,27 +1319,28 @@ function prepareCodexTraceEvent(payload) {
|
|
|
1126
1319
|
});
|
|
1127
1320
|
}
|
|
1128
1321
|
|
|
1129
|
-
async function prepareWebAgentExecution(ctx, state,
|
|
1130
|
-
const history = loadWebSessionHistory(state.webHistoryDir, containerName);
|
|
1322
|
+
async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
|
|
1323
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
1324
|
+
const agentSession = getWebAgentSession(history, sessionRef.agentId, { create: true });
|
|
1131
1325
|
const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
|
|
1132
1326
|
if (normalizedTemplate !== history.agentPromptCommand) {
|
|
1133
1327
|
history.agentPromptCommand = normalizedTemplate;
|
|
1134
|
-
saveWebSessionHistory(state.webHistoryDir, containerName, history);
|
|
1328
|
+
saveWebSessionHistory(state.webHistoryDir, sessionRef.containerName, history);
|
|
1135
1329
|
}
|
|
1136
1330
|
if (!isAgentPromptCommandEnabled(history.agentPromptCommand)) {
|
|
1137
1331
|
throw new Error('当前会话未配置 agentPromptCommand');
|
|
1138
1332
|
}
|
|
1139
1333
|
|
|
1140
|
-
await ensureWebContainer(ctx, state, containerName);
|
|
1334
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
1141
1335
|
const agentMeta = getAgentRuntimeMeta(history);
|
|
1142
|
-
const hasPriorConversation = hasAgentConversationHistory(
|
|
1336
|
+
const hasPriorConversation = hasAgentConversationHistory(agentSession);
|
|
1143
1337
|
let resumeAttempted = false;
|
|
1144
1338
|
let resumeSucceeded = false;
|
|
1145
1339
|
let resumeError = '';
|
|
1146
1340
|
|
|
1147
1341
|
if (hasPriorConversation && agentMeta.resumeSupported && agentMeta.resumeCommand) {
|
|
1148
1342
|
resumeAttempted = true;
|
|
1149
|
-
const resumeResult = await execCommandInWebContainer(ctx, containerName, agentMeta.resumeCommand);
|
|
1343
|
+
const resumeResult = await execCommandInWebContainer(ctx, sessionRef.containerName, agentMeta.resumeCommand);
|
|
1150
1344
|
if (resumeResult.exitCode === 0) {
|
|
1151
1345
|
resumeSucceeded = true;
|
|
1152
1346
|
} else {
|
|
@@ -1156,12 +1350,13 @@ async function prepareWebAgentExecution(ctx, state, containerName, prompt) {
|
|
|
1156
1350
|
|
|
1157
1351
|
const effectivePrompt = resumeSucceeded
|
|
1158
1352
|
? prompt
|
|
1159
|
-
: buildAgentPromptWithHistory(
|
|
1353
|
+
: buildAgentPromptWithHistory(agentSession, prompt);
|
|
1160
1354
|
const command = buildWebAgentExecCommand(history.agentPromptCommand, effectivePrompt, agentMeta.agentProgram);
|
|
1161
1355
|
const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
|
|
1162
1356
|
|
|
1163
1357
|
return {
|
|
1164
1358
|
history,
|
|
1359
|
+
agentSession,
|
|
1165
1360
|
agentMeta,
|
|
1166
1361
|
command,
|
|
1167
1362
|
contextMode,
|
|
@@ -1171,8 +1366,8 @@ async function prepareWebAgentExecution(ctx, state, containerName, prompt) {
|
|
|
1171
1366
|
};
|
|
1172
1367
|
}
|
|
1173
1368
|
|
|
1174
|
-
function finalizeWebAgentExecution(state,
|
|
1175
|
-
appendWebSessionMessage(state.webHistoryDir,
|
|
1369
|
+
function finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, meta, result) {
|
|
1370
|
+
appendWebSessionMessage(state.webHistoryDir, sessionRef, 'assistant', result.output, {
|
|
1176
1371
|
exitCode: result.exitCode,
|
|
1177
1372
|
mode: 'agent',
|
|
1178
1373
|
contextMode: meta.contextMode,
|
|
@@ -1180,21 +1375,19 @@ function finalizeWebAgentExecution(state, containerName, history, agentMeta, met
|
|
|
1180
1375
|
resumeSucceeded: meta.resumeSucceeded,
|
|
1181
1376
|
interrupted: result.interrupted === true
|
|
1182
1377
|
});
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
lastResumeOk: meta.resumeAttempted ? meta.resumeSucceeded : history.lastResumeOk,
|
|
1188
|
-
lastResumeError: meta.resumeAttempted ? (meta.resumeSucceeded ? '' : meta.resumeError) : history.lastResumeError || ''
|
|
1378
|
+
patchWebAgentSessionState(state.webHistoryDir, sessionRef, {
|
|
1379
|
+
lastResumeAt: meta.resumeAttempted ? new Date().toISOString() : (agentSession.lastResumeAt || null),
|
|
1380
|
+
lastResumeOk: meta.resumeAttempted ? meta.resumeSucceeded : agentSession.lastResumeOk,
|
|
1381
|
+
lastResumeError: meta.resumeAttempted ? (meta.resumeSucceeded ? '' : meta.resumeError) : (agentSession.lastResumeError || '')
|
|
1189
1382
|
});
|
|
1190
1383
|
}
|
|
1191
1384
|
|
|
1192
|
-
function appendWebAgentTraceMessage(webHistoryDir,
|
|
1385
|
+
function appendWebAgentTraceMessage(webHistoryDir, sessionRefOrContainerName, content, extra = {}) {
|
|
1193
1386
|
const text = String(content || '').trim();
|
|
1194
1387
|
if (!text) {
|
|
1195
1388
|
return;
|
|
1196
1389
|
}
|
|
1197
|
-
appendWebSessionMessage(webHistoryDir,
|
|
1390
|
+
appendWebSessionMessage(webHistoryDir, sessionRefOrContainerName, 'assistant', text, {
|
|
1198
1391
|
mode: 'agent',
|
|
1199
1392
|
streamTrace: true,
|
|
1200
1393
|
...extra
|
|
@@ -1982,7 +2175,7 @@ function listWebManyoyoContainers(ctx) {
|
|
|
1982
2175
|
return map;
|
|
1983
2176
|
}
|
|
1984
2177
|
|
|
1985
|
-
async function ensureWebContainer(ctx, state, containerInput) {
|
|
2178
|
+
async function ensureWebContainer(ctx, state, containerInput, messageSessionRef = null) {
|
|
1986
2179
|
const runtime = typeof containerInput === 'string'
|
|
1987
2180
|
? buildStaticContainerRuntime(ctx, containerInput)
|
|
1988
2181
|
: containerInput;
|
|
@@ -2014,14 +2207,24 @@ async function ensureWebContainer(ctx, state, containerInput) {
|
|
|
2014
2207
|
}
|
|
2015
2208
|
|
|
2016
2209
|
await ctx.waitForContainerReady(runtime.containerName);
|
|
2017
|
-
appendWebSessionMessage(
|
|
2210
|
+
appendWebSessionMessage(
|
|
2211
|
+
state.webHistoryDir,
|
|
2212
|
+
messageSessionRef || runtime.containerName,
|
|
2213
|
+
'system',
|
|
2214
|
+
`容器 ${runtime.containerName} 已创建并启动。`
|
|
2215
|
+
);
|
|
2018
2216
|
return;
|
|
2019
2217
|
}
|
|
2020
2218
|
|
|
2021
2219
|
const status = ctx.getContainerStatus(runtime.containerName);
|
|
2022
2220
|
if (status !== 'running') {
|
|
2023
2221
|
ctx.dockerExecArgs(['start', runtime.containerName], { stdio: 'pipe' });
|
|
2024
|
-
appendWebSessionMessage(
|
|
2222
|
+
appendWebSessionMessage(
|
|
2223
|
+
state.webHistoryDir,
|
|
2224
|
+
messageSessionRef || runtime.containerName,
|
|
2225
|
+
'system',
|
|
2226
|
+
`容器 ${runtime.containerName} 已启动。`
|
|
2227
|
+
);
|
|
2025
2228
|
}
|
|
2026
2229
|
}
|
|
2027
2230
|
|
|
@@ -2085,24 +2288,29 @@ async function execCommandInWebContainer(ctx, containerName, command, options =
|
|
|
2085
2288
|
});
|
|
2086
2289
|
}
|
|
2087
2290
|
|
|
2088
|
-
async function execAgentInWebContainerStream(ctx, state,
|
|
2291
|
+
async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerName, command, options = {}) {
|
|
2089
2292
|
const opts = options && typeof options === 'object' ? options : {};
|
|
2293
|
+
const sessionRef = typeof sessionRefOrContainerName === 'string'
|
|
2294
|
+
? { containerName: sessionRefOrContainerName, agentId: WEB_DEFAULT_AGENT_ID }
|
|
2295
|
+
: sessionRefOrContainerName;
|
|
2296
|
+
const sessionKey = buildWebSessionKey(sessionRef.containerName, sessionRef.agentId);
|
|
2090
2297
|
const agentProgram = typeof opts.agentProgram === 'string' ? opts.agentProgram : '';
|
|
2091
2298
|
const onEvent = typeof opts.onEvent === 'function' ? opts.onEvent : () => {};
|
|
2092
2299
|
const process = spawn(
|
|
2093
2300
|
ctx.dockerCmd,
|
|
2094
|
-
['exec', containerName, '/bin/bash', '-lc', command],
|
|
2301
|
+
['exec', sessionRef.containerName, '/bin/bash', '-lc', command],
|
|
2095
2302
|
{ stdio: ['ignore', 'pipe', 'pipe'] }
|
|
2096
2303
|
);
|
|
2097
2304
|
|
|
2098
2305
|
const runState = {
|
|
2099
|
-
containerName,
|
|
2306
|
+
containerName: sessionRef.containerName,
|
|
2307
|
+
sessionKey,
|
|
2100
2308
|
process,
|
|
2101
2309
|
command,
|
|
2102
2310
|
startedAt: new Date().toISOString(),
|
|
2103
2311
|
stopping: false
|
|
2104
2312
|
};
|
|
2105
|
-
state.agentRuns.set(containerName, runState);
|
|
2313
|
+
state.agentRuns.set(sessionRef.containerName, runState);
|
|
2106
2314
|
|
|
2107
2315
|
return await new Promise((resolve, reject) => {
|
|
2108
2316
|
const MAX_RAW_OUTPUT_CHARS = 32 * 1024 * 1024;
|
|
@@ -2115,6 +2323,7 @@ async function execAgentInWebContainerStream(ctx, state, containerName, command,
|
|
|
2115
2323
|
const structuredTraceState = {
|
|
2116
2324
|
toolNamesById: new Map()
|
|
2117
2325
|
};
|
|
2326
|
+
let contentDeltaAccumulator = '';
|
|
2118
2327
|
function appendChunk(chunk, target) {
|
|
2119
2328
|
if (!chunk) return;
|
|
2120
2329
|
const text = chunk.toString('utf-8');
|
|
@@ -2152,6 +2361,18 @@ async function execAgentInWebContainerStream(ctx, state, containerName, command,
|
|
|
2152
2361
|
traceEvent
|
|
2153
2362
|
});
|
|
2154
2363
|
});
|
|
2364
|
+
const deltaContent = extractContentDeltaFromPayload(agentProgram, payload, structuredTraceState);
|
|
2365
|
+
if (deltaContent !== null) {
|
|
2366
|
+
if (deltaContent.reset) {
|
|
2367
|
+
contentDeltaAccumulator = deltaContent.text;
|
|
2368
|
+
} else {
|
|
2369
|
+
contentDeltaAccumulator += deltaContent.text;
|
|
2370
|
+
}
|
|
2371
|
+
onEvent({
|
|
2372
|
+
type: 'content_delta',
|
|
2373
|
+
content: contentDeltaAccumulator
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2155
2376
|
return;
|
|
2156
2377
|
}
|
|
2157
2378
|
if (agentProgram === 'codex' && (/^OpenAI Codex\b/.test(rawLine) || /^tokens used\b/i.test(rawLine))) {
|
|
@@ -2201,11 +2422,11 @@ async function execAgentInWebContainerStream(ctx, state, containerName, command,
|
|
|
2201
2422
|
});
|
|
2202
2423
|
|
|
2203
2424
|
process.on('error', error => {
|
|
2204
|
-
state.agentRuns.delete(containerName);
|
|
2425
|
+
state.agentRuns.delete(sessionRef.containerName);
|
|
2205
2426
|
reject(error);
|
|
2206
2427
|
});
|
|
2207
2428
|
process.on('close', code => {
|
|
2208
|
-
state.agentRuns.delete(containerName);
|
|
2429
|
+
state.agentRuns.delete(sessionRef.containerName);
|
|
2209
2430
|
if (stdoutPending) {
|
|
2210
2431
|
emitStdoutTraceLine(stdoutPending);
|
|
2211
2432
|
stdoutPending = '';
|
|
@@ -2310,30 +2531,51 @@ function decodeSessionName(encoded) {
|
|
|
2310
2531
|
}
|
|
2311
2532
|
}
|
|
2312
2533
|
|
|
2313
|
-
function
|
|
2314
|
-
const
|
|
2315
|
-
if (!ctx.isValidContainerName(containerName)) {
|
|
2316
|
-
sendJson(res, 400, { error: `containerName 非法: ${containerName}` });
|
|
2534
|
+
function getValidSessionRef(ctx, res, encodedName) {
|
|
2535
|
+
const parsed = parseWebSessionKey(decodeSessionName(encodedName));
|
|
2536
|
+
if (!ctx.isValidContainerName(parsed.containerName)) {
|
|
2537
|
+
sendJson(res, 400, { error: `containerName 非法: ${parsed.containerName}` });
|
|
2538
|
+
return null;
|
|
2539
|
+
}
|
|
2540
|
+
if (!SAFE_CONTAINER_NAME_PATTERN.test(parsed.agentId)) {
|
|
2541
|
+
sendJson(res, 400, { error: `agentId 非法: ${parsed.agentId}` });
|
|
2317
2542
|
return null;
|
|
2318
2543
|
}
|
|
2319
|
-
return
|
|
2544
|
+
return parsed;
|
|
2320
2545
|
}
|
|
2321
2546
|
|
|
2322
|
-
function buildSessionSummary(ctx, state, containerMap,
|
|
2323
|
-
const
|
|
2547
|
+
function buildSessionSummary(ctx, state, containerMap, sessionRef) {
|
|
2548
|
+
const containerName = sessionRef && sessionRef.containerName ? sessionRef.containerName : '';
|
|
2549
|
+
const agentId = sessionRef && sessionRef.agentId ? sessionRef.agentId : WEB_DEFAULT_AGENT_ID;
|
|
2550
|
+
const history = loadWebSessionHistory(state.webHistoryDir, containerName);
|
|
2324
2551
|
const agentMeta = getAgentRuntimeMeta(history);
|
|
2325
|
-
const
|
|
2326
|
-
|
|
2327
|
-
|
|
2552
|
+
const agentSession = getWebAgentSession(history, agentId)
|
|
2553
|
+
|| (agentId === WEB_DEFAULT_AGENT_ID ? createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID) : null);
|
|
2554
|
+
if (!agentSession) {
|
|
2555
|
+
return null;
|
|
2556
|
+
}
|
|
2557
|
+
const latestMessage = agentSession.messages.length ? agentSession.messages[agentSession.messages.length - 1] : null;
|
|
2558
|
+
const containerInfo = containerMap[containerName] || {};
|
|
2559
|
+
const applied = history.applied && typeof history.applied === 'object' && !Array.isArray(history.applied)
|
|
2560
|
+
? history.applied
|
|
2561
|
+
: buildSessionFallbackApplied(ctx, state, containerName, history, {
|
|
2562
|
+
status: containerInfo.status || 'history'
|
|
2563
|
+
});
|
|
2564
|
+
const updatedAt = agentSession.updatedAt || history.updatedAt || (latestMessage && latestMessage.timestamp) || containerInfo.createdAt || null;
|
|
2328
2565
|
return {
|
|
2329
|
-
name,
|
|
2566
|
+
name: buildWebSessionKey(containerName, agentId),
|
|
2567
|
+
containerName,
|
|
2568
|
+
agentId,
|
|
2569
|
+
agentName: agentSession.agentName,
|
|
2330
2570
|
status: containerInfo.status || 'history',
|
|
2331
2571
|
image: containerInfo.image || '',
|
|
2332
2572
|
updatedAt,
|
|
2333
|
-
messageCount:
|
|
2573
|
+
messageCount: agentSession.messages.length,
|
|
2334
2574
|
agentEnabled: isAgentPromptCommandEnabled(history.agentPromptCommand),
|
|
2335
2575
|
agentProgram: agentMeta.agentProgram,
|
|
2336
|
-
resumeSupported: agentMeta.resumeSupported
|
|
2576
|
+
resumeSupported: agentMeta.resumeSupported,
|
|
2577
|
+
hostPath: applied.hostPath || '',
|
|
2578
|
+
containerPath: applied.containerPath || ''
|
|
2337
2579
|
};
|
|
2338
2580
|
}
|
|
2339
2581
|
|
|
@@ -2341,9 +2583,8 @@ function buildSessionFallbackApplied(ctx, state, name, history, summary) {
|
|
|
2341
2583
|
const snapshot = readWebConfigSnapshot(state.webConfigPath);
|
|
2342
2584
|
const defaults = buildConfigDefaults(ctx, snapshot.parseError ? {} : snapshot.parsed);
|
|
2343
2585
|
const effectiveAgentPromptCommand = history.agentPromptCommand || defaults.agentPromptCommand || '';
|
|
2344
|
-
const effectiveAgentProgram =
|
|
2345
|
-
const effectiveResumeSupported =
|
|
2346
|
-
|| Boolean(buildAgentResumeCommand(effectiveAgentProgram));
|
|
2586
|
+
const effectiveAgentProgram = resolveAgentProgram(effectiveAgentPromptCommand) || '';
|
|
2587
|
+
const effectiveResumeSupported = Boolean(buildAgentResumeCommand(effectiveAgentProgram));
|
|
2347
2588
|
const defaultCommand = buildDefaultCommand(
|
|
2348
2589
|
defaults.shellPrefix,
|
|
2349
2590
|
defaults.shell,
|
|
@@ -2373,24 +2614,34 @@ function buildSessionFallbackApplied(ctx, state, name, history, summary) {
|
|
|
2373
2614
|
}
|
|
2374
2615
|
|
|
2375
2616
|
function buildSessionDetail(ctx, state, containerMap, name) {
|
|
2376
|
-
const
|
|
2617
|
+
const sessionRef = typeof name === 'string' ? parseWebSessionKey(name) : name;
|
|
2618
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
2377
2619
|
const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
|
|
2378
|
-
const summary = buildSessionSummary(ctx, state, containerMap,
|
|
2379
|
-
const
|
|
2620
|
+
const summary = buildSessionSummary(ctx, state, containerMap, sessionRef);
|
|
2621
|
+
const agentSession = getWebAgentSession(history, sessionRef.agentId)
|
|
2622
|
+
|| (sessionRef.agentId === WEB_DEFAULT_AGENT_ID ? createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID) : null);
|
|
2623
|
+
const latestMessage = agentSession && agentSession.messages.length
|
|
2624
|
+
? agentSession.messages[agentSession.messages.length - 1]
|
|
2625
|
+
: null;
|
|
2380
2626
|
const applied = history.applied && typeof history.applied === 'object' && !Array.isArray(history.applied)
|
|
2381
2627
|
? history.applied
|
|
2382
|
-
: buildSessionFallbackApplied(ctx, state,
|
|
2628
|
+
: buildSessionFallbackApplied(ctx, state, sessionRef.containerName, history, summary || {});
|
|
2629
|
+
|
|
2630
|
+
if (!summary || !agentSession) {
|
|
2631
|
+
return null;
|
|
2632
|
+
}
|
|
2383
2633
|
|
|
2384
2634
|
return {
|
|
2385
2635
|
...summary,
|
|
2636
|
+
agentName: agentSession.agentName,
|
|
2386
2637
|
latestRole: latestMessage && latestMessage.role ? String(latestMessage.role) : '',
|
|
2387
2638
|
latestTimestamp: latestMessage && latestMessage.timestamp ? latestMessage.timestamp : summary.updatedAt,
|
|
2388
2639
|
agentPromptCommand: normalizedTemplate || '',
|
|
2389
|
-
agentProgram:
|
|
2390
|
-
resumeSupported:
|
|
2391
|
-
lastResumeAt:
|
|
2392
|
-
lastResumeOk: typeof
|
|
2393
|
-
lastResumeError:
|
|
2640
|
+
agentProgram: summary.agentProgram || '',
|
|
2641
|
+
resumeSupported: summary.resumeSupported === true,
|
|
2642
|
+
lastResumeAt: agentSession.lastResumeAt || null,
|
|
2643
|
+
lastResumeOk: typeof agentSession.lastResumeOk === 'boolean' ? agentSession.lastResumeOk : null,
|
|
2644
|
+
lastResumeError: agentSession.lastResumeError || '',
|
|
2394
2645
|
applied
|
|
2395
2646
|
};
|
|
2396
2647
|
}
|
|
@@ -2739,6 +2990,52 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2739
2990
|
}
|
|
2740
2991
|
}
|
|
2741
2992
|
const routes = [
|
|
2993
|
+
{
|
|
2994
|
+
method: 'GET',
|
|
2995
|
+
match: currentPath => currentPath === '/api/fs/directories' ? [] : null,
|
|
2996
|
+
handler: async () => {
|
|
2997
|
+
const requestUrl = new URL(req.url || '/api/fs/directories', 'http://localhost');
|
|
2998
|
+
const requestedPath = expandHomeAliasPath(String(requestUrl.searchParams.get('path') || '').trim() || os.homedir());
|
|
2999
|
+
const requestedBasePath = expandHomeAliasPath(String(requestUrl.searchParams.get('basePath') || '').trim());
|
|
3000
|
+
const realPath = fs.realpathSync(requestedPath);
|
|
3001
|
+
if (!fs.statSync(realPath).isDirectory()) {
|
|
3002
|
+
sendJson(res, 400, { error: `目录不存在: ${realPath}` });
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
let realBasePath = '';
|
|
3007
|
+
if (requestedBasePath) {
|
|
3008
|
+
realBasePath = fs.realpathSync(requestedBasePath);
|
|
3009
|
+
if (!fs.statSync(realBasePath).isDirectory()) {
|
|
3010
|
+
sendJson(res, 400, { error: `basePath 不是目录: ${realBasePath}` });
|
|
3011
|
+
return;
|
|
3012
|
+
}
|
|
3013
|
+
const relativeToBase = path.relative(realBasePath, realPath);
|
|
3014
|
+
if (relativeToBase.startsWith('..') || path.isAbsolute(relativeToBase)) {
|
|
3015
|
+
sendJson(res, 400, { error: '目录超出 basePath 范围' });
|
|
3016
|
+
return;
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
const parentPath = realBasePath
|
|
3021
|
+
? (realPath === realBasePath ? '' : path.dirname(realPath))
|
|
3022
|
+
: (realPath === path.parse(realPath).root ? '' : path.dirname(realPath));
|
|
3023
|
+
const entries = fs.readdirSync(realPath, { withFileTypes: true })
|
|
3024
|
+
.filter(entry => entry && entry.isDirectory())
|
|
3025
|
+
.map(entry => ({
|
|
3026
|
+
name: entry.name,
|
|
3027
|
+
path: path.join(realPath, entry.name)
|
|
3028
|
+
}))
|
|
3029
|
+
.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
|
|
3030
|
+
|
|
3031
|
+
sendJson(res, 200, {
|
|
3032
|
+
currentPath: realPath,
|
|
3033
|
+
basePath: realBasePath || '',
|
|
3034
|
+
parentPath,
|
|
3035
|
+
entries
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
},
|
|
2742
3039
|
{
|
|
2743
3040
|
method: 'GET',
|
|
2744
3041
|
match: currentPath => currentPath === '/api/config' ? [] : null,
|
|
@@ -2788,7 +3085,15 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2788
3085
|
]);
|
|
2789
3086
|
|
|
2790
3087
|
const sessions = Array.from(names)
|
|
2791
|
-
.
|
|
3088
|
+
.flatMap(name => {
|
|
3089
|
+
const history = loadWebSessionHistory(state.webHistoryDir, name);
|
|
3090
|
+
return listWebAgentSessions(history, { includeSyntheticDefault: true })
|
|
3091
|
+
.map(agentSession => buildSessionSummary(ctx, state, containerMap, {
|
|
3092
|
+
containerName: name,
|
|
3093
|
+
agentId: agentSession.agentId
|
|
3094
|
+
}))
|
|
3095
|
+
.filter(Boolean);
|
|
3096
|
+
})
|
|
2792
3097
|
.sort((a, b) => {
|
|
2793
3098
|
const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
|
2794
3099
|
const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
|
@@ -2813,44 +3118,70 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2813
3118
|
|
|
2814
3119
|
await ensureWebContainer(ctx, state, runtime);
|
|
2815
3120
|
setWebSessionAgentPromptCommand(state.webHistoryDir, runtime.containerName, runtime.agentPromptCommand);
|
|
2816
|
-
|
|
3121
|
+
patchWebSessionHistory(state.webHistoryDir, runtime.containerName, {
|
|
2817
3122
|
applied: runtime.applied
|
|
2818
3123
|
});
|
|
2819
3124
|
sendJson(res, 200, { name: runtime.containerName, applied: runtime.applied });
|
|
2820
3125
|
}
|
|
2821
3126
|
},
|
|
3127
|
+
{
|
|
3128
|
+
method: 'POST',
|
|
3129
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agents$/),
|
|
3130
|
+
handler: async match => {
|
|
3131
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3132
|
+
if (!sessionRef) {
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
3136
|
+
const agentSession = createWebAgentSession(history);
|
|
3137
|
+
saveWebSessionHistory(state.webHistoryDir, sessionRef.containerName, history);
|
|
3138
|
+
sendJson(res, 200, {
|
|
3139
|
+
name: buildWebSessionKey(sessionRef.containerName, agentSession.agentId),
|
|
3140
|
+
containerName: sessionRef.containerName,
|
|
3141
|
+
agentId: agentSession.agentId,
|
|
3142
|
+
agentName: agentSession.agentName
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
},
|
|
2822
3146
|
{
|
|
2823
3147
|
method: 'GET',
|
|
2824
3148
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/messages$/),
|
|
2825
3149
|
handler: async match => {
|
|
2826
|
-
const
|
|
2827
|
-
if (!
|
|
3150
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3151
|
+
if (!sessionRef) {
|
|
2828
3152
|
return;
|
|
2829
3153
|
}
|
|
2830
|
-
const history = loadWebSessionHistory(state.webHistoryDir, containerName);
|
|
2831
|
-
|
|
3154
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
3155
|
+
const agentSession = getWebAgentSession(history, sessionRef.agentId)
|
|
3156
|
+
|| createEmptyWebAgentSession(sessionRef.agentId);
|
|
3157
|
+
sendJson(res, 200, {
|
|
3158
|
+
name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
|
|
3159
|
+
containerName: sessionRef.containerName,
|
|
3160
|
+
agentId: sessionRef.agentId,
|
|
3161
|
+
messages: agentSession.messages
|
|
3162
|
+
});
|
|
2832
3163
|
}
|
|
2833
3164
|
},
|
|
2834
3165
|
{
|
|
2835
3166
|
method: 'GET',
|
|
2836
3167
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/detail$/),
|
|
2837
3168
|
handler: async match => {
|
|
2838
|
-
const
|
|
2839
|
-
if (!
|
|
3169
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3170
|
+
if (!sessionRef) {
|
|
2840
3171
|
return;
|
|
2841
3172
|
}
|
|
2842
3173
|
|
|
2843
3174
|
const containerMap = listWebManyoyoContainers(ctx);
|
|
2844
|
-
const detail = buildSessionDetail(ctx, state, containerMap,
|
|
2845
|
-
sendJson(res, 200, { name: containerName, detail });
|
|
3175
|
+
const detail = buildSessionDetail(ctx, state, containerMap, sessionRef);
|
|
3176
|
+
sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), detail });
|
|
2846
3177
|
}
|
|
2847
3178
|
},
|
|
2848
3179
|
{
|
|
2849
3180
|
method: 'POST',
|
|
2850
3181
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/run$/),
|
|
2851
3182
|
handler: async match => {
|
|
2852
|
-
const
|
|
2853
|
-
if (!
|
|
3183
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3184
|
+
if (!sessionRef) {
|
|
2854
3185
|
return;
|
|
2855
3186
|
}
|
|
2856
3187
|
|
|
@@ -2861,12 +3192,12 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2861
3192
|
return;
|
|
2862
3193
|
}
|
|
2863
3194
|
|
|
2864
|
-
await ensureWebContainer(ctx, state, containerName);
|
|
2865
|
-
appendWebSessionMessage(state.webHistoryDir,
|
|
2866
|
-
const result = await execCommandInWebContainer(ctx, containerName, command);
|
|
3195
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
3196
|
+
appendWebSessionMessage(state.webHistoryDir, sessionRef, 'user', command);
|
|
3197
|
+
const result = await execCommandInWebContainer(ctx, sessionRef.containerName, command);
|
|
2867
3198
|
appendWebSessionMessage(
|
|
2868
3199
|
state.webHistoryDir,
|
|
2869
|
-
|
|
3200
|
+
sessionRef,
|
|
2870
3201
|
'assistant',
|
|
2871
3202
|
result.output,
|
|
2872
3203
|
{ exitCode: result.exitCode }
|
|
@@ -2878,8 +3209,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2878
3209
|
method: 'POST',
|
|
2879
3210
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent$/),
|
|
2880
3211
|
handler: async match => {
|
|
2881
|
-
const
|
|
2882
|
-
if (!
|
|
3212
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3213
|
+
if (!sessionRef) {
|
|
2883
3214
|
return;
|
|
2884
3215
|
}
|
|
2885
3216
|
|
|
@@ -2892,21 +3223,21 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2892
3223
|
|
|
2893
3224
|
let prepared = null;
|
|
2894
3225
|
try {
|
|
2895
|
-
prepared = await prepareWebAgentExecution(ctx, state,
|
|
3226
|
+
prepared = await prepareWebAgentExecution(ctx, state, sessionRef, prompt);
|
|
2896
3227
|
} catch (e) {
|
|
2897
3228
|
sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
|
|
2898
3229
|
return;
|
|
2899
3230
|
}
|
|
2900
3231
|
|
|
2901
|
-
const {
|
|
2902
|
-
appendWebSessionMessage(state.webHistoryDir,
|
|
3232
|
+
const { agentSession, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
|
|
3233
|
+
appendWebSessionMessage(state.webHistoryDir, sessionRef, 'user', prompt, {
|
|
2903
3234
|
mode: 'agent',
|
|
2904
3235
|
contextMode
|
|
2905
3236
|
});
|
|
2906
|
-
const result = await execCommandInWebContainer(ctx, containerName, command, {
|
|
3237
|
+
const result = await execCommandInWebContainer(ctx, sessionRef.containerName, command, {
|
|
2907
3238
|
agentProgram: agentMeta.agentProgram
|
|
2908
3239
|
});
|
|
2909
|
-
finalizeWebAgentExecution(state,
|
|
3240
|
+
finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, {
|
|
2910
3241
|
contextMode,
|
|
2911
3242
|
resumeAttempted,
|
|
2912
3243
|
resumeSucceeded,
|
|
@@ -2926,8 +3257,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2926
3257
|
method: 'POST',
|
|
2927
3258
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent\/stream$/),
|
|
2928
3259
|
handler: async match => {
|
|
2929
|
-
const
|
|
2930
|
-
if (!
|
|
3260
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3261
|
+
if (!sessionRef) {
|
|
2931
3262
|
return;
|
|
2932
3263
|
}
|
|
2933
3264
|
|
|
@@ -2937,23 +3268,23 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2937
3268
|
sendJson(res, 400, { error: 'prompt 不能为空' });
|
|
2938
3269
|
return;
|
|
2939
3270
|
}
|
|
2940
|
-
if (state.agentRuns.has(containerName)) {
|
|
3271
|
+
if (state.agentRuns.has(sessionRef.containerName)) {
|
|
2941
3272
|
sendJson(res, 409, { error: '当前会话已有运行中的 agent 任务' });
|
|
2942
3273
|
return;
|
|
2943
3274
|
}
|
|
2944
3275
|
|
|
2945
3276
|
let prepared = null;
|
|
2946
3277
|
try {
|
|
2947
|
-
prepared = await prepareWebAgentExecution(ctx, state,
|
|
3278
|
+
prepared = await prepareWebAgentExecution(ctx, state, sessionRef, prompt);
|
|
2948
3279
|
} catch (e) {
|
|
2949
3280
|
sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
|
|
2950
3281
|
return;
|
|
2951
3282
|
}
|
|
2952
3283
|
|
|
2953
|
-
const {
|
|
3284
|
+
const { agentSession, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
|
|
2954
3285
|
const traceLines = ['[执行过程]'];
|
|
2955
3286
|
const traceEvents = [];
|
|
2956
|
-
appendWebSessionMessage(state.webHistoryDir,
|
|
3287
|
+
appendWebSessionMessage(state.webHistoryDir, sessionRef, 'user', prompt, {
|
|
2957
3288
|
mode: 'agent',
|
|
2958
3289
|
contextMode
|
|
2959
3290
|
});
|
|
@@ -2965,7 +3296,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2965
3296
|
});
|
|
2966
3297
|
sendNdjson(res, {
|
|
2967
3298
|
type: 'meta',
|
|
2968
|
-
containerName,
|
|
3299
|
+
containerName: sessionRef.containerName,
|
|
3300
|
+
sessionName: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
|
|
2969
3301
|
contextMode,
|
|
2970
3302
|
resumeAttempted,
|
|
2971
3303
|
resumeSucceeded,
|
|
@@ -2979,7 +3311,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2979
3311
|
}
|
|
2980
3312
|
|
|
2981
3313
|
try {
|
|
2982
|
-
const result = await execAgentInWebContainerStream(ctx, state,
|
|
3314
|
+
const result = await execAgentInWebContainerStream(ctx, state, sessionRef, command, {
|
|
2983
3315
|
agentProgram: agentMeta.agentProgram,
|
|
2984
3316
|
onEvent: event => {
|
|
2985
3317
|
if (event && event.type === 'trace' && event.text) {
|
|
@@ -2992,14 +3324,14 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2992
3324
|
}
|
|
2993
3325
|
});
|
|
2994
3326
|
traceLines.push(result.interrupted === true ? '[任务] 已停止' : '[任务] 已完成');
|
|
2995
|
-
appendWebAgentTraceMessage(state.webHistoryDir,
|
|
3327
|
+
appendWebAgentTraceMessage(state.webHistoryDir, sessionRef, traceLines.join('\n'), {
|
|
2996
3328
|
traceEvents,
|
|
2997
3329
|
contextMode,
|
|
2998
3330
|
resumeAttempted,
|
|
2999
3331
|
resumeSucceeded,
|
|
3000
3332
|
interrupted: result.interrupted === true
|
|
3001
3333
|
});
|
|
3002
|
-
finalizeWebAgentExecution(state,
|
|
3334
|
+
finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, {
|
|
3003
3335
|
contextMode,
|
|
3004
3336
|
resumeAttempted,
|
|
3005
3337
|
resumeSucceeded,
|
|
@@ -3016,7 +3348,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3016
3348
|
});
|
|
3017
3349
|
} catch (e) {
|
|
3018
3350
|
traceLines.push(`[错误] ${e && e.message ? e.message : 'Agent 执行失败'}`);
|
|
3019
|
-
appendWebAgentTraceMessage(state.webHistoryDir,
|
|
3351
|
+
appendWebAgentTraceMessage(state.webHistoryDir, sessionRef, traceLines.join('\n'), {
|
|
3020
3352
|
traceEvents,
|
|
3021
3353
|
contextMode,
|
|
3022
3354
|
resumeAttempted,
|
|
@@ -3036,11 +3368,11 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3036
3368
|
method: 'POST',
|
|
3037
3369
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent\/stop$/),
|
|
3038
3370
|
handler: async match => {
|
|
3039
|
-
const
|
|
3040
|
-
if (!
|
|
3371
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3372
|
+
if (!sessionRef) {
|
|
3041
3373
|
return;
|
|
3042
3374
|
}
|
|
3043
|
-
const stopped = stopWebAgentRun(state, containerName);
|
|
3375
|
+
const stopped = stopWebAgentRun(state, sessionRef.containerName);
|
|
3044
3376
|
if (!stopped) {
|
|
3045
3377
|
sendJson(res, 404, { error: '当前会话没有运行中的 agent 任务' });
|
|
3046
3378
|
return;
|
|
@@ -3052,30 +3384,45 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3052
3384
|
method: 'POST',
|
|
3053
3385
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/remove$/),
|
|
3054
3386
|
handler: async match => {
|
|
3055
|
-
const
|
|
3056
|
-
if (!
|
|
3387
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3388
|
+
if (!sessionRef) {
|
|
3057
3389
|
return;
|
|
3058
3390
|
}
|
|
3059
3391
|
|
|
3060
|
-
if (ctx.containerExists(containerName)) {
|
|
3061
|
-
ctx.removeContainer(containerName);
|
|
3062
|
-
appendWebSessionMessage(state.webHistoryDir,
|
|
3392
|
+
if (ctx.containerExists(sessionRef.containerName)) {
|
|
3393
|
+
ctx.removeContainer(sessionRef.containerName);
|
|
3394
|
+
appendWebSessionMessage(state.webHistoryDir, sessionRef, 'system', `容器 ${sessionRef.containerName} 已删除。`);
|
|
3063
3395
|
}
|
|
3064
3396
|
|
|
3065
|
-
sendJson(res, 200, { removed: true, name: containerName });
|
|
3397
|
+
sendJson(res, 200, { removed: true, name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId) });
|
|
3066
3398
|
}
|
|
3067
3399
|
},
|
|
3068
3400
|
{
|
|
3069
3401
|
method: 'POST',
|
|
3070
3402
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/remove-with-history$/),
|
|
3071
3403
|
handler: async match => {
|
|
3072
|
-
const
|
|
3073
|
-
if (!
|
|
3404
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3405
|
+
if (!sessionRef) {
|
|
3074
3406
|
return;
|
|
3075
3407
|
}
|
|
3076
3408
|
|
|
3077
|
-
|
|
3078
|
-
|
|
3409
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
3410
|
+
if (history.agents && typeof history.agents === 'object') {
|
|
3411
|
+
if (sessionRef.agentId === WEB_DEFAULT_AGENT_ID) {
|
|
3412
|
+
delete history.agents[WEB_DEFAULT_AGENT_ID];
|
|
3413
|
+
} else {
|
|
3414
|
+
delete history.agents[sessionRef.agentId];
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
if (!Object.keys(history.agents || {}).length && !ctx.containerExists(sessionRef.containerName)) {
|
|
3418
|
+
removeWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
3419
|
+
} else {
|
|
3420
|
+
saveWebSessionHistory(state.webHistoryDir, sessionRef.containerName, history);
|
|
3421
|
+
}
|
|
3422
|
+
sendJson(res, 200, {
|
|
3423
|
+
removedHistory: true,
|
|
3424
|
+
name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId)
|
|
3425
|
+
});
|
|
3079
3426
|
}
|
|
3080
3427
|
}
|
|
3081
3428
|
];
|
|
@@ -3293,9 +3640,13 @@ async function startWebServer(options) {
|
|
|
3293
3640
|
return;
|
|
3294
3641
|
}
|
|
3295
3642
|
|
|
3296
|
-
const
|
|
3297
|
-
if (!ctx.isValidContainerName(containerName)) {
|
|
3298
|
-
sendWebSocketUpgradeError(socket, 400, `containerName 非法: ${containerName}`);
|
|
3643
|
+
const sessionRef = parseWebSessionKey(decodeSessionName(terminalMatch[1]));
|
|
3644
|
+
if (!ctx.isValidContainerName(sessionRef.containerName)) {
|
|
3645
|
+
sendWebSocketUpgradeError(socket, 400, `containerName 非法: ${sessionRef.containerName}`);
|
|
3646
|
+
return;
|
|
3647
|
+
}
|
|
3648
|
+
if (!SAFE_CONTAINER_NAME_PATTERN.test(sessionRef.agentId)) {
|
|
3649
|
+
sendWebSocketUpgradeError(socket, 400, `agentId 非法: ${sessionRef.agentId}`);
|
|
3299
3650
|
return;
|
|
3300
3651
|
}
|
|
3301
3652
|
|
|
@@ -3309,11 +3660,11 @@ async function startWebServer(options) {
|
|
|
3309
3660
|
url.searchParams.get('rows')
|
|
3310
3661
|
);
|
|
3311
3662
|
|
|
3312
|
-
ensureWebContainer(ctx, state, containerName)
|
|
3663
|
+
ensureWebContainer(ctx, state, sessionRef.containerName)
|
|
3313
3664
|
.then(() => {
|
|
3314
3665
|
wsServer.handleUpgrade(req, socket, head, ws => {
|
|
3315
3666
|
wsServer.emit('connection', ws, req, {
|
|
3316
|
-
containerName,
|
|
3667
|
+
containerName: sessionRef.containerName,
|
|
3317
3668
|
cols,
|
|
3318
3669
|
rows
|
|
3319
3670
|
});
|