22333Misaka commited on
Commit
9f72602
·
verified ·
1 Parent(s): 1e07559

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +20 -0
  2. chat_history.html +1712 -0
  3. docker-compose.yml +27 -0
  4. gemini.py +0 -0
  5. index.html +2025 -0
  6. requirements.txt +15 -0
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1
2
+ FROM python:3.11-slim
3
+
4
+ ENV PYTHONDONTWRITEBYTECODE=1 \
5
+ PYTHONUNBUFFERED=1 \
6
+ PIP_NO_CACHE_DIR=1
7
+
8
+ WORKDIR /app
9
+
10
+ COPY requirements.txt .
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ COPY . .
14
+
15
+ EXPOSE 8000
16
+
17
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
18
+ CMD python -c "import requests; requests.get('http://localhost:8000/health', timeout=5)"
19
+
20
+ CMD ["python", "-u", "gemini.py"]
chat_history.html ADDED
@@ -0,0 +1,1712 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN" data-theme="light">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Business Gemini 智能对话</title>
7
+ <style>
8
+ /* ==================== 复用原有 CSS 变量 ==================== */
9
+ :root {
10
+ --primary: #2563eb;
11
+ --primary-hover: #1d4ed8;
12
+ --primary-light: rgba(37, 99, 235, 0.1);
13
+ --success: #10b981;
14
+ --danger: #ef4444;
15
+ --warning: #f59e0b;
16
+ --radius: 8px;
17
+ }
18
+
19
+ [data-theme="light"] {
20
+ --bg-color: #f1f5f9;
21
+ --card-bg: #ffffff;
22
+ --text-main: #1e293b;
23
+ --text-muted: #64748b;
24
+ --border: #e2e8f0;
25
+ --hover-bg: #f8fafc;
26
+ --input-bg: #ffffff;
27
+ --bubble-user: #2563eb;
28
+ --bubble-ai: #ffffff;
29
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
30
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
31
+ --bg-gradient: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
32
+ }
33
+
34
+ [data-theme="dark"] {
35
+ --bg-color: #0f172a;
36
+ --card-bg: #1e293b;
37
+ --text-main: #e2e8f0;
38
+ --text-muted: #94a3b8;
39
+ --border: #334155;
40
+ --hover-bg: rgba(255, 255, 255, 0.05);
41
+ --input-bg: #0f172a;
42
+ --bubble-user: #2563eb;
43
+ --bubble-ai: #1e293b;
44
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
45
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
46
+ --bg-gradient: radial-gradient(circle at 10% 20%, rgba(37, 99, 235, 0.1) 0%, transparent 20%);
47
+ }
48
+
49
+ * { margin: 0; padding: 0; box-sizing: border-box; transition: background-color 0.3s, border-color 0.3s; }
50
+
51
+ body {
52
+ font-family: 'Inter', -apple-system, sans-serif;
53
+ background-color: var(--bg-color);
54
+ background-image: var(--bg-gradient);
55
+ color: var(--text-main);
56
+ height: 100vh;
57
+ display: flex;
58
+ flex-direction: column;
59
+ overflow: hidden;
60
+ }
61
+
62
+ /* ==================== 布局结构 ==================== */
63
+ .header {
64
+ padding: 20px 30px;
65
+ background: rgba(255, 255, 255, 0.8);
66
+ backdrop-filter: blur(10px);
67
+ border-bottom: 1px solid var(--border);
68
+ display: flex;
69
+ justify-content: space-between;
70
+ align-items: center;
71
+ flex-shrink: 0;
72
+ z-index: 10;
73
+ }
74
+
75
+ [data-theme="dark"] .header { background: rgba(15, 23, 42, 0.8); }
76
+
77
+ .header h1 {
78
+ font-size: 20px;
79
+ font-weight: 700;
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 10px;
83
+ }
84
+
85
+ .header h1::before {
86
+ content: ''; width: 4px; height: 20px; background: var(--primary); border-radius: 2px;
87
+ }
88
+
89
+ .header-controls {
90
+ display: flex;
91
+ gap: 10px;
92
+ align-items: center;
93
+ }
94
+
95
+ .chat-container {
96
+ flex: 1;
97
+ max-width: 1000px;
98
+ width: 100%;
99
+ margin: 0 auto;
100
+ padding: 30px 20px 120px 20px;
101
+ overflow-y: auto;
102
+ scroll-behavior: smooth;
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: 24px;
106
+ }
107
+
108
+ /* ==================== 对话气泡样式 ==================== */
109
+ .message-row {
110
+ display: flex;
111
+ align-items: flex-start;
112
+ gap: 16px;
113
+ animation: slideIn 0.3s ease-out;
114
+ }
115
+
116
+ @keyframes slideIn {
117
+ from { opacity: 0; transform: translateY(10px); }
118
+ to { opacity: 1; transform: translateY(0); }
119
+ }
120
+
121
+ .message-row.user {
122
+ flex-direction: row-reverse;
123
+ }
124
+
125
+ .avatar {
126
+ width: 40px;
127
+ height: 40px;
128
+ border-radius: 50%;
129
+ display: flex;
130
+ align-items: center;
131
+ justify-content: center;
132
+ font-size: 18px;
133
+ flex-shrink: 0;
134
+ box-shadow: var(--shadow-sm);
135
+ background-color: var(--card-bg);
136
+ border: 1px solid var(--border);
137
+ }
138
+
139
+ .avatar.ai { color: var(--primary); background: var(--primary-light); border: none; }
140
+ .avatar.user { background: var(--card-bg); color: var(--text-muted); }
141
+
142
+ .message-content {
143
+ display: flex;
144
+ flex-direction: column;
145
+ max-width: 70%;
146
+ gap: 4px;
147
+ }
148
+
149
+ .message-row.user .message-content {
150
+ align-items: flex-end;
151
+ }
152
+
153
+ .bubble {
154
+ padding: 12px 16px;
155
+ border-radius: 12px;
156
+ font-size: 14px;
157
+ line-height: 1.6;
158
+ position: relative;
159
+ box-shadow: var(--shadow-sm);
160
+ word-wrap: break-word;
161
+ white-space: pre-wrap;
162
+ }
163
+
164
+ .message-row.user .bubble {
165
+ background: var(--bubble-user);
166
+ color: white;
167
+ border-top-right-radius: 2px;
168
+ }
169
+
170
+ .message-row.ai .bubble {
171
+ background: var(--bubble-ai);
172
+ color: var(--text-main);
173
+ border: 1px solid var(--border);
174
+ border-top-left-radius: 2px;
175
+ }
176
+
177
+ .timestamp {
178
+ font-size: 11px;
179
+ color: var(--text-muted);
180
+ margin: 0 4px;
181
+ }
182
+
183
+ /* ==================== 模型选择器 ==================== */
184
+ /* 主布局容器 */
185
+ .main-container {
186
+ display: flex;
187
+ flex: 1;
188
+ overflow: hidden;
189
+ }
190
+
191
+ /* 左侧会话列表 */
192
+ .session-sidebar {
193
+ width: 260px;
194
+ background: var(--bg-secondary);
195
+ border-right: 1px solid var(--border);
196
+ display: flex;
197
+ flex-direction: column;
198
+ flex-shrink: 0;
199
+ }
200
+
201
+ .session-header {
202
+ padding: 16px;
203
+ border-bottom: 1px solid var(--border);
204
+ display: flex;
205
+ justify-content: space-between;
206
+ align-items: center;
207
+ }
208
+
209
+ .session-header h3 {
210
+ margin: 0;
211
+ font-size: 14px;
212
+ color: var(--text-main);
213
+ }
214
+
215
+ .new-session-btn {
216
+ background: var(--primary);
217
+ color: white;
218
+ border: none;
219
+ padding: 6px 12px;
220
+ border-radius: 6px;
221
+ cursor: pointer;
222
+ font-size: 13px;
223
+ transition: background 0.2s;
224
+ }
225
+
226
+ .new-session-btn:hover {
227
+ background: var(--primary-dark);
228
+ }
229
+
230
+ .session-list {
231
+ flex: 1;
232
+ overflow-y: auto;
233
+ padding: 8px;
234
+ }
235
+
236
+ .session-item {
237
+ padding: 12px;
238
+ border-radius: 8px;
239
+ cursor: pointer;
240
+ margin-bottom: 4px;
241
+ display: flex;
242
+ justify-content: space-between;
243
+ align-items: center;
244
+ transition: background 0.2s;
245
+ }
246
+
247
+ .session-item:hover {
248
+ background: var(--bg-main);
249
+ }
250
+
251
+ .session-item.active {
252
+ background: var(--primary-light);
253
+ }
254
+
255
+ .session-name {
256
+ flex: 1;
257
+ overflow: hidden;
258
+ text-overflow: ellipsis;
259
+ white-space: nowrap;
260
+ font-size: 14px;
261
+ color: var(--text-main);
262
+ }
263
+
264
+ .session-actions {
265
+ display: flex;
266
+ gap: 4px;
267
+ visibility: hidden;
268
+ }
269
+
270
+ .session-item:hover .session-actions {
271
+ visibility: visible;
272
+ }
273
+
274
+ .session-action-btn {
275
+ background: none;
276
+ border: none;
277
+ cursor: pointer;
278
+ padding: 4px;
279
+ font-size: 12px;
280
+ opacity: 0.6;
281
+ transition: opacity 0.2s;
282
+ }
283
+
284
+ .session-action-btn:hover {
285
+ opacity: 1;
286
+ }
287
+
288
+ /* 聊天主区域 */
289
+ .chat-main {
290
+ flex: 1;
291
+ display: flex;
292
+ flex-direction: column;
293
+ overflow: hidden;
294
+ }
295
+
296
+ .model-selector {
297
+ display: flex;
298
+ align-items: center;
299
+ gap: 8px;
300
+ font-size: 13px;
301
+ color: var(--text-muted);
302
+ }
303
+
304
+ .model-selector label {
305
+ white-space: nowrap;
306
+ }
307
+
308
+ .model-selector select {
309
+ padding: 6px 12px;
310
+ border-radius: 8px;
311
+ border: 1px solid var(--border);
312
+ background: var(--bg-main);
313
+ color: var(--text-main);
314
+ font-size: 13px;
315
+ cursor: pointer;
316
+ outline: none;
317
+ min-width: 150px;
318
+ }
319
+
320
+ .model-selector select:hover {
321
+ border-color: var(--primary);
322
+ }
323
+
324
+ .model-selector select:focus {
325
+ border-color: var(--primary);
326
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
327
+ }
328
+
329
+ /* ==================== 模式切换开关 ==================== */
330
+ .mode-switch {
331
+ display: flex;
332
+ align-items: center;
333
+ gap: 8px;
334
+ font-size: 13px;
335
+ color: var(--text-muted);
336
+ }
337
+
338
+ .switch {
339
+ position: relative;
340
+ width: 44px;
341
+ height: 24px;
342
+ }
343
+
344
+ .switch input {
345
+ opacity: 0;
346
+ width: 0;
347
+ height: 0;
348
+ }
349
+
350
+ .slider {
351
+ position: absolute;
352
+ cursor: pointer;
353
+ top: 0;
354
+ left: 0;
355
+ right: 0;
356
+ bottom: 0;
357
+ background-color: var(--border);
358
+ transition: 0.3s;
359
+ border-radius: 24px;
360
+ }
361
+
362
+ .slider:before {
363
+ position: absolute;
364
+ content: "";
365
+ height: 18px;
366
+ width: 18px;
367
+ left: 3px;
368
+ bottom: 3px;
369
+ background-color: white;
370
+ transition: 0.3s;
371
+ border-radius: 50%;
372
+ }
373
+
374
+ input:checked + .slider {
375
+ background-color: var(--primary);
376
+ }
377
+
378
+ input:checked + .slider:before {
379
+ transform: translateX(20px);
380
+ }
381
+
382
+ /* 在CSS部分添加文件上传相关样式,在 .error-message 样式后面添加 -->
383
+ .error-message {
384
+ background: rgba(239, 68, 68, 0.1);
385
+ border: 1px solid var(--danger);
386
+ color: var(--danger);
387
+ padding: 8px 12px;
388
+ border-radius: 8px;
389
+ font-size: 13px;
390
+ }
391
+
392
+ /* 底部输入区 */
393
+ .input-area {
394
+ position: fixed;
395
+ bottom: 0;
396
+ left: 0;
397
+ right: 0;
398
+ background: var(--card-bg);
399
+ border-top: 1px solid var(--border);
400
+ padding: 16px 20px;
401
+ z-index: 100;
402
+ }
403
+
404
+ .input-wrapper {
405
+ max-width: 1000px;
406
+ margin: 0 auto;
407
+ display: flex;
408
+ gap: 12px;
409
+ align-items: flex-end;
410
+ }
411
+
412
+ .input-wrapper textarea {
413
+ flex: 1;
414
+ padding: 12px 16px;
415
+ border: 1px solid var(--border);
416
+ border-radius: 20px;
417
+ background: var(--input-bg);
418
+ color: var(--text-main);
419
+ font-size: 15px;
420
+ resize: none;
421
+ min-height: 44px;
422
+ max-height: 120px;
423
+ outline: none;
424
+ transition: border-color 0.2s;
425
+ font-family: inherit;
426
+ }
427
+
428
+ .input-wrapper textarea:focus {
429
+ border-color: var(--primary);
430
+ }
431
+
432
+ .input-wrapper textarea::placeholder {
433
+ color: var(--text-muted);
434
+ }
435
+
436
+ .send-btn {
437
+ width: 50px;
438
+ height: 50px;
439
+ border-radius: 12px;
440
+ border: none;
441
+ background: var(--primary);
442
+ color: white;
443
+ font-size: 18px;
444
+ cursor: pointer;
445
+ display: flex;
446
+ align-items: center;
447
+ justify-content: center;
448
+ transition: all 0.2s;
449
+ flex-shrink: 0;
450
+ }
451
+
452
+ .send-btn:hover:not(:disabled) {
453
+ background: var(--primary-hover);
454
+ transform: scale(1.05);
455
+ }
456
+
457
+ .send-btn:disabled {
458
+ opacity: 0.6;
459
+ cursor: not-allowed;
460
+ }
461
+
462
+ /* 主题切换按钮 */
463
+ .theme-toggle {
464
+ background: var(--card-bg);
465
+ border: 1px solid var(--border);
466
+ border-radius: 8px;
467
+ padding: 8px 12px;
468
+ cursor: pointer;
469
+ font-size: 14px;
470
+ color: var(--text-main);
471
+ transition: all 0.2s;
472
+ }
473
+
474
+ .theme-toggle:hover {
475
+ background: var(--hover-bg);
476
+ border-color: var(--primary);
477
+ }
478
+
479
+ /* 加载动画 */
480
+ .typing-indicator {
481
+ display: flex;
482
+ gap: 4px;
483
+ padding: 12px 16px;
484
+ background: var(--bubble-ai);
485
+ border: 1px solid var(--border);
486
+ border-radius: 12px;
487
+ border-top-left-radius: 2px;
488
+ }
489
+
490
+ .typing-indicator span {
491
+ width: 8px;
492
+ height: 8px;
493
+ background: var(--text-muted);
494
+ border-radius: 50%;
495
+ animation: typing 1.4s infinite ease-in-out;
496
+ }
497
+
498
+ .typing-indicator span:nth-child(1) { animation-delay: 0s; }
499
+ .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
500
+ .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
501
+
502
+ @keyframes typing {
503
+ 0%, 60%, 100% { transform: translateY(0); opacity: 0.6; }
504
+ 30% { transform: translateY(-10px); opacity: 1; }
505
+ }
506
+
507
+ /* ==================== 文件上传相关样式 ==================== */
508
+ .file-upload-btn {
509
+ width: 60px;
510
+ height: 60px;
511
+ border-radius: 12px;
512
+ background: var(--card-bg);
513
+ color: var(--text-main);
514
+ border: 1px solid var(--border);
515
+ cursor: pointer;
516
+ display: flex;
517
+ align-items: center;
518
+ justify-content: center;
519
+ font-size: 20px;
520
+ transition: transform 0.2s, background 0.2s, border-color 0.2s;
521
+ }
522
+
523
+ .file-upload-btn:hover {
524
+ background: var(--hover-bg);
525
+ border-color: var(--primary);
526
+ transform: scale(1.05);
527
+ }
528
+
529
+ .file-upload-btn:active {
530
+ transform: scale(0.95);
531
+ }
532
+
533
+ .file-upload-btn:disabled {
534
+ background: var(--hover-bg);
535
+ cursor: not-allowed;
536
+ transform: none;
537
+ opacity: 0.6;
538
+ }
539
+
540
+ .file-upload-btn.has-files {
541
+ background: var(--primary-light);
542
+ border-color: var(--primary);
543
+ color: var(--primary);
544
+ }
545
+
546
+ #fileInput {
547
+ display: none;
548
+ }
549
+
550
+ .uploaded-files-container {
551
+ max-width: 1000px;
552
+ width: 100%;
553
+ margin: 0 auto 10px auto;
554
+ padding: 0 20px;
555
+ }
556
+
557
+ .uploaded-files {
558
+ display: flex;
559
+ flex-wrap: wrap;
560
+ gap: 8px;
561
+ padding: 10px;
562
+ background: var(--hover-bg);
563
+ border-radius: 12px;
564
+ border: 1px solid var(--border);
565
+ }
566
+
567
+ .file-tag {
568
+ display: flex;
569
+ align-items: center;
570
+ gap: 6px;
571
+ padding: 6px 12px;
572
+ background: var(--card-bg);
573
+ border: 1px solid var(--border);
574
+ border-radius: 20px;
575
+ font-size: 13px;
576
+ color: var(--text-main);
577
+ animation: fadeIn 0.3s ease;
578
+ }
579
+
580
+ @keyframes fadeIn {
581
+ from { opacity: 0; transform: scale(0.9); }
582
+ to { opacity: 1; transform: scale(1); }
583
+ }
584
+
585
+ .file-tag .file-icon {
586
+ font-size: 14px;
587
+ }
588
+
589
+ .file-tag .file-name {
590
+ max-width: 150px;
591
+ overflow: hidden;
592
+ text-overflow: ellipsis;
593
+ white-space: nowrap;
594
+ }
595
+
596
+ .file-tag .remove-file {
597
+ width: 18px;
598
+ height: 18px;
599
+ border-radius: 50%;
600
+ background: var(--danger);
601
+ color: white;
602
+ border: none;
603
+ cursor: pointer;
604
+ display: flex;
605
+ align-items: center;
606
+ justify-content: center;
607
+ font-size: 12px;
608
+ line-height: 1;
609
+ padding: 0;
610
+ transition: transform 0.2s;
611
+ }
612
+
613
+ .file-tag .remove-file:hover {
614
+ transform: scale(1.1);
615
+ }
616
+
617
+ .file-uploading {
618
+ opacity: 0.6;
619
+ }
620
+
621
+ .file-uploading .file-name::after {
622
+ content: ' (上传中...)';
623
+ color: var(--text-muted);
624
+ }
625
+
626
+ .upload-progress {
627
+ position: absolute;
628
+ bottom: 0;
629
+ left: 0;
630
+ height: 3px;
631
+ background: var(--primary);
632
+ border-radius: 0 0 20px 20px;
633
+ transition: width 0.3s;
634
+ }
635
+ </style>
636
+ </head>
637
+ <body>
638
+
639
+ <!-- 顶部栏 -->
640
+ <div class="header">
641
+ <h1>Business Gemini <span style="font-weight: 400; color: var(--text-muted); font-size: 0.8em; margin-left: 8px;">智能对话</span></h1>
642
+ <div class="header-controls">
643
+ <div class="model-selector">
644
+ <label for="modelSelect">模型:</label>
645
+ <select id="modelSelect">
646
+ <option value="gemini-enterprise">加载中...</option>
647
+ </select>
648
+ </div>
649
+ <div class="model-selector">
650
+ <label for="accountSelect">指定账号:</label>
651
+ <select id="accountSelect">
652
+ <option value="">自动轮询</option>
653
+ </select>
654
+ </div>
655
+ <div class="mode-switch">
656
+ <span>非流式</span>
657
+ <label class="switch">
658
+ <input type="checkbox" id="streamMode" checked>
659
+ <span class="slider"></span>
660
+ </label>
661
+ <span>流式</span>
662
+ </div>
663
+ <button class="theme-toggle clear" onclick="clearChat()" title="清空对话">
664
+ 🗑️ 清空
665
+ </button>
666
+ <button class="theme-toggle home" onclick="window.location.href='./'" title="返回首页">
667
+ <span>🏠&nbsp;返回首页</span>
668
+ </button>
669
+ <button class="theme-toggle" onclick="toggleTheme()" title="切换主题">
670
+ <span id="themeIcon">☀️</span>
671
+ </button>
672
+ </div>
673
+ </div>
674
+
675
+ <!-- 主容器 -->
676
+ <div class="main-container">
677
+ <!-- 左侧会话列表 -->
678
+ <div class="session-sidebar">
679
+ <div class="session-header">
680
+ <h3>会话列表</h3>
681
+ <button class="new-session-btn" onclick="createNewSession()">+ 新建</button>
682
+ </div>
683
+ <div class="session-list" id="sessionList">
684
+ <!-- 会话项会动态插入 -->
685
+ </div>
686
+ </div>
687
+
688
+ <!-- 聊天主区域 -->
689
+ <div class="chat-main">
690
+ <!-- 聊天内容区 -->
691
+ <div class="chat-container" id="chatContainer">
692
+ <!-- 消息会通过 JS 动态插入到这里 -->
693
+ </div>
694
+
695
+ <!-- 修改底部输入区,添加文件上传按钮 -->
696
+ <div class="input-area">
697
+ <div class="uploaded-files-container" id="uploadedFilesContainer" style="display: none;">
698
+ <div class="uploaded-files" id="uploadedFiles">
699
+ <!-- 已上传的文件标签会动态插入这里 -->
700
+ </div>
701
+ </div>
702
+ <div class="input-wrapper">
703
+ <input type="file" id="fileInput" multiple accept="*">
704
+ <button class="file-upload-btn" id="uploadBtn" onclick="document.getElementById('fileInput').click()" title="上传文件">
705
+ 📎
706
+ </button>
707
+ <textarea id="userInput" placeholder="输入消息与 Business Gemini 对话..." onkeydown="handleKeyDown(event)"></textarea>
708
+ <button class="send-btn" id="sendBtn" onclick="sendMessage()">➤</button>
709
+ </div>
710
+ </div>
711
+ </div>
712
+ </div>
713
+
714
+ <script>
715
+ console.log('JavaScript 开始加载...');
716
+ // API 基础 URL
717
+ const API_BASE = '.';
718
+
719
+ // ==================== 全局状态 ====================
720
+ let chatHistory = [];
721
+ let isLoading = false;
722
+ let currentAIBubble = null;
723
+ let abortController = null;
724
+ let uploadedFiles = []; // 存储已上传的文件信息 {id, name, gemini_file_id}
725
+
726
+ // ==================== 会话管理状态 ====================
727
+ let sessions = []; // 所有会话列表
728
+ let currentSessionId = null; // 当前会话ID
729
+ const SESSIONS_STORAGE_KEY = 'chat_sessions';
730
+ const CURRENT_SESSION_KEY = 'current_session_id';
731
+
732
+ // ==================== 会话管理函数 ====================
733
+ // 生成唯一ID
734
+ function generateId() {
735
+ return Date.now().toString(36) + Math.random().toString(36).substr(2);
736
+ }
737
+
738
+ // 加载所有会话
739
+ function loadSessions() {
740
+ try {
741
+ const saved = localStorage.getItem(SESSIONS_STORAGE_KEY);
742
+ sessions = saved ? JSON.parse(saved) : [];
743
+ currentSessionId = localStorage.getItem(CURRENT_SESSION_KEY);
744
+
745
+ // 如果没有会话,创建一个默认会话
746
+ if (sessions.length === 0) {
747
+ createNewSession(true);
748
+ } else {
749
+ // 如果当前会话ID无效,选择第一个会话
750
+ if (!currentSessionId || !sessions.find(s => s.id === currentSessionId)) {
751
+ currentSessionId = sessions[0].id;
752
+ localStorage.setItem(CURRENT_SESSION_KEY, currentSessionId);
753
+ }
754
+ }
755
+
756
+ renderSessionList();
757
+ loadCurrentSessionHistory();
758
+ } catch (error) {
759
+ console.error('加载会话失败:', error);
760
+ sessions = [];
761
+ createNewSession(true);
762
+ }
763
+ }
764
+
765
+ // 保存所有会话
766
+ function saveSessions() {
767
+ try {
768
+ localStorage.setItem(SESSIONS_STORAGE_KEY, JSON.stringify(sessions));
769
+ localStorage.setItem(CURRENT_SESSION_KEY, currentSessionId);
770
+ } catch (error) {
771
+ console.error('保存会话失败:', error);
772
+ }
773
+ }
774
+
775
+ // 创建新会话
776
+ function createNewSession(isInit = false) {
777
+ const newSession = {
778
+ id: generateId(),
779
+ name: `新会话 ${sessions.length + 1}`,
780
+ history: [],
781
+ createdAt: Date.now(),
782
+ updatedAt: Date.now()
783
+ };
784
+ sessions.unshift(newSession);
785
+ currentSessionId = newSession.id;
786
+ chatHistory = [];
787
+
788
+ if (!isInit) {
789
+ saveSessions();
790
+ renderSessionList();
791
+ renderChatHistory();
792
+ }
793
+ }
794
+
795
+ // 切换会话
796
+ function switchSession(sessionId) {
797
+ if (sessionId === currentSessionId) return;
798
+
799
+ // 保存当前会话的历史
800
+ saveCurrentSessionHistory();
801
+
802
+ currentSessionId = sessionId;
803
+ localStorage.setItem(CURRENT_SESSION_KEY, currentSessionId);
804
+
805
+ loadCurrentSessionHistory();
806
+ renderSessionList();
807
+ renderChatHistory();
808
+
809
+ // 滚动到底部
810
+ setTimeout(() => {
811
+ const container = document.getElementById('chatContainer');
812
+ container.scrollTop = container.scrollHeight;
813
+ }, 100);
814
+ }
815
+
816
+ // 保存当前会话历史
817
+ function saveCurrentSessionHistory() {
818
+ const session = sessions.find(s => s.id === currentSessionId);
819
+ if (session) {
820
+ session.history = [...chatHistory];
821
+ session.updatedAt = Date.now();
822
+ // 如果有消息,用第一条用户消息作为会话名称
823
+ if (chatHistory.length > 0 && session.name.startsWith('新会话')) {
824
+ const firstUserMsg = chatHistory.find(m => m.role === 'user');
825
+ if (firstUserMsg) {
826
+ const content = typeof firstUserMsg.content === 'string'
827
+ ? firstUserMsg.content
828
+ : firstUserMsg.content.find(c => c.type === 'text')?.text || '';
829
+ session.name = content.substring(0, 20) + (content.length > 20 ? '...' : '');
830
+ }
831
+ }
832
+ saveSessions();
833
+ }
834
+ }
835
+
836
+ // 加载当前会话历史
837
+ function loadCurrentSessionHistory() {
838
+ const session = sessions.find(s => s.id === currentSessionId);
839
+ if (session) {
840
+ chatHistory = [...session.history];
841
+ } else {
842
+ chatHistory = [];
843
+ }
844
+ }
845
+
846
+ // 渲染会话列表
847
+ function renderSessionList() {
848
+ const listContainer = document.getElementById('sessionList');
849
+ listContainer.innerHTML = '';
850
+
851
+ sessions.forEach(session => {
852
+ const item = document.createElement('div');
853
+ item.className = `session-item ${session.id === currentSessionId ? 'active' : ''}`;
854
+ item.innerHTML = `
855
+ <span class="session-name" title="${escapeHtml(session.name)}">${escapeHtml(session.name)}</span>
856
+ <div class="session-actions">
857
+ <button class="session-action-btn" onclick="event.stopPropagation(); renameSession('${session.id}')" title="重命名">✏️</button>
858
+ <button class="session-action-btn" onclick="event.stopPropagation(); deleteSession('${session.id}')" title="删除">🗑️</button>
859
+ </div>
860
+ `;
861
+ item.onclick = () => switchSession(session.id);
862
+ listContainer.appendChild(item);
863
+ });
864
+ }
865
+
866
+ // 重命名会话
867
+ function renameSession(sessionId) {
868
+ const session = sessions.find(s => s.id === sessionId);
869
+ if (!session) return;
870
+
871
+ const newName = prompt('请输入新的会话名称:', session.name);
872
+ if (newName && newName.trim()) {
873
+ session.name = newName.trim();
874
+ session.updatedAt = Date.now();
875
+ saveSessions();
876
+ renderSessionList();
877
+ }
878
+ }
879
+
880
+ // 删除会话
881
+ function deleteSession(sessionId) {
882
+ if (sessions.length <= 1) {
883
+ alert('至少需要保留一个会话');
884
+ return;
885
+ }
886
+
887
+ if (!confirm('确定要删除这个会话吗?')) return;
888
+
889
+ const index = sessions.findIndex(s => s.id === sessionId);
890
+ if (index === -1) return;
891
+
892
+ sessions.splice(index, 1);
893
+
894
+ // 如果删除的是当前会话,切换到第一个会话
895
+ if (sessionId === currentSessionId) {
896
+ currentSessionId = sessions[0].id;
897
+ loadCurrentSessionHistory();
898
+ renderChatHistory();
899
+ }
900
+
901
+ saveSessions();
902
+ renderSessionList();
903
+ }
904
+
905
+ // ==================== 获取模型列表 ====================
906
+ async function loadModelList() {
907
+ try {
908
+ const response = await fetch(`${API_BASE}/api/models`);
909
+ if (!response.ok) {
910
+ throw new Error('获取模型列表失败');
911
+ }
912
+ const data = await response.json();
913
+ const models = data.models || [];
914
+
915
+ const select = document.getElementById('modelSelect');
916
+ select.innerHTML = ''; // 清空现有选项
917
+
918
+ if (models.length === 0) {
919
+ select.innerHTML = '<option value="gemini-enterprise">gemini-enterprise</option>';
920
+ } else {
921
+ models.forEach(model => {
922
+ const option = document.createElement('option');
923
+ option.value = model.id || model.name;
924
+ option.textContent = model.name || model.id;
925
+ select.appendChild(option);
926
+ });
927
+ }
928
+
929
+ // 从localStorage恢复上次选择的模型
930
+ const savedModel = localStorage.getItem('selectedModel');
931
+ if (savedModel && select.querySelector(`option[value="${savedModel}"]`)) {
932
+ select.value = savedModel;
933
+ }
934
+
935
+ // 监听模型选择变化,保存到localStorage
936
+ select.addEventListener('change', () => {
937
+ localStorage.setItem('selectedModel', select.value);
938
+ });
939
+ } catch (error) {
940
+ console.error('加载模型��表失败:', error);
941
+ // 失败时使用默认模型
942
+ const select = document.getElementById('modelSelect');
943
+ select.innerHTML = '<option value="gemini-enterprise">gemini-enterprise</option>';
944
+ }
945
+ }
946
+
947
+ // ==================== 获取当前选中的模型 ====================
948
+ function getSelectedModel() {
949
+ return document.getElementById('modelSelect').value || 'gemini-enterprise';
950
+ }
951
+
952
+ // ==================== 获取账号列表 ====================
953
+ async function loadAccountList() {
954
+ try {
955
+ const response = await fetch(`${API_BASE}/api/accounts`);
956
+ if (!response.ok) throw new Error('获取账号列表失败');
957
+ const data = await response.json();
958
+ const accounts = data.accounts || [];
959
+ const select = document.getElementById('accountSelect');
960
+ select.innerHTML = '<option value="">自动轮询</option>';
961
+ accounts.filter(a => a.available).forEach(account => {
962
+ const option = document.createElement('option');
963
+ option.value = account.id;
964
+ option.textContent = account.csesidx ? `账号${account.id} (${account.csesidx})` : `账号${account.id}`;
965
+ select.appendChild(option);
966
+ });
967
+ } catch (error) {
968
+ console.error('加载账号列表失败:', error);
969
+ }
970
+ }
971
+
972
+ // ==================== 获取当前选中的账号 ====================
973
+ function getSelectedAccount() {
974
+ return document.getElementById('accountSelect').value || null;
975
+ }
976
+
977
+ // ==================== 初始化 ====================
978
+ window.onload = () => {
979
+ console.log('页面加载完成,开始初始化...');
980
+ loadSessions(); // 加载会话列表(会自动加载当前会话历史)
981
+ loadModelList(); // 加载模型列表
982
+ loadAccountList(); // 加载账号列表
983
+ if (chatHistory.length === 0) {
984
+ addMessage('ai', '你好!有什么我可以帮你的吗?');
985
+ } else {
986
+ renderChatHistory();
987
+ }
988
+
989
+ // 初始化文件上传事件监听
990
+ document.getElementById('fileInput').addEventListener('change', handleFileSelect);
991
+
992
+ // 确保页面加载后滚动到底部
993
+ setTimeout(() => {
994
+ const container = document.getElementById('chatContainer');
995
+ container.scrollTop = container.scrollHeight;
996
+ }, 100);
997
+ };
998
+
999
+ // ==================== 主题切换 ====================
1000
+ function toggleTheme() {
1001
+ const html = document.documentElement;
1002
+ const currentTheme = html.getAttribute('data-theme');
1003
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
1004
+ html.setAttribute('data-theme', newTheme);
1005
+ document.getElementById('themeIcon').textContent = newTheme === 'dark' ? '🌙' : '☀️';
1006
+ localStorage.setItem('theme', newTheme);
1007
+ }
1008
+
1009
+ const savedTheme = localStorage.getItem('theme') || 'light';
1010
+ document.documentElement.setAttribute('data-theme', savedTheme);
1011
+ document.getElementById('themeIcon').textContent = savedTheme === 'dark' ? '🌙' : '☀️';
1012
+
1013
+ // ==================== 键盘事件处理 ====================
1014
+ function handleKeyDown(event) {
1015
+ if (event.keyCode === 13 && !event.shiftKey) {
1016
+ event.preventDefault();
1017
+ sendMessage();
1018
+ }
1019
+ }
1020
+
1021
+ // ==================== 发送消息 ====================
1022
+ async function sendMessage() {
1023
+ console.log('sendMessage 被调用');
1024
+ const input = document.getElementById('userInput');
1025
+ const text = input.value.trim();
1026
+ console.log('输入内容:', text, '加载状态:', isLoading);
1027
+ if (!text || isLoading) {
1028
+ console.log('条件不满足,返回');
1029
+ return;
1030
+ }
1031
+
1032
+ // 获取已上传的文件信息
1033
+ const attachments = uploadedFiles.map(f => ({
1034
+ name: f.name,
1035
+ isImage: f.isImage,
1036
+ previewUrl: f.previewUrl || null
1037
+ }));
1038
+
1039
+ // 添加用户消息(包含附件)
1040
+ addMessage('user', text, attachments);
1041
+ input.value = '';
1042
+
1043
+ // 设置加载状态
1044
+ setLoading(true);
1045
+
1046
+ // 获取流式模式设置
1047
+ const isStream = document.getElementById('streamMode').checked;
1048
+
1049
+ try {
1050
+ if (isStream) {
1051
+ await sendStreamRequest(text);
1052
+ } else {
1053
+ await sendNonStreamRequest(text);
1054
+ }
1055
+ } catch (error) {
1056
+ console.error('请求失败:', error);
1057
+ if (error.name !== 'AbortError') {
1058
+ addErrorMessage('请求失败: ' + error.message);
1059
+ }
1060
+ } finally {
1061
+ setLoading(false);
1062
+ // 发送成功后清空已上传的文件
1063
+ clearUploadedFiles();
1064
+ }
1065
+ }
1066
+
1067
+ // ==================== 流式请求 ====================
1068
+ async function sendStreamRequest(text) {
1069
+ // 显示等待动画
1070
+ const typingId = showTypingIndicator();
1071
+
1072
+ let aiMessageId = null;
1073
+ let fullContent = '';
1074
+
1075
+ abortController = new AbortController();
1076
+ console.log('开始发送流式请求...');
1077
+
1078
+ const response = await fetch(`${API_BASE}/v1/chat/completions`, {
1079
+ method: 'POST',
1080
+ headers: {
1081
+ 'Content-Type': 'application/json'
1082
+ },
1083
+ body: JSON.stringify({
1084
+ model: getSelectedModel(),
1085
+ messages: buildMessages(text),
1086
+ stream: true,
1087
+ account_id: getSelectedAccount()
1088
+ }),
1089
+ signal: abortController.signal
1090
+ });
1091
+
1092
+ if (!response.ok) {
1093
+ const errorData = await response.json();
1094
+ throw new Error(errorData.error || '请求失败');
1095
+ }
1096
+
1097
+ const reader = response.body.getReader();
1098
+ const decoder = new TextDecoder();
1099
+
1100
+ while (true) {
1101
+ const { done, value } = await reader.read();
1102
+ if (done) break;
1103
+
1104
+ const chunk = decoder.decode(value, { stream: true });
1105
+ const lines = chunk.split('\n');
1106
+
1107
+ for (const line of lines) {
1108
+ if (line.startsWith('data: ')) {
1109
+ const data = line.slice(6);
1110
+ if (data === '[DONE]') {
1111
+ // 流式结束
1112
+ break;
1113
+ }
1114
+ try {
1115
+ const parsed = JSON.parse(data);
1116
+ const content = parsed.choices?.[0]?.delta?.content;
1117
+ const accountCsesidx = parsed.account_csesidx;
1118
+ if (content) {
1119
+ // 收到第一个内容时,移除等待动画并创建AI消息气泡
1120
+ if (!aiMessageId) {
1121
+ removeTypingIndicator(typingId);
1122
+ aiMessageId = createAIBubble();
1123
+ }
1124
+ fullContent += content;
1125
+ updateAIBubble(aiMessageId, fullContent);
1126
+ }
1127
+ // 更新账号信息
1128
+ if (accountCsesidx && aiMessageId) {
1129
+ updateAIBubbleAccount(aiMessageId, accountCsesidx);
1130
+ }
1131
+ } catch (e) {
1132
+ // 忽略解析错误
1133
+ }
1134
+ }
1135
+ }
1136
+ }
1137
+
1138
+ // 如果没有收到任何内容,移除等待动画
1139
+ if (!aiMessageId) {
1140
+ removeTypingIndicator(typingId);
1141
+ }
1142
+
1143
+ // 保存到历史记录
1144
+ if (fullContent) {
1145
+ chatHistory.push({ role: 'ai', content: fullContent, time: new Date().toISOString() });
1146
+ saveChatHistory();
1147
+ }
1148
+ }
1149
+
1150
+ // ==================== 非流式请求 ====================
1151
+ async function sendNonStreamRequest(text) {
1152
+ // 显示加载指示器
1153
+ const loadingId = showTypingIndicator();
1154
+
1155
+ abortController = new AbortController();
1156
+
1157
+ const response = await fetch(`${API_BASE}/v1/chat/completions`, {
1158
+ method: 'POST',
1159
+ headers: {
1160
+ 'Content-Type': 'application/json'
1161
+ },
1162
+ body: JSON.stringify({
1163
+ model: getSelectedModel(),
1164
+ messages: buildMessages(text),
1165
+ stream: false,
1166
+ account_id: getSelectedAccount()
1167
+ }),
1168
+ signal: abortController.signal
1169
+ });
1170
+
1171
+ // 移除加载指示器
1172
+ removeTypingIndicator(loadingId);
1173
+
1174
+ if (!response.ok) {
1175
+ const errorData = await response.json();
1176
+ throw new Error(errorData.error || '请求失败');
1177
+ }
1178
+
1179
+ const data = await response.json();
1180
+ const content = data.choices?.[0]?.message?.content;
1181
+ const accountCsesidx = data.account_csesidx;
1182
+
1183
+ if (content) {
1184
+ // 使用createAIBubble以支持显示账号信息
1185
+ const aiMessageId = createAIBubble();
1186
+ updateAIBubble(aiMessageId, content);
1187
+ if (accountCsesidx) {
1188
+ updateAIBubbleAccount(aiMessageId, accountCsesidx);
1189
+ }
1190
+ // 保存到历史记录
1191
+ chatHistory.push({ role: 'ai', content: content, time: new Date().toISOString() });
1192
+ saveChatHistory();
1193
+ } else {
1194
+ addErrorMessage('未收到有效响应');
1195
+ }
1196
+ }
1197
+
1198
+ // ==================== 构建消息列表 ====================
1199
+ function buildMessages(currentText) {
1200
+ const messages = [];
1201
+
1202
+ // 添加历史消息(最近10条)
1203
+ const recentHistory = chatHistory.slice(-10);
1204
+ for (const msg of recentHistory) {
1205
+ messages.push({
1206
+ role: msg.role === 'ai' ? 'assistant' : 'user',
1207
+ content: msg.content
1208
+ });
1209
+ }
1210
+
1211
+ // 构建当前用户消息(支持文件)
1212
+ const fileIds = getUploadedFileIds();
1213
+ if (fileIds.length > 0) {
1214
+ // 使用OpenAI格式的content数组
1215
+ const contentParts = [];
1216
+
1217
+ // 添加文件引用
1218
+ for (const fileId of fileIds) {
1219
+ contentParts.push({
1220
+ type: 'file',
1221
+ file: { id: fileId }
1222
+ });
1223
+ }
1224
+
1225
+ // 添加文本内容
1226
+ contentParts.push({
1227
+ type: 'text',
1228
+ text: currentText
1229
+ });
1230
+
1231
+ messages.push({
1232
+ role: 'user',
1233
+ content: contentParts
1234
+ });
1235
+ } else {
1236
+ messages.push({
1237
+ role: 'user',
1238
+ content: currentText
1239
+ });
1240
+ }
1241
+
1242
+ return messages;
1243
+ }
1244
+
1245
+ // ==================== UI 操作函数 ====================
1246
+ function addMessage(role, content, attachments = []) {
1247
+ const container = document.getElementById('chatContainer');
1248
+ const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1249
+
1250
+ const rowDiv = document.createElement('div');
1251
+ rowDiv.className = `message-row ${role}`;
1252
+
1253
+ const avatarDiv = document.createElement('div');
1254
+ avatarDiv.className = `avatar ${role}`;
1255
+ avatarDiv.innerHTML = role === 'ai' ? '🤖' : '👤';
1256
+
1257
+ const contentWrapper = document.createElement('div');
1258
+ contentWrapper.className = 'message-content';
1259
+
1260
+ // 如果有附件,先显示附件
1261
+ if (attachments && attachments.length > 0) {
1262
+ const attachmentsContainer = document.createElement('div');
1263
+ attachmentsContainer.className = 'message-attachments';
1264
+ attachmentsContainer.style.cssText = 'display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px;';
1265
+
1266
+ for (const attachment of attachments) {
1267
+ if (attachment.isImage && attachment.previewUrl) {
1268
+ // 图片附件
1269
+ const img = document.createElement('img');
1270
+ img.src = attachment.previewUrl;
1271
+ img.style.cssText = 'max-width: 200px; max-height: 200px; border-radius: 8px; cursor: pointer; object-fit: cover;';
1272
+ img.title = attachment.name;
1273
+ img.onclick = function() {
1274
+ window.open(attachment.previewUrl, '_blank');
1275
+ };
1276
+ attachmentsContainer.appendChild(img);
1277
+ } else {
1278
+ // 非图片文件附件
1279
+ const fileTag = document.createElement('div');
1280
+ fileTag.style.cssText = 'display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: var(--primary-light); border-radius: 6px; font-size: 13px; color: var(--text-main);';
1281
+ fileTag.innerHTML = `<span>📄</span><span>${attachment.name}</span>`;
1282
+ attachmentsContainer.appendChild(fileTag);
1283
+ }
1284
+ }
1285
+ contentWrapper.appendChild(attachmentsContainer);
1286
+ }
1287
+
1288
+ const bubbleDiv = document.createElement('div');
1289
+ bubbleDiv.className = 'bubble';
1290
+ bubbleDiv.textContent = content;
1291
+
1292
+ const timeDiv = document.createElement('div');
1293
+ timeDiv.className = 'timestamp';
1294
+ timeDiv.innerText = time;
1295
+
1296
+ contentWrapper.appendChild(bubbleDiv);
1297
+ contentWrapper.appendChild(timeDiv);
1298
+
1299
+ rowDiv.appendChild(avatarDiv);
1300
+ rowDiv.appendChild(contentWrapper);
1301
+
1302
+ container.appendChild(rowDiv);
1303
+ container.scrollTop = container.scrollHeight;
1304
+
1305
+ // 保存到历史记录(包含附件)
1306
+ chatHistory.push({ role, content, attachments: attachments || [], time: new Date().toISOString() });
1307
+ saveChatHistory();
1308
+ }
1309
+
1310
+ function createAIBubble() {
1311
+ const container = document.getElementById('chatContainer');
1312
+ const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1313
+ const messageId = 'ai-msg-' + Date.now();
1314
+
1315
+ const rowDiv = document.createElement('div');
1316
+ rowDiv.className = 'message-row ai';
1317
+ rowDiv.id = messageId;
1318
+
1319
+ const avatarDiv = document.createElement('div');
1320
+ avatarDiv.className = 'avatar ai';
1321
+ avatarDiv.innerHTML = '🤖';
1322
+
1323
+ const contentWrapper = document.createElement('div');
1324
+ contentWrapper.className = 'message-content';
1325
+
1326
+ const bubbleDiv = document.createElement('div');
1327
+ bubbleDiv.className = 'bubble';
1328
+ bubbleDiv.id = messageId + '-bubble';
1329
+ bubbleDiv.textContent = '';
1330
+
1331
+ // 账号信息显示区域
1332
+ const accountDiv = document.createElement('div');
1333
+ accountDiv.className = 'account-info';
1334
+ accountDiv.id = messageId + '-account';
1335
+ accountDiv.style.cssText = 'font-size: 11px; color: var(--text-muted); margin-top: 4px;';
1336
+ accountDiv.textContent = '';
1337
+
1338
+ const timeDiv = document.createElement('div');
1339
+ timeDiv.className = 'timestamp';
1340
+ timeDiv.innerText = time;
1341
+
1342
+ contentWrapper.appendChild(bubbleDiv);
1343
+ contentWrapper.appendChild(accountDiv);
1344
+ contentWrapper.appendChild(timeDiv);
1345
+
1346
+ rowDiv.appendChild(avatarDiv);
1347
+ rowDiv.appendChild(contentWrapper);
1348
+
1349
+ container.appendChild(rowDiv);
1350
+ container.scrollTop = container.scrollHeight;
1351
+
1352
+ return messageId;
1353
+ }
1354
+
1355
+ // 更新AI消息的账号信息
1356
+ function updateAIBubbleAccount(messageId, accountCsesidx) {
1357
+ const accountDiv = document.getElementById(messageId + '-account');
1358
+ if (accountDiv && accountCsesidx) {
1359
+ accountDiv.textContent = '账号: ' + accountCsesidx;
1360
+ }
1361
+ }
1362
+
1363
+ function updateAIBubble(messageId, content) {
1364
+ const bubble = document.getElementById(messageId + '-bubble');
1365
+ if (bubble) {
1366
+ // 解析内容,将图片URL转换为图片元素
1367
+ bubble.innerHTML = parseContentWithImages(content);
1368
+ const container = document.getElementById('chatContainer');
1369
+ container.scrollTop = container.scrollHeight;
1370
+ }
1371
+ }
1372
+
1373
+ // 解析内容中的图片URL并转换为HTML
1374
+ function parseContentWithImages(content) {
1375
+ // 匹配图片URL的正则表达式(支持常见图片格式)
1376
+ const imageUrlRegex = /(https?:\/\/[^\s]+\.(?:png|jpg|jpeg|gif|webp|bmp|svg))/gi;
1377
+
1378
+ // 将内容按行分割处理
1379
+ const lines = content.split('\n');
1380
+ const processedLines = lines.map(line => {
1381
+ // 检查该行是否是纯图片URL
1382
+ const trimmedLine = line.trim();
1383
+ if (imageUrlRegex.test(trimmedLine) && trimmedLine.match(imageUrlRegex)?.[0] === trimmedLine) {
1384
+ // 重置正则表达式的lastIndex
1385
+ imageUrlRegex.lastIndex = 0;
1386
+ // 该行是纯图片URL,转换为图片元素
1387
+ return `<div class="ai-image-container"><img src="${escapeHtml(trimmedLine)}" alt="AI生成的图片" style="max-width: 300px; max-height: 300px; border-radius: 8px; cursor: pointer; margin: 8px 0;" onclick="window.open('${escapeHtml(trimmedLine)}', '_blank')" onerror="this.style.display='none'; this.nextSibling.style.display='inline';"><span style="display:none;">${escapeHtml(trimmedLine)}</span></div>`;
1388
+ }
1389
+ // 重置正则表达式的lastIndex
1390
+ imageUrlRegex.lastIndex = 0;
1391
+ // 普通文本行,转义HTML
1392
+ return escapeHtml(line);
1393
+ });
1394
+
1395
+ return processedLines.join('<br>');
1396
+ }
1397
+
1398
+ // HTML转义函数
1399
+ function escapeHtml(text) {
1400
+ const div = document.createElement('div');
1401
+ div.textContent = text;
1402
+ return div.innerHTML;
1403
+ }
1404
+
1405
+ function showTypingIndicator() {
1406
+ const container = document.getElementById('chatContainer');
1407
+ const indicatorId = 'typing-' + Date.now();
1408
+
1409
+ const rowDiv = document.createElement('div');
1410
+ rowDiv.className = 'message-row ai';
1411
+ rowDiv.id = indicatorId;
1412
+
1413
+ const avatarDiv = document.createElement('div');
1414
+ avatarDiv.className = 'avatar ai';
1415
+ avatarDiv.innerHTML = '🤖';
1416
+
1417
+ const contentWrapper = document.createElement('div');
1418
+ contentWrapper.className = 'message-content';
1419
+
1420
+ const indicator = document.createElement('div');
1421
+ indicator.className = 'typing-indicator';
1422
+ indicator.innerHTML = '<span></span><span></span><span></span>';
1423
+
1424
+ contentWrapper.appendChild(indicator);
1425
+ rowDiv.appendChild(avatarDiv);
1426
+ rowDiv.appendChild(contentWrapper);
1427
+
1428
+ container.appendChild(rowDiv);
1429
+ container.scrollTop = container.scrollHeight;
1430
+
1431
+ return indicatorId;
1432
+ }
1433
+
1434
+ function removeTypingIndicator(indicatorId) {
1435
+ const indicator = document.getElementById(indicatorId);
1436
+ if (indicator) {
1437
+ indicator.remove();
1438
+ }
1439
+ }
1440
+
1441
+ function addErrorMessage(message) {
1442
+ const container = document.getElementById('chatContainer');
1443
+
1444
+ const rowDiv = document.createElement('div');
1445
+ rowDiv.className = 'message-row ai';
1446
+
1447
+ const avatarDiv = document.createElement('div');
1448
+ avatarDiv.className = 'avatar ai';
1449
+ avatarDiv.innerHTML = '⚠️';
1450
+
1451
+ const contentWrapper = document.createElement('div');
1452
+ contentWrapper.className = 'message-content';
1453
+
1454
+ const errorDiv = document.createElement('div');
1455
+ errorDiv.className = 'error-message';
1456
+ errorDiv.textContent = message;
1457
+
1458
+ contentWrapper.appendChild(errorDiv);
1459
+ rowDiv.appendChild(avatarDiv);
1460
+ rowDiv.appendChild(contentWrapper);
1461
+
1462
+ container.appendChild(rowDiv);
1463
+ container.scrollTop = container.scrollHeight;
1464
+ }
1465
+
1466
+ function setLoading(loading) {
1467
+ isLoading = loading;
1468
+ const input = document.getElementById('userInput');
1469
+ const sendBtn = document.getElementById('sendBtn');
1470
+
1471
+ input.disabled = loading;
1472
+ sendBtn.disabled = loading;
1473
+ sendBtn.innerHTML = loading ? '⏳' : '➤';
1474
+ }
1475
+
1476
+ // ==================== 对话历史管理 ====================
1477
+ function saveChatHistory() {
1478
+ // 保存到当前会话
1479
+ saveCurrentSessionHistory();
1480
+ }
1481
+
1482
+ function loadChatHistory() {
1483
+ // 从当前会话加载(由loadSessions调用)
1484
+ const session = sessions.find(s => s.id === currentSessionId);
1485
+ if (session && session.history) {
1486
+ chatHistory = session.history;
1487
+ } else {
1488
+ chatHistory = [];
1489
+ }
1490
+ }
1491
+
1492
+ function renderChatHistory() {
1493
+ const container = document.getElementById('chatContainer');
1494
+ container.innerHTML = '';
1495
+
1496
+ for (const msg of chatHistory) {
1497
+ const time = new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1498
+
1499
+ const rowDiv = document.createElement('div');
1500
+ rowDiv.className = `message-row ${msg.role}`;
1501
+
1502
+ const avatarDiv = document.createElement('div');
1503
+ avatarDiv.className = `avatar ${msg.role}`;
1504
+ avatarDiv.innerHTML = msg.role === 'ai' ? '🤖' : '👤';
1505
+
1506
+ const contentWrapper = document.createElement('div');
1507
+ contentWrapper.className = 'message-content';
1508
+
1509
+ // 如果有附件,先显示附件(兼容旧的images字段)
1510
+ const attachments = msg.attachments || (msg.images ? msg.images.map(url => ({ isImage: true, previewUrl: url, name: '图片' })) : []);
1511
+ if (attachments && attachments.length > 0) {
1512
+ const attachmentsContainer = document.createElement('div');
1513
+ attachmentsContainer.className = 'message-attachments';
1514
+ attachmentsContainer.style.cssText = 'display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px;';
1515
+
1516
+ for (const attachment of attachments) {
1517
+ if (attachment.isImage && attachment.previewUrl) {
1518
+ // 图片附件
1519
+ const img = document.createElement('img');
1520
+ img.src = attachment.previewUrl;
1521
+ img.style.cssText = 'max-width: 200px; max-height: 200px; border-radius: 8px; cursor: pointer; object-fit: cover;';
1522
+ img.title = attachment.name || '图片';
1523
+ img.onclick = function() {
1524
+ window.open(attachment.previewUrl, '_blank');
1525
+ };
1526
+ attachmentsContainer.appendChild(img);
1527
+ } else {
1528
+ // 非图片文件附件
1529
+ const fileTag = document.createElement('div');
1530
+ fileTag.style.cssText = 'display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: var(--primary-light); border-radius: 6px; font-size: 13px; color: var(--text-main);';
1531
+ fileTag.innerHTML = `<span>📄</span><span>${attachment.name || '文件'}</span>`;
1532
+ attachmentsContainer.appendChild(fileTag);
1533
+ }
1534
+ }
1535
+ contentWrapper.appendChild(attachmentsContainer);
1536
+ }
1537
+
1538
+ const bubbleDiv = document.createElement('div');
1539
+ bubbleDiv.className = 'bubble';
1540
+ // AI消息需要解析图片URL
1541
+ if (msg.role === 'ai') {
1542
+ bubbleDiv.innerHTML = parseContentWithImages(msg.content);
1543
+ } else {
1544
+ bubbleDiv.textContent = msg.content;
1545
+ }
1546
+
1547
+ const timeDiv = document.createElement('div');
1548
+ timeDiv.className = 'timestamp';
1549
+ timeDiv.innerText = time;
1550
+
1551
+ contentWrapper.appendChild(bubbleDiv);
1552
+ contentWrapper.appendChild(timeDiv);
1553
+
1554
+ rowDiv.appendChild(avatarDiv);
1555
+ rowDiv.appendChild(contentWrapper);
1556
+
1557
+ container.appendChild(rowDiv);
1558
+ }
1559
+
1560
+ container.scrollTop = container.scrollHeight;
1561
+ }
1562
+
1563
+ function clearChat() {
1564
+ if (confirm('确定要清空当前会话的对话记录吗?')) {
1565
+ chatHistory = [];
1566
+ saveChatHistory(); // 保存到当前会话
1567
+ document.getElementById('chatContainer').innerHTML = '';
1568
+ addMessage('ai', '对话已清空。有什么我可以帮你的吗?');
1569
+ }
1570
+ }
1571
+
1572
+ // ==================== 文件上传功能 ====================
1573
+ function handleFileSelect(event) {
1574
+ const files = event.target.files;
1575
+ if (!files || files.length === 0) return;
1576
+
1577
+ for (const file of files) {
1578
+ uploadFile(file);
1579
+ }
1580
+
1581
+ // 清空input以便可以重复选择同一文件
1582
+ event.target.value = '';
1583
+ }
1584
+
1585
+ async function uploadFile(file) {
1586
+ const uploadBtn = document.getElementById('uploadBtn');
1587
+ const filesContainer = document.getElementById('uploadedFilesContainer');
1588
+ const filesList = document.getElementById('uploadedFiles');
1589
+
1590
+ // 显示文件容器
1591
+ filesContainer.style.display = 'block';
1592
+
1593
+ // 创建文件标签(上传中状态)
1594
+ const fileTag = document.createElement('div');
1595
+ fileTag.className = 'file-tag file-uploading';
1596
+ fileTag.id = 'file-' + Date.now();
1597
+ fileTag.innerHTML = `
1598
+ <span class="file-icon">📄</span>
1599
+ <span class="file-name">${file.name}</span>
1600
+ `;
1601
+ filesList.appendChild(fileTag);
1602
+
1603
+ try {
1604
+ const formData = new FormData();
1605
+ formData.append('file', file);
1606
+ formData.append('purpose', 'assistants');
1607
+
1608
+ const response = await fetch(`${API_BASE}/v1/files`, {
1609
+ method: 'POST',
1610
+ body: formData
1611
+ });
1612
+
1613
+ if (!response.ok) {
1614
+ const errorData = await response.json();
1615
+ throw new Error(errorData.error?.message || '上传失败');
1616
+ }
1617
+
1618
+ const data = await response.json();
1619
+
1620
+ // 更新文件标签为成功状态
1621
+ fileTag.className = 'file-tag';
1622
+ fileTag.innerHTML = `
1623
+ <span class="file-icon">📄</span>
1624
+ <span class="file-name">${file.name}</span>
1625
+ <button class="remove-file" onclick="removeFile('${fileTag.id}', '${data.id}')">×</button>
1626
+ `;
1627
+
1628
+ // 保存文件信息(包含图片预览)
1629
+ const fileInfo = {
1630
+ tagId: fileTag.id,
1631
+ id: data.id,
1632
+ name: file.name,
1633
+ gemini_file_id: data.gemini_file_id,
1634
+ isImage: file.type.startsWith('image/'),
1635
+ previewUrl: null
1636
+ };
1637
+
1638
+ // 如果是图片,生成预览URL(使用Promise确保同步完成)
1639
+ if (fileInfo.isImage) {
1640
+ await new Promise((resolve) => {
1641
+ const reader = new FileReader();
1642
+ reader.onload = function(e) {
1643
+ fileInfo.previewUrl = e.target.result;
1644
+ resolve();
1645
+ };
1646
+ reader.readAsDataURL(file);
1647
+ });
1648
+ }
1649
+
1650
+ uploadedFiles.push(fileInfo);
1651
+
1652
+ // 更新上传按钮状态
1653
+ updateUploadBtnState();
1654
+
1655
+ } catch (error) {
1656
+ console.error('文件上传失败:', error);
1657
+ fileTag.remove();
1658
+ alert('文件上传失败: ' + error.message);
1659
+
1660
+ // 如果没有文件了,隐藏容器
1661
+ if (uploadedFiles.length === 0) {
1662
+ filesContainer.style.display = 'none';
1663
+ }
1664
+ }
1665
+ }
1666
+
1667
+ function removeFile(tagId, fileId) {
1668
+ // 从DOM中移除
1669
+ const fileTag = document.getElementById(tagId);
1670
+ if (fileTag) {
1671
+ fileTag.remove();
1672
+ }
1673
+
1674
+ // 从数组中移除
1675
+ uploadedFiles = uploadedFiles.filter(f => f.tagId !== tagId);
1676
+
1677
+ // 更新UI状态
1678
+ updateUploadBtnState();
1679
+
1680
+ // 如果没有文件了,隐藏容器
1681
+ if (uploadedFiles.length === 0) {
1682
+ document.getElementById('uploadedFilesContainer').style.display = 'none';
1683
+ }
1684
+
1685
+ // 可选:调用删除API
1686
+ fetch(`${API_BASE}/v1/files/${fileId}`, { method: 'DELETE' }).catch(console.error);
1687
+ }
1688
+
1689
+ function getUploadedFileIds() {
1690
+ return uploadedFiles.map(f => f.id);
1691
+ }
1692
+
1693
+ function clearUploadedFiles() {
1694
+ uploadedFiles = [];
1695
+ document.getElementById('uploadedFiles').innerHTML = '';
1696
+ document.getElementById('uploadedFilesContainer').style.display = 'none';
1697
+ updateUploadBtnState();
1698
+ }
1699
+
1700
+ function updateUploadBtnState() {
1701
+ const uploadBtn = document.getElementById('uploadBtn');
1702
+ if (uploadedFiles.length > 0) {
1703
+ uploadBtn.classList.add('has-files');
1704
+ uploadBtn.title = `已上传 ${uploadedFiles.length} 个文件`;
1705
+ } else {
1706
+ uploadBtn.classList.remove('has-files');
1707
+ uploadBtn.title = '上传文件';
1708
+ }
1709
+ }
1710
+ </script>
1711
+ </body>
1712
+ </html>
docker-compose.yml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: business-gemini-new
2
+
3
+ services:
4
+ app:
5
+ build: .
6
+ container_name: business-gemini-new
7
+ restart: unless-stopped
8
+ ports:
9
+ - "8000:8000"
10
+ volumes:
11
+ - ./business_gemini_session.json:/app/business_gemini_session.json
12
+ - ./index.html:/app/index.html
13
+ - ./gemini.py:/app/gemini.py
14
+ environment:
15
+ - PYTHONUNBUFFERED=1
16
+ healthcheck:
17
+ test: [ "CMD", "python", "-c", "import requests; requests.get('http://localhost:8000/health', timeout=5)" ]
18
+ interval: 30s
19
+ timeout: 10s
20
+ retries: 3
21
+ start_period: 5s
22
+ networks:
23
+ - gemini-network
24
+
25
+ networks:
26
+ gemini-network:
27
+ driver: bridge
gemini.py ADDED
The diff for this file is too large to render. See raw diff
 
