From 8a548b69faf742d1c504a5b21d38545501f06523 Mon Sep 17 00:00:00 2001 From: ManJiang Date: Wed, 21 Jan 2026 10:25:16 +0800 Subject: [PATCH 1/3] =?UTF-8?q?#4384=20=E9=92=89=E9=92=89=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=9B=9E=E5=A4=8D=E5=8D=A1=E7=89=87=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 6 ++ .../sources/dingtalk/dingtalk_adapter.py | 64 +++++++++++++++++- .../sources/dingtalk/dingtalk_event.py | 65 +++++++++++++++---- 3 files changed, 122 insertions(+), 13 deletions(-) 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..0a21b233f 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,8 +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: + # 钉钉卡片流式更新 + # append=False always for full replacement if we are managing the buffer + # AICardReplier logic might vary, but LangBot uses append=False and sends full content? + # LangBot: content_value=content, append=False + + 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, @@ -224,6 +283,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..f56c7e582 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 cast, Any 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,55 @@ 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: - 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) + + # 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: + 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) + From 8289a650a42d5e6e952958fef04afb16057334ff Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Wed, 21 Jan 2026 12:46:49 +0800 Subject: [PATCH 2/3] chore: ruff format --- .../sources/dingtalk/dingtalk_adapter.py | 25 +++++++++---------- .../sources/dingtalk/dingtalk_event.py | 19 ++++++++------ 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py index 0a21b233f..e73f724ca 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py @@ -100,14 +100,16 @@ def meta(self) -> PlatformMetadata: id=cast(str, self.config.get("id")), support_streaming_message=True, ) - - async def create_message_card(self, message_id: str, incoming_message: dingtalk_stream.ChatbotMessage): + + 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 - + card_data = {"content": ""} # Initial content empty + try: card_instance_id = await card_instance.async_create_and_deliver_card( self.card_template_id, @@ -124,14 +126,11 @@ async def send_card_message(self, message_id: str, content: str, is_final: bool) return card_instance, card_instance_id = self.card_instance_id_dict[message_id] - content_key = 'content' - + content_key = "content" + try: # 钉钉卡片流式更新 - # append=False always for full replacement if we are managing the buffer - # AICardReplier logic might vary, but LangBot uses append=False and sends full content? - # LangBot: content_value=content, append=False - + await card_instance.async_streaming( card_instance_id, content_key=content_key, @@ -147,14 +146,14 @@ async def send_card_message(self, message_id: str, content: str, is_final: bool) await card_instance.async_streaming( card_instance_id, content_key=content_key, - content_value=content, # Keep existing content + 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) diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_event.py b/astrbot/core/platform/sources/dingtalk/dingtalk_event.py index f56c7e582..b39bea43d 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_event.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_event.py @@ -86,7 +86,9 @@ async def send(self, message: MessageChain): async def send_streaming(self, generator, use_fallback: bool = False): 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.") + 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: @@ -104,7 +106,7 @@ async def send_streaming(self, generator, use_fallback: bool = False): 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 @@ -118,7 +120,7 @@ async def send_streaming(self, generator, use_fallback: bool = False): buffer.squash_plain() await self.send(buffer) return await super().send_streaming(generator, use_fallback) - + full_content = "" seq = 0 try: @@ -126,14 +128,15 @@ async def send_streaming(self, generator, use_fallback: bool = False): 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) - + 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) - From 6205ef2612918e14f5d1f83f9d003736408dd59e Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Wed, 21 Jan 2026 12:47:45 +0800 Subject: [PATCH 3/3] chore: ruff format --- astrbot/core/platform/sources/dingtalk/dingtalk_event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_event.py b/astrbot/core/platform/sources/dingtalk/dingtalk_event.py index b39bea43d..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, Any +from typing import Any, cast import dingtalk_stream