xiaoyukkkk commited on
Commit
fa8692e
·
verified ·
1 Parent(s): d72552e

Delete templates

Browse files
templates/admin/index.html DELETED
@@ -1,438 +0,0 @@
1
- {% extends "base.html" %}
2
- {% from "components/alerts.html" import api_key_status, error_alert, no_accounts_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, admin_path_segment) }}
40
- {% if multi_account_mgr.accounts|length == 0 %}
41
- {{ no_accounts_alert() }}
42
- {% endif %}
43
-
44
- <div class="alert alert-primary">
45
- <div class="alert-icon">🔗</div>
46
- <div class="alert-content">
47
- <strong>API 接口</strong>
48
- <div style="margin-top: 6px; color: #86868b; font-size: 11px;">根据客户端选择对应接口</div>
49
- <div style="margin-top: 10px; display: grid; gap: 10px;">
50
- <div class="api-item">
51
- <span class="api-item-label">基础端点</span>
52
- <div class="api-item-content" style="display: flex; gap: 8px; flex: 1;">
53
- <code class="api-item-code">{{ api_base_url }}</code>
54
- <button class="btn-copy" onclick="navigator.clipboard.writeText('{{ api_base_url }}').then(() => { this.innerHTML = '✅'; setTimeout(() => this.innerHTML = '📋', 2000); })">📋</button>
55
- </div>
56
- </div>
57
- <div class="api-item">
58
- <span class="api-item-label">SDK 接口</span>
59
- <div class="api-item-content" style="display: flex; gap: 8px; flex: 1;">
60
- <code class="api-item-code">{{ api_base_v1 }}</code>
61
- <button class="btn-copy" onclick="navigator.clipboard.writeText('{{ api_base_v1 }}').then(() => { this.innerHTML = '✅'; setTimeout(() => this.innerHTML = '📋', 2000); })">📋</button>
62
- </div>
63
- </div>
64
- <div class="api-item">
65
- <span class="api-item-label">完整接口</span>
66
- <div class="api-item-content" style="display: flex; gap: 8px; flex: 1;">
67
- <code class="api-item-code">{{ api_endpoint }}</code>
68
- <button class="btn-copy" onclick="navigator.clipboard.writeText('{{ api_endpoint }}').then(() => { this.innerHTML = '✅'; setTimeout(() => this.innerHTML = '📋', 2000); })">📋</button>
69
- </div>
70
- </div>
71
- <div class="api-item">
72
- <span class="api-item-label">API 密钥</span>
73
- <div class="api-item-content" style="display: flex; gap: 8px; flex: 1;">
74
- <code class="api-item-code">{% if main.API_KEY %}{{ main.API_KEY }}{% else %}<span style="color: #ff9500;">未设置</span>{% endif %}</code>
75
- {% if main.API_KEY %}
76
- <button class="btn-copy" onclick="navigator.clipboard.writeText('{{ main.API_KEY }}').then(() => { this.innerHTML = '✅'; setTimeout(() => this.innerHTML = '📋', 2000); })">📋</button>
77
- {% endif %}
78
- </div>
79
- </div>
80
- </div>
81
- <div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(0,0,0,0.06);">
82
- <div style="font-size: 11px; color: #6b6b6b; margin-bottom: 6px;">支持的模型</div>
83
- <div style="display: flex; flex-wrap: wrap; gap: 6px;">
84
- <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>
85
- <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>
86
- <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>
87
- <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>
88
- <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>
89
- </div>
90
- <div style="margin-top: 8px; font-size: 11px; color: #86868b;">
91
- 📸 图片生成可在"系统设置"自定义配置
92
- </div>
93
- </div>
94
- </div>
95
- </div>
96
-
97
- <div class="section">
98
- <div class="section-title">账户状态 ({{ multi_account_mgr.accounts|length }} 个)</div>
99
- <div style="color: #6b6b6b; font-size: 12px; margin-bottom: 12px; padding-left: 4px;">
100
- 默认过期时间12小时,注意北京时间 • 批量上传使用 <code style="font-size: 11px; background: rgba(0,0,0,0.05); padding: 2px 6px; border-radius: 4px;">script/download-config.js</code> 油猴脚本
101
- </div>
102
- {{ account_table(accounts_data) }}
103
- </div>
104
- </div>
105
-
106
- <!-- Tab 2: API文档 -->
107
- <div id="tab-api" class="tab-content">
108
- <div class="section">
109
- <div class="section-title">API 端点列表</div>
110
-
111
- <div class="current-url-row">
112
- <span style="font-size:12px; font-weight:600; color:#0071e3; margin-right:8px;">当前页面:</span>
113
- <code style="background:none; padding:0; color:#1d1d1f;">{{ current_url }}</code>
114
- </div>
115
-
116
- <table class="ep-table">
117
- <tr>
118
- <td width="70"><span class="method m-post">POST</span></td>
119
- <td><span class="ep-path">/{{ api_path_segment }}v1/chat/completions</span></td>
120
- <td><span class="ep-desc">OpenAI 兼容对话接口</span></td>
121
- </tr>
122
- <tr>
123
- <td><span class="method m-get">GET</span></td>
124
- <td><span class="ep-path">/{{ api_path_segment }}v1/models</span></td>
125
- <td><span class="ep-desc">获取模型列表</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 }}</span></td>
130
- <td><span class="ep-desc">管理首页 (需登录)</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 }}/health</span></td>
135
- <td><span class="ep-desc">健康检查 (需登录)</span></td>
136
- </tr>
137
- <tr>
138
- <td><span class="method m-get">GET</span></td>
139
- <td><span class="ep-path">/{{ admin_path_segment }}/accounts</span></td>
140
- <td><span class="ep-desc">账户状态 JSON (需登录)</span></td>
141
- </tr>
142
- <tr>
143
- <td><span class="method m-get">GET</span></td>
144
- <td><span class="ep-path">/{{ admin_path_segment }}/log</span></td>
145
- <td><span class="ep-desc">获取日志 JSON (需登录)</span></td>
146
- </tr>
147
- <tr>
148
- <td><span class="method m-get">GET</span></td>
149
- <td><span class="ep-path">/{{ admin_path_segment }}/log/html</span></td>
150
- <td><span class="ep-desc">日志查看器 HTML (需登录)</span></td>
151
- </tr>
152
- <tr>
153
- <td><span class="method m-del">DEL</span></td>
154
- <td><span class="ep-path">/{{ admin_path_segment }}/log?confirm=yes</span></td>
155
- <td><span class="ep-desc">清空系统日志 (需登录)</span></td>
156
- </tr>
157
- <tr>
158
- <td><span class="method m-get">GET</span></td>
159
- <td><span class="ep-path">/public/stats</span></td>
160
- <td><span class="ep-desc">公开统计数据</span></td>
161
- </tr>
162
- <tr>
163
- <td><span class="method m-get">GET</span></td>
164
- <td><span class="ep-path">/public/log</span></td>
165
- <td><span class="ep-desc">公开日志 (JSON, 脱敏)</span></td>
166
- </tr>
167
- <tr>
168
- <td><span class="method m-get">GET</span></td>
169
- <td><span class="ep-path">/public/log/html</span></td>
170
- <td><span class="ep-desc">公开日志查看器 (HTML)</span></td>
171
- </tr>
172
- <tr>
173
- <td><span class="method m-get">GET</span></td>
174
- <td><span class="ep-path">/public/uptime</span></td>
175
- <td><span class="ep-desc">实时状态监控 (JSON)</span></td>
176
- </tr>
177
- <tr>
178
- <td><span class="method m-get">GET</span></td>
179
- <td><span class="ep-path">/public/uptime/html</span></td>
180
- <td><span class="ep-desc">实时状态监控页面 (HTML)</span></td>
181
- </tr>
182
- <tr>
183
- <td><span class="method m-get">GET</span></td>
184
- <td><span class="ep-path">/docs</span></td>
185
- <td><span class="ep-desc">Swagger API 文档</span></td>
186
- </tr>
187
- <tr>
188
- <td><span class="method m-get">GET</span></td>
189
- <td><span class="ep-path">/redoc</span></td>
190
- <td><span class="ep-desc">ReDoc API 文档</span></td>
191
- </tr>
192
- </table>
193
- </div>
194
- </div>
195
-
196
- <!-- Tab 3: 系统配置 -->
197
- <div id="tab-config" class="tab-content">
198
- <div class="section">
199
- <div class="section-title">当前配置状态</div>
200
- <div class="grid-env">
201
- <div class="stack-col">
202
- <div class="card">
203
- <h3>环境变量 <span class="badge badge-required">ENV</span></h3>
204
- <div style="margin-top: 12px;">
205
- <div class="env-var">
206
- <div><div class="env-name">ADMIN_KEY</div><div class="env-desc">管理员密钥</div></div>
207
- <div class="env-value">已设置</div>
208
- </div>
209
- <div class="env-var">
210
- <div><div class="env-name">PATH_PREFIX</div><div class="env-desc">API路径前缀</div></div>
211
- <div class="env-value">{{ main.PATH_PREFIX or '未设置' }}</div>
212
- </div>
213
- </div>
214
- </div>
215
-
216
- <div class="card">
217
- <h3>基础配置 <span class="badge badge-optional">YAML</span></h3>
218
- <div style="margin-top: 12px;">
219
- <div class="env-var">
220
- <div><div class="env-name">API_KEY</div><div class="env-desc">API访问密钥</div></div>
221
- <div class="env-value">{% if main.API_KEY %}已设置{% else %}未设置(公开访问){% endif %}</div>
222
- </div>
223
- <div class="env-var">
224
- <div><div class="env-name">BASE_URL</div><div class="env-desc">服务器URL</div></div>
225
- <div class="env-value">{% if main.BASE_URL %}已设置{% else %}自动检测{% endif %}</div>
226
- </div>
227
- <div class="env-var">
228
- <div><div class="env-name">PROXY</div><div class="env-desc">代理地址</div></div>
229
- <div class="env-value">{% if main.PROXY %}已设置{% else %}未设置{% endif %}</div>
230
- </div>
231
- </div>
232
- </div>
233
- </div>
234
-
235
- <div class="card">
236
- <h3>重试策略 <span class="badge badge-optional">YAML</span></h3>
237
- <div style="margin-top: 12px;">
238
- <div class="env-var">
239
- <div><div class="env-name">max_new_session_tries</div><div class="env-desc">新会话尝试数</div></div>
240
- <div class="env-value">{{ main.MAX_NEW_SESSION_TRIES }}</div>
241
- </div>
242
- <div class="env-var">
243
- <div><div class="env-name">max_request_retries</div><div class="env-desc">请求重试次数</div></div>
244
- <div class="env-value">{{ main.MAX_REQUEST_RETRIES }}</div>
245
- </div>
246
- <div class="env-var">
247
- <div><div class="env-name">max_account_switch_tries</div><div class="env-desc">账户切换次数</div></div>
248
- <div class="env-value">{{ main.MAX_ACCOUNT_SWITCH_TRIES }}</div>
249
- </div>
250
- <div class="env-var">
251
- <div><div class="env-name">account_failure_threshold</div><div class="env-desc">失败阈值</div></div>
252
- <div class="env-value">{{ main.ACCOUNT_FAILURE_THRESHOLD }} 次</div>
253
- </div>
254
- <div class="env-var">
255
- <div><div class="env-name">rate_limit_cooldown_seconds</div><div class="env-desc">429冷却时间</div></div>
256
- <div class="env-value">{{ main.RATE_LIMIT_COOLDOWN_SECONDS }} 秒</div>
257
- </div>
258
- <div class="env-var">
259
- <div><div class="env-name">session_cache_ttl_seconds</div><div class="env-desc">会话缓存时间</div></div>
260
- <div class="env-value">{{ main.SESSION_CACHE_TTL_SECONDS }} 秒</div>
261
- </div>
262
- </div>
263
- </div>
264
-
265
- <div class="card">
266
- <h3>公开展示 <span class="badge badge-optional">YAML</span></h3>
267
- <div style="margin-top: 12px;">
268
- <div class="env-var">
269
- <div><div class="env-name">LOGO_URL</div><div class="env-desc">Logo图片</div></div>
270
- <div class="env-value">{% if main.LOGO_URL %}已设置{% else %}未设置{% endif %}</div>
271
- </div>
272
- <div class="env-var">
273
- <div><div class="env-name">CHAT_URL</div><div class="env-desc">对话链接</div></div>
274
- <div class="env-value">{% if main.CHAT_URL %}已设置{% else %}未设置{% endif %}</div>
275
- </div>
276
- </div>
277
- </div>
278
- </div>
279
- </div>
280
- </div>
281
-
282
- <!-- Tab 4: 系统设置 -->
283
- <div id="tab-settings" class="tab-content">
284
- <div class="section">
285
- <div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 16px; gap: 16px; flex-wrap: wrap;">
286
- <div style="flex: 1; min-width: 200px;">
287
- <div class="section-title" style="margin-bottom: 4px;">系统设置</div>
288
- <div style="color: #6b6b6b; font-size: 11px; padding-left: 4px;">
289
- ✅ 配置修改后立即生效,无需重启 • 📋 优先级:YAML > 环境变量 > 默认值
290
- </div>
291
- </div>
292
- <div style="display: flex; gap: 10px; flex-shrink: 0;">
293
- <button class="btn" onclick="loadSettings()">重置</button>
294
- <button class="btn" onclick="saveSettings()">保存设置</button>
295
- </div>
296
- </div>
297
-
298
- <div class="grid-env">
299
- <div class="stack-col">
300
- <!-- 基础配置 -->
301
- <div class="card">
302
- <h3>基础配置</h3>
303
- <div style="margin-top: 12px;">
304
- <div class="setting-item">
305
- <label>API 访问密钥</label>
306
- <input type="text" id="setting-api-key" placeholder="留空则公开访问" />
307
- </div>
308
- <div class="setting-item">
309
- <label>服务器 URL</label>
310
- <input type="text" id="setting-base-url" placeholder="留空则自动检测" />
311
- </div>
312
- <div class="setting-item">
313
- <label>代理地址</label>
314
- <input type="text" id="setting-proxy" placeholder="如 http://127.0.0.1:7890" />
315
- </div>
316
- </div>
317
- </div>
318
-
319
- <!-- 图片生成配置 -->
320
- <div class="card">
321
- <h3>📸 图片生成配置</h3>
322
- <div style="margin-top: 12px;">
323
- <div class="setting-item">
324
- <label style="display: flex; align-items: center; gap: 8px;">
325
- <input type="checkbox" id="setting-image-enabled" style="width: auto;" />
326
- 启用图片生成
327
- </label>
328
- </div>
329
- <div class="setting-item">
330
- <label>支持的模型</label>
331
- <div id="setting-image-models" style="display: flex; flex-direction: column; gap: 6px; margin-top: 6px;">
332
- <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; font-size: 12px;">
333
- <input type="checkbox" value="gemini-3-pro-preview" style="width: auto;" /> gemini-3-pro-preview
334
- </label>
335
- <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; font-size: 12px;">
336
- <input type="checkbox" value="gemini-2.5-pro" style="width: auto;" /> gemini-2.5-pro
337
- </label>
338
- <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; font-size: 12px;">
339
- <input type="checkbox" value="gemini-2.5-flash" style="width: auto;" /> gemini-2.5-flash
340
- </label>
341
- <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; font-size: 12px;">
342
- <input type="checkbox" value="gemini-3-flash-preview" style="width: auto;" /> gemini-3-flash-preview
343
- </label>
344
- <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; font-size: 12px;">
345
- <input type="checkbox" value="gemini-auto" style="width: auto;" /> gemini-auto
346
- </label>
347
- </div>
348
- </div>
349
- </div>
350
- </div>
351
- </div>
352
-
353
- <div class="stack-col">
354
- <!-- 重试策略配置 -->
355
- <div class="card">
356
- <h3>🔄 重试策略配置</h3>
357
- <div style="margin-top: 12px;">
358
- <div class="setting-item">
359
- <label>新会话尝试账户数</label>
360
- <input type="number" id="setting-max-new-session" min="1" max="20" />
361
- </div>
362
- <div class="setting-item">
363
- <label>请求失败重试次数</label>
364
- <input type="number" id="setting-max-retries" min="1" max="10" />
365
- </div>
366
- <div class="setting-item">
367
- <label>账户切换尝试次数</label>
368
- <input type="number" id="setting-max-switch" min="1" max="20" />
369
- </div>
370
- <div class="setting-item">
371
- <label>账户失败阈值(次)</label>
372
- <input type="number" id="setting-failure-threshold" min="1" max="10" />
373
- </div>
374
- <div class="setting-item">
375
- <label>429 冷却时间(秒)</label>
376
- <input type="number" id="setting-cooldown" min="60" max="3600" />
377
- </div>
378
- <div class="setting-item">
379
- <label>会话缓存时间(秒)</label>
380
- <input type="number" id="setting-cache-ttl" min="300" max="86400" />
381
- </div>
382
- </div>
383
- </div>
384
-
385
- <!-- 公开展示配置 -->
386
- <div class="card">
387
- <h3>🎨 公开展示配置</h3>
388
- <div style="margin-top: 12px;">
389
- <div class="setting-item">
390
- <label>Logo URL</label>
391
- <input type="text" id="setting-logo-url" placeholder="留空则不显示" />
392
- </div>
393
- <div class="setting-item">
394
- <label>开始对话链接</label>
395
- <input type="text" id="setting-chat-url" placeholder="留空则不显示" />
396
- </div>
397
- <div class="setting-item">
398
- <label>Session 过期时间(小时)</label>
399
- <input type="number" id="setting-session-hours" min="1" max="168" />
400
- </div>
401
- </div>
402
- </div>
403
- </div>
404
- </div>
405
- </div>
406
- </div>
407
- </div>
408
-
409
- <!-- JSON 编辑器模态框 -->
410
- <div id="jsonModal" class="modal">
411
- <div class="modal-content">
412
- <div class="modal-header">
413
- <div class="modal-title">编辑账户配置</div>
414
- <button class="modal-close" onclick="closeModal()">&times;</button>
415
- </div>
416
- <div class="modal-body">
417
- <textarea id="jsonEditor" class="json-editor" placeholder="在此编辑 JSON 配置..."></textarea>
418
- <div id="jsonError" class="json-error"></div>
419
- <div style="margin-top: 12px; font-size: 12px; color: #6b6b6b;">
420
- <strong>提示:</strong>编辑完成后点击"保存"按钮。JSON 格式错误时无法保存。<br>
421
- 配置立即生效。重启后将从环境变量重新加载,建议同步更新 ACCOUNTS_CONFIG。
422
- </div>
423
- </div>
424
- <div class="modal-footer">
425
- <button class="btn btn-secondary" onclick="closeModal()">取消</button>
426
- <button class="btn btn-primary" onclick="saveConfig()">保存配置</button>
427
- </div>
428
- </div>
429
- </div>
430
- {% endblock %}
431
-
432
- {% block extra_js %}
433
- <script>
434
- // Define global variables for admin.js
435
- window.ADMIN_PATH = '{{ admin_path_segment }}';
436
- </script>
437
- <script src="/static/js/admin.js"></script>
438
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/admin/logs.html DELETED
@@ -1,613 +0,0 @@
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>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/auth/login.html DELETED
@@ -1,201 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>登录</title>
7
- <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
-
14
- body {
15
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
16
- background: #fafaf9;
17
- min-height: 100vh;
18
- display: flex;
19
- align-items: center;
20
- justify-content: center;
21
- padding: 20px;
22
- }
23
-
24
- .container {
25
- background: #fff;
26
- border: 1px solid #e5e5e5;
27
- border-radius: 12px;
28
- width: 100%;
29
- max-width: 380px;
30
- padding: 40px 32px;
31
- }
32
-
33
- .header {
34
- text-align: center;
35
- margin-bottom: 32px;
36
- }
37
-
38
- h1 {
39
- font-size: 22px;
40
- font-weight: 600;
41
- color: #1d1d1f;
42
- margin-bottom: 6px;
43
- }
44
-
45
- .subtitle {
46
- font-size: 14px;
47
- color: #86868b;
48
- }
49
-
50
- .error-box {
51
- padding: 12px 14px;
52
- background: #fff5f5;
53
- border: 1px solid #fecaca;
54
- border-radius: 8px;
55
- color: #dc2626;
56
- font-size: 14px;
57
- margin-bottom: 20px;
58
- }
59
-
60
- label {
61
- display: block;
62
- font-size: 14px;
63
- font-weight: 500;
64
- color: #1d1d1f;
65
- margin-bottom: 8px;
66
- }
67
-
68
- input[type="password"] {
69
- width: 100%;
70
- padding: 12px 14px;
71
- border: 1px solid #d4d4d4;
72
- border-radius: 8px;
73
- font-size: 15px;
74
- color: #1d1d1f;
75
- background: #fff;
76
- transition: border-color 0.15s;
77
- outline: none;
78
- margin-bottom: 20px;
79
- }
80
-
81
- input[type="password"]:focus {
82
- border-color: #0071e3;
83
- }
84
-
85
- input[type="password"]::placeholder {
86
- color: #c7c7cc;
87
- }
88
-
89
- button {
90
- width: 100%;
91
- padding: 12px;
92
- background: #0071e3;
93
- color: #fff;
94
- border: none;
95
- border-radius: 8px;
96
- font-size: 15px;
97
- font-weight: 500;
98
- cursor: pointer;
99
- transition: background 0.15s;
100
- }
101
-
102
- button:hover {
103
- background: #0077ed;
104
- }
105
-
106
- button:active {
107
- background: #006dd1;
108
- }
109
-
110
- .hint {
111
- margin-top: 20px;
112
- padding: 12px;
113
- background: #f6f6f8;
114
- border-radius: 8px;
115
- font-size: 13px;
116
- color: #86868b;
117
- line-height: 1.5;
118
- }
119
-
120
- .iframe-hint {
121
- margin-top: 16px;
122
- padding: 12px 14px;
123
- background: #fff4e6;
124
- border: 1px solid #ffd666;
125
- border-radius: 8px;
126
- font-size: 13px;
127
- color: #d46b08;
128
- line-height: 1.5;
129
- display: none;
130
- }
131
-
132
- .iframe-hint a {
133
- color: #0071e3;
134
- text-decoration: none;
135
- font-weight: 600;
136
- }
137
-
138
- .iframe-hint a:hover {
139
- text-decoration: underline;
140
- }
141
-
142
- @media (max-width: 480px) {
143
- .container {
144
- padding: 32px 24px;
145
- }
146
- }
147
- </style>
148
- <script>
149
- // 检测是否在 iframe 内
150
- if (window.self !== window.top) {
151
- document.addEventListener('DOMContentLoaded', function() {
152
- const hint = document.getElementById('iframe-hint');
153
- if (hint) {
154
- hint.style.display = 'block';
155
- // 获取当前完整 URL(去掉 iframe 参数)
156
- const currentUrl = window.location.href.replace(/[?&]__theme=.*?(&|$)/, '');
157
- const link = document.getElementById('direct-link');
158
- if (link) {
159
- link.href = currentUrl;
160
- }
161
- }
162
- });
163
- }
164
- </script>
165
- </head>
166
- <body>
167
- <div class="container">
168
- <div class="header">
169
- <h1>管理员登录</h1>
170
- <p class="subtitle">Gemini Business API</p>
171
- </div>
172
-
173
- {% if error %}
174
- <div class="error-box">
175
- ⚠️ {{ error }}
176
- </div>
177
- {% endif %}
178
-
179
- <form method="POST" action="{{ request.url.path }}">
180
- <label for="admin_key">密钥</label>
181
- <input
182
- type="password"
183
- id="admin_key"
184
- name="admin_key"
185
- placeholder="输入 ADMIN_KEY"
186
- required
187
- autofocus
188
- >
189
- <button type="submit">登录</button>
190
- </form>
191
-
192
- <div class="hint">
193
- 会话保持 24 小时
194
- </div>
195
-
196
- <div id="iframe-hint" class="iframe-hint">
197
- ⚠️ HuggingFace 内部无法登录,<a id="direct-link" href="#" target="_blank">点我跳转</a>
198
- </div>
199
- </div>
200
- </body>
201
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/base.html DELETED
@@ -1,20 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>{% block title %}Gemini Business API{% endblock %}</title>
7
-
8
- <!-- 页面特定样式 -->
9
- {% block extra_css %}{% endblock %}
10
- </head>
11
- <body>
12
- {% block content %}{% endblock %}
13
-
14
- <!-- 共享脚本 -->
15
- <script src="/static/js/api.js"></script>
16
-
17
- <!-- 页面特定脚本 -->
18
- {% block extra_js %}{% endblock %}
19
- </body>
20
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/components/account_table.html DELETED
@@ -1,58 +0,0 @@
1
- {# 账户表格组件 #}
2
- {% macro account_table(accounts_data) %}
3
- <table class="account-table">
4
- <thead>
5
- <tr>
6
- <th>账号ID</th>
7
- <th>状态</th>
8
- <th>过期时间</th>
9
- <th>剩余时长</th>
10
- <th>累计对话</th>
11
- <th style="text-align: center;">操作</th>
12
- </tr>
13
- </thead>
14
- <tbody>
15
- {% if accounts_data %}
16
- {% for account in accounts_data %}
17
- <tr style="opacity: {{ account.row_opacity }};">
18
- <td data-label="账号ID">
19
- <div style="display: flex; align-items: center; gap: 8px;">
20
- <span class="status-dot" style="background-color: {{ account.dot_color }};"></span>
21
- <span style="font-weight: 600;">{{ account.account_id }}</span>
22
- </div>
23
- </td>
24
- <td data-label="状态">
25
- <span style="color: {{ account.status_color }}; font-weight: 600; font-size: 12px;">{{ account.status_text }}</span>
26
- </td>
27
- <td data-label="过期时间">
28
- <span class="font-mono" style="font-size: 11px; color: #6b6b6b;">{{ account.expires_at or '未设置' }}</span>
29
- </td>
30
- <td data-label="剩余时长">
31
- <span style="color: {{ account.status_color }}; font-weight: 500; font-size: 12px;">{{ account.expire_display }}</span>
32
- </td>
33
- <td data-label="累计对话">
34
- <span style="color: #2563eb; font-weight: 600;">{{ account.conversation_count }}</span>
35
- </td>
36
- <td data-label="操作">
37
- <div style="display: flex; gap: 6px;">
38
- {% if account.is_expired %}
39
- <button onclick="deleteAccount('{{ account.account_id }}')" class="btn-sm btn-delete" title="删除">删除</button>
40
- {% elif account.is_disabled or account.is_permanently_failed %}
41
- <button onclick="enableAccount('{{ account.account_id }}')" class="btn-sm btn-enable" title="启用">启用</button>
42
- <button onclick="deleteAccount('{{ account.account_id }}')" class="btn-sm btn-delete" title="删除">删除</button>
43
- {% else %}
44
- <button onclick="disableAccount('{{ account.account_id }}')" class="btn-sm btn-disable" title="禁用">禁用</button>
45
- <button onclick="deleteAccount('{{ account.account_id }}')" class="btn-sm btn-delete" title="删除">删除</button>
46
- {% endif %}
47
- </div>
48
- </td>
49
- </tr>
50
- {% endfor %}
51
- {% else %}
52
- <tr>
53
- <td colspan="6" style="text-align: center; color: #6b6b6b; padding: 24px;">暂无账户</td>
54
- </tr>
55
- {% endif %}
56
- </tbody>
57
- </table>
58
- {% endmacro %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/components/alerts.html DELETED
@@ -1,51 +0,0 @@
1
- {# API 密钥状态提示 #}
2
- {% macro api_key_status(has_api_key) %}
3
- {% if has_api_key %}
4
- <div class="alert alert-success">
5
- <div class="alert-icon">🔒</div>
6
- <div class="alert-content">
7
- <strong>API 安全模式已启用</strong>
8
- <div class="alert-desc">API 端点需要携带 Authorization 密钥才能访问。</div>
9
- </div>
10
- </div>
11
- {% else %}
12
- <div class="alert alert-warning">
13
- <div class="alert-icon">⚠️</div>
14
- <div class="alert-content">
15
- <strong>API 密钥未设置</strong>
16
- <div class="alert-desc">API 端点当前允许公开访问。建议在 .env 文件中配置 <code>API_KEY</code> 环境变量以提升安全性。</div>
17
- </div>
18
- </div>
19
- {% endif %}
20
- {% endmacro %}
21
-
22
- {# 错误日志提示 #}
23
- {% macro error_alert(error_count, admin_path_segment) %}
24
- {% if error_count > 0 %}
25
- <div class="alert alert-error" id="error-alert">
26
- <div class="alert-icon">🚨</div>
27
- <div class="alert-content">
28
- <strong>检测到 {{ error_count }} 条错误日志</strong>
29
- <a href="/{{ admin_path_segment }}/log/html" class="alert-link" target="_blank">查看详情 &rarr;</a>
30
- <div class="alert-desc">建议及时查看并处理错误日志,确保系统正常运行</div>
31
- </div>
32
- <button class="alert-close" onclick="document.getElementById('error-alert').style.display='none'" title="关闭">&times;</button>
33
- </div>
34
- {% endif %}
35
- {% endmacro %}
36
-
37
- {# 无账户提示 #}
38
- {% macro no_accounts_alert() %}
39
- <div class="alert alert-info">
40
- <div class="alert-icon">💡</div>
41
- <div class="alert-content">
42
- <strong>暂无账户配置</strong>
43
- <div class="alert-desc" style="margin-top: 8px; line-height: 1.6;">
44
- 请添加 Gemini Business 账户以开始使用 API 服务。支持以下方式:<br>
45
- <strong style="margin-top: 6px; display: inline-block;">1. 使用浏览器脚本自动提取</strong> - 安装 <code style="font-size: 11px;">script/copy-config.js</code> 或 <code style="font-size: 11px;">script/download-config.js</code> 到 Tampermonkey,访问 Gemini Business 页面自动提取配置<br>
46
- <strong>2. 批量上传</strong> - 点击上方"📥 批量上传"按钮,上传 JSON 配置文件<br>
47
- <strong>3. 手动编辑</strong> - 点击上方"✏️ 编辑配置"按钮,直接编辑 JSON 配置
48
- </div>
49
- </div>
50
- </div>
51
- {% endmacro %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/public/logs.html DELETED
@@ -1,453 +0,0 @@
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: 1200px;
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 {
30
- color: #1a1a1a;
31
- font-size: 22px;
32
- font-weight: 600;
33
- margin-bottom: 20px;
34
- display: flex;
35
- align-items: center;
36
- justify-content: center;
37
- gap: 12px;
38
- }
39
- h1 img {
40
- width: 32px;
41
- height: 32px;
42
- border-radius: 8px;
43
- }
44
- .info-bar {
45
- background: #f9f9f9;
46
- border: 1px solid #e5e5e5;
47
- border-radius: 8px;
48
- padding: 12px 16px;
49
- margin-bottom: 16px;
50
- display: flex;
51
- align-items: center;
52
- justify-content: space-between;
53
- flex-wrap: wrap;
54
- gap: 12px;
55
- }
56
- .info-item {
57
- display: flex;
58
- align-items: center;
59
- gap: 6px;
60
- font-size: 13px;
61
- color: #6b6b6b;
62
- }
63
- .info-item strong { color: #1a1a1a; }
64
- .info-item a {
65
- color: #1a73e8;
66
- text-decoration: none;
67
- font-weight: 500;
68
- }
69
- .info-item a:hover { text-decoration: underline; }
70
- .stats {
71
- display: grid;
72
- grid-template-columns: repeat(4, 1fr);
73
- gap: 12px;
74
- margin-bottom: 16px;
75
- }
76
- .stat {
77
- background: #fafaf9;
78
- padding: 12px;
79
- border: 1px solid #e5e5e5;
80
- border-radius: 8px;
81
- text-align: center;
82
- transition: all 0.15s ease;
83
- }
84
- .stat:hover { border-color: #d4d4d4; }
85
- .stat-label { color: #6b6b6b; font-size: 11px; margin-bottom: 4px; }
86
- .stat-value { color: #1a1a1a; font-size: 18px; font-weight: 600; }
87
- .log-container {
88
- flex: 1;
89
- background: #fafaf9;
90
- border: 1px solid #e5e5e5;
91
- border-radius: 8px;
92
- padding: 12px;
93
- overflow-y: auto;
94
- scrollbar-width: thin;
95
- scrollbar-color: rgba(0,0,0,0.15) transparent;
96
- }
97
- .log-container::-webkit-scrollbar { width: 4px; }
98
- .log-container::-webkit-scrollbar-track { background: transparent; }
99
- .log-container::-webkit-scrollbar-thumb {
100
- background: rgba(0,0,0,0.15);
101
- border-radius: 2px;
102
- }
103
- .log-container::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.3); }
104
- .log-group {
105
- margin-bottom: 8px;
106
- border: 1px solid #e5e5e5;
107
- border-radius: 8px;
108
- background: white;
109
- }
110
- .log-group-header {
111
- padding: 10px 12px;
112
- background: #f9f9f9;
113
- border-radius: 8px 8px 0 0;
114
- cursor: pointer;
115
- display: flex;
116
- align-items: center;
117
- gap: 8px;
118
- transition: background 0.15s ease;
119
- }
120
- .log-group-header:hover { background: #f0f0f0; }
121
- .log-group-content { padding: 8px; }
122
- .log-entry {
123
- padding: 8px 10px;
124
- margin-bottom: 4px;
125
- background: white;
126
- border: 1px solid #e5e5e5;
127
- border-radius: 6px;
128
- display: flex;
129
- align-items: center;
130
- gap: 10px;
131
- font-size: 13px;
132
- transition: all 0.15s ease;
133
- }
134
- .log-entry:hover { border-color: #d4d4d4; }
135
- .log-time { color: #6b6b6b; font-size: 12px; min-width: 140px; }
136
- .log-status {
137
- padding: 2px 8px;
138
- border-radius: 4px;
139
- font-size: 11px;
140
- font-weight: 600;
141
- min-width: 60px;
142
- text-align: center;
143
- }
144
- .status-success { background: #d1fae5; color: #065f46; }
145
- .status-error { background: #fee2e2; color: #991b1b; }
146
- .status-in_progress { background: #fef3c7; color: #92400e; }
147
- .status-timeout { background: #fef3c7; color: #92400e; }
148
- .log-info { flex: 1; color: #374151; }
149
- .toggle-icon {
150
- display: inline-block;
151
- transition: transform 0.2s ease;
152
- }
153
- .toggle-icon.collapsed { transform: rotate(-90deg); }
154
- .subtitle-public {
155
- display: flex;
156
- justify-content: center;
157
- align-items: center;
158
- gap: 8px;
159
- flex-wrap: wrap;
160
- }
161
-
162
- @media (max-width: 768px) {
163
- body { padding: 0; }
164
- .container {
165
- padding: 15px;
166
- height: 100vh;
167
- border-radius: 0;
168
- max-width: 100%;
169
- }
170
- h1 { font-size: 18px; margin-bottom: 12px; }
171
- .subtitle-public {
172
- flex-direction: column;
173
- gap: 6px;
174
- }
175
- .subtitle-public span {
176
- font-size: 11px;
177
- line-height: 1.6;
178
- }
179
- .subtitle-public a {
180
- font-size: 12px;
181
- font-weight: 600;
182
- }
183
- .info-bar {
184
- padding: 10px 12px;
185
- flex-direction: column;
186
- align-items: flex-start;
187
- gap: 8px;
188
- }
189
- .info-item { font-size: 12px; }
190
- .stats {
191
- grid-template-columns: repeat(2, 1fr);
192
- gap: 8px;
193
- margin-bottom: 12px;
194
- }
195
- .stat { padding: 8px; }
196
- .stat-label { font-size: 10px; }
197
- .stat-value { font-size: 16px; }
198
- .log-container { padding: 8px; }
199
- .log-group { margin-bottom: 6px; }
200
- .log-group-header {
201
- padding: 8px 10px;
202
- font-size: 11px;
203
- flex-wrap: wrap;
204
- }
205
- .log-group-header span { font-size: 10px !important; }
206
- .log-entry {
207
- padding: 6px 8px;
208
- font-size: 11px;
209
- flex-direction: column;
210
- align-items: flex-start;
211
- gap: 4px;
212
- }
213
- .log-time {
214
- min-width: auto;
215
- font-size: 10px;
216
- }
217
- .log-info {
218
- font-size: 11px;
219
- word-break: break-word;
220
- }
221
- }
222
- </style>
223
- </head>
224
- <body>
225
- <div class="container">
226
- <h1>
227
- {% if logo_url %}<img src="{{ logo_url }}" alt="Logo">{% endif %}
228
- Gemini服务状态
229
- </h1>
230
- <div style="text-align: center; color: #999; font-size: 12px; margin-bottom: 16px;" class="subtitle-public">
231
- <span>展示最近1000条对话日志 · 每5秒自动更新</span>
232
- {% if chat_url %}<a href="{{ chat_url }}" target="_blank" style="color: #1a73e8; text-decoration: none;">开始对话</a>{% else %}<span style="color: #999;">开始对话</span>{% endif %}
233
- </div>
234
- <div class="stats">
235
- <div class="stat">
236
- <div class="stat-label">总访问</div>
237
- <div class="stat-value" id="stat-visitors">0</div>
238
- </div>
239
- <div class="stat">
240
- <div class="stat-label">每分钟请求</div>
241
- <div class="stat-value" id="stat-load">0</div>
242
- </div>
243
- <div class="stat">
244
- <div class="stat-label">平均响应</div>
245
- <div class="stat-value" id="stat-avg-time">-</div>
246
- </div>
247
- <div class="stat">
248
- <div class="stat-label">成功率</div>
249
- <div class="stat-value" id="stat-success-rate" style="color: #10b981;">-</div>
250
- </div>
251
- <div class="stat">
252
- <div class="stat-label">对话次数</div>
253
- <div class="stat-value" id="stat-total">0</div>
254
- </div>
255
- <div class="stat">
256
- <div class="stat-label">成功</div>
257
- <div class="stat-value" id="stat-success" style="color: #10b981;">0</div>
258
- </div>
259
- <div class="stat">
260
- <div class="stat-label">失败</div>
261
- <div class="stat-value" id="stat-error" style="color: #ef4444;">0</div>
262
- </div>
263
- <div class="stat">
264
- <div class="stat-label">更新时间</div>
265
- <div class="stat-value" id="stat-update-time" style="font-size: 14px; color: #6b6b6b;">--:--</div>
266
- </div>
267
- </div>
268
- <div class="log-container" id="log-container">
269
- <div style="text-align: center; color: #999; padding: 20px;">加载中...</div>
270
- </div>
271
- </div>
272
- <script>
273
- async function loadData() {
274
- try {
275
- // 并行加载日志和统计数据
276
- const [logsResponse, statsResponse] = await Promise.all([
277
- fetch('/public/log?limit=1000'),
278
- fetch('/public/stats')
279
- ]);
280
-
281
- const logsData = await logsResponse.json();
282
- const statsData = await statsResponse.json();
283
-
284
- displayLogs(logsData.logs);
285
- updateStats(logsData.logs, statsData);
286
- } catch (error) {
287
- document.getElementById('log-container').innerHTML = '<div style="text-align: center; color: #f44336; padding: 20px;">加载失败: ' + error.message + '</div>';
288
- }
289
- }
290
-
291
- function displayLogs(logs) {
292
- const container = document.getElementById('log-container');
293
- if (logs.length === 0) {
294
- container.innerHTML = '<div style="text-align: center; color: #999; padding: 20px;">暂无日志</div>';
295
- return;
296
- }
297
-
298
- // 读取折叠状态
299
- const foldState = JSON.parse(localStorage.getItem('public-log-fold-state') || '{}');
300
-
301
- let html = '';
302
- logs.forEach(log => {
303
- const reqId = log.request_id;
304
-
305
- // 状态图标和颜色
306
- let statusColor = '#ff9800';
307
- let statusText = '进行中';
308
-
309
- if (log.status === 'success') {
310
- statusColor = '#4caf50';
311
- statusText = '成功';
312
- } else if (log.status === 'error') {
313
- statusColor = '#f44336';
314
- statusText = '失败';
315
- } else if (log.status === 'timeout') {
316
- statusColor = '#ffc107';
317
- statusText = '超时';
318
- }
319
-
320
- // 检查折叠状态
321
- const isCollapsed = foldState[reqId] === true;
322
- const contentStyle = isCollapsed ? 'style="display: none;"' : '';
323
- const iconClass = isCollapsed ? 'class="toggle-icon collapsed"' : 'class="toggle-icon"';
324
-
325
- // 构建事件列表
326
- let eventsHtml = '';
327
- log.events.forEach(event => {
328
- let eventClass = 'log-entry';
329
- let eventLabel = '';
330
-
331
- if (event.type === 'start') {
332
- eventLabel = '<span style="color: #2563eb; font-weight: 600;">开始对话</span>';
333
- } else if (event.type === 'select') {
334
- eventLabel = '<span style="color: #8b5cf6; font-weight: 600;">选择</span>';
335
- } else if (event.type === 'retry') {
336
- eventLabel = '<span style="color: #f59e0b; font-weight: 600;">重试</span>';
337
- } else if (event.type === 'switch') {
338
- eventLabel = '<span style="color: #06b6d4; font-weight: 600;">切换</span>';
339
- } else if (event.type === 'complete') {
340
- if (event.status === 'success') {
341
- eventLabel = '<span style="color: #10b981; font-weight: 600;">完成</span>';
342
- } else if (event.status === 'error') {
343
- eventLabel = '<span style="color: #ef4444; font-weight: 600;">失败</span>';
344
- } else if (event.status === 'timeout') {
345
- eventLabel = '<span style="color: #f59e0b; font-weight: 600;">超时</span>';
346
- }
347
- }
348
-
349
- eventsHtml += `
350
- <div class="${eventClass}">
351
- <div class="log-time">${event.time}</div>
352
- <div style="min-width: 60px;">${eventLabel}</div>
353
- <div class="log-info">${event.content}</div>
354
- </div>
355
- `;
356
- });
357
-
358
- html += `
359
- <div class="log-group" data-req-id="${reqId}">
360
- <div class="log-group-header" onclick="toggleGroup('${reqId}')">
361
- <span style="color: ${statusColor}; font-weight: 600; font-size: 11px;">⬤ ${statusText}</span>
362
- <span style="color: #666; font-size: 11px; margin-left: 8px;">req_${reqId}</span>
363
- <span style="color: #999; font-size: 11px; margin-left: 8px;">${log.events.length}条事件</span>
364
- <span ${iconClass} style="margin-left: auto; color: #999;">▼</span>
365
- </div>
366
- <div class="log-group-content" ${contentStyle}>
367
- ${eventsHtml}
368
- </div>
369
- </div>
370
- `;
371
- });
372
-
373
- container.innerHTML = html;
374
- }
375
-
376
- function updateStats(logs, statsData) {
377
- const total = logs.length;
378
- const successLogs = logs.filter(log => log.status === 'success');
379
- const success = successLogs.length;
380
- const error = logs.filter(log => log.status === 'error').length;
381
-
382
- // 计算平均响应时间
383
- let avgTime = '-';
384
- if (success > 0) {
385
- let totalDuration = 0;
386
- let count = 0;
387
- successLogs.forEach(log => {
388
- log.events.forEach(event => {
389
- if (event.type === 'complete' && event.content.includes('耗时')) {
390
- const match = event.content.match(/([\d.]+)s/);
391
- if (match) {
392
- totalDuration += parseFloat(match[1]);
393
- count++;
394
- }
395
- }
396
- });
397
- });
398
- if (count > 0) {
399
- avgTime = (totalDuration / count).toFixed(1) + 's';
400
- }
401
- }
402
-
403
- // 计算成功率
404
- const totalCompleted = success + error;
405
- const successRate = totalCompleted > 0 ? ((success / totalCompleted) * 100).toFixed(1) + '%' : '-';
406
-
407
- // 更新日志统计
408
- document.getElementById('stat-total').textContent = total;
409
- document.getElementById('stat-success').textContent = success;
410
- document.getElementById('stat-error').textContent = error;
411
- document.getElementById('stat-success-rate').textContent = successRate;
412
- document.getElementById('stat-avg-time').textContent = avgTime;
413
-
414
- // 更新全局统计
415
- document.getElementById('stat-visitors').textContent = statsData.total_visitors;
416
-
417
- // 更新负载状态(带颜色)
418
- const loadElement = document.getElementById('stat-load');
419
- loadElement.textContent = statsData.requests_per_minute;
420
- loadElement.style.color = statsData.load_color;
421
-
422
- // 更新时间
423
- document.getElementById('stat-update-time').textContent = new Date().toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit', second: '2-digit'});
424
- }
425
-
426
- function toggleGroup(reqId) {
427
- const group = document.querySelector(`.log-group[data-req-id="${reqId}"]`);
428
- const content = group.querySelector('.log-group-content');
429
- const icon = group.querySelector('.toggle-icon');
430
-
431
- const isCollapsed = content.style.display === 'none';
432
- if (isCollapsed) {
433
- content.style.display = 'block';
434
- icon.classList.remove('collapsed');
435
- } else {
436
- content.style.display = 'none';
437
- icon.classList.add('collapsed');
438
- }
439
-
440
- // 保存折叠状态
441
- const foldState = JSON.parse(localStorage.getItem('public-log-fold-state') || '{}');
442
- foldState[reqId] = !isCollapsed;
443
- localStorage.setItem('public-log-fold-state', JSON.stringify(foldState));
444
- }
445
-
446
- // 初始加载
447
- loadData();
448
-
449
- // 自动刷新(每5秒)
450
- setInterval(loadData, 5000);
451
- </script>
452
- </body>
453
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/public/uptime.html DELETED
@@ -1,175 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Gemini Status</title>
7
- <style>
8
- * { margin: 0; padding: 0; box-sizing: border-box; }
9
- body {
10
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11
- background: #f5f5f7;
12
- color: #1d1d1f;
13
- min-height: 100vh;
14
- padding: 20px;
15
- }
16
- .container { max-width: 1200px; margin: 0 auto; }
17
- h1 {
18
- font-size: 24px;
19
- font-weight: 600;
20
- margin-bottom: 8px;
21
- color: #1d1d1f;
22
- }
23
- .subtitle { color: #86868b; font-size: 14px; margin-bottom: 24px; }
24
- .update-time { color: #86868b; font-size: 12px; margin-bottom: 16px; }
25
- .grid {
26
- display: grid;
27
- grid-template-columns: repeat(2, 1fr);
28
- gap: 16px;
29
- }
30
- .card {
31
- background: #fff;
32
- border: 1px solid #e5e5e5;
33
- border-radius: 12px;
34
- padding: 16px;
35
- box-shadow: 0 1px 3px rgba(0,0,0,0.04);
36
- }
37
- .card:hover { border-color: #d4d4d4; }
38
- .card-header {
39
- display: flex;
40
- justify-content: space-between;
41
- align-items: center;
42
- margin-bottom: 12px;
43
- }
44
- .service-name { font-weight: 600; font-size: 14px; color: #1d1d1f; }
45
- .status-badge {
46
- padding: 2px 8px;
47
- border-radius: 12px;
48
- font-size: 11px;
49
- font-weight: 600;
50
- }
51
- .status-up { background: #d1fae5; color: #065f46; }
52
- .status-down { background: #fee2e2; color: #991b1b; }
53
- .status-unknown { background: #f3f4f6; color: #6b7280; }
54
- .stats {
55
- display: flex;
56
- gap: 16px;
57
- margin-bottom: 12px;
58
- font-size: 12px;
59
- color: #86868b;
60
- }
61
- .stat-value { color: #1d1d1f; font-weight: 600; }
62
- .heartbeat-bar {
63
- display: flex;
64
- gap: 2px;
65
- height: 24px;
66
- align-items: flex-end;
67
- }
68
- .beat {
69
- flex: 1;
70
- min-width: 4px;
71
- max-width: 8px;
72
- border-radius: 2px;
73
- transition: all 0.2s;
74
- position: relative;
75
- }
76
- .beat:hover { opacity: 0.8; transform: scaleY(1.1); }
77
- .beat.up { background: #34c759; height: 100%; }
78
- .beat.down { background: #ff3b30; height: 100%; }
79
- .beat.empty { background: #e5e5ea; height: 40%; }
80
- .beat .tooltip {
81
- position: absolute;
82
- bottom: 100%;
83
- left: 50%;
84
- transform: translateX(-50%);
85
- background: #1d1d1f;
86
- color: #fff;
87
- padding: 6px 10px;
88
- border-radius: 6px;
89
- font-size: 11px;
90
- white-space: nowrap;
91
- opacity: 0;
92
- pointer-events: none;
93
- transition: opacity 0.15s;
94
- margin-bottom: 6px;
95
- z-index: 100;
96
- }
97
- .beat .tooltip::after {
98
- content: '';
99
- position: absolute;
100
- top: 100%;
101
- left: 50%;
102
- transform: translateX(-50%);
103
- border: 5px solid transparent;
104
- border-top-color: #1d1d1f;
105
- }
106
- .beat:hover .tooltip { opacity: 1; }
107
- @media (max-width: 768px) {
108
- .grid { grid-template-columns: 1fr; }
109
- .beat { min-width: 3px; max-width: 6px; }
110
- }
111
- </style>
112
- </head>
113
- <body>
114
- <div class="container">
115
- <h1>Gemini Status</h1>
116
- <p class="subtitle">服务状态监控</p>
117
- <p class="update-time" id="update-time">更新中...</p>
118
- <div class="grid" id="services"></div>
119
- </div>
120
- <script>
121
- async function loadStatus() {
122
- try {
123
- const res = await fetch('/public/uptime');
124
- const data = await res.json();
125
- renderServices(data);
126
- document.getElementById('update-time').textContent = '更新于 ' + data.updated_at;
127
- } catch (e) {
128
- document.getElementById('services').innerHTML = '<div class="card">加载失败</div>';
129
- }
130
- }
131
-
132
- function renderServices(data) {
133
- const container = document.getElementById('services');
134
- let html = '';
135
- for (const [id, svc] of Object.entries(data.services)) {
136
- const statusClass = svc.status === 'up' ? 'status-up' : svc.status === 'down' ? 'status-down' : 'status-unknown';
137
- const statusText = svc.status === 'up' ? '正常' : svc.status === 'down' ? '故障' : '未知';
138
-
139
- // 生成心跳条
140
- let beats = '';
141
- const maxBeats = 60;
142
- const heartbeats = svc.heartbeats || [];
143
- for (let i = 0; i < maxBeats; i++) {
144
- if (i < heartbeats.length) {
145
- const beat = heartbeats[i];
146
- const status = beat.success ? '成功' : '失败';
147
- beats += `<div class="beat ${beat.success ? 'up' : 'down'}"><span class="tooltip">${beat.time} · ${status}</span></div>`;
148
- } else {
149
- beats += '<div class="beat empty"></div>';
150
- }
151
- }
152
-
153
- html += `
154
- <div class="card">
155
- <div class="card-header">
156
- <span class="service-name">${svc.name}</span>
157
- <span class="status-badge ${statusClass}">${statusText}</span>
158
- </div>
159
- <div class="stats">
160
- <span>可用率 <span class="stat-value">${svc.uptime}%</span></span>
161
- <span>请求 <span class="stat-value">${svc.total}</span></span>
162
- <span>成功 <span class="stat-value">${svc.success}</span></span>
163
- </div>
164
- <div class="heartbeat-bar">${beats}</div>
165
- </div>
166
- `;
167
- }
168
- container.innerHTML = html;
169
- }
170
-
171
- loadStatus();
172
- setInterval(loadStatus, 5000);
173
- </script>
174
- </body>
175
- </html>