Merge pull request #858 from SuanmoSuanyangTechnology/feature/file_variable_zy

Feature/file variable zy
This commit is contained in:
yingzhao
2026-04-10 18:16:44 +08:00
committed by GitHub
18 changed files with 593 additions and 97 deletions

View File

@@ -1496,6 +1496,32 @@ export const en = {
resetFeaturesTip: 'Please reconfigure the [Conversation Features - File Upload] settings',
logTitle: 'Description',
range: 'Range',
body: 'BODY Parameter Example',
bodyRequestExample: `{
"message": "user message content",
// string, required, the conversation content entered by the user;
"conversation_id": "conversation_id",
// string, optional, session ID; for multi-turn conversations, pass the conversation_id from the previous response; omit on first request;
"user_id": "user_id",
// string, optional, end-user identifier to distinguish memory and sessions across users; recommended to pass your business system user ID;
"variables": {},
// object, optional (requires application configuration to take effect);
"stream": false,
// boolean, optional, whether to stream the response; defaults to false; when true, returns an SSE event stream;
"thinking": false,
// boolean, optional, whether to enable deep thinking; defaults to false (requires application configuration when true);
"files": [],
// array, optional, list of multimodal attachments (requires application configuration to take effect);
"version":"app_release_id"
// string, optional, application version ID; specify a historical release version ID, or omit to use the currently active version;
}`,
},
userMemory: {
userMemory: 'User Memory',
@@ -2239,6 +2265,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
addvariable: 'Chat Variables',
addChatVariable: 'Add Chat Variable',
editChatVariable: 'Edit Chat Variable',
invalidJSON: 'Invalid JSON format',
config: {
llm: {
@@ -2341,6 +2368,11 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
"eq": 'Is',
"ne": 'Is Not',
},
file: {
"empty": 'Not Exist',
"not_empty": 'Exists',
eq: 'All Are'
},
else_desc: 'Used to define the logic that should be executed when the if condition is not met.',
unset: 'Condition Not Set',
set: 'Set',
@@ -2519,6 +2551,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
'document-extractor.file_selector': 'File variable',
'list-operator.input_list': 'Input list',
},
checkListHasErrors: 'Please resolve all issues in the checklist before publishing',
},
emotionEngine: {
emotionEngineConfig: 'Emotion Engine Configuration',

View File

@@ -831,6 +831,32 @@ export const zh = {
resetFeaturesTip: '请重新配置【对话功能-文件上传】功能',
logTitle: '描述',
range: '范围',
body: 'BODY 参数示例',
bodyRequestExample: `{
"message": "用户消息内容",
// string必填用户输入的对话内容
"conversation_id": "conversation_id",
// string可选会话ID多轮对话时传上一次返回的conversation_id首次不传
"user_id": "user_id",
// string可选终端用户标识用于区分不同用户的记忆和会话建议传业务系统的用户ID
"variables": {},
// object可选需要应用配置才支持生效
"stream": false,
// boolean可选是否流式返回默认 falsetrue时返回SSE事件流
"thinking": false,
// boolean可选是否启用深度思考默认 falsetrue时需要应用配置才支持生效
"files": [],
// array可选多模态附件列表需要应用配置才支持生效
"version":"app_release_id"
//string可选应用版本ID指定历史发布版本ID不传则使用当前生效版本
}`,
},
table: {
totalRecords: '共 {{total}} 条记录'
@@ -2200,6 +2226,7 @@ export const zh = {
addvariable: '会话变量',
addChatVariable: '添加会话变量',
editChatVariable: '编辑会话变量',
invalidJSON: 'JSON 格式不正确',
config: {
llm: {
@@ -2302,6 +2329,11 @@ export const zh = {
"eq": '是',
"ne": '不是',
},
file: {
"empty": '不存在',
"not_empty": '存在',
eq: '全都是'
},
else_desc: '用于定义当 if 条件不满足时应执行的逻辑。',
unset: '条件未设置',
set: '已设置',
@@ -2483,6 +2515,7 @@ export const zh = {
'document-extractor.file_selector': '文件变量',
'list-operator.input_list': '输入变量',
},
checkListHasErrors: '发布前确认检查清单中所有问题均已解决',
},
emotionEngine: {
emotionEngineConfig: '情感引擎配置',

21
web/src/store/workflow.ts Normal file
View File

@@ -0,0 +1,21 @@
/*
* @Author: ZhaoYing
* @Date: 2026-04-10 18:11:19
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-10 18:11:19
*/
import { create } from 'zustand'
import type { NodeCheckResult } from '@/views/Workflow/components/CheckList'
interface WorkflowState {
checkResults: Record<string, NodeCheckResult[]>
setCheckResults: (appId: string, results: NodeCheckResult[]) => void
getCheckResults: (appId: string) => NodeCheckResult[]
}
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
checkResults: {},
setCheckResults: (appId, results) =>
set(state => ({ checkResults: { ...state.checkResults, [appId]: results } })),
getCheckResults: (appId) => get().checkResults[appId] ?? [],
}))

View File

@@ -420,4 +420,7 @@ body {
.ant-picker-outlined:focus,
.ant-picker-outlined:focus-within {
box-shadow: none;
}
.ͼ1.cm-focused {
outline: none;
}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:29
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-26 15:31:36
* @Last Modified time: 2026-04-10 18:09:56
*/
import { type FC, useState, useRef, useEffect } from 'react';
import clsx from 'clsx';
@@ -18,6 +18,7 @@ import ApiKeyConfigModal from './components/ApiKeyConfigModal';
import { getApiKeyList, getApiKeyStats, deleteApiKey } from '@/api/apiKey';
import { maskApiKeys } from '@/utils/apiKeyReplacer'
import RbCard from '@/components/RbCard/Card';
import CodeMirrorEditor from '@/components/CodeMirrorEditor'
/**
* API configuration page component
@@ -155,6 +156,21 @@ const Api: FC<{ application: Application | null }> = ({ application }) => {
{t('common.copy')}
</Button>
</Flex>
<div className="rb:font-medium rb:mt-4!">
{t('application.body')}
</div>
<Flex align="start" justify="space-between" className="rb:text-[#5B6167] rb:mt-3! rb:py-2! rb:px-4! rb:bg-white rb-border rb:rounded-lg rb:leading-5">
<CodeMirrorEditor readOnly={true} value={t('application.bodyRequestExample')} />
<Button className="rb:px-2! rb:h-7! rb:group" onClick={() => handleCopy(t('application.bodyRequestExample'))}>
<div
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
></div>
{t('common.copy')}
</Button>
</Flex>
</RbCard>
<RbCard
title={() => (<Flex align="center">

View File

@@ -2,12 +2,13 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:41
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-26 15:24:41
* @Last Modified time: 2026-04-10 17:02:07
*/
import { type FC, useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx';
import { Space, Input, Form, App, Flex } from 'antd';
import copy from 'copy-to-clipboard';
import Tag, { type TagProps } from './components/Tag'
import RbCard from '@/components/RbCard/Card'
@@ -17,6 +18,7 @@ import ReleaseShareModal from './components/ReleaseShareModal'
import AppSharingModal from './components/AppSharingModal'
import type { Release, ReleaseModalRef, ReleaseShareModalRef, AppSharingModalRef } from './types'
import type { Application } from '@/views/ApplicationManagement/types'
import { useWorkflowStore } from '@/store/workflow'
import Empty from '@/components/Empty'
import { formatDateTime } from '@/utils/format';
import Markdown from '@/components/Markdown'
@@ -40,6 +42,7 @@ const heightClass = 'rb:max-h-[calc(100vh-140px)]'
const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refresh}) => {
const { t } = useTranslation();
const { message } = App.useApp()
const { getCheckResults } = useWorkflowStore()
const releaseModalRef = useRef<ReleaseModalRef>(null)
const releaseShareModalRef = useRef<ReleaseShareModalRef>(null)
const appSharingModalRef = useRef<AppSharingModalRef>(null)
@@ -75,6 +78,10 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
if (!selectedVersion) return
appExport(data.id, data.name, { release_id: selectedVersion.id})
}
const handleCopy = (id: string) => {
copy(id)
message.success(t('common.copySuccess'))
}
return (
<Flex gap={12}>
<div className="rb:w-101 rb:h-full">
@@ -102,7 +109,7 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
</Tag>}
</>}
className={clsx("rb:hover:shadow-[0px_2px_8px_0px_rgba(0,0,0,0.2)]! rb:cursor-pointer rb:bg-white", {
'rb:border-[#171719]!': version.id === selectedVersion.id,
'rb:border! rb:border-[#171719]!': version.id === selectedVersion.id,
'rb:border-[#DFE4ED] ': version.id !== selectedVersion.id
})}
headerType="borderless"
@@ -140,13 +147,30 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
<RbButton type="primary" ghost onClick={() => releaseShareModalRef.current?.handleOpen()}>{t('application.share')}</RbButton>
{data?.type !== 'multi_agent' && <RbButton type="primary" ghost onClick={() => appSharingModalRef.current?.handleOpen()}>{t('application.sharing')}</RbButton>}
</>}
<RbButton type="primary" onClick={() => releaseModalRef.current?.handleOpen()}>{t('application.release')}</RbButton>
<RbButton type="primary" onClick={async () => {
if (data?.type === 'workflow') {
const errors = getCheckResults(data.id)
if (errors.length) {
message.error(t('workflow.checkListHasErrors'))
return
}
}
releaseModalRef.current?.handleOpen()
}}>{t('application.release')}</RbButton>
</Space>
</Flex>
{selectedVersion &&
<Flex gap={16} vertical className={`${heightClass} rb:overflow-y-auto`}>
<RbCard
title={t('application.VersionInformation')}
title={() => <Flex>{t('application.VersionInformation')}
<Flex align="center" className="rb:text-[#5B6167] rb:text-[12px]">
(ID: {selectedVersion.id}
<div className="rb:size-4.5 rb:ml-1 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/common/copy_dark.svg')]"
onClick={() => handleCopy(selectedVersion.id)}
></div>
)
</Flex>
</Flex>}
headerType="borderless"
>
<div className="rb:grid rb:grid-cols-3 rb:gap-4">

View File

@@ -207,7 +207,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
</Flex>}
extra={application?.type === 'workflow' && source !== 'sharing' && activeTab === 'arrangement'
? <Flex align="center" justify="end" gap={10} className="rb:h-8">
<CheckList workflowRef={workflowRef} />
<CheckList workflowRef={workflowRef} appId={application?.id ?? ''} />
<Popover content={t('application.features')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
<div
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/features.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"

View File

@@ -2,9 +2,9 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 18:34:04
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-31 15:35:13
* @Last Modified time: 2026-04-10 16:32:52
*/
import { type FC, useState } from 'react'
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import { Divider, Flex } from 'antd'
@@ -23,7 +23,6 @@ interface DataItem {
const ConversationMemory: FC = () => {
const { t } = useTranslation()
const { id } = useParams()
const [total, setTotal] = useState(0)
return (
<RbCard
@@ -32,14 +31,12 @@ const ConversationMemory: FC = () => {
headerClassName="rb:min-h-[54px]! rb:pt-0! rb:mb-0!"
bodyClassName="rb:p-4! rb:pt-0! rb:pb-1! rb:h-[calc(100%-54px)]!"
className="rb:h-full!"
extra={<div className="rb:text-[#5B6167] rb:leading-5">{t('userMemory.totalRagMemory')}: <span className="rb:font-medium rb:text-[#171719]">{total}</span></div>}
>
<PageScrollList<DataItem>
url={getRagContentUrl}
query={{ end_user_id: id }}
column={1}
gutter={0}
onTotalChange={setTotal}
renderItem={(item, index) => (
<div>
{index !== 0 && <Divider className="rb:mt-1! rb:mb-3! rb:ml-11!" />}

View File

@@ -5,7 +5,7 @@
* @Last Modified time: 2026-04-08 11:05:34
*/
import { forwardRef, useImperativeHandle, useState, useRef, useMemo } from 'react';
import { Form, Input, Select, InputNumber, Button, Row, Col, Flex, Spin } from 'antd';
import { Form, Input, Select, InputNumber, Button, Row, Col, Flex } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
@@ -124,7 +124,7 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
setFileList(list);
}
} else if (variable.type.includes('object') && variable.defaultValue) {
form.setFieldValue('defaultValue', JSON.stringify(variable.defaultValue, null, 2))
form.setFieldValue('defaultValue', variable.defaultValue ? JSON.stringify(variable.defaultValue, null, 2) : undefined)
}
} else {
form.resetFields();
@@ -342,7 +342,19 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
</Form.Item>
)
: (
<Form.Item name="defaultValue" label={t('workflow.config.parameter-extractor.default')}>
<Form.Item
name="defaultValue"
label={t('workflow.config.parameter-extractor.default')}
rules={[
(type === 'object' || type === 'array[object]') ? {
validator: (_, value) => {
if (!value) return Promise.resolve();
try { JSON.parse(value); return Promise.resolve(); }
catch { return Promise.reject(t('workflow.invalidJSON')); }
}
} : {}
]}
>
{type === 'number'
? <InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} />
: type === 'boolean'

View File

@@ -1,4 +1,4 @@
import { type FC, useState, useCallback, useEffect, useRef } from 'react'
import { useState, useCallback, useEffect, useRef, type FC } from 'react'
import { Popover, Flex } from 'antd'
import { WarningFilled } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
@@ -8,17 +8,19 @@ import type { WorkflowRef } from '@/views/ApplicationConfig/types'
import { nodeLibrary } from '../../constant'
import { getToolMethods } from '@/api/tools'
import RbDrawer from '@/components/RbDrawer'
import { useWorkflowStore } from '@/store/workflow'
interface CheckListProps {
workflowRef: React.RefObject<WorkflowRef>
appId: string
}
interface CheckError {
export interface CheckError {
key: string
message: string
}
interface NodeCheckResult {
export interface NodeCheckResult {
id: string
name: string
type: string
@@ -112,10 +114,67 @@ function validateNode(type: string, config: Record<string, any>): CheckError[] {
return errors
}
const CheckList: FC<CheckListProps> = ({ workflowRef }) => {
export async function runCheckOnGraph(
graph: import('@antv/x6').Graph,
t: (key: string) => string
): Promise<NodeCheckResult[]> {
const nodes = graph.getNodes()
const edges = graph.getEdges()
const targetIds = new Set<string>()
const childTargetIds = new Set<string>()
edges.forEach(e => {
targetIds.add(e.getTargetCellId())
const srcData = graph.getCellById(e.getSourceCellId())?.getData()
const tgtData = graph.getCellById(e.getTargetCellId())?.getData()
if (srcData?.cycle && tgtData?.cycle && srcData.cycle === tgtData.cycle) {
childTargetIds.add(e.getTargetCellId())
}
})
const checked: NodeCheckResult[] = []
for (const node of nodes) {
const data = node.getData()
if (!data || ['add-node', 'notes', 'cycle-start', 'break'].includes(data.type)) continue
const errors: CheckError[] = []
const isChildNode = !!data.cycle
const hasIncoming = isChildNode ? childTargetIds.has(node.id) : !['start', 'cycle-start'].includes(data.type) ? targetIds.has(node.id) : true
if (!hasIncoming) errors.push({ key: 'notConnected', message: t('workflow.notConnected') })
const configErrors = validateNode(data.type, data.config ?? {})
configErrors.forEach(e => {
errors.push({ key: e.key, message: `${t(`workflow.checkListErrors.${e.key}`)} ${t('workflow.cannotBeEmpty')}`.trim() })
})
if (data.type === 'tool') {
const toolId = data.config?.tool_id?.defaultValue ?? data.config?.tool_id
const toolParameters = data.config?.tool_parameters?.defaultValue ?? data.config?.tool_parameters ?? {}
if (toolId) {
try {
const methods = await getToolMethods(toolId) as Array<{ name: string; parameters: Array<{ name: string; required: boolean }> }>
const operation = toolParameters?.operation
const method = operation ? methods.find(m => m.name === operation) : methods[0]
if (method) {
method.parameters
.filter(p => p.required && (toolParameters[p.name] === undefined || toolParameters[p.name] === null || toolParameters[p.name] === ''))
.forEach(p => errors.push({ key: 'tool.tool_parameters', message: `${p.name} ${t('workflow.cannotBeEmpty')}` }))
}
} catch { /* ignore */ }
}
}
if (errors.length) {
checked.push({ id: node.id, name: data.name || t(`workflow.${data.type}`), type: data.type, icon: nodeIconMap[data.type] ?? '', errors })
}
}
return checked
}
const CheckList: FC<CheckListProps> = ({ workflowRef, appId }) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [results, setResults] = useState<NodeCheckResult[]>([])
const { setCheckResults, getCheckResults } = useWorkflowStore()
const results = getCheckResults(appId)
const timerRef = useRef<ReturnType<typeof setTimeout>>()
const runCheck = useCallback(async () => {
@@ -195,7 +254,7 @@ const CheckList: FC<CheckListProps> = ({ workflowRef }) => {
const scheduleCheck = useCallback(() => {
clearTimeout(timerRef.current)
timerRef.current = setTimeout(async () => {
setResults(await runCheck())
setCheckResults(appId, await runCheck())
}, 500)
}, [runCheck])
@@ -211,7 +270,7 @@ const CheckList: FC<CheckListProps> = ({ workflowRef }) => {
}
}, [workflowRef.current?.graphRef?.current])
const handleOpen = () => {
const handleOpen = () => {
setOpen(true)
}

View File

@@ -328,7 +328,7 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
};
const content = (
<Flex vertical gap={16} className="rb:max-h-[300px] rb:overflow-y-auto rb:p-3" style={{ minWidth: `${nodeWidth}px` }}>
<Flex vertical gap={16} className="rb:max-h-75 rb:overflow-y-auto rb:p-3" style={{ minWidth: `${nodeWidth}px` }}>
{nodeLibrary.map((category) => {
const sourceNodeData = sourceNode?.getData();
const isChildOfLoop = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'loop');

View File

@@ -4,7 +4,7 @@
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-25 15:23:45
*/
import { type FC } from 'react'
import { useMemo, type FC } from 'react'
import clsx from 'clsx'
import { useTranslation } from 'react-i18next';
import { Form, Button, Select, Space, Divider, InputNumber, type SelectProps, Flex, Row, Col } from 'antd'
@@ -15,7 +15,7 @@ import Editor from '../../Editor'
import { edgeAttrs, nodeWidth } from '../../../constant'
import RbButton from '@/components/RbButton';
import RadioGroupBtn from '../RadioGroupBtn'
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../../../utils'
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../../../utils';
interface CaseListProps {
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; input_type?: string; }[] }>;
@@ -49,6 +49,34 @@ const operatorsObj: { [key: string]: SelectProps['options'] } = {
boolean: [
{ value: 'eq', label: 'workflow.config.if-else.boolean.eq' },
{ value: 'ne', label: 'workflow.config.if-else.boolean.ne' },
],
object: [
{ value: 'eq', label: 'workflow.config.if-else.boolean.eq' },
{ value: 'ne', label: 'workflow.config.if-else.boolean.ne' },
{ value: 'empty', label: 'workflow.config.if-else.empty' },
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
],
file: [
{ value: 'empty', label: 'workflow.config.if-else.file.empty' },
{ value: 'not_empty', label: 'workflow.config.if-else.file.not_empty' },
],
// TODO包含、不包含、全都是
'array[file]': [
{ value: 'empty', label: 'workflow.config.if-else.empty' },
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
// { value: 'eq', label: 'workflow.config.if-else.eq' },
// { value: 'contains', label: 'workflow.config.if-else.contains' },
// { value: 'not_contains', label: 'workflow.config.if-else.not_contains' },
],
'array': [
{ value: 'contains', label: 'workflow.config.if-else.contains' },
{ value: 'not_contains', label: 'workflow.config.if-else.not_contains' },
{ value: 'empty', label: 'workflow.config.if-else.empty' },
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
],
'array[object]': [
{ value: 'empty', label: 'workflow.config.if-else.empty' },
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
]
}
@@ -247,6 +275,22 @@ const CaseList: FC<CaseListProps> = ({
form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], undefined);
};
const filterNumberOptions = useMemo(() => {
const filterList: Suggestion[] = []
options.forEach(vo => {
if (vo.children && vo.children?.length > 0) {
filterList.push({
...vo,
children: vo.children.filter(child => child.dataType === 'number')
})
} else if (vo.dataType === 'number') {
filterList.push(vo)
}
})
return filterList
}, [options])
return (
<>
<Form.List name={name}>
@@ -284,11 +328,15 @@ const CaseList: FC<CaseListProps> = ({
const currentCase = cases[caseIndex] || {};
const currentExpression = currentCase.expressions?.[conditionIndex] || {};
const currentOperator = currentExpression.operator;
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
const leftFieldValue = currentExpression.left;
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
const leftFieldType = leftFieldOption?.dataType;
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty' || leftFieldType === 'file' || leftFieldType === 'array[object]' || leftFieldType === 'array[file]';
const operatorList = leftFieldType && operatorsObj[leftFieldType]
? operatorsObj[leftFieldType]
: leftFieldType && leftFieldType?.includes('array')
? operatorsObj.array
: operatorsObj.default;
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
return (
<Flex key={conditionField.key} gap={4} align="start" className="rb:mb-2!">
@@ -312,7 +360,7 @@ const CaseList: FC<CaseListProps> = ({
<Col flex="1">
<Form.Item name={[conditionField.name, 'operator']} noStyle>
<Select
options={operatorList.map(vo => ({
options={(operatorList ?? []).map(vo => ({
...vo,
label: t(String(vo?.label || ''))
}))}
@@ -328,7 +376,9 @@ const CaseList: FC<CaseListProps> = ({
{!hideRightField && (
<div className="rb:py-1 rb:px-1.5">
{leftFieldType === 'number'
{leftFieldType === 'array[file]'
? <>TODO</>
: leftFieldType === 'number'
? <Flex align="center">
<Form.Item name={[conditionField.name, 'input_type']} noStyle>
<Select
@@ -345,24 +395,24 @@ const CaseList: FC<CaseListProps> = ({
{inputType === 'variable'
? <VariableSelect
placeholder={t('common.pleaseSelect')}
options={options.filter(vo => vo.dataType === 'number')}
options={filterNumberOptions}
allowClear={false}
variant="borderless"
size="small"
/>
: <InputNumber
placeholder={t('common.pleaseEnter')}
variant="borderless"
className="rb:w-full!"
onChange={(value) => form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)}
/>
placeholder={t('common.pleaseEnter')}
variant="borderless"
className="rb:w-full!"
onChange={(value) => form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)}
/>
}
</Form.Item>
</Flex>
: (
<Form.Item name={[conditionField.name, 'right']} noStyle>
{leftFieldType === 'boolean'
? <RadioGroupBtn options={[ { value: true, label: 'True' }, { value: false, label: 'False' }]} type="inner" />
{['boolean', 'array[boolean]'].includes(leftFieldType as string)
? <RadioGroupBtn options={[{ value: true, label: 'True' }, { value: false, label: 'False' }]} type="inner" />
: <Editor options={options} size="small" type="input" />
}
</Form.Item>

View File

@@ -1,4 +1,4 @@
import { type FC } from 'react'
import { type FC, useMemo } from 'react'
import clsx from 'clsx'
import { useTranslation } from 'react-i18next';
import { Form, Button, Select, InputNumber, Input, Divider, type SelectProps, Flex, Space, Row, Col } from 'antd'
@@ -47,6 +47,18 @@ const operatorsObj: { [key: string]: SelectProps['options'] } = {
{ value: 'ne', label: 'workflow.config.if-else.boolean.ne' },
{ value: 'empty', label: 'workflow.config.if-else.empty' },
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
],
// 为空、不为空
object: [
{ value: 'empty', label: 'workflow.config.if-else.empty' },
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
],
// 包含、不包含、为空、不为空
'array': [
{ value: 'contains', label: 'workflow.config.if-else.contains' },
{ value: 'not_contains', label: 'workflow.config.if-else.not_contains' },
{ value: 'empty', label: 'workflow.config.if-else.empty' },
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
]
}
@@ -81,6 +93,23 @@ const ConditionList: FC<CaseListProps> = ({
const currentValue = form.getFieldValue([parentName, 'logical_operator']);
form.setFieldValue([parentName, 'logical_operator'], currentValue === 'and' ? 'or' : 'and');
};
const getNumVariable = useMemo(() => {
const filterList: Suggestion[] = []
options.forEach(variable => {
if (variable.dataType === 'number') {
filterList.push(variable)
} else if (variable.dataType === 'file') {
filterList.push({
...variable,
disabled: true,
children: variable.children?.filter(child => child.dataType === 'number')
})
}
})
return filterList
}, [options])
return (
<>
<Form.List name={[parentName, 'expressions']}>
@@ -125,11 +154,19 @@ const ConditionList: FC<CaseListProps> = ({
const expressions = form.getFieldValue([parentName, 'expressions']) || [];
const currentExpression = expressions[index] || {};
const currentOperator = currentExpression.operator;
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
const leftFieldValue = currentExpression.left;
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
const leftFieldType = leftFieldOption?.dataType;
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty' || ['array[object]', 'object'].includes(leftFieldType as string);
const operatorList = leftFieldType && ['array[object]', 'object'].includes(leftFieldType)
? operatorsObj.object
: leftFieldType && ['array[boolean]', 'boolean'].includes(leftFieldType)
? operatorsObj.boolean
: leftFieldType && operatorsObj[leftFieldType]
? operatorsObj[leftFieldType]
: leftFieldType?.includes('array')
? operatorsObj.array
: operatorsObj.default
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
return (
<Flex
@@ -146,10 +183,11 @@ const ConditionList: FC<CaseListProps> = ({
<Form.Item name={[field.name, 'left']} noStyle>
<VariableSelect
options={options.filter(vo =>
vo.value.includes('sys.') ||
!['file', 'array[file]'].includes(vo.dataType) &&
(vo.value.includes('sys.') ||
vo.value.includes('conv.') ||
vo.nodeData.type === 'loop' ||
(vo.nodeData.cycle && vo.nodeData.cycle === selectedNode?.id)
(vo.nodeData.cycle && vo.nodeData.cycle === selectedNode?.id))
)}
size="small"
allowClear={false}
@@ -163,7 +201,7 @@ const ConditionList: FC<CaseListProps> = ({
<Col flex="96px">
<Form.Item name={[field.name, 'operator']} noStyle>
<Select
options={operatorList.map(vo => ({
options={(operatorList??[]).map(vo => ({
...vo,
label: t(String(vo?.label || ''))
}))}
@@ -198,7 +236,7 @@ const ConditionList: FC<CaseListProps> = ({
? (
<VariableSelect
placeholder={t('common.pleaseSelect')}
options={options.filter(vo => vo.dataType === 'number')}
options={getNumVariable}
allowClear={false}
variant="borderless"
size="small"
@@ -219,7 +257,7 @@ const ConditionList: FC<CaseListProps> = ({
: (
<Form.Item name={[field.name, 'right']} noStyle>
{leftFieldType === 'boolean'
? <RadioGroupBtn options={[ { value: true, label: 'True' }, { value: false, label: 'False' }]} />
? <RadioGroupBtn options={[ { value: true, label: 'True' }, { value: false, label: 'False' }]} type="inner" />
: <Input variant="borderless" placeholder={t('common.pleaseEnter')} />
}
</Form.Item>

View File

@@ -6,6 +6,7 @@ import VariableSelect from '../VariableSelect'
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import RadioGroupBtn from '../RadioGroupBtn'
import { getChildNodeVariables } from '../hooks/useVariableList'
import CodeMirrorEditor from '@/components/CodeMirrorEditor';
interface CycleVar {
name: string;
@@ -28,11 +29,17 @@ const types = [
'string',
'number',
'boolean',
'object',
'array[string]',
'array[number]',
'array[boolean]',
'array[object]'
]
const object_placeholder = `# example
# {
# "name": "redbear",
# "age": 2
# }`
const CycleVarsList: FC<CycleVarsListProps> = ({
value = [],
@@ -144,6 +151,13 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
{ value: true, label: 'True' },
{ value: false, label: 'False' }]}
/>
: currentType === 'object'
? <CodeMirrorEditor
language="json"
placeholder={object_placeholder}
variant="outlined"
size="small"
/>
: (
<Input.TextArea
placeholder={t('common.pleaseEnter')}

View File

@@ -38,12 +38,44 @@ const EditableTable: FC<EditableTableProps> = ({
...(typeOptions.length > 0 && { type: typeOptions[0].value })
});
// Filter options based on boolean type if needed
const booleanFilterOptions = useMemo(() => {
return filterBooleanType
? options.filter(option => option.dataType !== 'boolean')
: options
}, [options, filterBooleanType])
const namefilterOptions = useMemo(() => {
const filterList: Suggestion[] = [];
options.forEach(vo => {
if (vo.dataType === 'file') {
filterList.push({
...vo,
disabled: true,
children: vo.children?.filter(child => child.dataType !== 'boolean')
})
} else if (vo.dataType !== 'array[file]') {
filterList.push(vo)
}
})
return filterList
}, [options])
const valueFilterOptions = (type?: string) => {
let filterOptions: Suggestion[] = []
options.forEach(vo => {
if (type === 'file' && vo.dataType === 'file') {
filterOptions.push({
...vo,
children: []
})
} else if (type === 'file' && vo.dataType === 'array[file]') {
filterOptions.push(vo)
} else if (vo.dataType === 'file') {
filterOptions.push({
...vo,
disabled: true
})
} else if (vo.dataType !== 'array[file]') {
filterOptions.push(vo)
}
})
return filterOptions
}
const getColumns = (remove: (index: number) => void): TableProps<TableRow>['columns'] => {
const hasType = typeOptions.length > 0;
@@ -57,7 +89,7 @@ const EditableTable: FC<EditableTableProps> = ({
render: (_: any, __: TableRow, index: number) => (
<Form.Item name={[index, 'name']} className={formClassName}>
<Editor
options={booleanFilterOptions.filter(option => !option.dataType.includes('file'))}
options={namefilterOptions}
type="input"
className={contentClassName}
size={size}
@@ -105,9 +137,7 @@ const EditableTable: FC<EditableTableProps> = ({
>
{(form) => {
const currentType = form.getFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'type']);
const filteredOptions = currentType === 'file'
? booleanFilterOptions.filter(option => option.dataType.includes('file'))
: booleanFilterOptions.filter(option => !option.dataType.includes('file'));
const filteredOptions = valueFilterOptions(currentType)
return (
<Form.Item name={[index, 'value']} className={formClassName}>

View File

@@ -4,7 +4,7 @@
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-02 17:17:06
*/
import { type FC, useRef, useState } from "react";
import { type FC, useMemo, useRef, useState } from "react";
import { useTranslation } from 'react-i18next'
import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input, Flex, Radio } from 'antd'
import { CaretDownOutlined, CaretRightOutlined, SettingOutlined } from '@ant-design/icons';
@@ -84,6 +84,64 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
setCollapsed((prev: boolean) => !prev)
}
const filterVariables = useMemo(() => {
const filterList: Suggestion[] = []
options.forEach(variable => {
if (['number', 'string'].includes(variable.dataType)) {
filterList.push(variable)
} else if (variable.dataType === 'file') {
filterList.push({
...variable,
disabled: true,
children: variable.children?.filter(child => ['number', 'string'].includes(child.dataType))
})
}
})
return filterList
}, [options])
const filterVariablesWithFile = useMemo(() => {
const filterList: Suggestion[] = []
options.forEach(variable => {
if (['number', 'string', 'file', 'array[file]'].includes(variable.dataType)) {
filterList.push(variable)
}
})
return filterList
}, [options])
const jsonRawFilterVariables = useMemo(() => {
const filterList: Suggestion[] = []
options.forEach(variable => {
if (['number', 'string', 'array[string]', 'array[number]'].includes(variable.dataType)) {
filterList.push(variable)
} else if (variable.dataType === 'file') {
filterList.push({
...variable,
disabled: true,
children: variable.children?.filter(child => ['number', 'string', 'file', 'array[string]', 'array[number]'].includes(child.dataType))
})
}
})
return filterList
}, [options])
const fileFilterVariables = useMemo(() => {
const filterList: Suggestion[] = []
options.forEach(variable => {
if (['array[file]'].includes(variable.dataType)) {
filterList.push(variable)
} else if (variable.dataType === 'file') {
filterList.push({
...variable,
children: []
})
}
})
return filterList
}, [options])
return (
<>
<Flex align="center" justify="space-between" className="rb:mb-1!">
@@ -117,7 +175,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
<Form.Item name="url">
<Editor
key="url"
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
options={filterVariables}
variant="outlined"
type="input"
size="small"
@@ -134,7 +192,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
size="small"
parentName="headers"
title="HEADERS"
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
options={filterVariables}
/>
</Form.Item>
@@ -143,7 +201,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
size="small"
parentName="params"
title="PARAMS"
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
options={filterVariables}
/>
</Form.Item>
@@ -167,7 +225,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
<EditableTable
size="small"
parentName={['body', 'data']}
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number' || vo.dataType.includes('file'))}
options={filterVariablesWithFile}
typeOptions={[
{ label: 'text', value: 'text' },
{ label: 'file', value: 'file' }
@@ -180,7 +238,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
<EditableTable
size="small"
parentName={['body', 'data']}
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
options={filterVariablesWithFile}
filterBooleanType={true}
/>
</Form.Item>
@@ -190,7 +248,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
<MessageEditor
key="json"
parentName={['body', 'data']}
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
options={jsonRawFilterVariables}
isArray={false}
title="JSON"
titleVariant="borderless"
@@ -204,7 +262,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
<MessageEditor
key="raw"
parentName={['body', 'data']}
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
options={jsonRawFilterVariables}
isArray={false}
title="RAW TEXT"
titleVariant="borderless"
@@ -220,7 +278,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
<Editor
key={['body', 'data'].join('_')}
placeholder={t('common.pleaseSelect')}
options={options.filter(vo => vo.dataType.includes('file'))}
options={fileFilterVariables}
type="input"
size="small"
height={28}

View File

@@ -163,25 +163,45 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
form.setFieldsValue(inititalValue)
}
const getNumberOptions = useMemo(() => {
const list: Suggestion[] = []
// string -> string
// integer -> number
// number -> number
// boolean -> boolean【只能选true/false】
// array -> array[file]/array[object]/array[string]/array[number]/array[boolean]
// object -> object/file
const getFilterOptions = (type: string) => {
const filterList: Suggestion[] = [];
options.forEach(vo => {
if (vo.children && vo?.children?.length > 0) {
const filterChild = vo.children.filter(child => child.dataType === 'number')
if (vo.children && vo.children?.length > 0) {
const childOptions = vo.children?.filter(child => child.dataType === type || (type === 'integer' && child.dataType === 'number'))
if (filterChild.length > 0) {
list.push({ ...vo, disabled: vo.dataType !== 'number', children: filterChild })
} else if (vo.dataType === 'number') {
list.push({ ...vo, children: [] })
if (vo.dataType === type
|| (type === 'integer' && vo.dataType === 'number')
|| (type === 'array' && vo.dataType.includes(type))
|| (type === 'object' && vo.dataType === 'object')
) {
filterList.push({
...vo,
children: childOptions
})
} else if (childOptions.length > 0) {
filterList.push({
...vo,
disabled: true,
children: childOptions
})
}
} else if (vo.dataType === 'number') {
list.push({ ...vo })
} else if (vo.dataType === type
|| (type === 'integer' && vo.dataType === 'number')
|| (type === 'array' && vo.dataType.includes(type))
|| (type === 'object' && vo.dataType === 'object')) {
filterList.push(vo)
}
})
console.log('options', options, list)
return list
}, [options])
return filterList
}
return (
<>
@@ -205,7 +225,7 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
<Form.Item
name={['tool_parameters', parameter.name]}
label={<>
{parameter.name}
{parameter.name} <span className="rb:text-[#5B6167] rb:mx-1">({parameter.type})</span>
<Tooltip title={parameter.description} placement="right">
<div className="rb:size-3 rb:ml-0.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/question.svg')]"></div>
</Tooltip>
@@ -220,21 +240,12 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
? <Select size="small" options={parameter.enum.map(vo => ({ value: vo, label: vo }))} placeholder={t('common.pleaseSelect')} />
: parameter.type === 'boolean'
? <Switch size="small" />
: parameter.type === 'integer' || parameter.type === 'number'
? <Editor
variant="outlined"
type="input"
size="small"
height={28}
options={getNumberOptions}
placeholder={t('common.pleaseEnter')}
/>
: <Editor
variant="outlined"
type="input"
size="small"
height={28}
options={options}
options={getFilterOptions(parameter.type)}
placeholder={t('common.pleaseEnter')}
/>
}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 15:39:59
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-08 14:10:40
* @Last Modified time: 2026-04-10 17:24:19
*/
import { type FC, useEffect, useState, useMemo } from "react";
import clsx from 'clsx'
@@ -248,7 +248,7 @@ const Properties: FC<PropertiesProps> = ({
return null;
})() : null;
let filteredList = variableList.filter(variable => variable.dataType !== 'boolean');
let filteredList = variableList.filter(variable => !['boolean', 'object', 'array[boolean]'].includes(variable.dataType));
// If this LLM node is a child of iteration/loop, ensure parent variables are included
if (parentLoopNode) {
@@ -315,23 +315,103 @@ const Properties: FC<PropertiesProps> = ({
return filteredList;
}
if (nodeType === 'knowledge-retrieval' || nodeType === 'parameter-extractor' && key !== 'prompt' || nodeType === 'memory-read' || nodeType === 'question-classifier') {
if (nodeType === 'knowledge-retrieval') {
const allList = addParentIterationVars(variableList);
let filteredList: Suggestion[] = []
allList.forEach(variable => {
if (variable.dataType === 'string') {
filteredList.push(variable)
} else if (variable.dataType === 'file') {
filteredList.push({
...variable,
disabled: true,
children: variable.children.filter((child: Suggestion) => child.dataType === 'string')
})
}
})
return filteredList
}
if ((nodeType === 'parameter-extractor' && key === 'text')
|| (nodeType === 'question-classifier' && ['input_variable', 'categories'].includes(key as string))
) {
const allList = addParentIterationVars(variableList);
let filteredList: Suggestion[] = []
allList.forEach(variable => {
if (variable.dataType === 'string') {
filteredList.push(variable)
} else if (variable.dataType === 'file') {
filteredList.push({
...variable,
children: variable.children.filter((child: Suggestion) => child.dataType === 'string')
})
}
})
return filteredList
}
if ((nodeType === 'parameter-extractor' && key === 'prompt')
|| (nodeType === 'question-classifier' && key === 'user_supplement_prompt')
) {
const allList = addParentIterationVars(variableList);
let filteredList: Suggestion[] = []
allList.forEach(variable => {
if (['string', 'number'].includes(variable.dataType)) {
filteredList.push(variable)
} else if (variable.dataType === 'file') {
filteredList.push({
...variable,
disabled: true,
children: variable.children.filter((child: Suggestion) => ['string', 'number'].includes(child.dataType))
})
}
})
return filteredList
}
if (nodeType === 'memory-read') {
let filteredList = addParentIterationVars(variableList).filter(variable => variable.dataType === 'string');
return filteredList;
}
if (nodeType === 'memory-write') {
let filteredList = addParentIterationVars(variableList).filter(variable => variable.dataType === 'string' || variable.dataType.includes('file'));
return filteredList;
const allList = addParentIterationVars(variableList);
let filteredList: Suggestion[] = []
allList.forEach(variable => {
if (['string', 'array[file]'].includes(variable.dataType)) {
filteredList.push(variable)
} else if (variable.dataType === 'file') {
filteredList.push({
...variable,
children: variable.children.filter((child: Suggestion) => child.dataType === 'string')
})
}
})
return filteredList
}
if (nodeType === 'parameter-extractor' && key === 'prompt') {
let filteredList = addParentIterationVars(variableList).filter(variable => variable.dataType === 'string' || variable.dataType === 'number');
return filteredList;
}
if (nodeType === 'iteration' && key === 'output' || nodeType === 'loop' && key === 'condition') {
if ((nodeType === 'iteration' && key === 'output')) {
if (!selectedNode) return [];
let filteredList = nodeType === 'iteration'
? variableList.filter(variable => variable.value.includes('sys.'))
: addParentIterationVars(variableList).filter(variable => variable.nodeData.type !== 'loop');
let filteredList = variableList.filter(variable => variable.value.includes('sys.'))
const childVariables = getChildNodeVariables(selectedNode, graphRef);
const existingKeys = new Set(filteredList.map(v => v.key));
childVariables.forEach(v => {
if (!existingKeys.has(v.key)) {
filteredList.push(v);
existingKeys.add(v.key);
}
});
return filteredList.filter(variable => variable.dataType !== 'array[file]');
}
if (nodeType === 'loop' && key === 'condition') {
if (!selectedNode) return [];
let filteredList = addParentIterationVars(variableList).filter(variable => variable.nodeData.type !== 'loop');
const childVariables = getChildNodeVariables(selectedNode, graphRef);
const existingKeys = new Set(filteredList.map(v => v.key));
@@ -348,6 +428,23 @@ const Properties: FC<PropertiesProps> = ({
return variableList.filter(variable => variable.dataType.includes('array'));
}
if ((nodeType === 'if-else' && key === 'cases')) {
const allList = addParentIterationVars(variableList);
let filteredList: Suggestion[] = []
allList.forEach(variable => {
if (variable.dataType === 'file') {
filteredList.push({
...variable,
disabled: true,
})
} else {
filteredList.push(variable)
}
})
return filteredList
}
// For all other node types, add parent iteration variables if applicable
let baseList = variableList;
return addParentIterationVars(baseList);
@@ -848,7 +945,7 @@ const Properties: FC<PropertiesProps> = ({
options={getFilteredVariableList(selectedNode?.data?.type, key)}
/>
: config.type === 'editor'
? <Editor options={variableList} variant="outlined" size="small" placeholder={config.placeholder || t('common.pleaseEnter')} />
? <Editor options={getFilteredVariableList(selectedNode?.data?.type, key)} variant="outlined" size="small" placeholder={config.placeholder || t('common.pleaseEnter')} />
: null
}
</Form.Item>