Compare commits

...

7 Commits

Author SHA1 Message Date
Junyan Qin
239223be3f chore: release v4.0.4 2025-05-28 12:55:15 +08:00
Junyan Qin
b112cb320c fix: bad ability name in preproc check 2025-05-28 12:54:30 +08:00
Junyan Qin
5aaf2ba3ef fix: base url 2025-05-27 22:58:31 +08:00
Junyan Qin (Chin)
f1e9f46af1 feat: event log of bots (#1441)
* feat: basic arch of event log

* feat: complete event log framework

* fix: bad struct in bot log api

* feat: add event logging to all platform adapters

Co-Authored-By: wangcham233@gmail.com <651122857@qq.com>

* feat: add event logging to client classes

Co-Authored-By: wangcham233@gmail.com <651122857@qq.com>

* refactor: bot log getting api

* perf: logger for aiocqhttp and gewechat

* fix: add ignored logger in dingtalk

* fix: seq id bug in log getting

* feat: add logger in dingtalk,QQ official,Slack, wxoa

* feat: add logger for wecom

* feat: add logger for wecomcs

* perf(event logger): image processing

* 完成机器人日志的前端部分 (#1479)

* feat: webui  bot log framework done

* feat: bot log complete

* perf(bot-log): style

* chore: fix incompleted i18n

* feat: support message session copy

* fix: filter and badge text

* perf: styles

* feat: add bot toggle switch in bot card

* fix: linter errors

---------

Co-authored-by: Junyan Qin <rockchinq@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: wangcham233@gmail.com <651122857@qq.com>
Co-authored-by: HYana <65863826+KaedeSAMA@users.noreply.github.com>
2025-05-27 22:36:50 +08:00
aberry
8dfef1d118 Bugfix (#1482)
* Update modelscopechatcmpl.py

tool_call 流式输出的最后一个参数是 None,需要判断一下

* Update mcp.py

问题:闭包(closure)对循环变量 tool 的捕获,导致最终注册到 self.functions 里的所有 func,都会引用 同一个(最后一个)tool

解决:在定义 func 时,通过函数参数的 默认值 把当下的 tool “冻结”住

* Update mcp.py
2025-05-27 15:09:09 +08:00
Junyan Qin (Chin)
919a621bf8 fix: lru bug in t2i (#1445) (#1481) 2025-05-27 09:58:22 +08:00
Junyan Qin
3ac96f464d perf: show description in bot form 2025-05-23 10:31:11 +08:00
63 changed files with 1217 additions and 158 deletions

View File

@@ -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 连接,监听消息"""

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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'''

View File

@@ -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')

View File

@@ -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):

View 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)

View File

@@ -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,
}
)

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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):
"""主动发送消息 """主动发送消息

View File

@@ -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
View 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,
)

View File

@@ -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],

View File

@@ -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)

View File

@@ -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']

View File

@@ -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)

View File

@@ -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):

View File

@@ -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(

View File

@@ -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)

View File

@@ -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

View File

@@ -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(

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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(

View File

@@ -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

View File

@@ -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
View File

21
pkg/storage/mgr.py Normal file
View 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
View 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

View File

View 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}'))

View File

@@ -1,4 +1,4 @@
semantic_version = 'v4.0.3.3' semantic_version = 'v4.0.4'
required_database_version = 2 required_database_version = 2
"""标记本版本所需要的数据库结构版本,用于判断数据库迁移""" """标记本版本所需要的数据库结构版本,用于判断数据库迁移"""

View File

@@ -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.
{ {

10
web/package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View 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;
}
}

View 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>
);
}

View 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>
);
}

View 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;
}

View File

@@ -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>
); );

View File

@@ -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;
} }
} }

View File

@@ -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;
} }

View File

@@ -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),

View File

@@ -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>
); );
})} })}

View File

@@ -169,7 +169,6 @@ export default function LLMForm({
} else { } else {
form.reset(); form.reset();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}); });
}, []); }, []);

View File

@@ -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[];
} }

View File

@@ -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('/');

View File

@@ -0,0 +1,4 @@
export interface GetBotLogsRequest {
from_index: number; // 从某索引开始往前找,-1代表结尾也就是拉取最新的
max_count: number; // 最大拉取数量
}

View File

@@ -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;
}

View File

@@ -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';

View File

@@ -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}

View File

@@ -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',

View File

@@ -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: '插件管理',