| | |
| | | throw new Error(msg || "获取 AI 运行时信息失败"); |
| | | }; |
| | | |
| | | export const getAiSessions = async (promptCode = "home.default") => { |
| | | export const getAiSessions = async (promptCode = "home.default", keyword = "") => { |
| | | const res = await request.get("ai/chat/sessions", { |
| | | params: { promptCode }, |
| | | params: { promptCode, keyword }, |
| | | }); |
| | | const { code, msg, data } = res.data; |
| | | if (code === 200) { |
| | |
| | | throw new Error(msg || "删除 AI 会话失败"); |
| | | }; |
| | | |
| | | export const renameAiSession = async (sessionId, title) => { |
| | | const res = await request.post(`ai/chat/session/rename/${sessionId}`, { title }); |
| | | const { code, msg, data } = res.data; |
| | | if (code === 200) { |
| | | return data; |
| | | } |
| | | throw new Error(msg || "重命名 AI 会话失败"); |
| | | }; |
| | | |
| | | export const pinAiSession = async (sessionId, pinned) => { |
| | | const res = await request.post(`ai/chat/session/pin/${sessionId}`, { pinned }); |
| | | const { code, msg, data } = res.data; |
| | | if (code === 200) { |
| | | return data; |
| | | } |
| | | throw new Error(msg || "更新 AI 会话置顶状态失败"); |
| | | }; |
| | | |
| | | export const streamAiChat = async (payload, { signal, onEvent } = {}) => { |
| | | const token = getToken(); |
| | | const response = await fetch(`${PREFIX_BASE_URL}ai/chat/stream`, { |
| | |
| | | Box, |
| | | Button, |
| | | Chip, |
| | | Dialog, |
| | | DialogActions, |
| | | DialogContent, |
| | | DialogTitle, |
| | | Divider, |
| | | Drawer, |
| | | IconButton, |
| | |
| | | import CloseIcon from "@mui/icons-material/Close"; |
| | | import AddCommentOutlinedIcon from "@mui/icons-material/AddCommentOutlined"; |
| | | import DeleteOutlineOutlinedIcon from "@mui/icons-material/DeleteOutlineOutlined"; |
| | | import { getAiRuntime, getAiSessions, removeAiSession, streamAiChat } from "@/api/ai/chat"; |
| | | import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; |
| | | import PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined"; |
| | | import PushPinIcon from "@mui/icons-material/PushPin"; |
| | | import SearchOutlinedIcon from "@mui/icons-material/SearchOutlined"; |
| | | import { getAiRuntime, getAiSessions, pinAiSession, removeAiSession, renameAiSession, streamAiChat } from "@/api/ai/chat"; |
| | | |
| | | const DEFAULT_PROMPT_CODE = "home.default"; |
| | | |
| | |
| | | const [streaming, setStreaming] = useState(false); |
| | | const [usage, setUsage] = useState(null); |
| | | const [drawerError, setDrawerError] = useState(""); |
| | | const [sessionKeyword, setSessionKeyword] = useState(""); |
| | | const [renameDialog, setRenameDialog] = useState({ open: false, sessionId: null, title: "" }); |
| | | |
| | | const promptCode = runtime?.promptCode || DEFAULT_PROMPT_CODE; |
| | | |
| | |
| | | const initializeDrawer = async (targetSessionId = null) => { |
| | | await Promise.all([ |
| | | loadRuntime(targetSessionId), |
| | | loadSessions(), |
| | | loadSessions(sessionKeyword), |
| | | ]); |
| | | }; |
| | | |
| | |
| | | } |
| | | }; |
| | | |
| | | const loadSessions = async () => { |
| | | const loadSessions = async (keyword = sessionKeyword) => { |
| | | try { |
| | | const data = await getAiSessions(DEFAULT_PROMPT_CODE); |
| | | const data = await getAiSessions(DEFAULT_PROMPT_CODE, keyword); |
| | | setSessions(data); |
| | | } catch (error) { |
| | | const message = error.message || "获取 AI 会话列表失败"; |
| | |
| | | setUsage(null); |
| | | setDrawerError(""); |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | if (!open) { |
| | | return; |
| | | } |
| | | const timer = window.setTimeout(() => { |
| | | loadSessions(sessionKeyword); |
| | | }, 250); |
| | | return () => window.clearTimeout(timer); |
| | | }, [sessionKeyword, open]); |
| | | |
| | | const handleSwitchSession = async (targetSessionId) => { |
| | | if (streaming || targetSessionId === sessionId) { |
| | |
| | | startNewSession(); |
| | | await loadRuntime(null); |
| | | } |
| | | await loadSessions(); |
| | | await loadSessions(sessionKeyword); |
| | | } catch (error) { |
| | | const message = error.message || "删除 AI 会话失败"; |
| | | setDrawerError(message); |
| | | notify(message, { type: "error" }); |
| | | } |
| | | }; |
| | | |
| | | const handlePinSession = async (targetSessionId, pinned) => { |
| | | if (streaming || !targetSessionId) { |
| | | return; |
| | | } |
| | | try { |
| | | await pinAiSession(targetSessionId, pinned); |
| | | notify(pinned ? "会话已置顶" : "会话已取消置顶"); |
| | | await loadSessions(sessionKeyword); |
| | | } catch (error) { |
| | | const message = error.message || "更新会话置顶状态失败"; |
| | | setDrawerError(message); |
| | | notify(message, { type: "error" }); |
| | | } |
| | | }; |
| | | |
| | | const openRenameDialog = (item) => { |
| | | setRenameDialog({ |
| | | open: true, |
| | | sessionId: item?.sessionId || null, |
| | | title: item?.title || "", |
| | | }); |
| | | }; |
| | | |
| | | const closeRenameDialog = () => { |
| | | setRenameDialog({ open: false, sessionId: null, title: "" }); |
| | | }; |
| | | |
| | | const handleRenameSubmit = async () => { |
| | | if (streaming || !renameDialog.sessionId) { |
| | | return; |
| | | } |
| | | try { |
| | | await renameAiSession(renameDialog.sessionId, renameDialog.title); |
| | | notify("会话已重命名"); |
| | | closeRenameDialog(); |
| | | await loadSessions(sessionKeyword); |
| | | } catch (error) { |
| | | const message = error.message || "重命名会话失败"; |
| | | setDrawerError(message); |
| | | notify(message, { type: "error" }); |
| | | } |
| | |
| | | if (completed) { |
| | | await Promise.all([ |
| | | loadRuntime(completedSessionId), |
| | | loadSessions(), |
| | | loadSessions(sessionKeyword), |
| | | ]); |
| | | } |
| | | } |
| | |
| | | 新建会话 |
| | | </Button> |
| | | </Stack> |
| | | <TextField |
| | | value={sessionKeyword} |
| | | onChange={(event) => setSessionKeyword(event.target.value)} |
| | | fullWidth |
| | | size="small" |
| | | placeholder="搜索会话标题" |
| | | InputProps={{ |
| | | startAdornment: <SearchOutlinedIcon fontSize="small" sx={{ mr: 1, color: "text.secondary" }} />, |
| | | }} |
| | | sx={{ mb: 1.25 }} |
| | | /> |
| | | <Paper variant="outlined" sx={{ overflow: "hidden" }}> |
| | | {!sessions.length ? ( |
| | | <Box px={1.5} py={1.25}> |
| | |
| | | > |
| | | <ListItemText |
| | | primary={item.title || `会话 ${item.sessionId}`} |
| | | secondary={item.lastMessageTime || `Session ${item.sessionId}`} |
| | | secondary={item.lastMessagePreview || item.lastMessageTime || `Session ${item.sessionId}`} |
| | | primaryTypographyProps={{ |
| | | noWrap: true, |
| | | fontSize: 14, |
| | | fontWeight: item.pinned ? 700 : 400, |
| | | }} |
| | | secondaryTypographyProps={{ |
| | | noWrap: true, |
| | | fontSize: 12, |
| | | }} |
| | | /> |
| | | <IconButton |
| | | size="small" |
| | | edge="end" |
| | | disabled={streaming} |
| | | onClick={(event) => { |
| | | event.stopPropagation(); |
| | | handlePinSession(item.sessionId, !item.pinned); |
| | | }} |
| | | title={item.pinned ? "取消置顶" : "置顶会话"} |
| | | > |
| | | {item.pinned ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />} |
| | | </IconButton> |
| | | <IconButton |
| | | size="small" |
| | | edge="end" |
| | | disabled={streaming} |
| | | onClick={(event) => { |
| | | event.stopPropagation(); |
| | | openRenameDialog(item); |
| | | }} |
| | | title="重命名会话" |
| | | > |
| | | <EditOutlinedIcon fontSize="small" /> |
| | | </IconButton> |
| | | <IconButton |
| | | size="small" |
| | | edge="end" |
| | |
| | | </Box> |
| | | </Box> |
| | | </Box> |
| | | <Dialog open={renameDialog.open} onClose={closeRenameDialog} fullWidth maxWidth="xs"> |
| | | <DialogTitle>重命名会话</DialogTitle> |
| | | <DialogContent> |
| | | <TextField |
| | | value={renameDialog.title} |
| | | onChange={(event) => setRenameDialog((prev) => ({ ...prev, title: event.target.value }))} |
| | | autoFocus |
| | | margin="dense" |
| | | label="会话标题" |
| | | fullWidth |
| | | /> |
| | | </DialogContent> |
| | | <DialogActions> |
| | | <Button onClick={closeRenameDialog}>取消</Button> |
| | | <Button onClick={handleRenameSubmit} variant="contained" disabled={streaming || !renameDialog.title.trim()}> |
| | | 保存 |
| | | </Button> |
| | | </DialogActions> |
| | | </Dialog> |
| | | </Drawer> |
| | | ); |
| | | }; |
| | |
| | | package com.vincent.rsf.server.ai.controller; |
| | | |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest; |
| | | import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest; |
| | | import com.vincent.rsf.server.ai.dto.AiChatRequest; |
| | | import com.vincent.rsf.server.ai.service.AiChatService; |
| | | import com.vincent.rsf.server.system.controller.BaseController; |
| | |
| | | |
| | | @PreAuthorize("isAuthenticated()") |
| | | @GetMapping("/ai/chat/sessions") |
| | | public R sessions(@RequestParam(required = false) String promptCode) { |
| | | return R.ok().add(aiChatService.listSessions(promptCode, getLoginUserId(), getTenantId())); |
| | | public R sessions(@RequestParam(required = false) String promptCode, |
| | | @RequestParam(required = false) String keyword) { |
| | | return R.ok().add(aiChatService.listSessions(promptCode, keyword, getLoginUserId(), getTenantId())); |
| | | } |
| | | |
| | | @PreAuthorize("isAuthenticated()") |
| | |
| | | } |
| | | |
| | | @PreAuthorize("isAuthenticated()") |
| | | @PostMapping("/ai/chat/session/rename/{sessionId}") |
| | | public R renameSession(@PathVariable Long sessionId, @RequestBody AiChatSessionRenameRequest request) { |
| | | return R.ok("Update Success").add(aiChatService.renameSession(sessionId, request, getLoginUserId(), getTenantId())); |
| | | } |
| | | |
| | | @PreAuthorize("isAuthenticated()") |
| | | @PostMapping("/ai/chat/session/pin/{sessionId}") |
| | | public R pinSession(@PathVariable Long sessionId, @RequestBody AiChatSessionPinRequest request) { |
| | | return R.ok("Update Success").add(aiChatService.pinSession(sessionId, request, getLoginUserId(), getTenantId())); |
| | | } |
| | | |
| | | @PreAuthorize("isAuthenticated()") |
| | | @PostMapping(value = "/ai/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) |
| | | public SseEmitter stream(@RequestBody AiChatRequest request) { |
| | | String requestId = StringUtils.hasText(request.getRequestId()) |
| | |
| | | |
| | | private String promptCode; |
| | | |
| | | private Boolean pinned; |
| | | |
| | | private String lastMessagePreview; |
| | | |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | private Date lastMessageTime; |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.ai.dto; |
| | | |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | public class AiChatSessionPinRequest { |
| | | |
| | | private Boolean pinned; |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.ai.dto; |
| | | |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | public class AiChatSessionRenameRequest { |
| | | |
| | | private String title; |
| | | } |
| | |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | private Date lastMessageTime; |
| | | |
| | | @ApiModelProperty(value = "是否置顶") |
| | | private Integer pinned; |
| | | |
| | | @ApiModelProperty(value = "状态") |
| | | private Integer status; |
| | | |
| | |
| | | import com.vincent.rsf.server.ai.dto.AiChatMemoryDto; |
| | | import com.vincent.rsf.server.ai.dto.AiChatMessageDto; |
| | | import com.vincent.rsf.server.ai.dto.AiChatSessionDto; |
| | | import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest; |
| | | import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest; |
| | | import com.vincent.rsf.server.ai.entity.AiChatSession; |
| | | |
| | | import java.util.List; |
| | |
| | | |
| | | AiChatMemoryDto getMemory(Long userId, Long tenantId, String promptCode, Long sessionId); |
| | | |
| | | List<AiChatSessionDto> listSessions(Long userId, Long tenantId, String promptCode); |
| | | List<AiChatSessionDto> listSessions(Long userId, Long tenantId, String promptCode, String keyword); |
| | | |
| | | AiChatSession resolveSession(Long userId, Long tenantId, String promptCode, Long sessionId, String titleSeed); |
| | | |
| | | void saveRound(AiChatSession session, Long userId, Long tenantId, List<AiChatMessageDto> memoryMessages, String assistantContent); |
| | | |
| | | void removeSession(Long userId, Long tenantId, Long sessionId); |
| | | |
| | | AiChatSessionDto renameSession(Long userId, Long tenantId, Long sessionId, AiChatSessionRenameRequest request); |
| | | |
| | | AiChatSessionDto pinSession(Long userId, Long tenantId, Long sessionId, AiChatSessionPinRequest request); |
| | | } |
| | |
| | | import com.vincent.rsf.server.ai.dto.AiChatRequest; |
| | | import com.vincent.rsf.server.ai.dto.AiChatRuntimeDto; |
| | | import com.vincent.rsf.server.ai.dto.AiChatSessionDto; |
| | | import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest; |
| | | import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest; |
| | | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; |
| | | |
| | | import java.util.List; |
| | |
| | | |
| | | AiChatRuntimeDto getRuntime(String promptCode, Long sessionId, Long userId, Long tenantId); |
| | | |
| | | List<AiChatSessionDto> listSessions(String promptCode, Long userId, Long tenantId); |
| | | List<AiChatSessionDto> listSessions(String promptCode, String keyword, Long userId, Long tenantId); |
| | | |
| | | SseEmitter stream(AiChatRequest request, Long userId, Long tenantId); |
| | | |
| | | void removeSession(Long sessionId, Long userId, Long tenantId); |
| | | |
| | | AiChatSessionDto renameSession(Long sessionId, AiChatSessionRenameRequest request, Long userId, Long tenantId); |
| | | |
| | | AiChatSessionDto pinSession(Long sessionId, AiChatSessionPinRequest request, Long userId, Long tenantId); |
| | | } |
| | |
| | | import com.vincent.rsf.framework.exception.CoolException; |
| | | import com.vincent.rsf.server.ai.dto.AiChatMemoryDto; |
| | | import com.vincent.rsf.server.ai.dto.AiChatMessageDto; |
| | | import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest; |
| | | import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest; |
| | | import com.vincent.rsf.server.ai.dto.AiChatSessionDto; |
| | | import com.vincent.rsf.server.ai.entity.AiChatMessage; |
| | | import com.vincent.rsf.server.ai.entity.AiChatSession; |
| | |
| | | import java.util.ArrayList; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | | |
| | | @Service |
| | | @RequiredArgsConstructor |
| | |
| | | } |
| | | |
| | | @Override |
| | | public List<AiChatSessionDto> listSessions(Long userId, Long tenantId, String promptCode) { |
| | | public List<AiChatSessionDto> listSessions(Long userId, Long tenantId, String promptCode, String keyword) { |
| | | ensureIdentity(userId, tenantId); |
| | | String resolvedPromptCode = requirePromptCode(promptCode); |
| | | List<AiChatSession> sessions = aiChatSessionMapper.selectList(new LambdaQueryWrapper<AiChatSession>() |
| | |
| | | .eq(AiChatSession::getPromptCode, resolvedPromptCode) |
| | | .eq(AiChatSession::getDeleted, 0) |
| | | .eq(AiChatSession::getStatus, StatusType.ENABLE.val) |
| | | .like(StringUtils.hasText(keyword), AiChatSession::getTitle, keyword == null ? null : keyword.trim()) |
| | | .orderByDesc(AiChatSession::getPinned) |
| | | .orderByDesc(AiChatSession::getLastMessageTime) |
| | | .orderByDesc(AiChatSession::getId)); |
| | | if (Cools.isEmpty(sessions)) { |
| | |
| | | } |
| | | List<AiChatSessionDto> result = new ArrayList<>(); |
| | | for (AiChatSession session : sessions) { |
| | | result.add(AiChatSessionDto.builder() |
| | | .sessionId(session.getId()) |
| | | .title(session.getTitle()) |
| | | .promptCode(session.getPromptCode()) |
| | | .lastMessageTime(session.getLastMessageTime()) |
| | | .build()); |
| | | result.add(buildSessionDto(session)); |
| | | } |
| | | return result; |
| | | } |
| | |
| | | .setUserId(userId) |
| | | .setTenantId(tenantId) |
| | | .setLastMessageTime(now) |
| | | .setPinned(0) |
| | | .setStatus(StatusType.ENABLE.val) |
| | | .setDeleted(0) |
| | | .setCreateBy(userId) |
| | |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public AiChatSessionDto renameSession(Long userId, Long tenantId, Long sessionId, AiChatSessionRenameRequest request) { |
| | | ensureIdentity(userId, tenantId); |
| | | if (request == null || !StringUtils.hasText(request.getTitle())) { |
| | | throw new CoolException("会话标题不能为空"); |
| | | } |
| | | AiChatSession session = requireOwnedSession(sessionId, userId, tenantId); |
| | | Date now = new Date(); |
| | | AiChatSession update = new AiChatSession() |
| | | .setId(sessionId) |
| | | .setTitle(buildSessionTitle(request.getTitle())) |
| | | .setUpdateBy(userId) |
| | | .setUpdateTime(now); |
| | | aiChatSessionMapper.updateById(update); |
| | | return buildSessionDto(requireOwnedSession(sessionId, userId, tenantId)); |
| | | } |
| | | |
| | | @Override |
| | | public AiChatSessionDto pinSession(Long userId, Long tenantId, Long sessionId, AiChatSessionPinRequest request) { |
| | | ensureIdentity(userId, tenantId); |
| | | if (request == null || request.getPinned() == null) { |
| | | throw new CoolException("置顶状态不能为空"); |
| | | } |
| | | AiChatSession session = requireOwnedSession(sessionId, userId, tenantId); |
| | | Date now = new Date(); |
| | | AiChatSession update = new AiChatSession() |
| | | .setId(sessionId) |
| | | .setPinned(Boolean.TRUE.equals(request.getPinned()) ? 1 : 0) |
| | | .setUpdateBy(userId) |
| | | .setUpdateTime(now); |
| | | aiChatSessionMapper.updateById(update); |
| | | return buildSessionDto(requireOwnedSession(sessionId, userId, tenantId)); |
| | | } |
| | | |
| | | private AiChatSession findLatestSession(Long userId, Long tenantId, String promptCode) { |
| | | return aiChatSessionMapper.selectOne(new LambdaQueryWrapper<AiChatSession>() |
| | | .eq(AiChatSession::getUserId, userId) |
| | |
| | | .eq(AiChatSession::getUserId, userId) |
| | | .eq(AiChatSession::getTenantId, tenantId) |
| | | .eq(AiChatSession::getPromptCode, promptCode) |
| | | .eq(AiChatSession::getDeleted, 0) |
| | | .eq(AiChatSession::getStatus, StatusType.ENABLE.val) |
| | | .last("limit 1")); |
| | | if (session == null) { |
| | | throw new CoolException("AI 会话不存在或无权访问"); |
| | | } |
| | | return session; |
| | | } |
| | | |
| | | private AiChatSession requireOwnedSession(Long sessionId, Long userId, Long tenantId) { |
| | | if (sessionId == null) { |
| | | throw new CoolException("AI 会话 ID 不能为空"); |
| | | } |
| | | AiChatSession session = aiChatSessionMapper.selectOne(new LambdaQueryWrapper<AiChatSession>() |
| | | .eq(AiChatSession::getId, sessionId) |
| | | .eq(AiChatSession::getUserId, userId) |
| | | .eq(AiChatSession::getTenantId, tenantId) |
| | | .eq(AiChatSession::getDeleted, 0) |
| | | .eq(AiChatSession::getStatus, StatusType.ENABLE.val) |
| | | .last("limit 1")); |
| | |
| | | if (!StringUtils.hasText(titleSeed)) { |
| | | throw new CoolException("AI 会话标题不能为空"); |
| | | } |
| | | String title = titleSeed.trim().replace("\r", " ").replace("\n", " "); |
| | | return title.length() > 60 ? title.substring(0, 60) : title; |
| | | String title = titleSeed.trim() |
| | | .replace("\r", " ") |
| | | .replace("\n", " ") |
| | | .replaceAll("\\s+", " "); |
| | | int punctuationIndex = findSummaryBreakIndex(title); |
| | | if (punctuationIndex > 0) { |
| | | title = title.substring(0, punctuationIndex).trim(); |
| | | } |
| | | return title.length() > 48 ? title.substring(0, 48) : title; |
| | | } |
| | | |
| | | private int findSummaryBreakIndex(String title) { |
| | | String[] separators = {"。", "!", "?", ".", "!", "?"}; |
| | | int result = -1; |
| | | for (String separator : separators) { |
| | | int index = title.indexOf(separator); |
| | | if (index > 0 && (result < 0 || index < result)) { |
| | | result = index; |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private AiChatSessionDto buildSessionDto(AiChatSession session) { |
| | | AiChatMessage lastMessage = aiChatMessageMapper.selectOne(new LambdaQueryWrapper<AiChatMessage>() |
| | | .eq(AiChatMessage::getSessionId, session.getId()) |
| | | .eq(AiChatMessage::getDeleted, 0) |
| | | .orderByDesc(AiChatMessage::getSeqNo) |
| | | .orderByDesc(AiChatMessage::getId) |
| | | .last("limit 1")); |
| | | return AiChatSessionDto.builder() |
| | | .sessionId(session.getId()) |
| | | .title(session.getTitle()) |
| | | .promptCode(session.getPromptCode()) |
| | | .pinned(session.getPinned() != null && session.getPinned() == 1) |
| | | .lastMessagePreview(buildLastMessagePreview(lastMessage)) |
| | | .lastMessageTime(session.getLastMessageTime()) |
| | | .build(); |
| | | } |
| | | |
| | | private String buildLastMessagePreview(AiChatMessage message) { |
| | | if (message == null || !StringUtils.hasText(message.getContent())) { |
| | | return null; |
| | | } |
| | | String preview = message.getContent().trim() |
| | | .replace("\r", " ") |
| | | .replace("\n", " ") |
| | | .replaceAll("\\s+", " "); |
| | | String prefix = "assistant".equalsIgnoreCase(message.getRole()) ? "AI: " : "你: "; |
| | | String normalized = prefix + preview; |
| | | return normalized.length() > 80 ? normalized.substring(0, 80) : normalized; |
| | | } |
| | | |
| | | private void ensureIdentity(Long userId, Long tenantId) { |
| | |
| | | import com.vincent.rsf.server.ai.dto.AiChatRuntimeDto; |
| | | import com.vincent.rsf.server.ai.dto.AiChatStatusDto; |
| | | import com.vincent.rsf.server.ai.dto.AiChatSessionDto; |
| | | import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest; |
| | | import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest; |
| | | import com.vincent.rsf.server.ai.dto.AiResolvedConfig; |
| | | import com.vincent.rsf.server.ai.entity.AiParam; |
| | | import com.vincent.rsf.server.ai.entity.AiPrompt; |
| | |
| | | } |
| | | |
| | | @Override |
| | | public List<AiChatSessionDto> listSessions(String promptCode, Long userId, Long tenantId) { |
| | | public List<AiChatSessionDto> listSessions(String promptCode, String keyword, Long userId, Long tenantId) { |
| | | AiResolvedConfig config = aiConfigResolverService.resolve(promptCode, tenantId); |
| | | return aiChatMemoryService.listSessions(userId, tenantId, config.getPromptCode()); |
| | | return aiChatMemoryService.listSessions(userId, tenantId, config.getPromptCode(), keyword); |
| | | } |
| | | |
| | | @Override |
| | |
| | | } |
| | | |
| | | @Override |
| | | public AiChatSessionDto renameSession(Long sessionId, AiChatSessionRenameRequest request, Long userId, Long tenantId) { |
| | | return aiChatMemoryService.renameSession(userId, tenantId, sessionId, request); |
| | | } |
| | | |
| | | @Override |
| | | public AiChatSessionDto pinSession(Long sessionId, AiChatSessionPinRequest request, Long userId, Long tenantId) { |
| | | return aiChatMemoryService.pinSession(userId, tenantId, sessionId, request); |
| | | } |
| | | |
| | | @Override |
| | | public SseEmitter stream(AiChatRequest request, Long userId, Long tenantId) { |
| | | SseEmitter emitter = new SseEmitter(AiDefaults.SSE_TIMEOUT_MS); |
| | | CompletableFuture.runAsync(() -> doStream(request, userId, tenantId, emitter), aiChatTaskExecutor); |
| | |
| | | `user_id` bigint(20) NOT NULL COMMENT '用户 ID', |
| | | `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户', |
| | | `last_message_time` datetime DEFAULT NULL COMMENT '最后消息时间', |
| | | `pinned` tinyint(1) DEFAULT '0' COMMENT '是否置顶', |
| | | `status` int(11) DEFAULT '1' COMMENT '状态', |
| | | `deleted` int(11) DEFAULT '0' COMMENT '删除标记', |
| | | `create_time` datetime DEFAULT NULL COMMENT '创建时间', |
| | |
| | | EXECUTE builtin_code_stmt; |
| | | DEALLOCATE PREPARE builtin_code_stmt; |
| | | |
| | | SET @chat_session_pinned_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM `information_schema`.`COLUMNS` |
| | | WHERE `TABLE_SCHEMA` = DATABASE() |
| | | AND `TABLE_NAME` = 'sys_ai_chat_session' |
| | | AND `COLUMN_NAME` = 'pinned' |
| | | ); |
| | | SET @chat_session_pinned_sql := IF( |
| | | @chat_session_pinned_exists = 0, |
| | | 'ALTER TABLE `sys_ai_chat_session` ADD COLUMN `pinned` tinyint(1) DEFAULT ''0'' COMMENT ''是否置顶'' AFTER `last_message_time`', |
| | | 'SELECT 1' |
| | | ); |
| | | PREPARE chat_session_pinned_stmt FROM @chat_session_pinned_sql; |
| | | EXECUTE chat_session_pinned_stmt; |
| | | DEALLOCATE PREPARE chat_session_pinned_stmt; |
| | | |
| | | BEGIN; |
| | | INSERT INTO `sys_ai_prompt` |
| | | (`id`, `name`, `code`, `scene`, `system_prompt`, `user_prompt_template`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) |