Compare commits

...

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
5029d89630 Address code review feedback
- Add validation to ensure only one context parameter is provided in get_message_by_id
- Simplify logger check in error handling

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
2025-12-03 14:16:16 +00:00
copilot-swe-agent[bot]
b081ef89d5 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>
2025-12-03 14:13:47 +00:00
copilot-swe-agent[bot]
75d449f21a Initial plan 2025-12-03 14:06:21 +00:00
4 changed files with 151 additions and 7 deletions

View File

@@ -166,6 +166,11 @@ class QQOfficialClient:
else: else:
message_data['image_attachments'] = None 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 return message_data
async def is_image(self, attachment: dict) -> bool: async def is_image(self, attachment: dict) -> bool:
@@ -272,6 +277,57 @@ class QQOfficialClient:
return True return True
return time.time() > self.access_token_expiry_time 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()
# Validate that exactly one context parameter is provided
provided_contexts = sum([bool(channel_id), bool(group_openid), bool(user_openid)])
if provided_contexts == 0:
await self.logger.warning(f'Cannot fetch message {message_id}: no context provided')
return {}
if provided_contexts > 1:
await self.logger.warning(f'Cannot fetch message {message_id}: multiple contexts provided')
return {}
# 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}'
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: async def repeat_seed(self, bot_secret: str, target_size: int = 32) -> bytes:
seed = bot_secret seed = bot_secret
while len(seed) < target_size: while len(seed) < target_size:

View File

@@ -110,3 +110,10 @@ class QQOfficialEvent(dict):
文件类型 文件类型
""" """
return self.get('content_type', '') return self.get('content_type', '')
@property
def message_reference(self) -> dict:
"""
引用消息
"""
return self.get('message_reference', {})

View File

@@ -100,6 +100,7 @@ class PreProcessor(stage.PipelineStage):
plain_text = '' plain_text = ''
quote_msg = query.pipeline_config['trigger'].get('misc', '').get('combine-quote-message') quote_msg = query.pipeline_config['trigger'].get('misc', '').get('combine-quote-message')
quoted_text = '' # Store quoted message text
for me in query.message_chain: for me in query.message_chain:
if isinstance(me, platform_message.Plain): if isinstance(me, platform_message.Plain):
@@ -117,6 +118,7 @@ class PreProcessor(stage.PipelineStage):
elif isinstance(me, platform_message.Quote) and quote_msg: elif isinstance(me, platform_message.Quote) and quote_msg:
for msg in me.origin: for msg in me.origin:
if isinstance(msg, platform_message.Plain): if isinstance(msg, platform_message.Plain):
quoted_text += msg.text
content_list.append(provider_message.ContentElement.from_text(msg.text)) content_list.append(provider_message.ContentElement.from_text(msg.text))
elif isinstance(msg, platform_message.Image): elif isinstance(msg, platform_message.Image):
if selected_runner != 'local-agent' or ( 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)) content_list.append(provider_message.ContentElement.from_image_base64(msg.base64))
query.variables['user_message_text'] = plain_text 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) query.user_message = provider_message.Message(role='user', content=content_list)
# =========== 触发事件 PromptPreProcessing # =========== 触发事件 PromptPreProcessing

View File

@@ -16,6 +16,9 @@ from ..logger import EventLogger
class QQOfficialMessageConverter(abstract_platform_adapter.AbstractMessageConverter): class QQOfficialMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
def __init__(self, bot: QQOfficialClient = None):
self.bot = bot
@staticmethod @staticmethod
async def yiri2target(message_chain: platform_message.MessageChain): async def yiri2target(message_chain: platform_message.MessageChain):
content_list = [] content_list = []
@@ -31,10 +34,63 @@ class QQOfficialMessageConverter(abstract_platform_adapter.AbstractMessageConver
return content_list return content_list
@staticmethod 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):
async def target2yiri(message: str, message_id: str, pic_url: str, content_type):
yiri_msg_list = [] yiri_msg_list = []
yiri_msg_list.append(platform_message.Source(id=message_id, time=datetime.datetime.now())) 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:
# 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
await self.bot.logger.warning(f'Failed to fetch quoted message {referenced_msg_id}: {e}')
if pic_url is not None: if pic_url is not None:
base64_url = await image.get_qq_official_image_base64(pic_url=pic_url, content_type=content_type) 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)) yiri_msg_list.append(platform_message.Image(base64=base64_url))
@@ -45,20 +101,35 @@ class QQOfficialMessageConverter(abstract_platform_adapter.AbstractMessageConver
class QQOfficialEventConverter(abstract_platform_adapter.AbstractEventConverter): class QQOfficialEventConverter(abstract_platform_adapter.AbstractEventConverter):
def __init__(self, message_converter: QQOfficialMessageConverter):
self.message_converter = message_converter
@staticmethod @staticmethod
async def yiri2target(event: platform_events.MessageEvent) -> QQOfficialEvent: async def yiri2target(event: platform_events.MessageEvent) -> QQOfficialEvent:
return event.source_platform_object return event.source_platform_object
@staticmethod async def target2yiri(self, event: QQOfficialEvent):
async def target2yiri(event: QQOfficialEvent):
""" """
QQ官方消息转换为LB对象 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=event.content,
message_id=event.d_id, message_id=event.d_id,
pic_url=event.attachments, pic_url=event.attachments,
content_type=event.content_type, 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': if event.t == 'C2C_MESSAGE_CREATE':
@@ -135,20 +206,27 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter
config: dict config: dict
bot_account_id: str bot_account_id: str
bot_uuid: str = None bot_uuid: str = None
message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter() message_converter: QQOfficialMessageConverter
event_converter: QQOfficialEventConverter = QQOfficialEventConverter() event_converter: QQOfficialEventConverter
def __init__(self, config: dict, logger: EventLogger): def __init__(self, config: dict, logger: EventLogger):
bot = QQOfficialClient( bot = QQOfficialClient(
app_id=config['appid'], secret=config['secret'], token=config['token'], logger=logger, unified_mode=True 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__( super().__init__(
config=config, config=config,
logger=logger, logger=logger,
bot=bot, bot=bot,
bot_account_id=config['appid'], bot_account_id=config['appid'],
) )
self.message_converter = message_converter
self.event_converter = event_converter
async def reply_message( async def reply_message(
self, self,