evolclaw-web 1.2.2 → 1.3.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.
@@ -7,22 +7,57 @@
7
7
  *
8
8
  * subscribe: 30s 轮询 + JSON diff,仅变化时 push。
9
9
  */
10
+ import fs from 'fs';
11
+ import path from 'path';
10
12
  import { resolvePaths } from '../paths.js';
11
13
  import { ipcQuery } from '../ipc-client.js';
14
+ function readDefaultBaseagents() {
15
+ try {
16
+ const p = resolvePaths();
17
+ const defaultsPath = path.join(p.root, 'agents', 'defaults.json');
18
+ const raw = JSON.parse(fs.readFileSync(defaultsPath, 'utf-8'));
19
+ const baseagents = raw?.baseagents;
20
+ if (!baseagents || typeof baseagents !== 'object')
21
+ return [];
22
+ const active = typeof raw.active_baseagent === 'string' ? raw.active_baseagent : null;
23
+ return Object.entries(baseagents).map(([name, cfg]) => {
24
+ const c = cfg && typeof cfg === 'object' ? cfg : {};
25
+ return {
26
+ name,
27
+ active: name === active,
28
+ model: c.model ?? null,
29
+ effort: c.effort ?? c.reasoning ?? null,
30
+ };
31
+ });
32
+ }
33
+ catch {
34
+ return [];
35
+ }
36
+ }
12
37
  async function menuExec(payload) {
13
38
  const p = resolvePaths();
14
39
  const r = await ipcQuery(p.socket, { type: 'menu.exec', payload }, 5000);
15
40
  return r?.ok ? r.response : null;
16
41
  }
