@xcanwin/manyoyo 7.0.23 → 7.0.25
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/serve-log-reader.js +178 -0
- package/lib/web/AGENTS.md +32 -0
- package/lib/web/frontend/shadcn.html +86 -86
- package/lib/web/server.js +107 -0
- package/package.json +1 -1
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,
|