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