Spaces:
Sleeping
Sleeping
Upload main.py
Browse files
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
|
| 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
|
| 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 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 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 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
<
|
| 965 |
</div>
|
| 966 |
-
</div>
|
| 967 |
"""
|
| 968 |
else:
|
| 969 |
api_key_status = """
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
<
|
| 975 |
</div>
|
| 976 |
-
</div>
|
| 977 |
"""
|
| 978 |
|
|
|
|
| 979 |
error_alert = ""
|
| 980 |
if error_count > 0:
|
| 981 |
error_alert = f"""
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
<strong>检测到 {error_count} 条错误日志</strong>
|
| 986 |
-
<a href="/public/log/html" class="alert-link">查看详情 →</a>
|
| 987 |
</div>
|
| 988 |
-
</div>
|
| 989 |
"""
|
| 990 |
|
| 991 |
-
#
|
| 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 |
-
|
| 999 |
-
|
| 1000 |
-
dot_title = "可用" if is_avail else "不可用"
|
| 1001 |
|
| 1002 |
accounts_html += f"""
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
|
| 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
|
| 1017 |
-
<
|
| 1018 |
-
<
|
| 1019 |
</div>
|
| 1020 |
</div>
|
| 1021 |
-
</div>
|
| 1022 |
"""
|
| 1023 |
|
| 1024 |
-
# --- 3. 构建 HTML ---
|
| 1025 |
html_content = f"""
|
| 1026 |
<!DOCTYPE html>
|
| 1027 |
-
<html
|
| 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 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
|
| 1194 |
-
|
| 1195 |
-
|
| 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 |
-
|
| 1248 |
-
|
| 1249 |
-
{error_alert}
|
| 1250 |
|
| 1251 |
-
|
| 1252 |
-
<div class="section
|
| 1253 |
-
|
| 1254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1255 |
</div>
|
| 1256 |
-
</div>
|
| 1257 |
|
| 1258 |
-
|
| 1259 |
-
<div class="section
|
| 1260 |
-
|
| 1261 |
-
<div class="
|
| 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>
|
|
|
|
|
|
|
|
|
|
| 1267 |
</div>
|
| 1268 |
<div class="env-var">
|
| 1269 |
-
<div>
|
|
|
|
|
|
|
|
|
|
| 1270 |
<div class="env-value">当前: {PATH_PREFIX}</div>
|
| 1271 |
</div>
|
| 1272 |
<div class="env-var">
|
| 1273 |
-
<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>
|
|
|
|
|
|
|
|
|
|
| 1284 |
<div class="env-value">{MAX_NEW_SESSION_TRIES}</div>
|
| 1285 |
</div>
|
| 1286 |
<div class="env-var">
|
| 1287 |
-
<div>
|
|
|
|
|
|
|
|
|
|
| 1288 |
<div class="env-value">{MAX_REQUEST_RETRIES}</div>
|
| 1289 |
</div>
|
| 1290 |
<div class="env-var">
|
| 1291 |
-
<div>
|
|
|
|
|
|
|
|
|
|
| 1292 |
<div class="env-value">{ACCOUNT_FAILURE_THRESHOLD} 次</div>
|
| 1293 |
</div>
|
| 1294 |
<div class="env-var">
|
| 1295 |
-
<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 |
-
|
| 1343 |
-
<div class="section
|
| 1344 |
-
|
| 1345 |
-
<div class="
|
| 1346 |
-
<
|
| 1347 |
-
|
| 1348 |
-
<
|
| 1349 |
-
|
| 1350 |
-
|
| 1351 |
-
|
| 1352 |
-
|
| 1353 |
-
|
| 1354 |
-
|
| 1355 |
-
|
| 1356 |
-
|
| 1357 |
-
|
| 1358 |
-
|
| 1359 |
-
|
| 1360 |
-
|
|
|
|
| 1361 |
</div>
|
| 1362 |
</div>
|
| 1363 |
-
</div>
|
| 1364 |
|
| 1365 |
-
|
| 1366 |
-
|
| 1367 |
-
|
| 1368 |
-
|
| 1369 |
-
|
| 1370 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
</
|
| 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 =
|
| 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 =
|
| 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 |
-
|
| 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 |
-
|
| 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: #
|
| 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)
|