Spaces:
Paused
Paused
| """ | |
| FastAPI 应用组装:配置加载、账号池、会话缓存、浏览器管理、插件注册、路由挂载。 | |
| """ | |
| import asyncio | |
| import json | |
| import logging | |
| import os | |
| from contextlib import asynccontextmanager | |
| from pathlib import Path | |
| from typing import AsyncIterator | |
| from fastapi import FastAPI, Request | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import FileResponse, JSONResponse | |
| from core.account.pool import AccountPool | |
| from core.api.auth import ( | |
| AdminLoginAttemptStore, | |
| AdminSessionStore, | |
| configured_config_login_lock_seconds, | |
| configured_config_login_max_failures, | |
| config_login_enabled, | |
| ensure_config_secret_hashed, | |
| refresh_runtime_auth_settings, | |
| ) | |
| from core.api.anthropic_routes import create_anthropic_router | |
| from core.api.chat_handler import ChatHandler | |
| from core.api.config_routes import create_config_router | |
| from core.api.routes import create_router | |
| from core.config.repository import create_config_repository | |
| from core.config.settings import get, get_bool | |
| from core.constants import CDP_PORT_RANGE, CHROMIUM_BIN | |
| from core.plugin.base import PluginRegistry | |
| from core.plugin.claude import register_claude_plugin | |
| from core.runtime.browser_manager import BrowserManager | |
| from core.runtime.session_cache import SessionCache | |
| logger = logging.getLogger(__name__) | |
| STATIC_DIR = Path(__file__).resolve().parent / "static" | |
| _ACCOUNTS_ENV_KEY = "WEB2API_ACCOUNTS" | |
| def _seed_accounts_from_env(repo) -> bool: | |
| """ | |
| If WEB2API_ACCOUNTS is set and the database has no accounts yet, | |
| seed the database with accounts from the environment variable. | |
| Supported formats: | |
| - JSON array: full proxy group config (same as PUT /api/config body) | |
| - Simple comma-separated sessionKeys: creates one direct group with N claude accounts | |
| Returns True if accounts were seeded. | |
| """ | |
| raw = os.environ.get(_ACCOUNTS_ENV_KEY, "").strip() | |
| if not raw: | |
| return False | |
| existing = repo.load_groups() | |
| if existing and any(g.accounts for g in existing): | |
| return False | |
| # Try JSON format first | |
| try: | |
| data = json.loads(raw) | |
| if isinstance(data, list) and data: | |
| from core.config.repository import _raw_to_groups | |
| groups = _raw_to_groups(data) | |
| repo.save_groups(groups) | |
| total = sum(len(g.accounts) for g in groups) | |
| logger.info( | |
| "[env-seed] loaded %d accounts from %s (JSON format)", | |
| total, _ACCOUNTS_ENV_KEY, | |
| ) | |
| return True | |
| except (json.JSONDecodeError, TypeError): | |
| pass | |
| # Fall back to simple comma-separated sessionKeys | |
| keys = [k.strip() for k in raw.split(",") if k.strip()] | |
| if not keys: | |
| return False | |
| proxy_host = os.environ.get("WEB2API_ACCOUNTS_PROXY_HOST", "").strip() | |
| proxy_user = os.environ.get("WEB2API_ACCOUNTS_PROXY_USER", "").strip() | |
| proxy_pass = os.environ.get("WEB2API_ACCOUNTS_PROXY_PASS", "").strip() | |
| fingerprint_id = os.environ.get("WEB2API_ACCOUNTS_FINGERPRINT", "env-default").strip() | |
| use_proxy = bool(proxy_host) | |
| from core.config.schema import AccountConfig, ProxyGroupConfig | |
| accounts = [ | |
| AccountConfig( | |
| name=f"claude-{i + 1}", | |
| type="claude", | |
| auth={"sessionKey": key}, | |
| ) | |
| for i, key in enumerate(keys) | |
| ] | |
| group = ProxyGroupConfig( | |
| proxy_host=proxy_host, | |
| proxy_user=proxy_user, | |
| proxy_pass=proxy_pass, | |
| fingerprint_id=fingerprint_id, | |
| use_proxy=use_proxy, | |
| timezone=os.environ.get("WEB2API_ACCOUNTS_TIMEZONE"), | |
| accounts=accounts, | |
| ) | |
| repo.save_groups([group]) | |
| logger.info( | |
| "[env-seed] loaded %d sessionKeys from %s (comma format, proxy=%s)", | |
| len(keys), _ACCOUNTS_ENV_KEY, use_proxy, | |
| ) | |
| return True | |
| async def lifespan(app: FastAPI) -> AsyncIterator[None]: | |
| """启动时初始化配置与 ChatHandler,关闭时不做持久化(会话缓存进程内)。""" | |
| # 注册插件 | |
| register_claude_plugin() | |
| repo = create_config_repository() | |
| repo.init_schema() | |
| ensure_config_secret_hashed(repo) | |
| _seed_accounts_from_env(repo) | |
| app.state.config_repo = repo | |
| auth_settings = refresh_runtime_auth_settings(app) | |
| groups = repo.load_groups() | |
| chromium_bin = (get("browser", "chromium_bin") or "").strip() or CHROMIUM_BIN | |
| headless = get_bool("browser", "headless", False) | |
| no_sandbox = get_bool("browser", "no_sandbox", False) | |
| disable_gpu = get_bool("browser", "disable_gpu", False) | |
| disable_gpu_sandbox = get_bool("browser", "disable_gpu_sandbox", False) | |
| cdp_wait_max_attempts = int(get("browser", "cdp_wait_max_attempts") or 90) | |
| cdp_wait_interval_seconds = float( | |
| get("browser", "cdp_wait_interval_seconds") or 2.0 | |
| ) | |
| cdp_wait_connect_timeout_seconds = float( | |
| get("browser", "cdp_wait_connect_timeout_seconds") or 2.0 | |
| ) | |
| port_start = int(get("browser", "cdp_port_start") or 9223) | |
| port_count = int(get("browser", "cdp_port_count") or 20) | |
| port_range = ( | |
| list(range(port_start, port_start + port_count)) | |
| if port_count > 0 | |
| else list(CDP_PORT_RANGE) | |
| ) | |
| api_keys = auth_settings.api_keys | |
| pool = AccountPool.from_groups(groups) | |
| session_cache = SessionCache() | |
| browser_manager = BrowserManager( | |
| chromium_bin=chromium_bin, | |
| headless=headless, | |
| no_sandbox=no_sandbox, | |
| disable_gpu=disable_gpu, | |
| disable_gpu_sandbox=disable_gpu_sandbox, | |
| port_range=port_range, | |
| cdp_wait_max_attempts=cdp_wait_max_attempts, | |
| cdp_wait_interval_seconds=cdp_wait_interval_seconds, | |
| cdp_wait_connect_timeout_seconds=cdp_wait_connect_timeout_seconds, | |
| ) | |
| app.state.chat_handler = ChatHandler( | |
| pool=pool, | |
| session_cache=session_cache, | |
| browser_manager=browser_manager, | |
| config_repo=repo, | |
| ) | |
| app.state.session_cache = session_cache | |
| app.state.browser_manager = browser_manager | |
| app.state.admin_sessions = AdminSessionStore() | |
| app.state.admin_login_attempts = AdminLoginAttemptStore( | |
| max_failures=configured_config_login_max_failures(), | |
| lock_seconds=configured_config_login_lock_seconds(), | |
| ) | |
| if not groups: | |
| logger.warning("数据库无配置,服务已启动但当前无可用账号") | |
| if api_keys: | |
| logger.info("API 鉴权已启用,已加载 %d 个 API Key", len(api_keys)) | |
| if auth_settings.config_login_enabled: | |
| logger.info( | |
| "配置页登录已启用,失败 %d 次锁定 %d 秒", | |
| app.state.admin_login_attempts.max_failures, | |
| app.state.admin_login_attempts.lock_seconds, | |
| ) | |
| try: | |
| await app.state.chat_handler.prewarm_resident_browsers() | |
| except Exception: | |
| logger.exception("启动预热浏览器失败") | |
| app.state.maintenance_task = asyncio.create_task( | |
| app.state.chat_handler.run_maintenance_loop() | |
| ) | |
| logger.info("服务已就绪,已注册 type: %s", ", ".join(PluginRegistry.all_types())) | |
| yield | |
| task = getattr(app.state, "maintenance_task", None) | |
| handler = getattr(app.state, "chat_handler", None) | |
| if handler is not None: | |
| await handler.shutdown() | |
| if task is not None: | |
| try: | |
| await task | |
| except asyncio.CancelledError: | |
| pass | |
| app.state.chat_handler = None | |
| def create_app() -> FastAPI: | |
| app = FastAPI( | |
| title="Web2API(Plugin)", | |
| description="按 type 路由的 OpenAI 兼容接口,baseUrl: http://ip:port/{type}/v1/...", | |
| lifespan=lifespan, | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=False, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| def root() -> FileResponse: | |
| return FileResponse(STATIC_DIR / "index.html") | |
| def healthz(request: Request) -> JSONResponse: | |
| return JSONResponse( | |
| { | |
| "status": "ok", | |
| "config_login_enabled": config_login_enabled(request), | |
| "login": "/login", | |
| "config": "/config", | |
| } | |
| ) | |
| app.include_router(create_router()) | |
| app.include_router(create_anthropic_router()) | |
| app.include_router(create_config_router()) | |
| return app | |
| app = create_app() | |