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

Upload main.py

Browse files
Files changed (1) hide show
  1. main.py +344 -474
main.py CHANGED
@@ -7,13 +7,12 @@ 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
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
 
@@ -87,12 +86,6 @@ PATH_PREFIX = os.getenv("PATH_PREFIX") # 路径前缀(必需,用于隐
87
  ADMIN_KEY = os.getenv("ADMIN_KEY") # 管理员密钥(必需,用于访问管理端点)
88
  BASE_URL = os.getenv("BASE_URL") # 服务器完整URL(可选,用于图片URL生成)
89
 
90
- # ---------- 公开展示配置 ----------
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
  # 自动检测存储路径:优先使用持久化存储,否则使用临时存储
98
  if os.path.exists("/data"):
@@ -500,7 +493,7 @@ class JWTManager:
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,6 +643,8 @@ else:
650
  logger.info(f"[SYSTEM] 图片静态服务已启用: /images/ -> {IMAGE_DIR} (临时存储,重启会丢失)")
651
 
652
  # ---------- 认证装饰器 ----------
 
 
653
 
654
  def require_admin_key(func):
655
  """验证管理员密钥(支持 URL 参数或 Header)"""
@@ -929,11 +924,18 @@ 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:
@@ -941,537 +943,401 @@ def generate_admin_html(request: Request, show_hide_tip: bool = False) -> str:
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,7 +1560,7 @@ async def admin_logs_html(path_prefix: str, key: str = None, authorization: str
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,7 +2797,7 @@ async def get_public_logs(request: Request, limit: int = 100):
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,12 +3024,12 @@ async def get_public_logs_html():
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,10 +3159,7 @@ async def get_public_logs_html():
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,6 +3254,7 @@ async def get_public_logs_html():
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,6 +3264,12 @@ 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)
 
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
 
 
86
  ADMIN_KEY = os.getenv("ADMIN_KEY") # 管理员密钥(必需,用于访问管理端点)
87
  BASE_URL = os.getenv("BASE_URL") # 服务器完整URL(可选,用于图片URL生成)
88
 
 
 
 
 
 
 
89
  # ---------- 图片存储配置 ----------
90
  # 自动检测存储路径:优先使用持久化存储,否则使用临时存储
91
  if os.path.exists("/data"):
 
493
  data = json.loads(txt)
494
 
495
  key_bytes = base64.urlsafe_b64decode(data["xsrfToken"] + "==")
496
+ self.jwt = create_jwt(key_bytes, data["keyId"], self.config.csesidx)
497
  self.expires = time.time() + 270
498
  logger.info(f"[AUTH] [{self.config.account_id}] {req_tag}JWT 刷新成功")
499
 
 
643
  logger.info(f"[SYSTEM] 图片静态服务已启用: /images/ -> {IMAGE_DIR} (临时存储,重启会丢失)")
644
 
645
  # ---------- 认证装饰器 ----------
646
+ from functools import wraps
647
+ from fastapi import Request
648
 
649
  def require_admin_key(func):
650
  """验证管理员密钥(支持 URL 参数或 Header)"""
 
924
 
925
  return True
926
 
927
+ @app.get("/{path_prefix}/admin")
928
+ @app.get("/{path_prefix}/admin/")
929
+ async def admin_home(path_prefix: str, key: str = None, authorization: str = Header(None)):
930
+ """管理首页 - 显示API信息和错误提醒"""
931
+ # 验证路径前缀
932
+ if path_prefix != PATH_PREFIX:
933
+ raise HTTPException(404, "Not Found")
934
 
935
+ # 验证管理员密钥
936
+ admin_key = key or (authorization.replace("Bearer ", "") if authorization and authorization.startswith("Bearer ") else authorization)
937
+ if admin_key != ADMIN_KEY:
938
+ raise HTTPException(404, "Not Found")
939
  # 获取错误统计
940
  error_count = 0
941
  with log_lock:
 
943
  if log.get("level") in ["ERROR", "CRITICAL"]:
944
  error_count += 1
945
 
946
+ # API Key 状态
 
 
 
 
 
 
 
 
 
 
 
 
947
  api_key_status = ""
948
  if API_KEY:
