diff --git a/README.md b/README.md index 07237bfa4..4e8c1a829 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@

-![da32a1f2-8da6-46a6-8516-5ed57d50e4b9](https://github.com/user-attachments/assets/39dc39ae-9003-434b-81a6-006b3de5e7c2) - +![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9)

diff --git a/astrbot/core/pipeline/waking_check/stage.py b/astrbot/core/pipeline/waking_check/stage.py index 6fb3378ca..5c5163938 100644 --- a/astrbot/core/pipeline/waking_check/stage.py +++ b/astrbot/core/pipeline/waking_check/stage.py @@ -4,7 +4,7 @@ from astrbot import logger from typing import Union, AsyncGenerator from astrbot.core.platform.astr_message_event import AstrMessageEvent from astrbot.core.message.message_event_result import MessageEventResult, MessageChain -from astrbot.core.message.components import At, AtAll +from astrbot.core.message.components import At, AtAll, Reply from astrbot.core.star.star_handler import star_handlers_registry, EventType from astrbot.core.star.star import star_map from astrbot.core.star.filter.permission import PermissionTypeFilter @@ -80,11 +80,19 @@ class WakingCheckStage(Stage): event.message_str = event.message_str[len(wake_prefix) :].strip() break if not is_wake: - # 检查是否有 at 消息 + # 检查是否有at消息 / at全体成员消息 / 引用了bot的消息 for message in messages: - if (isinstance(message, At) and ( - str(message.qq) == str(event.get_self_id()) - )) or (isinstance(message, AtAll) and not self.ignore_at_all): + if ( + ( + isinstance(message, At) + and (str(message.qq) == str(event.get_self_id())) + ) + or (isinstance(message, AtAll) and not self.ignore_at_all) + or ( + isinstance(message, Reply) + and str(message.sender_id) == str(event.get_self_id()) + ) + ): is_wake = True event.is_wake = True wake_prefix = "" diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_event.py b/astrbot/core/platform/sources/dingtalk/dingtalk_event.py index 4834032f5..c2188dc36 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_event.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_event.py @@ -32,31 +32,31 @@ class DingtalkMessageEvent(AstrMessageEvent): ) elif isinstance(segment, Comp.Image): markdown_str = "" - if segment.file and segment.file.startswith("file:///"): - logger.warning( - "dingtalk only support url image, not: " + segment.file - ) - continue - elif segment.file and segment.file.startswith("http"): - markdown_str += f"![image]({segment.file})\n\n" - elif segment.file and segment.file.startswith("base64://"): - logger.warning("dingtalk only support url image, not base64") - continue - else: - logger.warning( - "dingtalk only support url image, not: " + segment.file - ) - continue - ret = await asyncio.get_event_loop().run_in_executor( - None, - client.reply_markdown, - "😄", - markdown_str, - self.message_obj.raw_message, - ) - logger.debug(f"send image: {ret}") + try: + if not segment.file: + logger.warning("钉钉图片 segment 缺少 file 字段,跳过") + continue + if segment.file.startswith(("http://", "https://")): + image_url = segment.file + else: + image_url = await segment.register_to_file_service() + markdown_str = f"![image]({image_url})\n\n" + + ret = await asyncio.get_event_loop().run_in_executor( + None, + client.reply_markdown, + "😄", + markdown_str, + self.message_obj.raw_message, + ) + logger.debug(f"send image: {ret}") + + except Exception as e: + logger.error(f"钉钉图片处理失败: {e}") + logger.warning(f"跳过图片发送: {image_path}") + continue async def send(self, message: MessageChain): await self.send_with_client(self.client, message) await super().send(message) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index 3cddeccce..58e3c9b19 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -2,6 +2,7 @@ import asyncio import base64 import json import os +import traceback import time from typing import Optional @@ -158,7 +159,6 @@ class WeChatPadProAdapter(Platform): os.makedirs(data_dir, exist_ok=True) with open(self.credentials_file, "w") as f: json.dump(credentials, f) - logger.info("成功保存 WeChatPadPro 凭据。") except Exception as e: logger.error(f"保存 WeChatPadPro 凭据失败: {e}") @@ -166,6 +166,8 @@ class WeChatPadProAdapter(Platform): """ 检查 WeChatPadPro 设备是否在线。 """ + if not self.auth_key: + return False url = f"{self.base_url}/login/GetLoginStatus" params = {"key": self.auth_key} @@ -184,12 +186,16 @@ class WeChatPadProAdapter(Platform): logger.info("WeChatPadPro 设备不在线。") return False else: - logger.error(f"未知的在线状态: {login_state:}") + logger.error(f"未知的在线状态: {response_data}") return False # Code == 300 为微信退出状态。 elif response.status == 200 and response_data.get("Code") == 300: logger.info("WeChatPadPro 设备已退出。") return False + elif response.status == 200 and response_data.get("Code") == -2: + # 该链接不存在 + self.auth_key = None + return False else: logger.error( f"检查在线状态失败: {response.status}, {response_data}" @@ -201,6 +207,7 @@ class WeChatPadProAdapter(Platform): return False except Exception as e: logger.error(f"检查在线状态时发生错误: {e}") + logger.error(traceback.format_exc()) return False async def generate_auth_key(self): @@ -224,7 +231,7 @@ class WeChatPadProAdapter(Platform): and len(response_data["Data"]) > 0 ): self.auth_key = response_data["Data"][0] - logger.info("成功获取授权码") + logger.info(f"成功获取授权码 {self.auth_key[:8]}...") else: logger.error( f"生成授权码成功但未找到授权码: {response_data}" @@ -250,7 +257,6 @@ class WeChatPadProAdapter(Platform): try: async with session.post(url, params=params, json=payload) as response: response_data = await response.json() - # 修正成功判断条件和数据提取路径 if response.status == 200 and response_data.get("Code") == 200: # 二维码地址在 Data.QrCodeUrl 字段中 if response_data.get("Data") and response_data["Data"].get( @@ -262,6 +268,13 @@ class WeChatPadProAdapter(Platform): f"获取登录二维码成功但未找到二维码地址: {response_data}" ) return None + elif "该 key 无效" in response_data.get("Text"): + logger.error( + "授权码无效,已经清除。请重新启动 AstrBot 或者本消息适配器。原因也可能是 WeChatPadPro 的 MySQL 服务没有启动成功,请检查 WeChatPadPro 服务的日志。" + ) + self.auth_key = None + self.save_credentials() + return None else: logger.error( f"获取登录二维码失败: {response.status}, {response_data}" @@ -354,7 +367,7 @@ class WeChatPadProAdapter(Platform): while True: try: async with websockets.connect(ws_url) as websocket: - logger.info("WebSocket 连接成功。") + logger.debug("WebSocket 连接成功。") # 设置空闲超时重连 wait_time = ( self.active_message_poll_interval @@ -369,7 +382,7 @@ class WeChatPadProAdapter(Platform): # logger.debug(message) # 不显示原始消息内容 asyncio.create_task(self.handle_websocket_message(message)) except asyncio.TimeoutError: - logger.warning(f"WebSocket 连接空闲超过 {wait_time} s") + logger.debug(f"WebSocket 连接空闲超过 {wait_time} s") break except websockets.exceptions.ConnectionClosedOK: logger.info("WebSocket 连接正常关闭。") @@ -492,7 +505,7 @@ class WeChatPadProAdapter(Platform): # 对于群聊,session_id 可以是群聊 ID 或发送者 ID + 群聊 ID (如果 unique_session 为 True) if self.unique_session: - abm.session_id = f"{from_user_name}_{to_user_name}" + abm.session_id = f"{from_user_name}#{abm.sender.user_id}" else: abm.session_id = from_user_name @@ -631,7 +644,11 @@ class WeChatPadProAdapter(Platform): # wechatpadpro 的格式: wxid # gewechat 的格式: msg_source = raw_message.get("msg_source", "") - if f"{abm.self_id}" in msg_source or f"{abm.self_id}," in msg_source or f",{abm.self_id}" in msg_source: + if ( + f"{abm.self_id}" in msg_source + or f"{abm.self_id}," in msg_source + or f",{abm.self_id}" in msg_source + ): at_me = True # 也检查 push_content 中是否有@提示 @@ -641,19 +658,28 @@ class WeChatPadProAdapter(Platform): if at_me: # 被@了,在消息开头插入At组件(参考gewechat的做法) - bot_nickname = await self._get_group_member_nickname(abm.group_id, abm.self_id) - abm.message.insert(0, At(qq=abm.self_id, name=bot_nickname or abm.self_id)) + bot_nickname = await self._get_group_member_nickname( + abm.group_id, abm.self_id + ) + abm.message.insert( + 0, At(qq=abm.self_id, name=bot_nickname or abm.self_id) + ) # 只有当消息内容不仅仅是@时才添加Plain组件 if "\u2005" in message_content: # 检查@之后是否还有其他内容 parts = message_content.split("\u2005") - if len(parts) > 1 and any(part.strip() for part in parts[1:]): + if len(parts) > 1 and any( + part.strip() for part in parts[1:] + ): abm.message.append(Plain(message_content)) else: # 检查是否只包含@机器人 is_pure_at = False - if bot_nickname and message_content.strip() == f"@{bot_nickname}": + if ( + bot_nickname + and message_content.strip() == f"@{bot_nickname}" + ): is_pure_at = True if not is_pure_at: abm.message.append(Plain(message_content)) @@ -806,7 +832,10 @@ class WeChatPadProAdapter(Platform): # 根据 session_id 判断消息类型 if "@chatroom" in session.session_id: dummy_message_obj.type = MessageType.GROUP_MESSAGE - dummy_message_obj.group_id = session.session_id + if "#" in session.session_id: + dummy_message_obj.group_id = session.session_id.split("#")[0] + else: + dummy_message_obj.group_id = session.session_id dummy_message_obj.sender = MessageMember(user_id="", nickname="") else: dummy_message_obj.type = MessageType.FRIEND_MESSAGE diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py index ab836ad28..3bb753dd4 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py @@ -81,12 +81,16 @@ class WeChatPadProMessageEvent(AstrMessageEvent): # logger.info(f"已添加 @ 信息: {message_text}") else: message_text = text + if self.get_group_id() and "#" in self.session_id: + session_id = self.session_id.split("#")[0] + else: + session_id = self.session_id payload = { "MsgItem": [ { "MsgType": 1, "TextContent": message_text, - "ToUserName": self.session_id, + "ToUserName": session_id, } ] } diff --git a/astrbot/core/utils/pip_installer.py b/astrbot/core/utils/pip_installer.py index a7c04d3d9..88cc21306 100644 --- a/astrbot/core/utils/pip_installer.py +++ b/astrbot/core/utils/pip_installer.py @@ -1,5 +1,6 @@ import logging import asyncio +import sys logger = logging.getLogger("astrbot") @@ -31,7 +32,10 @@ class PipInstaller: logger.info(f"Pip 包管理器: pip {' '.join(args)}") try: process = await asyncio.create_subprocess_exec( - "pip", *args, + sys.executable, + "-m", + "pip", + *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, ) @@ -47,6 +51,7 @@ class PipInstaller: except FileNotFoundError: # 没有 pip from pip import main as pip_main + result_code = await asyncio.to_thread(pip_main, args) # 清除 pip.main 导致的多余的 logging handlers