@xcanwin/manyoyo 5.7.3 → 5.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/web/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, '');
@@ -902,6 +1053,48 @@ function prepareStructuredTraceEvents(agentProgram, payload, state) {
902
1053
  return [];
903
1054
  }
904
1055
 
1056
+ function extractContentDeltaFromPayload(agentProgram, payload) {
1057
+ if (!payload || typeof payload !== 'object') {
1058
+ return null;
1059
+ }
1060
+ if (agentProgram === 'claude') {
1061
+ if (pickFirstString(payload.type) !== 'assistant') {
1062
+ return null;
1063
+ }
1064
+ const message = toPlainObject(payload.message);
1065
+ const content = Array.isArray(message.content) ? message.content : [];
1066
+ const text = content
1067
+ .filter(item => item && typeof item === 'object' && item.type === 'text')
1068
+ .map(item => collectStructuredText(item))
1069
+ .filter(Boolean)
1070
+ .join('\n')
1071
+ .trim();
1072
+ if (!text) {
1073
+ return null;
1074
+ }
1075
+ return { text, reset: true };
1076
+ }
1077
+ if (agentProgram === 'gemini' || agentProgram === 'opencode') {
1078
+ const eventType = pickFirstString(payload.type);
1079
+ if (eventType !== 'message') {
1080
+ return null;
1081
+ }
1082
+ const role = pickFirstString(payload.role);
1083
+ if (role !== 'assistant') {
1084
+ return null;
1085
+ }
1086
+ const text = collectStructuredText(payload.content);
1087
+ if (!text) {
1088
+ return null;
1089
+ }
1090
+ if (payload.delta === true) {
1091
+ return { text, reset: false };
1092
+ }
1093
+ return { text, reset: true };
1094
+ }
1095
+ return null;
1096
+ }
1097
+
905
1098
  function prepareCodexTraceEvent(payload) {
906
1099
  if (!payload || typeof payload !== 'object') {
907
1100
  return null;
@@ -1126,27 +1319,28 @@ function prepareCodexTraceEvent(payload) {
1126
1319
  });
1127
1320
  }
1128
1321
 
