evolclaw-web 1.2.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/server.js +182 -23
- package/dist/sources/gateway.js +44 -0
- package/dist/sources/session.js +1 -1
- package/dist/sources/stats.js +269 -136
- package/dist/static/app.js +1940 -270
- package/dist/static/index.html +122 -57
- package/dist/static/style.css +844 -19
- package/package.json +1 -1
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, monitor: 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
|
};
|
|
@@ -156,6 +768,7 @@ let monRange = '2m'; // Monitor 时间窗口:2m / 10m / 1h
|
|
|
156
768
|
|
|
157
769
|
function switchView(view) {
|
|
158
770
|
currentView = view;
|
|
771
|
+
localStorage.setItem(VIEW_KEY, view);
|
|
159
772
|
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.view === view));
|
|
160
773
|
document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id === 'view-' + view));
|
|
161
774
|
// 切换时按当前选择恢复订阅
|
|
@@ -165,6 +778,7 @@ function switchView(view) {
|
|
|
165
778
|
else if (view === 'system') subscribe('system', {});
|
|
166
779
|
else if (view === 'triggers') subscribe('triggers', { agent: trigSel.agent });
|
|
167
780
|
else if (view === 'monitor') subscribe('monitor', { range: monRange });
|
|
781
|
+
else if (view === 'gateway') subscribe('gateway', {});
|
|
168
782
|
else subscribe('agents', {});
|
|
169
783
|
if (state[view]) renderView(view);
|
|
170
784
|
}
|
|
@@ -183,6 +797,7 @@ function renderView(view) {
|
|
|
183
797
|
else if (view === 'system') renderSystem(state.system);
|
|
184
798
|
else if (view === 'triggers') renderTriggers(state.triggers);
|
|
185
799
|
else if (view === 'monitor') renderMonitor(state.monitor);
|
|
800
|
+
else if (view === 'gateway') renderGateway(state.gateway);
|
|
186
801
|
}
|
|
187
802
|
|
|
188
803
|
// ── 工具 ──
|
|
@@ -246,25 +861,38 @@ let _agSubtab = 'enabled'; // 'enabled' | 'disabled'
|
|
|
246
861
|
// stopped → connected(仅首次连接无消息时) → idle(收到第一条后) → working → idle ...
|
|
247
862
|
function agentStateBadge(s, agStatus, connStatus) {
|
|
248
863
|
if (agStatus === 'stopped' || connStatus === 'disconnected' || connStatus === 'failed')
|
|
249
|
-
return
|
|
864
|
+
return `<span class="state-badge stopped">${t('status.stopped')}</span>`;
|
|
250
865
|
if (connStatus === 'reconnecting')
|
|
251
|
-
return
|
|
866
|
+
return `<span class="state-badge stopped">${t('status.reconnecting')}</span>`;
|
|
252
867
|
if ((s.processing || 0) > 0)
|
|
253
|
-
return
|
|
868
|
+
return `<span class="state-badge working">${t('status.working')}</span>`;
|
|
254
869
|
// 收到过消息 → 永远是 idle,不再回到 connected
|
|
255
870
|
if ((s.messagesReceived || 0) > 0 || (s.messagesSent || 0) > 0)
|
|
256
|
-
return
|
|
257
|
-
return
|
|
871
|
+
return `<span class="state-badge idle">${t('status.idle')}</span>`;
|
|
872
|
+
return `<span class="state-badge connected">${t('status.connected')}</span>`;
|
|
258
873
|
}
|
|
259
874
|
|
|
260
875
|
// 发送方式图标标记
|
|
261
|
-
const MSG_KIND_META = {
|
|
262
|
-
|
|
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) {
|
|
263
890
|
let h = '';
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
if (
|
|
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>`;
|
|
268
896
|
return h;
|
|
269
897
|
}
|
|
270
898
|
|
|
@@ -273,7 +901,7 @@ function agentPreviewHtml(s) {
|
|
|
273
901
|
const clip = (t) => esc(String(t).replace(/\n/g, ' ').slice(0, 80));
|
|
274
902
|
const line = (dir, peer, text, kind, encrypt, chatmode) => {
|
|
275
903
|
const arrow = dir === 'in' ? '<span class="arrow-in">↓</span>' : '<span class="arrow-out">↑</span>';
|
|
276
|
-
const tags = msgTagsHtml(kind, encrypt, chatmode);
|
|
904
|
+
const tags = msgTagsHtml(kind, encrypt, chatmode, dir);
|
|
277
905
|
const peerHtml = peer ? `<span class="peer">${esc(shortAid(peer))}</span>: ` : '';
|
|
278
906
|
const textCls = dir === 'in' ? 'text-in' : 'text-out';
|
|
279
907
|
return `${arrow}${tags ? ' ' + tags + ' ' : ' '}${peerHtml}<span class="${textCls}">${clip(text)}</span>`;
|
|
@@ -289,24 +917,94 @@ function agentPreviewHtml(s) {
|
|
|
289
917
|
return '';
|
|
290
918
|
}
|
|
291
919
|
|
|
292
|
-
// HTML tooltip(最近 N
|
|
920
|
+
// HTML tooltip(最近 N 轮):时间 + 彩色箭头 + 方式 + 对端 + 文字
|
|
921
|
+
// 渲染为隐藏的内容持有节点(.msg-tip-src);实际展示由 initMsgTipFloat 的浮层负责
|
|
293
922
|
function recentMsgTooltipHtml(recent) {
|
|
294
923
|
if (!recent || !recent.length) return '';
|
|
295
|
-
let h = '<div class="msg-tip">';
|
|
924
|
+
let h = '<div class="msg-tip-src">';
|
|
296
925
|
for (const m of recent) {
|
|
297
926
|
const rcls = m.dir === 'in' ? 'tip-row-in' : 'tip-row-out';
|
|
298
927
|
const arrow = m.dir === 'in' ? '↓' : '↑';
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
const
|
|
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>` : '';
|
|
303
933
|
const peer = m.peer ? esc(shortAid(m.peer)) : '';
|
|
304
|
-
const text = esc(String(m.text).replace(/\n/g, ' ').slice(0,
|
|
305
|
-
|
|
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>`;
|
|
306
937
|
}
|
|
307
938
|
return h + '</div>';
|
|
308
939
|
}
|
|
309
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
|
+
|
|
310
1008
|
// 顶部统计条:Gateway / AIDs total·connected·offline / Messages ↓↑ / Traffic ↓↑ / Version·PID·Uptime
|
|
311
1009
|
function agentsStatsBar(data, aids, stats) {
|
|
312
1010
|
const connected = aids.filter(a => (a.status || 'connected') === 'connected').length;
|
|
@@ -324,12 +1022,12 @@ function agentsStatsBar(data, aids, stats) {
|
|
|
324
1022
|
const ver = data.version || '—';
|
|
325
1023
|
|
|
326
1024
|
let h = '<div class="agents-stats">';
|
|
327
|
-
h += `<span class="sg"><span class="sg-k"
|
|
328
|
-
h += `<span class="sg"><span class="sg-k"
|
|
329
|
-
`${offline ? ` · <span class="num-off">${offline}
|
|
330
|
-
h += `<span class="sg"><span class="sg-k"
|
|
331
|
-
h += `<span class="sg"><span class="sg-k"
|
|
332
|
-
h += `<span class="sg"><span class="sg-k"
|
|
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>`;
|
|
333
1031
|
h += '</div>';
|
|
334
1032
|
return h;
|
|
335
1033
|
}
|
|
@@ -337,21 +1035,21 @@ function agentsStatsBar(data, aids, stats) {
|
|
|
337
1035
|
// 操作列 HTML(启用页):停止/启动 + 清空队列(conditional) + ···(禁用/重载/编辑/md/删除)
|
|
338
1036
|
function agentOpsHtml(aid, ag, s) {
|
|
339
1037
|
if (_agentOps.has(aid)) {
|
|
340
|
-
return `<div class="agent-ops agent-ops-busy"><span class="ops-busy-label">${esc(_agentOps.get(aid) || '
|
|
1038
|
+
return `<div class="agent-ops agent-ops-busy"><span class="ops-busy-label">${esc(_agentOps.get(aid) || t('common.operating'))}</span></div>`;
|
|
341
1039
|
}
|
|
342
1040
|
const queued = s.queued || 0;
|
|
343
1041
|
const running = ag.status === 'running';
|
|
344
1042
|
let h = `<div class="agent-ops" data-aid="${esc(aid)}" data-status="${esc(ag.status)}">`;
|
|
345
|
-
if (running) h += `<button class="ctrl-btn ops-stop" data-op="stop"
|
|
346
|
-
else h += `<button class="ctrl-btn ops-start" data-op="start"
|
|
347
|
-
if (queued > 0) h += `<button class="ctrl-btn ops-clear-queue" data-op="clear-queue" title="
|
|
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>`;
|
|
348
1046
|
h += `<div class="ops-more"><button class="ctrl-btn ops-more-btn" data-op="more">···</button>` +
|
|
349
1047
|
`<div class="ops-dropdown">` +
|
|
350
|
-
`<button class="ops-dd-item" data-op="toggle"
|
|
351
|
-
`<button class="ops-dd-item" data-op="reload"
|
|
352
|
-
`<button class="ops-dd-item" data-op="edit"
|
|
353
|
-
`<a class="ops-dd-item" href="https://${esc(aid)}/agent.md" target="_blank" rel="noopener"
|
|
354
|
-
`<button class="ops-dd-item danger" data-op="delete"
|
|
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>` +
|
|
355
1053
|
`</div></div>`;
|
|
356
1054
|
h += '</div>';
|
|
357
1055
|
return h;
|
|
@@ -359,7 +1057,7 @@ function agentOpsHtml(aid, ag, s) {
|
|
|
359
1057
|
|
|
360
1058
|
function renderAgents(data) {
|
|
361
1059
|
const el = $('#view-agents');
|
|
362
|
-
if (!data) { el.innerHTML =
|
|
1060
|
+
if (!data) { el.innerHTML = `<div class="empty">${t('common.loading')}</div>`; return; }
|
|
363
1061
|
if (el.querySelector('.ops-more.open')) return;
|
|
364
1062
|
|
|
365
1063
|
const allAgents = data.agents || [];
|
|
@@ -375,27 +1073,27 @@ function renderAgents(data) {
|
|
|
375
1073
|
// 子标签栏
|
|
376
1074
|
let html = '<div class="agents-toolbar">' +
|
|
377
1075
|
`<div class="ag-subtabs">` +
|
|
378
|
-
`<button class="ag-subtab${_agSubtab === 'enabled' ? ' active' : ''}" data-subtab="enabled"
|
|
379
|
-
`<button class="ag-subtab${_agSubtab === 'disabled' ? ' active' : ''}" data-subtab="disabled"
|
|
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>` +
|
|
380
1078
|
`</div>` +
|
|
381
|
-
`<button class="ctrl-btn" id="agent-new-btn"
|
|
1079
|
+
`<button class="ctrl-btn" id="agent-new-btn">${t('action.new')}</button>` +
|
|
382
1080
|
'</div>';
|
|
383
1081
|
|
|
384
1082
|
if (!data.daemonRunning) {
|
|
385
|
-
html +=
|
|
1083
|
+
html += `<div class="banner">${t('agents.daemonStopped')}</div>`;
|
|
386
1084
|
}
|
|
387
1085
|
|
|
388
1086
|
if (_agSubtab === 'disabled') {
|
|
389
1087
|
const disabledAgents = allAgents.filter(ag => ag.status === 'disabled');
|
|
390
1088
|
if (!disabledAgents.length) {
|
|
391
|
-
html +=
|
|
1089
|
+
html += `<div class="empty">${t('agents.empty.disabled')}</div>`;
|
|
392
1090
|
} else {
|
|
393
|
-
html +=
|
|
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>`;
|
|
394
1092
|
for (const ag of disabledAgents) {
|
|
395
1093
|
const busy = _agentOps.has(ag.aid);
|
|
396
1094
|
const ops = busy
|
|
397
|
-
? `<div class="agent-ops agent-ops-busy"><span class="ops-busy-label">${esc(_agentOps.get(ag.aid) || '
|
|
398
|
-
: `<div class="agent-ops" data-aid="${esc(ag.aid)}" data-status="disabled"><button class="ctrl-btn ops-enable" data-op="toggle"
|
|
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>`;
|
|
399
1097
|
html += `<tr class="ag-main">` +
|
|
400
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>` +
|
|
401
1099
|
`<td style="font-size:11px;font-family:monospace">${esc(ag.projectPath || '—')}</td>` +
|
|
@@ -417,15 +1115,15 @@ function renderAgents(data) {
|
|
|
417
1115
|
const enabledAgents = allAgents.filter(ag => ag.status !== 'disabled')
|
|
418
1116
|
.sort((a, b) => totalMsgs(b) - totalMsgs(a));
|
|
419
1117
|
if (!enabledAgents.length) {
|
|
420
|
-
html +=
|
|
1118
|
+
html += `<div class="empty">${t('agents.empty.enabled')}</div>`;
|
|
421
1119
|
el.innerHTML = html;
|
|
422
1120
|
bindAgentsEvents(el);
|
|
423
1121
|
return;
|
|
424
1122
|
}
|
|
425
1123
|
|
|
426
1124
|
html += '<table><thead><tr>' +
|
|
427
|
-
'
|
|
428
|
-
'
|
|
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>` +
|
|
429
1127
|
'</tr></thead><tbody>';
|
|
430
1128
|
|
|
431
1129
|
for (const ag of enabledAgents) {
|
|
@@ -504,17 +1202,17 @@ function groupLabel(g) {
|
|
|
504
1202
|
|
|
505
1203
|
function renderCache(data) {
|
|
506
1204
|
const el = $('#view-cache');
|
|
507
|
-
if (!data) { el.innerHTML =
|
|
1205
|
+
if (!data) { el.innerHTML = `<div class="empty">${t('common.loading')}</div>`; return; }
|
|
508
1206
|
if (!data.daemonRunning) {
|
|
509
|
-
el.innerHTML =
|
|
1207
|
+
el.innerHTML = `<div class="banner">${t('cache.daemonStopped')}</div>`;
|
|
510
1208
|
return;
|
|
511
1209
|
}
|
|
512
1210
|
if (!data.supported || !data.stats) {
|
|
513
|
-
el.innerHTML =
|
|
1211
|
+
el.innerHTML = `<div class="banner">${t('cache.notSupported')}</div>`;
|
|
514
1212
|
return;
|
|
515
1213
|
}
|
|
516
1214
|
const s = data.stats;
|
|
517
|
-
const
|
|
1215
|
+
const tot = s.totals;
|
|
518
1216
|
const occ = s.occupancy || {};
|
|
519
1217
|
// 全部组占用合计
|
|
520
1218
|
let totalBytes = 0;
|
|
@@ -523,23 +1221,23 @@ function renderCache(data) {
|
|
|
523
1221
|
let html = '';
|
|
524
1222
|
|
|
525
1223
|
// ① 总览卡片
|
|
526
|
-
const rate = hitRate(
|
|
1224
|
+
const rate = hitRate(tot);
|
|
527
1225
|
html += '<div class="cache-cards">';
|
|
528
|
-
html += card('
|
|
529
|
-
html += card('
|
|
530
|
-
html += card('
|
|
531
|
-
html += card('
|
|
532
|
-
html += card('
|
|
533
|
-
html += card('
|
|
534
|
-
html += card('
|
|
535
|
-
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));
|
|
536
1234
|
html += '</div>';
|
|
537
1235
|
|
|
538
1236
|
// ② 按 group 表(每组命中率 + 占用 + 容量水位)
|
|
539
|
-
html +=
|
|
1237
|
+
html += `<h3 class="cache-h">${t('cache.section.byGroup')}</h3>`;
|
|
540
1238
|
html += '<table><thead><tr>' +
|
|
541
|
-
'
|
|
542
|
-
'
|
|
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>` +
|
|
543
1241
|
'</tr></thead><tbody>';
|
|
544
1242
|
const groups = Object.keys(s.byGroup).sort((a, b) => (s.byGroup[b].gets || 0) - (s.byGroup[a].gets || 0));
|
|
545
1243
|
for (const g of groups) {
|
|
@@ -565,11 +1263,15 @@ function renderCache(data) {
|
|
|
565
1263
|
html += '</tbody></table>';
|
|
566
1264
|
|
|
567
1265
|
// ③ 按 policy 表
|
|
568
|
-
html +=
|
|
1266
|
+
html += `<h3 class="cache-h">${t('cache.section.byPolicy')}</h3>`;
|
|
569
1267
|
html += '<table><thead><tr>' +
|
|
570
|
-
'
|
|
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>` +
|
|
571
1269
|
'</tr></thead><tbody>';
|
|
572
|
-
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
|
+
};
|
|
573
1275
|
for (const pol of ['on-reload', 'mtime', 'manual']) {
|
|
574
1276
|
const c = s.byPolicy[pol];
|
|
575
1277
|
if (!c || !c.gets) continue;
|
|
@@ -583,8 +1285,7 @@ function renderCache(data) {
|
|
|
583
1285
|
}
|
|
584
1286
|
html += '</tbody></table>';
|
|
585
1287
|
|
|
586
|
-
html +=
|
|
587
|
-
'渲染后结果(按 vars)不缓存,故不在此列。</div>';
|
|
1288
|
+
html += `<div class="cache-note">${t('cache.note')}</div>`;
|
|
588
1289
|
|
|
589
1290
|
el.innerHTML = html;
|
|
590
1291
|
}
|
|
@@ -605,7 +1306,7 @@ function renderMsg(data) {
|
|
|
605
1306
|
const messages = data.messages || [];
|
|
606
1307
|
|
|
607
1308
|
// 左:AID 列表
|
|
608
|
-
let aidsHtml =
|
|
1309
|
+
let aidsHtml = `<div class="col-title">${t('messages.colTitle.aid')}</div>`;
|
|
609
1310
|
for (const a of aids) {
|
|
610
1311
|
const sel = a.aid === msgSel.aid ? ' sel' : '';
|
|
611
1312
|
aidsHtml += `<div class="list-item${sel}" data-aid="${esc(a.aid)}">` +
|
|
@@ -618,10 +1319,10 @@ function renderMsg(data) {
|
|
|
618
1319
|
});
|
|
619
1320
|
|
|
620
1321
|
// 中:对端列表
|
|
621
|
-
let peersHtml =
|
|
1322
|
+
let peersHtml = `<div class="col-title">${t('messages.colTitle.peers')}</div>`;
|
|
622
1323
|
if (msgSel.aid) {
|
|
623
1324
|
const allSel = msgSel.peer === null ? ' sel' : '';
|
|
624
|
-
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>` +
|
|
625
1326
|
`<div class="sub">${peers.length} peers</div></div>`;
|
|
626
1327
|
for (const p of peers) {
|
|
627
1328
|
const sel = p.peerId === msgSel.peer ? ' sel' : '';
|
|
@@ -630,7 +1331,7 @@ function renderMsg(data) {
|
|
|
630
1331
|
`<div class="sub">↓${p.inbound} ↑${p.outbound} · ${fmtAgo(p.lastAt)}</div></div>`;
|
|
631
1332
|
}
|
|
632
1333
|
} else {
|
|
633
|
-
peersHtml +=
|
|
1334
|
+
peersHtml += `<div class="empty">${t('messages.empty.selectAid')}</div>`;
|
|
634
1335
|
}
|
|
635
1336
|
$('#msg-peers').innerHTML = peersHtml;
|
|
636
1337
|
$('#msg-peers').querySelectorAll('.list-item').forEach(item => {
|
|
@@ -639,7 +1340,7 @@ function renderMsg(data) {
|
|
|
639
1340
|
|
|
640
1341
|
// 右:消息流
|
|
641
1342
|
const stream = $('#msg-stream');
|
|
642
|
-
if (!msgSel.aid) { stream.innerHTML =
|
|
1343
|
+
if (!msgSel.aid) { stream.innerHTML = `<div class="empty">${t('messages.empty.selectToView')}</div>`; return; }
|
|
643
1344
|
const atBottom = stream.scrollHeight - stream.scrollTop - stream.clientHeight < 60;
|
|
644
1345
|
let msgHtml = '';
|
|
645
1346
|
for (const m of messages) {
|
|
@@ -647,15 +1348,19 @@ function renderMsg(data) {
|
|
|
647
1348
|
const arrow = m.dir === 'in' ? '↓' : '↑';
|
|
648
1349
|
const from = shortAid(m.from), to = shortAid(m.to);
|
|
649
1350
|
const tags = [];
|
|
650
|
-
if (m.chatType === 'group') tags.push('
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
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('');
|
|
654
1359
|
msgHtml += `<div class="bubble ${cls}">` +
|
|
655
1360
|
`<div class="meta">${fmtTime(m.ts)} ${arrow} ${esc(from)}→${esc(to)}${tagHtml}</div>` +
|
|
656
1361
|
`<div class="body">${esc(m.content)}</div></div>`;
|
|
657
1362
|
}
|
|
658
|
-
stream.innerHTML = msgHtml ||
|
|
1363
|
+
stream.innerHTML = msgHtml || `<div class="empty">${t('messages.empty.noMessages')}</div>`;
|
|
659
1364
|
if (atBottom) stream.scrollTop = stream.scrollHeight;
|
|
660
1365
|
}
|
|
661
1366
|
|
|
@@ -947,8 +1652,8 @@ function setAgentOp(aid, label) {
|
|
|
947
1652
|
if (ag.status === 'disabled') {
|
|
948
1653
|
// 禁用页:只有启用按钮 / 操作中态
|
|
949
1654
|
cell.innerHTML = _agentOps.has(aid)
|
|
950
|
-
? `<div class="agent-ops agent-ops-busy"><span class="ops-busy-label">${esc(_agentOps.get(aid) || '
|
|
951
|
-
: `<div class="agent-ops" data-aid="${esc(aid)}" data-status="disabled"><button class="ctrl-btn ops-enable" data-op="toggle"
|
|
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>`;
|
|
952
1657
|
} else {
|
|
953
1658
|
const statsByAid = {};
|
|
954
1659
|
for (const s of (state.agents.stats || [])) statsByAid[s.aid] = s;
|
|
@@ -1018,29 +1723,29 @@ async function withAgentOp(aid, label, fn) {
|
|
|
1018
1723
|
}
|
|
1019
1724
|
|
|
1020
1725
|
async function agentOpReload(aid, force = false) {
|
|
1021
|
-
await withAgentOp(aid, '
|
|
1726
|
+
await withAgentOp(aid, t('agents.op.reloading'), async () => {
|
|
1022
1727
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'reload', args: { aid, force } }));
|
|
1023
1728
|
if (r.error?.code === 'BUSY') {
|
|
1024
|
-
if (confirm(r.error.message + '\n
|
|
1729
|
+
if (confirm(r.error.message + '\n' + t('agents.op.confirmReload'))) { setAgentOp(aid, null); return agentOpReload(aid, true); }
|
|
1025
1730
|
return;
|
|
1026
1731
|
}
|
|
1027
1732
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1028
|
-
toast('
|
|
1733
|
+
toast(t('agents.op.reloaded'));
|
|
1029
1734
|
subscribe('agents', {});
|
|
1030
1735
|
});
|
|
1031
1736
|
}
|
|
1032
1737
|
|
|
1033
1738
|
async function agentOpToggle(aid, status) {
|
|
1034
1739
|
const action = status === 'disabled' ? 'enable' : 'disable';
|
|
1035
|
-
const label = action === 'disable' ? '
|
|
1740
|
+
const label = action === 'disable' ? t('agents.op.disabling') : t('agents.op.enabling');
|
|
1036
1741
|
await withAgentOp(aid, label, async () => {
|
|
1037
1742
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action, args: { aid } }));
|
|
1038
1743
|
if (r.error?.code === 'BUSY') {
|
|
1039
|
-
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')}?`)) {
|
|
1040
1745
|
const r2 = mResp(await menuSend({ type: 'menu.action', name: 'agent', action, args: { aid, force: true } }));
|
|
1041
1746
|
if (r2.error) toast(r2.error.message || r2.error.code, true);
|
|
1042
1747
|
else {
|
|
1043
|
-
toast(
|
|
1748
|
+
toast(action === 'disable' ? t('agents.op.disabled') : t('agents.op.enabled'));
|
|
1044
1749
|
// 禁用后立即切到禁用页;启用后等数据刷新(agent 需先完成启动才移到启用页)
|
|
1045
1750
|
if (action === 'disable') _agSubtab = 'disabled';
|
|
1046
1751
|
subscribe('agents', {});
|
|
@@ -1049,55 +1754,55 @@ async function agentOpToggle(aid, status) {
|
|
|
1049
1754
|
return;
|
|
1050
1755
|
}
|
|
1051
1756
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1052
|
-
toast(
|
|
1757
|
+
toast(action === 'disable' ? t('agents.op.disabled') : t('agents.op.enabled'));
|
|
1053
1758
|
if (action === 'disable') _agSubtab = 'disabled';
|
|
1054
1759
|
subscribe('agents', {});
|
|
1055
1760
|
});
|
|
1056
1761
|
}
|
|
1057
1762
|
|
|
1058
1763
|
async function agentOpDelete(aid) {
|
|
1059
|
-
if (!confirm(
|
|
1764
|
+
if (!confirm(t('agents.op.confirmDelete').replace('{aid}', aid))) return;
|
|
1060
1765
|
const purge = confirm('同时清除 agent 数据目录?');
|
|
1061
|
-
await withAgentOp(aid, '
|
|
1766
|
+
await withAgentOp(aid, t('agents.op.deleting'), async () => {
|
|
1062
1767
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'delete', args: { aid, purge } }));
|
|
1063
1768
|
if (r.error?.code === 'BUSY') {
|
|
1064
|
-
if (confirm(r.error.message + '\n
|
|
1769
|
+
if (confirm(r.error.message + '\n' + t('agents.op.confirmForceDelete'))) {
|
|
1065
1770
|
const r2 = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'delete', args: { aid, purge, force: true } }));
|
|
1066
1771
|
if (r2.error) toast(r2.error.message || r2.error.code, true);
|
|
1067
|
-
else { toast('
|
|
1772
|
+
else { toast(t('agents.op.deleted')); subscribe('agents', {}); }
|
|
1068
1773
|
}
|
|
1069
1774
|
return;
|
|
1070
1775
|
}
|
|
1071
1776
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1072
|
-
toast('
|
|
1777
|
+
toast(t('agents.op.deleted'));
|
|
1073
1778
|
subscribe('agents', {});
|
|
1074
1779
|
});
|
|
1075
1780
|
}
|
|
1076
1781
|
|
|
1077
1782
|
async function agentOpClearQueue(aid) {
|
|
1078
|
-
if (!confirm(
|
|
1079
|
-
await withAgentOp(aid, '
|
|
1783
|
+
if (!confirm(t('agents.op.confirmClearQueue').replace('{aid}', aid))) return;
|
|
1784
|
+
await withAgentOp(aid, t('common.operating'), async () => {
|
|
1080
1785
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'queue-clear', args: { aid } }));
|
|
1081
1786
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1082
|
-
toast(`✓
|
|
1787
|
+
toast(`✓ ${r.data?.cleared ?? 0} messages cleared`);
|
|
1083
1788
|
subscribe('agents', {});
|
|
1084
1789
|
});
|
|
1085
1790
|
}
|
|
1086
1791
|
|
|
1087
1792
|
async function agentOpStop(aid) {
|
|
1088
|
-
await withAgentOp(aid, '
|
|
1793
|
+
await withAgentOp(aid, t('agents.op.stopping'), async () => {
|
|
1089
1794
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'stop', args: { aid } }));
|
|
1090
1795
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1091
|
-
toast('
|
|
1796
|
+
toast(t('agents.op.stopped'));
|
|
1092
1797
|
subscribe('agents', {});
|
|
1093
1798
|
});
|
|
1094
1799
|
}
|
|
1095
1800
|
|
|
1096
1801
|
async function agentOpStart(aid) {
|
|
1097
|
-
await withAgentOp(aid, '
|
|
1802
|
+
await withAgentOp(aid, t('agents.op.starting'), async () => {
|
|
1098
1803
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'start', args: { aid } }));
|
|
1099
1804
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1100
|
-
toast('
|
|
1805
|
+
toast(t('agents.op.started'));
|
|
1101
1806
|
subscribe('agents', {});
|
|
1102
1807
|
});
|
|
1103
1808
|
}
|
|
@@ -1136,7 +1841,7 @@ async function agentOpNew() {
|
|
|
1136
1841
|
}
|
|
1137
1842
|
|
|
1138
1843
|
async function agentOpEdit(aid) {
|
|
1139
|
-
await withAgentOp(aid, '
|
|
1844
|
+
await withAgentOp(aid, t('common.operating'), async () => {
|
|
1140
1845
|
const qr = await menuSend({ type: 'menu.query', name: 'agent', args: { aid } });
|
|
1141
1846
|
const q = mResp(qr);
|
|
1142
1847
|
if (q.error) { toast(q.error.message || q.error.code, true); return; }
|
|
@@ -1148,10 +1853,10 @@ async function agentOpEdit(aid) {
|
|
|
1148
1853
|
if (projectRaw !== null) patch.projects = { defaultPath: projectRaw };
|
|
1149
1854
|
if (ownersRaw !== null) patch.owners = ownersRaw.split(',').map(s => s.trim()).filter(Boolean);
|
|
1150
1855
|
if (!Object.keys(patch).length) return;
|
|
1151
|
-
setAgentOp(aid, '
|
|
1856
|
+
setAgentOp(aid, t('common.operating'));
|
|
1152
1857
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'update', args: { aid, patch } }));
|
|
1153
1858
|
if (r.error) toast(r.error.message || r.error.code, true);
|
|
1154
|
-
else toast('
|
|
1859
|
+
else toast(t('agents.op.saved'));
|
|
1155
1860
|
});
|
|
1156
1861
|
}
|
|
1157
1862
|
|
|
@@ -1181,7 +1886,7 @@ function agentHealthCard(ag) {
|
|
|
1181
1886
|
for (const c of (ag.channels || [])) chans += channelHealthRow(c);
|
|
1182
1887
|
h += `<div class="ahc-row"><span class="ahc-k">渠道</span><span class="ahc-v">${chans || '<span style="color:var(--dim)">无</span>'}</span></div>`;
|
|
1183
1888
|
// 负载
|
|
1184
|
-
const load = `${ag.processing ?? 0} 处理中 · ${ag.pending ?? 0}
|
|
1889
|
+
const load = `${ag.processing ?? 0} 处理中 · ${ag.pending ?? 0} 待处理`;
|
|
1185
1890
|
h += `<div class="ahc-row"><span class="ahc-k">负载</span><span class="ahc-v">${load}</span></div>`;
|
|
1186
1891
|
// 活动
|
|
1187
1892
|
if (ag.lastActivity) h += `<div class="ahc-row"><span class="ahc-k">活动</span><span class="ahc-v">${fmtAgo(ag.lastActivity)} 前</span></div>`;
|
|
@@ -1288,6 +1993,276 @@ function bindSystemEvents(el, data) {
|
|
|
1288
1993
|
});
|
|
1289
1994
|
}
|
|
1290
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
|
+
|
|
1291
2266
|
// ── Triggers 视图 ──
|
|
1292
2267
|
function trigStatusBadge(status) {
|
|
1293
2268
|
const map = {
|
|
@@ -1380,6 +2355,8 @@ function renderTriggers(data) {
|
|
|
1380
2355
|
|
|
1381
2356
|
function startApp() {
|
|
1382
2357
|
initTabs();
|
|
2358
|
+
// 恢复保存的 tab 视图
|
|
2359
|
+
switchView(currentView);
|
|
1383
2360
|
connect();
|
|
1384
2361
|
$('#logout-btn').onclick = () => {
|
|
1385
2362
|
localStorage.removeItem(TOKEN_KEY);
|
|
@@ -1405,149 +2382,485 @@ function initTheme() {
|
|
|
1405
2382
|
['_monCpu', '_monMem', '_monMsg', '_monErr'].forEach(function (k) {
|
|
1406
2383
|
if (window[k]) { window[k].dispose(); window[k] = null; }
|
|
1407
2384
|
});
|
|
1408
|
-
loadUsageDashboard();
|
|
1409
2385
|
if (currentView === 'monitor') renderMonitor(state.monitor);
|
|
1410
2386
|
};
|
|
1411
2387
|
}
|
|
1412
2388
|
}
|
|
1413
2389
|
|
|
1414
|
-
// ── Usage
|
|
1415
|
-
let _hourlyChart = null;
|
|
1416
|
-
let _modelChart = null;
|
|
1417
|
-
|
|
2390
|
+
// ── Usage 相关函数 ──
|
|
1418
2391
|
function fmtTokens(n) {
|
|
1419
2392
|
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
1420
2393
|
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
|
|
1421
2394
|
return String(n);
|
|
1422
2395
|
}
|
|
1423
2396
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
const
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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;
|
|
1464
2449
|
}
|
|
1465
2450
|
|
|
1466
|
-
//
|
|
1467
|
-
|
|
1468
|
-
if (modelEl && data.top_models && data.top_models.length) {
|
|
1469
|
-
var isDark2 = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
1470
|
-
if (!_modelChart) _modelChart = echarts.init(modelEl, isDark2 ? 'dark' : null);
|
|
1471
|
-
_modelChart.setOption({
|
|
1472
|
-
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
|
1473
|
-
series: [{
|
|
1474
|
-
type: 'pie', radius: ['35%', '70%'], center: ['50%', '55%'],
|
|
1475
|
-
label: { fontSize: 10 },
|
|
1476
|
-
data: data.top_models.map(function(m) { return { name: m.model.split('/').pop(), value: m.total_tokens }; }),
|
|
1477
|
-
}]
|
|
1478
|
-
});
|
|
1479
|
-
}
|
|
2451
|
+
// 保存当前时间范围到全局变量,供详细统计使用
|
|
2452
|
+
window._currentOverviewTimeRange = { fromTs, toTs, rangeType, customFrom, customTo };
|
|
1480
2453
|
|
|
1481
|
-
//
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
return '<tr><td>' + (i + 1) + '</td><td>' + p.peer_key + '</td><td>' + fmtTokens(p.total_tokens) + '</td><td>' + p.call_count + '</td></tr>';
|
|
1488
|
-
}).join('') + '</tbody>';
|
|
2454
|
+
// 时间范围变化后,刷新明细的模型列表与查询结果(重置到第一页)
|
|
2455
|
+
if ($('#detail-model')) {
|
|
2456
|
+
const detailPageEl = $('#detail-page');
|
|
2457
|
+
if (detailPageEl) detailPageEl.value = '1';
|
|
2458
|
+
loadDetailModelList();
|
|
2459
|
+
queryDetailUsage();
|
|
1489
2460
|
}
|
|
1490
2461
|
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
// ── Usage Overview(全时段总览)──
|
|
1494
|
-
async function loadUsageOverview() {
|
|
1495
2462
|
let data;
|
|
1496
2463
|
try {
|
|
1497
|
-
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()), {
|
|
1498
2469
|
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
1499
2470
|
});
|
|
1500
2471
|
data = resp.ok ? await resp.json() : null;
|
|
1501
2472
|
} catch { data = null; }
|
|
1502
2473
|
|
|
1503
2474
|
const ts = (data && data.token_stats && data.token_stats.all_time) ? data.token_stats.all_time
|
|
1504
|
-
: { 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 };
|
|
1505
2476
|
const sessionCount = (data && data.session_count) || 0;
|
|
1506
2477
|
const msgIn = (data && data.msg_in) || 0;
|
|
1507
2478
|
const msgOut = (data && data.msg_out) || 0;
|
|
1508
|
-
|
|
1509
|
-
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;
|
|
1510
2482
|
|
|
1511
2483
|
const cardsEl = $('#ov-cards');
|
|
1512
2484
|
if (cardsEl) {
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
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;
|
|
1525
2507
|
}
|
|
1526
2508
|
|
|
1527
2509
|
const agentTbl = $('#ov-agent-table');
|
|
1528
2510
|
const agents = (data && data.token_stats && data.token_stats.by_agent) || [];
|
|
1529
2511
|
if (agentTbl) {
|
|
1530
2512
|
if (!agents.length) {
|
|
1531
|
-
agentTbl.innerHTML = '<tbody><tr><td
|
|
2513
|
+
agentTbl.innerHTML = '<tbody><tr><td>' + t('usage.overview.noData') + '</td></tr></tbody>';
|
|
1532
2514
|
} else {
|
|
1533
2515
|
agentTbl.innerHTML =
|
|
1534
|
-
'<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>' +
|
|
1535
2517
|
'<tbody>' + agents.map(function(a) {
|
|
1536
|
-
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
|
+
|
|
1537
2523
|
return '<tr><td title="' + esc(a.agent_aid) + '">' + esc(name) + '</td>' +
|
|
1538
2524
|
'<td>' + a.call_count + '</td>' +
|
|
1539
2525
|
'<td>' + fmtTokens(a.input_tokens) + '</td>' +
|
|
1540
2526
|
'<td>' + fmtTokens(a.output_tokens) + '</td>' +
|
|
1541
2527
|
'<td>' + fmtTokens(a.cache_creation_tokens) + '</td>' +
|
|
1542
2528
|
'<td>' + fmtTokens(a.cache_read_tokens) + '</td>' +
|
|
1543
|
-
'<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>';
|
|
1544
2532
|
}).join('') + '</tbody>';
|
|
1545
2533
|
}
|
|
1546
2534
|
}
|
|
2535
|
+
|
|
2536
|
+
// 保存总览数据供详细统计使用
|
|
2537
|
+
window._currentOverviewData = { ts, sessionCount, msgIn, msgOut, hitRate };
|
|
1547
2538
|
}
|
|
1548
2539
|
|
|
1549
|
-
function
|
|
1550
|
-
|
|
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>';
|
|
1551
2864
|
}
|
|
1552
2865
|
|
|
1553
2866
|
function fmtCost(usd, cny) {
|
|
@@ -1557,6 +2870,25 @@ function fmtCost(usd, cny) {
|
|
|
1557
2870
|
return parts.length ? parts.join(' / ') : '$0';
|
|
1558
2871
|
}
|
|
1559
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
|
+
|
|
1560
2892
|
// ── Usage subtab switching ──
|
|
1561
2893
|
function initUsageSubtabs() {
|
|
1562
2894
|
var btns = document.querySelectorAll('.usage-subtab');
|
|
@@ -1571,89 +2903,324 @@ function initUsageSubtabs() {
|
|
|
1571
2903
|
});
|
|
1572
2904
|
var panel = $('#usage-' + target);
|
|
1573
2905
|
if (panel) { panel.classList.add('active'); panel.style.display = ''; }
|
|
1574
|
-
if (target === 'overview')
|
|
1575
|
-
|
|
1576
|
-
|
|
2906
|
+
if (target === 'overview') {
|
|
2907
|
+
initOverviewFilters();
|
|
2908
|
+
loadUsageOverview();
|
|
2909
|
+
} else if (target === 'explorer') {
|
|
2910
|
+
initExplorer();
|
|
2911
|
+
// 自动加载模型列表和执行查询
|
|
2912
|
+
loadExplorerModels();
|
|
2913
|
+
setTimeout(() => runExplorerQuery(), 100);
|
|
2914
|
+
}
|
|
1577
2915
|
});
|
|
1578
2916
|
});
|
|
2917
|
+
|
|
2918
|
+
// 初始化总览页面的过滤器并加载默认数据(今日)
|
|
2919
|
+
initOverviewFilters();
|
|
2920
|
+
loadUsageOverview('today');
|
|
1579
2921
|
}
|
|
1580
2922
|
|
|
1581
2923
|
// ── Explorer ──
|
|
1582
2924
|
var _explorerChart = null;
|
|
1583
2925
|
var _explorerInited = false;
|
|
1584
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 的时间范围
|
|
1585
2929
|
|
|
1586
2930
|
function initExplorer() {
|
|
1587
2931
|
if (_explorerInited) return;
|
|
1588
2932
|
_explorerInited = true;
|
|
2933
|
+
|
|
2934
|
+
// 初始化时间范围选择
|
|
2935
|
+
initExplorerTimeFilters();
|
|
2936
|
+
|
|
2937
|
+
// 绑定查询按钮
|
|
1589
2938
|
var btn = $('#exp-query-btn');
|
|
1590
2939
|
if (btn) btn.onclick = runExplorerQuery;
|
|
1591
|
-
|
|
1592
|
-
var now = new Date();
|
|
1593
|
-
var from = new Date(now.getTime() - 7 * 86400000);
|
|
1594
|
-
var fromEl = $('#exp-from');
|
|
1595
|
-
var toEl = $('#exp-to');
|
|
1596
|
-
if (fromEl) fromEl.value = from.toISOString().slice(0, 10);
|
|
1597
|
-
if (toEl) toEl.value = now.toISOString().slice(0, 10);
|
|
2940
|
+
|
|
1598
2941
|
// Load sidebar lists
|
|
1599
2942
|
loadExplorerSidebar();
|
|
1600
2943
|
}
|
|
1601
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
|
+
|
|
1602
3102
|
async function loadExplorerSidebar() {
|
|
1603
3103
|
var token = localStorage.getItem(TOKEN_KEY);
|
|
1604
3104
|
var headers = { Authorization: 'Bearer ' + token };
|
|
1605
3105
|
try {
|
|
1606
|
-
var
|
|
1607
|
-
fetch('/api/stats/agents', { headers }),
|
|
1608
|
-
fetch('/api/stats/peers', { headers }),
|
|
1609
|
-
]);
|
|
3106
|
+
var agentsResp = await fetch(apiUrl('api/stats/agents'), { headers });
|
|
1610
3107
|
var agents = agentsResp.ok ? await agentsResp.json() : [];
|
|
1611
|
-
|
|
1612
|
-
|
|
3108
|
+
renderExplorerAgentList(agents);
|
|
3109
|
+
|
|
3110
|
+
// 初始加载时不加载 peers(等待用户选择 agent)
|
|
3111
|
+
renderExplorerPeerList([]);
|
|
1613
3112
|
} catch {}
|
|
1614
3113
|
}
|
|
1615
3114
|
|
|
1616
|
-
|
|
3115
|
+
// 渲染 Agent 列表
|
|
3116
|
+
function renderExplorerAgentList(agents) {
|
|
1617
3117
|
var agentList = $('#exp-agent-list');
|
|
1618
|
-
|
|
1619
|
-
if (!agentList || !peerList) return;
|
|
3118
|
+
if (!agentList) return;
|
|
1620
3119
|
|
|
1621
3120
|
// "All" item for agents
|
|
1622
3121
|
var allHtml = '<div class="exp-sidebar-item active" data-type="all" data-key="">' +
|
|
1623
|
-
'<span class="item-name"
|
|
3122
|
+
'<span class="item-name">' + t('usage.explorer.all') + '</span></div>';
|
|
1624
3123
|
|
|
1625
3124
|
agentList.innerHTML = allHtml + agents.map(function(a) {
|
|
1626
|
-
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');
|
|
1627
3126
|
return '<div class="exp-sidebar-item" data-type="agent" data-key="' + escHtml(a.agent_aid) + '">' +
|
|
1628
|
-
'<span class="item-name" title="' + escHtml(a.agent_aid) + '">' + escHtml(name) + '</span>'
|
|
1629
|
-
'<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>';
|
|
1630
3128
|
}).join('');
|
|
1631
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
|
+
|
|
1632
3185
|
peerList.innerHTML = peers.map(function(p) {
|
|
1633
3186
|
var name = p.peer_key || 'unknown';
|
|
1634
|
-
//
|
|
1635
|
-
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
|
+
|
|
1636
3204
|
return '<div class="exp-sidebar-item" data-type="peer" data-key="' + escHtml(p.peer_key) + '">' +
|
|
1637
|
-
'<span class="item-name" title="' + escHtml(name) + '">' +
|
|
3205
|
+
'<span class="item-name" title="' + escHtml(name) + '">' +
|
|
3206
|
+
(typeTag ? typeTag + ' ' : '') + escHtml(display) + (memberTag ? ' ' + memberTag : '') +
|
|
3207
|
+
'</span>' +
|
|
1638
3208
|
'<span class="item-meta">' + fmtTokens((p.input_tokens || 0) + (p.output_tokens || 0)) + '</span></div>';
|
|
1639
3209
|
}).join('');
|
|
1640
3210
|
|
|
1641
|
-
// Bind click events
|
|
1642
|
-
|
|
1643
|
-
allItems.forEach(function(el) {
|
|
3211
|
+
// Bind click events for peers
|
|
3212
|
+
peerList.querySelectorAll('.exp-sidebar-item').forEach(function(el) {
|
|
1644
3213
|
el.addEventListener('click', function() {
|
|
1645
|
-
// Clear active from all
|
|
1646
|
-
|
|
3214
|
+
// Clear active from all peers
|
|
3215
|
+
document.querySelectorAll('#exp-peer-list .exp-sidebar-item').forEach(function(x) { x.classList.remove('active'); });
|
|
1647
3216
|
el.classList.add('active');
|
|
3217
|
+
|
|
1648
3218
|
var type = el.getAttribute('data-type');
|
|
1649
3219
|
var key = el.getAttribute('data-key');
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
_expSelection = { type: type, key: key };
|
|
1655
|
-
$('#exp-selected-name').textContent = key;
|
|
1656
|
-
}
|
|
3220
|
+
_expSelection = { type: type, key: key };
|
|
3221
|
+
var name = el.querySelector('.item-name').textContent.trim();
|
|
3222
|
+
$('#exp-selected-name').textContent = name;
|
|
3223
|
+
|
|
1657
3224
|
runExplorerQuery();
|
|
1658
3225
|
});
|
|
1659
3226
|
});
|
|
@@ -1662,11 +3229,13 @@ function renderExplorerSidebar(agents, peers) {
|
|
|
1662
3229
|
function escHtml(s) { return (s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
1663
3230
|
|
|
1664
3231
|
async function runExplorerQuery() {
|
|
3232
|
+
// 使用 Explorer 自己的时间范围
|
|
3233
|
+
const fromTs = _expTimeRange.fromTs;
|
|
3234
|
+
const toTs = _expTimeRange.toTs;
|
|
3235
|
+
|
|
1665
3236
|
var params = new URLSearchParams();
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
if (fromEl && fromEl.value) params.set('from', String(new Date(fromEl.value + 'T00:00:00').getTime()));
|
|
1669
|
-
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));
|
|
1670
3239
|
// Inject selection from sidebar
|
|
1671
3240
|
if (_expSelection.type === 'agent' && _expSelection.key) params.set('agent', _expSelection.key);
|
|
1672
3241
|
if (_expSelection.type === 'peer' && _expSelection.key) params.set('peer', _expSelection.key);
|
|
@@ -1677,33 +3246,123 @@ async function runExplorerQuery() {
|
|
|
1677
3246
|
|
|
1678
3247
|
var data;
|
|
1679
3248
|
try {
|
|
1680
|
-
var resp = await fetch('
|
|
3249
|
+
var resp = await fetch(apiUrl('api/stats/explorer?' + params.toString()), {
|
|
1681
3250
|
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
1682
3251
|
});
|
|
1683
3252
|
if (!resp.ok) return;
|
|
1684
3253
|
data = await resp.json();
|
|
1685
3254
|
} catch { return; }
|
|
1686
3255
|
|
|
1687
|
-
//
|
|
3256
|
+
// 根据查询结果计算卡片数据
|
|
1688
3257
|
var cardsEl = $('#exp-detail-cards');
|
|
1689
|
-
if (
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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();
|
|
1699
3333
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
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';
|
|
1702
3361
|
}
|
|
1703
3362
|
|
|
1704
3363
|
if (!data || !data.length) {
|
|
1705
3364
|
var tbl = $('#usage-explorer-table');
|
|
1706
|
-
if (tbl) tbl.innerHTML = '<tr><td>
|
|
3365
|
+
if (tbl) tbl.innerHTML = '<tr><td>' + t('usage.explorer.noData') + '</td></tr>';
|
|
1707
3366
|
var chartEl = $('#usage-explorer-chart');
|
|
1708
3367
|
if (chartEl && _explorerChart) { _explorerChart.dispose(); _explorerChart = null; }
|
|
1709
3368
|
return;
|
|
@@ -1718,13 +3377,13 @@ async function runExplorerQuery() {
|
|
|
1718
3377
|
var periods = data.map(function(r) { return r.period; });
|
|
1719
3378
|
_explorerChart.setOption({
|
|
1720
3379
|
tooltip: { trigger: 'axis' },
|
|
1721
|
-
legend: { data: ['
|
|
3380
|
+
legend: { data: [t('usage.card.input'), t('usage.card.output')], top: 0, textStyle: { fontSize: 11 } },
|
|
1722
3381
|
grid: { top: 30, bottom: 30, left: 60, right: 16 },
|
|
1723
3382
|
xAxis: { type: 'category', data: periods, axisLabel: { fontSize: 10, rotate: 30 } },
|
|
1724
3383
|
yAxis: { type: 'value', axisLabel: { formatter: function(v) { return fmtTokens(v); } } },
|
|
1725
3384
|
series: [
|
|
1726
|
-
{ name: '
|
|
1727
|
-
{ 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' } },
|
|
1728
3387
|
]
|
|
1729
3388
|
});
|
|
1730
3389
|
}
|
|
@@ -1733,7 +3392,7 @@ async function runExplorerQuery() {
|
|
|
1733
3392
|
var tbl = $('#usage-explorer-table');
|
|
1734
3393
|
if (tbl) {
|
|
1735
3394
|
tbl.innerHTML =
|
|
1736
|
-
'<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>' +
|
|
1737
3396
|
'<tbody>' + data.map(function(r) {
|
|
1738
3397
|
return '<tr><td>' + r.period + '</td><td>' + fmtTokens(r.input_tokens) + '</td><td>' + fmtTokens(r.output_tokens) +
|
|
1739
3398
|
'</td><td>' + fmtTokens(r.cache_creation_tokens) + '</td><td>' + fmtTokens(r.cache_read_tokens) +
|
|
@@ -1956,14 +3615,25 @@ function monDualLine(elId, varKey, times, isDark, title, series, fmtY, yRange) {
|
|
|
1956
3615
|
});
|
|
1957
3616
|
}
|
|
1958
3617
|
|
|
1959
|
-
window.addEventListener('DOMContentLoaded', () => {
|
|
3618
|
+
window.addEventListener('DOMContentLoaded', async () => {
|
|
1960
3619
|
initTheme();
|
|
1961
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
|
+
}
|
|
1962
3634
|
if (localStorage.getItem(TOKEN_KEY)) {
|
|
1963
3635
|
showApp();
|
|
1964
3636
|
startApp();
|
|
1965
|
-
loadUsageDashboard();
|
|
1966
|
-
loadUsageOverview();
|
|
1967
3637
|
initUsageSubtabs();
|
|
1968
3638
|
} else {
|
|
1969
3639
|
showPairPage();
|