From 05148eeef860d33232874a640dbd67ba43ac5686 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期四, 19 三月 2026 12:51:49 +0800
Subject: [PATCH] #AI.配置中心可运营化

---
 rsf-admin/src/api/ai/mcpMount.js                                                              |    9 
 rsf-admin/src/page/system/aiShared/AiRuntimeSummary.jsx                                       |  104 +++++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiConfigOpsController.java      |   32 +
 rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiParam.java                        |   21 +
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiConfigOpsService.java            |   12 
 rsf-admin/src/page/system/aiParam/AiParamList.jsx                                             |    2 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiParamValidationSupport.java |  106 +++++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiParamController.java          |    7 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiPromptServiceImpl.java      |   23 +
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiMcpMountServiceImpl.java    |   42 ++
 rsf-admin/src/api/ai/configCenter.js                                                          |   28 +
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiParamValidateResultDto.java          |   19 +
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiPromptRenderSupport.java    |   81 ++++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiPromptPreviewRequest.java            |   25 +
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiParamServiceImpl.java       |   43 ++
 rsf-admin/src/page/system/aiPrompt/AiPromptForm.jsx                                           |  113 ++++++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiConfigSummaryDto.java                |   39 ++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiConfigOpsServiceImpl.java   |   57 +++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiMcpMountController.java       |    7 
 version/db/ai_feature.sql                                                                     |   72 ++++
 rsf-admin/src/page/system/aiMcpMount/AiMcpMountForm.jsx                                       |   67 +++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiPromptPreviewDto.java                |   17 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiParamService.java                |    3 
 rsf-admin/src/page/system/aiPrompt/AiPromptList.jsx                                           |    2 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptService.java               |    4 
 rsf-admin/src/page/system/aiMcpMount/AiMcpMountList.jsx                                       |    2 
 rsf-admin/src/page/system/aiParam/AiParamForm.jsx                                             |   81 ++++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiPromptController.java         |    7 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiMcpMountService.java             |    2 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java                     |    3 
 30 files changed, 1,024 insertions(+), 6 deletions(-)

diff --git a/rsf-admin/src/api/ai/configCenter.js b/rsf-admin/src/api/ai/configCenter.js
new file mode 100644
index 0000000..a916647
--- /dev/null
+++ b/rsf-admin/src/api/ai/configCenter.js
@@ -0,0 +1,28 @@
+import request from "@/utils/request";
+
+export const getAiConfigSummary = async (promptCode = "home.default") => {
+    const res = await request.get("ai/config/summary", { params: { promptCode } });
+    const { code, msg, data } = res.data;
+    if (code === 200) {
+        return data;
+    }
+    throw new Error(msg || "鑾峰彇 AI 杩愯鎬佹憳瑕佸け璐�");
+};
+
+export const validateAiParamDraft = async (payload) => {
+    const res = await request.post("aiParam/validate-draft", payload);
+    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;
+    if (code === 200) {
+        return data;
+    }
+    throw new Error(msg || "Prompt 棰勮澶辫触");
+};
diff --git a/rsf-admin/src/api/ai/mcpMount.js b/rsf-admin/src/api/ai/mcpMount.js
index bd8a831..fd16af3 100644
--- a/rsf-admin/src/api/ai/mcpMount.js
+++ b/rsf-admin/src/api/ai/mcpMount.js
@@ -18,6 +18,15 @@
     throw new Error(msg || "杩為�氭�ф祴璇曞け璐�");
 };
 
