diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 53e2db11e..e1b25a92b 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -116,6 +116,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": "", @@ -2295,6 +2304,32 @@ "description": "用户权限不足时是否回复", "type": "bool", }, + "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", + "condition": { + "platform_specific.lark.pre_ack_emoji.enable": True, + }, + }, + "platform_specific.telegram.pre_ack_emoji.enable": { + "description": "[Telegram] 启用预回应表情", + "type": "bool", + }, + "platform_specific.telegram.pre_ack_emoji.emojis": { + "description": "表情列表(Unicode)", + "type": "list", + "items": {"type": "string"}, + "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 50dc85a4b..5c075687f 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,26 @@ 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", {}) + .get(platform, {}) + .get("pre_ack_emoji", {}) + ) or {} + emojis = cfg.get("emojis") or [] + if ( + cfg.get("enable", False) + 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"{platform} 预回应表情发送失败: {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 52a882433..48c63ef2c 100644 --- a/astrbot/core/platform/astr_message_event.py +++ b/astrbot/core/platform/astr_message_event.py @@ -416,6 +416,16 @@ 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..2174c497c 100644 --- a/astrbot/core/platform/sources/lark/lark_event.py +++ b/astrbot/core/platform/sources/lark/lark_event.py @@ -107,6 +107,22 @@ 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 6784cfa3c..2da7aafe5 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): @@ -135,6 +136,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