|
|
| """
|
| FastAPI 应用主入口文件。
|
| 负责初始化应用、配置、加载资源、设置路由和启动服务器。
|
| """
|
|
|
| import sys
|
| import os
|
| import logging
|
| import json
|
| import asyncio
|
| import uvicorn
|
| from contextlib import asynccontextmanager
|
| from typing import AsyncGenerator
|
| from asyncio import TimeoutError
|
| from fastapi.staticfiles import StaticFiles
|
| from fastapi.responses import FileResponse
|
|
|
|
|
| from fastapi import FastAPI, HTTPException, Request, Response
|
| from fastapi.templating import Jinja2Templates
|
| from dotenv import load_dotenv
|
| import aiosqlite
|
| from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
| from sqlalchemy.orm import sessionmaker
|
| import httpx
|
|
|
|
|
|
|
| from app import config
|
|
|
| from app.api import endpoints as api_endpoints
|
| from app.api import cache_endpoints
|
| from app.api import v2_endpoints
|
|
|
| from app.web import routes as web_routes
|
|
|
| from app.handlers import error_handlers
|
|
|
| from app.core.keys import checker as key_checker
|
| from app.core.keys.manager import APIKeyManager
|
|
|
| from app.core.reporting import scheduler as reporting_scheduler
|
|
|
| from app.handlers.log_config import setup_logger
|
|
|
| from app.config import __version__, SECRET_KEY, load_model_limits
|
|
|
| from app.core.services.gemini import GeminiClient
|
| from app.core.context.store import ContextStore
|
| from app.core.database import utils as db_utils
|
| from app.core.dependencies import get_key_manager, get_http_client, get_db_session
|
| from app.core.cache.manager import CacheManager
|
| from app.core.cache.cleanup import start_cache_cleanup_scheduler
|
|
|
|
|
|
|
|
|
| load_dotenv()
|
|
|
|
|
| logger = setup_logger()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| @asynccontextmanager
|
| async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
| """
|
| FastAPI 应用的生命周期事件处理器。
|
| 在应用启动时执行初始化任务,在应用关闭时执行清理任务。
|
|
|
| Args:
|
| app (FastAPI): FastAPI 应用实例。
|
|
|
| Yields:
|
| None: 在应用运行期间 yield None。
|
| """
|
|
|
| logger.info(f"启动 Gemini API 代理 v{__version__}...")
|
|
|
|
|
| key_manager = APIKeyManager()
|
| http_client = httpx.AsyncClient()
|
| cache_manager = CacheManager()
|
| context_store_manager = ContextStore()
|
|
|
|
|
|
|
| app.state.key_manager = key_manager
|
| app.state.http_client = http_client
|
| app.state.cache_manager = cache_manager
|
| app.state.context_store_manager = context_store_manager
|
|
|
|
|
|
|
| db_engine = create_async_engine(db_utils.DATABASE_URL, echo=False)
|
|
|
| AsyncSessionFactory = sessionmaker(
|
| bind=db_engine,
|
| class_=AsyncSession,
|
| expire_on_commit=False
|
| )
|
|
|
| app.state.db_engine = db_engine
|
| app.state.AsyncSessionFactory = AsyncSessionFactory
|
| logger.info("数据库引擎和会话工厂已初始化。")
|
|
|
|
|
| RED = '\033[91m'
|
| RESET = '\033[0m'
|
| if not config.ADMIN_API_KEY:
|
|
|
| logger.warning(f"{RED}****************************************************************{RESET}")
|
| logger.warning(f"{RED}警告: 管理员 API Key (ADMIN_API_KEY) 未设置!{RESET}")
|
| logger.warning(f"{RED}部分管理功能(如代理 Key 管理)将不可用。{RESET}")
|
| logger.warning(f"{RED}强烈建议在环境变量中配置 ADMIN_API_KEY 以启用全部功能。{RESET}")
|
| logger.warning(f"{RED}****************************************************************{RESET}")
|
| else:
|
| logger.info("管理员 API Key (ADMIN_API_KEY) 已配置。")
|
|
|
|
|
| logger.info(f"Web UI 密码保护已启用: {'是' if config.WEB_UI_PASSWORDS else '否'}")
|
| logger.info(f"本地 IP 速率限制 (可能已废弃): 每分钟最大请求数={config.MAX_REQUESTS_PER_MINUTE}, 每 IP 每日最大请求数={config.MAX_REQUESTS_PER_DAY_PER_IP}")
|
| logger.info(f"全局禁用 Gemini 安全过滤: {config.DISABLE_SAFETY_FILTERING}")
|
| if config.DISABLE_SAFETY_FILTERING:
|
| logger.info("注意:全局安全过滤已禁用,所有请求将不包含安全设置。")
|
|
|
| if config.USAGE_REPORT_INTERVAL_MINUTES == 30:
|
| interval_env = os.environ.get("USAGE_REPORT_INTERVAL_MINUTES")
|
| if interval_env:
|
| try:
|
| if int(interval_env) <= 0:
|
| logger.warning("环境变量 USAGE_REPORT_INTERVAL_MINUTES 必须为正整数,当前设置无效,将使用默认值 30 分钟。")
|
| except ValueError:
|
| logger.warning(f"无法将环境变量 USAGE_REPORT_INTERVAL_MINUTES ('{interval_env}') 解析为整数,将使用默认值 30 分钟。")
|
|
|
| report_level_env = os.environ.get("REPORT_LOG_LEVEL", "INFO").upper()
|
| if config.REPORT_LOG_LEVEL_INT == logging.INFO and report_level_env != "INFO":
|
| logger.warning(f"无效的环境变量 REPORT_LOG_LEVEL 值 '{os.environ.get('REPORT_LOG_LEVEL')}'. 将使用默认日志级别 INFO。")
|
| else:
|
| logger.info(f"使用情况报告日志级别设置为: {config.REPORT_LOG_LEVEL_STR}")
|
|
|
|
|
| logger.info("正在执行初始 API 密钥检查...")
|
|
|
| async with AsyncSessionFactory() as db_session_for_check:
|
| try:
|
|
|
| await key_checker.check_keys(key_manager, app.state.http_client, db_session_for_check)
|
| except Exception as check_keys_err:
|
| logger.error(f"执行初始 API 密钥检查时发生错误: {check_keys_err}", exc_info=True)
|
|
|
|
|
|
|
| active_keys_count = key_manager.get_active_keys_count()
|
| logger.info(f"初始密钥检查摘要: 总配置数={key_checker.INITIAL_KEY_COUNT}, 有效数={active_keys_count}, 无效数={len(key_checker.INVALID_KEYS)}")
|
| if active_keys_count > 0:
|
| logger.info(f"最大 API 调用重试次数设置为 (基于有效密钥): {active_keys_count}")
|
| else:
|
| logger.error(f"{RED}没有有效的 API 密钥,服务可能无法正常运行!{RESET}")
|
|
|
|
|
| load_model_limits()
|
| logger.info(f"已加载模型限制配置: {list(config.MODEL_LIMITS.keys())}")
|
|
|
|
|
| if active_keys_count > 0:
|
| logger.info("尝试预获取可用模型列表...")
|
| try:
|
| if key_manager.api_keys:
|
| key_to_use = key_manager.api_keys[0]
|
|
|
| all_models = await asyncio.wait_for(
|
| GeminiClient.list_available_models(key_to_use, app.state.http_client),
|
| timeout=60.0
|
| )
|
|
|
| GeminiClient.AVAILABLE_MODELS = [model.replace("models/", "") for model in all_models]
|
| logger.info(f"成功预获取可用模型: {GeminiClient.AVAILABLE_MODELS}")
|
| else:
|
| logger.warning("没有可用的有效密钥来预取模型列表。")
|
| GeminiClient.AVAILABLE_MODELS = []
|
| except TimeoutError:
|
| logger.error("启动时预获取模型列表超时 (超过 60 秒)。将在第一次 /v1/models 请求时再次尝试。")
|
| GeminiClient.AVAILABLE_MODELS = []
|
| except Exception as e:
|
| logger.error(f"启动时预获取模型列表失败: {e}. 将在第一次 /v1/models 请求时再次尝试。", exc_info=True)
|
| GeminiClient.AVAILABLE_MODELS = []
|
|
|
|
|
| logger.info("设置后台调度器...")
|
| reporting_scheduler.setup_scheduler(key_manager, app.state.context_store_manager)
|
|
|
|
|
| logger.info("尝试启动缓存清理调度器...")
|
| try:
|
|
|
| cache_cleanup_scheduler_instance = start_cache_cleanup_scheduler()
|
| app.state.cache_cleanup_scheduler = cache_cleanup_scheduler_instance
|
| logger.info("缓存清理调度器已成功设置并启动。")
|
| except Exception as e:
|
| logger.error(f"启动缓存清理调度器失败: {e}", exc_info=True)
|
|
|
|
|
|
|
|
|
| logger.info("注册自定义 sys.excepthook...")
|
| sys.excepthook = error_handlers.handle_exception
|
|
|
|
|
| logger.info("正在初始化数据库表...")
|
| if app.state.db_engine:
|
| try:
|
| async with app.state.db_engine.begin() as conn:
|
|
|
| from app.core.database.models import Base
|
| await conn.run_sync(Base.metadata.create_all)
|
| logger.info("所有通过 SQLAlchemy Base 定义的数据库表已成功初始化/验证。")
|
| except TimeoutError:
|
| logger.error("数据库表初始化超时,应用可能无法正常运行。", exc_info=True)
|
| except Exception as e:
|
| logger.error(f"数据库表初始化失败,应用可能无法正常运行: {e}", exc_info=True)
|
|
|
|
|
| else:
|
| logger.error("数据库引擎未初始化,无法创建表!应用可能无法正常运行。")
|
|
|
| logger.info("应用程序启动完成。")
|
|
|
|
|
| logger.info("启动后台调度器...")
|
| reporting_scheduler.start_scheduler()
|
|
|
|
|
| yield
|
|
|
|
|
| logger.info("正在关闭应用程序...")
|
|
|
| reporting_scheduler.shutdown_scheduler()
|
|
|
| if hasattr(app.state, 'cache_cleanup_scheduler') and app.state.cache_cleanup_scheduler and app.state.cache_cleanup_scheduler.running:
|
| logger.info("正在关闭缓存清理调度器...")
|
| app.state.cache_cleanup_scheduler.shutdown()
|
|
|
| if hasattr(app.state, 'http_client') and app.state.http_client:
|
| logger.info("正在关闭 HTTP 客户端...")
|
| await app.state.http_client.aclose()
|
|
|
| if hasattr(app.state, 'db_engine') and app.state.db_engine:
|
| logger.info("正在关闭数据库引擎...")
|
| await app.state.db_engine.dispose()
|
| logger.info("应用程序关闭完成。")
|
|
|
|
|
|
|
|
|
| APP_ROOT_PATH = os.environ.get("ROOT_PATH", "")
|
| if APP_ROOT_PATH and not APP_ROOT_PATH.startswith("/"):
|
| APP_ROOT_PATH = "/" + APP_ROOT_PATH
|
|
|
| app = FastAPI(
|
| title="Gemini API 代理 (重构版)",
|
| version=__version__,
|
| description="一个重构后的 FastAPI 代理,用于 Google Gemini API,具有密钥轮换、使用情况跟踪和优化功能。",
|
| lifespan=lifespan,
|
| proxy_headers=True,
|
| root_path=APP_ROOT_PATH
|
| )
|
|
|
|
|
| from fastapi.responses import HTMLResponse
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| @app.middleware("http")
|
| async def add_permissions_policy_header(request: Request, call_next):
|
| response: Response = await call_next(request)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| policy = "geolocation=()"
|
|
|
| logger.debug(f"为路径 {request.url.path} 暂时移除了 Permissions-Policy 头部 (诊断模式)。")
|
| return response
|
| logger.info("已添加 Permissions-Policy 中间件。")
|
|
|
|
|
|
|
|
|
|
|
| app.add_exception_handler(HTTPException, error_handlers.global_exception_handler)
|
| logger.info("已为 HTTPException 注册全局异常处理器。")
|
| app.add_exception_handler(Exception, error_handlers.global_exception_handler)
|
| logger.info("已为通用 Exception 注册全局异常处理器。")
|
|
|
|
|
|
|
| app.include_router(api_endpoints.router, tags=["OpenAI Compatible API v1"])
|
| logger.info("已包含 OpenAI Compatible API (v1) 端点路由器。")
|
| app.include_router(v2_endpoints.v2_router, prefix="/v2", tags=["Gemini Native API v2"])
|
| logger.info("已包含 Gemini 原生 API 端点路由器 (/v2)。")
|
| app.include_router(cache_endpoints.router, prefix="/api")
|
| logger.info("已包含缓存管理 API 路由器 (/api)。")
|
|
|
|
|
| app.include_router(web_routes.router)
|
| logger.info("已包含 Web UI 路由器。")
|
|
|
|
|
|
|
| app.mount("/assets", StaticFiles(directory="assets"), name="assets")
|
| logger.info("已挂载静态文件目录 /assets。")
|
|
|
|
|
| @app.get("/favicon.ico", include_in_schema=False)
|
| async def favicon():
|
| """
|
| 提供 favicon.ico 文件。
|
| """
|
| return FileResponse("assets/favicon.ico")
|
|
|
|
|
|
|
| templates_for_404 = Jinja2Templates(directory="app/web/templates")
|
|
|
| @app.api_route("/{path_name:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"], include_in_schema=False)
|
| async def catch_all_404(request: Request, path_name: str):
|
| """
|
| 捕获所有未匹配的路径,并返回 404 HTML 页面。
|
| """
|
| logger.warning(f"访问了不存在的路径: /{path_name} (方法: {request.method}),将渲染 404 页面。")
|
| return templates_for_404.TemplateResponse(
|
| "404.html",
|
| {"request": request, "detail": f"路径 /{path_name} 未找到"},
|
| status_code=404
|
| )
|
| logger.info("已添加捕获所有路径的 404 处理器。")
|
|
|
| print("="*20 + " Registered Routes " + "="*20)
|
| for route in app.routes:
|
| if hasattr(route, "name"):
|
| print(f"Path: {getattr(route, 'path', 'N/A')}, Name: {route.name}")
|
| else:
|
|
|
| route_path = getattr(route, 'path', None)
|
| if route_path is None and hasattr(route, 'app'):
|
| route_path = getattr(getattr(route, 'app'), 'root_path', 'N/A (Mounted App)')
|
| print(f"Path: {route_path if route_path is not None else 'N/A'}, (No name attribute or complex route type)")
|
| print("="*50)
|
|
|
|
|
|
|
|
|
| if __name__ == "__main__":
|
|
|
| port = int(os.environ.get("PORT", 7860))
|
| host = os.environ.get("HOST", "0.0.0.0")
|
| reload_flag = os.environ.get("UVICORN_RELOAD", "true").lower() == "true"
|
| log_level = os.environ.get("UVICORN_LOG_LEVEL", "info").lower()
|
|
|
|
|
| logger.info(f"在 {host}:{port} 上启动 Uvicorn 服务器 (自动重载: {reload_flag}, 日志级别: {log_level})")
|
|
|
| uvicorn.run(
|
| "app.main:app",
|
| host=host,
|
| port=port,
|
| reload=reload_flag,
|
| log_level=log_level
|
| )
|
|
|