1414
1515import os
1616import time
17- from typing import Any , Dict , List , Optional , Literal
17+ from typing import Any , Dict , List , Literal , Optional
1818
1919import requests
2020
@@ -36,15 +36,15 @@ def _get_wechat_access_token() -> str:
3636
3737 Returns:
3838 str: The valid access token.
39-
39+
4040 Raises:
4141 ValueError: If credentials are missing or token retrieval fails.
42-
42+
4343 References:
4444 https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
4545 """
4646 global _wechat_access_token , _wechat_access_token_expires_at
47-
47+
4848 if _wechat_access_token and _wechat_access_token_expires_at > time .time ():
4949 return _wechat_access_token
5050
@@ -61,54 +61,60 @@ def _get_wechat_access_token() -> str:
6161
6262 if "access_token" in data :
6363 _wechat_access_token = data ["access_token" ]
64- _wechat_access_token_expires_at = time .time () + data .get ("expires_in" , 7200 ) - 60
64+ _wechat_access_token_expires_at = (
65+ time .time () + data .get ("expires_in" , 7200 ) - 60
66+ )
6567 logger .info ("WeChat access token refreshed." )
6668 return _wechat_access_token
6769 else :
6870 errcode = data .get ("errcode" )
6971 errmsg = data .get ("errmsg" , "Unknown error" )
7072 raise ValueError (f"Failed to get access token { errcode } : { errmsg } " )
7173
72- def _make_wechat_request (method : Literal ["GET" , "POST" ], endpoint : str , ** kwargs ) -> Dict [str , Any ]:
74+
75+ def _make_wechat_request (
76+ method : Literal ["GET" , "POST" ], endpoint : str , ** kwargs
77+ ) -> Dict [str , Any ]:
7378 r"""Makes a request to WeChat API with proper error handling.
74-
79+
7580 Args:
7681 method (Literal["GET", "POST"]): HTTP method ('GET' or 'POST').
7782 endpoint (str): API endpoint path.
7883 **kwargs: Additional arguments for requests.
79-
84+
8085 Returns:
8186 Dict[str, Any]: API response data.
82-
87+
8388 Raises:
8489 requests.exceptions.RequestException: If request fails.
8590 ValueError: If API returns an error.
8691 """
8792 global _wechat_access_token , _wechat_access_token_expires_at
8893 access_token = _get_wechat_access_token ()
89-
94+
9095 # Handle URL parameter concatenation
9196 separator = "&" if "?" in endpoint else "?"
9297 url = (
9398 f"https://api.weixin.qq.com{ endpoint } { separator } "
9499 f"access_token={ access_token } "
95100 )
96-
101+
97102 if method .upper () == "GET" :
98103 response = requests .get (url , ** kwargs )
99104 else :
100105 response = requests .post (url , ** kwargs )
101-
106+
102107 response .raise_for_status ()
103108 data = response .json ()
104-
109+
105110 if data .get ("errcode" ) and data .get ("errcode" ) != 0 :
106111 errcode = data .get ("errcode" )
107112 errmsg = data .get ("errmsg" , "Unknown error" )
108113 raise ValueError (f"WeChat API error { errcode } : { errmsg } " )
109-
114+
110115 return data
111116
117+
112118@MCPServer ()
113119class WeChatOfficialToolkit (BaseToolkit ):
114120 r"""A toolkit for WeChat Official Account operations.
@@ -128,21 +134,25 @@ def __init__(self, timeout: Optional[float] = None):
128134 r"""Initializes the WeChatOfficialToolkit."""
129135 super ().__init__ (timeout = timeout )
130136 self .base_url = "https://api.weixin.qq.com"
131-
137+
132138 # Validate credentials
133139 app_id = os .environ .get ("WECHAT_APP_ID" , "" )
134140 app_secret = os .environ .get ("WECHAT_APP_SECRET" , "" )
135-
141+
136142 if not all ([app_id , app_secret ]):
137143 raise ValueError (
138- "WeChat credentials missing. Set WECHAT_APP_ID and WECHAT_APP_SECRET."
144+ "WeChat credentials missing. Set WECHAT_APP_ID and"
145+ " WECHAT_APP_SECRET."
139146 )
140-
147+
141148 # Define full logic as class methods; top-level functions delegate here
142- @api_keys_required ([
143- (None , "WECHAT_APP_ID" ),
144- (None , "WECHAT_APP_SECRET" ),
145- ])
149+
150+ @api_keys_required (
151+ [
152+ (None , "WECHAT_APP_ID" ),
153+ (None , "WECHAT_APP_SECRET" ),
154+ ]
155+ )
146156 def send_customer_message (
147157 self ,
148158 openid : str ,
@@ -190,10 +200,12 @@ def send_customer_message(
190200 )
191201 return f"Message sent successfully to { openid } ."
192202
193- @api_keys_required ([
194- (None , "WECHAT_APP_ID" ),
195- (None , "WECHAT_APP_SECRET" ),
196- ])
203+ @api_keys_required (
204+ [
205+ (None , "WECHAT_APP_ID" ),
206+ (None , "WECHAT_APP_SECRET" ),
207+ ]
208+ )
197209 def get_user_info (
198210 self ,
199211 openid : str ,
@@ -203,23 +215,28 @@ def get_user_info(
203215
204216 Args:
205217 openid (str): The user's OpenID.
206- lang (str): Response language. Common values: "zh_CN", "zh_TW", "en". (default: "zh_CN")
218+ lang (str): Response language. Common values: "zh_CN", "zh_TW",
219+ "en". (default: "zh_CN")
207220
208221 Returns:
209- Dict[str, Any]: User information as dictionary or error information.
222+ Dict[str, Any]: User information as dictionary or error
223+ information.
210224
211225 References:
212- https://developers.weixin.qq.com/doc/offiaccount/User_Management/Getting_user_basic_information.html
226+ https://developers.weixin.qq.com/doc/offiaccount/User_Management/
227+ Getting_user_basic_information.html
213228 """
214229 data = _make_wechat_request (
215230 "GET" , f"/cgi-bin/user/info?openid={ openid } &lang={ lang } "
216231 )
217232 return data
218233
219- @api_keys_required ([
220- (None , "WECHAT_APP_ID" ),
221- (None , "WECHAT_APP_SECRET" ),
222- ])
234+ @api_keys_required (
235+ [
236+ (None , "WECHAT_APP_ID" ),
237+ (None , "WECHAT_APP_SECRET" ),
238+ ]
239+ )
223240 def get_followers_list (
224241 self ,
225242 next_openid : str = "" ,
@@ -233,18 +250,21 @@ def get_followers_list(
233250 Dict[str, Any]: Followers list as dictionary or error information.
234251
235252 References:
236- https://developers.weixin.qq.com/doc/offiaccount/User_Management/Getting_a_list_of_followers.html
253+ https://developers.weixin.qq.com/doc/offiaccount/User_Management/
254+ Getting_a_list_of_followers.html
237255 """
238256 endpoint = "/cgi-bin/user/get"
239257 if next_openid :
240258 endpoint += f"?next_openid={ next_openid } "
241259 data = _make_wechat_request ("GET" , endpoint )
242260 return data
243261
244- @api_keys_required ([
245- (None , "WECHAT_APP_ID" ),
246- (None , "WECHAT_APP_SECRET" ),
247- ])
262+ @api_keys_required (
263+ [
264+ (None , "WECHAT_APP_ID" ),
265+ (None , "WECHAT_APP_SECRET" ),
266+ ]
267+ )
248268 def upload_wechat_media (
249269 self ,
250270 media_type : Literal [
@@ -262,23 +282,27 @@ def upload_wechat_media(
262282 Args:
263283 media_type (str): Media type: "image", "voice", "video", "thumb".
264284 file_path (str): Local file path.
265- permanent (bool): Whether to upload as permanent media. (default: False)
266- description (Optional[str]): Video description in JSON format for permanent upload.
285+ permanent (bool): Whether to upload as permanent media.
286+ (default: :obj:`False`)
287+ description (Optional[str]): Video description in JSON format
288+ for permanent upload. (default: :obj:`None`)
267289
268290 Returns:
269291 Dict[str, Any]: Upload result with media_id or error information.
270292
271293 References:
272- - Temporary: https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Adding_Temporary_Assets.html
273- - Permanent: https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Adding_Permanent_Assets.html
294+ - Temporary: https://developers.weixin.qq.com/doc/offiaccount/
295+ Asset_Management/Adding_Temporary_Assets.html
296+ - Permanent: https://developers.weixin.qq.com/doc/offiaccount/
297+ Asset_Management/Adding_Permanent_Assets.html
274298 """
275299 if permanent :
276300 endpoint = f"/cgi-bin/material/add_material?type={ media_type } "
277301 data_payload = {}
278302 if media_type == "video" and description :
279303 data_payload ["description" ] = description
280304 with open (file_path , "rb" ) as media_file :
281- files = {"media" : media_file }
305+ files : Dict [ str , Any ] = {"media" : media_file }
282306 if media_type == "video" and description :
283307 files ["description" ] = (None , description )
284308 data = _make_wechat_request (
@@ -292,10 +316,12 @@ def upload_wechat_media(
292316
293317 return data
294318
295- @api_keys_required ([
296- (None , "WECHAT_APP_ID" ),
297- (None , "WECHAT_APP_SECRET" ),
298- ])
319+ @api_keys_required (
320+ [
321+ (None , "WECHAT_APP_ID" ),
322+ (None , "WECHAT_APP_SECRET" ),
323+ ]
324+ )
299325 def get_media_list (
300326 self ,
301327 media_type : Literal [
@@ -311,14 +337,15 @@ def get_media_list(
311337
312338 Args:
313339 media_type (str): Media type: "image", "voice", "video", "news".
314- offset (int): Starting position. (default: 0 )
315- count (int): Number of items (1-20). (default: 20 )
340+ offset (int): Starting position. (default: :obj:`0` )
341+ count (int): Number of items (1-20). (default: :obj:`20` )
316342
317343 Returns:
318344 Dict[str, Any]: Media list as dictionary or error information.
319345
320346 References:
321- https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Get_the_list_of_all_materials.html
347+ https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/
348+ Get_the_list_of_all_materials.html
322349 """
323350 payload = {"type" : media_type , "offset" : offset , "count" : count }
324351 data = _make_wechat_request (
@@ -329,10 +356,12 @@ def get_media_list(
329356 )
330357 return data
331358
332- @api_keys_required ([
333- (None , "WECHAT_APP_ID" ),
334- (None , "WECHAT_APP_SECRET" ),
335- ])
359+ @api_keys_required (
360+ [
361+ (None , "WECHAT_APP_ID" ),
362+ (None , "WECHAT_APP_SECRET" ),
363+ ]
364+ )
336365 def send_mass_message_to_all (
337366 self ,
338367 content : str ,
@@ -371,7 +400,8 @@ def send_mass_message_to_all(
371400
372401 References:
373402 - Mass send by OpenID list:
374- https://developers.weixin.qq.com/doc/service/api/notify/message/api_masssend.html
403+ https://developers.weixin.qq.com/doc/service/api/notify/message/
404+ api_masssend.html
375405 """
376406 # 1) Collect all follower OpenIDs
377407 all_openids : List [str ] = []
@@ -382,10 +412,16 @@ def send_mass_message_to_all(
382412 endpoint += f"?next_openid={ next_openid } "
383413 page = _make_wechat_request ("GET" , endpoint )
384414 data_block = page .get ("data" , {}) if isinstance (page , dict ) else {}
385- openids = data_block .get ("openid" , []) if isinstance (data_block , dict ) else []
415+ openids = (
416+ data_block .get ("openid" , [])
417+ if isinstance (data_block , dict )
418+ else []
419+ )
386420 if openids :
387421 all_openids .extend (openids )
388- next_openid = page .get ("next_openid" , "" ) if isinstance (page , dict ) else ""
422+ next_openid = (
423+ page .get ("next_openid" , "" ) if isinstance (page , dict ) else ""
424+ )
389425 if not next_openid :
390426 break
391427
@@ -434,6 +470,7 @@ def build_payload(openid_batch: List[str]) -> Dict[str, Any]:
434470 "batches" : (len (all_openids ) + batch_size - 1 ) // batch_size ,
435471 "results" : results ,
436472 }
473+
437474 def get_tools (self ) -> List [FunctionTool ]:
438475 r"""Returns toolkit functions as tools."""
439476 return [
@@ -443,4 +480,4 @@ def get_tools(self) -> List[FunctionTool]:
443480 FunctionTool (self .upload_wechat_media ),
444481 FunctionTool (self .get_media_list ),
445482 FunctionTool (self .send_mass_message_to_all ),
446- ]
483+ ]
0 commit comments