@xcanwin/manyoyo 7.0.21 → 7.0.24

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,8 @@ const {
30
30
  projectSessionEvents
31
31
  } = require('../core/events');
32
32
  const { FileEventStore } = require('../core/event-store');
33
+ const { buildManyoyoLogPath, getLocalDateTag } = require('../log-path');
34
+ const { isValidLogDateTag, readServeLogEntries } = require('../serve-log-reader');
33
35
 
34
36
 
35
37
  const WEB_HISTORY_MAX_MESSAGES = 500;
@@ -4572,6 +4574,33 @@ function sendWebUnauthorized(res, pathname, ctx) {
4572
4574
  );
4573
4575
  }
4574
4576
 
4577
+ // 运行日志查看接口的支撑函数。日志目录以 serve 实际使用的 logger.path 为准,
4578
+ // 拿不到时退回 ~/.manyoyo/logs/serve/ 的默认规则
4579
+ function resolveServeLogDir(ctx) {
4580
+ if (ctx && ctx.logger && typeof ctx.logger.path === 'string' && ctx.logger.path) {
4581
+ return path.dirname(ctx.logger.path);
4582
+ }
4583
+ return buildManyoyoLogPath('serve').dir;
4584
+ }
4585
+
4586
+ function listServeLogDates(ctx) {
4587
+ const dir = resolveServeLogDir(ctx);
4588
+ let names = [];
4589
+ try {
4590
+ names = fs.readdirSync(dir);
4591
+ } catch (e) {
4592
+ return [];
4593
+ }
4594
+ return names
4595
+ .map(name => {
4596
+ const matched = String(name).match(/^serve-(\d{4}-\d{2}-\d{2})\.log$/);
4597
+ return matched ? matched[1] : '';
4598
+ })
4599
+ .filter(Boolean)
4600
+ .sort()
4601
+ .reverse();
4602
+ }
4603
+
4575
4604
  async function handleWebApi(req, res, pathname, ctx, state) {
4576
4605
  // [P2-03] 对非只读请求校验自定义头,防止 CSRF 攻击
4577
4606
  // 跨站请求无法设置自定义头(浏览器同源策略),合法前端请求统一携带此头
@@ -4582,6 +4611,48 @@ async function handleWebApi(req, res, pathname, ctx, state) {
4582
4611
  }
4583
4612
  }
4584
4613
  const routes = [
4614
+ {
4615
+ method: 'GET',
4616
+ match: currentPath => currentPath === '/api/logs/dates' ? [] : null,
4617
+ handler: async () => {
4618
+ sendJson(res, 200, { dates: listServeLogDates(ctx) });
4619
+ }
4620
+ },
4621
+ {
4622
+ method: 'GET',
4623
+ match: currentPath => currentPath === '/api/logs' ? [] : null,
4624
+ handler: async () => {
4625
+ const requestUrl = new URL(req.url || '/api/logs', 'http://localhost');
4626
+ const rawDate = requestUrl.searchParams.get('date') || '';
4627
+ // date 会拼进文件名,必须白名单校验,否则能拼出目录穿越
4628
+ if (rawDate && !isValidLogDateTag(rawDate)) {
4629
+ sendJson(res, 400, { error: 'date 必须是 YYYY-MM-DD' });
4630
+ return;
4631
+ }
4632
+ const date = rawDate || getLocalDateTag();
4633
+ const levelParam = (requestUrl.searchParams.get('level') || '').trim();
4634
+ const levels = levelParam
4635
+ ? levelParam.split(',').map(item => item.trim().toUpperCase()).filter(Boolean)
4636
+ : [];
4637
+ const endOffsetParam = requestUrl.searchParams.get('endOffset');
4638
+ const result = readServeLogEntries(
4639
+ path.join(resolveServeLogDir(ctx), `serve-${date}.log`),
4640
+ {
4641
+ limit: Number(requestUrl.searchParams.get('limit')) || undefined,
4642
+ endOffset: endOffsetParam === null ? undefined : Number(endOffsetParam),
4643
+ levels,
4644
+ keyword: requestUrl.searchParams.get('keyword') || '',
4645
+ session: requestUrl.searchParams.get('session') || ''
4646
+ }
4647
+ );
4648
+ sendJson(res, 200, {
4649
+ date,
4650
+ entries: result.entries,
4651
+ nextEndOffset: result.nextEndOffset,
4652
+ limit: result.limit
4653
+ });
4654
+ }
4655
+ },
4585
4656
  {
4586
4657
  method: 'GET',
4587
4658
  match: currentPath => currentPath === '/api/fs/directories' ? [] : null,
@@ -5430,6 +5501,31 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5430
5501
  'Cache-Control': 'no-store',
5431
5502
  'X-Accel-Buffering': 'no'
5432
5503
  });
