mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-10 15:56:03 +00:00
Feature add n8 n (#1468)
* feat(n8n): 添加n8n工作流API支持 添加n8n工作流API作为新的运行器类型,支持通过webhook调用n8n工作流,并提供多种认证方式(Basic、JWT、Header)。新增N8nAuthFormComponent用于处理n8n认证表单联动,并更新相关配置文件和测试用例。 * chore: remove pip mirror url * perf: simplify ret def of pipeline metadata * feat(n8n): raise exc instead of ret as normal msg * perf: add var `user_message_text` * chore(n8n): migration and default config * chore: required database version --------- Co-authored-by: hengwei.wang <@> Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
||||
import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent';
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
|
||||
/**
|
||||
* N8n认证表单组件
|
||||
* 根据选择的认证类型动态显示相应的表单项
|
||||
*/
|
||||
export default function N8nAuthFormComponent({
|
||||
itemConfigList,
|
||||
onSubmit,
|
||||
initialValues,
|
||||
}: {
|
||||
itemConfigList: IDynamicFormItemSchema[];
|
||||
onSubmit?: (val: object) => unknown;
|
||||
initialValues?: Record<string, any>;
|
||||
}) {
|
||||
// 当前选择的认证类型
|
||||
const [authType, setAuthType] = useState<string>(
|
||||
initialValues?.['auth-type'] || 'none'
|
||||
);
|
||||
|
||||
// 根据 itemConfigList 动态生成 zod schema
|
||||
const formSchema = z.object(
|
||||
itemConfigList.reduce(
|
||||
(acc, item) => {
|
||||
let fieldSchema;
|
||||
switch (item.type) {
|
||||
case 'integer':
|
||||
fieldSchema = z.number();
|
||||
break;
|
||||
case 'float':
|
||||
fieldSchema = z.number();
|
||||
break;
|
||||
case 'boolean':
|
||||
fieldSchema = z.boolean();
|
||||
break;
|
||||
case 'string':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'array[string]':
|
||||
fieldSchema = z.array(z.string());
|
||||
break;
|
||||
case 'select':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'llm-model-selector':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'prompt-editor':
|
||||
fieldSchema = z.array(
|
||||
z.object({
|
||||
content: z.string(),
|
||||
role: z.string(),
|
||||
}),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
fieldSchema = z.string();
|
||||
}
|
||||
|
||||
if (
|
||||
item.required &&
|
||||
(fieldSchema instanceof z.ZodString ||
|
||||
fieldSchema instanceof z.ZodArray)
|
||||
) {
|
||||
fieldSchema = fieldSchema.min(1, { message: '此字段为必填项' });
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[item.name]: fieldSchema,
|
||||
};
|
||||
},
|
||||
{} as Record<string, z.ZodTypeAny>,
|
||||
),
|
||||
);
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: itemConfigList.reduce((acc, item) => {
|
||||
// 优先使用 initialValues,如果没有则使用默认值
|
||||
const value = initialValues?.[item.name] ?? item.default;
|
||||
return {
|
||||
...acc,
|
||||
[item.name]: value,
|
||||
};
|
||||
}, {} as FormValues),
|
||||
});
|
||||
|
||||
// 当 initialValues 变化时更新表单值
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
// 合并默认值和初始值
|
||||
const mergedValues = itemConfigList.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.name] = initialValues[item.name] ?? item.default;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
|
||||
Object.entries(mergedValues).forEach(([key, value]) => {
|
||||
form.setValue(key as keyof FormValues, value);
|
||||
});
|
||||
|
||||
// 更新认证类型
|
||||
setAuthType(mergedValues['auth-type'] as string || 'none');
|
||||
}
|
||||
}, [initialValues, form, itemConfigList]);
|
||||
|
||||
// 监听表单值变化
|
||||
useEffect(() => {
|
||||
const subscription = form.watch((value, { name }) => {
|
||||
// 如果认证类型变化,更新状态
|
||||
if (name === 'auth-type') {
|
||||
setAuthType(value['auth-type'] as string);
|
||||
}
|
||||
|
||||
// 获取完整的表单值,确保包含所有默认值
|
||||
const formValues = form.getValues();
|
||||
const finalValues = itemConfigList.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.name] = formValues[item.name] ?? item.default;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
|
||||
onSubmit?.(finalValues);
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [form, onSubmit, itemConfigList]);
|
||||
|
||||
// 根据认证类型过滤表单项
|
||||
const filteredConfigList = itemConfigList.filter((config) => {
|
||||
// 始终显示webhook-url、auth-type、timeout和output-key
|
||||
if (['webhook-url', 'auth-type', 'timeout', 'output-key'].includes(config.name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 根据认证类型显示相应的表单项
|
||||
if (authType === 'basic' && config.name.startsWith('basic-')) {
|
||||
return true;
|
||||
}
|
||||
if (authType === 'jwt' && config.name.startsWith('jwt-')) {
|
||||
return true;
|
||||
}
|
||||
if (authType === 'header' && config.name.startsWith('header-')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<div className="space-y-4">
|
||||
{filteredConfigList.map((config) => (
|
||||
<FormField
|
||||
key={config.id}
|
||||
control={form.control}
|
||||
name={config.name as keyof FormValues}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{i18nObj(config.label)}{' '}
|
||||
{config.required && <span className="text-red-500">*</span>}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<DynamicFormItemComponent config={config} field={field} />
|
||||
</FormControl>
|
||||
{config.description && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{i18nObj(config.description)}
|
||||
</p>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@/app/infra/entities/pipeline';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
import N8nAuthFormComponent from '@/app/home/components/dynamic-form/N8nAuthFormComponent';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@@ -244,6 +245,37 @@ export default function PipelineFormComponent({
|
||||
if (stage.name !== currentRunner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 对于n8n-service-api配置,使用N8nAuthFormComponent处理表单联动
|
||||
if (stage.name === 'n8n-service-api') {
|
||||
return (
|
||||
<div key={stage.name} className="space-y-4 mb-6">
|
||||
<div className="text-lg font-medium">{i18nObj(stage.label)}</div>
|
||||
{stage.description && (
|
||||
<div className="text-sm text-gray-500">
|
||||
{i18nObj(stage.description)}
|
||||
</div>
|
||||
)}
|
||||
<N8nAuthFormComponent
|
||||
itemConfigList={stage.config}
|
||||
initialValues={
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(form.watch(formName) as Record<string, any>)?.[stage.name] ||
|
||||
{}
|
||||
}
|
||||
onSubmit={(values) => {
|
||||
const currentValues =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(form.getValues(formName) as Record<string, any>) || {};
|
||||
form.setValue(formName, {
|
||||
...currentValues,
|
||||
[stage.name]: values,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -203,77 +203,10 @@ export interface MarketPluginResponse {
|
||||
}
|
||||
|
||||
interface GetPipelineConfig {
|
||||
ai: {
|
||||
'dashscope-app-api': {
|
||||
'api-key': string;
|
||||
'app-id': string;
|
||||
'app-type': 'agent' | 'workflow';
|
||||
'references-quote'?: string;
|
||||
};
|
||||
'dify-service-api': {
|
||||
'api-key': string;
|
||||
'app-type': 'chat' | 'agent' | 'workflow';
|
||||
'base-url': string;
|
||||
'thinking-convert': 'plain' | 'original' | 'remove';
|
||||
timeout?: number;
|
||||
};
|
||||
'local-agent': {
|
||||
'max-round': number;
|
||||
model: string;
|
||||
prompt: Array<{
|
||||
content: string;
|
||||
role: string;
|
||||
}>;
|
||||
};
|
||||
runner: {
|
||||
runner: 'local-agent' | 'dify-service-api' | 'dashscope-app-api';
|
||||
};
|
||||
};
|
||||
output: {
|
||||
'force-delay': {
|
||||
max: number;
|
||||
min: number;
|
||||
};
|
||||
'long-text-processing': {
|
||||
'font-path': string;
|
||||
strategy: 'forward' | 'image';
|
||||
threshold: number;
|
||||
};
|
||||
misc: {
|
||||
'at-sender': boolean;
|
||||
'hide-exception': boolean;
|
||||
'quote-origin': boolean;
|
||||
'track-function-calls': boolean;
|
||||
};
|
||||
};
|
||||
safety: {
|
||||
'content-filter': {
|
||||
'check-sensitive-words': boolean;
|
||||
scope: 'all' | 'income-msg' | 'output-msg';
|
||||
};
|
||||
'rate-limit': {
|
||||
limitation: number;
|
||||
strategy: 'drop' | 'wait';
|
||||
'window-length': number;
|
||||
};
|
||||
};
|
||||
trigger: {
|
||||
'access-control': {
|
||||
blacklist: string[];
|
||||
mode: 'blacklist' | 'whitelist';
|
||||
whitelist: string[];
|
||||
};
|
||||
'group-respond-rules': {
|
||||
at: boolean;
|
||||
prefix: string[];
|
||||
random: number;
|
||||
regexp: string[];
|
||||
};
|
||||
'ignore-rules': {
|
||||
prefix: string[];
|
||||
regexp: string[];
|
||||
};
|
||||
};
|
||||
ai: {};
|
||||
output: {};
|
||||
safety: {};
|
||||
trigger: {};
|
||||
}
|
||||
|
||||
interface GetPipeline {
|
||||
|
||||
Reference in New Issue
Block a user