Skip to content

Commit dbc7944

Browse files
committed
refactor: Enhance Wecom adapter with API delegation pattern
- Introduce WechatApiDelegate interface for handling different WeChat API types - Implement CorpWechatApiDelegate and PublicWechatApiDelegate for enterprise and public APIs respectively - Refactor WecomAdapter to utilize the new delegation structure for improved maintainability - Update message handling methods to leverage the new API delegates - Ensure proper setup and signature verification for both API types
1 parent 1b59515 commit dbc7944

File tree

3 files changed

+151
-36
lines changed

3 files changed

+151
-36
lines changed

kirara_ai/plugins/im_wecom_adapter/__init__.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import os
22

3-
from im_wecom_adapter.adapter import WecomAdapter, WecomConfig
4-
53
from kirara_ai.logger import get_logger
64
from kirara_ai.plugin_manager.plugin import Plugin
75
from kirara_ai.web.app import WebServer
86

7+
from .adapter import WecomAdapter, WecomConfig
8+
99
logger = get_logger("Wecom-Adapter")
1010

11+
__all__ = ["WecomAdapter", "WecomConfig"]
1112

1213
class WecomAdapterPlugin(Plugin):
1314
web_server: WebServer

kirara_ai/plugins/im_wecom_adapter/adapter.py

+31-34
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import aiohttp
99
from fastapi import FastAPI, HTTPException, Request, Response
1010
from pydantic import BaseModel, ConfigDict, Field
11+
from wechatpy.client import BaseWeChatClient
1112
from wechatpy.exceptions import InvalidSignatureException
1213
from wechatpy.replies import create_reply
1314

@@ -18,6 +19,8 @@
1819
from kirara_ai.web.app import WebServer
1920
from kirara_ai.workflow.core.dispatch.dispatcher import WorkflowDispatcher
2021

22+
from .delegates import CorpWechatApiDelegate, PublicWechatApiDelegate
23+
2124
WECOM_TEMP_DIR = os.path.join(os.getcwd(), 'data', 'temp', 'wecom')
2225

2326
WEBHOOK_URL_PREFIX = "/im/webhook/wechat"
@@ -68,9 +71,13 @@ def __init__(self, **kwargs: Any):
6871
class WeComUtils:
6972
"""企业微信相关的工具类"""
7073

71-
def __init__(self, access_token: str):
72-
self.access_token = access_token
74+
def __init__(self, client: BaseWeChatClient):
75+
self.client = client
7376
self.logger = get_logger("WeComUtils")
77+
78+
@property
79+
def access_token(self) -> str:
80+
return self.client.access_token
7481

7582
async def download_and_save_media(self, media_id: str, file_name: str) -> Optional[str]:
7683
"""下载并保存媒体文件到本地"""
@@ -114,7 +121,6 @@ def __init__(self, config: WecomConfig):
114121
else:
115122
self.app = self.web_server.app
116123

117-
self.setup_wechat_api()
118124
self.logger = get_logger("Wecom-Adapter")
119125
self.is_running = False
120126
if not self.config.host:
@@ -127,25 +133,20 @@ def __init__(self, config: WecomConfig):
127133

128134
self.reply_tasks = {}
129135

136+
# 根据配置选择合适的API代理
137+
self.setup_wechat_api()
138+
130139
def setup_wechat_api(self):
140+
"""根据配置设置微信API代理"""
131141
if self.config.corp_id:
132-
from wechatpy.enterprise import parse_message
133-
from wechatpy.enterprise.client import WeChatClient
134-
from wechatpy.enterprise.crypto import WeChatCrypto
135-
self.crypto = WeChatCrypto(
136-
self.config.token, self.config.encoding_aes_key, self.config.corp_id
137-
)
138-
self.client = WeChatClient(self.config.corp_id, self.config.secret)
139-
self.parse_message = parse_message
142+
self.api_delegate = CorpWechatApiDelegate()
140143
else:
141-
from wechatpy import WeChatClient
142-
from wechatpy.crypto import WeChatCrypto
143-
from wechatpy.parser import parse_message
144-
self.crypto = WeChatCrypto(
145-
self.config.token, self.config.encoding_aes_key, self.config.app_id
146-
)
147-
self.client = WeChatClient(self.config.app_id, self.config.secret)
148-
self.parse_message = parse_message
144+
self.api_delegate = PublicWechatApiDelegate()
145+
146+
self.api_delegate.setup_api(self.config)
147+
148+
# 设置工具类
149+
self.wecom_utils = WeComUtils(self.api_delegate.client)
149150