1129
- async function prepareWebAgentExecution(ctx, state, containerName, prompt) {
1130
- const history = loadWebSessionHistory(state.webHistoryDir, containerName);
1322
+ async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
1323
+ const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
1324
+ const agentSession = getWebAgentSession(history, sessionRef.agentId, { create: true });
1131
1325
  const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
1132
1326
  if (normalizedTemplate !== history.agentPromptCommand) {
1133
1327
  history.agentPromptCommand = normalizedTemplate;
1134
- saveWebSessionHistory(state.webHistoryDir, containerName, history);
1328
+ saveWebSessionHistory(state.webHistoryDir, sessionRef.containerName, history);
1135
1329
  }
1136
1330
  if (!isAgentPromptCommandEnabled(history.agentPromptCommand)) {
1137
1331
  throw new Error('当前会话未配置 agentPromptCommand');
1138
1332
  }
1139
1333
 
1140
- await ensureWebContainer(ctx, state, containerName);
1334
+ await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
1141
1335
  const agentMeta = getAgentRuntimeMeta(history);
1142
- const hasPriorConversation = hasAgentConversationHistory(history);
1336
+ const hasPriorConversation = hasAgentConversationHistory(agentSession);
1143
1337
  let resumeAttempted = false;
1144
1338
  let resumeSucceeded = false;
1145
1339
  let resumeError = '';
1146
1340
 
1147
1341
  if (hasPriorConversation && agentMeta.resumeSupported && agentMeta.resumeCommand) {
1148
1342
  resumeAttempted = true;
1149
- const resumeResult = await execCommandInWebContainer(ctx, containerName, agentMeta.resumeCommand);
1343
+ const resumeResult = await execCommandInWebContainer(ctx, sessionRef.containerName, agentMeta.resumeCommand);
1150
1344
  if (resumeResult.exitCode === 0) {
1151
1345
  resumeSucceeded = true;
1152
1346
  } else {
@@ -1156,12 +1350,13 @@ async function prepareWebAgentExecution(ctx, state, containerName, prompt) {
1156
1350
 
1157
1351
  const effectivePrompt = resumeSucceeded
1158
1352
  ? prompt
1159
- : buildAgentPromptWithHistory(history, prompt);
1353
+ : buildAgentPromptWithHistory(agentSession, prompt);
1160
1354
  const command = buildWebAgentExecCommand(history.agentPromptCommand, effectivePrompt, agentMeta.agentProgram);
1161
1355
  const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
1162
1356
 
1163
1357
  return {
1164
1358
  history,
1359
+ agentSession,
1165
1360
  agentMeta,
1166
1361
  command,
1167
1362
  contextMode,
@@ -1171,8 +1366,8 @@ async function prepareWebAgentExecution(ctx, state, containerName, prompt) {
1171
1366
  };
1172
1367
  }
1173
1368
 
1174
- function finalizeWebAgentExecution(state, containerName, history, agentMeta, meta, result) {
1175
- appendWebSessionMessage(state.webHistoryDir, containerName, 'assistant', result.output, {
1369
+ function finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, meta, result) {
1370
+ appendWebSessionMessage(state.webHistoryDir, sessionRef, 'assistant', result.output, {
1176
1371
  exitCode: result.exitCode,
1177
1372
  mode: 'agent',
1178
1373
  contextMode: meta.contextMode,
@@ -1180,21 +1375,19 @@ function finalizeWebAgentExecution(state, containerName, history, agentMeta, met
1180
1375
  resumeSucceeded: meta.resumeSucceeded,
1181
1376
  interrupted: result.interrupted === true
1182
1377
  });
1183
- 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 || ''
1378
+ patchWebAgentSessionState(state.webHistoryDir, sessionRef, {
1379
+ lastResumeAt: meta.resumeAttempted ? new Date().toISOString() : (agentSession.lastResumeAt || null),
1380
+ lastResumeOk: meta.resumeAttempted ? meta.resumeSucceeded : agentSession.lastResumeOk,
1381
+ lastResumeError: meta.resumeAttempted ? (meta.resumeSucceeded ? '' : meta.resumeError) : (agentSession.lastResumeError || '')
1189
1382
  });
1190
1383
  }
1191
1384
 
1192
- function appendWebAgentTraceMessage(webHistoryDir, containerName, content, extra = {}) {
1385
+ function appendWebAgentTraceMessage(webHistoryDir, sessionRefOrContainerName, content, extra = {}) {
1193
1386
  const text = String(content || '').trim();
1194
1387
  if (!text) {
1195
1388
  return;
1196
1389
  }
1197
- appendWebSessionMessage(webHistoryDir, containerName, 'assistant', text, {
1390
+ appendWebSessionMessage(webHistoryDir, sessionRefOrContainerName, 'assistant', text, {
1198
1391
  mode: 'agent',
1199
1392
  streamTrace: true,
1200
1393
  ...extra
@@ -1982,7 +2175,7 @@ function listWebManyoyoContainers(ctx) {
1982
2175
  return map;
1983
2176
  }
1984
2177
 
1985
- async function ensureWebContainer(ctx, state, containerInput) {
2178
+ async function ensureWebContainer(ctx, state, containerInput, messageSessionRef = null) {
1986
2179
  const runtime = typeof containerInput === 'string'
1987
2180
  ? buildStaticContainerRuntime(ctx, containerInput)
1988
2181
  : containerInput;
@@ -2014,14 +2207,24 @@ async function ensureWebContainer(ctx, state, containerInput) {
2014
2207
  }
2015
2208
 
2016
2209
  await ctx.waitForContainerReady(runtime.containerName);
2017
- appendWebSessionMessage(state.webHistoryDir, runtime.containerName, 'system', `容器 ${runtime.containerName} 已创建并启动。`);
2210
+ appendWebSessionMessage(
2211
+ state.webHistoryDir,
2212
+ messageSessionRef || runtime.containerName,
2213
+ 'system',
2214
+ `容器 ${runtime.containerName} 已创建并启动。`
2215
+ );
2018
2216
  return;
2019
2217
  }
2020
2218
 
2021
2219
  const status = ctx.getContainerStatus(runtime.containerName);
2022
2220
  if (status !== 'running') {
2023
2221
  ctx.dockerExecArgs(['start', runtime.containerName], { stdio: 'pipe' });
2024
- appendWebSessionMessage(state.webHistoryDir, runtime.containerName, 'system', `容器 ${runtime.containerName} 已启动。`);
2222
+ appendWebSessionMessage(
2223
+ state.webHistoryDir,
2224
+ messageSessionRef || runtime.containerName,
2225
+ 'system',
2226
+ `容器 ${runtime.containerName} 已启动。`
2227
+ );
2025
2228
  }
2026
2229
  }
2027
2230
 
@@ -2085,24 +2288,29 @@ async function execCommandInWebContainer(ctx, containerName, command, options =
2085
2288
  });
2086
2289
  }
2087
2290
 
2088
- async function execAgentInWebContainerStream(ctx, state, containerName, command, options = {}) {
2291
+ async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerName, command, options = {}) {
2089
2292
  const opts = options && typeof options === 'object' ? options : {};
2293
+ const sessionRef = typeof sessionRefOrContainerName === 'string'
2294
+ ? { containerName: sessionRefOrContainerName, agentId: WEB_DEFAULT_AGENT_ID }
2295
+ : sessionRefOrContainerName;
2296
+ const sessionKey = buildWebSessionKey(sessionRef.containerName, sessionRef.agentId);
2090
2297
  const agentProgram = typeof opts.agentProgram === 'string' ? opts.agentProgram : '';
2091
2298
  const onEvent = typeof opts.onEvent === 'function' ? opts.onEvent : () => {};
2092
2299
  const process = spawn(
2093
2300
  ctx.dockerCmd,
2094
- ['exec', containerName, '/bin/bash', '-lc', command],
2301
+ ['exec', sessionRef.containerName, '/bin/bash', '-lc', command],
2095
2302
  { stdio: ['ignore', 'pipe', 'pipe'] }
2096
2303
  );
2097
2304
 
2098
2305
  const runState = {
2099
- containerName,
2306
+ containerName: sessionRef.containerName,
2307
+ sessionKey,
2100
2308
  process,
2101
2309
  command,
2102
2310
  startedAt: new Date().toISOString(),
2103
2311
  stopping: false
2104
2312
  };
2105
- state.agentRuns.set(containerName, runState);
2313
+ state.agentRuns.set(sessionRef.containerName, runState);
2106
2314
 
2107
2315
  return await new Promise((resolve, reject) => {
2108
2316
  const MAX_RAW_OUTPUT_CHARS = 32 * 1024 * 1024;
@@ -2115,6 +2323,7 @@ async function execAgentInWebContainerStream(ctx, state, containerName, command,
2115
2323
  const structuredTraceState = {
2116
2324
  toolNamesById: new Map()
2117
2325
  };
2326
+ let contentDeltaAccumulator = '';
2118
2327
  function appendChunk(chunk, target) {
2119
2328
  if (!chunk) return;
2120
2329
  const text = chunk.toString('utf-8');
@@ -2152,6 +2361,18 @@ async function execAgentInWebContainerStream(ctx, state, containerName, command,
2152
2361
  traceEvent
2153
2362
  });
2154
2363
  });
2364
+ const deltaContent = extractContentDeltaFromPayload(agentProgram, payload, structuredTraceState);
2365
+ if (deltaContent !== null) {
2366
+ if (deltaContent.reset) {
2367
+ contentDeltaAccumulator = deltaContent.text;
2368
+ } else {
2369
+ contentDeltaAccumulator += deltaContent.text;
2370
+ }
2371
+ onEvent({
2372
+ type: 'content_delta',
2373
+ content: contentDeltaAccumulator
2374
+ });
2375
+ }
2155
2376
  return;
2156
2377
  }
2157
2378
  if (agentProgram === 'codex' && (/^OpenAI Codex\b/.test(rawLine) || /^tokens used\b/i.test(rawLine))) {
@@ -2201,11 +2422,11 @@ async function execAgentInWebContainerStream(ctx, state, containerName, command,
2201
2422
  });
2202
2423
 
2203
2424
  process.on('error', error => {
2204
- state.agentRuns.delete(containerName);
2425
+ state.agentRuns.delete(sessionRef.containerName);
2205
2426
  reject(error);
2206
2427
  });
2207
2428
  process.on('close', code => {
2208
- state.agentRuns.delete(containerName);
2429
+ state.agentRuns.delete(sessionRef.containerName);
2209
2430
  if (stdoutPending) {
2210
2431
  emitStdoutTraceLine(stdoutPending);
2211
2432
  stdoutPending = '';
@@ -2310,30 +2531,51 @@ function decodeSessionName(encoded) {
2310
2531
  }
2311
2532
  }
2312
2533
 
2313
- function getValidSessionName(ctx, res, encodedName) {
2314
- const containerName = decodeSessionName(encodedName);
2315
- if (!ctx.isValidContainerName(containerName)) {
2316
- sendJson(res, 400, { error: `containerName 非法: ${containerName}` });
2534
+ function getValidSessionRef(ctx, res, encodedName) {
2535
+ const parsed = parseWebSessionKey(decodeSessionName(encodedName));
2536
+ if (!ctx.isValidContainerName(parsed.containerName)) {
2537
+ sendJson(res, 400, { error: `containerName 非法: ${parsed.containerName}` });
2538
+ return null;
2539
+ }
2540
+ if (!SAFE_CONTAINER_NAME_PATTERN.test(parsed.agentId)) {
2541
+ sendJson(res, 400, { error: `agentId 非法: ${parsed.agentId}` });
2317
2542
  return null;
2318
2543
  }
2319
- return containerName;
2544
+ return parsed;
2320
2545
  }
2321
2546
 
2322
- function buildSessionSummary(ctx, state, containerMap, name) {
2323
- const history = loadWebSessionHistory(state.webHistoryDir, name);
2547
+ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
2548
+ const containerName = sessionRef && sessionRef.containerName ? sessionRef.containerName : '';
2549
+ const agentId = sessionRef && sessionRef.agentId ? sessionRef.agentId : WEB_DEFAULT_AGENT_ID;
2550
+ const history = loadWebSessionHistory(state.webHistoryDir, containerName);
2324
2551
  const agentMeta = getAgentRuntimeMeta(history);
2325
- const 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;
2552
+ const agentSession = getWebAgentSession(history, agentId)
2553
+ || (agentId === WEB_DEFAULT_AGENT_ID ? createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID) : null);
2554
+ if (!agentSession) {
2555
+ return null;
2556
+ }
2557
+ const latestMessage = agentSession.messages.length ? agentSession.messages[agentSession.messages.length - 1] : null;
2558
+ const containerInfo = containerMap[containerName] || {};
2559
+ const applied = history.applied && typeof history.applied === 'object' && !Array.isArray(history.applied)
2560
+ ? history.applied
2561
+ : buildSessionFallbackApplied(ctx, state, containerName, history, {
2562
+ status: containerInfo.status || 'history'
2563
+ });
2564
+ const updatedAt = agentSession.updatedAt || history.updatedAt || (latestMessage && latestMessage.timestamp) || containerInfo.createdAt || null;
2328
2565
  return {
2329
- name,
2566
+ name: buildWebSessionKey(containerName, agentId),
2567
+ containerName,
2568
+ agentId,
2569
+ agentName: agentSession.agentName,
2330
2570
  status: containerInfo.status || 'history',
2331
2571
  image: containerInfo.image || '',
2332
2572
  updatedAt,
2333
- messageCount: history.messages.length,
2573
+ messageCount: agentSession.messages.length,
2334
2574
  agentEnabled: isAgentPromptCommandEnabled(history.agentPromptCommand),
2335
2575
  agentProgram: agentMeta.agentProgram,
2336
- resumeSupported: agentMeta.resumeSupported
2576
+ resumeSupported: agentMeta.resumeSupported,
2577
+ hostPath: applied.hostPath || '',
2578
+ containerPath: applied.containerPath || ''
2337
2579
  };
2338
2580
  }
2339
2581
 
@@ -2341,9 +2583,8 @@ function buildSessionFallbackApplied(ctx, state, name, history, summary) {
2341
2583
  const snapshot = readWebConfigSnapshot(state.webConfigPath);
2342
2584
  const defaults = buildConfigDefaults(ctx, snapshot.parseError ? {} : snapshot.parsed);
2343
2585
  const effectiveAgentPromptCommand = history.agentPromptCommand || defaults.agentPromptCommand || '';
2344
- const effectiveAgentProgram = history.agentProgram || resolveAgentProgram(effectiveAgentPromptCommand) || '';
2345
- const effectiveResumeSupported = history.resumeSupported === true
2346
- || Boolean(buildAgentResumeCommand(effectiveAgentProgram));
2586
+ const effectiveAgentProgram = resolveAgentProgram(effectiveAgentPromptCommand) || '';
2587
+ const effectiveResumeSupported = Boolean(buildAgentResumeCommand(effectiveAgentProgram));
2347
2588
  const defaultCommand = buildDefaultCommand(
2348
2589
  defaults.shellPrefix,
2349
2590
  defaults.shell,
@@ -2373,24 +2614,34 @@ function buildSessionFallbackApplied(ctx, state, name, history, summary) {
2373
2614
  }
2374
2615
 
2375
2616
  function buildSessionDetail(ctx, state, containerMap, name) {
2376
- const history = loadWebSessionHistory(state.webHistoryDir, name);
2617
+ const sessionRef = typeof name === 'string' ? parseWebSessionKey(name) : name;
2618
+ const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
2377
2619
  const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
2378
- const summary = buildSessionSummary(ctx, state, containerMap, name);
2379
- const latestMessage = history.messages.length ? history.messages[history.messages.length - 1] : null;
2620
+ const summary = buildSessionSummary(ctx, state, containerMap, sessionRef);
2621
+ const agentSession = getWebAgentSession(history, sessionRef.agentId)
2622
+ || (sessionRef.agentId === WEB_DEFAULT_AGENT_ID ? createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID) : null);
2623
+ const latestMessage = agentSession && agentSession.messages.length
2624
+ ? agentSession.messages[agentSession.messages.length - 1]
2625
+ : null;
2380
2626
  const applied = history.applied && typeof history.applied === 'object' && !Array.isArray(history.applied)
2381
2627
  ? history.applied
2382
- : buildSessionFallbackApplied(ctx, state, name, history, summary);
2628
+ : buildSessionFallbackApplied(ctx, state, sessionRef.containerName, history, summary || {});
2629
+
2630
+ if (!summary || !agentSession) {
2631
+ return null;
2632
+ }
2383
2633
 
2384
2634
  return {
2385
2635
  ...summary,
2636
+ agentName: agentSession.agentName,
2386
2637
  latestRole: latestMessage && latestMessage.role ? String(latestMessage.role) : '',
2387
2638
  latestTimestamp: latestMessage && latestMessage.timestamp ? latestMessage.timestamp : summary.updatedAt,
2388
2639
  agentPromptCommand: normalizedTemplate || '',
2389
- agentProgram: 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 || '',
2640
+ agentProgram: summary.agentProgram || '',
2641
+ resumeSupported: summary.resumeSupported === true,
2642
+ lastResumeAt: agentSession.lastResumeAt || null,
2643
+ lastResumeOk: typeof agentSession.lastResumeOk === 'boolean' ? agentSession.lastResumeOk : null,
2644
+ lastResumeError: agentSession.lastResumeError || '',
2394
2645
  applied
2395
2646
  };
2396
2647
  }
@@ -2739,6 +2990,52 @@ async function handleWebApi(req, res, pathname, ctx, state) {
2739
2990
  }
2740
2991
  }
2741
2992
  const routes = [
2993
+ {
2994
+ method: 'GET',
2995
+ match: currentPath => currentPath === '/api/fs/directories' ? [] : null,
2996
+ handler: async () => {
2997
+ const requestUrl = new URL(req.url || '/api/fs/directories', 'http://localhost');
2998
+ const requestedPath = expandHomeAliasPath(String(requestUrl.searchParams.get('path') || '').trim() || os.homedir());
2999
+ const requestedBasePath = expandHomeAliasPath(String(requestUrl.searchParams.get('basePath') || '').trim());
3000
+ const realPath = fs.realpathSync(requestedPath);
3001
+ if (!fs.statSync(realPath).isDirectory()) {
3002
+ sendJson(res, 400, { error: `目录不存在: ${realPath}` });
3003
+ return;
3004
+ }
3005
+
3006
+ let realBasePath = '';
3007
+ if (requestedBasePath) {
3008
+ realBasePath = fs.realpathSync(requestedBasePath);
3009
+ if (!fs.statSync(realBasePath).isDirectory()) {
3010
+ sendJson(res, 400, { error: `basePath 不是目录: ${realBasePath}` });
3011
+ return;
3012
+ }
3013
+ const relativeToBase = path.relative(realBasePath, realPath);
3014
+ if (relativeToBase.startsWith('..') || path.isAbsolute(relativeToBase)) {
3015
+ sendJson(res, 400, { error: '目录超出 basePath 范围' });
3016
+ return;
3017
+ }
3018
+ }
3019
+
3020
+ const parentPath = realBasePath
3021
+ ? (realPath === realBasePath ? '' : path.dirname(realPath))
3022
+ : (realPath === path.parse(realPath).root ? '' : path.dirname(realPath));
3023
+ const entries = fs.readdirSync(realPath, { withFileTypes: true })
3024
+ .filter(entry => entry && entry.isDirectory())
3025
+ .map(entry => ({
3026
+ name: entry.name,
3027
+ path: path.join(realPath, entry.name)
3028
+ }))
3029
+ .sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
3030
+
3031
+ sendJson(res, 200, {
3032
+ currentPath: realPath,
3033
+ basePath: realBasePath || '',
3034
+ parentPath,
3035
+ entries
3036
+ });
3037
+ }
3038
+ },
2742
3039
  {
2743
3040
  method: 'GET',
2744
3041
  match: currentPath => currentPath === '/api/config' ? [] : null,
@@ -2788,7 +3085,15 @@ async function handleWebApi(req, res, pathname, ctx, state) {
2788
3085
  ]);
2789
3086
 
2790
3087
  const sessions = Array.from(names)
2791
- .map(name => buildSessionSummary(ctx, state, containerMap, name))
3088
+ .flatMap(name => {
3089
+ const history = loadWebSessionHistory(state.webHistoryDir, name);
3090
+ return listWebAgentSessions(history, { includeSyntheticDefault: true })
3091
+ .map(agentSession => buildSessionSummary(ctx, state, containerMap, {
3092
+ containerName: name,
3093
+ agentId: agentSession.agentId
3094
+ }))
3095
+ .filter(Boolean);
3096
+ })
2792
3097
  .sort((a, b) => {
2793
3098
  const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
2794
3099
  const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
@@ -2813,44 +3118,70 @@ async function handleWebApi(req, res, pathname, ctx, state) {
2813
3118
 
2814
3119
  await ensureWebContainer(ctx, state, runtime);
2815
3120
  setWebSessionAgentPromptCommand(state.webHistoryDir, runtime.containerName, runtime.agentPromptCommand);
2816
- patchWebSessionAgentState(state.webHistoryDir, runtime.containerName, {
3121
+ patchWebSessionHistory(state.webHistoryDir, runtime.containerName, {
2817
3122
  applied: runtime.applied
2818
3123
  });
2819
3124
  sendJson(res, 200, { name: runtime.containerName, applied: runtime.applied });
2820
3125
  }
2821
3126
  },
3127
+ {
3128
+ method: 'POST',
3129
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agents$/),
3130
+ handler: async match => {
3131
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
3132
+ if (!sessionRef) {
3133
+ return;
3134
+ }
3135
+ const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
3136
+ const agentSession = createWebAgentSession(history);
3137
+ saveWebSessionHistory(state.webHistoryDir, sessionRef.containerName, history);
3138
+ sendJson(res, 200, {
3139
+ name: buildWebSessionKey(sessionRef.containerName, agentSession.agentId),
3140
+ containerName: sessionRef.containerName,
3141
+ agentId: agentSession.agentId,
3142
+ agentName: agentSession.agentName
3143
+ });
3144
+ }
3145
+ },
2822
3146
  {
2823
3147
  method: 'GET',
2824
3148
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/messages$/),
2825
3149
  handler: async match => {
2826
- const containerName = getValidSessionName(ctx, res, match[1]);
2827
- if (!containerName) {
3150
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
3151
+ if (!sessionRef) {
2828
3152
  return;
2829
3153
  }
2830
- const history = loadWebSessionHistory(state.webHistoryDir, containerName);
2831
- sendJson(res, 200, { name: containerName, messages: history.messages });
3154
+ const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
3155
+ const agentSession = getWebAgentSession(history, sessionRef.agentId)
3156
+ || createEmptyWebAgentSession(sessionRef.agentId);
3157
+ sendJson(res, 200, {
3158
+ name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
3159
+ containerName: sessionRef.containerName,
3160
+ agentId: sessionRef.agentId,
3161
+ messages: agentSession.messages
3162
+ });
2832
3163
  }
2833
3164
  },
2834
3165
  {
2835
3166
  method: 'GET',
2836
3167
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/detail$/),
2837
3168
  handler: async match => {
2838
- const containerName = getValidSessionName(ctx, res, match[1]);
2839
- if (!containerName) {
3169
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
3170
+ if (!sessionRef) {
2840
3171
  return;
2841
3172
  }
2842
3173
 
2843
3174
  const containerMap = listWebManyoyoContainers(ctx);
2844
- const detail = buildSessionDetail(ctx, state, containerMap, containerName);
2845
- sendJson(res, 200, { name: containerName, detail });
3175
+ const detail = buildSessionDetail(ctx, state, containerMap, sessionRef);
3176
+ sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), detail });
2846
3177
  }
2847
3178
  },
2848
3179
  {
2849
3180
  method: 'POST',
2850
3181
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/run$/),
2851
3182
  handler: async match => {
2852
- const containerName = getValidSessionName(ctx, res, match[1]);
2853
- if (!containerName) {
3183
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
3184
+ if (!sessionRef) {
2854
3185
  return;
2855
3186
  }
2856
3187
 
@@ -2861,12 +3192,12 @@ async function handleWebApi(req, res, pathname, ctx, state) {
2861
3192
  return;
2862
3193
  }
2863
3194
 
2864
- await ensureWebContainer(ctx, state, containerName);
2865
- appendWebSessionMessage(state.webHistoryDir, containerName, 'user', command);
2866
- const result = await execCommandInWebContainer(ctx, containerName, command);
3195
+ await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
3196
+ appendWebSessionMessage(state.webHistoryDir, sessionRef, 'user', command);
3197
+ const result = await execCommandInWebContainer(ctx, sessionRef.containerName, command);
2867
3198
  appendWebSessionMessage(
2868
3199
  state.webHistoryDir,
2869
- containerName,
3200
+ sessionRef,
2870
3201
  'assistant',
2871
3202
  result.output,
2872
3203
  { exitCode: result.exitCode }
@@ -2878,8 +3209,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
2878
3209
  method: 'POST',
2879
3210
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent$/),
2880
3211
  handler: async match => {
2881
- const containerName = getValidSessionName(ctx, res, match[1]);
2882
- if (!containerName) {
3212
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
3213
+ if (!sessionRef) {
2883
3214
  return;
2884
3215
  }
2885
3216
 
@@ -2892,21 +3223,21 @@ async function handleWebApi(req, res, pathname, ctx, state) {
2892
3223
 
2893
3224
  let prepared = null;
2894
3225
  try {
2895
- prepared = await prepareWebAgentExecution(ctx, state, containerName, prompt);
3226
+ prepared = await prepareWebAgentExecution(ctx, state, sessionRef, prompt);
2896
3227
  } catch (e) {
2897
3228
  sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
2898
3229
  return;
2899
3230
  }
2900
3231
 
2901
- const { history, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
2902
- appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, {
3232
+ const { agentSession, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
3233
+ appendWebSessionMessage(state.webHistoryDir, sessionRef, 'user', prompt, {
2903
3234
  mode: 'agent',
2904
3235
  contextMode
2905
3236
  });
2906
- const result = await execCommandInWebContainer(ctx, containerName, command, {
3237
+ const result = await execCommandInWebContainer(ctx, sessionRef.containerName, command, {
2907
3238
  agentProgram: agentMeta.agentProgram
2908
3239
  });
2909
- finalizeWebAgentExecution(state, containerName, history, agentMeta, {
3240
+ finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, {
2910
3241
  contextMode,
2911
3242
  resumeAttempted,
2912
3243
  resumeSucceeded,
@@ -2926,8 +3257,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
2926
3257
  method: 'POST',
2927
3258
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent\/stream$/),
2928
3259
  handler: async match => {
2929
- const containerName = getValidSessionName(ctx, res, match[1]);
2930
- if (!containerName) {
3260
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
3261
+ if (!sessionRef) {
2931
3262
  return;
2932
3263
  }
2933
3264
 
@@ -2937,23 +3268,23 @@ async function handleWebApi(req, res, pathname, ctx, state) {
2937
3268
  sendJson(res, 400, { error: 'prompt 不能为空' });
2938
3269
  return;
2939
3270
  }
2940
- if (state.agentRuns.has(containerName)) {
3271
+ if (state.agentRuns.has(sessionRef.containerName)) {
2941
3272
  sendJson(res, 409, { error: '当前会话已有运行中的 agent 任务' });
2942
3273
  return;
2943
3274
  }
2944
3275
 
2945
3276
  let prepared = null;
2946
3277
  try {
2947
- prepared = await prepareWebAgentExecution(ctx, state, containerName, prompt);
3278
+ prepared = await prepareWebAgentExecution(ctx, state, sessionRef, prompt);
2948
3279
  } catch (e) {
2949
3280
  sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
2950
3281
  return;
2951
3282
  }
2952
3283
 
2953
- const { history, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
3284
+ const { agentSession, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
2954
3285
  const traceLines = ['[执行过程]'];
2955
3286
  const traceEvents = [];
2956
- appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, {
3287
+ appendWebSessionMessage(state.webHistoryDir, sessionRef, 'user', prompt, {
2957
3288
  mode: 'agent',
2958
3289
  contextMode
2959
3290
  });
@@ -2965,7 +3296,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
2965
3296
  });
2966
3297
  sendNdjson(res, {
2967
3298
  type: 'meta',
2968
- containerName,
3299
+ containerName: sessionRef.containerName,
3300
+ sessionName: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
2969
3301
  contextMode,
2970
3302
  resumeAttempted,
2971
3303
  resumeSucceeded,
@@ -2979,7 +3311,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
2979
3311
  }
2980
3312
 
2981
3313
  try {
2982
- const result = await execAgentInWebContainerStream(ctx, state, containerName, command, {
3314
+ const result = await execAgentInWebContainerStream(ctx, state, sessionRef, command, {
2983
3315
  agentProgram: agentMeta.agentProgram,
2984
3316
  onEvent: event => {
2985
3317
  if (event && event.type === 'trace' && event.text) {
@@ -2992,14 +3324,14 @@ async function handleWebApi(req, res, pathname, ctx, state) {
2992
3324
  }
2993
3325
  });
2994
3326
  traceLines.push(result.interrupted === true ? '[任务] 已停止' : '[任务] 已完成');
2995
- appendWebAgentTraceMessage(state.webHistoryDir, containerName, traceLines.join('\n'), {
3327
+ appendWebAgentTraceMessage(state.webHistoryDir, sessionRef, traceLines.join('\n'), {
2996
3328
  traceEvents,
2997
3329
  contextMode,
2998
3330
  resumeAttempted,
2999
3331
  resumeSucceeded,
3000
3332
  interrupted: result.interrupted === true
3001
3333
  });
3002
- finalizeWebAgentExecution(state, containerName, history, agentMeta, {
3334
+ finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, {
3003
3335
  contextMode,
3004
3336
  resumeAttempted,
3005
3337
  resumeSucceeded,
@@ -3016,7 +3348,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
3016
3348
  });
3017
3349
  } catch (e) {
3018
3350
  traceLines.push(`[错误] ${e && e.message ? e.message : 'Agent 执行失败'}`);
3019
- appendWebAgentTraceMessage(state.webHistoryDir, containerName, traceLines.join('\n'), {
3351
+ appendWebAgentTraceMessage(state.webHistoryDir, sessionRef, traceLines.join('\n'), {
3020
3352
  traceEvents,
3021
3353
  contextMode,
3022
3354
  resumeAttempted,
@@ -3036,11 +3368,11 @@ async function handleWebApi(req, res, pathname, ctx, state) {
3036
3368
  method: 'POST',
3037
3369
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent\/stop$/),
3038
3370
  handler: async match => {
3039
- const containerName = getValidSessionName(ctx, res, match[1]);
3040
- if (!containerName) {
3371
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
3372
+ if (!sessionRef) {
3041
3373
  return;
3042
3374
  }
3043
- const stopped = stopWebAgentRun(state, containerName);
3375
+ const stopped = stopWebAgentRun(state, sessionRef.containerName);
3044
3376
  if (!stopped) {
3045
3377
  sendJson(res, 404, { error: '当前会话没有运行中的 agent 任务' });
3046
3378
  return;
@@ -3052,30 +3384,45 @@ async function handleWebApi(req, res, pathname, ctx, state) {
3052
3384
  method: 'POST',
3053
3385
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/remove$/),
3054
3386
  handler: async match => {
3055
- const containerName = getValidSessionName(ctx, res, match[1]);
3056
- if (!containerName) {
3387
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
3388
+ if (!sessionRef) {
3057
3389
  return;
3058
3390
  }
3059
3391
 
3060
- if (ctx.containerExists(containerName)) {
3061
- ctx.removeContainer(containerName);
3062
- appendWebSessionMessage(state.webHistoryDir, containerName, 'system', `容器 ${containerName} 已删除。`);
3392
+ if (ctx.containerExists(sessionRef.containerName)) {
3393
+ ctx.removeContainer(sessionRef.containerName);
3394
+ appendWebSessionMessage(state.webHistoryDir, sessionRef, 'system', `容器 ${sessionRef.containerName} 已删除。`);
3063
3395
  }
3064
3396
 
3065
- sendJson(res, 200, { removed: true, name: containerName });
3397
+ sendJson(res, 200, { removed: true, name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId) });
3066
3398
  }
3067
3399
  },
3068
3400
  {
3069
3401
  method: 'POST',
3070
3402
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/remove-with-history$/),
3071
3403
  handler: async match => {
3072
- const containerName = getValidSessionName(ctx, res, match[1]);
3073
- if (!containerName) {
3404
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
3405
+ if (!sessionRef) {
3074
3406
  return;
3075
3407
  }
3076
3408
 
3077
- removeWebSessionHistory(state.webHistoryDir, containerName);
3078
- sendJson(res, 200, { removedHistory: true, name: containerName });
3409
+ const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
3410
+ if (history.agents && typeof history.agents === 'object') {
3411
+ if (sessionRef.agentId === WEB_DEFAULT_AGENT_ID) {
3412
+ delete history.agents[WEB_DEFAULT_AGENT_ID];
3413
+ } else {
3414
+ delete history.agents[sessionRef.agentId];
3415
+ }
3416
+ }
3417
+ if (!Object.keys(history.agents || {}).length && !ctx.containerExists(sessionRef.containerName)) {
3418
+ removeWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
3419
+ } else {
3420
+ saveWebSessionHistory(state.webHistoryDir, sessionRef.containerName, history);
3421
+ }
3422
+ sendJson(res, 200, {
3423
+ removedHistory: true,
3424
+ name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId)
3425
+ });
3079
3426
  }
3080
3427
  }
3081
3428
  ];
@@ -3293,9 +3640,13 @@ async function startWebServer(options) {
3293
3640
  return;
3294
3641
  }
3295
3642
 
3296
- const containerName = decodeSessionName(terminalMatch[1]);
3297
- if (!ctx.isValidContainerName(containerName)) {
3298
- sendWebSocketUpgradeError(socket, 400, `containerName 非法: ${containerName}`);
3643
+ const sessionRef = parseWebSessionKey(decodeSessionName(terminalMatch[1]));
3644
+ if (!ctx.isValidContainerName(sessionRef.containerName)) {
3645
+ sendWebSocketUpgradeError(socket, 400, `containerName 非法: ${sessionRef.containerName}`);
3646
+ return;
3647
+ }
3648
+ if (!SAFE_CONTAINER_NAME_PATTERN.test(sessionRef.agentId)) {
3649
+ sendWebSocketUpgradeError(socket, 400, `agentId 非法: ${sessionRef.agentId}`);
3299
3650
  return;
3300
3651
  }
3301
3652
 
@@ -3309,11 +3660,11 @@ async function startWebServer(options) {
3309
3660
  url.searchParams.get('rows')
3310
3661
  );
3311
3662
 
3312
- ensureWebContainer(ctx, state, containerName)
3663
+ ensureWebContainer(ctx, state, sessionRef.containerName)
3313
3664
  .then(() => {
3314
3665
  wsServer.handleUpgrade(req, socket, head, ws => {
3315
3666
  wsServer.emit('connection', ws, req, {
3316
- containerName,
3667
+ containerName: sessionRef.containerName,
3317
3668
  cols,
3318
3669
  rows
3319
3670
  });