4747QUICK_DISCONNECT_THRESHOLD = 5
4848MAX_QUICK_DISCONNECT_COUNT = 3
4949
50- API_BASE = "https://api.sgroup.qq.com"
50+ DEFAULT_API_BASE = "https://api.sgroup.qq.com"
5151TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken"
5252
53+
54+ def _get_api_base () -> str :
55+ """API 根地址,可通过 QQ_API_BASE 覆盖(如沙箱: https://sandbox.api.sgroup.qq.com)。"""
56+ return os .getenv ("QQ_API_BASE" , DEFAULT_API_BASE ).rstrip ("/" )
57+
58+
5359_token_cache : Optional [Dict [str , Any ]] = None
5460_token_lock = threading .Lock ()
5561
@@ -81,6 +87,8 @@ def _get_access_token_sync(app_id: str, client_secret: str) -> str:
8187 if not token :
8288 raise RuntimeError (f"No access_token in response: { data } " )
8389 expires_in = data .get ("expires_in" , 7200 )
90+ if isinstance (expires_in , str ):
91+ expires_in = int (expires_in )
8492 with _token_lock :
8593 _token_cache = {
8694 "token" : token ,
@@ -96,20 +104,31 @@ def clear_token_cache() -> None:
96104
97105
98106def _get_gateway_url_sync (access_token : str ) -> str :
99- try :
100- import urllib .request
107+ import urllib . error
108+ import urllib .request
101109
102- url = f"{ API_BASE } /gateway"
103- req = urllib .request .Request (
104- url ,
105- headers = {
106- "Authorization" : f"QQBot { access_token } " ,
107- "Content-Type" : "application/json" ,
108- },
109- method = "GET" ,
110- )
110+ url = f"{ _get_api_base ()} /gateway"
111+ req = urllib .request .Request (
112+ url ,
113+ headers = {
114+ "Authorization" : f"QQBot { access_token } " ,
115+ "Content-Type" : "application/json" ,
116+ },
117+ method = "GET" ,
118+ )
119+ try :
111120 with urllib .request .urlopen (req , timeout = 15 ) as resp :
112121 data = json .loads (resp .read ().decode ())
122+ except urllib .error .HTTPError as e :
123+ body = ""
124+ try :
125+ body = e .read ().decode () if e .fp else ""
126+ except Exception :
127+ pass
128+ msg = f"HTTP { e .code } : { e .reason } "
129+ if body :
130+ msg += f" | body: { body [:500 ]} "
131+ raise RuntimeError (f"Failed to get gateway url: { msg } " ) from e
113132 except Exception as e :
114133 raise RuntimeError (f"Failed to get gateway url: { e } " ) from e
115134 gateway_url = data .get ("url" )
@@ -126,7 +145,7 @@ def _api_request_sync(
126145) -> Dict [str , Any ]:
127146 import urllib .request
128147
129- url = f"{ API_BASE } { path } "
148+ url = f"{ _get_api_base () } { path } "
130149 data = None
131150 if body is not None :
132151 data = json .dumps (body ).encode ()
@@ -179,6 +198,8 @@ async def _get_access_token_async(app_id: str, client_secret: str) -> str:
179198 if not token :
180199 raise RuntimeError (f"No access_token: { data } " )
181200 expires_in = data .get ("expires_in" , 7200 )
201+ if isinstance (expires_in , str ):
202+ expires_in = int (expires_in )
182203 with _token_lock :
183204 _token_cache = {
184205 "token" : token ,
@@ -193,7 +214,7 @@ async def _api_request_async(
193214 path : str ,
194215 body : Optional [Dict [str , Any ]] = None ,
195216) -> Dict [str , Any ]:
196- url = f"{ API_BASE } { path } "
217+ url = f"{ _get_api_base () } { path } "
197218 async with aiohttp .ClientSession () as session :
198219 kwargs = {
199220 "headers" : {
0 commit comments