@xcanwin/manyoyo 7.0.20 → 7.0.23

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,82 +30,7 @@ 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
33
 
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)
109
34
 
110
35
  const WEB_HISTORY_MAX_MESSAGES = 500;
111
36
  const WEB_OUTPUT_MAX_CHARS = 16000;
@@ -2755,11 +2680,26 @@ function parseAndValidateConfigRaw(raw) {
2755
2680
  return config;
2756
2681
  }
2757
2682
 
2683
+ // 新建容器对话框会拿 defaults.hostPath 做预填,但 validateHostPath 明确拒绝
2684
+ // 根目录 / /home / $HOME。serve 以 $HOME 启动(root 用户下就是 /root)时,
2685
+ // 预填的值必定创建失败——这里直接留空,逼用户用"选择"选一个真实工作目录
2686
+ function sanitizeDefaultHostPath(hostPath) {
2687
+ const value = String(hostPath || '').trim();
2688
+ if (!value) {
2689
+ return '';
2690
+ }
2691
+ const homeDir = process.env.HOME || os.homedir() || '/home';
2692
+ if (value === '/' || value === '/home' || value === homeDir) {
2693
+ return '';
2694
+ }
2695
+ return value;
2696
+ }
2697
+
2758
2698
  function buildConfigDefaults(ctx, config) {
2759
2699
  const parsed = toPlainObject(config);
2760
2700
  const defaults = {
2761
2701
  containerName: hasOwn(parsed, 'containerName') ? String(parsed.containerName || '') : '',
2762
- hostPath: pickFirstString(parsed.hostPath, ctx.hostPath),
2702
+ hostPath: sanitizeDefaultHostPath(pickFirstString(parsed.hostPath, ctx.hostPath)),
2763
2703
  containerPath: pickFirstString(parsed.containerPath, ctx.containerPath),
2764
2704
  imageName: pickFirstString(parsed.imageName, ctx.imageName),
2765
2705
  imageVersion: pickFirstString(parsed.imageVersion, ctx.imageVersion),
@@ -3216,8 +3156,11 @@ function listWebManyoyoContainers(ctx) {
3216
3156
  });
3217
3157
 
3218
3158
  // 逐容器单独 docker inspect 是 N+1 同步阻塞调用(spawnSync 无 timeout,会
3219
- // 独占 Node 单线程事件循环);容器一多,serve 所有接口(包括正在流式输出
3220
- // 的 agent/stream)都会被拖慢甚至冻结。这里合并成一次批量 inspect。
3159
+ // 独占 Node 单线程事件循环),这里合并成一次批量 inspect。
3160
+ // 注意:31 个容器实测 `ps -a` 约 40ms、批量 `inspect` 约 45ms,容器运行时调用
3161
+ // 并不是 GET /api/sessions 慢的原因——真正的开销在逐容器同步读取并解析整份
3162
+ // 历史 JSON(见 buildSessionSummary 的 preloadedHistory 注释)。排查这条路径
3163
+ // 变慢时别先怀疑 docker/podman。
3221
3164
  const defaultCommandByName = {};
3222
3165
  if (candidates.length) {
3223
3166
  let inspectOutput = '';
@@ -3712,24 +3655,38 @@ async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerNa
3712
3655
  const sessionRef = typeof sessionRefOrContainerName === 'string'
3713
3656
  ? { containerName: sessionRefOrContainerName, agentId: WEB_DEFAULT_AGENT_ID }
3714
3657
  : sessionRefOrContainerName;
3715
- const sessionKey = buildWebSessionKey(sessionRef.containerName, sessionRef.agentId);
3716
3658
  const agentProgram = typeof opts.agentProgram === 'string' ? opts.agentProgram : '';
3717
3659
  const onEvent = typeof opts.onEvent === 'function' ? opts.onEvent : () => {};
3718
- const process = spawn(
3719
- ctx.dockerCmd,
3720
- ['exec', sessionRef.containerName, '/bin/bash', '-lc', command],
3721
- { stdio: ['ignore', 'pipe', 'pipe'] }
3722
- );
3660
+ // 调用方可能已经在路由入口占过锁(见 reserveWebAgentRun):复用那一份 runState,
3661
+ // 不要另起一个对象,否则 stopWebAgentRun 拿到的还是占位、杀不到真正的进程
3662
+ const runState = opts.runState && typeof opts.runState === 'object'
3663
+ ? opts.runState
3664
+ : reserveWebAgentRun(state, sessionRef);
3665
+ let process;
3666
+ try {
3667
+ process = spawn(
3668
+ ctx.dockerCmd,
3669
+ ['exec', sessionRef.containerName, '/bin/bash', '-lc', command],
3670
+ { stdio: ['ignore', 'pipe', 'pipe'] }
3671
+ );
3672
+ } catch (e) {
3673
+ // spawn 同步抛错(如 dockerCmd 不存在)时必须释放锁,否则这个容器永远发不出消息
3674
+ releaseWebAgentRun(state, sessionRef.containerName, runState);
3675
+ throw e;
3676
+ }
3723
3677
 
3724
- const runState = {
3725
- containerName: sessionRef.containerName,
3726
- sessionKey,
3727
- process,
3728
- command,
3729
- startedAt: new Date().toISOString(),
3730
- stopping: false
3731
- };
3678
+ runState.process = process;
3679
+ runState.command = command;
3680
+ runState.startedAt = new Date().toISOString();
3732
3681
  state.agentRuns.set(sessionRef.containerName, runState);
3682
+ // 准备阶段(拉起容器等)用户就点了停止:进程刚 spawn 出来立刻收掉
3683
+ if (runState.stopping === true) {
3684
+ try {
3685
+ process.kill('SIGTERM');
3686
+ } catch (e) {
3687
+ // 进程可能已经退出,忽略
3688
+ }
3689
+ }
3733
3690
 
3734
3691
  return await new Promise((resolve, reject) => {
3735
3692
  const MAX_RAW_OUTPUT_CHARS = 32 * 1024 * 1024;
@@ -3847,11 +3804,11 @@ async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerNa
3847
3804
  });
3848
3805
 
3849
3806
  process.on('error', error => {
3850
- state.agentRuns.delete(sessionRef.containerName);
3807
+ releaseWebAgentRun(state, sessionRef.containerName, runState);
3851
3808
  reject(error);
3852
3809
  });
3853
3810
  process.on('close', code => {
3854
- state.agentRuns.delete(sessionRef.containerName);
3811
+ releaseWebAgentRun(state, sessionRef.containerName, runState);
3855
3812
  if (stdoutPending) {
3856
3813
  emitStdoutTraceLine(stdoutPending);
3857
3814
  stdoutPending = '';
@@ -3934,9 +3891,43 @@ function resolveWebStreamControlEventType(event) {
3934
3891
  return 'process.stdout';
3935
3892
  }
3936
3893
 
3894
+ // 容器级运行锁的"占位"。必须在路由里同步调用:从 409 检查到真正 spawn 之间隔着
3895
+ // readJsonBody / prepareWebAgentExecution(含 ensureWebContainer 拉起容器)等多个
3896
+ // await,容器冷启动时这个窗口有好几秒。不先占住的话第二次发送会穿过 409 检查,
3897
+ // 同一容器里真的跑起两个 agent 进程,而且后者会覆盖前者的运行登记,
3898
+ // 让第一个进程再也停不掉
3899
+ function reserveWebAgentRun(state, sessionRef) {
3900
+ const runState = {
3901
+ containerName: sessionRef.containerName,
3902
+ sessionKey: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
3903
+ process: null,
3904
+ command: '',
3905
+ startedAt: new Date().toISOString(),
3906
+ stopping: false
3907
+ };
3908
+ state.agentRuns.set(sessionRef.containerName, runState);
3909
+ return runState;
3910
+ }
3911
+
3912
+ // 只有占位仍是自己那一份时才释放,避免误删别人的登记
3913
+ function releaseWebAgentRun(state, containerName, runState) {
3914
+ if (state.agentRuns.get(containerName) === runState) {
3915
+ state.agentRuns.delete(containerName);
3916
+ }
3917
+ }
3918
+
3937
3919
  function stopWebAgentRun(state, containerName) {
3938
3920
  const runState = state.agentRuns.get(containerName);
3939
- if (!runState || !runState.process || runState.process.killed) {
3921
+ if (!runState) {
3922
+ return false;
3923
+ }
3924
+ if (!runState.process) {
3925
+ // 还在准备阶段(拉起容器 / 探测 resume),进程尚未 spawn:先记下停止意图,
3926
+ // execAgentInWebContainerStream 拿到进程后会立刻收掉它
3927
+ runState.stopping = true;
3928
+ return true;
3929
+ }
3930
+ if (runState.process.killed) {
3940
3931
  return false;
3941
3932
  }
3942
3933
  runState.stopping = true;
@@ -4041,6 +4032,7 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef, preloadedHist
4041
4032
  status: containerInfo.status || 'history',
4042
4033
  defaultCommand: containerInfo.defaultCommand || ''
4043
4034
  });
4035
+ const agentRunState = state.agentRuns ? state.agentRuns.get(containerName) : undefined;
4044
4036
  const createdAt = agentSession.createdAt || containerInfo.createdAt || null;
4045
4037
  const updatedAt = agentSession.updatedAt
4046
4038
  || (latestMessage && latestMessage.timestamp)
@@ -4067,6 +4059,11 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef, preloadedHist
4067
4059
  hostPath: applied.hostPath || '',
4068
4060
  containerPath: applied.containerPath || '',
4069
4061
  archived: agentSession.archived === true,
4062
+ // 运行锁是容器级的,界面却是 agent 级的:把"容器在忙"和"是不是自己在忙"
4063
+ // 都透出去,同容器的其他 AGENT 才能提前禁用输入,而不是点了发送才吃 409
4064
+ containerBusy: agentRunState !== undefined,
4065
+ agentRunning: agentRunState !== undefined
4066
+ && agentRunState.sessionKey === buildWebSessionKey(containerName, agentId),
4070
4067
  ...(synthetic ? { synthetic: true } : {})
4071
4068
  };
4072
4069
  }
@@ -4292,123 +4289,6 @@ function renderLoginHtml(ctx) {
4292
4289
  return applyServeTitle(loadTemplate('login.html'), ctx);
4293
4290
  }
4294
4291
 
4295
- // DIAG_LOG:临时诊断日志查看页,自成一个内联 HTML 字符串(不接入 frontend-shadcn
4296
- // 构建流程),配合 /api/diag/logs 接口使用。排查结束后可整体删除本函数及其
4297
- // 调用点(全局搜索 "DIAG_LOG")。
4298
- function renderDiagLogsHtml() {
4299
- return `<!DOCTYPE html>
4300
- <html lang="zh-CN">
4301
- <head>
4302
- <meta charset="UTF-8">
4303
- <title>MANYOYO 诊断日志</title>
4304
- <style>
4305
- body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #0b0d12; color: #d7dae0; }
4306
- 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; }
4307
- header input, header select { background: #171a21; border: 1px solid #2c313d; color: #d7dae0; padding: 5px 8px; border-radius: 4px; font-size: 13px; }
4308
- header button { background: #2b6cb0; border: none; color: #fff; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; }
4309
- header button.secondary { background: #2c313d; }
4310
- header label { font-size: 12px; display: flex; align-items: center; gap: 4px; }
4311
- main { padding: 8px 16px 40px; }
4312
- table { width: 100%; border-collapse: collapse; font-size: 12px; }
4313
- th, td { text-align: left; padding: 4px 8px; border-bottom: 1px solid #1c202a; vertical-align: top; white-space: pre-wrap; word-break: break-all; }
4314
- th { position: sticky; top: 53px; background: #0b0d12; color: #8b93a3; font-weight: 600; }
4315
- tr:hover { background: #12151c; }
4316
- .cat { display: inline-block; padding: 1px 6px; border-radius: 3px; background: #1e2530; color: #8fd3ff; }
4317
- .status-ok { color: #7ee787; }
4318
- .status-err { color: #ff7b72; }
4319
- .dur-slow { color: #ffa657; font-weight: 600; }
4320
- #count { color: #8b93a3; font-size: 12px; }
4321
- </style>
4322
- </head>
4323
- <body>
4324
- <header>
4325
- <strong>诊断日志</strong>
4326
- <label>日期 <input id="f-date" type="text" placeholder="YYYY-MM-DD"></label>
4327
- <label>分类 <input id="f-category" type="text" placeholder="http_request / docker_exec / stream_event / stream_exec_start / stream_exec_end"></label>
4328
- <label>会话 <input id="f-session" type="text" placeholder="containerName 或 containerName~agentId"></label>
4329
- <label>关键字 <input id="f-keyword" type="text" placeholder="任意文本"></label>
4330
- <label>条数 <input id="f-limit" type="text" value="500" style="width:60px"></label>
4331
- <button id="btn-refresh">刷新</button>
4332
- <label><input id="f-auto" type="checkbox"> 每 2 秒自动刷新</label>
4333
- <span id="count"></span>
4334
- </header>
4335
- <main>
4336
- <table>
4337
- <thead><tr><th>时间</th><th>分类</th><th>详情</th></tr></thead>
4338
- <tbody id="rows"></tbody>
4339
- </table>
4340
- </main>
4341
- <script>
4342
- (function () {
4343
- var timer = null;
4344
- function fmtExtra(entry) {
4345
- var e = entry.extra || {};
4346
- var parts = [];
4347
- if (e.method) parts.push(e.method + ' ' + (e.path || ''));
4348
- if (typeof e.status === 'number') {
4349
- parts.push('<span class="' + (e.status < 400 ? 'status-ok' : 'status-err') + '">status=' + e.status + '</span>');
4350
- }
4351
- if (typeof e.durationMs === 'number') {
4352
- parts.push('<span class="' + (e.durationMs > 1000 ? 'dur-slow' : '') + '">' + e.durationMs + 'ms</span>');
4353
- }
4354
- if (e.session) parts.push('session=' + e.session);
4355
- if (e.args) parts.push('args=' + JSON.stringify(e.args));
4356
- if (e.type) parts.push('type=' + e.type);
4357
- if (typeof e.seq === 'number') parts.push('seq=' + e.seq);
4358
- if (typeof e.sinceStartMs === 'number') parts.push('sinceStart=' + e.sinceStartMs + 'ms');
4359
- if (typeof e.totalDurationMs === 'number') parts.push('total=' + e.totalDurationMs + 'ms');
4360
- if (typeof e.eventCount === 'number') parts.push('events=' + e.eventCount);
4361
- if (e.error) parts.push('<span class="status-err">error=' + e.error + '</span>');
4362
- var rest = {};
4363
- Object.keys(e).forEach(function (k) {
4364
- if (['method','path','status','durationMs','session','args','type','seq','sinceStartMs','totalDurationMs','eventCount','error'].indexOf(k) === -1) {
4365
- rest[k] = e[k];
4366
- }
4367
- });
4368
- if (Object.keys(rest).length) parts.push(JSON.stringify(rest));
4369
- return parts.join(' ');
4370
- }
4371
- function escapeHtml(s) {
4372
- return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
4373
- return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
4374
- });
4375
- }
4376
- function load() {
4377
- var params = new URLSearchParams();
4378
- var date = document.getElementById('f-date').value.trim();
4379
- var category = document.getElementById('f-category').value.trim();
4380
- var session = document.getElementById('f-session').value.trim();
4381
- var keyword = document.getElementById('f-keyword').value.trim();
4382
- var limit = document.getElementById('f-limit').value.trim();
4383
- if (date) params.set('date', date);
4384
- if (category) params.set('category', category);
4385
- if (session) params.set('session', session);
4386
- if (keyword) params.set('keyword', keyword);
4387
- if (limit) params.set('limit', limit);
4388
- fetch('/api/diag/logs?' + params.toString(), { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
4389
- .then(function (r) { return r.json(); })
4390
- .then(function (data) {
4391
- var entries = data.entries || [];
4392
- document.getElementById('count').textContent = entries.length + ' 条(' + data.date + ')';
4393
- var rows = entries.slice().reverse().map(function (entry) {
4394
- return '<tr><td>' + escapeHtml(entry.ts) + '</td><td><span class="cat">' + escapeHtml(entry.category) + '</span></td><td>' + fmtExtra(entry) + '</td></tr>';
4395
- });
4396
- document.getElementById('rows').innerHTML = rows.join('');
4397
- });
4398
- }
4399
- document.getElementById('btn-refresh').addEventListener('click', load);
4400
- document.getElementById('f-auto').addEventListener('change', function (e) {
4401
- if (timer) { clearInterval(timer); timer = null; }
4402
- if (e.target.checked) { timer = setInterval(load, 2000); }
4403
- });
4404
- load();
4405
- })();
4406
- </script>
4407
- </body>
4408
- </html>
4409
- `;
4410
- }
4411
-
4412
4292
  function toPositiveInt(value, fallback) {
4413
4293
  const parsed = Number.parseInt(value, 10);
4414
4294
  if (!Number.isFinite(parsed) || parsed <= 0) {
@@ -4702,23 +4582,6 @@ async function handleWebApi(req, res, pathname, ctx, state) {
4702
4582
  }
4703
4583
  }
4704
4584
  const routes = [
4705
- // DIAG_LOG:临时诊断日志查询接口,配合 /logs 页面使用,排查完成后与
4706
- // diagLog/readDiagLogEntries 一起整体删除即可(全局搜索 "DIAG_LOG")。
4707
- {
4708
- method: 'GET',
4709
- match: currentPath => currentPath === '/api/diag/logs' ? [] : null,
4710
- handler: async () => {
4711
- const requestUrl = new URL(req.url || '/api/diag/logs', 'http://localhost');
4712
- const entries = readDiagLogEntries(ctx, {
4713
- date: requestUrl.searchParams.get('date') || undefined,
4714
- category: requestUrl.searchParams.get('category') || undefined,
4715
- session: requestUrl.searchParams.get('session') || undefined,
4716
- keyword: requestUrl.searchParams.get('keyword') || undefined,
4717
- limit: Number(requestUrl.searchParams.get('limit')) || undefined
4718
- });
4719
- sendJson(res, 200, { date: requestUrl.searchParams.get('date') || getLocalDateTag(), entries });
4720
- }
4721
- },
4722
4585
  {
4723
4586
  method: 'GET',
4724
4587
  match: currentPath => currentPath === '/api/fs/directories' ? [] : null,
@@ -5525,6 +5388,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5525
5388
  sendJson(res, 409, { error: '当前容器已有运行中的 agent 任务,请等它结束或先停止' });
5526
5389
  return;
5527
5390
  }
5391
+ // 紧接着检查同步占位,中间不能有 await,否则并发请求会同时穿过上面这道检查
5392
+ const runState = reserveWebAgentRun(state, sessionRef);
5528
5393
 
5529
5394
  const userMessage = appendWebSessionMessage(state.webHistoryDir, sessionRef, 'user', prompt, {
5530
5395
  mode: 'agent',
@@ -5544,6 +5409,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5544
5409
  try {
5545
5410
  prepared = await prepareWebAgentExecution(ctx, state, sessionRef, prompt);
5546
5411
  } catch (e) {
5412
+ releaseWebAgentRun(state, sessionRef.containerName, runState);
5547
5413
  removeWebSessionMessage(state.webHistoryDir, sessionRef, traceMessage && traceMessage.id);
5548
5414
  removeWebSessionMessage(state.webHistoryDir, sessionRef, userMessage && userMessage.id);
5549
5415
  sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
@@ -5617,31 +5483,15 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5617
5483
  });
5618
5484
 
5619
5485
  let lastPartialPersistAt = 0;
5620
- const diagStreamStartedAt = Date.now(); // DIAG_LOG
5621
- let diagEventSeq = 0; // DIAG_LOG
5622
- diagLog(ctx, 'stream_exec_start', { // DIAG_LOG
5623
- session: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
5624
- agentProgram: agentMeta.agentProgram,
5625
- command
5626
- });
5627
5486
  try {
5628
5487
  const result = await execAgentInWebContainerStream(ctx, state, sessionRef, command, {
5629
5488
  agentProgram: agentMeta.agentProgram,
5489
+ // 复用路由入口占好的那把锁,而不是另起一份运行登记
5490
+ runState,
5630
5491
  onEvent: event => {
5631
- // token 级增量:只走网线,不进事件日志、不逐条打诊断日志,
5632
- // 历史落盘按 AGENT_STREAM_PARTIAL_PERSIST_INTERVAL_MS 节流
5492
+ // token 级增量:只走网线,不进事件日志,历史落盘按
5493
+ // AGENT_STREAM_PARTIAL_PERSIST_INTERVAL_MS 节流
5633
5494
  const isPartialDelta = event && event.type === 'content_delta' && event.partial === true;
5634
- if (!isPartialDelta) {
5635
- diagEventSeq += 1; // DIAG_LOG
5636
- diagLog(ctx, 'stream_event', { // DIAG_LOG
5637
- session: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
5638
- seq: diagEventSeq,
5639
- type: event && event.type,
5640
- sinceStartMs: Date.now() - diagStreamStartedAt,
5641
- textLength: event && typeof event.text === 'string' ? event.text.length : undefined,
5642
- contentLength: event && typeof event.content === 'string' ? event.content.length : undefined
5643
- });
5644
- }
5645
5495
  if (event && event.type === 'trace' && event.text) {
5646
5496
  traceLines.push(String(event.text));
5647
5497
  if (event.traceEvent && typeof event.traceEvent === 'object') {
@@ -5696,13 +5546,6 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5696
5546
  });
5697
5547
  }
5698
5548
  });
5699
- diagLog(ctx, 'stream_exec_end', { // DIAG_LOG
5700
- session: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
5701
- totalDurationMs: Date.now() - diagStreamStartedAt,
5702
- eventCount: diagEventSeq,
5703
- interrupted: result.interrupted === true,
5704
- exitCode: result.exitCode
5705
- });
5706
5549
  traceLines.push(result.interrupted === true ? '[任务] 已停止' : '[任务] 已完成');
5707
5550
  patchWebSessionMessage(state.webHistoryDir, sessionRef, userMessage && userMessage.id, {
5708
5551
  pending: false,
@@ -5921,29 +5764,6 @@ async function startWebServer(options) {
5921
5764
  throw new Error('Web 认证配置缺失,请设置 serve -U / serve -P');
5922
5765
  }
5923
5766
 
5924
- // DIAG_LOG:包一层记录每次 docker/podman 子进程调用的参数与耗时,覆盖
5925
- // listWebManyoyoContainers、ensureWebContainer 等所有经由 ctx.dockerExecArgs
5926
- // 发起的调用,不需要逐个调用点单独打点。
5927
- if (typeof ctx.dockerExecArgs === 'function') {
5928
- const rawDockerExecArgs = ctx.dockerExecArgs;
5929
- ctx.dockerExecArgs = (args, execOptions) => {
5930
- const startedAt = Date.now();
5931
- let errorMessage = '';
5932
- try {
5933
- return rawDockerExecArgs(args, execOptions);
5934
- } catch (e) {
5935
- errorMessage = e && e.message ? e.message : String(e);
5936
- throw e;
5937
- } finally {
5938
- diagLog(ctx, 'docker_exec', {
5939
- args: Array.isArray(args) ? args : [],
5940
- durationMs: Date.now() - startedAt,
5941
- error: errorMessage || undefined
5942
- });
5943
- }
5944
- };
5945
- }
5946
-
5947
5767
  const state = {
5948
5768
  webHistoryDir: options.webHistoryDir || path.join(os.homedir(), '.manyoyo', 'web-history'),
5949
5769
  webConfigPath: options.webConfigPath || getDefaultWebConfigPath(),
@@ -5973,20 +5793,6 @@ async function startWebServer(options) {
5973
5793
  });
5974
5794
 
5975
5795
  const server = http.createServer(async (req, res) => {
5976
- // DIAG_LOG:记录每个 HTTP 请求从进入到响应完成(含长连接的 agent/stream,
5977
- // 'finish' 在 chunked 响应整体结束时才触发)的方法/路径/状态码/耗时。
5978
- const diagRequestStartedAt = Date.now();
5979
- res.on('finish', () => {
5980
- const rawPath = String(req.url || '').split('?')[0];
5981
- const sessionMatch = rawPath.match(/^\/api\/sessions\/([^/]+)/);
5982
- diagLog(ctx, 'http_request', {
5983
- method: req.method,
5984
- path: rawPath,
5985
- status: res.statusCode,
5986
- durationMs: Date.now() - diagRequestStartedAt,
5987
- session: sessionMatch ? decodeURIComponent(sessionMatch[1]) : undefined
5988
- });
5989
- });
5990
5796
  try {
5991
5797
  const fallbackHost = `${formatUrlHost(ctx.serverHost)}:${ctx.serverPort}`;
5992
5798
  const url = new URL(req.url, `http://${req.headers.host || fallbackHost}`);
@@ -6021,13 +5827,6 @@ async function startWebServer(options) {
6021
5827
  return;
6022
5828
  }
6023
5829
 
6024
- // DIAG_LOG:临时诊断日志查看页,排查完成后与 renderDiagLogsHtml、
6025
- // /api/diag/logs 一起整体删除即可(全局搜索 "DIAG_LOG")。
6026
- if (req.method === 'GET' && pathname === '/logs') {
6027
- sendHtml(res, 200, renderDiagLogsHtml());
6028
- return;
6029
- }
6030
-
6031
5830
  const appFrontendMatch = pathname.match(/^\/app\/frontend\/([A-Za-z0-9._-]+)$/);
6032
5831
  if (req.method === 'GET' && appFrontendMatch) {
6033
5832
  const assetName = appFrontendMatch[1];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "7.0.20",
3
+ "version": "7.0.23",
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",