xiaoyukkkk commited on
Commit
56226a1
·
verified ·
1 Parent(s): a3c4d24

Upload main.py

Browse files
Files changed (1) hide show
  1. main.py +469 -360
main.py CHANGED
@@ -7,12 +7,13 @@ from dotenv import load_dotenv
7
 
8
  import httpx
9
  from fastapi import FastAPI, HTTPException, Header, Request
10
- from fastapi.responses import StreamingResponse, HTMLResponse
11
  from fastapi.staticfiles import StaticFiles
12
  from pydantic import BaseModel
13
  from util.streaming_parser import parse_json_array_stream_async
14
  from collections import deque
15
  from threading import Lock
 
16
 
17
  # ---------- 日志配置 ----------
18
 
@@ -90,6 +91,7 @@ BASE_URL = os.getenv("BASE_URL") # 服务器完整URL(可选,用
90
  LOGO_URL = os.getenv("LOGO_URL", "") # Logo URL(公开,为空则不显示)
91
  CHAT_URL = os.getenv("CHAT_URL", "") # 开始对话链接(公开,为空则不显示)
92
  MODEL_NAME = os.getenv("MODEL_NAME", "gemini-business") # 模型名称(公开)
 
93
 
94
  # ---------- 图片存储配置 ----------
95
  # 自动检测存储路径:优先使用持久化存储,否则使用临时存储
@@ -498,7 +500,7 @@ class JWTManager:
498
  data = json.loads(txt)
499
 
500
  key_bytes = base64.urlsafe_b64decode(data["xsrfToken"] + "==")
501
- self.jwt = create_jwt(key_bytes, data["keyId"], self.config.csesidx)
502
  self.expires = time.time() + 270
503
  logger.info(f"[AUTH] [{self.config.account_id}] {req_tag}JWT 刷新成功")
504
 
@@ -648,8 +650,6 @@ else:
648
  logger.info(f"[SYSTEM] 图片静态服务已启用: /images/ -> {IMAGE_DIR} (临时存储,重启会丢失)")
649
 
650
  # ---------- 认证装饰器 ----------
651
- from functools import wraps
652
- from fastapi import Request
653
 
654
  def require_admin_key(func):
655
  """验证管理员密钥(支持 URL 参数或 Header)"""
@@ -929,18 +929,11 @@ def verify_api_key(authorization: str = None):
929
 
930
  return True
931
 
932
- @app.get("/{path_prefix}/admin")
933
- @app.get("/{path_prefix}/admin/")
934
- async def admin_home(path_prefix: str, key: str = None, authorization: str = Header(None)):
935
- """管理首页 - 显示API信息和错误提醒"""
936
- # 验证路径前缀
937
- if path_prefix != PATH_PREFIX:
938
- raise HTTPException(404, "Not Found")
939
 
940
- # 验证管理员密钥
941
- admin_key = key or (authorization.replace("Bearer ", "") if authorization and authorization.startswith("Bearer ") else authorization)
942
- if admin_key != ADMIN_KEY:
943
- raise HTTPException(404, "Not Found")
944
  # 获取错误统计
945
  error_count = 0
946
  with log_lock:
