@xcanwin/manyoyo 7.0.15 → 7.0.17
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/frontend/shadcn.html +61 -61
- package/lib/web/server.js +307 -6
- package/package.json +1 -1
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;
|
|
@@ -3089,6 +3165,7 @@ function listWebManyoyoContainers(ctx) {
|
|
|
3089
3165
|
return map;
|
|
3090
3166
|
}
|
|
3091
3167
|
|
|
3168
|
+
const candidates = [];
|
|
3092
3169
|
output.trim().split('\n').forEach(line => {
|
|
3093
3170
|
const [name, status, image] = line.split('\t');
|
|
3094
3171
|
if (!ctx.isValidContainerName(name)) {
|
|
@@ -3098,23 +3175,46 @@ function listWebManyoyoContainers(ctx) {
|
|
|
3098
3175
|
if (!imageName.includes('manyoyo') && !name.startsWith('manyoyo-') && !name.startsWith('my-')) {
|
|
3099
3176
|
return;
|
|
3100
3177
|
}
|
|
3101
|
-
|
|
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 = '';
|
|
3102
3187
|
try {
|
|
3103
|
-
|
|
3188
|
+
inspectOutput = String(
|
|
3104
3189
|
ctx.dockerExecArgs(
|
|
3105
|
-
[
|
|
3190
|
+
[
|
|
3191
|
+
'inspect',
|
|
3192
|
+
'-f',
|
|
3193
|
+
'{{.Name}}\t{{index .Config.Labels "manyoyo.default_cmd"}}',
|
|
3194
|
+
...candidates.map(c => c.name)
|
|
3195
|
+
],
|
|
3106
3196
|
{ ignoreError: true }
|
|
3107
3197
|
) || ''
|
|
3108
|
-
)
|
|
3198
|
+
);
|
|
3109
3199
|
} catch (e) {
|
|
3110
|
-
|
|
3200
|
+
inspectOutput = '';
|
|
3111
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 }) => {
|
|
3112
3212
|
map[name] = {
|
|
3113
3213
|
name,
|
|
3114
3214
|
status: status || 'unknown',
|
|
3115
3215
|
image: imageName,
|
|
3116
3216
|
createdAt: estimateStartTimeFromStatus(status),
|
|
3117
|
-
defaultCommand
|
|
3217
|
+
defaultCommand: defaultCommandByName[name] || ''
|
|
3118
3218
|
};
|
|
3119
3219
|
});
|
|
3120
3220
|
|
|
@@ -4144,6 +4244,123 @@ function renderLoginHtml(ctx) {
|
|
|
4144
4244
|
return applyServeTitle(loadTemplate('login.html'), ctx);
|
|
4145
4245
|
}
|
|
4146
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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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
|
+
|
|
4147
4364
|
function toPositiveInt(value, fallback) {
|
|
4148
4365
|
const parsed = Number.parseInt(value, 10);
|
|
4149
4366
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
@@ -4437,6 +4654,23 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
4437
4654
|
}
|
|
4438
4655
|
}
|
|
4439
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
|
+
},
|
|
4440
4674
|
{
|
|
4441
4675
|
method: 'GET',
|
|
4442
4676
|
match: currentPath => currentPath === '/api/fs/directories' ? [] : null,
|
|
@@ -5311,10 +5545,26 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
5311
5545
|
pending: true
|
|
5312
5546
|
});
|
|
5313
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
|
+
});
|
|
5314
5555
|
try {
|
|
5315
5556
|
const result = await execAgentInWebContainerStream(ctx, state, sessionRef, command, {
|
|
5316
5557
|
agentProgram: agentMeta.agentProgram,
|
|
5317
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
|
+
});
|
|
5318
5568
|
if (event && event.type === 'trace' && event.text) {
|
|
5319
5569
|
traceLines.push(String(event.text));
|
|
5320
5570
|
if (event.traceEvent && typeof event.traceEvent === 'object') {
|
|
@@ -5355,6 +5605,13 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
5355
5605
|
});
|
|
5356
5606
|
}
|
|
5357
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
|
+
});
|
|
5358
5615
|
traceLines.push(result.interrupted === true ? '[任务] 已停止' : '[任务] 已完成');
|
|
5359
5616
|
patchWebSessionMessage(state.webHistoryDir, sessionRef, userMessage && userMessage.id, {
|
|
5360
5617
|
pending: false,
|
|
@@ -5568,6 +5825,29 @@ async function startWebServer(options) {
|
|
|
5568
5825
|
throw new Error('Web 认证配置缺失,请设置 serve -U / serve -P');
|
|
5569
5826
|
}
|
|
5570
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
|
+
|
|
5571
5851
|
const state = {
|
|
5572
5852
|
webHistoryDir: options.webHistoryDir || path.join(os.homedir(), '.manyoyo', 'web-history'),
|
|
5573
5853
|
webConfigPath: options.webConfigPath || getDefaultWebConfigPath(),
|
|
@@ -5597,6 +5877,20 @@ async function startWebServer(options) {
|
|
|
5597
5877
|
});
|
|
5598
5878
|
|
|
5599
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
|
+
});
|
|
5600
5894
|
try {
|
|
5601
5895
|
const fallbackHost = `${formatUrlHost(ctx.serverHost)}:${ctx.serverPort}`;
|
|
5602
5896
|
const url = new URL(req.url, `http://${req.headers.host || fallbackHost}`);
|
|
@@ -5631,6 +5925,13 @@ async function startWebServer(options) {
|
|
|
5631
5925
|
return;
|
|
5632
5926
|
}
|
|
5633
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
|
+
|
|
5634
5935
|
const appFrontendMatch = pathname.match(/^\/app\/frontend\/([A-Za-z0-9._-]+)$/);
|
|
5635
5936
|
if (req.method === 'GET' && appFrontendMatch) {
|
|
5636
5937
|
const assetName = appFrontendMatch[1];
|