Feat: Convert webhook data into the format required by the backend.

This commit is contained in:
bill 2025-12-10 16:48:27 +08:00
parent 4ee310970e
commit 5fd8c2c5a4
16 changed files with 215 additions and 134 deletions

View file

@ -20,6 +20,7 @@ import { CirclePlus, HelpCircle, Info } from 'lucide-react';
import { useId, useState, type FC, type FormEvent } from 'react'; import { useId, useState, type FC, type FormEvent } from 'react';
import { useTranslation } from '../../hooks/use-translation'; import { useTranslation } from '../../hooks/use-translation';
import type { NewField, SchemaType } from '../../types/json-schema'; import type { NewField, SchemaType } from '../../types/json-schema';
import { KeyInputProps } from './interface';
import SchemaTypeSelector from './schema-type-selector'; import SchemaTypeSelector from './schema-type-selector';
interface AddFieldButtonProps { interface AddFieldButtonProps {
@ -27,9 +28,10 @@ interface AddFieldButtonProps {
variant?: 'primary' | 'secondary'; variant?: 'primary' | 'secondary';
} }
const AddFieldButton: FC<AddFieldButtonProps> = ({ const AddFieldButton: FC<AddFieldButtonProps & KeyInputProps> = ({
onAddField, onAddField,
variant = 'primary', variant = 'primary',
pattern,
}) => { }) => {
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [fieldName, setFieldName] = useState(''); const [fieldName, setFieldName] = useState('');
@ -120,6 +122,7 @@ const AddFieldButton: FC<AddFieldButtonProps> = ({
placeholder={t.fieldNamePlaceholder} placeholder={t.fieldNamePlaceholder}
className="font-mono text-sm w-full" className="font-mono text-sm w-full"
required required
searchValue={pattern}
/> />
</div> </div>

View file

@ -0,0 +1,9 @@
import React, { useContext } from 'react';
import { KeyInputProps } from './interface';
export const KeyInputContext = React.createContext<KeyInputProps>({});
export function useInputPattern() {
const x = useContext(KeyInputContext);
return x.pattern;
}

View file

@ -0,0 +1 @@
export type KeyInputProps = { pattern?: RegExp | string };

View file

@ -16,6 +16,7 @@ import {
withObjectSchema, withObjectSchema,
} from '../../types/json-schema'; } from '../../types/json-schema';
import type { ValidationTreeNode } from '../../types/validation'; import type { ValidationTreeNode } from '../../types/validation';
import { useInputPattern } from './context';
import TypeDropdown from './type-dropdown'; import TypeDropdown from './type-dropdown';
import TypeEditor from './type-editor'; import TypeEditor from './type-editor';
@ -54,6 +55,8 @@ export const SchemaPropertyEditor: React.FC<SchemaPropertyEditorProps> = ({
'object' as SchemaType, 'object' as SchemaType,
); );
const pattern = useInputPattern();
// Update temp values when props change // Update temp values when props change
useEffect(() => { useEffect(() => {
setTempName(name); setTempName(name);
@ -123,6 +126,7 @@ export const SchemaPropertyEditor: React.FC<SchemaPropertyEditorProps> = ({
className="h-8 text-sm font-medium min-w-[120px] max-w-full z-10" className="h-8 text-sm font-medium min-w-[120px] max-w-full z-10"
autoFocus autoFocus
onFocus={(e) => e.target.select()} onFocus={(e) => e.target.select()}
searchValue={pattern}
/> />
) : ( ) : (
<button <button

View file

@ -8,6 +8,8 @@ import {
import type { JSONSchema, NewField } from '../../types/json-schema'; import type { JSONSchema, NewField } from '../../types/json-schema';
import { asObjectSchema, isBooleanSchema } from '../../types/json-schema'; import { asObjectSchema, isBooleanSchema } from '../../types/json-schema';
import AddFieldButton from './add-field-button'; import AddFieldButton from './add-field-button';
import { KeyInputContext } from './context';
import { KeyInputProps } from './interface';
import SchemaFieldList from './schema-field-list'; import SchemaFieldList from './schema-field-list';
/** @public */ /** @public */
@ -17,9 +19,10 @@ export interface SchemaVisualEditorProps {
} }
/** @public */ /** @public */
const SchemaVisualEditor: FC<SchemaVisualEditorProps> = ({ const SchemaVisualEditor: FC<SchemaVisualEditorProps & KeyInputProps> = ({
schema, schema,
onChange, onChange,
pattern,
}) => { }) => {
const t = useTranslation(); const t = useTranslation();
// Handle adding a top-level field // Handle adding a top-level field
@ -121,7 +124,7 @@ const SchemaVisualEditor: FC<SchemaVisualEditorProps> = ({
return ( return (
<div className="p-4 h-full flex flex-col overflow-auto jsonjoy"> <div className="p-4 h-full flex flex-col overflow-auto jsonjoy">
<div className="mb-6 shrink-0"> <div className="mb-6 shrink-0">
<AddFieldButton onAddField={handleAddField} /> <AddFieldButton onAddField={handleAddField} pattern={pattern} />
</div> </div>
<div className="grow overflow-auto"> <div className="grow overflow-auto">
@ -131,12 +134,14 @@ const SchemaVisualEditor: FC<SchemaVisualEditorProps> = ({
<p className="text-sm">{t.visualEditorNoFieldsHint2}</p> <p className="text-sm">{t.visualEditorNoFieldsHint2}</p>
</div> </div>
) : ( ) : (
<SchemaFieldList <KeyInputContext.Provider value={{ pattern }}>
schema={schema} <SchemaFieldList
onAddField={handleAddField} schema={schema}
onEditField={handleEditField} onAddField={handleAddField}
onDeleteField={handleDeleteField} onEditField={handleEditField}
/> onDeleteField={handleDeleteField}
/>
</KeyInputContext.Provider>
)} )}
</div> </div>
</div> </div>

View file

@ -193,3 +193,19 @@ export enum SwitchLogicOperator {
And = 'and', And = 'and',
Or = 'or', Or = 'or',
} }
export const WebhookAlgorithmList = [
'hs256',
'hs384',
'hs512',
'rs256',
'rs384',
'rs512',
'es256',
'es384',
'es512',
'ps256',
'ps384',
'ps512',
'none',
] as const;

View file

@ -1,32 +1,14 @@
import i18n from '@/locales/config';
import { BeginId } from '@/pages/agent/constant';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
const prefix = BeginId + '@';
interface VariableDisplayProps { interface VariableDisplayProps {
content: string; content: string;
getLabel?: (value?: string) => string | ReactNode; getLabel?: (value?: string) => string | ReactNode;
} }
// This component mimics the VariableNode's decorate function from PromptEditor // This component mimics the VariableNode's decorate function from PromptEditor
function VariableNodeDisplay({ function VariableNodeDisplay({ label }: { label: ReactNode }) {
value,
label,
}: {
value: string;
label: ReactNode;
}) {
let content: ReactNode = <span className="text-accent-primary">{label}</span>; let content: ReactNode = <span className="text-accent-primary">{label}</span>;
if (value.startsWith(prefix)) {
content = (
<div>
<span>{i18n.t(`flow.begin`)}</span> / {content}
</div>
);
}
return <div className="inline-flex items-center mr-1">{content}</div>; return <div className="inline-flex items-center mr-1">{content}</div>;
} }
@ -63,11 +45,7 @@ export function VariableDisplay({ content, getLabel }: VariableDisplayProps) {
if (label && label !== variableValue) { if (label && label !== variableValue) {
// If we found a valid label, render as variable node // If we found a valid label, render as variable node
elements.push( elements.push(
<VariableNodeDisplay <VariableNodeDisplay key={`variable-${index}`} label={label} />,
key={`variable-${index}`}
value={variableValue}
label={label}
/>,
); );
} else { } else {
// If no label found, keep as original text // If no label found, keep as original text

View file

@ -961,3 +961,7 @@ export enum WebhookSecurityAuthType {
Jwt = 'jwt', Jwt = 'jwt',
Hmac = 'hmac', Hmac = 'hmac',
} }
export const RateLimitPerList = ['minute', 'hour', 'day'];
export const WebhookMaxBodySize = ['10MB', '50MB', '100MB', '1000MB'];

View file

@ -12,6 +12,7 @@ import { RAGFlowSelect } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { FormTooltip } from '@/components/ui/tooltip'; import { FormTooltip } from '@/components/ui/tooltip';
import { WebhookAlgorithmList } from '@/constants/agent';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { t } from 'i18next'; import { t } from 'i18next';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
@ -35,53 +36,60 @@ const ModeOptions = [
{ value: AgentDialogueMode.Webhook, label: t('flow.webhook.name') }, { 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<typeof FormSchema>;
function BeginForm({ node }: INextOperatorForm) { function BeginForm({ node }: INextOperatorForm) {
const { t } = useTranslation(); const { t } = useTranslation();
const values = useValues(node); const values = useValues(node);
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(),
})
.optional(),
schema: z.record(z.any()).optional(),
response: z
.object({
status: z.number(),
headers: 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(),
});
const form = useForm({ const form = useForm({
defaultValues: values, defaultValues: values,
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),

View file

@ -1,6 +1,12 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { UseFormReturn } from 'react-hook-form'; import { UseFormReturn } from 'react-hook-form';
import { AgentDialogueMode } from '../../constant'; import {
AgentDialogueMode,
RateLimitPerList,
WebhookExecutionMode,
WebhookMaxBodySize,
WebhookSecurityAuthType,
} from '../../constant';
// const WebhookSchema = { // const WebhookSchema = {
// query: { // query: {
@ -47,11 +53,21 @@ const schema = {
}, },
}; };
const initialFormValuesMap = {
schema: schema,
'security.auth_type': WebhookSecurityAuthType.Basic,
'security.rate_limit.per': RateLimitPerList[0],
'security.max_body_size': WebhookMaxBodySize[0],
execution_mode: WebhookExecutionMode.Immediately,
};
export function useHandleModeChange(form: UseFormReturn<any>) { export function useHandleModeChange(form: UseFormReturn<any>) {
const handleModeChange = useCallback( const handleModeChange = useCallback(
(mode: AgentDialogueMode) => { (mode: AgentDialogueMode) => {
if (mode === AgentDialogueMode.Webhook) { if (mode === AgentDialogueMode.Webhook) {
form.setValue('schema', schema, { shouldDirty: true }); Object.entries(initialFormValuesMap).forEach(([key, value]) => {
form.setValue(key, value, { shouldDirty: true });
});
} }
}, },
[form], [form],

View file

@ -1,27 +1,14 @@
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 { WebhookAlgorithmList } from '@/constants/agent';
import { WebhookSecurityAuthType } from '@/pages/agent/constant'; import { WebhookSecurityAuthType } from '@/pages/agent/constant';
import { buildOptions } from '@/utils/form'; import { buildOptions } from '@/utils/form';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useFormContext, useWatch } from 'react-hook-form'; import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const AlgorithmOptions = buildOptions([ const AlgorithmOptions = buildOptions(WebhookAlgorithmList);
'hs256',
'hs384',
'hs512',
'rs256',
'rs384',
'rs512',
'es256',
'es384',
'es512',
'ps256',
'ps384',
'ps512',
'none',
]);
const RequiredClaimsOptions = buildOptions(['exp', 'sub']); const RequiredClaimsOptions = buildOptions(['exp', 'sub']);

View file

@ -50,7 +50,7 @@ export function DynamicResponse({
name, name,
label, label,
tooltip, tooltip,
keyField = 'variable', keyField = 'key',
valueField = 'value', valueField = 'value',
operatorField = 'type', operatorField = 'type',
}: SelectKeysProps) { }: SelectKeysProps) {

View file

@ -1,3 +1,4 @@
import { Collapse } from '@/components/collapse';
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 { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -8,8 +9,10 @@ import { buildOptions } from '@/utils/form';
import { useFormContext, useWatch } from 'react-hook-form'; import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
RateLimitPerList,
WebhookContentType, WebhookContentType,
WebhookExecutionMode, WebhookExecutionMode,
WebhookMaxBodySize,
WebhookMethod, WebhookMethod,
WebhookSecurityAuthType, WebhookSecurityAuthType,
} from '../../../constant'; } from '../../../constant';
@ -20,7 +23,7 @@ import { useShowSchemaDialog } from '../use-show-schema-dialog';
import { Auth } from './auth'; import { Auth } from './auth';
import { WebhookResponse } from './response'; import { WebhookResponse } from './response';
const RateLimitPerOptions = buildOptions(['minute', 'hour', 'day']); const RateLimitPerOptions = buildOptions(RateLimitPerList);
export function WebHook() { export function WebHook() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -55,40 +58,43 @@ export function WebHook() {
options={buildOptions(WebhookContentType)} options={buildOptions(WebhookContentType)}
></SelectWithSearch> ></SelectWithSearch>
</RAGFlowFormItem> </RAGFlowFormItem>
<Separator></Separator> <Collapse title={<div>Security</div>}>
<> <section className="space-y-4">
<RAGFlowFormItem <RAGFlowFormItem
name="security.auth_type" name="security.auth_type"
label={t('flow.webhook.authType')} label={t('flow.webhook.authType')}
> >
<SelectWithSearch <SelectWithSearch
options={buildOptions(WebhookSecurityAuthType)} options={buildOptions(WebhookSecurityAuthType)}
></SelectWithSearch> ></SelectWithSearch>
</RAGFlowFormItem> </RAGFlowFormItem>
<Auth></Auth> <Auth></Auth>
<RAGFlowFormItem <RAGFlowFormItem
name="security.rate_limit.limit" name="security.rate_limit.limit"
label={t('flow.webhook.limit')} label={t('flow.webhook.limit')}
> >
<Input type="number"></Input> <Input type="number"></Input>
</RAGFlowFormItem> </RAGFlowFormItem>
<RAGFlowFormItem <RAGFlowFormItem
name="security.rate_limit.per" name="security.rate_limit.per"
label={t('flow.webhook.per')} label={t('flow.webhook.per')}
> >
<SelectWithSearch options={RateLimitPerOptions}></SelectWithSearch> <SelectWithSearch options={RateLimitPerOptions}></SelectWithSearch>
</RAGFlowFormItem> </RAGFlowFormItem>
<RAGFlowFormItem <RAGFlowFormItem
name="security.max_body_size" name="security.max_body_size"
label={t('flow.webhook.maxBodySize')} label={t('flow.webhook.maxBodySize')}
> >
<Input></Input> <SelectWithSearch
</RAGFlowFormItem> options={buildOptions(WebhookMaxBodySize)}
<DynamicStringForm ></SelectWithSearch>
name="security.ip_whitelist" </RAGFlowFormItem>
label={t('flow.webhook.ipWhitelist')} <DynamicStringForm
></DynamicStringForm> name="security.ip_whitelist"
</> label={t('flow.webhook.ipWhitelist')}
></DynamicStringForm>
</section>
</Collapse>
<RAGFlowFormItem <RAGFlowFormItem
name="schema" name="schema"
label={t('flow.webhook.schema')} label={t('flow.webhook.schema')}
@ -120,6 +126,7 @@ export function WebHook() {
initialValues={schema} initialValues={schema}
hideModal={hideSchemaDialog} hideModal={hideSchemaDialog}
onOk={handleSchemaDialogOk} onOk={handleSchemaDialogOk}
pattern={''}
></SchemaDialog> ></SchemaDialog>
)} )}
</> </>

View file

@ -1,6 +1,6 @@
import { Collapse } from '@/components/collapse';
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 { Separator } from '@/components/ui/separator';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DynamicResponse } from './dynamic-response'; import { DynamicResponse } from './dynamic-response';
@ -8,8 +8,7 @@ export function WebhookResponse() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <Collapse title={<div>Response</div>}>
<Separator></Separator>
<section className="space-y-4"> <section className="space-y-4">
<RAGFlowFormItem <RAGFlowFormItem
name={'response.status'} name={'response.status'}
@ -26,6 +25,6 @@ export function WebhookResponse() {
label={t('flow.webhook.bodyTemplate')} label={t('flow.webhook.bodyTemplate')}
></DynamicResponse> ></DynamicResponse>
</section> </section>
</> </Collapse>
); );
} }

View file

@ -3,6 +3,7 @@ import {
JsonSchemaVisualizer, JsonSchemaVisualizer,
SchemaVisualEditor, SchemaVisualEditor,
} from '@/components/jsonjoy-builder'; } from '@/components/jsonjoy-builder';
import { KeyInputProps } from '@/components/jsonjoy-builder/components/schema-editor/interface';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Dialog, Dialog,
@ -20,7 +21,8 @@ export function SchemaDialog({
hideModal, hideModal,
onOk, onOk,
initialValues, initialValues,
}: IModalProps<any>) { pattern,
}: IModalProps<any> & KeyInputProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [schema, setSchema] = useState<JSONSchema>(initialValues); const [schema, setSchema] = useState<JSONSchema>(initialValues);
@ -36,7 +38,11 @@ export function SchemaDialog({
</DialogHeader> </DialogHeader>
<section className="flex overflow-auto"> <section className="flex overflow-auto">
<div className="flex-1"> <div className="flex-1">
<SchemaVisualEditor schema={schema} onChange={setSchema} /> <SchemaVisualEditor
schema={schema}
onChange={setSchema}
pattern={pattern}
/>
</div> </div>
<div className="flex-1"> <div className="flex-1">
<JsonSchemaVisualizer schema={schema} onChange={setSchema} /> <JsonSchemaVisualizer schema={schema} onChange={setSchema} />

View file

@ -24,6 +24,7 @@ import {
import pipe from 'lodash/fp/pipe'; import pipe from 'lodash/fp/pipe';
import isObject from 'lodash/isObject'; import isObject from 'lodash/isObject';
import { import {
AgentDialogueMode,
CategorizeAnchorPointPositions, CategorizeAnchorPointPositions,
FileType, FileType,
FileTypeSuffixMap, FileTypeSuffixMap,
@ -34,6 +35,7 @@ import {
Operator, Operator,
TypesWithArray, TypesWithArray,
} from './constant'; } from './constant';
import { BeginFormSchemaType } from './form/begin-form';
import { DataOperationsFormSchemaType } from './form/data-operations-form'; import { DataOperationsFormSchemaType } from './form/data-operations-form';
import { ExtractorFormSchemaType } from './form/extractor-form'; import { ExtractorFormSchemaType } from './form/extractor-form';
import { HierarchicalMergerFormSchemaType } from './form/hierarchical-merger-form'; import { HierarchicalMergerFormSchemaType } from './form/hierarchical-merger-form';
@ -312,6 +314,39 @@ function transformDataOperationsParams(params: DataOperationsFormSchemaType) {
}; };
} }
export function transformArrayToObject(
list?: Array<{ key: string; value: string }>,
) {
if (!Array.isArray(list)) return {};
return list?.reduce<Record<string, any>>((pre, cur) => {
pre[cur.key] = cur.value;
return pre;
}, {});
}
function transformBeginParams(params: BeginFormSchemaType) {
if (params.mode === AgentDialogueMode.Webhook) {
return {
...params,
security: {
...params.security,
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),
},
};
}
return {
...params,
};
}
// construct a dsl based on the node information of the graph // construct a dsl based on the node information of the graph
export const buildDslComponentsByGraph = ( export const buildDslComponentsByGraph = (
nodes: RAGFlowNodeType[], nodes: RAGFlowNodeType[],
@ -361,6 +396,9 @@ export const buildDslComponentsByGraph = (
case Operator.DataOperations: case Operator.DataOperations:
params = transformDataOperationsParams(params); params = transformDataOperationsParams(params);
break; break;
case Operator.Begin:
params = transformBeginParams(params);
break;
default: default:
break; break;
} }