Feat: Add dynamic webhook response.

This commit is contained in:
bill 2025-12-09 14:06:21 +08:00
parent f317c5116b
commit 137de4849d
4 changed files with 260 additions and 6 deletions

View file

@ -68,7 +68,7 @@ function BeginForm({ node }: INextOperatorForm) {
max_body_size: z.string(),
})
.optional(),
schema: z.string().optional(),
schema: z.record(z.any()).optional(),
response: z
.object({
status: z.number(),

View file

@ -0,0 +1,213 @@
import { BoolSegmented } from '@/components/bool-segmented';
import { KeyInput } from '@/components/key-input';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { useIsDarkTheme } from '@/components/theme-provider';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { Textarea } from '@/components/ui/textarea';
import { Editor, loader } from '@monaco-editor/react';
import { X } from 'lucide-react';
import { ReactNode, useCallback } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { InputMode, TypesWithArray } from '../../../constant';
import {
InputModeOptions,
buildConversationVariableSelectOptions,
} from '../../../utils';
import { DynamicFormHeader } from '../../components/dynamic-fom-header';
import { QueryVariable } from '../../components/query-variable';
loader.config({ paths: { vs: '/vs' } });
type SelectKeysProps = {
name: string;
label: ReactNode;
tooltip?: string;
keyField?: string;
valueField?: string;
operatorField?: string;
nodeId?: string;
};
const VariableTypeOptions = buildConversationVariableSelectOptions();
const modeField = 'input_mode';
const ConstantValueMap = {
[TypesWithArray.Boolean]: true,
[TypesWithArray.Number]: 0,
[TypesWithArray.String]: '',
[TypesWithArray.ArrayBoolean]: '[]',
[TypesWithArray.ArrayNumber]: '[]',
[TypesWithArray.ArrayString]: '[]',
[TypesWithArray.ArrayObject]: '[]',
[TypesWithArray.Object]: '{}',
};
export function DynamicResponse({
name,
label,
tooltip,
keyField = 'variable',
valueField = 'value',
operatorField = 'type',
}: SelectKeysProps) {
const form = useFormContext();
const isDarkTheme = useIsDarkTheme();
const { fields, remove, append } = useFieldArray({
name: name,
control: form.control,
});
const initializeValue = useCallback(
(mode: string, variableType: string, valueFieldAlias: string) => {
if (mode === InputMode.Variable) {
form.setValue(valueFieldAlias, '', { shouldDirty: true });
} else {
const val = ConstantValueMap[variableType as TypesWithArray];
form.setValue(valueFieldAlias, val, { shouldDirty: true });
}
},
[form],
);
const handleModeChange = useCallback(
(mode: string, valueFieldAlias: string, operatorFieldAlias: string) => {
const variableType = form.getValues(operatorFieldAlias);
initializeValue(mode, variableType, valueFieldAlias);
},
[form, initializeValue],
);
const handleVariableTypeChange = useCallback(
(variableType: string, valueFieldAlias: string, modeFieldAlias: string) => {
const mode = form.getValues(modeFieldAlias);
initializeValue(mode, variableType, valueFieldAlias);
},
[form, initializeValue],
);
const renderParameter = useCallback(
(operatorFieldName: string, modeFieldName: string) => {
const mode = form.getValues(modeFieldName);
const logicalOperator = form.getValues(operatorFieldName);
if (mode === InputMode.Constant) {
if (logicalOperator === TypesWithArray.Boolean) {
return <BoolSegmented></BoolSegmented>;
}
if (logicalOperator === TypesWithArray.Number) {
return <Input className="w-full" type="number"></Input>;
}
if (logicalOperator === TypesWithArray.String) {
return <Textarea></Textarea>;
}
return (
<Editor
height={300}
theme={isDarkTheme ? 'vs-dark' : 'vs'}
language={'json'}
options={{
minimap: { enabled: false },
automaticLayout: true,
}}
/>
);
}
return (
<QueryVariable
types={[logicalOperator]}
hideLabel
pureQuery
></QueryVariable>
);
},
[form, isDarkTheme],
);
return (
<section className="space-y-2">
<DynamicFormHeader
label={label}
tooltip={tooltip}
onClick={() =>
append({
[keyField]: '',
[valueField]: '',
[modeField]: InputMode.Constant,
[operatorField]: TypesWithArray.String,
})
}
></DynamicFormHeader>
<div className="space-y-5">
{fields.map((field, index) => {
const keyFieldAlias = `${name}.${index}.${keyField}`;
const valueFieldAlias = `${name}.${index}.${valueField}`;
const operatorFieldAlias = `${name}.${index}.${operatorField}`;
const modeFieldAlias = `${name}.${index}.${modeField}`;
return (
<section key={field.id} className="flex gap-2">
<div className="flex-1 space-y-3 min-w-0">
<div className="flex items-center">
<RAGFlowFormItem name={keyFieldAlias} className="flex-1 ">
<KeyInput></KeyInput>
</RAGFlowFormItem>
<Separator className="w-2" />
<RAGFlowFormItem name={operatorFieldAlias} className="flex-1">
{(field) => (
<SelectWithSearch
value={field.value}
onChange={(val) => {
handleVariableTypeChange(
val,
valueFieldAlias,
modeFieldAlias,
);
field.onChange(val);
}}
options={VariableTypeOptions}
></SelectWithSearch>
)}
</RAGFlowFormItem>
<Separator className="w-2" />
<RAGFlowFormItem name={modeFieldAlias} className="flex-1">
{(field) => (
<SelectWithSearch
value={field.value}
onChange={(val) => {
handleModeChange(
val,
valueFieldAlias,
operatorFieldAlias,
);
field.onChange(val);
}}
options={InputModeOptions}
></SelectWithSearch>
)}
</RAGFlowFormItem>
</div>
<RAGFlowFormItem name={valueFieldAlias} className="w-full">
{renderParameter(operatorFieldAlias, modeFieldAlias)}
</RAGFlowFormItem>
</div>
<Button variant={'ghost'} onClick={() => remove(index)}>
<X />
</Button>
</section>
);
})}
</div>
</section>
);
}

View file

@ -1,8 +1,10 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { Textarea } from '@/components/ui/textarea';
import { buildOptions } from '@/utils/form';
import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
WebhookContentType,
@ -12,11 +14,18 @@ import {
} from '../../../constant';
import { DynamicStringForm } from '../../components/dynamic-string-form';
import { Auth } from './auth';
import { WebhookResponse } from './response';
const RateLimitPerOptions = buildOptions(['minute', 'hour', 'day']);
export function WebHook() {
const { t } = useTranslation();
const form = useFormContext();
const executionMode = useWatch({
control: form.control,
name: 'execution_mode',
});
return (
<>
@ -33,7 +42,8 @@ export function WebHook() {
options={buildOptions(WebhookContentType)}
></SelectWithSearch>
</RAGFlowFormItem>
<section className="space-y-5 bg-bg-card p-2 rounded">
<Separator></Separator>
<>
<RAGFlowFormItem
name="security.auth_type"
label={t('flow.webhook.authType')}
@ -65,13 +75,10 @@ export function WebHook() {
name="security.ip_whitelist"
label={t('flow.webhook.ipWhitelist')}
></DynamicStringForm>
</section>
</>
<RAGFlowFormItem name="schema" label={t('flow.webhook.schema')}>
<Textarea></Textarea>
</RAGFlowFormItem>
<RAGFlowFormItem name="response" label={t('flow.webhook.response')}>
<Textarea></Textarea>
</RAGFlowFormItem>
<RAGFlowFormItem
name="execution_mode"
label={t('flow.webhook.executionMode')}
@ -80,6 +87,9 @@ export function WebHook() {
options={buildOptions(WebhookExecutionMode)}
></SelectWithSearch>
</RAGFlowFormItem>
{executionMode === WebhookExecutionMode.Immediately && (
<WebhookResponse></WebhookResponse>
)}
</>
);
}

View file

@ -0,0 +1,31 @@
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { useTranslation } from 'react-i18next';
import { DynamicResponse } from './dynamic-response';
export function WebhookResponse() {
const { t } = useTranslation();
return (
<>
<Separator></Separator>
<section className="space-y-4">
<RAGFlowFormItem
name={'response.status'}
label={t('flow.webhook.status')}
>
<Input type="number"></Input>
</RAGFlowFormItem>
<DynamicResponse
name="response.headers_template"
label={t('flow.webhook.headersTemplate')}
></DynamicResponse>
<DynamicResponse
name="response.body_template"
label={t('flow.webhook.bodyTemplate')}
></DynamicResponse>
</section>
</>
);
}