949
  api_key_status = """
950
+ <div style="background: #e8f5e9; padding: 15px; border-radius: 8px; margin: 20px 0;">
951
+ <strong style="color: #2e7d32;">🔒 API Key 验证已启用</strong>
952
+ <p style="color: #4caf50; margin-top: 8px; font-size: 14px;">
953
+ 请求时需要在 Authorization header 中携带密钥
954
+ </p>
955
  </div>
 
956
  """
957
  else:
958
  api_key_status = """
959
+ <div style="background: #fff3e0; padding: 15px; border-radius: 8px; margin: 20px 0;">
960
+ <strong style="color: #f57c00;">⚠️ API Key 验证未启用</strong>
961
+ <p style="color: #ff9800; margin-top: 8px; font-size: 14px;">
962
+ 任何人都可以访问此 API,建议设置 API_KEY 环境变量
963
+ </p>
964
  </div>
 
965
  """
966
 
967
+ # 错误提醒
968
  error_alert = ""
969
  if error_count > 0:
970
  error_alert = f"""
971
+ <div style="background: #ffebee; padding: 15px; border-radius: 8px; margin: 20px 0;">
972
+ <strong>检测到 <span style="color: #f44336; font-weight: bold; font-size: 18px;">{error_count}</span> 条错误日志</strong>
973
+ <a href="/public/log/html" style="color: #f44336; font-weight: bold; margin-left: 15px;">查看详情 →</a>
 
 
974
  </div>
 
975
  """
976
 
977
+ # 获取账户信息
978
  accounts_html = ""
979
  for account_id, account_manager in multi_account_mgr.accounts.items():
980
  config = account_manager.config
981
  remaining_hours = config.get_remaining_hours()
982
+
983
+ # 使用统一的格式化函数
984
  status_text, status_color, expire_display = format_account_expiration(remaining_hours)
985
+
986
+ availability = "可用" if account_manager.is_available else "不可用"
987
+ availability_color = "#4caf50" if account_manager.is_available else "#f44336"
 
988
 
989
  accounts_html += f"""
990
+ <div class="card">
991
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
992
+ <div>
993
+ <strong style="color: #1a1a1a; font-size: 14px;">{config.account_id}</strong>
994
+ <span style="background: {availability_color}; color: white; padding: 2px 6px; border-radius: 4px; font-size: 10px; margin-left: 6px;">{availability}</span>
995
+ </div>
996
+ <span style="color: {status_color}; font-weight: 600; font-size: 12px;">{status_text}</span>
 
 
 
 
 
997
  </div>
998
+ <div style="font-size: 12px; color: #6b6b6b; line-height: 1.6;">
999
+ <div>过期: {config.expires_at or '未设置'}</div>
1000
+ <div>剩余: <strong style="color: {status_color};">{expire_display}</strong></div>
1001
  </div>
1002
  </div>
 
1003
  """
