Feat: Display error messages from intermediate nodes of the webhook. #10427 (#11954)

### What problem does this PR solve?

Feat: Remove HMAC from the webhook #10427

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-12-19 12:56:56 +08:00
committed by GitHub
parent 6cd1824a77
commit 4cbe470089
27 changed files with 737 additions and 359 deletions

View File

@ -43,6 +43,7 @@ function BeginForm({ node }: INextOperatorForm) {
const form = useForm({
defaultValues: values,
resolver: zodResolver(BeginFormSchema),
mode: 'onChange',
});
useWatchFormChange(node?.id, form);

View File

@ -1,4 +1,4 @@
import { WebhookAlgorithmList } from '@/constants/agent';
import { WebhookJWTAlgorithmList } from '@/constants/agent';
import { z } from 'zod';
export const BeginFormSchema = z.object({
@ -30,7 +30,14 @@ export const BeginFormSchema = z.object({
max_body_size: z.string(),
jwt: z
.object({
algorithm: z.string().default(WebhookAlgorithmList[0]).optional(),
algorithm: z.string().default(WebhookJWTAlgorithmList[0]).optional(),
required_claims: z.array(z.object({ value: z.string() })),
})
.optional(),
hmac: z
.object({
header: z.string().optional(),
secret: z.string().optional(),
})
.optional(),
})

View File

@ -2,11 +2,11 @@ import { useCallback } from 'react';
import { UseFormReturn } from 'react-hook-form';
import {
AgentDialogueMode,
RateLimitPerList,
WebhookContentType,
WebhookExecutionMode,
WebhookMaxBodySize,
WebhookMethod,
WebhookRateLimitPer,
WebhookSecurityAuthType,
} from '../../constant';
@ -14,7 +14,7 @@ const initialFormValuesMap = {
methods: [WebhookMethod.Get],
schema: {},
'security.auth_type': WebhookSecurityAuthType.Basic,
'security.rate_limit.per': RateLimitPerList[0],
'security.rate_limit.per': WebhookRateLimitPer.Second,
'security.rate_limit.limit': 10,
'security.max_body_size': WebhookMaxBodySize[0],
'response.status': 200,

View File

@ -1,16 +1,15 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Input } from '@/components/ui/input';
import { WebhookAlgorithmList } from '@/constants/agent';
import { WebhookJWTAlgorithmList } from '@/constants/agent';
import { WebhookSecurityAuthType } from '@/pages/agent/constant';
import { buildOptions } from '@/utils/form';
import { useCallback } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { DynamicStringForm } from '../../components/dynamic-string-form';
const AlgorithmOptions = buildOptions(WebhookAlgorithmList);
const RequiredClaimsOptions = buildOptions(['exp', 'sub']);
const AlgorithmOptions = buildOptions(WebhookJWTAlgorithmList);
export function Auth() {
const { t } = useTranslation();
@ -88,38 +87,10 @@ export function Auth() {
>
<Input></Input>
</RAGFlowFormItem>
<RAGFlowFormItem
<DynamicStringForm
name="security.jwt.required_claims"
label={t('flow.webhook.requiredClaims')}
>
<SelectWithSearch options={RequiredClaimsOptions}></SelectWithSearch>
</RAGFlowFormItem>
</>
),
[t],
);
const renderHmacAuth = useCallback(
() => (
<>
<RAGFlowFormItem
name="security.hmac.header"
label={t('flow.webhook.header')}
>
<Input></Input>
</RAGFlowFormItem>
<RAGFlowFormItem
name="security.hmac.secret"
label={t('flow.webhook.secret')}
>
<Input></Input>
</RAGFlowFormItem>
<RAGFlowFormItem
name="security.hmac.algorithm"
label={t('flow.webhook.algorithm')}
>
<SelectWithSearch options={AlgorithmOptions}></SelectWithSearch>
</RAGFlowFormItem>
></DynamicStringForm>
</>
),
[t],
@ -129,11 +100,14 @@ export function Auth() {
[WebhookSecurityAuthType.Token]: renderTokenAuth,
[WebhookSecurityAuthType.Basic]: renderBasicAuth,
[WebhookSecurityAuthType.Jwt]: renderJwtAuth,
[WebhookSecurityAuthType.Hmac]: renderHmacAuth,
[WebhookSecurityAuthType.None]: () => null,
};
return AuthMap[
(authType ?? WebhookSecurityAuthType.None) as WebhookSecurityAuthType
]();
return (
<div key={`auth-${authType}`} className="space-y-5">
{AuthMap[
(authType ?? WebhookSecurityAuthType.None) as WebhookSecurityAuthType
]()}
</div>
);
}

View File

@ -6,15 +6,10 @@ import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch';
import { buildOptions } from '@/utils/form';
import { loader } from '@monaco-editor/react';
import { omit } from 'lodash';
import { X } from 'lucide-react';
import { ReactNode } from 'react';
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
import {
TypesWithArray,
WebhookContentType,
WebhookRequestParameters,
} from '../../../constant';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { TypesWithArray, WebhookRequestParameters } from '../../../constant';
import { DynamicFormHeader } from '../../components/dynamic-fom-header';
loader.config({ paths: { vs: '/vs' } });
@ -28,16 +23,9 @@ type SelectKeysProps = {
requiredField?: string;
nodeId?: string;
isObject?: boolean;
operatorList: WebhookRequestParameters[];
};
function buildParametersOptions(isObject: boolean) {
const list = isObject
? WebhookRequestParameters
: omit(WebhookRequestParameters, ['File']);
return buildOptions(list);
}
export function DynamicRequest({
name,
label,
@ -45,15 +33,9 @@ export function DynamicRequest({
keyField = 'key',
operatorField = 'type',
requiredField = 'required',
isObject = false,
operatorList,
}: SelectKeysProps) {
const form = useFormContext();
const contentType = useWatch({
name: 'content_types',
control: form.control,
});
const isFormDataContentType =
contentType === WebhookContentType.MultipartFormData;
const { fields, remove, append } = useFieldArray({
name: name,
@ -94,9 +76,7 @@ export function DynamicRequest({
onChange={(val) => {
field.onChange(val);
}}
options={buildParametersOptions(
isObject && isFormDataContentType,
)}
options={buildOptions(operatorList)}
></SelectWithSearch>
)}
</RAGFlowFormItem>

View File

@ -1,17 +1,20 @@
import { Collapse } from '@/components/collapse';
import CopyToClipboard from '@/components/copy-to-clipboard';
import { CopyToClipboardWithText } from '@/components/copy-to-clipboard';
import NumberInput from '@/components/originui/number-input';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Input } from '@/components/ui/input';
import { MultiSelect } from '@/components/ui/multi-select';
import { Textarea } from '@/components/ui/textarea';
import { buildOptions } from '@/utils/form';
import { useCallback } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
import {
RateLimitPerList,
WebhookMaxBodySize,
WebhookMethod,
WebhookRateLimitPer,
WebhookSecurityAuthType,
} from '../../../constant';
import { DynamicStringForm } from '../../components/dynamic-string-form';
@ -21,18 +24,32 @@ import { WebhookResponse } from './response';
const RateLimitPerOptions = buildOptions(RateLimitPerList);
const RequestLimitMap = {
[WebhookRateLimitPer.Second]: 100,
[WebhookRateLimitPer.Minute]: 1000,
[WebhookRateLimitPer.Hour]: 10000,
[WebhookRateLimitPer.Day]: 100000,
};
export function WebHook() {
const { t } = useTranslation();
const { id } = useParams();
const form = useFormContext();
const rateLimitPer = useWatch({
name: 'security.rate_limit.per',
control: form.control,
});
const getLimitRateLimitPerMax = useCallback((rateLimitPer: string) => {
return RequestLimitMap[rateLimitPer as keyof typeof RequestLimitMap] ?? 100;
}, []);
const text = `${location.protocol}//${location.host}/api/v1/webhook/${id}`;
return (
<>
<div className="bg-bg-card p-1 rounded-md flex gap-2">
<span className="flex-1 truncate">{text}</span>
<CopyToClipboard text={text}></CopyToClipboard>
</div>
<CopyToClipboardWithText text={text}></CopyToClipboardWithText>
<RAGFlowFormItem name="methods" label={t('flow.webhook.methods')}>
{(field) => (
<MultiSelect
@ -61,13 +78,28 @@ export function WebHook() {
name="security.rate_limit.limit"
label={t('flow.webhook.limit')}
>
<Input type="number"></Input>
<NumberInput
max={getLimitRateLimitPerMax(rateLimitPer)}
className="w-full"
></NumberInput>
</RAGFlowFormItem>
<RAGFlowFormItem
name="security.rate_limit.per"
label={t('flow.webhook.per')}
>
<SelectWithSearch options={RateLimitPerOptions}></SelectWithSearch>
{(field) => (
<SelectWithSearch
options={RateLimitPerOptions}
value={field.value}
onChange={(val) => {
field.onChange(val);
form.setValue(
'security.rate_limit.limit',
getLimitRateLimitPerMax(val),
);
}}
></SelectWithSearch>
)}
</RAGFlowFormItem>
<RAGFlowFormItem
name="security.max_body_size"

View File

@ -1,13 +1,40 @@
import { Collapse } from '@/components/collapse';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { WebhookContentType } from '@/pages/agent/constant';
import {
WebhookContentType,
WebhookRequestParameters,
} from '@/pages/agent/constant';
import { buildOptions } from '@/utils/form';
import { useMemo } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { DynamicRequest } from './dynamic-request';
export function WebhookRequestSchema() {
const { t } = useTranslation();
const form = useFormContext();
const contentType = useWatch({
name: 'content_types',
control: form.control,
});
const isFormDataContentType =
contentType === WebhookContentType.MultipartFormData;
const bodyOperatorList = useMemo(() => {
return isFormDataContentType
? [
WebhookRequestParameters.String,
WebhookRequestParameters.Number,
WebhookRequestParameters.Boolean,
WebhookRequestParameters.File,
]
: [
WebhookRequestParameters.String,
WebhookRequestParameters.Number,
WebhookRequestParameters.Boolean,
];
}, [isFormDataContentType]);
return (
<Collapse title={<div>{t('flow.webhook.schema')}</div>}>
@ -23,14 +50,20 @@ export function WebhookRequestSchema() {
<DynamicRequest
name="schema.query"
label={t('flow.webhook.queryParameters')}
operatorList={[
WebhookRequestParameters.String,
WebhookRequestParameters.Number,
WebhookRequestParameters.Boolean,
]}
></DynamicRequest>
<DynamicRequest
name="schema.headers"
label={t('flow.webhook.headerParameters')}
operatorList={[WebhookRequestParameters.String]}
></DynamicRequest>
<DynamicRequest
name="schema.body"
isObject
operatorList={bodyOperatorList}
label={t('flow.webhook.requestBodyParameters')}
></DynamicRequest>
</section>

View File

@ -49,7 +49,7 @@ export function WebhookResponse() {
name="response.body_template"
label={t('flow.webhook.bodyTemplate')}
>
<Textarea></Textarea>
<Textarea className="overflow-auto"></Textarea>
</RAGFlowFormItem>
</>
)}

View File

@ -0,0 +1,21 @@
import JsonView from 'react18-json-view';
export function JsonViewer({
data,
title,
}: {
data: Record<string, any>;
title: string;
}) {
return (
<section className="space-y-2">
<div>{title}</div>
<JsonView
src={data}
displaySize
collapseStringsAfterLength={100000000000}
className="w-full h-[200px] break-words overflow-auto scrollbar-auto p-2 bg-muted"
/>
</section>
);
}