diff --git a/service-web/client/package.json b/service-web/client/package.json index eaa7978..e006cf2 100644 --- a/service-web/client/package.json +++ b/service-web/client/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run" }, "dependencies": { "@ant-design/icons": "^6.0.0", @@ -41,6 +42,7 @@ "sass": "^1.89.2", "typescript": "~5.8.3", "vite": "^7.0.0", - "vite-plugin-javascript-obfuscator": "^3.1.0" + "vite-plugin-javascript-obfuscator": "^3.1.0", + "vitest": "^3.2.4" } } diff --git a/service-web/client/pnpm-lock.yaml b/service-web/client/pnpm-lock.yaml index d5a9534..82ba1e2 100644 --- a/service-web/client/pnpm-lock.yaml +++ b/service-web/client/pnpm-lock.yaml @@ -102,6 +102,9 @@ importers: vite-plugin-javascript-obfuscator: specifier: ^3.1.0 version: 3.1.0 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/debug@4.1.12)(sass@1.89.2) packages: @@ -499,6 +502,9 @@ packages: resolution: {integrity: sha512-CZFX7UZVN9VopGbjTx4UXaXsi9ewoM1buL0kY7j1ftYdSs7p2spv9opxFjHlQ/QGTgh4UqufYqJJ0WKLml7b6w==} engines: {node: '>=4.0'} + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} @@ -865,6 +871,9 @@ packages: '@swc/types@0.1.23': resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==} + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + '@types/d3-array@3.2.1': resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} @@ -961,6 +970,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -1033,6 +1045,35 @@ packages: peerDependencies: vite: ^4 || ^5 || ^6 || ^7.0.0-beta.0 + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@xyflow/react@12.7.1': resolution: {integrity: sha512-uvIPQIZdf8tt0mDWvhkEpg/7t5E/e/KE4RWjNczAEhEYA+uvLc+4A5kIPJqCjJJbVHfMiAojT5JOB5mB7/EgFw==} peerDependencies: @@ -1162,6 +1203,10 @@ packages: assert@2.0.0: resolution: {integrity: sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -1234,6 +1279,10 @@ packages: resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} engines: {node: '>=0.2.0'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1260,6 +1309,10 @@ packages: resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} engines: {node: '>=0.8'} + chai@5.2.0: + resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} + engines: {node: '>=12'} + chainsaw@0.1.0: resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} @@ -1293,6 +1346,10 @@ packages: resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==} engines: {pnpm: '>=8'} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + chevrotain-allstar@0.3.1: resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} peerDependencies: @@ -1616,6 +1673,10 @@ packages: resolution: {integrity: sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==} engines: {node: '>=8'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1705,6 +1766,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -1748,6 +1812,9 @@ packages: estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -1756,6 +1823,10 @@ packages: resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==} engines: {node: '>=8.3.0'} + expect-type@1.2.1: + resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} + engines: {node: '>=12.0.0'} + exsolve@1.0.5: resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==} @@ -2058,6 +2129,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + jsbarcode@3.12.1: resolution: {integrity: sha512-QZQSqIknC2Rr/YOUyOkCBqsoiBAOTYK+7yNN3JsqfoUtJtkazxNw1dmPpxuv7VVvqW13kA3/mKiLq+s/e3o9hQ==} @@ -2179,6 +2253,12 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.1.4: + resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + make-cancellable-promise@1.3.2: resolution: {integrity: sha512-GCXh3bq/WuMbS+Ky4JBPW1hYTOU+znU+Q5m9Pu+pI8EoUqIHk9+tviOKC6/qhHh8C4/As3tzJ69IF32kdz85ww==} @@ -2518,6 +2598,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + pdfjs-dist@4.3.136: resolution: {integrity: sha512-gzfnt1qc4yA+U46golPGYtU4WM2ssqP2MvFjKga8GEKOrEnzRPrA/9jogLLPYHiA3sGBPJ+p7BdAq+ytmw3jEg==} engines: {node: '>=18'} @@ -3105,6 +3189,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3141,6 +3228,12 @@ packages: resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} engines: {node: '>=0.8'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + string-convert@0.2.1: resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} @@ -3167,6 +3260,9 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + style-to-js@1.1.17: resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} @@ -3210,9 +3306,15 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.1: resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} @@ -3223,6 +3325,18 @@ packages: tinymce@6.8.6: resolution: {integrity: sha512-++XYEs8lKWvZxDCjrr8Baiw7KiikraZ5JkLMg6EdnUVNKJui0IsrAADj5MsyUeFkcEryfn2jd3p09H7REvewyg==} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.3: + resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} + engines: {node: '>=14.0.0'} + tmp@0.2.3: resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} engines: {node: '>=14.14'} @@ -3369,6 +3483,11 @@ packages: react: ^15.0.0 || ^16.0.0 || ^17.0.0 react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite-plugin-javascript-obfuscator@3.1.0: resolution: {integrity: sha512-sf4JFlG1iUPl7bLXHGOy+bKWOQUFyXzJFWa+n2S2xMMvyfM+V9R40HhpZoIF1eAjifArM1SF7fbSFIaTuUIbPA==} @@ -3412,6 +3531,34 @@ packages: yaml: optional: true + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-jsonrpc@8.2.0: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} @@ -3454,6 +3601,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} @@ -4007,6 +4159,8 @@ snapshots: '@javascript-obfuscator/estraverse@5.4.0': {} + '@jridgewell/sourcemap-codec@1.5.0': {} + '@kurkle/color@0.3.4': {} '@lightenna/react-mermaid-diagram@1.0.20(mermaid@11.7.0)(react@18.3.1)': @@ -4291,6 +4445,10 @@ snapshots: dependencies: '@swc/counter': 0.1.3 + '@types/chai@5.2.2': + dependencies: + '@types/deep-eql': 4.0.2 + '@types/d3-array@3.2.1': {} '@types/d3-axis@3.0.6': @@ -4412,6 +4570,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -4476,6 +4636,48 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.2 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.0 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.0.0(sass@1.89.2))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 7.0.0(sass@1.89.2) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.0.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.17 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.3 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.1.4 + tinyrainbow: 2.0.0 + '@xyflow/react@12.7.1(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@xyflow/system': 0.0.63 @@ -4800,6 +5002,8 @@ snapshots: object-is: 1.1.6 util: 0.12.5 + assertion-error@2.0.1: {} + async@3.2.6: {} asynckit@0.4.0: {} @@ -4870,6 +5074,8 @@ snapshots: buffers@0.1.1: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -4906,6 +5112,14 @@ snapshots: adler-32: 1.3.1 crc-32: 1.2.2 + chai@5.2.0: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.4 + pathval: 2.0.1 + chainsaw@0.1.0: dependencies: traverse: 0.3.9 @@ -4933,6 +5147,8 @@ snapshots: dependencies: '@kurkle/color': 0.3.4 + check-error@2.1.1: {} + chevrotain-allstar@0.3.1(chevrotain@11.0.3): dependencies: chevrotain: 11.0.3 @@ -5267,6 +5483,8 @@ snapshots: mimic-response: 2.1.0 optional: true + deep-eql@5.0.2: {} + deep-is@0.1.4: {} define-data-property@1.1.4: @@ -5361,6 +5579,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -5421,6 +5641,10 @@ snapshots: estree-util-is-identifier-name@3.0.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} exceljs@4.4.0: @@ -5435,6 +5659,8 @@ snapshots: unzipper: 0.10.14 uuid: 8.3.2 + expect-type@1.2.1: {} + exsolve@1.0.5: {} extend@3.0.2: {} @@ -5771,6 +5997,8 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + jsbarcode@3.12.1: {} json2mq@0.2.0: @@ -5877,6 +6105,12 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.1.4: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + make-cancellable-promise@1.3.2: {} make-dir@3.1.0: @@ -6359,6 +6593,8 @@ snapshots: pathe@2.0.3: {} + pathval@2.0.1: {} + pdfjs-dist@4.3.136: optionalDependencies: canvas: 2.11.2 @@ -7130,6 +7366,8 @@ snapshots: shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@3.0.7: optional: true @@ -7164,6 +7402,10 @@ snapshots: dependencies: frac: 1.1.2 + stackback@0.0.2: {} + + std-env@3.9.0: {} + string-convert@0.2.1: {} string-template@1.0.0: {} @@ -7197,6 +7439,10 @@ snapshots: ansi-regex: 5.0.1 optional: true + strip-literal@3.0.0: + dependencies: + js-tokens: 9.0.1 + style-to-js@1.1.17: dependencies: style-to-object: 1.0.9 @@ -7255,8 +7501,12 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + tinycolor2@1.6.0: {} + tinyexec@0.3.2: {} + tinyexec@1.0.1: {} tinyglobby@0.2.14: @@ -7266,6 +7516,12 @@ snapshots: tinymce@6.8.6: {} + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.3: {} + tmp@0.2.3: {} to-regex-range@5.0.1: @@ -7418,6 +7674,27 @@ snapshots: react-dom: 18.3.1(react@18.3.1) redux: 4.2.1 + vite-node@3.2.4(sass@1.89.2): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.0.0(sass@1.89.2) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-plugin-javascript-obfuscator@3.1.0: dependencies: anymatch: 3.1.3 @@ -7435,6 +7712,47 @@ snapshots: fsevents: 2.3.3 sass: 1.89.2 + vitest@3.2.4(@types/debug@4.1.12)(sass@1.89.2): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.0.0(sass@1.89.2)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.0 + debug: 4.4.1 + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.0.0(sass@1.89.2) + vite-node: 3.2.4(sass@1.89.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vscode-jsonrpc@8.2.0: {} vscode-languageserver-protocol@3.17.5: @@ -7479,6 +7797,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wide-align@1.1.5: dependencies: string-width: 4.2.3 diff --git a/service-web/client/src/pages/ai/flow/FlowChecker.test.tsx b/service-web/client/src/pages/ai/flow/FlowChecker.test.tsx new file mode 100644 index 0000000..e0542f1 --- /dev/null +++ b/service-web/client/src/pages/ai/flow/FlowChecker.test.tsx @@ -0,0 +1,113 @@ +import {expect, test} from 'vitest' +import {type Connection, type Node} from '@xyflow/react' +import { + atLeastOneEndNodeError, + atLeastOneStartNodeError, + checkAddConnection, + checkAddNode, + checkSave, + hasCycleError, + hasRedundantEdgeError, + multiEndNodeError, + multiStartNodeError, + nodeToSelfError, + sourceNodeNotFoundError, + startNodeToEndNodeError, + targetNodeNotFoundError +} from './FlowChecker.tsx' +import {uuid} from 'licia' + +const createNode = (id: string, type: string): Node => { + return { + data: {}, + position: { + x: 0, + y: 0 + }, + id, + type, + } +} + +const createStartNode = (id: string): Node => createNode(id, 'start-node') +const createEndNode = (id: string): Node => createNode(id, 'end-node') + +const createConnection = function (source: string, target: string, sourceHandle: string | null = null, targetHandle: string | null = null): Connection { + return { + source, + target, + sourceHandle, + targetHandle, + } +} + +/* check add node */ + +test(multiStartNodeError().message, () => { + expect(() => checkAddNode('start-node', [createStartNode(uuid())], [])).toThrowError(multiStartNodeError()) +}) + +test(multiEndNodeError().message, () => { + expect(() => checkAddNode('end-node', [createEndNode(uuid())], [])).toThrowError(multiEndNodeError()) +}) + +/* check add connection */ +test(sourceNodeNotFoundError().message, () => { + expect(() => checkAddConnection(createConnection('a', 'b'), [], [])) +}) + +test(targetNodeNotFoundError().message, () => { + expect(() => checkAddConnection(createConnection('a', 'b'), [createStartNode('a')], [])) +}) + +test(startNodeToEndNodeError().message, () => { + expect(() => checkAddConnection( + createConnection('a', 'b'), + [createStartNode('a'), createEndNode('b')], + [] + )) +}) + +test(nodeToSelfError().message, () => { + expect(() => { + // language=JSON + const { + nodes, + edges + } = JSON.parse('{\n "nodes": [\n {\n "id": "P14abHl4uY",\n "type": "start-node",\n "position": {\n "x": 100,\n "y": 100\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 82\n }\n },\n {\n "id": "3YDRebKqCX",\n "type": "end-node",\n "position": {\n "x": 773.3027344262372,\n "y": 101.42648884412338\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 74\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "YXJ91nHVaz",\n "type": "llm-node",\n "position": {\n "x": 430.94541183662506,\n "y": 101.42648884412338\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 74\n },\n "selected": true,\n "dragging": false\n }\n ],\n "edges": [\n {\n "source": "P14abHl4uY",\n "target": "YXJ91nHVaz",\n "id": "xy-edge__P14abHl4uY-YXJ91nHVaz"\n },\n {\n "source": "YXJ91nHVaz",\n "target": "3YDRebKqCX",\n "id": "xy-edge__YXJ91nHVaz-3YDRebKqCX"\n }\n ],\n "data": {}\n}') + checkAddConnection(createConnection('YXJ91nHVaz', 'YXJ91nHVaz'), nodes, edges) + }).toThrowError(nodeToSelfError()) +}) + +test(hasCycleError().message, () => { + expect(() => { + // language=JSON + const { + nodes, + edges, + } = JSON.parse('{\n "nodes": [\n {\n "id": "-DKfXm7r3f",\n "type": "start-node",\n "position": {\n "x": -75.45812782717618,\n "y": 14.410669352596976\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 82\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "2uL3Hw2CAW",\n "type": "end-node",\n "position": {\n "x": 734.7875356349059,\n "y": -1.2807079327602473\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 74\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "yp-yYfKUzC",\n "type": "llm-node",\n "position": {\n "x": 338.2236369686051,\n "y": -92.5759939566568\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 74\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "N4HQPN-NYZ",\n "type": "llm-node",\n "position": {\n "x": 332.51768159211156,\n "y": 114.26488844123382\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 74\n },\n "selected": true,\n "dragging": false\n }\n ],\n "edges": [\n {\n "source": "-DKfXm7r3f",\n "target": "yp-yYfKUzC",\n "id": "xy-edge__-DKfXm7r3f-yp-yYfKUzC"\n },\n {\n "source": "yp-yYfKUzC",\n "target": "2uL3Hw2CAW",\n "id": "xy-edge__yp-yYfKUzC-2uL3Hw2CAW"\n },\n {\n "source": "-DKfXm7r3f",\n "target": "N4HQPN-NYZ",\n "id": "xy-edge__-DKfXm7r3f-N4HQPN-NYZ"\n },\n {\n "source": "N4HQPN-NYZ",\n "target": "yp-yYfKUzC",\n "id": "xy-edge__N4HQPN-NYZ-yp-yYfKUzC"\n }\n ],\n "data": {}\n}') + // language=JSON + checkAddConnection(JSON.parse('{\n "source": "yp-yYfKUzC",\n "sourceHandle": null,\n "target": "N4HQPN-NYZ",\n "targetHandle": null\n}'), nodes, edges) + }).toThrowError(hasCycleError()) +}) + +test(hasRedundantEdgeError().message, () => { + expect(() => { + // language=JSON + const { + nodes, + edges, + } = JSON.parse('{\n "nodes": [\n {\n "id": "ldoKAzHnKF",\n "type": "llm-node",\n "position": {\n "x": 207,\n "y": -38\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 105\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "1eJtMoJWs6",\n "type": "llm-node",\n "position": {\n "x": 207,\n "y": 172.5\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 105\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "7e5vQLDGTl",\n "type": "start-node",\n "position": {\n "x": -162.3520537805597,\n "y": 67.84901301708827\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 105\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "Wyqg_bXILg",\n "type": "knowledge-node",\n "position": {\n "x": 560.402133595296,\n "y": -38.892263766178665\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "7DaF-0G-yv",\n "type": "llm-node",\n "position": {\n "x": 634.9924233956513,\n "y": 172.01821084172227\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "mymIbw_W6k",\n "type": "end-node",\n "position": {\n "x": 953.9302142661356,\n "y": 172.0182108417223\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": false,\n "dragging": false\n }\n ],\n "edges": [\n {\n "source": "7e5vQLDGTl",\n "target": "ldoKAzHnKF",\n "id": "xy-edge__7e5vQLDGTl-ldoKAzHnKF"\n },\n {\n "source": "ldoKAzHnKF",\n "target": "Wyqg_bXILg",\n "id": "xy-edge__ldoKAzHnKF-Wyqg_bXILg"\n },\n {\n "source": "7e5vQLDGTl",\n "target": "1eJtMoJWs6",\n "id": "xy-edge__7e5vQLDGTl-1eJtMoJWs6"\n },\n {\n "source": "Wyqg_bXILg",\n "target": "7DaF-0G-yv",\n "id": "xy-edge__Wyqg_bXILg-7DaF-0G-yv"\n },\n {\n "source": "1eJtMoJWs6",\n "target": "7DaF-0G-yv",\n "id": "xy-edge__1eJtMoJWs6-7DaF-0G-yv"\n },\n {\n "source": "7DaF-0G-yv",\n "target": "mymIbw_W6k",\n "id": "xy-edge__7DaF-0G-yv-mymIbw_W6k"\n }\n ],\n "data": {\n "7e5vQLDGTl": {\n "inputs": {\n "question": {\n "type": "text",\n "description": "问题"\n }\n }\n },\n "ldoKAzHnKF": {\n "model": "qwen3",\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "systemPrompt": "你是个聪明人"\n },\n "1eJtMoJWs6": {\n "model": "deepseek",\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "systemPrompt": "你也是个好人"\n }\n }\n}') + // language=JSON + checkAddConnection(JSON.parse('{\n "source": "1eJtMoJWs6",\n "sourceHandle": null,\n "target": "Wyqg_bXILg",\n "targetHandle": null\n}'), nodes, edges) + }).toThrowError(hasRedundantEdgeError()) +}) + +/* check save */ +test(atLeastOneStartNodeError().message, () => { + expect(() => checkSave([], [], {})).toThrowError(atLeastOneStartNodeError()) +}) + +test(atLeastOneEndNodeError().message, () => { + expect(() => checkSave([createStartNode(uuid())], [], {})).toThrowError(atLeastOneEndNodeError()) +}) diff --git a/service-web/client/src/pages/ai/flow/FlowChecker.tsx b/service-web/client/src/pages/ai/flow/FlowChecker.tsx new file mode 100644 index 0000000..37f7862 --- /dev/null +++ b/service-web/client/src/pages/ai/flow/FlowChecker.tsx @@ -0,0 +1,113 @@ +import {find, findIdx, isEqual, lpad, toStr} from 'licia' +import {type Connection, type Edge, getOutgoers, type Node} from '@xyflow/react' + +export class CheckError extends Error { + readonly id: string + + constructor( + id: number, + message: string, + ) { + super(message) + this.id = `E${lpad(toStr(id), 6, '0')}` + } + + public toString(): string { + return `${this.id}: ${this.message}` + } +} + +export const multiStartNodeError = () => new CheckError(100, '只能存在1个开始节点') +export const multiEndNodeError = () => new CheckError(101, '只能存在1个结束节点') + +const getNodeById = (id: string, nodes: Node[]) => find(nodes, (n: Node) => isEqual(n.id, id)) + +// @ts-ignore +export const checkAddNode: (type: string, nodes: Node[], edges: Edge[]) => void = (type, nodes, edges) => { + if (isEqual(type, 'start-node') && findIdx(nodes, (node: Node) => isEqual(type, node.type)) > -1) { + throw multiStartNodeError() + } + if (isEqual(type, 'end-node') && findIdx(nodes, (node: Node) => isEqual(type, node.type)) > -1) { + throw multiEndNodeError() + } +} + +export const sourceNodeNotFoundError = () => new CheckError(200, '连线起始节点未找到') +export const targetNodeNotFoundError = () => new CheckError(201, '连线目标节点未找到') +export const startNodeToEndNodeError = () => new CheckError(202, '开始节点不能直连结束节点') +export const nodeToSelfError = () => new CheckError(203, '节点不能直连自身') +export const hasCycleError = () => new CheckError(204, '禁止流程循环') +export const nodeNotOnlyToEndNode = () => new CheckError(206, '直连结束节点的节点不允许连接其他节点') +export const hasRedundantEdgeError = () => new CheckError(207, '禁止出现冗余边') + +export const checkAddConnection: (connection: Connection, nodes: Node[], edges: Edge[]) => void = (connection, nodes, edges) => { + let sourceNode = getNodeById(connection.source, nodes) + if (!sourceNode) { + throw sourceNodeNotFoundError() + } + let targetNode = getNodeById(connection.target, nodes) + if (!targetNode) { + throw targetNodeNotFoundError() + } + // 禁止短路整个流程 + if (isEqual('start-node', sourceNode.type) && isEqual('end-node', targetNode.type)) { + throw startNodeToEndNodeError() + } + + // 禁止流程出现环,必须是有向无环图 + const hasCycle = (node: Node, visited = new Set()) => { + if (visited.has(node.id)) return false + visited.add(node.id) + for (const outgoer of getOutgoers(node, nodes, edges)) { + if (isEqual(outgoer.id, sourceNode?.id)) return true + if (hasCycle(outgoer, visited)) return true + } + } + if (isEqual(sourceNode.id, targetNode.id)) { + throw nodeToSelfError() + } else if (hasCycle(targetNode)) { + throw hasCycleError() + } + + let outgoers = [targetNode, ...getOutgoers(sourceNode, nodes, edges)] + if (outgoers.length > 1 && findIdx(outgoers, (node: Node) => isEqual(node.type, 'end-node')) > -1) { + throw nodeNotOnlyToEndNode() + } + + /*const hasRedundant = (source: Node, target: Node) => { + const visited = new Set() + const queue = new Queue() + queue.enqueue(source) + visited.add(source.id) + while (queue.size > 0) { + const current = queue.dequeue()! + console.log(current.id) + for (const incomer of getIncomers(current, nodes, edges)) { + if (isEqual(incomer.id, target.id)) { + return true + } + if (!visited.has(incomer.id)) { + visited.add(incomer.id) + queue.enqueue(incomer) + } + } + } + return false + } + if (hasRedundant(sourceNode, targetNode)) { + throw new Error('出现冗余边') + }*/ +} + +export const atLeastOneStartNodeError = () => new CheckError(300, '至少存在1个开始节点') +export const atLeastOneEndNodeError = () => new CheckError(301, '至少存在1个结束节点') + +// @ts-ignore +export const checkSave: (nodes: Node[], edges: Edge[], data: any) => void = (nodes, edges, data) => { + if (nodes.filter(n => isEqual('start-node', n.type)).length < 1) { + throw atLeastOneStartNodeError() + } + if (nodes.filter(n => isEqual('end-node', n.type)).length < 1) { + throw atLeastOneEndNodeError() + } +} \ No newline at end of file diff --git a/service-web/client/src/pages/ai/flow/FlowEditor.tsx b/service-web/client/src/pages/ai/flow/FlowEditor.tsx index 0b5e201..fa318a0 100644 --- a/service-web/client/src/pages/ai/flow/FlowEditor.tsx +++ b/service-web/client/src/pages/ai/flow/FlowEditor.tsx @@ -1,25 +1,13 @@ import {PlusCircleFilled, SaveFilled} from '@ant-design/icons' -import { - Background, - BackgroundVariant, - type Connection, - Controls, - type Edge, - getOutgoers, - MiniMap, - type Node, - type NodeProps, - ReactFlow, -} from '@xyflow/react' +import {Background, BackgroundVariant, Controls, MiniMap, type NodeProps, ReactFlow,} from '@xyflow/react' import {useMount} from 'ahooks' import type {Schema} from 'amis' import {Button, Drawer, Dropdown, message, Space} from 'antd' -import {arrToMap, find, findIdx, isEqual, isNil, randomId} from 'licia' -import {type JSX, useState} from 'react' +import {arrToMap, find, isEqual, isNil, randomId} from 'licia' +import {type JSX, type MemoExoticComponent, useState} from 'react' import styled from 'styled-components' import '@xyflow/react/dist/style.css' import {amisRender, commonInfo, horizontalFormOptions} from '../../../util/amis.tsx' -import {buildEL} from './ElParser.tsx' import CodeNode from './node/CodeNode.tsx' import EndNode from './node/EndNode.tsx' import KnowledgeNode from './node/KnowledgeNode.tsx' @@ -27,6 +15,8 @@ import LlmNode from './node/LlmNode.tsx' import StartNode from './node/StartNode.tsx' import {useDataStore} from './store/DataStore.ts' import {useFlowStore} from './store/FlowStore.ts' +import SwitchNode from './node/SwitchNode.tsx' +import {checkAddConnection, checkAddNode, checkSave} from './FlowChecker.tsx' const FlowableDiv = styled.div` height: 100%; @@ -70,7 +60,7 @@ function FlowEditor() { const [nodeDef] = useState<{ key: string, name: string, - component: (props: NodeProps) => JSX.Element + component: MemoExoticComponent<(props: NodeProps) => JSX.Element> }[]>([ { key: 'start-node', @@ -97,13 +87,17 @@ function FlowEditor() { name: '代码执行', component: CodeNode, }, + { + key: 'switch-node', + name: '条件分支', + component: SwitchNode, + }, ]) const [open, setOpen] = useState(false) const {data, setData, getDataById, setDataById} = useDataStore() const { nodes, - getNodeById, addNode, removeNode, setNodes, @@ -184,79 +178,6 @@ function FlowEditor() { } } - const checkNode = (type: string) => { - if (isEqual(type, 'start-node') && findIdx(nodes, (node: Node) => isEqual(type, node.type)) > -1) { - throw new Error('只能存在1个开始节点') - } - if (isEqual(type, 'end-node') && findIdx(nodes, (node: Node) => isEqual(type, node.type)) > -1) { - throw new Error('只能存在1个结束节点') - } - } - - const checkConnection = (connection: Connection) => { - let sourceNode = getNodeById(connection.source) - if (!sourceNode) { - throw new Error('连线起始节点未找到') - } - let targetNode = getNodeById(connection.target) - if (!targetNode) { - throw new Error('连线目标节点未找到') - } - // 禁止短路整个流程 - if (isEqual('start-node', sourceNode.type) && isEqual('end-node', targetNode.type)) { - throw new Error('开始节点不能直连结束节点') - } - - // 禁止流程出现环,必须是有向无环图 - const hasCycle = (node: Node, visited = new Set()) => { - if (visited.has(node.id)) return false - visited.add(node.id) - for (const outgoer of getOutgoers(node, nodes, edges)) { - if (isEqual(outgoer.id, sourceNode?.id)) return true - if (hasCycle(outgoer, visited)) return true - } - } - if (isEqual(sourceNode.id, targetNode.id)) { - throw new Error('节点不能直连自身') - } else if (hasCycle(targetNode)) { - throw new Error('禁止流程循环') - } - - /*const hasRedundant = (source: Node, target: Node) => { - const visited = new Set() - const queue = new Queue() - queue.enqueue(source) - visited.add(source.id) - while (queue.size > 0) { - const current = queue.dequeue()! - console.log(current.id) - for (const incomer of getIncomers(current, nodes, edges)) { - if (isEqual(incomer.id, target.id)) { - return true - } - if (!visited.has(incomer.id)) { - visited.add(incomer.id) - queue.enqueue(incomer) - } - } - } - return false - } - if (hasRedundant(sourceNode, targetNode)) { - throw new Error('出现冗余边') - }*/ - } - - // @ts-ignore - const checkSave = (nodes: Node[], edges: Edge[], data: any) => { - if (nodes.filter(n => isEqual('start-node', n.type)).length < 1) { - throw new Error('至少存在1个开始节点') - } - if (nodes.filter(n => isEqual('end-node', n.type)).length < 1) { - throw new Error('至少存在1个结束节点') - } - } - // 用于透传node操作到主流程 const initialNodeHandlers = { getDataById, @@ -268,10 +189,11 @@ function FlowEditor() { useMount(() => { // language=JSON let initialData = JSON.parse('{\n "nodes": [\n {\n "id": "ldoKAzHnKF",\n "type": "llm-node",\n "position": {\n "x": 207,\n "y": -38\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 105\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "1eJtMoJWs6",\n "type": "llm-node",\n "position": {\n "x": 207,\n "y": 172.5\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 105\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "7e5vQLDGTl",\n "type": "start-node",\n "position": {\n "x": -162.3520537805597,\n "y": 67.84901301708827\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 105\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "Wyqg_bXILg",\n "type": "knowledge-node",\n "position": {\n "x": 560.402133595296,\n "y": -38.892263766178665\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "7DaF-0G-yv",\n "type": "llm-node",\n "position": {\n "x": 634.9924233956513,\n "y": 172.01821084172227\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "mymIbw_W6k",\n "type": "end-node",\n "position": {\n "x": 953.9302142661356,\n "y": 172.0182108417223\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": false,\n "dragging": false\n }\n ],\n "edges": [\n {\n "source": "7e5vQLDGTl",\n "target": "ldoKAzHnKF",\n "id": "xy-edge__7e5vQLDGTl-ldoKAzHnKF"\n },\n {\n "source": "ldoKAzHnKF",\n "target": "Wyqg_bXILg",\n "id": "xy-edge__ldoKAzHnKF-Wyqg_bXILg"\n },\n {\n "source": "7e5vQLDGTl",\n "target": "1eJtMoJWs6",\n "id": "xy-edge__7e5vQLDGTl-1eJtMoJWs6"\n },\n {\n "source": "Wyqg_bXILg",\n "target": "7DaF-0G-yv",\n "id": "xy-edge__Wyqg_bXILg-7DaF-0G-yv"\n },\n {\n "source": "1eJtMoJWs6",\n "target": "7DaF-0G-yv",\n "id": "xy-edge__1eJtMoJWs6-7DaF-0G-yv"\n },\n {\n "source": "7DaF-0G-yv",\n "target": "mymIbw_W6k",\n "id": "xy-edge__7DaF-0G-yv-mymIbw_W6k"\n }\n ],\n "data": {\n "7e5vQLDGTl": {\n "inputs": {\n "question": {\n "type": "text",\n "description": "问题"\n }\n }\n },\n "ldoKAzHnKF": {\n "model": "qwen3",\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "systemPrompt": "你是个聪明人"\n },\n "1eJtMoJWs6": {\n "model": "deepseek",\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "systemPrompt": "你也是个好人"\n }\n }\n}') - let initialNodes = initialData['nodes'] ?? [] - let initialEdges = initialData['edges'] ?? [] + // let initialData: any = {} + let initialNodes = initialData?.nodes ?? [] + let initialEdges = initialData?.edges ?? [] - let initialNodeData = initialData['data'] ?? {} + let initialNodeData = initialData?.data ?? {} setData(initialNodeData) for (let node of initialNodes) { @@ -290,7 +212,10 @@ function FlowEditor() { items: nodeDef.map(def => ({key: def.key, label: def.name})), onClick: ({key}) => { try { - checkNode(key) + if (commonInfo.debug) { + console.info('Add', key, JSON.stringify({nodes, edges, data})) + } + checkAddNode(key, nodes, edges) addNode({ id: randomId(10), type: key, @@ -299,7 +224,7 @@ function FlowEditor() { }) } catch (e) { // @ts-ignore - messageApi.error(e.message) + messageApi.error(e.toString()) } }, }} @@ -311,13 +236,15 @@ function FlowEditor() {