17
42
  async function buildSnapshot() {
18
- const [listResp, sysResp] = await Promise.all([
43
+ const [listResp, sysResp, checkResp] = await Promise.all([
19
44
  menuExec({ type: 'menu.list', id: 'sys-list' }),
20
45
  menuExec({ type: 'menu.query', id: 'sys-q', name: 'system' }),
46
+ menuExec({ type: 'menu.action', id: 'sys-check', name: 'system', action: 'check' }),
21
47
  ]);
22
48
  const daemonRunning = listResp !== null;
23
49
  // sysResp 形如 { type:'menu.response', id, name, data | error }
24
50
  const system = sysResp?.data ?? null;
25
- return { daemonRunning, system, upgrade: null, check: null };
51
+ // baseagents:以 defaults.json active/model/effort 为主,
52
+ // 再从后端返回的 baseagents([{name,version}])按 name 补上 CLI 版本号。
53
+ let baseagents = readDefaultBaseagents();
54
+ if (system && Array.isArray(system.baseagents)) {
55
+ const verByName = new Map(system.baseagents.map((b) => [b.name, b.version ?? null]));
56
+ baseagents = baseagents.map((b) => ({ ...b, version: verByName.get(b.name) ?? null }));
57
+ }
58
+ // checkResp 包含 evolagents 等健康检查数据
59
+ const check = checkResp?.data ?? null;
60
+ return { daemonRunning, system: system ? { ...system, baseagents } : system, upgrade: null, check };
26
61
  }
27
62
  export const systemSource = {
28
63
  kind: 'system',
@@ -70,7 +70,6 @@ const translations = {
70
70
  'agents.stats.online': '在线',
71
71
  'agents.stats.offline': '离线',
72
72
  'agents.stats.messages': 'Messages',
73
- 'agents.stats.traffic': 'Traffic',
74
73
  'agents.stats.version': 'Version',
75
74
  'agents.stats.pid': 'PID',
76
75
  'agents.stats.uptime': 'Uptime',
@@ -84,9 +83,9 @@ const translations = {
84
83
  'agents.th.runtime': '运行',
85
84
  'agents.th.received': '收',
86
85
  'agents.th.sent': '发',
87
- 'agents.th.bytesIn': '入字节',
88
- 'agents.th.bytesOut': '出字节',
89
- 'agents.th.peerCount': '对端数量',
86
+ 'agents.th.completed': '',
87
+ 'agents.th.errors': '',
88
+ 'agents.th.interrupts': '',
90
89
  'agents.th.lastActivity': '最后活动',
91
90
  'agents.th.operations': '操作',
92
91
  'agents.th.projectPath': '项目路径',
@@ -114,11 +113,11 @@ const translations = {
114
113
  'agents.op.viewAgentMd': '查看 agent.md ↗',
115
114
 
116
115
  // Messages view
117
- 'messages.colTitle.aid': 'AID',
118
- 'messages.colTitle.peers': 'Peers',
116
+ 'messages.colTitle.aid': 'Agent',
117
+ 'messages.colTitle.peers': 'Chats',
119
118
  'messages.colTitle.all': 'All',
120
- 'messages.empty.selectAid': '← 选择一个 AID',
121
- 'messages.empty.selectToView': '选择 AID 查看消息',
119
+ 'messages.empty.selectAid': '← 选择一个 Agent',
120
+ 'messages.empty.selectToView': '选择 Agent 查看消息',
122
121
  'messages.empty.noMessages': '暂无消息',
123
122
  'messages.tag.group': '群聊',
124
123
  'messages.tag.encrypted': '🔒密文',
@@ -345,7 +344,6 @@ const translations = {
345
344
  'agents.stats.online': 'online',
346
345
  'agents.stats.offline': 'offline',
347
346
  'agents.stats.messages': 'Messages',
348
- 'agents.stats.traffic': 'Traffic',
349
347
  'agents.stats.version': 'Version',
350
348
  'agents.stats.pid': 'PID',
351
349
  'agents.stats.uptime': 'Uptime',
@@ -359,9 +357,9 @@ const translations = {
359
357
  'agents.th.runtime': 'Runtime',
360
358
  'agents.th.received': 'Recv',
361
359
  'agents.th.sent': 'Sent',
362
- 'agents.th.bytesIn': 'Bytes In',
363
- 'agents.th.bytesOut': 'Bytes Out',
364
- 'agents.th.peerCount': 'Peers',
360
+ 'agents.th.completed': 'Done',
361
+ 'agents.th.errors': 'Err',
362
+ 'agents.th.interrupts': 'Int',
365
363
  'agents.th.lastActivity': 'Last Activity',
366
364
  'agents.th.operations': 'Operations',
367
365
  'agents.th.projectPath': 'Project Path',
@@ -389,11 +387,11 @@ const translations = {
389
387
  'agents.op.viewAgentMd': 'View agent.md ↗',
390
388
 
391
389
  // Messages view
392
- 'messages.colTitle.aid': 'AID',
393
- 'messages.colTitle.peers': 'Peers',
390
+ 'messages.colTitle.aid': 'Agent',
391
+ 'messages.colTitle.peers': 'Chats',
394
392
  'messages.colTitle.all': 'All',
395
- 'messages.empty.selectAid': '← Select an AID',
396
- 'messages.empty.selectToView': 'Select AID to view messages',
393
+ 'messages.empty.selectAid': '← Select Agent',
394
+ 'messages.empty.selectToView': 'Select Agent to view messages',
397
395
  'messages.empty.noMessages': 'No messages',
398
396
  'messages.tag.group': 'Group',
399
397
  'messages.tag.encrypted': '🔒Encrypted',
@@ -687,20 +685,44 @@ function connect() {
687
685
  ws.onopen = () => {
688
686
  setConnStatus('● ' + t('status.connected'), 'ok');
689
687
  reconnectDelay = 1000;
690
- subscribe(currentView, pendingSub || {});
688
+ // 获取可用的 baseagent
689
+ fetch(`${BASE}api/available-baseagents`)
690
+ .then(r => r.json())
691
+ .then(data => {
692
+ availableBaseagents = data;
693
+ // 如果当前没有选中 baseagent,默认选第一个可用的
694
+ if (!sessSel.baseagent) {
695
+ sessSel.baseagent = data.claude ? 'claude' : (data.codex ? 'codex' : null);
696
+ }
697
+ console.log('[ecweb] Available baseagents:', availableBaseagents, 'Selected:', sessSel.baseagent);
698
+ // 重新订阅当前视图(带上正确的参数)
699
+ if (currentView === 'session') {
700
+ subscribe('session', { sessionId: sessSel.sessionId, project: sessSel.project, baseagent: sessSel.baseagent });
701
+ } else {
702
+ subscribe(currentView, pendingSub || {});
703
+ }
704
+ })
705
+ .catch(err => {
706
+ console.warn('[ecweb] Failed to fetch available-baseagents:', err);
707
+ // 失败时默认使用 claude
708
+ availableBaseagents = { claude: true, codex: false };
709
+ if (!sessSel.baseagent) sessSel.baseagent = 'claude';
710
+ subscribe(currentView, pendingSub || {});
711
+ });
691
712
  };
692
713
 
693
714
  ws.onmessage = (ev) => {
694
715
  let msg;
695
716
  try { msg = JSON.parse(ev.data); } catch { return; }
696
717
  if (msg.type === 'pong') return;
697
- if (msg.type === 'error') { console.warn('server error:', msg.message); return; }
718
+ if (msg.type === 'error') { console.warn('[ecweb] Server error:', msg.message); return; }
698
719
  if (msg.type === 'menu.response') {
699
720
  const pend = _menuPending[msg.requestId];
700
721
  if (pend) { delete _menuPending[msg.requestId]; pend.resolve(msg.data); }
701
722
  return;
702
723
  }
703
724
  if (msg.type === 'snapshot' || msg.type === 'delta') {
725
+ console.log('[ecweb] Received', msg.type, 'for view:', msg.view, 'currentView:', currentView);
704
726
  // system 视图保留客户端写入的 check/upgrade,防止 3s 轮询覆盖
705
727
  if (msg.view === 'system' && state.system) {
706
728
  state.system = {
@@ -711,7 +733,10 @@ function connect() {
711
733
  } else {
712
734
  state[msg.view] = msg.data;
713
735
  }
714
- if (msg.view === currentView) renderView(currentView);
736
+ if (msg.view === currentView) {
737
+ console.log('[ecweb] Rendering view:', currentView, 'with data:', msg.data);
738
+ renderView(currentView);
739
+ }
715
740
  }
716
741
  };
717
742
 
@@ -732,7 +757,14 @@ function connect() {
732
757
  function subscribe(view, params) {
733
758
  pendingSub = params;
734
759
  if (ws && ws.readyState === WebSocket.OPEN) {
760
+ // session 视图添加 baseagent 参数
761
+ if (view === 'session' && sessSel.baseagent) {
762
+ params = { ...params, baseagent: sessSel.baseagent };
763
+ }
764
+ console.log('[ecweb] Subscribing:', view, params);
735
765
  ws.send(JSON.stringify({ type: 'subscribe', view, ...params }));
766
+ } else {
767
+ console.warn('[ecweb] WebSocket not ready, subscription pending');
736
768
  }
737
769
  }
738
770
 
@@ -759,12 +791,13 @@ setInterval(() => {
759
791
 
760
792
  // ── Tab 切换 ──
761
793
  let msgSel = { aid: null, peer: null };
762
- let sessSel = { sessionId: null, project: null };
794
+ let sessSel = { sessionId: null, project: null, baseagent: null };
763
795
  let trigSel = { agent: null };
764
796
  let sessSearch = '';
765
797
  let sessFilterNormal = false; // true=只显示有效会话(userMsgs >= 2)
766
798
  let sessChatMode = false; // false=完整视图,true=对话视图(折叠处理过程)
767
799
  let monRange = '2m'; // Monitor 时间窗口:2m / 10m / 1h
800
+ let availableBaseagents = { claude: false, codex: false }; // 可用的 baseagent
768
801
 
769
802
  function switchView(view) {
770
803
  currentView = view;
@@ -773,7 +806,7 @@ function switchView(view) {
773
806
  document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id === 'view-' + view));
774
807
  // 切换时按当前选择恢复订阅
775
808
  if (view === 'msg') subscribe('msg', { aid: msgSel.aid, peer: msgSel.peer });
776
- else if (view === 'session') subscribe('session', { sessionId: sessSel.sessionId, project: sessSel.project });
809
+ else if (view === 'session') subscribe('session', { sessionId: sessSel.sessionId, project: sessSel.project, baseagent: sessSel.baseagent });
777
810
  else if (view === 'cache') subscribe('cache', {});
778
811
  else if (view === 'system') subscribe('system', {});
779
812
  else if (view === 'triggers') subscribe('triggers', { agent: trigSel.agent });
@@ -805,6 +838,11 @@ function esc(s) {
805
838
  return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
806
839
  }
807
840
  function shortAid(aid) { return String(aid || '').split('.')[0]; }
841
+ function shortId(id) {
842
+ const s = String(id || '');
843
+ if (!s) return 'unknown';
844
+ return s.includes('.') ? shortAid(s) : (s.length > 18 ? s.slice(0, 10) + '…' + s.slice(-5) : s);
845
+ }
808
846
  function fmtBytes(b) {
809
847
  if (!b) return '0';
810
848
  const u = ['B', 'KB', 'MB', 'GB']; let i = Math.min(Math.floor(Math.log(b) / Math.log(1024)), 3);
@@ -867,7 +905,8 @@ function agentStateBadge(s, agStatus, connStatus) {
867
905
  if ((s.processing || 0) > 0)
868
906
  return `<span class="state-badge working">${t('status.working')}</span>`;
869
907
  // 收到过消息 → 永远是 idle,不再回到 connected
870
- if ((s.messagesReceived || 0) > 0 || (s.messagesSent || 0) > 0)
908
+ if ((s.received || 0) > 0 || (s.sent || 0) > 0 || (s.completed || 0) > 0 || (s.errors || 0) > 0 || (s.interrupts || 0) > 0 ||
909
+ (s.messagesReceived || 0) > 0 || (s.messagesSent || 0) > 0)
871
910
  return `<span class="state-badge idle">${t('status.idle')}</span>`;
872
911
  return `<span class="state-badge connected">${t('status.connected')}</span>`;
873
912
  }
@@ -1005,14 +1044,17 @@ function initMsgTipFloat() {
1005
1044
  window.addEventListener('scroll', hideNow, true);
1006
1045
  }
1007
1046
 
1008
- // 顶部统计条:Gateway / AIDs total·connected·offline / Messages ↓↑ / Traffic ↓↑ / Version·PID·Uptime
1009
- function agentsStatsBar(data, aids, stats) {
1047
+ // 顶部统计条:Gateway / AIDs total·connected·offline / Messages / Version·PID·Uptime
1048
+ function agentsStatsBar(data, aids, agentStats) {
1010
1049
  const connected = aids.filter(a => (a.status || 'connected') === 'connected').length;
1011
1050
  const offline = aids.length - connected;
1012
- let recv = 0, sent = 0, bin = 0, bout = 0;
1013
- for (const s of stats) {
1014
- recv += s.messagesReceived || 0; sent += s.messagesSent || 0;
1015
- bin += s.bytesReceived || 0; bout += s.bytesSent || 0;
1051
+ let recv = 0, sent = 0, done = 0, errors = 0, interrupts = 0;
1052
+ for (const s of agentStats) {
1053
+ recv += s.received || 0;
1054
+ sent += s.sent || 0;
1055
+ done += s.completed || 0;
1056
+ errors += s.errors || 0;
1057
+ interrupts += s.interrupts || 0;
1016
1058
  }
1017
1059
  const gws = [...new Set(aids.filter(a => a.gatewayUrl).map(a => a.gatewayUrl))];
1018
1060
  const gw = gws.length ? gws.map(esc).join(', ') : '—';
@@ -1025,13 +1067,19 @@ function agentsStatsBar(data, aids, stats) {
1025
1067
  h += `<span class="sg"><span class="sg-k">${t('agents.stats.gateway')}</span><span class="sg-gw">${gw}</span></span>`;
1026
1068
  h += `<span class="sg"><span class="sg-k">${t('agents.stats.aids')}</span>${aids.length} ${t('agents.stats.total')} · <span class="num-on">${connected} ${t('agents.stats.online')}</span>` +
1027
1069
  `${offline ? ` · <span class="num-off">${offline} ${t('agents.stats.offline')}</span>` : ''}</span>`;
1028
- h += `<span class="sg"><span class="sg-k">${t('agents.stats.messages')}</span><span class="in">↓${recv}</span> <span class="out">↑${sent}</span></span>`;
1029
- h += `<span class="sg"><span class="sg-k">${t('agents.stats.traffic')}</span><span class="in">↓${fmtBytes(bin)}</span> <span class="out">↑${fmtBytes(bout)}</span></span>`;
1070
+ h += `<span class="sg"><span class="sg-k">${t('agents.stats.messages')}</span>收 ${recv} · 发 ${sent} · 错 ${errors} · 断 ${interrupts} · 完 ${done}</span>`;
1030
1071
  h += `<span class="sg"><span class="sg-k">${t('agents.stats.version')}</span>${esc(ver)} · <span class="sg-k">${t('agents.stats.pid')}</span>${pid} · <span class="sg-k">${t('agents.stats.uptime')}</span>${uptime}</span>`;
1031
1072
  h += '</div>';
1032
1073
  return h;
1033
1074
  }
1034
1075
 
1076
+ function agentQueueHtml(s) {
1077
+ const processing = s.processing || 0;
1078
+ const queued = s.queued || 0;
1079
+ if (processing === 0 && queued === 0) return '<span class="ag-queue-empty">-</span>';
1080
+ return `<span class="ag-queue-num">${processing}/${queued}</span>`;
1081
+ }
1082
+
1035
1083
  // 操作列 HTML(启用页):停止/启动 + 清空队列(conditional) + ···(禁用/重载/编辑/md/删除)
1036
1084
  function agentOpsHtml(aid, ag, s) {
1037
1085
  if (_agentOps.has(aid)) {
@@ -1064,6 +1112,8 @@ function renderAgents(data) {
1064
1112
  const aids = data.aids || [];
1065
1113
  const statsByAid = {};
1066
1114
  for (const s of (data.stats || [])) statsByAid[s.aid] = s;
1115
+ const agentStatsByAid = {};
1116
+ for (const s of (data.agentStats || [])) agentStatsByAid[s.aid] = s;
1067
1117
  const aidConnByAid = {};
1068
1118
  for (const a of aids) aidConnByAid[a.aid] = a;
1069
1119
 
@@ -1107,13 +1157,13 @@ function renderAgents(data) {
1107
1157
  }
1108
1158
 
1109
1159
  // ── 启用页 ──
1110
- // 按收发消息总数降序排序(活跃的排前面)
1111
- const totalMsgs = (ag) => {
1112
- const s = statsByAid[ag.aid] || {};
1113
- return (s.messagesReceived || 0) + (s.messagesSent || 0);
1160
+ // 按全渠道任务活动降序排序(活跃的排前面)
1161
+ const totalActivity = (ag) => {
1162
+ const s = agentStatsByAid[ag.aid] || {};
1163
+ return (s.received || 0) + (s.sent || 0) + (s.completed || 0) + (s.errors || 0) + (s.interrupts || 0);
1114
1164
  };
1115
1165
  const enabledAgents = allAgents.filter(ag => ag.status !== 'disabled')
1116
- .sort((a, b) => totalMsgs(b) - totalMsgs(a));
1166
+ .sort((a, b) => totalActivity(b) - totalActivity(a));
1117
1167
  if (!enabledAgents.length) {
1118
1168
  html += `<div class="empty">${t('agents.empty.enabled')}</div>`;
1119
1169
  el.innerHTML = html;
@@ -1122,12 +1172,14 @@ function renderAgents(data) {
1122
1172
  }
1123
1173
 
1124
1174
  html += '<table><thead><tr>' +
1125
- `<th>${t('agents.th.aid')}</th><th>${t('agents.th.work')}</th><th>${t('agents.th.queue')}</th><th>${t('agents.th.model')}</th><th>${t('agents.th.runtime')}</th><th>${t('agents.th.received')}</th><th>${t('agents.th.sent')}</th>` +
1126
- `<th>${t('agents.th.bytesIn')}</th><th>${t('agents.th.bytesOut')}</th><th>${t('agents.th.peerCount')}</th><th>${t('agents.th.lastActivity')}</th><th>${t('agents.th.operations')}</th>` +
1175
+ `<th>${t('agents.th.aid')}</th><th>${t('agents.th.work')}</th><th>${t('agents.th.queue')}</th><th>${t('agents.th.model')}</th><th>${t('agents.th.runtime')}</th>` +
1176
+ `<th>${t('agents.th.received')}</th><th>${t('agents.th.sent')}</th><th>${t('agents.th.errors')}</th><th>${t('agents.th.interrupts')}</th><th>${t('agents.th.completed')}</th>` +
1177
+ `<th>${t('agents.th.lastActivity')}</th><th>${t('agents.th.operations')}</th>` +
1127
1178
  '</tr></thead><tbody>';
1128
1179
 
1129
1180
  for (const ag of enabledAgents) {
1130
1181
  const s = statsByAid[ag.aid] || {};
1182
+ const runStats = agentStatsByAid[ag.aid] || {};
1131
1183
  const conn = aidConnByAid[ag.aid] || {};
1132
1184
  const connStatus = conn.status || (ag.status === 'running' ? 'connected' : 'disconnected');
1133
1185
  const dotCls = connStatus === 'connected' ? 'on' : (connStatus === 'reconnecting' ? 'idle' : 'off');
@@ -1135,10 +1187,7 @@ function renderAgents(data) {
1135
1187
  const uptime = (connStatus === 'connected' && conn.lastConnectedAt) ? fmtDur((Date.now() - conn.lastConnectedAt) / 1000) : '—';
1136
1188
  const lastTs = Math.max(s.lastReceivedAt || 0, s.lastSentAt || 0, ag.lastActivity || 0);
1137
1189
  const preview = agentPreviewHtml(s);
1138
- // 队列数:不含正在处理的那条
1139
- const rawQueued = s.queued || 0;
1140
- const queued = rawQueued;
1141
- const queueCell = queued > 0 ? `<span class="ag-queue-num">${queued}</span>` : '<span style="color:var(--dim)">0</span>';
1190
+ const queueCell = agentQueueHtml(runStats);
1142
1191
  const model = ag.model || ag.baseagent || '—';
1143
1192
 
1144
1193
  const idCell = `<div class="ag-id"><span class="dot ${dotCls}" title="${esc(connStatus)}"></span>` +
@@ -1147,15 +1196,17 @@ function renderAgents(data) {
1147
1196
 
1148
1197
  html += `<tr class="ag-main">` +
1149
1198
  `<td>${idCell}</td>` +
1150
- `<td>${agentStateBadge(s, ag.status, connStatus)}</td>` +
1199
+ `<td>${agentStateBadge({ ...s, ...runStats }, ag.status, connStatus)}</td>` +
1151
1200
  `<td>${queueCell}</td>` +
1152
1201
  `<td style="font-size:11px;color:var(--dim)">${esc(model)}</td>` +
1153
1202
  `<td>${uptime}</td>` +
1154
- `<td>${s.messagesReceived ?? 0}</td><td>${s.messagesSent ?? 0}</td>` +
1155
- `<td>${fmtBytes(s.bytesReceived)}</td><td>${fmtBytes(s.bytesSent)}</td>` +
1156
- `<td>${s.uniquePeerCount ?? conn.peerCount ?? 0}</td>` +
1203
+ `<td>${runStats.received || 0}</td>` +
1204
+ `<td>${runStats.sent || 0}</td>` +
1205
+ `<td>${runStats.errors || 0}</td>` +
1206
+ `<td>${runStats.interrupts || 0}</td>` +
1207
+ `<td>${runStats.completed || 0}</td>` +
1157
1208
  `<td>${fmtAgo(lastTs)}</td>` +
1158
- `<td class="agent-ops-cell">${agentOpsHtml(ag.aid, ag, s)}</td>` +
1209
+ `<td class="agent-ops-cell">${agentOpsHtml(ag.aid, ag, runStats)}</td>` +
1159
1210
  '</tr>';
1160
1211
  // 自定义 tooltip(HTML,hover 显示)
1161
1212
  const recent = (s.recentMessages || []);
@@ -1168,7 +1219,7 @@ function renderAgents(data) {
1168
1219
  }
1169
1220
  html += '</tbody></table>';
1170
1221
  if (data.daemonRunning) {
1171
- html += agentsStatsBar(data, aids, data.stats || []);
1222
+ html += agentsStatsBar(data, aids, data.agentStats || []);
1172
1223
  }
1173
1224
  el.innerHTML = html;
1174
1225
  bindAgentsEvents(el);
@@ -1301,17 +1352,20 @@ function card(label, value, valCls, sub) {
1301
1352
  // ── Messages 视图 ──
1302
1353
  function renderMsg(data) {
1303
1354
  if (!data) return;
1304
- const aids = data.aids || [];
1355
+ const aids = data.scopes || data.aids || [];
1305
1356
  const peers = data.peers || [];
1306
1357
  const messages = data.messages || [];
1358
+ if (data.scope && data.scope !== msgSel.aid) msgSel.aid = data.scope;
1307
1359
 
1308
1360
  // 左:AID 列表
1309
1361
  let aidsHtml = `<div class="col-title">${t('messages.colTitle.aid')}</div>`;
1310
1362
  for (const a of aids) {
1311
1363
  const sel = a.aid === msgSel.aid ? ' sel' : '';
1364
+ const name = a.selfAID && a.selfAID !== 'unknown' ? shortAid(a.selfAID) : 'unknown';
1365
+ const groupBit = a.groupCount ? ` · 群 ${a.groupCount}` : '';
1312
1366
  aidsHtml += `<div class="list-item${sel}" data-aid="${esc(a.aid)}">` +
1313
- `<div class="name">${esc(shortAid(a.aid))}</div>` +
1314
- `<div class="sub">↓${a.totalIn} ↑${a.totalOut} · ${a.peerCount} peers</div></div>`;
1367
+ `<div class="name">${esc(name)}</div>` +
1368
+ `<div class="sub">↓${a.totalIn} ↑${a.totalOut} · ${a.peerCount} chats${groupBit} · ${fmtAgo(a.lastAt)}</div></div>`;
1315
1369
  }
1316
1370
  $('#msg-aids').innerHTML = aidsHtml;
1317
1371
  $('#msg-aids').querySelectorAll('.list-item').forEach(item => {
@@ -1323,11 +1377,16 @@ function renderMsg(data) {
1323
1377
  if (msgSel.aid) {
1324
1378
  const allSel = msgSel.peer === null ? ' sel' : '';
1325
1379
  peersHtml += `<div class="list-item${allSel}" data-peer=""><div class="name">${t('messages.colTitle.all')}</div>` +
1326
- `<div class="sub">${peers.length} peers</div></div>`;
1380
+ `<div class="sub">${peers.length} chats</div></div>`;
1327
1381
  for (const p of peers) {
1328
1382
  const sel = p.peerId === msgSel.peer ? ' sel' : '';
1383
+ const displayName = p.chatType === 'group'
1384
+ ? (p.groupName || p.peerName || p.groupId || p.peerId)
1385
+ : (p.peerName || p.peerId);
1386
+ const channelLabel = p.channelName && p.channelName !== 'main' ? `${p.channelType}/${p.channelName}` : (p.channelType || '');
1387
+ const typeLabel = p.chatType === 'group' ? `${channelLabel} · ${t('messages.tag.group')}` : channelLabel;
1329
1388
  peersHtml += `<div class="list-item${sel}" data-peer="${esc(p.peerId)}">` +
1330
- `<div class="name">${esc(p.peerName || shortAid(p.peerId))}</div>` +
1389
+ `<div class="name">${esc(shortId(displayName))} <span class="tag">${esc(typeLabel)}</span></div>` +
1331
1390
  `<div class="sub">↓${p.inbound} ↑${p.outbound} · ${fmtAgo(p.lastAt)}</div></div>`;
1332
1391
  }
1333
1392
  } else {
@@ -1346,8 +1405,9 @@ function renderMsg(data) {
1346
1405
  for (const m of messages) {
1347
1406
  const cls = m.dir === 'in' ? 'in' : 'out';
1348
1407
  const arrow = m.dir === 'in' ? '↓' : '↑';
1349
- const from = shortAid(m.from), to = shortAid(m.to);
1408
+ const from = shortId(m.from), to = shortId(m.to);
1350
1409
  const tags = [];
1410
+ if (m.channelType) tags.push(m.channelType);
1351
1411
  if (m.chatType === 'group') tags.push(t('messages.tag.group'));
1352
1412
  // 消息详情流的 kind 来自 jsonl 的 msgType(text/thought/image/file/command),
1353
1413
  // 与 agents 页内存态的 MsgKind(send/thought/inject/notify)不是同一套词汇。
@@ -1388,8 +1448,19 @@ function renderSession(data) {
1388
1448
  const projOpts = projects.map(p =>
1389
1449
  `<option value="${esc(p.encoded)}"${p.encoded === sessSel.project ? ' selected' : ''}>${esc(p.label)} (${p.count})</option>`
1390
1450
  ).join('');
1451
+
1452
+ // baseagent 下拉选择器(只显示可用的)
1453
+ let baseagentOpts = '';
1454
+ if (availableBaseagents.claude) {
1455
+ baseagentOpts += `<option value="claude"${sessSel.baseagent === 'claude' ? ' selected' : ''}>Claude</option>`;
1456
+ }
1457
+ if (availableBaseagents.codex) {
1458
+ baseagentOpts += `<option value="codex"${sessSel.baseagent === 'codex' ? ' selected' : ''}>Codex</option>`;
1459
+ }
1460
+
1391
1461
  const normalCount = transcripts.filter(t => (t.userMsgs || 0) >= 2).length;
1392
1462
  let listHtml = '<div class="sess-filter">' +
1463
+ `<select id="sess-baseagent" title="Base Agent">${baseagentOpts}</select>` +
1393
1464
  `<select id="sess-project">${projOpts}</select>` +
1394
1465
  `<input id="sess-search" type="text" placeholder="搜索标题/首条消息…" value="${esc(sessSearch)}">` +
1395
1466
  `<button id="sess-filter-btn" class="ctrl-btn${sessFilterNormal ? ' active' : ''}" title="只显示有效会话(≥2 条用户消息)">有效 ${normalCount}</button>` +
@@ -1417,11 +1488,19 @@ function renderSession(data) {
1417
1488
  $('#sess-list').innerHTML = listHtml;
1418
1489
 
1419
1490
  // 绑定交互(注意保持搜索框焦点)
1491
+ const baseagentSel = $('#sess-baseagent');
1492
+ if (baseagentSel) baseagentSel.onchange = () => {
1493
+ console.log('[ecweb] Baseagent changed to:', baseagentSel.value);
1494
+ sessSel = { sessionId: null, project: null, baseagent: baseagentSel.value };
1495
+ sessSearch = '';
1496
+ console.log('[ecweb] Subscribing to session with baseagent:', sessSel.baseagent);
1497
+ subscribe('session', { baseagent: sessSel.baseagent });
1498
+ };
1420
1499
  const projSel = $('#sess-project');
1421
1500
  if (projSel) projSel.onchange = () => {
1422
- sessSel = { sessionId: null, project: projSel.value };
1501
+ sessSel = { sessionId: null, project: projSel.value, baseagent: sessSel.baseagent };
1423
1502
  sessSearch = '';
1424
- subscribe('session', { project: sessSel.project });
1503
+ subscribe('session', { project: sessSel.project, baseagent: sessSel.baseagent });
1425
1504
  };
1426
1505
  const filterBtn = $('#sess-filter-btn');
1427
1506
  if (filterBtn) filterBtn.onclick = () => { sessFilterNormal = !sessFilterNormal; renderSession(state.session); };
@@ -1431,7 +1510,7 @@ function renderSession(data) {
1431
1510
  if (q) { searchEl.focus(); searchEl.setSelectionRange(searchEl.value.length, searchEl.value.length); }
1432
1511
  }
1433
1512
  $('#sess-list').querySelectorAll('.list-item').forEach(item => {
1434
- item.onclick = () => { sessSel = { sessionId: item.dataset.sid, project: sessSel.project }; subscribe('session', sessSel); };
1513
+ item.onclick = () => { sessSel = { sessionId: item.dataset.sid, project: sessSel.project, baseagent: sessSel.baseagent }; subscribe('session', sessSel); };
1435
1514
  });
1436
1515
 
1437
1516
  // 右:transcript 详情
@@ -1869,7 +1948,7 @@ function channelHealthRow(c) {
1869
1948
  if (c.flapCount > 0) meta += ` <span style="color:var(--red)">抖动 ${c.flapCount}</span>`;
1870
1949
  const reason = c.kickReason || c.lastError;
1871
1950
  if (reason && !c.connected) meta += ` <span style="color:var(--red)" title="${esc(reason)}">"${esc(reason)}"</span>`;
1872
- return `<div class="ch-row"><span class="dot ${dot}"></span>${esc(c.type)}${c.instName && c.instName !== c.type ? ' ' + esc(c.instName) : ''}${meta}</div>`;
1951
+ return `<div class="ch-row"><span class="dot ${dot}"></span>${esc(c.type)}${c.instName ? ' ' + esc(c.instName) : ''}${meta}</div>`;
1873
1952
  }
1874
1953
 
1875
1954
  function agentHealthCard(ag) {
@@ -1896,6 +1975,16 @@ function agentHealthCard(ag) {
1896
1975
  return h;
1897
1976
  }
1898
1977
 
1978
+ function systemBaseagentCards(baseagents) {
1979
+ const list = Array.isArray(baseagents) ? baseagents : [];
1980
+ return list.map(ba => {
1981
+ const ver = ba.version ? `・${ba.version}` : '';
1982
+ const title = `Baseagent・${ba.active ? '✓ ' : ''}${ba.name || 'unknown'}${ver}`;
1983
+ const detail = [ba.model, ba.effort].filter(Boolean).map(esc).join(' · ') || '未指定模型/强度';
1984
+ return `<div class="cache-card"><div class="card-label">${esc(title)}</div><div class="card-val">${detail}</div></div>`;
1985
+ }).join('');
1986
+ }
1987
+
1899
1988
  function renderSystem(data) {
1900
1989
  const el = $('#view-system');
1901
1990
  if (!data) { el.innerHTML = '<div class="empty">加载中…</div>'; return; }
@@ -1936,27 +2025,36 @@ function renderSystem(data) {
1936
2025
 
1937
2026
  // ③ 健康快照
1938
2027
  if (chk) {
2028
+ // 从 chk.structured 读取数据(后端返回的数据结构)
2029
+ const s = chk.structured || chk; // 兼容旧版本(如果 chk 本身就是 structured)
1939
2030
  html += '<div class="sys-health">';
1940
2031
  // 队列 + 近 1 小时(数字卡片同一行)
1941
2032
  html += '<div class="cache-cards" style="margin-bottom:8px">';
1942
- html += `<div class="cache-card"><div class="card-label">队列</div><div class="card-val">${chk.queue?.pending ?? 0} 待 · ${chk.queue?.processing ?? 0} 处理中</div></div>`;
1943
- const h = chk.lastHour;
2033
+ html += `<div class="cache-card"><div class="card-label">队列</div><div class="card-val">${s.queue?.pending ?? 0} 待 · ${s.queue?.processing ?? 0} 处理中</div></div>`;
2034
+ const h = s.lastHour;
1944
2035
  if (h) {
1945
2036
  const errDetail = h.errors > 0 ? ` (${Object.entries(h.errorsByType || {}).map(([t, c]) => `${t}:${c}`).join(', ')})` : '';
1946
2037
  const avg = h.completed > 0 ? ` · 均 ${(h.avgResponseMs / 1000).toFixed(1)}s` : '';
1947
2038
  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>`;
1948
2039
  }
2040
+ html += systemBaseagentCards(sys.baseagents);
1949
2041
  html += '</div>';
1950
2042
  // 每个 EvolAgent 一张卡片:后端 + 渠道健康 + 负载
1951
- if (chk.evolagents?.length) {
2043
+ // 排序:启用的(非 disabled)在前,停用的(disabled)在后
2044
+ if (s.evolagents?.length) {
2045
+ const sortedAgents = s.evolagents.slice().sort((a, b) => {
2046
+ const aDisabled = a.status === 'disabled' ? 1 : 0;
2047
+ const bDisabled = b.status === 'disabled' ? 1 : 0;
2048
+ return aDisabled - bDisabled;
2049
+ });
1952
2050
  html += '<div class="agent-health-grid">';
1953
- for (const ag of chk.evolagents) html += agentHealthCard(ag);
2051
+ for (const ag of sortedAgents) html += agentHealthCard(ag);
1954
2052
  html += '</div>';
1955
2053
  }
1956
2054
  // 未归属任何 EvolAgent 的渠道(系统级 / DefaultAgent)
1957
- if (chk.unownedChannels?.length) {
2055
+ if (s.unownedChannels?.length) {
1958
2056
  html += '<div class="cache-card" style="margin-top:8px"><div class="card-label">未归属渠道</div>';
1959
- for (const c of chk.unownedChannels) html += channelHealthRow(c);
2057
+ for (const c of s.unownedChannels) html += channelHealthRow(c);
1960
2058
  html += '</div>';
1961
2059
  }
1962
2060
  html += '</div>';
@@ -3534,21 +3632,21 @@ function renderMonitor(data) {
3534
3632
  $('#mon-agent-table-wrap').innerHTML =
3535
3633
  '<div class="mon-section-title">各 Agent 运行状态</div>' +
3536
3634
  '<table class="usage-table"><thead><tr>' +
3537
- '<th>Agent</th><th>状态</th><th>收</th><th>发</th><th>流入</th><th>流出</th><th>对端</th><th>队列</th><th>处理中</th>' +
3635
+ '<th>Agent</th><th>状态</th><th>收</th><th>发</th><th>错</th><th>断</th><th>完</th><th>队列</th><th>处理中</th>' +
3538
3636
  '</tr></thead><tbody>' +
3539
3637
  (agents.length ? agents.map(function (a) {
3540
- var st = a.stats || {};
3638
+ var st = a.runtimeStats || {};
3541
3639
  var dot = dotMap[a.status] || 'off';
3542
3640
  return '<tr>' +
3543
3641
  '<td title="' + esc(a.aid) + '">' + esc(a.agentName || shortAid(a.aid)) + '</td>' +
3544
3642
  '<td><span class="dot ' + dot + '"></span>' + esc(a.status) + '</td>' +
3545
- '<td>' + (st.messagesReceived || 0) + '</td>' +
3546
- '<td>' + (st.messagesSent || 0) + '</td>' +
3547
- '<td>' + fmtBytes(st.bytesReceived || 0) + '</td>' +
3548
- '<td>' + fmtBytes(st.bytesSent || 0) + '</td>' +
3549
- '<td>' + (st.uniquePeerCount || 0) + '</td>' +
3643
+ '<td>' + (st.received || 0) + '</td>' +
3644
+ '<td>' + (st.sent || 0) + '</td>' +
3645
+ '<td>' + (st.errors || 0) + '</td>' +
3646
+ '<td>' + (st.interrupts || 0) + '</td>' +
3647
+ '<td>' + (st.completed || 0) + '</td>' +
3550
3648
  '<td>' + (st.queued || 0) + '</td>' +
3551
- '<td>' + (st.processing ? '⚙ ' + st.processing : 0) + '</td>' +
3649
+ '<td>' + (st.processing || 0) + '</td>' +
3552
3650
  '</tr>';
3553
3651
  }).join('') : '<tr><td colspan="9" style="text-align:center;color:var(--dim)">暂无 Agent</td></tr>') +
3554
3652
  '</tbody></table>';
@@ -37,7 +37,7 @@
37
37
 
38
38
  </nav>
39
39
  <span style="flex:1"></span>
40
- 2026-06-19 08:46
40
+ 2026-06-29 10:58
41
41
  <span id="conn-status" class="conn-status" data-i18n="status.connecting">连接中…</span>
42
42
  <button id="lang-btn" class="theme-btn" title="Switch Language / 切换语言">🌐</button>
43
43
  <button id="theme-btn" class="theme-btn" title="Toggle Theme / 切换主题">🌗</button>
@@ -680,6 +680,7 @@ tr.ag-disabled .ag-name { color: var(--dim); }
680
680
 
681
681
  /* 队列数列 */
682
682
  .ag-queue-num { color: var(--orange); font-weight: 600; font-variant-numeric: tabular-nums; }
683
+ .ag-queue-empty { color: var(--dim); }
683
684
 
684
685
  /* 子标签栏 */
685
686
  .ag-subtabs { display: inline-flex; gap: 2px; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw-web",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "Web-based monitoring dashboard for EvolClaw",
5
5
  "type": "module",
6
6
  "bin": {