xiaoyukkkk commited on
Commit
da9c7fc
·
verified ·
1 Parent(s): 2a40305

Upload 8 files

Browse files
Files changed (3) hide show
  1. Dockerfile +14 -12
  2. README.md +35 -34
  3. main.py +175 -1720
Dockerfile CHANGED
@@ -1,13 +1,15 @@
1
- FROM python:3.11-slim
2
- WORKDIR /app
3
- COPY requirements.txt .
4
- RUN apt-get update && apt-get install -y --no-install-recommends \
5
- gcc \
6
- && pip install --no-cache-dir -r requirements.txt \
7
- && apt-get purge -y gcc \
8
- && apt-get autoremove -y \
9
- && rm -rf /var/lib/apt/lists/*
10
- COPY main.py .
11
- # 复制 util 目录
12
- COPY util ./util
 
 
13
  CMD ["python", "-u", "main.py"]
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ gcc \
6
+ && pip install --no-cache-dir -r requirements.txt \
7
+ && apt-get purge -y gcc \
8
+ && apt-get autoremove -y \
9
+ && rm -rf /var/lib/apt/lists/*
10
+ COPY main.py .
11
+ # 复制 core 模块
12
+ COPY core ./core
13
+ # 复制 util 目录
14
+ COPY util ./util
15
  CMD ["python", "-u", "main.py"]
README.md CHANGED
@@ -38,12 +38,12 @@ license: mit
38
 
39
  <table>
40
  <tr>
41
- <td><img src="https://github.com/user-attachments/assets/5378ad0b-2a96-4239-9582-df68e8ffa53f" alt="图片生成示例1" width="800"/></td>
42
- <td><img src="https://github.com/user-attachments/assets/82aeedf8-ad87-40bc-ae41-defa7271e8b1" alt="图片生成示例2" width="800"/></td>
43
  </tr>
44
  <tr>
45
- <td><img src="https://github.com/user-attachments/assets/772d29c3-a75c-4ab8-98eb-cefea164b62c" alt="图片生成示例3" width="800"/></td>
46
- <td><img src="https://github.com/user-attachments/assets/15d36604-dedd-4411-bbfb-f4cf95365101" alt="图片生成示例4" width="800"/></td>
47
  </tr>
48
  </table>
49
 
@@ -51,17 +51,17 @@ license: mit
51
 
52
  <table>
53
  <tr>
54
- <td><img src="https://github.com/user-attachments/assets/42c0a906-e926-4199-97e4-73ac06381b07" alt="管理面板1" width="800"/></td>
55
- <td><img src="https://github.com/user-attachments/assets/d6b9a16b-2ed1-4be9-8465-4c28f974daa7" alt="管理面板2" width="800"/></td>
56
  </tr>
57
  </table>
58
 
59
- ###开日志系统
60
 
61
  <table>
62
  <tr>
63
- <td><img src="https://github.com/user-attachments/assets/8640d7ce-9d22-41de-9907-5c2292883f0c" alt="日志系统1" width="800"/></td>
64
- <td><img src="https://github.com/user-attachments/assets/048a6253-fa7d-4e00-a748-2bb615e16638" alt="日志系统2" width="800"/></td>
65
  </tr>
66
  </table>
67
 
@@ -204,6 +204,27 @@ ACCOUNTS_CONFIG='[
204
  | `gemini-3-flash-preview` | Flash 3 预览版 | ❌ |
205
  | `gemini-3-pro-preview` | Pro 3 预览版 | ✅ |
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  ### 基本对话
208
 
209
  ```bash
@@ -273,27 +294,6 @@ curl -X POST http://localhost:7860/v1/v1/chat/completions \
273
  }'
274
  ```
275
 
276
- ### 其他端点
277
-
278
- | 端点 | 方法 | 说明 |
279
- | -------------------------- | ---- | --------------------- |
280
- | `/{PATH_PREFIX}/v1/models` | GET | 获取模型列表 |
281
- | `/{PATH_PREFIX}/admin` | GET | 管理面板(需ADMIN_KEY) |
282
- | `/public/log/html` | GET | 公开日志页面 |
283
- | `/health` | GET | 健康检查 |
284
-
285
- **访问示例**:
286
-
287
- 假设你的配置为:
288
- - Space URL: `https://your-space.hf.space`
289
- - PATH_PREFIX: `my_prefix`
290
- - ADMIN_KEY: `my_admin_key`
291
-
292
- 则访问地址为:
293
- - **管理面板**: `https://your-space.hf.space/my_prefix/admin?key=my_admin_key`
294
- - **公开日志**: `https://your-space.hf.space/public/log/html`
295
- - **API 端点**: `https://your-space.hf.space/my_prefix/v1/chat/completions`
296
-
297
  ## ❓ 常见问题
298
 
299
  ### 1. 图片生成后在哪里找到文件?
@@ -405,15 +405,16 @@ MIT License - 查看 [LICENSE](LICENSE) 文件了解详情
405
 
406
  ## 🙏 致谢
407
 
408
- - 源项目:[heixxin/gemini](https://huggingface.co/spaces/heixxin/gemini/tree/main) | [Linux.do 讨论](https://linux.do/t/topic/1226413)
409
- - 绘图参考:[Gemini-Link-System](https://github.com/qxd-ljy/Gemini-Link-System) | [Linux.do 讨论](https://linux.do/t/topic/1234363)
410
- - Gemini Business 2API Helper 参考:[Linux.do 讨论](https://linux.do/t/topic/1231008)
 
411
 
412
  ---
413
 
414
  ## ⭐ Star History
415
 
416
- [![Star History Chart](https://api.star-history.com/svg?repos=Dreamy-rain/gemini-business2api&type=Date)](https://star-history.com/#Dreamy-rain/gemini-business2api&Date)
417
 
418
  ---
419
 
 
38
 
39
  <table>
40
  <tr>
41
+ <td><img src="https://github.com/user-attachments/assets/d6837897-63f2-4a17-ba4a-f59030e37018" alt="图片生成示例1" width="800"/></td>
42
+ <td><img src="https://github.com/user-attachments/assets/dc597631-b00b-4307-bba1-c0ed21db0e1b" alt="图片生成示例2" width="800"/></td>
43
  </tr>
44
  <tr>
45
+ <td><img src="https://github.com/user-attachments/assets/4e3a1ffa-dea9-4207-ac9b-bb32f8e83c6f" alt="图片生成示例3" width="800"/></td>
46
+ <td><img src="https://github.com/user-attachments/assets/53a30edd-c2ec-4cd3-a0bd-ccf68884472a" alt="图片生成示例4" width="800"/></td>
47
  </tr>
48
  </table>
49
 
 
51
 
52
  <table>
53
  <tr>
54
+ <td><img src="https://github.com/user-attachments/assets/d0548b2b-b57e-4857-8ed0-b48b4daef34f" alt="管理面板1" width="800"/></td>
55
+ <td><img src="https://github.com/user-attachments/assets/6b2aff95-e48f-412f-9e6e-2e893595b6dd" alt="管理面板2" width="800"/></td>
56
  </tr>
57
  </table>
58
 
59
+ ### 日志系统
60
 
61
  <table>
62
  <tr>
63
+ <td><img src="https://github.com/user-attachments/assets/4c9c38c4-6322-4057-b5f0-a10f8b82b6ae" alt="日志系统1" width="800"/></td>
64
+ <td><img src="https://github.com/user-attachments/assets/095b86d7-3924-4258-954a-85bda9e8478e" alt="日志系统2" width="800"/></td>
65
  </tr>
66
  </table>
67
 
 
204
  | `gemini-3-flash-preview` | Flash 3 预览版 | ❌ |
205
  | `gemini-3-pro-preview` | Pro 3 预览版 | ✅ |
206
 
207
+ ### 访问端点
208
+
209
+ | 端点 | 方法 | 说明 |
210
+ | -------------------------- | ---- | --------------------- |
211
+ | `/{PATH_PREFIX}/v1/models` | GET | 获取模型列表 |
212
+ | `/{PATH_PREFIX}/admin` | GET | 管理面板(需ADMIN_KEY) |
213
+ | `/public/log/html` | GET | 公开日志页面 |
214
+ | `/health` | GET | 健康检查 |
215
+
216
+ **访问示例**:
217
+
218
+ 假设你的配置为:
219
+ - Space URL: `https://your-space.hf.space`
220
+ - PATH_PREFIX: `my_prefix`
221
+ - ADMIN_KEY: `my_admin_key`
222
+
223
+ 则访问地址为:
224
+ - **管理面板**: `https://your-space.hf.space/my_prefix/admin?key=my_admin_key`
225
+ - **公开日志**: `https://your-space.hf.space/public/log/html`
226
+ - **API 端点**: `https://your-space.hf.space/my_prefix/v1/chat/completions`
227
+
228
  ### 基本对话
229
 
230
  ```bash
 
294
  }'
295
  ```
296
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  ## ❓ 常见问题
298
 
299
  ### 1. 图片生成后在哪里找到文件?
 
405
 
406
  ## 🙏 致谢
407
 
408
+ * 源项目:[F佬 Linux.do 讨论](https://linux.do/t/topic/1225645)
409
+ * 源项目:[heixxin/gemini](https://huggingface.co/spaces/heixxin/gemini/tree/main) | [Linux.do 讨论](https://linux.do/t/topic/1226413)
410
+ * 绘图参考:[Gemini-Link-System](https://github.com/qxd-ljy/Gemini-Link-System) | [Linux.do 讨论](https://linux.do/t/topic/1234363)
411
+ * Gemini Business 2API Helper 参考:[Linux.do 讨论](https://linux.do/t/topic/1231008)
412
 
413
  ---
414
 
415
  ## ⭐ Star History
416
 
417
+ [![Star History Chart](https://api.star-history.com/svg?repos=Dreamy-rain/gemini-business2api&type=date&legend=top-left)](https://www.star-history.com/#Dreamy-rain/gemini-business2api&type=date&legend=top-left)
418
 
419
  ---
420
 
main.py CHANGED
@@ -6,7 +6,7 @@ import logging
6
  from dotenv import load_dotenv
7
 
8
  import httpx
9
- from fastapi import FastAPI, HTTPException, Header, Request
10
  from fastapi.responses import StreamingResponse, HTMLResponse, JSONResponse
11
  from fastapi.staticfiles import StaticFiles
12
  from pydantic import BaseModel
@@ -15,6 +15,9 @@ from collections import deque
15
  from threading import Lock
16
  from functools import wraps
17
 
 
 
 
18
  # ---------- 日志配置 ----------
19
 
20
  # 内存日志缓冲区 (保留最近 3000 条日志,重启后清空)
@@ -323,6 +326,19 @@ class MultiAccountManager:
323
  del self.global_session_cache[key]
324
  logger.info(f"[CACHE] LRU清理 {remove_count} 个最旧会话缓存")
325
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  async def set_session_cache(self, conv_key: str, account_id: str, session_id: str):
327
  """线程安全地设置会话缓存"""
328
  async with self._lock:
@@ -384,15 +400,32 @@ class MultiAccountManager:
384
  logger.info(f"[MULTI] [ACCOUNT] {req_tag}选择账户: {account_id}")
385
  return account
386
 
387
- # ---------- 多账户配置加载 ----------
388
- def load_multi_account_config() -> MultiAccountManager:
389
- """从环境变量加载多账户配置(仅支持 ACCOUNTS_CONFIG JSON 格式)"""
390
- manager = MultiAccountManager()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
 
 
392
  accounts_json = os.getenv("ACCOUNTS_CONFIG")
393
  if not accounts_json:
394
  raise ValueError(
395
- "未找到 ACCOUNTS_CONFIG 环境变量。\n"
396
  "请在环境变量中配置 JSON 格式的账户列表,格式示例:\n"
397
  '[{"id":"account_1","csesidx":"xxx","config_id":"yyy","secure_c_ses":"zzz","host_c_oses":null,"expires_at":"2025-12-23 10:59:21"}]'
398
  )
@@ -401,50 +434,86 @@ def load_multi_account_config() -> MultiAccountManager:
401
  accounts_data = json.loads(accounts_json)
402
  if not isinstance(accounts_data, list):
403
  raise ValueError("ACCOUNTS_CONFIG 必须是 JSON 数组格式")
 
 
 
 
 
 
 
404
 
405
- for i, acc in enumerate(accounts_data, 1):
406
- # 验证必需字段
407
- required_fields = ["secure_c_ses", "csesidx", "config_id"]
408
- missing_fields = [f for f in required_fields if f not in acc]
409
- if missing_fields:
410
- raise ValueError(f"账户 {i} 缺少必需字段: {', '.join(missing_fields)}")
411
-
412
- config = AccountConfig(
413
- account_id=acc.get("id", f"account_{i}"),
414
- secure_c_ses=acc["secure_c_ses"],
415
- host_c_oses=acc.get("host_c_oses"),
416
- csesidx=acc["csesidx"],
417
- config_id=acc["config_id"],
418
- expires_at=acc.get("expires_at")
419
- )
420
 
421
- # 检查账户是否已过期
422
- if config.is_expired():
423
- logger.warning(f"[CONFIG] 账户 {config.account_id} 已过期,跳过加载")
424
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
 
426
- manager.add_account(config)
 
 
 
427
 
428
- if not manager.accounts:
429
- raise ValueError("没有有效的账户配置(可能全部已过期)")
430
 
431
- logger.info(f"[CONFIG] 成功加载 {len(manager.accounts)} 个账户")
432
- return manager
433
 
434
- except json.JSONDecodeError as e:
435
- logger.error(f"[CONFIG] ACCOUNTS_CONFIG JSON 解析失败: {str(e)}")
436
- raise ValueError(f"ACCOUNTS_CONFIG 格式错误: {str(e)}")
437
- except KeyError as e:
438
- logger.error(f"[CONFIG] ACCOUNTS_CONFIG 缺少必需字段: {str(e)}")
439
- raise ValueError(f"ACCOUNTS_CONFIG 缺少必需字段: {str(e)}")
440
- except Exception as e:
441
- logger.error(f"[CONFIG] 加载账户配置失败: {str(e)}")
442
- raise
443
 
444
 
445
  # 初始化多账户管理器
446
  multi_account_mgr = load_multi_account_config()
447
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  # 验证必需的环境变量
449
  if not PATH_PREFIX:
450
  logger.error("[SYSTEM] 未配置 PATH_PREFIX 环境变量,请设置后重启")
@@ -649,23 +718,17 @@ if IMAGE_DIR == "/data/images":
649
  else:
650
  logger.info(f"[SYSTEM] 图片静态服务已启用: /images/ -> {IMAGE_DIR} (临时存储,重启会丢失)")
651
 
652
- # ---------- 认证装饰器 ----------
653
-
654
- def require_admin_key(func):
655
- """验证管理员密钥(支持 URL 参数或 Header)"""
656
- @wraps(func)
657
- async def wrapper(*args, key: str = None, authorization: str = None, **kwargs):
658
- # 支持 URL 参数 ?key=xxx 或 Authorization Header
659
- admin_key = key
660
- if not admin_key and authorization:
661
- admin_key = authorization.replace("Bearer ", "") if authorization.startswith("Bearer ") else authorization
662
-
663
- if admin_key != ADMIN_KEY:
664
- # 返回 404 而不是 401,假装端点不存在
665
- raise HTTPException(404, "Not Found")
666
 
667
- return await func(*args, **kwargs)
668
- return wrapper
 
669
 
670
  # ---------- 日志脱敏函数 ----------
671
  def get_sanitized_logs(limit: int = 100) -> list:
@@ -929,524 +992,6 @@ def verify_api_key(authorization: str = None):
929
 
930
  return True
931
 
932
- def generate_admin_html(request: Request, show_hide_tip: bool = False) -> str:
933
- """生成管理页面HTML - 端点带Key参数完整版"""
934
- # 获取当前页面的完整URL
935
- current_url = get_base_url(request)
936
-
937
- # 获取错误统计
938
- error_count = 0
939
- with log_lock:
940
- for log in log_buffer:
941
- if log.get("level") in ["ERROR", "CRITICAL"]:
942
- error_count += 1
943
-
944
- # --- 1. 构建提示信息 ---
945
- hide_tip = ""
946
- if show_hide_tip:
947
- hide_tip = """
948
- <div class="alert alert-info">
949
- <div class="alert-icon">💡</div>
950
- <div class="alert-content">
951
- <strong>提示</strong>:此页面默认在首页显示。如需隐藏,请设置环境变量:<br>
952
- <code style="margin-top:4px; display:inline-block;">HIDE_HOME_PAGE=true</code>
953
- </div>
954
- </div>
955
- """
956
-
957
- api_key_status = ""
958
- if API_KEY:
959
- api_key_status = """
960
- <div class="alert alert-success">
961
- <div class="alert-icon">🔒</div>
962
- <div class="alert-content">
963
- <strong>安全模式已启用</strong>
964
- <div class="alert-desc">请求 Header 需携带 Authorization 密钥。</div>
965
- </div>
966
- </div>
967
- """
968
- else:
969
- api_key_status = """
970
- <div class="alert alert-warning">
971
- <div class="alert-icon">⚠️</div>
972
- <div class="alert-content">
973
- <strong>API Key 未设置</strong>
974
- <div class="alert-desc">API 当前允许公开访问,建议配置 API_KEY。</div>
975
- </div>
976
- </div>
977
- """
978
-
979
- error_alert = ""
980
- if error_count > 0:
981
- error_alert = f"""
982
- <div class="alert alert-error">
983
- <div class="alert-icon">🚨</div>
984
- <div class="alert-content">
985
- <strong>检测到 {error_count} 条错误日志</strong>
986
- <a href="/public/log/html" class="alert-link">查看详情 &rarr;</a>
987
- </div>
988
- </div>
989
- """
990
-
991
- # --- 2. 构建账户卡片 ---
992
- accounts_html = ""
993
- for account_id, account_manager in multi_account_mgr.accounts.items():
994
- config = account_manager.config
995
- remaining_hours = config.get_remaining_hours()
996
- status_text, status_color, expire_display = format_account_expiration(remaining_hours)
997
-
998
- is_avail = account_manager.is_available
999
- dot_color = "#34c759" if is_avail else "#ff3b30"
1000
- dot_title = "可用" if is_avail else "不可用"
1001
-
1002
- accounts_html += f"""
1003
- <div class="card account-card">
1004
- <div class="acc-header">
1005
- <div class="acc-title">
1006
- <span class="status-dot" style="background-color: {dot_color};" title="{dot_title}"></span>
1007
- {config.account_id}
1008
- </div>
1009
- <span class="acc-status-text" style="color: {status_color}">{status_text}</span>
1010
- </div>
1011
- <div class="acc-body">
1012
- <div class="acc-row">
1013
- <span>过期时间</span>
1014
- <span class="font-mono">{config.expires_at or '未设置'}</span>
1015
- </div>
1016
- <div class="acc-row">
1017
- <span>剩余时长</span>
1018
- <span style="color: {status_color}; font-weight: 600;">{expire_display}</span>
1019
- </div>
1020
- </div>
1021
- </div>
1022
- """
1023
-
1024
- # --- 3. 构建 HTML ---
1025
- html_content = f"""
1026
- <!DOCTYPE html>
1027
- <html lang="zh-CN">
1028
- <head>
1029
- <meta charset="utf-8">
1030
- <meta name="viewport" content="width=device-width, initial-scale=1">
1031
- <title>系统管理 - Gemini Business API</title>
1032
- <style>
1033
- :root {{
1034
- --bg-body: #f5f5f7;
1035
- --text-main: #1d1d1f;
1036
- --text-sec: #86868b;
1037
- --border: #d2d2d7;
1038
- --border-light: #e5e5ea;
1039
- --blue: #0071e3;
1040
- --red: #ff3b30;
1041
- --green: #34c759;
1042
- --orange: #ff9500;
1043
- }}
1044
-
1045
- * {{ margin: 0; padding: 0; box-sizing: border-box; }}
1046
-
1047
- body {{
1048
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Helvetica, Arial, sans-serif;
1049
- background-color: var(--bg-body);
1050
- color: var(--text-main);
1051
- font-size: 13px;
1052
- line-height: 1.5;
1053
- -webkit-font-smoothing: antialiased;
1054
- padding: 30px 20px;
1055
- cursor: default;
1056
- }}
1057
-
1058
- .container {{ max-width: 1100px; margin: 0 auto; }}
1059
-
1060
- /* Header */
1061
- .header {{
1062
- display: flex;
1063
- justify-content: space-between;
1064
- align-items: center;
1065
- margin-bottom: 24px;
1066
- flex-wrap: wrap;
1067
- gap: 16px;
1068
- }}
1069
- .header-info h1 {{
1070
- font-size: 24px;
1071
- font-weight: 600;
1072
- letter-spacing: -0.5px;
1073
- color: var(--text-main);
1074
- margin-bottom: 4px;
1075
- }}
1076
- .header-info .subtitle {{ font-size: 14px; color: var(--text-sec); }}
1077
- .header-actions {{ display: flex; gap: 10px; }}
1078
-
1079
- /* Buttons */
1080
- .btn {{
1081
- display: inline-flex;
1082
- align-items: center;
1083
- padding: 8px 16px;
1084
- background: #ffffff;
1085
- border: 1px solid var(--border-light);
1086
- border-radius: 8px;
1087
- color: var(--text-main);
1088
- font-weight: 500;
1089
- text-decoration: none;
1090
- transition: all 0.2s;
1091
- font-size: 13px;
1092
- cursor: pointer;
1093
- box-shadow: 0 1px 2px rgba(0,0,0,0.03);
1094
- }}
1095
- .btn:hover {{ background: #fafafa; border-color: var(--border); text-decoration: none; }}
1096
- .btn-primary {{ background: var(--blue); color: white; border: none; }}
1097
- .btn-primary:hover {{ background: #0077ed; border: none; text-decoration: none; }}
1098
-
1099
- /* Alerts */
1100
- .alert {{
1101
- padding: 12px 16px;
1102
- border-radius: 10px;
1103
- display: flex;
1104
- align-items: flex-start;
1105
- gap: 12px;
1106
- font-size: 13px;
1107
- border: 1px solid transparent;
1108
- margin-bottom: 12px;
1109
- }}
1110
- .alert-icon {{ font-size: 16px; margin-top: 1px; flex-shrink: 0; }}
1111
- .alert-content {{ flex: 1; }}
1112
- .alert-desc {{ color: inherit; opacity: 0.9; margin-top: 2px; font-size: 12px; }}
1113
- .alert-link {{ color: inherit; text-decoration: underline; margin-left: 10px; font-weight: 600; cursor: pointer; }}
1114
- .alert-info {{ background: #eef7fe; border-color: #dcebfb; color: #1c5b96; }}
1115
- .alert-success {{ background: #eafbf0; border-color: #d3f3dd; color: #15682e; }}
1116
- .alert-warning {{ background: #fff8e6; border-color: #fcebc2; color: #9c6e03; }}
1117
- .alert-error {{ background: #ffebeb; border-color: #fddddd; color: #c41e1e; }}
1118
-
1119
- /* Sections & Grids */
1120
- .section {{ margin-bottom: 30px; }}
1121
- .section-title {{
1122
- font-size: 15px;
1123
- font-weight: 600;
1124
- color: var(--text-main);
1125
- margin-bottom: 12px;
1126
- padding-left: 4px;
1127
- }}
1128
- .grid-3 {{ display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; align-items: start; }}
1129
- .grid-env {{ display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }}
1130
- .account-grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }}
1131
- .stack-col {{ display: flex; flex-direction: column; gap: 16px; }}
1132
-
1133
- /* Cards */
1134
- .card {{
1135
- background: #fafaf9;
1136
- padding: 20px;
1137
- border: 1px solid #e5e5e5;
1138
- border-radius: 12px;
1139
- transition: all 0.15s ease;
1140
- }}
1141
- .card:hover {{ border-color: #d4d4d4; box-shadow: 0 0 8px rgba(0,0,0,0.08); }}
1142
- .card h3 {{
1143
- font-size: 13px;
1144
- font-weight: 600;
1145
- color: var(--text-sec);
1146
- margin-bottom: 12px;
1147
- padding-bottom: 8px;
1148
- border-bottom: 1px solid #f5f5f5;
1149
- text-transform: uppercase;
1150
- letter-spacing: 0.5px;
1151
- }}
1152
-
1153
- /* Account & Env Styles */
1154
- .account-card .acc-header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #f5f5f5; }}
1155
- .acc-title {{ font-weight: 600; font-size: 14px; display: flex; align-items: center; gap: 8px; }}
1156
- .status-dot {{ width: 8px; height: 8px; border-radius: 50%; }}
1157
- .acc-status-text {{ font-size: 12px; font-weight: 500; }}
1158
- .acc-row {{ display: flex; justify-content: space-between; font-size: 12px; margin-top: 6px; color: var(--text-sec); }}
1159
-
1160
- .env-var {{ display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #f5f5f5; }}
1161
- .env-var:last-child {{ border-bottom: none; }}
1162
- .env-name {{ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, monospace; font-size: 12px; color: var(--text-main); font-weight: 600; }}
1163
- .env-desc {{ font-size: 11px; color: var(--text-sec); margin-top: 2px; }}
1164
- .env-value {{ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, monospace; font-size: 12px; color: var(--text-sec); text-align: right; max-width: 50%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }}
1165
-
1166
- .badge {{ display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; vertical-align: middle; margin-left: 6px; }}
1167
- .badge-required {{ background: #ffebeb; color: #c62828; }}
1168
- .badge-optional {{ background: #e8f5e9; color: #2e7d32; }}
1169
-
1170
- code {{ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, monospace; background: #f5f5f7; padding: 2px 6px; border-radius: 4px; font-size: 12px; color: var(--blue); }}
1171
- a {{ color: var(--blue); text-decoration: none; }}
1172
- a:hover {{ text-decoration: underline; }}
1173
- .font-mono {{ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, monospace; }}
1174
-
1175
- /* --- Service Info Styles --- */
1176
- .model-grid {{ display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }}
1177
- .model-tag {{
1178
- background: #f0f0f2;
1179
- color: #1d1d1f;
1180
- padding: 4px 10px;
1181
- border-radius: 6px;
1182
- font-size: 12px;
1183
- font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, monospace;
1184
- border: 1px solid transparent;
1185
- }}
1186
- .model-tag.highlight {{ background: #eef7ff; color: #0071e3; border-color: #dcebfb; font-weight: 500; }}
1187
-
1188
- .info-box {{ background: #f9f9f9; border: 1px solid #e5e5ea; border-radius: 8px; padding: 14px; }}
1189
- .info-box-title {{ font-weight: 600; font-size: 12px; color: #1d1d1f; margin-bottom: 6px; }}
1190
- .info-box-text {{ font-size: 12px; color: #86868b; line-height: 1.5; }}
1191
-
1192
- .ep-table {{ width: 100%; border-collapse: collapse; font-size: 13px; }}
1193
- .ep-table tr {{ border-bottom: 1px solid #f5f5f5; }}
1194
- .ep-table tr:last-child {{ border-bottom: none; }}
1195
- .ep-table td {{ padding: 10px 0; vertical-align: middle; }}
1196
-
1197
- .method {{
1198
- display: inline-block;
1199
- padding: 2px 6px;
1200
- border-radius: 4px;
1201
- font-size: 10px;
1202
- font-weight: 700;
1203
- text-transform: uppercase;
1204
- min-width: 48px;
1205
- text-align: center;
1206
- margin-right: 8px;
1207
- }}
1208
- .m-post {{ background: #eafbf0; color: #166534; border: 1px solid #dcfce7; }}
1209
- .m-get {{ background: #eff6ff; color: #1e40af; border: 1px solid #dbeafe; }}
1210
- .m-del {{ background: #fef2f2; color: #991b1b; border: 1px solid #fee2e2; }}
1211
-
1212
- .ep-path {{ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, monospace; color: #1d1d1f; margin-right: 8px; font-size: 12px; }}
1213
- .ep-desc {{ color: #86868b; font-size: 12px; margin-left: auto; }}
1214
-
1215
- .current-url-row {{
1216
- display: flex;
1217
- align-items: center;
1218
- padding: 10px 12px;
1219
- background: #f2f7ff;
1220
- border-radius: 8px;
1221
- margin-bottom: 16px;
1222
- border: 1px solid #e1effe;
1223
- }}
1224
-
1225
- @media (max-width: 800px) {{
1226
- .grid-3, .grid-env {{ grid-template-columns: 1fr; }}
1227
- .header {{ flex-direction: column; align-items: flex-start; gap: 16px; }}
1228
- .header-actions {{ width: 100%; justify-content: flex-start; }}
1229
- .ep-table td {{ display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }}
1230
- .ep-desc {{ margin-left: 0; }}
1231
- }}
1232
- </style>
1233
- </head>
1234
- <body>
1235
- <div class="container">
1236
- <div class="header">
1237
- <div class="header-info">
1238
- <h1>Gemini-Business2api</h1>
1239
- <div class="subtitle">多账户代理面板</div>
1240
- </div>
1241
- <div class="header-actions">
1242
- <a href="/public/log/html" class="btn" target="_blank">📄 公开日志</a>
1243
- <a href="/{PATH_PREFIX}/admin/log/html?key={ADMIN_KEY}" class="btn btn-primary" target="_blank">🔧 管理日志</a>
1244
- </div>
1245
- </div>
1246
-
1247
- {hide_tip}
1248
- {api_key_status}
1249
- {error_alert}
1250
-
1251
- <div class="section">
1252
- <div class="section-title">账户状态 ({len(multi_account_mgr.accounts)} 个)</div>
1253
- <div style="color: #6b6b6b; font-size: 12px; margin-bottom: 12px; padding-left: 4px;">过期时间为12小时,可以自行修改时间,脚本可能有误差。</div>
1254
- <div class="account-grid">
1255
- {accounts_html if accounts_html else '<div class="card"><p style="color: #6b6b6b; font-size: 14px; text-align:center;">暂无账户</p></div>'}
1256
- </div>
1257
- </div>
1258
-
1259
- <div class="section">
1260
- <div class="section-title">环境变量配置</div>
1261
- <div class="grid-env">
1262
- <div class="stack-col">
1263
- <div class="card">
1264
- <h3>必需变量 <span class="badge badge-required">REQUIRED</span></h3>
1265
- <div style="margin-top: 12px;">
1266
- <div class="env-var">
1267
- <div><div class="env-name">ACCOUNTS_CONFIG</div><div class="env-desc">JSON格式账户列表</div></div>
1268
- </div>
1269
- <div class="env-var">
1270
- <div><div class="env-name">PATH_PREFIX</div><div class="env-desc">API路径前缀</div></div>
1271
- <div class="env-value">当前: {PATH_PREFIX}</div>
1272
- </div>
1273
- <div class="env-var">
1274
- <div><div class="env-name">ADMIN_KEY</div><div class="env-desc">管理员密钥</div></div>
1275
- <div class="env-value">已设置</div>
1276
- </div>
1277
- </div>
1278
- </div>
1279
-
1280
- <div class="card">
1281
- <h3>重试配置 <span class="badge badge-optional">OPTIONAL</span></h3>
1282
- <div style="margin-top: 12px;">
1283
- <div class="env-var">
1284
- <div><div class="env-name">MAX_NEW_SESSION_TRIES</div><div class="env-desc">新会话尝试账户数</div></div>
1285
- <div class="env-value">{MAX_NEW_SESSION_TRIES}</div>
1286
- </div>
1287
- <div class="env-var">
1288
- <div><div class="env-name">MAX_REQUEST_RETRIES</div><div class="env-desc">请求失败重试次数</div></div>
1289
- <div class="env-value">{MAX_REQUEST_RETRIES}</div>
1290
- </div>
1291
- <div class="env-var">
1292
- <div><div class="env-name">ACCOUNT_FAILURE_THRESHOLD</div><div class="env-desc">账户失败阈值</div></div>
1293
- <div class="env-value">{ACCOUNT_FAILURE_THRESHOLD} 次</div>
1294
- </div>
1295
- <div class="env-var">
1296
- <div><div class="env-name">ACCOUNT_COOLDOWN_SECONDS</div><div class="env-desc">账户冷却时间</div></div>
1297
- <div class="env-value">{ACCOUNT_COOLDOWN_SECONDS} 秒</div>
1298
- </div>
1299
- </div>
1300
- </div>
1301
- </div>
1302
-
1303
- <div class="card">
1304
- <h3>可选变量 <span class="badge badge-optional">OPTIONAL</span></h3>
1305
- <div style="margin-top: 12px;">
1306
- <div class="env-var">
1307
- <div><div class="env-name">API_KEY</div><div class="env-desc">API访问密钥</div></div>
1308
- <div class="env-value">{'已设置' if API_KEY else '未设置'}</div>
1309
- </div>
1310
- <div class="env-var">
1311
- <div><div class="env-name">BASE_URL</div><div class="env-desc">图片URL生成(推荐设置)</div></div>
1312
- <div class="env-value">{'已设置' if BASE_URL else '未设置(自动检测)'}</div>
1313
- </div>
1314
- <div class="env-var">
1315
- <div><div class="env-name">PROXY</div><div class="env-desc">代理地址</div></div>
1316
- <div class="env-value">{'已设置' if PROXY else '未设置'}</div>
1317
- </div>
1318
- <div class="env-var">
1319
- <div><div class="env-name">SESSION_CACHE_TTL_SECONDS</div><div class="env-desc">会话缓存过期时间</div></div>
1320
- <div class="env-value">{SESSION_CACHE_TTL_SECONDS} 秒</div>
1321
- </div>
1322
- <div class="env-var">
1323
- <div><div class="env-name">LOGO_URL</div><div class="env-desc">Logo URL(公开,为空则不显示)</div></div>
1324
- <div class="env-value">{'已设���' if LOGO_URL else '未设置'}</div>
1325
- </div>
1326
- <div class="env-var">
1327
- <div><div class="env-name">CHAT_URL</div><div class="env-desc">开始对话链接(公开,为空则不显示)</div></div>
1328
- <div class="env-value">{'已设置' if CHAT_URL else '未设置'}</div>
1329
- </div>
1330
- <div class="env-var">
1331
- <div><div class="env-name">MODEL_NAME</div><div class="env-desc">模型名称(公开)</div></div>
1332
- <div class="env-value">{MODEL_NAME}</div>
1333
- </div>
1334
- <div class="env-var">
1335
- <div><div class="env-name">HIDE_HOME_PAGE</div><div class="env-desc">隐藏首页管理面板</div></div>
1336
- <div class="env-value">{'已隐藏' if HIDE_HOME_PAGE else '未隐藏'}</div>
1337
- </div>
1338
- </div>
1339
- </div>
1340
- </div>
1341
- </div>
1342
-
1343
- <div class="section">
1344
- <div class="section-title">服务信息</div>
1345
- <div class="grid-3">
1346
- <div class="card">
1347
- <h3>支持的模型</h3>
1348
- <div class="model-grid">
1349
- <span class="model-tag">gemini-auto</span>
1350
- <span class="model-tag">gemini-2.5-flash</span>
1351
- <span class="model-tag">gemini-2.5-pro</span>
1352
- <span class="model-tag">gemini-3-flash-preview</span>
1353
- <span class="model-tag highlight">gemini-3-pro-preview</span>
1354
- </div>
1355
-
1356
- <div class="info-box">
1357
- <div class="info-box-title">📸 图片生成说明</div>
1358
- <div class="info-box-text">
1359
- 仅 <code style="background:none;padding:0;color:#0071e3;">gemini-3-pro-preview</code> 支持绘图。<br>
1360
- 路径: <code>{IMAGE_DIR}</code><br>
1361
- 类型: {'<span style="color: #34c759; font-weight: 600;">持久化(重启保留)</span>' if IMAGE_DIR == '/data/images' else '<span style="color: #ff3b30; font-weight: 600;">临时(重启丢失)</span>'}
1362
- </div>
1363
- </div>
1364
- </div>
1365
-
1366
- <div class="card" style="grid-column: span 2;">
1367
- <h3>API 端点</h3>
1368
-
1369
- <div class="current-url-row">
1370
- <span style="font-size:12px; font-weight:600; color:#0071e3; margin-right:8px;">当前页面:</span>
1371
- <code style="background:none; padding:0; color:#1d1d1f;">{current_url}</code>
1372
- </div>
1373
-
1374
- <table class="ep-table">
1375
- <tr>
1376
- <td width="70"><span class="method m-post">POST</span></td>
1377
- <td><span class="ep-path">/{PATH_PREFIX}/v1/chat/completions</span></td>
1378
- <td><span class="ep-desc">OpenAI 兼容对话接口</span></td>
1379
- </tr>
1380
- <tr>
1381
- <td><span class="method m-get">GET</span></td>
1382
- <td><span class="ep-path">/{PATH_PREFIX}/v1/models</span></td>
1383
- <td><span class="ep-desc">获取模型列表</span></td>
1384
- </tr>
1385
- <tr>
1386
- <td><span class="method m-get">GET</span></td>
1387
- <td><span class="ep-path">/{PATH_PREFIX}/admin</span></td>
1388
- <td><span class="ep-desc">管理首页</span></td>
1389
- </tr>
1390
- <tr>
1391
- <td><span class="method m-get">GET</span></td>
1392
- <td><span class="ep-path">/{PATH_PREFIX}/admin/health?key={{ADMIN_KEY}}</span></td>
1393
- <td><span class="ep-desc">健康检查 (需 Key)</span></td>
1394
- </tr>
1395
- <tr>
1396
- <td><span class="method m-get">GET</span></td>
1397
- <td><span class="ep-path">/{PATH_PREFIX}/admin/accounts?key={{ADMIN_KEY}}</span></td>
1398
- <td><span class="ep-desc">账户状态 JSON (需 Key)</span></td>
1399
- </tr>
1400
- <tr>
1401
- <td><span class="method m-get">GET</span></td>
1402
- <td><span class="ep-path">/{PATH_PREFIX}/admin/log?key={{ADMIN_KEY}}</span></td>
1403
- <td><span class="ep-desc">获取日志 JSON (需 Key)</span></td>
1404
- </tr>
1405
- <tr>
1406
- <td><span class="method m-get">GET</span></td>
1407
- <td><span class="ep-path">/{PATH_PREFIX}/admin/log/html?key={{ADMIN_KEY}}</span></td>
1408
- <td><span class="ep-desc">日志查看器 HTML (需 Key)</span></td>
1409
- </tr>
1410
- <tr>
1411
- <td><span class="method m-del">DEL</span></td>
1412
- <td><span class="ep-path">/{PATH_PREFIX}/admin/log?confirm=yes&key={{ADMIN_KEY}}</span></td>
1413
- <td><span class="ep-desc">清空系统日志 (需 Key)</span></td>
1414
- </tr>
1415
- <tr>
1416
- <td><span class="method m-get">GET</span></td>
1417
- <td><span class="ep-path">/public/stats</span></td>
1418
- <td><span class="ep-desc">公开统计数据</span></td>
1419
- </tr>
1420
- <tr>
1421
- <td><span class="method m-get">GET</span></td>
1422
- <td><span class="ep-path">/public/log</span></td>
1423
- <td><span class="ep-desc">公开日志 (JSON, 脱敏)</span></td>
1424
- </tr>
1425
- <tr>
1426
- <td><span class="method m-get">GET</span></td>
1427
- <td><span class="ep-path">/public/log/html</span></td>
1428
- <td><span class="ep-desc">公开日志查看器 (HTML)</span></td>
1429
- </tr>
1430
- <tr>
1431
- <td><span class="method m-get">GET</span></td>
1432
- <td><span class="ep-path">/docs</span></td>
1433
- <td><span class="ep-desc">Swagger API 文档</span></td>
1434
- </tr>
1435
- <tr>
1436
- <td><span class="method m-get">GET</span></td>
1437
- <td><span class="ep-path">/redoc</span></td>
1438
- <td><span class="ep-desc">ReDoc API 文档</span></td>
1439
- </tr>
1440
- </table>
1441
- </div>
1442
- </div>
1443
- </div>
1444
- </div>
1445
- </body>
1446
- </html>
1447
- """
1448
- return html_content
1449
-
1450
  @app.get("/")
1451
  async def home(request: Request):
1452
  """首页 - 默认显示管理面板(可通过环境变量隐藏)"""
@@ -1455,32 +1000,20 @@ async def home(request: Request):
1455
  raise HTTPException(404, "Not Found")
1456
 
1457
  # 显示管理页面(带隐藏提示)
1458
- html_content = generate_admin_html(request, show_hide_tip=True)
1459
  return HTMLResponse(content=html_content)
1460
 
1461
  @app.get("/{path_prefix}/admin")
1462
- @app.get("/{path_prefix}/admin/")
1463
  async def admin_home(path_prefix: str, request: Request, key: str = None, authorization: str = Header(None)):
1464
  """管理首页 - 显示API信息和错误提醒"""
1465
- # 验证路径前缀
1466
- if path_prefix != PATH_PREFIX:
1467
- raise HTTPException(404, "Not Found")
1468
-
1469
- # 验证管理员密钥
1470
- admin_key = key or (authorization.replace("Bearer ", "") if authorization and authorization.startswith("Bearer ") else authorization)
1471
- if admin_key != ADMIN_KEY:
1472
- raise HTTPException(404, "Not Found")
1473
-
1474
  # 显示管理页面(不显示隐藏提示)
1475
- html_content = generate_admin_html(request, show_hide_tip=False)
1476
  return HTMLResponse(content=html_content)
1477
 
1478
  @app.get("/{path_prefix}/v1/models")
 
1479
  async def list_models(path_prefix: str, authorization: str = Header(None)):
1480
- # 验证路径前缀
1481
- if path_prefix != PATH_PREFIX:
1482
- raise HTTPException(404, "Not Found")
1483
-
1484
  # 验证 API Key
1485
  verify_api_key(authorization)
1486
 
@@ -1497,41 +1030,22 @@ async def list_models(path_prefix: str, authorization: str = Header(None)):
1497
  return {"object": "list", "data": data}
1498
 
1499
  @app.get("/{path_prefix}/v1/models/{model_id}")
 
1500
  async def get_model(path_prefix: str, model_id: str, authorization: str = Header(None)):
1501
- # 验证路径前缀
1502
- if path_prefix != PATH_PREFIX:
1503
- raise HTTPException(404, "Not Found")
1504
-
1505
  # 验证 API Key
1506
  verify_api_key(authorization)
1507
 
1508
  return {"id": model_id, "object": "model"}
1509
 
1510
  @app.get("/{path_prefix}/admin/health")
 
1511
  async def admin_health(path_prefix: str, key: str = None, authorization: str = Header(None)):
1512
- # 验证路径前缀
1513
- if path_prefix != PATH_PREFIX:
1514
- raise HTTPException(404, "Not Found")
1515
-
1516
- # 验证管理员密钥
1517
- admin_key = key or (authorization.replace("Bearer ", "") if authorization and authorization.startswith("Bearer ") else authorization)
1518
- if admin_key != ADMIN_KEY:
1519
- raise HTTPException(404, "Not Found")
1520
-
1521
  return {"status": "ok", "time": datetime.utcnow().isoformat()}
1522
 
1523
  @app.get("/{path_prefix}/admin/accounts")
 
1524
  async def admin_get_accounts(path_prefix: str, key: str = None, authorization: str = Header(None)):
1525
  """获取所有账户的状态信息"""
1526
- # 验证路径前缀
1527
- if path_prefix != PATH_PREFIX:
1528
- raise HTTPException(404, "Not Found")
1529
-
1530
- # 验证管理员密钥
1531
- admin_key = key or (authorization.replace("Bearer ", "") if authorization and authorization.startswith("Bearer ") else authorization)
1532
- if admin_key != ADMIN_KEY:
1533
- raise HTTPException(404, "Not Found")
1534
-
1535
  accounts_info = []
1536
  for account_id, account_manager in multi_account_mgr.accounts.items():
1537
  config = account_manager.config
@@ -1555,7 +1069,41 @@ async def admin_get_accounts(path_prefix: str, key: str = None, authorization: s
1555
  "accounts": accounts_info
1556
  }
1557
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1558
  @app.get("/{path_prefix}/admin/log")
 
1559
  async def admin_get_logs(
1560
  path_prefix: str,
1561
  limit: int = 1500,
@@ -1576,15 +1124,6 @@ async def admin_get_logs(
1576
  - start_time: 开始时间 (格式: 2025-12-17 10:00:00)
1577
  - end_time: 结束时间 (格式: 2025-12-17 11:00:00)
1578
  """
1579
- # 验证路径前缀
1580
- if path_prefix != PATH_PREFIX:
1581
- raise HTTPException(404, "Not Found")
1582
-
1583
- # 验证管理员密钥
1584
- admin_key = key or (authorization.replace("Bearer ", "") if authorization and authorization.startswith("Bearer ") else authorization)
1585
- if admin_key != ADMIN_KEY:
1586
- raise HTTPException(404, "Not Found")
1587
-
1588
  with log_lock:
1589
  logs = list(log_buffer)
1590
 
@@ -1648,6 +1187,7 @@ async def admin_get_logs(
1648
  }
1649
 
1650
  @app.delete("/{path_prefix}/admin/log")
 
1651
  async def admin_clear_logs(path_prefix: str, confirm: str = None, key: str = None, authorization: str = Header(None)):
1652
  """
1653
  清空所有日志(内存缓冲 + 文件)
@@ -1655,15 +1195,6 @@ async def admin_clear_logs(path_prefix: str, confirm: str = None, key: str = Non
1655
  参数:
1656
  - confirm: 必须传入 "yes" 才能清空
1657
  """
1658
- # 验证路径前缀
1659
- if path_prefix != PATH_PREFIX:
1660
- raise HTTPException(404, "Not Found")
1661
-
1662
- # 验证管理员密钥
1663
- admin_key = key or (authorization.replace("Bearer ", "") if authorization and authorization.startswith("Bearer ") else authorization)
1664
- if admin_key != ADMIN_KEY:
1665
- raise HTTPException(404, "Not Found")
1666
-
1667
  if confirm != "yes":
1668
  raise HTTPException(
1669
  status_code=400,
@@ -1684,648 +1215,18 @@ async def admin_clear_logs(path_prefix: str, confirm: str = None, key: str = Non
1684
  }
1685
 
1686
  @app.get("/{path_prefix}/admin/log/html")
1687
- async def admin_logs_html(path_prefix: str, key: str = None, authorization: str = Header(None)):
1688
  """返回美化的 HTML 日志查看界面"""
1689
- # 验证路径前缀
1690
- if path_prefix != PATH_PREFIX:
1691
- raise HTTPException(404, "Not Found")
1692
-
1693
- # 验证管理员密钥
1694
- admin_key = key or (authorization.replace("Bearer ", "") if authorization and authorization.startswith("Bearer ") else authorization)
1695
- if admin_key != ADMIN_KEY:
1696
- raise HTTPException(404, "Not Found")
1697
-
1698
- html_content = r"""
1699
- <!DOCTYPE html>
1700
- <html>
1701
- <head>
1702
- <meta charset="utf-8">
1703
- <meta name="viewport" content="width=device-width, initial-scale=1">
1704
- <title>日志查看器</title>
1705
- <style>
1706
- * { margin: 0; padding: 0; box-sizing: border-box; }
1707
- html, body { height: 100%; overflow: hidden; }
1708
- body {
1709
- font-family: 'Consolas', 'Monaco', monospace;
1710
- background: #fafaf9;
1711
- display: flex;
1712
- align-items: center;
1713
- justify-content: center;
1714
- padding: 15px;
1715
- }
1716
- .container {
1717
- width: 100%;
1718
- max-width: 1400px;
1719
- height: calc(100vh - 30px);
1720
- background: white;
1721
- border-radius: 16px;
1722
- padding: 30px;
1723
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
1724
- display: flex;
1725
- flex-direction: column;
1726
- }
1727
- h1 { color: #1a1a1a; font-size: 22px; font-weight: 600; margin-bottom: 20px; text-align: center; }
1728
- .stats {
1729
- display: grid;
1730
- grid-template-columns: repeat(6, 1fr);
1731
- gap: 12px;
1732
- margin-bottom: 16px;
1733
- }
1734
- .stat {
1735
- background: #fafaf9;
1736
- padding: 12px;
1737
- border: 1px solid #e5e5e5;
1738
- border-radius: 8px;
1739
- text-align: center;
1740
- transition: all 0.15s ease;
1741
- }
1742
- .stat:hover { border-color: #d4d4d4; }
1743
- .stat-label { color: #6b6b6b; font-size: 11px; margin-bottom: 4px; }
1744
- .stat-value { color: #1a1a1a; font-size: 18px; font-weight: 600; }
1745
- .controls {
1746
- display: flex;
1747
- gap: 8px;
1748
- margin-bottom: 16px;
1749
- flex-wrap: wrap;
1750
- }
1751
- .controls input, .controls select, .controls button {
1752
- padding: 6px 10px;
1753
- border: 1px solid #e5e5e5;
1754
- border-radius: 8px;
1755
- font-size: 13px;
1756
- }
1757
- .controls select {
1758
- appearance: none;
1759
- -webkit-appearance: none;
1760
- -moz-appearance: none;
1761
- background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 5L6 8L9 5' stroke='%236b6b6b' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
1762
- background-repeat: no-repeat;
1763
- background-position: right 12px center;
1764
- padding-right: 32px;
1765
- }
1766
- .controls input[type="text"] { flex: 1; min-width: 150px; }
1767
- .controls button {
1768
- background: #1a73e8;
1769
- color: white;
1770
- border: none;
1771
- cursor: pointer;
1772
- font-weight: 500;
1773
- transition: background 0.15s ease;
1774
- display: flex;
1775
- align-items: center;
1776
- gap: 6px;
1777
- }
1778
- .controls button:hover { background: #1557b0; }
1779
- .controls button.danger { background: #dc2626; }
1780
- .controls button.danger:hover { background: #b91c1c; }
1781
- .controls button svg { flex-shrink: 0; }
1782
- .log-container {
1783
- flex: 1;
1784
- background: #fafaf9;
1785
- border: 1px solid #e5e5e5;
1786
- border-radius: 8px;
1787
- padding: 12px;
1788
- overflow-y: auto;
1789
- scrollbar-width: thin;
1790
- scrollbar-color: rgba(0,0,0,0.15) transparent;
1791
- }
1792
- /* Webkit 滚动条样式 - 更窄且不占位 */
1793
- .log-container::-webkit-scrollbar {
1794
- width: 4px;
1795
- }
1796
- .log-container::-webkit-scrollbar-track {
1797
- background: transparent;
1798
- }
1799
- .log-container::-webkit-scrollbar-thumb {
1800
- background: rgba(0,0,0,0.15);
1801
- border-radius: 2px;
1802
- }
1803
- .log-container::-webkit-scrollbar-thumb:hover {
1804
- background: rgba(0,0,0,0.3);
1805
- }
1806
- .log-entry {
1807
- padding: 8px 10px;
1808
- margin-bottom: 4px;
1809
- background: white;
1810
- border-radius: 6px;
1811
- border: 1px solid #e5e5e5;
1812
- font-size: 12px;
1813
- color: #1a1a1a;
1814
- display: flex;
1815
- align-items: center;
1816
- gap: 8px;
1817
- word-break: break-word;
1818
- }
1819
- .log-entry > div:first-child {
1820
- display: flex;
1821
- align-items: center;
1822
- gap: 8px;
1823
- }
1824
- .log-message {
1825
- flex: 1;
1826
- overflow: hidden;
1827
- text-overflow: ellipsis;
1828
- }
1829
- .log-entry:hover { border-color: #d4d4d4; }
1830
- .log-time { color: #6b6b6b; }
1831
- .log-level {
1832
- display: flex;
1833
- align-items: center;
1834
- gap: 4px;
1835
- padding: 2px 6px;
1836
- border-radius: 3px;
1837
- font-size: 10px;
1838
- font-weight: 600;
1839
- }
1840
- .log-level::before {
1841
- content: '';
1842
- width: 6px;
1843
- height: 6px;
1844
- border-radius: 50%;
1845
- }
1846
- .log-level.INFO { background: #e3f2fd; color: #1976d2; }
1847
- .log-level.INFO::before { background: #1976d2; }
1848
- .log-level.WARNING { background: #fff3e0; color: #f57c00; }
1849
- .log-level.WARNING::before { background: #f57c00; }
1850
- .log-level.ERROR { background: #ffebee; color: #d32f2f; }
1851
- .log-level.ERROR::before { background: #d32f2f; }
1852
- .log-level.DEBUG { background: #f3e5f5; color: #7b1fa2; }
1853
- .log-level.DEBUG::before { background: #7b1fa2; }
1854
- .log-group {
1855
- margin-bottom: 8px;
1856
- border: 1px solid #e5e5e5;
1857
- border-radius: 8px;
1858
- background: white;
1859
- }
1860
- .log-group-header {
1861
- padding: 10px 12px;
1862
- background: #f9f9f9;
1863
- border-radius: 8px 8px 0 0;
1864
- cursor: pointer;
1865
- display: flex;
1866
- align-items: center;
1867
- gap: 8px;
1868
- transition: background 0.15s ease;
1869
- }
1870
- .log-group-header:hover {
1871
- background: #f0f0f0;
1872
- }
1873
- .log-group-content {
1874
- padding: 8px;
1875
- }
1876
- .log-group .log-entry {
1877
- margin-bottom: 4px;
1878
- }
1879
- .log-group .log-entry:last-child {
1880
- margin-bottom: 0;
1881
- }
1882
- .toggle-icon {
1883
- display: inline-block;
1884
- transition: transform 0.2s ease;
1885
- }
1886
- .toggle-icon.collapsed {
1887
- transform: rotate(-90deg);
1888
- }
1889
- @media (max-width: 768px) {
1890
- body { padding: 0; }
1891
- .container { padding: 15px; height: 100vh; border-radius: 0; max-width: 100%; }
1892
- h1 { font-size: 18px; margin-bottom: 12px; }
1893
- .stats { grid-template-columns: repeat(3, 1fr); gap: 8px; }
1894
- .stat { padding: 8px; }
1895
- .controls { gap: 6px; }
1896
- .controls input, .controls select { min-height: 38px; }
1897
- .controls select { flex: 0 0 auto; }
1898
- .controls input[type="text"] { flex: 1 1 auto; min-width: 80px; }
1899
- .controls input[type="number"] { flex: 0 0 60px; }
1900
- .controls button { padding: 10px 8px; font-size: 12px; flex: 1 1 22%; justify-content: center; min-height: 38px; }
1901
- .log-entry {
1902
- font-size: 12px;
1903
- padding: 10px;
1904
- gap: 8px;
1905
- flex-direction: column;
1906
- align-items: flex-start;
1907
- }
1908
- .log-entry > div:first-child {
1909
- display: flex;
1910
- align-items: center;
1911
- gap: 6px;
1912
- width: 100%;
1913
- flex-wrap: wrap;
1914
- }
1915
- .log-time { font-size: 11px; color: #9e9e9e; }
1916
- .log-level { font-size: 10px; }
1917
- .log-message {
1918
- width: 100%;
1919
- white-space: normal;
1920
- word-break: break-word;
1921
- line-height: 1.5;
1922
- margin-top: 4px;
1923
- }
1924
- }
1925
- </style>
1926
- </head>
1927
- <body>
1928
- <div class="container">
1929
- <h1>Gemini API 日志查看器</h1>
1930
- <div class="stats">
1931
- <div class="stat">
1932
- <div class="stat-label">总数</div>
1933
- <div class="stat-value" id="total-count">-</div>
1934
- </div>
1935
- <div class="stat">
1936
- <div class="stat-label">对话</div>
1937
- <div class="stat-value" id="chat-count">-</div>
1938
- </div>
1939
- <div class="stat">
1940
- <div class="stat-label">INFO</div>
1941
- <div class="stat-value" id="info-count">-</div>
1942
- </div>
1943
- <div class="stat">
1944
- <div class="stat-label">WARNING</div>
1945
- <div class="stat-value" id="warning-count">-</div>
1946
- </div>
1947
- <div class="stat">
1948
- <div class="stat-label">ERROR</div>
1949
- <div class="stat-value" id="error-count">-</div>
1950
- </div>
1951
- <div class="stat">
1952
- <div class="stat-label">更新</div>
1953
- <div class="stat-value" id="last-update" style="font-size: 11px;">-</div>
1954
- </div>
1955
- </div>
1956
- <div class="controls">
1957
- <select id="level-filter">
1958
- <option value="">全部</option>
1959
- <option value="INFO">INFO</option>
1960
- <option value="WARNING">WARNING</option>
1961
- <option value="ERROR">ERROR</option>
1962
- </select>
1963
- <input type="text" id="search-input" placeholder="搜索...">
1964
- <input type="number" id="limit-input" value="1500" min="10" max="3000" step="100" style="width: 80px;">
1965
- <button onclick="loadLogs()">
1966
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1967
- <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
1968
- </svg>
1969
- 查询
1970
- </button>
1971
- <button onclick="exportJSON()">
1972
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1973
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
1974
- </svg>
1975
- 导出
1976
- </button>
1977
- <button id="auto-refresh-btn" onclick="toggleAutoRefresh()">
1978
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1979
- <polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
1980
- </svg>
1981
- 自动刷新
1982
- </button>
1983
- <button onclick="clearAllLogs()" class="danger">
1984
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1985
- <polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
1986
- </svg>
1987
- 清空
1988
- </button>
1989
- </div>
1990
- <div class="log-container" id="log-container">
1991
- <div style="color: #6b6b6b;">正在加载...</div>
1992
- </div>
1993
- </div>
1994
- <script>
1995
- let autoRefreshTimer = null;
1996
- async function loadLogs() {
1997
- const level = document.getElementById('level-filter').value;
1998
- const search = document.getElementById('search-input').value;
1999
- const limit = document.getElementById('limit-input').value;
2000
- // 从当前 URL 获取 key 参数
2001
- const urlParams = new URLSearchParams(window.location.search);
2002
- const key = urlParams.get('key');
2003
- // 构建 API URL(使用当前路径的前缀)
2004
- const pathPrefix = window.location.pathname.split('/')[1];
2005
- let url = `/${pathPrefix}/admin/log?limit=${limit}`;
2006
- if (key) url += `&key=${key}`;
2007
- if (level) url += `&level=${level}`;
2008
- if (search) url += `&search=${encodeURIComponent(search)}`;
2009
- try {
2010
- const response = await fetch(url);
2011
- if (!response.ok) {
2012
- throw new Error(`HTTP ${response.status}`);
2013
- }
2014
- const data = await response.json();
2015
- if (data && data.logs) {
2016
- displayLogs(data.logs);
2017
- updateStats(data.stats);
2018
- document.getElementById('last-update').textContent = new Date().toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'});
2019
- } else {
2020
- throw new Error('Invalid data format');
2021
- }
2022
- } catch (error) {
2023
- document.getElementById('log-container').innerHTML = '<div class="log-entry ERROR">加载失败: ' + error.message + '</div>';
2024
- }
2025
- }
2026
- function updateStats(stats) {
2027
- document.getElementById('total-count').textContent = stats.memory.total;
2028
- document.getElementById('info-count').textContent = stats.memory.by_level.INFO || 0;
2029
- document.getElementById('warning-count').textContent = stats.memory.by_level.WARNING || 0;
2030
- const errorCount = document.getElementById('error-count');
2031
- errorCount.textContent = stats.memory.by_level.ERROR || 0;
2032
- if (stats.errors && stats.errors.count > 0) errorCount.style.color = '#dc2626';
2033
- document.getElementById('chat-count').textContent = stats.chat_count || 0;
2034
- }
2035
- // 分类颜色配置(提取到外部避免重复定义)
2036
- const CATEGORY_COLORS = {
2037
- 'SYSTEM': '#9e9e9e',
2038
- 'CONFIG': '#607d8b',
2039
- 'LOG': '#9e9e9e',
2040
- 'AUTH': '#4caf50',
2041
- 'SESSION': '#00bcd4',
2042
- 'FILE': '#ff9800',
2043
- 'CHAT': '#2196f3',
2044
- 'API': '#8bc34a',
2045
- 'CACHE': '#9c27b0',
2046
- 'ACCOUNT': '#f44336',
2047
- 'MULTI': '#673ab7'
2048
- };
2049
-
2050
- // 账户颜色配置(提取到外部避免重复定义)
2051
- const ACCOUNT_COLORS = {
2052
- 'account_1': '#9c27b0',
2053
- 'account_2': '#e91e63',
2054
- 'account_3': '#00bcd4',
2055
- 'account_4': '#4caf50',
2056
- 'account_5': '#ff9800'
2057
- };
2058
-
2059
- function getCategoryColor(category) {
2060
- return CATEGORY_COLORS[category] || '#757575';
2061
- }
2062
-
2063
- function getAccountColor(accountId) {
2064
- return ACCOUNT_COLORS[accountId] || '#757575';
2065
- }
2066
-
2067
- function displayLogs(logs) {
2068
- const container = document.getElementById('log-container');
2069
- if (logs.length === 0) {
2070
- container.innerHTML = '<div class="log-entry">暂无日志</div>';
2071
- return;
2072
- }
2073
-
2074
- // 按请求ID分组
2075
- const groups = {};
2076
- const ungrouped = [];
2077
-
2078
- logs.forEach(log => {
2079
- const msg = escapeHtml(log.message);
2080
- const reqMatch = msg.match(/\[req_([a-z0-9]+)\]/);
2081
-
2082
- if (reqMatch) {
2083
- const reqId = reqMatch[1];
2084
- if (!groups[reqId]) {
2085
- groups[reqId] = [];
2086
- }
2087
- groups[reqId].push(log);
2088
- } else {
2089
- ungrouped.push(log);
2090
- }
2091
- });
2092
-
2093
- // 渲染分组
2094
- let html = '';
2095
-
2096
- // 先渲染未分组的日志
2097
- ungrouped.forEach(log => {
2098
- html += renderLogEntry(log);
2099
- });
2100
-
2101
- // 读取折叠状态
2102
- const foldState = JSON.parse(localStorage.getItem('log-fold-state') || '{}');
2103
-
2104
- // 按请求ID分组渲染(最新的组在下面)
2105
- Object.keys(groups).forEach(reqId => {
2106
- const groupLogs = groups[reqId];
2107
- const firstLog = groupLogs[0];
2108
- const lastLog = groupLogs[groupLogs.length - 1];
2109
-
2110
- // 判断状态
2111
- let status = 'in_progress';
2112
- let statusColor = '#ff9800';
2113
- let statusText = '进行中';
2114
-
2115
- if (lastLog.message.includes('响应完成') || lastLog.message.includes('非流式响应完成')) {
2116
- status = 'success';
2117
- statusColor = '#4caf50';
2118
- statusText = '成功';
2119
- } else if (lastLog.level === 'ERROR' || lastLog.message.includes('失败')) {
2120
- status = 'error';
2121
- statusColor = '#f44336';
2122
- statusText = '失败';
2123
- } else {
2124
- // 检查超时(最后日志超过 5 分钟)
2125
- const lastLogTime = new Date(lastLog.time);
2126
- const now = new Date();
2127
- const diffMinutes = (now - lastLogTime) / 1000 / 60;
2128
- if (diffMinutes > 5) {
2129
- status = 'timeout';
2130
- statusColor = '#ffc107';
2131
- statusText = '超时';
2132
- }
2133
- }
2134
-
2135
- // 提取账户ID和模型
2136
- const accountMatch = firstLog.message.match(/\[account_(\d+)\]/);
2137
- const modelMatch = firstLog.message.match(/收到请求: ([^ ]+)/);
2138
- const accountId = accountMatch ? `account_${accountMatch[1]}` : '';
2139
- const model = modelMatch ? modelMatch[1] : '';
2140
-
2141
- // 检查折叠状态
2142
- const isCollapsed = foldState[reqId] === true;
2143
- const contentStyle = isCollapsed ? 'style="display: none;"' : '';
2144
- const iconClass = isCollapsed ? 'class="toggle-icon collapsed"' : 'class="toggle-icon"';
2145
-
2146
- html += `
2147
- <div class="log-group" data-req-id="${reqId}">
2148
- <div class="log-group-header" onclick="toggleGroup('${reqId}')">
2149
- <span style="color: ${statusColor}; font-weight: 600; font-size: 11px;">⬤ ${statusText}</span>
2150
- <span style="color: #666; font-size: 11px; margin-left: 8px;">req_${reqId}</span>
2151
- ${accountId ? `<span style="color: ${getAccountColor(accountId)}; font-size: 11px; margin-left: 8px;">${accountId}</span>` : ''}
2152
- ${model ? `<span style="color: #999; font-size: 11px; margin-left: 8px;">${model}</span>` : ''}
2153
- <span style="color: #999; font-size: 11px; margin-left: 8px;">${groupLogs.length}条日志</span>
2154
- <span ${iconClass} style="margin-left: auto; color: #999;">▼</span>
2155
- </div>
2156
- <div class="log-group-content" ${contentStyle}>
2157
- ${groupLogs.map(log => renderLogEntry(log)).join('')}
2158
- </div>
2159
- </div>
2160
- `;
2161
- });
2162
-
2163
- container.innerHTML = html;
2164
-
2165
- // 自动滚动到底部,显示最新日志
2166
- container.scrollTop = container.scrollHeight;
2167
- }
2168
-
2169
- function renderLogEntry(log) {
2170
- const msg = escapeHtml(log.message);
2171
- let displayMsg = msg;
2172
- let categoryTags = [];
2173
- let accountId = null;
2174
-
2175
- // 解析所有标签:[CATEGORY1] [CATEGORY2] [account_X] [req_X] message
2176
- let remainingMsg = msg;
2177
- const tagRegex = /^\[([A-Z_a-z0-9]+)\]/;
2178
-
2179
- while (true) {
2180
- const match = remainingMsg.match(tagRegex);
2181
- if (!match) break;
2182
-
2183
- const tag = match[1];
2184
- remainingMsg = remainingMsg.substring(match[0].length).trim();
2185
-
2186
- // 跳过req_标签(已在组头部显示)
2187
- if (tag.startsWith('req_')) {
2188
- continue;
2189
- }
2190
- // 判断是否为账户ID
2191
- else if (tag.startsWith('account_')) {
2192
- accountId = tag;
2193
- } else {
2194
- // 普通分类标签
2195
- categoryTags.push(tag);
2196
- }
2197
- }
2198
-
2199
- displayMsg = remainingMsg;
2200
-
2201
- // 生成分类标签HTML
2202
- const categoryTagsHtml = categoryTags.map(cat =>
2203
- `<span class="log-category" style="background: ${getCategoryColor(cat)}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 2px;">${cat}</span>`
2204
- ).join('');
2205
-
2206
- // 生成账户标签HTML
2207
- const accountTagHtml = accountId
2208
- ? `<span style="color: ${getAccountColor(accountId)}; font-size: 11px; font-weight: 600; margin-left: 2px;">${accountId}</span>`
2209
- : '';
2210
-
2211
- return `
2212
- <div class="log-entry ${log.level}">
2213
- <div>
2214
- <span class="log-time">${log.time}</span>
2215
- <span class="log-level ${log.level}">${log.level}</span>
2216
- ${categoryTagsHtml}
2217
- ${accountTagHtml}
2218
- </div>
2219
- <div class="log-message">${displayMsg}</div>
2220
- </div>
2221
- `;
2222
- }
2223
-
2224
- function toggleGroup(reqId) {
2225
- const group = document.querySelector(`.log-group[data-req-id="${reqId}"]`);
2226
- const content = group.querySelector('.log-group-content');
2227
- const icon = group.querySelector('.toggle-icon');
2228
-
2229
- const isCollapsed = content.style.display === 'none';
2230
- if (isCollapsed) {
2231
- content.style.display = 'block';
2232
- icon.classList.remove('collapsed');
2233
- } else {
2234
- content.style.display = 'none';
2235
- icon.classList.add('collapsed');
2236
- }
2237
-
2238
- // 保存折叠状态到 localStorage
2239
- const foldState = JSON.parse(localStorage.getItem('log-fold-state') || '{}');
2240
- foldState[reqId] = !isCollapsed;
2241
- localStorage.setItem('log-fold-state', JSON.stringify(foldState));
2242
- }
2243
- function escapeHtml(text) {
2244
- const div = document.createElement('div');
2245
- div.textContent = text;
2246
- return div.innerHTML;
2247
- }
2248
- async function exportJSON() {
2249
- try {
2250
- const urlParams = new URLSearchParams(window.location.search);
2251
- const key = urlParams.get('key');
2252
- const pathPrefix = window.location.pathname.split('/')[1];
2253
- let url = `/${pathPrefix}/admin/log?limit=3000`;
2254
- if (key) url += `&key=${key}`;
2255
- const response = await fetch(url);
2256
- const data = await response.json();
2257
- const blob = new Blob([JSON.stringify({exported_at: new Date().toISOString(), logs: data.logs}, null, 2)], {type: 'application/json'});
2258
- const blobUrl = URL.createObjectURL(blob);
2259
- const a = document.createElement('a');
2260
- a.href = blobUrl;
2261
- a.download = 'logs_' + new Date().toISOString().slice(0, 19).replace(/:/g, '-') + '.json';
2262
- a.click();
2263
- URL.revokeObjectURL(blobUrl);
2264
- alert('导出成功');
2265
- } catch (error) {
2266
- alert('导出失败: ' + error.message);
2267
- }
2268
- }
2269
- async function clearAllLogs() {
2270
- if (!confirm('确定清空所有日志?')) return;
2271
- try {
2272
- const urlParams = new URLSearchParams(window.location.search);
2273
- const key = urlParams.get('key');
2274
- const pathPrefix = window.location.pathname.split('/')[1];
2275
- let url = `/${pathPrefix}/admin/log?confirm=yes`;
2276
- if (key) url += `&key=${key}`;
2277
- const response = await fetch(url, {method: 'DELETE'});
2278
- if (response.ok) {
2279
- alert('已清空');
2280
- loadLogs();
2281
- } else {
2282
- alert('清空失败');
2283
- }
2284
- } catch (error) {
2285
- alert('清空失败: ' + error.message);
2286
- }
2287
- }
2288
- let autoRefreshEnabled = true;
2289
- function toggleAutoRefresh() {
2290
- autoRefreshEnabled = !autoRefreshEnabled;
2291
- const btn = document.getElementById('auto-refresh-btn');
2292
- if (autoRefreshEnabled) {
2293
- btn.style.background = '#1a73e8';
2294
- autoRefreshTimer = setInterval(loadLogs, 5000);
2295
- } else {
2296
- btn.style.background = '#6b6b6b';
2297
- if (autoRefreshTimer) {
2298
- clearInterval(autoRefreshTimer);
2299
- autoRefreshTimer = null;
2300
- }
2301
- }
2302
- }
2303
- document.addEventListener('DOMContentLoaded', () => {
2304
- loadLogs();
2305
- autoRefreshTimer = setInterval(loadLogs, 5000);
2306
- document.getElementById('search-input').addEventListener('keypress', (e) => {
2307
- if (e.key === 'Enter') loadLogs();
2308
- });
2309
- document.getElementById('level-filter').addEventListener('change', loadLogs);
2310
- document.getElementById('limit-input').addEventListener('change', loadLogs);
2311
- });
2312
- </script>
2313
- </body>
2314
- </html>
2315
- """
2316
- return HTMLResponse(content=html_content)
2317
 
2318
  @app.post("/{path_prefix}/v1/chat/completions")
 
2319
  async def chat(
2320
  path_prefix: str,
2321
  req: ChatRequest,
2322
  request: Request,
2323
  authorization: Optional[str] = Header(None)
2324
  ):
2325
- # 0. 验证路径前缀
2326
- if path_prefix != PATH_PREFIX:
2327
- raise HTTPException(404, "Not Found")
2328
-
2329
  # 1. API Key 验证
2330
  verify_api_key(authorization)
2331
 
@@ -2484,12 +1385,23 @@ async def chat(
2484
  request
2485
  ):
2486
  yield chunk
 
 
 
 
2487
  break
2488
 
2489
  except (httpx.ConnectError, httpx.ReadTimeout, ssl.SSLError, HTTPException) as e:
2490
  # 记录当前失败的账户
2491
  failed_accounts.add(account_manager.config.account_id)
2492
 
 
 
 
 
 
 
 
2493
  retry_count += 1
2494
 
2495
  # 详细记录错误信息
@@ -2932,464 +1844,7 @@ async def get_public_logs(request: Request, limit: int = 100):
2932
  @app.get("/public/log/html")
2933
  async def get_public_logs_html():
2934
  """公开的脱敏日志查看器"""
2935
- html_content = r"""
2936
- <!DOCTYPE html>
2937
- <html>
2938
- <head>
2939
- <meta charset="utf-8">
2940
- <meta name="viewport" content="width=device-width, initial-scale=1">
2941
- <title>服务状态</title>
2942
- <style>
2943
- * { margin: 0; padding: 0; box-sizing: border-box; }
2944
- html, body { height: 100%; overflow: hidden; }
2945
- body {
2946
- font-family: 'Consolas', 'Monaco', monospace;
2947
- background: #fafaf9;
2948
- display: flex;
2949
- align-items: center;
2950
- justify-content: center;
2951
- padding: 15px;
2952
- }
2953
- .container {
2954
- width: 100%;
2955
- max-width: 1200px;
2956
- height: calc(100vh - 30px);
2957
- background: white;
2958
- border-radius: 16px;
2959
- padding: 30px;
2960
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
2961
- display: flex;
2962
- flex-direction: column;
2963
- }
2964
- h1 {
2965
- color: #1a1a1a;
2966
- font-size: 22px;
2967
- font-weight: 600;
2968
- margin-bottom: 20px;
2969
- display: flex;
2970
- align-items: center;
2971
- justify-content: center;
2972
- gap: 12px;
2973
- }
2974
- h1 img {
2975
- width: 32px;
2976
- height: 32px;
2977
- border-radius: 8px;
2978
- }
2979
- .info-bar {
2980
- background: #f9f9f9;
2981
- border: 1px solid #e5e5e5;
2982
- border-radius: 8px;
2983
- padding: 12px 16px;
2984
- margin-bottom: 16px;
2985
- display: flex;
2986
- align-items: center;
2987
- justify-content: space-between;
2988
- flex-wrap: wrap;
2989
- gap: 12px;
2990
- }
2991
- .info-item {
2992
- display: flex;
2993
- align-items: center;
2994
- gap: 6px;
2995
- font-size: 13px;
2996
- color: #6b6b6b;
2997
- }
2998
- .info-item strong { color: #1a1a1a; }
2999
- .info-item a {
3000
- color: #1a73e8;
3001
- text-decoration: none;
3002
- font-weight: 500;
3003
- }
3004
- .info-item a:hover { text-decoration: underline; }
3005
- .stats {
3006
- display: grid;
3007
- grid-template-columns: repeat(4, 1fr);
3008
- gap: 12px;
3009
- margin-bottom: 16px;
3010
- }
3011
- .stat {
3012
- background: #fafaf9;
3013
- padding: 12px;
3014
- border: 1px solid #e5e5e5;
3015
- border-radius: 8px;
3016
- text-align: center;
3017
- transition: all 0.15s ease;
3018
- }
3019
- .stat:hover { border-color: #d4d4d4; }
3020
- .stat-label { color: #6b6b6b; font-size: 11px; margin-bottom: 4px; }
3021
- .stat-value { color: #1a1a1a; font-size: 18px; font-weight: 600; }
3022
- .log-container {
3023
- flex: 1;
3024
- background: #fafaf9;
3025
- border: 1px solid #e5e5e5;
3026
- border-radius: 8px;
3027
- padding: 12px;
3028
- overflow-y: auto;
3029
- scrollbar-width: thin;
3030
- scrollbar-color: rgba(0,0,0,0.15) transparent;
3031
- }
3032
- .log-container::-webkit-scrollbar { width: 4px; }
3033
- .log-container::-webkit-scrollbar-track { background: transparent; }
3034
- .log-container::-webkit-scrollbar-thumb {
3035
- background: rgba(0,0,0,0.15);
3036
- border-radius: 2px;
3037
- }
3038
- .log-container::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.3); }
3039
- .log-group {
3040
- margin-bottom: 8px;
3041
- border: 1px solid #e5e5e5;
3042
- border-radius: 8px;
3043
- background: white;
3044
- }
3045
- .log-group-header {
3046
- padding: 10px 12px;
3047
- background: #f9f9f9;
3048
- border-radius: 8px 8px 0 0;
3049
- cursor: pointer;
3050
- display: flex;
3051
- align-items: center;
3052
- gap: 8px;
3053
- transition: background 0.15s ease;
3054
- }
3055
- .log-group-header:hover { background: #f0f0f0; }
3056
- .log-group-content { padding: 8px; }
3057
- .log-entry {
3058
- padding: 8px 10px;
3059
- margin-bottom: 4px;
3060
- background: white;
3061
- border: 1px solid #e5e5e5;
3062
- border-radius: 6px;
3063
- display: flex;
3064
- align-items: center;
3065
- gap: 10px;
3066
- font-size: 13px;
3067
- transition: all 0.15s ease;
3068
- }
3069
- .log-entry:hover { border-color: #d4d4d4; }
3070
- .log-time { color: #6b6b6b; font-size: 12px; min-width: 140px; }
3071
- .log-status {
3072
- padding: 2px 8px;
3073
- border-radius: 4px;
3074
- font-size: 11px;
3075
- font-weight: 600;
3076
- min-width: 60px;
3077
- text-align: center;
3078
- }
3079
- .status-success { background: #d1fae5; color: #065f46; }
3080
- .status-error { background: #fee2e2; color: #991b1b; }
3081
- .status-in_progress { background: #fef3c7; color: #92400e; }
3082
- .status-timeout { background: #fef3c7; color: #92400e; }
3083
- .log-info { flex: 1; color: #374151; }
3084
- .toggle-icon {
3085
- display: inline-block;
3086
- transition: transform 0.2s ease;
3087
- }
3088
- .toggle-icon.collapsed { transform: rotate(-90deg); }
3089
- .subtitle-public {
3090
- display: flex;
3091
- justify-content: center;
3092
- align-items: center;
3093
- gap: 8px;
3094
- flex-wrap: wrap;
3095
- }
3096
-
3097
- @media (max-width: 768px) {
3098
- body { padding: 0; }
3099
- .container {
3100
- padding: 15px;
3101
- height: 100vh;
3102
- border-radius: 0;
3103
- max-width: 100%;
3104
- }
3105
- h1 { font-size: 18px; margin-bottom: 12px; }
3106
- .subtitle-public {
3107
- flex-direction: column;
3108
- gap: 6px;
3109
- }
3110
- .subtitle-public span {
3111
- font-size: 11px;
3112
- line-height: 1.6;
3113
- }
3114
- .subtitle-public a {
3115
- font-size: 12px;
3116
- font-weight: 600;
3117
- }
3118
- .info-bar {
3119
- padding: 10px 12px;
3120
- flex-direction: column;
3121
- align-items: flex-start;
3122
- gap: 8px;
3123
- }
3124
- .info-item { font-size: 12px; }
3125
- .stats {
3126
- grid-template-columns: repeat(2, 1fr);
3127
- gap: 8px;
3128
- margin-bottom: 12px;
3129
- }
3130
- .stat { padding: 8px; }
3131
- .stat-label { font-size: 10px; }
3132
- .stat-value { font-size: 16px; }
3133
- .log-container { padding: 8px; }
3134
- .log-group { margin-bottom: 6px; }
3135
- .log-group-header {
3136
- padding: 8px 10px;
3137
- font-size: 11px;
3138
- flex-wrap: wrap;
3139
- }
3140
- .log-group-header span { font-size: 10px !important; }
3141
- .log-entry {
3142
- padding: 6px 8px;
3143
- font-size: 11px;
3144
- flex-direction: column;
3145
- align-items: flex-start;
3146
- gap: 4px;
3147
- }
3148
- .log-time {
3149
- min-width: auto;
3150
- font-size: 10px;
3151
- }
3152
- .log-info {
3153
- font-size: 11px;
3154
- word-break: break-word;
3155
- }
3156
- }
3157
- </style>
3158
- </head>
3159
- <body>
3160
- <div class="container">
3161
- <h1>
3162
- """ + (f'<img src="{LOGO_URL}" alt="Logo">' if LOGO_URL else '') + r"""
3163
- Gemini服务状态
3164
- </h1>
3165
- <div style="text-align: center; color: #999; font-size: 12px; margin-bottom: 16px;" class="subtitle-public">
3166
- <span>展示最近1000条对话日志 · 每5秒自动更新</span>
3167
- """ + (f'<a href="{CHAT_URL}" target="_blank" style="color: #1a73e8; text-decoration: none;">开始对话</a>' if CHAT_URL else '<span style="color: #999;">开始对话</span>') + r"""
3168
- </div>
3169
- <div class="stats">
3170
- <div class="stat">
3171
- <div class="stat-label">总访问</div>
3172
- <div class="stat-value" id="stat-visitors">0</div>
3173
- </div>
3174
- <div class="stat">
3175
- <div class="stat-label">每分钟请求</div>
3176
- <div class="stat-value" id="stat-load">0</div>
3177
- </div>
3178
- <div class="stat">
3179
- <div class="stat-label">平均响应</div>
3180
- <div class="stat-value" id="stat-avg-time">-</div>
3181
- </div>
3182
- <div class="stat">
3183
- <div class="stat-label">成功率</div>
3184
- <div class="stat-value" id="stat-success-rate" style="color: #10b981;">-</div>
3185
- </div>
3186
- <div class="stat">
3187
- <div class="stat-label">对话次数</div>
3188
- <div class="stat-value" id="stat-total">0</div>
3189
- </div>
3190
- <div class="stat">
3191
- <div class="stat-label">成功</div>
3192
- <div class="stat-value" id="stat-success" style="color: #10b981;">0</div>
3193
- </div>
3194
- <div class="stat">
3195
- <div class="stat-label">失败</div>
3196
- <div class="stat-value" id="stat-error" style="color: #ef4444;">0</div>
3197
- </div>
3198
- <div class="stat">
3199
- <div class="stat-label">更新时间</div>
3200
- <div class="stat-value" id="stat-update-time" style="font-size: 14px; color: #6b6b6b;">--:--</div>
3201
- </div>
3202
- </div>
3203
- <div class="log-container" id="log-container">
3204
- <div style="text-align: center; color: #999; padding: 20px;">加载中...</div>
3205
- </div>
3206
- </div>
3207
- <script>
3208
- async function loadData() {
3209
- try {
3210
- // 并行加载日志和统计数据
3211
- const [logsResponse, statsResponse] = await Promise.all([
3212
- fetch('/public/log?limit=1000'),
3213
- fetch('/public/stats')
3214
- ]);
3215
-
3216
- const logsData = await logsResponse.json();
3217
- const statsData = await statsResponse.json();
3218
-
3219
- displayLogs(logsData.logs);
3220
- updateStats(logsData.logs, statsData);
3221
- } catch (error) {
3222
- document.getElementById('log-container').innerHTML = '<div style="text-align: center; color: #f44336; padding: 20px;">加载失败: ' + error.message + '</div>';
3223
- }
3224
- }
3225
-
3226
- function displayLogs(logs) {
3227
- const container = document.getElementById('log-container');
3228
- if (logs.length === 0) {
3229
- container.innerHTML = '<div style="text-align: center; color: #999; padding: 20px;">暂无日志</div>';
3230
- return;
3231
- }
3232
-
3233
- // 读取折叠状态
3234
- const foldState = JSON.parse(localStorage.getItem('public-log-fold-state') || '{}');
3235
-
3236
- let html = '';
3237
- logs.forEach(log => {
3238
- const reqId = log.request_id;
3239
-
3240
- // 状态图标和颜色
3241
- let statusColor = '#ff9800';
3242
- let statusText = '进行中';
3243
-
3244
- if (log.status === 'success') {
3245
- statusColor = '#4caf50';
3246
- statusText = '成功';
3247
- } else if (log.status === 'error') {
3248
- statusColor = '#f44336';
3249
- statusText = '失败';
3250
- } else if (log.status === 'timeout') {
3251
- statusColor = '#ffc107';
3252
- statusText = '超时';
3253
- }
3254
-
3255
- // 检查折叠状态
3256
- const isCollapsed = foldState[reqId] === true;
3257
- const contentStyle = isCollapsed ? 'style="display: none;"' : '';
3258
- const iconClass = isCollapsed ? 'class="toggle-icon collapsed"' : 'class="toggle-icon"';
3259
-
3260
- // 构建事件列表
3261
- let eventsHtml = '';
3262
- log.events.forEach(event => {
3263
- let eventClass = 'log-entry';
3264
- let eventLabel = '';
3265
-
3266
- if (event.type === 'start') {
3267
- eventLabel = '<span style="color: #2563eb; font-weight: 600;">开始对话</span>';
3268
- } else if (event.type === 'select') {
3269
- eventLabel = '<span style="color: #8b5cf6; font-weight: 600;">选择</span>';
3270
- } else if (event.type === 'retry') {
3271
- eventLabel = '<span style="color: #f59e0b; font-weight: 600;">重试</span>';
3272
- } else if (event.type === 'switch') {
3273
- eventLabel = '<span style="color: #06b6d4; font-weight: 600;">切换</span>';
3274
- } else if (event.type === 'complete') {
3275
- if (event.status === 'success') {
3276
- eventLabel = '<span style="color: #10b981; font-weight: 600;">完成</span>';
3277
- } else if (event.status === 'error') {
3278
- eventLabel = '<span style="color: #ef4444; font-weight: 600;">失败</span>';
3279
- } else if (event.status === 'timeout') {
3280
- eventLabel = '<span style="color: #f59e0b; font-weight: 600;">超时</span>';
3281
- }
3282
- }
3283
-
3284
- eventsHtml += `
3285
- <div class="${eventClass}">
3286
- <div class="log-time">${event.time}</div>
3287
- <div style="min-width: 60px;">${eventLabel}</div>
3288
- <div class="log-info">${event.content}</div>
3289
- </div>
3290
- `;
3291
- });
3292
-
3293
- html += `
3294
- <div class="log-group" data-req-id="${reqId}">
3295
- <div class="log-group-header" onclick="toggleGroup('${reqId}')">
3296
- <span style="color: ${statusColor}; font-weight: 600; font-size: 11px;">⬤ ${statusText}</span>
3297
- <span style="color: #666; font-size: 11px; margin-left: 8px;">req_${reqId}</span>
3298
- ${accountId ? `<span style="color: ${getAccountColor(accountId)}; font-size: 11px; margin-left: 8px;">${accountId}</span>` : ''}
3299
- ${model ? `<span style="color: #999; font-size: 11px; margin-left: 8px;">${model}</span>` : ''}
3300
- <span style="color: #999; font-size: 11px; margin-left: 8px;">${log.events.length}条事件</span>
3301
- <span ${iconClass} style="margin-left: auto; color: #999;">▼</span>
3302
- </div>
3303
- <div class="log-group-content" ${contentStyle}>
3304
- ${eventsHtml}
3305
- </div>
3306
- </div>
3307
- `;
3308
- });
3309
-
3310
- container.innerHTML = html;
3311
- }
3312
-
3313
- function updateStats(logs, statsData) {
3314
- const total = logs.length;
3315
- const successLogs = logs.filter(log => log.status === 'success');
3316
- const success = successLogs.length;
3317
- const error = logs.filter(log => log.status === 'error').length;
3318
-
3319
- // 计算平均响应时间
3320
- let avgTime = '-';
3321
- if (success > 0) {
3322
- let totalDuration = 0;
3323
- let count = 0;
3324
- successLogs.forEach(log => {
3325
- log.events.forEach(event => {
3326
- if (event.type === 'complete' && event.content.includes('耗时')) {
3327
- const match = event.content.match(/([\d.]+)s/);
3328
- if (match) {
3329
- totalDuration += parseFloat(match[1]);
3330
- count++;
3331
- }
3332
- }
3333
- });
3334
- });
3335
- if (count > 0) {
3336
- avgTime = (totalDuration / count).toFixed(1) + 's';
3337
- }
3338
- }
3339
-
3340
- // 计算成功率
3341
- const totalCompleted = success + error;
3342
- const successRate = totalCompleted > 0 ? ((success / totalCompleted) * 100).toFixed(1) + '%' : '-';
3343
-
3344
- // 更新日志统计
3345
- document.getElementById('stat-total').textContent = total;
3346
- document.getElementById('stat-success').textContent = success;
3347
- document.getElementById('stat-error').textContent = error;
3348
- document.getElementById('stat-success-rate').textContent = successRate;
3349
- document.getElementById('stat-avg-time').textContent = avgTime;
3350
-
3351
- // 更新全局统计
3352
- document.getElementById('stat-visitors').textContent = statsData.total_visitors;
3353
-
3354
- // 更新负载状态(带颜色)
3355
- const loadElement = document.getElementById('stat-load');
3356
- loadElement.textContent = statsData.requests_per_minute;
3357
- loadElement.style.color = statsData.load_color;
3358
-
3359
- // 更新时间
3360
- document.getElementById('stat-update-time').textContent = new Date().toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit', second: '2-digit'});
3361
- }
3362
-
3363
- function toggleGroup(reqId) {
3364
- const group = document.querySelector(`.log-group[data-req-id="${reqId}"]`);
3365
- const content = group.querySelector('.log-group-content');
3366
- const icon = group.querySelector('.toggle-icon');
3367
-
3368
- const isCollapsed = content.style.display === 'none';
3369
- if (isCollapsed) {
3370
- content.style.display = 'block';
3371
- icon.classList.remove('collapsed');
3372
- } else {
3373
- content.style.display = 'none';
3374
- icon.classList.add('collapsed');
3375
- }
3376
-
3377
- // 保存折叠状态
3378
- const foldState = JSON.parse(localStorage.getItem('public-log-fold-state') || '{}');
3379
- foldState[reqId] = !isCollapsed;
3380
- localStorage.setItem('public-log-fold-state', JSON.stringify(foldState));
3381
- }
3382
-
3383
- // 初始加载
3384
- loadData();
3385
-
3386
- // 自动刷新(每5秒)
3387
- setInterval(loadData, 5000);
3388
- </script>
3389
- </body>
3390
- </html>
3391
- """
3392
- return HTMLResponse(content=html_content)
3393
 
3394
  # ---------- 全局 404 处理(必须在最后) ----------
3395
 
 
6
  from dotenv import load_dotenv
7
 
8
  import httpx
9
+ from fastapi import FastAPI, HTTPException, Header, Request, Body
10
  from fastapi.responses import StreamingResponse, HTMLResponse, JSONResponse
11
  from fastapi.staticfiles import StaticFiles
12
  from pydantic import BaseModel
 
15
  from threading import Lock
16
  from functools import wraps
17
 
18
+ # 导入认证装饰器
19
+ from core.auth import require_path_prefix, require_admin_auth, require_path_and_admin
20
+
21
  # ---------- 日志配置 ----------
22
 
23
  # 内存日志缓冲区 (保留最近 3000 条日志,重启后清空)
 
326
  del self.global_session_cache[key]
327
  logger.info(f"[CACHE] LRU清理 {remove_count} 个最旧会话缓存")
328
 
329
+ async def start_background_cleanup(self):
330
+ """启动后台缓存清理任务(每5分钟执行一次)"""
331
+ try:
332
+ while True:
333
+ await asyncio.sleep(300) # 5分钟
334
+ async with self._lock:
335
+ self._clean_expired_cache()
336
+ self._ensure_cache_size()
337
+ except asyncio.CancelledError:
338
+ logger.info("[CACHE] 后台清理任务已停止")
339
+ except Exception as e:
340
+ logger.error(f"[CACHE] 后台清理任务异常: {e}")
341
+
342
  async def set_session_cache(self, conv_key: str, account_id: str, session_id: str):
343
  """线程安全地设置会话缓存"""
344
  async with self._lock:
 
400
  logger.info(f"[MULTI] [ACCOUNT] {req_tag}选择账户: {account_id}")
401
  return account
402
 
403
+ # ---------- 配置文件管理 ----------
404
+ ACCOUNTS_FILE = "accounts.json"
405
+
406
+ def save_accounts_to_file(accounts_data: list):
407
+ """保存账户配置到文件"""
408
+ with open(ACCOUNTS_FILE, 'w', encoding='utf-8') as f:
409
+ json.dump(accounts_data, f, ensure_ascii=False, indent=2)
410
+ logger.info(f"[CONFIG] 配置已保存到 {ACCOUNTS_FILE}")
411
+
412
+ def load_accounts_from_source() -> list:
413
+ """优先从文件加载,否则从环境变量加载"""
414
+ # 优先从文件加载
415
+ if os.path.exists(ACCOUNTS_FILE):
416
+ try:
417
+ with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f:
418
+ accounts_data = json.load(f)
419
+ logger.info(f"[CONFIG] 从文件加载配置: {ACCOUNTS_FILE}")
420
+ return accounts_data
421
+ except Exception as e:
422
+ logger.warning(f"[CONFIG] 文件加载失败,尝试环境变量: {str(e)}")
423
 
424
+ # 从环境变量加载
425
  accounts_json = os.getenv("ACCOUNTS_CONFIG")
426
  if not accounts_json:
427
  raise ValueError(
428
+ "未找到配置文件或 ACCOUNTS_CONFIG 环境变量。\n"
429
  "请在环境变量中配置 JSON 格式的账户列表,格式示例:\n"
430
  '[{"id":"account_1","csesidx":"xxx","config_id":"yyy","secure_c_ses":"zzz","host_c_oses":null,"expires_at":"2025-12-23 10:59:21"}]'
431
  )
 
434
  accounts_data = json.loads(accounts_json)
435
  if not isinstance(accounts_data, list):
436
  raise ValueError("ACCOUNTS_CONFIG 必须是 JSON 数组格式")
437
+ # 首次从环境变量加载后,保存到文件
438
+ save_accounts_to_file(accounts_data)
439
+ logger.info(f"[CONFIG] 从环境变量加载配置并保存到文件")
440
+ return accounts_data
441
+ except json.JSONDecodeError as e:
442
+ logger.error(f"[CONFIG] ACCOUNTS_CONFIG JSON 解析失败: {str(e)}")
443
+ raise ValueError(f"ACCOUNTS_CONFIG 格式错误: {str(e)}")
444
 
445
+ def get_account_id(acc: dict, index: int) -> str:
446
+ """获取账户ID(有显式ID则使用,否则生成默认ID)"""
447
+ return acc.get("id", f"account_{index}")
 
 
 
 
 
 
 
 
 
 
 
 
448
 
449
+ # ---------- 多账户配置加载 ----------
450
+ def load_multi_account_config() -> MultiAccountManager:
451
+ """从文件或环境变量加载多账户配置"""
452
+ manager = MultiAccountManager()
453
+
454
+ accounts_data = load_accounts_from_source()
455
+
456
+ for i, acc in enumerate(accounts_data, 1):
457
+ # 验证必需字段
458
+ required_fields = ["secure_c_ses", "csesidx", "config_id"]
459
+ missing_fields = [f for f in required_fields if f not in acc]
460
+ if missing_fields:
461
+ raise ValueError(f"账户 {i} 缺少必需字段: {', '.join(missing_fields)}")
462
+
463
+ config = AccountConfig(
464
+ account_id=get_account_id(acc, i),
465
+ secure_c_ses=acc["secure_c_ses"],
466
+ host_c_oses=acc.get("host_c_oses"),
467
+ csesidx=acc["csesidx"],
468
+ config_id=acc["config_id"],
469
+ expires_at=acc.get("expires_at")
470
+ )
471
 
472
+ # 检查账户是否已过期
473
+ if config.is_expired():
474
+ logger.warning(f"[CONFIG] 账户 {config.account_id} 已过期,跳过加载")
475
+ continue
476
 
477
+ manager.add_account(config)
 
478
 
479
+ if not manager.accounts:
480
+ raise ValueError("没有有效的账户配置(可能全部已过期)")
481
 
482
+ logger.info(f"[CONFIG] 成功加载 {len(manager.accounts)} 个账户")
483
+ return manager
 
 
 
 
 
 
 
484
 
485
 
486
  # 初始化多账户管理器
487
  multi_account_mgr = load_multi_account_config()
488
 
489
+ def reload_accounts():
490
+ """重新加载账户配置(清空缓存并重新加载)"""
491
+ global multi_account_mgr
492
+ multi_account_mgr.global_session_cache.clear()
493
+ multi_account_mgr = load_multi_account_config()
494
+ logger.info(f"[CONFIG] 配置已重载,当前账户数: {len(multi_account_mgr.accounts)}")
495
+
496
+ def update_accounts_config(accounts_data: list):
497
+ """更新账户配置(保存到文件并重新加载)"""
498
+ save_accounts_to_file(accounts_data)
499
+ reload_accounts()
500
+
501
+ def delete_account(account_id: str):
502
+ """删除单个账户"""
503
+ accounts_data = load_accounts_from_source()
504
+
505
+ # 过滤掉要删除的账户
506
+ filtered = [
507
+ acc for i, acc in enumerate(accounts_data, 1)
508
+ if get_account_id(acc, i) != account_id
509
+ ]
510
+
511
+ if len(filtered) == len(accounts_data):
512
+ raise ValueError(f"账户 {account_id} 不存在")
513
+
514
+ save_accounts_to_file(filtered)
515
+ reload_accounts()
516
+
517
  # 验证必需的环境变量
518
  if not PATH_PREFIX:
519
  logger.error("[SYSTEM] 未配置 PATH_PREFIX 环境变量,请设置后重启")
 
718
  else:
719
  logger.info(f"[SYSTEM] 图片静态服务已启用: /images/ -> {IMAGE_DIR} (临时存储,重启会丢失)")
720
 
721
+ # ---------- 后台任务启动 ----------
722
+ @app.on_event("startup")
723
+ async def startup_event():
724
+ """应用启动时初始化后台任务"""
725
+ # 启动缓存清理任务
726
+ asyncio.create_task(multi_account_mgr.start_background_cleanup())
727
+ logger.info("[SYSTEM] 后台缓存清理任务已启动(间隔: 5分钟)")
 
 
 
 
 
 
 
728
 
729
+ # ---------- 导入模板模块 ----------
730
+ # 注意:必须在所有全局变量初始化之后导入,避免循环依赖
731
+ from core import templates
732
 
733
  # ---------- 日志脱敏函数 ----------
734
  def get_sanitized_logs(limit: int = 100) -> list:
 
992
 
993
  return True
994
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
995
  @app.get("/")
996
  async def home(request: Request):
997
  """首页 - 默认显示管理面板(可通过环境变量隐藏)"""
 
1000
  raise HTTPException(404, "Not Found")
1001
 
1002
  # 显示管理页面(带隐藏提示)
1003
+ html_content = templates.generate_admin_html(request, multi_account_mgr, show_hide_tip=True)
1004
  return HTMLResponse(content=html_content)
1005
 
1006
  @app.get("/{path_prefix}/admin")
1007
+ @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1008
  async def admin_home(path_prefix: str, request: Request, key: str = None, authorization: str = Header(None)):
1009
  """管理首页 - 显示API信息和错误提醒"""
 
 
 
 
 
 
 
 
 
1010
  # 显示管理页面(不显示隐藏提示)
1011
+ html_content = templates.generate_admin_html(request, multi_account_mgr, show_hide_tip=False)
1012
  return HTMLResponse(content=html_content)
1013
 
1014
  @app.get("/{path_prefix}/v1/models")
1015
+ @require_path_prefix(PATH_PREFIX)
1016
  async def list_models(path_prefix: str, authorization: str = Header(None)):
 
 
 
 
1017
  # 验证 API Key
1018
  verify_api_key(authorization)
1019
 
 
1030
  return {"object": "list", "data": data}
1031
 
1032
  @app.get("/{path_prefix}/v1/models/{model_id}")
1033
+ @require_path_prefix(PATH_PREFIX)
1034
  async def get_model(path_prefix: str, model_id: str, authorization: str = Header(None)):
 
 
 
 
1035
  # 验证 API Key
1036
  verify_api_key(authorization)
1037
 
1038
  return {"id": model_id, "object": "model"}
1039
 
1040
  @app.get("/{path_prefix}/admin/health")
1041
+ @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1042
  async def admin_health(path_prefix: str, key: str = None, authorization: str = Header(None)):
 
 
 
 
 
 
 
 
 
1043
  return {"status": "ok", "time": datetime.utcnow().isoformat()}
1044
 
1045
  @app.get("/{path_prefix}/admin/accounts")
1046
+ @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1047
  async def admin_get_accounts(path_prefix: str, key: str = None, authorization: str = Header(None)):
1048
  """获取所有账户的状态信息"""
 
 
 
 
 
 
 
 
 
1049
  accounts_info = []
1050
  for account_id, account_manager in multi_account_mgr.accounts.items():
1051
  config = account_manager.config
 
1069
  "accounts": accounts_info
1070
  }
1071
 
1072
+ @app.get("/{path_prefix}/admin/accounts-config")
1073
+ @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1074
+ async def admin_get_config(path_prefix: str, key: str = None, authorization: str = Header(None)):
1075
+ """获取完整账户配置"""
1076
+ try:
1077
+ accounts_data = load_accounts_from_source()
1078
+ return {"accounts": accounts_data}
1079
+ except Exception as e:
1080
+ logger.error(f"[CONFIG] 获取配置失败: {str(e)}")
1081
+ raise HTTPException(500, f"获取失败: {str(e)}")
1082
+
1083
+ @app.put("/{path_prefix}/admin/accounts-config")
1084
+ @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1085
+ async def admin_update_config(path_prefix: str, accounts_data: list = Body(...), key: str = None, authorization: str = Header(None)):
1086
+ """更新整个账户配置"""
1087
+ try:
1088
+ update_accounts_config(accounts_data)
1089
+ return {"status": "success", "message": "配置已更新", "account_count": len(multi_account_mgr.accounts)}
1090
+ except Exception as e:
1091
+ logger.error(f"[CONFIG] 更新配置失败: {str(e)}")
1092
+ raise HTTPException(500, f"更新失败: {str(e)}")
1093
+
1094
+ @app.delete("/{path_prefix}/admin/accounts/{account_id}")
1095
+ @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1096
+ async def admin_delete_account(path_prefix: str, account_id: str, key: str = None, authorization: str = Header(None)):
1097
+ """删除单个账户"""
1098
+ try:
1099
+ delete_account(account_id)
1100
+ return {"status": "success", "message": f"账户 {account_id} 已删除", "account_count": len(multi_account_mgr.accounts)}
1101
+ except Exception as e:
1102
+ logger.error(f"[CONFIG] 删除账户失败: {str(e)}")
1103
+ raise HTTPException(500, f"删除失败: {str(e)}")
1104
+
1105
  @app.get("/{path_prefix}/admin/log")
1106
+ @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1107
  async def admin_get_logs(
1108
  path_prefix: str,
1109
  limit: int = 1500,
 
1124
  - start_time: 开始时间 (格式: 2025-12-17 10:00:00)
1125
  - end_time: 结束时间 (格式: 2025-12-17 11:00:00)
1126
  """
 
 
 
 
 
 
 
 
 
1127
  with log_lock:
1128
  logs = list(log_buffer)
1129
 
 
1187
  }
1188
 
1189
  @app.delete("/{path_prefix}/admin/log")
1190
+ @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1191
  async def admin_clear_logs(path_prefix: str, confirm: str = None, key: str = None, authorization: str = Header(None)):
1192
  """
1193
  清空所有日志(内存缓冲 + 文件)
 
1195
  参数:
1196
  - confirm: 必须传入 "yes" 才能清空
1197
  """
 
 
 
 
 
 
 
 
 
1198
  if confirm != "yes":
1199
  raise HTTPException(
1200
  status_code=400,
 
1215
  }
1216
 
1217
  @app.get("/{path_prefix}/admin/log/html")
1218
+ async def admin_logs_html_route(path_prefix: str, key: str = None, authorization: str = Header(None)):
1219
  """返回美化的 HTML 日志查看界面"""
1220
+ return await templates.admin_logs_html(path_prefix, key, authorization)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1221
 
1222
  @app.post("/{path_prefix}/v1/chat/completions")
1223
+ @require_path_prefix(PATH_PREFIX)
1224
  async def chat(
1225
  path_prefix: str,
1226
  req: ChatRequest,
1227
  request: Request,
1228
  authorization: Optional[str] = Header(None)
1229
  ):
 
 
 
 
1230
  # 1. API Key 验证
1231
  verify_api_key(authorization)
1232
 
 
1385
  request
1386
  ):
1387
  yield chunk
1388
+
1389
+ # 请求成功,重置账户失败计数
1390
+ account_manager.is_available = True
1391
+ account_manager.error_count = 0
1392
  break
1393
 
1394
  except (httpx.ConnectError, httpx.ReadTimeout, ssl.SSLError, HTTPException) as e:
1395
  # 记录当前失败的账户
1396
  failed_accounts.add(account_manager.config.account_id)
1397
 
1398
+ # 增加账户失败计数(触发熔断机制)
1399
+ account_manager.last_error_time = time.time()
1400
+ account_manager.error_count += 1
1401
+ if account_manager.error_count >= ACCOUNT_FAILURE_THRESHOLD:
1402
+ account_manager.is_available = False
1403
+ logger.error(f"[ACCOUNT] [{account_manager.config.account_id}] [req_{request_id}] 请求连续失败{account_manager.error_count}次,账户已标记为不可用")
1404
+
1405
  retry_count += 1
1406
 
1407
  # 详细记录错误信息
 
1844
  @app.get("/public/log/html")
1845
  async def get_public_logs_html():
1846
  """公开的脱敏日志查看器"""
1847
+ return await templates.get_public_logs_html()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1848
 
1849
  # ---------- 全局 404 处理(必须在最后) ----------
1850