@xcanwin/manyoyo 5.7.3 → 5.7.4
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 +169 -0
- package/lib/web/frontend/app.html +32 -3
- package/lib/web/frontend/app.js +412 -102
- package/lib/web/server.js +457 -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, '');
|
|
@@ -1126,27 +1277,28 @@ function prepareCodexTraceEvent(payload) {
|
|
|
1126
1277
|
});
|
|
1127
1278
|
}
|
|
1128
1279
|
|
|
1129
|
-
async function prepareWebAgentExecution(ctx, state,
|
|
1130
|
-
const history = loadWebSessionHistory(state.webHistoryDir, containerName);
|
|
1280
|
+
async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
|
|
1281
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
1282
|
+
const agentSession = getWebAgentSession(history, sessionRef.agentId, { create: true });
|
|
1131
1283
|
const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
|
|
1132
1284
|
if (normalizedTemplate !== history.agentPromptCommand) {
|
|
1133
1285
|
history.agentPromptCommand = normalizedTemplate;
|
|
1134
|
-
saveWebSessionHistory(state.webHistoryDir, containerName, history);
|
|
1286
|
+
saveWebSessionHistory(state.webHistoryDir, sessionRef.containerName, history);
|
|
1135
1287
|
}
|
|
1136
1288
|
if (!isAgentPromptCommandEnabled(history.agentPromptCommand)) {
|
|
1137
1289
|
throw new Error('当前会话未配置 agentPromptCommand');
|
|
1138
1290
|
}
|
|
1139
1291
|
|
|
1140
|
-
await ensureWebContainer(ctx, state, containerName);
|
|
1292
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
1141
1293
|
const agentMeta = getAgentRuntimeMeta(history);
|
|
1142
|
-
const hasPriorConversation = hasAgentConversationHistory(
|
|
1294
|
+
const hasPriorConversation = hasAgentConversationHistory(agentSession);
|
|
1143
1295
|
let resumeAttempted = false;
|
|
1144
1296
|
let resumeSucceeded = false;
|
|
1145
1297
|
let resumeError = '';
|
|
1146
1298
|
|
|
1147
1299
|
if (hasPriorConversation && agentMeta.resumeSupported && agentMeta.resumeCommand) {
|
|
1148
1300
|
resumeAttempted = true;
|
|
1149
|
-
const resumeResult = await execCommandInWebContainer(ctx, containerName, agentMeta.resumeCommand);
|
|
1301
|
+
const resumeResult = await execCommandInWebContainer(ctx, sessionRef.containerName, agentMeta.resumeCommand);
|
|
1150
1302
|
if (resumeResult.exitCode === 0) {
|
|
1151
1303
|
resumeSucceeded = true;
|
|
1152
1304
|
} else {
|
|
@@ -1156,12 +1308,13 @@ async function prepareWebAgentExecution(ctx, state, containerName, prompt) {
|
|
|
1156
1308
|
|
|
1157
1309
|
const effectivePrompt = resumeSucceeded
|
|
1158
1310
|
? prompt
|
|
1159
|
-
: buildAgentPromptWithHistory(
|
|
1311
|
+
: buildAgentPromptWithHistory(agentSession, prompt);
|
|
1160
1312
|
const command = buildWebAgentExecCommand(history.agentPromptCommand, effectivePrompt, agentMeta.agentProgram);
|
|
1161
1313
|
const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
|
|
1162
1314
|
|
|
1163
1315
|
return {
|
|
1164
1316
|
history,
|
|
1317
|
+
agentSession,
|
|
1165
1318
|
agentMeta,
|
|
1166
1319
|
command,
|
|
1167
1320
|
contextMode,
|
|
@@ -1171,8 +1324,8 @@ async function prepareWebAgentExecution(ctx, state, containerName, prompt) {
|
|
|
1171
1324
|
};
|
|
1172
1325
|
}
|
|
1173
1326
|
|
|
1174
|
-
function finalizeWebAgentExecution(state,
|
|
1175
|
-
appendWebSessionMessage(state.webHistoryDir,
|
|
1327
|
+
function finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, meta, result) {
|
|
1328
|
+
appendWebSessionMessage(state.webHistoryDir, sessionRef, 'assistant', result.output, {
|
|
1176
1329
|
exitCode: result.exitCode,
|
|
1177
1330
|
mode: 'agent',
|
|
1178
1331
|
contextMode: meta.contextMode,
|
|
@@ -1180,21 +1333,19 @@ function finalizeWebAgentExecution(state, containerName, history, agentMeta, met
|
|
|
1180
1333
|
resumeSucceeded: meta.resumeSucceeded,
|
|
1181
1334
|
interrupted: result.interrupted === true
|
|
1182
1335
|
});
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
lastResumeOk: meta.resumeAttempted ? meta.resumeSucceeded : history.lastResumeOk,
|
|
1188
|
-
lastResumeError: meta.resumeAttempted ? (meta.resumeSucceeded ? '' : meta.resumeError) : history.lastResumeError || ''
|
|
1336
|
+
patchWebAgentSessionState(state.webHistoryDir, sessionRef, {
|
|
1337
|
+
lastResumeAt: meta.resumeAttempted ? new Date().toISOString() : (agentSession.lastResumeAt || null),
|
|
1338
|
+
lastResumeOk: meta.resumeAttempted ? meta.resumeSucceeded : agentSession.lastResumeOk,
|
|
1339
|
+
lastResumeError: meta.resumeAttempted ? (meta.resumeSucceeded ? '' : meta.resumeError) : (agentSession.lastResumeError || '')
|
|
1189
1340
|
});
|
|
1190
1341
|
}
|
|
1191
1342
|
|
|
1192
|
-
function appendWebAgentTraceMessage(webHistoryDir,
|
|
1343
|
+
function appendWebAgentTraceMessage(webHistoryDir, sessionRefOrContainerName, content, extra = {}) {
|
|
1193
1344
|
const text = String(content || '').trim();
|
|
1194
1345
|
if (!text) {
|
|
1195
1346
|
return;
|
|
1196
1347
|
}
|
|
1197
|
-
appendWebSessionMessage(webHistoryDir,
|
|
1348
|
+
appendWebSessionMessage(webHistoryDir, sessionRefOrContainerName, 'assistant', text, {
|
|
1198
1349
|
mode: 'agent',
|
|
1199
1350
|
streamTrace: true,
|
|
1200
1351
|
...extra
|
|
@@ -1982,7 +2133,7 @@ function listWebManyoyoContainers(ctx) {
|
|
|
1982
2133
|
return map;
|
|
1983
2134
|
}
|
|
1984
2135
|
|
|
1985
|
-
async function ensureWebContainer(ctx, state, containerInput) {
|
|
2136
|
+
async function ensureWebContainer(ctx, state, containerInput, messageSessionRef = null) {
|
|
1986
2137
|
const runtime = typeof containerInput === 'string'
|
|
1987
2138
|
? buildStaticContainerRuntime(ctx, containerInput)
|
|
1988
2139
|
: containerInput;
|
|
@@ -2014,14 +2165,24 @@ async function ensureWebContainer(ctx, state, containerInput) {
|
|
|
2014
2165
|
}
|
|
2015
2166
|
|
|
2016
2167
|
await ctx.waitForContainerReady(runtime.containerName);
|
|
2017
|
-
appendWebSessionMessage(
|
|
2168
|
+
appendWebSessionMessage(
|
|
2169
|
+
state.webHistoryDir,
|
|
2170
|
+
messageSessionRef || runtime.containerName,
|
|
2171
|
+
'system',
|
|
2172
|
+
`容器 ${runtime.containerName} 已创建并启动。`
|
|
2173
|
+
);
|
|
2018
2174
|
return;
|
|
2019
2175
|
}
|
|
2020
2176
|
|
|
2021
2177
|
const status = ctx.getContainerStatus(runtime.containerName);
|
|
2022
2178
|
if (status !== 'running') {
|
|
2023
2179
|
ctx.dockerExecArgs(['start', runtime.containerName], { stdio: 'pipe' });
|
|
2024
|
-
appendWebSessionMessage(
|
|
2180
|
+
appendWebSessionMessage(
|
|
2181
|
+
state.webHistoryDir,
|
|
2182
|
+
messageSessionRef || runtime.containerName,
|
|
2183
|
+
'system',
|
|
2184
|
+
`容器 ${runtime.containerName} 已启动。`
|
|
2185
|
+
);
|
|
2025
2186
|
}
|
|
2026
2187
|
}
|
|
2027
2188
|
|
|
@@ -2085,24 +2246,29 @@ async function execCommandInWebContainer(ctx, containerName, command, options =
|
|
|
2085
2246
|
});
|
|
2086
2247
|
}
|
|
2087
2248
|
|
|
2088
|
-
async function execAgentInWebContainerStream(ctx, state,
|
|
2249
|
+
async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerName, command, options = {}) {
|
|
2089
2250
|
const opts = options && typeof options === 'object' ? options : {};
|
|
2251
|
+
const sessionRef = typeof sessionRefOrContainerName === 'string'
|
|
2252
|
+
? { containerName: sessionRefOrContainerName, agentId: WEB_DEFAULT_AGENT_ID }
|
|
2253
|
+
: sessionRefOrContainerName;
|
|
2254
|
+
const sessionKey = buildWebSessionKey(sessionRef.containerName, sessionRef.agentId);
|
|
2090
2255
|
const agentProgram = typeof opts.agentProgram === 'string' ? opts.agentProgram : '';
|
|
2091
2256
|
const onEvent = typeof opts.onEvent === 'function' ? opts.onEvent : () => {};
|
|
2092
2257
|
const process = spawn(
|
|
2093
2258
|
ctx.dockerCmd,
|
|
2094
|
-
['exec', containerName, '/bin/bash', '-lc', command],
|
|
2259
|
+
['exec', sessionRef.containerName, '/bin/bash', '-lc', command],
|
|
2095
2260
|
{ stdio: ['ignore', 'pipe', 'pipe'] }
|
|
2096
2261
|
);
|
|
2097
2262
|
|
|
2098
2263
|
const runState = {
|
|
2099
|
-
containerName,
|
|
2264
|
+
containerName: sessionRef.containerName,
|
|
2265
|
+
sessionKey,
|
|
2100
2266
|
process,
|
|
2101
2267
|
command,
|
|
2102
2268
|
startedAt: new Date().toISOString(),
|
|
2103
2269
|
stopping: false
|
|
2104
2270
|
};
|
|
2105
|
-
state.agentRuns.set(containerName, runState);
|
|
2271
|
+
state.agentRuns.set(sessionRef.containerName, runState);
|
|
2106
2272
|
|
|
2107
2273
|
return await new Promise((resolve, reject) => {
|
|
2108
2274
|
const MAX_RAW_OUTPUT_CHARS = 32 * 1024 * 1024;
|
|
@@ -2201,11 +2367,11 @@ async function execAgentInWebContainerStream(ctx, state, containerName, command,
|
|
|
2201
2367
|
});
|
|
2202
2368
|
|
|
2203
2369
|
process.on('error', error => {
|
|
2204
|
-
state.agentRuns.delete(containerName);
|
|
2370
|
+
state.agentRuns.delete(sessionRef.containerName);
|
|
2205
2371
|
reject(error);
|
|
2206
2372
|
});
|
|
2207
2373
|
process.on('close', code => {
|
|
2208
|
-
state.agentRuns.delete(containerName);
|
|
2374
|
+
state.agentRuns.delete(sessionRef.containerName);
|
|
2209
2375
|
if (stdoutPending) {
|
|
2210
2376
|
emitStdoutTraceLine(stdoutPending);
|
|
2211
2377
|
stdoutPending = '';
|
|
@@ -2310,30 +2476,51 @@ function decodeSessionName(encoded) {
|
|
|
2310
2476
|
}
|
|
2311
2477
|
}
|
|
2312
2478
|
|
|
2313
|
-
function
|
|
2314
|
-
const
|
|
2315
|
-
if (!ctx.isValidContainerName(containerName)) {
|
|
2316
|
-
sendJson(res, 400, { error: `containerName 非法: ${containerName}` });
|
|
2479
|
+
function getValidSessionRef(ctx, res, encodedName) {
|
|
2480
|
+
const parsed = parseWebSessionKey(decodeSessionName(encodedName));
|
|
2481
|
+
if (!ctx.isValidContainerName(parsed.containerName)) {
|
|
2482
|
+
sendJson(res, 400, { error: `containerName 非法: ${parsed.containerName}` });
|
|
2483
|
+
return null;
|
|
2484
|
+
}
|
|
2485
|
+
if (!SAFE_CONTAINER_NAME_PATTERN.test(parsed.agentId)) {
|
|
2486
|
+
sendJson(res, 400, { error: `agentId 非法: ${parsed.agentId}` });
|
|
2317
2487
|
return null;
|
|
2318
2488
|
}
|
|
2319
|
-
return
|
|
2489
|
+
return parsed;
|
|
2320
2490
|
}
|
|
2321
2491
|
|
|
2322
|
-
function buildSessionSummary(ctx, state, containerMap,
|
|
2323
|
-
const
|
|
2492
|
+
function buildSessionSummary(ctx, state, containerMap, sessionRef) {
|
|
2493
|
+
const containerName = sessionRef && sessionRef.containerName ? sessionRef.containerName : '';
|
|
2494
|
+
const agentId = sessionRef && sessionRef.agentId ? sessionRef.agentId : WEB_DEFAULT_AGENT_ID;
|
|
2495
|
+
const history = loadWebSessionHistory(state.webHistoryDir, containerName);
|
|
2324
2496
|
const agentMeta = getAgentRuntimeMeta(history);
|
|
2325
|
-
const
|
|
2326
|
-
|
|
2327
|
-
|
|
2497
|
+
const agentSession = getWebAgentSession(history, agentId)
|
|
2498
|
+
|| (agentId === WEB_DEFAULT_AGENT_ID ? createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID) : null);
|
|
2499
|
+
if (!agentSession) {
|
|
2500
|
+
return null;
|
|
2501
|
+
}
|
|
2502
|
+
const latestMessage = agentSession.messages.length ? agentSession.messages[agentSession.messages.length - 1] : null;
|
|
2503
|
+
const containerInfo = containerMap[containerName] || {};
|
|
2504
|
+
const applied = history.applied && typeof history.applied === 'object' && !Array.isArray(history.applied)
|
|
2505
|
+
? history.applied
|
|
2506
|
+
: buildSessionFallbackApplied(ctx, state, containerName, history, {
|
|
2507
|
+
status: containerInfo.status || 'history'
|
|
2508
|
+
});
|
|
2509
|
+
const updatedAt = agentSession.updatedAt || history.updatedAt || (latestMessage && latestMessage.timestamp) || containerInfo.createdAt || null;
|
|
2328
2510
|
return {
|
|
2329
|
-
name,
|
|
2511
|
+
name: buildWebSessionKey(containerName, agentId),
|
|
2512
|
+
containerName,
|
|
2513
|
+
agentId,
|
|
2514
|
+
agentName: agentSession.agentName,
|
|
2330
2515
|
status: containerInfo.status || 'history',
|
|
2331
2516
|
image: containerInfo.image || '',
|
|
2332
2517
|
updatedAt,
|
|
2333
|
-
messageCount:
|
|
2518
|
+
messageCount: agentSession.messages.length,
|
|
2334
2519
|
agentEnabled: isAgentPromptCommandEnabled(history.agentPromptCommand),
|
|
2335
2520
|
agentProgram: agentMeta.agentProgram,
|
|
2336
|
-
resumeSupported: agentMeta.resumeSupported
|
|
2521
|
+
resumeSupported: agentMeta.resumeSupported,
|
|
2522
|
+
hostPath: applied.hostPath || '',
|
|
2523
|
+
containerPath: applied.containerPath || ''
|
|
2337
2524
|
};
|
|
2338
2525
|
}
|
|
2339
2526
|
|
|
@@ -2341,9 +2528,8 @@ function buildSessionFallbackApplied(ctx, state, name, history, summary) {
|
|
|
2341
2528
|
const snapshot = readWebConfigSnapshot(state.webConfigPath);
|
|
2342
2529
|
const defaults = buildConfigDefaults(ctx, snapshot.parseError ? {} : snapshot.parsed);
|
|
2343
2530
|
const effectiveAgentPromptCommand = history.agentPromptCommand || defaults.agentPromptCommand || '';
|
|
2344
|
-
const effectiveAgentProgram =
|
|
2345
|
-
const effectiveResumeSupported =
|
|
2346
|
-
|| Boolean(buildAgentResumeCommand(effectiveAgentProgram));
|
|
2531
|
+
const effectiveAgentProgram = resolveAgentProgram(effectiveAgentPromptCommand) || '';
|
|
2532
|
+
const effectiveResumeSupported = Boolean(buildAgentResumeCommand(effectiveAgentProgram));
|
|
2347
2533
|
const defaultCommand = buildDefaultCommand(
|
|
2348
2534
|
defaults.shellPrefix,
|
|
2349
2535
|
defaults.shell,
|
|
@@ -2373,24 +2559,34 @@ function buildSessionFallbackApplied(ctx, state, name, history, summary) {
|
|
|
2373
2559
|
}
|
|
2374
2560
|
|
|
2375
2561
|
function buildSessionDetail(ctx, state, containerMap, name) {
|
|
2376
|
-
const
|
|
2562
|
+
const sessionRef = typeof name === 'string' ? parseWebSessionKey(name) : name;
|
|
2563
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
2377
2564
|
const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
|
|
2378
|
-
const summary = buildSessionSummary(ctx, state, containerMap,
|
|
2379
|
-
const
|
|
2565
|
+
const summary = buildSessionSummary(ctx, state, containerMap, sessionRef);
|
|
2566
|
+
const agentSession = getWebAgentSession(history, sessionRef.agentId)
|
|
2567
|
+
|| (sessionRef.agentId === WEB_DEFAULT_AGENT_ID ? createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID) : null);
|
|
2568
|
+
const latestMessage = agentSession && agentSession.messages.length
|
|
2569
|
+
? agentSession.messages[agentSession.messages.length - 1]
|
|
2570
|
+
: null;
|
|
2380
2571
|
const applied = history.applied && typeof history.applied === 'object' && !Array.isArray(history.applied)
|
|
2381
2572
|
? history.applied
|
|
2382
|
-
: buildSessionFallbackApplied(ctx, state,
|
|
2573
|
+
: buildSessionFallbackApplied(ctx, state, sessionRef.containerName, history, summary || {});
|
|
2574
|
+
|
|
2575
|
+
if (!summary || !agentSession) {
|
|
2576
|
+
return null;
|
|
2577
|
+
}
|
|
2383
2578
|
|
|
2384
2579
|
return {
|
|
2385
2580
|
...summary,
|
|
2581
|
+
agentName: agentSession.agentName,
|
|
2386
2582
|
latestRole: latestMessage && latestMessage.role ? String(latestMessage.role) : '',
|
|
2387
2583
|
latestTimestamp: latestMessage && latestMessage.timestamp ? latestMessage.timestamp : summary.updatedAt,
|
|
2388
2584
|
agentPromptCommand: normalizedTemplate || '',
|
|
2389
|
-
agentProgram:
|
|
2390
|
-
resumeSupported:
|
|
2391
|
-
lastResumeAt:
|
|
2392
|
-
lastResumeOk: typeof
|
|
2393
|
-
lastResumeError:
|
|
2585
|
+
agentProgram: summary.agentProgram || '',
|
|
2586
|
+
resumeSupported: summary.resumeSupported === true,
|
|
2587
|
+
lastResumeAt: agentSession.lastResumeAt || null,
|
|
2588
|
+
lastResumeOk: typeof agentSession.lastResumeOk === 'boolean' ? agentSession.lastResumeOk : null,
|
|
2589
|
+
lastResumeError: agentSession.lastResumeError || '',
|
|
2394
2590
|
applied
|
|
2395
2591
|
};
|
|
2396
2592
|
}
|
|
@@ -2739,6 +2935,52 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2739
2935
|
}
|
|
2740
2936
|
}
|
|
2741
2937
|
const routes = [
|
|
2938
|
+
{
|
|
2939
|
+
method: 'GET',
|
|
2940
|
+
match: currentPath => currentPath === '/api/fs/directories' ? [] : null,
|
|
2941
|
+
handler: async () => {
|
|
2942
|
+
const requestUrl = new URL(req.url || '/api/fs/directories', 'http://localhost');
|
|
2943
|
+
const requestedPath = expandHomeAliasPath(String(requestUrl.searchParams.get('path') || '').trim() || os.homedir());
|
|
2944
|
+
const requestedBasePath = expandHomeAliasPath(String(requestUrl.searchParams.get('basePath') || '').trim());
|
|
2945
|
+
const realPath = fs.realpathSync(requestedPath);
|
|
2946
|
+
if (!fs.statSync(realPath).isDirectory()) {
|
|
2947
|
+
sendJson(res, 400, { error: `目录不存在: ${realPath}` });
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
let realBasePath = '';
|
|
2952
|
+
if (requestedBasePath) {
|
|
2953
|
+
realBasePath = fs.realpathSync(requestedBasePath);
|
|
2954
|
+
if (!fs.statSync(realBasePath).isDirectory()) {
|
|
2955
|
+
sendJson(res, 400, { error: `basePath 不是目录: ${realBasePath}` });
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
const relativeToBase = path.relative(realBasePath, realPath);
|
|
2959
|
+
if (relativeToBase.startsWith('..') || path.isAbsolute(relativeToBase)) {
|
|
2960
|
+
sendJson(res, 400, { error: '目录超出 basePath 范围' });
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
const parentPath = realBasePath
|
|
2966
|
+
? (realPath === realBasePath ? '' : path.dirname(realPath))
|
|
2967
|
+
: (realPath === path.parse(realPath).root ? '' : path.dirname(realPath));
|
|
2968
|
+
const entries = fs.readdirSync(realPath, { withFileTypes: true })
|
|
2969
|
+
.filter(entry => entry && entry.isDirectory())
|
|
2970
|
+
.map(entry => ({
|
|
2971
|
+
name: entry.name,
|
|
2972
|
+
path: path.join(realPath, entry.name)
|
|
2973
|
+
}))
|
|
2974
|
+
.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
|
|
2975
|
+
|
|
2976
|
+
sendJson(res, 200, {
|
|
2977
|
+
currentPath: realPath,
|
|
2978
|
+
basePath: realBasePath || '',
|
|
2979
|
+
parentPath,
|
|
2980
|
+
entries
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
},
|
|
2742
2984
|
{
|
|
2743
2985
|
method: 'GET',
|
|
2744
2986
|
match: currentPath => currentPath === '/api/config' ? [] : null,
|
|
@@ -2788,7 +3030,15 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2788
3030
|
]);
|
|
2789
3031
|
|
|
2790
3032
|
const sessions = Array.from(names)
|
|
2791
|
-
.
|
|
3033
|
+
.flatMap(name => {
|
|
3034
|
+
const history = loadWebSessionHistory(state.webHistoryDir, name);
|
|
3035
|
+
return listWebAgentSessions(history, { includeSyntheticDefault: true })
|
|
3036
|
+
.map(agentSession => buildSessionSummary(ctx, state, containerMap, {
|
|
3037
|
+
containerName: name,
|
|
3038
|
+
agentId: agentSession.agentId
|
|
3039
|
+
}))
|
|
3040
|
+
.filter(Boolean);
|
|
3041
|
+
})
|
|
2792
3042
|
.sort((a, b) => {
|
|
2793
3043
|
const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
|
2794
3044
|
const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
|
@@ -2813,44 +3063,70 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2813
3063
|
|
|
2814
3064
|
await ensureWebContainer(ctx, state, runtime);
|
|
2815
3065
|
setWebSessionAgentPromptCommand(state.webHistoryDir, runtime.containerName, runtime.agentPromptCommand);
|
|
2816
|
-
|
|
3066
|
+
patchWebSessionHistory(state.webHistoryDir, runtime.containerName, {
|
|
2817
3067
|
applied: runtime.applied
|
|
2818
3068
|
});
|
|
2819
3069
|
sendJson(res, 200, { name: runtime.containerName, applied: runtime.applied });
|
|
2820
3070
|
}
|
|
2821
3071
|
},
|
|
3072
|
+
{
|
|
3073
|
+
method: 'POST',
|
|
3074
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agents$/),
|
|
3075
|
+
handler: async match => {
|
|
3076
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3077
|
+
if (!sessionRef) {
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
3081
|
+
const agentSession = createWebAgentSession(history);
|
|
3082
|
+
saveWebSessionHistory(state.webHistoryDir, sessionRef.containerName, history);
|
|
3083
|
+
sendJson(res, 200, {
|
|
3084
|
+
name: buildWebSessionKey(sessionRef.containerName, agentSession.agentId),
|
|
3085
|
+
containerName: sessionRef.containerName,
|
|
3086
|
+
agentId: agentSession.agentId,
|
|
3087
|
+
agentName: agentSession.agentName
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
},
|
|
2822
3091
|
{
|
|
2823
3092
|
method: 'GET',
|
|
2824
3093
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/messages$/),
|
|
2825
3094
|
handler: async match => {
|
|
2826
|
-
const
|
|
2827
|
-
if (!
|
|
3095
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3096
|
+
if (!sessionRef) {
|
|
2828
3097
|
return;
|
|
2829
3098
|
}
|
|
2830
|
-
const history = loadWebSessionHistory(state.webHistoryDir, containerName);
|
|
2831
|
-
|
|
3099
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
3100
|
+
const agentSession = getWebAgentSession(history, sessionRef.agentId)
|
|
3101
|
+
|| createEmptyWebAgentSession(sessionRef.agentId);
|
|
3102
|
+
sendJson(res, 200, {
|
|
3103
|
+
name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
|
|
3104
|
+
containerName: sessionRef.containerName,
|
|
3105
|
+
agentId: sessionRef.agentId,
|
|
3106
|
+
messages: agentSession.messages
|
|
3107
|
+
});
|
|
2832
3108
|
}
|
|
2833
3109
|
},
|
|
2834
3110
|
{
|
|
2835
3111
|
method: 'GET',
|
|
2836
3112
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/detail$/),
|
|
2837
3113
|
handler: async match => {
|
|
2838
|
-
const
|
|
2839
|
-
if (!
|
|
3114
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3115
|
+
if (!sessionRef) {
|
|
2840
3116
|
return;
|
|
2841
3117
|
}
|
|
2842
3118
|
|
|
2843
3119
|
const containerMap = listWebManyoyoContainers(ctx);
|
|
2844
|
-
const detail = buildSessionDetail(ctx, state, containerMap,
|
|
2845
|
-
sendJson(res, 200, { name: containerName, detail });
|
|
3120
|
+
const detail = buildSessionDetail(ctx, state, containerMap, sessionRef);
|
|
3121
|
+
sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), detail });
|
|
2846
3122
|
}
|
|
2847
3123
|
},
|
|
2848
3124
|
{
|
|
2849
3125
|
method: 'POST',
|
|
2850
3126
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/run$/),
|
|
2851
3127
|
handler: async match => {
|
|
2852
|
-
const
|
|
2853
|
-
if (!
|
|
3128
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3129
|
+
if (!sessionRef) {
|
|
2854
3130
|
return;
|
|
2855
3131
|
}
|
|
2856
3132
|
|
|
@@ -2861,12 +3137,12 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2861
3137
|
return;
|
|
2862
3138
|
}
|
|
2863
3139
|
|
|
2864
|
-
await ensureWebContainer(ctx, state, containerName);
|
|
2865
|
-
appendWebSessionMessage(state.webHistoryDir,
|
|
2866
|
-
const result = await execCommandInWebContainer(ctx, containerName, command);
|
|
3140
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
3141
|
+
appendWebSessionMessage(state.webHistoryDir, sessionRef, 'user', command);
|
|
3142
|
+
const result = await execCommandInWebContainer(ctx, sessionRef.containerName, command);
|
|
2867
3143
|
appendWebSessionMessage(
|
|
2868
3144
|
state.webHistoryDir,
|
|
2869
|
-
|
|
3145
|
+
sessionRef,
|
|
2870
3146
|
'assistant',
|
|
2871
3147
|
result.output,
|
|
2872
3148
|
{ exitCode: result.exitCode }
|
|
@@ -2878,8 +3154,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2878
3154
|
method: 'POST',
|
|
2879
3155
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent$/),
|
|
2880
3156
|
handler: async match => {
|
|
2881
|
-
const
|
|
2882
|
-
if (!
|
|
3157
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3158
|
+
if (!sessionRef) {
|
|
2883
3159
|
return;
|
|
2884
3160
|
}
|
|
2885
3161
|
|
|
@@ -2892,21 +3168,21 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2892
3168
|
|
|
2893
3169
|
let prepared = null;
|
|
2894
3170
|
try {
|
|
2895
|
-
prepared = await prepareWebAgentExecution(ctx, state,
|
|
3171
|
+
prepared = await prepareWebAgentExecution(ctx, state, sessionRef, prompt);
|
|
2896
3172
|
} catch (e) {
|
|
2897
3173
|
sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
|
|
2898
3174
|
return;
|
|
2899
3175
|
}
|
|
2900
3176
|
|
|
2901
|
-
const {
|
|
2902
|
-
appendWebSessionMessage(state.webHistoryDir,
|
|
3177
|
+
const { agentSession, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
|
|
3178
|
+
appendWebSessionMessage(state.webHistoryDir, sessionRef, 'user', prompt, {
|
|
2903
3179
|
mode: 'agent',
|
|
2904
3180
|
contextMode
|
|
2905
3181
|
});
|
|
2906
|
-
const result = await execCommandInWebContainer(ctx, containerName, command, {
|
|
3182
|
+
const result = await execCommandInWebContainer(ctx, sessionRef.containerName, command, {
|
|
2907
3183
|
agentProgram: agentMeta.agentProgram
|
|
2908
3184
|
});
|
|
2909
|
-
finalizeWebAgentExecution(state,
|
|
3185
|
+
finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, {
|
|
2910
3186
|
contextMode,
|
|
2911
3187
|
resumeAttempted,
|
|
2912
3188
|
resumeSucceeded,
|
|
@@ -2926,8 +3202,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2926
3202
|
method: 'POST',
|
|
2927
3203
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent\/stream$/),
|
|
2928
3204
|
handler: async match => {
|
|
2929
|
-
const
|
|
2930
|
-
if (!
|
|
3205
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3206
|
+
if (!sessionRef) {
|
|
2931
3207
|
return;
|
|
2932
3208
|
}
|
|
2933
3209
|
|
|
@@ -2937,23 +3213,23 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2937
3213
|
sendJson(res, 400, { error: 'prompt 不能为空' });
|
|
2938
3214
|
return;
|
|
2939
3215
|
}
|
|
2940
|
-
if (state.agentRuns.has(containerName)) {
|
|
3216
|
+
if (state.agentRuns.has(sessionRef.containerName)) {
|
|
2941
3217
|
sendJson(res, 409, { error: '当前会话已有运行中的 agent 任务' });
|
|
2942
3218
|
return;
|
|
2943
3219
|
}
|
|
2944
3220
|
|
|
2945
3221
|
let prepared = null;
|
|
2946
3222
|
try {
|
|
2947
|
-
prepared = await prepareWebAgentExecution(ctx, state,
|
|
3223
|
+
prepared = await prepareWebAgentExecution(ctx, state, sessionRef, prompt);
|
|
2948
3224
|
} catch (e) {
|
|
2949
3225
|
sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
|
|
2950
3226
|
return;
|
|
2951
3227
|
}
|
|
2952
3228
|
|
|
2953
|
-
const {
|
|
3229
|
+
const { agentSession, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
|
|
2954
3230
|
const traceLines = ['[执行过程]'];
|
|
2955
3231
|
const traceEvents = [];
|
|
2956
|
-
appendWebSessionMessage(state.webHistoryDir,
|
|
3232
|
+
appendWebSessionMessage(state.webHistoryDir, sessionRef, 'user', prompt, {
|
|
2957
3233
|
mode: 'agent',
|
|
2958
3234
|
contextMode
|
|
2959
3235
|
});
|
|
@@ -2965,7 +3241,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2965
3241
|
});
|
|
2966
3242
|
sendNdjson(res, {
|
|
2967
3243
|
type: 'meta',
|
|
2968
|
-
containerName,
|
|
3244
|
+
containerName: sessionRef.containerName,
|
|
3245
|
+
sessionName: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
|
|
2969
3246
|
contextMode,
|
|
2970
3247
|
resumeAttempted,
|
|
2971
3248
|
resumeSucceeded,
|
|
@@ -2979,7 +3256,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2979
3256
|
}
|
|
2980
3257
|
|
|
2981
3258
|
try {
|
|
2982
|
-
const result = await execAgentInWebContainerStream(ctx, state,
|
|
3259
|
+
const result = await execAgentInWebContainerStream(ctx, state, sessionRef, command, {
|
|
2983
3260
|
agentProgram: agentMeta.agentProgram,
|
|
2984
3261
|
onEvent: event => {
|
|
2985
3262
|
if (event && event.type === 'trace' && event.text) {
|
|
@@ -2992,14 +3269,14 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2992
3269
|
}
|
|
2993
3270
|
});
|
|
2994
3271
|
traceLines.push(result.interrupted === true ? '[任务] 已停止' : '[任务] 已完成');
|
|
2995
|
-
appendWebAgentTraceMessage(state.webHistoryDir,
|
|
3272
|
+
appendWebAgentTraceMessage(state.webHistoryDir, sessionRef, traceLines.join('\n'), {
|
|
2996
3273
|
traceEvents,
|
|
2997
3274
|
contextMode,
|
|
2998
3275
|
resumeAttempted,
|
|
2999
3276
|
resumeSucceeded,
|
|
3000
3277
|
interrupted: result.interrupted === true
|
|
3001
3278
|
});
|
|
3002
|
-
finalizeWebAgentExecution(state,
|
|
3279
|
+
finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, {
|
|
3003
3280
|
contextMode,
|
|
3004
3281
|
resumeAttempted,
|
|
3005
3282
|
resumeSucceeded,
|
|
@@ -3016,7 +3293,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3016
3293
|
});
|
|
3017
3294
|
} catch (e) {
|
|
3018
3295
|
traceLines.push(`[错误] ${e && e.message ? e.message : 'Agent 执行失败'}`);
|
|
3019
|
-
appendWebAgentTraceMessage(state.webHistoryDir,
|
|
3296
|
+
appendWebAgentTraceMessage(state.webHistoryDir, sessionRef, traceLines.join('\n'), {
|
|
3020
3297
|
traceEvents,
|
|
3021
3298
|
contextMode,
|
|
3022
3299
|
resumeAttempted,
|
|
@@ -3036,11 +3313,11 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3036
3313
|
method: 'POST',
|
|
3037
3314
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent\/stop$/),
|
|
3038
3315
|
handler: async match => {
|
|
3039
|
-
const
|
|
3040
|
-
if (!
|
|
3316
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3317
|
+
if (!sessionRef) {
|
|
3041
3318
|
return;
|
|
3042
3319
|
}
|
|
3043
|
-
const stopped = stopWebAgentRun(state, containerName);
|
|
3320
|
+
const stopped = stopWebAgentRun(state, sessionRef.containerName);
|
|
3044
3321
|
if (!stopped) {
|
|
3045
3322
|
sendJson(res, 404, { error: '当前会话没有运行中的 agent 任务' });
|
|
3046
3323
|
return;
|
|
@@ -3052,30 +3329,45 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3052
3329
|
method: 'POST',
|
|
3053
3330
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/remove$/),
|
|
3054
3331
|
handler: async match => {
|
|
3055
|
-
const
|
|
3056
|
-
if (!
|
|
3332
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3333
|
+
if (!sessionRef) {
|
|
3057
3334
|
return;
|
|
3058
3335
|
}
|
|
3059
3336
|
|
|
3060
|
-
if (ctx.containerExists(containerName)) {
|
|
3061
|
-
ctx.removeContainer(containerName);
|
|
3062
|
-
appendWebSessionMessage(state.webHistoryDir,
|
|
3337
|
+
if (ctx.containerExists(sessionRef.containerName)) {
|
|
3338
|
+
ctx.removeContainer(sessionRef.containerName);
|
|
3339
|
+
appendWebSessionMessage(state.webHistoryDir, sessionRef, 'system', `容器 ${sessionRef.containerName} 已删除。`);
|
|
3063
3340
|
}
|
|
3064
3341
|
|
|
3065
|
-
sendJson(res, 200, { removed: true, name: containerName });
|
|
3342
|
+
sendJson(res, 200, { removed: true, name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId) });
|
|
3066
3343
|
}
|
|
3067
3344
|
},
|
|
3068
3345
|
{
|
|
3069
3346
|
method: 'POST',
|
|
3070
3347
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/remove-with-history$/),
|
|
3071
3348
|
handler: async match => {
|
|
3072
|
-
const
|
|
3073
|
-
if (!
|
|
3349
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3350
|
+
if (!sessionRef) {
|
|
3074
3351
|
return;
|
|
3075
3352
|
}
|
|
3076
3353
|
|
|
3077
|
-
|
|
3078
|
-
|
|
3354
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
3355
|
+
if (history.agents && typeof history.agents === 'object') {
|
|
3356
|
+
if (sessionRef.agentId === WEB_DEFAULT_AGENT_ID) {
|
|
3357
|
+
delete history.agents[WEB_DEFAULT_AGENT_ID];
|
|
3358
|
+
} else {
|
|
3359
|
+
delete history.agents[sessionRef.agentId];
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
if (!Object.keys(history.agents || {}).length && !ctx.containerExists(sessionRef.containerName)) {
|
|
3363
|
+
removeWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
3364
|
+
} else {
|
|
3365
|
+
saveWebSessionHistory(state.webHistoryDir, sessionRef.containerName, history);
|
|
3366
|
+
}
|
|
3367
|
+
sendJson(res, 200, {
|
|
3368
|
+
removedHistory: true,
|
|
3369
|
+
name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId)
|
|
3370
|
+
});
|
|
3079
3371
|
}
|
|
3080
3372
|
}
|
|
3081
3373
|
];
|
|
@@ -3293,9 +3585,13 @@ async function startWebServer(options) {
|
|
|
3293
3585
|
return;
|
|
3294
3586
|
}
|
|
3295
3587
|
|
|
3296
|
-
const
|
|
3297
|
-
if (!ctx.isValidContainerName(containerName)) {
|
|
3298
|
-
sendWebSocketUpgradeError(socket, 400, `containerName 非法: ${containerName}`);
|
|
3588
|
+
const sessionRef = parseWebSessionKey(decodeSessionName(terminalMatch[1]));
|
|
3589
|
+
if (!ctx.isValidContainerName(sessionRef.containerName)) {
|
|
3590
|
+
sendWebSocketUpgradeError(socket, 400, `containerName 非法: ${sessionRef.containerName}`);
|
|
3591
|
+
return;
|
|
3592
|
+
}
|
|
3593
|
+
if (!SAFE_CONTAINER_NAME_PATTERN.test(sessionRef.agentId)) {
|
|
3594
|
+
sendWebSocketUpgradeError(socket, 400, `agentId 非法: ${sessionRef.agentId}`);
|
|
3299
3595
|
return;
|
|
3300
3596
|
}
|
|
3301
3597
|
|
|
@@ -3309,11 +3605,11 @@ async function startWebServer(options) {
|
|
|
3309
3605
|
url.searchParams.get('rows')
|
|
3310
3606
|
);
|
|
3311
3607
|
|
|
3312
|
-
ensureWebContainer(ctx, state, containerName)
|
|
3608
|
+
ensureWebContainer(ctx, state, sessionRef.containerName)
|
|
3313
3609
|
.then(() => {
|
|
3314
3610
|
wsServer.handleUpgrade(req, socket, head, ws => {
|
|
3315
3611
|
wsServer.emit('connection', ws, req, {
|
|
3316
|
-
containerName,
|
|
3612
|
+
containerName: sessionRef.containerName,
|
|
3317
3613
|
cols,
|
|
3318
3614
|
rows
|
|
3319
3615
|
});
|