evolclaw-web 1.1.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +9 -9
- package/dist/process-utils.js +20 -12
- package/dist/server.js +256 -36
- package/dist/sources/aid.js +20 -1
- package/dist/sources/gateway.js +44 -0
- package/dist/sources/monitor.js +96 -0
- package/dist/sources/session.js +11 -2
- package/dist/sources/stats.js +269 -136
- package/dist/sources/system.js +2 -2
- package/dist/static/app.js +2509 -327
- package/dist/static/index.html +145 -51
- package/dist/static/style.css +1016 -25
- package/package.json +2 -2
- package/dist/sources/control.js +0 -58
package/dist/static/app.js
CHANGED
|
@@ -2,10 +2,609 @@
|
|
|
2
2
|
|
|
3
3
|
const $ = (sel) => document.querySelector(sel);
|
|
4
4
|
const TOKEN_KEY = 'ecWatchToken';
|
|
5
|
+
const LANG_KEY = 'ecWatchLang';
|
|
6
|
+
const VIEW_KEY = 'ecWatchCurrentView';
|
|
7
|
+
|
|
8
|
+
// ── 国际化 (i18n) ──
|
|
9
|
+
const translations = {
|
|
10
|
+
'zh-CN': {
|
|
11
|
+
// Tabs
|
|
12
|
+
'tab.agents': '智能体',
|
|
13
|
+
'tab.messages': '消息',
|
|
14
|
+
'tab.sessions': '会话',
|
|
15
|
+
'tab.triggers': '触发器',
|
|
16
|
+
'tab.cache': '缓存',
|
|
17
|
+
'tab.system': '系统',
|
|
18
|
+
'tab.gateway': '智能体网关',
|
|
19
|
+
'tab.usage': '用量',
|
|
20
|
+
'tab.monitor': '监控',
|
|
21
|
+
|
|
22
|
+
// Status
|
|
23
|
+
'status.connecting': '连接中…',
|
|
24
|
+
'status.connected': '已连接',
|
|
25
|
+
'status.disconnected': '已断开',
|
|
26
|
+
'status.reconnecting': '重连中',
|
|
27
|
+
'status.stopped': '停止',
|
|
28
|
+
'status.idle': 'idle',
|
|
29
|
+
'status.working': 'working',
|
|
30
|
+
|
|
31
|
+
// Actions
|
|
32
|
+
'action.logout': '退出',
|
|
33
|
+
'action.pair': '配对',
|
|
34
|
+
'action.stop': '停止',
|
|
35
|
+
'action.start': '启动',
|
|
36
|
+
'action.enable': '启用',
|
|
37
|
+
'action.disable': '禁用',
|
|
38
|
+
'action.reload': '重载配置',
|
|
39
|
+
'action.edit': '编辑配置',
|
|
40
|
+
'action.delete': '删除 Agent',
|
|
41
|
+
'action.clearQueue': '清空队列',
|
|
42
|
+
'action.new': '+ 新建',
|
|
43
|
+
'action.query': '查询',
|
|
44
|
+
|
|
45
|
+
// Pair page
|
|
46
|
+
'pair.title': '🔭 EvolClaw Watch',
|
|
47
|
+
'pair.hint': '输入终端显示的 6 位配对码',
|
|
48
|
+
'pair.placeholder': '000000',
|
|
49
|
+
'pair.error.length': '请输入 6 位配对码',
|
|
50
|
+
'pair.error.failed': '配对失败',
|
|
51
|
+
'pair.error.network': '网络错误',
|
|
52
|
+
'pair.error.tokenInvalid': 'token 已失效,请重新配对',
|
|
53
|
+
|
|
54
|
+
// Common
|
|
55
|
+
'common.loading': '加载中…',
|
|
56
|
+
'common.empty': '暂无数据',
|
|
57
|
+
'common.noData': '暂无',
|
|
58
|
+
'common.operating': '操作中…',
|
|
59
|
+
'common.buildTime': '构建时间',
|
|
60
|
+
|
|
61
|
+
// Agents view
|
|
62
|
+
'agents.subtitle.enabled': '启用',
|
|
63
|
+
'agents.subtitle.disabled': '禁用',
|
|
64
|
+
'agents.daemonStopped': '⚠ EvolClaw 主进程未运行,仅显示最近活动记录',
|
|
65
|
+
'agents.empty.disabled': '暂无禁用 Agent',
|
|
66
|
+
'agents.empty.enabled': '暂无启用 Agent',
|
|
67
|
+
'agents.stats.gateway': 'Gateway',
|
|
68
|
+
'agents.stats.aids': 'AIDs',
|
|
69
|
+
'agents.stats.total': 'total',
|
|
70
|
+
'agents.stats.online': '在线',
|
|
71
|
+
'agents.stats.offline': '离线',
|
|
72
|
+
'agents.stats.messages': 'Messages',
|
|
73
|
+
'agents.stats.traffic': 'Traffic',
|
|
74
|
+
'agents.stats.version': 'Version',
|
|
75
|
+
'agents.stats.pid': 'PID',
|
|
76
|
+
'agents.stats.uptime': 'Uptime',
|
|
77
|
+
|
|
78
|
+
// Agent table headers
|
|
79
|
+
'agents.th.agent': 'Agent',
|
|
80
|
+
'agents.th.aid': 'AID',
|
|
81
|
+
'agents.th.work': '工作',
|
|
82
|
+
'agents.th.queue': '队列',
|
|
83
|
+
'agents.th.model': '模型',
|
|
84
|
+
'agents.th.runtime': '运行',
|
|
85
|
+
'agents.th.received': '收',
|
|
86
|
+
'agents.th.sent': '发',
|
|
87
|
+
'agents.th.bytesIn': '入字节',
|
|
88
|
+
'agents.th.bytesOut': '出字节',
|
|
89
|
+
'agents.th.peerCount': '对端数量',
|
|
90
|
+
'agents.th.lastActivity': '最后活动',
|
|
91
|
+
'agents.th.operations': '操作',
|
|
92
|
+
'agents.th.projectPath': '项目路径',
|
|
93
|
+
|
|
94
|
+
// Agent operations
|
|
95
|
+
'agents.op.stopping': '停止中…',
|
|
96
|
+
'agents.op.starting': '启动中…',
|
|
97
|
+
'agents.op.reloading': '重载中…',
|
|
98
|
+
'agents.op.disabling': '禁用中…',
|
|
99
|
+
'agents.op.enabling': '启用中…',
|
|
100
|
+
'agents.op.deleting': '删除中…',
|
|
101
|
+
'agents.op.stopped': '✓ 已停止',
|
|
102
|
+
'agents.op.started': '✓ 已启动',
|
|
103
|
+
'agents.op.reloaded': '✓ 已重载',
|
|
104
|
+
'agents.op.disabled': '✓ 已禁用',
|
|
105
|
+
'agents.op.enabled': '✓ 已启用',
|
|
106
|
+
'agents.op.deleted': '✓ 已删除',
|
|
107
|
+
'agents.op.saved': '✓ 配置已保存,点「重载」生效',
|
|
108
|
+
'agents.op.confirmReload': '确认强制重载?',
|
|
109
|
+
'agents.op.confirmToggle': '确认强制',
|
|
110
|
+
'agents.op.confirmDelete': '删除 Agent {aid}?\n此操作不可恢复。',
|
|
111
|
+
'agents.op.confirmForceDelete': '确认强制删除?',
|
|
112
|
+
'agents.op.confirmClearQueue': '清空 {aid} 的待处理消息队列?',
|
|
113
|
+
'agents.op.clearQueueTitle': '清空 {count} 条待处理消息',
|
|
114
|
+
'agents.op.viewAgentMd': '查看 agent.md ↗',
|
|
115
|
+
|
|
116
|
+
// Messages view
|
|
117
|
+
'messages.colTitle.aid': 'AID',
|
|
118
|
+
'messages.colTitle.peers': 'Peers',
|
|
119
|
+
'messages.colTitle.all': 'All',
|
|
120
|
+
'messages.empty.selectAid': '← 选择一个 AID',
|
|
121
|
+
'messages.empty.selectToView': '选择 AID 查看消息',
|
|
122
|
+
'messages.empty.noMessages': '暂无消息',
|
|
123
|
+
'messages.tag.group': '群聊',
|
|
124
|
+
'messages.tag.encrypted': '🔒密文',
|
|
125
|
+
'messages.tag.plain': '明文',
|
|
126
|
+
'messages.tag.proactive': '自主',
|
|
127
|
+
'messages.tag.inject': '注入',
|
|
128
|
+
'messages.tag.responsive': '响应',
|
|
129
|
+
'messages.msgKind.reply': '回复',
|
|
130
|
+
'messages.msgKind.thought': '思考',
|
|
131
|
+
'messages.msgKind.inject': '注入',
|
|
132
|
+
'messages.msgKind.notify': '通知',
|
|
133
|
+
'messages.msgType.thought': '思考',
|
|
134
|
+
'messages.msgType.image': '图片',
|
|
135
|
+
'messages.msgType.file': '文件',
|
|
136
|
+
'messages.msgType.command': '命令',
|
|
137
|
+
|
|
138
|
+
// Sessions view
|
|
139
|
+
'sessions.filter.normal': '🔍 仅有效',
|
|
140
|
+
'sessions.filter.chat': '💬 对话',
|
|
141
|
+
'sessions.search.placeholder': '🔎 搜索 peer / 内容',
|
|
142
|
+
'sessions.empty.noMatch': '无匹配会话',
|
|
143
|
+
'sessions.empty.noSessions': '该项目暂无会话',
|
|
144
|
+
'sessions.empty.noContent': '该会话暂无内容',
|
|
145
|
+
'sessions.header.project': '项目',
|
|
146
|
+
'sessions.header.session': '会话',
|
|
147
|
+
'sessions.stat.context': '📐 {tokens} ctx',
|
|
148
|
+
'sessions.stat.cost': '💰 ${cost}',
|
|
149
|
+
'sessions.turnType.modelOutput': '模型输出',
|
|
150
|
+
'sessions.turnType.toolUse': '工具使用',
|
|
151
|
+
'sessions.turnType.toolResult': '工具结果',
|
|
152
|
+
'sessions.turnType.msgSend': '发送消息',
|
|
153
|
+
|
|
154
|
+
// Cache view
|
|
155
|
+
'cache.daemonStopped': '⚠ EvolClaw 主进程未运行,无缓存统计可显示',
|
|
156
|
+
'cache.notSupported': '⚠ 当前 EvolClaw 版本不支持 cache-stats(请升级 daemon)',
|
|
157
|
+
'cache.card.hitRate': '命中率',
|
|
158
|
+
'cache.card.reads': '读取总数',
|
|
159
|
+
'cache.card.entries': '缓存条目',
|
|
160
|
+
'cache.card.statChecks': 'stat 检查',
|
|
161
|
+
'cache.card.reReads': '重读',
|
|
162
|
+
'cache.card.evictions': '驱逐',
|
|
163
|
+
'cache.card.invalidations': '失效',
|
|
164
|
+
'cache.card.since': '统计起始',
|
|
165
|
+
'cache.card.ago': '前',
|
|
166
|
+
'cache.card.memory': '近似内存',
|
|
167
|
+
'cache.card.hit': '命中',
|
|
168
|
+
'cache.card.miss': '未命中',
|
|
169
|
+
'cache.section.byGroup': '按缓存组',
|
|
170
|
+
'cache.section.byPolicy': '按策略',
|
|
171
|
+
'cache.th.group': '组',
|
|
172
|
+
'cache.th.type': '类型',
|
|
173
|
+
'cache.th.reads': '读取',
|
|
174
|
+
'cache.th.hits': '命中',
|
|
175
|
+
'cache.th.misses': '未命中',
|
|
176
|
+
'cache.th.hitRate': '命中率',
|
|
177
|
+
'cache.th.reReads': '重读',
|
|
178
|
+
'cache.th.evictions': '驱逐',
|
|
179
|
+
'cache.th.entries': '条目',
|
|
180
|
+
'cache.th.memory': '内存',
|
|
181
|
+
'cache.th.capacity': '容量',
|
|
182
|
+
'cache.th.policy': '策略',
|
|
183
|
+
'cache.th.statChecks': 'stat 检查',
|
|
184
|
+
'cache.note': '注:config/defaults 与关系级 preferences 的读取也已并入本统计;渲染后结果(按 vars)不缓存,故不在此列。',
|
|
185
|
+
'cache.policy.onReload': '靠 reload 刷新,平时零检查',
|
|
186
|
+
'cache.policy.manual': '显式单刷',
|
|
187
|
+
'cache.policy.mtime': '每读 statSync 门控',
|
|
188
|
+
|
|
189
|
+
// Monitor view
|
|
190
|
+
'monitor.toolbar.timeRange': '时间范围',
|
|
191
|
+
'monitor.range.2m': '2 分钟',
|
|
192
|
+
'monitor.range.10m': '10 分钟',
|
|
193
|
+
'monitor.range.1h': '1 小时',
|
|
194
|
+
'monitor.legend.process': 'evolclaw 进程',
|
|
195
|
+
'monitor.legend.system': '整机系统',
|
|
196
|
+
|
|
197
|
+
// Usage view
|
|
198
|
+
'usage.subtab.overview': '总览',
|
|
199
|
+
'usage.subtab.explorer': '详细统计',
|
|
200
|
+
'usage.overview.range.today': '今日',
|
|
201
|
+
'usage.overview.range.week': '本周',
|
|
202
|
+
'usage.overview.range.lastWeek': '上周',
|
|
203
|
+
'usage.overview.range.month': '本月',
|
|
204
|
+
'usage.overview.range.last30': '最近30天',
|
|
205
|
+
'usage.overview.range.custom': '自定义',
|
|
206
|
+
'usage.card.input': '输入',
|
|
207
|
+
'usage.card.output': '输出',
|
|
208
|
+
'usage.card.cacheRead': '缓存读取',
|
|
209
|
+
'usage.card.cacheHit': '缓存命中',
|
|
210
|
+
'usage.card.calls': '调用',
|
|
211
|
+
'usage.card.sessionCount': '会话数',
|
|
212
|
+
'usage.card.msgIn': '收到消息',
|
|
213
|
+
'usage.card.msgOut': '发出消息',
|
|
214
|
+
'usage.card.modelCalls': '模型调用',
|
|
215
|
+
'usage.card.inputTokens': '输入 Token',
|
|
216
|
+
'usage.card.outputTokens': '输出 Token',
|
|
217
|
+
'usage.card.cacheCreation': '缓存创建',
|
|
218
|
+
'usage.card.cacheHitTokens': '缓存命中',
|
|
219
|
+
'usage.card.cacheHitRate': '缓存命中率',
|
|
220
|
+
'usage.card.totalCost': '总花费',
|
|
221
|
+
'usage.card.costOfficial': '官方价格',
|
|
222
|
+
'usage.card.costGateway': '网关价格',
|
|
223
|
+
'usage.card.sessionInfo': '会话信息',
|
|
224
|
+
'usage.card.usageInfo': '用量信息',
|
|
225
|
+
'usage.card.costInfo': '花费信息',
|
|
226
|
+
'usage.detail.title': '模型访问明细',
|
|
227
|
+
'usage.detail.agent': '智能体',
|
|
228
|
+
'usage.detail.model': '模型',
|
|
229
|
+
'usage.detail.error': '查询失败',
|
|
230
|
+
'usage.detail.th.time': '时间',
|
|
231
|
+
'usage.detail.th.agent': '智能体',
|
|
232
|
+
'usage.detail.th.peer': 'Peer',
|
|
233
|
+
'usage.detail.th.model': '模型',
|
|
234
|
+
'usage.detail.th.input': '输入',
|
|
235
|
+
'usage.detail.th.output': '输出',
|
|
236
|
+
'usage.detail.th.cacheCreation': '缓存创建',
|
|
237
|
+
'usage.detail.th.cacheRead': '缓存读取',
|
|
238
|
+
'usage.detail.th.costOfficial': '官方价格',
|
|
239
|
+
'usage.detail.th.costGateway': '网关价格',
|
|
240
|
+
'usage.detail.pageSize': '每页',
|
|
241
|
+
'usage.detail.prevPage': '上一页',
|
|
242
|
+
'usage.detail.nextPage': '下一页',
|
|
243
|
+
'usage.detail.pagination': '显示 {start}-{end} / 共 {total} 条 (第 {page}/{totalPages} 页)',
|
|
244
|
+
'usage.overview.title': '按 Agent 汇总(全时段)',
|
|
245
|
+
'usage.overview.noData': '暂无数据',
|
|
246
|
+
'usage.overview.th.agent': '智能体',
|
|
247
|
+
'usage.overview.th.calls': '调用',
|
|
248
|
+
'usage.overview.th.input': '输入',
|
|
249
|
+
'usage.overview.th.output': '输出',
|
|
250
|
+
'usage.overview.th.cacheCreation': '缓存创建',
|
|
251
|
+
'usage.overview.th.cacheHit': '缓存命中',
|
|
252
|
+
'usage.overview.th.cacheHitRate': '命中率',
|
|
253
|
+
'usage.overview.th.costOfficial': '官方价格',
|
|
254
|
+
'usage.overview.th.costGateway': '网关价格',
|
|
255
|
+
'usage.overview.th.cost': '花费',
|
|
256
|
+
'usage.dashboard.title.topPeers': 'Top Peers (Today)',
|
|
257
|
+
'usage.dashboard.th.rank': '#',
|
|
258
|
+
'usage.dashboard.th.peer': 'Peer',
|
|
259
|
+
'usage.dashboard.th.tokens': 'Tokens',
|
|
260
|
+
'usage.dashboard.th.calls': 'Calls',
|
|
261
|
+
'usage.explorer.sidebar.agents': '智能体',
|
|
262
|
+
'usage.explorer.sidebar.peers': '对端智能体',
|
|
263
|
+
'usage.explorer.chatType.group': '群聊',
|
|
264
|
+
'usage.explorer.chatType.private': '单聊',
|
|
265
|
+
'usage.explorer.memberCount': '人',
|
|
266
|
+
'usage.explorer.selectHint': '请从左侧选择 Agent 或 Peer',
|
|
267
|
+
'usage.explorer.all': '全部',
|
|
268
|
+
'usage.explorer.filter.from': 'From',
|
|
269
|
+
'usage.explorer.filter.to': 'To',
|
|
270
|
+
'usage.explorer.filter.model': 'Model',
|
|
271
|
+
'usage.explorer.filter.granularity': '粒度',
|
|
272
|
+
'usage.explorer.filter.granularity.hour': 'Hour',
|
|
273
|
+
'usage.explorer.filter.granularity.day': 'Day',
|
|
274
|
+
'usage.explorer.filter.granularity.week': 'Week',
|
|
275
|
+
'usage.explorer.filter.granularity.month': 'Month',
|
|
276
|
+
'usage.explorer.results': 'Results',
|
|
277
|
+
'usage.explorer.noData': 'No data for selected range.',
|
|
278
|
+
'usage.explorer.th.period': 'Period',
|
|
279
|
+
'usage.explorer.th.input': 'Input',
|
|
280
|
+
'usage.explorer.th.output': 'Output',
|
|
281
|
+
'usage.explorer.th.cacheCreation': 'Cache↑',
|
|
282
|
+
'usage.explorer.th.cacheHit': 'CacheHit',
|
|
283
|
+
'usage.explorer.th.calls': 'Calls',
|
|
284
|
+
},
|
|
285
|
+
'en-US': {
|
|
286
|
+
// Tabs
|
|
287
|
+
'tab.agents': 'Agents',
|
|
288
|
+
'tab.messages': 'Messages',
|
|
289
|
+
'tab.sessions': 'Sessions',
|
|
290
|
+
'tab.triggers': 'Triggers',
|
|
291
|
+
'tab.cache': 'Cache',
|
|
292
|
+
'tab.system': 'System',
|
|
293
|
+
'tab.gateway': 'Agent Gateway',
|
|
294
|
+
'tab.usage': 'Usage',
|
|
295
|
+
'tab.monitor': 'Monitor',
|
|
296
|
+
|
|
297
|
+
// Status
|
|
298
|
+
'status.connecting': 'Connecting…',
|
|
299
|
+
'status.connected': 'Connected',
|
|
300
|
+
'status.disconnected': 'Disconnected',
|
|
301
|
+
'status.reconnecting': 'Reconnecting',
|
|
302
|
+
'status.stopped': 'Stopped',
|
|
303
|
+
'status.idle': 'Idle',
|
|
304
|
+
'status.working': 'Working',
|
|
305
|
+
|
|
306
|
+
// Actions
|
|
307
|
+
'action.logout': 'Logout',
|
|
308
|
+
'action.pair': 'Pair',
|
|
309
|
+
'action.stop': 'Stop',
|
|
310
|
+
'action.start': 'Start',
|
|
311
|
+
'action.enable': 'Enable',
|
|
312
|
+
'action.disable': 'Disable',
|
|
313
|
+
'action.reload': 'Reload Config',
|
|
314
|
+
'action.edit': 'Edit Config',
|
|
315
|
+
'action.delete': 'Delete Agent',
|
|
316
|
+
'action.clearQueue': 'Clear Queue',
|
|
317
|
+
'action.new': '+ New',
|
|
318
|
+
'action.query': 'Query',
|
|
319
|
+
|
|
320
|
+
// Pair page
|
|
321
|
+
'pair.title': '🔭 EvolClaw Watch',
|
|
322
|
+
'pair.hint': 'Enter the 6-digit pairing code shown in terminal',
|
|
323
|
+
'pair.placeholder': '000000',
|
|
324
|
+
'pair.error.length': 'Please enter 6-digit pairing code',
|
|
325
|
+
'pair.error.failed': 'Pairing failed',
|
|
326
|
+
'pair.error.network': 'Network error',
|
|
327
|
+
'pair.error.tokenInvalid': 'Token expired, please pair again',
|
|
328
|
+
|
|
329
|
+
// Common
|
|
330
|
+
'common.loading': 'Loading…',
|
|
331
|
+
'common.empty': 'No data',
|
|
332
|
+
'common.noData': 'N/A',
|
|
333
|
+
'common.operating': 'Operating…',
|
|
334
|
+
'common.buildTime': 'Build Time',
|
|
335
|
+
|
|
336
|
+
// Agents view
|
|
337
|
+
'agents.subtitle.enabled': 'Enabled',
|
|
338
|
+
'agents.subtitle.disabled': 'Disabled',
|
|
339
|
+
'agents.daemonStopped': '⚠ EvolClaw daemon not running, showing recent activity only',
|
|
340
|
+
'agents.empty.disabled': 'No disabled agents',
|
|
341
|
+
'agents.empty.enabled': 'No enabled agents',
|
|
342
|
+
'agents.stats.gateway': 'Gateway',
|
|
343
|
+
'agents.stats.aids': 'AIDs',
|
|
344
|
+
'agents.stats.total': 'total',
|
|
345
|
+
'agents.stats.online': 'online',
|
|
346
|
+
'agents.stats.offline': 'offline',
|
|
347
|
+
'agents.stats.messages': 'Messages',
|
|
348
|
+
'agents.stats.traffic': 'Traffic',
|
|
349
|
+
'agents.stats.version': 'Version',
|
|
350
|
+
'agents.stats.pid': 'PID',
|
|
351
|
+
'agents.stats.uptime': 'Uptime',
|
|
352
|
+
|
|
353
|
+
// Agent table headers
|
|
354
|
+
'agents.th.agent': 'Agent',
|
|
355
|
+
'agents.th.aid': 'AID',
|
|
356
|
+
'agents.th.work': 'Work',
|
|
357
|
+
'agents.th.queue': 'Queue',
|
|
358
|
+
'agents.th.model': 'Model',
|
|
359
|
+
'agents.th.runtime': 'Runtime',
|
|
360
|
+
'agents.th.received': 'Recv',
|
|
361
|
+
'agents.th.sent': 'Sent',
|
|
362
|
+
'agents.th.bytesIn': 'Bytes In',
|
|
363
|
+
'agents.th.bytesOut': 'Bytes Out',
|
|
364
|
+
'agents.th.peerCount': 'Peers',
|
|
365
|
+
'agents.th.lastActivity': 'Last Activity',
|
|
366
|
+
'agents.th.operations': 'Operations',
|
|
367
|
+
'agents.th.projectPath': 'Project Path',
|
|
368
|
+
|
|
369
|
+
// Agent operations
|
|
370
|
+
'agents.op.stopping': 'Stopping…',
|
|
371
|
+
'agents.op.starting': 'Starting…',
|
|
372
|
+
'agents.op.reloading': 'Reloading…',
|
|
373
|
+
'agents.op.disabling': 'Disabling…',
|
|
374
|
+
'agents.op.enabling': 'Enabling…',
|
|
375
|
+
'agents.op.deleting': 'Deleting…',
|
|
376
|
+
'agents.op.stopped': '✓ Stopped',
|
|
377
|
+
'agents.op.started': '✓ Started',
|
|
378
|
+
'agents.op.reloaded': '✓ Reloaded',
|
|
379
|
+
'agents.op.disabled': '✓ Disabled',
|
|
380
|
+
'agents.op.enabled': '✓ Enabled',
|
|
381
|
+
'agents.op.deleted': '✓ Deleted',
|
|
382
|
+
'agents.op.saved': '✓ Config saved, click "Reload" to apply',
|
|
383
|
+
'agents.op.confirmReload': 'Force reload?',
|
|
384
|
+
'agents.op.confirmToggle': 'Force',
|
|
385
|
+
'agents.op.confirmDelete': 'Delete agent {aid}?\nThis cannot be undone.',
|
|
386
|
+
'agents.op.confirmForceDelete': 'Force delete?',
|
|
387
|
+
'agents.op.confirmClearQueue': 'Clear pending message queue for {aid}?',
|
|
388
|
+
'agents.op.clearQueueTitle': 'Clear {count} pending messages',
|
|
389
|
+
'agents.op.viewAgentMd': 'View agent.md ↗',
|
|
390
|
+
|
|
391
|
+
// Messages view
|
|
392
|
+
'messages.colTitle.aid': 'AID',
|
|
393
|
+
'messages.colTitle.peers': 'Peers',
|
|
394
|
+
'messages.colTitle.all': 'All',
|
|
395
|
+
'messages.empty.selectAid': '← Select an AID',
|
|
396
|
+
'messages.empty.selectToView': 'Select AID to view messages',
|
|
397
|
+
'messages.empty.noMessages': 'No messages',
|
|
398
|
+
'messages.tag.group': 'Group',
|
|
399
|
+
'messages.tag.encrypted': '🔒Encrypted',
|
|
400
|
+
'messages.tag.plain': 'Plain',
|
|
401
|
+
'messages.tag.proactive': 'Proactive',
|
|
402
|
+
'messages.tag.inject': 'Inject',
|
|
403
|
+
'messages.tag.responsive': 'Responsive',
|
|
404
|
+
'messages.msgKind.reply': 'Reply',
|
|
405
|
+
'messages.msgKind.thought': 'Thought',
|
|
406
|
+
'messages.msgKind.inject': 'Inject',
|
|
407
|
+
'messages.msgKind.notify': 'Notify',
|
|
408
|
+
'messages.msgType.thought': 'Thought',
|
|
409
|
+
'messages.msgType.image': 'Image',
|
|
410
|
+
'messages.msgType.file': 'File',
|
|
411
|
+
'messages.msgType.command': 'Command',
|
|
412
|
+
|
|
413
|
+
// Sessions view
|
|
414
|
+
'sessions.filter.normal': '🔍 Valid Only',
|
|
415
|
+
'sessions.filter.chat': '💬 Chat',
|
|
416
|
+
'sessions.search.placeholder': '🔎 Search peer / content',
|
|
417
|
+
'sessions.empty.noMatch': 'No matching sessions',
|
|
418
|
+
'sessions.empty.noSessions': 'No sessions in this project',
|
|
419
|
+
'sessions.empty.noContent': 'No content in this session',
|
|
420
|
+
'sessions.header.project': 'Project',
|
|
421
|
+
'sessions.header.session': 'Session',
|
|
422
|
+
'sessions.stat.context': '📐 {tokens} ctx',
|
|
423
|
+
'sessions.stat.cost': '💰 ${cost}',
|
|
424
|
+
'sessions.turnType.modelOutput': 'Model Output',
|
|
425
|
+
'sessions.turnType.toolUse': 'Tool Use',
|
|
426
|
+
'sessions.turnType.toolResult': 'Tool Result',
|
|
427
|
+
'sessions.turnType.msgSend': 'Send Message',
|
|
428
|
+
|
|
429
|
+
// Cache view
|
|
430
|
+
'cache.daemonStopped': '⚠ EvolClaw daemon not running, no cache stats available',
|
|
431
|
+
'cache.notSupported': '⚠ Current EvolClaw version does not support cache-stats (please upgrade daemon)',
|
|
432
|
+
'cache.card.hitRate': 'Hit Rate',
|
|
433
|
+
'cache.card.reads': 'Total Reads',
|
|
434
|
+
'cache.card.entries': 'Cache Entries',
|
|
435
|
+
'cache.card.statChecks': 'Stat Checks',
|
|
436
|
+
'cache.card.reReads': 'Re-reads',
|
|
437
|
+
'cache.card.evictions': 'Evictions',
|
|
438
|
+
'cache.card.invalidations': 'Invalidations',
|
|
439
|
+
'cache.card.since': 'Stats Since',
|
|
440
|
+
'cache.card.ago': 'ago',
|
|
441
|
+
'cache.card.memory': 'approx memory',
|
|
442
|
+
'cache.card.hit': 'hit',
|
|
443
|
+
'cache.card.miss': 'miss',
|
|
444
|
+
'cache.section.byGroup': 'By Cache Group',
|
|
445
|
+
'cache.section.byPolicy': 'By Policy',
|
|
446
|
+
'cache.th.group': 'Group',
|
|
447
|
+
'cache.th.type': 'Type',
|
|
448
|
+
'cache.th.reads': 'Reads',
|
|
449
|
+
'cache.th.hits': 'Hits',
|
|
450
|
+
'cache.th.misses': 'Misses',
|
|
451
|
+
'cache.th.hitRate': 'Hit Rate',
|
|
452
|
+
'cache.th.reReads': 'Re-reads',
|
|
453
|
+
'cache.th.evictions': 'Evictions',
|
|
454
|
+
'cache.th.entries': 'Entries',
|
|
455
|
+
'cache.th.memory': 'Memory',
|
|
456
|
+
'cache.th.capacity': 'Capacity',
|
|
457
|
+
'cache.th.policy': 'Policy',
|
|
458
|
+
'cache.th.statChecks': 'Stat Checks',
|
|
459
|
+
'cache.note': 'Note: Reads of config/defaults and relation-level preferences are included; rendered results (by vars) are not cached and not shown here.',
|
|
460
|
+
'cache.policy.onReload': 'Refresh on reload, zero checks normally',
|
|
461
|
+
'cache.policy.manual': 'Explicit single refresh',
|
|
462
|
+
'cache.policy.mtime': 'statSync gate on each read',
|
|
463
|
+
|
|
464
|
+
// Monitor view
|
|
465
|
+
'monitor.toolbar.timeRange': 'Time Range',
|
|
466
|
+
'monitor.range.2m': '2 minutes',
|
|
467
|
+
'monitor.range.10m': '10 minutes',
|
|
468
|
+
'monitor.range.1h': '1 hour',
|
|
469
|
+
'monitor.legend.process': 'evolclaw process',
|
|
470
|
+
'monitor.legend.system': 'system',
|
|
471
|
+
|
|
472
|
+
// Usage view
|
|
473
|
+
'usage.subtab.overview': 'Overview',
|
|
474
|
+
'usage.subtab.explorer': 'Detailed Statistics',
|
|
475
|
+
'usage.overview.range.today': 'Today',
|
|
476
|
+
'usage.overview.range.week': 'This Week',
|
|
477
|
+
'usage.overview.range.lastWeek': 'Last Week',
|
|
478
|
+
'usage.overview.range.month': 'This Month',
|
|
479
|
+
'usage.overview.range.last30': 'Last 30 Days',
|
|
480
|
+
'usage.overview.range.custom': 'Custom',
|
|
481
|
+
'usage.card.input': 'Input',
|
|
482
|
+
'usage.card.output': 'Output',
|
|
483
|
+
'usage.card.cacheRead': 'Cache Read',
|
|
484
|
+
'usage.card.cacheHit': 'Cache Hit',
|
|
485
|
+
'usage.card.calls': 'Calls',
|
|
486
|
+
'usage.card.sessionCount': 'Sessions',
|
|
487
|
+
'usage.card.msgIn': 'Received Messages',
|
|
488
|
+
'usage.card.msgOut': 'Sent Messages',
|
|
489
|
+
'usage.card.modelCalls': 'Model Calls',
|
|
490
|
+
'usage.card.inputTokens': 'Input Tokens',
|
|
491
|
+
'usage.card.outputTokens': 'Output Tokens',
|
|
492
|
+
'usage.card.cacheCreation': 'Cache Creation',
|
|
493
|
+
'usage.card.cacheHitTokens': 'Cache Hit',
|
|
494
|
+
'usage.card.cacheHitRate': 'Cache Hit Rate',
|
|
495
|
+
'usage.card.totalCost': 'Total Cost',
|
|
496
|
+
'usage.card.costOfficial': 'Official Price',
|
|
497
|
+
'usage.card.costGateway': 'Gateway Price',
|
|
498
|
+
'usage.card.sessionInfo': 'Session Info',
|
|
499
|
+
'usage.card.usageInfo': 'Usage Info',
|
|
500
|
+
'usage.card.costInfo': 'Cost Info',
|
|
501
|
+
'usage.detail.title': 'Model Access Details',
|
|
502
|
+
'usage.detail.agent': 'Agent',
|
|
503
|
+
'usage.detail.model': 'Model',
|
|
504
|
+
'usage.detail.error': 'Query failed',
|
|
505
|
+
'usage.detail.th.time': 'Time',
|
|
506
|
+
'usage.detail.th.agent': 'Agent',
|
|
507
|
+
'usage.detail.th.peer': 'Peer',
|
|
508
|
+
'usage.detail.th.model': 'Model',
|
|
509
|
+
'usage.detail.th.input': 'Input',
|
|
510
|
+
'usage.detail.th.output': 'Output',
|
|
511
|
+
'usage.detail.th.cacheCreation': 'Cache Creation',
|
|
512
|
+
'usage.detail.th.cacheRead': 'Cache Read',
|
|
513
|
+
'usage.detail.th.costOfficial': 'Official',
|
|
514
|
+
'usage.detail.th.costGateway': 'Gateway',
|
|
515
|
+
'usage.detail.pageSize': 'Per page',
|
|
516
|
+
'usage.detail.prevPage': 'Previous',
|
|
517
|
+
'usage.detail.nextPage': 'Next',
|
|
518
|
+
'usage.detail.pagination': 'Showing {start}-{end} of {total} (Page {page}/{totalPages})',
|
|
519
|
+
'usage.overview.title': 'Summary by Agent (All Time)',
|
|
520
|
+
'usage.overview.noData': 'No data',
|
|
521
|
+
'usage.overview.th.agent': 'Agent',
|
|
522
|
+
'usage.overview.th.calls': 'Calls',
|
|
523
|
+
'usage.overview.th.input': 'Input',
|
|
524
|
+
'usage.overview.th.output': 'Output',
|
|
525
|
+
'usage.overview.th.cacheCreation': 'Cache Creation',
|
|
526
|
+
'usage.overview.th.cacheHit': 'Cache Hit',
|
|
527
|
+
'usage.overview.th.cacheHitRate': 'Hit Rate',
|
|
528
|
+
'usage.overview.th.costOfficial': 'Official Price',
|
|
529
|
+
'usage.overview.th.costGateway': 'Gateway Price',
|
|
530
|
+
'usage.overview.th.cost': 'Cost',
|
|
531
|
+
'usage.dashboard.title.topPeers': 'Top Peers (Today)',
|
|
532
|
+
'usage.dashboard.th.rank': '#',
|
|
533
|
+
'usage.dashboard.th.peer': 'Peer',
|
|
534
|
+
'usage.dashboard.th.tokens': 'Tokens',
|
|
535
|
+
'usage.dashboard.th.calls': 'Calls',
|
|
536
|
+
'usage.explorer.sidebar.agents': 'Agents',
|
|
537
|
+
'usage.explorer.sidebar.peers': 'Peers',
|
|
538
|
+
'usage.explorer.chatType.group': 'Group',
|
|
539
|
+
'usage.explorer.chatType.private': 'Private',
|
|
540
|
+
'usage.explorer.memberCount': ' members',
|
|
541
|
+
'usage.explorer.selectHint': 'Select an Agent or Peer from the left',
|
|
542
|
+
'usage.explorer.all': 'All',
|
|
543
|
+
'usage.explorer.filter.from': 'From',
|
|
544
|
+
'usage.explorer.filter.to': 'To',
|
|
545
|
+
'usage.explorer.filter.model': 'Model',
|
|
546
|
+
'usage.explorer.filter.granularity': 'Granularity',
|
|
547
|
+
'usage.explorer.filter.granularity.hour': 'Hour',
|
|
548
|
+
'usage.explorer.filter.granularity.day': 'Day',
|
|
549
|
+
'usage.explorer.filter.granularity.week': 'Week',
|
|
550
|
+
'usage.explorer.filter.granularity.month': 'Month',
|
|
551
|
+
'usage.explorer.results': 'Results',
|
|
552
|
+
'usage.explorer.noData': 'No data for selected range.',
|
|
553
|
+
'usage.explorer.th.period': 'Period',
|
|
554
|
+
'usage.explorer.th.input': 'Input',
|
|
555
|
+
'usage.explorer.th.output': 'Output',
|
|
556
|
+
'usage.explorer.th.cacheCreation': 'Cache↑',
|
|
557
|
+
'usage.explorer.th.cacheHit': 'CacheHit',
|
|
558
|
+
'usage.explorer.th.calls': 'Calls',
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
let currentLang = localStorage.getItem(LANG_KEY) || 'zh-CN';
|
|
563
|
+
|
|
564
|
+
function t(key) {
|
|
565
|
+
return translations[currentLang]?.[key] || key;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function updateI18n() {
|
|
569
|
+
// 处理元素文本内容
|
|
570
|
+
document.querySelectorAll('[data-i18n]').forEach(el => {
|
|
571
|
+
const key = el.getAttribute('data-i18n');
|
|
572
|
+
const text = t(key);
|
|
573
|
+
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
|
|
574
|
+
el.placeholder = text;
|
|
575
|
+
} else if (el.tagName === 'OPTION') {
|
|
576
|
+
el.textContent = text;
|
|
577
|
+
} else {
|
|
578
|
+
el.textContent = text;
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
// 处理 title 属性(单独的 data-i18n-title)
|
|
582
|
+
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
|
583
|
+
const key = el.getAttribute('data-i18n-title');
|
|
584
|
+
el.title = t(key);
|
|
585
|
+
});
|
|
586
|
+
// 更新 html lang 属性
|
|
587
|
+
document.documentElement.lang = currentLang;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function toggleLang() {
|
|
591
|
+
currentLang = currentLang === 'zh-CN' ? 'en-US' : 'zh-CN';
|
|
592
|
+
localStorage.setItem(LANG_KEY, currentLang);
|
|
593
|
+
updateI18n();
|
|
594
|
+
// 强制重新渲染当前视图
|
|
595
|
+
if (state[currentView]) renderView(currentView);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ── 基础路径 ──
|
|
599
|
+
// 本地直连时页面在 "/",经 AUN Service Proxy 时页面在 "/ecweb/"。
|
|
600
|
+
// 取当前页面所在目录(含尾斜杠)作为所有 API/WS 的前缀,使绝对路径在两种
|
|
601
|
+
// 部署下都正确(proxy-server 用首段路径选服务,前缀不能丢)。
|
|
602
|
+
const BASE = location.pathname.replace(/[^/]*$/, '');
|
|
603
|
+
const apiUrl = (p) => BASE + p.replace(/^\/+/, '');
|
|
5
604
|
|
|
6
605
|
// ── 配对 ──
|
|
7
606
|
async function pair(code) {
|
|
8
|
-
const resp = await fetch('
|
|
607
|
+
const resp = await fetch(apiUrl('api/pair'), {
|
|
9
608
|
method: 'POST',
|
|
10
609
|
headers: { 'Content-Type': 'application/json' },
|
|
11
610
|
body: JSON.stringify({ code }),
|
|
@@ -13,6 +612,19 @@ async function pair(code) {
|
|
|
13
612
|
return resp.json();
|
|
14
613
|
}
|
|
15
614
|
|
|
615
|
+
// 本地直连免配对:用空码探测,服务端若判定本地直连会直接发 token。
|
|
616
|
+
// 远程(隧道/真远程)会返回配对码错误,此时回落到配对页。
|
|
617
|
+
async function tryLocalAutoPair() {
|
|
618
|
+
try {
|
|
619
|
+
const res = await pair('');
|
|
620
|
+
if (res && res.ok && res.token) {
|
|
621
|
+
localStorage.setItem(TOKEN_KEY, res.token);
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
} catch {}
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
|
|
16
628
|
function showPairPage(hint) {
|
|
17
629
|
if (ws) { try { ws.close(); } catch {} ws = null; }
|
|
18
630
|
$('#pair-page').style.display = 'flex';
|
|
@@ -31,7 +643,7 @@ function initPairUI() {
|
|
|
31
643
|
const err = $('#pair-error');
|
|
32
644
|
const submit = async () => {
|
|
33
645
|
const code = input.value.trim();
|
|
34
|
-
if (code.length !== 6) { err.textContent = '
|
|
646
|
+
if (code.length !== 6) { err.textContent = t('pair.error.length'); return; }
|
|
35
647
|
btn.disabled = true; err.textContent = '';
|
|
36
648
|
try {
|
|
37
649
|
const res = await pair(code);
|
|
@@ -40,10 +652,10 @@ function initPairUI() {
|
|
|
40
652
|
showApp();
|
|
41
653
|
startApp();
|
|
42
654
|
} else {
|
|
43
|
-
err.textContent = res.reason || '
|
|
655
|
+
err.textContent = res.reason || t('pair.error.failed');
|
|
44
656
|
}
|
|
45
657
|
} catch {
|
|
46
|
-
err.textContent = '
|
|
658
|
+
err.textContent = t('pair.error.network');
|
|
47
659
|
} finally {
|
|
48
660
|
btn.disabled = false;
|
|
49
661
|
}
|
|
@@ -56,9 +668,9 @@ function initPairUI() {
|
|
|
56
668
|
// ── WebSocket 客户端(自动重连)──
|
|
57
669
|
let ws = null;
|
|
58
670
|
let reconnectDelay = 1000;
|
|
59
|
-
let currentView = 'agents';
|
|
671
|
+
let currentView = localStorage.getItem(VIEW_KEY) || 'agents';
|
|
60
672
|
let pendingSub = null; // 重连后要恢复的订阅
|
|
61
|
-
const state = { agents: null, msg: null, session: null, cache: null, system: null, triggers: null };
|
|
673
|
+
const state = { agents: null, msg: null, session: null, cache: null, system: null, triggers: null, monitor: null, gateway: null };
|
|
62
674
|
|
|
63
675
|
function setConnStatus(text, cls) {
|
|
64
676
|
const el = $('#conn-status');
|
|
@@ -70,10 +682,10 @@ function connect() {
|
|
|
70
682
|
const token = localStorage.getItem(TOKEN_KEY);
|
|
71
683
|
if (!token) { showPairPage(); return; }
|
|
72
684
|
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
73
|
-
ws = new WebSocket(`${proto}://${location.host}
|
|
685
|
+
ws = new WebSocket(`${proto}://${location.host}${BASE}ws?token=${encodeURIComponent(token)}`);
|
|
74
686
|
|
|
75
687
|
ws.onopen = () => {
|
|
76
|
-
setConnStatus('●
|
|
688
|
+
setConnStatus('● ' + t('status.connected'), 'ok');
|
|
77
689
|
reconnectDelay = 1000;
|
|
78
690
|
subscribe(currentView, pendingSub || {});
|
|
79
691
|
};
|
|
@@ -106,10 +718,10 @@ function connect() {
|
|
|
106
718
|
ws.onclose = (ev) => {
|
|
107
719
|
if (ev.code === 4001) {
|
|
108
720
|
localStorage.removeItem(TOKEN_KEY);
|
|
109
|
-
showPairPage('
|
|
721
|
+
showPairPage(t('pair.error.tokenInvalid'));
|
|
110
722
|
return;
|
|
111
723
|
}
|
|
112
|
-
setConnStatus('○
|
|
724
|
+
setConnStatus('○ ' + t('status.reconnecting') + '…', 'err');
|
|
113
725
|
setTimeout(connect, reconnectDelay);
|
|
114
726
|
reconnectDelay = Math.min(reconnectDelay * 1.5, 15000);
|
|
115
727
|
};
|
|
@@ -150,10 +762,13 @@ let msgSel = { aid: null, peer: null };
|
|
|
150
762
|
let sessSel = { sessionId: null, project: null };
|
|
151
763
|
let trigSel = { agent: null };
|
|
152
764
|
let sessSearch = '';
|
|
765
|
+
let sessFilterNormal = false; // true=只显示有效会话(userMsgs >= 2)
|
|
153
766
|
let sessChatMode = false; // false=完整视图,true=对话视图(折叠处理过程)
|
|
767
|
+
let monRange = '2m'; // Monitor 时间窗口:2m / 10m / 1h
|
|
154
768
|
|
|
155
769
|
function switchView(view) {
|
|
156
770
|
currentView = view;
|
|
771
|
+
localStorage.setItem(VIEW_KEY, view);
|
|
157
772
|
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.view === view));
|
|
158
773
|
document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id === 'view-' + view));
|
|
159
774
|
// 切换时按当前选择恢复订阅
|
|
@@ -162,6 +777,8 @@ function switchView(view) {
|
|
|
162
777
|
else if (view === 'cache') subscribe('cache', {});
|
|
163
778
|
else if (view === 'system') subscribe('system', {});
|
|
164
779
|
else if (view === 'triggers') subscribe('triggers', { agent: trigSel.agent });
|
|
780
|
+
else if (view === 'monitor') subscribe('monitor', { range: monRange });
|
|
781
|
+
else if (view === 'gateway') subscribe('gateway', {});
|
|
165
782
|
else subscribe('agents', {});
|
|
166
783
|
if (state[view]) renderView(view);
|
|
167
784
|
}
|
|
@@ -179,6 +796,8 @@ function renderView(view) {
|
|
|
179
796
|
else if (view === 'cache') renderCache(state.cache);
|
|
180
797
|
else if (view === 'system') renderSystem(state.system);
|
|
181
798
|
else if (view === 'triggers') renderTriggers(state.triggers);
|
|
799
|
+
else if (view === 'monitor') renderMonitor(state.monitor);
|
|
800
|
+
else if (view === 'gateway') renderGateway(state.gateway);
|
|
182
801
|
}
|
|
183
802
|
|
|
184
803
|
// ── 工具 ──
|
|
@@ -231,74 +850,326 @@ function compareVer(a, b) {
|
|
|
231
850
|
return 0;
|
|
232
851
|
}
|
|
233
852
|
|
|
234
|
-
// ── Agents
|
|
853
|
+
// ── Agents 视图(对齐终端 watch aid:状态点前置 + 名字为主 + 两行 + 工作态着色 + 顶部统计条)──
|
|
854
|
+
|
|
855
|
+
// 逐 AID 异步操作状态(取代全局 _agentBusy):aid → 操作中的描述文字
|
|
856
|
+
const _agentOps = new Map(); // Map<aid, string>
|
|
857
|
+
let _agentBusy = false; // 保留兼容旧引用,不再用于阻塞渲染
|
|
858
|
+
let _agSubtab = 'enabled'; // 'enabled' | 'disabled'
|
|
859
|
+
|
|
860
|
+
// 工作状态徽标:一旦收到过消息就不再回 connected。
|
|
861
|
+
// stopped → connected(仅首次连接无消息时) → idle(收到第一条后) → working → idle ...
|
|
862
|
+
function agentStateBadge(s, agStatus, connStatus) {
|
|
863
|
+
if (agStatus === 'stopped' || connStatus === 'disconnected' || connStatus === 'failed')
|
|
864
|
+
return `<span class="state-badge stopped">${t('status.stopped')}</span>`;
|
|
865
|
+
if (connStatus === 'reconnecting')
|
|
866
|
+
return `<span class="state-badge stopped">${t('status.reconnecting')}</span>`;
|
|
867
|
+
if ((s.processing || 0) > 0)
|
|
868
|
+
return `<span class="state-badge working">${t('status.working')}</span>`;
|
|
869
|
+
// 收到过消息 → 永远是 idle,不再回到 connected
|
|
870
|
+
if ((s.messagesReceived || 0) > 0 || (s.messagesSent || 0) > 0)
|
|
871
|
+
return `<span class="state-badge idle">${t('status.idle')}</span>`;
|
|
872
|
+
return `<span class="state-badge connected">${t('status.connected')}</span>`;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// 发送方式图标标记
|
|
876
|
+
const MSG_KIND_META = {
|
|
877
|
+
send: { icon: '💬', label: () => t('messages.msgKind.reply') },
|
|
878
|
+
thought: { icon: '💭', label: () => t('messages.msgKind.thought') },
|
|
879
|
+
inject: { icon: '📥', label: () => t('messages.msgKind.inject') },
|
|
880
|
+
notify: { icon: '🔔', label: () => t('messages.msgKind.notify') }
|
|
881
|
+
};
|
|
882
|
+
// 消息详情流用:jsonl 持久化的 msgType 词汇(text 为普通回复,不另标)
|
|
883
|
+
const MSG_TYPE_META = {
|
|
884
|
+
thought: { icon: '💭', label: () => t('messages.msgType.thought') },
|
|
885
|
+
image: { icon: '🖼️', label: () => t('messages.msgType.image') },
|
|
886
|
+
file: { icon: '📎', label: () => t('messages.msgType.file') },
|
|
887
|
+
command: { icon: '⌘', label: () => t('messages.msgType.command') }
|
|
888
|
+
};
|
|
889
|
+
function msgTagsHtml(kind, encrypt, chatmode, dir) {
|
|
890
|
+
let h = '';
|
|
891
|
+
// 'send' 仅出向才是「回复」;入向是用户输入,不打回复标记
|
|
892
|
+
const km = (kind === 'send' && dir === 'in') ? null : MSG_KIND_META[kind];
|
|
893
|
+
if (km) h += `<span class="mtag${kind === 'send' ? ' mtag-reply' : ''}">${km.icon}${km.label()}</span>`;
|
|
894
|
+
if (encrypt != null) h += `<span class="mtag">${encrypt ? t('messages.tag.encrypted') : t('messages.tag.plain')}</span>`;
|
|
895
|
+
if (chatmode) h += `<span class="mtag">${chatmode === 'proactive' ? t('messages.tag.proactive') : (chatmode === 'inject' ? t('messages.tag.inject') : t('messages.tag.responsive'))}</span>`;
|
|
896
|
+
return h;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// 消息行:方向箭头 + 标记 + 对端 + 文字
|
|
900
|
+
function agentPreviewHtml(s) {
|
|
901
|
+
const clip = (t) => esc(String(t).replace(/\n/g, ' ').slice(0, 80));
|
|
902
|
+
const line = (dir, peer, text, kind, encrypt, chatmode) => {
|
|
903
|
+
const arrow = dir === 'in' ? '<span class="arrow-in">↓</span>' : '<span class="arrow-out">↑</span>';
|
|
904
|
+
const tags = msgTagsHtml(kind, encrypt, chatmode, dir);
|
|
905
|
+
const peerHtml = peer ? `<span class="peer">${esc(shortAid(peer))}</span>: ` : '';
|
|
906
|
+
const textCls = dir === 'in' ? 'text-in' : 'text-out';
|
|
907
|
+
return `${arrow}${tags ? ' ' + tags + ' ' : ' '}${peerHtml}<span class="${textCls}">${clip(text)}</span>`;
|
|
908
|
+
};
|
|
909
|
+
if ((s.processing || 0) > 0 && s.lastReceivedText)
|
|
910
|
+
return line('in', s.lastReceivedFrom, s.lastReceivedText, s.lastReceivedKind, s.lastReceivedEncrypt, s.lastReceivedChatmode);
|
|
911
|
+
const recvTs = s.lastReceivedAt || 0, sentTs = s.lastSentAt || 0;
|
|
912
|
+
if (!recvTs && !sentTs) return '';
|
|
913
|
+
if (sentTs > recvTs && s.lastSentText)
|
|
914
|
+
return line('out', s.lastSentTo, s.lastSentText, s.lastSentKind, s.lastSentEncrypt, s.lastSentChatmode);
|
|
915
|
+
if (s.lastReceivedText)
|
|
916
|
+
return line('in', s.lastReceivedFrom, s.lastReceivedText, s.lastReceivedKind, s.lastReceivedEncrypt, s.lastReceivedChatmode);
|
|
917
|
+
return '';
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// HTML tooltip(最近 N 轮):时间 + 彩色箭头 + 方式 + 对端 + 文字
|
|
921
|
+
// 渲染为隐藏的内容持有节点(.msg-tip-src);实际展示由 initMsgTipFloat 的浮层负责
|
|
922
|
+
function recentMsgTooltipHtml(recent) {
|
|
923
|
+
if (!recent || !recent.length) return '';
|
|
924
|
+
let h = '<div class="msg-tip-src">';
|
|
925
|
+
for (const m of recent) {
|
|
926
|
+
const rcls = m.dir === 'in' ? 'tip-row-in' : 'tip-row-out';
|
|
927
|
+
const arrow = m.dir === 'in' ? '↓' : '↑';
|
|
928
|
+
// 'send' 仅出向才是「回复」;入向是用户输入,不打回复标记
|
|
929
|
+
const km = (m.kind === 'send' && m.dir === 'in') ? null : MSG_KIND_META[m.kind];
|
|
930
|
+
const kh = km ? `<span class="tip-kind${m.kind === 'send' ? ' tip-kind-reply' : ''}">${km.icon}${km.label()}</span>` : '';
|
|
931
|
+
const enc = m.encrypt != null ? `<span class="tip-flag">${m.encrypt ? t('messages.tag.encrypted') : t('messages.tag.plain')}</span>` : '';
|
|
932
|
+
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>` : '';
|
|
933
|
+
const peer = m.peer ? esc(shortAid(m.peer)) : '';
|
|
934
|
+
const text = esc(String(m.text).replace(/\n/g, ' ').slice(0, 140));
|
|
935
|
+
const time = m.ts ? `<span class="tip-time">${fmtTime(m.ts)}</span>` : '';
|
|
936
|
+
h += `<div class="tip-row ${rcls}">${time}${arrow}${kh}${enc}${mode} <b>${peer}</b> ${text}</div>`;
|
|
937
|
+
}
|
|
938
|
+
return h + '</div>';
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// 单例浮层 tooltip:固定定位、自动翻转上下、横向夹取,确保始终在可视区域内;
|
|
942
|
+
// 鼠标可移动到 tooltip 上而不消失(延迟隐藏 + 进入取消)。
|
|
943
|
+
function initMsgTipFloat() {
|
|
944
|
+
if (initMsgTipFloat._done) return;
|
|
945
|
+
initMsgTipFloat._done = true;
|
|
946
|
+
|
|
947
|
+
let floatEl = null, hideTimer = null, curWrap = null;
|
|
948
|
+
const GAP = 8, MARGIN = 8;
|
|
949
|
+
|
|
950
|
+
function ensureFloat() {
|
|
951
|
+
if (floatEl) return floatEl;
|
|
952
|
+
floatEl = document.createElement('div');
|
|
953
|
+
floatEl.id = 'msg-tip-float';
|
|
954
|
+
floatEl.className = 'msg-tip';
|
|
955
|
+
document.body.appendChild(floatEl);
|
|
956
|
+
floatEl.addEventListener('mouseenter', cancelHide);
|
|
957
|
+
floatEl.addEventListener('mouseleave', scheduleHide);
|
|
958
|
+
return floatEl;
|
|
959
|
+
}
|
|
960
|
+
function cancelHide() { if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } }
|
|
961
|
+
function scheduleHide() { cancelHide(); hideTimer = setTimeout(hideNow, 180); }
|
|
962
|
+
function hideNow() { cancelHide(); curWrap = null; if (floatEl) floatEl.classList.remove('show'); }
|
|
963
|
+
|
|
964
|
+
function position(wrap) {
|
|
965
|
+
const f = floatEl;
|
|
966
|
+
const r = wrap.getBoundingClientRect();
|
|
967
|
+
const vw = document.documentElement.clientWidth;
|
|
968
|
+
const vh = document.documentElement.clientHeight;
|
|
969
|
+
const fw = f.offsetWidth, fh = f.offsetHeight;
|
|
970
|
+
// 纵向:优先放上方;上方放不下则放下方;都放不下则在视口内夹取
|
|
971
|
+
let top;
|
|
972
|
+
if (r.top - GAP - fh >= MARGIN) top = r.top - GAP - fh;
|
|
973
|
+
else if (r.bottom + GAP + fh <= vh - MARGIN) top = r.bottom + GAP;
|
|
974
|
+
else top = Math.max(MARGIN, Math.min(vh - MARGIN - fh, r.top - GAP - fh));
|
|
975
|
+
// 横向:对齐左缘,超出右界则左移,再夹取左界
|
|
976
|
+
let left = r.left;
|
|
977
|
+
if (left + fw > vw - MARGIN) left = vw - MARGIN - fw;
|
|
978
|
+
if (left < MARGIN) left = MARGIN;
|
|
979
|
+
f.style.top = Math.round(top) + 'px';
|
|
980
|
+
f.style.left = Math.round(left) + 'px';
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function show(wrap) {
|
|
984
|
+
const src = wrap.querySelector('.msg-tip-src');
|
|
985
|
+
if (!src || !src.innerHTML.trim()) return;
|
|
986
|
+
const f = ensureFloat();
|
|
987
|
+
cancelHide();
|
|
988
|
+
if (curWrap !== wrap) { f.innerHTML = src.innerHTML; curWrap = wrap; }
|
|
989
|
+
f.classList.add('show');
|
|
990
|
+
position(wrap);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
document.addEventListener('mouseover', (e) => {
|
|
994
|
+
const wrap = e.target.closest && e.target.closest('.ag-msg-wrap');
|
|
995
|
+
if (wrap) show(wrap);
|
|
996
|
+
});
|
|
997
|
+
document.addEventListener('mouseout', (e) => {
|
|
998
|
+
const wrap = e.target.closest && e.target.closest('.ag-msg-wrap');
|
|
999
|
+
if (!wrap) return;
|
|
1000
|
+
const to = e.relatedTarget;
|
|
1001
|
+
if (to && (wrap.contains(to) || (floatEl && floatEl.contains(to)))) return;
|
|
1002
|
+
scheduleHide();
|
|
1003
|
+
});
|
|
1004
|
+
// 滚动时隐藏,避免浮层与行脱节
|
|
1005
|
+
window.addEventListener('scroll', hideNow, true);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// 顶部统计条:Gateway / AIDs total·connected·offline / Messages ↓↑ / Traffic ↓↑ / Version·PID·Uptime
|
|
1009
|
+
function agentsStatsBar(data, aids, stats) {
|
|
1010
|
+
const connected = aids.filter(a => (a.status || 'connected') === 'connected').length;
|
|
1011
|
+
const offline = aids.length - connected;
|
|
1012
|
+
let recv = 0, sent = 0, bin = 0, bout = 0;
|
|
1013
|
+
for (const s of stats) {
|
|
1014
|
+
recv += s.messagesReceived || 0; sent += s.messagesSent || 0;
|
|
1015
|
+
bin += s.bytesReceived || 0; bout += s.bytesSent || 0;
|
|
1016
|
+
}
|
|
1017
|
+
const gws = [...new Set(aids.filter(a => a.gatewayUrl).map(a => a.gatewayUrl))];
|
|
1018
|
+
const gw = gws.length ? gws.map(esc).join(', ') : '—';
|
|
1019
|
+
const st = data.status || {};
|
|
1020
|
+
const pid = st.pid != null ? st.pid : '—';
|
|
1021
|
+
const uptime = st.uptime != null ? fmtDur(st.uptime / 1000) : '—';
|
|
1022
|
+
const ver = data.version || '—';
|
|
1023
|
+
|
|
1024
|
+
let h = '<div class="agents-stats">';
|
|
1025
|
+
h += `<span class="sg"><span class="sg-k">${t('agents.stats.gateway')}</span><span class="sg-gw">${gw}</span></span>`;
|
|
1026
|
+
h += `<span class="sg"><span class="sg-k">${t('agents.stats.aids')}</span>${aids.length} ${t('agents.stats.total')} · <span class="num-on">${connected} ${t('agents.stats.online')}</span>` +
|
|
1027
|
+
`${offline ? ` · <span class="num-off">${offline} ${t('agents.stats.offline')}</span>` : ''}</span>`;
|
|
1028
|
+
h += `<span class="sg"><span class="sg-k">${t('agents.stats.messages')}</span><span class="in">↓${recv}</span> <span class="out">↑${sent}</span></span>`;
|
|
1029
|
+
h += `<span class="sg"><span class="sg-k">${t('agents.stats.traffic')}</span><span class="in">↓${fmtBytes(bin)}</span> <span class="out">↑${fmtBytes(bout)}</span></span>`;
|
|
1030
|
+
h += `<span class="sg"><span class="sg-k">${t('agents.stats.version')}</span>${esc(ver)} · <span class="sg-k">${t('agents.stats.pid')}</span>${pid} · <span class="sg-k">${t('agents.stats.uptime')}</span>${uptime}</span>`;
|
|
1031
|
+
h += '</div>';
|
|
1032
|
+
return h;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// 操作列 HTML(启用页):停止/启动 + 清空队列(conditional) + ···(禁用/重载/编辑/md/删除)
|
|
1036
|
+
function agentOpsHtml(aid, ag, s) {
|
|
1037
|
+
if (_agentOps.has(aid)) {
|
|
1038
|
+
return `<div class="agent-ops agent-ops-busy"><span class="ops-busy-label">${esc(_agentOps.get(aid) || t('common.operating'))}</span></div>`;
|
|
1039
|
+
}
|
|
1040
|
+
const queued = s.queued || 0;
|
|
1041
|
+
const running = ag.status === 'running';
|
|
1042
|
+
let h = `<div class="agent-ops" data-aid="${esc(aid)}" data-status="${esc(ag.status)}">`;
|
|
1043
|
+
if (running) h += `<button class="ctrl-btn ops-stop" data-op="stop">${t('action.stop')}</button>`;
|
|
1044
|
+
else h += `<button class="ctrl-btn ops-start" data-op="start">${t('action.start')}</button>`;
|
|
1045
|
+
if (queued > 0) h += `<button class="ctrl-btn ops-clear-queue" data-op="clear-queue" title="${t('agents.op.clearQueueTitle').replace('{count}', queued)}">${t('action.clearQueue')}</button>`;
|
|
1046
|
+
h += `<div class="ops-more"><button class="ctrl-btn ops-more-btn" data-op="more">···</button>` +
|
|
1047
|
+
`<div class="ops-dropdown">` +
|
|
1048
|
+
`<button class="ops-dd-item" data-op="toggle">${t('action.disable')}</button>` +
|
|
1049
|
+
`<button class="ops-dd-item" data-op="reload">${t('action.reload')}</button>` +
|
|
1050
|
+
`<button class="ops-dd-item" data-op="edit">${t('action.edit')}</button>` +
|
|
1051
|
+
`<a class="ops-dd-item" href="https://${esc(aid)}/agent.md" target="_blank" rel="noopener">${t('agents.op.viewAgentMd')}</a>` +
|
|
1052
|
+
`<button class="ops-dd-item danger" data-op="delete">${t('action.delete')}</button>` +
|
|
1053
|
+
`</div></div>`;
|
|
1054
|
+
h += '</div>';
|
|
1055
|
+
return h;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
235
1058
|
function renderAgents(data) {
|
|
236
1059
|
const el = $('#view-agents');
|
|
237
|
-
if (!data) { el.innerHTML =
|
|
238
|
-
if (
|
|
1060
|
+
if (!data) { el.innerHTML = `<div class="empty">${t('common.loading')}</div>`; return; }
|
|
1061
|
+
if (el.querySelector('.ops-more.open')) return;
|
|
1062
|
+
|
|
1063
|
+
const allAgents = data.agents || [];
|
|
239
1064
|
const aids = data.aids || [];
|
|
240
1065
|
const statsByAid = {};
|
|
241
1066
|
for (const s of (data.stats || [])) statsByAid[s.aid] = s;
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
1067
|
+
const aidConnByAid = {};
|
|
1068
|
+
for (const a of aids) aidConnByAid[a.aid] = a;
|
|
1069
|
+
|
|
1070
|
+
const enabledCount = allAgents.filter(ag => ag.status !== 'disabled').length;
|
|
1071
|
+
const disabledCount = allAgents.filter(ag => ag.status === 'disabled').length;
|
|
1072
|
+
|
|
1073
|
+
// 子标签栏
|
|
1074
|
+
let html = '<div class="agents-toolbar">' +
|
|
1075
|
+
`<div class="ag-subtabs">` +
|
|
1076
|
+
`<button class="ag-subtab${_agSubtab === 'enabled' ? ' active' : ''}" data-subtab="enabled">${t('agents.subtitle.enabled')} (${enabledCount})</button>` +
|
|
1077
|
+
`<button class="ag-subtab${_agSubtab === 'disabled' ? ' active' : ''}" data-subtab="disabled">${t('agents.subtitle.disabled')} (${disabledCount})</button>` +
|
|
1078
|
+
`</div>` +
|
|
1079
|
+
`<button class="ctrl-btn" id="agent-new-btn">${t('action.new')}</button>` +
|
|
1080
|
+
'</div>';
|
|
245
1081
|
|
|
246
|
-
let html = '<div class="agents-toolbar"><button class="ctrl-btn" id="agent-new-btn">+ 新建 Agent</button></div>';
|
|
247
1082
|
if (!data.daemonRunning) {
|
|
248
|
-
html +=
|
|
1083
|
+
html += `<div class="banner">${t('agents.daemonStopped')}</div>`;
|
|
249
1084
|
}
|
|
250
|
-
|
|
251
|
-
|
|
1085
|
+
|
|
1086
|
+
if (_agSubtab === 'disabled') {
|
|
1087
|
+
const disabledAgents = allAgents.filter(ag => ag.status === 'disabled');
|
|
1088
|
+
if (!disabledAgents.length) {
|
|
1089
|
+
html += `<div class="empty">${t('agents.empty.disabled')}</div>`;
|
|
1090
|
+
} else {
|
|
1091
|
+
html += `<table><thead><tr><th>${t('agents.th.agent')}</th><th>${t('agents.th.projectPath')}</th><th>${t('agents.th.operations')}</th></tr></thead><tbody>`;
|
|
1092
|
+
for (const ag of disabledAgents) {
|
|
1093
|
+
const busy = _agentOps.has(ag.aid);
|
|
1094
|
+
const ops = busy
|
|
1095
|
+
? `<div class="agent-ops agent-ops-busy"><span class="ops-busy-label">${esc(_agentOps.get(ag.aid) || t('common.operating'))}</span></div>`
|
|
1096
|
+
: `<div class="agent-ops" data-aid="${esc(ag.aid)}" data-status="disabled"><button class="ctrl-btn ops-enable" data-op="toggle">${t('action.enable')}</button></div>`;
|
|
1097
|
+
html += `<tr class="ag-main">` +
|
|
1098
|
+
`<td><div class="ag-id"><span class="dot off"></span><span class="ag-id-text"><span class="ag-name">${esc(ag.displayName || shortAid(ag.aid))}</span><span class="ag-aid">${esc(ag.aid)}</span></span></div></td>` +
|
|
1099
|
+
`<td style="font-size:11px;font-family:monospace">${esc(ag.projectPath || '—')}</td>` +
|
|
1100
|
+
`<td class="agent-ops-cell">${ops}</td></tr>`;
|
|
1101
|
+
}
|
|
1102
|
+
html += '</tbody></table>';
|
|
1103
|
+
}
|
|
1104
|
+
el.innerHTML = html;
|
|
1105
|
+
bindAgentsEvents(el);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// ── 启用页 ──
|
|
1110
|
+
// 按收发消息总数降序排序(活跃的排前面)
|
|
1111
|
+
const totalMsgs = (ag) => {
|
|
1112
|
+
const s = statsByAid[ag.aid] || {};
|
|
1113
|
+
return (s.messagesReceived || 0) + (s.messagesSent || 0);
|
|
1114
|
+
};
|
|
1115
|
+
const enabledAgents = allAgents.filter(ag => ag.status !== 'disabled')
|
|
1116
|
+
.sort((a, b) => totalMsgs(b) - totalMsgs(a));
|
|
1117
|
+
if (!enabledAgents.length) {
|
|
1118
|
+
html += `<div class="empty">${t('agents.empty.enabled')}</div>`;
|
|
252
1119
|
el.innerHTML = html;
|
|
253
1120
|
bindAgentsEvents(el);
|
|
254
1121
|
return;
|
|
255
1122
|
}
|
|
256
1123
|
|
|
257
1124
|
html += '<table><thead><tr>' +
|
|
258
|
-
'
|
|
259
|
-
'
|
|
1125
|
+
`<th>${t('agents.th.aid')}</th><th>${t('agents.th.work')}</th><th>${t('agents.th.queue')}</th><th>${t('agents.th.model')}</th><th>${t('agents.th.runtime')}</th><th>${t('agents.th.received')}</th><th>${t('agents.th.sent')}</th>` +
|
|
1126
|
+
`<th>${t('agents.th.bytesIn')}</th><th>${t('agents.th.bytesOut')}</th><th>${t('agents.th.peerCount')}</th><th>${t('agents.th.lastActivity')}</th><th>${t('agents.th.operations')}</th>` +
|
|
260
1127
|
'</tr></thead><tbody>';
|
|
261
1128
|
|
|
262
|
-
for (const
|
|
263
|
-
const s = statsByAid[
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
ops = '<span style="color:var(--dim)">—</span>';
|
|
288
|
-
}
|
|
289
|
-
html += '<tr>' +
|
|
290
|
-
`<td><span class="dot ${dotCls}"></span>${esc(status)}</td>` +
|
|
291
|
-
`<td>${esc(shortAid(a.aid))}${name ? ` <span style="color:var(--dim)">(${esc(name)})</span>` : ''}</td>` +
|
|
1129
|
+
for (const ag of enabledAgents) {
|
|
1130
|
+
const s = statsByAid[ag.aid] || {};
|
|
1131
|
+
const conn = aidConnByAid[ag.aid] || {};
|
|
1132
|
+
const connStatus = conn.status || (ag.status === 'running' ? 'connected' : 'disconnected');
|
|
1133
|
+
const dotCls = connStatus === 'connected' ? 'on' : (connStatus === 'reconnecting' ? 'idle' : 'off');
|
|
1134
|
+
const name = s.selfName || ag.displayName || shortAid(ag.aid);
|
|
1135
|
+
const uptime = (connStatus === 'connected' && conn.lastConnectedAt) ? fmtDur((Date.now() - conn.lastConnectedAt) / 1000) : '—';
|
|
1136
|
+
const lastTs = Math.max(s.lastReceivedAt || 0, s.lastSentAt || 0, ag.lastActivity || 0);
|
|
1137
|
+
const preview = agentPreviewHtml(s);
|
|
1138
|
+
// 队列数:不含正在处理的那条
|
|
1139
|
+
const rawQueued = s.queued || 0;
|
|
1140
|
+
const queued = rawQueued;
|
|
1141
|
+
const queueCell = queued > 0 ? `<span class="ag-queue-num">${queued}</span>` : '<span style="color:var(--dim)">0</span>';
|
|
1142
|
+
const model = ag.model || ag.baseagent || '—';
|
|
1143
|
+
|
|
1144
|
+
const idCell = `<div class="ag-id"><span class="dot ${dotCls}" title="${esc(connStatus)}"></span>` +
|
|
1145
|
+
`<span class="ag-id-text"><span class="ag-name">${esc(name)}</span>` +
|
|
1146
|
+
`<span class="ag-aid">${esc(ag.aid)}</span></span></div>`;
|
|
1147
|
+
|
|
1148
|
+
html += `<tr class="ag-main">` +
|
|
1149
|
+
`<td>${idCell}</td>` +
|
|
1150
|
+
`<td>${agentStateBadge(s, ag.status, connStatus)}</td>` +
|
|
1151
|
+
`<td>${queueCell}</td>` +
|
|
1152
|
+
`<td style="font-size:11px;color:var(--dim)">${esc(model)}</td>` +
|
|
1153
|
+
`<td>${uptime}</td>` +
|
|
292
1154
|
`<td>${s.messagesReceived ?? 0}</td><td>${s.messagesSent ?? 0}</td>` +
|
|
293
|
-
`<td>${s.systemReceived ?? 0}/${s.systemSent ?? 0}</td>` +
|
|
294
1155
|
`<td>${fmtBytes(s.bytesReceived)}</td><td>${fmtBytes(s.bytesSent)}</td>` +
|
|
295
|
-
`<td>${s.uniquePeerCount ??
|
|
1156
|
+
`<td>${s.uniquePeerCount ?? conn.peerCount ?? 0}</td>` +
|
|
296
1157
|
`<td>${fmtAgo(lastTs)}</td>` +
|
|
297
|
-
`<td class="
|
|
298
|
-
`<td class="agent-ops-cell">${ops}</td>` +
|
|
1158
|
+
`<td class="agent-ops-cell">${agentOpsHtml(ag.aid, ag, s)}</td>` +
|
|
299
1159
|
'</tr>';
|
|
1160
|
+
// 自定义 tooltip(HTML,hover 显示)
|
|
1161
|
+
const recent = (s.recentMessages || []);
|
|
1162
|
+
const tipHtml = recentMsgTooltipHtml(recent);
|
|
1163
|
+
|
|
1164
|
+
html += `<tr class="ag-sub"><td colspan="12"><div class="ag-info">` +
|
|
1165
|
+
(ag.projectPath ? `<div class="ag-path">${esc(ag.projectPath)}</div>` : '') +
|
|
1166
|
+
(preview ? `<div class="ag-msg-wrap">${tipHtml}<div class="ag-msg">${preview}</div></div>` : '') +
|
|
1167
|
+
'</div></td></tr>';
|
|
300
1168
|
}
|
|
301
1169
|
html += '</tbody></table>';
|
|
1170
|
+
if (data.daemonRunning) {
|
|
1171
|
+
html += agentsStatsBar(data, aids, data.stats || []);
|
|
1172
|
+
}
|
|
302
1173
|
el.innerHTML = html;
|
|
303
1174
|
bindAgentsEvents(el);
|
|
304
1175
|
}
|
|
@@ -331,17 +1202,17 @@ function groupLabel(g) {
|
|
|
331
1202
|
|
|
332
1203
|
function renderCache(data) {
|
|
333
1204
|
const el = $('#view-cache');
|
|
334
|
-
if (!data) { el.innerHTML =
|
|
1205
|
+
if (!data) { el.innerHTML = `<div class="empty">${t('common.loading')}</div>`; return; }
|
|
335
1206
|
if (!data.daemonRunning) {
|
|
336
|
-
el.innerHTML =
|
|
1207
|
+
el.innerHTML = `<div class="banner">${t('cache.daemonStopped')}</div>`;
|
|
337
1208
|
return;
|
|
338
1209
|
}
|
|
339
1210
|
if (!data.supported || !data.stats) {
|
|
340
|
-
el.innerHTML =
|
|
1211
|
+
el.innerHTML = `<div class="banner">${t('cache.notSupported')}</div>`;
|
|
341
1212
|
return;
|
|
342
1213
|
}
|
|
343
1214
|
const s = data.stats;
|
|
344
|
-
const
|
|
1215
|
+
const tot = s.totals;
|
|
345
1216
|
const occ = s.occupancy || {};
|
|
346
1217
|
// 全部组占用合计
|
|
347
1218
|
let totalBytes = 0;
|
|
@@ -350,23 +1221,23 @@ function renderCache(data) {
|
|
|
350
1221
|
let html = '';
|
|
351
1222
|
|
|
352
1223
|
// ① 总览卡片
|
|
353
|
-
const rate = hitRate(
|
|
1224
|
+
const rate = hitRate(tot);
|
|
354
1225
|
html += '<div class="cache-cards">';
|
|
355
|
-
html += card('
|
|
356
|
-
html += card('
|
|
357
|
-
html += card('
|
|
358
|
-
html += card('
|
|
359
|
-
html += card('
|
|
360
|
-
html += card('
|
|
361
|
-
html += card('
|
|
362
|
-
html += card('
|
|
1226
|
+
html += card(t('cache.card.hitRate'), fmtPct(rate), rateCls(rate), `${fmtNum(tot.hits)} ${t('cache.card.hit')} / ${fmtNum(tot.misses)} ${t('cache.card.miss')}`);
|
|
1227
|
+
html += card(t('cache.card.reads'), fmtNum(tot.gets), '', `${fmtNum(tot.hits)} ${t('cache.card.hit')} · ${fmtNum(tot.misses)} ${t('cache.card.miss')}`);
|
|
1228
|
+
html += card(t('cache.card.entries'), fmtNum(s.size), '', fmtBytes(totalBytes) + ' ' + t('cache.card.memory'));
|
|
1229
|
+
html += card(t('cache.card.statChecks'), fmtNum(tot.statChecks), '', 'mtime ' + t('cache.policy.mtime'));
|
|
1230
|
+
html += card(t('cache.card.reReads'), fmtNum(tot.reReads), '', t('cache.policy.manual'));
|
|
1231
|
+
html += card(t('cache.card.evictions'), fmtNum(tot.evictions), tot.evictions ? 'idle' : '', 'LRU');
|
|
1232
|
+
html += card(t('cache.card.invalidations'), fmtNum(tot.invalidations), '', 'reload');
|
|
1233
|
+
html += card(t('cache.card.since'), fmtAgo(s.since) + ' ' + t('cache.card.ago'), '', fmtTime(s.since));
|
|
363
1234
|
html += '</div>';
|
|
364
1235
|
|
|
365
1236
|
// ② 按 group 表(每组命中率 + 占用 + 容量水位)
|
|
366
|
-
html +=
|
|
1237
|
+
html += `<h3 class="cache-h">${t('cache.section.byGroup')}</h3>`;
|
|
367
1238
|
html += '<table><thead><tr>' +
|
|
368
|
-
'
|
|
369
|
-
'
|
|
1239
|
+
`<th>${t('cache.th.group')}</th><th>${t('cache.th.type')}</th><th>${t('cache.th.reads')}</th><th>${t('cache.th.hits')}</th><th>${t('cache.th.misses')}</th><th>${t('cache.th.hitRate')}</th>` +
|
|
1240
|
+
`<th>${t('cache.th.reReads')}</th><th>${t('cache.th.evictions')}</th><th>${t('cache.th.entries')}</th><th>${t('cache.th.memory')}</th><th>${t('cache.th.capacity')}</th>` +
|
|
370
1241
|
'</tr></thead><tbody>';
|
|
371
1242
|
const groups = Object.keys(s.byGroup).sort((a, b) => (s.byGroup[b].gets || 0) - (s.byGroup[a].gets || 0));
|
|
372
1243
|
for (const g of groups) {
|
|
@@ -392,11 +1263,15 @@ function renderCache(data) {
|
|
|
392
1263
|
html += '</tbody></table>';
|
|
393
1264
|
|
|
394
1265
|
// ③ 按 policy 表
|
|
395
|
-
html +=
|
|
1266
|
+
html += `<h3 class="cache-h">${t('cache.section.byPolicy')}</h3>`;
|
|
396
1267
|
html += '<table><thead><tr>' +
|
|
397
|
-
'
|
|
1268
|
+
`<th>${t('cache.th.policy')}</th><th>${t('cache.th.reads')}</th><th>${t('cache.th.hits')}</th><th>${t('cache.th.misses')}</th><th>${t('cache.th.hitRate')}</th><th>${t('cache.th.statChecks')}</th><th>${t('cache.th.reReads')}</th>` +
|
|
398
1269
|
'</tr></thead><tbody>';
|
|
399
|
-
const POLICY_DESC = {
|
|
1270
|
+
const POLICY_DESC = {
|
|
1271
|
+
'on-reload': t('cache.policy.onReload'),
|
|
1272
|
+
'manual': t('cache.policy.manual'),
|
|
1273
|
+
'mtime': t('cache.policy.mtime')
|
|
1274
|
+
};
|
|
400
1275
|
for (const pol of ['on-reload', 'mtime', 'manual']) {
|
|
401
1276
|
const c = s.byPolicy[pol];
|
|
402
1277
|
if (!c || !c.gets) continue;
|
|
@@ -410,8 +1285,7 @@ function renderCache(data) {
|
|
|
410
1285
|
}
|
|
411
1286
|
html += '</tbody></table>';
|
|
412
1287
|
|
|
413
|
-
html +=
|
|
414
|
-
'渲染后结果(按 vars)不缓存,故不在此列。</div>';
|
|
1288
|
+
html += `<div class="cache-note">${t('cache.note')}</div>`;
|
|
415
1289
|
|
|
416
1290
|
el.innerHTML = html;
|
|
417
1291
|
}
|
|
@@ -432,7 +1306,7 @@ function renderMsg(data) {
|
|
|
432
1306
|
const messages = data.messages || [];
|
|
433
1307
|
|
|
434
1308
|
// 左:AID 列表
|
|
435
|
-
let aidsHtml =
|
|
1309
|
+
let aidsHtml = `<div class="col-title">${t('messages.colTitle.aid')}</div>`;
|
|
436
1310
|
for (const a of aids) {
|
|
437
1311
|
const sel = a.aid === msgSel.aid ? ' sel' : '';
|
|
438
1312
|
aidsHtml += `<div class="list-item${sel}" data-aid="${esc(a.aid)}">` +
|
|
@@ -445,10 +1319,10 @@ function renderMsg(data) {
|
|
|
445
1319
|
});
|
|
446
1320
|
|
|
447
1321
|
// 中:对端列表
|
|
448
|
-
let peersHtml =
|
|
1322
|
+
let peersHtml = `<div class="col-title">${t('messages.colTitle.peers')}</div>`;
|
|
449
1323
|
if (msgSel.aid) {
|
|
450
1324
|
const allSel = msgSel.peer === null ? ' sel' : '';
|
|
451
|
-
peersHtml += `<div class="list-item${allSel}" data-peer=""><div class="name"
|
|
1325
|
+
peersHtml += `<div class="list-item${allSel}" data-peer=""><div class="name">${t('messages.colTitle.all')}</div>` +
|
|
452
1326
|
`<div class="sub">${peers.length} peers</div></div>`;
|
|
453
1327
|
for (const p of peers) {
|
|
454
1328
|
const sel = p.peerId === msgSel.peer ? ' sel' : '';
|
|
@@ -457,7 +1331,7 @@ function renderMsg(data) {
|
|
|
457
1331
|
`<div class="sub">↓${p.inbound} ↑${p.outbound} · ${fmtAgo(p.lastAt)}</div></div>`;
|
|
458
1332
|
}
|
|
459
1333
|
} else {
|
|
460
|
-
peersHtml +=
|
|
1334
|
+
peersHtml += `<div class="empty">${t('messages.empty.selectAid')}</div>`;
|
|
461
1335
|
}
|
|
462
1336
|
$('#msg-peers').innerHTML = peersHtml;
|
|
463
1337
|
$('#msg-peers').querySelectorAll('.list-item').forEach(item => {
|
|
@@ -466,7 +1340,7 @@ function renderMsg(data) {
|
|
|
466
1340
|
|
|
467
1341
|
// 右:消息流
|
|
468
1342
|
const stream = $('#msg-stream');
|
|
469
|
-
if (!msgSel.aid) { stream.innerHTML =
|
|
1343
|
+
if (!msgSel.aid) { stream.innerHTML = `<div class="empty">${t('messages.empty.selectToView')}</div>`; return; }
|
|
470
1344
|
const atBottom = stream.scrollHeight - stream.scrollTop - stream.clientHeight < 60;
|
|
471
1345
|
let msgHtml = '';
|
|
472
1346
|
for (const m of messages) {
|
|
@@ -474,15 +1348,19 @@ function renderMsg(data) {
|
|
|
474
1348
|
const arrow = m.dir === 'in' ? '↓' : '↑';
|
|
475
1349
|
const from = shortAid(m.from), to = shortAid(m.to);
|
|
476
1350
|
const tags = [];
|
|
477
|
-
if (m.chatType === 'group') tags.push('
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const
|
|
1351
|
+
if (m.chatType === 'group') tags.push(t('messages.tag.group'));
|
|
1352
|
+
// 消息详情流的 kind 来自 jsonl 的 msgType(text/thought/image/file/command),
|
|
1353
|
+
// 与 agents 页内存态的 MsgKind(send/thought/inject/notify)不是同一套词汇。
|
|
1354
|
+
const mt = MSG_TYPE_META[m.msgType];
|
|
1355
|
+
if (mt) tags.push(`${mt.icon}${mt.label()}`);
|
|
1356
|
+
if (m.encrypt != null) tags.push(m.encrypt ? t('messages.tag.encrypted') : t('messages.tag.plain'));
|
|
1357
|
+
if (m.chatmode) tags.push(m.chatmode === 'proactive' ? t('messages.tag.proactive') : (m.chatmode === 'inject' ? t('messages.tag.inject') : t('messages.tag.responsive')));
|
|
1358
|
+
const tagHtml = tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('');
|
|
481
1359
|
msgHtml += `<div class="bubble ${cls}">` +
|
|
482
1360
|
`<div class="meta">${fmtTime(m.ts)} ${arrow} ${esc(from)}→${esc(to)}${tagHtml}</div>` +
|
|
483
1361
|
`<div class="body">${esc(m.content)}</div></div>`;
|
|
484
1362
|
}
|
|
485
|
-
stream.innerHTML = msgHtml ||
|
|
1363
|
+
stream.innerHTML = msgHtml || `<div class="empty">${t('messages.empty.noMessages')}</div>`;
|
|
486
1364
|
if (atBottom) stream.scrollTop = stream.scrollHeight;
|
|
487
1365
|
}
|
|
488
1366
|
|
|
@@ -502,17 +1380,19 @@ function renderSession(data) {
|
|
|
502
1380
|
|
|
503
1381
|
// 搜索过滤
|
|
504
1382
|
const q = sessSearch.trim().toLowerCase();
|
|
505
|
-
const filtered =
|
|
506
|
-
|
|
507
|
-
|
|
1383
|
+
const filtered = transcripts
|
|
1384
|
+
.filter(t => !sessFilterNormal || (t.userMsgs || 0) >= 2)
|
|
1385
|
+
.filter(t => !q || (t.title || '').toLowerCase().includes(q) || (t.firstUser || '').toLowerCase().includes(q));
|
|
508
1386
|
|
|
509
1387
|
// 左栏:过滤条 + 列表
|
|
510
1388
|
const projOpts = projects.map(p =>
|
|
511
1389
|
`<option value="${esc(p.encoded)}"${p.encoded === sessSel.project ? ' selected' : ''}>${esc(p.label)} (${p.count})</option>`
|
|
512
1390
|
).join('');
|
|
1391
|
+
const normalCount = transcripts.filter(t => (t.userMsgs || 0) >= 2).length;
|
|
513
1392
|
let listHtml = '<div class="sess-filter">' +
|
|
514
1393
|
`<select id="sess-project">${projOpts}</select>` +
|
|
515
1394
|
`<input id="sess-search" type="text" placeholder="搜索标题/首条消息…" value="${esc(sessSearch)}">` +
|
|
1395
|
+
`<button id="sess-filter-btn" class="ctrl-btn${sessFilterNormal ? ' active' : ''}" title="只显示有效会话(≥2 条用户消息)">有效 ${normalCount}</button>` +
|
|
516
1396
|
`<div class="sess-count">${filtered.length} / ${transcripts.length} 个会话</div></div>` +
|
|
517
1397
|
'<div class="sess-items">';
|
|
518
1398
|
|
|
@@ -543,6 +1423,8 @@ function renderSession(data) {
|
|
|
543
1423
|
sessSearch = '';
|
|
544
1424
|
subscribe('session', { project: sessSel.project });
|
|
545
1425
|
};
|
|
1426
|
+
const filterBtn = $('#sess-filter-btn');
|
|
1427
|
+
if (filterBtn) filterBtn.onclick = () => { sessFilterNormal = !sessFilterNormal; renderSession(state.session); };
|
|
546
1428
|
const searchEl = $('#sess-search');
|
|
547
1429
|
if (searchEl) {
|
|
548
1430
|
searchEl.oninput = () => { sessSearch = searchEl.value; renderSession(state.session); };
|
|
@@ -758,79 +1640,189 @@ function toast(text, isErr) {
|
|
|
758
1640
|
}
|
|
759
1641
|
|
|
760
1642
|
// ── Agents 操作 ──
|
|
761
|
-
|
|
1643
|
+
// (_agentBusy 已在 Agents 视图顶部声明,仅 agentOpNew 仍在用)
|
|
1644
|
+
|
|
1645
|
+
// 设置某 aid 的操作状态并立即刷新对应行的按钮区(不重渲整表)
|
|
1646
|
+
function setAgentOp(aid, label) {
|
|
1647
|
+
if (label == null) _agentOps.delete(aid); else _agentOps.set(aid, label);
|
|
1648
|
+
const cell = document.querySelector(`.agent-ops[data-aid="${CSS.escape(aid)}"], .agent-ops-busy[data-aid="${CSS.escape(aid)}"]`)?.closest('td');
|
|
1649
|
+
if (!cell || !state.agents) return;
|
|
1650
|
+
const ag = (state.agents.agents || []).find(x => x.aid === aid);
|
|
1651
|
+
if (!ag) return;
|
|
1652
|
+
if (ag.status === 'disabled') {
|
|
1653
|
+
// 禁用页:只有启用按钮 / 操作中态
|
|
1654
|
+
cell.innerHTML = _agentOps.has(aid)
|
|
1655
|
+
? `<div class="agent-ops agent-ops-busy"><span class="ops-busy-label">${esc(_agentOps.get(aid) || t('common.operating'))}</span></div>`
|
|
1656
|
+
: `<div class="agent-ops" data-aid="${esc(aid)}" data-status="disabled"><button class="ctrl-btn ops-enable" data-op="toggle">${t('action.enable')}</button></div>`;
|
|
1657
|
+
} else {
|
|
1658
|
+
const statsByAid = {};
|
|
1659
|
+
for (const s of (state.agents.stats || [])) statsByAid[s.aid] = s;
|
|
1660
|
+
cell.innerHTML = agentOpsHtml(aid, ag, statsByAid[aid] || {});
|
|
1661
|
+
}
|
|
1662
|
+
bindOpsCell(cell, aid, ag.status);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function bindOpsCell(cell, aid, status) {
|
|
1666
|
+
cell.querySelectorAll('button[data-op]').forEach(btn => {
|
|
1667
|
+
btn.addEventListener('click', (e) => {
|
|
1668
|
+
const op = btn.dataset.op;
|
|
1669
|
+
if (op === 'more') {
|
|
1670
|
+
const more = btn.closest('.ops-more');
|
|
1671
|
+
const wasOpen = more.classList.contains('open');
|
|
1672
|
+
document.querySelectorAll('.ops-more.open').forEach(m => m.classList.remove('open'));
|
|
1673
|
+
if (!wasOpen) more.classList.add('open');
|
|
1674
|
+
e.stopPropagation();
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
if (op === 'edit') agentOpEdit(aid);
|
|
1678
|
+
else if (op === 'reload') agentOpReload(aid);
|
|
1679
|
+
else if (op === 'toggle') agentOpToggle(aid, status);
|
|
1680
|
+
else if (op === 'delete') agentOpDelete(aid);
|
|
1681
|
+
else if (op === 'clear-queue') agentOpClearQueue(aid);
|
|
1682
|
+
else if (op === 'stop') agentOpStop(aid);
|
|
1683
|
+
else if (op === 'start') agentOpStart(aid);
|
|
1684
|
+
else if (op === 'mute') agentOpMute(aid);
|
|
1685
|
+
else if (op === 'unmute') agentOpUnmute(aid);
|
|
1686
|
+
});
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// click-outside 关闭下拉:全局只绑一次(避免每次重渲染叠加监听器)
|
|
1691
|
+
let _opsOutsideBound = false;
|
|
1692
|
+
function ensureOpsOutsideClose() {
|
|
1693
|
+
if (_opsOutsideBound) return;
|
|
1694
|
+
_opsOutsideBound = true;
|
|
1695
|
+
document.addEventListener('click', (e) => {
|
|
1696
|
+
if (e.target.closest && e.target.closest('.ops-more')) return; // 点在菜单内不关
|
|
1697
|
+
document.querySelectorAll('.ops-more.open').forEach(m => m.classList.remove('open'));
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
762
1700
|
|
|
763
1701
|
function bindAgentsEvents(el) {
|
|
764
1702
|
el.querySelector('#agent-new-btn')?.addEventListener('click', agentOpNew);
|
|
1703
|
+
ensureOpsOutsideClose();
|
|
1704
|
+
// 子标签切换:仅切视图变量并重渲,不重新订阅
|
|
1705
|
+
el.querySelectorAll('.ag-subtab').forEach(btn => {
|
|
1706
|
+
btn.addEventListener('click', () => {
|
|
1707
|
+
const tab = btn.dataset.subtab;
|
|
1708
|
+
if (tab && tab !== _agSubtab) { _agSubtab = tab; renderAgents(state.agents); }
|
|
1709
|
+
});
|
|
1710
|
+
});
|
|
765
1711
|
el.querySelectorAll('.agent-ops').forEach(div => {
|
|
766
1712
|
const aid = div.dataset.aid;
|
|
767
1713
|
const status = div.dataset.status;
|
|
768
|
-
div.
|
|
769
|
-
btn.addEventListener('click', () => {
|
|
770
|
-
const op = btn.dataset.op;
|
|
771
|
-
if (op === 'edit') agentOpEdit(aid);
|
|
772
|
-
else if (op === 'reload') agentOpReload(aid);
|
|
773
|
-
else if (op === 'toggle') agentOpToggle(aid, status);
|
|
774
|
-
else if (op === 'delete') agentOpDelete(aid);
|
|
775
|
-
});
|
|
776
|
-
});
|
|
1714
|
+
bindOpsCell(div.closest('td'), aid, status);
|
|
777
1715
|
});
|
|
778
1716
|
}
|
|
779
1717
|
|
|
1718
|
+
// 异步操作包装:设置 "操作中" 状态、执行、清除
|
|
1719
|
+
async function withAgentOp(aid, label, fn) {
|
|
1720
|
+
setAgentOp(aid, label);
|
|
1721
|
+
try { await fn(); }
|
|
1722
|
+
finally { setAgentOp(aid, null); }
|
|
1723
|
+
}
|
|
1724
|
+
|
|
780
1725
|
async function agentOpReload(aid, force = false) {
|
|
781
|
-
|
|
782
|
-
try {
|
|
1726
|
+
await withAgentOp(aid, t('agents.op.reloading'), async () => {
|
|
783
1727
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'reload', args: { aid, force } }));
|
|
784
1728
|
if (r.error?.code === 'BUSY') {
|
|
785
|
-
if (confirm(r.error.message + '\n
|
|
1729
|
+
if (confirm(r.error.message + '\n' + t('agents.op.confirmReload'))) { setAgentOp(aid, null); return agentOpReload(aid, true); }
|
|
786
1730
|
return;
|
|
787
1731
|
}
|
|
788
1732
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
789
|
-
toast('
|
|
1733
|
+
toast(t('agents.op.reloaded'));
|
|
790
1734
|
subscribe('agents', {});
|
|
791
|
-
}
|
|
792
|
-
finally { _agentBusy = false; }
|
|
1735
|
+
});
|
|
793
1736
|
}
|
|
794
1737
|
|
|
795
1738
|
async function agentOpToggle(aid, status) {
|
|
796
1739
|
const action = status === 'disabled' ? 'enable' : 'disable';
|
|
797
|
-
|
|
798
|
-
|
|
1740
|
+
const label = action === 'disable' ? t('agents.op.disabling') : t('agents.op.enabling');
|
|
1741
|
+
await withAgentOp(aid, label, async () => {
|
|
799
1742
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action, args: { aid } }));
|
|
800
1743
|
if (r.error?.code === 'BUSY') {
|
|
801
|
-
if (confirm(r.error.message + `\n
|
|
1744
|
+
if (confirm(r.error.message + `\n${t('agents.op.confirmToggle')}${action === 'disable' ? t('action.disable') : t('action.enable')}?`)) {
|
|
802
1745
|
const r2 = mResp(await menuSend({ type: 'menu.action', name: 'agent', action, args: { aid, force: true } }));
|
|
803
1746
|
if (r2.error) toast(r2.error.message || r2.error.code, true);
|
|
804
|
-
else {
|
|
1747
|
+
else {
|
|
1748
|
+
toast(action === 'disable' ? t('agents.op.disabled') : t('agents.op.enabled'));
|
|
1749
|
+
// 禁用后立即切到禁用页;启用后等数据刷新(agent 需先完成启动才移到启用页)
|
|
1750
|
+
if (action === 'disable') _agSubtab = 'disabled';
|
|
1751
|
+
subscribe('agents', {});
|
|
1752
|
+
}
|
|
805
1753
|
}
|
|
806
1754
|
return;
|
|
807
1755
|
}
|
|
808
1756
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
809
|
-
toast(
|
|
1757
|
+
toast(action === 'disable' ? t('agents.op.disabled') : t('agents.op.enabled'));
|
|
1758
|
+
if (action === 'disable') _agSubtab = 'disabled';
|
|
810
1759
|
subscribe('agents', {});
|
|
811
|
-
}
|
|
812
|
-
finally { _agentBusy = false; }
|
|
1760
|
+
});
|
|
813
1761
|
}
|
|
814
1762
|
|
|
815
1763
|
async function agentOpDelete(aid) {
|
|
816
|
-
if (!confirm(
|
|
1764
|
+
if (!confirm(t('agents.op.confirmDelete').replace('{aid}', aid))) return;
|
|
817
1765
|
const purge = confirm('同时清除 agent 数据目录?');
|
|
818
|
-
|
|
819
|
-
try {
|
|
1766
|
+
await withAgentOp(aid, t('agents.op.deleting'), async () => {
|
|
820
1767
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'delete', args: { aid, purge } }));
|
|
821
1768
|
if (r.error?.code === 'BUSY') {
|
|
822
|
-
if (confirm(r.error.message + '\n
|
|
1769
|
+
if (confirm(r.error.message + '\n' + t('agents.op.confirmForceDelete'))) {
|
|
823
1770
|
const r2 = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'delete', args: { aid, purge, force: true } }));
|
|
824
1771
|
if (r2.error) toast(r2.error.message || r2.error.code, true);
|
|
825
|
-
else { toast('
|
|
1772
|
+
else { toast(t('agents.op.deleted')); subscribe('agents', {}); }
|
|
826
1773
|
}
|
|
827
1774
|
return;
|
|
828
1775
|
}
|
|
829
1776
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
830
|
-
toast('
|
|
1777
|
+
toast(t('agents.op.deleted'));
|
|
831
1778
|
subscribe('agents', {});
|
|
832
|
-
}
|
|
833
|
-
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
async function agentOpClearQueue(aid) {
|
|
1783
|
+
if (!confirm(t('agents.op.confirmClearQueue').replace('{aid}', aid))) return;
|
|
1784
|
+
await withAgentOp(aid, t('common.operating'), async () => {
|
|
1785
|
+
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'queue-clear', args: { aid } }));
|
|
1786
|
+
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1787
|
+
toast(`✓ ${r.data?.cleared ?? 0} messages cleared`);
|
|
1788
|
+
subscribe('agents', {});
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
async function agentOpStop(aid) {
|
|
1793
|
+
await withAgentOp(aid, t('agents.op.stopping'), async () => {
|
|
1794
|
+
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'stop', args: { aid } }));
|
|
1795
|
+
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1796
|
+
toast(t('agents.op.stopped'));
|
|
1797
|
+
subscribe('agents', {});
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
async function agentOpStart(aid) {
|
|
1802
|
+
await withAgentOp(aid, t('agents.op.starting'), async () => {
|
|
1803
|
+
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'start', args: { aid } }));
|
|
1804
|
+
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1805
|
+
toast(t('agents.op.started'));
|
|
1806
|
+
subscribe('agents', {});
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
async function agentOpMute(aid) {
|
|
1811
|
+
await withAgentOp(aid, '禁言中…', async () => {
|
|
1812
|
+
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'mute', args: { aid } }));
|
|
1813
|
+
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1814
|
+
toast('✓ 已禁言');
|
|
1815
|
+
subscribe('agents', {});
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
async function agentOpUnmute(aid) {
|
|
1820
|
+
await withAgentOp(aid, '解禁中…', async () => {
|
|
1821
|
+
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'unmute', args: { aid } }));
|
|
1822
|
+
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1823
|
+
toast('✓ 已解禁');
|
|
1824
|
+
subscribe('agents', {});
|
|
1825
|
+
});
|
|
834
1826
|
}
|
|
835
1827
|
|
|
836
1828
|
async function agentOpNew() {
|
|
@@ -849,29 +1841,61 @@ async function agentOpNew() {
|
|
|
849
1841
|
}
|
|
850
1842
|
|
|
851
1843
|
async function agentOpEdit(aid) {
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
const [qr] = await Promise.all([
|
|
855
|
-
menuSend({ type: 'menu.query', name: 'agent', args: { aid } }),
|
|
856
|
-
]);
|
|
1844
|
+
await withAgentOp(aid, t('common.operating'), async () => {
|
|
1845
|
+
const qr = await menuSend({ type: 'menu.query', name: 'agent', args: { aid } });
|
|
857
1846
|
const q = mResp(qr);
|
|
858
|
-
if (q.error) { toast(q.error.message || q.error.code, true);
|
|
1847
|
+
if (q.error) { toast(q.error.message || q.error.code, true); return; }
|
|
859
1848
|
const cfg = q.data;
|
|
860
|
-
//
|
|
1849
|
+
setAgentOp(aid, null); // 查询完毕先恢复,等用户填完 prompt
|
|
861
1850
|
const projectRaw = prompt('项目路径:', cfg.config?.projects?.defaultPath || '');
|
|
862
1851
|
const ownersRaw = prompt('Owners(逗号分隔 AID):', (cfg.config?.owners || []).join(', '));
|
|
863
1852
|
const patch = {};
|
|
864
1853
|
if (projectRaw !== null) patch.projects = { defaultPath: projectRaw };
|
|
865
1854
|
if (ownersRaw !== null) patch.owners = ownersRaw.split(',').map(s => s.trim()).filter(Boolean);
|
|
866
|
-
if (Object.keys(patch).length
|
|
1855
|
+
if (!Object.keys(patch).length) return;
|
|
1856
|
+
setAgentOp(aid, t('common.operating'));
|
|
867
1857
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'update', args: { aid, patch } }));
|
|
868
1858
|
if (r.error) toast(r.error.message || r.error.code, true);
|
|
869
|
-
else toast('
|
|
870
|
-
}
|
|
871
|
-
finally { _agentBusy = false; }
|
|
1859
|
+
else toast(t('agents.op.saved'));
|
|
1860
|
+
});
|
|
872
1861
|
}
|
|
873
1862
|
|
|
874
1863
|
// ── System 视图 ──
|
|
1864
|
+
function channelHealthRow(c) {
|
|
1865
|
+
const dot = c.connected ? 'on' : (c.aidStatus === 'reconnecting' || c.aidStatus === 'kicked' ? 'idle' : 'off');
|
|
1866
|
+
let meta = '';
|
|
1867
|
+
if (c.aidStatus && c.aidStatus !== 'connected') meta += ` <span style="color:var(--dim)">${esc(c.aidStatus)}</span>`;
|
|
1868
|
+
if (c.reconnectCount > 0) meta += ` <span style="color:var(--dim)">重连 ${c.reconnectCount}</span>`;
|
|
1869
|
+
if (c.flapCount > 0) meta += ` <span style="color:var(--red)">抖动 ${c.flapCount}</span>`;
|
|
1870
|
+
const reason = c.kickReason || c.lastError;
|
|
1871
|
+
if (reason && !c.connected) meta += ` <span style="color:var(--red)" title="${esc(reason)}">"${esc(reason)}"</span>`;
|
|
1872
|
+
return `<div class="ch-row"><span class="dot ${dot}"></span>${esc(c.type)}${c.instName && c.instName !== c.type ? ' ' + esc(c.instName) : ''}${meta}</div>`;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
function agentHealthCard(ag) {
|
|
1876
|
+
const dot = ag.status === 'running' ? 'on' : ag.status === 'disabled' ? 'idle' : 'off';
|
|
1877
|
+
let h = `<div class="agent-health-card">`;
|
|
1878
|
+
h += `<div class="ahc-head"><span class="dot ${dot}"></span><span class="ahc-aid">${esc(ag.aid || ag.name)}</span><span class="ahc-status">${esc(ag.status)}</span></div>`;
|
|
1879
|
+
// 项目路径
|
|
1880
|
+
if (ag.projectPath) h += `<div class="ahc-row"><span class="ahc-k">项目</span><span class="ahc-v ahc-path" title="${esc(ag.projectPath)}">${esc(ag.projectPath)}</span></div>`;
|
|
1881
|
+
// 后端
|
|
1882
|
+
const backend = [ag.baseagent, ag.model, ag.effort].filter(Boolean).map(esc).join(' · ');
|
|
1883
|
+
h += `<div class="ahc-row"><span class="ahc-k">后端</span><span class="ahc-v">${backend || '—'}</span></div>`;
|
|
1884
|
+
// 渠道
|
|
1885
|
+
let chans = '';
|
|
1886
|
+
for (const c of (ag.channels || [])) chans += channelHealthRow(c);
|
|
1887
|
+
h += `<div class="ahc-row"><span class="ahc-k">渠道</span><span class="ahc-v">${chans || '<span style="color:var(--dim)">无</span>'}</span></div>`;
|
|
1888
|
+
// 负载
|
|
1889
|
+
const load = `${ag.processing ?? 0} 处理中 · ${ag.pending ?? 0} 待处理`;
|
|
1890
|
+
h += `<div class="ahc-row"><span class="ahc-k">负载</span><span class="ahc-v">${load}</span></div>`;
|
|
1891
|
+
// 活动
|
|
1892
|
+
if (ag.lastActivity) h += `<div class="ahc-row"><span class="ahc-k">活动</span><span class="ahc-v">${fmtAgo(ag.lastActivity)} 前</span></div>`;
|
|
1893
|
+
// 错误
|
|
1894
|
+
if (ag.error) h += `<div class="ahc-err">⚠ ${esc(String(ag.error).slice(0, 120))}</div>`;
|
|
1895
|
+
h += '</div>';
|
|
1896
|
+
return h;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
875
1899
|
function renderSystem(data) {
|
|
876
1900
|
const el = $('#view-system');
|
|
877
1901
|
if (!data) { el.innerHTML = '<div class="empty">加载中…</div>'; return; }
|
|
@@ -913,45 +1937,26 @@ function renderSystem(data) {
|
|
|
913
1937
|
// ③ 健康快照
|
|
914
1938
|
if (chk) {
|
|
915
1939
|
html += '<div class="sys-health">';
|
|
916
|
-
//
|
|
917
|
-
html += '<div style="
|
|
918
|
-
html +=
|
|
919
|
-
for (const ch of (chk.channels || [])) {
|
|
920
|
-
for (const inst of (ch.instances || [])) {
|
|
921
|
-
const dot = inst.connected ? 'on' : 'idle';
|
|
922
|
-
html += `<div><span class="dot ${dot}"></span>${esc(ch.type)}${inst.name !== ch.type ? ' ' + esc(inst.name) : ''}</div>`;
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
html += '</div>';
|
|
926
|
-
html += `<div class="cache-card"><div class="card-label">队列</div><div>待处理 ${chk.queue?.pending ?? 0}</div><div>处理中 ${chk.queue?.processing ?? 0}</div></div>`;
|
|
927
|
-
html += '</div>';
|
|
928
|
-
// 近 1 小时
|
|
1940
|
+
// 队列 + 近 1 小时(数字卡片同一行)
|
|
1941
|
+
html += '<div class="cache-cards" style="margin-bottom:8px">';
|
|
1942
|
+
html += `<div class="cache-card"><div class="card-label">队列</div><div class="card-val">${chk.queue?.pending ?? 0} 待 · ${chk.queue?.processing ?? 0} 处理中</div></div>`;
|
|
929
1943
|
const h = chk.lastHour;
|
|
930
1944
|
if (h) {
|
|
931
1945
|
const errDetail = h.errors > 0 ? ` (${Object.entries(h.errorsByType || {}).map(([t, c]) => `${t}:${c}`).join(', ')})` : '';
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
`<div>中断 ${h.interrupts}${h.completed > 0 ? ' · 平均 ' + (h.avgResponseMs / 1000).toFixed(1) + 's' : ''}</div>` +
|
|
935
|
-
`</div>`;
|
|
1946
|
+
const avg = h.completed > 0 ? ` · 均 ${(h.avgResponseMs / 1000).toFixed(1)}s` : '';
|
|
1947
|
+
html += `<div class="cache-card"><div class="card-label">近 1 小时</div><div class="card-val">收 ${h.received} · 完 ${h.completed} · 错 ${h.errors}${errDetail} · 断 ${h.interrupts}${avg}</div></div>`;
|
|
936
1948
|
}
|
|
937
|
-
|
|
1949
|
+
html += '</div>';
|
|
1950
|
+
// 每个 EvolAgent 一张卡片:后端 + 渠道健康 + 负载
|
|
938
1951
|
if (chk.evolagents?.length) {
|
|
939
|
-
html += '<div class="
|
|
940
|
-
for (const ag of chk.evolagents)
|
|
941
|
-
const dot = ag.status === 'running' ? 'on' : ag.status === 'disabled' ? 'idle' : 'off';
|
|
942
|
-
const tasks = ag.activeTasks > 0 ? ` · ${ag.activeTasks} 任务` : '';
|
|
943
|
-
const err = ag.error ? ` <span style="color:var(--red)">${esc(ag.error.slice(0, 60))}</span>` : '';
|
|
944
|
-
html += `<div><span class="dot ${dot}"></span>${esc(ag.name)} ${esc(ag.status)}${tasks}${err}</div>`;
|
|
945
|
-
}
|
|
1952
|
+
html += '<div class="agent-health-grid">';
|
|
1953
|
+
for (const ag of chk.evolagents) html += agentHealthCard(ag);
|
|
946
1954
|
html += '</div>';
|
|
947
1955
|
}
|
|
948
|
-
//
|
|
949
|
-
if (chk.
|
|
950
|
-
html += '<div class="cache-card"><div class="card-label"
|
|
951
|
-
for (const
|
|
952
|
-
const dot = ba.healthy ? (ba.activeStreams > 0 ? 'on' : 'idle') : 'off';
|
|
953
|
-
html += `<div><span class="dot ${dot}"></span>${esc(ba.name)} · 流 ${ba.activeStreams}</div>`;
|
|
954
|
-
}
|
|
1956
|
+
// 未归属任何 EvolAgent 的渠道(系统级 / DefaultAgent)
|
|
1957
|
+
if (chk.unownedChannels?.length) {
|
|
1958
|
+
html += '<div class="cache-card" style="margin-top:8px"><div class="card-label">未归属渠道</div>';
|
|
1959
|
+
for (const c of chk.unownedChannels) html += channelHealthRow(c);
|
|
955
1960
|
html += '</div>';
|
|
956
1961
|
}
|
|
957
1962
|
html += '</div>';
|
|
@@ -988,6 +1993,276 @@ function bindSystemEvents(el, data) {
|
|
|
988
1993
|
});
|
|
989
1994
|
}
|
|
990
1995
|
|
|
1996
|
+
// ── Gateway 视图(网关 = baseagent 后端接入配置) ──
|
|
1997
|
+
// 数据来自 daemon menu.query name=gateway(apiKey 已掩码)。
|
|
1998
|
+
// 写操作走 menuSend({name:'gateway', ...}):update/test/delete。
|
|
1999
|
+
|
|
2000
|
+
// 各 baseagent 类型的可编辑字段定义(驱动编辑表单与展示)
|
|
2001
|
+
const GATEWAY_FIELDS = {
|
|
2002
|
+
claude: [
|
|
2003
|
+
{ key: 'baseUrl', label: 'Base URL', placeholder: 'https://gateway.example.com(留空=官方)' },
|
|
2004
|
+
{ key: 'model', label: '默认模型', placeholder: 'opus / sonnet / claude-...' },
|
|
2005
|
+
{ key: 'effort', label: 'Effort', placeholder: 'low / medium / high / xhigh / max' },
|
|
2006
|
+
],
|
|
2007
|
+
codex: [
|
|
2008
|
+
{ key: 'baseUrl', label: 'Base URL', placeholder: 'https://gateway.example.com(留空=官方)' },
|
|
2009
|
+
{ key: 'model', label: '默认模型', placeholder: 'gpt-5.2-codex / ...' },
|
|
2010
|
+
{ key: 'effort', label: 'Effort', placeholder: 'low / medium / high' },
|
|
2011
|
+
{ key: 'reasoning', label: 'Reasoning', placeholder: '(可选)' },
|
|
2012
|
+
],
|
|
2013
|
+
gemini: [
|
|
2014
|
+
{ key: 'model', label: '默认模型', placeholder: 'gemini-2.5-flash / ...' },
|
|
2015
|
+
{ key: 'mode', label: '模式', placeholder: 'cli / sdk' },
|
|
2016
|
+
{ key: 'cliPath', label: 'CLI 路径', placeholder: 'gemini' },
|
|
2017
|
+
{ key: 'project', label: 'GCP Project', placeholder: '(Vertex 用)' },
|
|
2018
|
+
{ key: 'location', label: 'Location', placeholder: 'us-central1' },
|
|
2019
|
+
],
|
|
2020
|
+
};
|
|
2021
|
+
|
|
2022
|
+
const GATEWAY_TYPE_ICON = { claude: '🟣', codex: '🟢', gemini: '🔵' };
|
|
2023
|
+
|
|
2024
|
+
// 标记每条网关的运行时测试结果:`${scope}#${type}` → { ok, latency, modelCount, error }
|
|
2025
|
+
const _gwTest = new Map();
|
|
2026
|
+
let _gwEditing = null; // 当前编辑中的网关 key(`${scope}#${type}`)或 'new'
|
|
2027
|
+
|
|
2028
|
+
function gwKey(scope, type) { return scope + '#' + type; }
|
|
2029
|
+
|
|
2030
|
+
function renderGateway(data) {
|
|
2031
|
+
const el = $('#view-gateway');
|
|
2032
|
+
|
|
2033
|
+
if (!data) { el.innerHTML = '<div class="empty">加载中…</div>'; return; }
|
|
2034
|
+
if (data.error) {
|
|
2035
|
+
el.innerHTML = `<div class="empty">⚠ ${esc(data.error)}</div>`;
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
const gateways = data.gateways || [];
|
|
2039
|
+
const scopes = data.scopes || ['defaults'];
|
|
2040
|
+
|
|
2041
|
+
// 按 scope 分组
|
|
2042
|
+
const byScope = new Map();
|
|
2043
|
+
for (const s of scopes) byScope.set(s, []);
|
|
2044
|
+
for (const g of gateways) {
|
|
2045
|
+
if (!byScope.has(g.scope)) byScope.set(g.scope, []);
|
|
2046
|
+
byScope.get(g.scope).push(g);
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
let html = '<div class="gw-wrap">';
|
|
2050
|
+
|
|
2051
|
+
html += '<div class="gw-intro">网关 = 各 AI 后端(baseagent)的接入配置。Base URL 即网关地址,留空走官方端点。' +
|
|
2052
|
+
'此处为只读展示,配置请通过配置文件(defaults.json / agents/<aid>/config.json)管理。</div>';
|
|
2053
|
+
|
|
2054
|
+
// 只展示全局默认配置块(用于编辑)
|
|
2055
|
+
for (const [scope, list] of byScope) {
|
|
2056
|
+
if (scope !== 'defaults') continue; // 跳过 per-agent 原始配置块(已在下方 effective 展示)
|
|
2057
|
+
const scopeLabel = '🌐 全局默认 (defaults)';
|
|
2058
|
+
html += `<div class="gw-scope">`;
|
|
2059
|
+
html += `<div class="gw-scope-head"><span class="gw-scope-title">${scopeLabel}</span></div>`;
|
|
2060
|
+
html += '<div class="gw-cards">';
|
|
2061
|
+
if (!list.length) {
|
|
2062
|
+
html += '<div class="empty" style="padding:12px">该作用域暂无网关配置</div>';
|
|
2063
|
+
} else {
|
|
2064
|
+
for (const g of list) html += gatewayCard(g);
|
|
2065
|
+
}
|
|
2066
|
+
html += '</div></div>';
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
// ── Agent 使用配置(effective):紧凑表格 + 编辑按钮 ──
|
|
2070
|
+
const effective = data.effective || [];
|
|
2071
|
+
if (effective.length > 0) {
|
|
2072
|
+
html += '<div class="gw-effective-section">';
|
|
2073
|
+
html += '<div class="gw-scope-head"><span class="gw-scope-title">📋 Agent 网关配置</span></div>';
|
|
2074
|
+
html += '<table class="gw-eff-table"><thead><tr>' +
|
|
2075
|
+
'<th>Agent</th><th>Base Agent</th><th>Base URL</th><th>模型</th><th>API Key</th><th>Effort</th><th>来源</th>' +
|
|
2076
|
+
'</tr></thead><tbody>';
|
|
2077
|
+
for (const eff of effective) {
|
|
2078
|
+
const f = eff.fields || {};
|
|
2079
|
+
const blockSrc = eff.blockSource || 'defaults';
|
|
2080
|
+
const srcCls = blockSrc === 'agent' ? 'gw-src-agent' : 'gw-src-defaults';
|
|
2081
|
+
const srcLabel = blockSrc === 'agent' ? '⚡ agent' : '🔗 默认';
|
|
2082
|
+
const baseUrlVal = f.baseUrl?.value || '';
|
|
2083
|
+
const modelVal = f.model?.value || '';
|
|
2084
|
+
const keyVal = f.apiKey?.value || '';
|
|
2085
|
+
const effortVal = f.effort?.value || '';
|
|
2086
|
+
|
|
2087
|
+
html += `<tr class="gw-eff-tr${blockSrc === 'defaults' ? ' gw-eff-tr-inherited' : ''}">` +
|
|
2088
|
+
`<td class="gw-eff-td-aid" title="${esc(eff.aid)}">${esc(shortAid(eff.aid))}</td>` +
|
|
2089
|
+
`<td>${GATEWAY_TYPE_ICON[eff.type] || ''} ${esc(eff.type)}</td>` +
|
|
2090
|
+
`<td class="gw-eff-td-url" title="${esc(baseUrlVal)}">${baseUrlVal ? esc(baseUrlVal) : '<span class="gw-dim">官方</span>'}</td>` +
|
|
2091
|
+
`<td>${modelVal ? esc(modelVal) : '<span class="gw-dim">—</span>'}</td>` +
|
|
2092
|
+
`<td>${keyVal ? esc(keyVal) : '<span class="gw-dim">—</span>'}</td>` +
|
|
2093
|
+
`<td>${effortVal ? esc(effortVal) : '<span class="gw-dim">—</span>'}</td>` +
|
|
2094
|
+
`<td><span class="gw-eff-src-tag ${srcCls}">${srcLabel}</span></td>` +
|
|
2095
|
+
`</tr>`;
|
|
2096
|
+
}
|
|
2097
|
+
html += '</tbody></table></div>';
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
html += '</div>';
|
|
2101
|
+
el.innerHTML = html;
|
|
2102
|
+
bindGatewayEvents(el, data);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
function gatewayCard(g) {
|
|
2106
|
+
const key = gwKey(g.scope, g.type);
|
|
2107
|
+
const icon = GATEWAY_TYPE_ICON[g.type] || '⚙';
|
|
2108
|
+
const test = _gwTest.get(key);
|
|
2109
|
+
|
|
2110
|
+
// 连通性测试状态点
|
|
2111
|
+
let dot = '<span class="gw-dot gw-dot-unknown" title="未测试"></span>';
|
|
2112
|
+
if (test) {
|
|
2113
|
+
if (test.ok) dot = `<span class="gw-dot gw-dot-ok" title="${test.latency}ms · ${test.modelCount} 模型"></span>`;
|
|
2114
|
+
else dot = `<span class="gw-dot gw-dot-err" title="${esc(test.error || '失败')}"></span>`;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// API Key 展示
|
|
2118
|
+
let keyHtml;
|
|
2119
|
+
if (!g.apiKeyMask) keyHtml = '<span class="gw-dim">未配置</span>';
|
|
2120
|
+
else if (g.apiKeyIsEnvRef) keyHtml = `<code class="gw-env">${esc(g.apiKeyMask)}</code>`;
|
|
2121
|
+
else keyHtml = '<span class="gw-dim" title="明文密钥已隐藏,建议改用 $ENV 引用">*** (明文)</span>';
|
|
2122
|
+
|
|
2123
|
+
const rows = [];
|
|
2124
|
+
rows.push(['Base URL', g.baseUrl ? esc(g.baseUrl) : '<span class="gw-dim">官方端点</span>']);
|
|
2125
|
+
rows.push(['默认模型', g.model ? esc(g.model) : '<span class="gw-dim">—</span>']);
|
|
2126
|
+
rows.push(['API Key', keyHtml]);
|
|
2127
|
+
if (g.effort) rows.push(['Effort', esc(g.effort)]);
|
|
2128
|
+
if (g.reasoning) rows.push(['Reasoning', esc(g.reasoning)]);
|
|
2129
|
+
if (g.mode) rows.push(['模式', esc(g.mode)]);
|
|
2130
|
+
if (g.cliPath) rows.push(['CLI 路径', esc(g.cliPath)]);
|
|
2131
|
+
if (g.project) rows.push(['Project', esc(g.project)]);
|
|
2132
|
+
if (g.location) rows.push(['Location', esc(g.location)]);
|
|
2133
|
+
|
|
2134
|
+
let html = `<div class="gw-card" data-key="${esc(key)}">`;
|
|
2135
|
+
html += `<div class="gw-card-head">${dot}<span class="gw-card-icon">${icon}</span>` +
|
|
2136
|
+
`<span class="gw-card-title">${esc(g.name)}</span>` +
|
|
2137
|
+
`<span class="gw-card-type">${esc(g.type)}</span></div>`;
|
|
2138
|
+
html += '<div class="gw-card-body">';
|
|
2139
|
+
for (const [label, val] of rows) {
|
|
2140
|
+
html += `<div class="gw-row"><span class="gw-row-label">${esc(label)}</span><span class="gw-row-val">${val}</span></div>`;
|
|
2141
|
+
}
|
|
2142
|
+
html += '</div>';
|
|
2143
|
+
// 卡片操作按钮已移除(只读模式)
|
|
2144
|
+
html += '</div>';
|
|
2145
|
+
return html;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// ── Gateway 编辑/操作弹窗函数(已禁用,网关配置现为只读展示)──
|
|
2149
|
+
// 如需恢复网关编辑功能,取消下方注释即可。
|
|
2150
|
+
|
|
2151
|
+
/*
|
|
2152
|
+
// 编辑/新增弹窗
|
|
2153
|
+
function openGatewayEditor(scope, type, existing, scopes) {
|
|
2154
|
+
const isNew = !existing;
|
|
2155
|
+
const fields = GATEWAY_FIELDS[type] || GATEWAY_FIELDS.claude;
|
|
2156
|
+
|
|
2157
|
+
let html = '<div class="gw-modal-backdrop" id="gw-modal-backdrop"><div class="gw-modal">';
|
|
2158
|
+
html += `<div class="gw-modal-head">${isNew ? '添加网关' : '编辑网关'}</div>`;
|
|
2159
|
+
html += '<div class="gw-modal-body">';
|
|
2160
|
+
|
|
2161
|
+
// scope 选择(新增时可选,编辑时锁定)
|
|
2162
|
+
html += '<label class="gw-field"><span class="gw-field-label">作用域</span>';
|
|
2163
|
+
if (isNew) {
|
|
2164
|
+
html += '<select id="gw-f-scope">';
|
|
2165
|
+
for (const s of (scopes || ['defaults'])) {
|
|
2166
|
+
const lbl = s === 'defaults' ? '全局默认' : shortAid(s);
|
|
2167
|
+
html += `<option value="${esc(s)}"${s === scope ? ' selected' : ''}>${esc(lbl)}</option>`;
|
|
2168
|
+
}
|
|
2169
|
+
html += '</select>';
|
|
2170
|
+
} else {
|
|
2171
|
+
html += `<input id="gw-f-scope" type="text" value="${esc(scope)}" disabled>`;
|
|
2172
|
+
}
|
|
2173
|
+
html += '</label>';
|
|
2174
|
+
|
|
2175
|
+
// type 选择(新增时可选,编辑时锁定)
|
|
2176
|
+
html += '<label class="gw-field"><span class="gw-field-label">后端类型</span>';
|
|
2177
|
+
if (isNew) {
|
|
2178
|
+
html += '<select id="gw-f-type">';
|
|
2179
|
+
for (const t of ['claude', 'codex', 'gemini']) {
|
|
2180
|
+
html += `<option value="${t}"${t === type ? ' selected' : ''}>${t}</option>`;
|
|
2181
|
+
}
|
|
2182
|
+
html += '</select>';
|
|
2183
|
+
} else {
|
|
2184
|
+
html += `<input id="gw-f-type" type="text" value="${esc(type)}" disabled>`;
|
|
2185
|
+
}
|
|
2186
|
+
html += '</label>';
|
|
2187
|
+
|
|
2188
|
+
// 动态字段
|
|
2189
|
+
html += '<div id="gw-dyn-fields">';
|
|
2190
|
+
for (const f of fields) {
|
|
2191
|
+
const val = existing ? (existing[f.key] || '') : '';
|
|
2192
|
+
html += `<label class="gw-field"><span class="gw-field-label">${esc(f.label)}</span>` +
|
|
2193
|
+
`<input class="gw-dyn" data-key="${esc(f.key)}" type="text" value="${esc(val)}" placeholder="${esc(f.placeholder || '')}"></label>`;
|
|
2194
|
+
}
|
|
2195
|
+
html += '</div>';
|
|
2196
|
+
|
|
2197
|
+
// API Key(仅 $ENV 引用)
|
|
2198
|
+
const curKey = existing && existing.apiKeyIsEnvRef ? existing.apiKeyMask : '';
|
|
2199
|
+
html += '<label class="gw-field"><span class="gw-field-label">API Key 引用</span>' +
|
|
2200
|
+
`<input id="gw-f-apikey" type="text" value="${esc(curKey)}" placeholder="$ENV:ANTHROPIC_AUTH_TOKEN(留空不改)"></label>`;
|
|
2201
|
+
html += '<div class="gw-hint">仅支持环境变量引用,格式 <code>$ENV:变量名</code>。明文密钥请写入环境变量后引用。</div>';
|
|
2202
|
+
|
|
2203
|
+
html += '</div>'; // body
|
|
2204
|
+
html += '<div class="gw-modal-actions">' +
|
|
2205
|
+
'<button class="ctrl-btn" id="gw-cancel">取消</button> ' +
|
|
2206
|
+
'<button class="ctrl-btn primary" id="gw-save">保存</button>' +
|
|
2207
|
+
'</div>';
|
|
2208
|
+
html += '</div></div>';
|
|
2209
|
+
|
|
2210
|
+
const wrap = document.createElement('div');
|
|
2211
|
+
wrap.innerHTML = html;
|
|
2212
|
+
document.body.appendChild(wrap.firstChild);
|
|
2213
|
+
|
|
2214
|
+
const backdrop = $('#gw-modal-backdrop');
|
|
2215
|
+
const close = () => { try { backdrop.remove(); } catch {} };
|
|
2216
|
+
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(); });
|
|
2217
|
+
$('#gw-cancel').onclick = close;
|
|
2218
|
+
|
|
2219
|
+
// 新增时切换 type 重建动态字段
|
|
2220
|
+
if (isNew) {
|
|
2221
|
+
$('#gw-f-type').onchange = (e) => {
|
|
2222
|
+
const newType = e.target.value;
|
|
2223
|
+
const dyn = $('#gw-dyn-fields');
|
|
2224
|
+
const fs2 = GATEWAY_FIELDS[newType] || GATEWAY_FIELDS.claude;
|
|
2225
|
+
dyn.innerHTML = fs2.map(f =>
|
|
2226
|
+
`<label class="gw-field"><span class="gw-field-label">${esc(f.label)}</span>` +
|
|
2227
|
+
`<input class="gw-dyn" data-key="${esc(f.key)}" type="text" value="" placeholder="${esc(f.placeholder || '')}"></label>`
|
|
2228
|
+
).join('');
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
$('#gw-save').onclick = async () => {
|
|
2233
|
+
const fScope = $('#gw-f-scope').value;
|
|
2234
|
+
const fType = $('#gw-f-type').value;
|
|
2235
|
+
const patch = {};
|
|
2236
|
+
document.querySelectorAll('#gw-dyn-fields .gw-dyn').forEach(inp => {
|
|
2237
|
+
patch[inp.dataset.key] = inp.value.trim();
|
|
2238
|
+
});
|
|
2239
|
+
const apiKey = $('#gw-f-apikey').value.trim();
|
|
2240
|
+
if (apiKey) {
|
|
2241
|
+
if (!apiKey.startsWith('$ENV:')) { toast('API Key 必须是 $ENV:变量名 引用', true); return; }
|
|
2242
|
+
patch.apiKey = apiKey;
|
|
2243
|
+
}
|
|
2244
|
+
try {
|
|
2245
|
+
const r = mResp(await menuSend({
|
|
2246
|
+
type: 'menu.update', name: 'gateway',
|
|
2247
|
+
value: JSON.stringify({ scope: fScope, type: fType, patch }),
|
|
2248
|
+
}));
|
|
2249
|
+
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
2250
|
+
toast(r.data && r.data.reloaded ? '已保存并重载' : '已保存(未重载)');
|
|
2251
|
+
close();
|
|
2252
|
+
subscribe('gateway', {}); // 刷新
|
|
2253
|
+
} catch (e) { toast(e.message, true); }
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
*/
|
|
2257
|
+
|
|
2258
|
+
// openAgentSelectModal、showGatewayConfigModal、showPriceEditModal 等函数已移除(只读模式)
|
|
2259
|
+
// 如需恢复,取消上方注释块即可。
|
|
2260
|
+
|
|
2261
|
+
function bindGatewayEvents(el, data) {
|
|
2262
|
+
// 已移除所有编辑操作事件绑定(网关配置现为只读展示)
|
|
2263
|
+
void el; void data;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
991
2266
|
// ── Triggers 视图 ──
|
|
992
2267
|
function trigStatusBadge(status) {
|
|
993
2268
|
const map = {
|
|
@@ -1080,6 +2355,8 @@ function renderTriggers(data) {
|
|
|
1080
2355
|
|
|
1081
2356
|
function startApp() {
|
|
1082
2357
|
initTabs();
|
|
2358
|
+
// 恢复保存的 tab 视图
|
|
2359
|
+
switchView(currentView);
|
|
1083
2360
|
connect();
|
|
1084
2361
|
$('#logout-btn').onclick = () => {
|
|
1085
2362
|
localStorage.removeItem(TOKEN_KEY);
|
|
@@ -1102,154 +2379,488 @@ function initTheme() {
|
|
|
1102
2379
|
btn.textContent = next === 'dark' ? '☀️' : '🌙';
|
|
1103
2380
|
if (_hourlyChart) { _hourlyChart.dispose(); _hourlyChart = null; }
|
|
1104
2381
|
if (_modelChart) { _modelChart.dispose(); _modelChart = null; }
|
|
1105
|
-
|
|
2382
|
+
['_monCpu', '_monMem', '_monMsg', '_monErr'].forEach(function (k) {
|
|
2383
|
+
if (window[k]) { window[k].dispose(); window[k] = null; }
|
|
2384
|
+
});
|
|
2385
|
+
if (currentView === 'monitor') renderMonitor(state.monitor);
|
|
1106
2386
|
};
|
|
1107
2387
|
}
|
|
1108
2388
|
}
|
|
1109
2389
|
|
|
1110
|
-
// ── Usage
|
|
1111
|
-
let _hourlyChart = null;
|
|
1112
|
-
let _modelChart = null;
|
|
1113
|
-
|
|
2390
|
+
// ── Usage 相关函数 ──
|
|
1114
2391
|
function fmtTokens(n) {
|
|
1115
2392
|
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
1116
2393
|
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
|
|
1117
2394
|
return String(n);
|
|
1118
2395
|
}
|
|
1119
2396
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
data: data.top_models.map(function(m) { return { name: m.model.split('/').pop(), value: m.total_tokens }; }),
|
|
1173
|
-
}]
|
|
1174
|
-
});
|
|
2397
|
+
// ── Usage Overview(总览,支持日期范围筛选)──
|
|
2398
|
+
let _ovCurrentRange = 'today'; // 当前选择的范围
|
|
2399
|
+
|
|
2400
|
+
async function loadUsageOverview(rangeType, customFrom, customTo) {
|
|
2401
|
+
rangeType = rangeType || _ovCurrentRange;
|
|
2402
|
+
_ovCurrentRange = rangeType;
|
|
2403
|
+
|
|
2404
|
+
// 计算日期范围
|
|
2405
|
+
let fromTs, toTs;
|
|
2406
|
+
const now = new Date();
|
|
2407
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
2408
|
+
const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime();
|
|
2409
|
+
|
|
2410
|
+
switch (rangeType) {
|
|
2411
|
+
case 'today':
|
|
2412
|
+
fromTs = todayStart;
|
|
2413
|
+
toTs = todayEnd;
|
|
2414
|
+
break;
|
|
2415
|
+
case 'week': // 本周(周一到今天)
|
|
2416
|
+
const dayOfWeek = now.getDay() || 7; // 周日=7
|
|
2417
|
+
const weekStart = new Date(todayStart - (dayOfWeek - 1) * 86400000);
|
|
2418
|
+
fromTs = weekStart.getTime();
|
|
2419
|
+
toTs = todayEnd;
|
|
2420
|
+
break;
|
|
2421
|
+
case 'lastWeek': // 上周(上周一到上周日)
|
|
2422
|
+
const lastWeekEnd = new Date(todayStart - now.getDay() * 86400000);
|
|
2423
|
+
const lastWeekStart = new Date(lastWeekEnd.getTime() - 6 * 86400000);
|
|
2424
|
+
fromTs = lastWeekStart.getTime();
|
|
2425
|
+
toTs = new Date(lastWeekEnd.getTime() + 86400000 - 1).getTime();
|
|
2426
|
+
break;
|
|
2427
|
+
case 'month': // 本月
|
|
2428
|
+
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
2429
|
+
fromTs = monthStart.getTime();
|
|
2430
|
+
toTs = todayEnd;
|
|
2431
|
+
break;
|
|
2432
|
+
case 'last30': // 最近30天
|
|
2433
|
+
fromTs = todayStart - 29 * 86400000;
|
|
2434
|
+
toTs = todayEnd;
|
|
2435
|
+
break;
|
|
2436
|
+
case 'custom': // 自定义
|
|
2437
|
+
if (customFrom && customTo) {
|
|
2438
|
+
// 支持 datetime-local 输入,直接解析时间戳
|
|
2439
|
+
fromTs = new Date(customFrom).getTime();
|
|
2440
|
+
toTs = new Date(customTo).getTime();
|
|
2441
|
+
} else {
|
|
2442
|
+
fromTs = todayStart;
|
|
2443
|
+
toTs = todayEnd;
|
|
2444
|
+
}
|
|
2445
|
+
break;
|
|
2446
|
+
default:
|
|
2447
|
+
fromTs = null;
|
|
2448
|
+
toTs = null;
|
|
1175
2449
|
}
|
|
1176
2450
|
|
|
1177
|
-
//
|
|
1178
|
-
|
|
1179
|
-
if (peersEl && data.top_peers && data.top_peers.length) {
|
|
1180
|
-
peersEl.innerHTML =
|
|
1181
|
-
'<thead><tr><th>#</th><th>Peer</th><th>Tokens</th><th>Calls</th></tr></thead>' +
|
|
1182
|
-
'<tbody>' + data.top_peers.map(function(p, i) {
|
|
1183
|
-
return '<tr><td>' + (i + 1) + '</td><td>' + p.peer_key + '</td><td>' + fmtTokens(p.total_tokens) + '</td><td>' + p.call_count + '</td></tr>';
|
|
1184
|
-
}).join('') + '</tbody>';
|
|
1185
|
-
}
|
|
2451
|
+
// 保存当前时间范围到全局变量,供详细统计使用
|
|
2452
|
+
window._currentOverviewTimeRange = { fromTs, toTs, rangeType, customFrom, customTo };
|
|
1186
2453
|
|
|
1187
|
-
//
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
2454
|
+
// 时间范围变化后,刷新明细的模型列表与查询结果(重置到第一页)
|
|
2455
|
+
if ($('#detail-model')) {
|
|
2456
|
+
const detailPageEl = $('#detail-page');
|
|
2457
|
+
if (detailPageEl) detailPageEl.value = '1';
|
|
2458
|
+
loadDetailModelList();
|
|
2459
|
+
queryDetailUsage();
|
|
1192
2460
|
}
|
|
1193
|
-
}
|
|
1194
2461
|
|
|
1195
|
-
// ── Usage Overview(全时段总览)──
|
|
1196
|
-
async function loadUsageOverview() {
|
|
1197
2462
|
let data;
|
|
1198
2463
|
try {
|
|
1199
|
-
const
|
|
2464
|
+
const params = new URLSearchParams();
|
|
2465
|
+
if (fromTs) params.set('from', String(fromTs));
|
|
2466
|
+
if (toTs) params.set('to', String(toTs));
|
|
2467
|
+
|
|
2468
|
+
const resp = await fetch(apiUrl('api/stats/overview?' + params.toString()), {
|
|
1200
2469
|
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
1201
2470
|
});
|
|
1202
2471
|
data = resp.ok ? await resp.json() : null;
|
|
1203
2472
|
} catch { data = null; }
|
|
1204
2473
|
|
|
1205
2474
|
const ts = (data && data.token_stats && data.token_stats.all_time) ? data.token_stats.all_time
|
|
1206
|
-
: { input_tokens: 0, output_tokens: 0, cache_creation_tokens: 0, cache_read_tokens: 0, call_count: 0, cost_usd: 0, cost_cny: 0 };
|
|
2475
|
+
: { input_tokens: 0, output_tokens: 0, cache_creation_tokens: 0, cache_read_tokens: 0, call_count: 0, cost_official_usd: 0, cost_official_cny: 0, cost_usd: 0, cost_cny: 0 };
|
|
1207
2476
|
const sessionCount = (data && data.session_count) || 0;
|
|
1208
2477
|
const msgIn = (data && data.msg_in) || 0;
|
|
1209
2478
|
const msgOut = (data && data.msg_out) || 0;
|
|
1210
|
-
|
|
1211
|
-
const
|
|
2479
|
+
// 新的缓存命中率计算规则:缓存命中 / (缓存命中 + 缓存写入 + 输入token + 输出token)
|
|
2480
|
+
const totalTokens = ts.cache_read_tokens + ts.cache_creation_tokens + ts.input_tokens + ts.output_tokens;
|
|
2481
|
+
const hitRate = totalTokens > 0 ? (ts.cache_read_tokens / totalTokens) * 100 : 0;
|
|
1212
2482
|
|
|
1213
2483
|
const cardsEl = $('#ov-cards');
|
|
1214
2484
|
if (cardsEl) {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
2485
|
+
// 合并相关信息到大卡片中
|
|
2486
|
+
const sessionCard = makeMultiValueCard([
|
|
2487
|
+
{ label: t('usage.card.sessionCount'), value: sessionCount },
|
|
2488
|
+
{ label: t('usage.card.msgIn'), value: msgIn },
|
|
2489
|
+
{ label: t('usage.card.msgOut'), value: msgOut }
|
|
2490
|
+
], t('usage.card.sessionInfo'), 'session-group');
|
|
2491
|
+
|
|
2492
|
+
const usageCard = makeMultiValueCard([
|
|
2493
|
+
{ label: t('usage.card.modelCalls'), value: ts.call_count },
|
|
2494
|
+
{ label: t('usage.card.inputTokens'), value: fmtTokens(ts.input_tokens) },
|
|
2495
|
+
{ label: t('usage.card.outputTokens'), value: fmtTokens(ts.output_tokens) },
|
|
2496
|
+
{ label: t('usage.card.cacheCreation'), value: fmtTokens(ts.cache_creation_tokens) },
|
|
2497
|
+
{ label: t('usage.card.cacheHitTokens'), value: fmtTokens(ts.cache_read_tokens) },
|
|
2498
|
+
{ label: t('usage.card.cacheHitRate'), value: hitRate.toFixed(1) + '%' }
|
|
2499
|
+
], t('usage.card.usageInfo'), 'usage-group');
|
|
2500
|
+
|
|
2501
|
+
const costCard = makeMultiValueCard([
|
|
2502
|
+
{ label: t('usage.card.costOfficial'), value: fmtCost(ts.cost_official_usd, ts.cost_official_cny) },
|
|
2503
|
+
{ label: t('usage.card.costGateway'), value: fmtCost(ts.cost_usd, ts.cost_cny) }
|
|
2504
|
+
], t('usage.card.costInfo'), 'cost-group');
|
|
2505
|
+
|
|
2506
|
+
cardsEl.innerHTML = sessionCard + usageCard + costCard;
|
|
1227
2507
|
}
|
|
1228
2508
|
|
|
1229
2509
|
const agentTbl = $('#ov-agent-table');
|
|
1230
2510
|
const agents = (data && data.token_stats && data.token_stats.by_agent) || [];
|
|
1231
2511
|
if (agentTbl) {
|
|
1232
2512
|
if (!agents.length) {
|
|
1233
|
-
agentTbl.innerHTML = '<tbody><tr><td
|
|
2513
|
+
agentTbl.innerHTML = '<tbody><tr><td>' + t('usage.overview.noData') + '</td></tr></tbody>';
|
|
1234
2514
|
} else {
|
|
1235
2515
|
agentTbl.innerHTML =
|
|
1236
|
-
'<thead><tr><th>
|
|
2516
|
+
'<thead><tr><th>' + t('usage.overview.th.agent') + '</th><th>' + t('usage.overview.th.calls') + '</th><th>' + t('usage.overview.th.input') + '</th><th>' + t('usage.overview.th.output') + '</th><th>' + t('usage.overview.th.cacheCreation') + '</th><th>' + t('usage.overview.th.cacheHit') + '</th><th>' + t('usage.overview.th.cacheHitRate') + '</th><th>' + t('usage.overview.th.costOfficial') + '</th><th>' + t('usage.overview.th.costGateway') + '</th></tr></thead>' +
|
|
1237
2517
|
'<tbody>' + agents.map(function(a) {
|
|
1238
|
-
var name = a.agent_aid ? a.agent_aid.split('.')[0] : '(unknown)';
|
|
2518
|
+
var name = a.agent_name || (a.agent_aid ? a.agent_aid.split('.')[0] : '(unknown)');
|
|
2519
|
+
// 计算缓存命中率
|
|
2520
|
+
var totalTokens = (a.cache_read_tokens || 0) + (a.cache_creation_tokens || 0) + (a.input_tokens || 0) + (a.output_tokens || 0);
|
|
2521
|
+
var hitRate = totalTokens > 0 ? ((a.cache_read_tokens || 0) / totalTokens * 100).toFixed(1) : '0.0';
|
|
2522
|
+
|
|
1239
2523
|
return '<tr><td title="' + esc(a.agent_aid) + '">' + esc(name) + '</td>' +
|
|
1240
2524
|
'<td>' + a.call_count + '</td>' +
|
|
1241
2525
|
'<td>' + fmtTokens(a.input_tokens) + '</td>' +
|
|
1242
2526
|
'<td>' + fmtTokens(a.output_tokens) + '</td>' +
|
|
1243
2527
|
'<td>' + fmtTokens(a.cache_creation_tokens) + '</td>' +
|
|
1244
2528
|
'<td>' + fmtTokens(a.cache_read_tokens) + '</td>' +
|
|
1245
|
-
'<td>' +
|
|
2529
|
+
'<td>' + hitRate + '%</td>' +
|
|
2530
|
+
'<td>' + fmtCostSplit(a.cost_official_usd, a.cost_official_cny) + '</td>' +
|
|
2531
|
+
'<td>' + fmtCostSplit(a.cost_usd, a.cost_cny) + '</td></tr>';
|
|
1246
2532
|
}).join('') + '</tbody>';
|
|
1247
2533
|
}
|
|
1248
2534
|
}
|
|
2535
|
+
|
|
2536
|
+
// 保存总览数据供详细统计使用
|
|
2537
|
+
window._currentOverviewData = { ts, sessionCount, msgIn, msgOut, hitRate };
|
|
1249
2538
|
}
|
|
1250
2539
|
|
|
1251
|
-
function
|
|
1252
|
-
|
|
2540
|
+
function initOverviewFilters() {
|
|
2541
|
+
// 范围按钮切换
|
|
2542
|
+
document.querySelectorAll('.ov-range-btn').forEach(function(btn) {
|
|
2543
|
+
btn.addEventListener('click', function() {
|
|
2544
|
+
document.querySelectorAll('.ov-range-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
2545
|
+
btn.classList.add('active');
|
|
2546
|
+
|
|
2547
|
+
const range = btn.getAttribute('data-range');
|
|
2548
|
+
const customDateEl = $('#ov-custom-date');
|
|
2549
|
+
|
|
2550
|
+
if (range === 'custom') {
|
|
2551
|
+
if (customDateEl) customDateEl.style.display = 'flex';
|
|
2552
|
+
} else {
|
|
2553
|
+
if (customDateEl) customDateEl.style.display = 'none';
|
|
2554
|
+
loadUsageOverview(range);
|
|
2555
|
+
}
|
|
2556
|
+
});
|
|
2557
|
+
});
|
|
2558
|
+
|
|
2559
|
+
// 自定义日期查询按钮
|
|
2560
|
+
const queryBtn = $('#ov-query-btn');
|
|
2561
|
+
if (queryBtn) {
|
|
2562
|
+
queryBtn.addEventListener('click', function() {
|
|
2563
|
+
const fromEl = $('#ov-from');
|
|
2564
|
+
const toEl = $('#ov-to');
|
|
2565
|
+
if (fromEl && toEl && fromEl.value && toEl.value) {
|
|
2566
|
+
loadUsageOverview('custom', fromEl.value, toEl.value);
|
|
2567
|
+
}
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
// 设置默认日期为最近7天
|
|
2572
|
+
const now = new Date();
|
|
2573
|
+
const from = new Date(now.getTime() - 6 * 86400000);
|
|
2574
|
+
const fromEl = $('#ov-from');
|
|
2575
|
+
const toEl = $('#ov-to');
|
|
2576
|
+
if (fromEl) fromEl.value = formatDatetimeLocal(from);
|
|
2577
|
+
if (toEl) toEl.value = formatDatetimeLocal(now);
|
|
2578
|
+
|
|
2579
|
+
// 初始化明细查询
|
|
2580
|
+
initDetailQuery();
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// 格式化为 datetime-local 输入框的格式 (YYYY-MM-DDTHH:mm)
|
|
2584
|
+
function formatDatetimeLocal(date) {
|
|
2585
|
+
const year = date.getFullYear();
|
|
2586
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
2587
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
2588
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
2589
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
2590
|
+
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
// 模型访问明细查询
|
|
2594
|
+
function initDetailQuery() {
|
|
2595
|
+
// 填充Agent选择器
|
|
2596
|
+
loadDetailAgentList();
|
|
2597
|
+
// 填充Model选择器(按上面总览的时间范围)
|
|
2598
|
+
loadDetailModelList();
|
|
2599
|
+
|
|
2600
|
+
// 绑定分页大小变化
|
|
2601
|
+
const pageSizeEl = $('#detail-page-size');
|
|
2602
|
+
if (pageSizeEl) {
|
|
2603
|
+
pageSizeEl.addEventListener('change', function() {
|
|
2604
|
+
// 重置到第一页并查询
|
|
2605
|
+
const pageEl = $('#detail-page');
|
|
2606
|
+
if (pageEl) pageEl.value = '1';
|
|
2607
|
+
queryDetailUsage();
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// 绑定上一页/下一页按钮
|
|
2612
|
+
const prevBtn = $('#detail-prev-page');
|
|
2613
|
+
const nextBtn = $('#detail-next-page');
|
|
2614
|
+
if (prevBtn) {
|
|
2615
|
+
prevBtn.addEventListener('click', function() {
|
|
2616
|
+
const pageEl = $('#detail-page');
|
|
2617
|
+
if (pageEl && Number(pageEl.value) > 1) {
|
|
2618
|
+
pageEl.value = String(Number(pageEl.value) - 1);
|
|
2619
|
+
queryDetailUsage();
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
if (nextBtn) {
|
|
2624
|
+
nextBtn.addEventListener('click', function() {
|
|
2625
|
+
const pageEl = $('#detail-page');
|
|
2626
|
+
if (pageEl) {
|
|
2627
|
+
pageEl.value = String(Number(pageEl.value) + 1);
|
|
2628
|
+
queryDetailUsage();
|
|
2629
|
+
}
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// 绑定页码输入框回车事件
|
|
2634
|
+
const pageEl = $('#detail-page');
|
|
2635
|
+
if (pageEl) {
|
|
2636
|
+
pageEl.addEventListener('keypress', function(e) {
|
|
2637
|
+
if (e.key === 'Enter') {
|
|
2638
|
+
queryDetailUsage();
|
|
2639
|
+
}
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// 绑定Agent选择器变化事件
|
|
2644
|
+
const agentEl = $('#detail-agent');
|
|
2645
|
+
if (agentEl) {
|
|
2646
|
+
agentEl.addEventListener('change', function() {
|
|
2647
|
+
const pageEl = $('#detail-page');
|
|
2648
|
+
if (pageEl) pageEl.value = '1';
|
|
2649
|
+
queryDetailUsage();
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
// 绑定Model选择器变化事件
|
|
2654
|
+
const modelEl = $('#detail-model');
|
|
2655
|
+
if (modelEl) {
|
|
2656
|
+
modelEl.addEventListener('change', function() {
|
|
2657
|
+
const pageEl = $('#detail-page');
|
|
2658
|
+
if (pageEl) pageEl.value = '1';
|
|
2659
|
+
queryDetailUsage();
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
async function loadDetailAgentList() {
|
|
2665
|
+
try {
|
|
2666
|
+
const resp = await fetch(apiUrl('api/stats/agents'), {
|
|
2667
|
+
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
2668
|
+
});
|
|
2669
|
+
if (!resp.ok) return;
|
|
2670
|
+
const agents = await resp.json();
|
|
2671
|
+
|
|
2672
|
+
const selectEl = $('#detail-agent');
|
|
2673
|
+
if (selectEl && agents.length) {
|
|
2674
|
+
// 清空除第一个"全部"选项之外的所有选项
|
|
2675
|
+
while (selectEl.options.length > 1) {
|
|
2676
|
+
selectEl.remove(1);
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
agents.forEach(function(a) {
|
|
2680
|
+
const option = document.createElement('option');
|
|
2681
|
+
option.value = a.agent_aid;
|
|
2682
|
+
// 优先显示agent_name,没有则显示aid前缀
|
|
2683
|
+
option.textContent = a.agent_name || a.agent_aid.split('.')[0];
|
|
2684
|
+
selectEl.appendChild(option);
|
|
2685
|
+
});
|
|
2686
|
+
// 默认选中第一个agent
|
|
2687
|
+
if (agents.length > 0) {
|
|
2688
|
+
selectEl.value = agents[0].agent_aid;
|
|
2689
|
+
}
|
|
2690
|
+
// 加载完成后自动查询一次
|
|
2691
|
+
queryDetailUsage();
|
|
2692
|
+
}
|
|
2693
|
+
} catch {}
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
// 加载模型列表(按上面总览的时间范围)
|
|
2697
|
+
async function loadDetailModelList() {
|
|
2698
|
+
const selectEl = $('#detail-model');
|
|
2699
|
+
if (!selectEl) return;
|
|
2700
|
+
// 记住当前选中值,刷新后尽量保持
|
|
2701
|
+
const prev = selectEl.value;
|
|
2702
|
+
try {
|
|
2703
|
+
const timeRange = window._currentOverviewTimeRange || {};
|
|
2704
|
+
const params = new URLSearchParams();
|
|
2705
|
+
if (timeRange.fromTs) params.set('from', String(timeRange.fromTs));
|
|
2706
|
+
if (timeRange.toTs) params.set('to', String(timeRange.toTs));
|
|
2707
|
+
|
|
2708
|
+
const resp = await fetch(apiUrl('api/stats/models?' + params.toString()), {
|
|
2709
|
+
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
2710
|
+
});
|
|
2711
|
+
if (!resp.ok) return;
|
|
2712
|
+
const models = await resp.json();
|
|
2713
|
+
|
|
2714
|
+
// 清空除第一个"全部"选项之外的所有选项
|
|
2715
|
+
while (selectEl.options.length > 1) {
|
|
2716
|
+
selectEl.remove(1);
|
|
2717
|
+
}
|
|
2718
|
+
(models || []).forEach(function(m) {
|
|
2719
|
+
const option = document.createElement('option');
|
|
2720
|
+
option.value = m;
|
|
2721
|
+
option.textContent = m;
|
|
2722
|
+
selectEl.appendChild(option);
|
|
2723
|
+
});
|
|
2724
|
+
// 恢复之前的选择(若仍存在)
|
|
2725
|
+
if (prev && Array.prototype.some.call(selectEl.options, function(o) { return o.value === prev; })) {
|
|
2726
|
+
selectEl.value = prev;
|
|
2727
|
+
} else {
|
|
2728
|
+
selectEl.value = '';
|
|
2729
|
+
}
|
|
2730
|
+
} catch {}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
async function queryDetailUsage() {
|
|
2734
|
+
// 使用总览的时间范围
|
|
2735
|
+
const timeRange = window._currentOverviewTimeRange || {};
|
|
2736
|
+
const fromTs = timeRange.fromTs;
|
|
2737
|
+
const toTs = timeRange.toTs;
|
|
2738
|
+
|
|
2739
|
+
const agentEl = $('#detail-agent');
|
|
2740
|
+
const modelEl = $('#detail-model');
|
|
2741
|
+
const pageEl = $('#detail-page');
|
|
2742
|
+
const pageSizeEl = $('#detail-page-size');
|
|
2743
|
+
|
|
2744
|
+
const page = pageEl ? Number(pageEl.value) || 1 : 1;
|
|
2745
|
+
const pageSize = pageSizeEl ? Number(pageSizeEl.value) || 50 : 50;
|
|
2746
|
+
const offset = (page - 1) * pageSize;
|
|
2747
|
+
|
|
2748
|
+
const params = new URLSearchParams();
|
|
2749
|
+
if (fromTs) params.set('from', String(fromTs));
|
|
2750
|
+
if (toTs) params.set('to', String(toTs));
|
|
2751
|
+
if (agentEl && agentEl.value) params.set('agent', agentEl.value);
|
|
2752
|
+
if (modelEl && modelEl.value) params.set('model', modelEl.value);
|
|
2753
|
+
params.set('limit', String(pageSize));
|
|
2754
|
+
params.set('offset', String(offset));
|
|
2755
|
+
|
|
2756
|
+
try {
|
|
2757
|
+
const resp = await fetch(apiUrl('api/stats/detail?' + params.toString()), {
|
|
2758
|
+
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
2759
|
+
});
|
|
2760
|
+
if (!resp.ok) {
|
|
2761
|
+
showDetailError(t('usage.detail.error'));
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
const result = await resp.json();
|
|
2765
|
+
renderDetailTable(result.data, result.total, page, pageSize);
|
|
2766
|
+
} catch {
|
|
2767
|
+
showDetailError(t('usage.detail.error'));
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
function renderDetailTable(data, total, currentPage, pageSize) {
|
|
2772
|
+
const tableEl = $('#detail-table');
|
|
2773
|
+
if (!tableEl) return;
|
|
2774
|
+
|
|
2775
|
+
if (!data || !data.length) {
|
|
2776
|
+
tableEl.innerHTML = '<tbody><tr><td colspan="10" style="text-align:center;color:var(--dim)">' + t('usage.explorer.noData') + '</td></tr></tbody>';
|
|
2777
|
+
updatePaginationInfo(0, currentPage, pageSize);
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
const html = '<thead><tr>' +
|
|
2782
|
+
'<th>' + t('usage.detail.th.time') + '</th>' +
|
|
2783
|
+
'<th>' + t('usage.detail.th.agent') + '</th>' +
|
|
2784
|
+
'<th>' + t('usage.detail.th.peer') + '</th>' +
|
|
2785
|
+
'<th>' + t('usage.detail.th.model') + '</th>' +
|
|
2786
|
+
'<th>' + t('usage.detail.th.input') + '</th>' +
|
|
2787
|
+
'<th>' + t('usage.detail.th.output') + '</th>' +
|
|
2788
|
+
'<th>' + t('usage.detail.th.cacheCreation') + '</th>' +
|
|
2789
|
+
'<th>' + t('usage.detail.th.cacheRead') + '</th>' +
|
|
2790
|
+
'<th>' + t('usage.detail.th.costOfficial') + '</th>' +
|
|
2791
|
+
'<th>' + t('usage.detail.th.costGateway') + '</th>' +
|
|
2792
|
+
'</tr></thead><tbody>' +
|
|
2793
|
+
data.map(function(row) {
|
|
2794
|
+
const time = new Date(row.ts).toLocaleString();
|
|
2795
|
+
const agentName = row.agent_name || (row.agent_aid || '').split('.')[0];
|
|
2796
|
+
const peerName = (row.peer_key || '').replace(/^aun#/, '').split('.')[0];
|
|
2797
|
+
return '<tr>' +
|
|
2798
|
+
'<td style="white-space:nowrap">' + time + '</td>' +
|
|
2799
|
+
'<td title="' + esc(row.agent_aid) + '">' + esc(agentName) + '</td>' +
|
|
2800
|
+
'<td title="' + esc(row.peer_key) + '">' + esc(peerName) + '</td>' +
|
|
2801
|
+
'<td>' + esc(row.model || '') + '</td>' +
|
|
2802
|
+
'<td>' + fmtTokens(row.input_tokens || 0) + '</td>' +
|
|
2803
|
+
'<td>' + fmtTokens(row.output_tokens || 0) + '</td>' +
|
|
2804
|
+
'<td>' + fmtTokens(row.cache_creation_tokens || 0) + '</td>' +
|
|
2805
|
+
'<td>' + fmtTokens(row.cache_read_tokens || 0) + '</td>' +
|
|
2806
|
+
'<td>' + fmtCostCompact(row.cost_official_usd, row.cost_official_cny) + '</td>' +
|
|
2807
|
+
'<td>' + fmtCostCompact(row.cost_gateway_usd, row.cost_gateway_cny) + '</td>' +
|
|
2808
|
+
'</tr>';
|
|
2809
|
+
}).join('') +
|
|
2810
|
+
'</tbody>';
|
|
2811
|
+
|
|
2812
|
+
tableEl.innerHTML = html;
|
|
2813
|
+
updatePaginationInfo(total, currentPage, pageSize);
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
function updatePaginationInfo(total, currentPage, pageSize) {
|
|
2817
|
+
const infoEl = $('#detail-pagination-info');
|
|
2818
|
+
const prevBtn = $('#detail-prev-page');
|
|
2819
|
+
const nextBtn = $('#detail-next-page');
|
|
2820
|
+
|
|
2821
|
+
if (infoEl) {
|
|
2822
|
+
const start = total > 0 ? (currentPage - 1) * pageSize + 1 : 0;
|
|
2823
|
+
const end = Math.min(currentPage * pageSize, total);
|
|
2824
|
+
const totalPages = Math.ceil(total / pageSize) || 1;
|
|
2825
|
+
infoEl.textContent = t('usage.detail.pagination')
|
|
2826
|
+
.replace('{start}', start)
|
|
2827
|
+
.replace('{end}', end)
|
|
2828
|
+
.replace('{total}', total)
|
|
2829
|
+
.replace('{page}', currentPage)
|
|
2830
|
+
.replace('{totalPages}', totalPages);
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
if (prevBtn) prevBtn.disabled = currentPage <= 1;
|
|
2834
|
+
if (nextBtn) nextBtn.disabled = currentPage >= Math.ceil(total / pageSize);
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
function showDetailError(msg) {
|
|
2838
|
+
const tableEl = $('#detail-table');
|
|
2839
|
+
if (tableEl) {
|
|
2840
|
+
tableEl.innerHTML = '<tbody><tr><td colspan="10" style="text-align:center;color:var(--red)">' + esc(msg) + '</td></tr></tbody>';
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
function fmtCostCompact(usd, cny) {
|
|
2845
|
+
var parts = [];
|
|
2846
|
+
if (usd > 0) parts.push('$' + (usd < 0.01 ? usd.toFixed(4) : usd.toFixed(2)));
|
|
2847
|
+
if (cny > 0) parts.push('¥' + (cny < 0.01 ? cny.toFixed(4) : cny.toFixed(2)));
|
|
2848
|
+
if (parts.length === 0) return '-';
|
|
2849
|
+
return parts.join(' / ');
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
function ovCard(value, label, groupClass) {
|
|
2853
|
+
var cls = 'usage-card' + (groupClass ? ' ' + groupClass : '');
|
|
2854
|
+
return '<div class="' + cls + '"><div class="card-value">' + value + '</div><div class="card-label">' + label + '</div></div>';
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
// 创建多值卡片(合并多个指标到一个卡片中)
|
|
2858
|
+
function makeMultiValueCard(items, title, groupClass) {
|
|
2859
|
+
var cls = 'usage-card multi-value-card' + (groupClass ? ' ' + groupClass : '');
|
|
2860
|
+
var itemsHtml = items.map(function(item) {
|
|
2861
|
+
return '<div class="card-item"><div class="card-item-label">' + item.label + '</div><div class="card-item-value">' + item.value + '</div></div>';
|
|
2862
|
+
}).join('');
|
|
2863
|
+
return '<div class="' + cls + '"><div class="card-title">' + title + '</div><div class="card-items">' + itemsHtml + '</div></div>';
|
|
1253
2864
|
}
|
|
1254
2865
|
|
|
1255
2866
|
function fmtCost(usd, cny) {
|
|
@@ -1259,6 +2870,25 @@ function fmtCost(usd, cny) {
|
|
|
1259
2870
|
return parts.length ? parts.join(' / ') : '$0';
|
|
1260
2871
|
}
|
|
1261
2872
|
|
|
2873
|
+
// 分行显示美元和人民币
|
|
2874
|
+
function fmtCostSplit(usd, cny) {
|
|
2875
|
+
var parts = [];
|
|
2876
|
+
if (usd > 0) parts.push('$' + (usd < 0.01 ? usd.toFixed(4) : usd.toFixed(2)));
|
|
2877
|
+
if (cny > 0) parts.push('¥' + (cny < 0.01 ? cny.toFixed(4) : cny.toFixed(2)));
|
|
2878
|
+
if (parts.length === 0) return '<span style="color:var(--dim)">$0</span>';
|
|
2879
|
+
if (parts.length === 1) return parts[0];
|
|
2880
|
+
return parts[0] + '<br><span style="font-size:10px;color:var(--dim)">' + parts[1] + '</span>';
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
// 带标签的价格显示(用于卡片)
|
|
2884
|
+
function fmtCostWithLabel(usd, cny, label) {
|
|
2885
|
+
var parts = [];
|
|
2886
|
+
if (usd > 0) parts.push('$' + (usd < 0.01 ? usd.toFixed(4) : usd.toFixed(2)));
|
|
2887
|
+
if (cny > 0) parts.push('¥' + (cny < 0.01 ? cny.toFixed(4) : cny.toFixed(2)));
|
|
2888
|
+
var value = parts.length ? parts.join(' / ') : '$0';
|
|
2889
|
+
return '<div class="card-label" style="margin-bottom:4px;margin-top:0">' + label + '</div><div class="card-value" style="font-size:18px">' + value + '</div>';
|
|
2890
|
+
}
|
|
2891
|
+
|
|
1262
2892
|
// ── Usage subtab switching ──
|
|
1263
2893
|
function initUsageSubtabs() {
|
|
1264
2894
|
var btns = document.querySelectorAll('.usage-subtab');
|
|
@@ -1273,89 +2903,324 @@ function initUsageSubtabs() {
|
|
|
1273
2903
|
});
|
|
1274
2904
|
var panel = $('#usage-' + target);
|
|
1275
2905
|
if (panel) { panel.classList.add('active'); panel.style.display = ''; }
|
|
1276
|
-
if (target === 'overview')
|
|
1277
|
-
|
|
1278
|
-
|
|
2906
|
+
if (target === 'overview') {
|
|
2907
|
+
initOverviewFilters();
|
|
2908
|
+
loadUsageOverview();
|
|
2909
|
+
} else if (target === 'explorer') {
|
|
2910
|
+
initExplorer();
|
|
2911
|
+
// 自动加载模型列表和执行查询
|
|
2912
|
+
loadExplorerModels();
|
|
2913
|
+
setTimeout(() => runExplorerQuery(), 100);
|
|
2914
|
+
}
|
|
1279
2915
|
});
|
|
1280
2916
|
});
|
|
2917
|
+
|
|
2918
|
+
// 初始化总览页面的过滤器并加载默认数据(今日)
|
|
2919
|
+
initOverviewFilters();
|
|
2920
|
+
loadUsageOverview('today');
|
|
1281
2921
|
}
|
|
1282
2922
|
|
|
1283
2923
|
// ── Explorer ──
|
|
1284
2924
|
var _explorerChart = null;
|
|
1285
2925
|
var _explorerInited = false;
|
|
1286
2926
|
var _expSelection = { type: null, key: null }; // { type: 'agent'|'peer', key: string } or null
|
|
2927
|
+
var _expCurrentRange = 'today'; // Explorer 当前选择的时间范围
|
|
2928
|
+
var _expTimeRange = { fromTs: null, toTs: null }; // Explorer 的时间范围
|
|
1287
2929
|
|
|
1288
2930
|
function initExplorer() {
|
|
1289
2931
|
if (_explorerInited) return;
|
|
1290
2932
|
_explorerInited = true;
|
|
2933
|
+
|
|
2934
|
+
// 初始化时间范围选择
|
|
2935
|
+
initExplorerTimeFilters();
|
|
2936
|
+
|
|
2937
|
+
// 绑定查询按钮
|
|
1291
2938
|
var btn = $('#exp-query-btn');
|
|
1292
2939
|
if (btn) btn.onclick = runExplorerQuery;
|
|
1293
|
-
|
|
1294
|
-
var now = new Date();
|
|
1295
|
-
var from = new Date(now.getTime() - 7 * 86400000);
|
|
1296
|
-
var fromEl = $('#exp-from');
|
|
1297
|
-
var toEl = $('#exp-to');
|
|
1298
|
-
if (fromEl) fromEl.value = from.toISOString().slice(0, 10);
|
|
1299
|
-
if (toEl) toEl.value = now.toISOString().slice(0, 10);
|
|
2940
|
+
|
|
1300
2941
|
// Load sidebar lists
|
|
1301
2942
|
loadExplorerSidebar();
|
|
1302
2943
|
}
|
|
1303
2944
|
|
|
2945
|
+
// 初始化 Explorer 的时间范围选择
|
|
2946
|
+
function initExplorerTimeFilters() {
|
|
2947
|
+
// 范围按钮切换
|
|
2948
|
+
document.querySelectorAll('.exp-range-btn').forEach(function(btn) {
|
|
2949
|
+
btn.addEventListener('click', function() {
|
|
2950
|
+
document.querySelectorAll('.exp-range-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
2951
|
+
btn.classList.add('active');
|
|
2952
|
+
|
|
2953
|
+
const range = btn.getAttribute('data-range');
|
|
2954
|
+
const customDateEl = $('#exp-custom-date');
|
|
2955
|
+
|
|
2956
|
+
if (range === 'custom') {
|
|
2957
|
+
if (customDateEl) customDateEl.style.display = 'flex';
|
|
2958
|
+
} else {
|
|
2959
|
+
if (customDateEl) customDateEl.style.display = 'none';
|
|
2960
|
+
_expCurrentRange = range;
|
|
2961
|
+
calculateExplorerTimeRange(range);
|
|
2962
|
+
loadExplorerModels(); // 加载可用模型
|
|
2963
|
+
runExplorerQuery();
|
|
2964
|
+
}
|
|
2965
|
+
});
|
|
2966
|
+
});
|
|
2967
|
+
|
|
2968
|
+
// 自定义时间查询按钮
|
|
2969
|
+
const timeQueryBtn = $('#exp-time-query-btn');
|
|
2970
|
+
if (timeQueryBtn) {
|
|
2971
|
+
timeQueryBtn.addEventListener('click', function() {
|
|
2972
|
+
const fromEl = $('#exp-from');
|
|
2973
|
+
const toEl = $('#exp-to');
|
|
2974
|
+
if (fromEl && toEl && fromEl.value && toEl.value) {
|
|
2975
|
+
_expCurrentRange = 'custom';
|
|
2976
|
+
_expTimeRange.fromTs = new Date(fromEl.value).getTime();
|
|
2977
|
+
_expTimeRange.toTs = new Date(toEl.value).getTime();
|
|
2978
|
+
loadExplorerModels(); // 加载可用模型
|
|
2979
|
+
runExplorerQuery();
|
|
2980
|
+
}
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
// 设置默认时间范围(今日)并初始化日期选择器
|
|
2985
|
+
const now = new Date();
|
|
2986
|
+
const fromEl = $('#exp-from');
|
|
2987
|
+
const toEl = $('#exp-to');
|
|
2988
|
+
if (fromEl) fromEl.value = formatDatetimeLocal(new Date(now.getFullYear(), now.getMonth(), now.getDate()));
|
|
2989
|
+
if (toEl) toEl.value = formatDatetimeLocal(now);
|
|
2990
|
+
|
|
2991
|
+
// 计算默认时间范围(今日)
|
|
2992
|
+
calculateExplorerTimeRange('today');
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
// 计算 Explorer 的时间范围
|
|
2996
|
+
function calculateExplorerTimeRange(rangeType) {
|
|
2997
|
+
const now = new Date();
|
|
2998
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
2999
|
+
const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime();
|
|
3000
|
+
|
|
3001
|
+
switch (rangeType) {
|
|
3002
|
+
case 'today':
|
|
3003
|
+
_expTimeRange.fromTs = todayStart;
|
|
3004
|
+
_expTimeRange.toTs = todayEnd;
|
|
3005
|
+
break;
|
|
3006
|
+
case 'week':
|
|
3007
|
+
const dayOfWeek = now.getDay() || 7;
|
|
3008
|
+
const weekStart = new Date(todayStart - (dayOfWeek - 1) * 86400000);
|
|
3009
|
+
_expTimeRange.fromTs = weekStart.getTime();
|
|
3010
|
+
_expTimeRange.toTs = todayEnd;
|
|
3011
|
+
break;
|
|
3012
|
+
case 'lastWeek':
|
|
3013
|
+
const lastWeekEnd = new Date(todayStart - now.getDay() * 86400000);
|
|
3014
|
+
const lastWeekStart = new Date(lastWeekEnd.getTime() - 6 * 86400000);
|
|
3015
|
+
_expTimeRange.fromTs = lastWeekStart.getTime();
|
|
3016
|
+
_expTimeRange.toTs = new Date(lastWeekEnd.getTime() + 86400000 - 1).getTime();
|
|
3017
|
+
break;
|
|
3018
|
+
case 'month':
|
|
3019
|
+
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
3020
|
+
_expTimeRange.fromTs = monthStart.getTime();
|
|
3021
|
+
_expTimeRange.toTs = todayEnd;
|
|
3022
|
+
break;
|
|
3023
|
+
case 'last30':
|
|
3024
|
+
_expTimeRange.fromTs = todayStart - 29 * 86400000;
|
|
3025
|
+
_expTimeRange.toTs = todayEnd;
|
|
3026
|
+
break;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
// 加载 Explorer 可用的模型列表(根据当前时间范围)
|
|
3031
|
+
async function loadExplorerModels() {
|
|
3032
|
+
const params = new URLSearchParams();
|
|
3033
|
+
if (_expTimeRange.fromTs) params.set('from', String(_expTimeRange.fromTs));
|
|
3034
|
+
if (_expTimeRange.toTs) params.set('to', String(_expTimeRange.toTs));
|
|
3035
|
+
|
|
3036
|
+
try {
|
|
3037
|
+
const resp = await fetch(apiUrl('api/stats/models?' + params.toString()), {
|
|
3038
|
+
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
3039
|
+
});
|
|
3040
|
+
if (!resp.ok) return;
|
|
3041
|
+
const models = await resp.json();
|
|
3042
|
+
|
|
3043
|
+
const selectEl = $('#exp-model');
|
|
3044
|
+
if (selectEl) {
|
|
3045
|
+
const currentValue = selectEl.value;
|
|
3046
|
+
selectEl.innerHTML = '<option value="">' + t('usage.explorer.all') + '</option>';
|
|
3047
|
+
models.forEach(function(model) {
|
|
3048
|
+
const option = document.createElement('option');
|
|
3049
|
+
option.value = model;
|
|
3050
|
+
option.textContent = model;
|
|
3051
|
+
selectEl.appendChild(option);
|
|
3052
|
+
});
|
|
3053
|
+
// 恢复之前的选择(如果还存在)
|
|
3054
|
+
if (currentValue && models.includes(currentValue)) {
|
|
3055
|
+
selectEl.value = currentValue;
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
} catch {}
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
// 获取 Explorer 时间范围的总览数据
|
|
3062
|
+
async function fetchExplorerOverviewData(filterParams) {
|
|
3063
|
+
try {
|
|
3064
|
+
const params = new URLSearchParams();
|
|
3065
|
+
if (_expTimeRange.fromTs) params.set('from', String(_expTimeRange.fromTs));
|
|
3066
|
+
if (_expTimeRange.toTs) params.set('to', String(_expTimeRange.toTs));
|
|
3067
|
+
|
|
3068
|
+
// 添加筛选参数
|
|
3069
|
+
if (filterParams) {
|
|
3070
|
+
if (filterParams.agent) params.set('agent', filterParams.agent);
|
|
3071
|
+
if (filterParams.peer) params.set('peer', filterParams.peer);
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
const resp = await fetch(apiUrl('api/stats/overview?' + params.toString()), {
|
|
3075
|
+
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
3076
|
+
});
|
|
3077
|
+
const data = resp.ok ? await resp.json() : null;
|
|
3078
|
+
|
|
3079
|
+
if (data) {
|
|
3080
|
+
const ts = (data.token_stats && data.token_stats.all_time) ? data.token_stats.all_time
|
|
3081
|
+
: { input_tokens: 0, output_tokens: 0, cache_creation_tokens: 0, cache_read_tokens: 0, call_count: 0, cost_official_usd: 0, cost_official_cny: 0, cost_usd: 0, cost_cny: 0 };
|
|
3082
|
+
const sessionCount = data.session_count || 0;
|
|
3083
|
+
const msgIn = data.msg_in || 0;
|
|
3084
|
+
const msgOut = data.msg_out || 0;
|
|
3085
|
+
const totalTokens = ts.cache_read_tokens + ts.cache_creation_tokens + ts.input_tokens + ts.output_tokens;
|
|
3086
|
+
const hitRate = totalTokens > 0 ? (ts.cache_read_tokens / totalTokens) * 100 : 0;
|
|
3087
|
+
|
|
3088
|
+
return { ts, sessionCount, msgIn, msgOut, hitRate };
|
|
3089
|
+
}
|
|
3090
|
+
} catch {}
|
|
3091
|
+
|
|
3092
|
+
// 返回空数据
|
|
3093
|
+
return {
|
|
3094
|
+
ts: { call_count: 0, input_tokens: 0, output_tokens: 0, cache_creation_tokens: 0, cache_read_tokens: 0, cost_official_usd: 0, cost_official_cny: 0, cost_usd: 0, cost_cny: 0 },
|
|
3095
|
+
sessionCount: 0,
|
|
3096
|
+
msgIn: 0,
|
|
3097
|
+
msgOut: 0,
|
|
3098
|
+
hitRate: 0
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
|
|
1304
3102
|
async function loadExplorerSidebar() {
|
|
1305
3103
|
var token = localStorage.getItem(TOKEN_KEY);
|
|
1306
3104
|
var headers = { Authorization: 'Bearer ' + token };
|
|
1307
3105
|
try {
|
|
1308
|
-
var
|
|
1309
|
-
fetch('/api/stats/agents', { headers }),
|
|
1310
|
-
fetch('/api/stats/peers', { headers }),
|
|
1311
|
-
]);
|
|
3106
|
+
var agentsResp = await fetch(apiUrl('api/stats/agents'), { headers });
|
|
1312
3107
|
var agents = agentsResp.ok ? await agentsResp.json() : [];
|
|
1313
|
-
|
|
1314
|
-
|
|
3108
|
+
renderExplorerAgentList(agents);
|
|
3109
|
+
|
|
3110
|
+
// 初始加载时不加载 peers(等待用户选择 agent)
|
|
3111
|
+
renderExplorerPeerList([]);
|
|
1315
3112
|
} catch {}
|
|
1316
3113
|
}
|
|
1317
3114
|
|
|
1318
|
-
|
|
3115
|
+
// 渲染 Agent 列表
|
|
3116
|
+
function renderExplorerAgentList(agents) {
|
|
1319
3117
|
var agentList = $('#exp-agent-list');
|
|
1320
|
-
|
|
1321
|
-
if (!agentList || !peerList) return;
|
|
3118
|
+
if (!agentList) return;
|
|
1322
3119
|
|
|
1323
3120
|
// "All" item for agents
|
|
1324
3121
|
var allHtml = '<div class="exp-sidebar-item active" data-type="all" data-key="">' +
|
|
1325
|
-
'<span class="item-name"
|
|
3122
|
+
'<span class="item-name">' + t('usage.explorer.all') + '</span></div>';
|
|
1326
3123
|
|
|
1327
3124
|
agentList.innerHTML = allHtml + agents.map(function(a) {
|
|
1328
|
-
var name = a.agent_aid ? a.agent_aid.split('.')[0] : 'unknown';
|
|
3125
|
+
var name = a.agent_name || (a.agent_aid ? a.agent_aid.split('.')[0] : 'unknown');
|
|
1329
3126
|
return '<div class="exp-sidebar-item" data-type="agent" data-key="' + escHtml(a.agent_aid) + '">' +
|
|
1330
|
-
'<span class="item-name" title="' + escHtml(a.agent_aid) + '">' + escHtml(name) + '</span>'
|
|
1331
|
-
'<span class="item-meta">' + fmtTokens(a.input_tokens + a.output_tokens) + '</span></div>';
|
|
3127
|
+
'<span class="item-name" title="' + escHtml(a.agent_aid) + '">' + escHtml(name) + '</span></div>';
|
|
1332
3128
|
}).join('');
|
|
1333
3129
|
|
|
3130
|
+
// 绑定点击事件
|
|
3131
|
+
agentList.querySelectorAll('.exp-sidebar-item').forEach(function(el) {
|
|
3132
|
+
el.addEventListener('click', async function() {
|
|
3133
|
+
// Clear active from all
|
|
3134
|
+
document.querySelectorAll('#exp-agent-list .exp-sidebar-item').forEach(function(x) { x.classList.remove('active'); });
|
|
3135
|
+
el.classList.add('active');
|
|
3136
|
+
|
|
3137
|
+
var type = el.getAttribute('data-type');
|
|
3138
|
+
var key = el.getAttribute('data-key');
|
|
3139
|
+
|
|
3140
|
+
if (type === 'all') {
|
|
3141
|
+
_expSelection = { type: null, key: null };
|
|
3142
|
+
$('#exp-selected-name').textContent = t('usage.explorer.all');
|
|
3143
|
+
// 选择"全部"时,清空 peers 列表
|
|
3144
|
+
renderExplorerPeerList([]);
|
|
3145
|
+
} else {
|
|
3146
|
+
_expSelection = { type: type, key: key };
|
|
3147
|
+
var name = el.querySelector('.item-name').textContent.trim();
|
|
3148
|
+
$('#exp-selected-name').textContent = name;
|
|
3149
|
+
// 选择特定 agent 时,加载该 agent 的 peers
|
|
3150
|
+
await loadPeersForAgent(key);
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
runExplorerQuery();
|
|
3154
|
+
});
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
// 加载指定 agent 的 peers
|
|
3159
|
+
async function loadPeersForAgent(agentAid) {
|
|
3160
|
+
var token = localStorage.getItem(TOKEN_KEY);
|
|
3161
|
+
var headers = { Authorization: 'Bearer ' + token };
|
|
3162
|
+
try {
|
|
3163
|
+
const params = new URLSearchParams();
|
|
3164
|
+
params.set('agent', agentAid);
|
|
3165
|
+
// 不传递时间范围,获取该 agent 的所有 peers
|
|
3166
|
+
|
|
3167
|
+
var resp = await fetch(apiUrl('api/stats/peers?' + params.toString()), { headers });
|
|
3168
|
+
var peers = resp.ok ? await resp.json() : [];
|
|
3169
|
+
renderExplorerPeerList(peers);
|
|
3170
|
+
} catch {
|
|
3171
|
+
renderExplorerPeerList([]);
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
// 渲染 Peer 列表
|
|
3176
|
+
function renderExplorerPeerList(peers) {
|
|
3177
|
+
var peerList = $('#exp-peer-list');
|
|
3178
|
+
if (!peerList) return;
|
|
3179
|
+
|
|
3180
|
+
if (!peers || peers.length === 0) {
|
|
3181
|
+
peerList.innerHTML = '<div style="padding: 12px; color: var(--dim); font-size: 12px; text-align: center;">' + t('common.noData') + '</div>';
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
|
|
1334
3185
|
peerList.innerHTML = peers.map(function(p) {
|
|
1335
3186
|
var name = p.peer_key || 'unknown';
|
|
1336
|
-
//
|
|
1337
|
-
var display = name.replace(/^aun#/, '').split('.')[0];
|
|
3187
|
+
// 优先显示peer_name,否则简化显示peer_key
|
|
3188
|
+
var display = p.peer_name || name.replace(/^aun#/, '').split('#')[0].split('.')[0];
|
|
3189
|
+
|
|
3190
|
+
// 添加聊天类型标签
|
|
3191
|
+
var typeTag = '';
|
|
3192
|
+
if (p.peer_chat_type === 'group') {
|
|
3193
|
+
typeTag = '<span class="peer-tag peer-tag-group">' + t('usage.explorer.chatType.group') + '</span>';
|
|
3194
|
+
} else if (p.peer_chat_type === 'private') {
|
|
3195
|
+
typeTag = '<span class="peer-tag peer-tag-private">' + t('usage.explorer.chatType.private') + '</span>';
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
// 群聊人数标签
|
|
3199
|
+
var memberTag = '';
|
|
3200
|
+
if (p.peer_chat_type === 'group' && p.peer_group_member_count) {
|
|
3201
|
+
memberTag = '<span class="peer-tag peer-tag-count">' + p.peer_group_member_count + t('usage.explorer.memberCount') + '</span>';
|
|
3202
|
+
}
|
|
3203
|
+
|
|
1338
3204
|
return '<div class="exp-sidebar-item" data-type="peer" data-key="' + escHtml(p.peer_key) + '">' +
|
|
1339
|
-
'<span class="item-name" title="' + escHtml(name) + '">' +
|
|
3205
|
+
'<span class="item-name" title="' + escHtml(name) + '">' +
|
|
3206
|
+
(typeTag ? typeTag + ' ' : '') + escHtml(display) + (memberTag ? ' ' + memberTag : '') +
|
|
3207
|
+
'</span>' +
|
|
1340
3208
|
'<span class="item-meta">' + fmtTokens((p.input_tokens || 0) + (p.output_tokens || 0)) + '</span></div>';
|
|
1341
3209
|
}).join('');
|
|
1342
3210
|
|
|
1343
|
-
// Bind click events
|
|
1344
|
-
|
|
1345
|
-
allItems.forEach(function(el) {
|
|
3211
|
+
// Bind click events for peers
|
|
3212
|
+
peerList.querySelectorAll('.exp-sidebar-item').forEach(function(el) {
|
|
1346
3213
|
el.addEventListener('click', function() {
|
|
1347
|
-
// Clear active from all
|
|
1348
|
-
|
|
3214
|
+
// Clear active from all peers
|
|
3215
|
+
document.querySelectorAll('#exp-peer-list .exp-sidebar-item').forEach(function(x) { x.classList.remove('active'); });
|
|
1349
3216
|
el.classList.add('active');
|
|
3217
|
+
|
|
1350
3218
|
var type = el.getAttribute('data-type');
|
|
1351
3219
|
var key = el.getAttribute('data-key');
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
_expSelection = { type: type, key: key };
|
|
1357
|
-
$('#exp-selected-name').textContent = key;
|
|
1358
|
-
}
|
|
3220
|
+
_expSelection = { type: type, key: key };
|
|
3221
|
+
var name = el.querySelector('.item-name').textContent.trim();
|
|
3222
|
+
$('#exp-selected-name').textContent = name;
|
|
3223
|
+
|
|
1359
3224
|
runExplorerQuery();
|
|
1360
3225
|
});
|
|
1361
3226
|
});
|
|
@@ -1364,11 +3229,13 @@ function renderExplorerSidebar(agents, peers) {
|
|
|
1364
3229
|
function escHtml(s) { return (s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
1365
3230
|
|
|
1366
3231
|
async function runExplorerQuery() {
|
|
3232
|
+
// 使用 Explorer 自己的时间范围
|
|
3233
|
+
const fromTs = _expTimeRange.fromTs;
|
|
3234
|
+
const toTs = _expTimeRange.toTs;
|
|
3235
|
+
|
|
1367
3236
|
var params = new URLSearchParams();
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
if (fromEl && fromEl.value) params.set('from', String(new Date(fromEl.value + 'T00:00:00').getTime()));
|
|
1371
|
-
if (toEl && toEl.value) params.set('to', String(new Date(toEl.value + 'T23:59:59').getTime()));
|
|
3237
|
+
if (fromTs) params.set('from', String(fromTs));
|
|
3238
|
+
if (toTs) params.set('to', String(toTs));
|
|
1372
3239
|
// Inject selection from sidebar
|
|
1373
3240
|
if (_expSelection.type === 'agent' && _expSelection.key) params.set('agent', _expSelection.key);
|
|
1374
3241
|
if (_expSelection.type === 'peer' && _expSelection.key) params.set('peer', _expSelection.key);
|
|
@@ -1379,33 +3246,123 @@ async function runExplorerQuery() {
|
|
|
1379
3246
|
|
|
1380
3247
|
var data;
|
|
1381
3248
|
try {
|
|
1382
|
-
var resp = await fetch('
|
|
3249
|
+
var resp = await fetch(apiUrl('api/stats/explorer?' + params.toString()), {
|
|
1383
3250
|
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
1384
3251
|
});
|
|
1385
3252
|
if (!resp.ok) return;
|
|
1386
3253
|
data = await resp.json();
|
|
1387
3254
|
} catch { return; }
|
|
1388
3255
|
|
|
1389
|
-
//
|
|
3256
|
+
// 根据查询结果计算卡片数据
|
|
1390
3257
|
var cardsEl = $('#exp-detail-cards');
|
|
1391
|
-
if (
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
3258
|
+
if (cardsEl) {
|
|
3259
|
+
// 如果有筛选条件(agent/peer),使用查询结果计算;否则需要获取总览数据
|
|
3260
|
+
const hasFilter = _expSelection.type && _expSelection.key;
|
|
3261
|
+
|
|
3262
|
+
let cardData;
|
|
3263
|
+
let cardTitle = null; // 用于显示选中的 agent/peer 信息
|
|
3264
|
+
|
|
3265
|
+
if (hasFilter) {
|
|
3266
|
+
// 有筛选:根据查询结果计算 token 数据,并获取会话信息
|
|
3267
|
+
var totIn = 0, totOut = 0, totCacheCreation = 0, totCacheRead = 0, totCalls = 0;
|
|
3268
|
+
if (data && data.length) {
|
|
3269
|
+
data.forEach(function(r) {
|
|
3270
|
+
totIn += r.input_tokens || 0;
|
|
3271
|
+
totOut += r.output_tokens || 0;
|
|
3272
|
+
totCacheCreation += r.cache_creation_tokens || 0;
|
|
3273
|
+
totCacheRead += r.cache_read_tokens || 0;
|
|
3274
|
+
totCalls += r.call_count || 0;
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
const totalTokens = totCacheRead + totCacheCreation + totIn + totOut;
|
|
3278
|
+
const hitRate = totalTokens > 0 ? (totCacheRead / totalTokens) * 100 : 0;
|
|
3279
|
+
|
|
3280
|
+
// 构建筛选参数
|
|
3281
|
+
const filterParams = {};
|
|
3282
|
+
if (_expSelection.type === 'agent') filterParams.agent = _expSelection.key;
|
|
3283
|
+
if (_expSelection.type === 'peer') filterParams.peer = _expSelection.key;
|
|
3284
|
+
|
|
3285
|
+
// 获取该筛选条件下的会话信息
|
|
3286
|
+
const overviewData = await fetchExplorerOverviewData(filterParams);
|
|
3287
|
+
|
|
3288
|
+
cardData = {
|
|
3289
|
+
ts: {
|
|
3290
|
+
call_count: totCalls,
|
|
3291
|
+
input_tokens: totIn,
|
|
3292
|
+
output_tokens: totOut,
|
|
3293
|
+
cache_creation_tokens: totCacheCreation,
|
|
3294
|
+
cache_read_tokens: totCacheRead,
|
|
3295
|
+
cost_official_usd: overviewData.ts.cost_official_usd || 0,
|
|
3296
|
+
cost_official_cny: overviewData.ts.cost_official_cny || 0,
|
|
3297
|
+
cost_usd: overviewData.ts.cost_usd || 0,
|
|
3298
|
+
cost_cny: overviewData.ts.cost_cny || 0
|
|
3299
|
+
},
|
|
3300
|
+
sessionCount: overviewData.sessionCount || 0,
|
|
3301
|
+
msgIn: overviewData.msgIn || 0,
|
|
3302
|
+
msgOut: overviewData.msgOut || 0,
|
|
3303
|
+
hitRate: hitRate
|
|
3304
|
+
};
|
|
3305
|
+
|
|
3306
|
+
// 构建卡片标题
|
|
3307
|
+
if (_expSelection.type === 'agent') {
|
|
3308
|
+
// 从侧边栏获取 agent 名称
|
|
3309
|
+
const selectedItem = document.querySelector('#exp-agent-list .exp-sidebar-item.active .item-name');
|
|
3310
|
+
const agentName = selectedItem ? selectedItem.textContent.trim() : '';
|
|
3311
|
+
const agentAid = _expSelection.key;
|
|
3312
|
+
cardTitle = agentName && agentName !== agentAid.split('.')[0]
|
|
3313
|
+
? `${agentName} (AID: ${agentAid})`
|
|
3314
|
+
: `AID: ${agentAid}`;
|
|
3315
|
+
} else if (_expSelection.type === 'peer') {
|
|
3316
|
+
// 从侧边栏获取 peer 名称(去掉标签)
|
|
3317
|
+
const selectedItem = document.querySelector('#exp-peer-list .exp-sidebar-item.active .item-name');
|
|
3318
|
+
if (selectedItem) {
|
|
3319
|
+
// 克隆节点并移除所有标签元素
|
|
3320
|
+
const clone = selectedItem.cloneNode(true);
|
|
3321
|
+
const tags = clone.querySelectorAll('.peer-tag');
|
|
3322
|
+
tags.forEach(tag => tag.remove());
|
|
3323
|
+
const peerName = clone.textContent.trim();
|
|
3324
|
+
const peerKey = _expSelection.key;
|
|
3325
|
+
cardTitle = peerName ? `${peerName} (Peer: ${peerKey.split('#')[3] || peerKey.split('#')[0]})` : `Peer: ${peerKey}`;
|
|
3326
|
+
} else {
|
|
3327
|
+
cardTitle = `Peer: ${_expSelection.key}`;
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
} else {
|
|
3331
|
+
// 无筛选:获取 Explorer 时间范围的总览数据
|
|
3332
|
+
cardData = await fetchExplorerOverviewData();
|
|
1401
3333
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
3334
|
+
|
|
3335
|
+
const { ts, sessionCount, msgIn, msgOut, hitRate } = cardData;
|
|
3336
|
+
|
|
3337
|
+
// 不显示标题行,直接显示卡片
|
|
3338
|
+
// 注意:会话信息是该时间范围的总数(不区分 agent/peer)
|
|
3339
|
+
const sessionCard = makeMultiValueCard([
|
|
3340
|
+
{ label: t('usage.card.sessionCount'), value: sessionCount },
|
|
3341
|
+
{ label: t('usage.card.msgIn'), value: msgIn },
|
|
3342
|
+
{ label: t('usage.card.msgOut'), value: msgOut }
|
|
3343
|
+
], t('usage.card.sessionInfo'), 'session-group');
|
|
3344
|
+
|
|
3345
|
+
const usageCard = makeMultiValueCard([
|
|
3346
|
+
{ label: t('usage.card.modelCalls'), value: ts.call_count },
|
|
3347
|
+
{ label: t('usage.card.inputTokens'), value: fmtTokens(ts.input_tokens) },
|
|
3348
|
+
{ label: t('usage.card.outputTokens'), value: fmtTokens(ts.output_tokens) },
|
|
3349
|
+
{ label: t('usage.card.cacheCreation'), value: fmtTokens(ts.cache_creation_tokens) },
|
|
3350
|
+
{ label: t('usage.card.cacheHitTokens'), value: fmtTokens(ts.cache_read_tokens) },
|
|
3351
|
+
{ label: t('usage.card.cacheHitRate'), value: hitRate.toFixed(1) + '%' }
|
|
3352
|
+
], t('usage.card.usageInfo'), 'usage-group');
|
|
3353
|
+
|
|
3354
|
+
const costCard = makeMultiValueCard([
|
|
3355
|
+
{ label: t('usage.card.costOfficial'), value: fmtCost(ts.cost_official_usd, ts.cost_official_cny) },
|
|
3356
|
+
{ label: t('usage.card.costGateway'), value: fmtCost(ts.cost_usd, ts.cost_cny) }
|
|
3357
|
+
], t('usage.card.costInfo'), 'cost-group');
|
|
3358
|
+
|
|
3359
|
+
cardsEl.innerHTML = sessionCard + usageCard + costCard;
|
|
3360
|
+
cardsEl.style.display = 'flex';
|
|
1404
3361
|
}
|
|
1405
3362
|
|
|
1406
3363
|
if (!data || !data.length) {
|
|
1407
3364
|
var tbl = $('#usage-explorer-table');
|
|
1408
|
-
if (tbl) tbl.innerHTML = '<tr><td>
|
|
3365
|
+
if (tbl) tbl.innerHTML = '<tr><td>' + t('usage.explorer.noData') + '</td></tr>';
|
|
1409
3366
|
var chartEl = $('#usage-explorer-chart');
|
|
1410
3367
|
if (chartEl && _explorerChart) { _explorerChart.dispose(); _explorerChart = null; }
|
|
1411
3368
|
return;
|
|
@@ -1420,13 +3377,13 @@ async function runExplorerQuery() {
|
|
|
1420
3377
|
var periods = data.map(function(r) { return r.period; });
|
|
1421
3378
|
_explorerChart.setOption({
|
|
1422
3379
|
tooltip: { trigger: 'axis' },
|
|
1423
|
-
legend: { data: ['
|
|
3380
|
+
legend: { data: [t('usage.card.input'), t('usage.card.output')], top: 0, textStyle: { fontSize: 11 } },
|
|
1424
3381
|
grid: { top: 30, bottom: 30, left: 60, right: 16 },
|
|
1425
3382
|
xAxis: { type: 'category', data: periods, axisLabel: { fontSize: 10, rotate: 30 } },
|
|
1426
3383
|
yAxis: { type: 'value', axisLabel: { formatter: function(v) { return fmtTokens(v); } } },
|
|
1427
3384
|
series: [
|
|
1428
|
-
{ name: '
|
|
1429
|
-
{ name: '
|
|
3385
|
+
{ name: t('usage.card.input'), type: 'line', data: data.map(function(r) { return r.input_tokens; }), smooth: true, areaStyle: { opacity: 0.15 }, itemStyle: { color: '#4f6ef7' } },
|
|
3386
|
+
{ name: t('usage.card.output'), type: 'line', data: data.map(function(r) { return r.output_tokens; }), smooth: true, areaStyle: { opacity: 0.15 }, itemStyle: { color: '#38a169' } },
|
|
1430
3387
|
]
|
|
1431
3388
|
});
|
|
1432
3389
|
}
|
|
@@ -1435,7 +3392,7 @@ async function runExplorerQuery() {
|
|
|
1435
3392
|
var tbl = $('#usage-explorer-table');
|
|
1436
3393
|
if (tbl) {
|
|
1437
3394
|
tbl.innerHTML =
|
|
1438
|
-
'<thead><tr><th>
|
|
3395
|
+
'<thead><tr><th>' + t('usage.explorer.th.period') + '</th><th>' + t('usage.explorer.th.input') + '</th><th>' + t('usage.explorer.th.output') + '</th><th>' + t('usage.explorer.th.cacheCreation') + '</th><th>' + t('usage.explorer.th.cacheHit') + '</th><th>' + t('usage.explorer.th.calls') + '</th></tr></thead>' +
|
|
1439
3396
|
'<tbody>' + data.map(function(r) {
|
|
1440
3397
|
return '<tr><td>' + r.period + '</td><td>' + fmtTokens(r.input_tokens) + '</td><td>' + fmtTokens(r.output_tokens) +
|
|
1441
3398
|
'</td><td>' + fmtTokens(r.cache_creation_tokens) + '</td><td>' + fmtTokens(r.cache_read_tokens) +
|
|
@@ -1444,14 +3401,239 @@ async function runExplorerQuery() {
|
|
|
1444
3401
|
}
|
|
1445
3402
|
}
|
|
1446
3403
|
|
|
1447
|
-
|
|
3404
|
+
// ── Monitor ──────────────────────────────────────
|
|
3405
|
+
// 绑定时间范围切换按钮(只绑一次)
|
|
3406
|
+
let _monRangeBound = false;
|
|
3407
|
+
function bindMonRangeTabs() {
|
|
3408
|
+
if (_monRangeBound) return;
|
|
3409
|
+
var tabs = document.querySelectorAll('#view-monitor .mon-range');
|
|
3410
|
+
if (!tabs.length) return;
|
|
3411
|
+
tabs.forEach(function (btn) {
|
|
3412
|
+
btn.onclick = function () {
|
|
3413
|
+
monRange = btn.dataset.range;
|
|
3414
|
+
document.querySelectorAll('#view-monitor .mon-range').forEach(function (b) {
|
|
3415
|
+
b.classList.toggle('active', b.dataset.range === monRange);
|
|
3416
|
+
});
|
|
3417
|
+
// 切范围 → 重新订阅(源按 range 返回不同分辨率的 history)
|
|
3418
|
+
subscribe('monitor', { range: monRange });
|
|
3419
|
+
};
|
|
3420
|
+
});
|
|
3421
|
+
_monRangeBound = true;
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
function renderMonitor(data) {
|
|
3425
|
+
var wrap = $('#view-monitor .mon-layout');
|
|
3426
|
+
if (!wrap) return;
|
|
3427
|
+
bindMonRangeTabs();
|
|
3428
|
+
if (!data) { return; }
|
|
3429
|
+
if (!data.daemonRunning) {
|
|
3430
|
+
// 不清空骨架,仅在卡片区提示,避免破坏 toolbar
|
|
3431
|
+
var cardsEl0 = $('#mon-cards');
|
|
3432
|
+
if (cardsEl0) cardsEl0.innerHTML = '<div class="empty" style="grid-column:1/-1">daemon 未运行</div>';
|
|
3433
|
+
return;
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
var s = data.snapshot;
|
|
3437
|
+
// history 是三档分辨率对象 { fine, mid, coarse };按当前范围选一档
|
|
3438
|
+
var rangeKey = { '2m': 'fine', '10m': 'mid', '1h': 'coarse' }[monRange] || 'fine';
|
|
3439
|
+
var hist = data.history || {};
|
|
3440
|
+
var h = Array.isArray(hist) ? hist : (hist[rangeKey] || []);
|
|
3441
|
+
var sys = s.system || {};
|
|
3442
|
+
var lh = (s.stats && s.stats.lastHour) || {};
|
|
3443
|
+
var recentErrs = (s.stats && s.stats.recentErrors) || [];
|
|
3444
|
+
var errRate = (lh.received > 0) ? ((lh.errors / lh.received) * 100).toFixed(1) + '%' : '0%';
|
|
3445
|
+
var agents = s.agents || [];
|
|
3446
|
+
var connected = agents.filter(function (a) { return a.status === 'connected'; }).length;
|
|
3447
|
+
|
|
3448
|
+
// ── Stat cards ──
|
|
3449
|
+
var sysMemPct = (sys.memTotal > 0) ? Math.round((sys.memUsed / sys.memTotal) * 100) : 0;
|
|
3450
|
+
var cards = [
|
|
3451
|
+
['Uptime', fmtDur(s.uptimeMs / 1000)],
|
|
3452
|
+
['消息 (1h)', lh.received || 0],
|
|
3453
|
+
['在线 Agent', connected + '/' + agents.length],
|
|
3454
|
+
['平均响应', Math.round(lh.avgResponseMs || 0) + 'ms'],
|
|
3455
|
+
['错误率', errRate],
|
|
3456
|
+
['进程 CPU', (s.cpuPercent != null ? s.cpuPercent : 0) + '%'],
|
|
3457
|
+
['系统 CPU', (sys.cpuPercent != null ? sys.cpuPercent : 0) + '%'],
|
|
3458
|
+
['进程内存', fmtBytes(s.memory ? s.memory.rss : 0)],
|
|
3459
|
+
['系统内存', sysMemPct + '%'],
|
|
3460
|
+
];
|
|
3461
|
+
$('#mon-cards').innerHTML = cards.map(function (c) {
|
|
3462
|
+
return '<div class="usage-card"><div class="card-value">' + c[1] + '</div><div class="card-label">' + c[0] + '</div></div>';
|
|
3463
|
+
}).join('');
|
|
3464
|
+
|
|
3465
|
+
var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
3466
|
+
var ts = h.map(function (p) { return new Date(p.ts).toLocaleTimeString(); });
|
|
3467
|
+
var css = function (v) { return getComputedStyle(document.documentElement).getPropertyValue(v).trim(); };
|
|
3468
|
+
var cProc = css('--accent'), cSys = css('--orange');
|
|
3469
|
+
|
|
3470
|
+
// ── CPU dual-line:进程 vs 系统 ──
|
|
3471
|
+
monDualLine('mon-cpu-chart', '_monCpu', ts, isDark, 'CPU 占用',
|
|
3472
|
+
[
|
|
3473
|
+
{ name: 'evolclaw 进程', data: h.map(function (p) { return p.procCpu; }), color: cProc },
|
|
3474
|
+
{ name: '整机系统', data: h.map(function (p) { return p.sysCpu != null ? p.sysCpu : null; }), color: cSys },
|
|
3475
|
+
],
|
|
3476
|
+
function (v) { return Number(v).toFixed(1) + '%'; }, [0, 100]);
|
|
3477
|
+
|
|
3478
|
+
// ── Memory dual-line:进程 RSS vs 系统已用 ──
|
|
3479
|
+
monDualLine('mon-mem-chart', '_monMem', ts, isDark, '内存占用',
|
|
3480
|
+
[
|
|
3481
|
+
{ name: 'evolclaw RSS', data: h.map(function (p) { return p.procRss; }), color: cProc },
|
|
3482
|
+
{ name: '系统已用', data: h.map(function (p) { return p.sysMemUsed != null ? p.sysMemUsed : null; }), color: cSys },
|
|
3483
|
+
],
|
|
3484
|
+
function (v) { return fmtBytes(v); }, null);
|
|
3485
|
+
|
|
3486
|
+
// ── Message activity bar chart ──
|
|
3487
|
+
var msgEl = $('#mon-msg-chart');
|
|
3488
|
+
if (msgEl) {
|
|
3489
|
+
if (!window._monMsg) window._monMsg = echarts.init(msgEl, isDark ? 'dark' : null);
|
|
3490
|
+
window._monMsg.setOption({
|
|
3491
|
+
title: { text: '近一小时活动', left: 'center', top: 4, textStyle: { fontSize: 12, color: isDark ? '#e6edf3' : '#1a202c' } },
|
|
3492
|
+
tooltip: { trigger: 'axis' },
|
|
3493
|
+
grid: { top: 36, bottom: 24, left: 44, right: 12 },
|
|
3494
|
+
xAxis: { type: 'category', data: ['Received', 'Completed', 'Errors', 'Interrupts', 'ToolErr'], axisLabel: { fontSize: 9 } },
|
|
3495
|
+
yAxis: { type: 'value', minInterval: 1 },
|
|
3496
|
+
series: [{
|
|
3497
|
+
type: 'bar', barWidth: '45%',
|
|
3498
|
+
data: [
|
|
3499
|
+
{ value: lh.received || 0, itemStyle: { color: css('--accent') } },
|
|
3500
|
+
{ value: lh.completed || 0, itemStyle: { color: css('--green') } },
|
|
3501
|
+
{ value: lh.errors || 0, itemStyle: { color: css('--red') } },
|
|
3502
|
+
{ value: lh.interrupts || 0, itemStyle: { color: css('--orange') } },
|
|
3503
|
+
{ value: lh.toolErrors || 0, itemStyle: { color: css('--blue') } },
|
|
3504
|
+
],
|
|
3505
|
+
}],
|
|
3506
|
+
animation: false,
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
// ── Error breakdown donut ──
|
|
3511
|
+
var errEntries = Object.entries(lh.errorsByType || {});
|
|
3512
|
+
var errEl = $('#mon-err-chart');
|
|
3513
|
+
if (errEl) {
|
|
3514
|
+
if (errEntries.length) {
|
|
3515
|
+
if (!window._monErr) window._monErr = echarts.init(errEl, isDark ? 'dark' : null);
|
|
3516
|
+
window._monErr.setOption({
|
|
3517
|
+
title: { text: '错误分布', left: 'center', top: 4, textStyle: { fontSize: 12, color: isDark ? '#e6edf3' : '#1a202c' } },
|
|
3518
|
+
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
|
3519
|
+
series: [{
|
|
3520
|
+
type: 'pie', radius: ['32%', '64%'], center: ['50%', '56%'],
|
|
3521
|
+
label: { fontSize: 10 },
|
|
3522
|
+
data: errEntries.map(function (e) { return { name: e[0], value: e[1] }; }),
|
|
3523
|
+
}],
|
|
3524
|
+
animation: false,
|
|
3525
|
+
});
|
|
3526
|
+
} else {
|
|
3527
|
+
if (window._monErr) { window._monErr.dispose(); window._monErr = null; }
|
|
3528
|
+
errEl.innerHTML = '<div class="empty" style="padding:24px;font-size:12px">近一小时无错误</div>';
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
// ── Per-agent table ──
|
|
3533
|
+
var dotMap = { connected: 'on', reconnecting: 'idle', aid_blocked: 'idle', kicked: 'off', kicked_no_retry: 'off', failed: 'off', disabled: 'off' };
|
|
3534
|
+
$('#mon-agent-table-wrap').innerHTML =
|
|
3535
|
+
'<div class="mon-section-title">各 Agent 运行状态</div>' +
|
|
3536
|
+
'<table class="usage-table"><thead><tr>' +
|
|
3537
|
+
'<th>Agent</th><th>状态</th><th>收</th><th>发</th><th>流入</th><th>流出</th><th>对端</th><th>队列</th><th>处理中</th>' +
|
|
3538
|
+
'</tr></thead><tbody>' +
|
|
3539
|
+
(agents.length ? agents.map(function (a) {
|
|
3540
|
+
var st = a.stats || {};
|
|
3541
|
+
var dot = dotMap[a.status] || 'off';
|
|
3542
|
+
return '<tr>' +
|
|
3543
|
+
'<td title="' + esc(a.aid) + '">' + esc(a.agentName || shortAid(a.aid)) + '</td>' +
|
|
3544
|
+
'<td><span class="dot ' + dot + '"></span>' + esc(a.status) + '</td>' +
|
|
3545
|
+
'<td>' + (st.messagesReceived || 0) + '</td>' +
|
|
3546
|
+
'<td>' + (st.messagesSent || 0) + '</td>' +
|
|
3547
|
+
'<td>' + fmtBytes(st.bytesReceived || 0) + '</td>' +
|
|
3548
|
+
'<td>' + fmtBytes(st.bytesSent || 0) + '</td>' +
|
|
3549
|
+
'<td>' + (st.uniquePeerCount || 0) + '</td>' +
|
|
3550
|
+
'<td>' + (st.queued || 0) + '</td>' +
|
|
3551
|
+
'<td>' + (st.processing ? '⚙ ' + st.processing : 0) + '</td>' +
|
|
3552
|
+
'</tr>';
|
|
3553
|
+
}).join('') : '<tr><td colspan="9" style="text-align:center;color:var(--dim)">暂无 Agent</td></tr>') +
|
|
3554
|
+
'</tbody></table>';
|
|
3555
|
+
|
|
3556
|
+
// ── Recent errors(替换原 Channels 位置)──
|
|
3557
|
+
$('#mon-err-list').innerHTML =
|
|
3558
|
+
'<div class="mon-section-title">最近错误 <span class="mon-section-sub">(最多 50 条)</span></div>' +
|
|
3559
|
+
(recentErrs.length
|
|
3560
|
+
? '<div class="mon-err-rows">' + recentErrs.map(function (e) {
|
|
3561
|
+
var who = e.agentName ? shortAid(e.agentName) : '—';
|
|
3562
|
+
var tag = e.kind === 'tool'
|
|
3563
|
+
? '<span class="mon-err-tag tag-tool">工具</span>'
|
|
3564
|
+
: '<span class="mon-err-tag tag-task">任务</span>';
|
|
3565
|
+
var label = e.kind === 'tool' ? (e.toolName || 'tool') : (e.errorType || 'error');
|
|
3566
|
+
var msg = e.message ? esc(e.message) : '';
|
|
3567
|
+
return '<div class="mon-err-row">' +
|
|
3568
|
+
'<span class="mon-err-time">' + fmtAgo(e.ts) + '</span>' +
|
|
3569
|
+
tag +
|
|
3570
|
+
'<span class="mon-err-aid" title="' + esc(e.agentName || '') + '">' + esc(who) + '</span>' +
|
|
3571
|
+
'<span class="mon-err-kind">' + esc(label) + '</span>' +
|
|
3572
|
+
'<span class="mon-err-msg" title="' + msg + '">' + msg + '</span>' +
|
|
3573
|
+
'</div>';
|
|
3574
|
+
}).join('') + '</div>'
|
|
3575
|
+
: '<div class="empty" style="padding:24px;font-size:12px">暂无错误记录</div>');
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
// 双线时序图(进程 + 系统)。series: [{name,data,color}]
|
|
3579
|
+
function monDualLine(elId, varKey, times, isDark, title, series, fmtY, yRange) {
|
|
3580
|
+
var el = $('#' + elId);
|
|
3581
|
+
if (!el) return;
|
|
3582
|
+
if (!window[varKey]) window[varKey] = echarts.init(el, isDark ? 'dark' : null);
|
|
3583
|
+
window[varKey].setOption({
|
|
3584
|
+
title: { text: title, left: 'center', top: 4, textStyle: { fontSize: 12, color: isDark ? '#e6edf3' : '#1a202c' } },
|
|
3585
|
+
legend: { show: false },
|
|
3586
|
+
tooltip: {
|
|
3587
|
+
trigger: 'axis',
|
|
3588
|
+
formatter: function (params) {
|
|
3589
|
+
var lines = [params[0].axisValue];
|
|
3590
|
+
params.forEach(function (pt) {
|
|
3591
|
+
if (pt.value == null) return;
|
|
3592
|
+
lines.push(pt.marker + pt.seriesName + ': ' + (fmtY ? fmtY(pt.value) : pt.value));
|
|
3593
|
+
});
|
|
3594
|
+
return lines.join('<br/>');
|
|
3595
|
+
},
|
|
3596
|
+
},
|
|
3597
|
+
grid: { top: 36, bottom: 24, left: 56, right: 12 },
|
|
3598
|
+
xAxis: { type: 'category', data: times, boundaryGap: false, axisLabel: { fontSize: 9 } },
|
|
3599
|
+
yAxis: {
|
|
3600
|
+
type: 'value',
|
|
3601
|
+
min: (yRange ? yRange[0] : 0),
|
|
3602
|
+
max: (yRange ? yRange[1] : undefined),
|
|
3603
|
+
axisLabel: { formatter: fmtY ? function (v) { return fmtY(v); } : '{value}' },
|
|
3604
|
+
},
|
|
3605
|
+
series: series.map(function (sr) {
|
|
3606
|
+
return {
|
|
3607
|
+
name: sr.name, type: 'line', data: sr.data, smooth: true, symbol: 'none',
|
|
3608
|
+
connectNulls: true,
|
|
3609
|
+
lineStyle: { width: 2, color: sr.color },
|
|
3610
|
+
areaStyle: { color: sr.color, opacity: 0.08 },
|
|
3611
|
+
itemStyle: { color: sr.color },
|
|
3612
|
+
};
|
|
3613
|
+
}),
|
|
3614
|
+
animation: false,
|
|
3615
|
+
});
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
window.addEventListener('DOMContentLoaded', async () => {
|
|
1448
3619
|
initTheme();
|
|
1449
3620
|
initPairUI();
|
|
3621
|
+
initMsgTipFloat();
|
|
3622
|
+
|
|
3623
|
+
// 初始化语言切换
|
|
3624
|
+
const langBtn = $('#lang-btn');
|
|
3625
|
+
if (langBtn) {
|
|
3626
|
+
langBtn.addEventListener('click', toggleLang);
|
|
3627
|
+
}
|
|
3628
|
+
updateI18n(); // 应用当前语言
|
|
3629
|
+
|
|
3630
|
+
// 已有 token 直接进;否则先试本地直连免配对,失败再回落配对页。
|
|
3631
|
+
if (!localStorage.getItem(TOKEN_KEY)) {
|
|
3632
|
+
await tryLocalAutoPair();
|
|
3633
|
+
}
|
|
1450
3634
|
if (localStorage.getItem(TOKEN_KEY)) {
|
|
1451
3635
|
showApp();
|
|
1452
3636
|
startApp();
|
|
1453
|
-
loadUsageDashboard();
|
|
1454
|
-
loadUsageOverview();
|
|
1455
3637
|
initUsageSubtabs();
|
|
1456
3638
|
} else {
|
|
1457
3639
|
showPairPage();
|