| | |
| | | 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) { |