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.
- package/dist/server.js +73 -12
- package/dist/sources/monitor.js +90 -7
- package/dist/sources/msg.js +193 -7
- package/dist/sources/relation-roles.js +2 -24
- package/dist/sources/role-definitions.js +11 -29
- package/dist/sources/session-codex.js +43 -29
- package/dist/sources/session-origin.js +195 -0
- package/dist/sources/session.js +211 -33
- package/dist/static/app.js +421 -152
- package/dist/static/index.html +8 -9
- package/dist/static/style.css +81 -1
- package/dist/static/vendor/echarts-5.6.0.min.js +45 -0
- package/package.json +2 -2
package/dist/static/app.js
CHANGED
|
@@ -296,10 +296,19 @@ const translations = {
|
|
|
296
296
|
'sessions.turnType.toolUse': '工具使用',
|
|
297
297
|
'sessions.turnType.toolResult': '工具结果',
|
|
298
298
|
'sessions.turnType.msgSend': '发送消息',
|
|
299
|
-
'sessions.count': '{
|
|
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.
|
|
395
|
+
'monitor.chart.disk': '磁盘读写',
|
|
378
396
|
'monitor.chart.network': '网络占用',
|
|
379
|
-
'monitor.
|
|
380
|
-
'monitor.
|
|
381
|
-
'monitor.
|
|
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.
|
|
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': '{
|
|
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.
|
|
1270
|
+
'monitor.chart.disk': 'Disk I/O',
|
|
1236
1271
|
'monitor.chart.network': 'Network Usage',
|
|
1237
|
-
'monitor.
|
|
1238
|
-
'monitor.
|
|
1239
|
-
'monitor.
|
|
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.
|
|
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
|
|
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
|
-
|
|
2422
|
-
|
|
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
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
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
|
|
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
|
|
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', {
|
|
3066
|
-
`<label class="sess-switch" title="${t(
|
|
3067
|
-
`<span>${t(
|
|
3068
|
-
`<input id="sess-filter-toggle" type="checkbox"${
|
|
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.
|
|
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}
|
|
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 = () => {
|
|
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.
|
|
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
|
|
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
|
|
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', '
|
|
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 = '
|
|
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 '
|
|
7077
|
-
fromTs =
|
|
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 =
|
|
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
|
-
//
|
|
7352
|
-
|
|
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') || '
|
|
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('
|
|
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 = '
|
|
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
|
|
7633
|
-
if (
|
|
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(
|
|
7857
|
+
if (fromEl) fromEl.value = formatDatetimeLocal(new Date(last24.fromTs));
|
|
7683
7858
|
if (toEl) toEl.value = formatDatetimeLocal(now);
|
|
7684
7859
|
|
|
7685
|
-
//
|
|
7686
|
-
calculateExplorerTimeRange('
|
|
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 '
|
|
7697
|
-
_expTimeRange
|
|
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: {
|
|
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-
|
|
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', '
|
|
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
|
|
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
|
-
|
|
8142
|
-
|
|
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(
|
|
8381
|
+
function monitorProcessColor(key) {
|
|
8146
8382
|
var colors = ['#4f6ef7', '#38a169', '#dd6b20', '#805ad5', '#d53f8c', '#319795', '#e53e3e', '#3182ce', '#718096', '#b7791f'];
|
|
8147
|
-
var value =
|
|
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
|
-
|
|
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 || []))
|
|
8397
|
+
for (const process of (point.processes || [])) addProcess(process);
|
|
8156
8398
|
}
|
|
8157
|
-
return [...catalog.values()].sort(function (a, b) { return a.
|
|
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
|
|
8162
|
-
|
|
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.
|
|
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'), (
|
|
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 =
|
|
8215
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
8490
|
+
memorySeries,
|
|
8225
8491
|
function (v) { return fmtBytes(v); }, null);
|
|
8226
8492
|
|
|
8227
|
-
// ──
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
8276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
10248
|
-
<
|
|
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
|
|
10425
|
-
|
|
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
|
|