From 1a6ea64e80298e3f41234fe11ac1842f7979cd80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Thu, 14 May 2026 21:23:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E5=A4=9A=E7=89=88=E6=9C=AC=E7=AD=94=E6=A1=88=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为管理端、公共聊天和用户中心补充回答变体查询与切换能力 - 支持基于指定轮次重新生成并同步前后端多版本状态 - 保留 application.yml 与本地截图文件为未提交状态 --- .../controller/ai/ChatHistoryController.java | 20 + .../ai/PublicChatSessionController.java | 25 + .../controller/ai/UcBotController.java | 20 +- .../ai/UcChatHistoryController.java | 17 + .../service/TinyFlowService.java | 8 +- .../domain/dto/ChatRoundVariantView.java | 65 +++ .../service/ChatHistoryManageService.java | 10 + .../PublicChatSessionRestoreService.java | 6 + .../impl/ChatHistoryManageServiceImpl.java | 60 +- .../PublicChatSessionRestoreServiceImpl.java | 28 + .../chat-history/ChatHistoryDetailDrawer.vue | 102 +++- .../app/src/components/chat/chat.vue | 280 ++++++++- .../app/src/views/ai/chatHistory/index.vue | 93 ++- .../app/src/views/publicChat/index.vue | 537 +++++++++++++++++- .../app/src/components/chat/bubbleList.vue | 258 ++++++++- .../app/src/components/chat/sender.vue | 126 +++- .../views/assistantMarket/assistant/index.vue | 155 ++++- .../app/src/views/chatAssistant/index.vue | 165 +++++- .../app/src/views/chatHistory/index.vue | 106 +++- .../packages/types/src/chat-time.ts | 28 +- .../packages/utils/src/helpers/chat-time.ts | 450 ++++++++++++++- .../utils/src/helpers/chat-variant-switch.ts | 187 ++++++ .../packages/utils/src/helpers/index.ts | 1 + 23 files changed, 2625 insertions(+), 122 deletions(-) create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatRoundVariantView.java create mode 100644 easyflow-ui-usercenter/packages/utils/src/helpers/chat-variant-switch.ts diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ChatHistoryController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ChatHistoryController.java index 6aa6477..fb6fef2 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ChatHistoryController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ChatHistoryController.java @@ -2,17 +2,23 @@ package tech.easyflow.admin.controller.ai; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import tech.easyflow.chatlog.domain.dto.ChatHistoryPage; +import tech.easyflow.chatlog.domain.dto.ChatMessageRecord; import tech.easyflow.chatlog.domain.dto.ChatSessionPage; import tech.easyflow.chatlog.domain.dto.ChatSessionSummary; import tech.easyflow.chatlog.domain.query.ChatPageQuery; import tech.easyflow.chatlog.domain.query.ChatSessionFilterQuery; import tech.easyflow.chatlog.service.ChatHistoryManageService; import tech.easyflow.common.domain.Result; +import tech.easyflow.common.entity.LoginAccount; +import tech.easyflow.common.satoken.util.SaTokenUtil; +import tech.easyflow.common.web.jsonbody.JsonBody; import java.math.BigInteger; +import java.util.List; @RestController @RequestMapping("/api/v1/chatHistory") @@ -38,4 +44,18 @@ public class ChatHistoryController { public Result queryMessages(@PathVariable BigInteger sessionId, ChatPageQuery query) { return Result.ok(chatHistoryManageService.queryAdminMessages(sessionId, query)); } + + @GetMapping("/sessions/{sessionId}/rounds/{roundId}/variants") + public Result> listRoundVariants(@PathVariable BigInteger sessionId, + @PathVariable BigInteger roundId) { + return Result.ok(chatHistoryManageService.listAdminRoundVariants(sessionId, roundId)); + } + + @PostMapping("/sessions/{sessionId}/rounds/{roundId}/selectVariant") + public Result selectRoundVariant(@PathVariable BigInteger sessionId, + @PathVariable BigInteger roundId, + @JsonBody(value = "variantIndex", required = true) Integer variantIndex) { + LoginAccount account = SaTokenUtil.getLoginAccount(); + return Result.ok(chatHistoryManageService.selectAdminRoundVariant(sessionId, roundId, variantIndex, account.getId())); + } } diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PublicChatSessionController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PublicChatSessionController.java index adaffba..7ba0c10 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PublicChatSessionController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PublicChatSessionController.java @@ -1,15 +1,20 @@ package tech.easyflow.admin.controller.ai; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import tech.easyflow.chatlog.domain.dto.ChatMessageRecord; import tech.easyflow.chatlog.domain.dto.PublicChatSessionRestoreResult; import tech.easyflow.chatlog.service.PublicChatSessionRestoreService; import tech.easyflow.common.domain.Result; import tech.easyflow.common.entity.LoginAccount; import tech.easyflow.common.satoken.util.SaTokenUtil; +import tech.easyflow.common.web.jsonbody.JsonBody; import java.math.BigInteger; +import java.util.List; @RestController @RequestMapping("/api/v1/public-chat") @@ -35,4 +40,24 @@ public class PublicChatSessionController { ); return Result.ok(result); } + + @GetMapping("/session/{sessionId}/rounds/{roundId}/variants") + public Result> listRoundVariants(@PathVariable BigInteger sessionId, + @PathVariable BigInteger roundId, + BigInteger botId) { + LoginAccount account = SaTokenUtil.getLoginAccount(); + BigInteger userId = account == null ? null : account.getId(); + return Result.ok(publicChatSessionRestoreService.listVariants(userId, botId, sessionId, roundId)); + } + + @PostMapping("/session/{sessionId}/rounds/{roundId}/selectVariant") + public Result selectRoundVariant(@PathVariable BigInteger sessionId, + @PathVariable BigInteger roundId, + BigInteger botId, + @JsonBody(value = "variantIndex", required = true) Integer variantIndex) { + LoginAccount account = SaTokenUtil.getLoginAccount(); + BigInteger userId = account == null ? null : account.getId(); + BigInteger operatorId = account == null ? BigInteger.ZERO : account.getId(); + return Result.ok(publicChatSessionRestoreService.selectVariant(userId, botId, sessionId, roundId, variantIndex, operatorId)); + } } diff --git a/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcBotController.java b/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcBotController.java index 956abea..ea0a886 100644 --- a/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcBotController.java +++ b/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcBotController.java @@ -29,7 +29,9 @@ import tech.easyflow.common.satoken.util.SaTokenUtil; import tech.easyflow.common.web.controller.BaseCurdController; import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.common.web.jsonbody.JsonBody; +import tech.easyflow.chatlog.service.ChatRoundOperateService; import tech.easyflow.core.runtime.ChatChannel; +import tech.easyflow.core.runtime.ChatRuntimeExtKeys; import tech.easyflow.core.runtime.ChatRuntimeContext; import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot; import tech.easyflow.system.service.CategoryPermissionService; @@ -67,6 +69,8 @@ public class UcBotController extends BaseCurdController { private Cache cache; @Resource private AudioServiceManager audioServiceManager; + @Resource + private ChatRoundOperateService chatRoundOperateService; public UcBotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService, BotDocumentCollectionService botDocumentCollectionService) { @@ -153,13 +157,17 @@ public class UcBotController extends BaseCurdController { @JsonBody(value = "botId", required = true) BigInteger botId, @JsonBody(value = "conversationId", required = true) BigInteger conversationId, @JsonBody(value = "messages") List> messages, - @JsonBody(value = "attachments") List attachments + @JsonBody(value = "attachments") List attachments, + @JsonBody(value = "regenerateRoundId") BigInteger regenerateRoundId ) { BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult(); + if (regenerateRoundId != null) { + chatRoundOperateService.requireRegeneratableRound(conversationId, regenerateRoundId); + } // 前置校验:失败则直接返回错误SseEmitter - SseEmitter errorEmitter = botService.checkChatBeforeStart(botId, prompt, conversationId.toString(), chatCheckResult); + SseEmitter errorEmitter = botService.checkChatBeforeStart(botId, prompt, conversationId.toString(), chatCheckResult, regenerateRoundId); if (errorEmitter != null) { return errorEmitter; } @@ -171,7 +179,7 @@ public class UcBotController extends BaseCurdController { messages, chatCheckResult, attachments, - buildRuntimeContext(chatCheckResult.getAiBot(), conversationId, prompt, attachments) + buildRuntimeContext(chatCheckResult.getAiBot(), conversationId, prompt, attachments, regenerateRoundId) ); } @@ -287,7 +295,8 @@ public class UcBotController extends BaseCurdController { return super.onSaveOrUpdateBefore(entity, isSave); } - private ChatRuntimeContext buildRuntimeContext(Bot bot, BigInteger conversationId, String prompt, List attachments) { + private ChatRuntimeContext buildRuntimeContext(Bot bot, BigInteger conversationId, String prompt, List attachments, + BigInteger regenerateRoundId) { LoginAccount account = requireCurrentLoginAccount(); ChatRuntimeContext context = new ChatRuntimeContext(); context.setChannel(ChatChannel.USER_CENTER); @@ -303,6 +312,9 @@ public class UcBotController extends BaseCurdController { context.setSessionTitle(prompt.length() > 200 ? prompt.substring(0, 200) : prompt); context.setAnonymous(false); context.setAttachments(attachments); + if (regenerateRoundId != null) { + context.getExt().put(ChatRuntimeExtKeys.REGENERATE_ROUND_ID, regenerateRoundId); + } ChatTimeToolAvailabilityContext.bindLoggedInSnapshot(context, account, bot); return context; } diff --git a/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcChatHistoryController.java b/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcChatHistoryController.java index 6ebd230..81d9fc3 100644 --- a/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcChatHistoryController.java +++ b/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcChatHistoryController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import tech.easyflow.chatlog.domain.dto.ChatHistoryPage; +import tech.easyflow.chatlog.domain.dto.ChatMessageRecord; import tech.easyflow.chatlog.domain.dto.ChatSessionPage; import tech.easyflow.chatlog.domain.dto.ChatSessionSummary; import tech.easyflow.chatlog.domain.query.ChatPageQuery; @@ -17,6 +18,7 @@ import tech.easyflow.common.satoken.util.SaTokenUtil; import tech.easyflow.common.web.jsonbody.JsonBody; import java.math.BigInteger; +import java.util.List; @RestController @RequestMapping("/userCenter/chatHistory") @@ -61,4 +63,19 @@ public class UcChatHistoryController { chatHistoryManageService.deleteUserSession(account.getId(), sessionId, account.getId()); return Result.ok(); } + + @GetMapping("/sessions/{sessionId}/rounds/{roundId}/variants") + public Result> listRoundVariants(@PathVariable BigInteger sessionId, + @PathVariable BigInteger roundId) { + LoginAccount account = SaTokenUtil.getLoginAccount(); + return Result.ok(chatHistoryManageService.listUserRoundVariants(account.getId(), sessionId, roundId)); + } + + @PostMapping("/sessions/{sessionId}/rounds/{roundId}/selectVariant") + public Result selectRoundVariant(@PathVariable BigInteger sessionId, + @PathVariable BigInteger roundId, + @JsonBody(value = "variantIndex", required = true) Integer variantIndex) { + LoginAccount account = SaTokenUtil.getLoginAccount(); + return Result.ok(chatHistoryManageService.selectUserRoundVariant(account.getId(), sessionId, roundId, variantIndex, account.getId())); + } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/TinyFlowService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/TinyFlowService.java index a719744..683a47f 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/TinyFlowService.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/TinyFlowService.java @@ -31,9 +31,11 @@ public class TinyFlowService { ChainState chainState = chainStateRepository.load(executeId); ChainInfo res = getChainInfo(executeId, chainState); - for (NodeInfo node : nodes) { - processNodeState(executeId, node, chainStateRepository, nodeStateRepository); - res.getNodes().put(node.getNodeId(), node); + if (nodes != null) { + for (NodeInfo node : nodes) { + processNodeState(executeId, node, chainStateRepository, nodeStateRepository); + res.getNodes().put(node.getNodeId(), node); + } } return res; } diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatRoundVariantView.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatRoundVariantView.java new file mode 100644 index 0000000..2752a94 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatRoundVariantView.java @@ -0,0 +1,65 @@ +package tech.easyflow.chatlog.domain.dto; + +import java.io.Serializable; +import java.math.BigInteger; + +/** + * 轮次答案版本视图。 + */ +public class ChatRoundVariantView implements Serializable { + + private BigInteger roundId; + private Integer roundNo; + private Integer variantCount; + private Integer selectedVariantIndex; + private Boolean switchable; + private ChatMessageRecord selectedMessage; + + public BigInteger getRoundId() { + return roundId; + } + + public void setRoundId(BigInteger roundId) { + this.roundId = roundId; + } + + public Integer getRoundNo() { + return roundNo; + } + + public void setRoundNo(Integer roundNo) { + this.roundNo = roundNo; + } + + public Integer getVariantCount() { + return variantCount; + } + + public void setVariantCount(Integer variantCount) { + this.variantCount = variantCount; + } + + public Integer getSelectedVariantIndex() { + return selectedVariantIndex; + } + + public void setSelectedVariantIndex(Integer selectedVariantIndex) { + this.selectedVariantIndex = selectedVariantIndex; + } + + public Boolean getSwitchable() { + return switchable; + } + + public void setSwitchable(Boolean switchable) { + this.switchable = switchable; + } + + public ChatMessageRecord getSelectedMessage() { + return selectedMessage; + } + + public void setSelectedMessage(ChatMessageRecord selectedMessage) { + this.selectedMessage = selectedMessage; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatHistoryManageService.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatHistoryManageService.java index b1ab625..fad47a7 100644 --- a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatHistoryManageService.java +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatHistoryManageService.java @@ -1,12 +1,14 @@ package tech.easyflow.chatlog.service; import tech.easyflow.chatlog.domain.dto.ChatHistoryPage; +import tech.easyflow.chatlog.domain.dto.ChatMessageRecord; import tech.easyflow.chatlog.domain.dto.ChatSessionPage; import tech.easyflow.chatlog.domain.dto.ChatSessionSummary; import tech.easyflow.chatlog.domain.query.ChatPageQuery; import tech.easyflow.chatlog.domain.query.ChatSessionFilterQuery; import java.math.BigInteger; +import java.util.List; public interface ChatHistoryManageService { @@ -25,4 +27,12 @@ public interface ChatHistoryManageService { void renameUserSession(BigInteger userId, BigInteger sessionId, String title, BigInteger operatorId); void deleteUserSession(BigInteger userId, BigInteger sessionId, BigInteger operatorId); + + List listUserRoundVariants(BigInteger userId, BigInteger sessionId, BigInteger roundId); + + ChatMessageRecord selectUserRoundVariant(BigInteger userId, BigInteger sessionId, BigInteger roundId, Integer variantIndex, BigInteger operatorId); + + List listAdminRoundVariants(BigInteger sessionId, BigInteger roundId); + + ChatMessageRecord selectAdminRoundVariant(BigInteger sessionId, BigInteger roundId, Integer variantIndex, BigInteger operatorId); } diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/PublicChatSessionRestoreService.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/PublicChatSessionRestoreService.java index 90c9eea..9ea930a 100644 --- a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/PublicChatSessionRestoreService.java +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/PublicChatSessionRestoreService.java @@ -1,10 +1,16 @@ package tech.easyflow.chatlog.service; +import tech.easyflow.chatlog.domain.dto.ChatMessageRecord; import tech.easyflow.chatlog.domain.dto.PublicChatSessionRestoreResult; import java.math.BigInteger; +import java.util.List; public interface PublicChatSessionRestoreService { PublicChatSessionRestoreResult restoreSession(BigInteger userId, BigInteger assistantId, BigInteger sessionId, Integer limit); + + List listVariants(BigInteger userId, BigInteger assistantId, BigInteger sessionId, BigInteger roundId); + + ChatMessageRecord selectVariant(BigInteger userId, BigInteger assistantId, BigInteger sessionId, BigInteger roundId, Integer variantIndex, BigInteger operatorId); } diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatHistoryManageServiceImpl.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatHistoryManageServiceImpl.java index ac1d1bb..65cc8a2 100644 --- a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatHistoryManageServiceImpl.java +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatHistoryManageServiceImpl.java @@ -11,6 +11,7 @@ import tech.easyflow.chatlog.domain.query.ChatSessionFilterQuery; import tech.easyflow.chatlog.repository.analyticaldb.ChatAnalyticalDBRepository; import tech.easyflow.chatlog.service.ChatHistoryManageService; import tech.easyflow.chatlog.service.ChatHistoryQueryService; +import tech.easyflow.chatlog.service.ChatRoundOperateService; import tech.easyflow.chatlog.service.ChatSessionCommandService; import tech.easyflow.chatlog.service.ChatSessionQueryService; import tech.easyflow.common.web.exceptions.BusinessException; @@ -23,15 +24,18 @@ public class ChatHistoryManageServiceImpl implements ChatHistoryManageService { private final ChatSessionQueryService chatSessionQueryService; private final ChatSessionCommandService chatSessionCommandService; private final ChatHistoryQueryService chatHistoryQueryService; + private final ChatRoundOperateService chatRoundOperateService; private final ChatAnalyticalDBRepository chatAnalyticalDBRepository; public ChatHistoryManageServiceImpl(ChatSessionQueryService chatSessionQueryService, ChatSessionCommandService chatSessionCommandService, ChatHistoryQueryService chatHistoryQueryService, + ChatRoundOperateService chatRoundOperateService, ChatAnalyticalDBRepository chatAnalyticalDBRepository) { this.chatSessionQueryService = chatSessionQueryService; this.chatSessionCommandService = chatSessionCommandService; this.chatHistoryQueryService = chatHistoryQueryService; + this.chatRoundOperateService = chatRoundOperateService; this.chatAnalyticalDBRepository = chatAnalyticalDBRepository; } @@ -68,13 +72,21 @@ public class ChatHistoryManageServiceImpl implements ChatHistoryManageService { @Override public ChatHistoryPage queryUserMessages(BigInteger userId, BigInteger sessionId, ChatPageQuery query) { - getUserSession(userId, sessionId); + ChatSessionSummary summary = getUserSession(userId, sessionId); + ChatHistoryPage firstPage = restoreRecentMessages(summary, query); + if (firstPage != null) { + return firstPage; + } return chatHistoryQueryService.queryHistoryMessages(sessionId, query); } @Override public ChatHistoryPage queryAdminMessages(BigInteger sessionId, ChatPageQuery query) { - getAdminSession(sessionId); + ChatSessionSummary summary = getAdminSession(sessionId); + ChatHistoryPage firstPage = restoreRecentMessages(summary, query); + if (firstPage != null) { + return firstPage; + } return chatHistoryQueryService.queryHistoryMessages(sessionId, query); } @@ -92,4 +104,48 @@ public class ChatHistoryManageServiceImpl implements ChatHistoryManageService { getUserSession(userId, sessionId); chatSessionCommandService.deleteSession(sessionId, userId, operatorId); } + + @Override + public java.util.List listUserRoundVariants(BigInteger userId, BigInteger sessionId, BigInteger roundId) { + getUserSession(userId, sessionId); + return chatRoundOperateService.listVariants(sessionId, roundId); + } + + @Override + public ChatMessageRecord selectUserRoundVariant(BigInteger userId, BigInteger sessionId, BigInteger roundId, Integer variantIndex, BigInteger operatorId) { + getUserSession(userId, sessionId); + return chatRoundOperateService.selectVariant(sessionId, roundId, variantIndex, operatorId); + } + + @Override + public java.util.List listAdminRoundVariants(BigInteger sessionId, BigInteger roundId) { + getAdminSession(sessionId); + return chatRoundOperateService.listVariants(sessionId, roundId); + } + + @Override + public ChatMessageRecord selectAdminRoundVariant(BigInteger sessionId, BigInteger roundId, Integer variantIndex, BigInteger operatorId) { + getAdminSession(sessionId); + return chatRoundOperateService.selectVariant(sessionId, roundId, variantIndex, operatorId); + } + + private ChatHistoryPage restoreRecentMessages(ChatSessionSummary summary, ChatPageQuery query) { + if (summary == null || query == null || query.getPageNumber() != 1) { + return null; + } + java.util.List records = chatSessionQueryService.getRecentTail( + summary.getId(), + Math.toIntExact(query.getPageSize()) + ); + if (records == null || records.isEmpty()) { + return null; + } + ChatHistoryPage page = new ChatHistoryPage(); + page.setPageNumber(query.getPageNumber()); + page.setPageSize(query.getPageSize()); + page.setRecords(records); + long total = summary.getMessageCount() == null ? 0L : summary.getMessageCount(); + page.setTotal(Math.max(total, records.size())); + return page; + } } diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/PublicChatSessionRestoreServiceImpl.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/PublicChatSessionRestoreServiceImpl.java index d4023f0..43e4e8c 100644 --- a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/PublicChatSessionRestoreServiceImpl.java +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/PublicChatSessionRestoreServiceImpl.java @@ -5,8 +5,10 @@ import tech.easyflow.chatlog.config.ChatCacheProperties; import tech.easyflow.chatlog.domain.dto.ChatMessageRecord; import tech.easyflow.chatlog.domain.dto.ChatSessionSummary; import tech.easyflow.chatlog.domain.dto.PublicChatSessionRestoreResult; +import tech.easyflow.chatlog.service.ChatRoundOperateService; import tech.easyflow.chatlog.service.ChatSessionQueryService; import tech.easyflow.chatlog.service.PublicChatSessionRestoreService; +import tech.easyflow.common.web.exceptions.BusinessException; import java.math.BigInteger; import java.util.ArrayList; @@ -18,11 +20,14 @@ import java.util.Objects; public class PublicChatSessionRestoreServiceImpl implements PublicChatSessionRestoreService { private final ChatSessionQueryService chatSessionQueryService; + private final ChatRoundOperateService chatRoundOperateService; private final ChatCacheProperties chatCacheProperties; public PublicChatSessionRestoreServiceImpl(ChatSessionQueryService chatSessionQueryService, + ChatRoundOperateService chatRoundOperateService, ChatCacheProperties chatCacheProperties) { this.chatSessionQueryService = chatSessionQueryService; + this.chatRoundOperateService = chatRoundOperateService; this.chatCacheProperties = chatCacheProperties; } @@ -54,6 +59,18 @@ public class PublicChatSessionRestoreServiceImpl implements PublicChatSessionRes return result; } + @Override + public List listVariants(BigInteger userId, BigInteger assistantId, BigInteger sessionId, BigInteger roundId) { + requireOwnedSession(userId, assistantId, sessionId); + return chatRoundOperateService.listVariants(sessionId, roundId); + } + + @Override + public ChatMessageRecord selectVariant(BigInteger userId, BigInteger assistantId, BigInteger sessionId, BigInteger roundId, Integer variantIndex, BigInteger operatorId) { + requireOwnedSession(userId, assistantId, sessionId); + return chatRoundOperateService.selectVariant(sessionId, roundId, variantIndex, operatorId); + } + private int resolveLimit(Integer limit) { int defaultLimit = Math.max(chatCacheProperties.getTailSize(), 1); if (limit == null || limit <= 0) { @@ -61,4 +78,15 @@ public class PublicChatSessionRestoreServiceImpl implements PublicChatSessionRes } return Math.min(limit, defaultLimit); } + + private ChatSessionSummary requireOwnedSession(BigInteger userId, BigInteger assistantId, BigInteger sessionId) { + ChatSessionSummary summary = chatSessionQueryService.getSessionSummary(sessionId); + if (summary == null || Integer.valueOf(1).equals(summary.getIsDeleted())) { + throw new BusinessException("会话不存在"); + } + if (!Objects.equals(summary.getUserId(), userId) || !Objects.equals(summary.getAssistantId(), assistantId)) { + throw new BusinessException("无权访问该会话"); + } + return summary; + } } diff --git a/easyflow-ui-admin/app/src/components/chat-history/ChatHistoryDetailDrawer.vue b/easyflow-ui-admin/app/src/components/chat-history/ChatHistoryDetailDrawer.vue index b2d6863..b9784bc 100644 --- a/easyflow-ui-admin/app/src/components/chat-history/ChatHistoryDetailDrawer.vue +++ b/easyflow-ui-admin/app/src/components/chat-history/ChatHistoryDetailDrawer.vue @@ -2,9 +2,10 @@ import type { ChatTimeTimelineItem } from '@easyflow/types'; import { Close } from '@element-plus/icons-vue'; -import { ElButton, ElEmpty, ElIcon, ElScrollbar } from 'element-plus'; +import { ElButton, ElEmpty, ElIcon, ElMessage, ElScrollbar } from 'element-plus'; import ChatTimeMessageContent from '#/components/chat/ChatTimeMessageContent.vue'; +import ChatMessageActionBar from '#/components/chat-workspace/ChatMessageActionBar.vue'; interface ChatHistoryDetailDrawerProps { visible?: boolean; @@ -13,6 +14,7 @@ interface ChatHistoryDetailDrawerProps { messages?: ChatTimeTimelineItem[]; hasMore?: boolean; onLoadMore?: (() => Promise | void) | undefined; + switchingRoundIds?: string[]; } const props = withDefaults(defineProps(), { @@ -22,10 +24,17 @@ const props = withDefaults(defineProps(), { messages: () => [], hasMore: false, onLoadMore: undefined, + switchingRoundIds: () => [], }); const emit = defineEmits<{ close: []; + selectVariant: [ + payload: { + direction: 'next' | 'previous'; + item: ChatTimeTimelineItem; + }, + ]; }>(); function formatTime(value?: number | string) { @@ -57,6 +66,77 @@ function resolveSenderName(item: any) { async function handleLoadMore() { await props.onLoadMore?.(); } + +function shouldShowVariantNavigator(item: ChatTimeTimelineItem) { + return ( + item.role === 'assistant' && + isFinalAssistantInRound(item) && + Number(item.variantCount || 0) > 1 && + Boolean(item.roundId) + ); +} + +function canSwitchVariant( + item: ChatTimeTimelineItem, + direction: 'next' | 'previous', +) { + if ( + item.role !== 'assistant' || + !isFinalAssistantInRound(item) || + !item.switchable || + isVariantSwitching(item) + ) { + return false; + } + const current = Number(item.variantIndex || item.selectedVariantIndex || 1); + const total = Number(item.variantCount || 1); + if (direction === 'previous') { + return current > 1; + } + return current < total; +} + +function isVariantSwitching(item: ChatTimeTimelineItem) { + return Boolean( + item.roundId && props.switchingRoundIds.includes(String(item.roundId)), + ); +} + +function isFinalAssistantInRound(item: ChatTimeTimelineItem) { + if (item.role !== 'assistant') { + return false; + } + if (!item.roundId) { + return true; + } + for (let index = props.messages.length - 1; index >= 0; index -= 1) { + const candidate = props.messages[index]; + if ( + candidate?.role === 'assistant' && + candidate.roundId === item.roundId && + String(candidate.variantIndex || '') === String(item.variantIndex || '') + ) { + return candidate.id === item.id; + } + } + return true; +} + +function canCopyMessage(item: ChatTimeTimelineItem) { + return item.role !== 'tool' && Boolean(String(item.content || '').trim()); +} + +async function handleCopyMessage(item: ChatTimeTimelineItem) { + if (!canCopyMessage(item)) { + return; + } + try { + await navigator.clipboard.writeText(String(item.content || '')); + ElMessage.success('已复制'); + } catch { + ElMessage.error('复制失败'); + } +}