@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/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
- const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
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
- fs.writeFileSync(filePath, JSON.stringify(history, null, 4));
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 appendWebSessionMessage(webHistoryDir, containerName, role, content, extra = {}) {
193
- const history = loadWebSessionHistory(webHistoryDir, containerName);
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
- history.messages.push({
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 (history.messages.length > WEB_HISTORY_MAX_MESSAGES) {
204
- history.messages = history.messages.slice(-WEB_HISTORY_MAX_MESSAGES);
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 patchWebSessionAgentState(webHistoryDir, containerName, patch) {
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, containerName, prompt) {
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(history);
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(history, prompt);
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, containerName, history, agentMeta, meta, result) {
1175
- appendWebSessionMessage(state.webHistoryDir, containerName, 'assistant', result.output, {
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
- patchWebSessionAgentState(state.webHistoryDir, containerName, {
1184
- agentProgram: agentMeta.agentProgram,
1185
- resumeSupported: agentMeta.resumeSupported,
1186
- lastResumeAt: meta.resumeAttempted ? new Date().toISOString() : history.lastResumeAt || null,
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, containerName, content, extra = {}) {
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, containerName, 'assistant', text, {
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(state.webHistoryDir, runtime.containerName, 'system', `容器 ${runtime.containerName} 已创建并启动。`);
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(state.webHistoryDir, runtime.containerName, 'system', `容器 ${runtime.containerName} 已启动。`);
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, containerName, command, options = {}) {
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 getValidSessionName(ctx, res, encodedName) {
2314
- const containerName = decodeSessionName(encodedName);
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 containerName;
2489
+ return parsed;
2320
2490
  }
2321
2491
 
2322
- function buildSessionSummary(ctx, state, containerMap, name) {
2323
- const history = loadWebSessionHistory(state.webHistoryDir, name);
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 latestMessage = history.messages.length ? history.messages[history.messages.length - 1] : null;
2326
- const containerInfo = containerMap[name] || {};
2327
- const updatedAt = history.updatedAt || (latestMessage && latestMessage.timestamp) || containerInfo.createdAt || null;
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: history.messages.length,
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 = history.agentProgram || resolveAgentProgram(effectiveAgentPromptCommand) || '';
2345
- const effectiveResumeSupported = history.resumeSupported === true
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 history = loadWebSessionHistory(state.webHistoryDir, name);
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, name);
2379
- const latestMessage = history.messages.length ? history.messages[history.messages.length - 1] : null;
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, name, history, summary);
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: history.agentProgram || summary.agentProgram || '',
2390
- resumeSupported: history.resumeSupported === true || summary.resumeSupported === true,
2391
- lastResumeAt: history.lastResumeAt || null,
2392
- lastResumeOk: typeof history.lastResumeOk === 'boolean' ? history.lastResumeOk : null,
2393
- lastResumeError: history.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
- .map(name => buildSessionSummary(ctx, state, containerMap, name))
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
- patchWebSessionAgentState(state.webHistoryDir, runtime.containerName, {
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 containerName = getValidSessionName(ctx, res, match[1]);
2827
- if (!containerName) {
3095
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
3096
+ if (!sessionRef) {
2828
3097
  return;
2829
3098
  }
2830
- const history = loadWebSessionHistory(state.webHistoryDir, containerName);
2831
- sendJson(res, 200, { name: containerName, messages: history.messages });
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 containerName = getValidSessionName(ctx, res, match[1]);
2839
- if (!containerName) {
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, containerName);
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 containerName = getValidSessionName(ctx, res, match[1]);
2853
- if (!containerName) {
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, containerName, 'user', command);
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
- containerName,
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 containerName = getValidSessionName(ctx, res, match[1]);
2882
- if (!containerName) {
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, containerName, prompt);
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 { history, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
2902
- appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, {
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, containerName, history, agentMeta, {
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 containerName = getValidSessionName(ctx, res, match[1]);
2930
- if (!containerName) {
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, containerName, prompt);
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 { history, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
3229
+ const { agentSession, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
2954
3230
  const traceLines = ['[执行过程]'];
2955
3231
  const traceEvents = [];
2956
- appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, {
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, containerName, command, {
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, containerName, traceLines.join('\n'), {
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, containerName, history, agentMeta, {
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, containerName, traceLines.join('\n'), {
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 containerName = getValidSessionName(ctx, res, match[1]);
3040
- if (!containerName) {
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 containerName = getValidSessionName(ctx, res, match[1]);
3056
- if (!containerName) {
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, containerName, 'system', `容器 ${containerName} 已删除。`);
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 containerName = getValidSessionName(ctx, res, match[1]);
3073
- if (!containerName) {
3349
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
3350
+ if (!sessionRef) {
3074
3351
  return;
3075
3352
  }
3076
3353
 
3077
- removeWebSessionHistory(state.webHistoryDir, containerName);
3078
- sendJson(res, 200, { removedHistory: true, name: containerName });
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 containerName = decodeSessionName(terminalMatch[1]);
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
  });