mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-08 06:46:02 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f80f997a89 | ||
|
|
18529a42c1 | ||
|
|
3e707b4b6e | ||
|
|
62f0a938a8 | ||
|
|
ad3a163d82 | ||
|
|
f5a4503610 | ||
|
|
ec012cf5ed | ||
|
|
d70eceb72c | ||
|
|
f271608114 | ||
|
|
793f0a9c10 | ||
|
|
4f2ec195fc | ||
|
|
e6bc009414 | ||
|
|
20dc8fb5ab | ||
|
|
9a71edfeb0 | ||
|
|
fe3fd664af | ||
|
|
6402755ac6 | ||
|
|
ac8fe049de | ||
|
|
955b391253 | ||
|
|
08c6672841 | ||
|
|
8917050fae | ||
|
|
21daef46f7 | ||
|
|
8ad60b5b64 | ||
|
|
7e17c96c30 | ||
|
|
f17b06767e | ||
|
|
70a29fc623 | ||
|
|
239223be3f | ||
|
|
b112cb320c | ||
|
|
5aaf2ba3ef | ||
|
|
f1e9f46af1 | ||
|
|
8dfef1d118 | ||
|
|
919a621bf8 | ||
|
|
3ac96f464d | ||
|
|
f9f03b81d1 | ||
|
|
42171a9c07 | ||
|
|
f1f00115c9 |
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -7,7 +7,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: 运行环境
|
label: 运行环境
|
||||||
description: LangBot 版本、操作系统、系统架构、**Python版本**、**主机地理位置**
|
description: LangBot 版本、操作系统、系统架构、**Python版本**、**主机地理位置**
|
||||||
placeholder: 例如:v3.3.0、CentOS x64 Python 3.10.3、Docker 的系统直接写 Docker 就行
|
placeholder: 例如:v3.3.0、CentOS x64 Python 3.10.3、Docker
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -19,7 +19,7 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: 复现步骤
|
label: 复现步骤
|
||||||
description: 如何重现这个问题,越详细越好;提供越多信息,我们会越快解决问题。
|
description: 提供越多信息,我们会越快解决问题,建议多提供配置截图;**如果你不认真填写(只一两句话概括),我们会很生气并且立即关闭 issue 或两年后才回复你**
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/bug-report_en.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report_en.yml
vendored
@@ -7,7 +7,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Runtime environment
|
label: Runtime environment
|
||||||
description: LangBot version, operating system, system architecture, **Python version**, **host location**
|
description: LangBot version, operating system, system architecture, **Python version**, **host location**
|
||||||
placeholder: "For example: v3.3.0, CentOS x64 Python 3.10.3, Docker system directly write Docker"
|
placeholder: "For example: v3.3.0, CentOS x64 Python 3.10.3, Docker"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
@@ -154,3 +154,9 @@ docker compose up -d
|
|||||||
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
|
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
|
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
## 😎 保持更新
|
||||||
|
|
||||||
|
点击仓库右上角 Star 和 Watch 按钮,获取最新动态。
|
||||||
|
|
||||||
|

