From 2c6902b37b274a1cd0f32682370c25425ea828f5 Mon Sep 17 00:00:00 2001 From: Seayon <153660639@qq.com> Date: Sat, 13 Sep 2025 17:08:14 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20feat(platform):=20=E4=B8=BA=20T?= =?UTF-8?q?elegram=20=E5=92=8C=E9=A3=9E=E4=B9=A6=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E8=A1=A8=E6=83=85=E5=9B=9E=E5=BA=94=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持在收到命令时自动添加表情回应,提升用户交互体验 新增平台特异配置项,允许自定义启用状态和表情列表 --- astrbot/core/config/default.py | 46 +++++++++++++++++++ .../core/pipeline/preprocess_stage/stage.py | 15 ++++++ astrbot/core/platform/astr_message_event.py | 4 ++ .../core/platform/sources/lark/lark_event.py | 20 ++++++++ .../platform/sources/telegram/tg_event.py | 34 ++++++++++++++ 5 files changed, 119 insertions(+) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 139c37014..deda65da5 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -115,6 +115,15 @@ "port": 6185, }, "platform": [], + "platform_specific": { + # 平台特异配置:按平台分类,平台下按功能分组 + "lark": { + "pre_ack_emoji": {"enable": False, "emojis": ["Typing"]}, + }, + "telegram": { + "pre_ack_emoji": {"enable": False, "emojis": ["✍️"]}, + } + }, "wake_prefix": ["/"], "log_level": "INFO", "pip_install_arg": "", @@ -2206,6 +2215,43 @@ }, }, }, + "platform_specific_group": { + "name": "平台特异配置", + "metadata": { + "lark": { + "description": "飞书", + "type": "object", + "items": { + "platform_specific.lark.pre_ack_emoji.enable": { + "description": "预回应表情", + "type": "bool", + }, + "platform_specific.lark.pre_ack_emoji.emojis": { + "description": "表情列表(飞书表情枚举名)", + "type": "list", + "items": {"type": "string"}, + "hint": "参考:https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce", + }, + }, + }, + "telegram": { + "description": "Telegram", + "type": "object", + "items": { + "platform_specific.telegram.pre_ack_emoji.enable": { + "description": "预回应表情", + "type": "bool", + }, + "platform_specific.telegram.pre_ack_emoji.emojis": { + "description": "表情列表(Unicode,可多选)", + "type": "list", + "items": {"type": "string"}, + "hint": "Telegram 仅支持固定反应集合: 👍,👎,❤,🔥,🥰,👏,😁,🤔,🤯,😱,🤬,😢,🎉,🤩,🤮,💩,🙏,👌,🕊,🤡,🥱,🥴,😍,🐳,❤️‍🔥,🌚,🌭,💯,🤣,⚡,🍌,🏆,💔,🤨,😐,🍓,🍾,💋,🖕,😈,😴,😭,🤓,👻,👨‍💻,👀,🎃,🙈,😇,😨,🤝,✍,🤗,🫡,🎅,🎄,☃,💅,🤪,🗿,🆒,💘,🙉,🦄,😘,💊,🙊,😎,👾,🤷‍♂️,🤷,🤷‍♀️,😡", + }, + }, + }, + }, + }, "plugin_group": { "name": "插件配置", "metadata": { diff --git a/astrbot/core/pipeline/preprocess_stage/stage.py b/astrbot/core/pipeline/preprocess_stage/stage.py index 3e0d4e50f..f5e9a23b6 100644 --- a/astrbot/core/pipeline/preprocess_stage/stage.py +++ b/astrbot/core/pipeline/preprocess_stage/stage.py @@ -1,5 +1,6 @@ import traceback import asyncio +import random from typing import Union, AsyncGenerator from ..stage import Stage, register_stage from ..context import PipelineContext @@ -22,6 +23,20 @@ async def process( self, event: AstrMessageEvent ) -> Union[None, AsyncGenerator[None, None]]: """在处理事件之前的预处理""" + # 平台特异配置:platform_specific..pre_ack_emoji + supported = {"telegram", "lark"} + platform = event.get_platform_name() + cfg = ( + ((self.config.get("platform_specific", {}) or {}).get(platform) or {}) + .get("pre_ack_emoji") + or {} + ) + emojis = cfg.get("emojis") or [] + if cfg.get("enable") and platform in supported and emojis and event.is_at_or_wake_command: + try: + await event.react(random.choice(emojis)) + except Exception as e: + logger.warning(f"预回应表情发送失败: {e}") # 路径映射 if mappings := self.platform_settings.get("path_mapping", []): # 支持 Record,Image 消息段的路径映射。 diff --git a/astrbot/core/platform/astr_message_event.py b/astrbot/core/platform/astr_message_event.py index 75ea317ad..0e6befca6 100644 --- a/astrbot/core/platform/astr_message_event.py +++ b/astrbot/core/platform/astr_message_event.py @@ -412,6 +412,10 @@ async def send(self, message: MessageChain): ) self._has_send_oper = True + async def react(self, emoji: str): + """对消息添加表情回应。默认实现为发送一条包含该表情的消息。""" + await self.send(MessageChain([Plain(emoji)])) + async def get_group(self, group_id: str = None, **kwargs) -> Optional[Group]: """获取一个群聊的数据, 如果不填写 group_id: 如果是私聊消息,返回 None。如果是群聊消息,返回当前群聊的数据。 diff --git a/astrbot/core/platform/sources/lark/lark_event.py b/astrbot/core/platform/sources/lark/lark_event.py index 994d1495d..2342575a0 100644 --- a/astrbot/core/platform/sources/lark/lark_event.py +++ b/astrbot/core/platform/sources/lark/lark_event.py @@ -107,6 +107,26 @@ async def send(self, message: MessageChain): await super().send(message) + async def react(self, emoji: str): + request = ( + CreateMessageReactionRequest.builder() + .message_id(self.message_obj.message_id) + .request_body( + CreateMessageReactionRequestBody.builder() + .reaction_type( + Emoji.builder() + .emoji_type(emoji) + .build() + ) + .build() + ) + .build() + ) + response = await self.bot.im.v1.message_reaction.acreate(request) + if not response.success(): + logger.error(f"发送飞书表情回应失败({response.code}): {response.msg}") + return None + async def send_streaming(self, generator, use_fallback: bool = False): buffer = None async for chain in generator: diff --git a/astrbot/core/platform/sources/telegram/tg_event.py b/astrbot/core/platform/sources/telegram/tg_event.py index 5b3a1d916..563a3da20 100644 --- a/astrbot/core/platform/sources/telegram/tg_event.py +++ b/astrbot/core/platform/sources/telegram/tg_event.py @@ -16,6 +16,7 @@ from astrbot.core.utils.io import download_file from astrbot import logger from astrbot.core.utils.astrbot_path import get_astrbot_data_path +from telegram import ReactionTypeEmoji, ReactionTypeCustomEmoji class TelegramPlatformEvent(AstrMessageEvent): @@ -133,6 +134,39 @@ async def send(self, message: MessageChain): await self.send_with_client(self.client, message, self.get_sender_id()) await super().send(message) + async def react(self, emoji: str | None, big: bool = False): + """ + 给原消息添加 Telegram 反应: + - 普通 emoji:传入 '👍'、'😂' 等 + - 自定义表情:传入其 custom_emoji_id(纯数字字符串) + - 取消本机器人的反应:传入 None 或空字符串 + """ + try: + # 解析 chat_id(去掉超级群的 "#" 片段) + if self.get_message_type() == MessageType.GROUP_MESSAGE: + chat_id = (self.message_obj.group_id or "").split("#")[0] + else: + chat_id = self.get_sender_id() + + message_id = int(self.message_obj.message_id) + + # 组装 reaction 参数(必须是 ReactionType 的列表) + if not emoji: # 清空本 bot 的反应 + reaction_param = [] # 空列表表示移除本 bot 的反应 + elif emoji.isdigit(): # 自定义表情:传 custom_emoji_id + reaction_param = [ReactionTypeCustomEmoji(emoji)] + else: # 普通 emoji + reaction_param = [ReactionTypeEmoji(emoji)] + + await self.client.set_message_reaction( + chat_id=chat_id, + message_id=message_id, + reaction=reaction_param, # 注意是列表 + is_big=big, # 可选:大动画 + ) + except Exception as e: + logger.error(f"[Telegram] 添加反应失败: {e}") + async def send_streaming(self, generator, use_fallback: bool = False): message_thread_id = None From f5a832d3678473438b2b7d5b942f69ba329e73bf Mon Sep 17 00:00:00 2001 From: Seayon <153660639@qq.com> Date: Tue, 23 Sep 2025 11:15:00 +0800 Subject: [PATCH 2/4] Update astrbot/core/platform/astr_message_event.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- astrbot/core/platform/astr_message_event.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/astrbot/core/platform/astr_message_event.py b/astrbot/core/platform/astr_message_event.py index 0e6befca6..a9d746879 100644 --- a/astrbot/core/platform/astr_message_event.py +++ b/astrbot/core/platform/astr_message_event.py @@ -413,7 +413,13 @@ async def send(self, message: MessageChain): self._has_send_oper = True async def react(self, emoji: str): - """对消息添加表情回应。默认实现为发送一条包含该表情的消息。""" + """ + 对消息添加表情回应。 + + 默认实现为发送一条包含该表情的消息。 + 注意:此实现并不一定符合所有平台的原生“表情回应”行为。 + 如需支持平台原生的消息反应功能,请在对应平台的子类中重写本方法。 + """ await self.send(MessageChain([Plain(emoji)])) async def get_group(self, group_id: str = None, **kwargs) -> Optional[Group]: From cb9eb18683053770afefe8df8b55a60ce7b7d1c4 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 30 Sep 2025 17:04:56 +0800 Subject: [PATCH 3/4] style: ruff format --- astrbot/core/config/default.py | 2 +- astrbot/core/pipeline/preprocess_stage/stage.py | 13 ++++++++----- astrbot/core/platform/sources/lark/lark_event.py | 6 +----- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index e07418bd3..15a9bf405 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -123,7 +123,7 @@ }, "telegram": { "pre_ack_emoji": {"enable": False, "emojis": ["✍️"]}, - } + }, }, "wake_prefix": ["/"], "log_level": "INFO", diff --git a/astrbot/core/pipeline/preprocess_stage/stage.py b/astrbot/core/pipeline/preprocess_stage/stage.py index b29051cd1..fdabf0189 100644 --- a/astrbot/core/pipeline/preprocess_stage/stage.py +++ b/astrbot/core/pipeline/preprocess_stage/stage.py @@ -27,12 +27,15 @@ async def process( supported = {"telegram", "lark"} platform = event.get_platform_name() cfg = ( - ((self.config.get("platform_specific", {}) or {}).get(platform) or {}) - .get("pre_ack_emoji") - or {} - ) + (self.config.get("platform_specific", {}) or {}).get(platform) or {} + ).get("pre_ack_emoji") or {} emojis = cfg.get("emojis") or [] - if cfg.get("enable") and platform in supported and emojis and event.is_at_or_wake_command: + if ( + cfg.get("enable") + and platform in supported + and emojis + and event.is_at_or_wake_command + ): try: await event.react(random.choice(emojis)) except Exception as e: diff --git a/astrbot/core/platform/sources/lark/lark_event.py b/astrbot/core/platform/sources/lark/lark_event.py index 2342575a0..2174c497c 100644 --- a/astrbot/core/platform/sources/lark/lark_event.py +++ b/astrbot/core/platform/sources/lark/lark_event.py @@ -113,11 +113,7 @@ async def react(self, emoji: str): .message_id(self.message_obj.message_id) .request_body( CreateMessageReactionRequestBody.builder() - .reaction_type( - Emoji.builder() - .emoji_type(emoji) - .build() - ) + .reaction_type(Emoji.builder().emoji_type(emoji).build()) .build() ) .build() From 87b383fb21c0b38a71daa9bcc2308447407d08a4 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 30 Sep 2025 17:25:17 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E5=B9=B3=E5=8F=B0?= =?UTF-8?q?=E7=89=B9=E5=BC=82=E9=85=8D=E7=BD=AE=E7=9A=84=E9=A2=84=E5=9B=9E?= =?UTF-8?q?=E5=BA=94=E8=A1=A8=E6=83=85=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 33 +++++++------------ .../core/pipeline/preprocess_stage/stage.py | 11 ++++--- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 15a9bf405..e1b25a92b 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -2304,42 +2304,31 @@ "description": "用户权限不足时是否回复", "type": "bool", }, - }, - }, - }, - }, - "platform_specific_group": { - "name": "平台特异配置", - "metadata": { - "lark": { - "description": "飞书", - "type": "object", - "items": { "platform_specific.lark.pre_ack_emoji.enable": { - "description": "预回应表情", + "description": "[飞书] 启用预回应表情", "type": "bool", }, "platform_specific.lark.pre_ack_emoji.emojis": { "description": "表情列表(飞书表情枚举名)", "type": "list", "items": {"type": "string"}, - "hint": "参考:https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce", + "hint": "表情枚举名参考:https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce", + "condition": { + "platform_specific.lark.pre_ack_emoji.enable": True, + }, }, - }, - }, - "telegram": { - "description": "Telegram", - "type": "object", - "items": { "platform_specific.telegram.pre_ack_emoji.enable": { - "description": "预回应表情", + "description": "[Telegram] 启用预回应表情", "type": "bool", }, "platform_specific.telegram.pre_ack_emoji.emojis": { - "description": "表情列表(Unicode,可多选)", + "description": "表情列表(Unicode)", "type": "list", "items": {"type": "string"}, - "hint": "Telegram 仅支持固定反应集合: 👍,👎,❤,🔥,🥰,👏,😁,🤔,🤯,😱,🤬,😢,🎉,🤩,🤮,💩,🙏,👌,🕊,🤡,🥱,🥴,😍,🐳,❤️‍🔥,🌚,🌭,💯,🤣,⚡,🍌,🏆,💔,🤨,😐,🍓,🍾,💋,🖕,😈,😴,😭,🤓,👻,👨‍💻,👀,🎃,🙈,😇,😨,🤝,✍,🤗,🫡,🎅,🎄,☃,💅,🤪,🗿,🆒,💘,🙉,🦄,😘,💊,🙊,😎,👾,🤷‍♂️,🤷,🤷‍♀️,😡", + "hint": "Telegram 仅支持固定反应集合,参考:https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9", + "condition": { + "platform_specific.telegram.pre_ack_emoji.enable": True, + }, }, }, }, diff --git a/astrbot/core/pipeline/preprocess_stage/stage.py b/astrbot/core/pipeline/preprocess_stage/stage.py index fdabf0189..5c075687f 100644 --- a/astrbot/core/pipeline/preprocess_stage/stage.py +++ b/astrbot/core/pipeline/preprocess_stage/stage.py @@ -27,11 +27,13 @@ async def process( supported = {"telegram", "lark"} platform = event.get_platform_name() cfg = ( - (self.config.get("platform_specific", {}) or {}).get(platform) or {} - ).get("pre_ack_emoji") or {} + self.config.get("platform_specific", {}) + .get(platform, {}) + .get("pre_ack_emoji", {}) + ) or {} emojis = cfg.get("emojis") or [] if ( - cfg.get("enable") + cfg.get("enable", False) and platform in supported and emojis and event.is_at_or_wake_command @@ -39,7 +41,8 @@ async def process( try: await event.react(random.choice(emojis)) except Exception as e: - logger.warning(f"预回应表情发送失败: {e}") + logger.warning(f"{platform} 预回应表情发送失败: {e}") + # 路径映射 if mappings := self.platform_settings.get("path_mapping", []): # 支持 Record,Image 消息段的路径映射。