@xcanwin/manyoyo 5.7.2 → 5.7.4

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