|
||||||
@@ -135,3 +135,9 @@ Thank you for the following [code contributors](https://github.com/RockChinQ/Lan
|
|||||||
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
|
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
|
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
## 😎 Stay Ahead
|
||||||
|
|
||||||
|
Click the Star and Watch button in the upper right corner of the repository to get the latest updates.
|
||||||
|
|
||||||
|

|
||||||
@@ -134,3 +134,9 @@ LangBot への貢献に対して、以下の [コード貢献者](https://github
|
|||||||
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
|
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
|
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
## 😎 最新情報を入手
|
||||||
|
|
||||||
|
リポジトリの右上にある Star と Watch ボタンをクリックして、最新の更新を取得してください。
|
||||||
|
|
||||||
|

|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from v1 import client
|
from v1 import client # type: ignore
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
@@ -8,19 +8,13 @@ import json
|
|||||||
|
|
||||||
class TestDifyClient:
|
class TestDifyClient:
|
||||||
async def test_chat_messages(self):
|
async def test_chat_messages(self):
|
||||||
cln = client.AsyncDifyServiceClient(
|
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
|
||||||
api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL')
|
|
||||||
)
|
|
||||||
|
|
||||||
async for chunk in cln.chat_messages(
|
async for chunk in cln.chat_messages(inputs={}, query='调用工具查看现在几点?', user='test'):
|
||||||
inputs={}, query='调用工具查看现在几点?', user='test'
|
|
||||||
):
|
|
||||||
print(json.dumps(chunk, ensure_ascii=False, indent=4))
|
print(json.dumps(chunk, ensure_ascii=False, indent=4))
|
||||||
|
|
||||||
async def test_upload_file(self):
|
async def test_upload_file(self):
|
||||||
cln = client.AsyncDifyServiceClient(
|
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
|
||||||
api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL')
|
|
||||||
)
|
|
||||||
|
|
||||||
file_bytes = open('img.png', 'rb').read()
|
file_bytes = open('img.png', 'rb').read()
|
||||||
|
|
||||||
@@ -32,9 +26,7 @@ class TestDifyClient:
|
|||||||
print(json.dumps(resp, ensure_ascii=False, indent=4))
|
print(json.dumps(resp, ensure_ascii=False, indent=4))
|
||||||
|
|
||||||
async def test_workflow_run(self):
|
async def test_workflow_run(self):
|
||||||
cln = client.AsyncDifyServiceClient(
|
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
|
||||||
api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL')
|
|
||||||
)
|
|
||||||
|
|
||||||
# resp = await cln.workflow_run(inputs={}, user="test")
|
# resp = await cln.workflow_run(inputs={}, user="test")
|
||||||
# # print(json.dumps(resp, ensure_ascii=False, indent=4))
|
# # print(json.dumps(resp, ensure_ascii=False, indent=4))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import dingtalk_stream
|
import dingtalk_stream # type: ignore
|
||||||
from dingtalk_stream import AckMessage
|
from dingtalk_stream import AckMessage
|
||||||
|
|
||||||
|
|
||||||
@@ -27,9 +27,3 @@ class EchoTextHandler(dingtalk_stream.ChatbotHandler):
|
|||||||
await asyncio.sleep(0.1) # 异步等待,避免阻塞
|
await asyncio.sleep(0.1) # 异步等待,避免阻塞
|
||||||
|
|
||||||
return self.incoming_message
|
return self.incoming_message
|
||||||
|
|
||||||
|
|
||||||
async def get_dingtalk_client(client_id, client_secret):
|
|
||||||
from api import DingTalkClient # 延迟导入,避免循环导入
|
|
||||||
|
|
||||||
return DingTalkClient(client_id, client_secret)
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
import dingtalk_stream
|
import dingtalk_stream # type: ignore
|
||||||
from .EchoHandler import EchoTextHandler
|
from .EchoHandler import EchoTextHandler
|
||||||
from .dingtalkevent import DingTalkEvent
|
from .dingtalkevent import DingTalkEvent
|
||||||
import httpx
|
import httpx
|
||||||
@@ -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'
|
||||||
@@ -47,8 +49,8 @@ class DingTalkClient:
|
|||||||
self.access_token = response_data.get('accessToken')
|
self.access_token = response_data.get('accessToken')
|
||||||
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:
|
||||||
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)
|
||||||
@@ -84,10 +86,11 @@ class DingTalkClient:
|
|||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
file_bytes = response.content
|
file_bytes = response.content
|
||||||
base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式
|
mime_type = response.headers.get('Content-Type', 'application/octet-stream')
|
||||||
return base64_str
|
base64_str = base64.b64encode(file_bytes).decode('utf-8')
|
||||||
|
return f'data:{mime_type};base64,{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 +106,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,12 +118,12 @@ 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(
|
||||||
title='@'+incoming_message.sender_nick+' '+content,
|
title='@' + incoming_message.sender_nick + ' ' + content,
|
||||||
text='@'+incoming_message.sender_nick+' '+content,
|
text='@' + incoming_message.sender_nick + ' ' + content,
|
||||||
incoming_message=incoming_message,
|
incoming_message=incoming_message,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -191,7 +194,10 @@ class DingTalkClient:
|
|||||||
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:
|
||||||
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 +220,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 +246,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 连接,监听消息"""
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
import dingtalk_stream
|
import dingtalk_stream # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class DingTalkEvent(dict):
|
class DingTalkEvent(dict):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# 微信公众号的加解密算法与企业微信一样,所以直接使用企业微信的加解密算法文件
|
# 微信公众号的加解密算法与企业微信一样,所以直接使用企业微信的加解密算法文件
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from ..wecom_api.WXBizMsgCrypt3 import WXBizMsgCrypt
|
from libs.wecom_api.WXBizMsgCrypt3 import WXBizMsgCrypt
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from quart import Quart, request
|
from quart import Quart, request
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -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('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('拒绝请求')
|
||||||
raise Exception('拒绝请求')
|
raise Exception('拒绝请求')
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
encryt_msg = await request.data
|
encryt_msg = await request.data
|
||||||
@@ -72,6 +75,7 @@ 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('消息解密失败')
|
||||||
raise Exception('消息解密失败')
|
raise Exception('消息解密失败')
|
||||||
|
|
||||||
message_data = await self.get_message(xml_msg)
|
message_data = await self.get_message(xml_msg)
|
||||||
@@ -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('msg_signature不在请求体中')
|
||||||
raise Exception('msg_signature不在请求体中')
|
raise Exception('msg_signature不在请求体中')
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
@@ -221,6 +229,7 @@ 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('消息解密失败')
|
||||||
raise Exception('消息解密失败')
|
raise Exception('消息解密失败')
|
||||||
|
|
||||||
# 解析 XML
|
# 解析 XML
|
||||||
@@ -270,6 +279,7 @@ class OAClientForLongerResponse:
|
|||||||
return response_xml
|
return response_xml
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
await self.logger.error(f'handle_callback_request失败: {traceback.format_exc()}')
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
async def get_message(self, xml_msg: str):
|
async def get_message(self, xml_msg: str):
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def handle_validation(body: dict, bot_secret: str):
|
|||||||
|
|
||||||
|
|
||||||
class QQOfficialClient:
|
class QQOfficialClient:
|
||||||
def __init__(self, secret: str, token: str, app_id: str):
|
def __init__(self, secret: str, token: str, app_id: str, logger: None):
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
self.app.add_url_rule(
|
self.app.add_url_rule(
|
||||||
'/callback/command',
|
'/callback/command',
|
||||||
@@ -49,6 +49,7 @@ class QQOfficialClient:
|
|||||||
self.base_url = 'https://api.sgroup.qq.com'
|
self.base_url = 'https://api.sgroup.qq.com'
|
||||||
self.access_token = ''
|
self.access_token = ''
|
||||||
self.access_token_expiry_time = None
|
self.access_token_expiry_time = None
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def check_access_token(self):
|
async def check_access_token(self):
|
||||||
"""检查access_token是否存在"""
|
"""检查access_token是否存在"""
|
||||||
@@ -77,6 +78,7 @@ class QQOfficialClient:
|
|||||||
if access_token:
|
if access_token:
|
||||||
self.access_token = access_token
|
self.access_token = access_token
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f'获取access_token失败: {response_data}')
|
||||||
raise Exception(f'获取access_token失败: {e}')
|
raise Exception(f'获取access_token失败: {e}')
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
@@ -102,7 +104,7 @@ class QQOfficialClient:
|
|||||||
return {'code': 0, 'message': 'success'}
|
return {'code': 0, 'message': 'success'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
|
||||||
return {'error': str(e)}, 400
|
return {'error': str(e)}, 400
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||||
@@ -166,6 +168,7 @@ class QQOfficialClient:
|
|||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
await self.get_access_token()
|
await self.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
url = self.base_url + '/v2/users/' + user_openid + '/messages'
|
url = self.base_url + '/v2/users/' + user_openid + '/messages'
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
headers = {
|
headers = {
|
||||||
@@ -178,9 +181,11 @@ class QQOfficialClient:
|
|||||||
'msg_id': msg_id,
|
'msg_id': msg_id,
|
||||||
}
|
}
|
||||||
response = await client.post(url, headers=headers, json=data)
|
response = await client.post(url, headers=headers, json=data)
|
||||||
|
response_data = response.json()
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f'发送私聊消息失败: {response_data}')
|
||||||
raise ValueError(response)
|
raise ValueError(response)
|
||||||
|
|
||||||
async def send_group_text_msg(self, group_openid: str, content: str, msg_id: str):
|
async def send_group_text_msg(self, group_openid: str, content: str, msg_id: str):
|
||||||
@@ -188,6 +193,7 @@ class QQOfficialClient:
|
|||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
await self.get_access_token()
|
await self.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
url = self.base_url + '/v2/groups/' + group_openid + '/messages'
|
url = self.base_url + '/v2/groups/' + group_openid + '/messages'
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
headers = {
|
headers = {
|
||||||
@@ -203,6 +209,7 @@ class QQOfficialClient:
|
|||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f"发送群聊消息失败:{response.json()}")
|
||||||
raise Exception(response.read().decode())
|
raise Exception(response.read().decode())
|
||||||
|
|
||||||
async def send_channle_group_text_msg(self, channel_id: str, content: str, msg_id: str):
|
async def send_channle_group_text_msg(self, channel_id: str, content: str, msg_id: str):
|
||||||
@@ -210,6 +217,7 @@ class QQOfficialClient:
|
|||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
await self.get_access_token()
|
await self.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
url = self.base_url + '/channels/' + channel_id + '/messages'
|
url = self.base_url + '/channels/' + channel_id + '/messages'
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
headers = {
|
headers = {
|
||||||
@@ -225,12 +233,14 @@ class QQOfficialClient:
|
|||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f'发送频道群聊消息失败: {response.json()}')
|
||||||
raise Exception(response)
|
raise Exception(response)
|
||||||
|
|
||||||
async def send_channle_private_text_msg(self, guild_id: str, content: str, msg_id: str):
|
async def send_channle_private_text_msg(self, guild_id: str, content: str, msg_id: str):
|
||||||
"""发送频道私聊消息"""
|
"""发送频道私聊消息"""
|
||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
await self.get_access_token()
|
await self.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
url = self.base_url + '/dms/' + guild_id + '/messages'
|
url = self.base_url + '/dms/' + guild_id + '/messages'
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
@@ -247,6 +257,7 @@ class QQOfficialClient:
|
|||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f'发送频道私聊消息失败: {response.json()}')
|
||||||
raise Exception(response)
|
raise Exception(response)
|
||||||
|
|
||||||
async def is_token_expired(self):
|
async def is_token_expired(self):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
import traceback
|
||||||
from quart import Quart, jsonify, request
|
from quart import Quart, jsonify, request
|
||||||
from slack_sdk.web.async_client import AsyncWebClient
|
from slack_sdk.web.async_client import AsyncWebClient
|
||||||
from .slackevent import SlackEvent
|
from .slackevent import SlackEvent
|
||||||
@@ -7,7 +8,7 @@ from pkg.platform.types import events as platform_events
|
|||||||
|
|
||||||
|
|
||||||
class SlackClient:
|
class SlackClient:
|
||||||
def __init__(self, bot_token: str, signing_secret: str):
|
def __init__(self, bot_token: str, signing_secret: str, logger: None):
|
||||||
self.bot_token = bot_token
|
self.bot_token = bot_token
|
||||||
self.signing_secret = signing_secret
|
self.signing_secret = signing_secret
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
@@ -19,6 +20,7 @@ class SlackClient:
|
|||||||
'example': [],
|
'example': [],
|
||||||
}
|
}
|
||||||
self.bot_user_id = None # 避免机器人回复自己的消息
|
self.bot_user_id = None # 避免机器人回复自己的消息
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
try:
|
try:
|
||||||
@@ -32,6 +34,7 @@ class SlackClient:
|
|||||||
|
|
||||||
if self.bot_user_id and bot_user_id == self.bot_user_id:
|
if self.bot_user_id and bot_user_id == self.bot_user_id:
|
||||||
return jsonify({'status': 'ok'})
|
return jsonify({'status': 'ok'})
|
||||||
|
|
||||||
|
|
||||||
# 处理私信
|
# 处理私信
|
||||||
if data and data.get('event', {}).get('channel_type') in ['im']:
|
if data and data.get('event', {}).get('channel_type') in ['im']:
|
||||||
@@ -49,6 +52,7 @@ class SlackClient:
|
|||||||
return jsonify({'status': 'ok'})
|
return jsonify({'status': 'ok'})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
|
||||||
raise (e)
|
raise (e)
|
||||||
|
|
||||||
async def _handle_message(self, event: SlackEvent):
|
async def _handle_message(self, event: SlackEvent):
|
||||||
@@ -78,6 +82,7 @@ class SlackClient:
|
|||||||
self.bot_user_id = response['message']['bot_id']
|
self.bot_user_id = response['message']['bot_id']
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f"Error in send_message: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
async def send_message_to_one(self, text: str, user_id: str):
|
async def send_message_to_one(self, text: str, user_id: str):
|
||||||
@@ -88,6 +93,7 @@ class SlackClient:
|
|||||||
|
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f"Error in send_message: {traceback.format_exc()}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ from libs.wechatpad_api.api.chatroom import ChatRoomApi
|
|||||||
|
|
||||||
|
|
||||||
class WeChatPadClient:
|
class WeChatPadClient:
|
||||||
def __init__(self,base_url, token):
|
def __init__(self, base_url, token, logger=None):
|
||||||
self._login_api = LoginApi(base_url, token)
|
self._login_api = LoginApi(base_url, token)
|
||||||
self._friend_api = FriendApi(base_url, token)
|
self._friend_api = FriendApi(base_url, token)
|
||||||
self._message_api = MessageApi(base_url, token)
|
self._message_api = MessageApi(base_url, token)
|
||||||
self._user_api = UserApi(base_url, token)
|
self._user_api = UserApi(base_url, token)
|
||||||
self._download_api = DownloadApi(base_url, token)
|
self._download_api = DownloadApi(base_url, token)
|
||||||
self._chatroom_api = ChatRoomApi(base_url, token)
|
self._chatroom_api = ChatRoomApi(base_url, token)
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
def get_token(self,admin_key, day: int):
|
def get_token(self,admin_key, day: int):
|
||||||
'''获取token'''
|
'''获取token'''
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from .WXBizMsgCrypt3 import WXBizMsgCrypt
|
|||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
import httpx
|
import httpx
|
||||||
|
import traceback
|
||||||
from quart import Quart
|
from quart import Quart
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from typing import Callable, Dict, Any
|
from typing import Callable, Dict, Any
|
||||||
@@ -19,6 +20,7 @@ class WecomClient:
|
|||||||
token: str,
|
token: str,
|
||||||
EncodingAESKey: str,
|
EncodingAESKey: str,
|
||||||
contacts_secret: str,
|
contacts_secret: str,
|
||||||
|
logger: None,
|
||||||
):
|
):
|
||||||
self.corpid = corpid
|
self.corpid = corpid
|
||||||
self.secret = secret
|
self.secret = secret
|
||||||
@@ -28,6 +30,7 @@ class WecomClient:
|
|||||||
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
|
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
|
||||||
self.access_token = ''
|
self.access_token = ''
|
||||||
self.secret_for_contacts = contacts_secret
|
self.secret_for_contacts = contacts_secret
|
||||||
|
self.logger = logger
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
self.app.add_url_rule(
|
self.app.add_url_rule(
|
||||||
'/callback/command',
|
'/callback/command',
|
||||||
@@ -54,6 +57,7 @@ class WecomClient:
|
|||||||
if 'access_token' in data:
|
if 'access_token' in data:
|
||||||
return data['access_token']
|
return data['access_token']
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f"获取accesstoken失败:{response.json()}")
|
||||||
raise Exception(f'未获取access token: {data}')
|
raise Exception(f'未获取access token: {data}')
|
||||||
|
|
||||||
async def get_users(self):
|
async def get_users(self):
|
||||||
@@ -125,6 +129,7 @@ class WecomClient:
|
|||||||
response = await client.post(url, json=params)
|
response = await client.post(url, json=params)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f"发送图片失败:{data}")
|
||||||
raise Exception('Failed to send image: ' + str(e))
|
raise Exception('Failed to send image: ' + str(e))
|
||||||
|
|
||||||
# 企业微信错误码40014和42001,代表accesstoken问题
|
# 企业微信错误码40014和42001,代表accesstoken问题
|
||||||
@@ -159,6 +164,7 @@ class WecomClient:
|
|||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
return await self.send_private_msg(user_id, agent_id, content)
|
return await self.send_private_msg(user_id, agent_id, content)
|
||||||
if data['errcode'] != 0:
|
if data['errcode'] != 0:
|
||||||
|
await self.logger.error(f"发送消息失败:{data}")
|
||||||
raise Exception('Failed to send message: ' + str(data))
|
raise Exception('Failed to send message: ' + str(data))
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
@@ -175,6 +181,7 @@ class WecomClient:
|
|||||||
echostr = request.args.get('echostr')
|
echostr = request.args.get('echostr')
|
||||||
ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
|
ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
|
await self.logger.error("验证失败")
|
||||||
raise Exception(f'验证失败,错误码: {ret}')
|
raise Exception(f'验证失败,错误码: {ret}')
|
||||||
return reply_echo_str
|
return reply_echo_str
|
||||||
|
|
||||||
@@ -182,7 +189,9 @@ class WecomClient:
|
|||||||
encrypt_msg = await request.data
|
encrypt_msg = await request.data
|
||||||
ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
|
ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
|
await self.logger.error("消息解密失败")
|
||||||
raise Exception(f'消息解密失败,错误码: {ret}')
|
raise Exception(f'消息解密失败,错误码: {ret}')
|
||||||
|
|
||||||
|
|
||||||
# 解析消息并处理
|
# 解析消息并处理
|
||||||
message_data = await self.get_message(xml_msg)
|
message_data = await self.get_message(xml_msg)
|
||||||
@@ -193,6 +202,7 @@ class WecomClient:
|
|||||||
|
|
||||||
return 'success'
|
return 'success'
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
|
||||||
return f'Error processing request: {str(e)}', 400
|
return f'Error processing request: {str(e)}', 400
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||||
@@ -291,6 +301,7 @@ class WecomClient:
|
|||||||
except binascii.Error as e:
|
except binascii.Error as e:
|
||||||
raise ValueError(f'Invalid base64 string: {str(e)}')
|
raise ValueError(f'Invalid base64 string: {str(e)}')
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error("Image对象出错")
|
||||||
raise ValueError('image对象出错')
|
raise ValueError('image对象出错')
|
||||||
|
|
||||||
# 设置 multipart/form-data 格式的文件
|
# 设置 multipart/form-data 格式的文件
|
||||||
@@ -314,6 +325,7 @@ class WecomClient:
|
|||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
media_id = await self.upload_to_work(image)
|
media_id = await self.upload_to_work(image)
|
||||||
if data.get('errcode', 0) != 0:
|
if data.get('errcode', 0) != 0:
|
||||||
|
await self.logger.error(f"上传图片失败:{data}")
|
||||||
raise Exception('failed to upload file')
|
raise Exception('failed to upload file')
|
||||||
|
|
||||||
media_id = data.get('media_id')
|
media_id = data.get('media_id')
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import aiofiles
|
|||||||
|
|
||||||
|
|
||||||
class WecomCSClient:
|
class WecomCSClient:
|
||||||
def __init__(self, corpid: str, secret: str, token: str, EncodingAESKey: str):
|
def __init__(self, corpid: str, secret: str, token: str, EncodingAESKey: str, logger: None):
|
||||||
self.corpid = corpid
|
self.corpid = corpid
|
||||||
self.secret = secret
|
self.secret = secret
|
||||||
self.access_token_for_contacts = ''
|
self.access_token_for_contacts = ''
|
||||||
@@ -21,6 +21,7 @@ class WecomCSClient:
|
|||||||
self.aes = EncodingAESKey
|
self.aes = EncodingAESKey
|
||||||
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
|
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
|
||||||
self.access_token = ''
|
self.access_token = ''
|
||||||
|
self.logger = logger
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
self.app.add_url_rule(
|
self.app.add_url_rule(
|
||||||
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
|
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
|
||||||
@@ -186,6 +187,7 @@ class WecomCSClient:
|
|||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
return await self.send_text_msg(open_kfid, external_userid, msgid, content)
|
return await self.send_text_msg(open_kfid, external_userid, msgid, content)
|
||||||
if data['errcode'] != 0:
|
if data['errcode'] != 0:
|
||||||
|
await self.logger.error(f"发送消息失败:{data}")
|
||||||
raise Exception('Failed to send message')
|
raise Exception('Failed to send message')
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -224,7 +226,10 @@ class WecomCSClient:
|
|||||||
|
|
||||||
return 'success'
|
return 'success'
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
if self.logger:
|
||||||
|
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
|
||||||
|
else:
|
||||||
|
traceback.print_exc()
|
||||||
return f'Error processing request: {str(e)}', 400
|
return f'Error processing request: {str(e)}', 400
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||||
|
|||||||
8
main.py
8
main.py
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import argparse
|
||||||
# LangBot 终端启动入口
|
# LangBot 终端启动入口
|
||||||
# 在此层级解决依赖项检查。
|
# 在此层级解决依赖项检查。
|
||||||
# LangBot/main.py
|
# LangBot/main.py
|
||||||
@@ -16,6 +17,10 @@ asciiart = r"""
|
|||||||
|
|
||||||
|
|
||||||
async def main_entry(loop: asyncio.AbstractEventLoop):
|
async def main_entry(loop: asyncio.AbstractEventLoop):
|
||||||
|
parser = argparse.ArgumentParser(description='LangBot')
|
||||||
|
parser.add_argument('--skip-plugin-deps-check', action='store_true', help='跳过插件依赖项检查', default=False)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
print(asciiart)
|
print(asciiart)
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -39,7 +44,8 @@ async def main_entry(loop: asyncio.AbstractEventLoop):
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# check plugin deps
|
# check plugin deps
|
||||||
await deps.precheck_plugin_deps()
|
if not args.skip_plugin_deps_check:
|
||||||
|
await deps.precheck_plugin_deps()
|
||||||
|
|
||||||
# 检查pydantic版本,如果没有 pydantic.v1,则把 pydantic 映射为 v1
|
# 检查pydantic版本,如果没有 pydantic.v1,则把 pydantic 映射为 v1
|
||||||
import pydantic.version
|
import pydantic.version
|
||||||
|
|||||||
22
pkg/api/http/controller/groups/files.py
Normal file
22
pkg/api/http/controller/groups/files.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import quart
|
||||||
|
import mimetypes
|
||||||
|
|
||||||
|
from .. import group
|
||||||
|
|
||||||
|
|
||||||
|
@group.group_class('files', '/api/v1/files')
|
||||||
|
class FilesRouterGroup(group.RouterGroup):
|
||||||
|
async def initialize(self) -> None:
|
||||||
|
@self.route('/image/<image_key>', methods=['GET'], auth_type=group.AuthType.NONE)
|
||||||
|
async def _(image_key: str) -> quart.Response:
|
||||||
|
if not await self.ap.storage_mgr.storage_provider.exists(image_key):
|
||||||
|
return quart.Response(status=404)
|
||||||
|
|
||||||
|
image_bytes = await self.ap.storage_mgr.storage_provider.load(image_key)
|
||||||
|
mime_type = mimetypes.guess_type(image_key)[0]
|
||||||
|
if mime_type is None:
|
||||||
|
mime_type = 'image/jpeg'
|
||||||
|
|
||||||
|
return quart.Response(image_bytes, mimetype=mime_type)
|
||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import quart
|
import quart
|
||||||
|
|
||||||
from .. import group
|
from ... import group
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('pipelines', '/api/v1/pipelines')
|
@group.group_class('pipelines', '/api/v1/pipelines')
|
||||||
79
pkg/api/http/controller/groups/pipelines/webchat.py
Normal file
79
pkg/api/http/controller/groups/pipelines/webchat.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import quart
|
||||||
|
|
||||||
|
from ... import group
|
||||||
|
|
||||||
|
|
||||||
|
@group.group_class('webchat', '/api/v1/pipelines/<pipeline_uuid>/chat')
|
||||||
|
class WebChatDebugRouterGroup(group.RouterGroup):
|
||||||
|
async def initialize(self) -> None:
|
||||||
|
@self.route('/send', methods=['POST'])
|
||||||
|
async def send_message(pipeline_uuid: str) -> str:
|
||||||
|
"""发送调试消息到流水线"""
|
||||||
|
try:
|
||||||
|
data = await quart.request.get_json()
|
||||||
|
session_type = data.get('session_type', 'person')
|
||||||
|
message_chain_obj = data.get('message', [])
|
||||||
|
|
||||||
|
if not message_chain_obj:
|
||||||
|
return self.http_status(400, -1, 'message is required')
|
||||||
|
|
||||||
|
if session_type not in ['person', 'group']:
|
||||||
|
return self.http_status(400, -1, 'session_type must be person or group')
|
||||||
|
|
||||||
|
webchat_adapter = self.ap.platform_mgr.webchat_proxy_bot.adapter
|
||||||
|
|
||||||
|
if not webchat_adapter:
|
||||||
|
return self.http_status(404, -1, 'WebChat adapter not found')
|
||||||
|
|
||||||
|
result = await webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj)
|
||||||
|
|
||||||
|
return self.success(
|
||||||
|
data={
|
||||||
|
'message': result,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return self.http_status(500, -1, f'Internal server error: {str(e)}')
|
||||||
|
|
||||||
|
@self.route('/messages/<session_type>', methods=['GET'])
|
||||||
|
async def get_messages(pipeline_uuid: str, session_type: str) -> str:
|
||||||
|
"""获取调试消息历史"""
|
||||||
|
try:
|
||||||
|
if session_type not in ['person', 'group']:
|
||||||
|
return self.http_status(400, -1, 'session_type must be person or group')
|
||||||
|
|
||||||
|
webchat_adapter = self.ap.platform_mgr.webchat_proxy_bot.adapter
|
||||||
|
|
||||||
|
if not webchat_adapter:
|
||||||
|
return self.http_status(404, -1, 'WebChat adapter not found')
|
||||||
|
|
||||||
|
messages = webchat_adapter.get_webchat_messages(pipeline_uuid, session_type)
|
||||||
|
|
||||||
|
return self.success(data={'messages': messages})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return self.http_status(500, -1, f'Internal server error: {str(e)}')
|
||||||
|
|
||||||
|
@self.route('/reset/<session_type>', methods=['POST'])
|
||||||
|
async def reset_session(session_type: str) -> str:
|
||||||
|
"""重置调试会话"""
|
||||||
|
try:
|
||||||
|
if session_type not in ['person', 'group']:
|
||||||
|
return self.http_status(400, -1, 'session_type must be person or group')
|
||||||
|
|
||||||
|
webchat_adapter = None
|
||||||
|
for bot in self.ap.platform_mgr.bots:
|
||||||
|
if hasattr(bot.adapter, '__class__') and bot.adapter.__class__.__name__ == 'WebChatAdapter':
|
||||||
|
webchat_adapter = bot.adapter
|
||||||
|
break
|
||||||
|
|
||||||
|
if not webchat_adapter:
|
||||||
|
return self.http_status(404, -1, 'WebChat adapter not found')
|
||||||
|
|
||||||
|
webchat_adapter.reset_debug_session(session_type)
|
||||||
|
|
||||||
|
return self.success(data={'message': 'Session reset successfully'})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return self.http_status(500, -1, f'Internal server error: {str(e)}')
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ from . import groups
|
|||||||
from . import group
|
from . import group
|
||||||
from .groups import provider as groups_provider
|
from .groups import provider as groups_provider
|
||||||
from .groups import platform as groups_platform
|
from .groups import platform as groups_platform
|
||||||
|
from .groups import pipelines as groups_pipelines
|
||||||
|
|
||||||
importutil.import_modules_in_pkg(groups)
|
importutil.import_modules_in_pkg(groups)
|
||||||
importutil.import_modules_in_pkg(groups_provider)
|
importutil.import_modules_in_pkg(groups_provider)
|
||||||
importutil.import_modules_in_pkg(groups_platform)
|
importutil.import_modules_in_pkg(groups_platform)
|
||||||
|
importutil.import_modules_in_pkg(groups_pipelines)
|
||||||
|
|
||||||
|
|
||||||
class HTTPController:
|
class HTTPController:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -92,9 +93,25 @@ class BotService:
|
|||||||
if runtime_bot.enable:
|
if runtime_bot.enable:
|
||||||
await runtime_bot.run()
|
await runtime_bot.run()
|
||||||
|
|
||||||
|
# update all conversation that use this bot
|
||||||
|
for session in self.ap.sess_mgr.session_list:
|
||||||
|
if session.using_conversation is not None and session.using_conversation.bot_uuid == bot_uuid:
|
||||||
|
session.using_conversation = None
|
||||||
|
|
||||||
async def delete_bot(self, bot_uuid: str) -> None:
|
async def delete_bot(self, bot_uuid: str) -> None:
|
||||||
"""删除机器人"""
|
"""删除机器人"""
|
||||||
await self.ap.platform_mgr.remove_bot(bot_uuid)
|
await self.ap.platform_mgr.remove_bot(bot_uuid)
|
||||||
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
|
||||||
|
|||||||
@@ -112,6 +112,11 @@ class PipelineService:
|
|||||||
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
|
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
|
||||||
await self.ap.pipeline_mgr.load_pipeline(pipeline)
|
await self.ap.pipeline_mgr.load_pipeline(pipeline)
|
||||||
|
|
||||||
|
# update all conversation that use this pipeline
|
||||||
|
for session in self.ap.sess_mgr.session_list:
|
||||||
|
if session.using_conversation is not None and session.using_conversation.pipeline_uuid == pipeline_uuid:
|
||||||
|
session.using_conversation = None
|
||||||
|
|
||||||
async def delete_pipeline(self, pipeline_uuid: str) -> None:
|
async def delete_pipeline(self, pipeline_uuid: str) -> None:
|
||||||
await self.ap.persistence_mgr.execute_async(
|
await self.ap.persistence_mgr.execute_async(
|
||||||
sqlalchemy.delete(persistence_pipeline.LegacyPipeline).where(
|
sqlalchemy.delete(persistence_pipeline.LegacyPipeline).where(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -137,6 +137,12 @@ class Conversation(pydantic.BaseModel):
|
|||||||
|
|
||||||
use_funcs: typing.Optional[list[tools_entities.LLMFunction]]
|
use_funcs: typing.Optional[list[tools_entities.LLMFunction]]
|
||||||
|
|
||||||
|
pipeline_uuid: str
|
||||||
|
"""流水线UUID。"""
|
||||||
|
|
||||||
|
bot_uuid: str
|
||||||
|
"""机器人UUID。"""
|
||||||
|
|
||||||
uuid: typing.Optional[str] = None
|
uuid: typing.Optional[str] = None
|
||||||
"""该对话的 uuid,在创建时不会自动生成。而是当使用 Dify API 等由外部管理对话信息的服务时,用于绑定外部的会话。具体如何使用,取决于 Runner。"""
|
"""该对话的 uuid,在创建时不会自动生成。而是当使用 Dify API 等由外部管理对话信息的服务时,用于绑定外部的会话。具体如何使用,取决于 Runner。"""
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -66,13 +66,15 @@ class PersistenceManager:
|
|||||||
|
|
||||||
# write default pipeline
|
# write default pipeline
|
||||||
result = await self.execute_async(sqlalchemy.select(pipeline.LegacyPipeline))
|
result = await self.execute_async(sqlalchemy.select(pipeline.LegacyPipeline))
|
||||||
|
default_pipeline_uuid = None
|
||||||
if result.first() is None:
|
if result.first() is None:
|
||||||
self.ap.logger.info('Creating default pipeline...')
|
self.ap.logger.info('Creating default pipeline...')
|
||||||
|
|
||||||
pipeline_config = json.load(open('templates/default-pipeline-config.json', 'r', encoding='utf-8'))
|
pipeline_config = json.load(open('templates/default-pipeline-config.json', 'r', encoding='utf-8'))
|
||||||
|
|
||||||
|
default_pipeline_uuid = str(uuid.uuid4())
|
||||||
pipeline_data = {
|
pipeline_data = {
|
||||||
'uuid': str(uuid.uuid4()),
|
'uuid': default_pipeline_uuid,
|
||||||
'for_version': self.ap.ver_mgr.get_current_version(),
|
'for_version': self.ap.ver_mgr.get_current_version(),
|
||||||
'stages': pipeline_service.default_stage_order,
|
'stages': pipeline_service.default_stage_order,
|
||||||
'is_default': True,
|
'is_default': True,
|
||||||
@@ -82,6 +84,7 @@ class PersistenceManager:
|
|||||||
}
|
}
|
||||||
|
|
||||||
await self.execute_async(sqlalchemy.insert(pipeline.LegacyPipeline).values(pipeline_data))
|
await self.execute_async(sqlalchemy.insert(pipeline.LegacyPipeline).values(pipeline_data))
|
||||||
|
|
||||||
# =================================
|
# =================================
|
||||||
|
|
||||||
# run migrations
|
# run migrations
|
||||||
|
|||||||
@@ -28,7 +28,12 @@ class DBMigrateCombineQuoteMsgConfig(migration.DBMigration):
|
|||||||
await self.ap.persistence_mgr.execute_async(
|
await self.ap.persistence_mgr.execute_async(
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
||||||
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
|
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
|
||||||
.values({'config': config, 'for_version': self.ap.ver_mgr.get_current_version()})
|
.values(
|
||||||
|
{
|
||||||
|
'config': config,
|
||||||
|
'for_version': self.ap.ver_mgr.get_current_version(),
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def downgrade(self):
|
async def downgrade(self):
|
||||||
|
|||||||
49
pkg/persistence/migrations/dbm003_n8n_config.py
Normal file
49
pkg/persistence/migrations/dbm003_n8n_config.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from .. import migration
|
||||||
|
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
from ...entity.persistence import pipeline as persistence_pipeline
|
||||||
|
|
||||||
|
|
||||||
|
@migration.migration_class(3)
|
||||||
|
class DBMigrateN8nConfig(migration.DBMigration):
|
||||||
|
"""N8n配置"""
|
||||||
|
|
||||||
|
async def upgrade(self):
|
||||||
|
"""升级"""
|
||||||
|
# read all pipelines
|
||||||
|
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
|
||||||
|
|
||||||
|
for pipeline in pipelines:
|
||||||
|
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
||||||
|
|
||||||
|
config = serialized_pipeline['config']
|
||||||
|
|
||||||
|
if 'n8n-service-api' not in config['ai']:
|
||||||
|
config['ai']['n8n-service-api'] = {
|
||||||
|
'webhook-url': 'http://your-n8n-webhook-url',
|
||||||
|
'auth-type': 'none',
|
||||||
|
'basic-username': '',
|
||||||
|
'basic-password': '',
|
||||||
|
'jwt-secret': '',
|
||||||
|
'jwt-algorithm': 'HS256',
|
||||||
|
'header-name': '',
|
||||||
|
'header-value': '',
|
||||||
|
'timeout': 120,
|
||||||
|
'output-key': 'response',
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.ap.persistence_mgr.execute_async(
|
||||||
|
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
||||||
|
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
|
||||||
|
.values(
|
||||||
|
{
|
||||||
|
'config': config,
|
||||||
|
'for_version': self.ap.ver_mgr.get_current_version(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def downgrade(self):
|
||||||
|
"""降级"""
|
||||||
|
pass
|
||||||
@@ -51,11 +51,10 @@ class Controller:
|
|||||||
# find pipeline
|
# find pipeline
|
||||||
# Here firstly find the bot, then find the pipeline, in case the bot adapter's config is not the latest one.
|
# Here firstly find the bot, then find the pipeline, in case the bot adapter's config is not the latest one.
|
||||||
# Like aiocqhttp, once a client is connected, even the adapter was updated and restarted, the existing client connection will not be affected.
|
# Like aiocqhttp, once a client is connected, even the adapter was updated and restarted, the existing client connection will not be affected.
|
||||||
bot = await self.ap.platform_mgr.get_bot_by_uuid(selected_query.bot_uuid)
|
pipeline_uuid = selected_query.pipeline_uuid
|
||||||
if bot:
|
|
||||||
pipeline = await self.ap.pipeline_mgr.get_pipeline_by_uuid(
|
if pipeline_uuid:
|
||||||
bot.bot_entity.use_pipeline_uuid
|
pipeline = await self.ap.pipeline_mgr.get_pipeline_by_uuid(pipeline_uuid)
|
||||||
)
|
|
||||||
if pipeline:
|
if pipeline:
|
||||||
await pipeline.run(selected_query)
|
await pipeline.run(selected_query)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class QueryPool:
|
|||||||
message_event: platform_events.MessageEvent,
|
message_event: platform_events.MessageEvent,
|
||||||
message_chain: platform_message.MessageChain,
|
message_chain: platform_message.MessageChain,
|
||||||
adapter: msadapter.MessagePlatformAdapter,
|
adapter: msadapter.MessagePlatformAdapter,
|
||||||
|
pipeline_uuid: typing.Optional[str] = None,
|
||||||
) -> entities.Query:
|
) -> entities.Query:
|
||||||
async with self.condition:
|
async with self.condition:
|
||||||
query = entities.Query(
|
query = entities.Query(
|
||||||
@@ -48,6 +49,7 @@ class QueryPool:
|
|||||||
resp_messages=[],
|
resp_messages=[],
|
||||||
resp_message_chain=[],
|
resp_message_chain=[],
|
||||||
adapter=adapter,
|
adapter=adapter,
|
||||||
|
pipeline_uuid=pipeline_uuid,
|
||||||
)
|
)
|
||||||
self.queries.append(query)
|
self.queries.append(query)
|
||||||
self.query_id_counter += 1
|
self.query_id_counter += 1
|
||||||
|
|||||||
@@ -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'])
|
||||||
@@ -46,6 +45,8 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
query,
|
query,
|
||||||
session,
|
session,
|
||||||
query.pipeline_config['ai']['local-agent']['prompt'],
|
query.pipeline_config['ai']['local-agent']['prompt'],
|
||||||
|
query.pipeline_uuid,
|
||||||
|
query.bot_uuid,
|
||||||
)
|
)
|
||||||
|
|
||||||
conversation.use_llm_model = llm_model
|
conversation.use_llm_model = llm_model
|
||||||
@@ -59,7 +60,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 +83,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 +101,11 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
content_list.append(llm_entities.ContentElement.from_text(msg.text))
|
content_list.append(llm_entities.ContentElement.from_text(msg.text))
|
||||||
elif isinstance(msg, platform_message.Image):
|
elif isinstance(msg, platform_message.Image):
|
||||||
if selected_runner != 'local-agent' or query.use_llm_model.model_entity.abilities.__contains__(
|
if selected_runner != 'local-agent' or query.use_llm_model.model_entity.abilities.__contains__(
|
||||||
'vision'
|
'vision'
|
||||||
):
|
):
|
||||||
if msg.base64 is not None:
|
if msg.base64 is not None:
|
||||||
content_list.append(llm_entities.ContentElement.from_image_base64(msg.base64))
|
content_list.append(llm_entities.ContentElement.from_image_base64(msg.base64))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
query.variables['user_message_text'] = plain_text
|
query.variables['user_message_text'] = plain_text
|
||||||
|
|
||||||
query.user_message = llm_entities.Message(role='user', content=content_list)
|
query.user_message = llm_entities.Message(role='user', content=content_list)
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ from ..core import app, entities as core_entities
|
|||||||
from . import entities
|
from . import entities
|
||||||
|
|
||||||
|
|
||||||
preregistered_stages: dict[str, PipelineStage] = {}
|
preregistered_stages: dict[str, type[PipelineStage]] = {}
|
||||||
|
|
||||||
|
|
||||||
def stage_class(name: str):
|
def stage_class(name: str) -> typing.Callable[[type[PipelineStage]], type[PipelineStage]]:
|
||||||
def decorator(cls):
|
def decorator(cls: type[PipelineStage]) -> type[PipelineStage]:
|
||||||
preregistered_stages[name] = cls
|
preregistered_stages[name] = cls
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import abc
|
|||||||
from ..core import app
|
from ..core import app
|
||||||
from .types import message as platform_message
|
from .types import message as platform_message
|
||||||
from .types import events as platform_events
|
from .types import events as platform_events
|
||||||
|
from .logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class MessagePlatformAdapter(metaclass=abc.ABCMeta):
|
class MessagePlatformAdapter(metaclass=abc.ABCMeta):
|
||||||
@@ -22,7 +23,9 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta):
|
|||||||
|
|
||||||
ap: app.Application
|
ap: app.Application
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
logger: EventLogger
|
||||||
|
|
||||||
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
"""初始化适配器
|
"""初始化适配器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -31,6 +34,7 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta):
|
|||||||
"""
|
"""
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
||||||
"""主动发送消息
|
"""主动发送消息
|
||||||
|
|||||||
@@ -5,17 +5,18 @@ import asyncio
|
|||||||
import traceback
|
import traceback
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
|
|
||||||
# FriendMessage, Image, MessageChain, Plain
|
# FriendMessage, Image, MessageChain, Plain
|
||||||
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 +38,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,
|
||||||
@@ -62,12 +77,23 @@ class RuntimeBot:
|
|||||||
message_event=event,
|
message_event=event,
|
||||||
message_chain=event.message_chain,
|
message_chain=event.message_chain,
|
||||||
adapter=adapter,
|
adapter=adapter,
|
||||||
|
pipeline_uuid=self.bot_entity.use_pipeline_uuid,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def on_group_message(
|
async def on_group_message(
|
||||||
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,
|
||||||
@@ -76,6 +102,7 @@ class RuntimeBot:
|
|||||||
message_event=event,
|
message_event=event,
|
||||||
message_chain=event.message_chain,
|
message_chain=event.message_chain,
|
||||||
adapter=adapter,
|
adapter=adapter,
|
||||||
|
pipeline_uuid=self.bot_entity.use_pipeline_uuid,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.adapter.register_listener(platform_events.FriendMessage, on_friend_message)
|
self.adapter.register_listener(platform_events.FriendMessage, on_friend_message)
|
||||||
@@ -92,10 +119,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(),
|
||||||
@@ -121,6 +145,8 @@ class PlatformManager:
|
|||||||
|
|
||||||
bots: list[RuntimeBot]
|
bots: list[RuntimeBot]
|
||||||
|
|
||||||
|
webchat_proxy_bot: RuntimeBot
|
||||||
|
|
||||||
adapter_components: list[engine.Component]
|
adapter_components: list[engine.Component]
|
||||||
|
|
||||||
adapter_dict: dict[str, type[msadapter.MessagePlatformAdapter]]
|
adapter_dict: dict[str, type[msadapter.MessagePlatformAdapter]]
|
||||||
@@ -138,6 +164,31 @@ class PlatformManager:
|
|||||||
adapter_dict[component.metadata.name] = component.get_python_component_class()
|
adapter_dict[component.metadata.name] = component.get_python_component_class()
|
||||||
self.adapter_dict = adapter_dict
|
self.adapter_dict = adapter_dict
|
||||||
|
|
||||||
|
webchat_adapter_class = self.adapter_dict['webchat']
|
||||||
|
|
||||||
|
# initialize webchat adapter
|
||||||
|
webchat_logger = EventLogger(name='webchat-adapter', ap=self.ap)
|
||||||
|
webchat_adapter_inst = webchat_adapter_class(
|
||||||
|
{},
|
||||||
|
self.ap,
|
||||||
|
webchat_logger,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.webchat_proxy_bot = RuntimeBot(
|
||||||
|
ap=self.ap,
|
||||||
|
bot_entity=persistence_bot.Bot(
|
||||||
|
uuid='webchat-proxy-bot',
|
||||||
|
name='WebChat',
|
||||||
|
description='',
|
||||||
|
adapter='webchat',
|
||||||
|
adapter_config={},
|
||||||
|
enable=True,
|
||||||
|
),
|
||||||
|
adapter=webchat_adapter_inst,
|
||||||
|
logger=webchat_logger,
|
||||||
|
)
|
||||||
|
await self.webchat_proxy_bot.initialize()
|
||||||
|
|
||||||
await self.load_bots_from_db()
|
await self.load_bots_from_db()
|
||||||
|
|
||||||
def get_running_adapters(self) -> list[msadapter.MessagePlatformAdapter]:
|
def get_running_adapters(self) -> list[msadapter.MessagePlatformAdapter]:
|
||||||
@@ -166,9 +217,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()
|
||||||
|
|
||||||
@@ -191,7 +248,9 @@ class PlatformManager:
|
|||||||
return
|
return
|
||||||
|
|
||||||
def get_available_adapters_info(self) -> list[dict]:
|
def get_available_adapters_info(self) -> list[dict]:
|
||||||
return [component.to_plain_dict() for component in self.adapter_components]
|
return [
|
||||||
|
component.to_plain_dict() for component in self.adapter_components if component.metadata.name != 'webchat'
|
||||||
|
]
|
||||||
|
|
||||||
def get_available_adapter_info_by_name(self, name: str) -> dict | None:
|
def get_available_adapter_info_by_name(self, name: str) -> dict | None:
|
||||||
for component in self.adapter_components:
|
for component in self.adapter_components:
|
||||||
@@ -244,6 +303,8 @@ class PlatformManager:
|
|||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
# This method will only be called when the application launching
|
# This method will only be called when the application launching
|
||||||
|
await self.webchat_proxy_bot.run()
|
||||||
|
|
||||||
for bot in self.bots:
|
for bot in self.bots:
|
||||||
if bot.enable:
|
if bot.enable:
|
||||||
await bot.run()
|
await bot.run()
|
||||||
|
|||||||
233
pkg/platform/logger.py
Normal file
233
pkg/platform/logger.py
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typing
|
||||||
|
import mimetypes
|
||||||
|
import time
|
||||||
|
import enum
|
||||||
|
import pydantic
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from ..core import app
|
||||||
|
from .types import message as platform_message
|
||||||
|
|
||||||
|
|
||||||
|
class EventLogLevel(enum.Enum):
|
||||||
|
"""日志级别"""
|
||||||
|
|
||||||
|
DEBUG = 'debug'
|
||||||
|
INFO = 'info'
|
||||||
|
WARNING = 'warning'
|
||||||
|
ERROR = 'error'
|
||||||
|
|
||||||
|
|
||||||
|
class EventLog(pydantic.BaseModel):
|
||||||
|
seq_id: int
|
||||||
|
"""日志序号"""
|
||||||
|
|
||||||
|
timestamp: int
|
||||||
|
"""日志时间戳"""
|
||||||
|
|
||||||
|
level: EventLogLevel
|
||||||
|
"""日志级别"""
|
||||||
|
|
||||||
|
text: str
|
||||||
|
"""日志文本"""
|
||||||
|
|
||||||
|
images: typing.Optional[list[str]] = None
|
||||||
|
"""日志图片 URL 列表,需要通过 /api/v1/image/{uuid} 获取图片"""
|
||||||
|
|
||||||
|
message_session_id: typing.Optional[str] = None
|
||||||
|
"""消息会话ID,仅收发消息事件有值"""
|
||||||
|
|
||||||
|
def to_json(self) -> dict:
|
||||||
|
return {
|
||||||
|
'seq_id': self.seq_id,
|
||||||
|
'timestamp': self.timestamp,
|
||||||
|
'level': self.level.value,
|
||||||
|
'text': self.text,
|
||||||
|
'images': self.images,
|
||||||
|
'message_session_id': self.message_session_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
MAX_LOG_COUNT = 200
|
||||||
|
DELETE_COUNT_PER_TIME = 50
|
||||||
|
|
||||||
|
|
||||||
|
class EventLogger:
|
||||||
|
"""used for logging bot events"""
|
||||||
|
|
||||||
|
ap: app.Application
|
||||||
|
|
||||||
|
seq_id_inc: int
|
||||||
|
|
||||||
|
logs: list[EventLog]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
ap: app.Application,
|
||||||
|
):
|
||||||
|
self.name = name
|
||||||
|
self.ap = ap
|
||||||
|
self.logs = []
|
||||||
|
self.seq_id_inc = 0
|
||||||
|
|
||||||
|
async def get_logs(self, from_seq_id: int, max_count: int) -> typing.Tuple[list[EventLog], int]:
|
||||||
|
"""
|
||||||
|
获取日志,从 from_seq_id 开始获取 max_count 条历史日志
|
||||||
|
|
||||||
|
Args:
|
||||||
|
from_seq_id: 起始序号,-1 表示末尾
|
||||||
|
max_count: 最大数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[list[EventLog], int]: 日志列表,日志总数
|
||||||
|
"""
|
||||||
|
if len(self.logs) == 0:
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
if from_seq_id <= -1:
|
||||||
|
from_seq_id = self.logs[-1].seq_id
|
||||||
|
|
||||||
|
min_seq_id_in_logs = self.logs[0].seq_id
|
||||||
|
max_seq_id_in_logs = self.logs[-1].seq_id
|
||||||
|
|
||||||
|
if from_seq_id < min_seq_id_in_logs: # 需要的整个范围都已经被删除
|
||||||
|
return [], len(self.logs)
|
||||||
|
|
||||||
|
if (
|
||||||
|
from_seq_id > max_seq_id_in_logs and from_seq_id - max_count > max_seq_id_in_logs
|
||||||
|
): # 需要的整个范围都还没生成
|
||||||
|
return [], len(self.logs)
|
||||||
|
|
||||||
|
end_index = 1
|
||||||
|
|
||||||
|
for i, log in enumerate(self.logs):
|
||||||
|
if log.seq_id >= from_seq_id:
|
||||||
|
end_index = i + 1
|
||||||
|
break
|
||||||
|
|
||||||
|
start_index = max(0, end_index - max_count)
|
||||||
|
|
||||||
|
if max_count > 0:
|
||||||
|
return self.logs[start_index:end_index], len(self.logs)
|
||||||
|
else:
|
||||||
|
return [], len(self.logs)
|
||||||
|
|
||||||
|
async def _truncate_logs(self):
|
||||||
|
if len(self.logs) > MAX_LOG_COUNT:
|
||||||
|
for i in range(DELETE_COUNT_PER_TIME):
|
||||||
|
for image_key in self.logs[i].images:
|
||||||
|
await self.ap.storage_mgr.storage_provider.delete(image_key)
|
||||||
|
self.logs = self.logs[DELETE_COUNT_PER_TIME:]
|
||||||
|
|
||||||
|
async def _add_log(
|
||||||
|
self,
|
||||||
|
level: EventLogLevel,
|
||||||
|
text: str,
|
||||||
|
images: typing.Optional[list[platform_message.Image]] = None,
|
||||||
|
message_session_id: typing.Optional[str] = None,
|
||||||
|
no_throw: bool = True,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
image_keys = []
|
||||||
|
|
||||||
|
if images is None:
|
||||||
|
images = []
|
||||||
|
|
||||||
|
if message_session_id is None:
|
||||||
|
message_session_id = ''
|
||||||
|
|
||||||
|
if not isinstance(message_session_id, str):
|
||||||
|
message_session_id = str(message_session_id)
|
||||||
|
|
||||||
|
for img in images:
|
||||||
|
img_bytes, mime_type = await img.get_bytes()
|
||||||
|
extension = mimetypes.guess_extension(mime_type)
|
||||||
|
if extension is None:
|
||||||
|
extension = '.jpg'
|
||||||
|
image_key = f'{message_session_id}-{uuid.uuid4()}{extension}'
|
||||||
|
await self.ap.storage_mgr.storage_provider.save(image_key, img_bytes)
|
||||||
|
image_keys.append(image_key)
|
||||||
|
|
||||||
|
self.logs.append(
|
||||||
|
EventLog(
|
||||||
|
seq_id=self.seq_id_inc,
|
||||||
|
timestamp=int(time.time()),
|
||||||
|
level=level,
|
||||||
|
text=text,
|
||||||
|
images=image_keys,
|
||||||
|
message_session_id=message_session_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.seq_id_inc += 1
|
||||||
|
|
||||||
|
await self._truncate_logs()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if not no_throw:
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
async def info(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
images: typing.Optional[list[platform_message.Image]] = None,
|
||||||
|
message_session_id: typing.Optional[str] = None,
|
||||||
|
no_throw: bool = True,
|
||||||
|
):
|
||||||
|
await self._add_log(
|
||||||
|
level=EventLogLevel.INFO,
|
||||||
|
text=text,
|
||||||
|
images=images,
|
||||||
|
message_session_id=message_session_id,
|
||||||
|
no_throw=no_throw,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def debug(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
images: typing.Optional[list[platform_message.Image]] = None,
|
||||||
|
message_session_id: typing.Optional[str] = None,
|
||||||
|
no_throw: bool = True,
|
||||||
|
):
|
||||||
|
await self._add_log(
|
||||||
|
level=EventLogLevel.DEBUG,
|
||||||
|
text=text,
|
||||||
|
images=images,
|
||||||
|
message_session_id=message_session_id,
|
||||||
|
no_throw=no_throw,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def warning(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
images: typing.Optional[list[platform_message.Image]] = None,
|
||||||
|
message_session_id: typing.Optional[str] = None,
|
||||||
|
no_throw: bool = True,
|
||||||
|
):
|
||||||
|
await self._add_log(
|
||||||
|
level=EventLogLevel.WARNING,
|
||||||
|
text=text,
|
||||||
|
images=images,
|
||||||
|
message_session_id=message_session_id,
|
||||||
|
no_throw=no_throw,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def error(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
images: typing.Optional[list[platform_message.Image]] = None,
|
||||||
|
message_session_id: typing.Optional[str] = None,
|
||||||
|
no_throw: bool = True,
|
||||||
|
):
|
||||||
|
await self._add_log(
|
||||||
|
level=EventLogLevel.ERROR,
|
||||||
|
text=text,
|
||||||
|
images=images,
|
||||||
|
message_session_id=message_session_id,
|
||||||
|
no_throw=no_throw,
|
||||||
|
)
|
||||||
@@ -12,6 +12,7 @@ from ..types import message as platform_message
|
|||||||
from ..types import events as platform_events
|
from ..types import events as platform_events
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...utils import image
|
from ...utils import image
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class AiocqhttpMessageConverter(adapter.MessageConverter):
|
class AiocqhttpMessageConverter(adapter.MessageConverter):
|
||||||
@@ -209,8 +210,11 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
|
|||||||
|
|
||||||
ap: app.Application
|
ap: app.Application
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
on_websocket_connection_event_cache: typing.List[typing.Callable[[aiocqhttp.Event], None]] = []
|
||||||
|
|
||||||
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def shutdown_trigger_placeholder():
|
async def shutdown_trigger_placeholder():
|
||||||
while True:
|
while True:
|
||||||
@@ -219,6 +223,7 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.config['shutdown_trigger'] = shutdown_trigger_placeholder
|
self.config['shutdown_trigger'] = shutdown_trigger_placeholder
|
||||||
|
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.on_websocket_connection_event_cache = []
|
||||||
|
|
||||||
if 'access-token' in config:
|
if 'access-token' in config:
|
||||||
self.bot = aiocqhttp.CQHttp(access_token=config['access-token'])
|
self.bot = aiocqhttp.CQHttp(access_token=config['access-token'])
|
||||||
@@ -260,6 +265,7 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
|
|||||||
try:
|
try:
|
||||||
return await callback(await self.event_converter.target2yiri(event,self.bot), self)
|
return await callback(await self.event_converter.target2yiri(event,self.bot), self)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
await self.logger.error(f'Error in on_message: {traceback.format_exc()}')
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
if event_type == platform_events.GroupMessage:
|
if event_type == platform_events.GroupMessage:
|
||||||
@@ -267,6 +273,16 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
|
|||||||
elif event_type == platform_events.FriendMessage:
|
elif event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('private')(on_message)
|
self.bot.on_message('private')(on_message)
|
||||||
|
|
||||||
|
async def on_websocket_connection(event: aiocqhttp.Event):
|
||||||
|
for event in self.on_websocket_connection_event_cache:
|
||||||
|
if event.self_id == event.self_id and event.time == event.time:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.on_websocket_connection_event_cache.append(event)
|
||||||
|
await self.logger.info(f'WebSocket connection established, bot id: {event.self_id}')
|
||||||
|
|
||||||
|
self.bot.on_websocket_connection(on_websocket_connection)
|
||||||
|
|
||||||
def unregister_listener(
|
def unregister_listener(
|
||||||
self,
|
self,
|
||||||
event_type: typing.Type[platform_events.Event],
|
event_type: typing.Type[platform_events.Event],
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from ..types import events as platform_events
|
|||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from libs.dingtalk_api.api import DingTalkClient
|
from libs.dingtalk_api.api import DingTalkClient
|
||||||
import datetime
|
import datetime
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class DingTalkMessageConverter(adapter.MessageConverter):
|
class DingTalkMessageConverter(adapter.MessageConverter):
|
||||||
@@ -99,9 +100,10 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
|
|||||||
event_converter: DingTalkEventConverter = DingTalkEventConverter()
|
event_converter: DingTalkEventConverter = DingTalkEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'client_id',
|
'client_id',
|
||||||
'client_secret',
|
'client_secret',
|
||||||
@@ -120,6 +122,7 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
|
|||||||
robot_name=config['robot_name'],
|
robot_name=config['robot_name'],
|
||||||
robot_code=config['robot_code'],
|
robot_code=config['robot_code'],
|
||||||
markdown_card=config['markdown_card'],
|
markdown_card=config['markdown_card'],
|
||||||
|
logger=self.logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
@@ -154,8 +157,8 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
|
|||||||
await self.event_converter.target2yiri(event, self.config['robot_name']),
|
await self.event_converter.target2yiri(event, self.config['robot_name']),
|
||||||
self,
|
self,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in dingtalk callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('FriendMessage')(on_message)
|
self.bot.on_message('FriendMessage')(on_message)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from ...core import app
|
|||||||
from ..types import message as platform_message
|
from ..types import message as platform_message
|
||||||
from ..types import events as platform_events
|
from ..types import events as platform_events
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class DiscordMessageConverter(adapter.MessageConverter):
|
class DiscordMessageConverter(adapter.MessageConverter):
|
||||||
@@ -170,9 +171,10 @@ class DiscordAdapter(adapter.MessagePlatformAdapter):
|
|||||||
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
self.bot_account_id = self.config['client_id']
|
self.bot_account_id = self.config['client_id']
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from ...utils import image
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class GewechatMessageConverter(adapter.MessageConverter):
|
class GewechatMessageConverter(adapter.MessageConverter):
|
||||||
@@ -371,7 +372,7 @@ class GewechatMessageConverter(adapter.MessageConverter):
|
|||||||
quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') # 引用消息的原发送者
|
quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') # 引用消息的原发送者
|
||||||
ats_bot = ats_bot or (quote_id == tousername)
|
ats_bot = ats_bot or (quote_id == tousername)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'_ats_bot got except: {e}')
|
print(f'Error in gewechat _ats_bot: {e}')
|
||||||
finally:
|
finally:
|
||||||
return ats_bot
|
return ats_bot
|
||||||
|
|
||||||
@@ -477,9 +478,10 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
|
|||||||
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
self.quart_app = quart.Quart(__name__)
|
self.quart_app = quart.Quart(__name__)
|
||||||
|
|
||||||
self.message_converter = GewechatMessageConverter(config)
|
self.message_converter = GewechatMessageConverter(config)
|
||||||
@@ -503,7 +505,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
|
|||||||
try:
|
try:
|
||||||
event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id)
|
event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc()
|
await self.logger.error(f'Error in gewechat callback: {traceback.format_exc()}')
|
||||||
|
|
||||||
if event.__class__ in self.listeners:
|
if event.__class__ in self.listeners:
|
||||||
await self.listeners[event.__class__](event, self)
|
await self.listeners[event.__class__](event, self)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from ...core import app
|
|||||||
from ..types import message as platform_message
|
from ..types import message as platform_message
|
||||||
from ..types import events as platform_events
|
from ..types import events as platform_events
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class AESCipher(object):
|
class AESCipher(object):
|
||||||
@@ -338,9 +339,10 @@ class LarkAdapter(adapter.MessagePlatformAdapter):
|
|||||||
quart_app: quart.Quart
|
quart_app: quart.Quart
|
||||||
ap: app.Application
|
ap: app.Application
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
self.quart_app = quart.Quart(__name__)
|
self.quart_app = quart.Quart(__name__)
|
||||||
self.listeners = {}
|
self.listeners = {}
|
||||||
|
|
||||||
@@ -376,15 +378,15 @@ class LarkAdapter(adapter.MessagePlatformAdapter):
|
|||||||
if 'im.message.receive_v1' == type:
|
if 'im.message.receive_v1' == type:
|
||||||
try:
|
try:
|
||||||
event = await self.event_converter.target2yiri(p2v1, self.api_client)
|
event = await self.event_converter.target2yiri(p2v1, self.api_client)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in lark callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event.__class__ in self.listeners:
|
if event.__class__ in self.listeners:
|
||||||
await self.listeners[event.__class__](event, self)
|
await self.listeners[event.__class__](event, self)
|
||||||
|
|
||||||
return {'code': 200, 'message': 'ok'}
|
return {'code': 200, 'message': 'ok'}
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in lark callback: {traceback.format_exc()}")
|
||||||
return {'code': 500, 'message': 'error'}
|
return {'code': 500, 'message': 'error'}
|
||||||
|
|
||||||
async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
|
async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from ...pipeline.longtext.strategies import forward
|
|||||||
from ...platform.types import message as platform_message
|
from ...platform.types import message as platform_message
|
||||||
from ...platform.types import entities as platform_entities
|
from ...platform.types import entities as platform_entities
|
||||||
from ...platform.types import events as platform_events
|
from ...platform.types import events as platform_events
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class NakuruProjectMessageConverter(adapter_model.MessageConverter):
|
class NakuruProjectMessageConverter(adapter_model.MessageConverter):
|
||||||
@@ -71,9 +72,8 @@ class NakuruProjectMessageConverter(adapter_model.MessageConverter):
|
|||||||
content=content_list,
|
content=content_list,
|
||||||
)
|
)
|
||||||
nakuru_forward_node_list.append(nakuru_forward_node)
|
nakuru_forward_node_list.append(nakuru_forward_node)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
nakuru_msg_list.append(nakuru_forward_node_list)
|
nakuru_msg_list.append(nakuru_forward_node_list)
|
||||||
@@ -178,12 +178,13 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter):
|
|||||||
|
|
||||||
cfg: dict
|
cfg: dict
|
||||||
|
|
||||||
def __init__(self, cfg: dict, ap):
|
def __init__(self, cfg: dict, ap, logger: EventLogger):
|
||||||
"""初始化nakuru-project的对象"""
|
"""初始化nakuru-project的对象"""
|
||||||
cfg['port'] = cfg['ws_port']
|
cfg['port'] = cfg['ws_port']
|
||||||
del cfg['ws_port']
|
del cfg['ws_port']
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
self.listener_list = []
|
self.listener_list = []
|
||||||
self.bot = nakuru.CQHTTP(**self.cfg)
|
self.bot = nakuru.CQHTTP(**self.cfg)
|
||||||
|
|
||||||
@@ -275,7 +276,7 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter):
|
|||||||
# 注册监听器
|
# 注册监听器
|
||||||
self.bot.receiver(source_cls.__name__)(listener_wrapper)
|
self.bot.receiver(source_cls.__name__)(listener_wrapper)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
self.logger.error(f"Error in nakuru register_listener: {traceback.format_exc()}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def unregister_listener(
|
def unregister_listener(
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from .. import adapter
|
|||||||
from ...core import app
|
from ...core import app
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...command.errors import ParamNotEnoughError
|
from ...command.errors import ParamNotEnoughError
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class OAMessageConverter(adapter.MessageConverter):
|
class OAMessageConverter(adapter.MessageConverter):
|
||||||
@@ -63,10 +64,10 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
|
|||||||
event_converter: OAEventConverter = OAEventConverter()
|
event_converter: OAEventConverter = OAEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'token',
|
'token',
|
||||||
@@ -85,6 +86,7 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
|
|||||||
EncodingAESKey=config['EncodingAESKey'],
|
EncodingAESKey=config['EncodingAESKey'],
|
||||||
Appsecret=config['AppSecret'],
|
Appsecret=config['AppSecret'],
|
||||||
AppID=config['AppID'],
|
AppID=config['AppID'],
|
||||||
|
logger=self.logger,
|
||||||
)
|
)
|
||||||
elif self.config['Mode'] == 'passive':
|
elif self.config['Mode'] == 'passive':
|
||||||
self.bot = OAClientForLongerResponse(
|
self.bot = OAClientForLongerResponse(
|
||||||
@@ -93,6 +95,7 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
|
|||||||
Appsecret=config['AppSecret'],
|
Appsecret=config['AppSecret'],
|
||||||
AppID=config['AppID'],
|
AppID=config['AppID'],
|
||||||
LoadingMessage=config['LoadingMessage'],
|
LoadingMessage=config['LoadingMessage'],
|
||||||
|
logger=self.logger,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise KeyError('请设置微信公众号通信模式')
|
raise KeyError('请设置微信公众号通信模式')
|
||||||
@@ -122,8 +125,8 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.bot_account_id = event.receiver_id
|
self.bot_account_id = event.receiver_id
|
||||||
try:
|
try:
|
||||||
return await callback(await self.event_converter.target2yiri(event), self)
|
return await callback(await self.event_converter.target2yiri(event), self)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in officialaccount callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('text')(on_message)
|
self.bot.on_message('text')(on_message)
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ spec:
|
|||||||
label:
|
label:
|
||||||
en_US: Host
|
en_US: Host
|
||||||
zh_Hans: 监听主机
|
zh_Hans: 监听主机
|
||||||
|
description:
|
||||||
|
en_US: The host that Official Account listens on for Webhook connections.
|
||||||
|
zh_Hans: 微信公众号监听的主机,除非你知道自己在做什么,否则请写 0.0.0.0
|
||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
default: 0.0.0.0
|
default: 0.0.0.0
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from ...config import manager as cfg_mgr
|
|||||||
from ...platform.types import entities as platform_entities
|
from ...platform.types import entities as platform_entities
|
||||||
from ...platform.types import events as platform_events
|
from ...platform.types import events as platform_events
|
||||||
from ...platform.types import message as platform_message
|
from ...platform.types import message as platform_message
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class OfficialGroupMessage(platform_events.GroupMessage):
|
class OfficialGroupMessage(platform_events.GroupMessage):
|
||||||
@@ -357,10 +358,11 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter):
|
|||||||
group_msg_seq = None
|
group_msg_seq = None
|
||||||
c2c_msg_seq = None
|
c2c_msg_seq = None
|
||||||
|
|
||||||
def __init__(self, cfg: dict, ap: app.Application):
|
def __init__(self, cfg: dict, ap: app.Application, logger: EventLogger):
|
||||||
"""初始化适配器"""
|
"""初始化适配器"""
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
self.group_msg_seq = 1
|
self.group_msg_seq = 1
|
||||||
self.c2c_msg_seq = 1
|
self.c2c_msg_seq = 1
|
||||||
@@ -499,7 +501,7 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter):
|
|||||||
for event_handler in event_handler_mapping[event_type]:
|
for event_handler in event_handler_mapping[event_type]:
|
||||||
setattr(self.bot, event_handler, wrapper)
|
setattr(self.bot, event_handler, wrapper)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
self.logger.error(f"Error in qqbotpy callback: {traceback.format_exc()}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def unregister_listener(
|
def unregister_listener(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from ...command.errors import ParamNotEnoughError
|
|||||||
from libs.qq_official_api.api import QQOfficialClient
|
from libs.qq_official_api.api import QQOfficialClient
|
||||||
from libs.qq_official_api.qqofficialevent import QQOfficialEvent
|
from libs.qq_official_api.qqofficialevent import QQOfficialEvent
|
||||||
from ...utils import image
|
from ...utils import image
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class QQOfficialMessageConverter(adapter.MessageConverter):
|
class QQOfficialMessageConverter(adapter.MessageConverter):
|
||||||
@@ -139,9 +140,10 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter):
|
|||||||
message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter()
|
message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter()
|
||||||
event_converter: QQOfficialEventConverter = QQOfficialEventConverter()
|
event_converter: QQOfficialEventConverter = QQOfficialEventConverter()
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'appid',
|
'appid',
|
||||||
@@ -155,6 +157,7 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter):
|
|||||||
app_id=config['appid'],
|
app_id=config['appid'],
|
||||||
secret=config['secret'],
|
secret=config['secret'],
|
||||||
token=config['token'],
|
token=config['token'],
|
||||||
|
logger=self.logger
|
||||||
)
|
)
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
@@ -221,8 +224,8 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.bot_account_id = 'justbot'
|
self.bot_account_id = 'justbot'
|
||||||
try:
|
try:
|
||||||
return await callback(await self.event_converter.target2yiri(event), self)
|
return await callback(await self.event_converter.target2yiri(event), self)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in qqofficial callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('DIRECT_MESSAGE_CREATE')(on_message)
|
self.bot.on_message('DIRECT_MESSAGE_CREATE')(on_message)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from .. import adapter
|
|||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...command.errors import ParamNotEnoughError
|
from ...command.errors import ParamNotEnoughError
|
||||||
from ...utils import image
|
from ...utils import image
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class SlackMessageConverter(adapter.MessageConverter):
|
class SlackMessageConverter(adapter.MessageConverter):
|
||||||
@@ -91,9 +92,10 @@ class SlackAdapter(adapter.MessagePlatformAdapter):
|
|||||||
event_converter: SlackEventConverter = SlackEventConverter()
|
event_converter: SlackEventConverter = SlackEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'bot_token',
|
'bot_token',
|
||||||
'signing_secret',
|
'signing_secret',
|
||||||
@@ -102,7 +104,7 @@ class SlackAdapter(adapter.MessagePlatformAdapter):
|
|||||||
if missing_keys:
|
if missing_keys:
|
||||||
raise ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员')
|
raise ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员')
|
||||||
|
|
||||||
self.bot = SlackClient(bot_token=self.config['bot_token'], signing_secret=self.config['signing_secret'])
|
self.bot = SlackClient(bot_token=self.config['bot_token'], signing_secret=self.config['signing_secret'], logger=self.logger)
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
self,
|
self,
|
||||||
@@ -137,8 +139,8 @@ class SlackAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.bot_account_id = 'SlackBot'
|
self.bot_account_id = 'SlackBot'
|
||||||
try:
|
try:
|
||||||
return await callback(await self.event_converter.target2yiri(event, self.bot), self)
|
return await callback(await self.event_converter.target2yiri(event, self.bot), self)
|
||||||
except:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in slack callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('im')(on_message)
|
self.bot.on_message('im')(on_message)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from ...core import app
|
|||||||
from ..types import message as platform_message
|
from ..types import message as platform_message
|
||||||
from ..types import events as platform_events
|
from ..types import events as platform_events
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class TelegramMessageConverter(adapter.MessageConverter):
|
class TelegramMessageConverter(adapter.MessageConverter):
|
||||||
@@ -147,9 +148,10 @@ class TelegramAdapter(adapter.MessagePlatformAdapter):
|
|||||||
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
if update.message.from_user.is_bot:
|
if update.message.from_user.is_bot:
|
||||||
@@ -158,8 +160,8 @@ class TelegramAdapter(adapter.MessagePlatformAdapter):
|
|||||||
try:
|
try:
|
||||||
lb_event = await self.event_converter.target2yiri(update, self.bot, self.bot_account_id)
|
lb_event = await self.event_converter.target2yiri(update, self.bot, self.bot_account_id)
|
||||||
await self.listeners[type(lb_event)](lb_event, self)
|
await self.listeners[type(lb_event)](lb_event, self)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
print(traceback.format_exc())
|
await self.logger.error(f"Error in telegram callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
self.application = ApplicationBuilder().token(self.config['token']).build()
|
self.application = ApplicationBuilder().token(self.config['token']).build()
|
||||||
self.bot = self.application.bot
|
self.bot = self.application.bot
|
||||||
|
|||||||
209
pkg/platform/sources/webchat.py
Normal file
209
pkg/platform/sources/webchat.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import typing
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .. import adapter as msadapter
|
||||||
|
from ..types import events as platform_events, message as platform_message, entities as platform_entities
|
||||||
|
from ...core import app
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class WebChatMessage(BaseModel):
|
||||||
|
id: int
|
||||||
|
role: str
|
||||||
|
content: str
|
||||||
|
message_chain: list[dict]
|
||||||
|
timestamp: str
|
||||||
|
|
||||||
|
|
||||||
|
class WebChatSession:
|
||||||
|
id: str
|
||||||
|
message_lists: dict[str, list[WebChatMessage]] = {}
|
||||||
|
resp_waiters: dict[int, asyncio.Future[WebChatMessage]]
|
||||||
|
|
||||||
|
def __init__(self, id: str):
|
||||||
|
self.id = id
|
||||||
|
self.message_lists = {}
|
||||||
|
self.resp_waiters = {}
|
||||||
|
|
||||||
|
def get_message_list(self, pipeline_uuid: str) -> list[WebChatMessage]:
|
||||||
|
if pipeline_uuid not in self.message_lists:
|
||||||
|
self.message_lists[pipeline_uuid] = []
|
||||||
|
|
||||||
|
return self.message_lists[pipeline_uuid]
|
||||||
|
|
||||||
|
|
||||||
|
class WebChatAdapter(msadapter.MessagePlatformAdapter):
|
||||||
|
"""WebChat调试适配器,用于流水线调试"""
|
||||||
|
|
||||||
|
webchat_person_session: WebChatSession
|
||||||
|
webchat_group_session: WebChatSession
|
||||||
|
|
||||||
|
listeners: typing.Dict[
|
||||||
|
typing.Type[platform_events.Event],
|
||||||
|
typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], None],
|
||||||
|
] = {}
|
||||||
|
|
||||||
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
self.webchat_person_session = WebChatSession(id='webchatperson')
|
||||||
|
self.webchat_group_session = WebChatSession(id='webchatgroup')
|
||||||
|
|
||||||
|
self.bot_account_id = 'webchatbot'
|
||||||
|
|
||||||
|
async def send_message(
|
||||||
|
self,
|
||||||
|
target_type: str,
|
||||||
|
target_id: str,
|
||||||
|
message: platform_message.MessageChain,
|
||||||
|
) -> dict:
|
||||||
|
"""发送消息到调试会话"""
|
||||||
|
session_key = target_id
|
||||||
|
|
||||||
|
if session_key not in self.debug_messages:
|
||||||
|
self.debug_messages[session_key] = []
|
||||||
|
|
||||||
|
message_data = {
|
||||||
|
'id': len(self.debug_messages[session_key]) + 1,
|
||||||
|
'type': 'bot',
|
||||||
|
'content': str(message),
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'message_chain': [component.__dict__ for component in message],
|
||||||
|
}
|
||||||
|
|
||||||
|
self.debug_messages[session_key].append(message_data)
|
||||||
|
|
||||||
|
await self.logger.info(f'Send message to {session_key}: {message}')
|
||||||
|
|
||||||
|
return message_data
|
||||||
|
|
||||||
|
async def reply_message(
|
||||||
|
self,
|
||||||
|
message_source: platform_events.MessageEvent,
|
||||||
|
message: platform_message.MessageChain,
|
||||||
|
quote_origin: bool = False,
|
||||||
|
) -> dict:
|
||||||
|
"""回复消息"""
|
||||||
|
message_data = WebChatMessage(
|
||||||
|
id=-1,
|
||||||
|
role='assistant',
|
||||||
|
content=str(message),
|
||||||
|
message_chain=[component.__dict__ for component in message],
|
||||||
|
timestamp=datetime.now().isoformat(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# notify waiter
|
||||||
|
if isinstance(message_source, platform_events.FriendMessage):
|
||||||
|
self.webchat_person_session.resp_waiters[message_source.message_chain.message_id].set_result(message_data)
|
||||||
|
elif isinstance(message_source, platform_events.GroupMessage):
|
||||||
|
self.webchat_group_session.resp_waiters[message_source.message_chain.message_id].set_result(message_data)
|
||||||
|
|
||||||
|
return message_data.model_dump()
|
||||||
|
|
||||||
|
def register_listener(
|
||||||
|
self,
|
||||||
|
event_type: typing.Type[platform_events.Event],
|
||||||
|
func: typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], typing.Awaitable[None]],
|
||||||
|
):
|
||||||
|
"""注册事件监听器"""
|
||||||
|
self.listeners[event_type] = func
|
||||||
|
|
||||||
|
def unregister_listener(
|
||||||
|
self,
|
||||||
|
event_type: typing.Type[platform_events.Event],
|
||||||
|
func: typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], typing.Awaitable[None]],
|
||||||
|
):
|
||||||
|
"""取消注册事件监听器"""
|
||||||
|
del self.listeners[event_type]
|
||||||
|
|
||||||
|
async def run_async(self):
|
||||||
|
"""运行适配器"""
|
||||||
|
await self.logger.info('WebChat调试适配器已启动')
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
await self.logger.info('WebChat调试适配器已停止')
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def kill(self):
|
||||||
|
"""停止适配器"""
|
||||||
|
await self.logger.info('WebChat调试适配器正在停止')
|
||||||
|
|
||||||
|
async def send_webchat_message(
|
||||||
|
self, pipeline_uuid: str, session_type: str, message_chain_obj: typing.List[dict]
|
||||||
|
) -> dict:
|
||||||
|
"""发送调试消息到流水线"""
|
||||||
|
if session_type == 'person':
|
||||||
|
use_session = self.webchat_person_session
|
||||||
|
else:
|
||||||
|
use_session = self.webchat_group_session
|
||||||
|
|
||||||
|
message_chain = platform_message.MessageChain.parse_obj(message_chain_obj)
|
||||||
|
|
||||||
|
message_id = len(use_session.get_message_list(pipeline_uuid)) + 1
|
||||||
|
|
||||||
|
use_session.get_message_list(pipeline_uuid).append(
|
||||||
|
WebChatMessage(
|
||||||
|
id=message_id,
|
||||||
|
role='user',
|
||||||
|
content=str(message_chain),
|
||||||
|
message_chain=message_chain_obj,
|
||||||
|
timestamp=datetime.now().isoformat(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
message_chain.insert(0, platform_message.Source(id=message_id, time=datetime.now().timestamp()))
|
||||||
|
|
||||||
|
if session_type == 'person':
|
||||||
|
sender = platform_entities.Friend(id='webchatperson', nickname='User')
|
||||||
|
event = platform_events.FriendMessage(
|
||||||
|
sender=sender, message_chain=message_chain, time=datetime.now().timestamp()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
group = platform_entities.Group(
|
||||||
|
id='webchatgroup', name='Group', permission=platform_entities.Permission.Member
|
||||||
|
)
|
||||||
|
sender = platform_entities.GroupMember(
|
||||||
|
id='webchatperson',
|
||||||
|
member_name='User',
|
||||||
|
group=group,
|
||||||
|
permission=platform_entities.Permission.Member,
|
||||||
|
)
|
||||||
|
event = platform_events.GroupMessage(
|
||||||
|
sender=sender, message_chain=message_chain, time=datetime.now().timestamp()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.ap.platform_mgr.webchat_proxy_bot.bot_entity.use_pipeline_uuid = pipeline_uuid
|
||||||
|
|
||||||
|
if event.__class__ in self.listeners:
|
||||||
|
await self.listeners[event.__class__](event, self)
|
||||||
|
|
||||||
|
# set waiter
|
||||||
|
waiter = asyncio.Future[WebChatMessage]()
|
||||||
|
use_session.resp_waiters[message_id] = waiter
|
||||||
|
waiter.add_done_callback(lambda future: use_session.resp_waiters.pop(message_id))
|
||||||
|
|
||||||
|
resp_message = await waiter
|
||||||
|
|
||||||
|
resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1
|
||||||
|
|
||||||
|
use_session.get_message_list(pipeline_uuid).append(resp_message)
|
||||||
|
|
||||||
|
return resp_message.model_dump()
|
||||||
|
|
||||||
|
def get_webchat_messages(self, pipeline_uuid: str, session_type: str) -> list[dict]:
|
||||||
|
"""获取调试消息历史"""
|
||||||
|
if session_type == 'person':
|
||||||
|
return [message.model_dump() for message in self.webchat_person_session.get_message_list(pipeline_uuid)]
|
||||||
|
else:
|
||||||
|
return [message.model_dump() for message in self.webchat_group_session.get_message_list(pipeline_uuid)]
|
||||||
16
pkg/platform/sources/webchat.yaml
Normal file
16
pkg/platform/sources/webchat.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: MessagePlatformAdapter
|
||||||
|
metadata:
|
||||||
|
name: webchat
|
||||||
|
label:
|
||||||
|
en_US: "WebChat Debug"
|
||||||
|
zh_Hans: "网页聊天调试"
|
||||||
|
description:
|
||||||
|
en_US: "WebChat adapter for pipeline debugging"
|
||||||
|
zh_Hans: "用于流水线调试的网页聊天适配器"
|
||||||
|
icon: ""
|
||||||
|
spec: {}
|
||||||
|
execution:
|
||||||
|
python:
|
||||||
|
path: "webchat.py"
|
||||||
|
attr: "WebChatAdapter"
|
||||||
@@ -30,6 +30,7 @@ from ..types import message as platform_message
|
|||||||
from ..types import events as platform_events
|
from ..types import events as platform_events
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...utils import image
|
from ...utils import image
|
||||||
|
from ..logger import EventLogger
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from typing import Optional, List, Tuple
|
from typing import Optional, List, Tuple
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@@ -533,9 +534,10 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter):
|
|||||||
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
self.quart_app = quart.Quart(__name__)
|
self.quart_app = quart.Quart(__name__)
|
||||||
|
|
||||||
self.message_converter = WeChatPadMessageConverter(config)
|
self.message_converter = WeChatPadMessageConverter(config)
|
||||||
@@ -550,7 +552,7 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter):
|
|||||||
try:
|
try:
|
||||||
event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id)
|
event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in wechatpad callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event.__class__ in self.listeners:
|
if event.__class__ in self.listeners:
|
||||||
await self.listeners[event.__class__](event, self)
|
await self.listeners[event.__class__](event, self)
|
||||||
@@ -694,7 +696,8 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter):
|
|||||||
|
|
||||||
self.bot = WeChatPadClient(
|
self.bot = WeChatPadClient(
|
||||||
self.config['wechatpad_url'],
|
self.config['wechatpad_url'],
|
||||||
self.config["token"]
|
self.config["token"],
|
||||||
|
logger=self.logger
|
||||||
)
|
)
|
||||||
self.ap.logger.info(self.config["token"])
|
self.ap.logger.info(self.config["token"])
|
||||||
thread_1 = threading.Event()
|
thread_1 = threading.Event()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from ...core import app
|
|||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...command.errors import ParamNotEnoughError
|
from ...command.errors import ParamNotEnoughError
|
||||||
from ...utils import image
|
from ...utils import image
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class WecomMessageConverter(adapter.MessageConverter):
|
class WecomMessageConverter(adapter.MessageConverter):
|
||||||
@@ -134,10 +135,10 @@ class WecomAdapter(adapter.MessagePlatformAdapter):
|
|||||||
event_converter: WecomEventConverter = WecomEventConverter()
|
event_converter: WecomEventConverter = WecomEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'corpid',
|
'corpid',
|
||||||
@@ -156,6 +157,7 @@ class WecomAdapter(adapter.MessagePlatformAdapter):
|
|||||||
token=config['token'],
|
token=config['token'],
|
||||||
EncodingAESKey=config['EncodingAESKey'],
|
EncodingAESKey=config['EncodingAESKey'],
|
||||||
contacts_secret=config['contacts_secret'],
|
contacts_secret=config['contacts_secret'],
|
||||||
|
logger=self.logger
|
||||||
)
|
)
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
@@ -199,8 +201,8 @@ class WecomAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.bot_account_id = event.receiver_id
|
self.bot_account_id = event.receiver_id
|
||||||
try:
|
try:
|
||||||
return await callback(await self.event_converter.target2yiri(event), self)
|
return await callback(await self.event_converter.target2yiri(event), self)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in wecom callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('text')(on_message)
|
self.bot.on_message('text')(on_message)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from pkg.core import app
|
|||||||
from .. import adapter
|
from .. import adapter
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...command.errors import ParamNotEnoughError
|
from ...command.errors import ParamNotEnoughError
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class WecomMessageConverter(adapter.MessageConverter):
|
class WecomMessageConverter(adapter.MessageConverter):
|
||||||
@@ -124,10 +125,10 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter):
|
|||||||
event_converter: WecomEventConverter = WecomEventConverter()
|
event_converter: WecomEventConverter = WecomEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'corpid',
|
'corpid',
|
||||||
@@ -144,6 +145,7 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter):
|
|||||||
secret=config['secret'],
|
secret=config['secret'],
|
||||||
token=config['token'],
|
token=config['token'],
|
||||||
EncodingAESKey=config['EncodingAESKey'],
|
EncodingAESKey=config['EncodingAESKey'],
|
||||||
|
logger=self.logger
|
||||||
)
|
)
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
@@ -176,8 +178,8 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.bot_account_id = event.receiver_id
|
self.bot_account_id = event.receiver_id
|
||||||
try:
|
try:
|
||||||
return await callback(await self.event_converter.target2yiri(event), self)
|
return await callback(await self.event_converter.target2yiri(event), self)
|
||||||
except:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in wecomcs callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('text')(on_message)
|
self.bot.on_message('text')(on_message)
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import logging
|
|||||||
import typing
|
import typing
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import base64
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
import httpx
|
||||||
import pydantic.v1 as pydantic
|
import pydantic.v1 as pydantic
|
||||||
|
|
||||||
from . import entities as platform_entities
|
from . import entities as platform_entities
|
||||||
@@ -552,52 +555,29 @@ class Image(MessageComponent):
|
|||||||
image_id = image_id[1:]
|
image_id = image_id[1:]
|
||||||
return image_id
|
return image_id
|
||||||
|
|
||||||
async def download(
|
async def get_bytes(self) -> typing.Tuple[bytes, str]:
|
||||||
self,
|
"""获取图片的 bytes 和 mime type"""
|
||||||
filename: typing.Union[str, Path, None] = None,
|
if self.url:
|
||||||
directory: typing.Union[str, Path, None] = None,
|
async with httpx.AsyncClient() as client:
|
||||||
determine_type: bool = True,
|
response = await client.get(self.url)
|
||||||
):
|
response.raise_for_status()
|
||||||
"""下载图片到本地。
|
return response.content, response.headers.get('Content-Type')
|
||||||
|
elif self.base64:
|
||||||
|
mime_type = 'image/jpeg'
|
||||||
|
|
||||||
Args:
|
split_index = self.base64.find(';base64,')
|
||||||
filename: 下载到本地的文件路径。与 `directory` 二选一。
|
if split_index == -1:
|
||||||
directory: 下载到本地的文件夹路径。与 `filename` 二选一。
|
raise ValueError('Invalid base64 string')
|
||||||
determine_type: 是否自动根据图片类型确定拓展名,默认为 True。
|
|
||||||
"""
|
|
||||||
if not self.url:
|
|
||||||
logger.warning(f'图片 `{self.uuid}` 无 url 参数,下载失败。')
|
|
||||||
return
|
|
||||||
|
|
||||||
import httpx
|
mime_type = self.base64[5:split_index]
|
||||||
|
base64_data = self.base64[split_index + 8 :]
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
return base64.b64decode(base64_data), mime_type
|
||||||
response = await client.get(self.url)
|
elif self.path:
|
||||||
response.raise_for_status()
|
async with aiofiles.open(self.path, 'rb') as f:
|
||||||
content = response.content
|
return await f.read(), 'image/jpeg'
|
||||||
|
else:
|
||||||
if filename:
|
raise ValueError('Can not get bytes from image')
|
||||||
path = Path(filename)
|
|
||||||
if determine_type:
|
|
||||||
import imghdr
|
|
||||||
|
|
||||||
path = path.with_suffix('.' + str(imghdr.what(None, content)))
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
elif directory:
|
|
||||||
import imghdr
|
|
||||||
|
|
||||||
path = Path(directory)
|
|
||||||
path.mkdir(parents=True, exist_ok=True)
|
|
||||||
path = path / f'{self.uuid}.{imghdr.what(None, content)}'
|
|
||||||
else:
|
|
||||||
raise ValueError('请指定文件路径或文件夹路径!')
|
|
||||||
|
|
||||||
import aiofiles
|
|
||||||
|
|
||||||
async with aiofiles.open(path, 'wb') as f:
|
|
||||||
await f.write(content)
|
|
||||||
|
|
||||||
return path
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_local(
|
async def from_local(
|
||||||
|
|||||||
@@ -1,87 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import typing
|
import typing
|
||||||
import google.genai
|
|
||||||
from google.genai import types
|
|
||||||
|
|
||||||
from .. import errors, requester
|
from . import chatcmpl
|
||||||
from ....core import entities as core_entities
|
|
||||||
from ... import entities as llm_entities
|
|
||||||
from ...tools import entities as tools_entities
|
|
||||||
|
|
||||||
|
|
||||||
class GeminiChatCompletions(requester.LLMAPIRequester):
|
class GeminiChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||||
"""Google Gemini API 请求器"""
|
"""Google Gemini API 请求器"""
|
||||||
|
|
||||||
default_config: dict[str, typing.Any] = {
|
default_config: dict[str, typing.Any] = {
|
||||||
'base_url': 'https://generativelanguage.googleapis.com',
|
'base_url': 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||||
'timeout': 120,
|
'timeout': 120,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
"""初始化 Gemini API 客户端"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def invoke_llm(
|
|
||||||
self,
|
|
||||||
query: core_entities.Query,
|
|
||||||
model: requester.RuntimeLLMModel,
|
|
||||||
messages: typing.List[llm_entities.Message],
|
|
||||||
funcs: typing.List[tools_entities.LLMFunction] = None,
|
|
||||||
extra_args: dict[str, typing.Any] = {},
|
|
||||||
) -> llm_entities.Message:
|
|
||||||
"""调用 Gemini API 生成回复"""
|
|
||||||
try:
|
|
||||||
self.client = google.genai.Client(
|
|
||||||
api_key=model.token_mgr.get_token(),
|
|
||||||
http_options=types.HttpOptions(api_version='v1alpha'),
|
|
||||||
)
|
|
||||||
contents = []
|
|
||||||
|
|
||||||
system_content = None
|
|
||||||
|
|
||||||
for message in messages:
|
|
||||||
role = message.role
|
|
||||||
parts = []
|
|
||||||
|
|
||||||
if isinstance(message.content, str):
|
|
||||||
parts.append(types.Part.from_text(text=message.content))
|
|
||||||
elif isinstance(message.content, list):
|
|
||||||
for content in message.content:
|
|
||||||
if content.type == 'text':
|
|
||||||
parts.append(types.Part.from_text(text=content.text))
|
|
||||||
# elif content.type == 'image_url':
|
|
||||||
# parts.append(types.Part.from_image_url(url=content.image_url))
|
|
||||||
|
|
||||||
if role == 'system':
|
|
||||||
system_content = parts
|
|
||||||
else:
|
|
||||||
content = types.Content(role=role, parts=parts)
|
|
||||||
contents.append(content)
|
|
||||||
|
|
||||||
response = self.client.models.generate_content(
|
|
||||||
model=model.model_entity.name,
|
|
||||||
contents=contents,
|
|
||||||
config=types.GenerateContentConfig(
|
|
||||||
system_instruction=system_content,
|
|
||||||
**extra_args,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return llm_entities.Message(
|
|
||||||
role='assistant',
|
|
||||||
content=response.candidates[0].content.parts[0].text,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_message = str(e).lower()
|
|
||||||
if 'invalid api key' in error_message:
|
|
||||||
raise errors.RequesterError(f'无效的 API 密钥: {str(e)}')
|
|
||||||
elif 'not found' in error_message:
|
|
||||||
raise errors.RequesterError(f'请求路径错误或模型无效: {str(e)}')
|
|
||||||
elif any(keyword in error_message for keyword in ['rate limit', 'quota', 'permission denied']):
|
|
||||||
raise errors.RequesterError(f'请求过于频繁或余额不足: {str(e)}')
|
|
||||||
elif 'timeout' in error_message:
|
|
||||||
raise errors.RequesterError(f'请求超时: {str(e)}')
|
|
||||||
else:
|
|
||||||
raise errors.RequesterError(f'Gemini API 请求错误: {str(e)}')
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ spec:
|
|||||||
zh_Hans: 基础 URL
|
zh_Hans: 基础 URL
|
||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
default: "https://generativelanguage.googleapis.com"
|
default: "https://generativelanguage.googleapis.com/v1beta/openai"
|
||||||
- name: timeout
|
- name: timeout
|
||||||
label:
|
label:
|
||||||
en_US: Timeout
|
en_US: Timeout
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
3
pkg/provider/modelmgr/requesters/ppio.svg
Normal file
3
pkg/provider/modelmgr/requesters/ppio.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M29.7888 0.215881C13.3449 0.215881 0 13.5422 0 29.986C0 38.0916 3.24782 45.4527 8.51506 50.8223V30.0139C8.51506 24.3372 10.7299 18.9769 14.7408 14.966C18.7704 10.9365 24.112 8.74025 29.7981 8.74025H29.9749L29.7888 8.75886C41.5423 8.75886 51.0718 18.2883 51.0718 30.0326C51.0718 31.0562 50.9973 32.0613 50.8577 33.057L38.8343 20.9964C36.4333 18.5954 33.2134 17.2646 29.8074 17.2646C26.4013 17.2646 23.1907 18.5954 20.7805 20.9964C18.3609 23.4159 17.0394 26.6172 17.0394 30.0326C17.0394 33.4479 18.3702 36.6492 20.7805 39.0688C23.1814 41.4697 26.4013 42.8005 29.8074 42.8005C33.2134 42.8005 36.424 41.4697 38.8343 39.0688C41.077 36.826 42.3706 33.8946 42.5474 30.7584L49.6014 37.8403C46.4839 45.7319 38.797 51.3249 29.7981 51.3249C25.1357 51.3249 20.6874 49.8359 17.0301 47.072V56.9178C20.9014 58.7604 25.2195 59.7841 29.7794 59.7841C46.2233 59.7841 59.5682 46.4578 59.5682 30.0139C59.5868 13.5515 46.2512 0.225187 29.7981 0.225187L29.7888 0.215881Z" fill="#0062E2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -5,6 +5,7 @@ metadata:
|
|||||||
label:
|
label:
|
||||||
en_US: ppio
|
en_US: ppio
|
||||||
zh_Hans: 派欧云
|
zh_Hans: 派欧云
|
||||||
|
icon: ppio.svg
|
||||||
spec:
|
spec:
|
||||||
config:
|
config:
|
||||||
- name: base_url
|
- name: base_url
|
||||||
|
|||||||
159
pkg/provider/runners/n8nsvapi.py
Normal file
159
pkg/provider/runners/n8nsvapi.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typing
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from .. import runner
|
||||||
|
from ...core import app, entities as core_entities
|
||||||
|
from .. import entities as llm_entities
|
||||||
|
|
||||||
|
|
||||||
|
class N8nAPIError(Exception):
|
||||||
|
"""N8n API 请求失败"""
|
||||||
|
|
||||||
|
def __init__(self, message: str):
|
||||||
|
self.message = message
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
|
||||||
|
@runner.runner_class('n8n-service-api')
|
||||||
|
class N8nServiceAPIRunner(runner.RequestRunner):
|
||||||
|
"""N8n Service API 工作流请求器"""
|
||||||
|
|
||||||
|
def __init__(self, ap: app.Application, pipeline_config: dict):
|
||||||
|
self.ap = ap
|
||||||
|
self.pipeline_config = pipeline_config
|
||||||
|
|
||||||
|
# 获取webhook URL
|
||||||
|
self.webhook_url = self.pipeline_config['ai']['n8n-service-api']['webhook-url']
|
||||||
|
|
||||||
|
# 获取超时设置,默认为120秒
|
||||||
|
self.timeout = self.pipeline_config['ai']['n8n-service-api'].get('timeout', 120)
|
||||||
|
|
||||||
|
# 获取输出键名,默认为response
|
||||||
|
self.output_key = self.pipeline_config['ai']['n8n-service-api'].get('output-key', 'response')
|
||||||
|
|
||||||
|
# 获取认证类型,默认为none
|
||||||
|
self.auth_type = self.pipeline_config['ai']['n8n-service-api'].get('auth-type', 'none')
|
||||||
|
|
||||||
|
# 根据认证类型获取相应的认证信息
|
||||||
|
if self.auth_type == 'basic':
|
||||||
|
self.basic_username = self.pipeline_config['ai']['n8n-service-api'].get('basic-username', '')
|
||||||
|
self.basic_password = self.pipeline_config['ai']['n8n-service-api'].get('basic-password', '')
|
||||||
|
elif self.auth_type == 'jwt':
|
||||||
|
self.jwt_secret = self.pipeline_config['ai']['n8n-service-api'].get('jwt-secret', '')
|
||||||
|
self.jwt_algorithm = self.pipeline_config['ai']['n8n-service-api'].get('jwt-algorithm', 'HS256')
|
||||||
|
elif self.auth_type == 'header':
|
||||||
|
self.header_name = self.pipeline_config['ai']['n8n-service-api'].get('header-name', '')
|
||||||
|
self.header_value = self.pipeline_config['ai']['n8n-service-api'].get('header-value', '')
|
||||||
|
|
||||||
|
async def _preprocess_user_message(self, query: core_entities.Query) -> str:
|
||||||
|
"""预处理用户消息,提取纯文本
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 纯文本消息
|
||||||
|
"""
|
||||||
|
plain_text = ''
|
||||||
|
|
||||||
|
if isinstance(query.user_message.content, list):
|
||||||
|
for ce in query.user_message.content:
|
||||||
|
if ce.type == 'text':
|
||||||
|
plain_text += ce.text
|
||||||
|
# 注意:n8n webhook目前不支持直接处理图片,如需支持可在此扩展
|
||||||
|
elif isinstance(query.user_message.content, str):
|
||||||
|
plain_text = query.user_message.content
|
||||||
|
|
||||||
|
return plain_text
|
||||||
|
|
||||||
|
async def _call_webhook(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]:
|
||||||
|
"""调用n8n webhook"""
|
||||||
|
# 生成会话ID(如果不存在)
|
||||||
|
if not query.session.using_conversation.uuid:
|
||||||
|
query.session.using_conversation.uuid = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# 预处理用户消息
|
||||||
|
plain_text = await self._preprocess_user_message(query)
|
||||||
|
|
||||||
|
# 准备请求数据
|
||||||
|
payload = {
|
||||||
|
# 基本消息内容
|
||||||
|
'message': plain_text,
|
||||||
|
'user_message_text': plain_text,
|
||||||
|
'conversation_id': query.session.using_conversation.uuid,
|
||||||
|
'session_id': query.variables.get('session_id', ''),
|
||||||
|
'user_id': f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
||||||
|
'msg_create_time': query.variables.get('msg_create_time', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加所有变量到payload
|
||||||
|
payload.update(query.variables)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 准备请求头和认证信息
|
||||||
|
headers = {}
|
||||||
|
auth = None
|
||||||
|
|
||||||
|
# 根据认证类型设置相应的认证信息
|
||||||
|
if self.auth_type == 'basic':
|
||||||
|
# 使用Basic认证
|
||||||
|
auth = aiohttp.BasicAuth(self.basic_username, self.basic_password)
|
||||||
|
self.ap.logger.debug(f'using basic auth: {self.basic_username}')
|
||||||
|
elif self.auth_type == 'jwt':
|
||||||
|
# 使用JWT认证
|
||||||
|
import jwt
|
||||||
|
import time
|
||||||
|
|
||||||
|
# 创建JWT令牌
|
||||||
|
payload_jwt = {
|
||||||
|
'exp': int(time.time()) + 3600, # 1小时过期
|
||||||
|
'iat': int(time.time()),
|
||||||
|
'sub': 'n8n-webhook',
|
||||||
|
}
|
||||||
|
token = jwt.encode(payload_jwt, self.jwt_secret, algorithm=self.jwt_algorithm)
|
||||||
|
|
||||||
|
# 添加到Authorization头
|
||||||
|
headers['Authorization'] = f'Bearer {token}'
|
||||||
|
self.ap.logger.debug('using jwt auth')
|
||||||
|
elif self.auth_type == 'header':
|
||||||
|
# 使用自定义请求头认证
|
||||||
|
headers[self.header_name] = self.header_value
|
||||||
|
self.ap.logger.debug(f'using header auth: {self.header_name}')
|
||||||
|
else:
|
||||||
|
self.ap.logger.debug('no auth')
|
||||||
|
|
||||||
|
# 调用webhook
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
self.webhook_url, json=payload, headers=headers, auth=auth, timeout=self.timeout
|
||||||
|
) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
error_text = await response.text()
|
||||||
|
self.ap.logger.error(f'n8n webhook call failed: {response.status}, {error_text}')
|
||||||
|
raise Exception(f'n8n webhook call failed: {response.status}, {error_text}')
|
||||||
|
|
||||||
|
# 解析响应
|
||||||
|
response_data = await response.json()
|
||||||
|
self.ap.logger.debug(f'n8n webhook response: {response_data}')
|
||||||
|
|
||||||
|
# 从响应中提取输出
|
||||||
|
if self.output_key in response_data:
|
||||||
|
output_content = response_data[self.output_key]
|
||||||
|
else:
|
||||||
|
# 如果没有指定的输出键,则使用整个响应
|
||||||
|
output_content = json.dumps(response_data, ensure_ascii=False)
|
||||||
|
|
||||||
|
# 返回消息
|
||||||
|
yield llm_entities.Message(
|
||||||
|
role='assistant',
|
||||||
|
content=output_content,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.ap.logger.error(f'n8n webhook call exception: {str(e)}')
|
||||||
|
raise N8nAPIError(f'n8n webhook call exception: {str(e)}')
|
||||||
|
|
||||||
|
async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]:
|
||||||
|
"""运行请求"""
|
||||||
|
async for msg in self._call_webhook(query):
|
||||||
|
yield msg
|
||||||
@@ -41,6 +41,8 @@ class SessionManager:
|
|||||||
query: core_entities.Query,
|
query: core_entities.Query,
|
||||||
session: core_entities.Session,
|
session: core_entities.Session,
|
||||||
prompt_config: list[dict],
|
prompt_config: list[dict],
|
||||||
|
pipeline_uuid: str,
|
||||||
|
bot_uuid: str,
|
||||||
) -> core_entities.Conversation:
|
) -> core_entities.Conversation:
|
||||||
"""获取对话或创建对话"""
|
"""获取对话或创建对话"""
|
||||||
|
|
||||||
@@ -58,13 +60,15 @@ class SessionManager:
|
|||||||
messages=prompt_messages,
|
messages=prompt_messages,
|
||||||
)
|
)
|
||||||
|
|
||||||
if session.using_conversation is None:
|
if session.using_conversation is None or session.using_conversation.pipeline_uuid != pipeline_uuid:
|
||||||
conversation = core_entities.Conversation(
|
conversation = core_entities.Conversation(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
messages=[],
|
messages=[],
|
||||||
use_funcs=await self.ap.tool_mgr.get_all_functions(
|
use_funcs=await self.ap.tool_mgr.get_all_functions(
|
||||||
plugin_enabled=True,
|
plugin_enabled=True,
|
||||||
),
|
),
|
||||||
|
pipeline_uuid=pipeline_uuid,
|
||||||
|
bot_uuid=bot_uuid,
|
||||||
)
|
)
|
||||||
session.conversations.append(conversation)
|
session.conversations.append(conversation)
|
||||||
session.using_conversation = conversation
|
session.using_conversation = conversation
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ class RuntimeMCPSession:
|
|||||||
|
|
||||||
for tool in tools.tools:
|
for tool in tools.tools:
|
||||||
|
|
||||||
async def func(query: core_entities.Query, **kwargs):
|
async def func(query: core_entities.Query, *, _tool=tool, **kwargs):
|
||||||
result = await self.session.call_tool(tool.name, kwargs)
|
result = await self.session.call_tool(_tool.name, kwargs)
|
||||||
if result.isError:
|
if result.isError:
|
||||||
raise Exception(result.content[0].text)
|
raise Exception(result.content[0].text)
|
||||||
return result.content[0].text
|
return result.content[0].text
|
||||||
|
|||||||
0
pkg/storage/__init__.py
Normal file
0
pkg/storage/__init__.py
Normal file
21
pkg/storage/mgr.py
Normal file
21
pkg/storage/mgr.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
from ..core import app
|
||||||
|
from . import provider
|
||||||
|
from .providers import localstorage
|
||||||
|
|
||||||
|
|
||||||
|
class StorageMgr:
|
||||||
|
"""存储管理器"""
|
||||||
|
|
||||||
|
ap: app.Application
|
||||||
|
|
||||||
|
storage_provider: provider.StorageProvider
|
||||||
|
|
||||||
|
def __init__(self, ap: app.Application):
|
||||||
|
self.ap = ap
|
||||||
|
self.storage_provider = localstorage.LocalStorageProvider(ap)
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
await self.storage_provider.initialize()
|
||||||
44
pkg/storage/provider.py
Normal file
44
pkg/storage/provider.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import abc
|
||||||
|
|
||||||
|
from ..core import app
|
||||||
|
|
||||||
|
|
||||||
|
class StorageProvider(abc.ABC):
|
||||||
|
ap: app.Application
|
||||||
|
|
||||||
|
def __init__(self, ap: app.Application):
|
||||||
|
self.ap = ap
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def save(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
value: bytes,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def load(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
) -> bytes:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def exists(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def delete(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
):
|
||||||
|
pass
|
||||||
0
pkg/storage/providers/__init__.py
Normal file
0
pkg/storage/providers/__init__.py
Normal file
45
pkg/storage/providers/localstorage.py
Normal file
45
pkg/storage/providers/localstorage.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import aiofiles
|
||||||
|
|
||||||
|
from ...core import app
|
||||||
|
|
||||||
|
from .. import provider
|
||||||
|
|
||||||
|
|
||||||
|
LOCAL_STORAGE_PATH = os.path.join('data', 'storage')
|
||||||
|
|
||||||
|
|
||||||
|
class LocalStorageProvider(provider.StorageProvider):
|
||||||
|
def __init__(self, ap: app.Application):
|
||||||
|
super().__init__(ap)
|
||||||
|
if not os.path.exists(LOCAL_STORAGE_PATH):
|
||||||
|
os.makedirs(LOCAL_STORAGE_PATH)
|
||||||
|
|
||||||
|
async def save(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
value: bytes,
|
||||||
|
):
|
||||||
|
async with aiofiles.open(os.path.join(LOCAL_STORAGE_PATH, f'{key}'), 'wb') as f:
|
||||||
|
await f.write(value)
|
||||||
|
|
||||||
|
async def load(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
) -> bytes:
|
||||||
|
async with aiofiles.open(os.path.join(LOCAL_STORAGE_PATH, f'{key}'), 'rb') as f:
|
||||||
|
return await f.read()
|
||||||
|
|
||||||
|
async def exists(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
) -> bool:
|
||||||
|
return os.path.exists(os.path.join(LOCAL_STORAGE_PATH, f'{key}'))
|
||||||
|
|
||||||
|
async def delete(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
):
|
||||||
|
os.remove(os.path.join(LOCAL_STORAGE_PATH, f'{key}'))
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
semantic_version = 'v4.0.3.2'
|
semantic_version = 'v4.0.6'
|
||||||
|
|
||||||
required_database_version = 2
|
required_database_version = 3
|
||||||
"""标记本版本所需要的数据库结构版本,用于判断数据库迁移"""
|
"""标记本版本所需要的数据库结构版本,用于判断数据库迁移"""
|
||||||
|
|
||||||
debug_mode = False
|
debug_mode = False
|
||||||
|
|||||||
@@ -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.
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -204,7 +204,9 @@ async def get_slack_image_to_base64(pic_url: str, bot_token: str):
|
|||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(pic_url, headers=headers) as resp:
|
async with session.get(pic_url, headers=headers) as resp:
|
||||||
image_data = await resp.read()
|
mime_type = resp.headers.get("Content-Type", "application/octet-stream")
|
||||||
return base64.b64encode(image_data).decode('utf-8')
|
file_bytes = await resp.read()
|
||||||
|
base64_str = base64.b64encode(file_bytes).decode("utf-8")
|
||||||
|
return f"data:{mime_type};base64,{base64_str}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise (e)
|
raise (e)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "langbot"
|
name = "langbot"
|
||||||
version = "4.0.3"
|
version = "4.0.6"
|
||||||
description = "高稳定、支持扩展、多模态 - 大模型原生即时通信机器人平台"
|
description = "高稳定、支持扩展、多模态 - 大模型原生即时通信机器人平台"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10.1"
|
requires-python = ">=3.10.1"
|
||||||
@@ -45,11 +45,10 @@ dependencies = [
|
|||||||
"websockets>=15.0.1",
|
"websockets>=15.0.1",
|
||||||
"python-socks>=2.7.1", # dingtalk missing dependency
|
"python-socks>=2.7.1", # dingtalk missing dependency
|
||||||
"taskgroup==0.0.0a4", # graingert/taskgroup#20
|
"taskgroup==0.0.0a4", # graingert/taskgroup#20
|
||||||
"pip>=25.1.1", # pkg.core.bootutils.deps
|
"pip>=25.1.1",
|
||||||
"google-genai>=1.15.0",
|
|
||||||
"google-generativeai>=0.8.5",
|
|
||||||
"ruff>=0.11.9",
|
"ruff>=0.11.9",
|
||||||
"pre-commit>=4.2.0",
|
"pre-commit>=4.2.0",
|
||||||
|
"uv>=0.7.11",
|
||||||
]
|
]
|
||||||
keywords = [
|
keywords = [
|
||||||
"bot",
|
"bot",
|
||||||
@@ -181,3 +180,4 @@ skip-magic-trailing-comma = false
|
|||||||
|
|
||||||
# Like Black, automatically detect the appropriate line ending.
|
# Like Black, automatically detect the appropriate line ending.
|
||||||
line-ending = "auto"
|
line-ending = "auto"
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
"ignore-rules": {
|
"ignore-rules": {
|
||||||
"prefix": [],
|
"prefix": [],
|
||||||
"regexp": []
|
"regexp": []
|
||||||
|
},
|
||||||
|
"misc": {
|
||||||
|
"combine-quote-message": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"safety": {
|
"safety": {
|
||||||
@@ -55,6 +58,18 @@
|
|||||||
"api-key": "your-api-key",
|
"api-key": "your-api-key",
|
||||||
"app-id": "your-app-id",
|
"app-id": "your-app-id",
|
||||||
"references-quote": "参考资料来自:"
|
"references-quote": "参考资料来自:"
|
||||||
|
},
|
||||||
|
"n8n-service-api": {
|
||||||
|
"webhook-url": "http://your-n8n-webhook-url",
|
||||||
|
"auth-type": "none",
|
||||||
|
"basic-username": "",
|
||||||
|
"basic-password": "",
|
||||||
|
"jwt-secret": "",
|
||||||
|
"jwt-algorithm": "HS256",
|
||||||
|
"header-name": "",
|
||||||
|
"header-value": "",
|
||||||
|
"timeout": 120,
|
||||||
|
"output-key": "response"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"output": {
|
"output": {
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ stages:
|
|||||||
label:
|
label:
|
||||||
en_US: Aliyun Dashscope App API
|
en_US: Aliyun Dashscope App API
|
||||||
zh_Hans: 阿里云百炼平台 API
|
zh_Hans: 阿里云百炼平台 API
|
||||||
|
- name: n8n-service-api
|
||||||
|
label:
|
||||||
|
en_US: n8n Workflow API
|
||||||
|
zh_Hans: n8n 工作流 API
|
||||||
- name: local-agent
|
- name: local-agent
|
||||||
label:
|
label:
|
||||||
en_US: Local Agent
|
en_US: Local Agent
|
||||||
@@ -170,3 +174,127 @@ stages:
|
|||||||
type: string
|
type: string
|
||||||
required: false
|
required: false
|
||||||
default: '参考资料来自:'
|
default: '参考资料来自:'
|
||||||
|
- name: n8n-service-api
|
||||||
|
label:
|
||||||
|
en_US: n8n Workflow API
|
||||||
|
zh_Hans: n8n 工作流 API
|
||||||
|
description:
|
||||||
|
en_US: Configure the n8n workflow API of the pipeline
|
||||||
|
zh_Hans: 配置 n8n 工作流 API
|
||||||
|
config:
|
||||||
|
- name: webhook-url
|
||||||
|
label:
|
||||||
|
en_US: Webhook URL
|
||||||
|
zh_Hans: Webhook URL
|
||||||
|
description:
|
||||||
|
en_US: The webhook URL of the n8n workflow
|
||||||
|
zh_Hans: n8n 工作流的 webhook URL
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
- name: auth-type
|
||||||
|
label:
|
||||||
|
en_US: Authentication Type
|
||||||
|
zh_Hans: 认证类型
|
||||||
|
description:
|
||||||
|
en_US: The authentication type for the webhook call
|
||||||
|
zh_Hans: webhook 调用的认证类型
|
||||||
|
type: select
|
||||||
|
required: true
|
||||||
|
default: 'none'
|
||||||
|
options:
|
||||||
|
- name: 'none'
|
||||||
|
label:
|
||||||
|
en_US: None
|
||||||
|
zh_Hans: 无认证
|
||||||
|
- name: 'basic'
|
||||||
|
label:
|
||||||
|
en_US: Basic Auth
|
||||||
|
zh_Hans: 基本认证
|
||||||
|
- name: 'jwt'
|
||||||
|
label:
|
||||||
|
en_US: JWT
|
||||||
|
zh_Hans: JWT认证
|
||||||
|
- name: 'header'
|
||||||
|
label:
|
||||||
|
en_US: Header Auth
|
||||||
|
zh_Hans: 请求头认证
|
||||||
|
- name: basic-username
|
||||||
|
label:
|
||||||
|
en_US: Username
|
||||||
|
zh_Hans: 用户名
|
||||||
|
description:
|
||||||
|
en_US: The username for Basic Auth
|
||||||
|
zh_Hans: 基本认证的用户名
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
- name: basic-password
|
||||||
|
label:
|
||||||
|
en_US: Password
|
||||||
|
zh_Hans: 密码
|
||||||
|
description:
|
||||||
|
en_US: The password for Basic Auth
|
||||||
|
zh_Hans: 基本认证的密码
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
- name: jwt-secret
|
||||||
|
label:
|
||||||
|
en_US: Secret
|
||||||
|
zh_Hans: 密钥
|
||||||
|
description:
|
||||||
|
en_US: The secret for JWT authentication
|
||||||
|
zh_Hans: JWT认证的密钥
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
- name: jwt-algorithm
|
||||||
|
label:
|
||||||
|
en_US: Algorithm
|
||||||
|
zh_Hans: 算法
|
||||||
|
description:
|
||||||
|
en_US: The algorithm for JWT authentication
|
||||||
|
zh_Hans: JWT认证的算法
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: 'HS256'
|
||||||
|
- name: header-name
|
||||||
|
label:
|
||||||
|
en_US: Header Name
|
||||||
|
zh_Hans: 请求头名称
|
||||||
|
description:
|
||||||
|
en_US: The header name for Header Auth
|
||||||
|
zh_Hans: 请求头认证的名称
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
- name: header-value
|
||||||
|
label:
|
||||||
|
en_US: Header Value
|
||||||
|
zh_Hans: 请求头值
|
||||||
|
description:
|
||||||
|
en_US: The header value for Header Auth
|
||||||
|
zh_Hans: 请求头认证的值
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
- name: timeout
|
||||||
|
label:
|
||||||
|
en_US: Timeout
|
||||||
|
zh_Hans: 超时时间
|
||||||
|
description:
|
||||||
|
en_US: The timeout in seconds for the webhook call
|
||||||
|
zh_Hans: webhook 调用的超时时间(秒)
|
||||||
|
type: integer
|
||||||
|
required: false
|
||||||
|
default: 120
|
||||||
|
- name: output-key
|
||||||
|
label:
|
||||||
|
en_US: Output Key
|
||||||
|
zh_Hans: 输出键名
|
||||||
|
description:
|
||||||
|
en_US: The key name of the output in the webhook response
|
||||||
|
zh_Hans: webhook 响应中输出内容的键名
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: 'response'
|
||||||
|
|||||||
293
web/package-lock.json
generated
293
web/package-lock.json
generated
@@ -15,6 +15,8 @@
|
|||||||
"@radix-ui/react-dialog": "^1.1.13",
|
"@radix-ui/react-dialog": "^1.1.13",
|
||||||
"@radix-ui/react-hover-card": "^1.1.13",
|
"@radix-ui/react-hover-card": "^1.1.13",
|
||||||
"@radix-ui/react-label": "^2.1.6",
|
"@radix-ui/react-label": "^2.1.6",
|
||||||
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
"@radix-ui/react-select": "^2.2.4",
|
"@radix-ui/react-select": "^2.2.4",
|
||||||
"@radix-ui/react-slot": "^1.2.2",
|
"@radix-ui/react-slot": "^1.2.2",
|
||||||
"@radix-ui/react-switch": "^1.2.4",
|
"@radix-ui/react-switch": "^1.2.4",
|
||||||
@@ -36,6 +38,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",
|
||||||
@@ -1254,6 +1257,215 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover": {
|
||||||
|
"version": "1.1.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz",
|
||||||
|
"integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.10",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.2",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-popper": "1.2.7",
|
||||||
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
|
"@radix-ui/react-presence": "1.1.4",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"aria-hidden": "^1.2.4",
|
||||||
|
"react-remove-scroll": "^2.6.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-arrow": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
|
"version": "1.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
|
||||||
|
"integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": {
|
||||||
|
"version": "1.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
|
||||||
|
"integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^2.0.0",
|
||||||
|
"@radix-ui/react-arrow": "1.1.7",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-rect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-size": "1.1.1",
|
||||||
|
"@radix-ui/rect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||||
|
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-popper": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.2.6",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.6.tgz",
|
||||||
@@ -1388,6 +1600,78 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-scroll-area": {
|
||||||
|
"version": "1.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz",
|
||||||
|
"integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/number": "1.1.1",
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-presence": "1.1.4",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-select": {
|
"node_modules/@radix-ui/react-select": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.4.tgz",
|
||||||
@@ -6126,6 +6410,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",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
|
"dev:local": "NEXT_PUBLIC_API_BASE_URL=http://localhost:5300 next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
@@ -23,6 +24,8 @@
|
|||||||
"@radix-ui/react-dialog": "^1.1.13",
|
"@radix-ui/react-dialog": "^1.1.13",
|
||||||
"@radix-ui/react-hover-card": "^1.1.13",
|
"@radix-ui/react-hover-card": "^1.1.13",
|
||||||
"@radix-ui/react-label": "^2.1.6",
|
"@radix-ui/react-label": "^2.1.6",
|
||||||
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
"@radix-ui/react-select": "^2.2.4",
|
"@radix-ui/react-select": "^2.2.4",
|
||||||
"@radix-ui/react-slot": "^1.2.2",
|
"@radix-ui/react-slot": "^1.2.2",
|
||||||
"@radix-ui/react-switch": "^1.2.4",
|
"@radix-ui/react-switch": "^1.2.4",
|
||||||
@@ -44,6 +47,7 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.56.3",
|
"react-hook-form": "^7.56.3",
|
||||||
"react-i18next": "^15.5.1",
|
"react-i18next": "^15.5.1",
|
||||||
|
"react-photo-view": "^1.2.7",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss": "^4.1.5",
|
"tailwindcss": "^4.1.5",
|
||||||
|
|||||||
63
web/src/app/home/bots/bot-log/BotLogManager.ts
Normal file
63
web/src/app/home/bots/bot-log/BotLogManager.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
|
import {
|
||||||
|
BotLog,
|
||||||
|
GetBotLogsResponse,
|
||||||
|
} from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
||||||
|
|
||||||
|
export class BotLogManager {
|
||||||
|
private botId: string;
|
||||||
|
private callbacks: ((_: BotLog[]) => void)[] = [];
|
||||||
|
private intervalIds: number[] = [];
|
||||||
|
|
||||||
|
constructor(botId: string) {
|
||||||
|
this.botId = botId;
|
||||||
|
}
|
||||||
|
|
||||||
|
startListenServerPush() {
|
||||||
|
const timerNumber = setInterval(() => {
|
||||||
|
this.getLogList(-1, 50).then((response) => {
|
||||||
|
this.callbacks.forEach((callback) =>
|
||||||
|
callback(this.parseResponse(response)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
this.intervalIds.push(Number(timerNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
stopServerPush() {
|
||||||
|
this.intervalIds.forEach((id) => clearInterval(id));
|
||||||
|
this.intervalIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeLogPush(callback: (_: BotLog[]) => void) {
|
||||||
|
if (!this.callbacks.includes(callback)) {
|
||||||
|
this.callbacks.push(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.callbacks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日志页的基本信息
|
||||||
|
*/
|
||||||
|
private getLogList(next: number, count: number = 20) {
|
||||||
|
return httpClient.getBotLogs(this.botId, {
|
||||||
|
from_index: next,
|
||||||
|
max_count: count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFirstPage() {
|
||||||
|
return this.parseResponse(await this.getLogList(-1, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadMore(position: number, total: number) {
|
||||||
|
return this.parseResponse(await this.getLogList(position, total));
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseResponse(httpResponse: GetBotLogsResponse): BotLog[] {
|
||||||
|
return httpResponse.logs;
|
||||||
|
}
|
||||||
|
}
|
||||||
116
web/src/app/home/bots/bot-log/view/BotLogCard.tsx
Normal file
116
web/src/app/home/bots/bot-log/view/BotLogCard.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { BotLog } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
||||||
|
import styles from './botLog.module.css';
|
||||||
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
|
import { PhotoProvider } from 'react-photo-view';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export function BotLogCard({ botLog }: { botLog: BotLog }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const baseURL = httpClient.getBaseUrl();
|
||||||
|
|
||||||
|
function formatTime(timestamp: number) {
|
||||||
|
const now = new Date();
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
|
||||||
|
// 获取各个时间部分
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth() + 1; // 月份从0开始,需要+1
|
||||||
|
const day = date.getDate();
|
||||||
|
const hours = date.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
// 判断时间范围
|
||||||
|
const isToday = now.toDateString() === date.toDateString();
|
||||||
|
const isYesterday =
|
||||||
|
new Date(now.setDate(now.getDate() - 1)).toDateString() ===
|
||||||
|
date.toDateString();
|
||||||
|
const isThisYear = now.getFullYear() === year;
|
||||||
|
|
||||||
|
if (isToday) {
|
||||||
|
return `${hours}:${minutes}`; // 今天的消息:小时:分钟
|
||||||
|
} else if (isYesterday) {
|
||||||
|
return `${t('bots.yesterday')} ${hours}:${minutes}`; // 昨天的消息:昨天 小时:分钟
|
||||||
|
} else if (isThisYear) {
|
||||||
|
return t('bots.dateFormat', { month, day }); // 本年消息:x月x日
|
||||||
|
} else {
|
||||||
|
return t('bots.earlier'); // 更早的消息:更久之前
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubChatId(str: string) {
|
||||||
|
const strArr = str.split('');
|
||||||
|
return strArr;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={`${styles.botLogCardContainer}`}>
|
||||||
|
{/* 头部标签,时间 */}
|
||||||
|
<div className={`${styles.cardTitleContainer}`}>
|
||||||
|
<div className={`flex flex-row gap-4`}>
|
||||||
|
<div className={`${styles.tag}`}>{botLog.level}</div>
|
||||||
|
{botLog.message_session_id && (
|
||||||
|
<div
|
||||||
|
className={`${styles.tag} ${styles.chatTag}`}
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(botLog.message_session_id)
|
||||||
|
.then(() => {
|
||||||
|
toast.success(t('common.copySuccess'));
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="icon"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
p-id="1664"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M96.1 575.7a32.2 32.1 0 1 0 64.4 0 32.2 32.1 0 1 0-64.4 0Z"
|
||||||
|
p-id="1665"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M742.1 450.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.1 26-13.8 26-31s-11.7-31.3-26-31.4zM742.1 577.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.2 26-13.8 26-31s-11.7-31.3-26-31.4z"
|
||||||
|
p-id="1666"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M736.1 63.9H417c-70.4 0-128 57.6-128 128h-64.9c-70.4 0-128 57.6-128 128v128c-0.1 17.7 14.4 32 32.2 32 17.8 0 32.2-14.4 32.2-32.1V320c0-35.2 28.8-64 64-64H289v447.8c0 70.4 57.6 128 128 128h255.1c-0.1 35.2-28.8 63.8-64 63.8H224.5c-35.2 0-64-28.8-64-64V703.5c0-17.7-14.4-32.1-32.2-32.1-17.8 0-32.3 14.4-32.3 32.1v128.3c0 70.4 57.6 128 128 128h384.1c70.4 0 128-57.6 128-128h65c70.4 0 128-57.6 128-128V255.9l-193-192z m0.1 63.4l127.7 128.3H800c-35.2 0-64-28.8-64-64v-64.3h0.2z m64 641H416.1c-35.2 0-64-28.8-64-64v-513c0-35.2 28.8-64 64-64H671V191c0 70.4 57.6 128 128 128h65.2v385.3c0 35.2-28.8 64-64 64z"
|
||||||
|
p-id="1667"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
{/* 会话ID */}
|
||||||
|
|
||||||
|
<span className={`${styles.chatId}`}>
|
||||||
|
{getSubChatId(botLog.message_session_id)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>{formatTime(botLog.timestamp)}</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.cardTitleContainer} ${styles.cardText}`}>
|
||||||
|
{botLog.text}
|
||||||
|
</div>
|
||||||
|
<PhotoProvider className={``}>
|
||||||
|
<div className={`w-50 mt-2`}>
|
||||||
|
{botLog.images.map((item) => (
|
||||||
|
<img
|
||||||
|
key={item}
|
||||||
|
src={`${baseURL}/api/v1/files/image/${item}`}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PhotoProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
129
web/src/app/home/bots/bot-log/view/BotLogListComponent.tsx
Normal file
129
web/src/app/home/bots/bot-log/view/BotLogListComponent.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { BotLogManager } from '@/app/home/bots/bot-log/BotLogManager';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { BotLog } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
||||||
|
import { BotLogCard } from '@/app/home/bots/bot-log/view/BotLogCard';
|
||||||
|
import styles from './botLog.module.css';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export function BotLogListComponent({ botId }: { botId: string }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const manager = useRef(new BotLogManager(botId)).current;
|
||||||
|
const [botLogList, setBotLogList] = useState<BotLog[]>([]);
|
||||||
|
const [autoFlush, setAutoFlush] = useState(true);
|
||||||
|
const listContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const botLogListRef = useRef<BotLog[]>(botLogList);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initComponent();
|
||||||
|
return () => {
|
||||||
|
onDestroy();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
botLogListRef.current = botLogList;
|
||||||
|
}, [botLogList]);
|
||||||
|
|
||||||
|
// 观测自动刷新状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoFlush) {
|
||||||
|
manager.startListenServerPush();
|
||||||
|
} else {
|
||||||
|
manager.stopServerPush();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
manager.stopServerPush();
|
||||||
|
};
|
||||||
|
}, [autoFlush]);
|
||||||
|
|
||||||
|
function initComponent() {
|
||||||
|
// 订阅日志推送
|
||||||
|
manager.subscribeLogPush(handleBotLogPush);
|
||||||
|
// 加载第一页日志
|
||||||
|
manager.loadFirstPage().then((response) => {
|
||||||
|
setBotLogList(response.reverse());
|
||||||
|
});
|
||||||
|
// 监听滚动
|
||||||
|
listenScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDestroy() {
|
||||||
|
manager.dispose();
|
||||||
|
removeScrollListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
function listenScroll() {
|
||||||
|
if (!listContainerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = listContainerRef.current;
|
||||||
|
list.addEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeScrollListener() {
|
||||||
|
if (!listContainerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = listContainerRef.current;
|
||||||
|
list.removeEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMore() {
|
||||||
|
// 加载更多日志
|
||||||
|
const list = botLogListRef.current;
|
||||||
|
const lastSeq = list[list.length - 1].seq_id;
|
||||||
|
if (lastSeq === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
manager.loadMore(lastSeq - 1, 10).then((response) => {
|
||||||
|
setBotLogList([...list, ...response.reverse()]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBotLogPush(response: BotLog[]) {
|
||||||
|
setBotLogList(response.reverse());
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScroll = useCallback(
|
||||||
|
debounce(() => {
|
||||||
|
if (!listContainerRef.current) return;
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } =
|
||||||
|
listContainerRef.current;
|
||||||
|
const isBottom = scrollTop + clientHeight >= scrollHeight - 5;
|
||||||
|
const isTop = scrollTop === 0;
|
||||||
|
|
||||||
|
if (isBottom) {
|
||||||
|
setAutoFlush(false);
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
if (isTop) {
|
||||||
|
setAutoFlush(true);
|
||||||
|
}
|
||||||
|
if (!isTop && !isBottom) {
|
||||||
|
setAutoFlush(false);
|
||||||
|
}
|
||||||
|
}, 300), // 防抖延迟 300ms
|
||||||
|
[botLogList], // 依赖项为空
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.botLogListContainer} px-6`}
|
||||||
|
ref={listContainerRef}
|
||||||
|
>
|
||||||
|
<div className={`${styles.listHeader}`}>
|
||||||
|
<div className={'mr-2'}>{t('bots.enableAutoRefresh')}</div>
|
||||||
|
<Switch checked={autoFlush} onCheckedChange={(e) => setAutoFlush(e)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{botLogList.map((botLog) => {
|
||||||
|
return <BotLogCard botLog={botLog} key={botLog.seq_id} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
68
web/src/app/home/bots/bot-log/view/botLog.module.css
Normal file
68
web/src/app/home/bots/bot-log/view/botLog.module.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
.botLogListContainer {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 10rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.botLogCardContainer {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
padding: 1.2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listHeader {
|
||||||
|
width: 100%;
|
||||||
|
height: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.2rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background-color: #a5d8ff;
|
||||||
|
color: #ffffff;
|
||||||
|
max-width: 16rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatTag {
|
||||||
|
color: #626262;
|
||||||
|
background-color: #d1d1d1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatId {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardTitleContainer {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardText {
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
@@ -1,7 +1,35 @@
|
|||||||
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';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
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 +75,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();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-auto h-[40px]"
|
||||||
|
onClick={(e) => {
|
||||||
|
onClickLogIcon();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<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"></path>
|
||||||
|
</svg>
|
||||||
|
{t('bots.log')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ export interface IBotCardVO {
|
|||||||
iconURL: string;
|
iconURL: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
adapter: string;
|
||||||
adapterLabel: string;
|
adapterLabel: string;
|
||||||
|
adapterConfig: object;
|
||||||
usePipelineName: string;
|
usePipelineName: string;
|
||||||
|
enable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BotCardVO implements IBotCardVO {
|
export class BotCardVO implements IBotCardVO {
|
||||||
@@ -12,15 +15,21 @@ export class BotCardVO implements IBotCardVO {
|
|||||||
iconURL: string;
|
iconURL: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
adapter: string;
|
||||||
adapterLabel: string;
|
adapterLabel: string;
|
||||||
|
adapterConfig: object;
|
||||||
usePipelineName: string;
|
usePipelineName: string;
|
||||||
|
enable: boolean;
|
||||||
|
|
||||||
constructor(props: IBotCardVO) {
|
constructor(props: IBotCardVO) {
|
||||||
this.id = props.id;
|
this.id = props.id;
|
||||||
this.iconURL = props.iconURL;
|
this.iconURL = props.iconURL;
|
||||||
this.name = props.name;
|
this.name = props.name;
|
||||||
this.description = props.description;
|
this.description = props.description;
|
||||||
|
this.adapter = props.adapter;
|
||||||
|
this.adapterConfig = props.adapterConfig;
|
||||||
this.adapterLabel = props.adapterLabel;
|
this.adapterLabel = props.adapterLabel;
|
||||||
this.usePipelineName = props.usePipelineName;
|
this.usePipelineName = props.usePipelineName;
|
||||||
|
this.enable = props.enable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
/* background-color: aqua; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconImage {
|
.iconImage {
|
||||||
@@ -30,10 +29,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.basicInfoContainer {
|
.basicInfoContainer {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
min-width: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,4 +103,14 @@
|
|||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.botOperationContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 100%;
|
||||||
|
width: 3rem;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ export default function BotForm({
|
|||||||
default: item.default,
|
default: item.default,
|
||||||
id: UUID.generate(),
|
id: UUID.generate(),
|
||||||
label: item.label,
|
label: item.label,
|
||||||
|
description: item.description,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
required: item.required,
|
required: item.required,
|
||||||
type: parseDynamicFormItemType(item.type),
|
type: parseDynamicFormItemType(item.type),
|
||||||
|
|||||||
@@ -17,13 +17,18 @@ import {
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { i18nObj } from '@/i18n/I18nProvider';
|
import { i18nObj } from '@/i18n/I18nProvider';
|
||||||
|
import { BotLogListComponent } from '@/app/home/bots/bot-log/view/BotLogListComponent';
|
||||||
|
|
||||||
export default function BotConfigPage() {
|
export default function BotConfigPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
// 编辑机器人的modal
|
||||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||||
|
// 机器人日志的modal
|
||||||
|
const [logModalOpen, setLogModalOpen] = useState<boolean>(false);
|
||||||
const [botList, setBotList] = useState<BotCardVO[]>([]);
|
const [botList, setBotList] = useState<BotCardVO[]>([]);
|
||||||
const [isEditForm, setIsEditForm] = useState(false);
|
const [isEditForm, setIsEditForm] = useState(false);
|
||||||
const [nowSelectedBotUUID, setNowSelectedBotUUID] = useState<string>();
|
const [nowSelectedBotUUID, setNowSelectedBotUUID] = useState<string>();
|
||||||
|
const [nowSelectedBotLog, setNowSelectedBotLog] = useState<string>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getBotList();
|
getBotList();
|
||||||
@@ -47,10 +52,13 @@ export default function BotConfigPage() {
|
|||||||
iconURL: httpClient.getAdapterIconURL(bot.adapter),
|
iconURL: httpClient.getAdapterIconURL(bot.adapter),
|
||||||
name: bot.name,
|
name: bot.name,
|
||||||
description: bot.description,
|
description: bot.description,
|
||||||
|
adapter: bot.adapter,
|
||||||
|
adapterConfig: bot.adapter_config,
|
||||||
adapterLabel:
|
adapterLabel:
|
||||||
adapterList.find((item) => item.value === bot.adapter)?.label ||
|
adapterList.find((item) => item.value === bot.adapter)?.label ||
|
||||||
bot.adapter.substring(0, 10),
|
bot.adapter.substring(0, 10),
|
||||||
usePipelineName: bot.use_pipeline_name || '',
|
usePipelineName: bot.use_pipeline_name || '',
|
||||||
|
enable: bot.enable || false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
setBotList(botList);
|
setBotList(botList);
|
||||||
@@ -76,6 +84,11 @@ export default function BotConfigPage() {
|
|||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClickLogIcon(botId: string) {
|
||||||
|
setNowSelectedBotLog(botId);
|
||||||
|
setLogModalOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.configPageContainer}>
|
<div className={styles.configPageContainer}>
|
||||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||||
@@ -107,6 +120,15 @@ export default function BotConfigPage() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={logModalOpen} onOpenChange={setLogModalOpen}>
|
||||||
|
<DialogContent className="w-[700px] max-h-[80vh] p-0 flex flex-col">
|
||||||
|
<DialogHeader className="px-6 pt-6 pb-0">
|
||||||
|
<DialogTitle>{t('bots.botLogTitle')}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<BotLogListComponent botId={nowSelectedBotLog || ''} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* 注意:其余的返回内容需要保持在Spin组件外部 */}
|
{/* 注意:其余的返回内容需要保持在Spin组件外部 */}
|
||||||
<div className={`${styles.botListContainer}`}>
|
<div className={`${styles.botListContainer}`}>
|
||||||
<CreateCardComponent
|
<CreateCardComponent
|
||||||
@@ -123,7 +145,22 @@ export default function BotConfigPage() {
|
|||||||
selectBot(cardVO.id);
|
selectBot(cardVO.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BotCard botCardVO={cardVO} />
|
<BotCard
|
||||||
|
botCardVO={cardVO}
|
||||||
|
clickLogIconCallback={(id) => {
|
||||||
|
onClickLogIcon(id);
|
||||||
|
}}
|
||||||
|
setBotEnableCallback={(id, enable) => {
|
||||||
|
setBotList(
|
||||||
|
botList.map((bot) => {
|
||||||
|
if (bot.id === id) {
|
||||||
|
return { ...bot, enable: enable };
|
||||||
|
}
|
||||||
|
return bot;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
||||||
|
import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent';
|
||||||
|
import { i18nObj } from '@/i18n/I18nProvider';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* N8n认证表单组件
|
||||||
|
* 根据选择的认证类型动态显示相应的表单项
|
||||||
|
*/
|
||||||
|
export default function N8nAuthFormComponent({
|
||||||
|
itemConfigList,
|
||||||
|
onSubmit,
|
||||||
|
initialValues,
|
||||||
|
}: {
|
||||||
|
itemConfigList: IDynamicFormItemSchema[];
|
||||||
|
onSubmit?: (val: object) => unknown;
|
||||||
|
initialValues?: Record<string, string>;
|
||||||
|
}) {
|
||||||
|
// 当前选择的认证类型
|
||||||
|
const [authType, setAuthType] = useState<string>(
|
||||||
|
initialValues?.['auth-type'] || 'none',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 根据 itemConfigList 动态生成 zod schema
|
||||||
|
const formSchema = z.object(
|
||||||
|
itemConfigList.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
let fieldSchema;
|
||||||
|
switch (item.type) {
|
||||||
|
case 'integer':
|
||||||
|
fieldSchema = z.number();
|
||||||
|
break;
|
||||||
|
case 'float':
|
||||||
|
fieldSchema = z.number();
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
fieldSchema = z.boolean();
|
||||||
|
break;
|
||||||
|
case 'string':
|
||||||
|
fieldSchema = z.string();
|
||||||
|
break;
|
||||||
|
case 'array[string]':
|
||||||
|
fieldSchema = z.array(z.string());
|
||||||
|
break;
|
||||||
|
case 'select':
|
||||||
|
fieldSchema = z.string();
|
||||||
|
break;
|
||||||
|
case 'llm-model-selector':
|
||||||
|
fieldSchema = z.string();
|
||||||
|
break;
|
||||||
|
case 'prompt-editor':
|
||||||
|
fieldSchema = z.array(
|
||||||
|
z.object({
|
||||||
|
content: z.string(),
|
||||||
|
role: z.string(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fieldSchema = z.string();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
item.required &&
|
||||||
|
(fieldSchema instanceof z.ZodString ||
|
||||||
|
fieldSchema instanceof z.ZodArray)
|
||||||
|
) {
|
||||||
|
fieldSchema = fieldSchema.min(1, { message: '此字段为必填项' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[item.name]: fieldSchema,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{} as Record<string, z.ZodTypeAny>,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
const form = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: itemConfigList.reduce((acc, item) => {
|
||||||
|
// 优先使用 initialValues,如果没有则使用默认值
|
||||||
|
const value = initialValues?.[item.name] ?? item.default;
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[item.name]: value,
|
||||||
|
};
|
||||||
|
}, {} as FormValues),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当 initialValues 变化时更新表单值
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialValues) {
|
||||||
|
// 合并默认值和初始值
|
||||||
|
const mergedValues = itemConfigList.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
acc[item.name] = initialValues[item.name] ?? item.default;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>,
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.entries(mergedValues).forEach(([key, value]) => {
|
||||||
|
form.setValue(key as keyof FormValues, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新认证类型
|
||||||
|
setAuthType((mergedValues['auth-type'] as string) || 'none');
|
||||||
|
}
|
||||||
|
}, [initialValues, form, itemConfigList]);
|
||||||
|
|
||||||
|
// 监听表单值变化
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = form.watch((value, { name }) => {
|
||||||
|
// 如果认证类型变化,更新状态
|
||||||
|
if (name === 'auth-type') {
|
||||||
|
setAuthType(value['auth-type'] as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取完整的表单值,确保包含所有默认值
|
||||||
|
const formValues = form.getValues();
|
||||||
|
const finalValues = itemConfigList.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
acc[item.name] = formValues[item.name] ?? item.default;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>,
|
||||||
|
);
|
||||||
|
|
||||||
|
onSubmit?.(finalValues);
|
||||||
|
});
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [form, onSubmit, itemConfigList]);
|
||||||
|
|
||||||
|
// 根据认证类型过滤表单项
|
||||||
|
const filteredConfigList = itemConfigList.filter((config) => {
|
||||||
|
// 始终显示webhook-url、auth-type、timeout和output-key
|
||||||
|
if (
|
||||||
|
['webhook-url', 'auth-type', 'timeout', 'output-key'].includes(
|
||||||
|
config.name,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据认证类型显示相应的表单项
|
||||||
|
if (authType === 'basic' && config.name.startsWith('basic-')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (authType === 'jwt' && config.name.startsWith('jwt-')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (authType === 'header' && config.name.startsWith('header-')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{filteredConfigList.map((config) => (
|
||||||
|
<FormField
|
||||||
|
key={config.id}
|
||||||
|
control={form.control}
|
||||||
|
name={config.name as keyof FormValues}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{i18nObj(config.label)}{' '}
|
||||||
|
{config.required && <span className="text-red-500">*</span>}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<DynamicFormItemComponent config={config} field={field} />
|
||||||
|
</FormControl>
|
||||||
|
{config.description && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{i18nObj(config.description)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import {
|
|
||||||
DynamicFormItemType,
|
|
||||||
IDynamicFormItemSchema,
|
|
||||||
} from '@/app/infra/entities/form/dynamic';
|
|
||||||
import { DynamicFormItemConfig } from '@/app/home/components/dynamic-form/DynamicFormItemConfig';
|
|
||||||
|
|
||||||
export const testDynamicConfigList: IDynamicFormItemSchema[] = [
|
|
||||||
new DynamicFormItemConfig({
|
|
||||||
default: '',
|
|
||||||
id: '111',
|
|
||||||
label: {
|
|
||||||
zh_Hans: '测试字段string',
|
|
||||||
en_US: 'eng test',
|
|
||||||
},
|
|
||||||
name: 'string_test',
|
|
||||||
required: false,
|
|
||||||
type: DynamicFormItemType.STRING,
|
|
||||||
}),
|
|
||||||
new DynamicFormItemConfig({
|
|
||||||
default: '',
|
|
||||||
id: '222',
|
|
||||||
label: {
|
|
||||||
zh_Hans: '测试字段int',
|
|
||||||
en_US: 'int eng test',
|
|
||||||
},
|
|
||||||
name: 'int_test',
|
|
||||||
required: true,
|
|
||||||
type: DynamicFormItemType.INT,
|
|
||||||
}),
|
|
||||||
new DynamicFormItemConfig({
|
|
||||||
default: '',
|
|
||||||
id: '333',
|
|
||||||
label: {
|
|
||||||
zh_Hans: '测试字段boolean',
|
|
||||||
en_US: 'boolean eng test',
|
|
||||||
},
|
|
||||||
name: 'boolean_test',
|
|
||||||
required: false,
|
|
||||||
type: DynamicFormItemType.BOOLEAN,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
@@ -169,7 +169,6 @@ export default function LLMForm({
|
|||||||
} else {
|
} else {
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
import styles from './pipelineCard.module.css';
|
import styles from './pipelineCard.module.css';
|
||||||
import { PipelineCardVO } from '@/app/home/pipelines/components/pipeline-card/PipelineCardVO';
|
import { PipelineCardVO } from '@/app/home/pipelines/components/pipeline-card/PipelineCardVO';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
export default function PipelineCard({ cardVO }: { cardVO: PipelineCardVO }) {
|
export default function PipelineCard({
|
||||||
|
cardVO,
|
||||||
|
onDebug,
|
||||||
|
}: {
|
||||||
|
cardVO: PipelineCardVO;
|
||||||
|
onDebug: (pipelineId: string) => void;
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleDebugClick = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDebug(cardVO.id);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.cardContainer}`}>
|
<div className={`${styles.cardContainer}`}>
|
||||||
<div className={`${styles.basicInfoContainer}`}>
|
<div className={`${styles.basicInfoContainer}`}>
|
||||||
@@ -48,6 +61,22 @@ export default function PipelineCard({ cardVO }: { cardVO: PipelineCardVO }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleDebugClick}
|
||||||
|
title={t('pipelines.chat')}
|
||||||
|
className="mt-auto"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className={styles.debugButtonIcon}
|
||||||
|
>
|
||||||
|
<path d="M13 19.9C15.2822 19.4367 17 17.419 17 15V12C17 11.299 16.8564 10.6219 16.5846 10H7.41538C7.14358 10.6219 7 11.299 7 12V15C7 17.419 8.71776 19.4367 11 19.9V14H13V19.9ZM5.5358 17.6907C5.19061 16.8623 5 15.9534 5 15H2V13H5V12C5 11.3573 5.08661 10.7348 5.2488 10.1436L3.0359 8.86602L4.0359 7.13397L6.05636 8.30049C6.11995 8.19854 6.18609 8.09835 6.25469 8H17.7453C17.8139 8.09835 17.88 8.19854 17.9436 8.30049L19.9641 7.13397L20.9641 8.86602L18.7512 10.1436C18.9134 10.7348 19 11.3573 19 12V13H22V15H19C19 15.9534 18.8094 16.8623 18.4642 17.6907L20.9641 19.134L19.9641 20.866L17.4383 19.4077C16.1549 20.9893 14.1955 22 12 22C9.80453 22 7.84512 20.9893 6.56171 19.4077L4.0359 20.866L3.0359 19.134L5.5358 17.6907ZM8 6C8 3.79086 9.79086 2 12 2C14.2091 2 16 3.79086 16 6H8Z"></path>
|
||||||
|
</svg>
|
||||||
|
{t('pipelines.chat')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -67,9 +67,11 @@
|
|||||||
|
|
||||||
.operationContainer {
|
.operationContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
width: 5rem;
|
width: 8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operationDefaultBadge {
|
.operationDefaultBadge {
|
||||||
@@ -97,4 +99,9 @@
|
|||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.debugButtonIcon {
|
||||||
|
width: 1.2rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from '@/app/infra/entities/pipeline';
|
} from '@/app/infra/entities/pipeline';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||||
|
import N8nAuthFormComponent from '@/app/home/components/dynamic-form/N8nAuthFormComponent';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
@@ -244,6 +245,37 @@ export default function PipelineFormComponent({
|
|||||||
if (stage.name !== currentRunner) {
|
if (stage.name !== currentRunner) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 对于n8n-service-api配置,使用N8nAuthFormComponent处理表单联动
|
||||||
|
if (stage.name === 'n8n-service-api') {
|
||||||
|
return (
|
||||||
|
<div key={stage.name} className="space-y-4 mb-6">
|
||||||
|
<div className="text-lg font-medium">{i18nObj(stage.label)}</div>
|
||||||
|
{stage.description && (
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
{i18nObj(stage.description)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<N8nAuthFormComponent
|
||||||
|
itemConfigList={stage.config}
|
||||||
|
initialValues={
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(form.watch(formName) as Record<string, any>)?.[stage.name] ||
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
onSubmit={(values) => {
|
||||||
|
const currentValues =
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(form.getValues(formName) as Record<string, any>) || {};
|
||||||
|
form.setValue(formName, {
|
||||||
|
...currentValues,
|
||||||
|
[stage.name]: values,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
31
web/src/app/home/pipelines/debug-dialog/AtBadge.tsx
Normal file
31
web/src/app/home/pipelines/debug-dialog/AtBadge.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
|
interface AtBadgeProps {
|
||||||
|
targetName: string;
|
||||||
|
readonly?: boolean;
|
||||||
|
onRemove?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AtBadge({
|
||||||
|
targetName,
|
||||||
|
readonly = false,
|
||||||
|
onRemove,
|
||||||
|
}: AtBadgeProps) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center gap-1 px-2 py-1 text-sm bg-blue-100 text-blue-600 hover:bg-blue-200"
|
||||||
|
>
|
||||||
|
@{targetName}
|
||||||
|
{!readonly && onRemove && (
|
||||||
|
<button
|
||||||
|
onClick={onRemove}
|
||||||
|
className="ml-1 hover:text-blue-800 focus:outline-none"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
422
web/src/app/home/pipelines/debug-dialog/DebugDialog.tsx
Normal file
422
web/src/app/home/pipelines/debug-dialog/DebugDialog.tsx
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Pipeline } from '@/app/infra/entities/api';
|
||||||
|
import { Message } from '@/app/infra/entities/message';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import AtBadge from './AtBadge';
|
||||||
|
|
||||||
|
interface MessageComponent {
|
||||||
|
type: 'At' | 'Plain';
|
||||||
|
target?: string;
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DebugDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
pipelineId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DebugDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
pipelineId,
|
||||||
|
}: DebugDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [selectedPipelineId, setSelectedPipelineId] = useState(pipelineId);
|
||||||
|
const [sessionType, setSessionType] = useState<'person' | 'group'>('person');
|
||||||
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [pipelines, setPipelines] = useState<Pipeline[]>([]);
|
||||||
|
const [showAtPopover, setShowAtPopover] = useState(false);
|
||||||
|
const [hasAt, setHasAt] = useState(false);
|
||||||
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setSelectedPipelineId(pipelineId);
|
||||||
|
loadPipelines();
|
||||||
|
loadMessages(pipelineId);
|
||||||
|
}
|
||||||
|
}, [open, pipelineId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
loadMessages(selectedPipelineId);
|
||||||
|
}
|
||||||
|
}, [sessionType, selectedPipelineId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
popoverRef.current &&
|
||||||
|
!popoverRef.current.contains(event.target as Node) &&
|
||||||
|
!inputRef.current?.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setShowAtPopover(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showAtPopover) {
|
||||||
|
setIsHovering(true);
|
||||||
|
}
|
||||||
|
}, [showAtPopover]);
|
||||||
|
|
||||||
|
const loadPipelines = async () => {
|
||||||
|
try {
|
||||||
|
const response = await httpClient.getPipelines();
|
||||||
|
setPipelines(response.pipelines);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load pipelines:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadMessages = async (pipelineId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await httpClient.getWebChatHistoryMessages(
|
||||||
|
pipelineId,
|
||||||
|
sessionType,
|
||||||
|
);
|
||||||
|
setMessages(response.messages);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load messages:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (sessionType === 'group') {
|
||||||
|
if (value.endsWith('@')) {
|
||||||
|
setShowAtPopover(true);
|
||||||
|
} else if (showAtPopover && (!value.includes('@') || value.length > 1)) {
|
||||||
|
setShowAtPopover(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInputValue(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAtSelect = () => {
|
||||||
|
setHasAt(true);
|
||||||
|
setShowAtPopover(false);
|
||||||
|
setInputValue(inputValue.slice(0, -1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAtRemove = () => {
|
||||||
|
setHasAt(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (showAtPopover) {
|
||||||
|
handleAtSelect();
|
||||||
|
} else {
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Backspace' && hasAt && inputValue === '') {
|
||||||
|
handleAtRemove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessage = async () => {
|
||||||
|
if (!inputValue.trim() && !hasAt) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const messageChain = [];
|
||||||
|
|
||||||
|
let text_content = inputValue.trim();
|
||||||
|
if (hasAt) {
|
||||||
|
text_content = ' ' + text_content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAt) {
|
||||||
|
messageChain.push({
|
||||||
|
type: 'At',
|
||||||
|
target: 'webchatbot',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
messageChain.push({
|
||||||
|
type: 'Plain',
|
||||||
|
text: text_content,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasAt) {
|
||||||
|
// for showing
|
||||||
|
text_content = '@webchatbot' + text_content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userMessage: Message = {
|
||||||
|
id: -1,
|
||||||
|
role: 'user',
|
||||||
|
content: text_content,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
message_chain: messageChain,
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages((prevMessages) => [...prevMessages, userMessage]);
|
||||||
|
setInputValue('');
|
||||||
|
setHasAt(false);
|
||||||
|
|
||||||
|
const response = await httpClient.sendWebChatMessage(
|
||||||
|
sessionType,
|
||||||
|
messageChain,
|
||||||
|
selectedPipelineId,
|
||||||
|
120000,
|
||||||
|
);
|
||||||
|
|
||||||
|
setMessages((prevMessages) => [...prevMessages, response.message]);
|
||||||
|
} catch (
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
error: any
|
||||||
|
) {
|
||||||
|
console.log(error, 'type of error', typeof error);
|
||||||
|
console.error('Failed to send message:', error);
|
||||||
|
|
||||||
|
if (!error.message.includes('timeout') && sessionType === 'person') {
|
||||||
|
toast.error(t('pipelines.debugDialog.sendFailed'));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// const resetSession = async () => {
|
||||||
|
// try {
|
||||||
|
// await httpClient.resetWebChatSession(selectedPipelineId, sessionType);
|
||||||
|
// setMessages([]);
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Failed to reset session:', error);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
const renderMessageContent = (message: Message) => {
|
||||||
|
return (
|
||||||
|
<span className="text-base leading-relaxed align-middle whitespace-pre-wrap">
|
||||||
|
{(message.message_chain as MessageComponent[]).map(
|
||||||
|
(component, index) => {
|
||||||
|
if (component.type === 'At') {
|
||||||
|
return (
|
||||||
|
<AtBadge
|
||||||
|
key={index}
|
||||||
|
targetName={component.target || ''}
|
||||||
|
readonly={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (component.type === 'Plain') {
|
||||||
|
return <span key={index}>{component.text}</span>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="!max-w-[70vw] max-w-6xl h-[70vh] p-6 flex flex-col rounded-2xl shadow-2xl bg-white">
|
||||||
|
<DialogHeader className="pl-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<DialogTitle className="flex items-center gap-4 font-bold">
|
||||||
|
{t('pipelines.debugDialog.title')}
|
||||||
|
<Select
|
||||||
|
value={selectedPipelineId}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSelectedPipelineId(value);
|
||||||
|
loadMessages(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-white">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="rounded-xl shadow-lg">
|
||||||
|
{pipelines.map((pipeline) => (
|
||||||
|
<SelectItem
|
||||||
|
key={pipeline.uuid}
|
||||||
|
value={pipeline.uuid || ''}
|
||||||
|
className="rounded-lg"
|
||||||
|
>
|
||||||
|
{pipeline.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</DialogTitle>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-1 h-full min-h-0 border-t">
|
||||||
|
<div className="w-50 bg-white border-r p-6 pl-0 rounded-l-2xl flex-shrink-0 flex flex-col justify-start gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className={`w-full justify-center rounded-md px-4 py-6 text-base font-medium transition-none ${
|
||||||
|
sessionType === 'person'
|
||||||
|
? 'bg-[#2288ee] text-white hover:bg-[#2288ee] hover:text-white'
|
||||||
|
: 'bg-white text-gray-800 hover:bg-gray-100'
|
||||||
|
} border-0 shadow-none`}
|
||||||
|
onClick={() => setSessionType('person')}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M4 22C4 17.5817 7.58172 14 12 14C16.4183 14 20 17.5817 20 22H18C18 18.6863 15.3137 16 12 16C8.68629 16 6 18.6863 6 22H4ZM12 13C8.685 13 6 10.315 6 7C6 3.685 8.685 1 12 1C15.315 1 18 3.685 18 7C18 10.315 15.315 13 12 13ZM12 11C14.21 11 16 9.21 16 7C16 4.79 14.21 3 12 3C9.79 3 8 4.79 8 7C8 9.21 9.79 11 12 11Z"></path>
|
||||||
|
</svg>
|
||||||
|
{t('pipelines.debugDialog.privateChat')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className={`w-full justify-center rounded-md px-4 py-6 text-base font-medium transition-none ${
|
||||||
|
sessionType === 'group'
|
||||||
|
? 'bg-[#2288ee] text-white hover:bg-[#2288ee] hover:text-white'
|
||||||
|
: 'bg-white text-gray-800 hover:bg-gray-100'
|
||||||
|
} border-0 shadow-none`}
|
||||||
|
onClick={() => setSessionType('group')}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M2 22C2 17.5817 5.58172 14 10 14C14.4183 14 18 17.5817 18 22H16C16 18.6863 13.3137 16 10 16C6.68629 16 4 18.6863 4 22H2ZM10 13C6.685 13 4 10.315 4 7C4 3.685 6.685 1 10 1C13.315 1 16 3.685 16 7C16 10.315 13.315 13 10 13ZM10 11C12.21 11 14 9.21 14 7C14 4.79 12.21 3 10 3C7.79 3 6 4.79 6 7C6 9.21 7.79 11 10 11ZM18.2837 14.7028C21.0644 15.9561 23 18.752 23 22H21C21 19.564 19.5483 17.4671 17.4628 16.5271L18.2837 14.7028ZM17.5962 3.41321C19.5944 4.23703 21 6.20361 21 8.5C21 11.3702 18.8042 13.7252 16 13.9776V11.9646C17.6967 11.7222 19 10.264 19 8.5C19 7.11935 18.2016 5.92603 17.041 5.35635L17.5962 3.41321Z"></path>
|
||||||
|
</svg>
|
||||||
|
{t('pipelines.debugDialog.groupChat')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 flex flex-col w-[10rem] h-full min-h-0">
|
||||||
|
<ScrollArea className="flex-1 p-6 overflow-y-auto min-h-0 bg-white">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<div className="text-center text-muted-foreground py-12 text-lg">
|
||||||
|
{t('pipelines.debugDialog.noMessages')}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
messages.map((message) => (
|
||||||
|
<div
|
||||||
|
key={message.id + message.timestamp}
|
||||||
|
className={cn(
|
||||||
|
'flex',
|
||||||
|
message.role === 'user'
|
||||||
|
? 'justify-end'
|
||||||
|
: 'justify-start',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'max-w-md px-5 py-3 rounded-2xl',
|
||||||
|
message.role === 'user'
|
||||||
|
? 'bg-[#2288ee] text-white rounded-br-none'
|
||||||
|
: 'bg-gray-100 text-gray-900 rounded-bl-none',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{renderMessageContent(message)}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'text-xs mt-2',
|
||||||
|
message.role === 'user'
|
||||||
|
? 'text-white/70'
|
||||||
|
: 'text-gray-500',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{message.role === 'user'
|
||||||
|
? t('pipelines.debugDialog.userMessage')
|
||||||
|
: t('pipelines.debugDialog.botMessage')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<div className="border-t p-4 pb-0 bg-white flex gap-2">
|
||||||
|
<div className="flex-1 flex items-center gap-2">
|
||||||
|
{hasAt && (
|
||||||
|
<AtBadge targetName="webchatbot" onRemove={handleAtRemove} />
|
||||||
|
)}
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
placeholder={t('pipelines.debugDialog.inputPlaceholder')}
|
||||||
|
className="flex-1 rounded-md px-3 py-2 border border-gray-300 focus:border-[#2288ee] transition-none text-base"
|
||||||
|
/>
|
||||||
|
{showAtPopover && (
|
||||||
|
<div
|
||||||
|
ref={popoverRef}
|
||||||
|
className="absolute bottom-full left-0 mb-2 w-auto rounded-md border bg-white shadow-lg"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 px-4 py-1.5 rounded cursor-pointer',
|
||||||
|
isHovering ? 'bg-gray-100' : 'bg-white',
|
||||||
|
)}
|
||||||
|
onClick={handleAtSelect}
|
||||||
|
onMouseEnter={() => setIsHovering(true)}
|
||||||
|
onMouseLeave={() => setIsHovering(false)}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
@webchatbot - {t('pipelines.debugDialog.atTips')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={sendMessage}
|
||||||
|
disabled={!inputValue.trim() && !hasAt}
|
||||||
|
className="rounded-md bg-[#2288ee] hover:bg-[#2288ee] w-20 text-white px-6 py-2 text-base font-medium transition-none flex items-center gap-2 shadow-none"
|
||||||
|
>
|
||||||
|
<>{t('pipelines.debugDialog.send')}</>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import DebugDialog from './debug-dialog/DebugDialog';
|
||||||
|
|
||||||
export default function PluginConfigPage() {
|
export default function PluginConfigPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||||
@@ -32,6 +34,8 @@ export default function PluginConfigPage() {
|
|||||||
const [disableForm, setDisableForm] = useState(false);
|
const [disableForm, setDisableForm] = useState(false);
|
||||||
const [selectedPipelineIsDefault, setSelectedPipelineIsDefault] =
|
const [selectedPipelineIsDefault, setSelectedPipelineIsDefault] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [debugDialogOpen, setDebugDialogOpen] = useState(false);
|
||||||
|
const [debugPipelineId, setDebugPipelineId] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getPipelines();
|
getPipelines();
|
||||||
@@ -92,6 +96,11 @@ export default function PluginConfigPage() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDebug = (pipelineId: string) => {
|
||||||
|
setDebugPipelineId(pipelineId);
|
||||||
|
setDebugDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.configPageContainer}>
|
<div className={styles.configPageContainer}>
|
||||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||||
@@ -149,11 +158,17 @@ export default function PluginConfigPage() {
|
|||||||
getSelectedPipelineForm(pipeline.id);
|
getSelectedPipelineForm(pipeline.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PipelineCard cardVO={pipeline} />
|
<PipelineCard cardVO={pipeline} onDebug={handleDebug} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DebugDialog
|
||||||
|
open={debugDialogOpen}
|
||||||
|
onOpenChange={setDebugDialogOpen}
|
||||||
|
pipelineId={debugPipelineId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function PluginForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!pluginInfo || !pluginConfig) {
|
if (!pluginInfo || !pluginConfig) {
|
||||||
return <div>加载中...</div>;
|
return <div>{t('plugins.loading')}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletePlugin() {
|
function deletePlugin() {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
||||||
import { PipelineConfigTab } from '@/app/infra/entities/pipeline';
|
import { PipelineConfigTab } from '@/app/infra/entities/pipeline';
|
||||||
import { I18nLabel } from '@/app/infra/entities/common';
|
import { I18nLabel } from '@/app/infra/entities/common';
|
||||||
|
import { Message } from '@/app/infra/entities/message';
|
||||||
|
|
||||||
export interface ApiResponse<T> {
|
export interface ApiResponse<T> {
|
||||||
code: number;
|
code: number;
|
||||||
@@ -84,18 +85,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[];
|
||||||
}
|
}
|
||||||
@@ -211,77 +204,10 @@ export interface MarketPluginResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface GetPipelineConfig {
|
interface GetPipelineConfig {
|
||||||
ai: {
|
ai: object;
|
||||||
'dashscope-app-api': {
|
output: object;
|
||||||
'api-key': string;
|
safety: object;
|
||||||
'app-id': string;
|
trigger: object;
|
||||||
'app-type': 'agent' | 'workflow';
|
|
||||||
'references-quote'?: string;
|
|
||||||
};
|
|
||||||
'dify-service-api': {
|
|
||||||
'api-key': string;
|
|
||||||
'app-type': 'chat' | 'agent' | 'workflow';
|
|
||||||
'base-url': string;
|
|
||||||
'thinking-convert': 'plain' | 'original' | 'remove';
|
|
||||||
timeout?: number;
|
|
||||||
};
|
|
||||||
'local-agent': {
|
|
||||||
'max-round': number;
|
|
||||||
model: string;
|
|
||||||
prompt: Array<{
|
|
||||||
content: string;
|
|
||||||
role: string;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
runner: {
|
|
||||||
runner: 'local-agent' | 'dify-service-api' | 'dashscope-app-api';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
output: {
|
|
||||||
'force-delay': {
|
|
||||||
max: number;
|
|
||||||
min: number;
|
|
||||||
};
|
|
||||||
'long-text-processing': {
|
|
||||||
'font-path': string;
|
|
||||||
strategy: 'forward' | 'image';
|
|
||||||
threshold: number;
|
|
||||||
};
|
|
||||||
misc: {
|
|
||||||
'at-sender': boolean;
|
|
||||||
'hide-exception': boolean;
|
|
||||||
'quote-origin': boolean;
|
|
||||||
'track-function-calls': boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
safety: {
|
|
||||||
'content-filter': {
|
|
||||||
'check-sensitive-words': boolean;
|
|
||||||
scope: 'all' | 'income-msg' | 'output-msg';
|
|
||||||
};
|
|
||||||
'rate-limit': {
|
|
||||||
limitation: number;
|
|
||||||
strategy: 'drop' | 'wait';
|
|
||||||
'window-length': number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
trigger: {
|
|
||||||
'access-control': {
|
|
||||||
blacklist: string[];
|
|
||||||
mode: 'blacklist' | 'whitelist';
|
|
||||||
whitelist: string[];
|
|
||||||
};
|
|
||||||
'group-respond-rules': {
|
|
||||||
at: boolean;
|
|
||||||
prefix: string[];
|
|
||||||
random: number;
|
|
||||||
regexp: string[];
|
|
||||||
};
|
|
||||||
'ignore-rules': {
|
|
||||||
prefix: string[];
|
|
||||||
regexp: string[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetPipeline {
|
interface GetPipeline {
|
||||||
@@ -303,3 +229,11 @@ export interface GetPipelineResponseData {
|
|||||||
export interface GetPipelineMetadataResponseData {
|
export interface GetPipelineMetadataResponseData {
|
||||||
configs: PipelineConfigTab[];
|
configs: PipelineConfigTab[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiRespWebChatMessage {
|
||||||
|
message: Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiRespWebChatMessages {
|
||||||
|
messages: Message[];
|
||||||
|
}
|
||||||
|
|||||||
7
web/src/app/infra/entities/message/index.ts
Normal file
7
web/src/app/infra/entities/message/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export interface Message {
|
||||||
|
id: number;
|
||||||
|
role: 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
message_chain: object[];
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
@@ -29,7 +29,11 @@ import {
|
|||||||
GetPipelineResponseData,
|
GetPipelineResponseData,
|
||||||
GetPipelineMetadataResponseData,
|
GetPipelineMetadataResponseData,
|
||||||
AsyncTask,
|
AsyncTask,
|
||||||
|
ApiRespWebChatMessage,
|
||||||
|
ApiRespWebChatMessages,
|
||||||
} 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 +58,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 +81,9 @@ class HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底URL,如果使用未配置会走到这里
|
// 外部获取baseURL的方法
|
||||||
private getBaseUrl(): string {
|
getBaseUrl(): string {
|
||||||
// NOT IMPLEMENT
|
return this.baseURL;
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
// 服务端环境
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
// 客户端环境
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取Session
|
// 获取Session
|
||||||
@@ -303,6 +303,43 @@ class HttpClient {
|
|||||||
return this.delete(`/api/v1/pipelines/${uuid}`);
|
return this.delete(`/api/v1/pipelines/${uuid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ Debug WebChat API ============
|
||||||
|
public sendWebChatMessage(
|
||||||
|
sessionType: string,
|
||||||
|
messageChain: object[],
|
||||||
|
pipelineId: string,
|
||||||
|
timeout: number = 15000,
|
||||||
|
): Promise<ApiRespWebChatMessage> {
|
||||||
|
return this.post(
|
||||||
|
`/api/v1/pipelines/${pipelineId}/chat/send`,
|
||||||
|
{
|
||||||
|
session_type: sessionType,
|
||||||
|
message: messageChain,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getWebChatHistoryMessages(
|
||||||
|
pipelineId: string,
|
||||||
|
sessionType: string,
|
||||||
|
): Promise<ApiRespWebChatMessages> {
|
||||||
|
return this.get(
|
||||||
|
`/api/v1/pipelines/${pipelineId}/chat/messages/${sessionType}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetWebChatSession(
|
||||||
|
pipelineId: string,
|
||||||
|
sessionType: string,
|
||||||
|
): Promise<{ message: string }> {
|
||||||
|
return this.post(
|
||||||
|
`/api/v1/pipelines/${pipelineId}/chat/reset/${sessionType}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Platform API ============
|
// ============ Platform API ============
|
||||||
public getAdapters(): Promise<ApiRespPlatformAdapters> {
|
public getAdapters(): Promise<ApiRespPlatformAdapters> {
|
||||||
return this.get('/api/v1/platform/adapters');
|
return this.get('/api/v1/platform/adapters');
|
||||||
@@ -345,6 +382,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,9 +494,15 @@ class HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// export const httpClient = new HttpClient("https://version-4.langbot.dev");
|
const getBaseURL = (): string => {
|
||||||
// export const httpClient = new HttpClient('http://localhost:5300');
|
if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_API_BASE_URL) {
|
||||||
export const httpClient = new HttpClient('/');
|
return process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '/';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const httpClient = new HttpClient(getBaseURL());
|
||||||
|
|
||||||
// 临时写法,未来两种Client都继承自HttpClient父类,不允许共享方法
|
// 临时写法,未来两种Client都继承自HttpClient父类,不允许共享方法
|
||||||
export const spaceClient = new HttpClient('https://space.langbot.app');
|
export const spaceClient = new HttpClient('https://space.langbot.app');
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface GetBotLogsRequest {
|
||||||
|
from_index: number; // 从某索引开始往前找,-1代表结尾,也就是拉取最新的
|
||||||
|
max_count: number; // 最大拉取数量
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user