Feat: Set the return value of the webhook to a string. #10427 (#11945)

### What problem does this PR solve?

Feat: Set the return value of the webhook to a string. #10427

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-12-15 11:09:08 +08:00
committed by GitHub
parent 81eb03d230
commit 1ddd11f045
11 changed files with 132 additions and 147 deletions

View File

@ -2006,28 +2006,28 @@ Important structured information may include: names, dates, locations, events, k
schema: 'Schema', schema: 'Schema',
response: 'Response', response: 'Response',
executionMode: 'Execution mode', executionMode: 'Execution mode',
authMethods: 'Authentication Methods', authMethods: 'Authentication methods',
authType: 'Authentication Type', authType: 'Authentication type',
limit: 'Request Limit', limit: 'Request limit',
per: 'Time Period', per: 'Time period',
maxBodySize: 'Maximum Body Size', maxBodySize: 'Maximum body size',
ipWhitelist: 'IP Whitelist', ipWhitelist: 'Ip whitelist',
tokenHeader: 'Token Header', tokenHeader: 'Token header',
tokenValue: 'Token Value', tokenValue: 'Token value',
username: 'Username', username: 'Username',
password: 'Password', password: 'Password',
algorithm: 'Algorithm', algorithm: 'Algorithm',
secret: 'Secret', secret: 'Secret',
issuer: 'Issuer', issuer: 'Issuer',
audience: 'Audience', audience: 'Audience',
requiredClaims: 'Required Claims', requiredClaims: 'Required claims',
header: 'Header', header: 'Header',
status: 'Status', status: 'Status',
headersTemplate: 'Headers Template', headersTemplate: 'Headers template',
bodyTemplate: 'Body Template', bodyTemplate: 'Body template',
basic: 'Basic', basic: 'Basic',
bearer: 'Bearer', bearer: 'Bearer',
apiKey: 'Api Key', apiKey: 'Api key',
queryParameters: 'Query parameters', queryParameters: 'Query parameters',
headerParameters: 'Header parameters', headerParameters: 'Header parameters',
requestBodyParameters: 'Request body parameters', requestBodyParameters: 'Request body parameters',

View File

@ -1037,3 +1037,9 @@ export enum WebhookRequestParameters {
Number = TypesWithArray.Number, Number = TypesWithArray.Number,
Boolean = TypesWithArray.Boolean, Boolean = TypesWithArray.Boolean,
} }
export enum WebhookStatus {
Testing = 'testing',
Live = 'live',
Stopped = 'stopped',
}

View File