+export const validateDraftMcpConnectivity = async (payload) => {
+    const res = await request.post("aiMcpMount/connectivity/validate-draft", payload);
+    const { code, msg, data } = res.data;
+    if (code === 200) {
+        return data;
+    }
+    throw new Error(msg || "鑽夌杩為�氭�ф祴璇曞け璐�");
+};
+
 export const testMcpTool = async (mountId, payload) => {
     const res = await request.post(`aiMcpMount/${mountId}/tool/test`, payload);
     const { code, msg, data } = res.data;
diff --git a/rsf-admin/src/page/system/aiMcpMount/AiMcpMountForm.jsx b/rsf-admin/src/page/system/aiMcpMount/AiMcpMountForm.jsx
index b05ac16..7e1deee 100644
--- a/rsf-admin/src/page/system/aiMcpMount/AiMcpMountForm.jsx
+++ b/rsf-admin/src/page/system/aiMcpMount/AiMcpMountForm.jsx
@@ -1,18 +1,72 @@
-import React from "react";
+import React, { useState } from "react";
 import {
     FormDataConsumer,
     NumberInput,
     SelectInput,
     TextInput,
+    useNotify,
 } from "react-admin";
-import { Grid, Typography } from "@mui/material";
+import { Alert, Button, Grid, Stack, Typography } from "@mui/material";
 import StatusSelectInput from "@/page/components/StatusSelectInput";
+import { validateDraftMcpConnectivity } from "@/api/ai/mcpMount";
 
 const transportChoices = [
     { id: "SSE_HTTP", name: "SSE_HTTP" },
     { id: "STDIO", name: "STDIO" },
     { id: "BUILTIN", name: "BUILTIN" },
 ];
+
+const AiMcpDraftTestSection = ({ formData, readOnly }) => {
+    const notify = useNotify();
+    const [loading, setLoading] = useState(false);
+    const [result, setResult] = useState(null);
+
+    const handleValidate = async () => {
+        setLoading(true);
+        try {
+            const data = await validateDraftMcpConnectivity(formData);
+            setResult(data);
+            notify(data?.message || "鑽夌杩為�氭�ф祴璇曞畬鎴�");
+        } catch (error) {
+            const nextResult = {
+                healthStatus: "UNHEALTHY",
+                message: error?.message || "鑽夌杩為�氭�ф祴璇曞け璐�",
+            };
+            setResult(nextResult);
+            notify(nextResult.message, { type: "error" });
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    if (readOnly) {
+        return null;
+    }
+
+    return (
+        <>
+            <Grid item xs={12}>
+                <Stack direction="row" spacing={1} alignItems="center">
+                    <Button variant="outlined" onClick={handleValidate} disabled={loading}>
+                        {loading ? "娴嬭瘯涓�..." : "淇濆瓨鍓嶆祴璇�"}
+                    </Button>
+                    <Typography variant="body2" color="text.secondary">
+                        鐢ㄥ綋鍓嶈崏绋块厤缃洿鎺ユ牎楠岃繛閫氭�э紝涓嶄細钀藉簱銆�
+                    </Typography>
+                </Stack>
+            </Grid>
+            {result && (
+                <Grid item xs={12}>
+                    <Alert severity={result.healthStatus === "HEALTHY" ? "success" : "error"}>
+                        {result.message}
+                        {result.initElapsedMs ? ` 路 ${result.initElapsedMs} ms` : ""}
+                        {result.testedAt ? ` 路 ${result.testedAt}` : ""}
+                    </Alert>
+                </Grid>
+            )}
+        </>
+    );
+};
 
 const AiMcpMountForm = ({ readOnly = false }) => (
     <Grid container spacing={2} width={{ xs: "100%", xl: "80%" }}>
@@ -87,6 +141,9 @@
         <Grid item xs={12}>
             <TextInput source="memo" label="澶囨敞" fullWidth multiline minRows={3} disabled={readOnly} />
         </Grid>
+        <FormDataConsumer>
+            {({ formData }) => <AiMcpDraftTestSection formData={formData} readOnly={readOnly} />}
+        </FormDataConsumer>
         <Grid item xs={12}>
             <Typography variant="h6">杩愯鎬佷俊鎭�</Typography>
         </Grid>
@@ -102,6 +159,12 @@
         <Grid item xs={12}>
             <TextInput source="lastTestMessage" label="鏈�杩戞祴璇曠粨鏋�" fullWidth multiline minRows={3} disabled />
         </Grid>
+        <Grid item xs={12} md={6}>
+            <TextInput source="updateBy" label="鏈�杩戞洿鏂颁汉" fullWidth disabled />
+        </Grid>
+        <Grid item xs={12} md={6}>
+            <TextInput source="updateTime$" label="鏈�杩戞洿鏂版椂闂�" fullWidth disabled />
+        </Grid>
     </Grid>
 );
 
diff --git a/rsf-admin/src/page/system/aiMcpMount/AiMcpMountList.jsx b/rsf-admin/src/page/system/aiMcpMount/AiMcpMountList.jsx
index 9601238..16c5e85 100644
--- a/rsf-admin/src/page/system/aiMcpMount/AiMcpMountList.jsx
+++ b/rsf-admin/src/page/system/aiMcpMount/AiMcpMountList.jsx
@@ -33,6 +33,7 @@
 import AiConfigDialog from "../aiShared/AiConfigDialog";
 import AiMcpMountToolsPanel from "./AiMcpMountToolsPanel";
 import { testMcpConnectivity } from "@/api/ai/mcpMount";
+import AiRuntimeSummary from "../aiShared/AiRuntimeSummary";
 
 const filters = [
     <SearchInput source="condition" alwaysOn />,
@@ -298,6 +299,7 @@
                     </TopToolbar>
                 )}
             >
+                <AiRuntimeSummary />
                 <AiMcpMountCards
                     onView={(id) => openDialog("show", id)}
                     onEdit={(id) => openDialog("edit", id)}
diff --git a/rsf-admin/src/page/system/aiParam/AiParamForm.jsx b/rsf-admin/src/page/system/aiParam/AiParamForm.jsx
index 07fbd25..09eface 100644
--- a/rsf-admin/src/page/system/aiParam/AiParamForm.jsx
+++ b/rsf-admin/src/page/system/aiParam/AiParamForm.jsx
@@ -1,16 +1,69 @@
-import React from "react";
+import React, { useState } from "react";
 import {
     BooleanInput,
+    FormDataConsumer,
     NumberInput,
     SelectInput,
     TextInput,
+    useNotify,
 } from "react-admin";
-import { Grid, Typography } from "@mui/material";
+import { Alert, Button, Grid, Stack, Typography } from "@mui/material";
 import StatusSelectInput from "@/page/components/StatusSelectInput";
+import { validateAiParamDraft } from "@/api/ai/configCenter";
 
 const providerChoices = [
     { id: "OPENAI_COMPATIBLE", name: "OPENAI_COMPATIBLE" },
 ];
+
+const AiParamValidateSection = ({ formData, readOnly }) => {
+    const notify = useNotify();
+    const [loading, setLoading] = useState(false);
+    const [result, setResult] = useState(null);
+
+    const handleValidate = async () => {
+        setLoading(true);
+        try {
+            const data = await validateAiParamDraft(formData);
+            setResult(data);
+            notify(data?.message || "AI 鍙傛暟楠岃瘉鎴愬姛");
+        } catch (error) {
+            const nextResult = {
+                status: "INVALID",
+                message: error?.message || "AI 鍙傛暟楠岃瘉澶辫触",
+            };
+            setResult(nextResult);
+            notify(nextResult.message, { type: "error" });
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    return (
+        <>
+            {!readOnly && (
+                <Grid item xs={12}>
+                    <Stack direction="row" spacing={1} alignItems="center">
+                        <Button variant="outlined" onClick={handleValidate} disabled={loading}>
+                            {loading ? "楠岃瘉涓�..." : "淇濆瓨鍓嶉獙璇�"}
+                        </Button>
+                        <Typography variant="body2" color="text.secondary">
+                            浼氱洿鎺ユ牎楠屽綋鍓� Base URL銆丄PI Key 涓庢ā鍨嬫槸鍚﹀彲璋冪敤銆�
+                        </Typography>
+                    </Stack>
+                </Grid>
+            )}
+            {result && (
+                <Grid item xs={12}>
+                    <Alert severity={result.status === "VALID" ? "success" : "error"}>
+                        {result.message}
+                        {result.elapsedMs ? ` 路 ${result.elapsedMs} ms` : ""}
+                        {result.validatedAt ? ` 路 ${result.validatedAt}` : ""}
+                    </Alert>
+                </Grid>
+            )}
+        </>
+    );
+};
 
 const AiParamForm = ({ readOnly = false }) => (
     <Grid container spacing={2} width={{ xs: "100%", xl: "80%" }}>
@@ -53,6 +106,30 @@
         <Grid item xs={12}>
             <TextInput source="memo" label="澶囨敞" fullWidth multiline minRows={3} disabled={readOnly} />
         </Grid>
+        <FormDataConsumer>
+            {({ formData }) => <AiParamValidateSection formData={formData} readOnly={readOnly} />}
+        </FormDataConsumer>
+        <Grid item xs={12}>
+            <Typography variant="h6">杩愯涓庡璁′俊鎭�</Typography>
+        </Grid>
+        <Grid item xs={12} md={3}>
+            <TextInput source="validateStatus" label="鏈�杩戞牎楠岀姸鎬�" fullWidth disabled />
+        </Grid>
+        <Grid item xs={12} md={3}>
+            <TextInput source="lastValidateElapsedMs" label="鏈�杩戞牎楠岃�楁椂(ms)" fullWidth disabled />
+        </Grid>
+        <Grid item xs={12} md={3}>
+            <TextInput source="lastValidateTime$" label="鏈�杩戞牎楠屾椂闂�" fullWidth disabled />
+        </Grid>
+        <Grid item xs={12} md={3}>
+            <TextInput source="updateBy" label="鏈�杩戞洿鏂颁汉" fullWidth disabled />
+        </Grid>
+        <Grid item xs={12}>
+            <TextInput source="lastValidateMessage" label="鏈�杩戞牎楠岀粨鏋�" fullWidth multiline minRows={3} disabled />
+        </Grid>
+        <Grid item xs={12}>
+            <TextInput source="updateTime$" label="鏈�杩戞洿鏂版椂闂�" fullWidth disabled />
+        </Grid>
     </Grid>
 );
 
diff --git a/rsf-admin/src/page/system/aiParam/AiParamList.jsx b/rsf-admin/src/page/system/aiParam/AiParamList.jsx
index f7d167f..225fbd9 100644
--- a/rsf-admin/src/page/system/aiParam/AiParamList.jsx
+++ b/rsf-admin/src/page/system/aiParam/AiParamList.jsx
@@ -31,6 +31,7 @@
 import MyExportButton from "@/page/components/MyExportButton";
 import AiParamForm from "./AiParamForm";
 import AiConfigDialog from "../aiShared/AiConfigDialog";
+import AiRuntimeSummary from "../aiShared/AiRuntimeSummary";
 
 const filters = [
     <SearchInput source="condition" alwaysOn />,
@@ -232,6 +233,7 @@
                     </TopToolbar>
                 )}
             >
+                <AiRuntimeSummary />
                 <AiParamCards
                     onView={(id) => openDialog("show", id)}
                     onEdit={(id) => openDialog("edit", id)}
diff --git a/rsf-admin/src/page/system/aiPrompt/AiPromptForm.jsx b/rsf-admin/src/page/system/aiPrompt/AiPromptForm.jsx
index 5920bd5..5c6ad35 100644
--- a/rsf-admin/src/page/system/aiPrompt/AiPromptForm.jsx
+++ b/rsf-admin/src/page/system/aiPrompt/AiPromptForm.jsx
@@ -1,9 +1,106 @@
-import React from "react";
+import React, { useState } from "react";
 import {
+    FormDataConsumer,
     TextInput,
+    useNotify,
 } from "react-admin";
-import { Grid, Typography } from "@mui/material";
+import { Alert, Button, Grid, Stack, TextField, Typography } from "@mui/material";
 import StatusSelectInput from "@/page/components/StatusSelectInput";
+import { renderAiPromptPreview } from "@/api/ai/configCenter";
+
+const AiPromptPreviewSection = ({ formData }) => {
+    const notify = useNotify();
+    const [input, setInput] = useState("璇峰府鎴戞�荤粨褰撳墠椤甸潰鑳藉仛浠�涔�");
+    const [metadataText, setMetadataText] = useState("{\"path\":\"/system/aiPrompt\"}");
+    const [preview, setPreview] = useState(null);
+    const [loading, setLoading] = useState(false);
+
+    const handlePreview = async () => {
+        setLoading(true);
+        try {
+            const metadata = metadataText ? JSON.parse(metadataText) : {};
+            const data = await renderAiPromptPreview({
+                ...formData,
+                input,
+                metadata,
+            });
+            setPreview(data);
+            notify("Prompt 棰勮瀹屾垚");
+        } catch (error) {
+            setPreview(null);
+            notify(error?.message || "Prompt 棰勮澶辫触", { type: "error" });
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    return (
+        <>
+            <Grid item xs={12}>
+                <Typography variant="h6">Prompt 棰勮</Typography>
+            </Grid>
+            <Grid item xs={12}>
+                <TextField
+                    label="绀轰緥杈撳叆"
+                    value={input}
+                    onChange={(event) => setInput(event.target.value)}
+                    fullWidth
+                    multiline
+                    minRows={3}
+                />
+            </Grid>
+            <Grid item xs={12}>
+                <TextField
+                    label="绀轰緥鍏冩暟鎹� JSON"
+                    value={metadataText}
+                    onChange={(event) => setMetadataText(event.target.value)}
+                    fullWidth
+                    multiline
+                    minRows={3}
+                />
+            </Grid>
+            <Grid item xs={12}>
+                <Stack direction="row" spacing={1} alignItems="center">
+                    <Button variant="outlined" onClick={handlePreview} disabled={loading}>
+                        {loading ? "棰勮涓�..." : "棰勮娓叉煋"}
+                    </Button>
+                    <Typography variant="body2" color="text.secondary">
+                        鐢ㄥ綋鍓嶈〃鍗曞唴瀹规覆鏌� System Prompt 鍜� User Prompt銆�
+                    </Typography>
+                </Stack>
+            </Grid>
+            {preview && (
+                <>
+                    <Grid item xs={12}>
+                        <Alert severity="success">
+                            宸茶В鏋愬彉閲忥細{(preview.resolvedVariables || []).join(", ") || "鏃�"}
+                        </Alert>
+                    </Grid>
+                    <Grid item xs={12}>
+                        <TextField
+                            label="娓叉煋鍚庣殑 System Prompt"
+                            value={preview.renderedSystemPrompt || ""}
+                            fullWidth
+                            multiline
+                            minRows={5}
+                            InputProps={{ readOnly: true }}
+                        />
+                    </Grid>
+                    <Grid item xs={12}>
+                        <TextField
+                            label="娓叉煋鍚庣殑 User Prompt"
+                            value={preview.renderedUserPrompt || ""}
+                            fullWidth
+                            multiline
+                            minRows={5}
+                            InputProps={{ readOnly: true }}
+                        />
+                    </Grid>
+                </>
+            )}
+        </>
+    );
+};
 
 const AiPromptForm = ({ readOnly = false }) => (
     <Grid container spacing={2} width={{ xs: "100%", xl: "80%" }}>
@@ -31,6 +128,18 @@
         <Grid item xs={12}>
             <TextInput source="memo" label="澶囨敞" fullWidth multiline minRows={3} disabled={readOnly} />
         </Grid>
+        <FormDataConsumer>
+            {({ formData }) => <AiPromptPreviewSection formData={formData} />}
+        </FormDataConsumer>
+        <Grid item xs={12}>
+            <Typography variant="h6">杩愯涓庡璁′俊鎭�</Typography>
+        </Grid>
+        <Grid item xs={12} md={6}>
+            <TextInput source="updateBy" label="鏈�杩戞洿鏂颁汉" fullWidth disabled />
+        </Grid>
+        <Grid item xs={12} md={6}>
+            <TextInput source="updateTime$" label="鏈�杩戞洿鏂版椂闂�" fullWidth disabled />
+        </Grid>
     </Grid>
 );
 
diff --git a/rsf-admin/src/page/system/aiPrompt/AiPromptList.jsx b/rsf-admin/src/page/system/aiPrompt/AiPromptList.jsx
index 325b6dc..236fa02 100644
--- a/rsf-admin/src/page/system/aiPrompt/AiPromptList.jsx
+++ b/rsf-admin/src/page/system/aiPrompt/AiPromptList.jsx
@@ -31,6 +31,7 @@
 import MyExportButton from "@/page/components/MyExportButton";
 import AiPromptForm from "./AiPromptForm";
 import AiConfigDialog from "../aiShared/AiConfigDialog";
+import AiRuntimeSummary from "../aiShared/AiRuntimeSummary";
 
 const filters = [
     <SearchInput source="condition" alwaysOn />,
@@ -203,6 +204,7 @@
                     </TopToolbar>
                 )}
             >
+                <AiRuntimeSummary />
                 <AiPromptCards
                     onView={(id) => openDialog("show", id)}
                     onEdit={(id) => openDialog("edit", id)}
diff --git a/rsf-admin/src/page/system/aiShared/AiRuntimeSummary.jsx b/rsf-admin/src/page/system/aiShared/AiRuntimeSummary.jsx
new file mode 100644
index 0000000..bf0a840
--- /dev/null
+++ b/rsf-admin/src/page/system/aiShared/AiRuntimeSummary.jsx
@@ -0,0 +1,104 @@
+import React, { useEffect, useState } from "react";
+import { Alert, Box, Card, CardContent, Chip, CircularProgress, Grid, Stack, Typography } from "@mui/material";
+import { getAiConfigSummary } from "@/api/ai/configCenter";
+
+const AiRuntimeSummary = ({ promptCode = "home.default" }) => {
+    const [summary, setSummary] = useState(null);
+    const [loading, setLoading] = useState(true);
+    const [error, setError] = useState("");
+
+    useEffect(() => {
+        let active = true;
+        setLoading(true);
+        setError("");
+        getAiConfigSummary(promptCode)
+            .then((data) => {
+                if (!active) {
+                    return;
+                }
+                setSummary(data);
+            })
+            .catch((err) => {
+                if (!active) {
+                    return;
+                }
+                setError(err?.message || "鑾峰彇杩愯鎬佹憳瑕佸け璐�");
+            })
+            .finally(() => {
+                if (active) {
+                    setLoading(false);
+                }
+            });
+        return () => {
+            active = false;
+        };
+    }, [promptCode]);
+
+    return (
+        <Box px={2} pt={2}>
+            <Card
+                variant="outlined"
+                sx={{
+                    borderRadius: 3,
+                    boxShadow: "0 8px 24px rgba(15, 23, 42, 0.06)",
+                }}
+            >
+                <CardContent>
+                    <Stack direction="row" justifyContent="space-between" alignItems="center" mb={2}>
+                        <Box>
+                            <Typography variant="h6">褰撳墠杩愯鎬�</Typography>
+                            <Typography variant="body2" color="text.secondary">
+                                灞曠ず褰撳墠鐢熸晥鐨勬ā鍨嬨�丳rompt 涓� MCP 鎸傝浇淇℃伅銆�
+                            </Typography>
+                        </Box>
+                        {loading && <CircularProgress size={24} />}
+                    </Stack>
+                    {error && <Alert severity="error">{error}</Alert>}
+                    {!loading && !error && summary && (
+                        <Grid container spacing={2}>
+                            <Grid item xs={12} md={4}>
+                                <Typography variant="caption" color="text.secondary">褰撳墠妯″瀷</Typography>
+                                <Typography variant="body1">{summary.activeModel || "--"}</Typography>
+                                <Typography variant="body2" color="text.secondary">
+                                    {summary.activeParamName || "--"}
+                                </Typography>
+                                <Stack direction="row" spacing={1} mt={1} flexWrap="wrap" useFlexGap>
+                                    <Chip size="small" label={`鏍¢獙 ${summary.activeParamValidateStatus || "--"}`} />
+                                    <Chip size="small" variant="outlined" label={summary.activeParamValidatedAt || "鏈牎楠�"} />
+                                </Stack>
+                            </Grid>
+                            <Grid item xs={12} md={4}>
+                                <Typography variant="caption" color="text.secondary">褰撳墠 Prompt</Typography>
+                                <Typography variant="body1">{summary.promptName || "--"}</Typography>
+                                <Typography variant="body2" color="text.secondary">
+                                    {summary.promptCode || "--"} / {summary.promptScene || "--"}
+                                </Typography>
+                                <Typography variant="body2" color="text.secondary" mt={1}>
+                                    鏈�杩戞洿鏂帮細{summary.activePromptUpdatedAt || "--"} / {summary.activePromptUpdatedBy || "--"}
+                                </Typography>
+                            </Grid>
+                            <Grid item xs={12} md={4}>
+                                <Typography variant="caption" color="text.secondary">宸插惎鐢� MCP</Typography>
+                                <Typography variant="body1">{summary.enabledMcpCount ?? 0} 涓�</Typography>
+                                <Stack direction="row" spacing={1} mt={1} flexWrap="wrap" useFlexGap>
+                                    {(summary.enabledMcpNames || []).map((name) => (
+                                        <Chip key={name} size="small" variant="outlined" label={name} />
+                                    ))}
+                                </Stack>
+                            </Grid>
+                            {summary.activeParamValidateMessage && (
+                                <Grid item xs={12}>
+                                    <Alert severity={summary.activeParamValidateStatus === "VALID" ? "success" : "warning"}>
+                                        {summary.activeParamValidateMessage}
+                                    </Alert>
+                                </Grid>
+                            )}
+                        </Grid>
+                    )}
+                </CardContent>
+            </Card>
+        </Box>
+    );
+};
+
+export default AiRuntimeSummary;
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 fe09412..487a7a8 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
@@ -17,6 +17,9 @@
     public static final String MCP_HEALTH_NOT_TESTED = "NOT_TESTED";
     public static final String MCP_HEALTH_HEALTHY = "HEALTHY";
     public static final String MCP_HEALTH_UNHEALTHY = "UNHEALTHY";
+    public static final String PARAM_VALIDATE_NOT_TESTED = "NOT_TESTED";
+    public static final String PARAM_VALIDATE_VALID = "VALID";
+    public static final String PARAM_VALIDATE_INVALID = "INVALID";
     public static final long SSE_TIMEOUT_MS = 0L;
     public static final int DEFAULT_TIMEOUT_MS = 60000;
     public static final double DEFAULT_TEMPERATURE = 0.7D;
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiConfigOpsController.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiConfigOpsController.java
new file mode 100644
index 0000000..af68f61
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiConfigOpsController.java
@@ -0,0 +1,32 @@
+package com.vincent.rsf.server.ai.controller;
+
+import com.vincent.rsf.framework.common.R;
+import com.vincent.rsf.server.ai.dto.AiPromptPreviewRequest;
+import com.vincent.rsf.server.ai.service.AiConfigOpsService;
+import com.vincent.rsf.server.system.controller.BaseController;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequiredArgsConstructor
+public class AiConfigOpsController extends BaseController {
+
+    private final AiConfigOpsService aiConfigOpsService;
+
+    @PreAuthorize("hasAnyAuthority('system:aiParam:list','system:aiPrompt:list','system:aiMcpMount:list')")
+    @GetMapping("/ai/config/summary")
+    public R getSummary(@RequestParam(value = "promptCode", required = false) String promptCode) {
+        return R.ok().add(aiConfigOpsService.getSummary(promptCode, getTenantId()));
+    }
+
+    @PreAuthorize("hasAnyAuthority('system:aiPrompt:list','system:aiPrompt:save','system:aiPrompt:update')")
+    @PostMapping("/ai/config/prompt/render-preview")
+    public R renderPromptPreview(@RequestBody AiPromptPreviewRequest request) {
+        return R.ok().add(aiConfigOpsService.renderPromptPreview(request, getTenantId()));
+    }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiMcpMountController.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiMcpMountController.java
index 90ba38f..c37681b 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiMcpMountController.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiMcpMountController.java
@@ -76,6 +76,13 @@
         return R.ok().add(aiMcpMountService.testConnectivity(id, getLoginUserId(), getTenantId()));
     }
 
+    @PreAuthorize("hasAnyAuthority('system:aiMcpMount:save','system:aiMcpMount:update')")
+    @PostMapping("/aiMcpMount/connectivity/validate-draft")
+    public R testDraftConnectivity(@RequestBody AiMcpMount mount) {
+        mount.setTenantId(getTenantId());
+        return R.ok().add(aiMcpMountService.testDraftConnectivity(mount, getLoginUserId(), getTenantId()));
+    }
+
     @PreAuthorize("hasAuthority('system:aiMcpMount:update')")
     @PostMapping("/aiMcpMount/{id}/tool/test")
     public R testTool(@PathVariable("id") Long id, @RequestBody AiMcpToolTestRequest request) {
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 874c315..d58ac65 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
@@ -63,6 +63,13 @@
                 .last("limit 1")));
     }
 
+    @PreAuthorize("hasAnyAuthority('system:aiParam:save','system:aiParam:update')")
+    @PostMapping("/aiParam/validate-draft")
+    public R validateDraft(@RequestBody AiParam aiParam) {
+        aiParam.setTenantId(getTenantId());
+        return R.ok().add(aiParamService.validateDraft(aiParam, getTenantId()));
+    }
+
     @PreAuthorize("hasAuthority('system:aiParam:save')")
     @OperationLog("Create AiParam")
     @PostMapping("/aiParam/save")
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiPromptController.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiPromptController.java
index d35f879..4bb2d07 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiPromptController.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiPromptController.java
@@ -2,6 +2,7 @@
 
 import com.vincent.rsf.framework.common.R;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.vincent.rsf.server.ai.dto.AiPromptPreviewRequest;
 import com.vincent.rsf.server.ai.entity.AiPrompt;
 import com.vincent.rsf.server.ai.service.AiPromptService;
 import com.vincent.rsf.server.common.annotation.OperationLog;
@@ -63,6 +64,12 @@
                 .last("limit 1")));
     }
 