5504
+ // agent 流的生命周期日志:每轮最多三条(开始 / 客户端断开 / 结束),
5505
+ // 不是逐事件打点。没有这几条的话,用户报"跑到一半断流了"时服务端
5506
+ // 日志里查不到任何痕迹——断连本身只发生在客户端侧,服务端不主动感知
5507
+ const turnStartedAt = Date.now();
5508
+ const turnSessionKey = buildWebSessionKey(sessionRef.containerName, sessionRef.agentId);
5509
+ ctx.logger.info('agent turn started', {
5510
+ session: turnSessionKey,
5511
+ agentProgram: agentMeta.agentProgram,
5512
+ contextMode,
5513
+ resumeAttempted,
5514
+ resumeSucceeded
5515
+ });
5516
+ // 'close' 先于 'finish' 触发且 writableFinished 仍为 false,说明是
5517
+ // 客户端/反代把连接掐了,而不是我们正常收尾。此时容器里的任务还在跑,
5518
+ // 前端会转入恢复轮询——日志要把这个区别写清楚,否则事后无法归因
5519
+ res.on('close', () => {
5520
+ if (res.writableFinished) {
5521
+ return;
5522
+ }
5523
+ ctx.logger.warn('agent stream client disconnected', {
5524
+ session: turnSessionKey,
5525
+ elapsedMs: Date.now() - turnStartedAt,
5526
+ runContinues: state.agentRuns.has(sessionRef.containerName)
5527
+ });
5528
+ });
5433
5529
  // agent 可能几十秒不产生任何输出(长 WebSearch、长 Bash、长思考),
5434
5530
  // 而反向代理按"上游多久没发字节"计空闲超时(nginx proxy_read_timeout
5435
5531
  // 默认 60s),超时会 RST 掉这条流,浏览器侧抛 network error。这里定期
@@ -5546,6 +5642,12 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5546
5642
  });
5547
5643
  }
5548
5644
  });
5645
+ ctx.logger.info('agent turn finished', {
5646
+ session: turnSessionKey,
5647
+ durationMs: Date.now() - turnStartedAt,
5648
+ exitCode: result.exitCode,
5649
+ interrupted: result.interrupted === true
5650
+ });
5549
5651
  traceLines.push(result.interrupted === true ? '[任务] 已停止' : '[任务] 已完成');
5550
5652
  patchWebSessionMessage(state.webHistoryDir, sessionRef, userMessage && userMessage.id, {
5551
5653
  pending: false,
@@ -5585,6 +5687,11 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5585
5687
  interrupted: result.interrupted === true
5586
5688
  }, result.interrupted === true ? 'process.interrupted' : 'process.exited', { exitCode: result.exitCode });
5587
5689
  } catch (e) {
5690
+ ctx.logger.error('agent turn failed', {
5691
+ session: turnSessionKey,
5692
+ durationMs: Date.now() - turnStartedAt,
5693
+ message: e && e.message ? e.message : 'Agent 执行失败'
5694
+ });
5588
5695
  traceLines.push(`[错误] ${e && e.message ? e.message : 'Agent 执行失败'}`);
5589
5696
  patchWebSessionMessage(state.webHistoryDir, sessionRef, userMessage && userMessage.id, {
5590
5697
  pending: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "7.0.21",
3
+ "version": "7.0.24",
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",