index.html ADDED
@@ -0,0 +1,2025 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN" data-theme="light">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Business Gemini Pool 管理控制台</title>
7
+ <style>
8
+ /* [OPTIMIZATION] 1. 全局样式优化与变量调整 */
9
+ :root {
10
+ /* 核心颜色保持不变 */
11
+ --primary: #4285f4;
12
+ --primary-hover: #3367d6;
13
+ --primary-light: rgba(66, 133, 244, 0.1);
14
+ --success: #34a853;
15
+ --success-light: rgba(52, 168, 83, 0.1);
16
+ --danger: #ea4335;
17
+ --danger-light: rgba(234, 67, 53, 0.1);
18
+ --warning: #fbbc04;
19
+ --warning-light: rgba(251, 188, 4, 0.1);
20
+
21
+ /* [NEW] 引入更精细的变量控制 */
22
+ --radius-sm: 6px;
23
+ --radius-md: 12px; /* 增大圆角,更柔和 */
24
+ --radius-lg: 16px;
25
+ --transition-ease: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); /* [NEW] 现代化的缓动函数 */
26
+
27
+ --font-main: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; /* [NEW] 引入更适合UI的字体 */
28
+ }
29
+
30
+ /* [OPTIMIZATION] 2. Light & Dark Theme 优化,增强对比度和质感 */
31
+ [data-theme="light"] {
32
+ --bg-color: #f7f8fc; /* 更柔和的背景色 */
33
+ --card-bg: #ffffff;
34
+ --text-main: #1f2328;
35
+ --text-muted: #656d76;
36
+ --border: #e4e7eb; /* 更浅的边框色 */
37
+ --hover-bg: #f2f3f5;
38
+ --input-bg: #ffffff;
39
+ --shadow-sm: 0 1px 2px 0 rgba(27, 31, 35, 0.04);
40
+ --shadow-md: 0 4px 8px 0 rgba(27, 31, 35, 0.06), 0 1px 2px 0 rgba(27, 31, 35, 0.05); /* 更柔和的阴影 */
41
+ --shadow-lg: 0 10px 20px 0 rgba(27, 31, 35, 0.07), 0 3px 6px 0 rgba(27, 31, 35, 0.05);
42
+ }
43
+
44
+ [data-theme="dark"] {
45
+ --bg-color: #1a1b1e;
46
+ --card-bg: #242528;
47
+ --text-main: #e8eaed;
48
+ --text-muted: #9aa0a6;
49
+ --border: #3a3c40;
50
+ --hover-bg: #303134;
51
+ --input-bg: #2f3033;
52
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
53
+ --shadow-md: 0 4px 8px 0 rgba(0, 0, 0, 0.15), 0 1px 2px 0 rgba(0, 0, 0, 0.1);
54
+ --shadow-lg: 0 10px 20px 0 rgba(0, 0, 0, 0.2), 0 3px 6px 0 rgba(0, 0, 0, 0.15);
55
+ }
56
+
57
+ * {
58
+ margin: 0;
59
+ padding: 0;
60
+ box-sizing: border-box;
61
+ }
62
+
63
+ body {
64
+ font-family: var(--font-main);
65
+ background-color: var(--bg-color);
66
+ color: var(--text-main);
67
+ min-height: 100vh;
68
+ transition: background-color 0.3s, color 0.3s;
69
+ -webkit-font-smoothing: antialiased;
70
+ -moz-osx-font-smoothing: grayscale;
71
+ }
72
+
73
+ .container {
74
+ max-width: 1400px;
75
+ margin: 0 auto;
76
+ padding: 32px; /* 增加页面内边距 */
77
+ }
78
+
79
+ /* [OPTIMIZATION] 3. Header 重新设计,更简洁大气 */
80
+ .header {
81
+ display: flex;
82
+ justify-content: space-between;
83
+ align-items: center;
84
+ margin-bottom: 32px;
85
+ /* 移除背景和阴影,使其融入页面 */
86
+ }
87
+
88
+ .header-left { display: flex; align-items: center; gap: 16px; }
89
+
90
+ .logo {
91
+ width: 44px;
92
+ height: 44px;
93
+ background: linear-gradient(135deg, #4285f4, #34a853, #fbbc04, #ea4335);
94
+ border-radius: var(--radius-md);
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ color: white;
99
+ font-weight: 600;
100
+ font-size: 22px;
101
+ transform: rotate(-10deg); /* [NEW] 增加一点趣味性 */
102
+ transition: var(--transition-ease);
103
+ }
104
+ .logo:hover { transform: rotate(0deg) scale(1.05); }
105
+
106
+ .header h1 {
107
+ font-size: 26px; /* 增大标题字号 */
108
+ font-weight: 600;
109
+ color: var(--text-main);
110
+ }
111
+
112
+ .header h1 span {
113
+ color: var(--text-muted);
114
+ font-weight: 400;
115
+ font-size: 16px;
116
+ margin-left: 10px;
117
+ }
118
+
119
+ .header-right { display: flex; align-items: center; gap: 16px; }
120
+
121
+ .status-indicator {
122
+ display: flex;
123
+ align-items: center;
124
+ gap: 8px;
125
+ padding: 8px 16px;
126
+ background: var(--success-light);
127
+ border: 1px solid rgba(52, 168, 83, 0.2);
128
+ border-radius: 50px; /* 改为胶囊形状 */
129
+ font-size: 14px;
130
+ color: var(--success);
131
+ font-weight: 500;
132
+ }
133
+ .status-indicator::before {
134
+ content: ''; width: 8px; height: 8px;
135
+ background: var(--success); border-radius: 50%;
136
+ animation: pulse 2s infinite;
137
+ }
138
+ @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(0.9); } }
139
+
140
+ .theme-toggle {
141
+ width: 44px; height: 44px; border: 1px solid var(--border);
142
+ background: var(--card-bg); border-radius: var(--radius-md);
143
+ cursor: pointer; display: flex; align-items: center; justify-content: center;
144
+ font-size: 20px; transition: var(--transition-ease);
145
+ }
146
+ .theme-toggle:hover { background: var(--hover-bg); border-color: var(--primary); transform: translateY(-2px); }
147
+
148
+ /* [OPTIMIZATION] 4. Tabs 重新设计,更现代、更 subtle */
149
+ .tabs {
150
+ display: flex;
151
+ gap: 16px;
152
+ border-bottom: 1px solid var(--border); /* 底部线条导航 */
153
+ margin-bottom: 32px;
154
+ }
155
+ .tab {
156
+ padding: 14px 4px; /* 减少水平padding,通过gap控制间距 */
157
+ border: none; border-bottom: 2px solid transparent;
158
+ background: transparent; color: var(--text-muted);
159
+ font-size: 15px; font-weight: 500;
160
+ cursor: pointer; border-radius: 0;
161
+ transition: var(--transition-ease);
162
+ display: flex; align-items: center; justify-content: center;
163
+ gap: 8px;
164
+ }
165
+ .tab:hover { color: var(--primary); }
166
+ .tab.active { color: var(--primary); border-bottom-color: var(--primary); }
167
+ .tab-icon { font-size: 20px; }
168
+
169
+
170
+ /* Status Badge */
171
+ .badge {
172
+ display: inline-flex;
173
+ align-items: center;
174
+ gap: 6px;
175
+ padding: 6px 12px;
176
+ border-radius: 20px;
177
+ font-size: 12px;
178
+ font-weight: 500;
179
+ }
180
+
181
+ .badge::before {
182
+ content: '';
183
+ width: 6px;
184
+ height: 6px;
185
+ border-radius: 50%;
186
+ }
187
+
188
+ .badge-success {
189
+ background: var(--success-light);
190
+ color: var(--success);
191
+ }
192
+
193
+ .badge-success::before {
194
+ background: var(--success);
195
+ }
196
+
197
+ .badge-danger {
198
+ background: var(--danger-light);
199
+ color: var(--danger);
200
+ }
201
+
202
+ .badge-danger::before {
203
+ background: var(--danger);
204
+ }
205
+
206
+ .cooldown-hint {
207
+ display: block;
208
+ color: var(--text-muted);
209
+ font-size: 12px;
210
+ margin-top: 4px;
211
+ }
212
+
213
+ .log-level-control {
214
+ display: flex;
215
+ align-items: center;
216
+ gap: 8px;
217
+ background: var(--card-bg);
218
+ border: 1px solid var(--border);
219
+ border-radius: var(--radius-md);
220
+ padding: 6px 10px;
221
+ }
222
+ .log-level-control label {
223
+ font-size: 12px;
224
+ color: var(--text-muted);
225
+ }
226
+ .log-level-select {
227
+ border: 1px solid var(--border);
228
+ background: var(--input-bg);
229
+ color: var(--text-main);
230
+ border-radius: var(--radius-sm);
231
+ padding: 6px 8px;
232
+ }
233
+
234
+ .token-actions {
235
+ display: flex;
236
+ gap: 8px;
237
+ flex-wrap: wrap;
238
+ margin-bottom: 12px;
239
+ }
240
+ .token-input {
241
+ flex: 1;
242
+ min-width: 240px;
243
+ }
244
+
245
+ .badge-warning {
246
+ background: var(--warning-light);
247
+ color: #b06000;
248
+ }
249
+
250
+ .badge-warning::before {
251
+ background: var(--warning);
252
+ }
253
+
254
+ /* [OPTIMIZATION] 5. 动画效果增强 */
255
+ .tab-content { display: none; }
256
+ .tab-content.active { display: block; animation: contentFadeIn 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards; }
257
+ @keyframes contentFadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
258
+
259
+ /* [OPTIMIZATION] 6. Card 样式优化 */
260
+ .card {
261
+ background: var(--card-bg);
262
+ border-radius: var(--radius-lg);
263
+ box-shadow: var(--shadow-md);
264
+ border: 1px solid var(--border);
265
+ margin-bottom: 32px;
266
+ overflow: hidden;
267
+ transition: var(--transition-ease);
268
+ }
269
+ .card:hover { border-color: var(--primary-light); box-shadow: var(--shadow-lg); }
270
+
271
+ .card-header {
272
+ display: flex; justify-content: space-between; align-items: center;
273
+ padding: 20px 24px; border-bottom: 1px solid var(--border);
274
+ }
275
+ .card-title {
276
+ font-size: 18px; font-weight: 600; color: var(--text-main);
277
+ display: flex; align-items: center; gap: 12px;
278
+ }
279
+ .card-title-icon { font-size: 22px; color: var(--text-muted); }
280
+ .card-body { padding: 24px; }
281
+
282
+ /* [OPTIMIZATION] 7. Button 样式优化 */
283
+ .btn {
284
+ padding: 10px 20px; border: none; border-radius: var(--radius-md);
285
+ cursor: pointer; font-size: 14px; font-weight: 500;
286
+ display: inline-flex; align-items: center; justify-content: center; gap: 8px;
287
+ transition: var(--transition-ease); text-decoration: none;
288
+ }
289
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
290
+ .btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: var(--shadow-md); }
291
+ .btn-primary { background: var(--primary); color: white; }
292
+ .btn-primary:hover:not(:disabled) { background: var(--primary-hover); }
293
+
294
+ .btn-outline {
295
+ background: transparent; color: var(--text-muted);
296
+ border: 1px solid var(--border);
297
+ }
298
+ .btn-outline:hover:not(:disabled) { border-color: var(--text-main); color: var(--text-main); }
299
+ /* 其他按钮颜色保持 */
300
+ .btn-success { background: var(--success-light); color: var(--success); border: 1px solid rgba(52, 168, 83, 0.2); }
301
+ .btn-success:hover:not(:disabled) { background: var(--success); color: white; border-color: var(--success); }
302
+ .btn-danger { background: var(--danger-light); color: var(--danger); border: 1px solid rgba(234, 67, 53, 0.2); }
303
+ .btn-danger:hover:not(:disabled) { background: var(--danger); color: white; border-color: var(--danger); }
304
+
305
+ .btn-sm { padding: 6px 14px; font-size: 13px; border-radius: var(--radius-sm); }
306
+ .btn-icon { width: 32px; height: 32px; padding: 0; border-radius: var(--radius-md); display: inline-flex; align-items: center; justify-content: center; vertical-align: middle; }
307
+ .btn-warning { background: #fff3cd; color: #856404; border: 1px solid rgba(133, 100, 4, 0.2); }
308
+ .btn-warning:hover:not(:disabled) { background: #ffc107; color: #212529; border-color: #ffc107; }
309
+
310
+ /* [OPTIMIZATION] 8. Table 样式优化,增强可读性 */
311
+ .table-container { overflow-x: auto; }
312
+ table { width: 100%; border-collapse: collapse; }
313
+ th {
314
+ text-align: left; padding: 16px 24px; font-size: 13px;
315
+ font-weight: 500; color: var(--text-muted); text-transform: uppercase;
316
+ letter-spacing: 0.5px; background: transparent; /* 移除背景色 */
317
+ border-bottom: 2px solid var(--border); /* 加粗底部边框 */
318
+ }
319
+ td {
320
+ padding: 18px 24px; border-bottom: 1px solid var(--border);
321
+ font-size: 14px; color: var(--text-main);
322
+ transition: background-color 0.2s;
323
+ }
324
+ tr:last-child td { border-bottom: none; }
325
+ tr:hover td { background: var(--hover-bg); }
326
+
327
+ /* [OPTIMIZATION] 9. Form 样式优化 */
328
+ .form-group {
329
+ display: flex;
330
+ flex-direction: column;
331
+ margin-bottom: 20px;
332
+ }
333
+ .form-group label,
334
+ .form-label {
335
+ display: block;
336
+ margin-bottom: 12px;
337
+ font-size: 14px;
338
+ font-weight: 600;
339
+ color: var(--text-main);
340
+ letter-spacing: 0.2px;
341
+ }
342
+ .form-group input, .form-group textarea, .form-group select,
343
+ .form-input,
344
+ .form-textarea {
345
+ width: 100%;
346
+ padding: 14px 16px;
347
+ border-radius: var(--radius-md);
348
+ border: 1px solid var(--border);
349
+ background: var(--bg);
350
+ color: var(--text-main);
351
+ font-size: 14px;
352
+ transition: var(--transition-ease);
353
+ box-sizing: border-box;
354
+ line-height: 1.5;
355
+ }
356
+ .form-textarea {
357
+ min-height: 90px;
358
+ resize: vertical;
359
+ font-family: inherit;
360
+ }
361
+ .form-group input:focus, .form-group textarea:focus, .form-group select:focus {
362
+ outline: none;
363
+ border-color: var(--primary);
364
+ box-shadow: 0 0 0 3px var(--primary-light), 0 1px 2px rgba(0,0,0,0.05) inset;
365
+ }
366
+ .form-group input:disabled {
367
+ background: var(--hover-bg);
368
+ color: var(--text-muted);
369
+ cursor: not-allowed;
370
+ }
371
+ .form-group small {
372
+ display: block;
373
+ margin-top: 6px;
374
+ font-size: 13px;
375
+ color: var(--text-muted);
376
+ }
377
+ .form-row {
378
+ display: grid;
379
+ grid-template-columns: 1fr 1fr;
380
+ gap: 24px;
381
+ }
382
+
383
+ /* Settings Section 样式 */
384
+ .settings-section {
385
+ background: var(--bg);
386
+ border: 1px solid var(--border);
387
+ border-radius: var(--radius-lg);
388
+ padding: 28px;
389
+ margin-bottom: 28px;
390
+ }
391
+ .settings-section:last-child {
392
+ margin-bottom: 0;
393
+ }
394
+ .settings-section h3 {
395
+ font-size: 17px;
396
+ font-weight: 600;
397
+ color: var(--text-main);
398
+ margin-bottom: 24px;
399
+ padding-bottom: 16px;
400
+ border-bottom: 1px solid var(--border);
401
+ display: flex;
402
+ align-items: center;
403
+ }
404
+ .settings-section .form-group {
405
+ margin-bottom: 24px;
406
+ }
407
+ .settings-section .form-group:last-of-type {
408
+ margin-bottom: 20px;
409
+ }
410
+
411
+ /* [OPTIMIZATION] 10. Modal 动画与样式优化 */
412
+ .modal {
413
+ display: flex; /* 改为flex,便于控制 */
414
+ align-items: center; justify-content: center;
415
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
416
+ background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(5px);
417
+ z-index: 1000; opacity: 0; visibility: hidden;
418
+ transition: opacity 0.3s, visibility 0.3s;
419
+ }
420
+ .modal.show { opacity: 1; visibility: visible; }
421
+ .modal-content {
422
+ background: var(--card-bg); border-radius: var(--radius-lg);
423
+ width: 600px; max-width: 90vw; max-height: 90vh;
424
+ overflow-y: auto; box-shadow: var(--shadow-lg);
425
+ transform: translateY(20px) scale(0.98);
426
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
427
+ }
428
+ .modal.show .modal-content { transform: translateY(0) scale(1); }
429
+ .modal-header { padding: 24px; border-bottom: 1px solid var(--border); }
430
+ .modal-header h3 { font-size: 20px; font-weight: 600; display: inline-block; }
431
+ .modal-close {
432
+ width: 36px; height: 36px; border: none; background: transparent;
433
+ color: var(--text-muted); cursor: pointer; border-radius: 50%;
434
+ display: flex; align-items: center; justify-content: center;
435
+ font-size: 22px; transition: var(--transition-ease);
436
+ float: right;
437
+ }
438
+ .modal-close:hover { background: var(--hover-bg); color: var(--text-main); transform: rotate(90deg); }
439
+ .modal-body { padding: 24px; }
440
+ .modal-footer {
441
+ display: flex; justify-content: flex-end; gap: 12px;
442
+ padding: 20px 24px; border-top: 1px solid var(--border);
443
+ background: var(--hover-bg);
444
+ border-bottom-left-radius: var(--radius-lg);
445
+ border-bottom-right-radius: var(--radius-lg);
446
+ }
447
+
448
+
449
+ /* [OPTIMIZATION] 11. Stats Card 优化 */
450
+ .stats-grid {
451
+ display: grid;
452
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
453
+ gap: 24px;
454
+ margin-bottom: 32px;
455
+ }
456
+ .stat-card {
457
+ background: var(--card-bg); border: 1px solid var(--border);
458
+ border-radius: var(--radius-lg); padding: 24px;
459
+ display: flex; flex-direction: column; /* 垂直布局 */
460
+ align-items: flex-start; gap: 16px;
461
+ transition: var(--transition-ease);
462
+ /* [NEW] 入场动画 */
463
+ opacity: 0;
464
+ transform: translateY(20px);
465
+ animation: fadeIn-up 0.5s ease-out forwards;
466
+ }
467
+ /* [NEW] Staggered Animation for Stats Cards */
468
+ .stat-card:nth-child(1) { animation-delay: 0.1s; }
469
+ .stat-card:nth-child(2) { animation-delay: 0.2s; }
470
+ .stat-card:nth-child(3) { animation-delay: 0.3s; }
471
+ .stat-card:nth-child(4) { animation-delay: 0.4s; }
472
+
473
+ @keyframes fadeIn-up {
474
+ to {
475
+ opacity: 1;
476
+ transform: translateY(0);
477
+ }
478
+ }
479
+ .stat-card:hover { transform: translateY(-5px); box-shadow: var(--shadow-md); border-color: var(--primary); }
480
+
481
+ .stat-info-top { display: flex; justify-content: space-between; align-items: center; width: 100%; }
482
+ .stat-info-top p { font-size: 14px; font-weight: 500; color: var(--text-muted); }
483
+
484
+ .stat-icon {
485
+ width: 40px; height: 40px; border-radius: var(--radius-md);
486
+ display: flex; align-items: center; justify-content: center; font-size: 20px;
487
+ }
488
+
489
+ .stat-info-bottom h3 { font-size: 32px; font-weight: 600; color: var(--text-main); }
490
+ .stat-icon.blue { background: var(--primary-light); color: var(--primary); }
491
+ .stat-icon.green { background: var(--success-light); color: var(--success); }
492
+ .stat-icon.red { background: var(--danger-light); color: var(--danger); }
493
+ .stat-icon.yellow { background: var(--warning-light); color: #b06000; }
494
+
495
+ /* 其他样式保持或微调 */
496
+ .badge {
497
+ padding: 5px 12px; border-radius: 50px;
498
+ font-size: 12px; font-weight: 500;
499
+ }
500
+ .empty-state { text-align: center; padding: 80px 20px; color: var(--text-muted); }
501
+ .empty-state-icon { font-size: 56px; margin-bottom: 20px; opacity: 0.4; }
502
+
503
+ .toast {
504
+ position: fixed; bottom: 32px; left: 50%;
505
+ transform: translateX(-50%) translateY(100px);
506
+ background: var(--card-bg); border: 1px solid var(--border);
507
+ border-radius: var(--radius-md); padding: 16px 24px;
508
+ box-shadow: var(--shadow-lg); min-width: 320px;
509
+ z-index: 2000; opacity: 0; visibility: hidden;
510
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
511
+ display: flex; align-items: center; gap: 12px;
512
+ }
513
+ .toast.show { transform: translateX(-50%) translateY(0); opacity: 1; visibility: visible; }
514
+
515
+ /* [NEW] SVG Icon Styles */
516
+ .icon {
517
+ width: 1em;
518
+ height: 1em;
519
+ stroke-width: 2;
520
+ fill: none;
521
+ stroke: currentColor;
522
+ stroke-linecap: round;
523
+ stroke-linejoin: round;
524
+ }
525
+
526
+ /* Responsive */
527
+ @media (max-width: 768px) {
528
+ .container { padding: 24px 16px; }
529
+ .header { flex-direction: column; gap: 24px; text-align: center; }
530
+ .tabs {
531
+ gap: 8px;
532
+ /* [NEW] 允许在移动端横向滚动 */
533
+ overflow-x: auto;
534
+ white-space: nowrap;
535
+ -ms-overflow-style: none; /* IE and Edge */
536
+ scrollbar-width: none; /* Firefox */
537
+ }
538
+ .tabs::-webkit-scrollbar { display: none; } /* Chrome, Safari, and Opera */
539
+ .tab { flex-shrink: 0; }
540
+ .form-row { grid-template-columns: 1fr; }
541
+ .stats-grid { gap: 16px; }
542
+ }
543
+ </style>
544
+ </head>
545
+ <body>
546
+ <!-- [NEW] SVG Icon Definitions -->
547
+ <svg width="0" height="0" style="display: none;">
548
+ <symbol id="icon-users" viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></symbol>
549
+ <symbol id="icon-robot" viewBox="0 0 24 24"><path d="M12 8V4H8"></path><rect x="4" y="12" width="16" height="8" rx="2"></rect><path d="M2 12h20"></path><path d="M12 12V8a4 4 0 0 0-4-4"></path></symbol>
550
+ <symbol id="icon-settings" viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 0 2l-.15.08a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l-.22-.38a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1 0-2l.15-.08a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></symbol>
551
+ <symbol id="icon-server" viewBox="0 0 24 24"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></symbol>
552
+ <symbol id="icon-list" viewBox="0 0 24 24"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></symbol>
553
+ <symbol id="icon-plus" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></symbol>
554
+ <symbol id="icon-check" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"></polyline></symbol>
555
+ <symbol id="icon-x" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></symbol>
556
+ <symbol id="icon-refresh" viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></symbol>
557
+ <symbol id="icon-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></symbol>
558
+ <symbol id="icon-moon" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></symbol>
559
+ <symbol id="icon-message" viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></symbol>
560
+ <symbol id="icon-play" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"></polygon></symbol>
561
+ <symbol id="icon-pause" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></symbol>
562
+ <symbol id="icon-zap" viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></symbol>
563
+ <symbol id="icon-key" viewBox="0 0 24 24"><path d="M21 2l-2 2"></path><path d="M9 6l-2 2"></path><circle cx="7.5" cy="15.5" r="5.5"></circle><path d="M21 2l-9.6 9.6"></path><path d="M15.5 7.5l3 3"></path><path d="M16 13l-3-3"></path></symbol>
564
+ </svg>
565
+
566
+ <div class="container">
567
+ <!-- Header -->
568
+ <header class="header">
569
+ <div class="header-left">
570
+ <div class="logo">G</div>
571
+ <h1>Business Gemini Pool <span>管理控制台</span></h1>
572
+ </div>
573
+ <div class="header-right">
574
+ <div class="status-indicator" id="serviceStatus">服务运行中</div>
575
+ <div class="log-level-control">
576
+ <label for="logLevelSelect">日志</label>
577
+ <select id="logLevelSelect" class="log-level-select" onchange="updateLogLevel(this.value)">
578
+ <option value="DEBUG">DEBUG</option>
579
+ <option value="INFO" selected>INFO</option>
580
+ <option value="ERROR">ERROR</option>
581
+ </select>
582
+ </div>
583
+ <button class="btn btn-outline" id="loginButton" style="padding: 8px 12px;" onclick="showLoginModal()">登录</button>
584
+ <a href="chat_history.html" class="btn btn-primary" style="padding: 8px 16px; font-size: 14px; text-decoration: none; display: flex; align-items: center; gap: 6px;" title="进入在线对话">
585
+ <svg class="icon" style="width: 16px; height: 16px;"><use xlink:href="#icon-message"></use></svg>
586
+ 在线对话
587
+ </a>
588
+ <button class="theme-toggle" onclick="toggleTheme()" title="切换主题">
589
+ <span id="themeIconContainer">
590
+ <svg class="icon"><use xlink:href="#icon-sun"></use></svg>
591
+ </span>
592
+ </button>
593
+ </div>
594
+ </header>
595
+
596
+ <!-- Tabs -->
597
+ <div class="tabs">
598
+ <button class="tab active" onclick="switchTab('accounts')">
599
+ <svg class="icon tab-icon"><use xlink:href="#icon-users"></use></svg>
600
+ 账号管理
601
+ </button>
602
+ <button class="tab" onclick="switchTab('models')">
603
+ <svg class="icon tab-icon"><use xlink:href="#icon-robot"></use></svg>
604
+ 模型管理
605
+ </button>
606
+ <button class="tab" onclick="switchTab('settings')">
607
+ <svg class="icon tab-icon"><use xlink:href="#icon-settings"></use></svg>
608
+ 系统设置
609
+ </button>
610
+ <button class="tab" onclick="switchTab('tokens')">
611
+ <svg class="icon tab-icon"><use xlink:href="#icon-key"></use></svg>
612
+ Token 管理
613
+ </button>
614
+ </div>
615
+
616
+ <!-- 账号管理 -->
617
+ <div id="accounts" class="tab-content active">
618
+ <!-- Stats -->
619
+ <div class="stats-grid">
620
+ <div class="stat-card">
621
+ <div class="stat-info-top">
622
+ <p>总账号数</p>
623
+ <div class="stat-icon blue"><svg class="icon"><use xlink:href="#icon-users"></use></svg></div>
624
+ </div>
625
+ <div class="stat-info-bottom">
626
+ <h3 id="totalAccounts">0</h3>
627
+ </div>
628
+ </div>
629
+ <div class="stat-card">
630
+ <div class="stat-info-top">
631
+ <p>可用账号</p>
632
+ <div class="stat-icon green"><svg class="icon"><use xlink:href="#icon-check"></use></svg></div>
633
+ </div>
634
+ <div class="stat-info-bottom">
635
+ <h3 id="availableAccounts">0</h3>
636
+ </div>
637
+ </div>
638
+ <div class="stat-card">
639
+ <div class="stat-info-top">
640
+ <p>不可用账号</p>
641
+ <div class="stat-icon red"><svg class="icon"><use xlink:href="#icon-x"></use></svg></div>
642
+ </div>
643
+ <div class="stat-info-bottom">
644
+ <h3 id="unavailableAccounts">0</h3>
645
+ </div>
646
+ </div>
647
+ <div class="stat-card">
648
+ <div class="stat-info-top">
649
+ <p>当前轮训索引</p>
650
+ <div class="stat-icon yellow"><svg class="icon"><use xlink:href="#icon-refresh"></use></svg></div>
651
+ </div>
652
+ <div class="stat-info-bottom">
653
+ <h3 id="currentIndex">0</h3>
654
+ </div>
655
+ </div>
656
+ </div>
657
+
658
+ <div class="card">
659
+ <div class="card-header">
660
+ <div class="card-title">
661
+ <svg class="icon card-title-icon"><use xlink:href="#icon-list"></use></svg>
662
+ 账号列表
663
+ </div>
664
+ <button class="btn btn-primary" onclick="showAddAccountModal()">
665
+ <svg class="icon"><use xlink:href="#icon-plus"></use></svg>
666
+ 添加账号
667
+ </button>
668
+ </div>
669
+ <div class="table-container">
670
+ <table id="accountsTable">
671
+ <thead>
672
+ <tr>
673
+ <th>序号</th>
674
+ <th>Team ID</th>
675
+ <th>csesidx</th>
676
+ <th>User Agent</th>
677
+ <th>状态</th>
678
+ <th>操作</th>
679
+ </tr>
680
+ </thead>
681
+ <tbody id="accountsTableBody"></tbody>
682
+ </table>
683
+ </div>
684
+ </div>
685
+ </div>
686
+
687
+ <!-- 模型管理 (HTML结构类似,图标已替换) -->
688
+ <div id="models" class="tab-content">
689
+ <div class="card">
690
+ <div class="card-header">
691
+ <div class="card-title">
692
+ <svg class="icon card-title-icon"><use xlink:href="#icon-robot"></use></svg>
693
+ 模型列表
694
+ </div>
695
+ <button class="btn btn-primary" onclick="showAddModelModal()">
696
+ <svg class="icon"><use xlink:href="#icon-plus"></use></svg>
697
+ 添加模型
698
+ </button>
699
+ </div>
700
+ <div class="table-container">
701
+ <table id="modelsTable">
702
+ <thead>
703
+ <tr>
704
+ <th>模型ID</th>
705
+ <th>名称</th>
706
+ <th>描述</th>
707
+ <th>上下文长度</th>
708
+ <th>最大Token</th>
709
+ <th>状态</th>
710
+ <th>操作</th>
711
+ </tr>
712
+ </thead>
713
+ <tbody id="modelsTableBody"></tbody>
714
+ </table>
715
+ </div>
716
+ </div>
717
+ </div>
718
+
719
+ <!-- 系统设置 (HTML结构类似,图标已替换) -->
720
+ <div id="settings" class="tab-content">
721
+ <div class="card">
722
+ <div class="card-header">
723
+ <div class="card-title">
724
+ <svg class="icon card-title-icon"><use xlink:href="#icon-settings"></use></svg>
725
+ 系统配置
726
+ </div>
727
+ </div>
728
+ <div class="card-body">
729
+ <form id="settingsForm">
730
+ <div class="settings-section">
731
+ <h3>代理设置</h3>
732
+ <div class="form-group">
733
+ <label class="form-label" for="proxyUrl">代理地址</label>
734
+ <input type="text" class="form-input" id="proxyUrl" placeholder="http://127.0.0.1:7890">
735
+ <small>用于访问Google API的代理服务器地址</small>
736
+ <div class="proxy-status" id="proxyStatus"></div>
737
+ </div>
738
+ <div class="form-group">
739
+ <label class="form-label" for="imageOutputMode">图片输出模式</label>
740
+ <select class="form-input" id="imageOutputMode">
741
+ <option value="url">图片URL(默认)</option>
742
+ <option value="base64">Base64 Data URL</option>
743
+ </select>
744
+ <small>控制聊天接口返回的图片是以URL形式还是以 data:image/...;base64,... 形式输出</small>
745
+ </div>
746
+ <div style="display: flex; gap: 12px;">
747
+ <button type="button" class="btn btn-outline" onclick="testProxy()">
748
+ 测试代理
749
+ </button>
750
+ <button type="button" class="btn btn-primary" onclick="saveSettings()">
751
+ 保存设置
752
+ </button>
753
+ </div>
754
+ </div>
755
+
756
+ <div class="settings-section">
757
+ <h3><svg class="icon" style="width: 1em; height: 1em; vertical-align: -2px; margin-right: 8px;"><use xlink:href="#icon-server"></use></svg>服务信息</h3>
758
+ <div class="form-row">
759
+ <div class="form-group">
760
+ <label class="form-label">服务端口</label>
761
+ <input type="text" class="form-input" value="8000" disabled>
762
+ </div>
763
+ <div class="form-group">
764
+ <label class="form-label">API地址</label>
765
+ <input type="text" class="form-input" value="http://localhost:8000/v1" disabled>
766
+ </div>
767
+ </div>
768
+ </div>
769
+
770
+ <div class="settings-section">
771
+ <h3>配置文件</h3>
772
+ <div class="form-group">
773
+ <label class="form-label" for="configJson">当前配置 (JSON)</label>
774
+ <textarea class="form-textarea" id="configJson" rows="15" readonly></textarea>
775
+ <small>配置文件路径: business_gemini_session.json</small>
776
+ </div>
777
+ <div style="display: flex; gap: 12px; flex-wrap: wrap;">
778
+ <button type="button" class="btn btn-outline" onclick="refreshConfig()">
779
+ 刷新配置
780
+ </button>
781
+ <button type="button" class="btn btn-outline" onclick="downloadConfig()">
782
+ 下载配置
783
+ </button>
784
+ <button type="button" class="btn btn-primary" onclick="uploadConfig()">
785
+ 导入配置
786
+ </button>
787
+ <input type="file" id="configFileInput" accept=".json" style="display: none;" onchange="handleConfigUpload(event)">
788
+ </div>
789
+ </div>
790
+ </form>
791
+ </div>
792
+ </div>
793
+ </div>
794
+
795
+ <!-- Token 管理 -->
796
+ <div id="tokens" class="tab-content">
797
+ <div class="card">
798
+ <div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
799
+ <div class="card-title" style="display:flex; align-items:center; gap:8px;">
800
+ <svg class="icon card-title-icon"><use xlink:href="#icon-key"></use></svg>
801
+ Token 管理
802
+ </div>
803
+ <div class="token-actions">
804
+ <input id="manualToken" class="form-input token-input" placeholder="手动输入 Token(留空自动生成)">
805
+ <button class="btn btn-outline" type="button" onclick="generateToken()">生成 Token</button>
806
+ <button class="btn btn-primary" type="button" onclick="addToken()">添加 Token</button>
807
+ </div>
808
+ </div>
809
+ <table class="table">
810
+ <thead>
811
+ <tr>
812
+ <th style="width:70%;">Token</th>
813
+ <th>操作</th>
814
+ </tr>
815
+ </thead>
816
+ <tbody id="tokensTableBody">
817
+ <tr><td colspan="2" class="empty-state">加载中...</td></tr>
818
+ </tbody>
819
+ </table>
820
+ </div>
821
+ </div>
822
+ </div>
823
+
824
+ <!-- 模态框 (已优化关闭按钮) -->
825
+ <div class="modal" id="addAccountModal">
826
+ <div class="modal-content">
827
+ <div class="modal-header">
828
+ <h3>添加账号</h3>
829
+ <button class="modal-close" onclick="closeModal('addAccountModal')" title="关闭">&times;</button>
830
+ </div>
831
+ <!-- Modal Body and Footer ... (No functional changes needed) -->
832
+ <div class="modal-body">
833
+ <div class="form-group">
834
+ <label class="form-label" for="newAccountJson">粘贴账号JSON(可直接复制工具输出)</label>
835
+ <textarea class="form-textarea" id="newAccountJson" placeholder='{"team_id":"...","secure_c_ses":"...","host_c_oses":"...","csesidx":"...","user_agent":"..."}' rows="4"></textarea>
836
+ <div style="display:flex; gap:8px; margin-top:8px;">
837
+ <button class="btn btn-outline btn-sm" type="button" onclick="parseAccountJson()">解析填充</button>
838
+ <button class="btn btn-outline btn-sm" type="button" onclick="pasteAccountJson()">从剪贴板读取并填充</button>
839
+ </div>
840
+ </div>
841
+ <div class="form-group">
842
+ <label class="form-label" for="newTeamId">Team ID</label>
843
+ <input type="text" class="form-input" id="newTeamId" placeholder="输入Team ID">
844
+ </div>
845
+ <div class="form-group">
846
+ <label class="form-label" for="newSecureCses">Cookie中的__Secure-C_SES</label>
847
+ <textarea class="form-textarea" id="newSecureCses" placeholder="输入Cookie中的__Secure-C_SES" rows="3"></textarea>
848
+ </div>
849
+ <div class="form-group">
850
+ <label class="form-label" for="newHostCoses">Cookie中的__Host-C_OSES</label>
851
+ <textarea class="form-textarea" id="newHostCoses" placeholder="输入Cookie中的__Host-C_OSES" rows="3"></textarea>
852
+ </div>
853
+ <div class="form-group">
854
+ <label class="form-label" for="newCsesidx">CSESIDX</label>
855
+ <input type="text" class="form-input" id="newCsesidx" placeholder="输入CSESIDX">
856
+ </div>
857
+ <div class="form-group">
858
+ <label class="form-label" for="newUserAgent">User Agent</label>
859
+ <input type="text" class="form-input" id="newUserAgent" placeholder="输入User Agent">
860
+ </div>
861
+ </div>
862
+ <div class="modal-footer">
863
+ <button class="btn btn-outline" onclick="closeModal('addAccountModal')">取消</button>
864
+ <button class="btn btn-primary" onclick="saveNewAccount()">保存</button>
865
+ </div>
866
+ </div>
867
+ </div>
868
+ <!-- 编辑账号模态框 -->
869
+ <div class="modal" id="editAccountModal">
870
+ <div class="modal-content">
871
+ <div class="modal-header">
872
+ <h3>编辑账号</h3>
873
+ <button class="modal-close" onclick="closeModal('editAccountModal')" title="关闭">&times;</button>
874
+ </div>
875
+ <div class="modal-body">
876
+ <input type="hidden" id="editAccountId">
877
+ <div class="form-group">
878
+ <label class="form-label" for="editTeamId">Team ID</label>
879
+ <input type="text" class="form-input" id="editTeamId" placeholder="输入Team ID">
880
+ </div>
881
+ <div class="form-group">
882
+ <label class="form-label" for="editSecureCses">Cookie中的__Secure-C_SES</label>
883
+ <textarea class="form-textarea" id="editSecureCses" placeholder="输入Secure C Ses" rows="3"></textarea>
884
+ </div>
885
+ <div class="form-group">
886
+ <label class="form-label" for="editHostCoses">Cookie中的__Host-C_OSES</label>
887
+ <textarea class="form-textarea" id="editHostCoses" placeholder="输入Host C Oses" rows="3"></textarea>
888
+ </div>
889
+ <div class="form-group">
890
+ <label class="form-label" for="editCsesidx">CSESIDX</label>
891
+ <input type="text" class="form-input" id="editCsesidx" placeholder="输入CSESIDX">
892
+ </div>
893
+ <div class="form-group">
894
+ <label class="form-label" for="editUserAgent">User Agent</label>
895
+ <input type="text" class="form-input" id="editUserAgent" placeholder="输入User Agent">
896
+ </div>
897
+ </div>
898
+ <div class="modal-footer">
899
+ <button class="btn btn-outline" onclick="closeModal('editAccountModal')">取消</button>
900
+ <button class="btn btn-primary" onclick="updateAccount()">保存</button>
901
+ </div>
902
+ </div>
903
+ </div>
904
+
905
+ <!-- 刷新Cookie模态框 -->
906
+ <div class="modal" id="refreshCookieModal">
907
+ <div class="modal-content">
908
+ <div class="modal-header">
909
+ <h3>刷新账号Cookie</h3>
910
+ <button class="modal-close" onclick="closeModal('refreshCookieModal')" title="关闭">&times;</button>
911
+ </div>
912
+ <div class="modal-body">
913
+ <input type="hidden" id="refreshAccountId">
914
+ <p class="text-muted" style="margin-bottom: 16px;">请输入新的Cookie值来刷新账号认证信息。刷新后将清除JWT缓存。</p>
915
+ <div class="form-group">
916
+ <label class="form-label" for="refreshSecureCses">Cookie中的__Secure-C_SES <span style="color: var(--danger);">*</span></label>
917
+ <textarea class="form-textarea" id="refreshSecureCses" placeholder="输入新的__Secure-C_SES值" rows="3"></textarea>
918
+ </div>
919
+ <div class="form-group">
920
+ <label class="form-label" for="refreshHostCoses">Cookie中的__Host-C_OSES <span style="color: var(--danger);">*</span></label>
921
+ <textarea class="form-textarea" id="refreshHostCoses" placeholder="输入新的__Host-C_OSES值" rows="3"></textarea>
922
+ </div>
923
+ <div class="form-group">
924
+ <label class="form-label" for="refreshCsesidx">CSESIDX (可选)</label>
925
+ <input type="text" class="form-input" id="refreshCsesidx" placeholder="输入CSESIDX值">
926
+ </div>
927
+ <div class="form-group">
928
+ <label class="form-label">从JSON粘贴 (可选)</label>
929
+ <textarea class="form-textarea" id="refreshCookieJson" placeholder="粘贴Cookie JSON数据" rows="3"></textarea>
930
+ <div style="display:flex; gap:8px; margin-top:8px;">
931
+ <button class="btn btn-outline btn-sm" type="button" onclick="parseRefreshCookieJson()">解析填充</button>
932
+ <button class="btn btn-outline btn-sm" type="button" onclick="pasteRefreshCookieJson()">📋 粘贴并解析</button>
933
+ </div>
934
+ </div>
935
+ </div>
936
+ <div class="modal-footer">
937
+ <button class="btn btn-outline" onclick="closeModal('refreshCookieModal')">取消</button>
938
+ <button class="btn btn-primary" onclick="refreshAccountCookie()">刷新Cookie</button>
939
+ </div>
940
+ </div>
941
+ </div>
942
+
943
+ <!-- 添加模型模态框 -->
944
+ <div class="modal" id="addModelModal">
945
+ <div class="modal-content">
946
+ <div class="modal-header">
947
+ <h3>添加模型</h3>
948
+ <button class="modal-close" onclick="closeModal('addModelModal')" title="关闭">&times;</button>
949
+ </div>
950
+ <div class="modal-body">
951
+ <div class="form-row">
952
+ <div class="form-group">
953
+ <label class="form-label" for="newModelId">模型ID</label>
954
+ <input type="text" class="form-input" id="newModelId" placeholder="如: gemini-pro">
955
+ </div>
956
+ <div class="form-group">
957
+ <label class="form-label" for="newModelName">模型名称</label>
958
+ <input type="text" class="form-input" id="newModelName" placeholder="如: Gemini Pro">
959
+ </div>
960
+ </div>
961
+ <div class="form-group">
962
+ <label class="form-label" for="newModelDesc">描述</label>
963
+ <input type="text" class="form-input" id="newModelDesc" placeholder="模型描述">
964
+ </div>
965
+ <div class="form-row">
966
+ <div class="form-group">
967
+ <label class="form-label" for="newContextLength">上下文长度</label>
968
+ <input type="number" class="form-input" id="newContextLength" value="32768">
969
+ </div>
970
+ <div class="form-group">
971
+ <label class="form-label" for="newMaxTokens">最大Token</label>
972
+ <input type="number" class="form-input" id="newMaxTokens" value="8192">
973
+ </div>
974
+ </div>
975
+ </div>
976
+ <div class="modal-footer">
977
+ <button class="btn btn-outline" onclick="closeModal('addModelModal')">取消</button>
978
+ <button class="btn btn-primary" onclick="saveNewModel()">保存</button>
979
+ </div>
980
+ </div>
981
+ </div>
982
+
983
+ <!-- 登录模态框 -->
984
+ <div class="modal" id="loginModal">
985
+ <div class="modal-content">
986
+ <div class="modal-header">
987
+ <h3>管理员登录</h3>
988
+ <button class="modal-close" onclick="closeModal('loginModal')" title="关闭">&times;</button>
989
+ </div>
990
+ <div class="modal-body">
991
+ <div class="form-group">
992
+ <label class="form-label" for="loginPassword">后台密码</label>
993
+ <input type="password" class="form-input" id="loginPassword" placeholder="输入后台密码">
994
+ </div>
995
+ <p class="text-muted" style="font-size: 12px;">首次登录将设置当前密码为后台密码。</p>
996
+ </div>
997
+ <div class="modal-footer">
998
+ <button class="btn btn-outline" onclick="closeModal('loginModal')">取消</button>
999
+ <button class="btn btn-primary" onclick="submitLogin()">登录</button>
1000
+ </div>
1001
+ </div>
1002
+ </div>
1003
+
1004
+ <!-- 编辑模型模态框 -->
1005
+ <div class="modal" id="editModelModal">
1006
+ <div class="modal-content">
1007
+ <div class="modal-header">
1008
+ <h3>编辑模型</h3>
1009
+ <button class="modal-close" onclick="closeModal('editModelModal')" title="关闭">&times;</button>
1010
+ </div>
1011
+ <div class="modal-body">
1012
+ <input type="hidden" id="editModelOriginalId">
1013
+ <div class="form-row">
1014
+ <div class="form-group">
1015
+ <label class="form-label" for="editModelId">模型ID</label>
1016
+ <input type="text" class="form-input" id="editModelId" placeholder="如: gemini-pro" readonly style="background-color: var(--bg-tertiary); cursor: not-allowed;">
1017
+ </div>
1018
+ <div class="form-group">
1019
+ <label class="form-label" for="editModelName">模型名称</label>
1020
+ <input type="text" class="form-input" id="editModelName" placeholder="如: Gemini Pro">
1021
+ </div>
1022
+ </div>
1023
+ <div class="form-group">
1024
+ <label class="form-label" for="editModelDesc">描述</label>
1025
+ <input type="text" class="form-input" id="editModelDesc" placeholder="模型描述">
1026
+ </div>
1027
+ <div class="form-row">
1028
+ <div class="form-group">
1029
+ <label class="form-label" for="editContextLength">上下文长度</label>
1030
+ <input type="number" class="form-input" id="editContextLength">
1031
+ </div>
1032
+ <div class="form-group">
1033
+ <label class="form-label" for="editMaxTokens">最大Token</label>
1034
+ <input type="number" class="form-input" id="editMaxTokens">
1035
+ </div>
1036
+ </div>
1037
+ </div>
1038
+ <div class="modal-footer">
1039
+ <button class="btn btn-outline" onclick="closeModal('editModelModal')">取消</button>
1040
+ <button class="btn btn-primary" onclick="updateModel()">保存</button>
1041
+ </div>
1042
+ </div>
1043
+ </div>
1044
+
1045
+
1046
+ <!-- Toast通知 -->
1047
+ <div id="toastContainer" class="toast-container">
1048
+ <!-- Toasts will be injected here by JS -->
1049
+ </div>
1050
+ <div class="toast" id="toast"></div>
1051
+
1052
+ <script>
1053
+ // [OPTIMIZATION] 1. 脚本微调以适应新的图标
1054
+ function updateThemeIcon(theme) {
1055
+ const iconContainer = document.getElementById('themeIconContainer');
1056
+ if (iconContainer) {
1057
+ const iconId = theme === 'dark' ? 'icon-sun' : 'icon-moon';
1058
+ iconContainer.innerHTML = `<svg class="icon"><use xlink:href="#${iconId}"></use></svg>`;
1059
+ }
1060
+ }
1061
+
1062
+ // [OPTIMIZATION] 2. 改进Toast通知
1063
+ let toastTimeout;
1064
+ function showToast(message, type = 'info') {
1065
+ const toast = document.getElementById('toast');
1066
+ if (!toast) return;
1067
+
1068
+ let icon = '';
1069
+ let borderType = type; // 'success', 'error', 'info'
1070
+ switch(type) {
1071
+ case 'success':
1072
+ icon = '<svg class="icon" style="color: var(--success);"><use xlink:href="#icon-check"></use></svg>';
1073
+ break;
1074
+ case 'error':
1075
+ icon = '<svg class="icon" style="color: var(--danger);"><use xlink:href="#icon-x"></use></svg>';
1076
+ break;
1077
+ default:
1078
+ icon = '<svg class="icon" style="color: var(--primary);"><use xlink:href="#icon-server"></use></svg>';
1079
+ borderType = 'primary';
1080
+ break;
1081
+ }
1082
+
1083
+ toast.innerHTML = `${icon} <span class="toast-message">${message}</span>`;
1084
+ toast.className = `toast show`;
1085
+ toast.style.borderLeft = `4px solid var(--${borderType})`;
1086
+
1087
+ clearTimeout(toastTimeout);
1088
+ toastTimeout = setTimeout(() => {
1089
+ toast.classList.remove('show');
1090
+ }, 3500);
1091
+ }
1092
+
1093
+ // =======================================================
1094
+ // [FULL SCRIPT] 以下是完整的、未删减的功能性 JavaScript 代码
1095
+ // =======================================================
1096
+
1097
+ // API 基础 URL
1098
+ const API_BASE = '.';
1099
+
1100
+ // 全局数据缓存
1101
+ let accountsData = [];
1102
+ let modelsData = [];
1103
+ let configData = {};
1104
+ let currentEditAccountId = null;
1105
+ let currentEditModelId = null;
1106
+ const ADMIN_TOKEN_KEY = 'admin_token';
1107
+ let tokensData = [];
1108
+
1109
+ // --- 初始化 ---
1110
+ document.addEventListener('DOMContentLoaded', () => {
1111
+ initTheme();
1112
+ loadAllData();
1113
+ setInterval(checkServerStatus, 30000); // 每30秒检查一次服务状态
1114
+ updateLoginButton();
1115
+ });
1116
+
1117
+ // --- 核心加载与渲染 ---
1118
+ async function loadAllData() {
1119
+ await Promise.all([
1120
+ loadAccounts(),
1121
+ loadModels(),
1122
+ loadConfig(),
1123
+ checkServerStatus(),
1124
+ loadLogLevel(),
1125
+ loadTokens()
1126
+ ]);
1127
+ }
1128
+
1129
+ function getAuthHeaders() {
1130
+ const token = localStorage.getItem(ADMIN_TOKEN_KEY);
1131
+ return token ? { 'X-Admin-Token': token } : {};
1132
+ }
1133
+
1134
+ function updateLoginButton() {
1135
+ const token = localStorage.getItem(ADMIN_TOKEN_KEY);
1136
+ const btn = document.getElementById('loginButton');
1137
+ if (!btn) return;
1138
+ if (token) {
1139
+ btn.textContent = '注销';
1140
+ btn.disabled = false;
1141
+ btn.classList.remove('btn-disabled');
1142
+ btn.title = '注销登录';
1143
+ btn.onclick = logoutAdmin;
1144
+ } else {
1145
+ btn.textContent = '登录';
1146
+ btn.disabled = false;
1147
+ btn.classList.remove('btn-disabled');
1148
+ btn.title = '管理员登录';
1149
+ btn.onclick = showLoginModal;
1150
+ }
1151
+ }
1152
+
1153
+ async function apiFetch(url, options = {}) {
1154
+ const headers = Object.assign({}, options.headers || {}, getAuthHeaders());
1155
+ const res = await fetch(url, { ...options, headers });
1156
+ if (res.status === 401 || res.status === 403) {
1157
+ showLoginModal();
1158
+ updateLoginButton();
1159
+ throw new Error('需要登录');
1160
+ }
1161
+ return res;
1162
+ }
1163
+
1164
+ // --- 主题控制 ---
1165
+ function initTheme() {
1166
+ const savedTheme = localStorage.getItem('theme') || 'light';
1167
+ document.documentElement.setAttribute('data-theme', savedTheme);
1168
+ updateThemeIcon(savedTheme);
1169
+ }
1170
+
1171
+ function toggleTheme() {
1172
+ const current = document.documentElement.getAttribute('data-theme');
1173
+ const newTheme = current === 'dark' ? 'light' : 'dark';
1174
+ document.documentElement.setAttribute('data-theme', newTheme);
1175
+ localStorage.setItem('theme', newTheme);
1176
+ updateThemeIcon(newTheme);
1177
+ }
1178
+
1179
+ // --- 标签页控制 ---
1180
+ function switchTab(tabName) {
1181
+ document.querySelectorAll('.tab').forEach(btn => btn.classList.remove('active'));
1182
+ document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
1183
+
1184
+ const tabBtn = document.querySelector(`[onclick="switchTab('${tabName}')"]`);
1185
+ const tabContent = document.getElementById(tabName);
1186
+
1187
+ if (tabBtn) tabBtn.classList.add('active');
1188
+ if (tabContent) tabContent.classList.add('active');
1189
+ }
1190
+
1191
+ // --- 状态检查 ---
1192
+ async function checkServerStatus() {
1193
+ const indicator = document.getElementById('serviceStatus');
1194
+ if (!indicator) return;
1195
+ try {
1196
+ const res = await apiFetch(`${API_BASE}/api/status`);
1197
+ console.log('Server Status Response:', res);
1198
+ if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
1199
+ const data = await res.json();
1200
+ indicator.textContent = '服务运行中';
1201
+ indicator.classList.remove('offline');
1202
+ indicator.title = '服务连接正常 - ' + new Date().toLocaleString();
1203
+ } catch (e) {
1204
+ indicator.textContent = '服务离线';
1205
+ indicator.classList.add('offline');
1206
+ indicator.title = '无法连接到后端服务';
1207
+ }
1208
+ }
1209
+
1210
+ // --- 账号管理 (Accounts) ---
1211
+ async function loadAccounts() {
1212
+ try {
1213
+ const res = await apiFetch(`${API_BASE}/api/accounts`);
1214
+ const data = await res.json();
1215
+ accountsData = data.accounts || [];
1216
+ document.getElementById('currentIndex').textContent = data.current_index || 0;
1217
+ renderAccounts();
1218
+ updateAccountStats();
1219
+ } catch (e) {
1220
+ showToast('加载账号列表失败: ' + e.message, 'error');
1221
+ }
1222
+ }
1223
+
1224
+ function renderAccounts() {
1225
+ const tbody = document.getElementById('accountsTableBody');
1226
+ if (!tbody) return;
1227
+
1228
+ if (accountsData.length === 0) {
1229
+ tbody.innerHTML = `<tr><td colspan="6" class="empty-state">
1230
+ <div class="empty-state-icon"><svg class="icon"><use xlink:href="#icon-users"></use></svg></div>
1231
+ <h3>暂无账号</h3><p>点击 "添加账号" 按钮来创建一个。</p>
1232
+ </td></tr>`;
1233
+ return;
1234
+ }
1235
+
1236
+ tbody.innerHTML = accountsData.map((acc, index) => `
1237
+ <tr>
1238
+ <td>${index + 1}</td>
1239
+ <td><code>${acc.team_id || '-'}</code></td>
1240
+ <td><code>${acc.csesidx || '-'}</code></td>
1241
+ <td title="${acc.user_agent}">${acc.user_agent ? acc.user_agent.substring(0, 30) + '...' : '-'}</td>
1242
+ <td>
1243
+ <span class="badge ${acc.available ? 'badge-success' : 'badge-danger'}">${acc.available ? '可用' : '不可用'}</span>
1244
+ ${renderNextRefresh(acc)}
1245
+ </td>
1246
+ <td style="white-space: nowrap;">
1247
+ <button class="btn btn-sm ${acc.enabled !== false ? 'btn-warning' : 'btn-success'} btn-icon" onclick="toggleAccount(${acc.id})" title="${acc.enabled !== false ? '停用' : '启用'}"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-${acc.enabled !== false ? 'pause' : 'play'}"></use></svg></button>
1248
+ <button class="btn btn-sm btn-outline btn-icon" onclick="testAccount(${acc.id})" title="测试连接"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-zap"></use></svg></button>
1249
+ <button class="btn btn-sm btn-outline btn-icon" onclick="showRefreshCookieModal(${acc.id})" title="刷新Cookie"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-refresh"></use></svg></button>
1250
+ <button class="btn btn-sm btn-outline btn-icon" onclick="showEditAccountModal(${acc.id})" title="编辑"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-settings"></use></svg></button>
1251
+ <button class="btn btn-sm btn-danger btn-icon" onclick="deleteAccount(${acc.id})" title="删除"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-x"></use></svg></button>
1252
+ </td>
1253
+ </tr>
1254
+ `).join('');
1255
+ }
1256
+
1257
+ function updateAccountStats() {
1258
+ document.getElementById('totalAccounts').textContent = accountsData.length;
1259
+ document.getElementById('availableAccounts').textContent = accountsData.filter(a => a.available).length;
1260
+ document.getElementById('unavailableAccounts').textContent = accountsData.length - accountsData.filter(a => a.available).length;
1261
+ }
1262
+
1263
+ function renderNextRefresh(acc) {
1264
+ if (!acc || !acc.cooldown_until) return '';
1265
+ const now = Date.now();
1266
+ const ts = acc.cooldown_until * 1000;
1267
+ if (ts <= now) return '';
1268
+ const next = new Date(ts);
1269
+ const remaining = Math.max(0, ts - now);
1270
+ const minutes = Math.floor(remaining / 60000);
1271
+ const label = minutes >= 60
1272
+ ? `${Math.floor(minutes / 60)}小时${minutes % 60}分`
1273
+ : `${minutes}分`;
1274
+ return `<span class="cooldown-hint">下次恢复: ${next.toLocaleString()}(约${label})</span>`;
1275
+ }
1276
+
1277
+ function showAddAccountModal() {
1278
+ // 清空表单字段
1279
+ document.getElementById('newAccountJson').value = '';
1280
+ document.getElementById('newTeamId').value = '';
1281
+ document.getElementById('newSecureCses').value = '';
1282
+ document.getElementById('newHostCoses').value = '';
1283
+ document.getElementById('newCsesidx').value = '';
1284
+ document.getElementById('newUserAgent').value = '';
1285
+ openModal('addAccountModal');
1286
+ }
1287
+
1288
+ function showEditAccountModal(id) {
1289
+ const acc = accountsData.find(a => a.id === id);
1290
+ if (!acc) return;
1291
+
1292
+ document.getElementById('editAccountId').value = id;
1293
+ document.getElementById('editTeamId').value = acc.team_id || '';
1294
+ document.getElementById('editSecureCses').value = acc.secure_c_ses || '';
1295
+ document.getElementById('editHostCoses').value = acc.host_c_oses || '';
1296
+ document.getElementById('editCsesidx').value = acc.csesidx || '';
1297
+ document.getElementById('editUserAgent').value = acc.user_agent ? acc.user_agent.replace('...', '') : '';
1298
+
1299
+ openModal('editAccountModal');
1300
+ }
1301
+
1302
+ async function updateAccount() {
1303
+ const id = document.getElementById('editAccountId').value;
1304
+ const account = {};
1305
+
1306
+ const teamId = document.getElementById('editTeamId').value;
1307
+ const secureCses = document.getElementById('editSecureCses').value;
1308
+ const hostCoses = document.getElementById('editHostCoses').value;
1309
+ const csesidx = document.getElementById('editCsesidx').value;
1310
+ const userAgent = document.getElementById('editUserAgent').value;
1311
+
1312
+ if (teamId) account.team_id = teamId;
1313
+ if (secureCses) account.secure_c_ses = secureCses;
1314
+ if (hostCoses) account.host_c_oses = hostCoses;
1315
+ if (csesidx) account.csesidx = csesidx;
1316
+ if (userAgent) account.user_agent = userAgent;
1317
+
1318
+ try {
1319
+ const res = await apiFetch(`${API_BASE}/api/accounts/${id}`, {
1320
+ method: 'PUT',
1321
+ headers: { 'Content-Type': 'application/json' },
1322
+ body: JSON.stringify(account)
1323
+ });
1324
+ const data = await res.json();
1325
+
1326
+ if (data.success) {
1327
+ showToast('账号更新成功', 'success');
1328
+ closeModal('editAccountModal');
1329
+ loadAccounts();
1330
+ } else {
1331
+ showToast('更新失败: ' + (data.error || '未知错误'), 'error');
1332
+ }
1333
+ } catch (e) {
1334
+ showToast('更新失败: ' + e.message, 'error');
1335
+ }
1336
+ }
1337
+
1338
+ async function saveNewAccount() {
1339
+ const teamId = document.getElementById('newTeamId').value;
1340
+ const secureCses = document.getElementById('newSecureCses').value;
1341
+ const hostCoses = document.getElementById('newHostCoses').value;
1342
+ const csesidx = document.getElementById('newCsesidx').value;
1343
+ const userAgent = document.getElementById('newUserAgent').value;
1344
+
1345
+ try {
1346
+ const res = await apiFetch(`${API_BASE}/api/accounts`, {
1347
+ method: 'POST',
1348
+ headers: { 'Content-Type': 'application/json' },
1349
+ body: JSON.stringify({
1350
+ team_id: teamId,
1351
+ "secure_c_ses": secureCses,
1352
+ "host_c_oses": hostCoses,
1353
+ "csesidx": csesidx,
1354
+ "user_agent": userAgent })
1355
+ });
1356
+ const data = await res.json();
1357
+ if (!res.ok || data.error) throw new Error(data.error || data.detail || '添加失败');
1358
+ showToast('账号添加成功!', 'success');
1359
+ closeModal('addAccountModal');
1360
+ loadAccounts();
1361
+ } catch (e) {
1362
+ showToast('添加失败: ' + e.message, 'error');
1363
+ }
1364
+ }
1365
+
1366
+ function parseAccountJson(text) {
1367
+ const textarea = document.getElementById('newAccountJson');
1368
+ const raw = (typeof text === 'string' ? text : textarea.value || '').trim();
1369
+ if (!raw) {
1370
+ showToast('请先粘贴账号JSON', 'warning');
1371
+ return;
1372
+ }
1373
+ let acc;
1374
+ try {
1375
+ const parsed = JSON.parse(raw);
1376
+ acc = Array.isArray(parsed) ? parsed[0] : parsed;
1377
+ if (!acc || typeof acc !== 'object') throw new Error('格式不正确');
1378
+ } catch (err) {
1379
+ showToast('解析失败: ' + err.message, 'error');
1380
+ return;
1381
+ }
1382
+
1383
+ document.getElementById('newTeamId').value = acc.team_id || '';
1384
+ document.getElementById('newSecureCses').value = acc.secure_c_ses || '';
1385
+ document.getElementById('newHostCoses').value = acc.host_c_oses || '';
1386
+ document.getElementById('newCsesidx').value = acc.csesidx || '';
1387
+ document.getElementById('newUserAgent').value = acc.user_agent || '';
1388
+ showToast('已填充账号信息', 'success');
1389
+ }
1390
+
1391
+ async function pasteAccountJson() {
1392
+ try {
1393
+ if (!navigator.clipboard || !navigator.clipboard.readText) {
1394
+ showToast('当前环境不支持剪贴板API,请使用HTTPS或手动粘贴', 'warning');
1395
+ return;
1396
+ }
1397
+ const text = await navigator.clipboard.readText();
1398
+ document.getElementById('newAccountJson').value = text;
1399
+ parseAccountJson(text);
1400
+ } catch (e) {
1401
+ showToast('无法读取剪贴板: ' + e.message, 'error');
1402
+ }
1403
+ }
1404
+
1405
+ async function deleteAccount(id) {
1406
+ if (!confirm('确定要删除这个账号吗?')) return;
1407
+ try {
1408
+ const res = await apiFetch(`${API_BASE}/api/accounts/${id}`, { method: 'DELETE' });
1409
+ if (!res.ok) throw new Error((await res.json()).detail);
1410
+ showToast('账号删除成功!', 'success');
1411
+ loadAccounts();
1412
+ } catch (e) {
1413
+ showToast('删除失败: ' + e.message, 'error');
1414
+ }
1415
+ }
1416
+
1417
+ async function testAccount(id) {
1418
+ showToast(`正在测试账号ID: ${id}...`, 'info');
1419
+ try {
1420
+ const res = await apiFetch(`${API_BASE}/api/accounts/${id}/test`);
1421
+ const data = await res.json();
1422
+ if (res.ok && data.success) {
1423
+ showToast(`账号 ${id} 测试成功!`, 'success');
1424
+ } else {
1425
+ throw new Error(data.detail || '未知错误');
1426
+ }
1427
+ loadAccounts();
1428
+ } catch (e) {
1429
+ showToast(`账号 ${id} 测试失败: ${e.message}`, 'error');
1430
+ }
1431
+ }
1432
+
1433
+ async function toggleAccount(id) {
1434
+ const acc = accountsData.find(a => a.id === id);
1435
+ const action = acc && acc.enabled !== false ? '停用' : '启用';
1436
+ try {
1437
+ const res = await apiFetch(`${API_BASE}/api/accounts/${id}/toggle`, {
1438
+ method: 'POST',
1439
+ headers: { 'Content-Type': 'application/json' }
1440
+ });
1441
+ const data = await res.json();
1442
+ if (res.ok && data.success) {
1443
+ showToast(`账号 ${id} ${action}成功!`, 'success');
1444
+ loadAccounts();
1445
+ } else {
1446
+ throw new Error(data.error || data.detail || '未知错误');
1447
+ }
1448
+ } catch (e) {
1449
+ showToast(`账号 ${id} ${action}失败: ${e.message}`, 'error');
1450
+ }
1451
+ }
1452
+
1453
+ /**
1454
+ * 显示刷新Cookie的模态框
1455
+ * @param {number} id - 账号ID
1456
+ */
1457
+ function showRefreshCookieModal(id) {
1458
+ const acc = accountsData.find(a => a.id === id);
1459
+ if (!acc) {
1460
+ showToast('账号不存在', 'error');
1461
+ return;
1462
+ }
1463
+
1464
+ document.getElementById('refreshAccountId').value = id;
1465
+ document.getElementById('refreshSecureCses').value = '';
1466
+ document.getElementById('refreshHostCoses').value = '';
1467
+ document.getElementById('refreshCsesidx').value = '';
1468
+ document.getElementById('refreshCookieJson').value = '';
1469
+
1470
+ openModal('refreshCookieModal');
1471
+ }
1472
+
1473
+ /**
1474
+ * 从JSON解析并填充刷新Cookie表单
1475
+ * @param {string} text - JSON字符串
1476
+ */
1477
+ function parseRefreshCookieJson(text) {
1478
+ const textarea = document.getElementById('refreshCookieJson');
1479
+ const raw = (typeof text === 'string' ? text : textarea.value || '').trim();
1480
+ if (!raw) {
1481
+ showToast('请先粘贴Cookie JSON', 'warning');
1482
+ return;
1483
+ }
1484
+ let acc;
1485
+ try {
1486
+ const parsed = JSON.parse(raw);
1487
+ acc = Array.isArray(parsed) ? parsed[0] : parsed;
1488
+ if (!acc || typeof acc !== 'object') throw new Error('格式不正确');
1489
+ } catch (err) {
1490
+ showToast('解析失败: ' + err.message, 'error');
1491
+ return;
1492
+ }
1493
+
1494
+ document.getElementById('refreshSecureCses').value = acc.secure_c_ses || '';
1495
+ document.getElementById('refreshHostCoses').value = acc.host_c_oses || '';
1496
+ document.getElementById('refreshCsesidx').value = acc.csesidx || '';
1497
+ showToast('已填充Cookie信息', 'success');
1498
+ }
1499
+
1500
+ /**
1501
+ * 从剪贴板粘贴并解析刷新Cookie JSON
1502
+ */
1503
+ async function pasteRefreshCookieJson() {
1504
+ try {
1505
+ if (!navigator.clipboard || !navigator.clipboard.readText) {
1506
+ showToast('当前环境不支持剪贴板API,请使用HTTPS或手动粘贴', 'warning');
1507
+ return;
1508
+ }
1509
+ const text = await navigator.clipboard.readText();
1510
+ document.getElementById('refreshCookieJson').value = text;
1511
+ parseRefreshCookieJson(text);
1512
+ } catch (e) {
1513
+ showToast('无法读取剪贴板: ' + e.message, 'error');
1514
+ }
1515
+ }
1516
+
1517
+ /**
1518
+ * 刷新账号Cookie
1519
+ * 调用后端API更新账号的Cookie信息
1520
+ */
1521
+ async function refreshAccountCookie() {
1522
+ const id = document.getElementById('refreshAccountId').value;
1523
+ const secureCses = document.getElementById('refreshSecureCses').value.trim();
1524
+ const hostCoses = document.getElementById('refreshHostCoses').value.trim();
1525
+ const csesidx = document.getElementById('refreshCsesidx').value.trim();
1526
+
1527
+ // 验证必填字段
1528
+ if (!secureCses || !hostCoses) {
1529
+ showToast('secure_c_ses 和 host_c_oses 为必填项', 'warning');
1530
+ return;
1531
+ }
1532
+
1533
+ try {
1534
+ const res = await apiFetch(`${API_BASE}/api/accounts/${id}/refresh-cookie`, {
1535
+ method: 'POST',
1536
+ headers: { 'Content-Type': 'application/json' },
1537
+ body: JSON.stringify({
1538
+ secure_c_ses: secureCses,
1539
+ host_c_oses: hostCoses,
1540
+ csesidx: csesidx || undefined
1541
+ })
1542
+ });
1543
+ const data = await res.json();
1544
+
1545
+ if (res.ok && data.success) {
1546
+ showToast('Cookie刷新成功!', 'success');
1547
+ closeModal('refreshCookieModal');
1548
+ loadAccounts();
1549
+ } else {
1550
+ throw new Error(data.error || data.detail || '未知错误');
1551
+ }
1552
+ } catch (e) {
1553
+ showToast('Cookie刷新失败: ' + e.message, 'error');
1554
+ }
1555
+ }
1556
+
1557
+ // --- 模型管理 (Models) ---
1558
+ async function loadModels() {
1559
+ try {
1560
+ const res = await apiFetch(`${API_BASE}/api/models`);
1561
+ const data = await res.json();
1562
+ modelsData = data.models || [];
1563
+ renderModels();
1564
+ } catch (e) {
1565
+ showToast('加载模型列表失败: ' + e.message, 'error');
1566
+ }
1567
+ }
1568
+
1569
+ function escapeHtml(str) {
1570
+ if (!str) return '';
1571
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
1572
+ }
1573
+
1574
+ function renderModels() {
1575
+ const tbody = document.getElementById('modelsTableBody');
1576
+ if (!tbody) return;
1577
+ if (modelsData.length === 0) {
1578
+ tbody.innerHTML = `<tr><td colspan="7" class="empty-state">
1579
+ <div class="empty-state-icon"><svg class="icon"><use xlink:href="#icon-robot"></use></svg></div>
1580
+ <h3>暂无模型</h3><p>点击 "添加模型" 按钮来创建一个。</p>
1581
+ </td></tr>`;
1582
+ return;
1583
+ }
1584
+ tbody.innerHTML = modelsData.map((model, index) => {
1585
+ const safeId = escapeHtml(model.id);
1586
+ const safeName = escapeHtml(model.name);
1587
+ const safeDesc = escapeHtml(model.description);
1588
+ return `
1589
+ <tr>
1590
+ <td><code>${safeId}</code></td>
1591
+ <td>${safeName}</td>
1592
+ <td title="${safeDesc}">${model.description ? safeDesc.substring(0, 40) + '...' : ''}</td>
1593
+ <td>${model.context_length}</td>
1594
+ <td>${model.max_tokens}</td>
1595
+ <td><span class="badge ${model.is_public ? 'badge-success' : 'badge-warning'}">${model.is_public ? '公共' : '私有'}</span></td>
1596
+ <td>
1597
+ <button class="btn btn-sm btn-outline btn-icon" onclick="showEditModelModalByIndex(${index})" title="编辑">✏️</button>
1598
+ <button class="btn btn-sm btn-danger btn-icon" onclick="deleteModelByIndex(${index})" title="删除">🗑️</button>
1599
+ </td>
1600
+ </tr>
1601
+ `;
1602
+ }).join('');
1603
+ }
1604
+
1605
+ function showAddModelModal() {
1606
+ openModal('addModelModal');
1607
+ }
1608
+
1609
+ function showEditModelModalByIndex(index) {
1610
+ const model = modelsData[index];
1611
+ if (!model) return;
1612
+
1613
+ document.getElementById('editModelOriginalId').value = model.id;
1614
+ document.getElementById('editModelId').value = model.id;
1615
+ document.getElementById('editModelName').value = model.name || '';
1616
+ document.getElementById('editModelDesc').value = model.description || '';
1617
+ document.getElementById('editContextLength').value = model.context_length || '';
1618
+ document.getElementById('editMaxTokens').value = model.max_tokens || '';
1619
+
1620
+ openModal('editModelModal');
1621
+ }
1622
+
1623
+ async function updateModel() {
1624
+ const originalId = document.getElementById('editModelOriginalId').value;
1625
+ const model = {
1626
+ name: document.getElementById('editModelName').value,
1627
+ description: document.getElementById('editModelDesc').value,
1628
+ context_length: parseInt(document.getElementById('editContextLength').value) || 32000,
1629
+ max_tokens: parseInt(document.getElementById('editMaxTokens').value) || 8096
1630
+ };
1631
+
1632
+ try {
1633
+ const res = await apiFetch(`${API_BASE}/api/models/${encodeURIComponent(originalId)}`, {
1634
+ method: 'PUT',
1635
+ headers: { 'Content-Type': 'application/json' },
1636
+ body: JSON.stringify(model)
1637
+ });
1638
+ const data = await res.json();
1639
+
1640
+ if (data.success) {
1641
+ showToast('模型更新成功', 'success');
1642
+ closeModal('editModelModal');
1643
+ loadModels();
1644
+ } else {
1645
+ showToast('更新失败: ' + (data.error || '未知错误'), 'error');
1646
+ }
1647
+ } catch (e) {
1648
+ showToast('更新失败: ' + e.message, 'error');
1649
+ }
1650
+ }
1651
+
1652
+ /**
1653
+ * 保存新模型
1654
+ * 从添加模型模态框获取数据并调用API创建新模型
1655
+ */
1656
+ async function saveNewModel() {
1657
+ const modelId = document.getElementById('newModelId').value.trim();
1658
+ const modelName = document.getElementById('newModelName').value.trim();
1659
+ const modelDesc = document.getElementById('newModelDesc').value.trim();
1660
+ const contextLength = parseInt(document.getElementById('newContextLength').value) || 32000;
1661
+ const maxTokens = parseInt(document.getElementById('newMaxTokens').value) || 8096;
1662
+
1663
+ // 验证必填字段
1664
+ if (!modelId) {
1665
+ showToast('请输入模型ID', 'warning');
1666
+ return;
1667
+ }
1668
+ if (!modelName) {
1669
+ showToast('请输入模型名称', 'warning');
1670
+ return;
1671
+ }
1672
+
1673
+ const model = {
1674
+ id: modelId,
1675
+ name: modelName,
1676
+ description: modelDesc,
1677
+ context_length: contextLength,
1678
+ max_tokens: maxTokens
1679
+ };
1680
+
1681
+ try {
1682
+ const res = await apiFetch(`${API_BASE}/api/models`, {
1683
+ method: 'POST',
1684
+ headers: { 'Content-Type': 'application/json' },
1685
+ body: JSON.stringify(model)
1686
+ });
1687
+ const data = await res.json();
1688
+
1689
+ if (res.ok && (data.success || !data.error)) {
1690
+ showToast('模型添加成功', 'success');
1691
+ closeModal('addModelModal');
1692
+ // 清空表单
1693
+ document.getElementById('newModelId').value = '';
1694
+ document.getElementById('newModelName').value = '';
1695
+ document.getElementById('newModelDesc').value = '';
1696
+ document.getElementById('newContextLength').value = '';
1697
+ document.getElementById('newMaxTokens').value = '';
1698
+ loadModels();
1699
+ } else {
1700
+ throw new Error(data.error || '添加失败');
1701
+ }
1702
+ } catch (e) {
1703
+ showToast('添加模型失败: ' + e.message, 'error');
1704
+ }
1705
+ }
1706
+
1707
+ /**
1708
+ * 删除模型
1709
+ * @param {string} id - 模型ID
1710
+ */
1711
+ async function deleteModelByIndex(index) {
1712
+ const model = modelsData[index];
1713
+ if (!model) return;
1714
+ const id = model.id;
1715
+ if (!confirm(`确定要删除模型 "${id}" 吗?此操作不可恢复。`)) {
1716
+ return;
1717
+ }
1718
+
1719
+ try {
1720
+ const res = await apiFetch(`${API_BASE}/api/models/${encodeURIComponent(id)}`, {
1721
+ method: 'DELETE'
1722
+ });
1723
+ const data = await res.json();
1724
+
1725
+ if (res.ok && (data.success || !data.error)) {
1726
+ showToast('模型删除成功', 'success');
1727
+ loadModels();
1728
+ } else {
1729
+ throw new Error(data.error || '删除失败');
1730
+ }
1731
+ } catch (e) {
1732
+ showToast('删除模型失败: ' + e.message, 'error');
1733
+ }
1734
+ }
1735
+
1736
+ // --- 系统设置 (Settings) ---
1737
+ async function loadConfig() {
1738
+ try {
1739
+ const res = await apiFetch(`${API_BASE}/api/config`);
1740
+ configData = await res.json();
1741
+ document.getElementById('proxyUrl').value = configData.proxy || '';
1742
+ const imageModeSelect = document.getElementById('imageOutputMode');
1743
+ if (imageModeSelect) {
1744
+ const mode = (configData.image_output_mode || 'url');
1745
+ imageModeSelect.value = mode === 'base64' ? 'base64' : 'url';
1746
+ }
1747
+ document.getElementById('configJson').value = JSON.stringify(configData, null, 2);
1748
+ } catch (e) {
1749
+ showToast('加载配置失败: ' + e.message, 'error');
1750
+ }
1751
+ }
1752
+
1753
+ async function loadLogLevel() {
1754
+ try {
1755
+ const res = await apiFetch(`${API_BASE}/api/logging`);
1756
+ const data = await res.json();
1757
+ const select = document.getElementById('logLevelSelect');
1758
+ if (select && data.level) {
1759
+ select.value = data.level;
1760
+ }
1761
+ } catch (e) {
1762
+ console.warn('日志级别加载失败', e);
1763
+ }
1764
+ }
1765
+
1766
+ async function updateLogLevel(level) {
1767
+ try {
1768
+ const res = await apiFetch(`${API_BASE}/api/logging`, {
1769
+ method: 'POST',
1770
+ headers: { 'Content-Type': 'application/json' },
1771
+ body: JSON.stringify({ level })
1772
+ });
1773
+ const data = await res.json();
1774
+ if (!res.ok || data.error) {
1775
+ throw new Error(data.error || '设置失败');
1776
+ }
1777
+ showToast(`日志级别已切换为 ${data.level}`, 'success');
1778
+ } catch (e) {
1779
+ showToast('日志级别设置失败: ' + e.message, 'error');
1780
+ }
1781
+ }
1782
+
1783
+ // --- Token 管理 ---
1784
+ async function loadTokens() {
1785
+ try {
1786
+ const res = await apiFetch(`${API_BASE}/api/tokens`);
1787
+ const data = await res.json();
1788
+ tokensData = data.tokens || [];
1789
+ renderTokens();
1790
+ } catch (e) {
1791
+ showToast('加载 Token 失败: ' + e.message, 'error');
1792
+ }
1793
+ }
1794
+
1795
+ function renderTokens() {
1796
+ const tbody = document.getElementById('tokensTableBody');
1797
+ if (!tbody) return;
1798
+ if (!tokensData.length) {
1799
+ tbody.innerHTML = `<tr><td colspan="2" class="empty-state">暂无 Token</td></tr>`;
1800
+ return;
1801
+ }
1802
+ tbody.innerHTML = tokensData.map(token => `
1803
+ <tr>
1804
+ <td><code>${token}</code></td>
1805
+ <td style="white-space: nowrap;">
1806
+ <button class="btn btn-outline btn-sm" data-token="${token}" onclick="copyToken(this.dataset.token)" title="复制Token">复制</button>
1807
+ <button class="btn btn-danger btn-sm" data-token="${token}" onclick="deleteToken(this.dataset.token)" title="删除Token">删除</button>
1808
+ </td>
1809
+ </tr>
1810
+ `).join('');
1811
+ }
1812
+
1813
+ async function addToken() {
1814
+ const manual = document.getElementById('manualToken').value.trim();
1815
+ try {
1816
+ const res = await apiFetch(`${API_BASE}/api/tokens`, {
1817
+ method: 'POST',
1818
+ headers: { 'Content-Type': 'application/json' },
1819
+ body: JSON.stringify(manual ? { token: manual } : {})
1820
+ });
1821
+ const data = await res.json();
1822
+ if (!res.ok || data.error) throw new Error(data.error || '创建失败');
1823
+ document.getElementById('manualToken').value = data.token;
1824
+ showToast('Token 创建成功', 'success');
1825
+ loadTokens();
1826
+ } catch (e) {
1827
+ showToast('创建 Token 失败: ' + e.message, 'error');
1828
+ }
1829
+ }
1830
+
1831
+ function generateToken() {
1832
+ if (window.crypto && crypto.randomUUID) {
1833
+ document.getElementById('manualToken').value = crypto.randomUUID().replace(/-/g, '');
1834
+ } else {
1835
+ document.getElementById('manualToken').value = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
1836
+ }
1837
+ }
1838
+
1839
+ async function deleteToken(token) {
1840
+ if (!confirm('确定删除该 Token 吗?')) return;
1841
+ try {
1842
+ const res = await apiFetch(`${API_BASE}/api/tokens/${token}`, { method: 'DELETE' });
1843
+ const data = await res.json();
1844
+ if (!res.ok || data.error) throw new Error(data.error || '删除失败');
1845
+ showToast('Token 删除成功', 'success');
1846
+ loadTokens();
1847
+ } catch (e) {
1848
+ showToast('删除 Token 失败: ' + e.message, 'error');
1849
+ }
1850
+ }
1851
+
1852
+ function copyToken(token) {
1853
+ if (!token) {
1854
+ showToast('无效的 Token', 'warning');
1855
+ return;
1856
+ }
1857
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1858
+ navigator.clipboard.writeText(token).then(() => {
1859
+ showToast('已复制', 'success');
1860
+ }).catch(() => {
1861
+ fallbackCopy(token);
1862
+ });
1863
+ } else {
1864
+ fallbackCopy(token);
1865
+ }
1866
+ }
1867
+
1868
+ function fallbackCopy(text) {
1869
+ try {
1870
+ const textarea = document.createElement('textarea');
1871
+ textarea.value = text;
1872
+ document.body.appendChild(textarea);
1873
+ textarea.select();
1874
+ document.execCommand('copy');
1875
+ document.body.removeChild(textarea);
1876
+ showToast('已复制', 'success');
1877
+ } catch (err) {
1878
+ showToast('复制失败', 'error');
1879
+ }
1880
+ }
1881
+
1882
+ function logoutAdmin() {
1883
+ localStorage.removeItem(ADMIN_TOKEN_KEY);
1884
+ document.cookie = 'admin_token=; Max-Age=0; path=/';
1885
+ showToast('已注销', 'success');
1886
+ updateLoginButton();
1887
+ }
1888
+
1889
+ function showLoginModal() {
1890
+ document.getElementById('loginPassword').value = '';
1891
+ openModal('loginModal');
1892
+ }
1893
+
1894
+ async function submitLogin() {
1895
+ const pwd = document.getElementById('loginPassword').value;
1896
+ if (!pwd) {
1897
+ showToast('请输入密码', 'warning');
1898
+ return;
1899
+ }
1900
+ try {
1901
+ const res = await fetch(`${API_BASE}/api/auth/login`, {
1902
+ method: 'POST',
1903
+ headers: { 'Content-Type': 'application/json' },
1904
+ body: JSON.stringify({ password: pwd })
1905
+ });
1906
+ const data = await res.json();
1907
+ if (!res.ok || data.error) {
1908
+ throw new Error(data.error || '登录失败');
1909
+ }
1910
+ localStorage.setItem(ADMIN_TOKEN_KEY, data.token);
1911
+ showToast('登录成功', 'success');
1912
+ closeModal('loginModal');
1913
+ loadAllData();
1914
+ updateLoginButton();
1915
+ } catch (e) {
1916
+ showToast('登录失败: ' + e.message, 'error');
1917
+ }
1918
+ }
1919
+
1920
+ async function saveSettings() {
1921
+ const proxyUrl = document.getElementById('proxyUrl').value;
1922
+ const imageModeSelect = document.getElementById('imageOutputMode');
1923
+ const imageOutputMode = imageModeSelect ? imageModeSelect.value : 'url';
1924
+ try {
1925
+ const res = await apiFetch(`${API_BASE}/api/config`, {
1926
+ method: 'PUT',
1927
+ headers: { 'Content-Type': 'application/json' },
1928
+ body: JSON.stringify({ proxy: proxyUrl, image_output_mode: imageOutputMode })
1929
+ });
1930
+ if (!res.ok) throw new Error((await res.json()).detail);
1931
+ showToast('设置保存成功!', 'success');
1932
+ loadConfig();
1933
+ } catch (e) {
1934
+ showToast('保存失败: ' + e.message, 'error');
1935
+ }
1936
+ }
1937
+
1938
+ async function testProxy() {
1939
+ const proxyUrl = document.getElementById('proxyUrl').value;
1940
+ const proxyStatus = document.getElementById('proxyStatus');
1941
+ proxyStatus.textContent = '测试中...';
1942
+ proxyStatus.style.color = 'var(--text-muted)';
1943
+ try {
1944
+ const res = await apiFetch(`${API_BASE}/api/proxy/test`, {
1945
+ method: 'POST',
1946
+ headers: { 'Content-Type': 'application/json' },
1947
+ body: JSON.stringify({ proxy: proxyUrl })
1948
+ });
1949
+ const data = await res.json();
1950
+ if (res.ok && data.success) {
1951
+ proxyStatus.textContent = `测试成功! (${data.delay_ms}ms)`;
1952
+ proxyStatus.style.color = 'var(--success)';
1953
+ } else {
1954
+ throw new Error(data.detail);
1955
+ }
1956
+ } catch (e) {
1957
+ proxyStatus.textContent = `测试失败: ${e.message}`;
1958
+ proxyStatus.style.color = 'var(--danger)';
1959
+ }
1960
+ }
1961
+
1962
+ function refreshConfig() {
1963
+ loadConfig();
1964
+ showToast('配置已刷新', 'info');
1965
+ }
1966
+
1967
+ function downloadConfig() {
1968
+ const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(configData, null, 2));
1969
+ const downloadAnchorNode = document.createElement('a');
1970
+ downloadAnchorNode.setAttribute("href", dataStr);
1971
+ downloadAnchorNode.setAttribute("download", "business_gemini_session.json");
1972
+ document.body.appendChild(downloadAnchorNode);
1973
+ downloadAnchorNode.click();
1974
+ downloadAnchorNode.remove();
1975
+ showToast('配置文件已开始下载', 'success');
1976
+ }
1977
+
1978
+ function uploadConfig() {
1979
+ document.getElementById('configFileInput').click();
1980
+ }
1981
+
1982
+ function handleConfigUpload(event) {
1983
+ const file = event.target.files[0];
1984
+ if (!file) return;
1985
+ const reader = new FileReader();
1986
+ reader.onload = async (e) => {
1987
+ try {
1988
+ const newConfig = JSON.parse(e.target.result);
1989
+ const res = await apiFetch(`${API_BASE}/api/config/import`, {
1990
+ method: 'POST',
1991
+ headers: { 'Content-Type': 'application/json' },
1992
+ body: JSON.stringify(newConfig)
1993
+ });
1994
+ if (!res.ok) throw new Error((await res.json()).detail);
1995
+ showToast('配置导入成功!', 'success');
1996
+ loadAllData();
1997
+ } catch (err) {
1998
+ showToast('导入失败: ' + err.message, 'error');
1999
+ }
2000
+ };
2001
+ reader.readAsText(file);
2002
+ }
2003
+
2004
+ // --- 模态框控制 ---
2005
+ function openModal(modalId) {
2006
+ const modal = document.getElementById(modalId);
2007
+ if (modal) modal.classList.add('show');
2008
+ }
2009
+
2010
+ function closeModal(modalId) {
2011
+ const modal = document.getElementById(modalId);
2012
+ if (modal) modal.classList.remove('show');
2013
+ }
2014
+
2015
+ document.querySelectorAll('.modal').forEach(modal => {
2016
+ modal.addEventListener('click', (e) => {
2017
+ if (e.target.classList.contains('modal')) {
2018
+ closeModal(modal.id);
2019
+ }
2020
+ });
2021
+ });
2022
+ </script>
2023
+
2024
+ </body>
2025
+ </html>
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Business Gemini OpenAPI 兼容服务依赖
2
+ # Python 3.8+
3
+
4
+ # Web框架
5
+ flask>=2.0.0
6
+ flask-cors>=3.0.0
7
+
8
+ # HTTP请求
9
+ requests>=2.25.0
10
+
11
+ # SSL警告处理(requests依赖)
12
+ urllib3>=1.26.0
13
+
14
+ # 可选:用于从浏览器获取 Cookie (CDP)
15
+ websocket-client>=1.0.0