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 { useTranslation } from '../../hooks/use-translation';
import type { NewField, SchemaType } from '../../types/json-schema';
import { KeyInputProps } from './interface';
import SchemaTypeSelector from './schema-type-selector';
interface AddFieldButtonProps {
@ -27,9 +28,10 @@ interface AddFieldButtonProps {
variant?: 'primary' | 'secondary';
}
const AddFieldButton: FC<AddFieldButtonProps> = ({
const AddFieldButton: FC<AddFieldButtonProps & KeyInputProps> = ({
onAddField,
variant = 'primary',
pattern,
}) => {
const [dialogOpen, setDialogOpen] = useState(false);
const [fieldName, setFieldName] = useState('');
@ -120,6 +122,7 @@ const AddFieldButton: FC<AddFieldButtonProps> = ({
placeholder={t.fieldNamePlaceholder}
className="font-mono text-sm w-full"
required
searchValue={pattern}
/>
</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,
} from '../../types/json-schema';
import type { ValidationTreeNode } from '../../types/validation';
import { useInputPattern } from './context';
import TypeDropdown from './type-dropdown';
import TypeEditor from './type-editor';
@ -54,6 +55,8 @@ export const SchemaPropertyEditor: React.FC<SchemaPropertyEditorProps> = ({
'object' as SchemaType,
);
const pattern = useInputPattern();
// Update temp values when props change
useEffect(() => {
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"
autoFocus
onFocus={(e) => e.target.select()}
searchValue={pattern}
/>
) : (
<button

View file

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

View file

@ -193,3 +193,19 @@ export enum SwitchLogicOperator {
And = 'and',
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';
const prefix = BeginId + '@';
interface VariableDisplayProps {
content: string;
getLabel?: (value?: string) => string | ReactNode;
}
// This component mimics the VariableNode's decorate function from PromptEditor
function VariableNodeDisplay({
value,
label,
}: {
value: string;
label: ReactNode;
}) {
function VariableNodeDisplay({ label }: { label: ReactNode }) {
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>;
}
@ -63,11 +45,7 @@ export function VariableDisplay({ content, getLabel }: VariableDisplayProps) {
if (label && label !== variableValue) {
// If we found a valid label, render as variable node
elements.push(
<VariableNodeDisplay
key={`variable-${index}`}
value={variableValue}
label={label}
/>,
<VariableNodeDisplay key={`variable-${index}`} label={label} />,
);
} else {
// If no label found, keep as original text

View file

@ -961,3 +961,7 @@ export enum WebhookSecurityAuthType {
Jwt = 'jwt',
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 { 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';
@ -35,53 +36,60 @@ 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<typeof FormSchema>;
function BeginForm({ node }: INextOperatorForm) {
const { t } = useTranslation();
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({
defaultValues: values,
resolver: zodResolver(FormSchema),

View file

@ -1,6 +1,12 @@
import { useCallback } from 'react';
import { UseFormReturn } from 'react-hook-form';
import { AgentDialogueMode } from '../../constant';
import {
AgentDialogueMode,
RateLimitPerList,
WebhookExecutionMode,
WebhookMaxBodySize,
WebhookSecurityAuthType,
} from '../../constant';
// const WebhookSchema = {
// 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>) {
const handleModeChange = useCallback(
(mode: AgentDialogueMode) => {
if (mode === AgentDialogueMode.Webhook) {
form.setValue('schema', schema, { shouldDirty: true });
Object.entries(initialFormValuesMap).forEach(([key, value]) => {
form.setValue(key, value, { shouldDirty: true });
});
}
},
[form],

View file

@ -1,27 +1,14 @@
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 { 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';
const AlgorithmOptions = buildOptions([
'hs256',
'hs384',
'hs512',
'rs256',
'rs384',
'rs512',
'es256',
'es384',
'es512',
'ps256',
'ps384',
'ps512',
'none',
]);
const AlgorithmOptions = buildOptions(WebhookAlgorithmList);
const RequiredClaimsOptions = buildOptions(['exp', 'sub']);

View file

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

View file

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

View file

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

View file

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

View file

@ -24,6 +24,7 @@ import {
import pipe from 'lodash/fp/pipe';
import isObject from 'lodash/isObject';
import {
AgentDialogueMode,
CategorizeAnchorPointPositions,
FileType,
FileTypeSuffixMap,
@ -34,6 +35,7 @@ import {
Operator,
TypesWithArray,
} from './constant';
import { BeginFormSchemaType } from './form/begin-form';
import { DataOperationsFormSchemaType } from './form/data-operations-form';
import { ExtractorFormSchemaType } from './form/extractor-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
export const buildDslComponentsByGraph = (
nodes: RAGFlowNodeType[],
@ -361,6 +396,9 @@ export const buildDslComponentsByGraph = (
case Operator.DataOperations:
params = transformDataOperationsParams(params);
break;
case Operator.Begin:
params = transformBeginParams(params);
break;
default:
break;
}