@xcanwin/manyoyo 5.7.2 → 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 +171 -2
- package/lib/web/frontend/app.html +34 -5
- package/lib/web/frontend/app.js +512 -167
- package/lib/web/server.js +577 -192
- package/package.json +1 -1
package/lib/web/server.js
CHANGED
|
@@ -29,9 +29,14 @@ 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_.-]*)$/;
|
|
38
|
+
const SENSITIVE_CONFIG_KEY_PATTERN = /(pass(word)?|passwd|secret|token|api(?:_|-)?key|auth(?:_|-)?token|oauth(?:_|-)?token)$/i;
|
|
39
|
+
const REDACTED_CONFIG_VALUE = '***';
|
|
35
40
|
|
|
36
41
|
const YOLO_COMMAND_MAP = {
|
|
37
42
|
claude: 'IS_SANDBOX=1 claude --dangerously-skip-permissions',
|
|
@@ -113,62 +118,113 @@ function getWebHistoryFile(webHistoryDir, containerName) {
|
|
|
113
118
|
return path.join(webHistoryDir, `${containerName}.json`);
|
|
114
119
|
}
|
|
115
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
|
+
|
|
116
198
|
function loadWebSessionHistory(webHistoryDir, containerName) {
|
|
117
199
|
ensureWebHistoryDir(webHistoryDir);
|
|
118
200
|
const filePath = getWebHistoryFile(webHistoryDir, containerName);
|
|
119
201
|
if (!fs.existsSync(filePath)) {
|
|
120
|
-
return {
|
|
121
|
-
containerName,
|
|
122
|
-
updatedAt: null,
|
|
123
|
-
messages: [],
|
|
124
|
-
agentPromptCommand: '',
|
|
125
|
-
agentProgram: '',
|
|
126
|
-
resumeSupported: false,
|
|
127
|
-
lastResumeAt: null,
|
|
128
|
-
lastResumeOk: null,
|
|
129
|
-
lastResumeError: '',
|
|
130
|
-
applied: null
|
|
131
|
-
};
|
|
202
|
+
return normalizeWebHistoryRecord(containerName, {});
|
|
132
203
|
}
|
|
133
204
|
|
|
134
205
|
try {
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
containerName,
|
|
138
|
-
updatedAt: data.updatedAt || null,
|
|
139
|
-
messages: Array.isArray(data.messages) ? data.messages : [],
|
|
140
|
-
agentPromptCommand: typeof data.agentPromptCommand === 'string'
|
|
141
|
-
? data.agentPromptCommand
|
|
142
|
-
: '',
|
|
143
|
-
agentProgram: typeof data.agentProgram === 'string' ? data.agentProgram : '',
|
|
144
|
-
resumeSupported: data.resumeSupported === true,
|
|
145
|
-
lastResumeAt: typeof data.lastResumeAt === 'string' ? data.lastResumeAt : null,
|
|
146
|
-
lastResumeOk: typeof data.lastResumeOk === 'boolean' ? data.lastResumeOk : null,
|
|
147
|
-
lastResumeError: typeof data.lastResumeError === 'string' ? data.lastResumeError : '',
|
|
148
|
-
applied: data.applied && typeof data.applied === 'object' && !Array.isArray(data.applied)
|
|
149
|
-
? data.applied
|
|
150
|
-
: null
|
|
151
|
-
};
|
|
206
|
+
return normalizeWebHistoryRecord(containerName, JSON.parse(fs.readFileSync(filePath, 'utf-8')));
|
|
152
207
|
} catch (e) {
|
|
153
|
-
return {
|
|
154
|
-
containerName,
|
|
155
|
-
updatedAt: null,
|
|
156
|
-
messages: [],
|
|
157
|
-
agentPromptCommand: '',
|
|
158
|
-
agentProgram: '',
|
|
159
|
-
resumeSupported: false,
|
|
160
|
-
lastResumeAt: null,
|
|
161
|
-
lastResumeOk: null,
|
|
162
|
-
lastResumeError: '',
|
|
163
|
-
applied: null
|
|
164
|
-
};
|
|
208
|
+
return normalizeWebHistoryRecord(containerName, {});
|
|
165
209
|
}
|
|
166
210
|
}
|
|
167
211
|
|
|
168
212
|
function saveWebSessionHistory(webHistoryDir, containerName, history) {
|
|
169
213
|
ensureWebHistoryDir(webHistoryDir);
|
|
170
214
|
const filePath = getWebHistoryFile(webHistoryDir, containerName);
|
|
171
|
-
|
|
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));
|
|
172
228
|
}
|
|
173
229
|
|
|
174
230
|
function removeWebSessionHistory(webHistoryDir, containerName) {
|
|
@@ -187,10 +243,84 @@ function listWebHistorySessionNames(webHistoryDir, isValidContainerName) {
|
|
|
187
243
|
.filter(name => isValidContainerName(name));
|
|
188
244
|
}
|
|
189
245
|
|
|
190
|
-
function
|
|
191
|
-
|
|
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 });
|
|
192
322
|
const timestamp = new Date().toISOString();
|
|
193
|
-
|
|
323
|
+
agentSession.messages.push({
|
|
194
324
|
id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
|
195
325
|
role,
|
|
196
326
|
content,
|
|
@@ -198,30 +328,22 @@ function appendWebSessionMessage(webHistoryDir, containerName, role, content, ex
|
|
|
198
328
|
...extra
|
|
199
329
|
});
|
|
200
330
|
|
|
201
|
-
if (
|
|
202
|
-
|
|
331
|
+
if (agentSession.messages.length > WEB_HISTORY_MAX_MESSAGES) {
|
|
332
|
+
agentSession.messages = agentSession.messages.slice(-WEB_HISTORY_MAX_MESSAGES);
|
|
203
333
|
}
|
|
204
334
|
|
|
335
|
+
agentSession.updatedAt = timestamp;
|
|
205
336
|
history.updatedAt = timestamp;
|
|
206
|
-
saveWebSessionHistory(webHistoryDir, containerName, history);
|
|
337
|
+
saveWebSessionHistory(webHistoryDir, sessionRef.containerName, history);
|
|
207
338
|
}
|
|
208
339
|
|
|
209
340
|
function setWebSessionAgentPromptCommand(webHistoryDir, containerName, agentPromptCommand) {
|
|
210
341
|
const history = loadWebSessionHistory(webHistoryDir, containerName);
|
|
211
342
|
history.agentPromptCommand = normalizeAgentPromptCommandTemplate(agentPromptCommand, 'agentPromptCommand');
|
|
212
|
-
const agentProgram = resolveAgentProgram(history.agentPromptCommand);
|
|
213
|
-
const resumeCommand = buildAgentResumeCommand(agentProgram);
|
|
214
|
-
history.agentProgram = agentProgram || '';
|
|
215
|
-
history.resumeSupported = Boolean(resumeCommand);
|
|
216
|
-
if (!history.resumeSupported) {
|
|
217
|
-
history.lastResumeAt = null;
|
|
218
|
-
history.lastResumeOk = null;
|
|
219
|
-
history.lastResumeError = '';
|
|
220
|
-
}
|
|
221
343
|
saveWebSessionHistory(webHistoryDir, containerName, history);
|
|
222
344
|
}
|
|
223
345
|
|
|
224
|
-
function
|
|
346
|
+
function patchWebSessionHistory(webHistoryDir, containerName, patch) {
|
|
225
347
|
const history = loadWebSessionHistory(webHistoryDir, containerName);
|
|
226
348
|
if (!patch || typeof patch !== 'object') {
|
|
227
349
|
return history;
|
|
@@ -233,6 +355,37 @@ function patchWebSessionAgentState(webHistoryDir, containerName, patch) {
|
|
|
233
355
|
return history;
|
|
234
356
|
}
|
|
235
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
|
+
|
|
236
389
|
function stripAnsi(text) {
|
|
237
390
|
if (typeof text !== 'string') return '';
|
|
238
391
|
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
@@ -1124,27 +1277,28 @@ function prepareCodexTraceEvent(payload) {
|
|
|
1124
1277
|
});
|
|
1125
1278
|
}
|
|
1126
1279
|
|
|
1127
|
-
async function prepareWebAgentExecution(ctx, state,
|
|
1128
|
-
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 });
|
|
1129
1283
|
const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
|
|
1130
1284
|
if (normalizedTemplate !== history.agentPromptCommand) {
|
|
1131
1285
|
history.agentPromptCommand = normalizedTemplate;
|
|
1132
|
-
saveWebSessionHistory(state.webHistoryDir, containerName, history);
|
|
1286
|
+
saveWebSessionHistory(state.webHistoryDir, sessionRef.containerName, history);
|
|
1133
1287
|
}
|
|
1134
1288
|
if (!isAgentPromptCommandEnabled(history.agentPromptCommand)) {
|
|
1135
1289
|
throw new Error('当前会话未配置 agentPromptCommand');
|
|
1136
1290
|
}
|
|
1137
1291
|
|
|
1138
|
-
await ensureWebContainer(ctx, state, containerName);
|
|
1292
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
1139
1293
|
const agentMeta = getAgentRuntimeMeta(history);
|
|
1140
|
-
const hasPriorConversation = hasAgentConversationHistory(
|
|
1294
|
+
const hasPriorConversation = hasAgentConversationHistory(agentSession);
|
|
1141
1295
|
let resumeAttempted = false;
|
|
1142
1296
|
let resumeSucceeded = false;
|
|
1143
1297
|
let resumeError = '';
|
|
1144
1298
|
|
|
1145
1299
|
if (hasPriorConversation && agentMeta.resumeSupported && agentMeta.resumeCommand) {
|
|
1146
1300
|
resumeAttempted = true;
|
|
1147
|
-
const resumeResult = await execCommandInWebContainer(ctx, containerName, agentMeta.resumeCommand);
|
|
1301
|
+
const resumeResult = await execCommandInWebContainer(ctx, sessionRef.containerName, agentMeta.resumeCommand);
|
|
1148
1302
|
if (resumeResult.exitCode === 0) {
|
|
1149
1303
|
resumeSucceeded = true;
|
|
1150
1304
|
} else {
|
|
@@ -1154,12 +1308,13 @@ async function prepareWebAgentExecution(ctx, state, containerName, prompt) {
|
|
|
1154
1308
|
|
|
1155
1309
|
const effectivePrompt = resumeSucceeded
|
|
1156
1310
|
? prompt
|
|
1157
|
-
: buildAgentPromptWithHistory(
|
|
1311
|
+
: buildAgentPromptWithHistory(agentSession, prompt);
|
|
1158
1312
|
const command = buildWebAgentExecCommand(history.agentPromptCommand, effectivePrompt, agentMeta.agentProgram);
|
|
1159
1313
|
const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
|
|
1160
1314
|
|
|
1161
1315
|
return {
|
|
1162
1316
|
history,
|
|
1317
|
+
agentSession,
|
|
1163
1318
|
agentMeta,
|
|
1164
1319
|
command,
|
|
1165
1320
|
contextMode,
|
|
@@ -1169,8 +1324,8 @@ async function prepareWebAgentExecution(ctx, state, containerName, prompt) {
|
|
|
1169
1324
|
};
|
|
1170
1325
|
}
|
|
1171
1326
|
|
|
1172
|
-
function finalizeWebAgentExecution(state,
|
|
1173
|
-
appendWebSessionMessage(state.webHistoryDir,
|
|
1327
|
+
function finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, meta, result) {
|
|
1328
|
+
appendWebSessionMessage(state.webHistoryDir, sessionRef, 'assistant', result.output, {
|
|
1174
1329
|
exitCode: result.exitCode,
|
|
1175
1330
|
mode: 'agent',
|
|
1176
1331
|
contextMode: meta.contextMode,
|
|
@@ -1178,21 +1333,19 @@ function finalizeWebAgentExecution(state, containerName, history, agentMeta, met
|
|
|
1178
1333
|
resumeSucceeded: meta.resumeSucceeded,
|
|
1179
1334
|
interrupted: result.interrupted === true
|
|
1180
1335
|
});
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
lastResumeOk: meta.resumeAttempted ? meta.resumeSucceeded : history.lastResumeOk,
|
|
1186
|
-
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 || '')
|
|
1187
1340
|
});
|
|
1188
1341
|
}
|
|
1189
1342
|
|
|
1190
|
-
function appendWebAgentTraceMessage(webHistoryDir,
|
|
1343
|
+
function appendWebAgentTraceMessage(webHistoryDir, sessionRefOrContainerName, content, extra = {}) {
|
|
1191
1344
|
const text = String(content || '').trim();
|
|
1192
1345
|
if (!text) {
|
|
1193
1346
|
return;
|
|
1194
1347
|
}
|
|
1195
|
-
appendWebSessionMessage(webHistoryDir,
|
|
1348
|
+
appendWebSessionMessage(webHistoryDir, sessionRefOrContainerName, 'assistant', text, {
|
|
1196
1349
|
mode: 'agent',
|
|
1197
1350
|
streamTrace: true,
|
|
1198
1351
|
...extra
|
|
@@ -1300,6 +1453,59 @@ function toPlainObject(value) {
|
|
|
1300
1453
|
return value;
|
|
1301
1454
|
}
|
|
1302
1455
|
|
|
1456
|
+
function isSensitiveConfigKey(key) {
|
|
1457
|
+
const normalized = String(key || '').trim();
|
|
1458
|
+
return Boolean(normalized) && SENSITIVE_CONFIG_KEY_PATTERN.test(normalized);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function redactConfigValue(value) {
|
|
1462
|
+
if (Array.isArray(value)) {
|
|
1463
|
+
return value.map(item => redactConfigValue(item));
|
|
1464
|
+
}
|
|
1465
|
+
if (!value || typeof value !== 'object') {
|
|
1466
|
+
return REDACTED_CONFIG_VALUE;
|
|
1467
|
+
}
|
|
1468
|
+
return redactConfigObject(value);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function redactConfigObject(value) {
|
|
1472
|
+
if (Array.isArray(value)) {
|
|
1473
|
+
return value.map(item => redactConfigValue(item));
|
|
1474
|
+
}
|
|
1475
|
+
if (!value || typeof value !== 'object') {
|
|
1476
|
+
return value;
|
|
1477
|
+
}
|
|
1478
|
+
const result = {};
|
|
1479
|
+
Object.entries(toPlainObject(value)).forEach(([key, item]) => {
|
|
1480
|
+
if (isSensitiveConfigKey(key)) {
|
|
1481
|
+
result[key] = redactConfigValue(item);
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
if (Array.isArray(item)) {
|
|
1485
|
+
result[key] = item.map(entry => redactConfigValue(entry));
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
if (item && typeof item === 'object') {
|
|
1489
|
+
result[key] = redactConfigObject(item);
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
result[key] = item;
|
|
1493
|
+
});
|
|
1494
|
+
return result;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function buildSafeWebConfigSnapshot(snapshot, ctx) {
|
|
1498
|
+
const parsed = snapshot && snapshot.parseError ? {} : toPlainObject(snapshot && snapshot.parsed);
|
|
1499
|
+
return {
|
|
1500
|
+
path: snapshot && snapshot.path ? snapshot.path : path.resolve(getDefaultWebConfigPath()),
|
|
1501
|
+
parsed: redactConfigObject(parsed),
|
|
1502
|
+
defaults: redactConfigObject(buildConfigDefaults(ctx, parsed)),
|
|
1503
|
+
parseError: snapshot && snapshot.parseError ? snapshot.parseError : null,
|
|
1504
|
+
editable: false,
|
|
1505
|
+
notice: 'Web 端仅显示脱敏后的配置摘要;敏感值请在本地 manyoyo.json 中维护。'
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1303
1509
|
function pickFirstString() {
|
|
1304
1510
|
for (let i = 0; i < arguments.length; i += 1) {
|
|
1305
1511
|
const value = arguments[i];
|
|
@@ -1705,23 +1911,29 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
1705
1911
|
const hasConfigPorts = hasOwn(config, 'ports');
|
|
1706
1912
|
|
|
1707
1913
|
const requestName = pickFirstString(requestOptions.containerName, body.name);
|
|
1708
|
-
let containerName = pickFirstString(requestName, config.containerName);
|
|
1914
|
+
let containerName = pickFirstString(requestName, runConfig.containerName, config.containerName);
|
|
1709
1915
|
if (!containerName) {
|
|
1710
1916
|
containerName = `my-${ctx.formatDate()}`;
|
|
1711
1917
|
}
|
|
1712
1918
|
containerName = resolveNowTemplate(containerName, ctx.formatDate);
|
|
1713
1919
|
validateContainerNameStrict(containerName);
|
|
1714
1920
|
|
|
1715
|
-
const hostPath = pickFirstString(requestOptions.hostPath, config.hostPath, ctx.hostPath);
|
|
1921
|
+
const hostPath = pickFirstString(requestOptions.hostPath, runConfig.hostPath, config.hostPath, ctx.hostPath);
|
|
1716
1922
|
if (typeof ctx.validateHostPath === 'function') {
|
|
1717
1923
|
ctx.validateHostPath(hostPath);
|
|
1718
1924
|
} else {
|
|
1719
1925
|
validateWebHostPath(hostPath);
|
|
1720
1926
|
}
|
|
1721
1927
|
|
|
1722
|
-
const containerPath = pickFirstString(
|
|
1723
|
-
|
|
1724
|
-
|
|
1928
|
+
const containerPath = pickFirstString(
|
|
1929
|
+
requestOptions.containerPath,
|
|
1930
|
+
runConfig.containerPath,
|
|
1931
|
+
config.containerPath,
|
|
1932
|
+
ctx.containerPath,
|
|
1933
|
+
hostPath
|
|
1934
|
+
) || hostPath;
|
|
1935
|
+
const imageName = pickFirstString(requestOptions.imageName, runConfig.imageName, config.imageName, ctx.imageName);
|
|
1936
|
+
const imageVersion = pickFirstString(requestOptions.imageVersion, runConfig.imageVersion, config.imageVersion, ctx.imageVersion);
|
|
1725
1937
|
|
|
1726
1938
|
if (!/^[A-Za-z0-9][A-Za-z0-9._/:-]*$/.test(imageName)) {
|
|
1727
1939
|
throw new Error(`imageName 非法: ${imageName}`);
|
|
@@ -1730,7 +1942,7 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
1730
1942
|
|
|
1731
1943
|
let contModeArgs = Array.isArray(ctx.contModeArgs) ? ctx.contModeArgs.slice() : [];
|
|
1732
1944
|
let containerMode = '';
|
|
1733
|
-
const modeValue = pickFirstString(requestOptions.containerMode, config.containerMode);
|
|
1945
|
+
const modeValue = pickFirstString(requestOptions.containerMode, runConfig.containerMode, config.containerMode);
|
|
1734
1946
|
if (modeValue) {
|
|
1735
1947
|
const mode = resolveContainerModeArgs(modeValue);
|
|
1736
1948
|
containerMode = mode.mode;
|
|
@@ -1739,16 +1951,24 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
1739
1951
|
|
|
1740
1952
|
const shellPrefix = hasOwn(requestOptions, 'shellPrefix')
|
|
1741
1953
|
? String(requestOptions.shellPrefix || '')
|
|
1742
|
-
: (hasOwn(
|
|
1954
|
+
: (hasOwn(runConfig, 'shellPrefix')
|
|
1955
|
+
? String(runConfig.shellPrefix || '')
|
|
1956
|
+
: (hasOwn(config, 'shellPrefix') ? String(config.shellPrefix || '') : String(ctx.execCommandPrefix || '')));
|
|
1743
1957
|
let shell = hasOwn(requestOptions, 'shell')
|
|
1744
1958
|
? String(requestOptions.shell || '')
|
|
1745
|
-
: (hasOwn(
|
|
1959
|
+
: (hasOwn(runConfig, 'shell')
|
|
1960
|
+
? String(runConfig.shell || '')
|
|
1961
|
+
: (hasOwn(config, 'shell') ? String(config.shell || '') : String(ctx.execCommand || '')));
|
|
1746
1962
|
const shellSuffix = hasOwn(requestOptions, 'shellSuffix')
|
|
1747
1963
|
? String(requestOptions.shellSuffix || '')
|
|
1748
|
-
: (hasOwn(
|
|
1964
|
+
: (hasOwn(runConfig, 'shellSuffix')
|
|
1965
|
+
? String(runConfig.shellSuffix || '')
|
|
1966
|
+
: (hasOwn(config, 'shellSuffix') ? String(config.shellSuffix || '') : String(ctx.execCommandSuffix || '')));
|
|
1749
1967
|
const yolo = hasOwn(requestOptions, 'yolo')
|
|
1750
1968
|
? String(requestOptions.yolo || '')
|
|
1751
|
-
: (hasOwn(
|
|
1969
|
+
: (hasOwn(runConfig, 'yolo')
|
|
1970
|
+
? String(runConfig.yolo || '')
|
|
1971
|
+
: (hasOwn(config, 'yolo') ? String(config.yolo || '') : ''));
|
|
1752
1972
|
const yoloCommand = resolveYoloCommand(yolo);
|
|
1753
1973
|
if (yoloCommand) {
|
|
1754
1974
|
shell = yoloCommand;
|
|
@@ -1769,19 +1989,23 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
1769
1989
|
const resumeSupported = Boolean(buildAgentResumeCommand(agentProgram));
|
|
1770
1990
|
|
|
1771
1991
|
let containerEnvs = Array.isArray(ctx.containerEnvs) ? ctx.containerEnvs.slice() : [];
|
|
1772
|
-
|
|
1992
|
+
const hasRunEnv = hasOwn(runConfig, 'env');
|
|
1993
|
+
const hasRunEnvFile = hasOwn(runConfig, 'envFile');
|
|
1994
|
+
if (hasRequestEnv || hasRequestEnvFile || hasRunEnv || hasRunEnvFile || hasConfigEnv || hasConfigEnvFile) {
|
|
1773
1995
|
const configEnv = normalizeEnvMap(config.env, 'config.env');
|
|
1996
|
+
const runEnv = normalizeEnvMap(runConfig.env, runName ? `runs.${runName}.env` : 'run.env');
|
|
1774
1997
|
const requestEnv = hasRequestEnv ? normalizeEnvMap(requestOptions.env, 'createOptions.env') : {};
|
|
1775
|
-
const mergedEnv = { ...configEnv, ...requestEnv };
|
|
1998
|
+
const mergedEnv = { ...configEnv, ...runEnv, ...requestEnv };
|
|
1776
1999
|
const envArgs = [];
|
|
1777
2000
|
Object.entries(mergedEnv).forEach(([key, value]) => {
|
|
1778
2001
|
const parsed = parseEnvEntry(`${key}=${value}`);
|
|
1779
2002
|
envArgs.push('--env', `${parsed.key}=${parsed.value}`);
|
|
1780
2003
|
});
|
|
1781
2004
|
|
|
1782
|
-
const envFileList =
|
|
1783
|
-
|
|
1784
|
-
|
|
2005
|
+
const envFileList = []
|
|
2006
|
+
.concat(normalizeStringArray(config.envFile, 'config.envFile'))
|
|
2007
|
+
.concat(normalizeStringArray(runConfig.envFile, runName ? `runs.${runName}.envFile` : 'run.envFile'))
|
|
2008
|
+
.concat(hasRequestEnvFile ? normalizeStringArray(requestOptions.envFile, 'createOptions.envFile') : []);
|
|
1785
2009
|
const envFileArgs = [];
|
|
1786
2010
|
envFileList.forEach(filePath => {
|
|
1787
2011
|
envFileArgs.push(...parseEnvFileToArgs(filePath));
|
|
@@ -1791,10 +2015,12 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
1791
2015
|
}
|
|
1792
2016
|
|
|
1793
2017
|
let containerVolumes = Array.isArray(ctx.containerVolumes) ? ctx.containerVolumes.slice() : [];
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
2018
|
+
const hasRunVolumes = hasOwn(runConfig, 'volumes');
|
|
2019
|
+
if (hasRequestVolumes || hasRunVolumes || hasConfigVolumes) {
|
|
2020
|
+
const volumeList = []
|
|
2021
|
+
.concat(normalizeStringArray(config.volumes, 'config.volumes'))
|
|
2022
|
+
.concat(normalizeStringArray(runConfig.volumes, runName ? `runs.${runName}.volumes` : 'run.volumes'))
|
|
2023
|
+
.concat(hasRequestVolumes ? normalizeStringArray(requestOptions.volumes, 'createOptions.volumes') : []);
|
|
1798
2024
|
containerVolumes = [];
|
|
1799
2025
|
volumeList.forEach(volume => {
|
|
1800
2026
|
containerVolumes.push('--volume', normalizeVolume(volume));
|
|
@@ -1802,10 +2028,12 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
1802
2028
|
}
|
|
1803
2029
|
|
|
1804
2030
|
let containerPorts = Array.isArray(ctx.containerPorts) ? ctx.containerPorts.slice() : [];
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
2031
|
+
const hasRunPorts = hasOwn(runConfig, 'ports');
|
|
2032
|
+
if (hasRequestPorts || hasRunPorts || hasConfigPorts) {
|
|
2033
|
+
const portList = []
|
|
2034
|
+
.concat(normalizeStringArray(config.ports, 'config.ports'))
|
|
2035
|
+
.concat(normalizeStringArray(runConfig.ports, runName ? `runs.${runName}.ports` : 'run.ports'))
|
|
2036
|
+
.concat(hasRequestPorts ? normalizeStringArray(requestOptions.ports, 'createOptions.ports') : []);
|
|
1809
2037
|
containerPorts = [];
|
|
1810
2038
|
portList.forEach(port => {
|
|
1811
2039
|
containerPorts.push('--publish', port);
|
|
@@ -1905,7 +2133,7 @@ function listWebManyoyoContainers(ctx) {
|
|
|
1905
2133
|
return map;
|
|
1906
2134
|
}
|
|
1907
2135
|
|
|
1908
|
-
async function ensureWebContainer(ctx, state, containerInput) {
|
|
2136
|
+
async function ensureWebContainer(ctx, state, containerInput, messageSessionRef = null) {
|
|
1909
2137
|
const runtime = typeof containerInput === 'string'
|
|
1910
2138
|
? buildStaticContainerRuntime(ctx, containerInput)
|
|
1911
2139
|
: containerInput;
|
|
@@ -1937,14 +2165,24 @@ async function ensureWebContainer(ctx, state, containerInput) {
|
|
|
1937
2165
|
}
|
|
1938
2166
|
|
|
1939
2167
|
await ctx.waitForContainerReady(runtime.containerName);
|
|
1940
|
-
appendWebSessionMessage(
|
|
2168
|
+
appendWebSessionMessage(
|
|
2169
|
+
state.webHistoryDir,
|
|
2170
|
+
messageSessionRef || runtime.containerName,
|
|
2171
|
+
'system',
|
|
2172
|
+
`容器 ${runtime.containerName} 已创建并启动。`
|
|
2173
|
+
);
|
|
1941
2174
|
return;
|
|
1942
2175
|
}
|
|
1943
2176
|
|
|
1944
2177
|
const status = ctx.getContainerStatus(runtime.containerName);
|
|
1945
2178
|
if (status !== 'running') {
|
|
1946
2179
|
ctx.dockerExecArgs(['start', runtime.containerName], { stdio: 'pipe' });
|
|
1947
|
-
appendWebSessionMessage(
|
|
2180
|
+
appendWebSessionMessage(
|
|
2181
|
+
state.webHistoryDir,
|
|
2182
|
+
messageSessionRef || runtime.containerName,
|
|
2183
|
+
'system',
|
|
2184
|
+
`容器 ${runtime.containerName} 已启动。`
|
|
2185
|
+
);
|
|
1948
2186
|
}
|
|
1949
2187
|
}
|
|
1950
2188
|
|
|
@@ -2008,24 +2246,29 @@ async function execCommandInWebContainer(ctx, containerName, command, options =
|
|
|
2008
2246
|
});
|
|
2009
2247
|
}
|
|
2010
2248
|
|
|
2011
|
-
async function execAgentInWebContainerStream(ctx, state,
|
|
2249
|
+
async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerName, command, options = {}) {
|
|
2012
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);
|
|
2013
2255
|
const agentProgram = typeof opts.agentProgram === 'string' ? opts.agentProgram : '';
|
|
2014
2256
|
const onEvent = typeof opts.onEvent === 'function' ? opts.onEvent : () => {};
|
|
2015
2257
|
const process = spawn(
|
|
2016
2258
|
ctx.dockerCmd,
|
|
2017
|
-
['exec', containerName, '/bin/bash', '-lc', command],
|
|
2259
|
+
['exec', sessionRef.containerName, '/bin/bash', '-lc', command],
|
|
2018
2260
|
{ stdio: ['ignore', 'pipe', 'pipe'] }
|
|
2019
2261
|
);
|
|
2020
2262
|
|
|
2021
2263
|
const runState = {
|
|
2022
|
-
containerName,
|
|
2264
|
+
containerName: sessionRef.containerName,
|
|
2265
|
+
sessionKey,
|
|
2023
2266
|
process,
|
|
2024
2267
|
command,
|
|
2025
2268
|
startedAt: new Date().toISOString(),
|
|
2026
2269
|
stopping: false
|
|
2027
2270
|
};
|
|
2028
|
-
state.agentRuns.set(containerName, runState);
|
|
2271
|
+
state.agentRuns.set(sessionRef.containerName, runState);
|
|
2029
2272
|
|
|
2030
2273
|
return await new Promise((resolve, reject) => {
|
|
2031
2274
|
const MAX_RAW_OUTPUT_CHARS = 32 * 1024 * 1024;
|
|
@@ -2124,11 +2367,11 @@ async function execAgentInWebContainerStream(ctx, state, containerName, command,
|
|
|
2124
2367
|
});
|
|
2125
2368
|
|
|
2126
2369
|
process.on('error', error => {
|
|
2127
|
-
state.agentRuns.delete(containerName);
|
|
2370
|
+
state.agentRuns.delete(sessionRef.containerName);
|
|
2128
2371
|
reject(error);
|
|
2129
2372
|
});
|
|
2130
2373
|
process.on('close', code => {
|
|
2131
|
-
state.agentRuns.delete(containerName);
|
|
2374
|
+
state.agentRuns.delete(sessionRef.containerName);
|
|
2132
2375
|
if (stdoutPending) {
|
|
2133
2376
|
emitStdoutTraceLine(stdoutPending);
|
|
2134
2377
|
stdoutPending = '';
|
|
@@ -2216,6 +2459,15 @@ function sendHtml(res, statusCode, html, extraHeaders = {}) {
|
|
|
2216
2459
|
res.end(html);
|
|
2217
2460
|
}
|
|
2218
2461
|
|
|
2462
|
+
function sendRedirect(res, statusCode, location, extraHeaders = {}) {
|
|
2463
|
+
res.writeHead(statusCode, {
|
|
2464
|
+
Location: location,
|
|
2465
|
+
'Cache-Control': 'no-store',
|
|
2466
|
+
...extraHeaders
|
|
2467
|
+
});
|
|
2468
|
+
res.end('');
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2219
2471
|
function decodeSessionName(encoded) {
|
|
2220
2472
|
try {
|
|
2221
2473
|
return decodeURIComponent(encoded);
|
|
@@ -2224,30 +2476,51 @@ function decodeSessionName(encoded) {
|
|
|
2224
2476
|
}
|
|
2225
2477
|
}
|
|
2226
2478
|
|
|
2227
|
-
function
|
|
2228
|
-
const
|
|
2229
|
-
if (!ctx.isValidContainerName(containerName)) {
|
|
2230
|
-
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}` });
|
|
2231
2483
|
return null;
|
|
2232
2484
|
}
|
|
2233
|
-
|
|
2485
|
+
if (!SAFE_CONTAINER_NAME_PATTERN.test(parsed.agentId)) {
|
|
2486
|
+
sendJson(res, 400, { error: `agentId 非法: ${parsed.agentId}` });
|
|
2487
|
+
return null;
|
|
2488
|
+
}
|
|
2489
|
+
return parsed;
|
|
2234
2490
|
}
|
|
2235
2491
|
|
|
2236
|
-
function buildSessionSummary(ctx, state, containerMap,
|
|
2237
|
-
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);
|
|
2238
2496
|
const agentMeta = getAgentRuntimeMeta(history);
|
|
2239
|
-
const
|
|
2240
|
-
|
|
2241
|
-
|
|
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;
|
|
2242
2510
|
return {
|
|
2243
|
-
name,
|
|
2511
|
+
name: buildWebSessionKey(containerName, agentId),
|
|
2512
|
+
containerName,
|
|
2513
|
+
agentId,
|
|
2514
|
+
agentName: agentSession.agentName,
|
|
2244
2515
|
status: containerInfo.status || 'history',
|
|
2245
2516
|
image: containerInfo.image || '',
|
|
2246
2517
|
updatedAt,
|
|
2247
|
-
messageCount:
|
|
2518
|
+
messageCount: agentSession.messages.length,
|
|
2248
2519
|
agentEnabled: isAgentPromptCommandEnabled(history.agentPromptCommand),
|
|
2249
2520
|
agentProgram: agentMeta.agentProgram,
|
|
2250
|
-
resumeSupported: agentMeta.resumeSupported
|
|
2521
|
+
resumeSupported: agentMeta.resumeSupported,
|
|
2522
|
+
hostPath: applied.hostPath || '',
|
|
2523
|
+
containerPath: applied.containerPath || ''
|
|
2251
2524
|
};
|
|
2252
2525
|
}
|
|
2253
2526
|
|
|
@@ -2255,9 +2528,8 @@ function buildSessionFallbackApplied(ctx, state, name, history, summary) {
|
|
|
2255
2528
|
const snapshot = readWebConfigSnapshot(state.webConfigPath);
|
|
2256
2529
|
const defaults = buildConfigDefaults(ctx, snapshot.parseError ? {} : snapshot.parsed);
|
|
2257
2530
|
const effectiveAgentPromptCommand = history.agentPromptCommand || defaults.agentPromptCommand || '';
|
|
2258
|
-
const effectiveAgentProgram =
|
|
2259
|
-
const effectiveResumeSupported =
|
|
2260
|
-
|| Boolean(buildAgentResumeCommand(effectiveAgentProgram));
|
|
2531
|
+
const effectiveAgentProgram = resolveAgentProgram(effectiveAgentPromptCommand) || '';
|
|
2532
|
+
const effectiveResumeSupported = Boolean(buildAgentResumeCommand(effectiveAgentProgram));
|
|
2261
2533
|
const defaultCommand = buildDefaultCommand(
|
|
2262
2534
|
defaults.shellPrefix,
|
|
2263
2535
|
defaults.shell,
|
|
@@ -2287,24 +2559,34 @@ function buildSessionFallbackApplied(ctx, state, name, history, summary) {
|
|
|
2287
2559
|
}
|
|
2288
2560
|
|
|
2289
2561
|
function buildSessionDetail(ctx, state, containerMap, name) {
|
|
2290
|
-
const
|
|
2562
|
+
const sessionRef = typeof name === 'string' ? parseWebSessionKey(name) : name;
|
|
2563
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
2291
2564
|
const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
|
|
2292
|
-
const summary = buildSessionSummary(ctx, state, containerMap,
|
|
2293
|
-
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;
|
|
2294
2571
|
const applied = history.applied && typeof history.applied === 'object' && !Array.isArray(history.applied)
|
|
2295
2572
|
? history.applied
|
|
2296
|
-
: buildSessionFallbackApplied(ctx, state,
|
|
2573
|
+
: buildSessionFallbackApplied(ctx, state, sessionRef.containerName, history, summary || {});
|
|
2574
|
+
|
|
2575
|
+
if (!summary || !agentSession) {
|
|
2576
|
+
return null;
|
|
2577
|
+
}
|
|
2297
2578
|
|
|
2298
2579
|
return {
|
|
2299
2580
|
...summary,
|
|
2581
|
+
agentName: agentSession.agentName,
|
|
2300
2582
|
latestRole: latestMessage && latestMessage.role ? String(latestMessage.role) : '',
|
|
2301
2583
|
latestTimestamp: latestMessage && latestMessage.timestamp ? latestMessage.timestamp : summary.updatedAt,
|
|
2302
2584
|
agentPromptCommand: normalizedTemplate || '',
|
|
2303
|
-
agentProgram:
|
|
2304
|
-
resumeSupported:
|
|
2305
|
-
lastResumeAt:
|
|
2306
|
-
lastResumeOk: typeof
|
|
2307
|
-
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 || '',
|
|
2308
2590
|
applied
|
|
2309
2591
|
};
|
|
2310
2592
|
}
|
|
@@ -2563,6 +2845,12 @@ function bindTerminalWebSocket(ctx, state, ws, containerName, cols, rows) {
|
|
|
2563
2845
|
}
|
|
2564
2846
|
|
|
2565
2847
|
async function handleWebAuthRoutes(req, res, pathname, ctx, state) {
|
|
2848
|
+
if (req.method === 'GET' && pathname === '/favicon.ico') {
|
|
2849
|
+
res.writeHead(204, { 'Cache-Control': 'no-store' });
|
|
2850
|
+
res.end();
|
|
2851
|
+
return true;
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2566
2854
|
if (req.method === 'GET' && pathname === '/auth/login') {
|
|
2567
2855
|
sendHtml(res, 200, loadTemplate('login.html'));
|
|
2568
2856
|
return true;
|
|
@@ -2625,6 +2913,10 @@ function sendWebUnauthorized(res, pathname) {
|
|
|
2625
2913
|
sendJson(res, 401, { error: 'UNAUTHORIZED' });
|
|
2626
2914
|
return;
|
|
2627
2915
|
}
|
|
2916
|
+
if (pathname === '/' || pathname === '') {
|
|
2917
|
+
sendRedirect(res, 302, '/auth/login', { 'Set-Cookie': getWebAuthClearCookie() });
|
|
2918
|
+
return;
|
|
2919
|
+
}
|
|
2628
2920
|
sendHtml(
|
|
2629
2921
|
res,
|
|
2630
2922
|
401,
|
|
@@ -2645,19 +2937,58 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2645
2937
|
const routes = [
|
|
2646
2938
|
{
|
|
2647
2939
|
method: 'GET',
|
|
2648
|
-
match: currentPath => currentPath === '/api/
|
|
2940
|
+
match: currentPath => currentPath === '/api/fs/directories' ? [] : null,
|
|
2649
2941
|
handler: async () => {
|
|
2650
|
-
const
|
|
2651
|
-
const
|
|
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
|
+
|
|
2652
2976
|
sendJson(res, 200, {
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
defaults
|
|
2977
|
+
currentPath: realPath,
|
|
2978
|
+
basePath: realBasePath || '',
|
|
2979
|
+
parentPath,
|
|
2980
|
+
entries
|
|
2658
2981
|
});
|
|
2659
2982
|
}
|
|
2660
2983
|
},
|
|
2984
|
+
{
|
|
2985
|
+
method: 'GET',
|
|
2986
|
+
match: currentPath => currentPath === '/api/config' ? [] : null,
|
|
2987
|
+
handler: async () => {
|
|
2988
|
+
const snapshot = readWebConfigSnapshot(state.webConfigPath);
|
|
2989
|
+
sendJson(res, 200, buildSafeWebConfigSnapshot(snapshot, ctx));
|
|
2990
|
+
}
|
|
2991
|
+
},
|
|
2661
2992
|
{
|
|
2662
2993
|
method: 'PUT',
|
|
2663
2994
|
match: currentPath => currentPath === '/api/config' ? [] : null,
|
|
@@ -2699,7 +3030,15 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2699
3030
|
]);
|
|
2700
3031
|
|
|
2701
3032
|
const sessions = Array.from(names)
|
|
2702
|
-
.
|
|
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
|
+
})
|
|
2703
3042
|
.sort((a, b) => {
|
|
2704
3043
|
const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
|
2705
3044
|
const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
|
@@ -2724,44 +3063,70 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2724
3063
|
|
|
2725
3064
|
await ensureWebContainer(ctx, state, runtime);
|
|
2726
3065
|
setWebSessionAgentPromptCommand(state.webHistoryDir, runtime.containerName, runtime.agentPromptCommand);
|
|
2727
|
-
|
|
3066
|
+
patchWebSessionHistory(state.webHistoryDir, runtime.containerName, {
|
|
2728
3067
|
applied: runtime.applied
|
|
2729
3068
|
});
|
|
2730
3069
|
sendJson(res, 200, { name: runtime.containerName, applied: runtime.applied });
|
|
2731
3070
|
}
|
|
2732
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
|
+
},
|
|
2733
3091
|
{
|
|
2734
3092
|
method: 'GET',
|
|
2735
3093
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/messages$/),
|
|
2736
3094
|
handler: async match => {
|
|
2737
|
-
const
|
|
2738
|
-
if (!
|
|
3095
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3096
|
+
if (!sessionRef) {
|
|
2739
3097
|
return;
|
|
2740
3098
|
}
|
|
2741
|
-
const history = loadWebSessionHistory(state.webHistoryDir, containerName);
|
|
2742
|
-
|
|
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
|
+
});
|
|
2743
3108
|
}
|
|
2744
3109
|
},
|
|
2745
3110
|
{
|
|
2746
3111
|
method: 'GET',
|
|
2747
3112
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/detail$/),
|
|
2748
3113
|
handler: async match => {
|
|
2749
|
-
const
|
|
2750
|
-
if (!
|
|
3114
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3115
|
+
if (!sessionRef) {
|
|
2751
3116
|
return;
|
|
2752
3117
|
}
|
|
2753
3118
|
|
|
2754
3119
|
const containerMap = listWebManyoyoContainers(ctx);
|
|
2755
|
-
const detail = buildSessionDetail(ctx, state, containerMap,
|
|
2756
|
-
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 });
|
|
2757
3122
|
}
|
|
2758
3123
|
},
|
|
2759
3124
|
{
|
|
2760
3125
|
method: 'POST',
|
|
2761
3126
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/run$/),
|
|
2762
3127
|
handler: async match => {
|
|
2763
|
-
const
|
|
2764
|
-
if (!
|
|
3128
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3129
|
+
if (!sessionRef) {
|
|
2765
3130
|
return;
|
|
2766
3131
|
}
|
|
2767
3132
|
|
|
@@ -2772,12 +3137,12 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2772
3137
|
return;
|
|
2773
3138
|
}
|
|
2774
3139
|
|
|
2775
|
-
await ensureWebContainer(ctx, state, containerName);
|
|
2776
|
-
appendWebSessionMessage(state.webHistoryDir,
|
|
2777
|
-
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);
|
|
2778
3143
|
appendWebSessionMessage(
|
|
2779
3144
|
state.webHistoryDir,
|
|
2780
|
-
|
|
3145
|
+
sessionRef,
|
|
2781
3146
|
'assistant',
|
|
2782
3147
|
result.output,
|
|
2783
3148
|
{ exitCode: result.exitCode }
|
|
@@ -2789,8 +3154,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2789
3154
|
method: 'POST',
|
|
2790
3155
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent$/),
|
|
2791
3156
|
handler: async match => {
|
|
2792
|
-
const
|
|
2793
|
-
if (!
|
|
3157
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3158
|
+
if (!sessionRef) {
|
|
2794
3159
|
return;
|
|
2795
3160
|
}
|
|
2796
3161
|
|
|
@@ -2803,21 +3168,21 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2803
3168
|
|
|
2804
3169
|
let prepared = null;
|
|
2805
3170
|
try {
|
|
2806
|
-
prepared = await prepareWebAgentExecution(ctx, state,
|
|
3171
|
+
prepared = await prepareWebAgentExecution(ctx, state, sessionRef, prompt);
|
|
2807
3172
|
} catch (e) {
|
|
2808
3173
|
sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
|
|
2809
3174
|
return;
|
|
2810
3175
|
}
|
|
2811
3176
|
|
|
2812
|
-
const {
|
|
2813
|
-
appendWebSessionMessage(state.webHistoryDir,
|
|
3177
|
+
const { agentSession, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
|
|
3178
|
+
appendWebSessionMessage(state.webHistoryDir, sessionRef, 'user', prompt, {
|
|
2814
3179
|
mode: 'agent',
|
|
2815
3180
|
contextMode
|
|
2816
3181
|
});
|
|
2817
|
-
const result = await execCommandInWebContainer(ctx, containerName, command, {
|
|
3182
|
+
const result = await execCommandInWebContainer(ctx, sessionRef.containerName, command, {
|
|
2818
3183
|
agentProgram: agentMeta.agentProgram
|
|
2819
3184
|
});
|
|
2820
|
-
finalizeWebAgentExecution(state,
|
|
3185
|
+
finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, {
|
|
2821
3186
|
contextMode,
|
|
2822
3187
|
resumeAttempted,
|
|
2823
3188
|
resumeSucceeded,
|
|
@@ -2837,8 +3202,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2837
3202
|
method: 'POST',
|
|
2838
3203
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent\/stream$/),
|
|
2839
3204
|
handler: async match => {
|
|
2840
|
-
const
|
|
2841
|
-
if (!
|
|
3205
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3206
|
+
if (!sessionRef) {
|
|
2842
3207
|
return;
|
|
2843
3208
|
}
|
|
2844
3209
|
|
|
@@ -2848,23 +3213,23 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2848
3213
|
sendJson(res, 400, { error: 'prompt 不能为空' });
|
|
2849
3214
|
return;
|
|
2850
3215
|
}
|
|
2851
|
-
if (state.agentRuns.has(containerName)) {
|
|
3216
|
+
if (state.agentRuns.has(sessionRef.containerName)) {
|
|
2852
3217
|
sendJson(res, 409, { error: '当前会话已有运行中的 agent 任务' });
|
|
2853
3218
|
return;
|
|
2854
3219
|
}
|
|
2855
3220
|
|
|
2856
3221
|
let prepared = null;
|
|
2857
3222
|
try {
|
|
2858
|
-
prepared = await prepareWebAgentExecution(ctx, state,
|
|
3223
|
+
prepared = await prepareWebAgentExecution(ctx, state, sessionRef, prompt);
|
|
2859
3224
|
} catch (e) {
|
|
2860
3225
|
sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
|
|
2861
3226
|
return;
|
|
2862
3227
|
}
|
|
2863
3228
|
|
|
2864
|
-
const {
|
|
3229
|
+
const { agentSession, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
|
|
2865
3230
|
const traceLines = ['[执行过程]'];
|
|
2866
3231
|
const traceEvents = [];
|
|
2867
|
-
appendWebSessionMessage(state.webHistoryDir,
|
|
3232
|
+
appendWebSessionMessage(state.webHistoryDir, sessionRef, 'user', prompt, {
|
|
2868
3233
|
mode: 'agent',
|
|
2869
3234
|
contextMode
|
|
2870
3235
|
});
|
|
@@ -2876,7 +3241,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2876
3241
|
});
|
|
2877
3242
|
sendNdjson(res, {
|
|
2878
3243
|
type: 'meta',
|
|
2879
|
-
containerName,
|
|
3244
|
+
containerName: sessionRef.containerName,
|
|
3245
|
+
sessionName: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
|
|
2880
3246
|
contextMode,
|
|
2881
3247
|
resumeAttempted,
|
|
2882
3248
|
resumeSucceeded,
|
|
@@ -2890,7 +3256,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2890
3256
|
}
|
|
2891
3257
|
|
|
2892
3258
|
try {
|
|
2893
|
-
const result = await execAgentInWebContainerStream(ctx, state,
|
|
3259
|
+
const result = await execAgentInWebContainerStream(ctx, state, sessionRef, command, {
|
|
2894
3260
|
agentProgram: agentMeta.agentProgram,
|
|
2895
3261
|
onEvent: event => {
|
|
2896
3262
|
if (event && event.type === 'trace' && event.text) {
|
|
@@ -2903,14 +3269,14 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2903
3269
|
}
|
|
2904
3270
|
});
|
|
2905
3271
|
traceLines.push(result.interrupted === true ? '[任务] 已停止' : '[任务] 已完成');
|
|
2906
|
-
appendWebAgentTraceMessage(state.webHistoryDir,
|
|
3272
|
+
appendWebAgentTraceMessage(state.webHistoryDir, sessionRef, traceLines.join('\n'), {
|
|
2907
3273
|
traceEvents,
|
|
2908
3274
|
contextMode,
|
|
2909
3275
|
resumeAttempted,
|
|
2910
3276
|
resumeSucceeded,
|
|
2911
3277
|
interrupted: result.interrupted === true
|
|
2912
3278
|
});
|
|
2913
|
-
finalizeWebAgentExecution(state,
|
|
3279
|
+
finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, {
|
|
2914
3280
|
contextMode,
|
|
2915
3281
|
resumeAttempted,
|
|
2916
3282
|
resumeSucceeded,
|
|
@@ -2927,7 +3293,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2927
3293
|
});
|
|
2928
3294
|
} catch (e) {
|
|
2929
3295
|
traceLines.push(`[错误] ${e && e.message ? e.message : 'Agent 执行失败'}`);
|
|
2930
|
-
appendWebAgentTraceMessage(state.webHistoryDir,
|
|
3296
|
+
appendWebAgentTraceMessage(state.webHistoryDir, sessionRef, traceLines.join('\n'), {
|
|
2931
3297
|
traceEvents,
|
|
2932
3298
|
contextMode,
|
|
2933
3299
|
resumeAttempted,
|
|
@@ -2947,11 +3313,11 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2947
3313
|
method: 'POST',
|
|
2948
3314
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent\/stop$/),
|
|
2949
3315
|
handler: async match => {
|
|
2950
|
-
const
|
|
2951
|
-
if (!
|
|
3316
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3317
|
+
if (!sessionRef) {
|
|
2952
3318
|
return;
|
|
2953
3319
|
}
|
|
2954
|
-
const stopped = stopWebAgentRun(state, containerName);
|
|
3320
|
+
const stopped = stopWebAgentRun(state, sessionRef.containerName);
|
|
2955
3321
|
if (!stopped) {
|
|
2956
3322
|
sendJson(res, 404, { error: '当前会话没有运行中的 agent 任务' });
|
|
2957
3323
|
return;
|
|
@@ -2963,30 +3329,45 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2963
3329
|
method: 'POST',
|
|
2964
3330
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/remove$/),
|
|
2965
3331
|
handler: async match => {
|
|
2966
|
-
const
|
|
2967
|
-
if (!
|
|
3332
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3333
|
+
if (!sessionRef) {
|
|
2968
3334
|
return;
|
|
2969
3335
|
}
|
|
2970
3336
|
|
|
2971
|
-
if (ctx.containerExists(containerName)) {
|
|
2972
|
-
ctx.removeContainer(containerName);
|
|
2973
|
-
appendWebSessionMessage(state.webHistoryDir,
|
|
3337
|
+
if (ctx.containerExists(sessionRef.containerName)) {
|
|
3338
|
+
ctx.removeContainer(sessionRef.containerName);
|
|
3339
|
+
appendWebSessionMessage(state.webHistoryDir, sessionRef, 'system', `容器 ${sessionRef.containerName} 已删除。`);
|
|
2974
3340
|
}
|
|
2975
3341
|
|
|
2976
|
-
sendJson(res, 200, { removed: true, name: containerName });
|
|
3342
|
+
sendJson(res, 200, { removed: true, name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId) });
|
|
2977
3343
|
}
|
|
2978
3344
|
},
|
|
2979
3345
|
{
|
|
2980
3346
|
method: 'POST',
|
|
2981
3347
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/remove-with-history$/),
|
|
2982
3348
|
handler: async match => {
|
|
2983
|
-
const
|
|
2984
|
-
if (!
|
|
3349
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3350
|
+
if (!sessionRef) {
|
|
2985
3351
|
return;
|
|
2986
3352
|
}
|
|
2987
3353
|
|
|
2988
|
-
|
|
2989
|
-
|
|
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
|
+
});
|
|
2990
3371
|
}
|
|
2991
3372
|
}
|
|
2992
3373
|
];
|
|
@@ -3204,9 +3585,13 @@ async function startWebServer(options) {
|
|
|
3204
3585
|
return;
|
|
3205
3586
|
}
|
|
3206
3587
|
|
|
3207
|
-
const
|
|
3208
|
-
if (!ctx.isValidContainerName(containerName)) {
|
|
3209
|
-
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}`);
|
|
3210
3595
|
return;
|
|
3211
3596
|
}
|
|
3212
3597
|
|
|
@@ -3220,11 +3605,11 @@ async function startWebServer(options) {
|
|
|
3220
3605
|
url.searchParams.get('rows')
|
|
3221
3606
|
);
|
|
3222
3607
|
|
|
3223
|
-
ensureWebContainer(ctx, state, containerName)
|
|
3608
|
+
ensureWebContainer(ctx, state, sessionRef.containerName)
|
|
3224
3609
|
.then(() => {
|
|
3225
3610
|
wsServer.handleUpgrade(req, socket, head, ws => {
|
|
3226
3611
|
wsServer.emit('connection', ws, req, {
|
|
3227
|
-
containerName,
|
|
3612
|
+
containerName: sessionRef.containerName,
|
|
3228
3613
|
cols,
|
|
3229
3614
|
rows
|
|
3230
3615
|
});
|