+    @PreAuthorize("hasAnyAuthority('system:aiPrompt:list','system:aiPrompt:save','system:aiPrompt:update')")
+    @PostMapping("/aiPrompt/render-preview")
+    public R renderPreview(@RequestBody AiPromptPreviewRequest request) {
+        return R.ok().add(aiPromptService.renderPreview(request, getTenantId()));
+    }
+
     @PreAuthorize("hasAuthority('system:aiPrompt:save')")
     @OperationLog("Create AiPrompt")
     @PostMapping("/aiPrompt/save")
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiConfigSummaryDto.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiConfigSummaryDto.java
new file mode 100644
index 0000000..cf5a9f1
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiConfigSummaryDto.java
@@ -0,0 +1,39 @@
+package com.vincent.rsf.server.ai.dto;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@Builder
+public class AiConfigSummaryDto {
+
+    private String promptCode;
+
+    private String promptName;
+
+    private String promptScene;
+
+    private String activeParamName;
+
+    private String activeModel;
+
+    private String activeParamUpdatedAt;
+
+    private Long activeParamUpdatedBy;
+
+    private String activeParamValidateStatus;
+
+    private String activeParamValidateMessage;
+
+    private String activeParamValidatedAt;
+
+    private Integer enabledMcpCount;
+
+    private List<String> enabledMcpNames;
+
+    private String activePromptUpdatedAt;
+
+    private Long activePromptUpdatedBy;
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiParamValidateResultDto.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiParamValidateResultDto.java
new file mode 100644
index 0000000..9de7f91
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiParamValidateResultDto.java
@@ -0,0 +1,19 @@
+package com.vincent.rsf.server.ai.dto;
+
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
+public class AiParamValidateResultDto {
+
+    private String status;
+
+    private String message;
+
+    private String model;
+
+    private Long elapsedMs;
+
+    private String validatedAt;
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiPromptPreviewDto.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiPromptPreviewDto.java
new file mode 100644
index 0000000..0ce8b68
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiPromptPreviewDto.java
@@ -0,0 +1,17 @@
+package com.vincent.rsf.server.ai.dto;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@Builder
+public class AiPromptPreviewDto {
+
+    private String renderedSystemPrompt;
+
+    private String renderedUserPrompt;
+
+    private List<String> resolvedVariables;
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiPromptPreviewRequest.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiPromptPreviewRequest.java
new file mode 100644
index 0000000..aa9c6a2
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiPromptPreviewRequest.java
@@ -0,0 +1,25 @@
+package com.vincent.rsf.server.ai.dto;
+
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public class AiPromptPreviewRequest {
+
+    private Long id;
+
+    private String name;
+
+    private String code;
+
+    private String scene;
+
+    private String systemPrompt;
+
+    private String userPromptTemplate;
+
+    private String input;
+
+    private Map<String, Object> metadata;
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiParam.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiParam.java
index 49ee4cc..c2e2850 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiParam.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiParam.java
@@ -54,6 +54,20 @@
     @ApiModelProperty(value = "streamingEnabled")
     private Boolean streamingEnabled;
 
+    @ApiModelProperty(value = "鏈�杩戞牎楠岀姸鎬�")
+    private String validateStatus;
+
+    @ApiModelProperty(value = "鏈�杩戞牎楠屼俊鎭�")
+    private String lastValidateMessage;
+
+    @ApiModelProperty(value = "鏈�杩戞牎楠岃�楁椂")
+    private Long lastValidateElapsedMs;
+
+    @ApiModelProperty(value = "鏈�杩戞牎楠屾椂闂�")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date lastValidateTime;
+
     @ApiModelProperty(value = "鐘舵��")
     private Integer status;
 
@@ -106,4 +120,11 @@
         }
         return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.updateTime);
     }
+
+    public String getLastValidateTime$() {
+        if (this.lastValidateTime == null) {
+            return "";
+        }
+        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.lastValidateTime);
+    }
 }
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiConfigOpsService.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiConfigOpsService.java
new file mode 100644
index 0000000..5f780e7
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiConfigOpsService.java
@@ -0,0 +1,12 @@
+package com.vincent.rsf.server.ai.service;
+
+import com.vincent.rsf.server.ai.dto.AiConfigSummaryDto;
+import com.vincent.rsf.server.ai.dto.AiPromptPreviewDto;
+import com.vincent.rsf.server.ai.dto.AiPromptPreviewRequest;
+
+public interface AiConfigOpsService {
+
+    AiConfigSummaryDto getSummary(String promptCode, Long tenantId);
+
+    AiPromptPreviewDto renderPromptPreview(AiPromptPreviewRequest request, Long tenantId);
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiMcpMountService.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiMcpMountService.java
index 1f9f5bb..6f2f832 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiMcpMountService.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiMcpMountService.java
@@ -21,5 +21,7 @@
 
     AiMcpConnectivityTestDto testConnectivity(Long mountId, Long userId, Long tenantId);
 
+    AiMcpConnectivityTestDto testDraftConnectivity(AiMcpMount mount, Long userId, Long tenantId);
+
     AiMcpToolTestDto testTool(Long mountId, Long userId, Long tenantId, AiMcpToolTestRequest request);
 }
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 b92b9b9..7612957 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,6 +1,7 @@
 package com.vincent.rsf.server.ai.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.vincent.rsf.server.ai.dto.AiParamValidateResultDto;
 import com.vincent.rsf.server.ai.entity.AiParam;
 
 public interface AiParamService extends IService<AiParam> {
@@ -10,4 +11,6 @@
     void validateBeforeSave(AiParam aiParam, Long tenantId);
 
     void validateBeforeUpdate(AiParam aiParam, Long tenantId);
+
+    AiParamValidateResultDto validateDraft(AiParam aiParam, Long tenantId);
 }
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptService.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptService.java
index ef449dc..4a839bf 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptService.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptService.java
@@ -1,6 +1,8 @@
 package com.vincent.rsf.server.ai.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.vincent.rsf.server.ai.dto.AiPromptPreviewDto;
