mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-05 00:57:14 +00:00
feat: add unified webhook for wecom
This commit is contained in:
+38
-14
@@ -21,6 +21,7 @@ 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
|
||||||
@@ -31,13 +32,18 @@ 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',
|
# 只有在非统一模式下才注册独立路由
|
||||||
'handle_callback',
|
if not self.unified_mode:
|
||||||
self.handle_callback_request,
|
self.app.add_url_rule(
|
||||||
methods=['GET', 'POST'],
|
'/callback/command',
|
||||||
)
|
'handle_callback',
|
||||||
|
self.handle_callback_request,
|
||||||
|
methods=['GET', 'POST'],
|
||||||
|
)
|
||||||
|
|
||||||
self._message_handlers = {
|
self._message_handlers = {
|
||||||
'example': [],
|
'example': [],
|
||||||
}
|
}
|
||||||
@@ -168,25 +174,43 @@ 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:
|
||||||
|
响应数据
|
||||||
"""
|
"""
|
||||||
处理回调请求,包括 GET 验证和 POST 消息接收。
|
return await self._handle_callback_internal(req)
|
||||||
|
|
||||||
|
async def _handle_callback_internal(self, req):
|
||||||
|
"""
|
||||||
|
处理回调请求的内部实现,包括 GET 验证和 POST 消息接收。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
req: Quart Request 对象
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
msg_signature = request.args.get('msg_signature')
|
msg_signature = req.args.get('msg_signature')
|
||||||
timestamp = request.args.get('timestamp')
|
timestamp = req.args.get('timestamp')
|
||||||
nonce = request.args.get('nonce')
|
nonce = req.args.get('nonce')
|
||||||
|
|
||||||
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid)
|
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid)
|
||||||
if request.method == 'GET':
|
if req.method == 'GET':
|
||||||
echostr = request.args.get('echostr')
|
echostr = req.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 request.method == 'POST':
|
elif req.method == 'POST':
|
||||||
encrypt_msg = await request.data
|
encrypt_msg = await req.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('消息解密失败')
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ class BotsRouterGroup(group.RouterGroup):
|
|||||||
@self.route('/<bot_uuid>', methods=['GET', 'PUT', 'DELETE'])
|
@self.route('/<bot_uuid>', methods=['GET', 'PUT', 'DELETE'])
|
||||||
async def _(bot_uuid: str) -> str:
|
async def _(bot_uuid: str) -> str:
|
||||||
if quart.request.method == 'GET':
|
if quart.request.method == 'GET':
|
||||||
bot = await self.ap.bot_service.get_bot(bot_uuid)
|
# 返回运行时信息,包括webhook地址等
|
||||||
|
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})
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"""统一 Webhook 路由组
|
||||||
|
|
||||||
|
处理所有外部平台的回调请求,统一在一个端口上通过 bot_uuid 路由到对应的适配器。
|
||||||
|
|
||||||
|
路由格式:
|
||||||
|
- /bots/{bot_uuid} - 处理 bot 的 webhook 回调
|
||||||
|
- /bots/{bot_uuid}/{path} - 处理带子路径的 webhook 回调
|
||||||
|
|
||||||
|
Example:
|
||||||
|
http://your-server.com:5300/bots/550e8400-e29b-41d4-a716-446655440000
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import quart
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from .. import group
|
||||||
|
|
||||||
|
|
||||||
|
@group.group_class('webhooks', '/bots')
|
||||||
|
class WebhookRouterGroup(group.RouterGroup):
|
||||||
|
async def initialize(self) -> None:
|
||||||
|
@self.route('/<bot_uuid>', methods=['GET', 'POST'], auth_type=group.AuthType.NONE)
|
||||||
|
async def handle_webhook(bot_uuid: str):
|
||||||
|
"""处理 bot webhook 回调(无子路径)"""
|
||||||
|
return await self._dispatch_webhook(bot_uuid, '')
|
||||||
|
|
||||||
|
@self.route('/<bot_uuid>/<path:path>', methods=['GET', 'POST'], auth_type=group.AuthType.NONE)
|
||||||
|
async def handle_webhook_with_path(bot_uuid: str, path: str):
|
||||||
|
"""处理 bot webhook 回调(带子路径)"""
|
||||||
|
return await self._dispatch_webhook(bot_uuid, path)
|
||||||
|
|
||||||
|
async def _dispatch_webhook(self, bot_uuid: str, path: str):
|
||||||
|
"""分发 webhook 请求到对应的 bot adapter
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot_uuid: Bot 的 UUID
|
||||||
|
path: 子路径(如果有的话)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
适配器返回的响应
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 通过 UUID 获取运行时 bot
|
||||||
|
runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
|
||||||
|
|
||||||
|
if not runtime_bot:
|
||||||
|
return quart.jsonify({'error': 'Bot not found'}), 404
|
||||||
|
|
||||||
|
if not runtime_bot.enable:
|
||||||
|
return quart.jsonify({'error': 'Bot is disabled'}), 403
|
||||||
|
|
||||||
|
# 检查 adapter 是否支持统一 webhook
|
||||||
|
if not hasattr(runtime_bot.adapter, 'handle_unified_webhook'):
|
||||||
|
return quart.jsonify({'error': 'Adapter does not support unified webhook'}), 501
|
||||||
|
|
||||||
|
# 调用 adapter 的 handle_unified_webhook 方法,显式传递 request 对象
|
||||||
|
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,6 +58,17 @@ 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 的适配器生成 webhook URL
|
||||||
|
# 目前只有 wecom 支持
|
||||||
|
if persistence_bot['adapter'] == 'wecom':
|
||||||
|
api_port = self.ap.instance_config.data['api']['port']
|
||||||
|
webhook_url = f"/bots/{bot_uuid}"
|
||||||
|
adapter_runtime_values['webhook_url'] = webhook_url
|
||||||
|
adapter_runtime_values['webhook_full_url'] = f"http://<Your-Server-IP>:{api_port}{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
|
||||||
|
|||||||
@@ -232,6 +232,10 @@ 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()
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ 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,11 +143,12 @@ 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 对象
|
# 创建运行时 bot 对象,始终使用统一 webhook 模式
|
||||||
bot = WecomClient(
|
bot = WecomClient(
|
||||||
corpid=config['corpid'],
|
corpid=config['corpid'],
|
||||||
secret=config['secret'],
|
secret=config['secret'],
|
||||||
@@ -154,6 +156,7 @@ 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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -164,6 +167,9 @@ 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,
|
||||||
@@ -217,16 +223,41 @@ 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):
|
||||||
async def shutdown_trigger_placeholder():
|
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
||||||
|
# 保持运行但不启动独立端口
|
||||||
|
|
||||||
|
# 打印 webhook 回调地址
|
||||||
|
if self.bot_uuid and hasattr(self.logger, 'ap'):
|
||||||
|
try:
|
||||||
|
api_port = self.logger.ap.instance_config.data['api']['port']
|
||||||
|
webhook_url = f"http://127.0.0.1:{api_port}/bots/{self.bot_uuid}"
|
||||||
|
webhook_url_public = f"http://<Your-Public-IP>:{api_port}/bots/{self.bot_uuid}"
|
||||||
|
|
||||||
|
await self.logger.info(f"企业微信 Webhook 回调地址:")
|
||||||
|
await self.logger.info(f" 本地地址: {webhook_url}")
|
||||||
|
await self.logger.info(f" 公网地址: {webhook_url_public}")
|
||||||
|
await self.logger.info(f"请在企业微信后台配置此回调地址")
|
||||||
|
except Exception as e:
|
||||||
|
await self.logger.warning(f"无法生成 webhook URL: {e}")
|
||||||
|
|
||||||
|
async def keep_alive():
|
||||||
while True:
|
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,23 +11,6 @@ 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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IChooseAdapterEntity,
|
IChooseAdapterEntity,
|
||||||
IPipelineEntity,
|
IPipelineEntity,
|
||||||
@@ -112,12 +112,77 @@ 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();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 复制到剪贴板的辅助函数 - 使用页面上的真实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(() => {
|
||||||
// 拉取初始化表单信息
|
// 拉取初始化表单信息
|
||||||
@@ -133,12 +198,20 @@ export default function BotForm({
|
|||||||
console.log('form', form.getValues());
|
console.log('form', form.getValues());
|
||||||
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('');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -213,7 +286,7 @@ export default function BotForm({
|
|||||||
|
|
||||||
async function getBotConfig(
|
async function getBotConfig(
|
||||||
botId: string,
|
botId: string,
|
||||||
): Promise<z.infer<typeof formSchema>> {
|
): Promise<z.infer<typeof formSchema> & { webhook_full_url?: string }> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
httpClient
|
httpClient
|
||||||
.getBot(botId)
|
.getBot(botId)
|
||||||
@@ -226,6 +299,9 @@ 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 any).webhook_full_url
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -369,51 +445,83 @@ export default function BotForm({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 是否启用 & 绑定流水线 仅在编辑模式 */}
|
{/* 是否启用 & 绑定流水线 仅在编辑模式 */}
|
||||||
{initBotId && (
|
{initBotId && (
|
||||||
<div className="flex items-center gap-6">
|
<>
|
||||||
<FormField
|
<div className="flex items-center gap-6">
|
||||||
control={form.control}
|
<FormField
|
||||||
name="enable"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="enable"
|
||||||
<FormItem className="flex flex-col justify-start gap-[0.8rem] h-[3.8rem]">
|
render={({ field }) => (
|
||||||
<FormLabel>{t('common.enable')}</FormLabel>
|
<FormItem className="flex flex-col justify-start gap-[0.8rem] h-[3.8rem]">
|
||||||
<FormControl>
|
<FormLabel>{t('common.enable')}</FormLabel>
|
||||||
<Switch
|
<FormControl>
|
||||||
checked={field.value}
|
<Switch
|
||||||
onCheckedChange={field.onChange}
|
checked={field.value}
|
||||||
/>
|
onCheckedChange={field.onChange}
|
||||||
</FormControl>
|
/>
|
||||||
</FormItem>
|
</FormControl>
|
||||||
)}
|
</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 key={item.value} value={item.value}>
|
<SelectItem key={item.value} value={item.value}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 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
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ 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 {
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ const enUS = {
|
|||||||
deleteSuccess: 'Deleted successfully',
|
deleteSuccess: 'Deleted successfully',
|
||||||
deleteError: 'Delete failed: ',
|
deleteError: 'Delete failed: ',
|
||||||
addRound: 'Add Round',
|
addRound: 'Add Round',
|
||||||
|
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...',
|
||||||
@@ -149,6 +151,9 @@ 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: 'Plugins',
|
title: 'Plugins',
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ const jaJP = {
|
|||||||
deleteSuccess: '削除に成功しました',
|
deleteSuccess: '削除に成功しました',
|
||||||
deleteError: '削除に失敗しました:',
|
deleteError: '削除に失敗しました:',
|
||||||
addRound: 'ラウンドを追加',
|
addRound: 'ラウンドを追加',
|
||||||
|
copy: 'コピー',
|
||||||
copySuccess: 'コピーに成功しました',
|
copySuccess: 'コピーに成功しました',
|
||||||
|
copyFailed: 'コピーに失敗しました',
|
||||||
test: 'テスト',
|
test: 'テスト',
|
||||||
forgotPassword: 'パスワードを忘れた?',
|
forgotPassword: 'パスワードを忘れた?',
|
||||||
loading: '読み込み中...',
|
loading: '読み込み中...',
|
||||||
@@ -151,6 +153,9 @@ const jaJP = {
|
|||||||
log: 'ログ',
|
log: 'ログ',
|
||||||
configuration: '設定',
|
configuration: '設定',
|
||||||
logs: 'ログ',
|
logs: 'ログ',
|
||||||
|
webhookUrl: 'Webhook コールバック URL',
|
||||||
|
webhookUrlCopied: 'Webhook URL をコピーしました',
|
||||||
|
webhookUrlHint: '入力ボックスをクリックして全選択し、Ctrl+C (Mac: Cmd+C) でコピーするか、右側のボタンをクリックしてください',
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: 'プラグイン',
|
title: 'プラグイン',
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ const zhHans = {
|
|||||||
deleteSuccess: '删除成功',
|
deleteSuccess: '删除成功',
|
||||||
deleteError: '删除失败:',
|
deleteError: '删除失败:',
|
||||||
addRound: '添加回合',
|
addRound: '添加回合',
|
||||||
|
copy: '复制',
|
||||||
copySuccess: '复制成功',
|
copySuccess: '复制成功',
|
||||||
|
copyFailed: '复制失败',
|
||||||
test: '测试',
|
test: '测试',
|
||||||
forgotPassword: '忘记密码?',
|
forgotPassword: '忘记密码?',
|
||||||
loading: '加载中...',
|
loading: '加载中...',
|
||||||
@@ -146,6 +148,9 @@ const zhHans = {
|
|||||||
log: '日志',
|
log: '日志',
|
||||||
configuration: '配置',
|
configuration: '配置',
|
||||||
logs: '日志',
|
logs: '日志',
|
||||||
|
webhookUrl: 'Webhook 回调地址',
|
||||||
|
webhookUrlCopied: 'Webhook 地址已复制',
|
||||||
|
webhookUrlHint: '点击输入框自动全选,然后按 Ctrl+C (Mac: Cmd+C) 复制,或点击右侧按钮',
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: '插件管理',
|
title: '插件管理',
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ const zhHant = {
|
|||||||
deleteSuccess: '刪除成功',
|
deleteSuccess: '刪除成功',
|
||||||
deleteError: '刪除失敗:',
|
deleteError: '刪除失敗:',
|
||||||
addRound: '新增回合',
|
addRound: '新增回合',
|
||||||
|
copy: '複製',
|
||||||
copySuccess: '複製成功',
|
copySuccess: '複製成功',
|
||||||
|
copyFailed: '複製失敗',
|
||||||
test: '測試',
|
test: '測試',
|
||||||
forgotPassword: '忘記密碼?',
|
forgotPassword: '忘記密碼?',
|
||||||
loading: '載入中...',
|
loading: '載入中...',
|
||||||
@@ -146,6 +148,9 @@ const zhHant = {
|
|||||||
log: '日誌',
|
log: '日誌',
|
||||||
configuration: '設定',
|
configuration: '設定',
|
||||||
logs: '日誌',
|
logs: '日誌',
|
||||||
|
webhookUrl: 'Webhook 回調位址',
|
||||||
|
webhookUrlCopied: 'Webhook 位址已複製',
|
||||||
|
webhookUrlHint: '點擊輸入框自動全選,然後按 Ctrl+C (Mac: Cmd+C) 複製,或點擊右側按鈕',
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: '外掛管理',
|
title: '外掛管理',
|
||||||
|
|||||||
Reference in New Issue
Block a user