From e6369c9b64a82eeada6b6f0658d2ae786787e101 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期四, 12 三月 2026 10:23:32 +0800
Subject: [PATCH] #AI

---
 rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/controller/AiGatewayController.java |   42 
 rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiParamMapper.java            |   12 
 version/db/20260311_ai_param_menu.sql                                                       |  224 +++
 rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/dto/GatewayChatMessage.java         |   14 
 rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/config/AiGatewayProperties.java     |   34 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatStreamRequest.java             |   16 
 rsf-server/skills/rsf-server-maintainer/scripts/locate_module.py                            |  129 +
 rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiProperties.java                 |   47 
 rsf-ai-gateway/pom.xml                                                                      |   33 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiSessionRenameRequest.java          |   12 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiPromptContext.java               |   21 
 rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/service/GatewayStreamEvent.java     |   20 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiChatMessage.java                 |   25 
 rsf-server/skills/rsf-server-maintainer/SKILL.md                                            |   98 +
 rsf-admin/src/i18n/en.js                                                                    |   14 
 rsf-admin/src/page/ResourceContent.js                                                       |    5 
 rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/service/AiGatewayService.java       |  267 +++
 version/db/20260311_ai_param.sql                                                            |  270 +++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/GatewayChatMessage.java              |   14 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiChatSession.java                 |   27 
 rsf-admin/src/page/system/aiParam/AiParamEdit.jsx                                           |  136 +
 rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiController.java             |  260 +++
 rsf-admin/src/page/system/aiParam/index.jsx                                                 |   15 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/GatewayChatRequest.java              |   26 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiSessionService.java            |   34 
 rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/GatewayBoot.java                    |   18 
 rsf-ai-gateway/src/main/resources/application.yml                                           |   23 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiSessionServiceImpl.java   |  221 ++
 rsf-server/skills/rsf-server-maintainer/agents/openai.yaml                                  |    4 
 rsf-server/skills/rsf-server-maintainer/references/repo-map.md                              |   69 
 rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiParamServiceImpl.java |   66 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiSessionCreateRequest.java          |   14 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiRuntimeConfigService.java      |  119 +
 rsf-ai-gateway/gateway-run.log                                                              |   60 
 pom.xml                                                                                     |    1 
 rsf-admin/src/page/system/aiParam/AiParamList.jsx                                           |  120 +
 rsf-admin/src/api/ai/index.js                                                               |   43 
 rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiParam.java                  |  169 ++
 rsf-server/src/main/resources/mapper/system/AiParamMapper.xml                               |    5 
 rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiParamService.java          |   19 
 rsf-server/src/main/resources/application.yml                                               |   15 
 rsf-admin/src/i18n/zh.js                                                                    |   14 
 rsf-server/src/main/resources/application-dev.yml                                           |    5 
 version/db/init.sql                                                                         |   50 
 rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/dto/GatewayChatRequest.java         |   26 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiGatewayClient.java             |   68 
 rsf-admin/src/page/system/aiParam/AiParamCreate.jsx                                         |  175 ++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptContextProvider.java     |   10 
 rsf-admin/src/layout/AppBarToolbar.jsx                                                      |   16 
 rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiParamController.java    |  138 +
 rsf-server/src/main/resources/application-prod.yml                                          |    5 
 rsf-admin/src/ai/AiChatWidget.jsx                                                           |  755 ++++++++++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiWarehouseSummaryService.java   |  215 ++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptContextService.java      |   37 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiTaskSummaryService.java        |  136 +
 55 files changed, 4,408 insertions(+), 3 deletions(-)

diff --git a/pom.xml b/pom.xml
index f8d401c..828541a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,6 +21,7 @@
         <module>rsf-framework</module>
         <module>rsf-server</module>
         <module>rsf-open-api</module>
+        <module>rsf-ai-gateway</module>
     </modules>
 
     <properties>
diff --git a/rsf-admin/src/ai/AiChatWidget.jsx b/rsf-admin/src/ai/AiChatWidget.jsx
new file mode 100644
index 0000000..9c713e7
--- /dev/null
+++ b/rsf-admin/src/ai/AiChatWidget.jsx
@@ -0,0 +1,755 @@
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import {
+    Alert,
+    Avatar,
+    Box,
+    Button,
+    Chip,
+    CircularProgress,
+    Divider,
+    Drawer,
+    Fab,
+    IconButton,
+    List,
+    ListItemButton,
+    ListItemText,
+    MenuItem,
+    Select,
+    Stack,
+    TextField,
+    Tooltip,
+    Typography
+} from '@mui/material';
+import AddIcon from '@mui/icons-material/Add';
+import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline';
+import CloseIcon from '@mui/icons-material/Close';
+import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
+import SendIcon from '@mui/icons-material/Send';
+import StopCircleOutlinedIcon from '@mui/icons-material/StopCircleOutlined';
+import { createAiSession, getAiMessages, getAiModels, getAiSessions, removeAiSession, stopAiChat } from '@/api/ai';
+import { PREFIX_BASE_URL, TOKEN_HEADER_NAME } from '@/config/setting';
+import { getToken } from '@/utils/token-util';
+
+const DRAWER_WIDTH = 720;
+const SESSION_WIDTH = 220;
+
+const parseSseChunk = (chunk, onEvent) => {
+    const blocks = chunk.split('\n\n');
+    const remain = blocks.pop();
+    blocks.forEach((block) => {
+        if (!block.trim()) {
+            return;
+        }
+        let eventName = 'message';
+        const dataLines = [];
+        block.split('\n').forEach((line) => {
+            if (line.startsWith('event:')) {
+                eventName = line.substring(6).trim();
+            }
+            if (line.startsWith('data:')) {
+                dataLines.push(line.substring(5).trim());
+            }
+        });
+        if (!dataLines.length) {
+            return;
+        }
+        try {
+            onEvent(eventName, JSON.parse(dataLines.join('\n')));
+        } catch (error) {
+            console.error(error);
+        }
+    });
+    return remain || '';
+};
+
+const buildAssistantMessage = (sessionId, modelCode) => ({
+    id: `assistant-${Date.now()}`,
+    sessionId,
+    role: 'assistant',
+    content: '',
+    modelCode,
+    createTime: new Date().toISOString()
+});
+
+const buildFallbackAnswer = (modelCode, question) => `褰撳墠涓烘紨绀烘ā寮忥紝妯″瀷[${modelCode}]宸叉敹鍒颁綘鐨勯棶棰橈細${question}`;
+
+const shouldUseMockFallback = (models, modelCode) => {
+    const model = (models || []).find((item) => item.code === modelCode);
+    return model ? model.provider === 'mock' : String(modelCode || '').startsWith('mock');
+};
+
+const formatTime = (value) => {
+    if (!value) {
+        return '';
+    }
+    const date = new Date(value);
+    if (Number.isNaN(date.getTime())) {
+        return '';
+    }
+    return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
+};
+
+const formatPreview = (value) => {
+    if (!value) {
+        return '寮�濮嬩竴涓柊鐨勫璇�';
+    }
+    const normalized = String(value).replace(/\s+/g, ' ').trim();
+    return normalized.length > 22 ? `${normalized.slice(0, 22)}...` : normalized;
+};
+
+export const AiChatWidget = ({
+    trigger = 'fab',
+    buttonText = 'AI 瀵硅瘽',
+    buttonVariant = 'contained',
+    buttonSx = {}
+}) => {
+    const [open, setOpen] = useState(false);
+    const [loading, setLoading] = useState(false);
+    const [sending, setSending] = useState(false);
+    const [models, setModels] = useState([]);
+    const [sessions, setSessions] = useState([]);
+    const [activeSessionId, setActiveSessionId] = useState('');
+    const [messagesBySession, setMessagesBySession] = useState({});
+    const [draft, setDraft] = useState('');
+    const [error, setError] = useState('');
+    const streamControllerRef = useRef(null);
+    const messagesEndRef = useRef(null);
+
+    const scrollToBottom = (behavior = 'auto') => {
+        window.setTimeout(() => {
+            window.requestAnimationFrame(() => {
+                messagesEndRef.current?.scrollIntoView({ behavior, block: 'end' });
+            });
+        }, 0);
+    };
+
+    const activeSession = useMemo(
+        () => sessions.find((item) => item.id === activeSessionId) || null,
+        [sessions, activeSessionId]
+    );
+
+    const activeMessages = messagesBySession[activeSessionId] || [];
+
+    useEffect(() => {
+        if (open) {
+            bootstrap();
+        }
+    }, [open]);
+
+    useEffect(() => {
+        if (!open) {
+            return undefined;
+        }
+        const timer = window.setTimeout(() => {
+            scrollToBottom('auto');
+        }, 180);
+        return () => window.clearTimeout(timer);
+    }, [open, activeSessionId]);
+
+    useEffect(() => {
+        if (open && activeSessionId) {
+            loadMessages(activeSessionId);
+        }
+    }, [open, activeSessionId]);
+
+    useEffect(() => {
+        if (open && messagesEndRef.current) {
+            scrollToBottom('smooth');
+        }
+    }, [open, activeMessages]);
+
+    const bootstrap = async () => {
+        setLoading(true);
+        setError('');
+        try {
+            const [modelList, sessionList] = await Promise.all([getAiModels(), getAiSessions()]);
+            setModels(modelList);
+            if (sessionList.length > 0) {
+                setSessions(sessionList);
+                setActiveSessionId((prev) => prev || sessionList[0].id);
+            } else {
+                const created = await createAiSession({ modelCode: modelList[0]?.code });
+                setSessions(created ? [created] : []);
+                setActiveSessionId(created?.id || '');
+                if (created?.id) {
+                    setMessagesBySession((prev) => ({ ...prev, [created.id]: [] }));
+                }
+            }
+        } catch (bootstrapError) {
+            setError(bootstrapError.message || 'AI鍔╂墜鍒濆鍖栧け璐�');
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    const refreshSessions = async (preferSessionId) => {
+        try {
+            const sessionList = await getAiSessions();
+            if (sessionList.length > 0) {
+                setSessions(sessionList);
+                setActiveSessionId(preferSessionId || sessionList[0].id);
+            }
+        } catch (refreshError) {
+            setError(refreshError.message || '鍒锋柊浼氳瘽澶辫触');
+        }
+    };
+
+    const loadMessages = async (sessionId) => {
+        if (!sessionId || messagesBySession[sessionId]) {
+            return;
+        }
+        try {
+            const messageList = await getAiMessages(sessionId);
+            setMessagesBySession((prev) => {
+                const current = prev[sessionId];
+                if (current && current.length > 0) {
+                    return prev;
+                }
+                return { ...prev, [sessionId]: messageList };
+            });
+            scrollToBottom('auto');
+        } catch (messageError) {
+            setError(messageError.message || '鍔犺浇娑堟伅澶辫触');
+        }
+    };
+
+    const handleCreateSession = async () => {
+        try {
+            const created = await createAiSession({ modelCode: activeSession?.modelCode || models[0]?.code });
+            setSessions((prev) => [created, ...prev]);
+            setActiveSessionId(created.id);
+            setMessagesBySession((prev) => ({ ...prev, [created.id]: [] }));
+        } catch (createError) {
+            setError(createError.message || '鍒涘缓浼氳瘽澶辫触');
+        }
+    };
+
+    const handleDeleteSession = async (sessionId) => {
+        try {
+            await removeAiSession(sessionId);
+            const nextSessions = sessions.filter((item) => item.id !== sessionId);
+            setSessions(nextSessions);
+            setMessagesBySession((prev) => {
+                const next = { ...prev };
+                delete next[sessionId];
+                return next;
+            });
+            if (nextSessions.length > 0) {
+                setActiveSessionId(nextSessions[0].id);
+            } else {
+                const created = await createAiSession({ modelCode: models[0]?.code });
+                if (created?.id) {
+                    setSessions([created]);
+                    setActiveSessionId(created.id);
+                    setMessagesBySession({ [created.id]: [] });
+                }
+            }
+        } catch (deleteError) {
+            setError(deleteError.message || '鍒犻櫎浼氳瘽澶辫触');
+        }
+    };
+
+    const handleModelChange = (modelCode) => {
+        setSessions((prev) => prev.map((item) => item.id === activeSessionId ? { ...item, modelCode } : item));
+    };
+
+    const handleStop = async () => {
+        if (!activeSessionId) {
+            return;
+        }
+        if (streamControllerRef.current) {
+            streamControllerRef.current.abort();
+        }
+        try {
+            await stopAiChat({ sessionId: activeSessionId });
+        } catch (stopError) {
+            console.error(stopError);
+        } finally {
+            setSending(false);
+        }
+    };
+
+    const handleSend = async () => {
+        if (!draft.trim() || !activeSessionId || sending) {
+            return;
+        }
+        const userContent = draft.trim();
+        const sessionId = activeSessionId;
+        const modelCode = activeSession?.modelCode || models[0]?.code;
+        setDraft('');
+        setError('');
+        setSending(true);
+        setMessagesBySession((prev) => {
+            const current = prev[sessionId] || [];
+            return {
+                ...prev,
+                [sessionId]: [
+                    ...current,
+                    {
+                        id: `user-${Date.now()}`,
+                        sessionId,
+                        role: 'user',
+                        content: userContent,
+                        modelCode,
+                        createTime: new Date().toISOString()
+                    },
+                    buildAssistantMessage(sessionId, modelCode)
+                ]
+            };
+        });
+
+        const controller = new AbortController();
+        streamControllerRef.current = controller;
+        let buffer = '';
+        let receivedDelta = false;
+
+        try {
+            const response = await fetch(`${PREFIX_BASE_URL}ai/chat/stream`, {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json',
+                    Accept: 'text/event-stream',
+                    [TOKEN_HEADER_NAME]: getToken()
+                },
+                body: JSON.stringify({
+                    sessionId,
+                    message: userContent,
+                    modelCode
+                }),
+                signal: controller.signal
+            });
+
+            if (!response.ok || !response.body) {
+                throw new Error('AI娴佸紡璇锋眰澶辫触');
+            }
+
+            const reader = response.body.getReader();
+            const decoder = new TextDecoder('utf-8');
+
+            while (true) {
+                const { value, done } = await reader.read();
+                if (done) {
+                    break;
+                }
+                buffer += decoder.decode(value, { stream: true });
+                buffer = parseSseChunk(buffer, (eventName, payload) => {
+                    if (eventName === 'session' && payload.sessionId) {
+                        setSessions((prev) => prev.map((item) => item.id === sessionId ? { ...item, modelCode: payload.modelCode || modelCode } : item));
+                    }
+                    if (eventName === 'delta') {
+                        receivedDelta = true;
+                        setMessagesBySession((prev) => {
+                            const current = [...(prev[sessionId] || [])];
+                            if (!current.length) {
+                                return prev;
+                            }
+                            const last = current[current.length - 1];
+                            current[current.length - 1] = { ...last, content: `${last.content || ''}${payload.content || ''}` };
+                            return { ...prev, [sessionId]: current };
+                        });
+                    }
+                    if (eventName === 'error') {
+                        setError(payload.message || 'AI鏈嶅姟寮傚父');
+                    }
+                });
+            }
+
+            const latestMessages = await getAiMessages(sessionId);
+            setMessagesBySession((prev) => {
+                const current = prev[sessionId] || [];
+                if (!receivedDelta && current.length > 0 && shouldUseMockFallback(models, modelCode)) {
+                    const fallbackAnswer = buildFallbackAnswer(modelCode, userContent);
+                    const next = [...current];
+                    const last = next[next.length - 1];
+                    if (last && last.role === 'assistant' && !last.content) {
+                        next[next.length - 1] = { ...last, content: fallbackAnswer };
+                        return { ...prev, [sessionId]: next };
+                    }
+                }
+                if (latestMessages.length === 0 && current.length > 0) {
+                    return prev;
+                }
+                return { ...prev, [sessionId]: latestMessages };
+            });
+            setSessions((prev) => prev.map((item) => {
+                if (item.id !== sessionId) {
+                    return item;
+                }
+                const lastMessage = assistantReplyText(latestMessages) || item.lastMessage || userContent;
+                return {
+                    ...item,
+                    lastMessage,
+                    lastMessageAt: new Date().toISOString(),
+                    updateTime: new Date().toISOString()
+                };
+            }));
+            await refreshSessions(sessionId);
+        } catch (sendError) {
+            if (sendError.name !== 'AbortError') {
+                setError(sendError.message || '鍙戦�佹秷鎭け璐�');
+            }
+        } finally {
+            setSending(false);
+            streamControllerRef.current = null;
+        }
+    };
+
+    const assistantReplyText = (messageList) => {
+        const assistantMessages = (messageList || []).filter((item) => item.role === 'assistant');
+        if (!assistantMessages.length) {
+            return '';
+        }
+        return assistantMessages[assistantMessages.length - 1].content || '';
+    };
+
+    return (
+        <>
+            {trigger === 'fab' ? (
+                <Fab
+                    color="secondary"
+                    onClick={() => setOpen(true)}
+                    sx={{ position: 'fixed', right: 24, bottom: 24, zIndex: 1301 }}
+                >
+                    <ChatBubbleOutlineIcon />
+                </Fab>
+            ) : (
+                <Button
+                    variant={buttonVariant}
+                    startIcon={<ChatBubbleOutlineIcon />}
+                    onClick={() => setOpen(true)}
+                    sx={buttonSx}
+                >
+                    {buttonText}
+                </Button>
+            )}
+            <Drawer
+                anchor="right"
+                open={open}
+                onClose={() => setOpen(false)}
+                PaperProps={{
+                    sx: {
+                        width: DRAWER_WIDTH,
+                        maxWidth: '100vw',
+                        backgroundColor: '#f5f7fa'
+                    }
+                }}
+            >
+                <Stack sx={{ height: '100%' }}>
+                    <Stack
+                        direction="row"
+                        alignItems="center"
+                        spacing={1.5}
+                        sx={{
+                            px: 2,
+                            py: 1.5,
+                            backgroundColor: '#fff',
+                            color: 'text.primary',
+                            borderBottom: '1px solid rgba(0, 0, 0, 0.08)'
+                        }}
+                    >
+                        <Avatar
+                            sx={{
+                                width: 38,
+                                height: 38,
+                                bgcolor: 'rgba(25, 118, 210, 0.12)',
+                                color: 'primary.main',
+                                fontSize: 16,
+                                fontWeight: 700
+                            }}
+                        >
+                            AI
+                        </Avatar>
+                        <Box sx={{ flex: 1, minWidth: 0 }}>
+                            <Typography variant="h6" sx={{ fontWeight: 700, lineHeight: 1.2 }}>
+                                鏅鸿兘瀵硅瘽
+                            </Typography>
+                            <Typography variant="caption" color="text.secondary">
+                                澶氫細璇濄�佸妯″瀷銆佹祦寮忓搷搴�
+                            </Typography>
+                        </Box>
+                        <Chip
+                            size="small"
+                            label={sending ? '鐢熸垚涓�' : '鍦ㄧ嚎'}
+                            sx={{
+                                bgcolor: sending ? 'rgba(255, 167, 38, 0.14)' : 'rgba(76, 175, 80, 0.12)',
+                                color: sending ? '#ad6800' : '#2e7d32',
+                                border: '1px solid rgba(0, 0, 0, 0.06)'
+                            }}
+                        />
+                        <Tooltip title="鏂板缓浼氳瘽">
+                            <IconButton color="primary" onClick={handleCreateSession}>
+                                <AddIcon />
+                            </IconButton>
+                        </Tooltip>
+                        <IconButton color="default" onClick={() => setOpen(false)}>
+                            <CloseIcon />
+                        </IconButton>
+                    </Stack>
+                    <Divider />
+                    {error ? <Alert severity="error" sx={{ m: 1.5, borderRadius: 2 }}>{error}</Alert> : null}
+                    <Stack direction="row" sx={{ minHeight: 0, flex: 1 }}>
+                        <Box sx={{
+                            width: SESSION_WIDTH,
+                            borderRight: '1px solid',
+                            borderColor: 'rgba(0, 0, 0, 0.08)',
+                            display: 'flex',
+                            flexDirection: 'column',
+                            backgroundColor: '#f7f9fc'
+                        }}>
+                            <Box sx={{ px: 1.5, py: 1.25 }}>
+                                <Typography variant="caption" color="text.secondary" sx={{ fontWeight: 700 }}>
+                                    浼氳瘽鍒楄〃
+                                </Typography>
+                            </Box>
+                            <List sx={{ flex: 1, overflowY: 'auto', py: 0, px: 1 }}>
+                                {sessions.map((item) => (
+                                    <ListItemButton
+                                        key={item.id}
+                                        selected={item.id === activeSessionId}
+                                        onClick={() => setActiveSessionId(item.id)}
+                                        sx={{
+                                            alignItems: 'flex-start',
+                                            pr: 1,
+                                            mb: 0.75,
+                                            borderRadius: 2,
+                                            border: '1px solid',
+                                            borderColor: item.id === activeSessionId ? 'rgba(25, 118, 210, 0.28)' : 'rgba(0, 0, 0, 0.06)',
+                                            backgroundColor: item.id === activeSessionId ? '#eaf3fe' : '#fff',
+                                            boxShadow: item.id === activeSessionId ? '0 6px 16px rgba(25, 118, 210, 0.08)' : 'none'
+                                        }}
+                                    >
+                                        <ListItemText
+                                            primary={item.title || '鏂板璇�'}
+                                            secondary={`${formatPreview(item.lastMessage || item.modelCode)}${formatTime(item.updateTime || item.lastMessageAt) ? ` 路 ${formatTime(item.updateTime || item.lastMessageAt)}` : ''}`}
+                                            primaryTypographyProps={{ noWrap: true, fontSize: 13, fontWeight: 700 }}
+                                            secondaryTypographyProps={{ noWrap: true, fontSize: 11.5, color: 'text.secondary' }}
+                                        />
+                                        <IconButton
+                                            size="small"
+                                            edge="end"
+                                            sx={{ mt: 0.25 }}
+                                            onClick={(event) => {
+                                                event.stopPropagation();
+                                                handleDeleteSession(item.id);
+                                            }}
+                                        >
+                                            <DeleteOutlineIcon fontSize="inherit" />
+                                        </IconButton>
+                                    </ListItemButton>
+                                ))}
+                            </List>
+                        </Box>
+                        <Stack sx={{ flex: 1, minWidth: 0 }}>
+                            <Stack
+                                direction="row"
+                                alignItems="center"
+                                spacing={1}
+                                sx={{
+                                    px: 2,
+                                    py: 1.5,
+                                    backgroundColor: '#fff'
+                                }}
+                            >
+                                <Box sx={{ flex: 1, minWidth: 0 }}>
+                                    <Typography variant="body2" color="text.secondary">
+                                        褰撳墠妯″瀷
+                                    </Typography>
+                                    <Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
+                                        {activeSession?.title || '鏈懡鍚嶄細璇�'}
+                                    </Typography>
+                                </Box>
+                                <Select
+                                    size="small"
+                                    value={activeSession?.modelCode || models[0]?.code || ''}
+                                    onChange={(event) => handleModelChange(event.target.value)}
+                                    sx={{
+                                        minWidth: 176,
+                                        borderRadius: 2,
+                                        backgroundColor: '#fff',
+                                        boxShadow: '0 1px 2px rgba(15, 23, 42, 0.06)'
+                                    }}
+                                >
+                                    {models.map((model) => (
+                                        <MenuItem key={model.code} value={model.code}>
+                                            {model.name}
+                                        </MenuItem>
+                                    ))}
+                                </Select>
+                            </Stack>
+                            <Divider />
+                            <Box
+                                sx={{
+                                    flex: 1,
+                                    overflowY: 'auto',
+                                    px: 2.5,
+                                    py: 2,
+                                    backgroundColor: '#f5f7fa'
+                                }}
+                            >
+                                {loading ? (
+                                    <Stack alignItems="center" justifyContent="center" sx={{ height: '100%' }}>
+                                        <CircularProgress size={28} />
+                                    </Stack>
+                                ) : activeMessages.length === 0 ? (
+                                    <Stack
+                                        alignItems="center"
+                                        justifyContent="center"
+                                        spacing={1.25}
+                                        sx={{
+                                            height: '100%',
+                                            textAlign: 'center',
+                                            color: 'text.secondary',
+                                            px: 3
+                                        }}
+                                    >
+                                        <Avatar sx={{ width: 56, height: 56, bgcolor: 'rgba(25,118,210,0.1)', color: 'primary.main' }}>
+                                            AI
+                                        </Avatar>
+                                        <Typography variant="subtitle1" sx={{ fontWeight: 700, color: 'text.primary' }}>
+                                            寮�濮嬫柊鐨勬櫤鑳藉璇�
+                                        </Typography>
+                                        <Typography variant="body2">
+                                            鍙互鐩存帴鎻愰棶浠撳偍涓氬姟闂锛屾垨鍒囨崲妯″瀷寮�濮嬫柊鐨勪細璇濄��
+                                        </Typography>
+                                    </Stack>
+                                ) : (
+                                    <Stack spacing={2}>
+                                        {activeMessages.map((message) => (
+                                            <Box
+                                                key={message.id}
+                                                sx={{
+                                                    alignSelf: message.role === 'user' ? 'flex-end' : 'flex-start',
+                                                    maxWidth: '88%'
+                                                }}
+                                            >
+                                                <Stack
+                                                    direction={message.role === 'user' ? 'row-reverse' : 'row'}
+                                                    spacing={1}
+                                                    alignItems="flex-start"
+                                                >
+                                                    <Avatar
+                                                        sx={{
+                                                            width: 30,
+                                                            height: 30,
+                                                            mt: 0.25,
+                                                            bgcolor: message.role === 'user' ? '#1976d2' : '#eaf3fe',
+                                                            color: message.role === 'user' ? '#fff' : '#1976d2',
+                                                            fontSize: 12,
+                                                            fontWeight: 700
+                                                        }}
+                                                    >
+                                                        {message.role === 'user' ? '鎴�' : 'AI'}
+                                                    </Avatar>
+                                                    <Box
+                                                        sx={{
+                                                            maxWidth: 'calc(100% - 42px)',
+                                                            display: 'flex',
+                                                            flexDirection: 'column',
+                                                            alignItems: message.role === 'user' ? 'flex-end' : 'flex-start'
+                                                        }}
+                                                    >
+                                                        <Stack
+                                                            direction="row"
+                                                            spacing={0.75}
+                                                            alignItems="center"
+                                                            justifyContent={message.role === 'user' ? 'flex-end' : 'flex-start'}
+                                                            sx={{ px: 0.5, mb: 0.5 }}
+                                                        >
+                                                            <Typography variant="caption" color="text.secondary" sx={{ fontWeight: 700 }}>
+                                                                {message.role === 'assistant' ? message.modelCode || activeSession?.modelCode : '鎴�'}
+                                                            </Typography>
+                                                            <Typography variant="caption" color="text.disabled">
+                                                                {formatTime(message.createTime)}
+                                                            </Typography>
+                                                        </Stack>
+                                                        <Box
+                                                            sx={{
+                                                                px: 1.8,
+                                                                py: 1.35,
+                                                                minWidth: message.role === 'user' ? 96 : 'auto',
+                                                                maxWidth: message.role === 'user' ? '82%' : '100%',
+                                                                borderRadius: message.role === 'user' ? '20px 20px 8px 20px' : '20px 20px 20px 8px',
+                                                                background: '#fff',
+                                                                color: 'text.primary',
+                                                                boxShadow: '0 8px 18px rgba(15, 23, 42, 0.05)',
+                                                                border: '1px solid rgba(25, 118, 210, 0.08)',
+                                                                display: 'inline-block',
+                                                                whiteSpace: 'pre-wrap',
+                                                                wordBreak: 'normal',
+                                                                overflowWrap: 'anywhere',
+                                                                lineHeight: 1.7,
+                                                                fontSize: 14
+                                                            }}
+                                                        >
+                                                            {message.content || (message.role === 'assistant' && sending ? '姝e湪鐢熸垚鍥炲...' : '')}
+                                                        </Box>
+                                                    </Box>
+                                                </Stack>
+                                            </Box>
+                                        ))}
+                                        <div ref={messagesEndRef} />
+                                    </Stack>
+                                )}
+                            </Box>
+                            <Divider />
+                            <Box
+                                sx={{
+                                    p: 1.5,
+                                    backgroundColor: '#fff'
+                                }}
+                            >
+                                <Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
+                                    <Chip
+                                        size="small"
+                                        label={activeSession?.modelCode || '鏈�夋嫨妯″瀷'}
+                                        sx={{ bgcolor: 'rgba(25,118,210,0.08)', color: 'primary.main' }}
+                                    />
+                                    <Typography variant="caption" color="text.secondary">
+                                        `Enter` 鍙戦�侊紝`Shift + Enter` 鎹㈣
+                                    </Typography>
+                                </Stack>
+                                <TextField
+                                    fullWidth
+                                    multiline
+                                    minRows={3}
+                                    maxRows={6}
+                                    value={draft}
+                                    onChange={(event) => setDraft(event.target.value)}
+                                    placeholder="杈撳叆闂锛屾敮鎸佸浼氳瘽鍜屾ā鍨嬪垏鎹�"
+                                    onKeyDown={(event) => {
+                                        if (event.key === 'Enter' && !event.shiftKey) {
+                                            event.preventDefault();
+                                            handleSend();
+                                        }
+                                    }}
+                                    sx={{
+                                        '& .MuiOutlinedInput-root': {
+                                            borderRadius: 3,
+                                            backgroundColor: '#fff'
+                                        }
+                                    }}
+                                />
+                                <Stack direction="row" spacing={1} justifyContent="flex-end" sx={{ mt: 1.2 }}>
+                                    <Tooltip title="鍋滄鐢熸垚">
+                                        <span>
+                                            <IconButton color="warning" disabled={!sending} onClick={handleStop}>
+                                                <StopCircleOutlinedIcon />
+                                            </IconButton>
+                                        </span>
+                                    </Tooltip>
+                                    <Tooltip title="鍙戦��">
+                                        <span>
+                                            <IconButton color="primary" disabled={sending || !draft.trim()} onClick={handleSend}>
+                                                <SendIcon />
+                                            </IconButton>
+                                        </span>
+                                    </Tooltip>
+                                </Stack>
+                            </Box>
+                        </Stack>
+                    </Stack>
+                </Stack>
+            </Drawer>
+        </>
+    );
+};
diff --git a/rsf-admin/src/api/ai/index.js b/rsf-admin/src/api/ai/index.js
new file mode 100644
index 0000000..4bb5fab
--- /dev/null
+++ b/rsf-admin/src/api/ai/index.js
@@ -0,0 +1,43 @@
+import request from '../../utils/request';
+
+export async function getAiModels() {
+    const res = await request.get('/ai/model/list');
+    if (res.data.code === 200) {
+        return res.data.data || [];
+    }
+    return Promise.reject(new Error(res.data.msg || 'Load models failed'));
+}
+
+export async function getAiSessions() {
+    const res = await request.get('/ai/session/list');
+    if (res.data.code === 200) {
+        return res.data.data || [];
+    }
+    return Promise.reject(new Error(res.data.msg || 'Load sessions failed'));
+}
+
+export async function createAiSession(payload = {}) {
+    const res = await request.post('/ai/session/create', payload);
+    if (res.data.code === 200) {
+        return res.data.data;
+    }
+    return Promise.reject(new Error(res.data.msg || 'Create session failed'));
+}
+
+export async function removeAiSession(sessionId) {
+    const res = await request.post(`/ai/session/remove/${sessionId}`);
+    return res.data;
+}
+
+export async function getAiMessages(sessionId) {
+    const res = await request.get(`/ai/session/${sessionId}/messages`);
+    if (res.data.code === 200) {
+        return res.data.data || [];
+    }
+    return Promise.reject(new Error(res.data.msg || 'Load messages failed'));
+}
+
+export async function stopAiChat(payload) {
+    const res = await request.post('/ai/chat/stop', payload);
+    return res.data;
+}
diff --git a/rsf-admin/src/i18n/en.js b/rsf-admin/src/i18n/en.js
index 00fd50a..9c32fd3 100644
--- a/rsf-admin/src/i18n/en.js
+++ b/rsf-admin/src/i18n/en.js
@@ -150,6 +150,7 @@
         token: 'Token',
         operation: 'Operation',
         config: 'Config',
+        aiParam: 'AI Params',
         tenant: 'Tenant',
         userLogin: 'Token',
         customer: 'Customer',
@@ -401,6 +402,19 @@
                 content: "content",
                 type: "type",
             },
