evolclaw-web 1.2.0 → 1.2.3
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 +193 -25
- package/dist/sources/aid.js +4 -2
- package/dist/sources/baseagent-detector.js +72 -0
- package/dist/sources/gateway.js +44 -0
- package/dist/sources/msg.js +366 -31
- package/dist/sources/session-codex.js +618 -0
- package/dist/sources/session.js +25 -12
- package/dist/sources/stats.js +269 -136
- package/dist/sources/system.js +37 -2
- package/dist/static/app.js +2089 -321
- package/dist/static/index.html +122 -57
- package/dist/static/style.css +845 -19
- package/package.json +1 -1
package/dist/static/app.js
CHANGED
|
@@ -2,10 +2,607 @@
|
|
|
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.version': 'Version',
|
|
74
|
+
'agents.stats.pid': 'PID',
|
|
75
|
+
'agents.stats.uptime': 'Uptime',
|
|
76
|
+
|
|
77
|
+
// Agent table headers
|
|
78
|
+
'agents.th.agent': 'Agent',
|
|
79
|
+
'agents.th.aid': 'AID',
|
|
80
|
+
'agents.th.work': '工作',
|
|
81
|
+
'agents.th.queue': '队列',
|
|
82
|
+
'agents.th.model': '模型',
|
|
83
|
+
'agents.th.runtime': '运行',
|
|
84
|
+
'agents.th.received': '收',
|
|
85
|
+
'agents.th.sent': '发',
|
|
86
|
+
'agents.th.completed': '完',
|
|
87
|
+
'agents.th.errors': '错',
|
|
88
|
+
'agents.th.interrupts': '断',
|
|
89
|
+
'agents.th.lastActivity': '最后活动',
|
|
90
|
+
'agents.th.operations': '操作',
|
|
91
|
+
'agents.th.projectPath': '项目路径',
|
|
92
|
+
|
|
93
|
+
// Agent operations
|
|
94
|
+
'agents.op.stopping': '停止中…',
|
|
95
|
+
'agents.op.starting': '启动中…',
|
|
96
|
+
'agents.op.reloading': '重载中…',
|
|
97
|
+
'agents.op.disabling': '禁用中…',
|
|
98
|
+
'agents.op.enabling': '启用中…',
|
|
99
|
+
'agents.op.deleting': '删除中…',
|
|
100
|
+
'agents.op.stopped': '✓ 已停止',
|
|
101
|
+
'agents.op.started': '✓ 已启动',
|
|
102
|
+
'agents.op.reloaded': '✓ 已重载',
|
|
103
|
+
'agents.op.disabled': '✓ 已禁用',
|
|
104
|
+
'agents.op.enabled': '✓ 已启用',
|
|
105
|
+
'agents.op.deleted': '✓ 已删除',
|
|
106
|
+
'agents.op.saved': '✓ 配置已保存,点「重载」生效',
|
|
107
|
+
'agents.op.confirmReload': '确认强制重载?',
|
|
108
|
+
'agents.op.confirmToggle': '确认强制',
|
|
109
|
+
'agents.op.confirmDelete': '删除 Agent {aid}?\n此操作不可恢复。',
|
|
110
|
+
'agents.op.confirmForceDelete': '确认强制删除?',
|
|
111
|
+
'agents.op.confirmClearQueue': '清空 {aid} 的待处理消息队列?',
|
|
112
|
+
'agents.op.clearQueueTitle': '清空 {count} 条待处理消息',
|
|
113
|
+
'agents.op.viewAgentMd': '查看 agent.md ↗',
|
|
114
|
+
|
|
115
|
+
// Messages view
|
|
116
|
+
'messages.colTitle.aid': 'Agent',
|
|
117
|
+
'messages.colTitle.peers': 'Chats',
|
|
118
|
+
'messages.colTitle.all': 'All',
|
|
119
|
+
'messages.empty.selectAid': '← 选择一个 Agent',
|
|
120
|
+
'messages.empty.selectToView': '选择 Agent 查看消息',
|
|
121
|
+
'messages.empty.noMessages': '暂无消息',
|
|
122
|
+
'messages.tag.group': '群聊',
|
|
123
|
+
'messages.tag.encrypted': '🔒密文',
|
|
124
|
+
'messages.tag.plain': '明文',
|
|
125
|
+
'messages.tag.proactive': '自主',
|
|
126
|
+
'messages.tag.inject': '注入',
|
|
127
|
+
'messages.tag.responsive': '响应',
|
|
128
|
+
'messages.msgKind.reply': '回复',
|
|
129
|
+
'messages.msgKind.thought': '思考',
|
|
130
|
+
'messages.msgKind.inject': '注入',
|
|
131
|
+
'messages.msgKind.notify': '通知',
|
|
132
|
+
'messages.msgType.thought': '思考',
|
|
133
|
+
'messages.msgType.image': '图片',
|
|
134
|
+
'messages.msgType.file': '文件',
|
|
135
|
+
'messages.msgType.command': '命令',
|
|
136
|
+
|
|
137
|
+
// Sessions view
|
|
138
|
+
'sessions.filter.normal': '🔍 仅有效',
|
|
139
|
+
'sessions.filter.chat': '💬 对话',
|
|
140
|
+
'sessions.search.placeholder': '🔎 搜索 peer / 内容',
|
|
141
|
+
'sessions.empty.noMatch': '无匹配会话',
|
|
142
|
+
'sessions.empty.noSessions': '该项目暂无会话',
|
|
143
|
+
'sessions.empty.noContent': '该会话暂无内容',
|
|
144
|
+
'sessions.header.project': '项目',
|
|
145
|
+
'sessions.header.session': '会话',
|
|
146
|
+
'sessions.stat.context': '📐 {tokens} ctx',
|
|
147
|
+
'sessions.stat.cost': '💰 ${cost}',
|
|
148
|
+
'sessions.turnType.modelOutput': '模型输出',
|
|
149
|
+
'sessions.turnType.toolUse': '工具使用',
|
|
150
|
+
'sessions.turnType.toolResult': '工具结果',
|
|
151
|
+
'sessions.turnType.msgSend': '发送消息',
|
|
152
|
+
|
|
153
|
+
// Cache view
|
|
154
|
+
'cache.daemonStopped': '⚠ EvolClaw 主进程未运行,无缓存统计可显示',
|
|
155
|
+
'cache.notSupported': '⚠ 当前 EvolClaw 版本不支持 cache-stats(请升级 daemon)',
|
|
156
|
+
'cache.card.hitRate': '命中率',
|
|
157
|
+
'cache.card.reads': '读取总数',
|
|
158
|
+
'cache.card.entries': '缓存条目',
|
|
159
|
+
'cache.card.statChecks': 'stat 检查',
|
|
160
|
+
'cache.card.reReads': '重读',
|
|
161
|
+
'cache.card.evictions': '驱逐',
|
|
162
|
+
'cache.card.invalidations': '失效',
|
|
163
|
+
'cache.card.since': '统计起始',
|
|
164
|
+
'cache.card.ago': '前',
|
|
165
|
+
'cache.card.memory': '近似内存',
|
|
166
|
+
'cache.card.hit': '命中',
|
|
167
|
+
'cache.card.miss': '未命中',
|
|
168
|
+
'cache.section.byGroup': '按缓存组',
|
|
169
|
+
'cache.section.byPolicy': '按策略',
|
|
170
|
+
'cache.th.group': '组',
|
|
171
|
+
'cache.th.type': '类型',
|
|
172
|
+
'cache.th.reads': '读取',
|
|
173
|
+
'cache.th.hits': '命中',
|
|
174
|
+
'cache.th.misses': '未命中',
|
|
175
|
+
'cache.th.hitRate': '命中率',
|
|
176
|
+
'cache.th.reReads': '重读',
|
|
177
|
+
'cache.th.evictions': '驱逐',
|
|
178
|
+
'cache.th.entries': '条目',
|
|
179
|
+
'cache.th.memory': '内存',
|
|
180
|
+
'cache.th.capacity': '容量',
|
|
181
|
+
'cache.th.policy': '策略',
|
|
182
|
+
'cache.th.statChecks': 'stat 检查',
|
|
183
|
+
'cache.note': '注:config/defaults 与关系级 preferences 的读取也已并入本统计;渲染后结果(按 vars)不缓存,故不在此列。',
|
|
184
|
+
'cache.policy.onReload': '靠 reload 刷新,平时零检查',
|
|
185
|
+
'cache.policy.manual': '显式单刷',
|
|
186
|
+
'cache.policy.mtime': '每读 statSync 门控',
|
|
187
|
+
|
|
188
|
+
// Monitor view
|
|
189
|
+
'monitor.toolbar.timeRange': '时间范围',
|
|
190
|
+
'monitor.range.2m': '2 分钟',
|
|
191
|
+
'monitor.range.10m': '10 分钟',
|
|
192
|
+
'monitor.range.1h': '1 小时',
|
|
193
|
+
'monitor.legend.process': 'evolclaw 进程',
|
|
194
|
+
'monitor.legend.system': '整机系统',
|
|
195
|
+
|
|
196
|
+
// Usage view
|
|
197
|
+
'usage.subtab.overview': '总览',
|
|
198
|
+
'usage.subtab.explorer': '详细统计',
|
|
199
|
+
'usage.overview.range.today': '今日',
|
|
200
|
+
'usage.overview.range.week': '本周',
|
|
201
|
+
'usage.overview.range.lastWeek': '上周',
|
|
202
|
+
'usage.overview.range.month': '本月',
|
|
203
|
+
'usage.overview.range.last30': '最近30天',
|
|
204
|
+
'usage.overview.range.custom': '自定义',
|
|
205
|
+
'usage.card.input': '输入',
|
|
206
|
+
'usage.card.output': '输出',
|
|
207
|
+
'usage.card.cacheRead': '缓存读取',
|
|
208
|
+
'usage.card.cacheHit': '缓存命中',
|
|
209
|
+
'usage.card.calls': '调用',
|
|
210
|
+
'usage.card.sessionCount': '会话数',
|
|
211
|
+
'usage.card.msgIn': '收到消息',
|
|
212
|
+
'usage.card.msgOut': '发出消息',
|
|
213
|
+
'usage.card.modelCalls': '模型调用',
|
|
214
|
+
'usage.card.inputTokens': '输入 Token',
|
|
215
|
+
'usage.card.outputTokens': '输出 Token',
|
|
216
|
+
'usage.card.cacheCreation': '缓存创建',
|
|
217
|
+
'usage.card.cacheHitTokens': '缓存命中',
|
|
218
|
+
'usage.card.cacheHitRate': '缓存命中率',
|
|
219
|
+
'usage.card.totalCost': '总花费',
|
|
220
|
+
'usage.card.costOfficial': '官方价格',
|
|
221
|
+
'usage.card.costGateway': '网关价格',
|
|
222
|
+
'usage.card.sessionInfo': '会话信息',
|
|
223
|
+
'usage.card.usageInfo': '用量信息',
|
|
224
|
+
'usage.card.costInfo': '花费信息',
|
|
225
|
+
'usage.detail.title': '模型访问明细',
|
|
226
|
+
'usage.detail.agent': '智能体',
|
|
227
|
+
'usage.detail.model': '模型',
|
|
228
|
+
'usage.detail.error': '查询失败',
|
|
229
|
+
'usage.detail.th.time': '时间',
|
|
230
|
+
'usage.detail.th.agent': '智能体',
|
|
231
|
+
'usage.detail.th.peer': 'Peer',
|
|
232
|
+
'usage.detail.th.model': '模型',
|
|
233
|
+
'usage.detail.th.input': '输入',
|
|
234
|
+
'usage.detail.th.output': '输出',
|
|
235
|
+
'usage.detail.th.cacheCreation': '缓存创建',
|
|
236
|
+
'usage.detail.th.cacheRead': '缓存读取',
|
|
237
|
+
'usage.detail.th.costOfficial': '官方价格',
|
|
238
|
+
'usage.detail.th.costGateway': '网关价格',
|
|
239
|
+
'usage.detail.pageSize': '每页',
|
|
240
|
+
'usage.detail.prevPage': '上一页',
|
|
241
|
+
'usage.detail.nextPage': '下一页',
|
|
242
|
+
'usage.detail.pagination': '显示 {start}-{end} / 共 {total} 条 (第 {page}/{totalPages} 页)',
|
|
243
|
+
'usage.overview.title': '按 Agent 汇总(全时段)',
|
|
244
|
+
'usage.overview.noData': '暂无数据',
|
|
245
|
+
'usage.overview.th.agent': '智能体',
|
|
246
|
+
'usage.overview.th.calls': '调用',
|
|
247
|
+
'usage.overview.th.input': '输入',
|
|
248
|
+
'usage.overview.th.output': '输出',
|
|
249
|
+
'usage.overview.th.cacheCreation': '缓存创建',
|
|
250
|
+
'usage.overview.th.cacheHit': '缓存命中',
|
|
251
|
+
'usage.overview.th.cacheHitRate': '命中率',
|
|
252
|
+
'usage.overview.th.costOfficial': '官方价格',
|
|
253
|
+
'usage.overview.th.costGateway': '网关价格',
|
|
254
|
+
'usage.overview.th.cost': '花费',
|
|
255
|
+
'usage.dashboard.title.topPeers': 'Top Peers (Today)',
|
|
256
|
+
'usage.dashboard.th.rank': '#',
|
|
257
|
+
'usage.dashboard.th.peer': 'Peer',
|
|
258
|
+
'usage.dashboard.th.tokens': 'Tokens',
|
|
259
|
+
'usage.dashboard.th.calls': 'Calls',
|
|
260
|
+
'usage.explorer.sidebar.agents': '智能体',
|
|
261
|
+
'usage.explorer.sidebar.peers': '对端智能体',
|
|
262
|
+
'usage.explorer.chatType.group': '群聊',
|
|
263
|
+
'usage.explorer.chatType.private': '单聊',
|
|
264
|
+
'usage.explorer.memberCount': '人',
|
|
265
|
+
'usage.explorer.selectHint': '请从左侧选择 Agent 或 Peer',
|
|
266
|
+
'usage.explorer.all': '全部',
|
|
267
|
+
'usage.explorer.filter.from': 'From',
|
|
268
|
+
'usage.explorer.filter.to': 'To',
|
|
269
|
+
'usage.explorer.filter.model': 'Model',
|
|
270
|
+
'usage.explorer.filter.granularity': '粒度',
|
|
271
|
+
'usage.explorer.filter.granularity.hour': 'Hour',
|
|
272
|
+
'usage.explorer.filter.granularity.day': 'Day',
|
|
273
|
+
'usage.explorer.filter.granularity.week': 'Week',
|
|
274
|
+
'usage.explorer.filter.granularity.month': 'Month',
|
|
275
|
+
'usage.explorer.results': 'Results',
|
|
276
|
+
'usage.explorer.noData': 'No data for selected range.',
|
|
277
|
+
'usage.explorer.th.period': 'Period',
|
|
278
|
+
'usage.explorer.th.input': 'Input',
|
|
279
|
+
'usage.explorer.th.output': 'Output',
|
|
280
|
+
'usage.explorer.th.cacheCreation': 'Cache↑',
|
|
281
|
+
'usage.explorer.th.cacheHit': 'CacheHit',
|
|
282
|
+
'usage.explorer.th.calls': 'Calls',
|
|
283
|
+
},
|
|
284
|
+
'en-US': {
|
|
285
|
+
// Tabs
|
|
286
|
+
'tab.agents': 'Agents',
|
|
287
|
+
'tab.messages': 'Messages',
|
|
288
|
+
'tab.sessions': 'Sessions',
|
|
289
|
+
'tab.triggers': 'Triggers',
|
|
290
|
+
'tab.cache': 'Cache',
|
|
291
|
+
'tab.system': 'System',
|
|
292
|
+
'tab.gateway': 'Agent Gateway',
|
|
293
|
+
'tab.usage': 'Usage',
|
|
294
|
+
'tab.monitor': 'Monitor',
|
|
295
|
+
|
|
296
|
+
// Status
|
|
297
|
+
'status.connecting': 'Connecting…',
|
|
298
|
+
'status.connected': 'Connected',
|
|
299
|
+
'status.disconnected': 'Disconnected',
|
|
300
|
+
'status.reconnecting': 'Reconnecting',
|
|
301
|
+
'status.stopped': 'Stopped',
|
|
302
|
+
'status.idle': 'Idle',
|
|
303
|
+
'status.working': 'Working',
|
|
304
|
+
|
|
305
|
+
// Actions
|
|
306
|
+
'action.logout': 'Logout',
|
|
307
|
+
'action.pair': 'Pair',
|
|
308
|
+
'action.stop': 'Stop',
|
|
309
|
+
'action.start': 'Start',
|
|
310
|
+
'action.enable': 'Enable',
|
|
311
|
+
'action.disable': 'Disable',
|
|
312
|
+
'action.reload': 'Reload Config',
|
|
313
|
+
'action.edit': 'Edit Config',
|
|
314
|
+
'action.delete': 'Delete Agent',
|
|
315
|
+
'action.clearQueue': 'Clear Queue',
|
|
316
|
+
'action.new': '+ New',
|
|
317
|
+
'action.query': 'Query',
|
|
318
|
+
|
|
319
|
+
// Pair page
|
|
320
|
+
'pair.title': '🔭 EvolClaw Watch',
|
|
321
|
+
'pair.hint': 'Enter the 6-digit pairing code shown in terminal',
|
|
322
|
+
'pair.placeholder': '000000',
|
|
323
|
+
'pair.error.length': 'Please enter 6-digit pairing code',
|
|
324
|
+
'pair.error.failed': 'Pairing failed',
|
|
325
|
+
'pair.error.network': 'Network error',
|
|
326
|
+
'pair.error.tokenInvalid': 'Token expired, please pair again',
|
|
327
|
+
|
|
328
|
+
// Common
|
|
329
|
+
'common.loading': 'Loading…',
|
|
330
|
+
'common.empty': 'No data',
|
|
331
|
+
'common.noData': 'N/A',
|
|
332
|
+
'common.operating': 'Operating…',
|
|
333
|
+
'common.buildTime': 'Build Time',
|
|
334
|
+
|
|
335
|
+
// Agents view
|
|
336
|
+
'agents.subtitle.enabled': 'Enabled',
|
|
337
|
+
'agents.subtitle.disabled': 'Disabled',
|
|
338
|
+
'agents.daemonStopped': '⚠ EvolClaw daemon not running, showing recent activity only',
|
|
339
|
+
'agents.empty.disabled': 'No disabled agents',
|
|
340
|
+
'agents.empty.enabled': 'No enabled agents',
|
|
341
|
+
'agents.stats.gateway': 'Gateway',
|
|
342
|
+
'agents.stats.aids': 'AIDs',
|
|
343
|
+
'agents.stats.total': 'total',
|
|
344
|
+
'agents.stats.online': 'online',
|
|
345
|
+
'agents.stats.offline': 'offline',
|
|
346
|
+
'agents.stats.messages': 'Messages',
|
|
347
|
+
'agents.stats.version': 'Version',
|
|
348
|
+
'agents.stats.pid': 'PID',
|
|
349
|
+
'agents.stats.uptime': 'Uptime',
|
|
350
|
+
|
|
351
|
+
// Agent table headers
|
|
352
|
+
'agents.th.agent': 'Agent',
|
|
353
|
+
'agents.th.aid': 'AID',
|
|
354
|
+
'agents.th.work': 'Work',
|
|
355
|
+
'agents.th.queue': 'Queue',
|
|
356
|
+
'agents.th.model': 'Model',
|
|
357
|
+
'agents.th.runtime': 'Runtime',
|
|
358
|
+
'agents.th.received': 'Recv',
|
|
359
|
+
'agents.th.sent': 'Sent',
|
|
360
|
+
'agents.th.completed': 'Done',
|
|
361
|
+
'agents.th.errors': 'Err',
|
|
362
|
+
'agents.th.interrupts': 'Int',
|
|
363
|
+
'agents.th.lastActivity': 'Last Activity',
|
|
364
|
+
'agents.th.operations': 'Operations',
|
|
365
|
+
'agents.th.projectPath': 'Project Path',
|
|
366
|
+
|
|
367
|
+
// Agent operations
|
|
368
|
+
'agents.op.stopping': 'Stopping…',
|
|
369
|
+
'agents.op.starting': 'Starting…',
|
|
370
|
+
'agents.op.reloading': 'Reloading…',
|
|
371
|
+
'agents.op.disabling': 'Disabling…',
|
|
372
|
+
'agents.op.enabling': 'Enabling…',
|
|
373
|
+
'agents.op.deleting': 'Deleting…',
|
|
374
|
+
'agents.op.stopped': '✓ Stopped',
|
|
375
|
+
'agents.op.started': '✓ Started',
|
|
376
|
+
'agents.op.reloaded': '✓ Reloaded',
|
|
377
|
+
'agents.op.disabled': '✓ Disabled',
|
|
378
|
+
'agents.op.enabled': '✓ Enabled',
|
|
379
|
+
'agents.op.deleted': '✓ Deleted',
|
|
380
|
+
'agents.op.saved': '✓ Config saved, click "Reload" to apply',
|
|
381
|
+
'agents.op.confirmReload': 'Force reload?',
|
|
382
|
+
'agents.op.confirmToggle': 'Force',
|
|
383
|
+
'agents.op.confirmDelete': 'Delete agent {aid}?\nThis cannot be undone.',
|
|
384
|
+
'agents.op.confirmForceDelete': 'Force delete?',
|
|
385
|
+
'agents.op.confirmClearQueue': 'Clear pending message queue for {aid}?',
|
|
386
|
+
'agents.op.clearQueueTitle': 'Clear {count} pending messages',
|
|
387
|
+
'agents.op.viewAgentMd': 'View agent.md ↗',
|
|
388
|
+
|
|
389
|
+
// Messages view
|
|
390
|
+
'messages.colTitle.aid': 'Agent',
|
|
391
|
+
'messages.colTitle.peers': 'Chats',
|
|
392
|
+
'messages.colTitle.all': 'All',
|
|
393
|
+
'messages.empty.selectAid': '← Select Agent',
|
|
394
|
+
'messages.empty.selectToView': 'Select Agent to view messages',
|
|
395
|
+
'messages.empty.noMessages': 'No messages',
|
|
396
|
+
'messages.tag.group': 'Group',
|
|
397
|
+
'messages.tag.encrypted': '🔒Encrypted',
|
|
398
|
+
'messages.tag.plain': 'Plain',
|
|
399
|
+
'messages.tag.proactive': 'Proactive',
|
|
400
|
+
'messages.tag.inject': 'Inject',
|
|
401
|
+
'messages.tag.responsive': 'Responsive',
|
|
402
|
+
'messages.msgKind.reply': 'Reply',
|
|
403
|
+
'messages.msgKind.thought': 'Thought',
|
|
404
|
+
'messages.msgKind.inject': 'Inject',
|
|
405
|
+
'messages.msgKind.notify': 'Notify',
|
|
406
|
+
'messages.msgType.thought': 'Thought',
|
|
407
|
+
'messages.msgType.image': 'Image',
|
|
408
|
+
'messages.msgType.file': 'File',
|
|
409
|
+
'messages.msgType.command': 'Command',
|
|
410
|
+
|
|
411
|
+
// Sessions view
|
|
412
|
+
'sessions.filter.normal': '🔍 Valid Only',
|
|
413
|
+
'sessions.filter.chat': '💬 Chat',
|
|
414
|
+
'sessions.search.placeholder': '🔎 Search peer / content',
|
|
415
|
+
'sessions.empty.noMatch': 'No matching sessions',
|
|
416
|
+
'sessions.empty.noSessions': 'No sessions in this project',
|
|
417
|
+
'sessions.empty.noContent': 'No content in this session',
|
|
418
|
+
'sessions.header.project': 'Project',
|
|
419
|
+
'sessions.header.session': 'Session',
|
|
420
|
+
'sessions.stat.context': '📐 {tokens} ctx',
|
|
421
|
+
'sessions.stat.cost': '💰 ${cost}',
|
|
422
|
+
'sessions.turnType.modelOutput': 'Model Output',
|
|
423
|
+
'sessions.turnType.toolUse': 'Tool Use',
|
|
424
|
+
'sessions.turnType.toolResult': 'Tool Result',
|
|
425
|
+
'sessions.turnType.msgSend': 'Send Message',
|
|
426
|
+
|
|
427
|
+
// Cache view
|
|
428
|
+
'cache.daemonStopped': '⚠ EvolClaw daemon not running, no cache stats available',
|
|
429
|
+
'cache.notSupported': '⚠ Current EvolClaw version does not support cache-stats (please upgrade daemon)',
|
|
430
|
+
'cache.card.hitRate': 'Hit Rate',
|
|
431
|
+
'cache.card.reads': 'Total Reads',
|
|
432
|
+
'cache.card.entries': 'Cache Entries',
|
|
433
|
+
'cache.card.statChecks': 'Stat Checks',
|
|
434
|
+
'cache.card.reReads': 'Re-reads',
|
|
435
|
+
'cache.card.evictions': 'Evictions',
|
|
436
|
+
'cache.card.invalidations': 'Invalidations',
|
|
437
|
+
'cache.card.since': 'Stats Since',
|
|
438
|
+
'cache.card.ago': 'ago',
|
|
439
|
+
'cache.card.memory': 'approx memory',
|
|
440
|
+
'cache.card.hit': 'hit',
|
|
441
|
+
'cache.card.miss': 'miss',
|
|
442
|
+
'cache.section.byGroup': 'By Cache Group',
|
|
443
|
+
'cache.section.byPolicy': 'By Policy',
|
|
444
|
+
'cache.th.group': 'Group',
|
|
445
|
+
'cache.th.type': 'Type',
|
|
446
|
+
'cache.th.reads': 'Reads',
|
|
447
|
+
'cache.th.hits': 'Hits',
|
|
448
|
+
'cache.th.misses': 'Misses',
|
|
449
|
+
'cache.th.hitRate': 'Hit Rate',
|
|
450
|
+
'cache.th.reReads': 'Re-reads',
|
|
451
|
+
'cache.th.evictions': 'Evictions',
|
|
452
|
+
'cache.th.entries': 'Entries',
|
|
453
|
+
'cache.th.memory': 'Memory',
|
|
454
|
+
'cache.th.capacity': 'Capacity',
|
|
455
|
+
'cache.th.policy': 'Policy',
|
|
456
|
+
'cache.th.statChecks': 'Stat Checks',
|
|
457
|
+
'cache.note': 'Note: Reads of config/defaults and relation-level preferences are included; rendered results (by vars) are not cached and not shown here.',
|
|
458
|
+
'cache.policy.onReload': 'Refresh on reload, zero checks normally',
|
|
459
|
+
'cache.policy.manual': 'Explicit single refresh',
|
|
460
|
+
'cache.policy.mtime': 'statSync gate on each read',
|
|
461
|
+
|
|
462
|
+
// Monitor view
|
|
463
|
+
'monitor.toolbar.timeRange': 'Time Range',
|
|
464
|
+
'monitor.range.2m': '2 minutes',
|
|
465
|
+
'monitor.range.10m': '10 minutes',
|
|
466
|
+
'monitor.range.1h': '1 hour',
|
|
467
|
+
'monitor.legend.process': 'evolclaw process',
|
|
468
|
+
'monitor.legend.system': 'system',
|
|
469
|
+
|
|
470
|
+
// Usage view
|
|
471
|
+
'usage.subtab.overview': 'Overview',
|
|
472
|
+
'usage.subtab.explorer': 'Detailed Statistics',
|
|
473
|
+
'usage.overview.range.today': 'Today',
|
|
474
|
+
'usage.overview.range.week': 'This Week',
|
|
475
|
+
'usage.overview.range.lastWeek': 'Last Week',
|
|
476
|
+
'usage.overview.range.month': 'This Month',
|
|
477
|
+
'usage.overview.range.last30': 'Last 30 Days',
|
|
478
|
+
'usage.overview.range.custom': 'Custom',
|
|
479
|
+
'usage.card.input': 'Input',
|
|
480
|
+
'usage.card.output': 'Output',
|
|
481
|
+
'usage.card.cacheRead': 'Cache Read',
|
|
482
|
+
'usage.card.cacheHit': 'Cache Hit',
|
|
483
|
+
'usage.card.calls': 'Calls',
|
|
484
|
+
'usage.card.sessionCount': 'Sessions',
|
|
485
|
+
'usage.card.msgIn': 'Received Messages',
|
|
486
|
+
'usage.card.msgOut': 'Sent Messages',
|
|
487
|
+
'usage.card.modelCalls': 'Model Calls',
|
|
488
|
+
'usage.card.inputTokens': 'Input Tokens',
|
|
489
|
+
'usage.card.outputTokens': 'Output Tokens',
|
|
490
|
+
'usage.card.cacheCreation': 'Cache Creation',
|
|
491
|
+
'usage.card.cacheHitTokens': 'Cache Hit',
|
|
492
|
+
'usage.card.cacheHitRate': 'Cache Hit Rate',
|
|
493
|
+
'usage.card.totalCost': 'Total Cost',
|
|
494
|
+
'usage.card.costOfficial': 'Official Price',
|
|
495
|
+
'usage.card.costGateway': 'Gateway Price',
|
|
496
|
+
'usage.card.sessionInfo': 'Session Info',
|
|
497
|
+
'usage.card.usageInfo': 'Usage Info',
|
|
498
|
+
'usage.card.costInfo': 'Cost Info',
|
|
499
|
+
'usage.detail.title': 'Model Access Details',
|
|
500
|
+
'usage.detail.agent': 'Agent',
|
|
501
|
+
'usage.detail.model': 'Model',
|
|
502
|
+
'usage.detail.error': 'Query failed',
|
|
503
|
+
'usage.detail.th.time': 'Time',
|
|
504
|
+
'usage.detail.th.agent': 'Agent',
|
|
505
|
+
'usage.detail.th.peer': 'Peer',
|
|
506
|
+
'usage.detail.th.model': 'Model',
|
|
507
|
+
'usage.detail.th.input': 'Input',
|
|
508
|
+
'usage.detail.th.output': 'Output',
|
|
509
|
+
'usage.detail.th.cacheCreation': 'Cache Creation',
|
|
510
|
+
'usage.detail.th.cacheRead': 'Cache Read',
|
|
511
|
+
'usage.detail.th.costOfficial': 'Official',
|
|
512
|
+
'usage.detail.th.costGateway': 'Gateway',
|
|
513
|
+
'usage.detail.pageSize': 'Per page',
|
|
514
|
+
'usage.detail.prevPage': 'Previous',
|
|
515
|
+
'usage.detail.nextPage': 'Next',
|
|
516
|
+
'usage.detail.pagination': 'Showing {start}-{end} of {total} (Page {page}/{totalPages})',
|
|
517
|
+
'usage.overview.title': 'Summary by Agent (All Time)',
|
|
518
|
+
'usage.overview.noData': 'No data',
|
|
519
|
+
'usage.overview.th.agent': 'Agent',
|
|
520
|
+
'usage.overview.th.calls': 'Calls',
|
|
521
|
+
'usage.overview.th.input': 'Input',
|
|
522
|
+
'usage.overview.th.output': 'Output',
|
|
523
|
+
'usage.overview.th.cacheCreation': 'Cache Creation',
|
|
524
|
+
'usage.overview.th.cacheHit': 'Cache Hit',
|
|
525
|
+
'usage.overview.th.cacheHitRate': 'Hit Rate',
|
|
526
|
+
'usage.overview.th.costOfficial': 'Official Price',
|
|
527
|
+
'usage.overview.th.costGateway': 'Gateway Price',
|
|
528
|
+
'usage.overview.th.cost': 'Cost',
|
|
529
|
+
'usage.dashboard.title.topPeers': 'Top Peers (Today)',
|
|
530
|
+
'usage.dashboard.th.rank': '#',
|
|
531
|
+
'usage.dashboard.th.peer': 'Peer',
|
|
532
|
+
'usage.dashboard.th.tokens': 'Tokens',
|
|
533
|
+
'usage.dashboard.th.calls': 'Calls',
|
|
534
|
+
'usage.explorer.sidebar.agents': 'Agents',
|
|
535
|
+
'usage.explorer.sidebar.peers': 'Peers',
|
|
536
|
+
'usage.explorer.chatType.group': 'Group',
|
|
537
|
+
'usage.explorer.chatType.private': 'Private',
|
|
538
|
+
'usage.explorer.memberCount': ' members',
|
|
539
|
+
'usage.explorer.selectHint': 'Select an Agent or Peer from the left',
|
|
540
|
+
'usage.explorer.all': 'All',
|
|
541
|
+
'usage.explorer.filter.from': 'From',
|
|
542
|
+
'usage.explorer.filter.to': 'To',
|
|
543
|
+
'usage.explorer.filter.model': 'Model',
|
|
544
|
+
'usage.explorer.filter.granularity': 'Granularity',
|
|
545
|
+
'usage.explorer.filter.granularity.hour': 'Hour',
|
|
546
|
+
'usage.explorer.filter.granularity.day': 'Day',
|
|
547
|
+
'usage.explorer.filter.granularity.week': 'Week',
|
|
548
|
+
'usage.explorer.filter.granularity.month': 'Month',
|
|
549
|
+
'usage.explorer.results': 'Results',
|
|
550
|
+
'usage.explorer.noData': 'No data for selected range.',
|
|
551
|
+
'usage.explorer.th.period': 'Period',
|
|
552
|
+
'usage.explorer.th.input': 'Input',
|
|
553
|
+
'usage.explorer.th.output': 'Output',
|
|
554
|
+
'usage.explorer.th.cacheCreation': 'Cache↑',
|
|
555
|
+
'usage.explorer.th.cacheHit': 'CacheHit',
|
|
556
|
+
'usage.explorer.th.calls': 'Calls',
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
let currentLang = localStorage.getItem(LANG_KEY) || 'zh-CN';
|
|
561
|
+
|
|
562
|
+
function t(key) {
|
|
563
|
+
return translations[currentLang]?.[key] || key;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function updateI18n() {
|
|
567
|
+
// 处理元素文本内容
|
|
568
|
+
document.querySelectorAll('[data-i18n]').forEach(el => {
|
|
569
|
+
const key = el.getAttribute('data-i18n');
|
|
570
|
+
const text = t(key);
|
|
571
|
+
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
|
|
572
|
+
el.placeholder = text;
|
|
573
|
+
} else if (el.tagName === 'OPTION') {
|
|
574
|
+
el.textContent = text;
|
|
575
|
+
} else {
|
|
576
|
+
el.textContent = text;
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
// 处理 title 属性(单独的 data-i18n-title)
|
|
580
|
+
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
|
581
|
+
const key = el.getAttribute('data-i18n-title');
|
|
582
|
+
el.title = t(key);
|
|
583
|
+
});
|
|
584
|
+
// 更新 html lang 属性
|
|
585
|
+
document.documentElement.lang = currentLang;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function toggleLang() {
|
|
589
|
+
currentLang = currentLang === 'zh-CN' ? 'en-US' : 'zh-CN';
|
|
590
|
+
localStorage.setItem(LANG_KEY, currentLang);
|
|
591
|
+
updateI18n();
|
|
592
|
+
// 强制重新渲染当前视图
|
|
593
|
+
if (state[currentView]) renderView(currentView);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ── 基础路径 ──
|
|
597
|
+
// 本地直连时页面在 "/",经 AUN Service Proxy 时页面在 "/ecweb/"。
|
|
598
|
+
// 取当前页面所在目录(含尾斜杠)作为所有 API/WS 的前缀,使绝对路径在两种
|
|
599
|
+
// 部署下都正确(proxy-server 用首段路径选服务,前缀不能丢)。
|
|
600
|
+
const BASE = location.pathname.replace(/[^/]*$/, '');
|
|
601
|
+
const apiUrl = (p) => BASE + p.replace(/^\/+/, '');
|
|
5
602
|
|
|
6
603
|
// ── 配对 ──
|
|
7
604
|
async function pair(code) {
|
|
8
|
-
const resp = await fetch('
|
|
605
|
+
const resp = await fetch(apiUrl('api/pair'), {
|
|
9
606
|
method: 'POST',
|
|
10
607
|
headers: { 'Content-Type': 'application/json' },
|
|
11
608
|
body: JSON.stringify({ code }),
|
|
@@ -13,6 +610,19 @@ async function pair(code) {
|
|
|
13
610
|
return resp.json();
|
|
14
611
|
}
|
|
15
612
|
|
|
613
|
+
// 本地直连免配对:用空码探测,服务端若判定本地直连会直接发 token。
|
|
614
|
+
// 远程(隧道/真远程)会返回配对码错误,此时回落到配对页。
|
|
615
|
+
async function tryLocalAutoPair() {
|
|
616
|
+
try {
|
|
617
|
+
const res = await pair('');
|
|
618
|
+
if (res && res.ok && res.token) {
|
|
619
|
+
localStorage.setItem(TOKEN_KEY, res.token);
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
} catch {}
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
|
|
16
626
|
function showPairPage(hint) {
|
|
17
627
|
if (ws) { try { ws.close(); } catch {} ws = null; }
|
|
18
628
|
$('#pair-page').style.display = 'flex';
|
|
@@ -31,7 +641,7 @@ function initPairUI() {
|
|
|
31
641
|
const err = $('#pair-error');
|
|
32
642
|
const submit = async () => {
|
|
33
643
|
const code = input.value.trim();
|
|
34
|
-
if (code.length !== 6) { err.textContent = '
|
|
644
|
+
if (code.length !== 6) { err.textContent = t('pair.error.length'); return; }
|
|
35
645
|
btn.disabled = true; err.textContent = '';
|
|
36
646
|
try {
|
|
37
647
|
const res = await pair(code);
|
|
@@ -40,10 +650,10 @@ function initPairUI() {
|
|
|
40
650
|
showApp();
|
|
41
651
|
startApp();
|
|
42
652
|
} else {
|
|
43
|
-
err.textContent = res.reason || '
|
|
653
|
+
err.textContent = res.reason || t('pair.error.failed');
|
|
44
654
|
}
|
|
45
655
|
} catch {
|
|
46
|
-
err.textContent = '
|
|
656
|
+
err.textContent = t('pair.error.network');
|
|
47
657
|
} finally {
|
|
48
658
|
btn.disabled = false;
|
|
49
659
|
}
|
|
@@ -56,9 +666,9 @@ function initPairUI() {
|
|
|
56
666
|
// ── WebSocket 客户端(自动重连)──
|
|
57
667
|
let ws = null;
|
|
58
668
|
let reconnectDelay = 1000;
|
|
59
|
-
let currentView = 'agents';
|
|
669
|
+
let currentView = localStorage.getItem(VIEW_KEY) || 'agents';
|
|
60
670
|
let pendingSub = null; // 重连后要恢复的订阅
|
|
61
|
-
const state = { agents: null, msg: null, session: null, cache: null, system: null, triggers: null, monitor: null };
|
|
671
|
+
const state = { agents: null, msg: null, session: null, cache: null, system: null, triggers: null, monitor: null, gateway: null };
|
|
62
672
|
|
|
63
673
|
function setConnStatus(text, cls) {
|
|
64
674
|
const el = $('#conn-status');
|
|
@@ -70,25 +680,49 @@ function connect() {
|
|
|
70
680
|
const token = localStorage.getItem(TOKEN_KEY);
|
|
71
681
|
if (!token) { showPairPage(); return; }
|
|
72
682
|
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
73
|
-
ws = new WebSocket(`${proto}://${location.host}
|
|
683
|
+
ws = new WebSocket(`${proto}://${location.host}${BASE}ws?token=${encodeURIComponent(token)}`);
|
|
74
684
|
|
|
75
685
|
ws.onopen = () => {
|
|
76
|
-
setConnStatus('●
|
|
686
|
+
setConnStatus('● ' + t('status.connected'), 'ok');
|
|
77
687
|
reconnectDelay = 1000;
|
|
78
|
-
|
|
688
|
+
// 获取可用的 baseagent
|
|
689
|
+
fetch(`${BASE}api/available-baseagents`)
|
|
690
|
+
.then(r => r.json())
|
|
691
|
+
.then(data => {
|
|
692
|
+
availableBaseagents = data;
|
|
693
|
+
// 如果当前没有选中 baseagent,默认选第一个可用的
|
|
694
|
+
if (!sessSel.baseagent) {
|
|
695
|
+
sessSel.baseagent = data.claude ? 'claude' : (data.codex ? 'codex' : null);
|
|
696
|
+
}
|
|
697
|
+
console.log('[ecweb] Available baseagents:', availableBaseagents, 'Selected:', sessSel.baseagent);
|
|
698
|
+
// 重新订阅当前视图(带上正确的参数)
|
|
699
|
+
if (currentView === 'session') {
|
|
700
|
+
subscribe('session', { sessionId: sessSel.sessionId, project: sessSel.project, baseagent: sessSel.baseagent });
|
|
701
|
+
} else {
|
|
702
|
+
subscribe(currentView, pendingSub || {});
|
|
703
|
+
}
|
|
704
|
+
})
|
|
705
|
+
.catch(err => {
|
|
706
|
+
console.warn('[ecweb] Failed to fetch available-baseagents:', err);
|
|
707
|
+
// 失败时默认使用 claude
|
|
708
|
+
availableBaseagents = { claude: true, codex: false };
|
|
709
|
+
if (!sessSel.baseagent) sessSel.baseagent = 'claude';
|
|
710
|
+
subscribe(currentView, pendingSub || {});
|
|
711
|
+
});
|
|
79
712
|
};
|
|
80
713
|
|
|
81
714
|
ws.onmessage = (ev) => {
|
|
82
715
|
let msg;
|
|
83
716
|
try { msg = JSON.parse(ev.data); } catch { return; }
|
|
84
717
|
if (msg.type === 'pong') return;
|
|
85
|
-
if (msg.type === 'error') { console.warn('
|
|
718
|
+
if (msg.type === 'error') { console.warn('[ecweb] Server error:', msg.message); return; }
|
|
86
719
|
if (msg.type === 'menu.response') {
|
|
87
720
|
const pend = _menuPending[msg.requestId];
|
|
88
721
|
if (pend) { delete _menuPending[msg.requestId]; pend.resolve(msg.data); }
|
|
89
722
|
return;
|
|
90
723
|
}
|
|
91
724
|
if (msg.type === 'snapshot' || msg.type === 'delta') {
|
|
725
|
+
console.log('[ecweb] Received', msg.type, 'for view:', msg.view, 'currentView:', currentView);
|
|
92
726
|
// system 视图保留客户端写入的 check/upgrade,防止 3s 轮询覆盖
|
|
93
727
|
if (msg.view === 'system' && state.system) {
|
|
94
728
|
state.system = {
|
|
@@ -99,17 +733,20 @@ function connect() {
|
|
|
99
733
|
} else {
|
|
100
734
|
state[msg.view] = msg.data;
|
|
101
735
|
}
|
|
102
|
-
if (msg.view === currentView)
|
|
736
|
+
if (msg.view === currentView) {
|
|
737
|
+
console.log('[ecweb] Rendering view:', currentView, 'with data:', msg.data);
|
|
738
|
+
renderView(currentView);
|
|
739
|
+
}
|
|
103
740
|
}
|
|
104
741
|
};
|
|
105
742
|
|
|
106
743
|
ws.onclose = (ev) => {
|
|
107
744
|
if (ev.code === 4001) {
|
|
108
745
|
localStorage.removeItem(TOKEN_KEY);
|
|
109
|
-
showPairPage('
|
|
746
|
+
showPairPage(t('pair.error.tokenInvalid'));
|
|
110
747
|
return;
|
|
111
748
|
}
|
|
112
|
-
setConnStatus('○
|
|
749
|
+
setConnStatus('○ ' + t('status.reconnecting') + '…', 'err');
|
|
113
750
|
setTimeout(connect, reconnectDelay);
|
|
114
751
|
reconnectDelay = Math.min(reconnectDelay * 1.5, 15000);
|
|
115
752
|
};
|
|
@@ -120,7 +757,14 @@ function connect() {
|
|
|
120
757
|
function subscribe(view, params) {
|
|
121
758
|
pendingSub = params;
|
|
122
759
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
760
|
+
// session 视图添加 baseagent 参数
|
|
761
|
+
if (view === 'session' && sessSel.baseagent) {
|
|
762
|
+
params = { ...params, baseagent: sessSel.baseagent };
|
|
763
|
+
}
|
|
764
|
+
console.log('[ecweb] Subscribing:', view, params);
|
|
123
765
|
ws.send(JSON.stringify({ type: 'subscribe', view, ...params }));
|
|
766
|
+
} else {
|
|
767
|
+
console.warn('[ecweb] WebSocket not ready, subscription pending');
|
|
124
768
|
}
|
|
125
769
|
}
|
|
126
770
|
|
|
@@ -147,24 +791,27 @@ setInterval(() => {
|
|
|
147
791
|
|
|
148
792
|
// ── Tab 切换 ──
|
|
149
793
|
let msgSel = { aid: null, peer: null };
|
|
150
|
-
let sessSel = { sessionId: null, project: null };
|
|
794
|
+
let sessSel = { sessionId: null, project: null, baseagent: null };
|
|
151
795
|
let trigSel = { agent: null };
|
|
152
796
|
let sessSearch = '';
|
|
153
797
|
let sessFilterNormal = false; // true=只显示有效会话(userMsgs >= 2)
|
|
154
798
|
let sessChatMode = false; // false=完整视图,true=对话视图(折叠处理过程)
|
|
155
799
|
let monRange = '2m'; // Monitor 时间窗口:2m / 10m / 1h
|
|
800
|
+
let availableBaseagents = { claude: false, codex: false }; // 可用的 baseagent
|
|
156
801
|
|
|
157
802
|
function switchView(view) {
|
|
158
803
|
currentView = view;
|
|
804
|
+
localStorage.setItem(VIEW_KEY, view);
|
|
159
805
|
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.view === view));
|
|
160
806
|
document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id === 'view-' + view));
|
|
161
807
|
// 切换时按当前选择恢复订阅
|
|
162
808
|
if (view === 'msg') subscribe('msg', { aid: msgSel.aid, peer: msgSel.peer });
|
|
163
|
-
else if (view === 'session') subscribe('session', { sessionId: sessSel.sessionId, project: sessSel.project });
|
|
809
|
+
else if (view === 'session') subscribe('session', { sessionId: sessSel.sessionId, project: sessSel.project, baseagent: sessSel.baseagent });
|
|
164
810
|
else if (view === 'cache') subscribe('cache', {});
|
|
165
811
|
else if (view === 'system') subscribe('system', {});
|
|
166
812
|
else if (view === 'triggers') subscribe('triggers', { agent: trigSel.agent });
|
|
167
813
|
else if (view === 'monitor') subscribe('monitor', { range: monRange });
|
|
814
|
+
else if (view === 'gateway') subscribe('gateway', {});
|
|
168
815
|
else subscribe('agents', {});
|
|
169
816
|
if (state[view]) renderView(view);
|
|
170
817
|
}
|
|
@@ -183,6 +830,7 @@ function renderView(view) {
|
|
|
183
830
|
else if (view === 'system') renderSystem(state.system);
|
|
184
831
|
else if (view === 'triggers') renderTriggers(state.triggers);
|
|
185
832
|
else if (view === 'monitor') renderMonitor(state.monitor);
|
|
833
|
+
else if (view === 'gateway') renderGateway(state.gateway);
|
|
186
834
|
}
|
|
187
835
|
|
|
188
836
|
// ── 工具 ──
|
|
@@ -190,6 +838,11 @@ function esc(s) {
|
|
|
190
838
|
return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
191
839
|
}
|
|
192
840
|
function shortAid(aid) { return String(aid || '').split('.')[0]; }
|
|
841
|
+
function shortId(id) {
|
|
842
|
+
const s = String(id || '');
|
|
843
|
+
if (!s) return 'unknown';
|
|
844
|
+
return s.includes('.') ? shortAid(s) : (s.length > 18 ? s.slice(0, 10) + '…' + s.slice(-5) : s);
|
|
845
|
+
}
|
|
193
846
|
function fmtBytes(b) {
|
|
194
847
|
if (!b) return '0';
|
|
195
848
|
const u = ['B', 'KB', 'MB', 'GB']; let i = Math.min(Math.floor(Math.log(b) / Math.log(1024)), 3);
|
|
@@ -246,25 +899,39 @@ let _agSubtab = 'enabled'; // 'enabled' | 'disabled'
|
|
|
246
899
|
// stopped → connected(仅首次连接无消息时) → idle(收到第一条后) → working → idle ...
|
|
247
900
|
function agentStateBadge(s, agStatus, connStatus) {
|
|
248
901
|
if (agStatus === 'stopped' || connStatus === 'disconnected' || connStatus === 'failed')
|
|
249
|
-
return
|
|
902
|
+
return `<span class="state-badge stopped">${t('status.stopped')}</span>`;
|
|
250
903
|
if (connStatus === 'reconnecting')
|
|
251
|
-
return
|
|
904
|
+
return `<span class="state-badge stopped">${t('status.reconnecting')}</span>`;
|
|
252
905
|
if ((s.processing || 0) > 0)
|
|
253
|
-
return
|
|
906
|
+
return `<span class="state-badge working">${t('status.working')}</span>`;
|
|
254
907
|
// 收到过消息 → 永远是 idle,不再回到 connected
|
|
255
|
-
if ((s.
|
|
256
|
-
|
|
257
|
-
|
|
908
|
+
if ((s.received || 0) > 0 || (s.sent || 0) > 0 || (s.completed || 0) > 0 || (s.errors || 0) > 0 || (s.interrupts || 0) > 0 ||
|
|
909
|
+
(s.messagesReceived || 0) > 0 || (s.messagesSent || 0) > 0)
|
|
910
|
+
return `<span class="state-badge idle">${t('status.idle')}</span>`;
|
|
911
|
+
return `<span class="state-badge connected">${t('status.connected')}</span>`;
|
|
258
912
|
}
|
|
259
913
|
|
|
260
914
|
// 发送方式图标标记
|
|
261
|
-
const MSG_KIND_META = {
|
|
262
|
-
|
|
915
|
+
const MSG_KIND_META = {
|
|
916
|
+
send: { icon: '💬', label: () => t('messages.msgKind.reply') },
|
|
917
|
+
thought: { icon: '💭', label: () => t('messages.msgKind.thought') },
|
|
918
|
+
inject: { icon: '📥', label: () => t('messages.msgKind.inject') },
|
|
919
|
+
notify: { icon: '🔔', label: () => t('messages.msgKind.notify') }
|
|
920
|
+
};
|
|
921
|
+
// 消息详情流用:jsonl 持久化的 msgType 词汇(text 为普通回复,不另标)
|
|
922
|
+
const MSG_TYPE_META = {
|
|
923
|
+
thought: { icon: '💭', label: () => t('messages.msgType.thought') },
|
|
924
|
+
image: { icon: '🖼️', label: () => t('messages.msgType.image') },
|
|
925
|
+
file: { icon: '📎', label: () => t('messages.msgType.file') },
|
|
926
|
+
command: { icon: '⌘', label: () => t('messages.msgType.command') }
|
|
927
|
+
};
|
|
928
|
+
function msgTagsHtml(kind, encrypt, chatmode, dir) {
|
|
263
929
|
let h = '';
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
if (
|
|
930
|
+
// 'send' 仅出向才是「回复」;入向是用户输入,不打回复标记
|
|
931
|
+
const km = (kind === 'send' && dir === 'in') ? null : MSG_KIND_META[kind];
|
|
932
|
+
if (km) h += `<span class="mtag${kind === 'send' ? ' mtag-reply' : ''}">${km.icon}${km.label()}</span>`;
|
|
933
|
+
if (encrypt != null) h += `<span class="mtag">${encrypt ? t('messages.tag.encrypted') : t('messages.tag.plain')}</span>`;
|
|
934
|
+
if (chatmode) h += `<span class="mtag">${chatmode === 'proactive' ? t('messages.tag.proactive') : (chatmode === 'inject' ? t('messages.tag.inject') : t('messages.tag.responsive'))}</span>`;
|
|
268
935
|
return h;
|
|
269
936
|
}
|
|
270
937
|
|
|
@@ -273,7 +940,7 @@ function agentPreviewHtml(s) {
|
|
|
273
940
|
const clip = (t) => esc(String(t).replace(/\n/g, ' ').slice(0, 80));
|
|
274
941
|
const line = (dir, peer, text, kind, encrypt, chatmode) => {
|
|
275
942
|
const arrow = dir === 'in' ? '<span class="arrow-in">↓</span>' : '<span class="arrow-out">↑</span>';
|
|
276
|
-
const tags = msgTagsHtml(kind, encrypt, chatmode);
|
|
943
|
+
const tags = msgTagsHtml(kind, encrypt, chatmode, dir);
|
|
277
944
|
const peerHtml = peer ? `<span class="peer">${esc(shortAid(peer))}</span>: ` : '';
|
|
278
945
|
const textCls = dir === 'in' ? 'text-in' : 'text-out';
|
|
279
946
|
return `${arrow}${tags ? ' ' + tags + ' ' : ' '}${peerHtml}<span class="${textCls}">${clip(text)}</span>`;
|
|
@@ -289,32 +956,105 @@ function agentPreviewHtml(s) {
|
|
|
289
956
|
return '';
|
|
290
957
|
}
|
|
291
958
|
|
|
292
|
-
// HTML tooltip(最近 N
|
|
959
|
+
// HTML tooltip(最近 N 轮):时间 + 彩色箭头 + 方式 + 对端 + 文字
|
|
960
|
+
// 渲染为隐藏的内容持有节点(.msg-tip-src);实际展示由 initMsgTipFloat 的浮层负责
|
|
293
961
|
function recentMsgTooltipHtml(recent) {
|
|
294
962
|
if (!recent || !recent.length) return '';
|
|
295
|
-
let h = '<div class="msg-tip">';
|
|
963
|
+
let h = '<div class="msg-tip-src">';
|
|
296
964
|
for (const m of recent) {
|
|
297
965
|
const rcls = m.dir === 'in' ? 'tip-row-in' : 'tip-row-out';
|
|
298
966
|
const arrow = m.dir === 'in' ? '↓' : '↑';
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
const
|
|
967
|
+
// 'send' 仅出向才是「回复」;入向是用户输入,不打回复标记
|
|
968
|
+
const km = (m.kind === 'send' && m.dir === 'in') ? null : MSG_KIND_META[m.kind];
|
|
969
|
+
const kh = km ? `<span class="tip-kind${m.kind === 'send' ? ' tip-kind-reply' : ''}">${km.icon}${km.label()}</span>` : '';
|
|
970
|
+
const enc = m.encrypt != null ? `<span class="tip-flag">${m.encrypt ? t('messages.tag.encrypted') : t('messages.tag.plain')}</span>` : '';
|
|
971
|
+
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
972
|
const peer = m.peer ? esc(shortAid(m.peer)) : '';
|
|
304
|
-
const text = esc(String(m.text).replace(/\n/g, ' ').slice(0,
|
|
305
|
-
|
|
973
|
+
const text = esc(String(m.text).replace(/\n/g, ' ').slice(0, 140));
|
|
974
|
+
const time = m.ts ? `<span class="tip-time">${fmtTime(m.ts)}</span>` : '';
|
|
975
|
+
h += `<div class="tip-row ${rcls}">${time}${arrow}${kh}${enc}${mode} <b>${peer}</b> ${text}</div>`;
|
|
306
976
|
}
|
|
307
977
|
return h + '</div>';
|
|
308
978
|
}
|
|
309
979
|
|
|
310
|
-
//
|
|
311
|
-
|
|
980
|
+
// 单例浮层 tooltip:固定定位、自动翻转上下、横向夹取,确保始终在可视区域内;
|
|
981
|
+
// 鼠标可移动到 tooltip 上而不消失(延迟隐藏 + 进入取消)。
|
|
982
|
+
function initMsgTipFloat() {
|
|
983
|
+
if (initMsgTipFloat._done) return;
|
|
984
|
+
initMsgTipFloat._done = true;
|
|
985
|
+
|
|
986
|
+
let floatEl = null, hideTimer = null, curWrap = null;
|
|
987
|
+
const GAP = 8, MARGIN = 8;
|
|
988
|
+
|
|
989
|
+
function ensureFloat() {
|
|
990
|
+
if (floatEl) return floatEl;
|
|
991
|
+
floatEl = document.createElement('div');
|
|
992
|
+
floatEl.id = 'msg-tip-float';
|
|
993
|
+
floatEl.className = 'msg-tip';
|
|
994
|
+
document.body.appendChild(floatEl);
|
|
995
|
+
floatEl.addEventListener('mouseenter', cancelHide);
|
|
996
|
+
floatEl.addEventListener('mouseleave', scheduleHide);
|
|
997
|
+
return floatEl;
|
|
998
|
+
}
|
|
999
|
+
function cancelHide() { if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } }
|
|
1000
|
+
function scheduleHide() { cancelHide(); hideTimer = setTimeout(hideNow, 180); }
|
|
1001
|
+
function hideNow() { cancelHide(); curWrap = null; if (floatEl) floatEl.classList.remove('show'); }
|
|
1002
|
+
|
|
1003
|
+
function position(wrap) {
|
|
1004
|
+
const f = floatEl;
|
|
1005
|
+
const r = wrap.getBoundingClientRect();
|
|
1006
|
+
const vw = document.documentElement.clientWidth;
|
|
1007
|
+
const vh = document.documentElement.clientHeight;
|
|
1008
|
+
const fw = f.offsetWidth, fh = f.offsetHeight;
|
|
1009
|
+
// 纵向:优先放上方;上方放不下则放下方;都放不下则在视口内夹取
|
|
1010
|
+
let top;
|
|
1011
|
+
if (r.top - GAP - fh >= MARGIN) top = r.top - GAP - fh;
|
|
1012
|
+
else if (r.bottom + GAP + fh <= vh - MARGIN) top = r.bottom + GAP;
|
|
1013
|
+
else top = Math.max(MARGIN, Math.min(vh - MARGIN - fh, r.top - GAP - fh));
|
|
1014
|
+
// 横向:对齐左缘,超出右界则左移,再夹取左界
|
|
1015
|
+
let left = r.left;
|
|
1016
|
+
if (left + fw > vw - MARGIN) left = vw - MARGIN - fw;
|
|
1017
|
+
if (left < MARGIN) left = MARGIN;
|
|
1018
|
+
f.style.top = Math.round(top) + 'px';
|
|
1019
|
+
f.style.left = Math.round(left) + 'px';
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function show(wrap) {
|
|
1023
|
+
const src = wrap.querySelector('.msg-tip-src');
|
|
1024
|
+
if (!src || !src.innerHTML.trim()) return;
|
|
1025
|
+
const f = ensureFloat();
|
|
1026
|
+
cancelHide();
|
|
1027
|
+
if (curWrap !== wrap) { f.innerHTML = src.innerHTML; curWrap = wrap; }
|
|
1028
|
+
f.classList.add('show');
|
|
1029
|
+
position(wrap);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
document.addEventListener('mouseover', (e) => {
|
|
1033
|
+
const wrap = e.target.closest && e.target.closest('.ag-msg-wrap');
|
|
1034
|
+
if (wrap) show(wrap);
|
|
1035
|
+
});
|
|
1036
|
+
document.addEventListener('mouseout', (e) => {
|
|
1037
|
+
const wrap = e.target.closest && e.target.closest('.ag-msg-wrap');
|
|
1038
|
+
if (!wrap) return;
|
|
1039
|
+
const to = e.relatedTarget;
|
|
1040
|
+
if (to && (wrap.contains(to) || (floatEl && floatEl.contains(to)))) return;
|
|
1041
|
+
scheduleHide();
|
|
1042
|
+
});
|
|
1043
|
+
// 滚动时隐藏,避免浮层与行脱节
|
|
1044
|
+
window.addEventListener('scroll', hideNow, true);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// 顶部统计条:Gateway / AIDs total·connected·offline / Messages / Version·PID·Uptime
|
|
1048
|
+
function agentsStatsBar(data, aids, agentStats) {
|
|
312
1049
|
const connected = aids.filter(a => (a.status || 'connected') === 'connected').length;
|
|
313
1050
|
const offline = aids.length - connected;
|
|
314
|
-
let recv = 0, sent = 0,
|
|
315
|
-
for (const s of
|
|
316
|
-
recv += s.
|
|
317
|
-
|
|
1051
|
+
let recv = 0, sent = 0, done = 0, errors = 0, interrupts = 0;
|
|
1052
|
+
for (const s of agentStats) {
|
|
1053
|
+
recv += s.received || 0;
|
|
1054
|
+
sent += s.sent || 0;
|
|
1055
|
+
done += s.completed || 0;
|
|
1056
|
+
errors += s.errors || 0;
|
|
1057
|
+
interrupts += s.interrupts || 0;
|
|
318
1058
|
}
|
|
319
1059
|
const gws = [...new Set(aids.filter(a => a.gatewayUrl).map(a => a.gatewayUrl))];
|
|
320
1060
|
const gw = gws.length ? gws.map(esc).join(', ') : '—';
|
|
@@ -324,34 +1064,40 @@ function agentsStatsBar(data, aids, stats) {
|
|
|
324
1064
|
const ver = data.version || '—';
|
|
325
1065
|
|
|
326
1066
|
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">Version</span>${esc(ver)} · <span class="sg-k">PID</span>${pid} · <span class="sg-k">Uptime</span>${uptime}</span>`;
|
|
1067
|
+
h += `<span class="sg"><span class="sg-k">${t('agents.stats.gateway')}</span><span class="sg-gw">${gw}</span></span>`;
|
|
1068
|
+
h += `<span class="sg"><span class="sg-k">${t('agents.stats.aids')}</span>${aids.length} ${t('agents.stats.total')} · <span class="num-on">${connected} ${t('agents.stats.online')}</span>` +
|
|
1069
|
+
`${offline ? ` · <span class="num-off">${offline} ${t('agents.stats.offline')}</span>` : ''}</span>`;
|
|
1070
|
+
h += `<span class="sg"><span class="sg-k">${t('agents.stats.messages')}</span>收 ${recv} · 发 ${sent} · 错 ${errors} · 断 ${interrupts} · 完 ${done}</span>`;
|
|
1071
|
+
h += `<span class="sg"><span class="sg-k">${t('agents.stats.version')}</span>${esc(ver)} · <span class="sg-k">${t('agents.stats.pid')}</span>${pid} · <span class="sg-k">${t('agents.stats.uptime')}</span>${uptime}</span>`;
|
|
333
1072
|
h += '</div>';
|
|
334
1073
|
return h;
|
|
335
1074
|
}
|
|
336
1075
|
|
|
1076
|
+
function agentQueueHtml(s) {
|
|
1077
|
+
const processing = s.processing || 0;
|
|
1078
|
+
const queued = s.queued || 0;
|
|
1079
|
+
if (processing === 0 && queued === 0) return '<span class="ag-queue-empty">-</span>';
|
|
1080
|
+
return `<span class="ag-queue-num">${processing}/${queued}</span>`;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
337
1083
|
// 操作列 HTML(启用页):停止/启动 + 清空队列(conditional) + ···(禁用/重载/编辑/md/删除)
|
|
338
1084
|
function agentOpsHtml(aid, ag, s) {
|
|
339
1085
|
if (_agentOps.has(aid)) {
|
|
340
|
-
return `<div class="agent-ops agent-ops-busy"><span class="ops-busy-label">${esc(_agentOps.get(aid) || '
|
|
1086
|
+
return `<div class="agent-ops agent-ops-busy"><span class="ops-busy-label">${esc(_agentOps.get(aid) || t('common.operating'))}</span></div>`;
|
|
341
1087
|
}
|
|
342
1088
|
const queued = s.queued || 0;
|
|
343
1089
|
const running = ag.status === 'running';
|
|
344
1090
|
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="
|
|
1091
|
+
if (running) h += `<button class="ctrl-btn ops-stop" data-op="stop">${t('action.stop')}</button>`;
|
|
1092
|
+
else h += `<button class="ctrl-btn ops-start" data-op="start">${t('action.start')}</button>`;
|
|
1093
|
+
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
1094
|
h += `<div class="ops-more"><button class="ctrl-btn ops-more-btn" data-op="more">···</button>` +
|
|
349
1095
|
`<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"
|
|
1096
|
+
`<button class="ops-dd-item" data-op="toggle">${t('action.disable')}</button>` +
|
|
1097
|
+
`<button class="ops-dd-item" data-op="reload">${t('action.reload')}</button>` +
|
|
1098
|
+
`<button class="ops-dd-item" data-op="edit">${t('action.edit')}</button>` +
|
|
1099
|
+
`<a class="ops-dd-item" href="https://${esc(aid)}/agent.md" target="_blank" rel="noopener">${t('agents.op.viewAgentMd')}</a>` +
|
|
1100
|
+
`<button class="ops-dd-item danger" data-op="delete">${t('action.delete')}</button>` +
|
|
355
1101
|
`</div></div>`;
|
|
356
1102
|
h += '</div>';
|
|
357
1103
|
return h;
|
|
@@ -359,13 +1105,15 @@ function agentOpsHtml(aid, ag, s) {
|
|
|
359
1105
|
|
|
360
1106
|
function renderAgents(data) {
|
|
361
1107
|
const el = $('#view-agents');
|
|
362
|
-
if (!data) { el.innerHTML =
|
|
1108
|
+
if (!data) { el.innerHTML = `<div class="empty">${t('common.loading')}</div>`; return; }
|
|
363
1109
|
if (el.querySelector('.ops-more.open')) return;
|
|
364
1110
|
|
|
365
1111
|
const allAgents = data.agents || [];
|
|
366
1112
|
const aids = data.aids || [];
|
|
367
1113
|
const statsByAid = {};
|
|
368
1114
|
for (const s of (data.stats || [])) statsByAid[s.aid] = s;
|
|
1115
|
+
const agentStatsByAid = {};
|
|
1116
|
+
for (const s of (data.agentStats || [])) agentStatsByAid[s.aid] = s;
|
|
369
1117
|
const aidConnByAid = {};
|
|
370
1118
|
for (const a of aids) aidConnByAid[a.aid] = a;
|
|
371
1119
|
|
|
@@ -375,27 +1123,27 @@ function renderAgents(data) {
|
|
|
375
1123
|
// 子标签栏
|
|
376
1124
|
let html = '<div class="agents-toolbar">' +
|
|
377
1125
|
`<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"
|
|
1126
|
+
`<button class="ag-subtab${_agSubtab === 'enabled' ? ' active' : ''}" data-subtab="enabled">${t('agents.subtitle.enabled')} (${enabledCount})</button>` +
|
|
1127
|
+
`<button class="ag-subtab${_agSubtab === 'disabled' ? ' active' : ''}" data-subtab="disabled">${t('agents.subtitle.disabled')} (${disabledCount})</button>` +
|
|
380
1128
|
`</div>` +
|
|
381
|
-
`<button class="ctrl-btn" id="agent-new-btn"
|
|
1129
|
+
`<button class="ctrl-btn" id="agent-new-btn">${t('action.new')}</button>` +
|
|
382
1130
|
'</div>';
|
|
383
1131
|
|
|
384
1132
|
if (!data.daemonRunning) {
|
|
385
|
-
html +=
|
|
1133
|
+
html += `<div class="banner">${t('agents.daemonStopped')}</div>`;
|
|
386
1134
|
}
|
|
387
1135
|
|
|
388
1136
|
if (_agSubtab === 'disabled') {
|
|
389
1137
|
const disabledAgents = allAgents.filter(ag => ag.status === 'disabled');
|
|
390
1138
|
if (!disabledAgents.length) {
|
|
391
|
-
html +=
|
|
1139
|
+
html += `<div class="empty">${t('agents.empty.disabled')}</div>`;
|
|
392
1140
|
} else {
|
|
393
|
-
html +=
|
|
1141
|
+
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
1142
|
for (const ag of disabledAgents) {
|
|
395
1143
|
const busy = _agentOps.has(ag.aid);
|
|
396
1144
|
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"
|
|
1145
|
+
? `<div class="agent-ops agent-ops-busy"><span class="ops-busy-label">${esc(_agentOps.get(ag.aid) || t('common.operating'))}</span></div>`
|
|
1146
|
+
: `<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
1147
|
html += `<tr class="ag-main">` +
|
|
400
1148
|
`<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
1149
|
`<td style="font-size:11px;font-family:monospace">${esc(ag.projectPath || '—')}</td>` +
|
|
@@ -409,27 +1157,29 @@ function renderAgents(data) {
|
|
|
409
1157
|
}
|
|
410
1158
|
|
|
411
1159
|
// ── 启用页 ──
|
|
412
|
-
//
|
|
413
|
-
const
|
|
414
|
-
const s =
|
|
415
|
-
return (s.
|
|
1160
|
+
// 按全渠道任务活动降序排序(活跃的排前面)
|
|
1161
|
+
const totalActivity = (ag) => {
|
|
1162
|
+
const s = agentStatsByAid[ag.aid] || {};
|
|
1163
|
+
return (s.received || 0) + (s.sent || 0) + (s.completed || 0) + (s.errors || 0) + (s.interrupts || 0);
|
|
416
1164
|
};
|
|
417
1165
|
const enabledAgents = allAgents.filter(ag => ag.status !== 'disabled')
|
|
418
|
-
.sort((a, b) =>
|
|
1166
|
+
.sort((a, b) => totalActivity(b) - totalActivity(a));
|
|
419
1167
|
if (!enabledAgents.length) {
|
|
420
|
-
html +=
|
|
1168
|
+
html += `<div class="empty">${t('agents.empty.enabled')}</div>`;
|
|
421
1169
|
el.innerHTML = html;
|
|
422
1170
|
bindAgentsEvents(el);
|
|
423
1171
|
return;
|
|
424
1172
|
}
|
|
425
1173
|
|
|
426
1174
|
html += '<table><thead><tr>' +
|
|
427
|
-
'
|
|
428
|
-
'
|
|
1175
|
+
`<th>${t('agents.th.aid')}</th><th>${t('agents.th.work')}</th><th>${t('agents.th.queue')}</th><th>${t('agents.th.model')}</th><th>${t('agents.th.runtime')}</th>` +
|
|
1176
|
+
`<th>${t('agents.th.received')}</th><th>${t('agents.th.sent')}</th><th>${t('agents.th.errors')}</th><th>${t('agents.th.interrupts')}</th><th>${t('agents.th.completed')}</th>` +
|
|
1177
|
+
`<th>${t('agents.th.lastActivity')}</th><th>${t('agents.th.operations')}</th>` +
|
|
429
1178
|
'</tr></thead><tbody>';
|
|
430
1179
|
|
|
431
1180
|
for (const ag of enabledAgents) {
|
|
432
1181
|
const s = statsByAid[ag.aid] || {};
|
|
1182
|
+
const runStats = agentStatsByAid[ag.aid] || {};
|
|
433
1183
|
const conn = aidConnByAid[ag.aid] || {};
|
|
434
1184
|
const connStatus = conn.status || (ag.status === 'running' ? 'connected' : 'disconnected');
|
|
435
1185
|
const dotCls = connStatus === 'connected' ? 'on' : (connStatus === 'reconnecting' ? 'idle' : 'off');
|
|
@@ -437,10 +1187,7 @@ function renderAgents(data) {
|
|
|
437
1187
|
const uptime = (connStatus === 'connected' && conn.lastConnectedAt) ? fmtDur((Date.now() - conn.lastConnectedAt) / 1000) : '—';
|
|
438
1188
|
const lastTs = Math.max(s.lastReceivedAt || 0, s.lastSentAt || 0, ag.lastActivity || 0);
|
|
439
1189
|
const preview = agentPreviewHtml(s);
|
|
440
|
-
|
|
441
|
-
const rawQueued = s.queued || 0;
|
|
442
|
-
const queued = rawQueued;
|
|
443
|
-
const queueCell = queued > 0 ? `<span class="ag-queue-num">${queued}</span>` : '<span style="color:var(--dim)">0</span>';
|
|
1190
|
+
const queueCell = agentQueueHtml(runStats);
|
|
444
1191
|
const model = ag.model || ag.baseagent || '—';
|
|
445
1192
|
|
|
446
1193
|
const idCell = `<div class="ag-id"><span class="dot ${dotCls}" title="${esc(connStatus)}"></span>` +
|
|
@@ -449,15 +1196,17 @@ function renderAgents(data) {
|
|
|
449
1196
|
|
|
450
1197
|
html += `<tr class="ag-main">` +
|
|
451
1198
|
`<td>${idCell}</td>` +
|
|
452
|
-
`<td>${agentStateBadge(s, ag.status, connStatus)}</td>` +
|
|
1199
|
+
`<td>${agentStateBadge({ ...s, ...runStats }, ag.status, connStatus)}</td>` +
|
|
453
1200
|
`<td>${queueCell}</td>` +
|
|
454
1201
|
`<td style="font-size:11px;color:var(--dim)">${esc(model)}</td>` +
|
|
455
1202
|
`<td>${uptime}</td>` +
|
|
456
|
-
`<td>${
|
|
457
|
-
`<td>${
|
|
458
|
-
`<td>${
|
|
1203
|
+
`<td>${runStats.received || 0}</td>` +
|
|
1204
|
+
`<td>${runStats.sent || 0}</td>` +
|
|
1205
|
+
`<td>${runStats.errors || 0}</td>` +
|
|
1206
|
+
`<td>${runStats.interrupts || 0}</td>` +
|
|
1207
|
+
`<td>${runStats.completed || 0}</td>` +
|
|
459
1208
|
`<td>${fmtAgo(lastTs)}</td>` +
|
|
460
|
-
`<td class="agent-ops-cell">${agentOpsHtml(ag.aid, ag,
|
|
1209
|
+
`<td class="agent-ops-cell">${agentOpsHtml(ag.aid, ag, runStats)}</td>` +
|
|
461
1210
|
'</tr>';
|
|
462
1211
|
// 自定义 tooltip(HTML,hover 显示)
|
|
463
1212
|
const recent = (s.recentMessages || []);
|
|
@@ -470,7 +1219,7 @@ function renderAgents(data) {
|
|
|
470
1219
|
}
|
|
471
1220
|
html += '</tbody></table>';
|
|
472
1221
|
if (data.daemonRunning) {
|
|
473
|
-
html += agentsStatsBar(data, aids, data.
|
|
1222
|
+
html += agentsStatsBar(data, aids, data.agentStats || []);
|
|
474
1223
|
}
|
|
475
1224
|
el.innerHTML = html;
|
|
476
1225
|
bindAgentsEvents(el);
|
|
@@ -504,17 +1253,17 @@ function groupLabel(g) {
|
|
|
504
1253
|
|
|
505
1254
|
function renderCache(data) {
|
|
506
1255
|
const el = $('#view-cache');
|
|
507
|
-
if (!data) { el.innerHTML =
|
|
1256
|
+
if (!data) { el.innerHTML = `<div class="empty">${t('common.loading')}</div>`; return; }
|
|
508
1257
|
if (!data.daemonRunning) {
|
|
509
|
-
el.innerHTML =
|
|
1258
|
+
el.innerHTML = `<div class="banner">${t('cache.daemonStopped')}</div>`;
|
|
510
1259
|
return;
|
|
511
1260
|
}
|
|
512
1261
|
if (!data.supported || !data.stats) {
|
|
513
|
-
el.innerHTML =
|
|
1262
|
+
el.innerHTML = `<div class="banner">${t('cache.notSupported')}</div>`;
|
|
514
1263
|
return;
|
|
515
1264
|
}
|
|
516
1265
|
const s = data.stats;
|
|
517
|
-
const
|
|
1266
|
+
const tot = s.totals;
|
|
518
1267
|
const occ = s.occupancy || {};
|
|
519
1268
|
// 全部组占用合计
|
|
520
1269
|
let totalBytes = 0;
|
|
@@ -523,23 +1272,23 @@ function renderCache(data) {
|
|
|
523
1272
|
let html = '';
|
|
524
1273
|
|
|
525
1274
|
// ① 总览卡片
|
|
526
|
-
const rate = hitRate(
|
|
1275
|
+
const rate = hitRate(tot);
|
|
527
1276
|
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('
|
|
1277
|
+
html += card(t('cache.card.hitRate'), fmtPct(rate), rateCls(rate), `${fmtNum(tot.hits)} ${t('cache.card.hit')} / ${fmtNum(tot.misses)} ${t('cache.card.miss')}`);
|
|
1278
|
+
html += card(t('cache.card.reads'), fmtNum(tot.gets), '', `${fmtNum(tot.hits)} ${t('cache.card.hit')} · ${fmtNum(tot.misses)} ${t('cache.card.miss')}`);
|
|
1279
|
+
html += card(t('cache.card.entries'), fmtNum(s.size), '', fmtBytes(totalBytes) + ' ' + t('cache.card.memory'));
|
|
1280
|
+
html += card(t('cache.card.statChecks'), fmtNum(tot.statChecks), '', 'mtime ' + t('cache.policy.mtime'));
|
|
1281
|
+
html += card(t('cache.card.reReads'), fmtNum(tot.reReads), '', t('cache.policy.manual'));
|
|
1282
|
+
html += card(t('cache.card.evictions'), fmtNum(tot.evictions), tot.evictions ? 'idle' : '', 'LRU');
|
|
1283
|
+
html += card(t('cache.card.invalidations'), fmtNum(tot.invalidations), '', 'reload');
|
|
1284
|
+
html += card(t('cache.card.since'), fmtAgo(s.since) + ' ' + t('cache.card.ago'), '', fmtTime(s.since));
|
|
536
1285
|
html += '</div>';
|
|
537
1286
|
|
|
538
1287
|
// ② 按 group 表(每组命中率 + 占用 + 容量水位)
|
|
539
|
-
html +=
|
|
1288
|
+
html += `<h3 class="cache-h">${t('cache.section.byGroup')}</h3>`;
|
|
540
1289
|
html += '<table><thead><tr>' +
|
|
541
|
-
'
|
|
542
|
-
'
|
|
1290
|
+
`<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>` +
|
|
1291
|
+
`<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
1292
|
'</tr></thead><tbody>';
|
|
544
1293
|
const groups = Object.keys(s.byGroup).sort((a, b) => (s.byGroup[b].gets || 0) - (s.byGroup[a].gets || 0));
|
|
545
1294
|
for (const g of groups) {
|
|
@@ -565,11 +1314,15 @@ function renderCache(data) {
|
|
|
565
1314
|
html += '</tbody></table>';
|
|
566
1315
|
|
|
567
1316
|
// ③ 按 policy 表
|
|
568
|
-
html +=
|
|
1317
|
+
html += `<h3 class="cache-h">${t('cache.section.byPolicy')}</h3>`;
|
|
569
1318
|
html += '<table><thead><tr>' +
|
|
570
|
-
'
|
|
1319
|
+
`<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
1320
|
'</tr></thead><tbody>';
|
|
572
|
-
const POLICY_DESC = {
|
|
1321
|
+
const POLICY_DESC = {
|
|
1322
|
+
'on-reload': t('cache.policy.onReload'),
|
|
1323
|
+
'manual': t('cache.policy.manual'),
|
|
1324
|
+
'mtime': t('cache.policy.mtime')
|
|
1325
|
+
};
|
|
573
1326
|
for (const pol of ['on-reload', 'mtime', 'manual']) {
|
|
574
1327
|
const c = s.byPolicy[pol];
|
|
575
1328
|
if (!c || !c.gets) continue;
|
|
@@ -583,8 +1336,7 @@ function renderCache(data) {
|
|
|
583
1336
|
}
|
|
584
1337
|
html += '</tbody></table>';
|
|
585
1338
|
|
|
586
|
-
html +=
|
|
587
|
-
'渲染后结果(按 vars)不缓存,故不在此列。</div>';
|
|
1339
|
+
html += `<div class="cache-note">${t('cache.note')}</div>`;
|
|
588
1340
|
|
|
589
1341
|
el.innerHTML = html;
|
|
590
1342
|
}
|
|
@@ -600,17 +1352,20 @@ function card(label, value, valCls, sub) {
|
|
|
600
1352
|
// ── Messages 视图 ──
|
|
601
1353
|
function renderMsg(data) {
|
|
602
1354
|
if (!data) return;
|
|
603
|
-
const aids = data.aids || [];
|
|
1355
|
+
const aids = data.scopes || data.aids || [];
|
|
604
1356
|
const peers = data.peers || [];
|
|
605
1357
|
const messages = data.messages || [];
|
|
1358
|
+
if (data.scope && data.scope !== msgSel.aid) msgSel.aid = data.scope;
|
|
606
1359
|
|
|
607
1360
|
// 左:AID 列表
|
|
608
|
-
let aidsHtml =
|
|
1361
|
+
let aidsHtml = `<div class="col-title">${t('messages.colTitle.aid')}</div>`;
|
|
609
1362
|
for (const a of aids) {
|
|
610
1363
|
const sel = a.aid === msgSel.aid ? ' sel' : '';
|
|
1364
|
+
const name = a.selfAID && a.selfAID !== 'unknown' ? shortAid(a.selfAID) : 'unknown';
|
|
1365
|
+
const groupBit = a.groupCount ? ` · 群 ${a.groupCount}` : '';
|
|
611
1366
|
aidsHtml += `<div class="list-item${sel}" data-aid="${esc(a.aid)}">` +
|
|
612
|
-
`<div class="name">${esc(
|
|
613
|
-
`<div class="sub">↓${a.totalIn} ↑${a.totalOut} · ${a.peerCount}
|
|
1367
|
+
`<div class="name">${esc(name)}</div>` +
|
|
1368
|
+
`<div class="sub">↓${a.totalIn} ↑${a.totalOut} · ${a.peerCount} chats${groupBit} · ${fmtAgo(a.lastAt)}</div></div>`;
|
|
614
1369
|
}
|
|
615
1370
|
$('#msg-aids').innerHTML = aidsHtml;
|
|
616
1371
|
$('#msg-aids').querySelectorAll('.list-item').forEach(item => {
|
|
@@ -618,19 +1373,24 @@ function renderMsg(data) {
|
|
|
618
1373
|
});
|
|
619
1374
|
|
|
620
1375
|
// 中:对端列表
|
|
621
|
-
let peersHtml =
|
|
1376
|
+
let peersHtml = `<div class="col-title">${t('messages.colTitle.peers')}</div>`;
|
|
622
1377
|
if (msgSel.aid) {
|
|
623
1378
|
const allSel = msgSel.peer === null ? ' sel' : '';
|
|
624
|
-
peersHtml += `<div class="list-item${allSel}" data-peer=""><div class="name"
|
|
625
|
-
`<div class="sub">${peers.length}
|
|
1379
|
+
peersHtml += `<div class="list-item${allSel}" data-peer=""><div class="name">${t('messages.colTitle.all')}</div>` +
|
|
1380
|
+
`<div class="sub">${peers.length} chats</div></div>`;
|
|
626
1381
|
for (const p of peers) {
|
|
627
1382
|
const sel = p.peerId === msgSel.peer ? ' sel' : '';
|
|
1383
|
+
const displayName = p.chatType === 'group'
|
|
1384
|
+
? (p.groupName || p.peerName || p.groupId || p.peerId)
|
|
1385
|
+
: (p.peerName || p.peerId);
|
|
1386
|
+
const channelLabel = p.channelName && p.channelName !== 'main' ? `${p.channelType}/${p.channelName}` : (p.channelType || '');
|
|
1387
|
+
const typeLabel = p.chatType === 'group' ? `${channelLabel} · ${t('messages.tag.group')}` : channelLabel;
|
|
628
1388
|
peersHtml += `<div class="list-item${sel}" data-peer="${esc(p.peerId)}">` +
|
|
629
|
-
`<div class="name">${esc(
|
|
1389
|
+
`<div class="name">${esc(shortId(displayName))} <span class="tag">${esc(typeLabel)}</span></div>` +
|
|
630
1390
|
`<div class="sub">↓${p.inbound} ↑${p.outbound} · ${fmtAgo(p.lastAt)}</div></div>`;
|
|
631
1391
|
}
|
|
632
1392
|
} else {
|
|
633
|
-
peersHtml +=
|
|
1393
|
+
peersHtml += `<div class="empty">${t('messages.empty.selectAid')}</div>`;
|
|
634
1394
|
}
|
|
635
1395
|
$('#msg-peers').innerHTML = peersHtml;
|
|
636
1396
|
$('#msg-peers').querySelectorAll('.list-item').forEach(item => {
|
|
@@ -639,23 +1399,28 @@ function renderMsg(data) {
|
|
|
639
1399
|
|
|
640
1400
|
// 右:消息流
|
|
641
1401
|
const stream = $('#msg-stream');
|
|
642
|
-
if (!msgSel.aid) { stream.innerHTML =
|
|
1402
|
+
if (!msgSel.aid) { stream.innerHTML = `<div class="empty">${t('messages.empty.selectToView')}</div>`; return; }
|
|
643
1403
|
const atBottom = stream.scrollHeight - stream.scrollTop - stream.clientHeight < 60;
|
|
644
1404
|
let msgHtml = '';
|
|
645
1405
|
for (const m of messages) {
|
|
646
1406
|
const cls = m.dir === 'in' ? 'in' : 'out';
|
|
647
1407
|
const arrow = m.dir === 'in' ? '↓' : '↑';
|
|
648
|
-
const from =
|
|
1408
|
+
const from = shortId(m.from), to = shortId(m.to);
|
|
649
1409
|
const tags = [];
|
|
650
|
-
if (m.
|
|
651
|
-
if (m.
|
|
652
|
-
|
|
653
|
-
|
|
1410
|
+
if (m.channelType) tags.push(m.channelType);
|
|
1411
|
+
if (m.chatType === 'group') tags.push(t('messages.tag.group'));
|
|
1412
|
+
// 消息详情流的 kind 来自 jsonl 的 msgType(text/thought/image/file/command),
|
|
1413
|
+
// 与 agents 页内存态的 MsgKind(send/thought/inject/notify)不是同一套词汇。
|
|
1414
|
+
const mt = MSG_TYPE_META[m.msgType];
|
|
1415
|
+
if (mt) tags.push(`${mt.icon}${mt.label()}`);
|
|
1416
|
+
if (m.encrypt != null) tags.push(m.encrypt ? t('messages.tag.encrypted') : t('messages.tag.plain'));
|
|
1417
|
+
if (m.chatmode) tags.push(m.chatmode === 'proactive' ? t('messages.tag.proactive') : (m.chatmode === 'inject' ? t('messages.tag.inject') : t('messages.tag.responsive')));
|
|
1418
|
+
const tagHtml = tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('');
|
|
654
1419
|
msgHtml += `<div class="bubble ${cls}">` +
|
|
655
1420
|
`<div class="meta">${fmtTime(m.ts)} ${arrow} ${esc(from)}→${esc(to)}${tagHtml}</div>` +
|
|
656
1421
|
`<div class="body">${esc(m.content)}</div></div>`;
|
|
657
1422
|
}
|
|
658
|
-
stream.innerHTML = msgHtml ||
|
|
1423
|
+
stream.innerHTML = msgHtml || `<div class="empty">${t('messages.empty.noMessages')}</div>`;
|
|
659
1424
|
if (atBottom) stream.scrollTop = stream.scrollHeight;
|
|
660
1425
|
}
|
|
661
1426
|
|
|
@@ -683,8 +1448,19 @@ function renderSession(data) {
|
|
|
683
1448
|
const projOpts = projects.map(p =>
|
|
684
1449
|
`<option value="${esc(p.encoded)}"${p.encoded === sessSel.project ? ' selected' : ''}>${esc(p.label)} (${p.count})</option>`
|
|
685
1450
|
).join('');
|
|
1451
|
+
|
|
1452
|
+
// baseagent 下拉选择器(只显示可用的)
|
|
1453
|
+
let baseagentOpts = '';
|
|
1454
|
+
if (availableBaseagents.claude) {
|
|
1455
|
+
baseagentOpts += `<option value="claude"${sessSel.baseagent === 'claude' ? ' selected' : ''}>Claude</option>`;
|
|
1456
|
+
}
|
|
1457
|
+
if (availableBaseagents.codex) {
|
|
1458
|
+
baseagentOpts += `<option value="codex"${sessSel.baseagent === 'codex' ? ' selected' : ''}>Codex</option>`;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
686
1461
|
const normalCount = transcripts.filter(t => (t.userMsgs || 0) >= 2).length;
|
|
687
1462
|
let listHtml = '<div class="sess-filter">' +
|
|
1463
|
+
`<select id="sess-baseagent" title="Base Agent">${baseagentOpts}</select>` +
|
|
688
1464
|
`<select id="sess-project">${projOpts}</select>` +
|
|
689
1465
|
`<input id="sess-search" type="text" placeholder="搜索标题/首条消息…" value="${esc(sessSearch)}">` +
|
|
690
1466
|
`<button id="sess-filter-btn" class="ctrl-btn${sessFilterNormal ? ' active' : ''}" title="只显示有效会话(≥2 条用户消息)">有效 ${normalCount}</button>` +
|
|
@@ -712,11 +1488,19 @@ function renderSession(data) {
|
|
|
712
1488
|
$('#sess-list').innerHTML = listHtml;
|
|
713
1489
|
|
|
714
1490
|
// 绑定交互(注意保持搜索框焦点)
|
|
1491
|
+
const baseagentSel = $('#sess-baseagent');
|
|
1492
|
+
if (baseagentSel) baseagentSel.onchange = () => {
|
|
1493
|
+
console.log('[ecweb] Baseagent changed to:', baseagentSel.value);
|
|
1494
|
+
sessSel = { sessionId: null, project: null, baseagent: baseagentSel.value };
|
|
1495
|
+
sessSearch = '';
|
|
1496
|
+
console.log('[ecweb] Subscribing to session with baseagent:', sessSel.baseagent);
|
|
1497
|
+
subscribe('session', { baseagent: sessSel.baseagent });
|
|
1498
|
+
};
|
|
715
1499
|
const projSel = $('#sess-project');
|
|
716
1500
|
if (projSel) projSel.onchange = () => {
|
|
717
|
-
sessSel = { sessionId: null, project: projSel.value };
|
|
1501
|
+
sessSel = { sessionId: null, project: projSel.value, baseagent: sessSel.baseagent };
|
|
718
1502
|
sessSearch = '';
|
|
719
|
-
subscribe('session', { project: sessSel.project });
|
|
1503
|
+
subscribe('session', { project: sessSel.project, baseagent: sessSel.baseagent });
|
|
720
1504
|
};
|
|
721
1505
|
const filterBtn = $('#sess-filter-btn');
|
|
722
1506
|
if (filterBtn) filterBtn.onclick = () => { sessFilterNormal = !sessFilterNormal; renderSession(state.session); };
|
|
@@ -726,7 +1510,7 @@ function renderSession(data) {
|
|
|
726
1510
|
if (q) { searchEl.focus(); searchEl.setSelectionRange(searchEl.value.length, searchEl.value.length); }
|
|
727
1511
|
}
|
|
728
1512
|
$('#sess-list').querySelectorAll('.list-item').forEach(item => {
|
|
729
|
-
item.onclick = () => { sessSel = { sessionId: item.dataset.sid, project: sessSel.project }; subscribe('session', sessSel); };
|
|
1513
|
+
item.onclick = () => { sessSel = { sessionId: item.dataset.sid, project: sessSel.project, baseagent: sessSel.baseagent }; subscribe('session', sessSel); };
|
|
730
1514
|
});
|
|
731
1515
|
|
|
732
1516
|
// 右:transcript 详情
|
|
@@ -947,8 +1731,8 @@ function setAgentOp(aid, label) {
|
|
|
947
1731
|
if (ag.status === 'disabled') {
|
|
948
1732
|
// 禁用页:只有启用按钮 / 操作中态
|
|
949
1733
|
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"
|
|
1734
|
+
? `<div class="agent-ops agent-ops-busy"><span class="ops-busy-label">${esc(_agentOps.get(aid) || t('common.operating'))}</span></div>`
|
|
1735
|
+
: `<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
1736
|
} else {
|
|
953
1737
|
const statsByAid = {};
|
|
954
1738
|
for (const s of (state.agents.stats || [])) statsByAid[s.aid] = s;
|
|
@@ -1018,29 +1802,29 @@ async function withAgentOp(aid, label, fn) {
|
|
|
1018
1802
|
}
|
|
1019
1803
|
|
|
1020
1804
|
async function agentOpReload(aid, force = false) {
|
|
1021
|
-
await withAgentOp(aid, '
|
|
1805
|
+
await withAgentOp(aid, t('agents.op.reloading'), async () => {
|
|
1022
1806
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'reload', args: { aid, force } }));
|
|
1023
1807
|
if (r.error?.code === 'BUSY') {
|
|
1024
|
-
if (confirm(r.error.message + '\n
|
|
1808
|
+
if (confirm(r.error.message + '\n' + t('agents.op.confirmReload'))) { setAgentOp(aid, null); return agentOpReload(aid, true); }
|
|
1025
1809
|
return;
|
|
1026
1810
|
}
|
|
1027
1811
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1028
|
-
toast('
|
|
1812
|
+
toast(t('agents.op.reloaded'));
|
|
1029
1813
|
subscribe('agents', {});
|
|
1030
1814
|
});
|
|
1031
1815
|
}
|
|
1032
1816
|
|
|
1033
1817
|
async function agentOpToggle(aid, status) {
|
|
1034
1818
|
const action = status === 'disabled' ? 'enable' : 'disable';
|
|
1035
|
-
const label = action === 'disable' ? '
|
|
1819
|
+
const label = action === 'disable' ? t('agents.op.disabling') : t('agents.op.enabling');
|
|
1036
1820
|
await withAgentOp(aid, label, async () => {
|
|
1037
1821
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action, args: { aid } }));
|
|
1038
1822
|
if (r.error?.code === 'BUSY') {
|
|
1039
|
-
if (confirm(r.error.message + `\n
|
|
1823
|
+
if (confirm(r.error.message + `\n${t('agents.op.confirmToggle')}${action === 'disable' ? t('action.disable') : t('action.enable')}?`)) {
|
|
1040
1824
|
const r2 = mResp(await menuSend({ type: 'menu.action', name: 'agent', action, args: { aid, force: true } }));
|
|
1041
1825
|
if (r2.error) toast(r2.error.message || r2.error.code, true);
|
|
1042
1826
|
else {
|
|
1043
|
-
toast(
|
|
1827
|
+
toast(action === 'disable' ? t('agents.op.disabled') : t('agents.op.enabled'));
|
|
1044
1828
|
// 禁用后立即切到禁用页;启用后等数据刷新(agent 需先完成启动才移到启用页)
|
|
1045
1829
|
if (action === 'disable') _agSubtab = 'disabled';
|
|
1046
1830
|
subscribe('agents', {});
|
|
@@ -1049,55 +1833,55 @@ async function agentOpToggle(aid, status) {
|
|
|
1049
1833
|
return;
|
|
1050
1834
|
}
|
|
1051
1835
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1052
|
-
toast(
|
|
1836
|
+
toast(action === 'disable' ? t('agents.op.disabled') : t('agents.op.enabled'));
|
|
1053
1837
|
if (action === 'disable') _agSubtab = 'disabled';
|
|
1054
1838
|
subscribe('agents', {});
|
|
1055
1839
|
});
|
|
1056
1840
|
}
|
|
1057
1841
|
|
|
1058
1842
|
async function agentOpDelete(aid) {
|
|
1059
|
-
if (!confirm(
|
|
1843
|
+
if (!confirm(t('agents.op.confirmDelete').replace('{aid}', aid))) return;
|
|
1060
1844
|
const purge = confirm('同时清除 agent 数据目录?');
|
|
1061
|
-
await withAgentOp(aid, '
|
|
1845
|
+
await withAgentOp(aid, t('agents.op.deleting'), async () => {
|
|
1062
1846
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'delete', args: { aid, purge } }));
|
|
1063
1847
|
if (r.error?.code === 'BUSY') {
|
|
1064
|
-
if (confirm(r.error.message + '\n
|
|
1848
|
+
if (confirm(r.error.message + '\n' + t('agents.op.confirmForceDelete'))) {
|
|
1065
1849
|
const r2 = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'delete', args: { aid, purge, force: true } }));
|
|
1066
1850
|
if (r2.error) toast(r2.error.message || r2.error.code, true);
|
|
1067
|
-
else { toast('
|
|
1851
|
+
else { toast(t('agents.op.deleted')); subscribe('agents', {}); }
|
|
1068
1852
|
}
|
|
1069
1853
|
return;
|
|
1070
1854
|
}
|
|
1071
1855
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1072
|
-
toast('
|
|
1856
|
+
toast(t('agents.op.deleted'));
|
|
1073
1857
|
subscribe('agents', {});
|
|
1074
1858
|
});
|
|
1075
1859
|
}
|
|
1076
1860
|
|
|
1077
1861
|
async function agentOpClearQueue(aid) {
|
|
1078
|
-
if (!confirm(
|
|
1079
|
-
await withAgentOp(aid, '
|
|
1862
|
+
if (!confirm(t('agents.op.confirmClearQueue').replace('{aid}', aid))) return;
|
|
1863
|
+
await withAgentOp(aid, t('common.operating'), async () => {
|
|
1080
1864
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'queue-clear', args: { aid } }));
|
|
1081
1865
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1082
|
-
toast(`✓
|
|
1866
|
+
toast(`✓ ${r.data?.cleared ?? 0} messages cleared`);
|
|
1083
1867
|
subscribe('agents', {});
|
|
1084
1868
|
});
|
|
1085
1869
|
}
|
|
1086
1870
|
|
|
1087
1871
|
async function agentOpStop(aid) {
|
|
1088
|
-
await withAgentOp(aid, '
|
|
1872
|
+
await withAgentOp(aid, t('agents.op.stopping'), async () => {
|
|
1089
1873
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'stop', args: { aid } }));
|
|
1090
1874
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1091
|
-
toast('
|
|
1875
|
+
toast(t('agents.op.stopped'));
|
|
1092
1876
|
subscribe('agents', {});
|
|
1093
1877
|
});
|
|
1094
1878
|
}
|
|
1095
1879
|
|
|
1096
1880
|
async function agentOpStart(aid) {
|
|
1097
|
-
await withAgentOp(aid, '
|
|
1881
|
+
await withAgentOp(aid, t('agents.op.starting'), async () => {
|
|
1098
1882
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'start', args: { aid } }));
|
|
1099
1883
|
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
1100
|
-
toast('
|
|
1884
|
+
toast(t('agents.op.started'));
|
|
1101
1885
|
subscribe('agents', {});
|
|
1102
1886
|
});
|
|
1103
1887
|
}
|
|
@@ -1136,7 +1920,7 @@ async function agentOpNew() {
|
|
|
1136
1920
|
}
|
|
1137
1921
|
|
|
1138
1922
|
async function agentOpEdit(aid) {
|
|
1139
|
-
await withAgentOp(aid, '
|
|
1923
|
+
await withAgentOp(aid, t('common.operating'), async () => {
|
|
1140
1924
|
const qr = await menuSend({ type: 'menu.query', name: 'agent', args: { aid } });
|
|
1141
1925
|
const q = mResp(qr);
|
|
1142
1926
|
if (q.error) { toast(q.error.message || q.error.code, true); return; }
|
|
@@ -1148,10 +1932,10 @@ async function agentOpEdit(aid) {
|
|
|
1148
1932
|
if (projectRaw !== null) patch.projects = { defaultPath: projectRaw };
|
|
1149
1933
|
if (ownersRaw !== null) patch.owners = ownersRaw.split(',').map(s => s.trim()).filter(Boolean);
|
|
1150
1934
|
if (!Object.keys(patch).length) return;
|
|
1151
|
-
setAgentOp(aid, '
|
|
1935
|
+
setAgentOp(aid, t('common.operating'));
|
|
1152
1936
|
const r = mResp(await menuSend({ type: 'menu.action', name: 'agent', action: 'update', args: { aid, patch } }));
|
|
1153
1937
|
if (r.error) toast(r.error.message || r.error.code, true);
|
|
1154
|
-
else toast('
|
|
1938
|
+
else toast(t('agents.op.saved'));
|
|
1155
1939
|
});
|
|
1156
1940
|
}
|
|
1157
1941
|
|
|
@@ -1164,7 +1948,7 @@ function channelHealthRow(c) {
|
|
|
1164
1948
|
if (c.flapCount > 0) meta += ` <span style="color:var(--red)">抖动 ${c.flapCount}</span>`;
|
|
1165
1949
|
const reason = c.kickReason || c.lastError;
|
|
1166
1950
|
if (reason && !c.connected) meta += ` <span style="color:var(--red)" title="${esc(reason)}">"${esc(reason)}"</span>`;
|
|
1167
|
-
return `<div class="ch-row"><span class="dot ${dot}"></span>${esc(c.type)}${c.instName
|
|
1951
|
+
return `<div class="ch-row"><span class="dot ${dot}"></span>${esc(c.type)}${c.instName ? ' ' + esc(c.instName) : ''}${meta}</div>`;
|
|
1168
1952
|
}
|
|
1169
1953
|
|
|
1170
1954
|
function agentHealthCard(ag) {
|
|
@@ -1181,7 +1965,7 @@ function agentHealthCard(ag) {
|
|
|
1181
1965
|
for (const c of (ag.channels || [])) chans += channelHealthRow(c);
|
|
1182
1966
|
h += `<div class="ahc-row"><span class="ahc-k">渠道</span><span class="ahc-v">${chans || '<span style="color:var(--dim)">无</span>'}</span></div>`;
|
|
1183
1967
|
// 负载
|
|
1184
|
-
const load = `${ag.processing ?? 0} 处理中 · ${ag.pending ?? 0}
|
|
1968
|
+
const load = `${ag.processing ?? 0} 处理中 · ${ag.pending ?? 0} 待处理`;
|
|
1185
1969
|
h += `<div class="ahc-row"><span class="ahc-k">负载</span><span class="ahc-v">${load}</span></div>`;
|
|
1186
1970
|
// 活动
|
|
1187
1971
|
if (ag.lastActivity) h += `<div class="ahc-row"><span class="ahc-k">活动</span><span class="ahc-v">${fmtAgo(ag.lastActivity)} 前</span></div>`;
|
|
@@ -1191,6 +1975,16 @@ function agentHealthCard(ag) {
|
|
|
1191
1975
|
return h;
|
|
1192
1976
|
}
|
|
1193
1977
|
|
|
1978
|
+
function systemBaseagentCards(baseagents) {
|
|
1979
|
+
const list = Array.isArray(baseagents) ? baseagents : [];
|
|
1980
|
+
return list.map(ba => {
|
|
1981
|
+
const ver = ba.version ? `・${ba.version}` : '';
|
|
1982
|
+
const title = `Baseagent・${ba.active ? '✓ ' : ''}${ba.name || 'unknown'}${ver}`;
|
|
1983
|
+
const detail = [ba.model, ba.effort].filter(Boolean).map(esc).join(' · ') || '未指定模型/强度';
|
|
1984
|
+
return `<div class="cache-card"><div class="card-label">${esc(title)}</div><div class="card-val">${detail}</div></div>`;
|
|
1985
|
+
}).join('');
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1194
1988
|
function renderSystem(data) {
|
|
1195
1989
|
const el = $('#view-system');
|
|
1196
1990
|
if (!data) { el.innerHTML = '<div class="empty">加载中…</div>'; return; }
|
|
@@ -1231,27 +2025,36 @@ function renderSystem(data) {
|
|
|
1231
2025
|
|
|
1232
2026
|
// ③ 健康快照
|
|
1233
2027
|
if (chk) {
|
|
2028
|
+
// 从 chk.structured 读取数据(后端返回的数据结构)
|
|
2029
|
+
const s = chk.structured || chk; // 兼容旧版本(如果 chk 本身就是 structured)
|
|
1234
2030
|
html += '<div class="sys-health">';
|
|
1235
2031
|
// 队列 + 近 1 小时(数字卡片同一行)
|
|
1236
2032
|
html += '<div class="cache-cards" style="margin-bottom:8px">';
|
|
1237
|
-
html += `<div class="cache-card"><div class="card-label">队列</div><div class="card-val">${
|
|
1238
|
-
const h =
|
|
2033
|
+
html += `<div class="cache-card"><div class="card-label">队列</div><div class="card-val">${s.queue?.pending ?? 0} 待 · ${s.queue?.processing ?? 0} 处理中</div></div>`;
|
|
2034
|
+
const h = s.lastHour;
|
|
1239
2035
|
if (h) {
|
|
1240
2036
|
const errDetail = h.errors > 0 ? ` (${Object.entries(h.errorsByType || {}).map(([t, c]) => `${t}:${c}`).join(', ')})` : '';
|
|
1241
2037
|
const avg = h.completed > 0 ? ` · 均 ${(h.avgResponseMs / 1000).toFixed(1)}s` : '';
|
|
1242
2038
|
html += `<div class="cache-card"><div class="card-label">近 1 小时</div><div class="card-val">收 ${h.received} · 完 ${h.completed} · 错 ${h.errors}${errDetail} · 断 ${h.interrupts}${avg}</div></div>`;
|
|
1243
2039
|
}
|
|
2040
|
+
html += systemBaseagentCards(sys.baseagents);
|
|
1244
2041
|
html += '</div>';
|
|
1245
2042
|
// 每个 EvolAgent 一张卡片:后端 + 渠道健康 + 负载
|
|
1246
|
-
|
|
2043
|
+
// 排序:启用的(非 disabled)在前,停用的(disabled)在后
|
|
2044
|
+
if (s.evolagents?.length) {
|
|
2045
|
+
const sortedAgents = s.evolagents.slice().sort((a, b) => {
|
|
2046
|
+
const aDisabled = a.status === 'disabled' ? 1 : 0;
|
|
2047
|
+
const bDisabled = b.status === 'disabled' ? 1 : 0;
|
|
2048
|
+
return aDisabled - bDisabled;
|
|
2049
|
+
});
|
|
1247
2050
|
html += '<div class="agent-health-grid">';
|
|
1248
|
-
for (const ag of
|
|
2051
|
+
for (const ag of sortedAgents) html += agentHealthCard(ag);
|
|
1249
2052
|
html += '</div>';
|
|
1250
2053
|
}
|
|
1251
2054
|
// 未归属任何 EvolAgent 的渠道(系统级 / DefaultAgent)
|
|
1252
|
-
if (
|
|
2055
|
+
if (s.unownedChannels?.length) {
|
|
1253
2056
|
html += '<div class="cache-card" style="margin-top:8px"><div class="card-label">未归属渠道</div>';
|
|
1254
|
-
for (const c of
|
|
2057
|
+
for (const c of s.unownedChannels) html += channelHealthRow(c);
|
|
1255
2058
|
html += '</div>';
|
|
1256
2059
|
}
|
|
1257
2060
|
html += '</div>';
|
|
@@ -1288,6 +2091,276 @@ function bindSystemEvents(el, data) {
|
|
|
1288
2091
|
});
|
|
1289
2092
|
}
|
|
1290
2093
|
|
|
2094
|
+
// ── Gateway 视图(网关 = baseagent 后端接入配置) ──
|
|
2095
|
+
// 数据来自 daemon menu.query name=gateway(apiKey 已掩码)。
|
|
2096
|
+
// 写操作走 menuSend({name:'gateway', ...}):update/test/delete。
|
|
2097
|
+
|
|
2098
|
+
// 各 baseagent 类型的可编辑字段定义(驱动编辑表单与展示)
|
|
2099
|
+
const GATEWAY_FIELDS = {
|
|
2100
|
+
claude: [
|
|
2101
|
+
{ key: 'baseUrl', label: 'Base URL', placeholder: 'https://gateway.example.com(留空=官方)' },
|
|
2102
|
+
{ key: 'model', label: '默认模型', placeholder: 'opus / sonnet / claude-...' },
|
|
2103
|
+
{ key: 'effort', label: 'Effort', placeholder: 'low / medium / high / xhigh / max' },
|
|
2104
|
+
],
|
|
2105
|
+
codex: [
|
|
2106
|
+
{ key: 'baseUrl', label: 'Base URL', placeholder: 'https://gateway.example.com(留空=官方)' },
|
|
2107
|
+
{ key: 'model', label: '默认模型', placeholder: 'gpt-5.2-codex / ...' },
|
|
2108
|
+
{ key: 'effort', label: 'Effort', placeholder: 'low / medium / high' },
|
|
2109
|
+
{ key: 'reasoning', label: 'Reasoning', placeholder: '(可选)' },
|
|
2110
|
+
],
|
|
2111
|
+
gemini: [
|
|
2112
|
+
{ key: 'model', label: '默认模型', placeholder: 'gemini-2.5-flash / ...' },
|
|
2113
|
+
{ key: 'mode', label: '模式', placeholder: 'cli / sdk' },
|
|
2114
|
+
{ key: 'cliPath', label: 'CLI 路径', placeholder: 'gemini' },
|
|
2115
|
+
{ key: 'project', label: 'GCP Project', placeholder: '(Vertex 用)' },
|
|
2116
|
+
{ key: 'location', label: 'Location', placeholder: 'us-central1' },
|
|
2117
|
+
],
|
|
2118
|
+
};
|
|
2119
|
+
|
|
2120
|
+
const GATEWAY_TYPE_ICON = { claude: '🟣', codex: '🟢', gemini: '🔵' };
|
|
2121
|
+
|
|
2122
|
+
// 标记每条网关的运行时测试结果:`${scope}#${type}` → { ok, latency, modelCount, error }
|
|
2123
|
+
const _gwTest = new Map();
|
|
2124
|
+
let _gwEditing = null; // 当前编辑中的网关 key(`${scope}#${type}`)或 'new'
|
|
2125
|
+
|
|
2126
|
+
function gwKey(scope, type) { return scope + '#' + type; }
|
|
2127
|
+
|
|
2128
|
+
function renderGateway(data) {
|
|
2129
|
+
const el = $('#view-gateway');
|
|
2130
|
+
|
|
2131
|
+
if (!data) { el.innerHTML = '<div class="empty">加载中…</div>'; return; }
|
|
2132
|
+
if (data.error) {
|
|
2133
|
+
el.innerHTML = `<div class="empty">⚠ ${esc(data.error)}</div>`;
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
const gateways = data.gateways || [];
|
|
2137
|
+
const scopes = data.scopes || ['defaults'];
|
|
2138
|
+
|
|
2139
|
+
// 按 scope 分组
|
|
2140
|
+
const byScope = new Map();
|
|
2141
|
+
for (const s of scopes) byScope.set(s, []);
|
|
2142
|
+
for (const g of gateways) {
|
|
2143
|
+
if (!byScope.has(g.scope)) byScope.set(g.scope, []);
|
|
2144
|
+
byScope.get(g.scope).push(g);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
let html = '<div class="gw-wrap">';
|
|
2148
|
+
|
|
2149
|
+
html += '<div class="gw-intro">网关 = 各 AI 后端(baseagent)的接入配置。Base URL 即网关地址,留空走官方端点。' +
|
|
2150
|
+
'此处为只读展示,配置请通过配置文件(defaults.json / agents/<aid>/config.json)管理。</div>';
|
|
2151
|
+
|
|
2152
|
+
// 只展示全局默认配置块(用于编辑)
|
|
2153
|
+
for (const [scope, list] of byScope) {
|
|
2154
|
+
if (scope !== 'defaults') continue; // 跳过 per-agent 原始配置块(已在下方 effective 展示)
|
|
2155
|
+
const scopeLabel = '🌐 全局默认 (defaults)';
|
|
2156
|
+
html += `<div class="gw-scope">`;
|
|
2157
|
+
html += `<div class="gw-scope-head"><span class="gw-scope-title">${scopeLabel}</span></div>`;
|
|
2158
|
+
html += '<div class="gw-cards">';
|
|
2159
|
+
if (!list.length) {
|
|
2160
|
+
html += '<div class="empty" style="padding:12px">该作用域暂无网关配置</div>';
|
|
2161
|
+
} else {
|
|
2162
|
+
for (const g of list) html += gatewayCard(g);
|
|
2163
|
+
}
|
|
2164
|
+
html += '</div></div>';
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
// ── Agent 使用配置(effective):紧凑表格 + 编辑按钮 ──
|
|
2168
|
+
const effective = data.effective || [];
|
|
2169
|
+
if (effective.length > 0) {
|
|
2170
|
+
html += '<div class="gw-effective-section">';
|
|
2171
|
+
html += '<div class="gw-scope-head"><span class="gw-scope-title">📋 Agent 网关配置</span></div>';
|
|
2172
|
+
html += '<table class="gw-eff-table"><thead><tr>' +
|
|
2173
|
+
'<th>Agent</th><th>Base Agent</th><th>Base URL</th><th>模型</th><th>API Key</th><th>Effort</th><th>来源</th>' +
|
|
2174
|
+
'</tr></thead><tbody>';
|
|
2175
|
+
for (const eff of effective) {
|
|
2176
|
+
const f = eff.fields || {};
|
|
2177
|
+
const blockSrc = eff.blockSource || 'defaults';
|
|
2178
|
+
const srcCls = blockSrc === 'agent' ? 'gw-src-agent' : 'gw-src-defaults';
|
|
2179
|
+
const srcLabel = blockSrc === 'agent' ? '⚡ agent' : '🔗 默认';
|
|
2180
|
+
const baseUrlVal = f.baseUrl?.value || '';
|
|
2181
|
+
const modelVal = f.model?.value || '';
|
|
2182
|
+
const keyVal = f.apiKey?.value || '';
|
|
2183
|
+
const effortVal = f.effort?.value || '';
|
|
2184
|
+
|
|
2185
|
+
html += `<tr class="gw-eff-tr${blockSrc === 'defaults' ? ' gw-eff-tr-inherited' : ''}">` +
|
|
2186
|
+
`<td class="gw-eff-td-aid" title="${esc(eff.aid)}">${esc(shortAid(eff.aid))}</td>` +
|
|
2187
|
+
`<td>${GATEWAY_TYPE_ICON[eff.type] || ''} ${esc(eff.type)}</td>` +
|
|
2188
|
+
`<td class="gw-eff-td-url" title="${esc(baseUrlVal)}">${baseUrlVal ? esc(baseUrlVal) : '<span class="gw-dim">官方</span>'}</td>` +
|
|
2189
|
+
`<td>${modelVal ? esc(modelVal) : '<span class="gw-dim">—</span>'}</td>` +
|
|
2190
|
+
`<td>${keyVal ? esc(keyVal) : '<span class="gw-dim">—</span>'}</td>` +
|
|
2191
|
+
`<td>${effortVal ? esc(effortVal) : '<span class="gw-dim">—</span>'}</td>` +
|
|
2192
|
+
`<td><span class="gw-eff-src-tag ${srcCls}">${srcLabel}</span></td>` +
|
|
2193
|
+
`</tr>`;
|
|
2194
|
+
}
|
|
2195
|
+
html += '</tbody></table></div>';
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
html += '</div>';
|
|
2199
|
+
el.innerHTML = html;
|
|
2200
|
+
bindGatewayEvents(el, data);
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
function gatewayCard(g) {
|
|
2204
|
+
const key = gwKey(g.scope, g.type);
|
|
2205
|
+
const icon = GATEWAY_TYPE_ICON[g.type] || '⚙';
|
|
2206
|
+
const test = _gwTest.get(key);
|
|
2207
|
+
|
|
2208
|
+
// 连通性测试状态点
|
|
2209
|
+
let dot = '<span class="gw-dot gw-dot-unknown" title="未测试"></span>';
|
|
2210
|
+
if (test) {
|
|
2211
|
+
if (test.ok) dot = `<span class="gw-dot gw-dot-ok" title="${test.latency}ms · ${test.modelCount} 模型"></span>`;
|
|
2212
|
+
else dot = `<span class="gw-dot gw-dot-err" title="${esc(test.error || '失败')}"></span>`;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// API Key 展示
|
|
2216
|
+
let keyHtml;
|
|
2217
|
+
if (!g.apiKeyMask) keyHtml = '<span class="gw-dim">未配置</span>';
|
|
2218
|
+
else if (g.apiKeyIsEnvRef) keyHtml = `<code class="gw-env">${esc(g.apiKeyMask)}</code>`;
|
|
2219
|
+
else keyHtml = '<span class="gw-dim" title="明文密钥已隐藏,建议改用 $ENV 引用">*** (明文)</span>';
|
|
2220
|
+
|
|
2221
|
+
const rows = [];
|
|
2222
|
+
rows.push(['Base URL', g.baseUrl ? esc(g.baseUrl) : '<span class="gw-dim">官方端点</span>']);
|
|
2223
|
+
rows.push(['默认模型', g.model ? esc(g.model) : '<span class="gw-dim">—</span>']);
|
|
2224
|
+
rows.push(['API Key', keyHtml]);
|
|
2225
|
+
if (g.effort) rows.push(['Effort', esc(g.effort)]);
|
|
2226
|
+
if (g.reasoning) rows.push(['Reasoning', esc(g.reasoning)]);
|
|
2227
|
+
if (g.mode) rows.push(['模式', esc(g.mode)]);
|
|
2228
|
+
if (g.cliPath) rows.push(['CLI 路径', esc(g.cliPath)]);
|
|
2229
|
+
if (g.project) rows.push(['Project', esc(g.project)]);
|
|
2230
|
+
if (g.location) rows.push(['Location', esc(g.location)]);
|
|
2231
|
+
|
|
2232
|
+
let html = `<div class="gw-card" data-key="${esc(key)}">`;
|
|
2233
|
+
html += `<div class="gw-card-head">${dot}<span class="gw-card-icon">${icon}</span>` +
|
|
2234
|
+
`<span class="gw-card-title">${esc(g.name)}</span>` +
|
|
2235
|
+
`<span class="gw-card-type">${esc(g.type)}</span></div>`;
|
|
2236
|
+
html += '<div class="gw-card-body">';
|
|
2237
|
+
for (const [label, val] of rows) {
|
|
2238
|
+
html += `<div class="gw-row"><span class="gw-row-label">${esc(label)}</span><span class="gw-row-val">${val}</span></div>`;
|
|
2239
|
+
}
|
|
2240
|
+
html += '</div>';
|
|
2241
|
+
// 卡片操作按钮已移除(只读模式)
|
|
2242
|
+
html += '</div>';
|
|
2243
|
+
return html;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
// ── Gateway 编辑/操作弹窗函数(已禁用,网关配置现为只读展示)──
|
|
2247
|
+
// 如需恢复网关编辑功能,取消下方注释即可。
|
|
2248
|
+
|
|
2249
|
+
/*
|
|
2250
|
+
// 编辑/新增弹窗
|
|
2251
|
+
function openGatewayEditor(scope, type, existing, scopes) {
|
|
2252
|
+
const isNew = !existing;
|
|
2253
|
+
const fields = GATEWAY_FIELDS[type] || GATEWAY_FIELDS.claude;
|
|
2254
|
+
|
|
2255
|
+
let html = '<div class="gw-modal-backdrop" id="gw-modal-backdrop"><div class="gw-modal">';
|
|
2256
|
+
html += `<div class="gw-modal-head">${isNew ? '添加网关' : '编辑网关'}</div>`;
|
|
2257
|
+
html += '<div class="gw-modal-body">';
|
|
2258
|
+
|
|
2259
|
+
// scope 选择(新增时可选,编辑时锁定)
|
|
2260
|
+
html += '<label class="gw-field"><span class="gw-field-label">作用域</span>';
|
|
2261
|
+
if (isNew) {
|
|
2262
|
+
html += '<select id="gw-f-scope">';
|
|
2263
|
+
for (const s of (scopes || ['defaults'])) {
|
|
2264
|
+
const lbl = s === 'defaults' ? '全局默认' : shortAid(s);
|
|
2265
|
+
html += `<option value="${esc(s)}"${s === scope ? ' selected' : ''}>${esc(lbl)}</option>`;
|
|
2266
|
+
}
|
|
2267
|
+
html += '</select>';
|
|
2268
|
+
} else {
|
|
2269
|
+
html += `<input id="gw-f-scope" type="text" value="${esc(scope)}" disabled>`;
|
|
2270
|
+
}
|
|
2271
|
+
html += '</label>';
|
|
2272
|
+
|
|
2273
|
+
// type 选择(新增时可选,编辑时锁定)
|
|
2274
|
+
html += '<label class="gw-field"><span class="gw-field-label">后端类型</span>';
|
|
2275
|
+
if (isNew) {
|
|
2276
|
+
html += '<select id="gw-f-type">';
|
|
2277
|
+
for (const t of ['claude', 'codex', 'gemini']) {
|
|
2278
|
+
html += `<option value="${t}"${t === type ? ' selected' : ''}>${t}</option>`;
|
|
2279
|
+
}
|
|
2280
|
+
html += '</select>';
|
|
2281
|
+
} else {
|
|
2282
|
+
html += `<input id="gw-f-type" type="text" value="${esc(type)}" disabled>`;
|
|
2283
|
+
}
|
|
2284
|
+
html += '</label>';
|
|
2285
|
+
|
|
2286
|
+
// 动态字段
|
|
2287
|
+
html += '<div id="gw-dyn-fields">';
|
|
2288
|
+
for (const f of fields) {
|
|
2289
|
+
const val = existing ? (existing[f.key] || '') : '';
|
|
2290
|
+
html += `<label class="gw-field"><span class="gw-field-label">${esc(f.label)}</span>` +
|
|
2291
|
+
`<input class="gw-dyn" data-key="${esc(f.key)}" type="text" value="${esc(val)}" placeholder="${esc(f.placeholder || '')}"></label>`;
|
|
2292
|
+
}
|
|
2293
|
+
html += '</div>';
|
|
2294
|
+
|
|
2295
|
+
// API Key(仅 $ENV 引用)
|
|
2296
|
+
const curKey = existing && existing.apiKeyIsEnvRef ? existing.apiKeyMask : '';
|
|
2297
|
+
html += '<label class="gw-field"><span class="gw-field-label">API Key 引用</span>' +
|
|
2298
|
+
`<input id="gw-f-apikey" type="text" value="${esc(curKey)}" placeholder="$ENV:ANTHROPIC_AUTH_TOKEN(留空不改)"></label>`;
|
|
2299
|
+
html += '<div class="gw-hint">仅支持环境变量引用,格式 <code>$ENV:变量名</code>。明文密钥请写入环境变量后引用。</div>';
|
|
2300
|
+
|
|
2301
|
+
html += '</div>'; // body
|
|
2302
|
+
html += '<div class="gw-modal-actions">' +
|
|
2303
|
+
'<button class="ctrl-btn" id="gw-cancel">取消</button> ' +
|
|
2304
|
+
'<button class="ctrl-btn primary" id="gw-save">保存</button>' +
|
|
2305
|
+
'</div>';
|
|
2306
|
+
html += '</div></div>';
|
|
2307
|
+
|
|
2308
|
+
const wrap = document.createElement('div');
|
|
2309
|
+
wrap.innerHTML = html;
|
|
2310
|
+
document.body.appendChild(wrap.firstChild);
|
|
2311
|
+
|
|
2312
|
+
const backdrop = $('#gw-modal-backdrop');
|
|
2313
|
+
const close = () => { try { backdrop.remove(); } catch {} };
|
|
2314
|
+
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(); });
|
|
2315
|
+
$('#gw-cancel').onclick = close;
|
|
2316
|
+
|
|
2317
|
+
// 新增时切换 type 重建动态字段
|
|
2318
|
+
if (isNew) {
|
|
2319
|
+
$('#gw-f-type').onchange = (e) => {
|
|
2320
|
+
const newType = e.target.value;
|
|
2321
|
+
const dyn = $('#gw-dyn-fields');
|
|
2322
|
+
const fs2 = GATEWAY_FIELDS[newType] || GATEWAY_FIELDS.claude;
|
|
2323
|
+
dyn.innerHTML = fs2.map(f =>
|
|
2324
|
+
`<label class="gw-field"><span class="gw-field-label">${esc(f.label)}</span>` +
|
|
2325
|
+
`<input class="gw-dyn" data-key="${esc(f.key)}" type="text" value="" placeholder="${esc(f.placeholder || '')}"></label>`
|
|
2326
|
+
).join('');
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
$('#gw-save').onclick = async () => {
|
|
2331
|
+
const fScope = $('#gw-f-scope').value;
|
|
2332
|
+
const fType = $('#gw-f-type').value;
|
|
2333
|
+
const patch = {};
|
|
2334
|
+
document.querySelectorAll('#gw-dyn-fields .gw-dyn').forEach(inp => {
|
|
2335
|
+
patch[inp.dataset.key] = inp.value.trim();
|
|
2336
|
+
});
|
|
2337
|
+
const apiKey = $('#gw-f-apikey').value.trim();
|
|
2338
|
+
if (apiKey) {
|
|
2339
|
+
if (!apiKey.startsWith('$ENV:')) { toast('API Key 必须是 $ENV:变量名 引用', true); return; }
|
|
2340
|
+
patch.apiKey = apiKey;
|
|
2341
|
+
}
|
|
2342
|
+
try {
|
|
2343
|
+
const r = mResp(await menuSend({
|
|
2344
|
+
type: 'menu.update', name: 'gateway',
|
|
2345
|
+
value: JSON.stringify({ scope: fScope, type: fType, patch }),
|
|
2346
|
+
}));
|
|
2347
|
+
if (r.error) { toast(r.error.message || r.error.code, true); return; }
|
|
2348
|
+
toast(r.data && r.data.reloaded ? '已保存并重载' : '已保存(未重载)');
|
|
2349
|
+
close();
|
|
2350
|
+
subscribe('gateway', {}); // 刷新
|
|
2351
|
+
} catch (e) { toast(e.message, true); }
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
*/
|
|
2355
|
+
|
|
2356
|
+
// openAgentSelectModal、showGatewayConfigModal、showPriceEditModal 等函数已移除(只读模式)
|
|
2357
|
+
// 如需恢复,取消上方注释块即可。
|
|
2358
|
+
|
|
2359
|
+
function bindGatewayEvents(el, data) {
|
|
2360
|
+
// 已移除所有编辑操作事件绑定(网关配置现为只读展示)
|
|
2361
|
+
void el; void data;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
1291
2364
|
// ── Triggers 视图 ──
|
|
1292
2365
|
function trigStatusBadge(status) {
|
|
1293
2366
|
const map = {
|
|
@@ -1380,6 +2453,8 @@ function renderTriggers(data) {
|
|
|
1380
2453
|
|
|
1381
2454
|
function startApp() {
|
|
1382
2455
|
initTabs();
|
|
2456
|
+
// 恢复保存的 tab 视图
|
|
2457
|
+
switchView(currentView);
|
|
1383
2458
|
connect();
|
|
1384
2459
|
$('#logout-btn').onclick = () => {
|
|
1385
2460
|
localStorage.removeItem(TOKEN_KEY);
|
|
@@ -1405,149 +2480,485 @@ function initTheme() {
|
|
|
1405
2480
|
['_monCpu', '_monMem', '_monMsg', '_monErr'].forEach(function (k) {
|
|
1406
2481
|
if (window[k]) { window[k].dispose(); window[k] = null; }
|
|
1407
2482
|
});
|
|
1408
|
-
loadUsageDashboard();
|
|
1409
2483
|
if (currentView === 'monitor') renderMonitor(state.monitor);
|
|
1410
2484
|
};
|
|
1411
2485
|
}
|
|
1412
2486
|
}
|
|
1413
2487
|
|
|
1414
|
-
// ── Usage
|
|
1415
|
-
let _hourlyChart = null;
|
|
1416
|
-
let _modelChart = null;
|
|
1417
|
-
|
|
2488
|
+
// ── Usage 相关函数 ──
|
|
1418
2489
|
function fmtTokens(n) {
|
|
1419
2490
|
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
1420
2491
|
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
|
|
1421
2492
|
return String(n);
|
|
1422
2493
|
}
|
|
1423
2494
|
|
|
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
|
-
|
|
2495
|
+
// ── Usage Overview(总览,支持日期范围筛选)──
|
|
2496
|
+
let _ovCurrentRange = 'today'; // 当前选择的范围
|
|
2497
|
+
|
|
2498
|
+
async function loadUsageOverview(rangeType, customFrom, customTo) {
|
|
2499
|
+
rangeType = rangeType || _ovCurrentRange;
|
|
2500
|
+
_ovCurrentRange = rangeType;
|
|
2501
|
+
|
|
2502
|
+
// 计算日期范围
|
|
2503
|
+
let fromTs, toTs;
|
|
2504
|
+
const now = new Date();
|
|
2505
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
2506
|
+
const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime();
|
|
2507
|
+
|
|
2508
|
+
switch (rangeType) {
|
|
2509
|
+
case 'today':
|
|
2510
|
+
fromTs = todayStart;
|
|
2511
|
+
toTs = todayEnd;
|
|
2512
|
+
break;
|
|
2513
|
+
case 'week': // 本周(周一到今天)
|
|
2514
|
+
const dayOfWeek = now.getDay() || 7; // 周日=7
|
|
2515
|
+
const weekStart = new Date(todayStart - (dayOfWeek - 1) * 86400000);
|
|
2516
|
+
fromTs = weekStart.getTime();
|
|
2517
|
+
toTs = todayEnd;
|
|
2518
|
+
break;
|
|
2519
|
+
case 'lastWeek': // 上周(上周一到上周日)
|
|
2520
|
+
const lastWeekEnd = new Date(todayStart - now.getDay() * 86400000);
|
|
2521
|
+
const lastWeekStart = new Date(lastWeekEnd.getTime() - 6 * 86400000);
|
|
2522
|
+
fromTs = lastWeekStart.getTime();
|
|
2523
|
+
toTs = new Date(lastWeekEnd.getTime() + 86400000 - 1).getTime();
|
|
2524
|
+
break;
|
|
2525
|
+
case 'month': // 本月
|
|
2526
|
+
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
2527
|
+
fromTs = monthStart.getTime();
|
|
2528
|
+
toTs = todayEnd;
|
|
2529
|
+
break;
|
|
2530
|
+
case 'last30': // 最近30天
|
|
2531
|
+
fromTs = todayStart - 29 * 86400000;
|
|
2532
|
+
toTs = todayEnd;
|
|
2533
|
+
break;
|
|
2534
|
+
case 'custom': // 自定义
|
|
2535
|
+
if (customFrom && customTo) {
|
|
2536
|
+
// 支持 datetime-local 输入,直接解析时间戳
|
|
2537
|
+
fromTs = new Date(customFrom).getTime();
|
|
2538
|
+
toTs = new Date(customTo).getTime();
|
|
2539
|
+
} else {
|
|
2540
|
+
fromTs = todayStart;
|
|
2541
|
+
toTs = todayEnd;
|
|
2542
|
+
}
|
|
2543
|
+
break;
|
|
2544
|
+
default:
|
|
2545
|
+
fromTs = null;
|
|
2546
|
+
toTs = null;
|
|
1464
2547
|
}
|
|
1465
2548
|
|
|
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
|
-
}
|
|
2549
|
+
// 保存当前时间范围到全局变量,供详细统计使用
|
|
2550
|
+
window._currentOverviewTimeRange = { fromTs, toTs, rangeType, customFrom, customTo };
|
|
1480
2551
|
|
|
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>';
|
|
2552
|
+
// 时间范围变化后,刷新明细的模型列表与查询结果(重置到第一页)
|
|
2553
|
+
if ($('#detail-model')) {
|
|
2554
|
+
const detailPageEl = $('#detail-page');
|
|
2555
|
+
if (detailPageEl) detailPageEl.value = '1';
|
|
2556
|
+
loadDetailModelList();
|
|
2557
|
+
queryDetailUsage();
|
|
1489
2558
|
}
|
|
1490
2559
|
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
// ── Usage Overview(全时段总览)──
|
|
1494
|
-
async function loadUsageOverview() {
|
|
1495
2560
|
let data;
|
|
1496
2561
|
try {
|
|
1497
|
-
const
|
|
2562
|
+
const params = new URLSearchParams();
|
|
2563
|
+
if (fromTs) params.set('from', String(fromTs));
|
|
2564
|
+
if (toTs) params.set('to', String(toTs));
|
|
2565
|
+
|
|
2566
|
+
const resp = await fetch(apiUrl('api/stats/overview?' + params.toString()), {
|
|
1498
2567
|
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
1499
2568
|
});
|
|
1500
2569
|
data = resp.ok ? await resp.json() : null;
|
|
1501
2570
|
} catch { data = null; }
|
|
1502
2571
|
|
|
1503
2572
|
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 };
|
|
2573
|
+
: { 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
2574
|
const sessionCount = (data && data.session_count) || 0;
|
|
1506
2575
|
const msgIn = (data && data.msg_in) || 0;
|
|
1507
2576
|
const msgOut = (data && data.msg_out) || 0;
|
|
1508
|
-
|
|
1509
|
-
const
|
|
2577
|
+
// 新的缓存命中率计算规则:缓存命中 / (缓存命中 + 缓存写入 + 输入token + 输出token)
|
|
2578
|
+
const totalTokens = ts.cache_read_tokens + ts.cache_creation_tokens + ts.input_tokens + ts.output_tokens;
|
|
2579
|
+
const hitRate = totalTokens > 0 ? (ts.cache_read_tokens / totalTokens) * 100 : 0;
|
|
1510
2580
|
|
|
1511
2581
|
const cardsEl = $('#ov-cards');
|
|
1512
2582
|
if (cardsEl) {
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
2583
|
+
// 合并相关信息到大卡片中
|
|
2584
|
+
const sessionCard = makeMultiValueCard([
|
|
2585
|
+
{ label: t('usage.card.sessionCount'), value: sessionCount },
|
|
2586
|
+
{ label: t('usage.card.msgIn'), value: msgIn },
|
|
2587
|
+
{ label: t('usage.card.msgOut'), value: msgOut }
|
|
2588
|
+
], t('usage.card.sessionInfo'), 'session-group');
|
|
2589
|
+
|
|
2590
|
+
const usageCard = makeMultiValueCard([
|
|
2591
|
+
{ label: t('usage.card.modelCalls'), value: ts.call_count },
|
|
2592
|
+
{ label: t('usage.card.inputTokens'), value: fmtTokens(ts.input_tokens) },
|
|
2593
|
+
{ label: t('usage.card.outputTokens'), value: fmtTokens(ts.output_tokens) },
|
|
2594
|
+
{ label: t('usage.card.cacheCreation'), value: fmtTokens(ts.cache_creation_tokens) },
|
|
2595
|
+
{ label: t('usage.card.cacheHitTokens'), value: fmtTokens(ts.cache_read_tokens) },
|
|
2596
|
+
{ label: t('usage.card.cacheHitRate'), value: hitRate.toFixed(1) + '%' }
|
|
2597
|
+
], t('usage.card.usageInfo'), 'usage-group');
|
|
2598
|
+
|
|
2599
|
+
const costCard = makeMultiValueCard([
|
|
2600
|
+
{ label: t('usage.card.costOfficial'), value: fmtCost(ts.cost_official_usd, ts.cost_official_cny) },
|
|
2601
|
+
{ label: t('usage.card.costGateway'), value: fmtCost(ts.cost_usd, ts.cost_cny) }
|
|
2602
|
+
], t('usage.card.costInfo'), 'cost-group');
|
|
2603
|
+
|
|
2604
|
+
cardsEl.innerHTML = sessionCard + usageCard + costCard;
|
|
1525
2605
|
}
|
|
1526
2606
|
|
|
1527
2607
|
const agentTbl = $('#ov-agent-table');
|
|
1528
2608
|
const agents = (data && data.token_stats && data.token_stats.by_agent) || [];
|
|
1529
2609
|
if (agentTbl) {
|
|
1530
2610
|
if (!agents.length) {
|
|
1531
|
-
agentTbl.innerHTML = '<tbody><tr><td
|
|
2611
|
+
agentTbl.innerHTML = '<tbody><tr><td>' + t('usage.overview.noData') + '</td></tr></tbody>';
|
|
1532
2612
|
} else {
|
|
1533
2613
|
agentTbl.innerHTML =
|
|
1534
|
-
'<thead><tr><th>
|
|
2614
|
+
'<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
2615
|
'<tbody>' + agents.map(function(a) {
|
|
1536
|
-
var name = a.agent_aid ? a.agent_aid.split('.')[0] : '(unknown)';
|
|
2616
|
+
var name = a.agent_name || (a.agent_aid ? a.agent_aid.split('.')[0] : '(unknown)');
|
|
2617
|
+
// 计算缓存命中率
|
|
2618
|
+
var totalTokens = (a.cache_read_tokens || 0) + (a.cache_creation_tokens || 0) + (a.input_tokens || 0) + (a.output_tokens || 0);
|
|
2619
|
+
var hitRate = totalTokens > 0 ? ((a.cache_read_tokens || 0) / totalTokens * 100).toFixed(1) : '0.0';
|
|
2620
|
+
|
|
1537
2621
|
return '<tr><td title="' + esc(a.agent_aid) + '">' + esc(name) + '</td>' +
|
|
1538
2622
|
'<td>' + a.call_count + '</td>' +
|
|
1539
2623
|
'<td>' + fmtTokens(a.input_tokens) + '</td>' +
|
|
1540
2624
|
'<td>' + fmtTokens(a.output_tokens) + '</td>' +
|
|
1541
2625
|
'<td>' + fmtTokens(a.cache_creation_tokens) + '</td>' +
|
|
1542
2626
|
'<td>' + fmtTokens(a.cache_read_tokens) + '</td>' +
|
|
1543
|
-
'<td>' +
|
|
2627
|
+
'<td>' + hitRate + '%</td>' +
|
|
2628
|
+
'<td>' + fmtCostSplit(a.cost_official_usd, a.cost_official_cny) + '</td>' +
|
|
2629
|
+
'<td>' + fmtCostSplit(a.cost_usd, a.cost_cny) + '</td></tr>';
|
|
1544
2630
|
}).join('') + '</tbody>';
|
|
1545
2631
|
}
|
|
1546
2632
|
}
|
|
2633
|
+
|
|
2634
|
+
// 保存总览数据供详细统计使用
|
|
2635
|
+
window._currentOverviewData = { ts, sessionCount, msgIn, msgOut, hitRate };
|
|
1547
2636
|
}
|
|
1548
2637
|
|
|
1549
|
-
function
|
|
1550
|
-
|
|
2638
|
+
function initOverviewFilters() {
|
|
2639
|
+
// 范围按钮切换
|
|
2640
|
+
document.querySelectorAll('.ov-range-btn').forEach(function(btn) {
|
|
2641
|
+
btn.addEventListener('click', function() {
|
|
2642
|
+
document.querySelectorAll('.ov-range-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
2643
|
+
btn.classList.add('active');
|
|
2644
|
+
|
|
2645
|
+
const range = btn.getAttribute('data-range');
|
|
2646
|
+
const customDateEl = $('#ov-custom-date');
|
|
2647
|
+
|
|
2648
|
+
if (range === 'custom') {
|
|
2649
|
+
if (customDateEl) customDateEl.style.display = 'flex';
|
|
2650
|
+
} else {
|
|
2651
|
+
if (customDateEl) customDateEl.style.display = 'none';
|
|
2652
|
+
loadUsageOverview(range);
|
|
2653
|
+
}
|
|
2654
|
+
});
|
|
2655
|
+
});
|
|
2656
|
+
|
|
2657
|
+
// 自定义日期查询按钮
|
|
2658
|
+
const queryBtn = $('#ov-query-btn');
|
|
2659
|
+
if (queryBtn) {
|
|
2660
|
+
queryBtn.addEventListener('click', function() {
|
|
2661
|
+
const fromEl = $('#ov-from');
|
|
2662
|
+
const toEl = $('#ov-to');
|
|
2663
|
+
if (fromEl && toEl && fromEl.value && toEl.value) {
|
|
2664
|
+
loadUsageOverview('custom', fromEl.value, toEl.value);
|
|
2665
|
+
}
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
// 设置默认日期为最近7天
|
|
2670
|
+
const now = new Date();
|
|
2671
|
+
const from = new Date(now.getTime() - 6 * 86400000);
|
|
2672
|
+
const fromEl = $('#ov-from');
|
|
2673
|
+
const toEl = $('#ov-to');
|
|
2674
|
+
if (fromEl) fromEl.value = formatDatetimeLocal(from);
|
|
2675
|
+
if (toEl) toEl.value = formatDatetimeLocal(now);
|
|
2676
|
+
|
|
2677
|
+
// 初始化明细查询
|
|
2678
|
+
initDetailQuery();
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
// 格式化为 datetime-local 输入框的格式 (YYYY-MM-DDTHH:mm)
|
|
2682
|
+
function formatDatetimeLocal(date) {
|
|
2683
|
+
const year = date.getFullYear();
|
|
2684
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
2685
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
2686
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
2687
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
2688
|
+
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
// 模型访问明细查询
|
|
2692
|
+
function initDetailQuery() {
|
|
2693
|
+
// 填充Agent选择器
|
|
2694
|
+
loadDetailAgentList();
|
|
2695
|
+
// 填充Model选择器(按上面总览的时间范围)
|
|
2696
|
+
loadDetailModelList();
|
|
2697
|
+
|
|
2698
|
+
// 绑定分页大小变化
|
|
2699
|
+
const pageSizeEl = $('#detail-page-size');
|
|
2700
|
+
if (pageSizeEl) {
|
|
2701
|
+
pageSizeEl.addEventListener('change', function() {
|
|
2702
|
+
// 重置到第一页并查询
|
|
2703
|
+
const pageEl = $('#detail-page');
|
|
2704
|
+
if (pageEl) pageEl.value = '1';
|
|
2705
|
+
queryDetailUsage();
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
// 绑定上一页/下一页按钮
|
|
2710
|
+
const prevBtn = $('#detail-prev-page');
|
|
2711
|
+
const nextBtn = $('#detail-next-page');
|
|
2712
|
+
if (prevBtn) {
|
|
2713
|
+
prevBtn.addEventListener('click', function() {
|
|
2714
|
+
const pageEl = $('#detail-page');
|
|
2715
|
+
if (pageEl && Number(pageEl.value) > 1) {
|
|
2716
|
+
pageEl.value = String(Number(pageEl.value) - 1);
|
|
2717
|
+
queryDetailUsage();
|
|
2718
|
+
}
|
|
2719
|
+
});
|
|
2720
|
+
}
|
|
2721
|
+
if (nextBtn) {
|
|
2722
|
+
nextBtn.addEventListener('click', function() {
|
|
2723
|
+
const pageEl = $('#detail-page');
|
|
2724
|
+
if (pageEl) {
|
|
2725
|
+
pageEl.value = String(Number(pageEl.value) + 1);
|
|
2726
|
+
queryDetailUsage();
|
|
2727
|
+
}
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
// 绑定页码输入框回车事件
|
|
2732
|
+
const pageEl = $('#detail-page');
|
|
2733
|
+
if (pageEl) {
|
|
2734
|
+
pageEl.addEventListener('keypress', function(e) {
|
|
2735
|
+
if (e.key === 'Enter') {
|
|
2736
|
+
queryDetailUsage();
|
|
2737
|
+
}
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
// 绑定Agent选择器变化事件
|
|
2742
|
+
const agentEl = $('#detail-agent');
|
|
2743
|
+
if (agentEl) {
|
|
2744
|
+
agentEl.addEventListener('change', function() {
|
|
2745
|
+
const pageEl = $('#detail-page');
|
|
2746
|
+
if (pageEl) pageEl.value = '1';
|
|
2747
|
+
queryDetailUsage();
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
// 绑定Model选择器变化事件
|
|
2752
|
+
const modelEl = $('#detail-model');
|
|
2753
|
+
if (modelEl) {
|
|
2754
|
+
modelEl.addEventListener('change', function() {
|
|
2755
|
+
const pageEl = $('#detail-page');
|
|
2756
|
+
if (pageEl) pageEl.value = '1';
|
|
2757
|
+
queryDetailUsage();
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
async function loadDetailAgentList() {
|
|
2763
|
+
try {
|
|
2764
|
+
const resp = await fetch(apiUrl('api/stats/agents'), {
|
|
2765
|
+
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
2766
|
+
});
|
|
2767
|
+
if (!resp.ok) return;
|
|
2768
|
+
const agents = await resp.json();
|
|
2769
|
+
|
|
2770
|
+
const selectEl = $('#detail-agent');
|
|
2771
|
+
if (selectEl && agents.length) {
|
|
2772
|
+
// 清空除第一个"全部"选项之外的所有选项
|
|
2773
|
+
while (selectEl.options.length > 1) {
|
|
2774
|
+
selectEl.remove(1);
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
agents.forEach(function(a) {
|
|
2778
|
+
const option = document.createElement('option');
|
|
2779
|
+
option.value = a.agent_aid;
|
|
2780
|
+
// 优先显示agent_name,没有则显示aid前缀
|
|
2781
|
+
option.textContent = a.agent_name || a.agent_aid.split('.')[0];
|
|
2782
|
+
selectEl.appendChild(option);
|
|
2783
|
+
});
|
|
2784
|
+
// 默认选中第一个agent
|
|
2785
|
+
if (agents.length > 0) {
|
|
2786
|
+
selectEl.value = agents[0].agent_aid;
|
|
2787
|
+
}
|
|
2788
|
+
// 加载完成后自动查询一次
|
|
2789
|
+
queryDetailUsage();
|
|
2790
|
+
}
|
|
2791
|
+
} catch {}
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
// 加载模型列表(按上面总览的时间范围)
|
|
2795
|
+
async function loadDetailModelList() {
|
|
2796
|
+
const selectEl = $('#detail-model');
|
|
2797
|
+
if (!selectEl) return;
|
|
2798
|
+
// 记住当前选中值,刷新后尽量保持
|
|
2799
|
+
const prev = selectEl.value;
|
|
2800
|
+
try {
|
|
2801
|
+
const timeRange = window._currentOverviewTimeRange || {};
|
|
2802
|
+
const params = new URLSearchParams();
|
|
2803
|
+
if (timeRange.fromTs) params.set('from', String(timeRange.fromTs));
|
|
2804
|
+
if (timeRange.toTs) params.set('to', String(timeRange.toTs));
|
|
2805
|
+
|
|
2806
|
+
const resp = await fetch(apiUrl('api/stats/models?' + params.toString()), {
|
|
2807
|
+
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
2808
|
+
});
|
|
2809
|
+
if (!resp.ok) return;
|
|
2810
|
+
const models = await resp.json();
|
|
2811
|
+
|
|
2812
|
+
// 清空除第一个"全部"选项之外的所有选项
|
|
2813
|
+
while (selectEl.options.length > 1) {
|
|
2814
|
+
selectEl.remove(1);
|
|
2815
|
+
}
|
|
2816
|
+
(models || []).forEach(function(m) {
|
|
2817
|
+
const option = document.createElement('option');
|
|
2818
|
+
option.value = m;
|
|
2819
|
+
option.textContent = m;
|
|
2820
|
+
selectEl.appendChild(option);
|
|
2821
|
+
});
|
|
2822
|
+
// 恢复之前的选择(若仍存在)
|
|
2823
|
+
if (prev && Array.prototype.some.call(selectEl.options, function(o) { return o.value === prev; })) {
|
|
2824
|
+
selectEl.value = prev;
|
|
2825
|
+
} else {
|
|
2826
|
+
selectEl.value = '';
|
|
2827
|
+
}
|
|
2828
|
+
} catch {}
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
async function queryDetailUsage() {
|
|
2832
|
+
// 使用总览的时间范围
|
|
2833
|
+
const timeRange = window._currentOverviewTimeRange || {};
|
|
2834
|
+
const fromTs = timeRange.fromTs;
|
|
2835
|
+
const toTs = timeRange.toTs;
|
|
2836
|
+
|
|
2837
|
+
const agentEl = $('#detail-agent');
|
|
2838
|
+
const modelEl = $('#detail-model');
|
|
2839
|
+
const pageEl = $('#detail-page');
|
|
2840
|
+
const pageSizeEl = $('#detail-page-size');
|
|
2841
|
+
|
|
2842
|
+
const page = pageEl ? Number(pageEl.value) || 1 : 1;
|
|
2843
|
+
const pageSize = pageSizeEl ? Number(pageSizeEl.value) || 50 : 50;
|
|
2844
|
+
const offset = (page - 1) * pageSize;
|
|
2845
|
+
|
|
2846
|
+
const params = new URLSearchParams();
|
|
2847
|
+
if (fromTs) params.set('from', String(fromTs));
|
|
2848
|
+
if (toTs) params.set('to', String(toTs));
|
|
2849
|
+
if (agentEl && agentEl.value) params.set('agent', agentEl.value);
|
|
2850
|
+
if (modelEl && modelEl.value) params.set('model', modelEl.value);
|
|
2851
|
+
params.set('limit', String(pageSize));
|
|
2852
|
+
params.set('offset', String(offset));
|
|
2853
|
+
|
|
2854
|
+
try {
|
|
2855
|
+
const resp = await fetch(apiUrl('api/stats/detail?' + params.toString()), {
|
|
2856
|
+
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
2857
|
+
});
|
|
2858
|
+
if (!resp.ok) {
|
|
2859
|
+
showDetailError(t('usage.detail.error'));
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
const result = await resp.json();
|
|
2863
|
+
renderDetailTable(result.data, result.total, page, pageSize);
|
|
2864
|
+
} catch {
|
|
2865
|
+
showDetailError(t('usage.detail.error'));
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
function renderDetailTable(data, total, currentPage, pageSize) {
|
|
2870
|
+
const tableEl = $('#detail-table');
|
|
2871
|
+
if (!tableEl) return;
|
|
2872
|
+
|
|
2873
|
+
if (!data || !data.length) {
|
|
2874
|
+
tableEl.innerHTML = '<tbody><tr><td colspan="10" style="text-align:center;color:var(--dim)">' + t('usage.explorer.noData') + '</td></tr></tbody>';
|
|
2875
|
+
updatePaginationInfo(0, currentPage, pageSize);
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
const html = '<thead><tr>' +
|
|
2880
|
+
'<th>' + t('usage.detail.th.time') + '</th>' +
|
|
2881
|
+
'<th>' + t('usage.detail.th.agent') + '</th>' +
|
|
2882
|
+
'<th>' + t('usage.detail.th.peer') + '</th>' +
|
|
2883
|
+
'<th>' + t('usage.detail.th.model') + '</th>' +
|
|
2884
|
+
'<th>' + t('usage.detail.th.input') + '</th>' +
|
|
2885
|
+
'<th>' + t('usage.detail.th.output') + '</th>' +
|
|
2886
|
+
'<th>' + t('usage.detail.th.cacheCreation') + '</th>' +
|
|
2887
|
+
'<th>' + t('usage.detail.th.cacheRead') + '</th>' +
|
|
2888
|
+
'<th>' + t('usage.detail.th.costOfficial') + '</th>' +
|
|
2889
|
+
'<th>' + t('usage.detail.th.costGateway') + '</th>' +
|
|
2890
|
+
'</tr></thead><tbody>' +
|
|
2891
|
+
data.map(function(row) {
|
|
2892
|
+
const time = new Date(row.ts).toLocaleString();
|
|
2893
|
+
const agentName = row.agent_name || (row.agent_aid || '').split('.')[0];
|
|
2894
|
+
const peerName = (row.peer_key || '').replace(/^aun#/, '').split('.')[0];
|
|
2895
|
+
return '<tr>' +
|
|
2896
|
+
'<td style="white-space:nowrap">' + time + '</td>' +
|
|
2897
|
+
'<td title="' + esc(row.agent_aid) + '">' + esc(agentName) + '</td>' +
|
|
2898
|
+
'<td title="' + esc(row.peer_key) + '">' + esc(peerName) + '</td>' +
|
|
2899
|
+
'<td>' + esc(row.model || '') + '</td>' +
|
|
2900
|
+
'<td>' + fmtTokens(row.input_tokens || 0) + '</td>' +
|
|
2901
|
+
'<td>' + fmtTokens(row.output_tokens || 0) + '</td>' +
|
|
2902
|
+
'<td>' + fmtTokens(row.cache_creation_tokens || 0) + '</td>' +
|
|
2903
|
+
'<td>' + fmtTokens(row.cache_read_tokens || 0) + '</td>' +
|
|
2904
|
+
'<td>' + fmtCostCompact(row.cost_official_usd, row.cost_official_cny) + '</td>' +
|
|
2905
|
+
'<td>' + fmtCostCompact(row.cost_gateway_usd, row.cost_gateway_cny) + '</td>' +
|
|
2906
|
+
'</tr>';
|
|
2907
|
+
}).join('') +
|
|
2908
|
+
'</tbody>';
|
|
2909
|
+
|
|
2910
|
+
tableEl.innerHTML = html;
|
|
2911
|
+
updatePaginationInfo(total, currentPage, pageSize);
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
function updatePaginationInfo(total, currentPage, pageSize) {
|
|
2915
|
+
const infoEl = $('#detail-pagination-info');
|
|
2916
|
+
const prevBtn = $('#detail-prev-page');
|
|
2917
|
+
const nextBtn = $('#detail-next-page');
|
|
2918
|
+
|
|
2919
|
+
if (infoEl) {
|
|
2920
|
+
const start = total > 0 ? (currentPage - 1) * pageSize + 1 : 0;
|
|
2921
|
+
const end = Math.min(currentPage * pageSize, total);
|
|
2922
|
+
const totalPages = Math.ceil(total / pageSize) || 1;
|
|
2923
|
+
infoEl.textContent = t('usage.detail.pagination')
|
|
2924
|
+
.replace('{start}', start)
|
|
2925
|
+
.replace('{end}', end)
|
|
2926
|
+
.replace('{total}', total)
|
|
2927
|
+
.replace('{page}', currentPage)
|
|
2928
|
+
.replace('{totalPages}', totalPages);
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
if (prevBtn) prevBtn.disabled = currentPage <= 1;
|
|
2932
|
+
if (nextBtn) nextBtn.disabled = currentPage >= Math.ceil(total / pageSize);
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
function showDetailError(msg) {
|
|
2936
|
+
const tableEl = $('#detail-table');
|
|
2937
|
+
if (tableEl) {
|
|
2938
|
+
tableEl.innerHTML = '<tbody><tr><td colspan="10" style="text-align:center;color:var(--red)">' + esc(msg) + '</td></tr></tbody>';
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
function fmtCostCompact(usd, cny) {
|
|
2943
|
+
var parts = [];
|
|
2944
|
+
if (usd > 0) parts.push('$' + (usd < 0.01 ? usd.toFixed(4) : usd.toFixed(2)));
|
|
2945
|
+
if (cny > 0) parts.push('¥' + (cny < 0.01 ? cny.toFixed(4) : cny.toFixed(2)));
|
|
2946
|
+
if (parts.length === 0) return '-';
|
|
2947
|
+
return parts.join(' / ');
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
function ovCard(value, label, groupClass) {
|
|
2951
|
+
var cls = 'usage-card' + (groupClass ? ' ' + groupClass : '');
|
|
2952
|
+
return '<div class="' + cls + '"><div class="card-value">' + value + '</div><div class="card-label">' + label + '</div></div>';
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
// 创建多值卡片(合并多个指标到一个卡片中)
|
|
2956
|
+
function makeMultiValueCard(items, title, groupClass) {
|
|
2957
|
+
var cls = 'usage-card multi-value-card' + (groupClass ? ' ' + groupClass : '');
|
|
2958
|
+
var itemsHtml = items.map(function(item) {
|
|
2959
|
+
return '<div class="card-item"><div class="card-item-label">' + item.label + '</div><div class="card-item-value">' + item.value + '</div></div>';
|
|
2960
|
+
}).join('');
|
|
2961
|
+
return '<div class="' + cls + '"><div class="card-title">' + title + '</div><div class="card-items">' + itemsHtml + '</div></div>';
|
|
1551
2962
|
}
|
|
1552
2963
|
|
|
1553
2964
|
function fmtCost(usd, cny) {
|
|
@@ -1557,6 +2968,25 @@ function fmtCost(usd, cny) {
|
|
|
1557
2968
|
return parts.length ? parts.join(' / ') : '$0';
|
|
1558
2969
|
}
|
|
1559
2970
|
|
|
2971
|
+
// 分行显示美元和人民币
|
|
2972
|
+
function fmtCostSplit(usd, cny) {
|
|
2973
|
+
var parts = [];
|
|
2974
|
+
if (usd > 0) parts.push('$' + (usd < 0.01 ? usd.toFixed(4) : usd.toFixed(2)));
|
|
2975
|
+
if (cny > 0) parts.push('¥' + (cny < 0.01 ? cny.toFixed(4) : cny.toFixed(2)));
|
|
2976
|
+
if (parts.length === 0) return '<span style="color:var(--dim)">$0</span>';
|
|
2977
|
+
if (parts.length === 1) return parts[0];
|
|
2978
|
+
return parts[0] + '<br><span style="font-size:10px;color:var(--dim)">' + parts[1] + '</span>';
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
// 带标签的价格显示(用于卡片)
|
|
2982
|
+
function fmtCostWithLabel(usd, cny, label) {
|
|
2983
|
+
var parts = [];
|
|
2984
|
+
if (usd > 0) parts.push('$' + (usd < 0.01 ? usd.toFixed(4) : usd.toFixed(2)));
|
|
2985
|
+
if (cny > 0) parts.push('¥' + (cny < 0.01 ? cny.toFixed(4) : cny.toFixed(2)));
|
|
2986
|
+
var value = parts.length ? parts.join(' / ') : '$0';
|
|
2987
|
+
return '<div class="card-label" style="margin-bottom:4px;margin-top:0">' + label + '</div><div class="card-value" style="font-size:18px">' + value + '</div>';
|
|
2988
|
+
}
|
|
2989
|
+
|
|
1560
2990
|
// ── Usage subtab switching ──
|
|
1561
2991
|
function initUsageSubtabs() {
|
|
1562
2992
|
var btns = document.querySelectorAll('.usage-subtab');
|
|
@@ -1571,89 +3001,324 @@ function initUsageSubtabs() {
|
|
|
1571
3001
|
});
|
|
1572
3002
|
var panel = $('#usage-' + target);
|
|
1573
3003
|
if (panel) { panel.classList.add('active'); panel.style.display = ''; }
|
|
1574
|
-
if (target === 'overview')
|
|
1575
|
-
|
|
1576
|
-
|
|
3004
|
+
if (target === 'overview') {
|
|
3005
|
+
initOverviewFilters();
|
|
3006
|
+
loadUsageOverview();
|
|
3007
|
+
} else if (target === 'explorer') {
|
|
3008
|
+
initExplorer();
|
|
3009
|
+
// 自动加载模型列表和执行查询
|
|
3010
|
+
loadExplorerModels();
|
|
3011
|
+
setTimeout(() => runExplorerQuery(), 100);
|
|
3012
|
+
}
|
|
1577
3013
|
});
|
|
1578
3014
|
});
|
|
3015
|
+
|
|
3016
|
+
// 初始化总览页面的过滤器并加载默认数据(今日)
|
|
3017
|
+
initOverviewFilters();
|
|
3018
|
+
loadUsageOverview('today');
|
|
1579
3019
|
}
|
|
1580
3020
|
|
|
1581
3021
|
// ── Explorer ──
|
|
1582
3022
|
var _explorerChart = null;
|
|
1583
3023
|
var _explorerInited = false;
|
|
1584
3024
|
var _expSelection = { type: null, key: null }; // { type: 'agent'|'peer', key: string } or null
|
|
3025
|
+
var _expCurrentRange = 'today'; // Explorer 当前选择的时间范围
|
|
3026
|
+
var _expTimeRange = { fromTs: null, toTs: null }; // Explorer 的时间范围
|
|
1585
3027
|
|
|
1586
3028
|
function initExplorer() {
|
|
1587
3029
|
if (_explorerInited) return;
|
|
1588
3030
|
_explorerInited = true;
|
|
3031
|
+
|
|
3032
|
+
// 初始化时间范围选择
|
|
3033
|
+
initExplorerTimeFilters();
|
|
3034
|
+
|
|
3035
|
+
// 绑定查询按钮
|
|
1589
3036
|
var btn = $('#exp-query-btn');
|
|
1590
3037
|
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);
|
|
3038
|
+
|
|
1598
3039
|
// Load sidebar lists
|
|
1599
3040
|
loadExplorerSidebar();
|
|
1600
3041
|
}
|
|
1601
3042
|
|
|
3043
|
+
// 初始化 Explorer 的时间范围选择
|
|
3044
|
+
function initExplorerTimeFilters() {
|
|
3045
|
+
// 范围按钮切换
|
|
3046
|
+
document.querySelectorAll('.exp-range-btn').forEach(function(btn) {
|
|
3047
|
+
btn.addEventListener('click', function() {
|
|
3048
|
+
document.querySelectorAll('.exp-range-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
3049
|
+
btn.classList.add('active');
|
|
3050
|
+
|
|
3051
|
+
const range = btn.getAttribute('data-range');
|
|
3052
|
+
const customDateEl = $('#exp-custom-date');
|
|
3053
|
+
|
|
3054
|
+
if (range === 'custom') {
|
|
3055
|
+
if (customDateEl) customDateEl.style.display = 'flex';
|
|
3056
|
+
} else {
|
|
3057
|
+
if (customDateEl) customDateEl.style.display = 'none';
|
|
3058
|
+
_expCurrentRange = range;
|
|
3059
|
+
calculateExplorerTimeRange(range);
|
|
3060
|
+
loadExplorerModels(); // 加载可用模型
|
|
3061
|
+
runExplorerQuery();
|
|
3062
|
+
}
|
|
3063
|
+
});
|
|
3064
|
+
});
|
|
3065
|
+
|
|
3066
|
+
// 自定义时间查询按钮
|
|
3067
|
+
const timeQueryBtn = $('#exp-time-query-btn');
|
|
3068
|
+
if (timeQueryBtn) {
|
|
3069
|
+
timeQueryBtn.addEventListener('click', function() {
|
|
3070
|
+
const fromEl = $('#exp-from');
|
|
3071
|
+
const toEl = $('#exp-to');
|
|
3072
|
+
if (fromEl && toEl && fromEl.value && toEl.value) {
|
|
3073
|
+
_expCurrentRange = 'custom';
|
|
3074
|
+
_expTimeRange.fromTs = new Date(fromEl.value).getTime();
|
|
3075
|
+
_expTimeRange.toTs = new Date(toEl.value).getTime();
|
|
3076
|
+
loadExplorerModels(); // 加载可用模型
|
|
3077
|
+
runExplorerQuery();
|
|
3078
|
+
}
|
|
3079
|
+
});
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
// 设置默认时间范围(今日)并初始化日期选择器
|
|
3083
|
+
const now = new Date();
|
|
3084
|
+
const fromEl = $('#exp-from');
|
|
3085
|
+
const toEl = $('#exp-to');
|
|
3086
|
+
if (fromEl) fromEl.value = formatDatetimeLocal(new Date(now.getFullYear(), now.getMonth(), now.getDate()));
|
|
3087
|
+
if (toEl) toEl.value = formatDatetimeLocal(now);
|
|
3088
|
+
|
|
3089
|
+
// 计算默认时间范围(今日)
|
|
3090
|
+
calculateExplorerTimeRange('today');
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
// 计算 Explorer 的时间范围
|
|
3094
|
+
function calculateExplorerTimeRange(rangeType) {
|
|
3095
|
+
const now = new Date();
|
|
3096
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
3097
|
+
const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime();
|
|
3098
|
+
|
|
3099
|
+
switch (rangeType) {
|
|
3100
|
+
case 'today':
|
|
3101
|
+
_expTimeRange.fromTs = todayStart;
|
|
3102
|
+
_expTimeRange.toTs = todayEnd;
|
|
3103
|
+
break;
|
|
3104
|
+
case 'week':
|
|
3105
|
+
const dayOfWeek = now.getDay() || 7;
|
|
3106
|
+
const weekStart = new Date(todayStart - (dayOfWeek - 1) * 86400000);
|
|
3107
|
+
_expTimeRange.fromTs = weekStart.getTime();
|
|
3108
|
+
_expTimeRange.toTs = todayEnd;
|
|
3109
|
+
break;
|
|
3110
|
+
case 'lastWeek':
|
|
3111
|
+
const lastWeekEnd = new Date(todayStart - now.getDay() * 86400000);
|
|
3112
|
+
const lastWeekStart = new Date(lastWeekEnd.getTime() - 6 * 86400000);
|
|
3113
|
+
_expTimeRange.fromTs = lastWeekStart.getTime();
|
|
3114
|
+
_expTimeRange.toTs = new Date(lastWeekEnd.getTime() + 86400000 - 1).getTime();
|
|
3115
|
+
break;
|
|
3116
|
+
case 'month':
|
|
3117
|
+
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
3118
|
+
_expTimeRange.fromTs = monthStart.getTime();
|
|
3119
|
+
_expTimeRange.toTs = todayEnd;
|
|
3120
|
+
break;
|
|
3121
|
+
case 'last30':
|
|
3122
|
+
_expTimeRange.fromTs = todayStart - 29 * 86400000;
|
|
3123
|
+
_expTimeRange.toTs = todayEnd;
|
|
3124
|
+
break;
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
// 加载 Explorer 可用的模型列表(根据当前时间范围)
|
|
3129
|
+
async function loadExplorerModels() {
|
|
3130
|
+
const params = new URLSearchParams();
|
|
3131
|
+
if (_expTimeRange.fromTs) params.set('from', String(_expTimeRange.fromTs));
|
|
3132
|
+
if (_expTimeRange.toTs) params.set('to', String(_expTimeRange.toTs));
|
|
3133
|
+
|
|
3134
|
+
try {
|
|
3135
|
+
const resp = await fetch(apiUrl('api/stats/models?' + params.toString()), {
|
|
3136
|
+
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
3137
|
+
});
|
|
3138
|
+
if (!resp.ok) return;
|
|
3139
|
+
const models = await resp.json();
|
|
3140
|
+
|
|
3141
|
+
const selectEl = $('#exp-model');
|
|
3142
|
+
if (selectEl) {
|
|
3143
|
+
const currentValue = selectEl.value;
|
|
3144
|
+
selectEl.innerHTML = '<option value="">' + t('usage.explorer.all') + '</option>';
|
|
3145
|
+
models.forEach(function(model) {
|
|
3146
|
+
const option = document.createElement('option');
|
|
3147
|
+
option.value = model;
|
|
3148
|
+
option.textContent = model;
|
|
3149
|
+
selectEl.appendChild(option);
|
|
3150
|
+
});
|
|
3151
|
+
// 恢复之前的选择(如果还存在)
|
|
3152
|
+
if (currentValue && models.includes(currentValue)) {
|
|
3153
|
+
selectEl.value = currentValue;
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
} catch {}
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
// 获取 Explorer 时间范围的总览数据
|
|
3160
|
+
async function fetchExplorerOverviewData(filterParams) {
|
|
3161
|
+
try {
|
|
3162
|
+
const params = new URLSearchParams();
|
|
3163
|
+
if (_expTimeRange.fromTs) params.set('from', String(_expTimeRange.fromTs));
|
|
3164
|
+
if (_expTimeRange.toTs) params.set('to', String(_expTimeRange.toTs));
|
|
3165
|
+
|
|
3166
|
+
// 添加筛选参数
|
|
3167
|
+
if (filterParams) {
|
|
3168
|
+
if (filterParams.agent) params.set('agent', filterParams.agent);
|
|
3169
|
+
if (filterParams.peer) params.set('peer', filterParams.peer);
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
const resp = await fetch(apiUrl('api/stats/overview?' + params.toString()), {
|
|
3173
|
+
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
3174
|
+
});
|
|
3175
|
+
const data = resp.ok ? await resp.json() : null;
|
|
3176
|
+
|
|
3177
|
+
if (data) {
|
|
3178
|
+
const ts = (data.token_stats && data.token_stats.all_time) ? data.token_stats.all_time
|
|
3179
|
+
: { 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 };
|
|
3180
|
+
const sessionCount = data.session_count || 0;
|
|
3181
|
+
const msgIn = data.msg_in || 0;
|
|
3182
|
+
const msgOut = data.msg_out || 0;
|
|
3183
|
+
const totalTokens = ts.cache_read_tokens + ts.cache_creation_tokens + ts.input_tokens + ts.output_tokens;
|
|
3184
|
+
const hitRate = totalTokens > 0 ? (ts.cache_read_tokens / totalTokens) * 100 : 0;
|
|
3185
|
+
|
|
3186
|
+
return { ts, sessionCount, msgIn, msgOut, hitRate };
|
|
3187
|
+
}
|
|
3188
|
+
} catch {}
|
|
3189
|
+
|
|
3190
|
+
// 返回空数据
|
|
3191
|
+
return {
|
|
3192
|
+
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 },
|
|
3193
|
+
sessionCount: 0,
|
|
3194
|
+
msgIn: 0,
|
|
3195
|
+
msgOut: 0,
|
|
3196
|
+
hitRate: 0
|
|
3197
|
+
};
|
|
3198
|
+
}
|
|
3199
|
+
|
|
1602
3200
|
async function loadExplorerSidebar() {
|
|
1603
3201
|
var token = localStorage.getItem(TOKEN_KEY);
|
|
1604
3202
|
var headers = { Authorization: 'Bearer ' + token };
|
|
1605
3203
|
try {
|
|
1606
|
-
var
|
|
1607
|
-
fetch('/api/stats/agents', { headers }),
|
|
1608
|
-
fetch('/api/stats/peers', { headers }),
|
|
1609
|
-
]);
|
|
3204
|
+
var agentsResp = await fetch(apiUrl('api/stats/agents'), { headers });
|
|
1610
3205
|
var agents = agentsResp.ok ? await agentsResp.json() : [];
|
|
1611
|
-
|
|
1612
|
-
|
|
3206
|
+
renderExplorerAgentList(agents);
|
|
3207
|
+
|
|
3208
|
+
// 初始加载时不加载 peers(等待用户选择 agent)
|
|
3209
|
+
renderExplorerPeerList([]);
|
|
1613
3210
|
} catch {}
|
|
1614
3211
|
}
|
|
1615
3212
|
|
|
1616
|
-
|
|
3213
|
+
// 渲染 Agent 列表
|
|
3214
|
+
function renderExplorerAgentList(agents) {
|
|
1617
3215
|
var agentList = $('#exp-agent-list');
|
|
1618
|
-
|
|
1619
|
-
if (!agentList || !peerList) return;
|
|
3216
|
+
if (!agentList) return;
|
|
1620
3217
|
|
|
1621
3218
|
// "All" item for agents
|
|
1622
3219
|
var allHtml = '<div class="exp-sidebar-item active" data-type="all" data-key="">' +
|
|
1623
|
-
'<span class="item-name"
|
|
3220
|
+
'<span class="item-name">' + t('usage.explorer.all') + '</span></div>';
|
|
1624
3221
|
|
|
1625
3222
|
agentList.innerHTML = allHtml + agents.map(function(a) {
|
|
1626
|
-
var name = a.agent_aid ? a.agent_aid.split('.')[0] : 'unknown';
|
|
3223
|
+
var name = a.agent_name || (a.agent_aid ? a.agent_aid.split('.')[0] : 'unknown');
|
|
1627
3224
|
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>';
|
|
3225
|
+
'<span class="item-name" title="' + escHtml(a.agent_aid) + '">' + escHtml(name) + '</span></div>';
|
|
1630
3226
|
}).join('');
|
|
1631
3227
|
|
|
3228
|
+
// 绑定点击事件
|
|
3229
|
+
agentList.querySelectorAll('.exp-sidebar-item').forEach(function(el) {
|
|
3230
|
+
el.addEventListener('click', async function() {
|
|
3231
|
+
// Clear active from all
|
|
3232
|
+
document.querySelectorAll('#exp-agent-list .exp-sidebar-item').forEach(function(x) { x.classList.remove('active'); });
|
|
3233
|
+
el.classList.add('active');
|
|
3234
|
+
|
|
3235
|
+
var type = el.getAttribute('data-type');
|
|
3236
|
+
var key = el.getAttribute('data-key');
|
|
3237
|
+
|
|
3238
|
+
if (type === 'all') {
|
|
3239
|
+
_expSelection = { type: null, key: null };
|
|
3240
|
+
$('#exp-selected-name').textContent = t('usage.explorer.all');
|
|
3241
|
+
// 选择"全部"时,清空 peers 列表
|
|
3242
|
+
renderExplorerPeerList([]);
|
|
3243
|
+
} else {
|
|
3244
|
+
_expSelection = { type: type, key: key };
|
|
3245
|
+
var name = el.querySelector('.item-name').textContent.trim();
|
|
3246
|
+
$('#exp-selected-name').textContent = name;
|
|
3247
|
+
// 选择特定 agent 时,加载该 agent 的 peers
|
|
3248
|
+
await loadPeersForAgent(key);
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
runExplorerQuery();
|
|
3252
|
+
});
|
|
3253
|
+
});
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
// 加载指定 agent 的 peers
|
|
3257
|
+
async function loadPeersForAgent(agentAid) {
|
|
3258
|
+
var token = localStorage.getItem(TOKEN_KEY);
|
|
3259
|
+
var headers = { Authorization: 'Bearer ' + token };
|
|
3260
|
+
try {
|
|
3261
|
+
const params = new URLSearchParams();
|
|
3262
|
+
params.set('agent', agentAid);
|
|
3263
|
+
// 不传递时间范围,获取该 agent 的所有 peers
|
|
3264
|
+
|
|
3265
|
+
var resp = await fetch(apiUrl('api/stats/peers?' + params.toString()), { headers });
|
|
3266
|
+
var peers = resp.ok ? await resp.json() : [];
|
|
3267
|
+
renderExplorerPeerList(peers);
|
|
3268
|
+
} catch {
|
|
3269
|
+
renderExplorerPeerList([]);
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
// 渲染 Peer 列表
|
|
3274
|
+
function renderExplorerPeerList(peers) {
|
|
3275
|
+
var peerList = $('#exp-peer-list');
|
|
3276
|
+
if (!peerList) return;
|
|
3277
|
+
|
|
3278
|
+
if (!peers || peers.length === 0) {
|
|
3279
|
+
peerList.innerHTML = '<div style="padding: 12px; color: var(--dim); font-size: 12px; text-align: center;">' + t('common.noData') + '</div>';
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
|
|
1632
3283
|
peerList.innerHTML = peers.map(function(p) {
|
|
1633
3284
|
var name = p.peer_key || 'unknown';
|
|
1634
|
-
//
|
|
1635
|
-
var display = name.replace(/^aun#/, '').split('.')[0];
|
|
3285
|
+
// 优先显示peer_name,否则简化显示peer_key
|
|
3286
|
+
var display = p.peer_name || name.replace(/^aun#/, '').split('#')[0].split('.')[0];
|
|
3287
|
+
|
|
3288
|
+
// 添加聊天类型标签
|
|
3289
|
+
var typeTag = '';
|
|
3290
|
+
if (p.peer_chat_type === 'group') {
|
|
3291
|
+
typeTag = '<span class="peer-tag peer-tag-group">' + t('usage.explorer.chatType.group') + '</span>';
|
|
3292
|
+
} else if (p.peer_chat_type === 'private') {
|
|
3293
|
+
typeTag = '<span class="peer-tag peer-tag-private">' + t('usage.explorer.chatType.private') + '</span>';
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
// 群聊人数标签
|
|
3297
|
+
var memberTag = '';
|
|
3298
|
+
if (p.peer_chat_type === 'group' && p.peer_group_member_count) {
|
|
3299
|
+
memberTag = '<span class="peer-tag peer-tag-count">' + p.peer_group_member_count + t('usage.explorer.memberCount') + '</span>';
|
|
3300
|
+
}
|
|
3301
|
+
|
|
1636
3302
|
return '<div class="exp-sidebar-item" data-type="peer" data-key="' + escHtml(p.peer_key) + '">' +
|
|
1637
|
-
'<span class="item-name" title="' + escHtml(name) + '">' +
|
|
3303
|
+
'<span class="item-name" title="' + escHtml(name) + '">' +
|
|
3304
|
+
(typeTag ? typeTag + ' ' : '') + escHtml(display) + (memberTag ? ' ' + memberTag : '') +
|
|
3305
|
+
'</span>' +
|
|
1638
3306
|
'<span class="item-meta">' + fmtTokens((p.input_tokens || 0) + (p.output_tokens || 0)) + '</span></div>';
|
|
1639
3307
|
}).join('');
|
|
1640
3308
|
|
|
1641
|
-
// Bind click events
|
|
1642
|
-
|
|
1643
|
-
allItems.forEach(function(el) {
|
|
3309
|
+
// Bind click events for peers
|
|
3310
|
+
peerList.querySelectorAll('.exp-sidebar-item').forEach(function(el) {
|
|
1644
3311
|
el.addEventListener('click', function() {
|
|
1645
|
-
// Clear active from all
|
|
1646
|
-
|
|
3312
|
+
// Clear active from all peers
|
|
3313
|
+
document.querySelectorAll('#exp-peer-list .exp-sidebar-item').forEach(function(x) { x.classList.remove('active'); });
|
|
1647
3314
|
el.classList.add('active');
|
|
3315
|
+
|
|
1648
3316
|
var type = el.getAttribute('data-type');
|
|
1649
3317
|
var key = el.getAttribute('data-key');
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
_expSelection = { type: type, key: key };
|
|
1655
|
-
$('#exp-selected-name').textContent = key;
|
|
1656
|
-
}
|
|
3318
|
+
_expSelection = { type: type, key: key };
|
|
3319
|
+
var name = el.querySelector('.item-name').textContent.trim();
|
|
3320
|
+
$('#exp-selected-name').textContent = name;
|
|
3321
|
+
|
|
1657
3322
|
runExplorerQuery();
|
|
1658
3323
|
});
|
|
1659
3324
|
});
|
|
@@ -1662,11 +3327,13 @@ function renderExplorerSidebar(agents, peers) {
|
|
|
1662
3327
|
function escHtml(s) { return (s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
1663
3328
|
|
|
1664
3329
|
async function runExplorerQuery() {
|
|
3330
|
+
// 使用 Explorer 自己的时间范围
|
|
3331
|
+
const fromTs = _expTimeRange.fromTs;
|
|
3332
|
+
const toTs = _expTimeRange.toTs;
|
|
3333
|
+
|
|
1665
3334
|
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()));
|
|
3335
|
+
if (fromTs) params.set('from', String(fromTs));
|
|
3336
|
+
if (toTs) params.set('to', String(toTs));
|
|
1670
3337
|
// Inject selection from sidebar
|
|
1671
3338
|
if (_expSelection.type === 'agent' && _expSelection.key) params.set('agent', _expSelection.key);
|
|
1672
3339
|
if (_expSelection.type === 'peer' && _expSelection.key) params.set('peer', _expSelection.key);
|
|
@@ -1677,33 +3344,123 @@ async function runExplorerQuery() {
|
|
|
1677
3344
|
|
|
1678
3345
|
var data;
|
|
1679
3346
|
try {
|
|
1680
|
-
var resp = await fetch('
|
|
3347
|
+
var resp = await fetch(apiUrl('api/stats/explorer?' + params.toString()), {
|
|
1681
3348
|
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
1682
3349
|
});
|
|
1683
3350
|
if (!resp.ok) return;
|
|
1684
3351
|
data = await resp.json();
|
|
1685
3352
|
} catch { return; }
|
|
1686
3353
|
|
|
1687
|
-
//
|
|
3354
|
+
// 根据查询结果计算卡片数据
|
|
1688
3355
|
var cardsEl = $('#exp-detail-cards');
|
|
1689
|
-
if (
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
3356
|
+
if (cardsEl) {
|
|
3357
|
+
// 如果有筛选条件(agent/peer),使用查询结果计算;否则需要获取总览数据
|
|
3358
|
+
const hasFilter = _expSelection.type && _expSelection.key;
|
|
3359
|
+
|
|
3360
|
+
let cardData;
|
|
3361
|
+
let cardTitle = null; // 用于显示选中的 agent/peer 信息
|
|
3362
|
+
|
|
3363
|
+
if (hasFilter) {
|
|
3364
|
+
// 有筛选:根据查询结果计算 token 数据,并获取会话信息
|
|
3365
|
+
var totIn = 0, totOut = 0, totCacheCreation = 0, totCacheRead = 0, totCalls = 0;
|
|
3366
|
+
if (data && data.length) {
|
|
3367
|
+
data.forEach(function(r) {
|
|
3368
|
+
totIn += r.input_tokens || 0;
|
|
3369
|
+
totOut += r.output_tokens || 0;
|
|
3370
|
+
totCacheCreation += r.cache_creation_tokens || 0;
|
|
3371
|
+
totCacheRead += r.cache_read_tokens || 0;
|
|
3372
|
+
totCalls += r.call_count || 0;
|
|
3373
|
+
});
|
|
3374
|
+
}
|
|
3375
|
+
const totalTokens = totCacheRead + totCacheCreation + totIn + totOut;
|
|
3376
|
+
const hitRate = totalTokens > 0 ? (totCacheRead / totalTokens) * 100 : 0;
|
|
3377
|
+
|
|
3378
|
+
// 构建筛选参数
|
|
3379
|
+
const filterParams = {};
|
|
3380
|
+
if (_expSelection.type === 'agent') filterParams.agent = _expSelection.key;
|
|
3381
|
+
if (_expSelection.type === 'peer') filterParams.peer = _expSelection.key;
|
|
3382
|
+
|
|
3383
|
+
// 获取该筛选条件下的会话信息
|
|
3384
|
+
const overviewData = await fetchExplorerOverviewData(filterParams);
|
|
3385
|
+
|
|
3386
|
+
cardData = {
|
|
3387
|
+
ts: {
|
|
3388
|
+
call_count: totCalls,
|
|
3389
|
+
input_tokens: totIn,
|
|
3390
|
+
output_tokens: totOut,
|
|
3391
|
+
cache_creation_tokens: totCacheCreation,
|
|
3392
|
+
cache_read_tokens: totCacheRead,
|
|
3393
|
+
cost_official_usd: overviewData.ts.cost_official_usd || 0,
|
|
3394
|
+
cost_official_cny: overviewData.ts.cost_official_cny || 0,
|
|
3395
|
+
cost_usd: overviewData.ts.cost_usd || 0,
|
|
3396
|
+
cost_cny: overviewData.ts.cost_cny || 0
|
|
3397
|
+
},
|
|
3398
|
+
sessionCount: overviewData.sessionCount || 0,
|
|
3399
|
+
msgIn: overviewData.msgIn || 0,
|
|
3400
|
+
msgOut: overviewData.msgOut || 0,
|
|
3401
|
+
hitRate: hitRate
|
|
3402
|
+
};
|
|
3403
|
+
|
|
3404
|
+
// 构建卡片标题
|
|
3405
|
+
if (_expSelection.type === 'agent') {
|
|
3406
|
+
// 从侧边栏获取 agent 名称
|
|
3407
|
+
const selectedItem = document.querySelector('#exp-agent-list .exp-sidebar-item.active .item-name');
|
|
3408
|
+
const agentName = selectedItem ? selectedItem.textContent.trim() : '';
|
|
3409
|
+
const agentAid = _expSelection.key;
|
|
3410
|
+
cardTitle = agentName && agentName !== agentAid.split('.')[0]
|
|
3411
|
+
? `${agentName} (AID: ${agentAid})`
|
|
3412
|
+
: `AID: ${agentAid}`;
|
|
3413
|
+
} else if (_expSelection.type === 'peer') {
|
|
3414
|
+
// 从侧边栏获取 peer 名称(去掉标签)
|
|
3415
|
+
const selectedItem = document.querySelector('#exp-peer-list .exp-sidebar-item.active .item-name');
|
|
3416
|
+
if (selectedItem) {
|
|
3417
|
+
// 克隆节点并移除所有标签元素
|
|
3418
|
+
const clone = selectedItem.cloneNode(true);
|
|
3419
|
+
const tags = clone.querySelectorAll('.peer-tag');
|
|
3420
|
+
tags.forEach(tag => tag.remove());
|
|
3421
|
+
const peerName = clone.textContent.trim();
|
|
3422
|
+
const peerKey = _expSelection.key;
|
|
3423
|
+
cardTitle = peerName ? `${peerName} (Peer: ${peerKey.split('#')[3] || peerKey.split('#')[0]})` : `Peer: ${peerKey}`;
|
|
3424
|
+
} else {
|
|
3425
|
+
cardTitle = `Peer: ${_expSelection.key}`;
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
} else {
|
|
3429
|
+
// 无筛选:获取 Explorer 时间范围的总览数据
|
|
3430
|
+
cardData = await fetchExplorerOverviewData();
|
|
1699
3431
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
3432
|
+
|
|
3433
|
+
const { ts, sessionCount, msgIn, msgOut, hitRate } = cardData;
|
|
3434
|
+
|
|
3435
|
+
// 不显示标题行,直接显示卡片
|
|
3436
|
+
// 注意:会话信息是该时间范围的总数(不区分 agent/peer)
|
|
3437
|
+
const sessionCard = makeMultiValueCard([
|
|
3438
|
+
{ label: t('usage.card.sessionCount'), value: sessionCount },
|
|
3439
|
+
{ label: t('usage.card.msgIn'), value: msgIn },
|
|
3440
|
+
{ label: t('usage.card.msgOut'), value: msgOut }
|
|
3441
|
+
], t('usage.card.sessionInfo'), 'session-group');
|
|
3442
|
+
|
|
3443
|
+
const usageCard = makeMultiValueCard([
|
|
3444
|
+
{ label: t('usage.card.modelCalls'), value: ts.call_count },
|
|
3445
|
+
{ label: t('usage.card.inputTokens'), value: fmtTokens(ts.input_tokens) },
|
|
3446
|
+
{ label: t('usage.card.outputTokens'), value: fmtTokens(ts.output_tokens) },
|
|
3447
|
+
{ label: t('usage.card.cacheCreation'), value: fmtTokens(ts.cache_creation_tokens) },
|
|
3448
|
+
{ label: t('usage.card.cacheHitTokens'), value: fmtTokens(ts.cache_read_tokens) },
|
|
3449
|
+
{ label: t('usage.card.cacheHitRate'), value: hitRate.toFixed(1) + '%' }
|
|
3450
|
+
], t('usage.card.usageInfo'), 'usage-group');
|
|
3451
|
+
|
|
3452
|
+
const costCard = makeMultiValueCard([
|
|
3453
|
+
{ label: t('usage.card.costOfficial'), value: fmtCost(ts.cost_official_usd, ts.cost_official_cny) },
|
|
3454
|
+
{ label: t('usage.card.costGateway'), value: fmtCost(ts.cost_usd, ts.cost_cny) }
|
|
3455
|
+
], t('usage.card.costInfo'), 'cost-group');
|
|
3456
|
+
|
|
3457
|
+
cardsEl.innerHTML = sessionCard + usageCard + costCard;
|
|
3458
|
+
cardsEl.style.display = 'flex';
|
|
1702
3459
|
}
|
|
1703
3460
|
|
|
1704
3461
|
if (!data || !data.length) {
|
|
1705
3462
|
var tbl = $('#usage-explorer-table');
|
|
1706
|
-
if (tbl) tbl.innerHTML = '<tr><td>
|
|
3463
|
+
if (tbl) tbl.innerHTML = '<tr><td>' + t('usage.explorer.noData') + '</td></tr>';
|
|
1707
3464
|
var chartEl = $('#usage-explorer-chart');
|
|
1708
3465
|
if (chartEl && _explorerChart) { _explorerChart.dispose(); _explorerChart = null; }
|
|
1709
3466
|
return;
|
|
@@ -1718,13 +3475,13 @@ async function runExplorerQuery() {
|
|
|
1718
3475
|
var periods = data.map(function(r) { return r.period; });
|
|
1719
3476
|
_explorerChart.setOption({
|
|
1720
3477
|
tooltip: { trigger: 'axis' },
|
|
1721
|
-
legend: { data: ['
|
|
3478
|
+
legend: { data: [t('usage.card.input'), t('usage.card.output')], top: 0, textStyle: { fontSize: 11 } },
|
|
1722
3479
|
grid: { top: 30, bottom: 30, left: 60, right: 16 },
|
|
1723
3480
|
xAxis: { type: 'category', data: periods, axisLabel: { fontSize: 10, rotate: 30 } },
|
|
1724
3481
|
yAxis: { type: 'value', axisLabel: { formatter: function(v) { return fmtTokens(v); } } },
|
|
1725
3482
|
series: [
|
|
1726
|
-
{ name: '
|
|
1727
|
-
{ name: '
|
|
3483
|
+
{ 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' } },
|
|
3484
|
+
{ 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
3485
|
]
|
|
1729
3486
|
});
|
|
1730
3487
|
}
|
|
@@ -1733,7 +3490,7 @@ async function runExplorerQuery() {
|
|
|
1733
3490
|
var tbl = $('#usage-explorer-table');
|
|
1734
3491
|
if (tbl) {
|
|
1735
3492
|
tbl.innerHTML =
|
|
1736
|
-
'<thead><tr><th>
|
|
3493
|
+
'<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
3494
|
'<tbody>' + data.map(function(r) {
|
|
1738
3495
|
return '<tr><td>' + r.period + '</td><td>' + fmtTokens(r.input_tokens) + '</td><td>' + fmtTokens(r.output_tokens) +
|
|
1739
3496
|
'</td><td>' + fmtTokens(r.cache_creation_tokens) + '</td><td>' + fmtTokens(r.cache_read_tokens) +
|
|
@@ -1875,21 +3632,21 @@ function renderMonitor(data) {
|
|
|
1875
3632
|
$('#mon-agent-table-wrap').innerHTML =
|
|
1876
3633
|
'<div class="mon-section-title">各 Agent 运行状态</div>' +
|
|
1877
3634
|
'<table class="usage-table"><thead><tr>' +
|
|
1878
|
-
'<th>Agent</th><th>状态</th><th>收</th><th>发</th><th
|
|
3635
|
+
'<th>Agent</th><th>状态</th><th>收</th><th>发</th><th>错</th><th>断</th><th>完</th><th>队列</th><th>处理中</th>' +
|
|
1879
3636
|
'</tr></thead><tbody>' +
|
|
1880
3637
|
(agents.length ? agents.map(function (a) {
|
|
1881
|
-
var st = a.
|
|
3638
|
+
var st = a.runtimeStats || {};
|
|
1882
3639
|
var dot = dotMap[a.status] || 'off';
|
|
1883
3640
|
return '<tr>' +
|
|
1884
3641
|
'<td title="' + esc(a.aid) + '">' + esc(a.agentName || shortAid(a.aid)) + '</td>' +
|
|
1885
3642
|
'<td><span class="dot ' + dot + '"></span>' + esc(a.status) + '</td>' +
|
|
1886
|
-
'<td>' + (st.
|
|
1887
|
-
'<td>' + (st.
|
|
1888
|
-
'<td>' +
|
|
1889
|
-
'<td>' +
|
|
1890
|
-
'<td>' + (st.
|
|
3643
|
+
'<td>' + (st.received || 0) + '</td>' +
|
|
3644
|
+
'<td>' + (st.sent || 0) + '</td>' +
|
|
3645
|
+
'<td>' + (st.errors || 0) + '</td>' +
|
|
3646
|
+
'<td>' + (st.interrupts || 0) + '</td>' +
|
|
3647
|
+
'<td>' + (st.completed || 0) + '</td>' +
|
|
1891
3648
|
'<td>' + (st.queued || 0) + '</td>' +
|
|
1892
|
-
'<td>' + (st.processing
|
|
3649
|
+
'<td>' + (st.processing || 0) + '</td>' +
|
|
1893
3650
|
'</tr>';
|
|
1894
3651
|
}).join('') : '<tr><td colspan="9" style="text-align:center;color:var(--dim)">暂无 Agent</td></tr>') +
|
|
1895
3652
|
'</tbody></table>';
|
|
@@ -1956,14 +3713,25 @@ function monDualLine(elId, varKey, times, isDark, title, series, fmtY, yRange) {
|
|
|
1956
3713
|
});
|
|
1957
3714
|
}
|
|
1958
3715
|
|
|
1959
|
-
window.addEventListener('DOMContentLoaded', () => {
|
|
3716
|
+
window.addEventListener('DOMContentLoaded', async () => {
|
|
1960
3717
|
initTheme();
|
|
1961
3718
|
initPairUI();
|
|
3719
|
+
initMsgTipFloat();
|
|
3720
|
+
|
|
3721
|
+
// 初始化语言切换
|
|
3722
|
+
const langBtn = $('#lang-btn');
|
|
3723
|
+
if (langBtn) {
|
|
3724
|
+
langBtn.addEventListener('click', toggleLang);
|
|
3725
|
+
}
|
|
3726
|
+
updateI18n(); // 应用当前语言
|
|
3727
|
+
|
|
3728
|
+
// 已有 token 直接进;否则先试本地直连免配对,失败再回落配对页。
|
|
3729
|
+
if (!localStorage.getItem(TOKEN_KEY)) {
|
|
3730
|
+
await tryLocalAutoPair();
|
|
3731
|
+
}
|
|
1962
3732
|
if (localStorage.getItem(TOKEN_KEY)) {
|
|
1963
3733
|
showApp();
|
|
1964
3734
|
startApp();
|
|
1965
|
-
loadUsageDashboard();
|
|
1966
|
-
loadUsageOverview();
|
|
1967
3735
|
initUsageSubtabs();
|
|
1968
3736
|
} else {
|
|
1969
3737
|
showPairPage();
|