1004
 
 
1005
  html_content = f"""
1006
  <!DOCTYPE html>
1007
+ <html>
1008
+ <head>
1009
+ <meta charset="utf-8">
1010
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1011
+ <title>系统管理面板 - Gemini Business API</title>
1012
+ <style>
1013
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
1014
+ body {{
1015
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
1016
+ background: #fafaf9;
1017
+ min-height: 100vh;
1018
+ padding: 20px;
1019
+ }}
1020
+ .container {{
1021
+ max-width: 1200px;
1022
+ margin: 0 auto;
1023
+ background: white;
1024
+ border-radius: 16px;
1025
+ padding: 40px;
1026
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
1027
+ }}
1028
+ h1 {{
1029
+ color: #1a1a1a;
1030
+ font-size: 28px;
1031
+ font-weight: 600;
1032
+ margin-bottom: 8px;
1033
+ text-align: center;
1034
+ }}
1035
+ .subtitle {{
1036
+ text-align: center;
1037
+ color: #6b6b6b;
1038
+ font-size: 14px;
1039
+ margin-bottom: 30px;
1040
+ display: flex;
1041
+ justify-content: space-between;
1042
+ align-items: center;
1043
+ gap: 12px;
1044
+ }}
1045
+ .section {{
1046
+ margin-bottom: 24px;
1047
+ }}
1048
+ .section-title {{
1049
+ font-size: 18px;
1050
+ font-weight: 600;
1051
+ color: #1a1a1a;
1052
+ margin-bottom: 16px;
1053
+ }}
1054
+ .grid {{
1055
+ display: grid;
1056
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
1057
+ gap: 16px;
1058
+ margin-bottom: 16px;
1059
+ }}
1060
+ .card {{
1061
+ background: #fafaf9;
1062
+ padding: 20px;
1063
+ border: 1px solid #e5e5e5;
1064
+ border-radius: 12px;
1065
+ transition: all 0.15s ease;
1066
+ }}
1067
+ .card:hover {{
1068
+ border-color: #d4d4d4;
1069
+ box-shadow: 0 0 8px rgba(0,0,0,0.08);
1070
+ }}
1071
+ .card h3 {{
1072
+ font-size: 15px;
1073
+ color: #1a1a1a;
1074
+ margin-bottom: 12px;
1075
+ font-weight: 600;
1076
+ }}
1077
+ .btn {{
1078
+ display: inline-block;
1079
+ background: #1a73e8;
1080
+ color: white !important;
1081
+ padding: 8px 16px;
1082
+ border-radius: 8px;
1083
+ text-decoration: none;
1084
+ font-size: 14px;
1085
+ font-weight: 500;
1086
+ transition: background 0.15s ease;
1087
+ }}
1088
+ .btn:hover {{ background: #1557b0; }}
1089
+ .list {{ list-style: none; line-height: 1.8; }}
1090
+ .list li {{
1091
+ color: #6b6b6b;
1092
+ font-size: 13px;
1093
+ padding: 4px 0;
1094
+ }}
1095
+ .env-var {{
1096
+ display: flex;
1097
+ justify-content: space-between;
1098
+ align-items: center;
1099
+ padding: 8px 0;
1100
+ border-bottom: 1px solid #f0f0f0;
1101
+ }}
1102
+ .env-var:last-child {{
1103
+ border-bottom: none;
1104
+ }}
1105
+ .env-name {{
1106
+ font-family: 'Courier New', monospace;
1107
+ font-size: 13px;
1108
+ color: #1a73e8;
1109
+ font-weight: 600;
1110
+ }}
1111
+ .env-desc {{
1112
+ font-size: 12px;
1113
+ color: #6b6b6b;
1114
+ }}
1115
+ .env-value {{
1116
+ font-size: 12px;
1117
+ color: #9e9e9e;
1118
+ font-style: italic;
1119
+ }}
1120
+ .badge {{
1121
+ display: inline-block;
1122
+ padding: 2px 8px;
1123
+ border-radius: 4px;
1124
+ font-size: 11px;
1125
+ font-weight: 600;
1126
+ }}
1127
+ .badge-required {{
1128
+ background: #ffebee;
1129
+ color: #c62828;
1130
+ }}
1131
+ .badge-optional {{
1132
+ background: #e8f5e9;
1133
+ color: #2e7d32;
1134
+ }}
1135
+ code {{
1136
+ background: #f5f5f4;
1137
+ padding: 2px 6px;
1138
+ border-radius: 4px;
1139
+ font-size: 12px;
1140
+ color: #1a73e8;
1141
+ }}
1142
+ a {{ color: #1a73e8; text-decoration: none; }}
1143
+ a:hover {{ color: #1557b0; }}
1144
+ .account-grid {{
1145
+ display: grid;
1146
+ grid-template-columns: repeat(3, 1fr);
1147
+ gap: 16px;
1148
+ }}
1149
+ @media (max-width: 768px) {{
1150
+ .container {{ padding: 25px; }}
1151
+ h1 {{ font-size: 24px; }}
1152
+ .subtitle {{
1153
+ flex-direction: column;
1154
+ align-items: center;
1155
+ gap: 12px;
1156
+ }}
1157
+ .subtitle span {{
1158
+ text-align: center;
1159
+ font-size: 13px;
1160
+ }}
1161
+ .subtitle .btn {{
1162
+ width: 100%;
1163
+ text-align: center;
1164
+ }}
1165
+ .grid {{ grid-template-columns: 1fr; }}
1166
+ .account-grid {{ grid-template-columns: 1fr; }}
1167
+ }}
1168
+ </style>
1169
+ </head>
1170
+ <body>
1171
+ <div class="container">
1172
+ <h1>系统管理面板</h1>
1173
+ <div class="subtitle">
1174
+ <span>Gemini Business API - 多账户代理服务</span>
1175
+ <a href="/public/log/html" class="btn" style="font-size: 13px; padding: 6px 12px;">查看公开日志</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1176
  </div>
 
1177
 
1178
+ {api_key_status}
1179
+ {error_alert}
 
1180
 
1181
+ <!-- 账户状态 -->
1182
+ <div class="section">
1183
+ <div class="section-title">账户状态 ({len(multi_account_mgr.accounts)} 个)</div>
1184
+ <div style="background: #f0f9ff; padding: 10px 14px; border-radius: 6px; margin-bottom: 12px; border-left: 3px solid #3b82f6;">
1185
+ <p style="color: #1e40af; font-size: 12px; margin: 0;">
1186
+ ⏱️ 过期时间为12小时,可以自行修改时间,脚本可能有误差
1187
+ </p>
1188
+ </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>
1256
+ </div>
1257
+
1258
  <div class="card">
1259
  <h3>重试配置 <span class="badge badge-optional">OPTIONAL</span></h3>
1260
  <div style="margin-top: 12px;">
1261
  <div class="env-var">
1262
+ <div>
1263
+ <div class="env-name">MAX_NEW_SESSION_TRIES</div>
1264
+ <div class="env-desc">新会话尝试账户数</div>
1265
+ </div>
1266
  <div class="env-value">{MAX_NEW_SESSION_TRIES}</div>
1267
  </div>
1268
  <div class="env-var">
1269
+ <div>
1270
+ <div class="env-name">MAX_REQUEST_RETRIES</div>
1271
+ <div class="env-desc">请求失败重试次数</div>
1272
+ </div>
1273
  <div class="env-value">{MAX_REQUEST_RETRIES}</div>
1274
  </div>
1275
  <div class="env-var">
1276
+ <div>
1277
+ <div class="env-name">ACCOUNT_FAILURE_THRESHOLD</div>
1278
+ <div class="env-desc">账户失败阈值</div>
1279
+ </div>
1280
  <div class="env-value">{ACCOUNT_FAILURE_THRESHOLD} 次</div>
1281
  </div>
1282
  <div class="env-var">
1283
+ <div>
1284
+ <div class="env-name">ACCOUNT_COOLDOWN_SECONDS</div>
1285
+ <div class="env-desc">账户冷却时间</div>
1286
+ </div>
1287
  <div class="env-value">{ACCOUNT_COOLDOWN_SECONDS} 秒</div>
1288
  </div>
1289
  </div>
1290
  </div>
1291
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1292
  </div>
 
1293
 
1294
+ <!-- 模型与端点 -->
1295
+ <div class="section">
1296
+ <div class="section-title">服务信息</div>
1297
+ <div class="grid">
1298
+ <div class="card">
1299
+ <h3>支持的模型</h3>
1300
+ <ul class="list">
1301
+ <li><code>gemini-auto</code> - 自动选择(默认)</li>
1302
+ <li><code>gemini-2.5-flash</code> - Flash 2.5</li>
1303
+ <li><code>gemini-2.5-pro</code> - Pro 2.5</li>
1304
+ <li><code>gemini-3-flash-preview</code> - Flash 3 预览</li>
1305
+ <li><code>gemini-3-pro-preview</code> - Pro 3 预览 <strong style="color: #10b981;">(支持图片生成)</strong></li>
1306
+ </ul>
1307
+ <div style="margin-top: 16px; padding: 14px 16px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px;">
1308
+ <div style="font-weight: 600; color: #334155; margin-bottom: 10px; font-size: 13px;">图片生成说明</div>
1309
+ <div style="font-size: 13px; color: #475569; line-height: 1.8;">
1310
+ 仅 <code style="background: #e0e7ff; color: #4338ca; padding: 2px 6px; border-radius: 3px; font-weight: 500;">gemini-3-pro-preview</code> 支持图片生成<br>
1311
+ 保存路径: <code style="background: #e0e7ff; color: #4338ca; padding: 2px 6px; border-radius: 3px; font-weight: 500;">{IMAGE_DIR}</code><br>
1312
+ 存储类型: {'<span style="color: #059669; font-weight: 600;">持久化(重启保留)</span>' if IMAGE_DIR == '/data/images' else '<span style="color: #dc2626; font-weight: 600;">临时(重启丢失)</span>'}
1313
+ </div>
1314
  </div>
1315
  </div>
 
1316
 
1317
+ <div class="card" style="grid-column: span 2;">
1318
+ <h3>API 端点</h3>
1319
+ <ul class="list">
1320
+ <li><code>POST /{PATH_PREFIX}/v1/chat/completions</code> - 聊天接口(流式+多模态)</li>
1321
+ <li><code>GET /{PATH_PREFIX}/v1/models</code> - 获取模型列表</li>
1322
+ <li><code>GET /{PATH_PREFIX}/admin</code> - 管理首页</li>
1323
+ <li><code>GET /{PATH_PREFIX}/admin/health?key={{ADMIN_KEY}}</code> - 健康检查</li>
1324
+ <li><code>GET /{PATH_PREFIX}/admin/accounts?key={{ADMIN_KEY}}</code> - 获取账户状态(JSON)</li>
1325
+ <li><code>GET /{PATH_PREFIX}/admin/log?key={{ADMIN_KEY}}</code> - 获取日志(JSON)</li>
1326
+ <li><code>GET /{PATH_PREFIX}/admin/log/html?key={{ADMIN_KEY}}</code> - 日志查看器(HTML)</li>
1327
+ <li><code>DELETE /{PATH_PREFIX}/admin/log?confirm=yes&key={{ADMIN_KEY}}</code> - 清空日志</li>
1328
+ <li><code>GET /public/stats</code> - 公开统计信息</li>
1329
+ <li><code>GET /public/log</code> - 公开日志(JSON,脱敏)</li>
1330
+ <li><code>GET /public/log/html</code> - 公开日志查看器(HTML,脱敏)</li>
1331
+ <li><code>GET /docs</code> - FastAPI自动生成的API文档(Swagger UI)</li>
1332
+ <li><code>GET /redoc</code> - FastAPI自动生成的API文档(ReDoc)</li>
1333
+ </ul>
1334
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1335
  </div>
1336
  </div>
1337
  </div>
1338
+ </body>
 
1339
  </html>
1340
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1341
  return HTMLResponse(content=html_content)
1342
 
1343
  @app.get("/{path_prefix}/v1/models")
 
1560
  if admin_key != ADMIN_KEY:
1561
  raise HTTPException(404, "Not Found")
1562
 
1563
+ html_content = """
1564
  <!DOCTYPE html>
