From 4954d3978cf1967729a5a2d5b90f6baef18974da Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期一, 23 三月 2026 09:35:10 +0800
Subject: [PATCH] #ai redis+页面优化

---
 rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiAsyncConfig.java                     |   13 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiConfigResolverServiceImpl.java |   17 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java     |   97 +
 rsf-admin/src/api/ai/chat.js                                                                     |    4 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRuntimeDto.java                     |    4 
 rsf-admin/src/page/system/aiParam/AiParamList.jsx                                                |   40 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiParamController.java             |    7 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatModelOptionDto.java                 |   19 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiPromptServiceImpl.java         |   37 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiMcpMountServiceImpl.java       |   53 
 rsf-admin/src/api/ai/configCenter.js                                                             |    9 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRequest.java                        |    2 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java           |  151 ++
 rsf-admin/src/i18n/zh.js                                                                         |   17 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java              |    9 
 rsf-admin/src/i18n/en.js                                                                         |   15 
 rsf-server/src/main/resources/application-dev.yml                                                |    1 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiParamServiceImpl.java          |  151 ++
 rsf-admin/package.json                                                                           |    8 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiConfigOpsServiceImpl.java      |   39 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiRedisSupport.java              |  519 +++++++++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiConfigResolverService.java          |    2 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java                    |    2 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiCallLogServiceImpl.java        |   16 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiParamService.java                   |    9 
 rsf-admin/package-lock.json                                                                      | 1647 ++++++++++++++++++++++++++++++
 rsf-admin/src/page/system/aiParam/AiParamForm.jsx                                                |   12 
 rsf-admin/src/layout/AiChatDrawer.jsx                                                            |  278 +++++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java                        |   10 
 29 files changed, 3,087 insertions(+), 101 deletions(-)

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

--
Gitblit v1.9.1