diff --git a/.env b/.env index bd4d14a9..2265ce3e 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ -VITE_TITLE = "分布式平台" +VITE_TITLE = "AI自动化渗透测试平台" -VITE_VERSION = "1.0.0" \ No newline at end of file +VITE_VERSION = "1.0.0" diff --git a/.env.development b/.env.development index ce753ec8..498f9c9b 100644 --- a/.env.development +++ b/.env.development @@ -3,8 +3,8 @@ VITE_APP_MODE=legacy # VITE_BASE_URL = "http://192.168.3.17:8082/" -# VITE_BASE_URL = "http://legion-4g.yaklang.com:8080/pre/" +VITE_BASE_URL = "http://legion-4g.yaklang.com:8080/" # VITE_BASE_URL = "http://192.168.3.100:8082/" -VITE_BASE_URL = "http://192.168.3.3:8082/" +# VITE_BASE_URL = "http://192.168.3.3:8082/" diff --git a/.env.production b/.env.production index 5f471a2e..dc0a53a5 100644 --- a/.env.production +++ b/.env.production @@ -1,5 +1,5 @@ -VITE_BASE_URL = "http://192.168.3.3:8082/" +# VITE_BASE_URL = "http://192.168.3.3:8082/" # VITE_BASE_URL = "http://192.168.3.17:8082/" -# VITE_BASE_URL = "http://legion-4g.yaklang.com:8080/pre/" \ No newline at end of file +VITE_BASE_URL = "http://legion-4g.yaklang.com:8080/" diff --git a/.gitignore b/.gitignore index 4108b33e..5698c27c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,25 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist.zip +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.husky/pre-commit b/.husky/pre-commit index f841cf21..d5a13922 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,2 @@ #!/usr/bin/env sh -npx lint-staged +node scripts/pre-commit.mjs diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 694e9cdc..5c3953a7 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -1,4 +1,4 @@ { "*.{js,jsx}": ["eslint --fix"], - "*.{ts,tsx}": ["eslint --fix", "bash -c tsc"] + "*.{ts,tsx}": ["eslint --fix"] } diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index af58978e..00000000 --- a/.prettierrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "eslint-config-alloy/.prettierrc.js", - "tabWidth": 4, - "semi": true, - "singleQuote": true, - "printWidth": 80, - "bracketSpacing": true, - "arrowParens": "always" -} diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000..a5831169 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,44 @@ +// .prettierrc.js +export default { + // 一行最多 120 字符 + printWidth: 120, + // 使用 2 个空格缩进 + tabWidth: 2, + // 不使用缩进符,而使用空格 + useTabs: false, + // 行尾需要有分号 + semi: false, + // 使用单引号 + singleQuote: true, + // 对象的 key 仅在必要时用引号 + quoteProps: "as-needed", + // jsx 不使用单引号,而使用双引号 + jsxSingleQuote: false, + // 末尾需要有逗号 + trailingComma: "all", + // 大括号内的首尾需要空格 + bracketSpacing: true, + // jsx 标签的反尖括号需要换行 + bracketSameLine: false, + // 箭头函数,只有一个参数的时候,也需要括号 + arrowParens: "always", + // 每个文件格式化的范围是文件的全部内容 + rangeStart: 0, + rangeEnd: Infinity, + // 不需要写文件开头的 @prettier + requirePragma: false, + // 不需要自动在文件开头插入 @prettier + insertPragma: false, + // 使用默认的折行标准 + proseWrap: "preserve", + // 根据显示样式决定 html 要不要折行 + htmlWhitespaceSensitivity: "css", + // vue 文件中的 script 和 style 内不用缩进 + vueIndentScriptAndStyle: false, + // 换行符使用 lf + endOfLine: "lf", + // 格式化嵌入的内容 + embeddedLanguageFormatting: "auto", + // html, vue, jsx 中每个属性占一行 + singleAttributePerLine: false, + }; \ No newline at end of file diff --git a/README.en.md b/README.en.md index c6ea7638..aca11d53 100644 --- a/README.en.md +++ b/README.en.md @@ -2,7 +2,7 @@ #### Description -分布式平台 +AI自动化渗透测试平台 #### Software Architecture diff --git a/index.html b/index.html index fd58013b..5e7b1312 100644 --- a/index.html +++ b/index.html @@ -1,23 +1,23 @@ - - - - - - 自动化渗透系统 - - - -
- - + + + + + + + + AI自动化渗透测试平台 + + + + +
+ + + diff --git a/package.json b/package.json index 31a9e6b2..d2c7e325 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,10 @@ "scripts": { "dev": "vite --port 4173", "dev:irify": "vite --port 4174 --mode irify.development", - "build": "tsc && vite build", + "build": "vite build", "build:irify": "tsc && vite build --mode irify.production --outDir dist-irify", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "typecheck": "tsc --noEmit --pretty false", "preview": "vite preview", "prepare": "husky install", "commit": "git-cz", @@ -33,6 +34,19 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@hello-pangea/dnd": "16.5.0", + "@tanstack/react-query": "^5.95.0", + "@milkdown/kit": "^7.5.5", + "@milkdown/plugin-collab": "^7.5.0", + "@milkdown/react": "^7.5.0", + "@milkdown/theme-nord": "^7.5.0", + "@prosemirror-adapter/react": "^0.2.6", + "@rjsf/antd": "^5.22.4", + "@rjsf/core": "^5.22.4", + "@rjsf/utils": "^5.22.4", + "@rjsf/validator-ajv8": "^5.22.4", + "@streamdown/code": "^1.0.1", + "@streamdown/math": "^1.0.1", + "@streamdown/mermaid": "^1.0.1", "@uiw/react-md-editor": "3.6.5", "@viz-js/viz": "3.7.0", "@yakit-libs/color": "^1.0.2", @@ -46,6 +60,7 @@ "dayjs": "^1.11.13", "dnd-core": "16.0.1", "echarts": "^5.5.1", + "echarts-for-react": "^3.0.6", "eslint-import-resolver-alias": "^1.1.2", "event-source-polyfill": "^1.0.4", "file-saver": "^2.0.5", @@ -76,10 +91,15 @@ "react-monaco-editor": "0.54.0", "react-resize-detector": "6.7.8", "react-router-dom": "^6.26.2", + "react-virtuoso": "4.14.1", "react-wordcloud": "^1.2.7", "rehype-highlight": "^7.0.1", "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "rehype-slug": "^6.0.0", + "remark-directive": "^3.0.0", "remark-gfm": "^4.0.0", + "streamdown": "^2.1.0", "ts-pattern": "^4.0.4", "uuid": "^11.1.0", "zustand": "^4.5.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9ab0207..4aeba7c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 5.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@ant-design/pro-components': specifier: ^2.8.2 - version: 2.8.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.8.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@antv/data-set': specifier: ^0.11.8 version: 0.11.8 @@ -38,6 +38,45 @@ importers: '@hello-pangea/dnd': specifier: 16.5.0 version: 16.5.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@milkdown/kit': + specifier: ^7.5.5 + version: 7.19.0(@codemirror/language@6.12.2)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(typescript@5.6.2) + '@milkdown/plugin-collab': + specifier: ^7.5.0 + version: 7.19.0(y-prosemirror@1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29) + '@milkdown/react': + specifier: ^7.5.0 + version: 7.19.0(@codemirror/language@6.12.2)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2) + '@milkdown/theme-nord': + specifier: ^7.5.0 + version: 7.19.0 + '@prosemirror-adapter/react': + specifier: ^0.2.6 + version: 0.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@rjsf/antd': + specifier: ^5.22.4 + version: 5.24.13(@ant-design/icons@5.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@rjsf/core@5.24.13(@rjsf/utils@5.24.13(react@18.3.1))(react@18.3.1))(@rjsf/utils@5.24.13(react@18.3.1))(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dayjs@1.11.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@rjsf/core': + specifier: ^5.22.4 + version: 5.24.13(@rjsf/utils@5.24.13(react@18.3.1))(react@18.3.1) + '@rjsf/utils': + specifier: ^5.22.4 + version: 5.24.13(react@18.3.1) + '@rjsf/validator-ajv8': + specifier: ^5.22.4 + version: 5.24.13(@rjsf/utils@5.24.13(react@18.3.1)) + '@streamdown/code': + specifier: ^1.0.1 + version: 1.0.3(react@18.3.1) + '@streamdown/math': + specifier: ^1.0.1 + version: 1.0.2(react@18.3.1) + '@streamdown/mermaid': + specifier: ^1.0.1 + version: 1.0.2(react@18.3.1) + '@tanstack/react-query': + specifier: ^5.95.0 + version: 5.95.0(react@18.3.1) '@uiw/react-md-editor': specifier: 3.6.5 version: 3.6.5(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -55,7 +94,7 @@ importers: version: 3.8.0(react@18.3.1) antd: specifier: ^5.21.2 - version: 5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) axios: specifier: ^1.7.7 version: 1.7.7 @@ -77,6 +116,9 @@ importers: echarts: specifier: ^5.5.1 version: 5.5.1 + echarts-for-react: + specifier: ^3.0.6 + version: 3.0.6(echarts@5.5.1)(react@18.3.1) eslint-import-resolver-alias: specifier: ^1.1.2 version: 1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)) @@ -167,6 +209,9 @@ importers: react-router-dom: specifier: ^6.26.2 version: 6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-virtuoso: + specifier: 4.14.1 + version: 4.14.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-wordcloud: specifier: ^1.2.7 version: 1.2.7(react@18.3.1) @@ -176,9 +221,21 @@ importers: rehype-raw: specifier: ^7.0.0 version: 7.0.0 + rehype-sanitize: + specifier: ^6.0.0 + version: 6.0.0 + rehype-slug: + specifier: ^6.0.0 + version: 6.0.0 + remark-directive: + specifier: ^3.0.0 + version: 3.0.1 remark-gfm: specifier: ^4.0.0 version: 4.0.0 + streamdown: + specifier: ^2.1.0 + version: 2.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ts-pattern: specifier: ^4.0.4 version: 4.0.4 @@ -266,7 +323,7 @@ importers: version: 5.6.2 unocss: specifier: ^0.58.9 - version: 0.58.9(postcss@8.4.47)(rollup@4.24.0)(vite@5.4.8(@types/node@22.13.14)(sass@1.79.4)(terser@5.36.0)) + version: 0.58.9(postcss@8.5.8)(rollup@4.24.0)(vite@5.4.8(@types/node@22.13.14)(sass@1.79.4)(terser@5.36.0)) vite: specifier: ^5.4.8 version: 5.4.8(@types/node@22.13.14)(sass@1.79.4)(terser@5.36.0) @@ -396,6 +453,9 @@ packages: '@antfu/install-pkg@0.4.1': resolution: {integrity: sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw==} + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@antfu/utils@0.7.10': resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} @@ -623,6 +683,10 @@ packages: resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.7': resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==} engines: {node: '>=6.9.0'} @@ -631,6 +695,10 @@ packages: resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.25.7': resolution: {integrity: sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==} engines: {node: '>=6.9.0'} @@ -648,6 +716,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-syntax-jsx@7.25.7': resolution: {integrity: sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==} engines: {node: '>=6.9.0'} @@ -724,11 +797,126 @@ packages: resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + '@chenshuai2144/sketch-color@1.0.9': resolution: {integrity: sha512-obzSy26cb7Pm7OprWyVpgMpIlrZpZ0B7vbrU0RMbvRg0YAI890S5Xy02Aj1Nhl4+KTbi1lVYHt6HQP8Hm9s+1w==} peerDependencies: react: '>=16.12.0' + '@chevrotain/cst-dts-gen@11.1.2': + resolution: {integrity: sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==} + + '@chevrotain/gast@11.1.2': + resolution: {integrity: sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g==} + + '@chevrotain/regexp-to-ast@11.1.2': + resolution: {integrity: sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw==} + + '@chevrotain/types@11.1.2': + resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + + '@chevrotain/utils@11.1.2': + resolution: {integrity: sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==} + + '@codemirror/autocomplete@6.20.1': + resolution: {integrity: sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==} + + '@codemirror/commands@6.10.2': + resolution: {integrity: sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==} + + '@codemirror/lang-angular@0.1.4': + resolution: {integrity: sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==} + + '@codemirror/lang-cpp@6.0.3': + resolution: {integrity: sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==} + + '@codemirror/lang-css@6.3.1': + resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} + + '@codemirror/lang-go@6.0.1': + resolution: {integrity: sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==} + + '@codemirror/lang-html@6.4.11': + resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==} + + '@codemirror/lang-java@6.0.2': + resolution: {integrity: sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==} + + '@codemirror/lang-javascript@6.2.5': + resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==} + + '@codemirror/lang-jinja@6.0.0': + resolution: {integrity: sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/lang-less@6.0.2': + resolution: {integrity: sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==} + + '@codemirror/lang-liquid@6.3.2': + resolution: {integrity: sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw==} + + '@codemirror/lang-markdown@6.5.0': + resolution: {integrity: sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==} + + '@codemirror/lang-php@6.0.2': + resolution: {integrity: sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==} + + '@codemirror/lang-python@6.2.1': + resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==} + + '@codemirror/lang-rust@6.0.2': + resolution: {integrity: sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==} + + '@codemirror/lang-sass@6.0.2': + resolution: {integrity: sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==} + + '@codemirror/lang-sql@6.10.0': + resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==} + + '@codemirror/lang-vue@0.1.3': + resolution: {integrity: sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==} + + '@codemirror/lang-wast@6.0.2': + resolution: {integrity: sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==} + + '@codemirror/lang-xml@6.1.0': + resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==} + + '@codemirror/lang-yaml@6.1.2': + resolution: {integrity: sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==} + + '@codemirror/language-data@6.5.2': + resolution: {integrity: sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==} + + '@codemirror/language@6.12.2': + resolution: {integrity: sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==} + + '@codemirror/legacy-modes@6.5.2': + resolution: {integrity: sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==} + + '@codemirror/lint@6.9.5': + resolution: {integrity: sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==} + + '@codemirror/search@6.6.0': + resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==} + + '@codemirror/state@6.5.4': + resolution: {integrity: sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==} + + '@codemirror/theme-one-dark@6.1.3': + resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} + + '@codemirror/view@6.39.16': + resolution: {integrity: sha512-m6S22fFpKtOWhq8HuhzsI1WzUP/hB9THbDj0Tl5KX4gbO6Y91hwBl7Yky33NdvB6IffuRFiBxf1R8kJMyXmA4Q==} + '@commitlint/cli@19.6.1': resolution: {integrity: sha512-8hcyA6ZoHwWXC76BoC8qVOSr8xHy00LZhZpauiD0iO0VYbVhMnED0da85lTfIULxl7Lj4c6vZgF0Wu/ed1+jlQ==} engines: {node: '>=v18'} @@ -1011,6 +1199,15 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@hello-pangea/dnd@16.5.0': resolution: {integrity: sha512-n+am6O32jo/CFXciCysz83lPM3I3F58FJw4uS44TceieymcyxQSfzK5OhzPAKrVBZktmuOI6Zim9WABTMtXv4A==} peerDependencies: @@ -1036,6 +1233,9 @@ packages: '@iconify/utils@2.1.33': resolution: {integrity: sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==} + '@iconify/utils@3.1.0': + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} + '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -1054,12 +1254,66 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} '@juggle/resize-observer@3.4.0': resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + '@lezer/common@1.5.1': + resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==} + + '@lezer/cpp@1.1.5': + resolution: {integrity: sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==} + + '@lezer/css@1.3.1': + resolution: {integrity: sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==} + + '@lezer/go@1.0.1': + resolution: {integrity: sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/html@1.3.13': + resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==} + + '@lezer/java@1.1.3': + resolution: {integrity: sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.8': + resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==} + + '@lezer/markdown@1.6.3': + resolution: {integrity: sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==} + + '@lezer/php@1.0.5': + resolution: {integrity: sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==} + + '@lezer/python@1.1.18': + resolution: {integrity: sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==} + + '@lezer/rust@1.0.2': + resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==} + + '@lezer/sass@1.1.0': + resolution: {integrity: sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==} + + '@lezer/xml@1.0.6': + resolution: {integrity: sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==} + + '@lezer/yaml@1.0.4': + resolution: {integrity: sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==} + '@ljharb/resumer@0.0.1': resolution: {integrity: sha512-skQiAOrCfO7vRTq53cxznMpks7wS1va95UCidALlOVWqvBAzwPVErwizDwoMqNVMEn1mDq0utxZd02eIrvF1lw==} engines: {node: '>= 0.4'} @@ -1117,6 +1371,95 @@ packages: resolution: {integrity: sha512-cOZZOVhDSulgK0meTsTkmNXb1ahVvmTmWmfx9gRBwc6hq98wS9JP35ESIoNq3xqEan+UN+gn8187Z6E4NKhLsw==} hasBin: true + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + + '@mermaid-js/parser@1.0.0': + resolution: {integrity: sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==} + + '@milkdown/components@7.19.0': + resolution: {integrity: sha512-l/xasav/CPVXQZWs5oiFtnWw2zMk4Bq1EmKFElzsaKJCCW7ZBofasoGoQY5h0j+CDM8nAe8WLTq87WWWb9Ut6A==} + peerDependencies: + '@codemirror/language': ^6 + '@codemirror/state': ^6 + '@codemirror/view': ^6 + + '@milkdown/core@7.19.0': + resolution: {integrity: sha512-x5vxnVCxxKSGCa1+J7I4RzEDl4KkvsXJF6xm1zCtvj0BCsbCFGiUVx2AtLcFkkvWZ5530CuOouDJ9FC27yoCoA==} + + '@milkdown/crepe@7.19.0': + resolution: {integrity: sha512-3vY/5l8xc3LS1bh/bzzfuVhiFPP1gBYCSTp6iu6TXUgMHJvu8Tp2yhyBrjIWhiogA281cuA50i9gG47wOjWAIQ==} + + '@milkdown/ctx@7.19.0': + resolution: {integrity: sha512-tdG9jm6yk6PRSvFZW5rRSqOGrKdcNdbXJwfGiEGr538pgKYhJ/yKPF3HmfupkhGyxabRP/PydQa4q/N/OOs03g==} + + '@milkdown/exception@7.19.0': + resolution: {integrity: sha512-ykgjxqrOueTCjmDGr0aidulZa1mC6bg4f8eDyMiT0wd4vB+3iYmQxY8NxdKwUqlz4UM5KBnbyFlGlgQsQDL+ew==} + + '@milkdown/kit@7.19.0': + resolution: {integrity: sha512-q+FF2dLMpw056mwowg1de+vcDl2gLyNfBmOCJ1WjJSqw4evlcYcKjYgKZWViE/WLOjryQDQhDlg0g58/uUFiyQ==} + + '@milkdown/plugin-block@7.19.0': + resolution: {integrity: sha512-VCOscCUXOlkOO/i3PYUHHJ9nAk3rjBGlEB6Rs3Ge7hJbuv2Hb/5mTiWI2KRARu1deGaEaYUjsH8NX+BOH3ZMew==} + + '@milkdown/plugin-clipboard@7.19.0': + resolution: {integrity: sha512-V29/XE3M/ffvc5Owdm9tdUaD/GZ5AHMgbpBkd6+2/PH09MfGA9CPTpAjhqxxkWEi3CoT0HapdoceKuzd5bD7Nw==} + + '@milkdown/plugin-collab@7.19.0': + resolution: {integrity: sha512-00kD9NTcZt9v4ZrrM7coJ0VGQwNUI2mp1tGw2Uh3TTnDADYHSseQMNP1RozdrjUZP8iZlWurpzDIXlBHYnskOA==} + peerDependencies: + y-prosemirror: '*' + y-protocols: '*' + yjs: '*' + + '@milkdown/plugin-cursor@7.19.0': + resolution: {integrity: sha512-NGAuTxSbOdy3nHQ8bTk9I6Vi1eX14xpcN7QU65aIJM01RnGhTBx5cF6f82n0IWTXMbN+MVOuQfqywfdRx1ukLw==} + + '@milkdown/plugin-history@7.19.0': + resolution: {integrity: sha512-bC5bN0Ep5AC3hkiPS6LJTkorBoe5S7meJNzO0WqcIpkwckHD3M59Z7uz6dSUZdxVcL5IcFegDVsEYkiDq1Jcrw==} + + '@milkdown/plugin-indent@7.19.0': + resolution: {integrity: sha512-P9rJK9OHCmqry37pAFcknY3VVvAEpUt7RflfdjFXSb3aGyJb+TDaQBiTHgxzTTXmvmAaPGLm2uuGtx34ThrA1A==} + + '@milkdown/plugin-listener@7.19.0': + resolution: {integrity: sha512-shEVqcC2SKH0jaB74ReztNxG5hXjby26S1lN+evHKtsy2cB2vbWH2eHqSZkl8EGbLZJrqDsHwDKxTNTwB/oMSg==} + + '@milkdown/plugin-slash@7.19.0': + resolution: {integrity: sha512-mdcqxOC9voMHKBScCGZjtSU6xTd6/Z6Oc7XsTQ5Yc0XqRgqcYAL3ti/1dtP2BMy8zXu/pHegatOzGWO9DlrcUQ==} + + '@milkdown/plugin-tooltip@7.19.0': + resolution: {integrity: sha512-2HBiMgQ3aY/jdbxRRAbUkglk+PACCNGL7AERssRhr3G3Ph+eNwJYpK6VN7eHsUyDbFSP0koS7npr6U2kZWThFg==} + + '@milkdown/plugin-trailing@7.19.0': + resolution: {integrity: sha512-VbAqrvZq4S0kFVipuNmM+Qg8PvvT6SUXhRCMWzM0tX1I7H6Lie+oT/G5bpNPSKh7BNi7pbCllLGi0/L88vSzSw==} + + '@milkdown/plugin-upload@7.19.0': + resolution: {integrity: sha512-dAcxLf8TvCljgrRUa65lV3MYA5HAmCOjVHS0CzKCfC568T4eg3K2kSbr+EFEYCSR7vCbLjm/o9F4kI4qaWmAAw==} + + '@milkdown/preset-commonmark@7.19.0': + resolution: {integrity: sha512-8rPEd4S3ny5wuFJvnZdieedKxFeW3KU5Rz54LYhA/nYPG+tE9q5lqDs0ZzHNoJdXMiLWbNf/dd0QokHVNlbQLQ==} + + '@milkdown/preset-gfm@7.19.0': + resolution: {integrity: sha512-wW5ShJUhIaWNnbtv4IjV+xh9TvVId+Lm8CAurUs2E1nBX2N5wHTzzl2/9WOTt/g4u49e64rJewkwZJri8MPy7g==} + + '@milkdown/prose@7.19.0': + resolution: {integrity: sha512-T/uYqSr4YT4uZtu4nBxTWyvZhVs2Lzh9qpcYH81PVwtZUT3b57+e+39s1D7UKAwFGi0qB7qZu/53l6pcw8radg==} + + '@milkdown/react@7.19.0': + resolution: {integrity: sha512-rtvnM2tC8A0IAzkG2u7XYPNtN08Yek+z9AHUbf4SRyVbXsOCTRSZxA7FEGul1iLl6TUz4oU8tQnMHPH4iWXBtA==} + peerDependencies: + react: '*' + react-dom: '*' + + '@milkdown/theme-nord@7.19.0': + resolution: {integrity: sha512-TBbUuAJT8wiw3wZVVpDuA0U+UmNUulRgdtXnu72y2QNHdMLmYalw0Yjfx11cDEB52/Ld/z+xHuRvPNf30OSMjw==} + + '@milkdown/transformer@7.19.0': + resolution: {integrity: sha512-Ui1vwbyTd1nAaieTylI8ibNbXSAxygkiFjkwOPGO5w0Eu7leH+0hVrbeGUCSzYdJfjGsN537CYu/8kvLIR+lQg==} + + '@milkdown/utils@7.19.0': + resolution: {integrity: sha512-aIu8j7TypVn+4ZWgrIUjpljIulAVwNERWNZgkfYLQLOv+BbF1gIbpoB7t3w0RD2EeENrEu0P3J0Sl5LDMbyDRQ==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1129,6 +1472,9 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@ocavue/utils@1.5.0': + resolution: {integrity: sha512-zYOKnvUm8IQ86tNCrpUVBT0lEvoKvyf+9pPS7M4VDB8O1o+msaPMsb/xZx/w5hpcuN1dfehJrkY41ZcdGZpyqg==} + '@pkgr/core@0.1.1': resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -1148,6 +1494,15 @@ packages: '@probe.gl/stats@3.6.0': resolution: {integrity: sha512-JdALQXB44OP4kUBN/UrQgzbJe4qokbVF4Y8lkIA8iVCFnjVowWIgkD/z/0QO65yELT54tTrtepw1jScjKB+rhQ==} + '@prosemirror-adapter/core@0.2.6': + resolution: {integrity: sha512-7GXWQBR/bd6ngvek8SgtlEVC0x3KL7LKZ5cxyZl4IBAlRDbITQWsuKLChEXNan9A6+9Ry4a46crbDGTQlzooOg==} + + '@prosemirror-adapter/react@0.2.6': + resolution: {integrity: sha512-JdDqAJV7FnRyIlnXPP+E0Iu80BQPvHeB+7c0ZfdcHLfVb2BLTNHk6iOoqJmBJmqqHt/KjxXXTmxRUS4EEsjVlg==} + peerDependencies: + react: '*' + react-dom: '*' + '@rc-component/async-validator@5.0.4': resolution: {integrity: sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==} engines: {node: '>=14.x'} @@ -1216,6 +1571,36 @@ packages: resolution: {integrity: sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==} engines: {node: '>=14.0.0'} + '@rjsf/antd@5.24.13': + resolution: {integrity: sha512-UiWE8xoBxxCoe/SEkdQEmL5E6z3I1pw0+y0dTyGt8SHfAxxFc4/OWn7tKOAiNsKCXgf83t0JKn6CHWLD01sAdQ==} + engines: {node: '>=14'} + peerDependencies: + '@ant-design/icons': ^4.0.0 || ^5.0.0 + '@rjsf/core': ^5.24.x + '@rjsf/utils': ^5.24.x + antd: ^4.24.0 || ^5.8.5 + dayjs: ^1.8.0 + react: ^16.14.0 || >=17 + + '@rjsf/core@5.24.13': + resolution: {integrity: sha512-ONTr14s7LFIjx2VRFLuOpagL76sM/HPy6/OhdBfq6UukINmTIs6+aFN0GgcR0aXQHFDXQ7f/fel0o/SO05Htdg==} + engines: {node: '>=14'} + peerDependencies: + '@rjsf/utils': ^5.24.x + react: ^16.14.0 || >=17 + + '@rjsf/utils@5.24.13': + resolution: {integrity: sha512-rNF8tDxIwTtXzz5O/U23QU73nlhgQNYJ+Sv5BAwQOIyhIE2Z3S5tUiSVMwZHt0julkv/Ryfwi+qsD4FiE5rOuw==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.14.0 || >=17 + + '@rjsf/validator-ajv8@5.24.13': + resolution: {integrity: sha512-oWHP7YK581M8I5cF1t+UXFavnv+bhcqjtL1a7MG/Kaffi0EwhgcYjODrD8SsnrhncsEYMqSECr4ZOEoirnEUWw==} + engines: {node: '>=14'} + peerDependencies: + '@rjsf/utils': ^5.24.x + '@rollup/pluginutils@5.1.2': resolution: {integrity: sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==} engines: {node: '>=14.0.0'} @@ -1305,14 +1690,58 @@ packages: cpu: [x64] os: [win32] + '@shikijs/core@3.23.0': + resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} + + '@shikijs/engine-javascript@3.23.0': + resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} + + '@shikijs/engine-oniguruma@3.23.0': + resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} + + '@shikijs/langs@3.23.0': + resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} + + '@shikijs/themes@3.23.0': + resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} + + '@shikijs/types@3.23.0': + resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/is@0.14.0': resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} engines: {node: '>=6'} + '@streamdown/code@1.0.3': + resolution: {integrity: sha512-3Ym5TCLcGhrHY2qBaUVWpqNRtxnZvqh4Y5Qm/pTIKA4AmEWwAAoYjZnxG7mOsvOpWVWiDwETjUtchNL1XzQEAw==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + + '@streamdown/math@1.0.2': + resolution: {integrity: sha512-r8Ur9/lBuFnzZAFdEWrLUF2s/gRwRRRwruqltdZibyjbCBnuW7SJbFm26nXqvpJPW/gzpBUMrBVBzd88z05D5g==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + + '@streamdown/mermaid@1.0.2': + resolution: {integrity: sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + '@szmarczak/http-timer@1.1.2': resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} engines: {node: '>=6'} + '@tanstack/query-core@5.95.0': + resolution: {integrity: sha512-H1/CWCe8tGL3YIVeo770Z6kPbt0B3M1d/iQXIIK1qlFiFt6G2neYdkHgLapOC8uMYNt9DmHjmGukEKgdMk1P+A==} + + '@tanstack/react-query@5.95.0': + resolution: {integrity: sha512-EMP8B+BK9zvnAemT8M/y3z/WO0NjZ7fIUY3T3wnHYK6AA3qK/k33i7tPgCXCejhX0cd4I6bJIXN2GmjrHjDBzg==} + peerDependencies: + react: ^18 || ^19 + '@turf/bbox-polygon@6.5.0': resolution: {integrity: sha512-+/r0NyL1lOG3zKZmmf6L8ommU07HliP4dgYToMoTxqzsWzyLjaj/OzgQ8rBmv703WJX+aS6yCmLuIhYqyufyuw==} @@ -1352,9 +1781,99 @@ packages: '@types/conventional-commits-parser@5.0.0': resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + '@types/d3-timer@2.0.3': resolution: {integrity: sha512-jhAJzaanK5LqyLQ50jJNIrB8fjL9gwWZTgYjevPvkDLMU+kTAZkYsobI59nYoeSrH1PucuyJEi247Pb90t6XUg==} + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -1390,9 +1909,15 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/katex@0.16.8': + resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} + '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + '@types/lodash@4.17.10': resolution: {integrity: sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==} @@ -1449,6 +1974,9 @@ packages: '@types/supercluster@7.1.3': resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1635,6 +2163,35 @@ packages: '@viz-js/viz@3.7.0': resolution: {integrity: sha512-+nZ5LWe09ypRDk43pDU09wLW80kLAEonM7xES4s6AWiCUfAy5Vb+rxnQ3hKw79ptdaHJsyNIfwnzpEFtfMIyGA==} + '@vue/compiler-core@3.5.29': + resolution: {integrity: sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==} + + '@vue/compiler-dom@3.5.29': + resolution: {integrity: sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==} + + '@vue/compiler-sfc@3.5.29': + resolution: {integrity: sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==} + + '@vue/compiler-ssr@3.5.29': + resolution: {integrity: sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==} + + '@vue/reactivity@3.5.29': + resolution: {integrity: sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==} + + '@vue/runtime-core@3.5.29': + resolution: {integrity: sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==} + + '@vue/runtime-dom@3.5.29': + resolution: {integrity: sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==} + + '@vue/server-renderer@3.5.29': + resolution: {integrity: sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==} + peerDependencies: + vue: 3.5.29 + + '@vue/shared@3.5.29': + resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} + '@webgpu/glslang@0.0.15': resolution: {integrity: sha512-niT+Prh3Aff8Uf1MVBVUsaNjFj9rJAKDXuoHIKiQbB+6IUP/3J3JIhBNyZ7lDhytvXxw6ppgnwKZdDJ08UMj4Q==} @@ -1667,8 +2224,8 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} hasBin: true @@ -1681,6 +2238,14 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -2060,6 +2625,14 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + chevrotain-allstar@0.3.1: + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.1.2: + resolution: {integrity: sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -2129,9 +2702,16 @@ packages: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} engines: {node: '>=6'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + codemirror@5.65.18: resolution: {integrity: sha512-Gaz4gHnkbHMGgahNt3CA5HBk5lLQBqmD/pBgeB4kQU6OedZmqMBjlRF0LSrp2tJ4wlLNPm2FfaUd1pDy0mdlpA==} + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -2171,6 +2751,14 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + comment-json@2.4.2: resolution: {integrity: sha512-T+iXox779qsqneMYx/x5BZyz4xjCeQRmuNVzz8tko7qZUs3MlzpA3RAs+O1XsgcKToNBMIvfVzafGOeiU7RggA==} engines: {node: '>= 6'} @@ -2183,6 +2771,12 @@ packages: compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + compute-gcd@1.2.1: + resolution: {integrity: sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg==} + + compute-lcm@1.1.2: + resolution: {integrity: sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ==} + compute-scroll-into-view@3.1.0: resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==} @@ -2238,6 +2832,12 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + cosmiconfig-typescript-loader@5.0.0: resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==} engines: {node: '>=v16'} @@ -2267,6 +2867,9 @@ packages: resolution: {integrity: sha512-gYTKKexFO3kh200H1Nit76sRwRtOY32vQd3jpAQKpLtZqyNsSQNfI4N7o3eP2wUjV35pTWKRYqFUDBvUha/Pkw==} engines: {node: '>=0.10.0'} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-fetch@3.1.8: resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} @@ -2311,6 +2914,23 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.33.1: + resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==} + engines: {node: '>=0.10'} + cz-conventional-changelog@3.3.0: resolution: {integrity: sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==} engines: {node: '>= 10'} @@ -2325,6 +2945,22 @@ packages: d3-array@2.12.1: resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + d3-cloud@1.2.7: resolution: {integrity: sha512-8TrgcgwRIpoZYQp7s3fGB7tATWfhckRb8KcVd1bOgqkNdkJRDGWfdSf4HkHHzZxSczwQJdSxvfPudwir5IAJ3w==} @@ -2344,25 +2980,62 @@ packages: d3-composite-projections@1.4.0: resolution: {integrity: sha512-csygyxdRfy7aUYRPea23veM6sjisdHI+DNd0nHcAGMd2LyL2lklr+xLRzHiJ+hy1HGp6YgAtbqdJR8CsLolrNQ==} + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + d3-dispatch@1.0.6: resolution: {integrity: sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==} + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + d3-dsv@1.2.0: resolution: {integrity: sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==} hasBin: true + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + d3-ease@1.0.7: resolution: {integrity: sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==} + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + d3-force@2.1.1: resolution: {integrity: sha512-nAuHEzBqMvpFVMf9OX75d00OxvOXdxY+xECIXjW6Gv8BRrXu6gAWbv/9XKrvfJ5i5DCokDW7RYE50LRoK092ew==} + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + d3-format@1.4.5: resolution: {integrity: sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==} d3-format@2.0.0: resolution: {integrity: sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==} + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + d3-geo-projection@2.1.2: resolution: {integrity: sha512-zft6RRvPaB1qplTodBVcSH5Ftvmvvg0qoDiqpt+fyNthGr/qr+DD30cizNDluXjW7jmo7EKUTjvFCAHofv08Ow==} hasBin: true @@ -2373,6 +3046,10 @@ packages: d3-geo@2.0.2: resolution: {integrity: sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA==} + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + d3-hexbin@0.2.2: resolution: {integrity: sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w==} @@ -2385,6 +3062,10 @@ packages: d3-hierarchy@2.0.0: resolution: {integrity: sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw==} + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + d3-interpolate@1.4.0: resolution: {integrity: sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==} @@ -2401,51 +3082,115 @@ packages: d3-path@2.0.0: resolution: {integrity: sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==} + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + d3-quadtree@2.0.0: resolution: {integrity: sha512-b0Ed2t1UUalJpc3qXzKi+cPGxeXRr4KU9YSlocN74aTzp6R/Ud43t79yLLqxHRWZfsvWXmbDWPpoENK1K539xw==} + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + d3-regression@1.3.10: resolution: {integrity: sha512-PF8GWEL70cHHWpx2jUQXc68r1pyPHIA+St16muk/XRokETzlegj5LriNKg7o4LR0TySug4nHYPJNNRz/W+/Niw==} + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + d3-sankey@0.9.1: resolution: {integrity: sha512-nnRkDaUMjBdeuGg+kWGdA+tjG1AVTnJ+Ykw7ff7CZHVI17Hm5sy8n0UXykVffn13aNHwK5wPOdOt1gS1ZEaF+A==} d3-scale-chromatic@1.5.0: resolution: {integrity: sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==} + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + d3-scale@2.2.2: resolution: {integrity: sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==} d3-scale@3.3.0: resolution: {integrity: sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==} + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + d3-selection@1.4.2: resolution: {integrity: sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==} + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + d3-shape@1.3.7: resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + d3-time-format@2.3.0: resolution: {integrity: sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==} d3-time-format@3.0.0: resolution: {integrity: sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==} + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + d3-time@1.1.0: resolution: {integrity: sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==} d3-time@2.1.1: resolution: {integrity: sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==} + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + d3-timer@1.0.10: resolution: {integrity: sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==} - d3-transition@1.3.2: + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@1.3.2: resolution: {integrity: sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==} + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + d3-voronoi@1.1.4: resolution: {integrity: sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==} + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.13: + resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} + dagre@0.8.5: resolution: {integrity: sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==} @@ -2465,9 +3210,16 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -2530,6 +3282,9 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2581,9 +3336,15 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dom-align@1.12.4: + resolution: {integrity: sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==} + dompurify@2.5.8: resolution: {integrity: sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + dot-prop@4.2.1: resolution: {integrity: sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==} engines: {node: '>=4'} @@ -2613,6 +3374,12 @@ packages: earcut@2.2.4: resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} + echarts-for-react@3.0.6: + resolution: {integrity: sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==} + peerDependencies: + echarts: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + react: ^15.0.0 || >=16.0.0 + echarts@5.5.1: resolution: {integrity: sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==} @@ -2648,6 +3415,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -3130,6 +3901,9 @@ packages: github-slugger@1.5.0: resolution: {integrity: sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==} + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + gl-matrix@3.4.3: resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==} @@ -3219,6 +3993,9 @@ packages: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + hammerjs@2.0.8: resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} engines: {node: '>=0.8.0'} @@ -3277,6 +4054,15 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-from-dom@5.0.1: + resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} + + hast-util-from-html-isomorphic@2.0.0: + resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + hast-util-from-parse5@7.1.2: resolution: {integrity: sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==} @@ -3289,6 +4075,9 @@ packages: hast-util-heading-rank@2.1.1: resolution: {integrity: sha512-iAuRp+ESgJoRFJbSyaqsfvJDY6zzmFoEnL1gtz1+U8gKtGGj1p0CVlysuUAUjq95qlZESHINLThwJzNGmgGZxA==} + hast-util-heading-rank@3.0.0: + resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==} + hast-util-is-element@2.1.3: resolution: {integrity: sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==} @@ -3310,15 +4099,24 @@ packages: hast-util-raw@9.1.0: resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + hast-util-sanitize@5.0.2: + resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} + hast-util-select@5.0.1: resolution: {integrity: sha512-cxnImmR/tN/ipvbwGrKtEErmy83K1xWx8Bu7nImiwTOJ7X/fW1X6L1241ux+MYUXDwx8GxrE4LVmXRlEnbQsQA==} hast-util-to-html@8.0.4: resolution: {integrity: sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + hast-util-to-jsx-runtime@2.3.2: resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + hast-util-to-parse5@7.1.0: resolution: {integrity: sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==} @@ -3331,6 +4129,9 @@ packages: hast-util-to-string@2.0.0: resolution: {integrity: sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==} + hast-util-to-string@3.0.1: + resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + hast-util-to-text@4.0.2: resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} @@ -3399,6 +4200,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -3450,6 +4255,9 @@ packages: inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + inquirer@7.3.3: resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} engines: {node: '>=8.0.0'} @@ -3783,6 +4591,9 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + iterator.prototype@1.1.2: resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} @@ -3821,6 +4632,13 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-compare@0.2.2: + resolution: {integrity: sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==} + + json-schema-merge-allof@0.8.1: + resolution: {integrity: sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==} + engines: {node: '>=12.0.0'} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -3859,6 +4677,10 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + jspdf@2.5.2: resolution: {integrity: sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==} @@ -3866,6 +4688,10 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + katex@0.16.33: + resolution: {integrity: sha512-q3N5u+1sY9Bu7T4nlXoiRBXWfwSefNGoKeOwekV+gw0cAXQlz2Ww6BLcmBxVDeXBMUDQv6fK5bcNaJLxob3ZQA==} + hasBin: true + kdbush@3.0.0: resolution: {integrity: sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==} @@ -3884,6 +4710,9 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + kind-of@3.2.2: resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} engines: {node: '>=0.10.0'} @@ -3903,6 +4732,10 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + langium@4.2.1: + resolution: {integrity: sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==} + engines: {node: '>=20.10.0', npm: '>=10.2.3'} + latest-version@3.1.0: resolution: {integrity: sha512-Be1YRHWWlZaSsrz2U+VInk+tO0EwLIyV+23RhWLINJYwg/UIikxjlj3MhH37/6/EDCAusjajvMkMMUXRaMWl/w==} engines: {node: '>=4'} @@ -3911,6 +4744,12 @@ packages: resolution: {integrity: sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==} engines: {node: '>=8'} + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + lazy-cache@1.0.4: resolution: {integrity: sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==} engines: {node: '>=0.10.0'} @@ -3919,6 +4758,11 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lib0@0.2.117: + resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==} + engines: {node: '>=16'} + hasBin: true + libnpx@10.2.4: resolution: {integrity: sha512-BPc0D1cOjBeS8VIBKUu5F80s6njm0wbVt7CsGMrIcJ+SI7pi7V0uVPGpEMH9H5L8csOcclTxAXFE2VAsJXUhfA==} engines: {node: '>=4'} @@ -3976,6 +4820,9 @@ packages: lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -4070,6 +4917,9 @@ packages: magic-string@0.30.11: resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-dir@1.3.0: resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==} engines: {node: '>=4'} @@ -4093,9 +4943,34 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + markdown-to-jsx@7.7.17: + resolution: {integrity: sha512-7mG/1feQ0TX5I7YyMZVDgCC/y2I3CiEhIRQIhyov9nGBP5eoVrOXXHuL5ZP8GRfxVZKRiXWJgwXkb9It+nQZfQ==} + engines: {node: '>= 10'} + peerDependencies: + react: '>= 0.14.0' + peerDependenciesMeta: + react: + optional: true + + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + + marked@17.0.3: + resolution: {integrity: sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A==} + engines: {node: '>= 20'} + hasBin: true + mdast-util-definitions@5.1.2: resolution: {integrity: sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==} + mdast-util-definitions@6.0.0: + resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} + + mdast-util-directive@3.1.0: + resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} + mdast-util-find-and-replace@2.2.2: resolution: {integrity: sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==} @@ -4141,6 +5016,9 @@ packages: mdast-util-gfm@3.0.0: resolution: {integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==} + mdast-util-math@3.0.0: + resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} + mdast-util-mdx-expression@2.0.1: resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} @@ -4203,12 +5081,18 @@ packages: merge@2.1.1: resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==} + mermaid@11.12.3: + resolution: {integrity: sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==} + micromark-core-commonmark@1.1.0: resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} micromark-core-commonmark@2.0.2: resolution: {integrity: sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==} + micromark-extension-directive@3.0.2: + resolution: {integrity: sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==} + micromark-extension-gfm-autolink-literal@1.0.5: resolution: {integrity: sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==} @@ -4248,6 +5132,9 @@ packages: micromark-extension-gfm@3.0.0: resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-extension-math@3.1.0: + resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} + micromark-factory-destination@1.1.0: resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} @@ -4430,6 +5317,9 @@ packages: mlly@1.7.2: resolution: {integrity: sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==} + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + mock-property@1.0.3: resolution: {integrity: sha512-2emPTb1reeLLYwHxyVx993iYyCHEiRRO+y8NFXFPL5kl5q14sgTK76cXyEKkeKCHeRw35SfdkUJ10Q1KfHuiIQ==} engines: {node: '>= 0.4'} @@ -4466,11 +5356,26 @@ packages: mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@4.0.2: + resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==} + engines: {node: ^14 || ^16 || >=18} + hasBin: true + + nanoid@5.1.6: + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} + engines: {node: ^18 || >=20} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -4578,6 +5483,12 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.4: + resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -4586,6 +5497,9 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + os-homedir@1.0.2: resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} engines: {node: '>=0.10.0'} @@ -4649,6 +5563,9 @@ packages: package-manager-detector@0.2.1: resolution: {integrity: sha512-/hVW2fZvAdEas+wyKh0SnlZ2mx0NIa1+j11YaQkogEJkcMErbwchHCuo8z7lEtajZJQZ6rgZNVTWMVVd71Bjng==} + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -4685,6 +5602,9 @@ packages: parse5@7.2.1: resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -4730,6 +5650,9 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pbf@3.3.0: resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} hasBin: true @@ -4746,6 +5669,9 @@ packages: picocolors@1.1.0: resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -4762,9 +5688,18 @@ packages: pkg-types@1.2.0: resolution: {integrity: sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + point-at-length@1.1.0: resolution: {integrity: sha512-nNHDk9rNEh/91o2Y8kHLzBLNpLf80RYd2gCun9ss+V0ytRSf6XhryBTx071fesktjbachRmGuUbId+JQmzhRXw==} + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + polygon-clipping@0.15.7: resolution: {integrity: sha512-nhfdr83ECBg6xtqOAJab1tbksbBAOMUltN60bU+llHVOL0e5Onm1WpAXXWXVB39L8AJFssoIhEVuy/S90MmotA==} @@ -4785,6 +5720,10 @@ packages: resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + potpack@1.0.2: resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} @@ -4834,6 +5773,68 @@ packages: property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + prosemirror-changeset@2.4.0: + resolution: {integrity: sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-drop-indicator@0.1.3: + resolution: {integrity: sha512-fJV6G2tHIVXZLUuc60fS9ly1/GuGOlAZUm67S1El+kGFUYh27Hyv6hcGx3rrJ+Q/JZL5jnyAibIZYYWpPqE45g==} + + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} + + prosemirror-gapcursor@1.4.0: + resolution: {integrity: sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-model@1.25.4: + resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} + + prosemirror-safari-ime-span@1.0.2: + resolution: {integrity: sha512-QJqD8s1zE/CuK56kDsUhndh5hiHh/gFnAuPOA9ytva2s85/ZEt2tNWeALTJN48DtWghSKOmiBsvVn2OlnJ5H2w==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-transform@1.11.0: + resolution: {integrity: sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==} + + prosemirror-view@1.41.6: + resolution: {integrity: sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==} + + prosemirror-virtual-cursor@0.4.2: + resolution: {integrity: sha512-pUMKnIuOhhnMcgIJUjhIQTVJruBEGxfMBVQSrK0g2qhGPDm1i12KdsVaFw15dYk+29tZcxjMeR7P5VDKwmbwJg==} + peerDependencies: + prosemirror-model: ^1.0.0 + prosemirror-state: ^1.0.0 + prosemirror-view: ^1.0.0 + peerDependenciesMeta: + prosemirror-model: + optional: true + prosemirror-state: + optional: true + prosemirror-view: + optional: true + protocol-buffers-schema@3.6.0: resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} @@ -4869,6 +5870,12 @@ packages: raf@3.4.1: resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + rc-align@4.0.15: + resolution: {integrity: sha512-wqJtVH60pka/nOX7/IspElA8gjPNQKIx/ZqJ6heATCkXpe1Zg4cPVrMD2vC96wjsFFL8WsmhPbx9tdMo1qqlIA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + rc-cascader@3.28.1: resolution: {integrity: sha512-9+8oHIMWVLHxuaapDiqFNmD9KSyKN/P4bo9x/MBuDbyTqP8f2/POmmZxdXWBO3yq/uE3pKyQCXYNUxrNfHRv2A==} peerDependencies: @@ -4967,6 +5974,13 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' + rc-picker@2.7.6: + resolution: {integrity: sha512-H9if/BUJUZBOhPfWcPeT15JUI3/ntrG9muzERrXDkSoWmDj4yzmBvumozpxYrHwjcKnjyDGAke68d+whWwvhHA==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + rc-picker@4.6.15: resolution: {integrity: sha512-OWZ1yrMie+KN2uEUfYCfS4b2Vu6RC1FWwNI0s+qypsc3wRt7g+peuZKVIzXCTaJwyyZruo80+akPg2+GmyiJjw==} engines: {node: '>=8.x'} @@ -5084,6 +6098,13 @@ packages: react: '*' react-dom: '*' + rc-trigger@5.3.4: + resolution: {integrity: sha512-mQv+vas0TwKcjAO2izNPkqR4j86OemLRmvL2nOzdP9OWNWA1ivoTt5hzFqYNW9zACwmTezRiN8bttrC7cZzYSw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + rc-upload@4.8.1: resolution: {integrity: sha512-toEAhwl4hjLAI1u8/CgKWt30BR06ulPa4iGQSMvSXoHzO88gPCslxqV/mnn4gJU7PDoltGIC9Eh+wkeudqgHyw==} peerDependencies: @@ -5300,6 +6321,12 @@ packages: react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-virtuoso@4.14.1: + resolution: {integrity: sha512-NRUF1ak8lY+Tvc6WN9cce59gU+lilzVtOozP+pm9J7iHshLGGjsiAB4rB2qlBPHjFbcXOQpT+7womNHGDUql8w==} + peerDependencies: + react: '>=16 || >=17 || >= 18 || >= 19' + react-dom: '>=16 || >=17 || >= 18 || >=19' + react-window@1.8.11: resolution: {integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==} engines: {node: '>8.0.0'} @@ -5359,6 +6386,15 @@ packages: regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + regexp.prototype.flags@1.5.3: resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} engines: {node: '>= 0.4'} @@ -5387,9 +6423,15 @@ packages: rehype-autolink-headings@6.1.0: resolution: {integrity: sha512-v8xnvhDtxc2jTgnhVlR2sG168TyNTK93Myx+uTzQNpNxzkRnQKz3hZQKoh7fxuOzJhCGa/z6qqZSPB2BOi/HRw==} + rehype-harden@1.1.8: + resolution: {integrity: sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw==} + rehype-highlight@7.0.1: resolution: {integrity: sha512-dB/vVGFsbm7xPglqnYbg0ABg6rAuIWKycTvuXaOO27SgLoOFNoTlniTBtAxp3n5ZyMioW1a3KwiNqgjkb6Skjg==} + rehype-katex@7.0.1: + resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} + rehype-parse@8.0.5: resolution: {integrity: sha512-Ds3RglaY/+clEX2U2mHflt7NlMA72KspZ0JLUJgBBLpRddBcEw3H8uYZQliQriku22NZpYMfjDdSgHcjxue24A==} @@ -5403,21 +6445,39 @@ packages: resolution: {integrity: sha512-JAuCiYaBtcQ3ZGQa6fh1MeDCMXx2HbYqmQ2gTdjo4zNPjEdigo8cbk+AxFMo36OfEUluBQozvNzMWcvfeIVAQg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} + rehype-slug@5.0.0: resolution: {integrity: sha512-jnYsFKxRh+/tQa1L+SU/ykAPGOSVCqd0BwaOBPUANcvCu8d0/SZB4IalJkdJ+n6d1eAAS2YkvjUPi+2EGYtfCQ==} + rehype-slug@6.0.0: + resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} + rehype-stringify@9.0.4: resolution: {integrity: sha512-Uk5xu1YKdqobe5XpSskwPvo1XeHUUucWEQSl8hTrXt5selvca1e8K1EZ37E6YoZ4BT8BCqCdVfQW7OfHfthtVQ==} rehype@12.0.0: resolution: {integrity: sha512-gZcttmf9R5IYHb8AlI1rlmWqXS1yX0rSB/S5ZGJs8atfYZy2DobvH3Ic/gSzB+HL/+oOHPtBguw1TprfhxXBgQ==} + remark-directive@3.0.1: + resolution: {integrity: sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==} + remark-gfm@2.0.0: resolution: {integrity: sha512-waIv4Tjcd2CTUDxKRYzuPyIHw1FoX4H2GjXAzXV9PxQWb+dU4fJivd/FZ+nxyzPARrqTjMIkwIwPoWNbpBhjcQ==} remark-gfm@4.0.0: resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-inline-links@7.0.0: + resolution: {integrity: sha512-4uj1pPM+F495ySZhTIB6ay2oSkTsKgmYaKk/q5HIdhX2fuyLEegpjWa0VdJRJ01sgOqAFo7MBKdDUejIYBMVMQ==} + + remark-math@6.0.0: + resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} + remark-parse@10.0.2: resolution: {integrity: sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==} @@ -5427,12 +6487,21 @@ packages: remark-rehype@11.1.1: resolution: {integrity: sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==} + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + remark-rehype@9.1.0: resolution: {integrity: sha512-oLa6YmgAYg19zb0ZrBACh40hpBLteYROaPLhBXzLgjqyHQrN+gVP9N/FJvfzuNNuzCutktkroXEZBrxAxKhh7Q==} remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + remark@15.0.1: + resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + + remend@1.2.1: + resolution: {integrity: sha512-4wC12bgXsfKAjF1ewwkNIQz5sqewz/z1xgIgjEMb3r1pEytQ37F0Cm6i+OhbTWEvguJD7lhOUJhK5fSasw9f0w==} + remote-origin-url@1.0.0: resolution: {integrity: sha512-xHDM6IBqivpiQ1e4WOuFpM/T6rbzA/WBsu+3WLtgPOhHyjA0nYlijV3NprlTb4FcXlQ5+Q+z174sQ1NnUF5FwA==} engines: {node: '>= 0.8.0'} @@ -5526,6 +6595,12 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -5657,6 +6732,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shiki@3.23.0: + resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} + side-channel@1.0.6: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} @@ -5765,6 +6843,12 @@ packages: resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} engines: {node: '>=0.1.14'} + streamdown@2.3.0: + resolution: {integrity: sha512-OqS3by/lt91lSicE8RQP2nTsYI6Q/dQgGP2vcyn9YesCmRHhNjswAuBAZA1z0F4+oBU3II/eV51LqjCqwTb1lw==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -5856,9 +6940,18 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + style-to-object@0.3.0: resolution: {integrity: sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==} + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + style-to-object@1.0.8: resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} @@ -5879,6 +6972,9 @@ packages: stylis@4.3.4: resolution: {integrity: sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==} + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + supercluster@7.1.5: resolution: {integrity: sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==} @@ -5914,6 +7010,9 @@ packages: resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} engines: {node: ^14.18.0 || >=16.0.0} + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + tape@4.17.0: resolution: {integrity: sha512-KCuXjYxCZ3ru40dmND+oCLsXyuA8hoseu2SS404Px5ouyS0A99v8X/mdiLqsR5MTAyamMBN7PRwt2Dv3+xGIxw==} hasBin: true @@ -5961,6 +7060,10 @@ packages: tinyexec@0.3.0: resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + tinyqueue@2.0.3: resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} @@ -6009,6 +7112,10 @@ packages: peerDependencies: typescript: '>=4.2.0' + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + ts-pattern@4.0.4: resolution: {integrity: sha512-gDKHf3T+4X2w5k5IpD4FQ6P5b0LRYW7sJdf/yG7uAdxKitgk2Z8UZLdSBjrXs5mI0Vq6E/hRQd6+Kh3FbdjBzw==} @@ -6088,6 +7195,9 @@ packages: ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + uglify-js@2.8.29: resolution: {integrity: sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==} engines: {node: '>=0.8.0'} @@ -6162,6 +7272,9 @@ packages: unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + unist-util-stringify-position@3.0.3: resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} @@ -6301,6 +7414,21 @@ packages: validate-npm-package-name@3.0.0: resolution: {integrity: sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==} + validate.io-array@1.0.6: + resolution: {integrity: sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==} + + validate.io-function@1.0.2: + resolution: {integrity: sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==} + + validate.io-integer-array@1.0.0: + resolution: {integrity: sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA==} + + validate.io-integer@1.0.5: + resolution: {integrity: sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ==} + + validate.io-number@1.0.3: + resolution: {integrity: sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg==} + vfile-location@4.1.0: resolution: {integrity: sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==} @@ -6354,9 +7482,40 @@ packages: terser: optional: true + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vt-pbf@3.1.3: resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==} + vue@3.5.29: + resolution: {integrity: sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} @@ -6473,6 +7632,22 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y-prosemirror@1.3.7: + resolution: {integrity: sha512-NpM99WSdD4Fx4if5xOMDpPtU3oAmTSjlzh5U4353ABbRHl1HtAFUx6HlebLZfyFxXN9jzKMDkVbcRjqOZVkYQg==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + prosemirror-model: ^1.7.1 + prosemirror-state: ^1.2.3 + prosemirror-view: ^1.9.10 + y-protocols: ^1.0.1 + yjs: ^13.5.38 + + y-protocols@1.0.7: + resolution: {integrity: sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + yjs: ^13.0.0 + y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} @@ -6508,6 +7683,10 @@ packages: yargs@3.10.0: resolution: {integrity: sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A==} + yjs@13.6.29: + resolution: {integrity: sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -6590,14 +7769,14 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@ant-design/pro-card@2.9.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@ant-design/pro-card@2.9.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@ant-design/cssinjs': 1.21.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@ant-design/icons': 5.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-provider': 2.15.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-utils': 2.16.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-provider': 2.15.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-utils': 2.16.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@babel/runtime': 7.25.7 - antd: 5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + antd: 5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 omit.js: 2.0.2 rc-resize-observer: 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -6606,34 +7785,34 @@ snapshots: transitivePeerDependencies: - react-dom - '@ant-design/pro-components@2.8.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@ant-design/pro-card': 2.9.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-descriptions': 2.6.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-field': 2.17.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-form': 2.31.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-layout': 7.21.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-list': 2.6.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-provider': 2.15.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-skeleton': 2.2.1(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-table': 3.18.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-utils': 2.16.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-components@2.8.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@ant-design/pro-card': 2.9.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-descriptions': 2.6.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-field': 2.17.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-form': 2.31.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-layout': 7.21.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-list': 2.6.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-provider': 2.15.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-skeleton': 2.2.1(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-table': 3.18.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-utils': 2.16.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@babel/runtime': 7.25.7 - antd: 5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + antd: 5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: - rc-field-form - '@ant-design/pro-descriptions@2.6.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@ant-design/pro-descriptions@2.6.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@ant-design/pro-field': 2.17.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-form': 2.31.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-provider': 2.15.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-skeleton': 2.2.1(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-utils': 2.16.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-field': 2.17.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-form': 2.31.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-provider': 2.15.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-skeleton': 2.2.1(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-utils': 2.16.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@babel/runtime': 7.25.7 - antd: 5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + antd: 5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-resize-observer: 0.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -6641,14 +7820,14 @@ snapshots: - rc-field-form - react-dom - '@ant-design/pro-field@2.17.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@ant-design/pro-field@2.17.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@ant-design/icons': 5.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-provider': 2.15.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-utils': 2.16.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-provider': 2.15.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-utils': 2.16.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@babel/runtime': 7.25.7 '@chenshuai2144/sketch-color': 1.0.9(react@18.3.1) - antd: 5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + antd: 5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 dayjs: 1.11.13 lodash: 4.17.21 @@ -6660,16 +7839,16 @@ snapshots: transitivePeerDependencies: - react-dom - '@ant-design/pro-form@2.31.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@ant-design/pro-form@2.31.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@ant-design/icons': 5.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-field': 2.17.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-provider': 2.15.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-utils': 2.16.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-field': 2.17.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-provider': 2.15.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-utils': 2.16.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@babel/runtime': 7.25.7 '@chenshuai2144/sketch-color': 1.0.9(react@18.3.1) '@umijs/use-params': 1.0.9(react@18.3.1) - antd: 5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + antd: 5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 dayjs: 1.11.13 lodash: 4.17.21 @@ -6681,16 +7860,16 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@ant-design/pro-layout@7.21.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@ant-design/pro-layout@7.21.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@ant-design/cssinjs': 1.21.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@ant-design/icons': 5.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-provider': 2.15.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-utils': 2.16.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-provider': 2.15.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-utils': 2.16.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@babel/runtime': 7.25.7 '@umijs/route-utils': 4.0.1 '@umijs/use-params': 1.0.9(react@18.3.1) - antd: 5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + antd: 5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 lodash: 4.17.21 lodash-es: 4.17.21 @@ -6703,16 +7882,16 @@ snapshots: swr: 2.2.5(react@18.3.1) warning: 4.0.3 - '@ant-design/pro-list@2.6.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@ant-design/pro-list@2.6.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@ant-design/cssinjs': 1.21.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@ant-design/icons': 5.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-card': 2.9.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-field': 2.17.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-table': 3.18.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-utils': 2.16.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-card': 2.9.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-field': 2.17.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-table': 3.18.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-utils': 2.16.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@babel/runtime': 7.25.7 - antd: 5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + antd: 5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 dayjs: 1.11.13 rc-resize-observer: 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -6722,40 +7901,40 @@ snapshots: transitivePeerDependencies: - rc-field-form - '@ant-design/pro-provider@2.15.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@ant-design/pro-provider@2.15.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@ant-design/cssinjs': 1.21.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@babel/runtime': 7.25.7 '@ctrl/tinycolor': 3.6.1 - antd: 5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + antd: 5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) dayjs: 1.11.13 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) swr: 2.2.5(react@18.3.1) - '@ant-design/pro-skeleton@2.2.1(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@ant-design/pro-skeleton@2.2.1(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.7 - antd: 5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + antd: 5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@ant-design/pro-table@3.18.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@ant-design/pro-table@3.18.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@ant-design/cssinjs': 1.21.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@ant-design/icons': 5.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-card': 2.9.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-field': 2.17.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-form': 2.31.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-provider': 2.15.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-utils': 2.16.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-card': 2.9.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-field': 2.17.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-form': 2.31.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-provider': 2.15.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-utils': 2.16.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@babel/runtime': 7.25.7 '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@dnd-kit/modifiers': 6.0.1(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@dnd-kit/sortable': 7.0.2(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@dnd-kit/utilities': 3.2.2(react@18.3.1) - antd: 5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + antd: 5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 dayjs: 1.11.13 lodash: 4.17.21 @@ -6767,12 +7946,12 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@ant-design/pro-utils@2.16.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@ant-design/pro-utils@2.16.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@ant-design/icons': 5.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ant-design/pro-provider': 2.15.2(antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ant-design/pro-provider': 2.15.2(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@babel/runtime': 7.25.7 - antd: 5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + antd: 5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 dayjs: 1.11.13 lodash: 4.17.21 @@ -6797,6 +7976,11 @@ snapshots: package-manager-detector: 0.2.1 tinyexec: 0.3.0 + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 + '@antfu/utils@0.7.10': {} '@antv/adjust@0.2.5': @@ -7362,10 +8546,14 @@ snapshots: '@babel/helper-string-parser@7.25.9': {} + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-validator-identifier@7.25.7': {} '@babel/helper-validator-identifier@7.25.9': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.25.7': {} '@babel/helpers@7.25.7': @@ -7384,6 +8572,10 @@ snapshots: dependencies: '@babel/types': 7.26.0 + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + '@babel/plugin-syntax-jsx@7.25.7(@babel/core@7.25.7)': dependencies: '@babel/core': 7.25.7 @@ -7490,12 +8682,291 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@braintree/sanitize-url@7.1.2': {} + '@chenshuai2144/sketch-color@1.0.9(react@18.3.1)': dependencies: react: 18.3.1 reactcss: 1.2.3(react@18.3.1) tinycolor2: 1.6.0 + '@chevrotain/cst-dts-gen@11.1.2': + dependencies: + '@chevrotain/gast': 11.1.2 + '@chevrotain/types': 11.1.2 + lodash-es: 4.17.23 + + '@chevrotain/gast@11.1.2': + dependencies: + '@chevrotain/types': 11.1.2 + lodash-es: 4.17.23 + + '@chevrotain/regexp-to-ast@11.1.2': {} + + '@chevrotain/types@11.1.2': {} + + '@chevrotain/utils@11.1.2': {} + + '@codemirror/autocomplete@6.20.1': + dependencies: + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + + '@codemirror/commands@6.10.2': + dependencies: + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + + '@codemirror/lang-angular@0.1.4': + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.2 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-cpp@6.0.3': + dependencies: + '@codemirror/language': 6.12.2 + '@lezer/cpp': 1.1.5 + + '@codemirror/lang-css@6.3.1': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/css': 1.3.1 + + '@codemirror/lang-go@6.0.1': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/go': 1.0.1 + + '@codemirror/lang-html@6.4.11': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/css': 1.3.1 + '@lezer/html': 1.3.13 + + '@codemirror/lang-java@6.0.2': + dependencies: + '@codemirror/language': 6.12.2 + '@lezer/java': 1.1.3 + + '@codemirror/lang-javascript@6.2.5': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/lint': 6.9.5 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/javascript': 1.5.4 + + '@codemirror/lang-jinja@6.0.0': + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.2 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.2 + '@lezer/json': 1.0.3 + + '@codemirror/lang-less@6.0.2': + dependencies: + '@codemirror/lang-css': 6.3.1 + '@codemirror/language': 6.12.2 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-liquid@6.3.2': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-markdown@6.5.0': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/markdown': 1.6.3 + + '@codemirror/lang-php@6.0.2': + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/php': 1.0.5 + + '@codemirror/lang-python@6.2.1': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/python': 1.1.18 + + '@codemirror/lang-rust@6.0.2': + dependencies: + '@codemirror/language': 6.12.2 + '@lezer/rust': 1.0.2 + + '@codemirror/lang-sass@6.0.2': + dependencies: + '@codemirror/lang-css': 6.3.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/sass': 1.1.0 + + '@codemirror/lang-sql@6.10.0': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-vue@0.1.3': + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.2 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-wast@6.0.2': + dependencies: + '@codemirror/language': 6.12.2 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-xml@6.1.0': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/xml': 1.0.6 + + '@codemirror/lang-yaml@6.1.2': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + '@lezer/yaml': 1.0.4 + + '@codemirror/language-data@6.5.2': + dependencies: + '@codemirror/lang-angular': 0.1.4 + '@codemirror/lang-cpp': 6.0.3 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-go': 6.0.1 + '@codemirror/lang-html': 6.4.11 + '@codemirror/lang-java': 6.0.2 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/lang-jinja': 6.0.0 + '@codemirror/lang-json': 6.0.2 + '@codemirror/lang-less': 6.0.2 + '@codemirror/lang-liquid': 6.3.2 + '@codemirror/lang-markdown': 6.5.0 + '@codemirror/lang-php': 6.0.2 + '@codemirror/lang-python': 6.2.1 + '@codemirror/lang-rust': 6.0.2 + '@codemirror/lang-sass': 6.0.2 + '@codemirror/lang-sql': 6.10.0 + '@codemirror/lang-vue': 0.1.3 + '@codemirror/lang-wast': 6.0.2 + '@codemirror/lang-xml': 6.1.0 + '@codemirror/lang-yaml': 6.1.2 + '@codemirror/language': 6.12.2 + '@codemirror/legacy-modes': 6.5.2 + + '@codemirror/language@6.12.2': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + style-mod: 4.1.3 + + '@codemirror/legacy-modes@6.5.2': + dependencies: + '@codemirror/language': 6.12.2 + + '@codemirror/lint@6.9.5': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + crelt: 1.0.6 + + '@codemirror/search@6.6.0': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + crelt: 1.0.6 + + '@codemirror/state@6.5.4': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/theme-one-dark@6.1.3': + dependencies: + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/highlight': 1.2.3 + + '@codemirror/view@6.39.16': + dependencies: + '@codemirror/state': 6.5.4 + crelt: 1.0.6 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@commitlint/cli@19.6.1(@types/node@22.13.14)(typescript@5.6.2)': dependencies: '@commitlint/format': 19.5.0 @@ -7770,6 +9241,17 @@ snapshots: '@eslint/js@8.57.1': {} + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + '@hello-pangea/dnd@16.5.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.7 @@ -7812,6 +9294,12 @@ snapshots: transitivePeerDependencies: - supports-color + '@iconify/utils@3.1.0': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + mlly: 1.8.0 + '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -7830,6 +9318,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -7837,6 +9327,99 @@ snapshots: '@juggle/resize-observer@3.4.0': {} + '@lezer/common@1.5.1': {} + + '@lezer/cpp@1.1.5': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/css@1.3.1': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/go@1.0.1': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.1 + + '@lezer/html@1.3.13': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/java@1.1.3': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/lr@1.4.8': + dependencies: + '@lezer/common': 1.5.1 + + '@lezer/markdown@1.6.3': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + + '@lezer/php@1.0.5': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/python@1.1.18': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/rust@1.0.2': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/sass@1.1.0': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/xml@1.0.6': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/yaml@1.0.4': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + '@ljharb/resumer@0.0.1': dependencies: '@ljharb/through': 2.3.13 @@ -7892,6 +9475,307 @@ snapshots: rw: 1.3.3 sort-object: 3.0.3 + '@marijn/find-cluster-break@1.0.2': {} + + '@mermaid-js/parser@1.0.0': + dependencies: + langium: 4.2.1 + + '@milkdown/components@7.19.0(@codemirror/language@6.12.2)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(typescript@5.6.2)': + dependencies: + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@floating-ui/dom': 1.7.6 + '@milkdown/core': 7.19.0 + '@milkdown/ctx': 7.19.0 + '@milkdown/exception': 7.19.0 + '@milkdown/plugin-tooltip': 7.19.0 + '@milkdown/preset-commonmark': 7.19.0 + '@milkdown/preset-gfm': 7.19.0 + '@milkdown/prose': 7.19.0 + '@milkdown/transformer': 7.19.0 + '@milkdown/utils': 7.19.0 + '@types/lodash-es': 4.17.12 + clsx: 2.1.1 + dompurify: 3.3.1 + lodash-es: 4.17.21 + nanoid: 5.1.6 + unist-util-visit: 5.0.0 + vue: 3.5.29(typescript@5.6.2) + transitivePeerDependencies: + - supports-color + - typescript + + '@milkdown/core@7.19.0': + dependencies: + '@milkdown/ctx': 7.19.0 + '@milkdown/exception': 7.19.0 + '@milkdown/prose': 7.19.0 + '@milkdown/transformer': 7.19.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + '@milkdown/crepe@7.19.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(typescript@5.6.2)': + dependencies: + '@codemirror/commands': 6.10.2 + '@codemirror/language': 6.12.2 + '@codemirror/language-data': 6.5.2 + '@codemirror/state': 6.5.4 + '@codemirror/theme-one-dark': 6.1.3 + '@codemirror/view': 6.39.16 + '@milkdown/kit': 7.19.0(@codemirror/language@6.12.2)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(typescript@5.6.2) + '@types/lodash-es': 4.17.12 + clsx: 2.1.1 + codemirror: 6.0.2 + katex: 0.16.33 + lodash-es: 4.17.21 + prosemirror-virtual-cursor: 0.4.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6) + remark-math: 6.0.0 + unist-util-visit: 5.0.0 + vue: 3.5.29(typescript@5.6.2) + transitivePeerDependencies: + - prosemirror-model + - prosemirror-state + - prosemirror-view + - supports-color + - typescript + + '@milkdown/ctx@7.19.0': + dependencies: + '@milkdown/exception': 7.19.0 + + '@milkdown/exception@7.19.0': {} + + '@milkdown/kit@7.19.0(@codemirror/language@6.12.2)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(typescript@5.6.2)': + dependencies: + '@milkdown/components': 7.19.0(@codemirror/language@6.12.2)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(typescript@5.6.2) + '@milkdown/core': 7.19.0 + '@milkdown/ctx': 7.19.0 + '@milkdown/plugin-block': 7.19.0 + '@milkdown/plugin-clipboard': 7.19.0 + '@milkdown/plugin-cursor': 7.19.0 + '@milkdown/plugin-history': 7.19.0 + '@milkdown/plugin-indent': 7.19.0 + '@milkdown/plugin-listener': 7.19.0 + '@milkdown/plugin-slash': 7.19.0 + '@milkdown/plugin-tooltip': 7.19.0 + '@milkdown/plugin-trailing': 7.19.0 + '@milkdown/plugin-upload': 7.19.0 + '@milkdown/preset-commonmark': 7.19.0 + '@milkdown/preset-gfm': 7.19.0 + '@milkdown/prose': 7.19.0 + '@milkdown/transformer': 7.19.0 + '@milkdown/utils': 7.19.0 + transitivePeerDependencies: + - '@codemirror/language' + - '@codemirror/state' + - '@codemirror/view' + - supports-color + - typescript + + '@milkdown/plugin-block@7.19.0': + dependencies: + '@floating-ui/dom': 1.7.6 + '@milkdown/core': 7.19.0 + '@milkdown/ctx': 7.19.0 + '@milkdown/prose': 7.19.0 + '@milkdown/utils': 7.19.0 + '@types/lodash-es': 4.17.12 + lodash-es: 4.17.21 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-clipboard@7.19.0': + dependencies: + '@milkdown/core': 7.19.0 + '@milkdown/ctx': 7.19.0 + '@milkdown/prose': 7.19.0 + '@milkdown/utils': 7.19.0 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-collab@7.19.0(y-prosemirror@1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29)': + dependencies: + '@milkdown/core': 7.19.0 + '@milkdown/ctx': 7.19.0 + '@milkdown/exception': 7.19.0 + '@milkdown/prose': 7.19.0 + y-prosemirror: 1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29) + y-protocols: 1.0.7(yjs@13.6.29) + yjs: 13.6.29 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-cursor@7.19.0': + dependencies: + '@milkdown/ctx': 7.19.0 + '@milkdown/prose': 7.19.0 + '@milkdown/utils': 7.19.0 + prosemirror-drop-indicator: 0.1.3 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-history@7.19.0': + dependencies: + '@milkdown/core': 7.19.0 + '@milkdown/ctx': 7.19.0 + '@milkdown/prose': 7.19.0 + '@milkdown/utils': 7.19.0 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-indent@7.19.0': + dependencies: + '@milkdown/ctx': 7.19.0 + '@milkdown/prose': 7.19.0 + '@milkdown/utils': 7.19.0 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-listener@7.19.0': + dependencies: + '@milkdown/core': 7.19.0 + '@milkdown/ctx': 7.19.0 + '@milkdown/prose': 7.19.0 + '@types/lodash-es': 4.17.12 + lodash-es: 4.17.21 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-slash@7.19.0': + dependencies: + '@floating-ui/dom': 1.7.6 + '@milkdown/ctx': 7.19.0 + '@milkdown/prose': 7.19.0 + '@milkdown/utils': 7.19.0 + '@types/lodash-es': 4.17.12 + lodash-es: 4.17.21 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-tooltip@7.19.0': + dependencies: + '@floating-ui/dom': 1.7.6 + '@milkdown/ctx': 7.19.0 + '@milkdown/prose': 7.19.0 + '@milkdown/utils': 7.19.0 + '@types/lodash-es': 4.17.12 + lodash-es: 4.17.21 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-trailing@7.19.0': + dependencies: + '@milkdown/ctx': 7.19.0 + '@milkdown/prose': 7.19.0 + '@milkdown/utils': 7.19.0 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-upload@7.19.0': + dependencies: + '@milkdown/core': 7.19.0 + '@milkdown/ctx': 7.19.0 + '@milkdown/exception': 7.19.0 + '@milkdown/prose': 7.19.0 + '@milkdown/utils': 7.19.0 + transitivePeerDependencies: + - supports-color + + '@milkdown/preset-commonmark@7.19.0': + dependencies: + '@milkdown/core': 7.19.0 + '@milkdown/ctx': 7.19.0 + '@milkdown/exception': 7.19.0 + '@milkdown/prose': 7.19.0 + '@milkdown/transformer': 7.19.0 + '@milkdown/utils': 7.19.0 + remark-inline-links: 7.0.0 + unist-util-visit: 5.0.0 + unist-util-visit-parents: 6.0.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/preset-gfm@7.19.0': + dependencies: + '@milkdown/core': 7.19.0 + '@milkdown/ctx': 7.19.0 + '@milkdown/exception': 7.19.0 + '@milkdown/preset-commonmark': 7.19.0 + '@milkdown/prose': 7.19.0 + '@milkdown/transformer': 7.19.0 + '@milkdown/utils': 7.19.0 + prosemirror-safari-ime-span: 1.0.2 + remark-gfm: 4.0.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/prose@7.19.0': + dependencies: + '@milkdown/exception': 7.19.0 + prosemirror-changeset: 2.4.0 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.0 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.6 + + '@milkdown/react@7.19.0(@codemirror/language@6.12.2)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2)': + dependencies: + '@milkdown/crepe': 7.19.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(typescript@5.6.2) + '@milkdown/kit': 7.19.0(@codemirror/language@6.12.2)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(typescript@5.6.2) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@codemirror/language' + - '@codemirror/state' + - '@codemirror/view' + - prosemirror-model + - prosemirror-state + - prosemirror-view + - supports-color + - typescript + + '@milkdown/theme-nord@7.19.0': + dependencies: + '@milkdown/core': 7.19.0 + '@milkdown/ctx': 7.19.0 + '@milkdown/prose': 7.19.0 + clsx: 2.1.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/transformer@7.19.0': + dependencies: + '@milkdown/exception': 7.19.0 + '@milkdown/prose': 7.19.0 + remark: 15.0.1 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + '@milkdown/utils@7.19.0': + dependencies: + '@milkdown/core': 7.19.0 + '@milkdown/ctx': 7.19.0 + '@milkdown/exception': 7.19.0 + '@milkdown/prose': 7.19.0 + '@milkdown/transformer': 7.19.0 + nanoid: 5.1.6 + transitivePeerDependencies: + - supports-color + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7904,6 +9788,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@ocavue/utils@1.5.0': {} + '@pkgr/core@0.1.1': {} '@polka/url@1.0.0-next.28': {} @@ -7923,6 +9809,18 @@ snapshots: dependencies: '@babel/runtime': 7.25.7 + '@prosemirror-adapter/core@0.2.6': + dependencies: + tslib: 2.7.0 + + '@prosemirror-adapter/react@0.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@prosemirror-adapter/core': 0.2.6 + nanoid: 4.0.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.7.0 + '@rc-component/async-validator@5.0.4': dependencies: '@babel/runtime': 7.25.7 @@ -8000,6 +9898,47 @@ snapshots: '@remix-run/router@1.19.2': {} + '@rjsf/antd@5.24.13(@ant-design/icons@5.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@rjsf/core@5.24.13(@rjsf/utils@5.24.13(react@18.3.1))(react@18.3.1))(@rjsf/utils@5.24.13(react@18.3.1))(antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dayjs@1.11.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@ant-design/icons': 5.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@rjsf/core': 5.24.13(@rjsf/utils@5.24.13(react@18.3.1))(react@18.3.1) + '@rjsf/utils': 5.24.13(react@18.3.1) + antd: 5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + dayjs: 1.11.13 + lodash: 4.17.21 + lodash-es: 4.17.23 + rc-picker: 2.7.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@rjsf/core@5.24.13(@rjsf/utils@5.24.13(react@18.3.1))(react@18.3.1)': + dependencies: + '@rjsf/utils': 5.24.13(react@18.3.1) + lodash: 4.17.21 + lodash-es: 4.17.23 + markdown-to-jsx: 7.7.17(react@18.3.1) + prop-types: 15.8.1 + react: 18.3.1 + + '@rjsf/utils@5.24.13(react@18.3.1)': + dependencies: + json-schema-merge-allof: 0.8.1 + jsonpointer: 5.0.1 + lodash: 4.17.21 + lodash-es: 4.17.23 + react: 18.3.1 + react-is: 18.3.1 + + '@rjsf/validator-ajv8@5.24.13(@rjsf/utils@5.24.13(react@18.3.1))': + dependencies: + '@rjsf/utils': 5.24.13(react@18.3.1) + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + lodash: 4.17.21 + lodash-es: 4.17.23 + '@rollup/pluginutils@5.1.2(rollup@4.24.0)': dependencies: '@types/estree': 1.0.6 @@ -8056,11 +9995,70 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.24.0': optional: true + '@shikijs/core@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.4 + + '@shikijs/engine-oniguruma@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/themes@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/types@3.23.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/is@0.14.0': {} - '@szmarczak/http-timer@1.1.2': + '@streamdown/code@1.0.3(react@18.3.1)': + dependencies: + react: 18.3.1 + shiki: 3.23.0 + + '@streamdown/math@1.0.2(react@18.3.1)': + dependencies: + katex: 0.16.33 + react: 18.3.1 + rehype-katex: 7.0.1 + remark-math: 6.0.0 + transitivePeerDependencies: + - supports-color + + '@streamdown/mermaid@1.0.2(react@18.3.1)': + dependencies: + mermaid: 11.12.3 + react: 18.3.1 + + '@szmarczak/http-timer@1.1.2': + dependencies: + defer-to-connect: 1.1.3 + + '@tanstack/query-core@5.95.0': {} + + '@tanstack/react-query@5.95.0(react@18.3.1)': dependencies: - defer-to-connect: 1.1.3 + '@tanstack/query-core': 5.95.0 + react: 18.3.1 '@turf/bbox-polygon@6.5.0': dependencies: @@ -8121,8 +10119,123 @@ snapshots: dependencies: '@types/node': 22.13.14 + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.14 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.14 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + '@types/d3-timer@2.0.3': {} + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 2.0.3 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -8161,10 +10274,16 @@ snapshots: '@types/json5@0.0.29': {} + '@types/katex@0.16.8': {} + '@types/keyv@3.1.4': dependencies: '@types/node': 22.13.14 + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.10 + '@types/lodash@4.17.10': {} '@types/mapbox__point-geometry@0.1.4': {} @@ -8223,6 +10342,9 @@ snapshots: dependencies: '@types/geojson': 7946.0.14 + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -8391,7 +10513,7 @@ snapshots: gzip-size: 6.0.0 sirv: 2.0.4 - '@unocss/postcss@0.58.9(postcss@8.4.47)': + '@unocss/postcss@0.58.9(postcss@8.5.8)': dependencies: '@unocss/config': 0.58.9 '@unocss/core': 0.58.9 @@ -8399,7 +10521,7 @@ snapshots: css-tree: 2.3.1 fast-glob: 3.3.2 magic-string: 0.30.11 - postcss: 8.4.47 + postcss: 8.5.8 '@unocss/preset-attributify@0.58.9': dependencies: @@ -8511,6 +10633,60 @@ snapshots: '@viz-js/viz@3.7.0': {} + '@vue/compiler-core@3.5.29': + dependencies: + '@babel/parser': 7.29.0 + '@vue/shared': 3.5.29 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.29': + dependencies: + '@vue/compiler-core': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/compiler-sfc@3.5.29': + dependencies: + '@babel/parser': 7.29.0 + '@vue/compiler-core': 3.5.29 + '@vue/compiler-dom': 3.5.29 + '@vue/compiler-ssr': 3.5.29 + '@vue/shared': 3.5.29 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.8 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.29': + dependencies: + '@vue/compiler-dom': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/reactivity@3.5.29': + dependencies: + '@vue/shared': 3.5.29 + + '@vue/runtime-core@3.5.29': + dependencies: + '@vue/reactivity': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/runtime-dom@3.5.29': + dependencies: + '@vue/reactivity': 3.5.29 + '@vue/runtime-core': 3.5.29 + '@vue/shared': 3.5.29 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.29(vue@3.5.29(typescript@5.6.2))': + dependencies: + '@vue/compiler-ssr': 3.5.29 + '@vue/shared': 3.5.29 + vue: 3.5.29(typescript@5.6.2) + + '@vue/shared@3.5.29': {} + '@webgpu/glslang@0.0.15': {} '@webgpu/types@0.0.31': {} @@ -8534,8 +10710,7 @@ snapshots: acorn@8.12.1: {} - acorn@8.14.0: - optional: true + acorn@8.16.0: {} add-dom-event-listener@1.1.0: dependencies: @@ -8554,6 +10729,10 @@ snapshots: screenfull: 5.2.0 tslib: 2.7.0 + ajv-formats@2.1.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -8614,7 +10793,7 @@ snapshots: ansi-styles@6.2.1: {} - antd@5.21.2(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + antd@5.21.2(date-fns@2.30.0)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@ant-design/colors': 7.1.0 '@ant-design/cssinjs': 1.21.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -8646,7 +10825,7 @@ snapshots: rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-notification: 5.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-pagination: 4.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - rc-picker: 4.6.15(dayjs@1.11.13)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-picker: 4.6.15(date-fns@2.30.0)(dayjs@1.11.13)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-progress: 4.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-rate: 2.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-resize-observer: 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -9061,6 +11240,20 @@ snapshots: chardet@0.7.0: {} + chevrotain-allstar@0.3.1(chevrotain@11.1.2): + dependencies: + chevrotain: 11.1.2 + lodash-es: 4.17.23 + + chevrotain@11.1.2: + dependencies: + '@chevrotain/cst-dts-gen': 11.1.2 + '@chevrotain/gast': 11.1.2 + '@chevrotain/regexp-to-ast': 11.1.2 + '@chevrotain/types': 11.1.2 + '@chevrotain/utils': 11.1.2 + lodash-es: 4.17.23 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -9132,8 +11325,20 @@ snapshots: clsx@1.2.1: {} + clsx@2.1.1: {} + codemirror@5.65.18: {} + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/commands': 6.10.2 + '@codemirror/language': 6.12.2 + '@codemirror/lint': 6.9.5 + '@codemirror/search': 6.6.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -9170,6 +11375,10 @@ snapshots: commander@2.20.3: {} + commander@7.2.0: {} + + commander@8.3.0: {} + comment-json@2.4.2: dependencies: core-util-is: 1.0.3 @@ -9202,6 +11411,19 @@ snapshots: array-ify: 1.0.0 dot-prop: 5.3.0 + compute-gcd@1.2.1: + dependencies: + validate.io-array: 1.0.6 + validate.io-function: 1.0.2 + validate.io-integer-array: 1.0.0 + + compute-lcm@1.1.2: + dependencies: + compute-gcd: 1.2.1 + validate.io-array: 1.0.6 + validate.io-function: 1.0.2 + validate.io-integer-array: 1.0.0 + compute-scroll-into-view@3.1.0: {} concat-map@0.0.1: {} @@ -9262,6 +11484,14 @@ snapshots: core-util-is@1.0.3: {} + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + cosmiconfig-typescript-loader@5.0.0(@types/node@22.13.14)(cosmiconfig@9.0.0(typescript@5.6.2))(typescript@5.6.2): dependencies: '@types/node': 22.13.14 @@ -9290,6 +11520,8 @@ snapshots: dependencies: capture-stack-trace: 1.0.2 + crelt@1.0.6: {} + cross-fetch@3.1.8: dependencies: node-fetch: 2.7.0 @@ -9339,6 +11571,20 @@ snapshots: csstype@3.1.3: {} + csstype@3.2.3: {} + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.33.1 + + cytoscape-fcose@2.2.0(cytoscape@3.33.1): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.33.1 + + cytoscape@3.33.1: {} + cz-conventional-changelog@3.3.0(@types/node@22.13.14)(typescript@5.6.2): dependencies: chalk: 2.4.2 @@ -9361,6 +11607,24 @@ snapshots: dependencies: internmap: 1.0.1 + d3-array@3.2.4: + dependencies: + internmap: 1.0.1 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + d3-cloud@1.2.7: dependencies: d3-dispatch: 1.0.6 @@ -9378,26 +11642,61 @@ snapshots: d3-geo: 2.0.2 d3-path: 2.0.0 + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + d3-dispatch@1.0.6: {} + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + d3-dsv@1.2.0: dependencies: commander: 2.20.3 iconv-lite: 0.4.24 rw: 1.3.3 + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + d3-ease@1.0.7: {} + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + d3-force@2.1.1: dependencies: d3-dispatch: 1.0.6 d3-quadtree: 2.0.0 d3-timer: 1.0.10 + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + d3-format@1.4.5: {} d3-format@2.0.0: {} + d3-format@3.1.2: {} + d3-geo-projection@2.1.2: dependencies: commander: 2.20.3 @@ -9412,6 +11711,10 @@ snapshots: dependencies: d3-array: 2.12.1 + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + d3-hexbin@0.2.2: {} d3-hexjson@1.1.1: @@ -9422,6 +11725,8 @@ snapshots: d3-hierarchy@2.0.0: {} + d3-hierarchy@3.1.2: {} + d3-interpolate@1.4.0: dependencies: d3-color: 1.4.1 @@ -9438,10 +11743,23 @@ snapshots: d3-path@2.0.0: {} + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + d3-quadtree@2.0.0: {} + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + d3-regression@1.3.10: {} + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + d3-sankey@0.9.1: dependencies: d3-array: 1.2.4 @@ -9453,6 +11771,11 @@ snapshots: d3-color: 1.4.1 d3-interpolate: 1.4.0 + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + d3-scale@2.2.2: dependencies: d3-array: 1.2.4 @@ -9470,12 +11793,26 @@ snapshots: d3-time: 2.1.1 d3-time-format: 3.0.0 + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-selection@1.4.2: {} + d3-selection@3.0.0: {} + d3-shape@1.3.7: dependencies: d3-path: 1.0.9 + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + d3-time-format@2.3.0: dependencies: d3-time: 1.1.0 @@ -9484,14 +11821,24 @@ snapshots: dependencies: d3-time: 2.1.1 + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + d3-time@1.1.0: {} d3-time@2.1.1: dependencies: d3-array: 2.12.1 + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + d3-timer@1.0.10: {} + d3-timer@3.0.1: {} + d3-transition@1.3.2: dependencies: d3-color: 1.4.1 @@ -9501,8 +11848,63 @@ snapshots: d3-selection: 1.4.2 d3-timer: 1.0.10 + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + d3-voronoi@1.1.4: {} + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.13: + dependencies: + d3: 7.9.0 + lodash-es: 4.17.23 + dagre@0.8.5: dependencies: graphlib: 2.1.8 @@ -9528,8 +11930,14 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.25.7 + dayjs@1.11.13: {} + dayjs@1.11.19: {} + debug@3.2.7: dependencies: ms: 2.1.3 @@ -9587,6 +11995,10 @@ snapshots: defu@6.1.4: {} + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + delayed-stream@1.0.0: {} dequal@2.0.3: {} @@ -9627,9 +12039,15 @@ snapshots: dependencies: esutils: 2.0.3 + dom-align@1.12.4: {} + dompurify@2.5.8: optional: true + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dot-prop@4.2.1: dependencies: is-obj: 1.0.1 @@ -9656,6 +12074,13 @@ snapshots: earcut@2.2.4: {} + echarts-for-react@3.0.6(echarts@5.5.1)(react@18.3.1): + dependencies: + echarts: 5.5.1 + fast-deep-equal: 3.1.3 + react: 18.3.1 + size-sensor: 1.0.2 + echarts@5.5.1: dependencies: tslib: 2.3.0 @@ -9690,6 +12115,8 @@ snapshots: entities@4.5.0: {} + entities@7.0.1: {} + env-paths@2.2.1: {} environment@1.1.0: {} @@ -10347,6 +12774,8 @@ snapshots: github-slugger@1.5.0: {} + github-slugger@2.0.0: {} + gl-matrix@3.4.3: {} gl-vec2@1.3.0: {} @@ -10472,6 +12901,8 @@ snapshots: dependencies: duplexer: 0.1.2 + hachure-fill@0.5.2: {} + hammerjs@2.0.8: {} has-ansi@2.0.0: @@ -10512,6 +12943,28 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-from-dom@5.0.1: + dependencies: + '@types/hast': 3.0.4 + hastscript: 9.0.0 + web-namespaces: 2.0.1 + + hast-util-from-html-isomorphic@2.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-dom: 5.0.1 + hast-util-from-html: 2.0.3 + unist-util-remove-position: 5.0.0 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.2 + parse5: 7.2.1 + vfile: 6.0.3 + vfile-message: 4.0.2 + hast-util-from-parse5@7.1.2: dependencies: '@types/hast': 2.3.10 @@ -10539,6 +12992,10 @@ snapshots: dependencies: '@types/hast': 2.3.10 + hast-util-heading-rank@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-is-element@2.1.3: dependencies: '@types/hast': 2.3.10 @@ -10588,6 +13045,12 @@ snapshots: web-namespaces: 2.0.1 zwitch: 2.0.4 + hast-util-sanitize@5.0.2: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.2.0 + unist-util-position: 5.0.0 + hast-util-select@5.0.1: dependencies: '@types/hast': 2.3.10 @@ -10621,6 +13084,20 @@ snapshots: stringify-entities: 4.0.4 zwitch: 2.0.4 + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.2: dependencies: '@types/estree': 1.0.6 @@ -10641,6 +13118,26 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.6 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.1.3 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + hast-util-to-parse5@7.1.0: dependencies: '@types/hast': 2.3.10 @@ -10666,6 +13163,10 @@ snapshots: dependencies: '@types/hast': 2.3.10 + hast-util-to-string@3.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-text@4.0.2: dependencies: '@types/hast': 3.0.4 @@ -10744,6 +13245,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -10780,6 +13285,8 @@ snapshots: inline-style-parser@0.2.4: {} + inline-style-parser@0.2.7: {} + inquirer@7.3.3: dependencies: ansi-escapes: 4.3.2 @@ -11087,6 +13594,8 @@ snapshots: isobject@3.0.1: {} + isomorphic.js@0.2.5: {} + iterator.prototype@1.1.2: dependencies: define-properties: 1.2.1 @@ -11117,6 +13626,16 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema-compare@0.2.2: + dependencies: + lodash: 4.17.21 + + json-schema-merge-allof@0.8.1: + dependencies: + compute-lcm: 1.1.2 + json-schema-compare: 0.2.2 + lodash: 4.17.21 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -11151,6 +13670,8 @@ snapshots: jsonparse@1.3.1: {} + jsonpointer@5.0.1: {} + jspdf@2.5.2: dependencies: '@babel/runtime': 7.25.7 @@ -11170,6 +13691,10 @@ snapshots: object.assign: 4.1.5 object.values: 1.2.0 + katex@0.16.33: + dependencies: + commander: 8.3.0 + kdbush@3.0.0: {} kdbush@4.0.2: {} @@ -11186,6 +13711,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + khroma@2.1.0: {} + kind-of@3.2.2: dependencies: is-buffer: 1.1.6 @@ -11198,6 +13725,14 @@ snapshots: kolorist@1.8.0: {} + langium@4.2.1: + dependencies: + chevrotain: 11.1.2 + chevrotain-allstar: 0.3.1(chevrotain@11.1.2) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + latest-version@3.1.0: dependencies: package-json: 4.0.1 @@ -11206,6 +13741,10 @@ snapshots: dependencies: package-json: 6.5.0 + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + lazy-cache@1.0.4: {} levn@0.4.1: @@ -11213,6 +13752,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lib0@0.2.117: + dependencies: + isomorphic.js: 0.2.5 + libnpx@10.2.4: dependencies: dotenv: 5.0.1 @@ -11290,6 +13833,8 @@ snapshots: lodash-es@4.17.21: {} + lodash-es@4.17.23: {} + lodash.camelcase@4.3.0: {} lodash.clonedeep@4.5.0: {} @@ -11370,6 +13915,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + make-dir@1.3.0: dependencies: pify: 3.0.0 @@ -11441,12 +13990,40 @@ snapshots: markdown-table@3.0.4: {} + markdown-to-jsx@7.7.17(react@18.3.1): + optionalDependencies: + react: 18.3.1 + + marked@16.4.2: {} + + marked@17.0.3: {} + mdast-util-definitions@5.1.2: dependencies: '@types/mdast': 3.0.15 '@types/unist': 2.0.11 unist-util-visit: 4.1.2 + mdast-util-definitions@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + unist-util-visit: 5.0.0 + + mdast-util-directive@3.1.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.1 + stringify-entities: 4.0.4 + unist-util-visit-parents: 6.0.1 + transitivePeerDependencies: + - supports-color + mdast-util-find-and-replace@2.2.2: dependencies: '@types/mdast': 3.0.15 @@ -11587,6 +14164,18 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-math@3.0.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + longest-streak: 3.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + unist-util-remove-position: 5.0.0 + transitivePeerDependencies: + - supports-color + mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 @@ -11711,6 +14300,29 @@ snapshots: merge@2.1.1: {} + mermaid@11.12.3: + dependencies: + '@braintree/sanitize-url': 7.1.2 + '@iconify/utils': 3.1.0 + '@mermaid-js/parser': 1.0.0 + '@types/d3': 7.4.3 + cytoscape: 3.33.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) + cytoscape-fcose: 2.2.0(cytoscape@3.33.1) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.13 + dayjs: 1.11.19 + dompurify: 3.3.1 + katex: 0.16.33 + khroma: 2.1.0 + lodash-es: 4.17.23 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.3.6 + ts-dedent: 2.2.0 + uuid: 11.1.0 + micromark-core-commonmark@1.1.0: dependencies: decode-named-character-reference: 1.0.2 @@ -11749,6 +14361,16 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.1 + micromark-extension-directive@3.0.2: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + parse-entities: 4.0.1 + micromark-extension-gfm-autolink-literal@1.0.5: dependencies: micromark-util-character: 1.2.0 @@ -11853,6 +14475,16 @@ snapshots: micromark-util-combine-extensions: 2.0.1 micromark-util-types: 2.0.1 + micromark-extension-math@3.1.0: + dependencies: + '@types/katex': 0.16.8 + devlop: 1.1.0 + katex: 0.16.33 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + micromark-factory-destination@1.1.0: dependencies: micromark-util-character: 1.2.0 @@ -12144,6 +14776,13 @@ snapshots: pkg-types: 1.2.0 ufo: 1.5.4 + mlly@1.8.0: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + mock-property@1.0.3: dependencies: define-data-property: 1.1.4 @@ -12210,8 +14849,14 @@ snapshots: mute-stream@0.0.8: {} + nanoid@3.3.11: {} + nanoid@3.3.7: {} + nanoid@4.0.2: {} + + nanoid@5.1.6: {} + natural-compare@1.4.0: {} node-fetch-native@1.6.4: {} @@ -12318,6 +14963,14 @@ snapshots: dependencies: mimic-function: 5.0.1 + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.4: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.1.0 + regex-recursion: 6.0.2 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -12339,6 +14992,8 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 + orderedmap@2.1.1: {} + os-homedir@1.0.2: {} os-tmpdir@1.0.2: {} @@ -12398,6 +15053,8 @@ snapshots: package-manager-detector@0.2.1: {} + package-manager-detector@1.6.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -12448,6 +15105,8 @@ snapshots: dependencies: entities: 4.5.0 + path-data-parser@0.1.0: {} + path-exists@3.0.0: {} path-exists@4.0.0: {} @@ -12472,6 +15131,8 @@ snapshots: pathe@1.1.2: {} + pathe@2.0.3: {} + pbf@3.3.0: dependencies: ieee754: 1.2.1 @@ -12486,6 +15147,8 @@ snapshots: picocolors@1.1.0: {} + picocolors@1.1.1: {} + picomatch@2.3.1: {} pidtree@0.6.0: {} @@ -12498,12 +15161,25 @@ snapshots: mlly: 1.7.2 pathe: 1.1.2 + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + point-at-length@1.1.0: dependencies: abs-svg-path: 0.1.1 isarray: 0.0.1 parse-svg-path: 0.1.2 + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + polygon-clipping@0.15.7: dependencies: robust-predicates: 3.0.2 @@ -12527,6 +15203,12 @@ snapshots: picocolors: 1.1.0 source-map-js: 1.2.1 + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + potpack@1.0.2: {} potpack@2.0.0: {} @@ -12572,6 +15254,100 @@ snapshots: property-information@6.5.0: {} + property-information@7.1.0: {} + + prosemirror-changeset@2.4.0: + dependencies: + prosemirror-transform: 1.11.0 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + prosemirror-drop-indicator@0.1.3: + dependencies: + '@ocavue/utils': 1.5.0 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.6 + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.6 + + prosemirror-gapcursor@1.4.0: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.6 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.6 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-model@1.25.4: + dependencies: + orderedmap: 2.1.1 + + prosemirror-safari-ime-span@1.0.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.6 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.6 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.6 + + prosemirror-transform@1.11.0: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-view@1.41.6: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + prosemirror-virtual-cursor@0.4.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6): + optionalDependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.6 + protocol-buffers-schema@3.6.0: {} proxy-from-env@1.1.0: {} @@ -12602,6 +15378,16 @@ snapshots: performance-now: 2.1.0 optional: true + rc-align@4.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.25.7 + classnames: 2.5.1 + dom-align: 1.12.4 + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + resize-observer-polyfill: 1.5.1 + rc-cascader@3.28.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.25.7 @@ -12753,7 +15539,20 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - rc-picker@4.6.15(dayjs@1.11.13)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + rc-picker@2.7.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.25.7 + classnames: 2.5.1 + date-fns: 2.30.0 + dayjs: 1.11.19 + moment: 2.29.1 + rc-trigger: 5.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + shallowequal: 1.1.0 + + rc-picker@4.6.15(date-fns@2.30.0)(dayjs@1.11.13)(moment@2.29.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.25.7 '@rc-component/trigger': 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -12764,6 +15563,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: + date-fns: 2.30.0 dayjs: 1.11.13 moment: 2.29.1 @@ -12907,6 +15707,16 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + rc-trigger@5.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.25.7 + classnames: 2.5.1 + rc-align: 4.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + rc-upload@4.8.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.25.7 @@ -13175,6 +15985,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-virtuoso@4.14.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-window@1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.25.7 @@ -13260,6 +16075,16 @@ snapshots: regenerator-runtime@0.14.1: {} + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + regexp.prototype.flags@1.5.3: dependencies: call-bind: 1.0.7 @@ -13298,6 +16123,10 @@ snapshots: unified: 10.1.2 unist-util-visit: 4.1.2 + rehype-harden@1.1.8: + dependencies: + unist-util-visit: 5.0.0 + rehype-highlight@7.0.1: dependencies: '@types/hast': 3.0.4 @@ -13306,6 +16135,16 @@ snapshots: unist-util-visit: 5.0.0 vfile: 6.0.3 + rehype-katex@7.0.1: + dependencies: + '@types/hast': 3.0.4 + '@types/katex': 0.16.8 + hast-util-from-html-isomorphic: 2.0.0 + hast-util-to-text: 4.0.2 + katex: 0.16.33 + unist-util-visit-parents: 6.0.1 + vfile: 6.0.3 + rehype-parse@8.0.5: dependencies: '@types/hast': 2.3.10 @@ -13331,6 +16170,11 @@ snapshots: unified: 10.1.0 unist-util-visit: 4.1.0 + rehype-sanitize@6.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-sanitize: 5.0.2 + rehype-slug@5.0.0: dependencies: '@types/hast': 2.3.10 @@ -13341,6 +16185,14 @@ snapshots: unified: 10.1.2 unist-util-visit: 4.1.2 + rehype-slug@6.0.0: + dependencies: + '@types/hast': 3.0.4 + github-slugger: 2.0.0 + hast-util-heading-rank: 3.0.0 + hast-util-to-string: 3.0.1 + unist-util-visit: 5.0.0 + rehype-stringify@9.0.4: dependencies: '@types/hast': 2.3.10 @@ -13354,6 +16206,15 @@ snapshots: rehype-stringify: 9.0.4 unified: 10.1.2 + remark-directive@3.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-directive: 3.1.0 + micromark-extension-directive: 3.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + remark-gfm@2.0.0: dependencies: '@types/mdast': 3.0.15 @@ -13374,6 +16235,32 @@ snapshots: transitivePeerDependencies: - supports-color + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.0.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-inline-links@7.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-definitions: 6.0.0 + unist-util-visit: 5.0.0 + + remark-math@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-math: 3.0.0 + micromark-extension-math: 3.1.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + remark-parse@10.0.2: dependencies: '@types/mdast': 3.0.15 @@ -13399,6 +16286,14 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + remark-rehype@9.1.0: dependencies: '@types/hast': 2.3.10 @@ -13412,6 +16307,17 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 + remark@15.0.1: + dependencies: + '@types/mdast': 4.0.4 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remend@1.2.1: {} + remote-origin-url@1.0.0: dependencies: parse-git-config: 1.1.1 @@ -13514,6 +16420,15 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.24.0 fsevents: 2.3.3 + rope-sequence@1.3.4: {} + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + run-async@2.4.1: {} run-parallel@1.2.0: @@ -13639,6 +16554,17 @@ snapshots: shebang-regex@3.0.0: {} + shiki@3.23.0: + dependencies: + '@shikijs/core': 3.23.0 + '@shikijs/engine-javascript': 3.23.0 + '@shikijs/engine-oniguruma': 3.23.0 + '@shikijs/langs': 3.23.0 + '@shikijs/themes': 3.23.0 + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + side-channel@1.0.6: dependencies: call-bind: 1.0.7 @@ -13741,6 +16667,28 @@ snapshots: stackblur-canvas@2.7.0: optional: true + streamdown@2.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + clsx: 2.1.1 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + marked: 17.0.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + rehype-harden: 1.1.8 + rehype-raw: 7.0.0 + rehype-sanitize: 6.0.0 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remend: 1.2.1 + tailwind-merge: 3.5.0 + unified: 11.0.5 + unist-util-visit: 5.0.0 + unist-util-visit-parents: 6.0.1 + transitivePeerDependencies: + - supports-color + string-argv@0.3.2: {} string-convert@0.2.1: {} @@ -13848,10 +16796,20 @@ snapshots: strip-json-comments@3.1.1: {} + style-mod@4.1.3: {} + + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + style-to-object@0.3.0: dependencies: inline-style-parser: 0.1.1 + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + style-to-object@1.0.8: dependencies: inline-style-parser: 0.2.4 @@ -13884,6 +16842,8 @@ snapshots: stylis@4.3.4: {} + stylis@4.3.6: {} + supercluster@7.1.5: dependencies: kdbush: 3.0.0 @@ -13918,6 +16878,8 @@ snapshots: '@pkgr/core': 0.1.1 tslib: 2.7.0 + tailwind-merge@3.5.0: {} + tape@4.17.0: dependencies: '@ljharb/resumer': 0.0.1 @@ -13946,7 +16908,7 @@ snapshots: terser@5.36.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.14.0 + acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 optional: true @@ -13971,6 +16933,8 @@ snapshots: tinyexec@0.3.0: {} + tinyexec@1.0.2: {} + tinyqueue@2.0.3: {} tippy.js@6.3.7: @@ -14007,6 +16971,8 @@ snapshots: dependencies: typescript: 5.6.2 + ts-dedent@2.2.0: {} + ts-pattern@4.0.4: {} tsconfig-paths@3.15.0: @@ -14101,6 +17067,8 @@ snapshots: ufo@1.5.4: {} + ufo@1.6.3: {} + uglify-js@2.8.29: dependencies: source-map: 0.5.7 @@ -14208,6 +17176,11 @@ snapshots: dependencies: '@types/unist': 3.0.3 + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.0.0 + unist-util-stringify-position@3.0.3: dependencies: '@types/unist': 2.0.11 @@ -14259,13 +17232,13 @@ snapshots: universalify@2.0.1: {} - unocss@0.58.9(postcss@8.4.47)(rollup@4.24.0)(vite@5.4.8(@types/node@22.13.14)(sass@1.79.4)(terser@5.36.0)): + unocss@0.58.9(postcss@8.5.8)(rollup@4.24.0)(vite@5.4.8(@types/node@22.13.14)(sass@1.79.4)(terser@5.36.0)): dependencies: '@unocss/astro': 0.58.9(rollup@4.24.0)(vite@5.4.8(@types/node@22.13.14)(sass@1.79.4)(terser@5.36.0)) '@unocss/cli': 0.58.9(rollup@4.24.0) '@unocss/core': 0.58.9 '@unocss/extractor-arbitrary-variants': 0.58.9 - '@unocss/postcss': 0.58.9(postcss@8.4.47) + '@unocss/postcss': 0.58.9(postcss@8.5.8) '@unocss/preset-attributify': 0.58.9 '@unocss/preset-icons': 0.58.9 '@unocss/preset-mini': 0.58.9 @@ -14397,6 +17370,21 @@ snapshots: dependencies: builtins: 1.0.3 + validate.io-array@1.0.6: {} + + validate.io-function@1.0.2: {} + + validate.io-integer-array@1.0.0: + dependencies: + validate.io-array: 1.0.6 + validate.io-integer: 1.0.5 + + validate.io-integer@1.0.5: + dependencies: + validate.io-number: 1.0.3 + + validate.io-number@1.0.3: {} + vfile-location@4.1.0: dependencies: '@types/unist': 2.0.11 @@ -14445,12 +17433,41 @@ snapshots: sass: 1.79.4 terser: 5.36.0 + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.1.0: {} + vt-pbf@3.1.3: dependencies: '@mapbox/point-geometry': 0.1.0 '@mapbox/vector-tile': 1.3.1 pbf: 3.3.0 + vue@3.5.29(typescript@5.6.2): + dependencies: + '@vue/compiler-dom': 3.5.29 + '@vue/compiler-sfc': 3.5.29 + '@vue/runtime-dom': 3.5.29 + '@vue/server-renderer': 3.5.29(vue@3.5.29(typescript@5.6.2)) + '@vue/shared': 3.5.29 + optionalDependencies: + typescript: 5.6.2 + + w3c-keyname@2.2.8: {} + warning@4.0.3: dependencies: loose-envify: 1.4.0 @@ -14609,6 +17626,20 @@ snapshots: xtend@4.0.2: {} + y-prosemirror@1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29): + dependencies: + lib0: 0.2.117 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.6 + y-protocols: 1.0.7(yjs@13.6.29) + yjs: 13.6.29 + + y-protocols@1.0.7(yjs@13.6.29): + dependencies: + lib0: 0.2.117 + yjs: 13.6.29 + y18n@4.0.3: {} y18n@5.0.8: {} @@ -14657,6 +17688,10 @@ snapshots: decamelize: 1.2.0 window-size: 0.1.0 + yjs@13.6.29: + dependencies: + lib0: 0.2.117 + yocto-queue@0.1.0: {} yocto-queue@1.1.1: {} diff --git a/scripts/pre-commit.mjs b/scripts/pre-commit.mjs new file mode 100644 index 00000000..b4d9c28c --- /dev/null +++ b/scripts/pre-commit.mjs @@ -0,0 +1,50 @@ +import { spawnSync } from 'node:child_process' + +const lintableExtensions = new Set(['.js', '.jsx', '.ts', '.tsx']) + +const run = (command, args, options = {}) => + spawnSync(command, args, { + stdio: 'inherit', + encoding: 'utf8', + ...options, + }) + +const getStagedFiles = () => { + const result = spawnSync('git', ['diff', '--cached', '--name-only', '--diff-filter=ACMR'], { + encoding: 'utf8', + }) + + if (result.status !== 0) { + process.stderr.write(result.stderr || 'failed to read staged files\n') + process.exit(result.status ?? 1) + } + + return result.stdout + .split('\n') + .map((file) => file.trim()) + .filter(Boolean) +} + +const stagedFiles = getStagedFiles() +const lintableFiles = stagedFiles.filter((file) => { + const dotIndex = file.lastIndexOf('.') + if (dotIndex === -1) return false + return lintableExtensions.has(file.slice(dotIndex)) +}) + +if (lintableFiles.length === 0) { + process.exit(0) +} + +const eslintResult = run('pnpm', ['exec', 'eslint', '--fix', ...lintableFiles]) + +const addResult = run('git', ['add', '--', ...lintableFiles]) +if (addResult.status !== 0) { + process.exit(addResult.status ?? 1) +} + +if (eslintResult.status !== 0) { + console.warn('\npre-commit: eslint reported issues, but commit is not blocked. Run `pnpm lint` manually when needed.') +} + +process.exit(0) diff --git a/src/App.tsx b/src/App.tsx index b6462fe5..d9627cb5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,8 @@ import { useRoutes } from 'react-router-dom'; import routers from './App/routers/routers'; import irifyRouters from './App/routers/irify-routers'; -import '@yakit-libs/color/dist/index.css'; +// import '@yakit-libs/color/dist/index.css'; +import './styles/index.css'; const APP_MODE = import.meta.env.VITE_APP_MODE; diff --git a/src/App/AppLayout.tsx b/src/App/AppLayout.tsx index a825e644..312f13e7 100644 --- a/src/App/AppLayout.tsx +++ b/src/App/AppLayout.tsx @@ -1,301 +1,295 @@ -import type { MenuProps } from 'antd'; -import { Button, Layout, Menu, Spin } from 'antd'; +import type { MenuProps } from 'antd' +import { Button, Layout, Menu, Spin } from 'antd' import { - useEffect, - useMemo, - useRef, - // useRef -} from 'react'; -import { Outlet, useLocation, useNavigate } from 'react-router-dom'; -import { useRequest, useSafeState } from 'ahooks'; - -import routers from './routers/routers'; -import { useNetworkStatus, usePermissionsSlice } from '@/hooks'; -import { ROUTES } from '@/utils/routeMap'; - -import login_logo from '@/assets/compoments/telecommunicationsLogo.svg'; -// import login_logo from '@/assets/compoments/login_logo.png'; -// import header_text from '@/assets/login/header_text.png'; -import { SiderClose, SiderOpen } from '@/assets/compoments'; -import { UserCard } from './UserCard'; -import { findFullPath, findPathNodes, processMenu } from '@/utils'; -import { LeftOutlined } from '@ant-design/icons'; -import { getLicense } from '@/apis/login'; -import { match } from 'ts-pattern'; -import type { UseModalRefType } from '@/compoments/WizardModal/useModal'; -import { McpModal } from '@/utils/McpModel'; + useEffect, + useMemo, + useRef, + // useRef +} from 'react' +import { Outlet, useLocation, useNavigate } from 'react-router-dom' +import { useRequest, useSafeState } from 'ahooks' + +import routers from './routers/routers' +import { useNetworkStatus, usePermissionsSlice } from '@/hooks' +import { ROUTES } from '@/utils/routeMap' + +// import login_logo from '@/assets/compoments/telecommunicationsLogo.svg' +// import login_logo from '@/assets/compoments/login_logo.png' +// import header_text from '@/assets/login/header_text.png' +import { SiderClose, SiderOpen } from '@/assets/compoments' +import { UserCard } from './UserCard' +import { findFullPath, findPathNodes, processMenu } from '@/utils' +import { LeftOutlined } from '@ant-design/icons' +import { match } from 'ts-pattern' +import type { UseModalRefType } from '@/compoments/WizardModal/useModal' +import { McpModal } from '@/utils/McpModel' +import { shouldBypassLicense } from '@/utils/license' +import { YakitRoute } from '@/pages/AIAgent/enums/yakitRoute' +import { AIEngineHeaderControl } from '@/pages/AIAgent/ai-agent/components/AIEngineHeaderControl/AIEngineHeaderControl' // import useLoginStore from './store/loginStore'; -const { Header, Content, Sider } = Layout; +const { Header, Content, Sider } = Layout const AppLayout = () => { - const locations = useLocation(); - const navigate = useNavigate(); - const { status } = useNetworkStatus(); - // const store = useLoginStore.getState(); - - const mcpModalRef = useRef(null); - - const [collapsed, setCollapsed] = useSafeState(false); - const [headerTitle, setHeaderTitle] = useSafeState< - Array> | undefined - >([]); - - const { permissionsSlice } = usePermissionsSlice(); - - const { data: license, loading } = useRequest(async () => { - const { data } = await getLicense(); - const { license } = data; - return license?.length > 0 ? license : undefined; - }); - - // 路由重定向 - useEffect(() => { - if (locations.pathname === '/') { - const routesChildrenList = routers[0]?.children?.[0] ?? {}; - const getRoutesPath = findFullPath(routesChildrenList)?.[0]; - navigate(getRoutesPath); - } - }, [locations.pathname]); - - const items: MenuProps['items'] = useMemo(() => { - const routesList = routers[0]?.children ?? []; - - // 路由列表转换菜单 - const routerList = processMenu(routesList, navigate); - - // 获取 layout Header 面包屑 - const resultPathNodes = - findPathNodes(locations.pathname, routesList) ?? []; - const resultRouteList = match(resultPathNodes?.length) - .with(1, () => { - return [resultPathNodes?.[0]]; - }) - .with(2, () => { - return resultPathNodes?.slice(1); - }) - - .otherwise(() => []); - setHeaderTitle(resultRouteList); - - return routerList; - }, [routers, collapsed, permissionsSlice, locations.pathname]); - - // 超过二级路由点击时,获取2级路由作为key - const menuSelectedKeys = useMemo(() => { - const pathNameList = locations.pathname - .split('/') - .filter((it) => it !== ''); - const resultSelectedKey = - pathNameList.length > 2 - ? `/${pathNameList.slice(0, 2).join('/')}` - : `/${pathNameList.join('/')}`; - return resultSelectedKey; - }, [locations.pathname]); - - useEffect(() => { - !status && navigate(ROUTES.NETWORK_ERROR, { replace: true }); - }, [status]); - - useEffect(() => { - license && navigate(ROUTES.LICENSE, { state: { license } }); - }, [license]); - - // const timeoutRef = useRef(null); - // const STORAGE_KEY = 'lastActiveTime'; // 存储时间的 key - // const IDLE_TIMEOUT = 1000 * 60 * 30; // 30分钟 - - // // 触发事件的逻辑 - // const triggerEvent = () => { - // localStorage.removeItem(STORAGE_KEY); // 事件触发后清除时间记录 - // store.outLogin(); - // }; - - // // 重置定时器 - // const resetTimer = () => { - // if (timeoutRef.current) { - // clearTimeout(timeoutRef.current); - // } - - // const now = new Date().toISOString(); - // localStorage.setItem(STORAGE_KEY, now); // 将当前时间存储到 localStorage - - // timeoutRef.current = window.setTimeout(() => { - // triggerEvent(); - // }, IDLE_TIMEOUT); // 30分钟后触发 - // }; - - // // 页面加载时检测是否超时 - // const checkIdleTime = () => { - // const lastActiveTime = localStorage.getItem(STORAGE_KEY); - // if (lastActiveTime) { - // const lastActiveTimestamp = new Date(lastActiveTime).getTime(); - // const currentTimestamp = Date.now(); - - // const elapsed = currentTimestamp - lastActiveTimestamp; - // if (elapsed >= IDLE_TIMEOUT) { - // // 已经过了 30 分钟,立即触发事件 - // triggerEvent(); - // } else { - // // 未超过 1 分钟,继续倒计时剩余时间 - // timeoutRef.current = window.setTimeout(() => { - // triggerEvent(); - // }, IDLE_TIMEOUT - elapsed); - // } - // } else { - // // 如果没有存储时间,初始化定时器 - // resetTimer(); - // } - // }; - - // useEffect(() => { - // // 监听用户操作事件 - // const handleUserAction = () => resetTimer(); - - // window.addEventListener('mousemove', handleUserAction); - // window.addEventListener('keydown', handleUserAction); - // window.addEventListener('click', handleUserAction); - // window.addEventListener('scroll', handleUserAction); - - // // 检测页面加载时的空闲时间 - // checkIdleTime(); - - // return () => { - // // 清理事件监听和定时器 - // if (timeoutRef.current) { - // clearTimeout(timeoutRef.current); - // } - // window.removeEventListener('mousemove', handleUserAction); - // window.removeEventListener('keydown', handleUserAction); - // window.removeEventListener('click', handleUserAction); - // window.removeEventListener('scroll', handleUserAction); - // }; - // }, []); - return loading ? ( - -
- - ) : ( - - + const locations = useLocation() + const navigate = useNavigate() + const { status } = useNetworkStatus() + // const store = useLoginStore.getState(); + + const mcpModalRef = useRef(null) + + const [collapsed, setCollapsed] = useSafeState(false) + const [headerTitle, setHeaderTitle] = useSafeState> | undefined>([]) + const [headerPathNodes, setHeaderPathNodes] = useSafeState>>([]) + const bypassLicense = shouldBypassLicense() + + const { permissionsSlice } = usePermissionsSlice() + + const { loading } = useRequest( + async () => { + return undefined + // const { data } = await getLicense(); + // const { license } = data; + // return resolveLicenseGateValue(license); + }, + { manual: bypassLicense }, + ) + + // 路由重定向 + useEffect(() => { + if (locations.pathname === '/') { + const routesChildrenList = routers[0]?.children?.[0] ?? {} + const getRoutesPath = findFullPath(routesChildrenList)?.[0] + navigate(getRoutesPath) + } + }, [locations.pathname]) + + const items: MenuProps['items'] = useMemo(() => { + const routesList = routers[0]?.children ?? [] + + // 路由列表转换菜单 + const routerList = processMenu(routesList, navigate) + + // 获取 layout Header 面包屑 + const resultPathNodes = findPathNodes(locations.pathname, routesList) ?? [] + const pathDepth = resultPathNodes.length + const resultRouteList = match(pathDepth) + .with(1, () => { + return [resultPathNodes[0]] + }) + .with(2, () => { + return [resultPathNodes[1]] + }) + .with(3, () => { + return [resultPathNodes[2]] + }) + .otherwise(() => []) + + setHeaderPathNodes(resultPathNodes) + setHeaderTitle(resultRouteList) + + return routerList + }, [routers, collapsed, permissionsSlice, locations.pathname]) + + // 超过二级路由点击时,获取2级路由作为key + const menuSelectedKeys = useMemo(() => { + const pathNameList = locations.pathname.split('/').filter((it) => it !== '') + const resultSelectedKey = + pathNameList.length > 2 ? `/${pathNameList.slice(0, 2).join('/')}` : `/${pathNameList.join('/')}` + return resultSelectedKey + }, [locations.pathname]) + + useEffect(() => { + !status && navigate(ROUTES.NETWORK_ERROR, { replace: true }) + }, [status]) + + // useEffect(() => { + // license && navigate(ROUTES.LICENSE, { state: { license } }); + // }, [license]); + + // const timeoutRef = useRef(null); + // const STORAGE_KEY = 'lastActiveTime'; // 存储时间的 key + // const IDLE_TIMEOUT = 1000 * 60 * 30; // 30分钟 + + // // 触发事件的逻辑 + // const triggerEvent = () => { + // localStorage.removeItem(STORAGE_KEY); // 事件触发后清除时间记录 + // store.outLogin(); + // }; + + // // 重置定时器 + // const resetTimer = () => { + // if (timeoutRef.current) { + // clearTimeout(timeoutRef.current); + // } + + // const now = new Date().toISOString(); + // localStorage.setItem(STORAGE_KEY, now); // 将当前时间存储到 localStorage + + // timeoutRef.current = window.setTimeout(() => { + // triggerEvent(); + // }, IDLE_TIMEOUT); // 30分钟后触发 + // }; + + // // 页面加载时检测是否超时 + // const checkIdleTime = () => { + // const lastActiveTime = localStorage.getItem(STORAGE_KEY); + // if (lastActiveTime) { + // const lastActiveTimestamp = new Date(lastActiveTime).getTime(); + // const currentTimestamp = Date.now(); + + // const elapsed = currentTimestamp - lastActiveTimestamp; + // if (elapsed >= IDLE_TIMEOUT) { + // // 已经过了 30 分钟,立即触发事件 + // triggerEvent(); + // } else { + // // 未超过 1 分钟,继续倒计时剩余时间 + // timeoutRef.current = window.setTimeout(() => { + // triggerEvent(); + // }, IDLE_TIMEOUT - elapsed); + // } + // } else { + // // 如果没有存储时间,初始化定时器 + // resetTimer(); + // } + // }; + + // useEffect(() => { + // // 监听用户操作事件 + // const handleUserAction = () => resetTimer(); + + // window.addEventListener('mousemove', handleUserAction); + // window.addEventListener('keydown', handleUserAction); + // window.addEventListener('click', handleUserAction); + // window.addEventListener('scroll', handleUserAction); + + // // 检测页面加载时的空闲时间 + // checkIdleTime(); + + // return () => { + // // 清理事件监听和定时器 + // if (timeoutRef.current) { + // clearTimeout(timeoutRef.current); + // } + // window.removeEventListener('mousemove', handleUserAction); + // window.removeEventListener('keydown', handleUserAction); + // window.removeEventListener('click', handleUserAction); + // window.removeEventListener('scroll', handleUserAction); + // }; + // }, []); + return loading ? ( + +
+ + ) : ( + + +
+
+ {/* + {!collapsed ? : null} */} + { + !collapsed ? ( +
+ AI自动化渗透测试平台 +
+ ) : null + // + } +
+
setCollapsed((value) => !value)}> +
{collapsed ? : }
+
+
+ + + + + + {!collapsed && ( +
版本: 20240808-e01f03cb
+ )} + + + +
+ {headerTitle?.map((item) => { + if (!item) return null + + const pathDepth = headerPathNodes.length + const isThirdLevelRoute = pathDepth === 3 + const parentPath = headerPathNodes[pathDepth - 2]?.path + + if (isThirdLevelRoute) { + const navigatePath = parentPath + return (
navigatePath && navigate(`/${navigatePath}`)} > -
- {/* - {!collapsed ? ( - - ) : null} */} - {!collapsed ? ( -
- 自动化渗透系统 -
- ) : ( - - )} -
-
setCollapsed((value) => !value)} - > -
- {collapsed ? : } -
-
+ + {item.name}
- - - - - - {!collapsed && ( -
- 版本: 20240808-e01f03cb -
- )} - - - -
- {headerTitle?.map((item, index) => { - return index !== headerTitle.length - 1 && item ? ( -
navigate(`/${item.path}`)} - > - - {item.name} -
- ) : ( -
-
{item.name}
- -
- ); - })} - {/* + ) + } + + return ( +
+
+
{item.name}
+ {item.path === YakitRoute.AI_Agent && } +
+
+ +
+
+ ) + })} + {/* 分布式调度yaklang引擎,执行分布式脚本,获得结果 */} -
- {permissionsSlice.length > 0 && ( - - - - )} -
- - - ); -}; -export default AppLayout; +
+ {permissionsSlice.length > 0 && ( + + + + )} +
+ + + ) +} +export default AppLayout diff --git a/src/App/IRifyLayout.tsx b/src/App/IRifyLayout.tsx index 2e53f14c..474a4c78 100644 --- a/src/App/IRifyLayout.tsx +++ b/src/App/IRifyLayout.tsx @@ -24,6 +24,7 @@ import { MenuUnfoldOutlined, } from '@ant-design/icons'; import { getLicense } from '@/apis/login'; +import { resolveLicenseGateValue, shouldBypassLicense } from '@/utils/license'; import { useNetworkStatus } from '@/hooks'; import useLoginStore from '@/App/store/loginStore'; import { useTheme } from '@/theme'; @@ -99,6 +100,11 @@ const collapsibleNavItems = [ label: '节点管理', path: '/node-config/manage', }, + { + key: '/node-config/observability', + label: '扫描观测', + path: '/node-config/observability', + }, ], }, { @@ -125,12 +131,13 @@ const IRifyLayout: React.FC = () => { new Set(), ); const { isDark, themeMode, setThemeMode } = useTheme(); + const bypassLicense = shouldBypassLicense(); const { data: license, loading } = useRequest(async () => { const { data } = await getLicense(); const { license } = data; - return license?.length > 0 ? license : undefined; - }); + return resolveLicenseGateValue(license); + }, { manual: bypassLicense }); // Check network status useEffect(() => { @@ -202,6 +209,8 @@ const IRifyLayout: React.FC = () => { return ['节点配置', '节点安装']; if (path.startsWith('/node-config/manage')) return ['节点配置', '节点管理']; + if (path.startsWith('/node-config/observability')) + return ['节点配置', '扫描观测']; if (path.startsWith('/system-management/userinfo')) return ['系统管理', '用户管理']; diff --git a/src/App/UserCard.tsx b/src/App/UserCard.tsx index f6374268..5ae2ae74 100644 --- a/src/App/UserCard.tsx +++ b/src/App/UserCard.tsx @@ -1,13 +1,13 @@ -import type { FC } from 'react'; -import Avatar from '../assets/compoments/Avatar.png'; -import { CloseIcon } from '@/assets/compoments'; -import useLoginStore from './store/loginStore'; -import { usePermissionsSlice } from '@/hooks'; -import { Button, Popover } from 'antd'; -import { InfoCircleOutlined } from '@ant-design/icons'; -import { useSafeState } from 'ahooks'; -import { useNavigate } from 'react-router-dom'; -import { getLoginOut } from '@/apis/login'; +import type { FC } from 'react' +import Avatar from '../assets/compoments/Avatar.png' +import { CloseIcon } from '@/assets/compoments' +import useLoginStore from './store/loginStore' +import { usePermissionsSlice } from '@/hooks' +import { Button, Popover } from 'antd' +import { InfoCircleOutlined } from '@ant-design/icons' +import { useSafeState } from 'ahooks' +import { useNavigate } from 'react-router-dom' +import { getLoginOut } from '@/apis/login' /** * @@ -15,86 +15,80 @@ import { getLoginOut } from '@/apis/login'; * @returns */ const UserCard: FC<{ collapsed: boolean }> = ({ collapsed }) => { - const [open, setOpen] = useSafeState(false); - const navigate = useNavigate(); + const [open, setOpen] = useSafeState(false) + const navigate = useNavigate() - // 个人登录信息下拉菜单 - const { outLogin, userInfo } = useLoginStore((state) => state); - const { clearPower } = usePermissionsSlice(); + // 个人登录信息下拉菜单 + const { outLogin, userInfo } = useLoginStore((state) => state) + const { clearPower } = usePermissionsSlice() - const handleOutLogin = async () => { - await getLoginOut(); - outLogin(); - navigate('/login', { replace: true }); - clearPower(); - }; + const handleOutLogin = async () => { + await getLoginOut() + outLogin() + navigate('/login', { replace: true }) + clearPower() + } - return ( -
+ return ( +
+
+ {/* 头像 */} +
+ + {!collapsed && (
- {/* 头像 */} -
- - {!collapsed && ( -
-
- {userInfo.username ?? '未知用户'} -
-
- {userInfo.roles?.join('') ?? '未获取到该权限'} -
-
- )} -
+
{userInfo.username ?? '未知用户'}
+
+ {userInfo.roles?.join('') ?? '未获取到该权限'} +
- setOpen(open)} - content={ -
- - -
- } - title={ -
- - 确定退出登录吗? -
- } - trigger="click" + )} +
+
+ setOpen(open)} + content={ +
+ + +
+ } + title={ +
+ + 确定退出登录吗? +
+ } + trigger="click" + > +
+
- ); -}; +
+
+ ) +} -export { UserCard }; +export { UserCard } diff --git a/src/App/routers/AuthRoute.tsx b/src/App/routers/AuthRoute.tsx index 65e8d259..82a4af1e 100644 --- a/src/App/routers/AuthRoute.tsx +++ b/src/App/routers/AuthRoute.tsx @@ -1,20 +1,13 @@ -import type { FC, ReactNode } from 'react'; -import useLoginStore from '../store/loginStore'; -import { NoLoginPermission } from '@/compoments'; +import type { FC, ReactNode } from 'react' +import { Navigate } from 'react-router-dom' +import useLoginStore from '../store/loginStore' interface AuthRouteType { - children: ReactNode; + children: ReactNode } const AuthRoute: FC = ({ children }) => { - const { token } = useLoginStore((state) => state); - - // useEffect(() => { - // if (!token) { - // // dispatch(logOut(_pathname.slice(1))) - // } - // }, [token]); - - return token ? children : ; -}; -export default AuthRoute; + const { token } = useLoginStore((state) => state) + return token ? children : +} +export default AuthRoute diff --git a/src/App/routers/irify-routers.tsx b/src/App/routers/irify-routers.tsx index f1747b6d..48eb95dd 100644 --- a/src/App/routers/irify-routers.tsx +++ b/src/App/routers/irify-routers.tsx @@ -16,8 +16,9 @@ import { RuleEditor } from '@/pages/RuleManagement/RuleEditor'; import SSARiskAudit from '@/pages/SSARiskAudit'; import TaskList from '@/pages/SSAScanTask/TaskList'; import IRifyNodeManagePage from '@/pages/NodeManage/IRifyNodeManagePage'; +import IRifyScanObservabilityPage from '@/pages/NodeManage/IRifyScanObservabilityPage'; import IRifySystemManagementPage from '@/pages/SystemManagement/IRifySystemManagementPage'; -import ReportManage from '@/pages/ReportManage'; +import IRifyReportManagePage from '@/pages/IRifyReportManage'; import CompileArtifactsPage from '@/pages/CompileArtifacts/CompileArtifactsPage'; import IRifyDashboard from '@/pages/IRifyDashboard'; @@ -110,7 +111,7 @@ const irifyRouters: RouteObject[] = [ }, { path: 'reports', - element: , + element: , }, { path: 'node-config', @@ -123,6 +124,10 @@ const irifyRouters: RouteObject[] = [ path: 'manage', element: , }, + { + path: 'observability', + element: , + }, ], }, { diff --git a/src/App/routers/routers.tsx b/src/App/routers/routers.tsx index d8bee8a4..1f78cab7 100644 --- a/src/App/routers/routers.tsx +++ b/src/App/routers/routers.tsx @@ -1,365 +1,695 @@ -import type { RouteObject } from 'react-router-dom'; -import AppLayout from '../AppLayout'; -import type { ReactNode } from 'react'; -import AuthRoute from './AuthRoute'; -import Login from '@/pages/Login'; +import type { RouteObject } from 'react-router-dom' +import AppLayout from '../AppLayout' +import type { ReactNode } from 'react' +import AuthRoute from './AuthRoute' +// import Login from '@/pages/Login' import { - DataServiceIcon, - NodeConfigIcon, - PublicCodec, - ReportManageIcon, - SystemManagementIcon, - TaskCenterIcon, -} from '@/assets/menu'; + DataServiceIcon, + NodeConfigIcon, + PublicCodec, + ReportManageIcon, + SystemManagementIcon, + TaskCenterIcon, + AIOutlinedIcon, +} from '@/assets/menu' -import { NetworkError } from '@/pages/NetworkError'; +import { NetworkError } from '@/pages/NetworkError' -import TaskPageList from '@/pages/TaskPageList'; -import TaskScript from '@/pages/TaskScript'; -import TaskDetail from '@/pages/TaskDetail'; -import ReportManage from '@/pages/ReportManage'; -import NodeConfig from '@/pages/NodeConfig'; -import SystemManagement from '@/pages/SystemManagement'; -import { PortAssets } from '@/pages/DataService/PortAssets'; -import { AssetsVulns } from '@/pages/DataService/AssetsVulns'; -import { SensitiveMessage } from '@/pages/DataService/SensitiveMessage'; -import { CveLoophole } from '@/pages/DataService/CveLoophole'; -import NodeManagePage from '@/pages/NodeManage'; -import License from '@/pages/License'; -import { ModifyTaskScript } from '@/pages/TaskScript/taskScript/ModifyTaskScript'; -import ActiChainDNS from '@/pages/ActiChainDNS'; -import ICMPSize from '@/pages/ICMPSize'; -import TCPLog from '@/pages/TCPLog'; +import TaskPageList from '@/pages/TaskPageList' +import TaskScript from '@/pages/TaskScript' +import TaskDetail from '@/pages/TaskDetail' +import ReportManage from '@/pages/ReportManage' +import NodeConfig from '@/pages/NodeConfig' +import SystemManagement from '@/pages/SystemManagement' +import { PortAssets } from '@/pages/DataService/PortAssets' +import { AssetsVulns } from '@/pages/DataService/AssetsVulns' +import { SensitiveMessage } from '@/pages/DataService/SensitiveMessage' +import { CveLoophole } from '@/pages/DataService/CveLoophole' +import NodeManagePage from '@/pages/NodeManage' +import License from '@/pages/License' +import { ModifyTaskScript } from '@/pages/TaskScript/taskScript/ModifyTaskScript' +import ActiChainDNS from '@/pages/ActiChainDNS' +import ICMPSize from '@/pages/ICMPSize' +import TCPLog from '@/pages/TCPLog' // import MessageCollect from '@/pages/MessageCollect'; -import { ReverseLinkServerFacadeServer } from '@/pages/ReverseLinkServer/ReverseLinkServerFacadeServer'; -import GlobalReverseLink from '@/pages/ReverseLink'; -import { ReverseLinkServer } from '@/pages/ReverseLinkServer/ReverseLinkServer'; -import ApiOutlinedIcon from '@/assets/menu/ApiOutlinedIcon'; -import CodecEntry from '@/pages/Codec'; -import SpecialTask from '@/pages/SpecialTask'; -import CreateTask from '@/pages/CreateTask'; -import { ProjectManagement, ProjectEditor, CreateProject } from '@/pages/ProjecManagement'; -import RuleManagement from '@/pages/RuleManagement'; -import { RuleEditor } from '@/pages/RuleManagement/RuleEditor'; -// SSARisk pages removed - now using SSARiskAudit -import SSARiskAudit from '@/pages/SSARiskAudit'; -import TaskList from '@/pages/SSAScanTask/TaskList'; +import { ReverseLinkServerFacadeServer } from '@/pages/ReverseLinkServer/ReverseLinkServerFacadeServer' +import GlobalReverseLink from '@/pages/ReverseLink' +import { ReverseLinkServer } from '@/pages/ReverseLinkServer/ReverseLinkServer' +import ApiOutlinedIcon from '@/assets/menu/ApiOutlinedIcon' +import CodecEntry from '@/pages/Codec' +import SpecialTask from '@/pages/SpecialTask' +import CreateTask from '@/pages/CreateTask' +// import { ProjectManagement, ProjectEditor, CreateProject } from '@/pages/ProjecManagement' +// import RuleManagement from '@/pages/RuleManagement' +// import { RuleEditor } from '@/pages/RuleManagement/RuleEditor' +// import SSARiskAudit from '@/pages/SSARiskAudit' +// import TaskList from '@/pages/SSAScanTask/TaskList' +import { AIAgent } from '@/pages/AIAgent/ai-agent/AIAgent' +import AIAgentLog from '@/pages/AIAgent/ai-agent-log/AIAgentLog' +// import { TestAi } from '@/apis/AiEventApi/test' +import ForgeEditor from '@/pages/AIAgent/ai-agent/aiForge/forgeEditor/ForgeEditor' +import { YakitRoute } from '@/pages/AIAgent/enums/yakitRoute' +import Login from '@/pages/Login' // 继承路由接口,增加name字段 type RouteObjectRootMy = RouteObject & { - name?: string; - children?: RouteObjectRootMy[]; - key?: string; - icon?: ReactNode; - hidden?: boolean; -}; + name?: string + children?: RouteObjectRootMy[] + key?: string + icon?: ReactNode + hidden?: boolean + parentPath?: string + parentpath?: string +} const routers: RouteObjectRootMy[] = [ - { - path: '/', - element: ( - - - - ), - children: [ - { - path: 'task', - parentPath: '/', - name: '任务中心', - key: 'task', - icon: , - children: [ - { - path: 'new-create-task', - key: 'new-create-task', - name: '新建扫描', - element: , - hidden: false, - parentpath: '/task/new-create-task', - }, - { - path: 'special-task', - key: 'special-task', - name: '专项扫描', - element: , - hidden: false, - parentpath: '/task/special-task', - }, - { - path: 'task-list', - name: '任务列表', - key: 'task-list', - children: [ - { - index: true, - element: , - }, - { - path: 'detail', - key: 'task_detail', - element: , - name: '任务详情', - hidden: true, - parentpath: '/task/task-list', - }, - ], - }, - ], - }, - { - path: 'report-manage', - name: '报告管理', - key: 'project', - icon: , - element: , - }, - { - path: 'data-service', - name: '资产数据', - key: 'data-service', - icon: , - children: [ - { - path: 'port', - name: '端口资产', - key: 'port-assets', - element: , - }, - { - path: 'assets-vulns', - name: '漏洞与风险', - key: 'assets-vulns', - element: , - children: [ - { - path: 'audit-code', - name: '代码审计', - key: 'assets-vulns-audit-code', - element: null, - }, - ], - }, - { - path: 'sensitive-message', - name: '敏感信息', - key: 'sensitive-message', - element: , - }, - // { - // path: 'message-collect', - // name: '信息收集', - // key: 'message-collect', - // element: , - // }, - ], - }, - { - path: 'codec', - name: 'Codec', - key: 'codec', - icon: , - element: , - }, - { - path: 'acti-chain', - name: '反连', - key: 'acti-chain', - icon: , - children: [ - { - path: 'dns', - name: 'DNS Log', - key: 'acti-chain-dns', - element: , - }, - { - path: 'ICMP', - name: 'ICMP Size', - key: 'icmp-size-logger', - element: , - }, - { - path: 'tcp', - name: 'TCP-ProtLog', - key: 'tcp-prot-logger', - element: , - }, - { - path: 'reverse-link-server', - name: '反连服务器', - key: 'reverse-link-server', - element: , - }, - { - path: 'reverse-link-server/facade-server', - name: '反连服务器', - key: 'reverse-link-server', - element: , - hidden: true, - }, - ], - }, - { - path: 'node-config', - name: '节点配置', - key: 'node', - icon: , - children: [ - { - path: 'install', - name: '节点安装', - key: 'node-intsall', - element: , - }, - { - path: 'manage', - name: '节点管理', - key: 'node-manage', - element: , - }, - ], - }, - { - path: 'system-management', - name: '系统管理', - key: 'system', - icon: , - children: [ - { - path: 'userinfo', - name: '用户管理', - key: 'system-user', - element: , - }, + { + path: '/', + element: ( + + + + ), + // children: [ + // { + // path: 'task', + // parentPath: '/', + // name: '任务中心', + // key: 'task', + // icon: , + // children: [ + // { + // path: 'new-create-task', + // key: 'new-create-task', + // name: '新建扫描', + // element: , + // hidden: false, + // parentpath: '/task/new-create-task', + // }, + // { + // path: 'special-task', + // key: 'special-task', + // name: '专项扫描', + // element: , + // hidden: false, + // parentpath: '/task/special-task', + // }, + // { + // path: 'task-list', + // name: '任务列表', + // key: 'task-list', + // children: [ + // { + // index: true, + // element: , + // }, + // { + // path: 'detail', + // key: 'task_detail', + // element: , + // name: '任务详情', + // hidden: true, + // parentpath: '/task/task-list', + // }, + // ], + // }, + // ], + // }, + // { + // path: 'report-manage', + // name: '报告管理', + // key: 'project', + // icon: , + // element: , + // }, + // { + // path: 'data-service', + // name: '资产数据', + // key: 'data-service', + // icon: , + // children: [ + // { + // path: 'port', + // name: '端口资产', + // key: 'port-assets', + // element: , + // }, + // { + // path: 'assets-vulns', + // name: '漏洞与风险', + // key: 'assets-vulns', + // element: , + // children: [ + // { + // path: 'audit-code', + // name: '代码审计', + // key: 'assets-vulns-audit-code', + // element: null, + // }, + // ], + // }, + // { + // path: 'sensitive-message', + // name: '敏感信息', + // key: 'sensitive-message', + // element: , + // }, + // // { + // // path: 'message-collect', + // // name: '信息收集', + // // key: 'message-collect', + // // element: , + // // }, + // ], + // }, + // { + // path: 'codec', + // name: 'Codec', + // key: 'codec', + // icon: , + // element: , + // }, + // { + // path: 'acti-chain', + // name: '反连', + // key: 'acti-chain', + // icon: , + // children: [ + // { + // path: 'dns', + // name: 'DNS Log', + // key: 'acti-chain-dns', + // element: , + // }, + // { + // path: 'ICMP', + // name: 'ICMP Size', + // key: 'icmp-size-logger', + // element: , + // }, + // { + // path: 'tcp', + // name: 'TCP-ProtLog', + // key: 'tcp-prot-logger', + // element: , + // }, + // { + // path: 'reverse-link-server', + // name: '反连服务器', + // key: 'reverse-link-server', + // element: , + // }, + // { + // path: 'reverse-link-server/facade-server', + // name: '反连服务器', + // key: 'reverse-link-server', + // element: , + // hidden: true, + // }, + // ], + // }, + // { + // path: 'node-config', + // name: '节点配置', + // key: 'node', + // icon: , + // children: [ + // { + // path: 'install', + // name: '节点安装', + // key: 'node-intsall', + // element: , + // }, + // { + // path: 'manage', + // name: '节点管理', + // key: 'node-manage', + // element: , + // }, + // ], + // }, + // { + // path: 'system-management', + // name: '系统管理', + // key: 'system', + // icon: , + // children: [ + // { + // path: 'userinfo', + // name: '用户管理', + // key: 'system-user', + // element: , + // }, - { - path: 'task-script', - name: '脚本管理', - key: 'task-script', - children: [ - { - index: true, - element: , - }, - { - path: 'modify-task-script', - key: 'modify-task-script', - element: , - name: '分布式任务脚本', - hidden: true, - parentpath: '/task-script/task-script', - }, - ], - }, - { - path: 'cve-loophole', - name: '漏洞库管理', - key: 'cve-loophole', - element: , - }, - { - name: '全局反连', - path: 'global-reverse-link', - key: 'system-global-reverse-link', - element: , - }, - ], - }, - { - path: 'static-analysis', - name: '静态代码分析', - key: 'static-analysis', - icon: , - children: [ - { - path: 'project-management', - name: '项目管理', - key: 'static-project', - children: [ - { - index: true, - element: , - }, - { - path: 'create', - key: 'static-project-create', - element: , - name: '新建项目', - hidden: true, - parentpath: - '/static-analysis/project-management', - }, - { - path: 'edit', - key: 'static-project-edit', - element: , - name: '编辑项目', - hidden: true, - parentpath: - '/static-analysis/project-management', - }, - ], - }, - { - path: 'rule-management', - name: '规则管理', - key: 'static-rule', - children: [ - { - index: true, - element: , - }, - { - path: 'create', - key: 'static-rule-create', - element: , - name: '新建规则', - hidden: true, - parentpath: '/static-analysis/rule-management', - }, - ], - }, - { - path: 'ssa-risk', - name: '漏洞管理', - key: 'static-ssa-risk', - children: [ - { - path: 'audit', - key: 'static-ssa-risk-audit', - element: , - name: '缺陷审计', - hidden: true, - parentpath: '/static-analysis/ssa-risk', - }, - ], - }, - { - path: 'task-list', - name: '任务列表', - key: 'static-task-list', - element: , - }, - ], - }, + // { + // path: 'task-script', + // name: '脚本管理', + // key: 'task-script', + // children: [ + // { + // index: true, + // element: , + // }, + // { + // path: 'modify-task-script', + // key: 'modify-task-script', + // element: , + // name: '分布式任务脚本', + // hidden: true, + // parentpath: '/task-script/task-script', + // }, + // ], + // }, + // { + // path: 'cve-loophole', + // name: '漏洞库管理', + // key: 'cve-loophole', + // element: , + // }, + // { + // name: '全局反连', + // path: 'global-reverse-link', + // key: 'system-global-reverse-link', + // element: , + // }, + // ], + // }, + // { + // path: 'static-analysis', + // name: '静态代码分析', + // key: 'static-analysis', + // icon: , + // children: [ + // { + // path: 'project-management', + // name: '项目管理', + // key: 'static-project', + // children: [ + // { + // index: true, + // element: , + // }, + // { + // path: 'create', + // key: 'static-project-create', + // element: , + // name: '新建项目', + // hidden: true, + // parentpath: '/static-analysis/project-management', + // }, + // { + // path: 'edit', + // key: 'static-project-edit', + // element: , + // name: '编辑项目', + // hidden: true, + // parentpath: '/static-analysis/project-management', + // }, + // ], + // }, + // { + // path: 'rule-management', + // name: '规则管理', + // key: 'static-rule', + // children: [ + // { + // index: true, + // element: , + // }, + // { + // path: 'create', + // key: 'static-rule-create', + // element: , + // name: '新建规则', + // hidden: true, + // parentpath: '/static-analysis/rule-management', + // }, + // ], + // }, + // { + // path: 'ssa-risk', + // name: '漏洞管理', + // key: 'static-ssa-risk', + // children: [ + // { + // path: 'audit', + // key: 'static-ssa-risk-audit', + // element: , + // name: '缺陷审计', + // hidden: true, + // parentpath: '/static-analysis/ssa-risk', + // }, + // ], + // }, + // { + // path: 'task-list', + // name: '任务列表', + // key: 'static-task-list', + // element: , + // }, + // ], + // }, + // { + // name: 'AIAgent', + // path: YakitRoute.AI_Agent, + // key: YakitRoute.AI_Agent, + // icon: , + // element: , + // }, + // { + // path: YakitRoute.AddAIForge, + // name: '新增Forge', + // key: YakitRoute.AddAIForge, + // element: , + // hidden: true, + // }, + // { + // path: YakitRoute.ModifyAIForge, + // name: '编辑Forge', + // key: YakitRoute.ModifyAIForge, + // element: , + // hidden: true, + // }, + // ], + children: [ + { + path: 'task', + parentPath: '/', + name: '任务中心', + key: 'task', + icon: , + children: [ + { + path: 'new-create-task', + key: 'new-create-task', + name: '新建扫描', + element: , + hidden: false, + parentpath: '/task/new-create-task', + }, + { + path: 'special-task', + key: 'special-task', + name: '专项扫描', + element: , + hidden: false, + parentpath: '/task/special-task', + }, + { + path: 'task-list', + name: '任务列表', + key: 'task-list', + children: [ + { + index: true, + element: , + }, + { + path: 'detail', + key: 'task_detail', + element: , + name: '任务详情', + hidden: true, + parentpath: '/task/task-list', + }, + ], + }, + ], + }, + { + path: 'report-manage', + name: '报告管理', + key: 'project', + icon: , + element: , + }, + { + path: 'data-service', + name: '资产数据', + key: 'data-service', + icon: , + children: [ + { + path: 'port', + name: '端口资产', + key: 'port-assets', + element: , + }, + { + path: 'assets-vulns', + name: '漏洞与风险', + key: 'assets-vulns', + element: , + children: [ + { + path: 'audit-code', + name: '代码审计', + key: 'assets-vulns-audit-code', + element: null, + }, + ], + }, + { + path: 'sensitive-message', + name: '敏感信息', + key: 'sensitive-message', + element: , + }, + ], + }, + { + path: 'codec', + name: 'Codec', + key: 'codec', + icon: , + element: , + }, + { + path: 'acti-chain', + name: '反连', + key: 'acti-chain', + icon: , + children: [ + { + path: 'dns', + name: 'DNS Log', + key: 'acti-chain-dns', + element: , + }, + { + path: 'ICMP', + name: 'ICMP Size', + key: 'icmp-size-logger', + element: , + }, + { + path: 'tcp', + name: 'TCP-ProtLog', + key: 'tcp-prot-logger', + element: , + }, + { + path: 'reverse-link-server', + name: '反连服务器', + key: 'reverse-link-server', + element: , + }, + { + path: 'reverse-link-server/facade-server', + name: '反连服务器', + key: 'reverse-link-server', + element: , + hidden: true, + }, + ], + }, + { + path: 'node-config', + name: '节点配置', + key: 'node', + icon: , + children: [ + { + path: 'install', + name: '节点安装', + key: 'node-intsall', + element: , + }, + { + path: 'manage', + name: '节点管理', + key: 'node-manage', + element: , + }, + ], + }, + { + path: 'system-management', + name: '系统管理', + key: 'system', + icon: , + children: [ + { + path: 'userinfo', + name: '用户管理', + key: 'system-user', + element: , + }, + { + path: 'task-script', + name: '脚本管理', + key: 'task-script', + children: [ + { + index: true, + element: , + }, + { + path: 'modify-task-script', + key: 'modify-task-script', + element: , + name: '分布式任务脚本', + hidden: true, + parentpath: '/task-script/task-script', + }, + ], + }, + { + path: 'cve-loophole', + name: '漏洞库管理', + key: 'cve-loophole', + element: , + }, + { + name: '全局反连', + path: 'global-reverse-link', + key: 'system-global-reverse-link', + element: , + }, ], - }, - { - path: '/login', - element: , - }, - { - path: '/license', - element: , - }, + }, + // { + // path: 'static-analysis', + // name: '静态代码分析', + // key: 'static-analysis', + // icon: , + // children: [ + // { + // path: 'project-management', + // name: '项目管理', + // key: 'static-project', + // children: [ + // { + // index: true, + // element: , + // }, + // { + // path: 'create', + // key: 'static-project-create', + // element: , + // name: '新建项目', + // hidden: true, + // parentpath: '/static-analysis/project-management', + // }, + // { + // path: 'edit', + // key: 'static-project-edit', + // element: , + // name: '编辑项目', + // hidden: true, + // parentpath: '/static-analysis/project-management', + // }, + // ], + // }, + // { + // path: 'rule-management', + // name: '规则管理', + // key: 'static-rule', + // children: [ + // { + // index: true, + // element: , + // }, + // { + // path: 'create', + // key: 'static-rule-create', + // element: , + // name: '新建规则', + // hidden: true, + // parentpath: '/static-analysis/rule-management', + // }, + // ], + // }, + // { + // path: 'ssa-risk', + // name: '漏洞管理', + // key: 'static-ssa-risk', + // children: [ + // { + // path: 'audit', + // key: 'static-ssa-risk-audit', + // element: , + // name: '缺陷审计', + // hidden: true, + // parentpath: '/static-analysis/ssa-risk', + // }, + // ], + // }, + // { + // path: 'task-list', + // name: '任务列表', + // key: 'static-task-list', + // element: , + // }, + // ], + // }, + { + name: 'AIAgent', + path: YakitRoute.AI_Agent, + key: YakitRoute.AI_Agent, + icon: , + element: , + }, + { + path: YakitRoute.AddAIForge, + name: '新增Forge', + key: YakitRoute.AddAIForge, + element: , + hidden: true, + }, + { + path: YakitRoute.ModifyAIForge, + name: '编辑Forge', + key: YakitRoute.ModifyAIForge, + element: , + hidden: true, + }, + ], + }, + { + path: '/login', + element: , + // element: , + }, + { + path: '/license', + element: , + }, - { - path: '/network-err', - element: , - }, -]; + { + path: '/network-err', + element: , + }, + { + path: '/agent-log', + element: ( + + + + ), + }, +] -export type { RouteObjectRootMy }; +export type { RouteObjectRootMy } -export default routers; +export default routers diff --git a/src/App/store/loginStore.ts b/src/App/store/loginStore.ts index ec8cda43..973ddb20 100644 --- a/src/App/store/loginStore.ts +++ b/src/App/store/loginStore.ts @@ -1,36 +1,36 @@ -import { postLogin } from '@/apis/login'; -import type { PostRequestAuth } from '@/apis/login/types'; -import { create } from 'zustand'; +import { postLogin } from '@/apis/login' +import type { PostRequestAuth } from '@/apis/login/types' +import { create } from 'zustand' interface LoginStore { - userInfo: Partial; - token?: string; - login: (param: any) => Promise; - outLogin: () => Promise; + userInfo: Partial + token?: string + login: (param: any) => Promise + outLogin: () => Promise } const useLoginStore = create()((set) => ({ - userInfo: JSON?.parse(String(localStorage?.getItem('userInfo'))) || {}, - token: localStorage.getItem('token') || undefined, - login: async (param) => { - await postLogin(param).then((res) => { - const { data } = res; - const { token, user_info } = data; - if (token) { - localStorage.setItem('userInfo', JSON.stringify(user_info)); - localStorage.setItem('token', token); - } + userInfo: JSON?.parse(String(localStorage?.getItem('userInfo'))) || {}, + token: localStorage.getItem('token') || undefined, + login: async (param) => { + await postLogin(param).then((res) => { + const { data } = res + const { token, user_info } = data + if (token) { + localStorage.setItem('userInfo', JSON.stringify(user_info)) + localStorage.setItem('token', token) + } - set({ - userInfo: { ...user_info }, - token: token, - }); - }); - }, - outLogin: async () => { - localStorage.clear(); - set({ userInfo: {}, token: '' }); - }, -})); + set({ + userInfo: { ...user_info }, + token: token, + }) + }) + }, + outLogin: async () => { + localStorage.clear() + set({ userInfo: {}, token: '' }) + }, +})) -export default useLoginStore; +export default useLoginStore diff --git a/src/apiUtils/type.d.ts b/src/apiUtils/type.d.ts index c4aae41c..1fcf51ab 100644 --- a/src/apiUtils/type.d.ts +++ b/src/apiUtils/type.d.ts @@ -1,9 +1,6 @@ /** @name 必传参的异步方法 */ -export type APIFunc = (params: T, hiddenError?: boolean) => Promise; +export type APIFunc = (params: T, hiddenError?: boolean) => Promise /** @name 选传参的异步方法 */ -export type APIOptionalFunc = ( - params?: T, - hiddenError?: boolean, -) => Promise; +export type APIOptionalFunc = (params?: T, hiddenError?: boolean) => Promise /** @name 无参的异步方法 */ -export type APINoRequestFunc = (hiddenError?: boolean) => Promise; +export type APINoRequestFunc = (hiddenError?: boolean) => Promise diff --git a/src/apis/ActiChainApi/index.ts b/src/apis/ActiChainApi/index.ts index 6d9fc72e..57f7692b 100644 --- a/src/apis/ActiChainApi/index.ts +++ b/src/apis/ActiChainApi/index.ts @@ -1,88 +1,68 @@ -import axios from '@/utils/axios'; -import type { ResponseData } from '@/utils/commonTypes'; +import axios from '@/utils/axios' +import type { ResponseData } from '@/utils/commonTypes' import type { - TGetDnsQuryRequest, - TgetTunnelServerRequest, - TIcmpGenerateRequest, - TReverseDnsGenerateRequest, - TReverseDnsGenerateResponse, - TReverseStartFacadesRequest, - TTcpGenerateRequest, -} from './type'; + TGetDnsQuryRequest, + TgetTunnelServerRequest, + TIcmpGenerateRequest, + TReverseDnsGenerateRequest, + TReverseDnsGenerateResponse, + TReverseStartFacadesRequest, + TTcpGenerateRequest, +} from './type' // 获取内置 DNSLog 配置选项 -const getQuerySupportedDnsLogPlatforms = (): Promise< - ResponseData<{ list: Array }> -> => - axios.get }>>( - '/reverse/QuerySupportedDnsLogPlatforms', - ); +const getQuerySupportedDnsLogPlatforms = (): Promise }>> => + axios.get }>>('/api/reverse/QuerySupportedDnsLogPlatforms') // dns 获取配置 -const postReverseDnsGenerate = ( - data: TReverseDnsGenerateRequest, -): Promise> => - axios.post>( - '/reverse/dns/generate', - data, - ); +const postReverseDnsGenerate = (data: TReverseDnsGenerateRequest): Promise> => + axios.post>('/api/reverse/dns/generate', data) // 创建需要监听的域名 const getDnsQury = (data: TGetDnsQuryRequest): Promise> => - axios.post>(`/reverse/dns/query`, data); + axios.post>(`/api/reverse/dns/query`, data) // 断开需要监听的域名 const postSseDelete = (data: { key: string }): Promise> => - axios.post>(`/sse/delete`, data); + axios.post>(`/api/sse/delete`, data) // 生成 icmp 反链长度 const getIcmpGenerate = (): Promise> => - axios.get>( - '/reverse/icmp/generate', - ); + axios.get>('/api/reverse/icmp/generate') // 查询icmp结果,用sse返回 -const getIcmpQuery = (params: { - length: number; -}): Promise> => - axios.get>('/reverse/icmp/query', { params }); +const getIcmpQuery = (params: { length: number }): Promise> => + axios.get>('/api/reverse/icmp/query', { + params, + }) // 生成 icmp 反链长度 const getTcpGenerate = (): Promise> => - axios.get>( - '/reverse/tcp/generate', - ); + axios.get>('/api/reverse/tcp/generate') // 查询icmp结果,用sse返回 -const getTcoQuery = (params: { - token: string; -}): Promise> => - axios.get>('/reverse/tcp/query', { params }); +const getTcoQuery = (params: { token: string }): Promise> => + axios.get>('/api/reverse/tcp/query', { + params, + }) // 反连服务器 启动facedeServer -const postReverseStartFacades = ( - data: TReverseStartFacadesRequest, -): Promise> => - axios.post>('/reverse/start/facades', data); +const postReverseStartFacades = (data: TReverseStartFacadesRequest): Promise> => + axios.post>('/api/reverse/start/facades', data) // 反连服务器 获取监听地址 /reverse/get-tunnel-server -const getTunnelServer = ( - data: TgetTunnelServerRequest, -): Promise> => - axios.post>( - '/reverse/get_tunnel_server', - data, - ); +const getTunnelServer = (data: TgetTunnelServerRequest): Promise> => + axios.post>('/api/reverse/get_tunnel_server', data) export { - postReverseDnsGenerate, - getQuerySupportedDnsLogPlatforms, - getDnsQury, - postSseDelete, - getIcmpGenerate, - getIcmpQuery, - getTcpGenerate, - getTcoQuery, - postReverseStartFacades, - getTunnelServer, -}; + postReverseDnsGenerate, + getQuerySupportedDnsLogPlatforms, + getDnsQury, + postSseDelete, + getIcmpGenerate, + getIcmpQuery, + getTcpGenerate, + getTcoQuery, + postReverseStartFacades, + getTunnelServer, +} diff --git a/src/apis/AiEngineAdminApi/index.ts b/src/apis/AiEngineAdminApi/index.ts new file mode 100644 index 00000000..766a89a8 --- /dev/null +++ b/src/apis/AiEngineAdminApi/index.ts @@ -0,0 +1,65 @@ +import axios from '@/utils/axios' +import type { ResponseData } from '@/utils/commonTypes' +import { syncAIEngineAuth } from '@/utils/aiEngineAuth' + +export interface AIEngineStatus { + base_url?: string + command?: string[] + gateway_url?: string + host?: string + jwt_secret?: string + last_error?: string + pid?: number + port?: number + route_prefix?: string + running: boolean + started_at?: number +} + +type AIEngineStatusPayload = AIEngineStatus | ResponseData + +const AI_ENGINE_STATUS_CACHE_MS = 1000 + +let cachedAIEngineStatus: AIEngineStatus | undefined +let cachedAIEngineStatusAt = 0 +let pendingAIEngineStatusRequest: Promise | null = null + +const normalizeAIEngineStatus = (payload: AIEngineStatusPayload): AIEngineStatus => { + if (payload && typeof payload === 'object' && 'data' in payload && payload.data) { + return payload.data + } + return payload as AIEngineStatus +} + +const requestAIEngineStatus = async (url: string, method: 'get' | 'post') => { + const payload = await axios.request({ + url, + method, + skipBizCodeCheck: true, + }) + const status = syncAIEngineAuth(normalizeAIEngineStatus(payload)) + cachedAIEngineStatus = status + cachedAIEngineStatusAt = Date.now() + + return status +} + +const getAIEngineStatus = (): Promise => { + if (cachedAIEngineStatus && Date.now() - cachedAIEngineStatusAt < AI_ENGINE_STATUS_CACHE_MS) { + return Promise.resolve(cachedAIEngineStatus) + } + + if (!pendingAIEngineStatusRequest) { + pendingAIEngineStatusRequest = requestAIEngineStatus('/api/admin/ai-engine/status', 'get').finally(() => { + pendingAIEngineStatusRequest = null + }) + } + + return pendingAIEngineStatusRequest +} + +const postAIEngineStart = (): Promise => requestAIEngineStatus('/api/admin/ai-engine/start', 'post') + +const postAIEngineStop = (): Promise => requestAIEngineStatus('/api/admin/ai-engine/stop', 'post') + +export { getAIEngineStatus, postAIEngineStart, postAIEngineStop } diff --git a/src/apis/AiEventApi/index.ts b/src/apis/AiEventApi/index.ts new file mode 100644 index 00000000..53dad39d --- /dev/null +++ b/src/apis/AiEventApi/index.ts @@ -0,0 +1,253 @@ +import axios from '@/utils/axios' +import type { AIDelSessionParam, AIResponseData } from '@/utils/commonTypes' +import type { + TPostCreateSessionRequest, + TPostCreateSessionResponse, + TPostSendFirstMessageResponse, + TGetSettingResponse, + TPostSettingRequest, + TPostCancelMessageResponse, + TPostSendContinueMessageResponse, + GetGlobalNetworkConfigResponse, + GlobalNetworkConfig, + ListAIProvidersResponse, + TPostSettingAimodelsGetRequest, + TPostSettingAimodelsGetResponse, + TPostSettingAifocusGetResponse, + GetSessionAllResponse, + TPostSessionTitle, +} from './type' +import type { AIInputEvent } from '@/pages/AIAgent/ai-re-act/hooks/grpcApi' +import type { + AIConfigHealthCheckRequest, + AIConfigHealthCheckResponse, + AIGlobalConfig, + QueryAIProvidersRequest, + QueryAIProvidersResponse, +} from '@/pages/AIAgent/ai-agent/aiModelList/utils' +import type { GetThirdPartyAppConfigTemplateResponse } from '@/compoments/configNetwork/NewThirdPartyApplicationConfig' +import type { + AIForge, + AIForgeFilter, + GetAIForgeRequest, + QueryAIForgeRequest, + QueryAIForgeResponse, +} from '@/pages/AIAgent/ai-agent/type/forge' +import type { + ExportAIForgeRequest, + ExportImportAIForgeProgress, + ImportAIForgeRequest, +} from '@/pages/AIAgent/ai-agent/forgeName/type' +import useLoginStore from '@/App/store/loginStore' +import { getAIEngineAuthorizationHeader, isAIEnginePath, resolveAIEngineRequestURL } from '@/utils/aiEngineAuth' + +/** 通用 POST SSE 流式请求:发起 POST 后逐条解析 SSE data 帧并回调 */ +async function fetchSSEPost( + url: string, + data: TReq, + onProgress?: (progress: TProgress) => void, + signal?: AbortSignal, +): Promise { + const store = useLoginStore.getState() + const headers: HeadersInit = { + 'Content-Type': 'application/json;charset=UTF-8', + Accept: 'text/event-stream', + } + + if (isAIEnginePath(url)) { + const aiEngineAuthorization = await getAIEngineAuthorizationHeader() + if (aiEngineAuthorization) { + headers.Authorization = aiEngineAuthorization + } + } else if (store.token) { + headers.Authorization = store.token + } + + const response = await fetch(resolveAIEngineRequestURL(url) || url, { + method: 'POST', + headers, + body: JSON.stringify(data), + credentials: 'include', + signal, + }) + + if (!response.ok) { + throw new Error(`请求失败(${response.status})`) + } + + if (!response.body) return null + + const reader = response.body.getReader() + const decoder = new TextDecoder('utf-8') + let buffer = '' + + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const frames = buffer.split(/\r?\n\r?\n/g) + buffer = frames.pop() || '' + + for (const frame of frames) { + const dataLines = frame + .split(/\r?\n/g) + .filter((line) => line.startsWith('data:')) + .map((line) => line.slice(5).trim()) + + if (!dataLines.length) continue + const payload = dataLines.join('\n') + if (!payload || payload === '[DONE]') continue + + try { + onProgress?.(JSON.parse(payload) as TProgress) + } catch { + // 忽略非 JSON 帧 + } + } + } + + return null +} + +// #region 会话Session相关接口 +/** 创建会话通道, 并生成session(也可传 run_id 恢复已有会话) */ +const postCreateSession = (data?: TPostCreateSessionRequest): Promise => + axios.post('/agent/session', data) + +/** 获取会话列表(内存运行态 + DB 元数据) */ +const getSessionAll = (): Promise => + axios.get(`/agent/session/all`) + +/** 更新会话标题 */ +const postSessionTitle = (session: string, title: string): Promise> => + axios.post>(`/agent/session/${session}/title`, { title }) + +/** 删除会话 */ +const deleteSession = (data: AIDelSessionParam): Promise> => + axios.post>(`/agent/session/del`, data) + +// #endregion + +// #region 会话流相关接口 +/** 首条输入内容 */ +const postSendFirstMessage = ( + session: string, + data: AIInputEvent, +): Promise> => + axios.post>(`/agent/run/${session}`, data) + +/** 继续输入 */ +const postSendContinueMessage = ( + session: string, + data: AIInputEvent, +): Promise> => + axios.post>(`/agent/run/${session}/events/push`, data) + +/** 主动取消 */ +const postCancelMessage = (session: string): Promise => + axios.post(`/agent/run/${session}/cancel`, {}) +// #endregion + +// #region 配置相关接口 +/** 读取当前 AI Agent 聊天配置 */ +const getSetting = (): Promise => axios.get(`/agent/setting`) + +/** 获取全局网络配置 */ +const getSettingGlobal = (): Promise> => + axios.get>(`/agent/setting/global`) +// #endregion + +// 更新(Patch)配置 +const postSetting = (data: TPostSettingRequest): Promise> => + axios.post>(`/agent/setting`, data) + +// 保存全局网络配置 +const postSettingGlobal = (data: GlobalNetworkConfig): Promise> => + axios.post>(`/agent/setting/global`, data) + +// 获取 provider 列表 +const postSettingProvidersGet = (): Promise => + axios.post(`/agent/setting/providers/get`, {}) + +// 按 provider 配置拉取模型列表 +const postSettingAimodelsGet = (data: TPostSettingAimodelsGetRequest): Promise => + axios.post(`/agent/setting/aimodels/get`, data) + +// 获取 focus mode 列表 +const postSettingAifocusGet = (data?: any): Promise> => + axios.post>(`/agent/setting/aifocus/get`, data || {}) + +// 获取 AI 全局配置 +const getSettingAiconfig = (): Promise => axios.get(`/agent/setting/aiconfig`) + +// 更新 AI 全局配置 +const postSettingAiconfig = (data: AIGlobalConfig): Promise => + axios.post(`/agent/setting/aiconfig`, data) + +// 执行 AI 配置健康检查 +const postAIConfigHealthCheck = (data: AIConfigHealthCheckRequest): Promise => + axios.post(`/agent/setting/aiconfig/healthcheck`, data) + +// 获取 AI 第三方应用配置模板 +const postSettingAppconfigsTemplateGet = (): Promise => + axios.post(`/agent/setting/appconfigs/template/get`, {}) + +// 分页查询 AI Provider +const postSettingProvidersQuery = (data: QueryAIProvidersRequest): Promise => + axios.post(`/agent/setting/providers/query`, data) + +// 分页查询 AI Forge +export const postAiforgeQuery = (data: QueryAIForgeRequest): Promise => + axios.post(`/agent/forge/query`, data) + +// 按名称或 ID 获取单个 AI Forge +export const postAiforgeGet = (data: GetAIForgeRequest): Promise => + axios.post(`/agent/forge/get`, data) + +// 删除 AI Forge +export const postAiforgeDelete = (data: AIForgeFilter): Promise => + axios.post(`/agent/forge/delete`, data) + +// 创建 AI Forge +export const postAiforgeCreate = (data: AIForge): Promise<{ CreateID: number }> => + axios.post(`/agent/forge/create`, data) + +// 更新 AI Forge +export const postAiforgeUpdate = (data: AIForge): Promise => + axios.post(`/agent/forge/update`, data) + +// 导出一个或多个 AI Forge(SSE 流) +export const postAiforgeExport = ( + data: ExportAIForgeRequest, + onProgress?: (progress: ExportImportAIForgeProgress) => void, + signal?: AbortSignal, +): Promise => fetchSSEPost('/agent/forge/export', data, onProgress, signal) + +// 导入 AI Forge(SSE 流) +export const postAiforgeImport = ( + data: ImportAIForgeRequest, + onProgress?: (progress: ExportImportAIForgeProgress) => void, + signal?: AbortSignal, +): Promise => fetchSSEPost('/agent/forge/import', data, onProgress, signal) +export { + postCreateSession, + postSendFirstMessage, + postSendContinueMessage, + postCancelMessage, + getSetting, + postSetting, + getSettingGlobal, + postSettingGlobal, + postSettingProvidersGet, + postSettingAimodelsGet, + postSettingAifocusGet, + getSessionAll, + postSessionTitle, + deleteSession, + getSettingAiconfig, + postSettingAiconfig, + postSettingAppconfigsTemplateGet, + postSettingProvidersQuery, + postAIConfigHealthCheck, +} diff --git a/src/apis/AiEventApi/test.tsx b/src/apis/AiEventApi/test.tsx new file mode 100644 index 00000000..1ab99632 --- /dev/null +++ b/src/apis/AiEventApi/test.tsx @@ -0,0 +1,194 @@ +import showErrorMessage from '@/utils/showErrorMessage' +import { useMemoizedFn, useRequest } from 'ahooks' +import React, { useEffect } from 'react' +import { Badge, Button, Card, Descriptions, Space, Tag, Typography } from 'antd' +import { postCreateSession } from './index' +import useChatIPC from '@/pages/AIAgent/ai-re-act/hooks/useChatIPC' +import type { AIInputEvent, AIStartParams } from '@/pages/AIAgent/ai-re-act/hooks/grpcApi' +import { AIAgentLogChannelName } from '@/pages/AIAgent/defaultConstant' + +const TestAi: React.FC = () => { + // #region 创建session + const [session, setSession] = React.useState() + // 创建会话Session + const { runAsync: createSession } = useRequest(postCreateSession, { + manual: true, + onSuccess: (value) => { + console.log('createSession --- ', value) + setSession(value.run_id) + }, + onError: (error) => { + console.log('createSession error --- ', error) + showErrorMessage(error) + }, + }) + + const handleGenSession = useMemoizedFn(() => { + createSession({}) + }) + // #endregion + + // #region 会话相关逻辑(开始、持续、结束) + const [chatIPCData, events] = useChatIPC({ + channelName: AIAgentLogChannelName, + }) + + useEffect(() => { + events.onSwitchChat(session) + }, [session]) + + const handleStart = useMemoizedFn(() => { + if (!session) return + + const request: AIStartParams = { + EnableSystemFileSystemOperator: true, + UseDefaultAIConfig: true, + ForgeName: '', + DisallowRequireForUserPrompt: true, + ReviewPolicy: 'yolo', + AIReviewRiskControlScore: 0.5, + DisableToolUse: false, + AICallAutoRetry: 3, + AITransactionRetry: 5, + EnableAISearchTool: true, + EnableAISearchInternet: false, + EnableQwenNoThinkMode: false, + AllowPlanUserInteract: true, + PlanUserInteractMaxCount: 3, + AIService: 'aibalance', + ReActMaxIteration: 100, + TimelineItemLimit: 100, + TimelineContentSizeLimit: 20480, + UserInteractLimit: 0, + TimelineSessionID: session, + AIModelName: 'VESA-free', + UserQuery: '你是谁\n\n', + CoordinatorId: '', + Sequence: 1, + } + const aiInputEvent: AIInputEvent = { + IsStart: true, + Params: { ...request }, + } + events.onStart({ token: session, params: aiInputEvent }) + }) + + const handleStop = useMemoizedFn(() => { + session && events.onClose(session) + }) + // #endregion + + const isRunning = chatIPCData.execute + + // #region 打开/关闭日志独立页面 + const logWindowRef = React.useRef(null) + const [isLogWindowOpen, setIsLogWindowOpen] = React.useState(false) + + const closeLogWindow = useMemoizedFn(() => { + const w = logWindowRef.current + if (w && !w.closed) w.close() + logWindowRef.current = null + setIsLogWindowOpen(false) + }) + + const openLogWindow = useMemoizedFn(() => { + // HashRouter 下独立页面路径需要带上 `#/agent-log` + const base = import.meta.env.BASE_URL || '/' + const url = `${window.location.origin}${base}#/agent-log` + + // 独立页面(新标签页/新窗口由浏览器决定) + const w = window.open(url, '_blank') + if (!w) { + showErrorMessage('打开日志页面失败,请检查浏览器是否拦截了弹窗') + return + } + logWindowRef.current = w + setIsLogWindowOpen(true) + w.focus?.() + }) + + const handleToggleLogWindow = useMemoizedFn(() => { + const w = logWindowRef.current + if (w && !w.closed) closeLogWindow() + else openLogWindow() + }) + + // 轮询检测:用户手动关闭日志页后,按钮状态自动恢复 + useEffect(() => { + if (!isLogWindowOpen) return + const timer = window.setInterval(() => { + const w = logWindowRef.current + if (!w || w.closed) { + logWindowRef.current = null + setIsLogWindowOpen(false) + } + }, 500) + return () => window.clearInterval(timer) + }, [isLogWindowOpen]) + // #endregion + + return ( +
+ + + AI Agent 调试面板 + + {isRunning ? '会话执行中' : '会话未运行'} + + } + > + + + + {session ? ( + + {session} + + ) : ( + 暂无会话,请先创建 + )} + + + + + + + + + + + + {isRunning ? ( + + ) : ( + + )} + + + + + + 说明:该页面用于快速调试 AI Agent 的会话创建与启动流程, 上方展示当前会话 ID 与执行状态。 + + + +
+ ) +} + +export { TestAi } diff --git a/src/apis/AiEventApi/type.ts b/src/apis/AiEventApi/type.ts new file mode 100644 index 00000000..db79c6e7 --- /dev/null +++ b/src/apis/AiEventApi/type.ts @@ -0,0 +1,251 @@ +import type { AIAgentSetting } from '@/pages/AIAgent/ai-agent/aiAgentType'; + +interface AIParams { + forge_name?: string; + review_policy?: 'manual' | 'auto' | 'ai' | 'ai-auto'; + ai_service?: string; // "openai|aibalance|..." + ai_model_name?: string; + max_iteration?: number; + react_max_iteration?: number; + disable_tool_use?: boolean; + use_default_ai?: boolean; + attached_files?: string[]; + enable_system_file_system_operator?: boolean; + disallow_require_for_user_prompt?: boolean; + ai_review_risk_control_score?: number; + ai_call_auto_retry?: number; + ai_transaction_retry?: number; + enable_ai_search_tool?: boolean; + enable_ai_search_internet?: boolean; + enable_qwen_no_think_mode?: boolean; + allow_plan_user_interact?: boolean; + plan_user_interact_max_count?: number; + timeline_item_limit?: number; + timeline_content_size_limit?: number; + user_interact_limit?: number; + timeline_session_id?: string; +} + +interface RunEvent { + id: string; + type: + | 'listener_ready' + | 'stream' + | 'structured' + | 'thought' + | 'done' + | 'error' + | 'heartbeat' + | string; + coordinator_id: string; + ai_model_name: string; + node_id: string; + is_system: boolean; + is_stream: boolean; + is_reason: boolean; + stream_delta: string; + content: string; + timestamp: number; + task_index: string; + event_uuid: string; + task_uuid: string; +} + +interface TPostCreateSessionRequest { + run_id?: string; + params?: Partial; +} + +interface TPostCreateSessionResponse { + run_id: string; + status: string; +} + +interface TPostSendFirstMessageResponse { + run_id: string; + status: string; +} + +interface TPostSendContinueMessageResponse { + status: string; +} + +interface TGetSettingResponse extends AIAgentSetting { + UseDefaultAIConfig: boolean; + AIService: string; + AIModelName: string; + SelectedProviderID: number; + SelectedModelName: string; + SelectedModelTier: string; +} + +interface TPostSettingRequest { + ai_service: string; + selected_provider_id: number; + selected_model_name: string; + review_policy: string; +} + +interface TPostCancelMessageResponse { + run_id: string; + status: string; +} + +type TenumBuffer = Buffer | Uint8Array; + +interface AuthInfo { + AuthUsername: string; + AuthPassword: string; + AuthType: string; + Host: string; + Forbidden: boolean; +} + +interface ClientCertificates { + CrtPem: TenumBuffer; + KeyPem: TenumBuffer; + CaCertificates: TenumBuffer[]; + Pkcs12Bytes: TenumBuffer; + Pkcs12Password: TenumBuffer; + Host?: string; +} + +interface GlobalNetworkConfig { + DisableSystemDNS: boolean; + CustomDNSServers: string[]; + DNSFallbackTCP: boolean; + DNSFallbackDoH: boolean; + CustomDoHServers: string[]; + + ClientCertificates?: ClientCertificates[]; + + DisallowIPAddress: string[]; + DisallowDomain: string[]; + GlobalProxy: string[]; + EnableSystemProxyFromEnv: boolean; + SkipSaveHTTPFlow: boolean; + + AppConfigs: ThirdPartyApplicationConfig[]; + + AiApiPriority: string[]; + + AuthInfos: AuthInfo[]; + + SynScanNetInterface: string; + + ExcludePluginScanURIs: string[]; + IncludePluginScanURIs: string[]; + + DbSaveSync: boolean; + + CallPluginTimeout: number; + + MinTlsVersion: number; + MaxTlsVersion: number; + MaxContentLength: number | string; +} + +type GetGlobalNetworkConfigResponse = GlobalNetworkConfig; + +interface KVPair { + Key: string; + Value: string; + MarshalValue: string; +} + +interface ThirdPartyApplicationConfig { + Type: string; + APIKey: string; + UserIdentifier: string; + UserSecret: string; + Namespace: string; + Domain: string; + WebhookURL: string; + ExtraParams: KVPair[]; + Disabled: boolean; + Proxy: string; + NoHttps: boolean; +} + +interface ListAIProvidersResponse { + Providers?: { + Id: number; + Config: ThirdPartyApplicationConfig; + }[]; +} + +interface TPostSettingAimodelsGetRequest { + // 请求体支持多种格式 + Config: any; + // ```json + // { "Config": "openai" } + // ``` + + // 2) 传统 JSON 字符串: + + // ```json + // { "Config": "{\"Type\":\"openai\",\"APIKey\":\"***\"}" } + // ``` + + // 3) 对象格式: + + // ```json + // { + // "config": { + // "Type": "openai", + // "APIKey": "***", + // "Domain": "api.openai.com", + // "Proxy": "", + // "NoHttps": false + // } + // } + // ``` +} + +interface TPostSettingAimodelsGetResponse { + ModelName: string[]; +} + +interface TPostSettingAifocusGetResponse { + Name: string; + Description: string; + OutputExamplePrompt: string; + UsagePrompt: string; + VerboseName: string; + VerboseNameZh: string; +} + +interface GetSessionAllResponse { + sessions: { + run_id: string; + title: string; + status: 'pending' | 'running' | 'completed' | 'cancelled' | 'failed'; + created_at: string; + is_alive: boolean; + }[]; +} + +interface TPostSessionTitle { + run_id: string; + title: string; + status: string; +} + +export type { + TPostCreateSessionRequest, + TPostCreateSessionResponse, + TPostSendFirstMessageResponse, + TPostSendContinueMessageResponse, + TGetSettingResponse, + TPostSettingRequest, + RunEvent, + TPostCancelMessageResponse, + GetGlobalNetworkConfigResponse, + GlobalNetworkConfig, + ListAIProvidersResponse, + TPostSettingAimodelsGetRequest, + TPostSettingAimodelsGetResponse, + TPostSettingAifocusGetResponse, + GetSessionAllResponse, + TPostSessionTitle, +}; diff --git a/src/apis/CodecApi/index.ts b/src/apis/CodecApi/index.ts index d8c96114..9e6a5ce6 100644 --- a/src/apis/CodecApi/index.ts +++ b/src/apis/CodecApi/index.ts @@ -1,22 +1,12 @@ -import axios from '@/utils/axios'; -import type { ResponseData, TableResponseData } from '@/utils/commonTypes'; -import type { - TGetAllCodecMethodsResponse, - TPostRunCodecResponse, -} from './type'; +import axios from '@/utils/axios' +import type { ResponseData, TableResponseData } from '@/utils/commonTypes' +import type { TGetAllCodecMethodsResponse, TPostRunCodecResponse } from './type' // 获取所有编码方式 -const getAllCodecMethods = (): Promise< - ResponseData> -> => - axios.get< - never, - ResponseData> - >(`/codec/getAllCodecMethods`); +const getAllCodecMethods = (): Promise>> => + axios.get>>(`/api/codec/getAllCodecMethods`) -const postRunCodec = ( - data: TPostRunCodecResponse, -): Promise> => - axios.post>(`/run/codec`, data); +const postRunCodec = (data: TPostRunCodecResponse): Promise> => + axios.post>(`/api/run/codec`, data) -export { getAllCodecMethods, postRunCodec }; +export { getAllCodecMethods, postRunCodec } diff --git a/src/apis/CompileArtifactApi/index.ts b/src/apis/CompileArtifactApi/index.ts index 0889eca5..2bacdd14 100644 --- a/src/apis/CompileArtifactApi/index.ts +++ b/src/apis/CompileArtifactApi/index.ts @@ -1,70 +1,50 @@ -import axios from '@/utils/axios'; -import type { ResponseData } from '@/utils/commonTypes'; +import axios from '@/utils/axios' +import type { ResponseData } from '@/utils/commonTypes' import type { - TCompileArtifactSummary, - TCompileArtifactListParams, - TCompileArtifactListResponse, - TCompileArtifactDetail, - TCompileArtifactRebuildRequest, - TCompileArtifactRebuildResponse, -} from './type'; + TCompileArtifactSummary, + TCompileArtifactListParams, + TCompileArtifactListResponse, + TCompileArtifactDetail, + TCompileArtifactRebuildRequest, + TCompileArtifactRebuildResponse, +} from './type' -const unwrapNestedResponse = ( - response: ResponseData, -): ResponseData => { - const nested = response?.data; - if ( - nested && - typeof nested === 'object' && - 'code' in nested && - 'data' in nested - ) { - return { - ...response, - data: nested.data as T, - }; +const unwrapNestedResponse = (response: ResponseData): ResponseData => { + const nested = response?.data + if (nested && typeof nested === 'object' && 'code' in nested && 'data' in nested) { + return { + ...response, + data: nested.data as T, } - return response as ResponseData; -}; + } + return response as ResponseData +} -const queryCompileArtifactSummary = (): Promise< - ResponseData -> => - axios.get>( - '/ssa/compile-artifacts/summary', - ).then((response) => unwrapNestedResponse(response)); +const queryCompileArtifactSummary = (): Promise> => + axios + .get>('/api/ssa/compile-artifacts/summary') + .then((response) => unwrapNestedResponse(response)) const queryCompileArtifacts = ( - params?: TCompileArtifactListParams, + params?: TCompileArtifactListParams, ): Promise> => - axios.get>( - '/ssa/compile-artifacts', - { params }, - ).then((response) => - unwrapNestedResponse(response), - ); + axios + .get>('/api/ssa/compile-artifacts', { params }) + .then((response) => unwrapNestedResponse(response)) -const fetchCompileArtifactDetail = ( - seriesKey: string, -): Promise> => - axios.get>( - '/ssa/compile-artifacts/detail', - { params: { series_key: seriesKey } }, - ).then((response) => unwrapNestedResponse(response)); +const fetchCompileArtifactDetail = (seriesKey: string): Promise> => + axios + .get< + never, + ResponseData + >('/api/ssa/compile-artifacts/detail', { params: { series_key: seriesKey } }) + .then((response) => unwrapNestedResponse(response)) const forceRebuildCompileArtifact = ( - data: TCompileArtifactRebuildRequest, + data: TCompileArtifactRebuildRequest, ): Promise> => - axios.post>( - '/ssa/compile-artifacts/rebuild', - data, - ).then((response) => - unwrapNestedResponse(response), - ); + axios + .post>('/api/ssa/compile-artifacts/rebuild', data) + .then((response) => unwrapNestedResponse(response)) -export { - queryCompileArtifactSummary, - queryCompileArtifacts, - fetchCompileArtifactDetail, - forceRebuildCompileArtifact, -}; +export { queryCompileArtifactSummary, queryCompileArtifacts, fetchCompileArtifactDetail, forceRebuildCompileArtifact } diff --git a/src/apis/CveLoopholeApi/index.ts b/src/apis/CveLoopholeApi/index.ts index e8dd4eda..c23b0df9 100644 --- a/src/apis/CveLoopholeApi/index.ts +++ b/src/apis/CveLoopholeApi/index.ts @@ -1,28 +1,19 @@ -import axios from '@/utils/axios'; -import type { ResponseData, TableResponseData } from '@/utils/commonTypes'; -import type { - TCveQueryRequest, - TCveQueryResponse, - TGetCveUpdateResquest, -} from './type'; +import axios from '@/utils/axios' +import type { ResponseData, TableResponseData } from '@/utils/commonTypes' +import type { TCveQueryRequest, TCveQueryResponse, TGetCveUpdateResquest } from './type' // 节点是否安装成功 -const postCveQuery = ( - data: TCveQueryRequest, -): Promise>> => - axios.post>>( - `/cve/query`, - data, - ); +const postCveQuery = (data: TCveQueryRequest): Promise>> => + axios.post>>(`/api/cve/query`, data) // cve 数据库更新 -const getCveUpdate = ( - params: TGetCveUpdateResquest, - signal?: AbortSignal, -): Promise> => - axios.get>(`/cve/update`, { params, signal }); +const getCveUpdate = (params: TGetCveUpdateResquest, signal?: AbortSignal): Promise> => + axios.get>(`/api/cve/update`, { + params, + signal, + }) const getCveOfflineUpdate = (): Promise> => - axios.post>(`/reset/cve`); + axios.post>(`/api/reset/cve`) -export { postCveQuery, getCveUpdate, getCveOfflineUpdate }; +export { postCveQuery, getCveUpdate, getCveOfflineUpdate } diff --git a/src/apis/GlobalReverseLinkApi/index.ts b/src/apis/GlobalReverseLinkApi/index.ts index ac003b0f..fe482187 100644 --- a/src/apis/GlobalReverseLinkApi/index.ts +++ b/src/apis/GlobalReverseLinkApi/index.ts @@ -1,16 +1,11 @@ -import axios from '@/utils/axios'; -import type { ResponseData } from '@/utils/commonTypes'; -import type { - getReverseConfigResponse, - PostReverseConfigRequest, -} from './types'; +import axios from '@/utils/axios' +import type { ResponseData } from '@/utils/commonTypes' +import type { getReverseConfigResponse, PostReverseConfigRequest } from './types' -const postReverseConfig = ( - data: PostReverseConfigRequest, -): Promise> => - axios.post>('/reverse/config', data); +const postReverseConfig = (data: PostReverseConfigRequest): Promise> => + axios.post>('/api/reverse/config', data) const getReverseConfig = (): Promise> => - axios.get>('/reverse/config'); + axios.get>('/api/reverse/config') -export { postReverseConfig, getReverseConfig }; +export { postReverseConfig, getReverseConfig } diff --git a/src/apis/MessageCollectApi/index.ts b/src/apis/MessageCollectApi/index.ts index aad37072..ab70496d 100644 --- a/src/apis/MessageCollectApi/index.ts +++ b/src/apis/MessageCollectApi/index.ts @@ -1,61 +1,36 @@ -import axios from '@/utils/axios'; -import type { ResponseData, TableResponseData } from '@/utils/commonTypes'; -import type { - TGetCompanyInfoRequest, - TGetCompanyInfoResponse, - TGetDomainInfoResponse, -} from './type'; -import type { TreeGraphData } from '@antv/g6'; +import axios from '@/utils/axios' +import type { ResponseData, TableResponseData } from '@/utils/commonTypes' +import type { TGetCompanyInfoRequest, TGetCompanyInfoResponse, TGetDomainInfoResponse } from './type' +import type { TreeGraphData } from '@antv/g6' // 获取信息收集列表数据 const getCompanyInfo = ( - data: Partial, + data: Partial, ): Promise>> => - axios.post< - never, - ResponseData> - >('/assets/company-info', data); + axios.post>>('/api/assets/company-info', data) // 删除信息收集 数据 -const deleteCompanyInfo = (params: { - ids?: number[]; - all?: boolean; -}): Promise> => - axios.delete>('/assets/company-info', { - data: params, - }); +const deleteCompanyInfo = (params: { ids?: number[]; all?: boolean }): Promise> => + axios.delete>('/api/assets/company-info', { + data: params, + }) // 获取全部信息资产域名 const getAlldomains = (params: { - keyword: string; + keyword: string }): Promise>> => - axios.get>>( - '/assets/company-info', - { params }, - ); + axios.get>>('/api/assets/company-info', { params }) // 获取子域名爆破列表数据 const getDomainInfo = ( - params: Omit, + params: Omit, ): Promise>> => - axios.get>>( - '/assets/domain-info', - { params }, - ); + axios.get>>('/api/assets/domain-info', { params }) // 获取攻击路径图详情 -const getAttackPath = (params: { - task_id: string; - script_type: string; -}): Promise> => - axios.get>('/assets/attack-path', { - params, - }); +const getAttackPath = (params: { task_id: string; script_type: string }): Promise> => + axios.get>('/api/assets/attack-path', { + params, + }) -export { - getCompanyInfo, - deleteCompanyInfo, - getAlldomains, - getDomainInfo, - getAttackPath, -}; +export { getCompanyInfo, deleteCompanyInfo, getAlldomains, getDomainInfo, getAttackPath } diff --git a/src/apis/NodeConfigApi/index.ts b/src/apis/NodeConfigApi/index.ts index 5f73f82f..4736ce95 100644 --- a/src/apis/NodeConfigApi/index.ts +++ b/src/apis/NodeConfigApi/index.ts @@ -1,12 +1,8 @@ -import axios from '@/utils/axios'; -import type { ResponseData } from '@/utils/commonTypes'; +import axios from '@/utils/axios' +import type { ResponseData } from '@/utils/commonTypes' // 节点是否安装成功 -const getFileExists = ( - file_name: 'yak' | 'docker-compose', -): Promise> => - axios.get>( - `/file-exists?file_name=${file_name}`, - ); +const getFileExists = (file_name: 'yak' | 'docker-compose'): Promise> => + axios.get>(`/api/file-exists?file_name=${file_name}`) -export { getFileExists }; +export { getFileExists } diff --git a/src/apis/NodeManageApi/index.ts b/src/apis/NodeManageApi/index.ts index 856c595c..e570db7a 100644 --- a/src/apis/NodeManageApi/index.ts +++ b/src/apis/NodeManageApi/index.ts @@ -1,62 +1,80 @@ -import axios from '@/utils/axios'; -import type { ResponseData, TableResponseData } from '@/utils/commonTypes'; +import axios from '@/utils/axios' +import type { ResponseData, TableResponseData } from '@/utils/commonTypes' import type { - PostHostAliveDetectionRunRequest, - QueryPalmNodeParams, - TPostNodesDownloadDataRunRequest, -} from './type'; -import type { Palm } from '@/gen/schema'; + PostHostAliveDetectionRunRequest, + QueryPalmNodeParams, + ScannerObservabilityOverview, + TPostNodesDownloadDataRunRequest, +} from './type' +import type { Palm } from '@/gen/schema' // 获取节点管理 表格数据 -const getNodeManage = ( - params: QueryPalmNodeParams, -): Promise>> => - axios.get>>(`/node`, { - params, - }); +const getNodeManage = (params: QueryPalmNodeParams): Promise>> => + axios.get>>(`/api/node`, { + params, + }) // 删除节点 const deleteNodeManage = ( - params: QueryPalmNodeParams & { node_ids?: string }, -): Promise>> => - axios.delete>>(`/node`, { - params, - }); + params: QueryPalmNodeParams & { node_ids?: string }, +): Promise>> => + axios.delete>>(`/api/node`, { + params, + }) // 编辑节点 const postUpdateLocation = (data: { - location: string; - nickname: string; - node_id: string; -}): Promise> => - axios.post>('/node/update-location', data); + location: string + nickname: string + node_id: string +}): Promise> => axios.post>('/api/node/update-location', data) // 更新节点数据 -const postNodesDownloadDataRun = ( - data: TPostNodesDownloadDataRunRequest, -): Promise> => - axios.post>( - '/task/start/nodes-download-data/run', - data, - ); +const postNodesDownloadDataRun = (data: TPostNodesDownloadDataRunRequest): Promise> => + axios.post>('/api/task/start/nodes-download-data/run', data) // 获取网络检测表格数据 const postHostAliveDetectionRun = (data: { - dns_timeout: number; - hosts: string; - nodes_id: string[]; -}): Promise< - ResponseData> -> => - axios.post< - never, - ResponseData> - >('/task/start/host-alive-detection/run', data); + dns_timeout: number + hosts: string + nodes_id: string[] +}): Promise>> => + axios.post>>( + '/api/task/start/host-alive-detection/run', + data, + ) + +const getScannerObservabilityOverview = (params?: { + task_limit?: number +}): Promise> => + axios.get>('/api/ssa/observability/scanner/overview', { + params, + }) + +const exportScannerObservabilityDiagnostics = (params?: { + task_limit?: number + log_limit?: number + node_id?: string + task_id?: string +}): Promise> => + axios.get('/api/ssa/observability/scanner/diagnostics/export', { + params, + responseType: 'blob', + transformResponse: [ + (data) => ({ + data, + code: 200, + msg: '', + }), + ], + }) export { - getNodeManage, - postUpdateLocation, - postNodesDownloadDataRun, - deleteNodeManage, - postHostAliveDetectionRun, -}; + getNodeManage, + postUpdateLocation, + postNodesDownloadDataRun, + deleteNodeManage, + postHostAliveDetectionRun, + getScannerObservabilityOverview, + exportScannerObservabilityDiagnostics, +} diff --git a/src/apis/NodeManageApi/type.ts b/src/apis/NodeManageApi/type.ts index 9b2d43cc..28f1882a 100644 --- a/src/apis/NodeManageApi/type.ts +++ b/src/apis/NodeManageApi/type.ts @@ -29,9 +29,87 @@ interface PostHostAliveDetectionRunRequest { result: Array; } +interface ScannerObservabilityRunningTask { + node_id: string; + task_id: string; + root_task_id?: string; + sub_task_id?: string; + runtime_id?: string; + type: string; + status: string; + wait_ms: number; + start_timestamp: number; + running_timestamp: number; + ddl_timestamp: number; + elapsed_ms: number; +} + +interface ScannerObservabilityNode { + node_id: string; + nickname: string; + location: string; + external_ip: string; + main_addr: string; + last_seen_at: number; + online: boolean; + cpu_percent: number; + memory_percent: number; + network_upload: number; + network_download: number; + active_count: number; + queue_count: number; + capacity: number; + recent_avg_wait_ms: number; + recent_avg_exec_ms: number; + recent_completed_count: number; + rpc_error?: string; + running_tasks: ScannerObservabilityRunningTask[]; +} + +interface ScannerObservabilityRecentTask { + task_id: string; + project_name: string; + scan_batch: number; + execute_node: string; + status: string; + phase: string; + progress: number; + language: string; + source_origin: string; + error_message?: string; + created_at: number; + started_at?: number; + finished_at?: number; +} + +interface ScannerObservabilitySummary { + total_nodes: number; + online_nodes: number; + offline_nodes: number; + total_capacity: number; + total_active: number; + total_queued: number; + recent_avg_wait_ms: number; + recent_avg_exec_ms: number; + recent_completed_count: number; +} + +interface ScannerObservabilityOverview { + generated_at: number; + summary: ScannerObservabilitySummary; + nodes: ScannerObservabilityNode[]; + running_tasks: ScannerObservabilityRunningTask[]; + recent_tasks: ScannerObservabilityRecentTask[]; +} + export type { QueryPalmNodeParams, TPostNodesDownloadDataRunRequest, NetworkPingTableProp, PostHostAliveDetectionRunRequest, + ScannerObservabilityRunningTask, + ScannerObservabilityNode, + ScannerObservabilityRecentTask, + ScannerObservabilitySummary, + ScannerObservabilityOverview, }; diff --git a/src/apis/SSAIRApi/index.ts b/src/apis/SSAIRApi/index.ts index d66a6a91..dd7d8fc4 100644 --- a/src/apis/SSAIRApi/index.ts +++ b/src/apis/SSAIRApi/index.ts @@ -1,42 +1,18 @@ -import axios from '@/utils/axios'; -import type { ResponseData } from '@/utils/commonTypes'; -import type { TSSATaskResponse } from '@/apis/SSAScanTaskApi/type'; -import type { - TSSAIRCompileRequest, - TSSAIRCompileResponse, - TSSAIRHeadResponse, - TSSAIRScanRequest, -} from './type'; +import axios from '@/utils/axios' +import type { ResponseData } from '@/utils/commonTypes' +import type { TSSATaskResponse } from '@/apis/SSAScanTaskApi/type' +import type { TSSAIRCompileRequest, TSSAIRCompileResponse, TSSAIRHeadResponse, TSSAIRScanRequest } from './type' // POST /ssa/project/{id}/ir/compile - 编译 IR(写入 SSA-IR DB),返回 async_task_id -const compileSSAIR = ( - projectId: number, - data: TSSAIRCompileRequest, -): Promise> => - axios.post>( - `/ssa/project/${projectId}/ir/compile`, - data, - ); +const compileSSAIR = (projectId: number, data: TSSAIRCompileRequest): Promise> => + axios.post>(`/api/ssa/project/${projectId}/ir/compile`, data) // GET /ssa/project/{id}/ir/head - 查询 series HEAD -const getSSAIRHead = ( - projectId: number, - params?: { series_key?: string }, -): Promise> => - axios.get>( - `/ssa/project/${projectId}/ir/head`, - { params }, - ); +const getSSAIRHead = (projectId: number, params?: { series_key?: string }): Promise> => + axios.get>(`/api/ssa/project/${projectId}/ir/head`, { params }) // POST /ssa/project/{id}/ir/scan - 从 SSA-IR DB 的 program/HEAD 发起扫描 -const scanSSAIR = ( - projectId: number, - data?: TSSAIRScanRequest, -): Promise> => - axios.post>( - `/ssa/project/${projectId}/ir/scan`, - data || {}, - ); - -export { compileSSAIR, getSSAIRHead, scanSSAIR }; +const scanSSAIR = (projectId: number, data?: TSSAIRScanRequest): Promise> => + axios.post>(`/api/ssa/project/${projectId}/ir/scan`, data || {}) +export { compileSSAIR, getSSAIRHead, scanSSAIR } diff --git a/src/apis/SSAProjectApi/index.ts b/src/apis/SSAProjectApi/index.ts index 37636129..37cdc50b 100644 --- a/src/apis/SSAProjectApi/index.ts +++ b/src/apis/SSAProjectApi/index.ts @@ -1,83 +1,61 @@ -import axios from '@/utils/axios'; -import type { ResponseData } from '@/utils/commonTypes'; -import type { - TSSAProject, - TSSAProjectListResponse, - TSSAProjectRequest, - TScanPolicyConfig, -} from './type'; +import axios from '@/utils/axios' +import type { ResponseData } from '@/utils/commonTypes' +import type { TSSAProject, TSSAProjectListResponse, TSSAProjectRequest, TScanPolicyConfig } from './type' // GET /ssa/project - 查询项目列表 const getSSAProjects = ( - params: { - limit?: number; - page?: number; - order?: string; - order_by?: string; - project_name?: string; - language?: string; - tags?: string; - }, - signal?: AbortSignal, + params: { + limit?: number + page?: number + order?: string + order_by?: string + project_name?: string + language?: string + tags?: string + }, + signal?: AbortSignal, ): Promise> => - axios.get>(`/ssa/project`, { - params, - signal, - }); + axios.get>(`/api/ssa/project`, { + params, + signal, + }) // GET /ssa/project/fetch - 获取项目详情 -const fetchSSAProject = ( - params: { id: number }, - signal?: AbortSignal, -): Promise> => - axios.get>(`/ssa/project/fetch`, { - params, - signal, - }); +const fetchSSAProject = (params: { id: number }, signal?: AbortSignal): Promise> => + axios.get>(`/api/ssa/project/fetch`, { + params, + signal, + }) // POST /ssa/project - 创建/更新项目 -const postSSAProject = ( - data: TSSAProjectRequest, -): Promise> => - axios.post>(`/ssa/project`, data); +const postSSAProject = (data: TSSAProjectRequest): Promise> => + axios.post>(`/api/ssa/project`, data) // DELETE /ssa/project - 删除项目 const deleteSSAProject = (params: { - id: number; -}): Promise> => - axios.delete>(`/ssa/project`, { params }); + id: number + delete_mode?: 'config-only' | 'cascade' +}): Promise> => axios.delete>(`/api/ssa/project`, { params }) // POST /ssa/project/test-connection - 测试Git连接 const testGitConnection = (data: { - url: string; - auth?: { - kind?: 'none' | 'basic' | 'ssh'; - user_name?: string; - password?: string; - key_content?: string; - }; - proxy?: { - url?: string; - }; + url: string + auth?: { + kind?: 'none' | 'basic' | 'ssh' + user_name?: string + password?: string + key_content?: string + } + proxy?: { + url?: string + } }): Promise> => - axios.post>( - `/ssa/project/test-connection`, - data, - ); + axios.post>(`/api/ssa/project/test-connection`, data) // GET /ssa/scan-policy-config - 获取扫描策略配置 -const getScanPolicyConfig = ( - signal?: AbortSignal, -): Promise> => - axios.get>(`/ssa/scan-policy-config`, { - signal, - }); +const getScanPolicyConfig = (signal?: AbortSignal): Promise> => + axios.get>(`/api/ssa/scan-policy-config`, { + signal, + }) -export { - getSSAProjects, - fetchSSAProject, - postSSAProject, - deleteSSAProject, - testGitConnection, - getScanPolicyConfig, -}; +export { getSSAProjects, fetchSSAProject, postSSAProject, deleteSSAProject, testGitConnection, getScanPolicyConfig } diff --git a/src/apis/SSAProjectApi/type.ts b/src/apis/SSAProjectApi/type.ts index ea6a263f..d0246cf3 100644 --- a/src/apis/SSAProjectApi/type.ts +++ b/src/apis/SSAProjectApi/type.ts @@ -83,6 +83,8 @@ interface TSSAProjectRequest { // SSA 项目列表响应类型 type TSSAProjectListResponse = TableResponseData; +export type TSSAProjectDeleteMode = 'config-only' | 'cascade'; + // 扫描策略配置类型 export interface TScanPolicyConfig { version?: string; diff --git a/src/apis/SSAReportRecordApi/index.ts b/src/apis/SSAReportRecordApi/index.ts new file mode 100644 index 00000000..9d07e8cb --- /dev/null +++ b/src/apis/SSAReportRecordApi/index.ts @@ -0,0 +1,41 @@ +import axios from '@/utils/axios' +import type { ResponseData } from '@/utils/commonTypes' +import type { + TSSAReportRecord, + TSSAReportRecordCreateRequest, + TSSAReportRecordDetail, + TSSAReportRecordListAPIResponse, + TSSAReportRecordListResponse, + TSSAReportRecordQueryParams, +} from './type' + +const normalizeListResponse = (payload?: TSSAReportRecordListAPIResponse['data']): TSSAReportRecordListResponse => ({ + list: payload?.data || [], + pagemeta: { + page: payload?.paging?.pagemeta?.page || 1, + limit: payload?.paging?.pagemeta?.limit || 20, + total: payload?.paging?.pagemeta?.total || 0, + total_page: payload?.paging?.pagemeta?.total_page || 1, + }, +}) + +export const querySSAReportRecords = async ( + params?: TSSAReportRecordQueryParams, +): Promise> => { + const res = await axios.get('/api/ssa/report-records', { + params, + }) + return { + ...res, + data: normalizeListResponse(res.data), + } +} + +export const createSSAReportRecord = (data: TSSAReportRecordCreateRequest): Promise> => + axios.post>('/api/ssa/report-records', data) + +export const fetchSSAReportRecord = (id: number): Promise> => + axios.get>(`/api/ssa/report-records/${id}`) + +export const deleteSSAReportRecord = (id: number): Promise> => + axios.delete>(`/api/ssa/report-records/${id}`) diff --git a/src/apis/SSAReportRecordApi/type.ts b/src/apis/SSAReportRecordApi/type.ts new file mode 100644 index 00000000..c0531442 --- /dev/null +++ b/src/apis/SSAReportRecordApi/type.ts @@ -0,0 +1,86 @@ +import type { ResponseData } from '@/utils/commonTypes'; + +export interface TSSAReportRecord { + id: number; + title?: string; + hash?: string; + owner?: string; + from?: string; + report_type?: string; + scope_type?: string; + scope_name?: string; + project_name?: string; + program_name?: string; + task_id?: string; + task_count?: number; + scan_batch?: number; + risk_total?: number; + risk_critical?: number; + risk_high?: number; + risk_medium?: number; + risk_low?: number; + published_at?: number; + source_finished_at?: number; + created_at?: number; + updated_at?: number; +} + +export interface TSSAReportRecordDetail extends TSSAReportRecord { + json_raw?: string; +} + +export interface TSSAReportRecordQueryParams { + page?: number; + limit?: number; + order?: 'asc' | 'desc'; + order_by?: + | 'published_at' + | 'updated_at' + | 'created_at' + | 'risk_total' + | 'project_name'; + keyword?: string; + project_name?: string; + task_id?: string; + report_type?: string; + start?: number; + end?: number; +} + +export interface TSSAReportRecordListPayload { + data?: TSSAReportRecord[]; + paging?: { + pagemeta?: { + page: number; + limit: number; + total: number; + total_page: number; + }; + }; +} + +export interface TSSAReportRecordListResponse { + list: TSSAReportRecord[]; + pagemeta: { + page: number; + limit: number; + total: number; + total_page: number; + }; +} + +export interface TSSAReportRecordCreateRequest { + task_id?: string; + task_ids?: string; + ids?: string; + program_name?: string; + severity?: string; + risk_type?: string; + from_rule?: string; + latest_disposal_status?: string; + audited_state?: 'all' | 'audited' | 'unaudited'; + report_name?: string; +} + +export type TSSAReportRecordListAPIResponse = + ResponseData; diff --git a/src/apis/SSAReportRecordFileApi/index.ts b/src/apis/SSAReportRecordFileApi/index.ts new file mode 100644 index 00000000..7fa553f5 --- /dev/null +++ b/src/apis/SSAReportRecordFileApi/index.ts @@ -0,0 +1,43 @@ +import axios from '@/utils/axios' +import type { ResponseData } from '@/utils/commonTypes' +import type { + TSSAReportRecordFile, + TSSAReportRecordFileCreateRequest, + TSSAReportRecordFileListPayload, + TSSAReportRecordFileListResponse, +} from './type' + +export const querySSAReportRecordFiles = async ( + reportRecordId: number, +): Promise> => { + const res = await axios.get>( + `/api/ssa/report-records/${reportRecordId}/files`, + ) + return { + ...res, + data: { + list: res.data?.data || [], + }, + } +} + +export const createSSAReportRecordFile = ( + reportRecordId: number, + data: TSSAReportRecordFileCreateRequest, +): Promise> => + axios.post>(`/api/ssa/report-records/${reportRecordId}/files`, data) + +export const deleteSSAReportRecordFile = (fileId: number): Promise> => + axios.delete>(`/api/ssa/report-record-files/${fileId}`) + +export const downloadSSAReportRecordFile = (fileId: number): Promise> => + axios.get>(`/api/ssa/report-record-files/${fileId}/download`, { + responseType: 'blob', + transformResponse: [ + (data) => ({ + data, + code: 200, + msg: '', + }), + ], + }) diff --git a/src/apis/SSAReportRecordFileApi/type.ts b/src/apis/SSAReportRecordFileApi/type.ts new file mode 100644 index 00000000..56045ae7 --- /dev/null +++ b/src/apis/SSAReportRecordFileApi/type.ts @@ -0,0 +1,29 @@ +export interface TSSAReportRecordFile { + id: number; + report_record_id: number; + format?: string; + file_name?: string; + object_key?: string; + bucket?: string; + content_type?: string; + size_bytes?: number; + sha256?: string; + status?: string; + created_by?: string; + generation_error?: string; + created_at?: number; + updated_at?: number; +} + +export interface TSSAReportRecordFileCreateRequest { + format: 'pdf' | 'docx' | 'html'; + overwrite?: boolean; +} + +export interface TSSAReportRecordFileListPayload { + data?: TSSAReportRecordFile[]; +} + +export interface TSSAReportRecordFileListResponse { + list: TSSAReportRecordFile[]; +} diff --git a/src/apis/SSARiskApi/index.ts b/src/apis/SSARiskApi/index.ts index 0ec1bfc8..961b8c23 100644 --- a/src/apis/SSARiskApi/index.ts +++ b/src/apis/SSARiskApi/index.ts @@ -1,234 +1,188 @@ -import axios from '@/utils/axios'; -import type { ResponseData } from '@/utils/commonTypes'; +import axios from '@/utils/axios' +import type { ResponseData } from '@/utils/commonTypes' import type { - TSSARisk, - TSSARiskListResponse, - TSSARiskRequest, - TSSARiskBatchRequest, - TSSARiskQueryParams, - TSSARiskExportParams, - TSSARiskExportData, - TSSARiskImportResult, - TSSARiskFilterOptions, - TSSARiskAuditInfo, - TSSARiskRelatedFiles, - TSSARiskFileContent, - TSSARiskDisposalRequest, - TSSARiskDisposal, - TSSARiskDisposalListResponse, -} from './type'; + TSSARisk, + TSSARiskListResponse, + TSSARiskRequest, + TSSARiskBatchRequest, + TSSARiskQueryParams, + TSSARiskExportParams, + TSSARiskExportData, + TSSARiskImportResult, + TSSARiskFilterOptions, + TSSARiskAuditInfo, + TSSARiskRelatedFiles, + TSSARiskFileContent, + TSSARiskDisposalRequest, + TSSARiskDisposal, + TSSARiskDisposalListResponse, +} from './type' // GET /ssa/risk/filter-options - 获取筛选选项 -export const getSSARiskFilterOptions = ( - signal?: AbortSignal, -): Promise> => - axios.get>( - '/ssa/risk/filter-options', - { - signal, - }, - ); +export const getSSARiskFilterOptions = (signal?: AbortSignal): Promise> => + axios.get>('/api/ssa/risk/filter-options', { + signal, + }) // GET /ssa/risk - 查询 SSA 风险列表 export const getSSARisks = ( - params: TSSARiskQueryParams, - signal?: AbortSignal, + params: TSSARiskQueryParams, + signal?: AbortSignal, ): Promise> => - axios.get>('/ssa/risk', { - params, - signal, - }); + axios.get>('/api/ssa/risk', { + params, + signal, + }) // POST /ssa/risk - 创建/更新 SSA 风险 -export const postSSARisk = ( - data: TSSARiskRequest, -): Promise> => - axios.post>('/ssa/risk', data); +export const postSSARisk = (data: TSSARiskRequest): Promise> => + axios.post>('/api/ssa/risk', data) // POST /ssa/risk - 更新单个 SSA 风险(简化版,用于标记已读等) -export const updateSSARisk = ( - data: Partial & { id: number }, -): Promise> => - axios.post>('/ssa/risk', data); +export const updateSSARisk = (data: Partial & { id: number }): Promise> => + axios.post>('/api/ssa/risk', data) // DELETE /ssa/risk - 删除 SSA 风险 -export const deleteSSARisk = (params: { - id?: number; - hash?: string; -}): Promise> => - axios.delete>('/ssa/risk', { params }); +export const deleteSSARisk = (params: { id?: number; hash?: string }): Promise> => + axios.delete>('/api/ssa/risk', { params }) // GET /ssa/risk/fetch - 获取 SSA 风险详情 export const fetchSSARisk = ( - params: { id?: number; hash?: string }, - signal?: AbortSignal, + params: { id?: number; hash?: string }, + signal?: AbortSignal, ): Promise> => - axios.get>('/ssa/risk/fetch', { - params, - signal, - }); + axios.get>('/api/ssa/risk/fetch', { + params, + signal, + }) // POST /ssa/risk/batch - 批量操作 SSA 风险 -export const batchUpdateSSARisks = ( - data: TSSARiskBatchRequest, -): Promise> => - axios.post>('/ssa/risk/batch', data); +export const batchUpdateSSARisks = (data: TSSARiskBatchRequest): Promise> => + axios.post>('/api/ssa/risk/batch', data) // GET /ssa/risk/export - 导出 SSA 风险 -export const exportSSARisks = ( - params?: TSSARiskExportParams, -): Promise> => - axios.get>('/ssa/risk/export', { - params, - }); +export const exportSSARisks = (params?: TSSARiskExportParams): Promise> => + axios.get>('/api/ssa/risk/export', { + params, + }) // GET /ssa/risk/export - 下载 PDF 报告 -export const exportSSARiskReportPDF = ( - params: TSSARiskExportParams, -): Promise> => - axios.get>('/ssa/risk/export', { - params: { - ...params, - format: 'pdf', - }, - responseType: 'blob', - transformResponse: [ - (data) => ({ - data, - code: 200, - msg: '', - }), - ], - }); +export const exportSSARiskReportPDF = (params: TSSARiskExportParams): Promise> => + axios.get>('/api/ssa/risk/export', { + params: { + ...params, + format: 'pdf', + }, + responseType: 'blob', + transformResponse: [ + (data) => ({ + data, + code: 200, + msg: '', + }), + ], + }) // GET /ssa/risk/export - 下载 DOCX 报告 -export const exportSSARiskReportDocx = ( - params: TSSARiskExportParams, -): Promise> => - axios.get>('/ssa/risk/export', { - params: { - ...params, - format: 'docx', - }, - responseType: 'blob', - transformResponse: [ - (data) => ({ - data, - code: 200, - msg: '', - }), - ], - }); +export const exportSSARiskReportDocx = (params: TSSARiskExportParams): Promise> => + axios.get>('/api/ssa/risk/export', { + params: { + ...params, + format: 'docx', + }, + responseType: 'blob', + transformResponse: [ + (data) => ({ + data, + code: 200, + msg: '', + }), + ], + }) // POST /ssa/risk/import - 导入 SSA 风险 -export const importSSARisks = async ( - file: File, -): Promise> => { - console.log('[SSARiskApi] importSSARisks 开始, 文件:', file.name); - const formData = new FormData(); - formData.append('file', file); - try { - const response = await axios.post< - never, - ResponseData - >('/ssa/risk/import', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - console.log('[SSARiskApi] importSSARisks 响应:', response); - return response; - } catch (error) { - console.error('[SSARiskApi] importSSARisks 错误:', error); - throw error; - } -}; +export const importSSARisks = async (file: File): Promise> => { + const formData = new FormData() + formData.append('file', file) + try { + const response = await axios.post>('/api/ssa/risk/import', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + return response + } catch (error) { + console.error('[SSARiskApi] importSSARisks 错误:', error) + throw error + } +} // DELETE /ssa/risk/clear - 清空所有 SSA 风险 export const clearSSARisks = (): Promise> => - axios.delete>('/ssa/risk/clear'); + axios.delete>('/api/ssa/risk/clear') // GET /ssa/risk/audit - 获取 SSA 风险审计信息 export const getSSARiskAudit = ( - hash: string, - taskId?: string, - signal?: AbortSignal, + hash: string, + taskId?: string, + signal?: AbortSignal, ): Promise> => - axios.get>('/ssa/risk/audit', { - params: { hash, task_id: taskId }, - signal, - }); + axios.get>('/api/ssa/risk/audit', { + params: { hash, task_id: taskId }, + signal, + }) // GET /ssa/risk/audit/files - 获取风险关联的文件列表 export const getSsaRiskAuditFiles = ( - hash: string, - taskId?: string, - signal?: AbortSignal, + hash: string, + taskId?: string, + signal?: AbortSignal, ): Promise> => - axios.get>( - '/ssa/risk/audit/files', - { - params: { hash, task_id: taskId }, - signal, - }, - ); + axios.get>('/api/ssa/risk/audit/files', { + params: { hash, task_id: taskId }, + signal, + }) // GET /ssa/risk/audit/file/content - 获取指定文件的内容 export const getSsaRiskFileContent = ( - hash: string, - taskId: string | undefined, - filePath: string, - signal?: AbortSignal, + hash: string, + taskId: string | undefined, + filePath: string, + signal?: AbortSignal, ): Promise> => - axios.get>( - '/ssa/risk/audit/file/content', - { - params: { hash, task_id: taskId, file_path: filePath }, - signal, - }, - ); + axios.get>('/api/ssa/risk/audit/file/content', { + params: { hash, task_id: taskId, file_path: filePath }, + signal, + }) // POST /ssa/risk/disposal - 创建或更新 SSA 风险处置 -export const postSSARiskDisposal = ( - data: TSSARiskDisposalRequest, -): Promise> => - axios.post>('/ssa/risk/disposal', data); +export const postSSARiskDisposal = (data: TSSARiskDisposalRequest): Promise> => + axios.post>('/api/ssa/risk/disposal', data) // GET /ssa/risk/disposal - 查询 SSA 风险处置列表 export const getSSARiskDisposals = ( - params?: { - page?: number; - limit?: number; - risk_id?: number; - task_id?: string; - }, - signal?: AbortSignal, + params?: { + page?: number + limit?: number + risk_id?: number + task_id?: string + }, + signal?: AbortSignal, ): Promise> => - axios.get>( - '/ssa/risk/disposal', - { - params, - signal, - }, - ); + axios.get>('/api/ssa/risk/disposal', { + params, + signal, + }) // GET /ssa/risk/disposal/fetch - 获取单个 SSA 风险处置详情 -export const getSSARiskDisposal = ( - id: number, - signal?: AbortSignal, -): Promise> => - axios.get>( - '/ssa/risk/disposal/fetch', - { - params: { id }, - signal, - }, - ); +export const getSSARiskDisposal = (id: number, signal?: AbortSignal): Promise> => + axios.get>('/api/ssa/risk/disposal/fetch', { + params: { id }, + signal, + }) // DELETE /ssa/risk/disposal - 删除 SSA 风险处置 -export const deleteSSARiskDisposal = ( - id: number, -): Promise> => - axios.delete>('/ssa/risk/disposal', { - params: { id }, - }); +export const deleteSSARiskDisposal = (id: number): Promise> => + axios.delete>('/api/ssa/risk/disposal', { + params: { id }, + }) diff --git a/src/apis/SSAScanTaskApi/index.ts b/src/apis/SSAScanTaskApi/index.ts index 1451d2f4..b139b2f1 100644 --- a/src/apis/SSAScanTaskApi/index.ts +++ b/src/apis/SSAScanTaskApi/index.ts @@ -1,56 +1,57 @@ -import axios from '@/utils/axios'; -import type { ResponseData } from '@/utils/commonTypes'; +import axios from '@/utils/axios' +import type { ResponseData } from '@/utils/commonTypes' import type { - TSSAScanRequest, - TSSATaskResponse, - TSSATaskQueryParams, - TSSATaskListResponse, - TSSAArtifactMetricsSummary, - TSSAArtifactEventsResponse, -} from './type'; + TSSAScanRequest, + TSSAScanModeOverride, + TSSATaskResponse, + TSSATaskQueryParams, + TSSATaskListResponse, + TSSAArtifactMetricsSummary, + TSSAArtifactEventsResponse, +} from './type' // POST /ssa/project/{id}/scan - 创建扫描任务 const scanSSAProject = ( - projectId: number, - data?: TSSAScanRequest, + projectId: number, + data?: TSSAScanRequest, + opts?: { scan_mode?: TSSAScanModeOverride }, ): Promise> => - axios.post>( - `/ssa/project/${projectId}/scan`, - data || {}, - ); + axios.post>( + `/api/ssa/project/${projectId}/scan`, + data || {}, + opts?.scan_mode && opts.scan_mode !== 'auto' ? { params: { scan_mode: opts.scan_mode } } : undefined, + ) // GET /ssa/task - 查询任务列表 -const querySSATasks = ( - params?: TSSATaskQueryParams, -): Promise> => - axios.get>('/ssa/task', { - params, - }); +const querySSATasks = (params?: TSSATaskQueryParams): Promise> => + axios.get>('/api/ssa/task', { + params, + }) // DELETE /ssa/task/{task_id} - 取消任务 const cancelSSATask = (taskId: string): Promise> => - axios.delete>(`/ssa/task/${taskId}`); + axios.delete>(`/api/ssa/task/${taskId}`) -const querySSAArtifactSummary = ( - taskId: string, -): Promise> => - axios.get>( - `/ssa/task/${taskId}/artifact/summary`, - ); +// DELETE /ssa/task/{task_id}?delete_mode=record - 删除任务记录 +const deleteSSATaskRecord = (taskId: string): Promise> => + axios.delete>(`/api/ssa/task/${taskId}`, { + params: { delete_mode: 'record' }, + }) + +const querySSAArtifactSummary = (taskId: string): Promise> => + axios.get>(`/api/ssa/task/${taskId}/artifact/summary`) const querySSAArtifactEvents = ( - taskId: string, - params?: { stage?: string; limit?: number }, + taskId: string, + params?: { stage?: string; limit?: number }, ): Promise> => - axios.get>( - `/ssa/task/${taskId}/artifact/events`, - { params }, - ); + axios.get>(`/api/ssa/task/${taskId}/artifact/events`, { params }) export { - scanSSAProject, - querySSATasks, - cancelSSATask, - querySSAArtifactSummary, - querySSAArtifactEvents, -}; + scanSSAProject, + querySSATasks, + cancelSSATask, + deleteSSATaskRecord, + querySSAArtifactSummary, + querySSAArtifactEvents, +} diff --git a/src/apis/SSAScanTaskApi/type.ts b/src/apis/SSAScanTaskApi/type.ts index cd71363c..f79e944c 100644 --- a/src/apis/SSAScanTaskApi/type.ts +++ b/src/apis/SSAScanTaskApi/type.ts @@ -1,3 +1,5 @@ +export type TSSAScanModeOverride = 'auto' | 'memory' | 'ir-db'; + // SSA 扫描请求参数 export interface TSSAScanRequest { rule_groups?: string[]; @@ -44,6 +46,8 @@ export interface TSSATask { language?: string; source_origin?: string; scan_mode?: string; + compile_action?: string; + reuse_reason?: string; error_message?: string; started_at?: number; // Unix 时间戳(秒) finished_at?: number; // Unix 时间戳(秒) diff --git a/src/apis/SyntaxFlowRuleApi/index.ts b/src/apis/SyntaxFlowRuleApi/index.ts index f48c5091..3ea346c4 100644 --- a/src/apis/SyntaxFlowRuleApi/index.ts +++ b/src/apis/SyntaxFlowRuleApi/index.ts @@ -1,139 +1,118 @@ -import axios from '@/utils/axios'; -import type { ResponseData } from '@/utils/commonTypes'; +import axios from '@/utils/axios' +import type { ResponseData } from '@/utils/commonTypes' import type { - TSyntaxFlowRule, - TSyntaxFlowRuleListResponse, - TSyntaxFlowRuleRequest, - TAsyncTaskStatusResponse, - TSyntaxFlowRuleMetadataUpdateRequest, - TSyntaxFlowRuleFilterOptions, -} from './type'; + TSyntaxFlowRule, + TSyntaxFlowRuleListResponse, + TSyntaxFlowRuleRequest, + TAsyncTaskStatusResponse, + TSyntaxFlowRuleMetadataUpdateRequest, + TSyntaxFlowRuleFilterOptions, +} from './type' // GET /syntaxflow/rule const getSyntaxFlowRules = ( - params: { - limit?: number; - page?: number; - order?: string; - order_by?: string; - rule_name?: string; - language?: string; - type?: string; - severity?: string; - purpose?: string; - tag?: string; - is_build_in_rule?: boolean; - group_name?: string; - risk_type?: string; - }, - signal?: AbortSignal, + params: { + limit?: number + page?: number + order?: string + order_by?: string + rule_name?: string + language?: string + type?: string + severity?: string + purpose?: string + tag?: string + is_build_in_rule?: boolean + group_name?: string + risk_type?: string + }, + signal?: AbortSignal, ): Promise> => - axios.get>( - `/syntaxflow/rule`, - { params, signal }, - ); + axios.get>(`/api/syntaxflow/rule`, { params, signal }) // POST /syntaxflow/rule -const postSyntaxFlowRule = ( - data: TSyntaxFlowRuleRequest, -): Promise> => - axios.post>(`/syntaxflow/rule`, data); +const postSyntaxFlowRule = (data: TSyntaxFlowRuleRequest): Promise> => + axios.post>(`/api/syntaxflow/rule`, data) // DELETE /syntaxflow/rule -const deleteSyntaxFlowRule = (params: { - rule_name?: string; - rule_id?: string; -}): Promise> => - axios.delete>(`/syntaxflow/rule`, { params }); +const deleteSyntaxFlowRule = (params: { rule_name?: string; rule_id?: string }): Promise> => + axios.delete>(`/api/syntaxflow/rule`, { + params, + }) // GET /syntaxflow/rule/fetch const fetchSyntaxFlowRule = ( - params: { rule_name?: string; rule_id?: string }, - signal?: AbortSignal, + params: { rule_name?: string; rule_id?: string }, + signal?: AbortSignal, ): Promise> => - axios.get>(`/syntaxflow/rule/fetch`, { - params, - signal, - }); + axios.get>(`/api/syntaxflow/rule/fetch`, { + params, + signal, + }) // GET /syntaxflow/rule/export const exportSyntaxFlowRules = (params: { - password?: string; - rule_name?: string; - language?: string; - type?: string; - severity?: string; - purpose?: string; - tag?: string; - is_build_in_rule?: boolean; - group_name?: string; + password?: string + rule_name?: string + language?: string + type?: string + severity?: string + purpose?: string + tag?: string + is_build_in_rule?: boolean + group_name?: string }) => - axios.get(`/syntaxflow/rule/export`, { - params, - responseType: 'blob', - }); + axios.get(`/api/syntaxflow/rule/export`, { + params, + responseType: 'blob', + }) // POST /syntaxflow/rule/import const importSyntaxFlowRules = ( - data: FormData, - config?: { onUploadProgress?: (progressEvent: any) => void }, + data: FormData, + config?: { onUploadProgress?: (progressEvent: any) => void }, ): Promise> => - axios.post>(`/syntaxflow/rule/import`, data, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - ...config, - }); + axios.post>(`/api/syntaxflow/rule/import`, data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + ...config, + }) // GET /async/task/status -const getAsyncTaskStatus = ( - taskId: string, -): Promise> => - axios.get>( - `/async/task/status`, - { - params: { task_id: taskId }, - }, - ); +const getAsyncTaskStatus = (taskId: string): Promise> => + axios.get>(`/api/async/task/status`, { + params: { task_id: taskId }, + }) // POST /syntaxflow/rules/snapshot - 创建规则快照(发布给节点同步) const createRuleSnapshot = ( - note?: string, -): Promise< - ResponseData<{ version: string; hash: string; rule_count: number }> -> => - axios.post< - never, - ResponseData<{ version: string; hash: string; rule_count: number }> - >(`/syntaxflow/rules/snapshot`, { note }); + note?: string, +): Promise> => + axios.post>( + `/api/syntaxflow/rules/snapshot`, + { note }, + ) // PUT /syntaxflow/rule/update - 更新规则元数据 const updateSyntaxFlowRuleMetadata = ( - data: TSyntaxFlowRuleMetadataUpdateRequest, + data: TSyntaxFlowRuleMetadataUpdateRequest, ): Promise> => - axios.put>( - `/syntaxflow/rule/update`, - data, - ); + axios.put>(`/api/syntaxflow/rule/update`, data) // GET /syntaxflow/rule/filter-options - 获取规则筛选选项 -const getSyntaxFlowRuleFilterOptions = ( - signal?: AbortSignal, -): Promise> => - axios.get>( - `/syntaxflow/rule/filter-options`, - { signal }, - ); +const getSyntaxFlowRuleFilterOptions = (signal?: AbortSignal): Promise> => + axios.get>(`/api/syntaxflow/rule/filter-options`, { signal }) export { - getSyntaxFlowRules, - postSyntaxFlowRule, - deleteSyntaxFlowRule, - fetchSyntaxFlowRule, - exportSyntaxFlowRules, - importSyntaxFlowRules, - getAsyncTaskStatus, - createRuleSnapshot, - updateSyntaxFlowRuleMetadata, - getSyntaxFlowRuleFilterOptions, -}; + getSyntaxFlowRules, + postSyntaxFlowRule, + deleteSyntaxFlowRule, + fetchSyntaxFlowRule, + exportSyntaxFlowRules, + importSyntaxFlowRules, + getAsyncTaskStatus, + createRuleSnapshot, + updateSyntaxFlowRuleMetadata, + getSyntaxFlowRuleFilterOptions, +} diff --git a/src/apis/SystemManagementApi/index.ts b/src/apis/SystemManagementApi/index.ts index 0fa88187..ca97012e 100644 --- a/src/apis/SystemManagementApi/index.ts +++ b/src/apis/SystemManagementApi/index.ts @@ -1,55 +1,30 @@ -import axios from '@/utils/axios'; -import type { ResponseData, TableResponseData } from '@/utils/commonTypes'; -import type { - User, - UserRequest, - TAddUserRequest, - TAddUserResponse, - PostUserOperateRequest, -} from './types'; +import axios from '@/utils/axios' +import type { ResponseData, TableResponseData } from '@/utils/commonTypes' +import type { User, UserRequest, TAddUserRequest, TAddUserResponse, PostUserOperateRequest } from './types' // 获取用户管理 表格数据 -const getUserList = ( - params: UserRequest, -): Promise>> => - axios.get>>(`/user`, { - params, - }); +const getUserList = (params: UserRequest): Promise>> => + axios.get>>(`/api/user`, { + params, + }) // 添加用户 -const postAddUser = ( - data: TAddUserRequest, -): Promise> => - axios.post>('/user', data); +const postAddUser = (data: TAddUserRequest): Promise> => + axios.post>('/api/user', data) -const putEditUser = ( - data: TAddUserRequest, -): Promise> => - axios.put>('/user', data); +const putEditUser = (data: TAddUserRequest): Promise> => + axios.put>('/api/user', data) // 重置密码 -const postUserReset = (data: { - username: string; -}): Promise> => - axios.post>( - `/user/reset?username=${data.username}`, - ); +const postUserReset = (data: { username: string }): Promise> => + axios.post>(`/api/user/reset?username=${data.username}`) // 添加用户 const deleteUser = (username: string): Promise> => - axios.delete>(`/user?username=${username}`); + axios.delete>(`/api/user?username=${username}`) // 操作用户权限 /user/operate -const postUserOperate = ( - data: PostUserOperateRequest, -): Promise> => - axios.post>('/user/operate', data); +const postUserOperate = (data: PostUserOperateRequest): Promise> => + axios.post>('/api/user/operate', data) -export { - getUserList, - postAddUser, - postUserReset, - deleteUser, - postUserOperate, - putEditUser, -}; +export { getUserList, postAddUser, postUserReset, deleteUser, postUserOperate, putEditUser } diff --git a/src/apis/login/index.ts b/src/apis/login/index.ts index f950b0c6..6a27b4d0 100644 --- a/src/apis/login/index.ts +++ b/src/apis/login/index.ts @@ -1,38 +1,25 @@ -import axios from '@/utils/axios'; -import type { ResponseData } from '@/utils/commonTypes'; -import type { - GetCaptchaRequest, - GetLicenseResponse, - PostRequestAuth, - PostResponseAuth, -} from './types'; +import axios from '@/utils/axios' +import type { ResponseData } from '@/utils/commonTypes' +import type { GetCaptchaRequest, GetLicenseResponse, PostRequestAuth, PostResponseAuth } from './types' // 获取验证码 -const getCaptcha = () => - axios.get>('/captcha'); +const getCaptcha = () => axios.get>('/api/captcha') // 登录接口 -const postLogin = ( - data: PostResponseAuth, -): Promise> => - axios.post>('/auth', data); +const postLogin = (data: PostResponseAuth): Promise> => + axios.post>('/api/auth', data) // 退出登陆接口 -const getLoginOut = (): Promise> => - axios.get>('/user/loginOut'); +const getLoginOut = (): Promise> => axios.get>('/api/user/loginOut') const getLicense = (): Promise> => - axios.get>('/license'); + axios.get>('/api/license') -const postLicense = (data: { - license: string; -}): Promise> => - axios.post>('/license', data); +const postLicense = (data: { license: string }): Promise> => + axios.post>('/api/license', data) // 校验当前账号是否存在登录状态 -const getAuth = ( - username: string, -): Promise> => - axios.get(`/auth?username=${username}`); +const getAuth = (username: string): Promise> => + axios.get(`/api/auth?username=${username}`) -export { getCaptcha, postLogin, getLicense, postLicense, getLoginOut, getAuth }; +export { getCaptcha, postLogin, getLicense, postLicense, getLoginOut, getAuth } diff --git a/src/apis/login/types.ts b/src/apis/login/types.ts index b344c98f..57a1413d 100644 --- a/src/apis/login/types.ts +++ b/src/apis/login/types.ts @@ -1,33 +1,28 @@ // 获取验证码 interface GetCaptchaRequest { - captcha_id: string; - master_image_base64: string; + captcha_id: string + master_image_base64: string } // 登录接口请求参数 interface PostResponseAuth { - username: string; - password: string; - captcha_id: string; - code: string; + username: string + password: string + captcha_id: string + code: string } interface PostRequestAuth { - token: string; - user_info: { - email: string; - roles: Array; - username: string; - }; + token: string + user_info: { + email: string + roles: Array + username: string + } } interface GetLicenseResponse { - license: string; - org: string; + license: string + org: string } -export type { - GetCaptchaRequest, - PostResponseAuth, - PostRequestAuth, - GetLicenseResponse, -}; +export type { GetCaptchaRequest, PostResponseAuth, PostRequestAuth, GetLicenseResponse } diff --git a/src/apis/mcpLinkApi/index.ts b/src/apis/mcpLinkApi/index.ts new file mode 100644 index 00000000..3d080d0e --- /dev/null +++ b/src/apis/mcpLinkApi/index.ts @@ -0,0 +1,14 @@ +import axios from '@/utils/axios' +import type { ResponseData } from '@/utils/commonTypes' +import type { McpStatusResponse, StartMcpRequest } from './type' + +// 查询状态 mcp 链接状态 +// 后端路由为 /api/admin/mcp/status,vite proxy 按 /api 前缀转发 +const getMcpStatusApi = (): Promise> => + axios.get>('/api/admin/mcp/status') + +// 启动 /停止 Mcp +const postStartMcpApi = (params: StartMcpRequest): Promise> => + axios.post>('/api/admin/mcp/control', params) + +export { getMcpStatusApi, postStartMcpApi } diff --git a/src/apis/mcpLinkApi/type.ts b/src/apis/mcpLinkApi/type.ts new file mode 100644 index 00000000..600c49da --- /dev/null +++ b/src/apis/mcpLinkApi/type.ts @@ -0,0 +1,15 @@ +interface McpStatusResponse { + command?: string + running: boolean + script?: string +} + +interface StartMcpRequest { + action: 'start' | 'stop' | 'restart' + transport?: string + host?: string + port?: number + base_url?: string +} + +export type { McpStatusResponse, StartMcpRequest } diff --git a/src/apis/reportManage/index.ts b/src/apis/reportManage/index.ts index 90779d16..f4e287ca 100644 --- a/src/apis/reportManage/index.ts +++ b/src/apis/reportManage/index.ts @@ -1,68 +1,43 @@ -import axios from '@/utils/axios'; -import type { ResponseData, TableResponseData } from '@/utils/commonTypes'; +import axios from '@/utils/axios' +import type { ResponseData, TableResponseData } from '@/utils/commonTypes' -import type { - TReportRequest, - TReportResonse, - TSensitiveMessageReqeust, - TSensitiveMessageResponse, -} from './types'; +import type { TReportRequest, TReportResonse, TSensitiveMessageReqeust, TSensitiveMessageResponse } from './types' // 获取报告管理 表格数据 -const getssetsProts = ( - data: TReportRequest, -): Promise> => - axios.post>(`/report/items`, data); +const getssetsProts = (data: TReportRequest): Promise> => + axios.post>(`/api/report/items`, data) // 删除报告管理 const deleteProts = (params: any): Promise> => - axios.delete>(`/timeline/items`, { params }); + axios.delete>(`/api/timeline/items`, { + params, + }) const getTimelinId = ( - id: number, + id: number, ): Promise< + ResponseData<{ + data: { type: string; data: { id: string; blocks: any[] } } + }> +> => + axios.get< + never, ResponseData<{ - data: { type: string; data: { id: string; blocks: any[] } }; + data: { type: string; data: { id: string; blocks: any[] } } }> -> => - axios.get< - never, - ResponseData<{ - data: { type: string; data: { id: string; blocks: any[] } }; - }> - >(`/timeline/fetch?id=${id}`); + >(`/api/timeline/fetch?id=${id}`) // 获取 报告管理 任务组 -const getReportTaskGroups = (): Promise< - ResponseData<{ list: Array }> -> => - axios.get }>>( - '/report/task-groups', - ); +const getReportTaskGroups = (): Promise }>> => + axios.get }>>('/api/report/task-groups') const getSensitiveMessagePage = ( - data: TSensitiveMessageReqeust, + data: TSensitiveMessageReqeust, ): Promise>> => - axios.post< - never, - ResponseData> - >('/assets/sensitive-info', data); + axios.post>>('/api/assets/sensitive-info', data) // 更改敏感信息状态 -const postupdateStatus = (data: { - id: number; - status: number; -}): Promise> => - axios.post>( - '/assets/sensitive-info/update-status', - data, - ); +const postupdateStatus = (data: { id: number; status: number }): Promise> => + axios.post>('/api/assets/sensitive-info/update-status', data) -export { - getssetsProts, - deleteProts, - getTimelinId, - getReportTaskGroups, - getSensitiveMessagePage, - postupdateStatus, -}; +export { getssetsProts, deleteProts, getTimelinId, getReportTaskGroups, getSensitiveMessagePage, postupdateStatus } diff --git a/src/apis/task/index.ts b/src/apis/task/index.ts index a5311a90..79659dd9 100644 --- a/src/apis/task/index.ts +++ b/src/apis/task/index.ts @@ -1,238 +1,166 @@ -import axios from '@/utils/axios'; +import axios from '@/utils/axios' import type { - ResponseData, - TableRequestParam, - TableResponseData, - // TGetTimeLineRuntimeMessage, -} from '@/utils/commonTypes'; + ResponseData, + TableRequestParam, + TableResponseData, + // TGetTimeLineRuntimeMessage, +} from '@/utils/commonTypes' import type { - StopOnRunTaskRequest, - TaskGrounpResponse, - TaskListRequest, - TaskListResponse, - // TFetchProcessResponse, - GetAnalysisScriptResponse, - TGetStroageDetailRequest, - ThreatAnalysisScriptInformationRequest, - ThreatAnalysisScriptInformationResponse, - TNodeListRequest, - TPostRpcQueryYakPluginsParams, - TPostRpcQueryYakPluginsRequest, - TPostRpcQueryYakPluginsRequestTable, - TPostStorageTaskScriptRequest, - TPostTaskStartRequest, - TTaskGroupResponse, -} from './types'; + StopOnRunTaskRequest, + TaskGrounpResponse, + TaskListRequest, + TaskListResponse, + // TFetchProcessResponse, + GetAnalysisScriptResponse, + TGetStroageDetailRequest, + ThreatAnalysisScriptInformationRequest, + ThreatAnalysisScriptInformationResponse, + TNodeListRequest, + TPostRpcQueryYakPluginsParams, + TPostRpcQueryYakPluginsRequest, + TPostRpcQueryYakPluginsRequestTable, + TPostStorageTaskScriptRequest, + TPostTaskStartRequest, + TTaskGroupResponse, +} from './types' export const getTaskList = ( - data: TableRequestParam, + data: TableRequestParam, ): Promise>> => - axios.post>>( - '/task/list/batch-invoking-script-task', - data, - ); + axios.post>>('/api/task/list/batch-invoking-script-task', data) // 将新增 / 编辑 操作告知后端 const getTaskStream = (task_id: number): Promise> => - axios.get>(`/task/stream?task_id=${task_id}`); + axios.get>(`/api/task/stream?task_id=${task_id}`) // 将需要监听的任务详情进度 告知后端 const getSubtaskSteam = (task_id: string): Promise> => - axios.get>( - `/task/subtask/stream?task_id=${task_id}`, - ); + axios.get>(`/api/task/subtask/stream?task_id=${task_id}`) // 获取任务组 -const getScriptTaskGroup = (): Promise< - ResponseData> -> => - axios.get>>( - '/task/query/batch-invoking-script-task-group?page=1&limit=-1&is_enable_schedules=true', - ); +const getScriptTaskGroup = (): Promise>> => + axios.get>>( + '/api/task/query/batch-invoking-script-task-group?page=1&limit=-1&is_enable_schedules=true', + ) // 执行普通 / 定时任务 -const getTaskRun = ( - params: StopOnRunTaskRequest, -): Promise> => - axios.get>( - '/task/start/batch-invoking-script/run', - { params }, - ); +const getTaskRun = (params: StopOnRunTaskRequest): Promise> => + axios.get>('/api/task/start/batch-invoking-script/run', { params }) // 取消执行普通 / 定时任务 -const getTaskStop = ( - params: StopOnRunTaskRequest, -): Promise> => - axios.get>( - `/task/stop/batch-invoking-script-task`, - { params }, - ); +const getTaskStop = (params: StopOnRunTaskRequest): Promise> => + axios.get>(`/api/task/stop/batch-invoking-script-task`, { params }) // 删除普通/定时任务 const deleteTask = (id: number): Promise> => - axios.delete>( - `/task/start/batch-invoking-script-task?id=${id}`, - ); + axios.delete>(`/api/task/start/batch-invoking-script-task?id=${id}`) // 获取 task table list 执行节点 -const getBatchInvokingScriptTaskNode = (): Promise< - ResponseData> -> => - axios.get>>( - '/task/start/batch-invoking-script-task ', - ); +const getBatchInvokingScriptTaskNode = (): Promise>> => + axios.get>>('/api/task/start/batch-invoking-script-task ') // 添加任务组 -const postTaskGrounp = ( - data: TTaskGroupResponse, -): Promise> => - axios.post>( - '/task/query/batch-invoking-script-task-group', - data, - ); +const postTaskGrounp = (data: TTaskGroupResponse): Promise> => + axios.post>('/api/task/query/batch-invoking-script-task-group', data) // 删除任务组 -const deleteTaskGroup = ( - params: TTaskGroupResponse, -): Promise> => - axios.delete('/task/query/batch-invoking-script-task-group', { - data: params, - }); +const deleteTaskGroup = (params: TTaskGroupResponse): Promise> => + axios.delete('/api/task/query/batch-invoking-script-task-group', { + data: params, + }) // 获取脚本列表 const getAnalysisScript = (params?: { - script_name: string; + script_name: string }): Promise>> => - axios.get< - never, - ResponseData> - >('/threat/analysis/script', { params }); + axios.get>>('/api/threat/analysis/script', { + params, + }) // 添加删除 脚本列表 标签 const postAnalysisScript = ( - data: Partial<{ - script_name: string; - tags: string[]; - }>, -): Promise> => - axios.post>('/threat/analysis/script ', data); + data: Partial<{ + script_name: string + tags: string[] + }>, +): Promise> => axios.post>('/api/threat/analysis/script ', data) // 解析 Yaklang 脚本信息(后端返回参数描述以便前端动态渲染) const postThreatAnalysisScriptInformation = ( - data: ThreatAnalysisScriptInformationRequest, + data: ThreatAnalysisScriptInformationRequest, ): Promise> => - axios.post>( - '/threat/analysis/script/infomation', - data, - ); + axios.post>( + '/api/threat/analysis/script/infomation', + data, + ) // 获取 创建脚本任务 执行节点列表 -const getNodeList = (): Promise< - ResponseData> -> => - axios.get>>( - '/node?limit=1000&node_type=scanner&alive=true', - ); +const getNodeList = (): Promise>> => + axios.get>>( + '/api/node?limit=1000&node_type=scanner&alive=true', + ) // 获取插件列表 const postRpcQueryYakPlugins = ( - data: TPostRpcQueryYakPluginsParams, -): Promise< - ResponseData< - TPostRpcQueryYakPluginsRequestTable - > -> => - axios.post< - never, - ResponseData< - TPostRpcQueryYakPluginsRequestTable< - TPostRpcQueryYakPluginsRequest[] - > - > - >('/node/scanner/rpc/query-yak-plugins', data); + data: TPostRpcQueryYakPluginsParams, +): Promise>> => + axios.post>>( + '/api/node/scanner/rpc/query-yak-plugins', + data, + ) // 创建 脚本任务 -const postTaskStart = ( - data: TPostTaskStartRequest, -): Promise> => - axios.put>( - '/task/start/batch-invoking-script/run', - data, - ); +const postTaskStart = (data: TPostTaskStartRequest): Promise> => + axios.put>('/api/task/start/batch-invoking-script/run', data) // 获取脚本详情 -const getStroageDetail = (params: { - script_name: string; -}): Promise> => - axios.get>( - '/task/start/batch-invoking-script/storage/fetch', - { params }, - ); +const getStroageDetail = (params: { script_name: string }): Promise> => + axios.get>('/api/task/start/batch-invoking-script/storage/fetch', { + params, + }) // 任务列表 编辑回显 -const getTaskStartEditDispaly = ( - id: number, -): Promise> => - axios.get>( - `/task/start/batch-invoking-script-task/fetch?id=${id}`, - ); +const getTaskStartEditDispaly = (id: number): Promise> => + axios.get>(`/api/task/start/batch-invoking-script-task/fetch?id=${id}`) // 编辑任务 确认 -const postEditScriptTask = ( - data: TPostTaskStartRequest, -): Promise> => - axios.post>( - '/update/task/start/batch-invoking-script-task', - data, - ); +const postEditScriptTask = (data: TPostTaskStartRequest): Promise> => + axios.post>('/api/update/task/start/batch-invoking-script-task', data) // 删除 脚本 -const deleteAnalysisScript = ( - script_name: string, -): Promise> => - axios.delete>('/threat/analysis/script', { - params: { script_name }, - }); +const deleteAnalysisScript = (script_name: string): Promise> => + axios.delete>('/api/threat/analysis/script', { + params: { script_name }, + }) // 添加/编辑任务模版 -const postStorageTaskScript = ( - data: TPostStorageTaskScriptRequest, - force: boolean, -): Promise> => - axios.post>( - `/task/start/batch-invoking-script/storage?force=${force}`, - data, - ); +const postStorageTaskScript = (data: TPostStorageTaskScriptRequest, force: boolean): Promise> => + axios.post>(`/api/task/start/batch-invoking-script/storage?force=${force}`, data) // 批量删除任务 /api -const deleteScriptTask = (data: { - ids: number[]; -}): Promise> => - axios.post>( - '/task/start/batch-invoking-script-task/delete', - data, - ); +const deleteScriptTask = (data: { ids: number[] }): Promise> => + axios.post>('/api/task/start/batch-invoking-script-task/delete', data) export { - getScriptTaskGroup, - postTaskGrounp, - deleteTaskGroup, - getBatchInvokingScriptTaskNode, - getTaskRun, - getTaskStop, - deleteTask, - getAnalysisScript, - postAnalysisScript, - getNodeList, - postTaskStart, - postRpcQueryYakPlugins, - getTaskStartEditDispaly, - deleteAnalysisScript, - postEditScriptTask, - postStorageTaskScript, - deleteScriptTask, - getStroageDetail, - getTaskStream, - getSubtaskSteam, - postThreatAnalysisScriptInformation, -}; + getScriptTaskGroup, + postTaskGrounp, + deleteTaskGroup, + getBatchInvokingScriptTaskNode, + getTaskRun, + getTaskStop, + deleteTask, + getAnalysisScript, + postAnalysisScript, + getNodeList, + postTaskStart, + postRpcQueryYakPlugins, + getTaskStartEditDispaly, + deleteAnalysisScript, + postEditScriptTask, + postStorageTaskScript, + deleteScriptTask, + getStroageDetail, + getTaskStream, + getSubtaskSteam, + postThreatAnalysisScriptInformation, +} diff --git a/src/apis/task/types.ts b/src/apis/task/types.ts index 776cab86..302ed3d6 100644 --- a/src/apis/task/types.ts +++ b/src/apis/task/types.ts @@ -1,232 +1,232 @@ // 任务列表 - 任务状态枚举值 enum TTaskListStatus { - running = 'running', - success = 'success', - cancel = 'cancel', - waiting = 'waiting', - disabled = 'disabled', - failed = 'failed', - enabled = 'enabled', - finished = 'finished', + running = 'running', + success = 'success', + cancel = 'cancel', + waiting = 'waiting', + disabled = 'disabled', + failed = 'failed', + enabled = 'enabled', + finished = 'finished', } // 任务列表请求数据 type TaskListResponse = Partial<{ - task_name: string; - task_groups: Array; - node_ids: Array; - task_status: Array; - task_type: 1 | 2 | 3; -}>; + task_name: string + task_groups: Array + node_ids: Array + task_status: Array + task_type: 1 | 2 | 3 +}> // 任务列表相应数据 type TaskListRequest = Partial<{ - created_at: number; - scanner: string[]; - task_group: string; - task_id: string; - script_name?: string; - project_name?: string; - status: TTaskListStatus; - uodated_at: number; - start_at: number; - end_at: number; - start_timestamp: number; - end_timestamp: number; - progress?: number; -}> & { id: number }; + created_at: number + scanner: string[] + task_group: string + task_id: string + script_name?: string + project_name?: string + status: TTaskListStatus + uodated_at: number + start_at: number + end_at: number + start_timestamp: number + end_timestamp: number + progress?: number +}> & { id: number } // 任务分组相应数据 interface TaskGrounpResponse { - name: string; - task_ids: null | Array; + name: string + task_ids: null | Array } interface TTaskGroupResponse { - group_name: string; - new_group_name?: string; + group_name: string + new_group_name?: string } // 执行/取消 普通和定时任务请求参数 interface StopOnRunTaskRequest { - task_id: number; - task_type: number; + task_id: number + task_type: number } type GetAnalysisScriptResponse = Partial<{ - id: number; - description: string; - disallow_scheduled: boolean; - script_type: string; - tags: string[]; - script_name: string; - target: string[]; - sched_type: number; - start_timestamp: number; - end_timestamp: number; - task_type: number; - params: Record; - prompt_args: Record; - ip_list: string[]; - parameter: YakScriptParamFull[]; -}>; + id: number + description: string + disallow_scheduled: boolean + script_type: string + tags: string[] + script_name: string + target: string[] + sched_type: number + start_timestamp: number + end_timestamp: number + task_type: number + params: Record + prompt_args: Record + ip_list: string[] + parameter: YakScriptParamFull[] +}> type TNodeListRequest = Partial<{ - created_at: number; - id: number; - updated_at: number; - all_user: string; - cpu_percent: number; - external_ip: string; - go_arch: string; - go_os: string; - host_id: string; - hostname: string; - ip_address: string[]; - last_updated_timestamp: number; - mac_address: string[]; - main_addr: string; - main_mac: string; - memory_percent: number; - network_download: number; - network_upload: number; - node_id: string; - node_token: string; - node_type: string; - runtime_task_list?: any; - task_running: number; - version: string; -}>; + created_at: number + id: number + updated_at: number + all_user: string + cpu_percent: number + external_ip: string + go_arch: string + go_os: string + host_id: string + hostname: string + ip_address: string[] + last_updated_timestamp: number + mac_address: string[] + main_addr: string + main_mac: string + memory_percent: number + network_download: number + network_upload: number + node_id: string + node_token: string + node_type: string + runtime_task_list?: any + task_running: number + version: string +}> type TPromptArgs = Partial<{ - target: string; - 'preset-protes': string; - ports: string; - 'enable-brute': boolean; - 'enable-cve-baseline': boolean; - execution_node: number; - plugins: string; - sched_type: number; - timestamp: number[]; - interval_seconds: string; - interval_seconds_type: number; - execution_date: number; - start_timestamp: number; -}>; + target: string + 'preset-protes': string + ports: string + 'enable-brute': boolean + 'enable-cve-baseline': boolean + execution_node: number + plugins: string + sched_type: number + timestamp: number[] + interval_seconds: string + interval_seconds_type: number + execution_date: number + start_timestamp: number +}> type TPostTaskStartRequest = Partial<{ - task_id: string; - task_group: string; - script_type: string; - params: TPromptArgs; - scanner: string[]; - first: boolean; - start_timestamp: number; - end_timestamp: number; -}>; + task_id: string + task_group: string + script_type: string + params: TPromptArgs + scanner: string[] + first: boolean + start_timestamp: number + end_timestamp: number +}> interface Pagination { - page: number; - limit: number; - order_by: string; - order: string; + page: number + limit: number + order_by: string + order: string } type TPostRpcQueryYakPluginsParams = Partial<{ - pagination: Pagination; - nodes_id: string[]; - exclude_types: string[]; - keyword: string; - groups: string[]; -}>; + pagination: Pagination + nodes_id: string[] + exclude_types: string[] + keyword: string + groups: string[] +}> type TPostRpcQueryYakPluginsRequest = Partial<{ - CreatedAt: number; - Id: number; - Params: any; - ScriptName: string; - Type: string; -}>; + CreatedAt: number + Id: number + Params: any + ScriptName: string + Type: string +}> // 请求/响应:解析 Yaklang 脚本信息 interface ThreatAnalysisScriptInformationRequest { - script_name?: string; - script_content?: string; + script_name?: string + script_content?: string } interface YakScriptParamFull { - // same with feild_name - paramName?: string; - // same with default_value - paramValue?: string; - typeVerbose?: string; - fieldVerbose?: string; - help?: string; - required?: boolean; - group?: string; - extraSetting?: string; - methodType?: string; - jsonSchema?: string; - uiSchema?: string; + // same with feild_name + paramName?: string + // same with default_value + paramValue?: string + typeVerbose?: string + fieldVerbose?: string + help?: string + required?: boolean + group?: string + extraSetting?: string + methodType?: string + jsonSchema?: string + uiSchema?: string } interface ThreatAnalysisScriptInformationResponse { - cli_parameter?: YakScriptParamFull[]; + cli_parameter?: YakScriptParamFull[] } interface TPostRpcQueryYakPluginsRequestTable { - list: T; - groups: string[]; - pagemeta: { - limit: number; - page: number; - total: number; - total_page: number; - order: string; - order_by: string; - }; + list: T + groups: string[] + pagemeta: { + limit: number + page: number + total: number + total_page: number + order: string + order_by: string + } } // 添加/编辑任务模版请求参数 interface TPostStorageTaskScriptRequest { - script_name?: string; - description?: string; - script_type?: string; - param_files?: string; - tags?: string[]; - name?: string; - params: { - target: string; - 'preset-protes': string[]; - ports: string; - 'enable-brute': boolean; - 'enable-cve-baseline': boolean; - }; - script: string; + script_name?: string + description?: string + script_type?: string + param_files?: string + tags?: string[] + name?: string + params: { + target: string + 'preset-protes': string + ports: string + 'enable-brute': boolean + 'enable-cve-baseline': boolean + } + script: string } // 获取 脚本 详情 type TGetStroageDetailRequest = { - prompt_args: Pick; -} & Omit; + prompt_args: Pick +} & Omit export type { - TaskGrounpResponse, - TTaskGroupResponse, - TaskListResponse, - TaskListRequest, - StopOnRunTaskRequest, - GetAnalysisScriptResponse, - TNodeListRequest, - TPostTaskStartRequest, - TPostRpcQueryYakPluginsParams, - TPostRpcQueryYakPluginsRequest, - TPostRpcQueryYakPluginsRequestTable, - TPostStorageTaskScriptRequest, - TGetStroageDetailRequest, - ThreatAnalysisScriptInformationRequest, - ThreatAnalysisScriptInformationResponse, - YakScriptParamFull, -}; - -export { TTaskListStatus }; + TaskGrounpResponse, + TTaskGroupResponse, + TaskListResponse, + TaskListRequest, + StopOnRunTaskRequest, + GetAnalysisScriptResponse, + TNodeListRequest, + TPostTaskStartRequest, + TPostRpcQueryYakPluginsParams, + TPostRpcQueryYakPluginsRequest, + TPostRpcQueryYakPluginsRequestTable, + TPostStorageTaskScriptRequest, + TGetStroageDetailRequest, + ThreatAnalysisScriptInformationRequest, + ThreatAnalysisScriptInformationResponse, + YakScriptParamFull, +} + +export { TTaskListStatus } diff --git a/src/apis/taskDetail/index.ts b/src/apis/taskDetail/index.ts index 15938c7f..800e34ee 100644 --- a/src/apis/taskDetail/index.ts +++ b/src/apis/taskDetail/index.ts @@ -1,152 +1,126 @@ -import axios from '@/utils/axios'; -import type { ResponseData, TableResponseData } from '@/utils/commonTypes'; +import axios from '@/utils/axios' +import type { ResponseData, TableResponseData } from '@/utils/commonTypes' import type { - TReportTableResponse, - TGetAssetsProtsRequest, - TGetAssetsProtsResponse, - TGetAssetsVulnsRequest, - TGetAssetsVulnsResponse, - TGetAssertsDataResponse, - TTaskDetail, - TGetAssetsValueFilterResponse, - TGetAssetsProtsFilterResponse, -} from './types'; -import type { Palm } from '@/gen/schema'; + TReportTableResponse, + TGetAssetsProtsRequest, + TGetAssetsProtsResponse, + TGetAssetsVulnsRequest, + TGetAssetsVulnsResponse, + TGetAssertsDataResponse, + TTaskDetail, + TGetAssetsValueFilterResponse, + TGetAssetsProtsFilterResponse, + TDeleteAssetsPortsBody, + TDeleteAssetsHandlerResponse, +} from './types' +import type { Palm } from '@/gen/schema' // 获取任务详情 基础信息 -const getTaskDetail = ( - form_runtime_id: string, -): Promise> => - axios.get>( - `/task/detail?task_id=${form_runtime_id}`, - ); +const getTaskDetail = (form_runtime_id: string): Promise> => + axios.get>(`/api/task/detail?task_id=${form_runtime_id}`) const getTaskDetailTop = (id: number): Promise> => - axios.get>( - `/task/start/batch-invoking-script-task/fetch?id=${id}`, - ); + axios.get>(`/api/task/start/batch-invoking-script-task/fetch?id=${id}`) // 获取端口资产 表格数据 const postAssetsProts = ( - data: TGetAssetsProtsRequest, + data: TGetAssetsProtsRequest, ): Promise>> => - axios.post>>( - `/assets/ports`, - data, - ); + axios.post>>(`/api/assets/ports`, data) // 获取端口资产 高级筛选信息 -const getAssetsProtsFilter = (params?: { - task_id?: string; -}): Promise> => - axios.get(`/assets/ports`, { params }); +const getAssetsProtsFilter = (params?: { task_id?: string }): Promise> => + axios.get(`/api/assets/ports`, { params }) // 获取漏洞与风险 高级筛选信息 -const getAssetsValueFilter = (params?: { - task_id?: string; -}): Promise> => - axios>(`/assets/vulns`, { - params, - }); +const getAssetsValueFilter = (params?: { task_id?: string }): Promise> => + axios>(`/api/assets/vulns`, { + params, + }) // 获取漏洞与风险 表格数据 const postAssetsVulns = ( - data: TGetAssetsVulnsRequest, + data: TGetAssetsVulnsRequest, ): Promise>> => - axios.post>>( - `/assets/vulns`, - data, - ); + axios.post>>(`/api/assets/vulns`, data) // 获取报告详情 const getBatchInvokingScript = (params: { - task_id: string; - page: number; + task_id: string + page: number }): Promise>> => - axios.get>>( - `/task/start/batch-invoking-script/runtimes`, - { params }, - ); + axios.get>>( + `/api/task/start/batch-invoking-script/runtimes`, + { params }, + ) // 获取报告json const getTimelinRuntimeId = ( - runtime_id: string, + runtime_id: string, ): Promise< + ResponseData<{ + type: string + data: { data: { id: string; blocks: any[] } } + }> +> => + axios.get< + never, ResponseData<{ - type: string; - data: { data: { id: string; blocks: any[] } }; + type: string + data: { data: { id: string; blocks: any[] } } }> -> => - axios.get< - never, - ResponseData<{ - type: string; - data: { data: { id: string; blocks: any[] } }; - }> - >(`/timeline/fetch?runtime_id=${runtime_id}`); + >(`/api/timeline/fetch?runtime_id=${runtime_id}`) // 资产数据 表格数据 const postAssertsData = ( - params: TGetAssetsVulnsRequest, + params: TGetAssetsVulnsRequest, ): Promise>> => - axios.post>>( - `/assets/data`, - params, - ); + axios.post>>(`/api/assets/data`, params) // 获取 存活状态 高级筛选 const getAssertsDataRiskInfo = (params: { - task_id: string; -}): Promise< - ResponseData -> => - axios.get< - never, - ResponseData< - TGetAssertsDataResponse & { count: TGetAssertsDataResponse } - > - >(`/assets/data/riskInfo`, { params }); + task_id: string +}): Promise> => + axios.get>( + `/api/assets/data/riskInfo`, + { params }, + ) + +/** 删除端口资产:传 `ids` 勾选删除,或 `delete_all: true` 加当前表格筛选条件 */ +const deleteAssetsPorts = (data: TDeleteAssetsPortsBody): Promise> => + axios.delete>(`/api/assets/ports`, { data }) const getAssertsDataStateInfo = (params: { - task_id: string; -}): Promise< - ResponseData< - Omit & { state: { open: number } } - > -> => - axios.get< - never, - ResponseData< - Omit & { state: { open: number } } - > - >(`/assets/data/stateInfo`, { params }); + task_id: string +}): Promise & { state: { open: number } }>> => + axios.get & { state: { open: number } }>>( + `/api/assets/data/stateInfo`, + { params }, + ) // 获取资产数据 高级筛选存活状态 -const getAssertsDataStateTable = ( - task_id: string, -): Promise>> => - axios.get>>( - `/asserts/data/state_table?task_id=${task_id}`, - ); +const getAssertsDataStateTable = (task_id: string): Promise>> => + axios.get>>( + `/api/asserts/data/state_table?task_id=${task_id}`, + ) // 发送邮箱 -const PostSendEmailReportData = ( - data: Palm.SendSmtp, -): Promise> => - axios.post>('/send/smtp', data); +const PostSendEmailReportData = (data: Palm.SendSmtp): Promise> => + axios.post>('/api/send/smtp', data) export { - postAssetsProts, - getAssetsProtsFilter, - postAssetsVulns, - getTaskDetail, - getBatchInvokingScript, - postAssertsData, - getAssertsDataStateTable, - getTaskDetailTop, - PostSendEmailReportData, - getTimelinRuntimeId, - getAssertsDataRiskInfo, - getAssetsValueFilter, - getAssertsDataStateInfo, -}; + postAssetsProts, + getAssetsProtsFilter, + postAssetsVulns, + getTaskDetail, + getBatchInvokingScript, + postAssertsData, + getAssertsDataStateTable, + getTaskDetailTop, + PostSendEmailReportData, + getTimelinRuntimeId, + getAssertsDataRiskInfo, + getAssetsValueFilter, + getAssertsDataStateInfo, + deleteAssetsPorts, +} diff --git a/src/apis/taskDetail/types.ts b/src/apis/taskDetail/types.ts index b5484f87..bd89f8ab 100644 --- a/src/apis/taskDetail/types.ts +++ b/src/apis/taskDetail/types.ts @@ -1,145 +1,189 @@ // 获取详情 interface TTaskDetail { - ip_num: number; - port_num: number; - risk_num: number; - sensitive_num: number; + ip_num: number + port_num: number + risk_num: number + sensitive_num: number } // 获取端口资产 请求参数 interface TGetAssetsProtsRequest { - cpes?: string[]; - fingerprint?: string[]; - form_runtime_id?: string; - hosts?: string[]; - limit?: number; - order?: any; - order_by?: any; - page?: number; - ports?: string[]; - reason?: string[]; - services?: string[]; - state?: string; - tags?: string[]; - task_id?: string; - Ppge?: number; + cpes?: string[] + fingerprint?: string[] + form_runtime_id?: string + hosts?: string[] + limit?: number + order?: any + order_by?: any + page?: number + ports?: string[] + reason?: string[] + services?: string[] + state?: string + tags?: string[] + task_id?: string + Ppge?: number + service_type?: string[] } // 获取端口资产 响应参数 interface TGetAssetsProtsResponse { - cpes: string[]; - fingerprint: string; - host: string; - ip_integer: number; - port: number; - proto: string; - reason: string; - service_type: string; - state: string; - tags: string[]; - created_at: number; - id: number; - updated_at: number; + cpes: string[] + fingerprint: string + host: string + ip_integer: number + port: number + proto: string + reason: string + service_type: string + state: string + tags: string[] + created_at: number + id: number + updated_at: number } interface TGetAssetsProtsFilterResponse { - list: Array<{ - explain: string | null; - key: string; - value: string; - }>; + list: Array<{ + explain: string | null + key: string + value: string + }> } interface TGetAssetsVulnsRequest { - form_runtime_id?: string; - task_id?: string; - ip?: string; - keyword?: string; - limit?: number; - network?: string; - order?: any; - order_by?: string; - page?: number; - plugin?: string; - port?: string; - risk_type_verbose?: string; - severity?: string; - target?: string; - target_type?: string; - title?: string; + form_runtime_id?: string + task_id?: string + ip?: string + keyword?: string + limit?: number + network?: string + order?: any + order_by?: string + page?: number + plugin?: string + port?: string + risk_type_verbose?: string + severity?: string + target?: string + target_type?: string + title?: string } interface TGetAssetsVulnsResponse { - created_at: number; - detail: Record; - from_yak_script: string; - hash?: any; - host: string; - id: number; - ip_addr: string; - is_private_net: boolean; - payload: string; - plugin: string; - port: number; - reverse_token: string; - risk_type: string; - risk_type_verbose: string; - severity: string; - target: string; - target_raw: { - target: string; - title: string; - }; - target_type: string; - title: string; - title_verbose: string; - updated_at: number; - url: string; + created_at: number + detail: Record + from_yak_script: string + hash?: any + host: string + id: number + ip_addr: string + is_private_net: boolean + payload: string + plugin: string + port: number + reverse_token: string + risk_type: string + risk_type_verbose: string + severity: string + target: string + target_raw: { + target: string + title: string + } + target_type: string + title: string + title_verbose: string + updated_at: number + url: string } interface TGetAssertsDataResponse { - created_at: number; - critical: number; - high: number; - host: string; - id?: any; - state: 'open' | 'close' | 'unknwon'; - updated_at: number; - warning: number; - low?: number; - level: string; + created_at: number + critical: number + high: number + host: string + id?: any + state: 'open' | 'close' | 'unknwon' + updated_at: number + warning: number + low?: number + level: string } // 历史执行记录 报告信息响应数据 interface TReportTableResponse { - created_at: number; - id: number; - runtime_id: string; - subtask_failed_count: number; - subtask_succeeded_count: number; - subtask_total: number; - task_id: string; - updated_at: number; + created_at: number + id: number + runtime_id: string + subtask_failed_count: number + subtask_succeeded_count: number + subtask_total: number + task_id: string + updated_at: number } type TTableFilterList = Array<{ - explain?: null; - key?: string; - value?: string; -}>; + explain?: null + key?: string + value?: string +}> interface TGetAssetsValueFilterResponse { - list: TTableFilterList; - severity: TTableFilterList; + list: TTableFilterList + severity: TTableFilterList +} + +/** 端口资产:勾选删除 */ +interface TDeleteAssetsPortsByIds { + ids: number[] +} + +/** 端口资产:按当前筛选删除(可附带表格其它筛选字段) */ +interface TDeleteAssetsPortsByFilter extends Record { + delete_all: true + task_id?: string + host?: string + port?: string + service_type?: string[] + execute_node?: string[] +} + +type TDeleteAssetsPortsBody = TDeleteAssetsPortsByIds | TDeleteAssetsPortsByFilter + +/** 漏洞资产:勾选删除 */ +interface TDeleteAssetsVulnsByIds { + ids: number[] +} + +/** 漏洞资产:按当前筛选删除(可附带表格其它筛选字段) */ +interface TDeleteAssetsVulnsByFilter extends Record { + delete_all: true + task_id?: string + severity?: string[] + keyword?: string + target?: string + execute_node?: string[] +} + +type TDeleteAssetsVulnsBody = TDeleteAssetsVulnsByIds | TDeleteAssetsVulnsByFilter + +/** 资产删除接口成功时 data 字段形态(与后端 Handler 名一致) */ +interface TDeleteAssetsHandlerResponse { + from: string + ok: boolean } export type { - TGetAssetsProtsRequest, - TGetAssetsProtsResponse, - TGetAssetsVulnsRequest, - TGetAssetsVulnsResponse, - TReportTableResponse, - TGetAssertsDataResponse, - TTaskDetail, - TGetAssetsValueFilterResponse, - TGetAssetsProtsFilterResponse, -}; + TGetAssetsProtsRequest, + TGetAssetsProtsResponse, + TGetAssetsVulnsRequest, + TGetAssetsVulnsResponse, + TReportTableResponse, + TGetAssertsDataResponse, + TTaskDetail, + TGetAssetsValueFilterResponse, + TGetAssetsProtsFilterResponse, + TDeleteAssetsPortsBody, + TDeleteAssetsVulnsBody, + TDeleteAssetsHandlerResponse, +} diff --git a/src/assets/compoments/images-1.png b/src/assets/compoments/images-1.png new file mode 100644 index 00000000..3d233419 Binary files /dev/null and b/src/assets/compoments/images-1.png differ diff --git a/src/assets/compoments/images-2.png b/src/assets/compoments/images-2.png new file mode 100644 index 00000000..73e55130 Binary files /dev/null and b/src/assets/compoments/images-2.png differ diff --git a/src/assets/icon/colors.tsx b/src/assets/icon/colors.tsx index 84852750..19de7af8 100644 --- a/src/assets/icon/colors.tsx +++ b/src/assets/icon/colors.tsx @@ -2616,3 +2616,171 @@ const ColorsAI = () => { export const ColorsAIIcon = (props: Partial) => { return ; }; + +const ColorsSourceCode = () => { + const id = uuidv4(); + return ( + + + + + + + + + + + + + + + + + ); +}; +/** + * @description Outlin + */ +export const ColorsSourceCodeIcon = (props: Partial) => { + return ; +}; + +const ColorsPreViewMD = () => { + const id = uuidv4(); + return ( + + + + + + + + + + + + + + + + + ); +}; +/** + * @description Outlin + */ +export const ColorsPreViewMDIcon = (props: Partial) => { + return ; +}; diff --git a/src/assets/icon/outline.tsx b/src/assets/icon/outline.tsx index 828c3d07..2cfc45d3 100644 --- a/src/assets/icon/outline.tsx +++ b/src/assets/icon/outline.tsx @@ -8409,3 +8409,108 @@ const OutlineEarOff = () => ( export const OutlineEarOffIcon = (props: Partial) => { return ; }; + +const OutlineAI = () => ( + + + + + +); +/* + * @description Outline/AI + */ +export const OutlineAIIcon = (props: Partial) => { + return ; +}; + +const OutlineChevronsUpDown = () => ( + + + +); +/* + * @description Outline/ChevronsUpDown + */ +export const OutlineChevronsUpDownIcon = (props: Partial) => { + return ; +}; + +const OutlineChevronsDownUp = () => ( + + + +); +/* + * @description Outline/ChevronsDownUp + */ +export const OutlineChevronsDownUpIcon = (props: Partial) => { + return ; +}; + +const OutlineMessageCirclePlus = () => ( + + + +); + +/** + * @description Outline/MessageCirclePlus + */ +export const OutlineMessageCirclePlusIcon = (props: Partial) => { + return ; +}; diff --git a/src/assets/icon/solid.tsx b/src/assets/icon/solid.tsx index ae05e769..b7bfe524 100644 --- a/src/assets/icon/solid.tsx +++ b/src/assets/icon/solid.tsx @@ -3300,6 +3300,28 @@ export const SolidLightningboltIcon = (props: Partial) => { return ; }; +const SolidTool = () => ( + + + +); + +/** + * @description Icon/Solid/Tool + */ +export const SolidToolIcon = (props: Partial) => { + return ; +}; + const SolidLink = () => ( { + return ( + + + + ) +} + +const AIOutlinedIcon = () => { + return ( + + ) +} + +export default AIOutlinedIcon diff --git a/src/assets/menu/index.ts b/src/assets/menu/index.ts index 3edba705..6b0a1e3c 100644 --- a/src/assets/menu/index.ts +++ b/src/assets/menu/index.ts @@ -1,7 +1,8 @@ -export { default as LoginIcon } from './LoginIcon'; -export { default as TaskCenterIcon } from './TaskCenterIcon'; -export { default as ReportManageIcon } from './ReportManageIcon'; -export { default as DataServiceIcon } from './DataServiceIcon'; -export { default as NodeConfigIcon } from './NodeConfigIcon'; -export { default as SystemManagementIcon } from './SystemManagementIcon'; -export { default as PublicCodec } from './CodecIcon'; +export { default as LoginIcon } from './LoginIcon' +export { default as TaskCenterIcon } from './TaskCenterIcon' +export { default as ReportManageIcon } from './ReportManageIcon' +export { default as DataServiceIcon } from './DataServiceIcon' +export { default as NodeConfigIcon } from './NodeConfigIcon' +export { default as SystemManagementIcon } from './SystemManagementIcon' +export { default as PublicCodec } from './CodecIcon' +export { default as AIOutlinedIcon } from './AIOutlinedIcon' diff --git a/src/assets/newIcon.tsx b/src/assets/newIcon.tsx index 6d81df07..ab1c9803 100644 --- a/src/assets/newIcon.tsx +++ b/src/assets/newIcon.tsx @@ -2720,3 +2720,21 @@ const OutlinePlus = () => ( export const OutlinePlusIcon = (props: Partial) => { return ; }; + +const LineConversionSvg = () => ( + + + + +); +export const LineConversionIcon: React.FC = (props: any) => { + return ; +}; diff --git a/src/compoments/DataExport/DataExport.tsx b/src/compoments/DataExport/DataExport.tsx index 94037313..7844ac75 100644 --- a/src/compoments/DataExport/DataExport.tsx +++ b/src/compoments/DataExport/DataExport.tsx @@ -1,204 +1,170 @@ -import React, { useRef, useState } from 'react'; -import type { ButtonProps } from 'antd'; -import { Button, Modal, Space, Pagination } from 'antd'; -import showErrorMessage from '@/utils/showErrorMessage'; -import type { CellSetting } from './toExcel'; -import { export_json_to_excel } from './toExcel'; -import { useMemoizedFn } from 'ahooks'; -import './DataExport.css'; +import React, { useRef, useState } from 'react' +import type { ButtonProps } from 'antd' +import { Button, Modal, Space, Pagination } from 'antd' +import showErrorMessage from '@/utils/showErrorMessage' +import type { CellSetting } from './toExcel' +import { export_json_to_excel } from './toExcel' +import { useMemoizedFn } from 'ahooks' +import './DataExport.css' interface QueryGeneralResponse { - data: T[]; - pagemeta: PaginationSchema; + data: T[] + pagemeta: PaginationSchema } interface PaginationSchema { - page: number; - limit: number; - total: number; - total_page: number; + page: number + limit: number + total: number + total_page: number } let genDefaultPagination = (limit?: number, page?: number) => { - return { - limit: limit || 10, - page: page || 1, - } as any; -}; + return { + limit: limit || 10, + page: page || 1, + } as any +} interface ExportExcelProps { - btnProps?: ButtonProps; - getData: (query: PaginationSchema) => Promise; - fileName?: string; - pageSize?: number; - showButton?: boolean; - text?: string; - openModal?: boolean; + btnProps?: ButtonProps + getData: (query: PaginationSchema) => Promise + fileName?: string + pageSize?: number + showButton?: boolean + text?: string + openModal?: boolean } interface resProps { - header: string[]; - exportData: Array; - response: QueryGeneralResponse; - optsSingleCellSetting?: CellSetting; + header: string[] + exportData: Array + response: QueryGeneralResponse + optsSingleCellSetting?: CellSetting } interface PaginationProps { - page: number; - limit: number; + page: number + limit: number } -const maxCellNumber = 10000; // 最大单元格10w +const maxCellNumber = 10000 // 最大单元格10w export const ExportExcel: React.FC = (props) => { - const { - btnProps, - getData, - fileName = '端口资产', - pageSize = 10000, - showButton = true, - text, - // openModal = false, - } = props; - const [loading, setLoading] = useState(false); - const [visible, setVisible] = useState(false); - const [frequency, setFrequency] = useState(0); - const exportDataBatch = useRef>([]); // 保存导出的数据 - const exportNumber = useRef(); // 导出次数 - const headerExcel = useRef([]); // excel的头部 - const optsCell = useRef(); // excel的头部 - const [pagination, setPagination] = useState>({ - data: [], - pagemeta: genDefaultPagination(pageSize, 1), - }); + const { + btnProps, + getData, + fileName = '端口资产', + pageSize = 10000, + showButton = true, + text, + // openModal = false, + } = props + const [loading, setLoading] = useState(false) + const [visible, setVisible] = useState(false) + const [frequency, setFrequency] = useState(0) + const exportDataBatch = useRef>([]) // 保存导出的数据 + const exportNumber = useRef() // 导出次数 + const headerExcel = useRef([]) // excel的头部 + const optsCell = useRef() // excel的头部 + const [pagination, setPagination] = useState>({ + data: [], + pagemeta: genDefaultPagination(pageSize, 1), + }) - const toExcel = useMemoizedFn((query = { limit: pageSize, Ppge: 1 }) => { - setLoading(true); - getData(query as any) - .then((res: resProps) => { - if (res) { - const { - header, - exportData, - response, - optsSingleCellSetting, - } = res; - const { pagemeta } = response; - const totalCellNumber = header.length * exportData.length; - if ( - totalCellNumber < maxCellNumber && - pagemeta.total <= pageSize - ) { - // // 单元格数量小于最大单元格数量,直接导出 - export_json_to_excel({ - header: res.header, - data: res.exportData, - filename: `${fileName}1-${exportData.length}`, - autoWidth: true, - bookType: 'xlsx', - optsSingleCellSetting, - }); - } else { - // 分批导出 - const frequency = Math.ceil( - totalCellNumber / maxCellNumber, - ); // 导出次数 - exportNumber.current = Math.floor( - maxCellNumber / header.length, - ); // 每次导出的数量 - exportDataBatch.current = exportData; - headerExcel.current = header; - setFrequency(frequency); - setVisible(true); - } - optsCell.current = optsSingleCellSetting; - setPagination(response); - } - }) - .catch((e: any) => { - showErrorMessage(`数据导出失败: ${e}`); + const toExcel = useMemoizedFn((query = { limit: pageSize, Ppge: 1 }) => { + setLoading(true) + getData(query as any) + .then((res: resProps) => { + if (res) { + const { header, exportData, response, optsSingleCellSetting } = res + const { pagemeta } = response + const totalCellNumber = header.length * exportData.length + if (totalCellNumber < maxCellNumber && pagemeta.total <= pageSize) { + // // 单元格数量小于最大单元格数量,直接导出 + export_json_to_excel({ + header: res.header, + data: res.exportData, + filename: `${fileName}1-${exportData.length}`, + autoWidth: true, + bookType: 'xlsx', + optsSingleCellSetting, }) - .finally(() => setTimeout(() => setLoading(false), 300)); - }); + } else { + // 分批导出 + const frequency = Math.ceil(totalCellNumber / maxCellNumber) // 导出次数 + exportNumber.current = Math.floor(maxCellNumber / header.length) // 每次导出的数量 + exportDataBatch.current = exportData + headerExcel.current = header + setFrequency(frequency) + setVisible(true) + } + optsCell.current = optsSingleCellSetting + setPagination(response) + } + }) + .catch((e: any) => { + showErrorMessage(`数据导出失败: ${e}`) + }) + .finally(() => setTimeout(() => setLoading(false), 300)) + }) - // 分批导出 - const inBatchExport = (index: number) => { - if (!exportNumber.current) return; - const firstIndx = exportNumber.current * index; - const lastIndex = - (index === frequency - 1 && exportDataBatch.current?.length) || - (exportNumber.current && exportNumber.current * (index + 1)); - const name = `${fileName}(第${pagination.pagemeta.page}页${ - exportNumber.current && firstIndx + 1 - }-${lastIndex})`; - const list: Array = exportDataBatch.current?.slice( - firstIndx, - lastIndex + 1, - ); - export_json_to_excel({ - header: headerExcel.current, - data: list, - filename: name, - autoWidth: true, - bookType: 'xlsx', - optsSingleCellSetting: optsCell.current, - }); - }; + // 分批导出 + const inBatchExport = (index: number) => { + if (!exportNumber.current) return + const firstIndx = exportNumber.current * index + const lastIndex = + (index === frequency - 1 && exportDataBatch.current?.length) || + (exportNumber.current && exportNumber.current * (index + 1)) + const name = `${fileName}(第${pagination.pagemeta.page}页${exportNumber.current && firstIndx + 1}-${lastIndex})` + const list: Array = exportDataBatch.current?.slice(firstIndx, lastIndex + 1) + export_json_to_excel({ + header: headerExcel.current, + data: list, + filename: name, + autoWidth: true, + bookType: 'xlsx', + optsSingleCellSetting: optsCell.current, + }) + } - const onChange = (page: number, pageSize?: number) => { - const query: PaginationProps = { - page, - limit: pageSize || 10, - }; - toExcel(query); - }; - return ( - <> - {showButton ? ( - - ) : ( - toExcel()}>{text || '导出Excel'} - )} - setVisible(false)} - footer={null} - > - - {Array.from({ length: frequency }).map((_, index) => ( - - ))} - -
- `共 ${total} 条`} - hideOnSinglePage={true} - onChange={onChange} - /> -
-
- - ); -}; + const onChange = (page: number, pageSize?: number) => { + const query: PaginationProps = { + page, + limit: pageSize || 10, + } + toExcel(query) + } + return ( + <> + {showButton ? ( + + ) : ( + toExcel()}>{text || '导出Excel'} + )} + setVisible(false)} footer={null}> + + {Array.from({ length: frequency }).map((_, index) => ( + + ))} + +
+ `共 ${total} 条`} + hideOnSinglePage={true} + onChange={onChange} + /> +
+
+ + ) +} diff --git a/src/compoments/ImportExportModal/ImportExportModal.module.scss b/src/compoments/ImportExportModal/ImportExportModal.module.scss new file mode 100644 index 00000000..b2d76fa3 --- /dev/null +++ b/src/compoments/ImportExportModal/ImportExportModal.module.scss @@ -0,0 +1,14 @@ +.import-export-modal { + .export-hint { + background: var(--Colors-Use-Green-Bg); + color: var(--Colors-Use-Neutral-Text-3-Secondary); + padding: 8px 16px; + margin-bottom: 15px; + } + .import-hint { + background: var(--Colors-Use-Error-Bg); + color: var(--Colors-Use-Neutral-Text-3-Secondary); + padding: 8px 16px; + margin-bottom: 16px; + } +} diff --git a/src/compoments/ImportExportModal/ImportExportModal.tsx b/src/compoments/ImportExportModal/ImportExportModal.tsx new file mode 100644 index 00000000..2968a631 --- /dev/null +++ b/src/compoments/ImportExportModal/ImportExportModal.tsx @@ -0,0 +1,267 @@ +import { memo, useEffect, useMemo, useRef } from 'react' +import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton' +import { YakitModal, type YakitModalProp } from '@/compoments/yakitUI/YakitModal/YakitModal' +import type { LogListInfo } from '@/compoments/YakitUploadModal/YakitUploadModal' +import { ImportAndExportStatusInfo } from '@/compoments/YakitUploadModal/YakitUploadModal' +import { yakitNotify } from '@/utils/notification' +import { useMemoizedFn, useSafeState } from 'ahooks' +import { Form, type FormInstance, type FormProps } from 'antd' +import styles from './ImportExportModal.module.scss' +import { postAiforgeExport, postAiforgeImport } from '@/apis/AiEventApi' +import type { + ExportAIForgeRequest, + ExportImportAIForgeProgress, + ImportAIForgeRequest, +} from '@/pages/AIAgent/ai-agent/forgeName/type' + +// const { ipcRenderer } = window.require('electron'); + +const ImportExportModalSize = { + export: { + width: 520, + labelCol: 5, + wrapperCol: 18, + }, + import: { + width: 720, + labelCol: 6, + wrapperCol: 17, + }, +} + +type IsProgressFinished

= (progress: P) => boolean +type GetProgressValue

= (progress: P) => number + +export type ImportExportModalExtra = { + hint: boolean +} & { + title: string + type: 'export' | 'import' + apiKey: string +} +interface ImportExportModalProps { + getContainer?: HTMLElement + extra: ImportExportModalExtra + hasDesc?: boolean + modelProps?: YakitModalProp + formProps?: FormProps + renderForm: (form: FormInstance) => React.ReactNode + onBeforeSubmit?: (values: F) => Promise | void + onSubmitForm: (values: F) => R + getProgressValue: GetProgressValue

+ isProgressFinished: IsProgressFinished

+ getlogListInfo?: (stream: P[]) => LogListInfo[] + onFinished: (result: boolean) => void +} +/** + * 通用导入导出组件,参考ForgeName组件 + */ +const ImportExportModalInner = (props: ImportExportModalProps) => { + const { + getContainer, + extra, + hasDesc = true, + modelProps = {}, + formProps = {}, + renderForm, + onBeforeSubmit, + onSubmitForm, + getProgressValue, + isProgressFinished, + getlogListInfo, + onFinished, + } = props + + const [form] = Form.useForm() + + const abortControllerRef = useRef() + const [showProgressStream, setShowProgressStream] = useSafeState(false) + const importExportStreamRef = useRef([]) + const [progressStream, setProgressStream] = useSafeState([]) + + const handleReset = useMemoizedFn(() => { + abortControllerRef.current = undefined + setShowProgressStream(false) + importExportStreamRef.current = [] + setProgressStream([]) + }) + + const onSubmit = useMemoizedFn(async () => { + try { + const values = form.getFieldsValue() as F + await onBeforeSubmit?.(values) + const params = onSubmitForm(values) + + importExportStreamRef.current = [] + setProgressStream([]) + setShowProgressStream(true) + + const controller = new AbortController() + abortControllerRef.current = controller + + const handleProgress = (progress: ExportImportAIForgeProgress) => { + importExportStreamRef.current.unshift(progress as unknown as P) + setProgressStream([...importExportStreamRef.current]) + } + + if (extra.type === 'export') { + await postAiforgeExport(params as unknown as ExportAIForgeRequest, handleProgress, controller.signal) + } else { + await postAiforgeImport(params as unknown as ImportAIForgeRequest, handleProgress, controller.signal) + } + } catch (e) { + if ((e as Error)?.name !== 'AbortError') { + yakitNotify('error', `[${extra.apiKey}] error: ${e}`) + } + } + }) + + const onCancelStream = useMemoizedFn(() => { + abortControllerRef.current?.abort() + abortControllerRef.current = undefined + }) + + useEffect(() => { + if (progressStream.length && isProgressFinished(progressStream[0])) { + onFinished(true) + } + }, [progressStream.length]) + const streamData = useMemo(() => { + return { + Progress: progressStream.length ? getProgressValue(progressStream[0]) : 0, + } + }, [progressStream.length]) + const logListInfo = useMemo(() => { + return getlogListInfo?.(progressStream) || [] + }, [progressStream.length]) + const progressTitle = useMemo(() => { + return extra.type === 'export' + ? progressStream.length + ? isProgressFinished(progressStream[0]) + ? '导出完成' + : '导出中...' + : '导出中...' + : progressStream.length + ? isProgressFinished(progressStream[0]) + ? '导入完成' + : '导入中...' + : '导入中...' + }, [extra.type, progressStream.length]) + + const onCancel = useMemoizedFn(() => { + onFinished(false) + }) + + // modal header 描述文字 + const exportDescribeMemoizedFn = useMemoizedFn((type) => { + if (!hasDesc) return null + switch (type) { + case 'export': + return ( +

+ 远程模式下导出后请打开~Yakit\yakit-projects\projects路径查看导出文件,文件名无需填写后缀 +
+ ) + case 'import': + return ( +
+ 导入外部资源存在潜在风险,可能会被植入恶意代码或Payload,造成数据泄露、系统被入侵等严重后果。请务必谨慎考虑引入外部资源的必要性,并确保资源来源可信、内容安全。如果确实需要使用外部资源,建议优先选择官方发布的安全版本,或自行编写可控的数据源。同时,请保持系统和软件的最新版本,及时修复已知漏洞,做好日常安全防护。 +
+ ) + + default: + break + } + }) + + useEffect(() => { + if (extra.hint) { + handleReset() + form.resetFields() + } + // 关闭时重置所有数据 + return () => { + if (extra.hint) { + onCancelStream() + } + } + }, [extra.hint]) + + return ( + { + onCancelStream() + onCancel() + }} + footerStyle={{ justifyContent: 'flex-end' }} + footer={ + !showProgressStream ? ( + <> + {extra.type === 'export' && ( + + 取消 + + )} + {extra.type === 'import' ? '导入' : '确定'} + + ) : ( + { + onCancelStream() + onCancel() + }} + > + {progressStream.length ? (isProgressFinished(progressStream[0]) ? '完成' : '取消') : '取消'} + + ) + } + > + {!showProgressStream ? ( +
+
{ + e.preventDefault() + }} + > + {exportDescribeMemoizedFn(extra.type)} + {renderForm(form)} +
+
+ ) : ( +
+ +
+ )} +
+ ) +} +const ImportExportModal = memo((props: ImportExportModalProps) => ( + +)) as (props: ImportExportModalProps) => JSX.Element + +export default ImportExportModal diff --git a/src/compoments/JsonFormWrapper/JsonFormWrapper.module.scss b/src/compoments/JsonFormWrapper/JsonFormWrapper.module.scss new file mode 100644 index 00000000..5dd740e2 --- /dev/null +++ b/src/compoments/JsonFormWrapper/JsonFormWrapper.module.scss @@ -0,0 +1,70 @@ +.json-schema-box { + :global { + .has-error :not(.ant-input-disabled):not(.ant-input-borderless).ant-input:focus, + .has-error + :not(.ant-input-affix-wrapper-disabled):not( + .ant-input-affix-wrapper-borderless + ).ant-input-affix-wrapper:focus, + .has-error :not(.ant-input-disabled):not(.ant-input-borderless).ant-input-focused, + .has-error + :not(.ant-input-affix-wrapper-disabled):not( + .ant-input-affix-wrapper-borderless + ).ant-input-affix-wrapper-focused { + border-color: var(--Colors-Use-Error-Border); + // box-shadow: 0 0 0 2px var(--input-box-shadow-color); + } + .ant-form-item { + color: var(--Colors-Use-Neutral-Text-1-Title); + } + + .json-schema-row-form { + .ant-form-item { + display: flex; + flex-direction: row; + .ant-form-item-label { + flex: 8; + text-align: right; + label::after { + content: ":"; + position: relative; + top: -0.5px; + margin: 0 8px 0 2px; + z-index: 1; + display: inline-flex; + align-items: center; + } + } + .ant-form-item-control { + flex: 16; + overflow: hidden; + } + + .ant-form-item-label { + // 仅在相邻元素不是 .ant-form-item-control 时应用样式 + &:not(:has(+ .ant-form-item-control)) { + display: none; + width: 100%; + padding: 0; + margin-bottom: 20px; + text-align: left; + border: 0; + border-bottom: 1px solid var(--Colors-Use-Neutral-Border); + label { + font-size: 21px !important; + color: var(--Colors-Use-Neutral-Text-1-Title); + } + label::after { + content: ""; + position: relative; + top: -0.5px; + margin: 0 8px 0 2px; + z-index: 1; + display: inline-flex; + align-items: center; + } + } + } + } + } + } +} diff --git a/src/compoments/JsonFormWrapper/JsonFormWrapper.tsx b/src/compoments/JsonFormWrapper/JsonFormWrapper.tsx new file mode 100644 index 00000000..acb248f7 --- /dev/null +++ b/src/compoments/JsonFormWrapper/JsonFormWrapper.tsx @@ -0,0 +1,701 @@ +import React, { useEffect, useRef, useState } from 'react'; +import styles from './JsonFormWrapper.module.scss'; +import validator from '@rjsf/validator-ajv8'; // 添加这行 +import JsonForm from '@rjsf/antd'; +import type { RJSFSchema, UiSchema, WidgetProps } from '@rjsf/utils'; +import { YakitSelect } from '../yakitUI/YakitSelect/YakitSelect'; +import { YakitInput } from '../yakitUI/YakitInput/YakitInput'; +import { YakitDragger } from '../yakitUI/YakitForm/YakitForm'; +import { useGetState, useMemoizedFn, useUpdateEffect } from 'ahooks'; +import { YakitInputNumber } from '../yakitUI/YakitInputNumber/YakitInputNumber'; +import { Checkbox } from 'antd'; +import { YakitCheckbox } from '../yakitUI/YakitCheckbox/YakitCheckbox'; +import { YakitSwitch } from '../yakitUI/YakitSwitch/YakitSwitch'; +import { YakitRadioButtons } from '../yakitUI/YakitRadioButtons/YakitRadioButtons'; +import classNames from 'classnames'; +import ArrayFieldTemplate from './templates/ArrayFieldTemplate'; +import ObjectFieldTemplate from './templates/ObjectFieldTemplate'; +import { + type ColumnSchemaProps, + EditTable, + type UiSchemaTableProps, +} from './editTable/EditTable'; +import { cloneDeep } from 'lodash'; + +export const getJsonSchemaListResult = (obj: { [key: string]: any }) => { + // 此处的key用于筛选重复的表单数据 + let keyError: string[] = []; + let jsonSchemaError: JsonFormValidateProps[] = []; + let keySuccess: string[] = []; + let jsonSchemaSuccess: JsonFormValidateProps[] = []; + + Object.keys(obj).forEach((key) => { + const result: JsonFormValidateProps = obj[key](); + if (result.pass) { + if (!keySuccess.includes(result.key)) { + jsonSchemaSuccess.push(result); + keySuccess.push(result.key); + } + } else { + if (!keyError.includes(result.key)) { + jsonSchemaError.push(result); + keyError.push(result.key); + } + } + }); + + return { + jsonSchemaError, + jsonSchemaSuccess, + }; +}; + +export interface JsonFormValidateProps { + pass: boolean; + error?: any[]; + key: string; + value: string; +} + +export interface JsonFormSchemaListWrapper { + /** JsonSchema数据收集(PS:此处可能存在多个内部Form因此采用Ref数组的形式依次校验) */ + jsonSchemaListRef?: React.MutableRefObject<{ + [key: string]: any; + }>; + /** JsonSchema 默认值(PS:此值为对象 key值用于寻找对应默认值) */ + jsonSchemaInitial?: { + [key: string]: any; + }; +} + +export interface JsonFormWrapperProps { + jsonSchemaListRef: React.MutableRefObject<{ + [key: string]: any; + }>; + field: string; + value?: string; + onChange?: (v: string) => void; + schema: RJSFSchema; + uiSchema: UiSchema; + disabled?: boolean; +} + +/** 创建一个包装组件来处理 JsonForm */ +export const JsonFormWrapper: React.FC = React.memo( + (props) => { + const { jsonSchemaListRef, field, value, schema, uiSchema, disabled } = + props; + + const [formData, setFormData, getFormData] = useGetState( + value || {}, + ); + const jsonSchemaRef = useRef(); + // 用于强制刷新 + const [formKey, setFormKey] = useState(0); + + useEffect(() => { + if (jsonSchemaListRef.current) { + if (!jsonSchemaListRef.current.hasOwnProperty(field)) { + jsonSchemaListRef.current = { + ...jsonSchemaListRef.current, + [field]: validate, + }; + } + } + }, []); + + // 调用校验是否错误 + const validate = () => { + try { + const result = jsonSchemaRef.current?.validate(getFormData()); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return { + pass: (result?.errors || []).length === 0, + error: result?.errors, + key: field, + value: getFormData(), + } as JsonFormValidateProps; + } catch (error) { + // console.error("JsonForm validation error:", error) + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return { + pass: false, + key: field, + value: getFormData(), + } as JsonFormValidateProps; + } + }; + + useUpdateEffect(() => { + // 当外部 value 变化时更新内部状态 + value && setFormData(value); + }, [value]); + + // const UploadFolderPath = useMemoizedFn((props: FieldProps) => { + // const {formData, disabled, onChange} = props + // return ( + // <> + // ) + // }) + + // const fields: RegistryFieldsType = { + // "/test": UploadFolderPath, + // } + + const getTextWidget = useMemoizedFn((props: WidgetProps) => { + const { + id, + required, + readonly, + disabled, + value, + onChange, + onBlur, + onFocus, + autofocus, + options, + schema, + uiSchema, + } = props; + const uiStyle = uiSchema?.['ui:component_style'] || {}; + if (schema.type === 'number') { + return ( + { + onChange(value); + }} + onBlur={(value) => { + onBlur(id, value); + }} + onFocus={(value) => { + onFocus(id, value); + }} + /> + ); + } else if (schema.yakit_type === 'file') { + return getFileWidget(props); + } else if (schema.yakit_type === 'files') { + return getFilesWidget(props); + } else if (schema.yakit_type === 'folder') { + return getFolderWidget(props); + } + + return ( + { + onChange( + event.target.value === '' + ? options.emptyValue + : event.target.value, + ); + }} + onBlur={(event) => { + onBlur(id, event.target.value); + }} + onFocus={(event) => { + onFocus(id, event.target.value); + }} + /> + ); + }); + + const getTextareaWidget = useMemoizedFn((props: WidgetProps) => { + const { + id, + placeholder, + value, + required, + disabled, + autofocus, + readonly, + onBlur, + onFocus, + onChange, + options, + uiSchema, + } = props; + const uiStyle = uiSchema?.['ui:component_style'] || {}; + return ( + { + onChange( + event.target.value === '' + ? options.emptyValue + : event.target.value, + ); + }} + onBlur={(event) => { + onBlur(id, event.target.value); + }} + onFocus={(event) => { + onFocus(id, event.target.value); + }} + /> + ); + }); + + const getSelectWidget = useMemoizedFn((props: WidgetProps) => { + const { + id, + options, + multiple, + disabled, + readonly, + value, + autofocus, + onChange, + onBlur, + onFocus, + uiSchema, + } = props; + const { enumOptions = [], enumDisabled } = options; + const uiStyle = uiSchema?.['ui:component_style'] || {}; + let mode: any = multiple ? 'multiple' : 'default'; + mode = options.mode || mode; + return ( + onChange(value)} + onBlur={(value) => onBlur(id, value)} + onFocus={(value) => onFocus(id, value)} + style={uiStyle} + > + {enumOptions.map(({ value, label }: any, i: number) => { + const disabled: any = + enumDisabled && enumDisabled.indexOf(value) !== -1; + + return ( + + {label} + + ); + })} + + ); + }); + + // 应后端要求 此处替换控件为switch + const getSwitchWidget = useMemoizedFn((props: WidgetProps) => { + const { + label, + value, + disabled, + readonly, + autofocus, + onChange, + uiSchema, + }: WidgetProps = props; + const uiStyle = uiSchema?.['ui:component_style'] || {}; + return ( +
+
+ +
+
+ onChange(value)} + /> +
+
+ ); + }); + + const getCheckboxesWidget = useMemoizedFn((props: WidgetProps) => { + const { + id, + options, + value, + disabled, + readonly, + autofocus, + onChange, + uiSchema, + }: WidgetProps = props; + const uiStyle = uiSchema?.['ui:component_style'] || {}; + const { enumOptions, enumDisabled } = options; + return ( + onChange(value)} + style={uiStyle} + > + {(enumOptions as any[]).map( + (option: any, index: number) => { + const checked: boolean = + value.indexOf(option.value) !== -1; + const itemDisabled: any = + enumDisabled && + (enumDisabled as string[]).indexOf( + option.value, + ) !== -1; + return ( + + {option.label} + + ); + }, + )} + + ); + }); + + const getPasswordWidget = useMemoizedFn((props: WidgetProps) => { + const { + id, + required, + readonly, + disabled, + value, + onFocus, + onBlur, + onChange, + options, + autofocus, + uiSchema, + } = props; + const uiStyle = uiSchema?.['ui:component_style'] || {}; + return ( + { + onChange( + event.target.value === '' + ? options.emptyValue + : event.target.value, + ); + }} + onBlur={(event) => { + onBlur(id, event.target.value); + }} + onFocus={(event) => { + onFocus(id, event.target.value); + }} + /> + ); + }); + + const getRadioWidget = useMemoizedFn((props: WidgetProps) => { + const { options, value, disabled, onChange, uiSchema } = props; + const { enumOptions, enumDisabled } = options; + const uiStyle = uiSchema?.['ui:component_style'] || {}; + return ( + { + onChange(e.target.value); + }} + buttonStyle="solid" + options={(enumOptions as object[]).map((option: any) => { + const itemDisabled: any = + enumDisabled && + (enumDisabled as string[]).indexOf(option.value) !== + -1; + const info = { + value: option.value, + label: option.label, + disabled: itemDisabled, + }; + return info; + })} + /> + ); + }); + + const getUpDownWidget = useMemoizedFn((props: WidgetProps) => { + const { + id, + required, + readonly, + disabled, + value, + onChange, + onBlur, + onFocus, + autofocus, + options, + schema, + uiSchema, + } = props; + const uiStyle = uiSchema?.['ui:component_style'] || {}; + if (schema.multipleOf) { + options.step = schema.multipleOf; + } + + if (typeof schema.minimum !== 'undefined') { + options.min = schema.minimum; + } + + if (typeof schema.maximum !== 'undefined') { + options.max = schema.maximum; + } + return ( + onChange(value)} + onBlur={(value) => onBlur(id, value)} + onFocus={(value) => onFocus(id, value)} + style={uiStyle} + /> + ); + }); + + const getFileWidget = useMemoizedFn((props: WidgetProps) => { + const { disabled, value, onChange, options, uiSchema } = props; + const uiStyle = uiSchema?.['ui:component_style'] || {}; + return ( + { + onChange(value === '' ? options.emptyValue : value); + }} + /> + ); + }); + + const getFilesWidget = useMemoizedFn((props: WidgetProps) => { + const { disabled, value, onChange, options, uiSchema } = props; + const uiStyle = uiSchema?.['ui:component_style'] || {}; + return ( + { + onChange(value === '' ? options.emptyValue : value); + }} + /> + ); + }); + + const getFolderWidget = useMemoizedFn((props: WidgetProps) => { + const { disabled, value, onChange, options, uiSchema } = props; + const uiStyle = uiSchema?.['ui:component_style'] || {}; + return ( + { + onChange(value === '' ? options.emptyValue : value); + }} + /> + ); + }); + + const getTableWidget = useMemoizedFn((props: WidgetProps) => { + const { value, onChange, uiSchema } = props; + return ( + { + // 注:options.emptyValue在返回为数组时会导致校验无效 + onChange(arr); + }} + /> + ); + }); + + // 处理 allOf 条件更新 + const handleAllOfUpdate = (data: any) => { + const newData = { ...data }; + const allOfRules = schema?.allOf || []; + + allOfRules.forEach((rule: any) => { + const cond = rule.if?.properties; + const thenProps = rule.then?.properties; + if (!cond || !thenProps) return; + + // 判断 if 条件是否满足 + const isMatch = Object.entries(cond).every(([k, v]: any) => { + return newData[k] === v.const; + }); + + if (isMatch) { + Object.entries(thenProps).forEach(([key, val]: any) => { + if (val.const !== undefined) newData[key] = val.const; + }); + } + }); + + return newData; + }; + // const uiSchema: UiSchema = Object.keys(schema.properties || {}).reduce((acc, key) => { + // // 是否显示字段的 label + // acc[key] = { + // "ui:label": true + // } + // return acc + // }, {}) + // const uiSchema: UiSchema = { + // "ui:grid": [ + // { + // firstName: 7, + // lastName: 7, + // companyName: 7, + // b: 3 + // } + // ] + // } + + // { + // type: "object", + // properties: { + // firstName: { + // type: "string" + // }, + // lastName: { + // type: "string" + // }, + // companyName: { + // type: "string" + // }, + // b: { + // type: "boolean" + // } + // } + // } + + /* 字段名 */ + // unremovable: { + /* 全局 className */ + // "ui:classNames": "json-schema-row-form" + // } + return ( + { + const newFormData = cloneDeep(e.formData); + // 手动触发 allOf 条件逻辑 + const updated = handleAllOfUpdate(newFormData); + // 判断 allOf 更新后是否真的改变了数据 + const isSame = + JSON.stringify(updated) === JSON.stringify(e.formData); + // 更新表单值 + setFormData(updated); + // 强制刷新 + if (!isSame) { + setFormKey((k) => k + 1); + } + }} + /** + * 如果omitExtraData和liveOmit都被设置为true,那么当onChange被调用时, + * 不在任何表单字段中的额外表单数据值将被删除。默认为false。 + * */ + omitExtraData={true} + liveOmit={true} + // 如果设置为true,表单将在更改表单数据时执行验证并显示任何验证错误,而不仅仅是在提交时。 + liveValidate={true} + /** + * 当此属性设置为top或bottom时,错误列表(或ErrorList中定义的自定义错误列表)也将显示在表单的底部或顶部。 + * 当设置为false时,只显示内联输入验证错误。默认设置为top。 + * */ + showErrorList={false} + > + {/* 这行代码会移除默认的提交按钮 */} + {/* eslint-disable-next-line react/jsx-no-useless-fragment */} + <> + + ); + }, +); diff --git a/src/compoments/JsonFormWrapper/editTable/EditTable.module.scss b/src/compoments/JsonFormWrapper/editTable/EditTable.module.scss new file mode 100644 index 00000000..e9d4f017 --- /dev/null +++ b/src/compoments/JsonFormWrapper/editTable/EditTable.module.scss @@ -0,0 +1,115 @@ +.operation { + display: flex; + gap: 8px; + align-items: center; +} +.check-circle-icon { + color: var(--Colors-Use-Success-Primary); +} +.input-textarea { + :global { + .ant-input-suffix { + &:hover { + color: var(--Colors-Use-Main-Primary); + } + } + } +} + +.edit-table { + :global { + .ant-table-placeholder { + display: none; + } + } +} + +.edit-table-box { + :global { + // table相关 + .ant-table, + tbody tr ,.ant-table-cell-fix-right { + background-color: var(--Colors-Use-Basic-Background); + .yakit-content-single-ellipsis { + color: var(--Colors-Use-Neutral-Text-1-Title); + } + } + .ant-table-thead > tr > th, + .ant-table-tbody > tr > td { + padding: 8px 16px; + } + .ant-table-thead > tr > th { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 16px; + background: var(--Colors-Use-Neutral-Bg); + } + .ant-table.ant-table-bordered > .ant-table-container, + .ant-table.ant-table-bordered > .ant-table-container > .ant-table-content > table, + .ant-table.ant-table-bordered + > .ant-table-container + > .ant-table-content + > table + .ant-table-cell-fix-right-first::after { + border-color: var(--Colors-Use-Neutral-Border); + } + .ant-table-thead > tr > th, + .ant-table-tbody > tr > td { + border-bottom: 1px solid var(--Colors-Use-Neutral-Border); + } + .ant-table.ant-table-bordered > .ant-table-container > .ant-table-content > table > thead > tr > th, + .ant-table.ant-table-bordered > .ant-table-container > .ant-table-content > table > tbody > tr > td { + border-right: 1px solid var(--Colors-Use-Neutral-Border); + } + .ant-table-tbody > tr.ant-table-row { + vertical-align: top; + } + .ant-table-tbody > tr.ant-table-row:hover > td, + .ant-table-tbody > tr > td.ant-table-cell-row-hover { + background: var(--Colors-Use-Neutral-Bg-Hover); + } + .ant-table-filter-trigger:hover { + color: transparent; + background: transparent; + } + // 分页相关 + .ant-pagination-item-active { + border-color: #f28b44; + } + .ant-pagination-item-active a { + color: #f28b44; + } + .ant-pagination-item:hover a { + color: #f28b44; + } + .ant-pagination-prev:hover .ant-pagination-item-link, + .ant-pagination-next:hover .ant-pagination-item-link { + color: #f28b44; + border-color: #f28b44; + } + .ant-pagination-jump-prev .ant-pagination-item-container .ant-pagination-item-link-icon, + .ant-pagination-jump-next .ant-pagination-item-container .ant-pagination-item-link-icon { + color: #f28b44; + } + .ant-select:not(.ant-select-disabled):hover .ant-select-selector { + border-color: #f28b44; + } + .ant-select-focused:not(.ant-select-disabled).ant-select:not(.ant-select-customize-input) .ant-select-selector { + border-color: #f28b44; + box-shadow: 0 0 0 2px rgb(242 139 68 / 20%); + } + .ant-pagination-options-quick-jumper input:hover { + border-color: #f28b44; + } + .ant-pagination-options-quick-jumper input:focus, + .ant-pagination-options-quick-jumper input-focused { + border-color: #f28b44; + box-shadow: 0 0 0 2px rgb(242 139 68 / 20%); + } + .ant-select-item-option-selected:not(.ant-select-item-option-disabled) { + background-color: rgba(242, 139, 68, 0.1); + } + } +} diff --git a/src/compoments/JsonFormWrapper/editTable/EditTable.tsx b/src/compoments/JsonFormWrapper/editTable/EditTable.tsx new file mode 100644 index 00000000..b1054688 --- /dev/null +++ b/src/compoments/JsonFormWrapper/editTable/EditTable.tsx @@ -0,0 +1,669 @@ +/* eslint-disable no-async-promise-executor */ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { useMemoizedFn } from 'ahooks'; +import styles from './EditTable.module.scss'; +import { failed, warn } from '@/utils/notification'; +import classNames from 'classnames'; +import { Form, type FormInstance, Table, Tooltip } from 'antd'; +import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton'; +import { + EllipsisOutlined, + InfoCircleOutlined, + PlusOutlined, +} from '@ant-design/icons'; +import { YakitInput } from '@/compoments/yakitUI/YakitInput/YakitInput'; +import { v4 as uuidv4 } from 'uuid'; +import { YakitSelect } from '@/compoments/yakitUI/YakitSelect/YakitSelect'; +import { YakitSwitch } from '@/compoments/yakitUI/YakitSwitch/YakitSwitch'; +import type { DefaultOptionType } from 'antd/lib/select'; +import { YakitDropdownMenu } from '@/compoments/yakitUI/YakitDropdownMenu/YakitDropdownMenu'; +import { showByRightContext } from '@/compoments/yakitUI/YakitMenu/showByRightContext'; + +interface Item { + _id: string; + [key: string]: string; +} + +type CellTypeProps = 'string' | 'boolean'; + +interface EditableCellProps extends React.HTMLAttributes { + editing: boolean; + required: boolean; + type?: CellTypeProps; + selectOption: DefaultOptionType[]; + UiStyle?: UiKeyProps; + dataIndex: string; + title: any; + record: Item; + index: number; + children: React.ReactNode; + form: FormInstance; + setFoucusFun: (v: boolean) => void; +} + +const EditableCell: React.FC = ({ + editing, + required, + type, + selectOption, + UiStyle, + dataIndex, + title, + children, + setFoucusFun, + ...restProps +}) => { + const onFocus = useMemoizedFn(() => { + setFoucusFun(true); + }); + + const getNode = useMemoizedFn(() => { + if (UiStyle && UiStyle?.['ui:widget']) { + switch (UiStyle['ui:widget']) { + case 'textarea': + return ( + + ); + default: + return ; + } + } else { + switch (type) { + case 'string': + return selectOption.length > 0 ? ( + + ) : ( + + ); + case 'boolean': + return ; + default: + return ; + } + } + }); + + return ( + + {editing ? ( + + {getNode()} + + ) : ( + children + )} + + ); +}; + +interface PropertiesProps { + [key: string]: { + type: CellTypeProps; + title: string; + default?: string; + enum?: string[]; + description?: string; + }; +} + +interface itemsProps { + properties: PropertiesProps; + require: string[]; +} + +interface UiKeyProps { + // 目前支持的替换控件 + 'ui:widget': 'textarea'; + // "ui:grid": string | number +} + +interface UiKeysProps { + [key: string]: UiKeyProps | DefaultObjProps[]; +} + +interface DefaultObjProps { + [key: string]: any; +} + +export interface ColumnSchemaProps { + minItems?: number; + maxItems?: number; + items: itemsProps; +} + +export interface UiSchemaTableProps { + items?: any; + x?: number; + y?: number; +} + +export interface EditTableProps { + columnSchema: ColumnSchemaProps; + uiSchema: UiSchemaTableProps; + onChange?: (v: any) => void; + // 默认值 + value?: any; +} +export const EditTable: React.FC = (props) => { + const { columnSchema, uiSchema, onChange, value } = props; + const [form] = Form.useForm(); + const [data, setData] = useState([]); + const [cacheData, setCacheData] = useState([]); + const [editingId, setEditingId] = useState(''); + const [columns, setColumns] = useState([]); + const [requireList, setRequireList] = useState([]); + const [maxItems, setMaxItems] = useState(); + const [uiKeys, setUiKeys] = useState(); + const [defaultObj, setDefaultObj] = useState({}); + + // 默认数据注入 + useEffect(() => { + if (Array.isArray(value) && value.length > 0) { + const newData = value.map((item) => ({ ...item, _id: uuidv4() })); + setData(newData); + } + }, []); + + // 初始化表格数据 + useEffect(() => { + try { + // 在 EditTable 保存时,检测并“剥掉多余的嵌套层”: + if (columnSchema?.items?.properties?.properties) { + columnSchema.items.properties = columnSchema.items.properties + .properties as unknown as PropertiesProps; + } + // columnSchema + const { maxItems = 50, items } = columnSchema; + const { require = [], properties } = items; + const newColumns: any[] = []; + const defObj: DefaultObjProps = {}; + // uiSchema + const newUiKeys = uiSchema?.items as UiKeysProps; + const newGrid = (newUiKeys?.['ui:grid'] || []) as DefaultObjProps[]; + let widthObj: Record = {}; + newGrid.forEach((item) => { + widthObj = { + ...widthObj, + ...(item as Record), + }; + }); + + Object.keys(properties).forEach((key) => { + const { type, title, description } = properties[key]; + newColumns.push({ + title: description ? ( +
+ + {title || key} + + + + +
+ ) : ( +
{title || key}
+ ), + dataIndex: key, + editable: true, + type, + enum: properties[key]?.enum, + render: (text: any) => { + if (type === 'boolean') { + return ( + + ); + } + const keyUiSchema = newUiKeys?.[key]; + if ( + keyUiSchema && + !Array.isArray(keyUiSchema) && + keyUiSchema['ui:widget'] === 'textarea' + ) { + return ( + +
+ {text} +
+
+ ); + } + return ( +
+ {text || '-'} +
+ ); + }, + width: widthObj?.[key], + }); + + // 默认值赋值 + if (properties[key]?.default) { + defObj[key] = properties[key]?.default; + } + }); + setDefaultObj(defObj); + setUiKeys(newUiKeys); + setRequireList(require); + setColumns([...newColumns]); + setMaxItems(maxItems); + + // 如果表格为空,初始化一条新数据 由于需要校验数据不可直接保存 + if ( + data.length === 0 && + !(Array.isArray(value) && value.length > 0) + ) { + const newItem = { + _id: uuidv4(), + ...defObj, + }; + setCacheData([newItem]); + setEditingId(newItem._id); + form.setFieldsValue({ ...newItem }); + } + } catch (error) { + failed(`解析表格失败:${error}`); + } + }, [columnSchema, uiSchema]); + + const isEditing = useMemoizedFn( + (rowItem: Item) => rowItem._id === editingId, + ); + + const onEdit = useMemoizedFn( + (record: Partial & { _id: React.Key }) => { + form.setFieldsValue({ ...record }); + setEditingId(record._id); + }, + ); + + const onCancel = useMemoizedFn(() => { + setEditingId(''); + setCacheData([]); + }); + + const onDelete = useMemoizedFn((record: Item) => { + if (cacheData.length > 0 && record._id === cacheData[0]._id) { + if (record._id === editingId) { + setEditingId(''); + } + setCacheData([]); + return; + } + const newData = [...data].filter((item) => item._id !== record._id); + setData(newData); + onSubmit(newData); + if (record._id === editingId) { + setEditingId(''); + } + }); + + // 数据提交 + const onSubmit = useMemoizedFn((arr: Item[]) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const data = arr.map(({ _id, ...obj }) => obj); + onChange && onChange(data); + }); + + const onSave = useMemoizedFn((record: Item) => { + return new Promise(async (resolve, reject) => { + try { + const row = (await form.validateFields()) as Item; + const newData = [...data]; + const index = newData.findIndex( + (item) => record._id === item._id, + ); + // 修改 + if (index > -1) { + const item = newData[index]; + const newItem = { + ...item, + ...row, + }; + newData.splice(index, 1, newItem); + setData(newData); + setEditingId(''); + onSubmit(newData); + resolve(newItem); + } + // 新增 + else if ( + cacheData.length > 0 && + record._id === cacheData[0]._id + ) { + newData.push({ ...cacheData[0], ...row }); + setCacheData([]); + setData(newData); + setEditingId(''); + onSubmit(newData); + resolve(cacheData[0]); + } + } catch (errInfo) { + // eslint-disable-next-line prefer-promise-reject-errors + reject(); + } + }); + }); + + // 复制 + const onCopy = useMemoizedFn(async (record: Item) => { + // 校验复制行 如若为编辑状态且无法通过校验则无法复制 + if (record._id === editingId) { + try { + const item = await onSave(record); + const newRecord = { + ...item, + _id: uuidv4(), + }; + setData([...data, newRecord]); + } catch (error) { + warn('当前行校验未通过'); + } + } else { + const newRecord = { + ...record, + _id: uuidv4(), + }; + setData([...data, newRecord]); + } + }); + + // 失焦自动保存(如若此时点击可选组件时,不应调用自动保存) + const onBlur = useMemoizedFn((record: Item) => { + isFoucus.current = false; + setTimeout(async () => { + if (editingId && !isFoucus.current) { + try { + await onSave(record); + setEditingId(''); + } catch (error) { + // 保存失败时不退出编辑状态 + } + } + isFoucus.current = false; + }, 200); + }); + + const addCell = useMemoizedFn(async () => { + if (typeof maxItems === 'number' && data.length >= maxItems) { + warn(`已达最大数量${maxItems}`); + return; + } + if (cacheData.length !== 0) { + await onSave(cacheData[0]); + } + form.resetFields(); + if (Object.keys(defaultObj).length !== 0) { + form.setFieldsValue({ ...defaultObj }); + } + const _id = uuidv4(); + setCacheData([{ _id }]); + setEditingId(_id); + }); + + const realData = useMemo(() => { + return [...data, ...cacheData]; + }, [data, cacheData]); + + const realColumns = useMemo(() => { + return [ + ...columns, + { + title:
操作
, + dataIndex: 'operation', + width: 45, + fixed: 'right', + render: (_: any, record: Item) => { + const editTable = isEditing(record); + const editData = [ + { + key: 'save', + label: '保存', + }, + { + key: 'cancel', + label: '取消', + }, + ]; + const showData = [ + { + key: 'edit', + label: '编辑', + }, + { + key: 'copy', + label: '复制', + }, + ]; + const delData = [ + { + key: 'delete', + label: '删除', + }, + ]; + return ( + { + switch (key) { + case 'save': + onSave(record); + break; + case 'cancel': + onCancel(); + break; + case 'edit': + onEdit(record); + break; + case 'copy': + onCopy(record); + break; + case 'delete': + onDelete(record); + break; + default: + break; + } + }, + width: 80, + }} + dropdown={{ + trigger: ['click'], + placement: 'bottom', + }} + > + } + /> + + ); + }, + }, + ]; + }, [columns, editingId]); + + const isFoucus = useRef(false); + const setFoucusFun = useMemoizedFn((is: boolean) => { + isFoucus.current = is; + }); + const mergedColumns = realColumns.map((col) => { + if (!col.editable) { + return col; + } + return { + ...col, + onCell: (record: Item) => ({ + record, + dataIndex: col.dataIndex, + title: col.title, + editing: isEditing(record), + required: requireList.includes(col.dataIndex), + type: col.type, + selectOption: (col.enum || []).map((item: any) => ({ + label: item, + value: item, + })), + UiStyle: uiKeys?.[col.dataIndex], + form, + setFoucusFun: setFoucusFun, + onBlur: () => onBlur(record), + }), + }; + }); + + return ( +
+
+ e._id} + size="small" + className={classNames({ + [styles['edit-table']]: realData.length === 0, + })} + components={{ + body: { + cell: EditableCell, + }, + }} + dataSource={realData} + columns={mergedColumns} + pagination={false} + scroll={{ x: uiSchema?.x || 'max-content', y: uiSchema?.y }} + bordered={true} + onRow={(record) => ({ + onDoubleClick: () => { + // 如果当前没有编辑中的行,直接编辑 + if (editingId === '') { + onEdit(record); + return; + } + + // 如果点击的是当前编辑行,不做处理 + if (editingId === record._id) { + return; + } + + // 如果有其他行在编辑,先保存再编辑新行 + const editingRecord = data.find( + (item) => item._id === editingId, + ); + if (editingRecord) { + onSave(editingRecord); + onEdit(record); + } + }, + onContextMenu: (e) => { + e.preventDefault(); + showByRightContext({ + data: [ + { label: '编辑', key: 'edit' }, + { label: '复制', key: 'copy' }, + { label: '保存', key: 'save' }, + { label: '删除', key: 'delete' }, + ], + width: 80, + onClick: async (e) => { + switch (e.key) { + case 'edit': + // 如果当前没有编辑中的行,直接编辑 + if (editingId === '') { + onEdit(record); + return; + } + + // 如果点击的是当前编辑行,不做处理 + if (editingId === record._id) { + return; + } + + // 如果有其他行在编辑,先保存再编辑新行 + // eslint-disable-next-line no-case-declarations + const editingRecord = data.find( + (item) => + item._id === editingId, + ); + if (editingRecord) { + onSave(editingRecord); + onEdit(record); + } + return; + case 'copy': + onCopy(record); + return; + case 'save': + try { + await onSave(record); + } catch (error) { + warn('当前行校验未通过'); + } + + return; + case 'delete': + // eslint-disable-next-line no-case-declarations + const newData = data.filter( + (item) => + item._id !== record._id, + ); + setData(newData); + return; + } + }, + }); + }, + })} + /> + + + } + type="outline1" + > + 添加一行数据 + + + ); +}; diff --git a/src/compoments/JsonFormWrapper/templates/ArrayFieldTemplate.tsx b/src/compoments/JsonFormWrapper/templates/ArrayFieldTemplate.tsx new file mode 100644 index 00000000..de973845 --- /dev/null +++ b/src/compoments/JsonFormWrapper/templates/ArrayFieldTemplate.tsx @@ -0,0 +1,177 @@ +import React from 'react'; +import { + YakitButton, + type YakitButtonProp, +} from '../../yakitUI/YakitButton/YakitButton'; +import { PlusIcon, TrashIcon } from '@/assets/newIcon'; +// import {SolidArrownarrowdownIcon, SolidArrownarrowupIcon} from "@/assets/icon/solid" + +const AddButton: React.FC = (props) => ( + }> + 添加 + +); + +const ArrayFieldTemplate: React.FC = (props: any) => { + return ; +}; + +interface ArrayFieldTitleProps { + TitleField: any; + idSchema: any; + title: string; + required: boolean; +} + +const ArrayFieldTitle: React.FC = ( + props: ArrayFieldTitleProps, +) => { + const { idSchema, title }: ArrayFieldTitleProps = props; + + if (!title) { + return
; + } + + const id = `${idSchema.$id}__title`; + return
{title}
; +}; + +interface ArrayFieldDescriptionProps { + DescriptionField: any; + idSchema: any; + description: string; +} + +const ArrayFieldDescription: React.FC = ( + props: ArrayFieldDescriptionProps, +) => { + const { + DescriptionField, + idSchema, + description, + }: ArrayFieldDescriptionProps = props; + + if (!description) { + return
; + } + + const id = `${idSchema.$id}__description`; + + return ; +}; + +// Used in the two templates +const DefaultArrayItem: React.FC = (props) => { + const btnStyle: object = { + flex: 1, + paddingLeft: 6, + paddingRight: 6, + fontWeight: 'bold', + }; + const { + index, + children, + disabled, + readonly, + onDropIndexClick, + hasToolbar, + hasRemove, + } = props; + return ( +
+
{children}
+ {hasToolbar && ( +
+ {/* {(hasMoveUp || hasMoveDown) && ( + } + /> + )} + + {(hasMoveUp || hasMoveDown) && ( + } + /> + )} */} + + {hasRemove && ( + } + /> + )} +
+ )} +
+ ); +}; + +const DefaultNormalArrayFieldTemplate: React.FC = (props) => { + const { + TitleField, + idSchema, + title, + required, + uiSchema, + schema, + items, + canAdd, + onAddClick, + disabled, + readonly, + DescriptionField, + } = props; + + return ( +
+ + + {(uiSchema['ui:description'] || schema.description) && ( + + )} + +
+ {items && items.map((p: any) => DefaultArrayItem(p))} + {canAdd && ( +
+ +
+ )} +
+
+ ); +}; + +export default ArrayFieldTemplate; diff --git a/src/compoments/JsonFormWrapper/templates/ObjectFieldTemplate.tsx b/src/compoments/JsonFormWrapper/templates/ObjectFieldTemplate.tsx new file mode 100644 index 00000000..a8487353 --- /dev/null +++ b/src/compoments/JsonFormWrapper/templates/ObjectFieldTemplate.tsx @@ -0,0 +1,243 @@ +import classNames from 'classnames'; +import isObject from 'lodash/isObject'; +import isNumber from 'lodash/isNumber'; +import isString from 'lodash/isString'; +import { + type FormContextType, + type GenericObjectType, + type ObjectFieldTemplateProps, + type ObjectFieldTemplatePropertyType, + type RJSFSchema, + type StrictRJSFSchema, + type UiSchema, + canExpand, + descriptionId, + getTemplate, + getUiOptions, + titleId, +} from '@rjsf/utils'; +import Col from 'antd/lib/col'; +import Row from 'antd/lib/row'; +import { + ConfigConsumer, + type ConfigConsumerProps, +} from 'antd/lib/config-provider/context'; + +const DESCRIPTION_COL_STYLE = { + paddingBottom: '8px', +}; + +/** The `ObjectFieldTemplate` is the template to use to render all the inner properties of an object along with the + * title and description if available. If the object is expandable, then an `AddButton` is also rendered after all + * the properties. + * + * @param props - The `ObjectFieldTemplateProps` for this component + */ +export default function ObjectFieldTemplate< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: ObjectFieldTemplateProps) { + const { + description, + disabled, + formContext, + formData, + idSchema, + onAddClick, + properties, + readonly, + required, + registry, + schema, + title, + uiSchema, + } = props; + // console.log("ObjectFieldTemplate---", props) + + const uiOptions = getUiOptions(uiSchema); + const TitleFieldTemplate = getTemplate<'TitleFieldTemplate', T, S, F>( + 'TitleFieldTemplate', + registry, + uiOptions, + ); + const DescriptionFieldTemplate = getTemplate< + 'DescriptionFieldTemplate', + T, + S, + F + >('DescriptionFieldTemplate', registry, uiOptions); + // Button templates are not overridden in the uiSchema + const { + ButtonTemplates: { AddButton }, + } = registry.templates; + const { + colSpan = 24, + labelAlign = 'right', + rowGutter = 24, + } = formContext as GenericObjectType; + + const findSchema = (element: ObjectFieldTemplatePropertyType): S => + element.content.props.schema; + + const findSchemaType = (element: ObjectFieldTemplatePropertyType) => + findSchema(element).type; + + const findUiSchema = ( + element: ObjectFieldTemplatePropertyType, + ): UiSchema | undefined => element.content.props.uiSchema; + + const findUiSchemaField = (element: ObjectFieldTemplatePropertyType) => + getUiOptions(findUiSchema(element)).field; + + const findUiSchemaWidget = (element: ObjectFieldTemplatePropertyType) => + getUiOptions(findUiSchema(element)).widget; + + const calculateColSpan = (element: ObjectFieldTemplatePropertyType) => { + const type = findSchemaType(element); + const field = findUiSchemaField(element); + const widget = findUiSchemaWidget(element); + + const defaultColSpan = + properties.length < 2 || // Single or no field in object. + type === 'object' || + type === 'array' || + widget === 'textarea' + ? 24 + : 12; + + if (isObject(colSpan)) { + const colSpanObj: GenericObjectType = colSpan; + if (isString(widget)) { + return colSpanObj[widget]; + } + if (isString(field)) { + return colSpanObj[field]; + } + if (isString(type)) { + return colSpanObj[type]; + } + } + if (isNumber(colSpan)) { + return colSpan; + } + return defaultColSpan; + }; + + return ( + + {(configProps: ConfigConsumerProps) => { + const { getPrefixCls } = configProps; + const prefixCls = getPrefixCls('form'); + const labelClsBasic = `${prefixCls}-item-label`; + const labelColClassName = classNames( + labelClsBasic, + labelAlign === 'left' && `${labelClsBasic}-left`, + // labelCol.className, + ); + + return ( +
+ + {title && ( +
+ (idSchema)} + title={title} + required={required} + schema={schema} + uiSchema={uiSchema} + registry={registry} + /> + + )} + {description && ( + + (idSchema)} + description={description} + schema={schema} + uiSchema={uiSchema} + registry={registry} + /> + + )} + {uiSchema?.['ui:grid'] && + Array.isArray(uiSchema['ui:grid']) + ? uiSchema['ui:grid'].map((ui_row: any) => { + return ( + + + {Object.keys(ui_row).map( + (row_item) => { + let element = + properties.find( + (p: any) => + p.name === + row_item, + ); + if (element) { + return ( + + { + element.content + } + + ); + } else { + return null; + } + }, + )} + + + ); + }) + : properties + .filter((e: any) => !e.hidden) + .map( + ( + element: ObjectFieldTemplatePropertyType, + ) => ( + + {element.content} + + ), + )} + + + {canExpand(schema, uiSchema, formData) && ( + + + + + + + + )} + + ); + }} + + ); +} diff --git a/src/compoments/Loading/Loading.module.scss b/src/compoments/Loading/Loading.module.scss new file mode 100644 index 00000000..2be559f5 --- /dev/null +++ b/src/compoments/Loading/Loading.module.scss @@ -0,0 +1,36 @@ +.codeLoader { + width: 100%; + font-weight: bold; + opacity: 0.8; + line-height: 1; + display: inline-flex; + align-items: center; + display: flex; + align-items: center; + gap: 4px; + color: var(--Colors-Use-Neutral-Text-3-Secondary); + span { + display: inline-block; + animation: pulse 0.4s alternate infinite ease-in-out; + } + .text{ + flex: 1; + width: 0; + } + span:nth-child(odd) { + animation-delay: 0.4s; + } +} + +.paused { + span { + animation-play-state: paused; + } +} + +@keyframes pulse { + to { + transform: scale(0.8); + opacity: 0.5; + } +} diff --git a/src/compoments/Loading/Loading.tsx b/src/compoments/Loading/Loading.tsx new file mode 100644 index 00000000..50b63ee3 --- /dev/null +++ b/src/compoments/Loading/Loading.tsx @@ -0,0 +1,46 @@ +import type { FC, CSSProperties, ReactNode } from 'react'; +import classNames from 'classnames'; +import styles from './Loading.module.scss'; + +export interface LoaderProps { + size?: number; + color?: string; + loading?: boolean; + className?: string; + style?: CSSProperties; + children?: ReactNode; +} + +const Loader: FC = ({ + size = 32, + color, + loading = true, + className, + children, + style, +}) => { + return ( +
+
+ {'{'} + {'}'} +
+
{children}
+
+ ); +}; + +export default Loader; diff --git a/src/compoments/MarkDown/markdownRender.module.scss b/src/compoments/MarkDown/markdownRender.module.scss new file mode 100644 index 00000000..0861a3b1 --- /dev/null +++ b/src/compoments/MarkDown/markdownRender.module.scss @@ -0,0 +1,45 @@ +.safe-markdown { + color: var(--Colors-Use-Neutral-Text-1-Title); + background-color: transparent; + font-size: 14px; + :global { + pre { + position: relative; + overflow: hidden; + &:hover .copy-code-button { + pointer-events: all; + transform: translateX(0px); + opacity: 0.5; + } + + .copy-code-button { + position: absolute; + right: 4px; + top: 4px; + cursor: pointer; + height: 16px; + margin: 0px; + transform: translateX(16px); + pointer-events: none; + opacity: 0; + transition: all ease 0.3s; + svg { + width: 16px; + height: 16px; + } + + &:hover { + opacity: 1; + } + } + } + code { + background: var(--Colors-Use-Neutral-Bg); + color: var(--Colors-Use-Neutral-Text-1-Title); + border-radius: 4px; + } + pre > code { + font-size: 14px; + } + } +} \ No newline at end of file diff --git a/src/compoments/MarkDown/markdownRender.scss b/src/compoments/MarkDown/markdownRender.scss new file mode 100644 index 00000000..165cada5 --- /dev/null +++ b/src/compoments/MarkDown/markdownRender.scss @@ -0,0 +1,1663 @@ +.stream-markdown { + /* src/styles/index.css */ + + /* Import Tailwind's base styles */ + + *, + ::before, + ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; + } + + ::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; + } + + /* ! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com */ + + /* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + + *, + ::before, + ::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ + } + + ::before, + ::after { + --tw-content: ''; + } + + /* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + + html, + :host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + /* 3 */ + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ + } + + /* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + + body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ + } + + /* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + + hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ + } + + /* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + + /* +Remove the default font size and weight for headings. +*/ + + h1, + h2, + h3, + h4, + h5, + h6 { + font-size: inherit; + font-weight: inherit; + } + + /* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + + a { + color: inherit; + text-decoration: inherit; + } + + /* +Add the correct font weight in Edge and Safari. +*/ + + b, + strong { + font-weight: bolder; + } + + /* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + + code, + kbd, + samp, + pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ + } + + /* +Add the correct font size in all browsers. +*/ + + small { + font-size: 80%; + } + + /* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + + sub { + bottom: -0.25em; + } + + sup { + top: -0.5em; + } + + /* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + + table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ + } + + /* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + + button, + input, + optgroup, + select, + textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + letter-spacing: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ + } + + /* +Remove the inheritance of text transform in Edge and Firefox. +*/ + + button, + select { + text-transform: none; + } + + /* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + + button, + input:where([type='button']), + input:where([type='reset']), + input:where([type='submit']) { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ + } + + /* +Use the modern Firefox focus style for all focusable elements. +*/ + + :-moz-focusring { + outline: auto; + } + + /* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + + :-moz-ui-invalid { + box-shadow: none; + } + + /* +Add the correct vertical alignment in Chrome and Firefox. +*/ + + progress { + vertical-align: baseline; + } + + /* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + + ::-webkit-inner-spin-button, + ::-webkit-outer-spin-button { + height: auto; + } + + /* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + + [type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ + } + + /* +Remove the inner padding in Chrome and Safari on macOS. +*/ + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + /* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + + ::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ + } + + /* +Add the correct display in Chrome and Safari. +*/ + + summary { + display: list-item; + } + + /* +Removes the default spacing and border for appropriate elements. +*/ + + blockquote, + dl, + dd, + h1, + h2, + h3, + h4, + h5, + h6, + hr, + figure, + p, + pre { + margin: 0; + } + + fieldset { + margin: 0; + padding: 0; + } + + legend { + padding: 0; + } + + ol, + ul, + menu { + list-style: none; + margin: 0; + padding: 0; + } + + /* +Reset default styling for dialogs. +*/ + + dialog { + padding: 0; + } + + /* +Prevent resizing textareas horizontally by default. +*/ + + textarea { + resize: vertical; + } + + /* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + + input::placeholder, + textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ + } + + /* +Set the default cursor for buttons. +*/ + + button, + [role='button'] { + cursor: pointer; + } + + /* +Make sure disabled buttons don't get the pointer cursor. +*/ + + :disabled { + cursor: default; + } + + /* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + + img, + svg, + video, + canvas, + audio, + iframe, + embed, + object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ + } + + /* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + + img, + video { + max-width: 100%; + height: auto; + } + + /* Make elements with the HTML hidden attribute stay hidden by default */ + + [hidden]:where(:not([hidden='until-found'])) { + display: none; + } + + /* Import Tailwind's component styles */ + + .\!container { + width: 100% !important; + } + + .container { + width: 100%; + } + + @media (min-width: 640px) { + .\!container { + max-width: 640px !important; + } + + .container { + max-width: 640px; + } + } + + @media (min-width: 768px) { + .\!container { + max-width: 768px !important; + } + + .container { + max-width: 768px; + } + } + + @media (min-width: 1024px) { + .\!container { + max-width: 1024px !important; + } + + .container { + max-width: 1024px; + } + } + + @media (min-width: 1280px) { + .\!container { + max-width: 1280px !important; + } + + .container { + max-width: 1280px; + } + } + + @media (min-width: 1536px) { + .\!container { + max-width: 1536px !important; + } + + .container { + max-width: 1536px; + } + } + + /* Import Tailwind's utility styles */ + + .pointer-events-none { + pointer-events: none; + } + + .\!visible { + visibility: visible !important; + } + + .visible { + visibility: visible; + } + + .collapse { + visibility: collapse; + } + + .static { + position: static; + } + + .fixed { + position: fixed; + } + + .absolute { + position: absolute; + } + + .relative { + position: relative; + } + + .inset-0 { + inset: 0px; + } + + .bottom-2 { + bottom: 0.5rem; + } + + .bottom-4 { + bottom: 1rem; + } + + .left-2 { + left: 0.5rem; + } + + .left-4 { + left: 1rem; + } + + .right-0 { + right: 0px; + } + + .right-2 { + right: 0.5rem; + } + + .right-4 { + right: 1rem; + } + + .top-4 { + top: 1rem; + } + + .top-full { + top: 100%; + } + + .z-10 { + z-index: 10; + } + + .z-50 { + z-index: 50; + } + + .mx-4 { + margin-left: 1rem; + margin-right: 1rem; + } + + .my-4 { + margin-top: 1rem; + margin-bottom: 1rem; + } + + .my-6 { + margin-top: 1.5rem; + margin-bottom: 1.5rem; + } + + .mb-2 { + margin-bottom: 0.5rem; + } + + .ml-1 { + margin-left: 0.25rem; + } + + .mt-1 { + margin-top: 0.25rem; + } + + .mt-2 { + margin-top: 0.5rem; + } + + .mt-6 { + margin-top: 1.5rem; + } + + .box-content { + box-sizing: content-box; + } + + .block { + display: block; + } + + .inline-block { + display: inline-block; + } + + .inline { + display: inline; + } + + .flex { + display: flex; + } + + .inline-flex { + display: inline-flex; + } + + .table { + display: table; + } + + .table-cell { + display: table-cell; + } + + .table-row { + display: table-row; + } + + .grid { + display: grid; + } + + .contents { + display: contents; + } + + .list-item { + display: list-item; + } + + .\!hidden { + display: none !important; + } + + .hidden { + display: none; + } + + .size-4 { + width: 1rem; + height: 1rem; + } + + .size-full { + width: 100%; + height: 100%; + } + + .h-4 { + height: 1rem; + } + + .h-8 { + height: 2rem; + } + + .h-\[46px\] { + height: 46px; + } + + .h-auto { + height: auto; + } + + .h-full { + height: 100%; + } + + .h-screen { + height: 100vh; + } + + .max-h-32 { + max-height: 8rem; + } + + .min-h-28 { + min-height: 7rem; + } + + .min-h-\[200px\] { + min-height: 200px; + } + + .w-4 { + width: 1rem; + } + + .w-8 { + width: 2rem; + } + + .w-full { + width: 100%; + } + + .min-w-\[120px\] { + min-width: 120px; + } + + .max-w-full { + max-width: 100%; + } + + .max-w-md { + max-width: 28rem; + } + + .flex-1 { + flex: 1 1 0%; + } + + .\!shrink { + flex-shrink: 1 !important; + } + + .shrink { + flex-shrink: 1; + } + + .border-collapse { + border-collapse: collapse; + } + + .origin-center { + transform-origin: center; + } + + .rotate-180 { + --tw-rotate: 180deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) + skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + } + + .transform { + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) + skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + } + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + + .animate-spin { + animation: spin 1s linear infinite; + } + + .cursor-pointer { + cursor: pointer; + } + + .select-all { + -webkit-user-select: all; + user-select: all; + } + + .\!resize { + resize: both !important; + } + + .resize { + resize: both; + } + + .list-inside { + list-style-position: inside; + } + + .list-decimal { + list-style-type: decimal; + } + + .list-disc { + list-style-type: disc; + } + + .appearance-none { + -webkit-appearance: none; + appearance: none; + } + + .flex-col { + flex-direction: column; + } + + .items-center { + align-items: center; + } + + .justify-end { + justify-content: flex-end; + } + + .justify-center { + justify-content: center; + } + + .justify-between { + justify-content: space-between; + } + + .gap-1 { + gap: 0.25rem; + } + + .gap-2 { + gap: 0.5rem; + } + + .gap-4 { + gap: 1rem; + } + + .space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); + } + + .space-y-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); + } + + .space-y-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1rem * var(--tw-space-y-reverse)); + } + + .divide-y > :not([hidden]) ~ :not([hidden]) { + --tw-divide-y-reverse: 0; + border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); + } + + .overflow-hidden { + overflow: hidden; + } + + .overflow-x-auto { + overflow-x: auto; + } + + .overflow-y-auto { + overflow-y: auto; + } + + .truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .text-ellipsis { + text-overflow: ellipsis; + } + + .whitespace-normal { + white-space: normal; + } + + .whitespace-nowrap { + white-space: nowrap; + } + + .break-all { + word-break: break-all; + } + + .rounded { + border-radius: 0.25rem; + } + + .rounded-full { + border-radius: 9999px; + } + + .rounded-lg { + border-radius: 0.5rem; + } + + .rounded-md { + border-radius: 0.375rem; + } + + .rounded-xl { + border-radius: 0.75rem; + } + + .\!border { + border-width: 1px !important; + } + + .border { + border-width: 1px; + } + + .border-b { + border-bottom-width: 1px; + } + + .border-b-2 { + border-bottom-width: 2px; + } + + .border-l-4 { + border-left-width: 4px; + } + + .border-t { + border-top-width: 1px; + } + + .border-current { + border-color: currentColor; + } + + .border-red-200 { + --tw-border-opacity: 1; + border-color: rgb(254 202 202 / var(--tw-border-opacity, 1)); + } + + .bg-black\/10 { + background-color: rgb(0 0 0 / 0.1); + } + + .bg-blue-500 { + --tw-bg-opacity: 1; + background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1)); + } + + .bg-red-100 { + --tw-bg-opacity: 1; + background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1)); + } + + .bg-red-50 { + --tw-bg-opacity: 1; + background-color: rgb(254 242 242 / var(--tw-bg-opacity, 1)); + } + + .p-1 { + padding: 0.25rem; + } + + .p-1\.5 { + padding: 0.375rem; + } + + .p-2 { + padding: 0.5rem; + } + + .p-3 { + padding: 0.75rem; + } + + .p-4 { + padding: 1rem; + } + + .p-6 { + padding: 1.5rem; + } + + .px-1\.5 { + padding-left: 0.375rem; + padding-right: 0.375rem; + } + + .px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; + } + + .px-4 { + padding-left: 1rem; + padding-right: 1rem; + } + + .py-0\.5 { + padding-top: 0.125rem; + padding-bottom: 0.125rem; + } + + .py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; + } + + .py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } + + .pl-4 { + padding-left: 1rem; + } + + .text-left { + text-align: left; + } + + .text-center { + text-align: center; + } + + .font-mono { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + } + + .text-2xl { + font-size: 1.5rem; + line-height: 2rem; + } + + .text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; + } + + .text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; + } + + .text-base { + font-size: 1rem; + line-height: 1.5rem; + } + + .text-lg { + font-size: 1.125rem; + line-height: 1.75rem; + } + + .text-sm { + font-size: 0.875rem; + line-height: 1.25rem; + } + + .text-xl { + font-size: 1.25rem; + line-height: 1.75rem; + } + + .text-xs { + font-size: 0.75rem; + line-height: 1rem; + } + + .font-medium { + font-weight: 500; + } + + .font-semibold { + font-weight: 600; + } + + .lowercase { + text-transform: lowercase; + } + + .italic { + font-style: italic; + } + + .text-red-600 { + --tw-text-opacity: 1; + color: rgb(220 38 38 / var(--tw-text-opacity, 1)); + } + + .text-red-700 { + --tw-text-opacity: 1; + color: rgb(185 28 28 / var(--tw-text-opacity, 1)); + } + + .text-red-800 { + --tw-text-opacity: 1; + color: rgb(153 27 27 / var(--tw-text-opacity, 1)); + } + + .text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity, 1)); + } + + .underline { + text-decoration-line: underline; + } + + .opacity-0 { + opacity: 0; + } + + .opacity-50 { + opacity: 0.5; + } + + .shadow { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + } + + .shadow-lg { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + } + + .shadow-sm { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + } + + .blur { + --tw-blur: blur(8px); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) + var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); + } + + .\!filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) + var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow) !important; + } + + .filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) + var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); + } + + .backdrop-blur-sm { + --tw-backdrop-blur: blur(4px); + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) + var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) + var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) + var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) + var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + } + + .transition-all { + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; + } + + .transition-colors { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; + } + + .transition-transform { + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; + } + + .duration-150 { + transition-duration: 150ms; + } + + .duration-200 { + transition-duration: 200ms; + } + + .ease-out { + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + } + + .\[counter-increment\:line_0\] { + counter-increment: line 0; + } + + .\[counter-reset\:line\] { + counter-reset: line; + } + + .before\:mr-4::before { + content: var(--tw-content); + margin-right: 1rem; + } + + .before\:inline-block::before { + content: var(--tw-content); + display: inline-block; + } + + .before\:w-6::before { + content: var(--tw-content); + width: 1.5rem; + } + + .before\:select-none::before { + content: var(--tw-content); + -webkit-user-select: none; + user-select: none; + } + + .before\:text-right::before { + content: var(--tw-content); + text-align: right; + } + + .before\:font-mono::before { + content: var(--tw-content); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + } + + .before\:text-\[13px\]::before { + content: var(--tw-content); + font-size: 13px; + } + + .before\:content-\[counter\(line\)\]::before { + --tw-content: counter(line); + content: var(--tw-content); + } + + .before\:\[counter-increment\:line\]::before { + content: var(--tw-content); + counter-increment: line; + } + + .\*\:first\:mt-0:first-child > * { + margin-top: 0px; + } + + .\*\:last\:mb-0:last-child > * { + margin-bottom: 0px; + } + + .\*\:last\:after\:inline:last-child > *::after { + content: var(--tw-content); + display: inline; + } + + .\*\:last\:after\:align-baseline:last-child > *::after { + content: var(--tw-content); + vertical-align: baseline; + } + + .disabled\:cursor-not-allowed:disabled { + cursor: not-allowed; + } + + .disabled\:opacity-50:disabled { + opacity: 0.5; + } + + .group:hover .group-hover\:block { + display: block; + } + + .group:hover .group-hover\:opacity-100 { + opacity: 1; + } + + .\[\&\>p\]\:inline > p { + display: inline; + } + + .\[\&_svg\]\:h-auto svg { + height: auto; + } + + .\[\&_svg\]\:w-auto svg { + width: auto; + } + + li .\[li_\&\]\:pl-6 { + padding-left: 1.5rem; + } + + // --- Compact overrides for AI chat context --- + + & { + font-size: 13px !important; + line-height: 1.4 !important; + } + + [data-streamdown='code-block'] { + margin-top: 6px !important; + margin-bottom: 6px !important; + gap: 0 !important; + padding: 0 !important; + border-radius: 6px !important; + button svg { + pointer-events: auto !important; + width: 14px !important; + height: 14px !important; + } + } + + [data-streamdown='code-block-header'] { + height: 22px !important; + padding: 0 8px !important; + font-size: 11px !important; + background: var(--yakit-card-border-bg, rgba(0, 0, 0, 0.03)) !important; + border-bottom: 1px solid var(--yakit-border-color, rgba(0, 0, 0, 0.06)) !important; + border-radius: 6px 6px 0 0 !important; + gap: 4px !important; + + button svg { + width: 14px !important; + height: 14px !important; + } + } + + [data-streamdown='code-block-body'] { + padding: 6px 10px !important; + font-size: 12px !important; + line-height: 1.4 !important; + border-radius: 0 0 6px 6px !important; + border-top: none !important; + } + + h1 { + font-size: 1.125rem !important; + line-height: 1.5 !important; + margin-top: 8px !important; + margin-bottom: 2px !important; + } + + h2 { + font-size: 1rem !important; + line-height: 1.4 !important; + margin-top: 8px !important; + margin-bottom: 2px !important; + } + + h3 { + font-size: 0.9375rem !important; + line-height: 1.35 !important; + margin-top: 6px !important; + margin-bottom: 2px !important; + } + + h4 { + font-size: 0.875rem !important; + line-height: 1.3 !important; + margin-top: 6px !important; + margin-bottom: 2px !important; + } + + h5, + h6 { + font-size: 0.8125rem !important; + line-height: 1.25 !important; + margin-top: 4px !important; + margin-bottom: 2px !important; + } + + .space-y-4 > :not([hidden]) ~ :not([hidden]) { + margin-top: 4px !important; + } + + .space-y-2 > :not([hidden]) ~ :not([hidden]) { + margin-top: 1px !important; + } + + p { + margin-bottom: 2px !important; + line-height: 1.4 !important; + } + + ul, + ol { + margin-top: 1px !important; + margin-bottom: 1px !important; + padding-top: 0 !important; + padding-bottom: 0 !important; + } + + li { + line-height: 1.4 !important; + padding-top: 0 !important; + padding-bottom: 0 !important; + margin-top: 0 !important; + margin-bottom: 0 !important; + + p { + margin-bottom: 0 !important; + margin-top: 0 !important; + } + + > ul, + > ol { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + } + + li + li { + margin-top: 1px !important; + } + + ul.space-y-2 > :not([hidden]) ~ :not([hidden]), + ol.space-y-2 > :not([hidden]) ~ :not([hidden]) { + margin-top: 1px !important; + } + + blockquote { + margin-top: 4px !important; + margin-bottom: 4px !important; + padding-left: 0.75rem !important; + } + + hr { + margin-top: 6px !important; + margin-bottom: 6px !important; + } + + [data-streamdown='table-wrapper'] { + margin-top: 6px !important; + margin-bottom: 6px !important; + border-radius: 6px !important; + overflow: hidden !important; + + table { + th, + td { + padding: 2px 8px !important; + line-height: 1.4 !important; + font-size: 13px !important; + } + + th { + font-weight: 600 !important; + } + } + + > .flex.items-center.justify-end { + background: var(--yakit-card-border-bg, rgba(0, 0, 0, 0.03)) !important; + border-bottom: 1px solid var(--yakit-border-color, rgba(0, 0, 0, 0.06)) !important; + height: 22px !important; + padding: 0 8px !important; + border-radius: 6px 6px 0 0 !important; + gap: 4px !important; + + button svg { + width: 14px !important; + height: 14px !important; + } + } + } + + [data-streamdown='mermaid-block'] { + margin-top: 6px !important; + margin-bottom: 6px !important; + padding: 0 !important; + border-radius: 6px !important; + overflow: hidden !important; + + > .flex.items-center.justify-end.gap-2 { + height: 22px !important; + padding: 0 8px !important; + justify-content: flex-end !important; + background: var(--yakit-card-border-bg, rgba(0, 0, 0, 0.03)) !important; + border-bottom: 1px solid var(--yakit-border-color, rgba(0, 0, 0, 0.06)) !important; + gap: 4px !important; + box-sizing: content-box !important; + + button { + padding: 2px !important; + border: none !important; + background: transparent !important; + + svg { + width: 14px !important; + height: 14px !important; + } + } + } + + [data-streamdown='mermaid'] { + overflow: visible !important; + padding: 8px !important; + + > div { + transform: none !important; + touch-action: auto !important; + will-change: auto !important; + cursor: default !important; + pointer-events: none; + + > div { + pointer-events: auto; + } + } + + svg { + max-width: 100% !important; + height: auto !important; + display: block; + } + } + } + + .fixed.inset-0 { + [data-streamdown='mermaid'] svg { + max-width: 90vw !important; + max-height: 85vh !important; + width: auto !important; + height: auto !important; + } + } +} diff --git a/src/compoments/MarkDown/markdownRender.tsx b/src/compoments/MarkDown/markdownRender.tsx new file mode 100644 index 00000000..8d0df9e5 --- /dev/null +++ b/src/compoments/MarkDown/markdownRender.tsx @@ -0,0 +1,340 @@ +import { yakitNotify } from '@/utils/notification' +import classNames from 'classnames' +import styles from './markdownRender.module.scss' +import './markdownRender.scss' +import React, { useRef, useState, useEffect, useCallback } from 'react' +import { ErrorBoundary } from 'react-error-boundary' +import type { StreamdownProps, MathPlugin } from 'streamdown' +import { Streamdown } from 'streamdown' +import { mermaid } from '@streamdown/mermaid' +import { CopyComponents } from '../yakitUI/YakitTag/YakitTag' +import rehypeSlug from 'rehype-slug' +import rehypeSanitize from 'rehype-sanitize' +import { math } from '@streamdown/math' +import MDEditor from '@uiw/react-md-editor' +import { code } from '@streamdown/code' +const { Markdown } = MDEditor +function PreCode(props: { children?: React.ReactNode }) { + const ref = useRef(null) + const [copyStr, setCopyStr] = useState('') + useEffect(() => { + if (ref.current) { + // 初始赋值 + setCopyStr(ref.current.textContent || '') + // 监听后续变化 + const observer = new MutationObserver(() => { + if (ref.current) { + setCopyStr(ref.current.textContent || '') + } + }) + observer.observe(ref.current, { childList: true, subtree: true }) + return () => observer.disconnect() + } + }, []) + + return ( +
+      
+      {props.children}
+    
+ ) +} + +interface SafeMarkdownProp { + source?: string + className?: string + style?: React.CSSProperties +} + +// 目前非流式优先使用此SafeMarkdown,流式使用 StreamMarkdown +export const SafeMarkdown: React.FC = (props) => { + const { source, className, style } = props + return ( + { + return
{source}
+ }} + > + { + return ( + { + e.stopPropagation() + window.open(aProps.href || '', '_blank', 'noopener') + }} + /> + ) + }, + }} + /> +
+ ) +} + +function svgToPngBlob(svgEl: SVGSVGElement, scale = 2): Promise { + return new Promise((resolve, reject) => { + const cloned = svgEl.cloneNode(true) as SVGSVGElement + if (!cloned.getAttribute('xmlns')) cloned.setAttribute('xmlns', 'http://www.w3.org/2000/svg') + + const bbox = svgEl.getBoundingClientRect() + const w = Math.min(bbox.width * scale, 4096) + const h = Math.min(bbox.height * scale, 4096) + + const svgData = new XMLSerializer().serializeToString(cloned) + const dataUrl = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData))) + const img = new Image() + img.onload = () => { + try { + const canvas = document.createElement('canvas') + canvas.width = w + canvas.height = h + const ctx = canvas.getContext('2d') + if (!ctx) { + reject(new Error('Failed to get canvas 2d context')) + return + } + ctx.fillStyle = '#ffffff' + ctx.fillRect(0, 0, w, h) + ctx.drawImage(img, 0, 0, w, h) + canvas.toBlob((blob) => { + blob ? resolve(blob) : reject(new Error('toBlob returned null')) + }, 'image/png') + } catch (e) { + reject(e) + } + } + img.onerror = () => { + reject(new Error('Failed to load SVG as image')) + } + img.src = dataUrl + }) +} + +async function copyMermaidAsPng(mermaidBlock: HTMLElement) { + const svgEl = mermaidBlock.querySelector('[data-streamdown="mermaid"] svg') as SVGSVGElement | null + if (!svgEl) { + yakitNotify('error', 'No SVG found') + return + } + try { + const blob = await svgToPngBlob(svgEl, 2) + await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]) + yakitNotify('success', 'PNG copied to clipboard') + } catch (e) { + yakitNotify('error', `Copy PNG failed: ${e}`) + } +} + +async function downloadMermaidAsPng(mermaidBlock: HTMLElement) { + const svgEl = mermaidBlock.querySelector('[data-streamdown="mermaid"] svg') as SVGSVGElement | null + if (!svgEl) { + yakitNotify('error', 'No SVG found') + return + } + try { + const blob = await svgToPngBlob(svgEl, 2) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'diagram.png' + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } catch (e) { + yakitNotify('error', `Download PNG failed: ${e}`) + } +} + +interface StreamMarkdownProps extends StreamdownProps { + content?: string + wrapperClassName?: string +} + +const MERMAID_BTN_CLASS = 'cursor-pointer p-1 text-muted-foreground transition-all hover:text-foreground' + +const COPY_SOURCE_SVG = `` + +const COPY_PNG_SVG = `` + +const DOWNLOAD_SVG = `` + +export const StreamMarkdown: React.FC = React.memo((props) => { + const { content, wrapperClassName, ...restProps } = props + const wrapperRef = useRef(null) + + const handleMermaidAction = useCallback( + (e: MouseEvent) => { + const target = e.target as HTMLElement + const btn = target.closest('[data-mermaid-action]') as HTMLElement | null + if (!btn) return + const action = btn.getAttribute('data-mermaid-action') + const block = btn.closest('[data-streamdown="mermaid-block"]') as HTMLElement | null + if (!block) return + + e.stopPropagation() + e.preventDefault() + + if (action === 'copy-png') { + copyMermaidAsPng(block) + } else if (action === 'download-png') { + downloadMermaidAsPng(block) + } else if (action === 'copy-source') { + const src = content || '' + const mermaidBlocks = src.match(/```mermaid\n([\s\S]*?)```/g) + if (mermaidBlocks && mermaidBlocks.length > 0) { + const allBlocks = wrapperRef.current?.querySelectorAll('[data-streamdown="mermaid-block"]') + if (allBlocks) { + const idx = Array.from(allBlocks).indexOf(block) + const match = mermaidBlocks[idx >= 0 ? idx : 0] + const source = match?.replace(/```mermaid\n?/, '').replace(/\n?```$/, '') || '' + navigator.clipboard.writeText(source).then(() => { + yakitNotify('success', 'Mermaid source copied') + }) + } + } + } + }, + [content], + ) + + useEffect(() => { + const wrapper = wrapperRef.current + if (!wrapper) return + + const preventZoom = (e: WheelEvent) => { + if ((e.target as HTMLElement).closest('[data-streamdown="mermaid-block"]')) { + e.stopPropagation() + } + } + wrapper.addEventListener('wheel', preventZoom, { capture: true }) + + const preventPointer = (e: PointerEvent) => { + const t = e.target as HTMLElement + if (t.closest('[data-streamdown="mermaid"]') && !t.closest('button') && !t.closest('[data-mermaid-action]')) { + e.stopPropagation() + } + } + wrapper.addEventListener('pointerdown', preventPointer, { capture: true }) + + const injectButtons = () => { + const blocks = wrapper.querySelectorAll('[data-streamdown="mermaid-block"]') + blocks.forEach((block) => { + if (block.querySelector('[data-mermaid-action]')) return + const controlsArea = block.querySelector('.flex.items-center.justify-end.gap-2') + if (!controlsArea) return + + const mkBtn = (action: string, title: string, svg: string) => { + const btn = document.createElement('button') + btn.setAttribute('data-mermaid-action', action) + btn.className = MERMAID_BTN_CLASS + btn.title = title + btn.type = 'button' + btn.innerHTML = svg + return btn + } + + const downloadBtn = mkBtn('download-png', 'Download PNG', DOWNLOAD_SVG) + const copyPngBtn = mkBtn('copy-png', 'Copy as PNG', COPY_PNG_SVG) + const copySourceBtn = mkBtn('copy-source', 'Copy Source', COPY_SOURCE_SVG) + + const existingDownload = controlsArea.querySelector('[title="Download diagram"]') + if (existingDownload) existingDownload.remove() + + controlsArea.insertBefore(downloadBtn, controlsArea.firstChild) + controlsArea.insertBefore(copyPngBtn, controlsArea.firstChild) + controlsArea.insertBefore(copySourceBtn, controlsArea.firstChild) + }) + } + + const observer = new MutationObserver(() => injectButtons()) + observer.observe(wrapper, { childList: true, subtree: true }) + injectButtons() + + wrapper.addEventListener('click', handleMermaidAction) + return () => { + observer.disconnect() + wrapper.removeEventListener('click', handleMermaidAction) + wrapper.removeEventListener('wheel', preventZoom, { capture: true } as any) + wrapper.removeEventListener('pointerdown', preventPointer, { capture: true } as any) + } + }, [handleMermaidAction]) + + return ( + + ) +}) diff --git a/src/compoments/MilkdownEditor/Block/Block.module.scss b/src/compoments/MilkdownEditor/Block/Block.module.scss new file mode 100644 index 00000000..01f59d90 --- /dev/null +++ b/src/compoments/MilkdownEditor/Block/Block.module.scss @@ -0,0 +1,52 @@ +.block-wrapper { + position: absolute; + left: -32px !important; + .tooltip-popover-btn { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 4px; + border-radius: 4px; + border: 1px solid var(--Colors-Use-Neutral-Border); + cursor: pointer; + color: var(--Colors-Use-Neutral-Text-1-Title); + svg { + width: 16px; + height: 16px; + } + &:hover { + background: var(--Colors-Use-Neutral-Bg-Hover); + color: var(--Colors-Use-Main-Primary); + } + } +} +.block-wrapper[data-show="false"]{ + display: none; +} +.tooltip-popover { + padding-top: 0; + :global { + .ant-popover-arrow { + display: none; + } + .ant-popover-inner { + /* 投影-大 */ + box-shadow: 0px 6px 20px 0px var(--Colors-Use-Basic-Shadow); + border: 1px solid var(--Colors-Use-Neutral-Border); + border-radius: 8px; + } + } + .tooltip-popover-content { + width: 210px; + display: flex; + align-items: center; + // padding: 12px; + gap: 8px; + flex-wrap: wrap; + svg { + color: var(--Colors-Use-Neutral-Text-1-Title); + } + } +} diff --git a/src/compoments/MilkdownEditor/Block/Block.tsx b/src/compoments/MilkdownEditor/Block/Block.tsx new file mode 100644 index 00000000..462a7427 --- /dev/null +++ b/src/compoments/MilkdownEditor/Block/Block.tsx @@ -0,0 +1,185 @@ +import { BlockProvider } from '@milkdown/kit/plugin/block' +import { useInstance } from '@milkdown/react' +import { useCallback, useEffect, useRef, useState } from 'react' +import { usePluginViewContext } from '@prosemirror-adapter/react' +import { paragraphSchema } from '@milkdown/kit/preset/commonmark' +import { TextSelection } from '@milkdown/kit/prose/state' +import styles from './Block.module.scss' +import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton' +import { OutlinePlusIcon } from '@/assets/icon/outline' +import { YakitPopover } from '@/compoments/yakitUI/YakitPopover/YakitPopover' + +import { useMemoizedFn } from 'ahooks' +import type { Ctx } from '@milkdown/kit/ctx' +import { + createBlankHeading1, + createBlankHeading2, + createBlankHeading3, + createBlankOrderedList, + createBlankUnorderedList, + createBlankTask, + createBlankCodeBlock, + createBlankQuote, + createBlankHighLight, + uploadFileInMilkdown, + createDivider, +} from '../utils/utils' +import { Tooltip } from 'antd' +import type { BlockListProps } from '../constants' +import { createMilkdownMenuListByKey, localBlockKey, MilkdownMenuKeyEnum, onlineBlockKey } from '../constants' +import type { HttpUploadImgBaseRequest } from '@/apiUtils/http' +import { useStore } from '@/store' +import type { InitEditorHooksLocalProps } from '../utils/initEditor' + +interface BlockViewProps { + type: HttpUploadImgBaseRequest['type'] + notepadHash?: string + localProps?: InitEditorHooksLocalProps +} +export const BlockView: React.FC = (props) => { + const { notepadHash, type, localProps } = props + + const userInfo = useStore((s) => s.userInfo) + + const ref = useRef(null) + const blockProvider = useRef() + + const [visibleAdd, setVisibleAdd] = useState(false) + const [blockList, setBlockList] = useState( + localProps?.local ? createMilkdownMenuListByKey(localBlockKey) : createMilkdownMenuListByKey(onlineBlockKey), + ) // 后期选中某个类型的组件可能不会显示一些操作 + + const { view, prevState } = usePluginViewContext() + + const [loading, get] = useInstance() + const action = useCallback( + (fn: (ctx: Ctx) => void) => { + if (loading) return + get().action(fn) + }, + [loading], + ) + + useEffect(() => { + const div = ref.current + if (loading || !div) { + return + } + + const editor = get() + if (!editor) { + return + } + blockProvider.current = new BlockProvider({ + ctx: editor.ctx, + content: div, + }) + blockProvider.current?.update() + }, [loading]) + + useEffect(() => { + return () => { + // 单独的Effect中卸载,避免在开发过程中热加载的时候报错 + blockProvider.current?.destroy() + } + }, []) + + const onAddContent = useMemoizedFn(({ key }) => { + switch (key) { + case MilkdownMenuKeyEnum.Heading1: + createBlankHeading1(action, view) + break + case MilkdownMenuKeyEnum.Heading2: + createBlankHeading2(action, view) + break + + case MilkdownMenuKeyEnum.Heading3: + createBlankHeading3(action, view) + break + + case MilkdownMenuKeyEnum.OrderedList: + createBlankOrderedList(action, view) + break + + case MilkdownMenuKeyEnum.UnorderedList: + createBlankUnorderedList(action, view) + break + case MilkdownMenuKeyEnum.Task: + createBlankTask(action, view) + break + case MilkdownMenuKeyEnum.CodeBlock: + createBlankCodeBlock(action, view) + break + case MilkdownMenuKeyEnum.Quote: + createBlankQuote(action, view) + break + case MilkdownMenuKeyEnum.HighLight: + createBlankHighLight(action, view) + break + case MilkdownMenuKeyEnum.File: + uploadFileInMilkdown(action, { + type, + notepadHash: notepadHash || '', + userId: userInfo.user_id || 0, + }) + + break + case MilkdownMenuKeyEnum.Divider: + createDivider(action, view) + break + + default: + break + } + if (key !== MilkdownMenuKeyEnum.CodeBlock) { + view.focus() + } + setVisibleAdd(false) + blockProvider.current?.hide() + }) + const onAdd = (e) => { + e.preventDefault() + e.stopPropagation() + const editor = get() + if (!editor) return + if (!view.hasFocus()) view.focus() + const { state, dispatch } = view + const active = blockProvider.current?.active + if (!active) return + const $pos = active.$pos + const pos = $pos.pos + active.node.nodeSize + let tr = state.tr.insert(pos, paragraphSchema.type(editor.ctx).create()) + tr = tr.setSelection(TextSelection.near(tr.doc.resolve(pos))) + dispatch(tr.scrollIntoView()) + blockProvider.current?.hide() + } + return ( +
+ + {blockList.map((ele) => ( + + onAddContent(ele)} + /> + + ))} +
+ } + visible={visibleAdd} + onVisibleChange={setVisibleAdd} + > +
+ +
+ + + ) +} diff --git a/src/compoments/MilkdownEditor/Blockquote.tsx b/src/compoments/MilkdownEditor/Blockquote.tsx new file mode 100644 index 00000000..b9c75a11 --- /dev/null +++ b/src/compoments/MilkdownEditor/Blockquote.tsx @@ -0,0 +1,16 @@ +import {useNodeViewContext} from "@prosemirror-adapter/react" + +export const Blockquote = () => { + const {contentRef} = useNodeViewContext() + + return ( +
+ ) +} diff --git a/src/compoments/MilkdownEditor/CodeBlock/CodeBlock.tsx b/src/compoments/MilkdownEditor/CodeBlock/CodeBlock.tsx new file mode 100644 index 00000000..3187ed82 --- /dev/null +++ b/src/compoments/MilkdownEditor/CodeBlock/CodeBlock.tsx @@ -0,0 +1,68 @@ +import { YakitEditor } from '@/compoments/yakitUI/YakitEditor/YakitEditor' +import type { IMonacoEditor } from '@/utils/editors' +import { useNodeViewContext } from '@prosemirror-adapter/react' +import { useCreation, useInViewport, useMemoizedFn } from 'ahooks' +import React, { useState, useEffect, useRef } from 'react' +import { TextSelection } from '@milkdown/kit/prose/state' + +interface CustomCodeComponentProps { + // 是否控制编辑器类型 + isControlEditorType?: boolean +} +export const CustomCodeComponent: React.FC = ({ isControlEditorType = true }) => { + const { node, view, getPos, contentRef } = useNodeViewContext() + // 编辑器实例 + const [editor, setEditor] = useState() + + const codeRef = useRef(null) + const [inViewport = true] = useInViewport(codeRef) + const isFocusRef = useRef(false) // 是否已经初次聚焦 + + useEffect(() => { + if (!editor) return + if (!isFocusRef.current) { + editor.focus() + isFocusRef.current = true + } + }, [editor]) + + const readonly = useCreation(() => { + return !view.editable + }, [view.editable]) + + const updateEditorContent = useMemoizedFn((newContent) => { + try { + if (!inViewport) return + const { state, dispatch } = view + const start = getPos() || 0 + const end = start + node.nodeSize + if (newContent) { + const updatedContent = state.schema.nodes.code_block.create( + null, // 不带任何属性 + state.schema.text(newContent), + ) + const tr = state.tr.replaceWith(start, end, updatedContent) // 用新内容替换节点内容 + dispatch(tr) // 提交事务更新内容 + } else { + const updatedContent = state.schema.nodes.paragraph.create() + let tr = state.tr.deleteRange(start, end).insert(start, updatedContent) + const selection = TextSelection.near(tr.doc.resolve(start)) + tr.setSelection(selection) + dispatch(tr) + view.focus() + } + } catch (error) {} + }) + return ( +
+ {/*
*/} + +
+ ) +} diff --git a/src/compoments/MilkdownEditor/CollabManager.tsx b/src/compoments/MilkdownEditor/CollabManager.tsx new file mode 100644 index 00000000..ad483da7 --- /dev/null +++ b/src/compoments/MilkdownEditor/CollabManager.tsx @@ -0,0 +1,220 @@ +import {CollabService} from "@milkdown/plugin-collab" +import {Doc, Transaction, YTextEvent} from "yjs" +import {WebsocketProvider} from "./WebsocketProvider/WebsocketProvider" +import {ObservableV2} from "lib0/observable" +import isEqual from "lodash/isEqual" +import {CollabStatus} from "./MilkdownEditorType" +import {yakitNotify} from "@/utils/notification" +import {NotepadWsRequest} from "./WebsocketProvider/WebsocketProviderType" +import {notepadActions, notepadSaveStatus} from "./WebsocketProvider/constants" + +const {ipcRenderer} = window.require("electron") +export interface CollabUserInfo { + userId: number + name: string + color: string + heardImg: string +} + +interface CollabManagerEvents { + "offline-after": (event: CloseEvent) => void + "link-status-onchange": (s: CollabStatus) => void + "online-users": (s: CollabUserInfo[]) => void + "sync-title": (s: string) => void +} + +interface CollabNotepadWsRequest { + token: string + notepadHash: string + title: string +} + +export class CollabManager extends ObservableV2 { + private doc!: Doc + private wsProvider!: WebsocketProvider + + private collabStatus: CollabStatus + private users: CollabUserInfo[] + private title: string + + constructor( + private collabService: CollabService, + private user: CollabUserInfo, + private wsRequest: CollabNotepadWsRequest + ) { + super() + this.collabStatus = { + status: "disconnected", + isSynced: false, + saveStatus: notepadSaveStatus.saveProgress + } + this.users = [] + this.title = wsRequest.title || "" + } + + flush = async (template: string) => { + let wsUrl = "" + try { + wsUrl = await ipcRenderer.invoke("get-ws-url") + } catch (error) { + yakitNotify("error", `getWSUrl错误:${error}`) + } + if (!wsUrl) { + yakitNotify("error", "wsUrl数据错误") + return + } + this.doc?.destroy() + this.wsProvider?.destroy() + + this.doc = new Doc() + + const docTitle = this.doc.getText("title") // 使用 Y.Text 存储标题 + docTitle.observe((yTextEvent, transaction) => this.docObserveTitle(yTextEvent, transaction)) + + const url = wsUrl + "api/handle/tow/way/ws" + // const url = "ws://localhost:1880/ws/my-room" + this.wsProvider = new WebsocketProvider(url, this.wsRequest.notepadHash, this.doc, { + connect: true, + data: { + token: this.wsRequest.token, + messageType: "notepad", + notepadHash: this.wsRequest.notepadHash, + params: { + docType: notepadActions.join, + saveStatus: notepadSaveStatus.saveProgress, + userName: this.user.name + }, + yjsParams: "" + } + }) + this.wsProvider.awareness.setLocalStateField("user", { + userId: this.user.userId, + name: this.user.name, + color: this.user.color, + heardImg: this.user.heardImg + }) + this.wsProvider.on("status", (payload) => { + // 获取当前所有用户的状态 + const users = this.getOnlineUser() + this.setOnlineUsers([...users]) + if (users.length === 1) { + this.setCollabStatus({...this.collabStatus, isSynced: true, status: payload.status}) + } else { + this.setCollabStatus({...this.collabStatus, status: payload.status}) + } + }) + this.wsProvider.on("connection-close", (payload) => { + this.emit("offline-after", [payload]) + }) + this.collabService.bindDoc(this.doc).setAwareness(this.wsProvider.awareness) + this.wsProvider.once("synced", (isSynced: boolean) => { + this.setCollabStatus({...this.collabStatus, isSynced}) + }) + + this.wsProvider.once("online-user-count", (onlineUserCount: number) => { + this.collabService + .applyTemplate(template, () => { + return onlineUserCount < 2 && this.collabStatus.isSynced + }) + .connect() + }) + + this.wsProvider.on("saveStatus", ({saveStatus}) => { + this.setCollabStatus({...this.collabStatus, saveStatus}) + }) + // 监听在线用户数据 + this.wsProvider?.awareness?.on("change", (payload) => { + // 获取当前所有用户的状态 + const users = this.getOnlineUser() + this.setOnlineUsers([...users]) + }) + } + + private docObserveTitle(yarrayEvent: YTextEvent, tr: Transaction) { + if (tr.local) return + const value = this.doc.getText("title").toString() + this.onSetTitle(value) + } + + private getOnlineUser() { + const awarenessMap = this.wsProvider.awareness.getStates() + const users = Array.from(awarenessMap, ([key, value]) => value.user) + return users + } + + private onSetTitle(newTitle: string) { + if (!isEqual(this.title, newTitle)) { + this.title = newTitle + // 触发'状态变化'事件 + this.emit("sync-title", [newTitle]) + } + } + + // 用于更新collabStatus并触发事件 + private setCollabStatus(newStatus: CollabStatus) { + if (!isEqual(this.collabStatus, newStatus)) { + this.collabStatus = {...newStatus} + // 触发'状态变化'事件 + this.emit("link-status-onchange", [newStatus]) + } + } + + // 用于更新setOnlineUsers并触发事件 + private setOnlineUsers(newUsers: CollabUserInfo[]) { + if (!isEqual(this.users, newUsers)) { + this.users = [...newUsers] + // 触发'状态变化'事件 + this.emit("online-users", [newUsers]) + } + } + + setTitle(value) { + const docTitle = this.doc.getText("title") + if (isEqual(this.title, value)) return + this.title = value + // 两次修改封装到一个事务中,观察器只会被触发一次 + this.doc.transact(() => { + docTitle.delete(0, docTitle.length) + docTitle.insert(0, value) + }) + } + + sendContent(value: {content: string; title: string}) { + const {content, title} = value + const v: NotepadWsRequest = { + messageType: "notepad", + notepadHash: this.wsRequest.notepadHash, + params: { + content, + title, + docType: "editDoc", + saveStatus: notepadSaveStatus.saveProgress + }, + yjsParams: "", + token: this.wsRequest.token + } + if (this.wsProvider && this.wsProvider?.ws && this.wsProvider.ws?.readyState === WebSocket.OPEN) { + const sendValueString = JSON.stringify(v) + this.wsProvider?.ws?.send(Buffer.from(sendValueString)) + } + } + + destroy(): void { + this.doc.getText("title").unobserve((yTextEvent, transaction) => this.docObserveTitle(yTextEvent, transaction)) + this.wsProvider?.destroy() + super.destroy() + } + connect() { + Promise.resolve().then(() => { + this.wsProvider?.connect() + this.collabService?.connect() + }) + } + + disconnect() { + Promise.resolve().then(() => { + this.collabService?.disconnect() + this.wsProvider?.disconnect() + }) + } +} diff --git a/src/compoments/MilkdownEditor/CustomFile/CustomFile.module.scss b/src/compoments/MilkdownEditor/CustomFile/CustomFile.module.scss new file mode 100644 index 00000000..a800858a --- /dev/null +++ b/src/compoments/MilkdownEditor/CustomFile/CustomFile.module.scss @@ -0,0 +1,143 @@ +$fileDivHeight: 64px; +.file-custom { + display: flex; + width: 400px; + height: $fileDivHeight; + padding: 8px 12px 8px 8px; + align-items: center; + gap: 8px; + border-radius: 8px; + border: 1px solid var(--Colors-Use-Neutral-Border); + cursor: default; + user-select: none; + position: relative; + cursor: pointer; + margin-bottom: 20px; + .file-custom-content { + display: flex; + align-items: center; + flex: 1; + gap: 8px; + overflow: hidden; + .file-type { + flex-shrink: 0; + width: 48px; + height: 48px; + } + .file-info { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; + overflow: hidden; + .info-name { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 16px; + font-weight: 500; + line-height: 24px; + height: 24px; + + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + word-break: break-all; + } + } + .info-size { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + line-height: 16px; + } + } + .file-extra { + flex-shrink: 0; + + .x-btn { + display: none; + } + .percent-loading { + display: flex; + align-items: center; + gap: 4px; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + line-height: 16px; + cursor: pointer; + } + .loading { + display: flex; + align-items: center; + justify-content: flex-end; + min-width: 40px; + &:hover { + .x-btn { + display: flex; + } + .percent-loading { + display: none; + } + } + } + } + .file-spinning { + align-items: center; + gap: 4px; + padding-top: 4px; + } +} +.file-custom-selected { + border-color: var(--Colors-Use-Main-Primary); + &:hover { + border-color: var(--Colors-Use-Main-Focus); + box-shadow: 0 0 0 1px var(--Colors-Use-Basic-Shadow); + } +} + +.tooltip-action { + position: absolute; + bottom: $fileDivHeight; + display: flex; + align-items: center; + border-radius: 8px; + border: 1px solid var(--Colors-Use-Neutral-Border); + background: var(--Colors-Use-Basic-Background); + box-shadow: 0px 6px 20px 0px var(--Colors-Use-Basic-Shadow); + height: 52px; + padding: 12px; + gap: 8px; +} +.success-action { + display: flex; + align-items: center; + gap: 8px; +} + +.error-action { + display: flex; + align-items: center; + justify-content: flex-end; + min-width: 40px; + gap: 4px; +} +.x-circle-btn { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 4px; + cursor: pointer; + border-radius: 4px; + &:hover { + background-color: var(--Colors-Use-Neutral-Bg); + } + .x-circle-icon { + svg { + width: 20px; + height: 20px; + color: var(--Colors-Use-Error-Primary); + } + } +} diff --git a/src/compoments/MilkdownEditor/CustomFile/CustomFile.tsx b/src/compoments/MilkdownEditor/CustomFile/CustomFile.tsx new file mode 100644 index 00000000..8e8e09ab --- /dev/null +++ b/src/compoments/MilkdownEditor/CustomFile/CustomFile.tsx @@ -0,0 +1,533 @@ +import { useNodeViewContext } from '@prosemirror-adapter/react' +import styles from './CustomFile.module.scss' +import type { ReactNode } from 'react' +import { useEffect, useRef, useState } from 'react' +import { randomString } from '@/utils/randomUtil' +import { yakitNotify } from '@/utils/notification' +import { + IconNotepadFileTypeWord, + IconNotepadFileTypeCompress, + IconNotepadFileTypePPT, + IconNotepadFileTypePdf, + IconNotepadFileTypeUnknown, + IconNotepadFileTypeExcel, +} from '../icon/icon' +import { Progress, Tooltip } from 'antd' +import { + OutlineDocumentduplicateIcon, + OutlineDownloadIcon, + OutlineRefreshIcon, + OutlineUploadIcon, + OutlineXIcon, +} from '@/assets/icon/outline' +import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton' +import numeral from 'numeral' +import classNames from 'classnames' +import { TooltipIcon } from '../Tooltip/Tooltip' +import { useMemoizedFn } from 'ahooks' +import { SolidXcircleIcon } from '@/assets/icon/solid' +import { YakitHint } from '@/compoments/yakitUI/YakitHint/YakitHint' +import React from 'react' +import { SolidCloudDownloadIcon } from '@/assets/newIcon' +import type { DownloadUrlToLocal } from '@/hook/useDownloadUrlToLocal/useDownloadUrlToLocal' +import useDownloadUrlToLocalHooks from '@/hook/useDownloadUrlToLocal/useDownloadUrlToLocal' +import { + apiDownloadStorageType, + onOpenLocalFileByPath, + saveDialogAndGetLocalFileInfo, +} from '@/pages/notepadManage/notepadManage/utils' +import type { YakitHintProps } from '@/compoments/yakitUI/YakitHint/YakitHintType' +import type { UploadFileTypeProps, UploadOSSStartProps } from '@/hook/useUploadOSS/useUploadOSS' +import useUploadOSSHooks from '@/hook/useUploadOSS/useUploadOSS' +import { getHttpFileLinkInfo, getLocalFileLinkInfo } from './utils' +import { setClipboardText } from '@/utils/clipboard' +import { getFileNameByUrl } from '../utils/trackDeletePlugin' +import { httpDeleteNotepadFile } from '@/apiUtils/http' +import { useStore } from '@/store' +import { YakitSpin } from '@/compoments/yakitUI/YakitSpin/YakitSpin' +import { LogNodeStatusFileIcon, SolidYakCattleNoBackColorIcon } from '@/assets/icon/colors' + +interface CustomFileItem { + name: string + size: number + type: string + url: string + path: string +} +interface DownFileInfoProps { + url: string + path: string + fileName: string +} +/** 上传文件后,后端拼接hash和文件名的字符:&*&;方便截取文件名 */ +export const getTypeAndNameByPath = (path) => { + let newPath = decodeURIComponent(path.split('/').pop()) + const firstIndex = newPath.indexOf('&*&') + if (firstIndex !== -1) { + newPath = newPath.substring(firstIndex + 3, path.length) + } + const index = newPath.lastIndexOf('.') + const fileType = newPath.substring(index, newPath.length) + const fileName = newPath.split('\\').pop() || '未知命名' + return { fileType, fileName } +} +interface CustomFileProps { + type: UploadFileTypeProps +} + +export const renderFileTypeIcon = (params: { type: string; iconClassName?: string }) => { + const { type, iconClassName } = params + switch (type) { + case '.doc': + case 'doc': + return + case '.zip': + case 'zip': + case '.7z': + case '7z': + case '.tar': + case 'tar': + return + case '.ppt': + case 'ppt': + return + case '.csv': + case 'csv': + case '.xlsx': + case 'xlsx': + case '.numbers': + case 'numbers': + case 'xls': + case '.xls': + return + case '.pdf': + case 'pdf': + return + case '.txt': + case 'txt': + return + case '.yak': + case 'yak': + return + default: + return + } +} +export const CustomFile: React.FC = (props) => { + const { type } = props + const { node, contentRef, selected, view, getPos, setAttrs } = useNodeViewContext() + + const { attrs } = node + const [fileInfo, setFileInfo] = useState({ + name: '', + size: 0, + type: '', + url: '', + path: '', + }) + const [percent, setPercent] = useState(0) + const [loading, setLoading] = useState(false) + const [errorReason, setErrorReason] = useState('') + const [downFileInfo, setDownFileInfo] = useState() + const [queryFileErrorInfo, setQueryFileErrorInfo] = useState('') + const [loadingRefresh, setLoadingRefresh] = useState(false) + + const uploadTokenRef = useRef(randomString(40)) + + const userInfo = useStore((s) => s.userInfo) + + useEffect(() => { + const { fileId, path: initPath = '', uploadUserId = '' } = attrs + const path = initPath.replace(/\\/g, '\\') + if (fileId !== '0') { + getFileInfoByLink() + } else if (path) { + if (uploadUserId === '0' || uploadUserId !== userInfo.user_id) return + getLocalFileLinkInfo(path).then((fileInfo) => { + const { fileName, fileType } = getTypeAndNameByPath(path) + const item = { + name: fileName, + size: fileInfo.size, + type: fileType, + url: '', + path, + } + setFileInfo(item) + onUpload(path) + }) + } + }, [attrs.fileId, attrs.uploadUserId]) + const { onStart: onStartUpload, onCancel: onUploadCancel } = useUploadOSSHooks({ + taskToken: uploadTokenRef.current, + onUploadData: (p) => { + if (p >= 100) setLoading(false) + setPercent(p) + }, + onUploadEnd: () => { + setLoading(false) + setPercent(0) + }, + onUploadError: (reason) => { + setErrorReason(reason || '') + }, + setUrl: (url) => { + setAttrs({ fileId: url, uploadUserId: 0, path: '' }) + setFileInfo((v) => ({ ...v, url })) + }, + }) + + const getFileInfoByLink = useMemoizedFn(() => { + const { fileId, path: initPath = '' } = attrs + const path = initPath.replace(/\\/g, '\\') + if (fileId !== '0') { + setLoadingRefresh(true) + + apiDownloadStorageType(fileId).then((filePath) => { + getHttpFileLinkInfo(filePath, true) + .then((res) => { + const { fileType, fileName } = getTypeAndNameByPath(fileId) + const item = { + name: fileName, + size: res.size, + type: fileType, + url: filePath, + path, + } + setFileInfo(item) + }) + .catch((e) => { + setTimeout(() => { + setQueryFileErrorInfo(`${e}`) + }, 500) + }) + .finally(() => + setTimeout(() => { + setLoadingRefresh(false) + }, 200), + ) + }) + } + }) + const onUpload = (filePath) => { + if (!filePath) return + setLoading(true) + setErrorReason('') + setPercent(0) + const value: UploadOSSStartProps = { + filePath, + filedHash: attrs?.notepadHash || '', + type, + } + onStartUpload(value) + } + const onReloadUpload = useMemoizedFn((e) => { + e.stopPropagation() + e.preventDefault() + if (fileInfo) onUpload(fileInfo.path) + }) + const onCancel = useMemoizedFn(() => { + onUploadCancel().then(() => { + onRemoveFile() + }) + }) + + const onDown = (e) => { + e.stopPropagation() + e.preventDefault() + const { fileId } = attrs + apiDownloadStorageType(fileId).then((filePath) => { + saveDialogAndGetLocalFileInfo(filePath).then((v) => { + setDownFileInfo(v) + setFileInfo({ ...fileInfo, path: v.path }) + }) + }) + } + const onCopyLink = useMemoizedFn((e) => { + e.stopPropagation() + e.preventDefault() + if (fileInfo?.url) { + setClipboardText(fileInfo.url, { + failedCallback: () => { + yakitNotify('error', '复制失败') + }, + }) + } else { + yakitNotify('error', '复制失败') + } + }) + const onRemoveFile = useMemoizedFn(() => { + const { state, dispatch } = view + const { from, to } = state.selection + + // 如果选区是有效的 + if (from !== to) { + // 创建一个事务来删除选中的范围 + const tr = state.tr.delete(from, to) + + // 提交事务 + if (dispatch) { + dispatch(tr) + } + } else { + // 如果没有选中任何内容(光标仅在一个位置),删除当前节点 + const selectedNode = state.doc.nodeAt(from) + + if (selectedNode) { + // 使用 replaceRange 来删除选中的节点 + const tr = state.tr.delete(from - selectedNode.nodeSize, from) + + // 提交事务 + if (dispatch) { + dispatch(tr) + } + } + } + }) + const onOpenFile = useMemoizedFn(() => { + if (!fileInfo) return + onOpenLocalFileByPath(fileInfo?.path).then((flag) => { + if (!flag) { + setFileInfo({ ...fileInfo, path: '' }) + } + }) + }) + const onCancelDownload = useMemoizedFn(() => { + setFileInfo({ ...fileInfo, path: '' }) + setDownFileInfo(undefined) + }) + const onRefreshFileInfo = useMemoizedFn((e) => { + e.stopPropagation() + e.preventDefault() + setQueryFileErrorInfo('') + getFileInfoByLink() + }) + const errorNode = useMemoizedFn((error) => { + return ( + +
+ +
+
+ ) + }) + return ( + <> +
{ + e.stopPropagation() + e.preventDefault() + }} + > + {fileInfo.path || fileInfo.url ? ( + + {errorNode(errorReason)} + + } onClick={onReloadUpload} /> + +
+ ) : ( + <> + {loading ? ( +
+
+ {percent}% + null} + strokeWidth={12} + /> +
+ } + onClick={onCancel} + /> +
+ ) : ( +
+ {fileInfo.url && } onClick={onDown} />} + } onClick={onCopyLink} /> +
+ )} + + ) + } + /> + ) : queryFileErrorInfo ? ( + + {errorNode(queryFileErrorInfo)} + + } + loading={loadingRefresh} + onClick={onRefreshFileInfo} + /> + + + } + /> + ) : ( + + )} + + {downFileInfo && ( + setDownFileInfo(undefined)} + onCancelDownload={onCancelDownload} + onSuccess={onOpenFile} + // isEncodeURI={false} + /> + )} + + ) +} + +interface DownFilesModalProps { + /** 是否在流结束后删除oss上的资源。默认不删除 */ + isDeleteOOSAfterEnd?: boolean + visible: boolean + setVisible: (b: boolean) => void + url: string + fileName?: string + path: string + onCancelDownload: () => void + onSuccess?: () => void + yakitHintProps?: Omit + /** 是否需要编码,默认编码 */ + isEncodeURI?: boolean +} +/** 下载文件到本地 */ +export const DownFilesModal: React.FC = React.memo((props) => { + const { + isDeleteOOSAfterEnd, + visible, + setVisible, + fileName, + path, + url, + yakitHintProps, + onCancelDownload, + onSuccess, + isEncodeURI, + } = props + + const [percent, setPercent] = useState(0) + + const taskTokenRef = useRef(randomString(40)) + + const onProgressData = useMemoizedFn((newState) => { + const newPercent = Math.trunc(newState.percent * 100) + setPercent(newPercent) + }) + + const onUploadEnd = useMemoizedFn(() => { + setTimeout(() => { + setVisible(false) + setPercent(0) + if (isDeleteOOSAfterEnd) { + onDeleteOSSFile() + } + }, 1000) + }) + + const { onStart, onCancel: onNotepadDownCancel } = useDownloadUrlToLocalHooks({ + path, + taskToken: taskTokenRef.current, + onUploadData: onProgressData, + onUploadEnd, + onUploadSuccess: () => { + setPercent(0) + onSuccess && onSuccess() + }, + }) + useEffect(() => { + if (visible) { + const value: DownloadUrlToLocal = { + onlineUrl: url, + localPath: path, + isEncodeURI, + } + onStart(value) + } else { + onNotepadDownCancel() + setPercent(0) + } + }, [visible]) + + const onCancel = useMemoizedFn(() => { + onNotepadDownCancel().then(() => { + onCancelDownload() + }) + }) + // 删除oss资源 + const onDeleteOSSFile = useMemoizedFn(() => { + const fileName = getFileNameByUrl(url) + if (fileName) { + httpDeleteNotepadFile({ file_name: [fileName] }, false) + } + }) + return ( + } + okButtonProps={{ style: { display: 'none' } }} + isDrag={true} + mask={false} + title={fileName ?
{`${fileName}下载中...`}
: '下载中'} + {...(yakitHintProps || {})} + onCancel={onCancel} + visible={visible} + > + `已下载 ${percent}%`} + /> +
+ ) +}) + +interface CustomFileItemProps { + title: ReactNode + subTitle: ReactNode + describe?: ReactNode + extra: ReactNode +} +const CustomFileItem: React.FC = React.memo((props) => { + const { extra, title, subTitle, describe } = props + return ( +
+
{title}
+
+
{subTitle}
+ {describe &&
{describe}
} +
+
{extra}
+
+ ) +}) diff --git a/src/compoments/MilkdownEditor/CustomFile/utils.ts b/src/compoments/MilkdownEditor/CustomFile/utils.ts new file mode 100644 index 00000000..978e8656 --- /dev/null +++ b/src/compoments/MilkdownEditor/CustomFile/utils.ts @@ -0,0 +1,59 @@ +import {APIFunc} from "@/apiUtils/type" +import {yakitNotify} from "@/utils/notification" + +const {ipcRenderer} = window.require("electron") + +export interface HttpFileInfoRespose { + fileName: string + size: number + type: string +} +/**通过链接获取文件基本信息 */ +export const getHttpFileLinkInfo: APIFunc = (onlineUrl, hiddenError) => { + return new Promise((resolve, reject) => { + ipcRenderer + .invoke("get-http-file-link-info", encodeURI(onlineUrl)) + .then(resolve) + .catch((error) => { + if (!hiddenError) yakitNotify("error", `获取链接信息错误${error}`) + reject(error) + }) + }) +} + +export interface LocalFileInfoRespose { + size: number +} +/**通过本地路径获取文件基本信息 */ +export const getLocalFileLinkInfo: APIFunc = (path, hiddenError) => { + return new Promise((resolve, reject) => { + ipcRenderer + .invoke("fetch-file-info-by-path", path) + .then((res) => { + resolve({ + size: res.size + }) + }) + .catch((error) => { + if (!hiddenError) yakitNotify("error", `获取文件信息失败:${error}`) + reject(error) + }) + }) +} + +export interface GetLocalFileTypeRespose { + name: string + suffix: string +} +/**通过本地路径获取文件后缀/类型 */ +export const getLocalFileName: APIFunc = (path, hiddenError) => { + return new Promise((resolve, reject) => { + ipcRenderer + .invoke("fetch-file-name-by-path", path) + .then(resolve) + .catch((error) => { + if (!hiddenError) yakitNotify("error", `getLocalFileName 失败: ${error}`) + reject(error) + }) + }) +} diff --git a/src/compoments/MilkdownEditor/ListItem/ListItem.module.scss b/src/compoments/MilkdownEditor/ListItem/ListItem.module.scss new file mode 100644 index 00000000..86fd421f --- /dev/null +++ b/src/compoments/MilkdownEditor/ListItem/ListItem.module.scss @@ -0,0 +1,53 @@ +%dot-wrapper { + width: 20px; + height: 24px; + display: flex; + justify-content: center; + align-items: center; +} +.list-item-wrapper { + display: inline-flex; + align-items: flex-start; + gap: 8px; + margin-bottom: 8px; + width: 100%; + .bullet-dot-wrapper { + @extend %dot-wrapper; + .bullet-dot { + width: 5px; + height: 5px; + border-radius: 50%; + background-color: var(--Colors-Use-Main-Primary); + color: var(--Colors-Use-Main-On-Primary); + } + } + + .label-dot-wrapper { + @extend %dot-wrapper; + color: var(--Colors-Use-Main-Primary); + } + + .checked-dot-wrapper { + @extend %dot-wrapper; + color: var(--Colors-Use-Main-Primary); + padding-bottom: 2px; + } + + .list-item { + line-height: 24px; + flex: 1; + word-break: break-all; + } + + .list-item-checked { + text-decoration: line-through; + color: var(--Colors-Use-Neutral-Disable); + } +} + +.list-item-checked-wrapper { + border-radius: 4px; + &:hover { + background-color: var(--Colors-Use-Neutral-Bg-Hover); + } +} diff --git a/src/compoments/MilkdownEditor/ListItem/ListItem.tsx b/src/compoments/MilkdownEditor/ListItem/ListItem.tsx new file mode 100644 index 00000000..88cc5523 --- /dev/null +++ b/src/compoments/MilkdownEditor/ListItem/ListItem.tsx @@ -0,0 +1,55 @@ +import { YakitCheckbox } from '@/compoments/yakitUI/YakitCheckbox/YakitCheckbox' +import { useNodeViewContext } from '@prosemirror-adapter/react' +import React from 'react' +import styles from './ListItem.module.scss' +import classNames from 'classnames' +import { useCreation } from 'ahooks' + +export const ListItem: React.FC = () => { + const { node, view, contentRef, setAttrs } = useNodeViewContext() + + const { label, checked, listType } = node.attrs + + const disabled = useCreation(() => { + return !view.editable + }, [view.editable]) + + const onUpdateChecked = (val: boolean) => { + setAttrs({ checked: val }) + } + + const render = () => { + if (checked === null) { + if (listType === 'bullet') { + return ( +
+
+
+ ) + } + + return
{label}
+ } + + return ( +
+ onUpdateChecked(e.target.checked)} /> +
+ ) + } + return ( +
  • + {render()} +
    +
  • + ) +} diff --git a/src/compoments/MilkdownEditor/Mention/CustomMention.module.scss b/src/compoments/MilkdownEditor/Mention/CustomMention.module.scss new file mode 100644 index 00000000..74137b79 --- /dev/null +++ b/src/compoments/MilkdownEditor/Mention/CustomMention.module.scss @@ -0,0 +1,40 @@ +.mention-custom { + height: 24px; + padding: 4px 8px; + + color: var(--yakit-header-color); + text-align: center; + font-size: 12px; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.5px; + background-color: var(--Colors-Use-Main-Bg-Hover); + border-radius: 4px; + width: max-content; + cursor: pointer; + user-select: none; + display: inline-flex; + margin: 0 4px; +} + +.mention-custom-selected { + background-color: var(--Colors-Use-Main-Primary); + color: var(--Colors-Use-Main-On-Primary); +} + +.mention-custom-popover { + :global { + .ant-popover-inner { + border-radius: 4px; + } + .ant-popover-inner-content { + padding: 8px 12px; + } + } +} +.mention-custom-popover-title { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; +} diff --git a/src/compoments/MilkdownEditor/Mention/CustomMention.tsx b/src/compoments/MilkdownEditor/Mention/CustomMention.tsx new file mode 100644 index 00000000..85120856 --- /dev/null +++ b/src/compoments/MilkdownEditor/Mention/CustomMention.tsx @@ -0,0 +1,63 @@ +import { useNodeViewContext } from '@prosemirror-adapter/react' +import React, { useState } from 'react' +import styles from './CustomMention.module.scss' +import classNames from 'classnames' +import { YakitPopover } from '@/compoments/yakitUI/YakitPopover/YakitPopover' +import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton' +import { useMemoizedFn } from 'ahooks' +import type { API } from '@/services/swagger/resposeType' +import { apiNotepadEit } from './utils' +import { yakitNotify } from '@/utils/notification' +import { getMentionId } from '../utils/mentionPlugin' + +interface CustomMentionProps { + notepadHash: string +} +export const CustomMention: React.FC = (props) => { + const { notepadHash } = props + const { node, setAttrs, selected, contentRef } = useNodeViewContext() + const [visible, setVisible] = useState(false) + const onSendMessage = useMemoizedFn(() => { + if (!node.attrs?.userId) { + yakitNotify('error', '用户id不存在') + return + } + let mentionId = node.attrs?.mentionId + if (!mentionId) { + const id = getMentionId() + setAttrs({ mentionId: id }) + mentionId = id + } + const params: API.NotepadEitRequest = { + eitUser: node.attrs?.userId, + notepadHash, + mentionId, + } + apiNotepadEit(params).finally(() => setVisible(false)) + }) + return ( + + 发送提及通知? + + 通知 + +
    + } + visible={visible} + onVisibleChange={setVisible} + trigger="click" + overlayClassName={styles['mention-custom-popover']} + > +
    + + ) +} diff --git a/src/compoments/MilkdownEditor/Mention/MentionListView.module.scss b/src/compoments/MilkdownEditor/Mention/MentionListView.module.scss new file mode 100644 index 00000000..77ff15d8 --- /dev/null +++ b/src/compoments/MilkdownEditor/Mention/MentionListView.module.scss @@ -0,0 +1,81 @@ +.mention { + position: absolute; + display: flex; + flex-direction: column; + gap: 8px; + border: 1px solid var(--Colors-Use-Neutral-Border); + border-radius: 8px; + overflow: hidden; + padding: 0 12px; + box-shadow: + 0 3px 6px -4px rgba(0, 0, 0, 0.12), + 0 6px 16px 0 rgba(0, 0, 0, 0.08), + 0 9px 28px 8px rgba(0, 0, 0, 0.05); + background-color: var(--Colors-Use-Basic-Background); + .mention-user-list { + margin-bottom: 41px; + overflow-y: auto; + height: 150px; + .mention-user-row { + display: flex; + align-items: center; + width: 100%; + height: 32px; + cursor: pointer; + user-select: none; + line-height: 16px; + border-top: 1px solid var(--Colors-Use-Neutral-Border); + color: var(--yakit-header-color); + + &:first-child { + border-top-color: transparent; + } + &:hover { + background: var(--Colors-Use-Neutral-Bg); + border-radius: 8px; + border-top-color: transparent; + } + } + .mention-user-row:hover + .mention-user-row { + border-top-color: transparent; + } + .mention-user-item { + width: 100%; + padding: 8px 4px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + word-break: break-all; + } + .mention-user-item-selected { + background-color: var(--Colors-Use-Main-Bg); + border-radius: 8px; + } + .mention-user-row + .mention-user-row:has(.mention-user-item-selected), + .mention-user-row:has(.mention-user-item-selected) + .mention-user-row { + border-top-color: transparent; + } + } + .mention-footer { + position: absolute; + bottom: 0; + height: 41px; + display: flex; + justify-content: space-between; + align-items: center; + left: 0; + right: 0; + padding: 8px 16px; + border-top: 1px solid var(--Colors-Use-Neutral-Border); + background-color: var(--Colors-Use-Basic-Background); + z-index: 9; + .checkbox { + display: flex; + align-items: center; + gap: 4px; + } + } +} +.mention[data-show="false"] { + display: none; +} diff --git a/src/compoments/MilkdownEditor/Mention/MentionListView.tsx b/src/compoments/MilkdownEditor/Mention/MentionListView.tsx new file mode 100644 index 00000000..e5b5cafb --- /dev/null +++ b/src/compoments/MilkdownEditor/Mention/MentionListView.tsx @@ -0,0 +1,206 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react' +import type { Ctx } from '@milkdown/kit/ctx' +import { slashFactory, SlashProvider } from '@milkdown/kit/plugin/slash' +import { useInstance } from '@milkdown/react' +import { usePluginViewContext } from '@prosemirror-adapter/react' +import styles from './MentionListView.module.scss' +import { useDebounceEffect, useMemoizedFn } from 'ahooks' +import { YakitInput } from '@/compoments/yakitUI/YakitInput/YakitInput' +import { YakitCheckbox } from '@/compoments/yakitUI/YakitCheckbox/YakitCheckbox' +import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton' +import { apiGetUserSearch } from '@/pages/notepadManage/NotepadShareModal/utils' +import type { API } from '@/services/swagger/resposeType' +import { RollingLoadList } from '@/compoments/RollingLoadList/RollingLoadList' +import { yakitNotify } from '@/utils/notification' +import { callCommand } from '@milkdown/kit/utils' +import { getMentionId, mentionCommand } from '../utils/mentionPlugin' +import { apiNotepadEit } from './utils' +import classNames from 'classnames' +import type { InputRef } from 'antd' +import { useStore } from '@/store' +export const mentionFactory = slashFactory('Commands') + +interface MentionListViewProps { + notepadHash: string +} +const mentionWidth = 240 +const mentionTarget = '@' +export const MentionListView: React.FC = (props) => { + const { notepadHash } = props + const userInfo = useStore((s) => s.userInfo) + const [isSendMessage, setIsSendMessage] = useState(false) + const [listLoading, setListLoading] = useState(false) + const [keyWord, setKeyWord] = useState('') + const [userList, setUserList] = useState([]) + + const [currentSelected, setCurrentSelected] = useState() + + const ref = useRef(null) + const slashProvider = useRef() + const searchRef = useRef(null) + + const { view, prevState } = usePluginViewContext() + const [loading, get] = useInstance() + const action = useCallback( + (fn: (ctx: Ctx) => void) => { + if (loading) return + get().action(fn) + }, + [loading], + ) + + useEffect(() => { + const div = ref.current + if (loading || !div) { + return + } + slashProvider.current = new SlashProvider({ + content: div, + trigger: mentionTarget, + }) + }, [loading]) + + useEffect(() => { + return () => { + // 单独的Effect中卸载,避免报错 + slashProvider.current?.destroy() + } + }, []) + + useDebounceEffect( + () => { + if (loading || !slashProvider.current) return + slashProvider.current?.update(view, prevState) + if (shouldShow()) { + searchRef.current?.focus() + } + }, + [loading, view, prevState], + { wait: 200, leading: true }, + ) + + const shouldShow = useMemoizedFn(() => { + if (!slashProvider.current) return false + const currentTextBlockContent = slashProvider.current.getContent(view) + + if (!currentTextBlockContent) return false + + const target = currentTextBlockContent.at(-1) + + if (!target) return false + + return mentionTarget === target + }) + + const getUserList = useMemoizedFn((value) => { + setListLoading(true) + apiGetUserSearch({ keywords: value }) + .then((res) => { + // 需要过滤当前登录人 + const newData = [...(res.data || [])].filter((ele) => ele.id !== userInfo.user_id) + if (newData.length > 0) { + setCurrentSelected(newData[0]) + } + setUserList(newData) + }) + .finally(() => + setTimeout(() => { + setListLoading(false) + }, 200), + ) + }) + + const onSearch = useMemoizedFn((value: string) => { + getUserList(value) + }) + + const onPressEnter = useMemoizedFn((e) => { + onSearch(e.target.value) + }) + + const onNotepadEit = useMemoizedFn((row: API.UserList) => { + const mentionId = getMentionId() + action(callCommand(mentionCommand.key, { userName: row.name, userId: row.id, mentionId })) + // 关闭窗口 + view.focus() + slashProvider.current?.hide() + setKeyWord('') + setUserList([]) + setCurrentSelected(undefined) + // 发送通知 + if (isSendMessage) { + const params: API.NotepadEitRequest = { + eitUser: row.id, + notepadHash, + mentionId, + } + apiNotepadEit(params) + } + }) + + const onSure = useMemoizedFn(() => { + if (!currentSelected) { + yakitNotify('info', '请选择用户') + return + } + onNotepadEit(currentSelected) + }) + + return ( + + ) +} diff --git a/src/compoments/MilkdownEditor/Mention/utils.ts b/src/compoments/MilkdownEditor/Mention/utils.ts new file mode 100644 index 00000000..c24f930f --- /dev/null +++ b/src/compoments/MilkdownEditor/Mention/utils.ts @@ -0,0 +1,19 @@ +import {APIFunc} from "@/apiUtils/type" +import {NetWorkApi} from "@/services/fetch" +import {API} from "@/services/swagger/resposeType" +import {yakitNotify} from "@/utils/notification" + +export const apiNotepadEit: APIFunc = (query, hiddenError) => { + return new Promise((resolve, reject) => { + NetWorkApi({ + method: "post", + url: "notepad/eit", + data: {...query} + }) + .then(resolve) + .catch((err) => { + if (!hiddenError) yakitNotify("error", "apiNotepadEit 失败:" + err) + reject(err) + }) + }) +} diff --git a/src/compoments/MilkdownEditor/MilkdownEditor.tsx b/src/compoments/MilkdownEditor/MilkdownEditor.tsx new file mode 100644 index 00000000..7a2ba716 --- /dev/null +++ b/src/compoments/MilkdownEditor/MilkdownEditor.tsx @@ -0,0 +1,471 @@ +import {defaultValueCtx} from "@milkdown/kit/core" +import React, {useEffect, useRef, useState} from "react" + +import {Milkdown, MilkdownProvider} from "@milkdown/react" + +import {ProsemirrorAdapterProvider} from "@prosemirror-adapter/react" + +import "./css/index.scss" +import {yakitNotify} from "@/utils/notification" +import {getMarkdown} from "@milkdown/kit/utils" +import {CollabStatus, CustomMilkdownProps, DeleteOSSFileItem, MilkdownEditorProps} from "./MilkdownEditorType" + +import {diffLines} from "diff" +import { + useCreation, + useDebounceFn, + useInViewport, + useMemoizedFn, + useRafInterval, + useThrottleFn, + useUpdateEffect +} from "ahooks" + +import {httpDeleteNotepadFile} from "@/apiUtils/http" +import {deletedFileUrlsCtx} from "./utils/trackDeletePlugin" +import moment from "moment" +import {useStore} from "@/store" +import {CollabManager, CollabUserInfo} from "./CollabManager" +import emiter from "@/utils/eventBus/eventBus" + +import {collabServiceCtx} from "@milkdown/plugin-collab" +import {showYakitModal} from "../yakitUI/YakitModal/YakitModalConfirm" +import {tokenOverdue} from "@/services/fetch" +import {isBoolean} from "lodash" +import {notepadSaveStatus} from "./WebsocketProvider/constants" +import {API} from "@/services/swagger/resposeType" +import {apiSaveNotepad} from "@/pages/notepadManage/notepadManage/utils" +import useInitEditorHooks, {InitEditorHooksCollabProps} from "./utils/initEditor" +import {useGoEditNotepad} from "@/pages/notepadManage/hook/useGoEditNotepad" +const markdown1 = ` + +1. 5565 +2. 665fdsf + +#ggg + +![1.00]( + +Maybe more? ![]() + +[Mirone](https://github.com/Saul-Mirone) + +` +const markdown2 = `#ggg +fsdfsdf +` +export const randomMilkdownUserColor = () => `#${Math.floor(Math.random() * 16777215).toString(16)}` + +const saveHistoryIntervalTime = 1000 * 60 + +/**选择的图片转为Base64 */ +export const getBase64 = (file): Promise => + new Promise((resolve, reject) => { + const reader = new FileReader() + reader.readAsDataURL(file) + reader.onload = () => resolve(reader.result as string) + reader.onerror = (error) => reject(error) + }) +export const isDifferenceGreaterThan30Seconds = (timestamp1, timestamp2) => { + // 计算绝对时间差 + const difference = Math.abs(timestamp1 - timestamp2) + + // 判断是否大于30秒(10秒 = 10000毫秒) + return difference > 1000 * 30 +} +/**笔记本错误弹窗 */ +export const showOnlineErrorNotification = (event: {code: number; reason: string; onOk: () => void}) => { + const m = showYakitModal({ + title: "异常", + content: ( + + 错误原因:{event.code}:{event.reason} + + ), + maskClosable: false, + closable: false, + onOkText: "关闭页面", + cancelButtonProps: {style: {display: "none"}}, + onOk: () => { + event.onOk() + m.destroy() + }, + bodyStyle: {padding: 24} + }) +} + +const CustomMilkdown: React.FC = React.memo((props) => { + const userInfo = useStore((s) => s.userInfo) + + const {defaultValue, collabProps, setEditor, onSaveContentBeforeDestroy} = props + + const milkdownRef = useRef(null) + const [inViewport = true] = useInViewport(milkdownRef) + const collabManagerRef = useRef() // 协作管理器 + + const {goEditNotepad, goAddNotepad} = useGoEditNotepad() + + useEffect(() => { + return () => { + // 统一 + collabManagerRef.current?.destroy() + collabManagerRef.current = undefined + clearRemove() + onDeleteAllFiles() + } + }, []) + + //#region 编辑器初始 + const collabParams: InitEditorHooksCollabProps = useCreation(() => { + if (!collabProps) { + const def: InitEditorHooksCollabProps = { + title: "", + enableCollab: false, + milkdownHash: "", + routeInfo: { + pageId: "", + route: null + }, + onChangeWSLinkStatus: () => {}, + onChangeOnlineUser: () => {}, + onSetTitle: () => {}, + onCollab: () => {}, + onSaveHistory: () => {} + } + return def + } + return { + ...collabProps, + onCollab: (ctx) => onCollab(ctx), + onSaveHistory: (newValue) => onSaveHistory(newValue) + } + }, [collabProps]) + const {get, loading} = useInitEditorHooks({ + ...props, + inViewport, + collabProps: collabParams + }) + /**更新最新的editor */ + useEffect(() => { + if (loading) return + const editor = get() + if (editor) { + if (setEditor) setEditor(editor) + // DeletedFiles + editor.action(onSetDeletedFiles) + } + }, [loading, get]) + useEffect(() => { + return () => { + const value = get()?.action(getMarkdown()) || "" + onSaveContentBeforeDestroy && onSaveContentBeforeDestroy(value) + } + }, []) + //#endregion + + //#region 删除资源 + const [deletedFiles, setDeletedFiles] = useState([]) + const [interval, setInterval] = useState(undefined) // 文件删除间隔 + const clearRemove = useRafInterval(() => { + onDeleteFiles() + }, interval) + /** 设置当前文档中被删除的文件名称 */ + const onSetDeletedFiles = useDebounceFn( + useMemoizedFn((ctx) => { + // 获取 trackDeletePlugin 插件共享的值 + const urls = ctx.get(deletedFileUrlsCtx) + if (urls.length > 0) { + setInterval(1000) + } else { + setInterval(undefined) + } + setDeletedFiles(urls) + }), + {wait: 200} + ).run + + // 校验文件是否真的被删除 如果不在defaultValue中 则是真的删除 + const judgeDeleteFile = useMemoizedFn((fileNames: string[] = [])=>{ + const value = get()?.action(getMarkdown()) || "" + let newFileNames = fileNames.filter((fileName)=>!(value).includes(fileName)) + return newFileNames + }) + + /**删除文档中被删除的所有文件 */ + const onDeleteAllFiles = useMemoizedFn(() => { + if (deletedFiles.length > 0) { + httpDeleteNotepadFile({file_name: judgeDeleteFile(deletedFiles.map((ele) => ele.fileName))}, true) + } + }) + /**删除在文档中被删除30s的文件 */ + const onDeleteFiles = useMemoizedFn(() => { + const fileName: string[] = [] + const newDeletedFiles: DeleteOSSFileItem[] = [] + const length = deletedFiles.length + for (let index = 0; index < length; index++) { + const element = deletedFiles[index] + if (isDifferenceGreaterThan30Seconds(element.time, moment().valueOf())) { + fileName.push(element.fileName) + } else { + newDeletedFiles.push(element) + } + } + if (fileName.length > 0) { + setInterval(undefined) + httpDeleteNotepadFile({file_name: judgeDeleteFile(fileName)}, true).finally(() => { + // 暂不考虑删除失败的情况 + get()?.action((ctx) => ctx.update(deletedFileUrlsCtx, () => [...newDeletedFiles])) + }) + } + }) + //#endregion 删除资源 end + + //#region 协作文档 + const wsStatusRef = useRef({ + status: "disconnected", + isSynced: false, + saveStatus: notepadSaveStatus.saveProgress + }) // 记录当前链接状态 + // TODO - 共享标题 + useUpdateEffect(() => { + if (collabManagerRef.current) { + collabManagerRef.current.setTitle(collabParams.title) + } + }, [collabParams.title]) + + useUpdateEffect(() => { + if (inViewport) { + // 直接连接会collabService有意外报错情况 + // 第二次flush,之前的collabService只是断开连接没有销毁,所以值有保留,传入defaultValue编辑器内部会自己做对比显示最新的内容 + collabManagerRef.current?.flush(defaultValue || "") + } else { + onCollabDisconnect() + } + }, [inViewport]) + + const onCollab = useMemoizedFn((ctx) => { + const {milkdownHash, title} = collabParams + if (!collabParams.enableCollab) { + onCollabDisconnect() + return + } + if (!milkdownHash) { + // yakitNotify("error", "enableCollab true,启用协作文档时,hash值必须存在") + return + } + if (collabManagerRef.current) return + const user = { + userId: userInfo.user_id || 0, + name: userInfo.companyName || "", + color: `${randomMilkdownUserColor()}`, + heardImg: userInfo.companyHeadImg || "" + } + const collabService = ctx.get(collabServiceCtx) + collabManagerRef.current = new CollabManager(collabService, user, { + token: userInfo.token, + notepadHash: milkdownHash, + title + }) + collabManagerRef.current.flush(defaultValue || "") + collabManagerRef.current.on("offline-after", onLinkError) + collabManagerRef.current.on("online-users", onSetOnlineUsers) + collabManagerRef.current.on("link-status-onchange", onLineStatus) + collabManagerRef.current.on("sync-title", onSetTitle) + }) + + /**同步标题 */ + const onSetTitle = useDebounceFn( + useMemoizedFn((value: string) => { + collabParams?.onSetTitle(value) + }), + {wait: 200, leading: true} + ).run + + /**在线链接状态 */ + const onLineStatus = useDebounceFn( + useMemoizedFn((value: CollabStatus) => { + wsStatusRef.current = value + collabParams?.onChangeWSLinkStatus(value) + }), + {wait: 200, leading: true} + ).run + + /**获取到所有的在线用户,然后设置在线用户数据 */ + const onSetOnlineUsers = useDebounceFn( + useMemoizedFn((userList: CollabUserInfo[]) => { + collabParams?.onChangeOnlineUser(userList) + }), + {wait: 200, leading: true} + ).run + + /**处理ws链接出错 */ + const onLinkError = useDebounceFn( + useMemoizedFn((event: CloseEvent) => { + const {routeInfo} = collabParams + if (!routeInfo) { + yakitNotify("error", "当enableCollab为true,routeInfo必传") + return + } + switch (event.code) { + case 200: // 正常关闭 + break + case 401: // 401是指没有传token或者token过期 + loginOut(event) + break + + case 404: // 文档不存在/已经被删除 + onShowSave(event) + break + case 403: //没有编辑或者阅读的权限 + case 209: // 209 这种可以能处理出错,例如没传messageType + case 500: // 就可能是后端从http升级websocket失败啊,这种服务相关的 + onShowErrorModal(event) + break + default: + break + } + }), + {wait: 200, leading: true} + ).run + const loginOut = useMemoizedFn((event: CloseEvent) => { + const {routeInfo} = collabParams + if (!routeInfo) return + // 退出登录 + tokenOverdue({ + code: 401, + message: event.reason, + userInfo + }) + // 关闭笔记本编辑一级菜单 + setTimeout(() => { + emiter.emit( + "onCloseFirstMenu", + JSON.stringify({ + route: routeInfo.route + }) + ) + }, 1000) + }) + const onShowSave = useMemoizedFn((event: CloseEvent) => { + const {title} = collabParams + // 弹出框,显示网络异常,关闭/保存按钮 + const s = showYakitModal({ + title: "文档不存在/已经被删除", + content: 错误原因:{event.reason}, + maskClosable: false, + closable: false, + showConfirmLoading: true, + onOkText: "保存当前文档", + onCancelText: "不保存", + onOk: () => { + const markdownContent = get()?.action(getMarkdown()) || "" + // 有内容才保存,没有内容新建 + if (markdownContent) { + const params: API.PostNotepadRequest = { + title, + content: markdownContent + } + apiSaveNotepad(params).then((hash) => { + goEditNotepad({ + notepadHash: hash, + title + }) + onCloseCurrentPage() + s.destroy() + }) + } else { + goAddNotepad() + onCloseCurrentPage() + s.destroy() + } + }, + onCancel: () => { + onCloseCurrentPage() + s.destroy() + }, + bodyStyle: {padding: 24} + }) + }) + const onShowErrorModal = useMemoizedFn((event: CloseEvent) => { + // 弹出框,显示网络异常,关闭/保存按钮 + showOnlineErrorNotification({ + code: event.code, + reason: event.reason, + onOk: () => { + onCloseCurrentPage() + } + }) + yakitNotify("error", event.reason) + }) + const onCloseCurrentPage = useMemoizedFn(() => { + const {routeInfo} = collabParams + if (!routeInfo) return + emiter.emit("onCloseCurrentPage", routeInfo.pageId) + }) + const onCollabDisconnect = useMemoizedFn(() => { + collabManagerRef.current?.disconnect() + }) + //#endregion + + //#region 保存历史 + //是否保存历史,开启协作文档后,才有保存历史 + const isSaveHistory = useCreation(() => { + if (!collabParams.enableCollab) { + return false + } + if (isBoolean(collabParams.enableSaveHistory)) { + return collabParams.enableSaveHistory + } + return !!collabParams.enableSaveHistory?.enable + }, [collabParams.enableCollab, collabParams.enableSaveHistory]) + // 保存历史的间隔时间 默认每隔1min + const historyIntervalTime = useCreation(() => { + if (isBoolean(collabParams.enableSaveHistory)) { + return saveHistoryIntervalTime + } + return collabParams.enableSaveHistory?.interval || saveHistoryIntervalTime + }, [collabParams.enableSaveHistory]) + /**默认每隔1min 保存历史 */ + const onSaveHistory = useThrottleFn( + useMemoizedFn((value: string) => { + if (isSaveHistory) collabManagerRef.current?.sendContent({content: value, title: collabParams.title}) + }), + {wait: historyIntervalTime} + ).run + //#endregion + + /*TODO - 历史文档差异对比 */ + const onDifferences = useMemoizedFn((ctx) => { + // 获取两个文档的差异 + const differences = diffLines(markdown1, markdown2) + + let content = "" + + differences.forEach((part) => { + if (part.removed) { + content += `~~${part.value}~~\n` // 用删除线标记删除内容 + } else if (part.added) { + content += "`" + `${part.value}` + "`" + "\n" // 用高亮标记新增内容 + } else { + content += part.value // 未改变的内容保持原样 + } + }) + + ctx.set(defaultValueCtx, content) + }) + return ( +
    + +
    + ) +}) + +export const MilkdownEditor: React.FC = React.memo((props) => { + return ( + + + + + + ) +}) diff --git a/src/compoments/MilkdownEditor/MilkdownEditorType.ts b/src/compoments/MilkdownEditor/MilkdownEditorType.ts new file mode 100644 index 00000000..21933190 --- /dev/null +++ b/src/compoments/MilkdownEditor/MilkdownEditorType.ts @@ -0,0 +1,69 @@ +import type { Editor } from '@milkdown/kit/core'; +import type { MilkdownPlugin } from '@milkdown/kit/ctx'; +import type { CollabUserInfo } from './CollabManager'; +import type { + NotepadSaveStatusType, + WSConnectedStatusType, +} from './WebsocketProvider/WebsocketProviderType'; +import type { YakitRoute } from '@/pages/AIAgent/enums/yakitRoute'; + +export type EditorMilkdownProps = Editor; + +export interface CollabStatus { + /** ws链接状态 */ + status: WSConnectedStatusType; + /** 文档是否同步 */ + isSynced: boolean; + /** 文档是否保存 */ + saveStatus: NotepadSaveStatusType; +} + +export interface MilkdownCollabProps { + /** 文档标题 */ + title: string; + /** 启用协作文档 默认不启用 */ + enableCollab: boolean; + /** enableCollab为true,该字段必传,协作文档得唯一标识 */ + milkdownHash: string; + /** 编辑器使用的页面,enableCollab为true,该字段必传 */ + routeInfo: { pageId: string; route: YakitRoute | null }; + /** 是否开启保存历史 开启后默认间隔 1s */ + enableSaveHistory?: boolean | { enable: boolean; interval: number }; + /** 文档链接状态变化 */ + onChangeWSLinkStatus: (v: CollabStatus) => void; + /** 在线用户数据变化 */ + onChangeOnlineUser: (v: CollabUserInfo[]) => void; + /** 同步标题 */ + onSetTitle: (s: string) => void; +} +export interface CustomMilkdownProps { + /** 编辑器使用的模块名称,目前只有记事本(企业版叫云文档)中使用 */ + type: 'notepad'; + /** 设置为只读 */ + readonly?: boolean; + /** 编辑器默认值 */ + defaultValue?: string; + /** 编辑器是否传入type */ + isControlEditorType?: boolean; + editor?: EditorMilkdownProps; + setEditor?: (s: EditorMilkdownProps) => void; + /** 自定义插件 */ + customPlugin?: MilkdownPlugin | MilkdownPlugin[]; + + /** 协作文档相关参数 */ + collabProps?: MilkdownCollabProps; + /** 编辑器内容的变化 tip:在线协作时,A本地触发内容的变化,B编辑器会同步内容,但listener.markdownUpdated监听不到 */ + onMarkdownUpdated?: (next: string, per: string) => void; + /** 卸载前,抛出去最新的内容 */ + onSaveContentBeforeDestroy?: (value: string) => void; + /** 定位 dom元素id */ + positionElementId?: string; +} +export interface MilkdownEditorProps extends CustomMilkdownProps { + [key: string]: unknown; +} + +export interface DeleteOSSFileItem { + fileName: string; + time: number; +} diff --git a/src/compoments/MilkdownEditor/MilkdownHr/MilkdownHr.module.scss b/src/compoments/MilkdownEditor/MilkdownHr/MilkdownHr.module.scss new file mode 100644 index 00000000..54398620 --- /dev/null +++ b/src/compoments/MilkdownEditor/MilkdownHr/MilkdownHr.module.scss @@ -0,0 +1,10 @@ +.hr-body { + padding: 12px 4px; + margin-bottom: 24px; + .hr { + border-top: 1px solid var(--Colors-Use-Neutral-Border); + } + &:hover { + background-color: var(--Colors-Use-Neutral-Bg-Hover); + } +} diff --git a/src/compoments/MilkdownEditor/MilkdownHr/MilkdownHr.tsx b/src/compoments/MilkdownEditor/MilkdownHr/MilkdownHr.tsx new file mode 100644 index 00000000..e13fb0cb --- /dev/null +++ b/src/compoments/MilkdownEditor/MilkdownHr/MilkdownHr.tsx @@ -0,0 +1,12 @@ +import {useNodeViewContext} from "@prosemirror-adapter/react" +import styles from "./MilkdownHr.module.scss" +import React from "react" + +export const MilkdownHr: React.FC = () => { + const {contentRef} = useNodeViewContext() + return ( +
    +
    +
    + ) +} diff --git a/src/compoments/MilkdownEditor/Placeholder.tsx b/src/compoments/MilkdownEditor/Placeholder.tsx new file mode 100644 index 00000000..1fa78bbe --- /dev/null +++ b/src/compoments/MilkdownEditor/Placeholder.tsx @@ -0,0 +1,88 @@ +import { $ctx, $prose } from '@milkdown/kit/utils'; +import type { EditorState, Selection } from '@milkdown/kit/prose/state'; +import { Plugin, PluginKey } from '@milkdown/kit/prose/state'; +import type { Node } from '@milkdown/kit/prose/model'; +import { Decoration, DecorationSet } from '@milkdown/kit/prose/view'; +import { findParent } from '@milkdown/kit/prose'; +// import type { Editor } from '@milkdown/kit/core'; + +// type DefineFeature = ( +// editor: Editor, +// config?: Config, +// ) => void | Promise; + +const isInCodeBlock = (selection: Selection) => { + const type = selection.$from.parent.type; + return type.name === 'code_block'; +}; + +const isInList = (selection: Selection) => { + const type = selection.$from.node(selection.$from.depth - 1)?.type; + return type?.name === 'list_item'; +}; + +const isDocEmpty = (doc: Node) => { + return doc.childCount <= 1 && !doc.firstChild?.content.size; +}; + +function createPlaceholderDecoration( + state: EditorState, + placeholderText: string, +): Decoration | null { + const { selection } = state; + if (!selection.empty) return null; + + const $pos = selection.$anchor; + const node = $pos.parent; + if (node.content.size > 0) return null; + + const inTable = findParent((node) => node.type.name === 'table')($pos); + if (inTable) return null; + + const before = $pos.before(); + + return Decoration.node(before, before + node.nodeSize, { + class: 'crepe-placeholder', + 'data-placeholder': placeholderText, + }); +} + +interface PlaceholderConfig { + text: string; + mode: 'doc' | 'block'; +} + +export type PlaceHolderFeatureConfig = Partial; + +export const placeholderConfig = $ctx( + { + text: 'Please enter...', + mode: 'block', + } satisfies PlaceholderConfig, + 'placeholderConfigCtx', +); + +export const placeholderPlugin = $prose((ctx) => { + return new Plugin({ + key: new PluginKey('CREPE_PLACEHOLDER'), + props: { + decorations: (state) => { + const config = ctx.get(placeholderConfig.key); + if ((config.mode as string) === 'doc' && !isDocEmpty(state.doc)) + return null; + + if (isInCodeBlock(state.selection) || isInList(state.selection)) + return null; + + const placeholderText = config.text ?? 'Please enter...'; + const deco = createPlaceholderDecoration( + state, + placeholderText, + ); + if (!deco) return null; + + return DecorationSet.create(state.doc, [deco]); + }, + }, + }); +}); diff --git a/src/compoments/MilkdownEditor/Slash/Slash.module.scss b/src/compoments/MilkdownEditor/Slash/Slash.module.scss new file mode 100644 index 00000000..0789a3f0 --- /dev/null +++ b/src/compoments/MilkdownEditor/Slash/Slash.module.scss @@ -0,0 +1,44 @@ +.slash { + position: absolute; + display: flex; + flex-direction: column; + border: 1px solid var(--Colors-Use-Neutral-Border); + border-radius: 8px; + overflow: hidden; + padding: 4px 8px; + box-shadow: + 0 3px 6px -4px rgba(0, 0, 0, 0.12), + 0 6px 16px 0 rgba(0, 0, 0, 0.08), + 0 9px 28px 8px rgba(0, 0, 0, 0.05); + background-color: #fff; + width: 150px; + + .slash-title { + font-size: 12px; + color: var(--yakit-helper-text-color); + user-select: none; + padding: 4px; + } + .slash-item { + display: flex; + align-items: center; + font-weight: 500; + gap: 12px; + color: var(--yakit-header-color); + cursor: pointer; + padding: 4px; + border-radius: 4px; + line-height: 16px; + &:hover { + background-color: var(--Colors-Use-Neutral-Bg); + } + svg { + width: 20px; + height: 20px; + } + } +} + +.slash[data-show="false"] { + display: none; +} diff --git a/src/compoments/MilkdownEditor/Slash/Slash.tsx b/src/compoments/MilkdownEditor/Slash/Slash.tsx new file mode 100644 index 00000000..edf21816 --- /dev/null +++ b/src/compoments/MilkdownEditor/Slash/Slash.tsx @@ -0,0 +1,238 @@ +import {Ctx} from "@milkdown/kit/ctx" +import {slashFactory, SlashProvider} from "@milkdown/kit/plugin/slash" +import {useInstance} from "@milkdown/react" +import {usePluginViewContext} from "@prosemirror-adapter/react" +import React, {useCallback, useEffect, useRef} from "react" +import {useCreation, useDebounceEffect, useDebounceFn, useMemoizedFn} from "ahooks" +import {HttpUploadImgBaseRequest} from "@/apiUtils/http" +import {InitEditorHooksLocalProps} from "../utils/initEditor" +import { + MilkdownMenuType, + MilkdownMenuKeyEnum, + createMilkdownMenuListByKey, + baseSlashKey, + onlineCommonSlashKey, + localCommonSlashKey +} from "../constants" +import styles from "./Slash.module.scss" +import {EditorView} from "@milkdown/kit/prose/view" +import {EditorState} from "@milkdown/kit/prose/state" +import { + createBlankHeading1, + createBlankHeading2, + createBlankHeading3, + createBlankOrderedList, + createBlankUnorderedList, + createBlankTask, + createBlankCodeBlock, + createBlankQuote, + createBlankHighLight, + uploadFileInMilkdown, + createDivider, + createBlankText +} from "../utils/utils" +import {useStore} from "@/store" +import type {VirtualElement} from "@floating-ui/dom" +import {computePosition, flip, offset} from "@floating-ui/dom" +import {posToDOMRect} from "@milkdown/prose" + +export const slash = slashFactory("Commands") + +interface SlashViewProps { + type: HttpUploadImgBaseRequest["type"] + notepadHash?: string + localProps?: InitEditorHooksLocalProps +} + +export const SlashView: React.FC = (props) => { + const {type, notepadHash, localProps} = props + const ref = useRef(null) + const slashProvider = useRef() + const initializedRef = useRef(false) + + const userInfo = useStore((s) => s.userInfo) + + const {view, prevState} = usePluginViewContext() + const [loading, get] = useInstance() + const action = useCallback( + (fn: (ctx: Ctx) => void) => { + if (loading) return + get().action(fn) + }, + [loading] + ) + + useEffect(() => { + const div = ref.current + if (loading || !div) { + return + } + slashProvider.current = new SlashProvider({ + content: div, + shouldShow: (view: EditorView, prevState?: EditorState | undefined) => { + return onShouldShow(view) + } + }) + + return () => { + slashProvider.current?.destroy() + } + }, [loading]) + + useDebounceEffect( + () => { + if (loading || !slashProvider.current) return + slashUpdate(view, prevState) + }, + [loading, view, prevState], + {wait: 200, leading: true} + ) + + /**根据编辑器选中得变化更新 slash 得位置 */ + const slashUpdate = useDebounceFn( + (view: EditorView, prevState?: EditorState) => { + if (!ref.current) return + const {state, composing} = view + const {selection, doc} = state + const {ranges} = selection + const from = Math.min(...ranges.map((range) => range.$from.pos)) + const to = Math.max(...ranges.map((range) => range.$to.pos)) + const isSame = prevState && prevState.doc.eq(doc) && prevState.selection.eq(selection) + + if (!initializedRef.current) { + view.dom.parentElement?.appendChild(ref.current) + initializedRef.current = true + } + + if (composing || isSame) return + if (!onShouldShow(view)) { + slashProvider.current?.hide() + return + } + const virtualEl: VirtualElement = { + getBoundingClientRect: () => posToDOMRect(view, from, to) + } + computePosition(virtualEl, ref.current, { + placement: "top", + middleware: [flip()] + }).then(({x, y}) => { + if (!ref.current) return + let styleObj: {left: null | string; top: null | string} = { + left: `12px`, // 目前只有空段落才会显示,left为12; + top: `${y > 0 ? y : 0}px` + } + Object.assign(ref.current.style, styleObj) + }) + + slashProvider.current?.show() + }, + {wait: 200} + ).run + /**判断 slash 是否显示 */ + const onShouldShow = (view: EditorView): boolean => { + if (!ref.current) { + return false + } + const {selection} = view.state + const {$from} = selection + const node = $from.node() + const parentNode = $from.node(-1) + + // 段落开头并含有/才显示 + if ( + parentNode&&parentNode.type.name === "doc" && + node&&node.type.name === "paragraph" && + node.content.size === 1 && + node.textContent === "/" + ) { + return true + } + return false + } + + const slashList = useCreation(() => { + if (!!localProps?.local) { + return { + 基础: createMilkdownMenuListByKey(baseSlashKey), + 常用: createMilkdownMenuListByKey(localCommonSlashKey) + } + } else { + return { + 基础: createMilkdownMenuListByKey(baseSlashKey), + 常用: createMilkdownMenuListByKey(onlineCommonSlashKey) + } + } + }, [localProps?.local]) + + const onSelect = useMemoizedFn((key: MilkdownMenuType) => { + switch (key) { + case MilkdownMenuKeyEnum.Text: + createBlankText(action, view) + break + case MilkdownMenuKeyEnum.Heading1: + createBlankHeading1(action, view) + break + case MilkdownMenuKeyEnum.Heading2: + createBlankHeading2(action, view) + break + case MilkdownMenuKeyEnum.Heading3: + createBlankHeading3(action, view) + break + case MilkdownMenuKeyEnum.OrderedList: + createBlankOrderedList(action, view) + break + case MilkdownMenuKeyEnum.UnorderedList: + createBlankUnorderedList(action, view) + break + case MilkdownMenuKeyEnum.CodeBlock: + createBlankCodeBlock(action, view) + break + case MilkdownMenuKeyEnum.Quote: + createBlankQuote(action, view) + break + case MilkdownMenuKeyEnum.Divider: + createDivider(action, view) + break + case MilkdownMenuKeyEnum.Task: + createBlankTask(action, view) + break + case MilkdownMenuKeyEnum.HighLight: + createBlankHighLight(action, view) + break + case MilkdownMenuKeyEnum.File: + const {dispatch, state} = view + const {tr, selection} = state + const {from} = selection + dispatch(tr.deleteRange(from - 1, from)) + uploadFileInMilkdown(action, { + type, + notepadHash: notepadHash || "", + userId: userInfo.user_id || 0 + }) + break + default: + break + } + if (key !== MilkdownMenuKeyEnum.CodeBlock) { + view.focus() + } + slashProvider.current?.hide() + }) + return ( + + ) +} diff --git a/src/compoments/MilkdownEditor/Tooltip/Tooltip.module.scss b/src/compoments/MilkdownEditor/Tooltip/Tooltip.module.scss new file mode 100644 index 00000000..473c4ad2 --- /dev/null +++ b/src/compoments/MilkdownEditor/Tooltip/Tooltip.module.scss @@ -0,0 +1,164 @@ +.tooltip { + position: absolute; + + display: flex; + // padding: 12px; + align-items: center; + gap: 8px; + height: 52px; + + border-radius: 8px; + border: 1px solid var(--Colors-Use-Neutral-Border); + background: var(--Colors-Use-Basic-Background); + + /* 投影-大 */ + box-shadow: 0px 6px 20px 0px var(--Colors-Use-Basic-Shadow); + + svg { + width: 20px; + height: 20px; + color: var(--Colors-Use-Neutral-Text-1-Title); + } + .tooltip-t-wrapper { + padding: 12px 0 12px 12px; + } + .tooltip-light-wrapper { + padding: 12px 0; + } + .tooltip-t { + display: flex; + align-items: center; + gap: 4px; + padding: 4px; + cursor: pointer; + border-radius: 4px; + height: 28px; + &:hover { + background-color: var(--Colors-Use-Neutral-Bg); + } + + .t-icon { + svg { + width: 14px; + height: 14px; + } + } + } + + .tooltip-tool { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + border-radius: 0 4px 4px 0; + height: 28px; + margin: 12px; + margin-left: 0; + } +} +.tooltip[data-show="false"] { + display: none; +} +.tooltip-divider { + width: 1px; + height: 28px; + background-color: var(--Colors-Use-Neutral-Border); +} +.tooltip-divider-horizontal { + width: 100%; + height: 1px; + margin: 8px 0px; + background-color: var(--Colors-Use-Neutral-Border); +} +.tooltip-popover { + padding-top: 0; + :global { + .ant-popover-arrow { + display: none; + } + .ant-popover-inner { + /* 投影-大 */ + box-shadow: 0px 6px 20px 0px var(--Colors-Use-Basic-Shadow); + border: 1px solid var(--Colors-Use-Neutral-Border); + border-radius: 8px; + } + } + .tooltip-popover-content { + display: flex; + width: 160; + flex-direction: column; + align-items: flex-start; + .tooltip-list-item { + display: flex; + padding: 6px 8px; + align-items: center; + gap: 12px; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-weight: 400; + font-size: 14px; + border-radius: 4px; + width: 100%; + cursor: pointer; + &:hover { + background-color: var(--Colors-Use-Neutral-Bg); + } + .icon { + width: 20px; + height: 20px; + svg { + width: 20px; + height: 20px; + } + } + } + } + .tooltip-light-popover-content { + display: flex; + align-items: center; + gap: 8px; + .tooltip-type-item { + width: 20px; + height: 20px; + border-radius: 4px; + cursor: pointer; + } + .item-remove { + background-color: #fff; + background: linear-gradient(45deg, transparent 48.5%, #f00 56.5%, #f00 36.5%, transparent 51.5%); + border: 1px solid var(--Colors-Use-Neutral-Border); + } + .item-note { + background-color: var(--Colors-Use-Neutral-Border); + } + + .item-success { + background-color: var(--Colors-Use-Success-Primary); + } + .item-danger { + background-color: var(--Colors-Use-Error-Primary); + } + .item-warning { + background-color: var(--Colors-Use-Warning-Primary); + } + } +} + + +.tooltip-icon { + display: flex; + padding: 4px; + align-items: center; + gap: 4px; + border-radius: 4px; + height: 28px; + width: 28px; + cursor: pointer; + svg { + width: 20px; + height: 20px; + color: var(--Colors-Use-Neutral-Text-1-Title); + } + &:hover { + background-color: var(--Colors-Use-Neutral-Bg); + } +} \ No newline at end of file diff --git a/src/compoments/MilkdownEditor/Tooltip/Tooltip.tsx b/src/compoments/MilkdownEditor/Tooltip/Tooltip.tsx new file mode 100644 index 00000000..21f976b2 --- /dev/null +++ b/src/compoments/MilkdownEditor/Tooltip/Tooltip.tsx @@ -0,0 +1,491 @@ +import type { Ctx } from '@milkdown/kit/ctx' +import { tooltipFactory, TooltipProvider } from '@milkdown/kit/plugin/tooltip' +import { + createCodeBlockCommand, + paragraphSchema, + toggleEmphasisCommand, + toggleInlineCodeCommand, + toggleStrongCommand, + wrapInBlockquoteCommand, + wrapInBulletListCommand, + wrapInHeadingCommand, + wrapInOrderedListCommand, +} from '@milkdown/kit/preset/commonmark' +import { useInstance } from '@milkdown/react' +import { usePluginViewContext } from '@prosemirror-adapter/react' +import type { ReactNode } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' +import { callCommand } from '@milkdown/kit/utils' +import { useCreation, useDebounceEffect, useDebounceFn, useMemoizedFn } from 'ahooks' +import { IconBold, IconCode2, IconItalic, IconStrikethrough, IconType, IconUnderline } from '../icon/icon' +import styles from './Tooltip.module.scss' +import React from 'react' +import { OutlineChevrondownIcon, OutlineChevronupIcon, OutlineLightbulbIcon } from '@/assets/icon/outline' +import { YakitPopover } from '@/compoments/yakitUI/YakitPopover/YakitPopover' +import { toggleStrikethroughCommand } from '@milkdown/kit/preset/gfm' +import classNames from 'classnames' +import { alterCommand, alterToParagraphCommand } from '../utils/alertPlugin' +import { underlineCommand } from '../utils/underline' +import { commentCommand } from '../utils/commentPlugin' +import { setWrapInBlockType } from '../utils/utils' +import { Tooltip } from 'antd' +import type { MilkdownBaseUtilProps, TooltipListProps } from '../constants' +import { MilkdownMenu, MilkdownMenuKeyEnum, tooltipKey } from '../constants' +import { convertToListBullet, listToParagraphCommand } from '../utils/listPlugin' +import { headingToParagraphCommand, listToHeadingCommand } from '../utils/headingPlugin' +import { listToCodeCommand } from '../utils/codePlugin' +import { fileCustomSchema } from '../utils/uploadPlugin' +import type { EditorView } from '@milkdown/prose/view' +import { TextSelection, type EditorState } from '@milkdown/kit/prose/state' +import type { VirtualElement } from '@floating-ui/dom' +import { computePosition, flip, offset } from '@floating-ui/dom' +import { posToDOMRect } from '@milkdown/prose' +import { mentionCustomSchema } from '../utils/mentionPlugin' + +export const tooltip = tooltipFactory('Text') + +const highlight = [ + { + label: 'remove', + description: '清除高亮', + }, + { + label: 'note', + description: '灰色: :::note空格', + }, + { + label: 'success', + description: '绿色: :::success空格', + }, + { + label: 'danger', + description: '红色: :::danger空格', + }, + { + label: 'warning', + description: '黄色: :::warning空格', + }, +] +const tooltipWidth = 324 + +interface TooltipViewProps {} +export const TooltipView: React.FC = () => { + const [visibleText, setVisibleText] = useState(false) + const [visibleLight, setVisibleLight] = useState(false) + + const ref = useRef(null) + const tooltipProvider = useRef() + const initializedRef = useRef(false) + + const { view, prevState } = usePluginViewContext() + const [loading, get] = useInstance() + const action = useCallback( + (fn: (ctx: Ctx) => void) => { + if (loading) return + get().action(fn) + }, + [loading], + ) + + const tooltipTextList: TooltipListProps[] = useCreation(() => { + return tooltipKey.map((ele, index) => { + if (ele === 'key-divider') { + return { + key: `${index}-divider`, + label: 'divider', + } + } else { + return { + ...MilkdownMenu[ele], + key: ele, + } + } + }) + }, []) + + useEffect(() => { + if (loading) { + return + } + if (ref.current) { + tooltipProvider.current = new TooltipProvider({ + content: ref.current, + }) + } + }, [loading]) + useEffect(() => { + return () => { + // 单独的Effect中卸载,避免报错 + tooltipProvider.current?.destroy() + } + }, []) + + useDebounceEffect( + () => { + if (loading || !tooltipProvider.current) return + if (isSelectFile() || isSelectImg() || isSelectMention()) { + tooltipProvider.current?.hide() + return + } + tooltipUpdate(view, prevState) + }, + [loading, view, prevState], + { wait: 200, leading: true }, + ) + /** 根据编辑器选中得变化更新 tooltip 得位置 */ + const tooltipUpdate = useDebounceFn( + (view: EditorView, prevState?: EditorState) => { + if (!ref.current) return + const { state, composing } = view + const { selection, doc } = state + const { ranges } = selection + const from = Math.min(...ranges.map((range) => range.$from.pos)) + const to = Math.max(...ranges.map((range) => range.$to.pos)) + const isSame = prevState && prevState.doc.eq(doc) && prevState.selection.eq(selection) + + if (!initializedRef.current) { + view.dom.parentElement?.appendChild(ref.current) + initializedRef.current = true + } + + if (composing || isSame) return + if (!onShouldShow(view)) { + tooltipProvider.current?.hide() + return + } + const virtualEl: VirtualElement = { + getBoundingClientRect: () => posToDOMRect(view, from, to), + } + computePosition(virtualEl, ref.current, { + placement: 'top', + middleware: [flip()], + }).then(({ x, y }) => { + if (!ref.current) return + let styleObj: { left: null | string; top: null | string; right: null | string } = { + left: null, + top: `${y}px`, + right: null, + } + const { width } = view.dom.getBoundingClientRect() + const rightBoundary = width - x + if (rightBoundary < tooltipWidth) { + styleObj = { + ...styleObj, + right: '0px', + } + } else { + styleObj = { + ...styleObj, + left: `${x > 0 ? x : 0}px`, + } + } + Object.assign(ref.current.style, styleObj) + }) + + tooltipProvider.current?.show() + }, + { wait: 200 }, + ).run + /** 判断 tooltip 是否显示 */ + const onShouldShow = (view: EditorView): boolean => { + if (!ref.current) { + return false + } + const { doc, selection } = view.state + const { empty, from, to } = selection + + const isEmptyTextBlock = !doc.textBetween(from, to).length && view.state.selection instanceof TextSelection + + const isTooltipChildren = ref.current.contains(document.activeElement) + + const notHasFocus = !view.hasFocus() && !isTooltipChildren + + const isReadonly = !view.editable + + if (notHasFocus || empty || isEmptyTextBlock || isReadonly) return false + + return true + } + /** 判断选中节点是否为文件 */ + const isSelectFile = useMemoizedFn(() => { + const selectedNode = getSelectNode() + return selectedNode?.type.name === fileCustomSchema.node.id + }) + /** 判断选中节点是否为 图片 */ + const isSelectImg = useMemoizedFn(() => { + const { state } = view + const selectedNode = getSelectNode() + switch (selectedNode?.type.name) { + case state.schema.nodes.image?.name: + case state.schema.nodes['image-block']?.name: + return true + default: + return false + } + }) + /** 判断选中节点是否为 @/提及 */ + const isSelectMention = useMemoizedFn(() => { + const selectedNode = getSelectNode() + return selectedNode?.type.name === mentionCustomSchema.node.id + }) + /** + * 获取选中的节点 + */ + const getSelectNode = useMemoizedFn(() => { + const { from } = view.state.selection + const selectedNode = view.state.doc.nodeAt(from) + return selectedNode + }) + + const onBold = useMemoizedFn((e) => { + e.preventDefault() + action(callCommand(toggleStrongCommand.key)) + }) + + const onEmphasis = useMemoizedFn((e) => { + e.preventDefault() + action(callCommand(toggleEmphasisCommand.key)) + }) + const onStrikethrough = useMemoizedFn((e) => { + e.preventDefault() + action(callCommand(toggleStrikethroughCommand.key)) + }) + const onUnderline = useMemoizedFn((e) => { + e.preventDefault() + action(callCommand(underlineCommand.key)) + }) + const onAddCode = useMemoizedFn((e) => { + e.preventDefault() + action(callCommand(toggleInlineCodeCommand.key)) + }) + const onAddComment = useMemoizedFn((e) => { + e.preventDefault() + action(callCommand(commentCommand.key, '111')) + }) + const onText = useMemoizedFn(({ key }) => { + switch (key) { + case MilkdownMenuKeyEnum.Text: + convertToParagraph() + break + case MilkdownMenuKeyEnum.Heading1: + convertToHeading(1) + break + case MilkdownMenuKeyEnum.Heading2: + convertToHeading(2) + break + case MilkdownMenuKeyEnum.Heading3: + convertToHeading(3) + break + case MilkdownMenuKeyEnum.OrderedList: + action(callCommand(wrapInOrderedListCommand.key)) + break + case MilkdownMenuKeyEnum.UnorderedList: + action(callCommand(wrapInBulletListCommand.key)) + break + case MilkdownMenuKeyEnum.Task: + action(callCommand(convertToListBullet.key)) + break + case MilkdownMenuKeyEnum.CodeBlock: + convertToCode() + break + case MilkdownMenuKeyEnum.Quote: + action(callCommand(wrapInBlockquoteCommand.key)) + break + default: + break + } + tooltipProvider.current?.hide() + setVisibleText(false) + }) + /** + * 获取选中内容的父节点类型 + */ + const getParentNodeTypeBySelection = useMemoizedFn(() => { + try { + const { state } = view + const { selection } = state + const { $from } = selection + // 获取父节点类型 + const parentNode = $from.node(-1) // 使用 -1 获取上一级节点 + return parentNode.type.name + } catch (error) { + return '' + } + }) + /** 选中的内容是否是list */ + const isListTypeSelection = useMemoizedFn(() => { + try { + const { state } = view + return getParentNodeTypeBySelection() === state.schema.nodes.list_item.name + } catch (error) { + return false + } + }) + /** 选中的内容是否是heading */ + const isHeadingSelection = useMemoizedFn(() => { + try { + const { state } = view + const { selection } = state + const { $from } = selection + /** + * 获取父节点类型 + * 选中标题-1获取的父节点是doc,不适用 + * 选中有序列表用$from.depth获取的父节点是paragraph不适用 + */ + const parentNode = $from.node($from.depth) + return parentNode.type.name === state.schema.nodes.heading.name + } catch (error) { + return false + } + }) + /** + * 转为正文,目前除了常见的,还支持选中内容类型为list + */ + const convertToParagraph = useMemoizedFn(() => { + try { + const { dispatch, state } = view + if (isListTypeSelection()) { + action(callCommand(listToParagraphCommand.key)) + } + if (isHeadingSelection()) { + action(callCommand(headingToParagraphCommand.key)) + } else { + action((ctx) => { + const command = setWrapInBlockType(paragraphSchema.type(ctx)) + command(state, dispatch) + }) + } + } catch (error) {} + }) + /** + * 转为标题,目前除了常见的,还支持选中内容类型为list + * @param level 标题级别 + */ + const convertToHeading = useMemoizedFn((level: number) => { + try { + if (isListTypeSelection()) { + action(callCommand(listToHeadingCommand.key, level)) + } else { + action(callCommand(wrapInHeadingCommand.key, level)) + } + } catch (error) {} + }) + /** + * 转为代码块,目前除了常见的,还支持选中内容类型为list + */ + const convertToCode = useMemoizedFn(() => { + try { + if (isListTypeSelection()) { + action(callCommand(listToCodeCommand.key)) + } else { + action(callCommand(createCodeBlockCommand.key)) + } + } catch (error) {} + }) + /** 高亮操作 */ + const onLight = useMemoizedFn((type: string) => { + switch (type) { + case 'remove': + action(callCommand(alterToParagraphCommand.key)) + break + default: + action(callCommand(alterCommand.key, type)) + break + } + setVisibleLight(false) + }) + return ( +
    + + {tooltipTextList.map((ele) => { + if (ele.label === 'divider') { + return
    + } + const item = ele as MilkdownBaseUtilProps + return ( + +
    onText(item)}> + {item.icon} + {item.label} +
    +
    + ) + })} +
    + } + > +
    +
    + + {visibleText ? ( + + ) : ( + + )} +
    +
    +
    +
    + + {highlight.map((item) => ( + +
    onLight(item.label)} + /> + + ))} +
    + } + > +
    +
    + + {visibleLight ? ( + + ) : ( + + )} +
    +
    +
    +
    +
    + } onClick={onBold} /> + } onClick={onStrikethrough} /> + } onClick={onEmphasis} /> + } onClick={onUnderline} /> + } onClick={onAddCode} /> + {/* } onClick={onAddComment} /> */} +
    +
    + ) +} + +interface TooltipIconProps { + title?: ReactNode + icon: ReactNode + onClick: React.MouseEventHandler +} +export const TooltipIcon: React.FC = React.memo((props) => { + const { icon, onClick, title } = props + const node = ( +
    + {icon} +
    + ) + return title ? {node} : node +}) diff --git a/src/compoments/MilkdownEditor/WebsocketProvider/WebsocketProvider.ts b/src/compoments/MilkdownEditor/WebsocketProvider/WebsocketProvider.ts new file mode 100644 index 00000000..8a96df54 --- /dev/null +++ b/src/compoments/MilkdownEditor/WebsocketProvider/WebsocketProvider.ts @@ -0,0 +1,591 @@ +/** + * @module provider/websocket + */ + +/* eslint-env browser */ + +import * as Y from "yjs" // eslint-disable-line +import * as bc from "lib0/broadcastchannel" +import * as time from "lib0/time" +import * as encoding from "lib0/encoding" +import * as decoding from "lib0/decoding" +import * as syncProtocol from "y-protocols/sync" +import * as authProtocol from "y-protocols/auth" +import * as awarenessProtocol from "y-protocols/awareness" +import * as math from "lib0/math" +import * as url from "lib0/url" +import * as env from "lib0/environment" +import { + MessageHandlersProps, + NotepadWsRequest, + ObservableEvents, + WebsocketProviderAwarenessUpdateHandler, + WebsocketProviderBcSubscriber, + WebsocketProviderExitHandler, + WebsocketProviderGetSendData, + WebsocketProviderOptions, + WebsocketProviderUpdateHandler +} from "./WebsocketProviderType" +import { + messageAwareness, + messageSync, + messageQueryAwareness, + messageAuth, + notepadActions, + notepadSaveStatus +} from "./constants" +import {ObservableV2} from "lib0/observable" +import { JSONParseLog } from "@/utils/tool" + +/** + * encoder, decoder, provider, emitSynced, messageType + * @type {Array} + */ +const messageHandlers: MessageHandlersProps[] = [] + +messageHandlers[messageSync] = (encoder, decoder, provider, emitSynced, _messageType) => { + encoding.writeVarUint(encoder, messageSync) + const syncMessageType = syncProtocol.readSyncMessage(decoder, encoder, provider.doc, provider) + if (emitSynced && syncMessageType === syncProtocol.messageYjsSyncStep2 && !provider.synced) { + provider.synced = true + } +} + +messageHandlers[messageQueryAwareness] = (encoder, _decoder, provider, _emitSynced, _messageType) => { + encoding.writeVarUint(encoder, messageAwareness) + encoding.writeVarUint8Array( + encoder, + awarenessProtocol.encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())) + ) +} + +messageHandlers[messageAwareness] = (_encoder, decoder, provider, _emitSynced, _messageType) => { + awarenessProtocol.applyAwarenessUpdate(provider.awareness, decoding.readVarUint8Array(decoder), provider) +} + +messageHandlers[messageAuth] = (_encoder, decoder, provider, _emitSynced, _messageType) => { + authProtocol.readAuthMessage(decoder, provider.doc, (_ydoc, reason) => permissionDeniedHandler(provider, reason)) +} + +// @todo - this should depend on awareness.outdatedTime +const messageReconnectTimeout = 30000 + +/** + * @param {WebsocketProvider} provider + * @param {string} reason + */ +const permissionDeniedHandler = (provider: WebsocketProvider, reason: string) => + console.warn(`Permission denied to access ${provider.url}.\n${reason}`) + +/** + * @param {WebsocketProvider} provider + * @param {Uint8Array} buf + * @param {boolean} emitSynced + * @return {encoding.Encoder} + */ +const readMessage = (provider: WebsocketProvider, buf: Uint8Array, emitSynced: boolean): encoding.Encoder => { + const decoder = decoding.createDecoder(buf) + const encoder = encoding.createEncoder() + const messageType = decoding.readVarUint(decoder) + const messageHandler = provider.messageHandlers[messageType] + if (/** @type {any} */ messageHandler) { + messageHandler(encoder, decoder, provider, emitSynced, messageType) + } else { + console.error("Unable to compute message") + } + return encoder +} + +/** + * @param {WebsocketProvider} provider + */ +const setupWS = (provider: WebsocketProvider) => { + if (provider.shouldConnect && provider.ws === null) { + const websocket: WebSocket = new provider._WS(provider.url, provider.protocols) + websocket.binaryType = "arraybuffer" + provider.ws = websocket + provider.wsconnecting = true + provider.wsconnected = false + provider.synced = false + provider.onlineUserCount = 0 + + websocket.onmessage = (event) => { + try { + const bytes = Buffer.from(event.data).toString() + const data: NotepadWsRequest = JSONParseLog(bytes,{page:"WebsocketProvider", fun:"onMessage"}) + const yjsParams = Buffer.from(data.yjsParams, "base64") + provider.wsLastMessageReceived = time.getUnixTime() + const encoder = readMessage(provider, yjsParams, true) + + if (encoding.length(encoder) > 1) { + const messageUint8Array = encoding.toUint8Array(encoder) + const value = provider?.getSendData({buf: messageUint8Array, docType: notepadActions.edit}) + websocket.send(value) + } + if (!!data.params.userCount && data.params.docType === notepadActions.join) { + // 目前加入类型的消息会修改在线人数,用来做连接文档的初始化内容 + provider.onlineUserCount = data.params.userCount + } + if (data?.params?.saveStatus) { + provider.emit("saveStatus", [ + { + saveStatus: data.params.saveStatus + } + ]) + } + } catch (error) {} + } + websocket.onerror = (event) => { + provider.emit("connection-error", [event, provider]) + } + websocket.onclose = (event) => { + closeWebsocketConnection(provider, websocket, event) + } + websocket.onopen = () => { + provider.wsLastMessageReceived = time.getUnixTime() + provider.wsconnecting = false + provider.wsconnected = true + provider.wsUnsuccessfulReconnects = 0 + provider.emit("status", [ + { + status: "connected" + } + ]) + // always send sync step 1 when connected + const encoder = encoding.createEncoder() + encoding.writeVarUint(encoder, messageSync) + syncProtocol.writeSyncStep1(encoder, provider.doc) + const encoderUint8Array = encoding.toUint8Array(encoder) + websocket.send(provider?.getSendData({buf: encoderUint8Array, docType: notepadActions.join})) + + // broadcast local awareness state + if (provider.awareness.getLocalState() !== null) { + const encoderAwarenessState = encoding.createEncoder() + encoding.writeVarUint(encoderAwarenessState, messageAwareness) + encoding.writeVarUint8Array( + encoderAwarenessState, + awarenessProtocol.encodeAwarenessUpdate(provider.awareness, [provider.doc.clientID]) + ) + const encoderAwarenessStateUint8Array = encoding.toUint8Array(encoderAwarenessState) + websocket.send( + provider?.getSendData({buf: encoderAwarenessStateUint8Array, docType: notepadActions.edit}) + ) + } + } + provider.emit("status", [ + { + status: "connecting" + } + ]) + } +} + +/** + * Outsource this function so that a new websocket connection is created immediately. + * I suspect that the `ws.onclose` event is not always fired if there are network issues. + * + * @param {WebsocketProvider} provider + * @param {WebSocket} ws + * @param {CloseEvent} event + */ +const closeWebsocketConnection = (provider: WebsocketProvider, ws: WebSocket, event?: CloseEvent) => { + if (ws === provider.ws) { + if (event) provider.emit("connection-close", [event, provider]) + provider.ws = null + provider.wsconnecting = false + if (provider.wsconnected) { + provider.wsconnected = false + provider.synced = false + provider.onlineUserCount = 0 + // update awareness (all users except local left) + awarenessProtocol.removeAwarenessStates( + provider.awareness, + //@ts-ignore + Array.from(provider.awareness.getStates().keys()).filter((client) => client !== provider.doc.clientID), + provider + ) + provider.emit("status", [ + { + status: "disconnected" + } + ]) + } else { + provider.wsUnsuccessfulReconnects++ + } + const code = event?.code || 0 + switch (code) { + // event为undefined; + // 1.发送notepadActions.leave,主动断开,不重连,等待后端返回200后关闭ws + case 0: + break + case 401: + case 403: + case 404: + case 200: // 发送notepadActions.leave,后端会给200,正常关闭 + case 209: + case 500: + ws.close() + break + default: + // Start with no reconnect timeout and increase timeout by + // using exponential backoff starting with 100ms + setTimeout( + setupWS, + math.min(math.pow(2, provider.wsUnsuccessfulReconnects) * 100, provider.maxBackoffTime), + provider + ) + break + } + } +} + +/** + * @param {WebsocketProvider} provider + * @param {ArrayBuffer} buf + */ +const broadcastMessage = (provider, buf) => { + const ws = provider.ws + if (provider.wsconnected && ws && ws.readyState === ws.OPEN) { + ws.send(provider?.getSendData({buf, docType: notepadActions.edit})) + } + if (provider.bcconnected) { + bc.publish(provider.bcChannel, buf, provider) + } +} + +/** + * Websocket Provider for Yjs. Creates a websocket connection to sync the shared document. + * The document name is attached to the provided url. I.e. the following example + * creates a websocket connection to http://localhost:1234/my-document-name + * + * @example + * import * as Y from 'yjs' + * import { WebsocketProvider } from './WebsocketProvider' + * const doc = new Y.Doc() + * const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc) + * + * @extends {ObservableV2} + */ +export class WebsocketProvider extends ObservableV2 { + /** + * @param {string} serverUrl + * @param {string} room + * @param {Y.Doc} doc + * @param {object} opts + * @param {boolean} [opts.connect] + * @param {awarenessProtocol.Awareness} [opts.awareness] + * @param {Object} [opts.params] specify url parameters + * @param {Array} [opts.protocols] specify websocket protocols + * @param {typeof WebSocket} [opts.WebSocketPolyfill] Optionall provide a WebSocket polyfill + * @param {number} [opts.resyncInterval] Request server state every `resyncInterval` milliseconds + * @param {number} [opts.maxBackoffTime] Maximum amount of time to wait before trying to reconnect (we try to reconnect using exponential backoff) + * @param {boolean} [opts.disableBc] Disable cross-tab BroadcastChannel communication + */ + + public awareness: awarenessProtocol.Awareness + public messageHandlers: MessageHandlersProps[] + /** + * @type {WebSocket?} + */ + public ws: WebSocket | null + public doc: Y.Doc + public wsUnsuccessfulReconnects: number + public wsconnected: boolean + public wsconnecting: boolean + /** + * @type {boolean} + */ + public _synced: boolean + public wsLastMessageReceived: number + public _onlineUserCount: number + + public serverUrl: string + public bcChannel: string + public maxBackoffTime: number + /** + * The specified url parameters. This can be safely updated. The changed parameters will be used + * when a new connection is established. + * @type {Object} + */ + public params: {[key: string]: string} + /**send */ + public data?: NotepadWsRequest + public protocols: string[] + public _WS: typeof WebSocket + + public bcconnected: boolean + public disableBc: boolean + + /** + * Whether to connect to other peers or not + * @type {boolean} + */ + public shouldConnect: boolean + + public getSendData: WebsocketProviderGetSendData + + /** + * @type {number} + */ + private _resyncInterval: NodeJS.Timeout | number + private _bcSubscriber: WebsocketProviderBcSubscriber + private _updateHandler: WebsocketProviderUpdateHandler + private _awarenessUpdateHandler: WebsocketProviderAwarenessUpdateHandler + private _exitHandler: WebsocketProviderExitHandler + private _checkInterval: NodeJS.Timeout | number + + constructor(serverUrl: string, room: string, doc: Y.Doc, options: WebsocketProviderOptions = {}) { + super() + // Destructure options and assign default values + const { + connect = true, + awareness = new awarenessProtocol.Awareness(doc), + params = {}, + protocols = [], + WebSocketPolyfill = WebSocket, + resyncInterval = -1, + maxBackoffTime = 2500, + disableBc = false, + data + } = options + // ensure that url is always ends with / + while (serverUrl[serverUrl.length - 1] === "/") { + serverUrl = serverUrl.slice(0, serverUrl.length - 1) + } + this.serverUrl = serverUrl + this.bcChannel = serverUrl + "/" + room + this.maxBackoffTime = maxBackoffTime + + this.params = params + this.data = data + this.protocols = protocols + this.doc = doc + this._WS = WebSocketPolyfill + this.awareness = awareness + this.wsconnected = false + this.wsconnecting = false + this.bcconnected = false + this.disableBc = disableBc + this.wsUnsuccessfulReconnects = 0 + this.messageHandlers = messageHandlers.slice() + + this._synced = false + this._onlineUserCount = 0 + + this.ws = null + this.wsLastMessageReceived = 0 + + this.shouldConnect = connect + + this._resyncInterval = 0 + if (resyncInterval > 0) { + this._resyncInterval = setInterval(() => { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + // resend sync step 1 + const encoder = encoding.createEncoder() + encoding.writeVarUint(encoder, messageSync) + syncProtocol.writeSyncStep1(encoder, doc) + const resyncUint8Array = encoding.toUint8Array(encoder) + this.ws.send(this.getSendData({buf: resyncUint8Array, docType: notepadActions.join})) + } + }, resyncInterval) + } + /** + * @description content一直为空,只发送操作,后端不会存历史 + * @param {ArrayBuffer|undefined} buf + * @param {NotepadActionType} docType + */ + this.getSendData = (sendData) => { + if (!this.data) return Buffer.from("") + const {messageType, token, params, notepadHash} = this.data + const {buf, docType} = sendData + try { + const value: NotepadWsRequest = { + messageType, + notepadHash, + params: { + content: "", + title: "", + docType, + saveStatus: notepadSaveStatus.saveProgress, + userName: params.userName + }, + yjsParams: buf ? Buffer.from(buf).toString("base64") : "", + token + } + const jsonString = JSON.stringify(value) + const finalArrayBuffer = Buffer.from(jsonString) + return finalArrayBuffer + } catch (error) { + return Buffer.from("") + } + } + + /** + * @param {ArrayBuffer} data + * @param {any} origin + */ + this._bcSubscriber = (data: ArrayBuffer, origin: any) => { + if (origin !== this) { + const encoder = readMessage(this, new Uint8Array(data), false) + if (encoding.length(encoder) > 1) { + bc.publish(this.bcChannel, encoding.toUint8Array(encoder), this) + } + } + } + /** + * Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel) + * @param {Uint8Array} update + * @param {any} origin + */ + this._updateHandler = (update: Uint8Array, origin: any, ydoc: Y.Doc, tr: Y.Transaction) => { + if (origin !== this) { + const encoder = encoding.createEncoder() + encoding.writeVarUint(encoder, messageSync) + syncProtocol.writeUpdate(encoder, update) + broadcastMessage(this, encoding.toUint8Array(encoder)) + } + } + this.doc.on("update", this._updateHandler) + /** + * @param {added: number[]; updated: number[]; removed: number[]} changed + * @param {any} _origin + */ + this._awarenessUpdateHandler = ({added, updated, removed}, _origin) => { + const changedClients = added.concat(updated).concat(removed) + const encoder = encoding.createEncoder() + encoding.writeVarUint(encoder, messageAwareness) + encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(awareness, changedClients)) + broadcastMessage(this, encoding.toUint8Array(encoder)) + } + this._exitHandler = () => { + awarenessProtocol.removeAwarenessStates(this.awareness, [doc.clientID], "app closed") + } + if (env.isNode && typeof process !== "undefined") { + process.on("exit", this._exitHandler) + } + awareness.on("update", this._awarenessUpdateHandler) + this._checkInterval = setInterval(() => { + if (this.wsconnected && messageReconnectTimeout < time.getUnixTime() - this.wsLastMessageReceived) { + // no message received in a long time - not even your own awareness + // updates (which are updated every 15 seconds) + if (this.ws) closeWebsocketConnection(this, /** @type {WebSocket} */ this.ws) + } + }, messageReconnectTimeout / 10) + if (connect) { + this.connect() + } + } + + get url() { + const encodedParams = url.encodeQueryParams(this.params) + return this.serverUrl + "/" + (encodedParams.length === 0 ? "" : "?" + encodedParams) + } + /** + * @type {boolean} + */ + get synced() { + return this._synced + } + + set synced(state) { + if (this._synced !== state) { + this._synced = state + this.emit("synced", [state]) + this.emit("sync", [state]) + } + } + /** + * @type {number} + */ + get onlineUserCount() { + return this._onlineUserCount + } + + set onlineUserCount(count) { + if (this._onlineUserCount !== count) { + this._onlineUserCount = count + this.emit("online-user-count", [count]) + } + } + + destroy(): void { + if (this._resyncInterval !== 0) { + clearInterval(this._resyncInterval) + } + clearInterval(this._checkInterval) + this.disconnect() + if (env.isNode && typeof process !== "undefined") { + process.off("exit", this._exitHandler) + } + this.awareness.off("update", this._awarenessUpdateHandler) + this.doc.off("update", this._updateHandler) + super.destroy() + } + + connectBc(): void { + if (this.disableBc) { + return + } + if (!this.bcconnected) { + bc.subscribe(this.bcChannel, this._bcSubscriber) + this.bcconnected = true + } + // send sync step1 to bc + // write sync step 1 + const encoderSync = encoding.createEncoder() + encoding.writeVarUint(encoderSync, messageSync) + syncProtocol.writeSyncStep1(encoderSync, this.doc) + bc.publish(this.bcChannel, encoding.toUint8Array(encoderSync), this) + // broadcast local state + const encoderState = encoding.createEncoder() + encoding.writeVarUint(encoderState, messageSync) + syncProtocol.writeSyncStep2(encoderState, this.doc) + bc.publish(this.bcChannel, encoding.toUint8Array(encoderState), this) + // write queryAwareness + const encoderAwarenessQuery = encoding.createEncoder() + encoding.writeVarUint(encoderAwarenessQuery, messageQueryAwareness) + bc.publish(this.bcChannel, encoding.toUint8Array(encoderAwarenessQuery), this) + // broadcast local awareness state + const encoderAwarenessState = encoding.createEncoder() + encoding.writeVarUint(encoderAwarenessState, messageAwareness) + encoding.writeVarUint8Array( + encoderAwarenessState, + awarenessProtocol.encodeAwarenessUpdate(this.awareness, [this.doc.clientID]) + ) + bc.publish(this.bcChannel, encoding.toUint8Array(encoderAwarenessState), this) + } + + disconnectBc(): void { + // broadcast message with local awareness state set to null (indicating disconnect) + const encoder = encoding.createEncoder() + encoding.writeVarUint(encoder, messageAwareness) + encoding.writeVarUint8Array( + encoder, + awarenessProtocol.encodeAwarenessUpdate(this.awareness, [this.doc.clientID], new Map()) + ) + broadcastMessage(this, encoding.toUint8Array(encoder)) + if (this.bcconnected) { + bc.unsubscribe(this.bcChannel, this._bcSubscriber) + this.bcconnected = false + } + } + + disconnect(): void { + this.shouldConnect = false + this.disconnectBc() + if (!!this.ws && this.ws?.readyState === WebSocket.OPEN) { + const value = this.getSendData({buf: new Uint8Array(), docType: notepadActions.leave}) + this.ws?.send(value) + closeWebsocketConnection(this, this.ws) + } + } + + connect(): void { + this.shouldConnect = true + if (!this.wsconnected && this.ws === null) { + setupWS(this) + this.connectBc() + } + } +} diff --git a/src/compoments/MilkdownEditor/WebsocketProvider/WebsocketProviderType.d.ts b/src/compoments/MilkdownEditor/WebsocketProvider/WebsocketProviderType.d.ts new file mode 100644 index 00000000..839fd93e --- /dev/null +++ b/src/compoments/MilkdownEditor/WebsocketProvider/WebsocketProviderType.d.ts @@ -0,0 +1,119 @@ +import * as encoding from "lib0/encoding" +import * as decoding from "lib0/decoding" +import * as awarenessProtocol from "y-protocols/awareness" +import {ObservableV2} from "lib0/observable" +import {Doc, Transaction} from "yjs" +import {WebsocketProvider} from "./WebsocketProvider" +import {API} from "@/services/swagger/resposeType" + +type WebSocketType = typeof WebSocket +// 同步文档操作函数接口 +export type MessageHandlersProps = ( + encoder: encoding.Encoder, + decoder: decoding.Decoder, + provide: WebsocketProvider, + emitSynced: boolean, + messageType: number +) => void + +export interface NotepadActionsProps { + /**加入文档 */ + join: "joinDoc" + /**离开文档 */ + leave: "leaveDoc" + /**修改文档 */ + edit: "editDoc" +} + +// 通过映射类型获取 NotepadActionsProps 中所有字段的值,作为联合类型 +export type NotepadActionType = NotepadActionsProps[keyof NotepadActionsProps] + +export interface WebsocketProviderOptions { + /**为true 自动链接 */ + connect?: boolean + /**Awareness */ + awareness?: awarenessProtocol.Awareness + /**specify url parameters */ + params?: {[key: string]: string} + /***specify websocket protocols */ + protocols?: string[] + /**Optionall provide a WebSocket polyfill */ + WebSocketPolyfill?: WebSocketType + /**Request server state every `resyncInterval` milliseconds */ + resyncInterval?: number + /**Maximum amount of time to wait before trying to reconnect (we try to reconnect using exponential backoff) */ + maxBackoffTime?: number + /**Disable cross-tab BroadcastChannel communication */ + disableBc?: boolean + data?: NotepadWsRequest +} + +export type WebsocketProviderBcSubscriber = (data: ArrayBuffer, origin: any) => void +export type WebsocketProviderUpdateHandler = (update: Uint8Array, origin: any, ydoc: Doc, tr: Transaction) => void +export type WebsocketProviderAwarenessUpdateHandler = ( + changed: {added: number[]; updated: number[]; removed: number[]}, + origin: any +) => void + +export type WebsocketProviderGetSendData = (v: {buf?: Uint8Array; docType: NotepadActionType}) => Buffer + +export type WebsocketProviderExitHandler = () => void + +export interface WSConnectedStatusProps { + /**已连接 */ + connected: "connected" + /**断开链接 */ + disconnected: "disconnected" + /**连接中 */ + connecting: "connecting" +} +// 通过映射类型获取 WSConnectedStatusProps 中所有字段的值,作为联合类型 +export type WSConnectedStatusType = WSConnectedStatusProps[keyof WSConnectedStatusProps] + +export interface WebsocketProviderEmitOfStatus { + status: WSConnectedStatusType +} +export interface WebsocketProviderEmitOfSaveStatus { + saveStatus: NotepadSaveStatusType +} +export type ObservableEvents = { + /**文档是否同步完成 */ + synced: (text: boolean) => void // synced 事件,接收一个 boolean 类型的参数 + /*NOTE - 暂不知*/ + sync: (text: boolean) => void // sync 事件 + /**ws链接状态 */ + status: (s: WebsocketProviderEmitOfStatus) => void + /***ws 链接报错 */ + "connection-error": (event: Event, provider: WebsocketProvider) => void + /***ws 关闭链接 */ + "connection-close": (event: CloseEvent, provider: WebsocketProvider) => void + /**文档保存状态 */ + saveStatus: (s: WebsocketProviderEmitOfSaveStatus) => void + /**文档在线用户数 */ + "online-user-count": (n: number) => void +} + +export interface NotepadSaveStatusProps { + saveProgress: "saveProgress" + saveSuccess: "saveSuccess" + saveError: "saveError" +} +export type NotepadSaveStatusType = NotepadSaveStatusProps[keyof NotepadSaveStatusProps] + +interface NotepadWsRequestParams { + /**不传得话就是前端传什么给后端,后端原封不动传回;传的话后悔会在次基础上做历史记录保存和更新最新的文档 */ + content?: string + /**文档标题,传content就会传title */ + title?: string + docType: NotepadActionType + saveStatus: NotepadSaveStatusType + userName?: string + /**文档在线用户数 */ + userCount?: number +} +export interface NotepadWsRequest extends Omit { + params: NotepadWsRequestParams + yjsParams: string + token: string + notepadHash: string +} diff --git a/src/compoments/MilkdownEditor/WebsocketProvider/constants.ts b/src/compoments/MilkdownEditor/WebsocketProvider/constants.ts new file mode 100644 index 00000000..02261b06 --- /dev/null +++ b/src/compoments/MilkdownEditor/WebsocketProvider/constants.ts @@ -0,0 +1,30 @@ +import {NotepadActionsProps, NotepadSaveStatusProps, WSConnectedStatusProps} from "./WebsocketProviderType" + +/**同步消息 */ +export const messageSync = 0 +/**查询 Awareness 客户端的状态信息 */ +export const messageQueryAwareness = 3 +/**发送关于其他客户端状态的通知 */ +export const messageAwareness = 1 +/**身份验证 */ +export const messageAuth = 2 + +export const notepadActions: NotepadActionsProps = { + join: "joinDoc", + leave: "leaveDoc", + edit: "editDoc" +} + +/**ws 链接状态 */ +export const wsConnectedStatus: WSConnectedStatusProps = { + connected: "connected", + disconnected: "disconnected", + connecting: "connecting" +} + +/**ws 文档保存状态 */ +export const notepadSaveStatus: NotepadSaveStatusProps = { + saveProgress: "saveProgress", + saveSuccess: "saveSuccess", + saveError: "saveError" +} diff --git a/src/compoments/MilkdownEditor/constants.tsx b/src/compoments/MilkdownEditor/constants.tsx new file mode 100644 index 00000000..9010904d --- /dev/null +++ b/src/compoments/MilkdownEditor/constants.tsx @@ -0,0 +1,191 @@ +import {OutlineCameraIcon, OutlineLightbulbIcon, OutlinePaperclipIcon} from "@/assets/icon/outline" +import { + IconHeading1, + IconHeading2, + IconHeading3, + IconListOrdered, + IconList, + IconCheckSquare, + IconCurlyBraces, + IconQuote, + IconFlipVertical, + IconType +} from "./icon/icon" +import {ReactNode} from "react" + +export interface MilkdownBaseUtilProps { + key: string + icon: ReactNode + label: string + description: string +} +export interface BlockListProps { + key: MilkdownMenuType + icon: ReactNode + label: string + description: string +} +export type TooltipListProps = BlockListProps | {key: string; label: string} + +export const MilkdownMenu = { + text: { + key: "text", + icon: , + label: "文本", + description: "文本" + }, + heading1: { + key: "heading1", + icon: , + label: "一级标题", + description: "一级标题: #空格" + }, + heading2: { + key: "heading2", + icon: , + label: "二级标题", + description: "二级标题: ##空格" + }, + heading3: { + key: "heading3", + icon: , + label: "三级标题", + description: "三级标题: ###空格" + }, + orderedList: { + key: "orderedList", + icon: , + label: "有序列表", + description: "有序列表: 1.空格" + }, + unorderedList: { + key: "unorderedList", + icon: , + label: "无序列表", + description: "无序列表: -空格或*空格" + }, + task: { + key: "task", + icon: , + label: "任务", + description: "任务" + }, + codeBlock: { + key: "codeBlock", + icon: , + label: "代码块", + description: "代码块:```空格" + }, + quote: { + key: "quote", + icon: , + label: "引用", + description: "引用: >空格" + }, + highLight: { + key: "highLight", + icon: , + label: "高亮", + description: "高亮: :::success空格" + }, + file: { + key: "file", + icon: , + label: "上传文件", + description: "上传文件" + }, + divider: { + key: "divider", + icon: , + label: "分割线", + description: "分割线: ***" + } +} + +const MilkdownMenuKey = Object.keys(MilkdownMenu) +/**基础菜单中的key集合 */ +export type MilkdownMenuType = keyof typeof MilkdownMenu + +export const MilkdownMenuKeyEnum = Object.fromEntries( + MilkdownMenuKey.map((item) => [item.charAt(0).toUpperCase() + item.slice(1), item]) +) as { + [K in MilkdownMenuType as Capitalize]: K +} +/** + * 根据传的key生成对应的工具菜单,按key的顺序 + * @param {MilkdownMenuType[]} keys + * */ +export const createMilkdownMenuListByKey = (keys: MilkdownMenuType[]) => { + return keys.map((ele) => ({ + ...MilkdownMenu[ele], + key: ele + })) +} +//#region slash + +/**基础菜单 Key值 */ +export const baseSlashKey: MilkdownMenuType[] = [ + MilkdownMenuKeyEnum.Text, + MilkdownMenuKeyEnum.Heading1, + MilkdownMenuKeyEnum.Heading2, + MilkdownMenuKeyEnum.Heading3, + MilkdownMenuKeyEnum.OrderedList, + MilkdownMenuKeyEnum.UnorderedList, + MilkdownMenuKeyEnum.CodeBlock, + MilkdownMenuKeyEnum.Quote, + MilkdownMenuKeyEnum.Divider +] +/**显示常用菜单 Key值 */ +export const onlineCommonSlashKey: MilkdownMenuType[] = [ + MilkdownMenuKeyEnum.Task, + MilkdownMenuKeyEnum.HighLight, + MilkdownMenuKeyEnum.File +] + +/**本地常用菜单 Key值 */ +export const localCommonSlashKey: MilkdownMenuType[] = [MilkdownMenuKeyEnum.Task, MilkdownMenuKeyEnum.HighLight] + +//#endregion + +//#region Block +export const onlineBlockKey: MilkdownMenuType[] = [ + MilkdownMenuKeyEnum.Heading1, + MilkdownMenuKeyEnum.Heading2, + MilkdownMenuKeyEnum.Heading3, + MilkdownMenuKeyEnum.OrderedList, + MilkdownMenuKeyEnum.UnorderedList, + MilkdownMenuKeyEnum.Task, + MilkdownMenuKeyEnum.CodeBlock, + MilkdownMenuKeyEnum.Quote, + MilkdownMenuKeyEnum.HighLight, + MilkdownMenuKeyEnum.File, + MilkdownMenuKeyEnum.Divider +] +export const localBlockKey: MilkdownMenuType[] = [ + MilkdownMenuKeyEnum.Heading1, + MilkdownMenuKeyEnum.Heading2, + MilkdownMenuKeyEnum.Heading3, + MilkdownMenuKeyEnum.OrderedList, + MilkdownMenuKeyEnum.UnorderedList, + MilkdownMenuKeyEnum.Task, + MilkdownMenuKeyEnum.CodeBlock, + MilkdownMenuKeyEnum.Quote, + MilkdownMenuKeyEnum.HighLight, + MilkdownMenuKeyEnum.Divider +] +//#endregion +//#region Tooltip +export const tooltipKey: (MilkdownMenuType | "key-divider")[] = [ + MilkdownMenuKeyEnum.Text, + MilkdownMenuKeyEnum.Heading1, + MilkdownMenuKeyEnum.Heading2, + MilkdownMenuKeyEnum.Heading3, + "key-divider", + MilkdownMenuKeyEnum.OrderedList, + MilkdownMenuKeyEnum.UnorderedList, + MilkdownMenuKeyEnum.Task, + "key-divider", + MilkdownMenuKeyEnum.CodeBlock, + MilkdownMenuKeyEnum.Quote +] +//#endregion diff --git a/src/compoments/MilkdownEditor/css/alert.scss b/src/compoments/MilkdownEditor/css/alert.scss new file mode 100644 index 00000000..3947c7ff --- /dev/null +++ b/src/compoments/MilkdownEditor/css/alert.scss @@ -0,0 +1,29 @@ +@use "../../../theme/themeify.scss" as tools; +.alter-custom { + padding: 12px; + border-radius: 0 8px 8px 0; + margin-bottom: 20px; + min-height: 48px; +} + +.alter-custom-warning, +.alter-custom-caution { + background-color: tools.fetch-color-transparen(var(--Colors-Use-Warning-Primary), 10%); + border-left: 3px solid var(--Colors-Use-Warning-Primary); +} + +.alter-custom-danger { + background-color: tools.fetch-color-transparen(var(--Colors-Use-Error-Primary), 10%); + border-left: 3px solid var(--Colors-Use-Error-Primary); +} + +.alter-custom-success, +.alter-custom-tip { + background-color: tools.fetch-color-transparen(var(--Colors-Use-Success-Primary), 10%); + border-left: 3px solid var(--Colors-Use-Success-Primary); +} + +.alter-custom-note { + background-color: var(--Colors-Use-Neutral-Bg-Hover); + border-left: 3px solid var(--Colors-Use-Neutral-Border); +} diff --git a/src/compoments/MilkdownEditor/css/blockquote.scss b/src/compoments/MilkdownEditor/css/blockquote.scss new file mode 100644 index 00000000..1fa80c67 --- /dev/null +++ b/src/compoments/MilkdownEditor/css/blockquote.scss @@ -0,0 +1,6 @@ +.prose :where(blockquote p:first-of-type):not(:where([class~="not-prose"], [class~="not-prose"] *))::before { + content: ""; +} +.prose :where(blockquote p:last-of-type):not(:where([class~="not-prose"], [class~="not-prose"] *))::after { + content: ""; +} diff --git a/src/compoments/MilkdownEditor/css/codeBlock.scss b/src/compoments/MilkdownEditor/css/codeBlock.scss new file mode 100644 index 00000000..cc330368 --- /dev/null +++ b/src/compoments/MilkdownEditor/css/codeBlock.scss @@ -0,0 +1,161 @@ +milkdown-code-block { + display: block; + position: relative; + padding: 24px 40px; + background: #f5f5f5; + margin: 20px 0; + + .language-picker { + width: max-content; + position: absolute; + z-index: 1; + display: none; + padding-top: 16px; + } + + .language-picker.show { + display: block; + } + + .language-button { + display: flex; + align-items: center; + } + + .search-box { + display: flex; + align-items: center; + } + + .search-box .clear-icon { + cursor: pointer; + } + + .hidden { + display: none; + } + + .cm-editor { + outline: none !important; + } + + .language-button { + gap: 8px; + padding: 8px; + background: antiquewhite; + border-radius: 8px; + font-size: 14px; + margin-bottom: 24px; + opacity: 0; + transition: opacity 0.2s ease-in-out; + } + + &:hover .language-button { + opacity: 1; + } + + .language-button:hover { + background: bisque; + } + + .language-button .expand-icon { + transition: transform 0.2s ease-in-out; + } + + .language-button .expand-icon svg { + width: 16px; + height: 16px; + } + + .language-button[data-expanded="true"] .expand-icon { + transform: rotate(180deg); + } + + .language-button .expand-icon svg:focus, + .language-button .expand-icon:focus-visible { + outline: none; + } + + .list-wrapper { + background: antiquewhite; + border-radius: 16px; + box-shadow: + 0px 2px 6px 0px rgba(0, 0, 0, 0.15), + 0px 1px 2px 0px rgba(0, 0, 0, 0.3); + width: 220px; + } + + .language-list { + height: 356px; + overflow-y: auto; + margin: 0; + padding: 0; + } + + .language-list-item { + cursor: pointer; + margin: 0; + height: 32px; + display: flex; + align-items: center; + gap: 8px; + padding: 0 8px; + font-size: 12px; + } + + .language-list-item:hover { + background: cornsilk; + } + + .language-list-item:focus-visible { + outline: none; + background: cornsilk; + } + + .language-list-item .leading, + .language-list-item .leading svg { + width: 16px; + height: 16px; + } + + .list-wrapper { + padding-top: 20px; + } + + .search-box { + margin: 0 16px 12px; + background: white; + height: 32px; + border-radius: 4px; + outline: 2px solid fuchsia; + gap: 8px; + padding: 0 16px; + font-size: 12px; + } + + .search-box .search-input { + width: 100%; + } + + .search-box .search-icon svg { + width: 16px; + height: 16px; + } + + .search-box .clear-icon svg { + width: 16px; + height: 16px; + } + + .search-box input { + background: transparent; + } + + .search-box input:focus { + outline: none; + } +} + +.cm-editor { + font-size: 14px; +} diff --git a/src/compoments/MilkdownEditor/css/comment.scss b/src/compoments/MilkdownEditor/css/comment.scss new file mode 100644 index 00000000..1a8903a4 --- /dev/null +++ b/src/compoments/MilkdownEditor/css/comment.scss @@ -0,0 +1,3 @@ +.comment-custom { + border-bottom: 2px solid var(--Colors-Use-Yellow-Bg); +} diff --git a/src/compoments/MilkdownEditor/css/cursor.scss b/src/compoments/MilkdownEditor/css/cursor.scss new file mode 100644 index 00000000..c532b5ea --- /dev/null +++ b/src/compoments/MilkdownEditor/css/cursor.scss @@ -0,0 +1,32 @@ +/* this is a rough fix for the first cursor position when the first paragraph is empty */ +.ProseMirror > .ProseMirror-yjs-cursor:first-child { + margin-top: 16px; +} +/* This gives the remote user caret. The colors are automatically overwritten*/ +.ProseMirror-yjs-cursor { + position: relative; + margin-left: -1px; + margin-right: -1px; + border-left: 1px solid black; + border-right: 1px solid black; + border-color: orange; + word-break: normal; + pointer-events: none; +} +/* This renders the username above the caret */ +.ProseMirror-yjs-cursor > div { + position: absolute; + top: -1.05em; + left: -1px; + font-size: 13px; + background-color: rgb(250, 129, 0); + font-family: serif; + font-style: normal; + font-weight: normal; + line-height: normal; + user-select: none; + color: white; + padding-left: 2px; + padding-right: 2px; + white-space: nowrap; +} diff --git a/src/compoments/MilkdownEditor/css/heading.scss b/src/compoments/MilkdownEditor/css/heading.scss new file mode 100644 index 00000000..f636eb8f --- /dev/null +++ b/src/compoments/MilkdownEditor/css/heading.scss @@ -0,0 +1,45 @@ +.milkdown { + h1, + h2, + h3, + h4, + h5, + h6, + h7, + h8 { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-weight: 500; + } + h1 { + font-size: 32px; + line-height: 40px; + } + h2 { + font-size: 24px; + line-height: 32px; + } + h3 { + font-size: 20px; + line-height: 28px; + } + h4 { + font-size: 18px; + line-height: 26px; + } + h5 { + font-size: 16px; + line-height: 24px; + } + h6 { + font-size: 14px; + line-height: 20px; + } + h7 { + font-size: 12px; + line-height: 16px; + } + h8 { + font-size: 11px; + line-height: 14px; + } +} diff --git a/src/compoments/MilkdownEditor/css/imageBlock.scss b/src/compoments/MilkdownEditor/css/imageBlock.scss new file mode 100644 index 00000000..c97a4d6c --- /dev/null +++ b/src/compoments/MilkdownEditor/css/imageBlock.scss @@ -0,0 +1,197 @@ +.milkdown milkdown-image-block.selected > .image-wrapper:before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: color-mix(in srgb, #e1e2e8, transparent 60%); +} + +milkdown-image-block > .image-wrapper .operation { + gap: 16px; + right: 16px; + top: 16px; + opacity: 0; + transition: all 0.2s; +} + +milkdown-image-block:hover > .image-wrapper .operation { + opacity: 1; +} + +milkdown-image-block > .image-wrapper .operation > .operation-item { + color: var(--Colors-Use-Neutral-Disable); + padding: 2px; + background: rgba(0, 0, 0, 0.4); + border-radius: 50%; + height: 24px; + width: 24px; + display: flex; + justify-content: center; + align-items: center; +} + +milkdown-image-block > .image-wrapper .image-resize-handle { + height: 4px; + bottom: -2px; + max-width: 160px; + width: 100%; + background: var(--Colors-Use-Neutral-Disable); + opacity: 0; + transition: all 0.2s; + border-radius: 4px; +} + +milkdown-image-block:hover > .image-wrapper .image-resize-handle { + opacity: 1; +} + +milkdown-image-block > .caption-input { + margin: 4px auto 16px; +} + +milkdown-image-block > .image-edit { + align-items: center; + padding: 16px 24px; + gap: 16px; + background: oldlace; + height: 56px; +} + +milkdown-image-block > .image-edit .image-icon { + color: var(--Colors-Use-Neutral-Text-1-Title); +} + +milkdown-image-block > .image-edit .image-icon svg { + width: 24px; + height: 24px; +} + +milkdown-image-block > .image-edit .link-importer .placeholder { + color: var(--Colors-Use-Neutral-Text-1-Title); +} + +milkdown-image-block > .image-edit .link-importer .link-input-area { + line-height: 24px; + padding: 8px 0; +} + +milkdown-image-block > .image-edit .link-importer .placeholder .uploader { + gap: 8px; + color: var(--Colors-Use-Main-Border); + justify-content: center; + transition: color 0.2s; +} + +milkdown-image-block > .image-edit .link-importer.focus .placeholder .uploader { + color: unset; +} + +milkdown-image-block > .image-edit .link-importer .placeholder .uploader:hover { + color: var(--Colors-Use-Main-Border); +} + +milkdown-image-block > .image-edit .link-importer .placeholder .text { + margin-left: 8px; +} + +milkdown-image-block > .image-edit .confirm { + background: var(--Colors-Use-Neutral-Text-1-Title); + color: antiquewhite; + line-height: 40px; + padding: 0 24px; + border-radius: 100px; + font-size: 14px; +} + +milkdown-image-block { + outline: none; + margin: 16px 0; + display: block; +} + +milkdown-image-block > .image-wrapper { + position: relative; + width: fit-content; + margin: 0 auto; + min-width: 100px; +} + +milkdown-image-block > .image-wrapper .operation { + position: absolute; + display: flex; +} + +milkdown-image-block > .image-wrapper .operation > .operation-item { + cursor: pointer; +} + +milkdown-image-block > .image-wrapper img { + max-width: 100%; + min-height: 100px; + display: block; + object-fit: cover; +} + +milkdown-image-block > .image-wrapper > .image-resize-handle { + position: absolute; + left: 50%; + transform: translateX(-50%); +} + +milkdown-image-block > .image-wrapper > .image-resize-handle:hover { + cursor: row-resize; +} + +milkdown-image-block input { + background: transparent; + outline: none; + border: 0; +} + +milkdown-image-block > .caption-input { + display: block; + width: 100%; + text-align: center; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + line-height: 16px; +} + +milkdown-image-block > .image-edit { + display: flex; +} + +milkdown-image-block > .image-edit .confirm { + cursor: pointer; +} + +milkdown-image-block > .image-edit .link-importer { + position: relative; + flex: 1; +} + +milkdown-image-block > .image-edit .link-importer > .link-input-area { + width: 100%; +} + +milkdown-image-block > .image-edit .link-importer .placeholder { + position: absolute; + top: 0; + left: 0; + bottom: 0; + display: flex; + align-items: center; + cursor: text; +} + +milkdown-image-block > .image-edit .link-importer .placeholder .uploader { + cursor: pointer; + display: flex; +} + +milkdown-image-block .hidden { + display: none !important; +} diff --git a/src/compoments/MilkdownEditor/css/imageInline.scss b/src/compoments/MilkdownEditor/css/imageInline.scss new file mode 100644 index 00000000..9c41d3d4 --- /dev/null +++ b/src/compoments/MilkdownEditor/css/imageInline.scss @@ -0,0 +1,129 @@ +milkdown-image-inline { + display: inline-block !important; + vertical-align: bottom; +} + +milkdown-image-inline > .image-inline { + display: block; + height: 100%; + max-height: 40px; +} + +milkdown-image-inline.selected > .image-inline { + outline: 2px solid var(--Colors-Use-Main-Border); + vertical-align: bottom; +} + +milkdown-image-inline.selected > .empty-image-inline { + outline: 2px solid var(--Colors-Use-Main-Border); +} + +milkdown-image-inline > .empty-image-inline { + align-items: center; + padding: 0 8px; + gap: 8px; + background: oldlace; + border-radius: 4px; +} + +milkdown-image-inline > .empty-image-inline .image-icon { + display: flex; + align-items: center; + justify-content: center; + color: var(--Colors-Use-Neutral-Text-1-Title); +} + +milkdown-image-inline > .empty-image-inline .image-icon svg { + width: 16px; + height: 16px; +} + +milkdown-image-inline > .empty-image-inline .link-importer .placeholder { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; +} + +milkdown-image-inline > .empty-image-inline .link-importer .link-input-area { + line-height: 16px; + font-size: 12px; + padding: 8px 0; +} + +milkdown-image-inline > .empty-image-inline .link-importer .placeholder .uploader { + gap: 8px; + color: var(--Colors-Use-Main-Border); + justify-content: center; + transition: color 0.2s; +} + +milkdown-image-inline > .empty-image-inline .link-importer.focus .placeholder .uploader { + color: unset; +} + +milkdown-image-inline > .empty-image-inline .link-importer .placeholder .uploader:hover { + color: var(var(--Colors-Use-Main-Border)); +} + +milkdown-image-inline > .empty-image-inline .link-importer .placeholder .text { + margin-left: 8px; +} + +milkdown-image-inline > .empty-image-inline .confirm { + background: var(--Colors-Use-Neutral-Text-1-Title); + color: antiquewhite; + line-height: 28px; + padding: 0 12px; + border-radius: 8px; + font-size: 12px; +} + +milkdown-image-inline { + outline: none; + display: inline; +} + +milkdown-image-inline input { + background: transparent; + outline: none; + border: 0; +} + +milkdown-image-inline.empty { + vertical-align: bottom; +} + +milkdown-image-inline > .empty-image-inline { + display: inline-flex; +} + +milkdown-image-inline > .empty-image-inline .confirm { + cursor: pointer; +} + +milkdown-image-inline > .empty-image-inline .link-importer { + position: relative; + flex: 1; +} + +milkdown-image-inline > .empty-image-inline .link-importer > .link-input-area { + width: 208px; +} + +milkdown-image-inline > .empty-image-inline .link-importer .placeholder { + position: absolute; + top: 0; + left: 0; + bottom: 0; + display: flex; + align-items: center; + cursor: text; +} + +milkdown-image-inline > .empty-image-inline .link-importer .placeholder .uploader { + cursor: pointer; + display: flex; +} + +milkdown-image-inline .hidden { + display: none !important; +} diff --git a/src/compoments/MilkdownEditor/css/index.scss b/src/compoments/MilkdownEditor/css/index.scss new file mode 100644 index 00000000..cb03af01 --- /dev/null +++ b/src/compoments/MilkdownEditor/css/index.scss @@ -0,0 +1,70 @@ +@use "./alert.scss"; +@use "./comment.scss"; +@use "./heading.scss"; +@use "./codeBlock.scss"; +@use "./imageBlock.scss"; +@use "./imageInline.scss"; +@use "./listItemBlock.scss"; +@use "./placeholder.scss"; +@use "./blockquote.scss"; +@use "./table.scss"; +@use "./cursor.scss"; + +.milkdown { + overscroll-behavior: none; + position: relative; + word-wrap: break-word; + white-space: break-spaces; + + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 14px; + font-weight: 400; + line-height: 24px; + .editor { + padding: 0 0 24px; + } + code { + border-radius: 4px; + padding: 2px 6px; + background: var(--Colors-Use-Neutral-Bg-Hover); + border: 1px solid var(--Colors-Use-Neutral-Border); + color: var(--Colors-Use-Neutral-Text-1-Title); + display: inline-flex; + align-items: center; + font-size: 12px; + white-space: normal; + } + mark { + background-color: var(--Colors-Use-Main-Bg); + padding: 0 2px; + } + mark.highlight-pulse { + background-color: var(--Colors-Use-Main-Primary); + color: var(--Colors-Use-Main-On-Primary); + } + + /* 基础高亮 */ + .search-highlight { + background-color: rgba(255, 255, 0, 0.3); + border-radius: 2px; + } + + /* 当前选中项的高亮 */ + .search-highlight.current { + background-color: #ff9900; + border: 1px solid #ff5500; + } + + .ProseMirror-selectednode { + background-color: var(--Colors-Use-Main-Focus); + border-radius: 4px; + } + + .ProseMirror-focused { + outline: 0; + } + ::selection { + background-color: var(--Colors-Use-Main-Border); + color: var(--Colors-Use-Neutral-Text-1-Title); + } +} diff --git a/src/compoments/MilkdownEditor/css/listItemBlock.scss b/src/compoments/MilkdownEditor/css/listItemBlock.scss new file mode 100644 index 00000000..7e0580cb --- /dev/null +++ b/src/compoments/MilkdownEditor/css/listItemBlock.scss @@ -0,0 +1,11 @@ +.milkdown { + ul, + ol { + padding: 0; + } + li { + p { + margin-bottom: 0; + } + } +} diff --git a/src/compoments/MilkdownEditor/css/placeholder.scss b/src/compoments/MilkdownEditor/css/placeholder.scss new file mode 100644 index 00000000..a787ab41 --- /dev/null +++ b/src/compoments/MilkdownEditor/css/placeholder.scss @@ -0,0 +1,12 @@ +.milkdown { + .crepe-placeholder { + border-radius: 4px; + } + .crepe-placeholder::before { + position: absolute; + color: var(--Colors-Use-Neutral-Disable); + pointer-events: none; + height: 0; + content: attr(data-placeholder); + } +} diff --git a/src/compoments/MilkdownEditor/css/table.scss b/src/compoments/MilkdownEditor/css/table.scss new file mode 100644 index 00000000..b930a43f --- /dev/null +++ b/src/compoments/MilkdownEditor/css/table.scss @@ -0,0 +1,172 @@ +.milkdown { + padding: 0 !important; + table{ + margin-bottom: 20px; + } + th, + td { + border: 1px solid #ccc; + } + + milkdown-table-block { + display: block; + margin: 4px 0; + } + + milkdown-table-block table { + margin: 0 !important; + border-radius: 0 !important; + } + + milkdown-table-block .handle { + position: absolute; + z-index: 50; + cursor: grab; + font-size: 14px; + border-color: var(--Colors-Use-Neutral-Border); + } + + milkdown-table-block .cell-handle { + left: -999px; + top: -999px; + z-index: 100; + height: 24px; + transition: opacity 0.2s ease 0.2s; + background-color: #fff; + padding: 0 8px; + border-radius: 4px; + position: absolute; + display: flex; + justify-content: center; + align-items: center; + } + + milkdown-table-block .cell-handle[data-role="col-drag-handle"] { + transform: translateY(50%); + } + + milkdown-table-block .cell-handle[data-role="row-drag-handle"] { + transform: translateX(50%); + } + + milkdown-table-block .cell-handle[data-show="false"] { + opacity: 0; + } + + milkdown-table-block .line-handle[data-show="false"] { + opacity: 0; + } + + milkdown-table-block .handle:hover { + opacity: 1; + } + + milkdown-table-block .cell-handle .button-group { + position: absolute; + height: 30px; + top: -32px; + display: flex; + transform: translateX(-50%); + left: 50%; + background-color: var(--Colors-Use-Basic-Background); + border: 1px solid var(--Colors-Use-Neutral-Border); + border-radius: 4px; + } + milkdown-table-block .cell-handle .button-group[data-show="false"] { + display: none; + } + milkdown-table-block .cell-handle .button-group button { + padding-left: 8px; + padding-right: 8px; + width: max-content; + } + milkdown-table-block .cell-handle .button-group button:hover { + background-color: var(--Colors-Use-Neutral-Bg); + } + + milkdown-table-block .line-handle { + background-color: var(--Colors-Use-Main-Border); + transition: opacity 0.2s ease-in-out; + } + milkdown-table-block .line-handle[data-role="x-line-drag-handle"] { + height: 2px; + } + milkdown-table-block .line-handle[data-role="y-line-drag-handle"] { + width: 2px; + } + + milkdown-table-block .line-handle .add-button { + color: var(--Colors-Use-Main-Primary); + border: 1px solid var(--Colors-Use-Main-Border); + padding: 0 4px; + background: var(--Colors-Use-Basic-Background); + border-radius: 4px; + width: 24px; + cursor: pointer; + &:hover { + background: var(--Colors-Use-Neutral-Bg-Hover); + } + } + + milkdown-table-block .line-handle[data-role="x-line-drag-handle"] .add-button { + position: absolute; + transform: translateX(-100%); + top: -12px; + height: 24px; + } + + milkdown-table-block .line-handle[data-role="y-line-drag-handle"] .add-button { + position: absolute; + transform: translateX(-50%); + top: -24px; + height: 24px; + } + + milkdown-table-block .line-handle[data-display-type="indicator"] .add-button { + display: none; + } + + milkdown-table-block .drag-preview { + position: absolute; + z-index: 100; + border: 1px solid var(--Colors-Use-Neutral-Border); + opacity: 0.5; + display: flex; + flex-direction: column; + } + + milkdown-table-block .drag-preview table { + margin: 0; + } + + milkdown-table-block .drag-preview[data-show="false"] { + display: none; + } + + th, + td { + border: 1px solid var(--Colors-Use-Neutral-Border); + padding: 4px 16px; + .ProseMirror-selectednode { + background-color: transparent !important; + } + &:has(.ProseMirror-selectednode) { + outline: 1px solid var(--Colors-Use-Main-Border); + outline-offset: -1px; + } + + .ProseMirror-selectednode { + outline: none; + } + } + + .selectedCell { + &::after { + background-color: var(--Colors-Use-Main-Bg); + } + + ::selection { + background: transparent; + } + } +} diff --git a/src/compoments/MilkdownEditor/icon/icon.tsx b/src/compoments/MilkdownEditor/icon/icon.tsx new file mode 100644 index 00000000..840ed915 --- /dev/null +++ b/src/compoments/MilkdownEditor/icon/icon.tsx @@ -0,0 +1,504 @@ +import Icon from "@ant-design/icons" +import {CustomIconComponentProps} from "@ant-design/icons/lib/components/Icon" +import React from "react" + +interface IconProps extends CustomIconComponentProps { + onClick: (e: React.MouseEvent) => void + ref?: any +} + +const type = () => ( + + + + + +) +/** + * @description icon/type + */ +export const IconType = (props: Partial) => { + return +} + +const bold = () => ( + + + + +) +/** + * @description icon/bold + */ +export const IconBold = (props: Partial) => { + return +} + +const strikethrough = () => ( + + + + + +) +/** + * @description icon/strikethrough + */ +export const IconStrikethrough = (props: Partial) => { + return +} + +const italic = () => ( + + + + + +) +/** + * @description icon/italic + */ +export const IconItalic = (props: Partial) => { + return +} + +const underline = () => ( + + + + +) +/** + * @description icon/underline + */ +export const IconUnderline = (props: Partial) => { + return +} + +const code2 = () => ( + + + + + +) +/** + * @description icon/code-2 + */ +export const IconCode2 = (props: Partial) => { + return +} + +const heading1 = () => ( + + + + + + +) +/** + * @description icon/heading-1 + */ +export const IconHeading1 = (props: Partial) => { + return +} + +const heading2 = () => ( + + + + + + +) +/** + * @description icon/heading-2 + */ +export const IconHeading2 = (props: Partial) => { + return +} + +const heading3 = () => ( + + + + + + + +) +/** + * @description icon/heading-3 + */ +export const IconHeading3 = (props: Partial) => { + return +} + +const listOrdered = () => ( + + + + + + + + +) +/** + * @description icon/list-ordered + */ +export const IconListOrdered = (props: Partial) => { + return +} + +const list = () => ( + + + + + + + + +) +/** + * @description icon/list + */ +export const IconList = (props: Partial) => { + return +} + +const checkSquare = () => ( + + + + +) +/** + * @description icon/check-square + */ +export const IconCheckSquare = (props: Partial) => { + return +} + +const curlyBraces = () => ( + + + + +) +/** + * @description icon/curly-braces + */ +export const IconCurlyBraces = (props: Partial) => { + return +} + +const quote = () => ( + + + + +) +/** + * @description icon/quote + */ +export const IconQuote = (props: Partial) => { + return +} + +const flipVertical = () => ( + + + + + + + + +) +/** + * @description icon/flip-vertical + */ +export const IconFlipVertical = (props: Partial) => { + return +} +const notepadFileTypeDir = () => ( + + + + + +) +/** + * @description icon/记事本/文件类型/文件夹 Icon/notepad/file type/dir + */ +export const IconNotepadFileTypeDir = (props: Partial) => { + return +} + +const notepadFileTypeUnknown = () => ( + + + + + +) +/** + * @description icon/记事本/文件类型/未知 Icon/notepad/file type/unknown + */ +export const IconNotepadFileTypeUnknown = (props: Partial) => { + return +} + +const notepadFileTypeCompress = () => ( + + + + + +) +/** + * @description icon/记事本/文件类型/压缩文件 Icon/notepad/file type/Compress files + */ +export const IconNotepadFileTypeCompress = (props: Partial) => { + return +} + +const notepadFileTypePPT = () => ( + + + + + +) +/** + * @description icon/记事本/文件类型/ppt Icon/notepad/file type/ppt + */ +export const IconNotepadFileTypePPT = (props: Partial) => { + return +} + +const notepadFileTypeExcel = () => ( + + + + + + +) +/** + * @description icon/记事本/文件类型/excel Icon/notepad/file type/excel + */ +export const IconNotepadFileTypeExcel = (props: Partial) => { + return +} + +const notepadFileTypePdf = () => ( + + + + + +) +/** + * @description icon/记事本/文件类型/pdf Icon/notepad/file type/pdf + */ +export const IconNotepadFileTypePdf = (props: Partial) => { + return +} + +const notepadFileTypeWord = () => ( + + + + + +) +/** + * @description icon/记事本/文件类型/word Icon/notepad/file type/word + */ +export const IconNotepadFileTypeWord = (props: Partial) => { + return +} diff --git a/src/compoments/MilkdownEditor/utils/alertPlugin.ts b/src/compoments/MilkdownEditor/utils/alertPlugin.ts new file mode 100644 index 00000000..9a98d7d8 --- /dev/null +++ b/src/compoments/MilkdownEditor/utils/alertPlugin.ts @@ -0,0 +1,113 @@ +import {$command, $nodeSchema, $nodeAttr, $remark} from "@milkdown/utils" + +import {wrappingInputRule} from "@milkdown/prose/inputrules" +import {$inputRule} from "@milkdown/kit/utils" +import {wrapIn} from "@milkdown/kit/prose/commands" +import directive from "remark-directive" +import {Node} from "@milkdown/kit/prose/model" + +const alterCustomId = "alter-custom" +const alterCustomAttr = $nodeAttr("alter-custom", () => ({"data-type": alterCustomId})) +const customTypes = ["danger", "success", "tip", "note", "warning", "caution"] + +// warning caution +// danger +// success tip +// note +const rule = /:::(danger|success|tip|note|warning|caution)(\s|\n)/ + +export const alterCustomSchema = $nodeSchema(alterCustomId, (ctx) => ({ + group: "block", + content: `block*`, + atom: true, + // 从 DOM 中解析节点,动态设置属性 + parseDOM: [ + { + tag: `div[data-type='${alterCustomId}']`, + getAttrs: (dom) => { + return {type: dom.getAttribute("data-custom-type")} + } + } + ], + + // 将节点转为 DOM 结构,动态设置样式 + toDOM: (node) => { + const type = node.attrs.type + const className = `alter-custom alter-custom-${type}` + return ["div", {...ctx.get(alterCustomAttr.key)(node), class: className, "data-custom-type": type}, 0] // 0表示内嵌内容节点 + }, + + parseMarkdown: { + match: (node) => { + const {type, name} = node + return type === "containerDirective" && customTypes.includes(name as string) + }, + runner: (state, node, type) => { + if (type.name === alterCustomId) { + state.openNode(type, {type: node.name}).next(node.children).closeNode() + } + } + }, + toMarkdown: { + match: (node) => node.type.name === alterCustomId, + runner: (state, node) => { + const type = node.attrs.type + state + .openNode("containerDirective", undefined, { + name: `${type}` + }) + .next(node.content) + .closeNode() + } + }, + attrs: { + type: {default: "tip"} + } +})) + +const alterInputRule = $inputRule((ctx) => { + return wrappingInputRule(rule, alterCustomSchema.type(ctx), (match) => ({type: match[1]})) +}) + +export const alterCommand = $command( + `command-${alterCustomId}`, + (ctx) => (type) => + wrapIn(alterCustomSchema.type(ctx), { + type + }) +) + +export const alterToParagraphCommand = $command(`alterToParagraphCommand`, (ctx) => () => { + return (state, dispatch) => { + const {from, to} = state.selection + // 检查选区是否有效 + if (from === to) { + return false // 没有选中任何内容 + } + // 获取当前选中的节点 + const selectedNode = state.doc.nodeAt(from) + if (selectedNode && selectedNode.type.name === alterCustomId) { + // 创建新的内容节点,将子节点放入新的结构中 + const content = selectedNode.content as any as Node + const transaction = state.tr.replaceRangeWith(from, to, content) + if (dispatch) dispatch(transaction) + return true // 命令成功执行 + } + + return false // 命令未成功执行 + } +}) + +const remarkDirective = $remark(`remark-directive`, () => directive) + +export const alterCustomPlugin = () => { + return [ + ...remarkDirective, + alterCustomAttr, + alterCustomSchema.node, + alterCustomSchema.ctx, + alterInputRule, + alterCommand, + alterToParagraphCommand + ] +} diff --git a/src/compoments/MilkdownEditor/utils/cataloguePlugin.ts b/src/compoments/MilkdownEditor/utils/cataloguePlugin.ts new file mode 100644 index 00000000..c250d3f1 --- /dev/null +++ b/src/compoments/MilkdownEditor/utils/cataloguePlugin.ts @@ -0,0 +1,45 @@ +import {Plugin, PluginKey} from "@milkdown/kit/prose/state" +import {$prose} from "@milkdown/kit/utils" +import {MilkdownCatalogueProps} from "@/pages/notepadManage/modifyNotepad/ModifyNotepadType" + +const getHeading = (view) => { + const {state} = view + const {doc} = state + const headings: MilkdownCatalogueProps[] = [] + // 遍历文档节点,提取标题(heading) + doc.descendants((node) => { + if (node.type.name === state.schema.nodes.heading.name && node.content.size > 0) { + const {attrs} = node + headings.push({ + id: attrs.id, + title: node.textContent, // 标题文本 + key: "", + level: attrs.level, // 标题级别, + children: [] + }) + } + }) + return headings +} + +// 自定义插件:用于生成目录 +export const cataloguePlugin = (callback) => + $prose((ctx) => { + return new Plugin({ + key: new PluginKey("CUSTOM_MILKDOWN_PLUGIN_CATALOGUE"), + view: (editorView) => { + // 初始化目录 + const initHeadings = getHeading(editorView) + callback(initHeadings) + + return { + update: (view, prevState) => { + if (view.state.doc.eq(prevState.doc)) return + const headings = getHeading(view) + // 内容变化时更新目录 + callback(headings) + } + } + } + }) + }) diff --git a/src/compoments/MilkdownEditor/utils/codePlugin.ts b/src/compoments/MilkdownEditor/utils/codePlugin.ts new file mode 100644 index 00000000..e8bc19fa --- /dev/null +++ b/src/compoments/MilkdownEditor/utils/codePlugin.ts @@ -0,0 +1,11 @@ +import {$command} from "@milkdown/utils" +import {convertSelectionByNode} from "./utils" + +export const listToCodeCommand = $command(`listToCodeCommand`, (ctx) => () => (state, dispatch) => { + const command = convertSelectionByNode(state.schema.nodes.code_block) + return command(state, dispatch) +}) + +export const codeCustomPlugin = () => { + return [listToCodeCommand] +} diff --git a/src/compoments/MilkdownEditor/utils/commentPlugin.ts b/src/compoments/MilkdownEditor/utils/commentPlugin.ts new file mode 100644 index 00000000..e81a0839 --- /dev/null +++ b/src/compoments/MilkdownEditor/utils/commentPlugin.ts @@ -0,0 +1,92 @@ +import {$command, $nodeSchema, $nodeAttr} from "@milkdown/utils" +import {Attrs} from "@milkdown/kit/prose/model" +import {TextSelection} from "@milkdown/kit/prose/state" + +const commentCustomId = "comment-custom" +const commentCustomAttr = $nodeAttr("comment-custom", () => ({"data-type": commentCustomId})) + +const commentCustomSchema = $nodeSchema(commentCustomId, (ctx) => ({ + inline: true, + group: "inline", + content: 'text*', + atom: true, + // 从 DOM 中解析节点,动态设置属性 + parseDOM: [ + { + tag: `span[data-type='${commentCustomId}']`, + getAttrs: (dom) => { + return {commentId: dom.getAttribute("data-comment-id")} + } + } + ], + + // 将节点转为 DOM 结构,动态设置样式 + toDOM: (node) => { + const id = node.attrs.commentId + const className = `comment-custom` + return [ + "span", + { + ...ctx.get(commentCustomAttr.key)(node), + class: className, + "data-comment-id": id + }, + 0 + ] + }, + + parseMarkdown: { + match: (node) => { + const {type, name} = node + return type === "textDirective" && name === "comment" + }, + runner: (state, node, type) => { + if (type.name === commentCustomId) { + state + .openNode(type, {...(node.attributes as Attrs)}) + .next(node.children) + .closeNode() + } + } + }, + + toMarkdown: { + match: (node) => { + return node.type.name === commentCustomId + }, + runner: (state, node) => { + state + .openNode("textDirective", undefined, { + name: "comment", + attributes: { + commentId: node.attrs.commentId + } + }) + .next(node.content) + .closeNode() + } + }, + attrs: { + commentId: {default: "0"} + } +})) + +/**将选中内容替换为评论节点 */ +export const commentCommand = $command(`command-${commentCustomId}`, (ctx) => (id) => (state, dispatch) => { + const {selection, tr} = state + if (!(selection instanceof TextSelection)) return false + + const {from, to} = selection + const fragment = state.doc.slice(from, to).content // 获取 Fragment + dispatch?.( + tr + .setMeta(commentCustomId, true) + .replaceSelectionWith(commentCustomSchema.type(ctx).create({commentId: id}, fragment)) + .scrollIntoView() + ) + return true +}) + +export const commentCustomPlugin = () => { + return [commentCustomAttr, commentCustomSchema.node, commentCustomSchema.ctx, commentCommand] +} diff --git a/src/compoments/MilkdownEditor/utils/headingPlugin.ts b/src/compoments/MilkdownEditor/utils/headingPlugin.ts new file mode 100644 index 00000000..0b6a754c --- /dev/null +++ b/src/compoments/MilkdownEditor/utils/headingPlugin.ts @@ -0,0 +1,114 @@ +import {$command} from "@milkdown/utils" +import {convertSelectionByNode} from "./utils" +import {headingSchema} from "@milkdown/kit/preset/commonmark" +import {$prose} from "@milkdown/kit/utils" +import {Plugin, PluginKey} from "@milkdown/kit/prose/state" +import type {EditorView} from "@milkdown/prose/view" +import debounce from "lodash/debounce" + +export const listToHeadingCommand = $command( + `listToHeadingCommand`, + (ctx) => + (level: number = 0) => + (state, dispatch) => { + const command = convertSelectionByNode(headingSchema.type(ctx), {level}) + return command(state, dispatch) + } +) + +/**标题转正文 */ +export const headingToParagraphCommand = $command(`headingToParagraphCommand`, (ctx) => () => (state, dispatch) => { + const {$from, from, to} = state.selection + // 检查选区是否有效 + if (from === to) { + return false // 没有选中任何内容 + } + const parentNode = $from.node($from.depth) + if (parentNode && parentNode.type.name === state.schema.nodes.heading.name) { + let textNode = state.schema.text(parentNode.textContent) + const node = state.schema.nodes.paragraph.create( + null, + textNode // 使用列表项的内容填充 + ) + const start = $from.start() // 标题节点的起始位置 + const end = $from.end() // 标题节点的结束位置 + // 替换整个标题节点 + const transaction = state.tr.replaceRangeWith(start, end, node) + if (dispatch) dispatch(transaction) + return true + } + return false +}) + +export const customSyncHeadingIdPlugin = $prose((ctx) => { + const headingIdPluginKey = new PluginKey("CUSTOM_MILKDOWN_HEADING_ID") + let isComposed = false + const updateId = debounce((view: EditorView) => { + if (isComposed) return + + const tr = view.state.tr.setMeta("addToHistory", false) + + let found = false + if (headingSchema) { + view.state.doc.descendants((node, pos) => { + if (node.type === headingSchema.type(ctx)) { + if (node.textContent.trim().length === 0) return + + const attrs = node.attrs + const idString = node.textContent.length > 20 ? node.textContent.substring(0, 20) : node.textContent + const id = Buffer.from(idString).toString("hex") + if (attrs.id !== id) { + found = true + tr.setMeta(headingIdPluginKey, true).setNodeMarkup(pos, undefined, { + ...attrs, + id: id + }) + } + } + }) + } + if (found) view.dispatch(tr) + }, 500) + + return new Plugin({ + key: headingIdPluginKey, + view: (view) => { + updateId(view) + + return { + update: (view, prevState) => { + if (view.state.doc.eq(prevState.doc)) return + updateId(view) + } + } + }, + props: { + handleTextInput(view, from, to, text) { + // 检测是否为拼音中间状态 + if (!isComposed) { + updateId(view) + } + return false // 允许正常文本插入 + }, + handleDOMEvents: { + compositionstart: () => { + isComposed = true + return false + }, + compositionupdate: (view, event) => { + isComposed = true + return false + }, + compositionend: (view) => { + isComposed = false + updateId(view) + return false + } + } + } + }) +}) + +export const headingCustomPlugin = () => { + return [listToHeadingCommand, headingToParagraphCommand, customSyncHeadingIdPlugin] +} diff --git a/src/compoments/MilkdownEditor/utils/imageBlock.ts b/src/compoments/MilkdownEditor/utils/imageBlock.ts new file mode 100644 index 00000000..c4bbc5a7 --- /dev/null +++ b/src/compoments/MilkdownEditor/utils/imageBlock.ts @@ -0,0 +1,23 @@ +import {imageBlockSchema} from "@milkdown/kit/component/image-block" +import {Attrs} from "@milkdown/kit/prose/model" +import {$command} from "@milkdown/kit/utils" + +// 创建一个命令,用于插入 imageBlock +export const insertImageBlockCommand = $command("insertImageBlock", (ctx) => (props: Attrs | undefined) => { + return (state, dispatch) => { + if (!props) return false + + // 创建 imageBlock 节点,并设置 src 和 alt 属性 + const imageNode = imageBlockSchema.type(ctx).create({ + ...props + }) + + // 插入节点 + const transaction = state.tr.replaceSelectionWith(imageNode) + if (dispatch) { + dispatch(transaction) + } + + return true // 表示命令成功执行 + } +}) diff --git a/src/compoments/MilkdownEditor/utils/initEditor.tsx b/src/compoments/MilkdownEditor/utils/initEditor.tsx new file mode 100644 index 00000000..dfb8dd8b --- /dev/null +++ b/src/compoments/MilkdownEditor/utils/initEditor.tsx @@ -0,0 +1,551 @@ +import {useEditor} from "@milkdown/react" +import {block, blockConfig} from "@milkdown/plugin-block" // 引入block插件 +import {Ctx} from "@milkdown/kit/ctx" +import {BlockView} from "../Block/Block" +import {useNodeViewFactory, usePluginViewFactory} from "@prosemirror-adapter/react" +import {CustomMilkdownProps, MilkdownCollabProps} from "../MilkdownEditorType" +import {placeholderConfig, placeholderPlugin} from "../Placeholder" +import {fileCustomSchema, uploadCustomPlugin} from "./uploadPlugin" +import {upload, uploadConfig} from "@milkdown/kit/plugin/upload" +import {ImgMaxSize} from "@/pages/pluginEditor/pluginImageTextarea/PluginImageTextarea" +import {yakitNotify, yakitInfo} from "@/utils/notification" +import {Blockquote} from "../Blockquote" +import {CustomCodeComponent} from "../CodeBlock/CodeBlock" +import {ListItem} from "../ListItem/ListItem" +import {MilkdownHr} from "../MilkdownHr/MilkdownHr" +import {tooltip, TooltipView} from "../Tooltip/Tooltip" +import {alterCustomPlugin} from "./alertPlugin" +import {codeCustomPlugin} from "./codePlugin" +import {commentCustomPlugin} from "./commentPlugin" +import {headingCustomPlugin} from "./headingPlugin" +import {insertImageBlockCommand} from "./imageBlock" +import {listCustomPlugin} from "./listPlugin" +import {trackDeletePlugin} from "./trackDeletePlugin" +import {underlineCustomPlugin} from "./underline" +import {useCreation, useMemoizedFn} from "ahooks" +import {$view} from "@milkdown/kit/utils" +import {CustomFile} from "../CustomFile/CustomFile" +import {getBase64} from "../MilkdownEditor" +import {Node} from "@milkdown/kit/prose/model" +import {imageBlockComponent, imageBlockConfig} from "@milkdown/kit/component/image-block" +import {imageInlineComponent, inlineImageConfig} from "@milkdown/kit/component/image-inline" +import {html} from "atomico" +import {linkTooltipPlugin, linkTooltipConfig} from "@milkdown/kit/component/link-tooltip" +import { + blockquoteSchema, + codeBlockSchema, + commonmark, + hrSchema, + listItemSchema, + syncHeadingIdPlugin +} from "@milkdown/kit/preset/commonmark" +import {defaultValueCtx, Editor, editorViewOptionsCtx, rootCtx} from "@milkdown/kit/core" +import {listener, listenerCtx} from "@milkdown/kit/plugin/listener" +import {gfm} from "@milkdown/kit/preset/gfm" +import {history} from "@milkdown/kit/plugin/history" +import {clipboard} from "@milkdown/kit/plugin/clipboard" +import {cursor} from "@milkdown/kit/plugin/cursor" +import {trailing} from "@milkdown/kit/plugin/trailing" +import {collab} from "@milkdown/plugin-collab" +import {tableBlock} from "@milkdown/kit/component/table-block" +import {markCustomPlugin} from "./markPlugin" +import {jumpToLinePlugin, jumpToLinePluginKey} from "./jumpLine" +import {editorViewCtx} from "@milkdown/core" +import {TextSelection} from "@milkdown/kit/prose/state" +import type {EditorView} from "@milkdown/prose/view" +import {mentionFactory, MentionListView} from "../Mention/MentionListView" +import {mentionCustomPlugin, mentionCustomSchema} from "./mentionPlugin" +import {CustomMention} from "../Mention/CustomMention" +import {useEffect} from "react" +import emiter from "@/utils/eventBus/eventBus" +import {slash, SlashView} from "../Slash/Slash" +import { httpUploadImgPath } from "@/apiUtils/http" + +export interface InitEditorHooksCollabProps extends MilkdownCollabProps { + onCollab: (ctx: Ctx) => void + onSaveHistory: (newValue: string) => void +} +interface InitEditorHooksDiffProps { + onDiff: (ctx: Ctx) => void +} +export interface InitEditorHooksLocalProps { + local: boolean + upload: (path: string) => void +} +interface InitEditorHooksProps + extends Omit { + collabProps?: InitEditorHooksCollabProps + diffProps?: InitEditorHooksDiffProps + localProps?: InitEditorHooksLocalProps + inViewport?: boolean + +} +export default function useInitEditorHooks(props: InitEditorHooksProps) { + const { + type, + readonly, + defaultValue, + collabProps, + customPlugin, + onMarkdownUpdated, + diffProps, + localProps, + inViewport, + positionElementId, + isControlEditorType + } = props + + const nodeViewFactory = useNodeViewFactory() + const pluginViewFactory = usePluginViewFactory() + + const collabParams: InitEditorHooksCollabProps = useCreation(() => { + if (!collabProps) { + const def: InitEditorHooksCollabProps = { + title: "", + enableCollab: false, + milkdownHash: "", + routeInfo: { + pageId: "", + route: null + }, + onChangeWSLinkStatus: () => {}, + onChangeOnlineUser: () => {}, + onSetTitle: () => {}, + onCollab: () => {}, + onSaveHistory: () => {} + } + return def + } + return collabProps + }, [collabProps]) + + const {get, loading} = useEditor( + (root) => { + //#region 编辑器引用的相关插件 start + const blockPlugins = [ + block, + (ctx: Ctx) => () => { + ctx.set(block.key, { + view: pluginViewFactory({ + component: () => ( + + ) + }) + }) + }, + (ctx: Ctx) => () => { + ctx.update(blockConfig.key, () => ({ + filterNodes: (pos, node) => { + if (node.type.name === "paragraph" && !node.content.size) { + return true + } + return false + } + })) + } + ].flat() + const placeholder = [placeholderConfig, placeholderPlugin] + const uploadPlugins = [ + ...uploadCustomPlugin(), + upload, + $view(fileCustomSchema.node, () => + nodeViewFactory({ + component: () => + }) + ), + (ctx: Ctx) => () => { + ctx.update(uploadConfig.key, (prev) => ({ + ...prev, + uploader: async (files, schema) => { + const images: File[] = [] + for (let i = 0; i < files.length; i++) { + const file = files.item(i) + if (!file) { + continue + } + + // You can handle whatever the file type you want, we handle image here. + if (!file.type.includes("image")) { + continue + } + if (file.size > ImgMaxSize) { + yakitNotify("error", "图片大小不能超过1M") + continue + } + images.push(file) + } + const nodes: Node[] = await Promise.all( + images.map(async (image) => { + const alt = image.name + try { + const src = await uploadImg(image) + return schema.nodes["image-block"].createAndFill({ + src, + alt + }) as Node + } catch (error) { + return schema.nodes.image.createAndFill({ + src: "", + alt + }) as Node + } + }) + ) + return nodes + } + })) + } + ].flat() + + const imagePlugin = [ + imageBlockComponent, + imageInlineComponent, + insertImageBlockCommand, + // TODO 自定义imageBlockSchema.node + // $view(imageBlockSchema.node, () => + // nodeViewFactory({ + // component: () => + // }) + // ), + (ctx: Ctx) => () => { + ctx.update(imageBlockConfig.key, (value) => ({ + ...value, + captionIcon: () => html` + + + + `, + onUpload: async (image: File) => { + const url = uploadImg(image) + return url + } + })) + }, + (ctx: Ctx) => () => { + ctx.update(inlineImageConfig.key, (value) => ({ + ...value, + imageIcon: () => + html` + + + `, + onUpload: async (image: File) => { + const url = uploadImg(image) + return url + } + })) + } + ].flat() + + const linkTooltip = [ + linkTooltipPlugin, + (ctx: Ctx) => () => { + ctx.update(linkTooltipConfig.key, (defaultConfig) => ({ + ...defaultConfig, + linkIcon: () => "🔗", + editButton: () => "✎", + removeButton: () => "❌", + confirmButton: () => "✔️", + onCopyLink: (link: string) => { + yakitInfo("Link copied") + } + })) + } + ].flat() + + const listPlugin = [ + ...listCustomPlugin(), + $view(listItemSchema.node, () => + nodeViewFactory({ + component: ListItem + }) + ) + ].flat() + const headingPlugin = [...headingCustomPlugin()].flat() + const codePlugin = [ + ...codeCustomPlugin(), + $view(codeBlockSchema.node, () => { + return nodeViewFactory({ + component: () => , + stopEvent: (e) => true + }) + }) + ].flat() + const blockquotePlugin = [ + $view(blockquoteSchema.node, () => + nodeViewFactory({ + component: Blockquote + }) + ) + ].flat() + + const alterPlugin = [...alterCustomPlugin()].flat() + + const underlinePlugin = [...underlineCustomPlugin()].flat() + + const commentPlugin = [...commentCustomPlugin()].flat() + + const hrPlugin = [ + $view(hrSchema.node, () => + nodeViewFactory({ + component: MilkdownHr + }) + ) + ].flat() + const markPlugin = [...markCustomPlugin()].flat() + /**启动了在线协作才有 @ 提及 相关逻辑 */ + const mentionPlugin = [ + ...mentionCustomPlugin(), + mentionFactory, + $view(mentionCustomSchema.node, () => + nodeViewFactory({ + component: () => + }) + ), + (ctx: Ctx) => () => { + if (!!collabParams?.enableCollab) { + ctx.set(mentionFactory.key, { + view: pluginViewFactory({ + component: () => + }) + }) + } + } + ].flat() + const slashPlugin = [ + slash, + (ctx: Ctx) => () => { + ctx.set(slash.key, { + view: pluginViewFactory({ + component: () => ( + + ) + }) + }) + } + ].flat() + //#endregion + return ( + Editor.make() + .config((ctx) => { + ctx.set(rootCtx, root) + ctx.set(tooltip.key, { + view: pluginViewFactory({ + component: TooltipView + }) + }) + + // 配置为只读 + ctx.set(editorViewOptionsCtx, { + editable: () => !readonly + }) + ctx.set(defaultValueCtx, defaultValue || "") + collabParams.onCollab(ctx) + diffProps?.onDiff(ctx) + const listener = ctx.get(listenerCtx) + listener.markdownUpdated((ctx, nextMarkdown, prevMarkdown) => { + const isSave = nextMarkdown !== prevMarkdown + if (collabParams.enableCollab && isSave) { + collabParams.onSaveHistory(nextMarkdown) + } + if (isSave) { + onMarkdownUpdated && onMarkdownUpdated(nextMarkdown, prevMarkdown) + } + }) + }) + .use(commonmark.filter((x) => x !== syncHeadingIdPlugin)) + .use(gfm) + .use(cursor) + .use(tooltip) + .use(history) + .use(clipboard) + // trailing + .use(trailing) + // collab + .use(collab) + // listener + .use(listener) + // blockquote + .use(blockquotePlugin) + // block + .use(blockPlugins) + // upload + .use(uploadPlugins) + // image + .use(imagePlugin) + // listItem + .use(listPlugin) + .use(headingPlugin) + // code + .use(codePlugin) + // linkTooltip + .use(linkTooltip) + // placeholder + .use(placeholder) + // table + .use(tableBlock) + // alterCustomPlugin + .use(alterPlugin) + // underlinePlugin + .use(underlinePlugin) + // commentPlugin + .use(commentPlugin) + // hrPlugin + .use(hrPlugin) + // markPlugin + .use(markPlugin) + // slash + .use(slashPlugin) + // trackDeletePlugin + .use(trackDeletePlugin()) + // .use(jumpToLinePlugin(0)) + // mention 提及@ + .use(mentionPlugin) + .use(customPlugin || []) + ) + }, + [readonly, defaultValue, type, collabParams.enableCollab, collabParams.milkdownHash] + ) + + const uploadImg = async (image) => { + if (localProps?.local) { + // 目前本地编辑器不支持上次图片 + return "" + } + if (image.size > ImgMaxSize) { + yakitNotify("error", "图片大小不能超过1M") + return "" + } + try { + const base64 = await getBase64(image) + const src = await httpUploadImgPath({ + base64, + imgInfo: { + filename: image.name || "image.png", + contentType: image.type || "image/png" + }, + type: collabParams.enableCollab ? "notepad" : "img", + filedHash: collabParams.milkdownHash + }) + return src + } catch (error) { + return "" + } + } + //#region 位置定位 + useEffect(() => { + if (!inViewport) return + emiter.on("refreshPositionElement", onSetRefreshPositionElement) + return () => { + emiter.off("refreshPositionElement", onSetRefreshPositionElement) + } + }, [inViewport]) + useEffect(() => { + if (loading || !positionElementId) return + const editor = get() + if (editor) { + jumpByElementId(positionElementId) + } + }, [loading, positionElementId]) + /**根据id跳转到对应位置 */ + const jumpByElementId = useMemoizedFn((targetId: string) => { + setTimeout(() => { + const element = document.getElementById(targetId) + if (element) { + element.scrollIntoView({behavior: "smooth"}) + } + }, 200) + }) + const onSetRefreshPositionElement = useMemoizedFn((positionElementId) => { + jumpByElementId(positionElementId) + }) + // 调用跳转到行 + const jumpToFifthLine = (line: number) => { + if (!line) return + get()?.action((ctx) => { + const view = ctx.get(editorViewCtx) + jumpToLine(view, +line) + }) + } + //NOTE - 跳转到指定行的逻辑,暂未使用 + const jumpToLine = (view: EditorView, lineNumber: number) => { + if (!lineNumber) return + const doc = view.state.doc + let currentLine = 1 // 当前行号(从 1 开始) + let targetPos = 0 // 目标行的起始位置 + let isBreak = false + // 遍历文档节点,计算行号 + doc.descendants((node, pos) => { + if (currentLine > lineNumber || isBreak) return false // 提前终止遍历 + if (node.isText) { + const text = node.text || "" + const lines = text.split("\n") + for (let i = 0; i < lines.length; i++) { + if (currentLine === lineNumber) { + targetPos = pos + lines.slice(0, i).join("\n").length + if (i > 0) targetPos += 1 // 跳过换行符 + isBreak = true + break // 找到目标行,终止遍历 + } + currentLine++ + } + } else if (node.isBlock) { + // 块级节点(如段落、标题)默认占一行 + if (currentLine === lineNumber) { + targetPos = pos + 1 // 跳过节点开始位置 + isBreak = true + return false + } + currentLine++ + } + return true // 继续遍历子节点 + }) + + // 设置光标位置 + if (targetPos > 0 && targetPos <= doc.content.size) { + const tr = view.state.tr.setSelection(TextSelection.near(view.state.doc.resolve(targetPos))) + tr.scrollIntoView() + view.dispatch(tr) + view.focus() + + // // 获取目标位置的 DOM 节点 + // const domPos = view.domAtPos(targetPos) + // const targetNode = domPos.node + } + } + + //#endregion + return {get, loading, jumpToFifthLine} as const +} diff --git a/src/compoments/MilkdownEditor/utils/jumpLine.ts b/src/compoments/MilkdownEditor/utils/jumpLine.ts new file mode 100644 index 00000000..f6aea91d --- /dev/null +++ b/src/compoments/MilkdownEditor/utils/jumpLine.ts @@ -0,0 +1,71 @@ +import {MilkdownPlugin} from "@milkdown/kit/ctx" +import type {EditorView} from "@milkdown/prose/view" +import {Plugin, PluginKey} from "prosemirror-state" +import {$prose} from "@milkdown/kit/utils" +import {TextSelection} from "@milkdown/kit/prose/state" + +// 插件 Key +export const jumpToLinePluginKey = new PluginKey("jump-to-line") + +// 行号跳转插件 +export const jumpToLinePlugin = (line: number): MilkdownPlugin => + $prose((ctx) => { + // 跳转到指定行的逻辑 + const jumpToLine = (view: EditorView, lineNumber: number) => { + if (!lineNumber) return + const doc = view.state.doc + let currentLine = 1 // 当前行号(从 1 开始) + let targetPos = 0 // 目标行的起始位置 + + // 遍历文档节点,计算行号 + doc.descendants((node, pos) => { + if (currentLine > lineNumber) return false // 提前终止遍历 + if (node.isText) { + const text = node.text || "" + const lines = text.split("\n") + for (let i = 0; i < lines.length; i++) { + if (currentLine === lineNumber) { + targetPos = pos + lines.slice(0, i).join("\n").length + if (i > 0) targetPos += 1 // 跳过换行符 + return false // 找到目标行,终止遍历 + } + currentLine++ + } + } else if (node.isBlock) { + // 块级节点(如段落、标题)默认占一行 + if (currentLine === lineNumber) { + targetPos = pos + 1 // 跳过节点开始位置 + return false + } + currentLine++ + } + return true // 继续遍历子节点 + }) + + // 设置光标位置 + if (targetPos > 0 && targetPos <= doc.content.size) { + const tr = view.state.tr.setSelection(TextSelection.near(view.state.doc.resolve(targetPos))) + view.dispatch(tr) + view.focus() + // 滚动到目标位置 + view.dom.scrollIntoView({ + behavior: "smooth", + block: "center" + }) + } + } + // ProseMirror 插件 + const plugin = new Plugin({ + key: jumpToLinePluginKey, + view: (view) => { + // jumpToLine(view, line) + return { + update: (view, prevState) => { + jumpToLine(view, line) + } + } + } + }) + + return plugin + }) diff --git a/src/compoments/MilkdownEditor/utils/listPlugin.ts b/src/compoments/MilkdownEditor/utils/listPlugin.ts new file mode 100644 index 00000000..e97ccfed --- /dev/null +++ b/src/compoments/MilkdownEditor/utils/listPlugin.ts @@ -0,0 +1,21 @@ +import {$command} from "@milkdown/utils" +import {convertSelectionByNode, setWrapInBlockType} from "./utils" +import {listItemSchema, paragraphSchema} from "@milkdown/kit/preset/commonmark" + +export const listToParagraphCommand = $command(`listToParagraphCommand`, (ctx) => () => { + return (state, dispatch) => { + const command = convertSelectionByNode(paragraphSchema.type(ctx)) + return command(state, dispatch) + } +}) + +export const convertToListBullet = $command(`convertToListBullet`, (ctx) => () => { + return (state, dispatch) => { + const command = setWrapInBlockType(listItemSchema.type(ctx), {checked: false}) + return command(state, dispatch) + } +}) + +export const listCustomPlugin = () => { + return [listToParagraphCommand, convertToListBullet] +} diff --git a/src/compoments/MilkdownEditor/utils/markPlugin.ts b/src/compoments/MilkdownEditor/utils/markPlugin.ts new file mode 100644 index 00000000..c6808012 --- /dev/null +++ b/src/compoments/MilkdownEditor/utils/markPlugin.ts @@ -0,0 +1,64 @@ +import {$markAttr, $markSchema, $nodeSchema, $nodeAttr} from "@milkdown/kit/utils" +import {v4 as uuidv4} from "uuid" + +const markId = "mark-height-text" +const markMarkAttr = $markAttr(markId) + +const markSchema = $markSchema(markId, (ctx) => ({ + parseDOM: [ + { + tag: `mark[data-markjs=true]`, + getAttrs: (dom) => { + const id = uuidv4() + const attr = { + id: dom.getAttribute("id") || id, + class: dom.getAttribute("class") + } + return dom.nodeName === "MARK" + ? { + ...attr + } + : {} + } + } + ], + attrs: { + "data-markjs": {default: "true"}, + class: {default: ""}, + id: {default: ""} + }, + toDOM: (mark) => { + let attrs = { + ...mark.attrs + } + const target = document.getElementById(mark.attrs.id!)! + if (target) { + attrs = { + ...attrs, + class: target.getAttribute("class") + } + } + return ["mark", {...ctx.get(markMarkAttr.key)(mark), ...attrs}] + }, + parseMarkdown: { + match: (node) => { + return node.type === "textDirective" && node.name === "mark" + }, + runner: (state, node, markType) => { + if (markType.name === markId) { + state.addText("") + } + } + }, + + toMarkdown: { + match: (mark) => { + return mark.type.name === markId + }, + runner: (state, mark, node) => { + state.addNode("text", undefined, "") + } + } +})) + +export const markCustomPlugin = () => [markMarkAttr, markSchema.mark, markSchema.ctx] diff --git a/src/compoments/MilkdownEditor/utils/mentionPlugin.ts b/src/compoments/MilkdownEditor/utils/mentionPlugin.ts new file mode 100644 index 00000000..69bb4362 --- /dev/null +++ b/src/compoments/MilkdownEditor/utils/mentionPlugin.ts @@ -0,0 +1,112 @@ +import {$command, $nodeSchema, $nodeAttr} from "@milkdown/utils" +import {Attrs} from "@milkdown/kit/prose/model" +import {TextSelection} from "@milkdown/kit/prose/state" +import {v4 as uuidv4} from "uuid" +import moment from "moment" + +export const mentionCustomId = "mention-custom" + +const mentionCustomAttr = $nodeAttr("mention-custom", () => ({"data-type": mentionCustomId, "data-user-name": ""})) + +export const mentionCustomSchema = $nodeSchema(mentionCustomId, (ctx) => ({ + inline: true, + group: "inline", + content: "text*", + atom: true, + // 从 DOM 中解析节点,动态设置属性 + parseDOM: [ + { + tag: `div[data-type='${mentionCustomId}']`, + getAttrs: (dom) => { + return { + mentionId: dom.getAttribute("data-mention-id"), + userName: dom.getAttribute("data-user-name"), + userId: dom.getAttribute("data-user-id") + } + } + } + ], + + // 将节点转为 DOM 结构,动态设置样式 + toDOM: (node) => { + const className = `mention-custom` + return [ + "div", + { + ...ctx.get(mentionCustomAttr.key)(node), + class: className, + "data-mention-id": getMentionId(), + "data-user-name": node.attrs.userName, + "data-user-id": node.attrs.userId + }, + 0 + ] + }, + + parseMarkdown: { + match: (node) => { + const {type, name} = node + return type === "textDirective" && name === "mention" + }, + runner: (state, node, type) => { + if (type.name === mentionCustomId) { + state + .openNode(type, {...(node.attributes as Attrs)}) + .next(node.children) + .closeNode() + } + } + }, + + toMarkdown: { + match: (node) => { + return node.type.name === mentionCustomId + }, + runner: (state, node) => { + state + .openNode("textDirective", undefined, { + name: "mention", + attributes: { + mentionId: node.attrs.mentionId, + userName: node.attrs.userName, + userId: node.attrs.userId + } + }) + .next(node.content) + .closeNode() + } + }, + attrs: { + mentionId: {default: "0"}, + userName: {default: ""}, + userId: {default: ""} + } +})) + +/**生成提及单个组件id,用于跳转 */ +export const getMentionId = () => { + return `mention-${uuidv4()}-${moment().valueOf()}` +} +export const mentionCommand = $command(`command-${mentionCustomId}`, (ctx) => (params) => (state, dispatch) => { + const {selection, tr} = state + if (!(selection instanceof TextSelection)) return false + const {userName, userId, mentionId} = params as {userName: string; userId: string; mentionId?: string} + const {from} = selection + tr.deleteRange(from - 1, from) + const fragment = state.schema.text(`@${userName}`) + dispatch?.( + tr + .setMeta(mentionCustomId, true) + .replaceSelectionWith( + mentionCustomSchema + .type(ctx) + .create({mentionId: mentionId || getMentionId(), userName, userId}, fragment) + ) + .scrollIntoView() + ) + return true +}) + +export const mentionCustomPlugin = () => { + return [mentionCustomAttr, mentionCustomSchema.node, mentionCustomSchema.ctx, mentionCommand] +} diff --git a/src/compoments/MilkdownEditor/utils/trackDeletePlugin.ts b/src/compoments/MilkdownEditor/utils/trackDeletePlugin.ts new file mode 100644 index 00000000..1a840ab6 --- /dev/null +++ b/src/compoments/MilkdownEditor/utils/trackDeletePlugin.ts @@ -0,0 +1,112 @@ +import {Plugin, PluginKey} from "@milkdown/kit/prose/state" +import {$prose} from "@milkdown/kit/utils" +import {fileCustomId} from "./uploadPlugin" +import {createSlice} from "@milkdown/kit/ctx" +import moment from "moment" +import {DeleteOSSFileItem} from "../MilkdownEditorType" +import { ReplaceStep } from "@milkdown/kit/prose/transform" + +export const getFileNameByUrl = (url) => { + if (!url) return "" + const [name, path] = url.split("?")[0].split("/").reverse() + if (!name || !path) { + return "" + } + // const fileName = `${path}/${name}` + // return fileName + // 重要!!! 此处后端已处理,后续简化此逻辑 + return url +} + +/** + * 从 doc 中收集所有 OSS 文件 + */ +const collectOSSFiles = (doc, schema): Set => { + const set = new Set() + + doc.descendants((node) => { + let url = "" + try { + switch (node.type.name) { + case fileCustomId: + url = node.attrs?.fileId !== "0" ? node.attrs?.fileId : "" + break + + case schema.nodes.image?.name: + case schema.nodes["image-block"]?.name: + url = node.attrs?.src + break + + default: + break + } + } catch (error) {} + const fileName = getFileNameByUrl(url) + if (fileName) { + set.add(fileName) + } + }) + + return set +} + +export const deletedFileUrlsCtx = createSlice([], "deletedFileUrlsCtx") +// 自定义插件:查找删除的文件和图片的url +export const trackDeletePlugin = () => + $prose((ctx) => { + ctx.inject(deletedFileUrlsCtx) // 注入上下文 + // 在插件初始化时设置值 + ctx.set(deletedFileUrlsCtx, []) + return new Plugin({ + key: new PluginKey("MILKDOWN_PLUGIN_TRACK_DELETE"), + appendTransaction: (transactions, oldState, newState) => { + // doc 没变 + if (oldState.doc === newState.doc) return null + + // 不是用户真正删除行为,直接跳过 + const hasReplaceStep = transactions.some(tr => + tr.steps.some(step => step instanceof ReplaceStep) && tr.docChanged + ) + if (!hasReplaceStep) { + return null + } + + // 核心:语义 diff + const oldFiles = collectOSSFiles(oldState.doc, oldState.schema) + const newFiles = collectOSSFiles(newState.doc, newState.schema) + + // 真正删除的文件 + const deletedFiles = [...oldFiles].filter((f) => !newFiles.has(f)) + + // 被恢复的文件(undo) + const restoredFiles = [...newFiles].filter((f) => !oldFiles.has(f)) + + // ===================== + // 删除 + // ===================== + if (deletedFiles.length > 0) { + ctx.update(deletedFileUrlsCtx, (prev) => { + const exist = new Set(prev.map((i) => i.fileName)) + + const next = deletedFiles + .filter((f) => !exist.has(f)) + .map((fileName) => ({ + fileName, + time: moment().valueOf() + })) + + return [...prev, ...next] + }) + } + // ===================== + // 撤销恢复 + // ===================== + if (restoredFiles.length > 0) { + ctx.update(deletedFileUrlsCtx, (prev) => + prev.filter((item) => !restoredFiles.includes(item.fileName)) + ) + } + return null + } + }) + }) diff --git a/src/compoments/MilkdownEditor/utils/underline.ts b/src/compoments/MilkdownEditor/utils/underline.ts new file mode 100644 index 00000000..e9727b31 --- /dev/null +++ b/src/compoments/MilkdownEditor/utils/underline.ts @@ -0,0 +1,59 @@ +import {toggleMark} from "@milkdown/prose/commands" +import {$inputRule, $markAttr, $markSchema, $command} from "@milkdown/kit/utils" +import {markRule} from "@milkdown/kit/prose" + +const underlineId = "underline" +const underlineMarkAttr = $markAttr(underlineId) + +const underlineSchema = $markSchema(underlineId, (ctx) => ({ + parseDOM: [ + { + tag: `u`, + getAttrs: (dom) => { + // 确保从 DOM 中提取属性并且解析 标签为 underline mark + return dom.nodeName === "U" ? {} : false + } + } + ], + + toDOM: (mark) => { + return ["u", {...ctx.get(underlineMarkAttr.key)(mark), ...mark.attrs}] + }, + parseMarkdown: { + match: (node) => { + return node.type === "textDirective" && node.name === "u" + }, + runner: (state, node, markType) => { + if (markType.name === underlineId) { + state.openMark(markType).next(node.children).closeMark(markType) + } + } + }, + + toMarkdown: { + match: (mark) => { + return mark.type.name === underlineId + }, + runner: (state, mark) => { + state.withMark(mark, "textDirective", undefined, { + name: "u", + attributes: { + marker: mark.attrs.marker + } + }) + } + } +})) +// /((.*?)<\/u>|:u\[(.+?)\])/ /(.*?)<\/u>/ +const underlineInputRule = $inputRule((ctx) => { + return markRule(/(?:\:u\[)([^*_]+)(?:\](?:\s|$))/, underlineSchema.type(ctx)) +}) + +export const underlineCommand = $command("toggleUnderlineCommand", (ctx) => () => toggleMark(underlineSchema.type(ctx))) +export const underlineCustomPlugin = () => [ + underlineMarkAttr, + underlineSchema.mark, + underlineSchema.ctx, + underlineCommand, + underlineInputRule +] diff --git a/src/compoments/MilkdownEditor/utils/uploadPlugin.ts b/src/compoments/MilkdownEditor/utils/uploadPlugin.ts new file mode 100644 index 00000000..db3d40ef --- /dev/null +++ b/src/compoments/MilkdownEditor/utils/uploadPlugin.ts @@ -0,0 +1,105 @@ +import {$command, $nodeSchema, $nodeAttr} from "@milkdown/utils" +import {Attrs} from "@milkdown/kit/prose/model" +import {TextSelection} from "@milkdown/kit/prose/state" + +export const fileCustomId = "file-custom" +export const fileCustomAttr = $nodeAttr("file-custom", () => ({ + "data-type": fileCustomId, + contenteditable: "false" +})) + +export const fileCustomSchema = $nodeSchema(fileCustomId, (ctx) => ({ + group: "block", + content: "", + atom: true, + draggable: false, + isolating: true, // 阻止其他节点进入这个节点, + // 从 DOM 中解析节点,动态设置属性 + parseDOM: [ + { + tag: `div[data-type='${fileCustomId}']`, + getAttrs: (dom) => { + return { + fileId: dom.getAttribute("data-file-id"), + path: dom.getAttribute("data-path"), + notepadHash: dom.getAttribute("data-notepad-hash"), + uploadUserId: dom.getAttribute("data-upload-user-id") || 0 + } + } + } + ], + + // 将节点转为 DOM 结构,动态设置样式 + toDOM: (node) => { + const attrs = { + ...ctx.get(fileCustomAttr.key)(node), + "data-file-id": node.attrs.fileId, + "data-path": node.attrs.path, + "data-notepad-hash": node.attrs.notepadHash, + "data-upload-user-id": node.attrs.uploadUserId + } + return ["div", {...attrs}] + }, + + parseMarkdown: { + match: (node) => { + const {type, name} = node + return type === "containerDirective" && name === "file" + }, + runner: (state, node, type) => { + if (type.name === fileCustomId) { + state + .openNode(type, {...(node.attributes as Attrs)}) + .next(node.children) + .closeNode() + } + } + }, + + toMarkdown: { + match: (node) => { + return node.type.name === fileCustomId + }, + runner: (state, node) => { + if (node.attrs.fileId) { + state + .openNode("containerDirective", undefined, { + name: "file", + attributes: { + fileId: node.attrs.fileId, + notepadHash: node.attrs.notepadHash, + path: node.attrs.path, + uploadUserId: node.attrs.uploadUserId + } + }) + .next(node.content) + .closeNode() + } + } + }, + attrs: { + fileId: {default: "0"}, + path: {default: ""}, + notepadHash: {default: ""}, + uploadUserId: {default: 0} + } +})) + +export const fileCommand = $command(`command-${fileCustomId}`, (ctx) => (props: any) => (state, dispatch) => { + const {selection, tr} = state + if (!(selection instanceof TextSelection)) return false + + const {from, to} = selection + const fragment = state.doc.slice(from, to).content // 获取 Fragment + dispatch?.( + tr + .setMeta(fileCustomId, true) + .replaceSelectionWith(fileCustomSchema.type(ctx).create(props as Attrs, fragment)) + .scrollIntoView() + ) + return true +}) + +export const uploadCustomPlugin = () => { + return [fileCustomAttr, fileCustomSchema.node, fileCustomSchema.ctx, fileCommand] +} diff --git a/src/compoments/MilkdownEditor/utils/utils.ts b/src/compoments/MilkdownEditor/utils/utils.ts new file mode 100644 index 00000000..0c19ba6a --- /dev/null +++ b/src/compoments/MilkdownEditor/utils/utils.ts @@ -0,0 +1,297 @@ +import {yakitNotify} from "@/utils/notification" +import {Ctx} from "@milkdown/kit/ctx" +import { + blockquoteSchema, + bulletListSchema, + codeBlockSchema, + headingSchema, + hrSchema, + listItemSchema, + orderedListSchema, + paragraphSchema +} from "@milkdown/kit/preset/commonmark" +import {NodeType, Attrs} from "@milkdown/kit/prose/model" +import {Command, Transaction} from "@milkdown/kit/prose/state" +import {findWrapping} from "@milkdown/kit/prose/transform" +import {EditorView} from "@milkdown/kit/prose/view" +import {alterCustomSchema} from "./alertPlugin" +import {getLocalFileLinkInfo} from "../CustomFile/utils" +import {ImgMaxSize} from "@/pages/pluginEditor/pluginImageTextarea/PluginImageTextarea" +import {HttpUploadImgBaseRequest, httpUploadImgPath} from "@/apiUtils/http" +import {callCommand} from "@milkdown/kit/utils" +import {insertImageBlockCommand} from "./imageBlock" +import {fileCommand} from "./uploadPlugin" +import { handleOpenFileSystemDialog } from "@/utils/fileSystemDialog" + +const {ipcRenderer} = window.require("electron") + +export const clearContentAndSetBlockType = (nodeType: NodeType, attrs: Attrs | null = null): Command => { + return (state, dispatch) => { + if (dispatch) { + const tr = setBlockTypeCustom(clearRange(state.tr), nodeType, attrs) + dispatch(tr.scrollIntoView()) + } + return true + } +} + +const setBlockTypeCustom = (tr: Transaction, nodeType: NodeType, attrs: Attrs | null = null) => { + const {from, to} = tr.selection + return tr.setBlockType(from, to, nodeType, attrs) +} + +const clearRange = (tr: Transaction) => { + const {$from, $to} = tr.selection + const {pos: from} = $from + const {pos: to} = $to + tr = tr.deleteRange(from - $from.node().content.size, to) + return tr +} + +export const clearContentAndAddBlockType = (nodeType: NodeType, attrs: Attrs | null = null): Command => { + return (state, dispatch) => { + const tr = addBlockType(clearRange(state.tr), nodeType, attrs) + if (!tr) return false + + if (dispatch) dispatch(tr.scrollIntoView()) + + return true + } +} + +export const addBlockType = (tr: Transaction, nodeType: NodeType, attrs: Attrs | null = null) => { + const node = nodeType.createAndFill(attrs) + if (!node) return null + + return tr.replaceSelectionWith(node) +} + +export const setWrapInBlockType = (nodeType: NodeType, attrs: Attrs | null = null): Command => { + return (state, dispatch) => { + const tr = wrapInBlockType(state.tr, nodeType, attrs) + if (!tr) return false + + if (dispatch) dispatch(tr.scrollIntoView()) + + return true + } +} + +const wrapInBlockType = (tr: Transaction, nodeType: NodeType, attrs: Attrs | null = null) => { + const {$from, $to} = tr.selection + + const range = $from.blockRange($to) + const wrapping = range && findWrapping(range, nodeType, attrs) + if (!wrapping) return null + + return tr.wrap(range, wrapping) +} + +export const clearContentAndWrapInBlockType = (nodeType: NodeType, attrs: Attrs | null = null): Command => { + return (state, dispatch) => { + const tr = wrapInBlockType(clearRange(state.tr), nodeType, attrs) + if (!tr) return false + + if (dispatch) dispatch(tr.scrollIntoView()) + + return true + } +} + +/** + * @description 将选区转换为指定节点类型,未做更多的适配 + * @param nodeType + * @param attrs + * @returns {boolean} + */ +export const convertSelectionByNode = (nodeType: NodeType, attrs: Attrs | null = null): Command => { + return (state, dispatch) => { + try { + const {$from, from, to} = state.selection + // 检查选区是否有效 + if (from === to) { + return false // 没有选中任何内容 + } + // 获取父节点类型 + const parentNode = $from.node(-1) // 使用 -1 获取上一级节点 + + if (parentNode && parentNode.type.name === state.schema.nodes.list_item.name) { + let textNode = state.schema.text(parentNode.textContent) + if (parentNode.attrs?.listType === "ordered" && nodeType.name === state.schema.nodes.heading.name) { + // 有序列表转为标题的时候需要把序号带上 + textNode = state.schema.text(`${parentNode.attrs.label || ""}${parentNode.textContent}`) + } + + // 创建新的节点,并用它替换列表项 + const node = state.schema.nodes[nodeType.name].create( + attrs, // attrs属性 + textNode // 使用列表项的内容填充 + ) + + // 使用 transaction 将列表项替换为 + const tr = state.tr.replaceRangeWith($from.before(-1), $from.after(-1), node) + + if (dispatch) dispatch(tr) + + return true + } + return false + } catch (error) { + yakitNotify("error", `[${nodeType.name}]convertSelectionByNode执行失败:${error}`) + return false + } + } +} + +/**生成空白 正文 */ +export const createBlankText = (action: (fn: (ctx: Ctx) => void) => void, view: EditorView) => { + const {dispatch, state} = view + action((ctx) => { + const command = clearContentAndSetBlockType(paragraphSchema.type(ctx)) + command(state, dispatch) + }) +} + +/**生成空白 一级标题 */ +export const createBlankHeading1 = (action: (fn: (ctx: Ctx) => void) => void, view: EditorView) => { + const {dispatch, state} = view + action((ctx) => { + const command = clearContentAndSetBlockType(headingSchema.type(ctx), {level: 1}) + command(state, dispatch) + }) +} + +/**生成空白 二级标题 */ +export const createBlankHeading2 = (action: (fn: (ctx: Ctx) => void) => void, view: EditorView) => { + const {dispatch, state} = view + action((ctx) => { + const command = clearContentAndSetBlockType(headingSchema.type(ctx), {level: 2}) + command(state, dispatch) + }) +} + +/**生成空白 三级标题 */ +export const createBlankHeading3 = (action: (fn: (ctx: Ctx) => void) => void, view: EditorView) => { + const {dispatch, state} = view + action((ctx) => { + const command = clearContentAndSetBlockType(headingSchema.type(ctx), {level: 3}) + command(state, dispatch) + }) +} + +/**生成空白 有序列表 */ +export const createBlankOrderedList = (action: (fn: (ctx: Ctx) => void) => void, view: EditorView) => { + const {dispatch, state} = view + action((ctx) => { + const command = clearContentAndWrapInBlockType(orderedListSchema.type(ctx)) + command(state, dispatch) + }) +} + +/**生成空白 无序列表 */ +export const createBlankUnorderedList = (action: (fn: (ctx: Ctx) => void) => void, view: EditorView) => { + const {dispatch, state} = view + action((ctx) => { + const command = clearContentAndWrapInBlockType(bulletListSchema.type(ctx)) + command(state, dispatch) + }) +} + +/**生成空白 任务/可勾选框列表 */ +export const createBlankTask = (action: (fn: (ctx: Ctx) => void) => void, view: EditorView) => { + const {dispatch, state} = view + action((ctx) => { + const command = clearContentAndWrapInBlockType(listItemSchema.type(ctx), {checked: false}) + command(state, dispatch) + }) +} + +/**生成空白 代码块 */ +export const createBlankCodeBlock = (action: (fn: (ctx: Ctx) => void) => void, view: EditorView) => { + const {dispatch, state} = view + action((ctx) => { + const command = clearContentAndAddBlockType(codeBlockSchema.type(ctx)) + command(state, dispatch) + }) +} + +/**生成空白 引用 */ +export const createBlankQuote = (action: (fn: (ctx: Ctx) => void) => void, view: EditorView) => { + const {dispatch, state} = view + action((ctx) => { + const command = clearContentAndWrapInBlockType(blockquoteSchema.type(ctx)) + command(state, dispatch) + }) +} + +/**生成空白 高亮 */ +export const createBlankHighLight = (action: (fn: (ctx: Ctx) => void) => void, view: EditorView) => { + const {dispatch, state} = view + action((ctx) => { + const command = clearContentAndWrapInBlockType(alterCustomSchema.type(ctx)) + command(state, dispatch) + }) +} + +/**生成 分割线 */ +export const createDivider = (action: (fn: (ctx: Ctx) => void) => void, view: EditorView) => { + const {dispatch, state} = view + action((ctx) => { + const command = clearContentAndAddBlockType(hrSchema.type(ctx)) + command(state, dispatch) + }) +} + +const FileMaxSize = 1024 * 1024 * 1024 +const imgTypes = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", ".webp", ".svg"] + +/**上传图片和文件 */ +export const uploadFileInMilkdown = ( + action: (fn: (ctx: Ctx) => void) => void, + option: {type: HttpUploadImgBaseRequest["type"]; notepadHash: string; userId: number} +) => { + const {type, notepadHash, userId} = option + handleOpenFileSystemDialog({title: "请选择文件", properties: ["openFile"]}) + .then((data) => { + const filesLength = data.filePaths.length + if (filesLength) { + const path = data.filePaths[0].replace(/\\/g, "\\") + getLocalFileLinkInfo(path).then((res) => { + if (res.size > FileMaxSize) { + yakitNotify("error", "文件大小不能超过1G") + return + } + const index = path.lastIndexOf(".") + const fileType = path.substring(index, path.length) + if (imgTypes.includes(fileType)) { + if (res.size > ImgMaxSize) { + yakitNotify("error", "图片大小不能超过1M") + return + } + httpUploadImgPath({path, type, filedHash: notepadHash}) + .then((src) => { + action( + callCommand(insertImageBlockCommand.key, { + src, + alt: path, + title: "" + }) + ) + }) + .catch((e) => { + yakitNotify("error", `上传图片失败:${e}`) + }) + } else { + action( + callCommand(fileCommand.key, { + fileId: "0", + path, + notepadHash, + uploadUserId: userId + }) + ) + } + }) + } + }) +} diff --git a/src/compoments/NoLoginPermission/index.tsx b/src/compoments/NoLoginPermission/index.tsx index be73f19f..b645db1d 100644 --- a/src/compoments/NoLoginPermission/index.tsx +++ b/src/compoments/NoLoginPermission/index.tsx @@ -5,11 +5,17 @@ import showErrorMessage from '@/utils/showErrorMessage'; import { useNavigate } from 'react-router-dom'; import { useRequest } from 'ahooks'; import { getLicense } from '@/apis/login'; +import { resolveLicenseGateValue, shouldBypassLicense } from '@/utils/license'; const NoLoginPermission: FC = () => { const navigate = useNavigate(); + const bypassLicense = shouldBypassLicense(); const headLogin = () => { + if (bypassLicense) { + navigate('/login', { replace: true }); + return; + } runAsync(); }; @@ -17,7 +23,7 @@ const NoLoginPermission: FC = () => { async () => { const { data } = await getLicense(); const { license } = data; - return license?.length > 0 ? license : undefined; + return resolveLicenseGateValue(license); }, { manual: true, diff --git a/src/compoments/Placeholder.tsx b/src/compoments/Placeholder.tsx new file mode 100644 index 00000000..d4b9baab --- /dev/null +++ b/src/compoments/Placeholder.tsx @@ -0,0 +1,82 @@ +import { $ctx, $prose } from '@milkdown/kit/utils'; +import type { EditorState, Selection } from '@milkdown/kit/prose/state'; +import { Plugin, PluginKey } from '@milkdown/kit/prose/state'; +import type { Node } from '@milkdown/kit/prose/model'; +import { Decoration, DecorationSet } from '@milkdown/kit/prose/view'; +import { findParent } from '@milkdown/kit/prose'; + +const isInCodeBlock = (selection: Selection) => { + const type = selection.$from.parent.type; + return type.name === 'code_block'; +}; + +const isInList = (selection: Selection) => { + const type = selection.$from.node(selection.$from.depth - 1)?.type; + return type?.name === 'list_item'; +}; + +const isDocEmpty = (doc: Node) => { + return doc.childCount <= 1 && !doc.firstChild?.content.size; +}; + +function createPlaceholderDecoration( + state: EditorState, + placeholderText: string, +): Decoration | null { + const { selection } = state; + if (!selection.empty) return null; + + const $pos = selection.$anchor; + const node = $pos.parent; + if (node.content.size > 0) return null; + + const inTable = findParent((node) => node.type.name === 'table')($pos); + if (inTable) return null; + + const before = $pos.before(); + + return Decoration.node(before, before + node.nodeSize, { + class: 'crepe-placeholder', + 'data-placeholder': placeholderText, + }); +} + +interface PlaceholderConfig { + text: string; + mode: 'doc' | 'block'; +} + +export type PlaceHolderFeatureConfig = Partial; + +export const placeholderConfig = $ctx( + { + text: 'Please enter...', + mode: 'block', + } satisfies PlaceholderConfig, + 'placeholderConfigCtx', +); + +export const placeholderPlugin = $prose((ctx) => { + return new Plugin({ + key: new PluginKey('CREPE_PLACEHOLDER'), + props: { + decorations: (state) => { + const config = ctx.get(placeholderConfig.key) as any; + if (config.mode === 'doc' && !isDocEmpty(state.doc)) + return null; + + if (isInCodeBlock(state.selection) || isInList(state.selection)) + return null; + + const placeholderText = config.text ?? 'Please enter...'; + const deco = createPlaceholderDecoration( + state, + placeholderText, + ); + if (!deco) return null; + + return DecorationSet.create(state.doc, [deco]); + }, + }, + }); +}); diff --git a/src/compoments/RollingLoadList/RollingLoadList.tsx b/src/compoments/RollingLoadList/RollingLoadList.tsx index d32a4ce9..cff2ae4c 100644 --- a/src/compoments/RollingLoadList/RollingLoadList.tsx +++ b/src/compoments/RollingLoadList/RollingLoadList.tsx @@ -1,9 +1,11 @@ +import type { ReactNode } from 'react'; import React, { useEffect, useState, useRef, - type ReactNode, useMemo, + forwardRef, + useImperativeHandle, } from 'react'; import ReactResizeDetector from 'react-resize-detector'; import { @@ -17,6 +19,7 @@ import { LoadingOutlined } from '@ant-design/icons'; import './RollingLoadList.scss'; export interface RollingLoadListProps { + ref?: React.ForwardedRef; rowKey?: string; data: T[]; loadMoreData: () => void; @@ -37,6 +40,10 @@ export interface RollingLoadListProps { targetRef?: React.RefObject; } +export interface RollingLoadListRef { + containerRef: React.RefObject | null; + scrollTo: (index: number) => void; +} const classNameWidth: any = { 2: 'width-50', 3: 'width-33', @@ -44,7 +51,10 @@ const classNameWidth: any = { 5: 'width-20', }; -export const RollingLoadList = (props: RollingLoadListProps) => { +function RollingLoadListInner( + props: RollingLoadListProps, + ref: React.ForwardedRef, +) { const { data, loadMoreData, @@ -67,11 +77,20 @@ export const RollingLoadList = (props: RollingLoadListProps) => { const [vlistHeigth, setVListHeight] = useState(600); const [col, setCol] = useState(); const [computeOriginalList, setComputeOriginalList] = useState(false); - const containerRef = useRef(null); - const wrapperRef = useRef(null); + const containerRef = useRef(null); + const wrapperRef = useRef(null); let indexMapRef = useRef>(new Map()); let preLength = useRef(0); let preData = useRef([]); + useImperativeHandle( + ref, + () => ({ + containerRef, + scrollTo, + }), + [], + ); + const resetPre = useMemoizedFn(() => { preLength.current = 0; preData.current = []; @@ -123,7 +142,7 @@ export const RollingLoadList = (props: RollingLoadListProps) => { if (!hasMore) return; if (!containerRef || !wrapperRef) return; // wrapperRef 中的数据没有铺满 containerRef,那么就要请求更多的数据 - const containerHeight = containerRef.current?.clientHeight; + const containerHeight = containerRef.current?.clientHeight || 0; const wrapperHeight = wrapperRef.current?.clientHeight; if (wrapperHeight && wrapperHeight <= containerHeight) { loadMoreData(); @@ -197,7 +216,7 @@ export const RollingLoadList = (props: RollingLoadListProps) => { }; const contentScrollTop = dom.scrollTop; // 滚动条距离顶部 const clientHeight = dom.clientHeight; // 可视区域 - const scrollHeight = dom.scrollHeight; // 滚动条内容的总高度 + const scrollHeight = dom.scrollHeight; // 滚动条内容的总高度 const scrollBottom = scrollHeight - contentScrollTop - clientHeight; if (scrollBottom <= 500) { @@ -298,4 +317,9 @@ export const RollingLoadList = (props: RollingLoadListProps) => {
    ); -}; +} +export const RollingLoadList = forwardRef(RollingLoadListInner) as ( + props: RollingLoadListProps & { + ref?: React.ForwardedRef; + }, +) => JSX.Element; diff --git a/src/compoments/TableVirtualResize/styleFunction.scss b/src/compoments/TableVirtualResize/styleFunction.scss index 880f288d..804af773 100644 --- a/src/compoments/TableVirtualResize/styleFunction.scss +++ b/src/compoments/TableVirtualResize/styleFunction.scss @@ -1,13 +1,13 @@ // 颜色键值对——键一定是有序添加 $allColors: ( - blue: var(--yakit-tag-color-blue), - cyan: var(--yakit-tag-color-cyan), - green: var(--yakit-tag-color-green), - grey: var(--yakit-helper-text-color), - orange: var(--yakit-tag-color-orange), - purple: var(--yakit-tag-color-purple), - red: var(--yakit-tag-color-red), - yellow: var(--yakit-tag-color-yellow) + "blue": var(--yakit-tag-color-blue), + "cyan": var(--yakit-tag-color-cyan), + "green": var(--yakit-tag-color-green), + "grey": var(--yakit-helper-text-color), + "orange": var(--yakit-tag-color-orange), + "purple": var(--yakit-tag-color-purple), + "red": var(--yakit-tag-color-red), + "yellow": var(--yakit-tag-color-yellow) ); // 获取全部颜色集合里的颜色 @@ -73,7 +73,7 @@ $allColors: ( $pre: append($prefix, $type, $separator: comma); @if length($pre) == 1 { // 单色情况 - @if $type == red { + @if $type == "red" { // 单色-红色单独处理 .virtual-table-cell-red { background: var(--yakit-table-select-row-bg-color-red); diff --git a/src/compoments/WebTree/WebTree.module.scss b/src/compoments/WebTree/WebTree.module.scss new file mode 100644 index 00000000..e5311702 --- /dev/null +++ b/src/compoments/WebTree/WebTree.module.scss @@ -0,0 +1,197 @@ +.webTree { + display: flex; + flex-direction: column; + .tree-top-wrap { + display: flex; + justify-content: space-between; + } + + .tree-wrap { + flex: 1; + } + + :global { + .ant-tree-list-holder-inner{ + background-color: var(--Colors-Use-Basic-Background); + } + .yakitTreeNode-icon { + color: var(--Colors-Use-Neutral-Text-3-Secondary); + margin-top: 4px; + svg { + height: 16px; + width: 16px; + path { + stroke-width: 1.5; + } + } + } + + .yakit-flolder-icon { + color: var(--Colors-Use-Main-Primary); + } + + .ant-spin-spinning { + margin-top: 20px; + .ant-spin-dot { + margin: auto; + } + } + + .ant-tree-indent-unit { + width: 10px; + background: var(--Colors-Use-Basic-Background); + &::before { + right: 0px; + background: var(--Colors-Use-Basic-Background); + } + } + .ant-tree-show-line .ant-tree-indent-unit::before { + border-right: 1px solid var(--Colors-Use-Neutral-Border); + } + .ant-tree-indent-unit-end { + border-right: 1px solid var(--Colors-Use-Neutral-Border); + } + .ant-tree-switcher { + width: 22px; + background: var(--Colors-Use-Basic-Background); + } + .ant-tree-switcher-leaf-line { + display: none; + } + + .ant-tree .ant-tree-treenode { + padding: 0; + background: var(--Colors-Use-Basic-Background); + } + + .ant-tree-treenode.ant-tree-treenode-switcher-open:not(.ant-tree-treenode-selected), + .ant-tree-treenode.ant-tree-treenode-switcher-close:not(.ant-tree-treenode-selected) { + align-items: center; + width: 100%; + padding: 0; + background-color: var(--Colors-Use-Basic-Background); + &:hover:not(:has(.ant-tree-checkbox-checked)) { + .ant-tree-switcher { + background-color: var(--Colors-Use-Neutral-Bg); + border-radius: 4px 0 0 4px; + } + + } + } + + .ant-tree-treenode:not(.ant-tree-treenode-selected) { + &:hover:not(:has(.ant-tree-checkbox-checked)) { + .ant-tree-switcher.ant-tree-switcher-noop { + background-color: var(--Colors-Use-Neutral-Bg-Hover); + border-radius: 4px 0 0 4px; + } + .ant-tree-node-content-wrapper { + background-color: var(--Colors-Use-Neutral-Bg-Hover); + border-radius: 0 4px 4px 0; + } + } + } + + .ant-tree-node-content-wrapper { + padding-left: 0; + transition: none; + } + + .ant-tree-node-content-wrapper.ant-tree-node-selected { + .ant-tree-iconEle .anticon { + color: var(--Colors-Use-Main-On-Primary); + } + .ant-tree-title { + white-space: nowrap; + width: calc(100% - 24px); + color: var(--Colors-Use-Main-On-Primary); + } + } + + .ant-tree-treenode.ant-tree-treenode-switcher-close.ant-tree-treenode-selected, + .ant-tree-treenode.ant-tree-treenode-switcher-open.ant-tree-treenode-selected { + .ant-tree-switcher { + background-color: var(--Colors-Use-Main-Pressed); + border-radius: 4px 0 0 4px; + .ant-tree-switcher-icon { + color: var(--Colors-Use-Main-On-Primary); + } + } + + & > .ant-tree-node-content-wrapper.ant-tree-node-selected { + border-radius: 0 4px 4px 0; + } + + &:hover { + .ant-tree-switcher { + background-color: var(--Colors-Use-Main-Primary); + color: var(--Colors-Use-Main-On-Primary); + } + .ant-tree-node-content-wrapper.ant-tree-node-selected { + background-color: var(--Colors-Use-Main-Hover); + } + } + } + + .ant-tree-treenode.ant-tree-treenode-selected { + &:hover { + .ant-tree-switcher.ant-tree-switcher-noop { + background-color: var(--Colors-Use-Main-Hover); + } + .ant-tree-node-content-wrapper.ant-tree-node-selected { + background-color: var(--Colors-Use-Main-Hover); + } + } + .ant-tree-switcher.ant-tree-switcher-noop { + background-color: var(--Colors-Use-Main-Primary); + color: var(--Colors-Use-Main-On-Primary); + border-radius: 4px 0 0 4px; + } + } + + .ant-tree-iconEle { + margin-right: 2px; + } + + .ant-tree-node-content-wrapper { + display: flex; + width: 100%; + border-radius: 0 4px 4px 0; + } + + .ant-tree-title { + white-space: nowrap; + width: calc(100% - 24px); + color: var(--Colors-Use-Neutral-Text-1-Title); + } + + //checkbox放到最前面 + .ant-tree-treenode { + margin-bottom: 1px; + align-items: center; + .ant-tree-checkbox{ + margin-top: 0; + order: -1; + } + + // 让勾选的节点显示样式和选中一样 + &:has(.ant-tree-checkbox-checked) { + .ant-tree-node-content-wrapper{ + background-color: var(--Colors-Use-Main-Primary); + + span { + color: var(--Colors-Use-Main-On-Primary); + } + } + .ant-tree-switcher { + background-color: var(--Colors-Use-Main-Pressed); + border-radius: 4px 0 0 4px; + } + .ant-tree-node-content-wrapper,.ant-tree-switcher svg{ + color: var(--Colors-Use-Main-On-Primary) + } + } + } + + } +} diff --git a/src/compoments/WebTree/WebTree.tsx b/src/compoments/WebTree/WebTree.tsx new file mode 100644 index 00000000..b276ded2 --- /dev/null +++ b/src/compoments/WebTree/WebTree.tsx @@ -0,0 +1,452 @@ +import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'; +import type { TreeKey } from '../yakitUI/YakitTree/YakitTree'; +import YakitTree from '../yakitUI/YakitTree/YakitTree'; +import type { DataNode } from 'antd/es/tree'; +import { + useDebounceEffect, + useDebounceFn, + useInViewport, + useMemoizedFn, +} from 'ahooks'; +import { + OutlineDocumentIcon, + OutlineFolderremoveIcon, + OutlineLink2Icon, + OutlineVariableIcon, +} from '@/assets/icon/outline'; +import { yakitFailed } from '@/utils/notification'; +import { + SolidFolderIcon, + SolidFolderaddIcon, + SolidFolderopenIcon, +} from '@/assets/icon/solid'; +import { YakitInput } from '../yakitUI/YakitInput/YakitInput'; +import { YakitButton } from '../yakitUI/YakitButton/YakitButton'; +import { RefreshIcon } from '@/assets/newIcon'; +import { YakitSpin } from '../yakitUI/YakitSpin/YakitSpin'; +import styles from './WebTree.module.scss'; +import { requestYakURLList, YakURL } from '@/pages/AIAgent/utils/netif'; +import type { YakURLResource } from '@/pages/YakRunnerAuditCode/utils'; +import { useGetSetState } from '@/hooks'; + +type TreeNodeType = 'dir' | 'file' | 'query' | 'path'; +export interface TreeNode extends DataNode { + data?: YakURLResource; // 树节点其他额外数据 +} + +interface WebTreeProp { + ref?: React.Ref; + height: number; // 树高度 用于虚拟滚动 + searchInputDisabled?: boolean; + searchPlaceholder?: string; // 搜索框提示文案 + treeExtraQueryparams: string; // 树查询参数是一个json字符串 + refreshTreeFlag?: boolean; // 选中树节点后 表格参数改变导致树查询参数改变 是否需要刷新树 默认->不刷新 + onSelectNodesKeys: (selectedKeys: TreeKey[]) => void; // 选中节点 处理过后的 keys + /** runtime-id 网站树的过滤条件(runtime_id) */ + runTimeId?: string; + /** 多选 */ + multiple?: boolean; +} + +export const WebTree: React.FC = React.forwardRef((props, ref) => { + const { + height, + searchInputDisabled = false, + searchPlaceholder, + treeExtraQueryparams, + refreshTreeFlag = false, + onSelectNodesKeys, + runTimeId = '', + multiple = false, + } = props; + const [treeLoading, setTreeLoading] = useState(false); + // 未搜索情况时的网站树 + const [webTreeData, setWebTreeData] = useState([]); + // 已搜索情况时的网站树 + const [searchWebTreeData, setSearchWebTreeData] = useState([]); + /** 判断当前是否为已搜索的情况 */ + const searchTreeFlag = useRef(false); + + const [searchValue, setSearchValue, getSearchValue] = + useGetSetState(''); + const [expandedKeys, setExpandedKeys] = useState([]); // 展开树节点key集合 + const [selectedKeys, setSelectedKeys, getSelectedKeys] = useGetSetState< + TreeKey[] + >([]); // select树节点key集合 + const [selectedNodes, setSelectedNodes] = useGetSetState([]); // select树节点数据集合 + const webTreeRef = useRef(); + const [inViewport] = useInViewport(webTreeRef); + + useImperativeHandle(ref, () => ({ + onJumpWebTree, + })); + + const renderTreeNodeIcon = (treeNodeType: TreeNodeType) => { + const iconsEle = { + file: , + query: , + path: , + }; + return iconsEle[treeNodeType] || null; + }; + + const getTreeData = useMemoizedFn((searchKeyword = '') => { + if (treeLoading) return; + + // 由于这里会有闭包 30毫秒后再掉接口 + setTreeLoading(true); + setTimeout(() => { + let urlObj: YakURL; + const query: Array<{ Key: string; Value: string }> = []; + if (searchTreeFlag.current) { + setSearchWebTreeData([]); + query.push({ Key: 'search', Value: '1' }); + urlObj = { + FromRaw: '', + Schema: 'website', + User: '', + Pass: '', + Location: searchKeyword.replace(/^\/+/, ''), + Path: '/', + Query: [], + }; + } else { + setWebTreeData([]); + urlObj = { + FromRaw: '', + Schema: 'website', + User: '', + Pass: '', + Location: '', + Path: '///', + Query: [], + }; + } + + if (treeExtraQueryparams) { + query.push({ Key: 'params', Value: treeExtraQueryparams }); + } + + if (runTimeId) { + query.push({ Key: 'runtime_id', Value: runTimeId }); + } + + requestYakURLList( + { + ...urlObj, + Query: [...query], + }, + (res) => { + // 判断是否是搜索树 + if (searchTreeFlag.current) { + setSearchWebTreeData( + assembleFirstTreeNode(res.Resources), + ); + } else { + setWebTreeData(assembleFirstTreeNode(res.Resources)); + } + setTimeout(() => { + setTreeLoading(false); + }, 50); + }, + ).catch((error) => { + setTreeLoading(false); + yakitFailed( + `${t('YakitNotification.loadFailed', { colon: true })}${error}`, + ); + }); + }, 30); + }); + + // 树节点第一层组装树 + const assembleFirstTreeNode = (arr: YakURLResource[]) => { + return arr.map((item, index) => { + const id = item.VerboseName; + return { + title: item.VerboseName, + key: id, + isLeaf: !item.HaveChildrenNodes, + data: item, + icon: ({ expanded }) => { + if (item.ResourceType === 'dir') { + return expanded ? ( + + ) : ( + + ); + } + return renderTreeNodeIcon( + item.ResourceType as TreeNodeType, + ); + }, + } as TreeNode; + }); + }; + + const refreshChildrenByParent = useMemoizedFn( + (origin: TreeNode[], parentKey: string, nodes: TreeNode[]) => { + const arr: TreeNode[] = origin.map((node) => { + if (node.key === parentKey) { + return { + ...node, + children: nodes, + } as TreeNode; + } + if (node.children) { + return { + ...node, + children: refreshChildrenByParent( + node.children, + parentKey, + nodes, + ), + } as TreeNode; + } + return node; + }); + return arr; + }, + ); + + // 树子节点异步加载组装树 + const onLoadWebTreeData = ({ key, children, data }: any) => { + return new Promise((resolve, reject) => { + if (data === undefined) { + reject('node.data is empty'); + return; + } + const obj: YakURL = { ...data.Url, Query: [] }; + if (treeExtraQueryparams) { + obj.Query.push({ Key: 'params', Value: treeExtraQueryparams }); + } + + if (key.startsWith('https://')) { + obj.Query.push({ Key: 'schema', Value: 'https' }); + } else if (key.startsWith('http://')) { + obj.Query.push({ Key: 'schema', Value: 'http' }); + } + + if (runTimeId) { + obj.Query.push({ Key: 'runtime_id', Value: runTimeId }); + } + + requestYakURLList( + obj, + (rsp) => { + const newNodes: TreeNode[] = rsp.Resources.map( + (i, index) => { + const id = key + '/' + i.ResourceName; + return { + title: i.VerboseName, + key: id, + isLeaf: !i.HaveChildrenNodes, + data: i, + icon: ({ expanded }) => { + if (i.ResourceType === 'dir') { + return expanded ? ( + + ) : ( + + ); + } + return renderTreeNodeIcon( + i.ResourceType as TreeNodeType, + ); + }, + }; + }, + ); + // 判断是否是搜索树 + if (searchTreeFlag.current) { + setSearchWebTreeData((origin) => + refreshChildrenByParent(origin, key, newNodes), + ); + } else { + setWebTreeData((origin) => + refreshChildrenByParent(origin, key, newNodes), + ); + } + resolve(); + }, + reject, + ); + }); + }; + + // 搜索树 + const onSearchTree = useMemoizedFn((value: string) => { + const val = value.trim(); + const flag = val === '/' ? false : !!val.trim(); + searchTreeFlag.current = flag; + setExpandedKeys([]); + setSelectedKeys([]); + setSelectedNodes([]); + setSearchValue(val); + getTreeData(val); + }); + + useDebounceEffect( + () => { + if (treeExtraQueryparams && inViewport) { + if (refreshTreeFlag) { + refreshTree(); + } else if (!getSelectedKeys().length) { + if (searchTreeFlag.current) { + onSearchTree(getSearchValue()); + } else { + refreshTree(); + } + } + } + }, + [treeExtraQueryparams, refreshTreeFlag, inViewport], + { wait: 500 }, + ); + + // 刷新网站树 + const refreshTree = useDebounceFn( + () => { + setSearchValue(''); + searchTreeFlag.current = false; + setExpandedKeys([]); + setSelectedKeys([]); + setSelectedNodes([]); + getTreeData(); + }, + { wait: 200 }, + ).run; + + // 网站树跳转 -> 带到搜索框查询 + const onJumpWebTree = (value: string) => { + onSearchTree(value); + }; + + // 点击Select选中树 + const onSelectedKeys = useMemoizedFn( + ( + selectedKeys: TreeKey[], + info: { + selectedNodes: TreeNode[]; + }, + ) => { + setSelectedKeys(selectedKeys); + setSelectedNodes(info.selectedNodes); + }, + ); + + useEffect(() => { + const keys: string[] = Array.from( + new Set( + selectedNodes + .map((item) => { + if (item.data?.ResourceType === 'query') { + const urlItem = item.data.Extra.find( + (i) => i.Key === 'url', + ); + if (urlItem && urlItem.Value) { + const u = new URL(urlItem.Value); + return u.origin + u.pathname; + } else { + return ''; + } + } else { + return item.key as string; + } + }) + .filter(Boolean), + ), + ); + onSelectNodesKeys(keys); + }, [selectedNodes]); + + /** + * 计算树头部高度 + */ + const treeTopWrapRef = useRef(); + const [treeTopWrapHeight, setTreeTopWrapHeight] = useState(0); + const resizeObserver = new ResizeObserver( + (entries: ResizeObserverEntry[]) => { + entries.forEach((entry) => { + const target = entry.target; + setTreeTopWrapHeight(target.getBoundingClientRect().height); + }); + }, + ); + useEffect(() => { + if (treeTopWrapRef.current) { + resizeObserver.observe(treeTopWrapRef.current); + } + }, [treeTopWrapRef.current]); + + // 搜索框 + const onSearchChange = useMemoizedFn((e: { target: { value: string } }) => { + const value = e.target.value; + setSearchValue(value); + }); + + return ( +
    +
    + + } + onClick={refreshTree} + style={{ marginBottom: 15 }} + /> +
    +
    + {treeLoading ? ( + + ) : ( + + setExpandedKeys(expandedKeys) + } + selectedKeys={selectedKeys} + onSelect={onSelectedKeys} + selectable={!multiple} + checkable={multiple} + onCheck={(checkedKeys, info) => { + const keys = Array.isArray(checkedKeys) + ? checkedKeys + : checkedKeys.checked; + onSelectedKeys(keys, { + selectedNodes: info.checkedNodes, + }); + }} + checkedKeys={selectedKeys} + blockNode={true} + > + )} +
    +
    + ); +}); diff --git a/src/compoments/WizardTable/index.tsx b/src/compoments/WizardTable/index.tsx index 854b1c58..c957dc47 100644 --- a/src/compoments/WizardTable/index.tsx +++ b/src/compoments/WizardTable/index.tsx @@ -1,456 +1,392 @@ -import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react'; -import { Empty, message, Spin, Table } from 'antd'; -import type { AnyObject } from 'antd/es/_util/type'; -import { useRequest, useSafeState, useUpdateEffect } from 'ahooks'; - -import useListenWidth from '@/hooks/useListenHeight'; - -import WizardTableFilter from '../WizardTableFilter'; - -import { initialValue } from './data'; -import type { - RequestFunction, - TRecudeInitiakValue, - TWizardTableProps, -} from './types'; -import WizardProFilterDrawer from '../WizardProFilterDrawer'; -import { usePage } from '@/hooks'; -import { deepEqual } from '@/utils'; -import extendTableProps from './extendTableProps'; +import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react' +import { Empty, message, Spin, Table } from 'antd' +import type { AnyObject } from 'antd/es/_util/type' +import { useRequest, useSafeState, useUpdateEffect } from 'ahooks' + +import useListenWidth from '@/hooks/useListenHeight' + +import WizardTableFilter from '../WizardTableFilter' + +import { initialValue } from './data' +import type { RequestFunction, TRecudeInitiakValue, TWizardTableProps } from './types' +import WizardProFilterDrawer from '../WizardProFilterDrawer' +import { usePage } from '@/hooks' +import { deepEqual } from '@/utils' +import extendTableProps from './extendTableProps' // import './index.scss'; -import { match } from 'ts-pattern'; -import throttle from 'lodash/throttle'; -import styles from './tableStyled.module.scss'; +import { match } from 'ts-pattern' +import throttle from 'lodash/throttle' +import styles from './tableStyled.module.scss' const reducer = (state: T, payload: T): T => ({ - ...state, - ...payload, -}); - -// 分布式平台table -const WizardTable = ( - props: TWizardTableProps, -) => { - const { tableHeader, request, page, empotyNode } = props; - - const lastPage = useRef(0); // 跟踪上次的 page,防止重复请求 - const preFilter = useRef(undefined); // 跟踪上次的 filter, 触发请求 - const manualReq = useRef(false); - // 表格容器的 ref,用来控制滚动 - const tableContainerRef = useRef(null); - - const [state, dispatch] = useReducer(reducer, initialValue); - const { dataSource, params, filter } = state; - const { runAsync } = useRequest( - async (requests, reset?: boolean, arg?: any) => { - dispatch({ loading: true }); - try { - if (!state.loading) { - const data = await requests( - reset ? { ...params, page: 1 } : params, - { ...filter, ...arg }, - ); - const { list, pagemeta } = data; - dispatch({ - dataSource: - pagemeta?.page > 1 - ? (state.dataSource ?? []) - .concat(list ?? []) - .concat([]) - : (list ?? []).concat([]), - pagemeta, - params: pagemeta, - }); - return { - pagemeta, - list, - }; - } else { - manualReq.current = true; - } - - // 分页时追加数据,筛选或初始化时直接替换数据 - } finally { - if (manualReq.current) { - manualReq.current = false; - runAsync(request); - } - - dispatch({ loading: false, noResetFields: true }); - setIsBottom(false); - } - }, - { - manual: true, - onError: () => { - dispatch({ loading: false, noResetFields: true }); - }, - debounceWait: 300, - }, - ); - - // 接口请求执行体 - const requestTable = useCallback( - async (request: RequestFunction) => { - if ( - !request || - (deepEqual(preFilter.current, filter) && - params?.page === lastPage.current) - ) - return; // 检查是否已经发起过相同请求 - - lastPage.current = params!.page; // 更新 lastPage - preFilter.current = filter; - await runAsync(request); - }, - [params, filter], - ); - - const [wizardScrollHeight, wizardScrollWidth] = - useListenWidth(tableContainerRef); - const [isBottom, setIsBottom] = useSafeState(false); - - // 表格容器的 state, 用来保存计算得到的可滚动高度和表格高度 - const [height, setHeight] = useSafeState(0); - - // 动态计算表格高度 - useEffect(() => { - const tableFilterDomHeight = - tableContainerRef.current?.children[0].getBoundingClientRect() - .height ?? 0; - const antTableHeader = - tableContainerRef?.current && - tableContainerRef.current.querySelector('.ant-table-header'); - const antTableHeaderHeight = - antTableHeader?.getBoundingClientRect().height ?? 0; - - setHeight( - wizardScrollHeight - - tableFilterDomHeight - - antTableHeaderHeight - - 32, - ); - }, [wizardScrollHeight]); - - // 数据源更新后执行 - useEffect(() => { - const pagemeta = state.pagemeta; - const pagemetaStatus = - (pagemeta?.page ?? 0) * (pagemeta?.limit ?? 0) >= - (pagemeta?.total ?? 1); - - if ( - dataSource && - dataSource.length > 0 && - tableContainerRef.current && - !pagemetaStatus - ) { - // 获取表格 DOM 节点 - const tableElement = tableContainerRef.current.querySelector( - ':scope > :nth-child(2)', - ); - if (tableElement && !state.loading) { - const scrollHeight = tableElement.scrollHeight; - scrollHeight < height && - dispatch({ - params: { - limit: params!.limit, - page: state.pagemeta!.page + 1, - total: pagemeta!.total, - total_page: pagemeta!.total_page, - }, - }); - } + ...state, + ...payload, +}) + +// AI自动化渗透测试平台table +const WizardTable = (props: TWizardTableProps) => { + const { tableHeader, request, page, empotyNode } = props + + const lastPage = useRef(0) // 跟踪上次的 page,防止重复请求 + const preFilter = useRef(undefined) // 跟踪上次的 filter, 触发请求 + const manualReq = useRef(false) + // 表格容器的 ref,用来控制滚动 + const tableContainerRef = useRef(null) + + const [state, dispatch] = useReducer(reducer, initialValue) + const { dataSource, params, filter } = state + const { runAsync } = useRequest( + async (requests, reset?: boolean, arg?: any) => { + dispatch({ loading: true }) + try { + if (!state.loading) { + const data = await requests(reset ? { ...params, page: 1 } : params, { ...filter, ...arg }) + const { list, pagemeta } = data + dispatch({ + dataSource: + pagemeta?.page > 1 ? (state.dataSource ?? []).concat(list ?? []).concat([]) : (list ?? []).concat([]), + pagemeta, + params: pagemeta, + }) + return { + pagemeta, + list, + } } else { - setIsBottom(true); + manualReq.current = true } - }, [height, dataSource, state.loading]); - - useUpdateEffect(() => { - handleScrollToFirstRow(); - }, [filter]); - - // 表格滚动函数 - const throttledTableOnScrollFn = throttle((e: any) => { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - const { params, pagemeta } = state; - const hasMore = params!.limit * params!.page >= pagemeta!.total; - - const scrollBottomBoolean = - scrollTop + clientHeight >= scrollHeight - 500; - - setIsBottom(scrollTop + clientHeight >= scrollHeight - 48); - if ( - scrollBottomBoolean && - !state?.loading && - !hasMore && - dataSource?.length - ) { - // 滚动到底部且没有在加载中时,触发分页 - dispatch({ - params: { - limit: params!.limit, - page: (pagemeta?.page ?? 0) + 1, - total: pagemeta!.total, - total_page: pagemeta!.total_page, - }, - }); + // 分页时追加数据,筛选或初始化时直接替换数据 + } finally { + if (manualReq.current) { + manualReq.current = false + runAsync(request) } - }, 300); - - // 初始请求 - useEffect(() => { - requestTable(request); - }, [requestTable]); - - // 对外提供方法 - // 刷新 - page.refresh = async () => { - handleScrollToFirstRow(); - await runAsync(request, true); - }; - - // 手动触发 - page.onLoad = async (arg) => { - handleScrollToFirstRow(); - await runAsync(request, true, arg); - }; - - // 获取表格参数 - page.getParams = () => { - return { filter, params, loading: state.loading }; - }; - - // 清除页面选中项 - page.clear = async () => { - handleScrollToFirstRow(); - await dispatch({ + + dispatch({ loading: false, noResetFields: true }) + setIsBottom(false) + } + }, + { + manual: true, + onError: () => { + dispatch({ loading: false, noResetFields: true }) + }, + debounceWait: 300, + }, + ) + + // 接口请求执行体 + const requestTable = useCallback( + async (request: RequestFunction) => { + if (!request || (deepEqual(preFilter.current, filter) && params?.page === lastPage.current)) return // 检查是否已经发起过相同请求 + + lastPage.current = params!.page // 更新 lastPage + preFilter.current = filter + await runAsync(request) + }, + [params, filter], + ) + + const [wizardScrollHeight, wizardScrollWidth] = useListenWidth(tableContainerRef) + const [isBottom, setIsBottom] = useSafeState(false) + + // 表格容器的 state, 用来保存计算得到的可滚动高度和表格高度 + const [height, setHeight] = useSafeState(0) + + // 动态计算表格高度 + useEffect(() => { + const tableFilterDomHeight = tableContainerRef.current?.children[0].getBoundingClientRect().height ?? 0 + const antTableHeader = tableContainerRef?.current && tableContainerRef.current.querySelector('.ant-table-header') + const antTableHeaderHeight = antTableHeader?.getBoundingClientRect().height ?? 0 + + setHeight(wizardScrollHeight - tableFilterDomHeight - antTableHeaderHeight - 32) + }, [wizardScrollHeight]) + + // 数据源更新后执行 + useEffect(() => { + const pagemeta = state.pagemeta + const pagemetaStatus = (pagemeta?.page ?? 0) * (pagemeta?.limit ?? 0) >= (pagemeta?.total ?? 1) + + if (dataSource && dataSource.length > 0 && tableContainerRef.current && !pagemetaStatus) { + // 获取表格 DOM 节点 + const tableElement = tableContainerRef.current.querySelector(':scope > :nth-child(2)') + if (tableElement && !state.loading) { + const scrollHeight = tableElement.scrollHeight + scrollHeight < height && + dispatch({ params: { - page: 1, - limit: state.params!.limit, - total: state.pagemeta!.total, - total_page: state.pagemeta!.total_page, - }, - filter: {}, - getExternal: {}, - proSwitchStatus: false, - noResetFields: false, - }); - }; - - // 更改高级筛选项 - page.editFilter = (args) => { - handleScrollToFirstRow(); - dispatch({ - filter: { - ...state.filter, - ...args, + limit: params!.limit, + page: state.pagemeta!.page + 1, + total: pagemeta!.total, + total_page: pagemeta!.total_page, }, - }); - }; - page.getDataSource = () => { - return dataSource ?? []; - }; - - page.localRefrech = (args) => { - match(args) - .with({ operate: 'edit' }, ({ oldObj, newObj }) => { - // 查找满足部分匹配条件的对象索引 - const index = - dataSource?.findIndex((item) => - Object.entries(oldObj).every( - ([key, value]) => item[key] === value, - ), - ) ?? -1; - if (index !== -1) { - // 使用新的常量创建更新后的数据 - const updatedItem = { - ...(Array.isArray(dataSource) ? dataSource[index] : {}), - ...newObj, - }; - const list: any = [ - ...(dataSource ? dataSource.slice(0, index) : []), - updatedItem, - ...(dataSource ? dataSource.slice(index + 1) : []), - ]; - dispatch({ - dataSource: list, - }); - } else { - message.warning(`未找到该操作项`); - } - }) - // 删除 - .with({ operate: 'delete' }, ({ oldObj }) => { - if (args.operate === 'delete' && oldObj) { - // 过滤掉满足 oldObj 条件的对象 - const tragetDataSource = dataSource?.filter((item) => - Object.entries(oldObj).some(([key, value]) => { - if (Array.isArray(value)) { - // 如果值是数组,检查 item[key] 是否包含在数组中 - return !value.includes(item[key]); - } else { - // 如果是其他类型,直接检查是否不匹配 - return item[key] !== value; - } - }), - ); - - if (tragetDataSource?.length !== dataSource?.length) { - // 更新数据源 - dispatch({ - dataSource: tragetDataSource, - }); - } else { - message.warning('操作失败,请重试'); - } - } + }) + } + } else { + setIsBottom(true) + } + }, [height, dataSource, state.loading]) + + useUpdateEffect(() => { + handleScrollToFirstRow() + }, [filter]) + + // 表格滚动函数 + const throttledTableOnScrollFn = throttle((e: any) => { + const { scrollTop, scrollHeight, clientHeight } = e.target + + const { params, pagemeta } = state + const hasMore = params!.limit * params!.page >= pagemeta!.total + + const scrollBottomBoolean = scrollTop + clientHeight >= scrollHeight - 500 + + setIsBottom(scrollTop + clientHeight >= scrollHeight - 48) + + if (scrollBottomBoolean && !state?.loading && !hasMore && dataSource?.length) { + // 滚动到底部且没有在加载中时,触发分页 + dispatch({ + params: { + limit: params!.limit, + page: (pagemeta?.page ?? 0) + 1, + total: pagemeta!.total, + total_page: pagemeta!.total_page, + }, + }) + } + }, 300) + + // 初始请求 + useEffect(() => { + requestTable(request) + }, [requestTable]) + + // 对外提供方法 + // 刷新 + page.refresh = async () => { + handleScrollToFirstRow() + await runAsync(request, true) + } + + // 手动触发 + page.onLoad = async (arg) => { + handleScrollToFirstRow() + await runAsync(request, true, arg) + } + + // 获取表格参数 + page.getParams = () => { + return { filter, params, loading: state.loading } + } + + // 清除页面选中项 + page.clear = async () => { + handleScrollToFirstRow() + await dispatch({ + params: { + page: 1, + limit: state.params!.limit, + total: state.pagemeta!.total, + total_page: state.pagemeta!.total_page, + }, + filter: {}, + getExternal: {}, + proSwitchStatus: false, + noResetFields: false, + }) + } + + // 更改高级筛选项 + page.editFilter = (args) => { + handleScrollToFirstRow() + dispatch({ + filter: { + ...state.filter, + ...args, + }, + }) + } + page.getDataSource = () => { + return dataSource ?? [] + } + + page.localRefrech = (args) => { + match(args) + .with({ operate: 'edit' }, ({ oldObj, newObj }) => { + // 查找满足部分匹配条件的对象索引 + const index = + dataSource?.findIndex((item) => Object.entries(oldObj).every(([key, value]) => item[key] === value)) ?? -1 + if (index !== -1) { + // 使用新的常量创建更新后的数据 + const updatedItem = { + ...(Array.isArray(dataSource) ? dataSource[index] : {}), + ...newObj, + } + const list: any = [ + ...(dataSource ? dataSource.slice(0, index) : []), + updatedItem, + ...(dataSource ? dataSource.slice(index + 1) : []), + ] + dispatch({ + dataSource: list, + }) + } else { + message.warning(`未找到该操作项`) + } + }) + // 删除 + .with({ operate: 'delete' }, ({ oldObj }) => { + if (args.operate === 'delete' && oldObj) { + // 过滤掉满足 oldObj 条件的对象 + const tragetDataSource = dataSource?.filter((item) => + Object.entries(oldObj).some(([key, value]) => { + if (Array.isArray(value)) { + // 如果值是数组,检查 item[key] 是否包含在数组中 + return !value.includes(item[key]) + } else { + // 如果是其他类型,直接检查是否不匹配 + return item[key] !== value + } + }), + ) + + if (tragetDataSource?.length !== dataSource?.length) { + // 更新数据源 + dispatch({ + dataSource: tragetDataSource, }) - .exhaustive(); - }; + } else { + message.warning('操作失败,请重试') + } + } + }) + .exhaustive() + } + + const tableRef = useRef(null) + + // 回到顶部,并清空数据方法 + const handleScrollToFirstRow = () => { + if (tableRef.current) { + // 调用 scrollTo 方法,传递 (0, 0) 来滚动到顶部 + tableRef.current.scrollTo(0, 0) + dispatch({ + params: { + page: 1, + limit: state.params!.limit, + total: state.pagemeta!.total, + total_page: state.pagemeta!.total_page, + }, + }) + } + } - const tableRef = useRef(null); + // 底部loading状态 + const bottomLoading = useMemo(() => { + const pagemeta = state.pagemeta + const status = state.loading && state.dataSource!.length > 0 + const pagemetaStatus = (pagemeta?.page ?? 0) * (pagemeta?.limit ?? 0) >= (pagemeta?.total ?? 1) - // 回到顶部,并清空数据方法 - const handleScrollToFirstRow = () => { - if (tableRef.current) { - // 调用 scrollTo 方法,传递 (0, 0) 来滚动到顶部 - tableRef.current.scrollTo(0, 0); - dispatch({ - params: { - page: 1, - limit: state.params!.limit, - total: state.pagemeta!.total, - total_page: state.pagemeta!.total_page, - }, - }); - } - }; - - // 底部loading状态 - const bottomLoading = useMemo(() => { - const pagemeta = state.pagemeta; - const status = state.loading && state.dataSource!.length > 0; - const pagemetaStatus = - (pagemeta?.page ?? 0) * (pagemeta?.limit ?? 0) >= - (pagemeta?.total ?? 1); - - const width = - (tableContainerRef.current?.getBoundingClientRect().width ?? 0) - - 108; - - return ( -
    - {pagemetaStatus && - dataSource!.length !== 0 && - !state.loading && - isBottom && ( -
    - 已获取完毕所有数据 -
    - )} - {status && ( -
    - - 加载中... -
    - )} -
    - ); - }, [state.loading, wizardScrollWidth, isBottom]); + const width = (tableContainerRef.current?.getBoundingClientRect().width ?? 0) - 108 return ( -
    - {/* 表格部分 */} - -
    - -
    , - }} - /> - {(isBottom || state.loading) && dataSource?.length - ? bottomLoading - : null} - - - {/* 右侧抽屉 */} -
    - -
    - - ); -}; - -WizardTable.usePage = usePage; - -export default WizardTable; +
    + {pagemetaStatus && dataSource!.length !== 0 && !state.loading && isBottom && ( +
    + 已获取完毕所有数据 +
    + )} + {status && ( +
    + + 加载中... +
    + )} +
    + ) + }, [state.loading, wizardScrollWidth, isBottom]) + + return ( +
    + {/* 表格部分 */} + +
    + +
    , + }} + /> + {(isBottom || state.loading) && dataSource?.length ? bottomLoading : null} + + + {/* 右侧抽屉 */} +
    + +
    + + ) +} + +WizardTable.usePage = usePage + +export default WizardTable diff --git a/src/compoments/YakitTag/YakitCheckableTag.tsx b/src/compoments/YakitTag/YakitCheckableTag.tsx deleted file mode 100644 index 149cf870..00000000 --- a/src/compoments/YakitTag/YakitCheckableTag.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import type { YakitCheckableTagProps } from './YakitTagType'; -import styles from './YakitTag.module.scss'; -import { Tag } from 'antd'; - -const { CheckableTag } = Tag; - -export const YakitCheckableTag: React.FC = React.memo( - (props) => { - const { wrapClassName, disable, ...resProps } = props; - return ( -
    - { - if (!disable && props.onClick) props.onClick(e); - }} - onChange={(c) => { - if (!disable && props.onChange) props.onChange(c); - }} - > - {props.children} - -
    - ); - }, -); diff --git a/src/compoments/YakitTag/YakitTag.module.scss b/src/compoments/YakitTag/YakitTag.module.scss deleted file mode 100644 index ce19a559..00000000 --- a/src/compoments/YakitTag/YakitTag.module.scss +++ /dev/null @@ -1,241 +0,0 @@ -.yakit-tag-middle { - height: 20px; - border-radius: 4px; - padding: 2px 6px; - background: var(--yakit-card-background-color); - border: 1px solid var(--yakit-border-color); - color: var(--yakit-body-text-color); - display: inline-flex; - align-items: center; - font-size: 12px; - - path { - stroke-width: 1.5; - } - - :global { - .ant-tag-close-icon { - margin-left: 8px; - display: inline-flex; - align-items: center; - color: var(--yakit-body-text-color); - width: 12px; - height: 12px; - svg { - width: 12px; - height: 12px; - } - .anticon { - height: 12px; - } - &:hover { - color: var(--yakit-primary-5); - } - } - } -} - -.yakit-tag-small { - height: 16px; - border-radius: 2px; - padding: 0px 6px; - font-size: 11px; -} - -.yakit-tag-large { - height: 24px; - padding: 4px 8px; - font-size: 12px; - - :global { - .ant-tag-close-icon { - width: 14px; - height: 14px; - - svg { - width: 14px; - height: 14px; - } - } - - .anticon { - height: 14px; - } - } -} - -.yakit-tag-danger { - background: rgba(246, 84, 74, 0.08); - border: 1px solid rgba(244, 115, 107, 0.3); - color: #f4736b; - svg { - color: #f4736b; - } -} - -.yakit-tag-info { - background: rgba(242, 139, 68, 0.06); - border: 1px solid rgba(242, 139, 68, 0.3); - color: #f28b44; - svg { - color: #f28b44; - } -} - -.yakit-tag-success { - background: rgba(86, 201, 145, 0.1); - border: 1px solid rgba(86, 201, 145, 0.3); - color: #56c991; - svg { - color: #56c991; - } -} - -.yakit-tag-warning { - background: rgba(255, 182, 96, 0.1); - border: 1px solid rgba(255, 182, 96, 0.3); - color: #ffb660; - svg { - color: #ffb660; - } -} - -.yakit-tag-serious { - background: rgba(203, 35, 24, 0.2); - border: 1px solid #e99a95; - color: #cb2318; - svg { - color: #cb2318; - } -} - -.yakit-tag-purple { - background: rgba(136, 99, 247, 0.1); - border: 1px solid rgba(136, 99, 247, 0.3); - color: #8863f7; - svg { - color: #8863f7; - } -} - -.yakit-tag-blue { - background: rgba(74, 148, 248, 0.1); - border: 1px solid rgba(74, 148, 248, 0.3); - color: #4a94f8; - svg { - color: #4a94f8; - } -} - -.yakit-tag-cyan { - background: rgba(53, 216, 238, 0.1); - border: 1px solid rgba(53, 216, 238, 0.3); - color: #35d8ee; - svg { - color: #35d8ee; - } -} - -.yakit-tag-bluePurple { - background: rgba(218, 95, 221, 0.1); - border: 1px solid rgba(218, 95, 221, 0.3); - color: #da5fdd; - svg { - color: #da5fdd; - } -} - -.yakit-tag-yellow { - background: rgba(255, 213, 131, 0.1); - border: 1px solid #ffe5c6; - color: #ffb660; - svg { - color: #ffb660; - } -} - -.yakit-tag-white { - background: #ffffff; - border: 1px solid #eaecf3; - color: #31343f; -} - -.yakit-tag-disable { - background: #f8f8f8; - border: 1px solid #f0f1f3; - color: #ccd2de; - cursor: not-allowed; - - :global { - .ant-tag-close-icon { - cursor: not-allowed; - color: #ccd2de; - } - } -} - -// ========================== copy ========================== -.yakit-copy { - margin-left: 4px; - height: 100%; - display: inline-flex; - align-items: center; - cursor: pointer; -} - -// ========================== yakit-checked-tag start ========================== -.yakit-checked-tag-wrap { - display: inline-flex; - align-items: center; - user-select: none; - - :global { - .ant-tag { - line-height: 18px; - height: 20px; - } - - .ant-tag-checkable { - background-color: var(--yakit-background-color); - } - - .ant-tag-checkable-checked { - background-color: var(--yakit-primary-5); - } - - .ant-tag-checkable:not(.ant-tag-checkable-checked):hover { - color: var(--yakit-primary-5); - } - } -} - -.yakit-checked-tag-disable { - cursor: not-allowed; - - :global { - .ant-tag-checkable { - color: var(--yakit-disable-text-color); - } - - .ant-tag-checkable:not(.ant-tag-checkable-checked):hover { - color: var(--yakit-disable-text-color); - } - } -} - -.yakit-checked-tag-checked-disable { - cursor: not-allowed; - - :global { - .ant-tag-checkable-checked { - color: #fff; - background-color: var(--yakit-disable-text-color); - } - - .ant-tag-checkable:not(.ant-tag-checkable-checked):hover { - color: #fff; - } - } -} - -// ========================== yakit-checked-tag end ========================== diff --git a/src/compoments/YakitTag/YakitTag.tsx b/src/compoments/YakitTag/YakitTag.tsx deleted file mode 100644 index a714968d..00000000 --- a/src/compoments/YakitTag/YakitTag.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Tag } from 'antd'; -import React from 'react'; -import type { YakitTagProps } from './YakitTagType'; -import styles from './YakitTag.module.scss'; -import classNames from 'classnames'; - -/** - * 更新说明 - * 1、关闭按钮增加hover主题色 - * 2.height 修为不加border - */ - -/** - * @description: tag - * @augments TagProps 继承antd的TagProps默认属性 - * @param {middle|large|small} size 默认middle 16 20 24 - * @param {"danger" | "info" | "success" | "warning"|"serious" |"yellow"| "purple" | "blue" | "cyan" | "bluePurple"} color 颜色 - * @param {boolean} disable - * @param {boolean} enableCopy 是否可复制 - * @param {e} onAfterCopy 复制后的回调 - */ -export const YakitTag: React.FC = (props) => { - const { - color, - size, - disable, - className, - enableCopy, - copyText, - ...restProps - } = props; - return ( - - {(enableCopy && copyText) || props.children} - - ); -}; diff --git a/src/compoments/YakitTag/YakitTagType.d.ts b/src/compoments/YakitTag/YakitTagType.d.ts deleted file mode 100644 index 76a76a79..00000000 --- a/src/compoments/YakitTag/YakitTagType.d.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { TagProps } from 'antd'; -import type { CheckableTagProps } from 'antd/lib/tag'; -import type { YakitSizeType } from '../YakitInputNumber/YakitInputNumberType'; - -/** - * @description YakitTagProps 的属性 - * @augments TagProps 继承antd的TagProps默认属性 - * @param {middle|large|small} size 默认middle - * @param {"danger" | "info" | "success" | "warning" |"serious"|"yellow"| "purple" | "blue" | "cyan" | "bluePurple"|"white"} color 颜色 - * @param {boolean} disable - * @param {boolean} enableCopy 是否可复制 - * @param {(e:MouseEvent) => void} onAfterCopy 复制文字后得事件 enableCopy为true有效 - * @param {string} copyText 复制文字 - * @param {string} iconColor 复制Icon文字 - */ - -export type YakitTagColor = - | 'danger' - | 'info' - | 'success' - | 'green' - | 'warning' - | 'serious' - | 'red' - | 'yellow' - | 'purple' - | 'blue' - | 'cyan' - | 'bluePurple' - | 'white'; - -export interface YakitTagProps extends Omit { - size?: YakitSizeType; - color?: YakitTagColor; - disable?: boolean; - enableCopy?: boolean; - onAfterCopy?: (e: MouseEvent) => void; - copyText?: string; - iconColor?: string; -} -/** - * @description: 复制文字 - * @param {string} className 包装器修饰类 - * @param {(e:MouseEvent) => void} onAfterCopy 复制文字后得事件 - * @param {string} copyText 复制文字 - * @param {string} iconColor 复制Icon文字 - */ -export interface CopyComponentsProps { - className?: string; - onAfterCopy?: (e: MouseEvent) => void; - copyText: string; - iconColor?: string; -} - -/** - * @description: tag 多选 - * @param {ReactNode} children - * @param {string} wrapClassName - * @param {boolean} disable - */ -export interface YakitCheckableTagProps extends CheckableTagProps { - children?: ReactNode; - wrapClassName?: string; - disable?: boolean; -} diff --git a/src/compoments/YakitUploadModal/YakitUploadModal.module.scss b/src/compoments/YakitUploadModal/YakitUploadModal.module.scss new file mode 100644 index 00000000..f902e291 --- /dev/null +++ b/src/compoments/YakitUploadModal/YakitUploadModal.module.scss @@ -0,0 +1,331 @@ +.yakit-upload-component { + min-height: 85px; + .header { + padding: 16px 24px 8px 24px; + display: flex; + align-items: center; + gap: 16px; + align-self: stretch; + .title { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 14px; + font-weight: 600; + line-height: 20px; + flex: 1; + } + .extra { + display: flex; + cursor: pointer; + svg { + width: 20px; + height: 20px; + color: var(--Colors-Use-Neutral-Text-3-Secondary); + } + &:hover { + svg { + color: var(--Colors-Use-Main-Hover); + } + } + } + } + .explain { + padding: 0px 24px; + .explain-bg { + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px 16px; + border-radius: 8px; + background: linear-gradient(90deg, rgba(136, 99, 247, 0.1) 0%, rgba(247, 170, 99, 0.1) 100%); + .title { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + } + .content { + display: flex; + flex-direction: column; + gap: 2px; + .item { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + .dot { + display: flex; + height: 16px; + width: 16px; + padding: 2px 6px; + justify-content: center; + align-items: center; + border-radius: 8px; + background: rgba(136, 99, 247, 0.2); + font-size: 10px; + text-align: center; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-weight: 400; + line-height: 12px; + } + .text { + flex: 1; + color: var(--Colors-Use-Neutral-Text-3-Secondary); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; + .hight-text { + color: #f28b44; + } + } + } + } + } + } + .info-box { + display: flex; + padding: 16px 24px 24px 24px; + flex-direction: column; + align-items: flex-start; + gap: 16px; + align-self: stretch; + .input-box { + width: 100%; + .name { + display: flex; + flex-direction: row; + gap: 4px; + font-size: 14px; + font-weight: 400; + line-height: 20px; + margin-bottom: 6px; + .must { + color: #f4736b; + } + } + } + .card-box { + width: 100%; + border-radius: 4px; + background-color: rgb(240, 242, 245); + border: 1px solid #f0f0f0; + overflow: hidden; + .card-heard { + background-color: #ffffff; + height: 36px; + min-height: 36px; + box-sizing: content-box; + border-bottom: 1px solid var(--Colors-Use-Neutral-Border); + display: flex; + align-items: center; + padding: 0 12px; + } + .upload-dragger-box { + width: 100%; + height: 140px; + .upload-dragger { + border-color: transparent; + .upload-info { + display: flex; + padding: 8px 0px; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 4px; + .add-file-iconf { + height: 40px; + } + .content { + .title { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 14px; + font-weight: 600; + line-height: 20px; + .hight-light { + color: #f28b44; + text-decoration-line: underline; + } + } + .sub-title { + color: #b4bbca; + font-size: 14px; + font-weight: 400; + line-height: 20px; + } + } + } + } + :global { + .ant-upload.ant-upload-drag.ant-upload-drag-hover:not(.ant-upload-disabled) { + border-color: #f28b44; + } + } + } + .upload-editor-box { + height: 140px; + } + } + + .upload-list { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + width: 100%; + .upload-list-item { + width: 100%; + display: flex; + flex-direction: row; + padding: 2px 4px; + justify-content: flex-start; + align-items: center; + gap: 8px; + max-width: 100%; + height: 28px; + position: relative; + .link-icon { + display: flex; + color: #ccd2de; + svg { + width: 16px; + height: 16px; + } + } + .text { + font-size: 12px; + line-height: 16px; + font-weight: 400; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + .close-icon { + position: absolute; + display: flex; + align-items: center; + right: 4px; + opacity: 0; + cursor: pointer; + color: #b4bbca; + svg { + width: 16px; + height: 16px; + } + } + &:hover { + border-radius: 4px; + background: var(--Colors-Use-Neutral-Bg); + .close-icon { + opacity: 1; + } + } + } + } + } + .submit-box { + display: flex; + padding: 0px 24px 24px 24px; + justify-content: flex-end; + align-items: center; + gap: 8px; + align-self: stretch; + } +} +.yaklang-engine-hint-wrapper { + width: 100%; + display: flex; + gap: 16px; + position: relative; + + .hint-left-wrapper { + display: flex; + flex-direction: column; + justify-content: space-between; + + .hint-icon { + width: 32px; + height: 32px; + color: #ffb660; + svg { + width: 32px; + height: 32px; + } + } + } + + .hint-right-wrapper { + flex: 1; + overflow: hidden; + .hint-right-title { + height: 24px; + font-weight: 600; + font-size: 16px; + line-height: 24px; + color: var(--Colors-Use-Neutral-Text-1-Title); + margin-bottom: 8px; + } + + .hint-right-content { + margin-top: 8px; + font-size: 14px; + line-height: 20px; + color: var(--Colors-Use-Neutral-Text-1-Title); + } + + .hint-right-download { + .download-progress { + .progress-title { + margin-top: 6px; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: 16px; + } + } + + .download-info-wrapper { + margin-top: 8px; + display: flex; + height: 16px; + font-size: 12px; + line-height: 16px; + color: var(--Colors-Use-Neutral-Disable); + + .divider-wrapper { + margin: 0 8px; + width: 1px; + height: 16px; + display: flex; + align-items: center; + + .divider-style { + width: 100%; + height: 12px; + border: 1px solid var(--Colors-Use-Neutral-Border); + } + } + } + .log-info { + transition: height 0.2s ease; + overflow-y: auto; + .log-item { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; + + // white-space: nowrap; + // overflow: hidden; + // text-overflow: ellipsis; + } + } + .download-btn { + margin-top: 24px; + display: flex; + justify-content: right; + gap: 8px; + } + } + } +} \ No newline at end of file diff --git a/src/compoments/YakitUploadModal/YakitUploadModal.tsx b/src/compoments/YakitUploadModal/YakitUploadModal.tsx new file mode 100644 index 00000000..67c2ced2 --- /dev/null +++ b/src/compoments/YakitUploadModal/YakitUploadModal.tsx @@ -0,0 +1,138 @@ +import { type FC, memo } from 'react'; +import { Progress } from 'antd'; +import styles from './YakitUploadModal.module.scss'; +import { SolidDocumentdownloadIcon } from '@/assets/icon/solid'; + +export interface SaveProgressStream { + Progress: number; + Speed?: string; + CostDurationVerbose?: string; + RestDurationVerbose?: string; +} + +export interface LogListInfo { + message: string; + isError?: boolean; + key: string; +} + +export interface ImportAndExportStatusInfo { + title: string; + streamData: SaveProgressStream; + logListInfo: LogListInfo[]; + showDownloadDetail: boolean; // 是否显示-下载详细信息 +} + +export const ImportAndExportStatusInfo: FC = memo( + (props) => { + const { title, streamData, logListInfo, showDownloadDetail } = props; + + return ( +
    +
    +
    + +
    +
    + +
    +
    +
    + {title} +
    +
    + +
    + 进度 {Math.round(streamData.Progress * 100)}% +
    +
    + {showDownloadDetail && ( +
    + {streamData.RestDurationVerbose && ( + <> +
    + 剩余时间 : + {streamData.Progress === 1 + ? '0s' + : streamData.RestDurationVerbose} +
    +
    +
    +
    + + )} + {streamData.CostDurationVerbose && ( + <> +
    + 耗时 : + {streamData.CostDurationVerbose} +
    +
    +
    +
    + + )} + {streamData.Speed && ( + <> +
    + 下载速度 : {streamData.Speed}M/s +
    +
    +
    +
    + + )} +
    + )} +
    + {logListInfo.map((item) => ( +
    + {item.message} +
    + ))} +
    +
    +
    +
    + ); + }, +); diff --git a/src/compoments/businessUI/FilterPanel/FilterPanel.module.scss b/src/compoments/businessUI/FilterPanel/FilterPanel.module.scss new file mode 100644 index 00000000..acdfeec8 --- /dev/null +++ b/src/compoments/businessUI/FilterPanel/FilterPanel.module.scss @@ -0,0 +1,115 @@ +.filter-panel-wrapper { + height: 100%; + overflow: hidden; +} + +.filter-panel-container { + width: 224px; + height: 100%; + padding-top: 4px; + display: flex; + flex-direction: column; + .panel-header { + display: flex; + padding: 12px 12px 12px 16px; + justify-content: space-between; + align-items: center; + // border-bottom: 1px solid #eaecf3; + .header-title { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + line-height: 16px; + } + .panel-header-icon { + color: var(--Colors-Use-Neutral-Text-1-Title); + width: 16px; + height: 16px; + svg { + width: 16px; + height: 16px; + } + &:hover { + color: var(--Colors-Use-Main-Hover); + } + path { + stroke-width: 1.5; + } + } + } + .panel-content { + flex: 1 0 0; + overflow: hidden; + .content-body { + width: 100%; + height: 100%; + overflow: hidden auto; + .content-collapse { + width: 100%; + // padding-left: 4px; + :global { + .ant-collapse-content-box { + padding: 0 4px 12px !important; + } + .ant-collapse-item .ant-collapse-header{ + padding-left: 10px; + padding-right: 10px; + } + } + .clear-btn { + padding-right: 4px; + } + .list-item { + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 4px; + padding: 6px 12px 8px 6px; + cursor: pointer; + &:hover { + background-color: #f0f1f3; + } + .list-item-left { + display: flex; + align-items: center; + gap: 12px; + overflow: hidden; + margin-right: 12px; + padding-left: 2px; + line-height: 16px; + .item-title { + max-width: 120px; + overflow: hidden; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + line-height: 16px; + } + } + .list-item-extra { + color: #b4bbca; + text-align: right; + font-size: 12px; + line-height: 16px; + } + } + .list-item-active { + color: var(--Colors-Use-Main-Primary); + .list-item-left { + .item-title { + color: var(--Colors-Use-Main-Primary); + } + } + .list-item-extra { + color: var(--Colors-Use-Main-Primary); + } + } + } + .to-end { + text-align: center; + color: var(--Colors-Use-Neutral-Disable); + font-size: 11px; + padding-top: 12px; + padding-bottom: 24px; + } + } + } +} diff --git a/src/compoments/businessUI/FilterPanel/FilterPanel.tsx b/src/compoments/businessUI/FilterPanel/FilterPanel.tsx new file mode 100644 index 00000000..935545cb --- /dev/null +++ b/src/compoments/businessUI/FilterPanel/FilterPanel.tsx @@ -0,0 +1,141 @@ +import React, { useEffect, useRef, useState } from 'react' +import type { FilterPanelProps } from './FilterPanelType' +import { YakitSpin } from '@/compoments/yakitUI/YakitSpin/YakitSpin' +import { YakitEmpty } from '@/compoments/yakitUI/YakitEmpty/YakitEmpty' +import YakitCollapse from '@/compoments/yakitUI/YakitCollapse/YakitCollapse' +import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton' +import { useMemoizedFn } from 'ahooks' +import { YakitCheckbox } from '@/compoments/yakitUI/YakitCheckbox/YakitCheckbox' +import { Tooltip } from 'antd' +import { OutlineCloseIcon } from '@/assets/icon/outline' + +import styles from './FilterPanel.module.scss' +import classNames from 'classnames' + +const { YakitPanel } = YakitCollapse +export interface PluginsSearchData { + value: string + count: number + label: string +} +export const FilterPanel: React.FC = React.memo((props) => { + const { + wrapperClassName, + loading = false, + setVisible, + selecteds, + onSelect, + groupList, + noDataHint, + listClassName, + } = props + + const [activeKey, setActiveKey] = useState([]) + const groupListStrRef = useRef('[]') + useEffect(() => { + const excludeGroupExtraOptBtn = JSON.stringify(groupList.map((item) => ({ ...item, groupExtraOptBtn: undefined }))) + if (groupListStrRef.current === excludeGroupExtraOptBtn) { + return + } + groupListStrRef.current = excludeGroupExtraOptBtn + const keys = groupList.map((l) => l.groupKey) + setActiveKey(keys) + }, [groupList]) + + const onClear = useMemoizedFn((key: string) => { + const selected = { ...selecteds } + selected[key] = [] + onSelect({ ...selected }) + }) + const onCheck = useMemoizedFn((groupKey: string, data: PluginsSearchData, check: boolean) => { + const selected = { ...selecteds } + if (check) { + selected[groupKey] = [...(selected[groupKey] || []), data] + } else { + selected[groupKey] = (selected[groupKey] || []).filter((item) => item.value !== data.value) + } + onSelect({ ...selected }) + }) + const onClose = useMemoizedFn(() => { + setVisible(false) + }) + return ( +
    +
    +
    + 高级筛选 + + } + /> + +
    +
    + +
    + setActiveKey(key as string[])} + className={styles['content-collapse']} + > + {groupList.map((item) => ( + + {item.groupExtraOptBtn} + { + e.stopPropagation() + onClear(item.groupKey) + }} + > + 清空 + + + } + > + {(item.data || []).map((listItem) => { + const checked = + (selecteds[item.groupKey] || []).findIndex((ele) => ele.value === listItem.value) !== -1 + return ( + + ) + })} + + ))} + + {groupList.length > 0 &&
    已经到底啦~
    } + {groupList.length === 0 && } +
    +
    +
    +
    +
    + ) +}) diff --git a/src/compoments/businessUI/FilterPanel/FilterPanelType.ts b/src/compoments/businessUI/FilterPanel/FilterPanelType.ts new file mode 100644 index 00000000..abe5484b --- /dev/null +++ b/src/compoments/businessUI/FilterPanel/FilterPanelType.ts @@ -0,0 +1,40 @@ +import {API} from "@/services/swagger/resposeType" + +export interface FilterPanelGroupItem { + /** 单项过滤条件key */ + groupKey: string + /** 单项过滤条件展示名 */ + groupName: string + /** 单选过滤额外操作按钮 */ + groupExtraOptBtn?: React.ReactElement + /** 单项过滤条件-过滤内容列表 */ + data: { + /** 选项展示内容 */ + label: string + /** 选项值 */ + value: string + /** 选项统计总数 */ + count: number + }[] +} + +export interface FilterPanelProps { + /** 外框架修饰类 */ + wrapperClassName?: string + /** 列表架修饰类 */ + listClassName?: string + /** 加载状态 */ + loading?: boolean + /** 是否可见 */ + visible: boolean + /** 设置是否可见 */ + setVisible: (show: boolean) => any + /** 选中数据 */ + selecteds: Record + /** 选中数据回调 */ + onSelect: (value: Record) => any + /** 数据展示列表 */ + groupList: FilterPanelGroupItem[] + /** 数据为空时的提示信息 */ + noDataHint?: string +} diff --git a/src/compoments/businessUI/PluginTabs/PluginTabs.module.scss b/src/compoments/businessUI/PluginTabs/PluginTabs.module.scss new file mode 100644 index 00000000..8c7024af --- /dev/null +++ b/src/compoments/businessUI/PluginTabs/PluginTabs.module.scss @@ -0,0 +1,357 @@ +.plugin-tabs { + width: 100%; + height: 100%; + overflow: hidden; + + :global { + .ant-tabs { + width: 100%; + height: 100%; + } + + // tabPosition = top + .ant-tabs-top { + & > .ant-tabs-nav { + height: 25px; + margin-bottom: 0; + &::before { + border-bottom: 1px solid var(--Colors-Use-Neutral-Border); + // border-bottom: none; + z-index: 0; + } + .ant-tabs-ink-bar { + display: none; + } + + // 基础样式 + .ant-tabs-tab { + padding: 4px 8px; + border-radius: 4px 4px 0px 0px; + border-top: 1px solid var(--Colors-Use-Neutral-Border); + border-right: 1px solid var(--Colors-Use-Neutral-Border); + border-left: 1px solid var(--Colors-Use-Neutral-Border); + border-bottom: 0; + background: var(--Colors-Use-Neutral-Bg-Hover); + .ant-tabs-tab-btn { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + line-height: 16px; + } + } + .ant-tabs-tab + .ant-tabs-tab { + margin-left: 4px; + } + // 选中 + .ant-tabs-tab.ant-tabs-tab-active { + border-top: 1px solid var(--Colors-Use-Main-Border); + border-right: 1px solid var(--Colors-Use-Main-Border); + border-left: 1px solid var(--Colors-Use-Main-Border); + background: var(--Colors-Use-Main-Primary); + .ant-tabs-tab-btn { + color: var(--Colors-Use-Main-On-Primary); + text-shadow: unset; + } + } + // 禁用 + .ant-tabs-tab.ant-tabs-tab-disabled { + background: var(--Colors-Use-Neutral-Bg); + .ant-tabs-tab-btn { + color: var(--Colors-Use-Neutral-Disable); + } + } + } + } + // tabPosition = bottom + .ant-tabs-bottom { + & > .ant-tabs-nav { + height: 25px; + margin-top: 0; + &::before { + border-top: 1px solid var(--Colors-Use-Neutral-Border); + z-index: 0; + } + .ant-tabs-ink-bar { + display: none; + } + + // 基础样式 + .ant-tabs-tab { + padding: 4px 8px; + border-radius: 0px 0px 4px 4px; + border-bottom: 1px solid var(--Colors-Use-Neutral-Border); + border-right: 1px solid (--Colors-Use-Neutral-Border); + border-left: 1px solid (--Colors-Use-Neutral-Border); + border-top: 0; + background: var(--Colors-Use-Neutral-Bg-Hover); + .ant-tabs-tab-btn { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + line-height: 16px; + } + } + .ant-tabs-tab + .ant-tabs-tab { + margin-left: 4px; + } + // 选中 + .ant-tabs-tab.ant-tabs-tab-active { + border-bottom: 1px solid var(--Colors-Use-Main-Border); + border-right: 1px solid var(--Colors-Use-Main-Border); + border-left: 1px solid var(--Colors-Use-Main-Border); + background: var(--Colors-Use-Main-Primary); + .ant-tabs-tab-btn { + color: var(--Colors-Use-Neutral-Text-1-Title); + text-shadow: unset; + } + } + // 禁用 + .ant-tabs-tab.ant-tabs-tab-disabled { + background: var(--Colors-Use-Neutral-Bg); + .ant-tabs-tab-btn { + color: var(--Colors-Use-Neutral-Disable); + } + } + } + } + + // tabPosition = right + .ant-tabs-right { + & > .ant-tabs-nav { + width: 21px; + min-width: unset; + &::before { + height: 100%; + position: absolute; + top: 0; + left: 0; + border-left: 1px solid var(--Colors-Use-Neutral-Border); + z-index: 0; + content: ""; + } + .ant-tabs-ink-bar { + display: none; + } + + // 基础样式 + .ant-tabs-tab { + width: 21px; + padding: 8px 4px; + border-radius: 0px 4px 4px 0px; + border-top: 1px solid (--Colors-Use-Neutral-Border); + border-right: 1px solid (--Colors-Use-Neutral-Border); + border-bottom: 1px solid (--Colors-Use-Neutral-Border); + border-left: 0; + background: var(--Colors-Use-Neutral-Bg-Hover); + border-color: var(--Colors-Use-Neutral-Border); + .ant-tabs-tab-btn { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + line-height: 14px; + width: 12px; + writing-mode: vertical-rl; + } + } + .ant-tabs-tab + .ant-tabs-tab { + margin-top: 4px; + } + // 选中 + .ant-tabs-tab.ant-tabs-tab-active { + border-top: 1px solid var(--Colors-Use-Main-Border); + border-right: 1px solid var(--Colors-Use-Main-Border); + border-bottom: 1px solid var(--Colors-Use-Main-Border); + background: var(--Colors-Use-Main-Primary); + .ant-tabs-tab-btn { + color: var(--Colors-Use-Main-On-Primary); + text-shadow: unset; + } + } + // 禁用 + .ant-tabs-tab.ant-tabs-tab-disabled { + background: var(--Colors-Use-Neutral-Bg); + border-color: var(--Colors-Use-Neutral-Border); + .ant-tabs-tab-btn { + color: var(--Colors-Use-Neutral-Disable); + } + } + } + + & > .ant-tabs-content-holder { + border-right: 0; + margin-right: 0; + & > .ant-tabs-content > .ant-tabs-tabpane { + padding-right: 0; + } + } + } + // tabPosition = left + .ant-tabs-left { + & > .ant-tabs-nav { + width: 21px; + min-width: unset; + &::before { + height: 100%; + position: absolute; + top: 0; + right: 0; + border-right: 1px solid (--Colors-Use-Neutral-Border); + z-index: 0; + content: ""; + } + .ant-tabs-ink-bar { + display: none; + } + + // 基础样式 + .ant-tabs-tab { + width: 21px; + padding: 8px 4px; + border-radius: 4px 0px 0px 4px; + border-top: 1px solid (--Colors-Use-Neutral-Border); + border-left: 1px solid (--Colors-Use-Neutral-Border); + border-bottom: 1px solid (--Colors-Use-Neutral-Border); + border-right: 0; + background: var(--Colors-Use-Neutral-Bg-Hover); + .ant-tabs-tab-btn { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + line-height: 14px; + width: 12px; + writing-mode: vertical-rl; + } + } + .ant-tabs-tab + .ant-tabs-tab { + margin-top: 4px; + } + // 选中 + .ant-tabs-tab.ant-tabs-tab-active { + border-top: 1px solid var(--Colors-Use-Main-Border); + border-left: 1px solid var(--Colors-Use-Main-Border); + border-bottom: 1px solid var(--Colors-Use-Main-Border); + background: var(--Colors-Use-Main-Primary); + .ant-tabs-tab-btn { + color: var(--Colors-Use-Main-On-Primary); + text-shadow: unset; + } + } + // 禁用 + .ant-tabs-tab.ant-tabs-tab-disabled { + background: var(--Colors-Use-Neutral-Bg); + .ant-tabs-tab-btn { + color: var(--Colors-Use-Neutral-Disable); + } + } + } + + & > .ant-tabs-content-holder { + border-left: 0; + margin-left: 0; + & > .ant-tabs-content > .ant-tabs-tabpane { + padding-left: 0; + } + } + } + + // body + .ant-tabs-content-holder { + flex: 1; + overflow: hidden; + .ant-tabs-content { + width: 100%; + height: 100%; + } + } + } +} +.plugin-tabs-line { + width: 100%; + height: 100%; + overflow: hidden; + :global { + .ant-tabs-nav .ant-tabs-nav-list { + border-bottom: 1px solid (--Colors-Use-Neutral-Border); + } + .ant-tabs-top { + & > .ant-tabs-nav { + height: 34px; + margin-bottom: 0; + &::before { + border-bottom: 1px solid (--Colors-Use-Neutral-Border); + z-index: 0; + } + .ant-tabs-ink-bar { + display: none; + } + + // 基础样式 + .ant-tabs-tab { + padding: 4px 16px; + border: 0; + background: var(--Colors-Use-Neutral-Bg); + position: relative; + border-radius: 0; + &:hover { + background-color: var(--Colors-Use-Neutral-Bg); + } + .ant-tabs-tab-btn { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 600; + line-height: 16px; + } + .anticon { + margin-right: 8px; + svg { + width: 16px; + height: 16px; + color: var(--Colors-Use-Neutral-Text-3-Secondary); + } + } + } + .ant-tabs-tab + .ant-tabs-tab { + margin-left: 0; + &::before { + content: ""; + position: absolute; + height: 16px; + width: 1px; + background-color: (--Colors-Use-Neutral-Border); + left: 0px; + } + } + // 选中 + .ant-tabs-tab.ant-tabs-tab-active { + border: 0; + background: var(--Colors-Use-Main-Bg); + color: var(--Colors-Use-Main-On-Primary); + &::after { + content: ""; + position: absolute; + width: 100%; + bottom: -1px; + left: 0; + border-bottom: 3px solid var(--Colors-Use-Main-Primary); + } + .ant-tabs-tab-btn { + color: var(--Colors-Use-Main-Primary); + text-shadow: unset; + } + .anticon { + svg { + color: var(--Colors-Use-Main-Primary); + } + } + } + // 禁用 + .ant-tabs-tab.ant-tabs-tab-disabled { + background: var(--Colors-Use-Neutral-Bg); + .ant-tabs-tab-btn { + color: var(--Colors-Use-Neutral-Disable); + } + } + } + } + } +} diff --git a/src/compoments/businessUI/PluginTabs/PluginTabs.tsx b/src/compoments/businessUI/PluginTabs/PluginTabs.tsx new file mode 100644 index 00000000..5de7b815 --- /dev/null +++ b/src/compoments/businessUI/PluginTabs/PluginTabs.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import type { TabsProps } from 'antd'; +import { Tabs } from 'antd'; + +import classNames from 'classnames'; +import styles from './PluginTabs.module.scss'; + +const { TabPane } = Tabs; + +interface PluginTabsProps extends Omit { + /** @deprecated 组件无法设置该属性,默认定值为 default */ + size?: 'default'; + type?: 'card' | 'line'; + wrapperClassName?: string; +} + +const PluginTabs: React.FC = (props) => { + const { + children, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + size = 'default', + type = 'card', + wrapperClassName = '', + ...rest + } = props; + return ( +
    + + {children} + +
    + ); +}; + +/** @name 插件功能页面相关 Tabs 组件 */ +export default Object.assign(PluginTabs, { TabPane }); diff --git a/src/compoments/configNetwork/ConfigNetworkPage.module.scss b/src/compoments/configNetwork/ConfigNetworkPage.module.scss new file mode 100644 index 00000000..763ad5d9 --- /dev/null +++ b/src/compoments/configNetwork/ConfigNetworkPage.module.scss @@ -0,0 +1,790 @@ +.certificate-box { + margin-top: 10px; + display: flex; + flex-direction: row; + flex-wrap: wrap; + /* 这里设置为wrap */ + gap: 8px; + + .certificate-succee { + background-color: var(--Colors-Use-Green-Bg); + } + + .certificate-fail { + background-color: var(--Colors-Use-Red-Bg); + } + + .certificate-card-item { + border: 1px solid var(--Colors-Use-Neutral-Border); + border-radius: 4px; + padding: 2px; + + &:hover { + border: 1px solid var(--Colors-Use-Main-Primary); + } + + .certificate-card { + width: 104px; + height: 136px; + border-radius: 4px; + + position: relative; + + .decorate { + position: absolute; + right: 0px; + } + + .union { + position: absolute; + bottom: 4px; + right: 4px; + } + + .card-hide { + width: 0; + height: 0; + border-left: 17px solid transparent; + border-right: 17px solid transparent; + border-bottom: 17px solid var(--Colors-Use-Neutral-Border); + position: absolute; + right: -12px; + top: -3px; + transform: rotate(45deg); + } + + .fail-main { + display: flex; + align-items: center; + flex-direction: column; + padding: 16px 20px 18px; + + .title { + color: var(--Colors-Use-Neutral-Text-1-Title); + text-align: center; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 20px; + margin-bottom: 8px; + } + + .content { + color: var(--Colors-Use-Error-Primary); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + margin-bottom: 8px; + } + + &:hover { + .close { + display: block; + } + } + } + + .success-main { + display: flex; + align-items: center; + flex-direction: column; + padding: 16px 20px 18px; + + .title { + color: var(--Colors-Use-Neutral-Text-1-Title); + text-align: center; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 20px; + margin-bottom: 8px; + } + + .content { + color: var(--Colors-Use-Neutral-Text-3-Secondary); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + margin-bottom: 8px; + } + + .password { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 20px; + } + + &:hover { + .close { + display: block; + } + } + } + } + + &-footer { + display: flex; + gap: 8px; + justify-content: center; + padding: 4px 0; + + .icon-cog { + svg { + width: 18px; + height: 18px; + color: var(--Colors-Use-Neutral-Text-3-Secondary); + cursor: pointer; + + &:hover { + color: var(--Colors-Use-Main-Primary); + } + } + } + + .icon-trash { + svg { + width: 18px; + height: 18px; + color: var(--Colors-Use-Error-Primary); + cursor: pointer; + } + } + } + } +} + +.form-rule-code-customize-describe { + color: var(--Colors-Use-Neutral-Text-1-Title); + position: absolute; + font-size: 14px; + font-weight: 400; + transform: translate(32px, 10px); +} + +.form-rule-code-customize-item { + margin-top: 38px; +} + +.form-rule-body { + display: flex; + align-items: center; + + .form-rule { + height: 28px; + width: 100%; + background: var(--Colors-Use-Basic-Background); + border: 1px solid var(--Colors-Use-Neutral-Border); + color: var(--Colors-Use-Neutral-Text-1-Title); + border-radius: 4px; + display: flex; + align-items: center; + cursor: pointer; + + &:hover { + border-color: var(--Colors-Use-Main-Primary); + } + + &:focus, + &:active { + box-shadow: 0 0 0 2px var(--Colors-Use-Main-Focus); + } + + .form-rule-text { + padding: 6px 12px; + flex: 1; + font-size: 14px; + } + + .form-rule-icon { + width: 33px; + height: 26px; + background: var(--Colors-Use-Neutral-Bg-Hover); + display: flex; + align-items: center; + justify-content: center; + border-left: 1px solid var(--Colors-Use-Neutral-Border); + border-radius: 0px 4px 4px 0px; + cursor: pointer; + + &:hover { + svg { + color: var(--Colors-Use-Main-Primary); + } + } + + svg { + height: 16px; + width: 16px; + color: var(--Colors-Use-Neutral-Text-1-Title); + } + } + } +} + +.ntlm-config-drawer { + :global { + .ant-drawer-body { + padding: 0; + overflow: hidden; + } + } + + .heard-title { + display: flex; + gap: 8px; + + .title { + font-weight: 600; + font-size: 14px; + color: var(--Colors-Use-Neutral-Text-1-Title); + height: 20px; + line-height: 20px; + } + + .table-total { + font-weight: 400; + font-size: 12px; + color: var(--Colors-Use-Neutral-Disable); + + span { + color: var(--Colors-Use-Main-Primary); + } + } + } + + .heard-right-operation { + display: flex; + justify-content: flex-end; + align-items: center; + + .button-text:first-child { + margin-right: 0; + } + + .button-create, + .button-save { + margin-right: 8px; + } + + .icon-remove { + color: var(--Colors-Use-Neutral-Disable); + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + + svg { + width: 16px; + height: 16px; + } + + :hover { + color: var(--Colors-Use-Main-Hover); + } + } + } + + .ntlm-config-table { + height: 100%; + padding: 4px 8px 16px; + + .table-action-icon { + padding: 0 4px; + display: flex; + align-items: center; + + .action-icon { + svg { + &:hover { + color: var(--Colors-Use-Main-Primary); + } + } + } + + .action-icon-edit-disabled { + svg { + color: var(--Colors-Use-Neutral-Disable); + cursor: not-allowed; + + &:hover { + color: var(--Colors-Use-Neutral-Disable); + } + } + } + + .action-icon-ban-disabled { + svg { + color: var(--Colors-Use-Main-Primary); + } + } + + span { + padding: 0 12px; + border-right: 1px solid var(--Colors-Use-Neutral-Border); + color: var(--Colors-Use-Neutral-Text-1-Title); + + &:first-child { + padding-left: 0; + } + } + + .icon-trash { + color: var(--Colors-Use-Error-Primary); + } + } + } +} + +.config-select-path { + color: var(--Colors-Use-Main-Primary); + font-size: 12px; + cursor: pointer; +} + +.ai-sort-box { + width: 300px; +} + +.ai-sort-content { + .ai-sort-describe { + color: var(--Colors-Use-Neutral-Text-1-Title); + } + + .menu-list { + border-radius: 4px; + border: 1px solid var(--Colors-Use-Neutral-Border); + overflow: auto; + flex: 1; + + &-item { + display: flex; + padding: 2px 8px 2px 12px; + justify-content: space-between; + align-items: center; + align-self: stretch; + height: 24px; + box-sizing: content-box; + cursor: pointer; + + &-info { + display: flex; + align-items: center; + + .input { + margin-left: 12px; + } + + .drag-sort-icon { + color: var(--Colors-Use-Neutral-Disable); + + svg { + height: 16px; + width: 16px; + } + } + + .title { + margin-left: 12px; + font-size: 12px; + font-weight: 500; + line-height: 16px; + color: var(--Colors-Use-Neutral-Text-1-Title); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + &:hover { + border-radius: 4px; + background: var(--Colors-Use-Neutral-Bg-Hover); + + .form-outlined { + display: block; + } + + .trash-icon { + display: block; + } + } + } + + &-item-drag { + background: var(--Colors-Use-Main-Bg) !important; + border: 1px solid var(--Colors-Use-Main-Border) !important; + border-radius: 4px; + color: var(--Colors-Use-Neutral-Text-1-Title) !important; + + svg { + color: var(--Colors-Use-Main-Primary) !important; + } + } + } +} + +.sort-active-item { + padding: 2px; + display: flex; + justify-content: center; + border: 1px solid var(--Colors-Use-Neutral-Border); + margin-bottom: 8px; +} + +.config-form-wrapper { + .config-form { + padding: 24px 24px 0 24px; + .ai-third-party-application-config-collapse { + .panel-heard { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + line-height: 16px; + .title { + font-weight: 500; + color: var(--Colors-Use-Neutral-Text-1-Title); + } + .tip { + color: var(--Colors-Use-Neutral-Text-4-Help-text); + } + } + :global(.ant-collapse-header:hover) { + .panel-heard { + background-color: var(--Colors-Use-Basic-Background); + .title { + color: var(--Colors-Use-Main-Primary); + } + } + } + :global { + .ant-collapse-item { + background-color: var(--Colors-Use-Basic-Background); + .ant-collapse-header { + padding: 0; + &:hover { + background-color: var(--Colors-Use-Basic-Background); + color: var(--Colors-Use-Main-Primary); + .ant-collapse-expand-icon { + span { + svg { + color: var(--Colors-Use-Main-Primary); + } + } + } + } + } + .ant-collapse-content-box { + padding: 0; + } + } + } + } + } + .config-form-ai { + padding: 24px 24px 16px 24px; + } + + .ai-third-party-readonly { + padding: 24px 24px 16px 24px; + .readonly-ai-field-row { + display: flex; + align-items: center; + margin-bottom: 16px; + min-height: 32px; + } + .readonly-ai-label { + flex: 0 0 20.833%; + max-width: 200px; + padding-right: 12px; + text-align: right; + font-size: 14px; + line-height: 32px; + color: var(--Colors-Use-Neutral-Text-2); + word-break: keep-all; + } + .readonly-ai-control { + flex: 1; + min-width: 0; + } + } + + .ai-readonly-copy-input { + width: 100%; + :global { + .ant-input, + .ant-input-group .ant-input { + background-color: #f5f5f5 !important; + color: var(--Colors-Use-Neutral-Text-1-Title); + cursor: text; + } + .ant-input-group-addon { + background-color: #f5f5f5 !important; + border-color: var(--Colors-Use-Neutral-Border); + padding: 0 6px; + } + } + } + + .config-footer { + padding: 10px 24px; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 12px; + border-top: 1px solid var(--Colors-Use-Neutral-Border); + border-radius: 0 0 4px 4px; + background: var(--Colors-Use-Neutral-Bg); + } + :global { + .ant-form-item .ant-form-item-label > label { + height: 28px; + } + } +} + +.proxy-rules-config-overlay { + .textarea-header { + display: flex; + align-items: center; + gap: 4px; + .header-title { + font-weight: 600; + font-size: 16px; + line-height: 24px; + color: var(--Colors-Use-Neutral-Text-1-Title); + word-break: keep-all; + white-space: nowrap; + } + .header-hint-icon { + cursor: pointer; + svg { + color: var(--Colors-Use-Neutral-Text-1-Title); + width: 16px; + height: 16px; + } + } + } + + .icon-trash { + svg { + width: 18px; + height: 18px; + color: var(--Colors-Use-Error-Primary); + cursor: pointer; + } + } + + .icon-ban-active { + svg { + color: var(--Colors-Use-Main-Primary); + } + } + + .config-tab-item { + transform: rotate(180deg); + text-orientation: sideways-right; + writing-mode: vertical-rl; + } + + .disabled-badge { + display: inline-block; + margin-left: 8px; + padding: 2px 8px; + line-height: 16px; + background-color: var(--Colors-Use-Error-Primary); + color: var(--Colors-Use-Main-On-Primary); + border-radius: 10px; + vertical-align: middle; + box-shadow: none; + } + + .row-disabled { + :global { + .ant-table-cell { + color: var(--Colors-Use-Neutral-Disable) !important; + background-color: var(--Colors-Use-Neutral-Bg-Hover); + } + } + } + + .password_display_icon_text { + width: 200px; + overflow: hidden; + display: inline-block; + } + + .password_display_icon_eye svg { + width: 18px; + height: 18px; + cursor: pointer; + } + + :global { + .ant-table-cell:hover { + background: var(--Colors-Use-Basic-Background) !important; + } + + .ant-drawer-header-title { + flex-direction: row-reverse; + } + + .ant-drawer-body { + padding: 0; + } + + .ant-table.ant-table-bordered > .ant-table-container { + border-color: var(--Colors-Use-Neutral-Border); + } + .ant-table.ant-table-bordered > .ant-table-container > .ant-table-content > table, + .ant-table.ant-table-bordered > .ant-table-container > .ant-table-header > table { + border-color: var(--Colors-Use-Neutral-Border); + } + + .ant-table-thead > tr > th { + color: var(--Colors-Use-Neutral-Text-1-Title); + background: var(--Colors-Use-Neutral-Bg); + } + + .ant-table-tbody > tr > td { + background: var(--Colors-Use-Basic-Background); + } + + .ant-table-thead > tr > th, + .ant-table-tbody > tr > td { + border-bottom: 1px solid var(--Colors-Use-Neutral-Border); + } + + .ant-table.ant-table-bordered > .ant-table-container > .ant-table-content > table > thead > tr > th, + .ant-table.ant-table-bordered > .ant-table-container > .ant-table-content > table > tbody > tr > td { + border-right: 1px solid var(--Colors-Use-Neutral-Border); + color: var(--Colors-Use-Neutral-Text-1-Title); + } + + .ant-table-tbody > tr.ant-table-row:hover > td, + .ant-table-tbody > tr > td.ant-table-cell-row-hover { + background: var(--Colors-Use-Neutral-Bg-Hover) !important; + } + + .ant-pagination-total-text { + color: var(--Colors-Use-Neutral-Text-1-Title); + } + } +} + +.proxy-rules-config-modal .header-hint-icon { + cursor: pointer; + svg { + color: var(--Colors-Use-Neutral-Text-1-Title); + width: 16px; + height: 16px; + } +} + +.destination-address { + margin-bottom: 0 !important; + :global { + .ant-form-item-extra { + color: var(--Colors-Use-Neutral-Text-4-Help-text); + } + } +} + +.proxy-test-title { + color: var(--Colors-Use-Main-Primary); + display: inline-flex; + cursor: pointer; +} + +.proxy-test-title-disabled { + color: var(--Colors-Use-Neutral-Disable); +} + +.proxy-test-modal-content { + display: flex; + height: 400px; + flex-direction: column; + + .test-modal-content-res { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + + .proxy-test-modal-loading { + display: contents; + } + + .proxy-test-modal-detail { + width: 100%; + padding: 8px; + margin: 8px; + border-radius: 8px; + background: var(--Colors-Use-Neutral-Bg-Hover); + min-height: 60px; + display: flex; + align-items: center; + white-space: pre-line; + + svg { + margin-right: 6px; + color: var(--Colors-Use-Error-Primary); + } + } + + .proxy-test-modal-failed svg { + height: 48px; + width: 48px; + margin-right: 4px; + color: var(--Colors-Use-Error-Primary); + margin-bottom: 12px; + } + + .proxy-test-modal-success { + display: flex; + align-items: center; + margin-bottom: 20px; + + svg { + height: 48px; + width: 48px; + margin-right: 4px; + color: var(--Colors-Use-Success-Primary); + } + span { + font-size: 22px; + font-weight: bold; + color: var(--Colors-Use-Success-Primary); + } + } + } +} + +.ai-model-global-config-wrapper { + :global { + .ant-form-item:not(:last-child) { + margin-bottom: 0; + } + } + .ai-model-list-wrapper { + display: flex; + flex-direction: column; + width: 500px; + gap: 8px; + :global { + .ant-collapse-item .ant-collapse-content > .ant-collapse-content-box { + padding: 0 8px 8px; + } + } + .ai-model-list-header { + display: flex; + align-items: center; + } + } +} diff --git a/src/compoments/configNetwork/ConfigNetworkPage.tsx b/src/compoments/configNetwork/ConfigNetworkPage.tsx new file mode 100644 index 00000000..9f7d9af8 --- /dev/null +++ b/src/compoments/configNetwork/ConfigNetworkPage.tsx @@ -0,0 +1,45 @@ +import type { KVPair } from '@/pages/AIAgent/enums/external' + +export interface ThirdPartyApplicationConfig { + Type: + | 'zoomeye' + | 'hunter' + | 'shodan' + | 'fofa' + | 'github' + | 'openai' + | 'skylark' + | 'aliyun' + | 'tencent' + | 'quake' + | string + APIKey?: string + UserIdentifier?: string + UserSecret?: string + Namespace?: string + Domain?: string + WebhookURL?: string + ExtraParams?: KVPair[] + Disabled?: boolean + Proxy?: string + NoHttps?: boolean + APIType?: string + BaseURL?: string + Endpoint?: string + EnableEndpoint?: boolean + Headers?: KVPair[] + /** 为空,不传给后端 */ + MaxTokens?: number + /** 为空,不传给后端 */ + Temperature?: number + /** 为空,不传给后端 */ + TopP?: number + /** 为空,不传给后端 */ + TopK?: number + /** 为空,不传给后端 */ + FrequencyPenalty?: number + /** 为空,不传给后端 */ + ReasoningEffort?: string + /** 为空,不传给后端 */ + EnableThinkingOpt?: boolean +} diff --git a/src/compoments/configNetwork/NewThirdPartyApplicationConfig.tsx b/src/compoments/configNetwork/NewThirdPartyApplicationConfig.tsx new file mode 100644 index 00000000..1e93f323 --- /dev/null +++ b/src/compoments/configNetwork/NewThirdPartyApplicationConfig.tsx @@ -0,0 +1,1310 @@ +import type { FormLayout } from 'antd/es/form/Form' +import type { ThirdPartyApplicationConfig } from './ConfigNetworkPage' +import { YakitSelect } from '../yakitUI/YakitSelect/YakitSelect' +import { Collapse, Form } from 'antd' +import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' +import styles from './ConfigNetworkPage.module.scss' +import type { FormInstance } from 'antd/lib/form/Form' +import type { KVPair } from '@/pages/AIAgent/enums/external' +import type { SelectOptionsProps } from '@/pages/AIAgent/ai-agent/aiModelList/AIModelListType' +import { useGetSetState } from '@/hooks' +import { + AI_API_TYPE_OPTIONS, + DEFAULT_AI_API_TYPE, + grpcGetAIThirdPartyAppConfigTemplate, + normalizeAIAPIType, +} from '@/pages/AIAgent/ai-agent/aiModelList/utils' +import { cloneDeep } from 'lodash' +import { AIModelTypeEnum } from '@/pages/AIAgent/ai-agent/defaultConstant' +import { useCreation, useDebounceEffect, useDebounceFn, useMemoizedFn, useUpdateEffect } from 'ahooks' +import { OutlineInformationcircleIcon } from '@/assets/icon/outline' +import { YakitInput } from '../yakitUI/YakitInput/YakitInput' +import { YakitSwitch } from '../yakitUI/YakitSwitch/YakitSwitch' +import { YakitButton } from '../yakitUI/YakitButton/YakitButton' +import { YakitAutoComplete } from '../yakitUI/YakitAutoComplete/YakitAutoComplete' +import { YakitSpin } from '../yakitUI/YakitSpin/YakitSpin' +import { AIConfigAPIKeyFormItem } from '@/pages/AIAgent/ai-agent/aiModelList/aiModelForm/AIModelForm' +import type { YakitSelectProps } from '../yakitUI/YakitSelect/YakitSelectType' +import { postSettingAimodelsGet } from '@/apis/AiEventApi' +import { yakitNotify } from '@/utils/notification' +import YakitCollapse from '../yakitUI/YakitCollapse/YakitCollapse' +import { YakitTag } from '../yakitUI/YakitTag/YakitTag' +import classNames from 'classnames' +import { YakitModal } from '../yakitUI/YakitModal/YakitModal' +import { OutlineClipboardcopyIcon } from '@/assets/icon/outline' +import { setClipboardText } from '@/utils/clipboard' +import { YakitInputNumber } from '../yakitUI/YakitInputNumber/YakitInputNumber' + +export interface ThirdPartyAppConfigItemTemplate { + Required: boolean + Name: string + Verbose: string + Type: string + DefaultValue: string + Desc: string + Extra: string + Placeholder?: string +} + +export interface GetThirdPartyAppConfigTemplate { + Name: string + Verbose: string + Type: string + Items: ThirdPartyAppConfigItemTemplate[] +} + +export interface GetThirdPartyAppConfigTemplateResponse { + Templates: GetThirdPartyAppConfigTemplate[] +} + +export interface ThirdPartyApplicationConfigProp { + formValues?: ThirdPartyApplicationConfig + // 禁止类型改变 + disabledType?: boolean + // 是否可新增类型 + canAddType?: boolean + // 类型下拉是否只展示ai类型的 + isOnlyShowAiType?: boolean + onAdd: (i: ThirdPartyApplicationConfig) => void + onCancel: () => void + FormProps?: { + layout: FormLayout + labelCol: number + wrapperCol: number + } + + footer?: React.ReactNode +} + +const defautFormValues = { + Type: '', + api_key: '', + user_identifier: '', + ExtraParams: [] as KVPair[], +} + +export const EnableThinkingOptions = [ + { label: '不设置', value: 'no-set' }, + { label: '开启', value: 'open' }, + { label: '不开启', value: 'close' }, +] + +const defaultFormItems: ThirdPartyAppConfigItemTemplate[] = [ + { + DefaultValue: '', + Desc: 'APIKey / Token', + Extra: '', + Name: 'api_key', + Required: true, + Type: 'string', + Verbose: 'ApiKey', + }, + { + DefaultValue: '', + Desc: 'email / username', + Extra: '', + Name: 'user_identifier', + Required: false, + Type: 'string', + Verbose: '用户信息', + }, +] + +const aiModelTypeOptions: SelectOptionsProps[] = [ + { + label: '高质模型:执行复杂任务', + value: AIModelTypeEnum.TierIntelligent, + }, + { + label: '轻量模型:用于执行简单任务和会话', + value: AIModelTypeEnum.TierLightweight, + }, + { + label: '视觉模式:用于识别图片等,生成知识库和任务执行都会用到', + value: AIModelTypeEnum.TierVision, + }, +] +const aiAPITypeOptions: SelectOptionsProps[] = AI_API_TYPE_OPTIONS.map((item) => ({ label: item, value: item })) +const aiModelTypeItem: ThirdPartyAppConfigItemTemplate = { + Name: 'model_type', + Required: true, + Type: 'list', + DefaultValue: AIModelTypeEnum.TierIntelligent, + Desc: '', + Extra: `${JSON.stringify({ + options: aiModelTypeOptions, + })}`, + Verbose: '模型类型', +} + +interface NewThirdPartyApplicationConfigBaseProps + extends Omit { + ref?: React.ForwardedRef<{ form: FormInstance }> +} + +export const NewThirdPartyApplicationConfigBase: React.FC = React.memo( + forwardRef((props, ref) => { + const { formValues = defautFormValues, disabledType = false, canAddType = true, FormProps, footer } = props + const [form] = Form.useForm() + const typeVal = Form.useWatch('Type', form) + const typeValRef = useRef(typeVal) + const [options, setOptions] = useState([]) + const [templates, setTemplates, getTemplates] = useGetSetState([]) + const [modelOptionLoading, setModelOptionLoading] = useState(false) + const [modelNameAllOptions, setModelNameAllOptions] = useState([]) + const apiKeyWatch = Form.useWatch('api_key', form) + const execModelNameOption = useRef(false) + + useImperativeHandle( + ref, + () => ({ + form, + }), + [form], + ) + + // 获取类型 + useEffect(() => { + grpcGetAIThirdPartyAppConfigTemplate().then((res: GetThirdPartyAppConfigTemplateResponse) => { + const templates = res.Templates + let newOptions: SelectOptionsProps[] = [] + setTemplates(templates) + // eslint-disable-next-line max-nested-callbacks + newOptions = templates.map((item) => ({ + label: item.Verbose, + value: item.Name, + })) + setOptions(newOptions) + }) + }, []) + + useUpdateEffect(() => { + const templatesobj = getTemplates().find((item) => item.Name === typeValRef.current) + const modelType = templatesobj?.Type + if (apiKeyWatch && modelType === 'ai') { + execModelNameOption.current = true + getModelNameOption() + } else { + handleDefaultModalNameOption() + } + }, [apiKeyWatch]) + + const { run: getModelNameOption, cancel: cancelModelNameOption } = useDebounceFn( + useMemoizedFn(() => { + if (!execModelNameOption.current) return + setModelOptionLoading(true) + + const v = form.getFieldsValue() + postSettingAimodelsGet({ + Config: JSON.stringify(v), + }) + .then((res) => { + if (!execModelNameOption.current) return + const modalNamelist: SelectOptionsProps[] = + // eslint-disable-next-line max-nested-callbacks + res.ModelName.map((modelName: string) => ({ + label: modelName, + value: modelName, + })) + const name = getModelNameDefaultName() + // 确保默认值在选项里 + const hasDefault = modalNamelist.some( + // eslint-disable-next-line max-nested-callbacks + (item) => item.value === name, + ) + const newOptions = hasDefault + ? modalNamelist + : name + ? [{ label: name, value: name }, ...modalNamelist] + : modalNamelist + setModelNameAllOptions(newOptions) + yakitNotify('success', '获取成功') + }) + .catch((error) => { + if (!execModelNameOption.current) return + yakitNotify('error', error) + handleDefaultModalNameOption() + }) + .finally(() => { + setModelOptionLoading(false) + }) + }), + { wait: 500 }, + ) + const getModelNameDefaultName = () => { + const templatesobj = templates.find((item) => item.Name === typeVal) + const formItems = templatesobj?.Items || [] + const modelType = templatesobj?.Type + const obj = formItems.find((item) => modelType === 'ai' && item.Type === 'list' && item.Name === 'model') + return obj?.DefaultValue + } + const handleDefaultModalNameOption = () => { + const name = getModelNameDefaultName() + if (name) { + setModelNameAllOptions([{ label: name, value: name }]) + } else { + setModelNameAllOptions([]) + } + } + useDebounceEffect( + () => { + handleDefaultModalNameOption() + }, + [typeVal], + { wait: 300 }, + ) + useEffect(() => { + execModelNameOption.current = false + cancelModelNameOption() + typeValRef.current = typeVal + }, [typeVal]) + + // 切换类型,渲染不同表单项(目前只有输入框、开关、下拉) + const renderAllFormItems = useMemoizedFn(() => { + const templatesobj = templates.find((item) => item.Name === typeVal) + const formItems = templatesobj?.Items || [] + const modelType = templatesobj?.Type + return formItems.map((item, index) => ( + {renderSingleFormItem(item, modelType)} + )) + }) + const renderSingleFormItem = (item: ThirdPartyAppConfigItemTemplate, modelType?: string) => { + const formProps = { + rules: [ + { + required: item.Required, + message: `请填写${item.Verbose}`, + }, + ], + label: item.Verbose, + name: item.Name, + tooltip: item.Desc + ? { + icon: , + title: item.Desc, + } + : null, + } + switch (item.Type) { + case 'string': + return ( + + + + ) + case 'bool': + return ( + + + + ) + case 'list': + if (modelType === 'ai' && item.Name === 'model') { + // 模型名称 + return ( + + 如无法自动获取,请 + { + execModelNameOption.current = true + getModelNameOption() + }} + style={{ + padding: 0, + fontSize: 14, + }} + > + 点击刷新 + + 重新获取 +
    + } + > + { + execModelNameOption.current = true + getModelNameOption() + }} + // eslint-disable-next-line react/no-unstable-nested-components + dropdownRender={(menu) => { + return ( + // eslint-disable-next-line react/jsx-no-useless-fragment + <> + {menu} + + ) + }} + filterOption={(inputValue, option) => { + if (option?.value && typeof option?.value === 'string') { + return option?.value?.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 + } + return false + }} + /> + + ) + } + + // eslint-disable-next-line no-case-declarations + let selectProps: YakitSelectProps = {} + try { + selectProps.options = item.Extra ? JSON.parse(item.Extra)?.options : [] + } catch (error) {} + return ( + + + + ) + + default: + // eslint-disable-next-line react/jsx-no-useless-fragment + return <> + } + } + // 判断当前类型值是否在options存在 + const isInOptions = useMemo(() => { + return options.findIndex((item) => item.value === typeVal) !== -1 + }, [options, typeVal]) + + const initialValues = useMemo(() => { + const copyFormValues: any = { ...formValues } + Object.keys(copyFormValues).forEach((key) => { + if (copyFormValues[key] === 'true') { + copyFormValues[key] = true + } else if (copyFormValues[key] === 'false') { + copyFormValues[key] = false + } + }) + return copyFormValues + }, [formValues]) + + const defaultFormList = useCreation(() => { + // eslint-disable-next-line no-extra-boolean-cast + return [...defaultFormItems] + }, []) + return ( +
    +
    { + // 当类型改变时,表单项的值采用默认值 + if (changedValues.Type !== undefined) { + const templatesobj = templates.find((item) => item.Name === changedValues.Type) + const formItems = templatesobj?.Items || [] + formItems.forEach((item) => { + form.setFieldsValue({ + [item.Name]: ['string', 'list'].includes(item.Type) + ? item.DefaultValue + : item.DefaultValue === 'true', + }) + }) + } + }} + onSubmitCapture={(e) => { + e.preventDefault() + }} + className={styles['config-form']} + > + + {canAddType ? ( + { + if (option?.label && typeof option?.label === 'string') { + return option?.label?.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 + } + return false + }} + /> + ) : ( + { + if (option?.label && typeof option?.label === 'string') { + return option?.label?.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 + } + return false + }} + /> + )} + + {isInOptions ? ( + <>{renderAllFormItems()} + ) : ( + <> + {defaultFormList.map((item: any, index: number) => ( + {renderSingleFormItem(item)} + ))} + + )} + +
    {footer}
    +
    + ) + }), +) + +const defaultAIFormItemsOfAI: ThirdPartyAppConfigItemTemplate[] = [ + cloneDeep(aiModelTypeItem), + { + Name: 'api_type', + Required: true, + Type: 'list', + DefaultValue: DEFAULT_AI_API_TYPE, + Desc: '可选值: chat_completions / responses', + Extra: `${JSON.stringify({ + options: aiAPITypeOptions, + })}`, + Verbose: 'API类型', + }, + { + DefaultValue: 'memfit-light-free', + Desc: '', + Extra: '', + Name: 'model', + Required: true, + Type: 'list', + Verbose: '模型名称', + }, +] + +const isShowRequiredApiKey = (typeVal: string) => { + let Required = !['aibalance', 'comate'].includes(typeVal) + return { + isRequired: Required, + data: { + DefaultValue: 'free-user', + Desc: 'APIKey / Token', + Extra: '', + Name: 'api_key', + Required, + Type: 'list', + Verbose: 'ApiKey', + }, + } +} + +const pickOptionLabel = (opts: SelectOptionsProps[], value: unknown) => { + if (value === undefined || value === null || value === '') { + return '' + } + const hit = opts.find((o) => o.value === value) + return (hit?.label as string) || String(value) +} + +const formatReadonlyEmptyAsDash = (v: unknown) => { + if (v === undefined || v === null || v === '') { + return '-' + } + if (typeof v === 'boolean') { + return v ? 'true' : 'false' + } + return String(v) +} + +const headersToDisplayAndCopy = (headers: KVPair[] | undefined) => { + if (!headers?.length) { + return { display: '-', copy: '' } + } + const lines = headers.map((h) => `${h.Key}: ${h.Value}`) + return { display: lines.join('\n'), copy: lines.join('\n') } +} + +const buildDefaultAIFormItemsForType = (typeVal: string) => { + const items = cloneDeep(defaultAIFormItemsOfAI) + const { isRequired, data } = isShowRequiredApiKey(typeVal) + if (isRequired) { + items.push(data) + } + return items +} + +const buildOptionalAIFormItemsForType = (typeVal: string, enableEndpoint: boolean) => { + let newData = cloneDeep(optionalAIFormItemsOfAI) + const { isRequired, data } = isShowRequiredApiKey(typeVal) + if (!isRequired) { + newData.unshift(data) + } + if (enableEndpoint) { + return newData.filter((item) => item.Name !== 'base_url') + } + return newData.filter((item) => item.Name !== 'endpoint') +} + +const optionalAIFormItemsOfAI: ThirdPartyAppConfigItemTemplate[] = [ + { + DefaultValue: '', + Desc: '对于非标准openai风格的第三方api,可以自定义endpoint', + Extra: '', + Name: 'enable_endpoint', + Required: false, + Type: 'bool', + Verbose: '启用Endpoint', + }, + { + DefaultValue: '', + Desc: '可填写域名和URL,例如api.openai.com、https://api.openai.com/、https://api.openai.com/v1、https://api.openai.com//v1/chat/completions', + Extra: '', + Name: 'base_url', + Required: false, + Type: 'string', + Verbose: 'BaseURL', + Placeholder: '例如 api.openai.com、https://api.openai.com/', + }, + { + DefaultValue: '', + Desc: 'Endpoint', + Extra: '', + Name: 'endpoint', + Required: false, + Type: 'string', + Verbose: 'Endpoint', + Placeholder: '例如 https://api.openai.com/v1/chat/completions', + }, + { + DefaultValue: '', + Desc: '代理地址', + Extra: '', + Name: 'proxy', + Required: false, + Type: 'string', + Verbose: '代理地址', + }, +] + +export interface HTTPHeader { + Header: string + Value: string +} + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +type AIThirdPartyConfigReadonlyPanelProps = { + merged: Record +} + +const AIThirdPartyConfigReadonlyPanel: React.FC = React.memo((props) => { + const { merged } = props + const typeVal = String(merged.Type ?? '') + const enableEndpoint = !!merged.enable_endpoint + const defaultItems = useMemo(() => buildDefaultAIFormItemsForType(typeVal), [typeVal]) + const optionalItems = useMemo( + () => buildOptionalAIFormItemsForType(typeVal, enableEndpoint), + [typeVal, enableEndpoint], + ) + + const renderCopyRow = useMemoizedFn((key: string, label: string, display: string, clip: string) => ( +
    +
    {label}:
    +
    + } + onClick={() => setClipboardText(clip)} + /> + } + /> +
    +
    + )) + + const renderFieldByTemplate = useMemoizedFn((item: ThirdPartyAppConfigItemTemplate) => { + const raw = merged[item.Name] + if (item.Name === 'model_type' && item.Type === 'list') { + const label = pickOptionLabel(aiModelTypeOptions, raw) + const display = label || formatReadonlyEmptyAsDash(raw) + return renderCopyRow(item.Name, item.Verbose, display, String(raw ?? '')) + } + if (item.Name === 'api_type' && item.Type === 'list') { + const label = pickOptionLabel(aiAPITypeOptions, raw) + const display = label || formatReadonlyEmptyAsDash(raw) + return renderCopyRow(item.Name, item.Verbose, display, String(raw ?? '')) + } + if (item.Name === 'api_key') { + const display = formatReadonlyEmptyAsDash(raw) + return renderCopyRow(item.Name, item.Verbose, display, String(raw ?? '')) + } + if (item.Type === 'bool') { + const display = formatReadonlyEmptyAsDash(raw) + const clip = + raw === true || raw === 'true' ? 'true' : raw === false || raw === 'false' ? 'false' : String(raw ?? '') + return renderCopyRow(item.Name, item.Verbose, display, clip) + } + const display = formatReadonlyEmptyAsDash(raw) + return renderCopyRow(item.Name, item.Verbose, display, String(raw ?? '')) + }) + const headersPack = headersToDisplayAndCopy((merged.Headers as KVPair[]) || []) + + return ( +
    + {renderCopyRow('Type', '厂商', merged.Type, String(merged.Type ?? ''))} + {defaultItems.map((item) => renderFieldByTemplate(item))} + + + {optionalItems.map((item) => renderFieldByTemplate(item))} + {renderCopyRow('Headers', 'Header', headersPack.display, headersPack.copy)} + + +
    + ) +}) + +export interface AIThirdPartyApplicationConfig { + Type: string + api_key?: string + user_identifier?: string + model_type?: string + api_type?: string + model?: string + enable_endpoint?: boolean + base_url?: string + endpoint?: string + proxy?: string + Headers?: KVPair[] + ExtraParams?: KVPair[] + EnableThinkingOpt?: string + MaxTokens?: number + Temperature?: number + TopP?: number + TopK?: number + FrequencyPenalty?: number + ReasoningEffort?: string +} + +interface NewAIThirdPartyApplicationConfigBaseProps { + formValues?: AIThirdPartyApplicationConfig + // 禁止类型改变 + disabledType?: boolean + // 是否可新增类型 + canAddType?: boolean + FormProps?: { + layout: FormLayout + labelCol: number + wrapperCol: number + } + footer?: React.ReactNode + readOnly?: boolean + ref?: React.ForwardedRef<{ form: FormInstance }> +} + +export const NewAIThirdPartyApplicationConfigBase: React.FC = React.memo( + forwardRef((props, ref) => { + const { + formValues = defautFormValues as AIThirdPartyApplicationConfig, + disabledType = false, + canAddType = true, + FormProps, + footer, + readOnly, + } = props + const [form] = Form.useForm() + const typeVal = Form.useWatch('Type', form) + const [options, setOptions] = useState([]) + const [modelOptionLoading, setModelOptionLoading] = useState(false) + const [modelNameAllOptions, setModelNameAllOptions] = useState([]) + const apiKeyWatch = Form.useWatch('api_key', form) + const execModelNameOption = useRef(false) + const enableEndpointWatch = Form.useWatch('enable_endpoint', form) + const headers = Form.useWatch('Headers', form) || [] + const [visibleHTTPHeader, setVisibleHTTPHeader] = useState(false) + const headerItemRef = useRef() + const headerItemIndexRef = useRef() + const [activeKey, setActiveKey] = useState() + const onChangeCollapse = (key: string | string[]) => { + setActiveKey(key) + } + useImperativeHandle( + ref, + () => ({ + form, + }), + [form], + ) + + // 获取类型 + useEffect(() => { + grpcGetAIThirdPartyAppConfigTemplate().then((res: GetThirdPartyAppConfigTemplateResponse) => { + const templates = res.Templates + let newOptions: SelectOptionsProps[] = [] + newOptions = templates.map((item) => ({ label: item.Verbose, value: item.Name })) + setOptions(newOptions) + }) + }, []) + + useUpdateEffect(() => { + if (readOnly) return + if (apiKeyWatch) { + execModelNameOption.current = true + getModelNameOption() + } else { + handleDefaultModalNameOption() + } + }, [apiKeyWatch, readOnly]) + + const { run: getModelNameOption, cancel: cancelModelNameOption } = useDebounceFn( + useMemoizedFn(() => { + if (!execModelNameOption.current) return + setModelOptionLoading(true) + const v = form.getFieldsValue() + postSettingAimodelsGet({ + Config: JSON.stringify(v), + }) + .then((res) => { + if (!execModelNameOption.current) return + const modalNamelist: SelectOptionsProps[] = res.ModelName.map((modelName: string) => ({ + label: modelName, + value: modelName, + })) + const name = getModelNameDefaultName() + // 确保默认值在选项里 + const hasDefault = modalNamelist.some((item) => item.value === name) + const newOptions = hasDefault + ? modalNamelist + : name + ? [{ label: name, value: name }, ...modalNamelist] + : modalNamelist + setModelNameAllOptions(newOptions) + yakitNotify('success', '获取成功') + }) + .catch((error: any) => { + if (!execModelNameOption.current) return + yakitNotify('error', String(error)) + handleDefaultModalNameOption() + }) + .finally(() => { + setModelOptionLoading(false) + }) + }), + { wait: 500 }, + ) + + const newDefaultAIFormItemsOfAI = useCreation(() => { + let newAIFormItemsOfAI = cloneDeep(defaultAIFormItemsOfAI) + const { isRequired, data } = isShowRequiredApiKey(typeVal) + if (isRequired) { + newAIFormItemsOfAI.push(data) + } + return newAIFormItemsOfAI + }, [typeVal]) + + const newOptionalAIFormItemsOfAI = useCreation(() => { + let newData = cloneDeep(optionalAIFormItemsOfAI) + const { isRequired, data } = isShowRequiredApiKey(typeVal) + if (!isRequired) { + newData.unshift(data) + } + if (enableEndpointWatch) { + return newData.filter((item) => item.Name !== 'base_url') + } + return newData.filter((item) => item.Name !== 'endpoint') + }, [enableEndpointWatch, typeVal]) + + const allAIFormItemsOfAI = useCreation(() => { + return [...newDefaultAIFormItemsOfAI, ...newOptionalAIFormItemsOfAI] + }, [newDefaultAIFormItemsOfAI, newOptionalAIFormItemsOfAI]) + + const getModelNameDefaultName = () => { + const obj = allAIFormItemsOfAI.find((item) => item.Type === 'list' && item.Name === 'model') + return obj?.DefaultValue + } + const handleDefaultModalNameOption = () => { + const name = getModelNameDefaultName() + if (name) { + setModelNameAllOptions([{ label: name, value: name }]) + } else { + setModelNameAllOptions([]) + } + } + useDebounceEffect( + () => { + if (readOnly) return + handleDefaultModalNameOption() + }, + [typeVal, readOnly], + { wait: 300 }, + ) + useEffect(() => { + if (readOnly) return + execModelNameOption.current = false + cancelModelNameOption() + if (typeVal === 'custom') { + setActiveKey('1') + } else { + setActiveKey(undefined) + } + }, [typeVal, readOnly]) + + // 切换类型,渲染不同表单项(目前只有输入框、开关、下拉) + const renderAllFormItems = useMemoizedFn(() => { + return newDefaultAIFormItemsOfAI.map((item, index) => ( + {renderSingleFormItem(item)} + )) + }) + const renderOptionalFormItems = useMemoizedFn(() => { + return newOptionalAIFormItemsOfAI.map((item, index) => ( + {renderSingleFormItem(item)} + )) + }) + + const dropdownRender = useMemoizedFn((menu: React.ReactNode) => { + return {menu} + }) + + const renderSingleFormItem = (item: ThirdPartyAppConfigItemTemplate) => { + const formProps = { + rules: [{ required: item.Required, message: `请填写${item.Verbose}` }], + label: item.Verbose, + name: item.Name, + tooltip: item.Desc + ? { + icon: , + title: item.Desc, + } + : null, + } + switch (item.Type) { + case 'string': + return ( + + + + ) + case 'bool': + return ( + + + + ) + case 'list': + if (item.Name === 'model') { + // 模型名称 + return ( + + 如无法自动获取,请 + { + execModelNameOption.current = true + getModelNameOption() + }} + style={{ padding: 0, fontSize: 14 }} + > + 点击刷新 + + 重新获取 +
    + } + > + { + execModelNameOption.current = true + getModelNameOption() + }} + dropdownRender={dropdownRender} + filterOption={(inputValue: any, option: any) => { + if (option?.value && typeof option?.value === 'string') { + return option?.value?.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 + } + return false + }} + /> + + ) + } + if (item.Name === 'api_key') { + return + } else { + let selectProps: YakitSelectProps = {} + try { + selectProps.options = item.Extra ? JSON.parse(item.Extra)?.options : [] + } catch (error) {} + return ( + + + + ) + } + default: + return null + } + } + + const initialValues = useMemo(() => { + const copyFormValues: any = { ...formValues } + const aiFormValues = copyFormValues as typeof copyFormValues & { api_type?: string } + aiFormValues.api_type = normalizeAIAPIType(aiFormValues.api_type) + + Object.keys(copyFormValues).forEach((key) => { + if (copyFormValues[key] === 'true') { + copyFormValues[key] = true + } else if (copyFormValues[key] === 'false') { + copyFormValues[key] = false + } + }) + return copyFormValues + }, [formValues]) + + const onSaveHeaders = useMemoizedFn((val, updateIndex) => { + const obj = { + Key: val.Header, + Value: val.Value, + } + let headersList: KVPair[] = [] + if (updateIndex === undefined) { + headersList = [...headers, obj] + } else { + headers[updateIndex] = obj + headersList = [...headers] + } + form.setFieldsValue({ + Headers: headersList, + }) + }) + + const onRemoveHeaders = useMemoizedFn((index: number) => { + form.setFieldsValue({ + Headers: headers.filter((_: any, i: number) => i !== index), + }) + }) + + if (readOnly) { + return ( +
    + + {footer ?
    {footer}
    : null} +
    + ) + } + return ( +
    +
    { + // 当类型改变时,表单项的值采用默认值 + if (changedValues.Type !== undefined) { + allAIFormItemsOfAI.forEach((item) => { + form.setFieldsValue({ + [item.Name]: ['string', 'list'].includes(item.Type) + ? item.DefaultValue + : item.DefaultValue === 'true', + }) + }) + } + }} + onSubmitCapture={(e) => { + e.preventDefault() + }} + className={classNames(styles['config-form'], styles['config-form-ai'])} + > + + {canAddType ? ( + { + if (option?.label && typeof option?.label === 'string') { + return option?.label?.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 + } + return false + }} + /> + ) : ( + { + if (option?.label && typeof option?.label === 'string') { + return option?.label?.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 + } + return false + }} + /> + )} + + {renderAllFormItems()} + + + {/* 可选的表单项 */} + {renderOptionalFormItems()} + + {(headers || []).map((i: any, index: number) => { + return ( + { + headerItemRef.current = { + Header: i.Key, + Value: i.Value, + } + headerItemIndexRef.current = index + setVisibleHTTPHeader(true) + }} + closable + onClose={() => { + onRemoveHeaders(index) + }} + > + {i.Key} + + ) + })} + { + headerItemRef.current = undefined + headerItemIndexRef.current = undefined + setVisibleHTTPHeader(true) + }} + > + 添加 + + + + + 模型配置 + 以下值可以为空,为空代表不设置 +
    + } + key="2" + > + + + + + + + + + + + + + + + + + + + + + + + + +
    {footer}
    + +
    + ) + }), +) + +const NewThirdPartyApplicationConfig: React.FC = React.memo((props) => { + const { onCancel, onAdd, ...rest } = props + + const formRef = useRef<{ form: FormInstance }>(null) + return ( + + + 取消 + + { + formRef.current?.form?.validateFields().then((res) => { + const ExtraParams = Object.keys(res) + .filter((key) => key !== 'Type') + .map((key) => ({ Key: key, Value: res[key] })) + onAdd({ + Type: res.Type, + ExtraParams, + }) + }) + }} + > + 确定添加 + + + } + /> + ) +}) + +export interface InputHTTPHeaderFormProps { + visible: boolean + setVisible: (b: boolean) => void + onSave: (h: HTTPHeader, updateIndex?: number) => any + updateIndex?: number + initFormVal?: HTTPHeader +} + +export const InputHTTPHeaderForm: React.FC = React.memo((props) => { + const { visible, setVisible, onSave, initFormVal, updateIndex } = props + const [form] = Form.useForm() + + useEffect(() => { + if (visible) { + if (initFormVal !== undefined) { + form.setFieldsValue(initFormVal) + } else { + form.resetFields() + } + } + }, [visible]) + return ( + setVisible(false)} + zIndex={1002} + footer={null} + closable={true} + bodyStyle={{ padding: '16px 16px 0px' }} + destroyOnClose={true} + > +
    { + onSave(val, updateIndex) + setVisible(false) + form.resetFields() + }} + form={form} + className={styles['http-heard-form']} + > + + ({ value: ele, label: ele }))} + filterOption={(inputValue, option) => { + if (option?.value && typeof option?.value === 'string') { + return option?.value?.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 + } + return false + }} + size="middle" + /> + + + + + + + + 设置该 Header + + + +
    + ) +}) +export default NewThirdPartyApplicationConfig diff --git a/src/compoments/index.ts b/src/compoments/index.ts index fb7ea3af..de2b6a23 100644 --- a/src/compoments/index.ts +++ b/src/compoments/index.ts @@ -1,15 +1,15 @@ -export { default as WizardCaptcha } from './WizardCaptcha'; -export { default as WizardTable } from './WizardTable'; -export { default as WizardTableFilter } from './WizardTableFilter'; -export { default as WizardExport } from './WizardExportButton'; -export { default as WizardProFilterDrawer } from './WizardProFilterDrawer'; -export { default as WizardTooltip } from './WizardTooltip'; -export { default as WizardModal } from './WizardModal'; -export { default as ChunkUpload } from './ChunkUpload'; -export { default as EmptyBox } from './EmptyBox'; -export { default as NoLoginPermission } from './NoLoginPermission'; -export { default as WizardDrawer } from './WizardDrawer'; -export { default as Markdown } from './MarkDown'; -export { default as WizardAceEditor } from './WizardAceEditor'; -export { default as ExportButton } from './ExportButton'; -export { SortableList } from './SortableList'; +export { default as WizardCaptcha } from './WizardCaptcha' +export { default as WizardTable } from './WizardTable' +export { default as WizardTableFilter } from './WizardTableFilter' +export { default as WizardExport } from './WizardExportButton' +export { default as WizardProFilterDrawer } from './WizardProFilterDrawer' +export { default as WizardTooltip } from './WizardTooltip' +export { default as WizardModal } from './WizardModal' +export { default as ChunkUpload } from './ChunkUpload' +export { default as EmptyBox } from './EmptyBox' +export { default as NoLoginPermission } from './NoLoginPermission' +export { default as WizardDrawer } from './WizardDrawer' +export { default as Markdown } from './MarkDown' +export { default as WizardAceEditor } from './WizardAceEditor' +export { default as ExportButton } from './ExportButton' +export { SortableList } from './SortableList' diff --git a/src/compoments/yakitSideTab/YakitSideTab.module.scss b/src/compoments/yakitSideTab/YakitSideTab.module.scss new file mode 100644 index 00000000..d104cdf1 --- /dev/null +++ b/src/compoments/yakitSideTab/YakitSideTab.module.scss @@ -0,0 +1,188 @@ +@mixin tab() { + display: inline-flex; + flex-direction: column; + align-items: flex-start; + background: var(--Colors-Use-Neutral-Bg-Hover); + position: relative; + @content; +} +@mixin tab-item() { + padding: 12px 4px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + cursor: pointer; + border-bottom: 1px solid var(--Colors-Use-Neutral-Border); + position: relative; + user-select: none; + + background-color: var(--Colors-Use-Neutral-Bg-Hover); + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 600; + line-height: 16px; + + &:hover { + background-color: var(--Colors-Use-Neutral-Border); + } + + svg { + width: 16px; + height: 16px; + color: var(--Colors-Use-Neutral-Text-3-Secondary); + } + @content; +} + +@mixin tab-item-active() { + background-color: var(--Colors-Use-Main-Primary); + color: var(--Colors-Use-Main-On-Primary); + svg { + color: var(--Colors-Use-Main-On-Primary); + } + + &:hover { + background-color: var(--Colors-Use-Main-Hover); + + &::after { + background-color: var(--Colors-Use-Main-Hover); + } + } + @content; +} +%divider { + position: absolute; + content: ""; + background-color: var(--Colors-Use-Basic-Background); + bottom: 0; +} +.tab-wrap { + display: flex; + flex-direction: column; + height: 100%; +} +.tab-wrap-vertical { + flex-direction: row; +} + +.yakit-side-tab { + @include tab() { + height: 100%; + } +} +.yakit-side-tab-item { + @include tab-item() { + .item-text-left { + writing-mode: vertical-rl; + // text-orientation: sideways-right; + // transform: rotate(180deg); + } + .item-text-right { + writing-mode: vertical-rl; + // text-orientation: sideways-right; + // transform: rotate(0deg); + } + &::after { + @extend %divider; + width: 100%; + height: 1px; + right: 0; + } + } +} + +.yakit-side-tab-item-active { + @include tab-item-active(); +} +.yakit-side-tab-item-active-noHover { + &:hover { + background-color: var(--Colors-Use-Main-Primary); + + &::after { + background-color: var(--Colors-Use-Basic-Background); + } + } +} +.yakit-side-tab-item-show { + background-color: var(--Colors-Use-Neutral-Text-4-Help-text); + border-bottom-color: var(--Colors-Use-Neutral-Text-4-Help-text); + color: var(--Colors-Use-Neutral-Text-1-Title); + svg { + color: var(--Colors-Use-Neutral-Text-1-Title); + } + &.yakit-side-tab-item-active::before { + background-color: var(--Colors-Use-Neutral-Text-4-Help-text); + } + &.yakit-side-tab-item-active::after { + background-color: var(--Colors-Use-Neutral-Text-4-Help-text); + } +} +.yakit-side-tab-item-show:hover { + background-color: var(--Colors-Use-Main-Hover); + border-bottom-color: var(--Colors-Use-Main-Hover); + color: var(--Colors-Use-Main-On-Primary); + svg { + color: var(--Colors-Use-Main-On-Primary); + } + &.yakit-side-tab-item-active::before { + background-color: var(--Colors-Use-Main-Hover); + } + &.yakit-side-tab-item-active::after { + background-color: var(--Colors-Use-Main-Hover); + } +} + +.yakit-side-tab-horizontal { + @include tab() { + flex-direction: row; + width: 100%; + height: 24px; + &::after { + position: absolute; + content: ""; + width: 100%; + height: 1px; + bottom: 0; + background-color: var(--Colors-Use-Neutral-Border); + } + } +} +.yakit-side-tab-horizontal-item { + @include tab-item() { + padding: 4px 8px; + flex-direction: row; + height: 24px; + &::after { + @extend %divider; + width: 1px; + height: 100%; + right: 0; + } + } +} + +.yakit-side-tab-horizontal-item-active { + @include tab-item-active(); +} + +.tab-wrap-vertical-right { + .yakit-side-tab { + @include tab() { + height: unset; + padding-top: 12px; + background: unset; + gap: 4px; + } + } + .yakit-side-tab-item { + position: relative; + border-radius: 0 4px 4px 0; + border: 1px solid var(--Colors-Use-Neutral-Border); + border-left: 0; + &::after { + width: 0; + height: 0; + } + } +} diff --git a/src/compoments/yakitSideTab/YakitSideTab.tsx b/src/compoments/yakitSideTab/YakitSideTab.tsx new file mode 100644 index 00000000..fd135e68 --- /dev/null +++ b/src/compoments/yakitSideTab/YakitSideTab.tsx @@ -0,0 +1,237 @@ +import type { FC, ReactNode } from 'react'; +import { useState, memo } from 'react'; +import type { YakitSideTabProps, YakitTabsItemProps } from './YakitSideTabType'; +import classNames from 'classnames'; +import { useControllableValue, useCreation, useMemoizedFn } from 'ahooks'; +import { Tooltip } from 'antd'; +import styles from './YakitSideTab.module.scss'; + +export const YakitSideTab: FC = memo((props) => { + const { + yakitTabs, + activeKey, + onActiveKey, + activeShow = false, + barHint, + type = 'vertical', + children, + onTabPaneRender, + className = '', + btnItemClassName = '', + t, + } = props; + const [show, setShow] = useControllableValue(props, { + defaultValue: true, + valuePropName: 'show', + trigger: 'setShow', + }); + const onChange = useMemoizedFn((item) => { + if (!activeShow) { + if (item.value === activeKey) { + setShow((v) => !v); + } else { + setShow(true); + } + } + onActiveKey(item.value); + }); + const renderContent = useMemoizedFn(() => { + switch (type) { + case 'vertical': + return ( + <> +
    + {yakitTabs.map((item) => ( + + ))} +
    + {children} + + ); + + case 'vertical-right': + return ( + <> + {children} +
    + {yakitTabs.map((item) => ( + + ))} +
    + + ); + case 'horizontal': + return ( + <> +
    + {yakitTabs.map((item) => ( + + ))} +
    + {children} + + ); + default: + return
    未知type
    ; + } + }); + return ( +
    + {renderContent()} +
    + ); +}); + +const YakitTabsItem: React.FC = memo((props) => { + const { + item, + onChange, + className = '', + onTabPaneRender, + rotate, + barHint, + t, + } = props; + const [hover, setHover] = useState(false); + + const renderLabel = useCreation(() => { + if (typeof item.label === 'string') { + return t?.(item.label) || item.label; + } else if (typeof item.label === 'function') { + return item.label(); + } else { + return item.label; + } + }, [item.label]); + + const node: ReactNode[] = useCreation(() => { + return [ + + {renderLabel} + , + item.icon, + ]; + }, [renderLabel, item.icon, rotate]); + const [label, icon] = node; + + const tabDom = useMemoizedFn(() => { + return ( +
    onChange(item)} + onMouseEnter={() => setHover(true)} + onMouseLeave={() => setHover(false)} + > + {onTabPaneRender ? ( + onTabPaneRender(item, node) + ) : ( + <> + {icon} + {label} + + )} +
    + ); + }); + + const hint = barHint?.(item.value); + + return ( + // eslint-disable-next-line react/jsx-no-useless-fragment + <> + {hint ? ( + + {tabDom()} + + ) : ( + <>{tabDom()} + )} + + ); +}); diff --git a/src/compoments/yakitSideTab/YakitSideTabType.d.ts b/src/compoments/yakitSideTab/YakitSideTabType.d.ts new file mode 100644 index 00000000..b7215a7b --- /dev/null +++ b/src/compoments/yakitSideTab/YakitSideTabType.d.ts @@ -0,0 +1,46 @@ +import type { ReactNode } from 'react'; +export interface YakitSideTabProps + extends Pick { + yakitTabs: YakitTabsProps[]; + setYakitTabs?: (v: YakitTabsProps[]) => void; + + /** TODO 点击展示的tab状态 缓存的key */ + cacheKey?: string; + activeKey?: string; + onActiveKey: (s: string) => void; + + /** 只切换 不做显示隐藏 */ + activeShow?: boolean; + + setShow?: (s: boolean) => void; + /** type 为vertical-right ,show不生效 */ + show?: boolean; + /** + * vertical:tab在左边 + * vertical-right:tab在右边 + * horizontal:单击是切换tabContent,tab在上方 + */ + type?: 'vertical' | 'horizontal' | 'vertical-right'; + children?: ReactNode; + + className?: string; + btnItemClassName?: string; +} + +export interface YakitTabsProps { + icon?: ReactNode; + /** lable需要翻译,需要传t函数。若不做翻译,就不传t。也可以传自定义函数处理label */ + label: string | (() => ReactNode); + value: string; + hint?: () => string; +} + +export interface YakitTabsItemProps { + item: YakitTabsProps; + className?: string; + onChange: (v: YakitTabsProps) => void; + onTabPaneRender?: (item: YakitTabsProps, node: ReactNode[]) => ReactNode; + rotate?: 'left' | 'right'; + barHint?: (k: string) => string; + t?: (keys: KeyOrKeys, vars?: Vars, defaultValue?: string) => string; +} diff --git a/src/compoments/yakitUI/YakitAutoComplete/YakitAutoComplete.module.scss b/src/compoments/yakitUI/YakitAutoComplete/YakitAutoComplete.module.scss new file mode 100644 index 00000000..3b82e87b --- /dev/null +++ b/src/compoments/yakitUI/YakitAutoComplete/YakitAutoComplete.module.scss @@ -0,0 +1,143 @@ +// ========================== disabled ========================== +.yakit-auto-complete-disabled { + :global { + .ant-select-disabled.ant-select:not(.ant-select-customize-input) .ant-select-selector { + background-color: var(--Colors-Use-Neutral-Bg-Hover); + border: 1px solid var(--Colors-Use-Neutral-Border); + } + } +} + +// ========================== middle ========================== +.yakit-auto-complete-wrapper { + --yakit-box-shadow-color: var(--Colors-Use-Main-Focus); + --yakit-border-hover-color: var(--Colors-Use-Main-Hover); + --yakit-border-focus-color: var(--Colors-Use-Main-Primary); + --yakit-font-color: var(--Colors-Use-Main-Primary); + + display: inline-flex; + width: 100%; + + :global { + .ant-select { + width: 100%; + } + .ant-select-single:not(.ant-select-customize-input) .ant-select-selector { + height: 28px; + padding: 6px 12px; + border-radius: 4px; + border-color: var(--Colors-Use-Neutral-Border); + background: var(--Colors-Use-Basic-Background); + } + + .ant-select:not(.ant-select-customize-input) .ant-select-selector .ant-select-selection-search-input { + height: 26px; + font-size: 12px; + color: var(--Colors-Use-Neutral-Text-1-Title); + } + + .ant-select-disabled:not(.ant-select-customize-input) .ant-select-selector .ant-select-selection-search-input { + color: var(--Colors-Use-Neutral-Disable) + } + + .ant-select:not(.ant-select-disabled):hover .ant-select-selector { + border-color: var(--yakit-border-hover-color); + } + .ant-select-focused .ant-select-selector { + border-radius: 4px; + border-color: var(--yakit-border-focus-color); + box-shadow: 0 0 0 2px var(--yakit-box-shadow-color); + } + .ant-select-single .ant-select-selector .ant-select-selection-item, + .ant-select-single .ant-select-selector .ant-select-selection-placeholder { + line-height: 14px; + font-size: 12px; + color: var(--Colors-Use-Neutral-Disable); + } + } +} + +// ========================== small ========================== +.yakit-auto-complete-wrapper-small { + :global { + .ant-select-single:not(.ant-select-customize-input) .ant-select-selector { + height: 24px; + padding: 4px 8px; + } + + .ant-select:not(.ant-select-customize-input) .ant-select-selector .ant-select-selection-search-input { + height: 22px; + font-size: 12px; + } + } +} + +// ========================== large ========================== +.yakit-auto-complete-wrapper-large { + :global { + .ant-select-single:not(.ant-select-customize-input) .ant-select-selector { + height: 32px; + } + + .ant-select:not(.ant-select-customize-input) .ant-select-selector .ant-select-selection-search-input { + height: 30px; + font-size: 14px; + } + .ant-select-single .ant-select-selector .ant-select-selection-item, + .ant-select-single .ant-select-selector .ant-select-selection-placeholder { + line-height: 18px; + } + } +} + +// ========================== popup ========================== +.yakit-auto-complete-popup { + --auto-complete-font-color: var(--Colors-Use-Main-Primary); + --auto-complete-background-color: var(--Colors-Use-Main-Bg); + + padding: 6px 4px; + border-radius: 4px; + background: var(--Colors-Use-Basic-Background); + box-shadow: 0 8px 16px var(--Colors-Use-Basic-Shadow); + border: 1px solid var(--Colors-Use-Neutral-Border); + + :global { + .ant-select-item-option-active:not(.ant-select-item-option-disabled) { + color: var(--auto-complete-font-color); + background-color: var(--auto-complete-background-color); + } + + .ant-select-item { + border-radius: 4px; + padding: 4px 8px; + font-size: 12px; + color: var(--Colors-Use-Neutral-Text-1-Title); + } + } +} + +.yakit-auto-complete-popup-y { + transform: translateY(-2px); +} + +.yakit-option-item { + display: flex; + justify-content: space-between; + &-label { + flex: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + .option-item-close { + svg { + width: 16px; + height: 16px; + margin-top: 4px; + color: var(--Colors-Use-Main-Primary); + &:hover { + color: var(--Colors-Use-Error-Primary); + } + } + } +} diff --git a/src/compoments/yakitUI/YakitAutoComplete/YakitAutoComplete.tsx b/src/compoments/yakitUI/YakitAutoComplete/YakitAutoComplete.tsx new file mode 100644 index 00000000..f7ec80c3 --- /dev/null +++ b/src/compoments/yakitUI/YakitAutoComplete/YakitAutoComplete.tsx @@ -0,0 +1,195 @@ +import { AutoComplete } from 'antd' +import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' +import type { YakitAutoCompleteCacheDataHistoryProps, YakitAutoCompleteProps } from './YakitAutoCompleteType' +import styles from './YakitAutoComplete.module.scss' +import classNames from 'classnames' +import { useInViewport, useMemoizedFn } from 'ahooks' +import type { + YakitOptionTypeProps, + // onGetRemoteValuesBase, + // onSetRemoteValuesBase, +} from '../utils' +import { OutlineXIcon } from '@/assets/icon/outline' +export const defYakitAutoCompleteRef = { + onGetRemoteValues: () => ({ options: [], defaultValue: '' }), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onSetRemoteValues: (s: string) => {}, +} + +/** + * @description YakitAutoCompleteProps 的属性 + * @augments YakitAutoCompleteProps 继承antd的 AutoCompleteProps 默认属性 + * @param {"small" | "middle" | "large" } size 默认middle + * @param {string} cacheHistoryDataKey 用来缓存/获取历史数据的setRemoteValue/getRemoteValue,默认缓存 options 和 defaultValue + * @param {number} cacheHistoryListLength 缓存的历史记录list长度 + */ +export const YakitAutoComplete: React.FC = React.forwardRef((props, ref) => { + const { + size, + cacheHistoryDataKey, + // ref: autoRef = null, + initValue = '', + wrapperStyle, + isInit = true, + ...restProps + } = props + const autoCompleteRef = useRef(null) + const [inViewport = true] = useInViewport(autoCompleteRef) + const [mouseEnterItem, setMouseEnterItem] = useState('') + const [show, setShow] = useState(false) + const [loading, setLoading] = useState(false) + const [cacheHistoryData] = useState({ + options: [], + defaultValue: '', + }) + useEffect(() => { + inViewport && onGetRemoteValues(isInit) + }, [initValue, inViewport]) + useImperativeHandle( + ref, + () => ({ + onSetRemoteValues: (value: string) => { + const newValue = value || restProps.value + onSetRemoteValues(newValue) + }, + onGetRemoteValues: () => { + return cacheHistoryData + }, + }), + [cacheHistoryData, restProps.value, loading], + ) + /** @description 缓存 cacheHistoryDataKey 对应的数据 */ + const onSetRemoteValues = useMemoizedFn((newValue: string) => { + if (!newValue) return + if (!cacheHistoryDataKey) return + // onSetRemoteValuesBase({ + // cacheHistoryDataKey, + // newValue, + // isCacheDefaultValue, + // }).then((value: any) => { + // setCacheHistoryData({ + // defaultValue: value.defaultValue || '', + // options: value.options, + // }); + // }); + }) + /** @description 获取 cacheHistoryDataKey 对应的数据 */ + const onGetRemoteValues = useMemoizedFn((init?: boolean) => { + if (!cacheHistoryDataKey) return + if (init) setLoading(true) + // onGetRemoteValuesBase(cacheHistoryDataKey) + // .then((cacheData: any) => { + // let value = cacheData.defaultValue + // ? cacheData.defaultValue + // : ''; + // let newOption = cacheData.options || props.options || []; + // // 当缓存不存在的时候,若有初始默认值 + // if (cacheData.firstUse && initValue) { + // value = initValue; + // newOption = [{ value: initValue, label: initValue }]; + // // 主要是删缓存需要 + // onSetRemoteValues(initValue); + // } else { + // setCacheHistoryData({ + // defaultValue: value, + // options: newOption, + // }); + // } + // // 非form表单时,设置value + // // 在表单使用时,如果该值为true,这设置值的优先级高于组件外部直接使用form.setfeildvalue设置(场景:初始化) + // if (isCacheDefaultValue) { + // if (props.onChange) props.onChange(value, newOption); + // } + // }) + // .finally(() => { + // setTimeout(() => { + // setLoading(false); + // }, 200); + // }); + }) + + const delCatchOptionItem = (e: React.MouseEvent, item: YakitOptionTypeProps) => { + e.stopPropagation() + if (cacheHistoryDataKey && item) { + // onSetRemoteValuesBase({ + // cacheHistoryDataKey, + // newValue: '', + // isCacheDefaultValue, + // delCacheValue: item.value, + // }).then((value: any) => { + // setCacheHistoryData({ + // defaultValue: value.defaultValue, + // options: value.options, + // }); + // }); + } + } + + const renderItem = (item: YakitOptionTypeProps) => { + const copyItem: any = { ...item } + copyItem.label = ( +
    { + setMouseEnterItem(item.value) + }} + onMouseLeave={() => { + setMouseEnterItem('') + }} + > +
    {copyItem.label}
    + delCatchOptionItem(e, item)} + /> +
    + ) + return copyItem + } + + const options = useMemo(() => { + if (cacheHistoryData.options.length) { + return cacheHistoryData.options.map((item) => renderItem(item)) + } else { + return restProps.options + } + }, [cacheHistoryData, restProps]) + + return ( +
    + {loading ? ( + // eslint-disable-next-line react/jsx-no-useless-fragment + <> + ) : ( + { + setShow(open) + if (props.onDropdownVisibleChange) props.onDropdownVisibleChange(open) + }} + /> + )} +
    + ) +}) diff --git a/src/compoments/yakitUI/YakitAutoComplete/YakitAutoCompleteType.d.ts b/src/compoments/yakitUI/YakitAutoComplete/YakitAutoCompleteType.d.ts new file mode 100644 index 00000000..30c73c06 --- /dev/null +++ b/src/compoments/yakitUI/YakitAutoComplete/YakitAutoCompleteType.d.ts @@ -0,0 +1,33 @@ +import type { AutoCompleteProps } from 'antd' +import type { CacheDataHistoryProps } from '../utils' +import type { BaseSelectRef } from 'rc-select' +import type { CSSProperties } from 'react' + +/** + * @description YakitAutoCompleteProps 的属性 + * @augments AutoCompleteProps 继承antd的 AutoCompleteProps 默认属性 + * @param {"small" | "middle" | "large" } size 默认middle + * @param {string} cacheHistoryDataKey 用来缓存/获取历史数据的setRemoteValue/getRemoteValue,默认缓存 options 和 defaultValue + * @param {number} cacheHistoryListLength 缓存的历史记录list长度 + * @param {boolean} isCacheDefaultValue 是否缓存默认值 + */ +export interface YakitAutoCompleteProps extends AutoCompleteProps { + size?: 'small' | 'middle' | 'large' + cacheHistoryDataKey?: string + cacheHistoryListLength?: number + /** 是否缓存默认值 */ + isCacheDefaultValue?: boolean + ref?: React.ForwardedRef & Ref + /** 初始默认值 主要用于缓存值不存在时 */ + initValue?: string + wrapperStyle?: CSSProperties + /** 是否在显隐时使用Loading,Loading会导致控件闪烁 默认为true */ + isInit?: boolean +} + +export interface YakitAutoCompleteRefProps { + onSetRemoteValues: (s: string) => void + onGetRemoteValues: () => void +} + +export type YakitAutoCompleteCacheDataHistoryProps = CacheDataHistoryProps diff --git a/src/compoments/yakitUI/YakitAutoComplete/index.md b/src/compoments/yakitUI/YakitAutoComplete/index.md new file mode 100644 index 00000000..0233af43 --- /dev/null +++ b/src/compoments/yakitUI/YakitAutoComplete/index.md @@ -0,0 +1,26 @@ +--- +title: YakitAutoComplete +subtitle: 自动完成 +--- + +YakitAutoComplete 下拉选择列表。 + +## API + +### YakitAutoCompleteProps + +| 参数 | 说明 | 类型 | 默认值 | +| ---------------------- | -------------------------------------------------------------- | --------------------------------------------- | -------- | +| size | 菜单类型 | YakitSizeType | "middle" | +| cacheHistoryDataKey | 缓存数据 key值(目前只支持tags类型) | string | - | +| cacheHistoryListLength | 缓存数据 list(option数组)的长度(cacheHistoryDataKey存在才有效) | number | - | +| ref | - | React.ForwardedRef | - | + +## ref 提供设置缓存的方法,提供获取缓存值得方法; + +> onSetRemoteValues:是否要缓存需要外界通过ref得方式调用这个方法 + +| 方法 | 说明 | +| ----------------- | -------------- | +| onSetRemoteValues | 设置缓存得方法 | +| onGetRemoteValues | 获取缓存得方法 | diff --git a/src/compoments/yakitUI/YakitButton/YakitButton.tsx b/src/compoments/yakitUI/YakitButton/YakitButton.tsx index 7ed59aba..63633e8f 100644 --- a/src/compoments/yakitUI/YakitButton/YakitButton.tsx +++ b/src/compoments/yakitUI/YakitButton/YakitButton.tsx @@ -1,77 +1,80 @@ -import React, { useMemo } from 'react'; -import { Button, type ButtonProps } from 'antd'; +import React, { useMemo } from 'react' +import type { ButtonProps } from 'antd' +import { Button } from 'antd' -import styles from './yakitButton.module.scss'; -import classNames from 'classnames'; +import styles from './yakitButton.module.scss' +import classNames from 'classnames' +import { isNumber, isString } from 'lodash' -export interface YakitButtonProp - extends Omit { - type?: - | 'primary' - | 'secondary2' - | 'outline1' - | 'outline2' - | 'text' - | 'text2'; - /** 当colors和danger同时存在,以colors为准 */ - colors?: 'success' | 'danger' | 'primary'; - size?: 'small' | 'middle' | 'large' | 'max'; - isHover?: boolean; - isActive?: boolean; +export interface YakitButtonProp extends Omit { + type?: 'primary' | 'secondary2' | 'outline1' | 'outline2' | 'text' | 'text2' + /** 当colors和danger同时存在,以colors为准 */ + colors?: 'success' | 'danger' | 'primary' | 'infoBlue' + size?: 'small' | 'middle' | 'large' | 'max' + isHover?: boolean + isActive?: boolean + radius?: boolean | number | string } /** @name Yakit 主题按钮组件 */ export const YakitButton: React.FC = React.memo((props) => { - const { - size, - type, - colors, - isHover, - isActive, - children, - className, - danger, - ...resePopover - } = props; + const { size, type, colors, isHover, isActive, children, className, danger, radius, ...resePopover } = props + const typeClass = useMemo(() => { + if (type === 'secondary2') return 'yakit-button-secondary2' + if (type === 'outline1') return 'yakit-button-outline1' + if (type === 'outline2') return 'yakit-button-outline2' + if (type === 'text') return 'yakit-button-text' + if (type === 'text2') return 'yakit-button-text2' + return 'yakit-button-primary' + }, [type]) + const colorClass = useMemo(() => { + if (!colors && danger) return 'yakit-button-danger' + if (colors === 'success') return 'yakit-button-success' + if (colors === 'danger') return 'yakit-button-danger' + if (colors === 'infoBlue') return 'yakit-button-infoBlue' + return '' + }, [colors, danger]) - const typeClass = useMemo(() => { - if (type === 'secondary2') return 'yakit-button-secondary2'; - if (type === 'outline1') return 'yakit-button-outline1'; - if (type === 'outline2') return 'yakit-button-outline2'; - if (type === 'text') return 'yakit-button-text'; - if (type === 'text2') return 'yakit-button-text2'; - return 'yakit-button-primary'; - }, [type]); - const colorClass = useMemo(() => { - if (!colors && danger) return 'yakit-button-danger'; - if (colors === 'success') return 'yakit-button-success'; - if (colors === 'danger') return 'yakit-button-danger'; - return ''; - }, [colors, danger]); + const sizeClass = useMemo(() => { + if (size === 'small') return 'yakit-button-small-size' + if (size === 'large') return 'yakit-button-large-size' + if (size === 'max') return 'yakit-button-max-size' + return 'yakit-button-size' + }, [size]) - const sizeClass = useMemo(() => { - if (size === 'small') return 'yakit-button-small-size'; - if (size === 'large') return 'yakit-button-large-size'; - if (size === 'max') return 'yakit-button-max-size'; - return 'yakit-button-size'; - }, [size]); + const style: React.CSSProperties = useMemo(() => { + let styleObj = {} + if (isNumber(radius)) { + styleObj = { + '--yakit-button-border-radius': `${radius}px`, + } + } + if (isString(radius)) { + styleObj = { + '--yakit-button-border-radius': radius, + } + } + return styleObj as React.CSSProperties + }, [radius]) - return ( - - ); -}); + return ( + + ) +}) diff --git a/src/compoments/yakitUI/YakitButton/yakitButton.module.scss b/src/compoments/yakitUI/YakitButton/yakitButton.module.scss index c51d1db9..de3ab0db 100644 --- a/src/compoments/yakitUI/YakitButton/yakitButton.module.scss +++ b/src/compoments/yakitUI/YakitButton/yakitButton.module.scss @@ -36,7 +36,7 @@ &[disabled] { background: var(--Colors-Use-Main-Border); color: var(--Colors-Use-Basic-Background); - border: var(--Colors-Use-Main-Border); + border-color: var(--Colors-Use-Main-Border); } &[disabled]:hover { background: var(--Colors-Use-Main-Border); @@ -173,7 +173,16 @@ stroke-width: 1.5; } - &.yakit-hover-button { + &:focus { + background: var(--Colors-Use-Basic-Background); + color: fetch-color($type, "color-6"); + border-color: fetch-color($type, "color-6"); + svg { + color: fetch-color($type, "color-6"); + } + } + + &:hover { background: var(--Colors-Use-Basic-Background); color: fetch-color($type, "color-5"); border-color: fetch-color($type, "color-5"); @@ -181,7 +190,7 @@ color: fetch-color($type, "color-5"); } } - &.yakit-active-button { + &:active { background: var(--Colors-Use-Basic-Background); color: fetch-color($type, "color-6"); border-color: fetch-color($type, "color-6"); @@ -189,16 +198,7 @@ color: fetch-color($type, "color-6"); } } - - &:focus { - background: var(--Colors-Use-Basic-Background); - color: var(--Colors-Use-Neutral-Text-1-Title); - border-color: var(--Colors-Use-Neutral-Border); - svg { - color: var(--Colors-Use-Neutral-Text-3-Secondary); - } - } - &:hover { + &.yakit-hover-button { background: var(--Colors-Use-Basic-Background); color: fetch-color($type, "color-5"); border-color: fetch-color($type, "color-5"); @@ -206,7 +206,8 @@ color: fetch-color($type, "color-5"); } } - &:active { + + &.yakit-active-button { background: var(--Colors-Use-Basic-Background); color: fetch-color($type, "color-6"); border-color: fetch-color($type, "color-6"); @@ -431,11 +432,12 @@ } } .yakit-button { + --yakit-button-border-radius: 4px; display: inline-flex; justify-content: center; align-items: center; box-shadow: none; - border-radius: 4px; + border-radius: var(--yakit-button-border-radius); border: 1px solid transparent; &:global(.ant-btn-icon-only) { width: unset; @@ -507,512 +509,7 @@ .yakit-button-text2 { @include generateText2("primary"); } -@import "../../../theme/themeify.scss"; - -@mixin generatePrimary($type) { - --antd-wave-shadow-color: #{fetch-color($type, "color-5")}; - - background: fetch-color($type, "color-5"); - color: fetch-color($type, "color-7"); - border-color: fetch-color($type, "color-5"); - - &.yakit-hover-button { - background: fetch-color($type, "color-4"); - color: fetch-color($type, "color-7"); - border-color: fetch-color($type, "color-4"); - } - &.yakit-active-button { - background: fetch-color($type, "color-6"); - color: fetch-color($type, "color-7"); - border-color: fetch-color($type, "color-6"); - } - - &:focus { - background: fetch-color($type, "color-5"); - color: fetch-color($type, "color-7"); - border-color: fetch-color($type, "color-5"); - } - &:hover { - background: fetch-color($type, "color-4"); - color: fetch-color($type, "color-7"); - border-color: fetch-color($type, "color-4"); - } - &:active { - background: fetch-color($type, "color-6"); - color: fetch-color($type, "color-7"); - border-color: fetch-color($type, "color-6"); - } - &[disabled] { - background: var(--Colors-Use-Main-Border); - color: var(--Colors-Use-Basic-Background); - border: var(--Colors-Use-Main-Border); - } - &[disabled]:hover { - background: var(--Colors-Use-Main-Border); - color: var(--Colors-Use-Basic-Background); - border-color: var(--Colors-Use-Main-Border); - } -} - -@mixin generateSecondary2($type) { - --antd-wave-shadow-color: #{fetch-color($type, "color-5")}; - - background: var(--Colors-Use-Neutral-Bg-Hover); - color: var(--Colors-Use-Neutral-Text-1-Title); - border-color: var(--Colors-Use-Neutral-Bg-Hover); - svg { - color: var(--Colors-Use-Neutral-Text-3-Secondary); - } - - &.yakit-hover-button { - background: fetch-color($type, "color-1"); - color: fetch-color($type, "color-4"); - border-color: fetch-color($type, "color-1"); - svg { - color: fetch-color($type, "color-5"); - } - } - &.yakit-active-button { - background: fetch-color($type, "color-1"); - color: fetch-color($type, "color-6"); - border-color: fetch-color($type, "color-1"); - svg { - color: fetch-color($type, "color-6"); - } - } - - &:focus { - background: var(--Colors-Use-Neutral-Bg); - color: var(--Colors-Use-Neutral-Text-1-Title); - border-color: var(--Colors-Use-Neutral-Border); - svg { - color: var(--Colors-Use-Neutral-Text-3-Secondary); - } - } - &:hover { - background: fetch-color($type, "color-1"); - color: fetch-color($type, "color-4"); - border-color: transparent; - svg { - color: fetch-color($type, "color-5"); - } - } - &:active { - background: fetch-color($type, "color-1"); - color: fetch-color($type, "color-6"); - border-color: fetch-color($type, "color-3"); - svg { - color: fetch-color($type, "color-6"); - } - } - &[disabled] { - background: var(--Colors-Use-Neutral-Bg-Hover); - color: var(--Colors-Use-Neutral-Disable); - border-color: var(--Colors-Use-Neutral-Border); - svg { - color: var(--Colors-Use-Neutral-Disable); - } - } - &[disabled]:hover { - background: var(--Colors-Use-Neutral-Bg-Hover); - color: var(--Colors-Use-Neutral-Disable); - border-color: var(--Colors-Use-Neutral-Border); - svg { - color: var(--Colors-Use-Neutral-Disable); - } - } -} - -@mixin generateOutline1($type) { - --antd-wave-shadow-color: #{fetch-color($type, "color-5")}; - - background: transparent; - color: fetch-color($type, "color-5"); - border-color: fetch-color($type, "color-3"); - - &.yakit-hover-button { - background: fetch-color($type, "color-1"); - color: fetch-color($type, "color-4"); - border-color: fetch-color($type, "color-4"); - } - &.yakit-active-button { - background: fetch-color($type, "color-1"); - color: fetch-color($type, "color-6"); - border-color: fetch-color($type, "color-6"); - } - - &:focus { - background: transparent; - color: fetch-color($type, "color-5"); - border-color: fetch-color($type, "color-3"); - } - &:hover { - background: fetch-color($type, "color-1"); - color: fetch-color($type, "color-4"); - border-color: fetch-color($type, "color-4"); - } - &:active { - background: fetch-color($type, "color-1"); - color: fetch-color($type, "color-6"); - border-color: fetch-color($type, "color-6"); - } - &[disabled] { - background: var(--Colors-Use-Neutral-Bg); - color: var(--Colors-Use-Neutral-Disable); - border-color: var(--Colors-Use-Neutral-Border); - } - &[disabled]:hover { - background: var(--Colors-Use-Neutral-Bg); - color: var(--Colors-Use-Neutral-Disable); - border-color: var(--Colors-Use-Neutral-Border); - } -} - -@mixin generateOutline2($type) { - --antd-wave-shadow-color: #{fetch-color($type, "color-5")}; - - background: var(--Colors-Use-Basic-Background); - color: var(--Colors-Use-Neutral-Text-1-Title); - border-color: var(--Colors-Use-Neutral-Border); - svg { - color: var(--Colors-Use-Neutral-Text-3-Secondary); - } - - path { - stroke-width: 1.5; - } - - &.yakit-hover-button { - background: var(--Colors-Use-Basic-Background); - color: fetch-color($type, "color-5"); - border-color: fetch-color($type, "color-5"); - svg { - color: fetch-color($type, "color-5"); - } - } - &.yakit-active-button { - background: var(--Colors-Use-Basic-Background); - color: fetch-color($type, "color-6"); - border-color: fetch-color($type, "color-6"); - svg { - color: fetch-color($type, "color-6"); - } - } - &:focus { - background: var(--Colors-Use-Basic-Background); - color: var(--Colors-Use-Neutral-Text-1-Title); - border-color: var(--Colors-Use-Neutral-Border); - svg { - color: var(--Colors-Use-Neutral-Text-3-Secondary); - } - } - &:hover { - background: var(--Colors-Use-Basic-Background); - color: fetch-color($type, "color-5"); - border-color: fetch-color($type, "color-5"); - svg { - color: fetch-color($type, "color-5"); - } - } - &:active { - background: var(--Colors-Use-Basic-Background); - color: fetch-color($type, "color-6"); - border-color: fetch-color($type, "color-6"); - svg { - color: fetch-color($type, "color-6"); - } - } - &[disabled] { - background: var(--Colors-Use-Basic-Background); - color: var(--Colors-Use-Neutral-Disable); - border-color: var(--Colors-Use-Neutral-Border); - svg { - color: var(--Colors-Use-Neutral-Disable); - } - } - &[disabled]:hover { - background: var(--Colors-Use-Basic-Background); - color: var(--Colors-Use-Neutral-Disable); - border-color: var(--Colors-Use-Neutral-Border); - svg { - color: var(--Colors-Use-Neutral-Disable); - } - } -} - -@mixin generateText($type) { - --antd-wave-shadow-color: transparent; - - background: transparent; - color: fetch-color($type, "color-5"); - border-color: transparent; - - &.yakit-hover-button { - background: transparent; - color: fetch-color($type, "color-4"); - border-color: transparent; - } - &.yakit-active-button { - background: transparent; - color: fetch-color($type, "color-6"); - border-color: transparent; - } - - &:focus { - background: transparent; - color: fetch-color($type, "color-5"); - border-color: transparent; - } - &:hover { - background: transparent; - color: fetch-color($type, "color-4"); - border-color: transparent; - } - &:active { - background: transparent; - color: fetch-color($type, "color-6"); - border-color: transparent; - } - &[disabled] { - background: transparent; - color: var(--Colors-Use-Neutral-Disable); - border-color: transparent; - } - &[disabled]:hover { - background: transparent; - color: var(--Colors-Use-Neutral-Disable); - border-color: transparent; - } -} - -@mixin generateText2($type) { - --antd-wave-shadow-color: #{fetch-color($type, "color-5")}; - - background: transparent; - color: var(--Colors-Use-Neutral-Text-1-Title); - border-color: transparent; - svg { - color: var(--Colors-Use-Neutral-Text-3-Secondary); - } - - &.yakit-hover-button { - background: var(--Colors-Use-Neutral-Bg); - color: var(--Colors-Use-Neutral-Text-1-Title); - border-color: var(--Colors-Use-Neutral-Border); - svg { - color: var(--Colors-Use-Neutral-Text-3-Secondary); - } - } - &.yakit-active-button { - background: var(--Colors-Use-Neutral-Bg); - color: fetch-color($type, "color-5"); - border-color: var(--Colors-Use-Neutral-Border); - svg { - color: fetch-color($type, "color-5"); - } - } - - &:focus { - background: transparent; - color: var(--Colors-Use-Neutral-Text-1-Title); - border-color: transparent; - svg { - color: var(--Colors-Use-Neutral-Text-3-Secondary); - } - } - &:hover { - background: var(--Colors-Use-Neutral-Bg-Hover); - color: var(--Colors-Use-Neutral-Text-1-Title); - border-color: var(--Colors-Use-Neutral-Bg-Hover); - svg { - color: var(--Colors-Use-Neutral-Text-3-Secondary); - } - } - &:active { - background: var(--Colors-Use-Neutral-Bg); - color: fetch-color($type, "color-5"); - border-color: var(--Colors-Use-Neutral-Border); - svg { - color: fetch-color($type, "color-5"); - } - } - &[disabled] { - background: transparent; - color: var(--Colors-Use-Neutral-Disable); - border-color: transparent; - svg { - color: var(--Colors-Use-Neutral-Disable); - } - } - &[disabled]:hover { - background: transparent; - color: var(--Colors-Use-Neutral-Disable); - border-color: transparent; - svg { - color: var(--Colors-Use-Neutral-Disable); - } - } -} - -.yakit-button-small-size { - height: 20px; - padding: 3px 6px; - font-weight: 500; - font-size: 11px; - & > :global(.anticon + span), - & > :global(span + .anticon) { - margin-left: 2px; - } - - svg { - width: 12px; - height: 12px; - } - - &:global(.ant-btn-icon-only) { - padding: 2px; - svg { - width: 14px; - height: 14px; - } - } -} -.yakit-button-size { - height: 24px; - padding: 3px 8px; - font-weight: 500; - font-size: 12px; - & > :global(.anticon + span), - & > :global(span + .anticon) { - margin-left: 4px; - } - - svg { - width: 16px; - height: 16px; - } - - &:global(.ant-btn-icon-only) { - padding: 4px; - } -} -.yakit-button-large-size { - height: 28px; - padding: 4px 12px; - font-weight: 600; - font-size: 14px; - & > :global(.anticon + span), - & > :global(span + .anticon) { - margin-left: 4px; - } - - svg { - width: 16px; - height: 16px; - } - - &:global(.ant-btn-icon-only) { - padding: 3px; - svg { - width: 20px; - height: 20px; - } - } -} -.yakit-button-max-size { - height: 32px; - padding: 5px 12px; - font-weight: 500; - font-size: 14px; - & > :global(.anticon + span), - & > :global(span + .anticon) { - margin-left: 4px; - } - - svg { - width: 20px; - height: 20px; - } - - &:global(.ant-btn-icon-only) { - padding: 5px; - } -} -.yakit-button { - display: inline-flex; - justify-content: center; - align-items: center; - box-shadow: none; - border-radius: 4px; - border: 1px solid transparent; - &:global(.ant-btn-icon-only) { - width: unset; - } - &:global(.ant-btn.ant-btn-loading::before) { - background-color: var(--Colors-Use-Neutral-Bg-Hover); - } - &:global(.ant-btn .anticon) { - display: flex; - } - svg { - path { - stroke-width: 1.5; - } - } -} - -.yakit-button-primary { - @include generatePrimary("primary"); - &.yakit-button-success { - @include generatePrimary("success"); - } - &.yakit-button-danger { - @include generatePrimary("danger"); - } - &.yakit-button-infoBlue { - @include generatePrimary("infoBlue"); - } -} -.yakit-button-secondary2 { - @include generateSecondary2("primary"); - &.yakit-button-success { - @include generateSecondary2("success"); - } - &.yakit-button-danger { - @include generateSecondary2("danger"); - } - &.yakit-button-infoBlue { - @include generateSecondary2("infoBlue"); - } -} -.yakit-button-outline1 { - @include generateOutline1("primary"); - &.yakit-button-success { - @include generateOutline1("success"); - } - &.yakit-button-danger { - @include generateOutline1("danger"); - } - &.yakit-button-infoBlue { - @include generateOutline1("infoBlue"); - } -} -.yakit-button-outline2 { - @include generateOutline2("primary"); -} -.yakit-button-text { - @include generateText("primary"); - &.yakit-button-success { - @include generateText("success"); - } - &.yakit-button-danger { - @include generateText("danger"); - } - &.yakit-button-infoBlue { - @include generateText("infoBlue"); - } -} -.yakit-button-text2 { - @include generateText2("primary"); +.yakit-border-radius-button { + --yakit-button-border-radius: 8px; } diff --git a/src/compoments/yakitUI/YakitEditor/editors.module.scss b/src/compoments/yakitUI/YakitEditor/editors.module.scss new file mode 100644 index 00000000..223a70cd --- /dev/null +++ b/src/compoments/yakitUI/YakitEditor/editors.module.scss @@ -0,0 +1,115 @@ +// .hex-editor-style { +// :global { +// .byteValue { +// .nybbleHighValue { +// word-spacing: -0.5em; +// } +// .nybbleLowValue { +// word-spacing: -0.5em; +// } +// } +// .asciiValue { +// word-spacing: -0.5em; +// } +// } +// } +.monaco-editor-style { + :global { + .crlf::before { + opacity: 0; + } + .lf::before { + opacity: 0; + } + } +} +:global { + /* 普通 hover 行 */ + .monaco-editor .suggest-widget .monaco-list-row:hover:not(.focused):not(.selected) { + background-color: var(--Colors-Use-Neutral-Bg-Hover) !important; + color: var(--Colors-Use-Neutral-Text-3-Secondary) !important; + } + + .crlf::before { + content: "↵"; + background-color: var(--yakit-colors-Neutral-40); /* 灰色背景 */ + margin: 4px; + padding: 2px; /* 根据需要添加内边距 */ + border-radius: 3px; /* 添加圆角以使样式更好看 */ + color: var(--Colors-Use-Neutral-Text-1-Title); /* 设置文字颜色为白色,以便在灰色背景上更清晰可见 */ + opacity: 0.3; + } + .content-length:hover::after { + content: "发包时自动修复,一般无需指定"; + background-color: var(--Colors-Use-Neutral-Bg); + color: var(--Colors-Use-Neutral-Text-3-Secondary); + padding: 5px; + border: 1px solid var(--Colors-Use-Neutral-Border); + border-radius: 4px; + } + .lf::before { + content: "↓"; + background-color: var(--Colors-Use-Neutral-Bg); /* 灰色背景 */ + margin: 4px; + padding: 2px; /* 根据需要添加内边距 */ + border-radius: 3px; /* 添加圆角以使样式更好看 */ + color: var(--Colors-Use-Neutral-Text-1-Title); /* 设置文字颜色为白色,以便在灰色背景上更清晰可见 */ + opacity: 0.3; + } + .unicode-decode { + background-color: var(--Colors-Use-Blue-Bg-Hover); + } + + .unicode-decode::after { + content: " "; + background-color: var(--Colors-Use-Basic-Background); + } + + .unicode-decode-after { + color: var(--Colors-Use-Neutral-Text-1-Title); + background-color: var(--Colors-Use-Neutral-Bg); + padding-left: 2px; + border-radius: 3px; + opacity: 0.3; + font-weight: bold; + cursor: none; + user-select: none; + } +} +.new-http-packet-editor { + width: 100%; + height: 100%; + overflow: hidden; + background-color: var(--Colors-Use-Basic-Background); + :global { + .ant-card-head { + background-color: var(--Colors-Use-Basic-Background); + height: 32px; + min-height: 32px; + box-sizing: content-box; + border-bottom: 1px solid var(--Colors-Use-Neutral-Border); + .ant-card-head-wrapper { + height: 100%; + .ant-card-head-title { + padding: 0px; + display: flex; + align-items: center; + } + .ant-card-extra { + padding: 0px; + } + } + } + .flex-card { + border-radius: 4px 0 0 4px; + } + } +} +.type-options-checkable-tag { + display: flex; + :global { + .ant-tag { + margin-right: 2px; + } + } +} diff --git a/src/compoments/yakitUI/YakitEditor/editors.tsx b/src/compoments/yakitUI/YakitEditor/editors.tsx new file mode 100644 index 00000000..346c1761 --- /dev/null +++ b/src/compoments/yakitUI/YakitEditor/editors.tsx @@ -0,0 +1,433 @@ +import React, { useEffect, useState } from 'react' +import { Card, Form, Popover, Tooltip } from 'antd' +import { useMemoizedFn } from 'ahooks' +import { EnterOutlined, FullscreenOutlined, SettingOutlined } from '@ant-design/icons' +import classNames from 'classnames' +import { v4 as uuidv4 } from 'uuid' +import type * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api' +// 编辑器 注册 +import '@/utils/monacoSpec/theme' +import '@/utils/monacoSpec/fuzzHTTP' +import '@/utils/monacoSpec/yakEditor' +import '@/utils/monacoSpec/html' +import HexEditor from 'react-hex-editor' +import styles from './editors.module.scss' +// import { Buffer } from 'buffer'; +import type { OperationRecordRes, YakitEditorProps } from './YakitEditorType' +import { YakitEditor } from './YakitEditor' +import { SelectOne } from '@/compoments/ReportTemplate/compoments/utils/InputUtils' +import { StringToUint8Array } from '@/utils/str' +import { getRemoteValue, setRemoteValue } from '@/utils/kv' +import { YakitButton } from '../YakitButton/YakitButton' +import { showYakitModal } from '../YakitModal/YakitModalConfirm' +import { DataCompareModal } from '../compare/DataCompare' +import { showDrawer } from '@/utils/showModal' +import { YakitSwitch } from '../YakitSwitch/YakitSwitch' +export type IMonacoEditor = monacoEditor.editor.IStandaloneCodeEditor + +interface DataCompareProps { + rightCode: Uint8Array + /** 当存在leftCode时则使用leftCode,否则使用编辑器showValue */ + leftCode?: Uint8Array + leftTitle?: string + rightTitle?: string +} + +export interface NewHTTPPacketEditorProp { + /** yakit-editor组件基础属性 */ + disabled?: boolean + readOnly?: boolean + noLineNumber?: boolean + lineNumbersMinChars?: number + noMinimap?: boolean + onAddOverlayWidget?: (editor: IMonacoEditor, isShow?: boolean) => any + extraEditorProps?: YakitEditorProps | any + + /** 扩展属性 */ + originValue: string + defaultStringValue?: string + onChange?: (i: Buffer) => any + disableFullscreen?: boolean + defaultHeight?: number + bordered?: boolean + onEditor?: (editor: IMonacoEditor) => any + hideSearch?: boolean + extra?: React.ReactNode + extraEnd?: React.ReactNode + emptyOr?: React.ReactNode + + refreshTrigger?: boolean | any + simpleMode?: boolean + noHeader?: boolean + loading?: boolean + noModeTag?: boolean + + noPacketModifier?: boolean + noTitle?: boolean + title?: React.ReactNode + noHex?: boolean + + // lang + language?: 'html' | 'http' | 'yak' | any + + system?: string + isResponse?: boolean + utf8?: boolean + theme?: string + + isWebSocket?: boolean + webSocketValue?: Uint8Array + webSocketToServer?: Uint8Array + + /** @name 外部控制换行状态 */ + noWordWrapState?: boolean + /** @name 外部控制字体大小 */ + fontSizeState?: number + /** @name 是否显示换行符 */ + showLineBreaksState?: boolean + /** @name 是否增加OverlayWidget */ + isAddOverlayWidget?: boolean + /** @name 外部控制是否记录操作(拥有此项可记录字体大小及换行符) */ + editorOperationRecord?: string + /** @name 打开WebFuzzer的回调 */ + webFuzzerCallBack?: () => void + // 是否由外部接管children的渲染,如果是由外部接管children的渲染,则编辑器组件不再对children进行任何处理,完全由外部控制,适用于一些特殊场景,比如内嵌入一些特殊组件等 + children?: React.ReactNode + /** @name 是否显示显示Extra默认项 */ + showDefaultExtra?: boolean + /** @name 数据对比(默认无对比) */ + dataCompare?: DataCompareProps +} + +/** @name 获取换行符是否显示 */ +export const HTTP_PACKET_EDITOR_Line_Breaks = 'HTTP_PACKET_EDITOR_Line_Breaks' + +export const NewHTTPPacketEditor: React.FC = (props) => { + const { + emptyOr, + originValue, + isResponse, + readOnly, + editorOperationRecord, + dataCompare, + showDefaultExtra = true, + } = props + + const getEncoding = (): 'utf8' | 'latin1' | 'ascii' => { + if (isResponse || readOnly || props.utf8) { + return 'utf8' + } + // return "latin1" + return 'utf8' // 默认还是 UTF8 吧,不然识别不了 + } + const [mode] = useState('text') + const [strValue, setStrValue] = useState(originValue) + const [fontSize, setFontSize] = useState(12) + const [showLineBreaks, setShowLineBreaks] = useState(true) + const [hexValue, setHexValue] = useState(new Uint8Array(StringToUint8Array(originValue))) + const [monacoEditor, setMonacoEditor] = useState() + const [noWordwrap, setNoWordwrap] = useState(false) + const [popoverVisible, setPopoverVisible] = useState(false) + const [showValue, setShowValue] = useState(originValue) + // 对比loading + const [compareLoading, setCompareLoading] = useState(false) + // 编辑器Id 用于区分每个编辑器 + const [editorId] = useState(uuidv4()) + useEffect(() => { + if (editorOperationRecord) { + getRemoteValue(editorOperationRecord).then((data) => { + if (!data) return + let obj: OperationRecordRes = JSON.parse(data) + if (obj?.fontSize) { + setFontSize(obj?.fontSize) + } + if (typeof obj?.showBreak === 'boolean') { + setShowLineBreaks(obj?.showBreak) + } + }) + } + }, []) + + useEffect(() => { + setShowValue(originValue) + }, [originValue]) + + const openCompareModal = useMemoizedFn((dataCompare) => { + setCompareLoading(true) + setTimeout(() => { + const m = showYakitModal({ + title: null, + content: ( + m.destroy()} + rightTitle={dataCompare.rightTitle} + leftTitle={dataCompare.leftTitle} + leftCode={dataCompare.leftCode ? dataCompare.leftCode : showValue} + rightCode={dataCompare.rightCode} + loadCallBack={() => setCompareLoading(false)} + /> + ), + onCancel: () => { + m.destroy() + }, + width: 1200, + footer: null, + closable: false, + hiddenHeader: true, + }) + }, 500) + }) + + const [nonce, setNonce] = useState(0) + // The callback facilitates updates to the source data. + const handleSetValue = React.useCallback( + (offset: any, value: any) => { + hexValue[offset] = value + setNonce((v) => v + 1) + setHexValue(hexValue) + }, + [hexValue], + ) + + useEffect(() => { + if (readOnly) { + const value = originValue + setStrValue(value) + setHexValue(new Uint8Array(StringToUint8Array(originValue))) + } + if (readOnly && monacoEditor) { + monacoEditor.setSelection({ + startColumn: 0, + startLineNumber: 0, + endLineNumber: 0, + endColumn: 0, + }) + } + }, [originValue, readOnly, monacoEditor]) + + const empty = !!emptyOr && originValue.length === 0 + return ( +
    + + {!props.noTitle && + (props.title ? ( + props.title + ) : ( + {isResponse ? 'Response' : 'Request'} + ))} +
    + ) + } + bodyStyle={{ + padding: 0, + width: '100%', + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + flex: 1, + }} + extra={ + !props.noHeader && ( +
    + {props.extra} + {dataCompare && dataCompare.rightCode.length > 0 && ( + { + openCompareModal(dataCompare) + }} + > + 对比 + + )} + {showDefaultExtra && ( + <> + + } + onClick={() => { + setNoWordwrap(!noWordwrap) + }} + /> + + {!props.simpleMode && ( + { + e.preventDefault() + }} + size="small" + layout="horizontal" + wrapperCol={{ span: 14 }} + labelCol={{ span: 10 }} + > + {(fontSize || 0) > 0 && ( + { + setFontSize(size) + }} + /> + )} + + } + onClick={() => { + showDrawer({ + title: '全屏', + width: '100%', + content: ( +
    + +
    + ), + }) + setPopoverVisible(false) + }} + /> +
    + {(props.language === 'http' || !isResponse) && ( + + { + setRemoteValue(HTTP_PACKET_EDITOR_Line_Breaks, `${checked}`) + setShowLineBreaks(checked) + }} + /> + + )} + + } + onVisibleChange={(v) => { + setPopoverVisible(v) + }} + overlayInnerStyle={{ width: 300 }} + visible={popoverVisible} + > + } type="text" size="small" /> +
    + )} + + )} + {props.extraEnd} +
    + ) + } + > + {props.children ? ( +
    {props.children}
    + ) : ( +
    + {empty && props.emptyOr} + {mode === 'text' && !empty && ( + 0 ? showValue : strValue} + readOnly={props.readOnly} + disabled={props.disabled} + setValue={setStrValue} + noWordWrap={noWordwrap} + fontSize={fontSize} + showLineBreaks={showLineBreaks} + noPacketModifier={props.noPacketModifier} + editorDidMount={(editor) => { + setMonacoEditor(editor) + }} + editorOperationRecord={editorOperationRecord} + isWebSocket={props.isWebSocket} + webSocketValue={props.webSocketValue && new Buffer(props.webSocketValue).toString(getEncoding())} + webSocketToServer={ + props.webSocketToServer && new Buffer(props.webSocketToServer).toString(getEncoding()) + } + webFuzzerCallBack={props.webFuzzerCallBack} + editorId={editorId} + {...props.extraEditorProps} + /> + )} + {mode === 'hex' && !empty && ( + + )} +
    + )} + + + ) +} diff --git a/src/compoments/yakitUI/YakitForm/YakitForm.tsx b/src/compoments/yakitUI/YakitForm/YakitForm.tsx index 301f1b01..f5b9f8ea 100644 --- a/src/compoments/yakitUI/YakitForm/YakitForm.tsx +++ b/src/compoments/yakitUI/YakitForm/YakitForm.tsx @@ -1,58 +1,51 @@ -import { Upload, Form, Spin, Divider } from 'antd'; -import React, { type ReactNode, useMemo, useState } from 'react'; +import { Upload, Form, Spin, Divider } from 'antd' +import React, { type ReactNode, useMemo, useState } from 'react' import type { - FileDraggerProps, - YakitDraggerContentProps, - YakitDraggerProps, - YakitFormDraggerContentProps, - YakitFormDraggerProps, -} from './YakitFormType.d'; -import styles from './YakitForm.module.scss'; -import classNames from 'classnames'; -import { YakitInput } from '../YakitInput/YakitInput'; -import { useMemoizedFn } from 'ahooks'; -import { failed, yakitNotify } from '@/utils/notification'; + FileDraggerProps, + YakitDraggerContentProps, + YakitDraggerProps, + YakitFormDraggerContentProps, + YakitFormDraggerProps, +} from './YakitFormType' +import styles from './YakitForm.module.scss' +import classNames from 'classnames' +import { YakitInput } from '../YakitInput/YakitInput' +import { useMemoizedFn } from 'ahooks' +import { failed, yakitNotify } from '@/utils/notification' -const { Dragger } = Upload; +const { Dragger } = Upload const isAcceptEligible = (path: string, accept?: string) => { - const index = path.lastIndexOf('.'); - const fileType = path.substring(index, path.length); - if (accept === '.*') { - return index === -1 ? false : true; - } - return accept ? accept.split(',').includes(fileType) : true; -}; + const index = path.lastIndexOf('.') + const fileType = path.substring(index, path.length) + if (accept === '.*') { + return index === -1 ? false : true + } + return accept ? accept.split(',').includes(fileType) : true +} /** * @description:YakitFormDragger form表单的文件拖拽 文件夹不支持拖拽 * @augments YakitFormDraggerProps 继承antd的 DraggerProps 默认属性 和 YakitDraggerProps */ -export const YakitFormDragger: React.FC = React.memo( - (props) => { - const { - formItemProps = {}, - size, - formItemClassName, - ...restProps - } = props; - return ( - - - - ); - }, -); +export const YakitFormDragger: React.FC = React.memo((props) => { + const { formItemProps = {}, size, formItemClassName, ...restProps } = props + return ( + + + + ) +}) /** * @description:YakitDragger 支持拖拽:文件/文件夹 文件路径只包括文件夹或者文件的第一级路径, 不包括文件夹下面的子文件路径数; @@ -74,675 +67,611 @@ export const YakitFormDragger: React.FC = React.memo( /> */ export const YakitDragger: React.FC = React.memo((props) => { - const { - size, - inputProps = {}, - help = '可将文件拖入框内或点击此处', - value: fileName, - onChange: setFileName, - setContent, - showDefHelp = true, - selectType = 'file', - renderType = 'input', - textareaProps = {}, - disabled, - isShowPathNumber = true, - multiple, - } = props; - const [uploadLoading, setUploadLoading] = useState(false); - const [name, setName] = useState(''); + const { + size, + inputProps = {}, + help = '可将文件拖入框内或点击此处', + value: fileName, + onChange: setFileName, + setContent, + showDefHelp = true, + selectType = 'file', + renderType = 'input', + textareaProps = {}, + disabled, + isShowPathNumber = true, + multiple, + } = props + const [uploadLoading, setUploadLoading] = useState(false) + const [name, setName] = useState('') - const getContent = useMemoizedFn((path: string, fileType: string) => { - if (!path) { - failed('请输入路径'); - return; - } - const index = path.lastIndexOf('.'); - - if (selectType === 'file' && index === -1) { - failed('请输入正确的路径'); - return; - } + const getContent = useMemoizedFn((path: string, fileType: string) => { + if (!path) { + failed('请输入路径') + return + } + const index = path.lastIndexOf('.') - if (props.accept && !props.accept.split(',').includes(fileType)) { - failed(`仅支持${props.accept}格式的文件`); - return; - } - // 设置名字 - if (setFileName) { - setFileName(path); - } - if (selectType === 'file' && setContent) { - setUploadLoading(true); - // ipcRenderer - // .invoke("fetch-file-content", path) - // .then((res) => { - // setContent(res) - // }) - // .catch((err) => { - // failed("数据获取失败:" + err) - // setContent("") - // }) - // .finally(() => setTimeout(() => setUploadLoading(false), 200)) - } - }); + if (selectType === 'file' && index === -1) { + failed('请输入正确的路径') + return + } - const renderContentValue = useMemoizedFn(() => { - switch (renderType) { - case 'textarea': - return ( - { - setName(e.target.value); - if (setFileName) setFileName(e.target.value); - if (textareaProps.onChange) - textareaProps.onChange(e); - e.stopPropagation(); - }} - onPressEnter={(e) => { - e.stopPropagation(); - const index = name.lastIndexOf('.'); - if (selectType === 'file' && index === -1) { - failed('请输入正确的路径'); - return; - } - const type = name.substring(index, name.length); - getContent(name, type); - if (textareaProps.onPressEnter) - textareaProps.onPressEnter(e); - }} - onFocus={(e) => { - e.stopPropagation(); - if (textareaProps.onFocus) textareaProps.onFocus(e); - }} - onClick={(e) => { - e.stopPropagation(); - if (textareaProps.onClick) textareaProps.onClick(e); - }} - onBlur={(e) => { - e.stopPropagation(); - if (!name) return; - const index = name.lastIndexOf('.'); - if (selectType === 'file' && index === -1) { - failed('请输入正确的路径'); - return; - } - const type = name.substring(index, name.length); - getContent(name, type); - if (textareaProps.onBlur) textareaProps.onBlur(e); - }} - /> - ); + if (props.accept && !props.accept.split(',').includes(fileType)) { + failed(`仅支持${props.accept}格式的文件`) + return + } + // 设置名字 + if (setFileName) { + setFileName(path) + } + if (selectType === 'file' && setContent) { + setUploadLoading(true) + // ipcRenderer + // .invoke("fetch-file-content", path) + // .then((res) => { + // setContent(res) + // }) + // .catch((err) => { + // failed("数据获取失败:" + err) + // setContent("") + // }) + // .finally(() => setTimeout(() => setUploadLoading(false), 200)) + } + }) - default: - return ( - { - setName(e.target.value); - if (setFileName) setFileName(e.target.value); - if (inputProps.onChange) inputProps.onChange(e); - e.stopPropagation(); - }} - onPressEnter={(e) => { - e.stopPropagation(); - const index = name.lastIndexOf('.'); - if (selectType === 'file' && index === -1) { - failed('请输入正确的路径'); - return; - } - const type = name.substring(index, name.length); - getContent(name, type); - if (inputProps.onPressEnter) - inputProps.onPressEnter(e); - }} - onFocus={(e) => { - e.stopPropagation(); - if (inputProps.onFocus) inputProps.onFocus(e); - }} - onClick={(e) => { - e.stopPropagation(); - if (inputProps.onClick) inputProps.onClick(e); - }} - onBlur={(e) => { - e.stopPropagation(); - if (!name) return; - const index = name.lastIndexOf('.'); - if (selectType === 'file' && index === -1) { - failed('请输入正确的路径'); - return; - } - const type = name.substring(index, name.length); - getContent(name, type); - if (inputProps.onBlur) inputProps.onBlur(e); - }} - /> - ); - } - }); + const renderContentValue = useMemoizedFn(() => { + switch (renderType) { + case 'textarea': + return ( + { + setName(e.target.value) + if (setFileName) setFileName(e.target.value) + if (textareaProps.onChange) textareaProps.onChange(e) + e.stopPropagation() + }} + onPressEnter={(e) => { + e.stopPropagation() + const index = name.lastIndexOf('.') + if (selectType === 'file' && index === -1) { + failed('请输入正确的路径') + return + } + const type = name.substring(index, name.length) + getContent(name, type) + if (textareaProps.onPressEnter) textareaProps.onPressEnter(e) + }} + onFocus={(e) => { + e.stopPropagation() + if (textareaProps.onFocus) textareaProps.onFocus(e) + }} + onClick={(e) => { + e.stopPropagation() + if (textareaProps.onClick) textareaProps.onClick(e) + }} + onBlur={(e) => { + e.stopPropagation() + if (!name) return + const index = name.lastIndexOf('.') + if (selectType === 'file' && index === -1) { + failed('请输入正确的路径') + return + } + const type = name.substring(index, name.length) + getContent(name, type) + if (textareaProps.onBlur) textareaProps.onBlur(e) + }} + /> + ) - const renderContent = useMemoizedFn((helpNode: ReactNode) => { + default: return ( - - {renderContentValue()} -
    - {(showDefHelp && helpNode) || null} -
    -
    - ); - }); - /** - * @description 选择文件夹 - */ - const onUploadFolder = useMemoizedFn(() => { - if (disabled) return; - const properties = ['openDirectory']; - if (multiple !== false) { - properties.push('multiSelections'); - } - // ipcRenderer - // .invoke("openDialog", { - // title: "请选择文件夹", - // properties - // }) - // .then((data: {filePaths: string[]}) => { - // const filesLength = data.filePaths.length - // if (filesLength) { - // const absolutePath = data.filePaths.map((p) => p.replace(/\\/g, "\\")).join(",") - // // 设置名字 - // if (setFileName) setFileName(absolutePath) - // } - // }) - }); + { + setName(e.target.value) + if (setFileName) setFileName(e.target.value) + if (inputProps.onChange) inputProps.onChange(e) + e.stopPropagation() + }} + onPressEnter={(e) => { + e.stopPropagation() + const index = name.lastIndexOf('.') + if (selectType === 'file' && index === -1) { + failed('请输入正确的路径') + return + } + const type = name.substring(index, name.length) + getContent(name, type) + if (inputProps.onPressEnter) inputProps.onPressEnter(e) + }} + onFocus={(e) => { + e.stopPropagation() + if (inputProps.onFocus) inputProps.onFocus(e) + }} + onClick={(e) => { + e.stopPropagation() + if (inputProps.onClick) inputProps.onClick(e) + }} + onBlur={(e) => { + e.stopPropagation() + if (!name) return + const index = name.lastIndexOf('.') + if (selectType === 'file' && index === -1) { + failed('请输入正确的路径') + return + } + const type = name.substring(index, name.length) + getContent(name, type) + if (inputProps.onBlur) inputProps.onBlur(e) + }} + /> + ) + } + }) - const onUploadFile = useMemoizedFn(() => { - if (disabled) return; - const properties = ['openFile']; - if (multiple !== false) { - properties.push('multiSelections'); - } - // ipcRenderer - // .invoke("openDialog", { - // title: "请选择文件", - // properties - // }) - // .then((data: {filePaths: string[]}) => { - // const filesLength = data.filePaths.length - // if (filesLength) { - // const absolutePath: string[] = [] - // data.filePaths.forEach((p) => { - // const path = p.replace(/\\/g, "\\") - // if (isAcceptEligible(path, props.accept || ".*")) { - // absolutePath.push(path) - // } - // }) - // // 设置名字 - // if (setFileName) setFileName(absolutePath.join(",")) - // } - // }) - }); + const renderContent = useMemoizedFn((helpNode: ReactNode) => { + return ( + + {renderContentValue()} +
    + {(showDefHelp && helpNode) || null} +
    +
    + ) + }) + /** + * @description 选择文件夹 + */ + const onUploadFolder = useMemoizedFn(() => { + if (disabled) return + const input = document.createElement('input') + input.type = 'file' + if (multiple !== false) { + input.multiple = true + } + input.setAttribute('webkitdirectory', '') + input.setAttribute('directory', '') + input.onchange = () => { + const files = Array.from(input.files || []) + if (files.length && setFileName) { + const paths = files.map((file) => file.webkitRelativePath || file.name) + setFileName(paths.join(',')) + } + } + input.click() + }) - const afterFolderDrop = useMemoizedFn((e) => { - const { files = [] } = e.dataTransfer; - let paths: string[] = []; - let isNoFit: string[] = []; - const filesLength = files.length; - if (multiple === false && filesLength > 1) { - yakitNotify('error', '不支持多选'); - return; - } - for (let index = 0; index < filesLength; index++) { - const element = files[index]; - const path = element.path || ''; - const number = path.lastIndexOf('.'); - if (number !== -1) { - isNoFit.push(path); - } else { - paths.push(path); - } - } - if (isNoFit.length > 0) { - yakitNotify('error', '已自动过滤不符合条件的数据'); - } - if (filesLength > isNoFit.length && setFileName) - setFileName(paths.join(',')); - }); + const onUploadFile = useMemoizedFn(() => { + if (disabled) return + const input = document.createElement('input') + input.type = 'file' + if (multiple !== false) { + input.multiple = true + } + if (props.accept) { + input.accept = props.accept + } + input.onchange = () => { + const files = Array.from(input.files || []) + if (files.length) { + const absolutePath = files + .map((file) => file.name) + .filter((path) => isAcceptEligible(path, props.accept || '.*')) + if (setFileName) setFileName(absolutePath.join(',')) + } + } + input.click() + }) - const afterFileDrop = useMemoizedFn((e) => { - const { files = [] } = e.dataTransfer; - let paths: string[] = []; - let isNoFit: string[] = []; - const filesLength = files.length; - if (multiple === false && filesLength > 1) { - yakitNotify('error', '不支持多选'); - return; - } - for (let index = 0; index < filesLength; index++) { - const element = files[index]; - const path = element.path || ''; - if (isAcceptEligible(path, props.accept || '.*')) { - paths.push(path); - } else { - isNoFit.push(path); - } - } - if (isNoFit.length > 0) { - yakitNotify('error', '已自动过滤不符合条件的数据'); - } - if (filesLength > isNoFit.length && setFileName) - setFileName(paths.join(',')); - }); + const afterFolderDrop = useMemoizedFn((e) => { + const { files = [] } = e.dataTransfer + let paths: string[] = [] + let isNoFit: string[] = [] + const filesLength = files.length + if (multiple === false && filesLength > 1) { + yakitNotify('error', '不支持多选') + return + } + for (let index = 0; index < filesLength; index++) { + const element = files[index] + const path = element.path || '' + const number = path.lastIndexOf('.') + if (number !== -1) { + isNoFit.push(path) + } else { + paths.push(path) + } + } + if (isNoFit.length > 0) { + yakitNotify('error', '已自动过滤不符合条件的数据') + } + if (filesLength > isNoFit.length && setFileName) setFileName(paths.join(',')) + }) - const afterAllDrop = useMemoizedFn((e) => { - const { files = [] } = e.dataTransfer; - let paths: string[] = []; - const filesLength = files.length; - if (multiple === false && filesLength > 1) { - yakitNotify('error', '不支持多选'); - return; - } - for (let index = 0; index < filesLength; index++) { - const element = files[index]; - const path = element.path || ''; - paths.push(path); - } - if (setFileName) setFileName(paths.join(',')); - }); - const fileNumber = useMemo(() => { - let arr: string[] = []; - try { - const path = fileName || name; - arr = path ? path.split(',') : []; - } catch (error) { - yakitNotify('error', '文件路径数识别错误,请以逗号进行分割'); - } - return arr.length; - }, [fileName, name]); + const afterFileDrop = useMemoizedFn((e) => { + const { files = [] } = e.dataTransfer + let paths: string[] = [] + let isNoFit: string[] = [] + const filesLength = files.length + if (multiple === false && filesLength > 1) { + yakitNotify('error', '不支持多选') + return + } + for (let index = 0; index < filesLength; index++) { + const element = files[index] + const path = element.path || '' + if (isAcceptEligible(path, props.accept || '.*')) { + paths.push(path) + } else { + isNoFit.push(path) + } + } + if (isNoFit.length > 0) { + yakitNotify('error', '已自动过滤不符合条件的数据') + } + if (filesLength > isNoFit.length && setFileName) setFileName(paths.join(',')) + }) + + const afterAllDrop = useMemoizedFn((e) => { + const { files = [] } = e.dataTransfer + let paths: string[] = [] + const filesLength = files.length + if (multiple === false && filesLength > 1) { + yakitNotify('error', '不支持多选') + return + } + for (let index = 0; index < filesLength; index++) { + const element = files[index] + const path = element.path || '' + paths.push(path) + } + if (setFileName) setFileName(paths.join(',')) + }) + const fileNumber = useMemo(() => { + let arr: string[] = [] + try { + const path = fileName || name + arr = path ? path.split(',') : [] + } catch (error) { + yakitNotify('error', '文件路径数识别错误,请以逗号进行分割') + } + return arr.length + }, [fileName, name]) + return ( + <> + {selectType === 'file' && ( + + {renderContent( +
    + + {help} + { + e.stopPropagation() + onUploadFile() + }} + > + 上传文件 + + + {isShowPathNumber && ( + + 识别到 + {fileNumber} + 个文件路径 + + )} +
    , + )} +
    + )} + {selectType === 'folder' && ( + + {renderContent( +
    + + {help} + { + e.stopPropagation() + onUploadFolder() + }} + > + 上传文件夹 + + + {isShowPathNumber && ( + + 识别到 + {fileNumber} + 个文件路径 + + )} +
    , + )} +
    + )} + {selectType === 'all' && ( + + {renderContent( +
    + + {help} + { + e.stopPropagation() + onUploadFile() + }} + > + 上传文件 + + + { + e.stopPropagation() + onUploadFolder() + }} + > + 上传文件夹 + + + + {isShowPathNumber && ( + + 识别到 + {fileNumber} + 个文件路径 + + )} +
    , + )} +
    + )} + + ) +}) + +export const YakitDraggerContent: React.FC = React.memo((props) => { + const { + value, + disabled, + size, + textareaProps = {}, + onChange, + help, + showDefHelp, + fileLimit = 1024, + valueSeparator = ',', + ...restProps + } = props + const [uploadLoading, setUploadLoading] = useState(false) + const renderContent = useMemoizedFn((helpNode: ReactNode) => { return ( - <> - {selectType === 'file' && ( - - {renderContent( -
    - - {help} - { - e.stopPropagation(); - onUploadFile(); - }} - > - 上传文件 - - - {isShowPathNumber && ( - - 识别到 - - {fileNumber} - - 个文件路径 - - )} -
    , - )} -
    - )} - {selectType === 'folder' && ( - - {renderContent( -
    - - {help} - { - e.stopPropagation(); - onUploadFolder(); - }} - > - 上传文件夹 - - - {isShowPathNumber && ( - - 识别到 - - {fileNumber} - - 个文件路径 - - )} -
    , - )} -
    - )} - {selectType === 'all' && ( - - {renderContent( -
    - - {help} - { - e.stopPropagation(); - onUploadFile(); - }} - > - 上传文件 - - - { - e.stopPropagation(); - onUploadFolder(); - }} - > - 上传文件夹 - - + + { + if (onChange) onChange(e.target.value) + if (textareaProps.onChange) textareaProps.onChange(e) + e.stopPropagation() + }} + onPressEnter={(e) => { + e.stopPropagation() + if (textareaProps.onPressEnter) textareaProps.onPressEnter(e) + }} + onFocus={(e) => { + e.stopPropagation() + if (textareaProps.onFocus) textareaProps.onFocus(e) + }} + onClick={(e) => { + e.stopPropagation() + if (textareaProps.onClick) textareaProps.onClick(e) + }} + onBlur={(e) => { + e.stopPropagation() + if (textareaProps.onBlur) textareaProps.onBlur(e) + }} + /> +
    + {helpNode} +
    +
    + ) + }) - {isShowPathNumber && ( - - 识别到 - - {fileNumber} - - 个文件路径 - - )} -
    , - )} -
    - )} - - ); -}); + const onHandlerFile = useMemoizedFn((item: { size: number; path: string; file: File }) => { + if (item.size / 1024 > fileLimit) { + yakitNotify('error', `文件大小不能超过${fileLimit}KB`) + return + } + const path = item.path.replace(/\\/g, '\\') + if (isAcceptEligible(path, props.accept || '.*')) { + onGetContent(item.file) + } else { + yakitNotify('error', '文件类型不支持') + } + }) -export const YakitDraggerContent: React.FC = - React.memo((props) => { - const { - value, - disabled, - size, - textareaProps = {}, - onChange, - help, - showDefHelp, - fileLimit = 1024, - ...restProps - } = props; - const [uploadLoading, setUploadLoading] = useState(false); - const renderContent = useMemoizedFn((helpNode: ReactNode) => { - return ( - - { - if (onChange) onChange(e.target.value); - if (textareaProps.onChange) - textareaProps.onChange(e); - e.stopPropagation(); - }} - onPressEnter={(e) => { - e.stopPropagation(); - if (textareaProps.onPressEnter) - textareaProps.onPressEnter(e); - }} - onFocus={(e) => { - e.stopPropagation(); - if (textareaProps.onFocus) textareaProps.onFocus(e); - }} - onClick={(e) => { - e.stopPropagation(); - if (textareaProps.onClick) textareaProps.onClick(e); - }} - onBlur={(e) => { - e.stopPropagation(); - if (textareaProps.onBlur) textareaProps.onBlur(e); - }} - /> -
    - {helpNode} -
    -
    - ); - }); + const afterFileDrop = useMemoizedFn((e) => { + if (disabled) return + const { files = [] } = e.dataTransfer + const filesLength = files.length + if (filesLength > 1) { + yakitNotify('error', '多选的文件只会选择其中一个文件处理') + } + if (filesLength > 0) { + const item = files[0] + onHandlerFile({ + size: item.size, + path: item.name, + file: item, + }) + } + }) - const onHandlerFile = useMemoizedFn( - (item: { size: number; path: string }) => { - if (item.size / 1024 > fileLimit) { - yakitNotify('error', `文件大小不能超过${fileLimit}KB`); - return; - } - const path = item.path.replace(/\\/g, '\\'); - if (isAcceptEligible(path, props.accept || '.*')) { - onGetContent(); - } else { - yakitNotify('error', '文件类型不支持'); - } - }, - ); + const onUploadFile = useMemoizedFn((e) => { + e.stopPropagation() + if (disabled) return + const input = document.createElement('input') + input.type = 'file' + if (props.accept) { + input.accept = props.accept + } + input.onchange = () => { + const files = Array.from(input.files || []) + if (files.length > 1) { + yakitNotify('error', '只支持单文件上传') + } + if (files.length > 0) { + const item = files[0] + onHandlerFile({ + size: item.size, + path: item.name, + file: item, + }) + } + } + input.click() + }) - const afterFileDrop = useMemoizedFn((e) => { - if (disabled) return; - const { files = [] } = e.dataTransfer; - const filesLength = files.length; - if (filesLength > 1) { - yakitNotify('error', '多选的文件只会选择其中一个文件处理'); - } - if (filesLength > 0) { - const item = files[0]; - onHandlerFile(item); - } - }); + const onGetContent = useMemoizedFn(async (file: File) => { + setUploadLoading(true) + try { + const fileName = file.name || '' + const type = fileName.split('.').pop()?.toLowerCase() || '' + const typeArr = ['csv', 'xls', 'xlsx'] - const onUploadFile = useMemoizedFn((e) => { - e.stopPropagation(); - if (disabled) return; - // ipcRenderer - // .invoke("openDialog", { - // title: "请选择文件", - // properties: ["openFile"] - // }) - // .then((data: {filePaths: string[]}) => { - // const filesLength = data.filePaths.length - // if (filesLength === 1) { - // const path: string = data.filePaths[0].replace(/\\/g, "\\") - // ipcRenderer - // .invoke("fetch-file-info-by-path", path) - // .then((fileInfo) => { - // onHandlerFile({ - // size: fileInfo.size, - // path - // }) - // }) - // .catch((err) => { - // yakitNotify("error", "文件数据读取异常:" + err) - // }) - // } else if (filesLength > 1) { - // yakitNotify("error", "只支持单文件上传") - // } - // }) - }); + if (typeArr.includes(type)) { + if (type === 'xls') { + throw new Error('暂不支持 .xls,请转为 .xlsx') + } - const onGetContent = useMemoizedFn(() => { - setUploadLoading(true); - // ipcRenderer - // .invoke("fetch-file-content", path) - // .then((res: string | {name: string; data: string[]}[]) => { - // if (Array.isArray(res)) { - // // 表格文件读取出来的 - // let data: string[] = [] - // res.forEach((element) => { - // data = data.concat(element.data) - // }) - // const value = data.join(valueSeparator) - // if (onChange) onChange(value) - // } else { - // // 其他文件读取出来的 - // if (onChange) onChange(res) - // } - // }) - // .catch((err) => { - // failed("数据获取失败:" + err) - // }) - // .finally(() => setTimeout(() => setUploadLoading(false), 200)) - }); - return ( - { - return false; - }} - > - {renderContent( -
    - -
    , - )} -
    - ); - }); + if (type === 'csv') { + const text = await file.text() + const rows = text + .split(/\r?\n/) + .filter(Boolean) + .map((line) => line.trim()) + if (onChange) onChange(rows.join(valueSeparator)) + return + } -export const YakitFormDraggerContent: React.FC = - React.memo((props) => { - const { - formItemProps = {}, - size, - formItemClassName, - ...restProps - } = props; - return ( - setUploadLoading(false), 200) + } + }) + return ( + { + return false + }} + > + {renderContent( +
    + +
    , + )} +
    + ) +}) + +export const YakitFormDraggerContent: React.FC = React.memo((props) => { + const { formItemProps = {}, size, formItemClassName, ...restProps } = props + return ( + + + + ) +}) const FileDragger: React.FC = React.memo((props) => { - const { disabled, multiple, onDrop, className, children } = props; - return ( -
    { - e.preventDefault(); - e.stopPropagation(); - if (disabled) return; - const { files = [] } = e.dataTransfer; - const filesLength = files.length; - if (multiple === false && filesLength > 1) { - yakitNotify('error', '不允许多选'); - return; - } - if (onDrop) onDrop(e); - }} - className={classNames(styles['yakit-dragger'], className)} - > - {children} -
    - ); -}); + const { disabled, multiple, onDrop, className, children } = props + return ( +
    { + e.preventDefault() + e.stopPropagation() + if (disabled) return + const { files = [] } = e.dataTransfer + const filesLength = files.length + if (multiple === false && filesLength > 1) { + yakitNotify('error', '不允许多选') + return + } + if (onDrop) onDrop(e) + }} + className={classNames(styles['yakit-dragger'], className)} + > + {children} +
    + ) +}) diff --git a/src/compoments/yakitUI/YakitForm/YakitFormType.d.ts b/src/compoments/yakitUI/YakitForm/YakitFormType.d.ts index 6d1318e3..bed08896 100644 --- a/src/compoments/yakitUI/YakitForm/YakitFormType.d.ts +++ b/src/compoments/yakitUI/YakitForm/YakitFormType.d.ts @@ -1,72 +1,68 @@ -import type { FormItemProps, InputProps } from 'antd'; -import type { DraggerProps } from 'antd/lib/upload'; -import type { YakitSizeType } from '../YakitInputNumber/YakitInputNumberType'; -import type { InternalTextAreaProps } from '../YakitInput/YakitInputType'; -import type { ReactNode } from 'react'; +import type { YakitAutoCompleteProps } from './../YakitAutoComplete/YakitAutoCompleteType.d' +import type { FormItemProps, InputProps } from 'antd' +import type { DraggerProps } from 'antd/lib/upload' +import type { YakitSizeType } from '../YakitInputNumber/YakitInputNumberType' +import type { InternalTextAreaProps } from '../YakitInput/YakitInputType' +import type { ReactNode } from 'react' -type YakitDragger = Omit; +type YakitDragger = Omit export interface FileDraggerProps { - disabled?: boolean; - multiple?: boolean; - className?: string; - children?: ReactNode; - onDrop?: (e: React.DragEvent) => void; + disabled?: boolean + multiple?: boolean + className?: string + children?: ReactNode + onDrop?: (e: React.DragEvent) => void } export interface YakitFormDraggerProps extends YakitDraggerProps { - formItemClassName?: string; - formItemProps?: FormItemProps; + formItemClassName?: string + formItemProps?: FormItemProps } export interface YakitDraggerProps extends FileDraggerProps { - size?: YakitSizeType; - inputProps?: InputProps; - setContent?: (s: string) => void; - help?: ReactDOM; - showDefHelp?: boolean; - value?: string; - onChange?: (s: string) => void; - selectType?: 'file' | 'folder' | 'all'; + size?: YakitSizeType + inputProps?: InputProps + setContent?: (s: string) => void + help?: ReactDOM + showDefHelp?: boolean + value?: string + onChange?: (s: string) => void + selectType?: 'file' | 'folder' | 'all' - /** 展示组件 input|textarea */ - renderType?: 'input' | 'textarea'; - /** textarea的props */ - textareaProps?: InternalTextAreaProps; - /** 是否显示路径数量 */ - isShowPathNumber?: boolean; + /** 展示组件 input|textarea */ + renderType?: 'input' | 'textarea' | 'autoComplete' + /** autoComplete的props */ + autoCompleteProps?: YakitAutoCompleteProps + /** textarea的props */ + textareaProps?: InternalTextAreaProps + /** 是否显示路径数量 */ + isShowPathNumber?: boolean - /** 接受的文件类型 */ - accept?: string; + /** 接受的文件类型 */ + accept?: string } export interface YakitDraggerContentProps - extends Omit< - YakitDragger, - | 'showUploadList' - | 'directory' - | 'multiple' - | 'beforeUpload' - | 'onChange' - > { - /** textarea的props */ - textareaProps?: InternalTextAreaProps; - size?: YakitSizeType; - /** 回显的文本值 */ - value?: string; - /** @description 回显的文本回调事件 */ - onChange?: (s: string) => void; - help?: ReactDOM; - showDefHelp?: boolean; - // InputProps?: InputProps - // /** 展示组件 input|textarea */ - // renderType?: "input" | "textarea" - /** @default 500k */ - fileLimit?: number; - /** value的分隔符 @default ',' */ - valueSeparator?: string; + extends Omit { + /** textarea的props */ + textareaProps?: InternalTextAreaProps + size?: YakitSizeType + /** 回显的文本值 */ + value?: string + /** @description 回显的文本回调事件 */ + onChange?: (s: string) => void + help?: ReactDOM + showDefHelp?: boolean + // InputProps?: InputProps + // /** 展示组件 input|textarea */ + // renderType?: "input" | "textarea" + /** @default 500k */ + fileLimit?: number + /** value的分隔符 @default ',' */ + valueSeparator?: string } export interface YakitFormDraggerContentProps extends YakitDraggerContentProps { - formItemClassName?: string; - formItemProps?: FormItemProps; + formItemClassName?: string + formItemProps?: FormItemProps } diff --git a/src/compoments/yakitUI/YakitHint/YakitHintModal.tsx b/src/compoments/yakitUI/YakitHint/YakitHintModal.tsx index 74bc150d..1a177b2c 100644 --- a/src/compoments/yakitUI/YakitHint/YakitHintModal.tsx +++ b/src/compoments/yakitUI/YakitHint/YakitHintModal.tsx @@ -4,11 +4,11 @@ import Draggable from 'react-draggable'; import type { DraggableEvent, DraggableData } from 'react-draggable'; import type { HintModalProps, YakitHintModalProps } from './YakitHintType'; import { ShieldExclamationSvgIcon } from '@/assets/newIcon'; -import { YakitButton } from '../YakitButton/YakitButton'; import classNames from 'classnames'; import styles from './YakitHint.module.scss'; import { Resizable } from 're-resizable'; +import { YakitButton } from '../YakitButton/YakitButton'; export const YakitHintModal: React.FC = memo((props) => { const { diff --git a/src/compoments/yakitUI/YakitModal/YakitModal.tsx b/src/compoments/yakitUI/YakitModal/YakitModal.tsx index 7448a4ba..6c1a3f60 100644 --- a/src/compoments/yakitUI/YakitModal/YakitModal.tsx +++ b/src/compoments/yakitUI/YakitModal/YakitModal.tsx @@ -1,175 +1,175 @@ -import React, { type CSSProperties, type ReactNode, useMemo } from 'react'; -import { Modal, type ModalProps } from 'antd'; -import { YakitButton, type YakitButtonProp } from '../YakitButton/YakitButton'; -import { OutlineXIcon } from '@/assets/icon/outline'; -import classNames from 'classnames'; +import type { CSSProperties, ReactNode } from 'react' +import React, { useMemo } from 'react' +import type { ModalProps } from 'antd' +import { Modal } from 'antd' +import { useMemoizedFn } from 'ahooks' +import type { YakitButtonProp } from '../YakitButton/YakitButton' +import { YakitButton } from '../YakitButton/YakitButton' +import { OutlineXIcon } from '@/assets/icon/outline' +import classNames from 'classnames' -import styles from './yakitModal.module.scss'; +import styles from './yakitModal.module.scss' -export interface YakitModalProp - extends Omit< - ModalProps, - 'style' | 'cancelButtonProps' | 'okButtonProps' | 'okType' - > { - headerStyle?: CSSProperties; - footerStyle?: CSSProperties; +export interface YakitModalProp extends Omit { + headerStyle?: CSSProperties + footerStyle?: CSSProperties - cancelButtonProps?: YakitButtonProp; - okButtonProps?: YakitButtonProp; - okType?: YakitButtonProp['type']; + cancelButtonProps?: YakitButtonProp + okButtonProps?: YakitButtonProp + okType?: YakitButtonProp['type'] - /** @name 副标题 */ - subTitle?: ReactNode; - /** @name footer组件左侧操作区域 */ - footerExtra?: ReactNode; - /** 弹框类型 */ - type?: 'gray' | 'white'; - /** @name 弹框的尺寸 */ - size?: 'small' | 'large'; - /** - * @name 隐藏Header元素 - * @description 设为true时, headerStyle|title|subTitle|closable|closeIcon都将无效 - */ - hiddenHeader?: boolean; + onCloseX?: ModalProps['onCancel'] + + /** @name 副标题 */ + subTitle?: ReactNode + /** @name footer组件左侧操作区域 */ + footerExtra?: ReactNode + /** 弹框类型 */ + type?: 'gray' | 'white' + /** @name 弹框的尺寸 */ + size?: 'small' | 'large' + /** + * @name 隐藏Header元素 + * @description 设为true时, headerStyle|title|subTitle|closable|closeIcon都将无效 + */ + hiddenHeader?: boolean } /** 可以用,但是使用的时候考虑部分属性的覆盖重写问题, */ export const YakitModal: React.FC = (props) => { - const { - children, - /** 样式属性 ↓↓↓ */ - wrapClassName, - headerStyle, - bodyStyle, - footerStyle, - /** 原有属性 ↓↓↓ */ - closable = true, - closeIcon, - title, - footer, - cancelButtonProps, - cancelText = '取消', - okButtonProps, - confirmLoading, - okText = '确认', - okType, - onCancel, - onOk, - /** 自定义新增属性 ↓↓↓ */ - type = 'gray', - size = 'small', - subTitle, - footerExtra, - hiddenHeader = false, - ...resetProps - } = props; + const { + children, + /** 样式属性 ↓↓↓ */ + wrapClassName, + headerStyle, + bodyStyle, + footerStyle, + /** 原有属性 ↓↓↓ */ + closable = true, + closeIcon, + title, + footer, + cancelButtonProps, + cancelText, + okButtonProps, + confirmLoading, + okText, + okType, + onCloseX, + onCancel, + onOk, + /** 自定义新增属性 ↓↓↓ */ + type = 'gray', + size = 'small', + subTitle, + footerExtra, + hiddenHeader = false, + ...resetProps + } = props + + const typeClass = useMemo(() => { + if (type === 'white') return styles['yakit-modal-white'] + return styles['yakit-modal-gray'] + }, [type]) + const sizeClass = useMemo(() => { + if (size === 'large') return styles['yakit-modal-large'] + return styles['yakit-modal-small'] + }, [size]) - const typeClass = useMemo(() => { - if (type === 'white') return styles['yakit-modal-white']; - return styles['yakit-modal-gray']; - }, [type]); - const sizeClass = useMemo(() => { - if (size === 'large') return styles['yakit-modal-large']; - return styles['yakit-modal-small']; - }, [size]); + const onCancelX: YakitModalProp['onCancel'] = useMemoizedFn((e) => { + if (onCloseX) { + onCloseX(e) + } else { + onCancel?.(e) + } + }) + + const resultFooter = useMemo(() => { + if (footer === null) return null + if (footer) + return ( +
    + {footer as any} +
    + ) - const noFooterRender = useMemo(() => { - return ( - <> -
    - {footerExtra || null} -
    -
    - - {cancelText} - - - {okText} - -
    - - ); - }, [ - footerExtra, - size, - onCancel, - cancelButtonProps, - cancelText, - confirmLoading, - okType, - onOk, - okButtonProps, - okText, - ]); return ( - -
    - {!hiddenHeader && ( -
    - {!!title && ( -
    - {title} -
    - {subTitle} -
    -
    - )} - {closable && ( - : closeIcon} - onClick={onCancel} - /> - )} -
    - )} +
    +
    {footerExtra || null}
    +
    + + {cancelText ?? '取消'} + + + {okText ?? '确定'} + +
    +
    + ) as React.ReactNode + }, [ + cancelButtonProps, + cancelText, + footer, + footerExtra, + okButtonProps, + okText, + okType, + onCancel, + onOk, + confirmLoading, + size, + ]) -
    - {children} -
    + return ( + +
    + {!hiddenHeader && ( +
    + {!!title && ( +
    + {title} +
    {subTitle}
    +
    + )} + {closable && ( + } + onClick={onCancelX} + /> + )} +
    + )} - {footer === null ? null : ( -
    - {!footer - ? noFooterRender - : typeof footer === 'function' - ? null - : footer} -
    - )} -
    -
    - ); -}; +
    + {children} +
    + {resultFooter} +
    +
    + ) +} diff --git a/src/compoments/yakitUI/YakitModal/yakitModal.module.scss b/src/compoments/yakitUI/YakitModal/yakitModal.module.scss index c460289c..651971a8 100644 --- a/src/compoments/yakitUI/YakitModal/yakitModal.module.scss +++ b/src/compoments/yakitUI/YakitModal/yakitModal.module.scss @@ -43,6 +43,9 @@ .ant-modal-body { padding: 0; } + .ant-modal-content { + padding: 0; + } } } diff --git a/src/compoments/yakitUI/YakitPopconfirm/YakitPopconfirm.module.scss b/src/compoments/yakitUI/YakitPopconfirm/YakitPopconfirm.module.scss index cdad4c14..b4068878 100644 --- a/src/compoments/yakitUI/YakitPopconfirm/YakitPopconfirm.module.scss +++ b/src/compoments/yakitUI/YakitPopconfirm/YakitPopconfirm.module.scss @@ -3,10 +3,14 @@ .ant-popover-buttons { display: none; } + .ant-popconfirm-buttons { + display: none; + } .ant-popover-inner { border: 1px solid var(--Colors-Use-Neutral-Border); box-shadow: 0px 8px 16px var(--Colors-Use-Basic-Shadow); border-radius: 4px; + padding: 0px; } .ant-popover-arrow-content { box-shadow: 0px 8px 16px var(--Colors-Use-Basic-Shadow); diff --git a/src/compoments/yakitUI/YakitRoundCornerTag/YakitRoundCornerTag.module.scss b/src/compoments/yakitUI/YakitRoundCornerTag/YakitRoundCornerTag.module.scss new file mode 100644 index 00000000..f882ff04 --- /dev/null +++ b/src/compoments/yakitUI/YakitRoundCornerTag/YakitRoundCornerTag.module.scss @@ -0,0 +1,57 @@ +@use "../../../theme/themeify.scss" as tools; + +.yakit-round-corner-tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 0 6px; + border-radius: 8px; + + font-size: 11px; + font-weight: 400; + line-height: 16px; + white-space: nowrap; +} + +$color-type: ( + "blue": ( + color: var(--Colors-Use-Blue-Primary), + alph: 10% + ), + "green": ( + color: var(--Colors-Use-Green-Primary), + alph: 10% + ) +); + +@mixin generateColors() { + @each $type, $value in $color-type { + $color: map-get($value, "color"); + $bgColor: tools.fetch-color-transparen($color, map-get($value, "alph")); + + .yakit-round-corner-tag-#{$type} { + color: $color; + background-color: $bgColor; + svg { + color: $color; + } + } + } +} +@include generateColors(); + +.yakit-round-corner-tag-primary { + color: var(--Colors-Use-Neutral-Text-3-Secondary); + background-color: var(--Colors-Use-Neutral-Bg-Hover); + svg { + color: var(--Colors-Use-Neutral-Text-3-Secondary); + } +} + +.yakit-round-corner-tag-info { + color: var(--Colors-Use-Neutral-Text-3-Secondary); + background-color: tools.fetch-color-transparen(var(--Colors-Use-Neutral-Text-3-Secondary), 10%); + svg { + color: var(--Colors-Use-Neutral-Text-3-Secondary); + } +} diff --git a/src/compoments/yakitUI/YakitRoundCornerTag/YakitRoundCornerTag.tsx b/src/compoments/yakitUI/YakitRoundCornerTag/YakitRoundCornerTag.tsx new file mode 100644 index 00000000..9b380088 --- /dev/null +++ b/src/compoments/yakitUI/YakitRoundCornerTag/YakitRoundCornerTag.tsx @@ -0,0 +1,19 @@ +import React, { useMemo } from 'react' +import type { YakitRoundCornerTagProps } from './YakitRoundCornerTagType' +import classNames from 'classnames' +import styles from './YakitRoundCornerTag.module.scss' + +/** + * @name 圆角无边框-tag + */ +export const YakitRoundCornerTag: React.FC = (props) => { + const { wrapperClassName, color = 'primary', children } = props + + const colorClass = useMemo(() => { + return styles[`yakit-round-corner-tag-${color}`] + }, [color]) + + return ( +
    {children || null}
    + ) +} diff --git a/src/compoments/yakitUI/YakitRoundCornerTag/YakitRoundCornerTagType.d.ts b/src/compoments/yakitUI/YakitRoundCornerTag/YakitRoundCornerTagType.d.ts new file mode 100644 index 00000000..4494795e --- /dev/null +++ b/src/compoments/yakitUI/YakitRoundCornerTag/YakitRoundCornerTagType.d.ts @@ -0,0 +1,12 @@ +import type { ReactNode } from 'react' + +export interface YakitRoundCornerTagProps { + wrapperClassName?: string + /** + * round-corner-tag的颜色 + * @default primary (本质为灰色) + * @description primary | blue | green | info + */ + color?: 'primary' | 'blue' | 'green' | 'info' + children?: ReactNode +} diff --git a/src/compoments/yakitUI/YakitTag/YakitTag.module.scss b/src/compoments/yakitUI/YakitTag/YakitTag.module.scss index 24b31220..47be4086 100644 --- a/src/compoments/yakitUI/YakitTag/YakitTag.module.scss +++ b/src/compoments/yakitUI/YakitTag/YakitTag.module.scss @@ -16,16 +16,16 @@ :global { .ant-tag-close-icon { - margin-left: 8px; + margin-left: 4px; display: inline-flex; align-items: center; - color: var(--Colors-Use-Neutral-Text-1-Title); width: 12px; height: 12px; - + flex-shrink: 0; svg { width: 12px; height: 12px; + color: var(--Colors-Use-Neutral-Text-4-Help-text); } .anticon { @@ -138,7 +138,7 @@ } .yakit-tag-purple { - background: var(--Colors-Use-Purple-Bg); + background: var(--Colors-Use-Purple-Bg-Hover); border-color: var(--Colors-Use-Purple-Border); color: var(--Colors-Use-Purple-Primary); @@ -160,10 +160,10 @@ .yakit-tag-cyan { background: var(--Colors-Use-Cyan-Bg); border-color: var(--Colors-Use-Cyan-Border); - color: var(--Colors-Use-Cyan-Primary); + color: var(--yakit-colors-Cyan-80); svg { - color: var(--Colors-Use-Cyan-Primary); + color: var(--yakit-colors-Cyan-80); } } @@ -178,12 +178,12 @@ } .yakit-tag-yellow { - color: var(--Colors-Use-Yellow-Primary); + color: var(--yakit-colors-Yellow-80); border-color: var(--Colors-Use-Yellow-Border); background-color: var(--Colors-Use-Yellow-Bg); svg { - color: var(--Colors-Use-Yellow-Primary); + color: var(--yakit-colors-Yellow-80); } } @@ -193,10 +193,13 @@ color: var(--Colors-Use-Neutral-Text-1-Title); } -.yakit-tag-main { - background: var(--Colors-Use-Main-Bg); - border-color: var(--Colors-Use-Main-Border); - color: var(--Colors-Use-Neutral-Text-1-Title); +.yakit-tag-lakeBlue { + background: var(--Colors-Use-Lake-blue-Bg-Hover); + border-color: var(--Colors-Use-Lake-blue-Border); + color: var(--yakit-colors-Lake-blue-80); + svg { + color: var(--yakit-colors-Lake-blue-80); + } } .yakit-tag-disable { diff --git a/src/compoments/yakitUI/YakitTag/YakitTag.tsx b/src/compoments/yakitUI/YakitTag/YakitTag.tsx index 6a7b8bed..63ff7c41 100644 --- a/src/compoments/yakitUI/YakitTag/YakitTag.tsx +++ b/src/compoments/yakitUI/YakitTag/YakitTag.tsx @@ -1,14 +1,14 @@ -import { Tag } from 'antd'; -import React, { useMemo, useState } from 'react'; -import type { CopyComponentsProps, YakitTagProps } from './YakitTagType'; -import styles from './YakitTag.module.scss'; -import classNames from 'classnames'; -import { DocumentDuplicateSvgIcon } from '@/assets/newIcon'; -import { useMemoizedFn } from 'ahooks'; -import { CheckOutlined, LoadingOutlined } from '@ant-design/icons'; -import { success } from '@/utils/notification'; -import { OutlineXIcon } from '@/assets/icon/outline'; -import { match } from 'ts-pattern'; +import { Tag } from 'antd' +import React, { useState } from 'react' +import type { CopyComponentsProps, YakitTagProps } from './YakitTagType' +import styles from './YakitTag.module.scss' +import classNames from 'classnames' +import { DocumentDuplicateSvgIcon } from '@/assets/newIcon' +import { useMemoizedFn } from 'ahooks' +import { CheckOutlined, LoadingOutlined } from '@ant-design/icons' +import { success } from '@/utils/notification' +import { OutlineXIcon } from '@/assets/icon/outline' +import { setClipboardText } from '@/utils/clipboard' /** * 更新说明 @@ -26,108 +26,96 @@ import { match } from 'ts-pattern'; * @param {e} onAfterCopy 复制后的回调 */ export const YakitTag: React.FC = (props) => { - const { - color, - size, - disable, + const { color, size, disable, className, enableCopy, iconColor, copyText, border, fullRadius, ...restProps } = props + const onAfterCopy = useMemoizedFn((e) => { + if (props.onAfterCopy) props.onAfterCopy(e) + }) + return ( + ) || + props.closeIcon || + } + closable={!!(props.closable || enableCopy)} + className={classNames( + styles['yakit-tag-middle'], + { + [styles['yakit-tag-small']]: size === 'small', + [styles['yakit-tag-large']]: size === 'large', + [styles['yakit-tag-default-color']]: !color, + [styles['yakit-tag-danger']]: color === 'danger', + [styles['yakit-tag-info']]: color === 'info', + [styles['yakit-tag-success']]: color === 'success' || color === 'green', + [styles['yakit-tag-warning']]: color === 'warning', + [styles['yakit-tag-serious']]: color === 'serious' || color === 'red', + [styles['yakit-tag-yellow']]: color === 'yellow', + [styles['yakit-tag-purple']]: color === 'purple', + [styles['yakit-tag-blue']]: color === 'blue', + [styles['yakit-tag-cyan']]: color === 'cyan', + [styles['yakit-tag-bluePurple']]: color === 'bluePurple', + [styles['yakit-tag-white']]: color === 'white', + [styles['yakit-tag-lakeBlue']]: color === 'lakeBlue', + [styles['yakit-tag-border']]: border !== false, + [styles['yakit-tag-fullRadius']]: !!fullRadius, + }, className, - enableCopy, - iconColor, - copyText, - ...restProps - } = props; - const onAfterCopy = useMemoizedFn((e) => { - if (props.onAfterCopy) props.onAfterCopy(e); - }); - return ( - - )) || - props.closeIcon || - } - closable={props.closable || enableCopy} - className={classNames( - styles['yakit-tag-middle'], - { - [styles['yakit-tag-small']]: size === 'small', - [styles['yakit-tag-large']]: size === 'large', - [styles['yakit-tag-danger']]: color === 'danger', - [styles['yakit-tag-info']]: color === 'info', - [styles['yakit-tag-success']]: color === 'success', - [styles['yakit-tag-warning']]: color === 'warning', - [styles['yakit-tag-serious']]: color === 'serious', - [styles['yakit-tag-yellow']]: color === 'yellow', - [styles['yakit-tag-purple']]: color === 'purple', - [styles['yakit-tag-blue']]: color === 'blue', - [styles['yakit-tag-cyan']]: color === 'cyan', - [styles['yakit-tag-bluePurple']]: color === 'bluePurple', - [styles['yakit-tag-white']]: color === 'white', - [styles['yakit-tag-disable']]: !!disable, - }, - className, - )} - onClose={(e) => { - if (disable || enableCopy) return; - if (props.onClose) props.onClose(e); - }} - > - {(enableCopy && copyText) || props.children} - - ); -}; + )} + onClose={(e) => { + if (disable || enableCopy) return + if (props.onClose) props.onClose(e) + }} + > + {enableCopy && copyText ? ( + + {copyText} + + ) : ( + props.children + )} + + ) +} export const CopyComponents: React.FC = (props) => { - const { className, iconColor } = props; - const [loading, setLoading] = useState(false); - const [isShowSure, setIsShowSure] = useState(false); - const onCopy = useMemoizedFn((e) => { - e.stopPropagation(); - if (!props.copyText) return; - setLoading(true); - // ipcRenderer.invoke("set-copy-clipboard", props.copyText) + const { className, iconColor } = props + const [loading, setLoading] = useState(false) + const [isShowSure, setIsShowSure] = useState(false) + const onCopy = useMemoizedFn((e) => { + e.stopPropagation() + if (!props.copyText) return + setLoading(true) + setClipboardText(props.copyText, { + hiddenHint: true, + finalCallback: () => { setTimeout(() => { - setLoading(false); - setIsShowSure(true); - setTimeout(() => { - setIsShowSure(false); - }, 2000); - success('复制成功'); - }, 1000); - if (props.onAfterCopy) props.onAfterCopy(e); - }); - - const targetRender = useMemo(() => { - return match({ loading, isShowSure }) - .with({ loading: true }, () => ( - - )) - .with({ loading: false, isShowSure: true }, () => ( - - )) - .with({ loading: false, isShowSure: false }, () => ( - - )) - .exhaustive(); - }, [loading, isShowSure, iconColor]); - return ( -
    - {targetRender} -
    - ); -}; + setLoading(false) + setIsShowSure(true) + setTimeout(() => { + setIsShowSure(false) + }, 2000) + success('复制成功') + }, 1000) + }, + }) + if (props.onAfterCopy) props.onAfterCopy(e) + }) + return ( +
    + {(loading && ) || + (isShowSure && ( + + )) || ( + + )} +
    + ) +} diff --git a/src/compoments/yakitUI/YakitTag/YakitTagType.d.ts b/src/compoments/yakitUI/YakitTag/YakitTagType.d.ts index eb30ab7b..ea9e62cb 100644 --- a/src/compoments/yakitUI/YakitTag/YakitTagType.d.ts +++ b/src/compoments/yakitUI/YakitTag/YakitTagType.d.ts @@ -18,14 +18,17 @@ export type YakitTagColor = | 'danger' | 'info' | 'success' + | 'green' | 'warning' | 'serious' + | 'red' | 'yellow' | 'purple' | 'blue' | 'cyan' | 'bluePurple' - | 'white'; + | 'white' + | 'lakeBlue'; export interface YakitTagProps extends Omit { size?: YakitSizeType; @@ -35,6 +38,10 @@ export interface YakitTagProps extends Omit { onAfterCopy?: (e: MouseEvent) => void; copyText?: string; iconColor?: string; + /** 是否全圆角 */ + fullRadius?: boolean; + /** 是否有边框 */ + border?: boolean; } /** * @description: 复制文字 diff --git a/src/compoments/yakitUI/compare/AutoCard.css b/src/compoments/yakitUI/compare/AutoCard.css new file mode 100644 index 00000000..e69de29b diff --git a/src/compoments/yakitUI/compare/AutoCard.tsx b/src/compoments/yakitUI/compare/AutoCard.tsx new file mode 100644 index 00000000..7bd4bf79 --- /dev/null +++ b/src/compoments/yakitUI/compare/AutoCard.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import type { CardProps } from 'antd' +import { Card } from 'antd' + +import './AutoCard.css' + +export interface AutoCardProps extends CardProps { + style?: React.CSSProperties + children?: React.ReactNode +} + +export const AutoCard: React.FC = (props) => { + const { style, children, bodyStyle, ...rest } = props + + return ( + + {children} + + ) +} diff --git a/src/compoments/yakitUI/compare/DataCompare.module.scss b/src/compoments/yakitUI/compare/DataCompare.module.scss new file mode 100644 index 00000000..1c051cda --- /dev/null +++ b/src/compoments/yakitUI/compare/DataCompare.module.scss @@ -0,0 +1,61 @@ +.data-compare-modal { + height: 716px; + display: flex; + flex-direction: column; + .header { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 16px 24px 8px 24px; + .title { + color: #31343f; + font-family: PingFang HK; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 20px; + } + .close { + svg { + width: 20px; + height: 20px; + cursor: pointer; + color: #85899e; + } + &:hover { + svg { + color: var(--yakit-primary-4); + } + } + } + } + + .content { + flex: 1; + padding: 0px 24px 24px; + display: flex; + flex-direction: column; + .content-title { + display: flex; + justify-content: space-between; + border-radius: 4px 4px 0px 0px; + border: 1px solid var(--yakit-border-color); + padding: 6px 12px; + + color: var(--yakit-header-color); + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 16px; + } + .code { + flex: 1; + } + } + .footer { + padding: 24px; + display: flex; + justify-content: flex-end; + gap: 8px; + } +} diff --git a/src/compoments/yakitUI/compare/DataCompare.tsx b/src/compoments/yakitUI/compare/DataCompare.tsx new file mode 100644 index 00000000..baed13d0 --- /dev/null +++ b/src/compoments/yakitUI/compare/DataCompare.tsx @@ -0,0 +1,203 @@ +import React, { useEffect, useState, useRef, useImperativeHandle } from 'react' +import { Button, Space } from 'antd' +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api' +import styles from './DataCompare.module.scss' +import { LineConversionIcon, RemoveIcon } from '@/assets/newIcon' +import { AutoCard } from './AutoCard' + +interface textModelProps { + content: string + language: string +} + +interface DataCompareProps { + leftData?: string + rightData?: string +} +export const DataCompare: React.FC = (props) => { + const { leftData, rightData } = props + const [noWrap, setNoWrap] = useState(false) + + const [left, setLeft] = useState(leftData || '') + const [right, setRight] = useState(rightData || '') + + const codeComparisonRef = useRef(null) + return ( + + + + ) + } + return ( +
    + {isContinue && renderFooterRightExtra()} + {type === 'require_user_interactive' && ( + + 提交 + + )} +
    + ) + }, [ + isContinue, + reviewTreeOption, + forgeOption, + type, + aiOptionsLength, + isRequireQS, + requireLoading, + noAIOptionsList.showButton, + ]) + + const reviewHeardExtra = useCreation(() => { + // eslint-disable-next-line react/jsx-no-useless-fragment + let node: ReactNode = <> + switch (type) { + case 'tool_use_review_require': + case 'exec_aiforge_review_require': + /** NOTE 定义问题 */ + // eslint-disable-next-line no-case-declarations + const toolReviewData = review as AIAgentGrpcApi.ToolUseReviewRequire + if (toolReviewData.aiReview) { + const { interactive_id, score, level } = toolReviewData.aiReview + node = ( + <> + {!!interactive_id && !score && !countdown &&
    评估中...
    } + {!!score && ( +
    + AI  风险评分   + + {toolReviewData.aiReview.score || 0} + +
    + )} + + ) + } + + break + + default: + break + } + return node + }, [type, review, countdown]) + + return ( + <> +
    +
    +
    +
    + + {reviewTitle.title} +
    +
    {reviewTitle.subTitle}
    +
    +
    {reviewHeardExtra}
    +
    +
    +
    + {planReview} + {taskReview} + {toolReview} + {forgeReview} + {aiRequireReview} +
    + {!reviewTreeOption && (aiOptions || noAIOptionsAllowShowInput) ? ( +
    + {aiOptions} + {!!noAIOptionsAllowShowInput && + (editShow ? ( +
    + setReviewQS(e.target.value)} + /> + +
    +
    + } + onClick={() => handleCallbackEdit(false)} + radius={true} + /> + } + onClick={() => handleCallbackEdit(true)} + radius={true} + /> +
    +
    +
    + ) : ( + noAIOptionsAllowShowInput + ))} +
    + ) : null} +
    + {isEmbedded &&
    {footerNode}
    } +
    + {renderFooterExtra && renderFooterExtra(footerNode)} + + ) +}) + +const handleFetchParams = (jsonValue: string) => { + try { + const parseValue = JSON.parse(jsonValue) + if (Array.isArray(parseValue)) { + return parseValue as YakParamProps[] + } else { + return handleFetchParams(parseValue as string) + } + } catch (error) { + return [] + } +} +const ForgeReviewForm: React.FC = React.memo( + forwardRef((props, ref) => { + const { forge_name, forge_verbose_name, forge_desc, forge_params, editable } = props + const [loading, setLoading] = useState(false) + const [forge, setForge] = useState() + const [form] = Form.useForm() + + useImperativeHandle( + ref, + () => ({ + validateFields: form.validateFields, + }), + [form], + ) + + useEffect(() => { + getGrpcGetAIForge() + }, [forge_name]) + const getGrpcGetAIForge = useMemoizedFn(() => { + setLoading(true) + grpcGetAIForge({ ForgeName: forge_name }) + .then(setForge) + .finally(() => + // eslint-disable-next-line max-nested-callbacks + setTimeout(() => { + setLoading(false) + }, 200), + ) + }) + const params: YakParamProps[] = useCreation(() => { + if (!forge) return [] + const { ParamsUIConfig } = forge + + if (!ParamsUIConfig) { + return [ + { + Field: 'query', + FieldVerbose: 'query', + TypeVerbose: 'text', + DefaultValue: forge_params['query'] || '', + Help: '', + }, + ] + } + try { + const param: YakParamProps[] = handleFetchParams(ParamsUIConfig) + return param + } catch (error) { + return [] + } + }, [forge, forge_params]) + useEffect(() => { + if (!params) return + initRequiredFormValue() + }, [params]) + const initRequiredFormValue = useMemoizedFn(() => { + if (!params) return + let initRequiredFormValue: CustomPluginExecuteFormValue = {} + params.forEach((ele) => { + const value = getValueByType(forge_params[ele.Field], ele.TypeVerbose) + initRequiredFormValue = { + ...initRequiredFormValue, + [ele.Field]: value, + } + }) + if (!form) return + form.resetFields() + form.setFieldsValue({ ...initRequiredFormValue }) + }) + return ( + +
    +
    +
    + {forge?.ForgeVerboseName || forge_verbose_name || forge?.ForgeName || forge_name} +
    +
    描述:{forge?.Description || forge_desc}
    +
    +
    + {params?.length > 1 && ( +
    + + 参数组 +
    + )} +
    1, + })} + > +
    + {/* {params && ( + + )} */} + +
    +
    +
    +
    + ) + }), +) diff --git a/src/pages/AIAgent/ai-agent/components/aiReActChatReview/AIReActChatReviewType.d.ts b/src/pages/AIAgent/ai-agent/components/aiReActChatReview/AIReActChatReviewType.d.ts new file mode 100644 index 00000000..75b3b8aa --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiReActChatReview/AIReActChatReviewType.d.ts @@ -0,0 +1,25 @@ +import type { AIChatIPCSendParams } from '@/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent'; +import type { AIChatQSData } from '../../../ai-re-act/hooks/aiRender'; +import type { AIAgentGrpcApi } from '../../../ai-re-act/hooks/grpcApi'; + +export interface AIReActChatReviewProps { + info: AIChatQSData; + onSendAI: (params: AIChatIPCSendParams) => void; + planReviewTreeKeywordsMap?: Map< + string, + AIAgentGrpcApi.PlanReviewRequireExtra + >; + isEmbedded?: boolean; + renderFooterExtra?: (node: React.ReactNode) => React.ReactNode; + expand: boolean; + className?: string; +} + +export interface ForgeReviewFormRefProps { + validateFields: Promise; +} +export interface ForgeReviewFormProps extends AIAgentGrpcApi.ExecForgeReview { + ref: React.ForwardedRef; + /** 是否可编辑 */ + editable: boolean; +} diff --git a/src/pages/AIAgent/ai-agent/components/aiReviewResult/AIReviewResult.module.scss b/src/pages/AIAgent/ai-agent/components/aiReviewResult/AIReviewResult.module.scss new file mode 100644 index 00000000..3399530f --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiReviewResult/AIReviewResult.module.scss @@ -0,0 +1,48 @@ +@use "../../../../../styles/mixin.scss" as mixin; + +.title-wrapper { + display: flex; + align-items: center; + gap: 4px 8px; + overflow: hidden; + flex-wrap: wrap; + .title-main { + display: flex; + align-items: center; + gap: 4px; + overflow: hidden; + } + svg { + width: 16px; + height: 16px; + color: var(--Colors-Use-Neutral-Text-2-Primary); + } + + .title { + flex-shrink: 0; + margin-right: 4px; + } + .mpb-color-text { + @include mixin.mpb-color-text(); + @include mixin.text-ellipsis(1, false, false, false); + font-size: 12px; + font-weight: 500; + line-height: 16px; + } +} +.title-extra { + color: var(--Colors-Use-Neutral-Text-4-Help-text); + + font-size: 11px; + font-weight: 400; + line-height: 14px; + + min-width: 20px; + @include mixin.text-ellipsis(1, false, false, false); +} +.time { + color: var(--Colors-Use-Neutral-Text-4-Help-text); + + font-size: 11px; + line-height: 14px; +} diff --git a/src/pages/AIAgent/ai-agent/components/aiReviewResult/AIReviewResult.tsx b/src/pages/AIAgent/ai-agent/components/aiReviewResult/AIReviewResult.tsx new file mode 100644 index 00000000..f572a8f0 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiReviewResult/AIReviewResult.tsx @@ -0,0 +1,144 @@ +import { memo } from 'react'; +import type { AIReviewResultProps, AISingHaveColorTextProps } from './type'; +import { useCreation, useMemoizedFn } from 'ahooks'; +import styles from './AIReviewResult.module.scss'; +import React from 'react'; +import ChatCard from '../ChatCard'; +import ModalInfo from '../ModelInfo'; +import { PreWrapper } from '../ToolInvokerCard'; + +export const AIReviewResult: React.FC = memo((props) => { + const { info, timestamp } = props; + const { type, data } = info; + const title = useCreation(() => { + switch (type) { + case 'plan_review_require': + return '计划审阅'; + case 'task_review_require': + return '任务审阅'; + case 'tool_use_review_require': + return '工具审阅'; + case 'exec_aiforge_review_require': + return '智能应用审阅'; + case 'require_user_interactive': + return '主动询问'; + default: + return 'Review 决策'; + } + }, [type]); + const userAction = useCreation(() => { + let btnText = ''; + let userInput = ''; + try { + switch (type) { + case 'plan_review_require': + case 'task_review_require': + case 'tool_use_review_require': + case 'exec_aiforge_review_require': + // eslint-disable-next-line no-case-declarations + const userSelected = JSON.parse(data.selected || ''); + if (data.optionValue === 'continue') { + btnText = '立即执行'; + } else { + const selectBtn = data.selectors.find( + (item: any) => item.value === data.optionValue, + ); + btnText = selectBtn ? selectBtn.prompt : '未知操作'; + } + userInput = userSelected.extra_prompt || ''; + break; + case 'require_user_interactive': + // eslint-disable-next-line no-case-declarations + const aiSelected = JSON.parse(data.selected || ''); + // eslint-disable-next-line no-case-declarations + const aiSelectType = data.options.find( + (item: any) => + (item.prompt || item.prompt_title) === + data.optionValue, + ); + btnText = + aiSelectType?.prompt || + aiSelectType?.prompt_title || + '未知操作'; + userInput = aiSelected.suggestion || ''; + break; + default: + break; + } + } catch (error) {} + + return { + btnText, + userInput, + }; + }, [type, data]); + const renderContent = useMemoizedFn(() => { + let paramsValue = userAction.userInput; + switch (type) { + case 'tool_use_review_require': + // eslint-disable-next-line no-case-declarations + const { params } = data; + try { + paramsValue = paramsValue + ? paramsValue + : JSON.stringify(params, null, 2); + } catch (error) {} + break; + default: + break; + } + return paramsValue ? : null; + }); + return ( + } + title={title} + subTitle={userAction.btnText} + tip="" + modalInfo={{ + title: info.AIModelName, + time: timestamp, + icon: info.AIService, + }} + > + {renderContent()} + + ); +}); + +export const AISingHaveColorText: React.FC = + React.memo((props) => { + const { + title, + subTitle, + tip, + titleIcon, + modalInfo, + children, + ...reset + } = props; + return ( + } + titleText={ +
    +
    + {titleIcon} + {title} + {subTitle && ( +
    + {subTitle} +
    + )} +
    + {tip && ( +
    {tip}
    + )} +
    + } + {...reset} + > + {children} +
    + ); + }); diff --git a/src/pages/AIAgent/ai-agent/components/aiReviewResult/type.ts b/src/pages/AIAgent/ai-agent/components/aiReviewResult/type.ts new file mode 100644 index 00000000..d4ed3214 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiReviewResult/type.ts @@ -0,0 +1,16 @@ +import type { AIChatQSData } from '@/pages/AIAgent/ai-re-act/hooks/aiRender'; +import type { ReactNode } from 'react'; +import type { ChatCardProps } from '../ChatCard'; +import type { ModalInfoProps } from '../ModelInfo'; + +export interface AIReviewResultProps { + info: AIChatQSData; + timestamp: number; +} +export interface AISingHaveColorTextProps extends ChatCardProps { + title: ReactNode; + subTitle: ReactNode; + tip: ReactNode; + modalInfo?: ModalInfoProps; + children?: ReactNode; +} diff --git a/src/pages/AIAgent/ai-agent/components/aiStreamChatContent/AIStreamChatContent.module.scss b/src/pages/AIAgent/ai-agent/components/aiStreamChatContent/AIStreamChatContent.module.scss new file mode 100644 index 00000000..0152512f --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiStreamChatContent/AIStreamChatContent.module.scss @@ -0,0 +1,60 @@ +@use "../../../../../styles/mixin.scss" as mixin; +.ai-stream-chat-content-wrapper { + border-radius: 8px; + // background: var(--Colors-Use-Neutral-Bg-Hover); +} +.ai-stream-chat-content { + display: flex; + align-items: center; + justify-content: flex-start; + max-width: 100%; + gap: 4px; + border-left: 2px solid var(--Colors-Use-Neutral-Disable); + padding-left: 8px; + .title { + padding: 4px 0px; + border-radius: 8px 0 0 8px; + display: flex; + align-items: center; + flex-shrink: 0; + gap: 4px; + color: var(--Colors-Use-Neutral-Text-3-Secondary); + font-size: 11px; + font-weight: 500; + line-height: 14px; + // @include mixin.mpb-color-text(); + } + .ai-stream-content { + padding: 4px 12px 4px 0; + border-radius: 0 8px 8px 0; + color: var(--Colors-Use-Neutral-Text-4-Help-text); + font-size: 11px; + font-weight: 400; + line-height: 14px; + white-space: nowrap; + overflow: hidden; + display: flex; + justify-content: flex-end; + position: relative; + .ai-mask { + width: 100%; + height: 100%; + position: absolute; + top: 0; + right: 0; + pointer-events: none; + background: linear-gradient( + to right, + var(--Colors-Use-Basic-Background) 0%, + rgba(255, 255, 255, 0) 20%, + rgba(255, 255, 255, 0) 80%, + transparent 100% + ); + } + } +} + +.tooltip-stream-content { + max-height: 40vh; + overflow: auto; +} diff --git a/src/pages/AIAgent/ai-agent/components/aiStreamChatContent/AIStreamChatContent.tsx b/src/pages/AIAgent/ai-agent/components/aiStreamChatContent/AIStreamChatContent.tsx new file mode 100644 index 00000000..c2b4eca5 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiStreamChatContent/AIStreamChatContent.tsx @@ -0,0 +1,42 @@ +import React, { useMemo } from 'react' +import type { AIStreamChatContentProps } from './type' +import { Tooltip } from 'antd' +import { CopyComponents } from '@/compoments/yakitUI/YakitTag/YakitTag' +import styles from './AIStreamChatContent.module.scss' +import classNames from 'classnames' +import useAINodeLabel from '@/pages/AIAgent/ai-re-act/hooks/useAINodeLabel' + +const STREAM_MASK_THRESHOLD = 100 + +export const AIStreamChatContent: React.FC = React.memo((props) => { + const { content, nodeIdVerbose, referenceNode } = props + const { nodeLabel } = useAINodeLabel(nodeIdVerbose) + const shouldShowMask = useMemo(() => content.length > STREAM_MASK_THRESHOLD, [content]) + + return ( +
    +
    +
    + {/* */} + {nodeLabel} +
    +
    + + {content} + +
    + } + trigger="click" + > + {shouldShowMask &&
    } + {content} + + {referenceNode} +
    +
    +
    + ) +}) diff --git a/src/pages/AIAgent/ai-agent/components/aiStreamChatContent/type.ts b/src/pages/AIAgent/ai-agent/components/aiStreamChatContent/type.ts new file mode 100644 index 00000000..959c3a3f --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiStreamChatContent/type.ts @@ -0,0 +1,8 @@ +import type { AIOutputEvent } from '@/pages/AIAgent/ai-re-act/hooks/grpcApi'; +import type { ReactNode } from 'react'; + +export interface AIStreamChatContentProps { + content: string; + nodeIdVerbose: AIOutputEvent['NodeIdVerbose']; + referenceNode?: ReactNode; +} diff --git a/src/pages/AIAgent/ai-agent/components/aiTaskQuery/AITaskQuery.module.scss b/src/pages/AIAgent/ai-agent/components/aiTaskQuery/AITaskQuery.module.scss new file mode 100644 index 00000000..98cd65b5 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiTaskQuery/AITaskQuery.module.scss @@ -0,0 +1,99 @@ +%display-flex-center { + display: flex; + align-items: center; +} +%display-column-center { + display: flex; + flex-direction: column; +} +.ai-task-query { + min-height: 24px; + max-height: 300px; + margin: 4px 0; + position: relative; + .list-todo-icon { + svg { + width: 16px; + height: 16px; + color: var(--Colors-Use-Neutral-Text-2-Primary); + } + } + .question-mark-circle { + cursor: pointer; + svg { + width: 16px; + height: 16px; + color: var(--Colors-Use-Neutral-Text-4-Help-text); + } + } + .ai-task-query-list-wrapper { + @extend %display-column-center; + gap: 8px; + padding: 4px 4px 8px 4px; + border-radius: 8px; + border: 1px solid var(--Colors-Use-Neutral-Border); + background: var(--Colors-Use-Basic-Background); + .ai-task-query-list-header { + @extend %display-flex-center; + justify-content: space-between; + gap: 4px; + padding: 2px 8px; + .task-query-title { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.5px; + } + .header-left { + @extend %display-flex-center; + gap: 4px; + span { + margin-right: 0; + } + } + .header-right { + @extend %display-flex-center; + gap: 4px; + } + } + .task-query-list { + @extend %display-column-center; + gap: 8px; + max-height: 15vh; + overflow-y: auto; + .task-query-list-item { + @extend %display-flex-center; + justify-content: space-between; + height: 24px; + padding: 2px 8px; + gap: 8px; + border-radius: 4px; + &:hover { + background-color: var(--Colors-Use-Neutral-Bg-Hover); + } + .chat-icon, + .info-icon { + svg { + width: 16px; + height: 16px; + color: var(--Colors-Use-Neutral-Text-2-Primary); + } + } + .item-left { + @extend %display-flex-center; + gap: 4px; + overflow: hidden; + } + + .item-right { + @extend %display-flex-center; + gap: 4px; + } + } + } + .clear-btn { + padding: 3px 0; + } + } +} diff --git a/src/pages/AIAgent/ai-agent/components/aiTaskQuery/AITaskQuery.tsx b/src/pages/AIAgent/ai-agent/components/aiTaskQuery/AITaskQuery.tsx new file mode 100644 index 00000000..23bd4eda --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiTaskQuery/AITaskQuery.tsx @@ -0,0 +1,153 @@ +import React, { useMemo, useState } from 'react' +import type { AITaskQueryItemProps, AITaskQueryProps } from './type' +import { + OutlineArrowupIcon, + OutlineChatIcon, + OutlineInformationcircleIcon, + OutlineListTodoIcon, + OutlineTrashIcon, + OutlineXIcon, +} from '@/assets/icon/outline' +import styles from './AITaskQuery.module.scss' +import useChatIPCStore from '../../useContext/ChatIPCContent/useStore' +import useChatIPCDispatcher from '../../useContext/ChatIPCContent/useDispatcher' +import { useCreation, useDebounceFn, useMemoizedFn } from 'ahooks' +import { AIInputEventSyncTypeEnum } from '@/pages/AIAgent/ai-re-act/hooks/grpcApi' +import { Tooltip } from 'antd' +import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton' +import { YakitTag } from '@/compoments/yakitUI/YakitTag/YakitTag' + +export const AITaskQuery: React.FC = React.memo(() => { + const { chatIPCData } = useChatIPCStore() + const { handleSendSyncMessage } = useChatIPCDispatcher() + + const [loading, setLoading] = useState(false) + + const questionQueue = useMemo(() => { + return chatIPCData.questionQueue + }, [chatIPCData.questionQueue]) + + const [showList, setShowList] = useState(true) + + const onClearTaskQueue = useMemoizedFn(() => { + if (!chatIPCData.execute) return + setLoading(true) + handleSendSyncMessage({ + syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CLEAR_TASK, + params: {}, + }) + handleSendSyncMessage({ + syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO, + params: {}, + }) + setTimeout(() => { + setLoading(false) + setShowList(false) + }, 500) + }) + return ( +
    + {showList ? ( +
    +
    +
    + +
    任务队列
    + + {questionQueue.total} + + {/* */} +
    +
    + + 清空 + + } onClick={() => setShowList(false)} /> +
    +
    +
    + {questionQueue.data.map((item: any) => { + return + })} +
    +
    + ) : ( + } radius={9999} onClick={() => setShowList(true)}> + 任务队列 + + )} +
    + ) +}) + +const AITaskQueryItem: React.FC = React.memo((props) => { + const { item } = props + const { chatIPCData } = useChatIPCStore() + const [upLoading, setUpLoading] = useState(false) + const [removeLoading, setRemoveLoading] = useState(false) + const execute = useCreation(() => chatIPCData.execute, [chatIPCData.execute]) + const { handleSendSyncMessage } = useChatIPCDispatcher() + const onTaskUp = useDebounceFn( + () => { + if (!execute || upLoading) return + setUpLoading(true) + handleSendSyncMessage({ + syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_JUMP_QUEUE, + SyncJsonInput: JSON.stringify({ task_id: item.id }), + params: {}, + }) + handleSendSyncMessage({ + syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO, + params: {}, + }) + setTimeout(() => { + setUpLoading(false) + }, 500) + }, + { wait: 500, leading: true }, + ).run + const onTaskRemove = useDebounceFn( + () => { + if (!execute || removeLoading) return + setRemoveLoading(true) + handleSendSyncMessage({ + syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_REMOVE_TASK, + SyncJsonInput: JSON.stringify({ task_id: item.id }), + params: {}, + }) + handleSendSyncMessage({ + syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO, + params: {}, + }) + setTimeout(() => { + setRemoveLoading(false) + }, 500) + }, + { wait: 500, leading: true }, + ).run + return ( +
    +
    + + + {item.user_input} + +
    +
    + {item.focus_mode && ( + + + + )} + } onClick={onTaskUp} loading={upLoading} /> + } onClick={onTaskRemove} loading={removeLoading} /> +
    +
    + ) +}) diff --git a/src/pages/AIAgent/ai-agent/components/aiTaskQuery/type.ts b/src/pages/AIAgent/ai-agent/components/aiTaskQuery/type.ts new file mode 100644 index 00000000..f7843483 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiTaskQuery/type.ts @@ -0,0 +1,8 @@ +import type { AIAgentGrpcApi } from '@/pages/AIAgent/ai-re-act/hooks/grpcApi'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface AITaskQueryProps {} + +export interface AITaskQueryItemProps { + item: AIAgentGrpcApi.QuestionQueueItem; +} diff --git a/src/pages/AIAgent/ai-agent/components/aiToolDecision/AIToolDecision.module.scss b/src/pages/AIAgent/ai-agent/components/aiToolDecision/AIToolDecision.module.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/pages/AIAgent/ai-agent/components/aiToolDecision/AIToolDecision.tsx b/src/pages/AIAgent/ai-agent/components/aiToolDecision/AIToolDecision.tsx new file mode 100644 index 00000000..d204a8d9 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiToolDecision/AIToolDecision.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import type { AIToolDecisionProps } from './type'; +import { AISingHaveColorText } from '../aiReviewResult/AIReviewResult'; +import { SolidCursorclickIcon } from '@/assets/icon/solid'; +import useAINodeLabel from '@/pages/AIAgent/ai-re-act/hooks/useAINodeLabel'; + +export const AIToolDecision: React.FC = React.memo( + (props) => { + const { item } = props; + const { nodeLabel } = useAINodeLabel(item.data.i18n); + return ( + } + title="决策" + subTitle={nodeLabel} + tip={item.data?.summary} + modalInfo={{ + title: item.AIModelName, + time: item.Timestamp, + icon: item.AIService, + }} + /> + ); + }, +); diff --git a/src/pages/AIAgent/ai-agent/components/aiToolDecision/type.ts b/src/pages/AIAgent/ai-agent/components/aiToolDecision/type.ts new file mode 100644 index 00000000..f6337e82 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiToolDecision/type.ts @@ -0,0 +1,5 @@ +import type { ChatToolCallDecision } from '@/pages/AIAgent/ai-re-act/hooks/aiRender'; + +export interface AIToolDecisionProps { + item: ChatToolCallDecision; +} diff --git a/src/pages/AIAgent/ai-agent/components/aiTriageChat/AITriageChat.module.scss b/src/pages/AIAgent/ai-agent/components/aiTriageChat/AITriageChat.module.scss new file mode 100644 index 00000000..1d7fca55 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiTriageChat/AITriageChat.module.scss @@ -0,0 +1,31 @@ +// #region AITriageChatContent +.triage-chat-content { + display: flex; + .content-wrapper { + max-width: 80%; + padding: 4px 8px; + border-radius: 8px; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.5px; + word-break: break-all; + white-space: pre-wrap; + .loading-wrapper { + margin-top: 4px; + } + } +} +.triage-chat-question { + justify-content: flex-end; + .content-wrapper { + background: var(--Colors-Use-Main-Focus); + } +} +.triage-chat-answer { + .content-wrapper { + background: var(--Colors-Use-Neutral-Bg-Hover); + } +} +// #endregion diff --git a/src/pages/AIAgent/ai-agent/components/aiTriageChat/AITriageChat.tsx b/src/pages/AIAgent/ai-agent/components/aiTriageChat/AITriageChat.tsx new file mode 100644 index 00000000..c528c99d --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiTriageChat/AITriageChat.tsx @@ -0,0 +1,61 @@ +import React, { memo } from 'react'; +import type { AITriageChatContentProps } from './type'; + +import classNames from 'classnames'; +import styles from './AITriageChat.module.scss'; +import { PreWrapper } from '../ToolInvokerCard'; +import { useMemoizedFn } from 'ahooks'; +import { AIMilkdownInput } from '../aiMilkdownInput/AIMilkdownInput'; + +export const AITriageChatContent: React.FC = memo( + (props) => { + const { + isAnswer, + content, + contentClassName, + chatClassName, + extraValue, + } = props; + + const renderContent = useMemoizedFn(() => { + if (extraValue?.isForge) { + return ( + <> + {extraValue?.showForgeQuestion} + + + ); + } + if (extraValue?.showQS) { + return ( + + ); + } + return content; + }); + return ( +
    +
    + {renderContent()} +
    +
    + ); + }, +); diff --git a/src/pages/AIAgent/ai-agent/components/aiTriageChat/type.ts b/src/pages/AIAgent/ai-agent/components/aiTriageChat/type.ts new file mode 100644 index 00000000..791050ed --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiTriageChat/type.ts @@ -0,0 +1,10 @@ +import type { AIChatIPCStartParams } from '@/pages/AIAgent/ai-re-act/hooks/type'; +import type { ReactNode } from 'react'; + +export interface AITriageChatContentProps { + isAnswer?: boolean; + content: ReactNode; + contentClassName?: string; + chatClassName?: string; + extraValue?: AIChatIPCStartParams['extraValue']; +} diff --git a/src/pages/AIAgent/ai-agent/components/aiYaklangCode/AIYaklangCode.module.scss b/src/pages/AIAgent/ai-agent/components/aiYaklangCode/AIYaklangCode.module.scss new file mode 100644 index 00000000..22283327 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiYaklangCode/AIYaklangCode.module.scss @@ -0,0 +1,6 @@ +.ai-yaklang-code { + height: 200px; + background: var(--Colors-Use-Neutral-Bg); + padding: 8px; + border-radius: 4px; +} diff --git a/src/pages/AIAgent/ai-agent/components/aiYaklangCode/AIYaklangCode.tsx b/src/pages/AIAgent/ai-agent/components/aiYaklangCode/AIYaklangCode.tsx new file mode 100644 index 00000000..82ef29b6 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiYaklangCode/AIYaklangCode.tsx @@ -0,0 +1,42 @@ +import React, { useState } from 'react' +import type { AIYaklangCodeProps } from './type' +import ChatCard from '../ChatCard' +import { OutlinCompileTwoIcon } from '@/assets/icon/outline' +import ModalInfo from '../ModelInfo' +import styles from './AIYaklangCode.module.scss' +import { useMemoizedFn, useThrottleEffect } from 'ahooks' +import { AIStreamContentType } from '@/pages/AIAgent/ai-re-act/hooks/defaultConstant' +import { YakitEditor } from '@/compoments/yakitUI/YakitEditor/YakitEditor' +import { NewHTTPPacketEditor } from '@/compoments/yakitUI/YakitEditor/editors' + +export const AIYaklangCode: React.FC = React.memo((props) => { + const { content: defContent, nodeLabel, modalInfo, contentType, referenceNode } = props + const [content, setContent] = useState(defContent) + useThrottleEffect( + () => { + setContent(defContent) + }, + [defContent], + { wait: 500 }, + ) + const renderCode = useMemoizedFn(() => { + switch (contentType) { + case AIStreamContentType.CODE_YAKLANG: + return + case AIStreamContentType.CODE_HTTP_REQUEST: + return + default: + return null + } + }) + return ( + } + titleExtra={modalInfo && } + > +
    {renderCode()}
    + {referenceNode} +
    + ) +}) diff --git a/src/pages/AIAgent/ai-agent/components/aiYaklangCode/type.ts b/src/pages/AIAgent/ai-agent/components/aiYaklangCode/type.ts new file mode 100644 index 00000000..dd512ecb --- /dev/null +++ b/src/pages/AIAgent/ai-agent/components/aiYaklangCode/type.ts @@ -0,0 +1,11 @@ +import type { AIOutputEvent } from '@/pages/AIAgent/ai-re-act/hooks/grpcApi'; +import type { ModalInfoProps } from '../ModelInfo'; +import type { ReactNode } from 'react'; + +export interface AIYaklangCodeProps { + content: string; + nodeLabel: string; + modalInfo: ModalInfoProps; + contentType: AIOutputEvent['ContentType']; + referenceNode?: ReactNode; +} diff --git a/src/pages/AIAgent/ai-agent/defaultConstant.tsx b/src/pages/AIAgent/ai-agent/defaultConstant.tsx new file mode 100644 index 00000000..fa1c019f --- /dev/null +++ b/src/pages/AIAgent/ai-agent/defaultConstant.tsx @@ -0,0 +1,387 @@ +import type { ReactNode } from 'react' +import type { AIAgentSetting } from './aiAgentType' +import { + OutlineChipIcon, + OutlineCogIcon, + OutlineSparklesIcon, + OutlineWrenchIcon, + OutlineBookOpenTextIcon, + OutlineBotIcon, + OutlineDocumenttextIcon, + OutlineFolderopenIcon, + // OutlinePointerIcon, + OutlineEarOffIcon, + // OutlineAIIcon, +} from '@/assets/icon/outline' +import type { YakitSideTabProps, YakitTabsProps } from '@/compoments/yakitSideTab/YakitSideTabType' +import type { PaginationSchema } from '../enums/invoker/schema' +import { genDefaultPagination } from '../enums/invoker/schema' +import { + ChatGLMIcon, + ComateIcon, + DeepSeekIcon, + GeminiIcon, + MoonshotIcon, + OllamaIcon, + OpenAIIcon, + OpenRouterIcon, + SiliconFlowIcon, + TongyiIcon, + MemfitIcon, +} from './aiModelList/icon' +import type { UseChatIPCState } from '../ai-re-act/hooks/type' +import type { AIAgentGrpcApi } from '../ai-re-act/hooks/grpcApi' +import { AITaskStatus } from '../ai-re-act/hooks/grpcApi' +import { + SolidCursorclickIcon, + SolidHashtagIcon, + SolidLightbulbIcon, + SolidLightningboltIcon, + SolidToolIcon, +} from '@/assets/icon/solid' +import type { MCPServerType } from './type/aiMCP' +import { DefaultMemoryList, DefaultPlanHistoryList } from '../ai-re-act/hooks/defaultConstant' +// import { ColorsAIIcon } from '@/assets/icon/colors' +import type { AIGlobalConfig, AIModelTypeFileName } from './aiModelList/utils' +import type { YakitTagColor } from '@/compoments/yakitUI/YakitTag/YakitTagType' +import { cloneDeep } from 'lodash' + +/** AI-Agent 页面的唯一 id */ +export const YakitAIAgentPageID = 'yakit-ai-agent' + +export enum AIAgentTabListEnum { + History = 'history', + Setting = 'setting', + Forge_Name = 'forgeName', + Tool = 'tool', + AI_Model = 'AIModel', + MCP = 'mcp', + KnowledgeBase = 'knowledgeBase', +} +export const AiAgentTabList: YakitTabsProps[] = [ + { + value: AIAgentTabListEnum.History, + label: '历史会话', + icon: , + }, + { + value: AIAgentTabListEnum.Setting, + label: '配置', + icon: , + }, + // {value: AIAgentTabListEnum.Forge_Name, label: "技能", icon: }, + // {value: AIAgentTabListEnum.Tool, label: "工具", icon: }, + { + value: AIAgentTabListEnum.AI_Model, + label: 'AI模型', + icon: , + }, + // { value: AIAgentTabListEnum.MCP, label: 'MCP', icon: }, +] +export enum AIMentionTabsEnum { + /** forge 智能体 */ + Forge_Name = 'forgeName', + // /** 工具 */ + // Tool = 'tool', + // /** 知识库 */ + // KnowledgeBase = 'knowledgeBase', + // /** 文件系统 */ + // File_System = 'fileSystem', + /** 专注模式 */ + FocusMode = 'focusMode', +} +export const AIMentionTabs: YakitSideTabProps['yakitTabs'] = [ + { value: AIMentionTabsEnum.Forge_Name, label: '技能' }, + // { value: AIMentionTabsEnum.Tool, label: '工具' }, + // { value: AIMentionTabsEnum.KnowledgeBase, label: '知识库' }, + // { value: AIMentionTabsEnum.File_System, label: '文件系统' }, + { value: AIMentionTabsEnum.FocusMode, label: '专注模式' }, +] + +/** ai-agent 聊天全局配置参数默认值 */ +export const AIAgentSettingDefault: AIAgentSetting = { + EnableSystemFileSystemOperator: true, + UseDefaultAIConfig: true, + ForgeName: '', + DisallowRequireForUserPrompt: true, + ReviewPolicy: 'yolo', + AIReviewRiskControlScore: 0.5, + DisableToolUse: false, + AICallAutoRetry: 3, + AITransactionRetry: 5, + EnableAISearchTool: true, + EnableAISearchInternet: false, + EnableQwenNoThinkMode: false, + AllowPlanUserInteract: true, + PlanUserInteractMaxCount: 3, + AIService: '', + ReActMaxIteration: 100, + TimelineItemLimit: 100, + TimelineContentSizeLimit: 20, + UserInteractLimit: 0, + TimelineSessionID: '', + AICallTokenLimit: 40, +} + +/** mcp 自定义服务器配置类型选项 */ +export const MCPTransportTypeList: { value: MCPServerType; label: string }[] = [ + { label: 'SSE', value: 'sse' }, + { label: 'STDIO', value: 'stdio' }, +] + +/** + * @name 生成一个[AIAgentGrpcApi.PlanTask]任务信息 + * @description 生成的信息内不存在subtasks字段值 + */ +export const generateTaskChatExecution: (info?: AIAgentGrpcApi.PlanTask) => AIAgentGrpcApi.PlanTask = (info) => { + let data: AIAgentGrpcApi.PlanTask = { + index: '', + name: '', + goal: '', + progress: AITaskStatus.created, + isRemove: false, + tools: [], + description: '', + total_tool_call_count: 0, + success_tool_call_count: 0, + fail_tool_call_count: 0, + summary: '', + } + if (info) { + data.index = info.index || '' + data.name = info.name || '' + data.goal = info.goal || '' + data.progress = info.progress || AITaskStatus.created + data.isRemove = info.isRemove || false + data.tools = info.tools || [] + data.description = info.description || '' + data.total_tool_call_count = info.total_tool_call_count || 0 + data.success_tool_call_count = info.success_tool_call_count || 0 + data.fail_tool_call_count = info.fail_tool_call_count || 0 + data.summary = info.summary || '' + } + + return data +} +/** ai 左侧tab变化信号 */ +export enum SwitchAIAgentTabEventEnum { + /** 切换active */ + SET_TAB_ACTIVE = 'setTabActive', + /** 选中tab是否显示 */ + SET_TAB_SHOW = 'setTabShow', +} +export enum AITabsEnum { + Task_Content = 'task-content', + HTTP = 'http', + Risk = 'risk', + Operation_Log = 'operation-log', +} +/** @name AI 默认展示的tab集合 */ +export const AITabs = { + 'task-content': { label: '任务内容', value: AITabsEnum.Task_Content }, + http: { label: 'HTTP 流量', value: AITabsEnum.HTTP }, + risk: { label: '漏洞与风险', value: AITabsEnum.Risk }, + 'operation-log': { label: '读写日志', value: AITabsEnum.Operation_Log }, +} + +/** AI-Forge 列表查询条件里的页码默认条件 */ +export const AIForgeListDefaultPagination: PaginationSchema = { + ...genDefaultPagination(20), + OrderBy: 'id', +} + +export const tagColors: YakitTagColor[] = [ + 'blue', + 'bluePurple', + 'cyan', + 'green', + 'info', + 'purple', + 'success', + 'warning', + 'yellow', +] + +export const AIOnlineModelIconMap: Record = { + openai: , + chatglm: , + moonshot: , + tongyi: , + comate: , + deepseek: , + siliconflow: , + ollama: , + openrouter: , + gemini: , + 'yaklang-writer': , + 'yaklang-rag': , + 'yaklang-com-search': , + 'yakit-plugin-search': , + aibalance: , +} + +export enum AILocalModelTypeEnum { + AIChat = 'aichat', + Embedding = 'embedding', + SpeechToText = 'speech-to-text', +} + +export const AIReviewRuleOptions = [ + // { + // value: 'manual', + // label: 'Manual', + // describe: '所有审阅都由用户自己操作', + // }, + { + value: 'yolo', + label: 'Yolo', + describe: '所有审阅默认执行,不进行询问', + }, + // { + // value: 'ai', + // label: 'AI', + // describe: '由AI判断审阅风险,低风险默认执行,高风险由用户操作', + // }, +] as const +export type AIReviewRuleOptionsType = (typeof AIReviewRuleOptions)[number]['value'] +export const AIReviewRuleIconMap: Record = { + // manual: { + // icon: , + // activeIcon: , + // }, + yolo: { + icon: , + activeIcon: , + }, + // ai: { + // icon: , + // activeIcon: , + // }, +} +export enum AIMCPServerTypeEnum { + SSE = 'sse', + Stdio = 'stdio', +} +// #region ai hooks 默认值 +export const defaultChatIPCData: UseChatIPCState = { + execute: false, + runTimeIDs: [], + casualChat: { + elements: [], + }, + yakExecResult: { + card: [], + execFileRecord: new Map(), + yakExecResultLogs: [], + }, + taskChat: { + plan: [], + elements: [], + }, + grpcFolders: [], + questionQueue: { + total: 0, + data: [], + }, + casualStatus: { loading: false, title: '' }, + reActTimelines: [], + memoryList: { ...DefaultMemoryList }, + taskStatus: { loading: false, plan: '', task: '' }, + systemStream: '', + focusMode: '', + switchLoading: false, + planHistoryList: cloneDeep(DefaultPlanHistoryList), + cancelCasualLoading: false, + cancelTaskLoading: false, +} +// #endregion + +/** @name 任务回答类型对应图标 */ +export const taskAnswerToIconMap: Record = { + plan: , + execute: , + summary: , + 'call-tools': , + decision: , +} + +export enum ReActChatEventEnum { + /** 新建会话 */ + NEW_CHAT = 'new-chat', + /** 使用forge */ + OPEN_FORGE_FORM = 'open-forge-form', + /** 使用工具 */ + USE_AI_TOOL = 'use-ai-tool', +} + +export const iconMap = { + file: , + folder: , + forge: , + tool: , + knowledgeBase: , + focusMode: , +} + +export enum AttachedResourceTypeEnum { + CONTEXT_PROVIDER_TYPE_FILE = 'file', + CONTEXT_PROVIDER_TYPE_KNOWLEDGE_BASE = 'knowledge_base', + CONTEXT_PROVIDER_TYPE_AITOOL = 'aitool', + CONTEXT_PROVIDER_TYPE_AIFORGE = 'aiforge', +} + +export enum AttachedResourceKeyEnum { + CONTEXT_PROVIDER_KEY_FILE_PATH = 'file_path', + CONTEXT_PROVIDER_KEY_FILE_CONTENT = 'file_content', + CONTEXT_PROVIDER_KEY_NAME = 'name', + CONTEXT_PROVIDER_KEY_SYSTEM_FLAG = 'system_flag', +} + +export enum AIModelPolicyEnum { + // PolicyAuto 自动策略:根据请求内容自动选择最合适的模型 + PolicyAuto = 'auto', + // PolicyPerformance 性能优先:优先使用高智能模型 + PolicyPerformance = 'performance', + // PolicyCost 成本优先:优先使用轻量级/低成本模型 + PolicyCost = 'cost', + // PolicyBalance 平衡策略:在响应速度、智能程度和成本之间取得平衡 + PolicyBalance = 'balance', +} + +export const AIModelPolicyOptions = [ + { value: AIModelPolicyEnum.PolicyAuto, label: '自动选择' }, + { value: AIModelPolicyEnum.PolicyPerformance, label: '性能优先' }, + { value: AIModelPolicyEnum.PolicyCost, label: '成本优先' }, + { value: AIModelPolicyEnum.PolicyBalance, label: '平衡策略' }, +] + +/** ai model type,下拉选择的值 */ +export enum AIModelTypeEnum { + /** 高质模型 */ + TierIntelligent = 'intelligent', + /** 轻量模型 */ + TierLightweight = 'lightweight', + /** 视觉模式 */ + TierVision = 'vision', +} + +/** 接口字段名对应 */ +export const AIModelTypeInterFileNameEnum: { [K in AIModelTypeFileName]: K } = { + /** 高质模型 */ + IntelligentModels: 'IntelligentModels', + /** 轻量模型 */ + LightweightModels: 'LightweightModels', + /** 视觉模式 */ + VisionModels: 'VisionModels', +} + +export const defaultAIGlobalConfig: AIGlobalConfig = { + Enabled: false, + RoutingPolicy: AIModelPolicyEnum.PolicyAuto, + DisableFallback: false, + DefaultModelId: '', + GlobalWeight: 0, + IntelligentModels: [], + LightweightModels: [], + VisionModels: [], +} diff --git a/src/pages/AIAgent/ai-agent/forgeName/ForgeName.module.scss b/src/pages/AIAgent/ai-agent/forgeName/ForgeName.module.scss new file mode 100644 index 00000000..b3b26e7c --- /dev/null +++ b/src/pages/AIAgent/ai-agent/forgeName/ForgeName.module.scss @@ -0,0 +1,193 @@ +.forge-name { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + gap: 12px; + background-color: var(--Colors-Use-Basic-Background); + + .header-wrapper { + padding: 8px 12px 0 12px; + display: flex; + flex-direction: column; + gap: 12px; + .haeder-first { + display: flex; + justify-content: space-between; + align-items: center; + .first-title { + display: flex; + align-items: center; + gap: 4px; + + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 500; + } + .first-btns { + display: flex; + gap: 8px; + } + } + + .header-second { + .search-icon { + svg { + width: 16px; + height: 16px; + color: var(--Colors-Use-Neutral-Text-3-Secondary); + } + } + } + } +} + +.forge-name-list { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + .list-wrapper { + flex: 1; + overflow: hidden auto; + } +} + +.forge-list-opt { + cursor: pointer; + height: 41px; + padding: 10px 12px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 4px; + border-top: 1px solid var(--Colors-Use-Neutral-Border, #e6e8ed); + background: var(--Colors-Use-Basic-Background, #fff); + + &:last-child, + &:has(+ .forge-list-no-more), + &:has(+ .forge-list-loading) { + height: 42px; + border-bottom: 1px solid var(--Colors-Use-Neutral-Border); + } + &:hover { + background: var(--Colors-Use-Neutral-Bg-Hover); + .item-extra { + display: flex; + } + } + + .opt-title { + flex: 1; + overflow: hidden; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + line-height: 16px; + } + .item-extra { + display: none; + align-items: center; + gap: 4px; + .del-icon { + svg { + color: var(--Colors-Use-Status-High); + } + } + } +} +.item-extra-tooltip { + padding-bottom: 4px; + :global { + .ant-tooltip-arrow { + right: 4px; + bottom: 1px; + } + } +} + +.forge-opt-popover { + padding-left: 4px; + :global { + .ant-popover-inner{ + border: none; + } + .ant-popover-inner-content { + padding: 0; + } + } + .forge-detail { + width: 320px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; + border-radius: 4px; + border: 1px solid var(--Colors-Use-Neutral-Bg-Hover); + .detail-name { + overflow: hidden; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 14px; + font-weight: 500; + line-height: 20px; + letter-spacing: 0.1px; + } + .detail-content { + display: flex; + flex-direction: column; + gap: 12px; + .content-description { + color: var(--Colors-Use-Neutral-Text-3-Secondary); + font-size: 14px; + font-weight: 400; + line-height: 20px; + } + .content-tools { + padding: 8px; + display: flex; + flex-direction: column; + gap: 4px; + border-radius: 4px; + background: var(--Colors-Use-Neutral-Bg); + .tools-header { + display: flex; + align-items: center; + gap: 4px; + color: var(--Colors-Use-Neutral-Text-4-Help-text); + font-size: 12px; + font-weight: 400; + svg { + width: 16px; + height: 16px; + } + } + .tools-body { + display: flex; + flex-wrap: wrap; + gap: 4px; + .tool-tag { + margin-right: 0; + } + } + } + } + } +} + +.forge-list-no-more { + padding: 12px 0; + .no-more-title { + color: var(--Colors-Use-Neutral-Text-3-Secondary); + text-align: center; + font-size: 11px; + font-weight: 400; + line-height: 14px; + } +} + +.forge-list-loading { + padding: 10px 0; + .loading-style { + align-items: center; + } +} diff --git a/src/pages/AIAgent/ai-agent/forgeName/ForgeName.tsx b/src/pages/AIAgent/ai-agent/forgeName/ForgeName.tsx new file mode 100644 index 00000000..9ec0b3c9 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/forgeName/ForgeName.tsx @@ -0,0 +1,646 @@ +import type { Ref } from 'react' +import React, { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' +import type { + ExportAIForgeFormValues, + ExportAIForgeRequest, + ExportImportAIForgeProgress, + ImportAIForgeFormValues, + ImportAIForgeRequest, +} from './type' +import { OutlinePencilaltIcon, OutlineSearchIcon, OutlineTrashIcon } from '@/assets/icon/outline' +import { YakitInput } from '@/compoments/yakitUI/YakitInput/YakitInput' +import { useDebounceEffect, useDebounceFn, useInViewport, useMemoizedFn, useThrottleFn, useUpdateEffect } from 'ahooks' +import { YakitPopover } from '@/compoments/yakitUI/YakitPopover/YakitPopover' +import { SolidToolIcon } from '@/assets/icon/solid' +import { YakitTag } from '@/compoments/yakitUI/YakitTag/YakitTag' +import { YakitSpin } from '@/compoments/yakitUI/YakitSpin/YakitSpin' +import emiter from '@/utils/eventBus/eventBus' +import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton' +import { Form, Tooltip } from 'antd' +import { yakitNotify } from '@/utils/notification' +import { AIForgeListDefaultPagination, ReActChatEventEnum } from '../defaultConstant' +import { YakitPopconfirm } from '@/compoments/yakitUI/YakitPopconfirm/YakitPopconfirm' +import type { AIForge, QueryAIForgeRequest, QueryAIForgeResponse } from '../type/forge' +import type { ImportExportModalExtra } from '@/compoments/ImportExportModal/ImportExportModal' +import ImportExportModal from '@/compoments/ImportExportModal/ImportExportModal' +import { YakitFormDragger } from '@/compoments/yakitUI/YakitForm/YakitForm' +import { YakitSelect } from '@/compoments/yakitUI/YakitSelect/YakitSelect' +import type { AITool, GetAIToolListRequest } from '../type/aiTool' +import { grpcGetAIToolList } from '../aiToolList/utils' +import type { LogListInfo } from '@/compoments/YakitUploadModal/YakitUploadModal' +import classNames from 'classnames' +import styles from './ForgeName.module.scss' +import { YakitRoute } from '../../enums/yakitRoute' +import { useGetSetState } from '@/hooks' +import { genDefaultPagination, type PaginationSchema } from '@/pages/invoker/schema' +import { postAiforgeDelete, postAiforgeGet, postAiforgeQuery } from '@/apis/AiEventApi' + +export interface ForgeNameRef { + openAdd: () => void + openImport: () => void +} + +const ForgeName = (_: {}, ref: Ref) => { + // const {} = props + + // #region AIForge 模板增删改功能 使用功能 + // 新建 forge 模板 + const handleNewAIForge = useMemoizedFn(() => { + emiter.emit('menuOpenPage', JSON.stringify({ route: `/${YakitRoute.AddAIForge}` })) + }) + // 编辑 forge 模板 + const handleModifyAIForge = useMemoizedFn((info: AIForge) => { + const id = Number(info.Id) || 0 + if (!id) { + yakitNotify('error', `该模板 ID('${info.Id}') 异常, 无法编辑`) + return + } + emiter.emit( + 'menuOpenPage', + JSON.stringify({ + route: `/${YakitRoute.ModifyAIForge}`, + params: { id: id, source: YakitRoute.AI_Agent }, + }), + ) + }) + + // 删除的 forge 队列 + const [delStatus, setDelStatus] = useState([]) + // 删除 forge 模板 + const handleDeleteAIForge = useMemoizedFn((info: AIForge) => { + const id = Number(info.Id) || 0 + if (!id) { + yakitNotify('error', `该模板 ID('${info.Id}') 异常, 无法编辑`) + return + } + const isLoading = delStatus.includes(id) + if (isLoading) return + setDelStatus((old) => [...old, id]) + postAiforgeDelete({ Id: id }) + .then(() => { + yakitNotify('success', '删除Forge模板成功') + setData((old) => { + return { + ...old, + Total: old.Total - 1, + Data: old.Data.filter( + // eslint-disable-next-line max-nested-callbacks + (item: { Id: number }) => item.Id !== info.Id, + ), + } + }) + }) + .catch(() => {}) + .finally(() => { + setTimeout(() => { + // eslint-disable-next-line max-nested-callbacks + setDelStatus((old) => old.filter((el) => el !== id)) + }, 100) + }) + }) + // 点击使用 Forge + const handleOnClick = useMemoizedFn((info: AIForge) => { + emiter.emit( + 'onReActChatEvent', + JSON.stringify({ + type: ReActChatEventEnum.OPEN_FORGE_FORM, + params: { value: info }, + }), + ) + }) + // #region AI-Forge 列表数据 + // const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(false) + const [data, setData, getData] = useGetSetState({ + Pagination: { ...AIForgeListDefaultPagination }, + Data: [], + Total: 0, + }) + const isMore = useRef(true) + + const wrapperRef = useRef(null) + const forgeData = data.Data || [] + + // 获取 AI-Forge 列表 + const fetchData = useMemoizedFn((isInit?: boolean) => { + if (isInit) isMore.current = true + if (!isMore.current) return + const pageInfo = getData().Pagination + const nextPage = isInit ? 1 : (pageInfo.Page || 1) + 1 + const request: QueryAIForgeRequest = { + Pagination: { + ...pageInfo, + Page: nextPage, + }, + } + if (search) request.Filter = { Keyword: search } + + setLoading(true) + postAiforgeQuery(request) + .then((res) => { + const responseData = res.Data || [] + const newLength = responseData.length + if (newLength < request.Pagination.Limit) isMore.current = false + else isMore.current = true + + const oldData = getData().Data || [] + const newArr = isInit ? responseData : oldData.concat(responseData) + setData({ + ...res, + Pagination: request.Pagination, + Data: newArr, + }) + if (isInit) { + setTimeout(() => { + handleFillList() + }, 100) + } + }) + .catch(() => {}) + .finally(() => { + setTimeout(() => { + setLoading(false) + }, 300) + }) + }) + // 判断数据是否填充满列表 + const handleFillList = useMemoizedFn(() => { + if (wrapperRef && wrapperRef.current) { + const { clientHeight, scrollHeight } = wrapperRef.current + if (scrollHeight <= clientHeight + 20) { + fetchData() + } + } + }) + + useEffect(() => { + // fetchDataTotal() + + emiter.on('onTriggerRefreshForgeList', handleEmiterUpdateData) + return () => { + emiter.off('onTriggerRefreshForgeList', handleEmiterUpdateData) + } + }, []) + + const [search, setSearch] = useState('') + useDebounceEffect( + () => { + fetchData(true) + }, + [search], + { wait: 300 }, + ) + + // 通信触发的刷新列表请求 + const handleEmiterTriggerRefresh = useDebounceFn( + () => { + // fetchDataTotal() + isMore.current = true + fetchData(true) + }, + { wait: 300 }, + ).run + + // 通信触发更新数据请求 + const handleEmiterUpdateData = useMemoizedFn((id: string) => { + const forgesArr = getData().Data || [] + const findIndex = forgesArr.findIndex((item) => Number(item.Id) === Number(id)) + if (findIndex !== -1) { + // 存在数据则局部更新 + postAiforgeGet({ ID: Number(id) }) + .then((res) => { + setData((old) => { + const newData = { ...old } + newData.Data[findIndex] = res + return newData + }) + }) + .catch(() => {}) + } else { + // 不存在数据则刷新列表 + handleEmiterTriggerRefresh() + } + }) + + const wrapper = useRef(null) + const [inViewport = true] = useInViewport(wrapper) + + useUpdateEffect(() => { + if (inViewport) handleFillList() + }, [inViewport]) + + // 滚动加载更多 + const onScrollCapture = useThrottleFn( + () => { + if (!isMore.current) return + if (loading) return + if (wrapperRef && wrapperRef.current) { + const { clientHeight, scrollHeight, scrollTop } = wrapperRef.current + + const scrollBottom = scrollHeight - scrollTop - clientHeight + if (scrollBottom <= 50) { + fetchData() + } + } + }, + { wait: 200, leading: false }, + ).run + // #endregion + + // #region AIForge单个导入导出 + const [importExportExtra, setImportExportExtra] = useState({ + hint: false, + title: '导出技能', + type: 'export', + apiKey: 'ExportAIForge', + }) + const logListRef = useRef([]) + const forgeNamesRef = useRef([]) + const toolNames = useRef([]) + const outputNameRef = useRef('') + const handleOpenImportExportHint = useMemoizedFn((extra: Omit) => { + if (importExportExtra.hint) return + setImportExportExtra({ ...extra, hint: true }) + }) + + // const exportPath = useRef('') + const handleFinishedImportExportHint = useMemoizedFn((result: boolean) => { + if (result) { + const type = importExportExtra.type + if (type === 'import') { + // 刷新列表 + handleEmiterTriggerRefresh() + } else { + // if (exportPath.current) { + // openABSFileLocated(exportPath.current); + // } + } + yakitNotify('success', (importExportExtra.type === 'export' ? '导出' : '导入') + '成功') + } + // exportPath.current = '' + + const index = logListRef.current.findIndex((i) => i.isError) + if (index === -1) { + setImportExportExtra((prev) => { + return { + ...prev, + hint: false, + } + }) + } + logListRef.current = [] + }) + + const commonImportExportProps = useMemo(() => { + return { + getContainer: document.getElementById(`main-operator-page-body-${YakitRoute.AI_Agent}`) || undefined, + extra: importExportExtra, + getProgressValue: (p: ExportImportAIForgeProgress) => { + return p.Percent / 100 + }, + getlogListInfo: (stream: ExportImportAIForgeProgress[]) => { + logListRef.current = stream.map((item) => ({ + message: item.Message, + isError: item.MessageType === 'error', + key: String(Math.random() * 5), + })) + return logListRef.current + }, + onFinished: handleFinishedImportExportHint, + } + }, [importExportExtra]) + + // aiTools下拉列表 + const [selectLoading, setSelectLoading] = useState(false) + const selectDropdown = useMemoizedFn((originNode: React.ReactNode) => { + return ( +
    + {originNode} +
    + ) + }) + const [searchKeyword, setSearchKeyword] = useState('') + const [aiTool, setAiTool] = useState([]) + const [aiToolPagination, setAiToolPagination] = useState({ + ...genDefaultPagination(20), + OrderBy: 'created_at', + Page: 1, + }) + const { run: debouncedSearch } = useDebounceFn( + (value: string) => { + setAiTool([]) + setAiToolPagination((v) => ({ ...v, Page: 1 })) + setSearchKeyword(value) + getAiToolData(1, value) + }, + { + wait: 300, + }, + ) + const getAiToolData = async (page: number, keyword = searchKeyword) => { + setSelectLoading(true) + const paginationProps = { + ...aiToolPagination, + page: page, + } + const newQuery: GetAIToolListRequest = { + Query: keyword, + ToolName: '', + Pagination: paginationProps, + OnlyFavorites: false, + } + const isInit = page === 1 + try { + const res = await grpcGetAIToolList(newQuery) + if (!res.Tools) res.Tools = [] + if (res.Tools?.length > 0) { + setAiToolPagination((v) => ({ + ...v, + Page: paginationProps.page, + })) + } + const newData = res.Tools.map((item) => ({ ...item })) + const opsd = isInit ? newData : aiTool.concat(newData) + setAiTool(opsd) + } finally { + setSelectLoading(false) + } + } + // #endregion + + useImperativeHandle(ref, () => ({ + openAdd: handleNewAIForge, + openImport: () => + handleOpenImportExportHint({ + title: '导入技能', + type: 'import', + apiKey: 'ImportAIForge', + }), + })) + + return ( +
    +
    + {/*
    +
    + 技能库 + {total} +
    +
    + } + onClick={() => + handleOpenImportExportHint({ + title: "导入技能", + type: "import", + apiKey: "ImportAIForge" + }) + } + /> + } onClick={handleNewAIForge} /> +
    +
    */} + +
    + } + allowClear + placeholder="请输入关键词搜索" + value={search} + onChange={(e) => setSearch(e.target.value)} + /> +
    +
    + +
    +
    +
    + {forgeData.map((data, index) => { + const { Id, ForgeName, Description, ToolNames, ForgeVerboseName } = data + const key = Number(Id) || index + const tools = ToolNames ? ToolNames.filter(Boolean) : [] + const delLoading = delStatus.includes(Number(Id)) + + return ( + + +
    + {ForgeVerboseName || ForgeName} +
    + +
    +
    {Description || '暂无更多说明'}
    + + {tools?.length > 0 && ( +
    +
    + + 关联工具 +
    + +
    + {tools.map((tool) => { + return ( + + {tool} + + ) + })} +
    +
    + )} +
    +
    + } + > +
    handleOnClick(data)}> +
    + {ForgeVerboseName || ForgeName} +
    + +
    + {/* + } + onClick={(e) => { + e.stopPropagation() + forgeNamesRef.current = [ForgeName] + toolNames.current = tools + outputNameRef.current = ForgeVerboseName || ForgeName || '' + handleOpenImportExportHint({ + title: '导出技能', + type: 'export', + apiKey: 'ExportAIForge', + }) + }} + /> + */} + + } + onClick={(e) => { + e.stopPropagation() + handleModifyAIForge(data) + }} + /> + + + { + e?.stopPropagation() + handleDeleteAIForge(data) + }} + onCancel={(e) => { + e?.stopPropagation() + }} + > + } + onClick={(e) => { + e.stopPropagation() + }} + /> + + +
    +
    + + + ) + })} + + {!isMore.current && !loading && ( +
    +
    已经到底啦~
    +
    + )} + {loading && ( +
    + +
    + )} +
    +
    +
    + {importExportExtra.type === 'export' && ( + + {...commonImportExportProps} + formProps={{ + initialValues: { + OutputName: outputNameRef.current || '', + ToolNames: toolNames.current, + }, + }} + renderForm={() => ( + <> + + + + + { + const { target } = e + const ref: HTMLDivElement = target as unknown as HTMLDivElement + if (ref.scrollTop + ref.offsetHeight + 20 >= ref.scrollHeight && !selectLoading) { + getAiToolData(aiToolPagination.Page + 1, searchKeyword) + } + }} + dropdownRender={(originNode: React.ReactNode) => selectDropdown(originNode)} + mode="multiple" + onDropdownVisibleChange={(open) => { + if (open) { + getAiToolData(1, searchKeyword) + } else { + setAiToolPagination((v) => ({ + ...v, + Page: 1, + })) + setAiTool([]) + setSearchKeyword('') + } + }} + > + {aiTool.map((item) => ( + + {item.VerboseName || item.Name} + + ))} + + + + + + + )} + // onBeforeSubmit={async (values) => { + // let name = values.OutputName + '.zip' + // if (values.Password) name += '.enc' + // try { + // exportPath.current = await ipcRenderer.invoke('GenerateProjectsFilePath', name) + // } catch (error) {} + // }} + onSubmitForm={(values) => ({ + ForgeNames: forgeNamesRef.current, + ...values, + })} + isProgressFinished={(p: ExportImportAIForgeProgress) => p.Percent === 100 && p.MessageType === 'success'} + /> + )} + {importExportExtra.type === 'import' && ( + + {...commonImportExportProps} + renderForm={() => ( + <> + + + + + + )} + onSubmitForm={(values) => ({ + Overwrite: true, + ...values, + })} + isProgressFinished={(p: ExportImportAIForgeProgress) => p.Percent === 100} + /> + )} + + ) +} + +const ForwardForgeName = forwardRef(ForgeName) + +export default memo(ForwardForgeName) diff --git a/src/pages/AIAgent/ai-agent/forgeName/type.d.ts b/src/pages/AIAgent/ai-agent/forgeName/type.d.ts new file mode 100644 index 00000000..2ed3b0bd --- /dev/null +++ b/src/pages/AIAgent/ai-agent/forgeName/type.d.ts @@ -0,0 +1,29 @@ +export interface ForgeNameProps {} +export interface ExportAIForgeFormValues { + OutputName: string + ToolNames?: string[] + Password?: string +} +export interface ImportAIForgeFormValues { + InputPath: string + Password?: string +} +export interface ExportAIForgeRequest { + ForgeNames: string[] + TargetPath?: string + OutputName: string + ToolNames?: string[] + Password?: string +} +export interface ImportAIForgeRequest { + InputPath: string + Overwrite: boolean + NewForgeName?: string + Password?: string +} + +export interface ExportImportAIForgeProgress { + Percent: number + Message: string + MessageType: string +} diff --git a/src/pages/AIAgent/ai-agent/grpc.ts b/src/pages/AIAgent/ai-agent/grpc.ts new file mode 100644 index 00000000..a53e5d8e --- /dev/null +++ b/src/pages/AIAgent/ai-agent/grpc.ts @@ -0,0 +1,242 @@ +import type { APIFunc, APIOptionalFunc } from '@/apiUtils/type' +import axios from '@/utils/axios' +import { yakitNotify } from '@/utils/notification' +import type { + AIEventQueryRequest, + AIEventQueryResponse, + ExportAILogsRequest, + ExportAILogsResponse, + GetRandomAIMaterialsRequest, + GetRandomAIMaterialsResponse, +} from '../ai-re-act/hooks/grpcApi' +import type { DbOperateMessage } from '@/pages/layout/mainOperatorContent/utils' +import type { + AIForge, + AIForgeFilter, + GetAIForgeRequest, + QueryAIFocusRequest, + QueryAIFocusResponse, + QueryAIForgeRequest, + QueryAIForgeResponse, +} from './type/forge' +import type { YakQueryHTTPFlowResponse } from '@/compoments/HTTPFlowTable/HTTPFlowTable' +import type { YakQueryHTTPFlowRequest } from '@/utils/yakQueryHTTPFlow' +import type { AIChatQSData, AIChatQSDataTypeEnum } from '../ai-re-act/hooks/aiRender' +import { genBaseAIChatData, isToolExecStream } from '../ai-re-act/hooks/utils' +import { Uint8ArrayToString } from '@/utils/str' +import { convertNodeIdToVerbose } from '../ai-re-act/hooks/defaultConstant' + +// const { ipcRenderer } = window.require('electron'); + +/** + * @name 查询AI事件 + * - 查执行工具的 call_tool 和 tool_stdout + */ +export const grpcQueryAIEvent: APIFunc = (param, hiddenError) => { + return axios.post('/agent/event/query', param).catch((e) => { + if (!hiddenError) yakitNotify('error', '查询QueryAIEvent失败:' + e) + throw e + }) +} + +/** + * @name 查询工具详情前端显示数据 + * - 查执行工具的 call_tool 和 tool_stdout + */ +export const grpcQueryAIToolDetails: APIFunc = (param, hiddenError) => { + return new Promise(async (resolve, reject) => { + grpcQueryAIEvent(param) + .then((res: AIEventQueryResponse) => { + const { Events } = res + const list: AIChatQSData[] = [] + Events.filter((ele) => { + if (ele.Type === AIChatQSDataTypeEnum.STREAM && isToolExecStream(ele.NodeId)) return true + if (ele.Type === AIChatQSDataTypeEnum.TOOL_CALL_RESULT) return true + return false + }).forEach((item) => { + let ipcContent = '' + let ipcStreamDelta = '' + try { + ipcContent = Uint8ArrayToString(item.Content) || '' + ipcStreamDelta = Uint8ArrayToString(item.StreamDelta) || '' + } catch (error) {} + const current: AIChatQSData = { + ...genBaseAIChatData(item), + chatType: 'reAct', // 随机写的,后面需要开发UI时通过传入参数设置 + type: AIChatQSDataTypeEnum.STREAM, + data: { + CallToolID: item.CallToolID, + NodeId: item.NodeId, + NodeIdVerbose: item.NodeIdVerbose || convertNodeIdToVerbose(item.NodeId), + content: ipcContent + ipcStreamDelta, + ContentType: item.ContentType, + EventUUID: item.EventUUID, + status: 'end', + }, + } + list.push(current) + }) + resolve(list) + }) + .catch((e) => { + if (!hiddenError) yakitNotify('error', '查询 grpcQueryAIToolDetails 失败:' + e) + reject(e) + }) + }) +} + +/** @name 删除 AI 事件(支持 ClearAll) */ +// export const grpcDeleteAIEvent: APIFunc< +// AIEventDeleteRequest, +// DbOperateMessage +// > = (param, hiddenError) => { +// return new Promise(async (resolve, reject) => { +// ipcRenderer +// .invoke('DeleteAIEvent', param) +// .then(resolve) +// .catch((e) => { +// if (!hiddenError) +// yakitNotify('error', '删除DeleteAIEvent失败:' + e); +// reject(e); +// }); +// }); +// }; +/** @name 删除 AI 任务(支持 ClearAll) */ +// export const grpcDeleteAITask: APIFunc< +// AITaskDeleteRequest, +// DbOperateMessage +// > = (param, hiddenError) => { +// return new Promise(async (resolve, reject) => { +// ipcRenderer +// .invoke('DeleteAITask', param) +// .then(resolve) +// .catch((e) => { +// if (!hiddenError) +// yakitNotify('error', '删除DeleteAITask失败:' + e); +// reject(e); +// }); +// }); +// }; +// #endregion + +// #region AI-Forge 相关 grpc 接口 +/** @name 创建 AI-Forge */ +export const grpcCreateAIForge: APIFunc = (param, hiddenError) => { + return new Promise(async (resolve, reject) => { + ipcRenderer + .invoke('CreateAIForge', param) + .then(resolve) + .catch((e) => { + if (!hiddenError) yakitNotify('error', '创建AI-Forge失败:' + e) + reject(e) + }) + }) +} +/** @name 编辑 AI-Forge */ +export const grpcUpdateAIForge: APIFunc = (param, hiddenError) => { + return new Promise(async (resolve, reject) => { + ipcRenderer + .invoke('UpdateAIForge', param) + .then(resolve) + .catch((e) => { + if (!hiddenError) yakitNotify('error', '修改AI-Forge失败:' + e) + reject(e) + }) + }) +} +/** @name 删除 AI-Forge */ +export const grpcDeleteAIForge: APIFunc = (param, hiddenError) => { + return new Promise(async (resolve, reject) => { + ipcRenderer + .invoke('DeleteAIForge', param) + .then(resolve) + .catch((e) => { + if (!hiddenError) yakitNotify('error', '删除AI-Forge失败:' + e) + reject(e) + }) + }) +} +/** @name 查询 AI-Forge 列表 */ +export const grpcQueryAIForge: APIFunc = (param, hiddenError) => { + return new Promise(async (resolve, reject) => { + ipcRenderer + .invoke('QueryAIForge', param) + .then(resolve) + .catch((e) => { + if (!hiddenError) yakitNotify('error', '查询AI-Forge失败:' + e) + reject(e) + }) + }) +} +/** @name 查询 AI-Forge 单个详情 */ +export const grpcGetAIForge: APIFunc = (param, hiddenError) => { + return new Promise(async (resolve, reject) => { + ipcRenderer + .invoke('GetAIForge', param) + .then(resolve) + .catch((e) => { + if (!hiddenError) yakitNotify('error', 'grpcGetAIForge 查询Forge详情失败:' + e) + reject(e) + }) + }) +} + +/** @name 获取 HTTP 流列表 */ +export const grpcQueryHTTPFlows: APIFunc = async ( + param, + hiddenError, +) => { + return new Promise(async (resolve, reject) => { + ipcRenderer + .invoke('QueryHTTPFlows', param) + .then(resolve) + .catch((e) => { + if (!hiddenError) yakitNotify('error', 'grpcQueryHTTPFlows 查询HTTP流失败:' + e) + reject(e) + }) + }) +} +// #endregion + +export const grpcGetRandomAIMaterials: APIFunc = ( + param, + hiddenError, +) => { + return new Promise(async (resolve, reject) => { + ipcRenderer + .invoke('GetRandomAIMaterials', param) + .then(resolve) + .catch((e) => { + if (!hiddenError) yakitNotify('error', '查询 GetRandomAIMaterials 失败:' + e) + reject(e) + }) + }) +} + +export const grpcExportAILogs: APIFunc = (param, hiddenError) => { + return new Promise(async (resolve, reject) => { + ipcRenderer + .invoke('ExportAILogs', param) + .then(resolve) + .catch((e) => { + if (!hiddenError) yakitNotify('error', '导出 AI 日志失败:' + e) + reject(e) + }) + }) +} + +// export const grpcQueryAIFocus: APIOptionalFunc< +// QueryAIFocusRequest, +// QueryAIFocusResponse +// > = (param, hiddenError) => { +// return new Promise(async (resolve, reject) => { +// ipcRenderer +// .invoke('QueryAIFocus', param) +// .then(resolve) +// .catch((e) => { +// if (!hiddenError) +// yakitNotify('error', 'QueryAIFocus 查询详情失败:' + e); +// reject(e); +// }); +// }); +// }; diff --git a/src/pages/AIAgent/ai-agent/historyChat/HistoryChat.module.scss b/src/pages/AIAgent/ai-agent/historyChat/HistoryChat.module.scss new file mode 100644 index 00000000..e61e117b --- /dev/null +++ b/src/pages/AIAgent/ai-agent/historyChat/HistoryChat.module.scss @@ -0,0 +1,69 @@ +.history-chat { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + gap: 12px; + background-color: var(--Colors-Use-Basic-Background); + + .header-wrapper { + padding: 8px 12px 0 12px; + display: flex; + flex-direction: column; + gap: 12px; + + .haeder-first { + display: flex; + justify-content: space-between; + align-items: center; + .first-title { + display: flex; + align-items: center; + gap: 4px; + + color: var(--Colors-Use-Neutral-Text-3-Secondary); + font-size: 12px; + font-weight: 500; + } + + .header-actions { + display: flex; + align-items: center; + gap: 8px; + } + } + + .header-second { + .search-icon { + svg { + width: 16px; + height: 16px; + color: var(--Colors-Use-Neutral-Disable); + } + } + } + } + + .content { + flex: 1; + overflow: hidden auto; + } +} + +.clear-confirm-trigger { + width: 100%; + height: 100%; + display: flex; + align-items: center; +} + + +// .history-item-extra-tooltip { +// padding-bottom: 4px; +// :global { +// .ant-tooltip-arrow { +// right: 4px; +// bottom: 1px; +// } +// } +// } diff --git a/src/pages/AIAgent/ai-agent/historyChat/HistoryChat.tsx b/src/pages/AIAgent/ai-agent/historyChat/HistoryChat.tsx new file mode 100644 index 00000000..dd24bfab --- /dev/null +++ b/src/pages/AIAgent/ai-agent/historyChat/HistoryChat.tsx @@ -0,0 +1,186 @@ +import { memo, useState } from 'react' +import useAIAgentStore from '../useContext/useStore' +import useAIAgentDispatcher from '../useContext/useDispatcher' +import { useDebounce, useMemoizedFn } from 'ahooks' +import { yakitNotify } from '@/utils/notification' +import { ReActChatEventEnum } from '../defaultConstant' +import { OutlineMessageCirclePlusIcon, OutlineSearchIcon } from '@/assets/icon/outline' +import { Tooltip } from 'antd' +import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton' +import { YakitDropdownMenu } from '@/compoments/yakitUI/YakitDropdownMenu/YakitDropdownMenu' +import { YakitPopconfirm } from '@/compoments/yakitUI/YakitPopconfirm/YakitPopconfirm' +import { YakitRoundCornerTag } from '@/compoments/yakitUI/YakitRoundCornerTag/YakitRoundCornerTag' +import { YakitInput } from '@/compoments/yakitUI/YakitInput/YakitInput' +import styles from './HistoryChat.module.scss' +import type { AIAgentTriggerEventInfo } from '../aiAgentType' +import emiter from '@/utils/eventBus/eventBus' +import type { AIChatInfo } from '../type/aiChat' +import { SideSettingButton } from '../aiChatWelcome/AIChatWelcome' +import HistoryChatList, { DAY_MS, getChatTimestamp } from './HistoryChatList/HistoryChatList' +import { deleteSession } from '@/apis/AiEventApi' + +const clearLocalChats = (sessions: AIChatInfo[]) => + emiter.emit('onDelChats', JSON.stringify(sessions.map((item) => item.run_id))) + +const renderClearConfirm = (label: string, title: string, onConfirm: () => void) => { + return ( + +
    e.stopPropagation()}> + {label} +
    +
    + ) +} + +/** 向对话框组件进行事件触发的通信 */ +export const onNewChat = () => { + const info: AIAgentTriggerEventInfo = { type: ReActChatEventEnum.NEW_CHAT } + emiter.emit('onReActChatEvent', JSON.stringify(info)) +} +const HistoryChat = memo(() => { + const { chats, activeChat } = useAIAgentStore() + const { setChats, setActiveChat } = useAIAgentDispatcher() + + const [search, setSearch] = useState('') + const searchDebounce = useDebounce(search, { wait: 500 }) + + const [clearLoading, setClearLoading] = useState(false) + const handleClearAllChat = useMemoizedFn(async () => { + if (clearLoading) return + if (chats.length === 0) { + yakitNotify('info', '暂无可清除的会话') + return + } + + setClearLoading(true) + try { + await deleteSession({ DeleteAll: true }) + clearLocalChats(chats) + onNewChat() + setActiveChat?.(undefined) + setChats?.([]) + setSearch('') + yakitNotify('success', '已清空全部会话') + } catch (e) { + yakitNotify('error', '清除会话失败:' + e) + } finally { + setClearLoading(false) + } + }) + + const handleClearChatByDays = useMemoizedFn(async (days: number) => { + if (clearLoading) return + + const cutoffTimestamp = Date.now() - days * DAY_MS + const deletedChats = chats.filter((item) => getChatTimestamp(item) <= cutoffTimestamp) + + if (deletedChats.length === 0) { + yakitNotify('info', `暂无${days}天前的会话`) + return + } + setClearLoading(true) + try { + await deleteSession({ Filter: { SessionID: deletedChats.map((item) => item.run_id) } }) + + clearLocalChats(deletedChats) + + const nextChats = chats.filter((item) => getChatTimestamp(item) > cutoffTimestamp) + const activeDeleted = !!activeChat && deletedChats.some((item) => item.run_id === activeChat.run_id) + + if (nextChats.length === 0) { + onNewChat() + setActiveChat?.(undefined) + setSearch('') + } else if (activeDeleted) { + setActiveChat?.(nextChats[0]) + } + + setChats?.(nextChats) + yakitNotify('success', `已清除${days}天前的会话`) + } catch (e) { + yakitNotify('error', '清除会话失败:' + e) + } finally { + setClearLoading(false) + } + }) + + return ( +
    +
    +
    +
    + 历史会话 + {chats.length} +
    +
    + + handleClearChatByDays(1), + ), + }, + { + key: '7', + label: renderClearConfirm('一周前', '确定要清除一周前的会话吗?此操作不可恢复', () => + handleClearChatByDays(7), + ), + }, + { + key: '30', + label: renderClearConfirm('30天前', '确定要清除30天前的会话吗?此操作不可恢复', () => + handleClearChatByDays(30), + ), + }, + { type: 'divider' }, + { + key: 'all', + label: renderClearConfirm('清空全部', '确定要清除全部会话吗?此操作不可恢复', handleClearAllChat), + }, + ], + }} + dropdown={{ + trigger: ['click'], + placement: 'bottomRight', + disabled: clearLoading || chats.length === 0, + }} + > + + + 删除 + + + + + } onClick={() => onNewChat()} /> + + +
    +
    + +
    + } + placeholder="请输入关键词搜索" + allowClear + value={search} + onChange={(e) => setSearch(e.target.value)} + /> +
    +
    + +
    + +
    +
    + ) +}) + +export default HistoryChat diff --git a/src/pages/AIAgent/ai-agent/historyChat/HistoryChatList/HistoryChatList.module.scss b/src/pages/AIAgent/ai-agent/historyChat/HistoryChatList/HistoryChatList.module.scss new file mode 100644 index 00000000..41c07f14 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/historyChat/HistoryChatList/HistoryChatList.module.scss @@ -0,0 +1,99 @@ +.history-chat-list { + height: 100%; + padding-left: 12px; + padding-right: 12px; + overflow-y: auto; + .history-group { + & + .history-group { + margin-top: 12px; + } + } + .history-group-title { + padding: 0px 12px; + color: var(--Colors-Use-Neutral-Text-3-Secondary); + font-size: 11px; + font-weight: 500; + line-height: 16px; + } + .history-item { + height: 32px; + padding: 0 8px; + display: flex; + justify-content: space-between; + align-items: center; + border: 1px solid transparent; + border-radius: 4px; + cursor: pointer; + .item-info { + flex: 1; + overflow: hidden; + display: flex; + align-items: center; + gap: 4px; + .item-icon { + width: 24px; + height: 24px; + display: flex; + justify-content: center; + align-items: center; + svg { + width: 16px; + height: 16px; + color: var(--Colors-Use-Neutral-Disable); + } + } + .info-title { + flex: 1; + overflow: hidden; + color: var(--Colors-Use-Neutral-Text-2-Primary); + font-size: 12px; + font-weight: 600; + line-height: 16px; + } + } + .item-extra { + display: none; + align-items: center; + gap: 4px; + .del-icon { + svg { + color: var(--Colors-Use-Error-Primary); + } + } + } + } + .history-item:hover { + background: var(--Colors-Use-Neutral-Bg-Hover); + .item-extra { + display: flex; + } + } + .history-item-active { + border-color: var(--Colors-Use-Main-Border); + background: var(--Colors-Use-Main-Bg); + .item-info { + .item-icon { + svg { + color: var(--Colors-Use-Main-Primary); + } + } + .info-title { + color: var(--Colors-Use-Main-Primary); + } + } + } + .history-item:hover.history-item-active { + background: var(--Colors-Use-Main-Bg); + border-color: var(--Colors-Use-Main-Border); + .item-info { + .item-icon { + svg { + color: var(--Colors-Use-Main-Primary); + } + } + .info-title { + color: var(--Colors-Use-Main-Primary); + } + } + } +} diff --git a/src/pages/AIAgent/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx b/src/pages/AIAgent/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx new file mode 100644 index 00000000..62734655 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx @@ -0,0 +1,276 @@ +import { useMemo, useRef, useState, type FC } from 'react' +import styles from './HistoryChatList.module.scss' +import { OutlinePencilaltIcon, OutlineTrashIcon } from '@/assets/icon/outline' +import { SolidChatalt2Icon } from '@/assets/icon/solid' +import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton' +import { YakitPopconfirm } from '@/compoments/yakitUI/YakitPopconfirm/YakitPopconfirm' +import { Tooltip } from 'antd' +import classNames from 'classnames' +import dayjs from 'dayjs' +import { YakitAIAgentPageID } from '../../defaultConstant' +import { EditChatNameModal } from '../../UtilModals' +import type { AIChatInfo } from '../../type/aiChat' +import { useInfiniteScroll, useMemoizedFn } from 'ahooks' +// import { grpcDeleteAISession, grpcUpdateAISessionTitle } from '../../grpc' +import useAIAgentStore from '../../useContext/useStore' +import useAIAgentDispatcher from '../../useContext/useDispatcher' +import { yakitNotify } from '@/utils/notification' +import { onNewChat } from '../HistoryChat' +import emiter from '@/utils/eventBus/eventBus' +import { deleteSession } from '@/apis/AiEventApi' + +export const HOUR_MS = 60 * 60 * 1000 +export const DAY_MS = 24 * HOUR_MS +export const WEEK_MS = 7 * DAY_MS +export const THIRTY_DAYS_MS = 30 * DAY_MS + +const CHAT_GROUPS = [ + { key: 'justNow', label: '刚刚' }, + { key: 'oneHour', label: '一小时前' }, + { key: 'oneDay', label: '一天前' }, + { key: 'oneWeek', label: '一周前' }, + { key: 'thirtyDays', label: '30天前' }, +] as const + +type ChatGroupKey = (typeof CHAT_GROUPS)[number]['key'] + +export const normalizeTimestamp = (timestamp?: number | string) => { + if (!timestamp) return 0 + if (typeof timestamp === 'number') { + return timestamp < 1e12 ? timestamp * 1000 : timestamp + } + const trimmed = timestamp.trim() + if (!trimmed) return 0 + if (/^\d+$/.test(trimmed)) { + const value = Number(trimmed) + return value < 1e12 ? value * 1000 : value + } + const parsed = dayjs(trimmed) + return parsed.isValid() ? parsed.valueOf() : 0 +} + +export const getChatTimestamp = (item: AIChatInfo) => { + return normalizeTimestamp(item.created_at) +} + +const getChatGroupKey = (timestamp?: number | string): ChatGroupKey => { + const time = normalizeTimestamp(timestamp) + const diff = Math.max(Date.now() - time, 0) + + if (diff <= HOUR_MS) return 'justNow' + if (diff <= DAY_MS) return 'oneHour' + if (diff <= WEEK_MS) return 'oneDay' + if (diff <= THIRTY_DAYS_MS) return 'oneWeek' + return 'thirtyDays' +} + +const getNextActiveChat = (chats: AIChatInfo[], currentIndex: number) => { + const prev = chats[currentIndex - 1] + const next = chats[currentIndex + 1] + return prev ?? next +} + +const updateChatTitle = (list: AIChatInfo[], info: AIChatInfo) => { + return list.map((item) => { + if (item.run_id === info.run_id) { + return info + } + return item + }) +} + +const HistoryChatList: FC<{ + search: string +}> = ({ search }) => { + const { chats, activeChat } = useAIAgentStore() + const { setChats, setActiveChat, getChats } = useAIAgentDispatcher() + const listRef = useRef(null) + const chatTotalRef = useRef(0) + const editInfo = useRef() + const [delLoading, setDelLoading] = useState([]) + const [editShow, setEditShow] = useState(false) + + const activeSessionId = useMemo(() => { + return activeChat?.run_id || '' + }, [activeChat]) + + const { loading } = useInfiniteScroll( + async () => { + // const total = await loadHistoryData?.() + // chatTotalRef.current = total ?? 0 + return { list: [] } + }, + { + target: listRef, + isNoMore: () => (getChats?.().length || 0) >= chatTotalRef.current, + threshold: 100, + }, + ) + + const handleOpenEditName = useMemoizedFn((info: AIChatInfo) => { + if (editShow) return + editInfo.current = info + setEditShow(true) + }) + + const showHistory = useMemo(() => { + if (!search) return chats + return chats.filter((item) => item.title.toLowerCase().includes(search.toLowerCase())) + }, [chats, search]) + + const groupedHistory = useMemo(() => { + const groupMap = CHAT_GROUPS.reduce>( + (acc, item) => { + acc[item.key] = [] + return acc + }, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + {} as Record, + ) + + showHistory.forEach((item) => { + const groupKey = getChatGroupKey(getChatTimestamp(item)) + groupMap[groupKey].push(item) + }) + + return CHAT_GROUPS.map((item) => ({ + ...item, + list: groupMap[item.key], + })).filter((item) => item.list.length > 0) + }, [showHistory]) + + const handleCallbackEditName = useMemoizedFn(async (result: boolean, info?: AIChatInfo) => { + if (result && info) { + try { + // await grpcUpdateAISessionTitle({ SessionID: info.SessionID, Title: info.Title }) + setChats?.((old) => updateChatTitle(old, info)) + } catch (error) { + yakitNotify('error', '修改对话标题失败:' + error) + } + } + setEditShow(false) + editInfo.current = undefined + }) + + const handleDeleteChat = useMemoizedFn(async (info: AIChatInfo) => { + const { run_id } = info + const isLoading = delLoading.includes(run_id) + if (isLoading) return + const findIndex = chats.findIndex((item) => item.run_id === run_id) + if (findIndex === -1) { + yakitNotify('error', '未找到对应的对话') + return + } + setDelLoading((old) => [...old, run_id]) + + const newChats = chats.filter((item) => item.run_id !== run_id) + let active: AIChatInfo | undefined + if (newChats.length === 0) { + onNewChat() + } else { + active = getNextActiveChat(chats, findIndex) + } + + setChats && setChats(newChats) + + if (activeSessionId === run_id && active) { + handleSetActiveChat(active) + } + + try { + deleteSession({ Filter: { SessionID: [run_id] } }) + emiter.emit('onDelChats', JSON.stringify([run_id])) + } catch (error) { + yakitNotify('error', '删除会话失败:' + error) + } finally { + setDelLoading((old) => old.filter((el) => el !== run_id)) + } + }) + + const handleSetActiveChat = useMemoizedFn((info: AIChatInfo) => { + // 暂时性逻辑,因为老版本的对话信息里没有请求参数,导致在新版本无法使用对话里的重新执行功能 + // 所以会提示警告,由用户决定是否删除历史对话 + // if (!info.request) { + // yakitNotify("warning", "当前对话无请求参数信息,无法使用重新执行功能") + // } + setActiveChat && setActiveChat(info) + }) + + return ( +
    + {groupedHistory.map((group) => { + return ( +
    +
    {group.label}
    + {group.list.map((item) => { + const { run_id, title } = item + const delStatus = delLoading.includes(run_id) + return ( +
    handleSetActiveChat(item)} + > +
    +
    + +
    +
    + {title} +
    +
    + +
    + + } + onClick={(e) => { + e.stopPropagation() + handleOpenEditName(item) + }} + /> + + { + e?.stopPropagation() + handleDeleteChat(item) + }} + > + } + onClick={(e) => e.stopPropagation()} + /> + +
    +
    + ) + })} +
    + ) + })} + {loading &&
    加载中...
    } + + {editInfo.current && ( + + )} +
    + ) +} + +export default HistoryChatList diff --git a/src/pages/AIAgent/ai-agent/store/ChatDataStore.ts b/src/pages/AIAgent/ai-agent/store/ChatDataStore.ts new file mode 100644 index 00000000..d473f49b --- /dev/null +++ b/src/pages/AIAgent/ai-agent/store/ChatDataStore.ts @@ -0,0 +1,183 @@ +import type { SetStateAction } from 'react' +import type { AIChatData } from '../type/aiChat' +import type { AIChatQSData, ReActChatBaseInfo } from '@/pages/AIAgent/ai-re-act/hooks/aiRender' +import { AIModelTypeEnum } from '../defaultConstant' + +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P] +} + +interface GetContentMapParams { + session: string + chatType: ReActChatBaseInfo['chatType'] + mapKey: string +} + +/** 获取精确类型 */ +const getExactType: (value: any) => string = (value: any) => { + return Object.prototype.toString.call(value).slice(8, -1) +} + +export class ChatDataStore { + private map = new Map() + + /** 创建新的聊天数据 */ + public create(session: string): AIChatData { + if (this.map.has(session)) { + throw new Error(`Session: ${session} already exists`) + } + + const newData = this.initDefaultData() + this.map.set(session, newData) + return newData + } + + /** 判断指定聊天数据是否存在 */ + public has(session: string): boolean { + return this.map.has(session) + } + + /** 获取指定聊天数据 */ + public get(session: string): AIChatData | undefined { + return this.map.get(session) + } + + /** 获取会话聊天列表的数据 */ + public getContentMap({ session, chatType, mapKey }: GetContentMapParams): AIChatQSData | undefined { + const chatData = this.get(session) + if (!chatData) return undefined + try { + if (chatType === 'reAct') { + return chatData.casualChat.contents.get(mapKey) + } else if (chatType === 'task') { + return chatData.taskChat.contents.get(mapKey) + } + } catch (error) { + return undefined + } + } + + /** 设置指定聊天数据 */ + public set(session: string, value: SetStateAction): void { + const prev = this.map.get(session) + if (!prev) { + throw new Error(`Session: ${session} does not exist`) + } + + console.log('prev:', prev, value) + const next = typeof value === 'function' ? value(prev) : value + this.map.set(session, next) + } + + /** + * 增量更新指定聊天数据 + * @attention 增量更新逻辑只支持第一层和第二层的属性更新,超出需要重新开发逻辑 + */ + public updater(session: string, updateData: DeepPartial): void { + const prev = this.map.get(session) + if (!prev) { + throw new Error(`Session: ${session} does not exist`) + } + + const result = { ...prev } + + for (const key in updateData) { + if (updateData.hasOwnProperty(key)) { + // @ts-ignore + const updateValue = updateData[key] + + if (getExactType(updateValue) === 'Object' && !!updateValue) { + // 如果是对象且原对象有对应属性,进行深层更新 + if ( + key in result && + // @ts-ignore + getExactType(result[key]) === 'Object' + ) { + // @ts-ignore + result[key] = { ...result[key], ...updateValue } + } else { + // 直接赋值 + // @ts-ignore + result[key] = { ...updateValue } + } + } else { + // 直接更新一层属性 + // @ts-ignore + result[key] = updateValue + } + } + } + + this.map.set(session, result) + } + + /** 删除指定聊天数据 */ + public remove(session: string): void { + this.map.delete(session) + } + + /** 清空所有聊天数据 */ + public clear(): void { + this.map.clear() + } + + /** 生成初始化默认数据 */ + private initDefaultData(): AIChatData { + return { + coordinatorIDs: [], + runTimeIDs: [], + yakExecResult: { card: [], execFileRecord: new Map(), yakExecResultLogs: [] }, + aiPerfData: { + consumption: { + input_consumption: 0, + output_consumption: 0, + consumption_uuid: '', + tier_consumption: { + [AIModelTypeEnum.TierIntelligent]: { + input_consumption: 0, + output_consumption: 0, + }, + [AIModelTypeEnum.TierLightweight]: { + input_consumption: 0, + output_consumption: 0, + }, + [AIModelTypeEnum.TierVision]: { + input_consumption: 0, + output_consumption: 0, + }, + }, + }, + pressure: { + [AIModelTypeEnum.TierIntelligent]: [], + [AIModelTypeEnum.TierLightweight]: [], + [AIModelTypeEnum.TierVision]: [], + }, + firstCost: { + [AIModelTypeEnum.TierIntelligent]: [], + [AIModelTypeEnum.TierLightweight]: [], + [AIModelTypeEnum.TierVision]: [], + }, + totalCost: { + [AIModelTypeEnum.TierIntelligent]: [], + [AIModelTypeEnum.TierLightweight]: [], + [AIModelTypeEnum.TierVision]: [], + }, + }, + casualChat: { + elements: [], + contents: new Map(), + }, + taskChat: { + plan: [], + elements: [], + contents: new Map(), + }, + grpcFolders: [], + reActTimelines: [], + } + } +} + +export const aiChatDataStore = new ChatDataStore() +export const knowledgeBaseDataStore = new ChatDataStore() +export const histroyAiStore = new ChatDataStore() diff --git a/src/pages/AIAgent/ai-agent/store/tabSubscribe.ts b/src/pages/AIAgent/ai-agent/store/tabSubscribe.ts new file mode 100644 index 00000000..68ecddf9 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/store/tabSubscribe.ts @@ -0,0 +1,53 @@ +import type { YakitModalConfirmProps } from '@/compoments/yakitUI/YakitModal/YakitModalConfirm' +import { create } from 'zustand' +export interface YakitSecondaryConfirmProps extends Omit { + // 确认回调函数 + onOk: (m: any) => void + // 获取YakitModalConfirm弹窗实例 + getModal?: (m: any) => void +} + +/** + * @description 出现这个定义的原因,某些页面在关闭时,需要控制关闭确认是否提示,所以需要一个方法进行输出判断 + */ +export type SubscribeCloseType = YakitSecondaryConfirmProps | (() => Promise) + +/** + * @name 存放一级菜单各种操作时===二次确认提示的配置信息 + * @description Map结构存放的是各种情况的二次确认提示的配置信息(比如close,reset等等) + */ + +interface SubscribeCloseProps { + events: Map> + + getSubscribeClose: (key: string) => Record | undefined + setSubscribeClose: (key: string, p: Record) => void + removeSubscribeClose: (key: string) => void + + clearSubscribeClose: () => void +} +export const useSubscribeClose = create((set, get) => ({ + events: new Map(), + getSubscribeClose: (key) => get().events.get(key), + setSubscribeClose: (key, ev) => { + const newVal = get().events + newVal.set(key, ev) + set({ + events: newVal, + }) + }, + removeSubscribeClose: (key) => { + const newVal = get().events + newVal.delete(key) + set({ + events: newVal, + }) + }, + clearSubscribeClose: () => { + const newVal = get().events + newVal.clear() + set({ + events: newVal, + }) + }, +})) diff --git a/src/pages/AIAgent/ai-agent/template/template.module.scss b/src/pages/AIAgent/ai-agent/template/template.module.scss new file mode 100644 index 00000000..53f017af --- /dev/null +++ b/src/pages/AIAgent/ai-agent/template/template.module.scss @@ -0,0 +1,131 @@ +// #region QSInputTextarea +.qs-input-textarea { + padding: 0; + width: 100%; + min-height: 20px !important; + position: relative; +} +textarea.qs-input-textarea { + line-height: 20px; + color: var(--Colors-Use-Neutral-Text-1-Title); +} +// #endregion + +// #region QSInputTextarea +.ai-chat-textarea { + width: 100%; + min-height: 124px; + display: flex; + flex-direction: column; + gap: 2px; + + background: var(--Colors-Use-Neutral-Bg-Hover); + padding: 2px; + border-radius: 8px; + position: relative; + + .ai-chat-textarea-footer { + display: flex; + align-items: center; + gap: 4px; + height: 24px; + overflow: hidden; + + %self-adaptive { + display: flex; + overflow: hidden; + } + .review-rule-self-adaptive { + @extend %self-adaptive; + flex-shrink: 0; + } + .model-self-adaptive { + @extend %self-adaptive; + flex-shrink: 1; + min-width: 46px; + } + .focus-mode-self-adaptive { + @extend %self-adaptive; + flex-shrink: 9; + min-width: 68px; + } + } + + .divider-style { + width: 1px; + height: 12px; + background-color: var(--Colors-Use-Basic-Shadow); + } +} + +.ai-review-chat { + background: linear-gradient( + 135deg, + var(--Colors-Use-Success-Border) 0%, + var(--Colors-Use-Success-Focus) 40.44%, + var(--Colors-Use-Main-Focus) 80% + ); +} +.dragging-from-tree { + outline: 1px dashed var(--Colors-Use-Main-Primary); + border-radius: 8px; + .drag-hint { + position: absolute; + inset: 0; + z-index: 10; + border-radius: 8px; + + display: flex; + align-items: center; + justify-content: center; + + pointer-events: none; + + font-size: 14px; + color: var(--Colors-Use-Main-Primary); + background: var(--yakit-colors-Main-0); + backdrop-filter: blur(2px); + } +} +.textarea-wrapper { + display: flex; + flex-direction: column; + gap: 8px; + border: 1px solid var(--Colors-Use-Neutral-Border); + background: var(--Colors-Use-Basic-Background); + padding: 8px; + border-radius: 6px; + min-height: 94px; + .textarea-textarea { + max-height: 100px !important; + color: var(--Colors-Use-Neutral-Text-1-Title); + } + .footer { + display: flex; + gap: 4px; + justify-content: space-between; + align-items: center; + overflow: hidden; + height: 28px; + .footer-left { + display: flex; + align-items: center; + gap: 4px; + overflow: hidden; + } + .footer-right { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 4px; + overflow: hidden; + } + } +} + +.round-btn { + svg { + width: 14px; + } +} +// #endregion diff --git a/src/pages/AIAgent/ai-agent/template/template.tsx b/src/pages/AIAgent/ai-agent/template/template.tsx new file mode 100644 index 00000000..a3ac7d5e --- /dev/null +++ b/src/pages/AIAgent/ai-agent/template/template.tsx @@ -0,0 +1,355 @@ +import type { ReactNode } from 'react' +import React, { forwardRef, memo, useEffect, useImperativeHandle, useRef, useState } from 'react' +import type { + AIChatTextareaProps, + AIChatTextareaSubmit, + FileToChatQuestionList, + FooterLeftTypesComponentProps, + QSInputTextareaProps, +} from './type' +import { AIInputInnerFeatureEnum } from './type' +import { Input } from 'antd' +import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton' +import { OutlineArrowupIcon, OutlineAtsymbolIcon } from '@/assets/icon/outline' +import { useCreation, useInViewport, useMemoizedFn } from 'ahooks' +import type { TextAreaRef } from 'antd/lib/input/TextArea' +import classNames from 'classnames' +import styles from './template.module.scss' +import { AIMilkdownInput } from '../components/aiMilkdownInput/AIMilkdownInput' +import type { EditorMilkdownProps } from '@/compoments/MilkdownEditor/MilkdownEditorType' +import { callCommand, getMarkdown } from '@milkdown/kit/utils' +import useAIChatDrop from '../aiChatWelcome/hooks/useAIChatDrop' +import type { AIMentionCommandParams } from '../components/aiMilkdownInput/aiMilkdownMention/aiMentionPlugin' +import { aiMentionCommand } from '../components/aiMilkdownInput/aiMilkdownMention/aiMentionPlugin' +import emiter from '@/utils/eventBus/eventBus' +import type { AIAgentTriggerEventInfo } from '../aiAgentType' +import { extractDataWithMilkdown, setEditorValue } from '../components/aiMilkdownInput/utils' +import { editorViewCtx } from '@milkdown/kit/core' +import { convertKeyEventToKeyCombination } from '@/pages/AIAgent/utils/globalShortcutKey' +// import { YakitKeyBoard } from '@/pages/AIAgent/utils/globalShortcutKey/keyboard'; +import { AIModelSelect } from '../aiModelList/aiModelSelect/AIModelSelect' +import AIReviewRuleSelect from '@/pages/AIAgent/ai-re-act/aiReviewRuleSelect/AIReviewRuleSelect' +import { AIFocusMode } from '@/pages/AIAgent/ai-re-act/aiFocusMode/AIFocusMode' +import useAIAgentStore from '../useContext/useStore' +import { isString } from 'lodash' +// import type { OpenFileDropdownItem } from '../aiChatWelcome/OpenFileDropdown/OpenFileDropdown' +// import OpenFileDropdown from '../aiChatWelcome/OpenFileDropdown/OpenFileDropdown' +// import { UploadFileButton } from '@/pages/AIAgent/ai-re-act/aiReActChat/AIReActComponent' +import { insertAtCurrentPosition } from '../components/aiMilkdownInput/customPlugin' +import { YakitKeyBoard } from '../../enums/keyboard' + +/** @name AI-Agent专用Textarea组件,行高为20px */ +export const QSInputTextarea = memo( + forwardRef((props, ref) => { + const { className, ...rest } = props + + return ( + + ) + }), +) + +/** + * @name chat-问题输入框(带提交按钮) + * @description + */ +export const AIChatTextarea: React.FC = memo( + forwardRef((props, ref) => { + const { + loading, + inputFooterLeft, + inputFooterRight, + footer, + onSubmit, + className, + children, + defaultValue, + isOpen, + filterMentionType, + } = props + + const footerLeftTypes: FooterLeftTypesComponentProps[] = useCreation(() => { + if (props.footerLeftTypes?.length) { + const list = props.footerLeftTypes + .map((item) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + let node: FooterLeftTypesComponentProps = {} as FooterLeftTypesComponentProps + if (isString(item)) { + switch (item) { + case AIInputInnerFeatureEnum.AIReviewRuleSelect: + node = { + type: AIInputInnerFeatureEnum.AIReviewRuleSelect, + } + break + case AIInputInnerFeatureEnum.AIModelSelect: + node = { + type: AIInputInnerFeatureEnum.AIModelSelect, + props: { isOpen }, + } + break + case AIInputInnerFeatureEnum.AIFocusMode: + node = { + type: AIInputInnerFeatureEnum.AIFocusMode, + } + break + default: + break + } + } else { + node = item + } + return node + }) + .filter((ele) => !!ele?.type) + return list + } + return [ + { type: AIInputInnerFeatureEnum.AIReviewRuleSelect }, + { + type: AIInputInnerFeatureEnum.AIModelSelect, + props: { isOpen }, + }, + { type: AIInputInnerFeatureEnum.AIFocusMode }, + ] + }, [props.footerLeftTypes, isOpen]) + + const { setting } = useAIAgentStore() + + const [disabled, setDisabled] = useState(false) + + const { isHovering, dropRef } = useAIChatDrop({ + onFilesChange: (v) => onFilesChange(v), + }) + const [inViewport = true] = useInViewport(dropRef) + const editorMilkdown = useRef() + + useImperativeHandle(ref, () => { + return { + setMention: (v) => onSetMention(v), + setValue: (v) => onSetValue(v), + getValue: () => getMarkdownValue(), + editorMilkdown: editorMilkdown.current, + } + }, []) + // #region question-相关逻辑 + useEffect(() => { + if (inViewport) { + emiter.on('setAIInputByType', onSetAIInputByType) + return () => { + emiter.off('setAIInputByType', onSetAIInputByType) + } + } + }, [inViewport]) + + const onSetAIInputByType = useMemoizedFn((res) => { + try { + const data: AIAgentTriggerEventInfo = JSON.parse(res) + const { type } = data + switch (type) { + case 'mention': + // eslint-disable-next-line no-case-declarations + const params = data.params as AIMentionCommandParams + onSetMention(params) + break + + default: + break + } + } catch (error) {} + }) + + const handleSubmit = useMemoizedFn(() => { + const qs = getMarkdownValue() + if (!qs.trim() || !editorMilkdown.current) return + const { mentions, plainText } = extractDataWithMilkdown(editorMilkdown.current) + const value: AIChatTextareaSubmit = { + qs: plainText, + mentionList: mentions, + showQS: qs, + focusMode, + } + onSubmit && onSubmit(value) + }) + // #endregion + + // #region 编辑器-相关逻辑 + + const handleSetTextareaFocus = useMemoizedFn(() => { + editorMilkdown.current?.action((ctx) => { + const view = ctx.get(editorViewCtx) + view.focus() + }) + }) + + const onUpdateEditor = useMemoizedFn((editor: EditorMilkdownProps) => { + editorMilkdown.current = editor + }) + + const onFilesChange = useMemoizedFn((files: FileToChatQuestionList[]) => { + for (const item of files) { + onSetMention({ + mentionId: item.path, + mentionType: item.isFolder ? 'folder' : 'file', + mentionName: item.path, + }) + } + }) + /** 插入提及数据 */ + const onSetMention = useMemoizedFn((params: AIMentionCommandParams) => { + switch (params.mentionType) { + case 'focusMode': + onMemfitExtra(params) + break + default: + editorMilkdown.current?.action(callCommand(aiMentionCommand.key, params)) + break + } + }) + /** 设置编辑器值 */ + const onSetValue = useMemoizedFn((value: string) => { + if (!editorMilkdown.current) return + setEditorValue(editorMilkdown.current, value) + }) + const getMarkdownValue = useMemoizedFn(() => { + const value = editorMilkdown.current?.action(getMarkdown()) || '' + return value + }) + + const onUpdateContent = useMemoizedFn((value: string) => { + setDisabled(!value.trim()) + }) + // #endregion + const handleTextareaKeyDown = useMemoizedFn((e) => { + const keys = convertKeyEventToKeyCombination(e) + if (!e.nativeEvent?.isComposing && keys?.join() === YakitKeyBoard.Enter) { + e.stopPropagation() + e.preventDefault() + handleSubmit() + } + }) + const [focusMode, setFocusMode] = useState() + + const onMemfitExtra = useMemoizedFn((value: AIMentionCommandParams) => { + setFocusMode(value.mentionName) + }) + + const renderFooterLeftTypes = useMemoizedFn((types: FooterLeftTypesComponentProps[]) => { + let node: ReactNode[] = [] + types?.forEach((item, index) => { + if (index > 0 && index < types.length) { + node.push(
    ) + } + switch (item.type) { + case AIInputInnerFeatureEnum.AIReviewRuleSelect: + node.push( + item.component || ( + + ), + ) + break + case AIInputInnerFeatureEnum.AIModelSelect: + node.push( + item.component || ( + + ), + ) + break + case AIInputInnerFeatureEnum.AIFocusMode: + node.push( + item.component || ( + + ), + ) + break + default: + break + } + }) + return node + }) + // const onSetFileMention = useMemoizedFn((data: OpenFileDropdownItem) => { + // onSetMention({ + // mentionId: data.path, + // mentionType: data.isFolder ? 'folder' : 'file', + // mentionName: data.path, + // }) + // }) + const onMention = useMemoizedFn(() => { + editorMilkdown.current?.action(callCommand(insertAtCurrentPosition.key, '@')) + }) + return ( +
    + {isHovering &&
    松开以添加到对话
    } +
    + +
    + {inputFooterLeft ?? ( +
    + } onClick={onMention} /> + {/* + + */} +
    + )} + +
    + {inputFooterRight} + } + onClick={(e) => { + e.stopPropagation() + handleSubmit() + }} + /> +
    +
    +
    +
    + {footer ?? <>{renderFooterLeftTypes(footerLeftTypes)}} +
    + {children} +
    + ) + }), +) diff --git a/src/pages/AIAgent/ai-agent/template/type.ts b/src/pages/AIAgent/ai-agent/template/type.ts new file mode 100644 index 00000000..2a4754fc --- /dev/null +++ b/src/pages/AIAgent/ai-agent/template/type.ts @@ -0,0 +1,76 @@ +import type { ReactNode } from 'react'; +import type { TextAreaProps } from 'antd/lib/input'; +import type { AIMentionCommandParams } from '../components/aiMilkdownInput/aiMilkdownMention/aiMentionPlugin'; +import type { AIChatMentionProps } from '../components/aiChatMention/type'; +import type { AIReviewRuleSelectProps } from '@/pages/AIAgent/ai-re-act/aiReviewRuleSelect/type'; +import type { AIModelSelectProps } from '../aiModelList/aiModelSelect/AIModelSelectType'; +import type { AIFocusModeProps } from '@/pages/AIAgent/ai-re-act/aiFocusMode/type'; +import type { EditorMilkdownProps } from '../../enums/external'; + +export type QSInputTextareaProps = Omit; + +export interface AIChatTextareaSubmit { + /** 传给后端的内容 */ + qs: string; + /** 前端展示的md格式 */ + showQS?: string; + mentionList?: AIMentionCommandParams[]; + focusMode?: string; +} +export interface AIChatTextareaRefProps { + setMention: (v: AIMentionCommandParams) => void; + setValue: (v: string) => void; + getValue: () => void; + editorMilkdown?: EditorMilkdownProps; +} +export enum AIInputInnerFeatureEnum { + AIReviewRuleSelect = 'AIReviewRuleSelect', + AIModelSelect = 'AIModelSelect', + AIFocusMode = 'AIFocusMode', +} +export type AIInputInnerFeature = `${AIInputInnerFeatureEnum}`; +interface FooterLeftTypesBase { + type: T; + props?: U; + component?: ReactNode; +} +type AIReviewRuleSelectType = FooterLeftTypesBase< + AIInputInnerFeatureEnum.AIReviewRuleSelect, + AIReviewRuleSelectProps +>; +type AIModelSelectType = FooterLeftTypesBase< + AIInputInnerFeatureEnum.AIModelSelect, + AIModelSelectProps +>; +type AIFocusModeType = FooterLeftTypesBase< + AIInputInnerFeatureEnum.AIFocusMode, + AIFocusModeProps +>; +export type FooterLeftTypesComponentProps = + | AIReviewRuleSelectType + | AIModelSelectType + | AIFocusModeType; +export interface AIChatTextareaProps { + ref?: React.ForwardedRef; + /** 提交按钮的 loading 状态 */ + loading?: boolean; + /** 输入框左下角 */ + inputFooterLeft?: ReactNode; + /** 输入框右下角 */ + inputFooterRight?: ReactNode; + /** 底部 */ + footer?: ReactNode; + onSubmit?: (v: AIChatTextareaSubmit) => void; + className?: string; + children?: ReactNode; + defaultValue?: string; + /** ai模型不存在时,是否弹窗 */ + isOpen?: boolean; + filterMentionType?: AIChatMentionProps['filterMode']; + footerLeftTypes?: (AIInputInnerFeature | FooterLeftTypesComponentProps)[]; +} + +export interface FileToChatQuestionList { + path: string; + isFolder: boolean; +} diff --git a/src/pages/AIAgent/ai-agent/type/aiChat.d.ts b/src/pages/AIAgent/ai-agent/type/aiChat.d.ts new file mode 100644 index 00000000..99c37a85 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/type/aiChat.d.ts @@ -0,0 +1,40 @@ +import type { UseChatIPCState } from '@/pages/AIAgent/ai-re-act/hooks/type' +import type { AIChatQSData } from '@/pages/AIAgent/ai-re-act/hooks/aiRender' +import type { AIAgentGrpcApi } from '../../ai-re-act/hooks/grpcApi' + +export interface AIChatData { + /** 记录数据里所有的coordinatorIDs */ + coordinatorIDs: string[] + /** 记录数据里所有的runTimeIDs */ + runTimeIDs: UseChatIPCState['runTimeIDs'] + yakExecResult: UseChatIPCState['yakExecResult'] + /** 性能相关数据 */ + aiPerfData: { + /** 消耗Token */ + consumption: AIAgentGrpcApi.Consumption + /** 上下文压力 */ + pressure: Record + /** 首字符响应耗时 */ + firstCost: Record + /** 总对话耗时 */ + totalCost: Record + } + /** 自由对话(ReAct)会话 */ + casualChat: UseChatIPCState['casualChat'] & { + /** 会话内每条信息的详情 */ + contents: Map + } + taskChat: UseChatIPCState['taskChat'] & { contents: Map } + grpcFolders: UseChatIPCState['grpcFolders'] + reActTimelines: UseChatIPCState['reActTimelines'] +} + +/** UI-chat 信息 */ +export interface AIChatInfo { + /** 对话名称 */ + title: string + /** 创建时间 */ + created_at: string + /** 会话 run_id */ + run_id: string +} diff --git a/src/pages/AIAgent/ai-agent/type/aiMCP.ts b/src/pages/AIAgent/ai-agent/type/aiMCP.ts new file mode 100644 index 00000000..5dee2a04 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/type/aiMCP.ts @@ -0,0 +1,66 @@ +import type { PaginationSchema } from '../../enums/invoker/schema'; + +export interface GetAllMCPServersRequest { + Keyword: string; + ID?: number; + Pagination: PaginationSchema; + IsShowToolList: boolean; +} + +export interface GetAllMCPServersResponse { + MCPServers: MCPServer[]; + Pagination: PaginationSchema; + Total: number; +} +export interface MCPServer { + ID: number; + Name: string; + Type: MCPServerType; + URL: string; + Command: string; + Enable: boolean; + Tools: MCPServerTool[]; + ErrorMsg: string; +} + +export interface MCPServerTool { + Name: string; + Description: string; + Params: MCPServerToolParamInfo[]; +} +export interface MCPServerToolParamInfo { + Type: string; + Description: string; + Default: string; + Required: string; + Name: string; +} + +export interface UpdateMCPServerRequest { + ID: number; + Name: string; + Type: MCPServerType; + URL: string; + Command: string; + Enable: boolean; +} +export interface MCPServerFormData { + Name: string; + Type: string; + URL: string; + Command: string; +} + +export interface AddMCPServerRequest { + Name: string; + Type: MCPServerType; + URL: string; + Command: string; + Enable: boolean; +} + +export interface DeleteMCPServerRequest { + ID: number; +} + +export type MCPServerType = 'sse' | 'stdio'; diff --git a/src/pages/AIAgent/ai-agent/type/aiModel.ts b/src/pages/AIAgent/ai-agent/type/aiModel.ts new file mode 100644 index 00000000..1f0b4425 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/type/aiModel.ts @@ -0,0 +1,110 @@ +import type { ThirdPartyApplicationConfig } from '@/compoments/configNetwork/ConfigNetworkPage'; +import type { AIGlobalConfig } from '../aiModelList/utils'; + +export interface GeneralResponse { + Ok: boolean; + Reason: string; +} +export interface LocalModelConfig { + Name: string; + Type: string; + FileName: string; + DownloadURL: string; + Description: string; + DefaultPort: number; + IsReady: boolean; + IsLocal: boolean; + Path: string; + Status: LocalModelStatus | null; +} + +export interface LocalModelStatus { + Status: 'stopped' | 'starting' | 'running' | 'stopping' | 'error'; + Host: string; + Port: number; + Model: string; + ModelPath: string; + LlamaServerPath: string; + ContextSize: number; + ContBatching: boolean; + BatchSize: number; + Threads: number; + Detached: boolean; + Debug: boolean; + Pooling: string; + StartupTimeout: number; + Args: string[]; +} + +export interface GetSupportedLocalModelsResponse { + Models: LocalModelConfig[]; +} +export type IsLlamaServerReadyResponse = GeneralResponse; +export interface InstallLlamaServerRequest { + Proxy: string; + token: string; +} +export interface DownloadLocalModelRequest { + ModelName: string; + Proxy: string; + token: string; +} +export interface IsLocalModelReadyRequest { + ModelName: string; +} +export type IsLocalModelReadyResponse = GeneralResponse; + +export interface StartLocalModelRequest { + token: string; + ModelName: string; + Host: string; + Port: number; +} + +export interface StopLocalModelRequest { + ModelName: string; +} + +export interface AddLocalModelRequest { + Name: string; + ModelType: string; + Description: string; + Path: string; +} + +export interface DeleteLocalModelRequest { + Name: string; + DeleteSourceFile: boolean; +} + +export type UpdateLocalModelRequest = AddLocalModelRequest; + +export interface GetAllStartedLocalModelsResponse { + Models: StartedLocalModelInfo[]; +} +export interface StartedLocalModelInfo { + Name: string; + ModelType: string; + Host: string; + Port: number; +} +export interface ClearAllModelsRequest { + DeleteSourceFile: boolean; +} +export interface GetAIModelListResponse { + onlineModels: ThirdPartyApplicationConfig[]; + localModels: StartedLocalModelInfo[]; +} + +export interface GetAIModelAvailableTotalResponse { + onlineModelsTotal: number; + localModelsTotal: number; + onlineModels: AIGlobalConfig; + localModels: StartedLocalModelInfo[]; +} + +export interface IsForcedSetAIModalRequest { + noDataCall?: (v: GetAIModelAvailableTotalResponse) => void; + haveDataCall?: (v: GetAIModelAvailableTotalResponse) => void; + mountContainer?: HTMLElement | null; +} diff --git a/src/pages/AIAgent/ai-agent/type/aiTool.ts b/src/pages/AIAgent/ai-agent/type/aiTool.ts new file mode 100644 index 00000000..e0cf74cc --- /dev/null +++ b/src/pages/AIAgent/ai-agent/type/aiTool.ts @@ -0,0 +1,62 @@ +import type { PaginationSchema } from '@/pages/invoker/schema'; + +export interface AITool { + Name: string; + Description: string; + Content: string; + ToolPath: string; + Keywords: string[]; + IsFavorite: boolean; + ID: number; + VerboseName: string; +} +export interface GetAIToolListRequest { + Query: string; + ToolName: string; + Pagination: PaginationSchema; + OnlyFavorites: boolean; + ToolID?: number; +} +export interface GetAIToolListResponse { + Tools: AITool[]; + Pagination: PaginationSchema; + Total: number; +} +export interface ToggleAIToolFavoriteRequest { + /** @deprecated */ + ToolName?: string; + ID: number; +} +export interface ToggleAIToolFavoriteResponse { + IsFavorite: boolean; + Message: string; +} +export interface SaveAIToolRequest { + Name: string; + Description: string; + Content: string; + ToolPath: string; + Keywords: string[]; +} +export interface SaveAIToolV2Response { + IsSuccess: boolean; + Message: string; + AITool: AITool; +} +export interface UpdateAIToolRequest extends SaveAIToolRequest { + ID: number; +} +export interface DeleteAIToolRequest { + /** @deprecated */ + ToolNames?: string; + IDs: number[]; +} +export interface AIToolGenerateMetadataRequest { + ToolName: string; + Content: string; +} +export interface AIToolGenerateMetadataResponse { + Name: string; + Description: string; + Keywords: string[]; +} diff --git a/src/pages/AIAgent/ai-agent/type/forge.ts b/src/pages/AIAgent/ai-agent/type/forge.ts new file mode 100644 index 00000000..435cb86e --- /dev/null +++ b/src/pages/AIAgent/ai-agent/type/forge.ts @@ -0,0 +1,74 @@ +import type { PaginationSchema } from '@/pages/invoker/schema' + +/** forge 详情数据字段 */ +export interface AIForge { + Id: number + ForgeName: string + // yak type is yak script, config type is empty + /** yak 类型为脚本代码, config 类型为空 */ + ForgeContent?: string + // yak or config + ForgeType: 'yak' | 'config' | 'skillmd' + Description?: string + // json config for UI + ParamsUIConfig?: string + /** @deprecated cli parameters */ + Params?: string + // for user preferences + UserPersistentData?: string + /** 可选,列表 */ + ToolNames?: string[] + /** 可选,手输 */ + ToolKeywords?: string[] + Action?: string + /** 可选,手输 */ + Tag?: string[] + // 初始提示语 + InitPrompt?: string + // 持久化提示语 + PersistentPrompt?: string + // 计划提示语 + PlanPrompt?: string + // 结果提示语 + ResultPrompt?: string + /** 给用户看的展示名称 */ + ForgeVerboseName?: string +} + +/** forge 相关的过滤条件 */ +export interface AIForgeFilter { + /** name 模糊搜索 */ + ForgeName?: string + ForgeNames?: string[] + ForgeType?: AIForge['ForgeType'] + /** 多个字段的内容进行模糊搜索 */ + Keyword?: string + Tag?: string + Id?: number +} + +/** forge 列表的请求参数 */ +export interface QueryAIForgeRequest { + Pagination: PaginationSchema + Filter?: AIForgeFilter +} +/** forge 列表的响应数据结构 */ +export interface QueryAIForgeResponse { + Pagination: PaginationSchema + Data: AIForge[] + Total: number +} +export interface GetAIForgeRequest { + ID?: number + ForgeName?: string +} + +export interface AIFocus { + Name: string + Description: string + OutputExamplePrompt: string + UsagePrompt: string +} +export interface QueryAIFocusResponse { + Data: AIFocus[] +} diff --git a/src/pages/AIAgent/ai-agent/type/resposeType.ts b/src/pages/AIAgent/ai-agent/type/resposeType.ts new file mode 100644 index 00000000..f72f459c --- /dev/null +++ b/src/pages/AIAgent/ai-agent/type/resposeType.ts @@ -0,0 +1,1724 @@ +/** + * This file was auto-generated by swagger-to-ts. + * Do not make direct changes to the file. + */ + +export declare namespace API { + export interface YakVersionsInfoResponse { + data: YakVersionsInfoDetail[] + } + export interface YakVersionsInfoDetail { + type: string + content: string + } + export interface YakitPluginParam { + field: string + default_value: string + type_verbose: string + field_verbose: string + help: string + required?: boolean + group?: string + extra_setting?: string + method_type?: string + json_schema?: string + suggestion_data_expression?: string + ui_schema?: string + } + export interface WsRequest { + /** + * 类型必传, messageLog 消息通知, notepad 共享记事本 + */ + messageType: string + params: object + /** + * 文档协作参数 + */ + yjsParams?: string + /** + * 当类型为notepad token必传 + */ + token?: string + /** + * notepad hash 当类型为notepad 此项必传 + */ + notepadHash?: string + } + export interface WebsocketFlowResponse extends Paging { + data: WebsocketFlowList[] + } + export interface WebsocketFlowList extends GormBaseModel, WebsocketFlowDetail {} + export interface WebsocketFlowDetail { + websocketRequestHash: string + frameIndex: number + fromServer: boolean + messageType: string + data: string + dataSizeVerbose: string + dataLength: number + dataVerbose: string + isJson: boolean + isProtobuf: boolean + } + export interface UserOrdinaryResponse { + data: UserList[] + } + export interface UserListResponse extends Paging { + data: UserList[] + } + export interface UserList { + id: number + created_at: number + updated_at: number + name: string + from_platform: string + email?: string + appid: string + head_img: string + role: string + uid?: string + /** + * 所属组织 + */ + department?: string + } + export interface UserInfoByToken { + token: string + } + export interface UserData { + from_platform: string + appid: string + head_img: string + name: string + token: string + role: string + user_id: number + uid?: string + /** + * 判断是否首次登录 + */ + loginTime?: number + /** + * 企业版用户是否有审核权限 + */ + checkPlugin: boolean + } + export interface UrmUserListResponse extends Paging { + data: UrmUserList[] + } + export interface UrmUserList { + id: number + created_at: number + user_name: string + head_img: string + from_platform: string + uid: string + department_id?: number + department_name?: string + role_id?: number + role_name?: string + department_parent_id?: number + department_parent_name?: string + } + export interface UrmLoginRequest { + user_name: string + pwd: string + } + export interface UrmEditListResponse { + data: UrmUserList + } + export interface UpUserInfoRequest { + old_pwd?: string + pwd?: string + confirm_pwd?: string + head_img?: string + } + export interface UpPluginsUserRequest { + uuid: string[] + user_id: number + } + export interface UpPluginsPrivateRequest { + uuid: string + is_private: boolean + } + export interface UploadDataResponseDetail { + userName: string + fileName: string + filePath: string + } + export interface UploadDataResponse extends Paging { + data: UploadDataList[] + } + export interface UploadDataList extends GormBaseModel, UploadDataResponseDetail {} + export interface UpdateUserRole { + appid: string[] + operation: string + role?: string + } + export interface UpdatePayloadRequest { + content: string + id: number + hitCount?: number + } + export interface TouristUsedDetailResponse extends Paging { + data: TouristUsedDetail[] + } + export interface TouristUsedDetailRequest { + /** + * ip搜索 + */ + ip?: string + name?: string + /** + * 自选起始时间 + */ + startTime?: number + /** + * 自选结束时间 + */ + endTime?: number + afterId?: number + beforeId?: number + } + export interface TouristUsedDetail extends GormBaseModel { + userName?: string + ip: string + totalRequestTimes: string + /** + * 所属组织 + */ + departmentName?: string + } + export interface TouristTimesResponse { + dayTimes: number + /** + * 当天时长增率 + */ + dayTimesGain: string + /** + * up 增 down 减 + */ + dayTimesGainUpOrDown: string + weekTimes: number + /** + * 本周时长增率 + */ + weekTimesGain: string + /** + * up 增 down 减 + */ + weekTimesGainUpOrDown: string + monthTimes: number + /** + * 本月时长增率 + */ + monthTimesGain: string + /** + * up 增 down 减 + */ + monthTimesGainUpOrDown: string + } + export interface TouristRequest { + macCode: string + token?: string + form?: string + } + export interface TouristIncrResponse { + data: TouristIncrDetail[] + } + export interface TouristIncrDetail { + /** + * 图表纵坐标 + */ + count: number + /** + * 图表横坐标 + */ + searchTime: string + } + export interface TouristCityResponse { + /** + * 总数 + */ + total: number + /** + * 日期 + */ + date: number + data: TouristCityCount[] + } + export interface TouristCityCount { + city: string + count: number + } + export interface TouristAndUserResponse { + /** + * 总游客数 + */ + touristTotal: number + /** + * 总登录用户数 + */ + loginTotal: number + /** + * 日增量 + */ + dayNew: number + /** + * 日增率 + */ + dayGain: string + /** + * up 增 down 减 + */ + dayGainUpOrDown: string + /** + * 周增量 + */ + weekNew: number + /** + * 周增率 + */ + weekGain: string + /** + * up 增 down 减 + */ + weekGainUpOrDown: string + /** + * 月增量 + */ + monthNew: number + /** + * 月增率 + */ + monthGain: string + /** + * up 增 down 减 + */ + monthGainUpOrDown: string + /** + * 机构总数 + */ + institutionTotal: number + } + export interface TouristActivityRateResponse { + /** + * 日活 + */ + dayActive: number + /** + * 日活增率 + */ + dayActiveGain: string + /** + * up 增 down 减 + */ + dayActiveGainUpOrDown: string + /** + * 周活 + */ + weekActive: number + /** + * 周活增率 + */ + weekActiveGain: string + /** + * up 增 down 减 + */ + weekActiveGainUpOrDown: string + /** + * 月活 + */ + monthActive: number + /** + * 月活增率 + */ + monthActiveGain: string + /** + * up 增 down 减 + */ + monthActiveGainUpOrDown: string + } + export interface TouristActiveResponse { + data: TouristActiveDetail[] + } + export interface TouristActiveDetail { + /** + * 图表纵坐标 + */ + count: number + /** + * 图表横坐标 + */ + searchTime: string + } + export interface TemplateDetail extends GormBaseModel { + type: string + name: string + content: string + } + export interface SystemConfigResponse extends Paging { + data: SystemConfigList[] + } + export interface SystemConfigList extends GormBaseModel, SystemConfigDetail {} + export interface SystemConfigDetail { + configName: string + isOpen: boolean + content: string + } + export interface SSARiskWhereRequest extends Pagination, SSARiskWhere {} + export interface SSARiskWhere { + id?: number[] + search?: string + programName?: string[] + codeSourceUrl?: string[] + riskType?: string[] + severity?: string[] + fromRule?: string[] + runtimeID?: string[] + resultID?: number[] + tags?: string[] + title?: string + hash?: string[] + } + export interface SSARiskResponse extends Paging { + data: SSARiskResponseData[] + } + export interface SSARiskResponseData extends GormBaseModel, SSARisk {} + export interface SSARiskRequest { + content: string + } + export interface SSARisk { + hash: string + programName: string + codeSourceUrl: string + codeRange: string + codeFragment: string + title: string + titleVerbose: string + riskType: string + riskTypeVerbose: string + details: string + severity: string + fromRule: string + runtimeID: string + isPotential: boolean + cve: string + cveAccessVector: string + cveAccessComplexity: string + tags: string + resultID: number + variable: string + index: number + functionName: string + line: number + solution: string + description: string + ssaRiskCreatedAt: number + } + export interface ShareResponse { + share_id: string + extract_code?: string + token?: string + } + export interface ShareRequest { + expired_time: number + module: string + share_content: string + pwd: boolean + share_id?: string + limit_num?: number + token?: string + } + export interface RoleListResponse extends Paging { + data: RoleList[] + } + export interface RoleList { + id: number + name: string + createdAt: number + checkPlugin: boolean + } + export interface RiskUploadResponse extends Paging { + data: RiskLists[] + } + export interface RiskTypes { + risk_type: string + } + export interface RiskTypeResponse { + data: RiskTypes[] + } + export interface RiskRequest { + projectName: string + content: string + /** + * 外部项目模块 + */ + externalModule?: string + /** + * 外部 项目编号 + */ + externalProjectCode?: string + } + export interface RiskLists extends GormBaseModel, RiskList {} + export interface RiskList { + hash: string + user_name: string + rish_hash: string + ip: string + ip_integer: number + url: string + port: number + host: string + title: string + title_verbose: string + risk_type: string + risk_type_verbose: string + parameter: string + payload: string + details: string + severity: string + from_yak_script: string + waiting_verified: boolean + reverse_token: string + runtime_id: string + quoted_request: string + quoted_response: string + is_potential: boolean + cve: string + description: string + solution: string + risk_created_at: number + project_name: string + /** + * 外部项目模块 + */ + externalModule?: string + /** + * 外部 项目编号 + */ + externalProjectCode?: string + /** + * 所属组织 + */ + department?: string + } + export interface RiskFeedBackResponse extends Paging { + data: RiskFeedBackData[] + } + export interface RiskFeedBackRequest extends Pagination, GetRiskWhere {} + export interface RiskFeedBackData extends GormBaseModel, RiskFeedBack {} + export interface RiskFeedBack { + userName: string + riskHash: string + ip: string + url: string + port: number + host: string + title: string + titleVerbose: string + description: string + solution: string + riskType: string + riskTypeVerbose: string + parameter: string + payload: string + details: string + fromYakScript: string + waitingVerified: boolean + reverseToken: string + severity: string + request: string + response: string + ipInteger: number + runtimeId: string + cve: string + taskName: string + tags: string + riskCreatedAt: number + resultID: string + programName: string + syntaxFlowVariable: string + } + export interface RenamePayloadRequest { + /** + * 修改文件夹名 folder 修改文件名 group + */ + type: string + name: string + newName: string + } + export interface RemoteTunnelResponse { + server: string + secret: string + gen_tls_crt: string + } + export interface RemoteStatusResponse { + status: boolean + /** + * 控制端人员 + */ + user_name: string + } + export interface RemoteResponse extends Paging { + data: RemoteLists[] + } + export interface RemoteOperationResponseData { + port: number + note: string + id: string + auth: string + host: string + } + export interface RemoteOperationResponse { + data: RemoteOperationResponseData[] + } + export interface RemoteOperationRequest { + tunnel: string + addr: string + auth: string + note: string + /** + * true 连接, false 断开连接 + */ + status: boolean + } + export interface RemoteLists extends GormBaseModel, RemoteList {} + export interface RemoteList { + hash: string + addr: string + status: boolean + user_name: string + head_img: string + } + export interface QueryPluginsWhereListRequest extends PluginsWhere, QueryPluginsWhereList, Pagination {} + export interface QueryPluginsWhereList { + token?: string + uuid?: string[] + } + export interface ProjectListResponse extends Paging { + data: ProjectList[] + } + export interface ProjectListDetail { + userName: string + fileName: string + fileSize: string + filePath: string + /** + * 所属组织 + */ + department?: string + } + export interface ProjectList extends GormBaseModel, ProjectListDetail {} + export interface Principle { + user: string + role: string + user_id: number + head_img: string + from_platform: string + uid: string + } + export interface PostPluginsResponse extends PluginsDetail { + isUpdate?: boolean + } + export interface PostNotepadRequest { + hash?: string + title: string + content: string + } + export interface PostNotepadPermissionRequest { + notepadHash: string + userId: number[] + /** + * 'view', 'edit' 读写权限 '' 默认移除 + */ + permissionType: string + description?: string + } + export interface PluginTypeListResponse { + data: PluginTypeList[] + } + export interface PluginTypeList { + id: number + script_name: string + } + export interface PluginTopSearchResponse { + data: PluginTopSearch[] + } + export interface PluginTopSearch { + member: string + score: number + } + export interface PluginsWhereListRequest extends PluginsWhere, PluginsWhereList {} + export interface PluginsWhereList { + token?: string + /** + * 审核流程更新,需要提示强制更新版本 + */ + upgrade?: boolean + } + export interface PluginsWhereDownloadRequest extends PluginsWhere, PluginsWhereDownload {} + export interface PluginsWhereDownload { + /** + * 勾选删除 + */ + uuid?: string[] + page?: number + limit?: number + token?: string + } + export interface PluginsWhereDeleteRequest extends PluginsWhere, PluginsWhereDelete {} + export interface PluginsWhereDelete { + /** + * 删除原因 + */ + description?: string + /** + * 勾选删除 + */ + uuid?: string[] + } + export interface PluginsWhere { + /** 这个其实是一个boolean类型的数组, + * 但是后端没法表达boolean数组, + * 所以每次更新时,需要将后端的转换定义名改成《boolean》 + */ + is_private?: boolean[] + keywords?: string + plugin_type?: string[] + tags?: string[] + user_name?: string + user_id?: number + /** + * 默认 为所有时间, 当天 day, 本周 week, 本月 month, 年 year + */ + time_search?: string + /** + * 默认首页, mine 个人, recycle 回收站 check 审核页面, other 其他情况(针对yakit下载所以不区分私密公开) + */ + listType?: string + /** + * 审核状态,0待审核,1通过审核,2审核不通过,3审核中 + */ + status?: number[] + /** + * 根据插件名批量搜索 + */ + script_name?: string[] + pluginGroup?: PluginsWherePluginGroup + excludePluginTypes?: string[] + /** + * scriptName/help 模糊搜索 + */ + fieldKeywords?: string + official?: PluginsWhereOfficial[] + } + export interface PluginsWhereOfficial { + [key: string]: unknown + } + export interface PluginsWherePluginGroup { + unSetGroup?: boolean + group?: string[] + } + export interface PluginsSearchResponse { + data: PluginsSearch[] + } + export interface PluginsSearchRequest { + /** + * 默认首页 mine 个人, recycle 回收站 check 审核页面 + */ + listType?: string + token?: string + } + export interface PluginsSearchData { + value: string + count: number + label: string + } + export interface PluginsSearch { + groupKey: string + groupName: string + sort: number + data: PluginsSearchData[] + } + export interface PluginsRiskDetail { + level: string + typeVerbose: string + cve: string + /** + * 漏洞描述 + */ + description: string + /** + * 修复建议 + */ + solution: string + } + export interface PluginsResponse { + id: number + uuid: string + } + export interface PluginsRequest { + type?: string + content?: string + params?: YakitPluginParam[] + help?: string + tags?: string[] + script_name: string + enable_plugin_selector?: boolean + plugin_selector_types?: string + is_general_module?: boolean + download_total?: number + is_private?: boolean + group?: string + riskInfo?: PluginsRiskDetail[] + isCorePlugin?: boolean + /** + * 插件附件 + */ + pluginSupplement?: string + pluginEnvKey?: string[] + } + export interface PluginsRecycleRequest extends PluginsWhere, PluginsRecycle {} + export interface PluginsRecycle { + /** + * 勾选删除 + */ + uuid?: string[] + /** + * 必传, true 彻底删除, false还原 + */ + dumpType: string + } + export interface PluginsLogsTabResponse { + data: PluginsLogsTab[] + } + export interface PluginsLogsTab { + tabName: string + count: number + } + export interface PluginsLogsResponse extends Paging { + data: PluginsLogsDetail[] + } + export interface PluginsLogsDetailExpand { + /** + * 上级评论内容 + */ + parentComment?: LogsDetail + /** + * 合并处理人信息 + */ + handleUser?: HandleUser + } + export interface PluginsLogsDetail extends LogsDetail, PluginsLogsDetailExpand {} + export interface PluginsListResponse extends Paging { + data: PluginsDetail[] + } + export interface PluginsGroupWhere { + uuid?: string[] + } + export interface PluginsGroupResponse { + setGroup: string[] + allGroup: string[] + } + export interface PluginsGroupRequest extends PluginsWhere, PluginsGroupWhere {} + export interface PluginsGroup { + uuid: string[] + saveGroup: string[] + removeGroup: string[] + } + export interface PluginsEditRequest extends PluginsRequest, PluginsEdit {} + export interface PluginsEdit { + /** + * 为空时默认走新建 不为空时默认走修改 + */ + uuid?: string + /** + * 修改必传描述(我这没写成必传是因为新增和修改是一个按钮) + */ + logDescription?: string + } + export interface PluginsDetail extends GormBaseModel { + type: string + script_name: string + tags: string + content: string + params?: YakitPluginParam[] + authors: string + user_id?: number + head_img: string + /** + * 插件发布的时间 + */ + published_at: number + /** + * 下载次数 + */ + downloaded_total: number + /** + * 获得推荐的次数 + */ + stars: number + /** + * 审核状态 + */ + status: number + official: boolean + /** + * 当前用户是否已点赞 + */ + is_stars: boolean + help?: string + enable_plugin_selector?: boolean + plugin_selector_types?: string + is_general_module?: boolean + contributors?: string + uuid: string + is_private: boolean + /** + * 复制源插件 + */ + base_plugin_id?: number + /** + * 复制源插件名 + */ + base_script_name?: string + group?: string + riskInfo?: PluginsRiskDetail[] + /** + * 是否为内置插件 + */ + isCorePlugin?: boolean + /** + * 协作者 + */ + collaborator?: CollaboratorInfo[] + isAuthor?: boolean + /** + * 插件附件 + */ + pluginSupplement?: string + pluginEnvKey?: string[] + } + export interface PluginsAuditRequest extends PluginsRequest, PluginsAudit {} + export interface PluginsAuditDetailResponse extends PluginsDetail, PluginsAuditDetail, PluginsAuditButton {} + export interface PluginsAuditDetailRequest { + uuid: string + /** + * 请求页面 默认(check) 审核详情页 log 日志详情页 + */ + list_type?: string + /** + * 日志页跳转到详情页必传 + */ + up_log_id?: number + } + export interface PluginsAuditDetail { + /** + * 修改人 + */ + apply_user_name?: string + /** + * 修改人 + */ + apply_user_id?: number + /** + * 描述 + */ + logDescription?: string + apply_user_head_img?: string + /** + * 处理状态 0 待处理 1合并 2拒绝 + */ + merge_status?: number + /** + * 日志id + */ + up_log_id?: number + merge_before_plugins?: PluginsAuditDetailMergeBeforePlugins + } + export interface PluginsAuditDetailMergeBeforePlugins { + type?: string + script_name?: string + tags?: string + content?: string + params?: YakitPluginParam[] + /** + * 审核状态 + */ + status?: number + official?: boolean + help?: string + enable_plugin_selector?: boolean + plugin_selector_types?: string + is_general_module?: boolean + uuid?: string + is_private?: boolean + stars?: number + download_total?: number + group?: string + riskInfo?: PluginsRiskDetail[] + /** + * 是否为内置插件 + */ + isCorePlugin?: boolean + pluginEnvKey?: string[] + } + export interface PluginsAuditButton { + /** + * true 显示钢笔标签, false 其他 + */ + pen?: boolean + /** + * true 表示禁用, false 其他 + */ + disable?: boolean + } + export interface PluginsAudit { + pageType?: string + /** + * 审核 'true' 通过 'false' 不通过 + */ + status: string + /** + * 必传 + */ + uuid: string + /** + * 不通过时必传 + */ + logDescription?: string + /** + * 默认不传为管理审核页面, 'log' 为日志审核页面 + */ + listType?: string + /** + * 有对比的审核页 必传此id + */ + upPluginLogId?: number + } + export interface PluginMergeRequest extends PluginsRequest, PluginMerge {} + export interface PluginMerge { + /** + * 审核 'true' 通过 'false' 不通过 + */ + status: string + /** + * 必传 + */ + uuid: string + /** + * 不通过时必传 + */ + logDescription?: string + /** + * 有对比的审核页 必传此id + */ + upPluginLogId: number + } + export interface PluginIncreResponse { + day_incre_num: number + yesterday_incre_num: number + week_incre_num: number + lastWeek_incre_num: number + } + export interface PluginAuditRequest extends PluginsRequest, PluginAudit {} + export interface PluginAudit { + /** + * 审核 'true' 通过 'false' 不通过 + */ + status: string + /** + * 必传 + */ + uuid: string + /** + * 不通过时必传 + */ + logDescription?: string + } + export interface PayloadWhere { + group?: string + keyword?: string + folder?: string + } + export interface PayloadResponse extends Paging { + data: PayloadDetail[] + } + export interface PayloadRequest extends PayloadWhere, Pagination {} + export interface PayloadGroupResponse { + group?: string[] + nodes?: PayloadGroupNode[] + } + export interface PayloadGroupNode { + type: string + name: string + number: number + nodes: PayloadGroupNode[] + } + export interface PayloadFromFileResponse { + isBigFile?: boolean + data?: string + } + export interface PayloadDetail { + id: number + group: string + content: string + folder: string + hitCount: number + isFile: boolean + fileContent?: string + hash?: string + } + export interface ParentComment { + /** + * 操作人名称 + */ + userName: string + /** + * 操作人头像 + */ + headImg: string + /** + * 操作人是否是作者 true 是 false否 + */ + isAuthors: boolean + /** + * 审核状态 + */ + checkStatus: number + /** + * 描述/评论 + */ + description: string + /** + * 作者权限 admin:管理员 trusted:信任用户 ordinary:普通用户 auditor:审核员 + */ + userRole: string + } + export interface Paging { + pagemeta: PageMeta + } + export interface Pagination { + page: number + limit: number + order_by: string + order: string + after_id?: number + before_id?: number + } + export interface PageMeta { + /** + * 页面索引 + */ + page: number + /** + * 页面数据条数限制 + */ + limit: number + /** + * 总共数据条数 + */ + total: number + /** + * 总页数 + */ + total_page: number + } + export interface OperationsResponse extends Paging { + data: Operation[] + } + export interface Operation extends GormBaseModel, NewOperation {} + export interface NotepadEitRequest { + notepadHash?: string + eitUser?: number + mentionId?: string + } + export interface NotepadDownloadWhere { + hash?: string + } + export interface NotepadDownloadRequest extends GetNotepadRequest, NotepadDownloadWhere {} + export interface NewUrmResponse { + user_name: string + password: string + } + export interface NewUrmRequest { + user_name: string + department: number + role_id: number + } + export interface NewRoleRequest { + id?: number + name: string + pluginType?: string + pluginIds?: string + plugin?: PluginTypeList[] + } + export interface NewOperation { + type: string + trigger_user_unique_id: string + operation_plugin_id: string + extra?: string + } + export interface NavigationBarsResponse { + data: NavigationBarsListResponse[] + } + export interface NavigationBarsListResponse { + card: string + link?: string + otherLink?: string + sort?: number + } + export interface MITMRuleExtractedDataResponse extends Paging { + data: MITMRuleExtractedDataList[] + } + export interface MITMRuleExtractedDataList extends GormBaseModel, MITMRuleExtractedDataDetail {} + export interface MITMRuleExtractedDataDetail { + sourceType: string + traceId: string + regexp: string + ruleName: string + data: string + } + export interface MessageLogWhereRequest { + isAll: boolean + hash: string + } + export interface MessageLogResponse extends Paging { + data: MessageLogDetail[] + } + export interface MessageLogRequest { + afterId?: number + beforeId?: number + isRead?: string + } + export interface MessageLogDetail extends GormBaseModel { + handlerUserName: string + handlerHeadImag: string + handlerRole: string + upPluginType: string + scriptName: string + /** + * 插件uuid + */ + uuid: string + /** + * 日志id + */ + upPluginLogId: number + isRead: boolean + hash: string + description: string + status: number + /** + * 协作文档ID + */ + notepadHash?: string + /** + * 协作文档标题 + */ + notepadTitle?: string + /** + * 定位艾特位置 + */ + mentionId?: string + /** + * 任务名称 + */ + taskName?: string + /** + * 子任务ID + */ + subTaskId?: string + } + export interface LogsRequest { + uuid: string + token?: string + afterId?: number + beforeId?: number + logType?: string + } + export interface LogsDetail extends GormBaseModel { + /** + * 操作人名称 + */ + userName: string + /** + * 操作人头像 + */ + headImg: string + /** + * 作者权限 admin:管理员 trusted:信任用户 ordinary:普通用户 auditor:审核员 + */ + userRole: string + /** + * 操作人是否是插件作者 true 是 false否 + */ + isAuthors: boolean + /** + * 审核状态 + */ + checkStatus: number + /** + * 日志类型 submit:新增 delete:删除 update:修改 check:审核 recover:恢复 applyMerge: 合并申请 comment:评论 + */ + logType: string + /** + * 描述/评论 + */ + description: string + /** + * 登陆用户是否是插件作者 + */ + loginIsPluginUser: boolean + /** + * 登录用户是否是日志操作者 + */ + loginIsLogUser?: boolean + } + export interface IsExtractCodeResponse { + is_extract_code: boolean + } + export interface HTTPHeader { + header?: string + value?: string + } + export interface HTTPFlowWhere { + userName?: string + projectName?: string + sourceType?: string + methods?: string + searchURL?: string + statusCode?: string + haveCommonParams?: boolean + haveBody?: boolean + searchContentType?: string + beforeUpdatedAt?: number + afterUpdatedAt?: number + keyword?: string + onlyWebsocket?: boolean + /** + * 查询包含在这个 URL 中的搜索结果 + */ + includeInUrl?: string[] + /** + * 不查询当前 URL 中的结果 + */ + excludeInUrl?: string[] + /** + * 仅查看当前 IP 对应的结果或不包含 + */ + includeInIP?: string[] + excludeInIP?: string[] + includeInWhere?: string[] + includeId?: number[] + tags?: string[] + haveParamsTotal?: string + color?: string[] + offsetId?: number + afterBodyLength?: number + beforeBodyLength?: number + isWebsocket?: string + runtimeId?: string + fromPlugin?: string + full?: boolean + includePath?: string[] + excludePath?: string[] + includeSuffix?: string[] + excludeSuffix?: string[] + excludeContentType?: string[] + /** + * 外部项目模块 + */ + externalModule?: string + /** + * 外部 项目编号 + */ + externalProjectCode?: string + beforeHttpFlowUpdatedAt?: number + afterHttpFlowUpdatedAt?: number + departmentName?: string + } + export interface HTTPFlowRequest { + projectName: string + content: string + projectDescription?: string + /** + * 外部项目模块 + */ + externalModule?: string + /** + * 外部 项目编号 + */ + externalProjectCode?: string + } + export interface HTTPFlowListWhere extends Pagination, HTTPFlowWhere {} + export interface HTTPFlowListResponse extends Paging { + data: HTTPFlowList[] + } + export interface HTTPFlowList extends GormBaseModel, HTTPFlowDetail {} + export interface HTTPFlowDetail { + isHTTPS?: boolean + url?: string + requestHeader?: HTTPHeader[] + request?: string + responseHeader?: HTTPHeader[] + response?: string + sourceType?: string + path?: string + method?: string + bodyLength?: number + bodySizeVerbose?: string + requestLength?: number + requestSizeVerbose?: string + contentType?: string + statusCode?: number + getParamsTotal?: number + postParamsTotal?: number + cookieParamsTotal?: number + httpflowUpdatedAt?: number + httpflowCreatedAt?: number + hash?: string + getParams?: FuzzableParam[] + postParams?: FuzzableParam[] + cookieParams?: FuzzableParam[] + hostPort?: string + ipAddress?: string + htmlTitle?: string + tags?: string + noFixContentLength?: boolean + isWebsocket?: boolean + websocketHash?: string + /** + * 标记 UTF8 Invalid + */ + invalidForUTF8Request?: boolean + invalidForUTF8Response?: boolean + rawRequestBodyBase64?: string + rawResponseBodyBase64?: string + /** + * 可以安全的传输到 Fuzzer 的 HTTPRequest + */ + safeHTTPRequest?: string + domains?: string[] + rootDomains?: string[] + jsonObjects?: string[] + isTooLargeResponse?: boolean + tooLargeResponseHeaderFile?: string + tooLargeResponseBodyFile?: string + disableRenderStyles?: boolean + projectName?: string + userName?: string + projectDescription?: string + /** + * 外部项目模块 + */ + externalModule?: string + /** + * 外部 项目编号 + */ + externalProjectCode?: string + host?: string + /** + * 所属组织 + */ + department?: string + } + export interface HTTPFlowDeleteWhere { + deleteAll?: boolean + id?: number[] + itemHash?: string[] + urlPrefix?: string + urlPrefixBatch?: string[] + } + export interface HTTPFlowDeleteRequest extends HTTPFlowDeleteWhere, HTTPFlowWhere {} + export interface HTTPFlowBareResponse { + id: number + data: string + } + export interface HotPatchTemplateResponse extends Paging { + data: TemplateDetail[] + } + export interface HotPatchTemplateRequest { + type?: string + name?: string + } + export interface HotPatchTemplateDownloadRequest extends HotPatchTemplateRequest, HotPatchTemplateDownload {} + export interface HotPatchTemplateDownload { + page?: number + limit?: number + } + export interface HandleUser { + /** + * 流程操作人名称 + */ + handleUserName?: string + /** + * 流程操作人头像 + */ + handleHeadImg?: string + /** + * 流程作者权限 admin:管理员 trusted:信任用户 ordinary:普通用户 auditor:审核员 + */ + handleUserRole?: string + /** + * 是否是作者 + */ + isAuthor?: boolean + } + export interface GroupTableColumnResponse { + data: string[] + } + export interface GroupResponseDetail { + value: string + total: number + default: boolean + } + export interface GroupResponse { + data: GroupResponseDetail[] + } + export interface GroupRequest extends PluginsWhere, PluginsGroup {} + export interface GormBaseModel { + id: number + created_at: number + updated_at: number + } + export interface GetRiskWhere { + hash?: string[] + search?: string + net_work?: string + ports?: string + risk_type?: string + token?: string + waiting_verified?: boolean + severity?: string + user_name?: string + fromId?: number + untilId?: number + tags?: string + title?: string + fromYakScript?: string + /** + * 外部项目模块 + */ + externalModule?: string + /** + * 外部 项目编号 + */ + externalProjectCode?: string + } + export interface GetRemoteWhere { + user_name?: string + start_time?: number + end_time?: number + status?: string + } + export interface GetNotepadResponse extends Paging { + data: GetNotepadList[] + } + export interface GetNotepadRequest { + keywords?: string + /** + * 作者名称 + */ + user?: string + /** + * 协作者名称 + */ + collaborator?: string + } + export interface GetNotepadList extends GormBaseModel, GetNotepadDetail {} + export interface GetNotepadDetail { + title: string + content: string + userName: string + headImg: string + /** + * 协作者 + */ + collaborator?: CollaboratorInfo[] + hash: string + notepadUserId?: number + } + export interface FuzzableParam { + position?: string + paramName?: string + originValue?: string + autoTemplate?: string + isHTTPS?: boolean + } + export interface FlowRuleWhere { + ruleNames?: string[] + language?: string[] + groupNames?: string[] + severity?: string[] + purpose?: string[] + tag?: string[] + keyword?: string + filterRuleKind?: string + filterLibRuleKind?: string + } + export interface FlowRuleResponse extends Paging { + data: FlowRuleDetail[] + } + export interface FlowRuleRequest extends FlowRuleWhere, Pagination {} + export interface FlowRuleGroupWhere { + groupNames?: string[] + keyword?: string + filterGroupKind?: string + } + export interface FlowRuleGroupResponse extends Paging { + data: FlowRuleGroupDetail[] + } + export interface FlowRuleGroupRequest extends FlowRuleGroupWhere, Pagination {} + export interface FlowRuleGroupDetail { + groupName: string + count: number + isBuildIn?: boolean + } + export interface FlowRuleDetail { + id?: number + ruleName?: string + content?: string + language?: string + type?: string + severity?: string + purpose?: string + isBuildInRule?: boolean + title?: string + titleZh?: string + description?: string + verified?: boolean + allowIncluded?: boolean + includedName?: string + tag?: string + hash?: string + groupName?: string[] + ruleId?: string + version?: string + } + export interface Fields { + data: FieldName[] + } + export interface FieldName { + name?: string + verbose?: string + total?: number + } + export interface ExtractResponse { + extract_content: string + module: string + } + export interface ExportHTTPFlowWhere extends HTTPFlowListWhere { + fieldName: string[] + /** + * 导出类型 xml, csv + */ + exportType: string + /** + * 包含字段 响应包: response 请求包: request 请求大小: requestLen 标题: title + */ + includeField?: string[] + } + export interface EditUrmRequest { + uid: string + user_name?: string + department?: number + role_id?: number + } + export interface DepartmentListResponse extends Paging { + data: DepartmentList[] + } + export interface DepartmentList { + id: number + name: string + userNum: number + /** + * 是否有二级分组 + */ + exist_group?: boolean + } + export interface DepartmentGroupList { + data: DepartmentList[] + } + export interface DeleteUrm { + uid: string[] + } + export interface DeleteResource { + csrf_token?: string + file_name: string[] + /** + * 删除图片传'img' 视频传 'video' + */ + file_type: string + } + export interface DeletePluginUuid { + uuid: string[] + dump: boolean + keywords?: string + is_recycle?: boolean + } + export interface DeletePayloadRequest { + name?: string + group?: string + ids?: number[] + } + export interface DeleteOssResource { + file_name: string[] + } + export interface DeleteNotepadWhere { + hash?: string + } + export interface DeleteNotepadRequest extends GetNotepadRequest, DeleteNotepadWhere {} + export interface CopyPluginsRequest extends PluginsRequest, CopyPlugins {} + export interface CopyPlugins { + /** + * 复制插件id + */ + base_plugin_id: number + } + export interface CompanyLicenseConfigResponse extends Paging { + data: CompanyLicenseConfigList[] + } + export interface CompanyLicenseConfigList { + id: number + company: string + maxActivationNum: number + useActivationNum: number + maxUser: number + durationDate: number + currentTime?: number + } + export interface CommentLogRequest { + uuid: string + logId?: number + description?: string + } + export interface CollaboratorInfo { + user_id: number + head_img: string + user_name: string + role?: string + } + export interface ActionSucceeded { + /** + * 来源于哪个 API + */ + from: string + /** + * 执行状态 + */ + ok: boolean + } + export interface ActionFailed { + /** + * 来源于哪个 API + */ + from: string + /** + * 执行状态 + */ + ok: boolean + reason: string + } +} diff --git a/src/pages/AIAgent/ai-agent/useContext/AIAgentContext.ts b/src/pages/AIAgent/ai-agent/useContext/AIAgentContext.ts new file mode 100644 index 00000000..e19b8547 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/useContext/AIAgentContext.ts @@ -0,0 +1,43 @@ +import { createContext, type Dispatch, type SetStateAction } from 'react'; +import type { AIAgentSetting } from '../aiAgentType'; +import type { AIChatInfo } from '../type/aiChat'; + +export interface AIAgentContextStore { + /** 全局配置 */ + setting: AIAgentSetting; + /** 历史对话 */ + chats: AIChatInfo[]; + /** 当前展示对话 */ + activeChat?: AIChatInfo; +} + +export interface AIAgentContextDispatcher { + setSetting?: Dispatch>; + getSetting?: () => AIAgentSetting; + setChats?: Dispatch>; + getChats?: () => AIChatInfo[]; + setActiveChat?: Dispatch>; + + // getChatData?: (session: string) => AIChatData | undefined +} + +export interface AIAgentContextValue { + store: AIAgentContextStore; + dispatcher: AIAgentContextDispatcher; +} + +export default createContext({ + store: { + setting: {}, + chats: [], + activeChat: undefined, + }, + dispatcher: { + setSetting: undefined, + getSetting: undefined, + + setChats: undefined, + getChats: undefined, + setActiveChat: undefined, + }, +}); diff --git a/src/pages/AIAgent/ai-agent/useContext/ChatIPCContent/ChatIPCContent.ts b/src/pages/AIAgent/ai-agent/useContext/ChatIPCContent/ChatIPCContent.ts new file mode 100644 index 00000000..a1fdfa5f --- /dev/null +++ b/src/pages/AIAgent/ai-agent/useContext/ChatIPCContent/ChatIPCContent.ts @@ -0,0 +1,86 @@ +import { createContext } from 'react' +import { defaultChatIPCData } from '../../defaultConstant' +import { cloneDeep } from 'lodash' +import type { UseChatIPCEvents, UseChatIPCState } from '@/pages/AIAgent/ai-re-act/hooks/type' +import type { AIAgentGrpcApi, AIInputEvent, AIStartParams } from '@/pages/AIAgent/ai-re-act/hooks/grpcApi' +import type { AIChatQSData } from '@/pages/AIAgent/ai-re-act/hooks/aiRender' + +export interface ChatIPCContextStore { + chatIPCData: UseChatIPCState + reviewInfo?: AIChatQSData + planReviewTreeKeywordsMap: Map + reviewExpand: boolean +} + +export interface AIChatIPCSendParams { + /** InteractiveJSONInput */ + value: string + id: string + /** 用户审阅过程中选择得btn */ + optionValue?: string +} +type MakeOptional = Omit & Partial> +export interface AISendSyncMessageParams { + syncType: AIInputEvent['SyncType'] + SyncJsonInput?: AIInputEvent['SyncJsonInput'] + params?: MakeOptional + syncID?: string +} +export interface AISendConfigHotpatchParams { + hotpatchType: AIInputEvent['HotpatchType'] + params: MakeOptional +} +export interface ChatIPCContextDispatcher { + /** useChatIPC的各种事件 */ + chatIPCEvents: UseChatIPCEvents + /** 发送自由对话 */ + handleSendCasual: (params: AIChatIPCSendParams) => void + /** 任务规划 */ + handleSendTask: (params: AIChatIPCSendParams) => void + /** 开始 */ + // handleStart: (data: HandleStartParams) => void + /** 停止ai */ + handleStop: () => void + handleSend: (params: AIChatIPCSendParams) => void + /** 发送 Sync-Type */ + handleSendSyncMessage: (params: AISendSyncMessageParams) => void + /** 发送 Config-Hotpatch */ + handleSendConfigHotpatch: (params: AISendConfigHotpatchParams) => void +} + +export interface ChatIPCContextValue { + store: ChatIPCContextStore + dispatcher: ChatIPCContextDispatcher +} +export const defaultDispatcherOfChatIPC: ChatIPCContextDispatcher = { + chatIPCEvents: { + fetchToken: () => '', + fetchAIRequest: () => undefined, + fetchTaskChatID: () => undefined, + onSwitchChat: () => {}, + onStart: () => {}, + onSend: () => {}, + onClose: () => {}, + onReset: () => {}, + handleTaskReviewRelease: () => {}, + fetchChatDataStore: () => undefined, + onDelChats: () => {}, + handleCancelLoadingChange: () => {}, + }, + handleSendCasual: () => {}, + handleSendTask: () => {}, + handleSend: () => {}, + // handleStart: () => {}, + handleStop: () => {}, + handleSendSyncMessage: () => {}, + handleSendConfigHotpatch: () => {}, +} +export default createContext({ + store: { + chatIPCData: cloneDeep(defaultChatIPCData), + reviewInfo: undefined, + planReviewTreeKeywordsMap: new Map(), + reviewExpand: false, + }, + dispatcher: defaultDispatcherOfChatIPC, +}) diff --git a/src/pages/AIAgent/ai-agent/useContext/ChatIPCContent/useDispatcher.ts b/src/pages/AIAgent/ai-agent/useContext/ChatIPCContent/useDispatcher.ts new file mode 100644 index 00000000..26826d4a --- /dev/null +++ b/src/pages/AIAgent/ai-agent/useContext/ChatIPCContent/useDispatcher.ts @@ -0,0 +1,9 @@ +import { useContext } from 'react'; +import ChatIPCContext, { + type ChatIPCContextDispatcher, +} from './ChatIPCContent'; + +export default function useChatIPCDispatcher(): ChatIPCContextDispatcher { + const { dispatcher } = useContext(ChatIPCContext); + return dispatcher; +} diff --git a/src/pages/AIAgent/ai-agent/useContext/ChatIPCContent/useStore.ts b/src/pages/AIAgent/ai-agent/useContext/ChatIPCContent/useStore.ts new file mode 100644 index 00000000..a8c378c3 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/useContext/ChatIPCContent/useStore.ts @@ -0,0 +1,8 @@ +import { useContext } from 'react'; +import type { ChatIPCContextStore } from './ChatIPCContent'; +import ChatIPCContext from './ChatIPCContent'; + +export default function useChatIPCStore(): ChatIPCContextStore { + const { store } = useContext(ChatIPCContext); + return store; +} diff --git a/src/pages/AIAgent/ai-agent/useContext/useDispatcher.ts b/src/pages/AIAgent/ai-agent/useContext/useDispatcher.ts new file mode 100644 index 00000000..049a0a56 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/useContext/useDispatcher.ts @@ -0,0 +1,9 @@ +import { useContext } from 'react'; +import AIAgentContext, { + type AIAgentContextDispatcher, +} from './AIAgentContext'; + +export default function useAIAgentDispatcher(): AIAgentContextDispatcher { + const { dispatcher } = useContext(AIAgentContext); + return dispatcher; +} diff --git a/src/pages/AIAgent/ai-agent/useContext/useStore.ts b/src/pages/AIAgent/ai-agent/useContext/useStore.ts new file mode 100644 index 00000000..2406e549 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/useContext/useStore.ts @@ -0,0 +1,7 @@ +import { useContext } from 'react'; +import AIAgentContext, { type AIAgentContextStore } from './AIAgentContext'; + +export default function useAIAgentStore(): AIAgentContextStore { + const { store } = useContext(AIAgentContext); + return store; +} diff --git a/src/pages/AIAgent/ai-agent/utils.ts b/src/pages/AIAgent/ai-agent/utils.ts new file mode 100644 index 00000000..eaa7a831 --- /dev/null +++ b/src/pages/AIAgent/ai-agent/utils.ts @@ -0,0 +1,193 @@ +import type { AIAgentSetting } from './aiAgentType' +import isNil from 'lodash/isNil' +import { AIAgentSettingDefault, AttachedResourceKeyEnum, AttachedResourceTypeEnum } from './defaultConstant' +import type { AIAgentGrpcApi, AIInputEvent, AttachedResourceInfo } from '../ai-re-act/hooks/grpcApi' +import type { HandleStartParams } from './aiAgentChat/type' +import type { AIMentionCommandParams } from './components/aiMilkdownInput/aiMilkdownMention/aiMentionPlugin' + +/** + * @name 将一维tree转换成树结构 + */ +/** + * 将扁平数组转换为树形结构 + * @param {AIAgentGrpcApi.PlanTask[]} items 扁平数据数组 + * @returns {AIAgentGrpcApi.PlanTask[]} 树形结构数组 + */ +export const reviewListToTrees = (items: AIAgentGrpcApi.PlanTask[]): AIAgentGrpcApi.PlanTask[] => { + // 创建映射表,以id为键存储所有节点 + const map: Record = {} + const tree: AIAgentGrpcApi.PlanTask[] = [] + + // 首先构建所有节点的映射 + items.forEach((item) => { + // 如果是用户添加的节点且没有名称、目标和工具,则跳过 + if (item.isUserAdd && !item.name && !item.goal && !item.tools.length) return + // 创建节点副本并初始化children数组 + map[item.index] = { ...item, subtasks: [] } + }) + + // 构建树结构 + items.forEach((item) => { + const node: AIAgentGrpcApi.PlanTask = map[item.index] + if (!node) return // 如果节点不存在,跳过 + const parentId = getParentId(item.index) + // 如果有父节点,则添加到父节点的children中 + if (parentId && map[parentId]) { + // @ts-ignore + map[parentId].subtasks.push(node) + } + // 否则作为根节点 + else { + tree.push(node) + } + }) + + return tree +} + +/** + * 从节点ID提取父节点ID + * @param {String} id 当前节点ID + * @returns {String|null} 父节点ID或null(如果是根节点) + */ +const getParentId = (id: string) => { + const parts = id.split('-') + if (parts.length <= 1) return null + return parts.slice(0, -1).join('-') +} + +// #region chat相关工具 +/** @name 将Token转换为K/M等带单位字符 */ +export const formatNumberUnits = (num: number) => { + if (num >= 1048576) { + return (num / 1048576).toFixed(1) + 'M' + } else if (num >= 1024) { + return (num / 1024).toFixed(1) + 'K' + } else { + return num.toString() + } +} + +/** @name 将全局配置信息转换为可以请求的数据结构 */ +export const formatAIAgentSetting = (setting: AIAgentSetting): AIAgentSetting => { + const data: AIAgentSetting = { ...setting } + + try { + data.EnableSystemFileSystemOperator = + setting.EnableSystemFileSystemOperator ?? AIAgentSettingDefault.EnableSystemFileSystemOperator + + data.UseDefaultAIConfig = setting.UseDefaultAIConfig ?? AIAgentSettingDefault.UseDefaultAIConfig + + data.ForgeName = '' // 不传 + data.ForgeParams = undefined // 不传 + + data.DisallowRequireForUserPrompt = + setting.DisallowRequireForUserPrompt ?? AIAgentSettingDefault.DisallowRequireForUserPrompt + + data.ReviewPolicy = setting.ReviewPolicy || AIAgentSettingDefault.ReviewPolicy + + data.AIReviewRiskControlScore = setting.AIReviewRiskControlScore ?? AIAgentSettingDefault.AIReviewRiskControlScore + + data.DisableToolUse = setting.DisableToolUse ?? AIAgentSettingDefault.DisableToolUse + + data.AICallAutoRetry = setting.AICallAutoRetry ?? AIAgentSettingDefault.AICallAutoRetry + + data.AITransactionRetry = setting.AITransactionRetry ?? AIAgentSettingDefault.AITransactionRetry + + data.EnableAISearchTool = setting.EnableAISearchTool ?? AIAgentSettingDefault.EnableAISearchTool + data.EnableAISearchInternet = setting.EnableAISearchInternet ?? AIAgentSettingDefault.EnableAISearchInternet + data.EnableQwenNoThinkMode = setting.EnableQwenNoThinkMode ?? AIAgentSettingDefault.EnableQwenNoThinkMode + data.AllowPlanUserInteract = setting.AllowPlanUserInteract ?? AIAgentSettingDefault.AllowPlanUserInteract + + if (setting?.AllowPlanUserInteract) { + if (!isNil(setting?.PlanUserInteractMaxCount)) { + data.PlanUserInteractMaxCount = setting.PlanUserInteractMaxCount || 3 + } else { + data.PlanUserInteractMaxCount = 3 + } + } + if (!isNil(setting?.AIService)) { + data.AIService = setting.AIService + } + if (!isNil(setting?.ReActMaxIteration)) { + data.ReActMaxIteration = setting.ReActMaxIteration || AIAgentSettingDefault.ReActMaxIteration + } + if (!isNil(setting?.TimelineItemLimit)) { + data.TimelineItemLimit = setting.TimelineItemLimit || AIAgentSettingDefault.TimelineItemLimit + } + // TimelineContentSizeLimit 单位是KB,但传到后端需要转换为字节 + data.TimelineContentSizeLimit = + ((setting.TimelineContentSizeLimit ?? AIAgentSettingDefault.TimelineContentSizeLimit) as number) * 1024 + + if (!isNil(setting?.UserInteractLimit)) { + data.UserInteractLimit = setting.UserInteractLimit || AIAgentSettingDefault.UserInteractLimit + } + if (!isNil(setting?.TimelineSessionID)) { + data.TimelineSessionID = setting.TimelineSessionID || AIAgentSettingDefault.TimelineSessionID + } + } catch (error) {} + + return { ...data, ReviewPolicy: 'yolo' } +} + +const getResourceInfoByMention = (mention: AIMentionCommandParams): AttachedResourceInfo | null => { + switch (mention.mentionType) { + case 'file': + case 'folder': + return { + Type: AttachedResourceTypeEnum.CONTEXT_PROVIDER_TYPE_FILE, + Key: AttachedResourceKeyEnum.CONTEXT_PROVIDER_KEY_FILE_PATH, + Value: mention.mentionName, + } + case 'forge': + return { + Type: AttachedResourceTypeEnum.CONTEXT_PROVIDER_TYPE_AIFORGE, + Key: AttachedResourceKeyEnum.CONTEXT_PROVIDER_KEY_NAME, + Value: mention.mentionName, + } + case 'tool': + return { + Type: AttachedResourceTypeEnum.CONTEXT_PROVIDER_TYPE_AITOOL, + Key: AttachedResourceKeyEnum.CONTEXT_PROVIDER_KEY_NAME, + Value: mention.mentionName, + } + case 'knowledgeBase': + if (mention.mentionId === '@所有知识库') { + return { + Type: AttachedResourceTypeEnum.CONTEXT_PROVIDER_TYPE_KNOWLEDGE_BASE, + Key: AttachedResourceKeyEnum.CONTEXT_PROVIDER_KEY_SYSTEM_FLAG, + Value: 'all_knowledge_base', + } + } else { + return { + Type: AttachedResourceTypeEnum.CONTEXT_PROVIDER_TYPE_KNOWLEDGE_BASE, + Key: AttachedResourceKeyEnum.CONTEXT_PROVIDER_KEY_NAME, + Value: mention.mentionName, + } + } + + default: + return null + } +} +/** @name 将前端的结构转化为符合定义的结构 */ +export const getAIReActRequestParams = (value: HandleStartParams) => { + const { extraValue, mentionList = [], showQS } = value + let extra: HandleStartParams['extraValue'] = {} + let attachedResourceInfo: AIInputEvent['AttachedResourceInfo'] = [] + for (let item of mentionList) { + const addItem = getResourceInfoByMention(item) + if (addItem) { + attachedResourceInfo = [...attachedResourceInfo, addItem] // 不需要去重,按显示顺序给后端 + } + } + if (showQS) { + extra.showQS = showQS + } + extra = Object.assign(extraValue || {}, extra) + return { + extra, + attachedResourceInfo, + } +} +// #endregion diff --git a/src/pages/AIAgent/ai-re-act/aiFocusMode/AIFocusMode.module.scss b/src/pages/AIAgent/ai-re-act/aiFocusMode/AIFocusMode.module.scss new file mode 100644 index 00000000..04d888dc --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/aiFocusMode/AIFocusMode.module.scss @@ -0,0 +1,6 @@ +@use "../aiReviewRuleSelect/AIReviewRuleSelect.module.scss"; + +.focus-mode-default { + display: flex; + align-items: center; +} diff --git a/src/pages/AIAgent/ai-re-act/aiFocusMode/AIFocusMode.tsx b/src/pages/AIAgent/ai-re-act/aiFocusMode/AIFocusMode.tsx new file mode 100644 index 00000000..a67b12c1 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/aiFocusMode/AIFocusMode.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useRef, useState } from 'react' +import type { AIFocusModeProps } from './type' +import { OutlineMicroscopeIcon, OutlineXIcon } from '@/assets/icon/outline' +import { useInViewport, useMemoizedFn } from 'ahooks' +import classNames from 'classnames' +import { AIChatSelect } from '../aiReviewRuleSelect/AIReviewRuleSelect' +import type { AIInputEvent } from '../hooks/grpcApi' +import styles from './AIFocusMode.module.scss' +import { YakitSelect } from '@/compoments/yakitUI/YakitSelect/YakitSelect' +import type { YakitSelectProps } from '@/compoments/yakitUI/YakitSelect/YakitSelectType' +import { postSettingAifocusGet } from '@/apis/AiEventApi' +import { Tooltip } from 'antd' +import type { TPostSettingAifocusGetResponse } from '@/apis/AiEventApi/type' + +export const AIFocusMode: React.FC = React.memo((props) => { + const { value, onChange, className } = props + + const [focusModeList, setFocusModeList] = useState([]) + const [open, setOpen] = useState(false) + const ref = useRef(null) + + const [inViewPort = true] = useInViewport(ref) + + useEffect(() => { + if (inViewPort) getFocusMode() + }, [inViewPort]) + const getFocusMode = useMemoizedFn(async () => { + const res = await postSettingAifocusGet() + const list = (res.Data || []).map((item: TPostSettingAifocusGetResponse) => ({ + label: item.VerboseNameZh || item.Name, + value: item.Name, + describe: item.Description, + })) + setFocusModeList(list) + }) + const onSelectModel = useMemoizedFn((value: AIInputEvent['FocusModeLoop']) => { + onChange(value) + }) + + const onSetOpen = useMemoizedFn((v: boolean) => { + setOpen(v) + }) + const onRemove = useMemoizedFn(() => { + onChange(undefined) + setOpen(false) + }) + return ( +
    + { + return ( +
    +
    + + 专注模式 +
    + {menu} +
    + ) + }} + value={ + value || { + label: ( +
    + + 请选择 +
    + ), + value: '', + } + } + // placeholder="请选择" + onSelect={onSelectModel} + optionLabelProp="label" + open={open} + setOpen={onSetOpen} + > + {focusModeList?.map((item) => ( + + + + {item.label} + + +
    + } + > + +
    +
    {item.label}
    +
    +
    + + ))} + +
    + ) +}) diff --git a/src/pages/AIAgent/ai-re-act/aiFocusMode/type.ts b/src/pages/AIAgent/ai-re-act/aiFocusMode/type.ts new file mode 100644 index 00000000..35fa8258 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/aiFocusMode/type.ts @@ -0,0 +1,8 @@ +import type { Dispatch, SetStateAction } from 'react'; +import type { AIInputEvent } from '../hooks/grpcApi'; + +export interface AIFocusModeProps { + value: AIInputEvent['FocusModeLoop']; + onChange: Dispatch>; + className?: string; +} diff --git a/src/pages/AIAgent/ai-re-act/aiReActChat/AIReActChat.module.scss b/src/pages/AIAgent/ai-re-act/aiReActChat/AIReActChat.module.scss new file mode 100644 index 00000000..46b7549f --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/aiReActChat/AIReActChat.module.scss @@ -0,0 +1,269 @@ +@mixin content-side($type) { + .content-#{$type}-side { + width: 257px; + height: 100%; + display: flex; + @if $type == "left" { + border-right: 1px solid var(--Colors-Use-Neutral-Border); + // transition: + // border-right 0.3s ease-in-out, + // width 0.3s ease-in-out; + } @else { + justify-content: space-between; + border-left: 1px solid var(--Colors-Use-Neutral-Border); + // transition: + // border-left 0.3s ease-in-out, + // width 0.3s ease-in-out; + } + .open-wrapper { + width: 0; + height: 100%; + overflow: hidden; + opacity: 0; + } + @content; + } + .content-#{$type}-side-hidden { + overflow: hidden; + width: 30px; + .open-wrapper { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + opacity: 1; + padding: 8px 4px 4px; + cursor: pointer; + .text { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 500; + line-height: 16px; + text-align: center; + } + .side-header-btn { + border-radius: 50%; + background: var(--Colors-Use-Basic-Background); + } + } + } +} +%display-flex-center { + display: flex; + align-items: center; +} +%display-column-center { + display: flex; + flex-direction: column; +} +// #region AIReActChat +.ai-re-act { + @extend %display-flex-center; + align-items: flex-start; + height: 100%; + width: 100%; + overflow: hidden; + // transition: width 0.3s ease-in-out; + .open-wrapper { + width: 0; + height: 100%; + overflow: hidden; + opacity: 0; + } +} + +@include content-side("re-act") { + width: 100%; + border-left: 0; +} +.ai-re-act-chat { + width: 100%; + height: 100%; + padding-bottom: 12px; + display: flex; + flex-direction: column; + gap: 4px; + background: var(--Colors-Use-Basic-Background); + .chat-container { + .chat-header { + padding: 12px 8px 12px 0px; + } + } +} +.ai-re-act-chat-hidden { + width: 0; + overflow: hidden; +} +.chat-container { + flex: 1; + overflow: hidden; + height: 100%; + @extend %display-column-center; + .chat-header { + @extend %display-flex-center; + gap: 12px; + padding: 12px 8px; + height: 48px; + .chat-header-title { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 14px; + font-weight: 500; + line-height: 20px; + letter-spacing: 0.1px; + @extend %display-flex-center; + gap: 4px; + flex: 1; + } + .chat-header-extra { + @extend %display-flex-center; + justify-content: flex-end; + gap: 8px; + flex-shrink: 0; + .info-token { + display: flex; + align-items: center; + gap: 4px; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + } + .token-tag { + height: 16px; + padding: 2px 6px; + display: flex; + align-items: center; + gap: 4px; + border-radius: 2px; + svg { + width: 12px; + height: 12px; + } + } + .upload-token { + background: var(--Colors-Use-Error-Bg-Hover); + color: var(--Colors-Use-Error-Primary); + svg { + color: var(--Colors-Use-Error-Primary); + } + } + .download-token { + background: var(--Colors-Use-Neutral-Bg-Hover); + color: var(--Colors-Use-Neutral-Text-3-Secondary); + svg { + color: var(--Colors-Use-Neutral-Text-3-Secondary); + } + } + + .divider-style { + width: 1px; + height: 12px; + border-left: 1px solid var(--Colors-Use-Neutral-Border); + } + .info-time { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + line-height: 16px; + } + } + } +} + +.chat-footer { + min-height: 96px; + padding: 0px 12px 0 12px; + display: flex; + justify-content: center; + .footer-body { + height: 100%; + max-width: 784px; + width: 100%; + flex: 1; + position: relative; + } + + .footer-inputs { + .footer-inputs-file-list { + display: flex; + flex-direction: column; + } + .draggingFromTree { + outline: 2px dashed var(--Colors-Use-Main-Primary); + background: rgba(64, 158, 255, 0.06); + } + + .dragHint { + position: absolute; + inset: 0; + z-index: 10; + + display: flex; + align-items: center; + justify-content: center; + + pointer-events: none; + + font-size: 14px; + color: var(--Colors-Use-Main-Primary); + background: rgba(255, 255, 255, 0.6); + backdrop-filter: blur(2px); + } + .extra-footer-right { + display: flex; + align-items: center; + &-divider { + width: 1px; + height: 16px; + background: var(--Colors-Use-Neutral-Border); + margin: 0 8px; + } + } + } +} + +// #endregion + +// #region Footer +.rounded-icon-btn {} +.stop-icon { + animation: icon-breath 1s linear infinite; +} +@keyframes icon-breath { + 0% { + transform: scale(0.8); + border-color: rgba(255, 255, 255, 0); + } + 25% { + transform: scale(0.9); + border-color: rgba(255, 255, 255, 0.5); + } + 50% { + transform: scale(1.1); + border-color: rgba(255, 255, 255, 0.5); + } + 75% { + transform: scale(0.9); + border-color: rgba(255, 255, 255, 0.5); + } + 100% { + transform: scale(0.8); + border-color: rgba(255, 255, 255, 0); + } +} + +// #endregion + +.side-header-btn { + background: var(--Colors-Use-Basic-Background); +} +.timeline-message { + width: 100%; + height: 100%; + + background: var(--Colors-Use-Neutral-Bg-Hover); + color: var(--Colors-Use-Neutral-Text-2-Primary); + padding: 8px; + margin: 0; + font-size: 12px; +} diff --git a/src/pages/AIAgent/ai-re-act/aiReActChat/AIReActChat.tsx b/src/pages/AIAgent/ai-re-act/aiReActChat/AIReActChat.tsx new file mode 100644 index 00000000..33333168 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/aiReActChat/AIReActChat.tsx @@ -0,0 +1,294 @@ +import React, { forwardRef, useImperativeHandle, useRef } from 'react' + +import styles from './AIReActChat.module.scss' +import type { AIHandleStartResProps, AIReActChatProps, AISendResProps } from './AIReActChatType' +import { AIChatTextarea } from '@/pages/AIAgent/ai-agent/template/template' +import { AIReActChatContents } from '../aiReActChatContents/AIReActChatContents' +import type { AIChatTextareaRefProps, AIChatTextareaSubmit } from '@/pages/AIAgent/ai-agent/template/type' +import { useControllableValue, useCreation, useMemoizedFn } from 'ahooks' +import { yakitNotify } from '@/utils/notification' +import { ColorsChatIcon } from '@/assets/icon/colors' +import useAIAgentStore from '@/pages/AIAgent/ai-agent/useContext/useStore' +import classNames from 'classnames' +import useChatIPCStore from '@/pages/AIAgent/ai-agent/useContext/ChatIPCContent/useStore' +import useChatIPCDispatcher from '@/pages/AIAgent/ai-agent/useContext/ChatIPCContent/useDispatcher' +import { ChevrondownButton, ChevronleftButton, RoundedStopButton } from './AIReActComponent' +import type { AIInputEvent, AIStartParams } from '../hooks/grpcApi' +import { AITaskQuery } from '@/pages/AIAgent/ai-agent/components/aiTaskQuery/AITaskQuery' +import type { HandleStartParams } from '@/pages/AIAgent/ai-agent/aiAgentChat/type' +import { formatAIAgentSetting, getAIReActRequestParams } from '@/pages/AIAgent/ai-agent/utils' +import type { AIChatInfo } from '@/pages/AIAgent/ai-agent/type/aiChat' +import useAIAgentDispatcher from '@/pages/AIAgent/ai-agent/useContext/useDispatcher' +import { randomString } from '@/utils/randomUtil' +import { YakitTag } from '@/compoments/yakitUI/YakitTag/YakitTag' +import { postCreateSession } from '@/apis/AiEventApi' + +export const AIReActChat: React.FC = React.memo( + forwardRef((props, ref) => { + const { + mode, + chatContainerClassName, + chatContainerHeaderClassName, + title = '自由对话', + sendRequest, + startRequest, + externalParameters, + } = props + const { setChats, setActiveChat } = useAIAgentDispatcher() + + const { chatIPCData } = useChatIPCStore() + const { chatIPCEvents, handleStop } = useChatIPCDispatcher() + const execute = useCreation(() => chatIPCData.execute, [chatIPCData.execute]) + const focusMode = useCreation(() => chatIPCData.focusMode, [chatIPCData.focusMode]) + + const wrapperRef = useRef(null) + + const [showFreeChat, setShowFreeChat] = useControllableValue(props, { + defaultValue: true, + valuePropName: 'showFreeChat', + trigger: 'setShowFreeChat', + }) + + const { activeChat, setting } = useAIAgentStore() + + const questionQueue = useCreation(() => chatIPCData.questionQueue, [chatIPCData.questionQueue]) + + const aiChatTextareaRef = useRef({ + setMention: () => {}, + setValue: () => {}, + getValue: () => {}, + }) + useImperativeHandle(ref, () => { + return { + ...aiChatTextareaRef.current, + handleStart: (value) => handleStart(value), + } + }, []) + // #region 问题相关逻辑 + // 初始化 AI ReAct + const handleSubmit = useMemoizedFn((value: AIChatTextareaSubmit) => { + if (!setting) { + yakitNotify('error', '请先配置 AI ReAct 参数') + return + } + if (execute) { + handleSend(value) + } else { + handleStart(value) + } + onSetQuestion('') + }) + + const handleStart = useMemoizedFn(async (value: HandleStartParams) => { + const { qs, sessionId } = value + const sessionID = activeChat?.run_id || '' // 判断历史还是新建 + const request: AIStartParams = { + ...formatAIAgentSetting(setting), + UserQuery: qs, + CoordinatorId: '', + Sequence: 1, + } + + let session = '' + if (sessionID) { + session = sessionID + } else if (sessionId) { + session = sessionId + } else if (setting.TimelineSessionID) { + session = setting.TimelineSessionID + } else { + try { + const { run_id } = await postCreateSession({}) + session = run_id + } catch (error) { + yakitNotify('error', '创建会话失败,请稍后重试') + return + } + } + request.TimelineSessionID = session + const { extra, attachedResourceInfo } = getAIReActRequestParams(value) + // 发送初始化参数 + const aiInputEvent: AIInputEvent = { + IsStart: true, + Params: { + ...request, + }, + AttachedResourceInfo: attachedResourceInfo, + FocusModeLoop: value.focusMode, + } + const onStart = (res: AIHandleStartResProps) => { + const { params, onChat, onChatFromHistory } = res + if (!sessionID) { + // 创建新的聊天记录 + const newChat: AIChatInfo = { + run_id: session, + title: qs || `AI Agent - ${new Date().toLocaleString()}`, + created_at: new Date().toISOString(), + } + + setActiveChat && setActiveChat(newChat) + setChats && setChats((old) => [newChat, ...old]) + // 新建的额外操作 + onChat?.() + } else { + // 历史中的额外操作 + onChatFromHistory?.(sessionID) + } + aiChatTextareaRef.current.setMention({ + mentionId: params.FocusModeLoop || randomString(8), + mentionType: 'focusMode', + mentionName: params.FocusModeLoop || '', + }) + chatIPCEvents.onStart({ + token: request.TimelineSessionID!, + params, + extraValue: extra, + }) + } + if (startRequest) { + startRequest({ + params: aiInputEvent, + }) + .then((res) => { + onStart(res) + }) + .catch(() => { + onStart({ + params: aiInputEvent, + }) + }) + } else { + onStart({ + params: aiInputEvent, + }) + } + }) + + /** 自由对话 */ + const handleSend = useMemoizedFn((data: HandleStartParams) => { + if (!activeChat?.run_id) return + try { + const { extra, attachedResourceInfo } = getAIReActRequestParams(data) + const chatMessage: AIInputEvent = { + IsFreeInput: true, + FreeInput: data.qs, + AttachedResourceInfo: attachedResourceInfo, + FocusModeLoop: data.focusMode, + } + const onSend = (res: AISendResProps) => { + const { params } = res + chatIPCEvents.onSend({ + token: activeChat.run_id, + type: 'casual', + params: { + IsFreeInput: true, + ...params, + }, + extraValue: extra, + }) + } + if (sendRequest) { + sendRequest?.({ params: chatMessage }) + .then((res) => { + const { params } = res + // 发送到服务端 + onSend({ + params, + }) + }) + .catch(() => { + onSend({ + params: chatMessage, + }) + }) + } else { + onSend({ + params: chatMessage, + }) + } + } catch (error) {} + }) + + // #endregion + + const isShowRetract = useCreation(() => { + return mode === 'task' && showFreeChat + }, [mode, showFreeChat]) + const isShowExpand = useCreation(() => { + return mode === 'task' && !showFreeChat + }, [mode, showFreeChat]) + const handleSwitchShowFreeChat = useMemoizedFn((v) => { + setShowFreeChat(v) + }) + + const onSetQuestion = useMemoizedFn((value: string) => { + aiChatTextareaRef?.current?.setValue(value ?? '') + }) + return ( +
    +
    +
    +
    +
    + + {title} + {focusMode && 专注模式:{focusMode}} +
    +
    + {isShowRetract && + (externalParameters?.rightIcon ?? ( + handleSwitchShowFreeChat(false)} /> + ))} +
    +
    + +
    +
    +
    +
    + {execute && questionQueue?.total > 0 && } +
    + + {execute && ( + + )} +
    + } + footerLeftTypes={externalParameters?.footerLeftTypes} + /> +
    +
    +
    +
    +
    +
    handleSwitchShowFreeChat(true)}> + +
    自由对话
    +
    + + ) + }), +) diff --git a/src/pages/AIAgent/ai-re-act/aiReActChat/AIReActChatType.ts b/src/pages/AIAgent/ai-re-act/aiReActChat/AIReActChatType.ts new file mode 100644 index 00000000..64c63ca2 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/aiReActChat/AIReActChatType.ts @@ -0,0 +1,60 @@ +import type { + AIAgentChatMode, + HandleStartParams, +} from '@/pages/AIAgent/ai-agent/aiAgentChat/type'; +import type { AIChatQSData } from '../hooks/aiRender'; +import type { AIInputEvent } from '../hooks/grpcApi'; +import type { + AIChatTextareaProps, + AIChatTextareaRefProps, +} from '@/pages/AIAgent/ai-agent/template/type'; + +export interface AIReActChatRefProps extends AIChatTextareaRefProps { + handleStart: (value: HandleStartParams) => void; +} +export interface AIHandleStartParams { + params: AIInputEvent; +} +export interface AIHandleStartExtraProps { + chatId?: string; +} +export interface AIHandleStartResProps { + params: AIInputEvent; + extraParams?: AIHandleStartExtraProps; + onChat?: () => void; + onChatFromHistory?: (sessionID: string) => void; +} +export interface AISendParams { + params: AIInputEvent; +} +export interface AISendResProps { + params: AIInputEvent; +} +export interface AIReActChatProps { + mode: AIAgentChatMode; + chatContainerClassName?: string; + chatContainerHeaderClassName?: string; + showFreeChat: boolean; + setShowFreeChat: (show: boolean) => void; + title?: React.ReactNode; + ref?: React.ForwardedRef; + startRequest: (v: AIHandleStartParams) => Promise; + sendRequest?: (v: AISendParams) => Promise; + externalParameters?: { + rightIcon?: string | React.ReactNode; + isOpen?: boolean; + filterMentionType?: AIChatTextareaProps['filterMentionType']; + footerLeftTypes?: AIChatTextareaProps['footerLeftTypes']; + }; +} + +export interface AIReActLogProps { + logs: AIChatQSData[]; + setLogVisible: (visible: boolean) => void; +} + +export interface AIReActTimelineMessageProps { + message?: string; + loading: boolean; + setLoading: (loading: boolean) => void; +} diff --git a/src/pages/AIAgent/ai-re-act/aiReActChat/AIReActComponent.tsx b/src/pages/AIAgent/ai-re-act/aiReActChat/AIReActComponent.tsx new file mode 100644 index 00000000..31cd07cc --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/aiReActChat/AIReActComponent.tsx @@ -0,0 +1,46 @@ +import styles from './AIReActChat.module.scss' +import { SolidStopIcon } from '@/assets/icon/solid' +import React from 'react' +import { OutlineChevrondownIcon, OutlineChevronleftIcon, OutlinePaperclipIcon } from '@/assets/icon/outline' +import { YakitButton, type YakitButtonProp } from '@/compoments/yakitUI/YakitButton/YakitButton' + +export const RoundedStopButton: React.FC = React.memo((props) => { + return ( + } + radius="50%" + {...props} + /> + ) +}) + +export const ChevrondownButton: React.FC = React.memo((props) => { + return ( + } + size="small" + radius="50%" + {...props} + /> + ) +}) + +export const ChevronleftButton: React.FC = React.memo((props) => { + return } {...props} /> +}) + +export const UploadFileButton: React.FC = React.memo((props) => { + return ( + } + radius="50%" + {...props} + /> + ) +}) diff --git a/src/pages/AIAgent/ai-re-act/aiReActChatContents/AIReActChatContents.module.scss b/src/pages/AIAgent/ai-re-act/aiReActChatContents/AIReActChatContents.module.scss new file mode 100644 index 00000000..c381a8a1 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/aiReActChatContents/AIReActChatContents.module.scss @@ -0,0 +1,71 @@ +.ai-re-act-chat-contents { + width: 100%; + overflow: hidden auto; + display: flex; + justify-content: center; + padding-left: 4px; + flex: 1; + .re-act-contents-list { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + height: 100%; + } + .item-wrapper { + width: 100%; + box-sizing: border-box; + padding-right: 12px; + max-width: 784px; + margin: 0 auto; + .item-inner { + padding-top: 4px; + } + &:last-child { + padding-bottom: 0; + } + } + // 后代类名ai-stream-chat-content-wrapper且下一个也是 的不要 padding-top + .item-wrapper:has(:global(.ai-stream-chat-content-wrapper)) { + & + .item-wrapper:has(:global(.ai-stream-chat-content-wrapper)) { + .item-inner:has(:global(.ai-stream-chat-content-wrapper)) { + padding-top: 0; + } + } + } + .spin { + display: flex; + flex-direction: row; + justify-content: center; + gap: 4px; + padding: 4px; + box-sizing: border-box; + div { + color: var(--Colors-Use-Neutral-Text-4-Help-text); + font-size: 12px; + } + } +} +.ai-reference-node { + color: var(--Colors-Use-Main-Primary); + font-size: 11px; + font-style: normal; + font-weight: 400; + line-height: 14px; + margin-left: 4px; + letter-spacing: 0.5px; + cursor: pointer; + z-index: 1; +} +// .ai-text-markdown-reference-node { +// } +// .ai-yaklang-reference-node { +// padding-top: 8px; +// } +// .ai-text-plain-reference-node { +// } +// .ai-log-tool-reference-node { +// } +// .ai-stream-chat-reference-node { +// padding: 0 12px 8px 12px; +// } diff --git a/src/pages/AIAgent/ai-re-act/aiReActChatContents/AIReActChatContents.tsx b/src/pages/AIAgent/ai-re-act/aiReActChatContents/AIReActChatContents.tsx new file mode 100644 index 00000000..38cb939d --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/aiReActChatContents/AIReActChatContents.tsx @@ -0,0 +1,251 @@ +import React, { useCallback, useMemo, useState } from 'react' +import type { AIReActChatContentsPProps, AIReferenceNodeProps, AIStreamNodeProps } from './AIReActChatContentsType' +import styles from './AIReActChatContents.module.scss' +import { useCreation } from 'ahooks' +import { AIChatToolColorCard } from '@/pages/AIAgent/ai-agent/components/aiChatToolColorCard/AIChatToolColorCard' +import { AIMarkdown } from '@/pages/AIAgent/ai-agent/components/aiMarkdown/AIMarkdown' +import { AIStreamChatContent } from '@/pages/AIAgent/ai-agent/components/aiStreamChatContent/AIStreamChatContent' +import StreamCard from '@/pages/AIAgent/ai-agent/components/StreamCard' +import { taskAnswerToIconMap } from '@/pages/AIAgent/ai-agent/defaultConstant' +import useAINodeLabel from '../hooks/useAINodeLabel' +import { AIChatListItem } from '@/pages/AIAgent/ai-agent/components/aiChatListItem/AIChatListItem' +import { AIYaklangCode } from '@/pages/AIAgent/ai-agent/components/aiYaklangCode/AIYaklangCode' +import type { ModalInfoProps } from '@/pages/AIAgent/ai-agent/components/ModelInfo' +import { AIStreamContentType } from '../hooks/defaultConstant' +import { Virtuoso } from 'react-virtuoso' +import useVirtuosoAutoScroll from '../hooks/useVirtuosoAutoScroll' +import type { ReActChatRenderItem } from '../hooks/aiRender' +import useChatIPCStore from '@/pages/AIAgent/ai-agent/useContext/ChatIPCContent/useStore' +import Loading from '@/compoments/Loading/Loading' +import useAISystemStream from '../hooks/useAISystemStream' +import { ScrollText } from '@/pages/AIAgent/ai-agent/chatTemplate/TaskLoading/TaskLoading' +import { Code } from '@/pages/AIAgent/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard' +import { YakitModal } from '@/compoments/yakitUI/YakitModal/YakitModal' +import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton' + +const getAIReferenceNodeByType = (contentType?: string) => { + switch (contentType) { + case AIStreamContentType.TEXT_MARKDOWN: + return styles['ai-text-markdown-reference-node'] + case AIStreamContentType.CODE_YAKLANG: + case AIStreamContentType.CODE_HTTP_REQUEST: + return styles['ai-yaklang-reference-node'] + case AIStreamContentType.TEXT_PLAIN: + return styles['ai-text-plain-reference-node'] + case AIStreamContentType.LOG_TOOL: + return styles['ai-log-tool-reference-node'] + default: + return styles['ai-stream-chat-reference-node'] + } +} +export const AIStreamNode: React.FC = React.memo((props) => { + const { stream, aiMarkdownProps } = props + const { reference } = stream + const { NodeId, content, NodeIdVerbose, CallToolID, ContentType } = stream.data + const { yakExecResult } = useChatIPCStore().chatIPCData + const { nodeLabel } = useAINodeLabel(NodeIdVerbose) + + const modalInfo: ModalInfoProps = useCreation(() => { + return { + time: stream.Timestamp, + title: stream.AIModelName, + icon: stream.AIService, + } + }, [stream.Timestamp, stream.AIModelName, stream.AIService]) + const referenceNode = useCreation(() => { + const className = getAIReferenceNodeByType(ContentType) + return reference ? ( + + ) : ( + // eslint-disable-next-line react/jsx-no-useless-fragment + <> + ) + }, [reference, ContentType]) + + switch (ContentType) { + case AIStreamContentType.TEXT_MARKDOWN: + return ( + + ) + case AIStreamContentType.CODE_YAKLANG: + case AIStreamContentType.CODE_HTTP_REQUEST: + return ( + + ) + case AIStreamContentType.TEXT_PLAIN: + // eslint-disable-next-line no-case-declarations + const { execFileRecord } = yakExecResult + // eslint-disable-next-line no-case-declarations + const fileList = execFileRecord.get(CallToolID) + return ( + + ) + case AIStreamContentType.LOG_TOOL: + return + case AIStreamContentType.LOG_TOOL_ERROR_OUTPUT: + // eslint-disable-next-line react/jsx-no-useless-fragment + return <> + default: + return + } +}) +export const AIReActChatContents: React.FC = React.memo((props) => { + const { chats } = props + const { + casualStatus: { loading, title }, + systemStream, + } = useChatIPCStore().chatIPCData + const { virtuosoRef, setScrollerRef, handleTotalListHeightChanged } = useVirtuosoAutoScroll({ + total: chats.elements.length, + atBottomThreshold: 50, + }) + + const renderItem = useCallback( + (index: number, item?: ReActChatRenderItem) => { + if (!item?.token) return null + const hasNext = chats.elements.length - index > 1 + return + }, + [chats.elements.length], + ) + + const Item = useCallback( + ({ + children, + style, + 'data-index': dataIndex, + }: { + children?: React.ReactNode + style?: React.CSSProperties + 'data-index'?: number + }) => ( +
    +
    {children}
    +
    + ), + [], + ) + + const { displayValue, mode } = useAISystemStream({ + value: title, + systemStream, + }) + const Footer = useCallback( + () => + loading ? ( +
    + +
    + {mode === 'value' ? displayValue : } +
    +
    +
    + ) : null, + [loading, mode, displayValue], + ) + + const components = useMemo( + () => ({ + Item, + Footer, + }), + [Footer, Item], + ) + + return ( +
    + renderItem(index, item)} + initialTopMostItemIndex={chats.elements.length > 1 ? chats.elements.length - 1 : 0} + components={components} + atBottomThreshold={50} + increaseViewportBy={{ top: 300, bottom: 300 }} + className={styles['re-act-contents-list']} + /> +
    + ) +}) + +export const AIReferenceNode: React.FC = React.memo((props) => { + const { referenceList } = props + const [expand, setExpand] = useState(false) + + return ( + <> + { + e.stopPropagation() + setExpand(false) + }} + footer={ +
    + { + e.stopPropagation() + setExpand(false) + }} + > + 关闭 + +
    + } + > + +
    + { + e.stopPropagation() + setExpand(true) + }} + > + [参考资料] + + + ) +}) diff --git a/src/pages/AIAgent/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts b/src/pages/AIAgent/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts new file mode 100644 index 00000000..632ba68b --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts @@ -0,0 +1,19 @@ +import type { + ChatReferenceMaterialPayload, + ChatStream, +} from '../hooks/aiRender'; +import type { UseCasualChatState } from '../hooks/type'; + +export interface AIReActChatContentsPProps { + chats: UseCasualChatState; +} + +export interface AIStreamNodeProps { + stream: ChatStream; + aiMarkdownProps?: { className: string }; +} + +export interface AIReferenceNodeProps { + referenceList: ChatReferenceMaterialPayload; + className?: string; +} diff --git a/src/pages/AIAgent/ai-re-act/aiReActTaskChat/AIReActTaskChat.module.scss b/src/pages/AIAgent/ai-re-act/aiReActTaskChat/AIReActTaskChat.module.scss new file mode 100644 index 00000000..139f66cf --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/aiReActTaskChat/AIReActTaskChat.module.scss @@ -0,0 +1,105 @@ +@use "../aiReActChat/AIReActChat.module.scss" as AIReActChat; + +%display-flex-center { + display: flex; + align-items: center; +} +%display-column-center { + display: flex; + flex-direction: column; + align-items: center; +} +.tab-wrap { + width: 100%; + overflow: hidden; +} +.tab-content { + flex: 1; + width: 100%; + overflow: hidden; +} +.ai-re-act-task-chat { + @extend %display-flex-center; + align-items: flex-start; + flex: 1; + // overflow: auto hidden; + height: 100%; + @include AIReActChat.content-side("left"); + .chat-content-wrapper { + @extend %display-column-center; + flex: 1; + overflow: hidden; + height: 100%; + width: 100%; + position: relative; + min-width: 400px; + } + .header { + @extend %display-flex-center; + justify-content: space-between; + gap: 4px; + padding: 12px 12px 8px 12px; + border-bottom: 1px solid var(--Colors-Use-Neutral-Border); + width: 100%; + height: 45px; + .title { + @extend %display-flex-center; + gap: 4px; + color: var(--Colors-Use-Neutral-Text-1-Title); + + /* PingFang SC/T6/Medium */ + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + letter-spacing: 0.1px; + svg { + width: 20px; + height: 20px; + } + } + .extra { + @extend %display-flex-center; + gap: 8px; + flex-shrink: 0; + + .divider-style { + width: 1px; + height: 12px; + border-left: 1px solid var(--Colors-Use-Neutral-Border); + } + } + } +} +.footer { + position: absolute; + bottom: 8px; + margin: 0 auto; + display: flex; + gap: 12px; + .position-button { + border-color: var(--Colors-Use-Neutral-Border); + box-shadow: 0 4px 8px 0 var(--Colors-Use-Basic-Shadow); + svg { + color: var(--Colors-Use-Neutral-Text-3-Secondary); + } + &:hover { + border-color: var(--Colors-Use-Main-Primary); + svg { + color: var(--Colors-Use-Main-Primary); + } + } + } + .task-sub-button { + border-color: var(--Colors-Use-Error-Border); + background: var(--Colors-Use-Basic-Background); + &:hover { + border-color: var(--Colors-Use-Error-Primary); + } + } + .task-button { + &:hover { + background-color: var(--Colors-Use-Error-Hover); + } + } +} diff --git a/src/pages/AIAgent/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx b/src/pages/AIAgent/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx new file mode 100644 index 00000000..ca491f3e --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx @@ -0,0 +1,226 @@ +import React, { useEffect, useMemo, useState } from 'react' +import type { + AIReActTaskChatContentProps, + AIReActTaskChatLeftSideProps, + AIReActTaskChatProps, +} from './AIReActTaskChatType' +import styles from './AIReActTaskChat.module.scss' +import { ColorsBrainCircuitIcon } from '@/assets/icon/colors' +import { useControllableValue, useCreation, useMemoizedFn } from 'ahooks' +import classNames from 'classnames' +import { ChevrondownButton } from '../aiReActChat/AIReActComponent' +import { + OutlineArrowscollapseIcon, + OutlineArrowsexpandIcon, + OutlineExitIcon, + OutlinePositionIcon, + RedoDotIcon, +} from '@/assets/icon/outline' +import type { AIReviewType } from '../hooks/aiRender' +import { AIInputEventSyncTypeEnum } from '../hooks/grpcApi' +import { Tooltip } from 'antd' + +import { AIAgentChatStream, AIChatLeftSide } from '../../ai-agent/chatTemplate/AIAgentChatTemplate' +import useChatIPCStore from '../../ai-agent/useContext/ChatIPCContent/useStore' +import { AIReActTaskChatReview } from '../../ai-agent/aiAgentChat/AIAgentChat' +import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton' +import useChatIPCDispatcher from '../../ai-agent/useContext/ChatIPCContent/useDispatcher' +import { YakitPopconfirm } from '@/compoments/yakitUI/YakitPopconfirm/YakitPopconfirm' +import type { AIReActTaskChatReviewProps } from '../../ai-agent/aiAgentChat/type' +import useAIAgentStore from '../../ai-agent/useContext/useStore' + +const AIReActTaskChat: React.FC = React.memo((props) => { + const { setShowFreeChat, setTimeLine } = props + const { taskChat } = useChatIPCStore().chatIPCData + const [leftExpand, setLeftExpand] = useState(true) + const [expand, setExpand] = useState(false) + + const onIsExpand = useMemoizedFn(() => { + setLeftExpand(expand) + setShowFreeChat(expand) + setExpand((v) => !v) + }) + + useEffect(() => { + setTimeLine(leftExpand) + }, [leftExpand]) + + return ( +
    + + {!!taskChat?.elements?.length && ( +
    +
    +
    + + 深度规划 +
    +
    + : } + onClick={onIsExpand} + /> +
    +
    + +
    + )} +
    + ) +}) + +export default AIReActTaskChat + +const AIReActTaskChatContent: React.FC = React.memo(() => { + const { reviewInfo, planReviewTreeKeywordsMap, chatIPCData } = useChatIPCStore() + + const { activeChat } = useAIAgentStore() + const { taskChat } = chatIPCData + + const { handleSendSyncMessage, chatIPCEvents } = useChatIPCDispatcher() + + const streams = useCreation(() => { + return taskChat.elements + }, [taskChat.elements]) + + const [scrollToBottom, setScrollToBottom] = useState(false) + const onScrollToBottom = useMemoizedFn(() => { + setScrollToBottom((v) => !v) + }) + const getTaskId = useMemoizedFn(() => { + return chatIPCEvents.fetchTaskChatID()?.taskID + }) + /** 取消当前指定任务 */ + const onStopTask = useMemoizedFn(() => { + const taskId = getTaskId() + if (!taskId) return + handleSendSyncMessage({ + syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CANCEL_TASK, + SyncJsonInput: JSON.stringify({ task_id: taskId }), + }) + if (reviewInfo) { + chatIPCEvents.handleTaskReviewRelease((reviewInfo.data as AIReviewType).id) + } + }) + /** 取消当前执行的子任务 */ + const onStopSubTask = useMemoizedFn(() => { + handleSendSyncMessage({ + syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_SKIP_SUBTASK_IN_PLAN, + SyncJsonInput: JSON.stringify({ + reason: '用户认为这个任务不需要执行', + skip_current_task: true, + }), + }) + if (reviewInfo) { + chatIPCEvents.handleTaskReviewRelease((reviewInfo.data as AIReviewType).id) + } + }) + const onExtraAction: AIReActTaskChatReviewProps['onExtraAction'] = useMemoizedFn((type) => { + switch (type) { + case 'stopTask': + onStopTask() + break + case 'stopSubTask': + onStopSubTask() + break + default: + break + } + }) + return ( + <> +
    + +
    + {reviewInfo ? ( + 0} + /> + ) : ( + streams.length > 0 && ( +
    + {!!getTaskId() && ( + <> + {taskChat.plan.length > 0 && ( + + } + className={styles['task-sub-button']} + radius="28px" + size="large" + colors="danger" + > + 跳过子任务 + + + )} + + + } + className={styles['task-button']} + radius="28px" + size="large" + colors="danger" + /> + + + + )} + } + radius="50%" + onClick={onScrollToBottom} + className={styles['position-button']} + size="large" + /> +
    + ) + )} + + ) +}) + +export const AIReActTaskChatLeftSide: React.FC = React.memo((props) => { + const { taskChat } = useChatIPCStore().chatIPCData + const [leftExpand, setLeftExpand] = useControllableValue(props, { + defaultValue: true, + valuePropName: 'leftExpand', + trigger: 'setLeftExpand', + }) + const hasStreams = useMemo(() => { + return (taskChat?.elements?.length ?? 0) > 0 + }, [taskChat?.elements?.length]) + + return ( +
    + +
    setLeftExpand(true)}> + +
    任务列表
    +
    +
    + ) +}) diff --git a/src/pages/AIAgent/ai-re-act/aiReActTaskChat/AIReActTaskChatType.d.ts b/src/pages/AIAgent/ai-re-act/aiReActTaskChat/AIReActTaskChatType.d.ts new file mode 100644 index 00000000..381c48c8 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/aiReActTaskChat/AIReActTaskChatType.d.ts @@ -0,0 +1,12 @@ +export interface AIReActTaskChatProps { + setShowFreeChat: (show: boolean) => void; + setTimeLine: (show: boolean) => void; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface AIReActTaskChatContentProps {} + +export interface AIReActTaskChatLeftSideProps { + leftExpand: boolean; + setLeftExpand: (v: boolean) => void; +} diff --git a/src/pages/AIAgent/ai-re-act/aiReviewRuleSelect/AIReviewRuleSelect.module.scss b/src/pages/AIAgent/ai-re-act/aiReviewRuleSelect/AIReviewRuleSelect.module.scss new file mode 100644 index 00000000..bd6ae354 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/aiReviewRuleSelect/AIReviewRuleSelect.module.scss @@ -0,0 +1,180 @@ +%icon { + width: 16px; + height: 16px; +} + +.ai-chat-select-wrapper { + width: fit-content; + height: 24px; + overflow: hidden; + :global { + .ant-select { + border-radius: 20px; + background-color: transparent; + } + .ant-select-selection-search { + display: none; + } + .ant-select-single:hover { + background-color: var(--Colors-Use-Basic-Shadow); + } + .ant-select-single .ant-select-selector .ant-select-selection-item { + font-size: 11px; + display: flex; + align-items: center; + height: 16px; + line-height: 16px; + } + .ant-select-single:not(.ant-select-customize-input) .ant-select-selector { + padding: 4px; + display: flex; + align-items: center; + } + .ant-select .ant-select-arrow { + right: 0; + height: 16px; + padding: 0 4px; + top: 10px; + } + } +} + +.drop-select-wrapper { + max-width: 320px; + :global { + .ant-select-item-option-grouped { + padding-left: 8px; + } + .ant-select-item { + margin-bottom: 4px; + } + } + .select-title { + display: flex; + align-items: center; + gap: 4px; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 600; + padding: 2px 12px 4px 12px; + + svg { + color: var(--Colors-Use-Neutral-Text-2-Primary); + @extend %icon; + } + } +} + +.select-option-wrapper { + display: flex; + gap: 8px; + font-size: 12px; + .icon-wrapper { + width: 16px; + height: 16px; + display: flex; + svg { + @extend %icon; + } + } + .text-wrapper { + display: flex; + flex-direction: column; + gap: 4px; + line-height: 16px; + } + + .text { + color: var(--Colors-Use-Neutral-Text-1-Title); + } + .describe { + color: var(--Colors-Use-Neutral-Text-4-Help-text); + text-wrap: wrap; + } +} +.select-option-active-wrapper { + .text { + color: var(--Colors-Use-Main-Primary); + } +} +.select-option { + display: flex; + align-items: center; + gap: 4px; + color: var(--Colors-Use-Neutral-Text-1-Title); + + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + overflow: hidden; + + .select-option-text { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + word-break: break-all; + line-height: 16px; + } + .icon-wrapper { + display: flex; + width: 16px; + height: 16px; + color: var(--Colors-Use-Neutral-Text-2-Primary); + svg { + @extend %icon; + } + } +} + +.popover-wrapper { + width: 200px; + display: flex; + align-items: center; +} + +.review-rule-select-wrapper { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; + .siren-icon { + svg { + color: var(--Colors-Use-Neutral-Text-2-Primary); + } + } +} +.yakit-segmented-wrapper { + .segmented { + padding: 1px; + height: 24px; + display: flex; + align-items: center; + background-color: var(--Colors-Use-Basic-Shadow); + border-radius: 999px; + color: var(--Colors-Use-Neutral-Text-4-Help-text); + } + :global { + .ant-segmented-group { + height: 22px; + } + .ant-segmented-thumb { + border-radius: 50%; + } + .ant-segmented.ant-segmented-sm .ant-segmented-item-label { + height: 22px; + padding: 4px; + display: flex; + align-items: center; + } + .ant-segmented-item-icon { + svg { + width: 14px; + height: 14px; + } + } + .ant-segmented-item { + border-radius: 50%; + } + } +} diff --git a/src/pages/AIAgent/ai-re-act/aiReviewRuleSelect/AIReviewRuleSelect.tsx b/src/pages/AIAgent/ai-re-act/aiReviewRuleSelect/AIReviewRuleSelect.tsx new file mode 100644 index 00000000..7c402c4a --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/aiReviewRuleSelect/AIReviewRuleSelect.tsx @@ -0,0 +1,232 @@ +import React, { useEffect, useRef, useState } from 'react' +import type { AIChatSelectProps, ReviewRuleSelectProps } from './type' +import useAIAgentStore from '../../ai-agent/useContext/useStore' +import useAIAgentDispatcher from '../../ai-agent/useContext/useDispatcher' +import useChatIPCStore from '../../ai-agent/useContext/ChatIPCContent/useStore' +import useChatIPCDispatcher from '../../ai-agent/useContext/ChatIPCContent/useDispatcher' +import { AIInputEventHotPatchTypeEnum, type AIStartParams } from '../hooks/grpcApi' +import { useClickAway, useControllableValue, useCreation, useInViewport, useMemoizedFn } from 'ahooks' +import emiter from '@/utils/eventBus/eventBus' +import type { AIReviewRuleOptionsType } from '../../ai-agent/defaultConstant' +import { AIAgentSettingDefault, AIReviewRuleIconMap, AIReviewRuleOptions } from '../../ai-agent/defaultConstant' +import { isEqual } from 'lodash' +import classNames from 'classnames' +import styles from './AIReviewRuleSelect.module.scss' +import { YakitSegmented } from '@/compoments/yakitUI/YakitSegmented/YakitSegmented' +import type { AIAgentSetting } from '../../ai-agent/aiAgentType' +import { Tooltip } from 'antd' +import { YakitPopover } from '@/compoments/yakitUI/YakitPopover/YakitPopover' +import { FormItemSlider } from '../../ai-agent/AIChatSetting/AIChatSetting' +import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton' +import { OutlineSirenIcon } from '@/assets/icon/outline' +import { YakitSelect } from '@/compoments/yakitUI/YakitSelect/YakitSelect' + +const AIReviewRuleSelect: React.FC = React.memo((props) => { + const { setting } = useAIAgentStore() + const { setSetting } = useAIAgentDispatcher() + + const { chatIPCData } = useChatIPCStore() + const { handleSendConfigHotpatch } = useChatIPCDispatcher() + + const [visible, setVisible] = useState(false) + const [selectAIReviewRiskControlScore, setAIReviewRiskControlScore] = + useState() + + const refRef = useRef(null) + const [inViewport = true] = useInViewport(refRef) + + useEffect(() => { + if (!inViewport) return + emiter.on('onRefreshAIReviewRuleSelect', onRefreshAIReviewRuleSelect) + return () => { + emiter.off('onRefreshAIReviewRuleSelect', onRefreshAIReviewRuleSelect) + } + }, [inViewport]) + + const modelValue = useCreation(() => { + return 'yolo' + // return setting?.ReviewPolicy || AIAgentSettingDefault.ReviewPolicy + }, [setting?.ReviewPolicy, chatIPCData.execute]) + + const aiReviewRiskControlScore = useCreation(() => { + return setting?.AIReviewRiskControlScore || AIAgentSettingDefault.AIReviewRiskControlScore + }, [setting?.AIReviewRiskControlScore]) + + const onRefreshAIReviewRuleSelect = useMemoizedFn((res: string) => { + if (!chatIPCData.execute) return + const data = JSON.parse(res) + if (data?.reviewPolicy) { + handHotpatchReviewPolicy(data?.reviewPolicy as AIStartParams['ReviewPolicy']) + } + if (data?.aiReviewRiskControlScore) { + handHotpatchAIReviewRiskControlScore(data?.aiReviewRiskControlScore) + } + }) + + const handHotpatchReviewPolicy = useMemoizedFn((value: AIStartParams['ReviewPolicy']) => { + handleSendConfigHotpatch({ + hotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_AgreePolicy, + params: { + ReviewPolicy: value, + }, + }) + }) + + const handHotpatchAIReviewRiskControlScore = useMemoizedFn((value: number) => { + handleSendConfigHotpatch({ + hotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_RiskControlScore, + params: { + AIReviewRiskControlScore: value, + }, + }) + }) + + const onSelectModel = useMemoizedFn((value: AIStartParams['ReviewPolicy']) => { + setSetting && setSetting((old) => ({ ...old, ReviewPolicy: value })) + if (chatIPCData.execute && modelValue) { + handHotpatchReviewPolicy(value) + } + }) + + const onSetAIReviewRiskControlScore = useMemoizedFn((value?: number) => { + setSetting && setSetting((old) => ({ ...old, AIReviewRiskControlScore: value || 0 })) + }) + + const onVisibleChange = useMemoizedFn((v: boolean) => { + setVisible(v) + if ( + !v && + chatIPCData.execute && + selectAIReviewRiskControlScore && + !isEqual(selectAIReviewRiskControlScore, aiReviewRiskControlScore) + ) { + handHotpatchAIReviewRiskControlScore(selectAIReviewRiskControlScore) + onSetAIReviewRiskControlScore(selectAIReviewRiskControlScore) + } + if (v) setAIReviewRiskControlScore(aiReviewRiskControlScore) + }) + const getIcon = useMemoizedFn((value: AIReviewRuleOptionsType) => { + if (modelValue === value) { + return AIReviewRuleIconMap[value]?.activeIcon + } + return AIReviewRuleIconMap[value]?.icon + }) + return ( +
    + { + const showType = v as AIAgentSetting['ReviewPolicy'] + onSelectModel(showType) + }} + options={AIReviewRuleOptions.map((item) => ({ + icon: {getIcon(item.value)}, + value: item.value, + }))} + size="small" + wrapClassName={styles['yakit-segmented-wrapper']} + className={styles['segmented']} + /> + {modelValue === 'ai' && ( + + 风险阈值: + +
    + } + trigger={['hover', 'click']} + visible={visible} + onVisibleChange={onVisibleChange} + > + } + onClick={(e) => { + e.stopPropagation() + }} + /> + + )} + + ) +}) +export default AIReviewRuleSelect + +export const AIChatSelect: React.FC = React.memo((props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { getList, dropdownRender, children, setOpen: defSetOpen, ...rest } = props + const [open, setOpen] = useControllableValue(props, { + defaultValue: false, + valuePropName: 'open', + trigger: 'setOpen', + }) + + const selectWrapperRef = useRef(null) + const dropdownRenderRef = useRef(null) + const isHoveringByDropdown = useRef(false) + + useClickAway(() => { + if (open) setOpen(false) + }, [selectWrapperRef]) + + const onMouseEnterDropdown = useMemoizedFn((e) => { + e.stopPropagation() + isHoveringByDropdown.current = true + }) + const onMouseLeaveDropdown = useMemoizedFn((e) => { + e.stopPropagation() + setOpen(false) + isHoveringByDropdown.current = false + }) + const onSelectWrapperClick = useMemoizedFn((e) => { + e.stopPropagation() + onOpen() + }) + const onMouseEnterSelectWrapper = useMemoizedFn((e: React.MouseEvent) => { + e.stopPropagation() + onOpen() + }) + const onMouseLeaveSelectWrapper = useMemoizedFn((e) => { + e.stopPropagation() + setTimeout(() => { + if (!isHoveringByDropdown.current && open) setOpen(false) + }, 500) + }) + const onOpen = useMemoizedFn(() => { + setOpen(true) + getList && getList() + }) + const onDropdownRender = useMemoizedFn((menu) => ( +
    + {dropdownRender(menu, setOpen)} +
    + )) + return ( +
    + + {children} + +
    + ) +}) diff --git a/src/pages/AIAgent/ai-re-act/aiReviewRuleSelect/type.d.ts b/src/pages/AIAgent/ai-re-act/aiReviewRuleSelect/type.d.ts new file mode 100644 index 00000000..a35fc37e --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/aiReviewRuleSelect/type.d.ts @@ -0,0 +1,14 @@ +import type { YakitSelectProps } from '@/compoments/yakitUI/YakitSelect/YakitSelectType' + +export type AIReviewRuleSelectProps = ReviewRuleSelectProps + +export interface ReviewRuleSelectProps { + className?: string +} + +export interface AIChatSelectProps extends Omit { + dropdownRender: (menu: React.ReactElement, setOpen: (open: boolean) => void) => React.ReactElement + getList?: () => void + children?: React.ReactNode + setOpen?: (open: boolean) => void +} diff --git a/src/pages/AIAgent/ai-re-act/hooks/aiRender.ts b/src/pages/AIAgent/ai-re-act/hooks/aiRender.ts new file mode 100644 index 00000000..02e43218 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/aiRender.ts @@ -0,0 +1,252 @@ +import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' +import type { AIAgentGrpcApi, AIInputEvent, AIOutputEvent, AIOutputI18n, AITaskStatus } from './grpcApi' +import type { AIChatIPCStartParams } from './type' + +/** 工具流式输出里的可选操作列表 */ +export interface ToolStreamSelectors { + callToolId: string + InteractiveId: string + selectors: AIAgentGrpcApi.ReviewSelector[] +} + +/** 流式输出的信息内容 */ +export interface AIStreamOutput { + TaskIndex?: AIOutputEvent['TaskIndex'] + CallToolID: AIOutputEvent['CallToolID'] + EventUUID: AIOutputEvent['EventUUID'] + NodeId: AIOutputEvent['NodeId'] + NodeIdVerbose: AIOutputEvent['NodeIdVerbose'] + status: 'start' | 'end' + content: string + ContentType: AIOutputEvent['ContentType'] + selectors?: ToolStreamSelectors +} + +/** 工具结果的信息内容 */ +export interface AIToolResult { + type: '' | 'stream' | 'result' + TaskIndex?: AIOutputEvent['TaskIndex'] + callToolId: string + /** 工具名称 */ + toolName: string + /** 工具介绍 */ + toolDescription: string + /** 间隔时间(ms) */ + durationMS: AIAgentGrpcApi.AIToolCall['duration_ms'] + /** 间隔时间(s) */ + durationSeconds: AIAgentGrpcApi.AIToolCall['duration_seconds'] + /** 结束时间戳(s) */ + endTime: AIAgentGrpcApi.AIToolCall['end_time'] + /** 结束时间戳(ms) */ + endTimeMS: AIAgentGrpcApi.AIToolCall['end_time_ms'] + /** 开始时间戳(s) */ + startTime: AIAgentGrpcApi.AIToolCall['start_time'] + /** 开始时间戳(ms) */ + startTimeMS: AIAgentGrpcApi.AIToolCall['start_time_ms'] + stream: { EventUUID: AIOutputEvent['EventUUID'] } + tool: { + /** 工具执行完成的状态 default是后端没有发送状态type时前端默认值 */ + status: 'default' | 'success' | 'failed' | 'user_cancelled' + /** 执行完后的总结 */ + summary: string + /** tool stdout 内容展示前200个字符 */ + toolStdoutContent: { + content: string + /** @deprecated UI展示不显示 */ + isShowAll: boolean + } + /** 执行错误相关信息 */ + execError: string + /** 执行涉及的文件目录 */ + dirPath: string + /** 工具执行结果详情数据 */ + resultDetails: string + } +} + +/** 任务开始节点的信息 */ +export interface AITaskStartInfo { + taskIndex: string + taskName: string + goal: string + status?: AITaskStatus +} + +interface ReviewSelectedOption { + /** 已操作 review 的选项内容(json 模式) */ + selected?: string + optionValue?: string +} +/** 对 review 数据进行操作后的记录, 专用于 UI 上的历史展示 */ +export type UIPlanReview = AIAgentGrpcApi.PlanReviewRequire & + ReviewSelectedOption & { taskExtra?: Map } +export type UITaskReview = AIAgentGrpcApi.TaskReviewRequire & ReviewSelectedOption +export type UIToolUseReview = AIAgentGrpcApi.ToolUseReviewRequire & ReviewSelectedOption +export type UIRequireUserInteractive = AIAgentGrpcApi.AIReviewRequire & ReviewSelectedOption +export type UIExecAIForgeReview = AIAgentGrpcApi.ExecForgeReview & ReviewSelectedOption + +export type AIReviewType = + | UIPlanReview + | UITaskReview + | UIToolUseReview + | UIRequireUserInteractive + | UIExecAIForgeReview + +/** 插件执行中的文件操作记录 */ +export interface AIYakExecFileRecord extends StreamResult.Log { + /** 前端主动对接口流输出的文件记录进行先后操作的记录 */ + order: number +} + +/** 工具执行结果的决策展示数据 */ +export interface AIToolCallDecision extends Omit { + i18n: AIOutputI18n +} + +/** 任务规划-可执行任务的数据结构 */ +export interface AITaskInfoProps extends AIAgentGrpcApi.PlanTask { + /** 层级(代表在树里的第几层) */ + level: number + /** 是否是叶子任务节点 */ + isLeaf: boolean +} + +/** 任务规划-执行崩溃后的错误信息展示 */ +export interface FailTaskChatError { + NodeId: AIOutputEvent['NodeId'] + NodeIdVerbose: AIOutputEvent['NodeIdVerbose'] + content: string +} + +/** 自由对话崩溃的错误信息 */ +export interface FailReactError { + NodeId: AIOutputEvent['NodeId'] + NodeIdVerbose: AIOutputEvent['NodeIdVerbose'] + content: string +} + +/** 会话参参考资料 */ +export type ChatReferenceMaterialPayload = AIAgentGrpcApi.ReferenceMaterialPayload[] + +export enum AIChatQSDataTypeEnum { + /** 用户的自由输入 */ + QUESTION = 'question', + /** 流 */ + STREAM = 'stream', + /** 思考 */ + THOUGHT = 'thought', + /** 结果 */ + RESULT = 'result', + /** 工具总结 */ + TOOL_RESULT = 'tool_result', + /** 计划审阅 */ + PLAN_REVIEW_REQUIRE = 'plan_review_require', + /** 任务审阅 */ + TASK_REVIEW_REQUIRE = 'task_review_require', + /** 工具审阅 */ + TOOL_USE_REVIEW_REQUIRE = 'tool_use_review_require', + /** AI主动询问 */ + REQUIRE_USER_INTERACTIVE = 'require_user_interactive', + /** 智能体/forge审阅 */ + EXEC_AIFORGE_REVIEW_REQUIRE = 'exec_aiforge_review_require', + /** Divider Card */ + TASK_INDEX_NODE = 'task_index_node', + /** 工具决策 */ + TOOL_CALL_DECISION = 'tool_call_decision', + /** 当前任务规划结束标志 */ + END_PLAN_AND_EXECUTION = 'end_plan_and_execution', + /** 任务规划崩溃的错误信息 */ + FAIL_PLAN_AND_EXECUTION = 'fail_plan_and_execution', + /** ReAct任务崩溃的错误信息 */ + FAIL_REACT = 'fail_react_task', + /** 工具结果 */ + TOOL_CALL_RESULT = 'tool_call_result', + /** 参考资料 */ + Reference_Material = 'reference_material', + /** stream数据集合组 */ + STREAM_GROUP = 'stream_group', +} + +/** 控制UI渲染的数据数组元素 */ +export interface ReActChatBaseInfo { + chatType: 'reAct' | 'task' + token: string + type: AIChatQSDataTypeEnum + /** 触发渲染的次数, 无实际逻辑意义 */ + renderNum: number +} +export interface ReActChatElement extends ReActChatBaseInfo { + /** 标记不是组 */ + isGroup?: false +} +export interface ReActChatGroupElement extends ReActChatBaseInfo { + /** 标记是组 */ + isGroup: true + children: ReActChatElement[] +} + +export type ReActChatRenderItem = ReActChatElement | ReActChatGroupElement + +// #region chat 问答内容组件的类型集合(包括了类型推导) +export interface AIChatQSDataBase { + type: T + data: U + id: string + chatType: ReActChatBaseInfo['chatType'] + AIService: AIOutputEvent['AIService'] + AIModelName: AIOutputEvent['AIModelName'] + Timestamp: AIOutputEvent['Timestamp'] + /** 前端专属数据,供前端逻辑和UI处理使用 */ + extraValue?: AIChatIPCStartParams['extraValue'] + /** 参考资料 */ + reference?: ChatReferenceMaterialPayload + /** 父集合组的key(如果被收集到集合组中, 则存在该字段) */ + parentGroupKey?: string +} + +type ChatQuestion = AIChatQSDataBase +export type ChatStream = AIChatQSDataBase +type ChatToolCallResult = AIChatQSDataBase +type ChatThought = AIChatQSDataBase +type ChatResult = AIChatQSDataBase +type ChatToolResult = AIChatQSDataBase +type ChatPlanReviewRequire = AIChatQSDataBase +type ChatTaskReviewRequire = AIChatQSDataBase +type ChatToolUseReviewRequire = AIChatQSDataBase +type ChatRequireUserInteractive = AIChatQSDataBase< + AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE, + UIRequireUserInteractive +> +type ChatExecAIForgeReview = AIChatQSDataBase +type ChatTaskIndexNode = AIChatQSDataBase +export type ChatToolCallDecision = AIChatQSDataBase +type ChatPlanExecEnd = AIChatQSDataBase +type ChatFailPlanAndExecution = AIChatQSDataBase +type ChatFailReact = AIChatQSDataBase +type ChatReferenceMaterial = AIChatQSDataBase< + AIChatQSDataTypeEnum.Reference_Material, + { NodeId: AIOutputEvent['NodeId']; NodeIdVerbose: AIOutputEvent['NodeIdVerbose'] } +> +/** 用于渲染State定义使用, 无实际逻辑意义 */ +type ChatStreamGroup = AIChatQSDataBase + +export type AIChatQSData = + | ChatQuestion + | ChatStream + | ChatThought + | ChatResult + | ChatToolResult + | ChatPlanReviewRequire + | ChatTaskReviewRequire + | ChatToolUseReviewRequire + | ChatRequireUserInteractive + | ChatExecAIForgeReview + | ChatTaskIndexNode + | ChatToolCallDecision + | ChatPlanExecEnd + | ChatFailPlanAndExecution + | ChatFailReact + | ChatToolCallResult + | ChatReferenceMaterial + | ChatStreamGroup +// #endregion diff --git a/src/pages/AIAgent/ai-re-act/hooks/defaultConstant.ts b/src/pages/AIAgent/ai-re-act/hooks/defaultConstant.ts new file mode 100644 index 00000000..41e3f330 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/defaultConstant.ts @@ -0,0 +1,140 @@ +import type { AIToolResult } from './aiRender' +import type { AIAgentGrpcApi, AIOutputI18n } from './grpcApi' +import type { AIQuestionQueues, CasualLoadingStatus, PlanLoadingStatus } from './type' + +/** 工具执行结果-默认值 */ +export const DefaultAIToolResult: AIToolResult = { + type: '', + callToolId: '', + toolName: '-', + toolDescription: '', + startTime: 0, + startTimeMS: 0, + endTime: 0, + endTimeMS: 0, + durationMS: 0, + durationSeconds: 0, + stream: { EventUUID: '' }, + tool: { + status: 'default', + summary: '', + toolStdoutContent: { + content: '', + isShowAll: false, + }, + execError: '', + dirPath: '', + resultDetails: '', + }, +} +/** 工作执行结果总结-不同阶段的默认展示内容 */ +export const DefaultToolResultSummary: Record = { + failed: { wait: '获取失败原因中...', result: '执行失败' }, + success: { wait: '执行结果正在总结中...', result: '执行成功' }, + user_cancelled: { wait: '工具调用取消中...', result: '用户取消' }, +} + +/** AI 流式输出中, NodeId 对应展示的内容 */ +const AIStreamNodeIdToLabel: Record = { + 're-act-loop': { label: '推理与行动' }, + 'call-forge': { label: '智能应用' }, + 'call-tools': { label: '工具调用' }, + review: { label: '审查系统' }, + liteforge: { label: '轻量智能应用' }, + directly_answer: { label: '直接回答' }, + 'memory-reducer': { label: '记忆裁剪' }, + 'memory-timeline': { label: '记忆浓缩' }, + execute: { label: '执行' }, + summary: { label: '总结' }, + 'create-subtasks': { label: '创建子任务' }, + 'freedom-plan-review': { label: '计划审查' }, + 'dynamic-plan': { label: '动态规划' }, + 're-act-verify': { label: '核实结果' }, + result: { label: '结果输出' }, + plan: { label: '任务规划' }, + decision: { label: '决策' }, + output: { label: '通用输出' }, + forge: { label: '智能应用' }, + 're-act-loop-thought': { label: '思考' }, + 're-act-loop-answer-payload': { label: 'AI 响应' }, + 'enhance-query': { label: '知识增强' }, +} +/** 传入 NodeId, 输出展示内容的18n 结构 */ +export const convertNodeIdToVerbose = (nodeId: string) => { + const label = AIStreamNodeIdToLabel[nodeId]?.label || nodeId + const verbose18n: AIOutputI18n = { + Zh: label, + En: label, + } + return verbose18n +} + +/** AI 判断 review 的风险阈值等级对应的展示内容 */ +export const AIReviewJudgeLevelMap: Record = { + low: { label: '低风险自动同意' }, + middle: { label: '等待用户否决' }, + high: { label: '需人工确认' }, +} + +/** 流内容的展示类型枚举 */ +export enum AIStreamContentType { + /** 默认 */ + DEFAULT = 'default', + /** md格式 */ + TEXT_MARKDOWN = 'text/markdown', + /** 卡片/多行 */ + TEXT_PLAIN = 'text/plain', + /** tool 紫色卡片 */ + LOG_TOOL = 'log/tool', + /** tool 错误输出 */ + LOG_TOOL_ERROR_OUTPUT = 'log/tool-error-output', + // TIP - 下面类型都展示为编辑器,截取后面得type为编辑器的语言类型Type, 例如 code/yaklang 展示为编辑器, 编辑器类型为 yak + /** YakitEditor */ + CODE_YAKLANG = 'code/yaklang', + /** YakitEditor */ + CODE_PYTHON = 'code/python', + /** 请求包 */ + CODE_HTTP_REQUEST = 'code/http-request', +} + +/** 问题队列-默认值 */ +export const DeafultAIQuestionQueues: AIQuestionQueues = { + total: 0, + data: [], +} + +/** 记忆列表默认值 */ +export const DefaultMemoryList: AIAgentGrpcApi.MemoryEntryList = { + memories: [], + memory_pool_limit: 0, + memory_session_id: 'default', + total_memories: 0, + total_size: 0, + score_overview: { + A_total: 0, + C_total: 0, + E_total: 0, + O_total: 0, + P_total: 0, + R_total: 0, + T_total: 0, + }, +} + +/** 自由对话(ReAct)loading-默认值 */ +export const DefaultCasualLoadingStatus: CasualLoadingStatus = { + loading: false, + title: 'thinking...', +} +/** 任务规划loading-默认值 */ +export const DefaultPlanLoadingStatus: PlanLoadingStatus = { + loading: false, + plan: '加载中...', + task: '加载中...', +} + +export const DefaultPlanHistoryList: AIAgentGrpcApi.PlanHistoryList = { + records: [], + total: 0, + session_id: '', +} diff --git a/src/pages/AIAgent/ai-re-act/hooks/grpcApi.ts b/src/pages/AIAgent/ai-re-act/hooks/grpcApi.ts new file mode 100644 index 00000000..c35a104e --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/grpcApi.ts @@ -0,0 +1,748 @@ +import type { HoldGRPCStreamProps, StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' +import type { ExecResult, PaginationSchema } from '@/pages/invoker/schema' +import type { AITaskInfoProps } from './aiRender' +import type { AITool } from '../../ai-agent/type/aiTool' +import type { AIForge } from '../../ai-agent/type/forge' +import type { AIModelTypeEnum, AttachedResourceKeyEnum, AttachedResourceTypeEnum } from '../../ai-agent/defaultConstant' +import type { KnowledgeBaseEntry, KVPair } from '../../enums/external' + +// #region 双工接口请求和响应结构 +export interface McpConfig { + Type: string + Key: string + Url: string +} + +export interface AIStartParams { + CoordinatorId?: string + Sequence?: number + + McpServers?: McpConfig[] + + /** 问题 */ + UserQuery?: string + /** 是否允许使用文件系统工具权限 @default true */ + EnableSystemFileSystemOperator?: boolean + /** 是否使用系统默认ai配置 @default true */ + UseDefaultAIConfig?: boolean + + /** AI 模板名 */ + ForgeName?: string + /** 模板参数 */ + ForgeParams?: KVPair[] + + /** 是否禁用人机交互(AI 可能会主动问人问题)@default true */ + DisallowRequireForUserPrompt?: boolean + + /** + * - Review 政策 + * - 一般来说,如果 Review Handler 被 Forge 接管了,这个就不应该可以设置。 + * - 普通的 Forge 并不会设置 Review 政策 + * - ReviewPolicy 可选的选项如下: + * - 1. manual (全手动,大事小事所有的事情都由人来决策) + * - 2. yolo (全自动,所有的事情,都直接执行,无需参与 - 效果差,危险程度高) + * - 3. ai (AI 来进行初步决策,如果AI觉得风险程度比较高,则转交给人) + * @default manual + */ + ReviewPolicy?: 'manual' | 'yolo' | 'ai' + + /** + * - 如果 Review 交给 AI 来做的话,那么就会涉及到一个风险打分 + * - AIReviewRiskControlScore 就是低于这个分数,AI 自动同意。 + * - 如果高于这个分数,转成手动。 + */ + AIReviewRiskControlScore?: number + + /** 禁用任何外部工具,这就是一个纯聊天机器了 @default false */ + DisableToolUse?: boolean + + /** 默认是3,一般是说如果远端AI不稳定(网络原因)的时候,某一次对话重试几次? @default 3 */ + AICallAutoRetry?: number + + /** 默认5,AI如果回答质量不高的时候,调大AITransactionRetry可以有效重试回答 @default 5 */ + AITransactionRetry?: number + + /** 是否启用AI搜索本地工具的功能 @default true */ + EnableAISearchTool?: boolean + + /** 是否启用AI搜索互联网搜索引擎的功能 @default false */ + EnableAISearchInternet?: boolean + + /** 建议工具名 */ + IncludeSuggestedToolNames?: string[] + /** 建议工具关键词 */ + IncludeSuggestedToolKeywords?: string[] + /** 禁用工具名 */ + ExcludeToolNames?: string[] + + /** ollama 本地模型部署可以使用 /nothink 关闭,使用这个选项可以移除 qwen3 的思考模式 @default false */ + EnableQwenNoThinkMode?: boolean + + /** 在任务规划的时候,AI 是否被允许主动问用户问题 @default true */ + AllowPlanUserInteract?: boolean + + /** 在任务规划的时候,如果AI允许问用户问题,那么最多问几次 @default 3 */ + PlanUserInteractMaxCount?: number + + /** 是否允许生成报告,默认不允许 */ + AllowGenerateReport?: boolean + /** 选择 AI 服务 */ + AIService?: string + /** 选择 AI 服务模型名称 */ + AIModelName?: string + ReActMaxIteration?: number + /** 时间线上下文限制(默认100) */ + TimelineItemLimit?: number + /** 时间线上下文大小(20*1024) */ + TimelineContentSizeLimit?: number + /** 用户交互的最大次数限制,超过这个次数,AI 将不再被允许问用户问题 */ + UserInteractLimit?: number + /** timeline sessionID 用于多轮对话保持上下文 */ + TimelineSessionID?: string + // Token pressure limit, 当 AI 对话的 token 数量超过这个限制时,需要警告 (40*1024) + AICallTokenLimit?: number +} + +/** AIInputEvent-HotpatchType 的可选值 */ +export enum AIInputEventHotPatchTypeEnum { + HotPatchType_AllowRequireForUserInteract = 'AllowRequireForUserInteract', + HotPatchType_AgreePolicy = 'AgreePolicy', + HotPatchType_AIService = 'AIService', + HotPatchType_RiskControlScore = 'RiskControlScore', + HotPatchType_AIModelName = 'ModelName', +} + +/** AIInputEvent-SyncType 的可选值 */ +export enum AIInputEventSyncTypeEnum { + /** 获取正在执行的任务树 */ + SYNC_TYPE_PLAN = 'plan', + SYNC_TYPE_CONSUMPTION = 'consumption', + SYNC_TYPE_PING = 'ping', + SYNC_TYPE_PROCESS_EVENT = 'sync_process_event', + /** 获取队列信息 */ + SYNC_TYPE_QUEUE_INFO = 'queue_info', + /** 查看上下文 */ + SYNC_TYPE_TIMELINE = 'timeline', + SYNC_TYPE_KNOWLEDGE = 'enhance_knowledge', + SYNC_TYPE_UPDATE_CONFIG = 'set_config', + /** 获取当前会话的记忆列表 */ + SYNC_TYPE_MEMORY_CONTEXT = 'memory_sync', + SYNC_TYPE_REACT_CANCEL_CURRENT_TASK = 'react_cancel_current_task', + /** 队列置顶 */ + SYNC_TYPE_REACT_JUMP_QUEUE = 'react_jump_queue', + /** 移除队列 */ + SYNC_TYPE_REACT_REMOVE_TASK = 'react_remove_task', + /** 清空队列 */ + SYNC_TYPE_REACT_CLEAR_TASK = 'react_clear_task', + /** 恢复任务 */ + SYNC_TYPE_RECOVERY_PLAN_AND_EXEC = 'recovery_plan_and_exec', + /** 取消指定任务 */ + SYNC_TYPE_REACT_CANCEL_TASK = 'react_cancel_task', + /** 取消指定的任务树上的某个节点 */ + SYNC_TYPE_SKIP_SUBTASK_IN_PLAN = 'skip_subtask_in_plan', + /** 重跑指定的任务树上的某个节点 */ + SYNC_TYPE_REDO_SUBTASK_IN_PLAN = 'redo_subtask_in_plan', + /** 获取历史任务列表 */ + SYNC_TYPE_PLAN_EXEC_TASKS = 'plan_exec_tasks', +} + +export interface AIInputEvent { + IsStart?: boolean + Params?: AIStartParams // 提问问题相关 + + IsConfigHotpatch?: boolean + HotpatchType?: `${AIInputEventHotPatchTypeEnum}` + + IsInteractiveMessage?: boolean // 是否为交互消息(review) + InteractiveId?: string // id + InteractiveJSONInput?: string // {suggestion:"continue"}|{suggestion:"adjust_plan",extra_prompt:"xxx"} + + IsSyncMessage?: boolean + SyncType?: `${AIInputEventSyncTypeEnum}` + SyncJsonInput?: string + SyncID?: string + + IsFreeInput?: boolean + FreeInput?: string // 自由输入的文本 + + /** + * 问题关联的文件路径 + * @deprecated 改为 AttachedResourceInfo + * */ + AttachedFilePath?: string[] + /** 附加资源信息 */ + AttachedResourceInfo?: AttachedResourceInfo[] + /** 专注模式 */ + FocusModeLoop?: string +} +export interface AttachedResourceInfo { + Key: AttachedResourceKeyEnum + Type: AttachedResourceTypeEnum + Value: string +} +export interface AIOutputI18n { + Zh: string + En: string +} + +export interface AIOutputEvent { + CoordinatorId: string + Type: string + NodeId: string + // 系统输出 + IsSystem: boolean + // AI正常输出 + IsStream: boolean + // AI思考输出 + IsReason: boolean + /** base64-encoded bytes */ + StreamDelta: string + IsJson: boolean + IsResult: boolean + /** base64-encoded bytes */ + Content: string + Timestamp: number + // 任务索引 + TaskIndex: string + /** 是否禁用 markdown 渲染 UI */ + DisableMarkdown: boolean + /** 是否是同步消息 */ + IsSync: boolean + /** 用于同步消息的 ID */ + SyncID: string + /** 事件的唯一标识 */ + EventUUID: string + /** 节点 ID 的展示内容, 包含18n */ + NodeIdVerbose: AIOutputI18n + /** 内容的类型: markdown / yaklang_code / plain_code / text/plain */ + ContentType: string + /** 如果是调用工具相关的事件,那么这里是调用的ID */ + CallToolID: string + /** 如果是 AI 服务相关的事件,那么这里是 AI 服务的名称 */ + AIService: string + /** 如果是 AI 服务相关的事件,那么这里是 AI 模型的名称 */ + AIModelName: string +} +// #endregion + +/** 任务状态枚举 */ +export enum AITaskStatus { + success = 'completed', + inProgress = 'processing', + error = 'aborted', + cancel = 'cancel', + skipped = 'skipped', + created = '', +} + +export declare namespace AIAgentGrpcApi { + /** 上传/下载 Token 量 */ + export interface Consumption { + input_consumption: number + output_consumption: number + consumption_uuid: string + tier_consumption: Record + } + + /** 上下文压力 */ + export interface Pressure { + current_cost_token_size: number + model_name: string + model_tier: AIModelTypeEnum + pressure_token_size: number + timestamp: number + } + + /** 首字符响应耗时 */ + export interface AIFirstCostMS { + model_name: string + model_tier: AIModelTypeEnum + ms: number + provider_name: string + second: number + timestamp: number + } + /** 总对话耗时 */ + export interface AITotalCostMS { + model_name: string + model_tier: AIModelTypeEnum + ms: number + output_bytes: number + output_duration_ms: number + provider_name: string + second: number + token_rate: number + timestamp: number + } + + /** 普通日志 */ + export interface Log { + /** 等级 */ + level: string + /** 信息 */ + message: string + } + + /** 启动任务规划,并通过 coordinator_id 区分流中数据为(自由对话|任务规划) */ + export interface AIStartPlanAndExecution { + coordinator_id: string + 're-act_id': string + 're-act_task': string + } + + export interface AIChatThought { + thought: string + } + + export interface AIChatResult { + finished: boolean + result: string + success: boolean + timestamp: number + /** 在流中已经输出显示了如果为 true, 代表不展示 */ + after_stream: boolean + } + + /** structured|stream-finished 代表一个流式输出已经结束 */ + export interface AIStreamFinished { + coordinator_id: string + duration_ms: number + event_writer_id: string + is_reason: boolean + is_system: boolean + node_id: string + start_timestamp: number + task_index: string + } + + /** review_release 释放消息 */ + export interface ReviewRelease { + /** review对应的id */ + id: string + params: any + } + + /** review 对应的可选项 */ + export interface ReviewSelector { + id?: string + value: string + prompt: string + prompt_english: string + allow_extra_prompt: boolean + allow_params_modification?: boolean + param_schema: string + } + + /** 计划内单个任务的详情 */ + export interface PlanTask { + index: string + /** 任务名 */ + name: string + /** 正文 */ + goal: string + progress?: AITaskStatus + subtasks?: AITaskInfoProps[] + /** 评阅时树节点是否被删 */ + isRemove: boolean + /** 关联工具 */ + tools: string[] + /** 工具解释描述 */ + description: string + /** 是否为用户添加的节点 */ + isUserAdd?: boolean + /** 执行工具的总数 */ + total_tool_call_count: number + /** 执行工具成功的总数 */ + success_tool_call_count: number + /** 执行工具失败的总数 */ + fail_tool_call_count: number + /** 任务执行后的总结 */ + summary: string + } + /** 改变任务状态 */ + export interface ChangeTask { + task: { + index: string + /** 任务名 */ + name: string + /** 正文 */ + goal: string + /** 后端供(push_task|pop_task)对应的唯一标识 */ + task_uuid?: string + /** 后端供pop_task反馈的任务执行状态 */ + task_status?: AITaskStatus + } + type: string + } + /** 更新任务状态、收集任务总结 */ + export interface UpdateTaskInfo { + task: { + executed: boolean + executing: boolean + goal: string + long_summary: string + name: string + summary: string + } + type: string + } + /** 任务执行的 prompt */ + export interface TaskLog { + prompt: string + step: string + system: boolean + } + + /** plan_review_require */ + export interface PlanReviewRequire { + id: string + plans: { root_task: PlanTask } + selectors: ReviewSelector[] + plans_id: string + } + /** plan_task_analysis 计划树中任务的补充解释和工具数据 */ + export interface PlanReviewRequireExtra { + description: string + index: string + keywords: string[] + plans_id: string + } + + /** task_review_require */ + export interface TaskReviewRequire { + id: string + long_summary: string + selectors: ReviewSelector[] + short_summary: string + task: PlanTask + } + + /** AI 对 review 数据的风险评估 */ + export interface AIReviewJudgement { + /** 对应 tool_review 的 id */ + interactive_id: string + /** review 操作的得分 */ + score?: number + /** 多少秒后自动执行 review 的 continue 操作 */ + seconds?: number + /** AI 判断的风险阈值 */ + level?: string + /** level 字段对应 UI 展示内容(由前端独立赋值) */ + levelLabel?: string + /** AI 判断使用的意图 */ + reason?: string + } + + /** tool_use_review_require */ + export interface ToolUseReviewRequire { + id: string + params: Record + selectors: ReviewSelector[] + tool: string + tool_description: string + /** 前端自定义字段-展示 ai 评分使用 */ + aiReview?: AIReviewJudgement + } + + /** exec_aiforge_review_require */ + export interface ExecForgeReview { + id: string + forge_name: string + forge_verbose_name: string + forge_desc: string + forge_params: Record + selectors: ReviewSelector[] + /** 前端自定义字段-展示 ai 评分使用 */ + aiReview?: AIReviewJudgement + } + + /** AI交互 review 的选项数据 */ + export interface AIRequireOption { + index: number + prompt_title: string + prompt: string + } + /** require_user_interactive */ + export interface AIReviewRequire { + id: string + prompt: string + options: AIRequireOption[] + } + + /** 工具相关信息、执行结果与总结 */ + export interface AIToolCall { + /** 工具 ID */ + call_tool_id: string + /** 工具执行状态 */ + status?: string + /** 工具执行失败原因 */ + error?: string + /** 工具执行总结(不论成功失败) */ + summary?: string + /** 工具名和工具描述 */ + tool?: { name?: string; description?: string } + /** 间隔时间(ms) */ + duration_ms: number + /** 间隔时间(s) */ + duration_seconds: number + /** 结束时间戳(s) */ + end_time: number + /** 结束时间戳(ms) */ + end_time_ms: number + /** 开始时间戳(s) */ + start_time: number + /** 开始时间戳(ms) */ + start_time_ms: number + } + /** 工具执行时的可选操作(tool_call_watcher) */ + export interface AIToolCallWatcher { + call_tool_id: string + /** 选项传递的对应 InteractiveId */ + id: string + /** 选项列表 */ + selectors: ReviewSelector[] + tool: string + tool_description: string + } + + /** 工具执行时的更新文件目录路径 */ + export interface AIToolCallDirPath { + call_tool_id: string + dir_path: string + } + + /** 插件结果输出信息(yak_exec_result) */ + export interface AIPluginExecResult extends Partial> { + Message: string + } + /** 插件结果数据 */ + export type AICardMessage = StreamResult.Message + /** 插件结果-卡片数据 */ + export type AICard = StreamResult.Card + export type AICacheCard = HoldGRPCStreamProps.CacheCard + export type AIInfoCard = HoldGRPCStreamProps.InfoCards + + /** structured|timeline 类型数据结构 */ + export interface TimelineDumpOpt { + timestamp: string + type: string + content: string + } + export interface TimelineDump { + dump: string + entries: TimelineDumpOpt[] + limit: number + total_entries: number + } + + /** 文件系统操作相关 */ + export interface FileSystemPin { + path: string + timestamp: number + } + + /** 工具决策总结相关 */ + export interface ToolCallDecision { + action: string + call_tool_id: string + summary: string + i18n: { zh: string; en: string } + } + + /** 问题队列状态变化 */ + export interface QuestionQueueStatusChange { + /** 专注模式 */ + focus_mode: string + /** 问题ID */ + react_task_id: string + /** 问题内容 */ + react_task_input: string + /** 移除队列的原因 */ + reason?: string + } + export interface QuestionQueueItem { + created_at: string + focus_mode: string + id: string + status: AITaskStatus + user_input: string + } + /** 问题队列信息 */ + export interface QuestionQueues { + is_processing: boolean + queue_empty: boolean + queue_name: string + tasks: QuestionQueueItem[] + total_tasks: number + } + /** 问题状态变化消息 */ + export interface ReactTaskChanged { + react_task_id: string + react_task_now_status: string + react_task_old_status: string + } + + /** 准备执行任务规划的问题id(react_task_id) */ + export interface ReactTaskToAsync { + task_id: string + loop_name: string + task_index: string + task_user_input: string + } + + /** stream类型消息的参考补充消息 */ + export interface ReferenceMaterialPayload { + event_uuid: string + payload: string + type: string + } + + /** 实时时间线-单个时间点的数据 */ + export interface TimelineItem { + deleted: boolean + timestamp: number + id: number + /** 可选类型 "user_input" "tool_result", "user_interaction", "text", "raw" */ + type: string + entry_type?: string + /** 任务ID */ + task_id?: string + /** 实际内容 */ + content: string + /** 原始文本(用于调试) */ + raw_text?: string + + ShrinkResult?: string + ShrinkSimilarResult?: string + } + + /** 记忆数据-单项 */ + export interface MemoryEntry { + id: string + /** @description 可以传空,前端没有用到 */ + created_at: string + created_at_timestamp: number + content: string + tags: string[] + c_score: number + o_score: number + r_score: number + e_score: number + p_score: number + a_score: number + t_score: number + core_pact_vector: number[] + potential_questions: string[] + /** 记忆库页面有的数据,ai页面没有的 */ + memory_id?: string + } + /** 记忆列表 */ + export interface MemoryEntryList { + memories: MemoryEntry[] + memory_pool_limit: number + memory_session_id: string + total_memories: number + total_size: number + score_overview: { + A_total: number + C_total: number + E_total: number + O_total: number + P_total: number + R_total: number + T_total: number + } + } + + /** 任务规划历史数据-任务进度信息 */ + export interface PlanHistoryProgress { + total_tasks: number + completed_tasks: number + skipped_tasks: number + aborted_tasks: number + current_index: number + current_task_index: string + current_task: string + current_goal: string + phase: 'NotCompleted' | 'Completed' + updated_at: number + } + /** 任务规划历史数据 */ + export interface PlanHistory { + coordinator_id: string + created_at: string + created_at_unix: number + session_id: string + task_progress: PlanHistoryProgress + task_tree: AITaskInfoProps[] + updated_at: string + updated_at_unix: number + /** 任务树根节点的name(前端逻辑,仅供UI使用) */ + root_task_name: string + } + /** 任务规划历史列表 */ + export interface PlanHistoryList { + records: PlanHistory[] + total: number + session_id: string + } +} + +// #region AI相关普通接口的请求和定义结构 +export interface AIEventFilter { + EventUUIDS?: string[] + EventType?: string[] + CoordinatorId?: string[] + TaskIndex?: string[] + TaskUUID?: string[] + SessionID?: string + /** call_tool_id */ + ProcessID?: string + NodeId?: string[] +} + +/** QueryAIEvent 接口请求 */ +export interface AIEventQueryRequest { + Filter?: AIEventFilter + ProcessID?: string + Pagination?: PaginationSchema +} +/** QueryAIEvent 接口响应 */ +export interface AIEventQueryResponse { + Events: AIOutputEvent[] + Pagination: PaginationSchema + Total: number +} + +/** DeleteAIEvent 接口请求 */ +// export interface AIEventDeleteRequest { +// Filter?: AIEventFilter +// ClearAll?: boolean +// } + +// export interface AITaskDeleteRequest { +// Filter?: AIEventFilter +// } + +/** GetRandomAIMaterials 接口请求 */ +export interface GetRandomAIMaterialsRequest { + Limit: number +} +export interface GetRandomAIMaterialsResponse { + KnowledgeBaseEntries: KnowledgeBaseEntry[] + AITools: AITool[] + AIForges: AIForge[] +} + +/** AI-Log Export */ +export interface ExportAILogsRequest { + SessionID?: string + CoordinatorIDs?: string[] + ExportDataTypes: string[] + OutputPath?: string + MemoryID?: string +} +export interface ExportAILogsResponse { + FilePath: string +} +// #endregion diff --git a/src/pages/AIAgent/ai-re-act/hooks/type.ts b/src/pages/AIAgent/ai-re-act/hooks/type.ts new file mode 100644 index 00000000..609afe9c --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/type.ts @@ -0,0 +1,320 @@ +import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' +import type { + AIChatQSData, + AIStreamOutput, + AITaskInfoProps, + AIYakExecFileRecord, + ReActChatBaseInfo, + ReActChatRenderItem, +} from './aiRender' +import type { AIAgentGrpcApi, AIInputEvent, AIOutputEvent, AIStartParams, AITaskStatus } from './grpcApi' +import type { AIAgentSetting } from '@/pages/AIAgent/ai-agent/aiAgentType' +import type { Dispatch, SetStateAction } from 'react' +import type { AIChatData } from '@/pages/AIAgent/ai-agent/type/aiChat' +import type { ChatDataStore } from '@/pages/AIAgent/ai-agent/store/ChatDataStore' +import type { CustomPluginExecuteFormValue } from '../../enums/external' + +/** 公共 hoos 事件 */ +export interface UseHookBaseParams { + /** 将数据推送到日志集合中 */ + pushLog: (log: AIChatLogData) => void + /** 获取全部聊天数据 */ + getChatDataStore: () => AIChatData | undefined +} +interface UseHookBaseEvents { + handleSetData: (res: AIOutputEvent) => void + handleResetData: () => void +} +export type handleSendFunc = (params: { + request: AIInputEvent + optionValue?: string + extraValue?: AIChatIPCStartParams['extraValue'] + cb?: () => void +}) => void + +// #region useAIPerfData相关定义 +export type UseAIPerfDataParams = UseHookBaseParams + +export interface UseAIPerfDataEvents { + handleSetData: (res: AIOutputEvent) => void +} +// #endregion + +// #region useYakExecResult相关定义 +export type UseYakExecResultParams = UseHookBaseParams + +export interface UseYakExecResultState { + card: AIAgentGrpcApi.AIInfoCard[] + execFileRecord: Map + yakExecResultLogs: StreamResult.Log[] +} +export interface UseYakExecResultEvents extends UseHookBaseEvents { + /** 设置UI展示的列表数据 */ + handleSetYakResult: (newData: UseYakExecResultState) => void +} +// #endregion + +// #region useCasualChat相关定义 +export interface UseCasualChatParams extends UseHookBaseParams { + /** 获取流接口请求参数 */ + getRequest: () => AIAgentSetting | undefined + /** 触发 review-release 后的回调事件 */ + onReviewRelease: (id: string) => void +} + +export interface UseCasualChatState { + elements: ReActChatRenderItem[] +} +export interface UseCasualChatEvents extends UseHookBaseEvents { + handleSend: handleSendFunc + /** 设置UI展示的列表数据 */ + handleSetElements: (newElements: ReActChatRenderItem[]) => void +} +// #endregion + +// #region useTaskChat相关定义 +export interface UseTaskChatParams extends UseHookBaseParams { + /** 获取流接口请求参数 */ + getRequest: () => AIAgentSetting | undefined + /** 是否正在只读重放历史会话 */ + isHistoryReplay?: () => boolean + /** review 触发回调事件 */ + onReview?: (data: AIChatQSData) => void + /** plan_review 补充数据 */ + onReviewExtra?: (data: AIAgentGrpcApi.PlanReviewRequireExtra) => void + /** 触发 review-release 后的回调事件 */ + onReviewRelease?: (id: string) => void + /** 向接口发送消息 */ + sendRequest?: (request: AIInputEvent) => void +} + +export interface UseTaskChatState { + /** 正在执行的任务列表 */ + plan: AITaskInfoProps[] + elements: ReActChatRenderItem[] +} +export interface UseTaskChatEvents extends UseHookBaseEvents { + handleSend: handleSendFunc + /** 获取树结构的任务树 */ + fetchPlanTree: () => AIAgentGrpcApi.PlanTask | undefined + /** grpc接口关闭后的后续处理逻辑 */ + handleCloseGrpc: () => void + /** 当前任务规划结束-触发UI展示结束标识 */ + handlePlanExecEnd: (res: AIOutputEvent) => void + /** 设置UI展示的列表数据 */ + handleSetElements: (newElements: ReActChatRenderItem[]) => void +} +// #endregion + +// #region useChatIPC相关定义 +/** 会话类型 */ +export type ChatIPCSendType = 'casual' | 'task' | '' +/** 会话-通知消息回调 */ +export interface AIChatIPCNotifyMessage { + Type: AIOutputEvent['Type'] + NodeId: AIOutputEvent['NodeId'] + NodeIdVerbose: AIOutputEvent['NodeIdVerbose'] + Content: string + Timestamp: AIOutputEvent['Timestamp'] +} + +export interface UseChatIPCParams { + /** 文件数据缓存实例类 */ + cacheDataStore?: ChatDataStore + /** 跨标签页通信的频道名称 */ + channelName?: string + /** 设置会话的名字 */ + setSessionChatName?: (session: string, name: string) => void + + /** 查看历史会话数据 */ + onViewChat?: (session: string) => void + /** 出现任务规划的触发回调(id 是 coordinatorId) */ + onTaskStart?: () => void + /** 任务规划的 review 事件 */ + onTaskReview?: (data: AIChatQSData) => void + /** 任务规划中 plan_review 事件的补充数据 */ + onTaskReviewExtra?: (data: AIAgentGrpcApi.PlanReviewRequireExtra) => void + + /** 主动 review-release 的回调事件 */ + onReviewRelease?: (type: ChatIPCSendType, id: string) => void + + /** 接口结束断开的回调事件 */ + onEnd?: () => void + + /** 同步信息返回 */ + onSyncIDChange?: (syncID: string) => void +} + +/** 会话文件系统-pin */ +export interface AIFileSystemPin { + path: string + isFolder: boolean +} + +/** 自由对话-实时问题队列 */ +export interface AIQuestionQueues { + total: number + data: AIAgentGrpcApi.QuestionQueueItem[] +} + +/** 自由对话-loading状态信息 */ +export interface CasualLoadingStatus { + loading: boolean + title: string +} + +/** 任务规划-loading状态信息 */ +export interface PlanLoadingStatus { + loading: boolean + plan: string + task: string +} + +export interface UseChatIPCState { + /** 流执行状态 */ + execute: boolean + /** 运行时的runtimeid合集 */ + runTimeIDs: string[] + /** 插件输出的卡片数据 */ + yakExecResult: UseYakExecResultState + /** 自由对话相关数据 */ + casualChat: UseCasualChatState + /** 任务规划相关数据 */ + taskChat: UseTaskChatState + /** 接口运行过程中的数据文件夹合集 */ + grpcFolders: AIFileSystemPin[] + /** 问题队列信息 */ + questionQueue: AIQuestionQueues + /** 自由对话的loading状态信息 */ + casualStatus: CasualLoadingStatus + /** 时间线 */ + reActTimelines: AIAgentGrpcApi.TimelineItem[] + /** 记忆列表 */ + memoryList: AIAgentGrpcApi.MemoryEntryList + /** 任务规划的loading状态信息 */ + taskStatus: PlanLoadingStatus + /** 系统流信息(isSystem=true&type=stream) */ + systemStream: string + /** 专注模式状态(仅供自由对话[reAct])使用 */ + focusMode: string + /** 切换session时的loading状态 */ + switchLoading: boolean + /** 任务规划历史数据-任务树 */ + planHistoryList: AIAgentGrpcApi.PlanHistoryList + /** 用户主动取消问题的loading状态(自由对话) */ + cancelCasualLoading: boolean + /** 用户主动取消问题的loading状态(任务规划) */ + cancelTaskLoading: boolean +} + +/** 开始启动流接口的唯一token、请求参数和额外参数 */ +export interface AIChatIPCStartParams { + token: string + params: AIInputEvent + /** 供前端处理逻辑和UI的额外参数 */ + extraValue?: CustomPluginExecuteFormValue | Record +} + +/** 执行流途中发送消息的参数 */ +export interface AIChatSendParams { + token: string + type: ChatIPCSendType + params: AIInputEvent + optionValue?: string + extraValue?: AIChatIPCStartParams['extraValue'] +} + +/** 任务规划的taskID和状态 */ +export interface TaskChatTaskInfo { + taskID: string + status: AITaskStatus + coordinatorId: AIOutputEvent['CoordinatorId'] +} + +export interface UseChatIPCEvents { + /** 获取当前执行接口流的唯一标识符 */ + fetchToken: () => string + /** 获取当前执行接口流的请求参数 */ + fetchAIRequest: () => AIStartParams | undefined + /** 获取当前执行任务规划的问题详情 */ + fetchTaskChatID: () => TaskChatTaskInfo | undefined + /** 获取当前外界传入的数据类实例 */ + fetchChatDataStore: () => UseChatIPCParams['cacheDataStore'] + /** 切换历史会话展示 */ + onSwitchChat: (session?: string) => void + /** 开始执行接口流 */ + onStart: (params: AIChatIPCStartParams) => void + /** 向执行中的接口流主动输入信息 */ + onSend: (params: AIChatSendParams) => void + /** 主动结束正在执行中的接口流 */ + onClose: ( + token: string, + option?: { + tip: () => void + }, + ) => void + /** 重置所有数据 */ + onReset: () => void + /** 取消任务规划当前的Review */ + handleTaskReviewRelease: (id: string) => void + /** 删除会话操作的关联逻辑 */ + onDelChats: (session: string[]) => void + /** 用户主动取消问题的loading状态变换 */ + handleCancelLoadingChange: (type: ReActChatBaseInfo['chatType'], status: boolean) => void +} +// #endregion + +// #region useAIChatLog相关定义 +export interface useAIChatLogParams { + channelName: UseChatIPCParams['channelName'] +} + +export interface AIChatLogToInfo { + type: 'log' + Timestamp: AIOutputEvent['Timestamp'] + data: AIAgentGrpcApi.Log +} +export interface AIChatLogToStream { + type: 'stream' + Timestamp: AIOutputEvent['Timestamp'] + data: AIStreamOutput +} + +export type AIChatLogData = AIChatLogToInfo | AIChatLogToStream + +export interface UseAIChatLogEvents { + /** 获取当前执行接口流的唯一标识符 */ + pushLog: (log: AIChatLogData) => void + /** 都劝我 */ + sendStreamLog: (uuid: string) => void + /** 获取当前执行接口流的请求参数 */ + clearLogs: () => AIStartParams | undefined + /** 关闭展示日志的页面窗口 */ + cancelLogsWin: () => void +} +// #endregion + +// #region useChatContent相关定义 +export interface UseChatContentParams { + chatType: ReActChatRenderItem['chatType'] + getContentMap: (token: string) => AIChatQSData | undefined + setContentMap: (token: string, content: AIChatQSData) => void + deleteContentMap: (token: string) => void + setElements: Dispatch> + getElements: () => ReActChatRenderItem[] + /** 获取当前执行接口流的唯一标识符 */ + pushLog: (log: AIChatLogData) => void + /** 未识别的类型数据, 由外界自主识别处理 */ + handleUnkData: (res: AIOutputEvent) => void +} + +export type UseChatContentEvents = UseHookBaseEvents +// #endregion + +// #region useHistoryChat相关定义 +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface UseHistoryChatParams {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface UseHistoryChatEvents {} +// #endregion diff --git a/src/pages/AIAgent/ai-re-act/hooks/useAIChatLog.ts b/src/pages/AIAgent/ai-re-act/hooks/useAIChatLog.ts new file mode 100644 index 00000000..42fadc2d --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/useAIChatLog.ts @@ -0,0 +1,72 @@ +import { useRef } from 'react' +import { useMemoizedFn } from 'ahooks' +import type { AIChatLogData, AIChatLogToStream, UseAIChatLogEvents, useAIChatLogParams } from './type' +import { formatTimestamp } from '@/utils/timeUtil' +import cloneDeep from 'lodash/cloneDeep' +import { useBroadcastComm } from '@/hooks' +import type { MessageItem } from '../../ai-agent-log/AIAgentLog' + +function useAIChatLog(params?: useAIChatLogParams): UseAIChatLogEvents + +function useAIChatLog(params?: useAIChatLogParams) { + const channelName = params?.channelName + const hasChannel = !!channelName + + // 顶层调用 Hook;当 channelName 为空时内部不会创建真正的 BroadcastChannel + const broadcastComm = useBroadcastComm(channelName || '') + + const streamInfo = useRef>(new Map()) + + const pushLog = useMemoizedFn((info: AIChatLogData) => { + if (!hasChannel) return + + if (info.type === 'log') { + const logInfo = info.data + const sendData = { + level: logInfo.level, + message: logInfo.message, + timestamp: formatTimestamp(info.Timestamp), + } + broadcastComm.onSend(sendData) + } + if (info.type === 'stream') { + const { EventUUID, content } = info.data + const stream = streamInfo.current.get(EventUUID) + if (stream) { + stream.data.content += content + streamInfo.current.set(EventUUID, cloneDeep(stream)) + } else { + streamInfo.current.set(EventUUID, cloneDeep(info)) + } + } + }) + + const sendStreamLog = useMemoizedFn((uuid: string) => { + if (!hasChannel) return + + const stream = streamInfo.current.get(uuid) + if (!stream) return + const sendData = { + level: stream.data.NodeId, + message: stream.data.content, + timestamp: formatTimestamp(stream.Timestamp), + isStream: true, + } + streamInfo.current.delete(uuid) + broadcastComm.onSend(sendData) + }) + + const clearLogs = useMemoizedFn(() => { + streamInfo.current.clear() + // TODO 发送通信通知另一个页面清空展示的所有内容 + }) + + const cancelLogsWin = useMemoizedFn(() => { + clearLogs() + // TODO 发送关闭页面的通知 + }) + + return { pushLog, sendStreamLog, clearLogs, cancelLogsWin } +} + +export default useAIChatLog diff --git a/src/pages/AIAgent/ai-re-act/hooks/useAINodeLabel.ts b/src/pages/AIAgent/ai-re-act/hooks/useAINodeLabel.ts new file mode 100644 index 00000000..729a74f2 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/useAINodeLabel.ts @@ -0,0 +1,14 @@ +import { useCreation, useMemoizedFn } from 'ahooks'; +import type { AIOutputI18n } from './grpcApi'; + +function useAINodeLabel(params?: AIOutputI18n) { + const nodeLabel = useCreation(() => { + return params ? params.Zh : ''; + }, [params]); + const getLabelByParams = useMemoizedFn((value: AIOutputI18n) => { + return value.Zh || ''; + }); + return { nodeLabel, getLabelByParams }; +} + +export default useAINodeLabel; diff --git a/src/pages/AIAgent/ai-re-act/hooks/useAIPerfData.ts b/src/pages/AIAgent/ai-re-act/hooks/useAIPerfData.ts new file mode 100644 index 00000000..2fe9a66e --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/useAIPerfData.ts @@ -0,0 +1,99 @@ +import { useCreation, useMemoizedFn } from 'ahooks' +import type { AIChatLogData, UseAIPerfDataEvents, UseAIPerfDataParams } from './type' +import { base64ToJson, handleGrpcDataPushLog } from './utils' +import type { AIAgentGrpcApi, AIOutputEvent } from './grpcApi' + +// 属于该 hook 处理数据的类型 +export const UseAIPerfDataTypes = ['consumption', 'pressure', 'ai_first_byte_cost_ms', 'ai_total_cost_ms'] + +function useAIPerfData(params?: UseAIPerfDataParams): UseAIPerfDataEvents + +/** 提供 AI 硬件相关性能数据 */ +function useAIPerfData(params?: UseAIPerfDataParams) { + const { pushLog, getChatDataStore } = params || {} + + const handlePushLog = useMemoizedFn((log: AIChatLogData) => { + pushLog?.(log) + }) + + const handleSetData = useMemoizedFn((res: AIOutputEvent) => { + try { + let ipcContent = base64ToJson(res.Content) || '' + + if (res.Type === 'consumption') { + // 消耗Token + // 因为可能存在多个 ai 并发输出,所以这里的 token 量是一个集合 + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.Consumption + const consumption = getChatDataStore?.()?.aiPerfData?.consumption + if (consumption) { + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + consumption.input_consumption = data.input_consumption + consumption.output_consumption = data.output_consumption + consumption.tier_consumption = { ...data.tier_consumption } + } + return + } + + if (res.Type === 'pressure') { + // 上下文压力 + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.Pressure + const pressure = getChatDataStore?.()?.aiPerfData?.pressure + if (pressure) { + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + const target = pressure[data.model_tier] + if (!target) { + pressure[data.model_tier] = [{ ...data, timestamp: Number(res.Timestamp) || 0 }] + } else { + target.push({ ...data, timestamp: Number(res.Timestamp) || 0 }) + // if (target.length > 100) target.shift() + } + } + return + } + + if (res.Type === 'ai_first_byte_cost_ms') { + // 首字符响应耗时 + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.AIFirstCostMS + const firstCost = getChatDataStore?.()?.aiPerfData?.firstCost + if (firstCost) { + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + const target = firstCost[data.model_tier] + if (!target) { + firstCost[data.model_tier] = [{ ...data, timestamp: Number(res.Timestamp) || 0 }] + } else { + target.push({ ...data, timestamp: Number(res.Timestamp) || 0 }) + // if (target.length > 100) target.shift() + } + } + return + } + + if (res.Type === 'ai_total_cost_ms') { + // 总对话耗时 + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.AITotalCostMS + const totalCost = getChatDataStore?.()?.aiPerfData?.totalCost + if (totalCost) { + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + const target = totalCost[data.model_tier] + if (!target) { + totalCost[data.model_tier] = [{ ...data, timestamp: Number(res.Timestamp) || 0 }] + } else { + target.push({ ...data, timestamp: Number(res.Timestamp) || 0 }) + // if (target.length > 100) target.shift() + } + } + return + } + } catch (error) { + handleGrpcDataPushLog({ info: res, pushLog: handlePushLog }) + } + }) + + const events: UseAIPerfDataEvents = useCreation(() => { + return { handleSetData } + }, []) + + return events +} + +export default useAIPerfData diff --git a/src/pages/AIAgent/ai-re-act/hooks/useAISystemStream.ts b/src/pages/AIAgent/ai-re-act/hooks/useAISystemStream.ts new file mode 100644 index 00000000..d88f6afa --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/useAISystemStream.ts @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react'; +import { useTimeout } from 'ahooks'; + +interface UseAISystemStreamProps { + value?: string; + systemStream?: string; + delay?: number; + enabled?: boolean; +} +type AISystemStreamMode = 'value' | 'systemStream'; + +const useAISystemStream = ({ + value, + systemStream, + delay = 2000, + enabled = true, +}: UseAISystemStreamProps) => { + const [displayValue, setDisplayValue] = useState(value); + const [mode, setMode] = useState('value'); + const [timeoutDelay, setTimeoutDelay] = useState(); + + useTimeout(() => { + if (!enabled) return; + setDisplayValue(systemStream); + setTimeoutDelay(undefined); + setMode('systemStream'); + }, timeoutDelay); + + useEffect(() => { + if (!enabled) { + setTimeoutDelay(undefined); + return; + } + setDisplayValue(value); + setTimeoutDelay(delay); + setMode('value'); + }, [value, enabled, delay]); + + return { displayValue, mode }; +}; + +export default useAISystemStream; diff --git a/src/pages/AIAgent/ai-re-act/hooks/useCasualChat.ts b/src/pages/AIAgent/ai-re-act/hooks/useCasualChat.ts new file mode 100644 index 00000000..71bb33c3 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/useCasualChat.ts @@ -0,0 +1,412 @@ +import { useRef } from 'react' +import { useCreation, useMemoizedFn } from 'ahooks' +import cloneDeep from 'lodash/cloneDeep' +import { + base64ToJson, + genBaseAIChatData, + genErrorLogData, + handleGrpcDataPushLog, + isAutoExecuteReviewContinue, +} from './utils' +import { v4 as uuidv4 } from 'uuid' +import type { + AIChatLogData, + handleSendFunc, + UseCasualChatEvents, + UseCasualChatParams, + UseCasualChatState, +} from './type' +import { AIReviewJudgeLevelMap } from './defaultConstant' +import { yakitNotify } from '@/utils/notification' +import type { AIAgentGrpcApi, AIOutputEvent } from './grpcApi' +import type { AIChatQSData, AIReviewType, ReActChatRenderItem } from './aiRender' +import { AIChatQSDataTypeEnum } from './aiRender' +import useChatContent from './useChatContent' +import { useGetSetState } from '@/hooks' + +function useCasualChat(params?: UseCasualChatParams): [UseCasualChatState, UseCasualChatEvents] + +function useCasualChat(params?: UseCasualChatParams) { + const { pushLog, getChatDataStore, getRequest, onReviewRelease } = params || {} + + const handlePushLog = useMemoizedFn((logInfo: AIChatLogData) => { + pushLog && pushLog(logInfo) + }) + + const [elements, setElements, getElements] = useGetSetState([]) + const handleSetElements = useMemoizedFn((newElements: ReActChatRenderItem[]) => { + setElements(newElements) + }) + + const getContentMap = useMemoizedFn((mapKey: string) => { + const contentMap = getChatDataStore?.()?.casualChat?.contents + if (!contentMap) return undefined + return contentMap.get(mapKey) + }) + const setContentMap = useMemoizedFn((mapKey: string, value: AIChatQSData) => { + const contentMap = getChatDataStore?.()?.casualChat?.contents + contentMap && contentMap.set(mapKey, value) + }) + const deleteContentMap = useMemoizedFn((mapKey: string) => { + const contentMap = getChatDataStore?.()?.casualChat?.contents + contentMap && contentMap.delete(mapKey) + }) + + // #region review事件转换成UI处理逻辑 + const review = useRef() + + // tool_review + const handleToolReview = useMemoizedFn((res: AIOutputEvent) => { + try { + const ipcContent = base64ToJson(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ToolUseReviewRequire + if (!data?.id || !data?.selectors || !data?.selectors?.length) { + handlePushLog( + genErrorLogData( + res.Timestamp, + `${res.Type}数据异常: id:${data?.id || '-'}; selectors:${JSON.stringify(data?.selectors || '-')}`, + ), + ) + return + } + + const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: getRequest }) + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: 'reAct', + id: data.id, + type: AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE, + data: { + ...cloneDeep(data), + selected: isAuto ? JSON.stringify({ suggestion: 'continue' }) : undefined, + optionValue: isAuto ? 'continue' : undefined, + }, + } + review.current = isAuto ? undefined : chatData + setContentMap(chatData.id, cloneDeep(chatData)) + setElements((old) => [...old, { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'reAct' }]) + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: handlePushLog, + }) + } + }) + // forge_review 事件处理 + const handleExecForgeReview = useMemoizedFn((res: AIOutputEvent) => { + try { + const ipcContent = base64ToJson(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ExecForgeReview + if (!data?.id || !data?.selectors || !data?.selectors?.length) { + handlePushLog( + genErrorLogData( + res.Timestamp, + `${res.Type}数据异常: id:${data?.id || '-'}; selectors:${JSON.stringify(data?.selectors || '-')}`, + ), + ) + return + } + + const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: getRequest }) + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: 'reAct', + id: data.id, + type: AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE, + data: { + ...cloneDeep(data), + selected: isAuto ? JSON.stringify({ suggestion: 'continue' }) : undefined, + optionValue: isAuto ? 'continue' : undefined, + }, + } + review.current = isAuto ? undefined : chatData + setContentMap(chatData.id, cloneDeep(chatData)) + setElements((old) => [...old, { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'reAct' }]) + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: handlePushLog, + }) + } + }) + // AI人机交互的review事件处理 + const handleUserRequireReview = useMemoizedFn((res: AIOutputEvent) => { + try { + const ipcContent = base64ToJson(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.AIReviewRequire + if (!data?.id) { + handlePushLog(genErrorLogData(res.Timestamp, `${res.Type}数据异常: id:${data?.id || '-'}`)) + return + } + + review.current = { + ...genBaseAIChatData(res), + chatType: 'reAct', + id: data.id, + type: AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE, + data: cloneDeep(data), + } + setContentMap(review.current.id, cloneDeep(review.current)) + setElements((old) => { + const newArr = [...old] + if (!review.current) return newArr + newArr.push({ token: review.current.id, type: review.current.type, renderNum: 1, chatType: 'reAct' }) + return newArr + }) + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: handlePushLog, + }) + } + }) + + // 处理 tool_review和forge_view 的 ai 判断得分事件 + const handleReviewJudgement = useMemoizedFn((res: AIOutputEvent) => { + try { + const ipcContent = base64ToJson(res.Content) || '' + const score = JSON.parse(ipcContent) as AIAgentGrpcApi.AIReviewJudgement + if (!score?.interactive_id) { + handlePushLog( + genErrorLogData(res.Timestamp, `${res.Type}数据异常: interactive_id:${score?.interactive_id || '-'}`), + ) + return + } + + const { interactive_id } = score + score.levelLabel = AIReviewJudgeLevelMap[score?.level || '']?.label || undefined + if ( + review.current && + (review.current.type === AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE || + review.current.type === AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE) + ) { + if ( + !review.current.data.aiReview || + (review.current.data.aiReview && typeof review.current.data.aiReview.seconds === 'undefined') + ) { + review.current.data.aiReview = cloneDeep(score) + } + } + + const chatData = getContentMap(interactive_id) + if ( + !!chatData && + (chatData.type === AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE || + chatData.type === AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE) + ) { + if ( + !chatData.data.aiReview || + (chatData.data.aiReview && typeof chatData.data.aiReview.seconds === 'undefined') + ) { + // aiReview 没有或者 aiReview 的 seconds 为空时可以赋值 + chatData.data.aiReview = cloneDeep(score) + setElements((old) => { + return old.map((item) => { + if (item.token === chatData.id && item.type === chatData.type) { + return { ...item, renderNum: item.renderNum + 1 } + } + return item + }) + }) + } + } else { + handlePushLog( + genErrorLogData( + res.Timestamp, + `${res.Type}数据(interactive_id:${score?.interactive_id || '-'})未找到对应review`, + ), + ) + } + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: handlePushLog, + }) + } + }) + + // 释放当前review信息 + const handleReviewRelease = useMemoizedFn((res: AIOutputEvent) => { + try { + if (!review.current) return + + const ipcContent = base64ToJson(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ReviewRelease + if (!data?.id) { + handlePushLog(genErrorLogData(res.Timestamp, `${res.Type}数据异常: id:${data?.id || '-'}`)) + return + } + + const info = cloneDeep(review.current.data) as AIReviewType + if (info?.id !== data.id) { + handlePushLog( + genErrorLogData( + res.Timestamp, + `${res.Type}数据(id:${data?.id || '-'})和当前展示review数据(id:${info?.id || '-'})不匹配`, + ), + ) + return + } + + info.selected = JSON.stringify({ suggestion: 'continue' }) + info.optionValue = 'continue' + const chatData: AIChatQSData = { + ...review.current, + data: info as any, + } + review.current = undefined + setContentMap(chatData.id, cloneDeep(chatData)) + setElements((old) => { + return old.map((item) => { + if (item.token === chatData.id && item.type === chatData.type) { + return { ...item, renderNum: item.renderNum + 1 } + } + return item + }) + }) + + const isAuto = isAutoExecuteReviewContinue({ type: chatData.type, getFunc: getRequest }) + if (!isAuto) { + onReviewRelease && onReviewRelease(data.id) + } + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: handlePushLog, + }) + } + }) + // #endregion + + // 处理专属自由对话的特殊数据流 + const handleSpecialData = useMemoizedFn((res: AIOutputEvent) => { + try { + if (res.Type === 'tool_use_review_require') { + handleToolReview(res) + return + } + if (res.Type === 'require_user_interactive') { + handleUserRequireReview(res) + return + } + if (res.Type === 'exec_aiforge_review_require') { + handleExecForgeReview(res) + return + } + + if (['ai_review_start', 'ai_review_countdown', 'ai_review_end'].includes(res.Type)) { + handleReviewJudgement(res) + return + } + + if (res.Type === 'review_release') { + // review释放通知 + handleReviewRelease(res) + return + } + + if (res.Type === 'success_react_task') { + // ReAct任务成功结束标志 + // 暂时过滤不展示到UI上 + return + } + + // 未识别类型全部归档到日志处理 + handleGrpcDataPushLog({ info: res, pushLog: handlePushLog }) + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: handlePushLog, + }) + } + }) + + const chatContentEvent = useChatContent({ + chatType: 'reAct', + getContentMap, + setContentMap, + deleteContentMap, + setElements, + getElements, + pushLog: handlePushLog, + handleUnkData: handleSpecialData, + }) + // #endregion + + // 处理数据方法 + const handleSetData = useMemoizedFn((res: AIOutputEvent) => { + try { + chatContentEvent.handleSetData(res) + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: handlePushLog, + }) + } + }) + + // 用户问题或review的主动操作 + const handleSend: handleSendFunc = useMemoizedFn(({ request, optionValue, extraValue, cb }) => { + try { + const { IsInteractiveMessage, InteractiveId, IsFreeInput, FreeInput } = request + if (IsInteractiveMessage && InteractiveId) { + if (!review.current || (review.current.data as AIReviewType)?.id !== InteractiveId) { + yakitNotify('error', '未获取到 review 信息, 操作无效') + return + } + + ;(review.current.data as AIReviewType).selected = request.InteractiveJSONInput + ;(review.current.data as AIReviewType).optionValue = optionValue + + const chatData = cloneDeep(review.current) + review.current = undefined + setContentMap(chatData.id, chatData) + setElements((old) => { + return old.map((item) => { + if (item.token === chatData.id && item.type === chatData.type) { + return { ...item, renderNum: item.renderNum + 1 } + } + return item + }) + }) + } + + if (IsFreeInput && FreeInput) { + // 用户问题 + const chatData: AIChatQSData = { + id: uuidv4(), + chatType: 'reAct', + type: AIChatQSDataTypeEnum.QUESTION, + Timestamp: Date.now(), + data: { qs: FreeInput || '', setting: request }, + AIService: '', + AIModelName: '', + extraValue: extraValue, + } + + setContentMap(chatData.id, chatData) + setElements((old) => [...old, { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'reAct' }]) + } + + cb && cb() + } catch (error) {} + }) + + const handleResetData = useMemoizedFn(() => { + review.current = undefined + chatContentEvent.handleResetData() + setElements([]) + }) + + const state: UseCasualChatState = useCreation(() => { + return { elements } + }, [elements]) + + const events: UseCasualChatEvents = useCreation(() => { + return { handleSetData, handleResetData, handleSend, handleSetElements } + }, []) + + return [state, events] as const +} + +export default useCasualChat diff --git a/src/pages/AIAgent/ai-re-act/hooks/useChatContent.ts b/src/pages/AIAgent/ai-re-act/hooks/useChatContent.ts new file mode 100644 index 00000000..19d1bf93 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/useChatContent.ts @@ -0,0 +1,969 @@ +import { useRef } from 'react' +import { useCreation, useMemoizedFn } from 'ahooks' +import { + base64ToJson, + genBaseAIChatData, + genErrorLogData, + handleGrpcDataPushLog, + isToolStderrStream, + isToolStdoutStream, +} from './utils' +import type { UseChatContentEvents, UseChatContentParams } from './type' +import { + AIStreamContentType, + convertNodeIdToVerbose, + DefaultAIToolResult, + DefaultToolResultSummary, +} from './defaultConstant' +import type { AIAgentGrpcApi, AIOutputEvent } from './grpcApi' +import type { AIChatQSData, AIToolResult, ReActChatGroupElement } from './aiRender' +import { AIChatQSDataTypeEnum } from './aiRender' +import cloneDeep from 'lodash/cloneDeep' + +function useChatContent(params: UseChatContentParams): UseChatContentEvents + +function useChatContent(params: UseChatContentParams) { + const { chatType, getContentMap, setContentMap, getElements, setElements, pushLog, handleUnkData } = params + + /** 更新触发渲染的UI数据项 */ + const updateElements = useMemoizedFn( + (main: { mapKey: string; type: AIChatQSDataTypeEnum }, sub?: { mapKey: string; type: AIChatQSDataTypeEnum }) => { + // 先判断该项是否存在 + const target = getElements().findIndex( + (item) => item.token === main.mapKey && item.type === main.type && (sub ? item.isGroup : true), + ) + try { + if (target >= 0) { + const newArr = [...getElements()] + + const item = newArr[target] + const newItem = { ...item, renderNum: item.renderNum + 1 } + newArr[target] = newItem + + if (!sub || !newItem.isGroup) { + setElements(newArr) + return newArr + } + const newChildren = [...newItem.children] + const subIndex = newChildren.findIndex((item) => item.token === sub.mapKey && item.type === sub.type) + if (subIndex >= 0) { + newChildren[subIndex] = { ...newChildren[subIndex], renderNum: newChildren[subIndex].renderNum + 1 } + } else { + newChildren.push({ + chatType: chatType, + token: sub.mapKey, + type: sub.type, + renderNum: 1, + }) + } + newItem.children = newChildren + setElements(newArr) + } else { + if (sub) { + setElements((old) => + old.concat([ + { + chatType: chatType, + token: main.mapKey, + type: main.type, + renderNum: 1, + isGroup: true, + children: [{ chatType: chatType, token: sub.mapKey, type: sub.type, renderNum: 1 }], + }, + ]), + ) + } else { + setElements((old) => + old.concat([{ chatType: chatType, token: main.mapKey, type: main.type, renderNum: 1 }]), + ) + } + } + } catch (error) {} + }, + ) + /** 删除触发渲染的UI数据项 */ + // const deleteElements = useMemoizedFn((token: string, type: AIChatQSDataTypeEnum) => { + // // 先判断该项是否存在 + // const target = getElements().findIndex((item) => item.token === token && item.type === type) + // if (target >= 0) { + // setElements((old) => { + // const newArr = [...old] + // newArr.splice(target, 1) + // return newArr + // }) + // } + // }) + + /** + * - 存放 CallTollId 对应的stream类型集合({eventUUID, nodeID}[]) + * - 没有 CallToolId 的stream类型数据不进行记录 + * - 一般由stream-start类型进行记录,如果没有则由stream类型进行补充 + */ + const callToolIDToUUIDMap = useRef>(new Map()) + + /** + * - 存放 Type:stream NodeId:tool-xxx-stderr 的UUID对应的CallToolID + * - eventUUID => call_tool_id + */ + const toolResultErrorUUIDToCallToolID = useRef>(new Map()) + /** + * - 存放 Type:stream NodeId:tool-xxx-stderr 的内容数据 + * - call_tool_id => {content:string uuid:string status:"start" | "end"} + * - 当stream-finished触发后,将内容全部设置到工具结果对象中的execError字段中 + * - 本NodeId和stream类型中的其他NodeId有一样的后端逻辑,但是前端需要将其区分出来 + */ + const streamToToolResultError = useRef>( + new Map(), + ) + + // #region stream类型数据处理(OK) + /** 判断流式数据(type==='stream')在UI中是单项展示还是集合组展示 */ + const handleIsGroupDisplay = useMemoizedFn( + (params: { + mapKey: string + type: AIChatQSDataTypeEnum + nodeID: AIOutputEvent['NodeId'] + contentType: AIOutputEvent['ContentType'] + }) => { + const { mapKey, type, nodeID, contentType } = params + + const renderList = getElements() + if (contentType !== AIStreamContentType.DEFAULT || renderList.length === 0) { + // 非默认内容类型, 直接渲染 || 没有任何渲染数据, 直接渲染 + updateElements({ mapKey, type }) + return + } + + const lastRender = renderList[renderList.length - 1] + if (lastRender.token === mapKey) { + // 最后一项渲染数据就是当前数据,直接更新渲染次数 + updateElements({ mapKey, type: lastRender.type }, lastRender.isGroup ? { mapKey, type } : undefined) + return + } + const lastRenderData = getContentMap(lastRender.token) + if (!lastRenderData || lastRenderData.type !== AIChatQSDataTypeEnum.STREAM) { + updateElements({ mapKey, type }) + return + } + + if (lastRender.type === AIChatQSDataTypeEnum.STREAM && !lastRender.isGroup) { + // 单项的stream数据 + if (lastRenderData.data.NodeId === nodeID) { + // 命中单项,准备整合成组数据,将原有单项的token当成组token + const groupInfo: ReActChatGroupElement = { + chatType: chatType, + token: lastRender.token, + type: AIChatQSDataTypeEnum.STREAM_GROUP, + renderNum: 1, + isGroup: true, + children: [ + cloneDeep(lastRender), + { chatType: chatType, token: mapKey, type: AIChatQSDataTypeEnum.STREAM, renderNum: 1 }, + ], + } + const arr = groupInfo.children.map((item) => item.token) + for (let el of arr) { + const info = getContentMap(el) + if (info) setContentMap(el, { ...info, parentGroupKey: lastRender.token }) + } + setElements((old) => { + const newArr = [...old] + newArr.pop() + newArr.push(groupInfo) + return newArr + }) + } else { + // 未命中 + updateElements({ mapKey, type }) + } + } else if (lastRender.type === AIChatQSDataTypeEnum.STREAM_GROUP && lastRender.isGroup) { + // 组的stream数据 + if (lastRenderData.data.NodeId === nodeID) { + // 命中组内数据,追加到组内 + const subData = getContentMap(mapKey) + if (subData) { + setContentMap(mapKey, { ...subData, parentGroupKey: lastRender.token }) + } + updateElements({ mapKey: lastRender.token, type: lastRender.type }, { mapKey: mapKey, type: type }) + } else { + // 未命中 + updateElements({ mapKey, type }) + } + } else { + updateElements({ mapKey, type }) + } + }, + ) + + /** stream类型数据初始化 */ + const handleInitStream = useMemoizedFn((res: AIOutputEvent) => { + try { + // 属于日志数据的不进入UI展示 + if (res.IsSystem || res.IsReason) return + + const { CallToolID, NodeId } = res + if (!NodeId) return + + const ipcContent = base64ToJson(res.Content) || '' + const { event_writer_id } = JSON.parse(ipcContent) as { event_writer_id: string } + // event_writer_id为空 + if (!event_writer_id) { + pushLog(genErrorLogData(res.Timestamp, `${res.Type}数据(NodeId: ${NodeId}), event_writer_id 为空`)) + return + } + + // tool-xxx-stdout 数据单独初始化逻辑 + if (isToolStdoutStream(NodeId) && !!CallToolID) { + let toolResult = getContentMap(CallToolID) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { + pushLog( + genErrorLogData( + res.Timestamp, + `NodeID: ${NodeId} 的stream数据没有对应的工具结果(CallToolID: ${CallToolID})初始化`, + ), + ) + return + } + + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + toolResult.data.stream.EventUUID = event_writer_id + toolResult.data.type = 'stream' + setContentMap(event_writer_id, { + ...genBaseAIChatData(res), + id: event_writer_id, + chatType, + type: AIChatQSDataTypeEnum.STREAM, + data: { + NodeId, + NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(NodeId), + TaskIndex: res.TaskIndex || undefined, + CallToolID, + EventUUID: event_writer_id, + status: 'start', + content: '', + ContentType: res.ContentType, + }, + }) + return + } + // tool-xxx-stderr 数据单独初始化逻辑 + if (isToolStderrStream(NodeId) && !!CallToolID) { + if (!toolResultErrorUUIDToCallToolID.current.has(event_writer_id)) { + toolResultErrorUUIDToCallToolID.current.set(event_writer_id, CallToolID) + } + if (!streamToToolResultError.current.has(CallToolID)) { + streamToToolResultError.current.set(CallToolID, { + content: '', + uuid: event_writer_id, + status: 'start', + }) + } + return + } + + // 数据集合中对应的数据 + const streamData = getContentMap(event_writer_id) + + // 数据已存在,流数据输出顺序不对, 视为异常 + if (streamData) { + pushLog( + genErrorLogData( + res.Timestamp, + `异常 ${res.Type} 类型, NodeId: ${NodeId}, eventuuid: (${event_writer_id}), 已存在对应的数据`, + ), + ) + return + } + + // callToolID 对应 stream-eventuuid + if (CallToolID) { + const ids = callToolIDToUUIDMap.current.get(CallToolID) || [] + const idFind = ids.find((item) => item.eventUUID === event_writer_id && item.nodeID === NodeId) + !idFind && + callToolIDToUUIDMap.current.set(CallToolID, ids.concat([{ eventUUID: event_writer_id, nodeID: NodeId }])) + } + + setContentMap(event_writer_id, { + ...genBaseAIChatData(res), + id: event_writer_id, + chatType, + type: AIChatQSDataTypeEnum.STREAM, + data: { + NodeId, + NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(NodeId), + TaskIndex: res.TaskIndex || undefined, + CallToolID, + EventUUID: event_writer_id, + status: 'start', + content: '', + ContentType: res.ContentType, + }, + }) + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: pushLog, + }) + } + }) + + /** stream类型流数据进入 */ + const handleStream = useMemoizedFn((res: AIOutputEvent) => { + try { + // 属于日志数据的不进入UI展示 + if (res.IsSystem || res.IsReason) return + + const { CallToolID, EventUUID, NodeId } = res + if (!EventUUID || !NodeId) return + + const content = (base64ToJson(res.Content) || '') + (base64ToJson(res.StreamDelta) || '') + + // tool-xxx-stderr 数据单独处理逻辑 + if (!!CallToolID && isToolStderrStream(NodeId)) { + const call_tool_id = toolResultErrorUUIDToCallToolID.current.get(EventUUID) + const errorResult = streamToToolResultError.current.get(call_tool_id || '') + if (call_tool_id && errorResult) { + streamToToolResultError.current.set(call_tool_id, { + ...errorResult, + content: errorResult.content + content, + }) + } + return + } + // tool-xxx-stdout 数据单独处理逻辑 + if (!!CallToolID && isToolStdoutStream(NodeId)) { + const toolResult = getContentMap(CallToolID) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT || !toolResult.data.stream.EventUUID) { + pushLog( + genErrorLogData( + res.Timestamp, + `NodeID: ${NodeId} 的stream数据没有对应的工具结果(CallToolID: ${CallToolID})初始化`, + ), + ) + return + } + const toolForStreamData = getContentMap(toolResult.data.stream.EventUUID) + if (!toolForStreamData || toolForStreamData.type !== AIChatQSDataTypeEnum.STREAM) { + pushLog( + genErrorLogData( + res.Timestamp, + `EventUUID: ${toolResult.data.stream.EventUUID} 的stream数据没有对应的初始化`, + ), + ) + return + } + const isRender = !toolForStreamData.data.content + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + toolForStreamData.data.content += content + if (isRender) { + updateElements({ mapKey: toolResult.id, type: toolResult.type }) + } + return + } + + // 数据集合中对应的数据 + const streamData = getContentMap(EventUUID) + + // 数据不存在 + if (!streamData || streamData.type !== AIChatQSDataTypeEnum.STREAM) { + pushLog(genErrorLogData(res.Timestamp, `异常 stream 类型, NodeId: ${NodeId}, eventuuid: (${EventUUID})`)) + return + } + + // 没有经过stream初始化的数据,后补初始化状态情况 + if (CallToolID) { + const ids = callToolIDToUUIDMap.current.get(CallToolID) || [] + const idFind = ids.find((item) => item.eventUUID === EventUUID && item.nodeID === NodeId) + !idFind && callToolIDToUUIDMap.current.set(CallToolID, ids.concat([{ eventUUID: EventUUID, nodeID: NodeId }])) + } + + const isRender = !streamData.data.content + + const newStream = { + ...streamData, + Timestamp: isRender ? res.Timestamp : streamData.Timestamp, + data: { + ...streamData.data, + content: streamData.data.content + content, + }, + } + + setContentMap(EventUUID, newStream) + if (isRender) { + // 如果折叠写不完,则打开注释,然后将注释的下一个方法调用注释 + handleIsGroupDisplay({ + mapKey: EventUUID, + type: newStream.type, + nodeID: NodeId, + contentType: res.ContentType, + }) + } + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: pushLog, + }) + } + }) + + /** stream类型数据结束 */ + const handleEndStream = useMemoizedFn((res: AIOutputEvent) => { + try { + let ipcContent = base64ToJson(res.Content) || '' + const { event_writer_id, node_id } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStreamFinished + if (!event_writer_id) { + pushLog(genErrorLogData(res.Timestamp, `stream-finished数据, event_writer_id 为空`)) + return + } + + // tool-xxx-stderr 数据单独结束逻辑 + if (isToolStderrStream(node_id)) { + handleToolResultErrorEnd(event_writer_id) + return + } + // tool-xxx-stdout 数据单独结束逻辑 + if (isToolStdoutStream(node_id)) { + const toolResult = getContentMap(res.CallToolID) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT || !toolResult.data.stream.EventUUID) { + return + } + const toolForStreamData = getContentMap(toolResult.data.stream.EventUUID) + if (!toolForStreamData || toolForStreamData.type !== AIChatQSDataTypeEnum.STREAM) { + return + } + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + toolForStreamData.data.status = 'end' + const isShowAll = toolForStreamData.data.content.length > 25600 // 50KB大概字符数25600 + const displayContent = isShowAll + ? '...' + toolForStreamData.data.content.slice(-25600) + '...' + : toolForStreamData.data.content + toolResult.data.tool.toolStdoutContent = { content: displayContent, isShowAll } + updateElements({ mapKey: toolResult.id, type: toolResult.type }) + return + } + + // 数据集合中对应的数据 + const streamData = getContentMap(event_writer_id) + // 数据不存在 不输出到日志,因为日志的流数据也有该类型数据 + if (!streamData || streamData.type !== AIChatQSDataTypeEnum.STREAM) return + + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + streamData.data.status = 'end' + if (streamData.parentGroupKey) { + updateElements( + { mapKey: streamData.parentGroupKey, type: AIChatQSDataTypeEnum.STREAM_GROUP }, + { mapKey: event_writer_id, type: AIChatQSDataTypeEnum.STREAM }, + ) + } else { + updateElements({ mapKey: event_writer_id, type: AIChatQSDataTypeEnum.STREAM }) + } + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: pushLog, + }) + } + }) + + /** 工具结果错误信息结束输出 */ + const handleToolResultErrorEnd = useMemoizedFn((uuid: string) => { + const call_tool_id = toolResultErrorUUIDToCallToolID.current.get(uuid) + const errorResult = streamToToolResultError.current.get(call_tool_id || '') + if (!call_tool_id || !errorResult) return + + const toolResult = getContentMap(call_tool_id) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { + // 工具执行结果卡片UI没有展示时 + errorResult.status = 'end' + streamToToolResultError.current.set(call_tool_id, { ...errorResult }) + return + } else { + const showUI = + getElements().findIndex( + (item) => item.token === call_tool_id && item.type === AIChatQSDataTypeEnum.TOOL_RESULT, + ) >= 0 + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + toolResult.data.tool.execError = errorResult.content + if (showUI) updateElements({ mapKey: toolResult.id, type: toolResult.type }) + streamToToolResultError.current.delete(call_tool_id) + toolResultErrorUUIDToCallToolID.current.delete(uuid) + } + }) + // #endregion + + // #region 工具执行过程和结果相关数据逻辑 + /** 工具开始执行 */ + const handleStartTool = useMemoizedFn((res: AIOutputEvent) => { + try { + const ipcContent = base64ToJson(res.Content) || '' + const { call_tool_id, tool, start_time, start_time_ms } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall + if (!call_tool_id) { + pushLog(genErrorLogData(res.Timestamp, `${res.Type}数据, call_tool_id 为空`)) + return + } + + const toolResult: AIToolResult = { + ...cloneDeep(DefaultAIToolResult), + TaskIndex: res.TaskIndex || undefined, + callToolId: call_tool_id, + toolName: tool?.name || '-', + toolDescription: tool?.description || '', + startTime: start_time || 0, + startTimeMS: start_time_ms || 0, + } + + setContentMap(call_tool_id, { + ...genBaseAIChatData(res), + id: call_tool_id, + chatType, + type: AIChatQSDataTypeEnum.TOOL_RESULT, + data: toolResult, + }) + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: pushLog, + }) + } + }) + + /** 工具执行中的可操作项 */ + const handleExceTool = useMemoizedFn((res: AIOutputEvent) => { + try { + const ipcContent = base64ToJson(res.Content) || '' + const { call_tool_id, id, selectors } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCallWatcher + + if (!call_tool_id || !id || !selectors || !selectors?.length) { + pushLog( + genErrorLogData( + res.Timestamp, + `${res.Type}数据, call_tool_id: ${call_tool_id || '为空'} | id: ${id || '为空'}`, + ), + ) + return + } + + const toolResult = getContentMap(call_tool_id) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT || !toolResult.data.stream.EventUUID) { + pushLog( + genErrorLogData( + res.Timestamp, + `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应输出的tool_call_start类型初始化`, + ), + ) + return + } + const toolForStreamData = getContentMap(toolResult.data.stream.EventUUID) + if (!toolForStreamData || toolForStreamData.type !== AIChatQSDataTypeEnum.STREAM) { + pushLog( + genErrorLogData(res.Timestamp, `EventUUID: ${toolResult.data.stream.EventUUID} 的stream数据没有对应的初始化`), + ) + return + } + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + toolForStreamData.data.selectors = { + callToolId: call_tool_id, + InteractiveId: id, + selectors: selectors, + } + updateElements({ mapKey: toolResult.id, type: toolResult.type }) + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: pushLog, + }) + } + }) + + /** 工具执行中的工作文件目录路径 */ + const handleToolDirPath = useMemoizedFn((res: AIOutputEvent) => { + try { + const ipcContent = base64ToJson(res.Content) || '' + const { call_tool_id, dir_path } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCallDirPath + if (!call_tool_id) { + pushLog(genErrorLogData(res.Timestamp, `${res.Type}数据, call_tool_id 为空`)) + return + } + + const toolResult = getContentMap(call_tool_id) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { + pushLog( + genErrorLogData( + res.Timestamp, + `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应输出的tool_call_start类型初始化`, + ), + ) + return + } + + if (toolResult.data.tool.dirPath) { + pushLog( + genErrorLogData(res.Timestamp, `${res.Type}数据(call_tool_id:${call_tool_id}), dir_path已存在,不能重复设置`), + ) + return + } + + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + toolResult.data.tool.dirPath = dir_path || '' + if (toolResult.data.tool.status !== 'default') { + updateElements({ mapKey: toolResult.id, type: toolResult.type }) + } + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: pushLog, + }) + } + }) + + /** 工具执行的结果 */ + const handleToolResult = useMemoizedFn((res: AIOutputEvent, status: AIToolResult['tool']['status']) => { + try { + const ipcContent = base64ToJson(res.Content) || '' + const { call_tool_id, ...rest } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall + + if (!call_tool_id) { + pushLog(genErrorLogData(res.Timestamp, `${res.Type}数据, call_tool_id 为空`)) + return + } + + const toolResult = getContentMap(call_tool_id) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { + pushLog( + genErrorLogData( + res.Timestamp, + `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应输出的tool_call_start类型初始化`, + ), + ) + return + } + + // 下面的设置: 是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + + // 设置工具执行的开始时间、结束时间和持续时间等数据 + toolResult.data.type = 'result' + toolResult.data.startTime = rest.start_time || 0 + toolResult.data.startTimeMS = rest.start_time_ms || 0 + toolResult.data.endTime = rest.end_time || 0 + toolResult.data.endTimeMS = rest.end_time_ms || 0 + toolResult.data.durationMS = rest.duration_ms || 0 + toolResult.data.durationSeconds = rest.duration_seconds || 0 + toolResult.data.tool.status = status + + // 设置总结内容,没有就设置成获取中,有就使用获取到的内容 + toolResult.data.tool.summary = toolResult.data.tool.summary || DefaultToolResultSummary[status]?.wait || '' + // 设置执行结果错误数据内容(std_xxx_stderr) + const errorResult = streamToToolResultError.current.get(call_tool_id) + if (errorResult && errorResult.status === 'end') { + toolResult.data.tool.execError = errorResult.content + // error数据先出但未存在对应的工具执行结果,工具结果出现后直接使用并删除map中的缓存数据 + streamToToolResultError.current.delete(call_tool_id) + toolResultErrorUUIDToCallToolID.current.delete(errorResult.uuid) + } + updateElements({ mapKey: toolResult.id, type: toolResult.type }) + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: pushLog, + }) + } + }) + + /** 工具执行的总结 */ + const handleToolSummary = useMemoizedFn((res: AIOutputEvent) => { + try { + const ipcContent = base64ToJson(res.Content) || '' + const { call_tool_id, summary } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall + + if (!call_tool_id) { + pushLog(genErrorLogData(res.Timestamp, `${res.Type}数据, call_tool_id 为空`)) + return + } + + const toolResult = getContentMap(call_tool_id) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { + pushLog( + genErrorLogData( + res.Timestamp, + `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应输出的tool_call_start类型初始化`, + ), + ) + return + } + + const statusInfo = toolResult.data.tool.status + const summaryContent = !summary || summary === 'null' ? '' : summary + // 下面的设置: 是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + // 设置总结内容,没有就设置成默认的状态展示内容,有就使用获取到的内容 + toolResult.data.tool.summary = + statusInfo === 'user_cancelled' + ? '当前工具调用已被取消,会使用当前输出结果进行后续工作决策' + : summaryContent || DefaultToolResultSummary[toolResult.data.tool.status]?.result || '' + // 设置执行结果错误数据内容(std_xxx_stderr) + const errorResult = streamToToolResultError.current.get(call_tool_id) + if (errorResult && errorResult.status === 'end') { + toolResult.data.tool.execError = errorResult.content + // error数据先出但未存在对应的工具执行结果,工具结果出现后直接使用并删除map中的缓存数据 + streamToToolResultError.current.delete(call_tool_id) + toolResultErrorUUIDToCallToolID.current.delete(errorResult.uuid) + } + if (statusInfo !== 'default') updateElements({ mapKey: toolResult.id, type: toolResult.type }) + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: pushLog, + }) + } + }) + // #endregion + + // #region 自由对话和任务规划的公共类型数据处理逻辑 + /** 参考资料类型-数据处理 */ + const handleStreamAppendReference = useMemoizedFn((res: AIOutputEvent) => { + try { + const ipcContent = base64ToJson(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ReferenceMaterialPayload + + const chatData = getContentMap(data.event_uuid) + const toolResult = getContentMap(res.CallToolID || '') + if (chatData) { + // 下面的设置: 是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + chatData.reference = (chatData.reference || []).concat([data]) + if (chatData.parentGroupKey) { + updateElements( + { mapKey: chatData.parentGroupKey, type: AIChatQSDataTypeEnum.STREAM_GROUP }, + { mapKey: data.event_uuid, type: chatData.type }, + ) + } else if (chatData.type === AIChatQSDataTypeEnum.STREAM) { + if (!!toolResult && isToolStdoutStream(chatData.data.NodeId)) { + // 特殊情况,更新stdout流对应的工具执行结果卡片UI + updateElements({ mapKey: toolResult.id, type: toolResult.type }) + return + } + handleIsGroupDisplay({ + mapKey: chatData.id, + type: chatData.type, + nodeID: chatData.data.NodeId, + contentType: chatData.data.ContentType, + }) + } else { + updateElements({ mapKey: data.event_uuid, type: chatData.type }) + } + } else if ( + !!toolResult && + toolResult.type === AIChatQSDataTypeEnum.TOOL_RESULT && + toolResult.data.stream.EventUUID === data.event_uuid + ) { + toolResult.reference = (toolResult.reference || []).concat([data]) + updateElements({ mapKey: toolResult.id, type: toolResult.type }) + } else { + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + id: data.event_uuid, + chatType, + type: AIChatQSDataTypeEnum.Reference_Material, + data: { + NodeId: res.NodeId, + NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(res.NodeId), + }, + reference: [data], + } + setContentMap(chatData.id, chatData) + updateElements({ mapKey: chatData.id, type: AIChatQSDataTypeEnum.Reference_Material }) + } + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: pushLog, + }) + } + }) + // #endregion + + // 处理数据方法 + const handleSetData = useMemoizedFn((res: AIOutputEvent) => { + try { + let ipcContent = base64ToJson(res.Content) || '' + + // #region 自由对话(ReAct)专属类型(OK) + // 问题的思考 + if (res.Type === 'thought') { + const { thought } = (JSON.parse(ipcContent) as AIAgentGrpcApi.AIChatThought) || {} + + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType, + type: AIChatQSDataTypeEnum.THOUGHT, + data: thought || '', + } + setContentMap(chatData.id, chatData) + updateElements({ mapKey: chatData.id, type: chatData.type }) + return + } + + // 问题一次性的结果输出 + if (res.Type === 'result') { + const { result, after_stream } = (JSON.parse(ipcContent) as AIAgentGrpcApi.AIChatResult) || {} + if (after_stream) return + + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType, + type: AIChatQSDataTypeEnum.THOUGHT, + data: result || '', + } + setContentMap(chatData.id, chatData) + updateElements({ mapKey: chatData.id, type: chatData.type }) + return + } + + // ReAct任务(自由对话)崩溃的错误信息 + if (res.Type === 'fail_react_task') { + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType, + type: AIChatQSDataTypeEnum.FAIL_REACT, + data: { + content: ipcContent, + NodeId: res.NodeId, + NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(res.NodeId), + }, + } + setContentMap(chatData.id, chatData) + updateElements({ mapKey: chatData.id, type: chatData.type }) + return + } + // #endregion + + // #region stream类型数据处理(OK) + // stream数据开始标识 + if (res.Type === 'stream_start') { + handleInitStream(res) + return + } + + // stream数据 + if (res.Type === 'stream') { + handleStream(res) + return + } + + // stream数据结束标识 + if (res.Type === 'structured' && res.NodeId === 'stream-finished') { + handleEndStream(res) + return + } + // #endregion + + // #region 工具结果和总结数据处理逻辑 + // 工具执行-开始标识(OK) + if (res.Type === 'tool_call_start') { + handleStartTool(res) + return + } + + // 工具执行中-可操作选项 + if (res.Type === 'tool_call_watcher') { + handleExceTool(res) + return + } + + // 工具执行-工作目录路径 + if (res.Type === 'tool_call_log_dir') { + handleToolDirPath(res) + return + } + + // 工具执行结果-用户取消 + if (res.Type === 'tool_call_user_cancel') { + handleToolResult(res, 'user_cancelled') + return + } + // 工具执行结果-成功 + if (res.Type === 'tool_call_done') { + handleToolResult(res, 'success') + return + } + // 工具执行结果-失败 + if (res.Type === 'tool_call_error') { + handleToolResult(res, 'failed') + return + } + + if (res.Type === 'tool_call_summary') { + // 工具执行结果-总结 + handleToolSummary(res) + return + } + // #endregion + + // #region 任务规划和自由对话都有的公共类型(OK) + // 工具决策 + if (res.Type === 'tool_call_decision') { + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ToolCallDecision + const i18n = data?.i18n || { zh: data.action, en: data.action } + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType, + type: AIChatQSDataTypeEnum.TOOL_CALL_DECISION, + data: { + ...data, + i18n: { + Zh: i18n.zh, + En: i18n.en, + }, + }, + } + setContentMap(chatData.id, chatData) + updateElements({ mapKey: chatData.id, type: chatData.type }) + return + } + + // 任务规划崩溃的错误信息[在任务规划启动就崩溃时,出现在自由对话中] + if (res.Type === 'fail_plan_and_execution') { + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType, + type: AIChatQSDataTypeEnum.FAIL_PLAN_AND_EXECUTION, + data: { + content: ipcContent, + NodeId: res.NodeId, + NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(res.NodeId), + }, + } + setContentMap(chatData.id, chatData) + updateElements({ mapKey: chatData.id, type: chatData.type }) + return + } + + // 参考资料, 可独立或追加到别的类型数据中展示 + if (res.Type === 'reference_material') { + handleStreamAppendReference(res) + return + } + // #endregion + + handleUnkData(res) + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: pushLog, + }) + } + }) + + /** reset */ + const handleResetData = useMemoizedFn(() => { + callToolIDToUUIDMap.current.clear() + toolResultErrorUUIDToCallToolID.current.clear() + streamToToolResultError.current.clear() + }) + + const events: UseChatContentEvents = useCreation(() => { + return { handleSetData, handleResetData } + }, []) + + return events +} + +export default useChatContent diff --git a/src/pages/AIAgent/ai-re-act/hooks/useChatIPC.ts b/src/pages/AIAgent/ai-re-act/hooks/useChatIPC.ts new file mode 100644 index 00000000..82a6fd06 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/useChatIPC.ts @@ -0,0 +1,1268 @@ +import { useEffect, useRef, useState } from 'react' +import { yakitNotify } from '@/utils/notification' +import { useCreation, useInterval, useMemoizedFn, useThrottleFn } from 'ahooks' +import { useEventSource, useGetSetState, useThrottleState } from '@/hooks' +import useAIPerfData, { UseAIPerfDataTypes } from './useAIPerfData' +import useCasualChat from './useCasualChat' +import useYakExecResult, { UseYakExecResultTypes } from './useYakExecResult' +import useTaskChat from './useTaskChat' +import { base64ToJson, genErrorLogData, genExecTasks, handleGrpcDataPushLog } from './utils' +import type { + AIChatIPCNotifyMessage, + AIChatIPCStartParams, + AIChatSendParams, + AIFileSystemPin, + AIQuestionQueues, + CasualLoadingStatus, + PlanLoadingStatus, + TaskChatTaskInfo, + UseCasualChatEvents, + UseChatIPCEvents, + UseChatIPCParams, + UseChatIPCState, + UseHookBaseParams, +} from './type' +import type { AIAgentGrpcApi, AIInputEvent, AIOutputEvent, AIStartParams } from './grpcApi' +import { AIInputEventSyncTypeEnum, AITaskStatus } from './grpcApi' +import useAIChatLog from './useAIChatLog' +import cloneDeep from 'lodash/cloneDeep' +import { + convertNodeIdToVerbose, + DeafultAIQuestionQueues, + DefaultCasualLoadingStatus, + DefaultMemoryList, + DefaultPlanHistoryList, + DefaultPlanLoadingStatus, +} from './defaultConstant' +import useAINodeLabel from './useAINodeLabel' +import { grpcQueryAIEvent } from '../../ai-agent/grpc' +import type { AIChatData } from '../../ai-agent/type/aiChat' +import type { DeepPartial } from '../../ai-agent/store/ChatDataStore' +import { postCancelMessage, postSendContinueMessage, postSendFirstMessage } from '@/apis/AiEventApi' +import { omit } from 'lodash' +import type { ReActChatBaseInfo } from './aiRender' + +const HISTORY_EVENT_PAGE_SIZE = 500 + +const hasCachedChatRenderData = (chatData?: AIChatData) => { + if (!chatData) return false + return !!(chatData.casualChat?.elements?.length || chatData.taskChat?.elements?.length) +} + +function useChatIPC(params?: UseChatIPCParams): [UseChatIPCState, UseChatIPCEvents] + +function useChatIPC(params?: UseChatIPCParams) { + const { + cacheDataStore, + channelName, + setSessionChatName, + onTaskStart, + onTaskReview, + onTaskReviewExtra, + onReviewRelease, + onEnd, + onSyncIDChange, + } = params || {} + + const { getLabelByParams } = useAINodeLabel() + + // #region 全局公共方法集合 + /** 自由对话(ReAct)-review 信息的自动释放 */ + const handleCasualReviewRelease = useMemoizedFn((id: string) => { + onReviewRelease && onReviewRelease('casual', id) + }) + // 任务规划-review 信息的自动释放 + const handleTaskReviewRelease = useMemoizedFn((id: string) => { + onReviewRelease && onReviewRelease('task', id) + }) + + /** 消息通知提醒弹框 */ + const handleNotifyMessage = useMemoizedFn((message: AIChatIPCNotifyMessage) => { + const { NodeIdVerbose, Content } = message + const verbose = getLabelByParams(NodeIdVerbose) + yakitNotify('info', { + message: verbose, + description: Content, + }) + }) + + /** 发送会话的第一条消息 */ + const sendFirstMessage = useMemoizedFn(() => { + if (!chatID.current) { + yakitNotify('error', 'AI异常, 未记录session却处于执行状态, 请关闭AI页面重试!') + return + } + postSendFirstMessage(chatID.current, { ...aiRequestAll.current }).finally(() => { + if (aiRequest.current?.UserQuery) { + sendMessage({ + IsFreeInput: true, + FreeInput: aiRequest.current.UserQuery, + AttachedResourceInfo: aiRequestAll.current?.AttachedResourceInfo, + FocusModeLoop: aiRequestAll.current?.FocusModeLoop, + }) + } + }) + setTimeout(() => { + handleStartQuestionQueue() + }, 50) + }) + /** 会话中发送消息 */ + const sendMessage = useMemoizedFn((params: AIInputEvent) => { + if (!chatID.current) return + // console.log('send-ai-re-act---\n', chatID.current, params) + postSendContinueMessage(chatID.current, params) + }) + + /** 获取当前会话数据集类实例 */ + const fetchChatDataStore = useMemoizedFn(() => { + return cacheDataStore + }) + // #endregion + + // #region 全局状态变量 + /** 通信的唯一标识符 */ + const chatID = useRef('') + const fetchToken = useMemoizedFn(() => { + return chatID.current + }) + + /** 启动流接口的请求参数 */ + const aiRequestAll = useRef() + const aiRequest = useRef() + const fetchAIRequest = useMemoizedFn(() => { + return cloneDeep(aiRequest.current) + }) + const handleResetAIRequest = useMemoizedFn(() => { + aiRequest.current = undefined + aiRequestAll.current = undefined + }) + + /** 获取全部聊天数据 */ + const getChatDataStore: UseHookBaseParams['getChatDataStore'] = useMemoizedFn(() => { + if (!chatID.current) { + throw new Error('session is empty') + } + return cacheDataStore?.get(chatID.current) + }) + + // 通信的状态 + const [execute, setExecute, getExecute] = useGetSetState(false) + // #endregion + + // CoordinatorIDs + const updateCoordinatorIDs = useMemoizedFn((id: string) => { + const ids = getChatDataStore()?.coordinatorIDs + if (!ids) { + try { + cacheDataStore?.updater(chatID.current, { coordinatorIDs: [id] }) + } catch (error) {} + } else { + if (!ids.includes(id)) ids.push(id) + } + }) + + // #region 接口更新的(文件|文件夹)数据集合 + const [grpcFolders, setGrpcFolders] = useState([]) + const handleSetGrpcFolders = useMemoizedFn((info: AIFileSystemPin) => { + setGrpcFolders((old) => { + const isExist = old.find((item) => item.path === info.path) + if (isExist) return old + return [...old, info] + }) + }) + + const handleResetGrpcFile = useMemoizedFn(() => { + setGrpcFolders([]) + }) + // #endregion + + // #region grpc流里所有的runtimeIDs集合 + const [runTimeIDs, setRunTimeIDs] = useState([]) + + const handleResetRunTimeIDs = useMemoizedFn(() => { + setRunTimeIDs([]) + }) + // #endregion + + // #region 问题队列相关逻辑 + // 问题队列(自由对话专属)[todo: 后续存在任务规划的问题队列后,需要放入对应的hook中进行处理和储存] + const [questionQueue, setQuestionQueue] = useState(cloneDeep(DeafultAIQuestionQueues)) + + const handleResetQuestionQueue = useMemoizedFn(() => { + setQuestionQueue(cloneDeep(DeafultAIQuestionQueues)) + }) + // #endregion + + // #region 实时记忆列表相关逻辑 + const reactMemorys = useRef(cloneDeep(DefaultMemoryList)) + const taskMemorys = useRef(cloneDeep(DefaultMemoryList)) + const [memoryList, setMemoryList] = useState(cloneDeep(DefaultMemoryList)) + + const handleResetMemoryList = useMemoizedFn(() => { + reactMemorys.current = cloneDeep(DefaultMemoryList) + taskMemorys.current = cloneDeep(DefaultMemoryList) + setMemoryList(cloneDeep(DefaultMemoryList)) + }) + // #endregion + + // #region 时间线相关逻辑 + // 实时时间线 + const [reActTimelines, setReActTimelines] = useThrottleState([], { wait: 100 }) + + const handleResetReActTimelines = useMemoizedFn(() => { + setReActTimelines([]) + }) + // #endregion + + // #region 系统信息流展示相关逻辑 + /** 记录都存在过的系统信息uuid, 只展示最新的一条系统信息 */ + const systemEventUUID = useRef([]) + const [systemStream, setSystemStream] = useState('') + const handleSetSystemStream = useMemoizedFn((uuid: string, content: string) => { + const lastUUID = systemEventUUID.current[systemEventUUID.current.length - 1] + if (lastUUID) { + if (lastUUID === uuid) { + setSystemStream((old) => old + content) + } else { + if (systemEventUUID.current.includes(uuid)) return + systemEventUUID.current.push(uuid) + setSystemStream(content) + } + } else { + systemEventUUID.current.push(uuid) + setSystemStream(content) + } + }) + const handleResetSystemStream = useMemoizedFn(() => { + systemEventUUID.current = [] + setSystemStream('') + }) + // #endregion + + // #region 专注模式状态相关逻辑 + const focusOfTaskID = useRef('') + const [focusMode, setFocusMode] = useState('') + const handleFocusModeChange = useMemoizedFn((id: string, mode: string) => { + focusOfTaskID.current = id + setFocusMode(mode) + }) + + const handleResetFocusMode = useMemoizedFn(() => { + focusOfTaskID.current = '' + setFocusMode('') + }) + // #endregion + + // #region 历史任务规划列表相关逻辑 + const [planHistoryList, setPlanHistoryList] = useState( + cloneDeep(DefaultPlanHistoryList), + ) + const handlePlanHistoryListChange = useMemoizedFn((list: AIAgentGrpcApi.PlanHistoryList) => { + try { + const arr = cloneDeep(list.records) + if (!arr || arr.length === 0) { + setPlanHistoryList({ ...list }) + return + } + const newArr = arr + .map((item) => { + // 因为后端给过来的task_progress是一个json的string类型数据 + item.task_progress = JSON.parse(item.task_progress as unknown as string) as AIAgentGrpcApi.PlanHistoryProgress + // 因为后端给过来的task_tree是一个json的string类型数据,所以需要转换成树形结构的数据,供UI展示使用 + const tree = JSON.parse(item.task_tree as unknown as string) as AIAgentGrpcApi.PlanTask + // 记录任务虎根节点的名字,供UI展示使用 + item.root_task_name = tree.name + item.task_tree = genExecTasks(tree) + return item + }) + .filter((item) => item.task_progress.phase !== 'Completed') + setPlanHistoryList({ ...list, records: newArr }) + } catch (error) {} + }) + const handleResetPlanHistoryList = useMemoizedFn(() => { + setPlanHistoryList(cloneDeep(DefaultPlanHistoryList)) + }) + // #endregion + + // #region 单次流执行时的输出展示数据 + // 日志 + const logEvents = useAIChatLog({ channelName }) + + // AI性能相关数据和逻辑 + const aiPerfDataEvent = useAIPerfData({ + pushLog: logEvents.pushLog, + getChatDataStore, + }) + // 执行过程中插件输出的卡片 + const [yakExecResult, yakExecResultEvent] = useYakExecResult({ + pushLog: logEvents.pushLog, + getChatDataStore, + }) + // #endregion + + // #region 自由对话(ReAct)相关变量和hook + const casualChatID = useRef(0) + + /** 用户主动关闭当前问题的loading状态(自由对话) */ + const [cancelCasualLoading, setCancelCasualLoading] = useState(false) + + /** 自由对话(ReAct)的loading状态 */ + const [casualStatus, setCasualStatus] = useState(cloneDeep(DefaultCasualLoadingStatus)) + const handleResetCasualChatLoading = useMemoizedFn(() => { + casualChatID.current = 0 + setCasualStatus(cloneDeep(DefaultCasualLoadingStatus)) + }) + + const [casualChat, casualChatEvent] = useCasualChat({ + pushLog: logEvents.pushLog, + getChatDataStore, + getRequest: fetchAIRequest, + onReviewRelease: handleCasualReviewRelease, + }) + // #endregion + + // #region 任务规划相关变量和hook + const isHistoryReplay = useRef(false) + + /** 任务规划对应的问题信息, 供UI使用,因为任务结束后,该变量不会清空 */ + const taskChatID = useRef() + const fetchTaskChatID = useMemoizedFn(() => { + return taskChatID.current + }) + const handleResetTaskChatID = useMemoizedFn(() => { + taskChatID.current = undefined + }) + + /** 用户主动关闭当前问题的loading状态(任务规划) */ + const [cancelTaskLoading, setCancelTaskLoading] = useState(false) + + /** 当前任务规划对应的数据流-CoordinatorId */ + const planCoordinatorId = useRef('') + /** 任务规划的loading状态 */ + const [taskStatus, setTaskStatus] = useState(cloneDeep(DefaultPlanLoadingStatus)) + + const handleResetTaskChatLoading = useMemoizedFn(() => { + planCoordinatorId.current = '' + setTaskStatus(cloneDeep(DefaultPlanLoadingStatus)) + }) + + const [taskChat, taskChatEvent] = useTaskChat({ + pushLog: logEvents.pushLog, + getChatDataStore, + getRequest: fetchAIRequest, + isHistoryReplay: () => isHistoryReplay.current, + onReview: onTaskReview, + onReviewExtra: onTaskReviewExtra, + onReviewRelease: handleTaskReviewRelease, + sendRequest: sendMessage, + }) + // #endregion + + /** 用户主动取消问题的loading状态变换 */ + const handleCancelLoadingChange = useMemoizedFn((type: ReActChatBaseInfo['chatType'], status: boolean) => { + if (type === 'reAct') { + setCancelCasualLoading(status) + } else { + setCancelTaskLoading(status) + } + }) + + // #region 问题和问题队列相关逻辑 + /** 更新问题队列状态 */ + const handleTriggerQuestionQueueRequest = useThrottleFn( + () => { + sendMessage({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO }) + }, + { wait: 50, leading: false }, + ).run + + // 问题入队|出队变化时-进行通知逻辑 + const handleQuestionQueueStatusChange = useMemoizedFn((res: AIOutputEvent) => { + try { + const { NodeId } = res + const ipcContent = base64ToJson(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.QuestionQueueStatusChange + if (NodeId === 'react_task_dequeue') { + if (data.focus_mode) { + // 记录专注模式状态 + handleFocusModeChange(data.react_task_id, data.focus_mode) + } else { + // 非专注模式状态 + handleResetFocusMode() + } + } + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: logEvents.pushLog, + }) + } finally { + handleTriggerQuestionQueueRequest() + } + }) + + // 问题队列清空操作-进行通知逻辑 + const handleReActTaskCleared = useMemoizedFn((res: AIOutputEvent) => { + try { + const { Type, NodeId, NodeIdVerbose, Timestamp } = res + handleNotifyMessage({ + Type, + NodeId, + NodeIdVerbose, + Timestamp, + Content: '已清空所有任务队列数据', + }) + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: logEvents.pushLog, + }) + } + }) + // #endregion + + // #region review事件相关方法 + /** review 界面选项触发事件 */ + const onSend = useMemoizedFn(({ token, type, params, optionValue, extraValue }: AIChatSendParams) => { + try { + if (!execute) { + yakitNotify('warning', 'AI 未执行任务,无法发送选项') + return + } + if (!chatID.current || chatID.current !== token) { + yakitNotify('warning', '该选项非本次 AI 执行的回答选项') + return + } + + if (params.IsConfigHotpatch) { + aiRequest.current = { ...(aiRequest.current || {}), ...(params.Params || {}) } + } + + switch (type) { + case 'casual': + case 'task': { + const events: UseCasualChatEvents | UseChatIPCEvents = type === 'casual' ? casualChatEvent : taskChatEvent + events.handleSend({ + request: params, + optionValue, + extraValue, + cb: () => { + sendMessage(params) + }, + }) + break + } + + default: + sendMessage(params) + break + } + } catch (error) {} + }) + // #endregion + + // #region 外界进行删除会话数据操作时的重置逻辑 + const delChats = useRef([]) + const onDelChats = useMemoizedFn((session: string[]) => { + const filterSessions = session.filter((item) => !delChats.current.includes(item)) + delChats.current.push(...filterSessions) + + let failedSessions: string[] = [] + let err: any = null + for (let item of filterSessions) { + try { + cacheDataStore?.remove(item) + } catch (error) { + failedSessions.push(item) + err = error + } + } + if (failedSessions.length > 0 && !!err) { + yakitNotify('error', `删除会话(${failedSessions.join(',')})失败: ${err}`) + } + }) + // #endregion + + /** grpc接口流断开瞬间, 需要将状态相关变量进行重置 */ + const handleResetGrpcStatus = useMemoizedFn(() => { + taskChatEvent.handleCloseGrpc() + setExecute(false) + handleResetCasualChatLoading() + handleResetTaskChatID() + handleResetTaskChatLoading() + }) + + /** 流接口开始前需要重置的一些状态 */ + const handleResetBeforeStart = useMemoizedFn(() => { + handleResetFocusMode() + }) + + /** 重置所有数据 */ + const onReset = useMemoizedFn(() => { + chatID.current = '' + handleResetAIRequest() + setExecute(false) + handleResetGrpcFile() + handleResetRunTimeIDs() + handleResetQuestionQueue() + handleResetMemoryList() + handleResetReActTimelines() + handleResetSystemStream() + handleResetFocusMode() + handleResetPlanHistoryList() + handleResetCasualChatLoading() + handleResetTaskChatID() + handleResetTaskChatLoading() + + setCancelCasualLoading(false) + setCancelTaskLoading(false) + yakExecResultEvent.handleResetData() + casualChatEvent.handleResetData() + taskChatEvent.handleResetData() + }) + + /** 需要轮询获取最新的数据请求 */ + const handleStartQuestionQueue = useMemoizedFn(() => { + // 获取最新问题队列数据 + sendMessage({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO }) + // 获取最新记忆列表数据 + sendMessage({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_MEMORY_CONTEXT }) + }) + + /** 获取历史会话事件 */ + const fetchHistoryEvents = useMemoizedFn(async (session: string) => { + const events: AIOutputEvent[] = [] + let page = 1 + + while (true) { + const { Events, Total } = await grpcQueryAIEvent( + { + Filter: { + SessionID: session, + }, + Pagination: { + Page: page, + Limit: HISTORY_EVENT_PAGE_SIZE, + OrderBy: 'id', + Order: 'asc', + }, + }, + true, + ) + + events.push(...(Events || [])) + if (!Events?.length || Events.length < HISTORY_EVENT_PAGE_SIZE || events.length >= Total) break + page++ + } + + return events + }) + + /** 保存state类型的数据 */ + const saveStateDataOfEnd = useMemoizedFn((session: string) => { + if (delChats.current.includes(session)) { + // 该session对应的会话数据实例已被删除 + delChats.current = delChats.current.filter((item) => item !== session) + return + } + + const answer: DeepPartial = { + runTimeIDs: cloneDeep(runTimeIDs), + yakExecResult: cloneDeep(yakExecResult), + casualChat: cloneDeep(casualChat), + taskChat: cloneDeep(taskChat), + grpcFolders: cloneDeep(grpcFolders), + reActTimelines: cloneDeep(reActTimelines), + } + try { + cacheDataStore?.updater(session, answer) + } catch {} + }) + + const onStart = useMemoizedFn(async (args: AIChatIPCStartParams) => { + const { token, params, extraValue } = args + + if (execute) { + yakitNotify('warning', 'useChatIPC AI任务正在执行中,请稍后再试!') + return + } + if (chatID.current !== token) { + onReset() + try { + cacheDataStore?.create(token) + } catch (error) {} + } + handleResetBeforeStart() + setExecute(true) + chatID.current = token + + aiRequest.current = omit(params.Params, ['SelectedProviderID', 'SelectedModelName', 'SelectedModelTier']) + aiRequestAll.current = params + sseEvents.connect(`run/${token}/events`) + + // console.log('start-ai-re-act', token, params) + + // 初次用户对话的问题,属于自由对话中的问题 + casualChatEvent.handleSend({ + request: { ...params, IsFreeInput: true, FreeInput: params?.Params?.UserQuery || '' }, + extraValue, + }) + }) + + const handleMessage = useMemoizedFn((res: AIOutputEvent) => { + try { + // console.log('onMessage-res', res) + // 会话SSE已建立成功并准备发送信息 + if (res.Type === 'listener_ready') { + sendFirstMessage() + } + + // 记录会话中所有的 CoordinatorId + if (res.CoordinatorId) { + updateCoordinatorIDs(res.CoordinatorId) + } + + // 如果流数据中syncID出现信息,则向UI发送该信息 + if (res.SyncID) { + onSyncIDChange?.(res.SyncID) + } + + // 记录会话中所有的RunTimeID + setRunTimeIDs((old) => { + if (!res.CallToolID || old.includes(res.CallToolID)) return old + return [...old, res.CallToolID] + }) + + let ipcContent = base64ToJson(res.Content) || '' + // let ipcStreamDelta = base64ToJson(res.StreamDelta) || '' + // console.log('onStart-res', res, ipcContent, ipcStreamDelta) + + if (res.Type === 'structured' && res.NodeId === 'session_title') { + // 生成会话的名称 + const nameInfo = JSON.parse(ipcContent) as { title: string } + if (nameInfo && nameInfo.title && !!setSessionChatName) setSessionChatName(chatID.current, nameInfo.title) + return + } + + if (res.Type === 'start_plan_and_execution') { + // 触发任务规划,并传出任务规划流的标识 coordinator_id + const startInfo = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStartPlanAndExecution + if (startInfo.coordinator_id && planCoordinatorId.current !== startInfo.coordinator_id) { + // 设置任务规划对应的问题ID, 并清除自由对话(ReAct)的loading状态 + taskChatID.current = { + taskID: startInfo['re-act_task'], + status: AITaskStatus.inProgress, + coordinatorId: startInfo.coordinator_id, // 取消任务规划需要的数据id + } + casualChatID.current -= 1 + setCasualStatus((old) => ({ ...old, loading: casualChatID.current > 0 })) + // 标记grpc流里属于任务规划的流 + planCoordinatorId.current = startInfo.coordinator_id + // 任务规划的loading开始置为true + setTaskStatus(() => ({ loading: true, plan: '加载中...', task: '加载中...' })) + } + // 触发任务规划UI展示的回调 + onTaskStart && onTaskStart() + /** 获取最新任务树状态 */ + sendMessage({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN }) + /** 恢复任务规划的时候,这个指令执行成功后,在这里取消loading */ + setCancelTaskLoading(false) + return + } + if (res.Type === 'end_plan_and_execution') { + // 结束任务规划,并传出任务规划流的标识 coordinator_id + const startInfo = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStartPlanAndExecution + if (startInfo.coordinator_id && planCoordinatorId.current === startInfo.coordinator_id) { + casualChatID.current += 1 + setCasualStatus((old) => ({ ...old, loading: casualChatID.current > 0 })) + taskChatEvent.handlePlanExecEnd(res) + taskChatEvent.handleCloseGrpc() + handleResetTaskChatLoading() + } + return + } + + if (res.Type === 'memory_context') { + // 实时记忆列表 + const lists = JSON.parse(ipcContent) as AIAgentGrpcApi.MemoryEntryList + if (planCoordinatorId.current === res.CoordinatorId) { + taskMemorys.current = lists + } else { + reactMemorys.current = lists + } + try { + const newMemoryEntryList: AIAgentGrpcApi.MemoryEntryList = { + memories: [...(taskMemorys.current.memories || []), ...(reactMemorys.current.memories || [])], + memory_pool_limit: + Number(taskMemorys.current.memory_pool_limit) + Number(reactMemorys.current.memory_pool_limit), + memory_session_id: reactMemorys.current.memory_session_id, + total_memories: Number(taskMemorys.current.total_memories) + Number(reactMemorys.current.total_memories), + total_size: Number(taskMemorys.current.total_size) + Number(reactMemorys.current.total_size), + score_overview: { + A_total: + Number(taskMemorys.current.score_overview.A_total) + + Number(reactMemorys.current.score_overview.A_total), + C_total: + Number(taskMemorys.current.score_overview.C_total) + + Number(reactMemorys.current.score_overview.C_total), + E_total: + Number(taskMemorys.current.score_overview.E_total) + + Number(reactMemorys.current.score_overview.E_total), + + O_total: + Number(taskMemorys.current.score_overview.O_total) + + Number(reactMemorys.current.score_overview.O_total), + P_total: + Number(taskMemorys.current.score_overview.P_total) + + Number(reactMemorys.current.score_overview.P_total), + R_total: + Number(taskMemorys.current.score_overview.R_total) + + Number(reactMemorys.current.score_overview.R_total), + T_total: + Number(taskMemorys.current.score_overview.T_total) + + Number(reactMemorys.current.score_overview.T_total), + }, + } + setMemoryList(newMemoryEntryList) + } catch (error) {} + + return + } + + if (['filesystem_pin_directory', 'filesystem_pin_filename'].includes(res.Type)) { + // 会话在本地缓存数据的(文件夹/文件)路径-更新就通知[不区分自由对话和任务规划] + const { path } = JSON.parse(ipcContent) as AIAgentGrpcApi.FileSystemPin + handleSetGrpcFolders({ path, isFolder: res.Type === 'filesystem_pin_directory' }) + return + } + + if (res.Type === 'structured' && ['react_task_enqueue', 'react_task_dequeue'].includes(res.NodeId)) { + // 展示只通知自由对话里的问题出入队消息 + if (planCoordinatorId.current === res.CoordinatorId) return + // 问题入队/问题出队 + handleQuestionQueueStatusChange(res) + return + } + if (res.Type === 'structured' && res.NodeId === 'react_task_cleared') { + // 展示只通知自由对话里的问题出入队消息 + if (planCoordinatorId.current === res.CoordinatorId) return + // 问题队列清空操作 + handleReActTaskCleared(res) + return + } + + if (res.Type === 'structured' && res.NodeId === 'plan_exec_tasks') { + // 任务规划历史数据列表 + const list = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanHistoryList + handlePlanHistoryListChange(list) + return + } + + if (UseAIPerfDataTypes.includes(res.Type)) { + // AI性能数据处理 + aiPerfDataEvent.handleSetData(res) + return + } + + if (UseYakExecResultTypes.includes(res.Type)) { + // 执行过程中插件输出的卡片 + yakExecResultEvent.handleSetData(res) + return + } + + if (res.Type === 'structured' && res.NodeId === 'queue_info') { + // 因为问题队列也分自由对话和任务规划队列,所以需要先屏蔽处理任务规划的队列信息 + if (planCoordinatorId.current === res.CoordinatorId) return + // 问题队列信息由chatIPC-hook进行收集 + const { tasks, total_tasks } = JSON.parse(ipcContent) as AIAgentGrpcApi.QuestionQueues + setQuestionQueue({ + total: total_tasks, + data: tasks ?? [], + }) + return + } + + if (res.Type === 'structured' && res.NodeId === 'timeline_item') { + /* 实时时间线单条 */ + const timelineItem = JSON.parse(ipcContent) as AIAgentGrpcApi.TimelineItem + setReActTimelines((old) => [...old, timelineItem]) + return + } + + if (res.Type === 'structured') { + const obj = JSON.parse(ipcContent) || '' + + if (obj?.level) { + // 执行日志信息 + const data = obj as AIAgentGrpcApi.Log + logEvents.pushLog({ + type: 'log', + Timestamp: res.Timestamp, + data: data, + }) + } else if (res.NodeId === 'timeline') { + // 一次性获取完整时间线数据, 暂无使用位置 + return + } else if (res.NodeId === 'react_task_status_changed') { + // 只负责获取自由对话的任务状态 + if (planCoordinatorId.current === res.CoordinatorId) return + /* 问题的状态变化 */ + const { react_task_id, react_task_now_status } = JSON.parse(ipcContent) as AIAgentGrpcApi.ReactTaskChanged + + if (react_task_now_status === 'processing') { + casualChatID.current += 1 + setCasualStatus((old) => ({ ...old, loading: casualChatID.current > 0 })) + } + + if (['completed', 'aborted'].includes(react_task_now_status)) { + if (focusOfTaskID.current === react_task_id) handleResetFocusMode() + casualChatID.current -= 1 + setCasualStatus((old) => ({ ...old, loading: casualChatID.current > 0 })) + if (taskChatID.current?.taskID === react_task_id) { + taskChatID.current.status = react_task_now_status as AITaskStatus + setCancelTaskLoading(false) + } + } + return + } else if (res.NodeId === 'status') { + const data = JSON.parse(ipcContent) as { key: string; value: string } + if (data.key === 're-act-loading-status-key') { + if (planCoordinatorId.current === res.CoordinatorId) { + // 任务规划-loading展示标题 + setTaskStatus((old) => { + if (old.loading) { + return { ...old, task: data.value || '加载中...' } + } + return old + }) + } else { + // 自由对话-loading展示标题 + setCasualStatus((old) => { + if (old.loading) { + return { ...old, title: data.value || 'thinking...' } + } + return old + }) + } + } else if (data.key === 'plan-executing-loading-status-key') { + if (planCoordinatorId.current === res.CoordinatorId) { + // 任务规划-loading展示标题 + setTaskStatus((old) => { + if (old.loading) { + return { ...old, plan: data.value || '加载中...' } + } + return old + }) + } + } else { + // 执行状态卡片处理 + yakExecResultEvent.handleSetData(res) + } + } else { + // 因为流数据有日志类型,所以都放入日志逻辑过滤一遍 + if (res.NodeId === 'stream-finished') { + const { event_writer_id } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStreamFinished + if (!event_writer_id) { + logEvents.pushLog(genErrorLogData(res.Timestamp, `stream-finished数据异常, event_writer_id缺失`)) + return + } + logEvents.sendStreamLog(event_writer_id) + } + + if (planCoordinatorId.current === res.CoordinatorId) { + taskChatEvent.handleSetData(res) + } else { + casualChatEvent.handleSetData(res) + } + } + return + } + + if (res.Type === 'stream') { + if (res.IsSystem || res.IsReason) { + const { CallToolID, TaskIndex, NodeId, NodeIdVerbose, EventUUID, StreamDelta, ContentType } = res + if (!NodeId || !EventUUID) return + let ipcStreamDelta = base64ToJson(StreamDelta) || '' + const content = ipcContent + ipcStreamDelta + logEvents.pushLog({ + type: 'stream', + Timestamp: res.Timestamp, + data: { + TaskIndex, + CallToolID, + NodeId, + NodeIdVerbose: NodeIdVerbose || convertNodeIdToVerbose(NodeId), + EventUUID, + status: 'start', + content: content, + ContentType, + }, + }) + + // 输出实时系统信息流 + if (res.IsSystem) handleSetSystemStream(EventUUID, content) + return + } + + if (planCoordinatorId.current === res.CoordinatorId) { + taskChatEvent.handleSetData(res) + } else { + casualChatEvent.handleSetData(res) + } + return + } + + // 自由对话和任务规划共用的类型 + if (planCoordinatorId.current === res.CoordinatorId) { + taskChatEvent.handleSetData(res) + } else { + casualChatEvent.handleSetData(res) + } + return + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: logEvents.pushLog, + }) + } + }) + + const handleHistoryMessage = useMemoizedFn((res: AIOutputEvent) => { + try { + if (['listener_ready', 'heartbeat'].includes(res.Type)) return + + if (res.CoordinatorId) { + updateCoordinatorIDs(res.CoordinatorId) + } + + setRunTimeIDs((old) => { + if (!res.CallToolID || old.includes(res.CallToolID)) return old + return [...old, res.CallToolID] + }) + + const ipcContent = base64ToJson(res.Content) || '' + + if (res.Type === 'structured' && res.NodeId === 'session_title') { + const nameInfo = JSON.parse(ipcContent) as { title: string } + if (nameInfo?.title && setSessionChatName) setSessionChatName(chatID.current, nameInfo.title) + return + } + + if (res.Type === 'start_plan_and_execution') { + const startInfo = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStartPlanAndExecution + if (startInfo.coordinator_id) { + taskChatID.current = { + taskID: startInfo['re-act_task'], + status: AITaskStatus.inProgress, + coordinatorId: startInfo.coordinator_id, + } + planCoordinatorId.current = startInfo.coordinator_id + } + onTaskStart?.() + return + } + + if (res.Type === 'end_plan_and_execution') { + const endInfo = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStartPlanAndExecution + if (!endInfo.coordinator_id || planCoordinatorId.current === endInfo.coordinator_id) { + taskChatEvent.handlePlanExecEnd(res) + } + return + } + + if (res.Type === 'memory_context') { + const lists = JSON.parse(ipcContent) as AIAgentGrpcApi.MemoryEntryList + if (planCoordinatorId.current === res.CoordinatorId) { + taskMemorys.current = lists + } else { + reactMemorys.current = lists + } + return + } + + if (['filesystem_pin_directory', 'filesystem_pin_filename'].includes(res.Type)) { + const { path } = JSON.parse(ipcContent) as AIAgentGrpcApi.FileSystemPin + handleSetGrpcFolders({ path, isFolder: res.Type === 'filesystem_pin_directory' }) + return + } + + if (res.Type === 'structured' && res.NodeId === 'plan_exec_tasks') { + const list = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanHistoryList + handlePlanHistoryListChange(list) + return + } + + if (res.Type === 'structured' && res.NodeId === 'timeline_item') { + const timelineItem = JSON.parse(ipcContent) as AIAgentGrpcApi.TimelineItem + setReActTimelines((old) => [...old, timelineItem]) + return + } + + if (UseAIPerfDataTypes.includes(res.Type)) { + aiPerfDataEvent.handleSetData(res) + return + } + + if (UseYakExecResultTypes.includes(res.Type)) { + yakExecResultEvent.handleSetData(res) + return + } + + if (res.Type === 'structured') { + const obj = JSON.parse(ipcContent) || '' + if (obj?.level) { + logEvents.pushLog({ + type: 'log', + Timestamp: res.Timestamp, + data: obj as AIAgentGrpcApi.Log, + }) + return + } + + if ( + ['queue_info', 'react_task_enqueue', 'react_task_dequeue', 'react_task_cleared'].includes(res.NodeId) || + res.NodeId === 'react_task_status_changed' + ) { + return + } + + if (res.NodeId === 'status') { + return + } + } + + if (res.Type === 'stream' && (res.IsSystem || res.IsReason)) { + const { CallToolID, TaskIndex, NodeId, NodeIdVerbose, EventUUID, StreamDelta, ContentType } = res + if (!NodeId || !EventUUID) return + const ipcStreamDelta = base64ToJson(StreamDelta) || '' + logEvents.pushLog({ + type: 'stream', + Timestamp: res.Timestamp, + data: { + TaskIndex, + CallToolID, + NodeId, + NodeIdVerbose: NodeIdVerbose || convertNodeIdToVerbose(NodeId), + EventUUID, + status: 'start', + content: ipcContent + ipcStreamDelta, + ContentType, + }, + }) + return + } + + if (planCoordinatorId.current && planCoordinatorId.current === res.CoordinatorId) { + taskChatEvent.handleSetData(res) + } else { + casualChatEvent.handleSetData(res) + } + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: logEvents.pushLog, + }) + } + }) + + const replayHistorySession = useMemoizedFn(async (session: string) => { + const events = await fetchHistoryEvents(session) + if (!events.length) return + + isHistoryReplay.current = true + try { + for (const event of events) { + handleHistoryMessage(event) + } + } finally { + isHistoryReplay.current = false + } + }) + + const handleError = useMemoizedFn((err: any) => { + // console.log('error', err) + yakitNotify('error', `AI执行失败: ${err}`) + }) + const handleEnd = useMemoizedFn(() => { + // console.log('end') + saveStateDataOfEnd(chatID.current) + handleResetGrpcStatus() + if (endAfterSession.current) { + handleSwitchSessionData(endAfterSession.current) + } + onEnd && onEnd() + }) + + const sseEvents = useEventSource('', { + maxRetries: 1, + manual: true, + isAIAgent: true, + onsuccess: handleMessage, + onerror: handleError, + onend: handleEnd, + }) + + /** 切换session会话的数据 */ + const handleSwitchSessionData = useMemoizedFn(async (session: string) => { + if (!session) { + setTimeout(() => { + setSwitchLoading(false) + }, 200) + return + } + + onReset() + + if (session === 'clear') { + setTimeout(() => { + setSwitchLoading(false) + }, 200) + endAfterSession.current = '' + return + } + + const chatData = cacheDataStore?.get(session) + if (chatData && hasCachedChatRenderData(chatData)) { + chatID.current = session + setGrpcFolders(chatData.grpcFolders || []) + setRunTimeIDs(chatData.runTimeIDs || []) + setReActTimelines(() => chatData.reActTimelines || []) + yakExecResultEvent.handleSetYakResult(chatData.yakExecResult || {}) + casualChatEvent.handleSetElements(chatData.casualChat?.elements || []) + taskChatEvent.handleSetElements(chatData.taskChat?.elements || []) + } else { + try { + if (cacheDataStore?.has(session)) cacheDataStore.remove(session) + cacheDataStore?.create(session) + } catch (error) {} + + chatID.current = session + try { + await replayHistorySession(session) + } catch (error) { + yakitNotify('error', `加载历史会话失败: ${error}`) + } + } + endAfterSession.current = '' + setTimeout(() => { + setSwitchLoading(false) + }, 200) + }) + + const [switchLoading, setSwitchLoading] = useState(false) + /** + * 标记session会话切换后,是否设置新的session + * @return clear 代表清空数据并不设置数据 + * @return session 代表清空数据并设置新session对应的数据 + */ + const endAfterSession = useRef('') + const onSwitchChat = useMemoizedFn((session?: string) => { + if (!chatID.current && execute) { + yakitNotify('warning', 'AI异常, 未记录session却处于执行状态, 请关闭AI页面重试!') + return + } + if (!chatID.current && !session) return + if (session && chatID.current && chatID.current === session) return + + setSwitchLoading(true) + if (execute) { + endAfterSession.current = session || 'clear' + // 这里使用chatID是因为session是替换chatID的新值,所以需要先取消旧session的会话 + onClose(chatID.current) + } else { + endAfterSession.current = '' + // 直接切换数据逻辑 + handleSwitchSessionData(session || 'clear') + } + }) + + const onClose = useMemoizedFn((token: string, option?: { tip: () => void }) => { + sseEvents.disconnect() + postCancelMessage(token).catch((err) => { + yakitNotify('error', `会话已关闭, 取消请求失败: ${err}`) + }) + if (option?.tip) { + option.tip() + } else { + // yakitNotify("info", "useChatIPC AI 任务已取消") + } + }) + + useInterval( + () => { + handleStartQuestionQueue() + }, + execute ? 5000 : undefined, + ) + + useEffect(() => { + return () => { + if (getExecute() && chatID.current) { + onClose(chatID.current) + } + // 多个接口流不会清空,只在页面卸载时触发清空并关闭页面 + logEvents.cancelLogsWin() + } + }, []) + + const state: UseChatIPCState = useCreation(() => { + return { + execute, + runTimeIDs, + yakExecResult, + casualChat, + taskChat, + grpcFolders, + questionQueue, + casualStatus, + reActTimelines, + memoryList, + taskStatus, + systemStream, + focusMode, + switchLoading, + planHistoryList, + cancelCasualLoading, + cancelTaskLoading, + } + }, [ + execute, + runTimeIDs, + yakExecResult, + casualChat, + taskChat, + grpcFolders, + questionQueue, + casualStatus, + reActTimelines, + memoryList, + taskStatus, + systemStream, + focusMode, + switchLoading, + planHistoryList, + cancelCasualLoading, + cancelTaskLoading, + ]) + + const event: UseChatIPCEvents = useCreation(() => { + return { + fetchToken, + fetchAIRequest, + fetchTaskChatID, + fetchChatDataStore, + onSwitchChat, + onStart, + onSend, + onClose, + onReset, + handleTaskReviewRelease, + onDelChats, + handleCancelLoadingChange, + } + }, []) + + return [state, event] as const +} + +export default useChatIPC diff --git a/src/pages/AIAgent/ai-re-act/hooks/useFileTree.ts b/src/pages/AIAgent/ai-re-act/hooks/useFileTree.ts new file mode 100644 index 00000000..2b37ee30 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/useFileTree.ts @@ -0,0 +1,563 @@ +import type { + FileNodeMapProps, + FileNodeProps, +} from '@/pages/YakRunnerAuditCode/FileTree/FileTreeType'; +import { + FileDefault, + FileSuffix, + FolderDefault, +} from '@/pages/YakRunnerAuditCode/FileTree/icon'; +import { getNameByPath, getPathParent } from '@/pages/YakRunnerAuditCode/utils'; +// import type { +// FileMonitorItemProps, +// FileMonitorProps, +// sendDuplexConn, +// } from '@/utils/duplex/duplex'; +import emiter from '@/utils/eventBus/eventBus'; +import { StringToUint8Array } from '@/utils/str'; +import { useMemoizedFn, useThrottleFn } from 'ahooks'; +import cloneDeep from 'lodash/cloneDeep'; +import type { MutableRefObject } from 'react'; +import { useEffect, useRef } from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import type { + FileMonitorItemProps, + FileMonitorProps, +} from '../../enums/external'; + +// #region useFileTree 相关定义 +export interface UseFileTreeParams { + /** 需要加载的(文件夹|文件)路径 */ + target: { path: string; isFolder: boolean }; + /** 文件树的初始化完成的回调 */ + onInitComplete?: () => void; + /** 触发文件树数据重渲染的事件 */ + onRefreshTreeData?: () => void; +} + +export interface UseFileTreeState { + treeData: MutableRefObject; + folderChildrenSet: MutableRefObject>; +} +export interface UseFileTreeEvents { + /** 主动加载指定文件夹下的子集 */ + onLoadFolderChildren: (folderPath: string) => Promise; + /** 刷新整棵文件树数据 */ + onResetTree: () => Promise; + /** 获取指定节点的详情数据 */ + getDetailMap: (key: string) => FileNodeProps | undefined; +} +// #endregion + +function useFileTree( + params: UseFileTreeParams, +): [UseFileTreeState, UseFileTreeEvents]; + +function useFileTree(params: UseFileTreeParams) { + const { target, onInitComplete, onRefreshTreeData } = params || {}; + + // 主动触发UI文件树更新 + const onTriggerUIUpdate = useThrottleFn( + () => { + onRefreshTreeData && onRefreshTreeData(); + }, + { + wait: 400, + leading: false, + }, + ).run; + + // #region 监听文件夹的改变(非软件主动操作的改变) + const watchToken = useRef(uuidv4()); + // 结束文件夹监听 + const stopWatchFolder = useMemoizedFn(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const stopData = StringToUint8Array( + JSON.stringify({ + operate: 'stop', + id: watchToken.current, + }), + ); + // sendDuplexConn({ + // Data: stopData, + // MessageType: 'file_monitor', + // Timestamp: new Date().getTime(), + // }); + }); + // 启动文件夹监听 + const startWatchFolder = useMemoizedFn((folderPath: string) => { + stopWatchFolder(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const startData = StringToUint8Array( + JSON.stringify({ + operate: 'new', + path: folderPath, + id: watchToken.current, + }), + ); + // sendDuplexConn({ + // Data: startData, + // MessageType: 'file_monitor', + // Timestamp: new Date().getTime(), + // }); + }); + // #endregion + + // #region 文件树-相关数据和方法 + // 文件树数据 + const treeData = useRef(); + // 当前path对应的子集(文件/文件夹) + const folderNodeSet = useRef>(new Set()); + + // 文件树节点详情(不包含children数据) + const nodeDetailMap = useRef>(new Map()); + const setNodeDetailMap = useMemoizedFn((node: FileNodeProps) => { + nodeDetailMap.current.set(node.path, { ...node, children: undefined }); + }); + const deleteNodeDetailMap = useMemoizedFn((path: string) => { + nodeDetailMap.current.delete(path); + }); + + // 获取指定节点的详情数据 + const getDetailMap = useMemoizedFn((key: string) => { + return nodeDetailMap.current.get(key); + }); + + // 待获取子集的文件夹列表 + const pendingFolderList = useRef([]); + + // 初始化-开始获取文件树 + const initData = useMemoizedFn(async () => { + try { + const { path, isFolder } = target || {}; + if (!path) return; + const targetName = (await getNameByPath(path)) || path; + if (!targetName) return; + + let icon = ''; + if (isFolder) { + icon = FolderDefault; + } else { + const suffix = + targetName.indexOf('.') > -1 + ? targetName.split('.').pop() + : ''; + icon = suffix ? FileSuffix[suffix] || FileDefault : FileDefault; + } + + const node: FileNodeProps = { + parent: null, + name: targetName, + path: path, + isFolder: !!isFolder, + icon: icon, + depth: 1, + isLeaf: !isFolder, + children: [], + }; + setNodeDetailMap(node); + treeData.current = node; + if (isFolder) { + pendingFolderList.current.push(path); + startPolling(); + startWatchFolder(path); + } + onInitComplete && onInitComplete(); + } catch (error) {} + }); + + /** + * 更新文件树的某个节点数据 + * 解释 + * - 这里传入的是更新节点的父节点,这样保证了节点一定在树数据中存在 + */ + const updateTreeNodeData = useMemoizedFn((newNode: FileNodeProps) => { + try { + if (!treeData.current || !newNode.path) return; + if (treeData.current.path === newNode.path) { + treeData.current = cloneDeep(newNode); + return; + } + if (!treeData.current.children?.length) return; + + // eslint-disable-next-line no-inner-declarations + function diffNode( + parent: FileNodeProps, + oldNode: FileNodeProps, + newNode: FileNodeProps, + ) { + if (newNode.path.startsWith(oldNode.path)) { + if (oldNode.path === newNode.path) { + parent.children = parent.children?.map((item) => { + if (item.path === newNode.path) { + return cloneDeep(newNode); + } + return item; + }); + } else { + if (!oldNode.children?.length) return; + for (let item of oldNode.children) { + diffNode(oldNode, item, newNode); + } + } + } + return; + } + + for (let item of treeData.current.children) { + diffNode(treeData.current, item, newNode); + } + } catch (error) {} + }); + + // 获取指定节点的父节点 + const getParentNode = useMemoizedFn((parentPath: string) => { + try { + if (!treeData.current || !parentPath) return null; + if (treeData.current.path === parentPath) + return cloneDeep(treeData.current); + if (!treeData.current.children?.length) return null; + + let nodeDetail: FileNodeMapProps | null = null; + // eslint-disable-next-line no-inner-declarations + function diffNode(nodeInfo: FileNodeProps, parentPath: string) { + if (parentPath.startsWith(nodeInfo.path)) { + if (nodeInfo.path === parentPath) { + nodeDetail = cloneDeep(nodeInfo); + return; + } else { + if (!nodeInfo.children?.length) return; + for (let item of nodeInfo.children) { + diffNode(item, parentPath); + } + } + } + return; + } + + for (let item of treeData.current.children) { + diffNode(item, parentPath); + } + return nodeDetail; + } catch (error) { + return null; + } + }); + // #endregion + + // #region 文件树数据操作方法 + // 删除文件夹 + const deleteFolder = useMemoizedFn((folderPath: string) => { + if (folderPath === treeData.current?.path) { + stopPolling(); + stopWatchFolder(); + handleReset(); + onTriggerUIUpdate(); + return; + } + const nodeInfo = nodeDetailMap.current.get(folderPath); + if (!nodeInfo || !nodeInfo.parent) return; + const parentNode = getParentNode(nodeInfo.parent); + if (!parentNode) return; + parentNode.children = parentNode.children?.filter( + (item) => item.path !== folderPath, + ); + deleteNodeDetailMap(folderPath); + parentNode.isLeaf = !parentNode.children?.length; + if (parentNode.isLeaf) setNodeDetailMap(parentNode); + folderNodeSet.current.delete(folderPath); + updateTreeNodeData(parentNode); + onTriggerUIUpdate(); + }); + // 创建文件夹 + const createFolder = useMemoizedFn(async (folderPath: string) => { + const parentPath = await getPathParent(folderPath); + const folderName = await getNameByPath(folderPath); + if (!parentPath || !folderName) return; + const parentNode = getParentNode(parentPath); + if (!parentNode) return; + const newNode: FileNodeProps = { + parent: parentPath, + name: folderName, + path: folderPath, + isFolder: true, + icon: '_fd_default', + depth: parentNode.depth + 1, + isLeaf: false, + }; + setNodeDetailMap(newNode); + pendingFolderList.current.push(folderPath); + parentNode.children = (parentNode.children || []).concat([newNode]); + parentNode.isLeaf = false; + setNodeDetailMap(parentNode); + updateTreeNodeData(parentNode); + onTriggerUIUpdate(); + }); + + // 删除文件 + const deleteFile = useMemoizedFn((filePath: string) => { + const nodeInfo = nodeDetailMap.current.get(filePath); + if (!nodeInfo || !nodeInfo.parent) return; + const parentNode = getParentNode(nodeInfo.parent); + if (!parentNode) return; + parentNode.children = parentNode.children?.filter( + (item) => item.path !== filePath, + ); + deleteNodeDetailMap(filePath); + parentNode.isLeaf = !parentNode.children?.length; + if (parentNode.isLeaf) setNodeDetailMap(parentNode); + updateTreeNodeData(parentNode); + onTriggerUIUpdate(); + }); + // 创建文件 + const createFile = useMemoizedFn(async (filePath: string) => { + const parentPath = await getPathParent(filePath); + const fileName = await getNameByPath(filePath); + if (!parentPath || !fileName) return; + const parentNode = getParentNode(parentPath); + if (!parentNode) return; + const suffix = + fileName.indexOf('.') > -1 ? fileName.split('.').pop() : ''; + const newNode: FileNodeProps = { + parent: parentPath, + name: fileName, + path: filePath, + isFolder: false, + icon: suffix ? FileSuffix[suffix] || FileDefault : FileDefault, + depth: parentNode.depth + 1, + isLeaf: true, + }; + setNodeDetailMap(newNode); + parentNode.children = (parentNode.children || []).concat([newNode]); + parentNode.isLeaf = false; + setNodeDetailMap(parentNode); + updateTreeNodeData(parentNode); + onTriggerUIUpdate(); + }); + + // 外界触发的调整文件树数据 + const onTriggerUpdateTreeData = useMemoizedFn( + async (list: FileMonitorItemProps[]) => { + for (let ev of list) { + const { Op, Path, IsDir } = ev; + + if (IsDir) { + switch (Op) { + case 'delete': + deleteFolder(Path); + break; + case 'create': + createFolder(Path); + break; + + default: + break; + } + } else { + switch (Op) { + case 'delete': + deleteFile(Path); + break; + case 'create': + createFile(Path); + break; + + default: + break; + } + } + } + }, + ); + + // 文件树结构监控 + const onUpdateFileTree = useMemoizedFn((data) => { + try { + const event: FileMonitorProps = JSON.parse(data); + if (watchToken.current !== event.Id) return; + + if (event.ChangeEvents) { + onTriggerUpdateTreeData(event.ChangeEvents); + } + if (event.CreateEvents) { + onTriggerUpdateTreeData(event.CreateEvents); + } + if (event.DeleteEvents.length > 0) { + onTriggerUpdateTreeData(event.DeleteEvents); + } + } catch (error) {} + }); + // #endregion + + // #region 循环获取文件树的每层子集 + // 正在执行的队列(最大并发为3) + const executingQueue = useRef([]); + // 循环计时器 + const pollingTimer = useRef(null); + + // 处理接口返回的子集数据并更新所有数据 + // const handleFetchChildrenResponse = useMemoizedFn( + // (parentPath: string, res: FileNodeMapProps[], cb?: () => void) => { + // try { + // const parentNode = nodeDetailMap.current.get(parentPath); + // if (!parentNode) return; + + // if (!res?.length) { + // // 需要将空文件夹节点的信息标注为叶子结点 + // const newParentNode = { ...parentNode, isLeaf: true }; + // setNodeDetailMap(newParentNode); + // updateTreeNodeData({ ...newParentNode }); + // return; + // } else { + // // 添加子节点信息 + // const childs: FileNodeProps[] = []; + // for (let item of res) { + // const childNode: FileNodeProps = { + // ...item, + // depth: parentNode.depth + 1, + // }; + // if (!item.isFolder) childNode.isLeaf = true; + // if (item.isFolder) + // pendingFolderList.current.push(item.path); + // setNodeDetailMap(childNode); + // childs.push(childNode); + // } + // // 更新树上的节点数据 + // updateTreeNodeData({ ...parentNode, children: childs }); + // } + // folderNodeSet.current.add(parentPath); + // } catch (error) { + // } finally { + // cb && cb(); + // } + // }, + // ); + + // 获取文件夹的子集 + const fetchFolderChildren = useMemoizedFn((folderPath: string) => { + executingQueue.current.push(folderPath); + // grpcFetchFileTree(folderPath) + // .then((res) => { + // handleFetchChildrenResponse(folderPath, res); + // }) + // .catch(() => { + // // pendingFolderList.current.unshift(folderPath) + // }) + // .finally(() => { + // executingQueue.current = executingQueue.current.filter( + // (item) => item !== folderPath, + // ); + // }); + }); + + // 开启轮询获取文件树 + const startPolling = useMemoizedFn(() => { + if (pollingTimer.current) return; + + if (pendingFolderList.current.length > 0) { + const folderPath = pendingFolderList.current.shift()!; + fetchFolderChildren(folderPath); + } + + pollingTimer.current = setInterval(() => { + if (pendingFolderList.current.length === 0) return; + if (executingQueue.current.length >= 3) return; + const availableSlots = 3 - executingQueue.current.length; + // 将数组前三个裁剪出来 + const foldersToFetch = pendingFolderList.current.splice( + 0, + availableSlots, + ); + foldersToFetch.forEach((folderPath) => { + if (!folderPath) return; + fetchFolderChildren(folderPath); + }); + }, 1000); + }); + + // 停止轮询获取文件树 + const stopPolling = useMemoizedFn(() => { + if (pollingTimer.current) { + clearInterval(pollingTimer.current); + pollingTimer.current = null; + } + }); + // #endregion + + // 主动加载指定文件夹的子集 + const loadFolderChildren = useMemoizedFn((folderPath: string) => { + return new Promise((resolve, reject) => { + try { + if (!folderPath) { + // eslint-disable-next-line prefer-promise-reject-errors + reject('path is empty'); + return; + } + if (pendingFolderList.current.includes(folderPath)) { + pendingFolderList.current = + pendingFolderList.current.filter( + (item) => item !== folderPath, + ); + } + + executingQueue.current.push(`${folderPath}-trigger`); + // grpcFetchFileTree(folderPath) + // .then((res) => { + // handleFetchChildrenResponse(folderPath, res, () => { + // resolve(); + // }); + // }) + // .catch((err) => { + // reject(err); + // }) + // .finally(() => { + // executingQueue.current = executingQueue.current.filter( + // (item) => item !== `${folderPath}-trigger`, + // ); + // }); + } catch (error) { + reject(error); + } + }); + }); + + // 重置 + const handleReset = useMemoizedFn(() => { + treeData.current = undefined; + folderNodeSet.current.clear(); + nodeDetailMap.current.clear(); + pendingFolderList.current = []; + executingQueue.current = []; + }); + + // 刷新整棵文件树 + const onResetTree = useMemoizedFn(async () => { + return new Promise((resolve) => { + stopPolling(); + handleReset(); + initData(); + resolve(); + }); + }); + + useEffect(() => { + initData(); + + // 文件树结构监控(监听非软件操作变化) + emiter.on('onRefreshYakRunnerFileTree', onUpdateFileTree); + + return () => { + emiter.off('onRefreshYakRunnerFileTree', onUpdateFileTree); + stopPolling(); + stopWatchFolder(); + handleReset(); + }; + }, []); + + return [ + { treeData: treeData, folderChildrenSet: folderNodeSet }, + { onLoadFolderChildren: loadFolderChildren, onResetTree, getDetailMap }, + ]; +} + +export default useFileTree; diff --git a/src/pages/AIAgent/ai-re-act/hooks/useHistoryChat.ts b/src/pages/AIAgent/ai-re-act/hooks/useHistoryChat.ts new file mode 100644 index 00000000..89de6b1c --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/useHistoryChat.ts @@ -0,0 +1,6 @@ +function useHistoryChat(params?: any): any; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function useHistoryChat(params?: any) {} + +export default useHistoryChat; diff --git a/src/pages/AIAgent/ai-re-act/hooks/useTaskChat.ts b/src/pages/AIAgent/ai-re-act/hooks/useTaskChat.ts new file mode 100644 index 00000000..ea2f0944 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/useTaskChat.ts @@ -0,0 +1,676 @@ +import { useRef, useState } from 'react' +import { useCreation, useMemoizedFn } from 'ahooks' +import cloneDeep from 'lodash/cloneDeep' +import { AIReviewJudgeLevelMap } from './defaultConstant' +import type { AIChatLogData, handleSendFunc, UseTaskChatEvents, UseTaskChatParams, UseTaskChatState } from './type' +import { + base64ToJson, + genBaseAIChatData, + genErrorLogData, + genExecTasks, + handleGrpcDataPushLog, + isAutoExecuteReviewContinue, +} from './utils' +import { yakitNotify } from '@/utils/notification' +import type { AIAgentGrpcApi, AIOutputEvent } from './grpcApi' +import { AIInputEventSyncTypeEnum, AITaskStatus } from './grpcApi' +import type { AIChatQSData, AIReviewType, AITaskInfoProps, ReActChatRenderItem } from './aiRender' +import { AIChatQSDataTypeEnum } from './aiRender' +import useChatContent from './useChatContent' +import { has } from 'lodash' +import { useGetSetState } from '@/hooks' + +function useTaskChat(params?: UseTaskChatParams): [UseTaskChatState, UseTaskChatEvents] + +function useTaskChat(params?: UseTaskChatParams) { + const { + pushLog, + getChatDataStore, + getRequest, + isHistoryReplay, + onReview, + onReviewExtra, + onReviewRelease, + sendRequest, + } = params || {} + + const handlePushLog = useMemoizedFn((logInfo: AIChatLogData) => { + pushLog && pushLog(logInfo) + }) + + const [elements, setElements, getElements] = useGetSetState([]) + const handleSetElements = useMemoizedFn((newElements: ReActChatRenderItem[]) => { + setElements(newElements) + }) + + const getContentMap = useMemoizedFn((mapKey: string) => { + const contentMap = getChatDataStore?.()?.taskChat?.contents + if (!contentMap) return undefined + return contentMap.get(mapKey) + }) + const setContentMap = useMemoizedFn((mapKey: string, value: AIChatQSData) => { + const contentMap = getChatDataStore?.()?.taskChat?.contents + contentMap && contentMap.set(mapKey, value) + }) + const deleteContentMap = useMemoizedFn((mapKey: string) => { + const contentMap = getChatDataStore?.()?.taskChat?.contents + contentMap && contentMap.delete(mapKey) + }) + + // #region 任务树相关逻辑 + // plan_review 原始树结构 + const planTree = useRef() + /** 树结构的任务树 */ + const fetchPlanTree = useMemoizedFn(() => { + return cloneDeep(planTree.current) + }) + const [plan, setPlan] = useState([]) + const handleResetPlanTree = useMemoizedFn(() => { + planTree.current = undefined + setPlan([]) + }) + + /** 正在执行中的叶子任务的mapKey集合(已结束的叶子任务会被移除) */ + const activeLeafTasks = useRef>(new Set()) + const handleResetActiveLeafTasks = useMemoizedFn(() => { + activeLeafTasks.current.clear() + }) + + /** 任务节点开始执行, 生成UI展示的信息 */ + const handleTaskStartNode = useMemoizedFn((res: AIOutputEvent, nodeInfo: AIAgentGrpcApi.ChangeTask) => { + try { + // 任务树根节点不进行节点展示 + if (nodeInfo.task.index === '1') return + + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: 'task', + type: AIChatQSDataTypeEnum.TASK_INDEX_NODE, + data: { + taskIndex: nodeInfo.task.index, + taskName: nodeInfo.task.name, + goal: nodeInfo.task.goal, + status: AITaskStatus.inProgress, + }, + } + if (nodeInfo.task.task_uuid) chatData.id = nodeInfo.task.task_uuid + activeLeafTasks.current.add(chatData.id) + setContentMap(chatData.id, chatData) + setElements((old) => [...old, { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'task' }]) + } catch (error) {} + }) + /** 任务节点结束执行, 更新UI展示的信息 */ + const handleTaskEndNode = useMemoizedFn((nodeInfo: AIAgentGrpcApi.ChangeTask) => { + try { + // 任务树根节点不进行节点展示 + if (nodeInfo.task.index === '1') return + // 任务结束时, 如果没有task_uuid则不进行UI更新, 因为无法确定哪个节点结束了 + if (!nodeInfo.task.task_uuid) return + + // 删除正在执行队列里的叶子任务, 因为当前任务已经结束了 + activeLeafTasks.current.delete(nodeInfo.task.task_uuid) + + const taskNodeInfo = getContentMap(nodeInfo.task.task_uuid) + if (!taskNodeInfo || taskNodeInfo.type !== AIChatQSDataTypeEnum.TASK_INDEX_NODE) { + return + } + taskNodeInfo.data.status = nodeInfo.task.task_status + setElements((old) => { + return old.map((item) => { + if (item.token === taskNodeInfo.id && item.type === taskNodeInfo.type) { + return { ...item, renderNum: item.renderNum + 1 } + } + return item + }) + }) + } catch (error) {} + }) + + /** 更新任务树指定任务节点的状态 */ + const handleUpdateTaskState = useMemoizedFn((index: string, state: AITaskInfoProps['progress']) => { + setPlan((old) => { + return old.map((item) => { + if (item.index === index) item.progress = state + return item + }) + }) + }) + + /** 将任务树中, 所有进行中的任务, 变更为中止状态 */ + const handleAbortTaskState = useMemoizedFn(() => { + const leafTasks = activeLeafTasks.current + for (let mapKey of leafTasks) { + const taskNodeInfo = getContentMap(mapKey) + if (!taskNodeInfo || taskNodeInfo.type !== AIChatQSDataTypeEnum.TASK_INDEX_NODE) { + continue + } + taskNodeInfo.data.status = AITaskStatus.error + setElements((old) => { + return old.map((item) => { + if (item.token === taskNodeInfo.id && item.type === taskNodeInfo.type) { + return { ...item, renderNum: item.renderNum + 1 } + } + return item + }) + }) + } + handleResetActiveLeafTasks() + setPlan((old) => { + const newData = cloneDeep(old) + return newData.map((item) => { + if (item.progress === AITaskStatus.inProgress) { + item.progress = AITaskStatus.error + } + return item + }) + }) + }) + // #endregion + + // #region review事件转换成UI处理逻辑 + const review = useRef() + /** 记录plan_review补充信息的唯一ID */ + const currentPlansId = useRef('') + const handleResetReview = useMemoizedFn(() => { + review.current = undefined + currentPlansId.current = '' + }) + + // 将 review 数据处理成需要展示的UI数据 + const handleRviewDataToUI = useMemoizedFn((reviewInfo: AIChatQSData) => { + if (reviewInfo.type === AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE && reviewInfo.data.optionValue === 'continue') { + // plan_review, 选择是continue选项, 则进行UI任务树的生成 + const tasks = reviewInfo.data + planTree.current = cloneDeep(tasks.plans.root_task) + const plans = genExecTasks(tasks.plans.root_task) + setPlan(cloneDeep(plans)) + } + }) + + /** plan_review */ + const handlePlanReview = useMemoizedFn((res: AIOutputEvent) => { + try { + const ipcContent = base64ToJson(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanReviewRequire + + if (!data?.id || !data?.plans || !data?.plans?.root_task || !data?.selectors || !data?.selectors?.length) { + handlePushLog( + genErrorLogData( + res.Timestamp, + `${res.Type}数据异常: id:${data?.id || '-'}; selectors:${JSON.stringify( + data?.selectors || '-', + )}; plans:${data?.plans?.root_task ? 'valid' : 'invalid'} data`, + ), + ) + return + } + + const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: getRequest }) + const isHistory = !!isHistoryReplay?.() + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: 'task', + id: data.id, + type: AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE, + data: { + ...cloneDeep(data), + selected: isAuto ? JSON.stringify({ suggestion: 'continue' }) : undefined, + optionValue: isAuto ? 'continue' : undefined, + }, + } + review.current = isAuto ? undefined : chatData + if (isAuto || isHistory) { + setContentMap(chatData.id, cloneDeep(chatData)) + setElements((old) => [...old, { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'task' }]) + if (isAuto) handleRviewDataToUI(chatData) + } else { + onReview && onReview(cloneDeep(chatData)) + } + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: handlePushLog, + }) + } + }) + /** plan_review 的补充数据 */ + const handlePlanReviewExtra = useMemoizedFn((res: AIOutputEvent) => { + try { + if (!review.current || review.current.type !== AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE) { + handlePushLog(genErrorLogData(res.Timestamp, `${res.Type}数据异常: 未找到对应plan_review_require数据`)) + return + } + + const ipcContent = base64ToJson(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanReviewRequireExtra + if ( + !data?.plans_id || + !data?.index || + !data?.keywords?.length || + (currentPlansId.current && currentPlansId.current !== data.plans_id) + ) { + handlePushLog( + genErrorLogData( + res.Timestamp, + `${res.Type}数据异常: plans_id:${data?.plans_id || '-'};index:${ + data?.index || '-' + };keywords:${JSON.stringify(data?.keywords || '-')}`, + ), + ) + return + } + + if (!currentPlansId.current) currentPlansId.current = data.plans_id + const reviewInfo = review.current.data + if (!reviewInfo.taskExtra) reviewInfo.taskExtra = new Map() + reviewInfo.taskExtra.set(data.index, data) + + const isAuto = isAutoExecuteReviewContinue({ getFunc: getRequest }) + if (!isAuto && onReviewExtra) onReviewExtra(cloneDeep(data)) + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: handlePushLog, + }) + } + }) + // tool_review + const handleToolReview = useMemoizedFn((res: AIOutputEvent) => { + try { + const ipcContent = base64ToJson(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ToolUseReviewRequire + if (!data?.id || !data?.selectors || !data?.selectors?.length) { + handlePushLog( + genErrorLogData( + res.Timestamp, + `${res.Type}数据异常: id:${data?.id || '-'}; selectors:${JSON.stringify(data?.selectors || '-')}`, + ), + ) + return + } + + const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: getRequest }) + const isHistory = !!isHistoryReplay?.() + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: 'task', + id: data.id, + type: AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE, + data: { + ...cloneDeep(data), + selected: isAuto ? JSON.stringify({ suggestion: 'continue' }) : undefined, + optionValue: isAuto ? 'continue' : undefined, + }, + } + review.current = isAuto ? undefined : chatData + if (isAuto || isHistory) { + setContentMap(chatData.id, cloneDeep(chatData)) + setElements((old) => [...old, { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'task' }]) + } else { + onReview && onReview(cloneDeep(chatData)) + } + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: handlePushLog, + }) + } + }) + // task_review + const handleTaskReview = useMemoizedFn((res: AIOutputEvent) => { + try { + const ipcContent = base64ToJson(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.TaskReviewRequire + if (!data?.id || !data?.selectors || !data?.selectors?.length) { + handlePushLog( + genErrorLogData( + res.Timestamp, + `${res.Type}数据异常: id:${data?.id || '-'}; selectors:${JSON.stringify(data?.selectors || '-')}`, + ), + ) + return + } + + const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: getRequest }) + const isHistory = !!isHistoryReplay?.() + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: 'task', + id: data.id, + type: AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE, + data: { + ...cloneDeep(data), + selected: isAuto ? JSON.stringify({ suggestion: 'continue' }) : undefined, + optionValue: isAuto ? 'continue' : undefined, + }, + } + review.current = isAuto ? undefined : chatData + if (isAuto || isHistory) { + setContentMap(chatData.id, cloneDeep(chatData)) + setElements((old) => [...old, { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'task' }]) + } else { + onReview && onReview(cloneDeep(chatData)) + } + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: handlePushLog, + }) + } + }) + + // AI人机交互的review事件处理(require_user_interactive) + const handleUserRequireReview = useMemoizedFn((res: AIOutputEvent) => { + try { + const ipcContent = base64ToJson(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.AIReviewRequire + if (!data?.id) { + handlePushLog(genErrorLogData(res.Timestamp, `${res.Type}数据异常: id:${data?.id || '-'}`)) + return + } + + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: 'task', + id: data.id, + type: AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE, + data: cloneDeep(data), + } + review.current = chatData + if (isHistoryReplay?.()) { + setContentMap(chatData.id, cloneDeep(chatData)) + setElements((old) => [...old, { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'task' }]) + } else { + onReview && onReview(cloneDeep(chatData)) + } + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: handlePushLog, + }) + } + }) + + // 处理 tool_review和forge_view 的 ai 判断得分事件 + const handleReviewJudgement = useMemoizedFn((res: AIOutputEvent) => { + try { + const ipcContent = base64ToJson(res.Content) || '' + const score = JSON.parse(ipcContent) as AIAgentGrpcApi.AIReviewJudgement + + if (!score?.interactive_id) { + handlePushLog( + genErrorLogData(res.Timestamp, `${res.Type}数据异常: interactive_id:${score?.interactive_id || '-'}`), + ) + return + } + if ( + !review.current || + review.current.type !== AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE || + review.current.data.id !== score.interactive_id + ) { + handlePushLog( + genErrorLogData( + res.Timestamp, + `${res.Type}数据异常(interactive_id:${score?.interactive_id || '-'})未找到对应review`, + ), + ) + return + } + + score.levelLabel = AIReviewJudgeLevelMap[score?.level || '']?.label || undefined + const info = review.current.data + if (!info.aiReview || (info.aiReview && typeof info.aiReview.seconds === 'undefined')) { + // aiReview 没有或者 aiReview 的 seconds 为空时可以赋值 + info.aiReview = cloneDeep(score) + review.current.data = cloneDeep(info) + } + const isAuto = isAutoExecuteReviewContinue({ getFunc: getRequest }) + if (!isAuto) { + if (isHistoryReplay?.()) { + const currentReview = cloneDeep(review.current) + if (!currentReview) return + setContentMap(currentReview.id, currentReview) + setElements((old) => { + const index = old.findIndex((item) => item.token === currentReview.id && item.type === currentReview.type) + if (index === -1) return old + const next = [...old] + next[index] = { ...next[index], renderNum: next[index].renderNum + 1 } + return next + }) + } else if (onReview) { + onReview(cloneDeep(review.current)) + } + } + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: handlePushLog, + }) + } + }) + + // 释放当前review信息 + const handleReviewRelease = useMemoizedFn((res: AIOutputEvent) => { + try { + if (!review.current) return + + const ipcContent = base64ToJson(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ReviewRelease + if (!data?.id) { + handlePushLog(genErrorLogData(res.Timestamp, `${res.Type}数据异常: id:${data?.id || '-'}`)) + return + } + + const info = cloneDeep(review.current.data) as AIReviewType + if (info?.id !== data.id) { + handlePushLog( + genErrorLogData( + res.Timestamp, + `${res.Type}数据(id:${data?.id || '-'})和当前展示review数据(id:${info?.id || '-'})不匹配`, + ), + ) + return + } + + info.selected = JSON.stringify({ suggestion: 'continue' }) + info.optionValue = 'continue' + const chatData: AIChatQSData = { + ...review.current, + data: info as any, + } + handleResetReview() + handleRviewDataToUI(chatData) + setContentMap(chatData.id, chatData) + setElements((old) => { + return old.map((item) => { + if (item.token === chatData.id && item.type === chatData.type) { + return { ...item, renderNum: item.renderNum + 1 } + } + return item + }) + }) + + const isAuto = isAutoExecuteReviewContinue({ type: chatData.type, getFunc: getRequest }) + if (!isAuto && onReviewRelease) onReviewRelease(data.id) + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: handlePushLog, + }) + } + }) + // #endregion + + // 处理专属任务规划的特殊数据流 + const handleSpecialData = useMemoizedFn((res: AIOutputEvent) => { + try { + let ipcContent = base64ToJson(res.Content) || '' + + if (res.Type === 'structured' && res.NodeId === 'system') { + const data = JSON.parse(ipcContent) || '' + + if (!!data && typeof data === 'object' && data?.type === 'push_task') { + const info = JSON.parse(ipcContent) as AIAgentGrpcApi.ChangeTask + handleTaskStartNode(res, info) + handleUpdateTaskState(info.task.index, AITaskStatus.inProgress) + } + + if (!!data && typeof data === 'object' && data?.type === 'pop_task') { + // 结束任务 & 请求更新任务树最新状态数据 + const info = JSON.parse(ipcContent) as AIAgentGrpcApi.ChangeTask + handleTaskEndNode(info) + if (!isHistoryReplay?.()) { + sendRequest && sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN }) + } + } + return + } + + if (res.Type === 'plan_review_require') { + handlePlanReview(res) + return + } + if (res.Type === 'plan_task_analysis') { + handlePlanReviewExtra(res) + return + } + if (res.Type === 'tool_use_review_require') { + handleToolReview(res) + return + } + if (res.Type === 'task_review_require') { + handleTaskReview(res) + return + } + if (res.Type === 'require_user_interactive') { + handleUserRequireReview(res) + return + } + + if (['ai_review_start', 'ai_review_countdown', 'ai_review_end'].includes(res.Type)) { + handleReviewJudgement(res) + return + } + + if (res.Type === 'review_release') { + // review释放通知 + handleReviewRelease(res) + return + } + + if (res.Type === 'plan') { + // 更新正在执行的任务树 + const tasks = JSON.parse(ipcContent) as { root_task: AIAgentGrpcApi.PlanTask } + if (has(tasks, 'root_task')) { + planTree.current = cloneDeep(tasks.root_task) + const plans = genExecTasks(tasks.root_task) + setPlan(cloneDeep(plans)) + } else { + setPlan([]) + } + + return + } + + // 未识别类型全部归档到日志处理 + handleGrpcDataPushLog({ info: res, pushLog: handlePushLog }) + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: handlePushLog, + }) + } + }) + + const chatContentEvent = useChatContent({ + chatType: 'task', + getContentMap, + setContentMap, + deleteContentMap, + setElements, + getElements, + pushLog: handlePushLog, + handleUnkData: handleSpecialData, + }) + // #endregion + + /** 处理数据方法 */ + const handleSetData = useMemoizedFn((res: AIOutputEvent) => { + try { + chatContentEvent.handleSetData(res) + } catch (error) { + handleGrpcDataPushLog({ + info: res, + pushLog: handlePushLog, + }) + } + }) + + const handleResetData = useMemoizedFn(() => { + handleResetPlanTree() + handleResetActiveLeafTasks() + handleResetReview() + chatContentEvent.handleResetData() + setElements([]) + }) + + /** review 界面选项触发事件 */ + const handleSend: handleSendFunc = useMemoizedFn(({ request, optionValue, cb }) => { + try { + const { InteractiveId, InteractiveJSONInput } = request + if (!review.current || review.current.id !== InteractiveId) { + yakitNotify('error', '未获取到 review 信息, 操作无效') + return + } + + const chatData = cloneDeep(review.current) + ;(chatData.data as AIReviewType).selected = InteractiveJSONInput || '' + ;(chatData.data as AIReviewType).optionValue = optionValue + + handleResetReview() + handleRviewDataToUI(chatData) + setContentMap(chatData.id, chatData) + setElements((old) => old.concat({ token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'task' })) + + cb && cb() + } catch (error) {} + }) + + /** grpc接口关闭后的后续处理逻辑 */ + const handleCloseGrpc = useMemoizedFn(() => { + // 将进行中的任务变更为中止状态 + handleAbortTaskState() + }) + + /** 当前任务规划结束-触发UI展示结束标识 */ + const handlePlanExecEnd = useMemoizedFn((res: AIOutputEvent) => { + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: 'task', + type: AIChatQSDataTypeEnum.END_PLAN_AND_EXECUTION, + data: '', + } + setContentMap(chatData.id, chatData) + setElements((old) => [...old, { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'task' }]) + }) + + const state: UseTaskChatState = useCreation(() => { + return { plan, elements } + }, [plan, elements]) + + const events: UseTaskChatEvents = useCreation(() => { + return { + fetchPlanTree, + handleSetData, + handleResetData, + handleSend, + handleCloseGrpc, + handlePlanExecEnd, + handleSetElements, + } + }, []) + + return [state, events] as const +} + +export default useTaskChat diff --git a/src/pages/AIAgent/ai-re-act/hooks/useVirtuosoAutoScroll.ts b/src/pages/AIAgent/ai-re-act/hooks/useVirtuosoAutoScroll.ts new file mode 100644 index 00000000..07ed3a63 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/useVirtuosoAutoScroll.ts @@ -0,0 +1,205 @@ +import { useMemoizedFn, useMount, useThrottleFn } from 'ahooks'; +import { useRef } from 'react'; +import type { VirtuosoHandle } from 'react-virtuoso'; + +interface UseVirtuosoAutoScrollOptions { + atBottomThreshold?: number; + total?: number; +} + +const useVirtuosoAutoScroll = (options: UseVirtuosoAutoScrollOptions) => { + const { atBottomThreshold = 80, total } = options; + const virtuosoRef = useRef(null); + const isAtBottomRef = useRef(true); + const scrollerRef = useRef(null); + const isUserDraggingRef = useRef(false); // 用户是否正在拖动滚动条 + + // 检查是否接近底部 + const checkIsAtBottom = useMemoizedFn(() => { + const scroller = scrollerRef.current; + if (scroller instanceof HTMLElement) { + const distanceToBottom = + scroller.scrollHeight - + scroller.scrollTop - + scroller.clientHeight; + return distanceToBottom <= atBottomThreshold; + } + return true; + }); + + // wheel 事件处理 + const onWheel = useMemoizedFn((e: WheelEvent) => { + if (e.deltaY < 0) { + // 往上滑动,停止自动滚动 + isAtBottomRef.current = false; + } else if (e.deltaY > 0) { + // 往下滑动,检查是否接近底部 + const target = e.currentTarget as HTMLElement; + const distanceToBottom = + target.scrollHeight - target.scrollTop - target.clientHeight; + if (distanceToBottom <= atBottomThreshold) { + isAtBottomRef.current = true; + } + } + }); + + // scroll 事件处理(只在用户拖动滚动条时更新状态) + const { run: onScroll } = useThrottleFn( + () => { + if (isUserDraggingRef.current) { + isAtBottomRef.current = checkIsAtBottom(); + } + }, + { wait: 50 }, + ); + + // 鼠标按下(检测是否点击在滚动条区域) + const onMouseDown = useMemoizedFn((e: MouseEvent) => { + const target = e.currentTarget as HTMLElement; + const scrollbarWidth = target.offsetWidth - target.clientWidth; + // 点击位置在滚动条区域 + if (scrollbarWidth > 0 && e.offsetX >= target.clientWidth) { + isUserDraggingRef.current = true; + } + }); + + const onMouseUp = useMemoizedFn(() => { + if (isUserDraggingRef.current) { + isUserDraggingRef.current = false; + isAtBottomRef.current = checkIsAtBottom(); + } + }); + + // 键盘事件处理 + const onKeyDown = useMemoizedFn((e: KeyboardEvent) => { + if (e.key === 'PageDown' || e.key === 'End' || e.key === 'ArrowDown') { + // 延迟检查,等滚动完成 + setTimeout(() => { + isAtBottomRef.current = checkIsAtBottom(); + }, 50); + } else if ( + e.key === 'PageUp' || + e.key === 'Home' || + e.key === 'ArrowUp' + ) { + isAtBottomRef.current = false; + } + }); + + // 触摸事件处理 + const touchStartY = useRef(0); + const onTouchStart = useMemoizedFn((e: TouchEvent) => { + touchStartY.current = e.touches[0].clientY; + }); + + const onTouchMove = useMemoizedFn((e: TouchEvent) => { + const deltaY = touchStartY.current - e.touches[0].clientY; + if (deltaY < 0) { + isAtBottomRef.current = false; + } else if (deltaY > 0) { + isAtBottomRef.current = checkIsAtBottom(); + } + touchStartY.current = e.touches[0].clientY; + }); + + const setScrollerRef = useMemoizedFn((ref: HTMLElement | Window | null) => { + // 解绑旧的事件 + if (scrollerRef.current && scrollerRef.current instanceof HTMLElement) { + scrollerRef.current.removeEventListener( + 'wheel', + onWheel as EventListener, + ); + scrollerRef.current.removeEventListener('scroll', onScroll); + scrollerRef.current.removeEventListener('mousedown', onMouseDown); + scrollerRef.current.removeEventListener('keydown', onKeyDown); + scrollerRef.current.removeEventListener('touchstart', onTouchStart); + scrollerRef.current.removeEventListener('touchmove', onTouchMove); + document.removeEventListener('mouseup', onMouseUp); + } + scrollerRef.current = ref; + // 绑定新的事件 + if (ref && ref instanceof HTMLElement) { + ref.addEventListener('wheel', onWheel as EventListener, { + passive: true, + }); + ref.addEventListener('scroll', onScroll, { passive: true }); + ref.addEventListener('mousedown', onMouseDown); + ref.addEventListener('keydown', onKeyDown); + ref.addEventListener('touchstart', onTouchStart, { passive: true }); + ref.addEventListener('touchmove', onTouchMove, { passive: true }); + document.addEventListener('mouseup', onMouseUp); + } + }); + + // 组件卸载时清理事件 + useMount(() => { + return () => { + if ( + scrollerRef.current && + scrollerRef.current instanceof HTMLElement + ) { + scrollerRef.current.removeEventListener( + 'wheel', + onWheel as EventListener, + ); + scrollerRef.current.removeEventListener('scroll', onScroll); + scrollerRef.current.removeEventListener( + 'mousedown', + onMouseDown, + ); + scrollerRef.current.removeEventListener('keydown', onKeyDown); + scrollerRef.current.removeEventListener( + 'touchstart', + onTouchStart, + ); + scrollerRef.current.removeEventListener( + 'touchmove', + onTouchMove, + ); + } + document.removeEventListener('mouseup', onMouseUp); + }; + }); + + const scrollToIndex = useMemoizedFn( + (index: 'LAST' | number, behavior?: 'auto' | 'smooth') => { + if (index === 'LAST' || index === total! - 1) { + isAtBottomRef.current = true; + } + requestIdleCallback(() => { + virtuosoRef.current?.scrollToIndex({ + index, + align: 'end', + behavior: behavior || 'smooth', + }); + }); + }, + ); + + const smartScrollToBottom = () => { + requestAnimationFrame(() => { + const scroller = scrollerRef.current; + if (scroller instanceof HTMLElement) { + scroller.scrollTop = scroller.scrollHeight; + } + }); + }; + + const { run: handleTotalListHeightChanged } = useThrottleFn( + () => { + if (isAtBottomRef.current && total && total > 0) { + smartScrollToBottom(); + } + }, + { wait: 150 }, + ); + + return { + virtuosoRef, + setScrollerRef, + scrollToIndex, + handleTotalListHeightChanged, + }; +}; + +export default useVirtuosoAutoScroll; diff --git a/src/pages/AIAgent/ai-re-act/hooks/useYakExecResult.ts b/src/pages/AIAgent/ai-re-act/hooks/useYakExecResult.ts new file mode 100644 index 00000000..6a8872b1 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/useYakExecResult.ts @@ -0,0 +1,215 @@ +import { useRef, useState } from 'react'; +import { useCreation, useMemoizedFn } from 'ahooks'; +import { + checkStreamValidity, + convertCardInfo, +} from '@/hook/useHoldGRPCStream/useHoldGRPCStream'; +import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType'; +import type { + AIChatLogData, + UseYakExecResultEvents, + UseYakExecResultParams, + UseYakExecResultState, +} from './type'; +import { base64ToJson, handleGrpcDataPushLog } from './utils'; +import { v4 as uuidv4 } from 'uuid'; +import type { AIAgentGrpcApi, AIOutputEvent } from './grpcApi'; +import type { AIYakExecFileRecord } from './aiRender'; +import { useThrottleState } from '@/hooks'; + +// 属于该 hook 处理数据的类型 +export const UseYakExecResultTypes = ['yak_exec_result']; + +function useYakExecResult( + params?: UseYakExecResultParams, +): [UseYakExecResultState, UseYakExecResultEvents]; + +function useYakExecResult(params?: UseYakExecResultParams) { + const handlePushLog = useMemoizedFn((log: AIChatLogData) => { + if (params?.pushLog) { + params.pushLog(log); + } + }); + + // card + const cardKVPair = useRef>( + new Map(), + ); + const cardTimeRef = useRef(null); + const [card, setCard] = useState([]); + const [yakExecResultLogs, setYakExecResultLogs] = useState< + StreamResult.Log[] + >([]); // log + + const execFileRecordOrder = useRef(1); + /** 插件执行过程中的文件操作记录 */ + const [execFileRecord, setExecFileRecord] = useThrottleState< + Map + >(new Map(), { + wait: 2000, + }); + const updateExecFileRecord = useMemoizedFn( + (CallToolID: string, info: StreamResult.Log) => { + try { + setExecFileRecord((old) => { + const newMap = new Map(old); + const keyName = CallToolID || 'system'; + const record = newMap.get(keyName) || []; + record.push({ + ...info, + id: uuidv4(), + order: execFileRecordOrder.current++, + }); + newMap.set(keyName, [...record]); + return newMap; + }); + } catch (error) {} + }, + ); + + const onHandleCard = useMemoizedFn( + (value: AIAgentGrpcApi.AICardMessage) => { + const logData = value.content as StreamResult.Log; + const checkInfo: AIAgentGrpcApi.AICard = checkStreamValidity( + value.content as StreamResult.Log, + ); + if (!checkInfo) return; + const { id, data, tags } = checkInfo; + const { timestamp } = logData; + const originData = cardKVPair.current.get(id); + if (originData && originData.Timestamp > timestamp) { + return; + } + cardKVPair.current.set(id, { + Id: id, + Data: data, + Timestamp: timestamp, + Tags: Array.isArray(tags) ? tags : [], + }); + onSetCard(); + }, + ); + const onSetCard = useMemoizedFn(() => { + if (cardTimeRef.current) return; + cardTimeRef.current = setTimeout(() => { + const cacheCard: AIAgentGrpcApi.AIInfoCard[] = convertCardInfo( + cardKVPair.current, + ); + setCard(() => [...cacheCard]); + cardTimeRef.current = null; + }, 500); + }); + + /** + * @description 该方法可以记录yak_exec_result中所有的日志,根但是目前只对接level:file;后续据可需求更改 + */ + // const onHandleYakExecResultLogs = useMemoizedFn( + // (obj: AIAgentGrpcApi.AICardMessage) => { + // const log = obj.content as StreamResult.Log; + // setYakExecResultLogs((perLog) => [ + // ...perLog, + // { ...log, id: uuidv4() }, + // ]); + // }, + // ); + + const onHandleYakExecResult = useMemoizedFn( + (CallToolID: string, value: AIAgentGrpcApi.AIPluginExecResult) => { + try { + if (!value?.IsMessage) return; + const message = value?.Message || ''; + const obj: AIAgentGrpcApi.AICardMessage = JSON.parse( + Buffer.from(message, 'base64').toString('utf8'), + ); + + if (obj.type !== 'log') return; + const content = obj.content as StreamResult.Log; + switch (content.level) { + case 'feature-status-card-data': + onHandleCard(obj); + break; + case 'file': + updateExecFileRecord(CallToolID, content); + break; + default: + break; + } + } catch (error) {} + }, + ); + + /** AI执行过程中的status状态卡片化处理 */ + const onAIStatusHandleCard = useMemoizedFn( + (info: { key: string; value: string; timestamp: number }) => { + const { key, value, timestamp } = info; + const originData = cardKVPair.current.get(key); + if (originData && originData.Timestamp > timestamp) { + return; + } + cardKVPair.current.set(key, { + Id: key, + Data: value, + Timestamp: timestamp, + Tags: [], + }); + onSetCard(); + }, + ); + + const handleSetData = useMemoizedFn((res: AIOutputEvent) => { + try { + let ipcContent = base64ToJson(res.Content) || ''; + + if (res.Type === 'yak_exec_result') { + const data = JSON.parse( + ipcContent, + ) as AIAgentGrpcApi.AIPluginExecResult; + onHandleYakExecResult(res.CallToolID || '', data); + return; + } + if (res.Type === 'structured' && res.NodeId === 'status') { + // 执行状态卡片处理 + const data = JSON.parse(ipcContent) as { + key: string; + value: string; + }; + if (data.key !== 're-act-loading-status-key') { + onAIStatusHandleCard({ ...data, timestamp: res.Timestamp }); + } + return; + } + } catch (error) { + handleGrpcDataPushLog({ info: res, pushLog: handlePushLog }); + } + }); + + /** 重置所有数据 */ + const handleResetData = useMemoizedFn(() => { + cardKVPair.current = new Map(); + cardTimeRef.current = null; + setCard([]); + setYakExecResultLogs([]); + execFileRecordOrder.current = 1; + setExecFileRecord(new Map()); + }); + + const handleSetYakResult = useMemoizedFn( + (newData: UseYakExecResultState) => { + setCard(newData.card || []); + setExecFileRecord(newData.execFileRecord || new Map()); + setYakExecResultLogs(newData.yakExecResultLogs || []); + }, + ); + + const state: UseYakExecResultState = useCreation(() => { + return { card, execFileRecord, yakExecResultLogs }; + }, [card, execFileRecord, yakExecResultLogs]); + + const events: UseYakExecResultEvents = useCreation(() => { + return { handleSetData, handleResetData, handleSetYakResult }; + }, []); + + return [state, events] as const; +} + +export default useYakExecResult; diff --git a/src/pages/AIAgent/ai-re-act/hooks/utils.ts b/src/pages/AIAgent/ai-re-act/hooks/utils.ts new file mode 100644 index 00000000..7b9bd4c5 --- /dev/null +++ b/src/pages/AIAgent/ai-re-act/hooks/utils.ts @@ -0,0 +1,134 @@ +/** + * chat 对话数据相关处理工具 + */ +import { generateTaskChatExecution } from '@/pages/AIAgent/ai-agent/defaultConstant' +import { v4 as uuidv4 } from 'uuid' +import type { AIAgentGrpcApi, AIOutputEvent } from './grpcApi' +import type { AITaskInfoProps } from './aiRender' +import type { AIChatLogData, AIChatLogToInfo } from './type' +import type { AIAgentSetting } from '../../ai-agent/aiAgentType' + +/** 将 base64-encoded bytes 转换成json对象并parse出来 */ +export const base64ToJson = (base64: string) => { + if (!base64) return '' + // Base64 → Uint8Array + const binary = atob(base64) + const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0)) + // UTF-8 解码 + const utf8 = new TextDecoder('utf-8').decode(bytes) + return utf8 +} + +/** 生成AI-UI展示的必须基础数据 */ +export const genBaseAIChatData = (info: AIOutputEvent) => { + return { + id: uuidv4(), + AIService: info.AIService, + AIModelName: info.AIModelName, + Timestamp: info.Timestamp, + } +} + +/** 生成一个异常日志数据的对象 */ +export const genErrorLogData = ( + Timestamp: AIChatLogToInfo['Timestamp'], + message: AIChatLogToInfo['data']['message'], +): AIChatLogToInfo => { + return { + type: 'log', + Timestamp, + data: { level: 'error', message: message }, + } +} + +/** 将接口数据(AIOutputEvent)转换为日志数据(AIAgentGrpcApi.Log), 并push到日志队列中 */ +export const handleGrpcDataPushLog = (params: { info: AIOutputEvent; pushLog: (log: AIChatLogData) => void }) => { + try { + const { info, pushLog } = params + // 这类类型的数据从日志数据中屏蔽掉,后续的stream类型逻辑会使用到 + if (info.Type === 'stream_start') return + let ipcContent = base64ToJson(info.Content) || '' + const logInfo: AIChatLogData = { + type: 'log', + Timestamp: info.Timestamp, + data: { + level: `${info.Type}-${info.NodeId}`, + message: ipcContent, + }, + } + pushLog(logInfo) + } catch (error) {} +} + +// #region 处理任务规划-任务树相关方法 +/** 将传入任务区分出可执行任务和父任务两种情况 */ +const genExecTask = (params: { task: AIAgentGrpcApi.PlanTask; level: number; tasks: AITaskInfoProps[] }) => { + const { task, level, tasks } = params + + if (!Array.isArray(task.subtasks) || task.subtasks.length === 0) { + tasks.push({ ...task, subtasks: undefined, level, isLeaf: true }) + return + } else { + tasks.push({ ...task, subtasks: undefined, level, isLeaf: false }) + } + + for (let subtask of task.subtasks) { + genExecTask({ level: level + 1, task: subtask, tasks: tasks }) + } +} + +/** 将一颗任务树转换成可执行任务的一维数组 */ +export const genExecTasks = (taskTree: AIAgentGrpcApi.PlanTask) => { + const execTasks: AITaskInfoProps[] = [] + genExecTask({ task: taskTree, level: 1, tasks: execTasks }) + execTasks.shift() + return execTasks +} +// #endregion + +/** 将树结构任务列表转换成一维数组 */ +export const handleFlatAITree = (sum: AIAgentGrpcApi.PlanTask[], task: AIAgentGrpcApi.PlanTask) => { + if (!Array.isArray(sum)) return null + sum.push(generateTaskChatExecution(task)) + if (task.subtasks && task.subtasks.length > 0) { + for (let subtask of task.subtasks) { + handleFlatAITree(sum, subtask) + } + } +} + +/** 是否自动执行review的continue操作 */ +export const isAutoExecuteReviewContinue = (params: { type?: string; getFunc?: () => AIAgentSetting | undefined }) => { + try { + const { type, getFunc } = params + if (!!type && ['require_user_interactive'].includes(type)) { + // AI交互review不自动执行 + return false + } else { + if (getFunc) { + const request = getFunc() + return request ? request.ReviewPolicy === 'yolo' : false + } + return false + } + } catch (error) { + return false + } +} + +/** 判断是否为tool_xxx_stdout类型数据 */ +export const isToolStdoutStream = (nodeID: string) => { + if (!nodeID) return false + return nodeID.startsWith('tool-') && nodeID.endsWith('-stdout') +} +/** 判断是否为tool_xxx_stderr类型数据 */ +export const isToolStderrStream = (nodeID: string) => { + if (!nodeID) return false + return nodeID.startsWith('tool-') && nodeID.endsWith('-stderr') +} +/** 判断是否为工具执行的流程类型数据(call-tools 和 tool-xxx-stdout) */ +export const isToolExecStream = (nodeID: string) => { + if (nodeID === 'call-tools') return true + if (isToolStdoutStream(nodeID)) return true + return false +} diff --git a/src/pages/AIAgent/defaultConstant.ts b/src/pages/AIAgent/defaultConstant.ts new file mode 100644 index 00000000..841de0e0 --- /dev/null +++ b/src/pages/AIAgent/defaultConstant.ts @@ -0,0 +1,4 @@ +/** 跨页面通信的频道名称 */ +const AIAgentLogChannelName = 'ai-agent-broadcast-channel' + +export { AIAgentLogChannelName } diff --git a/src/pages/AIAgent/enums/aiAgent.ts b/src/pages/AIAgent/enums/aiAgent.ts new file mode 100644 index 00000000..8cde50b1 --- /dev/null +++ b/src/pages/AIAgent/enums/aiAgent.ts @@ -0,0 +1,24 @@ +export enum RemoteAIAgentGV { + /** @name ai-agent-chat 全局配置 */ + AIAgentChatSetting = 'ai-agent-chat-setting', + /** @name 历史对话 */ + AIAgentChatHistory = 'ai-agent-chat-history', + + /** + * 清空用户缓存数据的依据 + * 代码会向缓存里设置一个固定值,如果缓存值和固定值不一样, 则需要清除缓存数据,一样则不需要清除 + */ + AIAgentCacheClear = 'ai-agent-cache-clear', + + /** @name 替换 forge 模板时是否隐藏提示框, 直接进行替换 */ + AIAgentReplaceForgeNoPrompt = 'ai-agent-replace-forge-no-prompt', + + /** @name 替换tool时是否隐藏提示框, 直接进行替换 */ + AIAgentReplaceToolNoPrompt = 'ai-agent-replace-tool-no-prompt', + /** @name ai侧边栏展开收起模式 */ + AIAgentSideShowMode = 'ai-agent-side-show-mode', + /** @name 记忆库快捷删除 */ + AIMemoryRemove = 'ai-memory-remove', + /** @name AIModel检测结果中的编辑器的美化 */ + AIModelCheckResultEditorBeautify = 'ai-model-check-result-editor-beautify', +} diff --git a/src/pages/AIAgent/enums/external.d.ts b/src/pages/AIAgent/enums/external.d.ts new file mode 100644 index 00000000..15b3a2db --- /dev/null +++ b/src/pages/AIAgent/enums/external.d.ts @@ -0,0 +1,156 @@ +import type { Editor } from '@milkdown/kit/core' + +interface KVPair { + Key: string + Value: string +} + +interface KnowledgeBaseEntry { + ID: number + KnowledgeBaseId: number + HiddenIndex: string + KnowledgeTitle: string + KnowledgeType: string + ImportanceScore: number + Keywords: string[] + KnowledgeDetails: string + Summary: string + SourcePage: number + PotentialQuestions: string[] + PotentialQuestionsVector: number[] + CreatedAt?: string + UpdatedAt?: string +} + +interface CustomPluginExecuteFormValue { + [key: string]: number | string | boolean | string[] | Uint8Array | KVPair[] | number[] +} + +interface FileMonitorItemProps { + // 是否为文件夹 + IsDir: boolean + // 操作 + Op: 'delete' | 'create' + // 路径 + Path: string +} + +export interface FileMonitorProps { + Id: string + ChangeEvents: FileMonitorItemProps[] + CreateEvents: FileMonitorItemProps[] + DeleteEvents: FileMonitorItemProps[] +} + +export type EditorMilkdownProps = Editor + +export interface MilkdownCollabProps { + /** 文档标题 */ + title: string + /** 启用协作文档 默认不启用 */ + enableCollab: boolean + /** enableCollab为true,该字段必传,协作文档得唯一标识 */ + milkdownHash: string + /** 编辑器使用的页面,enableCollab为true,该字段必传 */ + routeInfo: { pageId: string; route: YakitRoute | null } + /** 是否开启保存历史 开启后默认间隔 1s */ + enableSaveHistory?: boolean | { enable: boolean; interval: number } + /** 文档链接状态变化 */ + onChangeWSLinkStatus: (v: CollabStatus) => void + /** 在线用户数据变化 */ + onChangeOnlineUser: (v: CollabUserInfo[]) => void + /** 同步标题 */ + onSetTitle: (s: string) => void +} +export interface CustomMilkdownProps { + /** 编辑器使用的模块名称,目前只有记事本(企业版叫云文档)中使用 */ + type: 'notepad' + /** 设置为只读 */ + readonly?: boolean + /** 编辑器默认值 */ + defaultValue?: string + /** 编辑器是否传入type */ + isControlEditorType?: boolean + editor?: EditorMilkdownProps + setEditor?: (s: EditorMilkdownProps) => void + /** 自定义插件 */ + customPlugin?: MilkdownPlugin | MilkdownPlugin[] + + /** 协作文档相关参数 */ + collabProps?: MilkdownCollabProps + /** 编辑器内容的变化 tip:在线协作时,A本地触发内容的变化,B编辑器会同步内容,但listener.markdownUpdated监听不到 */ + onMarkdownUpdated?: (next: string, per: string) => void + /** 卸载前,抛出去最新的内容 */ + onSaveContentBeforeDestroy?: (value: string) => void + /** 定位 dom元素id */ + positionElementId?: string +} +export type MilkdownEditorProps = CustomMilkdownProps + +export interface DeleteOSSFileItem { + fileName: string + time: number +} + +import type { MilkdownPlugin, TimerType } from '@milkdown/ctx' +import { createSlice, createTimer } from '@milkdown/ctx' +import { ctxCallOutOfScope } from '@milkdown/exception' +import type { Parser } from '@milkdown/transformer' +import { ParserState } from '@milkdown/transformer' + +import { withMeta } from '../__internal__' +import { remarkCtx } from './atoms' +import { SchemaReady, schemaCtx } from './schema' + +/// The timer which will be resolved when the parser plugin is ready. +export const ParserReady = createTimer('ParserReady') + +const outOfScope = (() => { + throw ctxCallOutOfScope() +}) as Parser + +/// A slice which contains the parser. +export const parserCtx = createSlice(outOfScope, 'parser') + +/// A slice which stores timers that need to be waited for before starting to run the plugin. +/// By default, it's `[SchemaReady]`. +export const parserTimerCtx = createSlice([] as TimerType[], 'parserTimer') + +/// The parser plugin. +/// This plugin will create a parser. +/// +/// This plugin will wait for the schema plugin. +export const parser: MilkdownPlugin = (ctx) => { + ctx.inject(parserCtx, outOfScope).inject(parserTimerCtx, [SchemaReady]).record(ParserReady) + + return async () => { + await ctx.waitTimers(parserTimerCtx) + const remark = ctx.get(remarkCtx) + const schema = ctx.get(schemaCtx) + + ctx.set(parserCtx, ParserState.create(schema, remark)) + ctx.done(ParserReady) + return () => { + ctx.remove(parserCtx).remove(parserTimerCtx).clearTimer(ParserReady) + } + } +} + +withMeta(parser, { + displayName: 'Parser', +}) + +export interface ReportItem { + type: string + content: string + direction?: boolean +} + +export { + KVPair, + KnowledgeBaseEntry, + CustomPluginExecuteFormValue, + FileMonitorItemProps, + FileMonitorProps, + EditorMilkdownProps, +} diff --git a/src/pages/AIAgent/enums/invoker/schema.ts b/src/pages/AIAgent/enums/invoker/schema.ts new file mode 100644 index 00000000..fd06571b --- /dev/null +++ b/src/pages/AIAgent/enums/invoker/schema.ts @@ -0,0 +1,218 @@ +import type { YakParamProps, YakRiskInfoProps } from '../plugins/pluginsType'; + +export interface ExecHistoryRecord { + Script: string; + ScriptId: string; + Timestamp: number; + DurationMs: number; + Params: string; + Ok: boolean; + Reason: string; + Id: string; + Stdout: Uint8Array; + Stderr: Uint8Array; + StderrLen: number; + StdoutLen: number; + Messages: Uint8Array; + FromYakModule: string; + RuntimeId: string; +} + +export interface PaginationSchema { + Page: number; + Limit: number; + OrderBy: string; + Order: string; + RawOrder?: string; + BeforeId?: number; + AfterId?: number; +} + +export type ExecHistoryRecordResponse = QueryGeneralResponse; + +export interface QueryGeneralResponse { + Data: T[]; + Pagination: PaginationSchema; + Total: number; +} + +export type QueryGeneralResponseProps = { + [key in DataKey]: T[]; +} & { + Pagination: PaginationSchema; + Total: number; +}; +export interface QueryGeneralRequest { + Pagination: PaginationSchema; +} + +export const genDefaultPagination = (limit?: number, page?: number) => { + return { + Limit: limit || 10, + Page: page || 1, + OrderBy: 'updated_at', + Order: 'desc', + } as PaginationSchema; +}; + +// export interface YakScriptParam { +// Field: string +// DefaultValue: string +// TypeVerbose: string +// FieldVerbose: string +// Help: string +// Value?: string | any +// Required?: boolean +// Group?: string +// ExtraSetting?: string +// BuildInParam?: boolean +// } + +export interface YakScriptHooks { + HookName: string; + Hooks: YakScriptHookItem[]; +} + +export interface YakScriptHookItem { + Verbose: string; + YakScriptId: number; + YakScriptName: string; +} + +export interface YakScript { + Id: number; + Content: string; + Type: string; + Params: YakParamProps[]; + CreatedAt: number; + ScriptName: string; + Help: string; + Level: string; + Author: string; + Tags: string; + IsHistory: boolean; + IsIgnore?: boolean; + IsGeneralModule?: boolean; + GeneralModuleVerbose?: string; + GeneralModuleKey?: string; + FromGit?: string; + EnablePluginSelector?: boolean; + PluginSelectorTypes?: string; + OnlineId: number; + OnlineScriptName: string; + OnlineContributors: string; + UserId: number; + UUID: string; + OnlineIsPrivate?: boolean; + HeadImg?: string; + OnlineBaseUrl?: string; + BaseOnlineId?: number; + OnlineOfficial?: boolean; + OnlineGroup?: string; + IsCorePlugin?: boolean; + UpdatedAt?: number; + // RiskType?: string 废弃 + // RiskDetail?: YakRiskInfoProps[] 废弃 + // RiskAnnotation?: string 废弃 + CollaboratorInfo?: Collaborator[]; + /**前端判断使用,该插件是否为本地插件,OnlineBaseUrl与当前最新的私有域不一样则为本地插件 */ + isLocalPlugin?: boolean; + RiskInfo?: YakRiskInfoProps[]; + IsUpdate?: boolean; + /** 全局变量 */ + PluginEnvKey?: string[]; +} + +export interface Collaborator { + HeadImg: string; + UserName: string; +} + +export type QueryYakScriptsResponse = QueryGeneralResponse; + +export interface QueryYakScriptRequest extends QueryGeneralRequest { + Type?: string; + Keyword?: string; + IsHistory?: boolean; + IsIgnore?: boolean; + IsBatch?: boolean; + IsGeneralModule?: boolean; + ExcludeNucleiWorkflow?: boolean; + ExcludeScriptNames?: string[]; + IncludedScriptNames?: string[]; + Tag?: string[]; + NoResultReturn?: boolean; + UserId?: number; + UserName?: string; + + // 展示信息中,插件商店的顺序和本地顺序不应该一样 + IgnoreGeneralModuleOrder?: boolean; + UUID?: string; + // 插件组 + Group?: { UnSetGroup: boolean; Group: string[]; IsPocBuiltIn?: string }; + ExcludeTypes?: string[]; + IsMITMParamPlugins?: number; //0->默认全部 1->是mitm带参数插件 2->mitm不带参数; + + // 关键词搜索 + FieldKeywords?: string; +} + +/* +* message ExecResult { + string Hash = 1; + string OutputJson = 2; + bytes Raw = 3; + bool IsMessage = 4; + bytes Message = 5; +} +* */ + +export interface ExecResult { + Hash: string; + OutputJson: string; + Raw: Uint8Array; + IsMessage: boolean; + Message: Uint8Array; + Id?: number; + Progress: number; + RuntimeID?: string; +} + +export interface TagsAndType { + Value: string; + Total: number; +} + +export interface GetYakScriptTagsAndTypeResponse { + Type: TagsAndType[]; + Tag: TagsAndType[]; + Group: TagsAndType[]; +} + +export interface GroupCount { + Value: string; + Total: number; + Default: boolean; + TemporaryId?: string; + IsPocBuiltIn?: boolean; +} + +export interface QueryYakScriptGroupResponse { + Group: GroupCount[]; +} + +export interface GetYakScriptGroupResponse { + SetGroup: string[]; + AllGroup: string[]; +} + +export interface SaveYakScriptGroupRequest { + Filter: QueryYakScriptRequest; + SaveGroup: string[]; + RemoveGroup: string[]; + PageId?: string; +} + +export interface ResetYakScriptGroupRequest { + Token: string; +} diff --git a/src/pages/AIAgent/enums/keyboard.ts b/src/pages/AIAgent/enums/keyboard.ts new file mode 100644 index 00000000..598b683f --- /dev/null +++ b/src/pages/AIAgent/enums/keyboard.ts @@ -0,0 +1,254 @@ +export enum YakitKeyBoard { + // #region 键盘上方数字按键 + Digit_0 = '0', + Digit_1 = '1', + Digit_2 = '2', + Digit_3 = '3', + Digit_4 = '4', + Digit_5 = '5', + Digit_6 = '6', + Digit_7 = '7', + Digit_8 = '8', + Digit_9 = '9', + // #endregion + + // #region 小键盘数字按键 + Numpad_0 = 'Numpad_0', + Numpad_1 = 'Numpad_1', + Numpad_2 = 'Numpad_2', + Numpad_3 = 'Numpad_3', + Numpad_4 = 'Numpad_4', + Numpad_5 = 'Numpad_5', + Numpad_6 = 'Numpad_6', + Numpad_7 = 'Numpad_7', + Numpad_8 = 'Numpad_8', + Numpad_9 = 'Numpad_9', + // #endregion + + // #region 字母 + KEY_A = 'A', + KEY_B = 'B', + KEY_C = 'C', + KEY_D = 'D', + KEY_E = 'E', + KEY_F = 'F', + KEY_G = 'G', + KEY_H = 'H', + KEY_I = 'I', + KEY_J = 'J', + KEY_K = 'K', + KEY_L = 'L', + KEY_M = 'M', + KEY_N = 'N', + KEY_O = 'O', + KEY_P = 'P', + KEY_Q = 'Q', + KEY_R = 'R', + KEY_S = 'S', + KEY_T = 'T', + KEY_U = 'U', + KEY_V = 'V', + KEY_W = 'W', + KEY_X = 'X', + KEY_Y = 'Y', + KEY_Z = 'Z', + // #endregion + + // #region F数字键 + F1 = 'F1', + F2 = 'F2', + F3 = 'F3', + F4 = 'F4', + F5 = 'F5', + F6 = 'F6', + F7 = 'F7', + F8 = 'F8', + F9 = 'F9', + F10 = 'F10', + F11 = 'F11', + F12 = 'F12', + // #endregion + + // #region 功能键 + Shift = 'Shift', + Control = 'Control', + Alt = 'Alt', + Meta = 'Meta', + + Escape = 'Escape', + Tab = 'Tab', + CapsLock = 'CapsLock', + Backspace = 'Backspace', + Enter = 'Enter', + + Backquote = '`', // `~ + Minus = '-', // -_ + Equal = '=', // =+ + BracketLeft = '[', // [{ + BracketRight = ']', // ]} + Backslash = '\\', // \| + Semicolon = ';', // ;: + Quote = "'", // '" + Comma = ',', // ,< + Period = '.', // .> + Slash = '/', // /? + Space = 'space', // 空格 + + Delete = 'delete', + Insert = 'insert', + Home = 'home', + End = 'end', + PageUp = 'pageup', + PageDown = 'pagedown', + // #endregion + + // #region 方向键 + UpArrow = 'up', + DownArrow = 'down', + LeftArrow = 'left', + RightArrow = 'right', + // #endregion + + // #region 小键盘功能键 + Numpad_Divide = 'Numpad_Divide', // / + Numpad_Multiply = 'Numpad_Multiply', // * + Numpad_Subtract = 'Numpad_Subtract', // - + Numpad_Add = 'Numpad_Add', // + + Numpad_Decimal = 'Numpad_Decimal', // . + // #endregion +} +export enum YakitKeyMod { + Alt = YakitKeyBoard.Alt, + Shift = YakitKeyBoard.Shift, + Control = YakitKeyBoard.Control, + Meta = YakitKeyBoard.Meta, + /** mac下对应cmd, win下对应ctrl */ + CtrlCmd = 'ctrl-cmd', +} +/** @name 小键盘上,键位锁定和未锁定时对应的键值 */ +export const NumpadKeyTableMaps: Record = { + 'Numpad0-0': 'Numpad0', + 'Numpad1-1': 'Numpad1', + 'Numpad2-2': 'Numpad2', + 'Numpad3-3': 'Numpad3', + 'Numpad4-4': 'Numpad4', + 'Numpad5-5': 'Numpad5', + 'Numpad6-6': 'Numpad6', + 'Numpad7-7': 'Numpad7', + 'Numpad8-8': 'Numpad8', + 'Numpad9-9': 'Numpad9', + 'NumpadDecimal-.': 'NumpadDecimal', + + 'Numpad0-Insert': 'Insert', + 'Numpad1-End': 'End', + 'Numpad2-ArrowDown': 'ArrowDown', + 'Numpad3-PageDown': 'Numpad3', + 'Numpad4-ArrowLeft': 'ArrowLeft', + 'Numpad6-ArrowRight': 'ArrowRight', + 'Numpad7-Home': 'Home', + 'Numpad8-ArrowUp': 'ArrowUp', + 'Numpad9-PageUp': 'PageUp', + 'NumpadDecimal-Delete': 'Delete', +} + +/** + * @name 物理按键到快捷键按键的映射 + * @key 快捷键按键 + * @value 物理按键 + */ +export const KeyboardToKeyTableMaps: Record = { + '0': ['Digit0'], + '1': ['Digit1'], + '2': ['Digit2'], + '3': ['Digit3'], + '4': ['Digit4'], + '5': ['Digit5'], + '6': ['Digit6'], + '7': ['Digit7'], + '8': ['Digit8'], + '9': ['Digit9'], + Numpad_0: ['Numpad0'], + Numpad_1: ['Numpad1'], + Numpad_2: ['Numpad2'], + Numpad_3: ['Numpad3'], + Numpad_4: ['Numpad4'], + Numpad_5: ['Numpad5'], + Numpad_6: ['Numpad6'], + Numpad_7: ['Numpad7'], + Numpad_8: ['Numpad8'], + Numpad_9: ['Numpad9'], + A: ['KeyA'], + B: ['KeyB'], + C: ['KeyC'], + D: ['KeyD'], + E: ['KeyE'], + F: ['KeyF'], + G: ['KeyG'], + H: ['KeyH'], + I: ['KeyI'], + J: ['KeyJ'], + K: ['KeyK'], + L: ['KeyL'], + M: ['KeyM'], + N: ['KeyN'], + O: ['KeyO'], + P: ['KeyP'], + Q: ['KeyQ'], + R: ['KeyR'], + S: ['KeyS'], + T: ['KeyT'], + U: ['KeyU'], + V: ['KeyV'], + W: ['KeyW'], + X: ['KeyX'], + Y: ['KeyY'], + Z: ['KeyZ'], + F1: ['F1'], + F2: ['F2'], + F3: ['F3'], + F4: ['F4'], + F5: ['F5'], + F6: ['F6'], + F7: ['F7'], + F8: ['F8'], + F9: ['F9'], + F10: ['F10'], + F11: ['F11'], + F12: ['F12'], + Shift: ['ShiftLeft', 'ShiftRight'], + Control: ['ControlLeft', 'ControlRight'], + Alt: ['AltLeft', 'AltRight'], + Meta: ['MetaLeft', 'MetaRight'], + Escape: ['Escape'], + Tab: ['Tab'], + CapsLock: ['CapsLock'], + Backspace: ['Backspace'], + Enter: ['Enter', 'NumpadEnter'], + '`': ['Backquote'], + '-': ['Minus'], + '=': ['Equal'], + '[': ['BracketLeft'], + ']': ['BracketRight'], + '\\': ['Backslash'], + ';': ['Semicolon'], + "'": ['Quote'], + ',': ['Comma'], + '.': ['Period'], + '/': ['Slash'], + space: ['Space'], + delete: ['Delete'], + insert: ['Insert'], + home: ['Home'], + end: ['End'], + pageup: ['PageUp'], + pagedown: ['PageDown'], + up: ['ArrowUp'], + down: ['ArrowDown'], + left: ['ArrowLeft'], + right: ['ArrowRight'], + Numpad_Divide: ['NumpadDivide'], + Numpad_Multiply: ['NumpadMultiply'], + Numpad_Subtract: ['NumpadSubtract'], + Numpad_Add: ['NumpadAdd'], + Numpad_Decimal: ['NumpadDecimal'], +} diff --git a/src/pages/AIAgent/enums/yakitRoute.ts b/src/pages/AIAgent/enums/yakitRoute.ts new file mode 100644 index 00000000..96c396cc --- /dev/null +++ b/src/pages/AIAgent/enums/yakitRoute.ts @@ -0,0 +1,127 @@ +/** 渲染端所有页面枚举 */ +export enum YakitRoute { + /** 首页 */ + NewHome = 'new-home', + /** 手动渗透 v1 */ + HTTPHacker = 'httpHacker', + /** mitm v2 */ + MITMHacker = 'mitm-hijack', + HTTPFuzzer = 'httpFuzzer', + WebsocketFuzzer = 'websocket-fuzzer', + Codec = 'codec', + DataCompare = 'dataCompare', + /** 基础工具 */ + Mod_ScanPort = 'scan-port', + PoC = 'poc', + Plugin_OP = 'plugin-op', + Mod_Brute = 'brute', + Plugin_Hub = 'plugin-hub', + BatchExecutorPage = 'batch-executor-page-ex', + /** 反连 */ + DNSLog = 'dnslog', + ICMPSizeLog = 'icmp-sizelog', + TCPPortLog = 'tcp-portlog', + PayloadGenerater_New = 'PayloadGenerater_New', + ReverseServer_New = 'ReverseServer_New', + ShellReceiver = 'shellReceiver', + /** 数据库 */ + DB_HTTPHistory = 'db-http-request', + DB_HTTPHistoryAnalysis = 'db-http-request-analysis', + DB_Report = 'db-reports-results', + DB_Risk = 'db-risks', + Misstatement = 'misstatement', + DB_Ports = 'db-ports', + DB_Domain = 'db-domains', + DB_CVE = 'cve', + /** 独立功能页面 */ + // Yak-Runner页面 + YakScript = 'yakScript', + // Payload页面 + PayloadManager = 'payload-manager', + // 配置管理页面(字典/代理/热加载) + ConfigManagement = 'config-management', + // 私有版用户管理 + AccountAdminPage = 'account-admin-page', + RoleAdminPage = 'role-admin-page', + HoleCollectPage = 'hole-collect-page', + LicenseAdminPage = 'license-admin-page', + // 公开版用户管理 + TrustListPage = 'trust-list-admin-page', + PlugInAdminPage = 'plug-in-admin-page', + // 远程管理 + ControlAdminPage = 'control-admin-page', + // 插件批量页面中未完成页面的点击弹出页面 + BatchExecutorRecover = 'batch-executor-recover', + // http-history页面右键菜单"数据包扫描"生成页面 + PacketScanPage = 'packet-scan-page', + // 新建插件页面 + AddYakitScript = 'add-yakit-script', + /** 简易版专属 */ + SimpleDetect = 'simple-detect', + // 录屏管理器 + ScreenRecorderPage = 'screen-recorder-page', + /** 全局功能-试验性功能-BAS实验室 */ + DB_ChaosMaker = 'db-chaosmaker', + /** 调试插件编辑器 */ + Beta_DebugTrafficAnalize = '**beta-debug-traffic-analize', + /** 调试插件编辑器 */ + Beta_DebugMonacoEditor = 'beta-debug-monaco-editor', + /** 靶场调试 */ + Beta_VulinboxManager = 'beta-vulinbox-manager', + /** 网络调试 */ + Beta_DiagnoseNetwork = 'beta-diagnose-network', + /** 配置全局 */ + Beta_ConfigNetwork = 'beta-config-network', + /** 插件管理 */ + Plugin_Audit = 'plugin-audit', + /** WebShell 管理 */ + Beta_WebShellManager = 'beta-webshell-manager', + Beta_WebShellOpt = 'beta-webshell-opt', + /** 数据统计 */ + Data_Statistics = 'data_statistics', + /** 空间引擎 */ + Space_Engine = 'space-engine', + /** YakRunner代码扫描 */ + YakRunner_Code_Scan = 'yakrunner-code-scan', + // YakRunner代码审计 + YakRunner_Audit_Code = 'yakrunner-audit-code', + // YakRunner项目管理 + YakRunner_Project_Manager = 'yakrunner-project-manager', + // YakRunner扫描历史 + YakRunner_ScanHistory = 'yakrunner_scanHistory', + /** 规则管理 */ + Rule_Management = 'rule-management', + /** 笔记本管理 */ + Notepad_Manage = 'notepad-manage', + /** 笔记本内容编辑页面 */ + Modify_Notepad = 'modify-notepad', + // YakRunner漏洞管理 + YakRunner_Audit_Hole = 'yakrunner-audit-hole', + // 系统配置(目前仅供IRify企业版配置水印) + System_Config = 'system-config', + // AI-Agent + AI_Agent = 'ai-agent', + + // 快捷键设置 + ShortcutKey = 'shortcut-key', + /** Java 反编译 */ + Yak_Java_Decompiler = 'yak-java-decompiler', + // 指纹库 + FingerprintManage = 'fingerprint-manage', + // ssa-result-diff + Ssa_Result_Diff = 'ssa-result-diff', + // 新建 Forge + AddAIForge = 'add-ai-forge', + // 编辑 Forge + ModifyAIForge = 'modify-ai-forge', + /** AI 工具新增 */ + AddAITool = 'add-ai-tool', + /** AI 工具编辑 */ + ModifyAITool = 'modify-ai-tool', + /** AI 知识库 */ + AI_REPOSITORY = 'ai-repository', + // SSA编译历史 + SSA_Compile_History = 'ssa-compile-history', + /** AI 记忆库 */ + AI_Memory = 'ai-memory', +} diff --git a/src/pages/AIAgent/hooks/useRafPolling/useRafPolling.ts b/src/pages/AIAgent/hooks/useRafPolling/useRafPolling.ts new file mode 100644 index 00000000..65bf91df --- /dev/null +++ b/src/pages/AIAgent/hooks/useRafPolling/useRafPolling.ts @@ -0,0 +1,98 @@ +import { useRef, useState, type DependencyList } from 'react'; +import { useMemoizedFn, useInterval, useUpdateEffect, useLatest } from 'ahooks'; + +export interface UseRafPollingOptions { + /** + * 获取当前数据的函数。 + * 返回 null 表示本次不更新。 + */ + getData: () => T | null; + /** + * 轮询间隔(毫秒),默认 200。 + */ + interval?: number; + /** + * 停止条件:返回 true 时停止轮询。 + */ + shouldStop?: (data: T) => boolean; + /** + * 是否需要更新数据,用于避免不必要的重渲染。 + * 返回 true 才会触发 setData。 + */ + shouldUpdate?: (prev: T | null, next: T) => boolean; + /** + * 数据克隆策略。 + * 如果传入,会在 setData 前执行。 + * 解决数据引用 导致的组件不更新问题。 + */ + clone?: (data: T) => T; + /** + * 重启依赖数组。 + * 当依赖变化时,重新启动轮询。 + */ + resetDeps?: DependencyList; +} + +/** + * 基于 requestAnimationFrame 的轮询 Hook。 + */ +export function useRafPolling(options: UseRafPollingOptions) { + const { + getData, + interval = 200, + shouldStop, + shouldUpdate, + clone, + resetDeps = [], + } = options; + + const [data, setData] = useState(() => getData()); + const [running, setRunning] = useState(true); + + const dataRef = useRef(data); + + const getDataRef = useLatest(getData); + const shouldStopRef = useLatest(shouldStop); + const shouldUpdateRef = useLatest(shouldUpdate); + const cloneRef = useLatest(clone); + + const tick = useMemoizedFn(() => { + if (!running) return; + const result = getDataRef.current(); + if (!result) return; + + if (shouldStopRef.current?.(result)) { + setRunning(false); + } + // 进行数据克隆,确保引用变化 + const clonedResult = cloneRef.current + ? cloneRef.current(result) + : result; + + const needUpdate = shouldUpdateRef.current + ? shouldUpdateRef.current(dataRef.current, clonedResult) + : true; + + // 存储 clone 后的数据,确保下次比较时引用不同 + dataRef.current = clonedResult; + + if (needUpdate) { + setData(clonedResult); + } + }); + + useUpdateEffect(() => { + const initial = getDataRef.current(); + const cloned = + initial && cloneRef.current ? cloneRef.current(initial) : initial; + + dataRef.current = cloned ?? null; + setData(cloned ?? null); + + setRunning(true); + }, resetDeps); + + useInterval(tick, running ? interval : undefined); + + return data; +} diff --git a/src/pages/AIAgent/plugins/editDetails/builtInData.tsx b/src/pages/AIAgent/plugins/editDetails/builtInData.tsx new file mode 100644 index 00000000..1af5a8ea --- /dev/null +++ b/src/pages/AIAgent/plugins/editDetails/builtInData.tsx @@ -0,0 +1,39 @@ +/** + * @name 插件数据修改时-内置的tags列表数据 + */ +export const BuiltInTags: string[] = [ + 'IOT', + '主流CMS', + '中间件', + '代码研发', + '功能类型', + '应用类型', + '网络设备', + '大数据平台', + '数据库服务', + '虚拟化服务', + '邮件服务器', + '集权管控类', + '主流应用框架', + '协同办公套件', + '通用漏洞检测', + '主流第三方服务', + '信息收集', + '数据处理', + '暴力破解', + '指纹识别', + '目录爆破', + '加解密工具', + '威胁情报', + '空间引擎', + 'AI工具', +]; + +/** @name Risk信息风险等级对应Tag组件颜色 */ +export const RiskLevelToTag: Record = { + critical: { color: 'serious', name: '严重' }, + high: { color: 'danger', name: '高危' }, + warning: { color: 'info', name: '中危' }, + low: { color: 'yellow', name: '低危' }, + info: { color: 'success', name: '信息' }, +}; diff --git a/src/pages/AIAgent/plugins/editDetails/utils.tsx b/src/pages/AIAgent/plugins/editDetails/utils.tsx new file mode 100644 index 00000000..53fec83e --- /dev/null +++ b/src/pages/AIAgent/plugins/editDetails/utils.tsx @@ -0,0 +1,200 @@ +import type { CodeToInfoResponseProps, PluginDataProps, YakParamProps } from '../pluginsType' +import { Uint8ArrayToString } from '@/utils/str' +import type { APIFunc } from '@/apiUtils/type' +import type { API } from '../../ai-agent/type/resposeType' +import { pluginParamsConvertLocalToOnline, riskDetailConvertLocalToOnline } from '../../utils/convert' +import { toolDelInvalidKV } from '../../utils/tool' +import type { YakExtraParamProps } from '../operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeardType' +import type { YakExecutorParam } from '../../types/interface/YakExecutorParams' + +// const { ipcRenderer } = window.require('electron'); + +/** -------------------- 数据结构转换 Start -------------------- */ +/** + * @name 线上插件数据结构(API.PluginsDetail)-转换成-提交修改插件数据结构(API.PluginsRequest) + * @param idModify 线上插件详细信息 + * @param modify 提交修改插件编辑信息 + */ +export const convertRemoteToRemoteInfo = (info: API.PluginsDetail, modify?: PluginDataProps) => { + // @ts-ignore + const request: API.PluginsRequest = { + ...info, + tags: undefined, + download_total: Number(info.downloaded_total) || 0, + } + try { + request.tags = (info.tags || '').split(',') || [] + } catch (error) {} + + if (modify) { + // 更新可编辑配置的内容 + request.script_name = modify.ScriptName + request.type = modify.Type + request.help = modify.Help + request.riskInfo = riskDetailConvertLocalToOnline(modify.RiskDetail) + request.tags = modify.Tags?.split(',') || [] + request.params = modify.Params ? pluginParamsConvertLocalToOnline(modify.Params) : undefined + request.enable_plugin_selector = modify.EnablePluginSelector + request.plugin_selector_types = modify.PluginSelectorTypes + request.content = modify.Content + request.pluginEnvKey = modify.PluginEnvKey + } + + // 没有tags就赋值为undefined + if (request.tags?.length === 0) request.tags = undefined + // 分组为空字符时清空值(影响后端数据处理) + if (!request.group) request.group = undefined + // 没有riskInfo就赋值为undefined + if ((request.riskInfo || []).length === 0) request.riskInfo = undefined + // 没有params就赋值为undefined + if ((request.params || []).length === 0) request.params = undefined + // 没有pluginEnvKey就赋值为undefined + if ((request.pluginEnvKey || []).length === 0) request.pluginEnvKey = undefined + + return toolDelInvalidKV(request) as API.PluginsRequest +} +/** -------------------- 数据结构转换 End -------------------- */ + +/** -------------------- 插件参数数据处理工具 Start -------------------- */ +/** + * @description 根据组名将参数分组 + * @returns 返回处理好分组后的数据 + */ +export const ParamsToGroupByGroupName = (arr: YakParamProps[]): YakExtraParamProps[] => { + let map = {} + let paramsGroupList: YakExtraParamProps[] = [] + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < arr.length; i++) { + let ai = arr[i] + // @ts-ignore + if (!map[ai.Group || 'default']) { + paramsGroupList.push({ + group: ai.Group || 'default', + data: [ai], + }) + // @ts-ignore + map[ai.Group || 'default'] = ai + } else { + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let j = 0; j < paramsGroupList.length; j++) { + let dj = paramsGroupList[j] + if (dj.group === (ai.Group || 'default')) { + dj.data.push(ai) + break + } + } + } + } + return paramsGroupList || [] +} + +/** + * @description 表单显示的值,根据类型返回对应的类型的值 + */ +export const getValueByType = (defaultValue: string, type: string): number | string | boolean | string[] => { + let value + switch (type) { + case 'uint': + value = parseInt(defaultValue || '0', 10) + break + case 'float': + value = parseFloat(defaultValue || '0.0') + break + case 'boolean': + value = defaultValue === 'true' || !!defaultValue === true + break + case 'select': + // 考虑(defaultValue)的数据可能本身就是一个数组 + if (Array.isArray(defaultValue)) { + value = defaultValue.length > 0 ? defaultValue : [] + } else { + const newVal = defaultValue ? defaultValue.split(',') : [] + value = newVal.length > 0 ? newVal : [] + } + break + default: + value = defaultValue ? defaultValue : '' + break + } + return value +} + +/** + * @description 处理最后的执行参数 + * @param {{[string]:any}} object + * @returns {YakExecutorParam[]} + */ +export const getYakExecutorParam = (object: { [key: string]: any }) => { + let newValue: YakExecutorParam[] = [] + Object.entries(object).forEach(([key, val]) => { + const isNodeBuffer = typeof Buffer !== 'undefined' && val instanceof Buffer + const isBinaryValue = isNodeBuffer || val instanceof Uint8Array || val instanceof ArrayBuffer + if (isBinaryValue) { + const uint8Val = val instanceof Uint8Array ? val : val instanceof ArrayBuffer ? new Uint8Array(val) : val + newValue = [ + ...newValue, + { + Key: key, + Value: Uint8ArrayToString(uint8Val), + }, + ] + return + } + if (val === true) { + newValue = [ + ...newValue, + { + Key: key, + Value: true, + }, + ] + return + } + if (val === false || val === undefined) { + return + } + newValue = [ + ...newValue, + { + Key: key, + Value: val, + }, + ] + }) + return newValue +} +/** -------------------- 插件参数数据处理工具 End -------------------- */ + +interface PluginCodeToInfoRequest { + /** 插件类型 */ + type: string + /** 插件源码 */ + code: string +} +/** @name 获取源码中的参数和风险信息 */ +export const onCodeToInfo: APIFunc = (params, hiddenError) => { + console.log('获取源码中的参数和风险信息:', params, hiddenError) + return new Promise((resolve) => { + // ipcRenderer (Electron) 不可用,暂不支持此功能 + resolve(null) + // const request: CodeToInfoRequestProps = { + // YakScriptType: params.type, + // YakScriptCode: params.code, + // } + // ipcRenderer + // .invoke('YaklangInspectInformation', request) + // .then((res: CodeToInfoResponseProps) => { + // resolve({ + // Information: res.Information || [], + // CliParameter: res.CliParameter || [], + // RiskInfo: res.RiskInfo || [], + // Tags: res.Tags || [], + // PluginEnvKey: res.PluginEnvKey || [], + // }) + // }) + // .catch(() => { + // if (!hiddenError) yakitNotify('error', '通过源码获取参数、漏洞与风险信息以及 tag 信息失败') + // resolve(null) + // }) + }) +} diff --git a/src/pages/AIAgent/plugins/operator/expandAndRetract/ExpandAndRetract.module.scss b/src/pages/AIAgent/plugins/operator/expandAndRetract/ExpandAndRetract.module.scss new file mode 100644 index 00000000..91ecf6cd --- /dev/null +++ b/src/pages/AIAgent/plugins/operator/expandAndRetract/ExpandAndRetract.module.scss @@ -0,0 +1,83 @@ +.expand-and-retract-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px; + border-bottom: 1px solid var(--Colors-Use-Neutral-Border); + // position: relative; + transition: background-color 0.3s ease; + background-color: var(--Colors-Use-Basic-Background); + + position: sticky; + top: 0; + z-index: 12; + &:hover { + cursor: pointer; + background-color: var(--Colors-Use-Neutral-Bg-Hover); + border-bottom-color: var(--Colors-Use-Main-Border); + .expand-and-retract-header-icon-body { + width: 76px; + border: 1px solid var(--Colors-Use-Main-Border); + color: var(--Colors-Use-Main-Primary); + .expand-and-retract-header-icon-text { + width: 48px; + opacity: 1; + } + } + } + &:focus, + &:active { + background-color: var(--Colors-Use-Neutral-Bg); + } +} + +.expand-and-retract-header-process { + background-color: var(--Colors-Use-Main-Bg); +} +.expand-and-retract-header-finished { + background-color: var(--Colors-Use-Success-Bg); +} + +.expand-and-retract-header-error { + background-color: var(--Colors-Use-Error-Bg); +} + +.expand-and-retract-header-icon-body { + display: flex; + align-items: center; + position: absolute; + right: 50%; + left: 50%; + top: 75%; + background-color: var(--Colors-Use-Basic-Background); + padding: 4px; + border: 1px solid var(--Colors-Use-Neutral-Border); + flex-shrink: 0; + color: var(--Colors-Use-Neutral-Text-1-Title); + width: 24px; + height: 24px; + border-radius: 12px; + transition: width 0.2s ease; + font-size: 12px; + z-index: 12; + + .expand-and-retract-icon { + margin-left: 1px; + margin-right: 4px; + } + + .expand-and-retract-header-icon-text { + display: flex; + align-items: center; + word-break: keep-all; + width: 0; + opacity: 0; + transition: + width 0.3s ease, + opacity 0.2s ease; + } + svg { + width: 12px; + height: 12px; + } +} diff --git a/src/pages/AIAgent/plugins/operator/expandAndRetract/ExpandAndRetract.tsx b/src/pages/AIAgent/plugins/operator/expandAndRetract/ExpandAndRetract.tsx new file mode 100644 index 00000000..6e38e4cf --- /dev/null +++ b/src/pages/AIAgent/plugins/operator/expandAndRetract/ExpandAndRetract.tsx @@ -0,0 +1,100 @@ +import type { MouseEventHandler, ReactNode } from 'react'; +import React from 'react'; +import styles from './ExpandAndRetract.module.scss'; +import { + OutlineChevrondoubledownIcon, + OutlineChevrondoubleupIcon, +} from '@/assets/icon/outline'; +import classNames from 'classnames'; + +/** 根据状态显示过度动画 */ +export type ExpandAndRetractExcessiveState = + | 'default' + | 'process' + | 'finished' + | 'error' + | 'paused'; +interface ExpandAndRetractProps { + onExpand: MouseEventHandler; + isExpand: boolean; + children?: ReactNode; + className?: string; + animationWrapperClassName?: string; + /** @description 默认/过程中/完成 根据状态显示过度动画 */ + status?: ExpandAndRetractExcessiveState; + /** 展开文案 */ + expandText?: string; + /** 收起文案 */ + retractText?: string; +} +export const ExpandAndRetract: React.FC = React.memo( + (props) => { + const { + isExpand, + onExpand, + children, + className = '', + animationWrapperClassName = '', + status = 'default', + expandText, + retractText, + } = props; + return ( +
    +
    + {isExpand ? ( + <> + + + {retractText || '收起参数'} + + + ) : ( + <> + + + {expandText || '展开参数'} + + + )} +
    + {children} +
    + ); + }, +); diff --git a/src/pages/AIAgent/plugins/operator/horizontalScrollCard/HorizontalScrollCard.module.scss b/src/pages/AIAgent/plugins/operator/horizontalScrollCard/HorizontalScrollCard.module.scss new file mode 100644 index 00000000..29e52d55 --- /dev/null +++ b/src/pages/AIAgent/plugins/operator/horizontalScrollCard/HorizontalScrollCard.module.scss @@ -0,0 +1,229 @@ +@use "sass:color"; + +%display-flex-center { + display: flex; + align-items: center; +} +%display-column-center { + display: flex; + flex-direction: column; + justify-content: center; +} +%direction-btn { + width: 24px; + height: 24px; + flex-shrink: 0; + background-color: var(--Colors-Use-Basic-Background); + border-radius: 50%; + border: 1px solid var(--Colors-Use-Neutral-Border); + @extend %display-flex-center; + justify-content: center; + cursor: pointer; + svg { + width: 12px; + height: 12px; + color: var(--Colors-Use-Neutral-Text-1-Title); + } +} + +$CardStatusColor: ( + purple: ( + bgColor: rgba(136, 99, 247, 0.1), + borderColor: rgba(136, 99, 247, 0.2) + ), + bluePurple: ( + bgColor: rgba(218, 95, 221, 0.1), + borderColor: rgba(218, 95, 221, 0.2) + ), + blue: ( + bgColor: rgba(74, 148, 248, 0.1), + borderColor: rgba(74, 148, 248, 0.2) + ), + lakeBlue: ( + bgColor: rgba(41, 188, 208, 0.1), + borderColor: rgba(41, 188, 208, 0.2) + ), + cyan: ( + bgColor: rgba(53, 216, 238, 0.1), + borderColor: rgba(53, 216, 238, 0.2) + ), + green: ( + bgColor: rgba(86, 201, 145, 0.1), + borderColor: rgba(86, 201, 145, 0.2) + ), + red: ( + bgColor: rgba(244, 115, 107, 0.1), + borderColor: rgba(244, 115, 107, 0.2) + ), + orange: ( + bgColor: rgba(255, 182, 96, 0.1), + borderColor: rgba(255, 182, 96, 0.2) + ), + yellow: ( + bgColor: rgba(255, 213, 131, 0.1), + borderColor: rgba(255, 213, 131, 0.2) + ) +); +$keyColor: map-keys($CardStatusColor); +@for $c from 1 through length($keyColor) { + $name: nth($keyColor, $c); + $currentColor: map-get($CardStatusColor, $name); + .card-item-bg-#{"" + $name} { + background-color: map-get($currentColor, "bgColor"); + } + .card-item-border-left-#{"" + $name} { + border-left: 2px solid map-get($currentColor, "borderColor"); + } +} +.text-error { + color: var(--Colors-Use-Error-Primary) !important; +} +.text-success { + color: var(--Colors-Use-Success-Primary) !important; +} +.horizontal-scroll-card { + @extend %display-column-center; + gap: 8px; + + ::-webkit-scrollbar { + width: 0; + height: 0; + } + &-heard { + @extend %display-flex-center; + gap: 4px; + &-title { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 500; + line-height: 16px; /* 133.333% */ + } + &-total { + display: flex; + padding: 2px 6px; + align-items: flex-start; + gap: 10px; + border-radius: 8px; + background: var(--Colors-Use-Neutral-Bg); + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 10px; + line-height: 12px; /* 120% */ + } + } + &-list-wrapper { + position: relative; + @extend %display-flex-center; + } + &-list { + display: flex; + align-items: stretch; + gap: 12px; + overflow: auto; + scroll-behavior: smooth; + + &-direction-left { + @extend %direction-btn; + position: absolute; + left: 0; + } + + &-direction-right { + @extend %direction-btn; + position: absolute; + right: 0; + } + + &-item-info-multiple { + @extend %display-column-center; + flex-shrink: 0; + border-radius: 4px; + height: 88px; + padding: 12px 16px; + overflow-x: auto; + overflow-y: hidden; + &-tag { + @extend %display-flex-center; + gap: 2px; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 14px; + font-weight: 600; + line-height: 20px; /* 142.857% */ + svg { + width: 16px; + height: 16px; + color: var(--Colors-Use-Neutral-Disable); + } + } + &-infos { + @extend %display-flex-center; + gap: 20px; + margin-top: 4px; + height: 44px; + &-item { + @extend %display-column-center; + gap: 4px; + flex-shrink: 0; + &-data { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 16px; + font-weight: 600; + line-height: 24px; /* 150% */ + } + &-id { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + line-height: 16px; /* 133.333% */ + } + } + } + } + &-item-info-single { + @extend %display-column-center; + justify-content: space-between; + border-radius: 4px; + flex: 1 0 fit-content; + min-width: 96px; + min-height: 68px; + padding: 12px 16px; + &-data { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 16px; + font-weight: 600; + line-height: 24px; /* 150% */ + } + &-id { + color: var(--Colors-Use-Neutral-Text-4-Help-text); + font-size: 12px; + font-weight: 400; + line-height: 16px; /* 133.333% */ + } + } + .card-item-small { + min-height: 50px; + padding: 8px 12px; + .horizontal-scroll-card-list-item-info-single-data { + font-size: 14px; + line-height: 20px; + } + .horizontal-scroll-card-list-item-info-single-id { + color: var(--Colors-Use-Neutral-Text-3-Secondary); + font-size: 11px; + line-height: 14px; + } + } + } +} +.card-item-compact { + flex-direction: row; + padding: 5px 16px; + align-items: center; + gap: 8px; + min-height: unset; + .horizontal-scroll-card-list-item-info-single-id { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + word-break: break-all; + } +} diff --git a/src/pages/AIAgent/plugins/operator/horizontalScrollCard/HorizontalScrollCard.tsx b/src/pages/AIAgent/plugins/operator/horizontalScrollCard/HorizontalScrollCard.tsx new file mode 100644 index 00000000..3c0c1c72 --- /dev/null +++ b/src/pages/AIAgent/plugins/operator/horizontalScrollCard/HorizontalScrollCard.tsx @@ -0,0 +1,417 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import type { + HorizontalScrollCardItemInfoSingleProps, + HorizontalScrollCardProps, + HorizontalScrollCardScrollProps, + StatusCardListProps, +} from './HorizontalScrollCardType'; +import styles from './HorizontalScrollCard.module.scss'; +import classNames from 'classnames'; +import { + OutlineChevrondoubleleftIcon, + OutlineChevrondoublerightIcon, + OutlineHashtagIcon, +} from '@/assets/icon/outline'; +import { useCreation, useLongPress, useThrottleFn } from 'ahooks'; +import ReactResizeDetector from 'react-resize-detector'; +import { isBoolean } from 'lodash'; + +const getTextColor = (id: string) => { + switch (true) { + case id.includes('success'): + case id.includes('成功'): + case id.includes('succeeded'): + case id.includes('finished'): + return 'success'; + case id.includes('error'): + case id.includes('失败'): + case id.includes('错误'): + case id.includes('fatal'): + case id.includes('missed'): + case id.includes('miss'): + case id.includes('failed'): + case id.includes('panic'): + return 'error'; + default: + return ''; + } +}; + +const bgColorList = [ + 'purple', + 'bluePurple', + 'blue', + 'lakeBlue', + 'cyan', + 'orange', + 'yellow', +]; +const getBgColor = (id: string) => { + if (getTextColor(id) === 'success') { + return 'green'; + } + if (getTextColor(id) === 'error') { + return 'red'; + } + const random = Math.floor(Math.random() * 7); + return bgColorList[random]; +}; + +export const HorizontalScrollCard: React.FC = + React.memo((props) => { + const { + title, + data = [], + compact = false, + hiddenHeard, + className, + itemProps, + } = props; + const [scroll, setScroll] = useState({ + scrollLeft: 0, + scrollRight: 0, + }); + const [cardWidth, setCardWidth] = useState(0); + const [cardWidthWrapper, setCardWidthWrapper] = useState(0); + + const horizontalScrollCardWrapperRef = useRef(null); + const horizontalScrollCardRef = useRef(null); + const scrollLeftIconRef = useRef(null); + const scrollRightIconRef = useRef(null); + + useEffect(() => { + if (!cardWidth) return; + if (!cardWidthWrapper) return; + if (cardWidth >= cardWidthWrapper && scroll.scrollRight === 0) { + setScroll({ + ...scroll, + scrollRight: 1, + }); + } + if (cardWidthWrapper > cardWidth && scroll.scrollRight > 0) { + setScroll({ + ...scroll, + scrollRight: 0, + }); + } + }, [cardWidth, cardWidthWrapper]); + + useLongPress( + () => { + if (!scrollLeftIconRef.current) return; + if (!horizontalScrollCardRef.current) return; + horizontalScrollCardRef.current.scrollLeft = 0; + }, + scrollLeftIconRef, + { + delay: 300, + onClick: (e) => { + e.stopPropagation(); + if (!horizontalScrollCardRef.current) return; + horizontalScrollCardRef.current.scrollLeft -= 100; + }, + onLongPressEnd: () => { + if (!horizontalScrollCardRef.current) return; + horizontalScrollCardRef.current.scrollLeft = + horizontalScrollCardRef.current.scrollLeft + 0; + }, + }, + ); + useLongPress( + () => { + if (!horizontalScrollCardRef.current) return; + if (!scrollRightIconRef.current) return; + horizontalScrollCardRef.current.scrollLeft = + horizontalScrollCardRef.current.scrollWidth; + }, + scrollRightIconRef, + { + delay: 300, + onClick: (e) => { + e.stopPropagation(); + if (!horizontalScrollCardRef.current) return; + horizontalScrollCardRef.current.scrollLeft += 100; + }, + onLongPressEnd: () => { + if (!horizontalScrollCardRef.current) return; + horizontalScrollCardRef.current.scrollLeft = + horizontalScrollCardRef.current.scrollLeft - 0; + }, + }, + ); + const onScrollCardList = useThrottleFn( + () => { + if (horizontalScrollCardRef.current) { + const { scrollWidth, scrollLeft, clientWidth } = + horizontalScrollCardRef.current; + const scrollRight = scrollWidth - scrollLeft - clientWidth; + setScroll({ + scrollLeft: scrollLeft, + scrollRight: scrollRight, + }); + } + }, + { wait: 200 }, + ).run; + const isHiddenHeard = useCreation(() => { + if (isBoolean(hiddenHeard)) return !hiddenHeard; + return !compact; + }, [compact, hiddenHeard]); + return ( +
    + {isHiddenHeard && ( +
    + + {title} + +
    + {data.length} +
    +
    + )} +
    + { + if (!w || !h) { + return; + } + setCardWidthWrapper(w); + }} + handleWidth={true} + handleHeight={true} + refreshMode="debounce" + refreshRate={50} + /> + {scroll.scrollLeft > 0 && ( +
    + +
    + )} +
    + {data.map((cardItem) => ( + + {cardItem.info.length > 1 ? ( + + ) : ( + + )} + + ))} + { + if (!w || !h) { + return; + } + setCardWidth(w); + }} + handleWidth={true} + handleHeight={true} + refreshMode="debounce" + refreshRate={50} + /> +
    + {scroll.scrollRight > 0 && ( +
    + +
    + )} +
    +
    + ); + }); + +export const HorizontalScrollCardItemInfoMultiple: React.FC = + React.memo((props) => { + const { tag, info = [] } = props; + const bgColor = useMemo(() => { + return getBgColor('11'); + }, [tag]); + return ( +
    +
    + + {tag} +
    +
    + {info.map((ele) => ( +
    +
    + {ele.Data} +
    +
    + {ele.Id} +
    +
    + ))} +
    +
    + ); + }); + +export const HorizontalScrollCardItemInfoSingle: React.FC = + React.memo((props) => { + const { tag, item, compact, className = '', size } = props; + const isSuccess = useMemo(() => { + return getTextColor(tag || item.Id) === 'success'; + }, [tag, item.Id]); + const isError = useMemo(() => { + return getTextColor(tag || item.Id) === 'error'; + }, [tag, item.Id]); + const bgColor = useMemo(() => { + return getBgColor(tag || item.Id); + }, [tag, item.Id]); + return ( +
    + {compact ? ( + <> +
    + {tag || item.Id} +
    +
    + {item.Data} +
    + + ) : ( + <> +
    + {item.Data} +
    +
    + {tag || item.Id} +
    + + )} +
    + ); + }); diff --git a/src/pages/AIAgent/plugins/operator/horizontalScrollCard/HorizontalScrollCardType.d.ts b/src/pages/AIAgent/plugins/operator/horizontalScrollCard/HorizontalScrollCardType.d.ts new file mode 100644 index 00000000..18f0f951 --- /dev/null +++ b/src/pages/AIAgent/plugins/operator/horizontalScrollCard/HorizontalScrollCardType.d.ts @@ -0,0 +1,52 @@ +import type { YakitSizeType } from '@/compoments/yakitUI/YakitInputNumber/YakitInputNumberType' +import type { HoldGRPCStreamProps } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' + +type ItemProps = Omit +export interface HorizontalScrollCardProps { + /** 卡片的标题 */ + title?: string + /** 卡片数据源 */ + data: HoldGRPCStreamProps.InfoCards[] + /** 卡片标题数据横向排列 */ + compact?: boolean + /** 隐藏头部 */ + hiddenHeard?: boolean + /** 自定义类名 */ + className?: string + /** 单个项 */ + itemProps?: ItemProps +} + +export interface StatusCardListProps { + /** 具体信息 */ + info: HoldGRPCStreamProps.InfoCard[] + tag: string + /** 单个项 */ + itemProps?: ItemProps +} +/** 插件返回的 CacheStatusCardProps */ +export interface StatusCardProps { + /** 描述信息 */ + Id: string + /** 具体数值信息 */ + Data: string + /** 时间 */ + Timestamp: number + Tags?: string +} + +/** info只有一条数据时,展示的组件对应的props */ +export interface HorizontalScrollCardItemInfoSingleProps { + item: StatusCardProps + tag: string + compact: boolean + className?: string + size?: YakitSizeType +} +/** 滚动信息记录 */ +export interface HorizontalScrollCardScrollProps { + /** 滚动条距离左边的距离 */ + scrollLeft: number + /** 滚动条距离右边的距离 */ + scrollRight: number +} diff --git a/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeard.module.scss b/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeard.module.scss new file mode 100644 index 00000000..aeb51a67 --- /dev/null +++ b/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeard.module.scss @@ -0,0 +1,123 @@ +@use "../../plugins.scss"; +.plugin-head-executing-wrapper { + @extend %display-flex-center; + justify-content: flex-end; + .plugin-head-executing { + @extend %display-flex-center; + justify-content: flex-end; + flex: 1; + // min-width: 280px; + flex-shrink: 0; + gap: 8px; + } +} + +.plugin-execute-form-item { + width: 100%; +} + +.plugin-execute-form-wrapper { + display: grid; + grid-template-rows: 1fr; + transition: all 0.3s ease; + padding: 24px 12px 0; + :global { + .ant-form { + overflow: hidden; + } + .json-schema-form { + display: block; + .json-schema-row-form { + .ant-form-item-label { + flex: 0 0 25% !important; + } + .ant-form-item-label + .ant-form-item-control { + flex: 0 0 50% !important; + } + // 需询问后端是否可从schema中移除 + #root_auth__error, + #root_proxy__error { + display: none; + } + } + } + } + .plugin-execute-form-operate { + @extend %display-flex-center; + .plugin-execute-form-operate-start { + margin-right: 8px; + } + } +} +.plugin-execute-form-wrapper-hidden { + grid-template-rows: 0fr; + padding-top: 0; +} +.code-wrapper { + width: 100%; + margin-bottom: 0px !important; + :global { + .ant-form-item-control-input-content { + height: 300px; + border: 1px solid var(--Colors-Use-Neutral-Border); + } + } +} +.code-error-wrapper { + :global { + .ant-card-bordered { + border: 0; + } + .ant-form-item-control-input-content { + border-radius: 2px; + border: 1px solid var(--Colors-Use-Error-Primary); + &:focus-within { + box-shadow: 0 0 0 2px var(--Colors-Use-Error-Bg-Hover); + } + } + } +} + +.plugin-execute-progress-wrapper { + @extend %display-flex-center; + flex: 1; + flex-shrink: 0; + gap: 4px; + max-width: 200px; + :global { + .ant-progress-line { + min-width: 120px; + } + .ant-progress-show-info .ant-progress-outer { + margin-right: calc(-2.5em - 8px); + padding-right: calc(2.5em + 8px); + } + .ant-progress-text { + width: 2.5em; + } + } + .plugin-execute-progress-name { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + line-height: 16px; /* 133.333% */ + max-width: 80px; + flex: 1; + display: flex; + } +} + +.plugin-head-executing-progress { + @extend %display-flex-center; + flex-wrap: wrap; + gap: 16px; + padding: 12px; + padding-bottom: 0; +} + +.expand-retract { + // padding-top: 0; + .expand-retract-animation-wrapper { + top: 85%; + } +} diff --git a/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeard.tsx b/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeard.tsx new file mode 100644 index 00000000..e7fd20f0 --- /dev/null +++ b/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeard.tsx @@ -0,0 +1,1015 @@ +// import React, { useEffect, useMemo, useRef, useState } from 'react' +// import { +// ExecuteEnterNodeByPluginParamsProps, +// FormExtraSettingProps, +// OutputFormComponentsByTypeProps, +// PluginExecuteDetailHeardProps, +// PluginExecuteExtraFormValue, +// CustomPluginExecuteFormValue, +// PluginExecuteProgressProps, +// YakExtraParamProps, +// FormContentItemByTypeProps, +// PluginFixFormParamsProps, +// RequestType, +// } from './LocalPluginExecuteDetailHeardType' +// import { PluginDetailHeader } from '../../baseTemplate' +import styles from './LocalPluginExecuteDetailHeard.module.scss' +// import { useCreation, useDebounceFn, useInViewport, useMemoizedFn, useNetwork } from 'ahooks' +// import { Divider, Form, Input, Progress } from 'antd' +// import { PluginParamDataEditorProps, YakParamProps } from '../../pluginsType' +// import { YakitInput } from '@/compoments/yakitUI/YakitInput/YakitInput' +// import { YakitInputNumber } from '@/compoments/yakitUI/YakitInputNumber/YakitInputNumber' +// import { YakitSwitch } from '@/compoments/yakitUI/YakitSwitch/YakitSwitch' +// import { HTTPPacketYakitEditor } from '@/compoments/yakitUI/YakitEditor/extraYakitEditor' +// import { +// YakitFormDragger, +// YakitFormDraggerContent, +// YakitFormDraggerContentPath, +// } from '@/compoments/yakitUI/YakitForm/YakitForm' +// import { failed } from '@/utils/notification' +// import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton' +// import classNames from 'classnames' +// import { YakitSelect } from '@/compoments/yakitUI/YakitSelect/YakitSelect' +// import { YakitSelectProps } from '@/compoments/yakitUI/YakitSelect/YakitSelectType' +// import { OutlineInformationcircleIcon } from '@/assets/icon/outline' +// import { YakExecutorParam } from '@/pages/invoker/YakExecutorParams' +// import { PluginExecuteExtraParamsRefProps } from './PluginExecuteExtraParams' +// import { DebugPluginRequest, apiCancelDebugPlugin, apiDebugPlugin, apiFetchOnlinePluginInfo } from '../../utils' +// import { YakitEditor } from '@/compoments/yakitUI/YakitEditor/YakitEditor' +// import { YakitRadioButtons } from '@/compoments/yakitUI/YakitRadioButtons/YakitRadioButtons' +// import { GetPluginLanguage } from '../../builtInData' +// import { ParamsToGroupByGroupName, getValueByType, getYakExecutorParam } from '../../editDetails/utils' +// import { ExpandAndRetract } from '../expandAndRetract/ExpandAndRetract' +// import { defPluginExecuteFormValue } from './constants' +// import { YakitAutoComplete } from '@/compoments/yakitUI/YakitAutoComplete/YakitAutoComplete' +// import { grpcFetchExpressionToResult } from '@/pages/pluginHub/utils/grpc' +// import { getJsonSchemaListResult, JsonFormWrapper } from '@/compoments/JsonFormWrapper/JsonFormWrapper' +// import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' +// import { JSONParseLog } from '@/utils/tool' + +import { YakitFormDragger, YakitFormDraggerContent } from '@/compoments/yakitUI/YakitForm/YakitForm' +import type { FormContentItemByTypeProps, FormExtraSettingProps } from './LocalPluginExecuteDetailHeardType' +import React from 'react' +import { OutputFormComponentsByType } from '@/pages/Plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeard' +import { failed } from '@/utils/notification' + +// const PluginExecuteExtraParams = React.lazy(() => import('./PluginExecuteExtraParams')) + +// /** 插件执行头部 */ +// export const LocalPluginExecuteDetailHeard: React.FC = React.memo((props) => { +// const { +// token, +// plugin, +// extraNode, +// debugPluginStreamEvent, +// progressList, +// setRuntimeId, +// runtimeId, +// executeStatus, +// setExecuteStatus, +// linkPluginConfig, +// onDownPlugin, +// isHiddenUUID, +// infoExtra, +// hiddenUpdateBtn, +// } = props + +// const [form] = Form.useForm() +// const requestType = Form.useWatch('requestType', form) + +// /**是否显示更新按钮 */ +// const [isShowUpdate, setIsShowUpdate] = useState(false) + +// const [isExpand, setIsExpand] = useState(true) +// /**额外参数弹出框 */ +// const [extraParamsVisible, setExtraParamsVisible] = useState(false) +// const [extraParamsValue, setExtraParamsValue] = useState({ +// ...defPluginExecuteFormValue, +// }) + +// const [customExtraParamsValue, setCustomExtraParamsValue] = useState({}) + +// const pluginExecuteExtraParamsRef = useRef() +// const localPluginExecuteDetailHeardRef = useRef(null) + +// const jsonSchemaListRef = useRef<{ +// [key: string]: any +// }>({}) + +// const [inViewport = true] = useInViewport(localPluginExecuteDetailHeardRef) +// const networkState = useNetwork() + +// /**必填的参数,作为页面上主要显示 */ +// const requiredParams: YakParamProps[] = useMemo(() => { +// return plugin.Params?.filter((ele) => ele.Required) || [] +// }, [plugin.Params]) +// /** 选填参数数据 */ +// const customParams: YakParamProps[] = useMemo(() => { +// return plugin.Params?.filter((ele) => !ele.Required) || [] +// }, [plugin.Params]) +// /**额外参数,根据参数组分类 */ +// const extraParamsGroup: YakExtraParamProps[] = useMemo(() => { +// const paramsList = plugin.Params?.filter((ele) => !ele.Required) || [] +// return ParamsToGroupByGroupName(paramsList) +// }, [plugin.Params]) +// useEffect(() => { +// if (['yak', 'lua', 'mitm', 'codec'].includes(plugin.Type)) { +// initFormValue() +// } else { +// form.resetFields() +// } +// }, [plugin.Params, plugin.ScriptName, plugin.Type]) +// useEffect(() => { +// if (inViewport && networkState.online) { +// getOnlinePlugin() +// } +// }, [inViewport, networkState.online]) +// const isExecuting = useCreation(() => { +// if (executeStatus === 'process') return true +// return false +// }, [executeStatus]) +// /**本地插件和内置插件不做更新相关逻辑 */ +// const getOnlinePlugin = useMemoizedFn(() => { +// if (!!plugin.isLocalPlugin) return +// if (!!plugin.IsCorePlugin) return +// apiFetchOnlinePluginInfo({ uuid: plugin.UUID }, true).then((info) => { +// if (Number(info.updated_at || 0) > Number(plugin.UpdatedAt || 0)) { +// setIsShowUpdate(true) +// } +// }) +// }) +// /**初始表单初始值 */ +// const initFormValue = useMemoizedFn(() => { +// initRequiredFormValue() +// initExtraFormValue() +// }) +// const initRequiredFormValue = useMemoizedFn(() => { +// // 必填参数 +// let initRequiredFormValue: CustomPluginExecuteFormValue = { ...defPluginExecuteFormValue, requestType: 'input' } +// requiredParams.forEach((ele) => { +// const value = getValueByType(ele.DefaultValue, ele.TypeVerbose) +// initRequiredFormValue = { +// ...initRequiredFormValue, +// [ele.Field]: value, +// } +// }) +// form.setFieldsValue({ ...initRequiredFormValue }) +// }) +// const initExtraFormValue = useMemoizedFn(() => { +// // 额外参数 +// let initExtraFormValue: CustomPluginExecuteFormValue = {} +// const extraParamsList = plugin.Params?.filter((ele) => !ele.Required) || [] +// extraParamsList.forEach((ele) => { +// const value = getValueByType(ele.DefaultValue, ele.TypeVerbose) +// initExtraFormValue = { +// ...initExtraFormValue, +// [ele.Field]: value, +// } +// }) +// switch (plugin.Type) { +// case 'yak': +// case 'lua': +// case 'mitm': +// case 'codec': +// setCustomExtraParamsValue({ ...initExtraFormValue }) +// break + +// default: +// break +// } +// }) +// /**yak/lua 根据后端返的生成;codec/mitm/port-scan/nuclei前端固定*/ +// const pluginParamsNodeByPluginType = useMemoizedFn((type: string) => { +// switch (type) { +// case 'yak': +// case 'lua': +// return ( +// +// ) +// case 'codec': +// const codecItem: YakParamProps = { +// Field: 'Input', +// FieldVerbose: 'Input', +// Required: true, +// TypeVerbose: 'yak', +// DefaultValue: '', +// Help: 'Input', +// } +// return ( +// <> +// {plugin.Params.length > 0 && requiredParams.length > 0 ? ( +// +// ) : null} +// +// +// ) +// case 'mitm': +// return ( +// <> +// {plugin.Params.length > 0 && requiredParams.length > 0 ? ( +// +// ) : null} +// +// +// ) +// case 'port-scan': +// case 'nuclei': +// return +// default: +// return <> +// } +// }) +// /**开始执行 */ +// const onStartExecute = useMemoizedFn((value) => { +// let yakExecutorParams: YakExecutorParam[] = [] +// yakExecutorParams = getYakExecutorParam({ ...value, ...customExtraParamsValue }) +// const input = value['Input'] +// const result = getJsonSchemaListResult(jsonSchemaListRef.current) + +// if (result.jsonSchemaError.length > 0) { +// failed(`jsonSchema校验失败`) +// return +// } +// result.jsonSchemaSuccess.forEach((item) => { +// yakExecutorParams.push({ +// Key: item.key, +// Value: JSON.stringify(item.value), +// }) +// }) + +// let executeParams: DebugPluginRequest = { +// Code: '', +// PluginType: plugin.Type, +// Input: input, +// HTTPRequestTemplate: { +// ...extraParamsValue, +// IsHttps: !!value.IsHttps, +// IsRawHTTPRequest: value.requestType === 'original', +// IsHttpFlowId: false, +// HTTPFlowId: [], +// RawHTTPRequest: value.rawHTTPRequest ? Buffer.from(value.rawHTTPRequest, 'utf8') : Buffer.from('', 'utf8'), +// }, +// ExecParams: yakExecutorParams, +// LinkPluginConfig: linkPluginConfig, +// PluginName: plugin.ScriptName, +// } +// debugPluginStreamEvent.reset() +// setRuntimeId('') +// apiDebugPlugin({ +// params: executeParams, +// token: token, +// pluginCustomParams: plugin.Params, +// }).then(() => { +// setExecuteStatus('process') +// setIsExpand(false) +// debugPluginStreamEvent.start() +// }) +// }) +// /**取消执行 */ +// const onStopExecute = useMemoizedFn((e) => { +// e.stopPropagation() +// apiCancelDebugPlugin(token).then(() => { +// debugPluginStreamEvent.stop() +// setExecuteStatus('finished') +// }) +// }) +// /**在顶部的执行按钮 */ +// const onExecuteInTop = useMemoizedFn((e) => { +// e.stopPropagation() +// form +// .validateFields() +// .then(onStartExecute) +// .catch(() => { +// setIsExpand(true) +// }) +// }) +// /**保存额外参数 */ +// const onSaveExtraParams = useMemoizedFn( +// (v: { customValue: CustomPluginExecuteFormValue; fixedValue: PluginExecuteExtraFormValue }) => { +// switch (plugin.Type) { +// case 'yak': +// case 'lua': +// setCustomExtraParamsValue({ ...v.customValue }) +// break +// case 'codec': +// case 'mitm': +// setCustomExtraParamsValue({ ...v.customValue }) +// setExtraParamsValue({ ...v.fixedValue }) +// break +// case 'port-scan': +// case 'nuclei': +// setExtraParamsValue({ ...v.fixedValue }) +// break +// default: +// break +// } +// setExtraParamsVisible(false) +// }, +// ) +// /**打开额外参数抽屉 */ +// const openExtraPropsDrawer = useMemoizedFn(() => { +// if (isExecuting) return +// setExtraParamsVisible(true) +// }) +// const isShowExtraParamsButton = useMemo(() => { +// switch (plugin.Type) { +// case 'codec': +// if (extraParamsGroup.length > 0) return true +// return false +// case 'mitm': +// if (extraParamsGroup.length > 0) return true +// return requestType === 'input' +// case 'port-scan': +// case 'nuclei': +// if (requestType !== 'input') return false +// return true +// default: +// return extraParamsGroup.length > 0 +// } +// }, [extraParamsGroup.length, plugin.Type, requestType]) +// const executeExtraParams: PluginExecuteExtraFormValue | CustomPluginExecuteFormValue = useMemo(() => { +// switch (plugin.Type) { +// case 'yak': +// case 'lua': +// return customExtraParamsValue +// case 'codec': +// case 'mitm': +// return { ...customExtraParamsValue, ...extraParamsValue } +// case 'port-scan': +// case 'nuclei': +// return extraParamsValue +// default: +// return {} +// } +// }, [plugin.Type, extraParamsValue, customExtraParamsValue]) +// const onExpand = useMemoizedFn((e) => { +// e.stopPropagation() +// setIsExpand(!isExpand) +// }) +// const onDown = useMemoizedFn((e) => { +// e.stopPropagation() +// onDownPlugin() +// }) + +// return ( +// <> +// +// +//
    +// {progressList.length === 1 && ( +// +// )} +// {isExecuting ? ( +// !isExpand && ( +// <> +// +// 停止 +// +// +// ) +// ) : ( +// <> +// {!isExpand && 执行} +// {extraNode} +// {!hiddenUpdateBtn && isShowUpdate && ( +// <> +//
    +// +// 更新 +// +// +// )} +// +// )} +//
    +//
    +// } +// img={plugin.HeadImg || ''} +// user={plugin.Author} +// pluginId={plugin.UUID} +// updated_at={plugin.UpdatedAt || 0} +// prImgs={(plugin.CollaboratorInfo || []).map((ele) => ({ +// headImg: ele.HeadImg, +// userName: ele.UserName, +// }))} +// type={plugin.Type} +// isHiddenUUID={isHiddenUUID} +// infoExtra={infoExtra} +// /> +//
    +//
    +//
    +// {pluginParamsNodeByPluginType(plugin.Type)} +// +//
    +// {isExecuting ? ( +// +// 停止 +// +// ) : ( +// +// 开始执行 +// +// )} +// {isShowExtraParamsButton && ( +// +// 额外参数 +// +// )} +//
    +//
    +// +//
    +// {progressList.length > 1 && ( +//
    +// {progressList.map((ele, index) => ( +// +// {index !== 0 && } +// +// +// ))} +//
    +// )} +// loading...}> +// +// +// +// ) +// }) + +// /**执行的入口通过插件参数生成组件 */ +// export const ExecuteEnterNodeByPluginParams: React.FC = React.memo((props) => { +// const { paramsList, pluginType, isExecuting, jsonSchemaListRef, jsonSchemaInitial } = props + +// return ( +// <> +// {paramsList.map((item) => ( +// +// +// +// ))} +// +// ) +// }) +// /**插件执行输入》输出form表单的组件item */ +export const FormContentItemByType: React.FC = React.memo((props) => { + const { item, disabled, pluginType, jsonSchemaListRef, jsonSchemaInitial } = props + let extraSetting: FormExtraSettingProps | undefined + try { + extraSetting = JSON.parse(item.ExtraSetting || '{}') || { + double: false, + data: [], + } + if (extraSetting && extraSetting.data) { + extraSetting.data = extraSetting.data.map((item) => { + return { + key: item?.key, + label: item?.label || item?.key || item?.value, + value: item?.value, + } + }) + } + } catch (error) { + failed('获取参数配置数据错误,请重新打开该页面') + } + switch (item.TypeVerbose) { + // 单选并获取文件内容 + case 'upload-file-content': + return ( + + ) + // 单选文件-路径 + case 'upload-path': + return ( + + ) + // 批量文件-路径 + case 'multiple-file-path': + return ( + + ) + // 单选文件夹-路径 + case 'upload-folder-path': + return ( + + ) + // 其他基础类型 + default: + return ( + + ) + } +}) + +// /**执行表单单个项 */ +// export const OutputFormComponentsByType: React.FC = (props) => { +// const { item, extraSetting, codeType, disabled, pluginType, jsonSchemaListRef, jsonSchemaInitial } = props +// const { t, i18n } = useI18nNamespaces(['yakitUi']) +// const [validateStatus, setValidateStatus] = useState<'success' | 'error'>('success') + +// const formProps = { +// rules: [{ required: item.Required }], +// label: item.FieldVerbose || item.Field, +// name: item.Field, +// className: styles['plugin-execute-form-item'], +// tooltip: item.Help +// ? { +// icon: , +// title: item.Help, +// } +// : null, +// } +// const onValidateStatus = useDebounceFn( +// (value: 'success' | 'error') => { +// setValidateStatus(value) +// }, +// { wait: 200, leading: true }, +// ).run + +// const [additionalConfig, setAdditionalConfig] = useState<{ inputOption: { label: string; value: any }[] }>() +// useEffect(() => { +// const { TypeVerbose, SuggestionDataExpression } = item +// setAdditionalConfig(undefined) +// if (TypeVerbose === 'string') { +// if (SuggestionDataExpression) { +// // 输入框提供可选择选项 +// grpcFetchExpressionToResult({ +// Expression: item.SuggestionDataExpression || '', +// ImportYaklangLibs: true, +// }) +// .then((res) => { +// const { BoolResult, Result } = res +// if (BoolResult && Result) { +// try { +// let arr: string[] = JSONParseLog(Result, { +// page: 'LocalPluginExecuteDetailHeard', +// fun: 'grpcFetchExpressionToResult', +// }) +// !Array.isArray(arr) && (arr = []) +// setAdditionalConfig({ +// inputOption: arr.map((item) => ({ +// label: item, +// value: item, +// })), +// }) +// } catch (error) {} +// } +// }) +// .catch(() => {}) +// } +// } +// }, [item]) + +// switch (item.TypeVerbose) { +// case 'string': +// return ( +// +// +// +// +// +// ) +// case 'text': +// return ( +// +// +// +// ) +// case 'uint': +// return ( +// { +// return String(value).replace(/\D/g, '') +// }} +// > +// +// +// ) +// case 'float': +// return ( +// +// +// +// ) +// case 'boolean': +// return ( +// +// +// +// ) +// case 'select': +// let selectProps: YakitSelectProps = { +// options: extraSetting?.data || [], +// } +// if (extraSetting?.double) { +// selectProps = { +// ...selectProps, +// mode: 'tags', +// } +// } +// return ( +// +// +// +// ) +// case 'http-packet': +// const defaultValue = item.DefaultValue || '' +// return ( +// { +// if (item.Required && value.length === 0) { +// onValidateStatus('error') +// return Promise.reject() +// } +// if (validateStatus === 'error') onValidateStatus('success') +// return Promise.resolve() +// }, +// }, +// ]} +// className={classNames(formProps.className, styles['code-wrapper'], { +// [styles['code-error-wrapper']]: validateStatus === 'error', +// })} +// initialValue={defaultValue} +// trigger="setValue" +// validateTrigger="setValue" +// validateStatus={validateStatus} +// help={validateStatus === 'error' ? t('YakitForm.field_required_with_label', { label: formProps.label }) : ''} +// > +// +// +// ) +// case 'yak': +// let language: string = pluginType || '' +// try { +// const info = JSONParseLog(item.ExtraSetting || '', { +// page: 'LocalPluginExecuteDetailHeard', +// fun: 'yak', +// }) as PluginParamDataEditorProps +// language = info?.language || pluginType || '' +// } catch (error) {} +// language = GetPluginLanguage(language || codeType || 'yak') + +// return ( +// { +// if (item.Required && value.length === 0) { +// onValidateStatus('error') +// return Promise.reject() +// } +// if (validateStatus === 'error') onValidateStatus('success') +// return Promise.resolve() +// }, +// }, +// ]} +// className={classNames(formProps.className, styles['code-wrapper'], { +// [styles['code-error-wrapper']]: validateStatus === 'error', +// })} +// initialValue={item.DefaultValue || ''} +// trigger="setValue" +// validateTrigger="setValue" +// validateStatus={validateStatus} +// help={validateStatus === 'error' ? t('YakitForm.field_required_with_label', { label: formProps.label }) : ''} +// > +// +// +// ) +// case 'json': +// if (typeof jsonSchemaListRef?.current !== 'object') return <> +// let schema: any = {} +// let uiSchema: any = {} +// let value: any = undefined +// try { +// schema = JSONParseLog(item?.JsonSchema || '{}', { page: 'LocalPluginExecuteDetailHeard', fun: 'schema' }) +// uiSchema = JSONParseLog(item?.UISchema || '{}', { page: 'LocalPluginExecuteDetailHeard', fun: 'uiSchema' }) +// if (jsonSchemaInitial && jsonSchemaInitial[item.Field]) { +// value = JSONParseLog(jsonSchemaInitial[item.Field], { +// page: 'LocalPluginExecuteDetailHeard', +// fun: 'jsonSchemaInitial', +// }) +// } +// } catch (error) { +// console.error('Parse JsonSchema failed:', error) +// } +// return ( +// +// ) +// default: +// return <> +// } +// } + +// export const PluginExecuteProgress: React.FC = React.memo((props) => { +// const { percent, name } = props +// return ( +//
    +//
    +// {name} +//
    +// `${percent}%`} +// /> +//
    +// ) +// }) +// /**固定的插件类型 mitm/port-scan/nuclei 显示的UI */ +// export const PluginFixFormParams: React.FC = React.memo((props) => { +// const { +// form, +// disabled, +// type = 'single', +// rawHTTPRequest = '', +// MockHTTPResponse = '', +// inputType, +// setInputType, +// isShowMockHTTPResponse, +// } = props +// const { t, i18n } = useI18nNamespaces(['plugin', 'yakitUi']) + +// const requestType: RequestType = Form.useWatch('requestType', form) +// const isShowResponse: boolean = Form.useWatch('simulationResponse', form) +// const rawItem = useMemo(() => { +// const codeItem: YakParamProps = { +// Field: 'rawHTTPRequest', +// FieldVerbose: t('PluginFixFormParams.data_packet'), +// Required: true, +// TypeVerbose: 'http-packet', +// DefaultValue: rawHTTPRequest, +// Help: '', +// } +// return codeItem +// }, [rawHTTPRequest, i18n.language]) + +// const mockItem = useMemo(() => { +// const codeItem: YakParamProps = { +// Field: 'mockHTTPResponse', +// FieldVerbose: t('PluginFixFormParams.data_packet'), +// Required: true, +// TypeVerbose: 'http-packet', +// DefaultValue: MockHTTPResponse, +// Help: '', +// } +// return codeItem +// }, [MockHTTPResponse, i18n.language]) + +// const requestTypeOptions = useCreation(() => { +// if (type === 'single') { +// return [ +// { +// value: 'original', +// label: t('PluginFixFormParams.original_request'), +// }, +// { +// value: 'input', +// label: t('PluginFixFormParams.request_configuration'), +// }, +// ] +// } +// return [ +// { +// value: 'original', +// label: t('PluginFixFormParams.original_request'), +// }, +// { +// value: 'input', +// label: t('PluginFixFormParams.request_configuration'), +// }, +// { +// value: 'httpFlowId', +// label: t('PluginFixFormParams.request_id'), +// }, +// ] +// }, [type, i18n.language]) +// return ( +// <> +// +// +// +// +// +// +// {requestType === 'original' && } +// {requestType === 'input' && ( +// <> +// {inputType && setInputType ? ( +// +// ) : ( +// +// )} +// +// )} +// {requestType === 'httpFlowId' && ( +// +// +// +// )} +// {isShowMockHTTPResponse && ( +// +// +// +// )} +// {isShowResponse && } +// +// ) +// }) diff --git a/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeardType.d.ts b/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeardType.d.ts new file mode 100644 index 00000000..9d8880e8 --- /dev/null +++ b/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeardType.d.ts @@ -0,0 +1,133 @@ +import type { YakScript } from '@/pages/invoker/schema' +import type { ReactNode } from 'react' +import type { YakParamProps } from '../../pluginsType' +import type { KVPair } from '@/models/kv' +import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' +import type { FormInstance } from 'antd' +import type { ExpandAndRetractExcessiveState } from '../expandAndRetract/ExpandAndRetract' + +export interface HTTPRequestBuilderParams { + IsHttps: boolean + + IsRawHTTPRequest: boolean + RawHTTPRequest: Uint8Array | Buffer + + Method: string + Input?: string + + Path: string[] + GetParams: KVPair[] + Headers: KVPair[] + Cookie: KVPair[] + + Body: Uint8Array + PostParams: KVPair[] + MultipartParams: KVPair[] + MultipartFileParams: KVPair[] + + IsHttpFlowId: boolean + HTTPFlowId: number[] +} + +export interface JsonFormSchemaListWrapper { + /** JsonSchema数据收集(PS:此处可能存在多个内部Form因此采用Ref数组的形式依次校验) */ + jsonSchemaListRef?: React.MutableRefObject<{ + [key: string]: any + }> + /** JsonSchema 默认值(PS:此值为对象 key值用于寻找对应默认值) */ + jsonSchemaInitial?: { + [key: string]: any + } +} + +export interface PluginExecuteDetailHeardProps { + token: string + /** 插件 */ + plugin: YakScript + /** 头部右侧额外dom */ + extraNode: ReactNode + debugPluginStreamEvent: { + start: () => void + stop: () => void + cancel: () => void + reset: () => void + } + progressList: StreamResult.Progress[] + runtimeId: string + setRuntimeId: (b: string) => void + executeStatus: ExpandAndRetractExcessiveState + setExecuteStatus: (b: ExpandAndRetractExcessiveState) => void + /** 插件UI联动相关参数 */ + linkPluginConfig?: HybridScanPluginConfig + onDownPlugin: () => void + /** 隐藏插件 ID */ + isHiddenUUID?: boolean + infoExtra?: ReactNode + /** 隐藏更新按钮 */ + hiddenUpdateBtn?: boolean +} + +export interface YakExtraParamProps { + /** 组名 */ + group: string + /** 组内的参数 */ + data: YakParamProps[] +} + +export interface ExecuteEnterNodeByPluginParamsProps extends JsonFormSchemaListWrapper { + paramsList: YakParamProps[] + pluginType?: string + isExecuting: boolean +} + +export interface OutputFormComponentsByTypeProps extends JsonFormSchemaListWrapper { + item: YakParamProps + extraSetting?: FormExtraSettingProps + /** 根据插件类型出编辑器类型/或者自己输入对应的编辑器类型 */ + codeType?: string + disabled?: boolean + /** 插件类型 */ + pluginType?: string +} + +export interface FormExtraSettingProps { + double: boolean + data: { key: string; label: string; value: string }[] +} + +export interface PluginExecuteProgressProps { + percent: number + name: string +} + +export interface PluginExecuteExtraFormValue extends HTTPRequestBuilderParams { + /** 前端使用,请求类型的选择 */ + requestType: RequestType + /** 前端使用,请求类型》原始请求:数据包 */ + rawHTTPRequest: string +} +/** 表单的key value类型 */ +export interface CustomPluginExecuteFormValue { + [key: string]: number | string | boolean | string[] | Uint8Array | KVPair[] | number[] +} + +export interface FormContentItemByTypeProps extends JsonFormSchemaListWrapper { + item: YakParamProps + pluginType?: string + disabled?: boolean +} + +export interface PluginFixFormParamsProps { + type?: 'single' | 'batch' + form: FormInstance + disabled: boolean + /** 原始请求中的数据包数据 */ + rawHTTPRequest?: string + /** 模拟响应中的数据包数据 */ + MockHTTPResponse?: string + inputType?: 'content' | 'path' + setInputType?: (v: 'content' | 'path') => void + isShowMockHTTPResponse?: boolean +} + +export type RequestType = 'original' | 'input' | 'httpFlowId' diff --git a/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/PluginExecuteExtraParams.module.scss b/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/PluginExecuteExtraParams.module.scss new file mode 100644 index 00000000..491ba817 --- /dev/null +++ b/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/PluginExecuteExtraParams.module.scss @@ -0,0 +1,108 @@ +%icon { + color: var(--Colors-Use-Main-Primary); +} +.plugin-execute-extra-params-drawer { + :global { + .ant-drawer-body { + padding: 16px 12px; + } + .ant-form-item-with-help .ant-form-item-explain { + font-size: 12px; + color: var(--Colors-Use-Neutral-Text-4-Help-text); + line-height: 16px; + min-height: 16px; + margin-top: 2px; + } + .ant-form-item-label > label { + color: var(--Colors-Use-Neutral-Text-1-Title); + } + // .ant-collapse-header-text { + // width: 100%; + // } + .ant-form-item { + margin-bottom: 8px; + } + } + .plugin-execute-extra-params-drawer-title { + display: flex; + justify-content: space-between; + align-items: center; + .plugin-execute-extra-params-drawer-title-text { + font-weight: 600; + font-size: 14px; + line-height: 20px; + color: var(--Colors-Use-Neutral-Text-1-Title); + } + .plugin-execute-extra-params-drawer-title-btns { + display: flex; + gap: 8px; + } + } + .to-end { + text-align: center; + color: var(--Colors-Use-Neutral-Disable); + font-size: 11px; + padding-top: 12px; + padding-bottom: 24px; + } +} +.plugin-extra-params { + .plus-icon { + svg { + @extend %icon; + } + } + .kv-params-wrapper { + & > :global(.ant-collapse-item .ant-collapse-content > .ant-collapse-content-box) { + // padding-right: 0; + } + .variable-list-wrapper { + & > :global(.ant-collapse-item .ant-collapse-content > .ant-collapse-content-box) { + padding-right: 16px; + } + } + } +} +.extra-params-node-type { + :global { + .ant-collapse-item:first-child .ant-collapse-header { + border-top: 0; + } + } +} + +.yakit-panel-heard { + display: flex; + justify-content: center; + align-items: center; + .yakit-panel-heard-number { + display: flex; + margin-left: 4px; + padding: 2px 6px; + border-radius: 8px; + background: var(--Colors-Use-Basic-Background); + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 10px; + line-height: 12px; /* 120% */ + } +} + +.additional-params-divider { + margin-bottom: 16px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 4px; + .text-style { + color: #b4bbca; + font-size: 12px; + font-weight: 600; + line-height: 16px; + } + .divider-style { + flex: 1; + overflow: hidden; + height: 1px; + border-top: 1px dashed #eaecf3; + } +} diff --git a/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/PluginExecuteExtraParams.tsx b/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/PluginExecuteExtraParams.tsx new file mode 100644 index 00000000..3acce0c5 --- /dev/null +++ b/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/PluginExecuteExtraParams.tsx @@ -0,0 +1,468 @@ +import React, { useMemo } from 'react' +import styles from './PluginExecuteExtraParams.module.scss' +import type { FormInstance } from 'antd' +import type { YakExtraParamProps, JsonFormSchemaListWrapper } from './LocalPluginExecuteDetailHeardType' +import { FormContentItemByType } from './LocalPluginExecuteDetailHeard' +import YakitCollapse from '@/compoments/yakitUI/YakitCollapse/YakitCollapse' +import classNames from 'classnames' + +const { YakitPanel } = YakitCollapse + +// type ExtraParamsValue = PluginExecuteExtraFormValue | CustomPluginExecuteFormValue +// interface PluginExecuteExtraParamsProps extends JsonFormSchemaListWrapper { +// ref?: any +// pluginType: string +// /** 选填参数数据 */ +// customPluginParams?: YakParamProps[] +// /** 是否隐藏固定参数UI */ +// hiddenFixedParams?: boolean +// extraParamsValue: ExtraParamsValue +// extraParamsGroup: YakExtraParamProps[] +// visible: boolean +// setVisible: (b: boolean) => void +// onSave: (value: { customValue: CustomPluginExecuteFormValue; fixedValue: PluginExecuteExtraFormValue }) => void +// } + +export interface PluginExecuteExtraParamsRefProps { + form: FormInstance +} +// const PluginExecuteExtraParams: React.FC = React.memo( +// React.forwardRef((props, ref) => { +// const { +// pluginType, +// customPluginParams = [], +// hiddenFixedParams, +// extraParamsGroup = [], +// extraParamsValue, +// visible, +// setVisible, +// onSave, +// jsonSchemaListRef, +// } = props + +// const [form] = Form.useForm() + +// const pathRef: React.MutableRefObject = useRef({ +// onGetRemoteValues: () => {}, +// onSetRemoteValues: (s: string[]) => {}, +// }) + +// useImperativeHandle(ref, () => ({ form }), [form]) +// useEffect(() => { +// if (visible) { +// form.setFieldsValue({ ...extraParamsValue }) +// } +// }, [visible, extraParamsValue]) +// const onClose = useMemoizedFn(() => { +// onSaveSetting() +// }) +// /** +// * @description 保存高级配置 +// */ +// const onSaveSetting = useMemoizedFn(() => { +// switch (pluginType) { +// case 'yak': +// case 'lua': +// form.validateFields().then((formValue: CustomPluginExecuteFormValue) => { +// onSave({ customValue: formValue, fixedValue: { ...defPluginExecuteFormValue } }) +// }) +// break +// case 'codec': +// case 'mitm': +// form.validateFields().then((formValue: HTTPRequestBuilderParams) => { +// if (formValue.Path) { +// pathRef.current.onSetRemoteValues(formValue.Path) +// } +// onSave(splitPluginParamsData(formValue, customPluginParams)) +// }) +// break +// case 'port-scan': +// case 'nuclei': +// form.validateFields().then((formValue: HTTPRequestBuilderParams) => { +// if (formValue.Path) { +// pathRef.current.onSetRemoteValues(formValue.Path) +// } +// onSave({ customValue: {}, fixedValue: formValue as PluginExecuteExtraFormValue }) +// }) +// break +// default: +// break +// } +// }) + +// /** yak/lua根据后端返的生成;mitm/port-scan/nuclei/codec前端固定; */ +// const pluginParamsNodeByPluginType = (type: string) => { +// switch (type) { +// case 'yak': +// case 'lua': +// return ( +//
    +// +//
    已经到底啦~
    +// +// ) +// case 'codec': +// case 'mitm': +// return ( +//
    +// {extraParamsGroup.length > 0 && ( +// <> +//
    +//
    自定义参数 (非必填)
    +//
    +//
    +// +// +// )} +// {!hiddenFixedParams && ( +// <> +//
    +//
    固定参数 (非必填)
    +//
    +//
    +// +// +// )} +//
    已经到底啦~
    +// +// ) +// case 'port-scan': +// case 'nuclei': +// return ( +//
    +// +//
    已经到底啦~
    +// +// ) + +// default: +// return <> +// } +// } +// /**重置固定的额外参数中的表单值 */ +// const onReset = useMemoizedFn((restValue) => { +// form.setFieldsValue({ ...restValue }) +// }) +// return ( +// +// {pluginParamsNodeByPluginType(pluginType)} +// +// ) +// }), +// ) +// export default PluginExecuteExtraParams + +interface ExtraParamsNodeByTypeProps extends JsonFormSchemaListWrapper { + extraParamsGroup: YakExtraParamProps[] + pluginType: string + // 是否应用默认值 + isDefaultActiveKey?: boolean + wrapperClassName?: string +} +export const ExtraParamsNodeByType: React.FC = React.memo((props) => { + const { extraParamsGroup, pluginType, jsonSchemaListRef, isDefaultActiveKey = true, wrapperClassName } = props + const defaultActiveKey = useMemo(() => { + if (!isDefaultActiveKey) return undefined + return extraParamsGroup.map((ele) => ele.group) + }, [extraParamsGroup, isDefaultActiveKey]) + return ( + + {extraParamsGroup.map((item) => ( + + {item.data?.map((formItem) => ( + + + + ))} + + ))} + + ) +}) + +// interface FixExtraParamsNodeProps { +// pathRef: React.MutableRefObject +// form: FormInstance +// onReset: (fields) => void + +// /** YakitCollapse的bordered 插件调试页面的样式需要该属性 */ +// bordered?: boolean +// /** HTTP方法和请求路径的wrapper样式 插件调试页面的样式需要该属性 */ +// httpPathWrapper?: string +// } +// type Fields = keyof HTTPRequestBuilderParams +// export const FixExtraParamsNode: React.FC = React.memo((props) => { +// const { onReset, pathRef, form, bordered, httpPathWrapper } = props +// const { t, i18n } = useI18nNamespaces(['plugin', 'yakitUi']) +// const [activeKey, setActiveKey] = useState(['GET 参数']) + +// const getParamsRef = useRef() +// const postParamsRef = useRef() +// const headersRef = useRef() +// const cookieRef = useRef() + +// const getParams = Form.useWatch('GetParams', form) +// const postParams = Form.useWatch('PostParams', form) +// const headers = Form.useWatch('Headers', form) +// const cookie = Form.useWatch('Cookie', form) + +// // 重置 +// const handleReset = ( +// e: React.MouseEvent, +// field: Fields, +// ref: React.MutableRefObject, +// ) => { +// e.stopPropagation() +// onReset({ +// [field]: [{ Key: '', Value: '' }], +// }) +// ref.current.setVariableActiveKey(['0']) +// } +// // 添加 +// const handleAdd = ( +// e: React.MouseEvent, +// field: Fields, +// actKey: string, +// ref: React.MutableRefObject, +// ) => { +// e.stopPropagation() +// const v = form.getFieldsValue() +// const variables = (v[field] || []) as KVPair[] +// const index = variables.findIndex((ele: KVPair) => !ele || (!ele.Key && !ele.Value)) +// if (index === -1) { +// onReset({ +// [field]: [...variables, { Key: '', Value: '' }], +// }) +// ref.current.setVariableActiveKey([...(ref.current.variableActiveKey || []), `${variables?.length || 0}`]) +// } else { +// yakitFailed(t('YakitNotification.complete_variable_before_add', { index })) +// } +// if (activeKey?.findIndex((ele) => ele === actKey) === -1) { +// setActiveKey([...activeKey, actKey]) +// } +// } +// // 删除 +// const handleRemove = (i: number, field: Fields) => { +// const v = form.getFieldsValue() +// const variables = (v[field] || []) as KVPair[] +// variables.splice(i, 1) +// onReset({ +// [field]: [...variables], +// }) +// } +// return ( +//
    +//
    +// +// ({ +// value: item, +// label: item, +// }))} +// size="small" +// /> +// +// +// ({ value: item, label: item }))} +// mode="tags" +// placeholder={t('YakitInput.please_enter')} +// cacheHistoryDataKey={RemotePluginGV.LocalExecuteExtraPath} +// isCacheDefaultValue={false} +// size="small" +// /> +// +//
    +// setActiveKey(key as string[])} +// bordered={!!bordered} +// className={styles['kv-params-wrapper']} +// > +// +// {t('FixExtraParamsNode.get_parameters')} +// {getParams?.length ? {getParams?.length} : ''} +//
    +// } +// key="GET 参数" +// extra={ +// <> +// handleReset(e, 'GetParams', getParamsRef)} +// size="small" +// > +// {t('YakitButton.reset')} +// +// +// handleAdd(e, 'GetParams', 'GET 参数', getParamsRef)} +// style={{ paddingRight: 0 }} +// size="small" +// > +// {t('YakitButton.add')} +// +// +// +// } +// > +// { +// handleRemove(i, 'GetParams') +// }} +// collapseWrapperClassName={styles['variable-list-wrapper']} +// /> +// +// +// {t('FixExtraParamsNode.post_parameters')} +// {postParams?.length ? ( +// {postParams?.length} +// ) : ( +// '' +// )} +// +// } +// key="POST 参数" +// extra={ +// <> +// handleReset(e, 'PostParams', postParamsRef)} +// size="small" +// > +// {t('YakitButton.reset')} +// +// +// handleAdd(e, 'PostParams', 'POST 参数', postParamsRef)} +// style={{ paddingRight: 0 }} +// size="small" +// > +// {t('YakitButton.add')} +// +// +// +// } +// forceRender={true} +// > +// { +// handleRemove(i, 'PostParams') +// }} +// collapseWrapperClassName={styles['variable-list-wrapper']} +// /> +// +// +// Header +// {headers?.length ? {headers?.length} : ''} +// +// } +// key="Header" +// extra={ +// <> +// handleReset(e, 'Headers', headersRef)} +// size="small" +// > +// {t('YakitButton.reset')} +// +// +// handleAdd(e, 'Headers', 'Header', headersRef)} +// style={{ paddingRight: 0 }} +// size="small" +// > +// {t('YakitButton.add')} +// +// +// +// } +// forceRender={true} +// > +// { +// handleRemove(i, 'Headers') +// }} +// collapseWrapperClassName={styles['variable-list-wrapper']} +// /> +// +// +// Cookie +// {cookie?.length ? {cookie?.length} : ''} +// +// } +// key="Cookie" +// extra={ +// <> +// handleReset(e, 'Cookie', cookieRef)} +// size="small" +// > +// {t('YakitButton.reset')} +// +// +// handleAdd(e, 'Cookie', 'Cookie', cookieRef)} +// style={{ paddingRight: 0 }} +// size="small" +// > +// {t('YakitButton.add')} +// +// +// +// } +// forceRender={true} +// > +// { +// handleRemove(i, 'Cookie') +// }} +// collapseWrapperClassName={styles['variable-list-wrapper']} +// /> +// +// +// +// ) +// }) diff --git a/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/constants.ts b/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/constants.ts new file mode 100644 index 00000000..c39e6354 --- /dev/null +++ b/src/pages/AIAgent/plugins/operator/localPluginExecuteDetailHeard/constants.ts @@ -0,0 +1,20 @@ +import type { PluginExecuteExtraFormValue } from './LocalPluginExecuteDetailHeardType' + +export const defPluginExecuteFormValue: PluginExecuteExtraFormValue = { + IsHttps: false, + IsRawHTTPRequest: false, + RawHTTPRequest: Buffer.from('', 'utf-8'), + Method: 'GET', + Path: [], + GetParams: [], + Headers: [], + Cookie: [], + Body: Buffer.from('', 'utf-8'), + PostParams: [], + MultipartParams: [], + MultipartFileParams: [], + IsHttpFlowId: false, + HTTPFlowId: [], + requestType: 'original', + rawHTTPRequest: '', +} diff --git a/src/pages/AIAgent/plugins/operator/pluginExecuteResult/PluginExecuteResult.module.scss b/src/pages/AIAgent/plugins/operator/pluginExecuteResult/PluginExecuteResult.module.scss new file mode 100644 index 00000000..8e635523 --- /dev/null +++ b/src/pages/AIAgent/plugins/operator/pluginExecuteResult/PluginExecuteResult.module.scss @@ -0,0 +1,161 @@ +@use "../../plugins.scss"; +.plugin-execute-result { + @extend %display-column-center; + justify-content: flex-start; + flex: 1; + overflow: hidden; + gap: 12px; + padding-top: 12px; + min-height: 500px; + .plugin-execute-result-wrapper { + @extend %display-column-center; + gap: 12px; + } + + .plugin-execute-result-tabBar { + display: inline-block; + margin-left: 4px; + padding: 0px 8px; + background: #f6544a; + border-radius: 8px; + color: #fff; + font-size: 12px; + font-weight: 400; + line-height: 16px; + } + + .plugin-execute-result-tabPane { + border: 1px solid var(--Colors-Use-Neutral-Border); + border-top: 0; + } + + .table-container { + border: 0 !important; + border-top: 1px solid var(--Colors-Use-Neutral-Border) !important; + } + .custom-table-container { + border: 0 !important; + } +} +.table-container { + border: 0 !important; + border-top: 1px solid var(--Colors-Use-Neutral-Border) !important; +} +.plugin-execute-result-tab-content { + @extend %display-column-center; + justify-content: flex-start; + height: 100%; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + line-height: 16px; /* 133.333% */ + overflow: hidden; + .plugin-execute-result-tab-content-head { + @extend %display-flex-center; + justify-content: space-between; + padding: 14px 12px 10px; + border-bottom: 1px solid var(--Colors-Use-Neutral-Border); + } +} +.plugin-execute-result-tab-content-body { + padding: 12px; + overflow: overlay; +} +.plugin-execute-http-flow { + @extend %display-flex-center; + align-items: flex-start; + flex: 1; + height: 100%; + .plugin-execute-web-tree { + @extend %display-column-center; + justify-content: flex-start; + height: 100%; + padding: 12px; + padding-right: 0; + } + .current-http-table-container { + border: 0 !important; + border-top: 1px solid var(--Colors-Use-Neutral-Border) !important; + } +} + +.plugin-execute-custom-table { + padding: 0; +} + +.plugin-execute-port-table-container { + border: 0 !important; +} +.plugin-execute-port-table-detail-body { + border-left: 0; + border-right: 0; + border-bottom: 0; +} + +.table-title { + color: var(--Colors-Use-Neutral-Disable); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; /* 133.333% */ + &-number { + color: var(--Colors-Use-Main-Primary); + margin-left: 4px; + } +} + +.plugin-execute-port-table { + @extend %display-flex-center; + align-items: flex-start; + gap: 8px; +} + +.plugin-execute-log { + .log-tab-name { + @extend %display-flex-center; + .tab-number { + @extend %display-flex-center; + padding: 2px 6px; + background-color: var(--Colors-Use-Neutral-Bg); + border-radius: 8px; + margin-left: 8px; + + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 10px; + font-style: normal; + font-weight: 400; + line-height: 12px; + text-align: center; + } + } + .log-tab-name-active { + .tab-number { + background-color: var(--Colors-Use-Main-Primary); + color: #fff; + } + } +} + +.risks-table { + @extend %display-column-center; + overflow: hidden; + height: 100%; + .risks-table-wrapper { + padding: 0; + } +} + +.table-renderTitle { + @extend %display-flex-center; + justify-content: space-between; + padding: 14px 12px 10px; + + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + line-height: 16px; /* 133.333% */ + .table-renderTitle-left { + @extend %display-flex-center; + gap: 8px; + } +} \ No newline at end of file diff --git a/src/pages/AIAgent/plugins/operator/pluginExecuteResult/PluginExecuteResult.tsx b/src/pages/AIAgent/plugins/operator/pluginExecuteResult/PluginExecuteResult.tsx new file mode 100644 index 00000000..e4d5a853 --- /dev/null +++ b/src/pages/AIAgent/plugins/operator/pluginExecuteResult/PluginExecuteResult.tsx @@ -0,0 +1,892 @@ +import PluginTabs from '@/compoments/businessUI/PluginTabs/PluginTabs' +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { HorizontalScrollCard } from '../horizontalScrollCard/HorizontalScrollCard' +import styles from './PluginExecuteResult.module.scss' +import type { + AuditHoleTableOnTabProps, + PluginExecuteCodeProps, + PluginExecuteCustomTableProps, + PluginExecuteLogProps, + PluginExecutePortTableProps, + PluginExecuteResultProps, + PluginExecuteResultTabContentProps, + PluginExecuteWebsiteTreeProps, + VulnerabilitiesRisksTableProps, +} from './PluginExecuteResultType' +import { YakitButton } from '@/compoments/yakitUI/YakitButton/YakitButton' +import { + useControllableValue, + useCreation, + useDebounceEffect, + useDebounceFn, + useInterval, + useMemoizedFn, + useUpdateEffect, +} from 'ahooks' +import emiter from '@/utils/eventBus/eventBus' +import type { RouteToPageProps } from '@/pages/AIAgent/types/interface/publicMenu' +import { YakitRoute } from '@/pages/AIAgent/enums/yakitRoute' +import { TableVirtualResize } from '@/compoments/TableVirtualResize/TableVirtualResize' +import type { SortProps } from '@/compoments/TableVirtualResize/TableVirtualResizeType' +import { formatJson } from '@/pages/AIAgent/utils/base' +// import { EngineConsole } from '@/compoments/baseConsole/BaseConsole'; +import { WebTree } from '@/compoments/WebTree/WebTree' +import classNames from 'classnames' +import ReactResizeDetector from 'react-resize-detector' +import { YakitResizeBox } from '@/compoments/yakitUI/YakitResizeBox/YakitResizeBox' +// import { ExportExcel } from '@/compoments/DataExport/DataExport'; +import type { HoldGRPCStreamProps, StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' +import { YakitEditor } from '@/compoments/yakitUI/YakitEditor/YakitEditor' +import { yakitFailed } from '@/utils/notification' +import { YakitSpin } from '@/compoments/yakitUI/YakitSpin/YakitSpin' +import { OutlineChartpieIcon, OutlineLogIcon, OutlineTerminalIcon } from '@/assets/icon/outline' +// import { LocalList, LocalPluginLog, LocalText } from './LocalPluginLog'; +import { ErrorBoundary } from 'react-error-boundary' +import type { QueryGeneralRequest } from '@/pages/AIAgent/enums/invoker/schema' + +export interface Paging { + Page: number + Limit: number + Order?: 'asc' | 'desc' | string + OrderBy?: 'created_at' | 'updated_at' | string + RawOrder?: string +} + +export interface QueryRisksRequest { + Pagination: Paging + Search: string + Network: string + Ports: string + RiskType: string + Token: string + WaitingVerified: boolean + Severity: string + FromId: number + UntilId: number + Tags: string + BeforeCreatedAt?: number + AfterCreatedAt?: number + /** 全部'' 已读:'true',未读:'false' */ + IsRead: string + Title: string + Ids: number[] + + /** 前端展示使用 列表 */ + RiskTypeList?: string[] + /** 前端展示使用 */ + SeverityList?: string[] + /** 前端展示使用 */ + TagList?: string[] + /** IP段 */ + IPList?: string[] + /** 前端展示使用 */ + CreatedAt?: number[] + + RuntimeId?: string + RuntimeIds?: string[] +} + +export const defQueryRisksRequest: QueryRisksRequest = { + Pagination: { Page: 1, Limit: 20, OrderBy: 'id', Order: 'desc' }, + Search: '', + Network: '', + Ports: '', + RiskType: '', + Token: '', + WaitingVerified: false, + Severity: '', + FromId: 0, + UntilId: 0, + Tags: '', + IsRead: '', // 全部'' 已读:'true',未读:'false' + Title: '', + RiskTypeList: [], + SeverityList: [], + TagList: [], + IPList: [], + Ids: [], + RuntimeId: '', + RuntimeIds: [], +} + +export interface QueryPortsRequest extends QueryGeneralRequest { + Hosts: string + Ports: string + State: 'open' | 'closed' | 'unknown' + Service: string + Title: string + TitleEffective: boolean + Keywords: string + ComplexSelect: string + RuntimeId: string + AfterId?: number + BeforeId?: number + All?: boolean + Order?: string + OrderBy?: string +} + +const { TabPane } = PluginTabs + +export const PluginExecuteResult: React.FC = React.memo((props) => { + const { + streamInfo, + runtimeId, + loading, + defaultActiveKey, + pluginExecuteResultWrapper = '', + PluginTabsRightNode, + isCrawler = false, + } = props + const [allTotal, setAllTotal] = useState(0) + const [tempTotal, setTempTotal] = useState(0) // 在risk表没有展示之前得临时显示在tab上得小红点计数 + const [interval, setInterval] = useState(1000) + + useUpdateEffect(() => { + setAllTotal(0) + setTempTotal(0) + if (streamInfo.tabsState.find((item) => item.type === 'ssa-risk')) { + setInterval(undefined) + } else { + setInterval(1000) + } + }, [runtimeId]) + useInterval(() => { + if (runtimeId) getTotal() + }, interval) + + const getTotal = useMemoizedFn(() => { + // apiQueryRisksTotalByRuntimeId(runtimeId).then((allRes) => { + // if (+allRes.Total > 0) { + // setTempTotal(+allRes.Total); + // } + // }); + }) + + /** + * 漏洞风险tab没有点击之前,tabContent不会渲染展示;不会请求数据 + * 强制渲染得话,组件内部不会请求数据 + * 采取:没有点击漏洞风险tab之前,由外面根据runtimeId查询是否有数据,有数据就展示对应得tab,以里面传出来得total为准,total>0后停止外面得useInterval, + */ + const onSetRiskTotal = useMemoizedFn((total) => { + if (total > 0) { + setAllTotal(total) + if (interval) setInterval(undefined) + } + }) + + const renderTabContent = useMemoizedFn((ele: HoldGRPCStreamProps.InfoTab) => { + switch (ele.type) { + case 'risk': + return runtimeId ? ( + + ) : null + case 'port': + return runtimeId ? : null + case 'http': + return runtimeId ? ( + + ) : null + case 'log': + return + case 'console': { + console.log('EngineConsole:', 123) + // return ; + return null + } + case 'table': + // eslint-disable-next-line no-case-declarations + let tableInfo: HoldGRPCStreamProps.InfoTable = streamInfo.tabsInfoState[ele.tabName] || { + columns: [], + data: [], + name: '', + } + // 过滤掉数据中含有对象与数组的数据 + tableInfo.data = tableInfo.data.filter((item) => + Object.values(item).every( + // eslint-disable-next-line max-nested-callbacks + (value) => !(typeof value === 'object'), + ), + ) + return + case 'text': + // eslint-disable-next-line no-case-declarations + const textInfo: HoldGRPCStreamProps.InfoText = streamInfo.tabsInfoState[ele.tabName] || { + content: '', + } + return + case 'result': + // eslint-disable-next-line no-case-declarations + // const { customProps } = ele; + // + return null + case 'ssa-risk': + return + default: + return null + } + }) + + const showTabs = useMemo(() => { + if (!tempTotal && !streamInfo.tabsState.find((item) => item.type === 'ssa-risk')) { + return streamInfo.tabsState.filter((item) => item.tabName !== '漏洞与风险') + } + return streamInfo.tabsState + }, [streamInfo.tabsState, tempTotal]) + + const tabBarRender = useMemoizedFn((tab: HoldGRPCStreamProps.InfoTab, length: number) => { + if (tab.type === 'risk') { + return ( + <> + {tab.tabName} + {length} + + ) + } + + return tab.tabName + }) + const cardState = useCreation(() => { + return streamInfo.cardState.filter((item) => item.tag !== 'no display') + }, [streamInfo.cardState]) + const showRiskTotal = useCreation(() => { + if (allTotal > 0) return allTotal + return tempTotal + }, [allTotal, tempTotal]) + return ( +
    + {cardState.length > 0 && ( +
    + +
    + )} + {showTabs.length > 0 && ( + + {showTabs.map((ele) => ( + + {renderTabContent(ele)} + + ))} + + )} +
    + ) +}) + +export const defQueryPortsRequest: QueryPortsRequest = { + Hosts: '', + Ports: '', + State: 'open', + Service: '', + Title: '', + TitleEffective: false, + Keywords: '', + ComplexSelect: '', + RuntimeId: '', + Pagination: { + Limit: 20, + Page: 1, + OrderBy: 'id', + Order: 'desc', + }, +} +const PluginExecutePortTable: React.FC = React.memo(() => { + // const { runtimeId } = props; + // const [params, setParams] = useState({ + // ...cloneDeep(defQueryPortsRequest), + // RuntimeId: runtimeId, + // }); + // const onJumpPort = useMemoizedFn(() => { + // const info: RouteToPageProps = { + // route: YakitRoute.DB_Ports, + // }; + // emiter.emit('menuOpenPage', JSON.stringify(info)); + // }); + return ( + // } + // size="small" + // onClick={onJumpPort} + // > + // 端口资产管理 + //
    + // } + // containerClassName={ + // styles['plugin-execute-port-table-container'] + // } + // btnSize="small" + // detailBodyClassName={ + // styles['plugin-execute-port-table-detail-body'] + // } + // /> + null + ) +}) +/** HTTP 流量 */ + +export const PluginExecuteHttpFlow: React.FC = React.memo((props) => { + const { + runtimeId, + // filterTagDom, + website = false, + // isCrawler = false, + } = props + + const [height, setHeight] = useState(300) // 表格所在div高度 + + const webTreeRef = useRef() + // 被点击的树节点URL参数 + // const [includeInUrl, setIncludeInUrl] = useState([]); + // const [treeQueryparams, setTreeQueryparams] = useState(''); + // const [refreshTreeFlag, setRefreshTreeFlag] = useState(false); + // 流量表筛选条件 改变 控制webtree刷新 + // const onQueryParams = useMemoizedFn( + // (queryParams: string, execFlag?: boolean) => { + // try { + // const treeQuery = JSON.parse(queryParams); + // delete treeQuery.IncludeInUrl; + // setTreeQueryparams(JSON.stringify(treeQuery)); + // setRefreshTreeFlag(!!execFlag); + // } catch (error) {} + // }, + // ); + + return ( +
    + + { + if (!w || !h) { + return + } + setHeight(h) + }} + handleHeight={true} + refreshMode="debounce" + refreshRate={50} + /> + + // setIncludeInUrl( + // selectKeys.map((i) => String(i)), + // ) + () => {} + } + runTimeId={runtimeId} + /> +
    + } + secondNode={ + // + null + } + /> + + ) +}) +/** 基础插件信息 / 日志 */ +export const PluginExecuteLog: React.FC = React.memo((props) => { + const { messageList, wrapperClassName } = props // loading + const [activeKey, setActiveKey] = useState('plugin-log') + + // const list: StreamResult.Log[] = useCreation(() => { + // return ( + // (messageList || []) + // .filter((i) => { + // return !( + // (i?.level || '').startsWith('json-feature') || + // (i?.level || '').startsWith('feature-') + // ); + // }) + // // .splice(0, 25) + // .reverse() + // ); + // }, [messageList]); + + const echartsLists: StreamResult.Log[] = useCreation(() => { + return messageList.filter((ele) => ele.level === 'json-graph') + }, [messageList]) + const textLists: StreamResult.Log[] = useCreation(() => { + const textTypes = ['text', 'code'] + return messageList.filter((ele) => textTypes.includes(ele.level)) + }, [messageList]) + const logTabs = useCreation(() => { + const tab = [ + { + name: '插件日志', + icon: , + number: 0, + type: 'plugin-log', + }, + ] + if (echartsLists.length) { + tab.push({ + name: '统计图表', + icon: , + number: echartsLists.length, + type: 'echarts-statistics', + }) + } + if (textLists.length) { + tab.push({ + name: '输出文本', + icon: , + number: textLists.length, + type: 'output-text', + }) + } + return tab + }, [echartsLists, textLists]) + + const renderTabContent = useMemoizedFn((type) => { + switch (type) { + case 'plugin-log': + // eslint-disable-next-line no-case-declarations + // const currentTime = moment().format('YYYY-MM-DD'); + return ( + // {currentTime} 日志查询结果} + // /> + null + ) + case 'echarts-statistics': + // return ; + return null + case 'output-text': + // return ; + return null + default: + return null + } + }) + const onTabChange = useMemoizedFn((key: string) => { + setActiveKey(key) + }) + + return ( + + {logTabs.map((ele) => ( + + {ele.icon} {ele.name} + {!!ele.number &&
    {ele.number}
    } + + } + key={ele.type} + > + {renderTabContent(ele.type)} +
    + ))} +
    + ) +}) + +/** 风险与漏洞tab表 */ +export const VulnerabilitiesRisksTable: React.FC = React.memo((props) => { + const { runtimeId, runTimeIDs, filterTagDom } = props + const [riskLoading, setRiskLoading] = useState(false) + const [allTotal, setAllTotal] = useControllableValue(props, { + defaultValue: 0, + valuePropName: 'allTotal', + trigger: 'setAllTotal', + }) + const [query, setQuery] = useState({ + ...defQueryRisksRequest, + RuntimeId: runtimeId, + RuntimeIds: runTimeIDs, + }) + + useUpdateEffect(() => { + setQuery((pre: any) => ({ ...pre, RuntimeId: runtimeId })) + }, [runtimeId]) + + useUpdateEffect(() => { + setQuery((pre: any) => ({ ...pre, RuntimeIds: runTimeIDs })) + }, [runTimeIDs]) + + const onJumpRisk = useMemoizedFn(() => { + const info: RouteToPageProps = { + route: YakitRoute.DB_Risk, + } + emiter.emit('menuOpenPage', JSON.stringify(info)) + }) + return ( +
    + + {/* +
    + 风险与漏洞 + + {filterTagDom} +
    + + 查看全部 + +
    + } + riskWrapperClassName={styles['risks-table-wrapper']} + tableVirtualResizeProps={{ + containerClassName: styles['table-container'], + titleHeight: 44, + rowSelection: undefined, + }} + yakitRiskDetailsBorder={false} + excludeColumnsKey={['action']} + allTotal={allTotal} + setAllTotal={setAllTotal} + /> */} + + + ) +}) + +/** 审计漏洞tab表 */ +export const AuditHoleTableOnTab: React.FC = React.memo((props) => { + const { runtimeId } = props + const [allTotal, setAllTotal] = useState(0) + + const onJumpAuditHole = useMemoizedFn(() => { + emiter.emit( + 'openPage', + JSON.stringify({ + route: YakitRoute.YakRunner_Audit_Hole, + params: { + RuntimeID: [runtimeId], + }, + }), + ) + }) + + const query = useMemo(() => { + return { + RuntimeID: [runtimeId], + } + }, [runtimeId]) + return ( +
    + +
    + 风险与漏洞 + +
    + + 查看全部 + +
    + } + riskWrapperClassName={styles['risks-table-wrapper']} + /> + + ) +}) + +/** 插件执行的tab content 结构 */ +const PluginExecuteResultTabContent: React.FC = React.memo((props) => { + const { title, extra, children, className = '' } = props + return ( +
    + {(title || extra) && ( +
    +
    {title}
    +
    {extra}
    +
    + )} +
    {children}
    +
    + ) +}) + +const PluginExecuteCustomTable: React.FC = React.memo((props) => { + const { + tableInfo: { columns = [], data = [], name = '' }, + } = props + const [tableData, setTableData] = useState(data) + const [columnsData, setColumnsData] = useState(columns) + + const [sorterTable, setSorterTable] = useState() + + const [query, setQuery] = useState({}) // 设置表头查询条件 + const [loading, setLoading] = useState(false) + + const firstItemRef = useRef() + + useEffect(() => { + setTimeout(() => { + const item = data[0] + firstItemRef.current = data[0] + onSetColumns(item) + }, 500) + }, []) + useDebounceEffect( + () => { + if (!firstItemRef.current) { + firstItemRef.current = data[0] + onSetColumns(firstItemRef.current) + } + queryData() + }, + [data], + { wait: 300, leading: true }, + ) + useUpdateEffect(() => { + update() + }, [query, sorterTable]) + const onSetColumns = useMemoizedFn((item) => { + if (!item) return + const newColumns = columns.map((ele) => ({ + ...ele, + sorterProps: { + sorter: !Number.isNaN(Number(item[ele.dataKey])), + }, + filterProps: { + filtersType: 'input', + }, + })) + setColumnsData(newColumns) + }) + const update = useDebounceFn( + () => { + setLoading(true) + new Promise((resolve, reject) => { + try { + queryData() + resolve(true) + } catch (error) { + reject(error) + } + }) + .catch((e) => { + yakitFailed('搜索失败:' + e) + }) + .finally(() => { + setTimeout(() => { + setLoading(false) + }, 200) + }) + }, + { + wait: 200, + }, + ).run + // 搜索 + const queryData = useMemoizedFn(() => { + try { + let list: any = [] + const length = data.length + const queryHaveValue = {} + // 找出有查询条件 + // eslint-disable-next-line guard-for-in + for (const key in query) { + const objItem = query[key] + if (objItem) { + // @ts-ignore + queryHaveValue[key] = query[key] + } + } + // 所有查询条件为空时,返回原始数据 + if (Object.getOwnPropertyNames(queryHaveValue).length === 0) { + list = [...data] + } else { + // 搜索 + for (let index = 0; index < length; index++) { + const elementArrayItem = data[index] + let isAdd: boolean[] = [] + // eslint-disable-next-line guard-for-in + for (const key in queryHaveValue) { + // @ts-ignore + const objItem = queryHaveValue[key] + const isHave = `${elementArrayItem[key]}`.includes(objItem) + isAdd.push(isHave) + } + // 所有条件都满足 + if (!isAdd.includes(false)) { + list.push(elementArrayItem) + } + isAdd = [] + } + } + const newDataTable = sorterTable?.order === 'none' ? list : sorterFunction(list, sorterTable, '') || [] + setTableData(newDataTable) + } catch (error) { + yakitFailed('搜索失败:' + error) + } + }) + const getData = useMemoizedFn(() => { + return new Promise((resolve) => { + const header = columns.map((ele) => ele.title) + const exportData = formatJson(header, data) + const params = { + header, + exportData, + response: { + Pagination: { + Page: 1, + }, + Data: data, + Total: data.length, + }, + } + resolve(params) + }) + }) + const onTableChange = useMemoizedFn((_: number, __: number, sorter: SortProps, filters: any) => { + setQuery(filters) + setSorterTable(sorter) + }) + return ( + { + if (!error) { + return
    未知错误
    + } + return ( +
    +

    弹框内逻辑性崩溃,请关闭重试!

    +
    {error?.message}
    +
    + ) + }} + > + + Total + {data.length} + + } + extra={ + null + // + } + className={styles['plugin-execute-custom-table']} + > + {}, + }} + columns={columnsData} + containerClassName={styles['custom-table-container']} + onChange={onTableChange} + /> + +
    + ) +}) + +const PluginExecuteCode: React.FC = React.memo((props) => { + const { content } = props + return +}) diff --git a/src/pages/AIAgent/plugins/operator/pluginExecuteResult/PluginExecuteResultType.d.ts b/src/pages/AIAgent/plugins/operator/pluginExecuteResult/PluginExecuteResultType.d.ts new file mode 100644 index 00000000..4b58577a --- /dev/null +++ b/src/pages/AIAgent/plugins/operator/pluginExecuteResult/PluginExecuteResultType.d.ts @@ -0,0 +1,145 @@ +import type { + HoldGRPCStreamInfo, + HoldGRPCStreamProps, + StreamResult, +} from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType'; +import type { ReactNode } from 'react'; + +export interface PluginExecuteResultProps { + runtimeId: string; + streamInfo: HoldGRPCStreamInfo; + loading: boolean; + defaultActiveKey?: string; + pluginExecuteResultWrapper?: string; + PluginTabsRightNode?: ReactNode; + isCrawler?: boolean; +} + +export interface VulnerabilitiesRisksTableProps { + runtimeId?: string; + runTimeIDs?: string[]; + filterTagDom?: ReactNode; + allTotal?: number; + setAllTotal?: (n: number) => void; +} + +export interface AuditHoleTableOnTabProps { + runtimeId: string; +} + +export interface PluginExecuteLogProps { + loading: boolean; + messageList: StreamResult.Log[]; + wrapperClassName?: string; +} + +export interface PluginExecuteResultTabContentProps { + title?: ReactNode; + extra?: ReactNode; + children?: ReactNode; + className?: string; +} + +export interface PluginExecuteWebsiteTreeProps { + runtimeId: string; + website?: boolean; + filterTagDom?: React.ReactNode; + isCrawler?: boolean; +} +export interface PluginExecutePortTableProps { + runtimeId: string; +} + +export interface PluginExecuteCustomTableProps { + tableInfo: HoldGRPCStreamProps.InfoTable; +} + +export interface PluginExecuteCodeProps { + content: string; +} + +export enum FileActionEnum { + Read_Action = 'READ', // 读取 + Write_Action = 'WRITE', // 写入 + Create_Action = 'CREATE', // 创建 + Delete_Action = 'DELETE', // 删除 + Status_Action = 'STATUS', // 查看元信息 + Chmod_Action = 'CHMOD', // 修改权限 + Find_Action = 'FIND', // 查找 +} +export declare namespace PluginExecuteLogFile { + export type FileActionMessage = + | ReadFileActionMessage + | WriteFileActionMessage + | CreateFileActionMessage + | DELETEFileActionMessage + | STATUSFileActionMessage + | CHMODFileActionMessage + | FINDFileActionMessage; + + export interface FileItem { + action: FileActionType; + action_message: FileActionMessage; + + dir: string; + is_dir: boolean; + path: string; + title: string; + } + + export type FileActionType = `${FileActionEnum}`; + export interface ReadFileActionMessage { + content: string; + length: number; + message: string; + offset: number; + unit: string; + } + export interface WriteFileActionMessage { + content: string; + length: number; + message: string; + mode: string; + } + export interface CreateFileActionMessage { + chmodMode: string; + isDir: boolean; + message: string; + } + export interface DELETEFileActionMessage { + message: string; + isDir: boolean; + } + export interface STATUSFileActionMessage { + message: string; + status: { + FileAttributes: number; + CreationTime: { + LowDateTime: number; + HighDateTime: number; + }; + LastAccessTime: { + LowDateTime: number; + HighDateTime: number; + }; + LastWriteTime: { + LowDateTime: number; + HighDateTime: number; + }; + FileSizeHigh: number; + FileSizeLow: number; + ReparseTag: number; + }; + } + + export interface CHMODFileActionMessage { + message: string; + chmodMode: string; + } + export interface FINDFileActionMessage { + condition: string; + content: string[]; + message: string; + mode: string; + } +} diff --git a/src/pages/AIAgent/plugins/plugins.scss b/src/pages/AIAgent/plugins/plugins.scss new file mode 100644 index 00000000..c3c60f5a --- /dev/null +++ b/src/pages/AIAgent/plugins/plugins.scss @@ -0,0 +1,309 @@ +%display-flex-center { + display: flex; + align-items: center; +} +%display-column-center { + display: flex; + flex-direction: column; + justify-content: center; +} +// -------------------- 审核状态图标颜色 ↓↓↓ -------------------- +.aduit-status-solid-circle-color { + color: var(--Colors-Use-Main-Primary); +} +.aduit-status-badge-check-color { + color: var(--Colors-Use-Green-Primary); +} +.aduit-status-ban-color { + color: var(--Colors-Use-Neutral-Disable); +} +.aduit-status-flag-color { + color: var(--Colors-Use-Blue-Primary); +} +// -------------------- tooltip组件样式 ↓↓↓ -------------------- +.plugins-tooltip { + .ant-tooltip-inner { + display: flex; + padding: 8px 12px; + border-radius: 4px; + background: var(--Colors-Use-Neutral-Text-1-Title); + border: 1px solid var(--Colors-Use-Neutral-Text-1-Title); + color: var(--Colors-Use-Basic-Background); + box-shadow: 0px 8px 16px 0px var(--Colors-Use-Basic-Shadow); + // border-bottom-width: 0px; + + font-size: 12px; + line-height: 16px; + cursor: text; + } +} +.plugins-tooltip.ant-tooltip-placement-top { + padding-bottom: 4px; + .ant-tooltip-arrow { + bottom: 1px; + } +} +.plugins-tooltip.ant-tooltip-placement-topLeft { + padding-bottom: 4px; + .ant-tooltip-arrow { + bottom: 1px; + left: 1px; + } +} +.plugins-tooltip.ant-tooltip-placement-topRight { + padding-bottom: 4px; + .ant-tooltip-arrow { + right: 1px; + bottom: 1px; + } +} +.plugins-tooltip.ant-tooltip-placement-bottom { + padding-top: 4px; + .ant-tooltip-arrow { + top: 1px; + } +} +.plugins-tooltip.ant-tooltip-placement-bottomLeft { + padding-top: 4px; + .ant-tooltip-arrow { + top: 1px; + left: 1px; + } +} +.plugins-tooltip.ant-tooltip-placement-bottomRight { + padding-top: 4px; + .ant-tooltip-arrow { + right: 1px; + top: 1px; + } +} +.plugins-tooltip.ant-tooltip-placement-right { + padding-left: 4px; + .ant-tooltip-arrow { + left: 1px; + + } +} +// -------------------- form组件样式 ↓↓↓ -------------------- +.plugins-form { + width: 100%; + .ant-row.ant-form-item { + gap: 2px; + .ant-form-item-label { + padding-bottom: 0; + } + .ant-form-item-label > label.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before { + display: none; + } + } +} +.plugins-item-required { + margin: 0 4px; + padding-top: 4px; + color: var(--Colors-Use-Error-Primary); + font-size: 14px; + line-height: 14px; +} +// -------------------- radio组件样式 ↓↓↓ -------------------- +.plugins-radio-wrapper { + .ant-radio:hover .ant-radio-inner { + border-color: var(--Colors-Use-Main-Border); + } + .ant-radio-checked .ant-radio-inner { + border-color: var(--Colors-Use-Main-Border); + } + .ant-radio-checked::after { + border-color: var(--Colors-Use-Main-Border); + } + .ant-radio-inner::after { + background-color: var(--Colors-Use-Main-Border); + } + .ant-radio-input:focus + .ant-radio-inner { + box-shadow: 0 0 0 3px var(--Colors-Use-Main-Focus); + } + .ant-radio.ant-radio-disabled .ant-radio-inner { + background-color: var(--Colors-Use-Neutral-Bg); + border-color: var(--Colors-Use-Neutral-Border); + } + .ant-radio.ant-radio-checked.ant-radio-disabled .ant-radio-inner { + border-color: var(--Colors-Use-Neutral-Border); + background-color: transparent; + } + .ant-radio.ant-radio-checked.ant-radio-disabled .ant-radio-inner:after { + background-color: var(--Colors-Use-Neutral-Bg); + } +} +// -------------------- anchor组件样式 ↓↓↓ -------------------- +.plugins-anchor { + &.ant-anchor-wrapper { + margin-left: 0; + padding-left: 0; + overflow: hidden; + } + + .ant-anchor { + display: flex; + .ant-anchor-link { + padding: 0; + } + .ant-anchor-ink { + display: none; + } + } +} + +// -------------------- 列表上方的header右侧样式 -------------------- +.divider-style { + width: 1px; + height: 16px; + background-color: var(--Colors-Use-Neutral-Bg); +} + +.extra-header-wrapper { + display: flex; + align-items: center; + gap: 16px; + + .divider-style { + width: 1px; + height: 16px; + background-color: var(--Colors-Use-Neutral-Bg); + } + + .btn-group-wrapper { + display: flex; + align-items: center; + gap: 8px; + } +} +// -------------------- + +// -------------------- 详情中列表item样式 -------------------- + +.plugin-details-opt-wrapper { + .plugin-details-item-wrapper { + width: 100%; + height: 46px; + padding-left: 6px; + padding-right: 8px; + display: flex; + align-items: center; + border-radius: 4px; + border: 1px solid transparent; + + .plugin-details-item { + flex: 1 0 0; + overflow: hidden; + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-top: 1px solid var(--Colors-Use-Neutral-Border); + border-bottom: 1px solid transparent; + .plugin-details-item-info { + flex: 1 0 0; + overflow: hidden; + display: flex; + align-items: center; + gap: 8px; + padding-left: 2px; + &-text-style { + flex: 1 0 0; + overflow: hidden; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + line-height: 16px; + } + } + .plugin-details-item-show { + min-width: 80px; + display: flex; + justify-content: right; + align-items: center; + gap: 8px; + &-icon-style { + svg { + width: 16px; + height: 16px; + color: var(--Colors-Use-Neutral-Text-3-Secondary); + } + } + } + } + .plugin-details-item-active { + border-color: transparent; + } + } + .plugin-details-item-wrapper-active { + border-color: var(--Colors-Use-Main-Border); + background: var(--Colors-Use-Main-Focus); + } + .plugin-details-item-wrapper-enableClick { + cursor: pointer; + &:hover { + background-color: var(--Colors-Use-Neutral-Bg-Hover); + .plugin-details-item { + border-color: transparent; + } + } + } +} +.plugin-details-opt-wrapper-enableClick:hover + .plugin-details-opt-wrapper-enableClick { + .plugin-details-item { + border-color: transparent; + } +} +.plugin-details-opt-wrapper-enableClick:has(.plugin-details-item-wrapper-active) + + .plugin-details-opt-wrapper-enableClick { + .plugin-details-item { + border-color: transparent; + } +} +// 详情-插件列表单项-源码弹窗 +.terminal-popover { + width: 429px; + padding-bottom: 4px; + padding-top: 4px; + + .ant-popover-inner-content { + height: 40vh; + padding: 4px; + } + + .ant-popover-inner { + border-radius: 4px; + } + + .ant-popover-arrow { + left: 0; + } +} +// -------------------- + +// -------------------- 详情-插件列表-过滤条件 +.details-filter-extra-wrapper { + display: inline-flex; + align-items: center; + gap: 4px; + path { + stroke-width: 1.5; + } + .loading-icon { + width: 26px; + text-align: center; + color: var(--Colors-Use-Main-Primary); + svg { + width: 16px; + height: 16px; + } + } +} + +// -------------------- + +.icon-svg-16 { + svg { + width: 16px; + height: 16px; + } +} diff --git a/src/pages/AIAgent/plugins/pluginsType.ts b/src/pages/AIAgent/plugins/pluginsType.ts new file mode 100644 index 00000000..916bf885 --- /dev/null +++ b/src/pages/AIAgent/plugins/pluginsType.ts @@ -0,0 +1,147 @@ +import type { YaklangInformation } from '@/utils/monacoSpec/yakEditor'; + +export interface GRPCRange { + Code: string; + StartLine: number; + StartColumn: number; + EndLine: number; + EndColumn: number; +} + +/** ---------- 前端插件数据结构定义 Start ---------- */ +/** 插件基础信息 */ +export interface PluginBaseParamProps { + /** 插件名称 */ + ScriptName: string; + /** 插件描述 */ + Help?: string; + /** 漏洞类型详情 */ + RiskDetail?: YakRiskInfoProps[]; + /** 插件tags */ + Tags?: string[]; +} + +/** 插件配置信息 */ +export interface PluginSettingParamProps { + /** 是否启用插件联动 */ + EnablePluginSelector?: boolean; + /** 联动插件类型 */ + PluginSelectorTypes?: string; + /** 源码 */ + Content: string; +} + +/** 插件参数-下拉框-配置数据 */ +export interface PluginParamDataSelectProps { + double?: boolean; + data: { label: string; value: string }[]; +} +/** 插件参数-编辑器-配置数据 */ +export interface PluginParamDataEditorProps { + language?: string; +} + +/** 可编辑插件配置数据 */ +export interface PluginDataProps { + ScriptName: string; + Type: string; + Help?: string; + /** 漏洞类型详情 */ + RiskDetail?: YakRiskInfoProps[]; + Tags?: string; + Params?: YakParamProps[]; + EnablePluginSelector?: boolean; + PluginSelectorTypes?: string; + Content: string; + /** 环境变量 */ + PluginEnvKey?: string[]; + /** 修改插件的原因 */ + modifyDescription?: string; +} +/** ---------- 前端插件数据结构定义 End ---------- */ + +/** ---------- grpc插件数据结构定义 Start ---------- */ +/** 插件参数配置数据 */ +export interface YakParamProps { + /** 参数名 */ + Field: string; + /** 参数展示名 */ + FieldVerbose: string; + /** 是否必填 */ + Required?: boolean; + /** 参数类型 */ + TypeVerbose: string; + /** 默认值 */ + DefaultValue: string; + /** 类型附带额外参数 */ + ExtraSetting?: string; + /** 帮助信息 */ + Help: string; + /** 参数组(非必填时选项) */ + Group?: string; + /** 后端自定义内容 */ + MethodType?: string; + JsonSchema?: string; + UISchema?: string; + /** 值 */ + Value?: any; + /** 建议表达式 */ + SuggestionDataExpression?: string; + + /** 是否开启缓存(动态表单upload-folder-path:目前由前端决定) */ + cacheRef?: any; + cacheHistoryDataKey?: string; +} + +/** 本地插件信息 */ +export interface localYakInfo { + Id?: number; + + ScriptName: string; + Content: string; + Type: string; + Help?: string; + RiskInfo?: YakRiskInfoProps[]; + Tags?: string; + Params?: YakParamProps[]; + EnablePluginSelector?: boolean; + PluginSelectorTypes?: string; + + Level?: string; + IsHistory?: boolean; + IsIgnore?: boolean; + IsGeneralModule?: boolean; + GeneralModuleVerbose?: string; + GeneralModuleKey?: string; + FromGit?: string; + IsCorePlugin?: boolean; + + /** 环境变量 */ + PluginEnvKey?: string[]; +} +/** ---------- grpc插件数据结构定义 End ---------- */ + +/** 源码转换为参数和风险信息(request) */ +export interface CodeToInfoRequestProps { + YakScriptType: string; + YakScriptCode: string; + Range?: GRPCRange; +} + +/** 风险信息 */ +export interface YakRiskInfoProps { + Level: string; + TypeVerbose: string; + CVE: string; + Description: string; + Solution: string; +} + +/** 源码转换为参数和风险信息(response) */ +export interface CodeToInfoResponseProps { + Information: YaklangInformation[]; + CliParameter: YakParamProps[]; + RiskInfo: YakRiskInfoProps[]; + Tags: string[]; + PluginEnvKey: string[]; +} diff --git a/src/pages/AIAgent/store/pageInfo.ts b/src/pages/AIAgent/store/pageInfo.ts new file mode 100644 index 00000000..39a32514 --- /dev/null +++ b/src/pages/AIAgent/store/pageInfo.ts @@ -0,0 +1,667 @@ +import type { AIMentionCommandParams } from '../ai-agent/components/aiMilkdownInput/aiMilkdownMention/aiMentionPlugin' +import { YakitRoute } from '../enums/yakitRoute' +import type { configManagementTabType } from '../types/interface/pageInfo' +import { createWithEqualityFn } from 'zustand/traditional' +import { subscribeWithSelector, persist, StorageValue } from 'zustand/middleware' +import { cloneDeep, debounce } from 'lodash' +import { JSONParseLog } from '../utils/tool' +import { yakitNotify } from '@/utils/notification' + +export type ConcurrencyAdvancedConfigValue = Pick< + AdvancedConfigValueProps, + | 'concurrent' + | 'minDelaySeconds' + | 'maxDelaySeconds' + | 'repeatTimes' + | 'disableUseConnPool' + | 'matchers' + | 'extractors' +> & { + disableAdvancedSet?: boolean +} + +/** + * @description 页面暂存数据 + * @property {PageNodeItemProps[]} pageNodeList 页面的一些信息 + * @property {string} routeKey 路由 + * @property {boolean} singleNode 是否为单开页面,单开页面的逻辑暂时没有写 + */ +export interface PageProps { + pageList: PageNodeItemProps[] + routeKey: string + singleNode: boolean + currentSelectPageId: string +} + +export interface PageNodeItemProps { + id: string + routeKey: string + pageGroupId: string + pageId: string + pageName: string + pageParamsInfo: PageParamsInfoProps + sortFieId: number + expand?: boolean + color?: string + // pageChildrenList: PageNodeItemProps[] +} + +/** 页面保存的数据 */ +interface PageParamsInfoProps { + /** YakitRoute.HTTPFuzzer webFuzzer页面缓存数据 */ + webFuzzerPageInfo?: WebFuzzerPageInfoProps + /** 批量执行页面 */ + pluginBatchExecutorPageInfo?: PluginBatchExecutorPageInfoProps + /** 专项漏洞页面 */ + pocPageInfo?: PocPageInfoProps + /** 弱口令页面 */ + brutePageInfo?: BrutePageInfoProps + /** 端口扫描页面 */ + scanPortPageInfo?: ScanPortPageInfoProps + /** 空间引擎页面 */ + spaceEnginePageInfo?: SpaceEnginePageInfoProps + /** 简易版 安全检测页面 */ + simpleDetectPageInfo?: SimpleDetectPageInfoProps + /** webSocketFuzzer页面 */ + websocketFuzzerPageInfo?: WebsocketFuzzerPageInfoProps + /** 流量分析器页面 */ + hTTPHistoryAnalysisPageInfo?: HTTPHistoryAnalysisPageInfo + /** 新建插件页面 */ + addYakitScriptPageInfo?: AddYakitScriptPageInfoProps + /** 打开插件仓库页面 */ + pluginHubPageInfo?: PluginHubPageInfoProps + /** 新建漏洞与风险统计页面 */ + riskPageInfo?: RiskPageInfoProps + /** hTTPHacker v1 */ + hTTPHackerPageInfo?: HTTPHackerPageInfoProps + auditCodePageInfo?: AuditCodePageInfoProps + codeScanPageInfo?: CodeScanPageInfoProps + /** 记事本编辑页面 */ + modifyNotepadPageInfo?: ModifyNotepadPageInfoProps + /** hTTPHacker v2 新版 */ + mitmHackerPageInfo?: MITMHackerPageInfoProps + /** 编辑 ai-forge 模板页面 */ + modifyAIForgePageInfo?: AIForgeEditorPageInfoProps + /** 编辑 ai tool 页面 */ + modifyAIToolPageInfo?: AIToolEditorPageInfoProps + /** 扫描历史页面 */ + yakRunnerScanHistory?: YakRunnerScanHistoryPageInfoProps + /** 并发页面高级配置 */ + ConcurrencyAdvancedConfigValue?: ConcurrencyAdvancedConfigValue + /** 规则管理页面 */ + ruleManagementPageInfo?: RuleManagementPageInfoProps + /** 审计漏洞页面 */ + auditHoleInfo?: AuditHoleInfoProps + /** 知识库页面 */ + AIRepository?: AIRepositoryProps + /** 配置管理页面 */ + configManagementParams?: { + tab?: configManagementTabType + } +} + +export interface AIForgeEditorPageInfoProps { + /** 编辑时使用,为 forge 模板 id */ + id: number + source: YakitRoute + [key: string]: any +} + +export interface AIToolEditorPageInfoProps { + id: number + source: YakitRoute +} + +export interface AddYakitScriptPageInfoProps { + /** 插件类型 */ + pluginType: string + /** 插件源码 */ + code: string + source: YakitRoute + [key: string]: any +} +export interface SpaceEnginePageInfoProps { + [key: string]: unknown +} + +export interface SimpleDetectPageInfoProps { + /** 执行批量执行的runtimeId */ + runtimeId: string +} + +export interface AIRepositoryProps { + defualtAIMentionCommandParams: AIMentionCommandParams[] +} + +export interface WebsocketFuzzerPageInfoProps { + wsTls?: boolean + wsRequest?: Uint8Array + wsToServer?: Uint8Array +} + +export interface HTTPHistoryAnalysisPageInfo { + verbose?: string + webFuzzer?: boolean + runtimeId?: string[] + sourceType?: string + pageId?: string + matchers?: HTTPResponseMatcher[] +} + +export interface PluginBatchExecutorPageInfoProps { + /** 执行批量执行的runtimeId */ + runtimeId: string + /** 批量执行结果的默认选中的tab默认值 */ + defaultActiveKey: string + /** 是否为https */ + https: boolean + /** 选中的数据History id */ + httpFlowIds: [] + /** 请求包 */ + request: Uint8Array + /** 执行任务的状态 */ + hybridScanMode: HybridScanModeType +} +export interface WebFuzzerPageInfoProps { + pageId: string + advancedConfigValue: AdvancedConfigValueProps + request: string + advancedConfigShow?: AdvancedConfigShowProps | null + // 高级配置中变量的二级Panel 展开项 + variableActiveKeys?: string[] + // 热加载代码 + hotPatchCode: string +} + +export interface PocPageInfoProps { + /** type 1会打开漏洞检测类型选择 2直接带着数据打开poc页面 */ + type?: number + /** 按组搜的选中 */ + selectGroup?: string[] + /** 按关键字搜的选中/poc内置组 */ + selectGroupListByKeyWord?: string[] + formValue?: HybridScanControlAfterRequest + /** 是否为https */ + https: boolean + /** 选中的数据History id */ + httpFlowIds: [] + /** 请求包 */ + request: Uint8Array + /** 执行批量执行的runtimeId */ + runtimeId: string + /** 执行任务的状态 */ + hybridScanMode: HybridScanModeType + /** 按关键词搜索的列表,搜索框的默认值, */ + defGroupKeywords?: string +} + +export interface BrutePageInfoProps { + /** 输入目标 */ + targets: string +} + +export interface ScanPortPageInfoProps { + /** 输入目标 */ + targets: string +} + +export interface PluginHubPageInfoProps { + /** 切换到插件仓库指定tab */ + tabActive: PluginSourceType + /** + * @param uuid 打开插件的uuid + * @param name 打开插件的name + * @param tabActive 主动跳到详情里的指定 tab 上 + * @description tabActive-如果想打开指定 tab 页面里的指定子 tab,可以使用'/'进行分割,例如:'log/check',log是主tab,check是子tab + */ + detailInfo?: { + uuid: string + name: string + tabActive?: string + isCorePlugin?: boolean + } + /** 是否刷新列表(传 true-刷新列表和高级筛选, false-刷新列表, 不传不刷新) */ + refeshList?: boolean + /** 是否打开管理分组抽屉 */ + openGroupDrawer?: boolean +} + +export interface RiskPageInfoProps { + /** 漏洞危险等级 */ + SeverityList?: string[] +} + +export interface AuditHoleInfoProps { + /** 漏洞危险等级 */ + Severity?: string[] + RuntimeID?: string[] +} + +interface ImmediatelyLaunchedInfo { + host?: string + port?: string + enableInitialPlugin?: boolean +} +export interface HTTPHackerPageInfoProps { + immediatelyLaunchedInfo?: ImmediatelyLaunchedInfo +} + +export interface MITMHackerPageInfoProps { + // 传空对象直接启动mitm 不传表示打开mitm + immediatelyLaunchedInfo?: ImmediatelyLaunchedInfo +} + +export interface AuditCodePageInfoProps { + Schema: string + // 基础路径 / 由Path、Variable、Value组成完整路径信息 + Path: string + Variable?: string + Value?: string + // 正常操作查询 + Location: string + Query?: { Key: string; Value: any }[] + // 文件与高亮信息 + CodeRange?: string + // 漏洞/规则 树所选中的下拉列表 + runtimeId?: string + // 左侧tab选中 + leftTabActive?: string + // 只看新增 + isShowCompare?: boolean + refreshRiskOrRuleList?: boolean +} + +export interface CodeScanPageInfoProps { + projectName?: string + projectId?: number + historyName?: string[] + codeScanMode?: SyntaxFlowScanModeType + runtimeId?: string + + // rule 相关过滤条件 + RuleIds?: number[] + GroupNames?: string[] + Keyword?: string + FilterLibRuleKind?: FilterLibRuleKind + // 所选规则总数(PS:在页面跳转时如若存在GroupNames则需要查询其total进行展示) + selectTotal?: number +} + +export interface ModifyNotepadPageInfoProps { + /** 需要跳转定位的dom元素id */ + domId?: string + /** 笔记本 线上:hash(string) 本地:Id(number) */ + notepadHash?: string | number + /** 笔记本标题 */ + title?: string + /** 搜索关键词 */ + keyWordInfo?: { keyWord: string; position: number; line?: number } + /** 自动带入内容 */ + content?: string +} + +export interface YakRunnerScanHistoryPageInfoProps { + Programs: string[] + ProjectIds: number[] +} + +export interface RuleManagementPageInfoProps { + RuleNames?: string[] +} +interface PageInfoStoreProps { + pages: Map + + selectGroupId: Map + + currentPageTabRouteKey: YakitRoute | string + + /** 设置 pages数据,例如:fuzzer缓存页面;未分组的关闭其他页面只保留当前页面 */ + setPagesData: (key: string, p: PageProps) => void + /** 设置组内的数据,例如:组内的关闭其他页面 */ + setPageNodeInfoByPageGroupId: (key: any, gId: string, list: PageNodeItemProps[]) => void + + /** 通过组的id获取组下的页面数据 */ + getPagesDataByGroupId: (key: string, gId: string) => PageNodeItemProps[] + /** 通过id获取页面数据 */ + queryPagesDataById: (key: string, pageId: string) => PageNodeItemProps | undefined + /** 新增缓存页面数据 */ + addPagesDataCache: (key: string, v: PageNodeItemProps) => void + /** 更新缓存页面数据 */ + updatePagesDataCacheById: (key: string, v: PageNodeItemProps) => void + /** 删除页面缓存数据 */ + removePagesDataCacheById: (key: string, id: string) => void + /** 通过组id 删除组数据以及组下的页面缓存数据 */ + removePagesDataCacheByGroupId: (key: string, gId: string) => void + + /** 设置页面中选中的组id */ + setSelectGroupId: (key: string, s: string) => void + /** 删除选中组 */ + removeCurrentSelectGroupId: (key: string) => void + + /** 获取当前选中组内的tab名称 返回string[] */ + getCurrentGroupAllTabName: (key: any) => string[] + /** 获取当前选中组 */ + getCurrentSelectGroup: (key: any) => PageNodeItemProps | undefined + clearAllData: () => void + clearDataByRoute: (key: string) => void + /** 只保留routeKey的数据,删除除此routeKey之外的数据 */ + clearOtherDataByRoute: (routeKey: string) => void + /** 设置当前激活的页面id */ + setCurrentSelectPageId: (routeKey: string, pageId: string) => void + /** 获取当前激活的页面id */ + getCurrentSelectPageId: (routeKey: string) => string + /** 通过 RuntimeId 获取页面数据 【批量执行/安全检测(简易版)】 */ + getPageInfoByRuntimeId: (routeKey: string, pageId: string) => PageNodeItemProps | undefined + /** 展开或者收起所有的组 isExpand:true展开/false收起 */ + updateGroupExpandOrRetract: (routeKey: string, isExpand: boolean) => void + + setCurrentPageTabRouteKey: (route: YakitRoute | string) => void + getCurrentPageTabRouteKey: () => YakitRoute | string +} +export const defPage: PageProps = { + pageList: [], + routeKey: '', + singleNode: false, + currentSelectPageId: '', +} +const pageInfoToRuntimeIdMap = { + [YakitRoute.BatchExecutorPage]: 'pluginBatchExecutorPageInfo', + [YakitRoute.SimpleDetect]: 'simpleDetectPageInfo', + [YakitRoute.PoC]: 'pocPageInfo', + [YakitRoute.YakRunner_Code_Scan]: 'codeScanPageInfo', +} +export const usePageInfo = createWithEqualityFn()( + subscribeWithSelector( + persist( + (set, get) => ({ + pages: new Map(), + selectGroupId: new Map(), + currentPageTabRouteKey: '', + + setPagesData: (key, values) => { + const newVal = new Map(get().pages).set(key, values) + set({ + ...get(), + pages: newVal, + }) + }, + setPageNodeInfoByPageGroupId: (key, groupId, list) => { + const newVal = get().pages + const current = newVal.get(key) || cloneDeep(defPage) + const groupList: PageNodeItemProps[] = current.pageList.filter((ele) => ele.pageGroupId !== groupId) + const newGroupList = groupList.concat(list) + newVal.set(key, { + ...current, + pageList: newGroupList, + }) + set({ + ...get(), + pages: newVal, + }) + }, + getPagesDataByGroupId: (key, groupId) => { + const { pages } = get() + const current = pages.get(key) || cloneDeep(defPage) + return current.pageList.filter((ele) => ele.pageGroupId === groupId) + }, + queryPagesDataById: (key, pageId) => { + const { pages } = get() + const current = pages.get(key) || cloneDeep(defPage) + return current.pageList.find((ele) => ele.pageId === pageId) + }, + getPageInfoByRuntimeId: (routeKey, runtimeId) => { + const { pages } = get() + const current = pages.get(routeKey) || cloneDeep(defPage) + return current.pageList.find( + // @ts-ignore + (ele) => ele?.pageParamsInfo[pageInfoToRuntimeIdMap[routeKey]]?.runtimeId === runtimeId, + ) + }, + addPagesDataCache: (key, value) => { + const newVal = get().pages + const current = newVal.get(key) || cloneDeep(defPage) + current.pageList.push(value) + newVal.set(key, { + ...current, + }) + set({ + ...get(), + pages: new Map(newVal), + }) + }, + updatePagesDataCacheById: (key, value) => { + const { pages } = get() + const current: PageProps = pages.get(key) || cloneDeep(defPage) + let updateIndex: number = current.pageList.findIndex((ele) => ele.pageId === value.pageId) + if (updateIndex !== -1) { + current.pageList[updateIndex] = { + ...value, + } + const newVal = pages.set(key, { + ...current, + }) + set({ + ...get(), + pages: newVal, + }) + } + }, + removePagesDataCacheById: (key, id) => { + const newVal = get().pages + const current: PageProps = newVal.get(key) || cloneDeep(defPage) + const newPageList = current.pageList.filter((ele) => ele.pageId !== id) + newVal.set(key, { + ...current, + pageList: newPageList, + }) + set({ + ...get(), + pages: newVal, + }) + }, + removePagesDataCacheByGroupId: (key, gId) => { + const newVal = get().pages + const current: PageProps = newVal.get(key) || cloneDeep(defPage) + const newPageList = current.pageList.filter((ele) => ele.pageGroupId !== gId) + newVal.set(key, { + ...current, + pageList: newPageList, + }) + set({ + ...get(), + pages: newVal, + }) + }, + setCurrentSelectGroup: () => {}, + setSelectGroupId: (key, val) => { + const selectGroupId = get().selectGroupId + selectGroupId.set(key, val) + set({ + ...get(), + selectGroupId, + }) + }, + removeCurrentSelectGroupId: (key) => { + const { selectGroupId } = get() + if (selectGroupId.size > 0) { + selectGroupId.delete(key) + set({ + ...get(), + selectGroupId, + }) + } + }, + getCurrentGroupAllTabName: (key) => { + const { selectGroupId, pages } = get() + const currentGroupId = selectGroupId.get(key) + const currentPages: PageProps = pages.get(key) || cloneDeep(defPage) + return currentPages.pageList.filter((ele) => ele.pageGroupId === currentGroupId).map((ele) => ele.pageName) + }, + getCurrentSelectGroup: (key) => { + const { selectGroupId, pages } = get() + const currentGroupId = selectGroupId.get(key) + const currentPages: PageProps = pages.get(key) || cloneDeep(defPage) + return currentPages.pageList.find((ele) => ele.pageGroupId === currentGroupId) + }, + clearAllData: () => { + set({ + pages: new Map(), + selectGroupId: new Map(), + }) + }, + clearDataByRoute: (key) => { + const { selectGroupId, pages } = get() + selectGroupId.delete(key) + pages.delete(key) + set({ + pages: new Map(pages), + selectGroupId: new Map(selectGroupId), + }) + }, + clearOtherDataByRoute: (key) => { + const { selectGroupId, pages } = get() + const newSelectGroupId = selectGroupId.get(key) + const newPages = pages.get(key) + set({ + pages: new Map().set(key, newPages), + selectGroupId: new Map().set(key, newSelectGroupId), + }) + }, + setCurrentSelectPageId: (key, pageId) => { + const newPages = get().pages + const currentPage = newPages.get(key) || cloneDeep(defPage) + newPages.set(key, { + ...currentPage, + currentSelectPageId: pageId, + }) + set({ + ...get(), + pages: newPages, + }) + }, + getCurrentSelectPageId: (key) => { + const { pages } = get() + const current = pages.get(key) || cloneDeep(defPage) + return current.currentSelectPageId + }, + updateGroupExpandOrRetract: (key) => { + const newPages = get().pages + const currentPage = newPages.get(key) || cloneDeep(defPage) + const newPageList = currentPage.pageList.map((ele) => { + if (ele.pageId.endsWith('group')) { + return { + ...ele, + expand: true, + } + } + return ele + }) + newPages.set(key, { + ...currentPage, + pageList: newPageList, + }) + set({ + ...get(), + pages: newPages, + }) + }, + setCurrentPageTabRouteKey: (route: YakitRoute | string) => { + set({ + ...get(), + currentPageTabRouteKey: route, + }) + }, + getCurrentPageTabRouteKey: () => { + const { currentPageTabRouteKey } = get() + return currentPageTabRouteKey + }, + }), + { + name: 'page-info', + storage: { + getItem: async (name: string) => { + try { + const str = sessionStorage.getItem(name) + if (!str) return null + const { state } = JSONParseLog(str, { + page: 'pageInfo', + }) + return { + state: { + ...state, + pages: new Map(state.pages), + selectGroupId: new Map(state.selectGroupId), + }, + } + } catch (error) { + yakitNotify('error', 'page-info解析数据错误:' + error) + return null + } + }, + setItem: async (name, value: StorageValue) => { + const str = JSON.stringify({ + state: { + ...value.state, + pages: Array.from(value.state.pages.entries()), + selectGroupId: Array.from(value.state.selectGroupId.entries()), + }, + }) + sessionStorage.setItem(name, str) + }, + removeItem: async (name: string): Promise => { + sessionStorage.removeItem(name) + }, + }, + }, + ), + ), + Object.is, +) + +export const saveFuzzerCache = debounce( + (selectedState: PageProps) => { + try { + const { pageList = [] } = selectedState || { + pageList: [], + } + const cache = getFuzzerProcessedCacheData(pageList) + setRemoteProjectValue(FuzzerRemoteGV.FuzzerCache, JSON.stringify(cache)).catch((error) => {}) + } catch (error) { + yakitNotify('error', 'webFuzzer缓存数据失败:' + error) + } + }, + 500, + { leading: true }, +) + +/** 处理WF需要缓存的数据 */ +export const getFuzzerProcessedCacheData = (pageList: any[]) => { + const cache = pageList.map((ele) => { + const advancedConfigValue = ele.pageParamsInfo?.webFuzzerPageInfo?.advancedConfigValue || defaultAdvancedConfigValue + const hotPatchCode = ele.pageParamsInfo?.webFuzzerPageInfo?.hotPatchCode + return { + groupChildren: [], + groupId: ele.pageGroupId, + id: ele.pageId, + pageParams: { + actualHost: advancedConfigValue.actualHost || '', + id: ele.pageId, + isHttps: advancedConfigValue.isHttps, + request: ele.pageParamsInfo?.webFuzzerPageInfo?.request || defaultPostTemplate, + params: advancedConfigValue.params, + extractors: advancedConfigValue.extractors, + matchers: advancedConfigValue.matchers, + repeatTimes: advancedConfigValue.repeatTimes, + concurrent: advancedConfigValue.concurrent, + minDelaySeconds: advancedConfigValue.minDelaySeconds, + maxDelaySeconds: advancedConfigValue.maxDelaySeconds, + hotPatchCode: hotPatchCode, + }, + sortFieId: ele.sortFieId, + verbose: ele.pageName, + expand: ele.expand, + color: ele.color, + } + }) + return cache +} diff --git a/src/pages/AIAgent/types/interface/YakExecutorParams.tsx b/src/pages/AIAgent/types/interface/YakExecutorParams.tsx new file mode 100644 index 00000000..748e6919 --- /dev/null +++ b/src/pages/AIAgent/types/interface/YakExecutorParams.tsx @@ -0,0 +1,4 @@ +export interface YakExecutorParam { + Key: string + Value: any +} diff --git a/src/pages/AIAgent/types/interface/pageInfo.ts b/src/pages/AIAgent/types/interface/pageInfo.ts new file mode 100644 index 00000000..f1b21363 --- /dev/null +++ b/src/pages/AIAgent/types/interface/pageInfo.ts @@ -0,0 +1,296 @@ +import type { YakitRoute } from '@/pages/AIAgent/enums/yakitRoute'; +import type { AIMentionCommandParams } from '@/pages/AIAgent/ai-agent/components/aiMilkdownInput/aiMilkdownMention/aiMentionPlugin'; + +export type configManagementTabType = 'payload' | 'proxy' | 'hotPatch'; + +/** + * @description 页面暂存数据 + * @property {PageNodeItemProps[]} pageNodeList 页面的一些信息 + * @property {string} routeKey 路由 + * @property {boolean} singleNode 是否为单开页面,单开页面的逻辑暂时没有写 + */ +export interface PageProps { + pageList: PageNodeItemProps[]; + routeKey: string; + singleNode: boolean; + currentSelectPageId: string; +} + +export interface PageNodeItemProps { + id: string; + routeKey: string; + pageGroupId: string; + pageId: string; + pageName: string; + pageParamsInfo: PageParamsInfoProps; + sortFieId: number; + expand?: boolean; + color?: string; + // pageChildrenList: PageNodeItemProps[] +} + +/** 页面保存的数据 */ +interface PageParamsInfoProps { + /** YakitRoute.HTTPFuzzer webFuzzer页面缓存数据 */ + webFuzzerPageInfo?: WebFuzzerPageInfoProps; + /** 批量执行页面 */ + pluginBatchExecutorPageInfo?: PluginBatchExecutorPageInfoProps; + /** 专项漏洞页面 */ + pocPageInfo?: PocPageInfoProps; + /** 弱口令页面 */ + brutePageInfo?: BrutePageInfoProps; + /** 端口扫描页面 */ + scanPortPageInfo?: ScanPortPageInfoProps; + /** 空间引擎页面 */ + spaceEnginePageInfo?: {}; + /** 简易版 安全检测页面 */ + simpleDetectPageInfo?: SimpleDetectPageInfoProps; + /** webSocketFuzzer页面 */ + websocketFuzzerPageInfo?: WebsocketFuzzerPageInfoProps; + /** 流量分析器页面 */ + hTTPHistoryAnalysisPageInfo?: HTTPHistoryAnalysisPageInfo; + /** 新建插件页面 */ + addYakitScriptPageInfo?: AddYakitScriptPageInfoProps; + /** 打开插件仓库页面 */ + pluginHubPageInfo?: PluginHubPageInfoProps; + /** 新建漏洞与风险统计页面 */ + riskPageInfo?: RiskPageInfoProps; + /** hTTPHacker v1 */ + hTTPHackerPageInfo?: HTTPHackerPageInfoProps; + auditCodePageInfo?: AuditCodePageInfoProps; + codeScanPageInfo?: CodeScanPageInfoProps; + /** 记事本编辑页面 */ + modifyNotepadPageInfo?: ModifyNotepadPageInfoProps; + /** hTTPHacker v2 新版 */ + mitmHackerPageInfo?: MITMHackerPageInfoProps; + /** 编辑 ai-forge 模板页面 */ + modifyAIForgePageInfo?: AIForgeEditorPageInfoProps; + /** 编辑 ai tool 页面 */ + modifyAIToolPageInfo?: AIToolEditorPageInfoProps; + /** 扫描历史页面 */ + yakRunnerScanHistory?: YakRunnerScanHistoryPageInfoProps; + /** 并发页面高级配置 */ + ConcurrencyAdvancedConfigValue?: ConcurrencyAdvancedConfigValue; + /** 规则管理页面 */ + ruleManagementPageInfo?: RuleManagementPageInfoProps; + /** 审计漏洞页面 */ + auditHoleInfo?: AuditHoleInfoProps; + /** 知识库页面 */ + AIRepository?: AIRepositoryProps; + /** 配置管理页面 */ + configManagementParams?: { + tab?: configManagementTabType; + }; +} + +export interface AIForgeEditorPageInfoProps { + /** 编辑时使用,为 forge 模板 id */ + id: number; + source: YakitRoute; + [key: string]: any; +} + +export interface AIToolEditorPageInfoProps { + id: number; + source: YakitRoute; +} + +export interface AddYakitScriptPageInfoProps { + /** 插件类型 */ + pluginType: string; + /** 插件源码 */ + code: string; + source: YakitRoute; + [key: string]: any; +} + +export interface SimpleDetectPageInfoProps { + /** 执行批量执行的runtimeId */ + runtimeId: string; +} + +export interface AIRepositoryProps { + defualtAIMentionCommandParams: AIMentionCommandParams[]; +} + +export interface WebsocketFuzzerPageInfoProps { + wsTls?: boolean; + wsRequest?: Uint8Array; + wsToServer?: Uint8Array; +} + +export interface HTTPHistoryAnalysisPageInfo { + verbose?: string; + webFuzzer?: boolean; + runtimeId?: string[]; + sourceType?: string; + pageId?: string; + matchers?: HTTPResponseMatcher[]; +} + +export interface PluginBatchExecutorPageInfoProps { + /** 执行批量执行的runtimeId */ + runtimeId: string; + /** 批量执行结果的默认选中的tab默认值 */ + defaultActiveKey: string; + /** 是否为https */ + https: boolean; + /** 选中的数据History id */ + httpFlowIds: []; + /** 请求包 */ + request: Uint8Array; + /** 执行任务的状态 */ + hybridScanMode: HybridScanModeType; +} +export interface WebFuzzerPageInfoProps { + pageId: string; + advancedConfigValue: AdvancedConfigValueProps; + request: string; + advancedConfigShow?: AdvancedConfigShowProps | null; + // 高级配置中变量的二级Panel 展开项 + variableActiveKeys?: string[]; + // 热加载代码 + hotPatchCode: string; +} + +export interface PocPageInfoProps { + /** type 1会打开漏洞检测类型选择 2直接带着数据打开poc页面 */ + type?: number; + /** 按组搜的选中 */ + selectGroup?: string[]; + /** 按关键字搜的选中/poc内置组 */ + selectGroupListByKeyWord?: string[]; + formValue?: HybridScanControlAfterRequest; + /** 是否为https */ + https: boolean; + /** 选中的数据History id */ + httpFlowIds: []; + /** 请求包 */ + request: Uint8Array; + /** 执行批量执行的runtimeId */ + runtimeId: string; + /** 执行任务的状态 */ + hybridScanMode: HybridScanModeType; + /** 按关键词搜索的列表,搜索框的默认值, */ + defGroupKeywords?: string; +} + +export interface BrutePageInfoProps { + /** 输入目标 */ + targets: string; +} + +export interface ScanPortPageInfoProps { + /** 输入目标 */ + targets: string; +} + +export interface PluginHubPageInfoProps { + /** 切换到插件仓库指定tab */ + tabActive: PluginSourceType; + /** + * @param uuid 打开插件的uuid + * @param name 打开插件的name + * @param tabActive 主动跳到详情里的指定 tab 上 + * @description tabActive-如果想打开指定 tab 页面里的指定子 tab,可以使用'/'进行分割,例如:'log/check',log是主tab,check是子tab + */ + detailInfo?: { + uuid: string; + name: string; + tabActive?: string; + isCorePlugin?: boolean; + }; + /** 是否刷新列表(传 true-刷新列表和高级筛选, false-刷新列表, 不传不刷新) */ + refeshList?: boolean; + /** 是否打开管理分组抽屉 */ + openGroupDrawer?: boolean; +} + +export interface RiskPageInfoProps { + /** 漏洞危险等级 */ + SeverityList?: string[]; +} + +export interface AuditHoleInfoProps { + /** 漏洞危险等级 */ + Severity?: string[]; + RuntimeID?: string[]; +} + +interface ImmediatelyLaunchedInfo { + host?: string; + port?: string; + enableInitialPlugin?: boolean; +} +export interface HTTPHackerPageInfoProps { + immediatelyLaunchedInfo?: ImmediatelyLaunchedInfo; +} + +export interface MITMHackerPageInfoProps { + // 传空对象直接启动mitm 不传表示打开mitm + immediatelyLaunchedInfo?: ImmediatelyLaunchedInfo; +} + +export interface AuditCodePageInfoProps { + Schema: string; + // 基础路径 / 由Path、Variable、Value组成完整路径信息 + Path: string; + Variable?: string; + Value?: string; + // 正常操作查询 + Location: string; + Query?: { Key: string; Value: any }[]; + // 文件与高亮信息 + CodeRange?: string; + // 漏洞/规则 树所选中的下拉列表 + runtimeId?: string; + // 左侧tab选中 + leftTabActive?: string; + // 只看新增 + isShowCompare?: boolean; + refreshRiskOrRuleList?: boolean; +} + +export interface CodeScanPageInfoProps { + projectName?: string; + projectId?: number; + historyName?: string[]; + codeScanMode?: SyntaxFlowScanModeType; + runtimeId?: string; + + // rule 相关过滤条件 + RuleIds?: number[]; + GroupNames?: string[]; + Keyword?: string; + FilterLibRuleKind?: FilterLibRuleKind; + // 所选规则总数(PS:在页面跳转时如若存在GroupNames则需要查询其total进行展示) + selectTotal?: number; +} + +export interface ModifyNotepadPageInfoProps { + /** 需要跳转定位的dom元素id */ + domId?: string; + /** 笔记本 线上:hash(string) 本地:Id(number) */ + notepadHash?: string | number; + /** 笔记本标题 */ + title?: string; + /** 搜索关键词 */ + keyWordInfo?: { keyWord: string; position: number; line?: number }; + /** 自动带入内容 */ + content?: string; +} + +export interface YakRunnerScanHistoryPageInfoProps { + Programs: string[]; + ProjectIds: number[]; +} + +export interface RuleManagementPageInfoProps { + RuleNames?: string[]; +} +export const defPage: PageProps = { + pageList: [], + routeKey: '', + singleNode: false, + currentSelectPageId: '', +}; diff --git a/src/pages/AIAgent/types/interface/publicMenu.ts b/src/pages/AIAgent/types/interface/publicMenu.ts new file mode 100644 index 00000000..02dce258 --- /dev/null +++ b/src/pages/AIAgent/types/interface/publicMenu.ts @@ -0,0 +1,13 @@ +import type { YakitRoute } from '../../enums/yakitRoute' + +/** + * @name Route信息(用于打开页面) + * @property route-页面的路由 + * @property pluginId-插件id(本地) + * @property pluginName-插件名称 + */ +export interface RouteToPageProps { + route: YakitRoute + pluginId?: number + pluginName?: string +} diff --git a/src/pages/AIAgent/utils/base.ts b/src/pages/AIAgent/utils/base.ts new file mode 100644 index 00000000..bf643713 --- /dev/null +++ b/src/pages/AIAgent/utils/base.ts @@ -0,0 +1,3 @@ +export const formatJson = (filterVal: string[], jsonData: any[]) => { + return jsonData.map((v) => filterVal.map((j) => v[j])); +}; diff --git a/src/pages/AIAgent/utils/convert.ts b/src/pages/AIAgent/utils/convert.ts new file mode 100644 index 00000000..bc357a77 --- /dev/null +++ b/src/pages/AIAgent/utils/convert.ts @@ -0,0 +1,45 @@ +import type { API } from '../ai-agent/type/resposeType' +import type { YakParamProps, YakRiskInfoProps } from '../plugins/pluginsType' + +/** + * @name 本地插件参数数据(YakParamProps)-转换成-线上插件参数数据(API.YakitPluginParam) + */ +export const pluginParamsConvertLocalToOnline = (local: YakParamProps[]) => { + return local.map((item) => { + const obj: API.YakitPluginParam = { + field: item.Field, + field_verbose: item.FieldVerbose, + required: item.Required, + type_verbose: item.TypeVerbose, + default_value: item.DefaultValue, + extra_setting: item.ExtraSetting, + help: item.Help, + group: item.Group, + method_type: item.MethodType || '', + json_schema: item.JsonSchema || '', + suggestion_data_expression: item.SuggestionDataExpression || '', + ui_schema: item.UISchema || '', + } + return obj + }) +} + +/** + * @name 本地插件风险数据(YakRiskInfoProps)-转换成-线上插件风险数据(API.PluginsRiskDetail) + */ +export const riskDetailConvertLocalToOnline = (risks?: YakRiskInfoProps[]) => { + const arr: API.PluginsRiskDetail[] = [] + const local = risks || [] + for (let item of local) { + if (item.Level && item.CVE && item.TypeVerbose) { + arr.push({ + level: item.Level, + cve: item.CVE, + typeVerbose: item.TypeVerbose, + description: item.Description, + solution: item.Solution, + }) + } + } + return arr +} diff --git a/src/pages/AIAgent/utils/globalShortcutKey.ts b/src/pages/AIAgent/utils/globalShortcutKey.ts new file mode 100644 index 00000000..d9cf814a --- /dev/null +++ b/src/pages/AIAgent/utils/globalShortcutKey.ts @@ -0,0 +1,44 @@ +import { KeyboardToKeyTableMaps, NumpadKeyTableMaps, YakitKeyMod } from '../enums/keyboard' +/** 缓存本次 yakit 打开后的按键 */ +const cacheKeyboardToKey: Record = {} + +const handleKeyboardToKey = (keyboard: KeyboardEvent): string | null => { + const { key, code } = keyboard + if (cacheKeyboardToKey[code]) return cacheKeyboardToKey[code] + if (cacheKeyboardToKey[`${code}-${key}`]) return cacheKeyboardToKey[`${code}-${key}`] + + // 解析是否为部分需要转换的物理按键集合 + const convertCodeValue = NumpadKeyTableMaps[`${code}-${key}`] + const isConvert = !!convertCodeValue + + // 键盘映射表的键集合 + const keys = Object.keys(KeyboardToKeyTableMaps) + let hitValue: string | null = null + + for (let el of keys) { + const keyValue = KeyboardToKeyTableMaps[el as keyof typeof KeyboardToKeyTableMaps] + if (keyValue.includes(convertCodeValue || code)) { + hitValue = el + cacheKeyboardToKey[isConvert ? `${code}-${key}` : code] = el + break + } + } + + return hitValue +} +export const convertKeyEventToKeyCombination = (event: KeyboardEvent): string[] | null => { + const { altKey, ctrlKey, metaKey, shiftKey } = event + + let key = handleKeyboardToKey(event) + + if (key) { + const keys: string[] = [] + ctrlKey && keys.push(YakitKeyMod.Control) + shiftKey && keys.push(YakitKeyMod.Shift) + altKey && keys.push(YakitKeyMod.Alt) + metaKey && keys.push(YakitKeyMod.Meta) + if (!keys.includes(key)) keys.push(key) + return keys + } + return null +} diff --git a/src/pages/AIAgent/utils/icon/icon.tsx b/src/pages/AIAgent/utils/icon/icon.tsx new file mode 100644 index 00000000..840ed915 --- /dev/null +++ b/src/pages/AIAgent/utils/icon/icon.tsx @@ -0,0 +1,504 @@ +import Icon from "@ant-design/icons" +import {CustomIconComponentProps} from "@ant-design/icons/lib/components/Icon" +import React from "react" + +interface IconProps extends CustomIconComponentProps { + onClick: (e: React.MouseEvent) => void + ref?: any +} + +const type = () => ( + + + + + +) +/** + * @description icon/type + */ +export const IconType = (props: Partial) => { + return +} + +const bold = () => ( + + + + +) +/** + * @description icon/bold + */ +export const IconBold = (props: Partial) => { + return +} + +const strikethrough = () => ( + + + + + +) +/** + * @description icon/strikethrough + */ +export const IconStrikethrough = (props: Partial) => { + return +} + +const italic = () => ( + + + + + +) +/** + * @description icon/italic + */ +export const IconItalic = (props: Partial) => { + return +} + +const underline = () => ( + + + + +) +/** + * @description icon/underline + */ +export const IconUnderline = (props: Partial) => { + return +} + +const code2 = () => ( + + + + + +) +/** + * @description icon/code-2 + */ +export const IconCode2 = (props: Partial) => { + return +} + +const heading1 = () => ( + + + + + + +) +/** + * @description icon/heading-1 + */ +export const IconHeading1 = (props: Partial) => { + return +} + +const heading2 = () => ( + + + + + + +) +/** + * @description icon/heading-2 + */ +export const IconHeading2 = (props: Partial) => { + return +} + +const heading3 = () => ( + + + + + + + +) +/** + * @description icon/heading-3 + */ +export const IconHeading3 = (props: Partial) => { + return +} + +const listOrdered = () => ( + + + + + + + + +) +/** + * @description icon/list-ordered + */ +export const IconListOrdered = (props: Partial) => { + return +} + +const list = () => ( + + + + + + + + +) +/** + * @description icon/list + */ +export const IconList = (props: Partial) => { + return +} + +const checkSquare = () => ( + + + + +) +/** + * @description icon/check-square + */ +export const IconCheckSquare = (props: Partial) => { + return +} + +const curlyBraces = () => ( + + + + +) +/** + * @description icon/curly-braces + */ +export const IconCurlyBraces = (props: Partial) => { + return +} + +const quote = () => ( + + + + +) +/** + * @description icon/quote + */ +export const IconQuote = (props: Partial) => { + return +} + +const flipVertical = () => ( + + + + + + + + +) +/** + * @description icon/flip-vertical + */ +export const IconFlipVertical = (props: Partial) => { + return +} +const notepadFileTypeDir = () => ( + + + + + +) +/** + * @description icon/记事本/文件类型/文件夹 Icon/notepad/file type/dir + */ +export const IconNotepadFileTypeDir = (props: Partial) => { + return +} + +const notepadFileTypeUnknown = () => ( + + + + + +) +/** + * @description icon/记事本/文件类型/未知 Icon/notepad/file type/unknown + */ +export const IconNotepadFileTypeUnknown = (props: Partial) => { + return +} + +const notepadFileTypeCompress = () => ( + + + + + +) +/** + * @description icon/记事本/文件类型/压缩文件 Icon/notepad/file type/Compress files + */ +export const IconNotepadFileTypeCompress = (props: Partial) => { + return +} + +const notepadFileTypePPT = () => ( + + + + + +) +/** + * @description icon/记事本/文件类型/ppt Icon/notepad/file type/ppt + */ +export const IconNotepadFileTypePPT = (props: Partial) => { + return +} + +const notepadFileTypeExcel = () => ( + + + + + + +) +/** + * @description icon/记事本/文件类型/excel Icon/notepad/file type/excel + */ +export const IconNotepadFileTypeExcel = (props: Partial) => { + return +} + +const notepadFileTypePdf = () => ( + + + + + +) +/** + * @description icon/记事本/文件类型/pdf Icon/notepad/file type/pdf + */ +export const IconNotepadFileTypePdf = (props: Partial) => { + return +} + +const notepadFileTypeWord = () => ( + + + + + +) +/** + * @description icon/记事本/文件类型/word Icon/notepad/file type/word + */ +export const IconNotepadFileTypeWord = (props: Partial) => { + return +} diff --git a/src/pages/AIAgent/utils/index.tsx b/src/pages/AIAgent/utils/index.tsx new file mode 100644 index 00000000..dae13f92 --- /dev/null +++ b/src/pages/AIAgent/utils/index.tsx @@ -0,0 +1,161 @@ +import { LogNodeStatusFileIcon, SolidYakCattleNoBackColorIcon } from '@/assets/icon/colors' +import { + IconNotepadFileTypeWord, + IconNotepadFileTypeCompress, + IconNotepadFileTypePPT, + IconNotepadFileTypeExcel, + IconNotepadFileTypePdf, + IconNotepadFileTypeUnknown, +} from './icon/icon' +import { YakitEditor } from '@/compoments/yakitUI/YakitEditor/YakitEditor' +import type { YakitTagColor } from '@/compoments/yakitUI/YakitTag/YakitTagType' +import type { ReactNode } from 'react' +import type { PluginExecuteLogFile } from '../plugins/operator/pluginExecuteResult/PluginExecuteResultType' +import { modeToPermissions } from './invoker' +import { FileActionEnum } from '../plugins/operator/pluginExecuteResult/PluginExecuteResultType.d' + +export const renderFileTypeIcon = (params: { type: string; iconClassName?: string }) => { + const { type, iconClassName } = params + switch (type) { + case '.doc': + case 'doc': + return + case '.zip': + case 'zip': + case '.7z': + case '7z': + case '.tar': + case 'tar': + return + case '.ppt': + case 'ppt': + return + case '.csv': + case 'csv': + case '.xlsx': + case 'xlsx': + case '.numbers': + case 'numbers': + case 'xls': + case '.xls': + return + case '.pdf': + case 'pdf': + return + case '.txt': + case 'txt': + return + case '.yak': + case 'yak': + return + default: + return + } +} + +export const getFileActionStatus = ( + action: PluginExecuteLogFile.FileActionType, + action_message: PluginExecuteLogFile.FileActionMessage, +) => { + let content: ReactNode = '' // 预览内容 + let message = '' // 操作描述 + let actionText = '未知操作' // 操作权限 + let color: YakitTagColor = 'white' + try { + switch (action) { + case FileActionEnum.Read_Action: + // eslint-disable-next-line no-case-declarations, @typescript-eslint/consistent-type-assertions + const read = { + ...action_message, + } as PluginExecuteLogFile.ReadFileActionMessage + // eslint-disable-next-line no-case-declarations + const readContent = read.content || '' + actionText = '读取' + message = `${read.offset}-${read.offset + read.length} ${read.unit}` + content = readContent.length > 200 ? readContent.substring(0, 200) + '...' : readContent + break + case FileActionEnum.Write_Action: + // eslint-disable-next-line no-case-declarations, @typescript-eslint/consistent-type-assertions + const write = { + ...action_message, + } as PluginExecuteLogFile.WriteFileActionMessage + // eslint-disable-next-line no-case-declarations + const writeContent = write.content || '' + actionText = '修改内容' + message = `修改方式:${write.mode}` + content = writeContent.length > 200 ? writeContent.substring(0, 200) + '...' : writeContent + break + case FileActionEnum.Create_Action: + // eslint-disable-next-line no-case-declarations, @typescript-eslint/consistent-type-assertions + const create = { + ...action_message, + } as PluginExecuteLogFile.CreateFileActionMessage + actionText = '创建' + color = 'success' + message = `创建${create.isDir ? '文件夹' : '文件'}` + content = '暂无可预览内容' + break + case FileActionEnum.Delete_Action: + // eslint-disable-next-line no-case-declarations, @typescript-eslint/consistent-type-assertions + const remove = { + ...action_message, + } as PluginExecuteLogFile.DELETEFileActionMessage + actionText = '删除' + color = 'danger' + message = `删除${remove.isDir ? '文件夹' : '文件'}` + content = `${remove.isDir ? '文件夹' : '文件'}已被删除,无法展示预览内容` + break + case FileActionEnum.Status_Action: + // eslint-disable-next-line no-case-declarations, @typescript-eslint/consistent-type-assertions + const status = { + ...action_message, + } as PluginExecuteLogFile.STATUSFileActionMessage + actionText = '查看元信息' + content = ( +
    + {/** NOTE - 个数过多后,可能会有性能影响 */} + +
    + ) + break + case FileActionEnum.Chmod_Action: + // eslint-disable-next-line no-case-declarations, @typescript-eslint/consistent-type-assertions + const chmod = { + ...action_message, + } as PluginExecuteLogFile.CHMODFileActionMessage + actionText = '修改权限' + // eslint-disable-next-line no-case-declarations + const mode = modeToPermissions(chmod.chmodMode) + content = ( + <> + 所属者权限: {mode ? mode[0] : '未知'} +
    + 所属组权限: {mode ? mode[1] : '未知'} +
    + 其他用户权限: {mode ? mode[2] : '未知'} + + ) + break + case FileActionEnum.Find_Action: + // eslint-disable-next-line no-case-declarations, @typescript-eslint/consistent-type-assertions + const find = { + ...action_message, + } as PluginExecuteLogFile.FINDFileActionMessage + actionText = '查找' + message = `通过${find.mode}查找到${find.content.length}个满足条件${find.condition}的文件` + content = '暂无可预览内容' + break + default: + break + } + } catch (error) { + actionText = action + message = action_message.message + } + return { + color, + action: actionText, + message, + content, + } +} diff --git a/src/pages/AIAgent/utils/invoker.tsx b/src/pages/AIAgent/utils/invoker.tsx new file mode 100644 index 00000000..ac094b44 --- /dev/null +++ b/src/pages/AIAgent/utils/invoker.tsx @@ -0,0 +1,129 @@ +import type { ReactNode } from 'react' +import { + FileActionEnum, + type PluginExecuteLogFile, +} from '../plugins/operator/pluginExecuteResult/PluginExecuteResultType.d' +import type { YakitTagColor } from '@/compoments/yakitUI/YakitTag/YakitTagType' +import { YakitEditor } from '@/compoments/yakitUI/YakitEditor/YakitEditor' + +/** + * + * @param fileItem 文件对象 + * @description 判断是否是插件执行日志文件对象 + * @returns + */ +export const isPluginExecuteLogFileItem = (fileItem: any) => { + return Object.hasOwn(fileItem, 'action') +} + +/** 根据权限码输出权限内容 */ +export const modeToPermissions = (mode: string) => { + // 确保输入八进制数 + if (!/^0[0-7]+$/.test(mode)) { + return null + } + + // 解析后三位 + const permissions = mode + .slice(-3) + .split('') + .map((digit: string) => { + const num = parseInt(digit, 8) // 将八进制字符转换为数字 + return ( + (num & 4 ? 'r' : '-') + // 读权限 + (num & 2 ? 'w' : '-') + // 写权限 + (num & 1 ? 'x' : '-') // 执行权限 + ) + }) + return permissions +} + +export const getFileActionStatus = ( + action: PluginExecuteLogFile.FileActionType, + action_message: PluginExecuteLogFile.FileActionMessage, +) => { + let content: ReactNode = '' // 预览内容 + let message = '' // 操作描述 + let actionText = '未知操作' // 操作权限 + let color: YakitTagColor = 'white' + try { + switch (action) { + case FileActionEnum.Read_Action: { + const read = action_message as PluginExecuteLogFile.ReadFileActionMessage + const readContent = read.content || '' + actionText = '读取' + message = `${read.offset}-${read.offset + read.length} ${read.unit}` + content = readContent.length > 200 ? readContent.substring(0, 200) + '...' : readContent + break + } + case FileActionEnum.Write_Action: { + const write = action_message as PluginExecuteLogFile.WriteFileActionMessage + const writeContent = write.content || '' + actionText = '修改内容' + message = `修改方式:${write.mode}` + content = writeContent.length > 200 ? writeContent.substring(0, 200) + '...' : writeContent + break + } + case FileActionEnum.Create_Action: { + const create = action_message as PluginExecuteLogFile.CreateFileActionMessage + actionText = '创建' + color = 'success' + message = `创建${create.isDir ? '文件夹' : '文件'}` + content = '暂无可预览内容' + break + } + case FileActionEnum.Delete_Action: { + const remove = action_message as PluginExecuteLogFile.DELETEFileActionMessage + actionText = '删除' + color = 'danger' + message = `删除${remove.isDir ? '文件夹' : '文件'}` + content = `${remove.isDir ? '文件夹' : '文件'}已被删除,无法展示预览内容` + break + } + case FileActionEnum.Status_Action: { + const status = action_message as PluginExecuteLogFile.STATUSFileActionMessage + actionText = '查看元信息' + content = ( +
    + {/** NOTE - 个数过多后,可能会有性能影响 */} + +
    + ) + break + } + case FileActionEnum.Chmod_Action: { + const chmod = action_message as PluginExecuteLogFile.CHMODFileActionMessage + actionText = '修改权限' + const mode = modeToPermissions(chmod.chmodMode) + content = ( + <> + 所属者权限: {mode ? mode[0] : '未知'} +
    + 所属组权限: {mode ? mode[1] : '未知'} +
    + 其他用户权限: {mode ? mode[2] : '未知'} + + ) + break + } + case FileActionEnum.Find_Action: { + const find = action_message as PluginExecuteLogFile.FINDFileActionMessage + actionText = '查找' + message = `通过${find.mode}查找到${find.content.length}个满足条件${find.condition}的文件` + content = '暂无可预览内容' + break + } + default: + break + } + } catch (error) { + actionText = action + message = action_message.message + } + return { + color, + action: actionText, + message, + content, + } +} diff --git a/src/pages/AIAgent/utils/netif.tsx b/src/pages/AIAgent/utils/netif.tsx new file mode 100644 index 00000000..b385716b --- /dev/null +++ b/src/pages/AIAgent/utils/netif.tsx @@ -0,0 +1,83 @@ +import type { + YakURLKVPair, + YakURLResource, +} from '@/pages/YakRunnerAuditCode/utils'; +import { yakitFailed } from '@/utils/notification'; + +export interface YakURL { + FromRaw: string; + Schema: string; + User: string; + Pass: string; + Location: string; + Path: string; + Query: YakURLKVPair[]; +} +export interface RequestYakURLResponse { + Page: number; + PageSize: number; + Total: number; + Resources: YakURLResource[]; +} + +// const { ipcRenderer } = window.require('electron'); + +export const requestYakURLList = ( + url: YakURL, + onResponse?: (response: RequestYakURLResponse) => any, + onError?: (e: any) => any, +) => { + url.Query = url.Query || []; + url.Query.push({ Key: 'op', Value: 'list' }); + return ipcRenderer + .invoke('RequestYakURL', { + Url: url, + Method: 'GET', + }) + .then((rsp: RequestYakURLResponse) => { + if (onResponse) { + onResponse(rsp); + } + return rsp; + }) + .catch((e: any) => { + yakitFailed(`加载失败: ${e}`); + if (onError) { + onError(e); + } + throw e; + }); +}; + +export const loadFromYakURLRaw = ( + url: string, + onResponse?: (response: RequestYakURLResponse) => any, + onError?: (e: any) => any, +) => { + return ipcRenderer + .invoke('RequestYakURL', { + Url: { + FromRaw: url, + Schema: '', + User: '', + Pass: '', + Location: '', + Path: '', + Query: [], + }, + Method: 'GET', + }) + .then((rsp: RequestYakURLResponse) => { + if (onResponse) { + onResponse(rsp); + } + return rsp; + }) + .catch((e: any) => { + yakitFailed(`加载失败: ${e}`); + if (onError) { + onError(e); + } + throw e; + }); +}; diff --git a/src/pages/AIAgent/utils/plugins.ts b/src/pages/AIAgent/utils/plugins.ts new file mode 100644 index 00000000..c447e76b --- /dev/null +++ b/src/pages/AIAgent/utils/plugins.ts @@ -0,0 +1,25 @@ +/** + * @description 取消DebugPlugin + */ +export const apiCancelDebugPlugin: (token: string) => Promise = ( + token, +) => { + return new Promise((resolve) => { + console.log('token:', token); + resolve(null); + // try { + // ipcRenderer + // .invoke(`cancel-DebugPlugin`, token) + // .then(() => { + // resolve(null); + // }) + // .catch((e: any) => { + // yakitNotify('error', '取消本地插件执行出错:' + e); + // reject(e); + // }); + // } catch (error) { + // yakitNotify('error', '取消本地插件执行出错:' + error); + // reject(error); + // } + }); +}; diff --git a/src/pages/AIAgent/utils/tool.ts b/src/pages/AIAgent/utils/tool.ts new file mode 100644 index 00000000..f591c567 --- /dev/null +++ b/src/pages/AIAgent/utils/tool.ts @@ -0,0 +1,39 @@ +import isNil from 'lodash/isNil' + +/** @name 将传入对象中值为null或undefined的键值对删除 */ +export const toolDelInvalidKV = (data: any) => { + try { + if (!data) return data + if (!isObject(data)) return data + for (const key in data) { + if (isNil(data[key])) { + delete data[key] + } else if (isObject(data[key])) { + toolDelInvalidKV(data[key]) + } + } + return data + } catch (error) { + return data + } +} + +/** 判断值是否为对象 */ +export const isObject = (value: any) => { + return Object.prototype.toString.call(value) === '[object Object]' +} + +export interface JSONParseLogOption { + page?: string + fun?: string + reviver?: (key: string, value: any) => any +} +export function JSONParseLog(text: string, option?: JSONParseLogOption) { + // eslint-disable-next-line no-useless-catch + try { + const result = JSON.parse(text, option?.reviver) + return result + } catch (err) { + throw err + } +} diff --git a/src/pages/DataService/AssetsVulns/index.tsx b/src/pages/DataService/AssetsVulns/index.tsx index 6718cc48..61bdb332 100644 --- a/src/pages/DataService/AssetsVulns/index.tsx +++ b/src/pages/DataService/AssetsVulns/index.tsx @@ -1,137 +1,127 @@ -import type { FC } from 'react'; -import { useMemo } from 'react'; +import type { FC } from 'react' +import { useMemo } from 'react' -import { WizardTable } from '@/compoments'; +import { WizardTable } from '@/compoments' -import { getAssetsValueFilter, postAssetsVulns } from '@/apis/taskDetail'; -import { AssetsVulnsColumns } from '@/pages/TaskDetail/compoments/Columns'; -import dayjs from 'dayjs'; -import { UploadOutlined } from '@ant-design/icons'; -import { useRequest, useSafeState } from 'ahooks'; -import { SeverityMapTag } from '@/pages/TaskDetail/compoments/utils'; -import { AssetsVulnsFilterDrawer } from '@/pages/TaskDetail/compoments/TableOptionsFilterDrawer/AssetsVulnsFilterDrawer'; -import { getBatchInvokingScriptTaskNode } from '@/apis/task'; +import { getAssetsValueFilter, postAssetsVulns } from '@/apis/taskDetail' +import { AssetsVulnsColumns } from '@/pages/TaskDetail/compoments/Columns' +import dayjs from 'dayjs' +import { UploadOutlined } from '@ant-design/icons' +import { useRequest, useSafeState } from 'ahooks' +import { SeverityMapTag } from '@/pages/TaskDetail/compoments/utils' +import { AssetsVulnsFilterDrawer } from '@/pages/TaskDetail/compoments/TableOptionsFilterDrawer/AssetsVulnsFilterDrawer' +import { getBatchInvokingScriptTaskNode } from '@/apis/task' const taskNameColumns: any = [ - { - title: '任务名', - dataIndex: 'task_id', - columnsHeaderFilterType: 'input', - width: 180, - }, -]; + { + title: '任务名', + dataIndex: 'task_id', + columnsHeaderFilterType: 'input', + width: 180, + }, +] const AssetsVulns: FC = () => { - const [page] = WizardTable.usePage(); - const [tableFilter, setTableFilter] = useSafeState< - Record | undefined - >({}); + const [page] = WizardTable.usePage() + const [tableFilter, setTableFilter] = useSafeState | undefined>({}) - const { data } = useRequest(async () => { - const { data } = await getAssetsValueFilter(); - const { list, severity } = data; - // 映射漏洞等级 字段 - const transformSeverityList = severity?.map((it) => { - const label = SeverityMapTag.find((item) => - item.key.includes(it.key ?? ''), - )?.name; - return { - Verbose: label, - Total: it.value, - value: it.key, - label: label, - }; - }); + const { data } = useRequest(async () => { + const { data } = await getAssetsValueFilter() + const { list, severity } = data + // 映射漏洞等级 字段 + const transformSeverityList = severity?.map((it) => { + const label = SeverityMapTag.find((item) => item.key.includes(it.key ?? ''))?.name + return { + Verbose: label, + Total: it.value, + value: it.key, + label: label, + } + }) - // 映射漏洞类型Top 10 字段 - const transformList = list?.map((it) => ({ - Verbose: it.key, - Total: it.value, - value: it.key, - label: it.key, - })); - return { - transformSeverityList, - transformList, - }; - }); + // 映射漏洞类型Top 10 字段 + const transformList = list?.map((it) => ({ + Verbose: it.key, + Total: it.value, + value: it.key, + label: it.key, + })) + return { + transformSeverityList, + transformList, + } + }) - // 获取执行节点 列表 - const { data: taskNodeData } = useRequest(async () => { - const result = await getBatchInvokingScriptTaskNode(); - const { - data: { list }, - } = result; - const resultData = Array.isArray(list) - ? list.map((it) => ({ label: it, value: it })) - : []; - return resultData; - }); + // 获取执行节点 列表 + const { data: taskNodeData } = useRequest(async () => { + const result = await getBatchInvokingScriptTaskNode() + const { + data: { list }, + } = result + const resultData = Array.isArray(list) ? list.map((it) => ({ label: it, value: it })) : [] + return resultData + }) - const columns = useMemo(() => { - const assetsVulnsColumnsProps = { - transformSeverityList: data?.transformSeverityList ?? [], - transformList: data?.transformList ?? [], - taskNodeData: taskNodeData ?? [], - }; - const columns = AssetsVulnsColumns(assetsVulnsColumnsProps); + const columns = useMemo(() => { + const assetsVulnsColumnsProps = { + transformSeverityList: data?.transformSeverityList ?? [], + transformList: data?.transformList ?? [], + taskNodeData: taskNodeData ?? [], + } + const columns = AssetsVulnsColumns(assetsVulnsColumnsProps) - const result = [ - ...columns.slice(0, 1), - ...taskNameColumns, - ...columns.slice(1), - ]; - return result; - }, [data, taskNodeData]); + const result = [...columns.slice(0, 1), ...taskNameColumns, ...columns.slice(1)] + return result + }, [data, taskNodeData]) - return ( - - - 导出 Excel - - ), - }, + return ( + + + 导出 Excel + + ), + }, - ProFilterSwitch: { - trigger: , - layout: 'vertical', - }, - }, - }} - request={async (params, filter) => { - setTableFilter(filter); - const { data } = await postAssetsVulns({ - ...params, - ...filter, - }); + ProFilterSwitch: { + trigger: , + layout: 'vertical', + }, + }, + }} + request={async (params, filter) => { + setTableFilter(filter) + const { data } = await postAssetsVulns({ + ...params, + ...filter, + }) - return { - list: data?.list ?? [], - pagemeta: data?.pagemeta, - }; - }} - /> - ); -}; + return { + list: data?.list ?? [], + pagemeta: data?.pagemeta, + } + }} + /> + ) +} -export { AssetsVulns }; +export { AssetsVulns } diff --git a/src/pages/DataService/CveLoophole/index.tsx b/src/pages/DataService/CveLoophole/index.tsx index e392ff0e..52c08485 100644 --- a/src/pages/DataService/CveLoophole/index.tsx +++ b/src/pages/DataService/CveLoophole/index.tsx @@ -1,280 +1,241 @@ -import type { FC } from 'react'; -import { useRef } from 'react'; +import type { FC } from 'react' +import { useRef } from 'react' -import classNames from 'classnames'; +import classNames from 'classnames' -import { postCveQuery } from '@/apis/CveLoopholeApi'; -import type { TCveQueryResponse } from '@/apis/CveLoopholeApi/type'; -import { WizardTable } from '@/compoments'; -import type { CreateTableProps } from '@/compoments/WizardTable/types'; -import { YakitTag } from '@/compoments/YakitTag/YakitTag'; -import styles from './CVETable.module.scss'; -import { SeverityMapTag } from '@/pages/TaskDetail/compoments/utils'; -import { CveLoopholeFilterDrawer } from './compoments/CveLoopholeFilterDrawer'; -import { Button, Input, Popover, Select, Space } from 'antd'; -import { useSafeState } from 'ahooks'; -import { SyncOutlined } from '@ant-design/icons'; -import { CveAllUpdateModal } from './compoments/CveAllUpdateModal'; -import type { UseModalRefType } from '@/compoments/WizardModal/useModal'; -import { CveDifferUpdateModal } from './compoments/CveDifferUpdateModal'; -import dayjs from 'dayjs'; +import { postCveQuery } from '@/apis/CveLoopholeApi' +import type { TCveQueryResponse } from '@/apis/CveLoopholeApi/type' +import { WizardTable } from '@/compoments' +import type { CreateTableProps } from '@/compoments/WizardTable/types' +import { YakitTag } from '@/compoments/yakitUI/YakitTag/YakitTag' +import styles from './CVETable.module.scss' +import { SeverityMapTag } from '@/pages/TaskDetail/compoments/utils' +import { CveLoopholeFilterDrawer } from './compoments/CveLoopholeFilterDrawer' +import { Button, Input, Popover, Select, Space } from 'antd' +import { useSafeState } from 'ahooks' +import { SyncOutlined } from '@ant-design/icons' +import { CveAllUpdateModal } from './compoments/CveAllUpdateModal' +import type { UseModalRefType } from '@/compoments/WizardModal/useModal' +import { CveDifferUpdateModal } from './compoments/CveDifferUpdateModal' +import dayjs from 'dayjs' const options = [ + { + value: 'cve', + label: 'CVE', + text: 'CVE编号或关键字搜索', + }, + { + value: 'cwe', + label: 'CWE', + text: 'CWE编号搜索', + }, +] + +const CveLoophole: FC = () => { + const [page] = WizardTable.usePage() + const CveAllUpdateModalRef = useRef(null) + const CveDifferUpdateModalRef = useRef(null) + + const [search, setSearch] = useSafeState({ + key: 'cve', + value: '', + }) + + const [open, setOpen] = useSafeState(false) + + const columns: CreateTableProps['columns'] = [ { - value: 'cve', - label: 'CVE', - text: 'CVE编号或关键字搜索', + dataIndex: 'cve', + title: 'CVE编号', + width: 120, }, { - value: 'cwe', - label: 'CWE', - text: 'CWE编号搜索', + dataIndex: 'description', + title: '概述', + width: 180, }, -]; - -const CveLoophole: FC = () => { - const [page] = WizardTable.usePage(); - const CveAllUpdateModalRef = useRef(null); - const CveDifferUpdateModalRef = useRef(null); - - const [search, setSearch] = useSafeState({ - key: 'cve', - value: '', - }); + { + dataIndex: 'cwe', + title: 'CWE编号', + width: 120, + render: (text: string) => + text ? ( +
    + {text.split('|').map((ele) => ( + + {ele} + + ))} +
    + ) : ( + '' + ), + }, + { + dataIndex: 'vulnerable_product', + title: '影响产品', + width: 120, + }, + { + dataIndex: 'severity', + title: '漏洞级别', + width: 120, + render: (value, record) => { + if (value) { + const toLowerCaseValue = value.toLowerCase() + const targetItem = SeverityMapTag.find((item) => item.key.includes(toLowerCaseValue)) + return ( +
    +
    {targetItem?.name}
    + {record.exploitability_score ?? 0} +
    + ) + } else { + return '-' + } + }, + }, + { + dataIndex: 'published_date', + title: '披露时间', + width: 180, + render: (value) => { + const d = dayjs(value) + return d.isValid() ? d.format('YYYY-MM-DD HH:mm') : '-' + }, + }, + { + dataIndex: 'last_modified_date', + title: '更新时间', + width: 180, + render: (value) => { + const d = dayjs(value) + return d.isValid() ? d.format('YYYY-MM-DD HH:mm') : '-' + }, + }, + ] - const [open, setOpen] = useSafeState(false); + return ( +
    + + + - setSearch((cur) => ({ - ...cur, - key: e, - })) - } - /> - it.value === search.key, - )?.text - } - onSearch={async (value) => { - const uselessKey = options.filter( - (it) => it.value !== search.key, - )[0].value; - - await page.editFilter({ - [search.key]: value, - [uselessKey]: undefined, - }); - setSearch((cur) => ({ - ...cur, - value, - })); - }} - /> - - setOpen(newOpen)} - content={ -
    -
    { - CveDifferUpdateModalRef.current?.open( - { - title: 'CVE 差量最新数据更新', - }, - ); - setOpen(false); - }} - > - 只更新最新数据 -
    -
    { - CveAllUpdateModalRef.current?.open( - { - title: 'CVE 数据库更新', - type: 'all', - }, - ); - setOpen(false); - }} - > - 全量更新 -
    -
    { - CveAllUpdateModalRef.current?.open( - { - title: 'CVE 数据库离线更新', - type: 'offline', - }, - ); - setOpen(false); - }} - > - 离线更新 -
    -
    - } - trigger="click" - > - -
    -
    - ), - ProFilterSwitch: { - trigger: , - layout: 'vertical', - }, - }, - }} - request={async (params, filter) => { - const { data } = await postCveQuery({ - ...params, - ...filter, - }); + }, + }} + request={async (params, filter) => { + const { data } = await postCveQuery({ + ...params, + ...filter, + }) - return { - list: data?.list ?? [], - pagemeta: data?.pagemeta, - }; - }} - /> - - - - ); -}; + return { + list: data?.list ?? [], + pagemeta: data?.pagemeta, + } + }} + /> + + + + ) +} -export { CveLoophole }; +export { CveLoophole } diff --git a/src/pages/DataService/PortAssets/buildDeleteAllBody.ts b/src/pages/DataService/PortAssets/buildDeleteAllBody.ts new file mode 100644 index 00000000..2b33f7d4 --- /dev/null +++ b/src/pages/DataService/PortAssets/buildDeleteAllBody.ts @@ -0,0 +1,24 @@ +/** + * 列表请求里的分页、排序字段不应传给「按筛选全部删除」接口 + */ +const DELETE_ALL_STRIP_KEYS = new Set(['page', 'limit', 'Ppge', 'order', 'order_by']) + +/** + * 组装 DELETE 请求体:delete_all + 当前表格筛选(与列表查询 filter 对齐,剔除分页排序) + */ +function buildDeleteAllBody( + filter: Record | undefined, + extra?: Record, +): Record { + const body: Record = { delete_all: true } + const merged = { ...(filter ?? {}), ...(extra ?? {}) } + for (const [k, v] of Object.entries(merged)) { + if (DELETE_ALL_STRIP_KEYS.has(k)) continue + if (v === undefined || v === '') continue + if (Array.isArray(v) && v.length === 0) continue + body[k] = v + } + return body +} + +export { buildDeleteAllBody, DELETE_ALL_STRIP_KEYS } diff --git a/src/pages/DataService/PortAssets/index.tsx b/src/pages/DataService/PortAssets/index.tsx index 443a65e9..45af701f 100644 --- a/src/pages/DataService/PortAssets/index.tsx +++ b/src/pages/DataService/PortAssets/index.tsx @@ -1,220 +1,280 @@ -import { useMemo, useRef, type FC } from 'react'; - -import { WizardTable } from '@/compoments'; - -import { postAssetsProts } from '@/apis/taskDetail'; -import { ProtColumns } from '@/pages/TaskDetail/compoments/Columns'; -import dayjs from 'dayjs'; -import { UploadOutlined } from '@ant-design/icons'; -import { AssetsProtsFilterDrawer } from '@/pages/TaskDetail/compoments/TableOptionsFilterDrawer/AssetsProtsFilterDrawer'; -import type { CreateTableProps } from '@/compoments/WizardTable/types'; -import type { TGetAssetsProtsResponse } from '@/apis/taskDetail/types'; -import { getAnalysisScript, getBatchInvokingScriptTaskNode } from '@/apis/task'; -import { useRequest, useSafeState } from 'ahooks'; -import CopyOutlined from '@/pages/TaskDetail/compoments/utils/CopyOutlined'; -import { copyToClipboard } from '@/utils'; -import { Button, message } from 'antd'; -import type { TDeleteValues } from '@/pages/ReportManage/ReportManage'; -import { CreateTaskScriptModal } from '@/pages/TaskPageList/compoment/CreateTaskScriptModal'; -import type { UseModalRefType } from '@/compoments/WizardModal/useModal'; +import { useMemo, useRef, type FC } from 'react' + +import { WizardTable } from '@/compoments' + +import { deleteAssetsPorts, postAssetsProts } from '@/apis/taskDetail' +import { ProtColumns } from '@/pages/TaskDetail/compoments/Columns' +import dayjs from 'dayjs' +import { UploadOutlined } from '@ant-design/icons' +import { AssetsProtsFilterDrawer } from '@/pages/TaskDetail/compoments/TableOptionsFilterDrawer/AssetsProtsFilterDrawer' +import type { CreateTableProps } from '@/compoments/WizardTable/types' +import type { TDeleteAssetsPortsBody, TGetAssetsProtsResponse } from '@/apis/taskDetail/types' +import { getAnalysisScript, getBatchInvokingScriptTaskNode } from '@/apis/task' +import { useRequest, useSafeState } from 'ahooks' +import CopyOutlined from '@/pages/TaskDetail/compoments/utils/CopyOutlined' +import { copyToClipboard } from '@/utils' +import { Button, message } from 'antd' +import type { TDeleteValues } from '@/pages/ReportManage/ReportManage' +import { CreateTaskScriptModal } from '@/pages/TaskPageList/compoment/CreateTaskScriptModal' +import type { UseModalRefType } from '@/compoments/WizardModal/useModal' +import modal from 'antd/es/modal' +import { buildDeleteAllBody } from './buildDeleteAllBody' + +const normalizeStringArrayFilter = (value?: string | string[]) => { + if (!value) return undefined + if (Array.isArray(value)) return value.length ? value : undefined + const list = value + .split(',') + .map((item) => item.trim()) + .filter(Boolean) + return list.length ? list : undefined +} const PortAssets: FC = () => { - const [page] = WizardTable.usePage(); - - const [tableFilter, setTableFilter] = useSafeState< - Record | undefined - >({}); - const [checkedValue, setCheckedValue] = useSafeState(); - const ipListRef = useRef([]); - const openCreateTaskModalRef = useRef(null); - - const taskNameColumns: CreateTableProps['columns'] = - [ - { - title: '网络地址', - dataIndex: 'host', - columnsHeaderFilterType: 'input', - width: 180, - rowSelection: 'checkbox', - rowSelectKeys: checkedValue, - onSelectChange: setCheckedValue, - render: (value) => - value ? ( -
    -
    {value}
    - { - copyToClipboard(value) - .then(() => { - message.success('复制成功'); - }) - .catch(() => { - message.info('复制失败,请重试'); - }); - }} - /> -
    - ) : ( - '-' - ), - }, - { - title: '任务名', - dataIndex: 'task_id', - columnsHeaderFilterType: 'input', - width: 240, - }, - ]; - - const { runAsync: hostListRunAsync, loading } = useRequest( - async (params) => { - const result = await postAssetsProts(params); - const list = result?.data?.list; - const resultHostList = list?.map((it) => it?.host); - ipListRef.current = resultHostList; - }, - { - manual: true, - onSuccess: () => { - run(); - }, - }, - ); - - // 获取执行节点 列表 - const { data: taskNodeData } = useRequest(async () => { - const result = await getBatchInvokingScriptTaskNode(); - const { - data: { list }, - } = result; - const resultData = Array.isArray(list) - ? list.map((it) => ({ label: it, value: it })) - : []; - return resultData; - }); - - const { run } = useRequest( - async () => { - const result = await getAnalysisScript(); - const { - data: { list }, - } = result; - const targetData = list.map((value) => ({ - ...value, - ip_list: ipListRef.current, - })); - return targetData; - }, - { - manual: true, - onSuccess: (values) => { - openCreateTaskModalRef.current?.open(values); - setCheckedValue({ host: { ids: [], isAll: false } }); - }, - }, - ); - - const columns = useMemo(() => { - const protColumnsList = ProtColumns({ - taskNodeData: taskNodeData ?? [], - }); - const result = [...taskNameColumns, ...protColumnsList.slice(1)]; - return result; - }, [taskNodeData, checkedValue]); - - const triggerScanBtnDisable = useMemo(() => { - const tableFilter = page.getParams()?.filter; - const isCidr = tableFilter?.cidr?.length > 0; - const isService = tableFilter?.service_type?.length > 0; - - const isAll = checkedValue?.host?.isAll; - const isIds = checkedValue?.host && checkedValue?.host?.ids?.length > 0; - return isCidr || isService || isAll || isIds; - }, [page.getParams()]); - - // 批量漏洞扫描 - const headVulnerabilityScanning = () => { - const isAll = checkedValue?.['host'].isAll; - const idsList = checkedValue?.['host'].ids; - const dataSource = page.getDataSource(); - const ipList = dataSource - .map((item) => (idsList?.includes(item.id) ? item.host : undefined)) - .filter((list) => list); - ipListRef.current = ipList; - if (!isAll) { - run(); + const [page] = WizardTable.usePage() + + const [tableFilter, setTableFilter] = useSafeState | undefined>({}) + const [checkedValue, setCheckedValue] = useSafeState() + const ipListRef = useRef([]) + const openCreateTaskModalRef = useRef(null) + + const taskNameColumns: CreateTableProps['columns'] = [ + { + title: '网络地址', + dataIndex: 'host', + columnsHeaderFilterType: 'input', + width: 180, + rowSelection: 'checkbox', + rowSelectKeys: checkedValue, + onSelectChange: setCheckedValue, + render: (value) => + value ? ( +
    +
    {value}
    + { + copyToClipboard(value) + .then(() => { + message.success('复制成功') + }) + .catch(() => { + message.info('复制失败,请重试') + }) + }} + /> +
    + ) : ( + '-' + ), + }, + { + title: '任务名', + dataIndex: 'task_id', + columnsHeaderFilterType: 'input', + width: 240, + }, + ] + + const { runAsync: hostListRunAsync, loading } = useRequest( + async (params) => { + const result = await postAssetsProts(params) + const list = result?.data?.list + const resultHostList = list?.map((it) => it?.host) + ipListRef.current = resultHostList + }, + { + manual: true, + onSuccess: () => { + run() + }, + }, + ) + + // 获取执行节点 列表 + const { data: taskNodeData } = useRequest(async () => { + const result = await getBatchInvokingScriptTaskNode() + const { + data: { list }, + } = result + const resultData = Array.isArray(list) ? list.map((it) => ({ label: it, value: it })) : [] + return resultData + }) + + const { run } = useRequest( + async () => { + const result = await getAnalysisScript() + const { + data: { list }, + } = result + const targetData = list.map((value) => ({ + ...value, + ip_list: ipListRef.current, + })) + return targetData + }, + { + manual: true, + onSuccess: (values) => { + openCreateTaskModalRef.current?.open(values) + setCheckedValue({ host: { ids: [], isAll: false } }) + }, + }, + ) + + const columns = useMemo(() => { + const protColumnsList = ProtColumns({ + taskNodeData: taskNodeData ?? [], + }) + const result = [...taskNameColumns, ...protColumnsList.slice(1)] + return result + }, [taskNodeData, checkedValue]) + + const triggerScanBtnDisable = useMemo(() => { + const tableFilter = page.getParams()?.filter + const isCidr = tableFilter?.cidr?.length > 0 + // const isService = tableFilter?.service_type?.length > 0 + + const isAll = checkedValue?.host?.isAll + const isIds = checkedValue?.host && checkedValue?.host?.ids?.length > 0 + return ( + isCidr || + // || isService + isAll || + isIds + ) + }, [page.getParams()]) + + // 批量漏洞扫描 + const headVulnerabilityScanning = () => { + const isAll = checkedValue?.['host'].isAll + const idsList = checkedValue?.['host'].ids + const dataSource = page.getDataSource() + const ipList = dataSource.map((item) => (idsList?.includes(item.id) ? item.host : undefined)).filter((list) => list) + ipListRef.current = ipList + if (!isAll) { + run() + } else { + hostListRunAsync({ + ...tableFilter, + limit: -1, + }) + } + } + + const hasDeleteSelection = useMemo(() => { + const sel = checkedValue?.host + const ids = sel?.ids + const isAll = sel?.isAll === true + return isAll || (Array.isArray(ids) && ids.length > 0) + }, [checkedValue]) + + const { runAsync: runDeletePorts } = useRequest(deleteAssetsPorts, { + manual: true, + }) + + const handleBatchDeletePorts = () => { + const sel = checkedValue?.host + const isAll = sel?.isAll === true + const ids = sel?.ids ?? [] + if (!isAll && !ids.length) return + const filter = page.getParams()?.filter as Record | undefined + modal.confirm({ + title: '批量删除', + content: isAll + ? '当前为表头全选,将按当前筛选条件删除全部匹配记录(非仅本页)。此操作不可恢复,确定继续?' + : '此操作不可恢复,确定删除选中的端口资产?', + okText: '确定', + cancelText: '取消', + okButtonProps: isAll ? { danger: true } : undefined, + async onOk() { + if (isAll) { + await runDeletePorts( + buildDeleteAllBody({ + ...filter, + service_type: filter?.service_type ? [filter?.service_type] : undefined, + }) as TDeleteAssetsPortsBody, + ) } else { - hostListRunAsync({ - ...tableFilter, - limit: -1, - }); + await runDeletePorts({ + ids: ids.map((id) => Number(id)), + }) } - }; + message.success('删除成功') + setCheckedValue({ host: { ids: [], isAll: false } }) + // 勿 await:WizardTable 内 ahooks useRequest 在并发/防抖下可能返回永不 resolve 的 Promise,确认框会卡 loading + Promise.resolve(page.onLoad()).catch(() => {}) + }, + }) + } - return ( - <> - - - 导出 Excel - - ), - }, - ProFilterSwitch: { - trigger: , - layout: 'vertical', - }, - trigger: ( - - ), - }, - }} - request={async (params, filter) => { - setTableFilter(filter); - const { data } = await postAssetsProts({ - ...params, - ...filter, - }); - - return { - list: data?.list ?? [], - pagemeta: data?.pagemeta, - }; - }} - /> - - - ); -}; + return ( + <> + + + 导出 Excel + + ), + }, + ProFilterSwitch: { + trigger: , + layout: 'vertical', + }, + trigger: ( +
    + + +
    + ), + }, + }} + request={async (params, filter) => { + setTableFilter(filter) + const { service_type, ...restFilter } = filter ?? {} + const { data } = await postAssetsProts({ + ...params, + ...restFilter, + service_type: normalizeStringArrayFilter(service_type), + }) + + return { + list: data?.list ?? [], + pagemeta: data?.pagemeta, + } + }} + /> + + + ) +} -export { PortAssets }; +export { PortAssets } diff --git a/src/pages/DataService/SensitiveMessage/index.tsx b/src/pages/DataService/SensitiveMessage/index.tsx index 88a24208..2154aea0 100644 --- a/src/pages/DataService/SensitiveMessage/index.tsx +++ b/src/pages/DataService/SensitiveMessage/index.tsx @@ -1,194 +1,187 @@ -import type { FC } from 'react'; +import type { FC } from 'react' -import { UploadOutlined } from '@ant-design/icons'; -import dayjs from 'dayjs'; +import { UploadOutlined } from '@ant-design/icons' +import dayjs from 'dayjs' -import { getSensitiveMessagePage, postupdateStatus } from '@/apis/reportManage'; -import type { TSensitiveMessageResponse } from '@/apis/reportManage/types'; -import { WizardTable } from '@/compoments'; -import type { CreateTableProps } from '@/compoments/WizardTable/types'; -import { message, Select } from 'antd'; -import { showErrorMessage } from '@/utils/showErrorMessage'; -import CopyOutlined from '@/pages/TaskDetail/compoments/utils/CopyOutlined'; -import { copyToClipboard } from '@/utils'; -import { useRequest, useSafeState } from 'ahooks'; -import { getBatchInvokingScriptTaskNode } from '@/apis/task'; +import { getSensitiveMessagePage, postupdateStatus } from '@/apis/reportManage' +import type { TSensitiveMessageResponse } from '@/apis/reportManage/types' +import { WizardTable } from '@/compoments' +import type { CreateTableProps } from '@/compoments/WizardTable/types' +import { message, Select } from 'antd' +import { showErrorMessage } from '@/utils/showErrorMessage' +import CopyOutlined from '@/pages/TaskDetail/compoments/utils/CopyOutlined' +import { copyToClipboard } from '@/utils' +import { useRequest, useSafeState } from 'ahooks' +import { getBatchInvokingScriptTaskNode } from '@/apis/task' const sensitiveInfoStatus = [ + { + value: 1, + label: '已处理', + color: 'success', + }, + { + value: 2, + label: '忽略', + color: 'default', + }, + { + value: 3, + label: '待处理', + color: 'warning', + }, +] + +const SensitiveMessage: FC<{ task_id?: string }> = ({ task_id }) => { + const [page] = WizardTable.usePage() + const [tableFilter, setTableFilter] = useSafeState | undefined>({}) + + // 获取执行节点 列表 + const { data: taskNodeData } = useRequest(async () => { + const result = await getBatchInvokingScriptTaskNode() + const { + data: { list }, + } = result + const resultData = Array.isArray(list) ? list.map((it) => ({ label: it, value: it })) : [] + return resultData + }) + const columns: CreateTableProps['columns'] = [ { - value: 1, - label: '已处理', - color: 'success', + title: '仓库名', + dataIndex: 'repo_name', + width: 120, }, { - value: 2, - label: '忽略', - color: 'default', + title: '文件路径', + dataIndex: 'file_path', + width: 240, + render: (value) => + value ? ( +
    +
    {value}
    + { + copyToClipboard(value) + .then(() => { + message.success('复制成功') + }) + .catch(() => { + message.info('复制失败,请重试') + }) + }} + /> +
    + ) : ( + '-' + ), }, { - value: 3, - label: '待处理', - color: 'warning', + title: '仓库描述', + dataIndex: 'repo_desc', + width: 240, }, -]; - -const SensitiveMessage: FC<{ task_id?: string }> = ({ task_id }) => { - const [page] = WizardTable.usePage(); - const [tableFilter, setTableFilter] = useSafeState< - Record | undefined - >({}); - - // 获取执行节点 列表 - const { data: taskNodeData } = useRequest(async () => { - const result = await getBatchInvokingScriptTaskNode(); - const { - data: { list }, - } = result; - const resultData = Array.isArray(list) - ? list.map((it) => ({ label: it, value: it })) - : []; - return resultData; - }); - const columns: CreateTableProps['columns'] = [ - { - title: '仓库名', - dataIndex: 'repo_name', - width: 120, - }, - { - title: '文件路径', - dataIndex: 'file_path', - width: 240, - render: (value) => - value ? ( -
    -
    {value}
    - { - copyToClipboard(value) - .then(() => { - message.success('复制成功'); - }) - .catch(() => { - message.info('复制失败,请重试'); - }); - }} - /> -
    - ) : ( - '-' - ), - }, - { - title: '仓库描述', - dataIndex: 'repo_desc', - width: 240, - }, - { - title: '匹配关键字', - dataIndex: 'keywords', - columnsHeaderFilterType: 'input', - width: 120, - }, - { - title: '状态', - dataIndex: 'status', - columnsHeaderFilterType: 'radio', - wizardColumnsOptions: sensitiveInfoStatus, - width: 120, - render: (value, render) => ( - + { + title: '匹配关键字', + dataIndex: 'keywords', + columnsHeaderFilterType: 'input', + width: 120, + }, + { + title: '状态', + dataIndex: 'status', + columnsHeaderFilterType: 'radio', + wizardColumnsOptions: sensitiveInfoStatus, + width: 120, + render: (value, render) => , + }, + { + title: '执行节点', + dataIndex: 'execute_node', + columnsHeaderFilterType: 'checkbox', + wizardColumnsOptions: taskNodeData, + width: 180, + }, + { + title: '发现时间', + dataIndex: 'created_at', + width: 240, + render: (val) => (val ? dayjs.unix(val).format('YYYY-MM-DD HH:mm') : '-'), + }, + ] + return ( + + + 导出 Excel + ), + }, }, - { - title: '执行节点', - dataIndex: 'execute_node', - columnsHeaderFilterType: 'checkbox', - wizardColumnsOptions: taskNodeData, - width: 180, - }, - { - title: '发现时间', - dataIndex: 'created_at', - width: 240, - render: (val) => - val ? dayjs.unix(val).format('YYYY-MM-DD HH:mm') : '-', - }, - ]; - return ( - - - 导出 Excel - - ), - }, - }, - }} - request={async (params, filter) => { - setTableFilter(filter); - const { data } = await getSensitiveMessagePage({ - ...params, - ...filter, - task_id, - }); + }} + request={async (params, filter) => { + setTableFilter(filter) + const { data } = await getSensitiveMessagePage({ + ...params, + ...filter, + task_id, + }) - return { - list: data?.list ?? [], - pagemeta: data?.pagemeta, - }; - }} - /> - ); -}; + return { + list: data?.list ?? [], + pagemeta: data?.pagemeta, + } + }} + /> + ) +} const SensitiveInfoStatus = ({ value, id }: { value: number; id: number }) => { - const status = sensitiveInfoStatus.find((item) => item.value === value); + const status = sensitiveInfoStatus.find((item) => item.value === value) - const { loading, run } = useRequest(postupdateStatus, { - manual: true, - onSuccess: () => { - message.success('修改成功'); - }, - onError: () => { - showErrorMessage('修改失败,请重试'); - }, - }); + const { loading, run } = useRequest(postupdateStatus, { + manual: true, + onSuccess: () => { + message.success('修改成功') + }, + onError: () => { + showErrorMessage('修改失败,请重试') + }, + }) - return ( - { + run({ + id, + status: value, + }) + }} + style={{ width: 120 }} + options={sensitiveInfoStatus} + /> + ) +} -export { SensitiveMessage }; +export { SensitiveMessage } diff --git a/src/pages/IRifyDashboard/IRifyDashboard.tsx b/src/pages/IRifyDashboard/IRifyDashboard.tsx index d5f1c247..8b0ef9f6 100644 --- a/src/pages/IRifyDashboard/IRifyDashboard.tsx +++ b/src/pages/IRifyDashboard/IRifyDashboard.tsx @@ -12,7 +12,7 @@ import { ArrowRightOutlined, RocketOutlined, } from '@ant-design/icons'; -import { useRequest } from 'ahooks'; +import { useQuery } from '@tanstack/react-query'; import { getSSAProjects } from '@/apis/SSAProjectApi'; import { getSSARisks } from '@/apis/SSARiskApi'; import { querySSATasks } from '@/apis/SSAScanTaskApi'; @@ -47,10 +47,7 @@ const statusLabels: Record = { cancelled: '已取消', }; -const formatProjectBatchLabel = ( - projectName?: string, - scanBatch?: number, -) => { +const formatProjectBatchLabel = (projectName?: string, scanBatch?: number) => { const name = (projectName || '').trim(); if (!name) return '未知项目'; if (scanBatch && scanBatch > 0) { @@ -59,132 +56,96 @@ const formatProjectBatchLabel = ( return name; }; -const getTotalFromRiskResp = (res: any) => Number(res?.data?.pagemeta?.total) || 0; +const getTotalFromRiskResp = (res: any) => + Number(res?.data?.pagemeta?.total) || 0; const severityOrder = ['critical', 'high', 'middle', 'low', 'info'] as const; type SeverityKey = (typeof severityOrder)[number]; -type TrendDirection = 'up' | 'down' | 'flat'; - -const getTrendTone = ( - metricKind: 'normal' | 'vuln', - direction: TrendDirection, -): 'good' | 'bad' | 'neutral' => { - if (direction === 'flat') return 'neutral'; - if (metricKind === 'vuln') { - return direction === 'down' ? 'good' : 'bad'; - } - return direction === 'up' ? 'good' : 'bad'; -}; - -const getTrendArrow = (direction: TrendDirection) => { - if (direction === 'up') return '↑'; - if (direction === 'down') return '↓'; - return '→'; -}; const IRifyDashboard: React.FC = () => { const navigate = useNavigate(); const { isDark } = useTheme(); const [workflowStep, setWorkflowStep] = useState(0); - // Fetch projects count - const { data: projectsData } = useRequest(async () => { - const res = await getSSAProjects({ page: 1, limit: 1 }); - return res.data; - }); - - // Fetch top 3 high-risk projects - const { data: topProjectsData } = useRequest(async () => { - const res = await getSSAProjects({ - page: 1, - limit: 3, - order: 'desc', - order_by: 'risk_count' - }); - return res.data; - }); - - // Fetch risks summary - const { data: risksData } = useRequest(async () => { - const res = await getSSARisks({ page: 1, limit: 1 }); - return res.data; - }); - - // Fetch severity counts using backend total for each severity. - // Do not use a small page list for stats; that causes obvious undercount. - const { data: severitySummary } = useRequest(async () => { - const [criticalRes, highRes, middleRes, warningRes, lowRes, infoRes] = - await Promise.all([ + const { data: dashboardData } = useQuery({ + queryKey: ['dashboard'], + queryFn: async () => { + const [ + projectsRes, + topProjectsRes, + risksRes, + criticalRes, + highRes, + middleRes, + warningRes, + lowRes, + infoRes, + scansRes, + ] = await Promise.all([ + getSSAProjects({ page: 1, limit: 1 }), + getSSAProjects({ + page: 1, + limit: 3, + order: 'desc', + order_by: 'risk_count', + }), + getSSARisks({ page: 1, limit: 1 }), getSSARisks({ page: 1, limit: 1, severity: 'critical' }), getSSARisks({ page: 1, limit: 1, severity: 'high' }), getSSARisks({ page: 1, limit: 1, severity: 'middle' }), getSSARisks({ page: 1, limit: 1, severity: 'warning' }), getSSARisks({ page: 1, limit: 1, severity: 'low' }), getSSARisks({ page: 1, limit: 1, severity: 'info' }), + querySSATasks({ page: 1, limit: 5 }), ]); - return { - critical: getTotalFromRiskResp(criticalRes), - high: getTotalFromRiskResp(highRes), - // warning 视为中危展示 - middle: - getTotalFromRiskResp(middleRes) + - getTotalFromRiskResp(warningRes), - low: getTotalFromRiskResp(lowRes), - info: getTotalFromRiskResp(infoRes), - }; + return { + projects: projectsRes.data, + topProjects: topProjectsRes.data, + risks: risksRes.data, + severitySummary: { + critical: getTotalFromRiskResp(criticalRes), + high: getTotalFromRiskResp(highRes), + // warning 视为中危展示 + middle: + getTotalFromRiskResp(middleRes) + + getTotalFromRiskResp(warningRes), + low: getTotalFromRiskResp(lowRes), + info: getTotalFromRiskResp(infoRes), + }, + scans: scansRes.data, + }; + }, }); - // Fetch recent scans - const { data: scansData } = useRequest(async () => { - const res = await querySSATasks({ page: 1, limit: 5 }); - return res.data; - }); + const projectCount = dashboardData?.projects?.pagemeta?.total || 0; + const riskCount = dashboardData?.risks?.pagemeta?.total || 0; + const scanCount = dashboardData?.scans?.pagemeta?.total || 0; + const topProjects = (dashboardData?.topProjects?.list || []) + .filter((project: any) => Number(project?.risk_count || 0) > 0) + .slice(0, 3); + + const severityCounts: Record = { + critical: dashboardData?.severitySummary?.critical || 0, + high: dashboardData?.severitySummary?.high || 0, + middle: dashboardData?.severitySummary?.middle || 0, + low: dashboardData?.severitySummary?.low || 0, + info: dashboardData?.severitySummary?.info || 0, + }; + + const recentScans = dashboardData?.scans?.list || []; // Calculate workflow step based on data useEffect(() => { - if ((projectsData?.pagemeta?.total || 0) > 0) { + if (projectCount > 0) { setWorkflowStep(1); - if ((scansData?.pagemeta?.total || 0) > 0) { + if (scanCount > 0) { setWorkflowStep(2); - if ((risksData?.pagemeta?.total || 0) > 0) { + if (riskCount > 0) { setWorkflowStep(3); } } } - }, [projectsData, risksData, scansData]); - - const projectCount = projectsData?.pagemeta?.total || 0; - const riskCount = risksData?.pagemeta?.total || 0; - const scanCount = scansData?.pagemeta?.total || 0; - const topProjects = (topProjectsData?.list || []) - .filter((project: any) => Number(project?.risk_count || 0) > 0) - .slice(0, 3); - - const metricTrends = { - projects: { - direction: 'up' as TrendDirection, - text: '较上周增加 2个', - }, - scans: { - direction: 'up' as TrendDirection, - text: '较上周增加 12次', - }, - vulns: { - direction: 'down' as TrendDirection, - text: '较上周减少 2.4%', - }, - }; - - // Severity breakdown should reflect global totals, not only the first page list. - const severityCounts: Record = { - critical: severitySummary?.critical || 0, - high: severitySummary?.high || 0, - middle: severitySummary?.middle || 0, - low: severitySummary?.low || 0, - info: severitySummary?.info || 0, - }; - - const recentScans = scansData?.list || []; + }, [projectCount, scanCount, riskCount]); // Dynamic text color based on theme const textColor = isDark ? '#E6EDF3' : '#1E293B'; @@ -214,7 +175,9 @@ const IRifyDashboard: React.FC = () => { {/* Workflow Stepper */} -
    0 ? 'compact' : ''}`}> +
    0 ? 'compact' : ''}`} + >
    = 1 ? 'completed' : ''} ${workflowStep === 0 ? 'active' : ''}`} @@ -281,19 +244,6 @@ const IRifyDashboard: React.FC = () => { > {projectCount}
    -
    - - {getTrendArrow( - metricTrends.projects.direction, - )} - - {metricTrends.projects.text} -
    查看全部 @@ -322,19 +272,6 @@ const IRifyDashboard: React.FC = () => { > {scanCount}
    -
    - - {getTrendArrow( - metricTrends.scans.direction, - )} - - {metricTrends.scans.text} -
    查看全部 @@ -346,7 +283,9 @@ const IRifyDashboard: React.FC = () => { - navigate(getRoutePath(RouteKey.IRIFY_VULNERABILITIES)) + navigate( + getRoutePath(RouteKey.IRIFY_VULNERABILITIES), + ) } >
    @@ -363,17 +302,6 @@ const IRifyDashboard: React.FC = () => { > {riskCount}
    -
    - - {getTrendArrow(metricTrends.vulns.direction)} - - {metricTrends.vulns.text} -
    查看全部 @@ -481,17 +409,29 @@ const IRifyDashboard: React.FC = () => { /> ) : (
    - - @@ -536,7 +476,11 @@ const IRifyDashboard: React.FC = () => {
    @@ -586,28 +532,35 @@ const IRifyDashboard: React.FC = () => { Top 3 高危项目排行
    - {topProjects.map((project: any, idx: number) => ( -
    - navigate( - getRoutePath( - RouteKey.IRIFY_VULNERABILITIES, - ), - ) - } - > - {idx + 1} - - {project.project_name} - - - {' '} - {project.risk_count || 0} - -
    - ))} + {topProjects.map( + (project: any, idx: number) => ( +
    + navigate( + getRoutePath( + RouteKey.IRIFY_VULNERABILITIES, + ), + ) + } + > + + {idx + 1} + + + { + project.project_name + } + + + {' '} + {project.risk_count || + 0} + +
    + ), + )}
    )} diff --git a/src/pages/IRifyReportManage/IRifyReportManagePage.scss b/src/pages/IRifyReportManage/IRifyReportManagePage.scss new file mode 100644 index 00000000..93d6eacd --- /dev/null +++ b/src/pages/IRifyReportManage/IRifyReportManagePage.scss @@ -0,0 +1,285 @@ +.irify-report-manage-page { + min-height: 100vh; + padding: 24px; + background-color: var(--irify-bg-base); + + .report-toolbar { + margin-bottom: 16px; + display: flex; + align-items: center; + justify-content: flex-end; + } + + .report-filter-card, + .report-table-card { + border: 1px solid var(--irify-border) !important; + border-radius: 4px !important; + background: var(--irify-bg-container) !important; + box-shadow: 0 1px 2px var(--irify-glass-shadow); + margin-bottom: 16px; + } + + .report-filter-row { + display: grid; + grid-template-columns: minmax(280px, 1.4fr) 180px 280px auto; + gap: 12px; + align-items: end; + } + + .report-filter-item { + margin-bottom: 0; + } + + .report-filter-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + padding-bottom: 1px; + } + + .report-table-card { + .ant-table { + font-size: 13px; + } + + .ant-table-thead > tr > th { + font-size: 13px; + font-weight: 600; + color: var(--irify-text-secondary); + background: transparent; + } + + .ant-table-cell { + vertical-align: top; + padding-top: 14px; + padding-bottom: 14px; + } + } + + .report-table-head { + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 16px; + margin-bottom: 14px; + + h3 { + margin: 0; + font-size: 16px; + color: var(--irify-text); + } + + .report-table-meta { + font-size: 13px; + color: var(--irify-text-secondary); + } + } + + .report-scope-cell, + .report-risk-cell, + .report-time-cell, + .report-status-cell { + display: flex; + flex-direction: column; + gap: 6px; + } + + .report-scope-head { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + } + + .report-scope-title { + font-size: 15px; + font-weight: 600; + color: var(--irify-text); + } + + .report-scope-tag { + margin-inline-end: 0; + border-radius: 999px; + } + + .report-scope-sub { + font-size: 12px; + color: var(--irify-text-secondary); + word-break: break-all; + } + + .report-scope-meta { + display: flex; + flex-wrap: wrap; + gap: 8px 14px; + font-size: 12px; + color: var(--irify-text-secondary); + } + + .report-risk-total { + display: inline-flex; + align-self: flex-start; + padding: 4px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; + + &.tone-critical { + color: #7a1fa2; + background: rgba(122, 31, 162, 0.08); + } + + &.tone-high { + color: #b42318; + background: rgba(180, 35, 24, 0.08); + } + + &.tone-medium { + color: #b54708; + background: rgba(181, 71, 8, 0.08); + } + + &.tone-clean { + color: #027a48; + background: rgba(2, 122, 72, 0.08); + } + } + + .report-risk-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + + .report-risk-hint { + font-size: 12px; + line-height: 1.6; + color: var(--irify-text-secondary); + } + + .report-time-primary { + font-weight: 600; + color: var(--irify-text); + } + + .report-time-secondary { + display: flex; + flex-direction: column; + gap: 2px; + font-size: 12px; + color: var(--irify-text-secondary); + } + + .report-status-cell { + gap: 10px; + } + + .report-action-group { + display: flex; + justify-content: flex-start; + } + + .report-file-panel { + margin-bottom: 20px; + padding: 16px 18px; + border: 1px solid var(--irify-border); + border-radius: 16px; + background: rgba(15, 23, 42, 0.02); + } + + .report-file-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 14px; + + h4 { + margin: 0; + font-size: 16px; + color: var(--irify-text); + } + } + + .report-file-list { + display: flex; + flex-direction: column; + gap: 10px; + } + + .report-file-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 12px 14px; + border: 1px solid var(--irify-border); + border-radius: 12px; + background: var(--irify-bg-container); + } + + .report-file-name { + font-size: 14px; + font-weight: 600; + color: var(--irify-text); + word-break: break-all; + } + + .report-file-sub { + display: flex; + flex-wrap: wrap; + gap: 8px 14px; + margin-top: 6px; + font-size: 12px; + color: var(--irify-text-secondary); + } + + @media (max-width: 1280px) { + .report-filter-row { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .report-filter-actions { + justify-content: flex-start; + } + } + + @media (max-width: 960px) { + padding: 16px; + + .report-table-head { + flex-direction: column; + align-items: flex-start; + } + + .report-file-head, + .report-file-item { + flex-direction: column; + align-items: flex-start; + } + } + + @media (max-width: 720px) { + .report-filter-row { + grid-template-columns: 1fr; + } + + .report-filter-actions { + width: 100%; + flex-wrap: wrap; + } + + .report-toolbar { + justify-content: stretch; + + .ant-space { + width: 100%; + display: flex; + } + + .ant-btn { + flex: 1; + } + } + } +} diff --git a/src/pages/IRifyReportManage/IRifyReportManagePage.tsx b/src/pages/IRifyReportManage/IRifyReportManagePage.tsx new file mode 100644 index 00000000..49085de8 --- /dev/null +++ b/src/pages/IRifyReportManage/IRifyReportManagePage.tsx @@ -0,0 +1,666 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { + DeleteOutlined, + EyeOutlined, + FolderOpenOutlined, + ReloadOutlined, +} from '@ant-design/icons'; +import { + Button, + Card, + DatePicker, + Drawer, + Empty, + Form, + Input, + Modal, + Space, + Table, + Tag, + message, +} from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { useRequest } from 'ahooks'; +import { useNavigate } from 'react-router-dom'; +import dayjs from 'dayjs'; +import type { Dayjs } from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import 'dayjs/locale/zh-cn'; + +import { + deleteSSAReportRecord, + fetchSSAReportRecord, + querySSAReportRecords, +} from '@/apis/SSAReportRecordApi'; +import { + createSSAReportRecordFile, + deleteSSAReportRecordFile, + downloadSSAReportRecordFile, + querySSAReportRecordFiles, +} from '@/apis/SSAReportRecordFileApi'; +import type { + TSSAReportRecord, + TSSAReportRecordDetail, +} from '@/apis/SSAReportRecordApi/type'; +import type { TSSAReportRecordFile } from '@/apis/SSAReportRecordFileApi/type'; +import ReportTemplate from '@/compoments/ReportTemplate'; +import { saveFile } from '@/utils'; +import { getRoutePath, RouteKey } from '@/utils/routeMap'; + +import './IRifyReportManagePage.scss'; + +dayjs.extend(relativeTime); +dayjs.locale('zh-cn'); + +const { RangePicker } = DatePicker; + +interface TFilterFormValues { + keyword?: string; + project_name?: string; + generated_at?: [Dayjs, Dayjs]; +} + +interface TAppliedFilters { + keyword?: string; + project_name?: string; + start?: number; + end?: number; +} + +interface TReportRecordItemJSON { + type: string; + content: string; +} + +const formatTimestamp = (value?: number) => { + if (!value || value <= 0) return '-'; + return dayjs.unix(value).format('YYYY-MM-DD HH:mm:ss'); +}; + +const formatRelativeTime = (value?: number) => { + if (!value || value <= 0) return '-'; + return dayjs.unix(value).fromNow(); +}; + +const getScanBatchText = (scanBatch?: number) => { + if (!scanBatch || scanBatch <= 0) return ''; + return `第${scanBatch}批`; +}; + +const buildScopeDisplayName = (record: TSSAReportRecord) => { + const projectName = (record.project_name || '').trim(); + const batchText = getScanBatchText(record.scan_batch); + if (projectName && batchText) { + return `${projectName} ${batchText}`; + } + if (projectName) { + return projectName; + } + return record.scope_name || record.title || '未命名报告'; +}; + +const getPreviewBlocks = (jsonRaw?: string) => { + if (!jsonRaw) return []; + try { + const parsed = JSON.parse(jsonRaw) as TReportRecordItemJSON[]; + if (!Array.isArray(parsed)) return []; + return parsed + .filter((item) => + ['markdown', 'json-table', 'search-json-table', 'raw'].includes( + item?.type, + ), + ) + .map((item) => ({ + type: item.type as + | 'markdown' + | 'json-table' + | 'search-json-table' + | 'raw', + data: item.content, + })); + } catch { + return []; + } +}; + +const IRifyReportManagePage: React.FC = () => { + const navigate = useNavigate(); + const [filterForm] = Form.useForm(); + const [page, setPage] = useState(1); + const [limit, setLimit] = useState(12); + const [filters, setFilters] = useState({}); + const [previewOpen, setPreviewOpen] = useState(false); + const [previewTitle, setPreviewTitle] = useState('报告预览'); + const [previewBlocks, setPreviewBlocks] = useState([]); + const [previewRecord, setPreviewRecord] = useState( + null, + ); + const [previewFiles, setPreviewFiles] = useState( + [], + ); + const [fileActionLoading, setFileActionLoading] = useState(''); + const previewRef = useRef(null); + + const { + data: reportResponse, + loading, + refresh, + } = useRequest( + async () => { + const res = await querySSAReportRecords({ + page, + limit, + order_by: 'published_at', + order: 'desc', + keyword: filters.keyword, + project_name: filters.project_name, + start: filters.start, + end: filters.end, + }); + return res.data; + }, + { + refreshDeps: [ + page, + limit, + filters.keyword, + filters.project_name, + filters.start, + filters.end, + ], + }, + ); + + const records = reportResponse?.list || []; + const total = reportResponse?.pagemeta?.total || 0; + + const handleFilterSubmit = useCallback((values: TFilterFormValues) => { + const [start, end] = values.generated_at || []; + setPage(1); + setFilters({ + keyword: values.keyword?.trim() || undefined, + project_name: values.project_name?.trim() || undefined, + start: start ? start.startOf('day').unix() : undefined, + end: end ? end.endOf('day').unix() : undefined, + }); + }, []); + + const handleFilterReset = useCallback(() => { + filterForm.resetFields(); + setPage(1); + setFilters({}); + }, [filterForm]); + + const handlePreview = useCallback(async (record: TSSAReportRecord) => { + try { + const [detailRes, filesRes] = await Promise.all([ + fetchSSAReportRecord(record.id), + querySSAReportRecordFiles(record.id), + ]); + const detail = detailRes.data as TSSAReportRecordDetail; + const blocks = getPreviewBlocks(detail.json_raw); + if (blocks.length === 0) { + message.warning('该报告暂无可预览内容'); + return; + } + setPreviewTitle(detail.title || record.title || '报告预览'); + setPreviewBlocks(blocks); + setPreviewRecord(record); + setPreviewFiles(filesRes.data?.list || []); + setPreviewOpen(true); + } catch { + message.error('获取报告详情失败'); + } + }, []); + + const handleDownloadFile = useCallback( + async (file: TSSAReportRecordFile) => { + if (!file.id) return; + try { + setFileActionLoading(`download-${file.id}`); + const res = await downloadSSAReportRecordFile(file.id); + if (!res.data) { + throw new Error('empty report file'); + } + saveFile( + res.data, + file.file_name || `report.${file.format || 'bin'}`, + ); + } catch { + message.error('下载文件失败'); + } finally { + setFileActionLoading(''); + } + }, + [], + ); + + const refreshPreviewFiles = useCallback(async (recordId: number) => { + const filesRes = await querySSAReportRecordFiles(recordId); + setPreviewFiles(filesRes.data?.list || []); + }, []); + + const handleCreateFile = useCallback( + async (format: 'pdf' | 'docx') => { + if (!previewRecord?.id) return; + try { + setFileActionLoading(`create-${format}`); + const created = await createSSAReportRecordFile( + previewRecord.id, + { + format, + overwrite: false, + }, + ); + if (created.data?.id) { + await refreshPreviewFiles(previewRecord.id); + await handleDownloadFile(created.data); + message.success(`${format.toUpperCase()} 文件已生成`); + } + } catch { + message.error(`${format.toUpperCase()} 文件生成失败`); + } finally { + setFileActionLoading(''); + } + }, + [handleDownloadFile, previewRecord, refreshPreviewFiles], + ); + + const handleDeleteFile = useCallback( + (file: TSSAReportRecordFile) => { + if (!file.id || !previewRecord?.id) return; + Modal.confirm({ + title: '删除导出文件', + content: `确定删除文件「${file.file_name || file.id}」吗?`, + okText: '删除', + cancelText: '取消', + okButtonProps: { danger: true }, + onOk: async () => { + try { + setFileActionLoading(`delete-${file.id}`); + await deleteSSAReportRecordFile(file.id); + await refreshPreviewFiles(previewRecord.id); + message.success('文件删除成功'); + } catch { + message.error('文件删除失败'); + } finally { + setFileActionLoading(''); + } + }, + }); + }, + [previewRecord, refreshPreviewFiles], + ); + + const handleDelete = useCallback( + (record: TSSAReportRecord) => { + Modal.confirm({ + title: '删除报告记录', + content: `确定删除报告「${record.title || record.scope_name || record.id}」吗?此操作不可恢复。`, + okText: '删除', + cancelText: '取消', + okButtonProps: { danger: true }, + onOk: async () => { + try { + await deleteSSAReportRecord(record.id); + message.success('删除成功'); + refresh(); + } catch { + message.error('删除失败'); + } + }, + }); + }, + [refresh], + ); + + const goToScans = useCallback( + (record?: TSSAReportRecord) => { + const base = getRoutePath(RouteKey.IRIFY_SCANS); + navigate( + record?.task_id ? `${base}?task_id=${record.task_id}` : base, + ); + }, + [navigate], + ); + + const columns = useMemo>( + () => [ + { + title: '报告 / 范围', + dataIndex: 'title', + key: 'title', + width: 340, + render: (_, record) => ( +
    +
    + + {record.title || '未命名报告'} + + + {record.report_type || 'ssa-scan'} + +
    +
    + {buildScopeDisplayName(record)} +
    +
    + Owner {record.owner || '-'} + + 任务范围{' '} + {record.task_count && record.task_count > 1 + ? `${record.task_count} 项` + : record.task_id || '-'} + +
    +
    + ), + }, + { + title: '安全风险', + dataIndex: 'risk_total', + key: 'risks', + width: 300, + render: (_, record) => { + const totalRiskCount = Number(record.risk_total || 0); + const toneClass = + Number(record.risk_critical || 0) > 0 + ? 'tone-critical' + : Number(record.risk_high || 0) > 0 + ? 'tone-high' + : totalRiskCount > 0 + ? 'tone-medium' + : 'tone-clean'; + return ( +
    +
    + {totalRiskCount > 0 + ? `共沉淀 ${totalRiskCount} 项风险` + : '当前报告未包含风险'} +
    +
    + + 严重 {record.risk_critical || 0} + + + 高危 {record.risk_high || 0} + + + 中危 {record.risk_medium || 0} + + + 低危 {record.risk_low || 0} + +
    +
    + ); + }, + }, + { + title: '时间', + dataIndex: 'published_at', + key: 'time', + width: 240, + render: (_, record) => ( +
    +
    + 记录生成:{formatTimestamp(record.published_at)} +
    +
    + + {formatRelativeTime(record.published_at)} + + + 源结果完成: + {formatTimestamp(record.source_finished_at)} + +
    +
    + ), + }, + { + title: '操作', + dataIndex: 'id', + key: 'actions', + fixed: 'right', + width: 300, + render: (_, record) => ( + + + + + + ), + }, + ], + [goToScans, handleDelete, handlePreview], + ); + + return ( +
    +
    + + + + +
    + + + + form={filterForm} + layout="vertical" + onFinish={handleFilterSubmit} + > +
    + + + + + + + + + +
    + + +
    +
    + +
    + + +
    +
    +

    报告记录列表

    +
    +
    + 当前页 {records.length} 项 / 共 {total} 项 +
    +
    + + + rowKey="id" + loading={loading} + dataSource={records} + columns={columns} + scroll={{ x: 1160 }} + pagination={{ + current: page, + pageSize: limit, + total, + showSizeChanger: true, + pageSizeOptions: [12, 24, 48], + onChange: (nextPage, nextPageSize) => { + setPage(nextPage); + setLimit(nextPageSize); + }, + showTotal: (all) => `共 ${all} 项`, + }} + locale={{ + emptyText: ( + + + + ), + }} + /> +
    + + setPreviewOpen(false)} + > +
    +
    +

    导出文件

    + + + + +
    + {previewFiles.length > 0 ? ( +
    + {previewFiles.map((file) => ( +
    +
    +
    + {file.file_name || + `file-${file.id}`} +
    +
    + + {String( + file.format || '', + ).toUpperCase()} + + + {file.size_bytes + ? `${(file.size_bytes / 1024).toFixed(1)} KB` + : '-'} + + + {formatTimestamp( + file.created_at, + )} + +
    +
    + + + + +
    + ))} +
    + ) : ( + + )} +
    +
    + +
    +
    +
    + ); +}; + +export default IRifyReportManagePage; diff --git a/src/pages/IRifyReportManage/index.ts b/src/pages/IRifyReportManage/index.ts new file mode 100644 index 00000000..4e91fa6e --- /dev/null +++ b/src/pages/IRifyReportManage/index.ts @@ -0,0 +1,3 @@ +import IRifyReportManagePage from './IRifyReportManagePage'; + +export default IRifyReportManagePage; diff --git a/src/pages/Login/LegacyLogin.tsx b/src/pages/Login/LegacyLogin.tsx index 7b5bc196..7a823c17 100644 --- a/src/pages/Login/LegacyLogin.tsx +++ b/src/pages/Login/LegacyLogin.tsx @@ -1,289 +1,275 @@ -import { useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Button, Card, Form, Input, message, Modal, Spin } from 'antd'; -import { showErrorMessage } from '@/utils/showErrorMessage'; -import { useRequest, useSafeState } from 'ahooks'; -import useLoginStore from '@/App/store/loginStore'; -import permissionsSliceFn from '@/App/store/powerStore'; -import { LoginIcon } from '@/assets/menu'; -import login_logo from '@/assets/compoments/telecommunicationsLogo.svg'; -// import login_logo from '@/assets/compoments/login_logo.png'; -import login_background from '@/assets/login/login_background.png'; -import { getAuth, getCaptcha, getLicense } from '@/apis/login'; -import { ExclamationCircleOutlined } from '@ant-design/icons'; +import { useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { Button, Card, Form, Input, message, Modal, Spin } from 'antd' +import { showErrorMessage } from '@/utils/showErrorMessage' +import { useRequest, useSafeState } from 'ahooks' +import useLoginStore from '@/App/store/loginStore' +import permissionsSliceFn from '@/App/store/powerStore' +import { LoginIcon } from '@/assets/menu' +// import login_logo from '@/assets/compoments/telecommunicationsLogo.svg' +// import login_logo from '@/assets/compoments/login_logo.png' +import login_logo_1 from '@/assets/compoments/images-1.png' +import login_logo_2 from '@/assets/compoments/images-2.png' + +import login_background from '@/assets/login/login_background.png' +import { getAuth, getCaptcha, getLicense } from '@/apis/login' +import { ExclamationCircleOutlined } from '@ant-design/icons' +import { resolveLicenseGateValue, shouldBypassLicense } from '@/utils/license' interface FieldType { - username: string; - password: string; - verificationCode: string; - code: string; + username: string + password: string + verificationCode: string + code: string } -const { Item } = Form; -const { Password } = Input; +const { Item } = Form +const { Password } = Input const Login = () => { - const navigate = useNavigate(); - const [form] = Form.useForm(); - const [modal, contextHolder] = Modal.useModal(); - const [buttonLoading, setButtonLoading] = useSafeState(false); + const navigate = useNavigate() + const [form] = Form.useForm() + const [modal, contextHolder] = Modal.useModal() + const [buttonLoading, setButtonLoading] = useSafeState(false) - const { login, token } = useLoginStore((state) => state); - const { updatePower } = permissionsSliceFn(); + const { login, token } = useLoginStore((state) => state) + const { updatePower } = permissionsSliceFn() + const bypassLicense = shouldBypassLicense() - const { runAsync } = useRequest( - async () => { - const { data } = await getLicense(); - const { license } = data; - return license?.length > 0 ? license : undefined; - }, - { - manual: true, - onSuccess: (license) => { - if (license) { - return navigate('/license', { state: { license } }); - } - if (token) { - return navigate('/'); - } - run(); - }, - }, - ); + const { runAsync } = useRequest( + async () => { + const { data } = await getLicense() + const { license } = data + return resolveLicenseGateValue(license) + }, + { + manual: true, + onSuccess: (license) => { + if (license) { + return navigate('/license', { state: { license } }) + } + if (token) { + return navigate('/') + } + run() + }, + }, + ) - // 获取验证码 - const { data, run, loading } = useRequest( - async () => { - const { data } = await getCaptcha(); - return data; - }, - { manual: true, onSuccess: () => setButtonLoading(false) }, - ); + // 获取验证码 + const { data, run, loading } = useRequest( + async () => { + const { data } = await getCaptcha() + return data + }, + { manual: true, onSuccess: () => setButtonLoading(false) }, + ) - const { run: authRun } = useRequest(getAuth, { - manual: true, - onSuccess: async (res) => { - if (res.data.status) { - confirm(); - } else { - const formValues = await form.validateFields(); - const verificationCode = formValues?.verificationCode; - // 将输入验证码全转为小写 - const toLowerCaseVerificationCode = - verificationCode && - verificationCode.replace(/[A-Za-z]/g, (char: string) => - char.toLowerCase(), - ); - loginFn({ - ...formValues, - code: toLowerCaseVerificationCode, - captcha_id: data?.captcha_id, - verificationCode: undefined, - }); - } - }, - onError: () => { - message.destroy(); - showErrorMessage('登录失败'); - }, - }); + const { run: authRun } = useRequest(getAuth, { + manual: true, + onSuccess: async (res) => { + if (res.data.status) { + confirm() + } else { + const formValues = await form.validateFields() + const verificationCode = formValues?.verificationCode + // 将输入验证码全转为小写 + const toLowerCaseVerificationCode = + verificationCode && verificationCode.replace(/[A-Za-z]/g, (char: string) => char.toLowerCase()) + loginFn({ + ...formValues, + code: toLowerCaseVerificationCode, + captcha_id: data?.captcha_id, + verificationCode: undefined, + }) + } + }, + onError: () => { + message.destroy() + showErrorMessage('登录失败') + }, + }) - useEffect(() => { - runAsync(); - }, []); + useEffect(() => { + if (bypassLicense) { + if (token) { + navigate('/') + } else { + run() + } + return + } + runAsync() + }, [bypassLicense, navigate, run, runAsync, token]) - const confirm = () => { - modal.confirm({ - title: '提示', - icon: , - content: '当前账号已登录,是否强制登录?', - okText: '确认', - cancelText: '取消', - async onOk() { - const formValues = form.getFieldsValue(); - const verificationCode = formValues?.verificationCode; - // 将输入验证码全转为小写 - const toLowerCaseVerificationCode = - verificationCode && - verificationCode.replace(/[A-Za-z]/g, (char: string) => - char.toLowerCase(), - ); - await loginFn({ - ...formValues, - code: toLowerCaseVerificationCode, - captcha_id: data?.captcha_id, - verificationCode: undefined, - }); - setButtonLoading(false); - }, - onCancel() { - setButtonLoading(false); - }, - }); - }; + const confirm = () => { + modal.confirm({ + title: '提示', + icon: , + content: '当前账号已登录,是否强制登录?', + okText: '确认', + cancelText: '取消', + async onOk() { + const formValues = form.getFieldsValue() + const verificationCode = formValues?.verificationCode + // 将输入验证码全转为小写 + const toLowerCaseVerificationCode = + verificationCode && verificationCode.replace(/[A-Za-z]/g, (char: string) => char.toLowerCase()) + await loginFn({ + ...formValues, + code: toLowerCaseVerificationCode, + captcha_id: data?.captcha_id, + verificationCode: undefined, + }) + setButtonLoading(false) + }, + onCancel() { + setButtonLoading(false) + }, + }) + } - // useEffect(() => { - // // 登录限制,如果有token,并且token未过期,就不能打开login页 - // if (token) { - // navigate('/'); - // } - // else { - // run(); - // } - // }, []); + // useEffect(() => { + // // 登录限制,如果有token,并且token未过期,就不能打开login页 + // if (token) { + // navigate('/'); + // } + // else { + // run(); + // } + // }, []); - const loginFn = async (values: FieldType) => { - try { - await login(values) - .then(() => { - updatePower(); - navigate('/'); - }) - .catch((err) => { - form.setFieldValue('verificationCode', undefined); - message.destroy(); - showErrorMessage(err); - run(); - }); - } catch { - setButtonLoading(false); - } finally { - setButtonLoading(false); - } - }; + const loginFn = async (values: FieldType) => { + try { + await login(values) + .then(() => { + updatePower() + navigate('/') + }) + .catch((err) => { + form.setFieldValue('verificationCode', undefined) + message.destroy() + showErrorMessage(err) + run() + }) + } catch { + setButtonLoading(false) + } finally { + setButtonLoading(false) + } + } - // 登录 - const onFinish = async (): Promise => { - try { - setButtonLoading(true); - await authRun(form.getFieldValue('username')); - } catch (err) { - setButtonLoading(false); - console.error(err); - } - }; + // 登录 + const onFinish = async (): Promise => { + try { + setButtonLoading(true) + await authRun(form.getFieldValue('username')) + } catch (err) { + setButtonLoading(false) + console.error(err) + } + } - return ( -
    -
    +
    +
    +
    +
    + + +
    +
    + +
    + +
    登录
    +
    +
    + + name="username" + rules={[ + { + required: true, + message: '请输入用户名字', + }, + ]} + > + + + + name="password" + rules={[ + { + required: true, + message: '请输入密码', + }, + ]} + extra={ + + } + > + + +
    + + name="verificationCode" + rules={[ + { + required: true, + message: '请输入验证码', + }, + { + min: 4, + message: '验证码格式不正确', + }, + ]} + > + + +
    + {!loading ? ( + run()} + src={`data:image/png;base64,${data?.master_image_base64}`} + /> + ) : ( + + )} +
    +
    + + - } - > - - -
    - - name="verificationCode" - rules={[ - { - required: true, - message: '请输入验证码', - }, - { - min: 4, - message: '验证码格式不正确', - }, - ]} - > - - -
    - {!loading ? ( - run()} - src={`data:image/png;base64,${data?.master_image_base64}`} - /> - ) : ( - - )} -
    -
    - - - - -
    -
    - {contextHolder} -
    - ); -}; -export default Login; + htmlType="submit" + loading={buttonLoading} + > + 登录 + + + + +
    + {contextHolder} +
    + ) +} +export default Login diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index 8f654ad6..1618ee59 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -1,18 +1,16 @@ -import LegacyLogin from './LegacyLogin'; -import IRifyLogin from './IRifyLogin'; +import LegacyLogin from './LegacyLogin' +import IRifyLogin from './IRifyLogin' -const APP_MODE = import.meta.env.VITE_APP_MODE as string; +const APP_MODE = import.meta.env.VITE_APP_MODE as string const loginComponents = { - legacy: LegacyLogin, - irify: IRifyLogin, -}; + legacy: LegacyLogin, + irify: IRifyLogin, +} const Login = () => { - const LoginComponent = - loginComponents[APP_MODE as keyof typeof loginComponents] || - LegacyLogin; - return ; -}; + const LoginComponent = loginComponents[APP_MODE as keyof typeof loginComponents] || LegacyLogin + return +} -export default Login; +export default Login diff --git a/src/pages/NodeConfig/NodeConfig.tsx b/src/pages/NodeConfig/NodeConfig.tsx index 930e80e2..0d5a03c7 100644 --- a/src/pages/NodeConfig/NodeConfig.tsx +++ b/src/pages/NodeConfig/NodeConfig.tsx @@ -1,250 +1,197 @@ -import type { FC } from 'react'; -import { useMemo } from 'react'; -import { CopyOutlined } from '@ant-design/icons'; -import { useRequest, useSafeState } from 'ahooks'; -import { - Alert, - Button, - Card, - Form, - Input, - InputNumber, - message, - Radio, - Select, - Spin, -} from 'antd'; -import { showErrorMessage } from '@/utils/showErrorMessage'; -import { getFileExists } from '@/apis/NodeConfigApi'; -import { copyToClipboard } from '@/utils'; - -const { Item } = Form; +import type { FC } from 'react' +import { useMemo } from 'react' +import { CopyOutlined } from '@ant-design/icons' +import { useRequest, useSafeState } from 'ahooks' +import { Alert, Button, Card, Form, Input, InputNumber, message, Radio, Select, Spin } from 'antd' +import { showErrorMessage } from '@/utils/showErrorMessage' +import { getFileExists } from '@/apis/NodeConfigApi' +import { copyToClipboard } from '@/utils' -const NodeConfig: FC = () => { - const [form] = Form.useForm(); - const [runCode, setRunCode] = useSafeState(); - const location = useMemo(() => window.location, []); - const serverHost = location.hostname; - const serverPort = - location.port || (location.protocol === 'https:' ? '443' : '80'); - - const { loading, runAsync } = useRequest(getFileExists, { manual: true }); - - const buildHostCommand = ( - nodeName: string, - goarch: 'amd64', - maxParallel: number, - ) => { - return [ - `curl -o yak "http://${location.host}/api/download/agent?goarch=${goarch}&goos=linux&name=yak"`, - 'chmod +x yak', - `SCANNODE_MAX_PARALLEL=${maxParallel} ./yak mq --server ${serverHost} --server-port ${serverPort} --id ${nodeName}`, - ].join(' && '); - }; - - const buildDockerCommand = ( - nodeName: string, - goarch: 'amd64', - maxParallel: number, - ) => { - return [ - `export YAKNODE_ID=${nodeName}`, - `export SERVER_HOST=${serverHost}`, - `export SERVER_PORT=${serverPort}`, - `export SCANNODE_MAX_PARALLEL=${maxParallel}`, - `curl -o yak "http://${location.host}/api/download/agent?goarch=${goarch}&goos=linux&name=yak"`, - 'chmod +x yak', - `curl -o docker-compose.yml "http://${location.host}/api/download/agent?goarch=${goarch}&goos=linux&name=docker-compose"`, - 'docker-compose up -d', - ].join(' && '); - }; - - const buildWindowsCommand = ( - nodeName: string, - goarch: 'amd64', - maxParallel: number, - ) => { - return [ - `$env:YAKNODE_ID="${nodeName}"`, - `$env:SERVER_HOST="${serverHost}"`, - `$env:SERVER_PORT="${serverPort}"`, - `$env:SCANNODE_MAX_PARALLEL="${maxParallel}"`, - `Invoke-WebRequest -Uri "http://${location.host}/api/download/agent?goarch=${goarch}&goos=windows&name=yak" -OutFile "yak.exe"`, - '.\\yak.exe mq --server $env:SERVER_HOST --server-port $env:SERVER_PORT --id $env:YAKNODE_ID', - ].join('; '); - }; - - const handleGenerate = async () => { - setRunCode(undefined); - try { - const data = await form.validateFields(); - const { point_type, node_name, goarch, max_parallel } = data; - const requiredFile: 'yak' | 'docker-compose' = - point_type === 'docker' ? 'docker-compose' : 'yak'; - // /file-exists 只做探活,不依赖响应体字段做阻断(该接口非标准 ResponseData) - await runAsync(requiredFile); - - if (point_type === 'host') { - setRunCode(buildHostCommand(node_name, goarch, max_parallel)); - return; - } - if (point_type === 'docker') { - setRunCode(buildDockerCommand(node_name, goarch, max_parallel)); - return; - } - setRunCode(buildWindowsCommand(node_name, goarch, max_parallel)); - } catch { - showErrorMessage('请检查输入参数后重试'); - } - }; - - const handleCopy = () => { - copyToClipboard(runCode!) - .then(() => { - message.success('复制成功'); - }) - .catch(() => { - message.info('复制失败,请重试'); - }); - }; - - return ( -
    - -
    - 节点安装与并发配置 -
    -
    - 在此生成节点启动命令。并发参数会以环境变量方式注入,适用于 - ScanNode 同机并发扫描控制。 -
    -
    - - -
    -
    - 节点基础信息 -
    -
    - - - - - - - - + + + + + + + + {title === '重置密码' ? ( + + { - model.close(); - form.resetFields(); - }} - > - 取消 - - - - } - width={550} - modal={model} - title={title} - afterClose={() => form.resetFields()} - > -
    - - - - - {title === '重置密码' ? ( - - - - ) : ( - - - - )} - {title !== '重置密码' && ( - { - if ( - typeof value === 'number' && - value < 1 - ) { - return Promise.reject( - new Error( - '账号有效期最少为一天', - ), - ); - } - if ( - typeof value === 'number' && - value > 100 * 10000 - ) { - return Promise.reject( - new Error( - '账号有效期最多为一百万天', - ), - ); - } - return Promise.resolve(); - }, - }, - ]} - > - - - )} - -
    - - ); -}); + icon={} + type="link" + className="p-0 m-0 h-4 w-4" + onClick={() => { + navigator.clipboard.writeText(form.getFieldValue('password')) + message.success('复制成功') + }} + /> + ) : null + } + /> +
    + ) : ( + + + + )} + {title !== '重置密码' && ( + { + if (typeof value === 'number' && value < 1) { + return Promise.reject(new Error('账号有效期最少为一天')) + } + if (typeof value === 'number' && value > 100 * 10000) { + return Promise.reject(new Error('账号有效期最多为一百万天')) + } + return Promise.resolve() + }, + }, + ]} + > + + + )} + +
    + + ) +}) -export { CreateUserModal }; +export { CreateUserModal } diff --git a/src/pages/TaskDetail/TaskDetail.tsx b/src/pages/TaskDetail/TaskDetail.tsx index dfb1a987..47e9aa8d 100644 --- a/src/pages/TaskDetail/TaskDetail.tsx +++ b/src/pages/TaskDetail/TaskDetail.tsx @@ -1,475 +1,427 @@ -import type { FC } from 'react'; -import { useEffect, useMemo } from 'react'; +import type { FC } from 'react' +import { useEffect, useMemo } from 'react' -import type { RadioChangeEvent } from 'antd'; -import { Empty, Radio, Spin } from 'antd'; -import { match } from 'ts-pattern'; -import { useRequest, useSafeState } from 'ahooks'; +import type { RadioChangeEvent } from 'antd' +import { Empty, Radio, Spin } from 'antd' +import { match } from 'ts-pattern' +import { useRequest, useSafeState } from 'ahooks' -import { WizardTable } from '@/compoments'; +import { WizardTable } from '@/compoments' import { - AssertsDataColumns, - AssetsVulnsColumns, - companyInfoColumns, - domainInfoColumns, - ProtColumns, -} from './compoments/Columns'; -import { TaskDetailSider } from './compoments/TaskDetailSider'; + AssertsDataColumns, + AssetsVulnsColumns, + companyInfoColumns, + domainInfoColumns, + ProtColumns, +} from './compoments/Columns' +import { TaskDetailSider } from './compoments/TaskDetailSider' import { - postAssertsData, - postAssetsProts, - postAssetsVulns, - getTaskDetail, - getTaskDetailTop, - getAssetsValueFilter, -} from '@/apis/taskDetail'; -import type { RequestFunction } from '@/compoments/WizardTable/types'; -import { useDependentCallback } from '@/hooks/useDependentCallback'; -import { useLocation } from 'react-router-dom'; -import { - detailHeaderGroupOptions, - SeverityMapTag, - targetRouteMap, -} from './compoments/utils'; + postAssertsData, + postAssetsProts, + postAssetsVulns, + getTaskDetail, + getTaskDetailTop, + getAssetsValueFilter, +} from '@/apis/taskDetail' +import type { RequestFunction } from '@/compoments/WizardTable/types' +import { useDependentCallback } from '@/hooks/useDependentCallback' +import { useLocation } from 'react-router-dom' +import { detailHeaderGroupOptions, SeverityMapTag, targetRouteMap } from './compoments/utils' -import { AssetsVulnsFilterDrawer } from './compoments/TableOptionsFilterDrawer/AssetsVulnsFilterDrawer'; -import { AssetsProtsFilterDrawer } from './compoments/TableOptionsFilterDrawer/AssetsProtsFilterDrawer'; -import { AssertsDataFilterDrawer } from './compoments/TableOptionsFilterDrawer/AssertsDataFilterDrawer'; -import dayjs from 'dayjs'; -import { UploadOutlined } from '@ant-design/icons'; -import { SensitiveMessage } from '../DataService/SensitiveMessage'; -import { getCompanyInfo, getDomainInfo } from '@/apis/MessageCollectApi'; -import { routeList, scriptTypeOption } from '../TaskScript/data'; -import { TaskRoadmap } from './TaskRoadmap'; -import type { TDetailDatailOptions, TTaskDetailHeaderGroups } from './types'; -import { exprotFileName, ExportRequestKey } from './types'; +import { AssetsVulnsFilterDrawer } from './compoments/TableOptionsFilterDrawer/AssetsVulnsFilterDrawer' +import { AssetsProtsFilterDrawer } from './compoments/TableOptionsFilterDrawer/AssetsProtsFilterDrawer' +import { AssertsDataFilterDrawer } from './compoments/TableOptionsFilterDrawer/AssertsDataFilterDrawer' +import dayjs from 'dayjs' +import { UploadOutlined } from '@ant-design/icons' +import { SensitiveMessage } from '../DataService/SensitiveMessage' +import { getCompanyInfo, getDomainInfo } from '@/apis/MessageCollectApi' +import { routeList, scriptTypeOption } from '../TaskScript/data' +import { TaskRoadmap } from './TaskRoadmap' +import type { TDetailDatailOptions, TTaskDetailHeaderGroups } from './types' +import { exprotFileName, ExportRequestKey } from './types' -const { Group } = Radio; +const { Group } = Radio const TaskDetail: FC = () => { - const [page] = WizardTable.usePage(); - const location = useLocation(); - - const { record } = location.state || {}; // 获取传递的 record 数据 - const [scriptType, setScriptType] = useSafeState([]); - const [tableFilter, setTableFilter] = useSafeState< - Record | undefined - >({}); - - const [headerGroupValue, setHeaderGroupValue] = - useSafeState(1); - const [columns, setColumns] = useSafeState([]); + const [page] = WizardTable.usePage() + const location = useLocation() - useEffect(() => { - const keys = Object.keys(targetRouteMap); - setHeaderGroupValue(() => (keys.includes(record?.script_type) ? 0 : 1)); - }, [record?.script_type]); + const { record } = location.state || {} // 获取传递的 record 数据 + const [scriptType, setScriptType] = useSafeState([]) + const [tableFilter, setTableFilter] = useSafeState | undefined>({}) - // 获取基础信息 - const { data, runAsync, loading } = useRequest( - async (id, task_id) => { - const result = await getTaskDetail(task_id); - const resultTop = await getTaskDetailTop(id); - const { data: dataTop } = resultTop; - const { data } = result; - const combinationData = [ - { - label: '任务ID', - value: dataTop?.task_id, - }, - { - label: '目标', - value: dataTop?.params?.target, - }, - { - label: '端口', - value: dataTop?.params?.ports, - }, - { - label: 'enable-brute', - value: dataTop?.params?.['enable-brute'], - }, - { - label: 'enable-cve-baseline', - value: dataTop?.params?.['enable-cve-baseline'], - }, - // { - // label: '节点', - // value: dataTop?.params?.plugins, - // }, - { - label: '存活主机数', - value: data.ip_num ?? 0, - }, - { - label: '开放端口数', - value: data?.port_num ?? 0, - }, - { - label: '漏洞与风险', - value: data.risk_num ?? 0, - }, - // TODO 缺少字段 - // { - // label: '收集资产数', - // value: data.risk_num ?? 0, - // }, - ]; + const [headerGroupValue, setHeaderGroupValue] = useSafeState(1) + const [columns, setColumns] = useSafeState([]) - const filterData: TDetailDatailOptions = combinationData.filter( - (item) => - item.value !== undefined && - item.value !== 'undefined' && - item.value !== null, - ); + useEffect(() => { + const keys = Object.keys(targetRouteMap) + setHeaderGroupValue(() => (keys.includes(record?.script_type) ? 0 : 1)) + }, [record?.script_type]) - return filterData; + // 获取基础信息 + const { data, runAsync, loading } = useRequest( + async (id, task_id) => { + const result = await getTaskDetail(task_id) + const resultTop = await getTaskDetailTop(id) + const { data: dataTop } = resultTop + const { data } = result + const combinationData = [ + { + label: '任务ID', + value: dataTop?.task_id, + }, + { + label: '目标', + value: dataTop?.params?.target, + }, + { + label: '端口', + value: dataTop?.params?.ports, + }, + { + label: 'enable-brute', + value: dataTop?.params?.['enable-brute'], }, { - manual: true, // 手动触发请求 + label: 'enable-cve-baseline', + value: dataTop?.params?.['enable-cve-baseline'], }, - ); + // { + // label: '节点', + // value: dataTop?.params?.plugins, + // }, + { + label: '存活主机数', + value: data.ip_num ?? 0, + }, + { + label: '开放端口数', + value: data?.port_num ?? 0, + }, + { + label: '漏洞与风险', + value: data.risk_num ?? 0, + }, + // TODO 缺少字段 + // { + // label: '收集资产数', + // value: data.risk_num ?? 0, + // }, + ] - const { runAsync: assetsVulnsFilterrunAsync } = useRequest( - async () => { - const { data } = await getAssetsValueFilter({ - task_id: record?.task_id, - }); - const { list, severity } = data; - // 映射漏洞等级 字段 - const transformSeverityList = severity?.map((it) => { - const label = SeverityMapTag.find((item) => - item.key.includes(it.key ?? ''), - )?.name; - return { - Verbose: label, - Total: it.value, - value: it.key, - label: label, - }; - }); + const filterData: TDetailDatailOptions = combinationData.filter( + (item) => item.value !== undefined && item.value !== 'undefined' && item.value !== null, + ) - // 映射漏洞类型Top 10 字段 - const transformList = list?.map((it) => ({ - Verbose: it.key, - Total: it.value, - value: it.key, - label: it.key, - })); - return { - transformSeverityList, - transformList, - }; - }, - { manual: true }, - ); + return filterData + }, + { + manual: true, // 手动触发请求 + }, + ) - const [isReady, setIsReady] = useSafeState(false); // 控制页面是否可以渲染 - const [tableLoading, setTableLoadings] = useSafeState(false); + const { runAsync: assetsVulnsFilterrunAsync } = useRequest( + async () => { + const { data } = await getAssetsValueFilter({ + task_id: record?.task_id, + }) + const { list, severity } = data + // 映射漏洞等级 字段 + const transformSeverityList = severity?.map((it) => { + const label = SeverityMapTag.find((item) => item.key.includes(it.key ?? ''))?.name + return { + Verbose: label, + Total: it.value, + value: it.key, + label: label, + } + }) - useEffect(() => { - // 请求数据并等待完成 - runAsync(record?.id, record?.task_id) - .then(() => { - setIsReady(true); // 数据加载完成,允许渲染 - }) - .catch((error) => { - setIsReady(true); // 数据加载完成,允许渲染 - console.error('加载任务详情失败:', error); - }); - }, [runAsync]); + // 映射漏洞类型Top 10 字段 + const transformList = list?.map((it) => ({ + Verbose: it.key, + Total: it.value, + value: it.key, + label: it.key, + })) + return { + transformSeverityList, + transformList, + } + }, + { manual: true }, + ) - const tableHeaderGroupOptions = useMemo(() => { - const filterTypeList = ['subdomain_scan', 'company_scan']; - const keys = Object.keys(targetRouteMap); - const targetOptions = filterTypeList.includes(record?.script_type) - ? detailHeaderGroupOptions - : detailHeaderGroupOptions.filter((item) => item.value !== 5); - const resultOptions = keys.includes(record?.script_type) - ? targetOptions - : targetOptions.filter((item) => item.value !== 0); - return resultOptions; - }, [record?.script_type]); + const [isReady, setIsReady] = useSafeState(false) // 控制页面是否可以渲染 + const [tableLoading, setTableLoadings] = useSafeState(false) - useEffect(() => { - const targetScriptType = scriptTypeOption - .filter((item) => item.value !== 'weakinfo') - .map((item) => item.value); - setScriptType(targetScriptType); - }, [record?.script_type]); + useEffect(() => { + // 请求数据并等待完成 + runAsync(record?.id, record?.task_id) + .then(() => { + setIsReady(true) // 数据加载完成,允许渲染 + }) + .catch((error) => { + setIsReady(true) // 数据加载完成,允许渲染 + console.error('加载任务详情失败:', error) + }) + }, [runAsync]) - // table 请求 此处因 columns 渲染为静态,所以等 datasource 数据回来之后在渲染columns, 解决竞态请求问题 - const requestCallback = useDependentCallback( - ( - params: Parameters['0'], - filter: Parameters['1'], - ) => { - return match(headerGroupValue) - .with(0, async () => null) - .with(1, async () => { - try { - setTableLoadings(true); - const { data } = await postAssetsProts({ - ...params, - ...filter, - order_by: filter?.order ? 'updated_at' : undefined, - task_id: record?.task_id, - }); - const targetProtColumns = ProtColumns().map((item) => - item.dataIndex === 'execute_node' - ? { - ...item, - wizardColumnsOptions: - record?.scanner?.map( - // eslint-disable-next-line max-nested-callbacks - (it: string) => ({ - label: it, - value: it, - }), - ), - } - : item, - ); - setColumns(targetProtColumns); - setTableLoadings(false); - return { - data, - }; - } catch { - setTableLoadings(false); - } - }) - .with(2, async () => { - try { - setTableLoadings(true); - const { data } = await postAssetsVulns({ - ...params, - ...filter, - task_id: record?.task_id, - }); - await assetsVulnsFilterrunAsync() - .then((filterData) => { - const targetFilterData = { - ...filterData, - taskNodeData: record?.scanner?.map( - // eslint-disable-next-line max-nested-callbacks - (it: string) => ({ - label: it, - value: it, - }), - ), - }; - const assetsVulnsColumns = - AssetsVulnsColumns(targetFilterData); - setColumns(assetsVulnsColumns); - }) - .catch(() => - AssetsVulnsColumns({ - transformSeverityList: [], - transformList: [], - }), - ); - setTableLoadings(false); - return { - data, - }; - } catch { - setTableLoadings(false); - } - }) - .with(3, async () => { - try { - setTableLoadings(true); - const { data } = await postAssertsData({ - ...params, - ...filter, - task_id: record?.task_id, - }); - setTableLoadings(false); - setColumns(AssertsDataColumns); - return { - data, - }; - } catch { - setTableLoadings(false); - } - }) - .with(4, async () => { - try { - setTableLoadings(true); - const { data } = await getCompanyInfo({ - ...params, - ...filter, - from_task_id: record?.task_id, - }); - setTableLoadings(false); - setColumns(companyInfoColumns); - return { - data, - }; - } catch { - setTableLoadings(false); - } - }) - .with(5, async () => { - try { - setTableLoadings(true); - const { data } = await getDomainInfo({ - ...params, - ...filter, - from_task_id: record?.task_id, - }); - setTableLoadings(false); - setColumns(domainInfoColumns); - return { - data, - }; - } catch { - setTableLoadings(false); - } - }) - .exhaustive(); - }, - [headerGroupValue], - ); + const tableHeaderGroupOptions = useMemo(() => { + const filterTypeList = ['subdomain_scan', 'company_scan'] + const keys = Object.keys(targetRouteMap) + const targetOptions = filterTypeList.includes(record?.script_type) + ? detailHeaderGroupOptions + : detailHeaderGroupOptions.filter((item) => item.value !== 5) + const resultOptions = keys.includes(record?.script_type) + ? targetOptions + : targetOptions.filter((item) => item.value !== 0) + return resultOptions + }, [record?.script_type]) - if (!isReady || loading) { - return ( - - ); - } + useEffect(() => { + const targetScriptType = scriptTypeOption.filter((item) => item.value !== 'weakinfo').map((item) => item.value) + setScriptType(targetScriptType) + }, [record?.script_type]) - // 枚举 展示table 高级筛选抽屉值 - const tableFilterEnum = (type: 1 | 2 | 3) => { - return match(type) - .with(1, () => ( - - )) - .with(2, () => ( - - )) - .with(3, () => ( - - )) - .exhaustive(); - }; + // table 请求 此处因 columns 渲染为静态,所以等 datasource 数据回来之后在渲染columns, 解决竞态请求问题 + const requestCallback = useDependentCallback( + (params: Parameters['0'], filter: Parameters['1']) => { + return match(headerGroupValue) + .with(0, async () => null) + .with(1, async () => { + try { + setTableLoadings(true) + const { data } = await postAssetsProts({ + ...params, + ...filter, + order_by: filter?.order ? 'updated_at' : undefined, + task_id: record?.task_id, + }) + const targetProtColumns = ProtColumns().map((item) => + item.dataIndex === 'execute_node' + ? { + ...item, + wizardColumnsOptions: record?.scanner?.map( + // eslint-disable-next-line max-nested-callbacks + (it: string) => ({ + label: it, + value: it, + }), + ), + } + : item, + ) + setColumns(targetProtColumns) + setTableLoadings(false) + return { + data, + } + } catch { + setTableLoadings(false) + } + }) + .with(2, async () => { + try { + setTableLoadings(true) + const { data } = await postAssetsVulns({ + ...params, + ...filter, + task_id: record?.task_id, + }) + await assetsVulnsFilterrunAsync() + .then((filterData) => { + const targetFilterData = { + ...filterData, + taskNodeData: record?.scanner?.map( + // eslint-disable-next-line max-nested-callbacks + (it: string) => ({ + label: it, + value: it, + }), + ), + } + const assetsVulnsColumns = AssetsVulnsColumns(targetFilterData) + setColumns(assetsVulnsColumns) + }) + .catch(() => + AssetsVulnsColumns({ + transformSeverityList: [], + transformList: [], + }), + ) + setTableLoadings(false) + return { + data, + } + } catch { + setTableLoadings(false) + } + }) + .with(3, async () => { + try { + setTableLoadings(true) + const { data } = await postAssertsData({ + ...params, + ...filter, + task_id: record?.task_id, + }) + setTableLoadings(false) + setColumns(AssertsDataColumns) + return { + data, + } + } catch { + setTableLoadings(false) + } + }) + .with(4, async () => { + try { + setTableLoadings(true) + const { data } = await getCompanyInfo({ + ...params, + ...filter, + from_task_id: record?.task_id, + }) + setTableLoadings(false) + setColumns(companyInfoColumns) + return { + data, + } + } catch { + setTableLoadings(false) + } + }) + .with(5, async () => { + try { + setTableLoadings(true) + const { data } = await getDomainInfo({ + ...params, + ...filter, + from_task_id: record?.task_id, + }) + setTableLoadings(false) + setColumns(domainInfoColumns) + return { + data, + } + } catch { + setTableLoadings(false) + } + }) + .exhaustive() + }, + [headerGroupValue], + ) - const headerGroupChange = (e: RadioChangeEvent) => { - setHeaderGroupValue(e.target.value); - if (e.target.value !== 0) { - page.clear(); - page.onLoad({ - task_type: e.target.value, - }); - } - }; + if (!isReady || loading) { + return + } - return ( -
    - - {routeList.includes(record?.script_type) && - headerGroupValue === 0 ? ( - { + return match(type) + .with(1, () => ) + .with(2, () => ) + .with(3, () => ) + .exhaustive() + } + + const headerGroupChange = (e: RadioChangeEvent) => { + setHeaderGroupValue(e.target.value) + if (e.target.value !== 0) { + page.clear() + page.onLoad({ + task_type: e.target.value, + }) + } + } + + return ( +
    + + {routeList.includes(record?.script_type) && headerGroupValue === 0 ? ( + + ) : scriptType.includes(record?.script_type) ? ( + + - ) : scriptType.includes(record?.script_type) ? ( - - - - ), - options: { - dowloadFile: - headerGroupValue === 0 || - headerGroupValue === 4 || - headerGroupValue === 5 - ? undefined - : { - fileName: - `${exprotFileName[headerGroupValue]} (` + - dayjs().unix() + - ').csv', - params: { - type: ExportRequestKey?.[ - headerGroupValue - ], - data: { - ...tableFilter, - limit: -1, - task_id: record?.task_id, - }, - }, - url: '/assets/export/report', - method: 'post', - type: 'primary', - title: ( -
    - - - 导出 Excel - -
    - ), - }, - ProFilterSwitch: { - trigger: - headerGroupValue === 0 || - headerGroupValue === 4 || - headerGroupValue === 5 - ? null - : tableFilterEnum(headerGroupValue), - layout: 'vertical', - }, + + ), + options: { + dowloadFile: + headerGroupValue === 0 || headerGroupValue === 4 || headerGroupValue === 5 + ? undefined + : { + fileName: `${exprotFileName[headerGroupValue]} (` + dayjs().unix() + ').csv', + params: { + type: ExportRequestKey?.[headerGroupValue], + data: { + ...tableFilter, + limit: -1, + task_id: record?.task_id, }, - }} - request={async (params, filter) => { - setTableFilter(filter); - const { data } = await requestCallback(params, filter); + }, + url: '/api/assets/export/report', + method: 'post', + type: 'primary', + title: ( +
    + + 导出 Excel +
    + ), + }, + ProFilterSwitch: { + trigger: + headerGroupValue === 0 || headerGroupValue === 4 || headerGroupValue === 5 + ? null + : tableFilterEnum(headerGroupValue), + layout: 'vertical', + }, + }, + }} + request={async (params, filter) => { + setTableFilter(filter) + const { data } = await requestCallback(params, filter) - return { - list: data?.list ?? [], - pagemeta: data?.pagemeta, - }; - }} - /> - ) : record?.script_type === 'weakinfo' ? ( - - ) : ( - - )} -
    - ); -}; + return { + list: data?.list ?? [], + pagemeta: data?.pagemeta, + } + }} + /> + ) : record?.script_type === 'weakinfo' ? ( + + ) : ( + + )} +
    + ) +} -export { TaskDetail }; +export { TaskDetail } diff --git a/src/pages/TaskDetail/compoments/Columns.tsx b/src/pages/TaskDetail/compoments/Columns.tsx index 85954f72..867c0f50 100644 --- a/src/pages/TaskDetail/compoments/Columns.tsx +++ b/src/pages/TaskDetail/compoments/Columns.tsx @@ -1,330 +1,265 @@ -import { message, Tag } from 'antd'; -import dayjs from 'dayjs'; +import { message, Tag } from 'antd' +import dayjs from 'dayjs' -import type { - TGetAssertsDataResponse, - TGetAssetsProtsResponse, - TGetAssetsVulnsResponse, -} from '@/apis/taskDetail/types'; -import type { CreateTableProps } from '@/compoments/WizardTable/types'; +import type { TGetAssertsDataResponse, TGetAssetsProtsResponse, TGetAssetsVulnsResponse } from '@/apis/taskDetail/types' +import type { CreateTableProps } from '@/compoments/WizardTable/types' -import { YakitTag } from '@/compoments/YakitTag/YakitTag'; -import type { YakitTagColor } from '@/compoments/YakitTag/YakitTagType'; -import { AssetsVulnsDetailOperate } from './AssetsVulnsDetailOperate'; -import { SeverityMapTag, survivalStatusList } from './utils'; -import CopyOutlined from './utils/CopyOutlined'; -import { copyToClipboard } from '@/utils'; -import type { - TGetCompanyInfoResponse, - TGetDomainInfoResponse, -} from '@/apis/MessageCollectApi/type'; +import { AssetsVulnsDetailOperate } from './AssetsVulnsDetailOperate' +import { SeverityMapTag, survivalStatusList } from './utils' +import CopyOutlined from './utils/CopyOutlined' +import { copyToClipboard } from '@/utils' +import type { TGetCompanyInfoResponse, TGetDomainInfoResponse } from '@/apis/MessageCollectApi/type' +import { YakitTag } from '@/compoments/yakitUI/YakitTag/YakitTag' +import type { YakitTagColor } from '@/compoments/yakitUI/YakitTag/YakitTagType' // 端口资产 columns -const ProtColumns = (filterData?: { - taskNodeData: any[]; -}): CreateTableProps['columns'] => [ +const ProtColumns = (filterData?: { taskNodeData: any[] }): CreateTableProps['columns'] => [ + { + title: '网络地址', + dataIndex: 'host', + columnsHeaderFilterType: 'input', + width: 180, + render: (value) => + value ? ( +
    +
    {value}
    + { + copyToClipboard(value) + .then(() => { + message.success('复制成功') + }) + .catch(() => { + message.info('复制失败,请重试') + }) + }} + /> +
    + ) : ( + '-' + ), + }, + { + title: '端口', + dataIndex: 'port', + columnsHeaderFilterType: 'input', + width: 120, + render: (_, render) => (render?.port ? {render?.port} : '-'), + }, + { + title: '协议', + dataIndex: 'proto', + width: 120, + render: (value) => (value ? {value} : '-'), + }, + { + title: '服务指纹', + columnsHeaderFilterType: 'input', + dataIndex: 'service_type', + width: 180, + }, + { + title: 'Title', + dataIndex: 'task_name', + columnsHeaderFilterType: 'input', + width: 180, + }, + { + title: '执行节点', + dataIndex: 'execute_node', + columnsHeaderFilterType: 'checkbox', + wizardColumnsOptions: filterData?.taskNodeData ?? [], + width: 180, + }, + { + title: '更新时间', + dataIndex: 'order', + columnsHeaderFilterType: 'orderby', + wizardColumnsOptions: [ + { + label: '顺序', + value: 'desc', + }, + { + label: '倒序', + value: 'asc', + }, + ], + width: 260, + render: (_, record) => (record?.updated_at ? dayjs.unix(record?.updated_at).format('YYYY-MM-DD HH:ss') : '-'), + }, +] + +// 漏洞与风险 columns +const AssetsVulnsColumns = (filterData: { + transformSeverityList: any[] + transformList: any[] + taskNodeData?: any[] +}): CreateTableProps['columns'] => { + return [ { - title: '网络地址', - dataIndex: 'host', - columnsHeaderFilterType: 'input', - width: 180, - render: (value) => - value ? ( -
    -
    {value}
    - { - copyToClipboard(value) - .then(() => { - message.success('复制成功'); - }) - .catch(() => { - message.info('复制失败,请重试'); - }); - }} - /> -
    - ) : ( - '-' - ), + title: '标题', + dataIndex: 'title', + columnsHeaderFilterType: 'input', + width: 320, }, { - title: '端口', - dataIndex: 'port', - columnsHeaderFilterType: 'input', - width: 120, - render: (_, render) => - render?.port ? {render?.port} : '-', + title: '类型', + dataIndex: 'risk_type_verbose', + columnsHeaderFilterType: 'checkbox', + wizardColumnsOptions: filterData.transformList ?? [], + width: 120, }, { - title: '协议', - dataIndex: 'proto', - width: 120, - render: (value) => (value ? {value} : '-'), + title: '等级', + dataIndex: 'severity', + columnsHeaderFilterType: 'checkbox', + wizardColumnsOptions: filterData.transformSeverityList ?? [], + width: 120, + render: (value) => { + const title = SeverityMapTag.find((item) => item.key.includes(value || '')) + return {title ? title.name : value || '-'} + }, }, { - title: '服务指纹', - columnsHeaderFilterType: 'input', - dataIndex: 'service_type', - width: 180, + title: 'IP', + dataIndex: 'ip_addr', + columnsHeaderFilterType: 'input', + width: 180, }, { - title: 'Title', - dataIndex: 'task_name', - columnsHeaderFilterType: 'input', - width: 180, + title: '执行节点', + dataIndex: 'execute_node', + columnsHeaderFilterType: 'checkbox', + wizardColumnsOptions: filterData?.taskNodeData ?? [], + width: 180, }, { - title: '执行节点', - dataIndex: 'execute_node', - columnsHeaderFilterType: 'checkbox', - wizardColumnsOptions: filterData?.taskNodeData ?? [], - width: 180, + title: 'Token', + dataIndex: 'reverse_token', + width: 120, }, { - title: '更新时间', - dataIndex: 'order', - columnsHeaderFilterType: 'orderby', - wizardColumnsOptions: [ - { - label: '顺序', - value: 'desc', - }, - { - label: '倒序', - value: 'asc', - }, - ], - width: 260, - render: (_, record) => - record?.updated_at - ? dayjs.unix(record?.updated_at).format('YYYY-MM-DD HH:ss') - : '-', + title: '操作', + width: 70, + fixed: 'right', + render: (_, render) => , }, -]; - -// 漏洞与风险 columns -const AssetsVulnsColumns = (filterData: { - transformSeverityList: any[]; - transformList: any[]; - taskNodeData?: any[]; -}): CreateTableProps['columns'] => { - return [ - { - title: '标题', - dataIndex: 'title', - columnsHeaderFilterType: 'input', - width: 320, - }, - { - title: '类型', - dataIndex: 'risk_type_verbose', - columnsHeaderFilterType: 'checkbox', - wizardColumnsOptions: filterData.transformList ?? [], - width: 120, - }, - { - title: '等级', - dataIndex: 'severity', - columnsHeaderFilterType: 'checkbox', - wizardColumnsOptions: filterData.transformSeverityList ?? [], - width: 120, - render: (value) => { - const title = SeverityMapTag.find((item) => - item.key.includes(value || ''), - ); - return ( - - {title ? title.name : value || '-'} - - ); - }, - }, - { - title: 'IP', - dataIndex: 'ip_addr', - columnsHeaderFilterType: 'input', - width: 180, - }, - { - title: '执行节点', - dataIndex: 'execute_node', - columnsHeaderFilterType: 'checkbox', - wizardColumnsOptions: filterData?.taskNodeData ?? [], - width: 180, - }, - { - title: 'Token', - dataIndex: 'reverse_token', - width: 120, - }, - { - title: '操作', - width: 70, - fixed: 'right', - render: (_, render) => , - }, - ]; -}; - -const AssertsDataColumns: CreateTableProps['columns'] = - [ - { - title: '资产地址', - dataIndex: 'host', - columnsHeaderFilterType: 'input', - width: 120, - }, - { - title: '存活状态', - dataIndex: 'state', - width: 120, - render: (_, record) => { - const target = survivalStatusList.find( - (it) => it.value === record?.state, - ); - return target ? ( -
    - {target?.label} -
    - ) : ( - '-' - ); - }, - }, - { - title: '风险状态', - dataIndex: 'level', - width: 120, - render: (value) => { - const title = SeverityMapTag.find((item) => - item.key.includes(value || ''), - ); - return ( - - {title ? title.name : value || '-'} - - ); - }, - }, - { - title: ( -
    - 漏洞数量 - - 严重 - - - 高 - - - 中 - - - 低 - -
    - ), - dataIndex: 'id', - width: 240, - render: (_, render) => { - return ( -
    - - {render?.critical ?? 0} - - - {render?.high ?? 0} - - - {render?.warning ?? 0} - - - {render?.low ?? 0} - -
    - ); - }, - }, - { - title: '扫描时间', - dataIndex: 'updated_at', - width: 180, - render: (value) => - value ? dayjs.unix(value).format('YYYY-MM-DD HH:mm') : '-', - }, - ]; + ] +} -const companyInfoColumns: CreateTableProps['columns'] = - [ - { - title: '公司名称', - dataIndex: 'keyword', - columnsHeaderFilterType: 'input', - width: 160, - render: (_, record) => record?.company_name ?? '-', - }, - { - title: '公司层级', - dataIndex: 'company_type', - width: 80, - render: (text) => { - return text === 1 ? '一级' : '二级'; - }, - }, - { - title: '域名', - dataIndex: 'domains', - width: 240, - }, - { - title: '创建时间', - dataIndex: 'updated_at', - width: 240, - render: (text) => dayjs.unix(text).format('YYYY-MM-DD HH:mm:ss'), - }, - ]; - -const domainInfoColumns: CreateTableProps['columns'] = [ - { - title: '爆破域名', - dataIndex: 'domain', +const AssertsDataColumns: CreateTableProps['columns'] = [ + { + title: '资产地址', + dataIndex: 'host', + columnsHeaderFilterType: 'input', + width: 120, + }, + { + title: '存活状态', + dataIndex: 'state', + width: 120, + render: (_, record) => { + const target = survivalStatusList.find((it) => it.value === record?.state) + return target ?
    {target?.label}
    : '-' }, - { - title: '爆破IP', - dataIndex: 'ip_addr', + }, + { + title: '风险状态', + dataIndex: 'level', + width: 120, + render: (value) => { + const title = SeverityMapTag.find((item) => item.key.includes(value || '')) + return {title ? title.name : value || '-'} }, - { - title: '更新时间', - dataIndex: 'created_at', - render: (text) => - text ? dayjs.unix(text).format('YYYY-MM-DD HH:mm:ss') : '-', + }, + { + title: ( +
    + 漏洞数量 + + 严重 + + + 高 + + + 中 + + +
    + ), + dataIndex: 'id', + width: 240, + render: (_, render) => { + return ( +
    + + {render?.critical ?? 0} + + + {render?.high ?? 0} + + + {render?.warning ?? 0} + + {render?.low ?? 0} +
    + ) }, -]; + }, + { + title: '扫描时间', + dataIndex: 'updated_at', + width: 180, + render: (value) => (value ? dayjs.unix(value).format('YYYY-MM-DD HH:mm') : '-'), + }, +] + +const companyInfoColumns: CreateTableProps['columns'] = [ + { + title: '公司名称', + dataIndex: 'keyword', + columnsHeaderFilterType: 'input', + width: 160, + render: (_, record) => record?.company_name ?? '-', + }, + { + title: '公司层级', + dataIndex: 'company_type', + width: 80, + render: (text) => { + return text === 1 ? '一级' : '二级' + }, + }, + { + title: '域名', + dataIndex: 'domains', + width: 240, + }, + { + title: '创建时间', + dataIndex: 'updated_at', + width: 240, + render: (text) => dayjs.unix(text).format('YYYY-MM-DD HH:mm:ss'), + }, +] + +const domainInfoColumns: CreateTableProps['columns'] = [ + { + title: '爆破域名', + dataIndex: 'domain', + }, + { + title: '爆破IP', + dataIndex: 'ip_addr', + }, + { + title: '更新时间', + dataIndex: 'created_at', + render: (text) => (text ? dayjs.unix(text).format('YYYY-MM-DD HH:mm:ss') : '-'), + }, +] -export { - ProtColumns, - AssetsVulnsColumns, - AssertsDataColumns, - companyInfoColumns, - domainInfoColumns, -}; +export { ProtColumns, AssetsVulnsColumns, AssertsDataColumns, companyInfoColumns, domainInfoColumns } diff --git a/src/pages/TaskDetail/compoments/TableOptionsFilterDrawer/AssetsProtsFilterDrawer.tsx b/src/pages/TaskDetail/compoments/TableOptionsFilterDrawer/AssetsProtsFilterDrawer.tsx index 6011819c..c1df26fd 100644 --- a/src/pages/TaskDetail/compoments/TableOptionsFilterDrawer/AssetsProtsFilterDrawer.tsx +++ b/src/pages/TaskDetail/compoments/TableOptionsFilterDrawer/AssetsProtsFilterDrawer.tsx @@ -1,170 +1,123 @@ /* eslint-disable max-nested-callbacks */ -import type { FC } from 'react'; -import { useMemo } from 'react'; +import type { FC } from 'react' +import { useMemo } from 'react' -import { getAssetsProtsFilter } from '@/apis/taskDetail'; -import { useRequest } from 'ahooks'; -import { Button, Collapse, Empty, Form } from 'antd'; -import { AssetsProtsGroupTag } from './AssetsProtsGroupTag'; -import { - AssetsProtsFilterDataList, - sortDataByList, - targetTitle, - updateAssetsProtsFilterDataList, -} from './data'; -import { match, P } from 'ts-pattern'; -import type { UsePageRef } from '@/hooks/usePage'; -import styles from './AssetsProtsFilterDrawerScss.module.scss'; -import classNames from 'classnames'; +import { getAssetsProtsFilter } from '@/apis/taskDetail' +import { useRequest } from 'ahooks' +import { Button, Collapse, Empty, Form } from 'antd' +import { AssetsProtsGroupTag } from './AssetsProtsGroupTag' +import { AssetsProtsFilterDataList, sortDataByList, targetTitle, updateAssetsProtsFilterDataList } from './data' +import { match, P } from 'ts-pattern' +import type { UsePageRef } from '@/hooks/usePage' +import styles from './AssetsProtsFilterDrawerScss.module.scss' +import classNames from 'classnames' -const { Item } = Form; +const { Item } = Form // 端口资产高级筛选 -const AssetsProtsFilterDrawer: FC<{ task_id?: string; page: UsePageRef }> = ({ - task_id, - page, -}) => { - const { data } = useRequest(async () => { - const { data } = await getAssetsProtsFilter({ - task_id, - }); - const list = data?.list ?? []; +const AssetsProtsFilterDrawer: FC<{ task_id?: string; page: UsePageRef }> = ({ task_id, page }) => { + const { data } = useRequest(async () => { + const { data } = await getAssetsProtsFilter({ + task_id, + }) + const list = data?.list ?? [] - const targetIpList = list - .filter((item) => item.explain === 'cidr') - .map((item) => ({ - label: item.key, - value: item.key, - cout: parseInt(item.value, 10), - })); + const targetIpList = list + .filter((item) => item.explain === 'cidr') + .map((item) => ({ + label: item.key, + value: item.key, + cout: parseInt(item.value, 10), + })) - const targetAssetsProtsFilterDataList = { - ...AssetsProtsFilterDataList, - ipList: targetIpList, - }; + const targetAssetsProtsFilterDataList = { + ...AssetsProtsFilterDataList, + ipList: targetIpList, + } - // 调用函数更新数据 - const updatedData = updateAssetsProtsFilterDataList( - list, - targetAssetsProtsFilterDataList, - ); + // 调用函数更新数据 + const updatedData = updateAssetsProtsFilterDataList(list, targetAssetsProtsFilterDataList) - const translateKeys = Object.keys(targetTitle); - const sortedData = sortDataByList(translateKeys, updatedData); + const translateKeys = Object.keys(targetTitle) + const sortedData = sortDataByList(translateKeys, updatedData) - const resultData = { - items: sortedData, - keys: Object.keys(sortedData), - }; + const resultData = { + items: sortedData, + keys: Object.keys(sortedData), + } - return resultData; - }); + return resultData + }) - const CollapseItems = useMemo(() => { - return match(data) - .with( - P.when( - (val) => - typeof val === 'object' && - val !== null && - 'keys' in val && - Array.isArray((val as { keys: unknown }).keys) && - (val as { keys: unknown[] }).keys.length > 0, - ), - (value) => { - return ( - { - const type = - it === 'ipList' ? 'cidr' : 'service_type'; + const CollapseItems = useMemo(() => { + return match(data) + .with( + P.when( + (val) => + typeof val === 'object' && + val !== null && + 'keys' in val && + Array.isArray((val as { keys: unknown }).keys) && + (val as { keys: unknown[] }).keys.length > 0, + ), + (value) => { + return ( + { + const type = it === 'ipList' ? 'cidr' : 'service_type' - return { - key: it, - style: { - borderBottom: '1px solid #EAECF3', - borderRadius: '0px', - marginBottom: '8px', - }, - extra: ( - - ), - label: targetTitle[ - it as - | 'group' - | 'sever' - | 'data' - | 'webSever' - | 'fingerprint' - | 'ipList' - ], - children: ( -
    - - - -
    - ), - }; - })} - defaultActiveKey={value?.keys} - /> - ); - }, - ) - .otherwise(() => ); - }, [data]); + const filterCheckedList = { + cidr: getTableParams?.cidr?.filter( + (key?: string) => !targetData?.map((item) => item.value).includes(key), + ), + service_type: getTableParams?.service_type?.filter( + (key?: string) => !targetData?.map((item) => item.value).includes(key), + ), + } + page.editFilter(filterCheckedList) + }} + > + 重置 + + ), + label: targetTitle[it as 'group' | 'sever' | 'data' | 'webSever' | 'fingerprint' | 'ipList'], + children: ( +
    + + + +
    + ), + } + })} + defaultActiveKey={value?.keys} + /> + ) + }, + ) + .otherwise(() => ) + }, [data, page]) - return CollapseItems; -}; + return CollapseItems +} -export { AssetsProtsFilterDrawer }; +export { AssetsProtsFilterDrawer } diff --git a/src/pages/TaskDetail/compoments/TableOptionsFilterDrawer/AssetsProtsGroupTag.tsx b/src/pages/TaskDetail/compoments/TableOptionsFilterDrawer/AssetsProtsGroupTag.tsx index fceaf23f..83e73622 100644 --- a/src/pages/TaskDetail/compoments/TableOptionsFilterDrawer/AssetsProtsGroupTag.tsx +++ b/src/pages/TaskDetail/compoments/TableOptionsFilterDrawer/AssetsProtsGroupTag.tsx @@ -1,43 +1,40 @@ -import { randomString } from '@/utils'; -import { Checkbox } from 'antd'; -import type { FC } from 'react'; +import { Checkbox } from 'antd' +import type { FC } from 'react' interface TIpTagProps { - value?: string[]; - onChange?: (value: TIpTagProps['value']) => TIpTagProps['value']; - data: Array; + value?: string[] + onChange?: (value: TIpTagProps['value']) => TIpTagProps['value'] + data: Array<{ label: string; value: string; cout: number }> } -const AssetsProtsGroupTag: FC = ({ - value = [], - onChange, - data, -}) => { - return ( -
    - {data.map((it) => ( -
    { - onChange?.( - value?.includes(it.value) - ? value?.filter((key) => key !== it.value) - : value?.concat(it.value), - ); - }} - className={`flex items-center justify-between cursor-pointer mt-3 color-${ - value?.includes(it.value) ? '[#4A94F8]' : '[#31343F]' - }`} - > -
    - -
    {it.label}
    -
    -
    {it.cout}
    -
    - ))} +const AssetsProtsGroupTag: FC = ({ value = [], onChange, data }) => { + const toggleValue = (itemValue: string) => { + onChange?.(value.includes(itemValue) ? value.filter((key) => key !== itemValue) : [...value, itemValue]) + } + + return ( +
    + {data.map((it) => ( +
    toggleValue(it.value)} + > +
    + e.stopPropagation()} + onChange={() => toggleValue(it.value)} + /> +
    {it.label}
    +
    +
    {it.cout}
    - ); -}; + ))} +
    + ) +} -export { AssetsProtsGroupTag }; +export { AssetsProtsGroupTag } diff --git a/src/pages/TaskPageList/compoment/TaskOperateTableRender.tsx b/src/pages/TaskPageList/compoment/TaskOperateTableRender.tsx index 1ffcf60d..56dd6c3c 100644 --- a/src/pages/TaskPageList/compoment/TaskOperateTableRender.tsx +++ b/src/pages/TaskPageList/compoment/TaskOperateTableRender.tsx @@ -1,929 +1,866 @@ -import type { FC } from 'react'; -import { useMemo, useRef } from 'react'; -import { Button, message, Modal, Popover, Spin, Tooltip } from 'antd'; -import { InfoCircleOutlined } from '@ant-design/icons'; +import type { FC } from 'react' +import { useMemo, useRef } from 'react' +import { Button, message, Modal, Popover, Spin, Tooltip } from 'antd' +import { InfoCircleOutlined } from '@ant-design/icons' -import { useMemoizedFn, useRequest, useSafeState } from 'ahooks'; -import showErrorMessage from '@/utils/showErrorMessage'; +import { useMemoizedFn, useRequest, useSafeState } from 'ahooks' +import showErrorMessage from '@/utils/showErrorMessage' -import PlayCircleOutlined from '@/assets/task/TablePlayCircleOutlined'; -import TableFormOutlined from '@/assets/task/TableFormOutlined'; -import TableDeleteOutlined from '@/assets/task/TableDeleteOutlined'; -import { - type StopOnRunTaskRequest, - type TaskListRequest, - TTaskListStatus, -} from '@/apis/task/types'; +import PlayCircleOutlined from '@/assets/task/TablePlayCircleOutlined' +import TableFormOutlined from '@/assets/task/TableFormOutlined' +import TableDeleteOutlined from '@/assets/task/TableDeleteOutlined' +import { type StopOnRunTaskRequest, type TaskListRequest, TTaskListStatus } from '@/apis/task/types' -import StopUsingIcon from '@/assets/task/StopUsingIcon'; +import StopUsingIcon from '@/assets/task/StopUsingIcon' import { - deleteTask, - getScriptTaskGroup, - getTaskRun, - getTaskStartEditDispaly, - getTaskStop, - getTaskStream, -} from '@/apis/task'; -import { match, P } from 'ts-pattern'; -import type { UsePageRef } from '@/hooks/usePage'; -import dayjs from 'dayjs'; -import { StartUpScriptModal } from '@/pages/TaskScript/compoment/StartUpScriptModal'; -import type { UseModalRefType } from '@/compoments/WizardModal/useModal'; + deleteTask, + getScriptTaskGroup, + getTaskRun, + getTaskStartEditDispaly, + getTaskStop, + getTaskStream, +} from '@/apis/task' +import { match, P } from 'ts-pattern' +import type { UsePageRef } from '@/hooks/usePage' +import dayjs from 'dayjs' +import { StartUpScriptModal } from '@/pages/TaskScript/compoment/StartUpScriptModal' +import type { UseModalRefType } from '@/compoments/WizardModal/useModal' // import { scriptTypeOption } from '@/pages/TaskScript/data'; interface TCommonTasksColumnsRenderProps { - record: TaskListRequest; - localRefrech: UsePageRef['localRefrech']; - headerGroupValue: 1 | 2 | 3; - setDeleteValues?: React.Dispatch< - React.SetStateAction< - Record< - string, - { - ids: any[]; - isAll: boolean; - } - > - > - >; + record: TaskListRequest + localRefrech: UsePageRef['localRefrech'] + headerGroupValue: 1 | 2 | 3 + setDeleteValues?: React.Dispatch< + React.SetStateAction< + Record< + string, + { + ids: any[] + isAll: boolean + } + > + > + > } // 任务列表 普通任务 / 定时任务操作项 const PublicAndExecutionOperateRender: FC = ({ - record, - localRefrech, - headerGroupValue, - setDeleteValues, + record, + localRefrech, + headerGroupValue, + setDeleteValues, }) => { - const StartUpScriptModalRef = useRef(null); - const itemsRef = useRef(null); - const [open, setOpen] = useSafeState({ - action: false, - delete: false, - }); - const { status } = record; - - const { run: TaskStearmRun } = useRequest(getTaskStream, { - manual: true, - onError: () => showErrorMessage('加入实时更新失败'), - }); - - // 获取 启动脚本任务 任务组参数 - const { run: runAsyncGroup } = useRequest( - async () => { - const result = await getScriptTaskGroup(); - const { - data: { list }, - } = result; - - const resultList = list?.map((it) => ({ - value: it.name, - label: it.name, - })); - return resultList; - }, - { - manual: true, - onSuccess: async (values) => { - await StartUpScriptModalRef.current?.open( - itemsRef.current, - values, - ); - }, - onError(error) { - message.destroy(); - showErrorMessage(error?.message ?? '获取失败'); - }, - }, - ); - - // 执行 - const { loading, runAsync } = useRequest( - async (params: StopOnRunTaskRequest) => { - const result = await getTaskRun(params); - const { data } = result; - return data; - }, - { - manual: true, - onSuccess: async (value) => { - localRefrech({ - operate: 'edit', - newObj: value, - oldObj: record, - }); - TaskStearmRun(value.id); - message.success('执行成功'); - setOpen((val) => ({ ...val, action: false })); - }, - onError(error) { - message.destroy(); - showErrorMessage(error?.message ?? '执行失败'); + const StartUpScriptModalRef = useRef(null) + const itemsRef = useRef(null) + const [open, setOpen] = useSafeState({ + action: false, + delete: false, + }) + const { status } = record + + const { run: TaskStearmRun } = useRequest(getTaskStream, { + manual: true, + onError: () => showErrorMessage('加入实时更新失败'), + }) + + // 获取 启动脚本任务 任务组参数 + const { run: runAsyncGroup } = useRequest( + async () => { + const result = await getScriptTaskGroup() + const { + data: { list }, + } = result + + const resultList = list?.map((it) => ({ + value: it.name, + label: it.name, + })) + return resultList + }, + { + manual: true, + onSuccess: async (values) => { + await StartUpScriptModalRef.current?.open(itemsRef.current, values) + }, + onError(error) { + message.destroy() + showErrorMessage(error?.message ?? '获取失败') + }, + }, + ) + + // 执行 + const { loading, runAsync } = useRequest( + async (params: StopOnRunTaskRequest) => { + const result = await getTaskRun(params) + const { data } = result + return data + }, + { + manual: true, + onSuccess: async (value) => { + localRefrech({ + operate: 'edit', + newObj: value, + oldObj: record, + }) + TaskStearmRun(value.id) + message.success('执行成功') + setOpen((val) => ({ ...val, action: false })) + }, + onError(error) { + message.destroy() + showErrorMessage(error?.message ?? '执行失败') + }, + }, + ) + // 确定执行操作 + const headImplement = async () => { + if (record.id) { + await runAsync({ task_id: record.id, task_type: headerGroupValue }) + } else { + showErrorMessage('未获取到当前任务ID') + } + } + + // 取消执行 + const { loading: stopRunning, runAsync: stopRunAsync } = useRequest( + async (params: StopOnRunTaskRequest) => { + const result = await getTaskStop(params) + const { data } = result + return data + }, + { + manual: true, + onSuccess: async (value) => { + localRefrech({ + operate: 'edit', + newObj: value, + oldObj: record, + }) + TaskStearmRun(value.id) + message.success('取消执行成功') + setOpen((val) => ({ ...val, action: false })) + }, + onError(error) { + message.destroy() + showErrorMessage(error?.message ?? '取消执行失败') + }, + }, + ) + + // 取消执行操作 + const headTaskStop = async () => { + if (record.id) { + await stopRunAsync({ + task_id: record.id, + task_type: headerGroupValue, + }) + } else { + showErrorMessage('未获取到当前任务ID') + } + } + + // 删除任务 + const { runAsync: deleteRunAsync, loading: DeleteLoading } = useRequest( + async (id: number) => { + const result = await deleteTask(id) + return result + }, + { manual: true }, + ) + + const headDeleteTask = async () => { + if (record.id) { + await deleteRunAsync(record.id) + localRefrech({ operate: 'delete', oldObj: { id: record.id } }) + setDeleteValues && + setDeleteValues((values) => { + const ids = values?.['task_name']?.ids?.filter((ids) => ids !== record.id) + return { + task_name: { + ids, + isAll: false, }, - }, - ); - // 确定执行操作 - const headImplement = async () => { - if (record.id) { - await runAsync({ task_id: record.id, task_type: headerGroupValue }); - } else { - showErrorMessage('未获取到当前任务ID'); + } + }) + message.success('删除成功') + setOpen((val) => ({ ...val, delete: false })) + } else { + showErrorMessage('未获取到当前任务ID') + } + } + + // 编辑任务 + const onEdit = async (status: 'edit' | 'execute') => { + if (record?.id) { + await getTaskStartEditDispaly(record.id).then(({ data }) => { + const transformModalFormdata = { + ...data, + id: record.id, + headerGroupValue, + start_timestamp: data?.start_timestamp && status === 'edit' ? data?.start_timestamp : undefined, + params: { + ...data.params, + }, } - }; - - // 取消执行 - const { loading: stopRunning, runAsync: stopRunAsync } = useRequest( - async (params: StopOnRunTaskRequest) => { - const result = await getTaskStop(params); - const { data } = result; - return data; - }, - { - manual: true, - onSuccess: async (value) => { - localRefrech({ - operate: 'edit', - newObj: value, - oldObj: record, - }); - TaskStearmRun(value.id); - message.success('取消执行成功'); - setOpen((val) => ({ ...val, action: false })); - }, - onError(error) { - message.destroy(); - showErrorMessage(error?.message ?? '取消执行失败'); - }, - }, - ); - - // 取消执行操作 - const headTaskStop = async () => { - if (record.id) { - await stopRunAsync({ - task_id: record.id, - task_type: headerGroupValue, - }); - } else { - showErrorMessage('未获取到当前任务ID'); - } - }; - - // 删除任务 - const { runAsync: deleteRunAsync, loading: DeleteLoading } = useRequest( - async (id: number) => { - const result = await deleteTask(id); - return result; - }, - { manual: true }, - ); - - const headDeleteTask = async () => { - if (record.id) { - await deleteRunAsync(record.id); - localRefrech({ operate: 'delete', oldObj: { id: record.id } }); - setDeleteValues && - setDeleteValues((values) => { - const ids = values?.['task_name']?.ids?.filter( - (ids) => ids !== record.id, - ); - return { - task_name: { - ids, - isAll: false, - }, - }; - }); - message.success('删除成功'); - setOpen((val) => ({ ...val, delete: false })); - } else { - showErrorMessage('未获取到当前任务ID'); - } - }; - - // 编辑任务 - const onEdit = async (status: 'edit' | 'execute') => { - if (record?.id) { - await getTaskStartEditDispaly(record.id).then(({ data }) => { - const transformModalFormdata = { - ...data, - id: record.id, - headerGroupValue, - start_timestamp: - data?.start_timestamp && status === 'edit' - ? data?.start_timestamp - : undefined, - params: { - ...data.params, - }, - }; - - itemsRef.current = transformModalFormdata; - runAsyncGroup(); - }); - } else { - showErrorMessage('未获取到当行数据ID'); - } - }; - - const remainingOperate = useMemo(() => { - return ( -
    - - - - onEdit('edit')} - > - - - - { - setOpen((val) => ({ ...val, delete: newOpen })); - }} - content={ -
    - - -
    - } - title={ -
    - - - 确认删除任务? - -
    - } - placement="left" - trigger="click" - > - -
    -
    - ); - }, [status, open]); + itemsRef.current = transformModalFormdata + runAsyncGroup() + }) + } else { + showErrorMessage('未获取到当行数据ID') + } + } - const executeMemoized = useMemoizedFn(() => { - return ( + const remainingOperate = useMemo(() => { + return ( +
    + + + + onEdit('edit')}> + + + + + { + setOpen((val) => ({ ...val, delete: newOpen })) + }} + content={ +
    + + +
    + } + title={
    - - + + 确认删除任务?
    - ); - }); + } + placement="left" + trigger="click" + > + +
    +
    + ) + }, [status, open]) + const executeMemoized = useMemoizedFn(() => { return ( - <> - {match(status) - .with( - P.not( - P.union( - TTaskListStatus.cancel, - TTaskListStatus.disabled, - TTaskListStatus.enabled, - TTaskListStatus.failed, - TTaskListStatus.finished, - TTaskListStatus.running, - TTaskListStatus.success, - TTaskListStatus.waiting, - ), - ), - () => remainingOperate, - ) - .with(P.string, () => ( -
    - {(status === 'success' || - status === 'failed' || - status === 'cancel' || - status === 'waiting') && - headerGroupValue === 1 ? ( - - - - -
    - } - title={ -
    - - - 立即执行该任务? - -
    - } - trigger="click" - onOpenChange={(newOpen) => - setOpen((val) => ({ - ...val, - action: newOpen, - })) - } - > -
    - -
    - - - ) : null} - {status === 'running' ? ( - - - - -
    - } - title={ -
    - - - 取消该任务? - -
    - } - trigger="click" - open={open.action} - onOpenChange={(newOpen) => - setOpen((val) => ({ - ...val, - action: newOpen, - })) - } - > -
    - -
    - - - ) : ( - headerGroupValue === 2 && ( - -
    { - Modal.info({ - title: '提示', - footer: () => executeMemoized(), - content: ( -
    - 需要再次执行该定时任务,请点击确定修改执行时间 -
    - ), - }); - }} - > - -
    -
    - ) - )} - - - onEdit('edit')} - > - - - - - - setOpen((val) => ({ - ...val, - delete: newOpen, - })) - } - content={ -
    - - -
    - } - title={ -
    - - - 确认删除任务? - -
    - } - placement="left" - trigger="click" - > - - - -
    +
    + + +
    + ) + }) + + return ( + <> + {match(status) + .with( + P.not( + P.union( + TTaskListStatus.cancel, + TTaskListStatus.disabled, + TTaskListStatus.enabled, + TTaskListStatus.failed, + TTaskListStatus.finished, + TTaskListStatus.running, + TTaskListStatus.success, + TTaskListStatus.waiting, + ), + ), + () => remainingOperate, + ) + .with(P.string, () => ( +
    + {(status === 'success' || status === 'failed' || status === 'cancel' || status === 'waiting') && + headerGroupValue === 1 ? ( + + + + +
    + } + title={ +
    + + 立即执行该任务? +
    + } + trigger="click" + onOpenChange={(newOpen) => + setOpen((val) => ({ + ...val, + action: newOpen, + })) + } + > +
    + +
    + + + ) : null} + {status === 'running' ? ( + + + +
    - )) - .with(P.nullish, () => remainingOperate) - .exhaustive()} - - - ); -}; + } + title={ +
    + + 取消该任务? +
    + } + trigger="click" + open={open.action} + onOpenChange={(newOpen) => + setOpen((val) => ({ + ...val, + action: newOpen, + })) + } + > +
    + +
    + + + ) : ( + headerGroupValue === 2 && ( + +
    { + Modal.info({ + title: '提示', + footer: () => executeMemoized(), + content:
    需要再次执行该定时任务,请点击确定修改执行时间
    , + }) + }} + > + +
    +
    + ) + )} + + + onEdit('edit')}> + + + + + + setOpen((val) => ({ + ...val, + delete: newOpen, + })) + } + content={ +
    + + +
    + } + title={ +
    + + 确认删除任务? +
    + } + placement="left" + trigger="click" + > + + + +
    + + )) + .with(P.nullish, () => remainingOperate) + .exhaustive()} + + + ) +} // 任务列表 周期任务操作项 const ExecutionOperateRender: FC = ({ - record, - localRefrech, - headerGroupValue, - setDeleteValues, + record, + localRefrech, + headerGroupValue, + setDeleteValues, }) => { - const StartUpScriptModalRef = useRef(null); - const [open, setOpen] = useSafeState({ - action: false, - delete: false, - }); - const itemsRef = useRef(null); - const { status } = record; - - // 获取 启动脚本任务 任务组参数 - const { run: runAsyncGroup, loading } = useRequest( - async () => { - const result = await getScriptTaskGroup(); - const { - data: { list }, - } = result; - - const resultList = list?.map((it) => ({ - value: it.name, - label: it.name, - })); - return resultList; - }, - { - manual: true, - onSuccess: async (values) => { - await StartUpScriptModalRef.current?.open( - itemsRef.current, - values, - ); - }, - }, - ); - - // 删除任务 - const { runAsync: deleteRunAsync, loading: DeleteLoading } = useRequest( - async (id: number) => { - const result = await deleteTask(id); - return result; - }, - { manual: true }, - ); - const headDeleteTask = async () => { - if (record.id) { - await deleteRunAsync(record.id); - localRefrech({ operate: 'delete', oldObj: { id: record.id } }); - setDeleteValues && - setDeleteValues((values) => { - const ids = values?.['task_name']?.ids?.filter( - (ids) => ids !== record.id, - ); - return { - task_name: { - ids, - isAll: false, - }, - }; - }); - message.success('删除成功'); - setOpen((val) => ({ ...val, delete: false })); - } else { - showErrorMessage('未获取到当前任务ID'); - } - }; - - // 取消执行 - const { loading: stopRunning, runAsync: stopRunAsync } = useRequest( - async (params: StopOnRunTaskRequest) => { - const result = await getTaskStop(params); - const { data } = result; - return data; - }, - { - manual: true, - onSuccess: async (value) => { - localRefrech({ - operate: 'edit', - newObj: value, - oldObj: record, - }); - message.success('取消执行成功'); - setOpen((val) => ({ ...val, action: false })); - }, - onError(error) { - message.destroy(); - showErrorMessage(error?.message ?? '取消执行失败'); + const StartUpScriptModalRef = useRef(null) + const [open, setOpen] = useSafeState({ + action: false, + delete: false, + }) + const itemsRef = useRef(null) + const { status } = record + + // 获取 启动脚本任务 任务组参数 + const { run: runAsyncGroup, loading } = useRequest( + async () => { + const result = await getScriptTaskGroup() + const { + data: { list }, + } = result + + const resultList = list?.map((it) => ({ + value: it.name, + label: it.name, + })) + return resultList + }, + { + manual: true, + onSuccess: async (values) => { + await StartUpScriptModalRef.current?.open(itemsRef.current, values) + }, + }, + ) + + // 删除任务 + const { runAsync: deleteRunAsync, loading: DeleteLoading } = useRequest( + async (id: number) => { + const result = await deleteTask(id) + return result + }, + { manual: true }, + ) + const headDeleteTask = async () => { + if (record.id) { + await deleteRunAsync(record.id) + localRefrech({ operate: 'delete', oldObj: { id: record.id } }) + setDeleteValues && + setDeleteValues((values) => { + const ids = values?.['task_name']?.ids?.filter((ids) => ids !== record.id) + return { + task_name: { + ids, + isAll: false, }, - }, - ); - // 取消执行操作 - const headTaskStop = async () => { - if (record.id) { - await stopRunAsync({ - task_id: record.id, - task_type: headerGroupValue, - }); - } else { - showErrorMessage('未获取到当前任务ID'); - } - }; - - // 执行 - const { loading: starLoading, runAsync } = useRequest( - async (params: StopOnRunTaskRequest) => { - const result = await getTaskRun(params); - const { data } = result; - return data; - }, - { - manual: true, - onSuccess: async (value) => { - localRefrech({ - operate: 'edit', - newObj: value, - oldObj: record, - }); - message.success('执行成功'); - setOpen((val) => ({ ...val, action: false })); - }, - onError(error) { - message.destroy(); - showErrorMessage(error?.message ?? '执行失败'); - }, - }, - ); - // 确定执行操作 - const headImplement = async () => { - if (record.id) { - await runAsync({ task_id: record.id, task_type: headerGroupValue }); - } else { - showErrorMessage('未获取到当前任务ID'); - } - }; - - // 编辑任务 - const onEdit = async () => { - if (record?.id) { - await getTaskStartEditDispaly(record.id).then(({ data }) => { - const transformModalFormdata = { - ...data, - headerGroupValue, - sched_type: 3, - timestamp: - data?.end_timestamp && data?.start_timestamp - ? [ - dayjs.unix(data?.start_timestamp), - dayjs.unix(data?.end_timestamp), - ] - : undefined, - execution_date: data?.params?.execution_date - ? `${dayjs.unix(data?.params?.execution_date)}` - : undefined, - params: { - ...data.params, - // 'preset-protes': data?.params?.['preset-protes'] - // ? (data?.params['preset-protes']) - // .split(', ') - // .map((item) => item.trim()) - // : [], - }, - }; - itemsRef.current = transformModalFormdata; - runAsyncGroup(); - }); - } else { - showErrorMessage('未获取到当行数据ID'); + } + }) + message.success('删除成功') + setOpen((val) => ({ ...val, delete: false })) + } else { + showErrorMessage('未获取到当前任务ID') + } + } + + // 取消执行 + const { loading: stopRunning, runAsync: stopRunAsync } = useRequest( + async (params: StopOnRunTaskRequest) => { + const result = await getTaskStop(params) + const { data } = result + return data + }, + { + manual: true, + onSuccess: async (value) => { + localRefrech({ + operate: 'edit', + newObj: value, + oldObj: record, + }) + message.success('取消执行成功') + setOpen((val) => ({ ...val, action: false })) + }, + onError(error) { + message.destroy() + showErrorMessage(error?.message ?? '取消执行失败') + }, + }, + ) + // 取消执行操作 + const headTaskStop = async () => { + if (record.id) { + await stopRunAsync({ + task_id: record.id, + task_type: headerGroupValue, + }) + } else { + showErrorMessage('未获取到当前任务ID') + } + } + + // 执行 + const { loading: starLoading, runAsync } = useRequest( + async (params: StopOnRunTaskRequest) => { + const result = await getTaskRun(params) + const { data } = result + return data + }, + { + manual: true, + onSuccess: async (value) => { + localRefrech({ + operate: 'edit', + newObj: value, + oldObj: record, + }) + message.success('执行成功') + setOpen((val) => ({ ...val, action: false })) + }, + onError(error) { + message.destroy() + showErrorMessage(error?.message ?? '执行失败') + }, + }, + ) + // 确定执行操作 + const headImplement = async () => { + if (record.id) { + await runAsync({ task_id: record.id, task_type: headerGroupValue }) + } else { + showErrorMessage('未获取到当前任务ID') + } + } + + // 编辑任务 + const onEdit = async () => { + if (record?.id) { + await getTaskStartEditDispaly(record.id).then(({ data }) => { + const transformModalFormdata = { + ...data, + headerGroupValue, + sched_type: 3, + timestamp: + data?.end_timestamp && data?.start_timestamp + ? [dayjs.unix(data?.start_timestamp), dayjs.unix(data?.end_timestamp)] + : undefined, + execution_date: data?.params?.execution_date ? `${dayjs.unix(data?.params?.execution_date)}` : undefined, + params: { + ...data.params, + // 'preset-protes': data?.params?.['preset-protes'] + // ? (data?.params['preset-protes']) + // .split(', ') + // .map((item) => item.trim()) + // : [], + }, } - }; - - const executionOperateOpearte = useMemo(() => { - return ( -
    - {/* 编辑操作 */} - - - - - - - - - + itemsRef.current = transformModalFormdata + runAsyncGroup() + }) + } else { + showErrorMessage('未获取到当行数据ID') + } + } + + const executionOperateOpearte = useMemo(() => { + return ( +
    + {/* 编辑操作 */} + + + + + + + + + + setOpen((val) => ({ ...val, delete: newOpen }))} + content={ +
    + + +
    + } + title={ +
    + + 确认删除任务? +
    + } + placement="left" + trigger="click" + > + + + +
    +
    + ) + }, [status, open]) + + return ( + <> + {match(status) + .with( + P.not( + P.union( + TTaskListStatus.cancel, + TTaskListStatus.disabled, + TTaskListStatus.enabled, + TTaskListStatus.failed, + TTaskListStatus.finished, + TTaskListStatus.running, + TTaskListStatus.success, + TTaskListStatus.waiting, + ), + ), + () => executionOperateOpearte, + ) + .with(P.string, (value) => ( +
    + {value === 'disabled' && ( + - setOpen((val) => ({ ...val, delete: newOpen })) - } - content={ -
    - - -
    - } - title={ -
    - - - 确认删除任务? - -
    - } - placement="left" - trigger="click" + open={open.action} + content={ +
    + + +
    + } + title={ +
    + + 立即执行该任务? +
    + } + trigger="click" + onOpenChange={(newOpen) => + setOpen((val) => ({ + ...val, + action: newOpen, + })) + } > - - - +
    + +
    -
    - ); - }, [status, open]); - - return ( - <> - {match(status) - .with( - P.not( - P.union( - TTaskListStatus.cancel, - TTaskListStatus.disabled, - TTaskListStatus.enabled, - TTaskListStatus.failed, - TTaskListStatus.finished, - TTaskListStatus.running, - TTaskListStatus.success, - TTaskListStatus.waiting, - ), - ), - () => executionOperateOpearte, - ) - .with(P.string, (value) => ( -
    - {value === 'disabled' && ( - - - - -
    - } - title={ -
    - - - 立即执行该任务? - -
    - } - trigger="click" - onOpenChange={(newOpen) => - setOpen((val) => ({ - ...val, - action: newOpen, - })) - } - > -
    - -
    - - - )} - {value === 'enabled' && ( - - - - -
    - } - title={ -
    - - - 停用该任务? - -
    - } - trigger="click" - onOpenChange={(newOpen) => - setOpen((val) => ({ - ...val, - action: newOpen, - })) - } - > -
    - -
    - - - )} - {!['disabled', 'enabled'].includes(value) && ( -
    - )} - {executionOperateOpearte} + + )} + {value === 'enabled' && ( + + + +
    - )) - .with(P.nullish, () => ( -
    -
    - {executionOperateOpearte} + } + title={ +
    + + 停用该任务?
    - )) - .exhaustive()} - - - ); -}; - -export { PublicAndExecutionOperateRender, ExecutionOperateRender }; + } + trigger="click" + onOpenChange={(newOpen) => + setOpen((val) => ({ + ...val, + action: newOpen, + })) + } + > +
    + +
    + + + )} + {!['disabled', 'enabled'].includes(value) &&
    } + {executionOperateOpearte} +
    + )) + .with(P.nullish, () => ( +
    +
    + {executionOperateOpearte} +
    + )) + .exhaustive()} + + + ) +} + +export { PublicAndExecutionOperateRender, ExecutionOperateRender } diff --git a/src/pages/TaskPageList/task.module.scss b/src/pages/TaskPageList/task.module.scss index 36ff3ac7..42a98e71 100644 --- a/src/pages/TaskPageList/task.module.scss +++ b/src/pages/TaskPageList/task.module.scss @@ -6,23 +6,22 @@ align-items: center; width: 100%; padding: 10px 8px; - border-top: 1px solid var(--irify-border); + border-top: 1px solid #eaecf3; transition: all 0.3s ease; /* 添加动画 */ min-height: 49px; - color: var(--irify-text); .tools-list-operate { display: none; transition: display 0.3s ease; /* 可选:如果动画库支持 display 的过渡 */ } &:last-child { - border-bottom: 1px solid var(--irify-border); + border-bottom: 1px solid #eaecf3; } &:hover { border-color: transparent; border-radius: 8px; - background-color: var(--irify-bg-elevated); + background-color: #f8f8f8; & + .tools-list-item { border-top-color: transparent; @@ -41,7 +40,7 @@ .tools-list-click { border-color: transparent; border-radius: 8px; - background-color: var(--irify-accent) !important; + background-color: #4a94f8 !important; color: #fff; transition: all 0.3s ease; /* 点击时添加过渡效果 */ diff --git a/src/pages/TaskScript/compoment/CreateTaskItems.tsx b/src/pages/TaskScript/compoment/CreateTaskItems.tsx index cdd38952..511726ed 100644 --- a/src/pages/TaskScript/compoment/CreateTaskItems.tsx +++ b/src/pages/TaskScript/compoment/CreateTaskItems.tsx @@ -1,1120 +1,806 @@ -import { - Input, - Radio, - Select, - Checkbox, - Popover, - Switch, - Space, - DatePicker, - Form, - Button, - InputNumber, -} from 'antd'; -import { match } from 'ts-pattern'; +import { Input, Radio, Select, Checkbox, Popover, Switch, Space, DatePicker, Form, Button, InputNumber } from 'antd' +import { match } from 'ts-pattern' // import { NodeCard } from './NodeCard'; -import { AddPlugins } from './AddPlugins'; -import { createRules } from '@/utils'; +import { AddPlugins } from './AddPlugins' +import { createRules } from '@/utils' import { - disabledDate, - disabledTime, - PresetPorts, - presetProtsGroupOptions, - scriptTypeOption, -} from '../data'; -import { QuestionCircleOutlined } from '@ant-design/icons'; - -import { ChunkUpload } from '@/compoments'; -import type { TScannerDataList } from './StartUpScriptModal'; -import { useMemoizedFn } from 'ahooks'; -import type { Dayjs } from 'dayjs'; -import dayjs from 'dayjs'; -import { useMemo, useRef } from 'react'; -import type { ItemType } from 'antd/es/menu/interface'; -import { AddSelectSearch } from './AddSelectSearch'; -import { NodeCard } from './NodeCard'; + disabledDate, + disabledTime, + PresetPorts, + presetProtesFromCheckboxValue, + presetProtesKeysFromPorts, + presetProtesToCheckboxValue, + presetProtsGroupOptions, + scriptTypeOption, +} from '../data' +import { QuestionCircleOutlined } from '@ant-design/icons' -type PresetKey = keyof typeof PresetPorts; +import { ChunkUpload } from '@/compoments' +import type { TScannerDataList } from './StartUpScriptModal' +import { useMemoizedFn } from 'ahooks' +import type { Dayjs } from 'dayjs' +import dayjs from 'dayjs' +import { useMemo, useRef } from 'react' +import type { ItemType } from 'antd/es/menu/interface' +import { AddSelectSearch } from './AddSelectSearch' +import { NodeCard } from './NodeCard' -export type TScriptGrounpList = Array<{ value: string; label: string }>; +export type TScriptGrounpList = Array<{ value: string; label: string }> type TCreateTaskItemsProps = ( - title: string, - scriptTypeValue?: string, - scriptGroupList?: TScriptGrounpList, - status?: 'edit' | 'add', - scannerDataList?: TScannerDataList, - keywordPlaceholder?: string, -) => ItemType[] | any; + title: string, + scriptTypeValue?: string, + scriptGroupList?: TScriptGrounpList, + status?: 'edit' | 'add', + scannerDataList?: TScannerDataList, + keywordPlaceholder?: string, +) => ItemType[] | any -const { Item } = Form; -const { RangePicker } = DatePicker; -const { Compact } = Space; +const { Item } = Form +const { RangePicker } = DatePicker +const { Compact } = Space // 设置调度 下拉选项 对应渲染dom const CreateTaskItems: TCreateTaskItemsProps = ( - title, - scriptTypeValue, - scriptGroupList = [], - status, - scannerDataList, - keywordPlaceholder, + title, + scriptTypeValue, + scriptGroupList = [], + status, + scannerDataList, + keywordPlaceholder, ) => { - const starTimeRf = useRef(); + const starTimeRf = useRef() - // 校验时间 - const validateStartTime = ( - value: [Dayjs | null, Dayjs | null], - ): string | any => { - if (value && value[0] && value[0].isBefore(dayjs())) { - return '开始时间不能小于当前时间'; - } - }; + // 校验时间 + const validateStartTime = (value: [Dayjs | null, Dayjs | null]): string | any => { + if (value && value[0] && value[0].isBefore(dayjs())) { + return '开始时间不能小于当前时间' + } + } - // 时间段 小时分钟禁用fn - const disabledTimeRangePicker = ( - date: Dayjs | null, - type: 'start' | 'end', - ) => { - const startDate = starTimeRf.current; - if (!date) return {}; + // 时间段 小时分钟禁用fn + const disabledTimeRangePicker = (date: Dayjs | null, type: 'start' | 'end') => { + const startDate = starTimeRf.current + if (!date) return {} - if (type === 'start') { - return disabledTime(date); - } + if (type === 'start') { + return disabledTime(date) + } - if (type === 'end' && startDate) { - const starMinutes = startDate.format('mm'); - return date.isSame(startDate, 'day') - ? { - disabledHours: () => [ - ...Array( - starMinutes === '59' - ? startDate.hour() + 1 - : startDate.hour(), - ).keys(), - ], - disabledMinutes: () => - date.hour() === startDate.hour() - ? [...Array(startDate.minute() + 1).keys()] - : [], - } - : {}; - } + if (type === 'end' && startDate) { + const starMinutes = startDate.format('mm') + return date.isSame(startDate, 'day') + ? { + disabledHours: () => [...Array(starMinutes === '59' ? startDate.hour() + 1 : startDate.hour()).keys()], + disabledMinutes: () => (date.hour() === startDate.hour() ? [...Array(startDate.minute() + 1).keys()] : []), + } + : {} + } - return {}; - }; + return {} + } - // 调度枚举对应展示dom fn - const schedulingTypeFn = useMemoizedFn((schedulingType: 1 | 2 | 3) => { - return match(schedulingType) - .with(1, () => { - return null; - }) - .with(2, () => { - return ( - { - if (!value) { - return Promise.resolve(); - } - if (value.isBefore(dayjs())) { - return Promise.reject( - new Error( - '所选时间不能小于当前时间', - ), - ); - } - return Promise.resolve(); - }, - }, - ]} - label="执行时间" - className="ml-14" - > - - - ); - }) - .with(3, () => { - return ( - <> - - 第一次是否执行 -
    - } - name="first" - > - - - - disabledDate(date)} // 禁用日期部分 - onCalendarChange={(dates) => { - if (dates && dates[0]) { - starTimeRf.current = dates[0]; // 记录选中的开始时间 - } - }} - disabledTime={(date, type) => - disabledTimeRangePicker(date, type) - } - /> - - 执行周期
    - } - > - - - - - - + + + + + ) + }) + .exhaustive() + }) - const scriptGroupListRef = useRef(); + const scriptGroupListRef = useRef() - const collapseChildren = [ - { - key: '1', - label: '基本信息', - forceRender: true, - style: { - borderBottom: '1px solid #EAECF3', - borderRadius: '0px', - marginBottom: '8px', - }, - children: ( -
    - 报告名称
    } - name={['params', 'report_name']} - rules={[ - { - message: '请输入报告名称', - required: true, - }, - ]} - > - - + const collapseChildren = [ + { + key: '1', + label: '基本信息', + forceRender: true, + style: { + borderBottom: '1px solid #EAECF3', + borderRadius: '0px', + marginBottom: '8px', + }, + children: ( +
    + 报告名称
    } + name={['params', 'report_name']} + rules={[ + { + message: '请输入报告名称', + required: true, + }, + ]} + > + + - 任务名称
    } - name="task_id" - className="hidden" - > - - - - - - - + + + + + + { - return ( - - {option.data.value} - (当前任务量 - {option.data.size}) - - ); - }} - options={ - Array.isArray(scannerDataList) - ? scannerDataList?.map( - (it) => ({ - label: it?.name, - value: it?.name, - size: it?.size, - }), - ) - : [] - } - /> - - ) : ( - it)} - rules={[ - { - required: true, - message: '请选择节点', - }, - ]} - label={ -
    - 节点选择 -
    - } - > - {scannerDataList && - scannerDataList.length !== 0 ? ( - - ) : ( -
    - - -
    - )} -
    - )) - ); - }} - - - ), - extra: ( - - {({ setFieldValue }) => { - return ( - - ); + > + + + 上传 + + } + > + { + setFieldValue(['params', scriptTypeValue === 'weakinfo' ? 'keyword' : 'target'], fileName) }} + /> - ), - }, - { - key: '3', - label: '设置调度', - forceRender: true, - style: { - borderBottom: '1px solid #EAECF3', - borderRadius: '0px', - marginBottom: '8px', - }, - extra: ( - - {({ setFieldValue }) => { + ) + }} + + 执行节点} + initialValue="1" + > + + + + {({ getFieldValue, setFieldValue }) => { + const executionNodeValue = getFieldValue(['params', 'execution_node']) + executionNodeValue === '2' && setFieldValue('scanner', undefined) + return ( + executionNodeValue === '1' && + (scannerDataList && scannerDataList?.length > 6 ? ( + 节点选择} + rules={[ + { + required: true, + message: '请选择节点', + }, + ]} + initialValue={[scannerDataList?.[0]?.name].filter((it) => it)} + > + + + prevValues?.['sched_type'] !== curValues?.['sched_type']}> + {({ getFieldValue }) => { + const formType = getFieldValue('sched_type') + return schedulingTypeFn(formType) + }} + + + ), + }, + + { + key: '4', + label: '高级参数', + forceRender: true, + style: { + borderBottom: '1px solid #EAECF3', + borderRadius: '0px', + marginBottom: '8px', + }, + extra: ( + + {({ setFieldValue }) => { + return ( + + ) + }} + + ), + children: ( +
    + {scriptTypeValue !== 'login_brute_scan' && ( + <> + 扫描模式
    } + className="ml-14" + rules={[ + { + message: '请选择扫描模式', + required: true, + }, + ]} + initialValue="syn+tcp" + > + +
    + + {/* TODO 若需字段联动 dependencies 需添加监听项 */} + {({ setFieldValue }) => { + return ( 调度类型} - name="sched_type" - initialValue={1} + name={['params', 'preset-protes']} + label={
    预设端口
    } + initialValue="all" + getValueFromEvent={presetProtesFromCheckboxValue} + getValueProps={(value) => ({ + value: presetProtesToCheckboxValue(value), + })} > - +
    + + +