1565
  <html>
1566
  <head>
 
2797
  @app.get("/public/log/html")
2798
  async def get_public_logs_html():
2799
  """公开的脱敏日志查看器"""
2800
+ html_content = """
2801
  <!DOCTYPE html>
2802
  <html>
2803
  <head>
 
3024
  <body>
3025
  <div class="container">
3026
  <h1>
3027
+ <img src="https://ai.chaosmedia.cn/assets/logo-15c3152d.png" alt="Logo">
3028
  Gemini服务状态
3029
  </h1>
3030
  <div style="text-align: center; color: #999; font-size: 12px; margin-bottom: 16px;" class="subtitle-public">
3031
  <span>展示最近1000条对话日志 · 每5秒自动更新</span>
3032
+ <a href="https://ai.chaosmedia.cn" target="_blank" style="color: #1a73e8; text-decoration: none;">开始对话</a>
3033
  </div>
3034
  <div class="stats">
3035
  <div class="stat">
 
3159
  <div class="log-group" data-req-id="${reqId}">
3160
  <div class="log-group-header" onclick="toggleGroup('${reqId}')">
3161
  <span style="color: ${statusColor}; font-weight: 600; font-size: 11px;">⬤ ${statusText}</span>
3162
+ <span style="color: #999; font-size: 11px;">${log.events.length}条事件</span>
 
 
 
3163
  <span ${iconClass} style="margin-left: auto; color: #999;">▼</span>
3164
  </div>
3165
  <div class="log-group-content" ${contentStyle}>
 
3254
  return HTMLResponse(content=html_content)
3255
 
3256
  # ---------- 全局 404 处理(必须在最后) ----------
3257
+ from fastapi.responses import JSONResponse
3258
 
3259
  @app.exception_handler(404)
3260
  async def not_found_handler(request: Request, exc: HTTPException):
 
3264
  content={"detail": "Not Found"}
3265
  )
3266
 
3267
+ # 捕获所有未匹配的路径(必须在所有路由之后)
3268
+ @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"])
3269
+ async def catch_all(path: str):
3270
+ """捕获所有未匹配的路径,返回 404"""
3271
+ raise HTTPException(404, "Not Found")
3272
+
3273
  if __name__ == "__main__":
3274
  import uvicorn
3275
  uvicorn.run(app, host="0.0.0.0", port=7860)