evolclaw-web 1.1.0 → 1.2.0
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/dist/index.js +9 -9
- package/dist/process-utils.js +20 -12
- package/dist/server.js +75 -14
- package/dist/sources/aid.js +20 -1
- package/dist/sources/monitor.js +96 -0
- package/dist/sources/session.js +10 -1
- package/dist/sources/system.js +2 -2
- package/dist/static/app.js +632 -120
- package/dist/static/index.html +31 -2
- package/dist/static/style.css +176 -10
- package/package.json +2 -2
- package/dist/sources/control.js +0 -58
package/dist/static/app.js
CHANGED
|
@@ -58,7 +58,7 @@ let ws = null;
|
|
|
58
58
|
let reconnectDelay = 1000;
|
|
59
59
|
let currentView = 'agents';
|
|
60
60
|
let pendingSub = null; // 重连后要恢复的订阅
|
|
61
|
-
const state = { agents: null, msg: null, session: null, cache: null, system: null, triggers: null };
|
|
61
|
+
const state = { agents: null, msg: null, session: null, cache: null, system: null, triggers: null, monitor: null };
|
|
62
62
|
|
|
63
63
|
function setConnStatus(text, cls) {
|
|
64
64
|
const el = $('#conn-status');
|
|
@@ -150,7 +150,9 @@ let msgSel = { aid: null, peer: null };
|
|
|
150
150
|
let sessSel = { sessionId: null, project: null };
|
|
151
151
|
let trigSel = { agent: null };
|
|
152
152
|
let sessSearch = '';
|
|
153
|
+
let sessFilterNormal = false; // true=只显示有效会话(userMsgs >= 2)
|
|
153
154
|
let sessChatMode = false; // false=完整视图,true=对话视图(折叠处理过程)
|
|
155
|
+
let monRange = '2m'; // Monitor 时间窗口:2m / 10m / 1h
|
|
154
156
|
|
|
155
157
|
function switchView(view) {
|
|
156
158
|
currentView = view;
|
|
@@ -162,6 +164,7 @@ function switchView(view) {
|
|
|
162
164
|
else if (view === 'cache') subscribe('cache', {});
|
|
163
165
|
else if (view === 'system') subscribe('system', {});
|
|
164
166
|
else if (view === 'triggers') subscribe('triggers', { agent: trigSel.agent });
|
|
167
|
+
else if (view === 'monitor') subscribe('monitor', { range: monRange });
|
|
165
168
|
else subscribe('agents', {});
|
|
166
169
|
if (state[view]) renderView(view);
|
|
167
170
|
}
|
|
@@ -179,6 +182,7 @@ function renderView(view) {
|
|
|
179
182
|
else if (view === 'cache') renderCache(state.cache);
|
|
180
183
|
else if (view === 'system') renderSystem(state.system);
|
|
181
184
|
else if (view === 'triggers') renderTriggers(state.triggers);
|
|
185
|
+
else if (view === 'monitor') renderMonitor(state.monitor);
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
// ── 工具 ──
|
|
@@ -231,74 +235,243 @@ function compareVer(a, b) {
|
|
|
231
235
|
return 0;
|
|
232
236
|
}
|
|
233
237
|
|
|
234
|
-
// ── Agents
|
|
238
|
+
// ── Agents 视图(对齐终端 watch aid:状态点前置 + 名字为主 + 两行 + 工作态着色 + 顶部统计条)──
|
|
239
|
+
|
|
240
|
+
// 逐 AID 异步操作状态(取代全局 _agentBusy):aid → 操作中的描述文字
|
|
241
|
+
const _agentOps = new Map(); // Map<aid, string>
|
|
242
|
+
let _agentBusy = false; // 保留兼容旧引用,不再用于阻塞渲染
|
|
243
|
+
let _agSubtab = 'enabled'; // 'enabled' | 'disabled'
|
|
244
|
+
|
|
245
|
+
// 工作状态徽标:一旦收到过消息就不再回 connected。
|
|
246
|
+
// stopped → connected(仅首次连接无消息时) → idle(收到第一条后) → working → idle ...
|
|
247
|
+
function agentStateBadge(s, agStatus, connStatus) {
|
|
248
|
+
if (agStatus === 'stopped' || connStatus === 'disconnected' || connStatus === 'failed')
|
|
249
|
+
return '<span class="state-badge stopped">停止</span>';
|
|
250
|
+
if (connStatus === 'reconnecting')
|
|
251
|
+
return '<span class="state-badge stopped">重连中</span>';
|
|
252
|
+
if ((s.processing || 0) > 0)
|
|
253
|
+
return '<span class="state-badge working">working</span>';
|
|
254
|
+
// 收到过消息 → 永远是 idle,不再回到 connected
|
|
255
|
+
if ((s.messagesReceived || 0) > 0 || (s.messagesSent || 0) > 0)
|
|
256
|
+
return '<span class="state-badge idle">idle</span>';
|
|
257
|
+
return '<span class="state-badge connected">connected</span>';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 发送方式图标标记
|
|
261
|
+
const MSG_KIND_META = { send: { icon: '💬', label: '回复' }, thought: { icon: '💭', label: '思考' }, inject: { icon: '📥', label: '注入' }, notify: { icon: '🔔', label: '通知' } };
|
|
262
|
+
function msgTagsHtml(kind, encrypt, chatmode) {
|
|
263
|
+
let h = '';
|
|
264
|
+
const km = MSG_KIND_META[kind];
|
|
265
|
+
if (km) h += `<span class="mtag">${km.icon}${km.label}</span>`;
|
|
266
|
+
if (encrypt != null) h += `<span class="mtag">${encrypt ? '🔒密文' : '明文'}</span>`;
|
|
267
|
+
if (chatmode) h += `<span class="mtag">${chatmode === 'proactive' ? '自主' : (chatmode === 'inject' ? '注入' : '响应')}</span>`;
|
|
268
|
+
return h;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 消息行:方向箭头 + 标记 + 对端 + 文字
|
|
272
|
+
function agentPreviewHtml(s) {
|
|
273
|
+
const clip = (t) => esc(String(t).replace(/\n/g, ' ').slice(0, 80));
|
|
274
|
+
const line = (dir, peer, text, kind, encrypt, chatmode) => {
|
|
275
|
+
const arrow = dir === 'in' ? '<span class="arrow-in">↓</span>' : '<span class="arrow-out">↑</span>';
|
|
276
|
+
const tags = msgTagsHtml(kind, encrypt, chatmode);
|
|
277
|
+
const peerHtml = peer ? `<span class="peer">${esc(shortAid(peer))}</span>: ` : '';
|
|
278
|
+
const textCls = dir === 'in' ? 'text-in' : 'text-out';
|
|
279
|
+
return `${arrow}${tags ? ' ' + tags + ' ' : ' '}${peerHtml}<span class="${textCls}">${clip(text)}</span>`;
|
|
280
|
+
};
|
|
281
|
+
if ((s.processing || 0) > 0 && s.lastReceivedText)
|
|
282
|
+
return line('in', s.lastReceivedFrom, s.lastReceivedText, s.lastReceivedKind, s.lastReceivedEncrypt, s.lastReceivedChatmode);
|
|
283
|
+
const recvTs = s.lastReceivedAt || 0, sentTs = s.lastSentAt || 0;
|
|
284
|
+
if (!recvTs && !sentTs) return '';
|
|
285
|
+
if (sentTs > recvTs && s.lastSentText)
|
|
286
|
+
return line('out', s.lastSentTo, s.lastSentText, s.lastSentKind, s.lastSentEncrypt, s.lastSentChatmode);
|
|
287
|
+
if (s.lastReceivedText)
|
|
288
|
+
return line('in', s.lastReceivedFrom, s.lastReceivedText, s.lastReceivedKind, s.lastReceivedEncrypt, s.lastReceivedChatmode);
|
|
289
|
+
return '';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// HTML tooltip(最近 N 轮):彩色箭头 + 方式 + 对端 + 文字
|
|
293
|
+
function recentMsgTooltipHtml(recent) {
|
|
294
|
+
if (!recent || !recent.length) return '';
|
|
295
|
+
let h = '<div class="msg-tip">';
|
|
296
|
+
for (const m of recent) {
|
|
297
|
+
const rcls = m.dir === 'in' ? 'tip-row-in' : 'tip-row-out';
|
|
298
|
+
const arrow = m.dir === 'in' ? '↓' : '↑';
|
|
299
|
+
const km = MSG_KIND_META[m.kind];
|
|
300
|
+
const kh = km ? `<span class="tip-kind">${km.icon}${km.label}</span>` : '';
|
|
301
|
+
const enc = m.encrypt != null ? `<span class="tip-flag">${m.encrypt ? '🔒密文' : '明文'}</span>` : '';
|
|
302
|
+
const mode = m.chatmode ? `<span class="tip-flag">${m.chatmode === 'proactive' ? '自主' : (m.chatmode === 'inject' ? '注入' : '响应')}</span>` : '';
|
|
303
|
+
const peer = m.peer ? esc(shortAid(m.peer)) : '';
|
|
304
|
+
const text = esc(String(m.text).replace(/\n/g, ' ').slice(0, 60));
|
|
305
|
+
h += `<div class="tip-row ${rcls}">${arrow}${kh}${enc}${mode} <b>${peer}</b> ${text}</div>`;
|
|
306
|
+
}
|
|
307
|
+
return h + '</div>';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 顶部统计条:Gateway / AIDs total·connected·offline / Messages ↓↑ / Traffic ↓↑ / Version·PID·Uptime
|
|
311
|
+
function agentsStatsBar(data, aids, stats) {
|
|
312
|
+
const connected = aids.filter(a => (a.status || 'connected') === 'connected').length;
|
|
313
|
+
const offline = aids.length - connected;
|
|
314
|
+
let recv = 0, sent = 0, bin = 0, bout = 0;
|
|
315
|
+
for (const s of stats) {
|
|
316
|
+
recv += s.messagesReceived || 0; sent += s.messagesSent || 0;
|
|
317
|
+
bin += s.bytesReceived || 0; bout += s.bytesSent || 0;
|
|
318
|
+
}
|
|
319
|
+
const gws = [...new Set(aids.filter(a => a.gatewayUrl).map(a => a.gatewayUrl))];
|
|
320
|
+
const gw = gws.length ? gws.map(esc).join(', ') : '—';
|
|
321
|
+
const st = data.status || {};
|
|
322
|
+
const pid = st.pid != null ? st.pid : '—';
|
|
323
|
+
const uptime = st.uptime != null ? fmtDur(st.uptime / 1000) : '—';
|
|
324
|
+
const ver = data.version || '—';
|
|
325
|
+
|
|
326
|
+
let h = '<div class="agents-stats">';
|
|
327
|
+
h += `<span class="sg"><span class="sg-k">Gateway</span><span class="sg-gw">${gw}</span></span>`;
|
|
328
|
+
h += `<span class="sg"><span class="sg-k">AIDs</span>${aids.length} total · <span class="num-on">${connected} 在线</span>` +
|
|
329
|
+
`${offline ? ` · <span class="num-off">${offline} 离线</span>` : ''}</span>`;
|
|
330
|
+
h += `<span class="sg"><span class="sg-k">Messages</span><span class="in">↓${recv}</span> <span class="out">↑${sent}</span></span>`;
|
|
331
|
+
h += `<span class="sg"><span class="sg-k">Traffic</span><span class="in">↓${fmtBytes(bin)}</span> <span class="out">↑${fmtBytes(bout)}</span></span>`;
|
|
332
|
+
h += `<span class="sg"><span class="sg-k">Version</span>${esc(ver)} · <span class="sg-k">PID</span>${pid} · <span class="sg-k">Uptime</span>${uptime}</span>`;
|
|
333
|
+
h += '</div>';
|
|
334
|
+
return h;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 操作列 HTML(启用页):停止/启动 + 清空队列(conditional) + ···(禁用/重载/编辑/md/删除)
|
|
338
|
+
function agentOpsHtml(aid, ag, s) {
|
|
339
|
+
if (_agentOps.has(aid)) {
|
|
340
|
+
return `<div class="agent-ops agent-ops-busy"><span class="ops-busy-label">${esc(_agentOps.get(aid) || '操作中…')}</span></div>`;
|
|
341
|
+
}
|
|
342
|
+
const queued = s.queued || 0;
|
|
343
|
+
const running = ag.status === 'running';
|
|
344
|
+
let h = `<div class="agent-ops" data-aid="${esc(aid)}" data-status="${esc(ag.status)}">`;
|
|
345
|
+
if (running) h += `<button class="ctrl-btn ops-stop" data-op="stop">停止</button>`;
|
|
346
|
+
else h += `<button class="ctrl-btn ops-start" data-op="start">启动</button>`;
|
|
347
|
+
if (queued > 0) h += `<button class="ctrl-btn ops-clear-queue" data-op="clear-queue" title="清空 ${queued} 条待处理消息">清空队列</button>`;
|
|
348
|
+
h += `<div class="ops-more"><button class="ctrl-btn ops-more-btn" data-op="more">···</button>` +
|
|
349
|
+
`<div class="ops-dropdown">` +
|
|
350
|
+
`<button class="ops-dd-item" data-op="toggle">禁用</button>` +
|
|
351
|
+
`<button class="ops-dd-item" data-op="reload">重载配置</button>` +
|
|
352
|
+
`<button class="ops-dd-item" data-op="edit">编辑配置</button>` +
|
|
353
|
+
`<a class="ops-dd-item" href="https://${esc(aid)}/agent.md" target="_blank" rel="noopener">查看 agent.md ↗</a>` +
|
|
354
|
+
`<button class="ops-dd-item danger" data-op="delete">删除 Agent</button>` +
|
|
355
|
+
`</div></div>`;
|
|
356
|
+
h += '</div>';
|
|
357
|
+
return h;
|
|
358
|
+
}
|
|
359
|
+
|
|
235
360
|
function renderAgents(data) {
|
|
236
361
|
const el = $('#view-agents');
|
|
237
362
|
if (!data) { el.innerHTML = '<div class="empty">加载中…</div>'; return; }
|
|
238
|
-
if (
|
|
363
|
+
if (el.querySelector('.ops-more.open')) return;
|
|
364
|
+
|
|
365
|
+
const allAgents = data.agents || [];
|
|
239
366
|
const aids = data.aids || [];
|
|
240
367
|
const statsByAid = {};
|
|
241
368
|
for (const s of (data.stats || [])) statsByAid[s.aid] = s;
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
369
|
+
const aidConnByAid = {};
|
|
370
|
+
for (const a of aids) aidConnByAid[a.aid] = a;
|
|
371
|
+
|
|
372
|
+
const enabledCount = allAgents.filter(ag => ag.status !== 'disabled').length;
|
|
373
|
+
const disabledCount = allAgents.filter(ag => ag.status === 'disabled').length;
|
|
374
|
+
|
|
375
|
+
// 子标签栏
|
|
376
|
+
let html = '<div class="agents-toolbar">' +
|
|
377
|
+
`<div class="ag-subtabs">` +
|
|
378
|
+
`<button class="ag-subtab${_agSubtab === 'enabled' ? ' active' : ''}" data-subtab="enabled">启用 (${enabledCount})</button>` +
|
|
379
|
+
`<button class="ag-subtab${_agSubtab === 'disabled' ? ' active' : ''}" data-subtab="disabled">禁用 (${disabledCount})</button>` +
|
|
380
|
+
`</div>` +
|
|
381
|
+
`<button class="ctrl-btn" id="agent-new-btn">+ 新建</button>` +
|
|
382
|
+
'</div>';
|
|
245
383
|
|
|
246
|
-
let html = '<div class="agents-toolbar"><button class="ctrl-btn" id="agent-new-btn">+ 新建 Agent</button></div>';
|
|
247
384
|
if (!data.daemonRunning) {
|
|
248
385
|
html += '<div class="banner">⚠ EvolClaw 主进程未运行,仅显示最近活动记录</div>';
|
|
249
386
|
}
|
|
250
|
-
|
|
251
|
-
|
|
387
|
+
|
|
388
|
+
if (_agSubtab === 'disabled') {
|
|
389
|
+
const disabledAgents = allAgents.filter(ag => ag.status === 'disabled');
|
|
390
|
+
if (!disabledAgents.length) {
|
|
391
|
+
html += '<div class="empty">暂无禁用 Agent</div>';
|
|
392
|
+
} else {
|
|
393
|
+
html += '<table><thead><tr><th>Agent</th><th>项目路径</th><th>操作</th></tr></thead><tbody>';
|
|
394
|
+
for (const ag of disabledAgents) {
|
|
395
|
+
const busy = _agentOps.has(ag.aid);
|
|
396
|
+
const ops = busy
|
|
397
|
+
? `<div class="agent-ops agent-ops-busy"><span class="ops-busy-label">${esc(_agentOps.get(ag.aid) || '操作中…')}</span></div>`
|
|
398
|
+
: `<div class="agent-ops" data-aid="${esc(ag.aid)}" data-status="disabled"><button class="ctrl-btn ops-enable" data-op="toggle">启用</button></div>`;
|
|
399
|
+
html += `<tr class="ag-main">` +
|
|
400
|
+
`<td><div class="ag-id"><span class="dot off"></span><span class="ag-id-text"><span class="ag-name">${esc(ag.displayName || shortAid(ag.aid))}</span><span class="ag-aid">${esc(ag.aid)}</span></span></div></td>` +
|
|
401
|
+
`<td style="font-size:11px;font-family:monospace">${esc(ag.projectPath || '—')}</td>` +
|
|
402
|
+
`<td class="agent-ops-cell">${ops}</td></tr>`;
|
|
403
|
+
}
|
|
404
|
+
html += '</tbody></table>';
|
|
405
|
+
}
|
|
406
|
+
el.innerHTML = html;
|
|
407
|
+
bindAgentsEvents(el);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── 启用页 ──
|
|
412
|
+
// 按收发消息总数降序排序(活跃的排前面)
|
|
413
|
+
const totalMsgs = (ag) => {
|
|
414
|
+
const s = statsByAid[ag.aid] || {};
|
|
415
|
+
return (s.messagesReceived || 0) + (s.messagesSent || 0);
|
|
416
|
+
};
|
|
417
|
+
const enabledAgents = allAgents.filter(ag => ag.status !== 'disabled')
|
|
418
|
+
.sort((a, b) => totalMsgs(b) - totalMsgs(a));
|
|
419
|
+
if (!enabledAgents.length) {
|
|
420
|
+
html += '<div class="empty">暂无启用 Agent</div>';
|
|
252
421
|
el.innerHTML = html;
|
|
253
422
|
bindAgentsEvents(el);
|
|
254
423
|
return;
|
|
255
424
|
}
|
|
256
425
|
|
|
257
426
|
html += '<table><thead><tr>' +
|
|
258
|
-
'<th
|
|
259
|
-
'<th>入字节</th><th>出字节</th><th
|
|
427
|
+
'<th>AID</th><th>工作</th><th>队列</th><th>模型</th><th>运行</th><th>收</th><th>发</th>' +
|
|
428
|
+
'<th>入字节</th><th>出字节</th><th>对端数量</th><th>最后活动</th><th>操作</th>' +
|
|
260
429
|
'</tr></thead><tbody>';
|
|
261
430
|
|
|
262
|
-
for (const
|
|
263
|
-
const s = statsByAid[
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
ops = '<span style="color:var(--dim)">—</span>';
|
|
288
|
-
}
|
|
289
|
-
html += '<tr>' +
|
|
290
|
-
`<td><span class="dot ${dotCls}"></span>${esc(status)}</td>` +
|
|
291
|
-
`<td>${esc(shortAid(a.aid))}${name ? ` <span style="color:var(--dim)">(${esc(name)})</span>` : ''}</td>` +
|
|
431
|
+
for (const ag of enabledAgents) {
|
|
432
|
+
const s = statsByAid[ag.aid] || {};
|
|
433
|
+
const conn = aidConnByAid[ag.aid] || {};
|
|
434
|
+
const connStatus = conn.status || (ag.status === 'running' ? 'connected' : 'disconnected');
|
|
435
|
+
const dotCls = connStatus === 'connected' ? 'on' : (connStatus === 'reconnecting' ? 'idle' : 'off');
|
|
436
|
+
const name = s.selfName || ag.displayName || shortAid(ag.aid);
|
|
437
|
+
const uptime = (connStatus === 'connected' && conn.lastConnectedAt) ? fmtDur((Date.now() - conn.lastConnectedAt) / 1000) : '—';
|
|
438
|
+
const lastTs = Math.max(s.lastReceivedAt || 0, s.lastSentAt || 0, ag.lastActivity || 0);
|
|
439
|
+
const preview = agentPreviewHtml(s);
|
|
440
|
+
// 队列数:不含正在处理的那条
|
|
441
|
+
const rawQueued = s.queued || 0;
|
|
442
|
+
const queued = rawQueued;
|
|
443
|
+
const queueCell = queued > 0 ? `<span class="ag-queue-num">${queued}</span>` : '<span style="color:var(--dim)">0</span>';
|
|
444
|
+
const model = ag.model || ag.baseagent || '—';
|
|
445
|
+
|
|
446
|
+
const idCell = `<div class="ag-id"><span class="dot ${dotCls}" title="${esc(connStatus)}"></span>` +
|
|
447
|
+
`<span class="ag-id-text"><span class="ag-name">${esc(name)}</span>` +
|
|
448
|
+
`<span class="ag-aid">${esc(ag.aid)}</span></span></div>`;
|
|
449
|
+
|
|
450
|
+
html += `<tr class="ag-main">` +
|
|
451
|
+
`<td>${idCell}</td>` +
|
|
452
|
+
`<td>${agentStateBadge(s, ag.status, connStatus)}</td>` +
|
|
453
|
+
`<td>${queueCell}</td>` +
|
|
454
|
+
`<td style="font-size:11px;color:var(--dim)">${esc(model)}</td>` +
|
|
455
|
+
`<td>${uptime}</td>` +
|
|
292
456
|
`<td>${s.messagesReceived ?? 0}</td><td>${s.messagesSent ?? 0}</td>` +
|
|
293
|
-
`<td>${s.systemReceived ?? 0}/${s.systemSent ?? 0}</td>` +
|
|
294
457
|
`<td>${fmtBytes(s.bytesReceived)}</td><td>${fmtBytes(s.bytesSent)}</td>` +
|
|
295
|
-
`<td>${s.uniquePeerCount ??
|
|
458
|
+
`<td>${s.uniquePeerCount ?? conn.peerCount ?? 0}</td>` +
|
|
296
459
|
`<td>${fmtAgo(lastTs)}</td>` +
|
|
297
|
-
`<td class="
|
|
298
|
-
`<td class="agent-ops-cell">${ops}</td>` +
|
|
460
|
+
`<td class="agent-ops-cell">${agentOpsHtml(ag.aid, ag, s)}</td>` +
|
|
299
461
|
'</tr>';
|
|
462
|
+
// 自定义 tooltip(HTML,hover 显示)
|
|
463
|
+
const recent = (s.recentMessages || []);
|
|
464
|
+
const tipHtml = recentMsgTooltipHtml(recent);
|
|
465
|
+
|
|
466
|
+
html += `<tr class="ag-sub"><td colspan="12"><div class="ag-info">` +
|
|
467
|
+
(ag.projectPath ? `<div class="ag-path">${esc(ag.projectPath)}</div>` : '') +
|
|
468
|
+
(preview ? `<div class="ag-msg-wrap">${tipHtml}<div class="ag-msg">${preview}</div></div>` : '') +
|
|
469
|
+
'</div></td></tr>';
|
|
300
470
|
}
|
|
301
471
|
html += '</tbody></table>';
|
|
472
|
+
if (data.daemonRunning) {
|
|
473
|
+
html += agentsStatsBar(data, aids, data.stats || []);
|
|
474
|
+
}
|
|
302
475
|
el.innerHTML = html;
|
|
303
476
|
bindAgentsEvents(el);
|
|
304
477
|
}
|
|
@@ -502,17 +675,19 @@ function renderSession(data) {
|
|
|
502
675
|
|
|
503
676
|
// 搜索过滤
|
|
504
677
|
const q = sessSearch.trim().toLowerCase();
|
|
505
|
-
const filtered =
|
|
506
|
-
|
|
507
|
-
|
|
678
|
+
const filtered = transcripts
|
|
679
|
+
.filter(t => !sessFilterNormal || (t.userMsgs || 0) >= 2)
|
|
680
|
+
.filter(t => !q || (t.title || '').toLowerCase().includes(q) || (t.firstUser || '').toLowerCase().includes(q));
|
|
508
681
|
|
|
509
682
|
// 左栏:过滤条 + 列表
|
|
510
683
|
const projOpts = projects.map(p =>
|
|
511
684
|
`<option value="${esc(p.encoded)}"${p.encoded === sessSel.project ? ' selected' : ''}>${esc(p.label)} (${p.count})</option>`
|
|
512
685
|
).join('');
|
|
686
|
+
const normalCount = transcripts.filter(t => (t.userMsgs || 0) >= 2).length;
|
|
513
687
|
let listHtml = '<div class="sess-filter">' +
|
|
514
688
|
`<select id="sess-project">${projOpts}</select>` +
|
|
515
689
|
`<input id="sess-search" type="text" placeholder="搜索标题/首条消息…" value="${esc(sessSearch)}">` +
|
|
690
|
+
`<button id="sess-filter-btn" class="ctrl-btn${sessFilterNormal ? ' active' : ''}" title="只显示有效会话(≥2 条用户消息)">有效 ${normalCount}</button>` +
|
|
516
691
|
`<div class="sess-count">${filtered.length} / ${transcripts.length} 个会话</div></div>` +
|
|
517
692
|
'<div class="sess-items">';
|
|
518
693
|
|
|
@@ -543,6 +718,8 @@ function renderSession(data) {
|
|
|
543
718
|
sessSearch = '';
|
|
544
719
|
subscribe('session', { project: sessSel.project });
|
|
545
720
|
};
|
|
721
|
+
const filterBtn = $('#sess-filter-btn');
|
|
722
|
+
if (filterBtn) filterBtn.onclick = () => { sessFilterNormal = !sessFilterNormal; renderSession(state.session); };
|
|
546
723
|
const searchEl = $('#sess-search');
|
|
547
724
|
if (searchEl) {
|
|
548
725
|
searchEl.oninput = () => { sessSearch = searchEl.value; renderSession(state.session); };
|
|
@@ -758,65 +935,130 @@ function toast(text, isErr) {
|
|
|
758
935
|
}
|
|
759
936
|
|
|
760
937
|
// ── Agents 操作 ──
|
|
761
|
-
|
|
938
|
+
// (_agentBusy 已在 Agents 视图顶部声明,仅 agentOpNew 仍在用)
|
|
939
|
+
|
|
940
|
+
// 设置某 aid 的操作状态并立即刷新对应行的按钮区(不重渲整表)
|
|
941
|
+
function setAgentOp(aid, label) {
|
|
942
|
+
if (label == null) _agentOps.delete(aid); else _agentOps.set(aid, label);
|
|
943
|
+
const cell = document.querySelector(`.agent-ops[data-aid="${CSS.escape(aid)}"], .agent-ops-busy[data-aid="${CSS.escape(aid)}"]`)?.closest('td');
|
|
944
|
+
if (!cell || !state.agents) return;
|
|
945
|
+
const ag = (state.agents.agents || []).find(x => x.aid === aid);
|
|
946
|
+
if (!ag) return;
|
|
947
|
+
if (ag.status === 'disabled') {
|
|
948
|
+
// 禁用页:只有启用按钮 / 操作中态
|
|
949
|
+
cell.innerHTML = _agentOps.has(aid)
|
|
950
|
+
? `<div class="agent-ops agent-ops-busy"><span class="ops-busy-label">${esc(_agentOps.get(aid) || '操作中…')}</span></div>`
|
|
951
|
+
: `<div class="agent-ops" data-aid="${esc(aid)}" data-status="disabled"><button class="ctrl-btn ops-enable" data-op="toggle">启用</button></div>`;
|
|
952
|
+
} else {
|
|
953
|
+
const statsByAid = {};
|
|
954
|
+
for (const s of (state.agents.stats || [])) statsByAid[s.aid] = s;
|
|
955
|
+
cell.innerHTML = agentOpsHtml(aid, ag, statsByAid[aid] || {});
|
|
956
|
+
}
|
|
957
|
+
bindOpsCell(cell, aid, ag.status);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function bindOpsCell(cell, aid, status) {
|
|
961
|
+
cell.querySelectorAll('button[data-op]').forEach(btn => {
|
|
962
|
+
btn.addEventListener('click', (e) => {
|
|
963
|
+
const op = btn.dataset.op;
|
|
964
|
+
if (op === 'more') {
|
|
965
|
+
const more = btn.closest('.ops-more');
|
|
966
|
+
const wasOpen = more.classList.contains('open');
|
|
967
|
+
document.querySelectorAll('.ops-more.open').forEach(m => m.classList.remove('open'));
|
|
968
|
+
if (!wasOpen) more.classList.add('open');
|
|
969
|
+
e.stopPropagation();
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (op === 'edit') agentOpEdit(aid);
|
|
973
|
+
else if (op === 'reload') agentOpReload(aid);
|
|
974
|
+
else if (op === 'toggle') agentOpToggle(aid, status);
|
|
975
|
+
else if (op === 'delete') agentOpDelete(aid);
|
|
976
|
+
else if (op === 'clear-queue') agentOpClearQueue(aid);
|
|
977
|
+
else if (op === 'stop') agentOpStop(aid);
|
|
978
|
+
else if (op === 'start') agentOpStart(aid);
|
|
979
|
+
else if (op === 'mute') agentOpMute(aid);
|
|
980
|
+
else if (op === 'unmute') agentOpUnmute(aid);
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// click-outside 关闭下拉:全局只绑一次(避免每次重渲染叠加监听器)
|
|
986
|
+
let _opsOutsideBound = false;
|
|
987
|
+
function ensureOpsOutsideClose() {
|
|
988
|
+
if (_opsOutsideBound) return;
|
|
989
|
+
_opsOutsideBound = true;
|
|
990
|
+
document.addEventListener('click', (e) => {
|
|
991
|
+
if (e.target.closest && e.target.closest('.ops-more')) return; // 点在菜单内不关
|
|
992
|
+
document.querySelectorAll('.ops-more.open').forEach(m => m.classList.remove('open'));
|
|
993
|
+
});
|
|
994
|
+
}
|
|
762
995
|
|
|
763
996
|
function bindAgentsEvents(el) {
|
|
764
997
|
el.querySelector('#agent-new-btn')?.addEventListener('click', agentOpNew);
|
|
998
|
+
ensureOpsOutsideClose();
|
|
999
|
+
// 子标签切换:仅切视图变量并重渲,不重新订阅
|
|
1000
|
+
el.querySelectorAll('.ag-subtab').forEach(btn => {
|
|
1001
|
+
btn.addEventListener('click', () => {
|
|
1002
|
+
const tab = btn.dataset.subtab;
|
|
1003
|
+
if (tab && tab !== _agSubtab) { _agSubtab = tab; renderAgents(state.agents); }
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
765
1006
|
el.querySelectorAll('.agent-ops').forEach(div => {
|
|
766
1007
|
const aid = div.dataset.aid;
|
|
767
1008
|
const status = div.dataset.status;
|
|
768
|
-
div.
|
|
769
|
-
btn.addEventListener('click', () => {
|
|
770
|
-
const op = btn.dataset.op;
|
|
771
|
-
if (op === 'edit') agentOpEdit(aid);
|
|
772
|
-
else if (op === 'reload') agentOpReload(aid);
|
|
773
|
-
else if (op === 'toggle') agentOpToggle(aid, status);
|
|
774
|
-
else if (op === 'delete') agentOpDelete(aid);
|
|
775
|
-
});
|
|
776
|
-
});
|
|
1009
|
+
bindOpsCell(div.closest('td'), aid, status);
|
|
777
1010
|
});
|
|
778
1011
|
}
|
|
779
1012
|
|
|
1013
|
+
// 异步操作包装:设置 "操作中" 状态、执行、清除
|
|
1014
|
+
async function withAgentOp(aid, label, fn) {
|
|
1015
|
+
setAgentOp(aid, label);
|
|
1016
|
+
try { await fn(); }
|
|
1017
|
+
finally { setAgentOp(aid, null); }
|
|
1018
|
+
}
|
|
1019
|
+
|
|
780
1020
|
async function agentOpReload(aid, force = false) {
|
|
781
|
-
|
|
782
|
-
try {
|
|
1021
|
+
await withAgentOp(aid, '重载中…', async () => {
|
|
783
1022
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'reload', args: { aid, force } }));
|
|
784
1023
|
if (r.error?.code === 'BUSY') {
|
|
785
|
-
if (confirm(r.error.message + '\n确认强制重载?')) return agentOpReload(aid, true);
|
|
1024
|
+
if (confirm(r.error.message + '\n确认强制重载?')) { setAgentOp(aid, null); return agentOpReload(aid, true); }
|
|
786
1025
|
return;
|
|
787
1026
|
}
|
|
788
1027
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
789
1028
|
toast('✓ 已重载');
|
|
790
1029
|
subscribe('agents', {});
|
|
791
|
-
}
|
|
792
|
-
finally { _agentBusy = false; }
|
|
1030
|
+
});
|
|
793
1031
|
}
|
|
794
1032
|
|
|
795
1033
|
async function agentOpToggle(aid, status) {
|
|
796
1034
|
const action = status === 'disabled' ? 'enable' : 'disable';
|
|
797
|
-
|
|
798
|
-
|
|
1035
|
+
const label = action === 'disable' ? '禁用中…' : '启用中…';
|
|
1036
|
+
await withAgentOp(aid, label, async () => {
|
|
799
1037
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action, args: { aid } }));
|
|
800
1038
|
if (r.error?.code === 'BUSY') {
|
|
801
1039
|
if (confirm(r.error.message + `\n确认强制${action === 'disable' ? '禁用' : '启用'}?`)) {
|
|
802
1040
|
const r2 = mResp(await menuSend({ type: 'menu.action', name: 'agent', action, args: { aid, force: true } }));
|
|
803
1041
|
if (r2.error) toast(r2.error.message || r2.error.code, true);
|
|
804
|
-
else {
|
|
1042
|
+
else {
|
|
1043
|
+
toast(`✓ 已${action === 'disable' ? '禁用' : '启用'}`);
|
|
1044
|
+
// 禁用后立即切到禁用页;启用后等数据刷新(agent 需先完成启动才移到启用页)
|
|
1045
|
+
if (action === 'disable') _agSubtab = 'disabled';
|
|
1046
|
+
subscribe('agents', {});
|
|
1047
|
+
}
|
|
805
1048
|
}
|
|
806
1049
|
return;
|
|
807
1050
|
}
|
|
808
1051
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
809
1052
|
toast(`✓ 已${action === 'disable' ? '禁用' : '启用'}`);
|
|
1053
|
+
if (action === 'disable') _agSubtab = 'disabled';
|
|
810
1054
|
subscribe('agents', {});
|
|
811
|
-
}
|
|
812
|
-
finally { _agentBusy = false; }
|
|
1055
|
+
});
|
|
813
1056
|
}
|
|
814
1057
|
|
|
815
1058
|
async function agentOpDelete(aid) {
|
|
816
1059
|
if (!confirm(`删除 Agent ${aid}?\n此操作不可恢复。`)) return;
|
|
817
1060
|
const purge = confirm('同时清除 agent 数据目录?');
|
|
818
|
-
|
|
819
|
-
try {
|
|
1061
|
+
await withAgentOp(aid, '删除中…', async () => {
|
|
820
1062
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'delete', args: { aid, purge } }));
|
|
821
1063
|
if (r.error?.code === 'BUSY') {
|
|
822
1064
|
if (confirm(r.error.message + '\n确认强制删除?')) {
|
|
@@ -829,8 +1071,53 @@ async function agentOpDelete(aid) {
|
|
|
829
1071
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
830
1072
|
toast('✓ 已删除');
|
|
831
1073
|
subscribe('agents', {});
|
|
832
|
-
}
|
|
833
|
-
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async function agentOpClearQueue(aid) {
|
|
1078
|
+
if (!confirm(`清空 ${aid} 的待处理消息队列?`)) return;
|
|
1079
|
+
await withAgentOp(aid, '清空中…', async () => {
|
|
1080
|
+
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'queue-clear', args: { aid } }));
|
|
1081
|
+
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1082
|
+
toast(`✓ 已清空 ${r.data?.cleared ?? 0} 条待处理消息`);
|
|
1083
|
+
subscribe('agents', {});
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async function agentOpStop(aid) {
|
|
1088
|
+
await withAgentOp(aid, '停止中…', async () => {
|
|
1089
|
+
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'stop', args: { aid } }));
|
|
1090
|
+
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1091
|
+
toast('✓ 已停止');
|
|
1092
|
+
subscribe('agents', {});
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
async function agentOpStart(aid) {
|
|
1097
|
+
await withAgentOp(aid, '启动中…', async () => {
|
|
1098
|
+
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'start', args: { aid } }));
|
|
1099
|
+
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1100
|
+
toast('✓ 已启动');
|
|
1101
|
+
subscribe('agents', {});
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
async function agentOpMute(aid) {
|
|
1106
|
+
await withAgentOp(aid, '禁言中…', async () => {
|
|
1107
|
+
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'mute', args: { aid } }));
|
|
1108
|
+
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1109
|
+
toast('✓ 已禁言');
|
|
1110
|
+
subscribe('agents', {});
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
async function agentOpUnmute(aid) {
|
|
1115
|
+
await withAgentOp(aid, '解禁中…', async () => {
|
|
1116
|
+
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'unmute', args: { aid } }));
|
|
1117
|
+
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1118
|
+
toast('✓ 已解禁');
|
|
1119
|
+
subscribe('agents', {});
|
|
1120
|
+
});
|
|
834
1121
|
}
|
|
835
1122
|
|
|
836
1123
|
async function agentOpNew() {
|
|
@@ -849,29 +1136,61 @@ async function agentOpNew() {
|
|
|
849
1136
|
}
|
|
850
1137
|
|
|
851
1138
|
async function agentOpEdit(aid) {
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
const [qr] = await Promise.all([
|
|
855
|
-
menuSend({ type: 'menu.query', name: 'agent', args: { aid } }),
|
|
856
|
-
]);
|
|
1139
|
+
await withAgentOp(aid, '查询中…', async () => {
|
|
1140
|
+
const qr = await menuSend({ type: 'menu.query', name: 'agent', args: { aid } });
|
|
857
1141
|
const q = mResp(qr);
|
|
858
|
-
if (q.error) { toast(q.error.message || q.error.code, true);
|
|
1142
|
+
if (q.error) { toast(q.error.message || q.error.code, true); return; }
|
|
859
1143
|
const cfg = q.data;
|
|
860
|
-
//
|
|
1144
|
+
setAgentOp(aid, null); // 查询完毕先恢复,等用户填完 prompt
|
|
861
1145
|
const projectRaw = prompt('项目路径:', cfg.config?.projects?.defaultPath || '');
|
|
862
1146
|
const ownersRaw = prompt('Owners(逗号分隔 AID):', (cfg.config?.owners || []).join(', '));
|
|
863
1147
|
const patch = {};
|
|
864
1148
|
if (projectRaw !== null) patch.projects = { defaultPath: projectRaw };
|
|
865
1149
|
if (ownersRaw !== null) patch.owners = ownersRaw.split(',').map(s => s.trim()).filter(Boolean);
|
|
866
|
-
if (Object.keys(patch).length
|
|
1150
|
+
if (!Object.keys(patch).length) return;
|
|
1151
|
+
setAgentOp(aid, '保存中…');
|
|
867
1152
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'update', args: { aid, patch } }));
|
|
868
1153
|
if (r.error) toast(r.error.message || r.error.code, true);
|
|
869
1154
|
else toast('✓ 配置已保存,点「重载」生效');
|
|
870
|
-
}
|
|
871
|
-
finally { _agentBusy = false; }
|
|
1155
|
+
});
|
|
872
1156
|
}
|
|
873
1157
|
|
|
874
1158
|
// ── System 视图 ──
|
|
1159
|
+
function channelHealthRow(c) {
|
|
1160
|
+
const dot = c.connected ? 'on' : (c.aidStatus === 'reconnecting' || c.aidStatus === 'kicked' ? 'idle' : 'off');
|
|
1161
|
+
let meta = '';
|
|
1162
|
+
if (c.aidStatus && c.aidStatus !== 'connected') meta += ` <span style="color:var(--dim)">${esc(c.aidStatus)}</span>`;
|
|
1163
|
+
if (c.reconnectCount > 0) meta += ` <span style="color:var(--dim)">重连 ${c.reconnectCount}</span>`;
|
|
1164
|
+
if (c.flapCount > 0) meta += ` <span style="color:var(--red)">抖动 ${c.flapCount}</span>`;
|
|
1165
|
+
const reason = c.kickReason || c.lastError;
|
|
1166
|
+
if (reason && !c.connected) meta += ` <span style="color:var(--red)" title="${esc(reason)}">"${esc(reason)}"</span>`;
|
|
1167
|
+
return `<div class="ch-row"><span class="dot ${dot}"></span>${esc(c.type)}${c.instName && c.instName !== c.type ? ' ' + esc(c.instName) : ''}${meta}</div>`;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function agentHealthCard(ag) {
|
|
1171
|
+
const dot = ag.status === 'running' ? 'on' : ag.status === 'disabled' ? 'idle' : 'off';
|
|
1172
|
+
let h = `<div class="agent-health-card">`;
|
|
1173
|
+
h += `<div class="ahc-head"><span class="dot ${dot}"></span><span class="ahc-aid">${esc(ag.aid || ag.name)}</span><span class="ahc-status">${esc(ag.status)}</span></div>`;
|
|
1174
|
+
// 项目路径
|
|
1175
|
+
if (ag.projectPath) h += `<div class="ahc-row"><span class="ahc-k">项目</span><span class="ahc-v ahc-path" title="${esc(ag.projectPath)}">${esc(ag.projectPath)}</span></div>`;
|
|
1176
|
+
// 后端
|
|
1177
|
+
const backend = [ag.baseagent, ag.model, ag.effort].filter(Boolean).map(esc).join(' · ');
|
|
1178
|
+
h += `<div class="ahc-row"><span class="ahc-k">后端</span><span class="ahc-v">${backend || '—'}</span></div>`;
|
|
1179
|
+
// 渠道
|
|
1180
|
+
let chans = '';
|
|
1181
|
+
for (const c of (ag.channels || [])) chans += channelHealthRow(c);
|
|
1182
|
+
h += `<div class="ahc-row"><span class="ahc-k">渠道</span><span class="ahc-v">${chans || '<span style="color:var(--dim)">无</span>'}</span></div>`;
|
|
1183
|
+
// 负载
|
|
1184
|
+
const load = `${ag.processing ?? 0} 处理中 · ${ag.pending ?? 0} 待处理 · ${ag.activeSessions ?? 0} 会话`;
|
|
1185
|
+
h += `<div class="ahc-row"><span class="ahc-k">负载</span><span class="ahc-v">${load}</span></div>`;
|
|
1186
|
+
// 活动
|
|
1187
|
+
if (ag.lastActivity) h += `<div class="ahc-row"><span class="ahc-k">活动</span><span class="ahc-v">${fmtAgo(ag.lastActivity)} 前</span></div>`;
|
|
1188
|
+
// 错误
|
|
1189
|
+
if (ag.error) h += `<div class="ahc-err">⚠ ${esc(String(ag.error).slice(0, 120))}</div>`;
|
|
1190
|
+
h += '</div>';
|
|
1191
|
+
return h;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
875
1194
|
function renderSystem(data) {
|
|
876
1195
|
const el = $('#view-system');
|
|
877
1196
|
if (!data) { el.innerHTML = '<div class="empty">加载中…</div>'; return; }
|
|
@@ -913,45 +1232,26 @@ function renderSystem(data) {
|
|
|
913
1232
|
// ③ 健康快照
|
|
914
1233
|
if (chk) {
|
|
915
1234
|
html += '<div class="sys-health">';
|
|
916
|
-
//
|
|
917
|
-
html += '<div style="
|
|
918
|
-
html +=
|
|
919
|
-
for (const ch of (chk.channels || [])) {
|
|
920
|
-
for (const inst of (ch.instances || [])) {
|
|
921
|
-
const dot = inst.connected ? 'on' : 'idle';
|
|
922
|
-
html += `<div><span class="dot ${dot}"></span>${esc(ch.type)}${inst.name !== ch.type ? ' ' + esc(inst.name) : ''}</div>`;
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
html += '</div>';
|
|
926
|
-
html += `<div class="cache-card"><div class="card-label">队列</div><div>待处理 ${chk.queue?.pending ?? 0}</div><div>处理中 ${chk.queue?.processing ?? 0}</div></div>`;
|
|
927
|
-
html += '</div>';
|
|
928
|
-
// 近 1 小时
|
|
1235
|
+
// 队列 + 近 1 小时(数字卡片同一行)
|
|
1236
|
+
html += '<div class="cache-cards" style="margin-bottom:8px">';
|
|
1237
|
+
html += `<div class="cache-card"><div class="card-label">队列</div><div class="card-val">${chk.queue?.pending ?? 0} 待 · ${chk.queue?.processing ?? 0} 处理中</div></div>`;
|
|
929
1238
|
const h = chk.lastHour;
|
|
930
1239
|
if (h) {
|
|
931
1240
|
const errDetail = h.errors > 0 ? ` (${Object.entries(h.errorsByType || {}).map(([t, c]) => `${t}:${c}`).join(', ')})` : '';
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
`<div>中断 ${h.interrupts}${h.completed > 0 ? ' · 平均 ' + (h.avgResponseMs / 1000).toFixed(1) + 's' : ''}</div>` +
|
|
935
|
-
`</div>`;
|
|
1241
|
+
const avg = h.completed > 0 ? ` · 均 ${(h.avgResponseMs / 1000).toFixed(1)}s` : '';
|
|
1242
|
+
html += `<div class="cache-card"><div class="card-label">近 1 小时</div><div class="card-val">收 ${h.received} · 完 ${h.completed} · 错 ${h.errors}${errDetail} · 断 ${h.interrupts}${avg}</div></div>`;
|
|
936
1243
|
}
|
|
937
|
-
|
|
1244
|
+
html += '</div>';
|
|
1245
|
+
// 每个 EvolAgent 一张卡片:后端 + 渠道健康 + 负载
|
|
938
1246
|
if (chk.evolagents?.length) {
|
|
939
|
-
html += '<div class="
|
|
940
|
-
for (const ag of chk.evolagents)
|
|
941
|
-
const dot = ag.status === 'running' ? 'on' : ag.status === 'disabled' ? 'idle' : 'off';
|
|
942
|
-
const tasks = ag.activeTasks > 0 ? ` · ${ag.activeTasks} 任务` : '';
|
|
943
|
-
const err = ag.error ? ` <span style="color:var(--red)">${esc(ag.error.slice(0, 60))}</span>` : '';
|
|
944
|
-
html += `<div><span class="dot ${dot}"></span>${esc(ag.name)} ${esc(ag.status)}${tasks}${err}</div>`;
|
|
945
|
-
}
|
|
1247
|
+
html += '<div class="agent-health-grid">';
|
|
1248
|
+
for (const ag of chk.evolagents) html += agentHealthCard(ag);
|
|
946
1249
|
html += '</div>';
|
|
947
1250
|
}
|
|
948
|
-
//
|
|
949
|
-
if (chk.
|
|
950
|
-
html += '<div class="cache-card"><div class="card-label"
|
|
951
|
-
for (const
|
|
952
|
-
const dot = ba.healthy ? (ba.activeStreams > 0 ? 'on' : 'idle') : 'off';
|
|
953
|
-
html += `<div><span class="dot ${dot}"></span>${esc(ba.name)} · 流 ${ba.activeStreams}</div>`;
|
|
954
|
-
}
|
|
1251
|
+
// 未归属任何 EvolAgent 的渠道(系统级 / DefaultAgent)
|
|
1252
|
+
if (chk.unownedChannels?.length) {
|
|
1253
|
+
html += '<div class="cache-card" style="margin-top:8px"><div class="card-label">未归属渠道</div>';
|
|
1254
|
+
for (const c of chk.unownedChannels) html += channelHealthRow(c);
|
|
955
1255
|
html += '</div>';
|
|
956
1256
|
}
|
|
957
1257
|
html += '</div>';
|
|
@@ -1102,7 +1402,11 @@ function initTheme() {
|
|
|
1102
1402
|
btn.textContent = next === 'dark' ? '☀️' : '🌙';
|
|
1103
1403
|
if (_hourlyChart) { _hourlyChart.dispose(); _hourlyChart = null; }
|
|
1104
1404
|
if (_modelChart) { _modelChart.dispose(); _modelChart = null; }
|
|
1405
|
+
['_monCpu', '_monMem', '_monMsg', '_monErr'].forEach(function (k) {
|
|
1406
|
+
if (window[k]) { window[k].dispose(); window[k] = null; }
|
|
1407
|
+
});
|
|
1105
1408
|
loadUsageDashboard();
|
|
1409
|
+
if (currentView === 'monitor') renderMonitor(state.monitor);
|
|
1106
1410
|
};
|
|
1107
1411
|
}
|
|
1108
1412
|
}
|
|
@@ -1184,12 +1488,6 @@ async function loadUsageDashboard() {
|
|
|
1184
1488
|
}).join('') + '</tbody>';
|
|
1185
1489
|
}
|
|
1186
1490
|
|
|
1187
|
-
// Topbar today cost
|
|
1188
|
-
var costEl = $('#today-cost');
|
|
1189
|
-
if (costEl) {
|
|
1190
|
-
var totalTokens = t.input_tokens + t.output_tokens;
|
|
1191
|
-
costEl.textContent = 'Today: ' + fmtTokens(totalTokens) + ' tokens · ' + t.call_count + ' calls';
|
|
1192
|
-
}
|
|
1193
1491
|
}
|
|
1194
1492
|
|
|
1195
1493
|
// ── Usage Overview(全时段总览)──
|
|
@@ -1444,6 +1742,220 @@ async function runExplorerQuery() {
|
|
|
1444
1742
|
}
|
|
1445
1743
|
}
|
|
1446
1744
|
|
|
1745
|
+
// ── Monitor ──────────────────────────────────────
|
|
1746
|
+
// 绑定时间范围切换按钮(只绑一次)
|
|
1747
|
+
let _monRangeBound = false;
|
|
1748
|
+
function bindMonRangeTabs() {
|
|
1749
|
+
if (_monRangeBound) return;
|
|
1750
|
+
var tabs = document.querySelectorAll('#view-monitor .mon-range');
|
|
1751
|
+
if (!tabs.length) return;
|
|
1752
|
+
tabs.forEach(function (btn) {
|
|
1753
|
+
btn.onclick = function () {
|
|
1754
|
+
monRange = btn.dataset.range;
|
|
1755
|
+
document.querySelectorAll('#view-monitor .mon-range').forEach(function (b) {
|
|
1756
|
+
b.classList.toggle('active', b.dataset.range === monRange);
|
|
1757
|
+
});
|
|
1758
|
+
// 切范围 → 重新订阅(源按 range 返回不同分辨率的 history)
|
|
1759
|
+
subscribe('monitor', { range: monRange });
|
|
1760
|
+
};
|
|
1761
|
+
});
|
|
1762
|
+
_monRangeBound = true;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
function renderMonitor(data) {
|
|
1766
|
+
var wrap = $('#view-monitor .mon-layout');
|
|
1767
|
+
if (!wrap) return;
|
|
1768
|
+
bindMonRangeTabs();
|
|
1769
|
+
if (!data) { return; }
|
|
1770
|
+
if (!data.daemonRunning) {
|
|
1771
|
+
// 不清空骨架,仅在卡片区提示,避免破坏 toolbar
|
|
1772
|
+
var cardsEl0 = $('#mon-cards');
|
|
1773
|
+
if (cardsEl0) cardsEl0.innerHTML = '<div class="empty" style="grid-column:1/-1">daemon 未运行</div>';
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
var s = data.snapshot;
|
|
1778
|
+
// history 是三档分辨率对象 { fine, mid, coarse };按当前范围选一档
|
|
1779
|
+
var rangeKey = { '2m': 'fine', '10m': 'mid', '1h': 'coarse' }[monRange] || 'fine';
|
|
1780
|
+
var hist = data.history || {};
|
|
1781
|
+
var h = Array.isArray(hist) ? hist : (hist[rangeKey] || []);
|
|
1782
|
+
var sys = s.system || {};
|
|
1783
|
+
var lh = (s.stats && s.stats.lastHour) || {};
|
|
1784
|
+
var recentErrs = (s.stats && s.stats.recentErrors) || [];
|
|
1785
|
+
var errRate = (lh.received > 0) ? ((lh.errors / lh.received) * 100).toFixed(1) + '%' : '0%';
|
|
1786
|
+
var agents = s.agents || [];
|
|
1787
|
+
var connected = agents.filter(function (a) { return a.status === 'connected'; }).length;
|
|
1788
|
+
|
|
1789
|
+
// ── Stat cards ──
|
|
1790
|
+
var sysMemPct = (sys.memTotal > 0) ? Math.round((sys.memUsed / sys.memTotal) * 100) : 0;
|
|
1791
|
+
var cards = [
|
|
1792
|
+
['Uptime', fmtDur(s.uptimeMs / 1000)],
|
|
1793
|
+
['消息 (1h)', lh.received || 0],
|
|
1794
|
+
['在线 Agent', connected + '/' + agents.length],
|
|
1795
|
+
['平均响应', Math.round(lh.avgResponseMs || 0) + 'ms'],
|
|
1796
|
+
['错误率', errRate],
|
|
1797
|
+
['进程 CPU', (s.cpuPercent != null ? s.cpuPercent : 0) + '%'],
|
|
1798
|
+
['系统 CPU', (sys.cpuPercent != null ? sys.cpuPercent : 0) + '%'],
|
|
1799
|
+
['进程内存', fmtBytes(s.memory ? s.memory.rss : 0)],
|
|
1800
|
+
['系统内存', sysMemPct + '%'],
|
|
1801
|
+
];
|
|
1802
|
+
$('#mon-cards').innerHTML = cards.map(function (c) {
|
|
1803
|
+
return '<div class="usage-card"><div class="card-value">' + c[1] + '</div><div class="card-label">' + c[0] + '</div></div>';
|
|
1804
|
+
}).join('');
|
|
1805
|
+
|
|
1806
|
+
var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
1807
|
+
var ts = h.map(function (p) { return new Date(p.ts).toLocaleTimeString(); });
|
|
1808
|
+
var css = function (v) { return getComputedStyle(document.documentElement).getPropertyValue(v).trim(); };
|
|
1809
|
+
var cProc = css('--accent'), cSys = css('--orange');
|
|
1810
|
+
|
|
1811
|
+
// ── CPU dual-line:进程 vs 系统 ──
|
|
1812
|
+
monDualLine('mon-cpu-chart', '_monCpu', ts, isDark, 'CPU 占用',
|
|
1813
|
+
[
|
|
1814
|
+
{ name: 'evolclaw 进程', data: h.map(function (p) { return p.procCpu; }), color: cProc },
|
|
1815
|
+
{ name: '整机系统', data: h.map(function (p) { return p.sysCpu != null ? p.sysCpu : null; }), color: cSys },
|
|
1816
|
+
],
|
|
1817
|
+
function (v) { return Number(v).toFixed(1) + '%'; }, [0, 100]);
|
|
1818
|
+
|
|
1819
|
+
// ── Memory dual-line:进程 RSS vs 系统已用 ──
|
|
1820
|
+
monDualLine('mon-mem-chart', '_monMem', ts, isDark, '内存占用',
|
|
1821
|
+
[
|
|
1822
|
+
{ name: 'evolclaw RSS', data: h.map(function (p) { return p.procRss; }), color: cProc },
|
|
1823
|
+
{ name: '系统已用', data: h.map(function (p) { return p.sysMemUsed != null ? p.sysMemUsed : null; }), color: cSys },
|
|
1824
|
+
],
|
|
1825
|
+
function (v) { return fmtBytes(v); }, null);
|
|
1826
|
+
|
|
1827
|
+
// ── Message activity bar chart ──
|
|
1828
|
+
var msgEl = $('#mon-msg-chart');
|
|
1829
|
+
if (msgEl) {
|
|
1830
|
+
if (!window._monMsg) window._monMsg = echarts.init(msgEl, isDark ? 'dark' : null);
|
|
1831
|
+
window._monMsg.setOption({
|
|
1832
|
+
title: { text: '近一小时活动', left: 'center', top: 4, textStyle: { fontSize: 12, color: isDark ? '#e6edf3' : '#1a202c' } },
|
|
1833
|
+
tooltip: { trigger: 'axis' },
|
|
1834
|
+
grid: { top: 36, bottom: 24, left: 44, right: 12 },
|
|
1835
|
+
xAxis: { type: 'category', data: ['Received', 'Completed', 'Errors', 'Interrupts', 'ToolErr'], axisLabel: { fontSize: 9 } },
|
|
1836
|
+
yAxis: { type: 'value', minInterval: 1 },
|
|
1837
|
+
series: [{
|
|
1838
|
+
type: 'bar', barWidth: '45%',
|
|
1839
|
+
data: [
|
|
1840
|
+
{ value: lh.received || 0, itemStyle: { color: css('--accent') } },
|
|
1841
|
+
{ value: lh.completed || 0, itemStyle: { color: css('--green') } },
|
|
1842
|
+
{ value: lh.errors || 0, itemStyle: { color: css('--red') } },
|
|
1843
|
+
{ value: lh.interrupts || 0, itemStyle: { color: css('--orange') } },
|
|
1844
|
+
{ value: lh.toolErrors || 0, itemStyle: { color: css('--blue') } },
|
|
1845
|
+
],
|
|
1846
|
+
}],
|
|
1847
|
+
animation: false,
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// ── Error breakdown donut ──
|
|
1852
|
+
var errEntries = Object.entries(lh.errorsByType || {});
|
|
1853
|
+
var errEl = $('#mon-err-chart');
|
|
1854
|
+
if (errEl) {
|
|
1855
|
+
if (errEntries.length) {
|
|
1856
|
+
if (!window._monErr) window._monErr = echarts.init(errEl, isDark ? 'dark' : null);
|
|
1857
|
+
window._monErr.setOption({
|
|
1858
|
+
title: { text: '错误分布', left: 'center', top: 4, textStyle: { fontSize: 12, color: isDark ? '#e6edf3' : '#1a202c' } },
|
|
1859
|
+
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
|
1860
|
+
series: [{
|
|
1861
|
+
type: 'pie', radius: ['32%', '64%'], center: ['50%', '56%'],
|
|
1862
|
+
label: { fontSize: 10 },
|
|
1863
|
+
data: errEntries.map(function (e) { return { name: e[0], value: e[1] }; }),
|
|
1864
|
+
}],
|
|
1865
|
+
animation: false,
|
|
1866
|
+
});
|
|
1867
|
+
} else {
|
|
1868
|
+
if (window._monErr) { window._monErr.dispose(); window._monErr = null; }
|
|
1869
|
+
errEl.innerHTML = '<div class="empty" style="padding:24px;font-size:12px">近一小时无错误</div>';
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// ── Per-agent table ──
|
|
1874
|
+
var dotMap = { connected: 'on', reconnecting: 'idle', aid_blocked: 'idle', kicked: 'off', kicked_no_retry: 'off', failed: 'off', disabled: 'off' };
|
|
1875
|
+
$('#mon-agent-table-wrap').innerHTML =
|
|
1876
|
+
'<div class="mon-section-title">各 Agent 运行状态</div>' +
|
|
1877
|
+
'<table class="usage-table"><thead><tr>' +
|
|
1878
|
+
'<th>Agent</th><th>状态</th><th>收</th><th>发</th><th>流入</th><th>流出</th><th>对端</th><th>队列</th><th>处理中</th>' +
|
|
1879
|
+
'</tr></thead><tbody>' +
|
|
1880
|
+
(agents.length ? agents.map(function (a) {
|
|
1881
|
+
var st = a.stats || {};
|
|
1882
|
+
var dot = dotMap[a.status] || 'off';
|
|
1883
|
+
return '<tr>' +
|
|
1884
|
+
'<td title="' + esc(a.aid) + '">' + esc(a.agentName || shortAid(a.aid)) + '</td>' +
|
|
1885
|
+
'<td><span class="dot ' + dot + '"></span>' + esc(a.status) + '</td>' +
|
|
1886
|
+
'<td>' + (st.messagesReceived || 0) + '</td>' +
|
|
1887
|
+
'<td>' + (st.messagesSent || 0) + '</td>' +
|
|
1888
|
+
'<td>' + fmtBytes(st.bytesReceived || 0) + '</td>' +
|
|
1889
|
+
'<td>' + fmtBytes(st.bytesSent || 0) + '</td>' +
|
|
1890
|
+
'<td>' + (st.uniquePeerCount || 0) + '</td>' +
|
|
1891
|
+
'<td>' + (st.queued || 0) + '</td>' +
|
|
1892
|
+
'<td>' + (st.processing ? '⚙ ' + st.processing : 0) + '</td>' +
|
|
1893
|
+
'</tr>';
|
|
1894
|
+
}).join('') : '<tr><td colspan="9" style="text-align:center;color:var(--dim)">暂无 Agent</td></tr>') +
|
|
1895
|
+
'</tbody></table>';
|
|
1896
|
+
|
|
1897
|
+
// ── Recent errors(替换原 Channels 位置)──
|
|
1898
|
+
$('#mon-err-list').innerHTML =
|
|
1899
|
+
'<div class="mon-section-title">最近错误 <span class="mon-section-sub">(最多 50 条)</span></div>' +
|
|
1900
|
+
(recentErrs.length
|
|
1901
|
+
? '<div class="mon-err-rows">' + recentErrs.map(function (e) {
|
|
1902
|
+
var who = e.agentName ? shortAid(e.agentName) : '—';
|
|
1903
|
+
var tag = e.kind === 'tool'
|
|
1904
|
+
? '<span class="mon-err-tag tag-tool">工具</span>'
|
|
1905
|
+
: '<span class="mon-err-tag tag-task">任务</span>';
|
|
1906
|
+
var label = e.kind === 'tool' ? (e.toolName || 'tool') : (e.errorType || 'error');
|
|
1907
|
+
var msg = e.message ? esc(e.message) : '';
|
|
1908
|
+
return '<div class="mon-err-row">' +
|
|
1909
|
+
'<span class="mon-err-time">' + fmtAgo(e.ts) + '</span>' +
|
|
1910
|
+
tag +
|
|
1911
|
+
'<span class="mon-err-aid" title="' + esc(e.agentName || '') + '">' + esc(who) + '</span>' +
|
|
1912
|
+
'<span class="mon-err-kind">' + esc(label) + '</span>' +
|
|
1913
|
+
'<span class="mon-err-msg" title="' + msg + '">' + msg + '</span>' +
|
|
1914
|
+
'</div>';
|
|
1915
|
+
}).join('') + '</div>'
|
|
1916
|
+
: '<div class="empty" style="padding:24px;font-size:12px">暂无错误记录</div>');
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// 双线时序图(进程 + 系统)。series: [{name,data,color}]
|
|
1920
|
+
function monDualLine(elId, varKey, times, isDark, title, series, fmtY, yRange) {
|
|
1921
|
+
var el = $('#' + elId);
|
|
1922
|
+
if (!el) return;
|
|
1923
|
+
if (!window[varKey]) window[varKey] = echarts.init(el, isDark ? 'dark' : null);
|
|
1924
|
+
window[varKey].setOption({
|
|
1925
|
+
title: { text: title, left: 'center', top: 4, textStyle: { fontSize: 12, color: isDark ? '#e6edf3' : '#1a202c' } },
|
|
1926
|
+
legend: { show: false },
|
|
1927
|
+
tooltip: {
|
|
1928
|
+
trigger: 'axis',
|
|
1929
|
+
formatter: function (params) {
|
|
1930
|
+
var lines = [params[0].axisValue];
|
|
1931
|
+
params.forEach(function (pt) {
|
|
1932
|
+
if (pt.value == null) return;
|
|
1933
|
+
lines.push(pt.marker + pt.seriesName + ': ' + (fmtY ? fmtY(pt.value) : pt.value));
|
|
1934
|
+
});
|
|
1935
|
+
return lines.join('<br/>');
|
|
1936
|
+
},
|
|
1937
|
+
},
|
|
1938
|
+
grid: { top: 36, bottom: 24, left: 56, right: 12 },
|
|
1939
|
+
xAxis: { type: 'category', data: times, boundaryGap: false, axisLabel: { fontSize: 9 } },
|
|
1940
|
+
yAxis: {
|
|
1941
|
+
type: 'value',
|
|
1942
|
+
min: (yRange ? yRange[0] : 0),
|
|
1943
|
+
max: (yRange ? yRange[1] : undefined),
|
|
1944
|
+
axisLabel: { formatter: fmtY ? function (v) { return fmtY(v); } : '{value}' },
|
|
1945
|
+
},
|
|
1946
|
+
series: series.map(function (sr) {
|
|
1947
|
+
return {
|
|
1948
|
+
name: sr.name, type: 'line', data: sr.data, smooth: true, symbol: 'none',
|
|
1949
|
+
connectNulls: true,
|
|
1950
|
+
lineStyle: { width: 2, color: sr.color },
|
|
1951
|
+
areaStyle: { color: sr.color, opacity: 0.08 },
|
|
1952
|
+
itemStyle: { color: sr.color },
|
|
1953
|
+
};
|
|
1954
|
+
}),
|
|
1955
|
+
animation: false,
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1447
1959
|
window.addEventListener('DOMContentLoaded', () => {
|
|
1448
1960
|
initTheme();
|
|
1449
1961
|
initPairUI();
|