feat(web): 增加分支节点,增加流程检查的测试

This commit is contained in:
2025-06-29 19:02:19 +08:00
parent 8884495a89
commit 779fd0eb18
11 changed files with 648 additions and 110 deletions

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -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())
})

View File

@@ -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<string>()) => {
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<string>()
const queue = new Queue<Node>()
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()
}
}

View File

@@ -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<string>()) => {
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<string>()
const queue = new Queue<Node>()
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() {
</Dropdown>
<Button type="primary" onClick={() => {
try {
if (commonInfo.debug) {
console.info('Save', JSON.stringify({nodes, edges, data}))
}
checkSave(nodes, edges, data)
let saveData = {nodes, edges, data}
console.log(JSON.stringify(saveData, null, 2))
console.log(buildEL(nodes, edges))
// let saveData = {nodes, edges, data}
// console.log(buildEL(nodes, edges))
} catch (e) {
// @ts-ignore
messageApi.error(e.message)
messageApi.error(e.toString())
}
}}>
<SaveFilled/>
@@ -341,11 +268,14 @@ function FlowEditor() {
onEdgesChange={onEdgesChange}
onConnect={(connection) => {
try {
checkConnection(connection)
if (commonInfo.debug) {
console.info('Connection', JSON.stringify(connection), JSON.stringify({nodes, edges, data}))
}
checkAddConnection(connection, nodes, edges)
onConnect(connection)
} catch (e) {
// @ts-ignore
messageApi.error(e.message)
messageApi.error(e.toString())
}
}}
// @ts-ignore

View File

@@ -1,5 +1,6 @@
import type {NodeProps} from '@xyflow/react'
import AmisNode, {inputsFormColumns, outputsFormColumns} from './AmisNode.tsx'
import React from 'react'
const CodeNode = (props: NodeProps) => AmisNode({
nodeProps: props,
@@ -48,4 +49,4 @@ const CodeNode = (props: NodeProps) => AmisNode({
],
})
export default CodeNode
export default React.memo(CodeNode)

View File

@@ -1,5 +1,6 @@
import type {NodeProps} from '@xyflow/react'
import AmisNode, {outputsFormColumns} from './AmisNode.tsx'
import React from 'react'
const EndNode = (props: NodeProps) => AmisNode({
nodeProps: props,
@@ -9,4 +10,4 @@ const EndNode = (props: NodeProps) => AmisNode({
columnSchema: outputsFormColumns(true),
})
export default EndNode
export default React.memo(EndNode)

View File

@@ -1,6 +1,7 @@
import type {NodeProps} from '@xyflow/react'
import {commonInfo} from '../../../../util/amis.tsx'
import AmisNode, {inputsFormColumns, outputsFormColumns} from './AmisNode.tsx'
import React from 'react'
const KnowledgeNode = (props: NodeProps) => AmisNode({
nodeProps: props,
@@ -62,4 +63,4 @@ const KnowledgeNode = (props: NodeProps) => AmisNode({
],
})
export default KnowledgeNode
export default React.memo(KnowledgeNode)

View File

@@ -1,6 +1,7 @@
import type {NodeProps} from '@xyflow/react'
import {Tag} from 'antd'
import AmisNode, {inputsFormColumns, outputsFormColumns} from './AmisNode.tsx'
import React from 'react'
const modelMap: Record<string, string> = {
qwen3: 'Qwen3',
@@ -47,4 +48,4 @@ const LlmNode = (props: NodeProps) => AmisNode({
],
})
export default LlmNode
export default React.memo(LlmNode)

View File

@@ -1,7 +1,7 @@
import type {NodeProps} from '@xyflow/react'
import {Tag} from 'antd'
import {each} from 'licia'
import type {JSX} from 'react'
import React, {type JSX} from 'react'
import {horizontalFormOptions} from '../../../../util/amis.tsx'
import AmisNode from './AmisNode.tsx'
@@ -18,7 +18,7 @@ const StartNode = (props: NodeProps) => AmisNode({
defaultNodeDescription: '定义输入变量',
extraNodeDescription: nodeData => {
const variables: JSX.Element[] = []
const inputs = (nodeData['inputs'] ?? {}) as Record<string, { type: string, description: string }>
const inputs = (nodeData?.inputs ?? {}) as Record<string, { type: string, description: string }>
each(inputs, (value, key) => {
variables.push(
<div className="mt-1 flex justify-between" key={key}>
@@ -65,4 +65,4 @@ const StartNode = (props: NodeProps) => AmisNode({
],
})
export default StartNode
export default React.memo(StartNode)

View File

@@ -0,0 +1,53 @@
import {Handle, type NodeProps, Position} from '@xyflow/react'
import AmisNode from './AmisNode.tsx'
import {Tag} from 'antd'
import React from 'react'
const cases = [
{
index: 1
},
{
index: 2
},
{
index: 3
}
]
const SwitchNode = (props: NodeProps) => AmisNode({
nodeProps: props,
type: 'normal',
defaultNodeName: '分支节点',
defaultNodeDescription: '根据不同的情况前往不同的分支',
columnSchema: [],
extraNodeDescription: nodeData => {
return (
<div className="mt-2">
{cases.map(item => (
<div key={item.index} className="mt-1">
<Tag className="m-0" color="blue"> {item.index}</Tag>
</div>
))}
</div>
)
},
handlers: nodeData => {
return (
<>
<Handle type="target" position={Position.Left}/>
{cases.map((item, index) => (
<Handle
type="source"
position={Position.Right}
key={item.index}
id={`${item.index}`}
style={{top: 85 + (25 * index)}}
/>
))}
</>
)
}
})
export default React.memo(SwitchNode)