| | |
| | | <module>rsf-framework</module> |
| | | <module>rsf-server</module> |
| | | <module>rsf-open-api</module> |
| | | <module>rsf-ai-gateway</module> |
| | | </modules> |
| | | |
| | | <properties> |
| New file |
| | |
| | | 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 ? '正在生成回复...' : '')} |
| | | </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> |
| | | </> |
| | | ); |
| | | }; |
| New file |
| | |
| | | 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; |
| | | } |
| | |
| | | token: 'Token', |
| | | operation: 'Operation', |
| | | config: 'Config', |
| | | aiParam: 'AI Params', |
| | | tenant: 'Tenant', |
| | | userLogin: 'Token', |
| | | customer: 'Customer', |
| | |
| | | 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", |
| | |
| | | token: '登录日志', |
| | | operation: '操作日志', |
| | | config: '配置参数', |
| | | aiParam: 'AI参数', |
| | | tenant: '租户管理', |
| | | userLogin: '登录日志', |
| | | customer: '客户表', |
| | |
| | | content: "配置内容", |
| | | type: "数据类型", |
| | | }, |
| | | aiParam: { |
| | | uuid: "编号", |
| | | name: "名称", |
| | | modelCode: "模型编码", |
| | | provider: "供应商", |
| | | chatUrl: "聊天地址", |
| | | apiKey: "API密钥", |
| | | modelName: "模型名称", |
| | | systemPrompt: "系统提示词", |
| | | maxContextMessages: "上下文轮数", |
| | | defaultFlag: "默认模型", |
| | | sort: "排序", |
| | | }, |
| | | tenant: { |
| | | name: "租户名", |
| | | flag: "代码", |
| | |
| | | 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 /> |
| | | </> |
| | | ); |
| | |
| | | |
| | | 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"; |
| | |
| | | return host; |
| | | case "config": |
| | | return config; |
| | | case "aiParam": |
| | | return aiParam; |
| | | case "tenant": |
| | | return tenant; |
| | | case "role": |
| | |
| | | } |
| | | }; |
| | | |
| | | export default ResourceContent; |
| | | export default ResourceContent; |
| New file |
| | |
| | | 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、deepseek-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; |
| New file |
| | |
| | | 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、deepseek-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; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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}` |
| | | } |
| | | }; |
| New file |
| | |
| | | [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 |
| New file |
| | |
| | | <?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> |
| New file |
| | |
| | | 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); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | 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; |
| | | |
| | | } |
| New file |
| | |
| | | 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<>(); |
| | | |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | 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; |
| | | |
| | | } |
| New file |
| | |
| | | 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 |
| New file |
| | |
| | | --- |
| | | 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. |
| New file |
| | |
| | | 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." |
| New file |
| | |
| | | # 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`. |
| New file |
| | |
| | | #!/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()) |
| New file |
| | |
| | | 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; |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | 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(); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | 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; |
| | | |
| | | } |
| New file |
| | |
| | | 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; |
| | | |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.ai.dto; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | |
| | | @Data |
| | | public class AiSessionRenameRequest implements Serializable { |
| | | |
| | | private String title; |
| | | |
| | | } |
| New file |
| | |
| | | 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; |
| | | |
| | | } |
| New file |
| | |
| | | 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<>(); |
| | | |
| | | } |
| New file |
| | |
| | | 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; |
| | | |
| | | } |
| New file |
| | |
| | | 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; |
| | | |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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); |
| | | |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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<>(); |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| New file |
| | |
| | | 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: 正常 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); |
| | | } |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | 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> { |
| | | |
| | | } |
| New file |
| | |
| | | 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(); |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | |
| | | #端口号 |
| | | port: 8081 |
| | | |
| | | ai: |
| | | gateway-base-url: http://127.0.0.1:8086 |
| | | |
| | | #仓库功能参数配置 |
| | | stock: |
| | | #是否允许打印货物标签, 默认允许打印,也可由供应商提供标签 |
| | |
| | | #判断是后检验合格后,才允许上架 |
| | | flagAvailable: true |
| | | #判断是否校验合格后,才允许收货 |
| | | flagReceiving: false |
| | | flagReceiving: false |
| | |
| | | #判断是后检验合格后,才允许上架 |
| | | flagAvailable: true |
| | | #判断是否校验合格后,才允许收货 |
| | | flagReceiving: false |
| | | flagReceiving: false |
| | | |
| | | ai: |
| | | gateway-base-url: http://127.0.0.1:8086 |
| | |
| | | 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
|
| New file |
| | |
| | | <?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> |
| New file |
| | |
| | | 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:正常,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; |
| New file |
| | |
| | | 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; |
| | |
| | | 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:正常,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`; |
| | |
| | | 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; |
| | | |
| | | -- ---------------------------- |
| | |
| | | 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; |
| | | |
| | | -- ---------------------------- |