Skip to content

Commit ca4b5a5

Browse files
authored
enhance: Integrate Wechat PR3087 (#3106)
1 parent 678d9df commit ca4b5a5

File tree

3 files changed

+171
-98
lines changed

3 files changed

+171
-98
lines changed

camel/toolkits/wechat_official_toolkit.py

Lines changed: 95 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import os
1616
import time
17-
from typing import Any, Dict, List, Optional, Literal
17+
from typing import Any, Dict, List, Literal, Optional
1818

1919
import 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()
113119
class 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

Comments
 (0)