+            aiParam: {
+                uuid: "uuid",
+                name: "name",
+                modelCode: "model code",
+                provider: "provider",
+                chatUrl: "chat url",
+                apiKey: "api key",
+                modelName: "model name",
+                systemPrompt: "system prompt",
+                maxContextMessages: "max context",
+                defaultFlag: "default",
+                sort: "sort",
+            },
             tenant: {
                 name: "name",
                 flag: "flag",
diff --git a/rsf-admin/src/i18n/zh.js b/rsf-admin/src/i18n/zh.js
index f54083c..a351480 100644
--- a/rsf-admin/src/i18n/zh.js
+++ b/rsf-admin/src/i18n/zh.js
@@ -151,6 +151,7 @@
         token: '鐧诲綍鏃ュ織',
         operation: '鎿嶄綔鏃ュ織',
         config: '閰嶇疆鍙傛暟',
+        aiParam: 'AI鍙傛暟',
         tenant: '绉熸埛绠$悊',
         userLogin: '鐧诲綍鏃ュ織',
         customer: '瀹㈡埛琛�',
@@ -430,6 +431,19 @@
                 content: "閰嶇疆鍐呭",
                 type: "鏁版嵁绫诲瀷",
             },
+            aiParam: {
+                uuid: "缂栧彿",
+                name: "鍚嶇О",
+                modelCode: "妯″瀷缂栫爜",
+                provider: "渚涘簲鍟�",
+                chatUrl: "鑱婂ぉ鍦板潃",
+                apiKey: "API瀵嗛挜",
+                modelName: "妯″瀷鍚嶇О",
+                systemPrompt: "绯荤粺鎻愮ず璇�",
+                maxContextMessages: "涓婁笅鏂囪疆鏁�",
+                defaultFlag: "榛樿妯″瀷",
+                sort: "鎺掑簭",
+            },
             tenant: {
                 name: "绉熸埛鍚�",
                 flag: "浠g爜",
diff --git a/rsf-admin/src/layout/AppBarToolbar.jsx b/rsf-admin/src/layout/AppBarToolbar.jsx
index 45cf9db..ff7cd2b 100644
--- a/rsf-admin/src/layout/AppBarToolbar.jsx
+++ b/rsf-admin/src/layout/AppBarToolbar.jsx
@@ -1,12 +1,28 @@
 import { LoadingIndicator, LocalesMenuButton } from 'react-admin';
 import { ThemeSwapper } from '../themes/ThemeSwapper';
 import { TenantTip } from './TenantTip';
+import { AiChatWidget } from '@/ai/AiChatWidget';
 
 export const AppBarToolbar = () => (
     <>
         <LocalesMenuButton />
         <ThemeSwapper />
         <LoadingIndicator />
+        <AiChatWidget
+            trigger="button"
+            buttonText="AI 瀵硅瘽"
+            buttonVariant="text"
+            buttonSx={{
+                minWidth: 'auto',
+                px: 1.25,
+                color: '#fff',
+                borderRadius: 2,
+                whiteSpace: 'nowrap',
+                '&:hover': {
+                    backgroundColor: 'rgba(255,255,255,0.12)'
+                }
+            }}
+        />
         <TenantTip />
     </>
 );
diff --git a/rsf-admin/src/page/ResourceContent.js b/rsf-admin/src/page/ResourceContent.js
index b9aefe3..a4f7965 100644
--- a/rsf-admin/src/page/ResourceContent.js
+++ b/rsf-admin/src/page/ResourceContent.js
@@ -6,6 +6,7 @@
 
 import host from "./system/host";
 import config from "./system/config";
+import aiParam from "./system/aiParam";
 import tenant from "./system/tenant";
 import role from "./system/role";
 import userLogin from "./system/userLogin";
@@ -77,6 +78,8 @@
       return host;
     case "config":
       return config;
+    case "aiParam":
+      return aiParam;
     case "tenant":
       return tenant;
     case "role":
@@ -216,4 +219,4 @@
   }
 };
 
