noke / noke.py
i3t2y
feat: noke v0.3.0 — 5-batch architecture upgrade
5039d41
Raw
History Blame Contribute Delete
3.4 kB
#!/usr/bin/env python3
import os
import logging
import asyncio
from pathlib import Path
from contextlib import asynccontextmanager
import uvicorn
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, RedirectResponse
from core.config import ConfigManager
from core.token_manager import TokenManager
from core.log_store import LogStore
from core.conversation_cache import ConversationCache
from core.ratelimit import RateLimiter
from routes import openai_compat, admin_api, claude_api, system_api, mcp_api
logging.basicConfig(
level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger("noke")
cfg = ConfigManager()
token_manager = TokenManager(cfg)
log_store = LogStore(max_entries=cfg.get("logging", "max_entries", default=500))
# 会话缓存 & 限流器
conv_cache = ConversationCache(max_size=2000, ttl=1800)
limiter = RateLimiter(
qps=cfg.get("proxy", "rate_limit_qps", default=0),
burst=cfg.get("proxy", "rate_limit_burst", default=0),
)
# 初始化各路由模块
openai_compat.init(token_manager, cfg, log_store, conv_cache, limiter)
admin_api.init(cfg, token_manager, log_store)
claude_api.init(token_manager, cfg, log_store, conv_cache, limiter)
system_api.init(token_manager, cfg, conv_cache, limiter)
mcp_api.init(token_manager, cfg)
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info(
"Noke started — tokens: %d, port: %d",
len(cfg.get("tokens", default=[])),
int(os.environ.get("PORT", cfg.get("server", "port", default=7860))),
)
# 后台定时 flush token 统计(每 60 秒,标脏才写盘)
async def _periodic_flush():
while True:
await asyncio.sleep(60)
token_manager.flush()
flush_task = asyncio.create_task(_periodic_flush())
yield
flush_task.cancel()
try:
await flush_task
except asyncio.CancelledError:
pass
await token_manager.close_all()
app = FastAPI(lifespan=lifespan)
# 全局异常处理:脱敏
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
import traceback
error_detail = f"{type(exc).__name__}: {str(exc)}\n{traceback.format_exc()}"
logger.error("Unhandled exception: %s", error_detail)
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=500,
content={"detail": "internal server error"}
)
app.include_router(claude_api.router)
app.include_router(openai_compat.router)
app.include_router(admin_api.router)
app.include_router(system_api.router)
app.include_router(mcp_api.router)
static_dir = Path(__file__).parent / "static"
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
@app.get("/")
async def root():
return RedirectResponse(url="/admin")
# health 现在由 system_api 提供,保留旧路径兼容
@app.get("/health")
async def health_legacy():
return {"status": "ok"}
@app.get("/admin")
async def admin_page():
return FileResponse(str(static_dir / "index.html"))
if __name__ == "__main__":
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
port = int(os.environ.get("PORT", 7860))
uvicorn.run(
app,
host=cfg.get("server", "host", default="0.0.0.0"),
port=port,
)