mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-09 07:16:04 +00:00
Feat/unified webhook (#1793)
* fix: wecombot id * feat: add unified webhook for wecom * feat: add support for wecombot,wxoa,slack and qqo * fix: slack adapter * feat: qqo * fix: errors when npm lint * fix: qqo webhook * feat: add wecomcs * fix: modify wecomcs * fix: import errors * feat: add configurable webhook display prefix (#1797) * Initial plan * Add webhook_display_prefix configuration option Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * perf: change config field name --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> Co-authored-by: Junyan Qin <rockchinq@gmail.com> * feat: finish the fxxking line adapter --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Junyan Qin <rockchinq@gmail.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
IChooseAdapterEntity,
|
||||
IPipelineEntity,
|
||||
@@ -112,11 +112,86 @@ export default function BotForm({
|
||||
IDynamicFormItemSchema[]
|
||||
>([]);
|
||||
const [, setIsLoading] = useState<boolean>(false);
|
||||
const [webhookUrl, setWebhookUrl] = useState<string>('');
|
||||
const webhookInputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
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() {
|
||||
initBotFormComponent().then(() => {
|
||||
// 拉取初始化表单信息
|
||||
@@ -131,12 +206,20 @@ export default function BotForm({
|
||||
form.setValue('use_pipeline_uuid', val.use_pipeline_uuid || '');
|
||||
handleAdapterSelect(val.adapter);
|
||||
// dynamicForm.setFieldsValue(val.adapter_config);
|
||||
|
||||
// 设置 webhook 地址(如果有)
|
||||
if (val.webhook_full_url) {
|
||||
setWebhookUrl(val.webhook_full_url);
|
||||
} else {
|
||||
setWebhookUrl('');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(t('bots.getBotConfigError') + err.message);
|
||||
});
|
||||
} else {
|
||||
form.reset();
|
||||
setWebhookUrl('');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -209,7 +292,7 @@ export default function BotForm({
|
||||
|
||||
async function getBotConfig(
|
||||
botId: string,
|
||||
): Promise<z.infer<typeof formSchema>> {
|
||||
): Promise<z.infer<typeof formSchema> & { webhook_full_url?: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
httpClient
|
||||
.getBot(botId)
|
||||
@@ -222,6 +305,10 @@ export default function BotForm({
|
||||
adapter_config: bot.adapter_config,
|
||||
enable: bot.enable ?? true,
|
||||
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) => {
|
||||
@@ -360,51 +447,86 @@ export default function BotForm({
|
||||
<div className="space-y-4">
|
||||
{/* 是否启用 & 绑定流水线 仅在编辑模式 */}
|
||||
{initBotId && (
|
||||
<div className="flex items-center gap-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enable"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-start gap-[0.8rem] h-[3.8rem]">
|
||||
<FormLabel>{t('common.enable')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<>
|
||||
<div className="flex items-center gap-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enable"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-start gap-[0.8rem] h-[3.8rem]">
|
||||
<FormLabel>{t('common.enable')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="use_pipeline_uuid"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-start gap-[0.8rem] h-[3.8rem]">
|
||||
<FormLabel>{t('bots.bindPipeline')}</FormLabel>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} {...field}>
|
||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue
|
||||
placeholder={t('bots.selectPipeline')}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="fixed z-[1000]">
|
||||
<SelectGroup>
|
||||
{pipelineNameList.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="use_pipeline_uuid"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-start gap-[0.8rem] h-[3.8rem]">
|
||||
<FormLabel>{t('bots.bindPipeline')}</FormLabel>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} {...field}>
|
||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue
|
||||
placeholder={t('bots.selectPipeline')}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="fixed z-[1000]">
|
||||
<SelectGroup>
|
||||
{pipelineNameList.map((item) => (
|
||||
<SelectItem
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</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
|
||||
|
||||
@@ -143,6 +143,7 @@ export interface Bot {
|
||||
use_pipeline_uuid?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
adapter_runtime_values?: object;
|
||||
}
|
||||
|
||||
export interface ApiRespKnowledgeBases {
|
||||
|
||||
@@ -41,6 +41,7 @@ const enUS = {
|
||||
addRound: 'Add Round',
|
||||
copy: 'Copy',
|
||||
copySuccess: 'Copy Successfully',
|
||||
copyFailed: 'Copy Failed',
|
||||
test: 'Test',
|
||||
forgotPassword: 'Forgot Password?',
|
||||
loading: 'Loading...',
|
||||
@@ -187,6 +188,10 @@ const enUS = {
|
||||
log: 'Log',
|
||||
configuration: 'Configuration',
|
||||
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: {
|
||||
title: 'Extensions',
|
||||
|
||||
@@ -42,6 +42,7 @@ const jaJP = {
|
||||
addRound: 'ラウンドを追加',
|
||||
copy: 'コピー',
|
||||
copySuccess: 'コピーに成功しました',
|
||||
copyFailed: 'コピーに失敗しました',
|
||||
test: 'テスト',
|
||||
forgotPassword: 'パスワードを忘れた?',
|
||||
loading: '読み込み中...',
|
||||
@@ -189,6 +190,10 @@ const jaJP = {
|
||||
log: 'ログ',
|
||||
configuration: '設定',
|
||||
logs: 'ログ',
|
||||
webhookUrl: 'Webhook コールバック URL',
|
||||
webhookUrlCopied: 'Webhook URL をコピーしました',
|
||||
webhookUrlHint:
|
||||
'入力ボックスをクリックして全選択し、Ctrl+C (Mac: Cmd+C) でコピーするか、右側のボタンをクリックしてください',
|
||||
},
|
||||
plugins: {
|
||||
title: '拡張機能',
|
||||
|
||||
@@ -41,6 +41,7 @@ const zhHans = {
|
||||
addRound: '添加回合',
|
||||
copy: '复制',
|
||||
copySuccess: '复制成功',
|
||||
copyFailed: '复制失败',
|
||||
test: '测试',
|
||||
forgotPassword: '忘记密码?',
|
||||
loading: '加载中...',
|
||||
@@ -182,6 +183,10 @@ const zhHans = {
|
||||
log: '日志',
|
||||
configuration: '配置',
|
||||
logs: '日志',
|
||||
webhookUrl: 'Webhook 回调地址',
|
||||
webhookUrlCopied: 'Webhook 地址已复制',
|
||||
webhookUrlHint:
|
||||
'点击输入框自动全选,然后按 Ctrl+C (Mac: Cmd+C) 复制,或点击右侧按钮',
|
||||
},
|
||||
plugins: {
|
||||
title: '插件扩展',
|
||||
|
||||
@@ -41,6 +41,7 @@ const zhHant = {
|
||||
addRound: '新增回合',
|
||||
copy: '複製',
|
||||
copySuccess: '複製成功',
|
||||
copyFailed: '複製失敗',
|
||||
test: '測試',
|
||||
forgotPassword: '忘記密碼?',
|
||||
loading: '載入中...',
|
||||
@@ -182,6 +183,10 @@ const zhHant = {
|
||||
log: '日誌',
|
||||
configuration: '設定',
|
||||
logs: '日誌',
|
||||
webhookUrl: 'Webhook 回調位址',
|
||||
webhookUrlCopied: 'Webhook 位址已複製',
|
||||
webhookUrlHint:
|
||||
'點擊輸入框自動全選,然後按 Ctrl+C (Mac: Cmd+C) 複製,或點擊右側按鈕',
|
||||
},
|
||||
plugins: {
|
||||
title: '外掛擴展',
|
||||
|
||||
Reference in New Issue
Block a user