File size: 5,624 Bytes
fa1140b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | """FastAPI 入口:组装上游 provider + 挂载 OpenAI / Anthropic 兼容路由。"""
from __future__ import annotations
import asyncio
import logging
from contextlib import asynccontextmanager
from pathlib import Path
import httpx
from fastapi import FastAPI
from app.account import AccountPool, FailReason, set_cooldown_policy
from app.adapters.anthropic_messages import router as anthropic_router
from app.adapters.openai_chat import router as openai_chat_router
from app.adapters.openai_images import router as openai_images_router
from app.adapters.openai_models import router as openai_models_router
from app.adapters.openai_responses import router as openai_responses_router
from app.admin import router as admin_router
from app.config import get_settings
from app.http_log import RequestResponseLogMiddleware
from app.logging_setup import setup_logging
from app.upstream import get_provider
logger = logging.getLogger(__name__)
async def _watch_pool(app: FastAPI, interval: float) -> None:
"""后台任务:周期扫描 account/*.json,把注册机/手动新写的账号热加载进池。
2026-08-08:注册机(独立进程)写盘新账号时网关不知情,之前要手动 /admin/reload
或重启才感知。本任务定期 reload:新号进池 + 建 provider;已删号(余额<=0 删除)
同步移除。与 /admin/reload 等价,但自动。
"""
while True:
await asyncio.sleep(interval)
try:
st = app.state
pool: AccountPool = st.pool
providers: dict[str, object] = st.providers
before = {a.name for a in pool.all()}
pool.reload()
after = {a.name for a in pool.all()}
new = after - before
removed = before - after
changed = False
for name in sorted(new):
acc = next(a for a in pool.all() if a.name == name)
providers[name] = get_provider(acc, st.settings, st.http_client)
changed = True
for name in sorted(removed):
providers.pop(name, None)
changed = True
if changed:
logger.info("pool hot-reload: +%d new, -%d removed (total=%d)",
len(new), len(removed), len(after))
except Exception: # noqa: BLE001
logger.warning("pool hot-reload failed", exc_info=True)
@asynccontextmanager
async def lifespan(app: FastAPI):
settings = get_settings()
setup_logging(settings)
# 同步可恢复失效的处理策略与冷却时长
seconds_map: dict[FailReason, float] = {}
if settings.cooldown_seconds_quota is not None:
seconds_map[FailReason.QUOTA_EXHAUSTED] = settings.cooldown_seconds_quota
if settings.cooldown_seconds_cf is not None:
seconds_map[FailReason.CF_CHALLENGE] = settings.cooldown_seconds_cf
set_cooldown_policy(
settings.quota_exhausted_action,
seconds=settings.cooldown_seconds,
seconds_map=seconds_map,
)
# proxy 未配置时 httpx 直连(proxy=None)
http_client = httpx.AsyncClient(
timeout=settings.request_timeout,
proxy=settings.effective_proxy(),
)
# 加载账号池;为每个账号建独立的 UpstreamProvider(共享 http_client)
pool = AccountPool.load(Path(settings.account_dir))
providers: dict[str, object] = {}
for acc in pool.all():
providers[acc.name] = get_provider(acc, settings, http_client)
app.state.settings = settings
app.state.http_client = http_client
app.state.pool = pool
app.state.providers = providers
# 面板生成的 API Key 库(多 key,文件存储;config 的 gateway_api_key 兼容并存)
from app.api_keys import load_keys
app.state.api_keys = load_keys(Path(settings.log_dir))
# token 用量存储(SQLite,logs/usage.db;面板 /admin/usage 聚合查询)
try:
from app import usage_store
usage_store.init(Path(settings.log_dir) / "usage.db")
except Exception: # noqa: BLE001
logger.warning("usage_store init failed (usage 统计将不可用)", exc_info=True)
n_all = len(pool.all())
n_ok = pool.available_count() if hasattr(pool, "available_count") else n_all
logger.info(
"startup accounts=%d available=%d proxy=%s log_file=%s timeout=%.0fs",
n_all,
n_ok,
settings.effective_proxy() or "(direct)",
f"{settings.log_dir}/{settings.log_filename}" if settings.log_enabled else "(disabled)",
settings.request_timeout,
)
# 账号池热加载后台任务(注册机写盘的新号自动进池,不用重启/手动 reload)
watch_task: asyncio.Task | None = None
watch_interval = float(getattr(settings, "pool_watch_interval", 0.0) or 0.0)
if watch_interval > 0:
watch_task = asyncio.create_task(_watch_pool(app, watch_interval))
logger.info("pool hot-reload enabled (every %.0fs)", watch_interval)
try:
yield
finally:
logger.info("shutdown")
if watch_task is not None:
watch_task.cancel()
await http_client.aclose()
app = FastAPI(title="anuma2api", version="0.1.0", lifespan=lifespan)
app.add_middleware(RequestResponseLogMiddleware)
app.include_router(openai_models_router)
app.include_router(openai_chat_router)
app.include_router(openai_responses_router)
app.include_router(openai_images_router)
app.include_router(anthropic_router)
app.include_router(admin_router)
@app.get("/healthz")
async def healthz() -> dict[str, str]:
return {"status": "ok"}
|