150151
def setup_routes(self):
151152
if self.config.host:
@@ -165,19 +166,16 @@ async def handle_check_request(request: Request):
165166
raise HTTPException(status_code=404)
166167

167168
signature = request.query_params.get("msg_signature", "")
169+
if not signature:
170+
signature = request.query_params.get("signature", "")
168171
timestamp = request.query_params.get("timestamp", "")
169172
nonce = request.query_params.get("nonce", "")
170173
echo_str = request.query_params.get("echostr", "")
171174

172-
173175
try:
174-
if self.config.corp_id:
175-
echo_str = self.crypto.check_signature(
176-
signature, timestamp, nonce, echo_str
177-
)
178-
else:
179-
from wechatpy.utils import check_signature
180-
check_signature(self.config.token, signature, timestamp, nonce)
176+
echo_str = self.api_delegate.check_signature(
177+
signature, timestamp, nonce, echo_str
178+
)
181179
return Response(content=echo_str, media_type="text/plain")
182180
except InvalidSignatureException:
183181
self.logger.error("failed to check signature, please check your settings.")
@@ -190,16 +188,18 @@ async def handle_message(request: Request):
190188
self.logger.warning("Wecom-Adapter is not running, skipping message request.")
191189
raise HTTPException(status_code=404)
192190
signature = request.query_params.get("msg_signature", "")
191+
if not signature:
192+
signature = request.query_params.get("signature", "")
193193
timestamp = request.query_params.get("timestamp", "")
194194
nonce = request.query_params.get("nonce", "")
195195
try:
196-
msg = self.crypto.decrypt_message(
196+
msg = self.api_delegate.decrypt_message(
197197
await request.body(), signature, timestamp, nonce
198198
)
199199
except InvalidSignatureException:
200200
self.logger.error("failed to check signature, please check your settings.")
201201
raise HTTPException(status_code=403)
202-
msg = self.parse_message(msg)
202+
msg = self.api_delegate.parse_message(msg)
203203

204204
if msg.id in self.reply_tasks:
205205
self.logger.debug(f"skip processing due to duplicate msgid: {msg.id}")
@@ -262,7 +262,7 @@ def convert_to_message(self, raw_message: Any, media_path: Optional[str] = None)
262262
async def _send_text(self, user_id: str, text: str):
263263
"""发送文本消息"""
264264
try:
265-
return self.client.message.send_text(self.config.app_id, user_id, text)
265+
return await self.api_delegate.send_text(self.config.app_id, user_id, text)
266266
except Exception as e:
267267
self.logger.error(f"Failed to send text message: {e}")
268268
raise e
@@ -271,10 +271,7 @@ async def _send_media(self, user_id: str, media_data: str, media_type: str):
271271
"""发送媒体消息的通用方法"""
272272
try:
273273
media_bytes = BytesIO(base64.b64decode(media_data))
274-
media_id = self.client.media.upload(
275-
media_type, media_bytes)["media_id"]
276-
send_method = getattr(self.client.message, f"send_{media_type}")
277-
return send_method(self.config.app, user_id, media_id)
274+
return await self.api_delegate.send_media(self.config.app_id, user_id, media_type, media_bytes)
278275
except Exception as e:
279276
self.logger.error(f"Failed to send {media_type} message: {e}")
280277
raise e
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
from abc import ABC, abstractmethod
2+
from io import BytesIO
3+
from typing import TYPE_CHECKING, Any
4+
5+
from kirara_ai.logger import get_logger
6+
7+
if TYPE_CHECKING:
8+
from .adapter import WecomConfig
9+
10+
class WechatApiDelegate(ABC):
11+
"""微信API代理接口,用于处理不同类型的微信API调用"""
12+
13+
@abstractmethod
14+
def setup_api(self, config: "WecomConfig"):
15+
"""设置API相关组件"""
16+
17+
@abstractmethod
18+
def check_signature(self, signature: str, timestamp: str, nonce: str, echo_str: str) -> str:
19+
"""验证签名"""
20+
21+
@abstractmethod
22+
def decrypt_message(self, message: bytes, signature: str, timestamp: str, nonce: str) -> str:
23+
"""解密消息"""
24+
25+
@abstractmethod
26+
def parse_message(self, message: str) -> Any:
27+
"""解析消息"""
28+
29+
@abstractmethod
30+
async def send_text(self, app_id: str, user_id: str, text: str) -> Any:
31+
"""发送文本消息"""
32+
33+
@abstractmethod
34+
async def send_media(self, app_id: str, user_id: str, media_type: str, media_bytes: BytesIO) -> Any:
35+
"""发送媒体消息"""
36+
37+
38+
class CorpWechatApiDelegate(WechatApiDelegate):
39+
"""企业微信API代理实现"""
40+
41+
def setup_api(self, config: "WecomConfig"):
42+
"""设置企业微信API相关组件"""
43+
from wechatpy.enterprise import parse_message
44+
from wechatpy.enterprise.client import WeChatClient
45+
from wechatpy.enterprise.crypto import WeChatCrypto
46+
47+
self.crypto = WeChatCrypto(
48+
config.token, config.encoding_aes_key, config.corp_id
49+
)
50+
self.client = WeChatClient(config.corp_id, config.secret)
51+
self.parse_message_func = parse_message
52+
self.logger = get_logger("CorpWechatApiDelegate")
53+
54+
def check_signature(self, signature: str, timestamp: str, nonce: str, echo_str: str) -> str:
55+
"""验证企业微信签名"""
56+
return self.crypto.check_signature(signature, timestamp, nonce, echo_str)
57+
58+
def decrypt_message(self, message: bytes, signature: str, timestamp: str, nonce: str) -> str:
59+
"""解密企业微信消息"""
60+
return self.crypto.decrypt_message(message, signature, timestamp, nonce)
61+
62+
def parse_message(self, message: str) -> Any:
63+
"""解析企业微信消息"""
64+
return self.parse_message_func(message)
65+
66+
async def send_text(self, app_id: str, user_id: str, text: str) -> Any:
67+
"""发送企业微信文本消息"""
68+
return self.client.message.send_text(app_id, user_id, text)
69+
70+
async def send_media(self, app_id: str, user_id: str, media_type: str, media_bytes: BytesIO) -> Any:
71+
"""发送企业微信媒体消息"""
72+
media_id = self.client.media.upload(media_type, media_bytes)["media_id"]
73+
send_method = getattr(self.client.message, f"send_{media_type}")
74+
return send_method(app_id, user_id, media_id)
75+
76+
77+
class PublicWechatApiDelegate(WechatApiDelegate):
78+
"""公众号微信API代理实现"""
79+
80+
def setup_api(self, config: "WecomConfig"):
81+
"""设置公众号API相关组件"""
82+
from wechatpy import WeChatClient
83+
from wechatpy.crypto import WeChatCrypto
84+
from wechatpy.parser import parse_message
85+
86+
self.crypto = WeChatCrypto(
87+
config.token, config.encoding_aes_key, config.app_id
88+
)
89+
self.client = WeChatClient(config.app_id, config.secret)
90+
self.parse_message_func = parse_message
91+
self.logger = get_logger("PublicWechatApiDelegate")
92+
93+
def check_signature(self, signature: str, timestamp: str, nonce: str, echo_str: str) -> str:
94+
"""验证公众号签名"""
95+
from wechatpy.utils import check_signature as wechat_check_signature
96+
wechat_check_signature(self.crypto.token, signature, timestamp, nonce)
97+
return echo_str
98+
99+
def decrypt_message(self, message: bytes, signature: str, timestamp: str, nonce: str) -> str:
100+
"""解密公众号消息"""
101+
return self.crypto.decrypt_message(message, signature, timestamp, nonce)
102+
103+
def parse_message(self, message: str) -> Any:
104+
"""解析公众号消息"""
105+
return self.parse_message_func(message)
106+
107+
async def send_text(self, app_id: str, user_id: str, text: str) -> Any:
108+
"""发送公众号文本消息"""
109+
# 公众号API不需要app_id参数
110+
return self.client.message.send_text(user_id, text)
111+
112+
async def send_media(self, app_id: str, user_id: str, media_type: str, media_bytes: BytesIO) -> Any:
113+
"""发送公众号媒体消息"""
114+
media_id = self.client.media.upload(media_type, media_bytes)["media_id"]
115+
send_method = getattr(self.client.message, f"send_{media_type}")
116+
# 公众号API不需要app_id参数
117+
return send_method(user_id, media_id)

0 commit comments

Comments
 (0)