xiaoyukkkk commited on
Commit
eee7141
·
verified ·
1 Parent(s): e796cfc

Upload 2 files

Browse files
templates/admin/index.html ADDED
@@ -0,0 +1,421 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% from "components/alerts.html" import api_key_status, error_alert %}
3
+ {% from "components/account_table.html" import account_table %}
4
+
5
+ {% block title %}系统管理 - Gemini Business API{% endblock %}
6
+
7
+ {% block extra_css %}
8
+ <link rel="stylesheet" href="/static/css/admin.css">
9
+ {% endblock %}
10
+
11
+ {% block content %}
12
+ <div class="container">
13
+ <div class="header">
14
+ <div class="header-info">
15
+ <h1>Gemini-Business2api</h1>
16
+ <div class="subtitle">多账户代理面板</div>
17
+ </div>
18
+ <div class="header-actions">
19
+ <a href="/public/uptime/html" class="btn" target="_blank">📊 状态监控</a>
20
+ <a href="/public/log/html" class="btn" target="_blank">📄 公开日志</a>
21
+ <a href="/{{ admin_path_segment }}/log/html" class="btn" target="_blank">🔧 管理日志</a>
22
+ <button class="btn" onclick="document.getElementById('fileInput').click()">📥 批量上传</button>
23
+ <input type="file" id="fileInput" accept=".json" multiple style="display:none" onchange="handleFileUpload(event)">
24
+ <button class="btn" onclick="showEditConfig()" id="edit-btn">✏️ 编辑配置</button>
25
+ </div>
26
+ </div>
27
+
28
+ <!-- Tabs Navigation -->
29
+ <div class="tabs-nav">
30
+ <button class="tab-button active" onclick="switchTab('accounts')">📋 账户管理</button>
31
+ <button class="tab-button" onclick="switchTab('api')">📚 API文档</button>
32
+ <button class="tab-button" onclick="switchTab('config')">⚙️ 系统配置</button>
33
+ <button class="tab-button" onclick="switchTab('settings')">🔧 系统设置</button>
34
+ </div>
35
+
36
+ <!-- Tab 1: 账户管理 -->
37
+ <div id="tab-accounts" class="tab-content active">
38
+ {{ api_key_status(has_api_key) }}
39
+ {{ error_alert(error_count) }}
40
+
41
+ <div class="alert alert-primary">
42
+ <div class="alert-icon">🔗</div>
43
+ <div class="alert-content">
44
+ <strong>API 接口信息</strong>
45
+ <div style="margin-top: 10px;">
46
+ <div style="margin-bottom: 12px;">
47
+ <div style="color: #86868b; font-size: 11px; margin-bottom: 4px;">基础端点(部分客户端)</div>
48
+ <code style="font-size: 11px; background: rgba(0,0,0,0.05); padding: 4px 8px; border-radius: 4px; display: inline-block; word-break: break-all;">{{ api_base_url }}</code>
49
+ </div>
50
+ <div style="margin-bottom: 12px;">
51
+ <div style="color: #86868b; font-size: 11px; margin-bottom: 4px;">API Base(OpenAI SDK 等)</div>
52
+ <code style="font-size: 11px; background: rgba(0,0,0,0.05); padding: 4px 8px; border-radius: 4px; display: inline-block; word-break: break-all;">{{ api_base_v1 }}</code>
53
+ </div>
54
+ <div style="margin-bottom: 12px;">
55
+ <div style="color: #86868b; font-size: 11px; margin-bottom: 4px;">完整聊天接口(直接调用)</div>
56
+ <code style="font-size: 11px; background: rgba(0,0,0,0.05); padding: 4px 8px; border-radius: 4px; display: inline-block; word-break: break-all;">{{ api_endpoint }}</code>
57
+ </div>
58
+ <div style="margin-bottom: 12px;">
59
+ <div style="color: #86868b; font-size: 11px; margin-bottom: 4px;">API 密钥</div>
60
+ <code style="font-size: 11px; background: rgba(0,0,0,0.05); padding: 4px 8px; border-radius: 4px; display: inline-block;">{% if main.API_KEY %}{{ main.API_KEY }}{% else %}<span style="color: #ff9500;">未设置(公开访问)</span>{% endif %}</code>
61
+ </div>
62
+ <div style="margin-bottom: 12px;">
63
+ <div style="color: #86868b; font-size: 11px; margin-bottom: 6px;">支持的模型</div>
64
+ <div style="display: flex; flex-wrap: wrap; gap: 6px;">
65
+ <span style="background: #f0f0f2; color: #1d1d1f; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;">gemini-auto</span>
66
+ <span style="background: #f0f0f2; color: #1d1d1f; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;">gemini-2.5-flash</span>
67
+ <span style="background: #f0f0f2; color: #1d1d1f; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;">gemini-2.5-pro</span>
68
+ <span style="background: #f0f0f2; color: #1d1d1f; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;">gemini-3-flash-preview</span>
69
+ <span style="background: #f0f0f2; color: #1d1d1f; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;">gemini-3-pro-preview</span>
70
+ </div>
71
+ </div>
72
+ <div style="background: rgba(0,0,0,0.03); padding: 10px; border-radius: 6px;">
73
+ <div style="font-size: 11px; color: #1d1d1f; margin-bottom: 4px; font-weight: 600;">📸 图片生成</div>
74
+ <div style="font-size: 11px; color: #86868b; line-height: 1.6;">
75
+ 可在"系统设置"标签页自定义配置
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </div>
81
+
82
+ <div class="section">
83
+ <div class="section-title">账户状态 ({{ multi_account_mgr.accounts|length }} 个)</div>
84
+ <div style="color: #6b6b6b; font-size: 12px; margin-bottom: 12px; padding-left: 4px;">
85
+ 默认过期时间12小时,注意北京时间 • 批量上传使用 <code style="font-size: 11px; background: rgba(0,0,0,0.05); padding: 2px 6px; border-radius: 4px;">script/download-config.js</code> 油猴脚本
86
+ </div>
87
+ {{ account_table(accounts_data) }}
88
+ </div>
89
+ </div>
90
+
91
+ <!-- Tab 2: API文档 -->
92
+ <div id="tab-api" class="tab-content">
93
+ <div class="section">
94
+ <div class="section-title">API 端点列表</div>
95
+
96
+ <div class="current-url-row">
97
+ <span style="font-size:12px; font-weight:600; color:#0071e3; margin-right:8px;">当前页面:</span>
98
+ <code style="background:none; padding:0; color:#1d1d1f;">{{ current_url }}</code>
99
+ </div>
100
+
101
+ <table class="ep-table">
102
+ <tr>
103
+ <td width="70"><span class="method m-post">POST</span></td>
104
+ <td><span class="ep-path">/{{ api_path_segment }}v1/chat/completions</span></td>
105
+ <td><span class="ep-desc">OpenAI 兼容对话接口</span></td>
106
+ </tr>
107
+ <tr>
108
+ <td><span class="method m-get">GET</span></td>
109
+ <td><span class="ep-path">/{{ api_path_segment }}v1/models</span></td>
110
+ <td><span class="ep-desc">获取模型列表</span></td>
111
+ </tr>
112
+ <tr>
113
+ <td><span class="method m-get">GET</span></td>
114
+ <td><span class="ep-path">/{{ admin_path_segment }}</span></td>
115
+ <td><span class="ep-desc">管理首页 (需登录)</span></td>
116
+ </tr>
117
+ <tr>
118
+ <td><span class="method m-get">GET</span></td>
119
+ <td><span class="ep-path">/{{ admin_path_segment }}/health</span></td>
120
+ <td><span class="ep-desc">健康检查 (需登录)</span></td>
121
+ </tr>
122
+ <tr>
123
+ <td><span class="method m-get">GET</span></td>
124
+ <td><span class="ep-path">/{{ admin_path_segment }}/accounts</span></td>
125
+ <td><span class="ep-desc">账户状态 JSON (需登录)</span></td>
126
+ </tr>
127
+ <tr>
128
+ <td><span class="method m-get">GET</span></td>
129
+ <td><span class="ep-path">/{{ admin_path_segment }}/log</span></td>
130
+ <td><span class="ep-desc">获取日志 JSON (需登录)</span></td>
131
+ </tr>
132
+ <tr>
133
+ <td><span class="method m-get">GET</span></td>
134
+ <td><span class="ep-path">/{{ admin_path_segment }}/log/html</span></td>
135
+ <td><span class="ep-desc">日志查看器 HTML (需登录)</span></td>
136
+ </tr>
137
+ <tr>
138
+ <td><span class="method m-del">DEL</span></td>
139
+ <td><span class="ep-path">/{{ admin_path_segment }}/log?confirm=yes</span></td>
140
+ <td><span class="ep-desc">清空系统日志 (需登录)</span></td>
141
+ </tr>
142
+ <tr>
143
+ <td><span class="method m-get">GET</span></td>
144
+ <td><span class="ep-path">/public/stats</span></td>
145
+ <td><span class="ep-desc">公开统计数据</span></td>
146
+ </tr>
147
+ <tr>
148
+ <td><span class="method m-get">GET</span></td>
149
+ <td><span class="ep-path">/public/log</span></td>
150
+ <td><span class="ep-desc">公开日志 (JSON, 脱敏)</span></td>
151
+ </tr>
152
+ <tr>
153
+ <td><span class="method m-get">GET</span></td>
154
+ <td><span class="ep-path">/public/log/html</span></td>
155
+ <td><span class="ep-desc">公开日志查看器 (HTML)</span></td>
156
+ </tr>
157
+ <tr>
158
+ <td><span class="method m-get">GET</span></td>
159
+ <td><span class="ep-path">/public/uptime</span></td>
160
+ <td><span class="ep-desc">实时状态监控 (JSON)</span></td>
161
+ </tr>
162
+ <tr>
163
+ <td><span class="method m-get">GET</span></td>
164
+ <td><span class="ep-path">/public/uptime/html</span></td>
165
+ <td><span class="ep-desc">实时状态监控页面 (HTML)</span></td>
166
+ </tr>
167
+ <tr>
168
+ <td><span class="method m-get">GET</span></td>
169
+ <td><span class="ep-path">/docs</span></td>
170
+ <td><span class="ep-desc">Swagger API 文档</span></td>
171
+ </tr>
172
+ <tr>
173
+ <td><span class="method m-get">GET</span></td>
174
+ <td><span class="ep-path">/redoc</span></td>
175
+ <td><span class="ep-desc">ReDoc API 文档</span></td>
176
+ </tr>
177
+ </table>
178
+ </div>
179
+ </div>
180
+
181
+ <!-- Tab 3: 系统配置 -->
182
+ <div id="tab-config" class="tab-content">
183
+ <div class="section">
184
+ <div class="section-title">当前配置状态</div>
185
+ <div class="grid-env">
186
+ <div class="stack-col">
187
+ <div class="card">
188
+ <h3>环境变量 <span class="badge badge-required">ENV</span></h3>
189
+ <div style="margin-top: 12px;">
190
+ <div class="env-var">
191
+ <div><div class="env-name">ADMIN_KEY</div><div class="env-desc">管理员密钥</div></div>
192
+ <div class="env-value">已设置</div>
193
+ </div>
194
+ <div class="env-var">
195
+ <div><div class="env-name">PATH_PREFIX</div><div class="env-desc">API路径前缀</div></div>
196
+ <div class="env-value">{{ main.PATH_PREFIX or '未设置' }}</div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+
201
+ <div class="card">
202
+ <h3>基础配置 <span class="badge badge-optional">YAML</span></h3>
203
+ <div style="margin-top: 12px;">
204
+ <div class="env-var">
205
+ <div><div class="env-name">API_KEY</div><div class="env-desc">API访问密钥</div></div>
206
+ <div class="env-value">{% if main.API_KEY %}已设置{% else %}未设置(公开访问){% endif %}</div>
207
+ </div>
208
+ <div class="env-var">
209
+ <div><div class="env-name">BASE_URL</div><div class="env-desc">服务器URL</div></div>
210
+ <div class="env-value">{% if main.BASE_URL %}已设置{% else %}自动检测{% endif %}</div>
211
+ </div>
212
+ <div class="env-var">
213
+ <div><div class="env-name">PROXY</div><div class="env-desc">代理地址</div></div>
214
+ <div class="env-value">{% if main.PROXY %}已设置{% else %}未设置{% endif %}</div>
215
+ </div>
216
+ </div>
217
+ </div>
218
+ </div>
219
+
220
+ <div class="card">
221
+ <h3>重试策略 <span class="badge badge-optional">YAML</span></h3>
222
+ <div style="margin-top: 12px;">
223
+ <div class="env-var">
224
+ <div><div class="env-name">max_new_session_tries</div><div class="env-desc">新会话尝试数</div></div>
225
+ <div class="env-value">{{ main.MAX_NEW_SESSION_TRIES }}</div>
226
+ </div>
227
+ <div class="env-var">
228
+ <div><div class="env-name">max_request_retries</div><div class="env-desc">请求重试次数</div></div>
229
+ <div class="env-value">{{ main.MAX_REQUEST_RETRIES }}</div>
230
+ </div>
231
+ <div class="env-var">
232
+ <div><div class="env-name">max_account_switch_tries</div><div class="env-desc">账户切换次数</div></div>
233
+ <div class="env-value">{{ main.MAX_ACCOUNT_SWITCH_TRIES }}</div>
234
+ </div>
235
+ <div class="env-var">
236
+ <div><div class="env-name">account_failure_threshold</div><div class="env-desc">失败阈值</div></div>
237
+ <div class="env-value">{{ main.ACCOUNT_FAILURE_THRESHOLD }} 次</div>
238
+ </div>
239
+ <div class="env-var">
240
+ <div><div class="env-name">rate_limit_cooldown_seconds</div><div class="env-desc">429冷却时间</div></div>
241
+ <div class="env-value">{{ main.RATE_LIMIT_COOLDOWN_SECONDS }} 秒</div>
242
+ </div>
243
+ <div class="env-var">
244
+ <div><div class="env-name">session_cache_ttl_seconds</div><div class="env-desc">会话缓存时间</div></div>
245
+ <div class="env-value">{{ main.SESSION_CACHE_TTL_SECONDS }} 秒</div>
246
+ </div>
247
+ </div>
248
+ </div>
249
+
250
+ <div class="card">
251
+ <h3>公开展示 <span class="badge badge-optional">YAML</span></h3>
252
+ <div style="margin-top: 12px;">
253
+ <div class="env-var">
254
+ <div><div class="env-name">LOGO_URL</div><div class="env-desc">Logo图片</div></div>
255
+ <div class="env-value">{% if main.LOGO_URL %}已设置{% else %}未设置{% endif %}</div>
256
+ </div>
257
+ <div class="env-var">
258
+ <div><div class="env-name">CHAT_URL</div><div class="env-desc">对话链接</div></div>
259
+ <div class="env-value">{% if main.CHAT_URL %}已设置{% else %}未设置{% endif %}</div>
260
+ </div>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ </div>
265
+ </div>
266
+
267
+ <!-- Tab 4: 系统设置 -->
268
+ <div id="tab-settings" class="tab-content">
269
+ <div class="section">
270
+ <div class="section-title">系统设置</div>
271
+ <div style="color: #6b6b6b; font-size: 12px; margin-bottom: 16px; padding-left: 4px;">
272
+ ✅ 所有配置修改后<strong>立即生效</strong>,无需重启服务。配置保存在 <code>data/settings.yaml</code><br>
273
+ 📋 配置优先级:<strong>YAML > 环境变量 > 默认值</strong>(安全配置除外,仅从环境变量读取)
274
+ </div>
275
+
276
+ <div class="grid-env">
277
+ <div class="stack-col">
278
+ <!-- 基础配置 -->
279
+ <div class="card">
280
+ <h3>基础配置</h3>
281
+ <div style="margin-top: 12px;">
282
+ <div class="setting-item">
283
+ <label>API 访问密钥</label>
284
+ <input type="text" id="setting-api-key" placeholder="留空则公开访问" />
285
+ </div>
286
+ <div class="setting-item">
287
+ <label>服务器 URL</label>
288
+ <input type="text" id="setting-base-url" placeholder="留空则自动检测" />
289
+ </div>
290
+ <div class="setting-item">
291
+ <label>代理地址</label>
292
+ <input type="text" id="setting-proxy" placeholder="如 http://127.0.0.1:7890" />
293
+ </div>
294
+ </div>
295
+ </div>
296
+
297
+ <!-- 图片生成配置 -->
298
+ <div class="card">
299
+ <h3>📸 图片生成配置</h3>
300
+ <div style="margin-top: 12px;">
301
+ <div class="setting-item">
302
+ <label style="display: flex; align-items: center; gap: 8px;">
303
+ <input type="checkbox" id="setting-image-enabled" style="width: auto;" />
304
+ 启用图片生成
305
+ </label>
306
+ </div>
307
+ <div class="setting-item">
308
+ <label>支持的模型</label>
309
+ <div id="setting-image-models" style="display: flex; flex-direction: column; gap: 6px; margin-top: 6px;">
310
+ <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; font-size: 12px;">
311
+ <input type="checkbox" value="gemini-3-pro-preview" style="width: auto;" /> gemini-3-pro-preview
312
+ </label>
313
+ <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; font-size: 12px;">
314
+ <input type="checkbox" value="gemini-2.5-pro" style="width: auto;" /> gemini-2.5-pro
315
+ </label>
316
+ <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; font-size: 12px;">
317
+ <input type="checkbox" value="gemini-2.5-flash" style="width: auto;" /> gemini-2.5-flash
318
+ </label>
319
+ <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; font-size: 12px;">
320
+ <input type="checkbox" value="gemini-3-flash-preview" style="width: auto;" /> gemini-3-flash-preview
321
+ </label>
322
+ <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; font-size: 12px;">
323
+ <input type="checkbox" value="gemini-auto" style="width: auto;" /> gemini-auto
324
+ </label>
325
+ </div>
326
+ </div>
327
+ </div>
328
+ </div>
329
+ </div>
330
+
331
+ <div class="stack-col">
332
+ <!-- 重试策略配置 -->
333
+ <div class="card">
334
+ <h3>🔄 重试策略配置</h3>
335
+ <div style="margin-top: 12px;">
336
+ <div class="setting-item">
337
+ <label>新会话尝试账户数</label>
338
+ <input type="number" id="setting-max-new-session" min="1" max="20" />
339
+ </div>
340
+ <div class="setting-item">
341
+ <label>请求失败重试次数</label>
342
+ <input type="number" id="setting-max-retries" min="1" max="10" />
343
+ </div>
344
+ <div class="setting-item">
345
+ <label>账户切换尝试次数</label>
346
+ <input type="number" id="setting-max-switch" min="1" max="20" />
347
+ </div>
348
+ <div class="setting-item">
349
+ <label>账户失败阈值(次)</label>
350
+ <input type="number" id="setting-failure-threshold" min="1" max="10" />
351
+ </div>
352
+ <div class="setting-item">
353
+ <label>429 冷却时间(秒)</label>
354
+ <input type="number" id="setting-cooldown" min="60" max="3600" />
355
+ </div>
356
+ <div class="setting-item">
357
+ <label>会话缓存时间(秒)</label>
358
+ <input type="number" id="setting-cache-ttl" min="300" max="86400" />
359
+ </div>
360
+ </div>
361
+ </div>
362
+
363
+ <!-- 公开展示配置 -->
364
+ <div class="card">
365
+ <h3>🎨 公开展示配置</h3>
366
+ <div style="margin-top: 12px;">
367
+ <div class="setting-item">
368
+ <label>Logo URL</label>
369
+ <input type="text" id="setting-logo-url" placeholder="留空则不显示" />
370
+ </div>
371
+ <div class="setting-item">
372
+ <label>开始对话链接</label>
373
+ <input type="text" id="setting-chat-url" placeholder="留空则不显示" />
374
+ </div>
375
+ <div class="setting-item">
376
+ <label>Session 过期时间(小时)</label>
377
+ <input type="number" id="setting-session-hours" min="1" max="168" />
378
+ </div>
379
+ </div>
380
+ </div>
381
+ </div>
382
+ </div>
383
+
384
+ <div style="margin-top: 20px; display: flex; gap: 12px; justify-content: flex-end;">
385
+ <button class="btn btn-secondary" onclick="loadSettings()">重置</button>
386
+ <button class="btn btn-primary" onclick="saveSettings()">保存设置</button>
387
+ </div>
388
+ </div>
389
+ </div>
390
+ </div>
391
+
392
+ <!-- JSON 编辑器模态框 -->
393
+ <div id="jsonModal" class="modal">
394
+ <div class="modal-content">
395
+ <div class="modal-header">
396
+ <div class="modal-title">编辑账户配置</div>
397
+ <button class="modal-close" onclick="closeModal()">&times;</button>
398
+ </div>
399
+ <div class="modal-body">
400
+ <textarea id="jsonEditor" class="json-editor" placeholder="在此编辑 JSON 配置..."></textarea>
401
+ <div id="jsonError" class="json-error"></div>
402
+ <div style="margin-top: 12px; font-size: 12px; color: #6b6b6b;">
403
+ <strong>提示:</strong>编辑完成后点击"保存"按钮。JSON 格式错误时无法保存。<br>
404
+ 配置立即生效。重启后将从环境变量重新加载,建议同步更新 ACCOUNTS_CONFIG。
405
+ </div>
406
+ </div>
407
+ <div class="modal-footer">
408
+ <button class="btn btn-secondary" onclick="closeModal()">取消</button>
409
+ <button class="btn btn-primary" onclick="saveConfig()">保存配置</button>
410
+ </div>
411
+ </div>
412
+ </div>
413
+ {% endblock %}
414
+
415
+ {% block extra_js %}
416
+ <script>
417
+ // Define global variables for admin.js
418
+ window.ADMIN_PATH = '{{ admin_path_segment }}';
419
+ </script>
420
+ <script src="/static/js/admin.js"></script>
421
+ {% endblock %}
templates/admin/logs.html CHANGED
@@ -0,0 +1,613 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>日志查看器</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ html, body { height: 100%; overflow: hidden; }
10
+ body {
11
+ font-family: 'Consolas', 'Monaco', monospace;
12
+ background: #fafaf9;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ padding: 15px;
17
+ }
18
+ .container {
19
+ width: 100%;
20
+ max-width: 1400px;
21
+ height: calc(100vh - 30px);
22
+ background: white;
23
+ border-radius: 16px;
24
+ padding: 30px;
25
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
26
+ display: flex;
27
+ flex-direction: column;
28
+ }
29
+ h1 { color: #1a1a1a; font-size: 22px; font-weight: 600; margin-bottom: 20px; text-align: center; }
30
+ .stats {
31
+ display: grid;
32
+ grid-template-columns: repeat(6, 1fr);
33
+ gap: 12px;
34
+ margin-bottom: 16px;
35
+ }
36
+ .stat {
37
+ background: #fafaf9;
38
+ padding: 12px;
39
+ border: 1px solid #e5e5e5;
40
+ border-radius: 8px;
41
+ text-align: center;
42
+ transition: all 0.15s ease;
43
+ }
44
+ .stat:hover { border-color: #d4d4d4; }
45
+ .stat-label { color: #6b6b6b; font-size: 11px; margin-bottom: 4px; }
46
+ .stat-value { color: #1a1a1a; font-size: 18px; font-weight: 600; }
47
+ .controls {
48
+ display: flex;
49
+ gap: 8px;
50
+ margin-bottom: 16px;
51
+ flex-wrap: wrap;
52
+ }
53
+ .controls input, .controls select, .controls button {
54
+ padding: 6px 10px;
55
+ border: 1px solid #e5e5e5;
56
+ border-radius: 8px;
57
+ font-size: 13px;
58
+ }
59
+ .controls select {
60
+ appearance: none;
61
+ -webkit-appearance: none;
62
+ -moz-appearance: none;
63
+ background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 5L6 8L9 5' stroke='%236b6b6b' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
64
+ background-repeat: no-repeat;
65
+ background-position: right 12px center;
66
+ padding-right: 32px;
67
+ }
68
+ .controls input[type="text"] { flex: 1; min-width: 150px; }
69
+ .controls button {
70
+ background: #1a73e8;
71
+ color: white;
72
+ border: none;
73
+ cursor: pointer;
74
+ font-weight: 500;
75
+ transition: background 0.15s ease;
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 6px;
79
+ }
80
+ .controls button:hover { background: #1557b0; }
81
+ .controls button.danger { background: #dc2626; }
82
+ .controls button.danger:hover { background: #b91c1c; }
83
+ .controls button svg { flex-shrink: 0; }
84
+ .log-container {
85
+ flex: 1;
86
+ background: #fafaf9;
87
+ border: 1px solid #e5e5e5;
88
+ border-radius: 8px;
89
+ padding: 12px;
90
+ overflow-y: auto;
91
+ scrollbar-width: thin;
92
+ scrollbar-color: rgba(0,0,0,0.15) transparent;
93
+ }
94
+ /* Webkit 滚动条样式 - 更窄且不占位 */
95
+ .log-container::-webkit-scrollbar {
96
+ width: 4px;
97
+ }
98
+ .log-container::-webkit-scrollbar-track {
99
+ background: transparent;
100
+ }
101
+ .log-container::-webkit-scrollbar-thumb {
102
+ background: rgba(0,0,0,0.15);
103
+ border-radius: 2px;
104
+ }
105
+ .log-container::-webkit-scrollbar-thumb:hover {
106
+ background: rgba(0,0,0,0.3);
107
+ }
108
+ .log-entry {
109
+ padding: 8px 10px;
110
+ margin-bottom: 4px;
111
+ background: white;
112
+ border-radius: 6px;
113
+ border: 1px solid #e5e5e5;
114
+ font-size: 12px;
115
+ color: #1a1a1a;
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 8px;
119
+ word-break: break-word;
120
+ }
121
+ .log-entry > div:first-child {
122
+ display: flex;
123
+ align-items: center;
124
+ gap: 8px;
125
+ }
126
+ .log-message {
127
+ flex: 1;
128
+ overflow: hidden;
129
+ text-overflow: ellipsis;
130
+ }
131
+ .log-entry:hover { border-color: #d4d4d4; }
132
+ .log-time { color: #6b6b6b; }
133
+ .log-level {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 4px;
137
+ padding: 2px 6px;
138
+ border-radius: 3px;
139
+ font-size: 10px;
140
+ font-weight: 600;
141
+ }
142
+ .log-level::before {
143
+ content: '';
144
+ width: 6px;
145
+ height: 6px;
146
+ border-radius: 50%;
147
+ }
148
+ .log-level.INFO { background: #e3f2fd; color: #1976d2; }
149
+ .log-level.INFO::before { background: #1976d2; }
150
+ .log-level.WARNING { background: #fff3e0; color: #f57c00; }
151
+ .log-level.WARNING::before { background: #f57c00; }
152
+ .log-level.ERROR { background: #ffebee; color: #d32f2f; }
153
+ .log-level.ERROR::before { background: #d32f2f; }
154
+ .log-level.DEBUG { background: #f3e5f5; color: #7b1fa2; }
155
+ .log-level.DEBUG::before { background: #7b1fa2; }
156
+ .log-group {
157
+ margin-bottom: 8px;
158
+ border: 1px solid #e5e5e5;
159
+ border-radius: 8px;
160
+ background: white;
161
+ }
162
+ .log-group-header {
163
+ padding: 10px 12px;
164
+ background: #f9f9f9;
165
+ border-radius: 8px 8px 0 0;
166
+ cursor: pointer;
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 8px;
170
+ transition: background 0.15s ease;
171
+ }
172
+ .log-group-header:hover {
173
+ background: #f0f0f0;
174
+ }
175
+ .log-group-content {
176
+ padding: 8px;
177
+ }
178
+ .log-group .log-entry {
179
+ margin-bottom: 4px;
180
+ }
181
+ .log-group .log-entry:last-child {
182
+ margin-bottom: 0;
183
+ }
184
+ .toggle-icon {
185
+ display: inline-block;
186
+ transition: transform 0.2s ease;
187
+ }
188
+ .toggle-icon.collapsed {
189
+ transform: rotate(-90deg);
190
+ }
191
+ @media (max-width: 768px) {
192
+ body { padding: 0; }
193
+ .container { padding: 15px; height: 100vh; border-radius: 0; max-width: 100%; }
194
+ h1 { font-size: 18px; margin-bottom: 12px; }
195
+ .stats { grid-template-columns: repeat(3, 1fr); gap: 8px; }
196
+ .stat { padding: 8px; }
197
+ .controls { gap: 6px; }
198
+ .controls input, .controls select { min-height: 38px; }
199
+ .controls select { flex: 0 0 auto; }
200
+ .controls input[type="text"] { flex: 1 1 auto; min-width: 80px; }
201
+ .controls input[type="number"] { flex: 0 0 60px; }
202
+ .controls button { padding: 10px 8px; font-size: 12px; flex: 1 1 22%; justify-content: center; min-height: 38px; }
203
+ .log-entry {
204
+ font-size: 12px;
205
+ padding: 10px;
206
+ gap: 8px;
207
+ flex-direction: column;
208
+ align-items: flex-start;
209
+ }
210
+ .log-entry > div:first-child {
211
+ display: flex;
212
+ align-items: center;
213
+ gap: 6px;
214
+ width: 100%;
215
+ flex-wrap: wrap;
216
+ }
217
+ .log-time { font-size: 11px; color: #9e9e9e; }
218
+ .log-level { font-size: 10px; }
219
+ .log-message {
220
+ width: 100%;
221
+ white-space: normal;
222
+ word-break: break-word;
223
+ line-height: 1.5;
224
+ margin-top: 4px;
225
+ }
226
+ }
227
+ </style>
228
+ </head>
229
+ <body>
230
+ <div class="container">
231
+ <h1>Gemini API 日志查看器</h1>
232
+ <div class="stats">
233
+ <div class="stat">
234
+ <div class="stat-label">总数</div>
235
+ <div class="stat-value" id="total-count">-</div>
236
+ </div>
237
+ <div class="stat">
238
+ <div class="stat-label">对话</div>
239
+ <div class="stat-value" id="chat-count">-</div>
240
+ </div>
241
+ <div class="stat">
242
+ <div class="stat-label">INFO</div>
243
+ <div class="stat-value" id="info-count">-</div>
244
+ </div>
245
+ <div class="stat">
246
+ <div class="stat-label">WARNING</div>
247
+ <div class="stat-value" id="warning-count">-</div>
248
+ </div>
249
+ <div class="stat">
250
+ <div class="stat-label">ERROR</div>
251
+ <div class="stat-value" id="error-count">-</div>
252
+ </div>
253
+ <div class="stat">
254
+ <div class="stat-label">更新</div>
255
+ <div class="stat-value" id="last-update" style="font-size: 11px;">-</div>
256
+ </div>
257
+ </div>
258
+ <div class="controls">
259
+ <select id="level-filter">
260
+ <option value="">全部</option>
261
+ <option value="INFO">INFO</option>
262
+ <option value="WARNING">WARNING</option>
263
+ <option value="ERROR">ERROR</option>
264
+ </select>
265
+ <input type="text" id="search-input" placeholder="搜索...">
266
+ <input type="number" id="limit-input" value="1500" min="10" max="3000" step="100" style="width: 80px;">
267
+ <button onclick="loadLogs()">
268
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
269
+ <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
270
+ </svg>
271
+ 查询
272
+ </button>
273
+ <button onclick="exportJSON()">
274
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
275
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
276
+ </svg>
277
+ 导出
278
+ </button>
279
+ <button id="auto-refresh-btn" onclick="toggleAutoRefresh()">
280
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
281
+ <polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
282
+ </svg>
283
+ 自动刷新
284
+ </button>
285
+ <button onclick="clearAllLogs()" class="danger">
286
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
287
+ <polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
288
+ </svg>
289
+ 清空
290
+ </button>
291
+ </div>
292
+ <div class="log-container" id="log-container">
293
+ <div style="color: #6b6b6b;">正在加载...</div>
294
+ </div>
295
+ </div>
296
+ <script>
297
+ let autoRefreshTimer = null;
298
+ async function loadLogs() {
299
+ const level = document.getElementById('level-filter').value;
300
+ const search = document.getElementById('search-input').value;
301
+ const limit = document.getElementById('limit-input').value;
302
+ // 从当前 URL 获取 key 参数
303
+ const urlParams = new URLSearchParams(window.location.search);
304
+ const key = urlParams.get('key');
305
+ // 构建 API URL(直接使用 /admin/log)
306
+ let url = `/admin/log?limit=${limit}`;
307
+ if (key) url += `&key=${key}`;
308
+ if (level) url += `&level=${level}`;
309
+ if (search) url += `&search=${encodeURIComponent(search)}`;
310
+ try {
311
+ const response = await fetch(url);
312
+ if (!response.ok) {
313
+ throw new Error(`HTTP ${response.status}`);
314
+ }
315
+ const data = await response.json();
316
+ if (data && data.logs) {
317
+ displayLogs(data.logs);
318
+ updateStats(data.stats);
319
+ document.getElementById('last-update').textContent = new Date().toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'});
320
+ } else {
321
+ throw new Error('Invalid data format');
322
+ }
323
+ } catch (error) {
324
+ document.getElementById('log-container').innerHTML = '<div class="log-entry ERROR">加载失败: ' + error.message + '</div>';
325
+ }
326
+ }
327
+ function updateStats(stats) {
328
+ document.getElementById('total-count').textContent = stats.memory.total;
329
+ document.getElementById('info-count').textContent = stats.memory.by_level.INFO || 0;
330
+ document.getElementById('warning-count').textContent = stats.memory.by_level.WARNING || 0;
331
+ const errorCount = document.getElementById('error-count');
332
+ errorCount.textContent = stats.memory.by_level.ERROR || 0;
333
+ if (stats.errors && stats.errors.count > 0) errorCount.style.color = '#dc2626';
334
+ document.getElementById('chat-count').textContent = stats.chat_count || 0;
335
+ }
336
+ // 分类颜色配置(提取到外部避免重复定义)
337
+ const CATEGORY_COLORS = {
338
+ 'SYSTEM': '#9e9e9e',
339
+ 'CONFIG': '#607d8b',
340
+ 'LOG': '#9e9e9e',
341
+ 'AUTH': '#4caf50',
342
+ 'SESSION': '#00bcd4',
343
+ 'FILE': '#ff9800',
344
+ 'CHAT': '#2196f3',
345
+ 'API': '#8bc34a',
346
+ 'CACHE': '#9c27b0',
347
+ 'ACCOUNT': '#f44336',
348
+ 'MULTI': '#673ab7'
349
+ };
350
+
351
+ // 账户颜色配置(提取到外部避免重复定义)
352
+ const ACCOUNT_COLORS = {
353
+ 'account_1': '#9c27b0',
354
+ 'account_2': '#e91e63',
355
+ 'account_3': '#00bcd4',
356
+ 'account_4': '#4caf50',
357
+ 'account_5': '#ff9800'
358
+ };
359
+
360
+ function getCategoryColor(category) {
361
+ return CATEGORY_COLORS[category] || '#757575';
362
+ }
363
+
364
+ function getAccountColor(accountId) {
365
+ return ACCOUNT_COLORS[accountId] || '#757575';
366
+ }
367
+
368
+ function displayLogs(logs) {
369
+ const container = document.getElementById('log-container');
370
+ if (logs.length === 0) {
371
+ container.innerHTML = '<div class="log-entry">暂无日志</div>';
372
+ return;
373
+ }
374
+
375
+ // 按请求ID分组
376
+ const groups = {};
377
+ const ungrouped = [];
378
+
379
+ logs.forEach(log => {
380
+ const msg = escapeHtml(log.message);
381
+ const reqMatch = msg.match(/\[req_([a-z0-9]+)\]/);
382
+
383
+ if (reqMatch) {
384
+ const reqId = reqMatch[1];
385
+ if (!groups[reqId]) {
386
+ groups[reqId] = [];
387
+ }
388
+ groups[reqId].push(log);
389
+ } else {
390
+ ungrouped.push(log);
391
+ }
392
+ });
393
+
394
+ // 渲染分组
395
+ let html = '';
396
+
397
+ // 先渲染未分组的日志
398
+ ungrouped.forEach(log => {
399
+ html += renderLogEntry(log);
400
+ });
401
+
402
+ // 读取折叠状态
403
+ const foldState = JSON.parse(localStorage.getItem('log-fold-state') || '{}');
404
+
405
+ // 按请求ID分组渲染(最新的组在下面)
406
+ Object.keys(groups).forEach(reqId => {
407
+ const groupLogs = groups[reqId];
408
+ const firstLog = groupLogs[0];
409
+ const lastLog = groupLogs[groupLogs.length - 1];
410
+
411
+ // 判断状态
412
+ let status = 'in_progress';
413
+ let statusColor = '#ff9800';
414
+ let statusText = '进行中';
415
+
416
+ if (lastLog.message.includes('响应完成') || lastLog.message.includes('非流式响应完成')) {
417
+ status = 'success';
418
+ statusColor = '#4caf50';
419
+ statusText = '成功';
420
+ } else if (lastLog.level === 'ERROR' || lastLog.message.includes('失败')) {
421
+ status = 'error';
422
+ statusColor = '#f44336';
423
+ statusText = '失败';
424
+ } else {
425
+ // 检查超时(最后日志超过 5 分钟)
426
+ const lastLogTime = new Date(lastLog.time);
427
+ const now = new Date();
428
+ const diffMinutes = (now - lastLogTime) / 1000 / 60;
429
+ if (diffMinutes > 5) {
430
+ status = 'timeout';
431
+ statusColor = '#ffc107';
432
+ statusText = '超时';
433
+ }
434
+ }
435
+
436
+ // 提取账户ID和模型
437
+ const accountMatch = firstLog.message.match(/\[account_(\d+)\]/);
438
+ const modelMatch = firstLog.message.match(/收到请求: ([^ ]+)/);
439
+ const accountId = accountMatch ? `account_${accountMatch[1]}` : '';
440
+ const model = modelMatch ? modelMatch[1] : '';
441
+
442
+ // 检查折叠状态
443
+ const isCollapsed = foldState[reqId] === true;
444
+ const contentStyle = isCollapsed ? 'style="display: none;"' : '';
445
+ const iconClass = isCollapsed ? 'class="toggle-icon collapsed"' : 'class="toggle-icon"';
446
+
447
+ html += `
448
+ <div class="log-group" data-req-id="${reqId}">
449
+ <div class="log-group-header" onclick="toggleGroup('${reqId}')">
450
+ <span style="color: ${statusColor}; font-weight: 600; font-size: 11px;">⬤ ${statusText}</span>
451
+ <span style="color: #666; font-size: 11px; margin-left: 8px;">req_${reqId}</span>
452
+ ${accountId ? `<span style="color: ${getAccountColor(accountId)}; font-size: 11px; margin-left: 8px;">${accountId}</span>` : ''}
453
+ ${model ? `<span style="color: #999; font-size: 11px; margin-left: 8px;">${model}</span>` : ''}
454
+ <span style="color: #999; font-size: 11px; margin-left: 8px;">${groupLogs.length}条日志</span>
455
+ <span ${iconClass} style="margin-left: auto; color: #999;">▼</span>
456
+ </div>
457
+ <div class="log-group-content" ${contentStyle}>
458
+ ${groupLogs.map(log => renderLogEntry(log)).join('')}
459
+ </div>
460
+ </div>
461
+ `;
462
+ });
463
+
464
+ container.innerHTML = html;
465
+
466
+ // 自动滚动到底部,显示最新日志
467
+ container.scrollTop = container.scrollHeight;
468
+ }
469
+
470
+ function renderLogEntry(log) {
471
+ const msg = escapeHtml(log.message);
472
+ let displayMsg = msg;
473
+ let categoryTags = [];
474
+ let accountId = null;
475
+
476
+ // 解析所有标签:[CATEGORY1] [CATEGORY2] [account_X] [req_X] message
477
+ let remainingMsg = msg;
478
+ const tagRegex = /^\[([A-Z_a-z0-9]+)\]/;
479
+
480
+ while (true) {
481
+ const match = remainingMsg.match(tagRegex);
482
+ if (!match) break;
483
+
484
+ const tag = match[1];
485
+ remainingMsg = remainingMsg.substring(match[0].length).trim();
486
+
487
+ // 跳过req_标签(已在组头部显示)
488
+ if (tag.startsWith('req_')) {
489
+ continue;
490
+ }
491
+ // 判断是否为账户ID
492
+ else if (tag.startsWith('account_')) {
493
+ accountId = tag;
494
+ } else {
495
+ // 普通分类标签
496
+ categoryTags.push(tag);
497
+ }
498
+ }
499
+
500
+ displayMsg = remainingMsg;
501
+
502
+ // 生成分类标签HTML
503
+ const categoryTagsHtml = categoryTags.map(cat =>
504
+ `<span class="log-category" style="background: ${getCategoryColor(cat)}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 2px;">${cat}</span>`
505
+ ).join('');
506
+
507
+ // 生成账户标签HTML
508
+ const accountTagHtml = accountId
509
+ ? `<span style="color: ${getAccountColor(accountId)}; font-size: 11px; font-weight: 600; margin-left: 2px;">${accountId}</span>`
510
+ : '';
511
+
512
+ return `
513
+ <div class="log-entry ${log.level}">
514
+ <div>
515
+ <span class="log-time">${log.time}</span>
516
+ <span class="log-level ${log.level}">${log.level}</span>
517
+ ${categoryTagsHtml}
518
+ ${accountTagHtml}
519
+ </div>
520
+ <div class="log-message">${displayMsg}</div>
521
+ </div>
522
+ `;
523
+ }
524
+
525
+ function toggleGroup(reqId) {
526
+ const group = document.querySelector(`.log-group[data-req-id="${reqId}"]`);
527
+ const content = group.querySelector('.log-group-content');
528
+ const icon = group.querySelector('.toggle-icon');
529
+
530
+ const isCollapsed = content.style.display === 'none';
531
+ if (isCollapsed) {
532
+ content.style.display = 'block';
533
+ icon.classList.remove('collapsed');
534
+ } else {
535
+ content.style.display = 'none';
536
+ icon.classList.add('collapsed');
537
+ }
538
+
539
+ // 保存折叠状态到 localStorage
540
+ const foldState = JSON.parse(localStorage.getItem('log-fold-state') || '{}');
541
+ foldState[reqId] = !isCollapsed;
542
+ localStorage.setItem('log-fold-state', JSON.stringify(foldState));
543
+ }
544
+ function escapeHtml(text) {
545
+ const div = document.createElement('div');
546
+ div.textContent = text;
547
+ return div.innerHTML;
548
+ }
549
+ async function exportJSON() {
550
+ try {
551
+ const urlParams = new URLSearchParams(window.location.search);
552
+ const key = urlParams.get('key');
553
+ let url = `/admin/log?limit=3000`;
554
+ if (key) url += `&key=${key}`;
555
+ const response = await fetch(url);
556
+ const data = await response.json();
557
+ const blob = new Blob([JSON.stringify({exported_at: new Date().toISOString(), logs: data.logs}, null, 2)], {type: 'application/json'});
558
+ const blobUrl = URL.createObjectURL(blob);
559
+ const a = document.createElement('a');
560
+ a.href = blobUrl;
561
+ a.download = 'logs_' + new Date().toISOString().slice(0, 19).replace(/:/g, '-') + '.json';
562
+ a.click();
563
+ URL.revokeObjectURL(blobUrl);
564
+ alert('导出成功');
565
+ } catch (error) {
566
+ alert('导出失败: ' + error.message);
567
+ }
568
+ }
569
+ async function clearAllLogs() {
570
+ if (!confirm('确定清空所有日志?')) return;
571
+ try {
572
+ const urlParams = new URLSearchParams(window.location.search);
573
+ const key = urlParams.get('key');
574
+ let url = `/admin/log?confirm=yes`;
575
+ if (key) url += `&key=${key}`;
576
+ const response = await fetch(url, {method: 'DELETE'});
577
+ if (response.ok) {
578
+ alert('已清空');
579
+ loadLogs();
580
+ } else {
581
+ alert('清空失败');
582
+ }
583
+ } catch (error) {
584
+ alert('清空失败: ' + error.message);
585
+ }
586
+ }
587
+ let autoRefreshEnabled = true;
588
+ function toggleAutoRefresh() {
589
+ autoRefreshEnabled = !autoRefreshEnabled;
590
+ const btn = document.getElementById('auto-refresh-btn');
591
+ if (autoRefreshEnabled) {
592
+ btn.style.background = '#1a73e8';
593
+ autoRefreshTimer = setInterval(loadLogs, 5000);
594
+ } else {
595
+ btn.style.background = '#6b6b6b';
596
+ if (autoRefreshTimer) {
597
+ clearInterval(autoRefreshTimer);
598
+ autoRefreshTimer = null;
599
+ }
600
+ }
601
+ }
602
+ document.addEventListener('DOMContentLoaded', () => {
603
+ loadLogs();
604
+ autoRefreshTimer = setInterval(loadLogs, 5000);
605
+ document.getElementById('search-input').addEventListener('keypress', (e) => {
606
+ if (e.key === 'Enter') loadLogs();
607
+ });
608
+ document.getElementById('level-filter').addEventListener('change', loadLogs);
609
+ document.getElementById('limit-input').addEventListener('change', loadLogs);
610
+ });
611
+ </script>
612
+ </body>
613
+ </html>