修复bug
This commit is contained in:
@@ -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 |
|
| Node.js | LTS | 安装 PM2 |
|
||||||
| PM2 | 最新 | 进程守护 |
|
| PM2 | 最新 | 进程守护 |
|
||||||
| 宝塔 | 7.x+ | Nginx 反代、SSL |
|
| 宝塔 | 7.x+ | Nginx 反代、SSL |
|
||||||
@@ -98,6 +98,8 @@ cd /opt/openai_node
|
|||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
pip install -r requirements.txt -U
|
pip install -r requirements.txt -U
|
||||||
|
# 若曾装过新版 bcrypt 导致 passlib 报错,可强制重装:
|
||||||
|
# pip install 'bcrypt>=4.0.0,<4.1.0' -U --force-reinstall
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.3 配置文件
|
### 3.3 配置文件
|
||||||
@@ -364,6 +366,8 @@ Web 保存节点配置会写入 `nodes.json` 并热加载;仅改 `ecosystem.co
|
|||||||
| 统计 IP 不对 | 宝塔是否传递 `X-Forwarded-For`(§5.2) |
|
| 统计 IP 不对 | 宝塔是否传递 `X-Forwarded-For`(§5.2) |
|
||||||
| Token 为 0 | 上游 `usage`;流式 `include_usage` |
|
| Token 为 0 | 上游 `usage`;流式 `include_usage` |
|
||||||
| PM2 找不到模块 | 确认使用 `/opt/openai_node/venv` 内 Python |
|
| 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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -27,7 +27,7 @@ import threading
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta, timezone
|
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:
|
try:
|
||||||
from typing import Annotated # Python 3.9+
|
from typing import Annotated # Python 3.9+
|
||||||
@@ -35,6 +35,16 @@ except ImportError: # Python 3.8
|
|||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
import httpx
|
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 import Depends, FastAPI, HTTPException, Query, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
|
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
|
||||||
@@ -634,7 +644,7 @@ async def record_api_log(
|
|||||||
usage: Dict[str, int],
|
usage: Dict[str, int],
|
||||||
node_id: Optional[str] = None,
|
node_id: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
await asyncio.to_thread(
|
await run_in_thread(
|
||||||
_record_api_log_sync,
|
_record_api_log_sync,
|
||||||
client_ip,
|
client_ip,
|
||||||
model,
|
model,
|
||||||
@@ -1541,14 +1551,14 @@ async def api_me(
|
|||||||
|
|
||||||
@app.get("/api/models/cards")
|
@app.get("/api/models/cards")
|
||||||
async def api_model_cards() -> List[Dict[str, Any]]:
|
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")
|
@app.get("/api/nodes")
|
||||||
async def api_nodes(
|
async def api_nodes(
|
||||||
_: Annotated[GateSessionUser, Depends(get_current_web_user)],
|
_: Annotated[GateSessionUser, Depends(get_current_web_user)],
|
||||||
) -> List[Dict[str, Any]]:
|
) -> 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")
|
@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],
|
"models": [{"id": m.id.strip(), "label": (m.label or m.id).strip()} for m in body.models],
|
||||||
}
|
}
|
||||||
nodes = list(_NODES.get("nodes", [])) + [node]
|
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
|
return node
|
||||||
|
|
||||||
|
|
||||||
@@ -1589,7 +1599,7 @@ async def api_nodes_update(
|
|||||||
"max_concurrent": body.max_concurrent,
|
"max_concurrent": body.max_concurrent,
|
||||||
"models": [{"id": m.id.strip(), "label": (m.label or m.id).strip()} for m in body.models],
|
"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]
|
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]
|
nodes = [n for n in _NODES.get("nodes", []) if str(n.get("id")) != node_id]
|
||||||
if len(nodes) == len(_NODES.get("nodes", [])):
|
if len(nodes) == len(_NODES.get("nodes", [])):
|
||||||
raise HTTPException(status_code=404, detail="节点不存在")
|
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})
|
return JSONResponse({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
@@ -1628,14 +1638,14 @@ async def api_nodes_test(
|
|||||||
async def api_stats_summary(
|
async def api_stats_summary(
|
||||||
_: Annotated[GateSessionUser, Depends(get_current_web_user)],
|
_: Annotated[GateSessionUser, Depends(get_current_web_user)],
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
return await asyncio.to_thread(_query_stats_summary)
|
return await run_in_thread(_query_stats_summary)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/stats/ips")
|
@app.get("/api/stats/ips")
|
||||||
async def api_stats_ips(
|
async def api_stats_ips(
|
||||||
_: Annotated[GateSessionUser, Depends(get_current_web_user)],
|
_: Annotated[GateSessionUser, Depends(get_current_web_user)],
|
||||||
) -> List[Dict[str, Any]]:
|
) -> 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")
|
@app.get("/api/stats/billing")
|
||||||
@@ -1643,7 +1653,7 @@ async def api_stats_billing(
|
|||||||
_: Annotated[GateSessionUser, Depends(get_current_web_user)],
|
_: Annotated[GateSessionUser, Depends(get_current_web_user)],
|
||||||
days: int = Query(30, ge=1, le=365),
|
days: int = Query(30, ge=1, le=365),
|
||||||
) -> Dict[str, Any]:
|
) -> 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")
|
@app.get("/api/stats/logs")
|
||||||
@@ -1652,7 +1662,7 @@ async def api_stats_logs(
|
|||||||
limit: int = Query(50, ge=1, le=500),
|
limit: int = Query(50, ge=1, le=500),
|
||||||
offset: int = Query(0, ge=0),
|
offset: int = Query(0, ge=0),
|
||||||
) -> Dict[str, Any]:
|
) -> 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}
|
return {"items": items, "total": total, "limit": limit, "offset": offset}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,6 +2,6 @@ typing_extensions>=4.5.0
|
|||||||
fastapi>=0.110.0
|
fastapi>=0.110.0
|
||||||
uvicorn[standard]>=0.27.0
|
uvicorn[standard]>=0.27.0
|
||||||
passlib[bcrypt]>=1.7.4
|
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
|
python-jose[cryptography]>=3.3.0
|
||||||
httpx>=0.27.0
|
httpx>=0.27.0
|
||||||
|
|||||||
Reference in New Issue
Block a user