zhou zhou
2 天以前 4954d3978cf1967729a5a2d5b90f6baef18974da
#ai redis+页面优化
2个文件已添加
27个文件已修改
3188 ■■■■■ 已修改文件
rsf-admin/package-lock.json 1647 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/package.json 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/api/ai/chat.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/api/ai/configCenter.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/en.js 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/zh.js 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/layout/AiChatDrawer.jsx 278 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiParam/AiParamForm.jsx 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiParam/AiParamList.jsx 40 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiAsyncConfig.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiParamController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatModelOptionDto.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRequest.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRuntimeDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiConfigResolverService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiParamService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiCallLogServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java 151 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiConfigOpsServiceImpl.java 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiConfigResolverServiceImpl.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiMcpMountServiceImpl.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiParamServiceImpl.java 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiPromptServiceImpl.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiRedisSupport.java 519 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/resources/application-dev.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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"
      }
    }
  }
}
}
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"
}
}
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) {
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;
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}",
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}",
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>
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} />
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>
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;
    }
}
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;
}
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());
    }
}
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}")
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatModelOptionDto.java
New file
@@ -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;
}
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;
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;
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);
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);
}
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);
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 冷启动兜底,正常情况下看板应直接消费实时计数。
        List<AiCallLog> callLogs = this.list(new LambdaQueryWrapper<AiCallLog>()
                .eq(AiCallLog::getTenantId, tenantId)
                .eq(AiCallLog::getDeleted, 0)
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);
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 场景解析成一份可直接执行的 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 配置解析失败" : 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(),
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();
    }
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()
        // 配置解析是多个入口共享的热点路径,命中缓存时可以避免三张配置表的重复查询。
        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;
    }
}
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,
                    "工具解析成功,共 " + 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 = "连通性测试成功,解析出 " + 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())) {
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("解析校验时间失败: " + 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();
    }
}
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 名称不能为空");
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiRedisSupport.java
New file
@@ -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、TTL 和序列化策略,避免业务类直接散写 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 维护滑动窗口,而不是简单计数器,避免窗口边界出现突刺误判。
            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;
    }
}
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