@xcanwin/manyoyo 7.0.13 → 7.0.16

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
@@ -30,6 +30,82 @@ const {
30
30
  projectSessionEvents
31
31
  } = require('../core/events');
32
32
  const { FileEventStore } = require('../core/event-store');
33
+ const { buildManyoyoLogPath, getLocalDateTag } = require('../log-path'); // DIAG_LOG
34
+
35
+ // DIAG_LOG BEGIN:临时详细诊断日志基础设施,用于排查 serve 接口超时 / agent
36
+ // 流式回复长时间卡在"…"不实时更新的问题。排查结束后可整体删除:全局搜索
37
+ // "DIAG_LOG" 能定位到全部相关代码(这段辅助函数、各处打点调用、下方
38
+ // /api/diag/logs 接口、/logs 页面路由),一并删掉不影响其它功能。
39
+ const DIAG_LOG_LINE_PATTERN = /^\[([^\]]+)\] \[pid:(\d+)\] \[(\w+)\] \[DIAG_LOG\] (\S+)(?: ([\s\S]*))?$/;
40
+
41
+ function diagLog(ctx, category, extra = {}) {
42
+ if (!ctx || !ctx.logger || typeof ctx.logger.info !== 'function') {
43
+ return;
44
+ }
45
+ ctx.logger.info(`[DIAG_LOG] ${String(category || '')}`, extra);
46
+ }
47
+
48
+ function parseDiagLogLine(line) {
49
+ const matched = String(line || '').match(DIAG_LOG_LINE_PATTERN);
50
+ if (!matched) {
51
+ return null;
52
+ }
53
+ let extra = {};
54
+ if (matched[5]) {
55
+ try {
56
+ extra = JSON.parse(matched[5]);
57
+ } catch (e) {
58
+ extra = { raw: matched[5] };
59
+ }
60
+ }
61
+ return {
62
+ ts: matched[1],
63
+ pid: Number(matched[2]),
64
+ level: matched[3],
65
+ category: matched[4],
66
+ extra
67
+ };
68
+ }
69
+
70
+ function diagLogDir(ctx) {
71
+ if (ctx && ctx.logger && typeof ctx.logger.path === 'string' && ctx.logger.path) {
72
+ return path.dirname(ctx.logger.path);
73
+ }
74
+ return buildManyoyoLogPath('serve').dir;
75
+ }
76
+
77
+ function readDiagLogEntries(ctx, options = {}) {
78
+ const dateTag = options.date ? String(options.date) : getLocalDateTag();
79
+ const filePath = path.join(diagLogDir(ctx), `serve-${dateTag}.log`);
80
+ if (!fs.existsSync(filePath)) {
81
+ return [];
82
+ }
83
+ const limit = Number.isInteger(options.limit) && options.limit > 0 ? Math.min(options.limit, 2000) : 500;
84
+ const category = options.category ? String(options.category) : '';
85
+ const session = options.session ? String(options.session) : '';
86
+ const keyword = options.keyword ? String(options.keyword).toLowerCase() : '';
87
+
88
+ const lines = fs.readFileSync(filePath, 'utf-8').split('\n');
89
+ const entries = [];
90
+ for (const line of lines) {
91
+ const entry = parseDiagLogLine(line);
92
+ if (!entry) {
93
+ continue;
94
+ }
95
+ if (category && entry.category !== category) {
96
+ continue;
97
+ }
98
+ if (session && entry.extra.session !== session) {
99
+ continue;
100
+ }
101
+ if (keyword && !line.toLowerCase().includes(keyword)) {
102
+ continue;
103
+ }
104
+ entries.push(entry);
105
+ }
106
+ return entries.slice(-limit);
107
+ }
108
+ // DIAG_LOG END(下方还有零散打点调用与 /api/diag/logs、/logs 路由,同样标记为 DIAG_LOG)
33
109
 
34
110
  const WEB_HISTORY_MAX_MESSAGES = 500;
35
111
  const WEB_OUTPUT_MAX_CHARS = 16000;
