mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +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:
|
||||
self.wxcpt = WXBizMsgCrypt(self.Token, self.EnCodingAESKey, '')
|
||||
await self.logger.info(f'{req.method} {req.url} {str(req.args)}')
|
||||
|
||||
if req.method == 'GET':
|
||||
return await self._handle_get_callback(req)
|
||||
|
||||
@@ -139,6 +139,58 @@ class WecomClient:
|
||||
await self.logger.error(f'发送图片失败:{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):
|
||||
if not await self.check_access_token():
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
@@ -287,7 +339,7 @@ class WecomClient:
|
||||
return ext
|
||||
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
|
||||
"""
|
||||
@@ -304,7 +356,7 @@ class WecomClient:
|
||||
file_bytes = await f.read()
|
||||
file_name = image.path.split('/')[-1]
|
||||
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]
|
||||
elif image.base64:
|
||||
try:
|
||||
@@ -339,7 +391,7 @@ class WecomClient:
|
||||
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_to_work(image)
|
||||
media_id = await self.upload_image_to_work(image)
|
||||
if data.get('errcode', 0) != 0:
|
||||
await self.logger.error(f'上传图片失败:{data}')
|
||||
raise Exception('failed to upload file')
|
||||
@@ -347,13 +399,128 @@ class WecomClient:
|
||||
media_id = data.get('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:
|
||||
response = await client.get(url)
|
||||
response.raise_for_status()
|
||||
return response.content
|
||||
|
||||
# 进行media_id的获取
|
||||
async def get_media_id(self, image: platform_message.Image):
|
||||
media_id = await self.upload_to_work(image=image)
|
||||
async def get_media_id(self, media: platform_message.Image | platform_message.Voice | platform_message.File):
|
||||
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
|
||||
|
||||
@@ -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.builtin.platform.message as platform_message
|
||||
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')
|
||||
@@ -74,12 +75,26 @@ class PreProcessor(stage.PipelineStage):
|
||||
self.ap.logger.debug(f'Bound MCP servers: {bound_mcp_servers}')
|
||||
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 = {
|
||||
'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}',
|
||||
'conversation_id': conversation.uuid,
|
||||
'msg_create_time': (
|
||||
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)
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class ChatMessageHandler(handler.MessageHandler):
|
||||
runner = r(self.ap, query.pipeline_config)
|
||||
break
|
||||
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:
|
||||
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)
|
||||
is_create_card = True
|
||||
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:
|
||||
text_length += len(result.content)
|
||||
@@ -102,7 +104,9 @@ class ChatMessageHandler(handler.MessageHandler):
|
||||
async for result in runner.run(query):
|
||||
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:
|
||||
text_length += len(result.content)
|
||||
@@ -113,7 +117,7 @@ class ChatMessageHandler(handler.MessageHandler):
|
||||
|
||||
query.session.using_conversation.messages.extend(query.resp_messages)
|
||||
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()
|
||||
|
||||
hide_exception_info = query.pipeline_config['output']['misc']['hide-exception']
|
||||
|
||||
@@ -8,6 +8,9 @@ import base64
|
||||
import uuid
|
||||
import os
|
||||
import datetime
|
||||
|
||||
# 使用BytesIO创建文件对象,避免路径问题
|
||||
import io
|
||||
import asyncio
|
||||
from enum import Enum
|
||||
|
||||
@@ -594,7 +597,7 @@ class DiscordMessageConverter(abstract_platform_adapter.AbstractMessageConverter
|
||||
break
|
||||
|
||||
text_string = ''
|
||||
image_files = []
|
||||
files = []
|
||||
|
||||
for ele in message_chain:
|
||||
if isinstance(ele, platform_message.Image):
|
||||
@@ -668,22 +671,67 @@ class DiscordMessageConverter(abstract_platform_adapter.AbstractMessageConverter
|
||||
continue # 跳过读取失败的文件
|
||||
|
||||
if image_bytes:
|
||||
# 使用BytesIO创建文件对象,避免路径问题
|
||||
import io
|
||||
|
||||
image_files.append(discord.File(fp=io.BytesIO(image_bytes), filename=filename))
|
||||
files.append(discord.File(fp=io.BytesIO(image_bytes), filename=filename))
|
||||
elif isinstance(ele, platform_message.Plain):
|
||||
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):
|
||||
for node in ele.node_list:
|
||||
(
|
||||
node_text,
|
||||
node_images,
|
||||
node_files,
|
||||
) = await DiscordMessageConverter.yiri2target(node.message_chain)
|
||||
text_string += node_text
|
||||
image_files.extend(node_images)
|
||||
files.extend(node_files)
|
||||
|
||||
return text_string, image_files
|
||||
return text_string, files
|
||||
|
||||
@staticmethod
|
||||
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()
|
||||
|
||||
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:
|
||||
# 获取频道对象
|
||||
@@ -1003,8 +1051,8 @@ class DiscordAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
'content': msg_to_send,
|
||||
}
|
||||
|
||||
if len(image_files) > 0:
|
||||
args['files'] = image_files
|
||||
if len(files) > 0:
|
||||
args['files'] = files
|
||||
|
||||
await channel.send(**args)
|
||||
|
||||
@@ -1018,15 +1066,16 @@ class DiscordAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
message: platform_message.MessageChain,
|
||||
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)
|
||||
|
||||
args = {
|
||||
'content': msg_to_send,
|
||||
}
|
||||
|
||||
if len(image_files) > 0:
|
||||
args['files'] = image_files
|
||||
if len(files) > 0:
|
||||
args['files'] = files
|
||||
|
||||
if quote_origin:
|
||||
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
|
||||
attachments = extra.get('attachments', {})
|
||||
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
|
||||
# Note: content is already stripped of mention patterns above
|
||||
if content:
|
||||
@@ -317,9 +321,6 @@ class KookAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
data = await response.json()
|
||||
if data.get('code') == 0:
|
||||
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
|
||||
else:
|
||||
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
|
||||
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
|
||||
|
||||
# Only process text messages (type 1, 2, 4, 9, 10) in GROUP or PERSON channels
|
||||
if event_type in [1, 2, 4, 9, 10] and channel_type in ['GROUP', 'PERSON']:
|
||||
# Only process text messages (type 1, 2, 4, 8, 9, 10) in GROUP or PERSON channels
|
||||
if event_type in [1, 2, 4, 8, 9, 10] and channel_type in ['GROUP', 'PERSON']:
|
||||
try:
|
||||
# Convert to LangBot event
|
||||
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,
|
||||
}
|
||||
await self.ws.send(json.dumps(ping_msg))
|
||||
await self.logger.debug(f'Sent PING with sn={self.current_sn}')
|
||||
except Exception:
|
||||
# Connection closed or send failed, exit loop
|
||||
break
|
||||
@@ -398,10 +397,9 @@ class KookAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
self.gateway_url = await self._get_gateway_url()
|
||||
|
||||
# Connect to WebSocket
|
||||
await self.logger.info(f'Connecting to KOOK WebSocket: {self.gateway_url}')
|
||||
async with websockets.connect(self.gateway_url) as ws:
|
||||
await self.logger.info(f'Connected to KOOK WebSocket: {self.gateway_url}')
|
||||
self.ws = ws
|
||||
await self.logger.info('KOOK WebSocket connected')
|
||||
|
||||
# Start heartbeat
|
||||
self.heartbeat_task = asyncio.create_task(self._heartbeat_loop())
|
||||
@@ -452,10 +450,11 @@ class KookAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
elif signal == 3: # PONG
|
||||
await self._handle_pong(msg_data.get('d', {}))
|
||||
elif signal == 5: # RECONNECT
|
||||
await self.logger.info('Received RECONNECT signal')
|
||||
# await self.logger.info('Received RECONNECT signal')
|
||||
break # Break to reconnect
|
||||
elif signal == 6: # RESUME ACK
|
||||
await self.logger.info('Resume successful')
|
||||
# await self.logger.info('Resume successful')
|
||||
pass
|
||||
except json.JSONDecodeError:
|
||||
await self.logger.error(f'Failed to parse message: {message}')
|
||||
except Exception as e:
|
||||
@@ -568,6 +567,8 @@ class KookAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
if quote_origin and msg_id:
|
||||
payload['quote'] = msg_id
|
||||
|
||||
payload['reply_msg_id'] = msg_id
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bot {self.config["token"]}',
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -297,6 +297,10 @@ class LarkMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
||||
message_content['content'] = new_list
|
||||
elif message.message_type == 'image':
|
||||
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']:
|
||||
if ele['tag'] == 'text':
|
||||
@@ -327,6 +331,33 @@ class LarkMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
||||
image_format = response.raw.headers['content-type']
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -259,19 +259,6 @@ class LINEAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
# 保持运行但不启动独立端口
|
||||
|
||||
# 打印 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():
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
@@ -155,20 +155,6 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd
|
||||
# 统一 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():
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
@@ -241,20 +241,6 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter
|
||||
# 统一 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():
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
@@ -188,21 +188,6 @@ class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
async def run_async(self):
|
||||
# 统一 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():
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
@@ -97,8 +97,6 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
||||
# 推送到所有相关连接
|
||||
await self.outbound_message_queue.put(message_data)
|
||||
|
||||
await self.logger.info(f'Send message to {target_id}: {message}')
|
||||
|
||||
return message_data
|
||||
|
||||
async def reply_message(
|
||||
@@ -242,7 +240,6 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
||||
|
||||
async def run_async(self):
|
||||
"""运行适配器"""
|
||||
await self.logger.info('WebSocket适配器已启动')
|
||||
|
||||
try:
|
||||
while True:
|
||||
@@ -258,12 +255,11 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
except asyncio.CancelledError:
|
||||
await self.logger.info('WebSocket适配器已停止')
|
||||
raise
|
||||
|
||||
async def kill(self):
|
||||
"""停止适配器"""
|
||||
await self.logger.info('WebSocket适配器正在停止')
|
||||
pass
|
||||
|
||||
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),
|
||||
}
|
||||
)
|
||||
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:
|
||||
for node in msg.node_list:
|
||||
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'])
|
||||
elif content['type'] == 'image':
|
||||
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):
|
||||
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'])
|
||||
if content['type'] == 'image':
|
||||
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(
|
||||
self,
|
||||
@@ -232,19 +254,6 @@ class WecomAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
return await self.bot.handle_unified_webhook(request)
|
||||
|
||||
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():
|
||||
while True:
|
||||
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')
|
||||
if voice_payload:
|
||||
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:
|
||||
yiri_msg_list.append(platform_message.Voice(base64=voice_payload))
|
||||
except Exception:
|
||||
@@ -113,7 +113,10 @@ class WecomBotMessageConverter(abstract_platform_adapter.AbstractMessageConverte
|
||||
if event.msgtype == 'link' and event.link:
|
||||
link = event.link
|
||||
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:
|
||||
yiri_msg_list.append(platform_message.Plain(text=summary))
|
||||
@@ -301,20 +304,6 @@ class WecomBotAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
# 统一 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():
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
@@ -213,23 +213,10 @@ class WecomCSAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
# 统一 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():
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await keep_alive()
|
||||
|
||||
async def kill(self) -> bool:
|
||||
|
||||
@@ -94,7 +94,6 @@ class LangflowAPIRunner(runner.RequestRunner):
|
||||
if is_stream:
|
||||
# 流式请求
|
||||
async with client.stream('POST', url, json=payload, headers=headers, timeout=120.0) as response:
|
||||
print(response)
|
||||
response.raise_for_status()
|
||||
|
||||
accumulated_content = ''
|
||||
|
||||
@@ -44,12 +44,35 @@ export function BotLogCard({ botLog }: { botLog: BotLog }) {
|
||||
const strArr = str.split('');
|
||||
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 (
|
||||
<div className={`${styles.botLogCardContainer}`}>
|
||||
{/* 头部标签,时间 */}
|
||||
<div className={`${styles.cardTitleContainer}`}>
|
||||
<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 && (
|
||||
<div
|
||||
className={`${styles.tag} ${styles.chatTag}`}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
'use client';
|
||||
|
||||
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 { BotLogCard } from '@/app/home/bots/components/bot-log/view/BotLogCard';
|
||||
import styles from './botLog.module.css';
|
||||
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 { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -14,9 +22,21 @@ export function BotLogListComponent({ botId }: { botId: string }) {
|
||||
const manager = useRef(new BotLogManager(botId)).current;
|
||||
const [botLogList, setBotLogList] = useState<BotLog[]>([]);
|
||||
const [autoFlush, setAutoFlush] = useState(true);
|
||||
const [selectedLevels, setSelectedLevels] = useState<string[]>([
|
||||
'info',
|
||||
'warning',
|
||||
'error',
|
||||
]);
|
||||
const listContainerRef = useRef<HTMLDivElement>(null);
|
||||
const botLogListRef = useRef<BotLog[]>(botLogList);
|
||||
|
||||
const logLevels = [
|
||||
{ value: 'error', label: 'ERROR' },
|
||||
{ value: 'warning', label: 'WARNING' },
|
||||
{ value: 'info', label: 'INFO' },
|
||||
{ value: 'debug', label: 'DEBUG' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
initComponent();
|
||||
return () => {
|
||||
@@ -28,6 +48,42 @@ export function BotLogListComponent({ botId }: { botId: string }) {
|
||||
botLogListRef.current = 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(() => {
|
||||
if (autoFlush) {
|
||||
@@ -116,9 +172,43 @@ export function BotLogListComponent({ botId }: { botId: string }) {
|
||||
<div className={`${styles.listHeader}`}>
|
||||
<div className={'mr-2'}>{t('bots.enableAutoRefresh')}</div>
|
||||
<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>
|
||||
|
||||
{botLogList.map((botLog) => {
|
||||
{filteredLogs.map((botLog) => {
|
||||
return <BotLogCard botLog={botLog} key={botLog.seq_id} />;
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -192,6 +192,10 @@ const enUS = {
|
||||
webhookUrlCopied: 'Webhook URL copied',
|
||||
webhookUrlHint:
|
||||
'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: {
|
||||
title: 'Extensions',
|
||||
|
||||
@@ -194,6 +194,10 @@ const jaJP = {
|
||||
webhookUrlCopied: 'Webhook URL をコピーしました',
|
||||
webhookUrlHint:
|
||||
'入力ボックスをクリックして全選択し、Ctrl+C (Mac: Cmd+C) でコピーするか、右側のボタンをクリックしてください',
|
||||
logLevel: 'ログレベル',
|
||||
allLevels: 'すべてのレベル',
|
||||
selectLevel: 'レベルを選択',
|
||||
levelsSelected: 'レベル選択済み',
|
||||
},
|
||||
plugins: {
|
||||
title: '拡張機能',
|
||||
|
||||
@@ -187,6 +187,10 @@ const zhHans = {
|
||||
webhookUrlCopied: 'Webhook 地址已复制',
|
||||
webhookUrlHint:
|
||||
'点击输入框自动全选,然后按 Ctrl+C (Mac: Cmd+C) 复制,或点击右侧按钮',
|
||||
logLevel: '日志级别',
|
||||
allLevels: '全部级别',
|
||||
selectLevel: '选择级别',
|
||||
levelsSelected: '个级别已选',
|
||||
},
|
||||
plugins: {
|
||||
title: '插件扩展',
|
||||
|
||||
@@ -187,6 +187,10 @@ const zhHant = {
|
||||
webhookUrlCopied: 'Webhook 位址已複製',
|
||||
webhookUrlHint:
|
||||
'點擊輸入框自動全選,然後按 Ctrl+C (Mac: Cmd+C) 複製,或點擊右側按鈕',
|
||||
logLevel: '日誌級別',
|
||||
allLevels: '全部級別',
|
||||
selectLevel: '選擇級別',
|
||||
levelsSelected: '個級別已選',
|
||||
},
|
||||
plugins: {
|
||||
title: '外掛擴展',
|
||||
|
||||
Reference in New Issue
Block a user