+import com.vincent.rsf.server.ai.dto.AiPromptPreviewRequest;
 import com.vincent.rsf.server.ai.entity.AiPrompt;
 
 public interface AiPromptService extends IService<AiPrompt> {
@@ -10,4 +12,6 @@
     void validateBeforeSave(AiPrompt aiPrompt, Long tenantId);
 
     void validateBeforeUpdate(AiPrompt aiPrompt, Long tenantId);
+
+    AiPromptPreviewDto renderPreview(AiPromptPreviewRequest request, Long tenantId);
 }
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
new file mode 100644
index 0000000..78b7183
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiConfigOpsServiceImpl.java
@@ -0,0 +1,57 @@
+package com.vincent.rsf.server.ai.service.impl;
+
+import com.vincent.rsf.server.ai.config.AiDefaults;
+import com.vincent.rsf.server.ai.dto.AiConfigSummaryDto;
+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.service.AiConfigOpsService;
+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;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class AiConfigOpsServiceImpl implements AiConfigOpsService {
+
+    private final AiParamService aiParamService;
+    private final AiPromptService aiPromptService;
+    private final AiMcpMountService aiMcpMountService;
+    private final AiPromptRenderSupport aiPromptRenderSupport;
+
+    @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);
+        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$())
+                .enabledMcpCount(mounts.size())
+                .enabledMcpNames(mounts.stream().map(AiMcpMount::getName).toList())
+                .activePromptUpdatedAt(activePrompt.getUpdateTime$())
+                .activePromptUpdatedBy(activePrompt.getUpdateBy())
+                .build();
+    }
+
+    @Override
+    public AiPromptPreviewDto renderPromptPreview(AiPromptPreviewRequest request, Long tenantId) {
+        return aiPromptService.renderPreview(request, tenantId);
+    }
+}
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 87eb622..3ef9423 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
@@ -121,6 +121,48 @@
     }
 
     @Override