@@ -948,417 +941,537 @@ async def admin_home(path_prefix: str, key: str = None, authorization: str = Hea
948
  if log.get("level") in ["ERROR", "CRITICAL"]:
949
  error_count += 1
950
 
951
- # API Key 状态
 
 
 
 
 
 
 
 
 
 
 
 
952
  api_key_status = ""
953
  if API_KEY:
954
  api_key_status = """
955
- <div style="background: #e8f5e9; padding: 15px; border-radius: 8px; margin: 20px 0;">
956
- <strong style="color: #2e7d32;">🔒 API Key 验证已启用</strong>
957
- <p style="color: #4caf50; margin-top: 8px; font-size: 14px;">
958
- 请求时需要在 Authorization header 中携带密钥
959
- </p>
960
  </div>
 
961
  """
962
  else:
963
  api_key_status = """
964
- <div style="background: #fff3e0; padding: 15px; border-radius: 8px; margin: 20px 0;">
965
- <strong style="color: #f57c00;">⚠️ API Key 验证未启用</strong>
966
- <p style="color: #ff9800; margin-top: 8px; font-size: 14px;">
967
- 任何人都可以访问此 API,建议设置 API_KEY 环境变量
968
- </p>
969
  </div>
 
970
  """
971
 
972
- # 错误提醒
973
  error_alert = ""
974
  if error_count > 0:
975
  error_alert = f"""
976
- <div style="background: #ffebee; padding: 15px; border-radius: 8px; margin: 20px 0;">
977
- <strong>检测到 <span style="color: #f44336; font-weight: bold; font-size: 18px;">{error_count}</span> 条错误日志</strong>
978
- <a href="/public/log/html" style="color: #f44336; font-weight: bold; margin-left: 15px;">查看详情 →</a>
 
 
979
  </div>
 
980
  """
981
 
982
- # 获取账户信息
983
  accounts_html = ""
984
  for account_id, account_manager in multi_account_mgr.accounts.items():
985
  config = account_manager.config
986
  remaining_hours = config.get_remaining_hours()
987
-
988
- # 使用统一的格式化函数
989
  status_text, status_color, expire_display = format_account_expiration(remaining_hours)
990
-
991
- availability = "可用" if account_manager.is_available else "不可用"
992
- availability_color = "#4caf50" if account_manager.is_available else "#f44336"
 
993
 
994
  accounts_html += f"""
995
- <div class="card">
996
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
997
- <div>
998
- <strong style="color: #1a1a1a; font-size: 14px;">{config.account_id}</strong>
999
- <span style="background: {availability_color}; color: white; padding: 2px 6px; border-radius: 4px; font-size: 10px; margin-left: 6px;">{availability}</span>
1000
- </div>
1001
- <span style="color: {status_color}; font-weight: 600; font-size: 12px;">{status_text}</span>
1002
  </div>
1003
- <div style="font-size: 12px; color: #6b6b6b; line-height: 1.6;">
1004
- <div>过期: {config.expires_at or '未设置'}</div>
1005
- <div>剩余: <strong style="color: {status_color};">{expire_display}</strong></div>
 
 
 
 
 
 
 
1006
  </div>
1007
  </div>
 
1008
  """
1009
 
 
1010
  html_content = f"""
1011
  <!DOCTYPE html>
1012
- <html>
1013
- <head>
1014
- <meta charset="utf-8">
1015
- <meta name="viewport" content="width=device-width, initial-scale=1">
1016
- <title>系统管理面板 - Gemini Business API</title>
1017
- <style>
1018
- * {{ margin: 0; padding: 0; box-sizing: border-box; }}
1019
- body {{
1020
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
1021
- background: #fafaf9;
1022
- min-height: 100vh;
1023
- padding: 20px;
1024
- }}
1025
- .container {{
1026
- max-width: 1200px;
1027
- margin: 0 auto;
1028
- background: white;
1029
- border-radius: 16px;
1030
- padding: 40px;
1031
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
1032
- }}
1033
- h1 {{
1034
- color: #1a1a1a;
1035
- font-size: 28px;
1036
- font-weight: 600;
1037
- margin-bottom: 8px;
1038
- text-align: center;
1039
- }}
1040
- .subtitle {{
1041
- text-align: center;
1042
- color: #6b6b6b;
1043
- font-size: 14px;
1044
- margin-bottom: 30px;
1045
- display: flex;
1046
- justify-content: space-between;
1047
- align-items: center;
1048
- gap: 12px;
1049
- }}
1050
- .section {{
1051
- margin-bottom: 24px;
1052
- }}
1053
- .section-title {{
1054
- font-size: 18px;
1055
- font-weight: 600;
1056
- color: #1a1a1a;
1057
- margin-bottom: 16px;
1058
- }}
1059
- .grid {{
1060
- display: grid;
1061
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
1062
- gap: 16px;
1063
- margin-bottom: 16px;
1064
- }}
1065
- .card {{
1066
- background: #fafaf9;
1067
- padding: 20px;
1068
- border: 1px solid #e5e5e5;
1069
- border-radius: 12px;
1070
- transition: all 0.15s ease;
1071
- }}
1072
- .card:hover {{
1073
- border-color: #d4d4d4;
1074
- box-shadow: 0 0 8px rgba(0,0,0,0.08);
1075
- }}
1076
- .card h3 {{
1077
- font-size: 15px;
1078
- color: #1a1a1a;
1079
- margin-bottom: 12px;
1080
- font-weight: 600;
1081
- }}
1082
- .btn {{
1083
- display: inline-block;
1084
- background: #1a73e8;
1085
- color: white !important;
1086
- padding: 8px 16px;
1087
- border-radius: 8px;
1088
- text-decoration: none;
1089
- font-size: 14px;
1090
- font-weight: 500;
1091
- transition: background 0.15s ease;
1092
- }}
1093
- .btn:hover {{ background: #1557b0; }}
1094
- .list {{ list-style: none; line-height: 1.8; }}
1095
- .list li {{
1096
- color: #6b6b6b;
1097
- font-size: 13px;
1098
- padding: 4px 0;
1099
- }}
1100
- .env-var {{
1101
- display: flex;
1102
- justify-content: space-between;
1103
- align-items: center;
1104
- padding: 8px 0;
1105
- border-bottom: 1px solid #f0f0f0;
1106
- }}
1107
- .env-var:last-child {{
1108
- border-bottom: none;
1109
- }}
1110
- .env-name {{
1111
- font-family: 'Courier New', monospace;
1112
- font-size: 13px;
1113
- color: #1a73e8;
1114
- font-weight: 600;
1115
- }}
1116
- .env-desc {{
1117
- font-size: 12px;
1118
- color: #6b6b6b;
1119
- }}
1120
- .env-value {{
1121
- font-size: 12px;
1122
- color: #9e9e9e;
1123
- font-style: italic;
1124
- }}
1125
- .badge {{
1126
- display: inline-block;
1127
- padding: 2px 8px;
1128
- border-radius: 4px;
1129
- font-size: 11px;
1130
- font-weight: 600;
1131
- }}
1132
- .badge-required {{
1133
- background: #ffebee;
1134
- color: #c62828;
1135
- }}
1136
- .badge-optional {{
1137
- background: #e8f5e9;
1138
- color: #2e7d32;
1139
- }}
1140
- code {{
1141
- background: #f5f5f4;
1142
- padding: 2px 6px;
1143
- border-radius: 4px;
1144
- font-size: 12px;
1145
- color: #1a73e8;
1146
- }}
1147
- a {{ color: #1a73e8; text-decoration: none; }}
1148
- a:hover {{ color: #1557b0; }}
1149
- .account-grid {{
1150
- display: grid;
1151
- grid-template-columns: repeat(3, 1fr);
1152
- gap: 16px;
1153
- }}
1154
- @media (max-width: 768px) {{
1155
- .container {{ padding: 25px; }}
1156
- h1 {{ font-size: 24px; }}
1157
- .subtitle {{
1158
- flex-direction: column;
1159
- align-items: center;
1160
- gap: 12px;
1161
- }}
1162
- .subtitle span {{
1163
- text-align: center;
1164
- font-size: 13px;
1165
- }}
1166
- .subtitle .btn {{
1167
- width: 100%;
1168
- text-align: center;
1169
- }}
1170
- .grid {{ grid-template-columns: 1fr; }}
1171
- .account-grid {{ grid-template-columns: 1fr; }}
1172
- }}
1173
- </style>
1174
- </head>
1175
- <body>
1176
- <div class="container">
1177
- <h1>系统管理面板</h1>
1178
- <div class="subtitle">
1179
- <span>Gemini Business API - 多账户代理服务</span>
1180
- <a href="/public/log/html" class="btn" style="font-size: 13px; padding: 6px 12px;">查看公开日志</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1181
  </div>
 
1182
 
1183
- {api_key_status}
1184
- {error_alert}
 
1185
 
1186
- <!-- 账户状态 -->
1187
- <div class="section">
1188
- <div class="section-title">账户状态 ({len(multi_account_mgr.accounts)} 个)</div>
1189
- <div class="account-grid">
1190
- {accounts_html if accounts_html else '<div class="card"><p style="color: #6b6b6b; font-size: 14px;">暂无账户</p></div>'}
1191
- </div>
1192
  </div>
 
1193
 
1194
- <!-- 环境变量配置 -->
1195
- <div class="section">
1196
- <div class="section-title">环境变量配置</div>
1197
- <div class="grid">
1198
  <div class="card">
1199
  <h3>必需变量 <span class="badge badge-required">REQUIRED</span></h3>
1200
  <div style="margin-top: 12px;">
1201
  <div class="env-var">
1202
- <div>
1203
- <div class="env-name">ACCOUNTS_CONFIG</div>
1204
- <div class="env-desc">JSON格式账户列表</div>
1205
- </div>
1206
  </div>
1207
  <div class="env-var">
1208
- <div>
1209
- <div class="env-name">PATH_PREFIX</div>
1210
- <div class="env-desc">API路径前缀</div>
1211
- </div>
1212
  <div class="env-value">当前: {PATH_PREFIX}</div>
1213
  </div>
1214
  <div class="env-var">
1215
- <div>
1216
- <div class="env-name">ADMIN_KEY</div>
1217
- <div class="env-desc">管理员密钥</div>
1218
- </div>
1219
  <div class="env-value">已设置</div>
1220
  </div>
1221
  </div>
1222
  </div>
1223
 
1224
- <div class="card">
1225
- <h3>可选变量 <span class="badge badge-optional">OPTIONAL</span></h3>
1226
- <div style="margin-top: 12px;">
1227
- <div class="env-var">
1228
- <div>
1229
- <div class="env-name">API_KEY</div>
1230
- <div class="env-desc">API访问密钥</div>
1231
- </div>
1232
- <div class="env-value">{'已设置' if API_KEY else '未设置'}</div>
1233
- </div>
1234
- <div class="env-var">
1235
- <div>
1236
- <div class="env-name">BASE_URL</div>
1237
- <div class="env-desc">图片URL生成(推荐设置)</div>
1238
- </div>
1239
- <div class="env-value">{'已设置' if BASE_URL else '未设置(自动检测)'}</div>
1240
- </div>
1241
- <div class="env-var">
1242
- <div>
1243
- <div class="env-name">PROXY</div>
1244
- <div class="env-desc">代理地址</div>
1245
- </div>
1246
- <div class="env-value">{'已设置' if PROXY else '未设置'}</div>
1247
- </div>
1248
- <div class="env-var">
1249
- <div>
1250
- <div class="env-name">SESSION_CACHE_TTL_SECONDS</div>
1251
- <div class="env-desc">会话缓存过期时间</div>
1252
- </div>
1253
- <div class="env-value">{SESSION_CACHE_TTL_SECONDS} 秒</div>
1254
- </div>
1255
- <div class="env-var">
1256
- <div>
1257
- <div class="env-name">LOGO_URL</div>
1258
- <div class="env-desc">Logo URL(公开,为空则不显示)</div>
1259
- </div>
1260
- <div class="env-value">{'已设置' if LOGO_URL else '未设置'}</div>
1261
- </div>
1262
- <div class="env-var">
1263
- <div>
1264
- <div class="env-name">CHAT_URL</div>
1265
- <div class="env-desc">开始对话链接(公开,为空则不显示)</div>
1266
- </div>
1267
- <div class="env-value">{'已设置' if CHAT_URL else '未设置'}</div>
1268
- </div>
1269
- <div class="env-var">
1270
- <div>
1271
- <div class="env-name">MODEL_NAME</div>
1272
- <div class="env-desc">模型名称(公开)</div>
1273
- </div>
1274
- <div class="env-value">{MODEL_NAME}</div>
1275
- </div>
1276
- </div>
1277
- </div>
1278
-
1279
  <div class="card">
1280
  <h3>重试配置 <span class="badge badge-optional">OPTIONAL</span></h3>
1281
  <div style="margin-top: 12px;">
1282
  <div class="env-var">
1283
- <div>
1284
- <div class="env-name">MAX_NEW_SESSION_TRIES</div>
1285
- <div class="env-desc">新会话尝试账户数</div>
1286
- </div>
1287
  <div class="env-value">{MAX_NEW_SESSION_TRIES}</div>
1288
  </div>
1289
  <div class="env-var">
1290
- <div>
1291
- <div class="env-name">MAX_REQUEST_RETRIES</div>
1292
- <div class="env-desc">请求失败重试次数</div>
1293
- </div>
1294
  <div class="env-value">{MAX_REQUEST_RETRIES}</div>
1295
  </div>
1296
  <div class="env-var">
1297
- <div>
1298
- <div class="env-name">ACCOUNT_FAILURE_THRESHOLD</div>
1299
- <div class="env-desc">账户失败阈值</div>
1300
- </div>
1301
  <div class="env-value">{ACCOUNT_FAILURE_THRESHOLD} 次</div>
1302
  </div>
1303
  <div class="env-var">
1304
- <div>
1305
- <div class="env-name">ACCOUNT_COOLDOWN_SECONDS</div>
1306
- <div class="env-desc">账户冷却时间</div>
1307
- </div>
1308
  <div class="env-value">{ACCOUNT_COOLDOWN_SECONDS} 秒</div>
1309
  </div>
1310
  </div>
1311
  </div>
1312
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1313
  </div>
 
1314
 
1315
- <!-- 模型与端点 -->
1316
- <div class="section">
1317
- <div class="section-title">服务信息</div>
1318
- <div class="grid">
1319
- <div class="card">
1320
- <h3>支持的模型</h3>
1321
- <ul class="list">
1322
- <li><code>gemini-auto</code> - 自动选择(默认)</li>
1323
- <li><code>gemini-2.5-flash</code> - Flash 2.5</li>
1324
- <li><code>gemini-2.5-pro</code> - Pro 2.5</li>
1325
- <li><code>gemini-3-flash-preview</code> - Flash 3 预览</li>
1326
- <li><code>gemini-3-pro-preview</code> - Pro 3 预览 <strong style="color: #10b981;">(支持图片生成)</strong></li>
1327
- </ul>
1328
- <div style="margin-top: 16px; padding: 14px 16px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px;">
1329
- <div style="font-weight: 600; color: #334155; margin-bottom: 10px; font-size: 13px;">图片生成说明</div>
1330
- <div style="font-size: 13px; color: #475569; line-height: 1.8;">
1331
- 仅 <code style="background: #e0e7ff; color: #4338ca; padding: 2px 6px; border-radius: 3px; font-weight: 500;">gemini-3-pro-preview</code> 支持图片生成<br>
1332
- 保存路径: <code style="background: #e0e7ff; color: #4338ca; padding: 2px 6px; border-radius: 3px; font-weight: 500;">{IMAGE_DIR}</code><br>
1333
- 存储类型: {'<span style="color: #059669; font-weight: 600;">持久化(重启保留)</span>' if IMAGE_DIR == '/data/images' else '<span style="color: #dc2626; font-weight: 600;">临时(重启丢失)</span>'}
1334
- </div>
1335
  </div>
1336
  </div>
 
1337
 
1338
- <div class="card" style="grid-column: span 2;">
1339
- <h3>API 端点</h3>
1340
- <ul class="list">
1341
- <li><code>POST /{PATH_PREFIX}/v1/chat/completions</code> - 聊天接口(流式+多模态)</li>
1342
- <li><code>GET /{PATH_PREFIX}/v1/models</code> - 获取模型列表</li>
1343
- <li><code>GET /{PATH_PREFIX}/admin</code> - 管理首页</li>
1344
- <li><code>GET /{PATH_PREFIX}/admin/health?key={{ADMIN_KEY}}</code> - 健康检查</li>
1345
- <li><code>GET /{PATH_PREFIX}/admin/accounts?key={{ADMIN_KEY}}</code> - 获取账户状态(JSON)</li>
1346
- <li><code>GET /{PATH_PREFIX}/admin/log?key={{ADMIN_KEY}}</code> - 获取日志(JSON)</li>
1347
- <li><code>GET /{PATH_PREFIX}/admin/log/html?key={{ADMIN_KEY}}</code> - 日志查看器(HTML)</li>
1348
- <li><code>DELETE /{PATH_PREFIX}/admin/log?confirm=yes&key={{ADMIN_KEY}}</code> - 清空日志</li>
1349
- <li><code>GET /public/stats</code> - 公开统计信息</li>
1350
- <li><code>GET /public/log</code> - 公开日志(JSON,脱敏)</li>
1351
- <li><code>GET /public/log/html</code> - 公开日志查看器(HTML,脱敏)</li>
1352
- <li><code>GET /docs</code> - FastAPI自动生成的API文档(Swagger UI)</li>
1353
- <li><code>GET /redoc</code> - FastAPI自动生成的API文档(ReDoc)</li>
1354
- </ul>
1355
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1356
  </div>
1357
  </div>
1358
  </div>
1359
- </body>
 
1360
  </html>
1361
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1362
  return HTMLResponse(content=html_content)
1363
 
1364
  @app.get("/{path_prefix}/v1/models")
@@ -1581,7 +1694,7 @@ async def admin_logs_html(path_prefix: str, key: str = None, authorization: str
1581
  if admin_key != ADMIN_KEY:
1582
  raise HTTPException(404, "Not Found")
1583
 
1584
- html_content = """
1585
  <!DOCTYPE html>
1586
  <html>
1587
  <head>
@@ -2818,7 +2931,7 @@ async def get_public_logs(request: Request, limit: int = 100):
2818
  @app.get("/public/log/html")
2819
  async def get_public_logs_html():
2820
  """公开的脱敏日志查看器"""
2821
- html_content = """
2822
  <!DOCTYPE html>
2823
  <html>
2824
  <head>
@@ -3045,12 +3158,12 @@ async def get_public_logs_html():
3045
  <body>
3046
  <div class="container">
3047
  <h1>
3048
- """ + (f'<img src="{LOGO_URL}" alt="Logo">' if LOGO_URL else '') + """
3049
  Gemini服务状态
3050
  </h1>
3051
  <div style="text-align: center; color: #999; font-size: 12px; margin-bottom: 16px;" class="subtitle-public">
3052
  <span>展示最近1000条对话日志 · 每5秒自动更新</span>
3053
- """ + (f'<a href="{CHAT_URL}" target="_blank" style="color: #1a73e8; text-decoration: none;">开始对话</a>' if CHAT_URL else '<span style="color: #999;">开始对话</span>') + """
3054
  </div>
3055
  <div class="stats">
3056
  <div class="stat">
@@ -3180,7 +3293,10 @@ async def get_public_logs_html():
3180
  <div class="log-group" data-req-id="${reqId}">
3181
  <div class="log-group-header" onclick="toggleGroup('${reqId}')">
3182
  <span style="color: ${statusColor}; font-weight: 600; font-size: 11px;">⬤ ${statusText}</span>
3183
- <span style="color: #999; font-size: 11px;">${log.events.length}条事件</span>
 
 
 
3184
  <span ${iconClass} style="margin-left: auto; color: #999;">▼</span>
3185
  </div>
3186
  <div class="log-group-content" ${contentStyle}>
@@ -3275,7 +3391,6 @@ async def get_public_logs_html():
3275
  return HTMLResponse(content=html_content)
3276
 
3277
  # ---------- 全局 404 处理(必须在最后) ----------
3278
- from fastapi.responses import JSONResponse
3279
 
3280
  @app.exception_handler(404)
3281
  async def not_found_handler(request: Request, exc: HTTPException):
@@ -3285,12 +3400,6 @@ async def not_found_handler(request: Request, exc: HTTPException):
3285
  content={"detail": "Not Found"}
3286
  )
3287
 
3288
- # 捕获所有未匹配的路径(必须在所有路由之后)
3289
- @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"])
3290
- async def catch_all(path: str):
3291
- """捕获所有未匹配的路径,返回 404"""
3292
- raise HTTPException(404, "Not Found")
3293
-
3294
  if __name__ == "__main__":
3295
  import uvicorn
3296
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
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
13
  from util.streaming_parser import parse_json_array_stream_async
14
  from collections import deque
15
  from threading import Lock
16
+ from functools import wraps
17
 
18
  # ---------- 日志配置 ----------
19
 
 
91
  LOGO_URL = os.getenv("LOGO_URL", "") # Logo URL(公开,为空则不显示)
92
  CHAT_URL = os.getenv("CHAT_URL", "") # 开始对话链接(公开,为空则不显示)
93
  MODEL_NAME = os.getenv("MODEL_NAME", "gemini-business") # 模型名称(公开)
94
+ HIDE_HOME_PAGE = os.getenv("HIDE_HOME_PAGE", "").lower() == "true" # 是否隐藏首页(默认不隐藏)
95
 
96
  # ---------- 图片存储配置 ----------
97
  # 自动检测存储路径:优先使用持久化存储,否则使用临时存储
 
500
  data = json.loads(txt)
501
 
502
  key_bytes = base64.urlsafe_b64decode(data["xsrfToken"] + "==")
503
+ self.jwt = create_jwt(key_bytes, data["keyId"], self.config.csesidx)
504
  self.expires = time.time() + 270
505
  logger.info(f"[AUTH] [{self.config.account_id}] {req_tag}JWT 刷新成功")
506
 
 
650
  logger.info(f"[SYSTEM] 图片静态服务已启用: /images/ -> {IMAGE_DIR} (临时存储,重启会丢失)")
651
 
652
  # ---------- 认证装饰器 ----------
 
 
653
 
654
  def require_admin_key(func):
655
  """验证管理员密钥(支持 URL 参数或 Header)"""
 
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:
 
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 class="account-grid">
1254
+ {accounts_html if accounts_html else '<div class="card"><p style="color: #6b6b6b; font-size: 14px; text-align:center;">暂无账户</p></div>'}
 
 
1255
  </div>
1256
+ </div>
1257
 
1258
+ <div class="section">
1259
+ <div class="section-title">环境变量配置</div>
1260
+ <div class="grid-env">
1261
+ <div class="stack-col">
1262
  <div class="card">
1263
  <h3>必需变量 <span class="badge badge-required">REQUIRED</span></h3>
1264
  <div style="margin-top: 12px;">
1265
  <div class="env-var">
1266
+ <div><div class="env-name">ACCOUNTS_CONFIG</div><div class="env-desc">JSON格式账户列表</div></div>
 
 
 
1267
  </div>
1268
  <div class="env-var">
1269
+ <div><div class="env-name">PATH_PREFIX</div><div class="env-desc">API路径前缀</div></div>
 
 
 
1270
  <div class="env-value">当前: {PATH_PREFIX}</div>
1271
  </div>
1272
  <div class="env-var">
1273
+ <div><div class="env-name">ADMIN_KEY</div><div class="env-desc">管理员密钥</div></div>
 
 
 
1274
  <div class="env-value">已设置</div>
1275
  </div>
1276
  </div>
1277
  </div>
1278
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1279
  <div class="card">
1280
  <h3>重试配置 <span class="badge badge-optional">OPTIONAL</span></h3>
1281
  <div style="margin-top: 12px;">
1282
  <div class="env-var">
1283
+ <div><div class="env-name">MAX_NEW_SESSION_TRIES</div><div class="env-desc">新会话尝试账户数</div></div>
 
 
 
1284
  <div class="env-value">{MAX_NEW_SESSION_TRIES}</div>
1285
  </div>
1286
  <div class="env-var">
1287
+ <div><div class="env-name">MAX_REQUEST_RETRIES</div><div class="env-desc">请求失败重试次数</div></div>
 
 
 
1288
  <div class="env-value">{MAX_REQUEST_RETRIES}</div>
1289
  </div>
1290
  <div class="env-var">
1291
+ <div><div class="env-name">ACCOUNT_FAILURE_THRESHOLD</div><div class="env-desc">账户失败阈值</div></div>
 
 
 
1292
  <div class="env-value">{ACCOUNT_FAILURE_THRESHOLD} 次</div>
1293
  </div>
1294
  <div class="env-var">
1295
+ <div><div class="env-name">ACCOUNT_COOLDOWN_SECONDS</div><div class="env-desc">账户冷却时间</div></div>
 
 
 
1296
  <div class="env-value">{ACCOUNT_COOLDOWN_SECONDS} 秒</div>
1297
  </div>
1298
  </div>
1299
  </div>
1300
  </div>
1301
+
1302
+ <div class="card">
1303
+ <h3>可选变量 <span class="badge badge-optional">OPTIONAL</span></h3>
1304
+ <div style="margin-top: 12px;">
1305
+ <div class="env-var">
1306
+ <div><div class="env-name">API_KEY</div><div class="env-desc">API访问密钥</div></div>
1307
+ <div class="env-value">{'已设置' if API_KEY else '未设置'}</div>
1308
+ </div>
1309
+ <div class="env-var">
1310
+ <div><div class="env-name">BASE_URL</div><div class="env-desc">图片URL生成(推荐设置)</div></div>
1311
+ <div class="env-value">{'已设置' if BASE_URL else '未设置(自动检测)'}</div>
1312
+ </div>
1313
+ <div class="env-var">
1314
+ <div><div class="env-name">PROXY</div><div class="env-desc">代理地址</div></div>
1315
+ <div class="env-value">{'已设置' if PROXY else '未设置'}</div>
1316
+ </div>
1317
+ <div class="env-var">
1318
+ <div><div class="env-name">SESSION_CACHE_TTL_SECONDS</div><div class="env-desc">会话缓存过期时间</div></div>
1319
+ <div class="env-value">{SESSION_CACHE_TTL_SECONDS} 秒</div>
1320
+ </div>
1321
+ <div class="env-var">
1322
+ <div><div class="env-name">LOGO_URL</div><div class="env-desc">Logo URL(公开,为空则不显示)</div></div>
1323
+ <div class="env-value">{'已设置' if LOGO_URL else '未设置'}</div>
1324
+ </div>
1325
+ <div class="env-var">
1326
+ <div><div class="env-name">CHAT_URL</div><div class="env-desc">开始对话链接(公开,为空则不显示)</div></div>
1327
+ <div class="env-value">{'已设置' if CHAT_URL else '未设置'}</div>
1328
+ </div>
1329
+ <div class="env-var">
1330
+ <div><div class="env-name">MODEL_NAME</div><div class="env-desc">模型名称(公开)</div></div>
1331
+ <div class="env-value">{MODEL_NAME}</div>
1332
+ </div>
1333
+ <div class="env-var">
1334
+ <div><div class="env-name">HIDE_HOME_PAGE</div><div class="env-desc">隐藏首页管理面板</div></div>
1335
+ <div class="env-value">{'已隐藏' if HIDE_HOME_PAGE else '未隐藏'}</div>
1336
+ </div>
1337
+ </div>
1338
+ </div>
1339
  </div>
1340
+ </div>
1341
 
1342
+ <div class="section">
1343
+ <div class="section-title">服务信息</div>
1344
+ <div class="grid-3">
1345
+ <div class="card">
1346
+ <h3>支持的模型</h3>
1347
+ <div class="model-grid">
1348
+ <span class="model-tag">gemini-auto</span>
1349
+ <span class="model-tag">gemini-2.5-flash</span>
1350
+ <span class="model-tag">gemini-2.5-pro</span>
1351
+ <span class="model-tag">gemini-3-flash-preview</span>
1352
+ <span class="model-tag highlight">gemini-3-pro-preview</span>
1353
+ </div>
1354
+
1355
+ <div class="info-box">
1356
+ <div class="info-box-title">📸 图片生成说明</div>
1357
+ <div class="info-box-text">
1358
+ 仅 <code style="background:none;padding:0;color:#0071e3;">gemini-3-pro-preview</code> 支持绘图。<br>
1359
+ 路径: <code>{IMAGE_DIR}</code><br>
1360
+ 类型: {'<span style="color: #34c759; font-weight: 600;">持久化(重启保留)</span>' if IMAGE_DIR == '/data/images' else '<span style="color: #ff3b30; font-weight: 600;">临时(重启丢失)</span>'}
 
1361
  </div>
1362
  </div>
1363
+ </div>
1364
 
1365
+ <div class="card" style="grid-column: span 2;">
1366
+ <h3>API 端点</h3>
1367
+
1368
+ <div class="current-url-row">
1369
+ <span style="font-size:12px; font-weight:600; color:#0071e3; margin-right:8px;">当前页面:</span>
1370
+ <code style="background:none; padding:0; color:#1d1d1f;">{current_url}</code>
 
 
 
 
 
 
 
 
 
 
 
1371
  </div>
1372
+
1373
+ <table class="ep-table">
1374
+ <tr>
1375
+ <td width="70"><span class="method m-post">POST</span></td>
1376
+ <td><span class="ep-path">/{PATH_PREFIX}/v1/chat/completions</span></td>
1377
+ <td><span class="ep-desc">OpenAI 兼容对话接口</span></td>
1378
+ </tr>
1379
+ <tr>
1380
+ <td><span class="method m-get">GET</span></td>
1381
+ <td><span class="ep-path">/{PATH_PREFIX}/v1/models</span></td>
1382
+ <td><span class="ep-desc">获取模型列表</span></td>
1383
+ </tr>
1384
+ <tr>
1385
+ <td><span class="method m-get">GET</span></td>
1386
+ <td><span class="ep-path">/{PATH_PREFIX}/admin</span></td>
1387
+ <td><span class="ep-desc">管理首页</span></td>
1388
+ </tr>
1389
+ <tr>
1390
+ <td><span class="method m-get">GET</span></td>
1391
+ <td><span class="ep-path">/{PATH_PREFIX}/admin/health?key={{ADMIN_KEY}}</span></td>
1392
+ <td><span class="ep-desc">健康检查 (需 Key)</span></td>
1393
+ </tr>
1394
+ <tr>
1395
+ <td><span class="method m-get">GET</span></td>
1396
+ <td><span class="ep-path">/{PATH_PREFIX}/admin/accounts?key={{ADMIN_KEY}}</span></td>
1397
+ <td><span class="ep-desc">账户状态 JSON (需 Key)</span></td>
1398
+ </tr>
1399
+ <tr>
1400
+ <td><span class="method m-get">GET</span></td>
1401
+ <td><span class="ep-path">/{PATH_PREFIX}/admin/log?key={{ADMIN_KEY}}</span></td>
1402
+ <td><span class="ep-desc">获取日志 JSON (需 Key)</span></td>
1403
+ </tr>
1404
+ <tr>
1405
+ <td><span class="method m-get">GET</span></td>
1406
+ <td><span class="ep-path">/{PATH_PREFIX}/admin/log/html?key={{ADMIN_KEY}}</span></td>
1407
+ <td><span class="ep-desc">日志查看器 HTML (需 Key)</span></td>
1408
+ </tr>
1409
+ <tr>
1410
+ <td><span class="method m-del">DEL</span></td>
1411
+ <td><span class="ep-path">/{PATH_PREFIX}/admin/log?confirm=yes&key={{ADMIN_KEY}}</span></td>
1412
+ <td><span class="ep-desc">清空系统日志 (需 Key)</span></td>
1413
+ </tr>
1414
+ <tr>
1415
+ <td><span class="method m-get">GET</span></td>
1416
+ <td><span class="ep-path">/public/stats</span></td>
1417
+ <td><span class="ep-desc">公开统计数据</span></td>
1418
+ </tr>
1419
+ <tr>
1420
+ <td><span class="method m-get">GET</span></td>
1421
+ <td><span class="ep-path">/public/log</span></td>
1422
+ <td><span class="ep-desc">公开日志 (JSON, 脱敏)</span></td>
1423
+ </tr>
1424
+ <tr>
1425
+ <td><span class="method m-get">GET</span></td>
1426
+ <td><span class="ep-path">/public/log/html</span></td>
1427
+ <td><span class="ep-desc">公开日志查看器 (HTML)</span></td>
1428
+ </tr>
1429
+ <tr>
1430
+ <td><span class="method m-get">GET</span></td>
1431
+ <td><span class="ep-path">/docs</span></td>
1432
+ <td><span class="ep-desc">Swagger API 文档</span></td>
1433
+ </tr>
1434
+ <tr>
1435
+ <td><span class="method m-get">GET</span></td>
1436
+ <td><span class="ep-path">/redoc</span></td>
1437
+ <td><span class="ep-desc">ReDoc API 文档</span></td>
1438
+ </tr>
1439
+ </table>
1440
  </div>
1441
  </div>
1442
  </div>
1443
+ </div>
1444
+ </body>
1445
  </html>
1446
  """
1447
+ return html_content
1448
+
1449
+ @app.get("/")
1450
+ async def home(request: Request):
1451
+ """首页 - 默认显示管理面板(可通过环境变量隐藏)"""
1452
+ # 检查是否隐藏首页
1453
+ if HIDE_HOME_PAGE:
1454
+ raise HTTPException(404, "Not Found")
1455
+
1456
+ # 显示管理页面(带隐藏提示)
1457
+ html_content = generate_admin_html(request, show_hide_tip=True)
1458
+ return HTMLResponse(content=html_content)
1459
+
1460
+ @app.get("/{path_prefix}/admin")
1461
+ @app.get("/{path_prefix}/admin/")
1462
+ async def admin_home(path_prefix: str, request: Request, key: str = None, authorization: str = Header(None)):
1463
+ """管理首页 - 显示API信息和错误提醒"""
1464
+ # 验证路径前缀
1465
+ if path_prefix != PATH_PREFIX:
1466
+ raise HTTPException(404, "Not Found")
1467
+
1468
+ # 验证管理员密钥
1469
+ admin_key = key or (authorization.replace("Bearer ", "") if authorization and authorization.startswith("Bearer ") else authorization)
1470
+ if admin_key != ADMIN_KEY:
1471
+ raise HTTPException(404, "Not Found")
1472
+
1473
+ # 显示管理页面(不显示隐藏提示)
1474
+ html_content = generate_admin_html(request, show_hide_tip=False)
1475
  return HTMLResponse(content=html_content)
1476
 
1477
  @app.get("/{path_prefix}/v1/models")
 
1694
  if admin_key != ADMIN_KEY:
1695
  raise HTTPException(404, "Not Found")
1696
 
1697
+ html_content = r"""
1698
  <!DOCTYPE html>
1699
  <html>
1700
  <head>
 
2931
  @app.get("/public/log/html")
2932
  async def get_public_logs_html():
2933
  """公开的脱敏日志查看器"""
2934
+ html_content = r"""
2935
  <!DOCTYPE html>
2936
  <html>
2937
  <head>
 
3158
  <body>
3159
  <div class="container">
3160
  <h1>
3161
+ """ + (f'<img src="{LOGO_URL}" alt="Logo">' if LOGO_URL else '') + r"""
3162
  Gemini服务状态
3163
  </h1>
3164
  <div style="text-align: center; color: #999; font-size: 12px; margin-bottom: 16px;" class="subtitle-public">
3165
  <span>展示最近1000条对话日志 · 每5秒自动更新</span>
3166
+ """ + (f'<a href="{CHAT_URL}" target="_blank" style="color: #1a73e8; text-decoration: none;">开始对话</a>' if CHAT_URL else '<span style="color: #999;">开始对话</span>') + r"""
3167
  </div>
3168
  <div class="stats">
3169
  <div class="stat">
 
3293
  <div class="log-group" data-req-id="${reqId}">
3294
  <div class="log-group-header" onclick="toggleGroup('${reqId}')">
3295
  <span style="color: ${statusColor}; font-weight: 600; font-size: 11px;">⬤ ${statusText}</span>
3296
+ <span style="color: #666; font-size: 11px; margin-left: 8px;">req_${reqId}</span>
3297
+ ${accountId ? `<span style="color: ${getAccountColor(accountId)}; font-size: 11px; margin-left: 8px;">${accountId}</span>` : ''}
3298
+ ${model ? `<span style="color: #999; font-size: 11px; margin-left: 8px;">${model}</span>` : ''}
3299
+ <span style="color: #999; font-size: 11px; margin-left: 8px;">${log.events.length}条事件</span>
3300
  <span ${iconClass} style="margin-left: auto; color: #999;">▼</span>
3301
  </div>
3302
  <div class="log-group-content" ${contentStyle}>
 
3391
  return HTMLResponse(content=html_content)
3392
 
3393
  # ---------- 全局 404 处理(必须在最后) ----------
 
3394
 
3395
  @app.exception_handler(404)
3396
  async def not_found_handler(request: Request, exc: HTTPException):
 
3400
  content={"detail": "Not Found"}
3401
  )
3402
 
 
 
 
 
 
 
3403
  if __name__ == "__main__":
3404
  import uvicorn
3405
  uvicorn.run(app, host="0.0.0.0", port=7860)