ec-web 0.0.3 → 0.0.5

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.
@@ -296,10 +296,19 @@ const translations = {
296
296
  'sessions.turnType.toolUse': '工具使用',
297
297
  'sessions.turnType.toolResult': '工具结果',
298
298
  'sessions.turnType.msgSend': '发送消息',
299
- 'sessions.count': '{filtered} / {total} 个会话',
299
+ 'sessions.count': '{count} 个会话',
300
300
  'sessions.filter.valid': '有效',
301
301
  'sessions.filter.validTitle': '只显示有效会话(≥1 条用户消息)',
302
+ 'sessions.filter.all': '全部',
303
+ 'sessions.filter.allTitle': '显示全部会话(包括无用户消息的会话)',
304
+ 'sessions.filter.sourceTitle': '按会话类型筛选',
305
+ 'sessions.filter.sourceAll': '全部类型',
302
306
  'sessions.messageCountTitle': '用户输入 {user} 条 / 共 {total} 条消息',
307
+ 'sessions.source.trigger': 'Trigger',
308
+ 'sessions.source.agent': 'Agent',
309
+ 'sessions.source.cli': 'CLI',
310
+ 'sessions.source.user': '用户',
311
+ 'sessions.source.unknown': 'Unknown',
303
312
  'sessions.selectLog': '选择会话查看 CC 日志',
304
313
  'sessions.chatView': '💬 对话视图',
305
314
  'sessions.fullView': '📜 完整视图',
@@ -309,7 +318,16 @@ const translations = {
309
318
  'sessions.empty.noDialogue': '该会话没有用户对话消息',
310
319
  'sessions.turnType.userInput': '用户输入',
311
320
  'sessions.turnType.toolCall': '工具调用',
321
+ 'sessions.turnType.subtask': '子任务',
312
322
  'sessions.turnType.system': '系统',
323
+ 'sessions.subtask.status.running': '运行中',
324
+ 'sessions.subtask.status.completed': '已完成',
325
+ 'sessions.subtask.status.failed': '失败',
326
+ 'sessions.subtask.status.stopped': '已停止',
327
+ 'sessions.subtask.status.killed': '已终止',
328
+ 'sessions.subtask.tools': '{count} 次工具',
329
+ 'sessions.subtask.tokens': '{count} tokens',
330
+ 'sessions.subtask.reason': '原因:{reason}',
313
331
  'sessions.block.thinking': '思考',
314
332
  'sessions.block.result': '结果',
315
333
  'sessions.block.resultError': '结果',
@@ -374,13 +392,11 @@ const translations = {
374
392
  'monitor.card.systemMemory': '系统内存',
375
393
  'monitor.chart.cpu': 'CPU 占用',
376
394
  'monitor.chart.memory': '内存占用',
377
- 'monitor.chart.activity1h': '近一小时活动',
395
+ 'monitor.chart.disk': '磁盘读写',
378
396
  'monitor.chart.network': '网络占用',
379
- 'monitor.activity.received': '接收',
380
- 'monitor.activity.completed': '完成',
381
- 'monitor.activity.errors': '错误',
382
- 'monitor.activity.interrupts': '中断',
383
- 'monitor.activity.toolErrors': '工具错误',
397
+ 'monitor.series.diskRead': '读取',
398
+ 'monitor.series.diskWrite': '写入',
399
+ 'monitor.series.processTotal': '关联进程',
384
400
  'monitor.series.processRss': 'Daemon 进程组 RSS',
385
401
  'monitor.processes.daemon': '主进程',
386
402
  'monitor.processes.ecweb': 'ECWeb',
@@ -394,7 +410,7 @@ const translations = {
394
410
  'monitor.processes.sandbox': '沙箱',
395
411
  'monitor.processes.runner-helper': 'Runner 辅助',
396
412
  'monitor.processes.child': '关联进程',
397
- 'monitor.series.systemUsed': '系统已用',
413
+ 'monitor.series.systemUsed': '整机系统(已用)',
398
414
  'monitor.series.networkDownload': '下载',
399
415
  'monitor.series.networkUpload': '上传',
400
416
  'monitor.th.received': '收',
@@ -641,6 +657,7 @@ const translations = {
641
657
  'triggers.feedback.defaultFailure': '默认:触发器失败:{{error.message}}',
642
658
  'triggers.field.model': '模型',
643
659
  'triggers.field.effort': '推理强度',
660
+ 'triggers.baseagent.mismatch': 'Trigger 固定:{trigger},Agent 当前:{agent}',
644
661
  'triggers.field.permission': '权限模式',
645
662
  'triggers.delivery.on': '启用',
646
663
  'triggers.delivery.off': '禁用',
@@ -705,7 +722,7 @@ const translations = {
705
722
  // Usage view
706
723
  'usage.subtab.overview': '总览',
707
724
  'usage.subtab.explorer': '详细统计',
708
- 'usage.overview.range.today': '今日',
725
+ 'usage.overview.range.last24': '最近24小时',
709
726
  'usage.overview.range.week': '本周',
710
727
  'usage.overview.range.lastWeek': '上周',
711
728
  'usage.overview.range.month': '本月',
@@ -1154,10 +1171,19 @@ const translations = {
1154
1171
  'sessions.turnType.toolUse': 'Tool Use',
1155
1172
  'sessions.turnType.toolResult': 'Tool Result',
1156
1173
  'sessions.turnType.msgSend': 'Send Message',
1157
- 'sessions.count': '{filtered} / {total} sessions',
1174
+ 'sessions.count': '{count} sessions',
1158
1175
  'sessions.filter.valid': 'Valid',
1159
1176
  'sessions.filter.validTitle': 'Only show valid sessions (at least 1 user message)',
1177
+ 'sessions.filter.all': 'All',
1178
+ 'sessions.filter.allTitle': 'Show all sessions, including sessions without user messages',
1179
+ 'sessions.filter.sourceTitle': 'Filter by session type',
1180
+ 'sessions.filter.sourceAll': 'All types',
1160
1181
  'sessions.messageCountTitle': '{user} user messages / {total} total messages',
1182
+ 'sessions.source.trigger': 'Trigger',
1183
+ 'sessions.source.agent': 'Agent',
1184
+ 'sessions.source.cli': 'CLI',
1185
+ 'sessions.source.user': 'User',
1186
+ 'sessions.source.unknown': 'Unknown',
1161
1187
  'sessions.selectLog': 'Select a session to view the CC log',
1162
1188
  'sessions.chatView': '💬 Chat View',
1163
1189
  'sessions.fullView': '📜 Full View',
@@ -1167,7 +1193,16 @@ const translations = {
1167
1193
  'sessions.empty.noDialogue': 'No user dialogue messages in this session',
1168
1194
  'sessions.turnType.userInput': 'User Input',
1169
1195
  'sessions.turnType.toolCall': 'Tool Call',
1196
+ 'sessions.turnType.subtask': 'Subtask',
1170
1197
  'sessions.turnType.system': 'System',
1198
+ 'sessions.subtask.status.running': 'Running',
1199
+ 'sessions.subtask.status.completed': 'Completed',
1200
+ 'sessions.subtask.status.failed': 'Failed',
1201
+ 'sessions.subtask.status.stopped': 'Stopped',
1202
+ 'sessions.subtask.status.killed': 'Killed',
1203
+ 'sessions.subtask.tools': '{count} tool uses',
1204
+ 'sessions.subtask.tokens': '{count} tokens',
1205
+ 'sessions.subtask.reason': 'Reason: {reason}',
1171
1206
  'sessions.block.thinking': 'Thinking',
1172
1207
  'sessions.block.result': 'Result',
1173
1208
  'sessions.block.resultError': 'Result',
@@ -1232,13 +1267,11 @@ const translations = {
1232
1267
  'monitor.card.systemMemory': 'System Memory',
1233
1268
  'monitor.chart.cpu': 'CPU Usage',
1234
1269
  'monitor.chart.memory': 'Memory Usage',
1235
- 'monitor.chart.activity1h': 'Activity in Last Hour',
1270
+ 'monitor.chart.disk': 'Disk I/O',
1236
1271
  'monitor.chart.network': 'Network Usage',
1237
- 'monitor.activity.received': 'Received',
1238
- 'monitor.activity.completed': 'Completed',
1239
- 'monitor.activity.errors': 'Errors',
1240
- 'monitor.activity.interrupts': 'Interrupts',
1241
- 'monitor.activity.toolErrors': 'ToolErr',
1272
+ 'monitor.series.diskRead': 'Read',
1273
+ 'monitor.series.diskWrite': 'Write',
1274
+ 'monitor.series.processTotal': 'Related Processes',
1242
1275
  'monitor.series.processRss': 'Daemon process group RSS',
1243
1276
  'monitor.processes.daemon': 'Daemon',
1244
1277
  'monitor.processes.ecweb': 'ECWeb',
@@ -1499,6 +1532,7 @@ const translations = {
1499
1532
  'triggers.feedback.defaultFailure': 'Default: 触发器失败:{{error.message}}',
1500
1533
  'triggers.field.model': 'Model',
1501
1534
  'triggers.field.effort': 'Effort',
1535
+ 'triggers.baseagent.mismatch': 'Trigger fixed: {trigger}; current Agent: {agent}',
1502
1536
  'triggers.field.permission': 'Permission Mode',
1503
1537
  'triggers.delivery.on': 'Enabled',
1504
1538
  'triggers.delivery.off': 'Disabled',
@@ -1563,7 +1597,7 @@ const translations = {
1563
1597
  // Usage view
1564
1598
  'usage.subtab.overview': 'Overview',
1565
1599
  'usage.subtab.explorer': 'Detailed Statistics',
1566
- 'usage.overview.range.today': 'Today',
1600
+ 'usage.overview.range.last24': 'Last 24 Hours',
1567
1601
  'usage.overview.range.week': 'This Week',
1568
1602
  'usage.overview.range.lastWeek': 'Last Week',
1569
1603
  'usage.overview.range.month': 'This Month',
@@ -2034,7 +2068,8 @@ let _triggerEdit = null;
2034
2068
  let _triggerHistory = null;
2035
2069
  let _triggerTest = null;
2036
2070
  let sessSearch = '';
2037
- let sessFilterNormal = false; // true=只显示有效会话(userMsgs >= 1)
2071
+ let sessShowAll = false; // false=只显示有效会话;true=显示全部
2072
+ let sessSourceFilter = 'all'; // all / trigger / agent / cli / user / unknown
2038
2073
  let sessChatMode = false; // false=完整视图,true=对话视图(折叠处理过程)
2039
2074
  let monRange = '2m'; // Monitor 时间窗口:2m / 10m / 1h
2040
2075
  let logSel = { source: null, level: 'all', module: 'all', search: '', limit: 500, follow: true };
@@ -2418,8 +2453,13 @@ const MSG_TYPE_META = {
2418
2453
  file: { icon: '📎', label: () => t('messages.msgType.file') },
2419
2454
  command: { icon: '⌘', label: () => t('messages.msgType.command') }
2420
2455
  };
2421
- function msgTagsHtml(kind, encrypt, chatmode, dir) {
2422
- let h = '';
2456
+ // 渠道标:AUN 是默认渠道,不标;其余渠道标出来,免得混排后分不清来路
2457
+ function channelTagHtml(channel, cls) {
2458
+ if (!channel || channel === 'aun') return '';
2459
+ return `<span class="${cls}">${esc(channel)}</span>`;
2460
+ }
2461
+ function msgTagsHtml(kind, encrypt, chatmode, dir, channel) {
2462
+ let h = channelTagHtml(channel, 'mtag mtag-channel');
2423
2463
  // 'send' 仅出向才是「回复」;入向是用户输入,不打回复标记
2424
2464
  const km = (kind === 'send' && dir === 'in') ? null : MSG_KIND_META[kind];
2425
2465
  if (km) h += `<span class="mtag${kind === 'send' ? ' mtag-reply' : ''}">${km.icon}${km.label()}</span>`;
@@ -2429,24 +2469,39 @@ function msgTagsHtml(kind, encrypt, chatmode, dir) {
2429
2469
  }
2430
2470
 
2431
2471
  // 消息行:方向箭头 + 标记 + 对端 + 文字
2472
+ function latestAgentPreviewMessage(s) {
2473
+ const recent = Array.isArray(s.recentMessages) ? s.recentMessages[0] : null;
2474
+ if (recent?.text) return recent;
2475
+ const recvTs = s.lastReceivedAt || 0;
2476
+ const sentTs = s.lastSentAt || 0;
2477
+ if (sentTs > recvTs && s.lastSentText) {
2478
+ return {
2479
+ dir: 'out', peer: s.lastSentTo, text: s.lastSentText, kind: s.lastSentKind,
2480
+ encrypt: s.lastSentEncrypt, chatmode: s.lastSentChatmode, channel: s.lastSentChannel,
2481
+ };
2482
+ }
2483
+ if (s.lastReceivedText) {
2484
+ return {
2485
+ dir: 'in', peer: s.lastReceivedFrom, text: s.lastReceivedText, kind: s.lastReceivedKind,
2486
+ encrypt: s.lastReceivedEncrypt, chatmode: s.lastReceivedChatmode, channel: s.lastReceivedChannel,
2487
+ };
2488
+ }
2489
+ return null;
2490
+ }
2491
+
2432
2492
  function agentPreviewHtml(s) {
2433
2493
  const clip = (t) => esc(String(t).replace(/\n/g, ' ').slice(0, 80));
2434
- const line = (dir, peer, text, kind, encrypt, chatmode) => {
2494
+ const line = (dir, peer, text, kind, encrypt, chatmode, channel) => {
2435
2495
  const arrow = dir === 'in' ? '<span class="arrow-in">↓</span>' : '<span class="arrow-out">↑</span>';
2436
- const tags = msgTagsHtml(kind, encrypt, chatmode, dir);
2496
+ const tags = msgTagsHtml(kind, encrypt, chatmode, dir, channel);
2437
2497
  const peerHtml = peer ? `<span class="peer">${esc(shortAid(peer))}</span>: ` : '';
2438
2498
  const textCls = dir === 'in' ? 'text-in' : 'text-out';
2439
2499
  return `${arrow}${tags ? ' ' + tags + ' ' : ' '}${peerHtml}<span class="${textCls}">${clip(text)}</span>`;
2440
2500
  };
2441
- if ((s.processing || 0) > 0 && s.lastReceivedText)
2442
- return line('in', s.lastReceivedFrom, s.lastReceivedText, s.lastReceivedKind, s.lastReceivedEncrypt, s.lastReceivedChatmode);
2443
- const recvTs = s.lastReceivedAt || 0, sentTs = s.lastSentAt || 0;
2444
- if (!recvTs && !sentTs) return '';
2445
- if (sentTs > recvTs && s.lastSentText)
2446
- return line('out', s.lastSentTo, s.lastSentText, s.lastSentKind, s.lastSentEncrypt, s.lastSentChatmode);
2447
- if (s.lastReceivedText)
2448
- return line('in', s.lastReceivedFrom, s.lastReceivedText, s.lastReceivedKind, s.lastReceivedEncrypt, s.lastReceivedChatmode);
2449
- return '';
2501
+ const latest = latestAgentPreviewMessage(s);
2502
+ return latest
2503
+ ? line(latest.dir, latest.peer, latest.text, latest.kind, latest.encrypt, latest.chatmode, latest.channel)
2504
+ : '';
2450
2505
  }
2451
2506
 
2452
2507
  // HTML tooltip(最近 N 轮):时间 + 彩色箭头 + 方式 + 对端 + 文字
@@ -2462,10 +2517,11 @@ function recentMsgTooltipHtml(recent) {
2462
2517
  const kh = km ? `<span class="tip-kind${m.kind === 'send' ? ' tip-kind-reply' : ''}">${km.icon}${km.label()}</span>` : '';
2463
2518
  const enc = m.encrypt != null ? `<span class="tip-flag">${m.encrypt ? t('messages.tag.encrypted') : t('messages.tag.plain')}</span>` : '';
2464
2519
  const mode = m.chatmode ? `<span class="tip-flag">${m.chatmode === 'proactive' ? t('messages.tag.proactive') : (m.chatmode === 'inject' ? t('messages.tag.inject') : t('messages.tag.responsive'))}</span>` : '';
2520
+ const chan = channelTagHtml(m.channel, 'tip-flag tip-flag-channel');
2465
2521
  const peer = m.peer ? esc(shortAid(m.peer)) : '';
2466
2522
  const text = esc(String(m.text).replace(/\n/g, ' ').slice(0, 140));
2467
2523
  const time = m.ts ? `<span class="tip-time">${fmtTime(m.ts)}</span>` : '';
2468
- h += `<div class="tip-row ${rcls}">${time}${arrow}${kh}${enc}${mode} <b>${peer}</b> ${text}</div>`;
2524
+ h += `<div class="tip-row ${rcls}">${time}${arrow}${chan}${kh}${enc}${mode} <b>${peer}</b> ${text}</div>`;
2469
2525
  }
2470
2526
  return h + '</div>';
2471
2527
  }
@@ -2861,6 +2917,14 @@ function card(label, value, valCls, sub) {
2861
2917
  }
2862
2918
 
2863
2919
  // ── Messages 视图 ──
2920
+ function msgChannelTone(channelType) {
2921
+ const normalized = String(channelType || '').trim().toLowerCase();
2922
+ if (normalized === 'qq') return 'qqbot';
2923
+ return ['aun', 'wechat', 'wecom', 'feishu', 'dingtalk', 'qqbot', 'daemon'].includes(normalized)
2924
+ ? normalized
2925
+ : 'other';
2926
+ }
2927
+
2864
2928
  function renderMsg(data) {
2865
2929
  if (!data) return;
2866
2930
  const aids = data.scopes || data.aids || [];
@@ -2893,7 +2957,7 @@ function renderMsg(data) {
2893
2957
  let aidsHtml = `<div class="col-title"><span>${t('messages.colTitle.aid')}</span><span class="col-count">${aids.length}</span></div>`;
2894
2958
  for (const a of aids) {
2895
2959
  const sel = a.aid === msgSel.aid ? ' sel' : '';
2896
- const name = a.selfAID && a.selfAID !== 'unknown' ? shortAid(a.selfAID) : 'unknown';
2960
+ const name = a.displayName || (a.selfAID && a.selfAID !== 'unknown' ? shortAid(a.selfAID) : 'unknown');
2897
2961
  const privateCount = Math.max(0, (a.peerCount || 0) - (a.groupCount || 0));
2898
2962
  const privateBit = tf('messages.privateCount', { count: privateCount });
2899
2963
  const groupBit = a.groupCount ? ` · ${tf('messages.groupCount', { count: a.groupCount })}` : '';
@@ -2937,7 +3001,9 @@ function renderMsg(data) {
2937
3001
  ? t('messages.tag.topic')
2938
3002
  : p.partition === 'legacy' ? t('messages.tag.legacy') : t('messages.tag.main');
2939
3003
  const partitionMark = `<span class="msg-partition-mark msg-partition-${esc(p.partition || 'main')}">${esc(partitionLabel)}</span>`;
2940
- const channelTag = channelLabel ? `<span class="msg-tag">${esc(channelLabel)}</span>` : '';
3004
+ const channelTag = channelLabel
3005
+ ? `<span class="msg-tag msg-channel msg-channel-${msgChannelTone(p.channelType)}">${esc(channelLabel)}</span>`
3006
+ : '';
2941
3007
  peersHtml += `<div class="list-item${sel}" data-peer="${esc(p.peerId)}">` +
2942
3008
  `<div class="name">${groupMark}${partitionMark}<span class="msg-peer-title" title="${esc(chatTitle)}">${esc(shortId(chatTitle))}</span>${channelTag}</div>` +
2943
3009
  `<div class="sub">${p.partition === 'thread' ? `${esc(shortId(displayName))} · ` : ''}↓${p.inbound} ↑${p.outbound} · ${fmtAgo(p.lastAt)}</div></div>`;
@@ -3024,6 +3090,20 @@ function renderMsgSessionHeader(context) {
3024
3090
  }
3025
3091
 
3026
3092
  // ── Sessions 视图 ──
3093
+ function filterSessionTranscripts(transcripts, showAll, sourceType = 'all') {
3094
+ return (transcripts || []).filter(session => {
3095
+ const valid = showAll || (session.userMsgs || 0) >= 1;
3096
+ const sourceMatches = sourceType === 'all' || (session.sourceType || 'unknown') === sourceType;
3097
+ return valid && sourceMatches;
3098
+ });
3099
+ }
3100
+
3101
+ function renderSessionSourceBadge(sourceType) {
3102
+ const type = ['trigger', 'agent', 'cli', 'user'].includes(sourceType) ? sourceType : 'unknown';
3103
+ const label = t(`sessions.source.${type}`);
3104
+ return `<span class="session-source-badge source-${type}" title="${esc(label)}">${esc(label)}</span>`;
3105
+ }
3106
+
3027
3107
  function renderSession(data) {
3028
3108
  if (!data) return;
3029
3109
  const projects = data.projects || [];
@@ -3038,8 +3118,7 @@ function renderSession(data) {
3038
3118
  }
3039
3119
 
3040
3120
  // 有效筛选
3041
- const filtered = transcripts
3042
- .filter(t => !sessFilterNormal || (t.userMsgs || 0) >= 1);
3121
+ const filtered = filterSessionTranscripts(transcripts, sessShowAll, sessSourceFilter);
3043
3122
 
3044
3123
  // 左栏:过滤条 + 列表
3045
3124
  const projOpts = projects.map(p =>
@@ -3054,18 +3133,25 @@ function renderSession(data) {
3054
3133
  if (availableBaseagents.codex) {
3055
3134
  baseagentOpts += `<option value="codex"${sessSel.baseagent === 'codex' ? ' selected' : ''}>Codex</option>`;
3056
3135
  }
3136
+ const sourceTypes = ['all', 'user', 'agent', 'trigger', 'cli', 'unknown'];
3137
+ const sourceOpts = sourceTypes.map(type => {
3138
+ const label = type === 'all' ? t('sessions.filter.sourceAll') : t(`sessions.source.${type}`);
3139
+ return `<option value="${type}"${sessSourceFilter === type ? ' selected' : ''}>${esc(label)}</option>`;
3140
+ }).join('');
3057
3141
 
3058
- const normalCount = transcripts.filter(t => (t.userMsgs || 0) >= 1).length;
3142
+ const filterLabelKey = sessShowAll ? 'sessions.filter.all' : 'sessions.filter.valid';
3143
+ const filterTitleKey = sessShowAll ? 'sessions.filter.allTitle' : 'sessions.filter.validTitle';
3059
3144
  let listHtml = '<div class="sess-filter">' +
3060
3145
  '<div class="sess-select-row">' +
3061
3146
  `<select id="sess-baseagent" title="Base Agent">${baseagentOpts}</select>` +
3062
3147
  `<select id="sess-project" title="${t('sessions.header.project')}">${projOpts}</select>` +
3148
+ `<select id="sess-source-type" title="${t('sessions.filter.sourceTitle')}">${sourceOpts}</select>` +
3063
3149
  '</div>' +
3064
3150
  '<div class="sess-filter-row">' +
3065
- `<div class="sess-count">${tf('sessions.count', { filtered: filtered.length, total: transcripts.length })}</div>` +
3066
- `<label class="sess-switch" title="${t('sessions.filter.validTitle')}">` +
3067
- `<span>${t('sessions.filter.valid')}</span>` +
3068
- `<input id="sess-filter-toggle" type="checkbox"${sessFilterNormal ? ' checked' : ''}>` +
3151
+ `<div class="sess-count">${tf('sessions.count', { count: filtered.length })}</div>` +
3152
+ `<label class="sess-switch" title="${t(filterTitleKey)}">` +
3153
+ `<span>${t(filterLabelKey)}</span>` +
3154
+ `<input id="sess-filter-toggle" type="checkbox"${sessShowAll ? ' checked' : ''}>` +
3069
3155
  '<span class="sess-switch-track" aria-hidden="true"><span class="sess-switch-thumb"></span></span>' +
3070
3156
  '</label>' +
3071
3157
  '</div></div>' +
@@ -3080,12 +3166,13 @@ function renderSession(data) {
3080
3166
  let badge = '';
3081
3167
  if (t.bound) {
3082
3168
  const dot = t.online ? '<span class="dot on"></span>' : '<span class="dot idle"></span>';
3083
- badge = `<span class="bind-badge">${dot}${esc(t.boundChannel || '')}·${esc(shortAid(t.boundPeer || ''))}</span>`;
3169
+ badge = `<span class="bind-badge">${dot}${esc(t.boundChannel || '')}·${esc(shortAid(t.boundSelf || ''))}</span>`;
3084
3170
  }
3171
+ const sourceBadge = renderSessionSourceBadge(t.sourceType);
3085
3172
  const msgs = `<span class="msg-count" title="${tf('sessions.messageCountTitle', { user: t.userMsgs || 0, total: t.totalMsgs || 0 })}">💬 ${t.userMsgs || 0}/${t.totalMsgs || 0}</span>`;
3086
3173
  listHtml += `<div class="list-item${sel}" data-sid="${esc(t.id)}">` +
3087
3174
  `<div class="name">${esc(title)}</div>` +
3088
- `<div class="sub">${fmtAgo(t.lastActivity)} · ${msgs}${t.gitBranch ? ' · ' + esc(t.gitBranch) : ''}${badge}</div>` +
3175
+ `<div class="sub">${fmtAgo(t.lastActivity)} · ${msgs} · ${sourceBadge}${badge}</div>` +
3089
3176
  '</div>';
3090
3177
  }
3091
3178
  listHtml += '</div>';
@@ -3105,7 +3192,12 @@ function renderSession(data) {
3105
3192
  subscribe('session', { project: sessSel.project, baseagent: sessSel.baseagent });
3106
3193
  };
3107
3194
  const filterToggle = $('#sess-filter-toggle');
3108
- if (filterToggle) filterToggle.onchange = () => { sessFilterNormal = filterToggle.checked; renderSession(state.session); };
3195
+ if (filterToggle) filterToggle.onchange = () => { sessShowAll = filterToggle.checked; renderSession(state.session); };
3196
+ const sourceTypeSelect = $('#sess-source-type');
3197
+ if (sourceTypeSelect) sourceTypeSelect.onchange = () => {
3198
+ sessSourceFilter = sourceTypeSelect.value;
3199
+ renderSession(state.session);
3200
+ };
3109
3201
  $('#sess-list').querySelectorAll('.list-item').forEach(item => {
3110
3202
  item.onclick = () => { sessSel = { sessionId: item.dataset.sid, project: sessSel.project, baseagent: sessSel.baseagent }; subscribe('session', sessSel); };
3111
3203
  });
@@ -3224,6 +3316,7 @@ const CAT_META = {
3224
3316
  tool_call: { label: () => t('sessions.turnType.toolCall'), icon: '🟣' },
3225
3317
  tool_result: { label: () => t('sessions.turnType.toolResult'), icon: '🟠' },
3226
3318
  msg_send: { label: () => t('sessions.turnType.msgSend'), icon: '📤' },
3319
+ subtask: { label: () => t('sessions.turnType.subtask'), icon: '◆' },
3227
3320
  system: { label: () => t('sessions.turnType.system'), icon: '⚪' },
3228
3321
  };
3229
3322
 
@@ -3240,7 +3333,7 @@ function renderSessHeader(h, baseagent) {
3240
3333
  let bind = '';
3241
3334
  if (h.bound) {
3242
3335
  const dot = h.online ? `<span class="dot on"></span>${t('common.online')}` : `<span class="dot idle"></span>${t('common.offline')}`;
3243
- bind = `<span class="sh-stat">🔗 ${esc(h.boundChannel || '')} · ${esc(shortAid(h.boundPeer || ''))} ${dot}</span>`;
3336
+ bind = `<span class="sh-stat">🔗 ${esc(h.boundChannel || '')} · ${esc(shortAid(h.boundSelf || ''))} ${dot}</span>`;
3244
3337
  }
3245
3338
  return '<div class="sess-header">' +
3246
3339
  '<div class="sh-title-row">' +
@@ -3273,6 +3366,7 @@ function renderCatBar(counts) {
3273
3366
  ['tool_call', counts.toolCall],
3274
3367
  ['tool_result', counts.toolResult],
3275
3368
  ['msg_send', counts.msgSend],
3369
+ ['subtask', counts.subtask],
3276
3370
  ];
3277
3371
  let s = '<div class="sh-cats">';
3278
3372
  for (const [cat, n] of items) {
@@ -3320,6 +3414,28 @@ function renderBlocks(blocks) {
3320
3414
  } else if (b.kind === 'tool_result') {
3321
3415
  const cls = b.isError ? 'blk-result err' : 'blk-result';
3322
3416
  out += `<details class="blk ${cls}" open><summary>${b.isError ? '✗ ' + t('sessions.block.resultError') : '↳ ' + t('sessions.block.result')}</summary><pre class="result-body">${esc(b.text)}</pre></details>`;
3417
+ } else if (b.kind === 'subtask') {
3418
+ const knownStatuses = ['running', 'completed', 'failed', 'stopped', 'killed'];
3419
+ const status = knownStatuses.includes(String(b.status)) ? String(b.status) : 'running';
3420
+ const statusLabel = t(`sessions.subtask.status.${status}`);
3421
+ const meta = [];
3422
+ if (b.subagentType) meta.push(esc(b.subagentType));
3423
+ if (b.toolUses) meta.push(esc(tf('sessions.subtask.tools', { count: fmtNum(b.toolUses) })));
3424
+ if (b.totalTokens) meta.push(esc(tf('sessions.subtask.tokens', { count: fmtNum(b.totalTokens) })));
3425
+ if (b.durationMs) meta.push(esc(fmtDur(b.durationMs / 1000)));
3426
+ if (b.terminalReason) meta.push(esc(tf('sessions.subtask.reason', { reason: b.terminalReason })));
3427
+ const progress = b.progress && b.progress !== b.description
3428
+ ? `<div class="subtask-progress">${esc(b.progress)}</div>`
3429
+ : '';
3430
+ out += `<div class="blk blk-subtask status-${status}">` +
3431
+ '<div class="subtask-main">' +
3432
+ `<span class="subtask-status-dot" aria-hidden="true"></span>` +
3433
+ `<span class="subtask-status">${esc(statusLabel)}</span>` +
3434
+ `<span class="subtask-title">${esc(b.description || b.taskId || '')}</span>` +
3435
+ '</div>' +
3436
+ (meta.length ? `<div class="subtask-meta">${meta.join('<span>·</span>')}</div>` : '') +
3437
+ progress +
3438
+ '</div>';
3323
3439
  }
3324
3440
  }
3325
3441
  return out;
@@ -5478,12 +5594,18 @@ function triggerOriginalEditState(t) {
5478
5594
  const id = triggerIdOf(t);
5479
5595
  const agentAid = triggerAgentOf(t);
5480
5596
  const rawFeedbackStrategy = String(feedback.strategy ?? t?.feedbackStrategy ?? 'target');
5597
+ const executionType = String(execution.type ?? t?.executionType ?? 'target_session');
5598
+ const agentBaseagent = String(triggerAgentInfo(agentAid)?.baseagent ?? triggerActiveBaseagentForAgent(agentAid));
5599
+ const persistedBaseagent = typeof execution.baseagent === 'string' && execution.baseagent.trim()
5600
+ ? execution.baseagent.trim()
5601
+ : (typeof t?.baseagent === 'string' && t.baseagent.trim() ? t.baseagent.trim() : '');
5602
+ const baseagent = persistedBaseagent || agentBaseagent;
5481
5603
  return {
5482
5604
  id,
5483
5605
  agentAid,
5484
5606
  name: String(definition?.name ?? t?.name ?? t?.label ?? ''),
5485
5607
  enabled: triggerIsEnabled(t),
5486
- executionType: String(execution.type ?? t?.executionType ?? 'target_session'),
5608
+ executionType,
5487
5609
  scriptPath: String(execution.script?.path ?? ''),
5488
5610
  scriptRuntime: String(execution.script?.runtime ?? 'node'),
5489
5611
  scriptTimeoutMs: Number(execution.script?.timeoutMs ?? 30_000),
@@ -5491,7 +5613,11 @@ function triggerOriginalEditState(t) {
5491
5613
  scheduleValue: source.scheduleValue,
5492
5614
  eventFilter: definition?.source?.type === 'event' ? (definition.source.filter ?? null) : null,
5493
5615
  timezone: String(source.timezone ?? ''),
5494
- baseagent: String(triggerAgentInfo(agentAid)?.baseagent ?? t?.baseagent ?? triggerActiveBaseagentForAgent(agentAid)),
5616
+ baseagent,
5617
+ agentBaseagent,
5618
+ baseagentMismatch: executionType === 'trigger_session'
5619
+ && Boolean(persistedBaseagent)
5620
+ && persistedBaseagent !== agentBaseagent,
5495
5621
  prompt: execution.type === 'script' ? '' : triggerPrompt(definition, t),
5496
5622
  model: String(execution.model ?? t?.model ?? ''),
5497
5623
  effort: String(execution.effort ?? t?.effort ?? ''),
@@ -6714,6 +6840,7 @@ function openTriggerEditDrawer(id, agent) {
6714
6840
  `<label class="ag-edit-field trig-script-field"><span>${t('triggers.field.scriptRuntime')}</span><select id="trig-edit-script-runtime">${triggerOptionHtml(['node', 'python', 'bash'], original.scriptRuntime, null)}</select></label>` +
6715
6841
  `<label class="ag-edit-field trig-script-field trig-script-path-field"><span>${t('triggers.field.scriptPath')}</span><input id="trig-edit-script-path" type="text" autocomplete="off" spellcheck="false" value="${esc(original.scriptPath)}" placeholder="${t('triggers.placeholder.scriptPath')}"></label>` +
6716
6842
  `</div>` +
6843
+ (original.baseagentMismatch ? `<div class="trig-baseagent-note">${esc(tf('triggers.baseagent.mismatch', { trigger: original.baseagent, agent: original.agentBaseagent }))}</div>` : '') +
6717
6844
  `<div class="trig-script-feedback">` +
6718
6845
  `<div class="trig-feedback-templates-title">${t('triggers.field.outputTemplate')}</div>` +
6719
6846
  `<div class="trig-feedback-templates">` +
@@ -6994,6 +7121,13 @@ function updateThemeButton() {
6994
7121
  }
6995
7122
 
6996
7123
  function disposeWindowChart(key) {
7124
+ const observerKey = '__ecwebChartResizeObserver_' + key;
7125
+ const observer = window[observerKey];
7126
+ if (observer) {
7127
+ observer.disconnect();
7128
+ window[observerKey] = null;
7129
+ }
7130
+
6997
7131
  const chart = window[key];
6998
7132
  if (chart && typeof chart.dispose === 'function') {
6999
7133
  chart.dispose();
@@ -7002,7 +7136,7 @@ function disposeWindowChart(key) {
7002
7136
  }
7003
7137
 
7004
7138
  function refreshThemeSensitiveViews() {
7005
- ['_hourlyChart', '_modelChart', '_monCpu', '_monMem', '_monMsg', '_monNet'].forEach(disposeWindowChart);
7139
+ ['_hourlyChart', '_modelChart', '_monCpu', '_monMem', '_monDisk', '_monNet'].forEach(disposeWindowChart);
7006
7140
  if (typeof _explorerChart !== 'undefined' && _explorerChart) {
7007
7141
  _explorerChart.dispose();
7008
7142
  _explorerChart = null;
@@ -7052,6 +7186,28 @@ function initTheme() {
7052
7186
  }
7053
7187
  }
7054
7188
 
7189
+ function chartThemeValue(name, fallback) {
7190
+ var value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
7191
+ return value || fallback;
7192
+ }
7193
+
7194
+ function chartTooltipOptions(isDark) {
7195
+ return {
7196
+ backgroundColor: chartThemeValue('--bg2', isDark ? '#161b22' : '#ffffff'),
7197
+ borderColor: chartThemeValue('--border', isDark ? '#30363d' : '#e2e6ed'),
7198
+ borderWidth: 1,
7199
+ padding: [10, 12],
7200
+ textStyle: {
7201
+ color: chartThemeValue('--fg', isDark ? '#e6edf3' : '#1a202c'),
7202
+ fontSize: 12,
7203
+ lineHeight: 20,
7204
+ },
7205
+ extraCssText: 'border-radius:8px;box-shadow:' + (isDark
7206
+ ? '0 14px 34px rgba(0,0,0,0.42)'
7207
+ : '0 14px 34px rgba(31,41,55,0.16)') + ';',
7208
+ };
7209
+ }
7210
+
7055
7211
  // ── Usage 相关函数 ──
7056
7212
  function fmtTokens(n) {
7057
7213
  if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
@@ -7060,7 +7216,12 @@ function fmtTokens(n) {
7060
7216
  }
7061
7217
 
7062
7218
  // ── Usage Overview(总览,支持日期范围筛选)──
7063
- let _ovCurrentRange = 'today'; // 当前选择的范围
7219
+ let _ovCurrentRange = 'last24'; // 当前选择的范围
7220
+
7221
+ function rollingLast24HourRange(now) {
7222
+ const toTs = (now || new Date()).getTime();
7223
+ return { fromTs: toTs - 24 * 60 * 60 * 1000, toTs };
7224
+ }
7064
7225
 
7065
7226
  async function loadUsageOverview(rangeType, customFrom, customTo) {
7066
7227
  rangeType = rangeType || _ovCurrentRange;
@@ -7073,9 +7234,8 @@ async function loadUsageOverview(rangeType, customFrom, customTo) {
7073
7234
  const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime();
7074
7235
 
7075
7236
  switch (rangeType) {
7076
- case 'today':
7077
- fromTs = todayStart;
7078
- toTs = todayEnd;
7237
+ case 'last24':
7238
+ ({ fromTs, toTs } = rollingLast24HourRange(now));
7079
7239
  break;
7080
7240
  case 'week': // 本周(周一到今天)
7081
7241
  const dayOfWeek = now.getDay() || 7; // 周日=7
@@ -7104,8 +7264,7 @@ async function loadUsageOverview(rangeType, customFrom, customTo) {
7104
7264
  fromTs = new Date(customFrom).getTime();
7105
7265
  toTs = new Date(customTo).getTime();
7106
7266
  } else {
7107
- fromTs = todayStart;
7108
- toTs = todayEnd;
7267
+ ({ fromTs, toTs } = rollingLast24HourRange(now));
7109
7268
  }
7110
7269
  break;
7111
7270
  default:
@@ -7256,12 +7415,19 @@ function formatDatetimeLocal(date) {
7256
7415
  }
7257
7416
 
7258
7417
  // 模型访问明细查询
7418
+ var _detailInited = false;
7419
+
7259
7420
  function initDetailQuery() {
7260
7421
  // 填充Agent选择器
7261
7422
  loadDetailAgentList();
7262
7423
  // 填充Model选择器(按上面总览的时间范围)
7263
7424
  loadDetailModelList();
7264
7425
 
7426
+ // 事件只绑一次:initOverviewFilters 会在每次切回 Overview 子 Tab 时重复调用,
7427
+ // 重复绑定会让一次 change 触发 N 次并发查询,慢的旧响应可能覆盖新响应(表现为“切换无效”)。
7428
+ if (_detailInited) return;
7429
+ _detailInited = true;
7430
+
7265
7431
  // 绑定分页大小变化
7266
7432
  const pageSizeEl = $('#detail-page-size');
7267
7433
  if (pageSizeEl) {
@@ -7348,10 +7514,8 @@ async function loadDetailAgentList() {
7348
7514
  option.textContent = a.agent_name || a.agent_aid.split('.')[0];
7349
7515
  selectEl.appendChild(option);
7350
7516
  });
7351
- // 默认选中第一个agent
7352
- if (agents.length > 0) {
7353
- selectEl.value = agents[0].agent_aid;
7354
- }
7517
+ // 默认查询全部智能体(首项 value=""),不要自动切到第一个 Agent。
7518
+ selectEl.value = '';
7355
7519
  // 加载完成后自动查询一次
7356
7520
  queryDetailUsage();
7357
7521
  }
@@ -7395,7 +7559,11 @@ async function loadDetailModelList() {
7395
7559
  } catch {}
7396
7560
  }
7397
7561
 
7562
+ var _detailQuerySeq = 0;
7563
+
7398
7564
  async function queryDetailUsage() {
7565
+ // 请求序号:并发查询时只认最后一次的响应,避免慢的旧响应覆盖新结果
7566
+ const seq = ++_detailQuerySeq;
7399
7567
  // 使用总览的时间范围
7400
7568
  const timeRange = window._currentOverviewTimeRange || {};
7401
7569
  const fromTs = timeRange.fromTs;
@@ -7422,13 +7590,16 @@ async function queryDetailUsage() {
7422
7590
  const resp = await fetch(authApiUrl('api/stats/detail?' + params.toString()), {
7423
7591
  headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
7424
7592
  });
7593
+ if (seq !== _detailQuerySeq) return; // 已有更新的查询发出,丢弃本次结果
7425
7594
  if (!resp.ok) {
7426
7595
  showDetailError(t('usage.detail.error'));
7427
7596
  return;
7428
7597
  }
7429
7598
  const result = await resp.json();
7599
+ if (seq !== _detailQuerySeq) return;
7430
7600
  renderDetailTable(result.data, result.total, page, pageSize);
7431
7601
  } catch {
7602
+ if (seq !== _detailQuerySeq) return;
7432
7603
  showDetailError(t('usage.detail.error'));
7433
7604
  }
7434
7605
  }
@@ -7559,7 +7730,7 @@ var _usageSubtabsBound = false;
7559
7730
 
7560
7731
  function activeOverviewRange() {
7561
7732
  const active = document.querySelector('.ov-range-btn.active');
7562
- return active ? active.getAttribute('data-range') || 'today' : 'today';
7733
+ return active ? active.getAttribute('data-range') || 'last24' : 'last24';
7563
7734
  }
7564
7735
 
7565
7736
  function refreshUsageView() {
@@ -7609,16 +7780,16 @@ function initUsageSubtabs() {
7609
7780
  });
7610
7781
  });
7611
7782
 
7612
- // 初始化总览页面的过滤器并加载默认数据(今日)
7783
+ // 初始化总览页面的过滤器并加载默认数据(最近 24 小时)
7613
7784
  initOverviewFilters();
7614
- loadUsageOverview('today');
7785
+ loadUsageOverview('last24');
7615
7786
  }
7616
7787
 
7617
7788
  // ── Explorer ──
7618
7789
  var _explorerChart = null;
7619
7790
  var _explorerInited = false;
7620
7791
  var _expSelection = { type: null, key: null }; // { type: 'agent'|'peer', key: string } or null
7621
- var _expCurrentRange = 'today'; // Explorer 当前选择的时间范围
7792
+ var _expCurrentRange = 'last24'; // Explorer 当前选择的时间范围
7622
7793
  var _expTimeRange = { fromTs: null, toTs: null }; // Explorer 的时间范围
7623
7794
 
7624
7795
  function initExplorer() {
@@ -7628,9 +7799,11 @@ function initExplorer() {
7628
7799
  // 初始化时间范围选择
7629
7800
  initExplorerTimeFilters();
7630
7801
 
7631
- // 绑定查询按钮
7632
- var btn = $('#exp-query-btn');
7633
- if (btn) btn.onclick = runExplorerQuery;
7802
+ // 模型、粒度下拉框变更后立即查询,不再需要额外的查询按钮。
7803
+ var modelFilter = $('#exp-model');
7804
+ if (modelFilter) modelFilter.addEventListener('change', runExplorerQuery);
7805
+ var granularityFilter = $('#exp-granularity');
7806
+ if (granularityFilter) granularityFilter.addEventListener('change', runExplorerQuery);
7634
7807
 
7635
7808
  // Load sidebar lists
7636
7809
  loadExplorerSidebar();
@@ -7653,6 +7826,7 @@ function initExplorerTimeFilters() {
7653
7826
  if (customDateEl) customDateEl.style.display = 'none';
7654
7827
  _expCurrentRange = range;
7655
7828
  calculateExplorerTimeRange(range);
7829
+ setExplorerGranularityForRange(range);
7656
7830
  loadExplorerModels(); // 加载可用模型
7657
7831
  runExplorerQuery();
7658
7832
  }
@@ -7675,15 +7849,22 @@ function initExplorerTimeFilters() {
7675
7849
  });
7676
7850
  }
7677
7851
 
7678
- // 设置默认时间范围(今日)并初始化日期选择器
7852
+ // 设置默认时间范围(最近 24 小时)并初始化日期选择器
7679
7853
  const now = new Date();
7854
+ const last24 = rollingLast24HourRange(now);
7680
7855
  const fromEl = $('#exp-from');
7681
7856
  const toEl = $('#exp-to');
7682
- if (fromEl) fromEl.value = formatDatetimeLocal(new Date(now.getFullYear(), now.getMonth(), now.getDate()));
7857
+ if (fromEl) fromEl.value = formatDatetimeLocal(new Date(last24.fromTs));
7683
7858
  if (toEl) toEl.value = formatDatetimeLocal(now);
7684
7859
 
7685
- // 计算默认时间范围(今日)
7686
- calculateExplorerTimeRange('today');
7860
+ // 计算默认时间范围(最近 24 小时)
7861
+ calculateExplorerTimeRange('last24');
7862
+ setExplorerGranularityForRange('last24');
7863
+ }
7864
+
7865
+ function setExplorerGranularityForRange(rangeType) {
7866
+ const granularityEl = $('#exp-granularity');
7867
+ if (rangeType === 'last24' && granularityEl) granularityEl.value = 'hour';
7687
7868
  }
7688
7869
 
7689
7870
  // 计算 Explorer 的时间范围
@@ -7693,9 +7874,8 @@ function calculateExplorerTimeRange(rangeType) {
7693
7874
  const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime();
7694
7875
 
7695
7876
  switch (rangeType) {
7696
- case 'today':
7697
- _expTimeRange.fromTs = todayStart;
7698
- _expTimeRange.toTs = todayEnd;
7877
+ case 'last24':
7878
+ _expTimeRange = rollingLast24HourRange(now);
7699
7879
  break;
7700
7880
  case 'week':
7701
7881
  const dayOfWeek = now.getDay() || 7;
@@ -8070,7 +8250,10 @@ async function runExplorerQuery() {
8070
8250
  _explorerChart = echarts.init(chartEl, isDark ? 'dark' : null);
8071
8251
  var periods = data.map(function(r) { return r.period; });
8072
8252
  _explorerChart.setOption({
8073
- tooltip: { trigger: 'axis' },
8253
+ tooltip: {
8254
+ trigger: 'axis',
8255
+ ...chartTooltipOptions(isDark),
8256
+ },
8074
8257
  legend: { data: [t('usage.card.input'), t('usage.card.output')], top: 0, textStyle: { fontSize: 11 } },
8075
8258
  grid: { top: 30, bottom: 30, left: 60, right: 16 },
8076
8259
  xAxis: { type: 'category', data: periods, axisLabel: { fontSize: 10, rotate: 30 } },
@@ -8108,12 +8291,58 @@ function monitorLayoutHtml() {
8108
8291
  '</div>' +
8109
8292
  `<div id="mon-cards" class="usage-cards"><div class="empty" style="grid-column:1/-1">${t('common.loading')}</div></div>` +
8110
8293
  '<div class="mon-row2"><div id="mon-cpu-chart" class="mon-chart"></div><div id="mon-mem-chart" class="mon-chart"></div></div>' +
8111
- '<div class="mon-row2"><div id="mon-msg-chart" class="mon-chart"></div><div id="mon-net-chart" class="mon-chart"></div></div>' +
8294
+ '<div class="mon-row2"><div id="mon-disk-chart" class="mon-chart"></div><div id="mon-net-chart" class="mon-chart"></div></div>' +
8112
8295
  '</div>';
8113
8296
  }
8114
8297
 
8115
8298
  function disposeMonitorCharts() {
8116
- ['_monCpu', '_monMem', '_monMsg', '_monNet'].forEach(disposeWindowChart);
8299
+ ['_monCpu', '_monMem', '_monDisk', '_monNet'].forEach(disposeWindowChart);
8300
+ }
8301
+
8302
+ function scheduleAfterLayout(callback) {
8303
+ if (typeof requestAnimationFrame === 'function') return requestAnimationFrame(callback);
8304
+ return setTimeout(callback, 16);
8305
+ }
8306
+
8307
+ function resizeChartAfterLayout(chart) {
8308
+ scheduleAfterLayout(function () {
8309
+ if (!chart || (typeof chart.isDisposed === 'function' && chart.isDisposed())) return;
8310
+ chart.resize();
8311
+ });
8312
+ }
8313
+
8314
+ function observeMonitorChartSize(el, varKey, chart) {
8315
+ const observerKey = '__ecwebChartResizeObserver_' + varKey;
8316
+ const previous = window[observerKey];
8317
+ if (previous) previous.disconnect();
8318
+
8319
+ if (typeof ResizeObserver === 'function') {
8320
+ const observer = new ResizeObserver(function () {
8321
+ if (window[varKey] !== chart || (typeof chart.isDisposed === 'function' && chart.isDisposed())) {
8322
+ observer.disconnect();
8323
+ return;
8324
+ }
8325
+ chart.resize();
8326
+ });
8327
+ observer.observe(el);
8328
+ window[observerKey] = observer;
8329
+ }
8330
+
8331
+ resizeChartAfterLayout(chart);
8332
+ }
8333
+
8334
+ function deferMonitorChartUntilVisible(el, render) {
8335
+ let attempts = 0;
8336
+ const retry = function () {
8337
+ if (!el.isConnected) return;
8338
+ if (el.clientWidth > 0 && el.clientHeight > 0) {
8339
+ render();
8340
+ return;
8341
+ }
8342
+ // The AUN proxy can deliver the first snapshot before its page layout settles.
8343
+ if (++attempts < 30) scheduleAfterLayout(retry);
8344
+ };
8345
+ scheduleAfterLayout(retry);
8117
8346
  }
8118
8347
 
8119
8348
  function bindMonRangeTabs(wrap) {
@@ -8136,36 +8365,65 @@ function formatMonitorResponse(ms) {
8136
8365
  return value >= 10000 ? (value / 1000).toFixed(1) + 's' : Math.round(value) + 'ms';
8137
8366
  }
8138
8367
 
8139
- function monitorProcessLabel(process) {
8368
+ function monitorProcessKind(process) {
8140
8369
  var knownKinds = ['daemon', 'ecweb', 'watch-web', 'restart-monitor', 'watch', 'watch-aid', 'codex-app-server', 'claude-runner', 'gemini-runner', 'sandbox', 'runner-helper', 'child'];
8141
- var kind = knownKinds.includes(process.kind) ? process.kind : 'child';
8142
- return `${t('monitor.processes.' + kind)} · ${process.name || 'process'} · #${process.pid}`;
8370
+ return knownKinds.includes(process.kind) ? process.kind : 'child';
8371
+ }
8372
+
8373
+ function monitorProcessKey(process) {
8374
+ return `${monitorProcessKind(process)}\u0000${process.name || 'process'}`;
8375
+ }
8376
+
8377
+ function monitorProcessLabel(process) {
8378
+ return `${t('monitor.processes.' + monitorProcessKind(process))} · ${process.name || 'process'}`;
8143
8379
  }
8144
8380
 
8145
- function monitorProcessColor(pid) {
8381
+ function monitorProcessColor(key) {
8146
8382
  var colors = ['#4f6ef7', '#38a169', '#dd6b20', '#805ad5', '#d53f8c', '#319795', '#e53e3e', '#3182ce', '#718096', '#b7791f'];
8147
- var value = Math.abs(Number(pid) || 0);
8383
+ var value = 0;
8384
+ for (var i = 0; i < key.length; i += 1) value = ((value * 31) + key.charCodeAt(i)) | 0;
8385
+ value = Math.abs(value);
8148
8386
  return colors[value % colors.length];
8149
8387
  }
8150
8388
 
8151
8389
  function monitorProcessSeries(history, currentProcesses, metric) {
8152
8390
  var catalog = new Map();
8153
- for (const process of (currentProcesses || [])) catalog.set(process.pid, process);
8391
+ function addProcess(process) {
8392
+ var key = monitorProcessKey(process);
8393
+ if (!catalog.has(key)) catalog.set(key, { key: key, kind: monitorProcessKind(process), name: process.name || 'process' });
8394
+ }
8395
+ for (const process of (currentProcesses || [])) addProcess(process);
8154
8396
  for (const point of history) {
8155
- for (const process of (point.processes || [])) catalog.set(process.pid, process);
8397
+ for (const process of (point.processes || [])) addProcess(process);
8156
8398
  }
8157
- return [...catalog.values()].sort(function (a, b) { return a.pid - b.pid; }).map(function (process) {
8399
+ return [...catalog.values()].sort(function (a, b) { return monitorProcessLabel(a).localeCompare(monitorProcessLabel(b)); }).map(function (process) {
8158
8400
  return {
8159
8401
  name: monitorProcessLabel(process),
8160
8402
  data: history.map(function (point) {
8161
- var sample = (point.processes || []).find(function (item) { return item.pid === process.pid; });
8162
- return sample ? sample[metric] : null;
8403
+ var value = 0;
8404
+ var hasSample = false;
8405
+ for (const sample of (point.processes || [])) {
8406
+ if (monitorProcessKey(sample) !== process.key) continue;
8407
+ value += Number(sample[metric]) || 0;
8408
+ hasSample = true;
8409
+ }
8410
+ return hasSample ? value : null;
8163
8411
  }),
8164
- color: monitorProcessColor(process.pid),
8412
+ color: monitorProcessColor(process.key),
8165
8413
  };
8166
8414
  });
8167
8415
  }
8168
8416
 
8417
+ function monitorAggregateSeries(name, history, metric, color) {
8418
+ return {
8419
+ name: name,
8420
+ data: history.map(function (point) { return point[metric] != null ? point[metric] : null; }),
8421
+ color: color,
8422
+ width: 3,
8423
+ areaOpacity: 0.12,
8424
+ };
8425
+ }
8426
+
8169
8427
  function renderMonitor(data) {
8170
8428
  var wrap = $('#sys-content .mon-layout');
8171
8429
  if (!wrap) return;
@@ -8187,6 +8445,8 @@ function renderMonitor(data) {
8187
8445
  var lh = (s.stats && s.stats.lastHour) || {};
8188
8446
  var errRate = (lh.received > 0) ? ((lh.errors / lh.received) * 100).toFixed(1) + '%' : '0%';
8189
8447
  var processTree = s.processTree || {};
8448
+ var cpuCount = Math.max(1, Number(s.cpuCount) || 1);
8449
+ var processCpuPercent = (Number(s.cpuPercent) || 0) / cpuCount;
8190
8450
 
8191
8451
  // ── Stat cards ──
8192
8452
  var sysMemPct = (sys.memTotal > 0) ? Math.round((sys.memUsed / sys.memTotal) * 100) : 0;
@@ -8195,7 +8455,7 @@ function renderMonitor(data) {
8195
8455
  [t('monitor.card.messages1h'), lh.received || 0],
8196
8456
  [t('monitor.card.avgResponse'), formatMonitorResponse(lh.avgResponseMs)],
8197
8457
  [t('monitor.card.errorRate'), errRate],
8198
- [t('monitor.card.processCpu'), (s.cpuPercent != null ? s.cpuPercent : 0) + '%'],
8458
+ [t('monitor.card.processCpu'), processCpuPercent.toFixed(1) + '%'],
8199
8459
  [t('monitor.card.systemCpu'), (sys.cpuPercent != null ? sys.cpuPercent : 0) + '%'],
8200
8460
  [t('monitor.card.processMemory'), fmtBytes(s.memory ? s.memory.rss : 0)],
8201
8461
  [t('monitor.card.processCount'), processTree.processCount || 1],
@@ -8211,52 +8471,32 @@ function renderMonitor(data) {
8211
8471
  var cProc = css('--accent'), cSys = css('--orange');
8212
8472
 
8213
8473
  var currentProcesses = Array.isArray(processTree.processes) ? processTree.processes : [];
8214
- var cpuSeries = monitorProcessSeries(h, currentProcesses, 'cpuPercent');
8215
- cpuSeries.push({ name: t('monitor.legend.system'), data: h.map(function (p) { return p.sysCpu != null ? p.sysCpu : null; }), color: cSys });
8474
+ var cpuSeries = [
8475
+ monitorAggregateSeries(t('monitor.legend.system'), h, 'sysCpu', cSys),
8476
+ monitorAggregateSeries(t('monitor.series.processTotal'), h, 'procCpu', cProc),
8477
+ ].concat(monitorProcessSeries(h, currentProcesses, 'cpuPercent'));
8216
8478
 
8217
- // ── CPU:每个关联 PID + 系统 ──
8479
+ // ── CPU:整机系统、关联进程、每类关联进程 ──
8218
8480
  monDualLine('mon-cpu-chart', '_monCpu', ts, isDark, t('monitor.chart.cpu'),
8219
8481
  cpuSeries,
8220
8482
  function (v) { return Number(v).toFixed(1) + '%'; }, null);
8221
8483
 
8222
- // ── Memory:每个关联 PID ──
8484
+ // ── Memory:整机系统、关联进程、每类关联进程 ──
8485
+ var memorySeries = [
8486
+ monitorAggregateSeries(t('monitor.series.systemUsed'), h, 'sysMemUsed', cSys),
8487
+ monitorAggregateSeries(t('monitor.series.processTotal'), h, 'procRss', cProc),
8488
+ ].concat(monitorProcessSeries(h, currentProcesses, 'rss'));
8223
8489
  monDualLine('mon-mem-chart', '_monMem', ts, isDark, t('monitor.chart.memory'),
8224
- monitorProcessSeries(h, currentProcesses, 'rss'),
8490
+ memorySeries,
8225
8491
  function (v) { return fmtBytes(v); }, null);
8226
8492
 
8227
- // ── Message activity bar chart ──
8228
- var msgEl = $('#mon-msg-chart');
8229
- if (msgEl) {
8230
- if (!window._monMsg) window._monMsg = echarts.init(msgEl, isDark ? 'dark' : null);
8231
- window._monMsg.setOption({
8232
- title: { text: t('monitor.chart.activity1h'), left: 'center', top: 4, textStyle: { fontSize: 12, color: isDark ? '#e6edf3' : '#1a202c' } },
8233
- tooltip: { trigger: 'axis' },
8234
- grid: { top: 36, bottom: 24, left: 44, right: 12 },
8235
- xAxis: {
8236
- type: 'category',
8237
- data: [
8238
- t('monitor.activity.received'),
8239
- t('monitor.activity.completed'),
8240
- t('monitor.activity.errors'),
8241
- t('monitor.activity.interrupts'),
8242
- t('monitor.activity.toolErrors'),
8243
- ],
8244
- axisLabel: { fontSize: 9 },
8245
- },
8246
- yAxis: { type: 'value', minInterval: 1 },
8247
- series: [{
8248
- type: 'bar', barWidth: '45%',
8249
- data: [
8250
- { value: lh.received || 0, itemStyle: { color: css('--accent') } },
8251
- { value: lh.completed || 0, itemStyle: { color: css('--green') } },
8252
- { value: lh.errors || 0, itemStyle: { color: css('--red') } },
8253
- { value: lh.interrupts || 0, itemStyle: { color: css('--orange') } },
8254
- { value: lh.toolErrors || 0, itemStyle: { color: css('--blue') } },
8255
- ],
8256
- }],
8257
- animation: false,
8258
- });
8259
- }
8493
+ // ── Disk throughput ──
8494
+ monDualLine('mon-disk-chart', '_monDisk', ts, isDark, t('monitor.chart.disk'),
8495
+ [
8496
+ { name: t('monitor.series.diskRead'), data: h.map(function (p) { return p.diskReadBps || 0; }), color: css('--blue') || cProc },
8497
+ { name: t('monitor.series.diskWrite'), data: h.map(function (p) { return p.diskWriteBps || 0; }), color: cSys },
8498
+ ],
8499
+ function (v) { return fmtBytes(v) + '/s'; }, null);
8260
8500
 
8261
8501
  // ── Network throughput ──
8262
8502
  monDualLine('mon-net-chart', '_monNet', ts, isDark, t('monitor.chart.network'),
@@ -8268,12 +8508,22 @@ function renderMonitor(data) {
8268
8508
 
8269
8509
  }
8270
8510
 
8271
- // 双线时序图(进程 + 系统)。series: [{name,data,color}]
8511
+ // 时序图。series: [{name,data,color,width?,areaOpacity?}]
8272
8512
  function monDualLine(elId, varKey, times, isDark, title, series, fmtY, yRange) {
8273
8513
  var el = $('#' + elId);
8274
8514
  if (!el) return;
8275
- if (!window[varKey]) window[varKey] = echarts.init(el, isDark ? 'dark' : null);
8276
- window[varKey].setOption({
8515
+ const render = function () {
8516
+ var chart = window[varKey];
8517
+ if (chart && typeof chart.getDom === 'function' && chart.getDom() !== el) {
8518
+ disposeWindowChart(varKey);
8519
+ chart = null;
8520
+ }
8521
+ if (!chart) {
8522
+ chart = echarts.init(el, isDark ? 'dark' : null);
8523
+ window[varKey] = chart;
8524
+ observeMonitorChartSize(el, varKey, chart);
8525
+ }
8526
+ chart.setOption({
8277
8527
  title: { text: title, left: 'center', top: 4, textStyle: { fontSize: 12, color: isDark ? '#e6edf3' : '#1a202c' } },
8278
8528
  legend: {
8279
8529
  show: series.length > 0,
@@ -8286,6 +8536,7 @@ function monDualLine(elId, varKey, times, isDark, title, series, fmtY, yRange) {
8286
8536
  },
8287
8537
  tooltip: {
8288
8538
  trigger: 'axis',
8539
+ ...chartTooltipOptions(isDark),
8289
8540
  formatter: function (params) {
8290
8541
  var lines = [params[0].axisValue];
8291
8542
  params.forEach(function (pt) {
@@ -8307,13 +8558,21 @@ function monDualLine(elId, varKey, times, isDark, title, series, fmtY, yRange) {
8307
8558
  return {
8308
8559
  name: sr.name, type: 'line', data: sr.data, smooth: true, symbol: 'none',
8309
8560
  connectNulls: true,
8310
- lineStyle: { width: 2, color: sr.color },
8311
- areaStyle: { color: sr.color, opacity: 0.08 },
8561
+ lineStyle: { width: sr.width || 2, color: sr.color },
8562
+ areaStyle: { color: sr.color, opacity: sr.areaOpacity != null ? sr.areaOpacity : 0.08 },
8312
8563
  itemStyle: { color: sr.color },
8313
8564
  };
8314
8565
  }),
8315
8566
  animation: false,
8316
- });
8567
+ });
8568
+ resizeChartAfterLayout(chart);
8569
+ };
8570
+
8571
+ if (el.clientWidth <= 0 || el.clientHeight <= 0) {
8572
+ deferMonitorChartUntilVisible(el, render);
8573
+ return;
8574
+ }
8575
+ render();
8317
8576
  }
8318
8577
 
8319
8578
  function renderRoles(data) {
@@ -8769,8 +9028,7 @@ function renderRoleDefinitions(data) {
8769
9028
  }
8770
9029
 
8771
9030
  if (!data) {
8772
- console.warn('[roleDefinitions] No data received');
8773
- grid.innerHTML = '<div class="role-error">❌ 未收到角色定义数据</div>';
9031
+ grid.innerHTML = `<div class="role-error">${esc(t('common.loading'))}</div>`;
8774
9032
  return;
8775
9033
  }
8776
9034
 
@@ -8877,6 +9135,8 @@ function renderDefaultRoleSelector(data) {
8877
9135
  alert('保存默认角色失败' + (detail ? ': ' + detail : ''));
8878
9136
  privateSelect.value = defaults.private;
8879
9137
  groupSelect.value = defaults.group;
9138
+ } else {
9139
+ subscribe('roleDefinitions', roleDefinitionsParams());
8880
9140
  }
8881
9141
  } catch (err) {
8882
9142
  console.error('[roleDefinitions] Failed to save defaultRoles:', err);
@@ -9154,6 +9414,7 @@ function renderCommandPermissionsList(roleDef, operations, readonly = false) {
9154
9414
 
9155
9415
  function renderCommandPermissionsEditor(roleDef, operations) {
9156
9416
  const stats = getCommandPermissionStats(roleDef?.commandPermissions);
9417
+ const rawPermissions = formatCommandPermissions(roleDef?.commandPermissions);
9157
9418
  return `
9158
9419
  <div class="form-section command-permissions-section">
9159
9420
  <h4>Command / CLI Permissions</h4>
@@ -9168,6 +9429,18 @@ function renderCommandPermissionsEditor(roleDef, operations) {
9168
9429
  ? '清单来自后端 operation registry。每行显示当前有效权限和命中的规则;修改后会写入精确 operation 规则。若要撤销内置继承规则,请对同名 operation 设置“拒绝”。'
9169
9430
  : 'The list comes from the backend operation registry. Each row shows the current effective permission and matched rule. Changes are saved as exact operation rules.'}
9170
9431
  </div>
9432
+ <details class="cli-raw-editor">
9433
+ <summary>${currentLang === 'zh-CN' ? '编辑原始规则 JSON' : 'Edit raw rules JSON'}</summary>
9434
+ <div class="cli-help">
9435
+ ${currentLang === 'zh-CN'
9436
+ ? '用于编辑通配符或未出现在 operation 清单中的规则。下方结构化修改会合并到这份 JSON。'
9437
+ : 'Use this for wildcard rules or operations missing from the catalog. Structured changes below are merged into this JSON.'}
9438
+ </div>
9439
+ <textarea id="edit-commandPermissions"
9440
+ class="form-textarea cli-raw-textarea"
9441
+ rows="12"
9442
+ spellcheck="false">${esc(rawPermissions)}</textarea>
9443
+ </details>
9171
9444
  ${renderCommandPermissionsList(roleDef, operations, false)}
9172
9445
  </div>
9173
9446
  `;
@@ -9209,21 +9482,24 @@ function validateCommandPermissionsClient(commandPermissions) {
9209
9482
 
9210
9483
  function collectCommandPermissions(container, fallback = {}) {
9211
9484
  const rows = Array.from(container.querySelectorAll('[data-command-operation]'));
9212
- if (rows.length === 0) {
9213
- const input = container.querySelector('#edit-commandPermissions');
9214
- if (!input) return { ok: true, value: getCommandPermissionsObject(fallback) };
9485
+ const input = container.querySelector('#edit-commandPermissions');
9486
+ let commandPermissions = getCommandPermissionsObject(fallback);
9487
+
9488
+ if (input) {
9215
9489
  try {
9216
9490
  const parsed = JSON.parse(input.value.trim() || '{}');
9217
9491
  const validationError = validateCommandPermissionsClient(parsed);
9218
9492
  if (validationError) return { ok: false, error: validationError };
9219
- return { ok: true, value: parsed };
9493
+ commandPermissions = parsed;
9220
9494
  } catch (err) {
9221
9495
  return { ok: false, error: `Command permissions JSON is invalid: ${err.message}` };
9222
9496
  }
9223
9497
  }
9224
9498
 
9499
+ if (rows.length === 0) return { ok: true, value: commandPermissions };
9500
+
9225
9501
  const operationsById = new Map((roleOperationsCache || []).map(operation => [operation.id, operation]));
9226
- const commandPermissions = { ...getCommandPermissionsObject(fallback) };
9502
+ commandPermissions = { ...commandPermissions };
9227
9503
 
9228
9504
  for (const row of rows) {
9229
9505
  const operationId = row.dataset.commandOperation;
@@ -9435,7 +9711,7 @@ function createRoleCard(roleName, roleDef) {
9435
9711
  const commandStats = getCommandPermissionStats(roleDef.commandPermissions);
9436
9712
 
9437
9713
  // 判断是否为内置角色
9438
- const builtinRoles = ['member', 'visitor'];
9714
+ const builtinRoles = ['admin', 'member', 'visitor'];
9439
9715
  const isBuiltin = builtinRoles.includes(roleName);
9440
9716
 
9441
9717
  card.innerHTML = `
@@ -9530,6 +9806,7 @@ function initRoleDefinitionsTab() {
9530
9806
  alert(t('roleDefs.saveFailed'));
9531
9807
  } else {
9532
9808
  alert(t('roleDefs.saveSuccess'));
9809
+ subscribe('roleDefinitions', roleDefinitionsParams());
9533
9810
  }
9534
9811
  } catch (err) {
9535
9812
  alert(t('pair.error.network'));
@@ -9575,6 +9852,7 @@ function initRoleDefinitionsTab() {
9575
9852
  alert(t('roleDefs.saveFailed') + ': ' + (err.error || 'unknown'));
9576
9853
  } else {
9577
9854
  alert('角色已删除');
9855
+ subscribe('roleDefinitions', roleDefinitionsParams());
9578
9856
  }
9579
9857
  } catch (err) {
9580
9858
  alert(t('pair.error.network'));
@@ -10244,14 +10522,8 @@ async function showRoleEditModal(roleName, roleDef) {
10244
10522
  formHtml += renderRoleUsageLimitsEditor(roleDef);
10245
10523
 
10246
10524
  formHtml += `
10247
- <div class="role-editor-tabs" role="tablist">
10248
- <button type="button" class="role-editor-tab active" data-role-tab="fields">Field Permissions</button>
10249
- <button type="button" class="role-editor-tab" data-role-tab="cli">CLI Permissions</button>
10250
- </div>
10251
-
10252
- <div class="role-editor-tab-panel active" data-role-tab-panel="fields">
10253
- <div class="form-section">
10254
- <h4>权限配置</h4>
10525
+ <div class="form-section field-permissions-section">
10526
+ <h4>Field Permissions</h4>
10255
10527
  `;
10256
10528
 
10257
10529
  const permissions = roleDef.permissions || {};
@@ -10421,16 +10693,13 @@ async function showRoleEditModal(roleName, roleDef) {
10421
10693
  formHtml += `</div>`;
10422
10694
  });
10423
10695
 
10424
- formHtml += `</div></div>
10425
- <div class="role-editor-tab-panel" data-role-tab-panel="cli">
10426
- ${renderCommandPermissionsEditor(roleDef, operations)}
10427
- </div>
10696
+ formHtml += `</div>
10697
+ ${renderCommandPermissionsEditor(roleDef, operations)}
10428
10698
  </div>`;
10429
10699
 
10430
10700
  body.innerHTML = formHtml;
10431
10701
 
10432
10702
  bindTagsInputEvents(body);
10433
- initRoleEditorTabs(body);
10434
10703
  initCommandPermissionEditor(body);
10435
10704
  initRoleModelPermissionEditor(body, roleName, roleDef);
10436
10705