From 4954d3978cf1967729a5a2d5b90f6baef18974da Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期一, 23 三月 2026 09:35:10 +0800
Subject: [PATCH] #ai redis+页面优化
---
rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiAsyncConfig.java | 13
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiConfigResolverServiceImpl.java | 17
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java | 97 +
rsf-admin/src/api/ai/chat.js | 4
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRuntimeDto.java | 4
rsf-admin/src/page/system/aiParam/AiParamList.jsx | 40
rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiParamController.java | 7
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatModelOptionDto.java | 19
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiPromptServiceImpl.java | 37
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiMcpMountServiceImpl.java | 53
rsf-admin/src/api/ai/configCenter.js | 9
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRequest.java | 2
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java | 151 ++
rsf-admin/src/i18n/zh.js | 17
rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java | 9
rsf-admin/src/i18n/en.js | 15
rsf-server/src/main/resources/application-dev.yml | 1
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiParamServiceImpl.java | 151 ++
rsf-admin/package.json | 8
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiConfigOpsServiceImpl.java | 39
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiRedisSupport.java | 519 +++++++++
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiConfigResolverService.java | 2
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java | 2
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiCallLogServiceImpl.java | 16
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiParamService.java | 9
rsf-admin/package-lock.json | 1647 ++++++++++++++++++++++++++++++
rsf-admin/src/page/system/aiParam/AiParamForm.jsx | 12
rsf-admin/src/layout/AiChatDrawer.jsx | 278 +++++
rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java | 10
29 files changed, 3,087 insertions(+), 101 deletions(-)
diff --git a/rsf-admin/package-lock.json b/rsf-admin/package-lock.json
index 41e5268..1bf9ecf 100644
--- a/rsf-admin/package-lock.json
+++ b/rsf-admin/package-lock.json
@@ -35,11 +35,13 @@
"react-barcode": "^1.6.1",
"react-dom": "^18.3.0",
"react-hook-form": "^7.53.0",
+ "react-markdown": "^10.1.0",
"react-router": "^6.22.0",
"react-router-dom": "^6.26.1",
"react-syntax-highlighter": "^15.5.0",
"react-to-print": "^2.14.11",
"recharts": "^2.15.0",
+ "remark-gfm": "^4.0.1",
"svgpath": "^2.6.0",
"three": "^0.155.0",
"tweedle.js": "^2.1.0"
@@ -104,6 +106,7 @@
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -438,6 +441,7 @@
"resolved": "https://registry.npmmirror.com/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz",
"integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@emotion/memoize": "^0.9.0"
}
@@ -453,6 +457,7 @@
"resolved": "https://registry.npmmirror.com/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -496,6 +501,7 @@
"resolved": "https://registry.npmmirror.com/@emotion/styled/-/styled-11.14.0.tgz",
"integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -1140,6 +1146,7 @@
"resolved": "https://registry.npmmirror.com/@mui/icons-material/-/icons-material-5.17.1.tgz",
"integrity": "sha512-CN86LocjkunFGG0yPlO4bgqHkNGgaEOEc3X/jG5Bzm401qYw79/SaLrofA7yAKCCXAGdIGnLoMHohc3+ubs95A==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.23.9"
},
@@ -1166,6 +1173,7 @@
"resolved": "https://registry.npmmirror.com/@mui/material/-/material-5.17.1.tgz",
"integrity": "sha512-2B33kQf+GmPnrvXXweWAx+crbiUEsxCdCN979QDYnlH9ox4pd+0/IBriWLV+l6ORoBF60w39cWjFnJYGFdzXcw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/core-downloads-tracker": "^5.17.1",
@@ -1401,6 +1409,7 @@
"resolved": "https://registry.npmmirror.com/@mui/system/-/system-6.4.11.tgz",
"integrity": "sha512-gibtsrZEwnDaT5+I/KloOj/yHluX5G8heknuxBpQOdEQ3Gc0avjSImn5hSeKp8D4thiwZiApuggIjZw1dQguUA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/private-theming": "^6.4.9",
@@ -1485,6 +1494,7 @@
"resolved": "https://registry.npmmirror.com/@mui/utils/-/utils-5.17.1.tgz",
"integrity": "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/types": "~7.2.15",
@@ -1803,6 +1813,7 @@
"resolved": "https://registry.npmmirror.com/@pixi/assets/-/assets-7.4.3.tgz",
"integrity": "sha512-StvjiJBSp/j9hHkGu8AFHNvwYUazXq64WhyhytztyDMRkg/l/cL7EcttY5T0qZNWlIpccdr60LUKrWDOuMpkiw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/css-font-loading-module": "^0.0.12"
},
@@ -1846,6 +1857,7 @@
"resolved": "https://registry.npmmirror.com/@pixi/core/-/core-7.4.3.tgz",
"integrity": "sha512-5YDs11faWgVVTL8VZtLU05/Fl47vaP5Tnsbf+y/WRR0VSW3KhRRGTBU1J3Gdc2xEWbJhUK07KGP7eSZpvtPVgA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@pixi/color": "7.4.3",
"@pixi/constants": "7.4.3",
@@ -1866,6 +1878,7 @@
"resolved": "https://registry.npmmirror.com/@pixi/display/-/display-7.4.3.tgz",
"integrity": "sha512-b5m2dAaoNAVdxz1oDaxl3XZ059NEOcNtGkxTOZ4EYCw/jcp9sZXkgSROHRzsGn4k+NugH7+9MP4Id2Z0kkdUhw==",
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@pixi/core": "7.4.3"
}
@@ -1875,6 +1888,7 @@
"resolved": "https://registry.npmmirror.com/@pixi/events/-/events-7.4.3.tgz",
"integrity": "sha512-o3j/5Dxq6WDVS6eHfURB/cf/MP+NcsF/eC5PnbSHjXxJmDE7PoTVwLvxexm5uuvNRpFh/6/Fn0V8Vl4gV8sc8w==",
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@pixi/core": "7.4.3",
"@pixi/display": "7.4.3"
@@ -1954,6 +1968,7 @@
"resolved": "https://registry.npmmirror.com/@pixi/graphics/-/graphics-7.4.3.tgz",
"integrity": "sha512-wWLivD8/URb8A7X4TqCZGG39C91IE+aOuWY/z9NCz5Z6WvA/VWnsc5fLTlO+ggjGHgKF0cSucCXZfUe1wm0AOQ==",
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@pixi/core": "7.4.3",
"@pixi/display": "7.4.3",
@@ -1971,6 +1986,7 @@
"resolved": "https://registry.npmmirror.com/@pixi/mesh/-/mesh-7.4.3.tgz",
"integrity": "sha512-CikqFPtKvU3Zj986/MSoC8X39CWv5CEpiEW/tYp47p4tgQNDSkNWYnDiNYgb+4VX6pNsBrgX4DALLdTR17SlSA==",
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@pixi/core": "7.4.3",
"@pixi/display": "7.4.3"
@@ -2061,6 +2077,7 @@
"resolved": "https://registry.npmmirror.com/@pixi/sprite/-/sprite-7.4.3.tgz",
"integrity": "sha512-iNBrpOFF9nXDT6m2jcyYy6l/sRzklLDDck1eFHprHZwvNquY2nzRfh+RGBCecxhBcijiLJ3fsZN33fP0LDXkvw==",
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@pixi/core": "7.4.3",
"@pixi/display": "7.4.3"
@@ -2102,6 +2119,7 @@
"resolved": "https://registry.npmmirror.com/@pixi/text/-/text-7.4.3.tgz",
"integrity": "sha512-IAF0iu04rPg3oiL0HZsEZI44fpJxq3UZ4xTmx8l1RyhhSXiElLvvSlSH57vt/BKMQZtCs+AqEit7yn8heK2+nQ==",
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@pixi/core": "7.4.3",
"@pixi/sprite": "7.4.3"
@@ -2603,6 +2621,15 @@
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
+ "node_modules/@types/debug": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
+ "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
"node_modules/@types/earcut": {
"version": "2.1.4",
"resolved": "https://registry.npmmirror.com/@types/earcut/-/earcut-2.1.4.tgz",
@@ -2613,8 +2640,16 @@
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
- "dev": true,
"license": "MIT"
+ },
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
},
"node_modules/@types/hast": {
"version": "2.3.10",
@@ -2642,12 +2677,28 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "20.17.46",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-20.17.46.tgz",
"integrity": "sha512-0PQHLhZPWOxGW4auogW0eOQAuNIlCYvibIpG67ja0TOJ6/sehu+1en7sfceUn+QQtx4Rk3GxbLNwPh0Cav7TWw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"undici-types": "~6.19.2"
}
@@ -2669,6 +2720,7 @@
"resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.21.tgz",
"integrity": "sha512-gXLBtmlcRJeT09/sI4PxVwyrku6SaNUj/6cMubjE6T6XdY1fDmBL7r0nX0jbSZPU/Xr0KuwLLZh6aOYY5d91Xw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -2680,6 +2732,7 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@@ -2760,6 +2813,7 @@
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
"dev": true,
"license": "BSD-2-Clause",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/types": "5.62.0",
@@ -2919,7 +2973,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
- "dev": true,
"license": "ISC"
},
"node_modules/@vitejs/plugin-react": {
@@ -2948,6 +3001,7 @@
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3257,6 +3311,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -3308,6 +3372,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001716",
"electron-to-chromium": "^1.5.149",
@@ -3398,6 +3463,16 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
@@ -3419,6 +3494,16 @@
"version": "1.2.4",
"resolved": "https://registry.npmmirror.com/character-entities/-/character-entities-1.2.4.tgz",
"integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
"license": "MIT",
"funding": {
"type": "github",
@@ -3560,7 +3645,8 @@
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/d3-array": {
"version": "3.2.4",
@@ -3742,6 +3828,7 @@
"resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"license": "MIT",
+ "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
@@ -3751,7 +3838,8 @@
"version": "1.11.13",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/debug": {
"version": "4.4.0",
@@ -3775,6 +3863,29 @@
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
+ },
+ "node_modules/decode-named-character-reference": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
+ "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/decode-named-character-reference/node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
},
"node_modules/decode-uri-component": {
"version": "0.2.2",
@@ -3833,6 +3944,28 @@
"license": "MIT",
"engines": {
"node": ">=0.4.0"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/dir-glob": {
@@ -4154,6 +4287,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -4410,6 +4544,16 @@
"node": ">=4.0"
}
},
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz",
@@ -4424,6 +4568,12 @@
"version": "4.0.7",
"resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "license": "MIT"
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/fast-deep-equal": {
@@ -5015,6 +5165,100 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-jsx-runtime/node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/hast-util-to-jsx-runtime/node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/hast-util-to-jsx-runtime/node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/hast-util-to-jsx-runtime/node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/hast-util-to-jsx-runtime/node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace/node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
"node_modules/hastscript": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/hastscript/-/hastscript-6.0.0.tgz",
@@ -5061,6 +5305,16 @@
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
+ },
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
},
"node_modules/ignore": {
"version": "5.3.2",
@@ -5125,6 +5379,12 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
+ },
+ "node_modules/inline-style-parser": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
+ "license": "MIT"
},
"node_modules/internal-slot": {
"version": "1.1.0",
@@ -5440,6 +5700,18 @@
"license": "MIT",
"engines": {
"node": ">=8"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-regex": {
@@ -5786,6 +6058,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -5822,6 +6104,16 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/markdown-table": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -5829,6 +6121,431 @@
"license": "MIT",
"engines": {
"node": ">= 0.4"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "escape-string-regexp": "^5.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
+ "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-from-markdown/node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/mdast-util-gfm": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-gfm-autolink-literal": "^2.0.0",
+ "mdast-util-gfm-footnote": "^2.0.0",
+ "mdast-util-gfm-strikethrough": "^2.0.0",
+ "mdast-util-gfm-table": "^2.0.0",
+ "mdast-util-gfm-task-list-item": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-autolink-literal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-find-and-replace": "^3.0.0",
+ "micromark-util-character": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-strikethrough": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-table": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "markdown-table": "^3.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-task-list-item": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression/node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx/node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx/node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/mdast-util-mdx-jsx/node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx/node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx/node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx/node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx/node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx/node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx/node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx/node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm/node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast/node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown/node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
"node_modules/memoize-one": {
@@ -5846,6 +6563,569 @@
"engines": {
"node": ">= 8"
}
+ },
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-extension-gfm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
+ "micromark-extension-gfm-footnote": "^2.0.0",
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
+ "micromark-extension-gfm-table": "^2.0.0",
+ "micromark-extension-gfm-tagfilter": "^2.0.0",
+ "micromark-extension-gfm-task-list-item": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-autolink-literal": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-strikethrough": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-table": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
+ "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-tagfilter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-task-list-item": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
@@ -6622,6 +7902,7 @@
"resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -6670,6 +7951,7 @@
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -6712,6 +7994,7 @@
"resolved": "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.56.3.tgz",
"integrity": "sha512-IK18V6GVbab4TAo1/cz3kqajxbDPGofdF0w7VHdCo0Nt8PrPlOZcuuDq9YYIV1BtjcX78x0XsldbQRQnQXWXmw==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -6727,7 +8010,44 @@
"version": "19.1.0",
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-19.1.0.tgz",
"integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/react-markdown": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
+ "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
+ },
+ "node_modules/react-markdown/node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
},
"node_modules/react-redux": {
"version": "8.1.3",
@@ -6789,6 +8109,7 @@
"resolved": "https://registry.npmmirror.com/react-router/-/react-router-6.30.0.tgz",
"integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@remix-run/router": "1.23.0"
},
@@ -6804,6 +8125,7 @@
"resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.30.0.tgz",
"integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@remix-run/router": "1.23.0",
"react-router": "6.30.0"
@@ -6920,6 +8242,7 @@
"resolved": "https://registry.npmmirror.com/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.9.2"
}
@@ -6990,6 +8313,81 @@
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/remark-gfm": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "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.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype/node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/remark-stringify": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
"node_modules/remove-accents": {
@@ -7498,6 +8896,30 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/stringify-entities/node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -7522,6 +8944,24 @@
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/style-to-js": {
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.14"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.7"
}
},
"node_modules/stylis": {
@@ -7594,6 +9034,26 @@
},
"engines": {
"node": ">=8.0"
+ }
+ },
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/tslib": {
@@ -7741,6 +9201,7 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -7773,6 +9234,129 @@
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unified/node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is/node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position/node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position/node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents/node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/unist-util-visit/node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
"node_modules/update-browserslist-db": {
@@ -7853,6 +9437,46 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message/node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/vfile/node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
"node_modules/victory-vendor": {
"version": "36.9.2",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
@@ -7881,6 +9505,7 @@
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -8103,6 +9728,16 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
}
}
-}
\ No newline at end of file
+}
diff --git a/rsf-admin/package.json b/rsf-admin/package.json
index 4cc1046..7c55d5b 100644
--- a/rsf-admin/package.json
+++ b/rsf-admin/package.json
@@ -39,14 +39,16 @@
"react-barcode": "^1.6.1",
"react-dom": "^18.3.0",
"react-hook-form": "^7.53.0",
+ "react-markdown": "^10.1.0",
"react-router": "^6.22.0",
"react-router-dom": "^6.26.1",
"react-syntax-highlighter": "^15.5.0",
"react-to-print": "^2.14.11",
+ "recharts": "^2.15.0",
+ "remark-gfm": "^4.0.1",
"svgpath": "^2.6.0",
"three": "^0.155.0",
- "tweedle.js": "^2.1.0",
- "recharts": "^2.15.0"
+ "tweedle.js": "^2.1.0"
},
"devDependencies": {
"@types/node": "^20.10.7",
@@ -64,4 +66,4 @@
"vite": "^5.3.5"
},
"name": "rsf"
-}
\ No newline at end of file
+}
diff --git a/rsf-admin/src/api/ai/chat.js b/rsf-admin/src/api/ai/chat.js
index 1b2117b..3d42719 100644
--- a/rsf-admin/src/api/ai/chat.js
+++ b/rsf-admin/src/api/ai/chat.js
@@ -2,9 +2,9 @@
import { PREFIX_BASE_URL, TOKEN_HEADER_NAME } from "@/config/setting";
import { getToken } from "@/utils/token-util";
-export const getAiRuntime = async (promptCode = "home.default", sessionId = null) => {
+export const getAiRuntime = async (promptCode = "home.default", sessionId = null, aiParamId = null) => {
const res = await request.get("ai/chat/runtime", {
- params: { promptCode, sessionId },
+ params: { promptCode, sessionId, aiParamId },
});
const { code, msg, data } = res.data;
if (code === 200) {
diff --git a/rsf-admin/src/api/ai/configCenter.js b/rsf-admin/src/api/ai/configCenter.js
index a916647..e947e7a 100644
--- a/rsf-admin/src/api/ai/configCenter.js
+++ b/rsf-admin/src/api/ai/configCenter.js
@@ -18,6 +18,15 @@
throw new Error(msg || "AI 鍙傛暟楠岃瘉澶辫触");
};
+export const setAiParamDefault = async (id) => {
+ const res = await request.post(`aiParam/set-default/${id}`);
+ const { code, msg, data } = res.data;
+ if (code === 200) {
+ return data;
+ }
+ throw new Error(msg || "璁剧疆榛樿 AI 鍙傛暟澶辫触");
+};
+
export const renderAiPromptPreview = async (payload) => {
const res = await request.post("aiPrompt/render-preview", payload);
const { code, msg, data } = res.data;
diff --git a/rsf-admin/src/i18n/en.js b/rsf-admin/src/i18n/en.js
index 78cc5cb..e83334f 100644
--- a/rsf-admin/src/i18n/en.js
+++ b/rsf-admin/src/i18n/en.js
@@ -277,6 +277,7 @@
model: "Model",
baseUrl: "Base URL",
apiKey: "API Key",
+ defaultStatus: "Default Status",
temperature: "Temperature",
topP: "Top P",
maxTokens: "Max Tokens",
@@ -292,6 +293,16 @@
emptyDescription: "Create an OpenAI-compatible model card first.",
streaming: "Streaming",
nonStreaming: "Non-streaming",
+ },
+ status: {
+ default: "Default",
+ nonDefault: "Non-default",
+ },
+ actions: {
+ setDefault: "Set Default",
+ currentDefault: "Current Default",
+ setDefaultSuccess: "Default AI parameter updated",
+ setDefaultFailed: "Failed to set the default AI parameter",
},
dialog: {
create: "New AI Parameter",
@@ -548,6 +559,10 @@
sessionMetric: "Session: %{id}",
promptMetric: "Prompt: %{value}",
modelMetric: "Model: %{value}",
+ modelSelectorLabel: "Chat Model",
+ modelSelectorHint: "Switching only affects subsequent replies in this session and does not change the global default model.",
+ modelSwitchFailed: "Failed to switch the chat model",
+ defaultModelSuffix: "(Default)",
mcpMetric: "MCP: %{value}",
historyMetric: "History: %{value}",
recentMetric: "Recent: %{value}",
diff --git a/rsf-admin/src/i18n/zh.js b/rsf-admin/src/i18n/zh.js
index 92be57e..08fe8bf 100644
--- a/rsf-admin/src/i18n/zh.js
+++ b/rsf-admin/src/i18n/zh.js
@@ -293,6 +293,7 @@
model: "妯″瀷",
baseUrl: "鏈嶅姟鍦板潃",
apiKey: "API 瀵嗛挜",
+ defaultStatus: "榛樿鐘舵��",
temperature: "娓╁害",
topP: "Top P 閲囨牱",
maxTokens: "鏈�澶� Tokens",
@@ -308,6 +309,16 @@
emptyDescription: "鍙互鍏堟柊寤轰竴涓� OpenAI 鍏煎妯″瀷鍙傛暟鍗$墖銆�",
streaming: "娴佸紡鍝嶅簲",
nonStreaming: "闈炴祦寮�",
+ },
+ status: {
+ default: "榛樿",
+ nonDefault: "闈為粯璁�",
+ },
+ actions: {
+ setDefault: "璁句负榛樿",
+ currentDefault: "褰撳墠榛樿",
+ setDefaultSuccess: "宸茶缃负榛樿 AI 鍙傛暟",
+ setDefaultFailed: "璁剧疆榛樿 AI 鍙傛暟澶辫触",
},
dialog: {
create: "鏂板缓 AI 鍙傛暟",
@@ -498,7 +509,7 @@
},
},
drawer: {
- title: "AI 瀵硅瘽",
+ title: "WMS 鍔╂墜",
runtimeFailed: "鑾峰彇 AI 杩愯鏃跺け璐�",
sessionListFailed: "鑾峰彇 AI 浼氳瘽鍒楄〃澶辫触",
sessionDeleted: "浼氳瘽宸插垹闄�",
@@ -564,6 +575,10 @@
sessionMetric: "Session: %{id}",
promptMetric: "Prompt: %{value}",
modelMetric: "Model: %{value}",
+ modelSelectorLabel: "瀵硅瘽妯″瀷",
+ modelSelectorHint: "鍒囨崲鍚庝粎褰卞搷褰撳墠浼氳瘽鍚庣画鍥炲锛屼笉浼氭敼鍔ㄥ叏灞�榛樿妯″瀷銆�",
+ modelSwitchFailed: "鍒囨崲瀵硅瘽妯″瀷澶辫触",
+ defaultModelSuffix: "(榛樿)",
mcpMetric: "MCP: %{value}",
historyMetric: "History: %{value}",
recentMetric: "Recent: %{value}",
diff --git a/rsf-admin/src/layout/AiChatDrawer.jsx b/rsf-admin/src/layout/AiChatDrawer.jsx
index 4eaeea9..92f29df 100644
--- a/rsf-admin/src/layout/AiChatDrawer.jsx
+++ b/rsf-admin/src/layout/AiChatDrawer.jsx
@@ -1,6 +1,8 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useNotify, useTranslate } from "react-admin";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
import {
Alert,
Box,
@@ -17,11 +19,14 @@
List,
ListItemButton,
ListItemText,
+ MenuItem,
Paper,
Stack,
TextField,
Typography,
} from "@mui/material";
+import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
+import { atomOneLight } from "react-syntax-highlighter/dist/esm/styles/hljs";
import SmartToyOutlinedIcon from "@mui/icons-material/SmartToyOutlined";
import SendRoundedIcon from "@mui/icons-material/SendRounded";
import StopCircleOutlinedIcon from "@mui/icons-material/StopCircleOutlined";
@@ -48,6 +53,176 @@
ANSWER: 2,
};
+const normalizeMarkdownContent = (content) => {
+ if (!content) {
+ return "";
+ }
+ return content
+ .replace(/\r\n/g, "\n")
+ .replace(/(\n[-*] .+)\n{2,}(?=[-*] )/g, "$1\n")
+ .replace(/(\n\d+\. .+)\n{2,}(?=\d+\. )/g, "$1\n")
+ .replace(/([^\n])\n{3,}/g, "$1\n\n");
+};
+
+const markdownSx = {
+ width: "100%",
+ fontSize: "0.84rem",
+ "& > *:first-of-type": {
+ mt: 0,
+ },
+ "& > *:last-child": {
+ mb: 0,
+ },
+ "& p": {
+ m: 0,
+ lineHeight: 1.28,
+ },
+ "& p + p": {
+ mt: 0.1,
+ },
+ "& h1, & h2, & h3, & h4, & h5, & h6": {
+ mt: 0.25,
+ mb: 0.04,
+ lineHeight: 1.16,
+ fontWeight: 700,
+ },
+ "& h1": {
+ fontSize: "0.96rem",
+ },
+ "& h2": {
+ fontSize: "0.92rem",
+ },
+ "& h3": {
+ fontSize: "0.89rem",
+ },
+ "& ul, & ol": {
+ my: 0.02,
+ pl: 1.3,
+ },
+ "& ul > li, & ol > li": {
+ lineHeight: 1.2,
+ },
+ "& li + li": {
+ mt: 0,
+ },
+ "& li > p": {
+ display: "inline",
+ m: 0,
+ lineHeight: "inherit",
+ },
+ "& li::marker": {
+ fontSize: "0.78rem",
+ },
+ "& blockquote": {
+ m: 0,
+ mt: 0.18,
+ px: 0.7,
+ py: 0.25,
+ borderLeft: "3px solid rgba(25, 118, 210, 0.35)",
+ bgcolor: "rgba(25, 118, 210, 0.06)",
+ },
+ "& hr": {
+ my: 0.25,
+ border: 0,
+ borderTop: "1px solid rgba(0, 0, 0, 0.12)",
+ },
+ "& table": {
+ width: "100%",
+ borderCollapse: "collapse",
+ mt: 0.18,
+ mb: 0.04,
+ fontSize: "0.78rem",
+ },
+ "& th, & td": {
+ border: "1px solid rgba(0, 0, 0, 0.12)",
+ px: 0.4,
+ py: 0.22,
+ textAlign: "left",
+ verticalAlign: "top",
+ },
+ "& th": {
+ bgcolor: "rgba(0, 0, 0, 0.04)",
+ fontWeight: 700,
+ },
+ "& a": {
+ color: "primary.main",
+ textDecoration: "underline",
+ wordBreak: "break-all",
+ },
+ "& img": {
+ maxWidth: "100%",
+ borderRadius: 1.5,
+ },
+ "& code": {
+ fontFamily: "'Consolas', 'Monaco', monospace",
+ },
+};
+
+const AiMarkdownContent = ({ content }) => (
+ <Box sx={markdownSx}>
+ <ReactMarkdown
+ remarkPlugins={[remarkGfm]}
+ components={{
+ p: ({ children }) => <Typography variant="body2">{children}</Typography>,
+ li: ({ children }) => <Box component="li" sx={{ fontSize: "0.875rem" }}>{children}</Box>,
+ blockquote: ({ children }) => <Box component="blockquote">{children}</Box>,
+ a: ({ href, children }) => (
+ <Box
+ component="a"
+ href={href}
+ target="_blank"
+ rel="noreferrer"
+ >
+ {children}
+ </Box>
+ ),
+ code({ inline, className, children, ...props }) {
+ const match = /language-(\w+)/.exec(className || "");
+ const code = String(children).replace(/\n$/, "");
+ if (!inline) {
+ return (
+ <Box sx={{ mt: 0.7, mb: 0.2, borderRadius: 1.5, overflow: "hidden" }}>
+ <SyntaxHighlighter
+ language={match?.[1]}
+ style={atomOneLight}
+ customStyle={{
+ margin: 0,
+ padding: "6px 8px",
+ borderRadius: 12,
+ fontSize: "0.74rem",
+ }}
+ wrapLongLines
+ PreTag="div"
+ {...props}
+ >
+ {code}
+ </SyntaxHighlighter>
+ </Box>
+ );
+ }
+ return (
+ <Box
+ component="code"
+ sx={{
+ px: 0.45,
+ py: "1px",
+ borderRadius: 0.75,
+ bgcolor: "rgba(0, 0, 0, 0.08)",
+ fontSize: "0.74em",
+ }}
+ {...props}
+ >
+ {children}
+ </Box>
+ );
+ },
+ }}
+ >
+ {normalizeMarkdownContent(content)}
+ </ReactMarkdown>
+ </Box>
+);
+
const AiChatDrawer = ({ open, onClose }) => {
const navigate = useNavigate();
const location = useLocation();
@@ -57,6 +232,7 @@
const messagesContainerRef = useRef(null);
const messagesBottomRef = useRef(null);
const [runtime, setRuntime] = useState(null);
+ const [selectedAiParamId, setSelectedAiParamId] = useState(null);
const [sessionId, setSessionId] = useState(null);
const [sessions, setSessions] = useState([]);
const [persistedMessages, setPersistedMessages] = useState([]);
@@ -80,6 +256,20 @@
]), [translate]);
const promptCode = runtime?.promptCode || DEFAULT_PROMPT_CODE;
+ const selectableModelOptions = useMemo(() => {
+ if (runtime?.modelOptions?.length) {
+ return runtime.modelOptions;
+ }
+ if (runtime?.model) {
+ return [{
+ aiParamId: runtime?.aiParamId ?? "CURRENT_MODEL",
+ name: runtime.model,
+ model: runtime.model,
+ active: true,
+ }];
+ }
+ return [];
+ }, [runtime]);
const runtimeSummary = useMemo(() => {
return {
@@ -138,19 +328,22 @@
]);
};
- const loadRuntime = async (targetSessionId = null) => {
+ const loadRuntime = async (targetSessionId = null, targetAiParamId = selectedAiParamId) => {
setLoadingRuntime(true);
setDrawerError("");
try {
- const data = await getAiRuntime(DEFAULT_PROMPT_CODE, targetSessionId);
+ const data = await getAiRuntime(DEFAULT_PROMPT_CODE, targetSessionId, targetAiParamId);
const historyMessages = data?.persistedMessages || [];
setRuntime(data);
+ setSelectedAiParamId(data?.aiParamId ?? null);
setSessionId(data?.sessionId || null);
setPersistedMessages(historyMessages);
setMessages(historyMessages);
+ return data;
} catch (error) {
const message = error.message || translate("ai.drawer.runtimeFailed");
setDrawerError(message);
+ return null;
} finally {
setLoadingRuntime(false);
}
@@ -201,6 +394,24 @@
setThinkingEvents([]);
setThinkingExpanded(true);
await loadRuntime(targetSessionId);
+ };
+
+ const handleModelChange = async (event) => {
+ if (streaming) {
+ return;
+ }
+ const rawValue = event.target.value;
+ const nextAiParamId = rawValue === "" ? null : Number(rawValue);
+ if (nextAiParamId === selectedAiParamId) {
+ return;
+ }
+ const previousAiParamId = selectedAiParamId;
+ setSelectedAiParamId(nextAiParamId);
+ const data = await loadRuntime(sessionId, nextAiParamId);
+ if (!data) {
+ setSelectedAiParamId(previousAiParamId);
+ notify(translate("ai.drawer.modelSwitchFailed"), { type: "error" });
+ }
};
const handleDeleteSession = async (targetSessionId) => {
@@ -433,11 +644,13 @@
let completed = false;
let completedSessionId = sessionId;
+ let completedAiParamId = selectedAiParamId;
try {
await streamAiChat(
{
sessionId,
+ aiParamId: selectedAiParamId,
promptCode,
messages: memoryMessages,
metadata: {
@@ -449,10 +662,12 @@
onEvent: (eventName, payload) => {
if (eventName === "start") {
setRuntime(payload);
+ setSelectedAiParamId(payload?.aiParamId ?? null);
if (payload?.sessionId) {
setSessionId(payload.sessionId);
completedSessionId = payload.sessionId;
}
+ completedAiParamId = payload?.aiParamId ?? completedAiParamId;
}
if (eventName === "delta") {
appendAssistantDelta(payload?.content || "");
@@ -490,7 +705,7 @@
setStreaming(false);
if (completed) {
await Promise.all([
- loadRuntime(completedSessionId),
+ loadRuntime(completedSessionId, completedAiParamId),
loadSessions(sessionKeyword),
]);
}
@@ -887,9 +1102,17 @@
<Typography variant="caption" display="block" sx={{ opacity: 0.72, mb: 0.5 }}>
{message.role === "user" ? translate("ai.drawer.userRole") : translate("ai.drawer.assistantRole")}
</Typography>
- <Typography variant="body2">
- {message.content || (streaming && index === messages.length - 1 ? translate("ai.drawer.thinking") : "")}
- </Typography>
+ {message.role === "assistant" ? (
+ <AiMarkdownContent
+ content={message.content || (streaming && index === messages.length - 1
+ ? translate("ai.drawer.thinking")
+ : "")}
+ />
+ ) : (
+ <Typography variant="body2" sx={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+ {message.content || ""}
+ </Typography>
+ )}
</Paper>
</Stack>
</Box>
@@ -925,7 +1148,47 @@
maxRows={6}
placeholder={translate("ai.drawer.inputPlaceholder")}
/>
- <Stack direction="row" spacing={1} justifyContent="flex-end" mt={1.25}>
+ <Stack
+ direction={{ xs: "column", sm: "row" }}
+ spacing={1}
+ justifyContent="space-between"
+ alignItems={{ xs: "stretch", sm: "center" }}
+ mt={1.25}
+ >
+ {!!selectableModelOptions.length && (
+ <TextField
+ select
+ size="small"
+ label={translate("ai.drawer.modelSelectorLabel")}
+ value={selectedAiParamId ?? runtime?.aiParamId ?? selectableModelOptions[0]?.aiParamId ?? ""}
+ onChange={handleModelChange}
+ disabled={streaming || loadingRuntime || selectableModelOptions.length <= 1}
+ SelectProps={{
+ MenuProps: {
+ disableScrollLock: true,
+ sx: {
+ zIndex: 1605,
+ },
+ PaperProps: {
+ sx: {
+ zIndex: 1606,
+ },
+ },
+ },
+ }}
+ sx={{
+ minWidth: { xs: "100%", sm: 260 },
+ maxWidth: { xs: "100%", sm: 320 },
+ }}
+ >
+ {selectableModelOptions.map((item) => (
+ <MenuItem key={String(item.aiParamId)} value={item.aiParamId}>
+ {`${item.name || item.model || "--"}${item.model && item.name !== item.model ? ` / ${item.model}` : ""}${item.active ? ` ${translate("ai.drawer.defaultModelSuffix")}` : ""}`}
+ </MenuItem>
+ ))}
+ </TextField>
+ )}
+ <Stack direction="row" spacing={1} justifyContent="flex-end">
<Button onClick={() => setInput("")}>{translate("ai.drawer.clearInput")}</Button>
{streaming ? (
<Button variant="outlined" color="warning" startIcon={<StopCircleOutlinedIcon />} onClick={() => stopStream(true)}>
@@ -936,6 +1199,7 @@
{translate("ai.drawer.send")}
</Button>
)}
+ </Stack>
</Stack>
</Box>
</Box>
diff --git a/rsf-admin/src/page/system/aiParam/AiParamForm.jsx b/rsf-admin/src/page/system/aiParam/AiParamForm.jsx
index eabd8dd..0de7085 100644
--- a/rsf-admin/src/page/system/aiParam/AiParamForm.jsx
+++ b/rsf-admin/src/page/system/aiParam/AiParamForm.jsx
@@ -9,7 +9,6 @@
useTranslate,
} from "react-admin";
import { Alert, Button, Grid, Stack, Typography } from "@mui/material";
-import StatusSelectInput from "@/page/components/StatusSelectInput";
import { validateAiParamDraft } from "@/api/ai/configCenter";
const providerChoices = [
@@ -106,7 +105,16 @@
<BooleanInput source="streamingEnabled" label="ai.param.fields.streamingEnabled" disabled={readOnly} />
</Grid>
<Grid item xs={12} md={6}>
- <StatusSelectInput disabled={readOnly} />
+ <SelectInput
+ source="status"
+ label="ai.param.fields.defaultStatus"
+ choices={[
+ { id: 1, name: "ai.param.status.default" },
+ { id: 0, name: "ai.param.status.nonDefault" },
+ ]}
+ fullWidth
+ disabled={readOnly}
+ />
</Grid>
<Grid item xs={12}>
<TextInput source="memo" label="common.field.memo" fullWidth multiline minRows={3} disabled={readOnly} />
diff --git a/rsf-admin/src/page/system/aiParam/AiParamList.jsx b/rsf-admin/src/page/system/aiParam/AiParamList.jsx
index 2d854cf..c77de87 100644
--- a/rsf-admin/src/page/system/aiParam/AiParamList.jsx
+++ b/rsf-admin/src/page/system/aiParam/AiParamList.jsx
@@ -29,10 +29,12 @@
import DeleteOutlineOutlinedIcon from "@mui/icons-material/DeleteOutlineOutlined";
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
+import CheckCircleOutlineRoundedIcon from "@mui/icons-material/CheckCircleOutlineRounded";
import MyExportButton from "@/page/components/MyExportButton";
import AiParamForm from "./AiParamForm";
import AiConfigDialog from "../aiShared/AiConfigDialog";
import AiRuntimeSummary from "../aiShared/AiRuntimeSummary";
+import { setAiParamDefault } from "@/api/ai/configCenter";
const filters = [
<SearchInput source="condition" alwaysOn />,
@@ -40,10 +42,10 @@
<TextInput source="model" label="ai.param.fields.model" />,
<SelectInput
source="status"
- label="common.field.status"
+ label="ai.param.fields.defaultStatus"
choices={[
- { id: "1", name: "common.enums.statusTrue" },
- { id: "0", name: "common.enums.statusFalse" },
+ { id: "1", name: "ai.param.status.default" },
+ { id: "0", name: "ai.param.status.nonDefault" },
]}
/>,
];
@@ -64,7 +66,7 @@
return value.length > max ? `${value.slice(0, max)}...` : value;
};
-const AiParamCards = ({ onView, onEdit, onDelete, deleting }) => {
+const AiParamCards = ({ onView, onEdit, onDelete, onSetDefault, updatingDefaultId, deleting }) => {
const translate = useTranslate();
const { data, isLoading } = useListContext();
const records = useMemo(() => (Array.isArray(data) ? data : []), [data]);
@@ -116,7 +118,7 @@
<Chip
size="small"
color={record.statusBool ? "success" : "default"}
- label={translate(record.statusBool ? "ai.common.enabled" : "ai.common.disabled")}
+ label={translate(record.statusBool ? "ai.param.status.default" : "ai.param.status.nonDefault")}
/>
</Stack>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap mt={1.5}>
@@ -164,6 +166,15 @@
<Button size="small" startIcon={<EditOutlinedIcon />} onClick={() => onEdit(record.id)}>
{translate("common.button.edit")}
</Button>
+ <Button
+ size="small"
+ color={record.statusBool ? "success" : "primary"}
+ startIcon={<CheckCircleOutlineRoundedIcon />}
+ onClick={() => onSetDefault(record)}
+ disabled={deleting || updatingDefaultId === record.id || record.statusBool}
+ >
+ {translate(record.statusBool ? "ai.param.actions.currentDefault" : "ai.param.actions.setDefault")}
+ </Button>
</Stack>
<Button
size="small"
@@ -189,6 +200,7 @@
const refresh = useRefresh();
const [deleteOne, { isPending: deleting }] = useDelete();
const [dialogState, setDialogState] = useState({ open: false, mode: "create", recordId: null });
+ const [updatingDefaultId, setUpdatingDefaultId] = useState(null);
const openDialog = (mode, recordId = null) => setDialogState({ open: true, mode, recordId });
const closeDialog = () => setDialogState({ open: false, mode: "create", recordId: null });
@@ -210,6 +222,22 @@
},
}
);
+ };
+
+ const handleSetDefault = async (record) => {
+ if (!record?.id || record.statusBool) {
+ return;
+ }
+ setUpdatingDefaultId(record.id);
+ try {
+ await setAiParamDefault(record.id);
+ notify(translate("ai.param.actions.setDefaultSuccess"));
+ refresh();
+ } catch (error) {
+ notify(error?.message || translate("ai.param.actions.setDefaultFailed"), { type: "error" });
+ } finally {
+ setUpdatingDefaultId(null);
+ }
};
const dialogTitle = {
@@ -239,6 +267,8 @@
onView={(id) => openDialog("show", id)}
onEdit={(id) => openDialog("edit", id)}
onDelete={handleDelete}
+ onSetDefault={handleSetDefault}
+ updatingDefaultId={updatingDefaultId}
deleting={deleting}
/>
</List>
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiAsyncConfig.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiAsyncConfig.java
index 4382dfa..467c45a 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiAsyncConfig.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiAsyncConfig.java
@@ -21,4 +21,17 @@
executor.initialize();
return executor;
}
+
+ @Bean(name = "aiMemoryTaskExecutor")
+ public Executor aiMemoryTaskExecutor() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(2);
+ executor.setMaxPoolSize(4);
+ executor.setQueueCapacity(200);
+ executor.setThreadNamePrefix("ai-memory-");
+ executor.setWaitForTasksToCompleteOnShutdown(true);
+ executor.setAwaitTerminationSeconds(30);
+ executor.initialize();
+ return executor;
+ }
}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java
index 444d930..ffd39c4 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java
@@ -25,4 +25,14 @@
public static final int MEMORY_SUMMARY_TRIGGER_MESSAGES = 12;
public static final int MEMORY_SUMMARY_MAX_LENGTH = 1200;
public static final int MEMORY_FACTS_MAX_LENGTH = 600;
+ public static final int CONFIG_CACHE_TTL_SECONDS = 300;
+ public static final int RUNTIME_CACHE_TTL_SECONDS = 120;
+ public static final int MEMORY_CACHE_TTL_SECONDS = 120;
+ public static final int SESSION_LIST_CACHE_TTL_SECONDS = 120;
+ public static final int MCP_PREVIEW_CACHE_TTL_SECONDS = 300;
+ public static final int MCP_HEALTH_CACHE_TTL_SECONDS = 120;
+ public static final int STREAM_STATE_TTL_SECONDS = 3600;
+ public static final int TOOL_RESULT_CACHE_TTL_SECONDS = 180;
+ public static final int CHAT_RATE_LIMIT_WINDOW_SECONDS = 60;
+ public static final int CHAT_RATE_LIMIT_MAX_REQUESTS = 30;
}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java
index b096123..83a659a 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java
@@ -31,8 +31,9 @@
@PreAuthorize("isAuthenticated()")
@GetMapping("/ai/chat/runtime")
public R runtime(@RequestParam(required = false) String promptCode,
- @RequestParam(required = false) Long sessionId) {
- return R.ok().add(aiChatService.getRuntime(promptCode, sessionId, getLoginUserId(), getTenantId()));
+ @RequestParam(required = false) Long sessionId,
+ @RequestParam(required = false) Long aiParamId) {
+ return R.ok().add(aiChatService.getRuntime(promptCode, sessionId, aiParamId, getLoginUserId(), getTenantId()));
}
/**
@@ -108,8 +109,8 @@
? request.getRequestId().trim()
: UUID.randomUUID().toString().replace("-", "");
request.setRequestId(requestId);
- log.info("AI chat request accepted, requestId={}, userId={}, tenantId={}, sessionId={}",
- requestId, getLoginUserId(), getTenantId(), request.getSessionId());
+ log.info("AI chat request accepted, requestId={}, userId={}, tenantId={}, sessionId={}, aiParamId={}",
+ requestId, getLoginUserId(), getTenantId(), request.getSessionId(), request.getAiParamId());
return aiChatService.stream(request, getLoginUserId(), getTenantId());
}
}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiParamController.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiParamController.java
index d58ac65..c0ae4a2 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiParamController.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiParamController.java
@@ -100,6 +100,13 @@
return R.ok("Update Success").add(aiParam);
}
+ @PreAuthorize("hasAuthority('system:aiParam:update')")
+ @OperationLog("Set Default AiParam")
+ @PostMapping("/aiParam/set-default/{id}")
+ public R setDefault(@PathVariable Long id) {
+ return R.ok("Update Success").add(aiParamService.setDefaultParam(id, getTenantId(), getLoginUserId()));
+ }
+
@PreAuthorize("hasAuthority('system:aiParam:remove')")
@OperationLog("Delete AiParam")
@PostMapping("/aiParam/remove/{ids}")
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatModelOptionDto.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatModelOptionDto.java
new file mode 100644
index 0000000..8f85ccc
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatModelOptionDto.java
@@ -0,0 +1,19 @@
+package com.vincent.rsf.server.ai.dto;
+
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
+public class AiChatModelOptionDto {
+
+ private Long aiParamId;
+
+ private String name;
+
+ private String model;
+
+ private String providerType;
+
+ private Boolean active;
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRequest.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRequest.java
index bc9f054..53cd86d 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRequest.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRequest.java
@@ -12,6 +12,8 @@
private Long sessionId;
+ private Long aiParamId;
+
private List<AiChatMessageDto> messages;
private String promptCode;
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRuntimeDto.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRuntimeDto.java
index 0a6db57..e298adc 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRuntimeDto.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRuntimeDto.java
@@ -13,12 +13,16 @@
private Long sessionId;
+ private Long aiParamId;
+
private String promptCode;
private String promptName;
private String model;
+ private List<AiChatModelOptionDto> modelOptions;
+
private Integer configuredMcpCount;
private Integer mountedMcpCount;
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java
index 940f251..61b9bf1 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java
@@ -11,7 +11,7 @@
public interface AiChatService {
- AiChatRuntimeDto getRuntime(String promptCode, Long sessionId, Long userId, Long tenantId);
+ AiChatRuntimeDto getRuntime(String promptCode, Long sessionId, Long aiParamId, Long userId, Long tenantId);
List<AiChatSessionDto> listSessions(String promptCode, String keyword, Long userId, Long tenantId);
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiConfigResolverService.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiConfigResolverService.java
index 0f83417..dc346c3 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiConfigResolverService.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiConfigResolverService.java
@@ -5,4 +5,6 @@
public interface AiConfigResolverService {
AiResolvedConfig resolve(String promptCode, Long tenantId);
+
+ AiResolvedConfig resolve(String promptCode, Long tenantId, Long aiParamId);
}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiParamService.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiParamService.java
index 7612957..3396333 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiParamService.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiParamService.java
@@ -1,13 +1,22 @@
package com.vincent.rsf.server.ai.service;
import com.baomidou.mybatisplus.extension.service.IService;
+import com.vincent.rsf.server.ai.dto.AiChatModelOptionDto;
import com.vincent.rsf.server.ai.dto.AiParamValidateResultDto;
import com.vincent.rsf.server.ai.entity.AiParam;
+
+import java.util.List;
public interface AiParamService extends IService<AiParam> {
AiParam getActiveParam(Long tenantId);
+ AiParam getChatParam(Long tenantId, Long aiParamId);
+
+ List<AiChatModelOptionDto> listChatModelOptions(Long tenantId);
+
+ AiParam setDefaultParam(Long id, Long tenantId, Long userId);
+
void validateBeforeSave(AiParam aiParam, Long tenantId);
void validateBeforeUpdate(AiParam aiParam, Long tenantId);
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiCallLogServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiCallLogServiceImpl.java
index d3827a6..1a3d1c4 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiCallLogServiceImpl.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiCallLogServiceImpl.java
@@ -26,6 +26,7 @@
private static final Pattern BEARER_PATTERN = Pattern.compile("(?i)(bearer\\s+)([a-z0-9._-]+)");
private final AiMcpCallLogMapper aiMcpCallLogMapper;
+ private final AiRedisSupport aiRedisSupport;
@Override
public AiCallLog startCallLog(String requestId, Long sessionId, Long userId, Long tenantId, String promptCode,
@@ -51,6 +52,7 @@
.setCreateTime(now)
.setUpdateTime(now);
this.save(callLog);
+ aiRedisSupport.recordObserveCallStarted(tenantId);
return callLog;
}
@@ -73,6 +75,10 @@
.set(AiCallLog::getToolFailureCount, (int) toolFailureCount)
.set(AiCallLog::getToolCallCount, (int) (toolSuccessCount + toolFailureCount))
.set(AiCallLog::getUpdateTime, new Date()));
+ AiCallLog latest = this.getById(callLogId);
+ if (latest != null) {
+ aiRedisSupport.recordObserveCallFinished(latest.getTenantId(), status, elapsedMs, firstTokenLatencyMs, totalTokens);
+ }
}
@Override
@@ -93,6 +99,10 @@
.set(AiCallLog::getToolFailureCount, (int) toolFailureCount)
.set(AiCallLog::getToolCallCount, (int) (toolSuccessCount + toolFailureCount))
.set(AiCallLog::getUpdateTime, new Date()));
+ AiCallLog latest = this.getById(callLogId);
+ if (latest != null) {
+ aiRedisSupport.recordObserveCallFinished(latest.getTenantId(), status, elapsedMs, firstTokenLatencyMs, null);
+ }
}
@Override
@@ -117,10 +127,16 @@
.setUserId(userId)
.setTenantId(tenantId)
.setCreateTime(new Date()));
+ aiRedisSupport.recordObserveToolCall(tenantId, toolName, status);
}
@Override
public AiObserveStatsDto getObserveStats(Long tenantId) {
+ return aiRedisSupport.getObserveStats(tenantId, () -> loadObserveStatsFromDatabase(tenantId));
+ }
+
+ private AiObserveStatsDto loadObserveStatsFromDatabase(Long tenantId) {
+ // 鏁版嵁搴撹仛鍚堝彧浣滀负 Redis 鍐峰惎鍔ㄥ厹搴曪紝姝e父鎯呭喌涓嬬湅鏉垮簲鐩存帴娑堣垂瀹炴椂璁℃暟銆�
List<AiCallLog> callLogs = this.list(new LambdaQueryWrapper<AiCallLog>()
.eq(AiCallLog::getTenantId, tenantId)
.eq(AiCallLog::getDeleted, 0)
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java
index d531a27..a7210c3 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java
@@ -16,19 +16,34 @@
import com.vincent.rsf.server.ai.service.AiChatMemoryService;
import com.vincent.rsf.server.system.enums.StatusType;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ConcurrentHashMap;
@Service
+@Slf4j
@RequiredArgsConstructor
public class AiChatMemoryServiceImpl implements AiChatMemoryService {
private final AiChatSessionMapper aiChatSessionMapper;
private final AiChatMessageMapper aiChatMessageMapper;
+ private final AiRedisSupport aiRedisSupport;
+ @Qualifier("aiMemoryTaskExecutor")
+ private final Executor aiMemoryTaskExecutor;
+
+ /**
+ * 鐢ㄤ袱涓湰鍦伴泦鍚堟妸鈥滃悓涓�涓細璇濈殑鎽樿鍒锋柊鈥濆悎骞舵垚涓茶浠诲姟锛岄伩鍏嶈繛缁秷鎭妸閲嶅浠诲姟濉炴弧绾跨▼姹犮��
+ */
+ private final Set<Long> refreshingSessionIds = ConcurrentHashMap.newKeySet();
+ private final Set<Long> pendingRefreshSessionIds = ConcurrentHashMap.newKeySet();
/**
* 璇诲彇浼氳瘽璁板繂蹇収銆�
@@ -39,11 +54,17 @@
public AiChatMemoryDto getMemory(Long userId, Long tenantId, String promptCode, Long sessionId) {
ensureIdentity(userId, tenantId);
String resolvedPromptCode = requirePromptCode(promptCode);
+ // 浼氳瘽璁板繂灞炰簬鍏稿瀷鈥滆澶氬啓灏戔�濇暟鎹紝鍏堣蛋鐭� TTL 缂撳瓨鑳芥槑鏄惧噺杞绘娊灞夊垵濮嬪寲鍜屽垏浼氳瘽鍘嬪姏銆�
+ AiChatMemoryDto cached = aiRedisSupport.getMemory(tenantId, userId, resolvedPromptCode, sessionId);
+ if (cached != null) {
+ return cached;
+ }
AiChatSession session = sessionId == null
? findLatestSession(userId, tenantId, resolvedPromptCode)
: getSession(sessionId, userId, tenantId, resolvedPromptCode);
+ AiChatMemoryDto memory;
if (session == null) {
- return AiChatMemoryDto.builder()
+ memory = AiChatMemoryDto.builder()
.sessionId(null)
.memorySummary(null)
.memoryFacts(null)
@@ -51,10 +72,12 @@
.persistedMessages(List.of())
.shortMemoryMessages(List.of())
.build();
+ aiRedisSupport.cacheMemory(tenantId, userId, resolvedPromptCode, sessionId, memory);
+ return memory;
}
List<AiChatMessageDto> persistedMessages = listMessages(session.getId());
List<AiChatMessageDto> shortMemoryMessages = tailMessagesByRounds(persistedMessages, AiDefaults.MEMORY_RECENT_ROUNDS);
- return AiChatMemoryDto.builder()
+ memory = AiChatMemoryDto.builder()
.sessionId(session.getId())
.memorySummary(session.getMemorySummary())
.memoryFacts(session.getMemoryFacts())
@@ -62,6 +85,11 @@
.persistedMessages(persistedMessages)
.shortMemoryMessages(shortMemoryMessages)
.build();
+ aiRedisSupport.cacheMemory(tenantId, userId, resolvedPromptCode, session.getId(), memory);
+ if (sessionId == null || !session.getId().equals(sessionId)) {
+ aiRedisSupport.cacheMemory(tenantId, userId, resolvedPromptCode, null, memory);
+ }
+ return memory;
}
/**
@@ -72,6 +100,10 @@
public List<AiChatSessionDto> listSessions(Long userId, Long tenantId, String promptCode, String keyword) {
ensureIdentity(userId, tenantId);
String resolvedPromptCode = requirePromptCode(promptCode);
+ List<AiChatSessionDto> cached = aiRedisSupport.getSessionList(tenantId, userId, resolvedPromptCode, keyword);
+ if (cached != null) {
+ return cached;
+ }
List<AiChatSession> sessions = aiChatSessionMapper.selectList(new LambdaQueryWrapper<AiChatSession>()
.eq(AiChatSession::getUserId, userId)
.eq(AiChatSession::getTenantId, tenantId)
@@ -83,12 +115,14 @@
.orderByDesc(AiChatSession::getLastMessageTime)
.orderByDesc(AiChatSession::getId));
if (Cools.isEmpty(sessions)) {
+ aiRedisSupport.cacheSessionList(tenantId, userId, resolvedPromptCode, keyword, List.of());
return List.of();
}
List<AiChatSessionDto> result = new ArrayList<>();
for (AiChatSession session : sessions) {
result.add(buildSessionDto(session));
}
+ aiRedisSupport.cacheSessionList(tenantId, userId, resolvedPromptCode, keyword, result);
return result;
}
@@ -118,12 +152,14 @@
.setUpdateBy(userId)
.setUpdateTime(now);
aiChatSessionMapper.insert(session);
+ evictConversationCaches(tenantId, userId);
return session;
}
/**
* 钀藉簱淇濆瓨涓�鏁磋疆瀵硅瘽銆�
- * 杩欓噷浼氶『搴忓啓鍏ユ湰杞敤鎴锋秷鎭拰妯″瀷鍥炲锛屽苟鍦ㄦ渶鍚庡埛鏂颁細璇濇爣棰樸�佹渶鍚庢椿璺冩椂闂村拰璁板繂鐢诲儚銆�
+ * 杩欓噷浼氶『搴忓啓鍏ユ湰杞敤鎴锋秷鎭拰妯″瀷鍥炲锛屽苟鍦ㄦ渶鍚庡埛鏂颁細璇濇爣棰樺拰娲昏穬鏃堕棿銆�
+ * 璁板繂鐢诲儚鏀逛负鍚庡彴寮傛鍒锋柊锛岄伩鍏嶆妸鎽樿閲嶇畻鑰楁椂鍘嬪湪鐢ㄦ埛鏈疆鍝嶅簲灏鹃儴銆�
*/
@Override
public void saveRound(AiChatSession session, Long userId, Long tenantId, List<AiChatMessageDto> memoryMessages, String assistantContent) {
@@ -150,7 +186,8 @@
.setUpdateBy(userId)
.setUpdateTime(now);
aiChatSessionMapper.updateById(update);
- refreshMemoryProfile(session.getId(), userId);
+ evictConversationCaches(tenantId, userId);
+ scheduleMemoryProfileRefresh(session.getId(), userId, tenantId);
}
/** 鍒犻櫎鏁翠釜浼氳瘽鍙婂叾娑堟伅銆� */
@@ -185,6 +222,7 @@
.setDeleted(1);
aiChatMessageMapper.updateById(updateMessage);
}
+ evictConversationCaches(tenantId, userId);
}
/** 鏇存柊浼氳瘽鏍囬骞惰繑鍥炴渶鏂颁細璇濇憳瑕併�� */
@@ -202,7 +240,9 @@
.setUpdateBy(userId)
.setUpdateTime(now);
aiChatSessionMapper.updateById(update);
- return buildSessionDto(requireOwnedSession(sessionId, userId, tenantId));
+ AiChatSessionDto sessionDto = buildSessionDto(requireOwnedSession(sessionId, userId, tenantId));
+ evictConversationCaches(tenantId, userId);
+ return sessionDto;
}
/** 鏇存柊浼氳瘽缃《鐘舵�併�� */
@@ -220,7 +260,9 @@
.setUpdateBy(userId)
.setUpdateTime(now);
aiChatSessionMapper.updateById(update);
- return buildSessionDto(requireOwnedSession(sessionId, userId, tenantId));
+ AiChatSessionDto sessionDto = buildSessionDto(requireOwnedSession(sessionId, userId, tenantId));
+ evictConversationCaches(tenantId, userId);
+ return sessionDto;
}
/** 娓呯┖鏌愪釜浼氳瘽鐨勫叏閮ㄦ秷鎭拰娲剧敓璁板繂瀛楁銆� */
@@ -243,6 +285,7 @@
.setUpdateBy(userId)
.setUpdateTime(new Date())
.setLastMessageTime(session.getCreateTime()));
+ evictConversationCaches(tenantId, userId);
}
/** 鍙繚鐣欐渶杩戜竴杞棶绛旓紝鐢ㄤ簬鎵嬪姩瑁佸壀闀夸細璇濄�� */
@@ -263,7 +306,46 @@
.setDeleted(1));
}
}
- refreshMemoryProfile(sessionId, userId);
+ evictConversationCaches(tenantId, userId);
+ scheduleMemoryProfileRefresh(sessionId, userId, tenantId);
+ }
+
+ private void evictConversationCaches(Long tenantId, Long userId) {
+ // 浼氳瘽鏍囬銆佹憳瑕併�佹渶杩戞秷鎭拰 runtime 閮戒細浜掔浉褰卞搷锛岀粺涓�鎸夌敤鎴风淮搴︿竴璧峰け鏁堟洿绋冲Ε銆�
+ aiRedisSupport.evictUserConversationCaches(tenantId, userId);
+ }
+
+ private void scheduleMemoryProfileRefresh(Long sessionId, Long userId, Long tenantId) {
+ if (sessionId == null) {
+ return;
+ }
+ if (!refreshingSessionIds.add(sessionId)) {
+ pendingRefreshSessionIds.add(sessionId);
+ return;
+ }
+ aiMemoryTaskExecutor.execute(() -> runMemoryProfileRefreshLoop(sessionId, userId, tenantId));
+ }
+
+ private void runMemoryProfileRefreshLoop(Long sessionId, Long userId, Long tenantId) {
+ try {
+ boolean shouldContinue;
+ do {
+ pendingRefreshSessionIds.remove(sessionId);
+ try {
+ refreshMemoryProfile(sessionId, userId);
+ evictConversationCaches(tenantId, userId);
+ } catch (Exception e) {
+ log.warn("AI memory profile refresh failed, sessionId={}, userId={}, tenantId={}, message={}",
+ sessionId, userId, tenantId, e.getMessage(), e);
+ }
+ shouldContinue = pendingRefreshSessionIds.remove(sessionId);
+ } while (shouldContinue);
+ } finally {
+ refreshingSessionIds.remove(sessionId);
+ if (pendingRefreshSessionIds.remove(sessionId) && refreshingSessionIds.add(sessionId)) {
+ aiMemoryTaskExecutor.execute(() -> runMemoryProfileRefreshLoop(sessionId, userId, tenantId));
+ }
+ }
}
private AiChatSession findLatestSession(Long userId, Long tenantId, String promptCode) {
@@ -459,6 +541,7 @@
/**
* 閲嶆柊璁$畻浼氳瘽鐨勬憳瑕佽蹇嗗拰鍏抽敭浜嬪疄銆�
* 杩欐槸鈥滄寔涔呭寲娑堟伅鈥濆拰鈥滄ā鍨嬩笂涓嬫枃娌荤悊鈥濅箣闂寸殑妗ユ鏂规硶銆�
+ * 鐜板湪瀹冭繍琛屽湪鍚庡彴绾跨▼閲岋紝鍥犳鍏佽鐭椂闂存渶缁堜竴鑷达紝鑰屼笉鏄己鍒舵湰杞悓姝ュ畬鎴愩��
*/
List<AiChatMessageDto> messages = listMessages(sessionId);
List<AiChatMessageDto> shortMemoryMessages = tailMessagesByRounds(messages, AiDefaults.MEMORY_RECENT_ROUNDS);
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java
index e7d842e..320421e 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java
@@ -8,6 +8,7 @@
import com.vincent.rsf.server.ai.dto.AiChatErrorDto;
import com.vincent.rsf.server.ai.dto.AiChatMemoryDto;
import com.vincent.rsf.server.ai.dto.AiChatMessageDto;
+import com.vincent.rsf.server.ai.dto.AiChatModelOptionDto;
import com.vincent.rsf.server.ai.dto.AiChatRequest;
import com.vincent.rsf.server.ai.dto.AiChatRuntimeDto;
import com.vincent.rsf.server.ai.dto.AiChatStatusDto;
@@ -27,6 +28,7 @@
import com.vincent.rsf.server.ai.service.AiChatService;
import com.vincent.rsf.server.ai.service.AiChatMemoryService;
import com.vincent.rsf.server.ai.service.AiConfigResolverService;
+import com.vincent.rsf.server.ai.service.AiParamService;
import com.vincent.rsf.server.ai.service.MountedToolCallback;
import com.vincent.rsf.server.ai.service.McpMountRuntimeFactory;
import io.micrometer.observation.ObservationRegistry;
@@ -78,8 +80,10 @@
private final AiConfigResolverService aiConfigResolverService;
private final AiChatMemoryService aiChatMemoryService;
+ private final AiParamService aiParamService;
private final McpMountRuntimeFactory mcpMountRuntimeFactory;
private final AiCallLogService aiCallLogService;
+ private final AiRedisSupport aiRedisSupport;
private final GenericApplicationContext applicationContext;
private final ObservationRegistry observationRegistry;
private final ObjectMapper objectMapper;
@@ -91,24 +95,31 @@
* 璇ユ柟娉曚笉浼氳Е鍙戞ā鍨嬭皟鐢紝鑰屾槸鎶婇厤缃В鏋愮粨鏋滃拰浼氳瘽璁板繂鑱氬悎鎴愬墠绔竴娆℃覆鏌撴墍闇�鐨勫揩鐓с��
*/
@Override
- public AiChatRuntimeDto getRuntime(String promptCode, Long sessionId, Long userId, Long tenantId) {
- AiResolvedConfig config = aiConfigResolverService.resolve(promptCode, tenantId);
+ public AiChatRuntimeDto getRuntime(String promptCode, Long sessionId, Long aiParamId, Long userId, Long tenantId) {
+ AiResolvedConfig config = aiConfigResolverService.resolve(promptCode, tenantId, aiParamId);
+ Long runtimeCacheAiParamId = aiParamId;
+ // runtime 鏄厤缃揩鐓у拰浼氳瘽璁板繂鐨勮仛鍚堣鍥撅紝鍗曠嫭缂撳瓨鑳藉噺灏戜竴娆¢〉闈㈣繘鍏ユ椂鐨勯噸澶嶆嫾瑁呫��
+ AiChatRuntimeDto cached = aiRedisSupport.getRuntime(tenantId, userId, config.getPromptCode(), sessionId, runtimeCacheAiParamId);
+ if (cached != null) {
+ return cached;
+ }
AiChatMemoryDto memory = aiChatMemoryService.getMemory(userId, tenantId, config.getPromptCode(), sessionId);
- return AiChatRuntimeDto.builder()
- .requestId(null)
- .sessionId(memory.getSessionId())
- .promptCode(config.getPromptCode())
- .promptName(config.getPrompt().getName())
- .model(config.getAiParam().getModel())
- .configuredMcpCount(config.getMcpMounts().size())
- .mountedMcpCount(config.getMcpMounts().size())
- .mountedMcpNames(config.getMcpMounts().stream().map(item -> item.getName()).toList())
- .mountErrors(List.of())
- .memorySummary(memory.getMemorySummary())
- .memoryFacts(memory.getMemoryFacts())
- .recentMessageCount(memory.getRecentMessageCount())
- .persistedMessages(memory.getPersistedMessages())
- .build();
+ List<AiChatModelOptionDto> modelOptions = aiParamService.listChatModelOptions(tenantId);
+ AiChatRuntimeDto runtime = buildRuntimeSnapshot(
+ null,
+ memory.getSessionId(),
+ config,
+ modelOptions,
+ config.getMcpMounts().size(),
+ config.getMcpMounts().stream().map(item -> item.getName()).toList(),
+ List.of(),
+ memory
+ );
+ aiRedisSupport.cacheRuntime(tenantId, userId, config.getPromptCode(), sessionId, runtimeCacheAiParamId, runtime);
+ if (memory.getSessionId() != null && !Objects.equals(memory.getSessionId(), sessionId)) {
+ aiRedisSupport.cacheRuntime(tenantId, userId, config.getPromptCode(), memory.getSessionId(), runtimeCacheAiParamId, runtime);
+ }
+ return runtime;
}
/**
@@ -174,14 +185,23 @@
Long sessionId = request.getSessionId();
Long callLogId = null;
String model = null;
+ String resolvedPromptCode = request.getPromptCode();
ThinkingTraceEmitter thinkingTraceEmitter = null;
try {
ensureIdentity(userId, tenantId);
AiResolvedConfig config = resolveConfig(request, tenantId);
+ List<AiChatModelOptionDto> modelOptions = aiParamService.listChatModelOptions(tenantId);
+ resolvedPromptCode = config.getPromptCode();
+ if (!aiRedisSupport.allowChatRequest(tenantId, userId, config.getPromptCode())) {
+ throw buildAiException("AI_RATE_LIMITED", AiErrorCategory.REQUEST, "RATE_LIMIT",
+ "褰撳墠鎻愰棶杩囦簬棰戠箒锛岃绋嶅悗鍐嶈瘯", null);
+ }
final String resolvedModel = config.getAiParam().getModel();
model = resolvedModel;
AiChatSession session = resolveSession(request, userId, tenantId, config.getPromptCode());
sessionId = session.getId();
+ // 娴佺姸鎬佽惤 Redis 鐨勭洰鏍囨槸缁欏瀹炰緥鍜屽悗缁繍缁存煡璇㈢暀缁熶竴鍏ュ彛锛屼笉鏇夸唬鏁版嵁搴撴棩蹇椼��
+ aiRedisSupport.markStreamState(requestId, tenantId, userId, sessionId, config.getPromptCode(), "RUNNING", null);
AiChatMemoryDto memory = loadMemory(userId, tenantId, config.getPromptCode(), session.getId());
List<AiChatMessageDto> mergedMessages = mergeMessages(memory.getShortMemoryMessages(), request.getMessages());
AiCallLog callLog = aiCallLogService.startCallLog(
@@ -198,21 +218,16 @@
);
callLogId = callLog.getId();
try (McpMountRuntimeFactory.McpMountRuntime runtime = createRuntime(config, userId)) {
- emitStrict(emitter, "start", AiChatRuntimeDto.builder()
- .requestId(requestId)
- .sessionId(session.getId())
- .promptCode(config.getPromptCode())
- .promptName(config.getPrompt().getName())
- .model(config.getAiParam().getModel())
- .configuredMcpCount(config.getMcpMounts().size())
- .mountedMcpCount(runtime.getMountedCount())
- .mountedMcpNames(runtime.getMountedNames())
- .mountErrors(runtime.getErrors())
- .memorySummary(memory.getMemorySummary())
- .memoryFacts(memory.getMemoryFacts())
- .recentMessageCount(memory.getRecentMessageCount())
- .persistedMessages(memory.getPersistedMessages())
- .build());
+ emitStrict(emitter, "start", buildRuntimeSnapshot(
+ requestId,
+ session.getId(),
+ config,
+ modelOptions,
+ runtime.getMountedCount(),
+ runtime.getMountedNames(),
+ runtime.getErrors(),
+ memory
+ ));
emitSafely(emitter, "status", AiChatStatusDto.builder()
.requestId(requestId)
.sessionId(session.getId())
@@ -259,6 +274,7 @@
toolSuccessCount.get(),
toolFailureCount.get()
);
+ aiRedisSupport.markStreamState(requestId, tenantId, userId, session.getId(), config.getPromptCode(), "COMPLETED", null);
log.info("AI chat completed, requestId={}, sessionId={}, elapsedMs={}, firstTokenLatencyMs={}",
requestId, session.getId(), System.currentTimeMillis() - startedAt, resolveFirstTokenLatency(startedAt, firstTokenAtRef.get()));
emitter.complete();
@@ -298,18 +314,21 @@
toolSuccessCount.get(),
toolFailureCount.get()
);
+ aiRedisSupport.markStreamState(requestId, tenantId, userId, session.getId(), config.getPromptCode(), "COMPLETED", null);
log.info("AI chat completed, requestId={}, sessionId={}, elapsedMs={}, firstTokenLatencyMs={}",
requestId, session.getId(), System.currentTimeMillis() - startedAt, resolveFirstTokenLatency(startedAt, firstTokenAtRef.get()));
emitter.complete();
}
} catch (AiChatException e) {
handleStreamFailure(emitter, requestId, sessionId, model, startedAt, firstTokenAtRef.get(), e,
- callLogId, toolSuccessCount.get(), toolFailureCount.get(), thinkingTraceEmitter);
+ callLogId, toolSuccessCount.get(), toolFailureCount.get(), thinkingTraceEmitter,
+ tenantId, userId, resolvedPromptCode);
} catch (Exception e) {
handleStreamFailure(emitter, requestId, sessionId, model, startedAt, firstTokenAtRef.get(),
buildAiException("AI_INTERNAL_ERROR", AiErrorCategory.INTERNAL, "INTERNAL",
e == null ? "AI 瀵硅瘽澶辫触" : e.getMessage(), e),
- callLogId, toolSuccessCount.get(), toolFailureCount.get(), thinkingTraceEmitter);
+ callLogId, toolSuccessCount.get(), toolFailureCount.get(), thinkingTraceEmitter,
+ tenantId, userId, resolvedPromptCode);
} finally {
log.debug("AI chat stream finished, requestId={}", requestId);
}
@@ -327,11 +346,34 @@
private AiResolvedConfig resolveConfig(AiChatRequest request, Long tenantId) {
/** 鎶婅姹傞噷鐨� Prompt 鍦烘櫙瑙f瀽鎴愪竴浠藉彲鐩存帴鎵ц鐨� AI 閰嶇疆銆� */
try {
- return aiConfigResolverService.resolve(request.getPromptCode(), tenantId);
+ return aiConfigResolverService.resolve(request.getPromptCode(), tenantId, request.getAiParamId());
} catch (Exception e) {
throw buildAiException("AI_CONFIG_RESOLVE_ERROR", AiErrorCategory.CONFIG, "CONFIG_RESOLVE",
e == null ? "AI 閰嶇疆瑙f瀽澶辫触" : e.getMessage(), e);
}
+ }
+
+ private AiChatRuntimeDto buildRuntimeSnapshot(String requestId, Long sessionId, AiResolvedConfig config,
+ List<AiChatModelOptionDto> modelOptions, Integer mountedMcpCount,
+ List<String> mountedMcpNames, List<String> mountErrors,
+ AiChatMemoryDto memory) {
+ return AiChatRuntimeDto.builder()
+ .requestId(requestId)
+ .sessionId(sessionId)
+ .aiParamId(config.getAiParam().getId())
+ .promptCode(config.getPromptCode())
+ .promptName(config.getPrompt().getName())
+ .model(config.getAiParam().getModel())
+ .modelOptions(modelOptions)
+ .configuredMcpCount(config.getMcpMounts().size())
+ .mountedMcpCount(mountedMcpCount)
+ .mountedMcpNames(mountedMcpNames)
+ .mountErrors(mountErrors)
+ .memorySummary(memory.getMemorySummary())
+ .memoryFacts(memory.getMemoryFacts())
+ .recentMessageCount(memory.getRecentMessageCount())
+ .persistedMessages(memory.getPersistedMessages())
+ .build();
}
private AiChatSession resolveSession(AiChatRequest request, Long userId, Long tenantId, String promptCode) {
@@ -420,7 +462,8 @@
private void handleStreamFailure(SseEmitter emitter, String requestId, Long sessionId, String model, long startedAt,
Long firstTokenAt, AiChatException exception, Long callLogId,
long toolSuccessCount, long toolFailureCount,
- ThinkingTraceEmitter thinkingTraceEmitter) {
+ ThinkingTraceEmitter thinkingTraceEmitter,
+ Long tenantId, Long userId, String promptCode) {
if (isClientAbortException(exception)) {
log.warn("AI chat aborted by client, requestId={}, sessionId={}, stage={}, message={}",
requestId, sessionId, exception.getStage(), exception.getMessage());
@@ -439,6 +482,7 @@
toolSuccessCount,
toolFailureCount
);
+ aiRedisSupport.markStreamState(requestId, tenantId, userId, sessionId, promptCode, "ABORTED", exception.getMessage());
emitter.completeWithError(exception);
return;
}
@@ -468,6 +512,7 @@
toolSuccessCount,
toolFailureCount
);
+ aiRedisSupport.markStreamState(requestId, tenantId, userId, sessionId, promptCode, "FAILED", exception.getMessage());
emitter.completeWithError(exception);
}
@@ -921,6 +966,38 @@
String mountName = delegate instanceof MountedToolCallback ? ((MountedToolCallback) delegate).getMountName() : null;
String toolCallId = requestId + "-tool-" + toolCallSequence.incrementAndGet();
long startedAt = System.currentTimeMillis();
+ // 杩欓噷鍙鍚屼竴 request 鍐呯殑閲嶅宸ュ叿璋冪敤鍋氱煭鏈熷鐢紝閬垮厤鎶婅法璇锋眰缁撴灉璇綋鎴愰�氱敤缂撳瓨銆�
+ AiRedisSupport.CachedToolResult cachedToolResult = aiRedisSupport.getToolResult(tenantId, requestId, toolName, toolInput);
+ if (cachedToolResult != null) {
+ emitSafely(emitter, "tool_result", AiChatToolEventDto.builder()
+ .requestId(requestId)
+ .sessionId(sessionId)
+ .toolCallId(toolCallId)
+ .toolName(toolName)
+ .mountName(mountName)
+ .status(cachedToolResult.isSuccess() ? "COMPLETED" : "FAILED")
+ .inputSummary(summarizeToolPayload(toolInput, 400))
+ .outputSummary(summarizeToolPayload(cachedToolResult.getOutput(), 600))
+ .errorMessage(cachedToolResult.getErrorMessage())
+ .durationMs(0L)
+ .timestamp(System.currentTimeMillis())
+ .build());
+ if (thinkingTraceEmitter != null) {
+ thinkingTraceEmitter.onToolResult(toolName, toolCallId, !cachedToolResult.isSuccess());
+ }
+ if (cachedToolResult.isSuccess()) {
+ toolSuccessCount.incrementAndGet();
+ aiCallLogService.saveMcpCallLog(callLogId, requestId, sessionId, toolCallId, mountName, toolName,
+ "COMPLETED", summarizeToolPayload(toolInput, 400), summarizeToolPayload(cachedToolResult.getOutput(), 600),
+ null, 0L, userId, tenantId);
+ return cachedToolResult.getOutput();
+ }
+ toolFailureCount.incrementAndGet();
+ aiCallLogService.saveMcpCallLog(callLogId, requestId, sessionId, toolCallId, mountName, toolName,
+ "FAILED", summarizeToolPayload(toolInput, 400), null, cachedToolResult.getErrorMessage(),
+ 0L, userId, tenantId);
+ throw new CoolException(cachedToolResult.getErrorMessage());
+ }
if (thinkingTraceEmitter != null) {
thinkingTraceEmitter.onToolStart(toolName, toolCallId);
}
@@ -952,6 +1029,7 @@
if (thinkingTraceEmitter != null) {
thinkingTraceEmitter.onToolResult(toolName, toolCallId, false);
}
+ aiRedisSupport.cacheToolResult(tenantId, requestId, toolName, toolInput, true, output, null);
toolSuccessCount.incrementAndGet();
aiCallLogService.saveMcpCallLog(callLogId, requestId, sessionId, toolCallId, mountName, toolName,
"COMPLETED", summarizeToolPayload(toolInput, 400), summarizeToolPayload(output, 600),
@@ -974,6 +1052,7 @@
if (thinkingTraceEmitter != null) {
thinkingTraceEmitter.onToolResult(toolName, toolCallId, true);
}
+ aiRedisSupport.cacheToolResult(tenantId, requestId, toolName, toolInput, false, null, e.getMessage());
toolFailureCount.incrementAndGet();
aiCallLogService.saveMcpCallLog(callLogId, requestId, sessionId, toolCallId, mountName, toolName,
"FAILED", summarizeToolPayload(toolInput, 400), null, e.getMessage(),
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiConfigOpsServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiConfigOpsServiceImpl.java
index 78b7183..8993260 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiConfigOpsServiceImpl.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiConfigOpsServiceImpl.java
@@ -5,11 +5,10 @@
import com.vincent.rsf.server.ai.dto.AiPromptPreviewDto;
import com.vincent.rsf.server.ai.dto.AiPromptPreviewRequest;
import com.vincent.rsf.server.ai.entity.AiMcpMount;
-import com.vincent.rsf.server.ai.entity.AiParam;
-import com.vincent.rsf.server.ai.entity.AiPrompt;
+import com.vincent.rsf.server.ai.dto.AiResolvedConfig;
import com.vincent.rsf.server.ai.service.AiConfigOpsService;
+import com.vincent.rsf.server.ai.service.AiConfigResolverService;
import com.vincent.rsf.server.ai.service.AiMcpMountService;
-import com.vincent.rsf.server.ai.service.AiParamService;
import com.vincent.rsf.server.ai.service.AiPromptService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -21,32 +20,30 @@
@RequiredArgsConstructor
public class AiConfigOpsServiceImpl implements AiConfigOpsService {
- private final AiParamService aiParamService;
- private final AiPromptService aiPromptService;
- private final AiMcpMountService aiMcpMountService;
+ private final AiConfigResolverService aiConfigResolverService;
private final AiPromptRenderSupport aiPromptRenderSupport;
+ private final AiPromptService aiPromptService;
@Override
public AiConfigSummaryDto getSummary(String promptCode, Long tenantId) {
String finalPromptCode = StringUtils.hasText(promptCode) ? promptCode : AiDefaults.DEFAULT_PROMPT_CODE;
- AiParam activeParam = aiParamService.getActiveParam(tenantId);
- AiPrompt activePrompt = aiPromptService.getActivePrompt(finalPromptCode, tenantId);
- List<AiMcpMount> mounts = aiMcpMountService.listActiveMounts(tenantId);
+ AiResolvedConfig resolvedConfig = aiConfigResolverService.resolve(finalPromptCode, tenantId);
+ List<AiMcpMount> mounts = resolvedConfig.getMcpMounts();
return AiConfigSummaryDto.builder()
- .promptCode(activePrompt.getCode())
- .promptName(activePrompt.getName())
- .promptScene(activePrompt.getScene())
- .activeParamName(activeParam.getName())
- .activeModel(activeParam.getModel())
- .activeParamUpdatedAt(activeParam.getUpdateTime$())
- .activeParamUpdatedBy(activeParam.getUpdateBy())
- .activeParamValidateStatus(activeParam.getValidateStatus())
- .activeParamValidateMessage(activeParam.getLastValidateMessage())
- .activeParamValidatedAt(activeParam.getLastValidateTime$())
+ .promptCode(resolvedConfig.getPrompt().getCode())
+ .promptName(resolvedConfig.getPrompt().getName())
+ .promptScene(resolvedConfig.getPrompt().getScene())
+ .activeParamName(resolvedConfig.getAiParam().getName())
+ .activeModel(resolvedConfig.getAiParam().getModel())
+ .activeParamUpdatedAt(resolvedConfig.getAiParam().getUpdateTime$())
+ .activeParamUpdatedBy(resolvedConfig.getAiParam().getUpdateBy())
+ .activeParamValidateStatus(resolvedConfig.getAiParam().getValidateStatus())
+ .activeParamValidateMessage(resolvedConfig.getAiParam().getLastValidateMessage())
+ .activeParamValidatedAt(resolvedConfig.getAiParam().getLastValidateTime$())
.enabledMcpCount(mounts.size())
.enabledMcpNames(mounts.stream().map(AiMcpMount::getName).toList())
- .activePromptUpdatedAt(activePrompt.getUpdateTime$())
- .activePromptUpdatedBy(activePrompt.getUpdateBy())
+ .activePromptUpdatedAt(resolvedConfig.getPrompt().getUpdateTime$())
+ .activePromptUpdatedBy(resolvedConfig.getPrompt().getUpdateBy())
.build();
}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiConfigResolverServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiConfigResolverServiceImpl.java
index b6087f5..7aa47b0 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiConfigResolverServiceImpl.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiConfigResolverServiceImpl.java
@@ -18,6 +18,7 @@
private final AiParamService aiParamService;
private final AiPromptService aiPromptService;
private final AiMcpMountService aiMcpMountService;
+ private final AiRedisSupport aiRedisSupport;
/**
* 鎸夌鎴疯В鏋愪竴娆″畬鏁寸殑 AI 杩愯閰嶇疆銆�
@@ -26,15 +27,27 @@
*/
@Override
public AiResolvedConfig resolve(String promptCode, Long tenantId) {
+ return resolve(promptCode, tenantId, null);
+ }
+
+ @Override
+ public AiResolvedConfig resolve(String promptCode, Long tenantId, Long aiParamId) {
if (tenantId == null) {
throw new CoolException("褰撳墠绉熸埛涓嶅瓨鍦�");
}
String finalPromptCode = StringUtils.hasText(promptCode) ? promptCode : AiDefaults.DEFAULT_PROMPT_CODE;
- return AiResolvedConfig.builder()
+ // 閰嶇疆瑙f瀽鏄涓叆鍙e叡浜殑鐑偣璺緞锛屽懡涓紦瀛樻椂鍙互閬垮厤涓夊紶閰嶇疆琛ㄧ殑閲嶅鏌ヨ銆�
+ AiResolvedConfig cached = aiRedisSupport.getResolvedConfig(tenantId, finalPromptCode, aiParamId);
+ if (cached != null) {
+ return cached;
+ }
+ AiResolvedConfig resolvedConfig = AiResolvedConfig.builder()
.promptCode(finalPromptCode)
- .aiParam(aiParamService.getActiveParam(tenantId))
+ .aiParam(aiParamService.getChatParam(tenantId, aiParamId))
.prompt(aiPromptService.getActivePrompt(finalPromptCode, tenantId))
.mcpMounts(aiMcpMountService.listActiveMounts(tenantId))
.build();
+ aiRedisSupport.cacheResolvedConfig(tenantId, finalPromptCode, aiParamId, resolvedConfig);
+ return resolvedConfig;
}
}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiMcpMountServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiMcpMountServiceImpl.java
index f09b0fa..409c20f 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiMcpMountServiceImpl.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiMcpMountServiceImpl.java
@@ -35,6 +35,7 @@
private final BuiltinMcpToolRegistry builtinMcpToolRegistry;
private final McpMountRuntimeFactory mcpMountRuntimeFactory;
private final ObjectMapper objectMapper;
+ private final AiRedisSupport aiRedisSupport;
/** 鏌ヨ鏌愪釜绉熸埛涓嬪綋鍓嶅惎鐢ㄧ殑 MCP 鎸傝浇鍒楄〃銆� */
@Override
@@ -77,6 +78,11 @@
@Override
public List<AiMcpToolPreviewDto> previewTools(Long mountId, Long userId, Long tenantId) {
AiMcpMount mount = requireMount(mountId, tenantId);
+ // 宸ュ叿鐩綍棰勮鍒濆鍖栨垚鏈珮锛屼絾鍙樺寲棰戠巼浣庯紝閫傚悎鍋氱鐞嗙鐭紦瀛樸��
+ List<AiMcpToolPreviewDto> cached = aiRedisSupport.getToolPreview(tenantId, mountId);
+ if (cached != null) {
+ return cached;
+ }
long startedAt = System.currentTimeMillis();
try (McpMountRuntimeFactory.McpMountRuntime runtime = mcpMountRuntimeFactory.create(List.of(mount), userId)) {
List<AiMcpToolPreviewDto> tools = buildToolPreviewDtos(runtime.getToolCallbacks(),
@@ -90,6 +96,7 @@
}
updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_HEALTHY,
"宸ュ叿瑙f瀽鎴愬姛锛屽叡 " + tools.size() + " 涓伐鍏�", System.currentTimeMillis() - startedAt);
+ aiRedisSupport.cacheToolPreview(tenantId, mountId, tools);
return tools;
} catch (CoolException e) {
throw e;
@@ -111,12 +118,16 @@
String message = String.join("锛�", runtime.getErrors());
updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_UNHEALTHY, message, elapsedMs);
AiMcpMount latest = requireMount(mount.getId(), tenantId);
- return buildConnectivityDto(latest, message, elapsedMs, runtime.getToolCallbacks().length);
+ AiMcpConnectivityTestDto connectivity = buildConnectivityDto(latest, message, elapsedMs, runtime.getToolCallbacks().length);
+ aiRedisSupport.cacheConnectivity(tenantId, mountId, connectivity);
+ return connectivity;
}
String message = "杩為�氭�ф祴璇曟垚鍔燂紝瑙f瀽鍑� " + runtime.getToolCallbacks().length + " 涓伐鍏�";
updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_HEALTHY, message, elapsedMs);
AiMcpMount latest = requireMount(mount.getId(), tenantId);
- return buildConnectivityDto(latest, message, elapsedMs, runtime.getToolCallbacks().length);
+ AiMcpConnectivityTestDto connectivity = buildConnectivityDto(latest, message, elapsedMs, runtime.getToolCallbacks().length);
+ aiRedisSupport.cacheConnectivity(tenantId, mountId, connectivity);
+ return connectivity;
} catch (CoolException e) {
throw e;
} catch (Exception e) {
@@ -124,7 +135,9 @@
String message = "杩為�氭�ф祴璇曞け璐�: " + e.getMessage();
updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_UNHEALTHY, message, elapsedMs);
AiMcpMount latest = requireMount(mount.getId(), tenantId);
- return buildConnectivityDto(latest, message, elapsedMs, 0);
+ AiMcpConnectivityTestDto connectivity = buildConnectivityDto(latest, message, elapsedMs, 0);
+ aiRedisSupport.cacheConnectivity(tenantId, mountId, connectivity);
+ return connectivity;
}
}
@@ -227,6 +240,40 @@
}
}
+ @Override
+ public boolean save(AiMcpMount entity) {
+ boolean saved = super.save(entity);
+ if (saved && entity != null && entity.getTenantId() != null) {
+ aiRedisSupport.evictMcpMountCaches(entity.getTenantId(), entity.getId());
+ }
+ return saved;
+ }
+
+ @Override
+ public boolean updateById(AiMcpMount entity) {
+ boolean updated = super.updateById(entity);
+ if (updated && entity != null && entity.getTenantId() != null) {
+ aiRedisSupport.evictMcpMountCaches(entity.getTenantId(), entity.getId());
+ }
+ return updated;
+ }
+
+ @Override
+ public boolean removeByIds(java.util.Collection<?> list) {
+ java.util.List<java.io.Serializable> ids = list == null ? java.util.List.of() : list.stream()
+ .filter(java.util.Objects::nonNull)
+ .map(item -> (java.io.Serializable) item)
+ .toList();
+ java.util.List<AiMcpMount> records = this.listByIds(ids);
+ boolean removed = super.removeByIds(list);
+ if (removed) {
+ records.stream()
+ .filter(java.util.Objects::nonNull)
+ .forEach(item -> aiRedisSupport.evictMcpMountCaches(item.getTenantId(), item.getId()));
+ }
+ return removed;
+ }
+
private void fillDefaults(AiMcpMount aiMcpMount) {
/** 涓烘寕杞借崏绋胯ˉ榻愮粺涓�榛樿鍊硷紝淇濊瘉鍚庣画杩愯鏃朵唬鐮佷笉闇�瑕侀噸澶嶅垽鏂┖鍊笺�� */
if (!StringUtils.hasText(aiMcpMount.getTransportType())) {
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiParamServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiParamServiceImpl.java
index 957c7b0..4e1e83e 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiParamServiceImpl.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiParamServiceImpl.java
@@ -4,6 +4,7 @@
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.ai.config.AiDefaults;
+import com.vincent.rsf.server.ai.dto.AiChatModelOptionDto;
import com.vincent.rsf.server.ai.dto.AiParamValidateResultDto;
import com.vincent.rsf.server.ai.entity.AiParam;
import com.vincent.rsf.server.ai.mapper.AiParamMapper;
@@ -11,16 +12,19 @@
import com.vincent.rsf.server.system.enums.StatusType;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.text.SimpleDateFormat;
import java.util.Date;
+import java.util.List;
@Service("aiParamService")
@RequiredArgsConstructor
public class AiParamServiceImpl extends ServiceImpl<AiParamMapper, AiParam> implements AiParamService {
private final AiParamValidationSupport aiParamValidationSupport;
+ private final AiRedisSupport aiRedisSupport;
@Override
public AiParam getActiveParam(Long tenantId) {
@@ -34,6 +38,67 @@
throw new CoolException("鏈壘鍒板惎鐢ㄤ腑鐨� AI 鍙傛暟閰嶇疆");
}
return aiParam;
+ }
+
+ @Override
+ public AiParam getChatParam(Long tenantId, Long aiParamId) {
+ ensureTenantId(tenantId);
+ if (aiParamId == null) {
+ return getActiveParam(tenantId);
+ }
+ AiParam aiParam = requireOwnedRecord(aiParamId, tenantId);
+ if (!AiDefaults.PARAM_VALIDATE_VALID.equals(aiParam.getValidateStatus())) {
+ throw new CoolException("鎵�閫� AI 妯″瀷鏈�氳繃鏍¢獙锛屾殏涓嶅彲鐢ㄤ簬瀵硅瘽");
+ }
+ return aiParam;
+ }
+
+ @Override
+ public List<AiChatModelOptionDto> listChatModelOptions(Long tenantId) {
+ ensureTenantId(tenantId);
+ List<AiParam> params = this.list(new LambdaQueryWrapper<AiParam>()
+ .eq(AiParam::getTenantId, tenantId)
+ .eq(AiParam::getDeleted, 0)
+ .eq(AiParam::getValidateStatus, AiDefaults.PARAM_VALIDATE_VALID)
+ .orderByDesc(AiParam::getStatus)
+ .orderByDesc(AiParam::getUpdateTime)
+ .orderByDesc(AiParam::getCreateTime)
+ .orderByDesc(AiParam::getId));
+ if (params.isEmpty()) {
+ return List.of(toChatModelOption(getActiveParam(tenantId)));
+ }
+ return params.stream()
+ .map(this::toChatModelOption)
+ .toList();
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public AiParam setDefaultParam(Long id, Long tenantId, Long userId) {
+ ensureTenantId(tenantId);
+ if (id == null) {
+ throw new CoolException("AI 鍙傛暟 ID 涓嶈兘涓虹┖");
+ }
+ AiParam target = requireOwnedRecord(id, tenantId);
+ if (!AiDefaults.PARAM_VALIDATE_VALID.equals(target.getValidateStatus())) {
+ throw new CoolException("浠呭厑璁稿皢鏍¢獙閫氳繃鐨� AI 鍙傛暟璁剧疆涓洪粯璁�");
+ }
+ Date now = new Date();
+ this.lambdaUpdate()
+ .eq(AiParam::getTenantId, tenantId)
+ .eq(AiParam::getDeleted, 0)
+ .set(AiParam::getStatus, StatusType.DISABLE.val)
+ .set(AiParam::getUpdateBy, userId)
+ .set(AiParam::getUpdateTime, now)
+ .update();
+ target.setStatus(StatusType.ENABLE.val);
+ target.setUpdateBy(userId);
+ target.setUpdateTime(now);
+ if (!super.updateById(target)) {
+ throw new CoolException("璁剧疆榛樿 AI 鍙傛暟澶辫触");
+ }
+ aiRedisSupport.evictTenantConfigCaches(tenantId);
+ return target;
}
@Override
@@ -56,6 +121,7 @@
AiParam current = requireOwnedRecord(aiParam.getId(), tenantId);
aiParam.setTenantId(current.getTenantId());
ensureBaseFields(aiParam);
+ ensureDefaultStillExists(tenantId, current, aiParam.getStatus());
ensureSingleActive(tenantId, aiParam.getId(), aiParam.getStatus());
applyValidation(aiParam);
}
@@ -66,6 +132,43 @@
fillDefaults(aiParam);
ensureBaseFields(aiParam);
return aiParamValidationSupport.validate(aiParam);
+ }
+
+ @Override
+ public boolean save(AiParam entity) {
+ boolean saved = super.save(entity);
+ if (saved && entity != null && entity.getTenantId() != null) {
+ aiRedisSupport.evictTenantConfigCaches(entity.getTenantId());
+ }
+ return saved;
+ }
+
+ @Override
+ public boolean updateById(AiParam entity) {
+ boolean updated = super.updateById(entity);
+ if (updated && entity != null && entity.getTenantId() != null) {
+ aiRedisSupport.evictTenantConfigCaches(entity.getTenantId());
+ }
+ return updated;
+ }
+
+ @Override
+ public boolean removeByIds(java.util.Collection<?> list) {
+ java.util.List<java.io.Serializable> ids = list == null ? java.util.List.of() : list.stream()
+ .filter(java.util.Objects::nonNull)
+ .map(item -> (java.io.Serializable) item)
+ .toList();
+ java.util.List<AiParam> records = this.listByIds(ids);
+ ensureRemovingDefaultIsSafe(records);
+ boolean removed = super.removeByIds(list);
+ if (removed) {
+ records.stream()
+ .map(AiParam::getTenantId)
+ .filter(java.util.Objects::nonNull)
+ .distinct()
+ .forEach(aiRedisSupport::evictTenantConfigCaches);
+ }
+ return removed;
}
private void ensureBaseFields(AiParam aiParam) {
@@ -99,6 +202,44 @@
}
if (this.count(wrapper) > 0) {
throw new CoolException("鍚屼竴绉熸埛浠呭厑璁镐竴鏉″惎鐢ㄤ腑鐨� AI 鍙傛暟閰嶇疆");
+ }
+ }
+
+ private void ensureDefaultStillExists(Long tenantId, AiParam current, Integer nextStatus) {
+ if (current == null || current.getStatus() == null || current.getStatus() != StatusType.ENABLE.val) {
+ return;
+ }
+ if (nextStatus != null && nextStatus == StatusType.ENABLE.val) {
+ return;
+ }
+ long otherDefaultCount = this.count(new LambdaQueryWrapper<AiParam>()
+ .eq(AiParam::getTenantId, tenantId)
+ .eq(AiParam::getDeleted, 0)
+ .eq(AiParam::getStatus, StatusType.ENABLE.val)
+ .ne(AiParam::getId, current.getId()));
+ if (otherDefaultCount == 0) {
+ throw new CoolException("璇峰厛灏嗗叾浠� AI 鍙傛暟璁剧疆涓洪粯璁わ紝鍐嶅彇娑堝綋鍓嶉粯璁�");
+ }
+ }
+
+ private void ensureRemovingDefaultIsSafe(List<AiParam> records) {
+ if (records == null || records.isEmpty()) {
+ return;
+ }
+ records.stream()
+ .filter(item -> item.getTenantId() != null && item.getStatus() != null && item.getStatus() == StatusType.ENABLE.val)
+ .map(AiParam::getTenantId)
+ .distinct()
+ .forEach(this::ensureTenantHasRemainingDefaultAfterRemove);
+ }
+
+ private void ensureTenantHasRemainingDefaultAfterRemove(Long tenantId) {
+ long defaultCount = this.count(new LambdaQueryWrapper<AiParam>()
+ .eq(AiParam::getTenantId, tenantId)
+ .eq(AiParam::getDeleted, 0)
+ .eq(AiParam::getStatus, StatusType.ENABLE.val));
+ if (defaultCount <= 1) {
+ throw new CoolException("榛樿 AI 鍙傛暟涓嶈兘鐩存帴鍒犻櫎锛岃鍏堝皢鍏朵粬閰嶇疆璁句负榛樿");
}
}
@@ -165,4 +306,14 @@
throw new CoolException("瑙f瀽鏍¢獙鏃堕棿澶辫触: " + e.getMessage());
}
}
+
+ private AiChatModelOptionDto toChatModelOption(AiParam aiParam) {
+ return AiChatModelOptionDto.builder()
+ .aiParamId(aiParam.getId())
+ .name(aiParam.getName())
+ .model(aiParam.getModel())
+ .providerType(aiParam.getProviderType())
+ .active(aiParam.getStatus() != null && aiParam.getStatus() == StatusType.ENABLE.val)
+ .build();
+ }
}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiPromptServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiPromptServiceImpl.java
index ba072da..c6d688f 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiPromptServiceImpl.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiPromptServiceImpl.java
@@ -18,6 +18,7 @@
public class AiPromptServiceImpl extends ServiceImpl<AiPromptMapper, AiPrompt> implements AiPromptService {
private final AiPromptRenderSupport aiPromptRenderSupport;
+ private final AiRedisSupport aiRedisSupport;
@Override
public AiPrompt getActivePrompt(String code, Long tenantId) {
@@ -71,6 +72,42 @@
);
}
+ @Override
+ public boolean save(AiPrompt entity) {
+ boolean saved = super.save(entity);
+ if (saved && entity != null && entity.getTenantId() != null) {
+ aiRedisSupport.evictTenantConfigCaches(entity.getTenantId());
+ }
+ return saved;
+ }
+
+ @Override
+ public boolean updateById(AiPrompt entity) {
+ boolean updated = super.updateById(entity);
+ if (updated && entity != null && entity.getTenantId() != null) {
+ aiRedisSupport.evictTenantConfigCaches(entity.getTenantId());
+ }
+ return updated;
+ }
+
+ @Override
+ public boolean removeByIds(java.util.Collection<?> list) {
+ java.util.List<java.io.Serializable> ids = list == null ? java.util.List.of() : list.stream()
+ .filter(java.util.Objects::nonNull)
+ .map(item -> (java.io.Serializable) item)
+ .toList();
+ java.util.List<AiPrompt> records = this.listByIds(ids);
+ boolean removed = super.removeByIds(list);
+ if (removed) {
+ records.stream()
+ .map(AiPrompt::getTenantId)
+ .filter(java.util.Objects::nonNull)
+ .distinct()
+ .forEach(aiRedisSupport::evictTenantConfigCaches);
+ }
+ return removed;
+ }
+
private void ensureRequiredFields(AiPrompt aiPrompt) {
if (!StringUtils.hasText(aiPrompt.getName())) {
throw new CoolException("Prompt 鍚嶇О涓嶈兘涓虹┖");
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiRedisSupport.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiRedisSupport.java
new file mode 100644
index 0000000..1a5c941
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiRedisSupport.java
@@ -0,0 +1,519 @@
+package com.vincent.rsf.server.ai.service.impl;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.vincent.rsf.server.ai.config.AiDefaults;
+import com.vincent.rsf.server.ai.dto.AiChatMemoryDto;
+import com.vincent.rsf.server.ai.dto.AiChatRuntimeDto;
+import com.vincent.rsf.server.ai.dto.AiChatSessionDto;
+import com.vincent.rsf.server.ai.dto.AiMcpConnectivityTestDto;
+import com.vincent.rsf.server.ai.dto.AiMcpToolPreviewDto;
+import com.vincent.rsf.server.ai.dto.AiObserveStatsDto;
+import com.vincent.rsf.server.ai.dto.AiResolvedConfig;
+import com.vincent.rsf.server.common.service.RedisService;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import redis.clients.jedis.Jedis;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.time.Instant;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AiRedisSupport {
+
+ /** 缁熶竴鏀跺彛 AI 妯″潡鐨� Redis key銆乀TL 鍜屽簭鍒楀寲绛栫暐锛岄伩鍏嶄笟鍔$被鐩存帴鏁e啓 Redis銆� */
+ private static final String CONFIG_KEY_PREFIX = "AI:CONFIG:";
+ private static final String RUNTIME_KEY_PREFIX = "AI:RUNTIME:";
+ private static final String MEMORY_KEY_PREFIX = "AI:MEMORY:";
+ private static final String SESSIONS_KEY_PREFIX = "AI:SESSIONS:";
+ private static final String MCP_PREVIEW_KEY_PREFIX = "AI:MCP:PREVIEW:";
+ private static final String MCP_HEALTH_KEY_PREFIX = "AI:MCP:HEALTH:";
+ private static final String STREAM_STATE_KEY_PREFIX = "AI:STREAM:";
+ private static final String TOOL_RESULT_KEY_PREFIX = "AI:TOOL:RESULT:";
+ private static final String RATE_LIMIT_KEY_PREFIX = "AI:RATE:";
+ private static final String OBSERVE_STATS_KEY_PREFIX = "AI:OBSERVE:STATS:";
+ private static final String OBSERVE_TOOL_RANK_KEY_PREFIX = "AI:OBSERVE:TOOL:RANK:";
+ private static final String OBSERVE_TOOL_FAIL_RANK_KEY_PREFIX = "AI:OBSERVE:TOOL:FAIL:RANK:";
+
+ private static final String FIELD_CALL_COUNT = "callCount";
+ private static final String FIELD_SUCCESS_COUNT = "successCount";
+ private static final String FIELD_FAILURE_COUNT = "failureCount";
+ private static final String FIELD_ELAPSED_SUM = "elapsedSum";
+ private static final String FIELD_ELAPSED_COUNT = "elapsedCount";
+ private static final String FIELD_FIRST_TOKEN_SUM = "firstTokenSum";
+ private static final String FIELD_FIRST_TOKEN_COUNT = "firstTokenCount";
+ private static final String FIELD_TOTAL_TOKENS_SUM = "totalTokensSum";
+ private static final String FIELD_TOTAL_TOKENS_COUNT = "totalTokensCount";
+ private static final String FIELD_TOOL_CALL_COUNT = "toolCallCount";
+ private static final String FIELD_TOOL_SUCCESS_COUNT = "toolSuccessCount";
+ private static final String FIELD_TOOL_FAILURE_COUNT = "toolFailureCount";
+
+ private final RedisService redisService;
+ private final ObjectMapper objectMapper;
+
+ public AiResolvedConfig getResolvedConfig(Long tenantId, String promptCode, Long aiParamId) {
+ return readJson(buildConfigKey(tenantId, promptCode, aiParamId), AiResolvedConfig.class);
+ }
+
+ public void cacheResolvedConfig(Long tenantId, String promptCode, Long aiParamId, AiResolvedConfig config) {
+ writeJson(buildConfigKey(tenantId, promptCode, aiParamId), config, AiDefaults.CONFIG_CACHE_TTL_SECONDS);
+ }
+
+ public void evictTenantConfigCaches(Long tenantId) {
+ deleteByPrefix(CONFIG_KEY_PREFIX + tenantId + ":");
+ deleteByPrefix(RUNTIME_KEY_PREFIX + tenantId + ":");
+ }
+
+ public AiChatRuntimeDto getRuntime(Long tenantId, Long userId, String promptCode, Long sessionId, Long aiParamId) {
+ return readJson(buildRuntimeKey(tenantId, userId, promptCode, sessionId, aiParamId), AiChatRuntimeDto.class);
+ }
+
+ public void cacheRuntime(Long tenantId, Long userId, String promptCode, Long sessionId, Long aiParamId, AiChatRuntimeDto runtime) {
+ writeJson(buildRuntimeKey(tenantId, userId, promptCode, sessionId, aiParamId), runtime, AiDefaults.RUNTIME_CACHE_TTL_SECONDS);
+ }
+
+ public AiChatMemoryDto getMemory(Long tenantId, Long userId, String promptCode, Long sessionId) {
+ return readJson(buildMemoryKey(tenantId, userId, promptCode, sessionId), AiChatMemoryDto.class);
+ }
+
+ public void cacheMemory(Long tenantId, Long userId, String promptCode, Long sessionId, AiChatMemoryDto memory) {
+ writeJson(buildMemoryKey(tenantId, userId, promptCode, sessionId), memory, AiDefaults.MEMORY_CACHE_TTL_SECONDS);
+ }
+
+ public List<AiChatSessionDto> getSessionList(Long tenantId, Long userId, String promptCode, String keyword) {
+ return readJson(buildSessionsKey(tenantId, userId, promptCode, keyword), new TypeReference<List<AiChatSessionDto>>() {
+ });
+ }
+
+ public void cacheSessionList(Long tenantId, Long userId, String promptCode, String keyword, List<AiChatSessionDto> sessions) {
+ writeJson(buildSessionsKey(tenantId, userId, promptCode, keyword), sessions, AiDefaults.SESSION_LIST_CACHE_TTL_SECONDS);
+ }
+
+ public void evictUserConversationCaches(Long tenantId, Long userId) {
+ deleteByPrefix(RUNTIME_KEY_PREFIX + tenantId + ":" + userId + ":");
+ deleteByPrefix(MEMORY_KEY_PREFIX + tenantId + ":" + userId + ":");
+ deleteByPrefix(SESSIONS_KEY_PREFIX + tenantId + ":" + userId + ":");
+ }
+
+ public List<AiMcpToolPreviewDto> getToolPreview(Long tenantId, Long mountId) {
+ return readJson(buildMcpPreviewKey(tenantId, mountId), new TypeReference<List<AiMcpToolPreviewDto>>() {
+ });
+ }
+
+ public void cacheToolPreview(Long tenantId, Long mountId, List<AiMcpToolPreviewDto> tools) {
+ writeJson(buildMcpPreviewKey(tenantId, mountId), tools, AiDefaults.MCP_PREVIEW_CACHE_TTL_SECONDS);
+ }
+
+ public AiMcpConnectivityTestDto getConnectivity(Long tenantId, Long mountId) {
+ return readJson(buildMcpHealthKey(tenantId, mountId), AiMcpConnectivityTestDto.class);
+ }
+
+ public void cacheConnectivity(Long tenantId, Long mountId, AiMcpConnectivityTestDto connectivity) {
+ writeJson(buildMcpHealthKey(tenantId, mountId), connectivity, AiDefaults.MCP_HEALTH_CACHE_TTL_SECONDS);
+ }
+
+ public void evictMcpMountCaches(Long tenantId, Long mountId) {
+ if (mountId != null) {
+ delete(buildMcpPreviewKey(tenantId, mountId));
+ delete(buildMcpHealthKey(tenantId, mountId));
+ } else {
+ deleteByPrefix(MCP_PREVIEW_KEY_PREFIX + tenantId + ":");
+ deleteByPrefix(MCP_HEALTH_KEY_PREFIX + tenantId + ":");
+ }
+ evictTenantConfigCaches(tenantId);
+ }
+
+ public boolean allowChatRequest(Long tenantId, Long userId, String promptCode) {
+ String key = buildRateLimitKey(tenantId, userId, promptCode);
+ long now = Instant.now().toEpochMilli();
+ long windowStart = now - (AiDefaults.CHAT_RATE_LIMIT_WINDOW_SECONDS * 1000L);
+ Boolean allowed = execute(jedis -> {
+ // 鐢� zset 缁存姢婊戝姩绐楀彛锛岃�屼笉鏄畝鍗曡鏁板櫒锛岄伩鍏嶇獥鍙h竟鐣屽嚭鐜扮獊鍒鸿鍒ゃ��
+ jedis.zremrangeByScore(key, 0, windowStart);
+ long count = jedis.zcard(key);
+ if (count >= AiDefaults.CHAT_RATE_LIMIT_MAX_REQUESTS) {
+ jedis.expire(key, AiDefaults.CHAT_RATE_LIMIT_WINDOW_SECONDS);
+ return Boolean.FALSE;
+ }
+ jedis.zadd(key, now, now + ":" + UUID.randomUUID());
+ jedis.expire(key, AiDefaults.CHAT_RATE_LIMIT_WINDOW_SECONDS);
+ return Boolean.TRUE;
+ });
+ return Boolean.TRUE.equals(allowed);
+ }
+
+ public void markStreamState(String requestId, Long tenantId, Long userId, Long sessionId, String promptCode,
+ String status, String errorMessage) {
+ if (!StringUtils.hasText(requestId)) {
+ return;
+ }
+ writeJson(buildStreamStateKey(tenantId, requestId), AiStreamState.builder()
+ .requestId(requestId)
+ .tenantId(tenantId)
+ .userId(userId)
+ .sessionId(sessionId)
+ .promptCode(promptCode)
+ .status(status)
+ .errorMessage(errorMessage)
+ .timestamp(Instant.now().toEpochMilli())
+ .build(), AiDefaults.STREAM_STATE_TTL_SECONDS);
+ }
+
+ public CachedToolResult getToolResult(Long tenantId, String requestId, String toolName, String toolInput) {
+ return readJson(buildToolResultKey(tenantId, requestId, toolName, toolInput), CachedToolResult.class);
+ }
+
+ public void cacheToolResult(Long tenantId, String requestId, String toolName, String toolInput,
+ boolean success, String output, String errorMessage) {
+ writeJson(buildToolResultKey(tenantId, requestId, toolName, toolInput), CachedToolResult.builder()
+ .success(success)
+ .output(output)
+ .errorMessage(errorMessage)
+ .build(), AiDefaults.TOOL_RESULT_CACHE_TTL_SECONDS);
+ }
+
+ public void recordObserveCallStarted(Long tenantId) {
+ executeVoid(jedis -> jedis.hincrBy(buildObserveStatsKey(tenantId), FIELD_CALL_COUNT, 1));
+ }
+
+ public void recordObserveCallFinished(Long tenantId, String status, Long elapsedMs, Long firstTokenLatencyMs, Integer totalTokens) {
+ executeVoid(jedis -> {
+ String key = buildObserveStatsKey(tenantId);
+ if ("COMPLETED".equals(status)) {
+ jedis.hincrBy(key, FIELD_SUCCESS_COUNT, 1);
+ } else if ("FAILED".equals(status)) {
+ jedis.hincrBy(key, FIELD_FAILURE_COUNT, 1);
+ }
+ if (elapsedMs != null) {
+ jedis.hincrBy(key, FIELD_ELAPSED_SUM, elapsedMs);
+ jedis.hincrBy(key, FIELD_ELAPSED_COUNT, 1);
+ }
+ if (firstTokenLatencyMs != null) {
+ jedis.hincrBy(key, FIELD_FIRST_TOKEN_SUM, firstTokenLatencyMs);
+ jedis.hincrBy(key, FIELD_FIRST_TOKEN_COUNT, 1);
+ }
+ if (totalTokens != null) {
+ jedis.hincrBy(key, FIELD_TOTAL_TOKENS_SUM, totalTokens.longValue());
+ jedis.hincrBy(key, FIELD_TOTAL_TOKENS_COUNT, 1);
+ }
+ });
+ }
+
+ public void recordObserveToolCall(Long tenantId, String toolName, String status) {
+ executeVoid(jedis -> {
+ String key = buildObserveStatsKey(tenantId);
+ jedis.hincrBy(key, FIELD_TOOL_CALL_COUNT, 1);
+ if ("COMPLETED".equals(status)) {
+ jedis.hincrBy(key, FIELD_TOOL_SUCCESS_COUNT, 1);
+ } else if ("FAILED".equals(status)) {
+ jedis.hincrBy(key, FIELD_TOOL_FAILURE_COUNT, 1);
+ }
+ if (StringUtils.hasText(toolName)) {
+ jedis.zincrby(buildToolRankKey(tenantId), 1D, toolName);
+ if ("FAILED".equals(status)) {
+ jedis.zincrby(buildToolFailRankKey(tenantId), 1D, toolName);
+ }
+ }
+ });
+ }
+
+ public AiObserveStatsDto getObserveStats(Long tenantId, Supplier<AiObserveStatsDto> fallbackLoader) {
+ AiObserveStatsDto cached = readObserveStats(tenantId);
+ if (cached != null) {
+ return cached;
+ }
+ // Redis 涓虹┖鏃跺啀鍥炴簮鏁版嵁搴擄紝閬垮厤绠$悊绔湅鏉挎瘡娆¢兘鎵叏閲忔棩蹇楄〃銆�
+ AiObserveStatsDto snapshot = fallbackLoader.get();
+ if (snapshot != null) {
+ seedObserveStats(tenantId, snapshot);
+ }
+ return snapshot;
+ }
+
+ private AiObserveStatsDto readObserveStats(Long tenantId) {
+ Map<String, String> fields = execute(jedis -> {
+ String key = buildObserveStatsKey(tenantId);
+ if (!jedis.exists(key)) {
+ return null;
+ }
+ return jedis.hgetAll(key);
+ });
+ if (fields == null || fields.isEmpty()) {
+ return null;
+ }
+ long callCount = parseLong(fields.get(FIELD_CALL_COUNT));
+ long successCount = parseLong(fields.get(FIELD_SUCCESS_COUNT));
+ long failureCount = parseLong(fields.get(FIELD_FAILURE_COUNT));
+ long elapsedSum = parseLong(fields.get(FIELD_ELAPSED_SUM));
+ long elapsedCount = parseLong(fields.get(FIELD_ELAPSED_COUNT));
+ long firstTokenSum = parseLong(fields.get(FIELD_FIRST_TOKEN_SUM));
+ long firstTokenCount = parseLong(fields.get(FIELD_FIRST_TOKEN_COUNT));
+ long totalTokensSum = parseLong(fields.get(FIELD_TOTAL_TOKENS_SUM));
+ long totalTokensCount = parseLong(fields.get(FIELD_TOTAL_TOKENS_COUNT));
+ long toolCallCount = parseLong(fields.get(FIELD_TOOL_CALL_COUNT));
+ long toolSuccessCount = parseLong(fields.get(FIELD_TOOL_SUCCESS_COUNT));
+ long toolFailureCount = parseLong(fields.get(FIELD_TOOL_FAILURE_COUNT));
+ return AiObserveStatsDto.builder()
+ .callCount(callCount)
+ .successCount(successCount)
+ .failureCount(failureCount)
+ .avgElapsedMs(elapsedCount == 0 ? 0L : elapsedSum / elapsedCount)
+ .avgFirstTokenLatencyMs(firstTokenCount == 0 ? 0L : firstTokenSum / firstTokenCount)
+ .totalTokens(totalTokensSum)
+ .avgTotalTokens(totalTokensCount == 0 ? 0L : totalTokensSum / totalTokensCount)
+ .toolCallCount(toolCallCount)
+ .toolSuccessCount(toolSuccessCount)
+ .toolFailureCount(toolFailureCount)
+ .toolSuccessRate(toolCallCount == 0 ? 0D : (toolSuccessCount * 100D) / toolCallCount)
+ .build();
+ }
+
+ private void seedObserveStats(Long tenantId, AiObserveStatsDto snapshot) {
+ executeVoid(jedis -> {
+ String key = buildObserveStatsKey(tenantId);
+ Map<String, String> values = new LinkedHashMap<>();
+ values.put(FIELD_CALL_COUNT, String.valueOf(defaultLong(snapshot.getCallCount())));
+ values.put(FIELD_SUCCESS_COUNT, String.valueOf(defaultLong(snapshot.getSuccessCount())));
+ values.put(FIELD_FAILURE_COUNT, String.valueOf(defaultLong(snapshot.getFailureCount())));
+ values.put(FIELD_ELAPSED_SUM, String.valueOf(defaultLong(snapshot.getAvgElapsedMs()) * defaultLong(snapshot.getCallCount())));
+ values.put(FIELD_ELAPSED_COUNT, String.valueOf(defaultLong(snapshot.getCallCount())));
+ values.put(FIELD_FIRST_TOKEN_SUM, String.valueOf(defaultLong(snapshot.getAvgFirstTokenLatencyMs()) * defaultLong(snapshot.getCallCount())));
+ values.put(FIELD_FIRST_TOKEN_COUNT, String.valueOf(defaultLong(snapshot.getCallCount())));
+ values.put(FIELD_TOTAL_TOKENS_SUM, String.valueOf(defaultLong(snapshot.getTotalTokens())));
+ values.put(FIELD_TOTAL_TOKENS_COUNT, String.valueOf(defaultLong(snapshot.getCallCount())));
+ values.put(FIELD_TOOL_CALL_COUNT, String.valueOf(defaultLong(snapshot.getToolCallCount())));
+ values.put(FIELD_TOOL_SUCCESS_COUNT, String.valueOf(defaultLong(snapshot.getToolSuccessCount())));
+ values.put(FIELD_TOOL_FAILURE_COUNT, String.valueOf(defaultLong(snapshot.getToolFailureCount())));
+ jedis.hset(key, values);
+ });
+ }
+
+ private String buildConfigKey(Long tenantId, String promptCode, Long aiParamId) {
+ return CONFIG_KEY_PREFIX + tenantId + ":" + safeToken(promptCode) + ":" + aiParamToken(aiParamId);
+ }
+
+ private String buildRuntimeKey(Long tenantId, Long userId, String promptCode, Long sessionId, Long aiParamId) {
+ return RUNTIME_KEY_PREFIX + tenantId + ":" + userId + ":" + safeToken(promptCode) + ":" + sessionToken(sessionId) + ":" + aiParamToken(aiParamId);
+ }
+
+ private String buildMemoryKey(Long tenantId, Long userId, String promptCode, Long sessionId) {
+ return MEMORY_KEY_PREFIX + tenantId + ":" + userId + ":" + safeToken(promptCode) + ":" + sessionToken(sessionId);
+ }
+
+ private String buildSessionsKey(Long tenantId, Long userId, String promptCode, String keyword) {
+ return SESSIONS_KEY_PREFIX + tenantId + ":" + userId + ":" + safeToken(promptCode) + ":" + safeToken(keyword);
+ }
+
+ private String buildMcpPreviewKey(Long tenantId, Long mountId) {
+ return MCP_PREVIEW_KEY_PREFIX + tenantId + ":" + mountId;
+ }
+
+ private String buildMcpHealthKey(Long tenantId, Long mountId) {
+ return MCP_HEALTH_KEY_PREFIX + tenantId + ":" + mountId;
+ }
+
+ private String buildStreamStateKey(Long tenantId, String requestId) {
+ return STREAM_STATE_KEY_PREFIX + tenantId + ":" + safeToken(requestId);
+ }
+
+ private String buildToolResultKey(Long tenantId, String requestId, String toolName, String toolInput) {
+ return TOOL_RESULT_KEY_PREFIX + tenantId + ":" + safeToken(requestId) + ":" + safeToken(toolName) + ":" + digest(toolInput);
+ }
+
+ private String buildRateLimitKey(Long tenantId, Long userId, String promptCode) {
+ return RATE_LIMIT_KEY_PREFIX + tenantId + ":" + userId + ":" + safeToken(promptCode);
+ }
+
+ private String buildObserveStatsKey(Long tenantId) {
+ return OBSERVE_STATS_KEY_PREFIX + tenantId;
+ }
+
+ private String buildToolRankKey(Long tenantId) {
+ return OBSERVE_TOOL_RANK_KEY_PREFIX + tenantId;
+ }
+
+ private String buildToolFailRankKey(Long tenantId) {
+ return OBSERVE_TOOL_FAIL_RANK_KEY_PREFIX + tenantId;
+ }
+
+ private String sessionToken(Long sessionId) {
+ return sessionId == null ? "LATEST" : String.valueOf(sessionId);
+ }
+
+ private String aiParamToken(Long aiParamId) {
+ return aiParamId == null ? "DEFAULT" : String.valueOf(aiParamId);
+ }
+
+ private String safeToken(String source) {
+ if (!StringUtils.hasText(source)) {
+ return "_";
+ }
+ return URLEncoder.encode(source.trim(), StandardCharsets.UTF_8);
+ }
+
+ private String digest(String source) {
+ try {
+ MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+ byte[] bytes = messageDigest.digest((source == null ? "" : source).getBytes(StandardCharsets.UTF_8));
+ StringBuilder builder = new StringBuilder();
+ for (byte value : bytes) {
+ builder.append(String.format("%02x", value));
+ }
+ return builder.toString();
+ } catch (Exception e) {
+ return safeToken(source);
+ }
+ }
+
+ private <T> T readJson(String key, Class<T> type) {
+ return readJson(key, value -> objectMapper.readValue(value, type));
+ }
+
+ private <T> T readJson(String key, TypeReference<T> typeReference) {
+ return readJson(key, value -> objectMapper.readValue(value, typeReference));
+ }
+
+ private <T> T readJson(String key, JsonReader<T> reader) {
+ return execute(jedis -> {
+ String value = jedis.get(key);
+ if (!StringUtils.hasText(value)) {
+ return null;
+ }
+ try {
+ return reader.read(value);
+ } catch (Exception e) {
+ // 鍙嶅簭鍒楀寲澶辫触鏃剁洿鎺ュ垹鍧忕紦瀛橈紝璁╀笅娆¤姹傝嚜鐒跺洖婧愰噸寤恒��
+ log.warn("AI redis cache deserialize failed, key={}, message={}", key, e.getMessage());
+ jedis.del(key);
+ return null;
+ }
+ });
+ }
+
+ private void writeJson(String key, Object value, int ttlSeconds) {
+ if (value == null) {
+ delete(key);
+ return;
+ }
+ executeVoid(jedis -> {
+ try {
+ jedis.setex(key, ttlSeconds, objectMapper.writeValueAsString(value));
+ } catch (Exception e) {
+ log.warn("AI redis cache serialize failed, key={}, message={}", key, e.getMessage());
+ }
+ });
+ }
+
+ private void delete(String key) {
+ executeVoid(jedis -> jedis.del(key));
+ }
+
+ private void deleteByPrefix(String prefix) {
+ executeVoid(jedis -> {
+ Set<String> keys = jedis.keys(prefix + "*");
+ if (keys == null || keys.isEmpty()) {
+ return;
+ }
+ jedis.del(keys.toArray(new String[0]));
+ });
+ }
+
+ private long parseLong(String source) {
+ if (!StringUtils.hasText(source)) {
+ return 0L;
+ }
+ try {
+ return Long.parseLong(source);
+ } catch (Exception e) {
+ return 0L;
+ }
+ }
+
+ private long defaultLong(Long value) {
+ return value == null ? 0L : value;
+ }
+
+ private <T> T execute(Function<Jedis, T> action) {
+ Jedis jedis = redisService.getJedis();
+ if (jedis == null) {
+ return null;
+ }
+ try (jedis) {
+ return action.apply(jedis);
+ } catch (Exception e) {
+ log.warn("AI redis operation skipped, message={}", e.getMessage());
+ return null;
+ }
+ }
+
+ private void executeVoid(Consumer<Jedis> action) {
+ Jedis jedis = redisService.getJedis();
+ if (jedis == null) {
+ return;
+ }
+ try (jedis) {
+ action.accept(jedis);
+ } catch (Exception e) {
+ log.warn("AI redis operation skipped, message={}", e.getMessage());
+ }
+ }
+
+ @FunctionalInterface
+ private interface JsonReader<T> {
+ T read(String value) throws Exception;
+ }
+
+ @Data
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class CachedToolResult {
+
+ private boolean success;
+
+ private String output;
+
+ private String errorMessage;
+ }
+
+ @Data
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ private static class AiStreamState {
+
+ private String requestId;
+
+ private Long tenantId;
+
+ private Long userId;
+
+ private Long sessionId;
+
+ private String promptCode;
+
+ private String status;
+
+ private String errorMessage;
+
+ private Long timestamp;
+ }
+}
diff --git a/rsf-server/src/main/resources/application-dev.yml b/rsf-server/src/main/resources/application-dev.yml
index 0f33198..b0df7ab 100644
--- a/rsf-server/src/main/resources/application-dev.yml
+++ b/rsf-server/src/main/resources/application-dev.yml
@@ -66,7 +66,6 @@
class: javax.net.ssl.SSLSocketFactory
redis:
host: 127.0.0.1
- password: xltys1995
port: 6379
timeout: 5000
index: 15
--
Gitblit v1.9.1