From e06fac2bb7da8bf43cc69017005ba498260330b8 Mon Sep 17 00:00:00 2001 From: Guanchao Wang Date: Fri, 10 Apr 2026 16:10:13 +0800 Subject: [PATCH] fix: add filereader for dingtalk,lark (#2122) * fix: add filereader for dingtalk * feat: add lark --- src/langbot/libs/dingtalk_api/api.py | 91 +++++++++++++++++++ .../libs/dingtalk_api/dingtalkevent.py | 16 ++++ src/langbot/pkg/platform/sources/dingtalk.py | 27 ++++++ src/langbot/pkg/platform/sources/lark.py | 28 ++++-- 4 files changed, 152 insertions(+), 10 deletions(-) diff --git a/src/langbot/libs/dingtalk_api/api.py b/src/langbot/libs/dingtalk_api/api.py index 49b81372..723ee9e8 100644 --- a/src/langbot/libs/dingtalk_api/api.py +++ b/src/langbot/libs/dingtalk_api/api.py @@ -182,6 +182,88 @@ class DingTalkClient: for handler in self._message_handlers[msg_type]: await handler(event) + async def _parse_quoted_message(self, replied_msg: dict) -> dict: + """Parse the quoted/replied message and extract its content. + + Args: + replied_msg: The repliedMsg object from DingTalk message + + Returns: + A dict containing the quoted message info with keys: + - message_id: The original message ID + - msg_type: The message type (text, file, picture, audio, etc.) + - content: The text content (if any) + - file_url: The file download URL (if file type) + - file_name: The file name (if file type) + - picture: The picture base64 (if picture type) + - audio: The audio base64 (if audio type) + """ + quote_info = { + 'message_id': replied_msg.get('msgId', ''), + 'msg_type': replied_msg.get('msgType', ''), + 'sender_id': replied_msg.get('senderId', ''), + } + + msg_type = replied_msg.get('msgType', '') + content = replied_msg.get('content', {}) + + # Handle content as string (JSON) or dict + if isinstance(content, str): + try: + content = json.loads(content) + except (json.JSONDecodeError, TypeError): + content = {} + + if msg_type == 'text': + # Text message + if isinstance(content, dict): + quote_info['content'] = content.get('content', '') + else: + quote_info['content'] = str(content) + + elif msg_type == 'file': + # File message + download_code = content.get('downloadCode') + file_name = content.get('fileName') + if download_code and file_name: + try: + quote_info['file_url'] = await self.get_file_url(download_code) + quote_info['file_name'] = file_name + except Exception as e: + if self.logger: + await self.logger.error(f'Failed to get quoted file URL: {e}') + + elif msg_type == 'picture': + # Picture message + download_code = content.get('downloadCode') + if download_code: + try: + quote_info['picture'] = await self.download_image(download_code) + except Exception as e: + if self.logger: + await self.logger.error(f'Failed to download quoted image: {e}') + + elif msg_type == 'audio': + # Audio message + download_code = content.get('downloadCode') + if download_code: + try: + quote_info['audio'] = await self.get_audio_url(download_code) + except Exception as e: + if self.logger: + await self.logger.error(f'Failed to get quoted audio: {e}') + + elif msg_type == 'richText': + # Rich text message - extract text content + rich_text = content.get('richText', []) + texts = [] + for item in rich_text: + if 'text' in item and item['text'] != '\n': + texts.append(item['text']) + quote_info['content'] = '\n'.join(texts) + + return quote_info + async def get_message(self, incoming_message: dingtalk_stream.chatbot.ChatbotMessage): try: # print(json.dumps(incoming_message.to_dict(), indent=4, ensure_ascii=False)) @@ -193,6 +275,15 @@ class DingTalkClient: elif str(incoming_message.conversation_type) == '2': message_data['conversation_type'] = 'GroupMessage' + # Check for quoted/replied message + raw_data = incoming_message.to_dict() + text_data = raw_data.get('text', {}) + if isinstance(text_data, dict) and text_data.get('isReplyMsg'): + replied_msg = text_data.get('repliedMsg', {}) + if replied_msg: + quote_info = await self._parse_quoted_message(replied_msg) + message_data['QuotedMessage'] = quote_info + if incoming_message.message_type == 'richText': data = incoming_message.rich_text_content.to_dict() diff --git a/src/langbot/libs/dingtalk_api/dingtalkevent.py b/src/langbot/libs/dingtalk_api/dingtalkevent.py index 29322bcb..be6842e9 100644 --- a/src/langbot/libs/dingtalk_api/dingtalkevent.py +++ b/src/langbot/libs/dingtalk_api/dingtalkevent.py @@ -47,6 +47,22 @@ class DingTalkEvent(dict): def conversation(self): return self.get('conversation_type', '') + @property + def quoted_message(self) -> Optional[Dict[str, Any]]: + """Get the quoted/replied message info if this is a reply message. + + Returns: + A dict containing: + - message_id: The original message ID + - msg_type: The message type (text, file, picture, audio, etc.) + - content: The text content (if any) + - file_url: The file download URL (if file type) + - file_name: The file name (if file type) + - picture: The picture base64 (if picture type) + - audio: The audio base64 (if audio type) + """ + return self.get('QuotedMessage') + def __getattr__(self, key: str) -> Optional[Any]: """ 允许通过属性访问数据中的任意字段。 diff --git a/src/langbot/pkg/platform/sources/dingtalk.py b/src/langbot/pkg/platform/sources/dingtalk.py index c7a8a430..f47f995d 100644 --- a/src/langbot/pkg/platform/sources/dingtalk.py +++ b/src/langbot/pkg/platform/sources/dingtalk.py @@ -88,6 +88,33 @@ class DingTalkMessageConverter(abstract_platform_adapter.AbstractMessageConverte else: yiri_msg_list.append(platform_message.Voice(base64=event.audio)) + # Handle quoted/replied message - extract content as top-level components + # so that plugins like FileReader can process them the same way as direct messages + if event.quoted_message: + quote_info = event.quoted_message + msg_type = quote_info.get('msg_type', '') + + # Process quoted file - add as top-level File component (same as private chat) + if msg_type == 'file' and quote_info.get('file_url'): + file_name = quote_info.get('file_name', 'file') + yiri_msg_list.append(platform_message.File(url=quote_info['file_url'], name=file_name)) + + # Process quoted image - add as top-level Image component + elif msg_type == 'picture' and quote_info.get('picture'): + yiri_msg_list.append(platform_message.Image(base64=quote_info['picture'])) + + # Process quoted audio - add as top-level Voice component + elif msg_type == 'audio' and quote_info.get('audio'): + yiri_msg_list.append(platform_message.Voice(base64=quote_info['audio'])) + + # Process quoted text - add as Plain text with context prefix + elif msg_type == 'text' and quote_info.get('content'): + yiri_msg_list.append(platform_message.Plain(text=f'[引用消息] {quote_info["content"]}')) + + # Process quoted rich text - add as Plain text with context prefix + elif msg_type == 'richText' and quote_info.get('content'): + yiri_msg_list.append(platform_message.Plain(text=f'[引用消息] {quote_info["content"]}')) + chain = platform_message.MessageChain(yiri_msg_list) return chain diff --git a/src/langbot/pkg/platform/sources/lark.py b/src/langbot/pkg/platform/sources/lark.py index ad9ae163..364382c7 100644 --- a/src/langbot/pkg/platform/sources/lark.py +++ b/src/langbot/pkg/platform/sources/lark.py @@ -709,21 +709,29 @@ class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter): message_chain = await LarkMessageConverter.target2yiri(event.event.message, api_client) # Check for quote/reply message + # Extract files/images/voice from quote and add them as top-level components + # so that plugins like FileReader can process them the same way as direct messages quote_message_id = LarkEventConverter._extract_quote_message_id(event.event.message) if quote_message_id: quote_chain = await LarkEventConverter._fetch_quoted_message(quote_message_id, api_client) if quote_chain: # Filter out Source component from quoted chain, keep only content - quote_origin = platform_message.MessageChain( - [comp for comp in quote_chain if not isinstance(comp, platform_message.Source)] - ) - if quote_origin: - message_chain.append( - platform_message.Quote( - message_id=quote_message_id, - origin=quote_origin, - ) - ) + quote_components = [comp for comp in quote_chain if not isinstance(comp, platform_message.Source)] + + # Add quoted content as top-level components instead of wrapping in Quote + for comp in quote_components: + if isinstance(comp, platform_message.File): + # Add file as top-level component (same as direct message) + message_chain.append(comp) + elif isinstance(comp, platform_message.Image): + # Add image as top-level component + message_chain.append(comp) + elif isinstance(comp, platform_message.Voice): + # Add voice as top-level component + message_chain.append(comp) + elif isinstance(comp, platform_message.Plain): + # Add text with context prefix + message_chain.append(platform_message.Plain(text=f'[引用消息] {comp.text}')) if event.event.message.chat_type == 'p2p': return platform_events.FriendMessage(