diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 87e8fc57..0dd9252e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,6 +23,8 @@ "@logicflow/core": "^2.0.9", "@logicflow/extension": "^2.0.13", "@reduxjs/toolkit": "^2.0.1", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "antd": "^5.22.2", "axios": "^1.6.2", "dagre": "^0.8.5", @@ -1149,6 +1151,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1174,6 +1192,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", @@ -2343,27 +2367,34 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.17.1", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peer": true, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dependencies": { + "ajv": "^8.0.0" + }, "peerDependencies": { - "ajv": "^6.9.1" + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, "node_modules/ansi-regex": { @@ -3197,6 +3228,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3222,6 +3269,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", @@ -3344,6 +3397,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.17.1.tgz", @@ -3956,9 +4014,9 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -5452,6 +5510,14 @@ "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz", @@ -5635,6 +5701,37 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peer": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "peer": true + }, "node_modules/screenfull": { "version": "5.2.0", "resolved": "https://registry.npmmirror.com/screenfull/-/screenfull-5.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0fe2cf1b..63dcaee2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,8 @@ "@logicflow/core": "^2.0.9", "@logicflow/extension": "^2.0.13", "@reduxjs/toolkit": "^2.0.1", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "antd": "^5.22.2", "axios": "^1.6.2", "dagre": "^0.8.5", diff --git a/frontend/src/pages/Workflow/Definition/Designer/components/NodeConfig/index.tsx b/frontend/src/pages/Workflow/Definition/Designer/components/NodeConfig/index.tsx index 56f68eec..cb6ebf4a 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/components/NodeConfig/index.tsx +++ b/frontend/src/pages/Workflow/Definition/Designer/components/NodeConfig/index.tsx @@ -1,7 +1,9 @@ -import React, { useMemo, useEffect } from 'react'; -import { Form, Input, Select, InputNumber, Switch, Divider } from 'antd'; +import React, { useMemo } from 'react'; +import { Form, Input, Select, InputNumber, Divider, Tooltip } from 'antd'; +import { InfoCircleOutlined } from '@ant-design/icons'; import type { Rule } from 'antd/es/form'; import { NodeType } from '../../../../types'; +import { validateNodeConfig } from './validate'; interface NodeConfigProps { nodeType: NodeType; @@ -9,42 +11,80 @@ interface NodeConfigProps { onValuesChange?: (changedValues: any, allValues: any) => void; } -interface FormFields { - [key: string]: any; +interface JsonSchema { + type: string; + title?: string; + description?: string; + properties?: Record; + required?: string[]; + minimum?: number; + maximum?: number; + minLength?: number; + maxLength?: number; + format?: string; + default?: any; + enum?: any[]; + enumNames?: string[]; + additionalProperties?: JsonSchema; } -// 处理JSON字符串中的转义字符 -const parseJsonSafely = (jsonString: string) => { +const parseJsonSafely = (jsonString: string): any => { + if (!jsonString) return {}; + try { - // 处理Windows风格的换行符 - const processed = jsonString.replace(/\r\n/g, '\n') - // 处理转义字符 - .replace(/\\/g, '\\\\') - // 处理引号 - .replace(/\\"/g, '\\"'); - return JSON.parse(processed); + // 如果已经是对象,直接返回 + if (typeof jsonString === 'object') { + return jsonString; + } + + // 打印原始字符串内容,帮助调试 + console.log('原始 JSON 字符串:', jsonString); + + // 尝试直接解析 + try { + return JSON.parse(jsonString); + } catch (e) { + console.log('直接解析失败,尝试预处理...'); + } + + // 预处理字符串 + let processedString = jsonString; + + // 1. 处理换行符 + processedString = processedString.replace(/[\r\n]+/g, ' '); + + // 2. 处理转义字符 + processedString = processedString.replace(/\\/g, '\\\\') + .replace(/\\\\"/g, '\\"') + .replace(/\\\\n/g, '\\n') + .replace(/\\\\r/g, '\\r') + .replace(/\\\\t/g, '\\t'); + + // 3. 确保属性名称正确引用 + processedString = processedString.replace(/(['"])?([a-zA-Z0-9_]+)(['"])?:/g, '"$2":'); + + console.log('处理后的 JSON 字符串:', processedString); + + return JSON.parse(processedString); } catch (error) { console.error('JSON解析错误:', error); - console.debug('原始JSON字符串:', jsonString); - return null; + console.error('解析失败的字符串:', jsonString); + // 返回空对象而不是 null,避免后续操作出错 + return {}; } }; const NodeConfig: React.FC = ({ nodeType, form, onValuesChange }) => { // 解析节点配置模式 const nodeSchema = useMemo(() => { - if (!nodeType.configSchema) { - console.warn('节点配置模式为空'); - return null; - } + if (!nodeType.configSchema) return null; return parseJsonSafely(nodeType.configSchema); }, [nodeType.configSchema]); // 解析节点默认配置 const nodeDefaultConfig = useMemo(() => { if (!nodeType.defaultConfig) return {}; - const config = parseJsonSafely(nodeType.defaultConfig); - return config || {}; + return parseJsonSafely(nodeType.defaultConfig) || {}; }, [nodeType.defaultConfig]); // 当前选中的执行器 @@ -54,160 +94,145 @@ const NodeConfig: React.FC = ({ nodeType, form, onValuesChange const executorSchema = useMemo(() => { if (!selectedExecutor || !nodeType.executors) return null; const executor = nodeType.executors.find(e => e.code === selectedExecutor); - if (!executor || !executor.configSchema) return null; + if (!executor?.configSchema) return null; return parseJsonSafely(executor.configSchema); }, [selectedExecutor, nodeType.executors]); - // 获取当前执行器的默认配置 - const executorDefaultConfig = useMemo(() => { - if (!selectedExecutor || !nodeType.executors) return {}; - const executor = nodeType.executors.find(e => e.code === selectedExecutor); - if (!executor || !executor.defaultConfig) return {}; - const config = parseJsonSafely(executor.defaultConfig); - return config || {}; - }, [selectedExecutor, nodeType.executors]); - - // 渲染基本配置表单项 - const renderBasicFormItems = () => ( - <> - - - - - - - - ); - - // 渲染网关节点配置 - const renderGatewayConfig = () => { - if (!nodeType.code?.includes('GATEWAY')) return null; - - return ( - - - - ); - }; - - // 渲染Shell执行器特定配置 - const renderShellExecutorConfig = () => { - if (!nodeType.code?.includes('SHELL')) return null; + // 验证配置 + const validateField = async (_: any, value: any) => { + const allValues = form.getFieldsValue(); + const validationResult = validateNodeConfig(nodeType, allValues); - return ( - <> - - ({ + label: fieldSchema.enumNames?.[index] || value, + value + }))} + /> - - + ); + } + if (fieldSchema.format === 'shell') { + return ( + + - - + ); + } + return ( + + + + ); + + case 'number': + return ( + + + + ); + + case 'object': + if (fieldSchema.additionalProperties) { + return ( + + - - - - - - - - - - - - - - )} - + ); + } + return null; + + default: + return null; + } + }; + + // 渲染节点基本配置 + const renderNodeConfig = () => { + if (!nodeSchema?.properties) return null; + return Object.entries(nodeSchema.properties).map(([fieldName, fieldSchema]) => + renderFormItem(fieldName, fieldSchema) + ); + }; + + // 渲染执行器配置 + const renderExecutorConfig = () => { + if (!executorSchema?.properties) return null; + return Object.entries(executorSchema.properties).map(([fieldName, fieldSchema]) => + renderFormItem(fieldName, fieldSchema) ); }; @@ -216,13 +241,15 @@ const NodeConfig: React.FC = ({ nodeType, form, onValuesChange form={form} layout="vertical" onValuesChange={onValuesChange} - initialValues={{ ...nodeDefaultConfig, ...executorDefaultConfig }} + initialValues={{ ...nodeDefaultConfig }} > - {renderBasicFormItems()} - - {renderGatewayConfig()} - {renderTimerConfig()} - {renderShellExecutorConfig()} + {renderNodeConfig()} + {selectedExecutor && ( + <> + + {renderExecutorConfig()} + + )} ); }; diff --git a/frontend/src/pages/Workflow/Definition/Designer/components/NodeConfig/validate.ts b/frontend/src/pages/Workflow/Definition/Designer/components/NodeConfig/validate.ts new file mode 100644 index 00000000..9ed7d298 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/components/NodeConfig/validate.ts @@ -0,0 +1,78 @@ +import Ajv from 'ajv'; +import addFormats from 'ajv-formats'; +import { NodeType } from '../../../../types'; + +const ajv = new Ajv({ + allErrors: true, + verbose: true, + $data: true, + strict: false +}); + +addFormats(ajv); + +export interface ValidationResult { + valid: boolean; + errors: string[]; +} + +export const validateNodeConfig = ( + nodeType: NodeType, + formData: any +): ValidationResult => { + console.log('Validating node config:', { nodeType, formData }); + + try { + // 1. 解析节点的 schema + const nodeSchema = JSON.parse(nodeType.configSchema); + console.log('Node schema:', nodeSchema); + + let errors: string[] = []; + + // 2. 验证基本配置 + const validateNode = ajv.compile(nodeSchema); + const nodeValid = validateNode(formData); + console.log('Node validation result:', { valid: nodeValid, errors: validateNode.errors }); + + if (!nodeValid && validateNode.errors) { + errors = validateNode.errors.map(err => { + const field = err.instancePath.replace('/', '') || '配置'; + return `${field}: ${err.message}`; + }); + } + + // 3. 如果有执行器配置,验证执行器配置 + if (formData.executor && nodeType.executors?.length > 0) { + const executor = nodeType.executors.find(e => e.code === formData.executor); + if (executor?.configSchema) { + console.log('Validating executor config:', executor.configSchema); + const executorSchema = JSON.parse(executor.configSchema); + const validateExecutor = ajv.compile(executorSchema); + const executorValid = validateExecutor(formData); + console.log('Executor validation result:', { valid: executorValid, errors: validateExecutor.errors }); + + if (!executorValid && validateExecutor.errors) { + errors = errors.concat( + validateExecutor.errors.map(err => { + const field = err.instancePath.replace('/', '') || '执行器配置'; + return `${field}: ${err.message}`; + }) + ); + } + } + } + + const result = { + valid: errors.length === 0, + errors + }; + console.log('Final validation result:', result); + return result; + } catch (error) { + console.error('Schema validation error:', error); + return { + valid: false, + errors: ['配置验证失败:schema 解析错误'] + }; + } +};