Feat: Add configuration for webhook to the begin node. #10427 (#11875)

### What problem does this PR solve?

Feat: Add configuration for webhook to the begin node. #10427

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-12-10 19:13:57 +08:00
committed by GitHub
parent badf33e3b9
commit 34d29d7e8b
25 changed files with 1097 additions and 117 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>
) : ( ) : (
<KeyInputContext.Provider value={{ pattern }}>
<SchemaFieldList <SchemaFieldList
schema={schema} schema={schema}
onAddField={handleAddField} onAddField={handleAddField}
onEditField={handleEditField} onEditField={handleEditField}
onDeleteField={handleDeleteField} 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

@ -1961,6 +1961,37 @@ Important structured information may include: names, dates, locations, events, k
removeFirst: 'Remove first', removeFirst: 'Remove first',
removeLast: 'Remove last', removeLast: 'Remove last',
}, },
webhook: {
name: 'Webhook',
methods: 'Methods',
contentTypes: 'Content types',
security: 'Security',
schema: 'Schema',
response: 'Response',
executionMode: 'Execution mode',
authMethods: 'Authentication Methods',
authType: 'Authentication Type',
limit: 'Request Limit',
per: 'Time Period',
maxBodySize: 'Maximum Body Size',
ipWhitelist: 'IP Whitelist',
tokenHeader: 'Token Header',
tokenValue: 'Token Value',
username: 'Username',
password: 'Password',
algorithm: 'Algorithm',
secret: 'Secret',
issuer: 'Issuer',
audience: 'Audience',
requiredClaims: 'Required Claims',
header: 'Header',
status: 'Status',
headersTemplate: 'Headers Template',
bodyTemplate: 'Body Template',
basic: 'Basic',
bearer: 'Bearer',
apiKey: 'Api Key',
},
}, },
llmTools: { llmTools: {
bad_calculator: { bad_calculator: {

View File

@ -1755,6 +1755,37 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
removeFirst: '移除第一个', removeFirst: '移除第一个',
removeLast: '移除最后一个', removeLast: '移除最后一个',
}, },
webhook: {
name: '网络钩子',
methods: '方法',
contentTypes: '内容类型',
security: '安全配置',
schema: '模式',
response: '响应',
executionMode: '执行模式',
authMethods: '认证方法',
authType: '认证类型',
limit: '请求限制',
per: '时间周期',
maxBodySize: '最大主体大小',
ipWhitelist: 'IP白名单',
tokenHeader: '令牌头部',
tokenValue: '令牌值',
username: '用户名',
password: '密码',
algorithm: '算法',
secret: '密钥',
issuer: '签发者',
audience: '受众',
requiredClaims: '必需声明',
header: '头部',
status: '状态',
headersTemplate: '头部模板',
bodyTemplate: '主体模板',
basic: '基础认证',
bearer: '承载令牌',
apiKey: 'API密钥',
},
}, },
footer: { footer: {
profile: 'All rights reserved @ React', profile: 'All rights reserved @ React',

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

@ -25,6 +25,7 @@ export * from './pipeline';
export enum AgentDialogueMode { export enum AgentDialogueMode {
Conversational = 'conversational', Conversational = 'conversational',
Task = 'task', Task = 'task',
Webhook = 'Webhook',
} }
import { ModelVariableType } from '@/constants/knowledge'; import { ModelVariableType } from '@/constants/knowledge';
@ -930,3 +931,37 @@ export enum AgentVariableType {
Begin = 'begin', Begin = 'begin',
Conversation = 'conversation', Conversation = 'conversation',
} }
export enum WebhookMethod {
Post = 'POST',
Get = 'GET',
Put = 'PUT',
Patch = 'PATCH',
Delete = 'DELETE',
Head = 'HEAD',
}
export enum WebhookContentType {
ApplicationJson = 'application/json',
MultipartFormData = 'multipart/form-data',
ApplicationXWwwFormUrlencoded = 'application/x-www-form-urlencoded',
TextPlain = 'text/plain',
ApplicationOctetStream = 'application/octet-stream',
}
export enum WebhookExecutionMode {
Immediately = 'Immediately',
Streaming = 'Streaming',
}
export enum WebhookSecurityAuthType {
None = 'none',
Token = 'token',
Basic = 'basic',
Jwt = 'jwt',
Hmac = 'hmac',
}
export const RateLimitPerList = ['minute', 'hour', 'day'];
export const WebhookMaxBodySize = ['10MB', '50MB', '100MB', '1000MB'];

View File

@ -42,9 +42,9 @@ import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output'; import { Output } from '../components/output';
import { PromptEditor } from '../components/prompt-editor'; import { PromptEditor } from '../components/prompt-editor';
import { QueryVariable } from '../components/query-variable'; import { QueryVariable } from '../components/query-variable';
import { SchemaDialog } from '../components/schema-dialog';
import { SchemaPanel } from '../components/schema-panel';
import { AgentTools, Agents } from './agent-tools'; import { AgentTools, Agents } from './agent-tools';
import { StructuredOutputDialog } from './structured-output-dialog';
import { StructuredOutputPanel } from './structured-output-panel';
import { useBuildPromptExtraPromptOptions } from './use-build-prompt-options'; import { useBuildPromptExtraPromptOptions } from './use-build-prompt-options';
import { import {
useHandleShowStructuredOutput, useHandleShowStructuredOutput,
@ -327,19 +327,17 @@ function AgentForm({ node }: INextOperatorForm) {
</Button> </Button>
</div> </div>
<StructuredOutputPanel <SchemaPanel value={structuredOutput}></SchemaPanel>
value={structuredOutput}
></StructuredOutputPanel>
</section> </section>
)} )}
</FormWrapper> </FormWrapper>
</Form> </Form>
{structuredOutputDialogVisible && ( {structuredOutputDialogVisible && (
<StructuredOutputDialog <SchemaDialog
hideModal={hideStructuredOutputDialog} hideModal={hideStructuredOutputDialog}
onOk={handleStructuredOutputDialogOk} onOk={handleStructuredOutputDialogOk}
initialValues={structuredOutput} initialValues={structuredOutput}
></StructuredOutputDialog> ></SchemaDialog>
)} )}
</> </>
); );

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';
@ -24,19 +25,17 @@ import { INextOperatorForm } from '../../interface';
import { ParameterDialog } from './parameter-dialog'; import { ParameterDialog } from './parameter-dialog';
import { QueryTable } from './query-table'; import { QueryTable } from './query-table';
import { useEditQueryRecord } from './use-edit-query'; import { useEditQueryRecord } from './use-edit-query';
import { useHandleModeChange } from './use-handle-mode-change';
import { useValues } from './use-values'; import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-change'; import { useWatchFormChange } from './use-watch-change';
import { WebHook } from './webhook';
const ModeOptions = [ const ModeOptions = [
{ value: AgentDialogueMode.Conversational, label: t('flow.conversational') }, { value: AgentDialogueMode.Conversational, label: t('flow.conversational') },
{ value: AgentDialogueMode.Task, label: t('flow.task') }, { value: AgentDialogueMode.Task, label: t('flow.task') },
{ value: AgentDialogueMode.Webhook, label: t('flow.webhook.name') },
]; ];
function BeginForm({ node }: INextOperatorForm) {
const { t } = useTranslation();
const values = useValues(node);
const FormSchema = z.object({ const FormSchema = z.object({
enablePrologue: z.boolean().optional(), enablePrologue: z.boolean().optional(),
prologue: z.string().trim().optional(), prologue: z.string().trim().optional(),
@ -53,8 +52,44 @@ function BeginForm({ node }: INextOperatorForm) {
}), }),
) )
.optional(), .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 form = useForm({ const form = useForm({
defaultValues: values, defaultValues: values,
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),
@ -72,6 +107,8 @@ function BeginForm({ node }: INextOperatorForm) {
const previousModeRef = useRef(mode); const previousModeRef = useRef(mode);
const { handleModeChange } = useHandleModeChange(form);
useEffect(() => { useEffect(() => {
if ( if (
previousModeRef.current === AgentDialogueMode.Task && previousModeRef.current === AgentDialogueMode.Task &&
@ -111,6 +148,10 @@ function BeginForm({ node }: INextOperatorForm) {
placeholder={t('common.pleaseSelect')} placeholder={t('common.pleaseSelect')}
options={ModeOptions} options={ModeOptions}
{...field} {...field}
onChange={(val) => {
handleModeChange(val);
field.onChange(val);
}}
></RAGFlowSelect> ></RAGFlowSelect>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@ -158,6 +199,9 @@ function BeginForm({ node }: INextOperatorForm) {
)} )}
/> />
)} )}
{mode === AgentDialogueMode.Webhook && <WebHook></WebHook>}
{mode !== AgentDialogueMode.Webhook && (
<>
{/* Create a hidden field to make Form instance record this */} {/* Create a hidden field to make Form instance record this */}
<FormField <FormField
control={form.control} control={form.control}
@ -197,6 +241,8 @@ function BeginForm({ node }: INextOperatorForm) {
submit={ok} submit={ok}
></ParameterDialog> ></ParameterDialog>
)} )}
</>
)}
</Form> </Form>
</section> </section>
); );