@ -83,7 +83,7 @@ function BeginForm({ node }: INextOperatorForm) {
}); });
return ( return (
<section className="px-5 space-y-5"> <section className="px-5 space-y-5 pb-4">
<Form {...form}> <Form {...form}>
<FormField <FormField
control={form.control} control={form.control}

View File

@ -69,10 +69,10 @@ export const BeginFormSchema = z.object({
response: z response: z
.object({ .object({
status: z.number(), status: z.number(),
headers_template: z.array( // headers_template: z.array(
z.object({ key: z.string(), value: z.string() }), // z.object({ key: z.string(), value: z.string() }),
), // ),
body_template: z.array(z.object({ key: z.string(), value: z.string() })), body_template: z.string().optional(),
}) })
.optional(), .optional(),
execution_mode: z.string().optional(), execution_mode: z.string().optional(),

View File

@ -15,7 +15,9 @@ const initialFormValuesMap = {
schema: {}, schema: {},
'security.auth_type': WebhookSecurityAuthType.Basic, 'security.auth_type': WebhookSecurityAuthType.Basic,
'security.rate_limit.per': RateLimitPerList[0], 'security.rate_limit.per': RateLimitPerList[0],
'security.rate_limit.limit': 10,
'security.max_body_size': WebhookMaxBodySize[0], 'security.max_body_size': WebhookMaxBodySize[0],
'response.status': 200,
execution_mode: WebhookExecutionMode.Immediately, execution_mode: WebhookExecutionMode.Immediately,
content_types: WebhookContentType.ApplicationJson, content_types: WebhookContentType.ApplicationJson,
}; };

View File

@ -9,8 +9,12 @@ import { loader } from '@monaco-editor/react';
import { omit } from 'lodash'; import { omit } from 'lodash';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form'; import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
import { TypesWithArray, WebhookRequestParameters } from '../../../constant'; import {
TypesWithArray,
WebhookContentType,
WebhookRequestParameters,
} from '../../../constant';
import { DynamicFormHeader } from '../../components/dynamic-fom-header'; import { DynamicFormHeader } from '../../components/dynamic-fom-header';
loader.config({ paths: { vs: '/vs' } }); loader.config({ paths: { vs: '/vs' } });
@ -44,6 +48,12 @@ export function DynamicRequest({
isObject = false, isObject = false,
}: SelectKeysProps) { }: SelectKeysProps) {
const form = useFormContext(); const form = useFormContext();
const contentType = useWatch({
name: 'content_types',
control: form.control,
});
const isFormDataContentType =
contentType === WebhookContentType.MultipartFormData;
const { fields, remove, append } = useFieldArray({ const { fields, remove, append } = useFieldArray({
name: name, name: name,
@ -84,7 +94,9 @@ export function DynamicRequest({
onChange={(val) => { onChange={(val) => {
field.onChange(val); field.onChange(val);
}} }}
options={buildParametersOptions(isObject)} options={buildParametersOptions(
isObject && isFormDataContentType,
)}
></SelectWithSearch> ></SelectWithSearch>
)} )}
</RAGFlowFormItem> </RAGFlowFormItem>

View File

@ -11,13 +11,9 @@ import { Editor, loader } from '@monaco-editor/react';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { ReactNode, useCallback } from 'react'; import { ReactNode, useCallback } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form'; import { useFieldArray, useFormContext } from 'react-hook-form';
import { InputMode, TypesWithArray } from '../../../constant'; import { TypesWithArray } from '../../../constant';
import { import { buildConversationVariableSelectOptions } from '../../../utils';
InputModeOptions,
buildConversationVariableSelectOptions,
} from '../../../utils';
import { DynamicFormHeader } from '../../components/dynamic-fom-header'; import { DynamicFormHeader } from '../../components/dynamic-fom-header';
import { QueryVariable } from '../../components/query-variable';
loader.config({ paths: { vs: '/vs' } }); loader.config({ paths: { vs: '/vs' } });
@ -33,8 +29,6 @@ type SelectKeysProps = {
const VariableTypeOptions = buildConversationVariableSelectOptions(); const VariableTypeOptions = buildConversationVariableSelectOptions();
const modeField = 'input_mode';
const ConstantValueMap = { const ConstantValueMap = {
[TypesWithArray.Boolean]: true, [TypesWithArray.Boolean]: true,
[TypesWithArray.Number]: 0, [TypesWithArray.Number]: 0,
@ -63,71 +57,46 @@ export function DynamicResponse({
}); });
const initializeValue = useCallback( const initializeValue = useCallback(
(mode: string, variableType: string, valueFieldAlias: string) => { (variableType: string, valueFieldAlias: string) => {
if (mode === InputMode.Variable) { const val = ConstantValueMap[variableType as TypesWithArray];
form.setValue(valueFieldAlias, '', { shouldDirty: true }); form.setValue(valueFieldAlias, val, { shouldDirty: true });
} else {
const val = ConstantValueMap[variableType as TypesWithArray];
form.setValue(valueFieldAlias, val, { shouldDirty: true });
}
}, },
[form], [form],
); );
const handleModeChange = useCallback(
(mode: string, valueFieldAlias: string, operatorFieldAlias: string) => {
const variableType = form.getValues(operatorFieldAlias);
initializeValue(mode, variableType, valueFieldAlias);
},
[form, initializeValue],
);
const handleVariableTypeChange = useCallback( const handleVariableTypeChange = useCallback(
(variableType: string, valueFieldAlias: string, modeFieldAlias: string) => { (variableType: string, valueFieldAlias: string) => {
const mode = form.getValues(modeFieldAlias); initializeValue(variableType, valueFieldAlias);
initializeValue(mode, variableType, valueFieldAlias);
}, },
[form, initializeValue], [initializeValue],
); );
const renderParameter = useCallback( const renderParameter = useCallback(
(operatorFieldName: string, modeFieldName: string) => { (operatorFieldName: string) => {
const mode = form.getValues(modeFieldName);
const logicalOperator = form.getValues(operatorFieldName); const logicalOperator = form.getValues(operatorFieldName);
if (mode === InputMode.Constant) { if (logicalOperator === TypesWithArray.Boolean) {
if (logicalOperator === TypesWithArray.Boolean) { return <BoolSegmented></BoolSegmented>;
return <BoolSegmented></BoolSegmented>; }
}
if (logicalOperator === TypesWithArray.Number) { if (logicalOperator === TypesWithArray.Number) {
return <Input className="w-full" type="number"></Input>; return <Input className="w-full" type="number"></Input>;
} }
if (logicalOperator === TypesWithArray.String) { if (logicalOperator === TypesWithArray.String) {
return <Textarea></Textarea>; return <Textarea></Textarea>;
}
return (
<Editor
height={300}
theme={isDarkTheme ? 'vs-dark' : 'vs'}
language={'json'}
options={{
minimap: { enabled: false },
automaticLayout: true,
}}
/>
);
} }
return ( return (
<QueryVariable <Editor
types={[logicalOperator]} height={300}
hideLabel theme={isDarkTheme ? 'vs-dark' : 'vs'}
pureQuery language={'json'}
></QueryVariable> options={{
minimap: { enabled: false },
automaticLayout: true,
}}
/>
); );
}, },
[form, isDarkTheme], [form, isDarkTheme],
@ -142,7 +111,6 @@ export function DynamicResponse({
append({ append({
[keyField]: '', [keyField]: '',
[valueField]: '', [valueField]: '',
[modeField]: InputMode.Constant,
[operatorField]: TypesWithArray.String, [operatorField]: TypesWithArray.String,
}) })
} }
@ -152,7 +120,6 @@ export function DynamicResponse({
const keyFieldAlias = `${name}.${index}.${keyField}`; const keyFieldAlias = `${name}.${index}.${keyField}`;
const valueFieldAlias = `${name}.${index}.${valueField}`; const valueFieldAlias = `${name}.${index}.${valueField}`;
const operatorFieldAlias = `${name}.${index}.${operatorField}`; const operatorFieldAlias = `${name}.${index}.${operatorField}`;
const modeFieldAlias = `${name}.${index}.${modeField}`;
return ( return (
<section key={field.id} className="flex gap-2"> <section key={field.id} className="flex gap-2">
@ -167,11 +134,7 @@ export function DynamicResponse({
<SelectWithSearch <SelectWithSearch
value={field.value} value={field.value}
onChange={(val) => { onChange={(val) => {
handleVariableTypeChange( handleVariableTypeChange(val, valueFieldAlias);
val,
valueFieldAlias,
modeFieldAlias,
);
field.onChange(val); field.onChange(val);
}} }}
options={VariableTypeOptions} options={VariableTypeOptions}
@ -179,25 +142,9 @@ export function DynamicResponse({
)} )}
</RAGFlowFormItem> </RAGFlowFormItem>
<Separator className="w-2" /> <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> </div>
<RAGFlowFormItem name={valueFieldAlias} className="w-full"> <RAGFlowFormItem name={valueFieldAlias} className="w-full">
{renderParameter(operatorFieldAlias, modeFieldAlias)} {renderParameter(operatorFieldAlias)}
</RAGFlowFormItem> </RAGFlowFormItem>
</div> </div>

View File

@ -1,16 +1,15 @@
import { Collapse } from '@/components/collapse'; import { Collapse } from '@/components/collapse';
import CopyToClipboard from '@/components/copy-to-clipboard';
import { SelectWithSearch } from '@/components/originui/select-with-search'; import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form'; import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { MultiSelect } from '@/components/ui/multi-select'; import { MultiSelect } from '@/components/ui/multi-select';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { buildOptions } from '@/utils/form'; import { buildOptions } from '@/utils/form';
import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
import { import {
RateLimitPerList, RateLimitPerList,
WebhookContentType,
WebhookExecutionMode,
WebhookMaxBodySize, WebhookMaxBodySize,
WebhookMethod, WebhookMethod,
WebhookSecurityAuthType, WebhookSecurityAuthType,
@ -24,15 +23,16 @@ const RateLimitPerOptions = buildOptions(RateLimitPerList);
export function WebHook() { export function WebHook() {
const { t } = useTranslation(); const { t } = useTranslation();
const form = useFormContext(); const { id } = useParams();
const executionMode = useWatch({ const text = `${location.protocol}//${location.host}/api/v1/webhook/${id}`;
control: form.control,
name: 'execution_mode',
});
return ( 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>
<RAGFlowFormItem name="methods" label={t('flow.webhook.methods')}> <RAGFlowFormItem name="methods" label={t('flow.webhook.methods')}>
{(field) => ( {(field) => (
<MultiSelect <MultiSelect
@ -45,14 +45,7 @@ export function WebHook() {
/> />
)} )}
</RAGFlowFormItem> </RAGFlowFormItem>
<RAGFlowFormItem
name="content_types"
label={t('flow.webhook.contentTypes')}
>
<SelectWithSearch
options={buildOptions(WebhookContentType)}
></SelectWithSearch>
</RAGFlowFormItem>
<Collapse title={<div>Security</div>}> <Collapse title={<div>Security</div>}>
<section className="space-y-4"> <section className="space-y-4">
<RAGFlowFormItem <RAGFlowFormItem
@ -98,17 +91,8 @@ export function WebHook() {
> >
<Textarea></Textarea> <Textarea></Textarea>
</RAGFlowFormItem> </RAGFlowFormItem>
<RAGFlowFormItem
name="execution_mode" <WebhookResponse></WebhookResponse>
label={t('flow.webhook.executionMode')}
>
<SelectWithSearch
options={buildOptions(WebhookExecutionMode)}
></SelectWithSearch>
</RAGFlowFormItem>
{executionMode === WebhookExecutionMode.Immediately && (
<WebhookResponse></WebhookResponse>
)}
</> </>
); );
} }

View File

@ -1,4 +1,8 @@
import { Collapse } from '@/components/collapse'; 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 { buildOptions } from '@/utils/form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DynamicRequest } from './dynamic-request'; import { DynamicRequest } from './dynamic-request';
@ -8,6 +12,14 @@ export function WebhookRequestSchema() {
return ( return (
<Collapse title={<div>{t('flow.webhook.schema')}</div>}> <Collapse title={<div>{t('flow.webhook.schema')}</div>}>
<section className="space-y-4"> <section className="space-y-4">
<RAGFlowFormItem
name="content_types"
label={t('flow.webhook.contentTypes')}
>
<SelectWithSearch
options={buildOptions(WebhookContentType)}
></SelectWithSearch>
</RAGFlowFormItem>
<DynamicRequest <DynamicRequest
name="schema.query" name="schema.query"
label={t('flow.webhook.queryParameters')} label={t('flow.webhook.queryParameters')}

View File

@ -1,29 +1,58 @@
import { Collapse } from '@/components/collapse'; import { Collapse } from '@/components/collapse';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form'; import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { WebhookExecutionMode } from '@/pages/agent/constant';
import { buildOptions } from '@/utils/form';
import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DynamicResponse } from './dynamic-response';
export function WebhookResponse() { export function WebhookResponse() {
const { t } = useTranslation(); const { t } = useTranslation();
const form = useFormContext();
const executionMode = useWatch({
control: form.control,
name: 'execution_mode',
});
return ( return (
<Collapse title={<div>Response</div>}> <Collapse title={<div>Response</div>}>
<section className="space-y-4"> <section className="space-y-4">
<RAGFlowFormItem <RAGFlowFormItem
name={'response.status'} name="execution_mode"
label={t('flow.webhook.status')} label={t('flow.webhook.executionMode')}
> >
<Input type="number"></Input> <SelectWithSearch
options={buildOptions(WebhookExecutionMode)}
></SelectWithSearch>
</RAGFlowFormItem> </RAGFlowFormItem>
<DynamicResponse {executionMode === WebhookExecutionMode.Immediately && (
name="response.headers_template" <>
label={t('flow.webhook.headersTemplate')} <RAGFlowFormItem
></DynamicResponse> name={'response.status'}
<DynamicResponse label={t('flow.webhook.status')}
name="response.body_template" >
label={t('flow.webhook.bodyTemplate')} <Input type="number"></Input>
></DynamicResponse> </RAGFlowFormItem>
{/* <DynamicResponse
name="response.headers_template"
label={t('flow.webhook.headersTemplate')}
></DynamicResponse> */}
{/* <DynamicResponse
name="response.body_template"
label={t('flow.webhook.bodyTemplate')}
></DynamicResponse> */}
<RAGFlowFormItem
name="response.body_template"
label={t('flow.webhook.bodyTemplate')}
>
<Textarea></Textarea>
</RAGFlowFormItem>
</>
)}
</section> </section>
</Collapse> </Collapse>
); );

View File

@ -355,13 +355,6 @@ function transformBeginParams(params: BeginFormSchemaType) {
...params.security, ...params.security,
ip_whitelist: params.security?.ip_whitelist.map((x) => x.value), ip_whitelist: params.security?.ip_whitelist.map((x) => x.value),
}, },
response: {
...params.response,
headers_template: transformArrayToObject(
params.response?.headers_template,
),
body_template: transformArrayToObject(params.response?.body_template),
},
}; };
} }