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', resetFeaturesTip: 'Please reconfigure the [Conversation Features - File Upload] settings',
logTitle: 'Description', logTitle: 'Description',
range: 'Range', 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: {
userMemory: 'User Memory', userMemory: 'User Memory',
@@ -2239,6 +2265,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
addvariable: 'Chat Variables', addvariable: 'Chat Variables',
addChatVariable: 'Add Chat Variable', addChatVariable: 'Add Chat Variable',
editChatVariable: 'Edit Chat Variable', editChatVariable: 'Edit Chat Variable',
invalidJSON: 'Invalid JSON format',
config: { config: {
llm: { llm: {
@@ -2341,6 +2368,11 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
"eq": 'Is', "eq": 'Is',
"ne": 'Is Not', "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.', else_desc: 'Used to define the logic that should be executed when the if condition is not met.',
unset: 'Condition Not Set', unset: 'Condition Not Set',
set: 'Set', set: 'Set',
@@ -2519,6 +2551,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
'document-extractor.file_selector': 'File variable', 'document-extractor.file_selector': 'File variable',
'list-operator.input_list': 'Input list', 'list-operator.input_list': 'Input list',
}, },
checkListHasErrors: 'Please resolve all issues in the checklist before publishing',
}, },
emotionEngine: { emotionEngine: {
emotionEngineConfig: 'Emotion Engine Configuration', emotionEngineConfig: 'Emotion Engine Configuration',

View File

@@ -831,6 +831,32 @@ export const zh = {
resetFeaturesTip: '请重新配置【对话功能-文件上传】功能', resetFeaturesTip: '请重新配置【对话功能-文件上传】功能',
logTitle: '描述', logTitle: '描述',
range: '范围', 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: { table: {
totalRecords: '共 {{total}} 条记录' totalRecords: '共 {{total}} 条记录'
@@ -2200,6 +2226,7 @@ export const zh = {
addvariable: '会话变量', addvariable: '会话变量',
addChatVariable: '添加会话变量', addChatVariable: '添加会话变量',
editChatVariable: '编辑会话变量', editChatVariable: '编辑会话变量',
invalidJSON: 'JSON 格式不正确',
config: { config: {
llm: { llm: {
@@ -2302,6 +2329,11 @@ export const zh = {
"eq": '是', "eq": '是',
"ne": '不是', "ne": '不是',
}, },
file: {
"empty": '不存在',
"not_empty": '存在',
eq: '全都是'
},
else_desc: '用于定义当 if 条件不满足时应执行的逻辑。', else_desc: '用于定义当 if 条件不满足时应执行的逻辑。',
unset: '条件未设置', unset: '条件未设置',
set: '已设置', set: '已设置',
@@ -2483,6 +2515,7 @@ export const zh = {
'document-extractor.file_selector': '文件变量', 'document-extractor.file_selector': '文件变量',
'list-operator.input_list': '输入变量', 'list-operator.input_list': '输入变量',
}, },
checkListHasErrors: '发布前确认检查清单中所有问题均已解决',
}, },
emotionEngine: { emotionEngine: {
emotionEngineConfig: '情感引擎配置', 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

@@ -421,3 +421,6 @@ body {
.ant-picker-outlined:focus-within { .ant-picker-outlined:focus-within {
box-shadow: none; box-shadow: none;
} }
.ͼ1.cm-focused {
outline: none;
}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:29:29 * @Date: 2026-02-03 16:29:29
* @Last Modified by: ZhaoYing * @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 { type FC, useState, useRef, useEffect } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
@@ -18,6 +18,7 @@ import ApiKeyConfigModal from './components/ApiKeyConfigModal';
import { getApiKeyList, getApiKeyStats, deleteApiKey } from '@/api/apiKey'; import { getApiKeyList, getApiKeyStats, deleteApiKey } from '@/api/apiKey';
import { maskApiKeys } from '@/utils/apiKeyReplacer' import { maskApiKeys } from '@/utils/apiKeyReplacer'
import RbCard from '@/components/RbCard/Card'; import RbCard from '@/components/RbCard/Card';
import CodeMirrorEditor from '@/components/CodeMirrorEditor'
/** /**
* API configuration page component * API configuration page component
@@ -155,6 +156,21 @@ const Api: FC<{ application: Application | null }> = ({ application }) => {
{t('common.copy')} {t('common.copy')}
</Button> </Button>
</Flex> </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>
<RbCard <RbCard
title={() => (<Flex align="center"> title={() => (<Flex align="center">

View File

@@ -2,12 +2,13 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:29:41 * @Date: 2026-02-03 16:29:41
* @Last Modified by: ZhaoYing * @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 { type FC, useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import clsx from 'clsx'; import clsx from 'clsx';
import { Space, Input, Form, App, Flex } from 'antd'; import { Space, Input, Form, App, Flex } from 'antd';
import copy from 'copy-to-clipboard';
import Tag, { type TagProps } from './components/Tag' import Tag, { type TagProps } from './components/Tag'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard/Card'
@@ -17,6 +18,7 @@ import ReleaseShareModal from './components/ReleaseShareModal'
import AppSharingModal from './components/AppSharingModal' import AppSharingModal from './components/AppSharingModal'
import type { Release, ReleaseModalRef, ReleaseShareModalRef, AppSharingModalRef } from './types' import type { Release, ReleaseModalRef, ReleaseShareModalRef, AppSharingModalRef } from './types'
import type { Application } from '@/views/ApplicationManagement/types' import type { Application } from '@/views/ApplicationManagement/types'
import { useWorkflowStore } from '@/store/workflow'
import Empty from '@/components/Empty' import Empty from '@/components/Empty'
import { formatDateTime } from '@/utils/format'; import { formatDateTime } from '@/utils/format';
import Markdown from '@/components/Markdown' 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 ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refresh}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { message } = App.useApp() const { message } = App.useApp()
const { getCheckResults } = useWorkflowStore()
const releaseModalRef = useRef<ReleaseModalRef>(null) const releaseModalRef = useRef<ReleaseModalRef>(null)
const releaseShareModalRef = useRef<ReleaseShareModalRef>(null) const releaseShareModalRef = useRef<ReleaseShareModalRef>(null)
const appSharingModalRef = useRef<AppSharingModalRef>(null) const appSharingModalRef = useRef<AppSharingModalRef>(null)
@@ -75,6 +78,10 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
if (!selectedVersion) return if (!selectedVersion) return
appExport(data.id, data.name, { release_id: selectedVersion.id}) appExport(data.id, data.name, { release_id: selectedVersion.id})
} }
const handleCopy = (id: string) => {
copy(id)
message.success(t('common.copySuccess'))
}
return ( return (
<Flex gap={12}> <Flex gap={12}>
<div className="rb:w-101 rb:h-full"> <div className="rb:w-101 rb:h-full">
@@ -102,7 +109,7 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
</Tag>} </Tag>}
</>} </>}
className={clsx("rb:hover:shadow-[0px_2px_8px_0px_rgba(0,0,0,0.2)]! rb:cursor-pointer rb:bg-white", { 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 'rb:border-[#DFE4ED] ': version.id !== selectedVersion.id
})} })}
headerType="borderless" 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> <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>} {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> </Space>
</Flex> </Flex>
{selectedVersion && {selectedVersion &&
<Flex gap={16} vertical className={`${heightClass} rb:overflow-y-auto`}> <Flex gap={16} vertical className={`${heightClass} rb:overflow-y-auto`}>
<RbCard <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" headerType="borderless"
> >
<div className="rb:grid rb:grid-cols-3 rb:gap-4"> <div className="rb:grid rb:grid-cols-3 rb:gap-4">

View File

@@ -207,7 +207,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
</Flex>} </Flex>}
extra={application?.type === 'workflow' && source !== 'sharing' && activeTab === 'arrangement' extra={application?.type === 'workflow' && source !== 'sharing' && activeTab === 'arrangement'
? <Flex align="center" justify="end" gap={10} className="rb:h-8"> ? <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]!' }}> <Popover content={t('application.features')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
<div <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" 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 * @Author: ZhaoYing
* @Date: 2026-02-03 18:34:04 * @Date: 2026-02-03 18:34:04
* @Last Modified by: ZhaoYing * @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 { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { Divider, Flex } from 'antd' import { Divider, Flex } from 'antd'
@@ -23,7 +23,6 @@ interface DataItem {
const ConversationMemory: FC = () => { const ConversationMemory: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { id } = useParams() const { id } = useParams()
const [total, setTotal] = useState(0)
return ( return (
<RbCard <RbCard
@@ -32,14 +31,12 @@ const ConversationMemory: FC = () => {
headerClassName="rb:min-h-[54px]! rb:pt-0! rb:mb-0!" 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)]!" bodyClassName="rb:p-4! rb:pt-0! rb:pb-1! rb:h-[calc(100%-54px)]!"
className="rb:h-full!" 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> <PageScrollList<DataItem>
url={getRagContentUrl} url={getRagContentUrl}
query={{ end_user_id: id }} query={{ end_user_id: id }}
column={1} column={1}
gutter={0} gutter={0}
onTotalChange={setTotal}
renderItem={(item, index) => ( renderItem={(item, index) => (
<div> <div>
{index !== 0 && <Divider className="rb:mt-1! rb:mb-3! rb:ml-11!" />} {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 * @Last Modified time: 2026-04-08 11:05:34
*/ */
import { forwardRef, useImperativeHandle, useState, useRef, useMemo } from 'react'; 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 { PlusOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -124,7 +124,7 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
setFileList(list); setFileList(list);
} }
} else if (variable.type.includes('object') && variable.defaultValue) { } 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 { } else {
form.resetFields(); form.resetFields();
@@ -342,7 +342,19 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
</Form.Item> </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' {type === 'number'
? <InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} /> ? <InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} />
: type === 'boolean' : 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 { Popover, Flex } from 'antd'
import { WarningFilled } from '@ant-design/icons' import { WarningFilled } from '@ant-design/icons'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -8,17 +8,19 @@ import type { WorkflowRef } from '@/views/ApplicationConfig/types'
import { nodeLibrary } from '../../constant' import { nodeLibrary } from '../../constant'
import { getToolMethods } from '@/api/tools' import { getToolMethods } from '@/api/tools'
import RbDrawer from '@/components/RbDrawer' import RbDrawer from '@/components/RbDrawer'
import { useWorkflowStore } from '@/store/workflow'
interface CheckListProps { interface CheckListProps {
workflowRef: React.RefObject<WorkflowRef> workflowRef: React.RefObject<WorkflowRef>
appId: string
} }
interface CheckError { export interface CheckError {
key: string key: string
message: string message: string
} }
interface NodeCheckResult { export interface NodeCheckResult {
id: string id: string
name: string name: string
type: string type: string
@@ -112,10 +114,67 @@ function validateNode(type: string, config: Record<string, any>): CheckError[] {
return errors 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 { t } = useTranslation()
const [open, setOpen] = useState(false) 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 timerRef = useRef<ReturnType<typeof setTimeout>>()
const runCheck = useCallback(async () => { const runCheck = useCallback(async () => {
@@ -195,7 +254,7 @@ const CheckList: FC<CheckListProps> = ({ workflowRef }) => {
const scheduleCheck = useCallback(() => { const scheduleCheck = useCallback(() => {
clearTimeout(timerRef.current) clearTimeout(timerRef.current)
timerRef.current = setTimeout(async () => { timerRef.current = setTimeout(async () => {
setResults(await runCheck()) setCheckResults(appId, await runCheck())
}, 500) }, 500)
}, [runCheck]) }, [runCheck])
@@ -211,7 +270,7 @@ const CheckList: FC<CheckListProps> = ({ workflowRef }) => {
} }
}, [workflowRef.current?.graphRef?.current]) }, [workflowRef.current?.graphRef?.current])
const handleOpen = () => { const handleOpen = () => {
setOpen(true) setOpen(true)
} }

View File

@@ -328,7 +328,7 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
}; };
const content = ( 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) => { {nodeLibrary.map((category) => {
const sourceNodeData = sourceNode?.getData(); const sourceNodeData = sourceNode?.getData();
const isChildOfLoop = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'loop'); 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 by: ZhaoYing
* @Last Modified time: 2026-03-25 15:23:45 * @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 clsx from 'clsx'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Form, Button, Select, Space, Divider, InputNumber, type SelectProps, Flex, Row, Col } from 'antd' 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 { edgeAttrs, nodeWidth } from '../../../constant'
import RbButton from '@/components/RbButton'; import RbButton from '@/components/RbButton';
import RadioGroupBtn from '../RadioGroupBtn' import RadioGroupBtn from '../RadioGroupBtn'
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../../../utils' import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../../../utils';
interface CaseListProps { interface CaseListProps {
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; input_type?: string; }[] }>; 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: [ boolean: [
{ value: 'eq', label: 'workflow.config.if-else.boolean.eq' }, { value: 'eq', label: 'workflow.config.if-else.boolean.eq' },
{ value: 'ne', label: 'workflow.config.if-else.boolean.ne' }, { 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); 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 ( return (
<> <>
<Form.List name={name}> <Form.List name={name}>
@@ -284,11 +328,15 @@ const CaseList: FC<CaseListProps> = ({
const currentCase = cases[caseIndex] || {}; const currentCase = cases[caseIndex] || {};
const currentExpression = currentCase.expressions?.[conditionIndex] || {}; const currentExpression = currentCase.expressions?.[conditionIndex] || {};
const currentOperator = currentExpression.operator; const currentOperator = currentExpression.operator;
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
const leftFieldValue = currentExpression.left; const leftFieldValue = currentExpression.left;
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue); const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
const leftFieldType = leftFieldOption?.dataType; 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; const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
return ( return (
<Flex key={conditionField.key} gap={4} align="start" className="rb:mb-2!"> <Flex key={conditionField.key} gap={4} align="start" className="rb:mb-2!">
@@ -312,7 +360,7 @@ const CaseList: FC<CaseListProps> = ({
<Col flex="1"> <Col flex="1">
<Form.Item name={[conditionField.name, 'operator']} noStyle> <Form.Item name={[conditionField.name, 'operator']} noStyle>
<Select <Select
options={operatorList.map(vo => ({ options={(operatorList ?? []).map(vo => ({
...vo, ...vo,
label: t(String(vo?.label || '')) label: t(String(vo?.label || ''))
}))} }))}
@@ -328,7 +376,9 @@ const CaseList: FC<CaseListProps> = ({
{!hideRightField && ( {!hideRightField && (
<div className="rb:py-1 rb:px-1.5"> <div className="rb:py-1 rb:px-1.5">
{leftFieldType === 'number' {leftFieldType === 'array[file]'
? <>TODO</>
: leftFieldType === 'number'
? <Flex align="center"> ? <Flex align="center">
<Form.Item name={[conditionField.name, 'input_type']} noStyle> <Form.Item name={[conditionField.name, 'input_type']} noStyle>
<Select <Select
@@ -345,24 +395,24 @@ const CaseList: FC<CaseListProps> = ({
{inputType === 'variable' {inputType === 'variable'
? <VariableSelect ? <VariableSelect
placeholder={t('common.pleaseSelect')} placeholder={t('common.pleaseSelect')}
options={options.filter(vo => vo.dataType === 'number')} options={filterNumberOptions}
allowClear={false} allowClear={false}
variant="borderless" variant="borderless"
size="small" size="small"
/> />
: <InputNumber : <InputNumber
placeholder={t('common.pleaseEnter')} placeholder={t('common.pleaseEnter')}
variant="borderless" variant="borderless"
className="rb:w-full!" className="rb:w-full!"
onChange={(value) => form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)} onChange={(value) => form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)}
/> />
} }
</Form.Item> </Form.Item>
</Flex> </Flex>
: ( : (
<Form.Item name={[conditionField.name, 'right']} noStyle> <Form.Item name={[conditionField.name, 'right']} noStyle>
{leftFieldType === 'boolean' {['boolean', 'array[boolean]'].includes(leftFieldType as string)
? <RadioGroupBtn options={[ { value: true, label: 'True' }, { value: false, label: 'False' }]} type="inner" /> ? <RadioGroupBtn options={[{ value: true, label: 'True' }, { value: false, label: 'False' }]} type="inner" />
: <Editor options={options} size="small" type="input" /> : <Editor options={options} size="small" type="input" />
} }
</Form.Item> </Form.Item>

View File

@@ -1,4 +1,4 @@
import { type FC } from 'react' import { type FC, useMemo } from 'react'
import clsx from 'clsx' import clsx from 'clsx'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Form, Button, Select, InputNumber, Input, Divider, type SelectProps, Flex, Space, Row, Col } from 'antd' 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: 'ne', label: 'workflow.config.if-else.boolean.ne' },
{ value: 'empty', label: 'workflow.config.if-else.empty' }, { value: 'empty', label: 'workflow.config.if-else.empty' },
{ value: 'not_empty', label: 'workflow.config.if-else.not_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']); const currentValue = form.getFieldValue([parentName, 'logical_operator']);
form.setFieldValue([parentName, 'logical_operator'], currentValue === 'and' ? 'or' : 'and'); 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 ( return (
<> <>
<Form.List name={[parentName, 'expressions']}> <Form.List name={[parentName, 'expressions']}>
@@ -125,11 +154,19 @@ const ConditionList: FC<CaseListProps> = ({
const expressions = form.getFieldValue([parentName, 'expressions']) || []; const expressions = form.getFieldValue([parentName, 'expressions']) || [];
const currentExpression = expressions[index] || {}; const currentExpression = expressions[index] || {};
const currentOperator = currentExpression.operator; const currentOperator = currentExpression.operator;
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
const leftFieldValue = currentExpression.left; const leftFieldValue = currentExpression.left;
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue); const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
const leftFieldType = leftFieldOption?.dataType; 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; const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
return ( return (
<Flex <Flex
@@ -146,10 +183,11 @@ const ConditionList: FC<CaseListProps> = ({
<Form.Item name={[field.name, 'left']} noStyle> <Form.Item name={[field.name, 'left']} noStyle>
<VariableSelect <VariableSelect
options={options.filter(vo => options={options.filter(vo =>
vo.value.includes('sys.') || !['file', 'array[file]'].includes(vo.dataType) &&
(vo.value.includes('sys.') ||
vo.value.includes('conv.') || vo.value.includes('conv.') ||
vo.nodeData.type === 'loop' || vo.nodeData.type === 'loop' ||
(vo.nodeData.cycle && vo.nodeData.cycle === selectedNode?.id) (vo.nodeData.cycle && vo.nodeData.cycle === selectedNode?.id))
)} )}
size="small" size="small"
allowClear={false} allowClear={false}
@@ -163,7 +201,7 @@ const ConditionList: FC<CaseListProps> = ({
<Col flex="96px"> <Col flex="96px">
<Form.Item name={[field.name, 'operator']} noStyle> <Form.Item name={[field.name, 'operator']} noStyle>
<Select <Select
options={operatorList.map(vo => ({ options={(operatorList??[]).map(vo => ({
...vo, ...vo,
label: t(String(vo?.label || '')) label: t(String(vo?.label || ''))
}))} }))}
@@ -198,7 +236,7 @@ const ConditionList: FC<CaseListProps> = ({
? ( ? (
<VariableSelect <VariableSelect
placeholder={t('common.pleaseSelect')} placeholder={t('common.pleaseSelect')}
options={options.filter(vo => vo.dataType === 'number')} options={getNumVariable}
allowClear={false} allowClear={false}
variant="borderless" variant="borderless"
size="small" size="small"
@@ -219,7 +257,7 @@ const ConditionList: FC<CaseListProps> = ({
: ( : (
<Form.Item name={[field.name, 'right']} noStyle> <Form.Item name={[field.name, 'right']} noStyle>
{leftFieldType === 'boolean' {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')} /> : <Input variant="borderless" placeholder={t('common.pleaseEnter')} />
} }
</Form.Item> </Form.Item>

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 15:39:59 * @Date: 2026-02-03 15:39:59
* @Last Modified by: ZhaoYing * @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 { type FC, useEffect, useState, useMemo } from "react";
import clsx from 'clsx' import clsx from 'clsx'
@@ -248,7 +248,7 @@ const Properties: FC<PropertiesProps> = ({
return null; return null;
})() : 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 this LLM node is a child of iteration/loop, ensure parent variables are included
if (parentLoopNode) { if (parentLoopNode) {
@@ -315,23 +315,103 @@ const Properties: FC<PropertiesProps> = ({
return filteredList; 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'); let filteredList = addParentIterationVars(variableList).filter(variable => variable.dataType === 'string');
return filteredList; return filteredList;
} }
if (nodeType === 'memory-write') { if (nodeType === 'memory-write') {
let filteredList = addParentIterationVars(variableList).filter(variable => variable.dataType === 'string' || variable.dataType.includes('file')); const allList = addParentIterationVars(variableList);
return filteredList; 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') { if (nodeType === 'parameter-extractor' && key === 'prompt') {
let filteredList = addParentIterationVars(variableList).filter(variable => variable.dataType === 'string' || variable.dataType === 'number'); let filteredList = addParentIterationVars(variableList).filter(variable => variable.dataType === 'string' || variable.dataType === 'number');
return filteredList; return filteredList;
} }
if (nodeType === 'iteration' && key === 'output' || nodeType === 'loop' && key === 'condition') {
if ((nodeType === 'iteration' && key === 'output')) {
if (!selectedNode) return []; if (!selectedNode) return [];
let filteredList = nodeType === 'iteration' let filteredList = variableList.filter(variable => variable.value.includes('sys.'))
? variableList.filter(variable => variable.value.includes('sys.')) const childVariables = getChildNodeVariables(selectedNode, graphRef);
: addParentIterationVars(variableList).filter(variable => variable.nodeData.type !== 'loop'); 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 childVariables = getChildNodeVariables(selectedNode, graphRef);
const existingKeys = new Set(filteredList.map(v => v.key)); 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')); 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 // For all other node types, add parent iteration variables if applicable
let baseList = variableList; let baseList = variableList;
return addParentIterationVars(baseList); return addParentIterationVars(baseList);
@@ -848,7 +945,7 @@ const Properties: FC<PropertiesProps> = ({
options={getFilteredVariableList(selectedNode?.data?.type, key)} options={getFilteredVariableList(selectedNode?.data?.type, key)}
/> />
: config.type === 'editor' : 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 : null
} }
</Form.Item> </Form.Item>