From 205180387a258c7d37cf6ce53d6bb43f98f65093 Mon Sep 17 00:00:00 2001 From: MiSeya <1710669647@qq> Date: Mon, 19 May 2025 21:12:09 +0800 Subject: [PATCH 1/4] =?UTF-8?q?Fix:=E4=BF=AE=E5=A4=8D=E4=BA=86WeChatPadPro?= =?UTF-8?q?=E5=9C=A8=E9=87=8D=E6=96=B0=E7=99=BB=E5=BD=95=E6=97=B6=E4=B8=BA?= =?UTF-8?q?=E6=96=B0=E8=AE=BE=E5=A4=87=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E5=BB=B6=E9=95=BF=E5=88=9D=E5=A7=8B=E5=8C=96Auth=5FKey?= =?UTF-8?q?=E6=9C=89=E6=95=88=E6=9C=9F=E8=87=B3365=E5=A4=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wechatpadpro/wechatpadpro_adapter.py | 72 +++++++++++-------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index 47d4fa9df..2e2ab3878 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -69,39 +69,42 @@ class WeChatPadProAdapter(Platform): self.auth_key = loaded_credentials.get("auth_key") self.wxid = loaded_credentials.get("wxid") + isLoginIn = await self.check_online_status() + # 检查在线状态 - if self.auth_key and await self.check_online_status(): - logger.info("WeChatPadPro 设备已在线,跳过扫码登录。") + if self.auth_key and isLoginIn: + logger.info("WeChatPadPro 设备已在线,凭据存在,跳过扫码登录。") # 如果在线,连接 WebSocket 接收消息 self.ws_handle_task = asyncio.create_task(self.connect_websocket()) else: - logger.info("WeChatPadPro 设备不在线或无可用凭据,开始扫码登录流程。") # 1. 生成授权码 - await self.generate_auth_key() - if not self.auth_key: - logger.error("无法获取授权码,WeChatPadPro 适配器启动失败。") - return + logger.info("WeChatPadPro 无可用凭据,将生成新的授权码。") + await self.generate_auth_key() # 2. 获取登录二维码 - qr_code_url = await self.get_login_qr_code() + if not isLoginIn: + logger.info("WeChatPadPro 设备已离线,开始扫码登录。") + qr_code_url = await self.get_login_qr_code() - if qr_code_url: - logger.info(f"请扫描以下二维码登录: {qr_code_url}") - else: - logger.error("无法获取登录二维码。") - return + if qr_code_url: + logger.info(f"请扫描以下二维码登录: {qr_code_url}") + else: + logger.error("无法获取登录二维码。") + return - # 3. 检测扫码状态 - login_successful = await self.check_login_status() + # 3. 检测扫码状态 + login_successful = await self.check_login_status() - if login_successful: - # 登录成功后,连接 WebSocket 接收消息 - self.ws_handle_task = asyncio.create_task(self.connect_websocket()) - else: - logger.warning("登录失败或超时,WeChatPadPro 适配器将关闭。") - await self.terminate() - return + if login_successful: + logger.info("登录成功,WeChatPadPro适配器已连接。") + else: + logger.warning("登录失败或超时,WeChatPadPro 适配器将关闭。") + await self.terminate() + return + + # 登录成功后,连接 WebSocket 接收消息 + self.ws_handle_task = asyncio.create_task(self.connect_websocket()) self._shutdown_event = asyncio.Event() await self._shutdown_event.wait() @@ -156,16 +159,29 @@ class WeChatPadProAdapter(Platform): if login_state == 1: logger.info("WeChatPadPro 设备当前在线。") return True - else: + # login_state == 3 为离线状态 + elif login_state == 3: logger.info( - f"WeChatPadPro 设备不在线,登录状态: {login_state}" + "WeChatPadPro 设备不在线。" ) return False + else: + logger.error( + f"未知的在线状态: {login_state:}" + ) + return False + # Code == 300 为微信退出状态。 + elif response.status == 200 and response_data.get("Code") == 300: + logger.info( + "WeChatPadPro 设备已退出。" + ) + return False else: logger.error( f"检查在线状态失败: {response.status}, {response_data}" ) return False + except aiohttp.ClientConnectorError as e: logger.error(f"连接到 WeChatPadPro 服务失败: {e}") return False @@ -179,7 +195,7 @@ class WeChatPadProAdapter(Platform): """ url = f"{self.base_url}/admin/GenAuthKey1" params = {"key": self.admin_key} - payload = {"Count": 1, "Days": 30} # 生成一个有效期30天的授权码 + payload = {"Count": 1, "Days": 365} # 生成一个有效期365天的授权码 async with aiohttp.ClientSession() as session: try: @@ -336,7 +352,7 @@ class WeChatPadProAdapter(Platform): message = await asyncio.wait_for( websocket.recv(), timeout=wait_time ) - logger.info(message) + # logger.debug(message) # 不显示原始消息内容 asyncio.create_task(self.handle_websocket_message(message)) except asyncio.TimeoutError: logger.warning( @@ -350,7 +366,7 @@ class WeChatPadProAdapter(Platform): logger.error(f"处理 WebSocket 消息时发生错误: {e}") break except Exception as e: - logger.error(f"WebSocket 连接失败: {e}") + logger.error(f"WebSocket 连接失败: {e}, 请检查WeChatPadPro服务状态,或尝试重启WeChatPadPro适配器。") await asyncio.sleep(5) async def handle_websocket_message(self, message: str): @@ -425,7 +441,7 @@ class WeChatPadProAdapter(Platform): ): # 再根据消息类型处理消息内容 await self._process_message_content(abm, raw_message, msg_type, content) - + return abm return None From d09b70b295be88452040c3eb4fd52455cdb0692a Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 20 May 2025 01:38:13 -0400 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=88=E4=B8=AA=E4=BA=BA=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=EF=BC=89=E4=B8=8B=E6=97=A0=E6=B3=95=E5=9B=9E=E5=A4=8D?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 6 ++ .../weixin_offacc_adapter.py | 52 +++++++++++++---- .../weixin_offacc_event.py | 56 ++++++++++++++++--- packages/astrbot/main.py | 6 ++ 4 files changed, 101 insertions(+), 19 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index f05e17f43..c78bd0480 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -176,6 +176,7 @@ CONFIG_METADATA_2 = { "api_base_url": "https://api.weixin.qq.com/cgi-bin/", "callback_server_host": "0.0.0.0", "port": 6194, + "active_send_mode": False }, "wecom(企业微信)": { "id": "wecom", @@ -220,6 +221,11 @@ CONFIG_METADATA_2 = { }, }, "items": { + "active_send_mode": { + "description": "是否换用主动发送接口", + "type": "bool", + "desc": "只有企业认证的公众号才能主动发送。主动发送接口的限制会少一些。" + }, "wpp_active_message_poll": { "description": "是否启用主动消息轮询", "type": "bool", diff --git a/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py b/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py index 5ed589516..04186ff9d 100644 --- a/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +++ b/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py @@ -20,7 +20,7 @@ from requests import Response from wechatpy.utils import check_signature from wechatpy.crypto import WeChatCrypto from wechatpy import WeChatClient -from wechatpy.messages import TextMessage, ImageMessage, VoiceMessage +from wechatpy.messages import TextMessage, ImageMessage, VoiceMessage, BaseMessage from wechatpy.exceptions import InvalidSignatureException from wechatpy import parse_message from .weixin_offacc_event import WeixinOfficialAccountPlatformEvent @@ -87,7 +87,11 @@ class WecomServer: logger.info(f"解析成功: {msg}") if self.callback: - await self.callback(msg) + result_xml = await self.callback(msg) + if not result_xml: + return "success" + if isinstance(result_xml, str): + return result_xml return "success" @@ -117,6 +121,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform): self.api_base_url = platform_config.get( "api_base_url", "https://api.weixin.qq.com/cgi-bin/" ) + self.active_send_mode = self.config.get("active_send_mode", False) if not self.api_base_url: self.api_base_url = "https://api.weixin.qq.com/cgi-bin/" @@ -138,9 +143,29 @@ class WeixinOfficialAccountPlatformAdapter(Platform): self.client.API_BASE_URL = self.api_base_url - async def callback(msg): + # 微信公众号必须 5 秒内进行回复,否则会重试 3 次,我们需要对其进行消息排重 + # msgid -> Future + self.wexin_event_workers: dict[str, asyncio.Future] = {} + + async def callback(msg: BaseMessage): try: - await self.convert_message(msg) + if self.active_send_mode: + await self.convert_message(msg, None) + else: + if msg.id in self.wexin_event_workers: + future = self.wexin_event_workers[msg.id] + logger.debug(f"duplicate message id checked: {msg.id}") + else: + future = asyncio.get_event_loop().create_future() + self.wexin_event_workers[msg.id] = future + await self.convert_message(msg, future) + # I love shield so much! + result = await asyncio.wait_for(asyncio.shield(future), 60) # wait for 60s + logger.debug(f"Got future result: {result}") + self.wexin_event_workers.pop(msg.id, None) + return result # xml. see weixin_offacc_event.py + except asyncio.TimeoutError: + pass except Exception as e: logger.error(f"转换消息时出现异常: {e}") @@ -163,7 +188,9 @@ class WeixinOfficialAccountPlatformAdapter(Platform): async def run(self): await self.server.start_polling() - async def convert_message(self, msg) -> AstrBotMessage | None: + async def convert_message( + self, msg, future: asyncio.Future = None + ) -> AstrBotMessage | None: abm = AstrBotMessage() if isinstance(msg, TextMessage): abm.message_str = msg.content @@ -177,7 +204,6 @@ class WeixinOfficialAccountPlatformAdapter(Platform): abm.message_id = msg.id abm.timestamp = msg.time abm.session_id = abm.sender.user_id - abm.raw_message = msg elif msg.type == "image": assert isinstance(msg, ImageMessage) abm.message_str = "[图片]" @@ -191,7 +217,6 @@ class WeixinOfficialAccountPlatformAdapter(Platform): abm.message_id = msg.id abm.timestamp = msg.time abm.session_id = abm.sender.user_id - abm.raw_message = msg elif msg.type == "voice": assert isinstance(msg, VoiceMessage) @@ -209,7 +234,9 @@ class WeixinOfficialAccountPlatformAdapter(Platform): audio = AudioSegment.from_file(path) audio.export(path_wav, format="wav") except Exception as e: - logger.error(f"转换音频失败: {e}。如果没有安装 pydub 和 ffmpeg 请先安装。") + logger.error( + f"转换音频失败: {e}。如果没有安装 pydub 和 ffmpeg 请先安装。" + ) path_wav = path return @@ -224,11 +251,16 @@ class WeixinOfficialAccountPlatformAdapter(Platform): abm.message_id = msg.id abm.timestamp = msg.time abm.session_id = abm.sender.user_id - abm.raw_message = msg else: logger.warning(f"暂未实现的事件: {msg.type}") + future.set_result(None) return - + # 很不优雅 :( + abm.raw_message = { + "message": msg, + "future": future, + "active_send_mode": self.active_send_mode, + } logger.info(f"abm: {abm}") await self.handle_msg(abm) diff --git a/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py b/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py index 9519cd497..102812705 100644 --- a/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +++ b/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py @@ -4,6 +4,8 @@ from astrbot.api.event import AstrMessageEvent, MessageChain from astrbot.api.platform import AstrBotMessage, PlatformMetadata from astrbot.api.message_components import Plain, Image, Record from wechatpy import WeChatClient +from wechatpy.replies import TextReply, ImageReply, VoiceReply + from astrbot.api import logger @@ -82,12 +84,23 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): async def send(self, message: MessageChain): message_obj = self.message_obj + active_send_mode = message_obj.raw_message.get("active_send_mode", False) for comp in message.chain: if isinstance(comp, Plain): # Split long text messages if needed plain_chunks = await self.split_plain(comp.text) for chunk in plain_chunks: - self.client.message.send_text(message_obj.sender.user_id, chunk) + if active_send_mode: + self.client.message.send_text(message_obj.sender.user_id, chunk) + else: + reply = TextReply( + content=chunk, + message=self.message_obj.raw_message["message"], + ) + xml = reply.render() + future = self.message_obj.raw_message["future"] + assert isinstance(future, asyncio.Future) + future.set_result(xml) await asyncio.sleep(0.5) # Avoid sending too fast elif isinstance(comp, Image): img_path = await comp.convert_to_file_path() @@ -102,10 +115,22 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): ) return logger.debug(f"微信公众平台上传图片返回: {response}") - self.client.message.send_image( - message_obj.sender.user_id, - response["media_id"], - ) + + if active_send_mode: + self.client.message.send_image( + message_obj.sender.user_id, + response["media_id"], + ) + else: + reply = ImageReply( + media_id=response["media_id"], + message=self.message_obj.raw_message["message"], + ) + xml = reply.render() + future = self.message_obj.raw_message["future"] + assert isinstance(future, asyncio.Future) + future.set_result(xml) + elif isinstance(comp, Record): record_path = await comp.convert_to_file_path() # 转成amr @@ -124,10 +149,23 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): ) return logger.info(f"微信公众平台上传语音返回: {response}") - self.client.message.send_voice( - message_obj.sender.user_id, - response["media_id"], - ) + + + if active_send_mode: + self.client.message.send_voice( + message_obj.sender.user_id, + response["media_id"], + ) + else: + reply = VoiceReply( + media_id=response["media_id"], + message=self.message_obj.raw_message["message"], + ) + xml = reply.render() + future = self.message_obj.raw_message["future"] + assert isinstance(future, asyncio.Future) + future.set_result(xml) + else: logger.warning(f"还没实现这个消息类型的发送逻辑: {comp.type}。") diff --git a/packages/astrbot/main.py b/packages/astrbot/main.py index 04d4cadb7..92ef6bfea 100644 --- a/packages/astrbot/main.py +++ b/packages/astrbot/main.py @@ -1462,3 +1462,9 @@ UID: {user_id} 此 ID 可用于设置管理员。 plugin_cfg["reset"] = reset_cfg alter_cmd_cfg["astrbot"] = plugin_cfg sp.put("alter_cmd", alter_cmd_cfg) + + @filter.command("test") + async def test_to(self, event: AstrMessageEvent): + import asyncio + await asyncio.sleep(10) + yield event.plain_result("OK") From 56bf5d38a12e6e1fe2d2068c562d49b55edb38b8 Mon Sep 17 00:00:00 2001 From: YOO_koishi <2358181935@qq.com> Date: Tue, 20 May 2025 13:51:11 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=94=A7fix:=20=E4=BF=AE=E6=94=B9logger?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E7=AD=89=E7=BA=A7=E4=B8=BAdebug=E7=BA=A7?= =?UTF-8?q?=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/provider/sources/volcengine_tts.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/astrbot/core/provider/sources/volcengine_tts.py b/astrbot/core/provider/sources/volcengine_tts.py index e713345b4..dca0196b1 100644 --- a/astrbot/core/provider/sources/volcengine_tts.py +++ b/astrbot/core/provider/sources/volcengine_tts.py @@ -61,9 +61,9 @@ class ProviderVolcengineTTS(TTSProvider): payload = self._build_request_payload(text) - logger.info(f"请求头: {headers}") - logger.info(f"请求 URL: {self.api_base}") - logger.info(f"请求体: {json.dumps(payload, ensure_ascii=False)[:100]}...") + logger.debug(f"请求头: {headers}") + logger.debug(f"请求 URL: {self.api_base}") + logger.debug(f"请求体: {json.dumps(payload, ensure_ascii=False)[:100]}...") try: async with aiohttp.ClientSession() as session: @@ -73,10 +73,10 @@ class ProviderVolcengineTTS(TTSProvider): headers=headers, timeout=self.timeout ) as response: - logger.info(f"响应状态码: {response.status}") + logger.debug(f"响应状态码: {response.status}") response_text = await response.text() - logger.info(f"响应内容: {response_text[:200]}...") + logger.debug(f"响应内容: {response_text[:200]}...") if response.status == 200: resp_data = json.loads(response_text) @@ -103,5 +103,5 @@ class ProviderVolcengineTTS(TTSProvider): except Exception as e: error_details = traceback.format_exc() - logger.info(f"火山引擎 TTS 异常详情: {error_details}") + logger.debug(f"火山引擎 TTS 异常详情: {error_details}") raise Exception(f"火山引擎 TTS 异常: {str(e)}") \ No newline at end of file From f62157be722c93e565c5a124a6f76c43c1bdc35c Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 20 May 2025 02:00:54 -0400 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=93=A6=20release:=20v3.5.11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 2 +- changelogs/v3.5.11.md | 7 +++++++ pyproject.toml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 changelogs/v3.5.11.md diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index e3e1a7c2f..dfccf2ac1 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -5,7 +5,7 @@ import os from astrbot.core.utils.astrbot_path import get_astrbot_data_path -VERSION = "3.5.10" +VERSION = "3.5.11" DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db") # 默认配置 diff --git a/changelogs/v3.5.11.md b/changelogs/v3.5.11.md new file mode 100644 index 000000000..9550e26c5 --- /dev/null +++ b/changelogs/v3.5.11.md @@ -0,0 +1,7 @@ +# What's Changed + +1. 新增:火山引擎 TTS +2. 修复:修复了 WeChatPadPro 在重新登录时为新设备的问题 +2. ‼️修复:微信公众号(个人认证或者未认证)的情况下能接收但无法回复消息的问题 +3. 修复:Minimax TTS 相关问题 +4. 优化:登录界面侧边栏、关于页面样式,修复如果此前已经登录但未自行跳转的问题 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7b38e8b43..f9971501c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "AstrBot" -version = "3.5.10" +version = "3.5.11" description = "易上手的多平台 LLM 聊天机器人及开发框架" readme = "README.md" requires-python = ">=3.10"