Spaces:
Running
Running
Upload 9 files
Browse files- Dockerfile +4 -2
- main.py +151 -139
Dockerfile
CHANGED
|
@@ -8,12 +8,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
| 8 |
&& apt-get autoremove -y \
|
| 9 |
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
COPY main.py .
|
| 11 |
-
# 复制 uptime_tracker 模块
|
| 12 |
-
COPY uptime_tracker.py .
|
| 13 |
# 复制 core 模块
|
| 14 |
COPY core ./core
|
| 15 |
# 复制 util 目录
|
| 16 |
COPY util ./util
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
# 创建数据目录
|
| 18 |
RUN mkdir -p ./data/images
|
| 19 |
# 声明数据卷(运行时需要 -v 挂载才能持久化)
|
|
|
|
| 8 |
&& apt-get autoremove -y \
|
| 9 |
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
COPY main.py .
|
|
|
|
|
|
|
| 11 |
# 复制 core 模块
|
| 12 |
COPY core ./core
|
| 13 |
# 复制 util 目录
|
| 14 |
COPY util ./util
|
| 15 |
+
# 复制 templates 目录
|
| 16 |
+
COPY templates ./templates
|
| 17 |
+
# 复制 static 目录
|
| 18 |
+
COPY static ./static
|
| 19 |
# 创建数据目录
|
| 20 |
RUN mkdir -p ./data/images
|
| 21 |
# 声明数据卷(运行时需要 -v 挂载才能持久化)
|
main.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import json, time, os, asyncio, uuid, ssl, re, yaml
|
| 2 |
from datetime import datetime, timezone, timedelta
|
| 3 |
from typing import List, Optional, Union, Dict, Any
|
| 4 |
from pathlib import Path
|
|
@@ -15,6 +15,27 @@ from util.streaming_parser import parse_json_array_stream_async
|
|
| 15 |
from collections import deque
|
| 16 |
from threading import Lock
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
# 导入认证模块
|
| 19 |
from core.auth import verify_api_key
|
| 20 |
from core.session_auth import is_logged_in, login_user, logout_user, require_login, generate_session_secret
|
|
@@ -45,7 +66,12 @@ from core.account import (
|
|
| 45 |
)
|
| 46 |
|
| 47 |
# 导入 Uptime 追踪器
|
| 48 |
-
import uptime_tracker
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
# ---------- 日志配置 ----------
|
| 51 |
|
|
@@ -54,7 +80,6 @@ log_buffer = deque(maxlen=3000)
|
|
| 54 |
log_lock = Lock()
|
| 55 |
|
| 56 |
# 统计数据持久化
|
| 57 |
-
STATS_FILE = "data/stats.json"
|
| 58 |
stats_lock = asyncio.Lock() # 改为异步锁
|
| 59 |
|
| 60 |
async def load_stats():
|
|
@@ -118,102 +143,32 @@ memory_handler = MemoryLogHandler()
|
|
| 118 |
memory_handler.setFormatter(logging.Formatter("%(asctime)s | %(levelname)s | %(message)s", datefmt="%H:%M:%S"))
|
| 119 |
logger.addHandler(memory_handler)
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
# ---------- YAML 配置系统 ----------
|
| 124 |
-
SETTINGS_FILE = "data/settings.yaml"
|
| 125 |
-
|
| 126 |
-
# 默认配置
|
| 127 |
-
DEFAULT_SETTINGS = {
|
| 128 |
-
"basic": {
|
| 129 |
-
"api_key": "",
|
| 130 |
-
"base_url": "",
|
| 131 |
-
"proxy": ""
|
| 132 |
-
},
|
| 133 |
-
"image_generation": {
|
| 134 |
-
"enabled": True,
|
| 135 |
-
"supported_models": ["gemini-3-pro-preview"],
|
| 136 |
-
"last_updated": None
|
| 137 |
-
},
|
| 138 |
-
"retry": {
|
| 139 |
-
"max_new_session_tries": 5,
|
| 140 |
-
"max_request_retries": 3,
|
| 141 |
-
"max_account_switch_tries": 5,
|
| 142 |
-
"account_failure_threshold": 3,
|
| 143 |
-
"rate_limit_cooldown_seconds": 600,
|
| 144 |
-
"session_cache_ttl_seconds": 3600
|
| 145 |
-
},
|
| 146 |
-
"public_display": {
|
| 147 |
-
"logo_url": "",
|
| 148 |
-
"chat_url": ""
|
| 149 |
-
},
|
| 150 |
-
"session": {
|
| 151 |
-
"expire_hours": 24
|
| 152 |
-
}
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
def load_settings() -> dict:
|
| 156 |
-
"""加载 YAML 配置"""
|
| 157 |
-
Path("data").mkdir(exist_ok=True)
|
| 158 |
-
if Path(SETTINGS_FILE).exists():
|
| 159 |
-
try:
|
| 160 |
-
with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
|
| 161 |
-
settings = yaml.safe_load(f) or {}
|
| 162 |
-
# 合并默认配置(确保新增配置项有默认值)
|
| 163 |
-
for key, value in DEFAULT_SETTINGS.items():
|
| 164 |
-
if key not in settings:
|
| 165 |
-
settings[key] = value
|
| 166 |
-
elif isinstance(value, dict):
|
| 167 |
-
for k, v in value.items():
|
| 168 |
-
if k not in settings[key]:
|
| 169 |
-
settings[key][k] = v
|
| 170 |
-
return settings
|
| 171 |
-
except Exception as e:
|
| 172 |
-
print(f"[WARN] 加载配置文件失败: {e},使用默认配置")
|
| 173 |
-
# 创建默认配置文件
|
| 174 |
-
save_settings(DEFAULT_SETTINGS)
|
| 175 |
-
return DEFAULT_SETTINGS.copy()
|
| 176 |
-
|
| 177 |
-
def save_settings(settings: dict):
|
| 178 |
-
"""保存 YAML 配置"""
|
| 179 |
-
Path("data").mkdir(exist_ok=True)
|
| 180 |
-
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
|
| 181 |
-
yaml.dump(settings, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
|
| 182 |
-
|
| 183 |
-
# 加载配置
|
| 184 |
-
settings = load_settings()
|
| 185 |
-
|
| 186 |
-
# ---------- 配置(从 settings.yaml 读取)----------
|
| 187 |
-
PROXY = settings["basic"]["proxy"]
|
| 188 |
TIMEOUT_SECONDS = 600
|
| 189 |
-
API_KEY =
|
| 190 |
-
PATH_PREFIX =
|
| 191 |
-
ADMIN_KEY =
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
| 195 |
|
| 196 |
# ---------- 公开展示配置 ----------
|
| 197 |
-
LOGO_URL =
|
| 198 |
-
CHAT_URL =
|
| 199 |
|
| 200 |
# ---------- 图片生成配置 ----------
|
| 201 |
-
IMAGE_GENERATION_ENABLED =
|
| 202 |
-
IMAGE_GENERATION_MODELS =
|
| 203 |
|
| 204 |
-
# ----------
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
MAX_NEW_SESSION_TRIES = settings["retry"]["max_new_session_tries"]
|
| 212 |
-
MAX_REQUEST_RETRIES = settings["retry"]["max_request_retries"]
|
| 213 |
-
MAX_ACCOUNT_SWITCH_TRIES = settings["retry"]["max_account_switch_tries"]
|
| 214 |
-
ACCOUNT_FAILURE_THRESHOLD = settings["retry"]["account_failure_threshold"]
|
| 215 |
-
RATE_LIMIT_COOLDOWN_SECONDS = settings["retry"]["rate_limit_cooldown_seconds"]
|
| 216 |
-
SESSION_CACHE_TTL_SECONDS = settings["retry"]["session_cache_ttl_seconds"]
|
| 217 |
|
| 218 |
# ---------- 模型映射配置 ----------
|
| 219 |
MODEL_MAPPING = {
|
|
@@ -299,6 +254,17 @@ logger.info("[SYSTEM] 系统初始化完成")
|
|
| 299 |
# ---------- OpenAI 兼容接口 ----------
|
| 300 |
app = FastAPI(title="Gemini-Business OpenAI Gateway")
|
| 301 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
# ---------- Session 中间件配置 ----------
|
| 303 |
from starlette.middleware.sessions import SessionMiddleware
|
| 304 |
app.add_middleware(
|
|
@@ -363,6 +329,15 @@ async def startup_event():
|
|
| 363 |
"""应用启动时初始化后台任务"""
|
| 364 |
global global_stats
|
| 365 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
# 加载统计数据
|
| 367 |
global_stats = await load_stats()
|
| 368 |
logger.info(f"[SYSTEM] 统计数据已加载: {global_stats['total_requests']} 次请求, {global_stats['total_visitors']} 位访客")
|
|
@@ -375,10 +350,6 @@ async def startup_event():
|
|
| 375 |
asyncio.create_task(uptime_tracker.uptime_aggregation_task())
|
| 376 |
logger.info("[SYSTEM] Uptime 数据聚合任务已启动(间隔: 240秒)")
|
| 377 |
|
| 378 |
-
# ---------- 导入模板模块 ----------
|
| 379 |
-
# 注意:必须在所有全局变量初始化之后导入,避免循环依赖
|
| 380 |
-
from core import templates
|
| 381 |
-
|
| 382 |
# ---------- 日志脱敏函数 ----------
|
| 383 |
def get_sanitized_logs(limit: int = 100) -> list:
|
| 384 |
"""获取脱敏后的日志列表,按请求ID分组并提取关键事件"""
|
|
@@ -611,6 +582,24 @@ def create_chunk(id: str, created: int, model: str, delta: dict, finish_reason:
|
|
| 611 |
}
|
| 612 |
return json.dumps(chunk)
|
| 613 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 614 |
@app.get("/")
|
| 615 |
async def home(request: Request):
|
| 616 |
"""首页 - 根据PATH_PREFIX配置决定行为"""
|
|
@@ -620,7 +609,8 @@ async def home(request: Request):
|
|
| 620 |
else:
|
| 621 |
# 未设置PATH_PREFIX(公开模式),根据登录状态重定向
|
| 622 |
if is_logged_in(request):
|
| 623 |
-
|
|
|
|
| 624 |
else:
|
| 625 |
return RedirectResponse(url="/login", status_code=302)
|
| 626 |
|
|
@@ -630,7 +620,7 @@ async def home(request: Request):
|
|
| 630 |
@app.get("/login")
|
| 631 |
async def admin_login_get(request: Request, error: str = None):
|
| 632 |
"""登录页面"""
|
| 633 |
-
return
|
| 634 |
|
| 635 |
@app.post("/login")
|
| 636 |
async def admin_login_post(request: Request, admin_key: str = Form(...)):
|
|
@@ -641,7 +631,7 @@ async def admin_login_post(request: Request, admin_key: str = Form(...)):
|
|
| 641 |
return RedirectResponse(url="/", status_code=302)
|
| 642 |
else:
|
| 643 |
logger.warning(f"[AUTH] 登录失败 - 密钥错误")
|
| 644 |
-
return
|
| 645 |
|
| 646 |
@app.post("/logout")
|
| 647 |
@require_login(redirect_to_login=False)
|
|
@@ -656,7 +646,7 @@ if PATH_PREFIX:
|
|
| 656 |
@app.get(f"/{PATH_PREFIX}/login")
|
| 657 |
async def admin_login_get_prefixed(request: Request, error: str = None):
|
| 658 |
"""登录页面(带前缀)"""
|
| 659 |
-
return
|
| 660 |
|
| 661 |
@app.post(f"/{PATH_PREFIX}/login")
|
| 662 |
async def admin_login_post_prefixed(request: Request, admin_key: str = Form(...)):
|
|
@@ -667,7 +657,7 @@ if PATH_PREFIX:
|
|
| 667 |
return RedirectResponse(url=f"/{PATH_PREFIX}", status_code=302)
|
| 668 |
else:
|
| 669 |
logger.warning(f"[AUTH] 登录失败 - 密钥错误")
|
| 670 |
-
return
|
| 671 |
|
| 672 |
@app.post(f"/{PATH_PREFIX}/logout")
|
| 673 |
@require_login(redirect_to_login=False)
|
|
@@ -684,8 +674,8 @@ if PATH_PREFIX:
|
|
| 684 |
@require_login()
|
| 685 |
async def admin_home_no_prefix(request: Request):
|
| 686 |
"""管理首页"""
|
| 687 |
-
|
| 688 |
-
return
|
| 689 |
|
| 690 |
# 带PATH_PREFIX的管理端点(如果配置了PATH_PREFIX)
|
| 691 |
if PATH_PREFIX:
|
|
@@ -809,33 +799,45 @@ async def admin_enable_account(request: Request, account_id: str):
|
|
| 809 |
@require_login()
|
| 810 |
async def admin_get_settings(request: Request):
|
| 811 |
"""获取系统设置"""
|
| 812 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 813 |
|
| 814 |
@app.put("/admin/settings")
|
| 815 |
@require_login()
|
| 816 |
async def admin_update_settings(request: Request, new_settings: dict = Body(...)):
|
| 817 |
"""更新系统设置"""
|
| 818 |
-
global
|
| 819 |
global IMAGE_GENERATION_ENABLED, IMAGE_GENERATION_MODELS
|
| 820 |
global MAX_NEW_SESSION_TRIES, MAX_REQUEST_RETRIES, MAX_ACCOUNT_SWITCH_TRIES
|
| 821 |
global ACCOUNT_FAILURE_THRESHOLD, RATE_LIMIT_COOLDOWN_SECONDS, SESSION_CACHE_TTL_SECONDS
|
| 822 |
global SESSION_EXPIRE_HOURS, multi_account_mgr, http_client
|
| 823 |
|
| 824 |
try:
|
| 825 |
-
# 合并设置(保留未修改的项)
|
| 826 |
-
for key, value in new_settings.items():
|
| 827 |
-
if key in settings and isinstance(value, dict):
|
| 828 |
-
settings[key].update(value)
|
| 829 |
-
else:
|
| 830 |
-
settings[key] = value
|
| 831 |
-
|
| 832 |
-
# 添加更新时间
|
| 833 |
-
if "image_generation" in settings:
|
| 834 |
-
settings["image_generation"]["last_updated"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 835 |
-
|
| 836 |
-
# 保存到文件
|
| 837 |
-
save_settings(settings)
|
| 838 |
-
|
| 839 |
# 保存旧配置用于对比
|
| 840 |
old_proxy = PROXY
|
| 841 |
old_retry_config = {
|
|
@@ -844,21 +846,27 @@ async def admin_update_settings(request: Request, new_settings: dict = Body(...)
|
|
| 844 |
"session_cache_ttl_seconds": SESSION_CACHE_TTL_SECONDS
|
| 845 |
}
|
| 846 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 847 |
# 更新全局变量(实时生效)
|
| 848 |
-
API_KEY =
|
| 849 |
-
PROXY =
|
| 850 |
-
BASE_URL =
|
| 851 |
-
LOGO_URL =
|
| 852 |
-
CHAT_URL =
|
| 853 |
-
IMAGE_GENERATION_ENABLED =
|
| 854 |
-
IMAGE_GENERATION_MODELS =
|
| 855 |
-
MAX_NEW_SESSION_TRIES =
|
| 856 |
-
MAX_REQUEST_RETRIES =
|
| 857 |
-
MAX_ACCOUNT_SWITCH_TRIES =
|
| 858 |
-
ACCOUNT_FAILURE_THRESHOLD =
|
| 859 |
-
RATE_LIMIT_COOLDOWN_SECONDS =
|
| 860 |
-
SESSION_CACHE_TTL_SECONDS =
|
| 861 |
-
SESSION_EXPIRE_HOURS =
|
| 862 |
|
| 863 |
# 检查是否需要重建 HTTP 客户端(代理变化)
|
| 864 |
if old_proxy != PROXY:
|
|
@@ -960,7 +968,7 @@ async def admin_clear_logs(request: Request, confirm: str = None):
|
|
| 960 |
@require_login()
|
| 961 |
async def admin_logs_html_route(request: Request):
|
| 962 |
"""返回美化的 HTML 日志查看界面"""
|
| 963 |
-
return
|
| 964 |
|
| 965 |
# 带PATH_PREFIX的管理API端点(如果配置了PATH_PREFIX)
|
| 966 |
if PATH_PREFIX:
|
|
@@ -1590,9 +1598,9 @@ async def get_public_uptime(days: int = 90):
|
|
| 1590 |
return await uptime_tracker.get_uptime_summary(days)
|
| 1591 |
|
| 1592 |
@app.get("/public/uptime/html")
|
| 1593 |
-
async def get_public_uptime_html():
|
| 1594 |
"""Uptime 监控页面(类似 status.openai.com)"""
|
| 1595 |
-
return
|
| 1596 |
|
| 1597 |
@app.get("/public/stats")
|
| 1598 |
async def get_public_stats():
|
|
@@ -1677,9 +1685,13 @@ async def get_public_logs(request: Request, limit: int = 100):
|
|
| 1677 |
return {"total": 0, "logs": [], "error": str(e)}
|
| 1678 |
|
| 1679 |
@app.get("/public/log/html")
|
| 1680 |
-
async def get_public_logs_html():
|
| 1681 |
"""公开的脱敏日志查看器"""
|
| 1682 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1683 |
|
| 1684 |
# ---------- 全局 404 处理(必须在最后) ----------
|
| 1685 |
|
|
|
|
| 1 |
+
import json, time, os, asyncio, uuid, ssl, re, yaml, shutil
|
| 2 |
from datetime import datetime, timezone, timedelta
|
| 3 |
from typing import List, Optional, Union, Dict, Any
|
| 4 |
from pathlib import Path
|
|
|
|
| 15 |
from collections import deque
|
| 16 |
from threading import Lock
|
| 17 |
|
| 18 |
+
# ---------- 数据目录配置 ----------
|
| 19 |
+
# 自动检测环境:HF Spaces Pro 使用 /data,本地使用 ./data
|
| 20 |
+
if os.path.exists("/data"):
|
| 21 |
+
DATA_DIR = "/data" # HF Pro 持久化存储
|
| 22 |
+
logger_prefix = "[HF-PRO]"
|
| 23 |
+
else:
|
| 24 |
+
DATA_DIR = "./data" # 本地持久化存储
|
| 25 |
+
logger_prefix = "[LOCAL]"
|
| 26 |
+
|
| 27 |
+
# 确保数据目录存在
|
| 28 |
+
os.makedirs(DATA_DIR, exist_ok=True)
|
| 29 |
+
|
| 30 |
+
# 统一的数据文件路径
|
| 31 |
+
ACCOUNTS_FILE = os.path.join(DATA_DIR, "accounts.json")
|
| 32 |
+
SETTINGS_FILE = os.path.join(DATA_DIR, "settings.yaml")
|
| 33 |
+
STATS_FILE = os.path.join(DATA_DIR, "stats.json")
|
| 34 |
+
IMAGE_DIR = os.path.join(DATA_DIR, "images")
|
| 35 |
+
|
| 36 |
+
# 确保图片目录存在
|
| 37 |
+
os.makedirs(IMAGE_DIR, exist_ok=True)
|
| 38 |
+
|
| 39 |
# 导入认证模块
|
| 40 |
from core.auth import verify_api_key
|
| 41 |
from core.session_auth import is_logged_in, login_user, logout_user, require_login, generate_session_secret
|
|
|
|
| 66 |
)
|
| 67 |
|
| 68 |
# 导入 Uptime 追踪器
|
| 69 |
+
from core import uptime as uptime_tracker
|
| 70 |
+
|
| 71 |
+
# 导入配置管理和模板系统
|
| 72 |
+
from fastapi.templating import Jinja2Templates
|
| 73 |
+
from core.config import config_manager, config
|
| 74 |
+
from util.template_helpers import prepare_admin_template_data
|
| 75 |
|
| 76 |
# ---------- 日志配置 ----------
|
| 77 |
|
|
|
|
| 80 |
log_lock = Lock()
|
| 81 |
|
| 82 |
# 统计数据持久化
|
|
|
|
| 83 |
stats_lock = asyncio.Lock() # 改为异步锁
|
| 84 |
|
| 85 |
async def load_stats():
|
|
|
|
| 143 |
memory_handler.setFormatter(logging.Formatter("%(asctime)s | %(levelname)s | %(message)s", datefmt="%H:%M:%S"))
|
| 144 |
logger.addHandler(memory_handler)
|
| 145 |
|
| 146 |
+
# ---------- 配置管理(使用统一配置系统)----------
|
| 147 |
+
# 所有配置通过 config_manager 访问,优先级:环境变量 > YAML > 默认值
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
TIMEOUT_SECONDS = 600
|
| 149 |
+
API_KEY = config.basic.api_key
|
| 150 |
+
PATH_PREFIX = config.security.path_prefix
|
| 151 |
+
ADMIN_KEY = config.security.admin_key
|
| 152 |
+
PROXY = config.basic.proxy
|
| 153 |
+
BASE_URL = config.basic.base_url
|
| 154 |
+
SESSION_SECRET_KEY = config.security.session_secret_key
|
| 155 |
+
SESSION_EXPIRE_HOURS = config.session.expire_hours
|
| 156 |
|
| 157 |
# ---------- 公开展示配置 ----------
|
| 158 |
+
LOGO_URL = config.public_display.logo_url
|
| 159 |
+
CHAT_URL = config.public_display.chat_url
|
| 160 |
|
| 161 |
# ---------- 图片生成配置 ----------
|
| 162 |
+
IMAGE_GENERATION_ENABLED = config.image_generation.enabled
|
| 163 |
+
IMAGE_GENERATION_MODELS = config.image_generation.supported_models
|
| 164 |
|
| 165 |
+
# ---------- 重试配置 ----------
|
| 166 |
+
MAX_NEW_SESSION_TRIES = config.retry.max_new_session_tries
|
| 167 |
+
MAX_REQUEST_RETRIES = config.retry.max_request_retries
|
| 168 |
+
MAX_ACCOUNT_SWITCH_TRIES = config.retry.max_account_switch_tries
|
| 169 |
+
ACCOUNT_FAILURE_THRESHOLD = config.retry.account_failure_threshold
|
| 170 |
+
RATE_LIMIT_COOLDOWN_SECONDS = config.retry.rate_limit_cooldown_seconds
|
| 171 |
+
SESSION_CACHE_TTL_SECONDS = config.retry.session_cache_ttl_seconds
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
# ---------- 模型映射配置 ----------
|
| 174 |
MODEL_MAPPING = {
|
|
|
|
| 254 |
# ---------- OpenAI 兼容接口 ----------
|
| 255 |
app = FastAPI(title="Gemini-Business OpenAI Gateway")
|
| 256 |
|
| 257 |
+
# ---------- 模板系统配置 ----------
|
| 258 |
+
templates = Jinja2Templates(directory="templates")
|
| 259 |
+
|
| 260 |
+
# 开发模式:支持热更新
|
| 261 |
+
if os.getenv("ENV") == "development":
|
| 262 |
+
templates.env.auto_reload = True
|
| 263 |
+
logger.info("[SYSTEM] 模板热更新已启用(开发模式)")
|
| 264 |
+
|
| 265 |
+
# 挂载静态文件
|
| 266 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 267 |
+
|
| 268 |
# ---------- Session 中间件配置 ----------
|
| 269 |
from starlette.middleware.sessions import SessionMiddleware
|
| 270 |
app.add_middleware(
|
|
|
|
| 329 |
"""应用启动时初始化后台任务"""
|
| 330 |
global global_stats
|
| 331 |
|
| 332 |
+
# 文件迁移逻辑:将根目录的旧文件迁移到 data 目录
|
| 333 |
+
old_accounts = "accounts.json"
|
| 334 |
+
if os.path.exists(old_accounts) and not os.path.exists(ACCOUNTS_FILE):
|
| 335 |
+
try:
|
| 336 |
+
shutil.copy(old_accounts, ACCOUNTS_FILE)
|
| 337 |
+
logger.info(f"{logger_prefix} 已迁移 {old_accounts} -> {ACCOUNTS_FILE}")
|
| 338 |
+
except Exception as e:
|
| 339 |
+
logger.warning(f"{logger_prefix} 文件迁移失败: {e}")
|
| 340 |
+
|
| 341 |
# 加载统计数据
|
| 342 |
global_stats = await load_stats()
|
| 343 |
logger.info(f"[SYSTEM] 统计数据已加载: {global_stats['total_requests']} 次请求, {global_stats['total_visitors']} 位访客")
|
|
|
|
| 350 |
asyncio.create_task(uptime_tracker.uptime_aggregation_task())
|
| 351 |
logger.info("[SYSTEM] Uptime 数据聚合任务已启动(间隔: 240秒)")
|
| 352 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
# ---------- 日志脱敏函数 ----------
|
| 354 |
def get_sanitized_logs(limit: int = 100) -> list:
|
| 355 |
"""获取脱敏后的日志列表,按请求ID分组并提取关键事件"""
|
|
|
|
| 582 |
}
|
| 583 |
return json.dumps(chunk)
|
| 584 |
|
| 585 |
+
# ---------- 辅助函数 ----------
|
| 586 |
+
|
| 587 |
+
def get_admin_template_data(request: Request):
|
| 588 |
+
"""获取管理页面模板数据(避免重复代码)"""
|
| 589 |
+
return prepare_admin_template_data(
|
| 590 |
+
request, multi_account_mgr, log_buffer, log_lock,
|
| 591 |
+
api_key=API_KEY, base_url=BASE_URL, proxy=PROXY,
|
| 592 |
+
logo_url=LOGO_URL, chat_url=CHAT_URL, path_prefix=PATH_PREFIX,
|
| 593 |
+
max_new_session_tries=MAX_NEW_SESSION_TRIES,
|
| 594 |
+
max_request_retries=MAX_REQUEST_RETRIES,
|
| 595 |
+
max_account_switch_tries=MAX_ACCOUNT_SWITCH_TRIES,
|
| 596 |
+
account_failure_threshold=ACCOUNT_FAILURE_THRESHOLD,
|
| 597 |
+
rate_limit_cooldown_seconds=RATE_LIMIT_COOLDOWN_SECONDS,
|
| 598 |
+
session_cache_ttl_seconds=SESSION_CACHE_TTL_SECONDS
|
| 599 |
+
)
|
| 600 |
+
|
| 601 |
+
# ---------- 路由定义 ----------
|
| 602 |
+
|
| 603 |
@app.get("/")
|
| 604 |
async def home(request: Request):
|
| 605 |
"""首页 - 根据PATH_PREFIX配置决定行为"""
|
|
|
|
| 609 |
else:
|
| 610 |
# 未设置PATH_PREFIX(公开模式),根据登录状态重定向
|
| 611 |
if is_logged_in(request):
|
| 612 |
+
template_data = get_admin_template_data(request)
|
| 613 |
+
return templates.TemplateResponse("admin/index.html", template_data)
|
| 614 |
else:
|
| 615 |
return RedirectResponse(url="/login", status_code=302)
|
| 616 |
|
|
|
|
| 620 |
@app.get("/login")
|
| 621 |
async def admin_login_get(request: Request, error: str = None):
|
| 622 |
"""登录页面"""
|
| 623 |
+
return templates.TemplateResponse("auth/login.html", {"request": request, "error": error})
|
| 624 |
|
| 625 |
@app.post("/login")
|
| 626 |
async def admin_login_post(request: Request, admin_key: str = Form(...)):
|
|
|
|
| 631 |
return RedirectResponse(url="/", status_code=302)
|
| 632 |
else:
|
| 633 |
logger.warning(f"[AUTH] 登录失败 - 密钥错误")
|
| 634 |
+
return templates.TemplateResponse("auth/login.html", {"request": request, "error": "密钥错误,请重试"})
|
| 635 |
|
| 636 |
@app.post("/logout")
|
| 637 |
@require_login(redirect_to_login=False)
|
|
|
|
| 646 |
@app.get(f"/{PATH_PREFIX}/login")
|
| 647 |
async def admin_login_get_prefixed(request: Request, error: str = None):
|
| 648 |
"""登录页面(带前缀)"""
|
| 649 |
+
return templates.TemplateResponse("auth/login.html", {"request": request, "error": error})
|
| 650 |
|
| 651 |
@app.post(f"/{PATH_PREFIX}/login")
|
| 652 |
async def admin_login_post_prefixed(request: Request, admin_key: str = Form(...)):
|
|
|
|
| 657 |
return RedirectResponse(url=f"/{PATH_PREFIX}", status_code=302)
|
| 658 |
else:
|
| 659 |
logger.warning(f"[AUTH] 登录失败 - 密钥错误")
|
| 660 |
+
return templates.TemplateResponse("auth/login.html", {"request": request, "error": "密钥错误,请重试"})
|
| 661 |
|
| 662 |
@app.post(f"/{PATH_PREFIX}/logout")
|
| 663 |
@require_login(redirect_to_login=False)
|
|
|
|
| 674 |
@require_login()
|
| 675 |
async def admin_home_no_prefix(request: Request):
|
| 676 |
"""管理首页"""
|
| 677 |
+
template_data = get_admin_template_data(request)
|
| 678 |
+
return templates.TemplateResponse("admin/index.html", template_data)
|
| 679 |
|
| 680 |
# 带PATH_PREFIX的管理端点(如果配置了PATH_PREFIX)
|
| 681 |
if PATH_PREFIX:
|
|
|
|
| 799 |
@require_login()
|
| 800 |
async def admin_get_settings(request: Request):
|
| 801 |
"""获取系统设置"""
|
| 802 |
+
# 返回当前配置(转换为字典格式)
|
| 803 |
+
return {
|
| 804 |
+
"basic": {
|
| 805 |
+
"api_key": config.basic.api_key,
|
| 806 |
+
"base_url": config.basic.base_url,
|
| 807 |
+
"proxy": config.basic.proxy
|
| 808 |
+
},
|
| 809 |
+
"image_generation": {
|
| 810 |
+
"enabled": config.image_generation.enabled,
|
| 811 |
+
"supported_models": config.image_generation.supported_models
|
| 812 |
+
},
|
| 813 |
+
"retry": {
|
| 814 |
+
"max_new_session_tries": config.retry.max_new_session_tries,
|
| 815 |
+
"max_request_retries": config.retry.max_request_retries,
|
| 816 |
+
"max_account_switch_tries": config.retry.max_account_switch_tries,
|
| 817 |
+
"account_failure_threshold": config.retry.account_failure_threshold,
|
| 818 |
+
"rate_limit_cooldown_seconds": config.retry.rate_limit_cooldown_seconds,
|
| 819 |
+
"session_cache_ttl_seconds": config.retry.session_cache_ttl_seconds
|
| 820 |
+
},
|
| 821 |
+
"public_display": {
|
| 822 |
+
"logo_url": config.public_display.logo_url,
|
| 823 |
+
"chat_url": config.public_display.chat_url
|
| 824 |
+
},
|
| 825 |
+
"session": {
|
| 826 |
+
"expire_hours": config.session.expire_hours
|
| 827 |
+
}
|
| 828 |
+
}
|
| 829 |
|
| 830 |
@app.put("/admin/settings")
|
| 831 |
@require_login()
|
| 832 |
async def admin_update_settings(request: Request, new_settings: dict = Body(...)):
|
| 833 |
"""更新系统设置"""
|
| 834 |
+
global API_KEY, PROXY, BASE_URL, LOGO_URL, CHAT_URL
|
| 835 |
global IMAGE_GENERATION_ENABLED, IMAGE_GENERATION_MODELS
|
| 836 |
global MAX_NEW_SESSION_TRIES, MAX_REQUEST_RETRIES, MAX_ACCOUNT_SWITCH_TRIES
|
| 837 |
global ACCOUNT_FAILURE_THRESHOLD, RATE_LIMIT_COOLDOWN_SECONDS, SESSION_CACHE_TTL_SECONDS
|
| 838 |
global SESSION_EXPIRE_HOURS, multi_account_mgr, http_client
|
| 839 |
|
| 840 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 841 |
# 保存旧配置用于对比
|
| 842 |
old_proxy = PROXY
|
| 843 |
old_retry_config = {
|
|
|
|
| 846 |
"session_cache_ttl_seconds": SESSION_CACHE_TTL_SECONDS
|
| 847 |
}
|
| 848 |
|
| 849 |
+
# 保存到 YAML
|
| 850 |
+
config_manager.save_yaml(new_settings)
|
| 851 |
+
|
| 852 |
+
# 热更新配置
|
| 853 |
+
config_manager.reload()
|
| 854 |
+
|
| 855 |
# 更新全局变量(实时生效)
|
| 856 |
+
API_KEY = config.basic.api_key
|
| 857 |
+
PROXY = config.basic.proxy
|
| 858 |
+
BASE_URL = config.basic.base_url
|
| 859 |
+
LOGO_URL = config.public_display.logo_url
|
| 860 |
+
CHAT_URL = config.public_display.chat_url
|
| 861 |
+
IMAGE_GENERATION_ENABLED = config.image_generation.enabled
|
| 862 |
+
IMAGE_GENERATION_MODELS = config.image_generation.supported_models
|
| 863 |
+
MAX_NEW_SESSION_TRIES = config.retry.max_new_session_tries
|
| 864 |
+
MAX_REQUEST_RETRIES = config.retry.max_request_retries
|
| 865 |
+
MAX_ACCOUNT_SWITCH_TRIES = config.retry.max_account_switch_tries
|
| 866 |
+
ACCOUNT_FAILURE_THRESHOLD = config.retry.account_failure_threshold
|
| 867 |
+
RATE_LIMIT_COOLDOWN_SECONDS = config.retry.rate_limit_cooldown_seconds
|
| 868 |
+
SESSION_CACHE_TTL_SECONDS = config.retry.session_cache_ttl_seconds
|
| 869 |
+
SESSION_EXPIRE_HOURS = config.session.expire_hours
|
| 870 |
|
| 871 |
# 检查是否需要重建 HTTP 客户端(代理变化)
|
| 872 |
if old_proxy != PROXY:
|
|
|
|
| 968 |
@require_login()
|
| 969 |
async def admin_logs_html_route(request: Request):
|
| 970 |
"""返回美化的 HTML 日志查看界面"""
|
| 971 |
+
return templates.TemplateResponse("admin/logs.html", {"request": request})
|
| 972 |
|
| 973 |
# 带PATH_PREFIX的管理API端点(如果配置了PATH_PREFIX)
|
| 974 |
if PATH_PREFIX:
|
|
|
|
| 1598 |
return await uptime_tracker.get_uptime_summary(days)
|
| 1599 |
|
| 1600 |
@app.get("/public/uptime/html")
|
| 1601 |
+
async def get_public_uptime_html(request: Request):
|
| 1602 |
"""Uptime 监控页面(类似 status.openai.com)"""
|
| 1603 |
+
return templates.TemplateResponse("public/uptime.html", {"request": request})
|
| 1604 |
|
| 1605 |
@app.get("/public/stats")
|
| 1606 |
async def get_public_stats():
|
|
|
|
| 1685 |
return {"total": 0, "logs": [], "error": str(e)}
|
| 1686 |
|
| 1687 |
@app.get("/public/log/html")
|
| 1688 |
+
async def get_public_logs_html(request: Request):
|
| 1689 |
"""公开的脱敏日志查看器"""
|
| 1690 |
+
return templates.TemplateResponse("public/logs.html", {
|
| 1691 |
+
"request": request,
|
| 1692 |
+
"logo_url": LOGO_URL,
|
| 1693 |
+
"chat_url": CHAT_URL
|
| 1694 |
+
})
|
| 1695 |
|
| 1696 |
# ---------- 全局 404 处理(必须在最后) ----------
|
| 1697 |
|