View File

@ -0,0 +1,76 @@
import { useCallback } from 'react';
import { UseFormReturn } from 'react-hook-form';
import {
AgentDialogueMode,
RateLimitPerList,
WebhookExecutionMode,
WebhookMaxBodySize,
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,
'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) {
Object.entries(initialFormValuesMap).forEach(([key, value]) => {
form.setValue(key, value, { shouldDirty: true });
});
}
},
[form],
);
return { handleModeChange };
}

View File

@ -0,0 +1,28 @@
import { JSONSchema } from '@/components/jsonjoy-builder';
import { useSetModalState } from '@/hooks/common-hooks';
import { useCallback } from 'react';
import { UseFormReturn } from 'react-hook-form';
export function useShowSchemaDialog(form: UseFormReturn<any>) {
const {
visible: schemaDialogVisible,
showModal: showSchemaDialog,
hideModal: hideSchemaDialog,
} = useSetModalState();
const handleSchemaDialogOk = useCallback(
(values: JSONSchema) => {
// Sync data to canvas
form.setValue('schema', values);
hideSchemaDialog();
},
[form, hideSchemaDialog],
);
return {
schemaDialogVisible,
showSchemaDialog,
hideSchemaDialog,
handleSchemaDialogOk,
};
}

View File

@ -1,6 +1,7 @@
import { omit } from 'lodash'; import { omit } from 'lodash';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form'; import { UseFormReturn, useWatch } from 'react-hook-form';
import { AgentDialogueMode } from '../../constant';
import { BeginQuery } from '../../interface'; import { BeginQuery } from '../../interface';
import useGraphStore from '../../store'; import useGraphStore from '../../store';
@ -20,9 +21,21 @@ export function useWatchFormChange(id?: string, form?: UseFormReturn) {
if (id) { if (id) {
values = form?.getValues() || {}; values = form?.getValues() || {};
let outputs: Record<string, any> = {};
// For webhook mode, use schema properties as direct outputs
// Each property (body, headers, query) should be able to show secondary menu
if (
values.mode === AgentDialogueMode.Webhook &&
values.schema?.properties
) {
outputs = { ...values.schema.properties };
}
const nextValues = { const nextValues = {
...values, ...values,
inputs: transferInputsArrayToObject(values.inputs), inputs: transferInputsArrayToObject(values.inputs),
outputs,
}; };
updateNodeForm(id, nextValues); updateNodeForm(id, nextValues);

View File

@ -0,0 +1,139 @@
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(WebhookAlgorithmList);
const RequiredClaimsOptions = buildOptions(['exp', 'sub']);
export function Auth() {
const { t } = useTranslation();
const form = useFormContext();
const authType = useWatch({
name: 'security.auth_type',
control: form.control,
});
const renderTokenAuth = useCallback(
() => (
<>
<RAGFlowFormItem
name="security.token.token_header"
label={t('flow.webhook.tokenHeader')}
>
<Input></Input>
</RAGFlowFormItem>
<RAGFlowFormItem
name="security.token.token_value"
label={t('flow.webhook.tokenValue')}
>
<Input></Input>
</RAGFlowFormItem>
</>
),
[t],
);
const renderBasicAuth = useCallback(
() => (
<>
<RAGFlowFormItem
name="security.basic_auth.username"
label={t('flow.webhook.username')}
>
<Input></Input>
</RAGFlowFormItem>
<RAGFlowFormItem
name="security.basic_auth.password"
label={t('flow.webhook.password')}
>
<Input></Input>
</RAGFlowFormItem>
</>
),
[t],
);
const renderJwtAuth = useCallback(
() => (
<>
<RAGFlowFormItem
name="security.jwt.algorithm"
label={t('flow.webhook.algorithm')}
>
<SelectWithSearch options={AlgorithmOptions}></SelectWithSearch>
</RAGFlowFormItem>
<RAGFlowFormItem
name="security.jwt.secret"
label={t('flow.webhook.secret')}
>
<Input></Input>
</RAGFlowFormItem>
<RAGFlowFormItem
name="security.jwt.issuer"
label={t('flow.webhook.issuer')}
>
<Input></Input>
</RAGFlowFormItem>
<RAGFlowFormItem
name="security.jwt.audience"
label={t('flow.webhook.audience')}
>
<Input></Input>
</RAGFlowFormItem>
<RAGFlowFormItem
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>
</>
),
[t],
);
const AuthMap = {
[WebhookSecurityAuthType.Token]: renderTokenAuth,
[WebhookSecurityAuthType.Basic]: renderBasicAuth,
[WebhookSecurityAuthType.Jwt]: renderJwtAuth,
[WebhookSecurityAuthType.Hmac]: renderHmacAuth,
[WebhookSecurityAuthType.None]: () => null,
};
return AuthMap[
(authType ?? WebhookSecurityAuthType.None) as WebhookSecurityAuthType
]();
}

View File

@ -0,0 +1,213 @@
import { BoolSegmented } from '@/components/bool-segmented';
import { KeyInput } from '@/components/key-input';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { useIsDarkTheme } from '@/components/theme-provider';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { Textarea } from '@/components/ui/textarea';
import { Editor, loader } from '@monaco-editor/react';
import { X } from 'lucide-react';
import { ReactNode, useCallback } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { InputMode, TypesWithArray } from '../../../constant';
import {
InputModeOptions,
buildConversationVariableSelectOptions,
} from '../../../utils';
import { DynamicFormHeader } from '../../components/dynamic-fom-header';
import { QueryVariable } from '../../components/query-variable';
loader.config({ paths: { vs: '/vs' } });
type SelectKeysProps = {
name: string;
label: ReactNode;
tooltip?: string;
keyField?: string;
valueField?: string;
operatorField?: string;
nodeId?: string;
};
const VariableTypeOptions = buildConversationVariableSelectOptions();
const modeField = 'input_mode';
const ConstantValueMap = {
[TypesWithArray.Boolean]: true,
[TypesWithArray.Number]: 0,
[TypesWithArray.String]: '',
[TypesWithArray.ArrayBoolean]: '[]',
[TypesWithArray.ArrayNumber]: '[]',
[TypesWithArray.ArrayString]: '[]',
[TypesWithArray.ArrayObject]: '[]',
[TypesWithArray.Object]: '{}',
};
export function DynamicResponse({
name,
label,
tooltip,
keyField = 'key',
valueField = 'value',
operatorField = 'type',
}: SelectKeysProps) {
const form = useFormContext();
const isDarkTheme = useIsDarkTheme();
const { fields, remove, append } = useFieldArray({
name: name,
control: form.control,
});
const initializeValue = useCallback(
(mode: string, variableType: string, valueFieldAlias: string) => {
if (mode === InputMode.Variable) {
form.setValue(valueFieldAlias, '', { shouldDirty: true });
} else {
const val = ConstantValueMap[variableType as TypesWithArray];
form.setValue(valueFieldAlias, val, { shouldDirty: true });
}
},
[form],
);
const handleModeChange = useCallback(
(mode: string, valueFieldAlias: string, operatorFieldAlias: string) => {
const variableType = form.getValues(operatorFieldAlias);
initializeValue(mode, variableType, valueFieldAlias);
},
[form, initializeValue],
);
const handleVariableTypeChange = useCallback(
(variableType: string, valueFieldAlias: string, modeFieldAlias: string) => {
const mode = form.getValues(modeFieldAlias);
initializeValue(mode, variableType, valueFieldAlias);
},
[form, initializeValue],
);
const renderParameter = useCallback(
(operatorFieldName: string, modeFieldName: string) => {
const mode = form.getValues(modeFieldName);
const logicalOperator = form.getValues(operatorFieldName);
if (mode === InputMode.Constant) {
if (logicalOperator === TypesWithArray.Boolean) {
return <BoolSegmented></BoolSegmented>;
}
if (logicalOperator === TypesWithArray.Number) {
return <Input className="w-full" type="number"></Input>;
}
if (logicalOperator === TypesWithArray.String) {
return <Textarea></Textarea>;
}
return (
<Editor
height={300}
theme={isDarkTheme ? 'vs-dark' : 'vs'}
language={'json'}
options={{
minimap: { enabled: false },
automaticLayout: true,
}}
/>
);
}
return (
<QueryVariable
types={[logicalOperator]}
hideLabel
pureQuery
></QueryVariable>
);
},
[form, isDarkTheme],
);
return (
<section className="space-y-2">
<DynamicFormHeader
label={label}
tooltip={tooltip}
onClick={() =>
append({
[keyField]: '',
[valueField]: '',
[modeField]: InputMode.Constant,
[operatorField]: TypesWithArray.String,
})
}
></DynamicFormHeader>
<div className="space-y-5">
{fields.map((field, index) => {
const keyFieldAlias = `${name}.${index}.${keyField}`;
const valueFieldAlias = `${name}.${index}.${valueField}`;
const operatorFieldAlias = `${name}.${index}.${operatorField}`;
const modeFieldAlias = `${name}.${index}.${modeField}`;
return (
<section key={field.id} className="flex gap-2">
<div className="flex-1 space-y-3 min-w-0">
<div className="flex items-center">
<RAGFlowFormItem name={keyFieldAlias} className="flex-1 ">
<KeyInput></KeyInput>
</RAGFlowFormItem>
<Separator className="w-2" />
<RAGFlowFormItem name={operatorFieldAlias} className="flex-1">
{(field) => (
<SelectWithSearch
value={field.value}
onChange={(val) => {
handleVariableTypeChange(
val,
valueFieldAlias,
modeFieldAlias,
);
field.onChange(val);
}}
options={VariableTypeOptions}
></SelectWithSearch>
)}
</RAGFlowFormItem>
<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>
<RAGFlowFormItem name={valueFieldAlias} className="w-full">
{renderParameter(operatorFieldAlias, modeFieldAlias)}
</RAGFlowFormItem>
</div>
<Button variant={'ghost'} onClick={() => remove(index)}>
<X />
</Button>
</section>
);
})}
</div>
</section>
);
}

View File

@ -0,0 +1,134 @@
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 { Textarea } from '@/components/ui/textarea';
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';
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 { WebhookResponse } from './response';
const RateLimitPerOptions = buildOptions(RateLimitPerList);
export function WebHook() {
const { t } = useTranslation();
const form = useFormContext();
const executionMode = useWatch({
control: form.control,
name: 'execution_mode',
});
const {
showSchemaDialog,
schemaDialogVisible,
hideSchemaDialog,
handleSchemaDialogOk,
} = useShowSchemaDialog(form);
const schema = form.getValues('schema');
return (
<>
<RAGFlowFormItem name="methods" label={t('flow.webhook.methods')}>
<SelectWithSearch
options={buildOptions(WebhookMethod)}
></SelectWithSearch>
</RAGFlowFormItem>
<RAGFlowFormItem
name="content_types"
label={t('flow.webhook.contentTypes')}
>
<SelectWithSearch
options={buildOptions(WebhookContentType)}
></SelectWithSearch>
</RAGFlowFormItem>
<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')}
className="hidden"
>
<Textarea></Textarea>
</RAGFlowFormItem>
<RAGFlowFormItem
name="execution_mode"
label={t('flow.webhook.executionMode')}
>
<SelectWithSearch
options={buildOptions(WebhookExecutionMode)}
></SelectWithSearch>
</RAGFlowFormItem>
{executionMode === WebhookExecutionMode.Immediately && (
<WebhookResponse></WebhookResponse>
)}
<Separator></Separator>
<section className="flex justify-between items-center">
Schema
<Button variant={'ghost'} onClick={showSchemaDialog}>
{t('flow.structuredOutput.configuration')}
</Button>
</section>
<SchemaPanel value={schema}></SchemaPanel>
{schemaDialogVisible && (
<SchemaDialog
initialValues={schema}
hideModal={hideSchemaDialog}
onOk={handleSchemaDialogOk}
pattern={''}
></SchemaDialog>
)}
</>
);
}

View File

@ -0,0 +1,30 @@
import { Collapse } from '@/components/collapse';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Input } from '@/components/ui/input';
import { useTranslation } from 'react-i18next';
import { DynamicResponse } from './dynamic-response';
export function WebhookResponse() {
const { t } = useTranslation();
return (
<Collapse title={<div>Response</div>}>
<section className="space-y-4">
<RAGFlowFormItem
name={'response.status'}
label={t('flow.webhook.status')}
>
<Input type="number"></Input>
</RAGFlowFormItem>
<DynamicResponse
name="response.headers_template"
label={t('flow.webhook.headersTemplate')}
></DynamicResponse>
<DynamicResponse
name="response.body_template"
label={t('flow.webhook.bodyTemplate')}
></DynamicResponse>
</section>
</Collapse>
);
}

View File

@ -0,0 +1,46 @@
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Trash2 } from 'lucide-react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { DynamicFormHeader, FormListHeaderProps } from './dynamic-fom-header';
type DynamicStringFormProps = { name: string } & FormListHeaderProps;
export function DynamicStringForm({ name, label }: DynamicStringFormProps) {
const form = useFormContext();
const { fields, append, remove } = useFieldArray({
name: name,
control: form.control,
});
return (
<section>
<DynamicFormHeader
label={label}
onClick={() => append({ value: '' })}
></DynamicFormHeader>
<div className="space-y-4">
{fields.map((field, index) => (
<div key={field.id} className="flex items-center gap-2">
<RAGFlowFormItem
name={`${name}.${index}.value`}
label="delimiter"
labelClassName="!hidden"
className="flex-1 !m-0"
>
<Input className="!m-0"></Input>
</RAGFlowFormItem>
<Button
type="button"
variant={'ghost'}
onClick={() => remove(index)}
>
<Trash2 />
</Button>
</div>
))}
</div>
</section>
);
}

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,
@ -16,11 +17,12 @@ import { IModalProps } from '@/interfaces/common';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export function StructuredOutputDialog({ 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 StructuredOutputDialog({
</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

@ -1,6 +1,6 @@
import { JSONSchema, JsonSchemaVisualizer } from '@/components/jsonjoy-builder'; import { JSONSchema, JsonSchemaVisualizer } from '@/components/jsonjoy-builder';
export function StructuredOutputPanel({ value }: { value: JSONSchema }) { export function SchemaPanel({ value }: { value: JSONSchema }) {
return ( return (
<section className="h-48"> <section className="h-48">
<JsonSchemaVisualizer <JsonSchemaVisualizer

View File

@ -2,7 +2,9 @@ import { getStructuredDatatype } from '@/utils/canvas-util';
import { get, isPlainObject } from 'lodash'; import { get, isPlainObject } from 'lodash';
import { ReactNode, useCallback } from 'react'; import { ReactNode, useCallback } from 'react';
import { import {
AgentDialogueMode,
AgentStructuredOutputField, AgentStructuredOutputField,
BeginId,
JsonSchemaDataType, JsonSchemaDataType,
Operator, Operator,
} from '../constant'; } from '../constant';
@ -16,36 +18,94 @@ function getNodeId(value: string) {
} }
export function useShowSecondaryMenu() { export function useShowSecondaryMenu() {
const { getOperatorTypeFromId } = useGraphStore((state) => state); const { getOperatorTypeFromId, getNode } = useGraphStore((state) => state);
const showSecondaryMenu = useCallback( const showSecondaryMenu = useCallback(
(value: string, outputLabel: string) => { (value: string, outputLabel: string) => {
const nodeId = getNodeId(value); const nodeId = getNodeId(value);
return ( const operatorType = getOperatorTypeFromId(nodeId);
getOperatorTypeFromId(nodeId) === Operator.Agent &&
// For Agent nodes, show secondary menu for 'structured' field
if (
operatorType === Operator.Agent &&
outputLabel === AgentStructuredOutputField outputLabel === AgentStructuredOutputField
) {
return true;
}
// For Begin nodes in webhook mode, show secondary menu for schema properties (body, headers, query, etc.)
if (operatorType === Operator.Begin) {
const node = getNode(nodeId);
const mode = get(node, 'data.form.mode');
if (mode === AgentDialogueMode.Webhook) {
// Check if this output field is from the schema
const outputs = get(node, 'data.form.outputs', {});
const outputField = outputs[outputLabel];
// Show secondary menu if the field is an object or has properties
return (
outputField &&
(outputField.type === 'object' ||
(outputField.properties &&
Object.keys(outputField.properties).length > 0))
); );
}
}
return false;
}, },
[getOperatorTypeFromId], [getOperatorTypeFromId, getNode],
); );
return showSecondaryMenu; return showSecondaryMenu;
} }
function useGetBeginOutputsOrSchema() {
const { getNode } = useGraphStore((state) => state);
const getBeginOutputs = useCallback(() => {
const node = getNode(BeginId);
const outputs = get(node, 'data.form.outputs', {});
return outputs;
}, [getNode]);
const getBeginSchema = useCallback(() => {
const node = getNode(BeginId);
const outputs = get(node, 'data.form.schema', {});
return outputs;
}, [getNode]);
return { getBeginOutputs, getBeginSchema };
}
export function useGetStructuredOutputByValue() { export function useGetStructuredOutputByValue() {
const { getNode } = useGraphStore((state) => state); const { getNode, getOperatorTypeFromId } = useGraphStore((state) => state);
const { getBeginOutputs } = useGetBeginOutputsOrSchema();
const getStructuredOutput = useCallback( const getStructuredOutput = useCallback(
(value: string) => { (value: string) => {
const node = getNode(getNodeId(value)); const nodeId = getNodeId(value);
const structuredOutput = get( const node = getNode(nodeId);
const operatorType = getOperatorTypeFromId(nodeId);
const fields = splitValue(value);
const outputLabel = fields.at(1);
let structuredOutput;
if (operatorType === Operator.Agent) {
structuredOutput = get(
node, node,
`data.form.outputs.${AgentStructuredOutputField}`, `data.form.outputs.${AgentStructuredOutputField}`,
); );
} else if (operatorType === Operator.Begin) {
// For Begin nodes in webhook mode, get the specific schema property
const outputs = getBeginOutputs();
if (outputLabel) {
structuredOutput = outputs[outputLabel];
}
}
return structuredOutput; return structuredOutput;
}, },
[getNode], [getBeginOutputs, getNode, getOperatorTypeFromId],
); );
return getStructuredOutput; return getStructuredOutput;
@ -66,13 +126,14 @@ export function useFindAgentStructuredOutputLabel() {
icon?: ReactNode; icon?: ReactNode;
}>, }>,
) => { ) => {
// agent structured output
const fields = splitValue(value); const fields = splitValue(value);
const operatorType = getOperatorTypeFromId(fields.at(0));
// Handle Agent structured fields
if ( if (
getOperatorTypeFromId(fields.at(0)) === Operator.Agent && operatorType === Operator.Agent &&
fields.at(1)?.startsWith(AgentStructuredOutputField) fields.at(1)?.startsWith(AgentStructuredOutputField)
) { ) {
// is agent structured output
const agentOption = options.find((x) => value.includes(x.value)); const agentOption = options.find((x) => value.includes(x.value));
const jsonSchemaFields = fields const jsonSchemaFields = fields
.at(1) .at(1)
@ -84,6 +145,19 @@ export function useFindAgentStructuredOutputLabel() {
value: value, value: value,
}; };
} }
// Handle Begin webhook fields
if (operatorType === Operator.Begin && fields.at(1)) {
const fieldOption = options
.filter((x) => x.parentLabel === BeginId)
.find((x) => value.startsWith(x.value));
return {
...fieldOption,
label: fields.at(1),
value: value,
};
}
}, },
[getOperatorTypeFromId], [getOperatorTypeFromId],
); );
@ -94,6 +168,7 @@ export function useFindAgentStructuredOutputLabel() {
export function useFindAgentStructuredOutputTypeByValue() { export function useFindAgentStructuredOutputTypeByValue() {
const { getOperatorTypeFromId } = useGraphStore((state) => state); const { getOperatorTypeFromId } = useGraphStore((state) => state);
const filterStructuredOutput = useGetStructuredOutputByValue(); const filterStructuredOutput = useGetStructuredOutputByValue();
const { getBeginSchema } = useGetBeginOutputsOrSchema();
const findTypeByValue = useCallback( const findTypeByValue = useCallback(
( (
@ -136,10 +211,12 @@ export function useFindAgentStructuredOutputTypeByValue() {
} }
const fields = splitValue(value); const fields = splitValue(value);
const nodeId = fields.at(0); const nodeId = fields.at(0);
const operatorType = getOperatorTypeFromId(nodeId);
const jsonSchema = filterStructuredOutput(value); const jsonSchema = filterStructuredOutput(value);
// Handle Agent structured fields
if ( if (
getOperatorTypeFromId(nodeId) === Operator.Agent && operatorType === Operator.Agent &&
fields.at(1)?.startsWith(AgentStructuredOutputField) fields.at(1)?.startsWith(AgentStructuredOutputField)
) { ) {
const jsonSchemaFields = fields const jsonSchemaFields = fields
@ -151,13 +228,32 @@ export function useFindAgentStructuredOutputTypeByValue() {
return type; return type;
} }
} }
// Handle Begin webhook fields (body, headers, query, etc.)
if (operatorType === Operator.Begin) {
const outputLabel = fields.at(1);
const schema = getBeginSchema();
if (outputLabel && schema) {
const jsonSchemaFields = fields.at(1);
if (jsonSchemaFields) {
const type = findTypeByValue(schema, jsonSchemaFields);
return type;
}
}
}
}, },
[filterStructuredOutput, findTypeByValue, getOperatorTypeFromId], [
filterStructuredOutput,
findTypeByValue,
getBeginSchema,
getOperatorTypeFromId,
],
); );
return findAgentStructuredOutputTypeByValue; return findAgentStructuredOutputTypeByValue;
} }
// TODO: Consider merging with useFindAgentStructuredOutputLabel
export function useFindAgentStructuredOutputLabelByValue() { export function useFindAgentStructuredOutputLabelByValue() {
const { getNode } = useGraphStore((state) => state); const { getNode } = useGraphStore((state) => state);

View File

@ -314,10 +314,12 @@ export function useFilterQueryVariableOptionsByTypes({
? toLower(y.type).includes(toLower(x)) ? toLower(y.type).includes(toLower(x))
: toLower(y.type) === toLower(x), : toLower(y.type) === toLower(x),
) || ) ||
// agent structured output
isAgentStructured( isAgentStructured(
y.value, y.value,
y.value.slice(-AgentStructuredOutputField.length), y.value.slice(-AgentStructuredOutputField.length),
), // agent structured output ) ||
y.value.startsWith(BeginId), // begin node outputs
), ),
}; };
}) })

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,41 @@ 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) => {
if (cur.key) {
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 +398,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;
} }