diff --git a/DEPLOY.md b/DEPLOY.md index 022837f..60b9b45 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -54,7 +54,7 @@ apt install -y python3 python3-venv python3-pip git curl | 组件 | 版本建议 | 用途 | |------|----------|------| -| Python | 3.10+(`python3 -V` 确认) | 运行网关 | +| Python | **3.8+**(推荐 3.10+;`python3 -V` 确认) | 运行网关 | | Node.js | LTS | 安装 PM2 | | PM2 | 最新 | 进程守护 | | 宝塔 | 7.x+ | Nginx 反代、SSL | @@ -98,6 +98,8 @@ cd /opt/openai_node python3 -m venv venv source venv/bin/activate pip install -r requirements.txt -U +# 若曾装过新版 bcrypt 导致 passlib 报错,可强制重装: +# pip install 'bcrypt>=4.0.0,<4.1.0' -U --force-reinstall ``` ### 3.3 配置文件 @@ -364,6 +366,8 @@ Web 保存节点配置会写入 `nodes.json` 并热加载;仅改 `ecosystem.co | 统计 IP 不对 | 宝塔是否传递 `X-Forwarded-For`(§5.2) | | Token 为 0 | 上游 `usage`;流式 `include_usage` | | PM2 找不到模块 | 确认使用 `/opt/openai_node/venv` 内 Python | +| `asyncio` 无 `to_thread` | Python 3.8:拉最新代码(已用 `run_in_thread` 兼容) | +| bcrypt `__about__` 报错 | `pip install 'bcrypt>=4.0.0,<4.1.0' -U --force-reinstall` 后重启 PM2 | --- diff --git a/__pycache__/main.cpython-310.pyc b/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000..0207613 Binary files /dev/null and b/__pycache__/main.cpython-310.pyc differ diff --git a/main.py b/main.py index a998ed1..68755d3 100644 --- a/main.py +++ b/main.py @@ -27,7 +27,7 @@ import threading from contextlib import asynccontextmanager from dataclasses import dataclass from datetime import datetime, timedelta, timezone -from typing import Any, AsyncIterator, Dict, List, Optional, Tuple +from typing import Any, AsyncIterator, Callable, Dict, List, Optional, Tuple, TypeVar try: from typing import Annotated # Python 3.9+ @@ -35,6 +35,16 @@ except ImportError: # Python 3.8 from typing_extensions import Annotated import httpx + +_T = TypeVar("_T") + + +async def run_in_thread(func: Callable[..., _T], /, *args: Any) -> _T: + """在线程池执行阻塞函数(兼容 Python 3.8,无 asyncio.to_thread)。""" + if hasattr(asyncio, "to_thread"): + return await run_in_thread(func, *args) # type: ignore[attr-defined] + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, func, *args) from fastapi import Depends, FastAPI, HTTPException, Query, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse @@ -634,7 +644,7 @@ async def record_api_log( usage: Dict[str, int], node_id: Optional[str] = None, ) -> None: - await asyncio.to_thread( + await run_in_thread( _record_api_log_sync, client_ip, model, @@ -1541,14 +1551,14 @@ async def api_me( @app.get("/api/models/cards") async def api_model_cards() -> List[Dict[str, Any]]: - return await asyncio.to_thread(build_model_cards) + return await run_in_thread(build_model_cards) @app.get("/api/nodes") async def api_nodes( _: Annotated[GateSessionUser, Depends(get_current_web_user)], ) -> List[Dict[str, Any]]: - return await asyncio.to_thread(list_nodes_with_status) + return await run_in_thread(list_nodes_with_status) @app.post("/api/nodes") @@ -1566,7 +1576,7 @@ async def api_nodes_create( "models": [{"id": m.id.strip(), "label": (m.label or m.id).strip()} for m in body.models], } nodes = list(_NODES.get("nodes", [])) + [node] - await asyncio.to_thread(save_nodes_config, {"nodes": nodes}) + await run_in_thread(save_nodes_config, {"nodes": nodes}) return node @@ -1589,7 +1599,7 @@ async def api_nodes_update( "max_concurrent": body.max_concurrent, "models": [{"id": m.id.strip(), "label": (m.label or m.id).strip()} for m in body.models], } - await asyncio.to_thread(save_nodes_config, {"nodes": nodes}) + await run_in_thread(save_nodes_config, {"nodes": nodes}) return nodes[idx] @@ -1601,7 +1611,7 @@ async def api_nodes_delete( nodes = [n for n in _NODES.get("nodes", []) if str(n.get("id")) != node_id] if len(nodes) == len(_NODES.get("nodes", [])): raise HTTPException(status_code=404, detail="节点不存在") - await asyncio.to_thread(save_nodes_config, {"nodes": nodes}) + await run_in_thread(save_nodes_config, {"nodes": nodes}) return JSONResponse({"ok": True}) @@ -1628,14 +1638,14 @@ async def api_nodes_test( async def api_stats_summary( _: Annotated[GateSessionUser, Depends(get_current_web_user)], ) -> Dict[str, Any]: - return await asyncio.to_thread(_query_stats_summary) + return await run_in_thread(_query_stats_summary) @app.get("/api/stats/ips") async def api_stats_ips( _: Annotated[GateSessionUser, Depends(get_current_web_user)], ) -> List[Dict[str, Any]]: - return await asyncio.to_thread(_query_stats_ips) + return await run_in_thread(_query_stats_ips) @app.get("/api/stats/billing") @@ -1643,7 +1653,7 @@ async def api_stats_billing( _: Annotated[GateSessionUser, Depends(get_current_web_user)], days: int = Query(30, ge=1, le=365), ) -> Dict[str, Any]: - return await asyncio.to_thread(_query_stats_billing, days) + return await run_in_thread(_query_stats_billing, days) @app.get("/api/stats/logs") @@ -1652,7 +1662,7 @@ async def api_stats_logs( limit: int = Query(50, ge=1, le=500), offset: int = Query(0, ge=0), ) -> Dict[str, Any]: - items, total = await asyncio.to_thread(_query_stats_logs, limit, offset) + items, total = await run_in_thread(_query_stats_logs, limit, offset) return {"items": items, "total": total, "limit": limit, "offset": offset} diff --git a/requirements.txt b/requirements.txt index 9c13b8d..8340416 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,6 @@ typing_extensions>=4.5.0 fastapi>=0.110.0 uvicorn[standard]>=0.27.0 passlib[bcrypt]>=1.7.4 -bcrypt>=4.0.0,<5.0.0 +bcrypt>=4.0.0,<4.1.0 python-jose[cryptography]>=3.3.0 httpx>=0.27.0