mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-08 06:46:02 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
239223be3f | ||
|
|
b112cb320c | ||
|
|
5aaf2ba3ef | ||
|
|
f1e9f46af1 | ||
|
|
8dfef1d118 | ||
|
|
919a621bf8 | ||
|
|
3ac96f464d | ||
|
|
f9f03b81d1 | ||
|
|
42171a9c07 | ||
|
|
f1f00115c9 |
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -7,7 +7,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: 运行环境
|
label: 运行环境
|
||||||
description: LangBot 版本、操作系统、系统架构、**Python版本**、**主机地理位置**
|
description: LangBot 版本、操作系统、系统架构、**Python版本**、**主机地理位置**
|
||||||
placeholder: 例如:v3.3.0、CentOS x64 Python 3.10.3、Docker 的系统直接写 Docker 就行
|
placeholder: 例如:v3.3.0、CentOS x64 Python 3.10.3、Docker
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/bug-report_en.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report_en.yml
vendored
@@ -7,7 +7,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Runtime environment
|
label: Runtime environment
|
||||||
description: LangBot version, operating system, system architecture, **Python version**, **host location**
|
description: LangBot version, operating system, system architecture, **Python version**, **host location**
|
||||||
placeholder: "For example: v3.3.0, CentOS x64 Python 3.10.3, Docker system directly write Docker"
|
placeholder: "For example: v3.3.0, CentOS x64 Python 3.10.3, Docker"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class DingTalkClient:
|
|||||||
robot_name: str,
|
robot_name: str,
|
||||||
robot_code: str,
|
robot_code: str,
|
||||||
markdown_card: bool,
|
markdown_card: bool,
|
||||||
|
logger: None,
|
||||||
):
|
):
|
||||||
"""初始化 WebSocket 连接并自动启动"""
|
"""初始化 WebSocket 连接并自动启动"""
|
||||||
self.credential = dingtalk_stream.Credential(client_id, client_secret)
|
self.credential = dingtalk_stream.Credential(client_id, client_secret)
|
||||||
@@ -34,6 +35,7 @@ class DingTalkClient:
|
|||||||
self.robot_code = robot_code
|
self.robot_code = robot_code
|
||||||
self.access_token_expiry_time = ''
|
self.access_token_expiry_time = ''
|
||||||
self.markdown_card = markdown_card
|
self.markdown_card = markdown_card
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def get_access_token(self):
|
async def get_access_token(self):
|
||||||
url = 'https://api.dingtalk.com/v1.0/oauth2/accessToken'
|
url = 'https://api.dingtalk.com/v1.0/oauth2/accessToken'
|
||||||
@@ -48,7 +50,7 @@ class DingTalkClient:
|
|||||||
expires_in = int(response_data.get('expireIn', 7200))
|
expires_in = int(response_data.get('expireIn', 7200))
|
||||||
self.access_token_expiry_time = time.time() + expires_in - 60
|
self.access_token_expiry_time = time.time() + expires_in - 60
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(e)
|
await self.logger.error("failed to get access token in dingtalk")
|
||||||
|
|
||||||
async def is_token_expired(self):
|
async def is_token_expired(self):
|
||||||
"""检查token是否过期"""
|
"""检查token是否过期"""
|
||||||
@@ -73,7 +75,7 @@ class DingTalkClient:
|
|||||||
result = response.json()
|
result = response.json()
|
||||||
download_url = result.get('downloadUrl')
|
download_url = result.get('downloadUrl')
|
||||||
else:
|
else:
|
||||||
raise Exception(f'Error: {response.status_code}, {response.text}')
|
await self.logger.error(f"failed to get download url: {response.json()}")
|
||||||
|
|
||||||
if download_url:
|
if download_url:
|
||||||
return await self.download_url_to_base64(download_url)
|
return await self.download_url_to_base64(download_url)
|
||||||
@@ -87,7 +89,7 @@ class DingTalkClient:
|
|||||||
base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式
|
base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式
|
||||||
return base64_str
|
return base64_str
|
||||||
else:
|
else:
|
||||||
raise Exception('获取文件失败')
|
await self.logger.error(f"failed to get files: {response.json()}")
|
||||||
|
|
||||||
async def get_audio_url(self, download_code: str):
|
async def get_audio_url(self, download_code: str):
|
||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
@@ -103,7 +105,7 @@ class DingTalkClient:
|
|||||||
if download_url:
|
if download_url:
|
||||||
return await self.download_url_to_base64(download_url)
|
return await self.download_url_to_base64(download_url)
|
||||||
else:
|
else:
|
||||||
raise Exception('获取音频失败')
|
await self.logger.error(f"failed to get audio: {response.json()}")
|
||||||
else:
|
else:
|
||||||
raise Exception(f'Error: {response.status_code}, {response.text}')
|
raise Exception(f'Error: {response.status_code}, {response.text}')
|
||||||
|
|
||||||
@@ -115,7 +117,7 @@ class DingTalkClient:
|
|||||||
if event:
|
if event:
|
||||||
await self._handle_message(event)
|
await self._handle_message(event)
|
||||||
|
|
||||||
async def send_message(self, content: str, incoming_message,at:bool):
|
async def send_message(self, content: str, incoming_message,at:bool):
|
||||||
if self.markdown_card:
|
if self.markdown_card:
|
||||||
if at:
|
if at:
|
||||||
self.EchoTextHandler.reply_markdown(
|
self.EchoTextHandler.reply_markdown(
|
||||||
@@ -190,8 +192,11 @@ class DingTalkClient:
|
|||||||
copy_message_data = message_data.copy()
|
copy_message_data = message_data.copy()
|
||||||
del copy_message_data['IncomingMessage']
|
del copy_message_data['IncomingMessage']
|
||||||
# print("message_data:", json.dumps(copy_message_data, indent=4, ensure_ascii=False))
|
# print("message_data:", json.dumps(copy_message_data, indent=4, ensure_ascii=False))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
if self.logger:
|
||||||
|
await self.logger.error(f"Error in get_message: {traceback.format_exc()}")
|
||||||
|
else:
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
return message_data
|
return message_data
|
||||||
|
|
||||||
@@ -214,9 +219,12 @@ class DingTalkClient:
|
|||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
await client.post(url, headers=headers, json=data)
|
response = await client.post(url, headers=headers, json=data)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"failed to send proactive massage to person: {traceback.format_exc()}")
|
||||||
|
raise Exception(f"failed to send proactive massage to person: {traceback.format_exc()}")
|
||||||
|
|
||||||
async def send_proactive_message_to_group(self, target_id: str, content: str):
|
async def send_proactive_message_to_group(self, target_id: str, content: str):
|
||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
@@ -237,9 +245,12 @@ class DingTalkClient:
|
|||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
await client.post(url, headers=headers, json=data)
|
response = await client.post(url, headers=headers, json=data)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"failed to send proactive massage to group: {traceback.format_exc()}")
|
||||||
|
raise Exception(f"failed to send proactive massage to group: {traceback.format_exc()}")
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""启动 WebSocket 连接,监听消息"""
|
"""启动 WebSocket 连接,监听消息"""
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ xml_template = """
|
|||||||
|
|
||||||
|
|
||||||
class OAClient:
|
class OAClient:
|
||||||
def __init__(self, token: str, EncodingAESKey: str, AppID: str, Appsecret: str):
|
def __init__(self, token: str, EncodingAESKey: str, AppID: str, Appsecret: str, logger: None):
|
||||||
self.token = token
|
self.token = token
|
||||||
self.aes = EncodingAESKey
|
self.aes = EncodingAESKey
|
||||||
self.appid = AppID
|
self.appid = AppID
|
||||||
@@ -43,6 +43,7 @@ class OAClient:
|
|||||||
self.access_token_expiry_time = None
|
self.access_token_expiry_time = None
|
||||||
self.msg_id_map = {}
|
self.msg_id_map = {}
|
||||||
self.generated_content = {}
|
self.generated_content = {}
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
try:
|
try:
|
||||||
@@ -54,6 +55,7 @@ class OAClient:
|
|||||||
echostr = request.args.get('echostr', '')
|
echostr = request.args.get('echostr', '')
|
||||||
msg_signature = request.args.get('msg_signature', '')
|
msg_signature = request.args.get('msg_signature', '')
|
||||||
if msg_signature is None:
|
if msg_signature is None:
|
||||||
|
await self.logger.error(f'msg_signature不在请求体中')
|
||||||
raise Exception('msg_signature不在请求体中')
|
raise Exception('msg_signature不在请求体中')
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
@@ -64,6 +66,7 @@ class OAClient:
|
|||||||
if check_signature == signature:
|
if check_signature == signature:
|
||||||
return echostr # 验证成功返回echostr
|
return echostr # 验证成功返回echostr
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f'拒绝请求')
|
||||||
raise Exception('拒绝请求')
|
raise Exception('拒绝请求')
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
encryt_msg = await request.data
|
encryt_msg = await request.data
|
||||||
@@ -72,8 +75,9 @@ class OAClient:
|
|||||||
xml_msg = xml_msg.decode('utf-8')
|
xml_msg = xml_msg.decode('utf-8')
|
||||||
|
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
|
await self.logger.error(f'消息解密失败')
|
||||||
raise Exception('消息解密失败')
|
raise Exception('消息解密失败')
|
||||||
|
|
||||||
message_data = await self.get_message(xml_msg)
|
message_data = await self.get_message(xml_msg)
|
||||||
if message_data:
|
if message_data:
|
||||||
event = OAEvent.from_payload(message_data)
|
event = OAEvent.from_payload(message_data)
|
||||||
@@ -114,6 +118,7 @@ class OAClient:
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
await self.logger.error(f'handle_callback_request失败: {traceback.format_exc()}')
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
async def get_message(self, xml_msg: str):
|
async def get_message(self, xml_msg: str):
|
||||||
@@ -176,6 +181,7 @@ class OAClientForLongerResponse:
|
|||||||
AppID: str,
|
AppID: str,
|
||||||
Appsecret: str,
|
Appsecret: str,
|
||||||
LoadingMessage: str,
|
LoadingMessage: str,
|
||||||
|
logger: None,
|
||||||
):
|
):
|
||||||
self.token = token
|
self.token = token
|
||||||
self.aes = EncodingAESKey
|
self.aes = EncodingAESKey
|
||||||
@@ -197,6 +203,7 @@ class OAClientForLongerResponse:
|
|||||||
self.loading_message = LoadingMessage
|
self.loading_message = LoadingMessage
|
||||||
self.msg_queue = {}
|
self.msg_queue = {}
|
||||||
self.user_msg_queue = {}
|
self.user_msg_queue = {}
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
try:
|
try:
|
||||||
@@ -207,6 +214,7 @@ class OAClientForLongerResponse:
|
|||||||
msg_signature = request.args.get('msg_signature', '')
|
msg_signature = request.args.get('msg_signature', '')
|
||||||
|
|
||||||
if msg_signature is None:
|
if msg_signature is None:
|
||||||
|
await self.logger.error(f'msg_signature不在请求体中')
|
||||||
raise Exception('msg_signature不在请求体中')
|
raise Exception('msg_signature不在请求体中')
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
@@ -221,7 +229,9 @@ class OAClientForLongerResponse:
|
|||||||
xml_msg = xml_msg.decode('utf-8')
|
xml_msg = xml_msg.decode('utf-8')
|
||||||
|
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
|
await self.logger.error(f'消息解密失败')
|
||||||
raise Exception('消息解密失败')
|
raise Exception('消息解密失败')
|
||||||
|
|
||||||
|
|
||||||
# 解析 XML
|
# 解析 XML
|
||||||
root = ET.fromstring(xml_msg)
|
root = ET.fromstring(xml_msg)
|
||||||
@@ -270,6 +280,7 @@ class OAClientForLongerResponse:
|
|||||||
return response_xml
|
return response_xml
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
await self.logger.error(f'handle_callback_request失败: {traceback.format_exc()}')
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
async def get_message(self, xml_msg: str):
|
async def get_message(self, xml_msg: str):
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def handle_validation(body: dict, bot_secret: str):
|
|||||||
|
|
||||||
|
|
||||||
class QQOfficialClient:
|
class QQOfficialClient:
|
||||||
def __init__(self, secret: str, token: str, app_id: str):
|
def __init__(self, secret: str, token: str, app_id: str, logger: None):
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
self.app.add_url_rule(
|
self.app.add_url_rule(
|
||||||
'/callback/command',
|
'/callback/command',
|
||||||
@@ -49,6 +49,7 @@ class QQOfficialClient:
|
|||||||
self.base_url = 'https://api.sgroup.qq.com'
|
self.base_url = 'https://api.sgroup.qq.com'
|
||||||
self.access_token = ''
|
self.access_token = ''
|
||||||
self.access_token_expiry_time = None
|
self.access_token_expiry_time = None
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def check_access_token(self):
|
async def check_access_token(self):
|
||||||
"""检查access_token是否存在"""
|
"""检查access_token是否存在"""
|
||||||
@@ -77,6 +78,7 @@ class QQOfficialClient:
|
|||||||
if access_token:
|
if access_token:
|
||||||
self.access_token = access_token
|
self.access_token = access_token
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f'获取access_token失败: {response_data}')
|
||||||
raise Exception(f'获取access_token失败: {e}')
|
raise Exception(f'获取access_token失败: {e}')
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
@@ -102,7 +104,7 @@ class QQOfficialClient:
|
|||||||
return {'code': 0, 'message': 'success'}
|
return {'code': 0, 'message': 'success'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
|
||||||
return {'error': str(e)}, 400
|
return {'error': str(e)}, 400
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||||
@@ -166,6 +168,7 @@ class QQOfficialClient:
|
|||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
await self.get_access_token()
|
await self.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
url = self.base_url + '/v2/users/' + user_openid + '/messages'
|
url = self.base_url + '/v2/users/' + user_openid + '/messages'
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
headers = {
|
headers = {
|
||||||
@@ -178,9 +181,11 @@ class QQOfficialClient:
|
|||||||
'msg_id': msg_id,
|
'msg_id': msg_id,
|
||||||
}
|
}
|
||||||
response = await client.post(url, headers=headers, json=data)
|
response = await client.post(url, headers=headers, json=data)
|
||||||
|
response_data = response.json()
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f'发送私聊消息失败: {response_data}')
|
||||||
raise ValueError(response)
|
raise ValueError(response)
|
||||||
|
|
||||||
async def send_group_text_msg(self, group_openid: str, content: str, msg_id: str):
|
async def send_group_text_msg(self, group_openid: str, content: str, msg_id: str):
|
||||||
@@ -188,6 +193,7 @@ class QQOfficialClient:
|
|||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
await self.get_access_token()
|
await self.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
url = self.base_url + '/v2/groups/' + group_openid + '/messages'
|
url = self.base_url + '/v2/groups/' + group_openid + '/messages'
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
headers = {
|
headers = {
|
||||||
@@ -203,6 +209,7 @@ class QQOfficialClient:
|
|||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f"发送群聊消息失败:{response.json()}")
|
||||||
raise Exception(response.read().decode())
|
raise Exception(response.read().decode())
|
||||||
|
|
||||||
async def send_channle_group_text_msg(self, channel_id: str, content: str, msg_id: str):
|
async def send_channle_group_text_msg(self, channel_id: str, content: str, msg_id: str):
|
||||||
@@ -210,6 +217,7 @@ class QQOfficialClient:
|
|||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
await self.get_access_token()
|
await self.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
url = self.base_url + '/channels/' + channel_id + '/messages'
|
url = self.base_url + '/channels/' + channel_id + '/messages'
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
headers = {
|
headers = {
|
||||||
@@ -225,12 +233,14 @@ class QQOfficialClient:
|
|||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f'发送频道群聊消息失败: {response.json()}')
|
||||||
raise Exception(response)
|
raise Exception(response)
|
||||||
|
|
||||||
async def send_channle_private_text_msg(self, guild_id: str, content: str, msg_id: str):
|
async def send_channle_private_text_msg(self, guild_id: str, content: str, msg_id: str):
|
||||||
"""发送频道私聊消息"""
|
"""发送频道私聊消息"""
|
||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
await self.get_access_token()
|
await self.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
url = self.base_url + '/dms/' + guild_id + '/messages'
|
url = self.base_url + '/dms/' + guild_id + '/messages'
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
@@ -247,6 +257,7 @@ class QQOfficialClient:
|
|||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f'发送频道私聊消息失败: {response.json()}')
|
||||||
raise Exception(response)
|
raise Exception(response)
|
||||||
|
|
||||||
async def is_token_expired(self):
|
async def is_token_expired(self):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
import traceback
|
||||||
from quart import Quart, jsonify, request
|
from quart import Quart, jsonify, request
|
||||||
from slack_sdk.web.async_client import AsyncWebClient
|
from slack_sdk.web.async_client import AsyncWebClient
|
||||||
from .slackevent import SlackEvent
|
from .slackevent import SlackEvent
|
||||||
@@ -7,7 +8,7 @@ from pkg.platform.types import events as platform_events
|
|||||||
|
|
||||||
|
|
||||||
class SlackClient:
|
class SlackClient:
|
||||||
def __init__(self, bot_token: str, signing_secret: str):
|
def __init__(self, bot_token: str, signing_secret: str, logger: None):
|
||||||
self.bot_token = bot_token
|
self.bot_token = bot_token
|
||||||
self.signing_secret = signing_secret
|
self.signing_secret = signing_secret
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
@@ -19,6 +20,7 @@ class SlackClient:
|
|||||||
'example': [],
|
'example': [],
|
||||||
}
|
}
|
||||||
self.bot_user_id = None # 避免机器人回复自己的消息
|
self.bot_user_id = None # 避免机器人回复自己的消息
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
try:
|
try:
|
||||||
@@ -32,6 +34,7 @@ class SlackClient:
|
|||||||
|
|
||||||
if self.bot_user_id and bot_user_id == self.bot_user_id:
|
if self.bot_user_id and bot_user_id == self.bot_user_id:
|
||||||
return jsonify({'status': 'ok'})
|
return jsonify({'status': 'ok'})
|
||||||
|
|
||||||
|
|
||||||
# 处理私信
|
# 处理私信
|
||||||
if data and data.get('event', {}).get('channel_type') in ['im']:
|
if data and data.get('event', {}).get('channel_type') in ['im']:
|
||||||
@@ -49,6 +52,7 @@ class SlackClient:
|
|||||||
return jsonify({'status': 'ok'})
|
return jsonify({'status': 'ok'})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
|
||||||
raise (e)
|
raise (e)
|
||||||
|
|
||||||
async def _handle_message(self, event: SlackEvent):
|
async def _handle_message(self, event: SlackEvent):
|
||||||
@@ -78,6 +82,7 @@ class SlackClient:
|
|||||||
self.bot_user_id = response['message']['bot_id']
|
self.bot_user_id = response['message']['bot_id']
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f"Error in send_message: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
async def send_message_to_one(self, text: str, user_id: str):
|
async def send_message_to_one(self, text: str, user_id: str):
|
||||||
@@ -88,6 +93,7 @@ class SlackClient:
|
|||||||
|
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f"Error in send_message: {traceback.format_exc()}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ from libs.wechatpad_api.api.chatroom import ChatRoomApi
|
|||||||
|
|
||||||
|
|
||||||
class WeChatPadClient:
|
class WeChatPadClient:
|
||||||
def __init__(self,base_url, token):
|
def __init__(self, base_url, token, logger=None):
|
||||||
self._login_api = LoginApi(base_url, token)
|
self._login_api = LoginApi(base_url, token)
|
||||||
self._friend_api = FriendApi(base_url, token)
|
self._friend_api = FriendApi(base_url, token)
|
||||||
self._message_api = MessageApi(base_url, token)
|
self._message_api = MessageApi(base_url, token)
|
||||||
self._user_api = UserApi(base_url, token)
|
self._user_api = UserApi(base_url, token)
|
||||||
self._download_api = DownloadApi(base_url, token)
|
self._download_api = DownloadApi(base_url, token)
|
||||||
self._chatroom_api = ChatRoomApi(base_url, token)
|
self._chatroom_api = ChatRoomApi(base_url, token)
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
def get_token(self,admin_key, day: int):
|
def get_token(self,admin_key, day: int):
|
||||||
'''获取token'''
|
'''获取token'''
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from .WXBizMsgCrypt3 import WXBizMsgCrypt
|
|||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
import httpx
|
import httpx
|
||||||
|
import traceback
|
||||||
from quart import Quart
|
from quart import Quart
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from typing import Callable, Dict, Any
|
from typing import Callable, Dict, Any
|
||||||
@@ -19,6 +20,7 @@ class WecomClient:
|
|||||||
token: str,
|
token: str,
|
||||||
EncodingAESKey: str,
|
EncodingAESKey: str,
|
||||||
contacts_secret: str,
|
contacts_secret: str,
|
||||||
|
logger: None,
|
||||||
):
|
):
|
||||||
self.corpid = corpid
|
self.corpid = corpid
|
||||||
self.secret = secret
|
self.secret = secret
|
||||||
@@ -28,6 +30,7 @@ class WecomClient:
|
|||||||
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
|
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
|
||||||
self.access_token = ''
|
self.access_token = ''
|
||||||
self.secret_for_contacts = contacts_secret
|
self.secret_for_contacts = contacts_secret
|
||||||
|
self.logger = logger
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
self.app.add_url_rule(
|
self.app.add_url_rule(
|
||||||
'/callback/command',
|
'/callback/command',
|
||||||
@@ -54,6 +57,7 @@ class WecomClient:
|
|||||||
if 'access_token' in data:
|
if 'access_token' in data:
|
||||||
return data['access_token']
|
return data['access_token']
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f"获取accesstoken失败:{response.json()}")
|
||||||
raise Exception(f'未获取access token: {data}')
|
raise Exception(f'未获取access token: {data}')
|
||||||
|
|
||||||
async def get_users(self):
|
async def get_users(self):
|
||||||
@@ -125,6 +129,7 @@ class WecomClient:
|
|||||||
response = await client.post(url, json=params)
|
response = await client.post(url, json=params)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f"发送图片失败:{data}")
|
||||||
raise Exception('Failed to send image: ' + str(e))
|
raise Exception('Failed to send image: ' + str(e))
|
||||||
|
|
||||||
# 企业微信错误码40014和42001,代表accesstoken问题
|
# 企业微信错误码40014和42001,代表accesstoken问题
|
||||||
@@ -159,6 +164,7 @@ class WecomClient:
|
|||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
return await self.send_private_msg(user_id, agent_id, content)
|
return await self.send_private_msg(user_id, agent_id, content)
|
||||||
if data['errcode'] != 0:
|
if data['errcode'] != 0:
|
||||||
|
await self.logger.error(f"发送消息失败:{data}")
|
||||||
raise Exception('Failed to send message: ' + str(data))
|
raise Exception('Failed to send message: ' + str(data))
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
@@ -175,6 +181,7 @@ class WecomClient:
|
|||||||
echostr = request.args.get('echostr')
|
echostr = request.args.get('echostr')
|
||||||
ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
|
ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
|
await self.logger.error("验证失败")
|
||||||
raise Exception(f'验证失败,错误码: {ret}')
|
raise Exception(f'验证失败,错误码: {ret}')
|
||||||
return reply_echo_str
|
return reply_echo_str
|
||||||
|
|
||||||
@@ -182,7 +189,9 @@ class WecomClient:
|
|||||||
encrypt_msg = await request.data
|
encrypt_msg = await request.data
|
||||||
ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
|
ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
|
await self.logger.error("消息解密失败")
|
||||||
raise Exception(f'消息解密失败,错误码: {ret}')
|
raise Exception(f'消息解密失败,错误码: {ret}')
|
||||||
|
|
||||||
|
|
||||||
# 解析消息并处理
|
# 解析消息并处理
|
||||||
message_data = await self.get_message(xml_msg)
|
message_data = await self.get_message(xml_msg)
|
||||||
@@ -193,6 +202,7 @@ class WecomClient:
|
|||||||
|
|
||||||
return 'success'
|
return 'success'
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
|
||||||
return f'Error processing request: {str(e)}', 400
|
return f'Error processing request: {str(e)}', 400
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||||
@@ -291,6 +301,7 @@ class WecomClient:
|
|||||||
except binascii.Error as e:
|
except binascii.Error as e:
|
||||||
raise ValueError(f'Invalid base64 string: {str(e)}')
|
raise ValueError(f'Invalid base64 string: {str(e)}')
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error("Image对象出错")
|
||||||
raise ValueError('image对象出错')
|
raise ValueError('image对象出错')
|
||||||
|
|
||||||
# 设置 multipart/form-data 格式的文件
|
# 设置 multipart/form-data 格式的文件
|
||||||
@@ -314,6 +325,7 @@ class WecomClient:
|
|||||||
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_to_work(image)
|
||||||
if data.get('errcode', 0) != 0:
|
if data.get('errcode', 0) != 0:
|
||||||
|
await self.logger.error(f"上传图片失败:{data}")
|
||||||
raise Exception('failed to upload file')
|
raise Exception('failed to upload file')
|
||||||
|
|
||||||
media_id = data.get('media_id')
|
media_id = data.get('media_id')
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import aiofiles
|
|||||||
|
|
||||||
|
|
||||||
class WecomCSClient:
|
class WecomCSClient:
|
||||||
def __init__(self, corpid: str, secret: str, token: str, EncodingAESKey: str):
|
def __init__(self, corpid: str, secret: str, token: str, EncodingAESKey: str, logger: None):
|
||||||
self.corpid = corpid
|
self.corpid = corpid
|
||||||
self.secret = secret
|
self.secret = secret
|
||||||
self.access_token_for_contacts = ''
|
self.access_token_for_contacts = ''
|
||||||
@@ -21,6 +21,7 @@ class WecomCSClient:
|
|||||||
self.aes = EncodingAESKey
|
self.aes = EncodingAESKey
|
||||||
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
|
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
|
||||||
self.access_token = ''
|
self.access_token = ''
|
||||||
|
self.logger = logger
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
self.app.add_url_rule(
|
self.app.add_url_rule(
|
||||||
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
|
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
|
||||||
@@ -186,6 +187,7 @@ class WecomCSClient:
|
|||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
return await self.send_text_msg(open_kfid, external_userid, msgid, content)
|
return await self.send_text_msg(open_kfid, external_userid, msgid, content)
|
||||||
if data['errcode'] != 0:
|
if data['errcode'] != 0:
|
||||||
|
await self.logger.error(f"发送消息失败:{data}")
|
||||||
raise Exception('Failed to send message')
|
raise Exception('Failed to send message')
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -224,7 +226,10 @@ class WecomCSClient:
|
|||||||
|
|
||||||
return 'success'
|
return 'success'
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
if self.logger:
|
||||||
|
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
|
||||||
|
else:
|
||||||
|
traceback.print_exc()
|
||||||
return f'Error processing request: {str(e)}', 400
|
return f'Error processing request: {str(e)}', 400
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||||
|
|||||||
22
pkg/api/http/controller/groups/files.py
Normal file
22
pkg/api/http/controller/groups/files.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import quart
|
||||||
|
import mimetypes
|
||||||
|
|
||||||
|
from .. import group
|
||||||
|
|
||||||
|
|
||||||
|
@group.group_class('files', '/api/v1/files')
|
||||||
|
class FilesRouterGroup(group.RouterGroup):
|
||||||
|
async def initialize(self) -> None:
|
||||||
|
@self.route('/image/<image_key>', methods=['GET'], auth_type=group.AuthType.NONE)
|
||||||
|
async def _(image_key: str) -> quart.Response:
|
||||||
|
if not await self.ap.storage_mgr.storage_provider.exists(image_key):
|
||||||
|
return quart.Response(status=404)
|
||||||
|
|
||||||
|
image_bytes = await self.ap.storage_mgr.storage_provider.load(image_key)
|
||||||
|
mime_type = mimetypes.guess_type(image_key)[0]
|
||||||
|
if mime_type is None:
|
||||||
|
mime_type = 'image/jpeg'
|
||||||
|
|
||||||
|
return quart.Response(image_bytes, mimetype=mime_type)
|
||||||
@@ -29,3 +29,16 @@ class BotsRouterGroup(group.RouterGroup):
|
|||||||
elif quart.request.method == 'DELETE':
|
elif quart.request.method == 'DELETE':
|
||||||
await self.ap.bot_service.delete_bot(bot_uuid)
|
await self.ap.bot_service.delete_bot(bot_uuid)
|
||||||
return self.success()
|
return self.success()
|
||||||
|
|
||||||
|
@self.route('/<bot_uuid>/logs', methods=['POST'])
|
||||||
|
async def _(bot_uuid: str) -> str:
|
||||||
|
json_data = await quart.request.json
|
||||||
|
from_index = json_data.get('from_index', -1)
|
||||||
|
max_count = json_data.get('max_count', 10)
|
||||||
|
logs, total_count = await self.ap.bot_service.list_event_logs(bot_uuid, from_index, max_count)
|
||||||
|
return self.success(
|
||||||
|
data={
|
||||||
|
'logs': logs,
|
||||||
|
'total_count': total_count,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
import typing
|
||||||
|
|
||||||
from ....core import app
|
from ....core import app
|
||||||
from ....entity.persistence import bot as persistence_bot
|
from ....entity.persistence import bot as persistence_bot
|
||||||
@@ -98,3 +99,14 @@ class BotService:
|
|||||||
await self.ap.persistence_mgr.execute_async(
|
await self.ap.persistence_mgr.execute_async(
|
||||||
sqlalchemy.delete(persistence_bot.Bot).where(persistence_bot.Bot.uuid == bot_uuid)
|
sqlalchemy.delete(persistence_bot.Bot).where(persistence_bot.Bot.uuid == bot_uuid)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def list_event_logs(
|
||||||
|
self, bot_uuid: str, from_index: int, max_count: int
|
||||||
|
) -> typing.Tuple[list[dict], int, int, int]:
|
||||||
|
runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
|
||||||
|
if runtime_bot is None:
|
||||||
|
raise Exception('Bot not found')
|
||||||
|
|
||||||
|
logs, total_count = await runtime_bot.logger.get_logs(from_index, max_count)
|
||||||
|
|
||||||
|
return [log.to_json() for log in logs], total_count
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from ..api.http.service import model as model_service
|
|||||||
from ..api.http.service import pipeline as pipeline_service
|
from ..api.http.service import pipeline as pipeline_service
|
||||||
from ..api.http.service import bot as bot_service
|
from ..api.http.service import bot as bot_service
|
||||||
from ..discover import engine as discover_engine
|
from ..discover import engine as discover_engine
|
||||||
|
from ..storage import mgr as storagemgr
|
||||||
from ..utils import logcache
|
from ..utils import logcache
|
||||||
from . import taskmgr
|
from . import taskmgr
|
||||||
from . import entities as core_entities
|
from . import entities as core_entities
|
||||||
@@ -96,6 +97,8 @@ class Application:
|
|||||||
|
|
||||||
log_cache: logcache.LogCache = None
|
log_cache: logcache.LogCache = None
|
||||||
|
|
||||||
|
storage_mgr: storagemgr.StorageMgr = None
|
||||||
|
|
||||||
# ========= HTTP Services =========
|
# ========= HTTP Services =========
|
||||||
|
|
||||||
user_service: user_service.UserService = None
|
user_service: user_service.UserService = None
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from ...api.http.service import model as model_service
|
|||||||
from ...api.http.service import pipeline as pipeline_service
|
from ...api.http.service import pipeline as pipeline_service
|
||||||
from ...api.http.service import bot as bot_service
|
from ...api.http.service import bot as bot_service
|
||||||
from ...discover import engine as discover_engine
|
from ...discover import engine as discover_engine
|
||||||
|
from ...storage import mgr as storagemgr
|
||||||
from ...utils import logcache
|
from ...utils import logcache
|
||||||
from .. import taskmgr
|
from .. import taskmgr
|
||||||
|
|
||||||
@@ -50,6 +51,10 @@ class BuildAppStage(stage.BootingStage):
|
|||||||
log_cache = logcache.LogCache()
|
log_cache = logcache.LogCache()
|
||||||
ap.log_cache = log_cache
|
ap.log_cache = log_cache
|
||||||
|
|
||||||
|
storage_mgr_inst = storagemgr.StorageMgr(ap)
|
||||||
|
await storage_mgr_inst.initialize()
|
||||||
|
ap.storage_mgr = storage_mgr_inst
|
||||||
|
|
||||||
persistence_mgr_inst = persistencemgr.PersistenceManager(ap)
|
persistence_mgr_inst = persistencemgr.PersistenceManager(ap)
|
||||||
ap.persistence_mgr = persistence_mgr_inst
|
ap.persistence_mgr = persistence_mgr_inst
|
||||||
await persistence_mgr_inst.initialize()
|
await persistence_mgr_inst.initialize()
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=16)
|
@functools.lru_cache(maxsize=16)
|
||||||
def get_font(self, query: core_entities.Query):
|
def get_font(self, font_path: str):
|
||||||
return ImageFont.truetype(
|
return ImageFont.truetype(
|
||||||
query.pipeline_config['output']['long-text-processing']['font-path'],
|
font_path,
|
||||||
32,
|
32,
|
||||||
encoding='utf-8',
|
encoding='utf-8',
|
||||||
)
|
)
|
||||||
@@ -146,7 +146,9 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy):
|
|||||||
self.ap.logger.debug('lines: {}, text_width: {}'.format(lines, text_width))
|
self.ap.logger.debug('lines: {}, text_width: {}'.format(lines, text_width))
|
||||||
for line in lines:
|
for line in lines:
|
||||||
# 如果长了就分割
|
# 如果长了就分割
|
||||||
line_width = self.get_font(query).getlength(line)
|
line_width = self.get_font(query.pipeline_config['output']['long-text-processing']['font-path']).getlength(
|
||||||
|
line
|
||||||
|
)
|
||||||
self.ap.logger.debug('line_width: {}'.format(line_width))
|
self.ap.logger.debug('line_width: {}'.format(line_width))
|
||||||
if line_width < text_width:
|
if line_width < text_width:
|
||||||
final_lines.append(line)
|
final_lines.append(line)
|
||||||
@@ -167,7 +169,9 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy):
|
|||||||
|
|
||||||
final_lines.append(rest_text[:point])
|
final_lines.append(rest_text[:point])
|
||||||
rest_text = rest_text[point:]
|
rest_text = rest_text[point:]
|
||||||
line_width = self.text_render_font.getlength(rest_text)
|
line_width = self.get_font(
|
||||||
|
query.pipeline_config['output']['long-text-processing']['font-path']
|
||||||
|
).getlength(rest_text)
|
||||||
if line_width < text_width:
|
if line_width < text_width:
|
||||||
final_lines.append(rest_text)
|
final_lines.append(rest_text)
|
||||||
break
|
break
|
||||||
@@ -187,7 +191,7 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy):
|
|||||||
(offset_x, offset_y + 35 * line_number),
|
(offset_x, offset_y + 35 * line_number),
|
||||||
final_line,
|
final_line,
|
||||||
fill=(0, 0, 0),
|
fill=(0, 0, 0),
|
||||||
font=self.text_render_font,
|
font=self.get_font(query.pipeline_config['output']['long-text-processing']['font-path']),
|
||||||
)
|
)
|
||||||
# 遍历此行,检查是否有emoji
|
# 遍历此行,检查是否有emoji
|
||||||
idx_in_line = 0
|
idx_in_line = 0
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
|
|
||||||
session = await self.ap.sess_mgr.get_session(query)
|
session = await self.ap.sess_mgr.get_session(query)
|
||||||
|
|
||||||
|
|
||||||
# 非 local-agent 时,llm_model 为 None
|
# 非 local-agent 时,llm_model 为 None
|
||||||
llm_model = (
|
llm_model = (
|
||||||
await self.ap.model_mgr.get_model_by_uuid(query.pipeline_config['ai']['local-agent']['model'])
|
await self.ap.model_mgr.get_model_by_uuid(query.pipeline_config['ai']['local-agent']['model'])
|
||||||
@@ -59,7 +58,7 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
|
|
||||||
if selected_runner == 'local-agent':
|
if selected_runner == 'local-agent':
|
||||||
query.use_funcs = (
|
query.use_funcs = (
|
||||||
conversation.use_funcs if query.use_llm_model.model_entity.abilities.__contains__('tool_call') else None
|
conversation.use_funcs if query.use_llm_model.model_entity.abilities.__contains__('func_call') else None
|
||||||
)
|
)
|
||||||
|
|
||||||
query.variables = {
|
query.variables = {
|
||||||
@@ -82,7 +81,7 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
content_list = []
|
content_list = []
|
||||||
|
|
||||||
plain_text = ''
|
plain_text = ''
|
||||||
qoute_msg = query.pipeline_config["trigger"].get("misc",'').get("combine-quote-message")
|
qoute_msg = query.pipeline_config['trigger'].get('misc', '').get('combine-quote-message')
|
||||||
|
|
||||||
for me in query.message_chain:
|
for me in query.message_chain:
|
||||||
if isinstance(me, platform_message.Plain):
|
if isinstance(me, platform_message.Plain):
|
||||||
@@ -100,13 +99,11 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
content_list.append(llm_entities.ContentElement.from_text(msg.text))
|
content_list.append(llm_entities.ContentElement.from_text(msg.text))
|
||||||
elif isinstance(msg, platform_message.Image):
|
elif isinstance(msg, platform_message.Image):
|
||||||
if selected_runner != 'local-agent' or query.use_llm_model.model_entity.abilities.__contains__(
|
if selected_runner != 'local-agent' or query.use_llm_model.model_entity.abilities.__contains__(
|
||||||
'vision'
|
'vision'
|
||||||
):
|
):
|
||||||
if msg.base64 is not None:
|
if msg.base64 is not None:
|
||||||
content_list.append(llm_entities.ContentElement.from_image_base64(msg.base64))
|
content_list.append(llm_entities.ContentElement.from_image_base64(msg.base64))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
query.variables['user_message_text'] = plain_text
|
query.variables['user_message_text'] = plain_text
|
||||||
|
|
||||||
query.user_message = llm_entities.Message(role='user', content=content_list)
|
query.user_message = llm_entities.Message(role='user', content=content_list)
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ from ..core import app, entities as core_entities
|
|||||||
from . import entities
|
from . import entities
|
||||||
|
|
||||||
|
|
||||||
preregistered_stages: dict[str, PipelineStage] = {}
|
preregistered_stages: dict[str, type[PipelineStage]] = {}
|
||||||
|
|
||||||
|
|
||||||
def stage_class(name: str):
|
def stage_class(name: str) -> typing.Callable[[type[PipelineStage]], type[PipelineStage]]:
|
||||||
def decorator(cls):
|
def decorator(cls: type[PipelineStage]) -> type[PipelineStage]:
|
||||||
preregistered_stages[name] = cls
|
preregistered_stages[name] = cls
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import abc
|
|||||||
from ..core import app
|
from ..core import app
|
||||||
from .types import message as platform_message
|
from .types import message as platform_message
|
||||||
from .types import events as platform_events
|
from .types import events as platform_events
|
||||||
|
from .logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class MessagePlatformAdapter(metaclass=abc.ABCMeta):
|
class MessagePlatformAdapter(metaclass=abc.ABCMeta):
|
||||||
@@ -22,7 +23,9 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta):
|
|||||||
|
|
||||||
ap: app.Application
|
ap: app.Application
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
logger: EventLogger
|
||||||
|
|
||||||
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
"""初始化适配器
|
"""初始化适配器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -31,6 +34,7 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta):
|
|||||||
"""
|
"""
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
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):
|
||||||
"""主动发送消息
|
"""主动发送消息
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ import sqlalchemy
|
|||||||
from . import adapter as msadapter
|
from . import adapter as msadapter
|
||||||
|
|
||||||
from ..core import app, entities as core_entities, taskmgr
|
from ..core import app, entities as core_entities, taskmgr
|
||||||
from .types import events as platform_events
|
from .types import events as platform_events, message as platform_message
|
||||||
|
|
||||||
from ..discover import engine
|
from ..discover import engine
|
||||||
|
|
||||||
from ..entity.persistence import bot as persistence_bot
|
from ..entity.persistence import bot as persistence_bot
|
||||||
|
|
||||||
|
from .logger import EventLogger
|
||||||
|
|
||||||
# 处理 3.4 移除了 YiriMirai 之后,插件的兼容性问题
|
# 处理 3.4 移除了 YiriMirai 之后,插件的兼容性问题
|
||||||
from . import types as mirai
|
from . import types as mirai
|
||||||
|
|
||||||
@@ -37,23 +39,37 @@ class RuntimeBot:
|
|||||||
|
|
||||||
task_context: taskmgr.TaskContext
|
task_context: taskmgr.TaskContext
|
||||||
|
|
||||||
|
logger: EventLogger
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
ap: app.Application,
|
ap: app.Application,
|
||||||
bot_entity: persistence_bot.Bot,
|
bot_entity: persistence_bot.Bot,
|
||||||
adapter: msadapter.MessagePlatformAdapter,
|
adapter: msadapter.MessagePlatformAdapter,
|
||||||
|
logger: EventLogger,
|
||||||
):
|
):
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
self.bot_entity = bot_entity
|
self.bot_entity = bot_entity
|
||||||
self.enable = bot_entity.enable
|
self.enable = bot_entity.enable
|
||||||
self.adapter = adapter
|
self.adapter = adapter
|
||||||
self.task_context = taskmgr.TaskContext()
|
self.task_context = taskmgr.TaskContext()
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
async def on_friend_message(
|
async def on_friend_message(
|
||||||
event: platform_events.FriendMessage,
|
event: platform_events.FriendMessage,
|
||||||
adapter: msadapter.MessagePlatformAdapter,
|
adapter: msadapter.MessagePlatformAdapter,
|
||||||
):
|
):
|
||||||
|
image_components = [
|
||||||
|
component for component in event.message_chain if isinstance(component, platform_message.Image)
|
||||||
|
]
|
||||||
|
|
||||||
|
await self.logger.info(
|
||||||
|
f'{event.message_chain}',
|
||||||
|
images=image_components,
|
||||||
|
message_session_id=f'person_{event.sender.id}',
|
||||||
|
)
|
||||||
|
|
||||||
await self.ap.query_pool.add_query(
|
await self.ap.query_pool.add_query(
|
||||||
bot_uuid=self.bot_entity.uuid,
|
bot_uuid=self.bot_entity.uuid,
|
||||||
launcher_type=core_entities.LauncherTypes.PERSON,
|
launcher_type=core_entities.LauncherTypes.PERSON,
|
||||||
@@ -68,6 +84,16 @@ class RuntimeBot:
|
|||||||
event: platform_events.GroupMessage,
|
event: platform_events.GroupMessage,
|
||||||
adapter: msadapter.MessagePlatformAdapter,
|
adapter: msadapter.MessagePlatformAdapter,
|
||||||
):
|
):
|
||||||
|
image_components = [
|
||||||
|
component for component in event.message_chain if isinstance(component, platform_message.Image)
|
||||||
|
]
|
||||||
|
|
||||||
|
await self.logger.info(
|
||||||
|
f'{event.message_chain}',
|
||||||
|
images=image_components,
|
||||||
|
message_session_id=f'group_{event.group.id}',
|
||||||
|
)
|
||||||
|
|
||||||
await self.ap.query_pool.add_query(
|
await self.ap.query_pool.add_query(
|
||||||
bot_uuid=self.bot_entity.uuid,
|
bot_uuid=self.bot_entity.uuid,
|
||||||
launcher_type=core_entities.LauncherTypes.GROUP,
|
launcher_type=core_entities.LauncherTypes.GROUP,
|
||||||
@@ -92,10 +118,7 @@ class RuntimeBot:
|
|||||||
self.task_context.set_current_action('Exited.')
|
self.task_context.set_current_action('Exited.')
|
||||||
return
|
return
|
||||||
self.task_context.set_current_action('Exited with error.')
|
self.task_context.set_current_action('Exited with error.')
|
||||||
self.task_context.log(f'平台适配器运行出错: {e}')
|
await self.logger.error(f'平台适配器运行出错:\n{e}\n{traceback.format_exc()}')
|
||||||
self.task_context.log(f'Traceback: {traceback.format_exc()}')
|
|
||||||
self.ap.logger.error(f'平台适配器运行出错: {e}')
|
|
||||||
self.ap.logger.debug(f'Traceback: {traceback.format_exc()}')
|
|
||||||
|
|
||||||
self.task_wrapper = self.ap.task_mgr.create_task(
|
self.task_wrapper = self.ap.task_mgr.create_task(
|
||||||
exception_wrapper(),
|
exception_wrapper(),
|
||||||
@@ -166,9 +189,15 @@ class PlatformManager:
|
|||||||
elif isinstance(bot_entity, dict):
|
elif isinstance(bot_entity, dict):
|
||||||
bot_entity = persistence_bot.Bot(**bot_entity)
|
bot_entity = persistence_bot.Bot(**bot_entity)
|
||||||
|
|
||||||
adapter_inst = self.adapter_dict[bot_entity.adapter](bot_entity.adapter_config, self.ap)
|
logger = EventLogger(name=f'platform-adapter-{bot_entity.name}', ap=self.ap)
|
||||||
|
|
||||||
runtime_bot = RuntimeBot(ap=self.ap, bot_entity=bot_entity, adapter=adapter_inst)
|
adapter_inst = self.adapter_dict[bot_entity.adapter](
|
||||||
|
bot_entity.adapter_config,
|
||||||
|
self.ap,
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
|
||||||
|
runtime_bot = RuntimeBot(ap=self.ap, bot_entity=bot_entity, adapter=adapter_inst, logger=logger)
|
||||||
|
|
||||||
await runtime_bot.initialize()
|
await runtime_bot.initialize()
|
||||||
|
|
||||||
|
|||||||
233
pkg/platform/logger.py
Normal file
233
pkg/platform/logger.py
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typing
|
||||||
|
import mimetypes
|
||||||
|
import time
|
||||||
|
import enum
|
||||||
|
import pydantic
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from ..core import app
|
||||||
|
from .types import message as platform_message
|
||||||
|
|
||||||
|
|
||||||
|
class EventLogLevel(enum.Enum):
|
||||||
|
"""日志级别"""
|
||||||
|
|
||||||
|
DEBUG = 'debug'
|
||||||
|
INFO = 'info'
|
||||||
|
WARNING = 'warning'
|
||||||
|
ERROR = 'error'
|
||||||
|
|
||||||
|
|
||||||
|
class EventLog(pydantic.BaseModel):
|
||||||
|
seq_id: int
|
||||||
|
"""日志序号"""
|
||||||
|
|
||||||
|
timestamp: int
|
||||||
|
"""日志时间戳"""
|
||||||
|
|
||||||
|
level: EventLogLevel
|
||||||
|
"""日志级别"""
|
||||||
|
|
||||||
|
text: str
|
||||||
|
"""日志文本"""
|
||||||
|
|
||||||
|
images: typing.Optional[list[str]] = None
|
||||||
|
"""日志图片 URL 列表,需要通过 /api/v1/image/{uuid} 获取图片"""
|
||||||
|
|
||||||
|
message_session_id: typing.Optional[str] = None
|
||||||
|
"""消息会话ID,仅收发消息事件有值"""
|
||||||
|
|
||||||
|
def to_json(self) -> dict:
|
||||||
|
return {
|
||||||
|
'seq_id': self.seq_id,
|
||||||
|
'timestamp': self.timestamp,
|
||||||
|
'level': self.level.value,
|
||||||
|
'text': self.text,
|
||||||
|
'images': self.images,
|
||||||
|
'message_session_id': self.message_session_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
MAX_LOG_COUNT = 200
|
||||||
|
DELETE_COUNT_PER_TIME = 50
|
||||||
|
|
||||||
|
|
||||||
|
class EventLogger:
|
||||||
|
"""used for logging bot events"""
|
||||||
|
|
||||||
|
ap: app.Application
|
||||||
|
|
||||||
|
seq_id_inc: int
|
||||||
|
|
||||||
|
logs: list[EventLog]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
ap: app.Application,
|
||||||
|
):
|
||||||
|
self.name = name
|
||||||
|
self.ap = ap
|
||||||
|
self.logs = []
|
||||||
|
self.seq_id_inc = 0
|
||||||
|
|
||||||
|
async def get_logs(self, from_seq_id: int, max_count: int) -> typing.Tuple[list[EventLog], int]:
|
||||||
|
"""
|
||||||
|
获取日志,从 from_seq_id 开始获取 max_count 条历史日志
|
||||||
|
|
||||||
|
Args:
|
||||||
|
from_seq_id: 起始序号,-1 表示末尾
|
||||||
|
max_count: 最大数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[list[EventLog], int]: 日志列表,日志总数
|
||||||
|
"""
|
||||||
|
if len(self.logs) == 0:
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
if from_seq_id <= -1:
|
||||||
|
from_seq_id = self.logs[-1].seq_id
|
||||||
|
|
||||||
|
min_seq_id_in_logs = self.logs[0].seq_id
|
||||||
|
max_seq_id_in_logs = self.logs[-1].seq_id
|
||||||
|
|
||||||
|
if from_seq_id < min_seq_id_in_logs: # 需要的整个范围都已经被删除
|
||||||
|
return [], len(self.logs)
|
||||||
|
|
||||||
|
if (
|
||||||
|
from_seq_id > max_seq_id_in_logs and from_seq_id - max_count > max_seq_id_in_logs
|
||||||
|
): # 需要的整个范围都还没生成
|
||||||
|
return [], len(self.logs)
|
||||||
|
|
||||||
|
end_index = 1
|
||||||
|
|
||||||
|
for i, log in enumerate(self.logs):
|
||||||
|
if log.seq_id >= from_seq_id:
|
||||||
|
end_index = i + 1
|
||||||
|
break
|
||||||
|
|
||||||
|
start_index = max(0, end_index - max_count)
|
||||||
|
|
||||||
|
if max_count > 0:
|
||||||
|
return self.logs[start_index:end_index], len(self.logs)
|
||||||
|
else:
|
||||||
|
return [], len(self.logs)
|
||||||
|
|
||||||
|
async def _truncate_logs(self):
|
||||||
|
if len(self.logs) > MAX_LOG_COUNT:
|
||||||
|
for i in range(DELETE_COUNT_PER_TIME):
|
||||||
|
for image_key in self.logs[i].images:
|
||||||
|
await self.ap.storage_mgr.storage_provider.delete(image_key)
|
||||||
|
self.logs = self.logs[DELETE_COUNT_PER_TIME:]
|
||||||
|
|
||||||
|
async def _add_log(
|
||||||
|
self,
|
||||||
|
level: EventLogLevel,
|
||||||
|
text: str,
|
||||||
|
images: typing.Optional[list[platform_message.Image]] = None,
|
||||||
|
message_session_id: typing.Optional[str] = None,
|
||||||
|
no_throw: bool = True,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
image_keys = []
|
||||||
|
|
||||||
|
if images is None:
|
||||||
|
images = []
|
||||||
|
|
||||||
|
if message_session_id is None:
|
||||||
|
message_session_id = ''
|
||||||
|
|
||||||
|
if not isinstance(message_session_id, str):
|
||||||
|
message_session_id = str(message_session_id)
|
||||||
|
|
||||||
|
for img in images:
|
||||||
|
img_bytes, mime_type = await img.get_bytes()
|
||||||
|
extension = mimetypes.guess_extension(mime_type)
|
||||||
|
if extension is None:
|
||||||
|
extension = '.jpg'
|
||||||
|
image_key = f'{message_session_id}-{uuid.uuid4()}{extension}'
|
||||||
|
await self.ap.storage_mgr.storage_provider.save(image_key, img_bytes)
|
||||||
|
image_keys.append(image_key)
|
||||||
|
|
||||||
|
self.logs.append(
|
||||||
|
EventLog(
|
||||||
|
seq_id=self.seq_id_inc,
|
||||||
|
timestamp=int(time.time()),
|
||||||
|
level=level,
|
||||||
|
text=text,
|
||||||
|
images=image_keys,
|
||||||
|
message_session_id=message_session_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.seq_id_inc += 1
|
||||||
|
|
||||||
|
await self._truncate_logs()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if not no_throw:
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
async def info(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
images: typing.Optional[list[platform_message.Image]] = None,
|
||||||
|
message_session_id: typing.Optional[str] = None,
|
||||||
|
no_throw: bool = True,
|
||||||
|
):
|
||||||
|
await self._add_log(
|
||||||
|
level=EventLogLevel.INFO,
|
||||||
|
text=text,
|
||||||
|
images=images,
|
||||||
|
message_session_id=message_session_id,
|
||||||
|
no_throw=no_throw,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def debug(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
images: typing.Optional[list[platform_message.Image]] = None,
|
||||||
|
message_session_id: typing.Optional[str] = None,
|
||||||
|
no_throw: bool = True,
|
||||||
|
):
|
||||||
|
await self._add_log(
|
||||||
|
level=EventLogLevel.DEBUG,
|
||||||
|
text=text,
|
||||||
|
images=images,
|
||||||
|
message_session_id=message_session_id,
|
||||||
|
no_throw=no_throw,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def warning(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
images: typing.Optional[list[platform_message.Image]] = None,
|
||||||
|
message_session_id: typing.Optional[str] = None,
|
||||||
|
no_throw: bool = True,
|
||||||
|
):
|
||||||
|
await self._add_log(
|
||||||
|
level=EventLogLevel.WARNING,
|
||||||
|
text=text,
|
||||||
|
images=images,
|
||||||
|
message_session_id=message_session_id,
|
||||||
|
no_throw=no_throw,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def error(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
images: typing.Optional[list[platform_message.Image]] = None,
|
||||||
|
message_session_id: typing.Optional[str] = None,
|
||||||
|
no_throw: bool = True,
|
||||||
|
):
|
||||||
|
await self._add_log(
|
||||||
|
level=EventLogLevel.ERROR,
|
||||||
|
text=text,
|
||||||
|
images=images,
|
||||||
|
message_session_id=message_session_id,
|
||||||
|
no_throw=no_throw,
|
||||||
|
)
|
||||||
@@ -12,6 +12,7 @@ from ..types import message as platform_message
|
|||||||
from ..types import events as platform_events
|
from ..types import events as platform_events
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...utils import image
|
from ...utils import image
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class AiocqhttpMessageConverter(adapter.MessageConverter):
|
class AiocqhttpMessageConverter(adapter.MessageConverter):
|
||||||
@@ -209,8 +210,11 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
|
|||||||
|
|
||||||
ap: app.Application
|
ap: app.Application
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
on_websocket_connection_event_cache: typing.List[typing.Callable[[aiocqhttp.Event], None]] = []
|
||||||
|
|
||||||
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def shutdown_trigger_placeholder():
|
async def shutdown_trigger_placeholder():
|
||||||
while True:
|
while True:
|
||||||
@@ -219,6 +223,7 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.config['shutdown_trigger'] = shutdown_trigger_placeholder
|
self.config['shutdown_trigger'] = shutdown_trigger_placeholder
|
||||||
|
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.on_websocket_connection_event_cache = []
|
||||||
|
|
||||||
if 'access-token' in config:
|
if 'access-token' in config:
|
||||||
self.bot = aiocqhttp.CQHttp(access_token=config['access-token'])
|
self.bot = aiocqhttp.CQHttp(access_token=config['access-token'])
|
||||||
@@ -260,6 +265,7 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
|
|||||||
try:
|
try:
|
||||||
return await callback(await self.event_converter.target2yiri(event,self.bot), self)
|
return await callback(await self.event_converter.target2yiri(event,self.bot), self)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
await self.logger.error(f'Error in on_message: {traceback.format_exc()}')
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
if event_type == platform_events.GroupMessage:
|
if event_type == platform_events.GroupMessage:
|
||||||
@@ -267,6 +273,16 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
|
|||||||
elif event_type == platform_events.FriendMessage:
|
elif event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('private')(on_message)
|
self.bot.on_message('private')(on_message)
|
||||||
|
|
||||||
|
async def on_websocket_connection(event: aiocqhttp.Event):
|
||||||
|
for event in self.on_websocket_connection_event_cache:
|
||||||
|
if event.self_id == event.self_id and event.time == event.time:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.on_websocket_connection_event_cache.append(event)
|
||||||
|
await self.logger.info(f'WebSocket connection established, bot id: {event.self_id}')
|
||||||
|
|
||||||
|
self.bot.on_websocket_connection(on_websocket_connection)
|
||||||
|
|
||||||
def unregister_listener(
|
def unregister_listener(
|
||||||
self,
|
self,
|
||||||
event_type: typing.Type[platform_events.Event],
|
event_type: typing.Type[platform_events.Event],
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from ..types import events as platform_events
|
|||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from libs.dingtalk_api.api import DingTalkClient
|
from libs.dingtalk_api.api import DingTalkClient
|
||||||
import datetime
|
import datetime
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class DingTalkMessageConverter(adapter.MessageConverter):
|
class DingTalkMessageConverter(adapter.MessageConverter):
|
||||||
@@ -99,9 +100,10 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
|
|||||||
event_converter: DingTalkEventConverter = DingTalkEventConverter()
|
event_converter: DingTalkEventConverter = DingTalkEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'client_id',
|
'client_id',
|
||||||
'client_secret',
|
'client_secret',
|
||||||
@@ -120,6 +122,7 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
|
|||||||
robot_name=config['robot_name'],
|
robot_name=config['robot_name'],
|
||||||
robot_code=config['robot_code'],
|
robot_code=config['robot_code'],
|
||||||
markdown_card=config['markdown_card'],
|
markdown_card=config['markdown_card'],
|
||||||
|
logger=self.logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
@@ -154,8 +157,8 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
|
|||||||
await self.event_converter.target2yiri(event, self.config['robot_name']),
|
await self.event_converter.target2yiri(event, self.config['robot_name']),
|
||||||
self,
|
self,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in dingtalk callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('FriendMessage')(on_message)
|
self.bot.on_message('FriendMessage')(on_message)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from ...core import app
|
|||||||
from ..types import message as platform_message
|
from ..types import message as platform_message
|
||||||
from ..types import events as platform_events
|
from ..types import events as platform_events
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class DiscordMessageConverter(adapter.MessageConverter):
|
class DiscordMessageConverter(adapter.MessageConverter):
|
||||||
@@ -170,9 +171,10 @@ class DiscordAdapter(adapter.MessagePlatformAdapter):
|
|||||||
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
self.bot_account_id = self.config['client_id']
|
self.bot_account_id = self.config['client_id']
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from ...utils import image
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class GewechatMessageConverter(adapter.MessageConverter):
|
class GewechatMessageConverter(adapter.MessageConverter):
|
||||||
@@ -371,7 +372,7 @@ class GewechatMessageConverter(adapter.MessageConverter):
|
|||||||
quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') # 引用消息的原发送者
|
quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') # 引用消息的原发送者
|
||||||
ats_bot = ats_bot or (quote_id == tousername)
|
ats_bot = ats_bot or (quote_id == tousername)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'_ats_bot got except: {e}')
|
print(f'Error in gewechat _ats_bot: {e}')
|
||||||
finally:
|
finally:
|
||||||
return ats_bot
|
return ats_bot
|
||||||
|
|
||||||
@@ -477,9 +478,10 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
|
|||||||
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
self.quart_app = quart.Quart(__name__)
|
self.quart_app = quart.Quart(__name__)
|
||||||
|
|
||||||
self.message_converter = GewechatMessageConverter(config)
|
self.message_converter = GewechatMessageConverter(config)
|
||||||
@@ -503,7 +505,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
|
|||||||
try:
|
try:
|
||||||
event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id)
|
event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc()
|
await self.logger.error(f'Error in gewechat callback: {traceback.format_exc()}')
|
||||||
|
|
||||||
if event.__class__ in self.listeners:
|
if event.__class__ in self.listeners:
|
||||||
await self.listeners[event.__class__](event, self)
|
await self.listeners[event.__class__](event, self)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from ...core import app
|
|||||||
from ..types import message as platform_message
|
from ..types import message as platform_message
|
||||||
from ..types import events as platform_events
|
from ..types import events as platform_events
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class AESCipher(object):
|
class AESCipher(object):
|
||||||
@@ -338,9 +339,10 @@ class LarkAdapter(adapter.MessagePlatformAdapter):
|
|||||||
quart_app: quart.Quart
|
quart_app: quart.Quart
|
||||||
ap: app.Application
|
ap: app.Application
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
self.quart_app = quart.Quart(__name__)
|
self.quart_app = quart.Quart(__name__)
|
||||||
self.listeners = {}
|
self.listeners = {}
|
||||||
|
|
||||||
@@ -376,15 +378,15 @@ class LarkAdapter(adapter.MessagePlatformAdapter):
|
|||||||
if 'im.message.receive_v1' == type:
|
if 'im.message.receive_v1' == type:
|
||||||
try:
|
try:
|
||||||
event = await self.event_converter.target2yiri(p2v1, self.api_client)
|
event = await self.event_converter.target2yiri(p2v1, self.api_client)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in lark callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event.__class__ in self.listeners:
|
if event.__class__ in self.listeners:
|
||||||
await self.listeners[event.__class__](event, self)
|
await self.listeners[event.__class__](event, self)
|
||||||
|
|
||||||
return {'code': 200, 'message': 'ok'}
|
return {'code': 200, 'message': 'ok'}
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in lark callback: {traceback.format_exc()}")
|
||||||
return {'code': 500, 'message': 'error'}
|
return {'code': 500, 'message': 'error'}
|
||||||
|
|
||||||
async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
|
async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from ...pipeline.longtext.strategies import forward
|
|||||||
from ...platform.types import message as platform_message
|
from ...platform.types import message as platform_message
|
||||||
from ...platform.types import entities as platform_entities
|
from ...platform.types import entities as platform_entities
|
||||||
from ...platform.types import events as platform_events
|
from ...platform.types import events as platform_events
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class NakuruProjectMessageConverter(adapter_model.MessageConverter):
|
class NakuruProjectMessageConverter(adapter_model.MessageConverter):
|
||||||
@@ -71,9 +72,8 @@ class NakuruProjectMessageConverter(adapter_model.MessageConverter):
|
|||||||
content=content_list,
|
content=content_list,
|
||||||
)
|
)
|
||||||
nakuru_forward_node_list.append(nakuru_forward_node)
|
nakuru_forward_node_list.append(nakuru_forward_node)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
nakuru_msg_list.append(nakuru_forward_node_list)
|
nakuru_msg_list.append(nakuru_forward_node_list)
|
||||||
@@ -178,12 +178,13 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter):
|
|||||||
|
|
||||||
cfg: dict
|
cfg: dict
|
||||||
|
|
||||||
def __init__(self, cfg: dict, ap):
|
def __init__(self, cfg: dict, ap, logger: EventLogger):
|
||||||
"""初始化nakuru-project的对象"""
|
"""初始化nakuru-project的对象"""
|
||||||
cfg['port'] = cfg['ws_port']
|
cfg['port'] = cfg['ws_port']
|
||||||
del cfg['ws_port']
|
del cfg['ws_port']
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
self.listener_list = []
|
self.listener_list = []
|
||||||
self.bot = nakuru.CQHTTP(**self.cfg)
|
self.bot = nakuru.CQHTTP(**self.cfg)
|
||||||
|
|
||||||
@@ -275,7 +276,7 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter):
|
|||||||
# 注册监听器
|
# 注册监听器
|
||||||
self.bot.receiver(source_cls.__name__)(listener_wrapper)
|
self.bot.receiver(source_cls.__name__)(listener_wrapper)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
self.logger.error(f"Error in nakuru register_listener: {traceback.format_exc()}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def unregister_listener(
|
def unregister_listener(
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from .. import adapter
|
|||||||
from ...core import app
|
from ...core import app
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...command.errors import ParamNotEnoughError
|
from ...command.errors import ParamNotEnoughError
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class OAMessageConverter(adapter.MessageConverter):
|
class OAMessageConverter(adapter.MessageConverter):
|
||||||
@@ -63,10 +64,10 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
|
|||||||
event_converter: OAEventConverter = OAEventConverter()
|
event_converter: OAEventConverter = OAEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'token',
|
'token',
|
||||||
@@ -85,6 +86,7 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
|
|||||||
EncodingAESKey=config['EncodingAESKey'],
|
EncodingAESKey=config['EncodingAESKey'],
|
||||||
Appsecret=config['AppSecret'],
|
Appsecret=config['AppSecret'],
|
||||||
AppID=config['AppID'],
|
AppID=config['AppID'],
|
||||||
|
logger=self.logger,
|
||||||
)
|
)
|
||||||
elif self.config['Mode'] == 'passive':
|
elif self.config['Mode'] == 'passive':
|
||||||
self.bot = OAClientForLongerResponse(
|
self.bot = OAClientForLongerResponse(
|
||||||
@@ -93,6 +95,7 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
|
|||||||
Appsecret=config['AppSecret'],
|
Appsecret=config['AppSecret'],
|
||||||
AppID=config['AppID'],
|
AppID=config['AppID'],
|
||||||
LoadingMessage=config['LoadingMessage'],
|
LoadingMessage=config['LoadingMessage'],
|
||||||
|
logger=self.logger,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise KeyError('请设置微信公众号通信模式')
|
raise KeyError('请设置微信公众号通信模式')
|
||||||
@@ -122,8 +125,8 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.bot_account_id = event.receiver_id
|
self.bot_account_id = event.receiver_id
|
||||||
try:
|
try:
|
||||||
return await callback(await self.event_converter.target2yiri(event), self)
|
return await callback(await self.event_converter.target2yiri(event), self)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in officialaccount callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('text')(on_message)
|
self.bot.on_message('text')(on_message)
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ spec:
|
|||||||
label:
|
label:
|
||||||
en_US: Host
|
en_US: Host
|
||||||
zh_Hans: 监听主机
|
zh_Hans: 监听主机
|
||||||
|
description:
|
||||||
|
en_US: The host that Official Account listens on for Webhook connections.
|
||||||
|
zh_Hans: 微信公众号监听的主机,除非你知道自己在做什么,否则请写 0.0.0.0
|
||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
default: 0.0.0.0
|
default: 0.0.0.0
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from ...config import manager as cfg_mgr
|
|||||||
from ...platform.types import entities as platform_entities
|
from ...platform.types import entities as platform_entities
|
||||||
from ...platform.types import events as platform_events
|
from ...platform.types import events as platform_events
|
||||||
from ...platform.types import message as platform_message
|
from ...platform.types import message as platform_message
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class OfficialGroupMessage(platform_events.GroupMessage):
|
class OfficialGroupMessage(platform_events.GroupMessage):
|
||||||
@@ -357,10 +358,11 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter):
|
|||||||
group_msg_seq = None
|
group_msg_seq = None
|
||||||
c2c_msg_seq = None
|
c2c_msg_seq = None
|
||||||
|
|
||||||
def __init__(self, cfg: dict, ap: app.Application):
|
def __init__(self, cfg: dict, ap: app.Application, logger: EventLogger):
|
||||||
"""初始化适配器"""
|
"""初始化适配器"""
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
self.group_msg_seq = 1
|
self.group_msg_seq = 1
|
||||||
self.c2c_msg_seq = 1
|
self.c2c_msg_seq = 1
|
||||||
@@ -499,7 +501,7 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter):
|
|||||||
for event_handler in event_handler_mapping[event_type]:
|
for event_handler in event_handler_mapping[event_type]:
|
||||||
setattr(self.bot, event_handler, wrapper)
|
setattr(self.bot, event_handler, wrapper)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
self.logger.error(f"Error in qqbotpy callback: {traceback.format_exc()}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def unregister_listener(
|
def unregister_listener(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from ...command.errors import ParamNotEnoughError
|
|||||||
from libs.qq_official_api.api import QQOfficialClient
|
from libs.qq_official_api.api import QQOfficialClient
|
||||||
from libs.qq_official_api.qqofficialevent import QQOfficialEvent
|
from libs.qq_official_api.qqofficialevent import QQOfficialEvent
|
||||||
from ...utils import image
|
from ...utils import image
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class QQOfficialMessageConverter(adapter.MessageConverter):
|
class QQOfficialMessageConverter(adapter.MessageConverter):
|
||||||
@@ -139,9 +140,10 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter):
|
|||||||
message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter()
|
message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter()
|
||||||
event_converter: QQOfficialEventConverter = QQOfficialEventConverter()
|
event_converter: QQOfficialEventConverter = QQOfficialEventConverter()
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'appid',
|
'appid',
|
||||||
@@ -155,6 +157,7 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter):
|
|||||||
app_id=config['appid'],
|
app_id=config['appid'],
|
||||||
secret=config['secret'],
|
secret=config['secret'],
|
||||||
token=config['token'],
|
token=config['token'],
|
||||||
|
logger=self.logger
|
||||||
)
|
)
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
@@ -221,8 +224,8 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.bot_account_id = 'justbot'
|
self.bot_account_id = 'justbot'
|
||||||
try:
|
try:
|
||||||
return await callback(await self.event_converter.target2yiri(event), self)
|
return await callback(await self.event_converter.target2yiri(event), self)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in qqofficial callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('DIRECT_MESSAGE_CREATE')(on_message)
|
self.bot.on_message('DIRECT_MESSAGE_CREATE')(on_message)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from .. import adapter
|
|||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...command.errors import ParamNotEnoughError
|
from ...command.errors import ParamNotEnoughError
|
||||||
from ...utils import image
|
from ...utils import image
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class SlackMessageConverter(adapter.MessageConverter):
|
class SlackMessageConverter(adapter.MessageConverter):
|
||||||
@@ -91,9 +92,10 @@ class SlackAdapter(adapter.MessagePlatformAdapter):
|
|||||||
event_converter: SlackEventConverter = SlackEventConverter()
|
event_converter: SlackEventConverter = SlackEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'bot_token',
|
'bot_token',
|
||||||
'signing_secret',
|
'signing_secret',
|
||||||
@@ -102,7 +104,7 @@ class SlackAdapter(adapter.MessagePlatformAdapter):
|
|||||||
if missing_keys:
|
if missing_keys:
|
||||||
raise ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员')
|
raise ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员')
|
||||||
|
|
||||||
self.bot = SlackClient(bot_token=self.config['bot_token'], signing_secret=self.config['signing_secret'])
|
self.bot = SlackClient(bot_token=self.config['bot_token'], signing_secret=self.config['signing_secret'], logger=self.logger)
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
self,
|
self,
|
||||||
@@ -137,8 +139,8 @@ class SlackAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.bot_account_id = 'SlackBot'
|
self.bot_account_id = 'SlackBot'
|
||||||
try:
|
try:
|
||||||
return await callback(await self.event_converter.target2yiri(event, self.bot), self)
|
return await callback(await self.event_converter.target2yiri(event, self.bot), self)
|
||||||
except:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in slack callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('im')(on_message)
|
self.bot.on_message('im')(on_message)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from ...core import app
|
|||||||
from ..types import message as platform_message
|
from ..types import message as platform_message
|
||||||
from ..types import events as platform_events
|
from ..types import events as platform_events
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class TelegramMessageConverter(adapter.MessageConverter):
|
class TelegramMessageConverter(adapter.MessageConverter):
|
||||||
@@ -147,9 +148,10 @@ class TelegramAdapter(adapter.MessagePlatformAdapter):
|
|||||||
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
if update.message.from_user.is_bot:
|
if update.message.from_user.is_bot:
|
||||||
@@ -158,8 +160,8 @@ class TelegramAdapter(adapter.MessagePlatformAdapter):
|
|||||||
try:
|
try:
|
||||||
lb_event = await self.event_converter.target2yiri(update, self.bot, self.bot_account_id)
|
lb_event = await self.event_converter.target2yiri(update, self.bot, self.bot_account_id)
|
||||||
await self.listeners[type(lb_event)](lb_event, self)
|
await self.listeners[type(lb_event)](lb_event, self)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
print(traceback.format_exc())
|
await self.logger.error(f"Error in telegram callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
self.application = ApplicationBuilder().token(self.config['token']).build()
|
self.application = ApplicationBuilder().token(self.config['token']).build()
|
||||||
self.bot = self.application.bot
|
self.bot = self.application.bot
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from ..types import message as platform_message
|
|||||||
from ..types import events as platform_events
|
from ..types import events as platform_events
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...utils import image
|
from ...utils import image
|
||||||
|
from ..logger import EventLogger
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from typing import Optional, List, Tuple
|
from typing import Optional, List, Tuple
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@@ -533,9 +534,10 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter):
|
|||||||
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
self.quart_app = quart.Quart(__name__)
|
self.quart_app = quart.Quart(__name__)
|
||||||
|
|
||||||
self.message_converter = WeChatPadMessageConverter(config)
|
self.message_converter = WeChatPadMessageConverter(config)
|
||||||
@@ -550,7 +552,7 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter):
|
|||||||
try:
|
try:
|
||||||
event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id)
|
event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in wechatpad callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event.__class__ in self.listeners:
|
if event.__class__ in self.listeners:
|
||||||
await self.listeners[event.__class__](event, self)
|
await self.listeners[event.__class__](event, self)
|
||||||
@@ -694,7 +696,8 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter):
|
|||||||
|
|
||||||
self.bot = WeChatPadClient(
|
self.bot = WeChatPadClient(
|
||||||
self.config['wechatpad_url'],
|
self.config['wechatpad_url'],
|
||||||
self.config["token"]
|
self.config["token"],
|
||||||
|
logger=self.logger
|
||||||
)
|
)
|
||||||
self.ap.logger.info(self.config["token"])
|
self.ap.logger.info(self.config["token"])
|
||||||
thread_1 = threading.Event()
|
thread_1 = threading.Event()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from ...core import app
|
|||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...command.errors import ParamNotEnoughError
|
from ...command.errors import ParamNotEnoughError
|
||||||
from ...utils import image
|
from ...utils import image
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class WecomMessageConverter(adapter.MessageConverter):
|
class WecomMessageConverter(adapter.MessageConverter):
|
||||||
@@ -134,10 +135,10 @@ class WecomAdapter(adapter.MessagePlatformAdapter):
|
|||||||
event_converter: WecomEventConverter = WecomEventConverter()
|
event_converter: WecomEventConverter = WecomEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'corpid',
|
'corpid',
|
||||||
@@ -156,6 +157,7 @@ class WecomAdapter(adapter.MessagePlatformAdapter):
|
|||||||
token=config['token'],
|
token=config['token'],
|
||||||
EncodingAESKey=config['EncodingAESKey'],
|
EncodingAESKey=config['EncodingAESKey'],
|
||||||
contacts_secret=config['contacts_secret'],
|
contacts_secret=config['contacts_secret'],
|
||||||
|
logger=self.logger
|
||||||
)
|
)
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
@@ -199,8 +201,8 @@ class WecomAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.bot_account_id = event.receiver_id
|
self.bot_account_id = event.receiver_id
|
||||||
try:
|
try:
|
||||||
return await callback(await self.event_converter.target2yiri(event), self)
|
return await callback(await self.event_converter.target2yiri(event), self)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in wecom callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('text')(on_message)
|
self.bot.on_message('text')(on_message)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from pkg.core import app
|
|||||||
from .. import adapter
|
from .. import adapter
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...command.errors import ParamNotEnoughError
|
from ...command.errors import ParamNotEnoughError
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class WecomMessageConverter(adapter.MessageConverter):
|
class WecomMessageConverter(adapter.MessageConverter):
|
||||||
@@ -124,10 +125,10 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter):
|
|||||||
event_converter: WecomEventConverter = WecomEventConverter()
|
event_converter: WecomEventConverter = WecomEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'corpid',
|
'corpid',
|
||||||
@@ -144,6 +145,7 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter):
|
|||||||
secret=config['secret'],
|
secret=config['secret'],
|
||||||
token=config['token'],
|
token=config['token'],
|
||||||
EncodingAESKey=config['EncodingAESKey'],
|
EncodingAESKey=config['EncodingAESKey'],
|
||||||
|
logger=self.logger
|
||||||
)
|
)
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
@@ -176,8 +178,8 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.bot_account_id = event.receiver_id
|
self.bot_account_id = event.receiver_id
|
||||||
try:
|
try:
|
||||||
return await callback(await self.event_converter.target2yiri(event), self)
|
return await callback(await self.event_converter.target2yiri(event), self)
|
||||||
except:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in wecomcs callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('text')(on_message)
|
self.bot.on_message('text')(on_message)
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import logging
|
|||||||
import typing
|
import typing
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import base64
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
import httpx
|
||||||
import pydantic.v1 as pydantic
|
import pydantic.v1 as pydantic
|
||||||
|
|
||||||
from . import entities as platform_entities
|
from . import entities as platform_entities
|
||||||
@@ -552,52 +555,29 @@ class Image(MessageComponent):
|
|||||||
image_id = image_id[1:]
|
image_id = image_id[1:]
|
||||||
return image_id
|
return image_id
|
||||||
|
|
||||||
async def download(
|
async def get_bytes(self) -> typing.Tuple[bytes, str]:
|
||||||
self,
|
"""获取图片的 bytes 和 mime type"""
|
||||||
filename: typing.Union[str, Path, None] = None,
|
if self.url:
|
||||||
directory: typing.Union[str, Path, None] = None,
|
async with httpx.AsyncClient() as client:
|
||||||
determine_type: bool = True,
|
response = await client.get(self.url)
|
||||||
):
|
response.raise_for_status()
|
||||||
"""下载图片到本地。
|
return response.content, response.headers.get('Content-Type')
|
||||||
|
elif self.base64:
|
||||||
|
mime_type = 'image/jpeg'
|
||||||
|
|
||||||
Args:
|
split_index = self.base64.find(';base64,')
|
||||||
filename: 下载到本地的文件路径。与 `directory` 二选一。
|
if split_index == -1:
|
||||||
directory: 下载到本地的文件夹路径。与 `filename` 二选一。
|
raise ValueError('Invalid base64 string')
|
||||||
determine_type: 是否自动根据图片类型确定拓展名,默认为 True。
|
|
||||||
"""
|
|
||||||
if not self.url:
|
|
||||||
logger.warning(f'图片 `{self.uuid}` 无 url 参数,下载失败。')
|
|
||||||
return
|
|
||||||
|
|
||||||
import httpx
|
mime_type = self.base64[5:split_index]
|
||||||
|
base64_data = self.base64[split_index + 8 :]
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
return base64.b64decode(base64_data), mime_type
|
||||||
response = await client.get(self.url)
|
elif self.path:
|
||||||
response.raise_for_status()
|
async with aiofiles.open(self.path, 'rb') as f:
|
||||||
content = response.content
|
return await f.read(), 'image/jpeg'
|
||||||
|
else:
|
||||||
if filename:
|
raise ValueError('Can not get bytes from image')
|
||||||
path = Path(filename)
|
|
||||||
if determine_type:
|
|
||||||
import imghdr
|
|
||||||
|
|
||||||
path = path.with_suffix('.' + str(imghdr.what(None, content)))
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
elif directory:
|
|
||||||
import imghdr
|
|
||||||
|
|
||||||
path = Path(directory)
|
|
||||||
path.mkdir(parents=True, exist_ok=True)
|
|
||||||
path = path / f'{self.uuid}.{imghdr.what(None, content)}'
|
|
||||||
else:
|
|
||||||
raise ValueError('请指定文件路径或文件夹路径!')
|
|
||||||
|
|
||||||
import aiofiles
|
|
||||||
|
|
||||||
async with aiofiles.open(path, 'wb') as f:
|
|
||||||
await f.write(content)
|
|
||||||
|
|
||||||
return path
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_local(
|
async def from_local(
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester):
|
|||||||
|
|
||||||
if chunk.choices[0].delta.tool_calls is not None:
|
if chunk.choices[0].delta.tool_calls is not None:
|
||||||
for tool_call in chunk.choices[0].delta.tool_calls:
|
for tool_call in chunk.choices[0].delta.tool_calls:
|
||||||
|
if tool_call.function.arguments is None:
|
||||||
|
continue
|
||||||
for tc in tool_calls:
|
for tc in tool_calls:
|
||||||
if tc.index == tool_call.index:
|
if tc.index == tool_call.index:
|
||||||
tc.function.arguments += tool_call.function.arguments
|
tc.function.arguments += tool_call.function.arguments
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ class RuntimeMCPSession:
|
|||||||
|
|
||||||
for tool in tools.tools:
|
for tool in tools.tools:
|
||||||
|
|
||||||
async def func(query: core_entities.Query, **kwargs):
|
async def func(query: core_entities.Query, *, _tool=tool, **kwargs):
|
||||||
result = await self.session.call_tool(tool.name, kwargs)
|
result = await self.session.call_tool(_tool.name, kwargs)
|
||||||
if result.isError:
|
if result.isError:
|
||||||
raise Exception(result.content[0].text)
|
raise Exception(result.content[0].text)
|
||||||
return result.content[0].text
|
return result.content[0].text
|
||||||
|
|||||||
0
pkg/storage/__init__.py
Normal file
0
pkg/storage/__init__.py
Normal file
21
pkg/storage/mgr.py
Normal file
21
pkg/storage/mgr.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
from ..core import app
|
||||||
|
from . import provider
|
||||||
|
from .providers import localstorage
|
||||||
|
|
||||||
|
|
||||||
|
class StorageMgr:
|
||||||
|
"""存储管理器"""
|
||||||
|
|
||||||
|
ap: app.Application
|
||||||
|
|
||||||
|
storage_provider: provider.StorageProvider
|
||||||
|
|
||||||
|
def __init__(self, ap: app.Application):
|
||||||
|
self.ap = ap
|
||||||
|
self.storage_provider = localstorage.LocalStorageProvider(ap)
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
await self.storage_provider.initialize()
|
||||||
44
pkg/storage/provider.py
Normal file
44
pkg/storage/provider.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import abc
|
||||||
|
|
||||||
|
from ..core import app
|
||||||
|
|
||||||
|
|
||||||
|
class StorageProvider(abc.ABC):
|
||||||
|
ap: app.Application
|
||||||
|
|
||||||
|
def __init__(self, ap: app.Application):
|
||||||
|
self.ap = ap
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def save(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
value: bytes,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def load(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
) -> bytes:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def exists(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def delete(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
):
|
||||||
|
pass
|
||||||
0
pkg/storage/providers/__init__.py
Normal file
0
pkg/storage/providers/__init__.py
Normal file
45
pkg/storage/providers/localstorage.py
Normal file
45
pkg/storage/providers/localstorage.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import aiofiles
|
||||||
|
|
||||||
|
from ...core import app
|
||||||
|
|
||||||
|
from .. import provider
|
||||||
|
|
||||||
|
|
||||||
|
LOCAL_STORAGE_PATH = os.path.join('data', 'storage')
|
||||||
|
|
||||||
|
|
||||||
|
class LocalStorageProvider(provider.StorageProvider):
|
||||||
|
def __init__(self, ap: app.Application):
|
||||||
|
super().__init__(ap)
|
||||||
|
if not os.path.exists(LOCAL_STORAGE_PATH):
|
||||||
|
os.makedirs(LOCAL_STORAGE_PATH)
|
||||||
|
|
||||||
|
async def save(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
value: bytes,
|
||||||
|
):
|
||||||
|
async with aiofiles.open(os.path.join(LOCAL_STORAGE_PATH, f'{key}'), 'wb') as f:
|
||||||
|
await f.write(value)
|
||||||
|
|
||||||
|
async def load(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
) -> bytes:
|
||||||
|
async with aiofiles.open(os.path.join(LOCAL_STORAGE_PATH, f'{key}'), 'rb') as f:
|
||||||
|
return await f.read()
|
||||||
|
|
||||||
|
async def exists(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
) -> bool:
|
||||||
|
return os.path.exists(os.path.join(LOCAL_STORAGE_PATH, f'{key}'))
|
||||||
|
|
||||||
|
async def delete(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
):
|
||||||
|
os.remove(os.path.join(LOCAL_STORAGE_PATH, f'{key}'))
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
semantic_version = 'v4.0.3.2'
|
semantic_version = 'v4.0.4'
|
||||||
|
|
||||||
required_database_version = 2
|
required_database_version = 2
|
||||||
"""标记本版本所需要的数据库结构版本,用于判断数据库迁移"""
|
"""标记本版本所需要的数据库结构版本,用于判断数据库迁移"""
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import re
|
import re
|
||||||
import inspect
|
import inspect
|
||||||
|
import typing
|
||||||
|
|
||||||
|
|
||||||
def get_func_schema(function: callable) -> dict:
|
def get_func_schema(function: typing.Callable) -> dict:
|
||||||
"""
|
"""
|
||||||
Return the data schema of a function.
|
Return the data schema of a function.
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
"ignore-rules": {
|
"ignore-rules": {
|
||||||
"prefix": [],
|
"prefix": [],
|
||||||
"regexp": []
|
"regexp": []
|
||||||
|
},
|
||||||
|
"misc": {
|
||||||
|
"combine-quote-message": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"safety": {
|
"safety": {
|
||||||
|
|||||||
10
web/package-lock.json
generated
10
web/package-lock.json
generated
@@ -36,6 +36,7 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.56.3",
|
"react-hook-form": "^7.56.3",
|
||||||
"react-i18next": "^15.5.1",
|
"react-i18next": "^15.5.1",
|
||||||
|
"react-photo-view": "^1.2.7",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss": "^4.1.5",
|
"tailwindcss": "^4.1.5",
|
||||||
@@ -6126,6 +6127,15 @@
|
|||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/react-photo-view": {
|
||||||
|
"version": "1.2.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/react-photo-view/-/react-photo-view-1.2.7.tgz",
|
||||||
|
"integrity": "sha512-MfOWVPxuibncRLaycZUNxqYU8D9IA+rbGDDaq6GM8RIoGJal592hEJoRAyRSI7ZxyyJNJTLMUWWL3UIXHJJOpw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-remove-scroll": {
|
"node_modules/react-remove-scroll": {
|
||||||
"version": "2.6.3",
|
"version": "2.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.56.3",
|
"react-hook-form": "^7.56.3",
|
||||||
"react-i18next": "^15.5.1",
|
"react-i18next": "^15.5.1",
|
||||||
|
"react-photo-view": "^1.2.7",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss": "^4.1.5",
|
"tailwindcss": "^4.1.5",
|
||||||
|
|||||||
63
web/src/app/home/bots/bot-log/BotLogManager.ts
Normal file
63
web/src/app/home/bots/bot-log/BotLogManager.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
|
import {
|
||||||
|
BotLog,
|
||||||
|
GetBotLogsResponse,
|
||||||
|
} from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
||||||
|
|
||||||
|
export class BotLogManager {
|
||||||
|
private botId: string;
|
||||||
|
private callbacks: ((_: BotLog[]) => void)[] = [];
|
||||||
|
private intervalIds: number[] = [];
|
||||||
|
|
||||||
|
constructor(botId: string) {
|
||||||
|
this.botId = botId;
|
||||||
|
}
|
||||||
|
|
||||||
|
startListenServerPush() {
|
||||||
|
const timerNumber = setInterval(() => {
|
||||||
|
this.getLogList(-1, 50).then((response) => {
|
||||||
|
this.callbacks.forEach((callback) =>
|
||||||
|
callback(this.parseResponse(response)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
this.intervalIds.push(Number(timerNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
stopServerPush() {
|
||||||
|
this.intervalIds.forEach((id) => clearInterval(id));
|
||||||
|
this.intervalIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeLogPush(callback: (_: BotLog[]) => void) {
|
||||||
|
if (!this.callbacks.includes(callback)) {
|
||||||
|
this.callbacks.push(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.callbacks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日志页的基本信息
|
||||||
|
*/
|
||||||
|
private getLogList(next: number, count: number = 20) {
|
||||||
|
return httpClient.getBotLogs(this.botId, {
|
||||||
|
from_index: next,
|
||||||
|
max_count: count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFirstPage() {
|
||||||
|
return this.parseResponse(await this.getLogList(-1, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadMore(position: number, total: number) {
|
||||||
|
return this.parseResponse(await this.getLogList(position, total));
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseResponse(httpResponse: GetBotLogsResponse): BotLog[] {
|
||||||
|
return httpResponse.logs;
|
||||||
|
}
|
||||||
|
}
|
||||||
116
web/src/app/home/bots/bot-log/view/BotLogCard.tsx
Normal file
116
web/src/app/home/bots/bot-log/view/BotLogCard.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { BotLog } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
||||||
|
import styles from './botLog.module.css';
|
||||||
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
|
import { PhotoProvider } from 'react-photo-view';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export function BotLogCard({ botLog }: { botLog: BotLog }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const baseURL = httpClient.getBaseUrl();
|
||||||
|
|
||||||
|
function formatTime(timestamp: number) {
|
||||||
|
const now = new Date();
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
|
||||||
|
// 获取各个时间部分
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth() + 1; // 月份从0开始,需要+1
|
||||||
|
const day = date.getDate();
|
||||||
|
const hours = date.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
// 判断时间范围
|
||||||
|
const isToday = now.toDateString() === date.toDateString();
|
||||||
|
const isYesterday =
|
||||||
|
new Date(now.setDate(now.getDate() - 1)).toDateString() ===
|
||||||
|
date.toDateString();
|
||||||
|
const isThisYear = now.getFullYear() === year;
|
||||||
|
|
||||||
|
if (isToday) {
|
||||||
|
return `${hours}:${minutes}`; // 今天的消息:小时:分钟
|
||||||
|
} else if (isYesterday) {
|
||||||
|
return `${t('bots.yesterday')} ${hours}:${minutes}`; // 昨天的消息:昨天 小时:分钟
|
||||||
|
} else if (isThisYear) {
|
||||||
|
return t('bots.dateFormat', { month, day }); // 本年消息:x月x日
|
||||||
|
} else {
|
||||||
|
return t('bots.earlier'); // 更早的消息:更久之前
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubChatId(str: string) {
|
||||||
|
const strArr = str.split('');
|
||||||
|
return strArr;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={`${styles.botLogCardContainer}`}>
|
||||||
|
{/* 头部标签,时间 */}
|
||||||
|
<div className={`${styles.cardTitleContainer}`}>
|
||||||
|
<div className={`flex flex-row gap-4`}>
|
||||||
|
<div className={`${styles.tag}`}>{botLog.level}</div>
|
||||||
|
{botLog.message_session_id && (
|
||||||
|
<div
|
||||||
|
className={`${styles.tag} ${styles.chatTag}`}
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(botLog.message_session_id)
|
||||||
|
.then(() => {
|
||||||
|
toast.success(t('common.copySuccess'));
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="icon"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
p-id="1664"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M96.1 575.7a32.2 32.1 0 1 0 64.4 0 32.2 32.1 0 1 0-64.4 0Z"
|
||||||
|
p-id="1665"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M742.1 450.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.1 26-13.8 26-31s-11.7-31.3-26-31.4zM742.1 577.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.2 26-13.8 26-31s-11.7-31.3-26-31.4z"
|
||||||
|
p-id="1666"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M736.1 63.9H417c-70.4 0-128 57.6-128 128h-64.9c-70.4 0-128 57.6-128 128v128c-0.1 17.7 14.4 32 32.2 32 17.8 0 32.2-14.4 32.2-32.1V320c0-35.2 28.8-64 64-64H289v447.8c0 70.4 57.6 128 128 128h255.1c-0.1 35.2-28.8 63.8-64 63.8H224.5c-35.2 0-64-28.8-64-64V703.5c0-17.7-14.4-32.1-32.2-32.1-17.8 0-32.3 14.4-32.3 32.1v128.3c0 70.4 57.6 128 128 128h384.1c70.4 0 128-57.6 128-128h65c70.4 0 128-57.6 128-128V255.9l-193-192z m0.1 63.4l127.7 128.3H800c-35.2 0-64-28.8-64-64v-64.3h0.2z m64 641H416.1c-35.2 0-64-28.8-64-64v-513c0-35.2 28.8-64 64-64H671V191c0 70.4 57.6 128 128 128h65.2v385.3c0 35.2-28.8 64-64 64z"
|
||||||
|
p-id="1667"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
{/* 会话ID */}
|
||||||
|
|
||||||
|
<span className={`${styles.chatId}`}>
|
||||||
|
{getSubChatId(botLog.message_session_id)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>{formatTime(botLog.timestamp)}</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.cardTitleContainer} ${styles.cardText}`}>
|
||||||
|
{botLog.text}
|
||||||
|
</div>
|
||||||
|
<PhotoProvider className={``}>
|
||||||
|
<div className={`w-50 mt-2`}>
|
||||||
|
{botLog.images.map((item) => (
|
||||||
|
<img
|
||||||
|
key={item}
|
||||||
|
src={`${baseURL}/api/v1/files/image/${item}`}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PhotoProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
129
web/src/app/home/bots/bot-log/view/BotLogListComponent.tsx
Normal file
129
web/src/app/home/bots/bot-log/view/BotLogListComponent.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { BotLogManager } from '@/app/home/bots/bot-log/BotLogManager';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { BotLog } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
||||||
|
import { BotLogCard } from '@/app/home/bots/bot-log/view/BotLogCard';
|
||||||
|
import styles from './botLog.module.css';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export function BotLogListComponent({ botId }: { botId: string }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const manager = useRef(new BotLogManager(botId)).current;
|
||||||
|
const [botLogList, setBotLogList] = useState<BotLog[]>([]);
|
||||||
|
const [autoFlush, setAutoFlush] = useState(true);
|
||||||
|
const listContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const botLogListRef = useRef<BotLog[]>(botLogList);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initComponent();
|
||||||
|
return () => {
|
||||||
|
onDestroy();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
botLogListRef.current = botLogList;
|
||||||
|
}, [botLogList]);
|
||||||
|
|
||||||
|
// 观测自动刷新状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoFlush) {
|
||||||
|
manager.startListenServerPush();
|
||||||
|
} else {
|
||||||
|
manager.stopServerPush();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
manager.stopServerPush();
|
||||||
|
};
|
||||||
|
}, [autoFlush]);
|
||||||
|
|
||||||
|
function initComponent() {
|
||||||
|
// 订阅日志推送
|
||||||
|
manager.subscribeLogPush(handleBotLogPush);
|
||||||
|
// 加载第一页日志
|
||||||
|
manager.loadFirstPage().then((response) => {
|
||||||
|
setBotLogList(response.reverse());
|
||||||
|
});
|
||||||
|
// 监听滚动
|
||||||
|
listenScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDestroy() {
|
||||||
|
manager.dispose();
|
||||||
|
removeScrollListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
function listenScroll() {
|
||||||
|
if (!listContainerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = listContainerRef.current;
|
||||||
|
list.addEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeScrollListener() {
|
||||||
|
if (!listContainerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = listContainerRef.current;
|
||||||
|
list.removeEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMore() {
|
||||||
|
// 加载更多日志
|
||||||
|
const list = botLogListRef.current;
|
||||||
|
const lastSeq = list[list.length - 1].seq_id;
|
||||||
|
if (lastSeq === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
manager.loadMore(lastSeq - 1, 10).then((response) => {
|
||||||
|
setBotLogList([...list, ...response.reverse()]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBotLogPush(response: BotLog[]) {
|
||||||
|
setBotLogList(response.reverse());
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScroll = useCallback(
|
||||||
|
debounce(() => {
|
||||||
|
if (!listContainerRef.current) return;
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } =
|
||||||
|
listContainerRef.current;
|
||||||
|
const isBottom = scrollTop + clientHeight >= scrollHeight - 5;
|
||||||
|
const isTop = scrollTop === 0;
|
||||||
|
|
||||||
|
if (isBottom) {
|
||||||
|
setAutoFlush(false);
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
if (isTop) {
|
||||||
|
setAutoFlush(true);
|
||||||
|
}
|
||||||
|
if (!isTop && !isBottom) {
|
||||||
|
setAutoFlush(false);
|
||||||
|
}
|
||||||
|
}, 300), // 防抖延迟 300ms
|
||||||
|
[botLogList], // 依赖项为空
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.botLogListContainer} px-6`}
|
||||||
|
ref={listContainerRef}
|
||||||
|
>
|
||||||
|
<div className={`${styles.listHeader}`}>
|
||||||
|
<div className={'mr-2'}>{t('bots.enableAutoRefresh')}</div>
|
||||||
|
<Switch checked={autoFlush} onCheckedChange={(e) => setAutoFlush(e)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{botLogList.map((botLog) => {
|
||||||
|
return <BotLogCard botLog={botLog} key={botLog.seq_id} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
68
web/src/app/home/bots/bot-log/view/botLog.module.css
Normal file
68
web/src/app/home/bots/bot-log/view/botLog.module.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
.botLogListContainer {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 10rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.botLogCardContainer {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
padding: 1.2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listHeader {
|
||||||
|
width: 100%;
|
||||||
|
height: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.2rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background-color: #a5d8ff;
|
||||||
|
color: #ffffff;
|
||||||
|
max-width: 16rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatTag {
|
||||||
|
color: #626262;
|
||||||
|
background-color: #d1d1d1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatId {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardTitleContainer {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardText {
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
@@ -1,7 +1,34 @@
|
|||||||
import { BotCardVO } from '@/app/home/bots/components/bot-card/BotCardVO';
|
import { BotCardVO } from '@/app/home/bots/components/bot-card/BotCardVO';
|
||||||
import styles from './botCard.module.css';
|
import styles from './botCard.module.css';
|
||||||
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export default function BotCard({
|
||||||
|
botCardVO,
|
||||||
|
clickLogIconCallback,
|
||||||
|
setBotEnableCallback,
|
||||||
|
}: {
|
||||||
|
botCardVO: BotCardVO;
|
||||||
|
clickLogIconCallback: (id: string) => void;
|
||||||
|
setBotEnableCallback: (id: string, enable: boolean) => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
function onClickLogIcon() {
|
||||||
|
clickLogIconCallback(botCardVO.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBotEnable(enable: boolean) {
|
||||||
|
return httpClient.updateBot(botCardVO.id, {
|
||||||
|
name: botCardVO.name,
|
||||||
|
description: botCardVO.description,
|
||||||
|
adapter: botCardVO.adapter,
|
||||||
|
adapter_config: botCardVO.adapterConfig,
|
||||||
|
enable: enable,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default function BotCard({ botCardVO }: { botCardVO: BotCardVO }) {
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.cardContainer}`}>
|
<div className={`${styles.cardContainer}`}>
|
||||||
<div className={`${styles.iconBasicInfoContainer}`}>
|
<div className={`${styles.iconBasicInfoContainer}`}>
|
||||||
@@ -47,6 +74,44 @@ export default function BotCard({ botCardVO }: { botCardVO: BotCardVO }) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={`${styles.botOperationContainer}`}>
|
||||||
|
<Switch
|
||||||
|
checked={botCardVO.enable}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
setBotEnable(e)
|
||||||
|
.then(() => {
|
||||||
|
setBotEnableCallback(botCardVO.id, e);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
toast.error(t('bots.setBotEnableError'));
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`${styles.botLogsIcon}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
onClickLogIcon();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-[24px] h-[24px] z-10"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M21 8V20.9932C21 21.5501 20.5552 22 20.0066 22H3.9934C3.44495 22 3 21.556 3 21.0082V2.9918C3 2.45531 3.4487 2 4.00221 2H14.9968L21 8ZM19 9H14V4H5V20H19V9ZM8 7H11V9H8V7ZM8 11H16V13H8V11ZM8 15H16V17H8V15Z"
|
||||||
|
fill="#9A9A9A"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ export interface IBotCardVO {
|
|||||||
iconURL: string;
|
iconURL: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
adapter: string;
|
||||||
adapterLabel: string;
|
adapterLabel: string;
|
||||||
|
adapterConfig: object;
|
||||||
usePipelineName: string;
|
usePipelineName: string;
|
||||||
|
enable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BotCardVO implements IBotCardVO {
|
export class BotCardVO implements IBotCardVO {
|
||||||
@@ -12,15 +15,21 @@ export class BotCardVO implements IBotCardVO {
|
|||||||
iconURL: string;
|
iconURL: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
adapter: string;
|
||||||
adapterLabel: string;
|
adapterLabel: string;
|
||||||
|
adapterConfig: object;
|
||||||
usePipelineName: string;
|
usePipelineName: string;
|
||||||
|
enable: boolean;
|
||||||
|
|
||||||
constructor(props: IBotCardVO) {
|
constructor(props: IBotCardVO) {
|
||||||
this.id = props.id;
|
this.id = props.id;
|
||||||
this.iconURL = props.iconURL;
|
this.iconURL = props.iconURL;
|
||||||
this.name = props.name;
|
this.name = props.name;
|
||||||
this.description = props.description;
|
this.description = props.description;
|
||||||
|
this.adapter = props.adapter;
|
||||||
|
this.adapterConfig = props.adapterConfig;
|
||||||
this.adapterLabel = props.adapterLabel;
|
this.adapterLabel = props.adapterLabel;
|
||||||
this.usePipelineName = props.usePipelineName;
|
this.usePipelineName = props.usePipelineName;
|
||||||
|
this.enable = props.enable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
/* background-color: aqua; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconImage {
|
.iconImage {
|
||||||
@@ -30,10 +29,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.basicInfoContainer {
|
.basicInfoContainer {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
min-width: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,4 +103,14 @@
|
|||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.botOperationContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 100%;
|
||||||
|
width: 3rem;
|
||||||
|
gap: 0.4rem;
|
||||||
}
|
}
|
||||||
@@ -202,6 +202,7 @@ export default function BotForm({
|
|||||||
default: item.default,
|
default: item.default,
|
||||||
id: UUID.generate(),
|
id: UUID.generate(),
|
||||||
label: item.label,
|
label: item.label,
|
||||||
|
description: item.description,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
required: item.required,
|
required: item.required,
|
||||||
type: parseDynamicFormItemType(item.type),
|
type: parseDynamicFormItemType(item.type),
|
||||||
|
|||||||
@@ -17,13 +17,18 @@ import {
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { i18nObj } from '@/i18n/I18nProvider';
|
import { i18nObj } from '@/i18n/I18nProvider';
|
||||||
|
import { BotLogListComponent } from '@/app/home/bots/bot-log/view/BotLogListComponent';
|
||||||
|
|
||||||
export default function BotConfigPage() {
|
export default function BotConfigPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
// 编辑机器人的modal
|
||||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||||
|
// 机器人日志的modal
|
||||||
|
const [logModalOpen, setLogModalOpen] = useState<boolean>(false);
|
||||||
const [botList, setBotList] = useState<BotCardVO[]>([]);
|
const [botList, setBotList] = useState<BotCardVO[]>([]);
|
||||||
const [isEditForm, setIsEditForm] = useState(false);
|
const [isEditForm, setIsEditForm] = useState(false);
|
||||||
const [nowSelectedBotUUID, setNowSelectedBotUUID] = useState<string>();
|
const [nowSelectedBotUUID, setNowSelectedBotUUID] = useState<string>();
|
||||||
|
const [nowSelectedBotLog, setNowSelectedBotLog] = useState<string>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getBotList();
|
getBotList();
|
||||||
@@ -47,10 +52,13 @@ export default function BotConfigPage() {
|
|||||||
iconURL: httpClient.getAdapterIconURL(bot.adapter),
|
iconURL: httpClient.getAdapterIconURL(bot.adapter),
|
||||||
name: bot.name,
|
name: bot.name,
|
||||||
description: bot.description,
|
description: bot.description,
|
||||||
|
adapter: bot.adapter,
|
||||||
|
adapterConfig: bot.adapter_config,
|
||||||
adapterLabel:
|
adapterLabel:
|
||||||
adapterList.find((item) => item.value === bot.adapter)?.label ||
|
adapterList.find((item) => item.value === bot.adapter)?.label ||
|
||||||
bot.adapter.substring(0, 10),
|
bot.adapter.substring(0, 10),
|
||||||
usePipelineName: bot.use_pipeline_name || '',
|
usePipelineName: bot.use_pipeline_name || '',
|
||||||
|
enable: bot.enable || false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
setBotList(botList);
|
setBotList(botList);
|
||||||
@@ -76,6 +84,11 @@ export default function BotConfigPage() {
|
|||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClickLogIcon(botId: string) {
|
||||||
|
setNowSelectedBotLog(botId);
|
||||||
|
setLogModalOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.configPageContainer}>
|
<div className={styles.configPageContainer}>
|
||||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||||
@@ -107,6 +120,15 @@ export default function BotConfigPage() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={logModalOpen} onOpenChange={setLogModalOpen}>
|
||||||
|
<DialogContent className="w-[700px] max-h-[80vh] p-0 flex flex-col">
|
||||||
|
<DialogHeader className="px-6 pt-6 pb-0">
|
||||||
|
<DialogTitle>{t('bots.botLogTitle')}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<BotLogListComponent botId={nowSelectedBotLog || ''} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* 注意:其余的返回内容需要保持在Spin组件外部 */}
|
{/* 注意:其余的返回内容需要保持在Spin组件外部 */}
|
||||||
<div className={`${styles.botListContainer}`}>
|
<div className={`${styles.botListContainer}`}>
|
||||||
<CreateCardComponent
|
<CreateCardComponent
|
||||||
@@ -123,7 +145,22 @@ export default function BotConfigPage() {
|
|||||||
selectBot(cardVO.id);
|
selectBot(cardVO.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BotCard botCardVO={cardVO} />
|
<BotCard
|
||||||
|
botCardVO={cardVO}
|
||||||
|
clickLogIconCallback={(id) => {
|
||||||
|
onClickLogIcon(id);
|
||||||
|
}}
|
||||||
|
setBotEnableCallback={(id, enable) => {
|
||||||
|
setBotList(
|
||||||
|
botList.map((bot) => {
|
||||||
|
if (bot.id === id) {
|
||||||
|
return { ...bot, enable: enable };
|
||||||
|
}
|
||||||
|
return bot;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -169,7 +169,6 @@ export default function LLMForm({
|
|||||||
} else {
|
} else {
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -84,18 +84,10 @@ export interface Adapter {
|
|||||||
description: I18nLabel;
|
description: I18nLabel;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
spec: {
|
spec: {
|
||||||
config: AdapterSpecConfig[];
|
config: IDynamicFormItemSchema[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdapterSpecConfig {
|
|
||||||
default: string | number | boolean | Array<unknown>;
|
|
||||||
label: I18nLabel;
|
|
||||||
name: string;
|
|
||||||
required: boolean;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApiRespPlatformBots {
|
export interface ApiRespPlatformBots {
|
||||||
bots: Bot[];
|
bots: Bot[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import {
|
|||||||
GetPipelineMetadataResponseData,
|
GetPipelineMetadataResponseData,
|
||||||
AsyncTask,
|
AsyncTask,
|
||||||
} from '@/app/infra/entities/api';
|
} from '@/app/infra/entities/api';
|
||||||
|
import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest';
|
||||||
|
import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
||||||
|
|
||||||
type JSONValue = string | number | boolean | JSONObject | JSONArray | null;
|
type JSONValue = string | number | boolean | JSONObject | JSONArray | null;
|
||||||
interface JSONObject {
|
interface JSONObject {
|
||||||
@@ -54,12 +56,14 @@ export let systemInfo: ApiRespSystemInfo | null = null;
|
|||||||
class HttpClient {
|
class HttpClient {
|
||||||
private instance: AxiosInstance;
|
private instance: AxiosInstance;
|
||||||
private disableToken: boolean = false;
|
private disableToken: boolean = false;
|
||||||
|
private baseURL: string;
|
||||||
// 暂不需要SSR
|
// 暂不需要SSR
|
||||||
// private ssrInstance: AxiosInstance | null = null
|
// private ssrInstance: AxiosInstance | null = null
|
||||||
|
|
||||||
constructor(baseURL?: string, disableToken?: boolean) {
|
constructor(baseURL: string, disableToken?: boolean) {
|
||||||
|
this.baseURL = baseURL;
|
||||||
this.instance = axios.create({
|
this.instance = axios.create({
|
||||||
baseURL: baseURL || this.getBaseUrl(),
|
baseURL: baseURL,
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -75,15 +79,9 @@ class HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底URL,如果使用未配置会走到这里
|
// 外部获取baseURL的方法
|
||||||
private getBaseUrl(): string {
|
getBaseUrl(): string {
|
||||||
// NOT IMPLEMENT
|
return this.baseURL;
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
// 服务端环境
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
// 客户端环境
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取Session
|
// 获取Session
|
||||||
@@ -345,6 +343,13 @@ class HttpClient {
|
|||||||
return this.delete(`/api/v1/platform/bots/${uuid}`);
|
return this.delete(`/api/v1/platform/bots/${uuid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getBotLogs(
|
||||||
|
botId: string,
|
||||||
|
request: GetBotLogsRequest,
|
||||||
|
): Promise<GetBotLogsResponse> {
|
||||||
|
return this.post(`/api/v1/platform/bots/${botId}/logs`, request);
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Plugins API ============
|
// ============ Plugins API ============
|
||||||
public getPlugins(): Promise<ApiRespPlugins> {
|
public getPlugins(): Promise<ApiRespPlugins> {
|
||||||
return this.get('/api/v1/plugins');
|
return this.get('/api/v1/plugins');
|
||||||
@@ -450,7 +455,7 @@ class HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// export const httpClient = new HttpClient("https://version-4.langbot.dev");
|
// export const httpClient = new HttpClient('https://event-log.langbot.dev');
|
||||||
// export const httpClient = new HttpClient('http://localhost:5300');
|
// export const httpClient = new HttpClient('http://localhost:5300');
|
||||||
export const httpClient = new HttpClient('/');
|
export const httpClient = new HttpClient('/');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface GetBotLogsRequest {
|
||||||
|
from_index: number; // 从某索引开始往前找,-1代表结尾,也就是拉取最新的
|
||||||
|
max_count: number; // 最大拉取数量
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export interface GetBotLogsResponse {
|
||||||
|
logs: BotLog[];
|
||||||
|
total_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BotLog {
|
||||||
|
images: [];
|
||||||
|
level: string;
|
||||||
|
message_session_id: string;
|
||||||
|
seq_id: number;
|
||||||
|
text: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import './global.css';
|
import './global.css';
|
||||||
|
import 'react-photo-view/dist/react-photo-view.css';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
import I18nProvider from '@/i18n/I18nProvider';
|
import I18nProvider from '@/i18n/I18nProvider';
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ function DialogContent({
|
|||||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
onInteractOutside={() => {}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const enUS = {
|
|||||||
deleteSuccess: 'Deleted successfully',
|
deleteSuccess: 'Deleted successfully',
|
||||||
deleteError: 'Delete failed: ',
|
deleteError: 'Delete failed: ',
|
||||||
addRound: 'Add Round',
|
addRound: 'Add Round',
|
||||||
|
copySuccess: 'Copy Successfully',
|
||||||
test: 'Test',
|
test: 'Test',
|
||||||
},
|
},
|
||||||
notFound: {
|
notFound: {
|
||||||
@@ -120,6 +121,13 @@ const enUS = {
|
|||||||
adapterConfig: 'Adapter Configuration',
|
adapterConfig: 'Adapter Configuration',
|
||||||
bindPipeline: 'Bind Pipeline',
|
bindPipeline: 'Bind Pipeline',
|
||||||
selectPipeline: 'Select Pipeline',
|
selectPipeline: 'Select Pipeline',
|
||||||
|
botLogTitle: 'Bot Log',
|
||||||
|
enableAutoRefresh: 'Enable Auto Refresh',
|
||||||
|
session: 'Session',
|
||||||
|
yesterday: 'Yesterday',
|
||||||
|
earlier: 'Earlier',
|
||||||
|
dateFormat: '{{month}}/{{day}}',
|
||||||
|
setBotEnableError: 'Failed to set bot enable status',
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: 'Plugins',
|
title: 'Plugins',
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const zhHans = {
|
|||||||
deleteSuccess: '删除成功',
|
deleteSuccess: '删除成功',
|
||||||
deleteError: '删除失败:',
|
deleteError: '删除失败:',
|
||||||
addRound: '添加回合',
|
addRound: '添加回合',
|
||||||
|
copySuccess: '复制成功',
|
||||||
test: '测试',
|
test: '测试',
|
||||||
},
|
},
|
||||||
notFound: {
|
notFound: {
|
||||||
@@ -118,6 +119,13 @@ const zhHans = {
|
|||||||
adapterConfig: '适配器配置',
|
adapterConfig: '适配器配置',
|
||||||
bindPipeline: '绑定流水线',
|
bindPipeline: '绑定流水线',
|
||||||
selectPipeline: '选择流水线',
|
selectPipeline: '选择流水线',
|
||||||
|
botLogTitle: '机器人日志',
|
||||||
|
enableAutoRefresh: '开启自动刷新',
|
||||||
|
session: '会话',
|
||||||
|
yesterday: '昨天',
|
||||||
|
earlier: '更久之前',
|
||||||
|
dateFormat: '{{month}}月{{day}}日',
|
||||||
|
setBotEnableError: '设置机器人启用状态失败',
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: '插件管理',
|
title: '插件管理',
|
||||||
|
|||||||
Reference in New Issue
Block a user