mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-17 18:06:06 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65463820f3 | |||
| 9d5de54379 | |||
| 9846099957 |
@@ -30,8 +30,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
ports:
|
ports:
|
||||||
- 5300:5300 # For web ui and webhook callback
|
- 5300:5300 # For web ui
|
||||||
- 2280-2285:2280-2285 # For platform reverse connection
|
- 2280-2290:2280-2290 # For platform webhook
|
||||||
networks:
|
networks:
|
||||||
- langbot_network
|
- langbot_network
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "langbot"
|
name = "langbot"
|
||||||
version = "4.6.1"
|
version = "4.5.4"
|
||||||
description = "Easy-to-use global IM bot platform designed for LLM era"
|
description = "Easy-to-use global IM bot platform designed for LLM era"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
@@ -63,7 +63,7 @@ dependencies = [
|
|||||||
"langchain-text-splitters>=0.0.1",
|
"langchain-text-splitters>=0.0.1",
|
||||||
"chromadb>=0.4.24",
|
"chromadb>=0.4.24",
|
||||||
"qdrant-client (>=1.15.1,<2.0.0)",
|
"qdrant-client (>=1.15.1,<2.0.0)",
|
||||||
"langbot-plugin==0.2.0",
|
"langbot-plugin==0.2.0b1",
|
||||||
"asyncpg>=0.30.0",
|
"asyncpg>=0.30.0",
|
||||||
"line-bot-sdk>=3.19.0",
|
"line-bot-sdk>=3.19.0",
|
||||||
"tboxsdk>=0.0.10",
|
"tboxsdk>=0.0.10",
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""LangBot - Easy-to-use global IM bot platform designed for LLM era"""
|
"""LangBot - Easy-to-use global IM bot platform designed for LLM era"""
|
||||||
|
|
||||||
__version__ = '4.6.1'
|
__version__ = '4.5.4'
|
||||||
|
|||||||
@@ -23,25 +23,20 @@ xml_template = """
|
|||||||
|
|
||||||
|
|
||||||
class OAClient:
|
class OAClient:
|
||||||
def __init__(self, token: str, EncodingAESKey: str, AppID: str, Appsecret: str, logger: None, unified_mode: bool = False):
|
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
|
||||||
self.appsecret = Appsecret
|
self.appsecret = Appsecret
|
||||||
self.base_url = 'https://api.weixin.qq.com'
|
self.base_url = 'https://api.weixin.qq.com'
|
||||||
self.access_token = ''
|
self.access_token = ''
|
||||||
self.unified_mode = unified_mode
|
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
|
self.app.add_url_rule(
|
||||||
# 只有在非统一模式下才注册独立路由
|
'/callback/command',
|
||||||
if not self.unified_mode:
|
'handle_callback',
|
||||||
self.app.add_url_rule(
|
self.handle_callback_request,
|
||||||
'/callback/command',
|
methods=['GET', 'POST'],
|
||||||
'handle_callback',
|
)
|
||||||
self.handle_callback_request,
|
|
||||||
methods=['GET', 'POST'],
|
|
||||||
)
|
|
||||||
|
|
||||||
self._message_handlers = {
|
self._message_handlers = {
|
||||||
'example': [],
|
'example': [],
|
||||||
}
|
}
|
||||||
@@ -51,39 +46,19 @@ class OAClient:
|
|||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
"""处理回调请求(独立端口模式,使用全局 request)。"""
|
|
||||||
return await self._handle_callback_internal(request)
|
|
||||||
|
|
||||||
async def handle_unified_webhook(self, req):
|
|
||||||
"""处理回调请求(统一 webhook 模式,显式传递 request)。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
req: Quart Request 对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应数据
|
|
||||||
"""
|
|
||||||
return await self._handle_callback_internal(req)
|
|
||||||
|
|
||||||
async def _handle_callback_internal(self, req):
|
|
||||||
"""处理回调请求的内部实现,包括 GET 验证和 POST 消息接收。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
req: Quart Request 对象
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# 每隔100毫秒查询是否生成ai回答
|
# 每隔100毫秒查询是否生成ai回答
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
signature = req.args.get('signature', '')
|
signature = request.args.get('signature', '')
|
||||||
timestamp = req.args.get('timestamp', '')
|
timestamp = request.args.get('timestamp', '')
|
||||||
nonce = req.args.get('nonce', '')
|
nonce = request.args.get('nonce', '')
|
||||||
echostr = req.args.get('echostr', '')
|
echostr = request.args.get('echostr', '')
|
||||||
msg_signature = req.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不在请求体中')
|
await self.logger.error('msg_signature不在请求体中')
|
||||||
raise Exception('msg_signature不在请求体中')
|
raise Exception('msg_signature不在请求体中')
|
||||||
|
|
||||||
if req.method == 'GET':
|
if request.method == 'GET':
|
||||||
# 校验签名
|
# 校验签名
|
||||||
check_str = ''.join(sorted([self.token, timestamp, nonce]))
|
check_str = ''.join(sorted([self.token, timestamp, nonce]))
|
||||||
check_signature = hashlib.sha1(check_str.encode('utf-8')).hexdigest()
|
check_signature = hashlib.sha1(check_str.encode('utf-8')).hexdigest()
|
||||||
@@ -93,8 +68,8 @@ class OAClient:
|
|||||||
else:
|
else:
|
||||||
await self.logger.error('拒绝请求')
|
await self.logger.error('拒绝请求')
|
||||||
raise Exception('拒绝请求')
|
raise Exception('拒绝请求')
|
||||||
elif req.method == 'POST':
|
elif request.method == 'POST':
|
||||||
encryt_msg = await req.data
|
encryt_msg = await request.data
|
||||||
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid)
|
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid)
|
||||||
ret, xml_msg = wxcpt.DecryptMsg(encryt_msg, msg_signature, timestamp, nonce)
|
ret, xml_msg = wxcpt.DecryptMsg(encryt_msg, msg_signature, timestamp, nonce)
|
||||||
xml_msg = xml_msg.decode('utf-8')
|
xml_msg = xml_msg.decode('utf-8')
|
||||||
@@ -207,7 +182,6 @@ class OAClientForLongerResponse:
|
|||||||
Appsecret: str,
|
Appsecret: str,
|
||||||
LoadingMessage: str,
|
LoadingMessage: str,
|
||||||
logger: None,
|
logger: None,
|
||||||
unified_mode: bool = False,
|
|
||||||
):
|
):
|
||||||
self.token = token
|
self.token = token
|
||||||
self.aes = EncodingAESKey
|
self.aes = EncodingAESKey
|
||||||
@@ -215,18 +189,13 @@ class OAClientForLongerResponse:
|
|||||||
self.appsecret = Appsecret
|
self.appsecret = Appsecret
|
||||||
self.base_url = 'https://api.weixin.qq.com'
|
self.base_url = 'https://api.weixin.qq.com'
|
||||||
self.access_token = ''
|
self.access_token = ''
|
||||||
self.unified_mode = unified_mode
|
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
|
self.app.add_url_rule(
|
||||||
# 只有在非统一模式下才注册独立路由
|
'/callback/command',
|
||||||
if not self.unified_mode:
|
'handle_callback',
|
||||||
self.app.add_url_rule(
|
self.handle_callback_request,
|
||||||
'/callback/command',
|
methods=['GET', 'POST'],
|
||||||
'handle_callback',
|
)
|
||||||
self.handle_callback_request,
|
|
||||||
methods=['GET', 'POST'],
|
|
||||||
)
|
|
||||||
|
|
||||||
self._message_handlers = {
|
self._message_handlers = {
|
||||||
'example': [],
|
'example': [],
|
||||||
}
|
}
|
||||||
@@ -237,44 +206,24 @@ class OAClientForLongerResponse:
|
|||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
"""处理回调请求(独立端口模式,使用全局 request)。"""
|
|
||||||
return await self._handle_callback_internal(request)
|
|
||||||
|
|
||||||
async def handle_unified_webhook(self, req):
|
|
||||||
"""处理回调请求(统一 webhook 模式,显式传递 request)。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
req: Quart Request 对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应数据
|
|
||||||
"""
|
|
||||||
return await self._handle_callback_internal(req)
|
|
||||||
|
|
||||||
async def _handle_callback_internal(self, req):
|
|
||||||
"""处理回调请求的内部实现,包括 GET 验证和 POST 消息接收。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
req: Quart Request 对象
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
signature = req.args.get('signature', '')
|
signature = request.args.get('signature', '')
|
||||||
timestamp = req.args.get('timestamp', '')
|
timestamp = request.args.get('timestamp', '')
|
||||||
nonce = req.args.get('nonce', '')
|
nonce = request.args.get('nonce', '')
|
||||||
echostr = req.args.get('echostr', '')
|
echostr = request.args.get('echostr', '')
|
||||||
msg_signature = req.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不在请求体中')
|
await self.logger.error('msg_signature不在请求体中')
|
||||||
raise Exception('msg_signature不在请求体中')
|
raise Exception('msg_signature不在请求体中')
|
||||||
|
|
||||||
if req.method == 'GET':
|
if request.method == 'GET':
|
||||||
check_str = ''.join(sorted([self.token, timestamp, nonce]))
|
check_str = ''.join(sorted([self.token, timestamp, nonce]))
|
||||||
check_signature = hashlib.sha1(check_str.encode('utf-8')).hexdigest()
|
check_signature = hashlib.sha1(check_str.encode('utf-8')).hexdigest()
|
||||||
return echostr if check_signature == signature else '拒绝请求'
|
return echostr if check_signature == signature else '拒绝请求'
|
||||||
|
|
||||||
elif req.method == 'POST':
|
elif request.method == 'POST':
|
||||||
encryt_msg = await req.data
|
encryt_msg = await request.data
|
||||||
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid)
|
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid)
|
||||||
ret, xml_msg = wxcpt.DecryptMsg(encryt_msg, msg_signature, timestamp, nonce)
|
ret, xml_msg = wxcpt.DecryptMsg(encryt_msg, msg_signature, timestamp, nonce)
|
||||||
xml_msg = xml_msg.decode('utf-8')
|
xml_msg = xml_msg.decode('utf-8')
|
||||||
|
|||||||
@@ -10,20 +10,38 @@ import traceback
|
|||||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||||
|
|
||||||
|
|
||||||
|
def handle_validation(body: dict, bot_secret: str):
|
||||||
|
# bot正确的secert是32位的,此处仅为了适配演示demo
|
||||||
|
while len(bot_secret) < 32:
|
||||||
|
bot_secret = bot_secret * 2
|
||||||
|
bot_secret = bot_secret[:32]
|
||||||
|
# 实际使用场景中以上三行内容可清除
|
||||||
|
|
||||||
|
seed_bytes = bot_secret.encode()
|
||||||
|
|
||||||
|
signing_key = ed25519.Ed25519PrivateKey.from_private_bytes(seed_bytes)
|
||||||
|
|
||||||
|
msg = body['d']['event_ts'] + body['d']['plain_token']
|
||||||
|
msg_bytes = msg.encode()
|
||||||
|
|
||||||
|
signature = signing_key.sign(msg_bytes)
|
||||||
|
|
||||||
|
signature_hex = signature.hex()
|
||||||
|
|
||||||
|
response = {'plain_token': body['d']['plain_token'], 'signature': signature_hex}
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
class QQOfficialClient:
|
class QQOfficialClient:
|
||||||
def __init__(self, secret: str, token: str, app_id: str, logger: None, unified_mode: bool = False):
|
def __init__(self, secret: str, token: str, app_id: str, logger: None):
|
||||||
self.unified_mode = unified_mode
|
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
|
self.app.add_url_rule(
|
||||||
# 只有在非统一模式下才注册独立路由
|
'/callback/command',
|
||||||
if not self.unified_mode:
|
'handle_callback',
|
||||||
self.app.add_url_rule(
|
self.handle_callback_request,
|
||||||
'/callback/command',
|
methods=['GET', 'POST'],
|
||||||
'handle_callback',
|
)
|
||||||
self.handle_callback_request,
|
|
||||||
methods=['GET', 'POST'],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.secret = secret
|
self.secret = secret
|
||||||
self.token = token
|
self.token = token
|
||||||
self.app_id = app_id
|
self.app_id = app_id
|
||||||
@@ -64,45 +82,18 @@ class QQOfficialClient:
|
|||||||
raise Exception(f'获取access_token失败: {e}')
|
raise Exception(f'获取access_token失败: {e}')
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
"""处理回调请求(独立端口模式,使用全局 request)"""
|
"""处理回调请求"""
|
||||||
return await self._handle_callback_internal(request)
|
|
||||||
|
|
||||||
async def handle_unified_webhook(self, req):
|
|
||||||
"""处理回调请求(统一 webhook 模式,显式传递 request)。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
req: Quart Request 对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应数据
|
|
||||||
"""
|
|
||||||
return await self._handle_callback_internal(req)
|
|
||||||
|
|
||||||
async def _handle_callback_internal(self, req):
|
|
||||||
"""处理回调请求的内部实现。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
req: Quart Request 对象
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
|
# 读取请求数据
|
||||||
body = await req.get_data()
|
body = await request.get_data()
|
||||||
|
|
||||||
print(f'[QQ Official] Received request, body length: {len(body)}')
|
|
||||||
|
|
||||||
if not body or len(body) == 0:
|
|
||||||
print('[QQ Official] Received empty body, might be health check or GET request')
|
|
||||||
return {'code': 0, 'message': 'ok'}, 200
|
|
||||||
|
|
||||||
payload = json.loads(body)
|
payload = json.loads(body)
|
||||||
|
|
||||||
|
# 验证是否为回调验证请求
|
||||||
if payload.get('op') == 13:
|
if payload.get('op') == 13:
|
||||||
validation_data = payload.get('d')
|
# 生成签名
|
||||||
if not validation_data:
|
response = handle_validation(payload, self.secret)
|
||||||
return {'error': "missing 'd' field"}, 400
|
|
||||||
response = await self.verify(validation_data)
|
return response
|
||||||
return response, 200
|
|
||||||
|
|
||||||
if payload.get('op') == 0:
|
if payload.get('op') == 0:
|
||||||
message_data = await self.get_message(payload)
|
message_data = await self.get_message(payload)
|
||||||
@@ -113,7 +104,6 @@ class QQOfficialClient:
|
|||||||
return {'code': 0, 'message': 'success'}
|
return {'code': 0, 'message': 'success'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'[QQ Official] ERROR: {traceback.format_exc()}')
|
|
||||||
await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}')
|
await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}')
|
||||||
return {'error': str(e)}, 400
|
return {'error': str(e)}, 400
|
||||||
|
|
||||||
@@ -271,26 +261,3 @@ class QQOfficialClient:
|
|||||||
if self.access_token_expiry_time is None:
|
if self.access_token_expiry_time is None:
|
||||||
return True
|
return True
|
||||||
return time.time() > self.access_token_expiry_time
|
return time.time() > self.access_token_expiry_time
|
||||||
|
|
||||||
async def repeat_seed(self, bot_secret: str, target_size: int = 32) -> bytes:
|
|
||||||
seed = bot_secret
|
|
||||||
while len(seed) < target_size:
|
|
||||||
seed *= 2
|
|
||||||
return seed[:target_size].encode("utf-8")
|
|
||||||
|
|
||||||
async def verify(self, validation_payload: dict):
|
|
||||||
seed = await self.repeat_seed(self.secret)
|
|
||||||
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(seed)
|
|
||||||
|
|
||||||
event_ts = validation_payload.get("event_ts", "")
|
|
||||||
plain_token = validation_payload.get("plain_token", "")
|
|
||||||
msg = event_ts + plain_token
|
|
||||||
|
|
||||||
# sign
|
|
||||||
signature = private_key.sign(msg.encode()).hex()
|
|
||||||
|
|
||||||
response = {
|
|
||||||
"plain_token": plain_token,
|
|
||||||
"signature": signature,
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
|
|||||||
@@ -8,19 +8,14 @@ import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
|||||||
|
|
||||||
|
|
||||||
class SlackClient:
|
class SlackClient:
|
||||||
def __init__(self, bot_token: str, signing_secret: str, logger: None, unified_mode: bool = False):
|
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.unified_mode = unified_mode
|
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
self.client = AsyncWebClient(self.bot_token)
|
self.client = AsyncWebClient(self.bot_token)
|
||||||
|
self.app.add_url_rule(
|
||||||
# 只有在非统一模式下才注册独立路由
|
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
|
||||||
if not self.unified_mode:
|
)
|
||||||
self.app.add_url_rule(
|
|
||||||
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
|
|
||||||
)
|
|
||||||
|
|
||||||
self._message_handlers = {
|
self._message_handlers = {
|
||||||
'example': [],
|
'example': [],
|
||||||
}
|
}
|
||||||
@@ -28,28 +23,8 @@ class SlackClient:
|
|||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
"""处理回调请求(独立端口模式,使用全局 request)"""
|
|
||||||
return await self._handle_callback_internal(request)
|
|
||||||
|
|
||||||
async def handle_unified_webhook(self, req):
|
|
||||||
"""处理回调请求(统一 webhook 模式,显式传递 request)。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
req: Quart Request 对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应数据
|
|
||||||
"""
|
|
||||||
return await self._handle_callback_internal(req)
|
|
||||||
|
|
||||||
async def _handle_callback_internal(self, req):
|
|
||||||
"""处理回调请求的内部实现。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
req: Quart Request 对象
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
body = await req.get_data()
|
body = await request.get_data()
|
||||||
data = json.loads(body)
|
data = json.loads(body)
|
||||||
if 'type' in data:
|
if 'type' in data:
|
||||||
if data['type'] == 'url_verification':
|
if data['type'] == 'url_verification':
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ class StreamSessionManager:
|
|||||||
|
|
||||||
|
|
||||||
class WecomBotClient:
|
class WecomBotClient:
|
||||||
def __init__(self, Token: str, EnCodingAESKey: str, Corpid: str, logger: EventLogger, unified_mode: bool = False):
|
def __init__(self, Token: str, EnCodingAESKey: str, Corpid: str, logger: EventLogger):
|
||||||
"""企业微信智能机器人客户端。
|
"""企业微信智能机器人客户端。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -208,7 +208,6 @@ class WecomBotClient:
|
|||||||
EnCodingAESKey: 企业微信消息加解密密钥。
|
EnCodingAESKey: 企业微信消息加解密密钥。
|
||||||
Corpid: 企业 ID。
|
Corpid: 企业 ID。
|
||||||
logger: 日志记录器。
|
logger: 日志记录器。
|
||||||
unified_mode: 是否使用统一 webhook 模式(默认 False)。
|
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> client = WecomBotClient(Token='token', EnCodingAESKey='aeskey', Corpid='corp', logger=logger)
|
>>> client = WecomBotClient(Token='token', EnCodingAESKey='aeskey', Corpid='corp', logger=logger)
|
||||||
@@ -218,15 +217,10 @@ class WecomBotClient:
|
|||||||
self.EnCodingAESKey = EnCodingAESKey
|
self.EnCodingAESKey = EnCodingAESKey
|
||||||
self.Corpid = Corpid
|
self.Corpid = Corpid
|
||||||
self.ReceiveId = ''
|
self.ReceiveId = ''
|
||||||
self.unified_mode = unified_mode
|
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
|
self.app.add_url_rule(
|
||||||
# 只有在非统一模式下才注册独立路由
|
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['POST', 'GET']
|
||||||
if not self.unified_mode:
|
)
|
||||||
self.app.add_url_rule(
|
|
||||||
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['POST', 'GET']
|
|
||||||
)
|
|
||||||
|
|
||||||
self._message_handlers = {
|
self._message_handlers = {
|
||||||
'example': [],
|
'example': [],
|
||||||
}
|
}
|
||||||
@@ -365,7 +359,7 @@ class WecomBotClient:
|
|||||||
return await self._encrypt_and_reply(payload, nonce)
|
return await self._encrypt_and_reply(payload, nonce)
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
"""企业微信回调入口(独立端口模式,使用全局 request)。
|
"""企业微信回调入口。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Quart Response: 根据请求类型返回验证、首包或刷新结果。
|
Quart Response: 根据请求类型返回验证、首包或刷新结果。
|
||||||
@@ -373,34 +367,15 @@ class WecomBotClient:
|
|||||||
Example:
|
Example:
|
||||||
作为 Quart 路由处理函数直接注册并使用。
|
作为 Quart 路由处理函数直接注册并使用。
|
||||||
"""
|
"""
|
||||||
return await self._handle_callback_internal(request)
|
|
||||||
|
|
||||||
async def handle_unified_webhook(self, req):
|
|
||||||
"""处理回调请求(统一 webhook 模式,显式传递 request)。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
req: Quart Request 对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应数据
|
|
||||||
"""
|
|
||||||
return await self._handle_callback_internal(req)
|
|
||||||
|
|
||||||
async def _handle_callback_internal(self, req):
|
|
||||||
"""处理回调请求的内部实现,包括 GET 验证和 POST 消息接收。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
req: Quart Request 对象
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
self.wxcpt = WXBizMsgCrypt(self.Token, self.EnCodingAESKey, '')
|
self.wxcpt = WXBizMsgCrypt(self.Token, self.EnCodingAESKey, '')
|
||||||
await self.logger.info(f'{req.method} {req.url} {str(req.args)}')
|
await self.logger.info(f'{request.method} {request.url} {str(request.args)}')
|
||||||
|
|
||||||
if req.method == 'GET':
|
if request.method == 'GET':
|
||||||
return await self._handle_get_callback(req)
|
return await self._handle_get_callback()
|
||||||
|
|
||||||
if req.method == 'POST':
|
if request.method == 'POST':
|
||||||
return await self._handle_post_callback(req)
|
return await self._handle_post_callback()
|
||||||
|
|
||||||
return Response('', status=405)
|
return Response('', status=405)
|
||||||
|
|
||||||
@@ -408,13 +383,13 @@ class WecomBotClient:
|
|||||||
await self.logger.error(traceback.format_exc())
|
await self.logger.error(traceback.format_exc())
|
||||||
return Response('Internal Server Error', status=500)
|
return Response('Internal Server Error', status=500)
|
||||||
|
|
||||||
async def _handle_get_callback(self, req) -> tuple[Response, int] | Response:
|
async def _handle_get_callback(self) -> tuple[Response, int] | Response:
|
||||||
"""处理企业微信的 GET 验证请求。"""
|
"""处理企业微信的 GET 验证请求。"""
|
||||||
|
|
||||||
msg_signature = unquote(req.args.get('msg_signature', ''))
|
msg_signature = unquote(request.args.get('msg_signature', ''))
|
||||||
timestamp = unquote(req.args.get('timestamp', ''))
|
timestamp = unquote(request.args.get('timestamp', ''))
|
||||||
nonce = unquote(req.args.get('nonce', ''))
|
nonce = unquote(request.args.get('nonce', ''))
|
||||||
echostr = unquote(req.args.get('echostr', ''))
|
echostr = unquote(request.args.get('echostr', ''))
|
||||||
|
|
||||||
if not all([msg_signature, timestamp, nonce, echostr]):
|
if not all([msg_signature, timestamp, nonce, echostr]):
|
||||||
await self.logger.error('请求参数缺失')
|
await self.logger.error('请求参数缺失')
|
||||||
@@ -427,16 +402,16 @@ class WecomBotClient:
|
|||||||
|
|
||||||
return Response(decrypted_str, mimetype='text/plain')
|
return Response(decrypted_str, mimetype='text/plain')
|
||||||
|
|
||||||
async def _handle_post_callback(self, req) -> tuple[Response, int] | Response:
|
async def _handle_post_callback(self) -> tuple[Response, int] | Response:
|
||||||
"""处理企业微信的 POST 回调请求。"""
|
"""处理企业微信的 POST 回调请求。"""
|
||||||
|
|
||||||
self.stream_sessions.cleanup()
|
self.stream_sessions.cleanup()
|
||||||
|
|
||||||
msg_signature = unquote(req.args.get('msg_signature', ''))
|
msg_signature = unquote(request.args.get('msg_signature', ''))
|
||||||
timestamp = unquote(req.args.get('timestamp', ''))
|
timestamp = unquote(request.args.get('timestamp', ''))
|
||||||
nonce = unquote(req.args.get('nonce', ''))
|
nonce = unquote(request.args.get('nonce', ''))
|
||||||
|
|
||||||
encrypted_json = await req.get_json()
|
encrypted_json = await request.get_json()
|
||||||
encrypted_msg = (encrypted_json or {}).get('encrypt', '')
|
encrypted_msg = (encrypted_json or {}).get('encrypt', '')
|
||||||
if not encrypted_msg:
|
if not encrypted_msg:
|
||||||
await self.logger.error("请求体中缺少 'encrypt' 字段")
|
await self.logger.error("请求体中缺少 'encrypt' 字段")
|
||||||
|
|||||||
@@ -29,7 +29,12 @@ class WecomBotEvent(dict):
|
|||||||
"""
|
"""
|
||||||
用户名称
|
用户名称
|
||||||
"""
|
"""
|
||||||
return self.get('username', '') or self.get('from', {}).get('alias', '') or self.get('from', {}).get('name', '') or self.userid
|
return (
|
||||||
|
self.get('username', '')
|
||||||
|
or self.get('from', {}).get('alias', '')
|
||||||
|
or self.get('from', {}).get('name', '')
|
||||||
|
or self.userid
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def chatname(self) -> str:
|
def chatname(self) -> str:
|
||||||
@@ -65,7 +70,7 @@ class WecomBotEvent(dict):
|
|||||||
消息id
|
消息id
|
||||||
"""
|
"""
|
||||||
return self.get('msgid', '')
|
return self.get('msgid', '')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ai_bot_id(self) -> str:
|
def ai_bot_id(self) -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ class WecomClient:
|
|||||||
EncodingAESKey: str,
|
EncodingAESKey: str,
|
||||||
contacts_secret: str,
|
contacts_secret: str,
|
||||||
logger: None,
|
logger: None,
|
||||||
unified_mode: bool = False,
|
|
||||||
):
|
):
|
||||||
self.corpid = corpid
|
self.corpid = corpid
|
||||||
self.secret = secret
|
self.secret = secret
|
||||||
@@ -32,18 +31,13 @@ class WecomClient:
|
|||||||
self.access_token = ''
|
self.access_token = ''
|
||||||
self.secret_for_contacts = contacts_secret
|
self.secret_for_contacts = contacts_secret
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.unified_mode = unified_mode
|
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
|
self.app.add_url_rule(
|
||||||
# 只有在非统一模式下才注册独立路由
|
'/callback/command',
|
||||||
if not self.unified_mode:
|
'handle_callback',
|
||||||
self.app.add_url_rule(
|
self.handle_callback_request,
|
||||||
'/callback/command',
|
methods=['GET', 'POST'],
|
||||||
'handle_callback',
|
)
|
||||||
self.handle_callback_request,
|
|
||||||
methods=['GET', 'POST'],
|
|
||||||
)
|
|
||||||
|
|
||||||
self._message_handlers = {
|
self._message_handlers = {
|
||||||
'example': [],
|
'example': [],
|
||||||
}
|
}
|
||||||
@@ -167,43 +161,25 @@ class WecomClient:
|
|||||||
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):
|
||||||
"""处理回调请求(独立端口模式,使用全局 request)。"""
|
|
||||||
return await self._handle_callback_internal(request)
|
|
||||||
|
|
||||||
async def handle_unified_webhook(self, req):
|
|
||||||
"""处理回调请求(统一 webhook 模式,显式传递 request)。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
req: Quart Request 对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应数据
|
|
||||||
"""
|
"""
|
||||||
return await self._handle_callback_internal(req)
|
处理回调请求,包括 GET 验证和 POST 消息接收。
|
||||||
|
|
||||||
async def _handle_callback_internal(self, req):
|
|
||||||
"""
|
|
||||||
处理回调请求的内部实现,包括 GET 验证和 POST 消息接收。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
req: Quart Request 对象
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
msg_signature = req.args.get('msg_signature')
|
msg_signature = request.args.get('msg_signature')
|
||||||
timestamp = req.args.get('timestamp')
|
timestamp = request.args.get('timestamp')
|
||||||
nonce = req.args.get('nonce')
|
nonce = request.args.get('nonce')
|
||||||
|
|
||||||
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid)
|
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid)
|
||||||
if req.method == 'GET':
|
if request.method == 'GET':
|
||||||
echostr = req.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('验证失败')
|
await self.logger.error('验证失败')
|
||||||
raise Exception(f'验证失败,错误码: {ret}')
|
raise Exception(f'验证失败,错误码: {ret}')
|
||||||
return reply_echo_str
|
return reply_echo_str
|
||||||
|
|
||||||
elif req.method == 'POST':
|
elif request.method == 'POST':
|
||||||
encrypt_msg = await req.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('消息解密失败')
|
await self.logger.error('消息解密失败')
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import aiofiles
|
|||||||
|
|
||||||
|
|
||||||
class WecomCSClient:
|
class WecomCSClient:
|
||||||
def __init__(self, corpid: str, secret: str, token: str, EncodingAESKey: str, logger: None, unified_mode: bool = False):
|
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 = ''
|
||||||
@@ -22,15 +22,10 @@ class WecomCSClient:
|
|||||||
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.logger = logger
|
||||||
self.unified_mode = unified_mode
|
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
|
self.app.add_url_rule(
|
||||||
# 只有在非统一模式下才注册独立路由
|
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
|
||||||
if not self.unified_mode:
|
)
|
||||||
self.app.add_url_rule(
|
|
||||||
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
|
|
||||||
)
|
|
||||||
|
|
||||||
self._message_handlers = {
|
self._message_handlers = {
|
||||||
'example': [],
|
'example': [],
|
||||||
}
|
}
|
||||||
@@ -197,45 +192,27 @@ class WecomCSClient:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
"""处理回调请求(独立端口模式,使用全局 request)。"""
|
|
||||||
return await self._handle_callback_internal(request)
|
|
||||||
|
|
||||||
async def handle_unified_webhook(self, req):
|
|
||||||
"""处理回调请求(统一 webhook 模式,显式传递 request)。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
req: Quart Request 对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应数据
|
|
||||||
"""
|
"""
|
||||||
return await self._handle_callback_internal(req)
|
处理回调请求,包括 GET 验证和 POST 消息接收。
|
||||||
|
|
||||||
async def _handle_callback_internal(self, req):
|
|
||||||
"""
|
|
||||||
处理回调请求的内部实现,包括 GET 验证和 POST 消息接收。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
req: Quart Request 对象
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
msg_signature = req.args.get('msg_signature')
|
msg_signature = request.args.get('msg_signature')
|
||||||
timestamp = req.args.get('timestamp')
|
timestamp = request.args.get('timestamp')
|
||||||
nonce = req.args.get('nonce')
|
nonce = request.args.get('nonce')
|
||||||
try:
|
try:
|
||||||
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid)
|
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f'初始化失败,错误码: {e}')
|
raise Exception(f'初始化失败,错误码: {e}')
|
||||||
|
|
||||||
if req.method == 'GET':
|
if request.method == 'GET':
|
||||||
echostr = req.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:
|
||||||
raise Exception(f'验证失败,错误码: {ret}')
|
raise Exception(f'验证失败,错误码: {ret}')
|
||||||
return reply_echo_str
|
return reply_echo_str
|
||||||
|
|
||||||
elif req.method == 'POST':
|
elif request.method == 'POST':
|
||||||
encrypt_msg = await req.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:
|
||||||
raise Exception(f'消息解密失败,错误码: {ret}')
|
raise Exception(f'消息解密失败,错误码: {ret}')
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ class BotsRouterGroup(group.RouterGroup):
|
|||||||
@self.route('/<bot_uuid>', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
@self.route('/<bot_uuid>', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
||||||
async def _(bot_uuid: str) -> str:
|
async def _(bot_uuid: str) -> str:
|
||||||
if quart.request.method == 'GET':
|
if quart.request.method == 'GET':
|
||||||
# 返回运行时信息,包括webhook地址等
|
bot = await self.ap.bot_service.get_bot(bot_uuid)
|
||||||
bot = await self.ap.bot_service.get_runtime_bot_info(bot_uuid)
|
|
||||||
if bot is None:
|
if bot is None:
|
||||||
return self.http_status(404, -1, 'bot not found')
|
return self.http_status(404, -1, 'bot not found')
|
||||||
return self.success(data={'bot': bot})
|
return self.success(data={'bot': bot})
|
||||||
|
|||||||
@@ -21,22 +21,6 @@ class PluginsRouterGroup(group.RouterGroup):
|
|||||||
|
|
||||||
return self.success(data={'plugins': plugins})
|
return self.success(data={'plugins': plugins})
|
||||||
|
|
||||||
@self.route('/debug-info', methods=['GET'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
|
||||||
async def _() -> str:
|
|
||||||
"""Get plugin debug information including debug URL and key"""
|
|
||||||
debug_info = await self.ap.plugin_connector.get_debug_info()
|
|
||||||
|
|
||||||
# Get debug URL from config
|
|
||||||
plugin_config = self.ap.instance_config.data.get('plugin', {})
|
|
||||||
debug_url = plugin_config.get('display_plugin_debug_url', 'http://localhost:5401')
|
|
||||||
|
|
||||||
return self.success(
|
|
||||||
data={
|
|
||||||
'debug_url': debug_url,
|
|
||||||
'plugin_debug_key': debug_info.get('plugin_debug_key', ''),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.route(
|
@self.route(
|
||||||
'/<author>/<plugin_name>/upgrade',
|
'/<author>/<plugin_name>/upgrade',
|
||||||
methods=['POST'],
|
methods=['POST'],
|
||||||
|
|||||||
@@ -1,57 +1,49 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import quart
|
import quart
|
||||||
import traceback
|
|
||||||
|
|
||||||
from .. import group
|
from .. import group
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('webhooks', '/bots')
|
@group.group_class('webhooks', '/api/v1/webhooks')
|
||||||
class WebhookRouterGroup(group.RouterGroup):
|
class WebhooksRouterGroup(group.RouterGroup):
|
||||||
async def initialize(self) -> None:
|
async def initialize(self) -> None:
|
||||||
@self.route('/<bot_uuid>', methods=['GET', 'POST'], auth_type=group.AuthType.NONE)
|
@self.route('', methods=['GET', 'POST'])
|
||||||
async def handle_webhook(bot_uuid: str):
|
async def _() -> str:
|
||||||
"""处理 bot webhook 回调(无子路径)"""
|
if quart.request.method == 'GET':
|
||||||
return await self._dispatch_webhook(bot_uuid, '')
|
webhooks = await self.ap.webhook_service.get_webhooks()
|
||||||
|
return self.success(data={'webhooks': webhooks})
|
||||||
|
elif quart.request.method == 'POST':
|
||||||
|
json_data = await quart.request.json
|
||||||
|
name = json_data.get('name', '')
|
||||||
|
url = json_data.get('url', '')
|
||||||
|
description = json_data.get('description', '')
|
||||||
|
enabled = json_data.get('enabled', True)
|
||||||
|
|
||||||
@self.route('/<bot_uuid>/<path:path>', methods=['GET', 'POST'], auth_type=group.AuthType.NONE)
|
if not name:
|
||||||
async def handle_webhook_with_path(bot_uuid: str, path: str):
|
return self.http_status(400, -1, 'Name is required')
|
||||||
"""处理 bot webhook 回调(带子路径)"""
|
if not url:
|
||||||
return await self._dispatch_webhook(bot_uuid, path)
|
return self.http_status(400, -1, 'URL is required')
|
||||||
|
|
||||||
async def _dispatch_webhook(self, bot_uuid: str, path: str):
|
webhook = await self.ap.webhook_service.create_webhook(name, url, description, enabled)
|
||||||
"""分发 webhook 请求到对应的 bot adapter
|
return self.success(data={'webhook': webhook})
|
||||||
|
|
||||||
Args:
|
@self.route('/<int:webhook_id>', methods=['GET', 'PUT', 'DELETE'])
|
||||||
bot_uuid: Bot 的 UUID
|
async def _(webhook_id: int) -> str:
|
||||||
path: 子路径(如果有的话)
|
if quart.request.method == 'GET':
|
||||||
|
webhook = await self.ap.webhook_service.get_webhook(webhook_id)
|
||||||
|
if webhook is None:
|
||||||
|
return self.http_status(404, -1, 'Webhook not found')
|
||||||
|
return self.success(data={'webhook': webhook})
|
||||||
|
|
||||||
Returns:
|
elif quart.request.method == 'PUT':
|
||||||
适配器返回的响应
|
json_data = await quart.request.json
|
||||||
"""
|
name = json_data.get('name')
|
||||||
try:
|
url = json_data.get('url')
|
||||||
|
description = json_data.get('description')
|
||||||
runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
|
enabled = json_data.get('enabled')
|
||||||
|
|
||||||
if not runtime_bot:
|
await self.ap.webhook_service.update_webhook(webhook_id, name, url, description, enabled)
|
||||||
return quart.jsonify({'error': 'Bot not found'}), 404
|
return self.success()
|
||||||
|
|
||||||
if not runtime_bot.enable:
|
elif quart.request.method == 'DELETE':
|
||||||
return quart.jsonify({'error': 'Bot is disabled'}), 403
|
await self.ap.webhook_service.delete_webhook(webhook_id)
|
||||||
|
return self.success()
|
||||||
|
|
||||||
if not hasattr(runtime_bot.adapter, 'handle_unified_webhook'):
|
|
||||||
return quart.jsonify({'error': 'Adapter does not support unified webhook'}), 501
|
|
||||||
|
|
||||||
|
|
||||||
response = await runtime_bot.adapter.handle_unified_webhook(
|
|
||||||
bot_uuid=bot_uuid,
|
|
||||||
path=path,
|
|
||||||
request=quart.request,
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.ap.logger.error(f'Webhook dispatch error for bot {bot_uuid}: {traceback.format_exc()}')
|
|
||||||
return quart.jsonify({'error': str(e)}), 500
|
|
||||||
|
|||||||
@@ -58,16 +58,6 @@ class BotService:
|
|||||||
if runtime_bot is not None:
|
if runtime_bot is not None:
|
||||||
adapter_runtime_values['bot_account_id'] = runtime_bot.adapter.bot_account_id
|
adapter_runtime_values['bot_account_id'] = runtime_bot.adapter.bot_account_id
|
||||||
|
|
||||||
# Webhook URL for unified webhook adapters (independent of bot running state)
|
|
||||||
if persistence_bot['adapter'] in ['wecom', 'wecombot', 'officialaccount', 'qqofficial', 'slack', 'wecomcs', 'LINE']:
|
|
||||||
webhook_prefix = self.ap.instance_config.data['api'].get('webhook_prefix', 'http://127.0.0.1:5300')
|
|
||||||
webhook_url = f'/bots/{bot_uuid}'
|
|
||||||
adapter_runtime_values['webhook_url'] = webhook_url
|
|
||||||
adapter_runtime_values['webhook_full_url'] = f'{webhook_prefix}{webhook_url}'
|
|
||||||
else:
|
|
||||||
adapter_runtime_values['webhook_url'] = None
|
|
||||||
adapter_runtime_values['webhook_full_url'] = None
|
|
||||||
|
|
||||||
persistence_bot['adapter_runtime_values'] = adapter_runtime_values
|
persistence_bot['adapter_runtime_values'] = adapter_runtime_values
|
||||||
|
|
||||||
return persistence_bot
|
return persistence_bot
|
||||||
|
|||||||
@@ -74,16 +74,7 @@ class KnowledgeService:
|
|||||||
# Only internal KBs support file storage
|
# Only internal KBs support file storage
|
||||||
if runtime_kb.get_type() != 'internal':
|
if runtime_kb.get_type() != 'internal':
|
||||||
raise Exception('Only internal knowledge bases support file storage')
|
raise Exception('Only internal knowledge bases support file storage')
|
||||||
result = await runtime_kb.store_file(file_id)
|
return await runtime_kb.store_file(file_id)
|
||||||
|
|
||||||
# Update the KB's updated_at timestamp
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_rag.KnowledgeBase)
|
|
||||||
.values(updated_at=sqlalchemy.func.now())
|
|
||||||
.where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def retrieve_knowledge_base(self, kb_uuid: str, query: str) -> list[dict]:
|
async def retrieve_knowledge_base(self, kb_uuid: str, query: str) -> list[dict]:
|
||||||
"""检索知识库"""
|
"""检索知识库"""
|
||||||
@@ -112,13 +103,6 @@ class KnowledgeService:
|
|||||||
raise Exception('Only internal knowledge bases support file deletion')
|
raise Exception('Only internal knowledge bases support file deletion')
|
||||||
await runtime_kb.delete_file(file_id)
|
await runtime_kb.delete_file(file_id)
|
||||||
|
|
||||||
# Update the KB's updated_at timestamp
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_rag.KnowledgeBase)
|
|
||||||
.values(updated_at=sqlalchemy.func.now())
|
|
||||||
.where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def delete_knowledge_base(self, kb_uuid: str) -> None:
|
async def delete_knowledge_base(self, kb_uuid: str) -> None:
|
||||||
"""删除知识库"""
|
"""删除知识库"""
|
||||||
await self.ap.rag_mgr.delete_knowledge_base(kb_uuid)
|
await self.ap.rag_mgr.delete_knowledge_base(kb_uuid)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ class KnowledgeBase(Base):
|
|||||||
name = sqlalchemy.Column(sqlalchemy.String, index=True)
|
name = sqlalchemy.Column(sqlalchemy.String, index=True)
|
||||||
description = sqlalchemy.Column(sqlalchemy.Text)
|
description = sqlalchemy.Column(sqlalchemy.Text)
|
||||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, default=sqlalchemy.func.now())
|
created_at = sqlalchemy.Column(sqlalchemy.DateTime, default=sqlalchemy.func.now())
|
||||||
updated_at = sqlalchemy.Column(sqlalchemy.DateTime, default=sqlalchemy.func.now(), onupdate=sqlalchemy.func.now())
|
|
||||||
embedding_model_uuid = sqlalchemy.Column(sqlalchemy.String, default='')
|
embedding_model_uuid = sqlalchemy.Column(sqlalchemy.String, default='')
|
||||||
top_k = sqlalchemy.Column(sqlalchemy.Integer, default=5)
|
top_k = sqlalchemy.Column(sqlalchemy.Integer, default=5)
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import sqlalchemy
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class(13)
|
|
||||||
class DBMigrateKnowledgeBaseUpdatedAt(migration.DBMigration):
|
|
||||||
"""Add updated_at field to knowledge_bases table"""
|
|
||||||
|
|
||||||
async def upgrade(self):
|
|
||||||
"""Upgrade"""
|
|
||||||
# Get all column names from the table
|
|
||||||
columns = []
|
|
||||||
|
|
||||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.text(
|
|
||||||
"SELECT column_name FROM information_schema.columns WHERE table_name = 'knowledge_bases';"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
all_result = result.fetchall()
|
|
||||||
columns = [row[0] for row in all_result]
|
|
||||||
else:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text('PRAGMA table_info(knowledge_bases);'))
|
|
||||||
all_result = result.fetchall()
|
|
||||||
columns = [row[1] for row in all_result]
|
|
||||||
|
|
||||||
# Check and add updated_at column
|
|
||||||
if 'updated_at' not in columns:
|
|
||||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.text(
|
|
||||||
'ALTER TABLE knowledge_bases ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# SQLite doesn't support DEFAULT CURRENT_TIMESTAMP in ALTER TABLE
|
|
||||||
# Add column without default first
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.text('ALTER TABLE knowledge_bases ADD COLUMN updated_at DATETIME')
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set initial updated_at values to created_at for existing records
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.text('UPDATE knowledge_bases SET updated_at = created_at WHERE updated_at IS NULL')
|
|
||||||
)
|
|
||||||
|
|
||||||
async def downgrade(self):
|
|
||||||
"""Downgrade"""
|
|
||||||
pass
|
|
||||||
@@ -6,6 +6,7 @@ from .. import stage, entities
|
|||||||
from langbot_plugin.api.entities.builtin.provider import message as provider_message
|
from langbot_plugin.api.entities.builtin.provider import message as provider_message
|
||||||
import langbot_plugin.api.entities.events as events
|
import langbot_plugin.api.entities.events as events
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
||||||
|
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
||||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
||||||
|
|
||||||
|
|
||||||
@@ -74,12 +75,19 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
self.ap.logger.debug(f'Bound MCP servers: {bound_mcp_servers}')
|
self.ap.logger.debug(f'Bound MCP servers: {bound_mcp_servers}')
|
||||||
self.ap.logger.debug(f'Use funcs: {query.use_funcs}')
|
self.ap.logger.debug(f'Use funcs: {query.use_funcs}')
|
||||||
|
|
||||||
|
# Extract sender name from message event
|
||||||
|
sender_name = ''
|
||||||
|
if isinstance(query.message_event, (platform_events.FriendMessage, platform_events.GroupMessage)):
|
||||||
|
sender_name = query.message_event.sender.get_name()
|
||||||
|
|
||||||
variables = {
|
variables = {
|
||||||
'session_id': f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
'session_id': f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
||||||
'conversation_id': conversation.uuid,
|
'conversation_id': conversation.uuid,
|
||||||
'msg_create_time': (
|
'msg_create_time': (
|
||||||
int(query.message_event.time) if query.message_event.time else int(datetime.datetime.now().timestamp())
|
int(query.message_event.time) if query.message_event.time else int(datetime.datetime.now().timestamp())
|
||||||
),
|
),
|
||||||
|
'sender_id': str(query.sender_id),
|
||||||
|
'sender_name': sender_name,
|
||||||
}
|
}
|
||||||
query.variables.update(variables)
|
query.variables.update(variables)
|
||||||
|
|
||||||
|
|||||||
@@ -245,10 +245,6 @@ class PlatformManager:
|
|||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 如果 adapter 支持 set_bot_uuid 方法,设置 bot_uuid(用于统一 webhook)
|
|
||||||
if hasattr(adapter_inst, 'set_bot_uuid'):
|
|
||||||
adapter_inst.set_bot_uuid(bot_entity.uuid)
|
|
||||||
|
|
||||||
runtime_bot = RuntimeBot(ap=self.ap, bot_entity=bot_entity, adapter=adapter_inst, logger=logger)
|
runtime_bot = RuntimeBot(ap=self.ap, bot_entity=bot_entity, adapter=adapter_inst, logger=logger)
|
||||||
|
|
||||||
await runtime_bot.initialize()
|
await runtime_bot.initialize()
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ class LINEEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
|||||||
class LINEAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
class LINEAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||||
bot: MessagingApi
|
bot: MessagingApi
|
||||||
api_client: ApiClient
|
api_client: ApiClient
|
||||||
parser: WebhookParser
|
|
||||||
|
|
||||||
bot_account_id: str # 用于在流水线中识别at是否是本bot,直接以bot_name作为标识
|
bot_account_id: str # 用于在流水线中识别at是否是本bot,直接以bot_name作为标识
|
||||||
message_converter: LINEMessageConverter
|
message_converter: LINEMessageConverter
|
||||||
@@ -133,7 +132,7 @@ class LINEAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
]
|
]
|
||||||
|
|
||||||
config: dict
|
config: dict
|
||||||
bot_uuid: str = None
|
quart_app: quart.Quart
|
||||||
|
|
||||||
card_id_dict: dict[str, str] # 消息id到卡片id的映射,便于创建卡片后的发送消息到指定卡片
|
card_id_dict: dict[str, str] # 消息id到卡片id的映射,便于创建卡片后的发送消息到指定卡片
|
||||||
|
|
||||||
@@ -150,6 +149,7 @@ class LINEAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
config=config,
|
config=config,
|
||||||
logger=logger,
|
logger=logger,
|
||||||
|
quart_app=quart.Quart(__name__),
|
||||||
listeners={},
|
listeners={},
|
||||||
card_id_dict={},
|
card_id_dict={},
|
||||||
seq=1,
|
seq=1,
|
||||||
@@ -163,6 +163,29 @@ class LINEAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
bot_account_id=bot_account_id,
|
bot_account_id=bot_account_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@self.quart_app.route('/line/callback', methods=['POST'])
|
||||||
|
async def line_callback():
|
||||||
|
try:
|
||||||
|
signature = quart.request.headers.get('X-Line-Signature')
|
||||||
|
body = await quart.request.get_data(as_text=True)
|
||||||
|
events = parser.parse(body, signature) # 解密解析消息
|
||||||
|
|
||||||
|
try:
|
||||||
|
# print(events)
|
||||||
|
lb_event = await self.event_converter.target2yiri(events[0], self.api_client)
|
||||||
|
if lb_event.__class__ in self.listeners:
|
||||||
|
await self.listeners[lb_event.__class__](lb_event, self)
|
||||||
|
except InvalidSignatureError:
|
||||||
|
self.logger.info(
|
||||||
|
f'Invalid signature. Please check your channel access token/channel secret.{traceback.format_exc()}'
|
||||||
|
)
|
||||||
|
return quart.Response('Invalid signature', status=400)
|
||||||
|
|
||||||
|
return {'code': 200, 'message': 'ok'}
|
||||||
|
except Exception:
|
||||||
|
await self.logger.error(f'Error in LINE callback: {traceback.format_exc()}')
|
||||||
|
return {'code': 500, 'message': 'error'}
|
||||||
|
|
||||||
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):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -213,73 +236,18 @@ class LINEAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
):
|
):
|
||||||
self.listeners.pop(event_type)
|
self.listeners.pop(event_type)
|
||||||
|
|
||||||
def set_bot_uuid(self, bot_uuid: str):
|
|
||||||
"""设置 bot UUID(用于生成 webhook URL)"""
|
|
||||||
self.bot_uuid = bot_uuid
|
|
||||||
|
|
||||||
async def handle_unified_webhook(self, bot_uuid: str, path: str, request):
|
|
||||||
"""处理统一 webhook 请求。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
bot_uuid: Bot 的 UUID
|
|
||||||
path: 子路径(如果有的话)
|
|
||||||
request: Quart Request 对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应数据
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
signature = request.headers.get('X-Line-Signature')
|
|
||||||
body = await request.get_data(as_text=True)
|
|
||||||
|
|
||||||
# Check if signature header exists
|
|
||||||
if not signature:
|
|
||||||
await self.logger.warning('Missing X-Line-Signature header')
|
|
||||||
return quart.Response('Missing X-Line-Signature header', status=400)
|
|
||||||
|
|
||||||
try:
|
|
||||||
events = self.parser.parse(body, signature) # 解密解析消息
|
|
||||||
except InvalidSignatureError:
|
|
||||||
await self.logger.info(
|
|
||||||
f'Invalid signature. Please check your channel access token/channel secret.{traceback.format_exc()}'
|
|
||||||
)
|
|
||||||
return quart.Response('Invalid signature', status=400)
|
|
||||||
|
|
||||||
# 处理事件
|
|
||||||
if events and len(events) > 0:
|
|
||||||
lb_event = await self.event_converter.target2yiri(events[0], self.api_client)
|
|
||||||
if lb_event.__class__ in self.listeners:
|
|
||||||
await self.listeners[lb_event.__class__](lb_event, self)
|
|
||||||
|
|
||||||
return {'code': 200, 'message': 'ok'}
|
|
||||||
except Exception:
|
|
||||||
await self.logger.error(f'Error in LINE callback: {traceback.format_exc()}')
|
|
||||||
print(traceback.format_exc())
|
|
||||||
return {'code': 500, 'message': 'error'}
|
|
||||||
|
|
||||||
async def run_async(self):
|
async def run_async(self):
|
||||||
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
port = self.config['port']
|
||||||
# 保持运行但不启动独立端口
|
|
||||||
|
|
||||||
# 打印 webhook 回调地址
|
async def shutdown_trigger_placeholder():
|
||||||
if self.bot_uuid and hasattr(self.logger, 'ap'):
|
|
||||||
try:
|
|
||||||
api_port = self.logger.ap.instance_config.data['api']['port']
|
|
||||||
webhook_url = f'http://127.0.0.1:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
webhook_url_public = f'http://<Your-Public-IP>:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
|
|
||||||
await self.logger.info('LINE Webhook 回调地址:')
|
|
||||||
await self.logger.info(f' 本地地址: {webhook_url}')
|
|
||||||
await self.logger.info(f' 公网地址: {webhook_url_public}')
|
|
||||||
await self.logger.info('请在 LINE 后台配置此回调地址')
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.warning(f'无法生成 webhook URL: {e}')
|
|
||||||
|
|
||||||
async def keep_alive():
|
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
await keep_alive()
|
await self.quart_app.run_task(
|
||||||
|
host='0.0.0.0',
|
||||||
|
port=port,
|
||||||
|
shutdown_trigger=shutdown_trigger_placeholder,
|
||||||
|
)
|
||||||
|
|
||||||
async def kill(self) -> bool:
|
async def kill(self) -> bool:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -22,6 +22,18 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
default: ""
|
default: ""
|
||||||
|
- name: port
|
||||||
|
label:
|
||||||
|
en_US: Webhook Port
|
||||||
|
zh_Hans: Webhook端口
|
||||||
|
description:
|
||||||
|
en_US: Only valid when webhook mode is enabled, please fill in the webhook port
|
||||||
|
zh_Hans: 请填写 Webhook 端口
|
||||||
|
ja_JP: Webhookポートを入力してください
|
||||||
|
zh_Hant: 請填寫 Webhook 端口
|
||||||
|
type: integer
|
||||||
|
required: true
|
||||||
|
default: 2287
|
||||||
- name: channel_secret
|
- name: channel_secret
|
||||||
label:
|
label:
|
||||||
en_US: Channel secret
|
en_US: Channel secret
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from langbot.libs.official_account_api.api import OAClientForLongerResponse
|
|||||||
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
||||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
||||||
from ..logger import EventLogger
|
from langbot.pkg.platform.logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class OAMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
class OAMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
||||||
@@ -58,16 +58,13 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd
|
|||||||
message_converter: OAMessageConverter = OAMessageConverter()
|
message_converter: OAMessageConverter = OAMessageConverter()
|
||||||
event_converter: OAEventConverter = OAEventConverter()
|
event_converter: OAEventConverter = OAEventConverter()
|
||||||
bot: typing.Union[OAClient, OAClientForLongerResponse] = pydantic.Field(exclude=True)
|
bot: typing.Union[OAClient, OAClientForLongerResponse] = pydantic.Field(exclude=True)
|
||||||
bot_uuid: str = None
|
|
||||||
|
|
||||||
def __init__(self, config: dict, logger: EventLogger):
|
def __init__(self, config: dict, logger: EventLogger):
|
||||||
# 校验必填项
|
|
||||||
required_keys = ['token', 'EncodingAESKey', 'AppSecret', 'AppID', 'Mode']
|
required_keys = ['token', 'EncodingAESKey', 'AppSecret', 'AppID', 'Mode']
|
||||||
missing_keys = [k for k in required_keys if k not in config]
|
missing_keys = [k for k in required_keys if k not in config]
|
||||||
if missing_keys:
|
if missing_keys:
|
||||||
raise Exception(f'OfficialAccount 缺少配置项: {missing_keys}')
|
raise Exception(f'OfficialAccount 缺少配置项: {missing_keys}')
|
||||||
|
|
||||||
# 创建运行时 bot 对象,始终使用统一 webhook 模式
|
|
||||||
if config['Mode'] == 'drop':
|
if config['Mode'] == 'drop':
|
||||||
bot = OAClient(
|
bot = OAClient(
|
||||||
token=config['token'],
|
token=config['token'],
|
||||||
@@ -75,7 +72,6 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd
|
|||||||
Appsecret=config['AppSecret'],
|
Appsecret=config['AppSecret'],
|
||||||
AppID=config['AppID'],
|
AppID=config['AppID'],
|
||||||
logger=logger,
|
logger=logger,
|
||||||
unified_mode=True,
|
|
||||||
)
|
)
|
||||||
elif config['Mode'] == 'passive':
|
elif config['Mode'] == 'passive':
|
||||||
bot = OAClientForLongerResponse(
|
bot = OAClientForLongerResponse(
|
||||||
@@ -85,7 +81,6 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd
|
|||||||
AppID=config['AppID'],
|
AppID=config['AppID'],
|
||||||
LoadingMessage=config.get('LoadingMessage', ''),
|
LoadingMessage=config.get('LoadingMessage', ''),
|
||||||
logger=logger,
|
logger=logger,
|
||||||
unified_mode=True,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise KeyError('请设置微信公众号通信模式')
|
raise KeyError('请设置微信公众号通信模式')
|
||||||
@@ -134,46 +129,16 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd
|
|||||||
elif event_type == platform_events.GroupMessage:
|
elif event_type == platform_events.GroupMessage:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def set_bot_uuid(self, bot_uuid: str):
|
|
||||||
"""设置 bot UUID(用于生成 webhook URL)"""
|
|
||||||
self.bot_uuid = bot_uuid
|
|
||||||
|
|
||||||
async def handle_unified_webhook(self, bot_uuid: str, path: str, request):
|
|
||||||
"""处理统一 webhook 请求。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
bot_uuid: Bot 的 UUID
|
|
||||||
path: 子路径(如果有的话)
|
|
||||||
request: Quart Request 对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应数据
|
|
||||||
"""
|
|
||||||
return await self.bot.handle_unified_webhook(request)
|
|
||||||
|
|
||||||
async def run_async(self):
|
async def run_async(self):
|
||||||
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
async def shutdown_trigger_placeholder():
|
||||||
# 保持运行但不启动独立端口
|
|
||||||
|
|
||||||
# 打印 webhook 回调地址
|
|
||||||
if self.bot_uuid and hasattr(self.logger, 'ap'):
|
|
||||||
try:
|
|
||||||
api_port = self.logger.ap.instance_config.data['api']['port']
|
|
||||||
webhook_url = f'http://127.0.0.1:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
webhook_url_public = f'http://<Your-Public-IP>:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
|
|
||||||
await self.logger.info('微信公众号 Webhook 回调地址:')
|
|
||||||
await self.logger.info(f' 本地地址: {webhook_url}')
|
|
||||||
await self.logger.info(f' 公网地址: {webhook_url_public}')
|
|
||||||
await self.logger.info('请在微信公众号后台配置此回调地址')
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.warning(f'无法生成 webhook URL: {e}')
|
|
||||||
|
|
||||||
async def keep_alive():
|
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
await keep_alive()
|
await self.bot.run_task(
|
||||||
|
host=self.config['host'],
|
||||||
|
port=self.config['port'],
|
||||||
|
shutdown_trigger=shutdown_trigger_placeholder,
|
||||||
|
)
|
||||||
|
|
||||||
async def kill(self) -> bool:
|
async def kill(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -53,6 +53,23 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
default: "AI正在思考中,请发送任意内容获取回复。"
|
default: "AI正在思考中,请发送任意内容获取回复。"
|
||||||
|
- name: host
|
||||||
|
label:
|
||||||
|
en_US: Host
|
||||||
|
zh_Hans: 监听主机
|
||||||
|
description:
|
||||||
|
en_US: The host that Official Account listens on for Webhook connections.
|
||||||
|
zh_Hans: 微信公众号监听的主机,除非你知道自己在做什么,否则请写 0.0.0.0
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
default: 0.0.0.0
|
||||||
|
- name: port
|
||||||
|
label:
|
||||||
|
en_US: Port
|
||||||
|
zh_Hans: 监听端口
|
||||||
|
type: integer
|
||||||
|
required: true
|
||||||
|
default: 2287
|
||||||
execution:
|
execution:
|
||||||
python:
|
python:
|
||||||
path: ./officialaccount.py
|
path: ./officialaccount.py
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
|||||||
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
||||||
from langbot.libs.qq_official_api.api import QQOfficialClient
|
from langbot.libs.qq_official_api.api import QQOfficialClient
|
||||||
from langbot.libs.qq_official_api.qqofficialevent import QQOfficialEvent
|
from langbot.libs.qq_official_api.qqofficialevent import QQOfficialEvent
|
||||||
from ...utils import image
|
from langbot.pkg.utils import image
|
||||||
from ..logger import EventLogger
|
from langbot.pkg.platform.logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class QQOfficialMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
class QQOfficialMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
||||||
@@ -134,14 +134,11 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter
|
|||||||
bot: QQOfficialClient
|
bot: QQOfficialClient
|
||||||
config: dict
|
config: dict
|
||||||
bot_account_id: str
|
bot_account_id: str
|
||||||
bot_uuid: str = None
|
|
||||||
message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter()
|
message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter()
|
||||||
event_converter: QQOfficialEventConverter = QQOfficialEventConverter()
|
event_converter: QQOfficialEventConverter = QQOfficialEventConverter()
|
||||||
|
|
||||||
def __init__(self, config: dict, logger: EventLogger):
|
def __init__(self, config: dict, logger: EventLogger):
|
||||||
bot = QQOfficialClient(
|
bot = QQOfficialClient(app_id=config['appid'], secret=config['secret'], token=config['token'], logger=logger)
|
||||||
app_id=config['appid'], secret=config['secret'], token=config['token'], logger=logger, unified_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
config=config,
|
config=config,
|
||||||
@@ -226,46 +223,16 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter
|
|||||||
self.bot.on_message('GROUP_AT_MESSAGE_CREATE')(on_message)
|
self.bot.on_message('GROUP_AT_MESSAGE_CREATE')(on_message)
|
||||||
self.bot.on_message('AT_MESSAGE_CREATE')(on_message)
|
self.bot.on_message('AT_MESSAGE_CREATE')(on_message)
|
||||||
|
|
||||||
def set_bot_uuid(self, bot_uuid: str):
|
|
||||||
"""设置 bot UUID(用于生成 webhook URL)"""
|
|
||||||
self.bot_uuid = bot_uuid
|
|
||||||
|
|
||||||
async def handle_unified_webhook(self, bot_uuid: str, path: str, request):
|
|
||||||
"""处理统一 webhook 请求。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
bot_uuid: Bot 的 UUID
|
|
||||||
path: 子路径(如果有的话)
|
|
||||||
request: Quart Request 对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应数据
|
|
||||||
"""
|
|
||||||
return await self.bot.handle_unified_webhook(request)
|
|
||||||
|
|
||||||
async def run_async(self):
|
async def run_async(self):
|
||||||
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
async def shutdown_trigger_placeholder():
|
||||||
# 保持运行但不启动独立端口
|
|
||||||
|
|
||||||
# 打印 webhook 回调地址
|
|
||||||
if self.bot_uuid and hasattr(self.logger, 'ap'):
|
|
||||||
try:
|
|
||||||
api_port = self.logger.ap.instance_config.data['api']['port']
|
|
||||||
webhook_url = f'http://127.0.0.1:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
webhook_url_public = f'http://<Your-Public-IP>:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
|
|
||||||
await self.logger.info('QQ 官方机器人 Webhook 回调地址:')
|
|
||||||
await self.logger.info(f' 本地地址: {webhook_url}')
|
|
||||||
await self.logger.info(f' 公网地址: {webhook_url_public}')
|
|
||||||
await self.logger.info('请在 QQ 官方机器人后台配置此回调地址')
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.warning(f'无法生成 webhook URL: {e}')
|
|
||||||
|
|
||||||
async def keep_alive():
|
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
await keep_alive()
|
await self.bot.run_task(
|
||||||
|
host='0.0.0.0',
|
||||||
|
port=self.config['port'],
|
||||||
|
shutdown_trigger=shutdown_trigger_placeholder,
|
||||||
|
)
|
||||||
|
|
||||||
async def kill(self) -> bool:
|
async def kill(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
default: ""
|
default: ""
|
||||||
|
- name: port
|
||||||
|
label:
|
||||||
|
en_US: Port
|
||||||
|
zh_Hans: 监听端口
|
||||||
|
type: integer
|
||||||
|
required: true
|
||||||
|
default: 2284
|
||||||
- name: token
|
- name: token
|
||||||
label:
|
label:
|
||||||
en_US: Token
|
en_US: Token
|
||||||
|
|||||||
@@ -97,12 +97,13 @@ class SlackEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
|||||||
class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||||
bot: SlackClient
|
bot: SlackClient
|
||||||
bot_account_id: str
|
bot_account_id: str
|
||||||
bot_uuid: str = None
|
|
||||||
message_converter: SlackMessageConverter = SlackMessageConverter()
|
message_converter: SlackMessageConverter = SlackMessageConverter()
|
||||||
event_converter: SlackEventConverter = SlackEventConverter()
|
event_converter: SlackEventConverter = SlackEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
|
|
||||||
def __init__(self, config: dict, logger: EventLogger):
|
def __init__(self, config: dict, logger: EventLogger):
|
||||||
|
self.config = config
|
||||||
|
self.logger = logger
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'bot_token',
|
'bot_token',
|
||||||
'signing_secret',
|
'signing_secret',
|
||||||
@@ -111,18 +112,8 @@ class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
if missing_keys:
|
if missing_keys:
|
||||||
raise command_errors.ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员')
|
raise command_errors.ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员')
|
||||||
|
|
||||||
bot = SlackClient(
|
self.bot = SlackClient(
|
||||||
bot_token=config['bot_token'],
|
bot_token=self.config['bot_token'], signing_secret=self.config['signing_secret'], logger=self.logger
|
||||||
signing_secret=config['signing_secret'],
|
|
||||||
logger=logger,
|
|
||||||
unified_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
config=config,
|
|
||||||
logger=logger,
|
|
||||||
bot=bot,
|
|
||||||
bot_account_id=config['bot_token'],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
@@ -174,45 +165,16 @@ class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
elif event_type == platform_events.GroupMessage:
|
elif event_type == platform_events.GroupMessage:
|
||||||
self.bot.on_message('channel')(on_message)
|
self.bot.on_message('channel')(on_message)
|
||||||
|
|
||||||
def set_bot_uuid(self, bot_uuid: str):
|
|
||||||
"""设置 bot UUID(用于生成 webhook URL)"""
|
|
||||||
self.bot_uuid = bot_uuid
|
|
||||||
|
|
||||||
async def handle_unified_webhook(self, bot_uuid: str, path: str, request):
|
|
||||||
"""处理统一 webhook 请求。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
bot_uuid: Bot 的 UUID
|
|
||||||
path: 子路径(如果有的话)
|
|
||||||
request: Quart Request 对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应数据
|
|
||||||
"""
|
|
||||||
return await self.bot.handle_unified_webhook(request)
|
|
||||||
|
|
||||||
async def run_async(self):
|
async def run_async(self):
|
||||||
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
async def shutdown_trigger_placeholder():
|
||||||
# 保持运行但不启动独立端口
|
|
||||||
|
|
||||||
# 打印 webhook 回调地址
|
|
||||||
if self.bot_uuid and hasattr(self.logger, 'ap'):
|
|
||||||
try:
|
|
||||||
api_port = self.logger.ap.instance_config.data['api']['port']
|
|
||||||
webhook_url = f"http://127.0.0.1:{api_port}/bots/{self.bot_uuid}"
|
|
||||||
webhook_url_public = f"http://<Your-Public-IP>:{api_port}/bots/{self.bot_uuid}"
|
|
||||||
|
|
||||||
await self.logger.info(f"Slack 机器人 Webhook 回调地址:")
|
|
||||||
await self.logger.info(f" 本地地址: {webhook_url}")
|
|
||||||
await self.logger.info(f" 公网地址: {webhook_url_public}")
|
|
||||||
await self.logger.info(f"请在 Slack 后台配置此回调地址")
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.warning(f"无法生成 webhook URL: {e}")
|
|
||||||
|
|
||||||
async def keep_alive():
|
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
await keep_alive()
|
|
||||||
|
await self.bot.run_task(
|
||||||
|
host='0.0.0.0',
|
||||||
|
port=self.config['port'],
|
||||||
|
shutdown_trigger=shutdown_trigger_placeholder,
|
||||||
|
)
|
||||||
|
|
||||||
async def kill(self) -> bool:
|
async def kill(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
default: ""
|
default: ""
|
||||||
|
- name: port
|
||||||
|
label:
|
||||||
|
en_US: Port
|
||||||
|
zh_Hans: 监听端口
|
||||||
|
type: int
|
||||||
|
required: true
|
||||||
|
default: 2288
|
||||||
execution:
|
execution:
|
||||||
python:
|
python:
|
||||||
path: ./slack.py
|
path: ./slack.py
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
|||||||
|
|
||||||
# 从message_source获取pipeline_uuid和connection_id
|
# 从message_source获取pipeline_uuid和connection_id
|
||||||
pipeline_uuid = self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid
|
pipeline_uuid = self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid
|
||||||
session_type = 'group' if isinstance(message_source, platform_events.GroupMessage) else 'person'
|
# session_type = 'group' if isinstance(message_source, platform_events.GroupMessage) else 'person'
|
||||||
|
|
||||||
# 生成新的消息ID
|
# 生成新的消息ID
|
||||||
msg_id = len(session.get_message_list(pipeline_uuid)) + 1
|
msg_id = len(session.get_message_list(pipeline_uuid)) + 1
|
||||||
@@ -134,15 +134,13 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
|||||||
# 保存到历史记录
|
# 保存到历史记录
|
||||||
session.get_message_list(pipeline_uuid).append(message_data)
|
session.get_message_list(pipeline_uuid).append(message_data)
|
||||||
|
|
||||||
# 直接广播到所有该pipeline的连接,包含session_type信息
|
# 直接广播到所有该pipeline的连接
|
||||||
await ws_connection_manager.broadcast_to_pipeline(
|
await ws_connection_manager.broadcast_to_pipeline(
|
||||||
pipeline_uuid,
|
pipeline_uuid,
|
||||||
{
|
{
|
||||||
'type': 'response',
|
'type': 'response',
|
||||||
'session_type': session_type,
|
|
||||||
'data': message_data.model_dump(),
|
'data': message_data.model_dump(),
|
||||||
},
|
},
|
||||||
session_type=session_type,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return message_data.model_dump()
|
return message_data.model_dump()
|
||||||
@@ -164,7 +162,6 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
|||||||
)
|
)
|
||||||
|
|
||||||
pipeline_uuid = self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid
|
pipeline_uuid = self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid
|
||||||
session_type = 'group' if isinstance(message_source, platform_events.GroupMessage) else 'person'
|
|
||||||
message_list = session.get_message_list(pipeline_uuid)
|
message_list = session.get_message_list(pipeline_uuid)
|
||||||
|
|
||||||
# 检查是否是新的流式消息(通过bot_message对象判断)
|
# 检查是否是新的流式消息(通过bot_message对象判断)
|
||||||
@@ -200,15 +197,13 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
|||||||
if is_final and bot_message.tool_calls is None:
|
if is_final and bot_message.tool_calls is None:
|
||||||
message_list[-1] = message_data
|
message_list[-1] = message_data
|
||||||
|
|
||||||
# 直接广播到所有该pipeline的连接,包含session_type信息
|
# 直接广播到所有该pipeline的连接
|
||||||
await ws_connection_manager.broadcast_to_pipeline(
|
await ws_connection_manager.broadcast_to_pipeline(
|
||||||
pipeline_uuid,
|
pipeline_uuid,
|
||||||
{
|
{
|
||||||
'type': 'response',
|
'type': 'response',
|
||||||
'session_type': session_type,
|
|
||||||
'data': message_data.model_dump(),
|
'data': message_data.model_dump(),
|
||||||
},
|
},
|
||||||
session_type=session_type,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return message_data.model_dump()
|
return message_data.model_dump()
|
||||||
@@ -349,15 +344,13 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
|||||||
)
|
)
|
||||||
use_session.get_message_list(pipeline_uuid).append(user_message)
|
use_session.get_message_list(pipeline_uuid).append(user_message)
|
||||||
|
|
||||||
# 广播用户消息到所有连接(包括发送者),包含session_type信息
|
# 广播用户消息到所有连接(包括发送者)
|
||||||
await ws_connection_manager.broadcast_to_pipeline(
|
await ws_connection_manager.broadcast_to_pipeline(
|
||||||
pipeline_uuid,
|
pipeline_uuid,
|
||||||
{
|
{
|
||||||
'type': 'user_message',
|
'type': 'user_message',
|
||||||
'session_type': session_type,
|
|
||||||
'data': user_message.model_dump(),
|
'data': user_message.model_dump(),
|
||||||
},
|
},
|
||||||
session_type=session_type,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加消息源
|
# 添加消息源
|
||||||
|
|||||||
@@ -134,20 +134,9 @@ class WebSocketConnectionManager:
|
|||||||
connection_ids = self.session_connections.get(session_type, set())
|
connection_ids = self.session_connections.get(session_type, set())
|
||||||
return [self.connections[cid] for cid in connection_ids if cid in self.connections]
|
return [self.connections[cid] for cid in connection_ids if cid in self.connections]
|
||||||
|
|
||||||
async def broadcast_to_pipeline(self, pipeline_uuid: str, message: dict, session_type: str = None):
|
async def broadcast_to_pipeline(self, pipeline_uuid: str, message: dict):
|
||||||
"""向指定流水线的所有连接广播消息
|
"""向指定流水线的所有连接广播消息"""
|
||||||
|
|
||||||
Args:
|
|
||||||
pipeline_uuid: 流水线UUID
|
|
||||||
message: 要广播的消息
|
|
||||||
session_type: 可选的会话类型过滤器,如果提供则只向匹配的session_type连接广播
|
|
||||||
"""
|
|
||||||
connections = await self.get_connections_by_pipeline(pipeline_uuid)
|
connections = await self.get_connections_by_pipeline(pipeline_uuid)
|
||||||
|
|
||||||
# 如果指定了session_type,只向匹配的连接广播
|
|
||||||
if session_type is not None:
|
|
||||||
connections = [conn for conn in connections if conn.session_type == session_type]
|
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
for conn in connections:
|
for conn in connections:
|
||||||
tasks.append(self.send_to_connection(conn.connection_id, message))
|
tasks.append(self.send_to_connection(conn.connection_id, message))
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import datetime
|
|||||||
from langbot.libs.wecom_api.api import WecomClient
|
from langbot.libs.wecom_api.api import WecomClient
|
||||||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
||||||
from langbot.libs.wecom_api.wecomevent import WecomEvent
|
from langbot.libs.wecom_api.wecomevent import WecomEvent
|
||||||
from ...utils import image
|
from langbot.pkg.utils import image
|
||||||
from ..logger import EventLogger
|
from langbot.pkg.platform.logger import EventLogger
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
||||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
||||||
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
||||||
@@ -131,7 +131,6 @@ class WecomAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
message_converter: WecomMessageConverter = WecomMessageConverter()
|
message_converter: WecomMessageConverter = WecomMessageConverter()
|
||||||
event_converter: WecomEventConverter = WecomEventConverter()
|
event_converter: WecomEventConverter = WecomEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
bot_uuid: str = None
|
|
||||||
|
|
||||||
def __init__(self, config: dict, logger: EventLogger):
|
def __init__(self, config: dict, logger: EventLogger):
|
||||||
# 校验必填项
|
# 校验必填项
|
||||||
@@ -142,12 +141,11 @@ class WecomAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
'EncodingAESKey',
|
'EncodingAESKey',
|
||||||
'contacts_secret',
|
'contacts_secret',
|
||||||
]
|
]
|
||||||
|
|
||||||
missing_keys = [key for key in required_keys if key not in config]
|
missing_keys = [key for key in required_keys if key not in config]
|
||||||
if missing_keys:
|
if missing_keys:
|
||||||
raise Exception(f'Wecom 缺少配置项: {missing_keys}')
|
raise Exception(f'Wecom 缺少配置项: {missing_keys}')
|
||||||
|
|
||||||
# 创建运行时 bot 对象,始终使用统一 webhook 模式
|
# 创建运行时 bot 对象
|
||||||
bot = WecomClient(
|
bot = WecomClient(
|
||||||
corpid=config['corpid'],
|
corpid=config['corpid'],
|
||||||
secret=config['secret'],
|
secret=config['secret'],
|
||||||
@@ -155,7 +153,6 @@ class WecomAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
EncodingAESKey=config['EncodingAESKey'],
|
EncodingAESKey=config['EncodingAESKey'],
|
||||||
contacts_secret=config['contacts_secret'],
|
contacts_secret=config['contacts_secret'],
|
||||||
logger=logger,
|
logger=logger,
|
||||||
unified_mode=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -165,10 +162,6 @@ class WecomAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
bot_account_id='',
|
bot_account_id='',
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_bot_uuid(self, bot_uuid: str):
|
|
||||||
"""设置 bot UUID(用于生成 webhook URL)"""
|
|
||||||
self.bot_uuid = bot_uuid
|
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
self,
|
self,
|
||||||
message_source: platform_events.MessageEvent,
|
message_source: platform_events.MessageEvent,
|
||||||
@@ -187,6 +180,9 @@ class WecomAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
await self.bot.send_image(fixed_user_id, Wecom_event.agent_id, content['media_id'])
|
await self.bot.send_image(fixed_user_id, Wecom_event.agent_id, content['media_id'])
|
||||||
|
|
||||||
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):
|
||||||
|
"""企业微信目前只有发送给个人的方法,
|
||||||
|
构造target_id的方式为前半部分为账户id,后半部分为agent_id,中间使用“|”符号隔开。
|
||||||
|
"""
|
||||||
content_list = await WecomMessageConverter.yiri2target(message, self.bot)
|
content_list = await WecomMessageConverter.yiri2target(message, self.bot)
|
||||||
parts = target_id.split('|')
|
parts = target_id.split('|')
|
||||||
user_id = parts[0]
|
user_id = parts[0]
|
||||||
@@ -218,38 +214,16 @@ class WecomAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
elif event_type == platform_events.GroupMessage:
|
elif event_type == platform_events.GroupMessage:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def handle_unified_webhook(self, bot_uuid: str, path: str, request):
|
|
||||||
"""处理统一 webhook 请求。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
bot_uuid: Bot 的 UUID
|
|
||||||
path: 子路径(如果有的话)
|
|
||||||
request: Quart Request 对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应数据
|
|
||||||
"""
|
|
||||||
return await self.bot.handle_unified_webhook(request)
|
|
||||||
|
|
||||||
async def run_async(self):
|
async def run_async(self):
|
||||||
if self.bot_uuid and hasattr(self.logger, 'ap'):
|
async def shutdown_trigger_placeholder():
|
||||||
try:
|
|
||||||
api_port = self.logger.ap.instance_config.data['api']['port']
|
|
||||||
webhook_url = f'http://127.0.0.1:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
webhook_url_public = f'http://<Your-Public-IP>:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
|
|
||||||
await self.logger.info('企业微信 Webhook 回调地址:')
|
|
||||||
await self.logger.info(f' 本地地址: {webhook_url}')
|
|
||||||
await self.logger.info(f' 公网地址: {webhook_url_public}')
|
|
||||||
await self.logger.info('请在企业微信后台配置此回调地址')
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.warning(f'无法生成 webhook URL: {e}')
|
|
||||||
|
|
||||||
async def keep_alive():
|
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
await keep_alive()
|
await self.bot.run_task(
|
||||||
|
host=self.config['host'],
|
||||||
|
port=self.config['port'],
|
||||||
|
shutdown_trigger=shutdown_trigger_placeholder,
|
||||||
|
)
|
||||||
|
|
||||||
async def kill(self) -> bool:
|
async def kill(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -11,6 +11,23 @@ metadata:
|
|||||||
icon: wecom.png
|
icon: wecom.png
|
||||||
spec:
|
spec:
|
||||||
config:
|
config:
|
||||||
|
- name: host
|
||||||
|
label:
|
||||||
|
en_US: Host
|
||||||
|
zh_Hans: 监听主机
|
||||||
|
description:
|
||||||
|
en_US: Webhook host, unless you know what you're doing, please write 0.0.0.0
|
||||||
|
zh_Hans: Webhook 监听主机,除非你知道自己在做什么,否则请写 0.0.0.0
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
default: "0.0.0.0"
|
||||||
|
- name: port
|
||||||
|
label:
|
||||||
|
en_US: Port
|
||||||
|
zh_Hans: 监听端口
|
||||||
|
type: integer
|
||||||
|
required: true
|
||||||
|
default: 2290
|
||||||
- name: corpid
|
- name: corpid
|
||||||
label:
|
label:
|
||||||
en_US: Corpid
|
en_US: Corpid
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platf
|
|||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
||||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
||||||
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
||||||
from ..logger import EventLogger
|
from langbot.pkg.platform.logger import EventLogger
|
||||||
from langbot.libs.wecom_ai_bot_api.wecombotevent import WecomBotEvent
|
from langbot.libs.wecom_ai_bot_api.wecombotevent import WecomBotEvent
|
||||||
from langbot.libs.wecom_ai_bot_api.api import WecomBotClient
|
from langbot.libs.wecom_ai_bot_api.api import WecomBotClient
|
||||||
|
|
||||||
@@ -88,20 +88,19 @@ class WecomBotAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
message_converter: WecomBotMessageConverter = WecomBotMessageConverter()
|
message_converter: WecomBotMessageConverter = WecomBotMessageConverter()
|
||||||
event_converter: WecomBotEventConverter = WecomBotEventConverter()
|
event_converter: WecomBotEventConverter = WecomBotEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
bot_uuid: str = None
|
|
||||||
|
|
||||||
def __init__(self, config: dict, logger: EventLogger):
|
def __init__(self, config: dict, logger: EventLogger):
|
||||||
required_keys = ['Token', 'EncodingAESKey', 'Corpid', 'BotId']
|
required_keys = ['Token', 'EncodingAESKey', 'Corpid', 'BotId', 'port']
|
||||||
missing_keys = [key for key in required_keys if key not in config]
|
missing_keys = [key for key in required_keys if key not in config]
|
||||||
if missing_keys:
|
if missing_keys:
|
||||||
raise Exception(f'WecomBot 缺少配置项: {missing_keys}')
|
raise Exception(f'WecomBot 缺少配置项: {missing_keys}')
|
||||||
|
|
||||||
|
# 创建运行时 bot 对象
|
||||||
bot = WecomBotClient(
|
bot = WecomBotClient(
|
||||||
Token=config['Token'],
|
Token=config['Token'],
|
||||||
EnCodingAESKey=config['EncodingAESKey'],
|
EnCodingAESKey=config['EncodingAESKey'],
|
||||||
Corpid=config['Corpid'],
|
Corpid=config['Corpid'],
|
||||||
logger=logger,
|
logger=logger,
|
||||||
unified_mode=True,
|
|
||||||
)
|
)
|
||||||
bot_account_id = config['BotId']
|
bot_account_id = config['BotId']
|
||||||
|
|
||||||
@@ -190,46 +189,16 @@ class WecomBotAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
except Exception:
|
except Exception:
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
|
|
||||||
def set_bot_uuid(self, bot_uuid: str):
|
|
||||||
"""设置 bot UUID(用于生成 webhook URL)"""
|
|
||||||
self.bot_uuid = bot_uuid
|
|
||||||
|
|
||||||
async def handle_unified_webhook(self, bot_uuid: str, path: str, request):
|
|
||||||
"""处理统一 webhook 请求。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
bot_uuid: Bot 的 UUID
|
|
||||||
path: 子路径(如果有的话)
|
|
||||||
request: Quart Request 对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应数据
|
|
||||||
"""
|
|
||||||
return await self.bot.handle_unified_webhook(request)
|
|
||||||
|
|
||||||
async def run_async(self):
|
async def run_async(self):
|
||||||
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
async def shutdown_trigger_placeholder():
|
||||||
# 保持运行但不启动独立端口
|
|
||||||
|
|
||||||
# 打印 webhook 回调地址
|
|
||||||
if self.bot_uuid and hasattr(self.logger, 'ap'):
|
|
||||||
try:
|
|
||||||
api_port = self.logger.ap.instance_config.data['api']['port']
|
|
||||||
webhook_url = f'http://127.0.0.1:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
webhook_url_public = f'http://<Your-Public-IP>:{api_port}/bots/{self.bot_uuid}'
|
|
||||||
|
|
||||||
await self.logger.info('企业微信机器人 Webhook 回调地址:')
|
|
||||||
await self.logger.info(f' 本地地址: {webhook_url}')
|
|
||||||
await self.logger.info(f' 公网地址: {webhook_url_public}')
|
|
||||||
await self.logger.info('请在企业微信后台配置此回调地址')
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.warning(f'无法生成 webhook URL: {e}')
|
|
||||||
|
|
||||||
async def keep_alive():
|
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
await keep_alive()
|
await self.bot.run_task(
|
||||||
|
host='0.0.0.0',
|
||||||
|
port=self.config['port'],
|
||||||
|
shutdown_trigger=shutdown_trigger_placeholder,
|
||||||
|
)
|
||||||
|
|
||||||
async def kill(self) -> bool:
|
async def kill(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ metadata:
|
|||||||
icon: wecombot.png
|
icon: wecombot.png
|
||||||
spec:
|
spec:
|
||||||
config:
|
config:
|
||||||
|
- name: port
|
||||||
|
label:
|
||||||
|
en_US: Port
|
||||||
|
zh_Hans: 监听端口
|
||||||
|
type: integer
|
||||||
|
required: true
|
||||||
|
default: 2291
|
||||||
- name: Corpid
|
- name: Corpid
|
||||||
label:
|
label:
|
||||||
en_US: Corpid
|
en_US: Corpid
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ class WecomCSAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
bot: WecomCSClient = pydantic.Field(exclude=True)
|
bot: WecomCSClient = pydantic.Field(exclude=True)
|
||||||
message_converter: WecomMessageConverter = WecomMessageConverter()
|
message_converter: WecomMessageConverter = WecomMessageConverter()
|
||||||
event_converter: WecomEventConverter = WecomEventConverter()
|
event_converter: WecomEventConverter = WecomEventConverter()
|
||||||
bot_uuid: str = None
|
|
||||||
|
|
||||||
def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger):
|
def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger):
|
||||||
required_keys = [
|
required_keys = [
|
||||||
@@ -140,7 +139,6 @@ class WecomCSAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
token=config['token'],
|
token=config['token'],
|
||||||
EncodingAESKey=config['EncodingAESKey'],
|
EncodingAESKey=config['EncodingAESKey'],
|
||||||
logger=logger,
|
logger=logger,
|
||||||
unified_mode=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -172,10 +170,6 @@ class WecomCSAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
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):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def set_bot_uuid(self, bot_uuid: str):
|
|
||||||
"""设置 bot UUID(用于生成 webhook URL)"""
|
|
||||||
self.bot_uuid = bot_uuid
|
|
||||||
|
|
||||||
def register_listener(
|
def register_listener(
|
||||||
self,
|
self,
|
||||||
event_type: typing.Type[platform_events.Event],
|
event_type: typing.Type[platform_events.Event],
|
||||||
@@ -196,41 +190,16 @@ class WecomCSAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
elif event_type == platform_events.GroupMessage:
|
elif event_type == platform_events.GroupMessage:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def handle_unified_webhook(self, bot_uuid: str, path: str, request):
|
|
||||||
"""处理统一 webhook 请求。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
bot_uuid: Bot 的 UUID
|
|
||||||
path: 子路径(如果有的话)
|
|
||||||
request: Quart Request 对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应数据
|
|
||||||
"""
|
|
||||||
return await self.bot.handle_unified_webhook(request)
|
|
||||||
|
|
||||||
async def run_async(self):
|
async def run_async(self):
|
||||||
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
async def shutdown_trigger_placeholder():
|
||||||
# 保持运行但不启动独立端口
|
|
||||||
|
|
||||||
# 打印 webhook 回调地址
|
|
||||||
if self.bot_uuid and hasattr(self.logger, 'ap'):
|
|
||||||
try:
|
|
||||||
api_port = self.logger.ap.instance_config.data['api']['port']
|
|
||||||
webhook_url = f"http://127.0.0.1:{api_port}/bots/{self.bot_uuid}"
|
|
||||||
webhook_url_public = f"http://<Your-Public-IP>:{api_port}/bots/{self.bot_uuid}"
|
|
||||||
|
|
||||||
await self.logger.info(f"企业微信客服 Webhook 回调地址:")
|
|
||||||
await self.logger.info(f" 本地地址: {webhook_url}")
|
|
||||||
await self.logger.info(f" 公网地址: {webhook_url_public}")
|
|
||||||
await self.logger.info(f"请在企业微信后台配置此回调地址")
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.warning(f"无法生成 webhook URL: {e}")
|
|
||||||
|
|
||||||
async def keep_alive():
|
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
await keep_alive()
|
|
||||||
|
await self.bot.run_task(
|
||||||
|
host='0.0.0.0',
|
||||||
|
port=self.config['port'],
|
||||||
|
shutdown_trigger=shutdown_trigger_placeholder,
|
||||||
|
)
|
||||||
|
|
||||||
async def kill(self) -> bool:
|
async def kill(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ metadata:
|
|||||||
icon: wecom.png
|
icon: wecom.png
|
||||||
spec:
|
spec:
|
||||||
config:
|
config:
|
||||||
|
- name: port
|
||||||
|
label:
|
||||||
|
en_US: Port
|
||||||
|
zh_Hans: 监听端口
|
||||||
|
type: int
|
||||||
|
required: true
|
||||||
|
default: 2289
|
||||||
- name: corpid
|
- name: corpid
|
||||||
label:
|
label:
|
||||||
en_US: Corpid
|
en_US: Corpid
|
||||||
|
|||||||
@@ -385,12 +385,6 @@ class PluginRuntimeConnector:
|
|||||||
async def get_plugin_assets(self, plugin_author: str, plugin_name: str, filepath: str) -> dict[str, Any]:
|
async def get_plugin_assets(self, plugin_author: str, plugin_name: str, filepath: str) -> dict[str, Any]:
|
||||||
return await self.handler.get_plugin_assets(plugin_author, plugin_name, filepath)
|
return await self.handler.get_plugin_assets(plugin_author, plugin_name, filepath)
|
||||||
|
|
||||||
async def get_debug_info(self) -> dict[str, Any]:
|
|
||||||
"""Get debug information including debug key and WS URL"""
|
|
||||||
if not self.is_enable_plugin:
|
|
||||||
return {}
|
|
||||||
return await self.handler.get_debug_info()
|
|
||||||
|
|
||||||
async def emit_event(
|
async def emit_event(
|
||||||
self,
|
self,
|
||||||
event: events.BaseEventModel,
|
event: events.BaseEventModel,
|
||||||
|
|||||||
@@ -758,12 +758,3 @@ class RuntimeConnectionHandler(handler.Handler):
|
|||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def get_debug_info(self) -> dict[str, Any]:
|
|
||||||
"""Get debug information including debug key and WS URL"""
|
|
||||||
result = await self.call_action(
|
|
||||||
LangBotToRuntimeAction.GET_DEBUG_INFO,
|
|
||||||
{},
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|||||||
@@ -298,6 +298,8 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
'langbot_session_id': query.variables['session_id'],
|
'langbot_session_id': query.variables['session_id'],
|
||||||
'langbot_conversation_id': query.variables['conversation_id'],
|
'langbot_conversation_id': query.variables['conversation_id'],
|
||||||
'langbot_msg_create_time': query.variables['msg_create_time'],
|
'langbot_msg_create_time': query.variables['msg_create_time'],
|
||||||
|
'langbot_sender_id': query.variables.get('sender_id', ''),
|
||||||
|
'langbot_sender_name': query.variables.get('sender_name', ''),
|
||||||
}
|
}
|
||||||
|
|
||||||
inputs.update(query.variables)
|
inputs.update(query.variables)
|
||||||
@@ -576,6 +578,8 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
'langbot_session_id': query.variables['session_id'],
|
'langbot_session_id': query.variables['session_id'],
|
||||||
'langbot_conversation_id': query.variables['conversation_id'],
|
'langbot_conversation_id': query.variables['conversation_id'],
|
||||||
'langbot_msg_create_time': query.variables['msg_create_time'],
|
'langbot_msg_create_time': query.variables['msg_create_time'],
|
||||||
|
'langbot_sender_id': query.variables.get('sender_id', ''),
|
||||||
|
'langbot_sender_name': query.variables.get('sender_name', ''),
|
||||||
}
|
}
|
||||||
|
|
||||||
inputs.update(query.variables)
|
inputs.update(query.variables)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import langbot
|
|||||||
|
|
||||||
semantic_version = f'v{langbot.__version__}'
|
semantic_version = f'v{langbot.__version__}'
|
||||||
|
|
||||||
required_database_version = 13
|
required_database_version = 12
|
||||||
"""Tag the version of the database schema, used to check if the database needs to be migrated"""
|
"""Tag the version of the database schema, used to check if the database needs to be migrated"""
|
||||||
|
|
||||||
debug_mode = False
|
debug_mode = False
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
admins: []
|
admins: []
|
||||||
api:
|
api:
|
||||||
port: 5300
|
port: 5300
|
||||||
webhook_prefix: 'http://127.0.0.1:5300'
|
|
||||||
command:
|
command:
|
||||||
enable: true
|
enable: true
|
||||||
prefix:
|
prefix:
|
||||||
@@ -49,4 +48,3 @@ plugin:
|
|||||||
runtime_ws_url: 'ws://langbot_plugin_runtime:5400/control/ws'
|
runtime_ws_url: 'ws://langbot_plugin_runtime:5400/control/ws'
|
||||||
enable_marketplace: true
|
enable_marketplace: true
|
||||||
cloud_service_url: 'https://space.langbot.app'
|
cloud_service_url: 'https://space.langbot.app'
|
||||||
display_plugin_debug_url: 'http://localhost:5401'
|
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
"""
|
|
||||||
Tests for webhook_prefix configuration
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import pytest
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
def _apply_env_overrides_to_config(cfg: dict) -> dict:
|
|
||||||
"""Apply environment variable overrides to data/config.yaml
|
|
||||||
|
|
||||||
Environment variables should be uppercase and use __ (double underscore)
|
|
||||||
to represent nested keys. For example:
|
|
||||||
- CONCURRENCY__PIPELINE overrides concurrency.pipeline
|
|
||||||
- PLUGIN__RUNTIME_WS_URL overrides plugin.runtime_ws_url
|
|
||||||
|
|
||||||
Arrays and dict types are ignored.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
cfg: Configuration dictionary
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Updated configuration dictionary
|
|
||||||
"""
|
|
||||||
|
|
||||||
def convert_value(value: str, original_value: Any) -> Any:
|
|
||||||
"""Convert string value to appropriate type based on original value
|
|
||||||
|
|
||||||
Args:
|
|
||||||
value: String value from environment variable
|
|
||||||
original_value: Original value to infer type from
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Converted value (falls back to string if conversion fails)
|
|
||||||
"""
|
|
||||||
if isinstance(original_value, bool):
|
|
||||||
return value.lower() in ('true', '1', 'yes', 'on')
|
|
||||||
elif isinstance(original_value, int):
|
|
||||||
try:
|
|
||||||
return int(value)
|
|
||||||
except ValueError:
|
|
||||||
# If conversion fails, keep as string (user error, but non-breaking)
|
|
||||||
return value
|
|
||||||
elif isinstance(original_value, float):
|
|
||||||
try:
|
|
||||||
return float(value)
|
|
||||||
except ValueError:
|
|
||||||
# If conversion fails, keep as string (user error, but non-breaking)
|
|
||||||
return value
|
|
||||||
else:
|
|
||||||
return value
|
|
||||||
|
|
||||||
# Process environment variables
|
|
||||||
for env_key, env_value in os.environ.items():
|
|
||||||
# Check if the environment variable is uppercase and contains __
|
|
||||||
if not env_key.isupper():
|
|
||||||
continue
|
|
||||||
if '__' not in env_key:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Convert environment variable name to config path
|
|
||||||
# e.g., CONCURRENCY__PIPELINE -> ['concurrency', 'pipeline']
|
|
||||||
keys = [key.lower() for key in env_key.split('__')]
|
|
||||||
|
|
||||||
# Navigate to the target value and validate the path
|
|
||||||
current = cfg
|
|
||||||
|
|
||||||
for i, key in enumerate(keys):
|
|
||||||
if not isinstance(current, dict) or key not in current:
|
|
||||||
break
|
|
||||||
|
|
||||||
if i == len(keys) - 1:
|
|
||||||
# At the final key - check if it's a scalar value
|
|
||||||
if isinstance(current[key], (dict, list)):
|
|
||||||
# Skip dict and list types
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# Valid scalar value - convert and set it
|
|
||||||
converted_value = convert_value(env_value, current[key])
|
|
||||||
current[key] = converted_value
|
|
||||||
else:
|
|
||||||
# Navigate deeper
|
|
||||||
current = current[key]
|
|
||||||
|
|
||||||
return cfg
|
|
||||||
|
|
||||||
|
|
||||||
class TestWebhookDisplayPrefix:
|
|
||||||
"""Test webhook_prefix configuration functionality"""
|
|
||||||
|
|
||||||
def test_default_webhook_prefix(self):
|
|
||||||
"""Test that the default webhook display prefix is correctly set"""
|
|
||||||
cfg = {'api': {'port': 5300, 'webhook_prefix': 'http://127.0.0.1:5300'}}
|
|
||||||
|
|
||||||
# Should have the default value
|
|
||||||
assert cfg['api']['webhook_prefix'] == 'http://127.0.0.1:5300'
|
|
||||||
|
|
||||||
def test_webhook_prefix_env_override(self):
|
|
||||||
"""Test overriding webhook_prefix via environment variable"""
|
|
||||||
cfg = {'api': {'port': 5300, 'webhook_prefix': 'http://127.0.0.1:5300'}}
|
|
||||||
|
|
||||||
# Set environment variable
|
|
||||||
os.environ['API__WEBHOOK_PREFIX'] = 'https://example.com:8080'
|
|
||||||
|
|
||||||
result = _apply_env_overrides_to_config(cfg)
|
|
||||||
|
|
||||||
assert result['api']['webhook_prefix'] == 'https://example.com:8080'
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
del os.environ['API__WEBHOOK_PREFIX']
|
|
||||||
|
|
||||||
def test_webhook_prefix_with_custom_domain(self):
|
|
||||||
"""Test webhook_prefix with custom domain"""
|
|
||||||
cfg = {'api': {'port': 5300, 'webhook_prefix': 'http://127.0.0.1:5300'}}
|
|
||||||
|
|
||||||
# Set to a custom domain
|
|
||||||
os.environ['API__WEBHOOK_PREFIX'] = 'https://bot.mycompany.com'
|
|
||||||
|
|
||||||
result = _apply_env_overrides_to_config(cfg)
|
|
||||||
|
|
||||||
assert result['api']['webhook_prefix'] == 'https://bot.mycompany.com'
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
del os.environ['API__WEBHOOK_PREFIX']
|
|
||||||
|
|
||||||
def test_webhook_prefix_with_subdirectory(self):
|
|
||||||
"""Test webhook_prefix with subdirectory path"""
|
|
||||||
cfg = {'api': {'port': 5300, 'webhook_prefix': 'http://127.0.0.1:5300'}}
|
|
||||||
|
|
||||||
# Set to a URL with subdirectory
|
|
||||||
os.environ['API__WEBHOOK_PREFIX'] = 'https://example.com/langbot'
|
|
||||||
|
|
||||||
result = _apply_env_overrides_to_config(cfg)
|
|
||||||
|
|
||||||
assert result['api']['webhook_prefix'] == 'https://example.com/langbot'
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
del os.environ['API__WEBHOOK_PREFIX']
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
pytest.main([__file__, '-v'])
|
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
"""
|
||||||
|
Test that preproc module adds sender_id and sender_name variables.
|
||||||
|
These tests verify the variables are correctly extracted from message events.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import Mock, AsyncMock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_sender_variables_friend_message():
|
||||||
|
"""Test sender_id and sender_name are extracted from FriendMessage"""
|
||||||
|
# Create mock Friend sender
|
||||||
|
mock_sender = Mock()
|
||||||
|
mock_sender.id = 'test_user_123'
|
||||||
|
mock_sender.nickname = 'Test User'
|
||||||
|
mock_sender.remark = 'Test Remark'
|
||||||
|
mock_sender.get_name = Mock(return_value='Test User')
|
||||||
|
|
||||||
|
# Verify get_name returns nickname
|
||||||
|
assert mock_sender.get_name() == 'Test User'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_sender_variables_group_message():
|
||||||
|
"""Test sender_id and sender_name are extracted from GroupMessage"""
|
||||||
|
# Create mock GroupMember sender
|
||||||
|
mock_sender = Mock()
|
||||||
|
mock_sender.id = 'group_user_456'
|
||||||
|
mock_sender.member_name = 'Group User Name'
|
||||||
|
mock_sender.get_name = Mock(return_value='Group User Name')
|
||||||
|
|
||||||
|
# Verify get_name returns member_name
|
||||||
|
assert mock_sender.get_name() == 'Group User Name'
|
||||||
|
|
||||||
|
|
||||||
|
def test_sender_id_string_conversion():
|
||||||
|
"""Test sender_id is converted to string"""
|
||||||
|
# Test integer sender_id
|
||||||
|
sender_id_int = 12345
|
||||||
|
assert str(sender_id_int) == '12345'
|
||||||
|
|
||||||
|
# Test string sender_id
|
||||||
|
sender_id_str = 'user_abc'
|
||||||
|
assert str(sender_id_str) == 'user_abc'
|
||||||
|
|
||||||
|
|
||||||
|
def test_sender_name_empty_fallback():
|
||||||
|
"""Test sender_name defaults to empty string when not available"""
|
||||||
|
# When sender has no name
|
||||||
|
sender_name = ''
|
||||||
|
assert sender_name == ''
|
||||||
|
|
||||||
|
|
||||||
|
def test_variables_dict_structure():
|
||||||
|
"""Test the variables dictionary has expected structure"""
|
||||||
|
# Simulate what the variables dict should look like
|
||||||
|
variables = {
|
||||||
|
'session_id': 'person_12345',
|
||||||
|
'conversation_id': 'conv-uuid-123',
|
||||||
|
'msg_create_time': 1609459200,
|
||||||
|
'sender_id': '12345',
|
||||||
|
'sender_name': 'Test User',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify all expected keys are present
|
||||||
|
assert 'session_id' in variables
|
||||||
|
assert 'conversation_id' in variables
|
||||||
|
assert 'msg_create_time' in variables
|
||||||
|
assert 'sender_id' in variables
|
||||||
|
assert 'sender_name' in variables
|
||||||
|
|
||||||
|
# Verify values
|
||||||
|
assert variables['sender_id'] == '12345'
|
||||||
|
assert variables['sender_name'] == 'Test User'
|
||||||
|
|
||||||
|
|
||||||
|
def test_dify_workflow_inputs_structure():
|
||||||
|
"""Test the Dify workflow inputs have expected legacy variables"""
|
||||||
|
plain_text = 'Hello world'
|
||||||
|
variables = {
|
||||||
|
'session_id': 'person_12345',
|
||||||
|
'conversation_id': 'conv-uuid-123',
|
||||||
|
'msg_create_time': 1609459200,
|
||||||
|
'sender_id': '12345',
|
||||||
|
'sender_name': 'Test User',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Simulate Dify workflow inputs structure
|
||||||
|
inputs = {
|
||||||
|
'langbot_user_message_text': plain_text,
|
||||||
|
'langbot_session_id': variables['session_id'],
|
||||||
|
'langbot_conversation_id': variables['conversation_id'],
|
||||||
|
'langbot_msg_create_time': variables['msg_create_time'],
|
||||||
|
'langbot_sender_id': variables.get('sender_id', ''),
|
||||||
|
'langbot_sender_name': variables.get('sender_name', ''),
|
||||||
|
}
|
||||||
|
inputs.update(variables)
|
||||||
|
|
||||||
|
# Verify all legacy variables are present
|
||||||
|
assert inputs['langbot_user_message_text'] == 'Hello world'
|
||||||
|
assert inputs['langbot_session_id'] == 'person_12345'
|
||||||
|
assert inputs['langbot_conversation_id'] == 'conv-uuid-123'
|
||||||
|
assert inputs['langbot_msg_create_time'] == 1609459200
|
||||||
|
assert inputs['langbot_sender_id'] == '12345'
|
||||||
|
assert inputs['langbot_sender_name'] == 'Test User'
|
||||||
|
|
||||||
|
# Verify regular variables are also present
|
||||||
|
assert inputs['sender_id'] == '12345'
|
||||||
|
assert inputs['sender_name'] == 'Test User'
|
||||||
@@ -74,18 +74,11 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@types/debug": "^4.1.12",
|
|
||||||
"@types/estree": "^1.0.8",
|
|
||||||
"@types/estree-jsx": "^1.0.5",
|
|
||||||
"@types/hast": "^3.0.4",
|
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/mdast": "^4.0.4",
|
|
||||||
"@types/ms": "^2.1.0",
|
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@types/unist": "^3.0.3",
|
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.2.4",
|
"eslint-config-next": "15.2.4",
|
||||||
"eslint-config-prettier": "^10.1.2",
|
"eslint-config-prettier": "^10.1.2",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IChooseAdapterEntity,
|
IChooseAdapterEntity,
|
||||||
IPipelineEntity,
|
IPipelineEntity,
|
||||||
@@ -112,86 +112,11 @@ export default function BotForm({
|
|||||||
IDynamicFormItemSchema[]
|
IDynamicFormItemSchema[]
|
||||||
>([]);
|
>([]);
|
||||||
const [, setIsLoading] = useState<boolean>(false);
|
const [, setIsLoading] = useState<boolean>(false);
|
||||||
const [webhookUrl, setWebhookUrl] = useState<string>('');
|
|
||||||
const webhookInputRef = React.useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBotFormValues();
|
setBotFormValues();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 复制到剪贴板的辅助函数 - 使用页面上的真实input元素
|
|
||||||
const copyToClipboard = () => {
|
|
||||||
console.log('[Copy] Attempting to copy from input element');
|
|
||||||
|
|
||||||
const inputElement = webhookInputRef.current;
|
|
||||||
if (!inputElement) {
|
|
||||||
console.error('[Copy] Input element not found');
|
|
||||||
toast.error(t('common.copyFailed'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 确保input元素可见且未被禁用
|
|
||||||
inputElement.disabled = false;
|
|
||||||
inputElement.readOnly = false;
|
|
||||||
|
|
||||||
// 聚焦并选中所有文本
|
|
||||||
inputElement.focus();
|
|
||||||
inputElement.select();
|
|
||||||
|
|
||||||
// 尝试使用现代API
|
|
||||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
||||||
console.log(
|
|
||||||
'[Copy] Using Clipboard API with input value:',
|
|
||||||
inputElement.value,
|
|
||||||
);
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(inputElement.value)
|
|
||||||
.then(() => {
|
|
||||||
console.log('[Copy] Clipboard API success');
|
|
||||||
inputElement.blur(); // 取消选中
|
|
||||||
inputElement.readOnly = true;
|
|
||||||
toast.success(t('bots.webhookUrlCopied'));
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(
|
|
||||||
'[Copy] Clipboard API failed, trying execCommand:',
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
// 降级到execCommand
|
|
||||||
const successful = document.execCommand('copy');
|
|
||||||
console.log('[Copy] execCommand result:', successful);
|
|
||||||
inputElement.blur();
|
|
||||||
inputElement.readOnly = true;
|
|
||||||
if (successful) {
|
|
||||||
toast.success(t('bots.webhookUrlCopied'));
|
|
||||||
} else {
|
|
||||||
toast.error(t('common.copyFailed'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 直接使用execCommand
|
|
||||||
console.log(
|
|
||||||
'[Copy] Using execCommand with input value:',
|
|
||||||
inputElement.value,
|
|
||||||
);
|
|
||||||
const successful = document.execCommand('copy');
|
|
||||||
console.log('[Copy] execCommand result:', successful);
|
|
||||||
inputElement.blur();
|
|
||||||
inputElement.readOnly = true;
|
|
||||||
if (successful) {
|
|
||||||
toast.success(t('bots.webhookUrlCopied'));
|
|
||||||
} else {
|
|
||||||
toast.error(t('common.copyFailed'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Copy] Copy failed:', err);
|
|
||||||
inputElement.readOnly = true;
|
|
||||||
toast.error(t('common.copyFailed'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function setBotFormValues() {
|
function setBotFormValues() {
|
||||||
initBotFormComponent().then(() => {
|
initBotFormComponent().then(() => {
|
||||||
// 拉取初始化表单信息
|
// 拉取初始化表单信息
|
||||||
@@ -206,20 +131,12 @@ export default function BotForm({
|
|||||||
form.setValue('use_pipeline_uuid', val.use_pipeline_uuid || '');
|
form.setValue('use_pipeline_uuid', val.use_pipeline_uuid || '');
|
||||||
handleAdapterSelect(val.adapter);
|
handleAdapterSelect(val.adapter);
|
||||||
// dynamicForm.setFieldsValue(val.adapter_config);
|
// dynamicForm.setFieldsValue(val.adapter_config);
|
||||||
|
|
||||||
// 设置 webhook 地址(如果有)
|
|
||||||
if (val.webhook_full_url) {
|
|
||||||
setWebhookUrl(val.webhook_full_url);
|
|
||||||
} else {
|
|
||||||
setWebhookUrl('');
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
toast.error(t('bots.getBotConfigError') + err.message);
|
toast.error(t('bots.getBotConfigError') + err.message);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
form.reset();
|
form.reset();
|
||||||
setWebhookUrl('');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -292,7 +209,7 @@ export default function BotForm({
|
|||||||
|
|
||||||
async function getBotConfig(
|
async function getBotConfig(
|
||||||
botId: string,
|
botId: string,
|
||||||
): Promise<z.infer<typeof formSchema> & { webhook_full_url?: string }> {
|
): Promise<z.infer<typeof formSchema>> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
httpClient
|
httpClient
|
||||||
.getBot(botId)
|
.getBot(botId)
|
||||||
@@ -305,10 +222,6 @@ export default function BotForm({
|
|||||||
adapter_config: bot.adapter_config,
|
adapter_config: bot.adapter_config,
|
||||||
enable: bot.enable ?? true,
|
enable: bot.enable ?? true,
|
||||||
use_pipeline_uuid: bot.use_pipeline_uuid ?? '',
|
use_pipeline_uuid: bot.use_pipeline_uuid ?? '',
|
||||||
webhook_full_url: bot.adapter_runtime_values
|
|
||||||
? ((bot.adapter_runtime_values as Record<string, unknown>)
|
|
||||||
.webhook_full_url as string)
|
|
||||||
: undefined,
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -447,86 +360,51 @@ export default function BotForm({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 是否启用 & 绑定流水线 仅在编辑模式 */}
|
{/* 是否启用 & 绑定流水线 仅在编辑模式 */}
|
||||||
{initBotId && (
|
{initBotId && (
|
||||||
<>
|
<div className="flex items-center gap-6">
|
||||||
<div className="flex items-center gap-6">
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="enable"
|
||||||
name="enable"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem className="flex flex-col justify-start gap-[0.8rem] h-[3.8rem]">
|
||||||
<FormItem className="flex flex-col justify-start gap-[0.8rem] h-[3.8rem]">
|
<FormLabel>{t('common.enable')}</FormLabel>
|
||||||
<FormLabel>{t('common.enable')}</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<Switch
|
||||||
<Switch
|
checked={field.value}
|
||||||
checked={field.value}
|
onCheckedChange={field.onChange}
|
||||||
onCheckedChange={field.onChange}
|
/>
|
||||||
/>
|
</FormControl>
|
||||||
</FormControl>
|
</FormItem>
|
||||||
</FormItem>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="use_pipeline_uuid"
|
name="use_pipeline_uuid"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col justify-start gap-[0.8rem] h-[3.8rem]">
|
<FormItem className="flex flex-col justify-start gap-[0.8rem] h-[3.8rem]">
|
||||||
<FormLabel>{t('bots.bindPipeline')}</FormLabel>
|
<FormLabel>{t('bots.bindPipeline')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select onValueChange={field.onChange} {...field}>
|
<Select onValueChange={field.onChange} {...field}>
|
||||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||||
<SelectValue
|
<SelectValue
|
||||||
placeholder={t('bots.selectPipeline')}
|
placeholder={t('bots.selectPipeline')}
|
||||||
/>
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="fixed z-[1000]">
|
<SelectContent className="fixed z-[1000]">
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{pipelineNameList.map((item) => (
|
{pipelineNameList.map((item) => (
|
||||||
<SelectItem
|
<SelectItem key={item.value} value={item.value}>
|
||||||
key={item.value}
|
{item.label}
|
||||||
value={item.value}
|
</SelectItem>
|
||||||
>
|
))}
|
||||||
{item.label}
|
</SelectGroup>
|
||||||
</SelectItem>
|
</SelectContent>
|
||||||
))}
|
</Select>
|
||||||
</SelectGroup>
|
</FormControl>
|
||||||
</SelectContent>
|
</FormItem>
|
||||||
</Select>
|
)}
|
||||||
</FormControl>
|
/>
|
||||||
</FormItem>
|
</div>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Webhook 地址显示(统一 Webhook 模式) */}
|
|
||||||
{webhookUrl && (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('bots.webhookUrl')}</FormLabel>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Input
|
|
||||||
ref={webhookInputRef}
|
|
||||||
value={webhookUrl}
|
|
||||||
readOnly
|
|
||||||
className="flex-1 bg-gray-50 dark:bg-gray-900"
|
|
||||||
onClick={(e) => {
|
|
||||||
// 点击输入框时自动全选
|
|
||||||
(e.target as HTMLInputElement).select();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={copyToClipboard}
|
|
||||||
>
|
|
||||||
{t('common.copy')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
|
||||||
{t('bots.webhookUrlHint')}
|
|
||||||
</p>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -509,17 +509,6 @@ export default function ExternalKBForm({
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{t('knowledge.retrieverInstallInfo')}{' '}
|
|
||||||
<a
|
|
||||||
href="https://space.langbot.app/market?category=KnowledgeRetriever"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-primary underline hover:no-underline"
|
|
||||||
>
|
|
||||||
{t('knowledge.retrieverMarketLink')}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export default function PipelineDialog({
|
|||||||
// 编辑流水线时的对话框
|
// 编辑流水线时的对话框
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="overflow-hidden p-0 !max-w-[80vw] h-[75vh] flex">
|
<DialogContent className="overflow-hidden p-0 !max-w-[50rem] h-[75vh] flex">
|
||||||
<SidebarProvider className="items-start w-full flex h-full min-h-0">
|
<SidebarProvider className="items-start w-full flex h-full min-h-0">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
collapsible="none"
|
collapsible="none"
|
||||||
|
|||||||
@@ -20,13 +20,6 @@ import { toast } from 'sonner';
|
|||||||
import AtBadge from './AtBadge';
|
import AtBadge from './AtBadge';
|
||||||
import { WebSocketClient } from '@/app/infra/websocket/WebSocketClient';
|
import { WebSocketClient } from '@/app/infra/websocket/WebSocketClient';
|
||||||
import ImagePreviewDialog from './ImagePreviewDialog';
|
import ImagePreviewDialog from './ImagePreviewDialog';
|
||||||
import ReactMarkdown from 'react-markdown';
|
|
||||||
import remarkGfm from 'remark-gfm';
|
|
||||||
import rehypeHighlight from 'rehype-highlight';
|
|
||||||
import rehypeRaw from 'rehype-raw';
|
|
||||||
import rehypeSlug from 'rehype-slug';
|
|
||||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
|
||||||
import '@/styles/github-markdown.css';
|
|
||||||
|
|
||||||
interface DebugDialogProps {
|
interface DebugDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -57,9 +50,7 @@ export default function DebugDialog({
|
|||||||
const [previewImageUrl, setPreviewImageUrl] = useState<string>('');
|
const [previewImageUrl, setPreviewImageUrl] = useState<string>('');
|
||||||
const [showImagePreview, setShowImagePreview] = useState(false);
|
const [showImagePreview, setShowImagePreview] = useState(false);
|
||||||
const [quotedMessage, setQuotedMessage] = useState<Message | null>(null);
|
const [quotedMessage, setQuotedMessage] = useState<Message | null>(null);
|
||||||
const [rawModeMessages, setRawModeMessages] = useState<Set<string>>(
|
const [hoveredMessageId, setHoveredMessageId] = useState<number | null>(null);
|
||||||
new Set(),
|
|
||||||
);
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const popoverRef = useRef<HTMLDivElement>(null);
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -204,8 +195,6 @@ export default function DebugDialog({
|
|||||||
// 监听 sessionType 和 selectedPipelineId 变化,重新加载消息和连接
|
// 监听 sessionType 和 selectedPipelineId 变化,重新加载消息和连接
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
// 清空当前消息,避免显示旧的消息
|
|
||||||
setMessages([]);
|
|
||||||
loadMessages(selectedPipelineId);
|
loadMessages(selectedPipelineId);
|
||||||
initWebSocket(selectedPipelineId);
|
initWebSocket(selectedPipelineId);
|
||||||
}
|
}
|
||||||
@@ -565,111 +554,7 @@ export default function DebugDialog({
|
|||||||
return t('bots.earlier');
|
return t('bots.earlier');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate a unique key for a message
|
|
||||||
const getMessageKey = (message: Message): string => {
|
|
||||||
return `${message.id}-${message.timestamp}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Toggle raw mode for a message (by default, messages are in markdown mode)
|
|
||||||
const toggleRawMode = (message: Message) => {
|
|
||||||
const key = getMessageKey(message);
|
|
||||||
setRawModeMessages((prev) => {
|
|
||||||
const newSet = new Set(prev);
|
|
||||||
if (newSet.has(key)) {
|
|
||||||
newSet.delete(key);
|
|
||||||
} else {
|
|
||||||
newSet.add(key);
|
|
||||||
}
|
|
||||||
return newSet;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if message has any Plain text content
|
|
||||||
const hasPlainText = (message: Message): boolean => {
|
|
||||||
return message.message_chain.some((c) => c.type === 'Plain');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract plain text from message chain
|
|
||||||
const getPlainText = (message: Message): string => {
|
|
||||||
return message.message_chain
|
|
||||||
.filter((c) => c.type === 'Plain')
|
|
||||||
.map((c) => (c as Plain).text)
|
|
||||||
.join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderMessageContent = (message: Message) => {
|
const renderMessageContent = (message: Message) => {
|
||||||
const key = getMessageKey(message);
|
|
||||||
const isRawMode = rawModeMessages.has(key);
|
|
||||||
|
|
||||||
// By default, render with markdown if there's plain text (unless raw mode is enabled)
|
|
||||||
if (!isRawMode && hasPlainText(message)) {
|
|
||||||
const plainText = getPlainText(message);
|
|
||||||
const nonPlainComponents = message.message_chain.filter(
|
|
||||||
(c) => c.type !== 'Plain' && c.type !== 'Source',
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="text-base leading-relaxed align-middle">
|
|
||||||
{/* Render non-Plain components first */}
|
|
||||||
{nonPlainComponents.map((component, index) =>
|
|
||||||
renderMessageComponent(component, index),
|
|
||||||
)}
|
|
||||||
{/* Render Plain text as markdown */}
|
|
||||||
<div className="markdown-body">
|
|
||||||
<ReactMarkdown
|
|
||||||
remarkPlugins={[remarkGfm]}
|
|
||||||
rehypePlugins={[
|
|
||||||
rehypeRaw,
|
|
||||||
rehypeHighlight,
|
|
||||||
rehypeSlug,
|
|
||||||
[
|
|
||||||
rehypeAutolinkHeadings,
|
|
||||||
{
|
|
||||||
behavior: 'wrap',
|
|
||||||
properties: {
|
|
||||||
className: ['anchor'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]}
|
|
||||||
components={{
|
|
||||||
ul: ({ children }) => <ul className="list-disc">{children}</ul>,
|
|
||||||
ol: ({ children }) => (
|
|
||||||
<ol className="list-decimal">{children}</ol>
|
|
||||||
),
|
|
||||||
li: ({ children }) => <li className="ml-4">{children}</li>,
|
|
||||||
img: ({ src, alt, ...props }) => {
|
|
||||||
const imageSrc = src || '';
|
|
||||||
|
|
||||||
if (typeof imageSrc !== 'string') {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
src={src}
|
|
||||||
alt={alt || ''}
|
|
||||||
className="max-w-full h-auto rounded-lg my-4"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
src={imageSrc}
|
|
||||||
alt={alt || ''}
|
|
||||||
className="max-w-lg h-auto my-4"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{plainText}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-base leading-relaxed align-middle whitespace-pre-wrap">
|
<div className="text-base leading-relaxed align-middle whitespace-pre-wrap">
|
||||||
{message.message_chain.map((component, index) =>
|
{message.message_chain.map((component, index) =>
|
||||||
@@ -734,109 +619,68 @@ export default function DebugDialog({
|
|||||||
<div
|
<div
|
||||||
key={message.id + message.timestamp}
|
key={message.id + message.timestamp}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex',
|
'flex group',
|
||||||
message.role === 'user' ? 'justify-end' : 'justify-start',
|
message.role === 'user' ? 'justify-end' : 'justify-start',
|
||||||
)}
|
)}
|
||||||
|
onMouseEnter={() => setHoveredMessageId(message.id)}
|
||||||
|
onMouseLeave={() => setHoveredMessageId(null)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'max-w-3xl px-5 py-3 rounded-2xl',
|
'relative flex items-end gap-2',
|
||||||
message.role === 'user'
|
message.role === 'user' ? 'flex-row-reverse' : 'flex-row',
|
||||||
? 'user-message-bubble bg-blue-100 dark:bg-blue-900 text-gray-900 dark:text-gray-100 rounded-br-none'
|
|
||||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-bl-none',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{renderMessageContent(message)}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-xs mt-2 flex items-center justify-between gap-2',
|
'max-w-md px-5 py-3 rounded-2xl',
|
||||||
message.role === 'user'
|
message.role === 'user'
|
||||||
? 'text-gray-600 dark:text-gray-300'
|
? 'bg-[#2288ee] text-white rounded-br-none'
|
||||||
: 'text-gray-500 dark:text-gray-400',
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-bl-none',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
{renderMessageContent(message)}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'text-xs mt-2 flex items-center justify-between gap-2',
|
||||||
|
message.role === 'user'
|
||||||
|
? 'text-white/70'
|
||||||
|
: 'text-gray-500 dark:text-gray-400',
|
||||||
|
)}
|
||||||
|
>
|
||||||
<span>
|
<span>
|
||||||
{message.role === 'user'
|
{message.role === 'user'
|
||||||
? t('pipelines.debugDialog.userMessage')
|
? t('pipelines.debugDialog.userMessage')
|
||||||
: t('pipelines.debugDialog.botMessage')}
|
: t('pipelines.debugDialog.botMessage')}
|
||||||
</span>
|
</span>
|
||||||
{hasPlainText(message) && (
|
<span className="text-[10px]">
|
||||||
<button
|
{formatTimestamp(getMessageTimestamp(message))}
|
||||||
onClick={() => toggleRawMode(message)}
|
</span>
|
||||||
className={cn(
|
|
||||||
'px-1.5 py-0.5 rounded text-[10px] transition-colors',
|
|
||||||
message.role === 'user'
|
|
||||||
? 'hover:bg-blue-200 dark:hover:bg-blue-800'
|
|
||||||
: 'hover:bg-gray-200 dark:hover:bg-gray-700',
|
|
||||||
)}
|
|
||||||
title={
|
|
||||||
rawModeMessages.has(getMessageKey(message))
|
|
||||||
? t('pipelines.debugDialog.showMarkdown')
|
|
||||||
: t('pipelines.debugDialog.showRaw')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{rawModeMessages.has(getMessageKey(message)) ? (
|
|
||||||
<span className="flex items-center gap-0.5">
|
|
||||||
<svg
|
|
||||||
className="w-3 h-3"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5z" />
|
|
||||||
</svg>
|
|
||||||
MD
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="flex items-center gap-0.5">
|
|
||||||
<svg
|
|
||||||
className="w-3 h-3"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M4 6h16M4 12h16M4 18h7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{t('pipelines.debugDialog.showRaw')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => setQuotedMessage(message)}
|
|
||||||
className={cn(
|
|
||||||
'px-1.5 py-0.5 rounded text-[10px] transition-colors flex items-center gap-0.5',
|
|
||||||
message.role === 'user'
|
|
||||||
? 'hover:bg-blue-200 dark:hover:bg-blue-800'
|
|
||||||
: 'hover:bg-gray-200 dark:hover:bg-gray-700',
|
|
||||||
)}
|
|
||||||
title={t('pipelines.debugDialog.reply')}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-3 h-3"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{t('pipelines.debugDialog.reply')}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px]">
|
|
||||||
{formatTimestamp(getMessageTimestamp(message))}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
{hoveredMessageId === message.id && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 px-2 text-xs opacity-0 group-hover:opacity-100 transition-opacity bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 whitespace-nowrap"
|
||||||
|
onClick={() => setQuotedMessage(message)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-3 h-3 mr-1"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{t('pipelines.debugDialog.reply')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import rehypeHighlight from 'rehype-highlight';
|
|||||||
import rehypeSlug from 'rehype-slug';
|
import rehypeSlug from 'rehype-slug';
|
||||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
||||||
import { getAPILanguageCode } from '@/i18n/I18nProvider';
|
import { getAPILanguageCode } from '@/i18n/I18nProvider';
|
||||||
import '@/styles/github-markdown.css';
|
import './github-markdown.css';
|
||||||
|
|
||||||
export default function PluginReadme({
|
export default function PluginReadme({
|
||||||
pluginAuthor,
|
pluginAuthor,
|
||||||
|
|||||||
@@ -24,9 +24,6 @@ import {
|
|||||||
Power,
|
Power,
|
||||||
Github,
|
Github,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
Code,
|
|
||||||
Copy,
|
|
||||||
Bug,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -41,11 +38,6 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from '@/components/ui/popover';
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
@@ -113,11 +105,6 @@ export default function PluginConfigPage() {
|
|||||||
);
|
);
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
const [refreshKey, setRefreshKey] = useState(0);
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
const [debugInfo, setDebugInfo] = useState<{
|
|
||||||
debug_url: string;
|
|
||||||
plugin_debug_key: string;
|
|
||||||
} | null>(null);
|
|
||||||
const [debugPopoverOpen, setDebugPopoverOpen] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPluginSystemStatus = async () => {
|
const fetchPluginSystemStatus = async () => {
|
||||||
@@ -387,22 +374,6 @@ export default function PluginConfigPage() {
|
|||||||
[uploadPluginFile, isPluginSystemReady, t],
|
[uploadPluginFile, isPluginSystemReady, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleShowDebugInfo = async () => {
|
|
||||||
try {
|
|
||||||
const info = await httpClient.getPluginDebugInfo();
|
|
||||||
setDebugInfo(info);
|
|
||||||
setDebugPopoverOpen(true);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch debug info:', error);
|
|
||||||
toast.error(t('plugins.failedToGetDebugInfo'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopyDebugInfo = (text: string) => {
|
|
||||||
navigator.clipboard.writeText(text);
|
|
||||||
toast.success(t('plugins.copiedToClipboard'));
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPluginDisabledState = () => (
|
const renderPluginDisabledState = () => (
|
||||||
<div className="flex flex-col items-center justify-center h-[60vh] text-center pt-[10vh]">
|
<div className="flex flex-col items-center justify-center h-[60vh] text-center pt-[10vh]">
|
||||||
<Power className="w-16 h-16 text-gray-400 mb-4" />
|
<Power className="w-16 h-16 text-gray-400 mb-4" />
|
||||||
@@ -495,92 +466,7 @@ export default function PluginConfigPage() {
|
|||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end items-center gap-2">
|
<div className="flex flex-row justify-end items-center">
|
||||||
{activeTab === 'installed' && (
|
|
||||||
<Popover
|
|
||||||
open={debugPopoverOpen}
|
|
||||||
onOpenChange={setDebugPopoverOpen}
|
|
||||||
>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="px-4 py-5 cursor-pointer"
|
|
||||||
onClick={handleShowDebugInfo}
|
|
||||||
>
|
|
||||||
<Code className="w-4 h-4 mr-2" />
|
|
||||||
{t('plugins.debugInfo')}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-[380px]" align="end">
|
|
||||||
<div className="space-y-3">
|
|
||||||
{/* Header with icon and title */}
|
|
||||||
<div className="flex items-center gap-2 pb-2 border-b">
|
|
||||||
<Bug className="w-4 h-4" />
|
|
||||||
<h4 className="font-semibold text-sm">
|
|
||||||
{t('plugins.debugInfoTitle')}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Debug URL row */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label className="text-sm font-medium whitespace-nowrap min-w-[50px]">
|
|
||||||
{t('plugins.debugUrl')}:
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={debugInfo?.debug_url || ''}
|
|
||||||
readOnly
|
|
||||||
className="w-[220px] font-mono text-xs h-8"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 shrink-0"
|
|
||||||
onClick={() =>
|
|
||||||
handleCopyDebugInfo(debugInfo?.debug_url || '')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Copy className="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Debug Key row */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label className="text-sm font-medium whitespace-nowrap min-w-[50px]">
|
|
||||||
{t('plugins.debugKey')}:
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={
|
|
||||||
debugInfo?.plugin_debug_key ||
|
|
||||||
t('plugins.noDebugKey')
|
|
||||||
}
|
|
||||||
readOnly
|
|
||||||
className="w-[220px] font-mono text-xs h-8"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 shrink-0"
|
|
||||||
onClick={() =>
|
|
||||||
handleCopyDebugInfo(
|
|
||||||
debugInfo?.plugin_debug_key || '',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={!debugInfo?.plugin_debug_key}
|
|
||||||
>
|
|
||||||
<Copy className="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{!debugInfo?.plugin_debug_key && (
|
|
||||||
<p className="text-xs text-muted-foreground ml-[58px]">
|
|
||||||
{t('plugins.debugKeyDisabled')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
)}
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="default" className="px-6 py-4 cursor-pointer">
|
<Button variant="default" className="px-6 py-4 cursor-pointer">
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ export interface KnowledgeBase {
|
|||||||
description: string;
|
description: string;
|
||||||
embedding_model_uuid: string;
|
embedding_model_uuid: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
|
||||||
top_k: number;
|
top_k: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +142,6 @@ export interface Bot {
|
|||||||
use_pipeline_uuid?: string;
|
use_pipeline_uuid?: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
adapter_runtime_values?: object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiRespKnowledgeBases {
|
export interface ApiRespKnowledgeBases {
|
||||||
|
|||||||
@@ -639,13 +639,6 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
return this.get('/api/v1/system/status/plugin-system');
|
return this.get('/api/v1/system/status/plugin-system');
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPluginDebugInfo(): Promise<{
|
|
||||||
debug_url: string;
|
|
||||||
plugin_debug_key: string;
|
|
||||||
}> {
|
|
||||||
return this.get('/api/v1/plugins/debug-info');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ User API ============
|
// ============ User API ============
|
||||||
public checkIfInited(): Promise<{ initialized: boolean }> {
|
public checkIfInited(): Promise<{ initialized: boolean }> {
|
||||||
return this.get('/api/v1/user/init');
|
return this.get('/api/v1/user/init');
|
||||||
|
|||||||
@@ -79,10 +79,8 @@ export class WebSocketClient {
|
|||||||
// 构建WebSocket URL
|
// 构建WebSocket URL
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
// extract host from process.env.NEXT_PUBLIC_API_BASE_URL
|
// extract host from process.env.NEXT_PUBLIC_API_BASE_URL
|
||||||
// 如果环境变量未定义,使用当前页面的 host (适配生产环境)
|
|
||||||
const host =
|
const host =
|
||||||
process.env.NEXT_PUBLIC_API_BASE_URL?.split('://')[1] ||
|
process.env.NEXT_PUBLIC_API_BASE_URL?.split('://')[1] || '';
|
||||||
window.location.host;
|
|
||||||
const url = `${protocol}//${host}/api/v1/pipelines/${this.pipelineId}/ws/connect?session_type=${this.sessionType}`;
|
const url = `${protocol}//${host}/api/v1/pipelines/${this.pipelineId}/ws/connect?session_type=${this.sessionType}`;
|
||||||
|
|
||||||
this.ws = new WebSocket(url);
|
this.ws = new WebSocket(url);
|
||||||
@@ -151,28 +149,12 @@ export class WebSocketClient {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'response':
|
case 'response':
|
||||||
// 检查 session_type 是否匹配 - 如果消息没有 session_type 或者不匹配当前session,都忽略
|
|
||||||
if (!data.session_type || data.session_type !== this.sessionType) {
|
|
||||||
// 忽略不匹配的 session_type 消息
|
|
||||||
console.debug(
|
|
||||||
`忽略不匹配的消息: 当前session=${this.sessionType}, 消息session=${data.session_type}`,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (data.data) {
|
if (data.data) {
|
||||||
this.onMessageCallback?.(data.data);
|
this.onMessageCallback?.(data.data);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'user_message':
|
case 'user_message':
|
||||||
// 检查 session_type 是否匹配 - 如果消息没有 session_type 或者不匹配当前session,都忽略
|
|
||||||
if (!data.session_type || data.session_type !== this.sessionType) {
|
|
||||||
// 忽略不匹配的 session_type 消息
|
|
||||||
console.debug(
|
|
||||||
`忽略不匹配的用户消息: 当前session=${this.sessionType}, 消息session=${data.session_type}`,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// 用户消息广播(包括自己发送的消息)
|
// 用户消息广播(包括自己发送的消息)
|
||||||
if (data.data) {
|
if (data.data) {
|
||||||
this.onMessageCallback?.(data.data);
|
this.onMessageCallback?.(data.data);
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ const enUS = {
|
|||||||
addRound: 'Add Round',
|
addRound: 'Add Round',
|
||||||
copy: 'Copy',
|
copy: 'Copy',
|
||||||
copySuccess: 'Copy Successfully',
|
copySuccess: 'Copy Successfully',
|
||||||
copyFailed: 'Copy Failed',
|
|
||||||
test: 'Test',
|
test: 'Test',
|
||||||
forgotPassword: 'Forgot Password?',
|
forgotPassword: 'Forgot Password?',
|
||||||
loading: 'Loading...',
|
loading: 'Loading...',
|
||||||
@@ -188,10 +187,6 @@ const enUS = {
|
|||||||
log: 'Log',
|
log: 'Log',
|
||||||
configuration: 'Configuration',
|
configuration: 'Configuration',
|
||||||
logs: 'Logs',
|
logs: 'Logs',
|
||||||
webhookUrl: 'Webhook Callback URL',
|
|
||||||
webhookUrlCopied: 'Webhook URL copied',
|
|
||||||
webhookUrlHint:
|
|
||||||
'Click the input to select all, then press Ctrl+C (Mac: Cmd+C) to copy, or click the button',
|
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: 'Extensions',
|
title: 'Extensions',
|
||||||
@@ -236,15 +231,6 @@ const enUS = {
|
|||||||
failedToGetStatus: 'Failed to get plugin system status',
|
failedToGetStatus: 'Failed to get plugin system status',
|
||||||
pluginSystemNotReady:
|
pluginSystemNotReady:
|
||||||
'Plugin system is not ready, cannot perform this operation',
|
'Plugin system is not ready, cannot perform this operation',
|
||||||
debugInfo: 'Debug Info',
|
|
||||||
debugInfoTitle: 'Plugin Debug Information',
|
|
||||||
debugUrl: 'Debug URL',
|
|
||||||
debugKey: 'Debug Key',
|
|
||||||
noDebugKey: '(Not Set)',
|
|
||||||
debugKeyDisabled:
|
|
||||||
'Debug key is not set, plugin debugging does not require authentication',
|
|
||||||
failedToGetDebugInfo: 'Failed to get debug information',
|
|
||||||
copiedToClipboard: 'Copied to clipboard',
|
|
||||||
deleting: 'Deleting...',
|
deleting: 'Deleting...',
|
||||||
deletePlugin: 'Delete Plugin',
|
deletePlugin: 'Delete Plugin',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
@@ -539,8 +525,6 @@ const enUS = {
|
|||||||
imageUploadFailed: 'Image upload failed',
|
imageUploadFailed: 'Image upload failed',
|
||||||
reply: 'Reply',
|
reply: 'Reply',
|
||||||
replyTo: 'Reply to',
|
replyTo: 'Reply to',
|
||||||
showMarkdown: 'Show Markdown',
|
|
||||||
showRaw: 'Show Raw',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
knowledge: {
|
knowledge: {
|
||||||
@@ -619,8 +603,6 @@ const enUS = {
|
|||||||
retriever: 'Retriever',
|
retriever: 'Retriever',
|
||||||
selectRetriever: 'Select a retriever...',
|
selectRetriever: 'Select a retriever...',
|
||||||
retrieverConfiguration: 'Retriever Configuration',
|
retrieverConfiguration: 'Retriever Configuration',
|
||||||
retrieverInstallInfo: 'You can install Knowledge Retriever plugins from',
|
|
||||||
retrieverMarketLink: 'here',
|
|
||||||
},
|
},
|
||||||
register: {
|
register: {
|
||||||
title: 'Initialize LangBot 👋',
|
title: 'Initialize LangBot 👋',
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ const jaJP = {
|
|||||||
addRound: 'ラウンドを追加',
|
addRound: 'ラウンドを追加',
|
||||||
copy: 'コピー',
|
copy: 'コピー',
|
||||||
copySuccess: 'コピーに成功しました',
|
copySuccess: 'コピーに成功しました',
|
||||||
copyFailed: 'コピーに失敗しました',
|
|
||||||
test: 'テスト',
|
test: 'テスト',
|
||||||
forgotPassword: 'パスワードを忘れた?',
|
forgotPassword: 'パスワードを忘れた?',
|
||||||
loading: '読み込み中...',
|
loading: '読み込み中...',
|
||||||
@@ -190,10 +189,6 @@ const jaJP = {
|
|||||||
log: 'ログ',
|
log: 'ログ',
|
||||||
configuration: '設定',
|
configuration: '設定',
|
||||||
logs: 'ログ',
|
logs: 'ログ',
|
||||||
webhookUrl: 'Webhook コールバック URL',
|
|
||||||
webhookUrlCopied: 'Webhook URL をコピーしました',
|
|
||||||
webhookUrlHint:
|
|
||||||
'入力ボックスをクリックして全選択し、Ctrl+C (Mac: Cmd+C) でコピーするか、右側のボタンをクリックしてください',
|
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: '拡張機能',
|
title: '拡張機能',
|
||||||
@@ -238,15 +233,6 @@ const jaJP = {
|
|||||||
failedToGetStatus: 'プラグインシステム状態の取得に失敗しました',
|
failedToGetStatus: 'プラグインシステム状態の取得に失敗しました',
|
||||||
pluginSystemNotReady:
|
pluginSystemNotReady:
|
||||||
'プラグインシステムが準備されていません。この操作を実行できません',
|
'プラグインシステムが準備されていません。この操作を実行できません',
|
||||||
debugInfo: 'デバッグ情報',
|
|
||||||
debugInfoTitle: 'プラグインデバッグ情報',
|
|
||||||
debugUrl: 'デバッグURL',
|
|
||||||
debugKey: 'デバッグキー',
|
|
||||||
noDebugKey: '(未設定)',
|
|
||||||
debugKeyDisabled:
|
|
||||||
'デバッグキーが設定されていません。プラグインデバッグには認証が不要です',
|
|
||||||
failedToGetDebugInfo: 'デバッグ情報の取得に失敗しました',
|
|
||||||
copiedToClipboard: 'クリップボードにコピーしました',
|
|
||||||
deleting: '削除中...',
|
deleting: '削除中...',
|
||||||
deletePlugin: 'プラグインを削除',
|
deletePlugin: 'プラグインを削除',
|
||||||
cancel: 'キャンセル',
|
cancel: 'キャンセル',
|
||||||
@@ -543,8 +529,6 @@ const jaJP = {
|
|||||||
imageUploadFailed: '画像のアップロードに失敗しました',
|
imageUploadFailed: '画像のアップロードに失敗しました',
|
||||||
reply: '返信',
|
reply: '返信',
|
||||||
replyTo: '返信先',
|
replyTo: '返信先',
|
||||||
showMarkdown: 'Markdownで表示',
|
|
||||||
showRaw: '原文で表示',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
knowledge: {
|
knowledge: {
|
||||||
@@ -624,8 +608,6 @@ const jaJP = {
|
|||||||
retriever: '検索器',
|
retriever: '検索器',
|
||||||
selectRetriever: '検索器を選択...',
|
selectRetriever: '検索器を選択...',
|
||||||
retrieverConfiguration: '検索器設定',
|
retrieverConfiguration: '検索器設定',
|
||||||
retrieverInstallInfo: 'ナレッジ検索器プラグインは',
|
|
||||||
retrieverMarketLink: 'こちらからインストールできます',
|
|
||||||
},
|
},
|
||||||
register: {
|
register: {
|
||||||
title: 'LangBot を初期化 👋',
|
title: 'LangBot を初期化 👋',
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ const zhHans = {
|
|||||||
addRound: '添加回合',
|
addRound: '添加回合',
|
||||||
copy: '复制',
|
copy: '复制',
|
||||||
copySuccess: '复制成功',
|
copySuccess: '复制成功',
|
||||||
copyFailed: '复制失败',
|
|
||||||
test: '测试',
|
test: '测试',
|
||||||
forgotPassword: '忘记密码?',
|
forgotPassword: '忘记密码?',
|
||||||
loading: '加载中...',
|
loading: '加载中...',
|
||||||
@@ -183,10 +182,6 @@ const zhHans = {
|
|||||||
log: '日志',
|
log: '日志',
|
||||||
configuration: '配置',
|
configuration: '配置',
|
||||||
logs: '日志',
|
logs: '日志',
|
||||||
webhookUrl: 'Webhook 回调地址',
|
|
||||||
webhookUrlCopied: 'Webhook 地址已复制',
|
|
||||||
webhookUrlHint:
|
|
||||||
'点击输入框自动全选,然后按 Ctrl+C (Mac: Cmd+C) 复制,或点击右侧按钮',
|
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: '插件扩展',
|
title: '插件扩展',
|
||||||
@@ -227,14 +222,6 @@ const zhHans = {
|
|||||||
loadingStatus: '正在检查插件系统状态...',
|
loadingStatus: '正在检查插件系统状态...',
|
||||||
failedToGetStatus: '获取插件系统状态失败',
|
failedToGetStatus: '获取插件系统状态失败',
|
||||||
pluginSystemNotReady: '插件系统未就绪,无法执行此操作',
|
pluginSystemNotReady: '插件系统未就绪,无法执行此操作',
|
||||||
debugInfo: '调试信息',
|
|
||||||
debugInfoTitle: '插件调试信息',
|
|
||||||
debugUrl: '调试地址',
|
|
||||||
debugKey: '调试密钥',
|
|
||||||
noDebugKey: '(未设置)',
|
|
||||||
debugKeyDisabled: '未设置调试密钥,插件调试无需认证',
|
|
||||||
failedToGetDebugInfo: '获取调试信息失败',
|
|
||||||
copiedToClipboard: '已复制到剪贴板',
|
|
||||||
deleting: '删除中...',
|
deleting: '删除中...',
|
||||||
deletePlugin: '删除插件',
|
deletePlugin: '删除插件',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
@@ -521,8 +508,6 @@ const zhHans = {
|
|||||||
imageUploadFailed: '图片上传失败',
|
imageUploadFailed: '图片上传失败',
|
||||||
reply: '回复',
|
reply: '回复',
|
||||||
replyTo: '回复给',
|
replyTo: '回复给',
|
||||||
showMarkdown: '渲染',
|
|
||||||
showRaw: '原文',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
knowledge: {
|
knowledge: {
|
||||||
@@ -596,8 +581,6 @@ const zhHans = {
|
|||||||
retriever: '检索器',
|
retriever: '检索器',
|
||||||
selectRetriever: '选择一个检索器...',
|
selectRetriever: '选择一个检索器...',
|
||||||
retrieverConfiguration: '检索器配置',
|
retrieverConfiguration: '检索器配置',
|
||||||
retrieverInstallInfo: '您可以从',
|
|
||||||
retrieverMarketLink: '此处安装知识检索器插件',
|
|
||||||
},
|
},
|
||||||
register: {
|
register: {
|
||||||
title: '初始化 LangBot 👋',
|
title: '初始化 LangBot 👋',
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ const zhHant = {
|
|||||||
addRound: '新增回合',
|
addRound: '新增回合',
|
||||||
copy: '複製',
|
copy: '複製',
|
||||||
copySuccess: '複製成功',
|
copySuccess: '複製成功',
|
||||||
copyFailed: '複製失敗',
|
|
||||||
test: '測試',
|
test: '測試',
|
||||||
forgotPassword: '忘記密碼?',
|
forgotPassword: '忘記密碼?',
|
||||||
loading: '載入中...',
|
loading: '載入中...',
|
||||||
@@ -183,10 +182,6 @@ const zhHant = {
|
|||||||
log: '日誌',
|
log: '日誌',
|
||||||
configuration: '設定',
|
configuration: '設定',
|
||||||
logs: '日誌',
|
logs: '日誌',
|
||||||
webhookUrl: 'Webhook 回調位址',
|
|
||||||
webhookUrlCopied: 'Webhook 位址已複製',
|
|
||||||
webhookUrlHint:
|
|
||||||
'點擊輸入框自動全選,然後按 Ctrl+C (Mac: Cmd+C) 複製,或點擊右側按鈕',
|
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: '外掛擴展',
|
title: '外掛擴展',
|
||||||
@@ -227,14 +222,6 @@ const zhHant = {
|
|||||||
loadingStatus: '正在檢查外掛系統狀態...',
|
loadingStatus: '正在檢查外掛系統狀態...',
|
||||||
failedToGetStatus: '取得外掛系統狀態失敗',
|
failedToGetStatus: '取得外掛系統狀態失敗',
|
||||||
pluginSystemNotReady: '外掛系統未就緒,無法執行此操作',
|
pluginSystemNotReady: '外掛系統未就緒,無法執行此操作',
|
||||||
debugInfo: '偵錯資訊',
|
|
||||||
debugInfoTitle: '外掛偵錯資訊',
|
|
||||||
debugUrl: '偵錯位址',
|
|
||||||
debugKey: '偵錯金鑰',
|
|
||||||
noDebugKey: '(未設定)',
|
|
||||||
debugKeyDisabled: '未設定偵錯金鑰,外掛偵錯無需認證',
|
|
||||||
failedToGetDebugInfo: '取得偵錯資訊失敗',
|
|
||||||
copiedToClipboard: '已複製到剪貼簿',
|
|
||||||
deleting: '刪除中...',
|
deleting: '刪除中...',
|
||||||
deletePlugin: '刪除外掛',
|
deletePlugin: '刪除外掛',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
@@ -519,8 +506,6 @@ const zhHant = {
|
|||||||
imageUploadFailed: '圖片上傳失敗',
|
imageUploadFailed: '圖片上傳失敗',
|
||||||
reply: '回覆',
|
reply: '回覆',
|
||||||
replyTo: '回覆給',
|
replyTo: '回覆給',
|
||||||
showMarkdown: '渲染',
|
|
||||||
showRaw: '原文',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
knowledge: {
|
knowledge: {
|
||||||
@@ -594,8 +579,6 @@ const zhHant = {
|
|||||||
retriever: '檢索器',
|
retriever: '檢索器',
|
||||||
selectRetriever: '選擇一個檢索器...',
|
selectRetriever: '選擇一個檢索器...',
|
||||||
retrieverConfiguration: '檢索器配置',
|
retrieverConfiguration: '檢索器配置',
|
||||||
retrieverInstallInfo: '您可以從',
|
|
||||||
retrieverMarketLink: '此處安裝知識檢索器插件',
|
|
||||||
},
|
},
|
||||||
register: {
|
register: {
|
||||||
title: '初始化 LangBot 👋',
|
title: '初始化 LangBot 👋',
|
||||||
|
|||||||
Reference in New Issue
Block a user