@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/docker/res/agent/update-agents.sh +1 -1
- package/lib/web/frontend/shadcn.html +86 -86
- package/lib/web/server.js +349 -10
- 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;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
3188
|
+
inspectOutput = String(
|
|
3103
3189
|
ctx.dockerExecArgs(
|
|
3104
|
-
[
|
|
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
|
-
)
|
|
3198
|
+
);
|
|
3108
3199
|
} catch (e) {
|
|
3109
|
-
|
|
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
|
-
|
|
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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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
|
-
|
|
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];
|