Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/langbot/libs/qq_official_api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ async def get_message(self, msg: dict) -> Dict[str, Any]:
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:
Expand Down Expand Up @@ -272,6 +277,57 @@ async def is_token_expired(self):
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()

# 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:
seed = bot_secret
while len(seed) < target_size:
Expand Down
7 changes: 7 additions & 0 deletions src/langbot/libs/qq_official_api/qqofficialevent.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,10 @@ def content_type(self) -> str:
文件类型
"""
return self.get('content_type', '')

@property
def message_reference(self) -> dict:
"""
引用消息
"""
return self.get('message_reference', {})
3 changes: 3 additions & 0 deletions src/langbot/pkg/pipeline/preproc/preproc.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ async def process(

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):
Expand All @@ -117,6 +118,7 @@ async def process(
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 (
Expand All @@ -126,6 +128,7 @@ async def process(
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
Expand Down
92 changes: 85 additions & 7 deletions src/langbot/pkg/platform/sources/qqofficial.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@


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 = []
Expand All @@ -31,10 +34,63 @@ async def yiri2target(message_chain: platform_message.MessageChain):

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:
# 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:
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))
Expand All @@ -45,20 +101,35 @@ async def target2yiri(message: str, message_id: str, pic_url: str, content_type)


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':
Expand Down Expand Up @@ -135,20 +206,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,
Expand Down
Loading