Spaces:
Sleeping
Sleeping
Upload main.py
Browse files
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
|
| 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 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 952 |
api_key_status = ""
|
| 953 |
if API_KEY:
|
| 954 |
api_key_status = """
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
</div>
|
|
|
|
| 961 |
"""
|
| 962 |
else:
|
| 963 |
api_key_status = """
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
</div>
|
|
|
|
| 970 |
"""
|
| 971 |
|
| 972 |
-
# 错误提醒
|
| 973 |
error_alert = ""
|
| 974 |
if error_count > 0:
|
| 975 |
error_alert = f"""
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 992 |
-
|
|
|
|
| 993 |
|
| 994 |
accounts_html += f"""
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
</div>
|
| 1001 |
-
<span style="color: {status_color}; font-weight: 600; font-size: 12px;">{status_text}</span>
|
| 1002 |
</div>
|
| 1003 |
-
<
|
| 1004 |
-
|
| 1005 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1006 |
</div>
|
| 1007 |
</div>
|
|
|
|
| 1008 |
"""
|
| 1009 |
|
|
|
|
| 1010 |
html_content = f"""
|
| 1011 |
<!DOCTYPE html>
|
| 1012 |
-
<html>
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
body
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
|
| 1113 |
-
|
| 1114 |
-
|
| 1115 |
-
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
-
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
|
| 1133 |
-
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
|
| 1137 |
-
|
| 1138 |
-
|
| 1139 |
-
|
| 1140 |
-
|
| 1141 |
-
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
|
| 1159 |
-
|
| 1160 |
-
|
| 1161 |
-
|
| 1162 |
-
|
| 1163 |
-
|
| 1164 |
-
|
| 1165 |
-
|
| 1166 |
-
|
| 1167 |
-
|
| 1168 |
-
|
| 1169 |
-
|
| 1170 |
-
|
| 1171 |
-
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1181 |
</div>
|
|
|
|
| 1182 |
|
| 1183 |
-
|
| 1184 |
-
|
|
|
|
| 1185 |
|
| 1186 |
-
|
| 1187 |
-
<div class="section">
|
| 1188 |
-
|
| 1189 |
-
<div class="
|
| 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 |
-
|
| 1197 |
-
<div class="
|
| 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 |
-
|
| 1318 |
-
<div class="
|
| 1319 |
-
<
|
| 1320 |
-
|
| 1321 |
-
<
|
| 1322 |
-
|
| 1323 |
-
|
| 1324 |
-
|
| 1325 |
-
|
| 1326 |
-
|
| 1327 |
-
|
| 1328 |
-
|
| 1329 |
-
|
| 1330 |
-
|
| 1331 |
-
|
| 1332 |
-
|
| 1333 |
-
|
| 1334 |
-
</div>
|
| 1335 |
</div>
|
| 1336 |
</div>
|
|
|
|
| 1337 |
|
| 1338 |
-
|
| 1339 |
-
|
| 1340 |
-
|
| 1341 |
-
|
| 1342 |
-
|
| 1343 |
-
|
| 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 |
-
</
|
|
|
|
| 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: #
|
|
|
|
|
|
|
|
|
|
| 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">查看详情 →</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)
|