Skip to content

Commit 5eba9e7

Browse files
fix(sing): 修复带歌名唱歌无声卡死
HTTPXClient 仅在创建/重置 client 时持锁,避免单个请求拖死 worker; 网易云搜歌与登录状态检查增加 15s 超时,失败走现有兜底文案。 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e2e568c commit 5eba9e7

2 files changed

Lines changed: 49 additions & 18 deletions

File tree

src/plugins/sing/ncm_login/__init__.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import re
23

34
import httpx
@@ -27,6 +28,7 @@ class NCMLoginConfig(BaseModel, extra="ignore"):
2728

2829

2930
ncm_cfg = NCMLoginConfig()
31+
NCM_SEARCH_TIMEOUT = 15.0
3032

3133
ncm_login_cmd = on_command("网易云登录", priority=10, block=True, permission=permission_for_command("sing.ncm_login"))
3234
ncm_logout_cmd = on_command("网易云登出", priority=10, block=True, permission=permission_for_command("sing.ncm_logout"))
@@ -138,14 +140,32 @@ async def get_song_id(song_name: str):
138140
if song_name.isdigit():
139141
return song_name
140142

141-
res = await ncm.cloudsearch.GetSearchResult(song_name, 1, 10)
143+
try:
144+
res = await asyncio.wait_for(
145+
ncm.cloudsearch.GetSearchResult(song_name, 1, 10),
146+
timeout=NCM_SEARCH_TIMEOUT,
147+
)
148+
except TimeoutError:
149+
logger.warning(f"ncm cloudsearch timeout for song {song_name!r}")
150+
return None
151+
except Exception as e:
152+
logger.warning(f"ncm cloudsearch failed for song {song_name!r}: {e}")
153+
return None
154+
142155
if "result" not in res or "songCount" not in res["result"]:
143156
return None
144157

145158
if res["result"]["songCount"] == 0:
146159
return None
147160

148-
logged_in = await is_ncm_logged_in()
161+
try:
162+
logged_in = await asyncio.wait_for(is_ncm_logged_in(), timeout=NCM_SEARCH_TIMEOUT)
163+
except TimeoutError:
164+
logger.warning(f"ncm login status check timeout for song {song_name!r}")
165+
return None
166+
except Exception as e:
167+
logger.warning(f"ncm login status check failed for song {song_name!r}: {e}")
168+
return None
149169

150170
for song in res["result"]["songs"]:
151171
# 如果未登录,跳过vip

src/shared/utils/__init__.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import asyncio
2-
from contextlib import asynccontextmanager
32
from typing import Any
43

54
import httpx
@@ -40,23 +39,20 @@ class HTTPXClient:
4039
}
4140

4241
@classmethod
43-
@asynccontextmanager
44-
async def get_client(cls):
42+
async def _ensure_client(cls) -> httpx.AsyncClient:
4543
async with cls._lock:
4644
if cls._client is None or cls._client.is_closed:
4745
cls._client = httpx.AsyncClient(
4846
timeout=cls.DEFAULT_TIMEOUT,
4947
)
50-
try:
51-
yield cls._client
52-
except httpx.TransportError as e:
53-
logger.error(f"httpx client transport error: {e}")
54-
await cls.close()
55-
cls._client = None
56-
raise
57-
except httpx.HTTPError as e:
58-
logger.warning(f"httpx client HTTP error: {e}")
59-
raise
48+
return cls._client
49+
50+
@classmethod
51+
async def _reset_client(cls) -> None:
52+
async with cls._lock:
53+
if cls._client and not cls._client.is_closed:
54+
await cls._client.aclose()
55+
cls._client = None
6056

6157
@classmethod
6258
async def close(cls):
@@ -76,10 +72,15 @@ def configure_defaults(cls, timeout: float = DEFAULT_TIMEOUT, retry_config: dict
7672
async def get(cls, url: str, **kwargs) -> httpx.Response | None:
7773
@retry(**cls.DEFAULT_RETRY)
7874
async def _get():
79-
async with cls.get_client() as client:
75+
client = await cls._ensure_client()
76+
try:
8077
response = await client.get(url, **kwargs)
8178
response.raise_for_status()
8279
return response
80+
except httpx.TransportError as e:
81+
logger.error(f"httpx client transport error: {e}")
82+
await cls._reset_client()
83+
raise
8384

8485
try:
8586
return await _get()
@@ -91,10 +92,15 @@ async def _get():
9192
async def post(cls, url: str, json: dict[str, Any] | None = None, **kwargs) -> httpx.Response | None:
9293
@retry(**cls.DEFAULT_RETRY)
9394
async def _post():
94-
async with cls.get_client() as client:
95+
client = await cls._ensure_client()
96+
try:
9597
response = await client.post(url, json=json, **kwargs)
9698
response.raise_for_status()
9799
return response
100+
except httpx.TransportError as e:
101+
logger.error(f"httpx client transport error: {e}")
102+
await cls._reset_client()
103+
raise
98104

99105
try:
100106
return await _post()
@@ -106,10 +112,15 @@ async def _post():
106112
async def delete(cls, url: str, **kwargs) -> httpx.Response | None:
107113
@retry(**cls.DEFAULT_RETRY)
108114
async def _delete():
109-
async with cls.get_client() as client:
115+
client = await cls._ensure_client()
116+
try:
110117
response = await client.delete(url, **kwargs)
111118
response.raise_for_status()
112119
return response
120+
except httpx.TransportError as e:
121+
logger.error(f"httpx client transport error: {e}")
122+
await cls._reset_client()
123+
raise
113124

114125
try:
115126
return await _delete()

0 commit comments

Comments
 (0)