+    public AiMcpConnectivityTestDto testDraftConnectivity(AiMcpMount mount, Long userId, Long tenantId) {
+        ensureTenantId(tenantId);
+        if (userId == null) {
+            throw new CoolException("褰撳墠鐧诲綍鐢ㄦ埛涓嶅瓨鍦�");
+        }
+        if (mount == null) {
+            throw new CoolException("MCP 鎸傝浇鍙傛暟涓嶈兘涓虹┖");
+        }
+        mount.setTenantId(tenantId);
+        fillDefaults(mount);
+        ensureRequiredFields(mount, tenantId);
+        long startedAt = System.currentTimeMillis();
+        try (McpMountRuntimeFactory.McpMountRuntime runtime = mcpMountRuntimeFactory.create(List.of(mount), userId)) {
+            long elapsedMs = System.currentTimeMillis() - startedAt;
+            if (!runtime.getErrors().isEmpty()) {
+                return AiMcpConnectivityTestDto.builder()
+                        .mountId(mount.getId())
+                        .mountName(mount.getName())
+                        .healthStatus(AiDefaults.MCP_HEALTH_UNHEALTHY)
+                        .message(String.join("锛�", runtime.getErrors()))
+                        .initElapsedMs(elapsedMs)
+                        .toolCount(runtime.getToolCallbacks().length)
+                        .testedAt(new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()))
+                        .build();
+            }
+            return AiMcpConnectivityTestDto.builder()
+                    .mountId(mount.getId())
+                    .mountName(mount.getName())
+                    .healthStatus(AiDefaults.MCP_HEALTH_HEALTHY)
+                    .message("鑽夌杩為�氭�ф祴璇曟垚鍔燂紝瑙f瀽鍑� " + runtime.getToolCallbacks().length + " 涓伐鍏�")
+                    .initElapsedMs(elapsedMs)
+                    .toolCount(runtime.getToolCallbacks().length)
+                    .testedAt(new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()))
+                    .build();
+        } catch (CoolException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new CoolException("鑽夌杩為�氭�ф祴璇曞け璐�: " + e.getMessage());
+        }
+    }
+
+    @Override
     public AiMcpToolTestDto testTool(Long mountId, Long userId, Long tenantId, AiMcpToolTestRequest request) {
         if (userId == null) {
             throw new CoolException("褰撳墠鐧诲綍鐢ㄦ埛涓嶅瓨鍦�");
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 07715d5..957c7b0 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,15 +4,23 @@
 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.AiParamValidateResultDto;
 import com.vincent.rsf.server.ai.entity.AiParam;
 import com.vincent.rsf.server.ai.mapper.AiParamMapper;
 import com.vincent.rsf.server.ai.service.AiParamService;
 import com.vincent.rsf.server.system.enums.StatusType;
+import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
 @Service("aiParamService")
+@RequiredArgsConstructor
 public class AiParamServiceImpl extends ServiceImpl<AiParamMapper, AiParam> implements AiParamService {
+
+    private final AiParamValidationSupport aiParamValidationSupport;
 
     @Override
     public AiParam getActiveParam(Long tenantId) {
@@ -35,6 +43,7 @@
         fillDefaults(aiParam);
         ensureBaseFields(aiParam);
         ensureSingleActive(tenantId, null, aiParam.getStatus());
+        applyValidation(aiParam);
     }
 
     @Override
@@ -48,6 +57,15 @@
         aiParam.setTenantId(current.getTenantId());
         ensureBaseFields(aiParam);
         ensureSingleActive(tenantId, aiParam.getId(), aiParam.getStatus());
+        applyValidation(aiParam);
+    }
+
+    @Override
+    public AiParamValidateResultDto validateDraft(AiParam aiParam, Long tenantId) {
+        ensureTenantId(tenantId);
+        fillDefaults(aiParam);
+        ensureBaseFields(aiParam);
+        return aiParamValidationSupport.validate(aiParam);
     }
 
     private void ensureBaseFields(AiParam aiParam) {
@@ -118,8 +136,33 @@
         if (aiParam.getStreamingEnabled() == null) {
             aiParam.setStreamingEnabled(Boolean.TRUE);
         }
+        if (!StringUtils.hasText(aiParam.getValidateStatus())) {
+            aiParam.setValidateStatus(AiDefaults.PARAM_VALIDATE_NOT_TESTED);
+        }
         if (aiParam.getStatus() == null) {
             aiParam.setStatus(StatusType.ENABLE.val);
         }
     }
+
+    private void applyValidation(AiParam aiParam) {
+        AiParamValidateResultDto validateResult = aiParamValidationSupport.validate(aiParam);
+        aiParam.setValidateStatus(validateResult.getStatus());
+        aiParam.setLastValidateMessage(validateResult.getMessage());
+        aiParam.setLastValidateElapsedMs(validateResult.getElapsedMs());
+        aiParam.setLastValidateTime(parseDate(validateResult.getValidatedAt()));
+        if (!AiDefaults.PARAM_VALIDATE_VALID.equals(validateResult.getStatus())) {
+            throw new CoolException(validateResult.getMessage());
+        }
+    }
+
+    private Date parseDate(String dateTime) {
+        if (!StringUtils.hasText(dateTime)) {
+            return null;
+        }
+        try {
+            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateTime);
+        } catch (Exception e) {
+            throw new CoolException("瑙f瀽鏍¢獙鏃堕棿澶辫触: " + e.getMessage());
+        }
+    }
 }
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiParamValidationSupport.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiParamValidationSupport.java
new file mode 100644
index 0000000..9312751
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiParamValidationSupport.java
@@ -0,0 +1,106 @@
+package com.vincent.rsf.server.ai.service.impl;
+
+import com.vincent.rsf.framework.exception.CoolException;
+import com.vincent.rsf.server.ai.config.AiDefaults;
+import com.vincent.rsf.server.ai.dto.AiParamValidateResultDto;
+import com.vincent.rsf.server.ai.entity.AiParam;
+import io.micrometer.observation.ObservationRegistry;
+import lombok.RequiredArgsConstructor;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.model.tool.DefaultToolCallingManager;
+import org.springframework.ai.model.tool.ToolCallingManager;
+import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.ai.openai.OpenAiChatOptions;
+import org.springframework.ai.openai.api.OpenAiApi;
+import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
+import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver;
+import org.springframework.ai.util.json.schema.SchemaType;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class AiParamValidationSupport {
+
+    private final GenericApplicationContext applicationContext;
+    private final ObservationRegistry observationRegistry;
+
+    public AiParamValidateResultDto validate(AiParam aiParam) {
+        long startedAt = System.currentTimeMillis();
+        try {
+            OpenAiChatModel chatModel = createChatModel(aiParam);
+            ChatResponse response = chatModel.call(new Prompt(List.of(new UserMessage("璇峰洖澶� OK"))));
+            if (response == null || response.getResult() == null || response.getResult().getOutput() == null
+                    || !StringUtils.hasText(response.getResult().getOutput().getText())) {
+                throw new CoolException("妯″瀷宸茶繛鎺ワ紝浣嗘湭杩斿洖鏈夋晥鍝嶅簲");
+            }
+            long elapsedMs = System.currentTimeMillis() - startedAt;
+            return AiParamValidateResultDto.builder()
+                    .status(AiDefaults.PARAM_VALIDATE_VALID)
+                    .message("妯″瀷杩為�氭垚鍔�")
+                    .model(aiParam.getModel())
+                    .elapsedMs(elapsedMs)
+                    .validatedAt(formatDate(new Date()))
+                    .build();
+        } catch (Exception e) {
+            long elapsedMs = System.currentTimeMillis() - startedAt;
+            String message = e instanceof CoolException ? e.getMessage() : "妯″瀷楠岃瘉澶辫触: " + e.getMessage();
+            return AiParamValidateResultDto.builder()
+                    .status(AiDefaults.PARAM_VALIDATE_INVALID)
+                    .message(message)
+                    .model(aiParam.getModel())
+                    .elapsedMs(elapsedMs)
+                    .validatedAt(formatDate(new Date()))
+                    .build();
+        }
+    }
+
+    private OpenAiChatModel createChatModel(AiParam aiParam) {
+        OpenAiApi openAiApi = buildOpenAiApi(aiParam);
+        ToolCallingManager toolCallingManager = DefaultToolCallingManager.builder()
+                .observationRegistry(observationRegistry)
+                .toolCallbackResolver(new SpringBeanToolCallbackResolver(applicationContext, SchemaType.OPEN_API_SCHEMA))
+                .toolExecutionExceptionProcessor(new DefaultToolExecutionExceptionProcessor(false))
+                .build();
+        return new OpenAiChatModel(
+                openAiApi,
+                OpenAiChatOptions.builder()
+                        .model(aiParam.getModel())
+                        .temperature(aiParam.getTemperature())
+                        .topP(aiParam.getTopP())
+                        .maxTokens(aiParam.getMaxTokens())
+                        .streamUsage(true)
+                        .build(),
+                toolCallingManager,
+                org.springframework.retry.support.RetryTemplate.builder().maxAttempts(1).build(),
+                observationRegistry
+        );
+    }
+
+    private OpenAiApi buildOpenAiApi(AiParam aiParam) {
+        int timeoutMs = aiParam.getTimeoutMs() == null ? AiDefaults.DEFAULT_TIMEOUT_MS : aiParam.getTimeoutMs();
+        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
+        requestFactory.setConnectTimeout(timeoutMs);
+        requestFactory.setReadTimeout(timeoutMs);
+        return OpenAiApi.builder()
+                .baseUrl(aiParam.getBaseUrl())
+                .apiKey(aiParam.getApiKey())
+                .restClientBuilder(RestClient.builder().requestFactory(requestFactory))
+                .webClientBuilder(WebClient.builder())
+                .build();
+    }
+
+    private String formatDate(Date date) {
+        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
+    }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiPromptRenderSupport.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiPromptRenderSupport.java
new file mode 100644
index 0000000..0251428
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiPromptRenderSupport.java
@@ -0,0 +1,81 @@
+package com.vincent.rsf.server.ai.service.impl;
+
+import com.vincent.rsf.server.ai.dto.AiPromptPreviewDto;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Component
+public class AiPromptRenderSupport {
+
+    private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{\\{?([a-zA-Z0-9_.-]+)}}?");
+
+    public AiPromptPreviewDto render(String systemPrompt, String userPromptTemplate, String input, Map<String, Object> metadata) {
+        String finalInput = input == null ? "" : input;
+        return AiPromptPreviewDto.builder()
+                .renderedSystemPrompt(renderTemplate(systemPrompt, finalInput, metadata))
+                .renderedUserPrompt(renderUserPrompt(userPromptTemplate, finalInput, metadata))
+                .resolvedVariables(resolveVariables(systemPrompt, userPromptTemplate, metadata))
+                .build();
+    }
+
+    public String renderUserPrompt(String userPromptTemplate, String input, Map<String, Object> metadata) {
+        if (!StringUtils.hasText(userPromptTemplate)) {
+            return input;
+        }
+        String rendered = replaceTemplateVariables(userPromptTemplate, input, metadata);
+        if (Objects.equals(rendered, userPromptTemplate)) {
+            return userPromptTemplate + "\n\n" + input;
+        }
+        return rendered;
+    }
+
+    private String renderTemplate(String template, String input, Map<String, Object> metadata) {
+        if (!StringUtils.hasText(template)) {
+            return template;
+        }
+        return replaceTemplateVariables(template, input, metadata);
+    }
+
+    private String replaceTemplateVariables(String template, String input, Map<String, Object> metadata) {
+        String rendered = template
+                .replace("{{input}}", input)
+                .replace("{input}", input);
+        if (metadata == null || metadata.isEmpty()) {
+            return rendered;
+        }
+        for (Map.Entry<String, Object> entry : metadata.entrySet()) {
+            String value = entry.getValue() == null ? "" : String.valueOf(entry.getValue());
+            rendered = rendered.replace("{{" + entry.getKey() + "}}", value);
+            rendered = rendered.replace("{" + entry.getKey() + "}", value);
+        }
+        return rendered;
+    }
+
+    private List<String> resolveVariables(String systemPrompt, String userPromptTemplate, Map<String, Object> metadata) {
+        LinkedHashSet<String> variables = new LinkedHashSet<>();
+        collectVariables(variables, systemPrompt);
+        collectVariables(variables, userPromptTemplate);
+        if (metadata != null && !metadata.isEmpty()) {
+            variables.addAll(metadata.keySet());
+        }
+        return new ArrayList<>(variables);
+    }
+
+    private void collectVariables(LinkedHashSet<String> variables, String template) {
+        if (!StringUtils.hasText(template)) {
+            return;
+        }
+        Matcher matcher = VARIABLE_PATTERN.matcher(template);
+        while (matcher.find()) {
+            variables.add(matcher.group(1));
+        }
+    }
+}
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 b969e4e..ba072da 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
@@ -3,15 +3,21 @@
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.vincent.rsf.framework.exception.CoolException;
+import com.vincent.rsf.server.ai.dto.AiPromptPreviewDto;
+import com.vincent.rsf.server.ai.dto.AiPromptPreviewRequest;
 import com.vincent.rsf.server.ai.entity.AiPrompt;
 import com.vincent.rsf.server.ai.mapper.AiPromptMapper;
 import com.vincent.rsf.server.ai.service.AiPromptService;
 import com.vincent.rsf.server.system.enums.StatusType;
+import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 
 @Service("aiPromptService")
+@RequiredArgsConstructor
 public class AiPromptServiceImpl extends ServiceImpl<AiPromptMapper, AiPrompt> implements AiPromptService {
+
+    private final AiPromptRenderSupport aiPromptRenderSupport;
 
     @Override
     public AiPrompt getActivePrompt(String code, Long tenantId) {
@@ -48,6 +54,23 @@
         ensureUniqueCode(aiPrompt.getCode(), tenantId, aiPrompt.getId());
     }
 
+    @Override
+    public AiPromptPreviewDto renderPreview(AiPromptPreviewRequest request, Long tenantId) {
+        ensureTenantId(tenantId);
+        if (request == null) {
+            throw new CoolException("Prompt 棰勮鍙傛暟涓嶈兘涓虹┖");
+        }
+        if (!StringUtils.hasText(request.getSystemPrompt())) {
+            throw new CoolException("绯荤粺 Prompt 涓嶈兘涓虹┖");
+        }
+        return aiPromptRenderSupport.render(
+                request.getSystemPrompt(),
+                request.getUserPromptTemplate(),
+                request.getInput(),
+                request.getMetadata()
+        );
+    }
+
     private void ensureRequiredFields(AiPrompt aiPrompt) {
         if (!StringUtils.hasText(aiPrompt.getName())) {
             throw new CoolException("Prompt 鍚嶇О涓嶈兘涓虹┖");
diff --git a/version/db/ai_feature.sql b/version/db/ai_feature.sql
index 35b3112..8ca9631 100644
--- a/version/db/ai_feature.sql
+++ b/version/db/ai_feature.sql
@@ -13,6 +13,10 @@
   `max_tokens` int(11) DEFAULT NULL COMMENT '鏈�澶oken',
   `timeout_ms` int(11) DEFAULT NULL COMMENT '瓒呮椂鏃堕棿',
   `streaming_enabled` tinyint(1) DEFAULT '1' COMMENT '鏄惁鍚敤娴佸紡鍝嶅簲',
+  `validate_status` varchar(32) DEFAULT 'NOT_TESTED' COMMENT '鏈�杩戞牎楠岀姸鎬�',
+  `last_validate_message` varchar(500) DEFAULT NULL COMMENT '鏈�杩戞牎楠屼俊鎭�',
+  `last_validate_elapsed_ms` bigint(20) DEFAULT NULL COMMENT '鏈�杩戞牎楠岃�楁椂',
+  `last_validate_time` datetime DEFAULT NULL COMMENT '鏈�杩戞牎楠屾椂闂�',
   `tenant_id` bigint(20) DEFAULT NULL COMMENT '绉熸埛',
   `status` int(11) DEFAULT '1' COMMENT '鐘舵��',
   `deleted` int(11) DEFAULT '0' COMMENT '鍒犻櫎鏍囪',
@@ -109,6 +113,74 @@
   KEY `idx_sys_ai_chat_message_session_seq` (`session_id`,`seq_no`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 瀵硅瘽娑堟伅';
 
+SET @ai_param_validate_status_exists := (
+  SELECT COUNT(1)
+  FROM `information_schema`.`COLUMNS`
+  WHERE `TABLE_SCHEMA` = DATABASE()
+    AND `TABLE_NAME` = 'sys_ai_param'
+    AND `COLUMN_NAME` = 'validate_status'
+);
+SET @ai_param_validate_status_sql := IF(
+  @ai_param_validate_status_exists = 0,
+  'ALTER TABLE `sys_ai_param` ADD COLUMN `validate_status` varchar(32) DEFAULT ''NOT_TESTED'' COMMENT ''鏈�杩戞牎楠岀姸鎬�'' AFTER `streaming_enabled`',
+  'SELECT 1'
+);
+PREPARE ai_param_validate_status_stmt FROM @ai_param_validate_status_sql;
+EXECUTE ai_param_validate_status_stmt;
+DEALLOCATE PREPARE ai_param_validate_status_stmt;
+
+SET @ai_param_last_validate_message_exists := (
+  SELECT COUNT(1)
+  FROM `information_schema`.`COLUMNS`
+  WHERE `TABLE_SCHEMA` = DATABASE()
+    AND `TABLE_NAME` = 'sys_ai_param'
+    AND `COLUMN_NAME` = 'last_validate_message'
+);
+SET @ai_param_last_validate_message_sql := IF(
+  @ai_param_last_validate_message_exists = 0,
+  'ALTER TABLE `sys_ai_param` ADD COLUMN `last_validate_message` varchar(500) DEFAULT NULL COMMENT ''鏈�杩戞牎楠屼俊鎭�'' AFTER `validate_status`',
+  'SELECT 1'
+);
+PREPARE ai_param_last_validate_message_stmt FROM @ai_param_last_validate_message_sql;
+EXECUTE ai_param_last_validate_message_stmt;
+DEALLOCATE PREPARE ai_param_last_validate_message_stmt;
+
+SET @ai_param_last_validate_elapsed_exists := (
+  SELECT COUNT(1)
+  FROM `information_schema`.`COLUMNS`
+  WHERE `TABLE_SCHEMA` = DATABASE()
+    AND `TABLE_NAME` = 'sys_ai_param'
+    AND `COLUMN_NAME` = 'last_validate_elapsed_ms'
+);
+SET @ai_param_last_validate_elapsed_sql := IF(
+  @ai_param_last_validate_elapsed_exists = 0,
+  'ALTER TABLE `sys_ai_param` ADD COLUMN `last_validate_elapsed_ms` bigint(20) DEFAULT NULL COMMENT ''鏈�杩戞牎楠岃�楁椂'' AFTER `last_validate_message`',
+  'SELECT 1'
+);
+PREPARE ai_param_last_validate_elapsed_stmt FROM @ai_param_last_validate_elapsed_sql;
+EXECUTE ai_param_last_validate_elapsed_stmt;
+DEALLOCATE PREPARE ai_param_last_validate_elapsed_stmt;
+
+SET @ai_param_last_validate_time_exists := (
+  SELECT COUNT(1)
+  FROM `information_schema`.`COLUMNS`
+  WHERE `TABLE_SCHEMA` = DATABASE()
+    AND `TABLE_NAME` = 'sys_ai_param'
+    AND `COLUMN_NAME` = 'last_validate_time'
+);
+SET @ai_param_last_validate_time_sql := IF(
+  @ai_param_last_validate_time_exists = 0,
+  'ALTER TABLE `sys_ai_param` ADD COLUMN `last_validate_time` datetime DEFAULT NULL COMMENT ''鏈�杩戞牎楠屾椂闂�'' AFTER `last_validate_elapsed_ms`',
+  'SELECT 1'
+);
+PREPARE ai_param_last_validate_time_stmt FROM @ai_param_last_validate_time_sql;
+EXECUTE ai_param_last_validate_time_stmt;
+DEALLOCATE PREPARE ai_param_last_validate_time_stmt;
+
+UPDATE `sys_ai_param`
+SET `validate_status` = 'NOT_TESTED'
+WHERE `validate_status` IS NULL OR `validate_status` = '';
+
 SET @builtin_code_exists := (
   SELECT COUNT(1)
   FROM `information_schema`.`COLUMNS`

--
Gitblit v1.9.1