-export default ResourceContent;
\ No newline at end of file
+export default ResourceContent;
diff --git a/rsf-admin/src/page/system/aiParam/AiParamCreate.jsx b/rsf-admin/src/page/system/aiParam/AiParamCreate.jsx
new file mode 100644
index 0000000..15ed6ac
--- /dev/null
+++ b/rsf-admin/src/page/system/aiParam/AiParamCreate.jsx
@@ -0,0 +1,175 @@
+import React from "react";
+import {
+    CreateBase,
+    useTranslate,
+    TextInput,
+    NumberInput,
+    SaveButton,
+    SelectInput,
+    Toolbar,
+    useNotify,
+    Form,
+} from 'react-admin';
+import {
+    Dialog,
+    DialogActions,
+    DialogContent,
+    DialogTitle,
+    Stack,
+    Grid,
+    Box,
+} from '@mui/material';
+import DialogCloseButton from "@/page/components/DialogCloseButton";
+import StatusSelectInput from "@/page/components/StatusSelectInput";
+import MemoInput from "@/page/components/MemoInput";
+
+const yesNoChoices = [
+    { id: 1, name: 'common.enums.true' },
+    { id: 0, name: 'common.enums.false' },
+];
+
+const providerChoices = [
+    { id: 'openai', name: 'OpenAI Compatible' },
+    { id: 'mock', name: 'Mock' },
+];
+
+const AiParamCreate = (props) => {
+    const { open, setOpen } = props;
+
+    const translate = useTranslate();
+    const notify = useNotify();
+
+    const handleClose = (event, reason) => {
+        if (reason !== "backdropClick") {
+            setOpen(false);
+        }
+    };
+
+    const handleSuccess = async () => {
+        setOpen(false);
+        notify('common.response.success');
+    };
+
+    const handleError = async (error) => {
+        notify(error.message || 'common.response.fail', { type: 'error', messageArgs: { _: error.message } });
+    };
+
+    return (
+        <CreateBase
+            record={{ defaultFlag: 0, sort: 0, maxContextMessages: 12, status: 1, provider: 'openai' }}
+            transform={(data) => {
+                return data;
+            }}
+            mutationOptions={{ onSuccess: handleSuccess, onError: handleError }}
+        >
+            <Dialog
+                open={open}
+                onClose={handleClose}
+                aria-labelledby="form-dialog-title"
+                fullWidth
+                disableRestoreFocus
+                maxWidth="md"
+            >
+                <Form>
+                    <DialogTitle id="form-dialog-title" sx={{
+                        position: 'sticky',
+                        top: 0,
+                        backgroundColor: 'background.paper',
+                        zIndex: 1000
+                    }}
+                    >
+                        {translate('create.title')}
+                        <Box sx={{ position: 'absolute', top: 8, right: 8, zIndex: 1001 }}>
+                            <DialogCloseButton onClose={handleClose} />
+                        </Box>
+                    </DialogTitle>
+                    <DialogContent sx={{ mt: 2 }}>
+                        <Grid container rowSpacing={2} columnSpacing={2}>
+                            <Grid item xs={6} display="flex" gap={1}>
+                                <TextInput label="table.field.aiParam.name" source="name" parse={v => v} fullWidth />
+                            </Grid>
+                            <Grid item xs={6} display="flex" gap={1}>
+                                <TextInput label="table.field.aiParam.modelCode" source="modelCode" parse={v => v} fullWidth />
+                            </Grid>
+                            <Grid item xs={6} display="flex" gap={1}>
+                                <SelectInput
+                                    label="table.field.aiParam.provider"
+                                    source="provider"
+                                    choices={providerChoices}
+                                    fullWidth
+                                />
+                            </Grid>
+                            <Grid item xs={6} display="flex" gap={1}>
+                                <TextInput
+                                    label="table.field.aiParam.modelName"
+                                    source="modelName"
+                                    parse={v => v}
+                                    helperText="濉啓鐪熷疄妯″瀷鍚嶏紝渚嬪 gpt-4o-mini銆乨eepseek-chat"
+                                    fullWidth
+                                />
+                            </Grid>
+                            <Grid item xs={12} display="flex" gap={1}>
+                                <TextInput
+                                    label="table.field.aiParam.chatUrl"
+                                    source="chatUrl"
+                                    parse={v => v}
+                                    helperText="鏀寔濉啓 baseUrl锛屽 https://api.openai.com 鎴� https://api.siliconflow.cn锛岀郴缁熶細鑷姩琛ュ叏涓� /v1/chat/completions"
+                                    fullWidth
+                                />
+                            </Grid>
+                            <Grid item xs={12} display="flex" gap={1}>
+                                <TextInput
+                                    label="table.field.aiParam.apiKey"
+                                    source="apiKey"
+                                    parse={v => v}
+                                    type="password"
+                                    helperText="OpenAI 鎺ュ彛妯″紡涓嬪~鍐� Bearer Token锛屾棤闇�鎵嬪姩鍔� Bearer 鍓嶇紑"
+                                    fullWidth
+                                />
+                            </Grid>
+                            <Grid item xs={6} display="flex" gap={1}>
+                                <NumberInput label="table.field.aiParam.maxContextMessages" source="maxContextMessages" fullWidth />
+                            </Grid>
+                            <Grid item xs={6} display="flex" gap={1}>
+                                <NumberInput label="table.field.aiParam.sort" source="sort" fullWidth />
+                            </Grid>
+                            <Grid item xs={6} display="flex" gap={1}>
+                                <SelectInput
+                                    label="table.field.aiParam.defaultFlag"
+                                    source="defaultFlag"
+                                    choices={yesNoChoices}
+                                    fullWidth
+                                />
+                            </Grid>
+                            <Grid item xs={6} display="flex" gap={1}>
+                                <StatusSelectInput fullWidth />
+                            </Grid>
+                            <Grid item xs={12} display="flex" gap={1}>
+                                <TextInput
+                                    label="table.field.aiParam.systemPrompt"
+                                    source="systemPrompt"
+                                    parse={v => v}
+                                    fullWidth
+                                    multiline
+                                    minRows={4}
+                                />
+                            </Grid>
+                            <Grid item xs={12} display="flex" gap={1}>
+                                <Stack direction="column" spacing={1} width={'100%'}>
+                                    <MemoInput />
+                                </Stack>
+                            </Grid>
+                        </Grid>
+                    </DialogContent>
+                    <DialogActions sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
+                        <Toolbar sx={{ width: '100%', justifyContent: 'space-between' }}  >
+                            <SaveButton />
+                        </Toolbar>
+                    </DialogActions>
+                </Form>
+            </Dialog>
+        </CreateBase>
+    )
+}
+
+export default AiParamCreate;
diff --git a/rsf-admin/src/page/system/aiParam/AiParamEdit.jsx b/rsf-admin/src/page/system/aiParam/AiParamEdit.jsx
new file mode 100644
index 0000000..6165be5
--- /dev/null
+++ b/rsf-admin/src/page/system/aiParam/AiParamEdit.jsx
@@ -0,0 +1,136 @@
+import React from "react";
+import {
+    Edit,
+    SimpleForm,
+    useTranslate,
+    TextInput,
+    NumberInput,
+    SaveButton,
+    SelectInput,
+    Toolbar,
+    DeleteButton,
+} from 'react-admin';
+import { useFormContext } from "react-hook-form";
+import { Stack, Grid, Typography } from '@mui/material';
+import { EDIT_MODE } from '@/config/setting';
+import EditBaseAside from "@/page/components/EditBaseAside";
+import CustomerTopToolBar from "@/page/components/EditTopToolBar";
+import MemoInput from "@/page/components/MemoInput";
+import StatusSelectInput from "@/page/components/StatusSelectInput";
+
+const yesNoChoices = [
+    { id: 1, name: 'common.enums.true' },
+    { id: 0, name: 'common.enums.false' },
+];
+
+const providerChoices = [
+    { id: 'openai', name: 'OpenAI Compatible' },
+    { id: 'mock', name: 'Mock' },
+];
+
+const FormToolbar = () => {
+    const { getValues } = useFormContext();
+
+    return (
+        <Toolbar sx={{ justifyContent: 'space-between' }}>
+            <SaveButton />
+            <DeleteButton mutationMode="optimistic" />
+        </Toolbar>
+    )
+}
+
+const AiParamEdit = () => {
+    const translate = useTranslate();
+
+    return (
+        <Edit
+            redirect="list"
+            mutationMode={EDIT_MODE}
+            actions={<CustomerTopToolBar />}
+            aside={<EditBaseAside />}
+        >
+            <SimpleForm
+                shouldUnregister
+                warnWhenUnsavedChanges
+                toolbar={<FormToolbar />}
+                mode="onTouched"
+                defaultValues={{}}
+            >
+                <Grid container width={{ xs: '100%', xl: '80%' }} rowSpacing={3} columnSpacing={3}>
+                    <Grid item xs={12} md={8}>
+                        <Typography variant="h6" gutterBottom>
+                            {translate('common.edit.title.main')}
+                        </Typography>
+                        <Stack direction='row' gap={2}>
+                            <TextInput label="table.field.aiParam.uuid" source="uuid" parse={v => v} disabled />
+                            <TextInput label="table.field.aiParam.name" source="name" parse={v => v} />
+                        </Stack>
+                        <Stack direction='row' gap={2}>
+                            <TextInput label="table.field.aiParam.modelCode" source="modelCode" parse={v => v} />
+                            <SelectInput
+                                label="table.field.aiParam.provider"
+                                source="provider"
+                                choices={providerChoices}
+                            />
+                        </Stack>
+                        <Stack direction='row' gap={2}>
+                            <TextInput
+                                label="table.field.aiParam.modelName"
+                                source="modelName"
+                                parse={v => v}
+                                helperText="濉啓鐪熷疄妯″瀷鍚嶏紝渚嬪 gpt-4o-mini銆乨eepseek-chat"
+                            />
+                            <NumberInput label="table.field.aiParam.maxContextMessages" source="maxContextMessages" />
+                        </Stack>
+                        <Stack direction='row' gap={2}>
+                            <NumberInput label="table.field.aiParam.sort" source="sort" />
+                            <SelectInput
+                                label="table.field.aiParam.defaultFlag"
+                                source="defaultFlag"
+                                choices={yesNoChoices}
+                            />
+                        </Stack>
+                        <Stack direction='row' gap={2}>
+                            <TextInput
+                                label="table.field.aiParam.chatUrl"
+                                source="chatUrl"
+                                parse={v => v}
+                                helperText="鏀寔濉啓 baseUrl锛屽 https://api.openai.com 鎴� https://api.siliconflow.cn锛岀郴缁熶細鑷姩琛ュ叏涓� /v1/chat/completions"
+                                fullWidth
+                            />
+                        </Stack>
+                        <Stack direction='row' gap={2}>
+                            <TextInput
+                                label="table.field.aiParam.apiKey"
+                                source="apiKey"
+                                parse={v => v}
+                                type="password"
+                                helperText="OpenAI 鎺ュ彛妯″紡涓嬪~鍐� Bearer Token锛屾棤闇�鎵嬪姩鍔� Bearer 鍓嶇紑"
+                                fullWidth
+                            />
+                        </Stack>
+                        <Stack direction='row' gap={2}>
+                            <TextInput
+                                label="table.field.aiParam.systemPrompt"
+                                source="systemPrompt"
+                                parse={v => v}
+                                fullWidth
+                                multiline
+                                minRows={5}
+                            />
+                        </Stack>
+                    </Grid>
+                    <Grid item xs={12} md={4}>
+                        <Typography variant="h6" gutterBottom>
+                            {translate('common.edit.title.common')}
+                        </Typography>
+                        <StatusSelectInput />
+                        <MemoInput />
+                    </Grid>
+                </Grid>
+            </SimpleForm>
+        </Edit >
+    )
+}
+
+export default AiParamEdit;
diff --git a/rsf-admin/src/page/system/aiParam/AiParamList.jsx b/rsf-admin/src/page/system/aiParam/AiParamList.jsx
new file mode 100644
index 0000000..8f12399
--- /dev/null
+++ b/rsf-admin/src/page/system/aiParam/AiParamList.jsx
@@ -0,0 +1,120 @@
+import React, { useState } from "react";
+import {
+    List,
+    DatagridConfigurable,
+    SearchInput,
+    TopToolbar,
+    SelectColumnsButton,
+    EditButton,
+    FilterButton,
+    BulkDeleteButton,
+    WrapperField,
+    TextField,
+    NumberField,
+    DateField,
+    BooleanField,
+    TextInput,
+    DateInput,
+    SelectInput,
+    DeleteButton,
+} from 'react-admin';
+import { Box } from '@mui/material';
+import { styled } from '@mui/material/styles';
+import EmptyData from "@/page/components/EmptyData";
+import MyCreateButton from "@/page/components/MyCreateButton";
+import MyExportButton from '@/page/components/MyExportButton';
+import { OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
+import AiParamCreate from "./AiParamCreate";
+
+const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
+    '& .css-1vooibu-MuiSvgIcon-root': {
+        height: '.9em'
+    },
+    '& .RaDatagrid-row': {
+        cursor: 'auto'
+    },
+    '& .opt': {
+        width: 200
+    },
+}));
+
+const filters = [
+    <SearchInput source="condition" alwaysOn />,
+    <DateInput label='common.time.after' source="timeStart" alwaysOn />,
+    <DateInput label='common.time.before' source="timeEnd" alwaysOn />,
+    <TextInput source="name" label="table.field.aiParam.name" />,
+    <TextInput source="modelCode" label="table.field.aiParam.modelCode" />,
+    <TextInput source="provider" label="table.field.aiParam.provider" />,
+    <TextInput source="modelName" label="table.field.aiParam.modelName" />,
+    <SelectInput
+        source="defaultFlag"
+        label="table.field.aiParam.defaultFlag"
+        choices={[
+            { id: '1', name: 'common.enums.true' },
+            { id: '0', name: 'common.enums.false' },
+        ]}
+    />,
+    <TextInput label="common.field.memo" source="memo" />,
+    <SelectInput
+        label="common.field.status"
+        source="status"
+        choices={[
+            { id: '1', name: 'common.enums.statusTrue' },
+            { id: '0', name: 'common.enums.statusFalse' },
+        ]}
+    />,
+]
+
+const AiParamList = () => {
+    const [createDialog, setCreateDialog] = useState(false);
+
+    return (
+        <Box display="flex">
+            <List
+                title={"menu.aiParam"}
+                empty={<EmptyData onClick={() => { setCreateDialog(true) }} />}
+                filters={filters}
+                sort={{ field: "sort", order: "asc" }}
+                actions={(
+                    <TopToolbar>
+                        <FilterButton />
+                        <MyCreateButton onClick={() => { setCreateDialog(true) }} />
+                        <SelectColumnsButton preferenceKey='aiParam' />
+                        <MyExportButton />
+                    </TopToolbar>
+                )}
+                perPage={DEFAULT_PAGE_SIZE}
+            >
+                <StyledDatagrid
+                    preferenceKey='aiParam'
+                    bulkActionButtons={() => <BulkDeleteButton mutationMode={OPERATE_MODE} />}
+                    rowClick={false}
+                    omit={['id', 'createTime', 'memo', 'statusBool', 'defaultFlagBool']}
+                >
+                    <NumberField source="id" />
+                    <TextField source="uuid" label="table.field.aiParam.uuid" />
+                    <TextField source="name" label="table.field.aiParam.name" />
+                    <TextField source="modelCode" label="table.field.aiParam.modelCode" />
+                    <TextField source="provider" label="table.field.aiParam.provider" />
+                    <TextField source="modelName" label="table.field.aiParam.modelName" />
+                    <NumberField source="maxContextMessages" label="table.field.aiParam.maxContextMessages" />
+                    <NumberField source="sort" label="table.field.aiParam.sort" />
+                    <BooleanField source="defaultFlagBool" label="table.field.aiParam.defaultFlag" sortable={false} />
+                    <BooleanField source="statusBool" label="common.field.status" sortable={false} />
+                    <DateField source="updateTime" label="common.field.updateTime" showTime />
+                    <TextField source="memo" label="common.field.memo" sortable={false} />
+                    <WrapperField cellClassName="opt" label="common.field.opt">
+                        <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} />
+                        <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} />
+                    </WrapperField>
+                </StyledDatagrid>
+            </List>
+            <AiParamCreate
+                open={createDialog}
+                setOpen={setCreateDialog}
+            />
+        </Box>
+    )
+}
+
+export default AiParamList;
diff --git a/rsf-admin/src/page/system/aiParam/index.jsx b/rsf-admin/src/page/system/aiParam/index.jsx
new file mode 100644
index 0000000..917e915
--- /dev/null
+++ b/rsf-admin/src/page/system/aiParam/index.jsx
@@ -0,0 +1,15 @@
+import {
+    ShowGuesser,
+} from "react-admin";
+
+import AiParamList from "./AiParamList";
+import AiParamEdit from "./AiParamEdit";
+
+export default {
+    list: AiParamList,
+    edit: AiParamEdit,
+    show: ShowGuesser,
+    recordRepresentation: (record) => {
+        return `${record.name}`
+    }
+};
diff --git a/rsf-ai-gateway/gateway-run.log b/rsf-ai-gateway/gateway-run.log
new file mode 100644
index 0000000..23a675c
--- /dev/null
+++ b/rsf-ai-gateway/gateway-run.log
@@ -0,0 +1,60 @@
+[INFO] Scanning for projects...
+[WARNING] 
+[WARNING] Some problems were encountered while building the effective model for com.vincent:rsf-server:jar:1.0.0
+[WARNING] 'dependencies.dependency.systemPath' for RouteUtils:RouteUtils:jar should not point at files within the project directory, ${project.basedir}/src/main/resources/lib/RouteUtils.jar will be unresolvable by dependent projects @ line 41, column 16
+[WARNING] 
+[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.
+[WARNING] 
+[WARNING] For this reason, future Maven versions might no longer support building such malformed projects.
+[WARNING] 
+[INFO] 
+[INFO] ---------------------< com.vincent:rsf-ai-gateway >---------------------
+[INFO] Building rsf-ai-gateway 1.0.0
+[INFO]   from pom.xml
+[INFO] --------------------------------[ jar ]---------------------------------
+[INFO] 
+[INFO] >>> spring-boot:2.5.3:run (default-cli) > test-compile @ rsf-ai-gateway >>>
+[INFO] 
+[INFO] --- resources:3.2.0:resources (default-resources) @ rsf-ai-gateway ---
+[INFO] Using 'UTF-8' encoding to copy filtered resources.
+[INFO] Using 'UTF-8' encoding to copy filtered properties files.
+[INFO] Copying 1 resource
+[INFO] Copying 0 resource
+[INFO] 
+[INFO] --- compiler:3.8.1:compile (default-compile) @ rsf-ai-gateway ---
+[INFO] Nothing to compile - all classes are up to date
+[INFO] 
+[INFO] --- resources:3.2.0:testResources (default-testResources) @ rsf-ai-gateway ---
+[INFO] Using 'UTF-8' encoding to copy filtered resources.
+[INFO] Using 'UTF-8' encoding to copy filtered properties files.
+[INFO] skip non existing resourceDirectory C:\env\code\wms-master\rsf-ai-gateway\src\test\resources
+[INFO] 
+[INFO] --- compiler:3.8.1:testCompile (default-testCompile) @ rsf-ai-gateway ---
+[INFO] No sources to compile
+[INFO] 
+[INFO] <<< spring-boot:2.5.3:run (default-cli) < test-compile @ rsf-ai-gateway <<<
+[INFO] 
+[INFO] 
+[INFO] --- spring-boot:2.5.3:run (default-cli) @ rsf-ai-gateway ---
+[INFO] Attaching agents: []
+
+  .   ____          _            __ _ _
+ /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
+( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
+ \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
+  '  |____| .__|_| |_|_| |_\__, | / / / /
+ =========|_|==============|___/=/_/_/_/
+ :: Spring Boot ::                (v2.5.3)
+
+2026-03-11 15:04:08.306  INFO 23404 --- [           main] com.vincent.rsf.ai.gateway.GatewayBoot   : Starting GatewayBoot using Java 17.0.14 on WIN-P7MH6EA0OTE with PID 23404 (C:\env\code\wms-master\rsf-ai-gateway\target\classes started by Administrator in C:\env\code\wms-master\rsf-ai-gateway)
+2026-03-11 15:04:08.307  INFO 23404 --- [           main] com.vincent.rsf.ai.gateway.GatewayBoot   : No active profile set, falling back to default profiles: default
+2026-03-11 15:04:08.788  INFO 23404 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8086 (http)
+2026-03-11 15:04:08.795  INFO 23404 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
+2026-03-11 15:04:08.795  INFO 23404 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.50]
+2026-03-11 15:04:08.835  INFO 23404 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
+2026-03-11 15:04:08.835  INFO 23404 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 500 ms
+2026-03-11 15:04:09.014  INFO 23404 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8086 (http) with context path ''
+2026-03-11 15:04:09.019  INFO 23404 --- [           main] com.vincent.rsf.ai.gateway.GatewayBoot   : Started GatewayBoot in 0.93 seconds (JVM running for 1.099)
+2026-03-11 15:04:33.345  INFO 23404 --- [nio-8086-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
+2026-03-11 15:04:33.345  INFO 23404 --- [nio-8086-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
+2026-03-11 15:04:33.346  INFO 23404 --- [nio-8086-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
diff --git a/rsf-ai-gateway/pom.xml b/rsf-ai-gateway/pom.xml
new file mode 100644
index 0000000..77581f5
--- /dev/null
+++ b/rsf-ai-gateway/pom.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <name>rsf-ai-gateway</name>
+    <artifactId>rsf-ai-gateway</artifactId>
+    <version>1.0.0</version>
+    <packaging>jar</packaging>
+
+    <parent>
+        <groupId>com.vincent</groupId>
+        <artifactId>rsf</artifactId>
+        <version>1.0.0</version>
+    </parent>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>rsf-ai-gateway</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/GatewayBoot.java b/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/GatewayBoot.java
new file mode 100644
index 0000000..652213e
--- /dev/null
+++ b/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/GatewayBoot.java
@@ -0,0 +1,18 @@
+package com.vincent.rsf.ai.gateway;
+
+import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.SpringApplication;
+
+@SpringBootApplication(exclude = {
+        DataSourceAutoConfiguration.class,
+        DruidDataSourceAutoConfigure.class
+})
+public class GatewayBoot {
+
+    public static void main(String[] args) {
+        SpringApplication.run(GatewayBoot.class, args);
+    }
+
+}
diff --git a/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/config/AiGatewayProperties.java b/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/config/AiGatewayProperties.java
new file mode 100644
index 0000000..0b7fcd4
--- /dev/null
+++ b/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/config/AiGatewayProperties.java
@@ -0,0 +1,34 @@
+package com.vincent.rsf.ai.gateway.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "gateway.ai")
+public class AiGatewayProperties {
+
+    private String defaultModelCode = "mock-general";
+
+    private Integer connectTimeoutMillis = 10000;
+
+    private Integer readTimeoutMillis = 0;
+
+    private List<ModelConfig> models = new ArrayList<>();
+
+    @Data
+    public static class ModelConfig {
+        private String code;
+        private String name;
+        private String provider = "mock";
+        private String chatUrl;
+        private String apiKey;
+        private String modelName;
+        private Boolean enabled = true;
+    }
+
+}
diff --git a/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/controller/AiGatewayController.java b/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/controller/AiGatewayController.java
new file mode 100644
index 0000000..f8842d9
--- /dev/null
+++ b/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/controller/AiGatewayController.java
@@ -0,0 +1,42 @@
+package com.vincent.rsf.ai.gateway.controller;
+
+import com.vincent.rsf.ai.gateway.dto.GatewayChatRequest;
+import com.vincent.rsf.ai.gateway.service.AiGatewayService;
+import com.vincent.rsf.ai.gateway.service.GatewayStreamEvent;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
+
+import javax.annotation.Resource;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+@RestController
+@RequestMapping("/internal/chat")
+public class AiGatewayController {
+
+    @Resource
+    private AiGatewayService aiGatewayService;
+    @Resource
+    private ObjectMapper objectMapper;
+
+    @PostMapping(value = "/stream", produces = "application/x-ndjson")
+    public StreamingResponseBody stream(@RequestBody GatewayChatRequest request) {
+        return outputStream -> {
+            try {
+                aiGatewayService.stream(request, event -> {
+                    String json = objectMapper.writeValueAsString(event) + "\n";
+                    outputStream.write(json.getBytes(StandardCharsets.UTF_8));
+                    outputStream.flush();
+                });
+            } catch (Exception e) {
+                throw new IOException(e);
+            }
+        };
+    }
+
+}
diff --git a/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/dto/GatewayChatMessage.java b/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/dto/GatewayChatMessage.java
new file mode 100644
index 0000000..fcf8f78
--- /dev/null
+++ b/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/dto/GatewayChatMessage.java
@@ -0,0 +1,14 @@
+package com.vincent.rsf.ai.gateway.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class GatewayChatMessage implements Serializable {
+
+    private String role;
+
+    private String content;
+
+}
diff --git a/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/dto/GatewayChatRequest.java b/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/dto/GatewayChatRequest.java
new file mode 100644
index 0000000..cf105b9
--- /dev/null
+++ b/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/dto/GatewayChatRequest.java
@@ -0,0 +1,26 @@
+package com.vincent.rsf.ai.gateway.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class GatewayChatRequest implements Serializable {
+
+    private String sessionId;
+
+    private String modelCode;
+
+    private String systemPrompt;
+
+    private String chatUrl;
+
+    private String apiKey;
+
+    private String modelName;
+
+    private List<GatewayChatMessage> messages = new ArrayList<>();
+
+}
diff --git a/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/service/AiGatewayService.java b/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/service/AiGatewayService.java
new file mode 100644
index 0000000..da04faa
--- /dev/null
+++ b/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/service/AiGatewayService.java
@@ -0,0 +1,267 @@
+package com.vincent.rsf.ai.gateway.service;
+
+import com.vincent.rsf.ai.gateway.config.AiGatewayProperties;
+import com.vincent.rsf.ai.gateway.dto.GatewayChatMessage;
+import com.vincent.rsf.ai.gateway.dto.GatewayChatRequest;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class AiGatewayService {
+
+    @Resource
+    private AiGatewayProperties aiGatewayProperties;
+    @Resource
+    private ObjectMapper objectMapper;
+
+    public interface EventConsumer {
+        void accept(GatewayStreamEvent event) throws Exception;
+    }
+
+    public void stream(GatewayChatRequest request, EventConsumer consumer) throws Exception {
+        AiGatewayProperties.ModelConfig modelConfig = resolveModel(request);
+        if (modelConfig == null || modelConfig.getChatUrl() == null || modelConfig.getChatUrl().trim().isEmpty()) {
+            mockStream(request, modelConfig, consumer);
+            return;
+        }
+        openAiCompatibleStream(request, modelConfig, consumer);
+    }
+
+    private AiGatewayProperties.ModelConfig resolveModel(GatewayChatRequest request) {
+        String modelCode = request.getModelCode();
+        String targetCode = (modelCode == null || modelCode.trim().isEmpty())
+                ? aiGatewayProperties.getDefaultModelCode()
+                : modelCode;
+        for (AiGatewayProperties.ModelConfig model : aiGatewayProperties.getModels()) {
+            if (Boolean.TRUE.equals(model.getEnabled()) && targetCode.equals(model.getCode())) {
+                return mergeRequestOverride(model, request);
+            }
+        }
+        if ((request.getChatUrl() != null && !request.getChatUrl().trim().isEmpty())
+                || (request.getModelName() != null && !request.getModelName().trim().isEmpty())) {
+            AiGatewayProperties.ModelConfig modelConfig = new AiGatewayProperties.ModelConfig();
+            modelConfig.setCode(targetCode);
+            modelConfig.setName(targetCode);
+            modelConfig.setProvider("custom");
+            modelConfig.setEnabled(true);
+            return mergeRequestOverride(modelConfig, request);
+        }
+        return null;
+    }
+
+    private AiGatewayProperties.ModelConfig mergeRequestOverride(AiGatewayProperties.ModelConfig source,
+                                                                 GatewayChatRequest request) {
+        AiGatewayProperties.ModelConfig target = new AiGatewayProperties.ModelConfig();
+        target.setCode(source.getCode());
+        target.setName(source.getName());
+        target.setProvider(source.getProvider());
+        target.setChatUrl(normalizeChatUrl(source.getChatUrl()));
+        target.setApiKey(source.getApiKey());
+        target.setModelName(source.getModelName());
+        target.setEnabled(source.getEnabled());
+        if (request.getChatUrl() != null && !request.getChatUrl().trim().isEmpty()) {
+            target.setChatUrl(normalizeChatUrl(request.getChatUrl().trim()));
+        }
+        if (request.getApiKey() != null && !request.getApiKey().trim().isEmpty()) {
+            target.setApiKey(request.getApiKey().trim());
+        }
+        if (request.getModelName() != null && !request.getModelName().trim().isEmpty()) {
+            target.setModelName(request.getModelName().trim());
+        }
+        return target;
+    }
+
+    private void mockStream(GatewayChatRequest request, AiGatewayProperties.ModelConfig modelConfig,
+                            EventConsumer consumer) throws Exception {
+        String modelCode = modelConfig == null ? aiGatewayProperties.getDefaultModelCode() : modelConfig.getCode();
+        String lastQuestion = "";
+        List<GatewayChatMessage> messages = request.getMessages();
+        for (int i = messages.size() - 1; i >= 0; i--) {
+            GatewayChatMessage message = messages.get(i);
+            if ("user".equalsIgnoreCase(message.getRole())) {
+                lastQuestion = message.getContent();
+                break;
+            }
+        }
+        String answer = "褰撳墠涓烘紨绀烘ā寮忥紝妯″瀷[" + modelCode + "]宸叉敹鍒颁綘鐨勯棶棰橈細" + lastQuestion;
+        for (char c : answer.toCharArray()) {
+            consumer.accept(new GatewayStreamEvent()
+                    .setType("delta")
+                    .setModelCode(modelCode)
+                    .setContent(String.valueOf(c)));
+            Thread.sleep(20L);
+        }
+        consumer.accept(new GatewayStreamEvent()
+                .setType("done")
+                .setModelCode(modelCode));
+    }
+
+    private void openAiCompatibleStream(GatewayChatRequest request, AiGatewayProperties.ModelConfig modelConfig,
+                                        EventConsumer consumer) throws Exception {
+        HttpURLConnection connection = null;
+        try {
+            connection = (HttpURLConnection) new URL(modelConfig.getChatUrl()).openConnection();
+            connection.setConnectTimeout(aiGatewayProperties.getConnectTimeoutMillis());
+            connection.setReadTimeout(aiGatewayProperties.getReadTimeoutMillis());
+            connection.setRequestMethod("POST");
+            connection.setDoOutput(true);
+            connection.setRequestProperty("Content-Type", "application/json");
+            connection.setRequestProperty("Accept", "text/event-stream");
+            if (modelConfig.getApiKey() != null && !modelConfig.getApiKey().trim().isEmpty()) {
+                connection.setRequestProperty("Authorization", "Bearer " + modelConfig.getApiKey().trim());
+            }
+
+            Map<String, Object> body = new LinkedHashMap<>();
+            body.put("model", modelConfig.getModelName());
+            body.put("stream", true);
+            body.put("messages", buildMessages(request));
+
+            try (OutputStream outputStream = connection.getOutputStream()) {
+                outputStream.write(objectMapper.writeValueAsBytes(body));
+                outputStream.flush();
+            }
+
+            int statusCode = connection.getResponseCode();
+            InputStream inputStream = statusCode >= 400 ? connection.getErrorStream() : connection.getInputStream();
+            if (inputStream == null) {
+                consumer.accept(new GatewayStreamEvent()
+                        .setType("error")
+                        .setModelCode(modelConfig.getCode())
+                        .setMessage("妯″瀷鏈嶅姟鏃犲搷搴�"));
+                return;
+            }
+            if (statusCode >= 400) {
+                consumer.accept(new GatewayStreamEvent()
+                        .setType("error")
+                        .setModelCode(modelConfig.getCode())
+                        .setMessage(readErrorMessage(inputStream, statusCode)));
+                return;
+            }
+
+            try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    if (line.trim().isEmpty() || !line.startsWith("data:")) {
+                        continue;
+                    }
+                    String payload = line.substring(5).trim();
+                    if ("[DONE]".equals(payload)) {
+                        consumer.accept(new GatewayStreamEvent()
+                                .setType("done")
+                                .setModelCode(modelConfig.getCode()));
+                        break;
+                    }
+                    JsonNode root = objectMapper.readTree(payload);
+                    JsonNode choice = root.path("choices").path(0);
+                    JsonNode delta = choice.path("delta");
+                    JsonNode contentNode = delta.path("content");
+                    if (!contentNode.isMissingNode() && !contentNode.isNull()) {
+                        consumer.accept(new GatewayStreamEvent()
+                                .setType("delta")
+                                .setModelCode(modelConfig.getCode())
+                                .setContent(contentNode.asText()));
+                    }
+                    JsonNode finishReason = choice.path("finish_reason");
+                    if (!finishReason.isMissingNode() && !finishReason.isNull()) {
+                        consumer.accept(new GatewayStreamEvent()
+                                .setType("done")
+                                .setModelCode(modelConfig.getCode()));
+                        break;
+                    }
+                }
+            }
+        } catch (Exception e) {
+            consumer.accept(new GatewayStreamEvent()
+                    .setType("error")
+                    .setModelCode(modelConfig.getCode())
+                    .setMessage(e.getMessage()));
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    private List<Map<String, String>> buildMessages(GatewayChatRequest request) {
+        List<Map<String, String>> output = new ArrayList<>();
+        if (request.getSystemPrompt() != null && !request.getSystemPrompt().trim().isEmpty()) {
+            Map<String, String> systemMessage = new LinkedHashMap<>();
+            systemMessage.put("role", "system");
+            systemMessage.put("content", request.getSystemPrompt());
+            output.add(systemMessage);
+        }
+        for (GatewayChatMessage message : request.getMessages()) {
+            Map<String, String> item = new LinkedHashMap<>();
+            item.put("role", message.getRole());
+            item.put("content", message.getContent());
+            output.add(item);
+        }
+        return output;
+    }
+
+    private String normalizeChatUrl(String chatUrl) {
+        if (chatUrl == null) {
+            return null;
+        }
+        String normalized = chatUrl.trim();
+        if (normalized.isEmpty()) {
+            return normalized;
+        }
+        if (normalized.endsWith("/chat/completions") || normalized.endsWith("/v1/chat/completions")) {
+            return normalized;
+        }
+        if (normalized.endsWith("/v1")) {
+            return normalized + "/chat/completions";
+        }
+        if (normalized.contains("/v1/")) {
+            return normalized;
+        }
+        if (normalized.endsWith("/")) {
+            return normalized + "v1/chat/completions";
+        }
+        return normalized + "/v1/chat/completions";
+    }
+
+    private String readErrorMessage(InputStream inputStream, int statusCode) {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
+            StringBuilder builder = new StringBuilder();
+            String line;
+            while ((line = reader.readLine()) != null) {
+                builder.append(line);
+            }
+            String body = builder.toString();
+            if (body.isEmpty()) {
+                return "妯″瀷鏈嶅姟璋冪敤澶辫触锛岀姸鎬佺爜锛�" + statusCode;
+            }
+            JsonNode root = objectMapper.readTree(body);
+            JsonNode errorNode = root.path("error");
+            if (!errorNode.isMissingNode() && !errorNode.isNull()) {
+                String message = errorNode.path("message").asText("");
+                if (!message.isEmpty()) {
+                    return message;
+                }
+            }
+            if (root.path("message").isTextual()) {
+                return root.path("message").asText();
+            }
+            return body;
+        } catch (Exception ignore) {
+            return "妯″瀷鏈嶅姟璋冪敤澶辫触锛岀姸鎬佺爜锛�" + statusCode;
+        }
+    }
+
+}
diff --git a/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/service/GatewayStreamEvent.java b/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/service/GatewayStreamEvent.java
new file mode 100644
index 0000000..7ea9d37
--- /dev/null
+++ b/rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/service/GatewayStreamEvent.java
@@ -0,0 +1,20 @@
+package com.vincent.rsf.ai.gateway.service;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+@Data
+@Accessors(chain = true)
+public class GatewayStreamEvent implements Serializable {
+
+    private String type;
+
+    private String content;
+
+    private String message;
+
+    private String modelCode;
+
+}
diff --git a/rsf-ai-gateway/src/main/resources/application.yml b/rsf-ai-gateway/src/main/resources/application.yml
new file mode 100644
index 0000000..78797bf
--- /dev/null
+++ b/rsf-ai-gateway/src/main/resources/application.yml
@@ -0,0 +1,23 @@
+server:
+  port: 8086
+
+spring:
+  application:
+    name: rsf-ai-gateway
+
+gateway:
+  ai:
+    default-model-code: mock-general
+    connect-timeout-millis: 10000
+    read-timeout-millis: 0
+    models:
+      - code: mock-general
+        name: Mock General
+        provider: mock
+        model-name: mock-general
+        enabled: true
+      - code: mock-creative
+        name: Mock Creative
+        provider: mock
+        model-name: mock-creative
+        enabled: true
diff --git a/rsf-server/skills/rsf-server-maintainer/SKILL.md b/rsf-server/skills/rsf-server-maintainer/SKILL.md
new file mode 100644
index 0000000..93fef0e
--- /dev/null
+++ b/rsf-server/skills/rsf-server-maintainer/SKILL.md
@@ -0,0 +1,98 @@
+---
+name: rsf-server-maintainer
+description: Maintain and extend the Java Spring Boot rsf-server module in wms-master. Use when tasks involve reading or changing API/controller logic, manager or system domain services, MyBatis mapper XML SQL, security permissions, profile configuration, or module-level build checks that include rsf-server and its sibling modules.
+---
+
+# Rsf Server Maintainer
+
+## Overview
+
+Use this skill to make safe, minimal, and consistent backend changes in `rsf-server`.
+Follow the repository's existing controller -> service -> mapper -> XML flow and validate changes before finishing.
+
+## Workflow
+
+1. Identify the domain before editing.
+- Use `api` for external integration endpoints (`erp`, `mes`, `mcp`, `pda`, `wcs`).
+- Use `manager` for warehouse business CRUD and workflows.
+- Use `system` for auth, menu, role, tenant, and shared platform settings.
+
+2. Locate related files quickly.
+- Run `python skills/rsf-server-maintainer/scripts/locate_module.py <keyword>`.
+- If needed, use `rg`:
+```powershell
+rg -n "<keyword>" src/main/java/com/vincent/rsf/server
+rg -n "<keyword>" src/main/resources/mapper
+```
+
+3. Apply the minimum consistent change set.
+- For endpoint changes, update controller method, request/response model, and service call together.
+- For data access changes, keep mapper interface method signatures and XML `namespace`/`id` aligned.
+- For entity schema changes, update entity annotations and dependent params/dto wrappers in the same pass.
+- Preserve existing response style (`R.ok().add(...)`, `R.error(...)`) and method-level `@PreAuthorize`.
+
+4. Validate before finishing.
+- Run targeted checks with `rg` to confirm all required symbols and paths exist.
+- Build from workspace root (`wms-master`) so parent modules resolve:
+```powershell
+mvn -pl rsf-server -am -DskipTests package
+```
+- If full build is heavy, run at least:
+```powershell
+mvn -pl rsf-server -am -DskipTests compile
+```
+
+## Repository Conventions
+
+1. Keep package boundaries stable.
+- Java root is `src/main/java/com/vincent/rsf/server`.
+- Mapper XML root is `src/main/resources/mapper/{manager,system}`.
+- `mybatis-plus.mapper-locations` uses `classpath:mapper/*/*.xml`.
+
+2. Follow existing layered patterns.
+- Most services use `IService<T>` + `ServiceImpl<Mapper, Entity>`.
+- Many mapper interfaces extend `BaseMapper<T>`.
+- Controller methods often extend `BaseController` helpers (`buildParam`, `getLoginUserId`).
+
+3. Respect security and tenancy behavior.
+- Public endpoint whitelist lives in `common/security/SecurityConfig.java` (`FILTER_PATH`).
+- Internal auth checks use `@PreAuthorize` on controller methods.
+- Tenant filtering is enforced by `common/config/MybatisPlusConfig.java`; avoid bypassing tenant constraints accidentally.
+
+4. Keep runtime assumptions unchanged unless requested.
+- Active profile is selected in `src/main/resources/application.yml`.
+- Project uses a local system-scope jar at `src/main/resources/lib/RouteUtils.jar`.
+- Keep existing text literals and encoding style unless the task explicitly asks for normalization.
+
+5. Keep encoding and diff size stable.
+- Do not change file encoding unless explicitly requested.
+- Keep edited files in UTF-8.
+- Prefer the smallest possible code change that satisfies the requirement.
+
+## Change Playbooks
+
+1. Add or update an endpoint.
+- Locate controller by route keyword.
+- Confirm service method exists; add/update in interface and impl together.
+- Add authorization annotation when endpoint is not in public `FILTER_PATH`.
+
+2. Add or update SQL.
+- Prefer `LambdaQueryWrapper` when simple CRUD is enough.
+- If XML is required, update mapper interface and XML in the same edit.
+- Keep XML `namespace` equal to mapper interface FQCN.
+
+3. Add or update an entity field.
+- Update entity with MyBatis annotations (`@TableField`, `@TableLogic`, `typeHandler`) as needed.
+- Update request params/dto and controller mapping logic if the field crosses API boundaries.
+
+## Resources
+
+1. Use `references/repo-map.md` for fast path lookup and command snippets.
+2. Use `scripts/locate_module.py` to locate likely controller/service/mapper/entity/XML files by keyword.
+
+## Done Criteria
+
+1. Keep code changes scoped to the requested behavior.
+2. Keep controller/service/mapper/XML references consistent.
+3. Confirm security and tenant behavior remain coherent.
+4. Run at least one build or compile command, or explain why it could not run.
diff --git a/rsf-server/skills/rsf-server-maintainer/agents/openai.yaml b/rsf-server/skills/rsf-server-maintainer/agents/openai.yaml
new file mode 100644
index 0000000..363026c
--- /dev/null
+++ b/rsf-server/skills/rsf-server-maintainer/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+  display_name: "RSF Server Maintainer"
+  short_description: "Maintain RSF server backend workflows"
+  default_prompt: "Use $rsf-server-maintainer to analyze and safely modify the RSF server backend codebase."
diff --git a/rsf-server/skills/rsf-server-maintainer/references/repo-map.md b/rsf-server/skills/rsf-server-maintainer/references/repo-map.md
new file mode 100644
index 0000000..6c7ba72
--- /dev/null
+++ b/rsf-server/skills/rsf-server-maintainer/references/repo-map.md
@@ -0,0 +1,69 @@
+# RSF Server Repo Map
+
+## 1. Module Layout
+
+- Workspace root: `C:/env/code/wms-master`
+- Parent pom: `pom.xml` (modules: `rsf-common`, `rsf-framework`, `rsf-server`, `rsf-open-api`)
+- Server module: `rsf-server`
+- Server boot class: `rsf-server/src/main/java/com/vincent/rsf/server/ServerBoot.java`
+
+## 2. Core Paths Inside rsf-server
+
+- Java root: `src/main/java/com/vincent/rsf/server`
+- Integration API controllers: `src/main/java/com/vincent/rsf/server/api/controller`
+- Warehouse business domain: `src/main/java/com/vincent/rsf/server/manager`
+- Platform/system domain: `src/main/java/com/vincent/rsf/server/system`
+- Shared infrastructure/config/security/utils: `src/main/java/com/vincent/rsf/server/common`
+- Mapper XML root: `src/main/resources/mapper`
+- Manager mapper XML: `src/main/resources/mapper/manager`
+- System mapper XML: `src/main/resources/mapper/system`
+- Runtime config: `src/main/resources/application*.yml`
+
+## 3. Observed Size (for search strategy)
+
+- `manager/controller`: about 107 files
+- `api/controller`: about 46 files
+- `system/controller`: about 38 files
+- `manager/service/impl`: about 68 files
+- `mapper/manager`: about 67 XML files
+- `mapper/system`: about 30 XML files
+
+Use keyword-first narrowing before opening files.
+
+## 4. Request and Data Flow Pattern
+
+1. Controller receives request, often returns `R.ok().add(...)` or `R.error(...)`.
+2. Service interface in `service/` and implementation in `service/impl/`.
+3. Mapper interface in `mapper/` extends `BaseMapper<T>`.
+4. Optional custom SQL in mapper XML with matching `namespace` and statement `id`.
+5. Entity annotations (`@TableName`, `@TableField`, `@TableLogic`) define persistence behavior.
+
+## 5. Security and Tenancy Anchors
+
+- Public route whitelist: `common/security/SecurityConfig.java` (`FILTER_PATH`)
+- Method-level permission: `@PreAuthorize(...)`
+- Tenant interceptor: `common/config/MybatisPlusConfig.java`
+
+Check these files whenever endpoint visibility or cross-tenant behavior changes.
+
+## 6. Useful Commands
+
+```powershell
+# Find files by keyword in server Java
+rg -n "<keyword>" rsf-server/src/main/java/com/vincent/rsf/server
+
+# Find SQL/XML references
+rg -n "<keyword>" rsf-server/src/main/resources/mapper
+
+# Build rsf-server with dependent modules from workspace root
+mvn -pl rsf-server -am -DskipTests compile
+
+# Full package build
+mvn -pl rsf-server -am -DskipTests package
+```
+
+## 7. Constraints and Notes
+
+- No `src/test` directory is currently present in `rsf-server`.
+- `rsf-server` depends on sibling modules; run Maven from workspace root for reliable resolution.
+- A local system-scope jar is referenced at `rsf-server/src/main/resources/lib/RouteUtils.jar`.
diff --git a/rsf-server/skills/rsf-server-maintainer/scripts/locate_module.py b/rsf-server/skills/rsf-server-maintainer/scripts/locate_module.py
new file mode 100644
index 0000000..8805eb6
--- /dev/null
+++ b/rsf-server/skills/rsf-server-maintainer/scripts/locate_module.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python3
+"""Locate likely RSF backend files for a feature/module keyword.
+
+Usage:
+    python skills/rsf-server-maintainer/scripts/locate_module.py basStation
+    python skills/rsf-server-maintainer/scripts/locate_module.py order --repo C:/env/code/wms-master/rsf-server
+"""
+
+from __future__ import annotations
+
+import argparse
+import re
+from pathlib import Path
+from typing import Iterable, List, Tuple
+
+
+def parse_args() -> argparse.Namespace:
+    parser = argparse.ArgumentParser(
+        description="Locate related controller/service/mapper/entity/XML files by keyword.")
+    parser.add_argument("keyword", help="Module keyword, for example: basStation, order, user")
+    parser.add_argument(
+        "--repo",
+        default=str(Path(__file__).resolve().parents[3]),
+        help="Path to rsf-server repository root (default: auto-detect from script location)",
+    )
+    parser.add_argument(
+        "--limit",
+        type=int,
+        default=80,
+        help="Max results per section (default: 80)",
+    )
+    return parser.parse_args()
+
+
+def normalize_keyword(value: str) -> str:
+    return value.strip().lower()
+
+
+def collect_files(root: Path, patterns: Iterable[str]) -> List[Path]:
+    files: List[Path] = []
+    for pattern in patterns:
+        files.extend(root.glob(pattern))
+    return [p for p in files if p.is_file()]
+
+
+def find_name_matches(files: Iterable[Path], keyword: str) -> List[Path]:
+    matches = []
+    for file in files:
+        text = str(file).lower()
+        if keyword in text:
+            matches.append(file)
+    return sorted(set(matches))
+
+
+def find_line_matches(files: Iterable[Path], keyword: str, pattern: re.Pattern[str]) -> List[Tuple[Path, int, str]]:
+    out: List[Tuple[Path, int, str]] = []
+    for file in files:
+        try:
+            content = file.read_text(encoding="utf-8", errors="ignore").splitlines()
+        except OSError:
+            continue
+        for index, line in enumerate(content, start=1):
+            lower = line.lower()
+            if keyword in lower and pattern.search(line):
+                out.append((file, index, line.strip()))
+    return out
+
+
+def print_file_section(title: str, repo: Path, files: List[Path], limit: int) -> None:
+    print(f"\n[{title}] {len(files)}")
+    for path in files[:limit]:
+        rel = path.relative_to(repo)
+        print(f"  - {rel.as_posix()}")
+    if len(files) > limit:
+        print(f"  ... ({len(files) - limit} more)")
+
+
+def print_line_section(title: str, repo: Path, rows: List[Tuple[Path, int, str]], limit: int) -> None:
+    print(f"\n[{title}] {len(rows)}")
+    for file, line_no, line in rows[:limit]:
+        rel = file.relative_to(repo)
+        print(f"  - {rel.as_posix()}:{line_no}: {line}")
+    if len(rows) > limit:
+        print(f"  ... ({len(rows) - limit} more)")
+
+
+def main() -> int:
+    args = parse_args()
+    repo = Path(args.repo).resolve()
+    keyword = normalize_keyword(args.keyword)
+
+    if not keyword:
+        raise SystemExit("keyword must not be empty")
+
+    java_root = repo / "src/main/java/com/vincent/rsf/server"
+    mapper_root = repo / "src/main/resources/mapper"
+
+    if not java_root.exists() or not mapper_root.exists():
+        raise SystemExit(f"Invalid repo root for rsf-server: {repo}")
+
+    java_files = collect_files(
+        repo,
+        [
+            "src/main/java/com/vincent/rsf/server/**/*.java",
+            "src/main/resources/mapper/**/*.xml",
+        ],
+    )
+
+    name_matches = find_name_matches(java_files, keyword)
+
+    controller_files = collect_files(repo, ["src/main/java/com/vincent/rsf/server/**/controller/**/*.java"])
+    mapping_pattern = re.compile(r"@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping)")
+    authority_pattern = re.compile(r"@PreAuthorize")
+
+    endpoint_rows = find_line_matches(controller_files, keyword, mapping_pattern)
+    authority_rows = find_line_matches(controller_files, keyword, authority_pattern)
+
+    print(f"repo: {repo}")
+    print(f"keyword: {keyword}")
+
+    print_file_section("Path matches", repo, name_matches, args.limit)
+    print_line_section("Endpoint annotation matches", repo, endpoint_rows, args.limit)
+    print_line_section("Authority annotation matches", repo, authority_rows, args.limit)
+
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiProperties.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiProperties.java
new file mode 100644
index 0000000..751d469
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiProperties.java
@@ -0,0 +1,47 @@
+package com.vincent.rsf.server.ai.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "ai")
+public class AiProperties {
+
+    private String gatewayBaseUrl = "http://127.0.0.1:8086";
+
+    private Integer sessionTtlSeconds = 86400;
+
+    private Integer maxContextMessages = 12;
+
+    private String systemPrompt = "浣犳槸WMS绯荤粺鍐呯殑鏅鸿兘鍔╂墜锛屽洖绛旀椂浼樺厛淇濇寔鍑嗙‘銆佺畝娲侊紝骞剁粨鍚堜笂涓嬫枃甯姪鐢ㄦ埛鐞嗚В浠撳偍涓氬姟銆�";
+
+    private String defaultModelCode = "mock-general";
+
+    private List<ModelConfig> models = new ArrayList<>();
+
+    public List<ModelConfig> getEnabledModels() {
+        return models.stream().filter(model -> Boolean.TRUE.equals(model.getEnabled())).collect(Collectors.toList());
+    }
+
+    public String resolveDefaultModelCode() {
+        if (defaultModelCode != null && !defaultModelCode.trim().isEmpty()) {
+            return defaultModelCode;
+        }
+        return getEnabledModels().isEmpty() ? "mock-general" : getEnabledModels().get(0).getCode();
+    }
+
+    @Data
+    public static class ModelConfig {
+        private String code;
+        private String name;
+        private String provider;
+        private Boolean enabled = true;
+    }
+
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiController.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiController.java
new file mode 100644
index 0000000..3768be3
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiController.java
@@ -0,0 +1,260 @@
+package com.vincent.rsf.server.ai.controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.vincent.rsf.framework.common.R;
+import com.vincent.rsf.server.ai.config.AiProperties;
+import com.vincent.rsf.server.ai.dto.AiChatStreamRequest;
+import com.vincent.rsf.server.ai.dto.AiSessionCreateRequest;
+import com.vincent.rsf.server.ai.dto.AiSessionRenameRequest;
+import com.vincent.rsf.server.ai.dto.GatewayChatMessage;
+import com.vincent.rsf.server.ai.dto.GatewayChatRequest;
+import com.vincent.rsf.server.ai.model.AiChatMessage;
+import com.vincent.rsf.server.ai.model.AiChatSession;
+import com.vincent.rsf.server.ai.model.AiPromptContext;
+import com.vincent.rsf.server.ai.service.AiGatewayClient;
+import com.vincent.rsf.server.ai.service.AiPromptContextService;
+import com.vincent.rsf.server.ai.service.AiRuntimeConfigService;
+import com.vincent.rsf.server.ai.service.AiSessionService;
+import com.vincent.rsf.server.system.controller.BaseController;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import javax.annotation.Resource;
+import java.io.IOException;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/ai")
+public class AiController extends BaseController {
+
+    @Resource
+    private AiSessionService aiSessionService;
+    @Resource
+    private AiProperties aiProperties;
+    @Resource
+    private AiGatewayClient aiGatewayClient;
+    @Resource
+    private AiRuntimeConfigService aiRuntimeConfigService;
+    @Resource
+    private AiPromptContextService aiPromptContextService;
+
+    @GetMapping("/model/list")
+    public R modelList() {
+        List<Map<String, Object>> models = new java.util.ArrayList<>();
+        for (AiRuntimeConfigService.ModelRuntimeConfig model : aiRuntimeConfigService.listEnabledModels()) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("code", model.getCode());
+            item.put("name", model.getName());
+            item.put("provider", model.getProvider());
+            item.put("enabled", model.getEnabled());
+            models.add(item);
+        }
+        return R.ok().add(models);
+    }
+
+    @GetMapping("/session/list")
+    public R sessionList() {
+        return R.ok().add(aiSessionService.listSessions(getTenantId(), getLoginUserId()));
+    }
+
+    @PostMapping("/session/create")
+    public R createSession(@RequestBody(required = false) AiSessionCreateRequest request) {
+        AiChatSession session = aiSessionService.createSession(
+                getTenantId(),
+                getLoginUserId(),
+                request == null ? null : request.getTitle(),
+                request == null ? null : request.getModelCode()
+        );
+        return R.ok().add(session);
+    }
+
+    @PostMapping("/session/{sessionId}/rename")
+    public R renameSession(@PathVariable("sessionId") String sessionId, @RequestBody AiSessionRenameRequest request) {
+        AiChatSession session = aiSessionService.renameSession(getTenantId(), getLoginUserId(), sessionId, request.getTitle());
+        return R.ok().add(session);
+    }
+
+    @PostMapping("/session/remove/{sessionId}")
+    public R removeSession(@PathVariable("sessionId") String sessionId) {
+        aiSessionService.removeSession(getTenantId(), getLoginUserId(), sessionId);
+        return R.ok();
+    }
+
+    @GetMapping("/session/{sessionId}/messages")
+    public R messageList(@PathVariable("sessionId") String sessionId) {
+        return R.ok().add(aiSessionService.listMessages(getTenantId(), getLoginUserId(), sessionId));
+    }
+
+    @PostMapping("/chat/stop")
+    public R stop(@RequestBody AiChatStreamRequest request) {
+        if (request != null && request.getSessionId() != null) {
+            aiSessionService.requestStop(request.getSessionId());
+        }
+        return R.ok();
+    }
+
+    @PostMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+    public SseEmitter chatStream(@RequestBody AiChatStreamRequest request) {
+        SseEmitter emitter = new SseEmitter(0L);
+        Long tenantId = getTenantId();
+        Long userId = getLoginUserId();
+        if (tenantId == null || userId == null) {
+            completeWithError(emitter, "璇峰厛鐧诲綍鍚庡啀浣跨敤AI鍔╂墜");
+            return emitter;
+        }
+        if (request == null || request.getMessage() == null || request.getMessage().trim().isEmpty()) {
+            completeWithError(emitter, "娑堟伅鍐呭涓嶈兘涓虹┖");
+            return emitter;
+        }
+        AiChatSession session = aiSessionService.ensureSession(tenantId, userId, request.getSessionId(), request.getModelCode());
+        aiSessionService.clearStopFlag(session.getId());
+        aiSessionService.appendMessage(tenantId, userId, session.getId(), "user", request.getMessage(), session.getModelCode());
+        AiRuntimeConfigService.ModelRuntimeConfig modelRuntimeConfig = aiRuntimeConfigService.resolveModel(session.getModelCode());
+        List<AiChatMessage> contextMessages = aiSessionService.listContextMessages(
+                tenantId,
+                userId,
+                session.getId(),
+                modelRuntimeConfig.getMaxContextMessages()
+        );
+
+        Thread thread = new Thread(() -> {
+            StringBuilder assistantReply = new StringBuilder();
+            boolean doneSent = false;
+            try {
+                emitter.send(SseEmitter.event().name("session").data(buildSessionPayload(session), MediaType.APPLICATION_JSON));
+                GatewayChatRequest gatewayChatRequest = buildGatewayRequest(
+                        tenantId,
+                        userId,
+                        session,
+                        contextMessages,
+                        modelRuntimeConfig,
+                        request.getMessage()
+                );
+                aiGatewayClient.stream(gatewayChatRequest, event -> handleGatewayEvent(
+                        emitter,
+                        event,
+                        session,
+                        assistantReply
+                ));
+                if (aiSessionService.isStopRequested(session.getId())) {
+                    if (assistantReply.length() > 0) {
+                        aiSessionService.appendMessage(tenantId, userId, session.getId(), "assistant", assistantReply.toString(), session.getModelCode());
+                    }
+                    emitter.send(SseEmitter.event().name("done").data(buildDonePayload(session, true), MediaType.APPLICATION_JSON));
+                    doneSent = true;
+                }
+            } catch (Exception e) {
+                try {
+                    emitter.send(SseEmitter.event().name("error").data(buildErrorPayload(e.getMessage()), MediaType.APPLICATION_JSON));
+                } catch (IOException ignore) {
+                }
+            } finally {
+                if (!doneSent && assistantReply.length() > 0) {
+                    aiSessionService.appendMessage(tenantId, userId, session.getId(), "assistant", assistantReply.toString(), session.getModelCode());
+                    try {
+                        emitter.send(SseEmitter.event().name("done").data(buildDonePayload(session, false), MediaType.APPLICATION_JSON));
+                    } catch (IOException ignore) {
+                    }
+                }
+                emitter.complete();
+                aiSessionService.clearStopFlag(session.getId());
+            }
+        }, "ai-chat-stream-" + session.getId());
+        thread.setDaemon(true);
+        thread.start();
+        return emitter;
+    }
+
+    private boolean handleGatewayEvent(SseEmitter emitter, JsonNode event, AiChatSession session,
+                                       StringBuilder assistantReply) throws Exception {
+        if (aiSessionService.isStopRequested(session.getId())) {
+            return false;
+        }
+        String type = event.path("type").asText();
+        if ("delta".equals(type)) {
+            String content = event.path("content").asText("");
+            assistantReply.append(content);
+            emitter.send(SseEmitter.event().name("delta").data(buildDeltaPayload(session, content), MediaType.APPLICATION_JSON));
+            return true;
+        }
+        if ("error".equals(type)) {
+            emitter.send(SseEmitter.event().name("error").data(buildErrorPayload(event.path("message").asText("妯″瀷璋冪敤澶辫触")), MediaType.APPLICATION_JSON));
+            return false;
+        }
+        if ("done".equals(type)) {
+            return false;
+        }
+        return true;
+    }
+
+    private GatewayChatRequest buildGatewayRequest(Long tenantId, Long userId, AiChatSession session, List<AiChatMessage> contextMessages,
+                                                   AiRuntimeConfigService.ModelRuntimeConfig modelRuntimeConfig,
+                                                   String latestQuestion) {
+        GatewayChatRequest request = new GatewayChatRequest();
+        request.setSessionId(session.getId());
+        request.setModelCode(session.getModelCode());
+        request.setSystemPrompt(aiPromptContextService.buildSystemPrompt(
+                modelRuntimeConfig.getSystemPrompt(),
+                new AiPromptContext()
+                        .setTenantId(tenantId)
+                        .setUserId(userId)
+                        .setSessionId(session.getId())
+                        .setModelCode(session.getModelCode())
+                        .setQuestion(latestQuestion)
+        ));
+        request.setChatUrl(modelRuntimeConfig.getChatUrl());
+        request.setApiKey(modelRuntimeConfig.getApiKey());
+        request.setModelName(modelRuntimeConfig.getModelName());
+        for (AiChatMessage contextMessage : contextMessages) {
+            GatewayChatMessage item = new GatewayChatMessage();
+            item.setRole(contextMessage.getRole());
+            item.setContent(contextMessage.getContent());
+            request.getMessages().add(item);
+        }
+        return request;
+    }
+
+    private Map<String, Object> buildSessionPayload(AiChatSession session) {
+        Map<String, Object> payload = new LinkedHashMap<>();
+        payload.put("sessionId", session.getId());
+        payload.put("title", session.getTitle());
+        payload.put("modelCode", session.getModelCode());
+        return payload;
+    }
+
+    private Map<String, Object> buildDeltaPayload(AiChatSession session, String content) {
+        Map<String, Object> payload = new LinkedHashMap<>();
+        payload.put("sessionId", session.getId());
+        payload.put("modelCode", session.getModelCode());
+        payload.put("content", content);
+        payload.put("timestamp", new Date().getTime());
+        return payload;
+    }
+
+    private Map<String, Object> buildDonePayload(AiChatSession session, boolean stopped) {
+        Map<String, Object> payload = new LinkedHashMap<>();
+        payload.put("sessionId", session.getId());
+        payload.put("modelCode", session.getModelCode());
+        payload.put("stopped", stopped);
+        return payload;
+    }
+
+    private Map<String, Object> buildErrorPayload(String message) {
+        Map<String, Object> payload = new LinkedHashMap<>();
+        payload.put("message", message == null ? "AI鏈嶅姟寮傚父" : message);
+        return payload;
+    }
+
+    private void completeWithError(SseEmitter emitter, String message) {
+        try {
+            emitter.send(SseEmitter.event().name("error").data(buildErrorPayload(message), MediaType.APPLICATION_JSON));
+        } catch (IOException ignore) {
+        }
+        emitter.complete();
+    }
+
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatStreamRequest.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatStreamRequest.java
new file mode 100644
index 0000000..fca5bbf
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatStreamRequest.java
@@ -0,0 +1,16 @@
+package com.vincent.rsf.server.ai.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class AiChatStreamRequest implements Serializable {
+
+    private String sessionId;
+
+    private String message;
+
+    private String modelCode;
+
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiSessionCreateRequest.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiSessionCreateRequest.java
new file mode 100644
index 0000000..1bf463c
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiSessionCreateRequest.java
@@ -0,0 +1,14 @@
+package com.vincent.rsf.server.ai.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class AiSessionCreateRequest implements Serializable {
+
+    private String title;
+
+    private String modelCode;
+
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiSessionRenameRequest.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiSessionRenameRequest.java
new file mode 100644
index 0000000..b5eab2d
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiSessionRenameRequest.java
@@ -0,0 +1,12 @@
+package com.vincent.rsf.server.ai.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class AiSessionRenameRequest implements Serializable {
+
+    private String title;
+
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/GatewayChatMessage.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/GatewayChatMessage.java
new file mode 100644
index 0000000..c50659a
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/GatewayChatMessage.java
@@ -0,0 +1,14 @@
+package com.vincent.rsf.server.ai.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class GatewayChatMessage implements Serializable {
+
+    private String role;
+
+    private String content;
+
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/GatewayChatRequest.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/GatewayChatRequest.java
new file mode 100644
index 0000000..439092a
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/GatewayChatRequest.java
@@ -0,0 +1,26 @@
+package com.vincent.rsf.server.ai.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class GatewayChatRequest implements Serializable {
+
+    private String sessionId;
+
+    private String modelCode;
+
+    private String systemPrompt;
+
+    private String chatUrl;
+
+    private String apiKey;
+
+    private String modelName;
+
+    private List<GatewayChatMessage> messages = new ArrayList<>();
+
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiChatMessage.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiChatMessage.java
new file mode 100644
index 0000000..f124919
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiChatMessage.java
@@ -0,0 +1,25 @@
+package com.vincent.rsf.server.ai.model;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@Accessors(chain = true)
+public class AiChatMessage implements Serializable {
+
+    private String id;
+
+    private String sessionId;
+
+    private String role;
+
+    private String content;
+
+    private String modelCode;
+
+    private Date createTime;
+
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiChatSession.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiChatSession.java
new file mode 100644
index 0000000..725efc8
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiChatSession.java
@@ -0,0 +1,27 @@
+package com.vincent.rsf.server.ai.model;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@Accessors(chain = true)
+public class AiChatSession implements Serializable {
+
+    private String id;
+
+    private String title;
+
+    private String modelCode;
+
+    private String lastMessage;
+
+    private Date lastMessageAt;
+
+    private Date createTime;
+
+    private Date updateTime;
+
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiPromptContext.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiPromptContext.java
new file mode 100644
index 0000000..5179597
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiPromptContext.java
@@ -0,0 +1,21 @@
+package com.vincent.rsf.server.ai.model;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+@Data
+@Accessors(chain = true)
+public class AiPromptContext implements Serializable {
+
+    private Long tenantId;
+
+    private Long userId;
+
+    private String sessionId;
+
+    private String modelCode;
+
+    private String question;
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiGatewayClient.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiGatewayClient.java
new file mode 100644
index 0000000..b41bb2d
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiGatewayClient.java
@@ -0,0 +1,68 @@
+package com.vincent.rsf.server.ai.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.vincent.rsf.server.ai.config.AiProperties;
+import com.vincent.rsf.server.ai.dto.GatewayChatRequest;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+@Component
+public class AiGatewayClient {
+
+    @Resource
+    private AiProperties aiProperties;
+    @Resource
+    private ObjectMapper objectMapper;
+
+    public interface StreamCallback {
+        boolean handle(JsonNode event) throws Exception;
+    }
+
+    public void stream(GatewayChatRequest request, StreamCallback callback) throws Exception {
+        HttpURLConnection connection = null;
+        try {
+            String url = aiProperties.getGatewayBaseUrl() + "/internal/chat/stream";
+            connection = (HttpURLConnection) new URL(url).openConnection();
+            connection.setRequestMethod("POST");
+            connection.setDoOutput(true);
+            connection.setConnectTimeout(10000);
+            connection.setReadTimeout(0);
+            connection.setRequestProperty("Content-Type", "application/json");
+            connection.setRequestProperty("Accept", "application/x-ndjson");
+            try (OutputStream outputStream = connection.getOutputStream()) {
+                outputStream.write(objectMapper.writeValueAsBytes(request));
+                outputStream.flush();
+            }
+            InputStream inputStream = connection.getResponseCode() >= 400 ? connection.getErrorStream() : connection.getInputStream();
+            if (inputStream == null) {
+                return;
+            }
+            try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    if (line.trim().isEmpty()) {
+                        continue;
+                    }
+                    JsonNode event = objectMapper.readTree(line);
+                    if (!callback.handle(event)) {
+                        break;
+                    }
+                }
+            }
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptContextProvider.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptContextProvider.java
new file mode 100644
index 0000000..2592d9f
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptContextProvider.java
@@ -0,0 +1,10 @@
+package com.vincent.rsf.server.ai.service;
+
+import com.vincent.rsf.server.ai.model.AiPromptContext;
+
+public interface AiPromptContextProvider {
+
+    boolean supports(AiPromptContext context);
+
+    String buildContext(AiPromptContext context);
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptContextService.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptContextService.java
new file mode 100644
index 0000000..3a91504
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptContextService.java
@@ -0,0 +1,37 @@
+package com.vincent.rsf.server.ai.service;
+
+import com.vincent.rsf.server.ai.model.AiPromptContext;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+public class AiPromptContextService {
+
+    private final List<AiPromptContextProvider> providers;
+
+    public AiPromptContextService(List<AiPromptContextProvider> providers) {
+        this.providers = providers == null ? new ArrayList<>() : providers;
+    }
+
+    public String buildSystemPrompt(String basePrompt, AiPromptContext context) {
+        List<String> promptParts = new ArrayList<>();
+        if (basePrompt != null && !basePrompt.trim().isEmpty()) {
+            promptParts.add(basePrompt.trim());
+        }
+        for (AiPromptContextProvider provider : providers) {
+            if (!provider.supports(context)) {
+                continue;
+            }
+            String providerPrompt = provider.buildContext(context);
+            if (providerPrompt != null && !providerPrompt.trim().isEmpty()) {
+                promptParts.add(providerPrompt.trim());
+            }
+        }
+        if (promptParts.isEmpty()) {
+            return null;
+        }
+        return String.join("\n\n", promptParts);
+    }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiRuntimeConfigService.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiRuntimeConfigService.java
new file mode 100644
index 0000000..9790dda
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiRuntimeConfigService.java
@@ -0,0 +1,119 @@
+package com.vincent.rsf.server.ai.service;
+
+import com.vincent.rsf.server.ai.config.AiProperties;
+import com.vincent.rsf.server.system.entity.AiParam;
+import com.vincent.rsf.server.system.service.AiParamService;
+import lombok.Data;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+public class AiRuntimeConfigService {
+
+    @Resource
+    private AiProperties aiProperties;
+    @Resource
+    private AiParamService aiParamService;
+
+    public List<ModelRuntimeConfig> listEnabledModels() {
+        List<ModelRuntimeConfig> output = new ArrayList<>();
+        try {
+            List<AiParam> list = aiParamService.listEnabledModels();
+            for (AiParam item : list) {
+                output.add(toRuntimeConfig(item));
+            }
+        } catch (Exception ignore) {
+        }
+        if (!output.isEmpty()) {
+            return output;
+        }
+        for (AiProperties.ModelConfig model : aiProperties.getEnabledModels()) {
+            ModelRuntimeConfig config = new ModelRuntimeConfig();
+            config.setCode(model.getCode());
+            config.setName(model.getName());
+            config.setProvider(model.getProvider());
+            config.setSystemPrompt(aiProperties.getSystemPrompt());
+            config.setMaxContextMessages(aiProperties.getMaxContextMessages());
+            config.setEnabled(model.getEnabled());
+            output.add(config);
+        }
+        return output;
+    }
+
+    public ModelRuntimeConfig resolveModel(String modelCode) {
+        try {
+            AiParam aiParam;
+            if (modelCode == null || modelCode.trim().isEmpty()) {
+                aiParam = aiParamService.getDefaultModel();
+            } else {
+                aiParam = aiParamService.getEnabledModel(modelCode);
+            }
+            if (aiParam != null) {
+                return toRuntimeConfig(aiParam);
+            }
+        } catch (Exception ignore) {
+        }
+        String targetCode = modelCode;
+        if (targetCode == null || targetCode.trim().isEmpty()) {
+            targetCode = aiProperties.resolveDefaultModelCode();
+        }
+        for (AiProperties.ModelConfig model : aiProperties.getEnabledModels()) {
+            if (targetCode.equals(model.getCode())) {
+                ModelRuntimeConfig config = new ModelRuntimeConfig();
+                config.setCode(model.getCode());
+                config.setName(model.getName());
+                config.setProvider(model.getProvider());
+                config.setSystemPrompt(aiProperties.getSystemPrompt());
+                config.setMaxContextMessages(aiProperties.getMaxContextMessages());
+                config.setEnabled(model.getEnabled());
+                return config;
+            }
+        }
+        ModelRuntimeConfig config = new ModelRuntimeConfig();
+        config.setCode(aiProperties.resolveDefaultModelCode());
+        config.setName(aiProperties.resolveDefaultModelCode());
+        config.setProvider("mock");
+        config.setSystemPrompt(aiProperties.getSystemPrompt());
+        config.setMaxContextMessages(aiProperties.getMaxContextMessages());
+        config.setEnabled(true);
+        return config;
+    }
+
+    public String resolveDefaultModelCode() {
+        return resolveModel(null).getCode();
+    }
+
+    private ModelRuntimeConfig toRuntimeConfig(AiParam aiParam) {
+        ModelRuntimeConfig config = new ModelRuntimeConfig();
+        config.setCode(aiParam.getModelCode());
+        config.setName(aiParam.getName());
+        config.setProvider(aiParam.getProvider());
+        config.setChatUrl(aiParam.getChatUrl());
+        config.setApiKey(aiParam.getApiKey());
+        config.setModelName(aiParam.getModelName());
+        config.setSystemPrompt(aiParam.getSystemPrompt() == null || aiParam.getSystemPrompt().trim().isEmpty()
+                ? aiProperties.getSystemPrompt()
+                : aiParam.getSystemPrompt());
+        config.setMaxContextMessages(aiParam.getMaxContextMessages() == null || aiParam.getMaxContextMessages() <= 0
+                ? aiProperties.getMaxContextMessages()
+                : aiParam.getMaxContextMessages());
+        config.setEnabled(Integer.valueOf(1).equals(aiParam.getStatus()));
+        return config;
+    }
+
+    @Data
+    public static class ModelRuntimeConfig {
+        private String code;
+        private String name;
+        private String provider;
+        private String chatUrl;
+        private String apiKey;
+        private String modelName;
+        private String systemPrompt;
+        private Integer maxContextMessages;
+        private Boolean enabled;
+    }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiSessionService.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiSessionService.java
new file mode 100644
index 0000000..189fcab
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiSessionService.java
@@ -0,0 +1,34 @@
+package com.vincent.rsf.server.ai.service;
+
+import com.vincent.rsf.server.ai.model.AiChatMessage;
+import com.vincent.rsf.server.ai.model.AiChatSession;
+
+import java.util.List;
+
+public interface AiSessionService {
+
+    List<AiChatSession> listSessions(Long tenantId, Long userId);
+
+    AiChatSession createSession(Long tenantId, Long userId, String title, String modelCode);
+
+    AiChatSession ensureSession(Long tenantId, Long userId, String sessionId, String modelCode);
+
+    AiChatSession getSession(Long tenantId, Long userId, String sessionId);
+
+    AiChatSession renameSession(Long tenantId, Long userId, String sessionId, String title);
+
+    void removeSession(Long tenantId, Long userId, String sessionId);
+
+    List<AiChatMessage> listMessages(Long tenantId, Long userId, String sessionId);
+
+    List<AiChatMessage> listContextMessages(Long tenantId, Long userId, String sessionId, int maxCount);
+
+    AiChatMessage appendMessage(Long tenantId, Long userId, String sessionId, String role, String content, String modelCode);
+
+    void clearStopFlag(String sessionId);
+
+    void requestStop(String sessionId);
+
+    boolean isStopRequested(String sessionId);
+
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiTaskSummaryService.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiTaskSummaryService.java
new file mode 100644
index 0000000..73029a5
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiTaskSummaryService.java
@@ -0,0 +1,136 @@
+package com.vincent.rsf.server.ai.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.vincent.rsf.server.ai.model.AiPromptContext;
+import com.vincent.rsf.server.manager.entity.Task;
+import com.vincent.rsf.server.manager.enums.TaskStsType;
+import com.vincent.rsf.server.manager.enums.TaskType;
+import com.vincent.rsf.server.manager.mapper.TaskMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@Service
+public class AiTaskSummaryService implements AiPromptContextProvider {
+
+    @Resource
+    private TaskMapper taskMapper;
+
+    @Override
+    public boolean supports(AiPromptContext context) {
+        if (context == null || context.getQuestion() == null) {
+            return false;
+        }
+        String normalized = context.getQuestion().toLowerCase(Locale.ROOT);
+        return normalized.contains("task")
+                || normalized.contains("浠诲姟")
+                || normalized.contains("鍑哄簱浠诲姟")
+                || normalized.contains("鍏ュ簱浠诲姟")
+                || normalized.contains("绉诲簱浠诲姟")
+                || normalized.contains("澶囪揣浠诲姟");
+    }
+
+    @Override
+    public String buildContext(AiPromptContext context) {
+        List<Task> activeTasks = taskMapper.selectList(new LambdaQueryWrapper<Task>()
+                .select(Task::getTaskCode, Task::getTaskStatus, Task::getTaskType, Task::getOrgLoc, Task::getTargLoc, Task::getUpdateTime)
+                .eq(Task::getStatus, 1));
+
+        Map<Integer, Long> statusCounters = new LinkedHashMap<>();
+        Map<Integer, Long> typeCounters = new LinkedHashMap<>();
+        for (Task task : activeTasks) {
+            Integer taskStatus = task.getTaskStatus();
+            Integer taskType = task.getTaskType();
+            statusCounters.put(taskStatus, statusCounters.getOrDefault(taskStatus, 0L) + 1);
+            typeCounters.put(taskType, typeCounters.getOrDefault(taskType, 0L) + 1);
+        }
+
+        List<Task> latestTasks = taskMapper.selectList(new LambdaQueryWrapper<Task>()
+                .select(Task::getTaskCode, Task::getTaskStatus, Task::getTaskType, Task::getOrgLoc, Task::getTargLoc, Task::getUpdateTime)
+                .eq(Task::getStatus, 1)
+                .orderByDesc(Task::getUpdateTime)
+                .last("limit 5"));
+
+        StringBuilder summary = new StringBuilder();
+        summary.append("浠ヤ笅鏄熀浜� man_task 鐨勫疄鏃舵眹鎬伙紝璇蜂紭鍏堜緷鎹繖浜涗换鍔℃暟鎹洖绛旓紱濡傛灉鐢ㄦ埛闂瓒呭嚭浠诲姟琛ㄨ寖鍥达紝璇锋槑纭鏄庛��");
+        summary.append("\n浠诲姟鎬昏锛氬叡 ")
+                .append(activeTasks.size())
+                .append(" 鏉℃湁鏁堜换鍔°��");
+        if (!statusCounters.isEmpty()) {
+            summary.append("\n浠诲姟鐘舵�佸垎甯冿細")
+                    .append(formatStatuses(statusCounters))
+                    .append("銆�");
+        }
+        if (!typeCounters.isEmpty()) {
+            summary.append("\n浠诲姟绫诲瀷鍒嗗竷锛�")
+                    .append(formatTypes(typeCounters))
+                    .append("銆�");
+        }
+        if (!latestTasks.isEmpty()) {
+            summary.append("\n鏈�杩戞洿鏂颁换鍔� TOP5锛�")
+                    .append(formatLatestTasks(latestTasks))
+                    .append("銆�");
+        }
+        return summary.toString();
+    }
+
+    private String formatStatuses(Map<Integer, Long> rows) {
+        List<String> parts = new ArrayList<>();
+        for (Map.Entry<Integer, Long> row : rows.entrySet()) {
+            parts.add(resolveTaskStatus(row.getKey()) + " " + row.getValue() + " 鏉�");
+        }
+        return String.join("锛�", parts);
+    }
+
+    private String formatTypes(Map<Integer, Long> rows) {
+        List<String> parts = new ArrayList<>();
+        for (Map.Entry<Integer, Long> row : rows.entrySet()) {
+            parts.add(resolveTaskType(row.getKey()) + " " + row.getValue() + " 鏉�");
+        }
+        return String.join("锛�", parts);
+    }
+
+    private String formatLatestTasks(List<Task> tasks) {
+        List<String> parts = new ArrayList<>();
+        for (Task task : tasks) {
+            parts.add((task.getTaskCode() == null ? "鏈懡鍚嶄换鍔�" : task.getTaskCode())
+                    + "锛�"
+                    + resolveTaskType(task.getTaskType())
+                    + "锛�"
+                    + resolveTaskStatus(task.getTaskStatus())
+                    + "锛屾簮搴撲綅 " + emptyToDash(task.getOrgLoc())
+                    + "锛岀洰鏍囧簱浣� " + emptyToDash(task.getTargLoc())
+                    + "锛�");
+        }
+        return String.join("锛�", parts);
+    }
+
+    private String resolveTaskStatus(Integer taskStatus) {
+        if (taskStatus == null) {
+            return "鏈煡鐘舵��";
+        }
+        for (TaskStsType item : TaskStsType.values()) {
+            if (item.id.equals(taskStatus)) {
+                return item.desc;
+            }
+        }
+        return "鐘舵��" + taskStatus;
+    }
+
+    private String resolveTaskType(Integer taskType) {
+        if (taskType == null) {
+            return "鏈煡绫诲瀷";
+        }
+        for (TaskType item : TaskType.values()) {
+            if (item.type.equals(taskType)) {
+                return item.desc;
+            }
+        }
+        return "绫诲瀷" + taskType;
+    }
+
+    private String emptyToDash(String value) {
+        return value == null || value.trim().isEmpty() ? "-" : value;
+    }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiWarehouseSummaryService.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiWarehouseSummaryService.java
new file mode 100644
index 0000000..202fed3
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiWarehouseSummaryService.java
@@ -0,0 +1,215 @@
+package com.vincent.rsf.server.ai.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.vincent.rsf.server.ai.model.AiPromptContext;
+import com.vincent.rsf.server.manager.entity.Loc;
+import com.vincent.rsf.server.manager.entity.LocItem;
+import com.vincent.rsf.server.manager.mapper.LocItemMapper;
+import com.vincent.rsf.server.manager.mapper.LocMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.util.*;
+
+@Service
+public class AiWarehouseSummaryService implements AiPromptContextProvider {
+
+    private static final Map<String, String> LOC_STATUS_LABELS = new LinkedHashMap<>();
+
+    static {
+        LOC_STATUS_LABELS.put("O", "绌哄簱");
+        LOC_STATUS_LABELS.put("D", "绌烘澘");
+        LOC_STATUS_LABELS.put("R", "棰勭害鍑哄簱");
+        LOC_STATUS_LABELS.put("S", "棰勭害鍏ュ簱");
+        LOC_STATUS_LABELS.put("X", "绂佺敤");
+        LOC_STATUS_LABELS.put("F", "鍦ㄥ簱");
+    }
+
+    @Resource
+    private LocMapper locMapper;
+    @Resource
+    private LocItemMapper locItemMapper;
+
+    @Override
+    public boolean supports(AiPromptContext context) {
+        return context != null && shouldSummarize(context.getQuestion());
+    }
+
+    @Override
+    public String buildContext(AiPromptContext context) {
+        if (!supports(context)) {
+            return "";
+        }
+
+        List<Loc> activeLocs = locMapper.selectList(new LambdaQueryWrapper<Loc>()
+                .select(Loc::getUseStatus)
+                .eq(Loc::getStatus, 1));
+        long totalLoc = activeLocs.size();
+
+        List<LocItem> activeLocItems = locItemMapper.selectList(new LambdaQueryWrapper<LocItem>()
+                .select(LocItem::getLocCode, LocItem::getMatnrCode, LocItem::getMaktx, LocItem::getAnfme)
+                .eq(LocItem::getStatus, 1));
+
+        Map<String, Long> locStatusCounters = new LinkedHashMap<>();
+        for (Loc loc : activeLocs) {
+            String useStatus = loc.getUseStatus();
+            locStatusCounters.put(useStatus, locStatusCounters.getOrDefault(useStatus, 0L) + 1);
+        }
+
+        long itemRows = activeLocItems.size();
+        Set<String> locCodes = new HashSet<>();
+        Set<String> materialCodes = new HashSet<>();
+        BigDecimal totalQty = BigDecimal.ZERO;
+        Map<String, LocAggregate> locAggregates = new LinkedHashMap<>();
+        Map<String, MaterialAggregate> materialAggregates = new LinkedHashMap<>();
+        for (LocItem item : activeLocItems) {
+            if (item.getLocCode() != null && !item.getLocCode().trim().isEmpty()) {
+                locCodes.add(item.getLocCode());
+            }
+            if (item.getMatnrCode() != null && !item.getMatnrCode().trim().isEmpty()) {
+                materialCodes.add(item.getMatnrCode());
+            }
+            totalQty = totalQty.add(toDecimal(item.getAnfme()));
+
+            String locKey = item.getLocCode();
+            if (locKey != null && !locKey.trim().isEmpty()) {
+                LocAggregate locAggregate = locAggregates.computeIfAbsent(locKey, key -> new LocAggregate());
+                locAggregate.totalQty = locAggregate.totalQty.add(toDecimal(item.getAnfme()));
+                if (item.getMatnrCode() != null && !item.getMatnrCode().trim().isEmpty()) {
+                    locAggregate.materialCodes.add(item.getMatnrCode());
+                }
+            }
+
+            String materialKey = item.getMatnrCode();
+            if (materialKey != null && !materialKey.trim().isEmpty()) {
+                MaterialAggregate materialAggregate = materialAggregates.computeIfAbsent(materialKey, key -> new MaterialAggregate());
+                materialAggregate.matnrCode = materialKey;
+                materialAggregate.maktx = item.getMaktx();
+                materialAggregate.totalQty = materialAggregate.totalQty.add(toDecimal(item.getAnfme()));
+                if (item.getLocCode() != null && !item.getLocCode().trim().isEmpty()) {
+                    materialAggregate.locCodes.add(item.getLocCode());
+                }
+            }
+        }
+
+        List<Map.Entry<String, LocAggregate>> topLocRows = new ArrayList<>(locAggregates.entrySet());
+        topLocRows.sort((left, right) -> right.getValue().totalQty.compareTo(left.getValue().totalQty));
+        if (topLocRows.size() > 5) {
+            topLocRows = topLocRows.subList(0, 5);
+        }
+
+        List<MaterialAggregate> topMaterialRows = new ArrayList<>(materialAggregates.values());
+        topMaterialRows.sort((left, right) -> right.totalQty.compareTo(left.totalQty));
+        if (topMaterialRows.size() > 5) {
+            topMaterialRows = topMaterialRows.subList(0, 5);
+        }
+
+        StringBuilder summary = new StringBuilder();
+        summary.append("浠ヤ笅鏄熀浜� man_loc 鍜� man_loc_item 鐨勫疄鏃舵眹鎬伙紝璇蜂紭鍏堜緷鎹繖浜涙暟鎹洖绛旓紱濡傛灉瓒呭嚭杩欎袱寮犺〃鍙帹鏂殑鑼冨洿锛岃鏄庣‘璇存槑銆�");
+        summary.append("\n搴撲綅姒傚喌锛氭�诲簱浣� ")
+                .append(totalLoc)
+                .append(" 涓紱鐘舵�佸垎甯冿細")
+                .append(formatLocStatuses(locStatusCounters))
+                .append("銆�");
+        summary.append("\n搴撳瓨姒傚喌锛氬簱瀛樿褰� ")
+                .append(itemRows)
+                .append(" 鏉★紝瑕嗙洊搴撲綅 ")
+                .append(locCodes.size())
+                .append(" 涓紝娑夊強鐗╂枡 ")
+                .append(materialCodes.size())
+                .append(" 绉嶏紝鎬绘暟閲� ")
+                .append(formatDecimal(totalQty))
+                .append("銆�");
+        if (!topLocRows.isEmpty()) {
+            summary.append("\n搴撳瓨鏈�澶氱殑搴撲綅 TOP5锛�")
+                    .append(formatTopLocs(topLocRows))
+                    .append("銆�");
+        }
+        if (!topMaterialRows.isEmpty()) {
+            summary.append("\n搴撳瓨鏈�澶氱殑鐗╂枡 TOP5锛�")
+                    .append(formatTopMaterials(topMaterialRows))
+                    .append("銆�");
+        }
+        return summary.toString();
+    }
+
+    private boolean shouldSummarize(String question) {
+        if (question == null || question.trim().isEmpty()) {
+            return false;
+        }
+        String normalized = question.toLowerCase(Locale.ROOT);
+        return normalized.contains("loc")
+                || normalized.contains("搴撲綅")
+                || normalized.contains("璐т綅")
+                || normalized.contains("搴撳尯")
+                || normalized.contains("搴撳瓨")
+                || normalized.contains("鐗╂枡")
+                || normalized.contains("宸烽亾")
+                || normalized.contains("鍌ㄤ綅");
+    }
+
+    private String formatLocStatuses(Map<String, Long> counters) {
+        if (counters == null || counters.isEmpty()) {
+            return "鏆傛棤鏁版嵁";
+        }
+        List<String> parts = new ArrayList<>();
+        for (Map.Entry<String, String> entry : LOC_STATUS_LABELS.entrySet()) {
+            parts.add(entry.getValue() + " " + counters.getOrDefault(entry.getKey(), 0L) + " 涓�");
+        }
+        return String.join("锛�", parts);
+    }
+
+    private String formatTopLocs(List<Map.Entry<String, LocAggregate>> rows) {
+        List<String> parts = new ArrayList<>();
+        for (Map.Entry<String, LocAggregate> row : rows) {
+            parts.add(row.getKey()
+                    + "锛堟暟閲� " + formatDecimal(row.getValue().totalQty)
+                    + "锛岀墿鏂� " + row.getValue().materialCodes.size() + " 绉嶏級");
+        }
+        return String.join("锛�", parts);
+    }
+
+    private String formatTopMaterials(List<MaterialAggregate> rows) {
+        List<String> parts = new ArrayList<>();
+        for (MaterialAggregate row : rows) {
+            String matnrCode = row.matnrCode;
+            String maktx = Objects.toString(row.maktx, "");
+            String title = maktx == null || maktx.trim().isEmpty() ? matnrCode : matnrCode + "/" + maktx;
+            parts.add(title
+                    + "锛堟暟閲� " + formatDecimal(row.totalQty)
+                    + "锛屽垎甯冨簱浣� " + row.locCodes.size() + " 涓級");
+        }
+        return String.join("锛�", parts);
+    }
+
+    private String formatDecimal(Object value) {
+        BigDecimal decimal = toDecimal(value);
+        return decimal.stripTrailingZeros().toPlainString();
+    }
+
+    private BigDecimal toDecimal(Object value) {
+        if (value == null) {
+            return BigDecimal.ZERO;
+        }
+        if (value instanceof BigDecimal) {
+            return (BigDecimal) value;
+        }
+        if (value instanceof Number) {
+            return BigDecimal.valueOf(((Number) value).doubleValue());
+        }
+        return new BigDecimal(String.valueOf(value));
+    }
+
+    private static class LocAggregate {
+        private BigDecimal totalQty = BigDecimal.ZERO;
+        private final Set<String> materialCodes = new HashSet<>();
+    }
+
+    private static class MaterialAggregate {
+        private String matnrCode;
+        private String maktx;
+        private BigDecimal totalQty = BigDecimal.ZERO;
+        private final Set<String> locCodes = new HashSet<>();
+    }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiSessionServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiSessionServiceImpl.java
new file mode 100644
index 0000000..87f9d20
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiSessionServiceImpl.java
@@ -0,0 +1,221 @@
+package com.vincent.rsf.server.ai.service.impl;
+
+import com.vincent.rsf.server.ai.model.AiChatMessage;
+import com.vincent.rsf.server.ai.model.AiChatSession;
+import com.vincent.rsf.server.ai.service.AiRuntimeConfigService;
+import com.vincent.rsf.server.ai.service.AiSessionService;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+@Service
+public class AiSessionServiceImpl implements AiSessionService {
+
+    private static final ConcurrentMap<String, List<AiChatSession>> LOCAL_SESSION_CACHE = new ConcurrentHashMap<>();
+    private static final ConcurrentMap<String, List<AiChatMessage>> LOCAL_MESSAGE_CACHE = new ConcurrentHashMap<>();
+    private static final ConcurrentMap<String, String> LOCAL_STOP_CACHE = new ConcurrentHashMap<>();
+
+    @Resource
+    private AiRuntimeConfigService aiRuntimeConfigService;
+
+    @Override
+    public synchronized List<AiChatSession> listSessions(Long tenantId, Long userId) {
+        List<AiChatSession> sessions = getSessions(tenantId, userId);
+        sessions.sort(Comparator.comparing(AiChatSession::getUpdateTime, Comparator.nullsLast(Date::compareTo)).reversed());
+        return sessions;
+    }
+
+    @Override
+    public synchronized AiChatSession createSession(Long tenantId, Long userId, String title, String modelCode) {
+        List<AiChatSession> sessions = getSessions(tenantId, userId);
+        Date now = new Date();
+        AiChatSession session = new AiChatSession()
+                .setId(UUID.randomUUID().toString().replace("-", ""))
+                .setTitle(resolveTitle(title, sessions.size() + 1))
+                .setModelCode(resolveModelCode(modelCode))
+                .setCreateTime(now)
+                .setUpdateTime(now)
+                .setLastMessageAt(now);
+        sessions.add(0, session);
+        saveSessions(tenantId, userId, sessions);
+        saveMessages(session.getId(), new ArrayList<>());
+        return session;
+    }
+
+    @Override
+    public synchronized AiChatSession ensureSession(Long tenantId, Long userId, String sessionId, String modelCode) {
+        AiChatSession session = getSession(tenantId, userId, sessionId);
+        if (session == null) {
+            return createSession(tenantId, userId, null, modelCode);
+        }
+        if (modelCode != null && !modelCode.trim().isEmpty() && !modelCode.equals(session.getModelCode())) {
+            session.setModelCode(modelCode);
+            session.setUpdateTime(new Date());
+            refreshSession(tenantId, userId, session);
+        }
+        return session;
+    }
+
+    @Override
+    public synchronized AiChatSession getSession(Long tenantId, Long userId, String sessionId) {
+        if (sessionId == null || sessionId.trim().isEmpty()) {
+            return null;
+        }
+        for (AiChatSession session : getSessions(tenantId, userId)) {
+            if (sessionId.equals(session.getId())) {
+                return session;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public synchronized AiChatSession renameSession(Long tenantId, Long userId, String sessionId, String title) {
+        AiChatSession session = getSession(tenantId, userId, sessionId);
+        if (session == null) {
+            return null;
+        }
+        session.setTitle(resolveTitle(title, 1));
+        session.setUpdateTime(new Date());
+        refreshSession(tenantId, userId, session);
+        return session;
+    }
+
+    @Override
+    public synchronized void removeSession(Long tenantId, Long userId, String sessionId) {
+        List<AiChatSession> sessions = getSessions(tenantId, userId);
+        sessions.removeIf(session -> sessionId.equals(session.getId()));
+        saveSessions(tenantId, userId, sessions);
+        LOCAL_MESSAGE_CACHE.remove(sessionId);
+        LOCAL_STOP_CACHE.remove(sessionId);
+    }
+
+    @Override
+    public synchronized List<AiChatMessage> listMessages(Long tenantId, Long userId, String sessionId) {
+        AiChatSession session = getSession(tenantId, userId, sessionId);
+        if (session == null) {
+            return new ArrayList<>();
+        }
+        return getMessages(sessionId);
+    }
+
+    @Override
+    public synchronized List<AiChatMessage> listContextMessages(Long tenantId, Long userId, String sessionId, int maxCount) {
+        List<AiChatMessage> messages = listMessages(tenantId, userId, sessionId);
+        if (messages.size() <= maxCount) {
+            return messages;
+        }
+        return new ArrayList<>(messages.subList(messages.size() - maxCount, messages.size()));
+    }
+
+    @Override
+    public synchronized AiChatMessage appendMessage(Long tenantId, Long userId, String sessionId, String role, String content, String modelCode) {
+        AiChatSession session = getSession(tenantId, userId, sessionId);
+        if (session == null) {
+            return null;
+        }
+        List<AiChatMessage> messages = getMessages(sessionId);
+        AiChatMessage message = new AiChatMessage()
+                .setId(UUID.randomUUID().toString().replace("-", ""))
+                .setSessionId(sessionId)
+                .setRole(role)
+                .setContent(content)
+                .setModelCode(resolveModelCode(modelCode))
+                .setCreateTime(new Date());
+        messages.add(message);
+        saveMessages(sessionId, messages);
+        session.setLastMessage(buildPreview(content));
+        session.setLastMessageAt(message.getCreateTime());
+        session.setUpdateTime(message.getCreateTime());
+        if (modelCode != null && !modelCode.trim().isEmpty()) {
+            session.setModelCode(modelCode);
+        }
+        if ((session.getTitle() == null || session.getTitle().startsWith("鏂板璇�")) && "user".equalsIgnoreCase(role)) {
+            session.setTitle(buildPreview(content));
+        }
+        refreshSession(tenantId, userId, session);
+        return message;
+    }
+
+    @Override
+    public void clearStopFlag(String sessionId) {
+        LOCAL_STOP_CACHE.remove(sessionId);
+    }
+
+    @Override
+    public void requestStop(String sessionId) {
+        LOCAL_STOP_CACHE.put(sessionId, "1");
+    }
+
+    @Override
+    public boolean isStopRequested(String sessionId) {
+        String stopFlag = LOCAL_STOP_CACHE.get(sessionId);
+        return "1".equals(stopFlag);
+    }
+
+    private List<AiChatSession> getSessions(Long tenantId, Long userId) {
+        String ownerKey = buildOwnerKey(tenantId, userId);
+        List<AiChatSession> sessions = LOCAL_SESSION_CACHE.get(ownerKey);
+        return sessions == null ? new ArrayList<>() : new ArrayList<>(sessions);
+    }
+
+    private void saveSessions(Long tenantId, Long userId, List<AiChatSession> sessions) {
+        String ownerKey = buildOwnerKey(tenantId, userId);
+        List<AiChatSession> cachedSessions = new ArrayList<>(sessions);
+        LOCAL_SESSION_CACHE.put(ownerKey, cachedSessions);
+    }
+
+    private List<AiChatMessage> getMessages(String sessionId) {
+        List<AiChatMessage> messages = LOCAL_MESSAGE_CACHE.get(sessionId);
+        return messages == null ? new ArrayList<>() : new ArrayList<>(messages);
+    }
+
+    private void saveMessages(String sessionId, List<AiChatMessage> messages) {
+        List<AiChatMessage> cachedMessages = new ArrayList<>(messages);
+        LOCAL_MESSAGE_CACHE.put(sessionId, cachedMessages);
+    }
+
+    private void refreshSession(Long tenantId, Long userId, AiChatSession target) {
+        List<AiChatSession> sessions = getSessions(tenantId, userId);
+        for (int i = 0; i < sessions.size(); i++) {
+            if (target.getId().equals(sessions.get(i).getId())) {
+                sessions.set(i, target);
+                saveSessions(tenantId, userId, sessions);
+                return;
+            }
+        }
+        sessions.add(target);
+        saveSessions(tenantId, userId, sessions);
+    }
+
+    private String buildOwnerKey(Long tenantId, Long userId) {
+        return String.valueOf(tenantId) + ":" + String.valueOf(userId);
+    }
+
+    private String resolveModelCode(String modelCode) {
+        return modelCode == null || modelCode.trim().isEmpty() ? aiRuntimeConfigService.resolveDefaultModelCode() : modelCode;
+    }
+
+    private String resolveTitle(String title, int index) {
+        if (title == null || title.trim().isEmpty()) {
+            return "鏂板璇� " + index;
+        }
+        return buildPreview(title);
+    }
+
+    private String buildPreview(String content) {
+        if (content == null || content.trim().isEmpty()) {
+            return "鏂板璇�";
+        }
+        String normalized = content.replace("\r", " ").replace("\n", " ").trim();
+        return normalized.length() > 24 ? normalized.substring(0, 24) : normalized;
+    }
+
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiParamController.java b/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiParamController.java
new file mode 100644
index 0000000..badcaec
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiParamController.java
@@ -0,0 +1,138 @@
+package com.vincent.rsf.server.system.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.vincent.rsf.framework.common.Cools;
+import com.vincent.rsf.framework.common.R;
+import com.vincent.rsf.framework.common.SnowflakeIdWorker;
+import com.vincent.rsf.server.common.annotation.OperationLog;
+import com.vincent.rsf.server.common.domain.BaseParam;
+import com.vincent.rsf.server.common.domain.KeyValVo;
+import com.vincent.rsf.server.common.domain.PageParam;
+import com.vincent.rsf.server.common.utils.ExcelUtil;
+import com.vincent.rsf.server.system.entity.AiParam;
+import com.vincent.rsf.server.system.service.AiParamService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+public class AiParamController extends BaseController {
+
+    @Autowired
+    private AiParamService aiParamService;
+    @Autowired
+    private SnowflakeIdWorker snowflakeIdWorker;
+
+    @PreAuthorize("hasAuthority('system:aiParam:list')")
+    @PostMapping("/aiParam/page")
+    public R page(@RequestBody Map<String, Object> map) {
+        BaseParam baseParam = buildParam(map, BaseParam.class);
+        PageParam<AiParam, BaseParam> pageParam = new PageParam<>(baseParam, AiParam.class);
+        return R.ok().add(aiParamService.page(pageParam, pageParam.buildWrapper(true)));
+    }
+
+    @PreAuthorize("hasAuthority('system:aiParam:list')")
+    @PostMapping("/aiParam/list")
+    public R list(@RequestBody Map<String, Object> map) {
+        return R.ok().add(aiParamService.list());
+    }
+
+    @PreAuthorize("hasAuthority('system:aiParam:list')")
+    @PostMapping({"/aiParam/many/{ids}", "/aiParams/many/{ids}"})
+    public R many(@PathVariable Long[] ids) {
+        return R.ok().add(aiParamService.listByIds(Arrays.asList(ids)));
+    }
+
+    @PreAuthorize("hasAuthority('system:aiParam:list')")
+    @GetMapping("/aiParam/{id}")
+    public R get(@PathVariable("id") Long id) {
+        return R.ok().add(aiParamService.getById(id));
+    }
+
+    @PreAuthorize("hasAuthority('system:aiParam:save')")
+    @OperationLog("Create AiParam")
+    @PostMapping("/aiParam/save")
+    public R save(@RequestBody AiParam aiParam) {
+        if (Cools.isEmpty(aiParam.getModelCode())) {
+            return R.error("妯″瀷缂栫爜涓嶈兘涓虹┖");
+        }
+        if (aiParamService.existsModelCode(aiParam.getModelCode(), null)) {
+            return R.error("妯″瀷缂栫爜宸插瓨鍦�");
+        }
+        Date now = new Date();
+        aiParam.setUuid(String.valueOf(snowflakeIdWorker.nextId()).substring(3));
+        aiParam.setCreateBy(getLoginUserId());
+        aiParam.setCreateTime(now);
+        aiParam.setUpdateBy(getLoginUserId());
+        aiParam.setUpdateTime(now);
+        if (aiParam.getDefaultFlag() == null) {
+            aiParam.setDefaultFlag(0);
+        }
+        if (!aiParamService.save(aiParam)) {
+            return R.error("Save Fail");
+        }
+        if (Integer.valueOf(1).equals(aiParam.getDefaultFlag())) {
+            aiParamService.resetDefaultFlag(aiParam.getId());
+        }
+        return R.ok("Save Success").add(aiParam);
+    }
+
+    @PreAuthorize("hasAuthority('system:aiParam:update')")
+    @OperationLog("Update AiParam")
+    @PostMapping("/aiParam/update")
+    public R update(@RequestBody AiParam aiParam) {
+        if (Cools.isEmpty(aiParam.getModelCode())) {
+            return R.error("妯″瀷缂栫爜涓嶈兘涓虹┖");
+        }
+        if (aiParamService.existsModelCode(aiParam.getModelCode(), aiParam.getId())) {
+            return R.error("妯″瀷缂栫爜宸插瓨鍦�");
+        }
+        aiParam.setUpdateBy(getLoginUserId());
+        aiParam.setUpdateTime(new Date());
+        if (!aiParamService.updateById(aiParam)) {
+            return R.error("Update Fail");
+        }
+        if (Integer.valueOf(1).equals(aiParam.getDefaultFlag())) {
+            aiParamService.resetDefaultFlag(aiParam.getId());
+        }
+        return R.ok("Update Success").add(aiParam);
+    }
+
+    @PreAuthorize("hasAuthority('system:aiParam:remove')")
+    @OperationLog("Delete AiParam")
+    @PostMapping("/aiParam/remove/{ids}")
+    public R remove(@PathVariable Long[] ids) {
+        if (!aiParamService.removeByIds(Arrays.asList(ids))) {
+            return R.error("Delete Fail");
+        }
+        return R.ok("Delete Success").add(ids);
+    }
+
+    @PreAuthorize("hasAuthority('system:aiParam:list')")
+    @PostMapping("/aiParam/query")
+    public R query(@RequestParam(required = false) String condition) {
+        List<KeyValVo> vos = new ArrayList<>();
+        LambdaQueryWrapper<AiParam> wrapper = new LambdaQueryWrapper<>();
+        if (!Cools.isEmpty(condition)) {
+            wrapper.like(AiParam::getName, condition).or().like(AiParam::getModelCode, condition);
+        }
+        aiParamService.page(new Page<>(1, 30), wrapper).getRecords().forEach(
+                item -> vos.add(new KeyValVo(item.getId(), item.getName()))
+        );
+        return R.ok().add(vos);
+    }
+
+    @PreAuthorize("hasAuthority('system:aiParam:list')")
+    @PostMapping("/aiParam/export")
+    public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
+        ExcelUtil.build(ExcelUtil.create(aiParamService.list(), AiParam.class), response);
+    }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiParam.java b/rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiParam.java
new file mode 100644
index 0000000..1cac3f7
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiParam.java
@@ -0,0 +1,169 @@
+package com.vincent.rsf.server.system.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.vincent.rsf.framework.common.Cools;
+import com.vincent.rsf.framework.common.SpringUtils;
+import com.vincent.rsf.server.system.service.TenantService;
+import com.vincent.rsf.server.system.service.UserService;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+@Data
+@Accessors(chain = true)
+@TableName("sys_ai_param")
+public class AiParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value= "ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty(value= "缂栧彿")
+    private String uuid;
+
+    @ApiModelProperty(value= "鍚嶇О")
+    private String name;
+
+    @ApiModelProperty(value= "妯″瀷缂栫爜")
+    private String modelCode;
+
+    @ApiModelProperty(value= "渚涘簲鍟�")
+    private String provider;
+
+    @ApiModelProperty(value= "鑱婂ぉ鍦板潃")
+    private String chatUrl;
+
+    @ApiModelProperty(value= "API瀵嗛挜")
+    private String apiKey;
+
+    @ApiModelProperty(value= "妯″瀷鍚嶇О")
+    private String modelName;
+
+    @ApiModelProperty(value= "绯荤粺鎻愮ず璇�")
+    private String systemPrompt;
+
+    @ApiModelProperty(value= "涓婁笅鏂囪疆鏁�")
+    private Integer maxContextMessages;
+
+    @ApiModelProperty(value= "榛樿妯″瀷 1: 鏄�  0: 鍚�")
+    private Integer defaultFlag;
+
+    @ApiModelProperty(value= "鎺掑簭")
+    private Integer sort;
+
+    @ApiModelProperty(value= "鐘舵�� 1: 姝e父  0: 鍐荤粨  ")
+    private Integer status;
+
+    @ApiModelProperty(value= "鏄惁鍒犻櫎 1: 鏄�  0: 鍚�  ")
+    private Integer deleted;
+
+    @ApiModelProperty(value= "绉熸埛")
+    private Long tenantId;
+
+    @ApiModelProperty(value= "娣诲姞浜哄憳")
+    private Long createBy;
+
+    @ApiModelProperty(value= "娣诲姞鏃堕棿")
+    @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
+    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
+    private Date createTime;
+
+    @ApiModelProperty(value= "淇敼浜哄憳")
+    private Long updateBy;
+
+    @ApiModelProperty(value= "淇敼鏃堕棿")
+    @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
+    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
+    private Date updateTime;
+
+    @ApiModelProperty(value= "澶囨敞")
+    private String memo;
+
+    public String getTenantId$(){
+        TenantService service = SpringUtils.getBean(TenantService.class);
+        Tenant tenant = service.getById(this.tenantId);
+        if (!Cools.isEmpty(tenant)){
+            return String.valueOf(tenant.getName());
+        }
+        return null;
+    }
+
+    public String getCreateBy$(){
+        UserService service = SpringUtils.getBean(UserService.class);
+        User user = service.getById(this.createBy);
+        if (!Cools.isEmpty(user)){
+            return String.valueOf(user.getNickname());
+        }
+        return null;
+    }
+
+    public String getCreateTime$(){
+        if (Cools.isEmpty(this.createTime)){
+            return "";
+        }
+        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.createTime);
+    }
+
+    public String getUpdateBy$(){
+        UserService service = SpringUtils.getBean(UserService.class);
+        User user = service.getById(this.updateBy);
+        if (!Cools.isEmpty(user)){
+            return String.valueOf(user.getNickname());
+        }
+        return null;
+    }
+
+    public String getUpdateTime$(){
+        if (Cools.isEmpty(this.updateTime)){
+            return "";
+        }
+        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.updateTime);
+    }
+
+    public Boolean getStatusBool(){
+        if (null == this.status){ return null; }
+        switch (this.status){
+            case 1:
+                return true;
+            case 0:
+                return false;
+            default:
+                return null;
+        }
+    }
+
+    public Boolean getDefaultFlagBool(){
+        if (null == this.defaultFlag){ return null; }
+        switch (this.defaultFlag){
+            case 1:
+                return true;
+            case 0:
+                return false;
+            default:
+                return null;
+        }
+    }
+
+    public String getDefaultFlag$(){
+        if (null == this.defaultFlag){ return null; }
+        switch (this.defaultFlag){
+            case 1:
+                return "鏄�";
+            case 0:
+                return "鍚�";
+            default:
+                return String.valueOf(this.defaultFlag);
+        }
+    }
+
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiParamMapper.java b/rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiParamMapper.java
new file mode 100644
index 0000000..8c2d513
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiParamMapper.java
@@ -0,0 +1,12 @@
+package com.vincent.rsf.server.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.vincent.rsf.server.system.entity.AiParam;
+import org.apache.ibatis.annotations.Mapper;
+import org.springframework.stereotype.Repository;
+
+@Mapper
+@Repository
+public interface AiParamMapper extends BaseMapper<AiParam> {
+
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiParamService.java b/rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiParamService.java
new file mode 100644
index 0000000..39900e8
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiParamService.java
@@ -0,0 +1,19 @@
+package com.vincent.rsf.server.system.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.vincent.rsf.server.system.entity.AiParam;
+
+import java.util.List;
+
+public interface AiParamService extends IService<AiParam> {
+
+    boolean existsModelCode(String modelCode, Long excludeId);
+
+    void resetDefaultFlag(Long excludeId);
+
+    List<AiParam> listEnabledModels();
+
+    AiParam getEnabledModel(String modelCode);
+
+    AiParam getDefaultModel();
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiParamServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiParamServiceImpl.java
new file mode 100644
index 0000000..d4518f7
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiParamServiceImpl.java
@@ -0,0 +1,66 @@
+package com.vincent.rsf.server.system.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.vincent.rsf.server.system.entity.AiParam;
+import com.vincent.rsf.server.system.mapper.AiParamMapper;
+import com.vincent.rsf.server.system.service.AiParamService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service("aiParamService")
+public class AiParamServiceImpl extends ServiceImpl<AiParamMapper, AiParam> implements AiParamService {
+
+    @Override
+    public boolean existsModelCode(String modelCode, Long excludeId) {
+        LambdaQueryWrapper<AiParam> wrapper = new LambdaQueryWrapper<AiParam>()
+                .eq(AiParam::getModelCode, modelCode);
+        if (excludeId != null) {
+            wrapper.ne(AiParam::getId, excludeId);
+        }
+        return this.count(wrapper) > 0;
+    }
+
+    @Override
+    public void resetDefaultFlag(Long excludeId) {
+        LambdaUpdateWrapper<AiParam> wrapper = new LambdaUpdateWrapper<AiParam>()
+                .set(AiParam::getDefaultFlag, 0)
+                .eq(AiParam::getDefaultFlag, 1);
+        if (excludeId != null) {
+            wrapper.ne(AiParam::getId, excludeId);
+        }
+        this.update(wrapper);
+    }
+
+    @Override
+    public List<AiParam> listEnabledModels() {
+        return this.list(new LambdaQueryWrapper<AiParam>()
+                .eq(AiParam::getStatus, 1)
+                .orderByDesc(AiParam::getDefaultFlag)
+                .orderByAsc(AiParam::getSort, AiParam::getId));
+    }
+
+    @Override
+    public AiParam getEnabledModel(String modelCode) {
+        return this.getOne(new LambdaQueryWrapper<AiParam>()
+                .eq(AiParam::getModelCode, modelCode)
+                .eq(AiParam::getStatus, 1)
+                .last("limit 1"));
+    }
+
+    @Override
+    public AiParam getDefaultModel() {
+        AiParam aiParam = this.getOne(new LambdaQueryWrapper<AiParam>()
+                .eq(AiParam::getDefaultFlag, 1)
+                .eq(AiParam::getStatus, 1)
+                .orderByAsc(AiParam::getSort, AiParam::getId)
+                .last("limit 1"));
+        if (aiParam != null) {
+            return aiParam;
+        }
+        List<AiParam> list = listEnabledModels();
+        return list.isEmpty() ? null : list.get(0);
+    }
+}
diff --git a/rsf-server/src/main/resources/application-dev.yml b/rsf-server/src/main/resources/application-dev.yml
index 2bef992..dcced99 100644
--- a/rsf-server/src/main/resources/application-dev.yml
+++ b/rsf-server/src/main/resources/application-dev.yml
@@ -99,6 +99,9 @@
       #绔彛鍙�
       port: 8081
 
+ai:
+  gateway-base-url: http://127.0.0.1:8086
+
 #浠撳簱鍔熻兘鍙傛暟閰嶇疆
 stock:
   #鏄惁鍏佽鎵撳嵃璐х墿鏍囩锛� 榛樿鍏佽鎵撳嵃锛屼篃鍙敱渚涘簲鍟嗘彁渚涙爣绛�
@@ -110,4 +113,4 @@
     #鍒ゆ柇鏄悗妫�楠屽悎鏍煎悗锛屾墠鍏佽涓婃灦
     flagAvailable: true
     #鍒ゆ柇鏄惁鏍¢獙鍚堟牸鍚庯紝鎵嶅厑璁告敹璐�
-    flagReceiving: false
\ No newline at end of file
+    flagReceiving: false
diff --git a/rsf-server/src/main/resources/application-prod.yml b/rsf-server/src/main/resources/application-prod.yml
index ac13fe0..86bec2e 100644
--- a/rsf-server/src/main/resources/application-prod.yml
+++ b/rsf-server/src/main/resources/application-prod.yml
@@ -103,4 +103,7 @@
     #鍒ゆ柇鏄悗妫�楠屽悎鏍煎悗锛屾墠鍏佽涓婃灦
     flagAvailable: true
     #鍒ゆ柇鏄惁鏍¢獙鍚堟牸鍚庯紝鎵嶅厑璁告敹璐�
-    flagReceiving: false
\ No newline at end of file
+    flagReceiving: false
+
+ai:
+  gateway-base-url: http://127.0.0.1:8086
diff --git a/rsf-server/src/main/resources/application.yml b/rsf-server/src/main/resources/application.yml
index 0c99aef..8fae7b8 100644
--- a/rsf-server/src/main/resources/application.yml
+++ b/rsf-server/src/main/resources/application.yml
@@ -44,6 +44,21 @@
   file:
     path: logs/@pom.artifactId@
 
+ai:
+  session-ttl-seconds: 86400
+  max-context-messages: 12
+  default-model-code: mock-general
+  system-prompt: 浣犳槸WMS绯荤粺鍐呯殑鏅鸿兘鍔╂墜锛屽洖绛旀椂浼樺厛淇濇寔鍑嗙‘銆佺畝娲侊紝骞剁粨鍚堜笂涓嬫枃甯姪鐢ㄦ埛鐞嗚В浠撳偍涓氬姟銆�
+  models:
+    - code: mock-general
+      name: Mock General
+      provider: mock
+      enabled: true
+    - code: mock-creative
+      name: Mock Creative
+      provider: mock
+      enabled: true
+
 # 涓嬩綅鏈洪厤缃�
 wcs-slave:
   agv: false
diff --git a/rsf-server/src/main/resources/mapper/system/AiParamMapper.xml b/rsf-server/src/main/resources/mapper/system/AiParamMapper.xml
new file mode 100644
index 0000000..43b0299
--- /dev/null
+++ b/rsf-server/src/main/resources/mapper/system/AiParamMapper.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.vincent.rsf.server.system.mapper.AiParamMapper">
+
+</mapper>
diff --git a/version/db/20260311_ai_param.sql b/version/db/20260311_ai_param.sql
new file mode 100644
index 0000000..9349c1f
--- /dev/null
+++ b/version/db/20260311_ai_param.sql
@@ -0,0 +1,270 @@
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+CREATE TABLE IF NOT EXISTS `sys_ai_param` (
+  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `uuid` varchar(255) DEFAULT NULL COMMENT '缂栧彿',
+  `name` varchar(255) DEFAULT NULL COMMENT '鍚嶇О',
+  `model_code` varchar(255) DEFAULT NULL COMMENT '妯″瀷缂栫爜',
+  `provider` varchar(255) DEFAULT NULL COMMENT '渚涘簲鍟�',
+  `chat_url` varchar(512) DEFAULT NULL COMMENT '鑱婂ぉ鍦板潃',
+  `api_key` varchar(512) DEFAULT NULL COMMENT 'API瀵嗛挜',
+  `model_name` varchar(255) DEFAULT NULL COMMENT '妯″瀷鍚嶇О',
+  `system_prompt` text COMMENT '绯荤粺鎻愮ず璇�',
+  `max_context_messages` int(11) DEFAULT NULL COMMENT '涓婁笅鏂囪疆鏁�',
+  `default_flag` int(1) NOT NULL DEFAULT '0' COMMENT '榛樿妯″瀷{1:鏄�,0:鍚',
+  `sort` int(11) DEFAULT NULL COMMENT '鎺掑簭',
+  `status` int(1) NOT NULL DEFAULT '1' COMMENT '鐘舵�亄1:姝e父,0:鍐荤粨}',
+  `deleted` int(1) NOT NULL DEFAULT '0' COMMENT '鏄惁鍒犻櫎{1:鏄�,0:鍚',
+  `tenant_id` bigint(20) DEFAULT NULL COMMENT '绉熸埛[sys_tenant]',
+  `create_by` bigint(20) DEFAULT NULL COMMENT '娣诲姞浜哄憳[sys_user]',
+  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '娣诲姞鏃堕棿',
+  `update_by` bigint(20) DEFAULT NULL COMMENT '淇敼浜哄憳[sys_user]',
+  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '淇敼鏃堕棿',
+  `memo` varchar(255) DEFAULT NULL COMMENT '澶囨敞',
+  PRIMARY KEY (`id`),
+  KEY `idx_ai_param_model_code` (`model_code`),
+  KEY `idx_ai_param_deleted_code` (`deleted`,`model_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO `sys_ai_param`
+(`uuid`, `name`, `model_code`, `provider`, `chat_url`, `api_key`, `model_name`, `system_prompt`, `max_context_messages`, `default_flag`, `sort`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`)
+SELECT '6702082748514305', '閫氱敤鍔╂墜', 'mock-general', 'mock', NULL, NULL, 'mock-general', '浣犳槸WMS绯荤粺鍐呯殑鏅鸿兘鍔╂墜锛屽洖绛旀椂浼樺厛淇濇寔鍑嗙‘銆佺畝娲侊紝骞剁粨鍚堜笂涓嬫枃甯姪鐢ㄦ埛鐞嗚В浠撳偍涓氬姟銆�', 12, 1, 1, 1, 0, 1, 2, NOW(), 2, NOW(), '榛樿婕旂ず妯″瀷'
+FROM DUAL
+WHERE NOT EXISTS (
+    SELECT 1
+    FROM `sys_ai_param`
+    WHERE `model_code` = 'mock-general'
+);
+
+INSERT INTO `sys_ai_param`
+(`uuid`, `name`, `model_code`, `provider`, `chat_url`, `api_key`, `model_name`, `system_prompt`, `max_context_messages`, `default_flag`, `sort`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`)
+SELECT '6702082748514306', '鍒涙剰鍔╂墜', 'mock-creative', 'mock', NULL, NULL, 'mock-creative', '浣犳槸WMS绯荤粺鍐呯殑鏅鸿兘鍔╂墜锛屽洖绛旀椂鍙互鏇寸伒娲诲湴缁勭粐琛ㄨ揪锛屼絾缁撹蹇呴』鍑嗙‘銆�', 12, 0, 2, 1, 0, 1, 2, NOW(), 2, NOW(), '婕旂ず鍒涙剰妯″瀷'
+FROM DUAL
+WHERE NOT EXISTS (
+    SELECT 1
+    FROM `sys_ai_param`
+    WHERE `model_code` = 'mock-creative'
+);
+
+SET @ai_parent_menu_id := (
+    SELECT `id`
+    FROM `sys_menu`
+    WHERE `component` = 'aiParam'
+    LIMIT 1
+);
+
+INSERT INTO `sys_menu`
+(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
+SELECT 'menu.aiParam', 1, 'menu.system', '1', 'menu.system', '/system/aiParam', 'aiParam', NULL, NULL, 0, NULL, 'SmartToy', 9, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
+FROM DUAL
+WHERE @ai_parent_menu_id IS NULL;
+
+SET @ai_parent_menu_id := COALESCE(
+    @ai_parent_menu_id,
+    LAST_INSERT_ID(),
+    (
+        SELECT `id`
+        FROM `sys_menu`
+        WHERE `component` = 'aiParam'
+        LIMIT 1
+    )
+);
+
+SET @ai_query_menu_id := (
+    SELECT `id`
+    FROM `sys_menu`
+    WHERE `parent_id` = @ai_parent_menu_id
+      AND `name` = 'Query AiParam'
+    LIMIT 1
+);
+
+INSERT INTO `sys_menu`
+(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
+SELECT 'Query AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:list', NULL, 0, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
+FROM DUAL
+WHERE @ai_query_menu_id IS NULL;
+
+SET @ai_query_menu_id := COALESCE(
+    @ai_query_menu_id,
+    LAST_INSERT_ID(),
+    (
+        SELECT `id`
+        FROM `sys_menu`
+        WHERE `parent_id` = @ai_parent_menu_id
+          AND `name` = 'Query AiParam'
+        LIMIT 1
+    )
+);
+
+SET @ai_create_menu_id := (
+    SELECT `id`
+    FROM `sys_menu`
+    WHERE `parent_id` = @ai_parent_menu_id
+      AND `name` = 'Create AiParam'
+    LIMIT 1
+);
+
+INSERT INTO `sys_menu`
+(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
+SELECT 'Create AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:save', NULL, 1, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
+FROM DUAL
+WHERE @ai_create_menu_id IS NULL;
+
+SET @ai_create_menu_id := COALESCE(
+    @ai_create_menu_id,
+    LAST_INSERT_ID(),
+    (
+        SELECT `id`
+        FROM `sys_menu`
+        WHERE `parent_id` = @ai_parent_menu_id
+          AND `name` = 'Create AiParam'
+        LIMIT 1
+    )
+);
+
+SET @ai_update_menu_id := (
+    SELECT `id`
+    FROM `sys_menu`
+    WHERE `parent_id` = @ai_parent_menu_id
+      AND `name` = 'Update AiParam'
+    LIMIT 1
+);
+
+INSERT INTO `sys_menu`
+(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
+SELECT 'Update AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:update', NULL, 2, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
+FROM DUAL
+WHERE @ai_update_menu_id IS NULL;
+
+SET @ai_update_menu_id := COALESCE(
+    @ai_update_menu_id,
+    LAST_INSERT_ID(),
+    (
+        SELECT `id`
+        FROM `sys_menu`
+        WHERE `parent_id` = @ai_parent_menu_id
+          AND `name` = 'Update AiParam'
+        LIMIT 1
+    )
+);
+
+SET @ai_delete_menu_id := (
+    SELECT `id`
+    FROM `sys_menu`
+    WHERE `parent_id` = @ai_parent_menu_id
+      AND `name` = 'Delete AiParam'
+    LIMIT 1
+);
+
+INSERT INTO `sys_menu`
+(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
+SELECT 'Delete AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:remove', NULL, 3, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
+FROM DUAL
+WHERE @ai_delete_menu_id IS NULL;
+
+SET @ai_delete_menu_id := COALESCE(
+    @ai_delete_menu_id,
+    LAST_INSERT_ID(),
+    (
+        SELECT `id`
+        FROM `sys_menu`
+        WHERE `parent_id` = @ai_parent_menu_id
+          AND `name` = 'Delete AiParam'
+        LIMIT 1
+    )
+);
+
+SET @ai_export_menu_id := (
+    SELECT `id`
+    FROM `sys_menu`
+    WHERE `parent_id` = @ai_parent_menu_id
+      AND `name` = 'Export AiParam'
+    LIMIT 1
+);
+
+INSERT INTO `sys_menu`
+(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
+SELECT 'Export AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:list', NULL, 4, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
+FROM DUAL
+WHERE @ai_export_menu_id IS NULL;
+
+SET @ai_export_menu_id := COALESCE(
+    @ai_export_menu_id,
+    LAST_INSERT_ID(),
+    (
+        SELECT `id`
+        FROM `sys_menu`
+        WHERE `parent_id` = @ai_parent_menu_id
+          AND `name` = 'Export AiParam'
+        LIMIT 1
+    )
+);
+
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
+SELECT 1, @ai_parent_menu_id
+FROM DUAL
+WHERE @ai_parent_menu_id IS NOT NULL
+  AND NOT EXISTS (
+      SELECT 1
+      FROM `sys_role_menu`
+      WHERE `role_id` = 1
+        AND `menu_id` = @ai_parent_menu_id
+  );
+
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
+SELECT 1, @ai_query_menu_id
+FROM DUAL
+WHERE @ai_query_menu_id IS NOT NULL
+  AND NOT EXISTS (
+      SELECT 1
+      FROM `sys_role_menu`
+      WHERE `role_id` = 1
+        AND `menu_id` = @ai_query_menu_id
+  );
+
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
+SELECT 1, @ai_create_menu_id
+FROM DUAL
+WHERE @ai_create_menu_id IS NOT NULL
+  AND NOT EXISTS (
+      SELECT 1
+      FROM `sys_role_menu`
+      WHERE `role_id` = 1
+        AND `menu_id` = @ai_create_menu_id
+  );
+
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
+SELECT 1, @ai_update_menu_id
+FROM DUAL
+WHERE @ai_update_menu_id IS NOT NULL
+  AND NOT EXISTS (
+      SELECT 1
+      FROM `sys_role_menu`
+      WHERE `role_id` = 1
+        AND `menu_id` = @ai_update_menu_id
+  );
+
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
+SELECT 1, @ai_delete_menu_id
+FROM DUAL
+WHERE @ai_delete_menu_id IS NOT NULL
+  AND NOT EXISTS (
+      SELECT 1
+      FROM `sys_role_menu`
+      WHERE `role_id` = 1
+        AND `menu_id` = @ai_delete_menu_id
+  );
+
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
+SELECT 1, @ai_export_menu_id
+FROM DUAL
+WHERE @ai_export_menu_id IS NOT NULL
+  AND NOT EXISTS (
+      SELECT 1
+      FROM `sys_role_menu`
+      WHERE `role_id` = 1
+        AND `menu_id` = @ai_export_menu_id
+  );
+
+SET FOREIGN_KEY_CHECKS = 1;
diff --git a/version/db/20260311_ai_param_menu.sql b/version/db/20260311_ai_param_menu.sql
new file mode 100644
index 0000000..0ec3a0b
--- /dev/null
+++ b/version/db/20260311_ai_param_menu.sql
@@ -0,0 +1,224 @@
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+SET @ai_parent_menu_id := (
+    SELECT `id`
+    FROM `sys_menu`
+    WHERE `component` = 'aiParam'
+    LIMIT 1
+);
+
+INSERT INTO `sys_menu`
+(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
+SELECT 'menu.aiParam', 1, 'menu.system', '1', 'menu.system', '/system/aiParam', 'aiParam', NULL, NULL, 0, NULL, 'SmartToy', 9, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
+FROM DUAL
+WHERE @ai_parent_menu_id IS NULL;
+
+SET @ai_parent_menu_id := COALESCE(
+    @ai_parent_menu_id,
+    LAST_INSERT_ID(),
+    (
+        SELECT `id`
+        FROM `sys_menu`
+        WHERE `component` = 'aiParam'
+        LIMIT 1
+    )
+);
+
+SET @ai_query_menu_id := (
+    SELECT `id`
+    FROM `sys_menu`
+    WHERE `parent_id` = @ai_parent_menu_id
+      AND `name` = 'Query AiParam'
+    LIMIT 1
+);
+
+INSERT INTO `sys_menu`
+(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
+SELECT 'Query AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:list', NULL, 0, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
+FROM DUAL
+WHERE @ai_query_menu_id IS NULL;
+
+SET @ai_query_menu_id := COALESCE(
+    @ai_query_menu_id,
+    LAST_INSERT_ID(),
+    (
+        SELECT `id`
+        FROM `sys_menu`
+        WHERE `parent_id` = @ai_parent_menu_id
+          AND `name` = 'Query AiParam'
+        LIMIT 1
+    )
+);
+
+SET @ai_create_menu_id := (
+    SELECT `id`
+    FROM `sys_menu`
+    WHERE `parent_id` = @ai_parent_menu_id
+      AND `name` = 'Create AiParam'
+    LIMIT 1
+);
+
+INSERT INTO `sys_menu`
+(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
+SELECT 'Create AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:save', NULL, 1, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
+FROM DUAL
+WHERE @ai_create_menu_id IS NULL;
+
+SET @ai_create_menu_id := COALESCE(
+    @ai_create_menu_id,
+    LAST_INSERT_ID(),
+    (
+        SELECT `id`
+        FROM `sys_menu`
+        WHERE `parent_id` = @ai_parent_menu_id
+          AND `name` = 'Create AiParam'
+        LIMIT 1
+    )
+);
+
+SET @ai_update_menu_id := (
+    SELECT `id`
+    FROM `sys_menu`
+    WHERE `parent_id` = @ai_parent_menu_id
+      AND `name` = 'Update AiParam'
+    LIMIT 1
+);
+
+INSERT INTO `sys_menu`
+(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
+SELECT 'Update AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:update', NULL, 2, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
+FROM DUAL
+WHERE @ai_update_menu_id IS NULL;
+
+SET @ai_update_menu_id := COALESCE(
+    @ai_update_menu_id,
+    LAST_INSERT_ID(),
+    (
+        SELECT `id`
+        FROM `sys_menu`
+        WHERE `parent_id` = @ai_parent_menu_id
+          AND `name` = 'Update AiParam'
+        LIMIT 1
+    )
+);
+
+SET @ai_delete_menu_id := (
+    SELECT `id`
+    FROM `sys_menu`
+    WHERE `parent_id` = @ai_parent_menu_id
+      AND `name` = 'Delete AiParam'
+    LIMIT 1
+);
+
+INSERT INTO `sys_menu`
+(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
+SELECT 'Delete AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:remove', NULL, 3, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
+FROM DUAL
+WHERE @ai_delete_menu_id IS NULL;
+
+SET @ai_delete_menu_id := COALESCE(
+    @ai_delete_menu_id,
+    LAST_INSERT_ID(),
+    (
+        SELECT `id`
+        FROM `sys_menu`
+        WHERE `parent_id` = @ai_parent_menu_id
+          AND `name` = 'Delete AiParam'
+        LIMIT 1
+    )
+);
+
+SET @ai_export_menu_id := (
+    SELECT `id`
+    FROM `sys_menu`
+    WHERE `parent_id` = @ai_parent_menu_id
+      AND `name` = 'Export AiParam'
+    LIMIT 1
+);
+
+INSERT INTO `sys_menu`
+(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
+SELECT 'Export AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:list', NULL, 4, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
+FROM DUAL
+WHERE @ai_export_menu_id IS NULL;
+
+SET @ai_export_menu_id := COALESCE(
+    @ai_export_menu_id,
+    LAST_INSERT_ID(),
+    (
+        SELECT `id`
+        FROM `sys_menu`
+        WHERE `parent_id` = @ai_parent_menu_id
+          AND `name` = 'Export AiParam'
+        LIMIT 1
+    )
+);
+
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
+SELECT 1, @ai_parent_menu_id
+FROM DUAL
+WHERE @ai_parent_menu_id IS NOT NULL
+  AND NOT EXISTS (
+      SELECT 1
+      FROM `sys_role_menu`
+      WHERE `role_id` = 1
+        AND `menu_id` = @ai_parent_menu_id
+  );
+
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
+SELECT 1, @ai_query_menu_id
+FROM DUAL
+WHERE @ai_query_menu_id IS NOT NULL
+  AND NOT EXISTS (
+      SELECT 1
+      FROM `sys_role_menu`
+      WHERE `role_id` = 1
+        AND `menu_id` = @ai_query_menu_id
+  );
+
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
+SELECT 1, @ai_create_menu_id
+FROM DUAL
+WHERE @ai_create_menu_id IS NOT NULL
+  AND NOT EXISTS (
+      SELECT 1
+      FROM `sys_role_menu`
+      WHERE `role_id` = 1
+        AND `menu_id` = @ai_create_menu_id
+  );
+
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
+SELECT 1, @ai_update_menu_id
+FROM DUAL
+WHERE @ai_update_menu_id IS NOT NULL
+  AND NOT EXISTS (
+      SELECT 1
+      FROM `sys_role_menu`
+      WHERE `role_id` = 1
+        AND `menu_id` = @ai_update_menu_id
+  );
+
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
+SELECT 1, @ai_delete_menu_id
+FROM DUAL
+WHERE @ai_delete_menu_id IS NOT NULL
+  AND NOT EXISTS (
+      SELECT 1
+      FROM `sys_role_menu`
+      WHERE `role_id` = 1
+        AND `menu_id` = @ai_delete_menu_id
+  );
+
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
+SELECT 1, @ai_export_menu_id
+FROM DUAL
+WHERE @ai_export_menu_id IS NOT NULL
+  AND NOT EXISTS (
+      SELECT 1
+      FROM `sys_role_menu`
+      WHERE `role_id` = 1
+        AND `menu_id` = @ai_export_menu_id
+  );
+
+SET FOREIGN_KEY_CHECKS = 1;
diff --git a/version/db/init.sql b/version/db/init.sql
index 6c56f06..c84ebee 100644
--- a/version/db/init.sql
+++ b/version/db/init.sql
@@ -50,6 +50,44 @@
 COMMIT;
 
 -- ----------------------------
+-- Table structure for sys_ai_param
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_ai_param`;
+CREATE TABLE `sys_ai_param` (
+  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `uuid` varchar(255) DEFAULT NULL COMMENT '缂栧彿',
+  `name` varchar(255) DEFAULT NULL COMMENT '鍚嶇О',
+  `model_code` varchar(255) DEFAULT NULL COMMENT '妯″瀷缂栫爜',
+  `provider` varchar(255) DEFAULT NULL COMMENT '渚涘簲鍟�',
+  `chat_url` varchar(512) DEFAULT NULL COMMENT '鑱婂ぉ鍦板潃',
+  `api_key` varchar(512) DEFAULT NULL COMMENT 'API瀵嗛挜',
+  `model_name` varchar(255) DEFAULT NULL COMMENT '妯″瀷鍚嶇О',
+  `system_prompt` text COMMENT '绯荤粺鎻愮ず璇�',
+  `max_context_messages` int(11) DEFAULT NULL COMMENT '涓婁笅鏂囪疆鏁�',
+  `default_flag` int(1) NOT NULL DEFAULT '0' COMMENT '榛樿妯″瀷{1:鏄�,0:鍚',
+  `sort` int(11) DEFAULT NULL COMMENT '鎺掑簭',
+  `status` int(1) NOT NULL DEFAULT '1' COMMENT '鐘舵�亄1:姝e父,0:鍐荤粨}',
+  `deleted` int(1) NOT NULL DEFAULT '0' COMMENT '鏄惁鍒犻櫎{1:鏄�,0:鍚',
+  `tenant_id` bigint(20) DEFAULT NULL COMMENT '绉熸埛[sys_tenant]',
+  `create_by` bigint(20) DEFAULT NULL COMMENT '娣诲姞浜哄憳[sys_user]',
+  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '娣诲姞鏃堕棿',
+  `update_by` bigint(20) DEFAULT NULL COMMENT '淇敼浜哄憳[sys_user]',
+  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '淇敼鏃堕棿',
+  `memo` varchar(255) DEFAULT NULL COMMENT '澶囨敞',
+  PRIMARY KEY (`id`),
+  KEY `idx_ai_param_model_code` (`model_code`),
+  KEY `idx_ai_param_deleted_code` (`deleted`,`model_code`)
+) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
+
+-- ----------------------------
+-- Records of sys_ai_param
+-- ----------------------------
+BEGIN;
+INSERT INTO `sys_ai_param` (`id`, `uuid`, `name`, `model_code`, `provider`, `chat_url`, `api_key`, `model_name`, `system_prompt`, `max_context_messages`, `default_flag`, `sort`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`) VALUES (1, '6702082748514305', '閫氱敤鍔╂墜', 'mock-general', 'mock', NULL, NULL, 'mock-general', '浣犳槸WMS绯荤粺鍐呯殑鏅鸿兘鍔╂墜锛屽洖绛旀椂浼樺厛淇濇寔鍑嗙‘銆佺畝娲侊紝骞剁粨鍚堜笂涓嬫枃甯姪鐢ㄦ埛鐞嗚В浠撳偍涓氬姟銆�', 12, 1, 1, 1, 0, 1, 2, '2025-02-05 14:16:51', 2, '2025-02-05 14:16:51', '榛樿婕旂ず妯″瀷');
+INSERT INTO `sys_ai_param` (`id`, `uuid`, `name`, `model_code`, `provider`, `chat_url`, `api_key`, `model_name`, `system_prompt`, `max_context_messages`, `default_flag`, `sort`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`) VALUES (2, '6702082748514306', '鍒涙剰鍔╂墜', 'mock-creative', 'mock', NULL, NULL, 'mock-creative', '浣犳槸WMS绯荤粺鍐呯殑鏅鸿兘鍔╂墜锛屽洖绛旀椂鍙互鏇寸伒娲诲湴缁勭粐琛ㄨ揪锛屼絾缁撹蹇呴』鍑嗙‘銆�', 12, 0, 2, 1, 0, 1, 2, '2025-02-05 14:16:51', 2, '2025-02-05 14:16:51', '婕旂ず鍒涙剰妯″瀷');
+COMMIT;
+
+-- ----------------------------
 -- Table structure for sys_dept
 -- ----------------------------
 DROP TABLE IF EXISTS `sys_dept`;
@@ -198,6 +236,12 @@
 INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (44, 'Create Tenant', 42, NULL, '1.42', NULL, NULL, NULL, NULL, NULL, 1, 'system:tenant:save', NULL, 1, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL);
 INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (45, 'Update Tenant', 42, NULL, '1.42', NULL, NULL, NULL, NULL, NULL, 1, 'system:tenant:update', NULL, 2, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL);
 INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (46, 'Delete Tenant', 42, NULL, '1.42', NULL, NULL, NULL, NULL, NULL, 1, 'system:tenant:remove', NULL, 3, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (47, 'menu.aiParam', 1, 'menu.system', '1', 'menu.system', '/system/aiParam', 'aiParam', NULL, NULL, 0, NULL, 'SmartToy', 9, NULL, 1, 1, 0, NULL, NULL, '2024-09-10 15:08:30', 2, NULL);
+INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (48, 'Query AiParam', 47, NULL, '1,47', NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:list', NULL, 0, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (49, 'Create AiParam', 47, NULL, '1,47', NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:save', NULL, 1, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (50, 'Update AiParam', 47, NULL, '1,47', NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:update', NULL, 2, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (51, 'Delete AiParam', 47, NULL, '1,47', NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:remove', NULL, 3, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (52, 'Export AiParam', 47, NULL, '1,47', NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:list', NULL, 4, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL);
 COMMIT;
 
 -- ----------------------------
@@ -345,6 +389,12 @@
 INSERT INTO `sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (44, 1, 44);
 INSERT INTO `sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (45, 1, 45);
 INSERT INTO `sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (46, 1, 46);
+INSERT INTO `sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (47, 1, 47);
+INSERT INTO `sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (48, 1, 48);
+INSERT INTO `sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (49, 1, 49);
+INSERT INTO `sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (50, 1, 50);
+INSERT INTO `sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (51, 1, 51);
+INSERT INTO `sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (52, 1, 52);
 COMMIT;
 
 -- ----------------------------

--
Gitblit v1.9.1