package com.vincent.rsf.server.ai.service.impl.conversation;
|
|
import com.vincent.rsf.framework.exception.CoolException;
|
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 com.vincent.rsf.server.ai.mapper.AiChatMessageMapper;
|
import com.vincent.rsf.server.ai.mapper.AiChatSessionMapper;
|
import com.vincent.rsf.server.system.enums.StatusType;
|
import lombok.RequiredArgsConstructor;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.support.TransactionSynchronization;
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.util.StringUtils;
|
|
import java.util.ArrayList;
|
import java.util.Date;
|
import java.util.List;
|
|
@Service
|
@RequiredArgsConstructor
|
public class AiConversationCommandService {
|
|
private final AiChatSessionMapper aiChatSessionMapper;
|
private final AiChatMessageMapper aiChatMessageMapper;
|
private final AiConversationQueryService aiConversationQueryService;
|
private final AiMemoryProfileService aiMemoryProfileService;
|
|
@Transactional(rollbackFor = Exception.class)
|
public AiChatSession resolveSession(Long userId, Long tenantId, String promptCode, Long sessionId, String titleSeed) {
|
aiConversationQueryService.ensureIdentity(userId, tenantId);
|
String resolvedPromptCode = aiConversationQueryService.requirePromptCode(promptCode);
|
if (sessionId != null) {
|
return aiConversationQueryService.getSession(sessionId, userId, tenantId, resolvedPromptCode);
|
}
|
Date now = new Date();
|
AiChatSession session = new AiChatSession()
|
.setTitle(aiConversationQueryService.buildSessionTitle(titleSeed))
|
.setPromptCode(resolvedPromptCode)
|
.setUserId(userId)
|
.setTenantId(tenantId)
|
.setLastMessageTime(now)
|
.setPinned(0)
|
.setStatus(StatusType.ENABLE.val)
|
.setDeleted(0)
|
.setCreateBy(userId)
|
.setCreateTime(now)
|
.setUpdateBy(userId)
|
.setUpdateTime(now);
|
aiChatSessionMapper.insert(session);
|
afterConversationMutationCommitted(() -> aiConversationQueryService.evictConversationCaches(tenantId, userId));
|
return session;
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public void saveRound(AiChatSession session, Long userId, Long tenantId, List<AiChatMessageDto> memoryMessages, String assistantContent) {
|
if (session == null || session.getId() == null) {
|
throw new CoolException("AI 会话不存在");
|
}
|
aiConversationQueryService.ensureIdentity(userId, tenantId);
|
List<AiChatMessageDto> normalizedMessages = normalizeMessages(memoryMessages);
|
if (normalizedMessages.isEmpty()) {
|
throw new CoolException("本轮没有可保存的对话消息");
|
}
|
int nextSeqNo = aiConversationQueryService.findNextSeqNo(session.getId());
|
Date now = new Date();
|
List<AiChatMessage> records = new ArrayList<>();
|
for (AiChatMessageDto message : normalizedMessages) {
|
records.add(buildMessageEntity(session.getId(), nextSeqNo++, message.getRole(), message.getContent(), userId, tenantId, now));
|
}
|
if (StringUtils.hasText(assistantContent)) {
|
records.add(buildMessageEntity(session.getId(), nextSeqNo, "assistant", assistantContent, userId, tenantId, now));
|
}
|
aiChatMessageMapper.insertBatch(records);
|
aiChatSessionMapper.updateById(new AiChatSession()
|
.setId(session.getId())
|
.setTitle(aiConversationQueryService.resolveUpdatedTitle(session.getTitle(), normalizedMessages))
|
.setLastMessageTime(now)
|
.setUpdateBy(userId)
|
.setUpdateTime(now));
|
afterConversationMutationCommitted(() -> {
|
aiConversationQueryService.evictConversationCaches(tenantId, userId);
|
aiMemoryProfileService.scheduleMemoryProfileRefresh(session.getId(), userId, tenantId);
|
});
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public void removeSession(Long userId, Long tenantId, Long sessionId) {
|
aiConversationQueryService.ensureIdentity(userId, tenantId);
|
AiChatSession session = aiConversationQueryService.requireOwnedSession(sessionId, userId, tenantId);
|
aiChatSessionMapper.updateById(new AiChatSession()
|
.setId(session.getId())
|
.setDeleted(1)
|
.setUpdateBy(userId)
|
.setUpdateTime(new Date()));
|
aiChatMessageMapper.softDeleteBySessionId(sessionId);
|
afterConversationMutationCommitted(() -> aiConversationQueryService.evictConversationCaches(tenantId, userId));
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public AiChatSessionDto renameSession(Long userId, Long tenantId, Long sessionId, AiChatSessionRenameRequest request) {
|
aiConversationQueryService.ensureIdentity(userId, tenantId);
|
if (request == null || !StringUtils.hasText(request.getTitle())) {
|
throw new CoolException("会话标题不能为空");
|
}
|
AiChatSession session = aiConversationQueryService.requireOwnedSession(sessionId, userId, tenantId);
|
aiChatSessionMapper.updateById(new AiChatSession()
|
.setId(sessionId)
|
.setTitle(aiConversationQueryService.buildSessionTitle(request.getTitle()))
|
.setUpdateBy(userId)
|
.setUpdateTime(new Date()));
|
afterConversationMutationCommitted(() -> aiConversationQueryService.evictConversationCaches(tenantId, userId));
|
return reloadSessionDto(sessionId, userId, tenantId, session.getPromptCode());
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public AiChatSessionDto pinSession(Long userId, Long tenantId, Long sessionId, AiChatSessionPinRequest request) {
|
aiConversationQueryService.ensureIdentity(userId, tenantId);
|
if (request == null || request.getPinned() == null) {
|
throw new CoolException("置顶状态不能为空");
|
}
|
AiChatSession session = aiConversationQueryService.requireOwnedSession(sessionId, userId, tenantId);
|
aiChatSessionMapper.updateById(new AiChatSession()
|
.setId(sessionId)
|
.setPinned(Boolean.TRUE.equals(request.getPinned()) ? 1 : 0)
|
.setUpdateBy(userId)
|
.setUpdateTime(new Date()));
|
afterConversationMutationCommitted(() -> aiConversationQueryService.evictConversationCaches(tenantId, userId));
|
return reloadSessionDto(sessionId, userId, tenantId, session.getPromptCode());
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public void clearSessionMemory(Long userId, Long tenantId, Long sessionId) {
|
aiConversationQueryService.ensureIdentity(userId, tenantId);
|
AiChatSession session = aiConversationQueryService.requireOwnedSession(sessionId, userId, tenantId);
|
aiChatMessageMapper.softDeleteBySessionId(sessionId);
|
aiChatSessionMapper.updateById(new AiChatSession()
|
.setId(sessionId)
|
.setMemorySummary(null)
|
.setMemoryFacts(null)
|
.setUpdateBy(userId)
|
.setUpdateTime(new Date())
|
.setLastMessageTime(session.getCreateTime()));
|
afterConversationMutationCommitted(() -> aiConversationQueryService.evictConversationCaches(tenantId, userId));
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public void retainLatestRound(Long userId, Long tenantId, Long sessionId) {
|
aiConversationQueryService.ensureIdentity(userId, tenantId);
|
aiConversationQueryService.requireOwnedSession(sessionId, userId, tenantId);
|
List<AiChatMessage> records = aiConversationQueryService.listMessageRecords(sessionId);
|
if (records.isEmpty()) {
|
return;
|
}
|
List<AiChatMessage> retained = aiConversationQueryService.tailMessageRecordsByRounds(records, 1);
|
List<Long> retainedIds = retained.stream().map(AiChatMessage::getId).toList();
|
List<Long> deletedIds = records.stream()
|
.map(AiChatMessage::getId)
|
.filter(id -> !retainedIds.contains(id))
|
.toList();
|
if (!deletedIds.isEmpty()) {
|
aiChatMessageMapper.softDeleteByIds(deletedIds);
|
}
|
afterConversationMutationCommitted(() -> {
|
aiConversationQueryService.evictConversationCaches(tenantId, userId);
|
aiMemoryProfileService.scheduleMemoryProfileRefresh(sessionId, userId, tenantId);
|
});
|
}
|
|
private AiChatSessionDto reloadSessionDto(Long sessionId, Long userId, Long tenantId, String promptCode) {
|
return aiConversationQueryService.listSessions(userId, tenantId, promptCode, null).stream()
|
.filter(item -> sessionId.equals(item.getSessionId()))
|
.findFirst()
|
.orElseThrow(() -> new CoolException("AI 会话不存在或无权访问"));
|
}
|
|
private List<AiChatMessageDto> normalizeMessages(List<AiChatMessageDto> memoryMessages) {
|
List<AiChatMessageDto> normalized = new ArrayList<>();
|
if (memoryMessages == null || memoryMessages.isEmpty()) {
|
return normalized;
|
}
|
for (AiChatMessageDto item : memoryMessages) {
|
if (item == null || !StringUtils.hasText(item.getContent())) {
|
continue;
|
}
|
String role = item.getRole() == null ? "user" : item.getRole().toLowerCase();
|
if ("system".equals(role)) {
|
continue;
|
}
|
AiChatMessageDto normalizedItem = new AiChatMessageDto();
|
normalizedItem.setRole("assistant".equals(role) ? "assistant" : "user");
|
normalizedItem.setContent(item.getContent().trim());
|
normalized.add(normalizedItem);
|
}
|
return normalized;
|
}
|
|
private AiChatMessage buildMessageEntity(Long sessionId, int seqNo, String role, String content, Long userId, Long tenantId, Date createTime) {
|
return new AiChatMessage()
|
.setSessionId(sessionId)
|
.setSeqNo(seqNo)
|
.setRole(role)
|
.setContent(content)
|
.setContentLength(content == null ? 0 : content.length())
|
.setUserId(userId)
|
.setTenantId(tenantId)
|
.setDeleted(0)
|
.setCreateBy(userId)
|
.setCreateTime(createTime);
|
}
|
|
private void afterConversationMutationCommitted(Runnable action) {
|
if (action == null) {
|
return;
|
}
|
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
action.run();
|
return;
|
}
|
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
@Override
|
public void afterCommit() {
|
action.run();
|
}
|
});
|
}
|
}
|