diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index fa370a4d8..1a1802c30 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -321,6 +321,7 @@ class ChatProviderTemplate(TypedDict): "enable": False, "client_id": "", "client_secret": "", + "card_template_id": "", }, "Telegram": { "id": "telegram", @@ -582,6 +583,11 @@ class ChatProviderTemplate(TypedDict): "type": "string", "hint": "可选:填写 Misskey 网盘中目标文件夹的 ID,上传的文件将放置到该文件夹内。留空则使用账号网盘根目录。", }, + "card_template_id": { + "description": "卡片模板 ID", + "type": "string", + "hint": "可选。钉钉互动卡片模板 ID。启用后将使用互动卡片进行流式回复。", + }, "telegram_command_register": { "description": "Telegram 命令注册", "type": "bool", diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py index ec2b29a64..e73f724ca 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py @@ -39,7 +39,7 @@ async def process(self, event: dingtalk_stream.EventMessage): @register_platform_adapter( - "dingtalk", "钉钉机器人官方 API 适配器", support_streaming_message=False + "dingtalk", "钉钉机器人官方 API 适配器", support_streaming_message=True ) class DingtalkPlatformAdapter(Platform): def __init__( @@ -75,6 +75,8 @@ async def process(self, message: dingtalk_stream.CallbackMessage): ) self.client_ = client # 用于 websockets 的 client self._shutdown_event: threading.Event | None = None + self.card_template_id = platform_config.get("card_template_id") + self.card_instance_id_dict = {} def _id_to_sid(self, dingtalk_id: str | None) -> str: if not dingtalk_id: @@ -96,9 +98,65 @@ def meta(self) -> PlatformMetadata: name="dingtalk", description="钉钉机器人官方 API 适配器", id=cast(str, self.config.get("id")), - support_streaming_message=False, + support_streaming_message=True, ) + async def create_message_card( + self, message_id: str, incoming_message: dingtalk_stream.ChatbotMessage + ): + if not self.card_template_id: + return False + + card_instance = dingtalk_stream.AICardReplier(self.client_, incoming_message) + card_data = {"content": ""} # Initial content empty + + try: + card_instance_id = await card_instance.async_create_and_deliver_card( + self.card_template_id, + card_data, + ) + self.card_instance_id_dict[message_id] = (card_instance, card_instance_id) + return True + except Exception as e: + logger.error(f"创建钉钉卡片失败: {e}") + return False + + async def send_card_message(self, message_id: str, content: str, is_final: bool): + if message_id not in self.card_instance_id_dict: + return + + card_instance, card_instance_id = self.card_instance_id_dict[message_id] + content_key = "content" + + try: + # 钉钉卡片流式更新 + + await card_instance.async_streaming( + card_instance_id, + content_key=content_key, + content_value=content, + append=False, + finished=is_final, + failed=False, + ) + except Exception as e: + logger.error(f"发送钉钉卡片消息失败: {e}") + # Try to report failure + try: + await card_instance.async_streaming( + card_instance_id, + content_key=content_key, + content_value=content, # Keep existing content + append=False, + finished=True, + failed=True, + ) + except Exception: + pass + + if is_final: + self.card_instance_id_dict.pop(message_id, None) + async def convert_msg( self, message: dingtalk_stream.ChatbotMessage, @@ -224,6 +282,7 @@ async def handle_msg(self, abm: AstrBotMessage): platform_meta=self.meta(), session_id=abm.session_id, client=self.client, + adapter=self, ) self._event_queue.put_nowait(event) diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_event.py b/astrbot/core/platform/sources/dingtalk/dingtalk_event.py index 197701e0d..5af0d6eb0 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_event.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_event.py @@ -1,5 +1,5 @@ import asyncio -from typing import cast +from typing import Any, cast import dingtalk_stream @@ -16,9 +16,11 @@ def __init__( platform_meta, session_id, client: dingtalk_stream.ChatbotHandler, + adapter: "Any" = None, ): super().__init__(message_str, message_obj, platform_meta, session_id) self.client = client + self.adapter = adapter async def send_with_client( self, @@ -83,14 +85,58 @@ async def send(self, message: MessageChain): await super().send(message) async def send_streaming(self, generator, use_fallback: bool = False): - buffer = None - async for chain in generator: + if not self.adapter or not self.adapter.card_template_id: + logger.warning( + f"DingTalk streaming is enabled, but 'card_template_id' is not configured for platform '{self.platform_meta.id}'. Falling back to text streaming." + ) + # Fallback to default behavior (buffer and send) + buffer = None + async for chain in generator: + if not buffer: + buffer = chain + else: + buffer.chain.extend(chain.chain) + if not buffer: + return None + buffer.squash_plain() + await self.send(buffer) + return await super().send_streaming(generator, use_fallback) + + # Create card + msg_id = self.message_obj.message_id + incoming_msg = self.message_obj.raw_message + created = await self.adapter.create_message_card(msg_id, incoming_msg) + + if not created: + # Fallback to default behavior (buffer and send) + buffer = None + async for chain in generator: + if not buffer: + buffer = chain + else: + buffer.chain.extend(chain.chain) if not buffer: - buffer = chain - else: - buffer.chain.extend(chain.chain) - if not buffer: - return None - buffer.squash_plain() - await self.send(buffer) - return await super().send_streaming(generator, use_fallback) + return None + buffer.squash_plain() + await self.send(buffer) + return await super().send_streaming(generator, use_fallback) + + full_content = "" + seq = 0 + try: + async for chain in generator: + for segment in chain.chain: + if isinstance(segment, Comp.Plain): + full_content += segment.text + + seq += 1 + if seq % 2 == 0: # Update every 2 chunks to be more responsive than 8 + await self.adapter.send_card_message( + msg_id, full_content, is_final=False + ) + + await self.adapter.send_card_message(msg_id, full_content, is_final=True) + except Exception as e: + logger.error(f"DingTalk streaming error: {e}") + # Try to ensure final state is sent or cleaned up? + await self.adapter.send_card_message(msg_id, full_content, is_final=True)