From b081ef89d5cd29501e5a1be01465679002026108 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:13:47 +0000 Subject: [PATCH] Add QQ bot quoted message support for Dify integration - Added message_reference property to QQOfficialEvent class - Implemented get_message_by_id method in QQOfficialClient to fetch referenced messages - Updated QQOfficialMessageConverter to handle quoted messages and create Quote components - Modified QQOfficialEventConverter to pass message reference context - Updated QQOfficialAdapter to properly initialize converters with bot reference - Added quoted_message_text variable in pipeline preprocessor for Dify workflow integration Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> --- src/langbot/libs/qq_official_api/api.py | 50 ++++++++++ .../libs/qq_official_api/qqofficialevent.py | 7 ++ src/langbot/pkg/pipeline/preproc/preproc.py | 3 + .../pkg/platform/sources/qqofficial.py | 93 +++++++++++++++++-- 4 files changed, 146 insertions(+), 7 deletions(-) diff --git a/src/langbot/libs/qq_official_api/api.py b/src/langbot/libs/qq_official_api/api.py index e4d4e468..f5c4c618 100644 --- a/src/langbot/libs/qq_official_api/api.py +++ b/src/langbot/libs/qq_official_api/api.py @@ -166,6 +166,11 @@ class QQOfficialClient: else: message_data['image_attachments'] = None + # Extract message_reference if present + message_reference = msg.get('d', {}).get('message_reference', {}) + if message_reference: + message_data['message_reference'] = message_reference + return message_data async def is_image(self, attachment: dict) -> bool: @@ -272,6 +277,51 @@ class QQOfficialClient: return True return time.time() > self.access_token_expiry_time + async def get_message_by_id(self, message_id: str, channel_id: str = None, group_openid: str = None, user_openid: str = None) -> Dict[str, Any]: + """根据消息ID获取消息内容 + + Args: + message_id: 消息ID + channel_id: 频道ID(频道消息需要) + group_openid: 群组openid(群消息需要) + user_openid: 用户openid(私聊消息需要) + + Returns: + 消息内容字典 + """ + if not await self.check_access_token(): + await self.get_access_token() + + # Determine which API endpoint to use based on provided parameters + if channel_id: + # Channel message + url = f'{self.base_url}/channels/{channel_id}/messages/{message_id}' + elif group_openid: + # Group message + url = f'{self.base_url}/v2/groups/{group_openid}/messages/{message_id}' + elif user_openid: + # Private message + url = f'{self.base_url}/v2/users/{user_openid}/messages/{message_id}' + else: + await self.logger.warning(f'Cannot fetch message {message_id}: no valid context provided') + return {} + + async with httpx.AsyncClient() as client: + headers = { + 'Authorization': f'QQBot {self.access_token}', + 'Content-Type': 'application/json', + } + try: + response = await client.get(url, headers=headers) + if response.status_code == 200: + return response.json() + else: + await self.logger.warning(f'Failed to fetch message {message_id}: {response.status_code}') + return {} + except Exception as e: + await self.logger.warning(f'Error fetching message {message_id}: {e}') + return {} + async def repeat_seed(self, bot_secret: str, target_size: int = 32) -> bytes: seed = bot_secret while len(seed) < target_size: diff --git a/src/langbot/libs/qq_official_api/qqofficialevent.py b/src/langbot/libs/qq_official_api/qqofficialevent.py index 7c29b9d8..bfdeac0e 100644 --- a/src/langbot/libs/qq_official_api/qqofficialevent.py +++ b/src/langbot/libs/qq_official_api/qqofficialevent.py @@ -110,3 +110,10 @@ class QQOfficialEvent(dict): 文件类型 """ return self.get('content_type', '') + + @property + def message_reference(self) -> dict: + """ + 引用消息 + """ + return self.get('message_reference', {}) diff --git a/src/langbot/pkg/pipeline/preproc/preproc.py b/src/langbot/pkg/pipeline/preproc/preproc.py index ace432d8..d1381c25 100644 --- a/src/langbot/pkg/pipeline/preproc/preproc.py +++ b/src/langbot/pkg/pipeline/preproc/preproc.py @@ -100,6 +100,7 @@ class PreProcessor(stage.PipelineStage): plain_text = '' quote_msg = query.pipeline_config['trigger'].get('misc', '').get('combine-quote-message') + quoted_text = '' # Store quoted message text for me in query.message_chain: if isinstance(me, platform_message.Plain): @@ -117,6 +118,7 @@ class PreProcessor(stage.PipelineStage): elif isinstance(me, platform_message.Quote) and quote_msg: for msg in me.origin: if isinstance(msg, platform_message.Plain): + quoted_text += msg.text content_list.append(provider_message.ContentElement.from_text(msg.text)) elif isinstance(msg, platform_message.Image): if selected_runner != 'local-agent' or ( @@ -126,6 +128,7 @@ class PreProcessor(stage.PipelineStage): content_list.append(provider_message.ContentElement.from_image_base64(msg.base64)) query.variables['user_message_text'] = plain_text + query.variables['quoted_message_text'] = quoted_text # Add quoted text as variable query.user_message = provider_message.Message(role='user', content=content_list) # =========== 触发事件 PromptPreProcessing diff --git a/src/langbot/pkg/platform/sources/qqofficial.py b/src/langbot/pkg/platform/sources/qqofficial.py index a3288793..05e5f641 100644 --- a/src/langbot/pkg/platform/sources/qqofficial.py +++ b/src/langbot/pkg/platform/sources/qqofficial.py @@ -16,6 +16,9 @@ from ..logger import EventLogger class QQOfficialMessageConverter(abstract_platform_adapter.AbstractMessageConverter): + def __init__(self, bot: QQOfficialClient = None): + self.bot = bot + @staticmethod async def yiri2target(message_chain: platform_message.MessageChain): content_list = [] @@ -31,10 +34,64 @@ class QQOfficialMessageConverter(abstract_platform_adapter.AbstractMessageConver return content_list - @staticmethod - async def target2yiri(message: str, message_id: str, pic_url: str, content_type): + async def target2yiri(self, message: str, message_id: str, pic_url: str, content_type, message_reference: dict = None, event_type: str = None, channel_id: str = None, group_openid: str = None, user_openid: str = None): yiri_msg_list = [] yiri_msg_list.append(platform_message.Source(id=message_id, time=datetime.datetime.now())) + + # Handle quoted message if message_reference exists + if message_reference and message_reference.get('message_id') and self.bot: + referenced_msg_id = message_reference.get('message_id') + try: + # Fetch the referenced message + referenced_msg = await self.bot.get_message_by_id( + referenced_msg_id, + channel_id=channel_id, + group_openid=group_openid, + user_openid=user_openid + ) + + if referenced_msg: + # Create message chain for the quoted content + quoted_content = referenced_msg.get('content', '') + quoted_chain = platform_message.MessageChain() + + if quoted_content: + quoted_chain.append(platform_message.Plain(text=quoted_content)) + + # Add images if present in quoted message + quoted_attachments = referenced_msg.get('attachments', []) + for attachment in quoted_attachments: + if attachment.get('content_type', '').startswith('image/'): + img_url = attachment.get('url', '') + if img_url: + try: + img_base64 = await image.get_qq_official_image_base64( + pic_url=img_url if img_url.startswith('https://') else 'https://' + img_url, + content_type=attachment.get('content_type', '') + ) + quoted_chain.append(platform_message.Image(base64=img_base64)) + except Exception as e: + # If image fetch fails, just skip it + pass + + # Get sender info from referenced message + quoted_sender_id = referenced_msg.get('author', {}).get('id', '') or \ + referenced_msg.get('author', {}).get('user_openid', '') or \ + referenced_msg.get('author', {}).get('member_openid', '') + + # Add Quote component + yiri_msg_list.append( + platform_message.Quote( + id=referenced_msg_id, + sender_id=quoted_sender_id, + origin=quoted_chain, + ) + ) + except Exception as e: + # If fetching quoted message fails, log and continue + if self.bot and hasattr(self.bot, 'logger'): + await self.bot.logger.warning(f'Failed to fetch quoted message {referenced_msg_id}: {e}') + if pic_url is not None: base64_url = await image.get_qq_official_image_base64(pic_url=pic_url, content_type=content_type) yiri_msg_list.append(platform_message.Image(base64=base64_url)) @@ -45,20 +102,35 @@ class QQOfficialMessageConverter(abstract_platform_adapter.AbstractMessageConver class QQOfficialEventConverter(abstract_platform_adapter.AbstractEventConverter): + def __init__(self, message_converter: QQOfficialMessageConverter): + self.message_converter = message_converter + @staticmethod async def yiri2target(event: platform_events.MessageEvent) -> QQOfficialEvent: return event.source_platform_object - @staticmethod - async def target2yiri(event: QQOfficialEvent): + async def target2yiri(self, event: QQOfficialEvent): """ QQ官方消息转换为LB对象 """ - yiri_chain = await QQOfficialMessageConverter.target2yiri( + # Get message reference if present + message_reference = event.message_reference + + # Determine context based on event type + channel_id = event.channel_id if event.t in ['AT_MESSAGE_CREATE', 'DIRECT_MESSAGE_CREATE'] else None + group_openid = event.group_openid if event.t == 'GROUP_AT_MESSAGE_CREATE' else None + user_openid = event.user_openid if event.t == 'C2C_MESSAGE_CREATE' else None + + yiri_chain = await self.message_converter.target2yiri( message=event.content, message_id=event.d_id, pic_url=event.attachments, content_type=event.content_type, + message_reference=message_reference, + event_type=event.t, + channel_id=channel_id, + group_openid=group_openid, + user_openid=user_openid, ) if event.t == 'C2C_MESSAGE_CREATE': @@ -135,20 +207,27 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter config: dict bot_account_id: str bot_uuid: str = None - message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter() - event_converter: QQOfficialEventConverter = QQOfficialEventConverter() + message_converter: QQOfficialMessageConverter + event_converter: QQOfficialEventConverter def __init__(self, config: dict, logger: EventLogger): bot = QQOfficialClient( app_id=config['appid'], secret=config['secret'], token=config['token'], logger=logger, unified_mode=True ) + # Initialize converters with bot reference + message_converter = QQOfficialMessageConverter(bot=bot) + event_converter = QQOfficialEventConverter(message_converter=message_converter) + super().__init__( config=config, logger=logger, bot=bot, bot_account_id=config['appid'], ) + + self.message_converter = message_converter + self.event_converter = event_converter async def reply_message( self,