@@ -2068,7 +2144,7 @@ async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
2068
2144
 
2069
2145
  function finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, meta, result) {
2070
2146
  const turnUsage = extractTurnUsage(agentMeta.agentProgram, result.stdout);
2071
- appendWebSessionMessage(state.webHistoryDir, sessionRef, 'assistant', result.output, {
2147
+ const replyMessage = appendWebSessionMessage(state.webHistoryDir, sessionRef, 'assistant', result.output, {
2072
2148
  exitCode: result.exitCode,
2073
2149
  mode: 'agent',
2074
2150
  contextMode: meta.contextMode,
@@ -2090,6 +2166,7 @@ function finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, m
2090
2166
  patch.usageTotal = accumulateUsageTotal(agentSession.usageTotal, turnUsage);
2091
2167
  }
2092
2168
  patchWebAgentSessionState(state.webHistoryDir, sessionRef, patch);
2169
+ return replyMessage;
2093
2170
  }
2094
2171
 
2095
2172
  function appendWebAgentTraceMessage(webHistoryDir, sessionRefOrContainerName, content, extra = {}) {
@@ -3088,6 +3165,7 @@ function listWebManyoyoContainers(ctx) {
3088
3165
  return map;
3089
3166
  }
3090
3167
 
3168
+ const candidates = [];
3091
3169
  output.trim().split('\n').forEach(line => {
3092
3170
  const [name, status, image] = line.split('\t');
3093
3171
  if (!ctx.isValidContainerName(name)) {
@@ -3097,23 +3175,46 @@ function listWebManyoyoContainers(ctx) {
3097
3175
  if (!imageName.includes('manyoyo') && !name.startsWith('manyoyo-') && !name.startsWith('my-')) {
3098
3176
  return;
3099
3177
  }
3100
- let defaultCommand = '';
3178
+ candidates.push({ name, status, imageName });
3179
+ });
3180
+
3181
+ // 逐容器单独 docker inspect 是 N+1 同步阻塞调用(spawnSync 无 timeout,会
3182
+ // 独占 Node 单线程事件循环);容器一多,serve 所有接口(包括正在流式输出
3183
+ // 的 agent/stream)都会被拖慢甚至冻结。这里合并成一次批量 inspect。
3184
+ const defaultCommandByName = {};
3185
+ if (candidates.length) {
3186
+ let inspectOutput = '';
3101
3187
  try {
3102
- defaultCommand = String(
3188
+ inspectOutput = String(
3103
3189
  ctx.dockerExecArgs(
3104
- ['inspect', '-f', '{{index .Config.Labels "manyoyo.default_cmd"}}', name],
3190
+ [
3191
+ 'inspect',
3192
+ '-f',
3193
+ '{{.Name}}\t{{index .Config.Labels "manyoyo.default_cmd"}}',
3194
+ ...candidates.map(c => c.name)
3195
+ ],
3105
3196
  { ignoreError: true }
3106
3197
  ) || ''
3107
- ).trim();
3198
+ );
3108
3199
  } catch (e) {
3109
- defaultCommand = '';
3200
+ inspectOutput = '';
3110
3201
  }
3202
+ inspectOutput.split('\n').forEach(line => {
3203
+ const [rawName, defaultCommand] = line.split('\t');
3204
+ if (!rawName) {
3205
+ return;
3206
+ }
3207
+ defaultCommandByName[rawName.replace(/^\//, '')] = (defaultCommand || '').trim();
3208
+ });
3209
+ }
3210
+
3211
+ candidates.forEach(({ name, status, imageName }) => {
3111
3212
  map[name] = {
3112
3213
  name,
3113
3214
  status: status || 'unknown',
3114
3215
  image: imageName,
3115
3216
  createdAt: estimateStartTimeFromStatus(status),
3116
- defaultCommand
3217
+ defaultCommand: defaultCommandByName[name] || ''
3117
3218
  };
3118
3219
  });
3119
3220
 
@@ -3799,11 +3900,37 @@ function stopWebAgentRun(state, containerName) {
3799
3900
  try {
3800
3901
  runState.process.kill('SIGTERM');
3801
3902
  } catch (e) {
3802
- return false;
3903
+ // 进程可能已自行退出,视为停止成功(close 事件会收尾)
3803
3904
  }
3804
3905
  return true;
3805
3906
  }
3806
3907
 
3908
+ // serve 重启等原因会丢失 agentRuns 运行登记,但消息里可能残留 pending 标记
3909
+ // (孤儿任务:容器里的进程或许还在跑,但再没有任何代码会去收尾这些消息)。
3910
+ // 前端把 pending 视为"任务进行中"会永久禁用输入,这里把它们标记为已中断
3911
+ function interruptOrphanPendingMessages(webHistoryDir, sessionRef) {
3912
+ const history = loadWebSessionHistory(webHistoryDir, sessionRef.containerName);
3913
+ const agentSession = getWebAgentSession(history, sessionRef.agentId, { includeArchived: true });
3914
+ if (!agentSession || !Array.isArray(agentSession.messages)) {
3915
+ return;
3916
+ }
3917
+ let touched = false;
3918
+ for (const message of agentSession.messages) {
3919
+ if (message && message.pending === true) {
3920
+ message.pending = false;
3921
+ message.interrupted = true;
3922
+ touched = true;
3923
+ }
3924
+ }
3925
+ if (!touched) {
3926
+ return;
3927
+ }
3928
+ const timestamp = new Date().toISOString();
3929
+ agentSession.updatedAt = timestamp;
3930
+ history.updatedAt = timestamp;
3931
+ saveWebSessionHistory(webHistoryDir, sessionRef.containerName, history);
3932
+ }
3933
+
3807
3934
  function sendHtml(res, statusCode, html, extraHeaders = {}) {
3808
3935
  res.writeHead(statusCode, {
3809
3936
  'Content-Type': 'text/html; charset=utf-8',
@@ -4117,6 +4244,123 @@ function renderLoginHtml(ctx) {
4117
4244
  return applyServeTitle(loadTemplate('login.html'), ctx);
4118
4245
  }
4119
4246
 
4247
+ // DIAG_LOG:临时诊断日志查看页,自成一个内联 HTML 字符串(不接入 frontend-shadcn
4248
+ // 构建流程),配合 /api/diag/logs 接口使用。排查结束后可整体删除本函数及其
4249
+ // 调用点(全局搜索 "DIAG_LOG")。
4250
+ function renderDiagLogsHtml() {
4251
+ return `<!DOCTYPE html>
4252
+ <html lang="zh-CN">
4253
+ <head>
4254
+ <meta charset="UTF-8">
4255
+ <title>MANYOYO 诊断日志</title>
4256
+ <style>
4257
+ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #0b0d12; color: #d7dae0; }
4258
+ header { padding: 12px 16px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; border-bottom: 1px solid #262b36; position: sticky; top: 0; background: #0b0d12; }
4259
+ header input, header select { background: #171a21; border: 1px solid #2c313d; color: #d7dae0; padding: 5px 8px; border-radius: 4px; font-size: 13px; }
4260
+ header button { background: #2b6cb0; border: none; color: #fff; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; }
4261
+ header button.secondary { background: #2c313d; }
4262
+ header label { font-size: 12px; display: flex; align-items: center; gap: 4px; }
4263
+ main { padding: 8px 16px 40px; }
4264
+ table { width: 100%; border-collapse: collapse; font-size: 12px; }
4265
+ th, td { text-align: left; padding: 4px 8px; border-bottom: 1px solid #1c202a; vertical-align: top; white-space: pre-wrap; word-break: break-all; }
4266
+ th { position: sticky; top: 53px; background: #0b0d12; color: #8b93a3; font-weight: 600; }
4267
+ tr:hover { background: #12151c; }
4268
+ .cat { display: inline-block; padding: 1px 6px; border-radius: 3px; background: #1e2530; color: #8fd3ff; }
4269
+ .status-ok { color: #7ee787; }
4270
+ .status-err { color: #ff7b72; }
4271
+ .dur-slow { color: #ffa657; font-weight: 600; }
4272
+ #count { color: #8b93a3; font-size: 12px; }
4273
+ </style>
4274
+ </head>
4275
+ <body>
4276
+ <header>
4277
+ <strong>诊断日志</strong>
4278
+ <label>日期 <input id="f-date" type="text" placeholder="YYYY-MM-DD"></label>
4279
+ <label>分类 <input id="f-category" type="text" placeholder="http_request / docker_exec / stream_event / stream_exec_start / stream_exec_end"></label>
4280
+ <label>会话 <input id="f-session" type="text" placeholder="containerName 或 containerName~agentId"></label>
4281
+ <label>关键字 <input id="f-keyword" type="text" placeholder="任意文本"></label>
4282
+ <label>条数 <input id="f-limit" type="text" value="500" style="width:60px"></label>
4283
+ <button id="btn-refresh">刷新</button>
4284
+ <label><input id="f-auto" type="checkbox"> 每 2 秒自动刷新</label>
4285
+ <span id="count"></span>
4286
+ </header>
4287
+ <main>
4288
+ <table>
4289
+ <thead><tr><th>时间</th><th>分类</th><th>详情</th></tr></thead>
4290
+ <tbody id="rows"></tbody>
4291
+ </table>
4292
+ </main>
4293
+ <script>
4294
+ (function () {
4295
+ var timer = null;
4296
+ function fmtExtra(entry) {
4297
+ var e = entry.extra || {};
4298
+ var parts = [];
4299
+ if (e.method) parts.push(e.method + ' ' + (e.path || ''));
4300
+ if (typeof e.status === 'number') {
4301
+ parts.push('<span class="' + (e.status < 400 ? 'status-ok' : 'status-err') + '">status=' + e.status + '</span>');
4302
+ }
4303
+ if (typeof e.durationMs === 'number') {
4304
+ parts.push('<span class="' + (e.durationMs > 1000 ? 'dur-slow' : '') + '">' + e.durationMs + 'ms</span>');
4305
+ }
4306
+ if (e.session) parts.push('session=' + e.session);
4307
+ if (e.args) parts.push('args=' + JSON.stringify(e.args));
4308
+ if (e.type) parts.push('type=' + e.type);
4309
+ if (typeof e.seq === 'number') parts.push('seq=' + e.seq);
4310
+ if (typeof e.sinceStartMs === 'number') parts.push('sinceStart=' + e.sinceStartMs + 'ms');
4311
+ if (typeof e.totalDurationMs === 'number') parts.push('total=' + e.totalDurationMs + 'ms');
4312
+ if (typeof e.eventCount === 'number') parts.push('events=' + e.eventCount);
4313
+ if (e.error) parts.push('<span class="status-err">error=' + e.error + '</span>');
4314
+ var rest = {};
4315
+ Object.keys(e).forEach(function (k) {
4316
+ if (['method','path','status','durationMs','session','args','type','seq','sinceStartMs','totalDurationMs','eventCount','error'].indexOf(k) === -1) {
4317
+ rest[k] = e[k];
4318
+ }
4319
+ });
4320
+ if (Object.keys(rest).length) parts.push(JSON.stringify(rest));
4321
+ return parts.join(' ');
4322
+ }
4323
+ function escapeHtml(s) {
4324
+ return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
4325
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
4326
+ });
4327
+ }
4328
+ function load() {
4329
+ var params = new URLSearchParams();
4330
+ var date = document.getElementById('f-date').value.trim();
4331
+ var category = document.getElementById('f-category').value.trim();
4332
+ var session = document.getElementById('f-session').value.trim();
4333
+ var keyword = document.getElementById('f-keyword').value.trim();
4334
+ var limit = document.getElementById('f-limit').value.trim();
4335
+ if (date) params.set('date', date);
4336
+ if (category) params.set('category', category);
4337
+ if (session) params.set('session', session);
4338
+ if (keyword) params.set('keyword', keyword);
4339
+ if (limit) params.set('limit', limit);
4340
+ fetch('/api/diag/logs?' + params.toString(), { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
4341
+ .then(function (r) { return r.json(); })
4342
+ .then(function (data) {
4343
+ var entries = data.entries || [];
4344
+ document.getElementById('count').textContent = entries.length + ' 条(' + data.date + ')';
4345
+ var rows = entries.slice().reverse().map(function (entry) {
4346
+ return '<tr><td>' + escapeHtml(entry.ts) + '</td><td><span class="cat">' + escapeHtml(entry.category) + '</span></td><td>' + fmtExtra(entry) + '</td></tr>';
4347
+ });
4348
+ document.getElementById('rows').innerHTML = rows.join('');
4349
+ });
4350
+ }
4351
+ document.getElementById('btn-refresh').addEventListener('click', load);
4352
+ document.getElementById('f-auto').addEventListener('change', function (e) {
4353
+ if (timer) { clearInterval(timer); timer = null; }
4354
+ if (e.target.checked) { timer = setInterval(load, 2000); }
4355
+ });
4356
+ load();
4357
+ })();
4358
+ </script>
4359
+ </body>
4360
+ </html>
4361
+ `;
4362
+ }
4363
+
4120
4364
  function toPositiveInt(value, fallback) {
4121
4365
  const parsed = Number.parseInt(value, 10);
4122
4366
  if (!Number.isFinite(parsed) || parsed <= 0) {
@@ -4410,6 +4654,23 @@ async function handleWebApi(req, res, pathname, ctx, state) {
4410
4654
  }
4411
4655
  }
4412
4656
  const routes = [
4657
+ // DIAG_LOG:临时诊断日志查询接口,配合 /logs 页面使用,排查完成后与
4658
+ // diagLog/readDiagLogEntries 一起整体删除即可(全局搜索 "DIAG_LOG")。
4659
+ {
4660
+ method: 'GET',
4661
+ match: currentPath => currentPath === '/api/diag/logs' ? [] : null,
4662
+ handler: async () => {
4663
+ const requestUrl = new URL(req.url || '/api/diag/logs', 'http://localhost');
4664
+ const entries = readDiagLogEntries(ctx, {
4665
+ date: requestUrl.searchParams.get('date') || undefined,
4666
+ category: requestUrl.searchParams.get('category') || undefined,
4667
+ session: requestUrl.searchParams.get('session') || undefined,
4668
+ keyword: requestUrl.searchParams.get('keyword') || undefined,
4669
+ limit: Number(requestUrl.searchParams.get('limit')) || undefined
4670
+ });
4671
+ sendJson(res, 200, { date: requestUrl.searchParams.get('date') || getLocalDateTag(), entries });
4672
+ }
4673
+ },
4413
4674
  {
4414
4675
  method: 'GET',
4415
4676
  match: currentPath => currentPath === '/api/fs/directories' ? [] : null,
@@ -5212,7 +5473,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5212
5473
  return;
5213
5474
  }
5214
5475
  if (state.agentRuns.has(sessionRef.containerName)) {
5215
- sendJson(res, 409, { error: '当前会话已有运行中的 agent 任务' });
5476
+ // 锁是容器级的:同一容器里其他 agent 会话在跑任务时同样命中这里
5477
+ sendJson(res, 409, { error: '当前容器已有运行中的 agent 任务,请等它结束或先停止' });
5216
5478
  return;
5217
5479
  }
5218
5480
 
@@ -5259,6 +5521,10 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5259
5521
  type: 'meta',
5260
5522
  containerName: sessionRef.containerName,
5261
5523
  sessionName: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
5524
+ // 前端用这两个 id 把本地乐观占位(local-* / __streaming_trace__)换成
5525
+ // 服务端持久化 id,轮询对账时 React key 才能对得上、DOM 不整体重建
5526
+ userMessageId: userMessage && userMessage.id,
5527
+ traceMessageId: traceMessage && traceMessage.id,
5262
5528
  contextMode,
5263
5529
  resumeAttempted,
5264
5530
  resumeSucceeded,
@@ -5279,10 +5545,26 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5279
5545
  pending: true
5280
5546
  });
5281
5547
 
5548
+ const diagStreamStartedAt = Date.now(); // DIAG_LOG
5549
+ let diagEventSeq = 0; // DIAG_LOG
5550
+ diagLog(ctx, 'stream_exec_start', { // DIAG_LOG
5551
+ session: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
5552
+ agentProgram: agentMeta.agentProgram,
5553
+ command
5554
+ });
5282
5555
  try {
5283
5556
  const result = await execAgentInWebContainerStream(ctx, state, sessionRef, command, {
5284
5557
  agentProgram: agentMeta.agentProgram,
5285
5558
  onEvent: event => {
5559
+ diagEventSeq += 1; // DIAG_LOG
5560
+ diagLog(ctx, 'stream_event', { // DIAG_LOG
5561
+ session: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
5562
+ seq: diagEventSeq,
5563
+ type: event && event.type,
5564
+ sinceStartMs: Date.now() - diagStreamStartedAt,
5565
+ textLength: event && typeof event.text === 'string' ? event.text.length : undefined,
5566
+ contentLength: event && typeof event.content === 'string' ? event.content.length : undefined
5567
+ });
5286
5568
  if (event && event.type === 'trace' && event.text) {
5287
5569
  traceLines.push(String(event.text));
5288
5570
  if (event.traceEvent && typeof event.traceEvent === 'object') {
@@ -5323,6 +5605,13 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5323
5605
  });
5324
5606
  }
5325
5607
  });
5608
+ diagLog(ctx, 'stream_exec_end', { // DIAG_LOG
5609
+ session: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
5610
+ totalDurationMs: Date.now() - diagStreamStartedAt,
5611
+ eventCount: diagEventSeq,
5612
+ interrupted: result.interrupted === true,
5613
+ exitCode: result.exitCode
5614
+ });
5326
5615
  traceLines.push(result.interrupted === true ? '[任务] 已停止' : '[任务] 已完成');
5327
5616
  patchWebSessionMessage(state.webHistoryDir, sessionRef, userMessage && userMessage.id, {
5328
5617
  pending: false,
@@ -5342,7 +5631,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5342
5631
  if (streamingReplyMessageId) {
5343
5632
  removeWebSessionMessage(state.webHistoryDir, sessionRef, streamingReplyMessageId);
5344
5633
  }
5345
- finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, {
5634
+ const replyMessage = finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, {
5346
5635
  contextMode,
5347
5636
  resumeAttempted,
5348
5637
  resumeSucceeded,
@@ -5353,6 +5642,9 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5353
5642
  type: 'result',
5354
5643
  exitCode: result.exitCode,
5355
5644
  output: result.output,
5645
+ // 最终 assistant 消息的持久化 id:前端把 __streaming__ 占位换成它,
5646
+ // 发送后的首次对账才不会重建整个消息列表 DOM(滚动位置/选区保持)
5647
+ replyMessageId: replyMessage && replyMessage.id,
5356
5648
  contextMode,
5357
5649
  resumeAttempted,
5358
5650
  resumeSucceeded,
@@ -5397,6 +5689,9 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5397
5689
  }
5398
5690
  const stopped = stopWebAgentRun(state, sessionRef.containerName);
5399
5691
  if (!stopped) {
5692
+ // 运行登记不存在(serve 重启后常见的孤儿任务):把残留的
5693
+ // pending 消息标记为已中断,否则前端会因 pending 永久禁用输入
5694
+ interruptOrphanPendingMessages(state.webHistoryDir, sessionRef);
5400
5695
  sendJson(res, 404, { error: '当前会话没有运行中的 agent 任务' });
5401
5696
  return;
5402
5697
  }
@@ -5530,6 +5825,29 @@ async function startWebServer(options) {
5530
5825
  throw new Error('Web 认证配置缺失,请设置 serve -U / serve -P');
5531
5826
  }
5532
5827
 
5828
+ // DIAG_LOG:包一层记录每次 docker/podman 子进程调用的参数与耗时,覆盖
5829
+ // listWebManyoyoContainers、ensureWebContainer 等所有经由 ctx.dockerExecArgs
5830
+ // 发起的调用,不需要逐个调用点单独打点。
5831
+ if (typeof ctx.dockerExecArgs === 'function') {
5832
+ const rawDockerExecArgs = ctx.dockerExecArgs;
5833
+ ctx.dockerExecArgs = (args, execOptions) => {
5834
+ const startedAt = Date.now();
5835
+ let errorMessage = '';
5836
+ try {
5837
+ return rawDockerExecArgs(args, execOptions);
5838
+ } catch (e) {
5839
+ errorMessage = e && e.message ? e.message : String(e);
5840
+ throw e;
5841
+ } finally {
5842
+ diagLog(ctx, 'docker_exec', {
5843
+ args: Array.isArray(args) ? args : [],
5844
+ durationMs: Date.now() - startedAt,
5845
+ error: errorMessage || undefined
5846
+ });
5847
+ }
5848
+ };
5849
+ }
5850
+
5533
5851
  const state = {
5534
5852
  webHistoryDir: options.webHistoryDir || path.join(os.homedir(), '.manyoyo', 'web-history'),
5535
5853
  webConfigPath: options.webConfigPath || getDefaultWebConfigPath(),
@@ -5559,6 +5877,20 @@ async function startWebServer(options) {
5559
5877
  });
5560
5878
 
5561
5879
  const server = http.createServer(async (req, res) => {
5880
+ // DIAG_LOG:记录每个 HTTP 请求从进入到响应完成(含长连接的 agent/stream,
5881
+ // 'finish' 在 chunked 响应整体结束时才触发)的方法/路径/状态码/耗时。
5882
+ const diagRequestStartedAt = Date.now();
5883
+ res.on('finish', () => {
5884
+ const rawPath = String(req.url || '').split('?')[0];
5885
+ const sessionMatch = rawPath.match(/^\/api\/sessions\/([^/]+)/);
5886
+ diagLog(ctx, 'http_request', {
5887
+ method: req.method,
5888
+ path: rawPath,
5889
+ status: res.statusCode,
5890
+ durationMs: Date.now() - diagRequestStartedAt,
5891
+ session: sessionMatch ? decodeURIComponent(sessionMatch[1]) : undefined
5892
+ });
5893
+ });
5562
5894
  try {
5563
5895
  const fallbackHost = `${formatUrlHost(ctx.serverHost)}:${ctx.serverPort}`;
5564
5896
  const url = new URL(req.url, `http://${req.headers.host || fallbackHost}`);
@@ -5593,6 +5925,13 @@ async function startWebServer(options) {
5593
5925
  return;
5594
5926
  }
5595
5927
 
5928
+ // DIAG_LOG:临时诊断日志查看页,排查完成后与 renderDiagLogsHtml、
5929
+ // /api/diag/logs 一起整体删除即可(全局搜索 "DIAG_LOG")。
5930
+ if (req.method === 'GET' && pathname === '/logs') {
5931
+ sendHtml(res, 200, renderDiagLogsHtml());
5932
+ return;
5933
+ }
5934
+
5596
5935
  const appFrontendMatch = pathname.match(/^\/app\/frontend\/([A-Za-z0-9._-]+)$/);
5597
5936
  if (req.method === 'GET' && appFrontendMatch) {
5598
5937
  const assetName = appFrontendMatch[1];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "7.0.13",
3
+ "version": "7.0.16",
4
4
  "imageVersion": "1.9.1-common",
5
5
  "playwrightCliVersion": "0.1.18",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",