From a6bd765a024f1463c331edfbbba6b67fea0b60ae Mon Sep 17 00:00:00 2001 From: balibabu Date: Fri, 12 Dec 2025 09:59:54 +0800 Subject: [PATCH] Feat: Flatten the request schema of the webhook #10427 (#11917) ### What problem does this PR solve? Feat: Flatten the request schema of the webhook #10427 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/locales/en.ts | 3 + web/src/locales/zh.ts | 3 + web/src/pages/agent/form/begin-form/index.tsx | 54 +-------- web/src/pages/agent/form/begin-form/schema.ts | 81 ++++++++++++++ .../form/begin-form/use-handle-mode-change.ts | 51 +-------- .../agent/form/begin-form/use-watch-change.ts | 21 +++- .../begin-form/webhook/dynamic-request.tsx | 103 ++++++++++++++++++ .../agent/form/begin-form/webhook/index.tsx | 46 +++----- .../begin-form/webhook/request-schema.tsx | 26 +++++ web/src/pages/agent/utils.ts | 23 +++- 10 files changed, 276 insertions(+), 135 deletions(-) create mode 100644 web/src/pages/agent/form/begin-form/schema.ts create mode 100644 web/src/pages/agent/form/begin-form/webhook/dynamic-request.tsx create mode 100644 web/src/pages/agent/form/begin-form/webhook/request-schema.tsx diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 00b8552ca..c879afbbd 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -2008,6 +2008,9 @@ Important structured information may include: names, dates, locations, events, k basic: 'Basic', bearer: 'Bearer', apiKey: 'Api Key', + queryParameters: 'Query parameters', + headerParameters: 'Header parameters', + requestBodyParameters: 'Request body parameters', }, }, llmTools: { diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index b09b6ca21..dba89ea92 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -1802,6 +1802,9 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`, basic: '基础认证', bearer: '承载令牌', apiKey: 'API密钥', + queryParameters: '查询参数', + headerParameters: '请求头参数', + requestBodyParameters: '请求体参数', }, }, footer: { diff --git a/web/src/pages/agent/form/begin-form/index.tsx b/web/src/pages/agent/form/begin-form/index.tsx index c86f24cac..b7bcd549b 100644 --- a/web/src/pages/agent/form/begin-form/index.tsx +++ b/web/src/pages/agent/form/begin-form/index.tsx @@ -12,18 +12,17 @@ import { RAGFlowSelect } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { Textarea } from '@/components/ui/textarea'; import { FormTooltip } from '@/components/ui/tooltip'; -import { WebhookAlgorithmList } from '@/constants/agent'; import { zodResolver } from '@hookform/resolvers/zod'; import { t } from 'i18next'; import { Plus } from 'lucide-react'; import { memo, useEffect, useRef } from 'react'; import { useForm, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { z } from 'zod'; import { AgentDialogueMode } from '../../constant'; import { INextOperatorForm } from '../../interface'; import { ParameterDialog } from './parameter-dialog'; import { QueryTable } from './query-table'; +import { BeginFormSchema } from './schema'; import { useEditQueryRecord } from './use-edit-query'; import { useHandleModeChange } from './use-handle-mode-change'; import { useValues } from './use-values'; @@ -36,55 +35,6 @@ const ModeOptions = [ { value: AgentDialogueMode.Webhook, label: t('flow.webhook.name') }, ]; -const FormSchema = z.object({ - enablePrologue: z.boolean().optional(), - prologue: z.string().trim().optional(), - mode: z.string(), - inputs: z - .array( - z.object({ - key: z.string(), - type: z.string(), - value: z.string(), - optional: z.boolean(), - name: z.string(), - options: z.array(z.union([z.number(), z.string(), z.boolean()])), - }), - ) - .optional(), - methods: z.string().optional(), - content_types: z.string().optional(), - security: z - .object({ - auth_type: z.string(), - ip_whitelist: z.array(z.object({ value: z.string() })), - rate_limit: z.object({ - limit: z.number(), - per: z.string().optional(), - }), - max_body_size: z.string(), - jwt: z - .object({ - algorithm: z.string().default(WebhookAlgorithmList[0]).optional(), - }) - .optional(), - }) - .optional(), - schema: z.record(z.any()).optional(), - response: z - .object({ - status: z.number(), - headers_template: z.array( - z.object({ key: z.string(), value: z.string() }), - ), - body_template: z.array(z.object({ key: z.string(), value: z.string() })), - }) - .optional(), - execution_mode: z.string().optional(), -}); - -export type BeginFormSchemaType = z.infer; - function BeginForm({ node }: INextOperatorForm) { const { t } = useTranslation(); @@ -92,7 +42,7 @@ function BeginForm({ node }: INextOperatorForm) { const form = useForm({ defaultValues: values, - resolver: zodResolver(FormSchema), + resolver: zodResolver(BeginFormSchema), }); useWatchFormChange(node?.id, form); diff --git a/web/src/pages/agent/form/begin-form/schema.ts b/web/src/pages/agent/form/begin-form/schema.ts new file mode 100644 index 000000000..280649ae5 --- /dev/null +++ b/web/src/pages/agent/form/begin-form/schema.ts @@ -0,0 +1,81 @@ +import { WebhookAlgorithmList } from '@/constants/agent'; +import { z } from 'zod'; + +export const BeginFormSchema = z.object({ + enablePrologue: z.boolean().optional(), + prologue: z.string().trim().optional(), + mode: z.string(), + inputs: z + .array( + z.object({ + key: z.string(), + type: z.string(), + value: z.string(), + optional: z.boolean(), + name: z.string(), + options: z.array(z.union([z.number(), z.string(), z.boolean()])), + }), + ) + .optional(), + methods: z.array(z.string()).optional(), + content_types: z.string().optional(), + security: z + .object({ + auth_type: z.string(), + ip_whitelist: z.array(z.object({ value: z.string() })), + rate_limit: z.object({ + limit: z.number(), + per: z.string().optional(), + }), + max_body_size: z.string(), + jwt: z + .object({ + algorithm: z.string().default(WebhookAlgorithmList[0]).optional(), + }) + .optional(), + }) + .optional(), + schema: z + .object({ + query: z + .array( + z.object({ + key: z.string(), + type: z.string(), + required: z.boolean(), + }), + ) + .optional(), + headers: z + .array( + z.object({ + key: z.string(), + type: z.string(), + required: z.boolean(), + }), + ) + .optional(), + body: z + .array( + z.object({ + key: z.string(), + type: z.string(), + required: z.boolean(), + }), + ) + .optional(), + }) + .optional(), + response: z + .object({ + status: z.number(), + headers_template: z.array( + z.object({ key: z.string(), value: z.string() }), + ), + body_template: z.array(z.object({ key: z.string(), value: z.string() })), + }) + .optional(), + execution_mode: z.string().optional(), +}); + +export type BeginFormSchemaType = z.infer; diff --git a/web/src/pages/agent/form/begin-form/use-handle-mode-change.ts b/web/src/pages/agent/form/begin-form/use-handle-mode-change.ts index e85ed5a6e..8e83ca29c 100644 --- a/web/src/pages/agent/form/begin-form/use-handle-mode-change.ts +++ b/web/src/pages/agent/form/begin-form/use-handle-mode-change.ts @@ -3,62 +3,21 @@ import { UseFormReturn } from 'react-hook-form'; import { AgentDialogueMode, RateLimitPerList, + WebhookContentType, WebhookExecutionMode, WebhookMaxBodySize, + WebhookMethod, WebhookSecurityAuthType, } from '../../constant'; -// const WebhookSchema = { -// query: { -// type: 'object', -// required: [], -// properties: { -// // debug: { type: 'boolean' }, -// // event: { type: 'string' }, -// }, -// }, - -// headers: { -// type: 'object', -// required: [], -// properties: { -// // 'X-Trace-ID': { type: 'string' }, -// }, -// }, - -// body: { -// type: 'object', -// required: [], -// properties: { -// id: { type: 'string' }, -// payload: { type: 'object' }, -// }, -// }, -// }; - -const schema = { - properties: { - query: { - type: 'object', - description: '', - }, - headers: { - type: 'object', - description: '', - }, - body: { - type: 'object', - description: '', - }, - }, -}; - const initialFormValuesMap = { - schema: schema, + methods: [WebhookMethod.Get], + schema: {}, 'security.auth_type': WebhookSecurityAuthType.Basic, 'security.rate_limit.per': RateLimitPerList[0], 'security.max_body_size': WebhookMaxBodySize[0], execution_mode: WebhookExecutionMode.Immediately, + content_types: WebhookContentType.ApplicationJson, }; export function useHandleModeChange(form: UseFormReturn) { diff --git a/web/src/pages/agent/form/begin-form/use-watch-change.ts b/web/src/pages/agent/form/begin-form/use-watch-change.ts index 02158e969..e84ceb07c 100644 --- a/web/src/pages/agent/form/begin-form/use-watch-change.ts +++ b/web/src/pages/agent/form/begin-form/use-watch-change.ts @@ -1,9 +1,10 @@ -import { omit } from 'lodash'; +import { isEmpty, omit } from 'lodash'; import { useEffect } from 'react'; import { UseFormReturn, useWatch } from 'react-hook-form'; import { AgentDialogueMode } from '../../constant'; import { BeginQuery } from '../../interface'; import useGraphStore from '../../store'; +import { BeginFormSchemaType } from './schema'; export function transferInputsArrayToObject(inputs: BeginQuery[] = []) { return inputs.reduce>>((pre, cur) => { @@ -13,6 +14,20 @@ export function transferInputsArrayToObject(inputs: BeginQuery[] = []) { }, {}); } +function transformRequestSchemaToOutput(schema: BeginFormSchemaType['schema']) { + const outputs: Record = {}; + + Object.entries(schema || {}).forEach(([key, value]) => { + if (Array.isArray(value)) { + for (const cur of value) { + outputs[`${key}.${cur.key}`] = { type: cur.type }; + } + } + }); + + return outputs; +} + export function useWatchFormChange(id?: string, form?: UseFormReturn) { let values = useWatch({ control: form?.control }); const updateNodeForm = useGraphStore((state) => state.updateNodeForm); @@ -27,9 +42,9 @@ export function useWatchFormChange(id?: string, form?: UseFormReturn) { // Each property (body, headers, query) should be able to show secondary menu if ( values.mode === AgentDialogueMode.Webhook && - values.schema?.properties + !isEmpty(values.schema) ) { - outputs = { ...values.schema.properties }; + outputs = transformRequestSchemaToOutput(values.schema); } const nextValues = { diff --git a/web/src/pages/agent/form/begin-form/webhook/dynamic-request.tsx b/web/src/pages/agent/form/begin-form/webhook/dynamic-request.tsx new file mode 100644 index 000000000..f84e26d16 --- /dev/null +++ b/web/src/pages/agent/form/begin-form/webhook/dynamic-request.tsx @@ -0,0 +1,103 @@ +import { KeyInput } from '@/components/key-input'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { Button } from '@/components/ui/button'; +import { Separator } from '@/components/ui/separator'; +import { Switch } from '@/components/ui/switch'; +import { loader } from '@monaco-editor/react'; +import { X } from 'lucide-react'; +import { ReactNode } from 'react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { TypesWithArray } from '../../../constant'; +import { buildConversationVariableSelectOptions } from '../../../utils'; +import { DynamicFormHeader } from '../../components/dynamic-fom-header'; + +loader.config({ paths: { vs: '/vs' } }); + +type SelectKeysProps = { + name: string; + label: ReactNode; + tooltip?: string; + keyField?: string; + operatorField?: string; + requiredField?: string; + nodeId?: string; +}; + +const VariableTypeOptions = buildConversationVariableSelectOptions(); + +export function DynamicRequest({ + name, + label, + tooltip, + keyField = 'key', + operatorField = 'type', + requiredField = 'required', +}: SelectKeysProps) { + const form = useFormContext(); + + const { fields, remove, append } = useFieldArray({ + name: name, + control: form.control, + }); + + return ( +
+ + append({ + [keyField]: '', + [operatorField]: TypesWithArray.String, + [requiredField]: false, + }) + } + > +
+ {fields.map((field, index) => { + const keyFieldAlias = `${name}.${index}.${keyField}`; + const operatorFieldAlias = `${name}.${index}.${operatorField}`; + const requiredFieldAlias = `${name}.${index}.${requiredField}`; + + return ( +
+
+
+ + + + + + {(field) => ( + { + field.onChange(val); + }} + options={VariableTypeOptions} + > + )} + + + + {(field) => ( + + )} + +
+
+ + +
+ ); + })} +
+
+ ); +} diff --git a/web/src/pages/agent/form/begin-form/webhook/index.tsx b/web/src/pages/agent/form/begin-form/webhook/index.tsx index 86e844b07..e32af973b 100644 --- a/web/src/pages/agent/form/begin-form/webhook/index.tsx +++ b/web/src/pages/agent/form/begin-form/webhook/index.tsx @@ -1,9 +1,8 @@ import { Collapse } from '@/components/collapse'; import { SelectWithSearch } from '@/components/originui/select-with-search'; import { RAGFlowFormItem } from '@/components/ragflow-form'; -import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Separator } from '@/components/ui/separator'; +import { MultiSelect } from '@/components/ui/multi-select'; import { Textarea } from '@/components/ui/textarea'; import { buildOptions } from '@/utils/form'; import { useFormContext, useWatch } from 'react-hook-form'; @@ -17,10 +16,8 @@ import { WebhookSecurityAuthType, } from '../../../constant'; import { DynamicStringForm } from '../../components/dynamic-string-form'; -import { SchemaDialog } from '../../components/schema-dialog'; -import { SchemaPanel } from '../../components/schema-panel'; -import { useShowSchemaDialog } from '../use-show-schema-dialog'; import { Auth } from './auth'; +import { WebhookRequestSchema } from './request-schema'; import { WebhookResponse } from './response'; const RateLimitPerOptions = buildOptions(RateLimitPerList); @@ -34,21 +31,19 @@ export function WebHook() { name: 'execution_mode', }); - const { - showSchemaDialog, - schemaDialogVisible, - hideSchemaDialog, - handleSchemaDialogOk, - } = useShowSchemaDialog(form); - - const schema = form.getValues('schema'); - return ( <> - + {(field) => ( + + )} + )} - -
- Schema - -
- - {schemaDialogVisible && ( - - )} ); } diff --git a/web/src/pages/agent/form/begin-form/webhook/request-schema.tsx b/web/src/pages/agent/form/begin-form/webhook/request-schema.tsx new file mode 100644 index 000000000..6fe78e60b --- /dev/null +++ b/web/src/pages/agent/form/begin-form/webhook/request-schema.tsx @@ -0,0 +1,26 @@ +import { Collapse } from '@/components/collapse'; +import { useTranslation } from 'react-i18next'; +import { DynamicRequest } from './dynamic-request'; + +export function WebhookRequestSchema() { + const { t } = useTranslation(); + + return ( + {t('flow.webhook.schema')}}> +
+ + + +
+
+ ); +} diff --git a/web/src/pages/agent/utils.ts b/web/src/pages/agent/utils.ts index 592d92e45..efa0a3a33 100644 --- a/web/src/pages/agent/utils.ts +++ b/web/src/pages/agent/utils.ts @@ -35,7 +35,7 @@ import { Operator, TypesWithArray, } from './constant'; -import { BeginFormSchemaType } from './form/begin-form'; +import { BeginFormSchemaType } from './form/begin-form/schema'; import { DataOperationsFormSchemaType } from './form/data-operations-form'; import { ExtractorFormSchemaType } from './form/extractor-form'; import { HierarchicalMergerFormSchemaType } from './form/hierarchical-merger-form'; @@ -326,10 +326,31 @@ export function transformArrayToObject( }, {}); } +function transformRequestSchemaToJsonschema( + schema: BeginFormSchemaType['schema'], +) { + const jsonSchema: Record = {}; + Object.entries(schema || {}).forEach(([key, value]) => { + if (Array.isArray(value)) { + jsonSchema[key] = { + type: 'object', + required: value.filter((x) => x.required).map((x) => x.key), + properties: value.reduce>((pre, cur) => { + pre[cur.key] = { type: cur.type }; + return pre; + }, {}), + }; + } + }); + + return jsonSchema; +} + function transformBeginParams(params: BeginFormSchemaType) { if (params.mode === AgentDialogueMode.Webhook) { return { ...params, + schema: transformRequestSchemaToJsonschema(params.schema), security: { ...params.security, ip_whitelist: params.security?.ip_whitelist.map((x) => x.value),