/* * @Author: ZhaoYing * @Date: 2026-02-03 15:06:18 * @Last Modified by: ZhaoYing * @Last Modified time: 2026-04-03 20:28:08 */ import LoopNode from './components/Nodes/LoopNode'; import NormalNode from './components/Nodes/NormalNode'; import ConditionNode from './components/Nodes/ConditionNode'; import GroupStartNode from './components/Nodes/GroupStartNode'; import AddNode from './components/Nodes/AddNode' import NoteNode from './components/Nodes/NoteNode'; import type { PortMetadata, GroupMetadata } from '@antv/x6/lib/model/port'; import type { ReactShapeConfig } from '@antv/x6-react-shape'; import { memoryConfigListUrl } from '@/api/memory' import type { NodeLibrary } from './types' /** * Workflow node library configuration * Defines all available node types, their icons, and configuration schemas */ export const nodeLibrary: NodeLibrary[] = [ { category: "coreNode", nodes: [ { type: "start", icon: 'rb:bg-[url("@/assets/images/workflow/start.svg")]', config: { variables: { type: 'define', sys: [ { name: "message", type: "string", readonly: true }, { name: "conversation_id", type: "string", readonly: true }, { name: "execution_id", type: "string", readonly: true }, { name: "workspace_id", type: "string", readonly: true }, { name: "user_id", type: "string", readonly: true }, { name: "files", type: "array[file]", readonly: true }, ], defaultValue: [] } } }, { type: "end", icon: 'rb:bg-[url("@/assets/images/workflow/end.svg")]', config: { output: { type: 'editor' } } }, // { type: "answer", icon: answerIcon }, ] }, { category: "aiAndCognitiveProcessing", nodes: [ { type: "llm", icon: 'rb:bg-[url("@/assets/images/workflow/llm.svg")]', config: { model_id: { type: 'define', params: { type: 'llm,chat' }, // llm/chat valueKey: 'id', labelKey: 'name', }, temperature: { type: 'define', max: 2, min: 0, step: 0.1, defaultValue: 0.7 }, max_tokens: { type: 'define', max: 32000, min: 256, step: 1, defaultValue: 2000 }, context: { type: 'variableList', placeholder: 'workflow.config.llm.contextPlaceholder' }, messages: { type: 'define', defaultValue: [ { role: 'SYSTEM', content: undefined, readonly: true }, ], placeholder: 'workflow.config.llm.messagesPlaceholder' }, memory: { type: 'memoryConfig', defaultValue: { enable: false, enable_window: false, window_size: 20 } }, vision: { type: 'switch' }, vision_input: { type: 'variableList', onFilterVariableNames: ['sys.files'] } } }, { type: "knowledge-retrieval", icon: 'rb:bg-[url("@/assets/images/workflow/rag.svg")]', config: { query: { type: 'variableList', }, knowledge_retrieval: { type: 'knowledge' } } }, { type: "parameter-extractor", icon: 'rb:bg-[url("@/assets/images/workflow/parameter_extraction.svg")]', config: { model_id: { type: 'modelSelect', params: { type: 'llm,chat' }, // llm/chat }, text: { type: 'variableList', filterLoopIterationVars: true, placeholder: 'workflow.config.parameter-extractor.textPlaceholder' }, params: { type: 'paramList', }, prompt: { type: 'messageEditor', isArray: false, titleVariant: 'borderless', placeholder: 'workflow.config.parameter-extractor.promptPlaceholder' }, } } ] }, { category: "cognitiveUpgrading", nodes: [ { type: "memory-read", icon: 'rb:bg-[url("@/assets/images/workflow/memory-read.svg")]', config: { message: { type: 'editor', isArray: false }, config_id: { type: 'customSelect', url: memoryConfigListUrl, valueKey: 'config_id', labelKey: 'config_name' }, search_switch: { type: 'select', options: [ { value: '0', label: 'memoryConversation.deepThinking' }, { value: '1', label: 'memoryConversation.normalReply' }, { value: '2', label: 'memoryConversation.quickReply' }, ], needTranslation: true } } }, { type: "memory-write", icon: 'rb:bg-[url("@/assets/images/workflow/memory-write.svg")]', config: { message: { type: 'editor', isArray: false, hidden: true, }, messages: { type: 'messageEditor', defaultValue: [], placeholder: 'workflow.config.llm.messagesPlaceholder', isArray: true }, config_id: { type: 'customSelect', url: memoryConfigListUrl, valueKey: 'config_id', labelKey: 'config_name' } } }, ] }, { category: "flowControl", nodes: [ { type: "if-else", icon: 'rb:bg-[url("@/assets/images/workflow/condition.svg")]', config: { cases: { type: 'caseList', defaultValue: [ { logical_operator: 'and', expressions: [] } ] } } }, { type: "question-classifier", icon: 'rb:bg-[url("@/assets/images/workflow/question-classifier.svg")]', config: { model_id: { type: 'modelSelect', params: { type: 'llm,chat' }, // llm/chat }, input_variable: { type: 'variableList', }, categories: { type: 'categoryList', defaultValue: [ {}, {} ] }, user_supplement_prompt: { type: 'messageEditor', isArray: false, titleVariant: 'borderless', placeholder: 'common.pleaseEnter' } } }, { type: "iteration", icon: 'rb:bg-[url("@/assets/images/workflow/iteration.svg")]', config: { input: { type: 'variableList', filterNodeTypes: ['knowledge-retrieval', 'iteration', 'loop', 'parameter-extractor', 'code', 'CONVERSATION'], filterVariableNames: ['message'] }, parallel: { type: 'switch', defaultValue: false }, parallel_count: { type: 'slider', min: 1, max: 10, step: 1, defaultValue: 10, dependsOn: 'parallel', dependsOnValue: true }, flatten: { // Flatten output type: 'switch', defaultValue: false }, output: { type: 'variableList', filterChildNodes: true }, output_type: { type: 'define', } }, }, { type: "loop", icon: 'rb:bg-[url("@/assets/images/workflow/loop.svg")]', config: { cycle_vars: { type: 'cycleVarsList', defaultValue: [] }, condition: { type: 'conditionList', showLabel: true, defaultValue: { logical_operator: 'and', expressions: [] } }, max_loop: { type: 'slider', min: 1, max: 100, step: 1, defaultValue: 10 }, } }, { type: "cycle-start", icon: 'rb:bg-[url("@/assets/images/workflow/start.svg")]'}, { type: "break", icon: 'rb:bg-[url("@/assets/images/workflow/break.svg")]'}, { type: "var-aggregator", icon: 'rb:bg-[url("@/assets/images/workflow/aggregator.svg")]', config: { group: { type: 'switch', defaultValue: false }, group_variables: { type: 'groupVariableList', defaultValue: [], }, group_type: { type: 'define', } } }, { type: "assigner", icon: 'rb:bg-[url("@/assets/images/workflow/assigner.svg")]', config: { assignments: { type: 'assignmentList', filterLoopIterationVars: true } } }, ] }, { category: "externalInteraction", nodes: [ { type: "http-request", icon: 'rb:bg-[url("@/assets/images/workflow/http_request.svg")]', config: { method: { type: 'select', options: [ { label: 'GET', value: 'GET' }, { label: 'POST', value: 'POST' }, { label: 'HEAD', value: 'HEAD' }, { label: 'PATCH', value: 'PATCH' }, { label: 'PUT', value: 'PUT' }, { label: 'DELETE', value: 'DELETE' }, ], defaultValue: 'GET' }, url: { type: 'messageEditor', isArray: false, }, auth: { type: 'define', defaultValue: { auth_type: 'none' } }, headers: { type: 'define', defaultValue: [] }, params: { type: 'define', defaultValue: [] }, body: { type: 'define', defaultValue: { 'content_type': 'none' } }, verify_ssl: { type: 'switch', defaultValue: false }, timeouts: { type: 'define', defaultValue: {} }, retry: { type: 'switch', defaultValue: { enable: false } }, error_handle: { type: 'define', defaultValue: { method: 'none' } } } }, { type: "tool", icon: 'rb:bg-[url("@/assets/images/workflow/tools.svg")]', config: { tool_id: { type: 'cascader' }, tool_parameters: { type: 'define' } } }, { type: "code", icon: 'rb:bg-[url("@/assets/images/workflow/code_execution.svg")]', config: { input_variables: { type: 'inputList', defaultValue: [{ name: 'arg1' }, { name: 'arg2' }] }, language: { type: 'select', defaultValue: 'python3' }, code: { type: 'messageEditor', isArray: false, language: ['python3', 'javascript'], titleVariant: 'borderless', defaultValue: `def main(arg1: str, arg2: str): return { "result": arg1 + arg2, }` }, output_variables: { type: 'outputList', defaultValue: [{name: 'result', type: 'string'}] }, } }, { type: "jinja-render", icon: 'rb:bg-[url("@/assets/images/workflow/template_rendering.svg")]', config: { mapping: { type: 'mappingList', defaultValue: [{name: 'arg1'}] }, template: { type: 'messageEditor', isArray: false, language: 'jinja2', titleVariant: 'borderless', defaultValue: "{{arg1}}" }, } }, { type: "document-extractor", icon: 'rb:bg-[url("@/assets/images/workflow/document-extractor.svg")]', config: { file_selector: { type: 'variableList', placeholder: 'common.pleaseSelect', onFilterVariableNames: ['sys.files'] } } }, { type: "list-operator", icon: 'rb:bg-[url("@/assets/images/workflow/list-operator.svg")]', config: { input_list: { type: 'variableList', }, filter_by: { type: 'define', defaultValue: { enabled: false, conditions: [{}] } }, order_by: { type: 'define', defaultValue: { "enabled": false, "key": "", "value": "asc" } }, limit: { type: 'define', defaultValue: { "enabled": false, "size": 1 } }, extract_by: { type: 'define', defaultValue: { "enabled": false, "serial": "" } }, } }, ] }, ]; export const THEME_MAP: Record = { blue: { outer: '#2E90FA', title: '#D1E9FF', bg: '#EFF8FF', border: '#84CAFF', }, cyan: { outer: '#06AED4', title: '#CFF9FE', bg: '#ECFDFF', border: '#67E3F9', }, green: { outer: '#16B364', title: '#D3F8DF', bg: '#EDFCF2', border: '#73E2A3', }, yellow: { outer: '#EAAA08', title: '#FEF7C3', bg: '#FEFBE8', border: '#FDE272', }, pink: { outer: '#EE46BC', title: '#FCE7F6', bg: '#FDF2FA', border: '#FAA7E0', }, violet: { outer: '#875BF7', title: '#ECE9FE', bg: '#F5F3FF', border: '#C3B5FD', }, } export const notesConfig = { type: "notes", icon: 'rb:bg-[url("@/assets/images/workflow/unknown.svg")]', config: { text: { type: 'define', }, theme: { type: 'define', defaultValue: 'blue', }, width: { type: 'define', width: 240, }, height: { type: 'define', height: 120, }, author: { type: 'define', }, show_author: { type: 'define', defaultValue: true } } } export const unknownNode = { type: 'unknown', icon: 'rb:bg-[url("@/assets/images/workflow/unknown.svg")]' } export const noteNode = { type: 'notes', icon: 'rb:bg-[url("@/assets/images/workflow/unknown.svg")]' } export const nodeWidth = 240; export const conditionNodePortItemArgsY = 60; export const conditionNodeItemHeight = 26; export const conditionNodeHeight = 110; /** * Node registration library for X6 graph * Maps node shapes to their React components */ export const nodeRegisterLibrary: ReactShapeConfig[] = [ { shape: 'loop-node', width: nodeWidth, height: 120, component: LoopNode, }, { shape: 'iteration-node', width: nodeWidth, height: 120, component: LoopNode, }, { shape: 'normal-node', width: 120, height: 40, component: NormalNode, }, { shape: 'condition-node', width: nodeWidth, height: conditionNodeHeight, component: ConditionNode, }, { shape: 'cycle-start', width: 36, height: 36, component: GroupStartNode, }, { shape: 'add-node', width: 100, height: 28, component: AddNode, }, { shape: 'notes-node', width: nodeWidth, height: 120, component: NoteNode, }, ]; /** * Port configuration interface */ interface PortsConfig { /** Port group metadata */ groups?: GroupMetadata; /** Port item metadata array */ items?: PortMetadata[]; } /** * Node configuration interface */ interface NodeConfig { /** Node width in pixels */ width: number; /** Node height in pixels */ height: number; /** Node shape type */ shape: string; /** Port configuration */ ports?: PortsConfig; } /** Edge color for normal state */ export const edge_color = '#D4D5D9'; /** Edge color for selected state */ export const edge_selected_color = '#171719' export const edge_width = 2; /** Port color */ export const port_color = '#171719' /** * Unified port markup configuration * Defines SVG elements for port rendering */ export const portMarkup = [ { tagName: 'circle', selector: 'body', }, { tagName: 'text', selector: 'label', }, ]; /** * Unified port attributes configuration * Defines visual styling for ports */ export const portAttrs = { body: { r: 6, magnet: true, stroke: port_color, strokeWidth: edge_width, fill: port_color, }, label: { text: '+', fontSize: 12, fontWeight: 'bold', fill: '#FFFFFF', textAnchor: 'middle', textVerticalAnchor: 'middle', pointerEvents: 'none', }, } export const portTextAttrs = { fontSize: 12, fill: '#5B6167' } /** * Port position arguments */ export const portItemArgsY = 26; export const portArgs = { x: nodeWidth, y: portItemArgsY } const defaultPortGroup = { position: { name: 'absolute' }, markup: [ { tagName: 'rect', selector: 'body' }, { tagName: 'circle', selector: 'hoverBody' }, { tagName: 'text', selector: 'label' }, ], attrs: { body: { width: 1, height: 8, x: 0.75, magnet: true, stroke: port_color, strokeWidth: edge_width, fill: port_color, }, hoverBody: { r: 6, cy: 2, magnet: true, stroke: port_color, strokeWidth: edge_width, fill: port_color, opacity: 0, }, label: { text: '+', fontSize: 12, fontWeight: 'bold', fill: '#FFFFFF', textAnchor: 'middle', textVerticalAnchor: 'middle', pointerEvents: 'none', y: '0.15em', opacity: 0, }, }, } const leftPortGroup = { position: { name: 'absolute' }, markup: [{ tagName: 'rect', selector: 'body' }], attrs: { body: { width: 1, height: 8, x: -1.75, y: -4, magnet: true, stroke: port_color, strokeWidth: edge_width, fill: port_color, }, }, } /** * Unified port group configuration * Defines port positions and attributes for different sides */ export const defaultAbsolutePortGroups = { right: defaultPortGroup, left: leftPortGroup, } /** * Default port items for standard nodes */ export const defaultPortItems = [ { group: 'left', args: { x: 0, y: portItemArgsY }, }, { group: 'right', args: { x: nodeWidth, y: portItemArgsY }, }, ]; /** * Graph node library configuration * Maps node types to their visual and structural properties */ export const graphNodeLibrary: Record = { iteration: { width: nodeWidth, height: 140, shape: 'iteration-node', ports: { groups: defaultAbsolutePortGroups, items: defaultPortItems, }, }, loop: { width: nodeWidth, height: 140, shape: 'loop-node', ports: { groups: defaultAbsolutePortGroups, items: defaultPortItems, }, }, 'if-else': { width: nodeWidth, height: conditionNodeHeight, shape: 'condition-node', ports: { groups: defaultAbsolutePortGroups, items: [ defaultPortItems[0], ...(['IF', 'ELSE'].map((_, index) => ({ group: 'right', id: `CASE${index + 1}`, args: { ...portArgs, y: portItemArgsY * index + conditionNodePortItemArgsY, }, }))), ], }, }, 'question-classifier': { width: nodeWidth, height: conditionNodeHeight, shape: 'condition-node', ports: { groups: defaultAbsolutePortGroups, items: [ defaultPortItems[0], ...(['分类1', '分类2'].map((_text, index) => ({ group: 'right', id: `CASE${index + 1}`, args: { ...portArgs, y: portItemArgsY * index + conditionNodePortItemArgsY, }, }))), ], }, }, start: { width: nodeWidth, height: 76, shape: 'normal-node', ports: { groups: { right: defaultPortGroup}, items: [defaultPortItems[1]], }, }, 'cycle-start': { width: 36, height: 36, shape: 'cycle-start', ports: { groups: { right: defaultPortGroup }, items: [{ group: 'right', args: { x: 36, y: 18 } }], }, }, 'add-node': { width: 100, height: 28, shape: 'add-node', ports: { groups: { left: leftPortGroup }, items: [{ group: 'left', args: { x: 0, y: 18 }}], }, }, default: { width: nodeWidth, height: 76, shape: 'normal-node', ports: { groups: defaultAbsolutePortGroups, items: defaultPortItems, }, }, cycleStart: { width: 36, height: 36, shape: 'cycle-start', ports: { groups: { right: defaultPortGroup }, items: [{ group: 'right', args: { x: 36, y: 18 }}], }, }, addStart: { width: 100, height: 28, shape: 'add-node', ports: { groups: { left: leftPortGroup }, items: [{ group: 'left', args: { x: 0, y: 14 } }], }, }, break: { width: nodeWidth, height: 76, shape: 'normal-node', ports: { groups: { left: leftPortGroup }, items: [defaultPortItems[0]], }, }, notes: { width: nodeWidth, height: 120, shape: 'notes-node', } } /** * Output variable configuration interface */ export interface OutputVariable { /** Default output variables */ default?: Array<{ name: string; type: string; }>; /** Dynamically defined variable keys */ define?: string[]; /** System-level output variables */ sys?: Array<{ name: string; type: string; }>; /** Error-related output variables */ error?: Array<{ name: string; type: string; }>; } /** * Default edge attributes configuration * Defines visual styling for edges/connections */ export const edgeAttrs = { attrs: { line: { stroke: edge_color, strokeWidth: edge_width, targetMarker: null, sourceMarker: null, }, }, } /** * Edge hover tool: circular "+" button shown at midpoint on hover */ export const edgeHoverTool = { name: 'button', args: { markup: [ { tagName: 'circle', selector: 'button', attrs: { r: 6, stroke: port_color, strokeWidth: edge_width, fill: port_color, cursor: 'pointer', }, }, { tagName: 'text', textContent: '+', selector: 'icon', attrs: { fontSize: 12, fontWeight: 'bold', fill: '#FFFFFF', textAnchor: 'middle', textVerticalAnchor: 'middle', pointerEvents: 'none', y: '0.3em', }, }, ], distance: 0.5, offset: { x: 0, y: 0 }, onClick({ e, cell: edge }: any) { e.stopPropagation(); const graph = edge.model?.graph; if (!graph) return; const sourceCell = graph.getCellById(edge.getSourceCellId()); const targetCell = graph.getCellById(edge.getTargetCellId()); const sourcePort = edge.getSourcePortId(); const targetPort = edge.getTargetPortId(); if (!sourceCell || !targetCell) return; const rect = (e.target as HTMLElement).getBoundingClientRect(); const tempDiv = document.createElement('div'); tempDiv.style.position = 'fixed'; tempDiv.style.left = rect.left + 'px'; tempDiv.style.top = rect.top + 'px'; tempDiv.style.width = '1px'; tempDiv.style.height = '1px'; tempDiv.style.zIndex = '9999'; document.body.appendChild(tempDiv); window.dispatchEvent(new CustomEvent('port:click', { detail: { node: sourceCell, port: sourcePort, element: tempDiv, rect, edgeInsertion: { edge, sourceCell, targetCell, sourcePort, targetPort } } })); }, }, }