mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-14 14:31:00 +00:00
Feat/complete adapter features (#1849)
* feat: add voice and file supports for wecom * feat: add and in query variables * feat: supports for lark recv file message * feat: kook recv voice msg * feat: supports for Voice and File in discord * chore: remove debug msg * perf: remove unnecessary bot logs * feat: implement bot log filtering and per label color (#1839) * feat: add sender_name and group_name in query variables
This commit is contained in:
committed by
GitHub
parent
daf56e5dc2
commit
6421a6f5cb
@@ -394,7 +394,6 @@ class WecomBotClient:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.wxcpt = WXBizMsgCrypt(self.Token, self.EnCodingAESKey, '')
|
self.wxcpt = WXBizMsgCrypt(self.Token, self.EnCodingAESKey, '')
|
||||||
await self.logger.info(f'{req.method} {req.url} {str(req.args)}')
|
|
||||||
|
|
||||||
if req.method == 'GET':
|
if req.method == 'GET':
|
||||||
return await self._handle_get_callback(req)
|
return await self._handle_get_callback(req)
|
||||||
|
|||||||
@@ -139,6 +139,58 @@ class WecomClient:
|
|||||||
await self.logger.error(f'发送图片失败:{data}')
|
await self.logger.error(f'发送图片失败:{data}')
|
||||||
raise Exception('Failed to send image: ' + str(data))
|
raise Exception('Failed to send image: ' + str(data))
|
||||||
|
|
||||||
|
async def send_voice(self, user_id: str, agent_id: int, media_id: str):
|
||||||
|
if not await self.check_access_token():
|
||||||
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
|
url = self.base_url + '/message/send?access_token=' + self.access_token
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
params = {
|
||||||
|
'touser': user_id,
|
||||||
|
'msgtype': 'voice',
|
||||||
|
'agentid': agent_id,
|
||||||
|
'voice': {
|
||||||
|
'media_id': media_id,
|
||||||
|
},
|
||||||
|
'safe': 0,
|
||||||
|
'enable_id_trans': 0,
|
||||||
|
'enable_duplicate_check': 0,
|
||||||
|
'duplicate_check_interval': 1800,
|
||||||
|
}
|
||||||
|
response = await client.post(url, json=params)
|
||||||
|
data = response.json()
|
||||||
|
if data['errcode'] == 40014 or data['errcode'] == 42001:
|
||||||
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
|
return await self.send_voice(user_id, agent_id, media_id)
|
||||||
|
if data['errcode'] != 0:
|
||||||
|
await self.logger.error(f'发送语音失败:{data}')
|
||||||
|
raise Exception('Failed to send voice: ' + str(data))
|
||||||
|
|
||||||
|
async def send_file(self, user_id: str, agent_id: int, media_id: str):
|
||||||
|
if not await self.check_access_token():
|
||||||
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
|
url = self.base_url + '/message/send?access_token=' + self.access_token
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
params = {
|
||||||
|
'touser': user_id,
|
||||||
|
'msgtype': 'file',
|
||||||
|
'agentid': agent_id,
|
||||||
|
'file': {
|
||||||
|
'media_id': media_id,
|
||||||
|
},
|
||||||
|
'safe': 0,
|
||||||
|
'enable_id_trans': 0,
|
||||||
|
'enable_duplicate_check': 0,
|
||||||
|
'duplicate_check_interval': 1800,
|
||||||
|
}
|
||||||
|
response = await client.post(url, json=params)
|
||||||
|
data = response.json()
|
||||||
|
if data['errcode'] == 40014 or data['errcode'] == 42001:
|
||||||
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
|
return await self.send_file(user_id, agent_id, media_id)
|
||||||
|
if data['errcode'] != 0:
|
||||||
|
await self.logger.error(f'发送文件失败:{data}')
|
||||||
|
raise Exception('Failed to send file: ' + str(data))
|
||||||
|
|
||||||
async def send_private_msg(self, user_id: str, agent_id: int, content: str):
|
async def send_private_msg(self, user_id: str, agent_id: int, content: str):
|
||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
@@ -287,7 +339,7 @@ class WecomClient:
|
|||||||
return ext
|
return ext
|
||||||
return 'jpg' # 默认返回jpg
|
return 'jpg' # 默认返回jpg
|
||||||
|
|
||||||
async def upload_to_work(self, image: platform_message.Image):
|
async def upload_image_to_work(self, image: platform_message.Image):
|
||||||
"""
|
"""
|
||||||
获取 media_id
|
获取 media_id
|
||||||
"""
|
"""
|
||||||
@@ -304,7 +356,7 @@ class WecomClient:
|
|||||||
file_bytes = await f.read()
|
file_bytes = await f.read()
|
||||||
file_name = image.path.split('/')[-1]
|
file_name = image.path.split('/')[-1]
|
||||||
elif image.url:
|
elif image.url:
|
||||||
file_bytes = await self.download_image_to_bytes(image.url)
|
file_bytes = await self.download_media_to_bytes(image.url)
|
||||||
file_name = image.url.split('/')[-1]
|
file_name = image.url.split('/')[-1]
|
||||||
elif image.base64:
|
elif image.base64:
|
||||||
try:
|
try:
|
||||||
@@ -339,7 +391,7 @@ class WecomClient:
|
|||||||
data = response.json()
|
data = response.json()
|
||||||
if data['errcode'] == 40014 or data['errcode'] == 42001:
|
if data['errcode'] == 40014 or data['errcode'] == 42001:
|
||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
media_id = await self.upload_to_work(image)
|
media_id = await self.upload_image_to_work(image)
|
||||||
if data.get('errcode', 0) != 0:
|
if data.get('errcode', 0) != 0:
|
||||||
await self.logger.error(f'上传图片失败:{data}')
|
await self.logger.error(f'上传图片失败:{data}')
|
||||||
raise Exception('failed to upload file')
|
raise Exception('failed to upload file')
|
||||||
@@ -347,13 +399,128 @@ class WecomClient:
|
|||||||
media_id = data.get('media_id')
|
media_id = data.get('media_id')
|
||||||
return media_id
|
return media_id
|
||||||
|
|
||||||
async def download_image_to_bytes(self, url: str) -> bytes:
|
async def upload_voice_to_work(self, voice: platform_message.Voice):
|
||||||
|
"""
|
||||||
|
上传语音文件到企业微信
|
||||||
|
"""
|
||||||
|
if not await self.check_access_token():
|
||||||
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
|
url = self.base_url + '/media/upload?access_token=' + self.access_token + '&type=file'
|
||||||
|
file_bytes = None
|
||||||
|
file_name = 'voice.mp3'
|
||||||
|
|
||||||
|
if voice.path:
|
||||||
|
async with aiofiles.open(voice.path, 'rb') as f:
|
||||||
|
file_bytes = await f.read()
|
||||||
|
file_name = voice.path.split('/')[-1]
|
||||||
|
elif voice.url:
|
||||||
|
file_bytes = await self.download_media_to_bytes(voice.url)
|
||||||
|
file_name = voice.url.split('/')[-1]
|
||||||
|
elif voice.base64:
|
||||||
|
try:
|
||||||
|
base64_data = voice.base64
|
||||||
|
if ',' in base64_data:
|
||||||
|
base64_data = base64_data.split(',', 1)[1]
|
||||||
|
padding = 4 - (len(base64_data) % 4) if len(base64_data) % 4 else 0
|
||||||
|
padded_base64 = base64_data + '=' * padding
|
||||||
|
file_bytes = base64.b64decode(padded_base64)
|
||||||
|
except binascii.Error as e:
|
||||||
|
raise ValueError(f'Invalid base64 string: {str(e)}')
|
||||||
|
else:
|
||||||
|
await self.logger.error('Voice对象出错')
|
||||||
|
raise ValueError('voice对象出错')
|
||||||
|
|
||||||
|
boundary = '-------------------------acebdf13572468'
|
||||||
|
headers = {'Content-Type': f'multipart/form-data; boundary={boundary}'}
|
||||||
|
body = (
|
||||||
|
(
|
||||||
|
f'--{boundary}\r\n'
|
||||||
|
f'Content-Disposition: form-data; name="media"; filename="{file_name}"; filelength={len(file_bytes)}\r\n'
|
||||||
|
f'Content-Type: application/octet-stream\r\n\r\n'
|
||||||
|
).encode('utf-8')
|
||||||
|
+ file_bytes
|
||||||
|
+ f'\r\n--{boundary}--\r\n'.encode('utf-8')
|
||||||
|
)
|
||||||
|
|
||||||
|
# print(body)
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(url, headers=headers, content=body)
|
||||||
|
data = response.json()
|
||||||
|
if data['errcode'] == 40014 or data['errcode'] == 42001:
|
||||||
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
|
media_id = await self.upload_voice_to_work(voice)
|
||||||
|
if data.get('errcode', 0) != 0:
|
||||||
|
await self.logger.error(f'上传语音文件失败:{data}')
|
||||||
|
raise Exception('failed to upload file')
|
||||||
|
media_id = data.get('media_id')
|
||||||
|
return media_id
|
||||||
|
|
||||||
|
async def upload_file_to_work(self, file: platform_message.File):
|
||||||
|
"""
|
||||||
|
上传文件到企业微信
|
||||||
|
"""
|
||||||
|
if not await self.check_access_token():
|
||||||
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
|
url = self.base_url + '/media/upload?access_token=' + self.access_token + '&type=file'
|
||||||
|
file_bytes = None
|
||||||
|
file_name = 'file.txt'
|
||||||
|
if file.path:
|
||||||
|
async with aiofiles.open(file.path, 'rb') as f:
|
||||||
|
file_bytes = await f.read()
|
||||||
|
file_name = file.path.split('/')[-1]
|
||||||
|
elif file.url:
|
||||||
|
file_bytes = await self.download_media_to_bytes(file.url)
|
||||||
|
file_name = file.url.split('/')[-1]
|
||||||
|
elif file.base64:
|
||||||
|
try:
|
||||||
|
base64_data = file.base64
|
||||||
|
if ',' in base64_data:
|
||||||
|
base64_data = base64_data.split(',', 1)[1]
|
||||||
|
padding = 4 - (len(base64_data) % 4) if len(base64_data) % 4 else 0
|
||||||
|
padded_base64 = base64_data + '=' * padding
|
||||||
|
file_bytes = base64.b64decode(padded_base64)
|
||||||
|
except binascii.Error as e:
|
||||||
|
raise ValueError(f'Invalid base64 string: {str(e)}')
|
||||||
|
else:
|
||||||
|
await self.logger.error('File对象出错')
|
||||||
|
raise ValueError('file对象出错')
|
||||||
|
boundary = '-------------------------acebdf13572468'
|
||||||
|
headers = {'Content-Type': f'multipart/form-data; boundary={boundary}'}
|
||||||
|
body = (
|
||||||
|
(
|
||||||
|
f'--{boundary}\r\n'
|
||||||
|
f'Content-Disposition: form-data; name="media"; filename="{file_name}"; filelength={len(file_bytes)}\r\n'
|
||||||
|
f'Content-Type: application/octet-stream\r\n\r\n'
|
||||||
|
).encode('utf-8')
|
||||||
|
+ file_bytes
|
||||||
|
+ f'\r\n--{boundary}--\r\n'.encode('utf-8')
|
||||||
|
)
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(url, headers=headers, content=body)
|
||||||
|
data = response.json()
|
||||||
|
if data['errcode'] == 40014 or data['errcode'] == 42001:
|
||||||
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
|
media_id = await self.upload_file_to_work(file)
|
||||||
|
if data.get('errcode', 0) != 0:
|
||||||
|
await self.logger.error(f'上传文件失败:{data}')
|
||||||
|
raise Exception('failed to upload file')
|
||||||
|
media_id = data.get('media_id')
|
||||||
|
return media_id
|
||||||
|
|
||||||
|
async def download_media_to_bytes(self, url: str) -> bytes:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
response = await client.get(url)
|
response = await client.get(url)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.content
|
return response.content
|
||||||
|
|
||||||
# 进行media_id的获取
|
# 进行media_id的获取
|
||||||
async def get_media_id(self, image: platform_message.Image):
|
async def get_media_id(self, media: platform_message.Image | platform_message.Voice | platform_message.File):
|
||||||
media_id = await self.upload_to_work(image=image)
|
if isinstance(media, platform_message.Image):
|
||||||
|
media_id = await self.upload_image_to_work(image=media)
|
||||||
|
elif isinstance(media, platform_message.Voice):
|
||||||
|
media_id = await self.upload_voice_to_work(voice=media)
|
||||||
|
elif isinstance(media, platform_message.File):
|
||||||
|
media_id = await self.upload_file_to_work(file=media)
|
||||||
|
else:
|
||||||
|
raise ValueError('Unsupported media type')
|
||||||
return media_id
|
return media_id
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from langbot_plugin.api.entities.builtin.provider import message as provider_mes
|
|||||||
import langbot_plugin.api.entities.events as events
|
import langbot_plugin.api.entities.events as events
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
||||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
||||||
|
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
||||||
|
|
||||||
|
|
||||||
@stage.stage_class('PreProcessor')
|
@stage.stage_class('PreProcessor')
|
||||||
@@ -74,12 +75,26 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
self.ap.logger.debug(f'Bound MCP servers: {bound_mcp_servers}')
|
self.ap.logger.debug(f'Bound MCP servers: {bound_mcp_servers}')
|
||||||
self.ap.logger.debug(f'Use funcs: {query.use_funcs}')
|
self.ap.logger.debug(f'Use funcs: {query.use_funcs}')
|
||||||
|
|
||||||
|
sender_name = ''
|
||||||
|
|
||||||
|
if isinstance(query.message_event, platform_events.GroupMessage):
|
||||||
|
sender_name = query.message_event.sender.member_name
|
||||||
|
elif isinstance(query.message_event, platform_events.FriendMessage):
|
||||||
|
sender_name = query.message_event.sender.nickname
|
||||||
|
|
||||||
variables = {
|
variables = {
|
||||||
|
'launcher_type': query.session.launcher_type.value,
|
||||||
|
'launcher_id': query.session.launcher_id,
|
||||||
|
'sender_id': query.sender_id,
|
||||||
'session_id': f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
'session_id': f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
||||||
'conversation_id': conversation.uuid,
|
'conversation_id': conversation.uuid,
|
||||||
'msg_create_time': (
|
'msg_create_time': (
|
||||||
int(query.message_event.time) if query.message_event.time else int(datetime.datetime.now().timestamp())
|
int(query.message_event.time) if query.message_event.time else int(datetime.datetime.now().timestamp())
|
||||||
),
|
),
|
||||||
|
'group_name': query.message_event.group.name
|
||||||
|
if isinstance(query.message_event, platform_events.GroupMessage)
|
||||||
|
else '',
|
||||||
|
'sender_name': sender_name,
|
||||||
}
|
}
|
||||||
query.variables.update(variables)
|
query.variables.update(variables)
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class ChatMessageHandler(handler.MessageHandler):
|
|||||||
runner = r(self.ap, query.pipeline_config)
|
runner = r(self.ap, query.pipeline_config)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise ValueError(f'未找到请求运行器: {query.pipeline_config["ai"]["runner"]["runner"]}')
|
raise ValueError(f'Request Runner not found: {query.pipeline_config["ai"]["runner"]["runner"]}')
|
||||||
if is_stream:
|
if is_stream:
|
||||||
resp_message_id = uuid.uuid4()
|
resp_message_id = uuid.uuid4()
|
||||||
|
|
||||||
@@ -91,7 +91,9 @@ class ChatMessageHandler(handler.MessageHandler):
|
|||||||
await query.adapter.create_message_card(str(resp_message_id), query.message_event)
|
await query.adapter.create_message_card(str(resp_message_id), query.message_event)
|
||||||
is_create_card = True
|
is_create_card = True
|
||||||
query.resp_messages.append(result)
|
query.resp_messages.append(result)
|
||||||
self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}')
|
self.ap.logger.info(
|
||||||
|
f'Conversation({query.query_id}) Streaming Response: {self.cut_str(result.readable_str())}'
|
||||||
|
)
|
||||||
|
|
||||||
if result.content is not None:
|
if result.content is not None:
|
||||||
text_length += len(result.content)
|
text_length += len(result.content)
|
||||||
@@ -102,7 +104,9 @@ class ChatMessageHandler(handler.MessageHandler):
|
|||||||
async for result in runner.run(query):
|
async for result in runner.run(query):
|
||||||
query.resp_messages.append(result)
|
query.resp_messages.append(result)
|
||||||
|
|
||||||
self.ap.logger.info(f'对话({query.query_id})响应: {self.cut_str(result.readable_str())}')
|
self.ap.logger.info(
|
||||||
|
f'Conversation({query.query_id}) Response: {self.cut_str(result.readable_str())}'
|
||||||
|
)
|
||||||
|
|
||||||
if result.content is not None:
|
if result.content is not None:
|
||||||
text_length += len(result.content)
|
text_length += len(result.content)
|
||||||
@@ -113,7 +117,7 @@ class ChatMessageHandler(handler.MessageHandler):
|
|||||||
|
|
||||||
query.session.using_conversation.messages.extend(query.resp_messages)
|
query.session.using_conversation.messages.extend(query.resp_messages)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.ap.logger.error(f'对话({query.query_id})请求失败: {type(e).__name__} {str(e)}')
|
self.ap.logger.error(f'Conversation({query.query_id}) Request Failed: {type(e).__name__} {str(e)}')
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
hide_exception_info = query.pipeline_config['output']['misc']['hide-exception']
|
hide_exception_info = query.pipeline_config['output']['misc']['hide-exception']
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import base64
|
|||||||
import uuid
|
import uuid
|
||||||
import os
|
import os
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
# 使用BytesIO创建文件对象,避免路径问题
|
||||||
|
import io
|
||||||
import asyncio
|
import asyncio
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
@@ -594,7 +597,7 @@ class DiscordMessageConverter(abstract_platform_adapter.AbstractMessageConverter
|
|||||||
break
|
break
|
||||||
|
|
||||||
text_string = ''
|
text_string = ''
|
||||||
image_files = []
|
files = []
|
||||||
|
|
||||||
for ele in message_chain:
|
for ele in message_chain:
|
||||||
if isinstance(ele, platform_message.Image):
|
if isinstance(ele, platform_message.Image):
|
||||||
@@ -668,22 +671,67 @@ class DiscordMessageConverter(abstract_platform_adapter.AbstractMessageConverter
|
|||||||
continue # 跳过读取失败的文件
|
continue # 跳过读取失败的文件
|
||||||
|
|
||||||
if image_bytes:
|
if image_bytes:
|
||||||
# 使用BytesIO创建文件对象,避免路径问题
|
files.append(discord.File(fp=io.BytesIO(image_bytes), filename=filename))
|
||||||
import io
|
|
||||||
|
|
||||||
image_files.append(discord.File(fp=io.BytesIO(image_bytes), filename=filename))
|
|
||||||
elif isinstance(ele, platform_message.Plain):
|
elif isinstance(ele, platform_message.Plain):
|
||||||
text_string += ele.text
|
text_string += ele.text
|
||||||
|
elif isinstance(ele, platform_message.Voice):
|
||||||
|
file_bytes = None
|
||||||
|
filename = f'{uuid.uuid4()}.mp3'
|
||||||
|
if ele.base64:
|
||||||
|
if ele.base64.startswith('data:'):
|
||||||
|
data_header = ele.base64.split(',')[0]
|
||||||
|
if 'wav' in data_header:
|
||||||
|
filename = f'{uuid.uuid4()}.wav'
|
||||||
|
elif 'mp3' in data_header:
|
||||||
|
filename = f'{uuid.uuid4()}.mp3'
|
||||||
|
elif 'ogg' in data_header:
|
||||||
|
filename = f'{uuid.uuid4()}.ogg'
|
||||||
|
elif 'm4a' in data_header:
|
||||||
|
filename = f'{uuid.uuid4()}.m4a'
|
||||||
|
elif 'aac' in data_header:
|
||||||
|
filename = f'{uuid.uuid4()}.aac'
|
||||||
|
elif 'flac' in data_header:
|
||||||
|
filename = f'{uuid.uuid4()}.flac'
|
||||||
|
elif 'alac' in data_header:
|
||||||
|
filename = f'{uuid.uuid4()}.alac'
|
||||||
|
elif 'opus' in data_header:
|
||||||
|
filename = f'{uuid.uuid4()}.opus'
|
||||||
|
elif 'webm' in data_header:
|
||||||
|
filename = f'{uuid.uuid4()}.webm'
|
||||||
|
|
||||||
|
file_base64 = ele.base64.split(',')[-1]
|
||||||
|
file_bytes = base64.b64decode(file_base64)
|
||||||
|
elif ele.url:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(ele.url) as response:
|
||||||
|
file_bytes = await response.read()
|
||||||
|
if file_bytes:
|
||||||
|
files.append(discord.File(fp=io.BytesIO(file_bytes), filename=filename))
|
||||||
|
elif isinstance(ele, platform_message.File):
|
||||||
|
file_bytes = None
|
||||||
|
filename = f'{uuid.uuid4()}.{ele.name.split(".")[-1]}'
|
||||||
|
if ele.base64:
|
||||||
|
if ele.base64.startswith('data:'):
|
||||||
|
file_base64 = ele.base64.split(',')[1]
|
||||||
|
file_bytes = base64.b64decode(file_base64)
|
||||||
|
else:
|
||||||
|
file_bytes = base64.b64decode(ele.base64)
|
||||||
|
elif ele.url:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(ele.url) as response:
|
||||||
|
file_bytes = await response.read()
|
||||||
|
if file_bytes:
|
||||||
|
files.append(discord.File(fp=io.BytesIO(file_bytes), filename=filename))
|
||||||
elif isinstance(ele, platform_message.Forward):
|
elif isinstance(ele, platform_message.Forward):
|
||||||
for node in ele.node_list:
|
for node in ele.node_list:
|
||||||
(
|
(
|
||||||
node_text,
|
node_text,
|
||||||
node_images,
|
node_files,
|
||||||
) = await DiscordMessageConverter.yiri2target(node.message_chain)
|
) = await DiscordMessageConverter.yiri2target(node.message_chain)
|
||||||
text_string += node_text
|
text_string += node_text
|
||||||
image_files.extend(node_images)
|
files.extend(node_files)
|
||||||
|
|
||||||
return text_string, image_files
|
return text_string, files
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def target2yiri(message: discord.Message) -> platform_message.MessageChain:
|
async def target2yiri(message: discord.Message) -> platform_message.MessageChain:
|
||||||
@@ -990,7 +1038,7 @@ class DiscordAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
await self.voice_manager.cleanup_inactive_connections()
|
await self.voice_manager.cleanup_inactive_connections()
|
||||||
|
|
||||||
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
||||||
msg_to_send, image_files = await self.message_converter.yiri2target(message)
|
msg_to_send, files = await self.message_converter.yiri2target(message)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 获取频道对象
|
# 获取频道对象
|
||||||
@@ -1003,8 +1051,8 @@ class DiscordAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
'content': msg_to_send,
|
'content': msg_to_send,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(image_files) > 0:
|
if len(files) > 0:
|
||||||
args['files'] = image_files
|
args['files'] = files
|
||||||
|
|
||||||
await channel.send(**args)
|
await channel.send(**args)
|
||||||
|
|
||||||
@@ -1018,15 +1066,16 @@ class DiscordAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
message: platform_message.MessageChain,
|
message: platform_message.MessageChain,
|
||||||
quote_origin: bool = False,
|
quote_origin: bool = False,
|
||||||
):
|
):
|
||||||
msg_to_send, image_files = await self.message_converter.yiri2target(message)
|
msg_to_send, files = await self.message_converter.yiri2target(message)
|
||||||
|
|
||||||
assert isinstance(message_source.source_platform_object, discord.Message)
|
assert isinstance(message_source.source_platform_object, discord.Message)
|
||||||
|
|
||||||
args = {
|
args = {
|
||||||
'content': msg_to_send,
|
'content': msg_to_send,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(image_files) > 0:
|
if len(files) > 0:
|
||||||
args['files'] = image_files
|
args['files'] = files
|
||||||
|
|
||||||
if quote_origin:
|
if quote_origin:
|
||||||
args['reference'] = message_source.source_platform_object
|
args['reference'] = message_source.source_platform_object
|
||||||
|
|||||||
@@ -137,7 +137,11 @@ class KookMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
|||||||
# For file messages, content is typically the file URL
|
# For file messages, content is typically the file URL
|
||||||
attachments = extra.get('attachments', {})
|
attachments = extra.get('attachments', {})
|
||||||
file_name = attachments.get('name', 'file')
|
file_name = attachments.get('name', 'file')
|
||||||
components.append(platform_message.Plain(text=f'[File: {file_name}]'))
|
components.append(platform_message.File(url=content, name=file_name))
|
||||||
|
elif msg_type == 8: # Audio message
|
||||||
|
# For audio messages, content is typically the audio URL
|
||||||
|
attachments = extra.get('attachments', {})
|
||||||
|
components.append(platform_message.Voice(url=content))
|
||||||
elif msg_type == 9: # KMarkdown message
|
elif msg_type == 9: # KMarkdown message
|
||||||
# Note: content is already stripped of mention patterns above
|
# Note: content is already stripped of mention patterns above
|
||||||
if content:
|
if content:
|
||||||
@@ -317,9 +321,6 @@ class KookAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
data = await response.json()
|
data = await response.json()
|
||||||
if data.get('code') == 0:
|
if data.get('code') == 0:
|
||||||
user_info = data['data']
|
user_info = data['data']
|
||||||
await self.logger.info(
|
|
||||||
f'Retrieved bot user info: {user_info.get("username")} (ID: {user_info.get("id")})'
|
|
||||||
)
|
|
||||||
return user_info
|
return user_info
|
||||||
else:
|
else:
|
||||||
raise Exception(f'Failed to get bot user info: {data.get("message")}')
|
raise Exception(f'Failed to get bot user info: {data.get("message")}')
|
||||||
@@ -343,11 +344,10 @@ class KookAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
|
|
||||||
# Ignore messages from bot itself to prevent infinite loops
|
# Ignore messages from bot itself to prevent infinite loops
|
||||||
if self.bot_account_id and str(author_id) == self.bot_account_id:
|
if self.bot_account_id and str(author_id) == self.bot_account_id:
|
||||||
await self.logger.debug(f'Ignoring message from bot itself (author_id: {author_id})')
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Only process text messages (type 1, 2, 4, 9, 10) in GROUP or PERSON channels
|
# Only process text messages (type 1, 2, 4, 8, 9, 10) in GROUP or PERSON channels
|
||||||
if event_type in [1, 2, 4, 9, 10] and channel_type in ['GROUP', 'PERSON']:
|
if event_type in [1, 2, 4, 8, 9, 10] and channel_type in ['GROUP', 'PERSON']:
|
||||||
try:
|
try:
|
||||||
# Convert to LangBot event
|
# Convert to LangBot event
|
||||||
lb_event = await self.event_converter.target2yiri(data, self.bot_account_id)
|
lb_event = await self.event_converter.target2yiri(data, self.bot_account_id)
|
||||||
@@ -377,7 +377,6 @@ class KookAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
'sn': self.current_sn,
|
'sn': self.current_sn,
|
||||||
}
|
}
|
||||||
await self.ws.send(json.dumps(ping_msg))
|
await self.ws.send(json.dumps(ping_msg))
|
||||||
await self.logger.debug(f'Sent PING with sn={self.current_sn}')
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# Connection closed or send failed, exit loop
|
# Connection closed or send failed, exit loop
|
||||||
break
|
break
|
||||||
@@ -398,10 +397,9 @@ class KookAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
self.gateway_url = await self._get_gateway_url()
|
self.gateway_url = await self._get_gateway_url()
|
||||||
|
|
||||||
# Connect to WebSocket
|
# Connect to WebSocket
|
||||||
await self.logger.info(f'Connecting to KOOK WebSocket: {self.gateway_url}')
|
|
||||||
async with websockets.connect(self.gateway_url) as ws:
|
async with websockets.connect(self.gateway_url) as ws:
|
||||||
|
await self.logger.info(f'Connected to KOOK WebSocket: {self.gateway_url}')
|
||||||
self.ws = ws
|
self.ws = ws
|
||||||
await self.logger.info('KOOK WebSocket connected')
|
|
||||||
|
|
||||||
# Start heartbeat
|
# Start heartbeat
|
||||||
self.heartbeat_task = asyncio.create_task(self._heartbeat_loop())
|
self.heartbeat_task = asyncio.create_task(self._heartbeat_loop())
|
||||||
@@ -452,10 +450,11 @@ class KookAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
elif signal == 3: # PONG
|
elif signal == 3: # PONG
|
||||||
await self._handle_pong(msg_data.get('d', {}))
|
await self._handle_pong(msg_data.get('d', {}))
|
||||||
elif signal == 5: # RECONNECT
|
elif signal == 5: # RECONNECT
|
||||||
await self.logger.info('Received RECONNECT signal')
|
# await self.logger.info('Received RECONNECT signal')
|
||||||
break # Break to reconnect
|
break # Break to reconnect
|
||||||
elif signal == 6: # RESUME ACK
|
elif signal == 6: # RESUME ACK
|
||||||
await self.logger.info('Resume successful')
|
# await self.logger.info('Resume successful')
|
||||||
|
pass
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
await self.logger.error(f'Failed to parse message: {message}')
|
await self.logger.error(f'Failed to parse message: {message}')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -568,6 +567,8 @@ class KookAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
if quote_origin and msg_id:
|
if quote_origin and msg_id:
|
||||||
payload['quote'] = msg_id
|
payload['quote'] = msg_id
|
||||||
|
|
||||||
|
payload['reply_msg_id'] = msg_id
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
'Authorization': f'Bot {self.config["token"]}',
|
'Authorization': f'Bot {self.config["token"]}',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -297,6 +297,10 @@ class LarkMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
|||||||
message_content['content'] = new_list
|
message_content['content'] = new_list
|
||||||
elif message.message_type == 'image':
|
elif message.message_type == 'image':
|
||||||
message_content['content'] = [{'tag': 'img', 'image_key': message_content['image_key'], 'style': []}]
|
message_content['content'] = [{'tag': 'img', 'image_key': message_content['image_key'], 'style': []}]
|
||||||
|
elif message.message_type == 'file':
|
||||||
|
message_content['content'] = [
|
||||||
|
{'tag': 'file', 'file_key': message_content['file_key'], 'file_name': message_content['file_name']}
|
||||||
|
]
|
||||||
|
|
||||||
for ele in message_content['content']:
|
for ele in message_content['content']:
|
||||||
if ele['tag'] == 'text':
|
if ele['tag'] == 'text':
|
||||||
@@ -327,6 +331,33 @@ class LarkMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
|||||||
image_format = response.raw.headers['content-type']
|
image_format = response.raw.headers['content-type']
|
||||||
|
|
||||||
lb_msg_list.append(platform_message.Image(base64=f'data:{image_format};base64,{image_base64}'))
|
lb_msg_list.append(platform_message.Image(base64=f'data:{image_format};base64,{image_base64}'))
|
||||||
|
elif ele['tag'] == 'file':
|
||||||
|
file_key = ele['file_key']
|
||||||
|
file_name = ele['file_name']
|
||||||
|
|
||||||
|
request: GetMessageResourceRequest = (
|
||||||
|
GetMessageResourceRequest.builder()
|
||||||
|
.message_id(message.message_id)
|
||||||
|
.file_key(file_key)
|
||||||
|
.type('file')
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
response: GetMessageResourceResponse = await api_client.im.v1.message_resource.aget(request)
|
||||||
|
|
||||||
|
if not response.success():
|
||||||
|
raise Exception(
|
||||||
|
f'client.im.v1.message_resource.get failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
||||||
|
)
|
||||||
|
|
||||||
|
file_bytes = response.file.read()
|
||||||
|
file_base64 = base64.b64encode(file_bytes).decode()
|
||||||
|
|
||||||
|
file_format = response.raw.headers['content-type']
|
||||||
|
|
||||||
|
lb_msg_list.append(
|
||||||
|
platform_message.File(base64=f'data:{file_format};base64,{file_base64}', name=file_name)
|
||||||
|
)
|
||||||
|
|
||||||
return platform_message.MessageChain(lb_msg_list)
|
return platform_message.MessageChain(lb_msg_list)
|
||||||
|
|
||||||
|
|||||||
@@ -259,19 +259,6 @@ class LINEAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
# 保持运行但不启动独立端口
|
# 保持运行但不启动独立端口
|
||||||
|
|
||||||
# 打印 webhook 回调地址
|
# 打印 webhook 回调地址
|
||||||
if self.bot_uuid and hasattr(self.logger, 'ap'):
|
|
||||||
try:
|
|
||||||
api_port = self.logger.ap.instance_config.data['api']['port']
|
|
||||||
webhook_url = f'http://127.0.0.1:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
webhook_url_public = f'http://<Your-Public-IP>:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
|
|
||||||
await self.logger.info('LINE Webhook 回调地址:')
|
|
||||||
await self.logger.info(f' 本地地址: {webhook_url}')
|
|
||||||
await self.logger.info(f' 公网地址: {webhook_url_public}')
|
|
||||||
await self.logger.info('请在 LINE 后台配置此回调地址')
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.warning(f'无法生成 webhook URL: {e}')
|
|
||||||
|
|
||||||
async def keep_alive():
|
async def keep_alive():
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|||||||
@@ -155,20 +155,6 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd
|
|||||||
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
||||||
# 保持运行但不启动独立端口
|
# 保持运行但不启动独立端口
|
||||||
|
|
||||||
# 打印 webhook 回调地址
|
|
||||||
if self.bot_uuid and hasattr(self.logger, 'ap'):
|
|
||||||
try:
|
|
||||||
api_port = self.logger.ap.instance_config.data['api']['port']
|
|
||||||
webhook_url = f'http://127.0.0.1:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
webhook_url_public = f'http://<Your-Public-IP>:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
|
|
||||||
await self.logger.info('微信公众号 Webhook 回调地址:')
|
|
||||||
await self.logger.info(f' 本地地址: {webhook_url}')
|
|
||||||
await self.logger.info(f' 公网地址: {webhook_url_public}')
|
|
||||||
await self.logger.info('请在微信公众号后台配置此回调地址')
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.warning(f'无法生成 webhook URL: {e}')
|
|
||||||
|
|
||||||
async def keep_alive():
|
async def keep_alive():
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|||||||
@@ -241,20 +241,6 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter
|
|||||||
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
||||||
# 保持运行但不启动独立端口
|
# 保持运行但不启动独立端口
|
||||||
|
|
||||||
# 打印 webhook 回调地址
|
|
||||||
if self.bot_uuid and hasattr(self.logger, 'ap'):
|
|
||||||
try:
|
|
||||||
api_port = self.logger.ap.instance_config.data['api']['port']
|
|
||||||
webhook_url = f'http://127.0.0.1:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
webhook_url_public = f'http://<Your-Public-IP>:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
|
|
||||||
await self.logger.info('QQ 官方机器人 Webhook 回调地址:')
|
|
||||||
await self.logger.info(f' 本地地址: {webhook_url}')
|
|
||||||
await self.logger.info(f' 公网地址: {webhook_url_public}')
|
|
||||||
await self.logger.info('请在 QQ 官方机器人后台配置此回调地址')
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.warning(f'无法生成 webhook URL: {e}')
|
|
||||||
|
|
||||||
async def keep_alive():
|
async def keep_alive():
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|||||||
@@ -188,21 +188,6 @@ class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
async def run_async(self):
|
async def run_async(self):
|
||||||
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
||||||
# 保持运行但不启动独立端口
|
# 保持运行但不启动独立端口
|
||||||
|
|
||||||
# 打印 webhook 回调地址
|
|
||||||
if self.bot_uuid and hasattr(self.logger, 'ap'):
|
|
||||||
try:
|
|
||||||
api_port = self.logger.ap.instance_config.data['api']['port']
|
|
||||||
webhook_url = f'http://127.0.0.1:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
webhook_url_public = f'http://<Your-Public-IP>:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
|
|
||||||
await self.logger.info('Slack 机器人 Webhook 回调地址:')
|
|
||||||
await self.logger.info(f' 本地地址: {webhook_url}')
|
|
||||||
await self.logger.info(f' 公网地址: {webhook_url_public}')
|
|
||||||
await self.logger.info('请在 Slack 后台配置此回调地址')
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.warning(f'无法生成 webhook URL: {e}')
|
|
||||||
|
|
||||||
async def keep_alive():
|
async def keep_alive():
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|||||||
@@ -97,8 +97,6 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
|||||||
# 推送到所有相关连接
|
# 推送到所有相关连接
|
||||||
await self.outbound_message_queue.put(message_data)
|
await self.outbound_message_queue.put(message_data)
|
||||||
|
|
||||||
await self.logger.info(f'Send message to {target_id}: {message}')
|
|
||||||
|
|
||||||
return message_data
|
return message_data
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
@@ -242,7 +240,6 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
|||||||
|
|
||||||
async def run_async(self):
|
async def run_async(self):
|
||||||
"""运行适配器"""
|
"""运行适配器"""
|
||||||
await self.logger.info('WebSocket适配器已启动')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
@@ -258,12 +255,11 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
|||||||
|
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
await self.logger.info('WebSocket适配器已停止')
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def kill(self):
|
async def kill(self):
|
||||||
"""停止适配器"""
|
"""停止适配器"""
|
||||||
await self.logger.info('WebSocket适配器正在停止')
|
pass
|
||||||
|
|
||||||
async def _process_image_components(self, message_chain_obj: list):
|
async def _process_image_components(self, message_chain_obj: list):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -35,6 +35,20 @@ class WecomMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
|||||||
'media_id': await bot.get_media_id(msg),
|
'media_id': await bot.get_media_id(msg),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
elif type(msg) is platform_message.Voice:
|
||||||
|
content_list.append(
|
||||||
|
{
|
||||||
|
'type': 'voice',
|
||||||
|
'media_id': await bot.get_media_id(msg),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif type(msg) is platform_message.File:
|
||||||
|
content_list.append(
|
||||||
|
{
|
||||||
|
'type': 'file',
|
||||||
|
'media_id': await bot.get_media_id(msg),
|
||||||
|
}
|
||||||
|
)
|
||||||
elif type(msg) is platform_message.Forward:
|
elif type(msg) is platform_message.Forward:
|
||||||
for node in msg.node_list:
|
for node in msg.node_list:
|
||||||
content_list.extend((await WecomMessageConverter.yiri2target(node.message_chain, bot)))
|
content_list.extend((await WecomMessageConverter.yiri2target(node.message_chain, bot)))
|
||||||
@@ -185,6 +199,10 @@ class WecomAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
await self.bot.send_private_msg(fixed_user_id, Wecom_event.agent_id, content['content'])
|
await self.bot.send_private_msg(fixed_user_id, Wecom_event.agent_id, content['content'])
|
||||||
elif content['type'] == 'image':
|
elif content['type'] == 'image':
|
||||||
await self.bot.send_image(fixed_user_id, Wecom_event.agent_id, content['media_id'])
|
await self.bot.send_image(fixed_user_id, Wecom_event.agent_id, content['media_id'])
|
||||||
|
elif content['type'] == 'voice':
|
||||||
|
await self.bot.send_voice(fixed_user_id, Wecom_event.agent_id, content['media_id'])
|
||||||
|
elif content['type'] == 'file':
|
||||||
|
await self.bot.send_file(fixed_user_id, Wecom_event.agent_id, content['media_id'])
|
||||||
|
|
||||||
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
||||||
content_list = await WecomMessageConverter.yiri2target(message, self.bot)
|
content_list = await WecomMessageConverter.yiri2target(message, self.bot)
|
||||||
@@ -197,6 +215,10 @@ class WecomAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
await self.bot.send_private_msg(user_id, agent_id, content['content'])
|
await self.bot.send_private_msg(user_id, agent_id, content['content'])
|
||||||
if content['type'] == 'image':
|
if content['type'] == 'image':
|
||||||
await self.bot.send_image(user_id, agent_id, content['media'])
|
await self.bot.send_image(user_id, agent_id, content['media'])
|
||||||
|
if content['type'] == 'voice':
|
||||||
|
await self.bot.send_voice(user_id, agent_id, content['media'])
|
||||||
|
if content['type'] == 'file':
|
||||||
|
await self.bot.send_file(user_id, agent_id, content['media'])
|
||||||
|
|
||||||
def register_listener(
|
def register_listener(
|
||||||
self,
|
self,
|
||||||
@@ -232,19 +254,6 @@ class WecomAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
return await self.bot.handle_unified_webhook(request)
|
return await self.bot.handle_unified_webhook(request)
|
||||||
|
|
||||||
async def run_async(self):
|
async def run_async(self):
|
||||||
if self.bot_uuid and hasattr(self.logger, 'ap'):
|
|
||||||
try:
|
|
||||||
api_port = self.logger.ap.instance_config.data['api']['port']
|
|
||||||
webhook_url = f'http://127.0.0.1:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
webhook_url_public = f'http://<Your-Public-IP>:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
|
|
||||||
await self.logger.info('企业微信 Webhook 回调地址:')
|
|
||||||
await self.logger.info(f' 本地地址: {webhook_url}')
|
|
||||||
await self.logger.info(f' 公网地址: {webhook_url_public}')
|
|
||||||
await self.logger.info('请在企业微信后台配置此回调地址')
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.warning(f'无法生成 webhook URL: {e}')
|
|
||||||
|
|
||||||
async def keep_alive():
|
async def keep_alive():
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class WecomBotMessageConverter(abstract_platform_adapter.AbstractMessageConverte
|
|||||||
voice_payload = voice_info.get('base64') or voice_info.get('url')
|
voice_payload = voice_info.get('base64') or voice_info.get('url')
|
||||||
if voice_payload:
|
if voice_payload:
|
||||||
if voice_info.get('base64') and not voice_payload.startswith('data:'):
|
if voice_info.get('base64') and not voice_payload.startswith('data:'):
|
||||||
voice_payload = f"data:audio/mpeg;base64,{voice_info.get('base64')}"
|
voice_payload = f'data:audio/mpeg;base64,{voice_info.get("base64")}'
|
||||||
try:
|
try:
|
||||||
yiri_msg_list.append(platform_message.Voice(base64=voice_payload))
|
yiri_msg_list.append(platform_message.Voice(base64=voice_payload))
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -113,7 +113,10 @@ class WecomBotMessageConverter(abstract_platform_adapter.AbstractMessageConverte
|
|||||||
if event.msgtype == 'link' and event.link:
|
if event.msgtype == 'link' and event.link:
|
||||||
link = event.link
|
link = event.link
|
||||||
summary = '\n'.join(
|
summary = '\n'.join(
|
||||||
filter(None, [link.get('title', ''), link.get('description') or link.get('digest', ''), link.get('url', '')])
|
filter(
|
||||||
|
None,
|
||||||
|
[link.get('title', ''), link.get('description') or link.get('digest', ''), link.get('url', '')],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if summary:
|
if summary:
|
||||||
yiri_msg_list.append(platform_message.Plain(text=summary))
|
yiri_msg_list.append(platform_message.Plain(text=summary))
|
||||||
@@ -301,20 +304,6 @@ class WecomBotAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
||||||
# 保持运行但不启动独立端口
|
# 保持运行但不启动独立端口
|
||||||
|
|
||||||
# 打印 webhook 回调地址
|
|
||||||
if self.bot_uuid and hasattr(self.logger, 'ap'):
|
|
||||||
try:
|
|
||||||
api_port = self.logger.ap.instance_config.data['api']['port']
|
|
||||||
webhook_url = f'http://127.0.0.1:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
webhook_url_public = f'http://<Your-Public-IP>:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
|
|
||||||
await self.logger.info('企业微信机器人 Webhook 回调地址:')
|
|
||||||
await self.logger.info(f' 本地地址: {webhook_url}')
|
|
||||||
await self.logger.info(f' 公网地址: {webhook_url_public}')
|
|
||||||
await self.logger.info('请在企业微信后台配置此回调地址')
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.warning(f'无法生成 webhook URL: {e}')
|
|
||||||
|
|
||||||
async def keep_alive():
|
async def keep_alive():
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|||||||
@@ -213,23 +213,10 @@ class WecomCSAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
||||||
# 保持运行但不启动独立端口
|
# 保持运行但不启动独立端口
|
||||||
|
|
||||||
# 打印 webhook 回调地址
|
|
||||||
if self.bot_uuid and hasattr(self.logger, 'ap'):
|
|
||||||
try:
|
|
||||||
api_port = self.logger.ap.instance_config.data['api']['port']
|
|
||||||
webhook_url = f"http://127.0.0.1:{api_port}/bots/{self.bot_uuid}"
|
|
||||||
webhook_url_public = f"http://<Your-Public-IP>:{api_port}/bots/{self.bot_uuid}"
|
|
||||||
|
|
||||||
await self.logger.info(f"企业微信客服 Webhook 回调地址:")
|
|
||||||
await self.logger.info(f" 本地地址: {webhook_url}")
|
|
||||||
await self.logger.info(f" 公网地址: {webhook_url_public}")
|
|
||||||
await self.logger.info(f"请在企业微信后台配置此回调地址")
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.warning(f"无法生成 webhook URL: {e}")
|
|
||||||
|
|
||||||
async def keep_alive():
|
async def keep_alive():
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
await keep_alive()
|
await keep_alive()
|
||||||
|
|
||||||
async def kill(self) -> bool:
|
async def kill(self) -> bool:
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ class LangflowAPIRunner(runner.RequestRunner):
|
|||||||
if is_stream:
|
if is_stream:
|
||||||
# 流式请求
|
# 流式请求
|
||||||
async with client.stream('POST', url, json=payload, headers=headers, timeout=120.0) as response:
|
async with client.stream('POST', url, json=payload, headers=headers, timeout=120.0) as response:
|
||||||
print(response)
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
accumulated_content = ''
|
accumulated_content = ''
|
||||||
|
|||||||
@@ -44,12 +44,35 @@ export function BotLogCard({ botLog }: { botLog: BotLog }) {
|
|||||||
const strArr = str.split('');
|
const strArr = str.split('');
|
||||||
return strArr;
|
return strArr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据日志级别返回对应的样式类
|
||||||
|
function getLevelStyles(level: string) {
|
||||||
|
switch (level.toLowerCase()) {
|
||||||
|
case 'error':
|
||||||
|
return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400';
|
||||||
|
case 'warning':
|
||||||
|
return 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400';
|
||||||
|
case 'info':
|
||||||
|
return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400';
|
||||||
|
case 'debug':
|
||||||
|
return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.botLogCardContainer}`}>
|
<div className={`${styles.botLogCardContainer}`}>
|
||||||
{/* 头部标签,时间 */}
|
{/* 头部标签,时间 */}
|
||||||
<div className={`${styles.cardTitleContainer}`}>
|
<div className={`${styles.cardTitleContainer}`}>
|
||||||
<div className={`flex flex-row gap-2 items-center`}>
|
<div className={`flex flex-row gap-2 items-center`}>
|
||||||
<div className={`${styles.tag}`}>{botLog.level}</div>
|
<div
|
||||||
|
className={`px-2 py-1 rounded text-xs font-medium uppercase ${getLevelStyles(
|
||||||
|
botLog.level,
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
{botLog.level}
|
||||||
|
</div>
|
||||||
{botLog.message_session_id && (
|
{botLog.message_session_id && (
|
||||||
<div
|
<div
|
||||||
className={`${styles.tag} ${styles.chatTag}`}
|
className={`${styles.tag} ${styles.chatTag}`}
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { BotLogManager } from '@/app/home/bots/components/bot-log/BotLogManager';
|
import { BotLogManager } from '@/app/home/bots/components/bot-log/BotLogManager';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
||||||
import { BotLog } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
import { BotLog } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
||||||
import { BotLogCard } from '@/app/home/bots/components/bot-log/view/BotLogCard';
|
import { BotLogCard } from '@/app/home/bots/components/bot-log/view/BotLogCard';
|
||||||
import styles from './botLog.module.css';
|
import styles from './botLog.module.css';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/components/ui/popover';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { ChevronDownIcon } from 'lucide-react';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@@ -14,9 +22,21 @@ export function BotLogListComponent({ botId }: { botId: string }) {
|
|||||||
const manager = useRef(new BotLogManager(botId)).current;
|
const manager = useRef(new BotLogManager(botId)).current;
|
||||||
const [botLogList, setBotLogList] = useState<BotLog[]>([]);
|
const [botLogList, setBotLogList] = useState<BotLog[]>([]);
|
||||||
const [autoFlush, setAutoFlush] = useState(true);
|
const [autoFlush, setAutoFlush] = useState(true);
|
||||||
|
const [selectedLevels, setSelectedLevels] = useState<string[]>([
|
||||||
|
'info',
|
||||||
|
'warning',
|
||||||
|
'error',
|
||||||
|
]);
|
||||||
const listContainerRef = useRef<HTMLDivElement>(null);
|
const listContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const botLogListRef = useRef<BotLog[]>(botLogList);
|
const botLogListRef = useRef<BotLog[]>(botLogList);
|
||||||
|
|
||||||
|
const logLevels = [
|
||||||
|
{ value: 'error', label: 'ERROR' },
|
||||||
|
{ value: 'warning', label: 'WARNING' },
|
||||||
|
{ value: 'info', label: 'INFO' },
|
||||||
|
{ value: 'debug', label: 'DEBUG' },
|
||||||
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initComponent();
|
initComponent();
|
||||||
return () => {
|
return () => {
|
||||||
@@ -28,6 +48,42 @@ export function BotLogListComponent({ botId }: { botId: string }) {
|
|||||||
botLogListRef.current = botLogList;
|
botLogListRef.current = botLogList;
|
||||||
}, [botLogList]);
|
}, [botLogList]);
|
||||||
|
|
||||||
|
// 根据级别过滤日志
|
||||||
|
const filteredLogs = useMemo(() => {
|
||||||
|
if (selectedLevels.length === 0) {
|
||||||
|
return botLogList;
|
||||||
|
}
|
||||||
|
return botLogList.filter((log) => selectedLevels.includes(log.level));
|
||||||
|
}, [botLogList, selectedLevels]);
|
||||||
|
|
||||||
|
const handleLevelToggle = (levelValue: string) => {
|
||||||
|
setSelectedLevels((prev) => {
|
||||||
|
if (prev.includes(levelValue)) {
|
||||||
|
return prev.filter((l) => l !== levelValue);
|
||||||
|
} else {
|
||||||
|
return [...prev, levelValue];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDisplayText = () => {
|
||||||
|
if (selectedLevels.length === 0) {
|
||||||
|
return t('bots.selectLevel');
|
||||||
|
}
|
||||||
|
if (selectedLevels.length === logLevels.length) {
|
||||||
|
return t('bots.allLevels');
|
||||||
|
}
|
||||||
|
// 如果选中3个或以上,显示数量
|
||||||
|
if (selectedLevels.length >= 3) {
|
||||||
|
return `${selectedLevels.length} ${t('bots.levelsSelected')}`;
|
||||||
|
}
|
||||||
|
// 显示选中级别的标签(大写形式)
|
||||||
|
return logLevels
|
||||||
|
.filter((level) => selectedLevels.includes(level.value))
|
||||||
|
.map((level) => level.label)
|
||||||
|
.join(', ');
|
||||||
|
};
|
||||||
|
|
||||||
// 观测自动刷新状态
|
// 观测自动刷新状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoFlush) {
|
if (autoFlush) {
|
||||||
@@ -116,9 +172,43 @@ export function BotLogListComponent({ botId }: { botId: string }) {
|
|||||||
<div className={`${styles.listHeader}`}>
|
<div className={`${styles.listHeader}`}>
|
||||||
<div className={'mr-2'}>{t('bots.enableAutoRefresh')}</div>
|
<div className={'mr-2'}>{t('bots.enableAutoRefresh')}</div>
|
||||||
<Switch checked={autoFlush} onCheckedChange={(e) => setAutoFlush(e)} />
|
<Switch checked={autoFlush} onCheckedChange={(e) => setAutoFlush(e)} />
|
||||||
|
<div className={'ml-4 mr-2'}>{t('bots.logLevel')}</div>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-[180px] flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<span className="text-sm truncate flex-1 text-left">
|
||||||
|
{getDisplayText()}
|
||||||
|
</span>
|
||||||
|
<ChevronDownIcon className="ml-2 h-4 w-4 flex-shrink-0" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[180px] p-2">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{logLevels.map((level) => (
|
||||||
|
<div key={level.value} className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id={level.value}
|
||||||
|
checked={selectedLevels.includes(level.value)}
|
||||||
|
onCheckedChange={() => handleLevelToggle(level.value)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={level.value}
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
||||||
|
>
|
||||||
|
{level.label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{botLogList.map((botLog) => {
|
{filteredLogs.map((botLog) => {
|
||||||
return <BotLogCard botLog={botLog} key={botLog.seq_id} />;
|
return <BotLogCard botLog={botLog} key={botLog.seq_id} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -192,6 +192,10 @@ const enUS = {
|
|||||||
webhookUrlCopied: 'Webhook URL copied',
|
webhookUrlCopied: 'Webhook URL copied',
|
||||||
webhookUrlHint:
|
webhookUrlHint:
|
||||||
'Click the input to select all, then press Ctrl+C (Mac: Cmd+C) to copy, or click the button',
|
'Click the input to select all, then press Ctrl+C (Mac: Cmd+C) to copy, or click the button',
|
||||||
|
logLevel: 'Log Level',
|
||||||
|
allLevels: 'All Levels',
|
||||||
|
selectLevel: 'Select Level',
|
||||||
|
levelsSelected: 'levels selected',
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: 'Extensions',
|
title: 'Extensions',
|
||||||
|
|||||||
@@ -194,6 +194,10 @@ const jaJP = {
|
|||||||
webhookUrlCopied: 'Webhook URL をコピーしました',
|
webhookUrlCopied: 'Webhook URL をコピーしました',
|
||||||
webhookUrlHint:
|
webhookUrlHint:
|
||||||
'入力ボックスをクリックして全選択し、Ctrl+C (Mac: Cmd+C) でコピーするか、右側のボタンをクリックしてください',
|
'入力ボックスをクリックして全選択し、Ctrl+C (Mac: Cmd+C) でコピーするか、右側のボタンをクリックしてください',
|
||||||
|
logLevel: 'ログレベル',
|
||||||
|
allLevels: 'すべてのレベル',
|
||||||
|
selectLevel: 'レベルを選択',
|
||||||
|
levelsSelected: 'レベル選択済み',
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: '拡張機能',
|
title: '拡張機能',
|
||||||
|
|||||||
@@ -187,6 +187,10 @@ const zhHans = {
|
|||||||
webhookUrlCopied: 'Webhook 地址已复制',
|
webhookUrlCopied: 'Webhook 地址已复制',
|
||||||
webhookUrlHint:
|
webhookUrlHint:
|
||||||
'点击输入框自动全选,然后按 Ctrl+C (Mac: Cmd+C) 复制,或点击右侧按钮',
|
'点击输入框自动全选,然后按 Ctrl+C (Mac: Cmd+C) 复制,或点击右侧按钮',
|
||||||
|
logLevel: '日志级别',
|
||||||
|
allLevels: '全部级别',
|
||||||
|
selectLevel: '选择级别',
|
||||||
|
levelsSelected: '个级别已选',
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: '插件扩展',
|
title: '插件扩展',
|
||||||
|
|||||||
@@ -187,6 +187,10 @@ const zhHant = {
|
|||||||
webhookUrlCopied: 'Webhook 位址已複製',
|
webhookUrlCopied: 'Webhook 位址已複製',
|
||||||
webhookUrlHint:
|
webhookUrlHint:
|
||||||
'點擊輸入框自動全選,然後按 Ctrl+C (Mac: Cmd+C) 複製,或點擊右側按鈕',
|
'點擊輸入框自動全選,然後按 Ctrl+C (Mac: Cmd+C) 複製,或點擊右側按鈕',
|
||||||
|
logLevel: '日誌級別',
|
||||||
|
allLevels: '全部級別',
|
||||||
|
selectLevel: '選擇級別',
|
||||||
|
levelsSelected: '個級別已選',
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: '外掛擴展',
|
title: '外掛擴展',
|
||||||
|
|||||||
Reference in New Issue
Block a user