Merge pull request #428 from SuanmoSuanyangTechnology/feature/workflow_import_zy

Feature/workflow import zy
This commit is contained in:
yingzhao
2026-03-02 18:29:25 +08:00
committed by GitHub
14 changed files with 205 additions and 70 deletions

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>未知节点</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="应用管理-工作流-配置-结束" transform="translate(-81, -222)">
<g id="未知节点" transform="translate(81, 222)">
<rect id="矩形" fill="#4DA8FF" x="0" y="0" width="24" height="24" rx="8"></rect>
<g id="未知" transform="translate(4, 4)" stroke="#FFFFFF">
<g id="编组-29" transform="translate(1.5, 1.5)">
<g id="编组-32" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2">
<rect id="矩形" x="0" y="0" width="2.78571429" height="2.78571429" rx="0.5"></rect>
<rect id="矩形备份-2" x="10.2142857" y="0" width="2.78571429" height="2.78571429" rx="0.5"></rect>
<rect id="矩形" x="0" y="10.2142857" width="2.78571429" height="2.78571429" rx="0.5"></rect>
<rect id="矩形备份-2" x="10.2142857" y="10.2142857" width="2.78571429" height="2.78571429" rx="0.5"></rect>
<line x1="1.39285714" y1="2.78571429" x2="1.39285714" y2="10.2142857" id="路径-10"></line>
<line x1="11.6071429" y1="2.78571429" x2="11.6071429" y2="10.2142857" id="路径-10"></line>
<line x1="2.78571429" y1="1.39285714" x2="10.2142857" y2="1.39285714" id="路径-11"></line>
<line x1="2.78571429" y1="11.6071429" x2="10.2218696" y2="11.6071429" id="路径-12"></line>
</g>
<path d="M6.52432916,9.87401948 C6.13772983,9.87401948 5.82432916,9.56061881 5.82432916,9.17401948 C5.82432916,8.78742016 6.13772983,8.47401948 6.52432916,8.47401948 C6.91092848,8.47401948 7.22432916,8.78742016 7.22432916,9.17401948 C7.22432916,9.56061881 6.91092848,9.87401948 6.52432916,9.87401948 Z M8.31902118,5.62806989 C8.07271219,6.00231919 7.78239354,6.33608471 7.45624872,6.61995827 C7.28497017,6.76209568 7.1395834,6.94091014 7.02820004,7.14642499 C6.96128207,7.37469384 6.93624197,7.61622669 6.95463661,7.85600603 L5.97147432,7.85600603 L5.97147432,7.55843743 C5.96278552,7.25062126 6.0177749,6.94479831 6.131988,6.66574207 C6.29645546,6.367538 6.50993979,6.10844775 6.7606742,5.90275181 C6.96315112,5.72946983 7.15324854,5.53812192 7.32917462,5.33051172 C7.42118807,5.19526753 7.47065705,5.02865038 7.46962638,4.85745421 C7.46993348,4.61851645 7.37673645,4.39189307 7.21546905,4.23943033 C7.02808491,4.06206133 6.7899692,3.96970505 6.54664986,3.98002011 C6.30313821,3.97665394 6.06636072,4.07119573 5.87783981,4.24706618 C5.68380967,4.4710619 5.55131388,4.75479898 5.49660839,5.06346565 C5.4631688,5.29236377 4.49338418,5.39154635 4.50003404,4.92612469 C4.52964278,4.40848145 4.74202371,3.92666766 5.08863079,3.59089435 C5.47706668,3.20376734 5.97741851,2.99447314 6.49314835,3.003393 C7.03166324,2.97113454 7.56128279,3.16980909 7.97792275,3.56037182 C8.31228969,3.88267896 8.5034992,4.35809104 8.49960592,4.85745421 C8.50560934,5.12879049 8.44291047,5.3963476 8.31902118,5.62806989 L8.31902118,5.62806989 Z" id="形状结合" stroke-width="0.2" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -23,6 +23,7 @@ export interface ChatItem {
status?: string; status?: string;
subContent?: Record<string, any>[]; subContent?: Record<string, any>[];
files?: any[]; files?: any[];
error?: string;
} }
/** /**

View File

@@ -1341,7 +1341,9 @@ export const en = {
dynamicMatchSkill: 'Dynamic Match Skill', dynamicMatchSkill: 'Dynamic Match Skill',
executeTask: 'Execute Task', executeTask: 'Execute Task',
importWorkflow: 'Import Workflow', import: 'Import Application',
importWorkflow: 'Third-Party Workflow',
importThirdParty: 'Import Workflow',
platform: 'Source Platform', platform: 'Source Platform',
upload: 'Upload & Parse', upload: 'Upload & Parse',
complex: 'Compatibility Analysis', complex: 'Compatibility Analysis',
@@ -1355,6 +1357,7 @@ export const en = {
gotoList: 'Return to Application List', gotoList: 'Return to Application List',
gotoDetail: 'View Details', gotoDetail: 'View Details',
dify: 'Dify', dify: 'Dify',
pleaseUploadFile: 'Please upload workflow file',
}, },
userMemory: { userMemory: {
userMemory: 'User Memory', userMemory: 'User Memory',
@@ -1985,6 +1988,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
evolutionAndGovernance: 'Evolution & Governance', evolutionAndGovernance: 'Evolution & Governance',
self_optimization: 'Self Optimization', self_optimization: 'Self Optimization',
process_evolution: 'Process Evolution', process_evolution: 'Process Evolution',
unknown: 'Unknown Node',
clickToConfigure: 'Click to configure node parameters', clickToConfigure: 'Click to configure node parameters',
nodeProperties: 'Node Properties', nodeProperties: 'Node Properties',

View File

@@ -736,7 +736,9 @@ export const zh = {
dynamicMatchSkill: '动态匹配技能', dynamicMatchSkill: '动态匹配技能',
executeTask: '执行任务', executeTask: '执行任务',
importWorkflow: '导入工作流', import: '导入应用',
importWorkflow: '第三方工作流',
importThirdParty: '导入工作流',
platform: '来源平台', platform: '来源平台',
upload: '上传与解析', upload: '上传与解析',
complex: '兼容性分析', complex: '兼容性分析',
@@ -751,6 +753,7 @@ export const zh = {
gotoList: '返回应用列表', gotoList: '返回应用列表',
gotoDetail: '查看详情', gotoDetail: '查看详情',
dify: 'Dify', dify: 'Dify',
pleaseUploadFile: '请上传工作流文件',
}, },
table: { table: {
totalRecords: '共 {{total}} 条记录' totalRecords: '共 {{total}} 条记录'
@@ -1982,6 +1985,7 @@ export const zh = {
evolutionAndGovernance: '演化与治理', evolutionAndGovernance: '演化与治理',
self_optimization: '自我优化', self_optimization: '自我优化',
process_evolution: '流程演化', process_evolution: '流程演化',
unknown: '未知节点',
clickToConfigure: '点击配置节点参数', clickToConfigure: '点击配置节点参数',
nodeProperties: '节点属性', nodeProperties: '节点属性',
@@ -2169,6 +2173,9 @@ export const zh = {
output_variables: '输出变量', output_variables: '输出变量',
refreshTip: '同步函数签名至代码', refreshTip: '同步函数签名至代码',
}, },
unknown: {
replaceNodeType: '替换节点'
},
name: '键', name: '键',
type: '类型', type: '类型',
value: '值', value: '值',
@@ -2200,7 +2207,8 @@ export const zh = {
iteration: '迭代', iteration: '迭代',
input_cycle_vars: '初始循环变量', input_cycle_vars: '初始循环变量',
output_cycle_vars: '最终循环变量', output_cycle_vars: '最终循环变量',
} },
sureReplace: '确认替换',
}, },
emotionEngine: { emotionEngine: {
emotionEngineConfig: '情感引擎配置', emotionEngineConfig: '情感引擎配置',

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-28 14:08:14 * @Date: 2026-02-28 14:08:14
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-28 16:20:40 * @Last Modified time: 2026-03-02 17:39:49
*/ */
/** /**
* UploadWorkflowModal Component * UploadWorkflowModal Component
@@ -14,7 +14,7 @@
* 4. Completed - Show success message and options * 4. Completed - Show success message and options
*/ */
import { forwardRef, useImperativeHandle, useState, useMemo } from 'react'; import { forwardRef, useImperativeHandle, useState, useMemo } from 'react';
import { Form, Select, Steps, Flex, Alert, Input, Button, Result } from 'antd'; import { Form, Select, Steps, Flex, Alert, Input, Button, Result, message } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { UploadWorkflowModalData, UploadData, UploadWorkflowModalRef } from '../types' import type { UploadWorkflowModalData, UploadData, UploadWorkflowModalRef } from '../types'
@@ -92,18 +92,22 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
switch(current) { switch(current) {
case 0: // Step 1: Upload file case 0: // Step 1: Upload file
if (!values.file || values.file.length === 0) {
message.warning(t('application.pleaseUploadFile'));
return;
}
const formData = new FormData(); const formData = new FormData();
setFirstFormData(values); setFirstFormData(values);
formData.append('platform', values.platform); formData.append('platform', values.platform);
formData.append('file', values.file[0]); formData.append('file', values.file[0]);
// Call import workflow API // Call import workflow API
importWorkflow(formData) importWorkflow(formData)
.then(res => { .then(res => {
const response = res as UploadData; const response = res as UploadData;
const { errors, warnings } = response; const { errors, warnings } = response;
setData(response); setData(response);
// Navigate to error/warning step if any, otherwise go to confirmation // Navigate to error/warning step if any, otherwise go to confirmation
if (errors.length || warnings.length) { if (errors.length || warnings.length) {
setCurrent(1); setCurrent(1);
@@ -203,7 +207,7 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
{t('common.cancel')} {t('common.cancel')}
</Button>, </Button>,
<Button <Button
key="submit" key="nextStep"
type="primary" type="primary"
loading={loading} loading={loading}
onClick={handleSave} onClick={handleSave}
@@ -215,7 +219,7 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
return null; return null;
default: // Steps 1-2 default: // Steps 1-2
return [ return [
<Button onClick={handleClose}> <Button key="cancel" onClick={handleClose}>
{t('common.cancel')} {t('common.cancel')}
</Button>, </Button>,
<Button key="back" onClick={handleLastStep}> <Button key="back" onClick={handleLastStep}>
@@ -235,7 +239,7 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
return ( return (
<RbModal <RbModal
title={t('application.importWorkflow')} title={t('application.importThirdParty')}
open={visible} open={visible}
onCancel={handleClose} onCancel={handleClose}
okText={t('application.nextStep')} okText={t('application.nextStep')}
@@ -262,7 +266,10 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
platform: 'dify' platform: 'dify'
}} }}
> >
<Form.Item name="platform" label={t('application.platform')}> <Form.Item
name="platform" label={t('application.platform')}
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<Select <Select
placeholder={t('common.pleaseSelect')} placeholder={t('common.pleaseSelect')}
options={['dify'].map(value => ({ options={['dify'].map(value => ({
@@ -270,16 +277,17 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
}))} }))}
/> />
</Form.Item> </Form.Item>
<Form.Item name="file" valuePropName="fileList" noStyle> <Form.Item
name="file"
valuePropName="fileList"
noStyle
>
<UploadFiles <UploadFiles
isAutoUpload={false} isAutoUpload={false}
isCanDrag={true} isCanDrag={true}
fileSize={100} fileSize={100}
maxCount={1} maxCount={1}
fileType={['yml', 'yaml', 'zip', 'json']} fileType={['yml', 'yaml', 'zip', 'json']}
onChange={(fileList) => {
console.log('文件列表变化:', fileList);
}}
/> />
</Form.Item> </Form.Item>
</Form> </Form>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:34:12 * @Date: 2026-02-03 16:34:12
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-09 13:52:22 * @Last Modified time: 2026-03-02 17:48:51
*/ */
/** /**
* Application Management Page * Application Management Page
@@ -12,7 +12,7 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Row, Col, App, Select, Space } from 'antd'; import { Button, Row, Col, App, Select, Space, Dropdown } from 'antd';
import clsx from 'clsx'; import clsx from 'clsx';
import { DeleteOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
@@ -86,6 +86,13 @@ const ApplicationManagement: React.FC = () => {
const handleImport = () => { const handleImport = () => {
uploadWorkflowModalRef.current?.handleOpen() uploadWorkflowModalRef.current?.handleOpen()
} }
const handleClick = ({ key }: { key: string } ) => {
switch (key) {
case 'thirdParty':
handleImport()
break;
}
}
return ( return (
<> <>
<Row gutter={16} className="rb:mb-4"> <Row gutter={16} className="rb:mb-4">
@@ -111,9 +118,16 @@ const ApplicationManagement: React.FC = () => {
</Col> </Col>
<Col span={12} className="rb:text-right"> <Col span={12} className="rb:text-right">
<Space size={12}> <Space size={12}>
<Button onClick={handleImport}> <Dropdown
{t('application.importWorkflow')} menu={{ items: [
</Button> { key: 'thirdParty', label: t('application.importWorkflow') },
], onClick: handleClick }}
placement="bottomRight"
>
<Button>
{t('application.import')}
</Button>
</Dropdown>
<Button type="primary" onClick={handleCreate}> <Button type="primary" onClick={handleCreate}>
{t('application.createApplication')} {t('application.createApplication')}
</Button> </Button>

View File

@@ -320,7 +320,8 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
newList[lastIndex] = { newList[lastIndex] = {
...newList[lastIndex], ...newList[lastIndex],
status, status,
content: newList[lastIndex].content === '' ? null : newList[lastIndex].content error,
content: newList[lastIndex].content === '' ? null : newList[lastIndex].content,
} }
} }
return newList return newList

View File

@@ -217,14 +217,20 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
children: ( children: (
detail detail
? ( ? (
<div className="rb:bg-[#FBFDFF] rb:rounded-md"> <div className="rb:bg-[#FBFDFF] rb:rounded-md">
<Button type="link" icon={<ArrowLeftOutlined />} onClick={() => setDetail(null)} className="rb:px-0! rb:text-[12px]!"> <Button type="link" icon={<ArrowLeftOutlined />} onClick={() => setDetail(null)} className="rb:px-0! rb:text-[12px]!">
{t('common.return')} {t('common.return')}
</Button> </Button>
{renderDetailChild(detail.subContent)} {renderDetailChild(detail.subContent)}
</div> </div>
) )
: renderChild(item.subContent) : <>
{item.error
? <div className={clsx("rb:bg-[#FBFDFF] rb:rounded-md rb:py-2 rb:px-3 ", getStatus('failed'))}>
<Markdown content={item.error} />
</div>
: renderChild(item.subContent)
}</>
) )
}]} }]}
/> />

View File

@@ -30,7 +30,8 @@ export interface LexicalEditorProps {
lineHeight?: number; lineHeight?: number;
size?: 'default' | 'small'; size?: 'default' | 'small';
type?: 'input' | 'textarea', type?: 'input' | 'textarea',
language?: 'string' | 'jinja2' language?: 'string' | 'jinja2';
className?: string;
} }
const theme = { const theme = {
@@ -58,7 +59,9 @@ const Editor: FC<LexicalEditorProps> =({
variant = 'borderless', variant = 'borderless',
size = 'default', size = 'default',
type = 'textarea', type = 'textarea',
language = 'string' language = 'string',
height,
className
}) => { }) => {
const [_count, setCount] = useState(0); const [_count, setCount] = useState(0);
const [enableJinja2, setEnableJinja2] = useState(false) const [enableJinja2, setEnableJinja2] = useState(false)
@@ -156,23 +159,23 @@ const Editor: FC<LexicalEditorProps> =({
}; };
const minheight = useMemo(() => { const minheight = useMemo(() => {
if (type === 'input') { if (type === 'input') {
return `${size === 'small' ? 26 : 30}px` return `${height ? height : size === 'small' ? 28 : 30}px`
} }
return `${size === 'small' ? 60 : 120}px` return `${height ? height : size === 'small' ? 60 : 120}px`
}, [type, size]) }, [type, size, height])
const fontSize = useMemo(() => { const fontSize = useMemo(() => {
return `${size === 'small' ? 12 : 14}px` return `${size === 'small' ? 12 : 14}px`
}, [size]) }, [size])
const lineHeight = useMemo(() => { const lineHeight = useMemo(() => {
return `${size === 'small' ? 16 : 20}px` return `${height ? height : size === 'small' ? 16 : 20}px`
}, [size]) }, [size])
const placeHolderMinheight = useMemo(() => { const placeHolderMinheight = useMemo(() => {
return `${size === 'small' ? 16 : 30}px` return `${height ? height : size === 'small' ? 16 : 30}px`
}, [type, size]) }, [type, size, height])
return ( return (
<LexicalComposer initialConfig={initialConfig}> <LexicalComposer initialConfig={initialConfig}>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }} className={className}>
<RichTextPlugin <RichTextPlugin
contentEditable={ contentEditable={
enableLineNumbers ? ( enableLineNumbers ? (

View File

@@ -1,9 +1,11 @@
import { type FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button, Select, Table, Form, type TableProps } from 'antd'; import { Button, Select, Table, Form, type TableProps } from 'antd';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'; import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin';
import Empty from '@/components/Empty'; import Empty from '@/components/Empty';
import VariableSelect from '../VariableSelect'; import Editor from '../../Editor'
export interface TableRow { export interface TableRow {
key?: string; key?: string;
@@ -21,7 +23,7 @@ interface EditableTableProps {
size?: "small" size?: "small"
} }
const EditableTable: React.FC<EditableTableProps> = ({ const EditableTable: FC<EditableTableProps> = ({
parentName, parentName,
title, title,
options = [], options = [],
@@ -37,6 +39,13 @@ const EditableTable: React.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 booleanFilterOptions = useMemo(() => {
return filterBooleanType
? options.filter(option => option.dataType !== 'boolean')
: options
}, [options, filterBooleanType])
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;
const cellClassName="rb:p-1!" const cellClassName="rb:p-1!"
@@ -49,14 +58,12 @@ const EditableTable: React.FC<EditableTableProps> = ({
className: cellClassName, className: cellClassName,
render: (_: any, __: TableRow, index: number) => ( render: (_: any, __: TableRow, index: number) => (
<Form.Item name={[index, 'name']} noStyle> <Form.Item name={[index, 'name']} noStyle>
<VariableSelect <Editor
placeholder={t('common.pleaseSelect')} options={booleanFilterOptions}
// size="small" type="input"
options={options}
filterBooleanType={filterBooleanType}
popupMatchSelectWidth={false}
className={contentClassName} className={contentClassName}
size={size} size={size}
height={16}
/> />
</Form.Item> </Form.Item>
) )
@@ -101,19 +108,17 @@ const EditableTable: React.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 = currentType === 'file'
? options.filter(option => option.dataType === 'file') ? booleanFilterOptions.filter(option => option.dataType.includes('file'))
: options; : booleanFilterOptions;
return ( return (
<Form.Item name={[index, 'value']} noStyle> <Form.Item name={[index, 'value']} noStyle>
<VariableSelect <Editor
placeholder={t('common.pleaseSelect')}
// size="small"
options={filteredOptions} options={filteredOptions}
filterBooleanType={filterBooleanType} type="input"
popupMatchSelectWidth={false}
className={contentClassName} className={contentClassName}
size={size} size={size}
height={16}
/> />
</Form.Item> </Form.Item>
); );

View File

@@ -1,8 +1,8 @@
/* /*
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-09 18:35:43 * @Date: 2026-02-09 18:35:43
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-09 18:35:43 * @Last Modified time: 2026-03-02 17:24:51
*/ */
import { type FC, useRef, useState } from "react"; import { type FC, useRef, useState } from "react";
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -13,7 +13,6 @@ import Editor from '../../Editor'
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import AuthConfigModal from './AuthConfigModal' import AuthConfigModal from './AuthConfigModal'
import type { AuthConfigModalRef, HttpRequestConfigForm } from './types' import type { AuthConfigModalRef, HttpRequestConfigForm } from './types'
import VariableSelect from "../VariableSelect";
import MessageEditor from '../MessageEditor' import MessageEditor from '../MessageEditor'
import EditableTable from './EditableTable' import EditableTable from './EditableTable'
import { portTextAttrs } from '../../../constant' import { portTextAttrs } from '../../../constant'
@@ -159,7 +158,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={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number' || vo.dataType.includes('file'))}
typeOptions={[ typeOptions={[
{ label: 'text', value: 'text' }, { label: 'text', value: 'text' },
{ label: 'file', value: 'file' } { label: 'file', value: 'file' }
@@ -201,10 +200,10 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
} }
{values?.body?.content_type === 'binary' && {values?.body?.content_type === 'binary' &&
<Form.Item name={['body', 'data']} noStyle> <Form.Item name={['body', 'data']} noStyle>
<VariableSelect <Editor
placeholder={t('common.pleaseSelect')} placeholder={t('common.pleaseSelect')}
options={options.filter(vo => vo.dataType.includes('file'))} options={options.filter(vo => vo.dataType.includes('file'))}
filterBooleanType={true} type="input"
size="small" size="small"
/> />
</Form.Item> </Form.Item>

View File

@@ -1,14 +1,14 @@
/* /*
* @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-02-11 12:07:06 * @Last Modified time: 2026-03-02 17:06:41
*/ */
import { type FC, useEffect, useState, useMemo } from "react"; import { type FC, useEffect, useState, useMemo } from "react";
import clsx from 'clsx' import clsx from 'clsx'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Graph, Node } from '@antv/x6'; import { Graph, Node } from '@antv/x6';
import { Form, Input, Select, InputNumber, Switch, Divider, Space } from 'antd' import { Form, Input, Select, InputNumber, Switch, Divider, Space, Button } from 'antd'
import { CaretDownOutlined, CaretRightOutlined } from '@ant-design/icons'; import { CaretDownOutlined, CaretRightOutlined } from '@ant-design/icons';
import type { NodeConfig, NodeProperties, ChatVariable } from '../../types' import type { NodeConfig, NodeProperties, ChatVariable } from '../../types'
@@ -36,6 +36,7 @@ import Editor, { type LexicalEditorProps } from "../Editor";
import RbSlider from './RbSlider' import RbSlider from './RbSlider'
import JinjaRender from './JinjaRender' import JinjaRender from './JinjaRender'
import CodeExecution from './CodeExecution' import CodeExecution from './CodeExecution'
import { nodeLibrary } from '../../constant';
/** /**
* Props for Properties component * Props for Properties component
@@ -69,7 +70,8 @@ interface PropertiesProps {
const Properties: FC<PropertiesProps> = ({ const Properties: FC<PropertiesProps> = ({
selectedNode, selectedNode,
graphRef, graphRef,
chatVariables chatVariables,
blankClick
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [form] = Form.useForm<NodeConfig>(); const [form] = Form.useForm<NodeConfig>();
@@ -80,9 +82,8 @@ const Properties: FC<PropertiesProps> = ({
useEffect(() => { useEffect(() => {
if (selectedNode?.getData()?.id) { if (selectedNode?.getData()?.id) {
setOutputCollapsed(true) setOutputCollapsed(true)
} else {
form.resetFields()
} }
form.resetFields()
}, [selectedNode?.getData()?.id]) }, [selectedNode?.getData()?.id])
useEffect(() => { useEffect(() => {
@@ -94,7 +95,7 @@ const Properties: FC<PropertiesProps> = ({
initialValue[key] = config[key].defaultValue initialValue[key] = config[key].defaultValue
} }
}) })
form.setFieldsValue({ form.setFieldsValue({
type, type,
id: selectedNode.id, id: selectedNode.id,
@@ -380,6 +381,41 @@ const Properties: FC<PropertiesProps> = ({
} }
} }
console.log('variableList', variableList, currentNodeVariables) console.log('variableList', variableList, currentNodeVariables)
const handleSureReplace = () => {
const { replaceNode } = values;
const nodeLibraryConfig = [...nodeLibrary]
.flatMap(category => category.nodes)
.find(n => n.type === replaceNode)
if (replaceNode && nodeLibraryConfig) {
// Preserve existing config values when switching node types
const currentData = selectedNode?.data || {};
const currentConfig = currentData.config || {};
const newConfig = nodeLibraryConfig.config || {};
// Merge configs: keep existing values for matching keys, add new keys from template
const mergedConfig: Record<string, any> = {};
Object.keys(newConfig).forEach(key => {
if (currentConfig[key] && currentConfig[key].defaultValue !== undefined) {
// Preserve existing value if it exists
mergedConfig[key] = {
...newConfig[key],
defaultValue: currentConfig[key].defaultValue
};
} else {
// Use new config template
mergedConfig[key] = { ...newConfig[key] };
}
});
selectedNode?.setData({
...currentData,
...nodeLibraryConfig,
config: mergedConfig
})
blankClick()
}
}
return ( return (
<div className={clsx("rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3 rb:pb-6", styles.properties)}> <div className={clsx("rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3 rb:pb-6", styles.properties)}>
@@ -399,8 +435,27 @@ const Properties: FC<PropertiesProps> = ({
<Form.Item name="id" label="ID"> <Form.Item name="id" label="ID">
<Input disabled /> <Input disabled />
</Form.Item> </Form.Item>
{selectedNode?.data?.type === 'unknown'
{selectedNode?.data?.type === 'http-request' ? <>
<Form.Item name="replaceNode" label={t('workflow.config.unknown.replaceNodeType')}>
<Select
options={nodeLibrary.map(category => ({
label: t(`workflow.${category.category}`),
options: category.nodes.filter(item => !['cycle-start', 'break'].includes(item.type)).map(node => ({
label: <div className="rb:flex rb:items-center rb:gap-2 rb:flex-1">
<img src={node.icon} className="rb:size-3.5" />
<div className="rb:wrap-break-word rb:line-clamp-1">{t(`workflow.${node.type}`)}</div>
</div>,
value: node.type
}))
}))}
placeholder={t('common.pleaseSelect')}
allowClear
/>
</Form.Item>
<Button type="primary" size="small" className="rb:text-[12px]!" onClick={handleSureReplace}>{t('workflow.sureReplace')}</Button>
</>
: selectedNode?.data?.type === 'http-request'
? <HttpRequest ? <HttpRequest
options={variableList} options={variableList}
selectedNode={selectedNode} selectedNode={selectedNode}

View File

@@ -47,6 +47,7 @@ import breakIcon from '@/assets/images/workflow/break.png'
import assignerIcon from '@/assets/images/workflow/assigner.png' import assignerIcon from '@/assets/images/workflow/assigner.png'
import memoryReadIcon from '@/assets/images/workflow/memory-read.png' import memoryReadIcon from '@/assets/images/workflow/memory-read.png'
import memoryWriteIcon from '@/assets/images/workflow/memory-write.png' import memoryWriteIcon from '@/assets/images/workflow/memory-write.png'
import unknownIcon from '@/assets/images/workflow/unknown.svg'
import { memoryConfigListUrl } from '@/api/memory' import { memoryConfigListUrl } from '@/api/memory'
@@ -524,6 +525,10 @@ export const nodeLibrary: NodeLibrary[] = [
// ] // ]
// }, // },
]; ];
export const unknownNode = {
type: 'unknown',
icon: unknownIcon
}
export const nodeWidth = 240; export const nodeWidth = 240;
/** /**

View File

@@ -12,7 +12,7 @@ import { Graph, Node, MiniMap, Snapline, Clipboard, Keyboard, type Edge } from '
import { register } from '@antv/x6-react-shape'; import { register } from '@antv/x6-react-shape';
import type { PortMetadata } from '@antv/x6/lib/model/port'; import type { PortMetadata } from '@antv/x6/lib/model/port';
import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary, portMarkup, portAttrs, edgeAttrs, edge_color, edge_selected_color, portTextAttrs, defaultAbsolutePortGroups, nodeWidth } from '../constant'; import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary, portMarkup, portAttrs, edgeAttrs, edge_color, edge_selected_color, portTextAttrs, defaultAbsolutePortGroups, nodeWidth, unknownNode } from '../constant';
import type { WorkflowConfig, NodeProperties, ChatVariable } from '../types'; import type { WorkflowConfig, NodeProperties, ChatVariable } from '../types';
import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application' import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application'
@@ -128,7 +128,7 @@ export const useWorkflowGraph = ({
if (nodes.length) { if (nodes.length) {
const nodeList = nodes.map(node => { const nodeList = nodes.map(node => {
const { id, type, name, position, config = {} } = node const { id, type, name, position, config = {} } = node
let nodeLibraryConfig = [...nodeLibrary] let nodeLibraryConfig = [...nodeLibrary, { nodes: [unknownNode] }]
.flatMap(category => category.nodes) .flatMap(category => category.nodes)
.find(n => n.type === type) .find(n => n.type === type)
nodeLibraryConfig = JSON.parse(JSON.stringify({ config: {}, ...nodeLibraryConfig })) as NodeProperties nodeLibraryConfig = JSON.parse(JSON.stringify({ config: {}, ...nodeLibraryConfig })) as NodeProperties