From 68daf391af0f0a509aa1ef39112796fe3b11bcfc Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Tue, 23 Dec 2025 15:57:11 +0800 Subject: [PATCH] =?UTF-8?q?ai=E5=AF=B9=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/xyzh/ai/client/dto/ChatRequest.java | 5 +- .../xyzh/ai/controller/ChatController.java | 16 +- .../ai/service/impl/AgentChatServiceImpl.java | 1 + .../ai/src/main/resources/application-dev.yml | 2 +- .../ai/src/main/resources/application.yml | 2 +- .../workcase/service/WorkcaseChatService.java | 84 -------- .../system/controller/GuestController.java | 1 + .../system/service/impl/GuestServiceImpl.java | 2 + .../service/impl/SysUserServiceImpl.java | 9 +- .../resources/mapper/user/TbSysUserMapper.xml | 3 + .../mapper/user/TbSysUserRoleMapper.xml | 5 +- .../{KnowledgeInit.java => AiInit.java} | 34 ++- .../controller/WorkcaseChatContorller.java | 107 +--------- .../service/WorkcaseChatServiceImpl.java | 200 ------------------ .../workcase/service/WorkcaseServiceImpl.java | 9 - .../packages/workcase_wechat/api/ai/aiChat.ts | 194 +++++++++++++++++ .../packages/workcase_wechat/api/ai/index.ts | 1 + .../packages/workcase_wechat/api/index.ts | 3 +- .../api/workcase/workcaseChat.ts | 158 +++++++++++--- .../packages/workcase_wechat/config/index.ts | 1 + .../workcase_wechat/pages/index/index.scss | 44 ++++ .../workcase_wechat/pages/index/index.uvue | 132 +++++------- .../workcase_wechat/types/ai/aiChat.ts | 118 +++++++++++ 23 files changed, 608 insertions(+), 523 deletions(-) rename urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/{KnowledgeInit.java => AiInit.java} (85%) create mode 100644 urbanLifelineWeb/packages/workcase_wechat/api/ai/aiChat.ts create mode 100644 urbanLifelineWeb/packages/workcase_wechat/api/ai/index.ts create mode 100644 urbanLifelineWeb/packages/workcase_wechat/config/index.ts diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/ChatRequest.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/ChatRequest.java index 9a685d39..260415fe 100644 --- a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/ChatRequest.java +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/ChatRequest.java @@ -17,9 +17,10 @@ import org.xyzh.api.ai.dto.DifyFileInfo; public class ChatRequest { /** - * 输入变量 + * 输入变量(Dify API 必需字段) */ - private Map inputs; + @JSONField(serializeFeatures = com.alibaba.fastjson2.JSONWriter.Feature.WriteMapNullValue) + private Map inputs = new java.util.HashMap<>(); /** * 用户问题 diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/controller/ChatController.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/controller/ChatController.java index 0e028785..ff5ae510 100644 --- a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/controller/ChatController.java +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/controller/ChatController.java @@ -66,7 +66,7 @@ public class ChatController { chat.setUserType(false); if(NonUtils.isNotEmpty(token)){ LoginDomain loginDomain = LoginUtil.getCurrentLogin(); - if (NonUtils.isNotEmpty(loginDomain)) { + if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") { chat.setUserType(true); } } @@ -95,7 +95,7 @@ public class ChatController { chat.setUserType(false); if(NonUtils.isNotEmpty(token)){ LoginDomain loginDomain = LoginUtil.getCurrentLogin(); - if (NonUtils.isNotEmpty(loginDomain)) { + if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") { chat.setUserType(true); } } @@ -114,7 +114,7 @@ public class ChatController { chat.setUserType(false); if(NonUtils.isNotEmpty(token)){ LoginDomain loginDomain = LoginUtil.getCurrentLogin(); - if (NonUtils.isNotEmpty(loginDomain)) { + if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") { chat.setUserType(true); } } @@ -135,7 +135,7 @@ public class ChatController { filter.setUserType(false); if(NonUtils.isNotEmpty(token)){ LoginDomain loginDomain = LoginUtil.getCurrentLogin(); - if (NonUtils.isNotEmpty(loginDomain)) { + if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") { filter.setUserType(true); } } @@ -161,7 +161,7 @@ public class ChatController { filter.setUserType(false); if(NonUtils.isNotEmpty(token)){ LoginDomain loginDomain = LoginUtil.getCurrentLogin(); - if (NonUtils.isNotEmpty(loginDomain)) { + if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") { filter.setUserType(true); } } @@ -192,7 +192,7 @@ public class ChatController { chatPrepareData.setUserType(false); if(NonUtils.isNotEmpty(token)){ LoginDomain loginDomain = LoginUtil.getCurrentLogin(); - if (NonUtils.isNotEmpty(loginDomain)) { + if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") { chatPrepareData.setUserType(true); } } @@ -245,7 +245,7 @@ public class ChatController { filter.setUserType(false); if(NonUtils.isNotEmpty(token)){ LoginDomain loginDomain = LoginUtil.getCurrentLogin(); - if (NonUtils.isNotEmpty(loginDomain)) { + if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") { filter.setUserType(true); } } @@ -278,7 +278,7 @@ public class ChatController { filter.setUserType(false); if(NonUtils.isNotEmpty(token)){ LoginDomain loginDomain = LoginUtil.getCurrentLogin(); - if (NonUtils.isNotEmpty(loginDomain)) { + if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") { filter.setUserType(true); } } diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentChatServiceImpl.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentChatServiceImpl.java index f47d4746..40416218 100644 --- a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentChatServiceImpl.java +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentChatServiceImpl.java @@ -326,6 +326,7 @@ public class AgentChatServiceImpl implements AgentChatService { chatRequest.setQuery(query); chatRequest.setUser(userId); chatRequest.setResponseMode("streaming"); + chatRequest.setInputs(new HashMap<>()); // Dify API 要求 inputs 必传 if (filesData != null && !filesData.isEmpty()) { chatRequest.setFiles(filesData); diff --git a/urbanLifelineServ/ai/src/main/resources/application-dev.yml b/urbanLifelineServ/ai/src/main/resources/application-dev.yml index 7fa0b20c..da48f901 100644 --- a/urbanLifelineServ/ai/src/main/resources/application-dev.yml +++ b/urbanLifelineServ/ai/src/main/resources/application-dev.yml @@ -17,7 +17,7 @@ auth: - /error - /actuator/health - /actuator/info - - /ai/chat/* # AI对话,有非系统用户对话的接口,无登录状态 + - /ai/chat/** # AI对话,有非系统用户对话的接口,无登录状态 security: aes: diff --git a/urbanLifelineServ/ai/src/main/resources/application.yml b/urbanLifelineServ/ai/src/main/resources/application.yml index f2d8df13..dc0f506e 100644 --- a/urbanLifelineServ/ai/src/main/resources/application.yml +++ b/urbanLifelineServ/ai/src/main/resources/application.yml @@ -17,7 +17,7 @@ auth: - /error - /actuator/health - /actuator/info - - /ai/chat/* # AI对话,有非系统用户对话的接口,无登录状态 + - /ai/chat/** # AI对话,有非系统用户对话的接口,无登录状态 security: aes: diff --git a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WorkcaseChatService.java b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WorkcaseChatService.java index 1ba64c5f..c4e97cb3 100644 --- a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WorkcaseChatService.java +++ b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WorkcaseChatService.java @@ -1,11 +1,6 @@ package org.xyzh.api.workcase.service; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import org.xyzh.api.ai.dto.ChatPrepareData; -import org.xyzh.api.ai.dto.TbChat; -import org.xyzh.api.ai.dto.TbChatMessage; import org.xyzh.api.workcase.dto.TbWordCloudDTO; -import org.xyzh.api.workcase.dto.TbWorkcaseDTO; import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.core.page.PageRequest; @@ -18,85 +13,6 @@ import org.xyzh.common.core.page.PageRequest; */ public interface WorkcaseChatService { - // ========================= 聊天管理 ========================== - /** - * @description 来客创建聊天对话 - * @param - * @author yslg - * @since 2025-12-18 - */ - ResultDomain createChat(TbChat chat); - - /** - * @description 更新聊天名称 - * @param - * @author yslg - * @since 2025-12-18 - */ - ResultDomain updateChat(TbChat chat); - - /** - * 获取聊天列表 - * @param agentId 智能体ID - * @return 聊天列表 - */ - ResultDomain getChatList(TbChat filter); - - // ========================= 聊天信息管理 ====================== - - /** - * 获取会话消息列表 - * @param filter 会话过滤条件(包含agentId, chatId, userId, userType) - * @return 会话消息列表 - */ - ResultDomain getChatMessageList(TbChat filter); - - // 用户转人工后,就不和智能体聊天了,在微信客服里聊天 - /** - * 准备聊天数据(POST传递复杂参数) - * @param prepareData 对话准备数据(包含agentId, chatId, query, files, userId, userType) - * @return ResultDomain 返回sessionId - */ - ResultDomain prepareChatMessageSession(ChatPrepareData prepareData); - - /** - * 流式对话(SSE)- 使用sessionId建立SSE连接 产生chatMessage - * @param sessionId 会话标识 - * @return SseEmitter 流式推送对象 - */ - SseEmitter streamChatMessageWithSse(String sessionId); - - /** - * 停止对话生成(通过Dify TaskID) - * @param filter 会话过滤条件(包含agentId, userId, userType) - * @param taskId Dify任务ID - * @return 停止结果 - */ - ResultDomain stopChatMessageByTaskId(TbChat filter, String taskId); - - /** - * 评价 - * @param filter 会话过滤条件(包含agentId, chatId, userId, userType) - * @param messageId 消息ID - * @param comment 评价 - * @return 评价结果 - */ - ResultDomain commentChatMessage(TbChat filter, String messageId, String comment); - - // =============================== 对话分析 ========================== - - /** - * 对话分析, 提取出工单相关的内容,这里有智能体调用等 - * @param chatId 对话ID - * @return 对话分析结果 - */ - ResultDomain analyzeChat(String chatId); - - // 对话总结 - ResultDomain summaryChat(String chatId); - - - // =============================== 对话、工单等词云管理 ========================== /** diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/GuestController.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/GuestController.java index 0ebfa35f..46160110 100644 --- a/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/GuestController.java +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/GuestController.java @@ -178,6 +178,7 @@ public class GuestController { // 3. 来客不存在,创建新来客 if (guest == null) { TbGuestDTO newGuest = new TbGuestDTO(); + newGuest.setOptsn(IdUtil.getOptsn()); newGuest.setUserId(IdUtil.generateID()); newGuest.setWechatId(loginParam.getWechatId()); newGuest.setPhone(loginParam.getPhone()); diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/GuestServiceImpl.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/GuestServiceImpl.java index 853c6979..51de183d 100644 --- a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/GuestServiceImpl.java +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/GuestServiceImpl.java @@ -13,6 +13,7 @@ import org.xyzh.common.core.page.PageDomain; import org.xyzh.common.core.page.PageRequest; import org.xyzh.common.dto.sys.TbGuestDTO; import org.xyzh.common.dto.sys.TbSysUserRoleDTO; +import org.xyzh.common.utils.id.IdUtil; import org.xyzh.system.mapper.user.TbGuestMapper; import org.xyzh.system.mapper.user.TbSysUserRoleMapper; @@ -48,6 +49,7 @@ public class GuestServiceImpl implements GuestService{ // 绑定访客角色(role_guest) TbSysUserRoleDTO userRole = new TbSysUserRoleDTO(); + userRole.setOptsn(IdUtil.getOptsn()); userRole.setUserId(guest.getUserId()); userRole.setRoleId("role_guest"); userRole.setDeptId("dept_root"); diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysUserServiceImpl.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysUserServiceImpl.java index 51f0ce90..870f8666 100644 --- a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysUserServiceImpl.java +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysUserServiceImpl.java @@ -224,12 +224,15 @@ public class SysUserServiceImpl implements SysUserService { @Override public ResultDomain getLoginUser(SysUserVO filter) { - // 登录查询语义与 getUser 相同(可根据用户名/手机号/邮箱查询) + // 登录查询语义与 getUser 相同(可根据用户名/手机号/邮箱/wechatId查询) if(NonUtils.isNotNull(filter.getPhone())){ filter.setPhone(filter.getPhone()); } - SysUserVO userVO = userMapper.getUserByFilter(filter).get(0); - return ResultDomain.success("查询成功", userVO); + List list = userMapper.getUserByFilter(filter); + if (list == null || list.isEmpty()) { + return ResultDomain.failure("用户不存在"); + } + return ResultDomain.success("查询成功", list.get(0)); } @Override diff --git a/urbanLifelineServ/system/src/main/resources/mapper/user/TbSysUserMapper.xml b/urbanLifelineServ/system/src/main/resources/mapper/user/TbSysUserMapper.xml index 8bd41c58..99cdda09 100644 --- a/urbanLifelineServ/system/src/main/resources/mapper/user/TbSysUserMapper.xml +++ b/urbanLifelineServ/system/src/main/resources/mapper/user/TbSysUserMapper.xml @@ -172,6 +172,9 @@ AND ui.username = #{filter.username} + + AND u.wechat_id = #{filter.wechatId} + AND (u.deleted IS NULL OR u.deleted = false) diff --git a/urbanLifelineServ/system/src/main/resources/mapper/user/TbSysUserRoleMapper.xml b/urbanLifelineServ/system/src/main/resources/mapper/user/TbSysUserRoleMapper.xml index b437e819..ed136fbf 100644 --- a/urbanLifelineServ/system/src/main/resources/mapper/user/TbSysUserRoleMapper.xml +++ b/urbanLifelineServ/system/src/main/resources/mapper/user/TbSysUserRoleMapper.xml @@ -7,6 +7,7 @@ + @@ -62,7 +63,7 @@ - user_id, role_id, + user_id, role_id, dept_id, optsn, creator, updater, dept_path, remark, create_time, update_time, delete_time, deleted @@ -73,6 +74,7 @@ user_id, role_id, + dept_id, optsn, creator, @@ -88,6 +90,7 @@ #{userId}, #{roleId}, + #{deptId}, #{optsn}, #{creator}, diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/KnowledgeInit.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/AiInit.java similarity index 85% rename from urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/KnowledgeInit.java rename to urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/AiInit.java index 8025c61f..f382b431 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/KnowledgeInit.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/AiInit.java @@ -9,7 +9,9 @@ import org.slf4j.LoggerFactory; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.xyzh.api.ai.dto.TbAgent; import org.xyzh.api.ai.dto.TbKnowledge; +import org.xyzh.api.ai.service.AgentService; import org.xyzh.api.ai.service.KnowledgeService; import org.xyzh.api.system.service.SysConfigService; import org.xyzh.common.core.domain.ResultDomain; @@ -23,8 +25,8 @@ import org.xyzh.common.utils.id.IdUtil; * @since 2025-12-18 */ @Configuration -public class KnowledgeInit { - private static final Logger logger = LoggerFactory.getLogger(KnowledgeInit.class); +public class AiInit { + private static final Logger logger = LoggerFactory.getLogger(AiInit.class); private static final String SERVICE_WORKCASE = "workcase"; private static final String CATEGORY_INTERNAL = "internal"; @@ -36,6 +38,9 @@ public class KnowledgeInit { @DubboReference(version = "1.0.0", group = "system", timeout = 30000, retries = 0) private SysConfigService sysConfigService; + @DubboReference(version = "1.0.0", group = "ai", timeout = 30000, retries = 0) + private AgentService agentService; + @Bean public CommandLineRunner knowledgeInitRunner() { return args -> { @@ -58,6 +63,31 @@ public class KnowledgeInit { }; } + @Bean + public CommandLineRunner agentInitRunner(){ + return args -> { + logger.info("开始初始化客服系统智能体..."); + TbAgent agent = new TbAgent(); + agent.setIsOuter(true); + agent.setName("泰豪小电"); + ResultDomain listDomain = agentService.getAgentList(agent); + if (listDomain.getSuccess()&&!listDomain.getDataList().isEmpty()) { + logger.info("泰豪小电智能体已经存在"); + return; + } + agent.setApiKey("app-CDKy0wYkPnl6dA6G7eu113Vw"); + agent.setIntroduce("您好,我是泰豪小电智能客服。请描述您的问题,我会尽力协助。"); + agent.setCategory("客服智能体"); + agent.setCategory("user_admin"); + ResultDomain resultDomain = agentService.addAgent(agent); + if(resultDomain.getSuccess()){ + logger.info("泰豪小电智能体初始化成功"); + }else{ + logger.error("泰豪小电智能体初始化失败"+resultDomain.getMessage()); + } + }; + } + /** * 构建8个知识库配置 */ diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java index 6f0c5ea3..39b6f912 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java @@ -2,6 +2,7 @@ package org.xyzh.workcase.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -11,16 +12,11 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import org.xyzh.api.ai.dto.ChatPrepareData; -import org.xyzh.api.ai.dto.TbChat; -import org.xyzh.api.ai.dto.TbChatMessage; import org.xyzh.api.workcase.dto.TbChatRoomDTO; import org.xyzh.api.workcase.dto.TbChatRoomMemberDTO; import org.xyzh.api.workcase.dto.TbChatRoomMessageDTO; import org.xyzh.api.workcase.dto.TbCustomerServiceDTO; import org.xyzh.api.workcase.dto.TbWordCloudDTO; -import org.xyzh.api.workcase.dto.TbWorkcaseDTO; import org.xyzh.api.workcase.service.ChatRoomService; import org.xyzh.api.workcase.service.WorkcaseChatService; import org.xyzh.api.workcase.vo.ChatMemberVO; @@ -49,6 +45,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; * @since 2025-12-19 */ @Tag(name = "工单对话") +@Validated @RestController @RequestMapping("/workcase/chat") public class WorkcaseChatContorller { @@ -59,106 +56,6 @@ public class WorkcaseChatContorller { @Autowired private ChatRoomService chatRoomService; - // ========================= AI对话管理 ========================= - - @Operation(summary = "创建对话") - @PreAuthorize("hasAuthority('workcase:chat:create')") - @PostMapping - public ResultDomain createChat(@RequestBody TbChat chat) { - ValidationResult vr = ValidationUtils.validate(chat, Arrays.asList( - ValidationUtils.requiredString("userId", "用户ID") - )); - if (!vr.isValid()) { - return ResultDomain.failure(vr.getAllErrors()); - } - return workcaseChatService.createChat(chat); - } - - @Operation(summary = "更新对话") - @PreAuthorize("hasAuthority('workcase:chat:update')") - @PutMapping - public ResultDomain updateChat(@RequestBody TbChat chat) { - ValidationResult vr = ValidationUtils.validate(chat, Arrays.asList( - ValidationUtils.requiredString("chatId", "对话ID") - )); - if (!vr.isValid()) { - return ResultDomain.failure(vr.getAllErrors()); - } - return workcaseChatService.updateChat(chat); - } - - @Operation(summary = "查询对话列表") - @PreAuthorize("hasAuthority('workcase:chat:list')") - @PostMapping("/list") - public ResultDomain getChatList(@RequestBody TbChat filter) { - return workcaseChatService.getChatList(filter); - } - - @Operation(summary = "获取对话消息列表") - @PreAuthorize("hasAuthority('workcase:chat:message')") - @PostMapping("/message/list") - public ResultDomain getChatMessageList(@RequestBody TbChat filter) { - ValidationResult vr = ValidationUtils.validate(filter, Arrays.asList( - ValidationUtils.requiredString("chatId", "对话ID") - )); - if (!vr.isValid()) { - return ResultDomain.failure(vr.getAllErrors()); - } - return workcaseChatService.getChatMessageList(filter); - } - - @Operation(summary = "准备对话会话") - @PreAuthorize("hasAuthority('workcase:chat:stream')") - @PostMapping("/prepare") - public ResultDomain prepareChatMessageSession(@RequestBody ChatPrepareData prepareData) { - ValidationResult vr = ValidationUtils.validate(prepareData, Arrays.asList( - ValidationUtils.requiredString("chatId", "对话ID"), - ValidationUtils.requiredString("query", "用户问题") - )); - if (!vr.isValid()) { - return ResultDomain.failure(vr.getAllErrors()); - } - return workcaseChatService.prepareChatMessageSession(prepareData); - } - - @Operation(summary = "流式对话(SSE)") - @PreAuthorize("hasAuthority('workcase:chat:stream')") - @GetMapping(value = "/stream/{sessionId}", produces = "text/event-stream") - public SseEmitter streamChatMessage(@PathVariable String sessionId) { - return workcaseChatService.streamChatMessageWithSse(sessionId); - } - - @Operation(summary = "停止对话") - @PreAuthorize("hasAuthority('workcase:chat:stream')") - @PostMapping("/stop/{taskId}") - public ResultDomain stopChat(@RequestBody TbChat filter, @PathVariable String taskId) { - return workcaseChatService.stopChatMessageByTaskId(filter, taskId); - } - - @Operation(summary = "评论对话消息") - @PreAuthorize("hasAuthority('workcase:chat:message')") - @PostMapping("/comment") - public ResultDomain commentChatMessage(@RequestBody TbChat filter, - @RequestParam String messageId, @RequestParam String comment) { - return workcaseChatService.commentChatMessage(filter, messageId, comment); - } - - // ========================= 对话分析 ========================= - - @Operation(summary = "分析对话(AI预填工单信息)") - @PreAuthorize("hasAuthority('workcase:chat:analyze')") - @GetMapping("/analyze/{chatId}") - public ResultDomain analyzeChat(@PathVariable String chatId) { - return workcaseChatService.analyzeChat(chatId); - } - - @Operation(summary = "总结对话") - @PreAuthorize("hasAuthority('workcase:chat:analyze')") - @PostMapping("/summary/{chatId}") - public ResultDomain summaryChat(@PathVariable String chatId) { - return workcaseChatService.summaryChat(chatId); - } - // ========================= ChatRoom聊天室管理(实时IM) ========================= @Operation(summary = "创建聊天室(转人工时调用)") diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseChatServiceImpl.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseChatServiceImpl.java index 71fe9f3e..250a7d48 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseChatServiceImpl.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseChatServiceImpl.java @@ -4,24 +4,16 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.List; -import org.apache.dubbo.config.annotation.DubboReference; import org.apache.dubbo.config.annotation.DubboService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import org.xyzh.api.ai.dto.ChatPrepareData; -import org.xyzh.api.ai.dto.TbChat; -import org.xyzh.api.ai.dto.TbChatMessage; -import org.xyzh.api.ai.service.AgentChatService; import org.xyzh.api.workcase.dto.TbWordCloudDTO; -import org.xyzh.api.workcase.dto.TbWorkcaseDTO; import org.xyzh.api.workcase.service.WorkcaseChatService; import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.core.page.PageDomain; import org.xyzh.common.core.page.PageParam; import org.xyzh.common.core.page.PageRequest; -import org.xyzh.common.redis.service.RedisService; import org.xyzh.common.utils.id.IdUtil; import org.xyzh.workcase.mapper.TbWordCloudMapper; @@ -30,201 +22,9 @@ public class WorkcaseChatServiceImpl implements WorkcaseChatService{ private static final Logger logger = LoggerFactory.getLogger(WorkcaseChatServiceImpl.class); - private static final String CHAT_COUNT_KEY_PREFIX = "workcase:chat:count:"; - private static final int TRANSFER_HUMAN_THRESHOLD = 3; - - @DubboReference(version = "1.0.0", group = "ai", check = false, retries = 0) - private AgentChatService agentChatService; - @Autowired private TbWordCloudMapper wordCloudMapper; - @Autowired - private RedisService redisService; - - // ========================= 聊天管理 ========================== - - @Override - public ResultDomain createChat(TbChat chat) { - logger.info("创建对话: userId={}", chat.getUserId()); - ResultDomain result = agentChatService.createChat(chat); - if (result.getSuccess() && result.getData() != null) { - redisService.set(CHAT_COUNT_KEY_PREFIX + result.getData().getChatId(), 0); - } - return result; - } - - @Override - public ResultDomain updateChat(TbChat chat) { - logger.info("更新对话: chatId={}", chat.getChatId()); - return agentChatService.updateChat(chat); - } - - @Override - public ResultDomain getChatList(TbChat filter) { - return agentChatService.getChatList(filter); - } - - // ========================= 聊天信息管理 ====================== - - @Override - public ResultDomain getChatMessageList(TbChat filter) { - return agentChatService.getChatMessageList(filter); - } - - @Override - public ResultDomain prepareChatMessageSession(ChatPrepareData prepareData) { - logger.info("准备对话会话: chatId={}, query={}", prepareData.getChatId(), prepareData.getQuery()); - - String chatId = prepareData.getChatId(); - Object countObj = redisService.get(CHAT_COUNT_KEY_PREFIX + chatId); - int chatCount = (countObj != null) ? (Integer) countObj : 0; - chatCount++; - redisService.set(CHAT_COUNT_KEY_PREFIX + chatId, chatCount); - - ResultDomain result = agentChatService.prepareChatMessageSession(prepareData); - - if (result.getSuccess() && chatCount >= TRANSFER_HUMAN_THRESHOLD) { - logger.info("已达到{}次AI对话,建议转人工: chatId={}", TRANSFER_HUMAN_THRESHOLD, chatId); - } - - return result; - } - - @Override - public SseEmitter streamChatMessageWithSse(String sessionId) { - return agentChatService.streamChatMessageWithSse(sessionId); - } - - @Override - public ResultDomain stopChatMessageByTaskId(TbChat filter, String taskId) { - return agentChatService.stopChatMessageByTaskId(filter, taskId); - } - - @Override - public ResultDomain commentChatMessage(TbChat filter, String messageId, String comment) { - return agentChatService.commentChatMessage(filter, messageId, comment); - } - - // =============================== 对话分析 ========================== - - @Override - public ResultDomain analyzeChat(String chatId) { - logger.info("分析对话内容,生成工单预填信息: chatId={}", chatId); - - TbChat filter = new TbChat(); - filter.setChatId(chatId); - ResultDomain msgResult = agentChatService.getChatMessageList(filter); - - if (!msgResult.getSuccess() || msgResult.getDataList() == null || msgResult.getDataList().isEmpty()) { - return ResultDomain.failure("获取对话消息失败或消息为空"); - } - - List messages = msgResult.getDataList(); - - // ============== 伪代码:调用AI分析对话,自动生成工单预填信息 ============== - // 步骤5:AI根据聊天对话,自动生成部分工单信息,预填入小程序的工单创建表单 - // - // 1. 构建对话上下文 - // StringBuilder conversationContext = new StringBuilder(); - // for (TbChatMessage msg : messages) { - // conversationContext.append(msg.getRole()).append(": ").append(msg.getContent()).append("\n"); - // } - // - // 2. 调用Dify工作流或Agent进行对话分析 - // DifyWorkflowRequest request = new DifyWorkflowRequest(); - // request.setWorkflowId("workcase-analysis-workflow"); - // request.setInputs(Map.of("conversation", conversationContext.toString())); - // DifyWorkflowResponse response = difyService.runWorkflow(request); - // - // 3. 解析AI分析结果,提取工单预填信息 - // JSONObject analysisResult = JSON.parseObject(response.getOutputs()); - // TbWorkcaseDTO workcase = new TbWorkcaseDTO(); - // workcase.setType(analysisResult.getString("type")); // 问题类型:如"设备故障"、"维修申请" - // workcase.setDevice(analysisResult.getString("device")); // 设备名称:如"燃气管道"、"电梯" - // workcase.setDeviceCode(analysisResult.getString("deviceCode")); // 设备编号(如果用户提到) - // workcase.setEmergency(analysisResult.getString("emergency")); // 紧急程度:normal/urgent/critical - // workcase.setRemark(analysisResult.getString("description")); // 问题描述摘要 - // ============== 伪代码结束 ============== - - // 模拟AI分析结果(实际应由AI返回) - TbWorkcaseDTO workcase = new TbWorkcaseDTO(); - workcase.setType("设备故障"); - workcase.setDevice("待用户确认"); - workcase.setDeviceCode(""); - workcase.setEmergency("normal"); - workcase.setRemark("对话ID:" + chatId + " | " + buildConversationSummary(messages)); - - logger.info("对话分析完成,工单预填信息已生成: chatId={}", chatId); - return ResultDomain.success("对话分析完成", workcase); - } - - private String buildConversationSummary(List messages) { - StringBuilder summary = new StringBuilder(); - for (TbChatMessage msg : messages) { - if ("user".equals(msg.getRole())) { - summary.append(msg.getContent()).append(" "); - } - } - String result = summary.toString().trim(); - return result.length() > 200 ? result.substring(0, 200) + "..." : result; - } - - @Override - public ResultDomain summaryChat(String chatId) { - logger.info("总结对话: chatId={}", chatId); - - TbChat filter = new TbChat(); - filter.setChatId(chatId); - ResultDomain msgResult = agentChatService.getChatMessageList(filter); - - if (!msgResult.getSuccess() || msgResult.getDataList() == null) { - return ResultDomain.failure("获取对话消息失败"); - } - - List messages = msgResult.getDataList(); - - // TODO: 调用AI进行对话总结,提取关键词更新词云 - // 伪代码: - // String summary = aiService.summarize(messages); - // List keywords = aiService.extractKeywords(messages); - // updateWordCloud(keywords); - - extractAndUpdateWordCloud(messages); - - TbWorkcaseDTO summary = new TbWorkcaseDTO(); - logger.info("对话总结完成: chatId={}", chatId); - return ResultDomain.success("对话总结完成", summary); - } - - private void extractAndUpdateWordCloud(List messages) { - // TODO: 调用AI提取关键词 - // 伪代码:List keywords = aiService.extractKeywords(messages); - - // 模拟提取的关键词 - String[] mockKeywords = {"故障", "维修", "设备"}; - String today = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE); - - for (String keyword : mockKeywords) { - TbWordCloudDTO queryFilter = new TbWordCloudDTO(); - queryFilter.setWord(keyword); - queryFilter.setCategory("fault"); - queryFilter.setStatDate(today); - TbWordCloudDTO existing = wordCloudMapper.selectWordCloudOne(queryFilter); - if (existing != null) { - wordCloudMapper.incrementFrequency(existing.getWordId(), 1); - } else { - TbWordCloudDTO wordCloud = new TbWordCloudDTO(); - wordCloud.setWordId(IdUtil.generateUUID()); - wordCloud.setWord(keyword); - wordCloud.setCategory("fault"); - wordCloud.setStatDate(today); - wordCloud.setFrequency("1"); - wordCloudMapper.insertWordCloud(wordCloud); - } - } - } - // =============================== 词云管理 ========================== @Override diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java index 1b7ec28e..7c085d29 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java @@ -9,7 +9,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.xyzh.api.workcase.dto.TbWorkcaseDTO; import org.xyzh.api.workcase.dto.TbWorkcaseDeviceDTO; import org.xyzh.api.workcase.dto.TbWorkcaseProcessDTO; -import org.xyzh.api.workcase.service.WorkcaseChatService; import org.xyzh.api.workcase.service.WorkcaseService; import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.core.page.PageDomain; @@ -21,7 +20,6 @@ import org.xyzh.workcase.mapper.TbWorkcaseDeviceMapper; import org.xyzh.workcase.mapper.TbWorkcaseMapper; import org.xyzh.workcase.mapper.TbWorkcaseProcessMapper; -import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; @DubboService(version = "1.0.0",group = "workcase",timeout = 30000,retries = 0) @@ -37,9 +35,6 @@ public class WorkcaseServiceImpl implements WorkcaseService { @Autowired private TbWorkcaseDeviceMapper workcaseDeviceMapper; - @Autowired - private WorkcaseChatService workcaseChatService; - // ====================== 工单管理 ====================== @Override @@ -103,11 +98,9 @@ public class WorkcaseServiceImpl implements WorkcaseService { if ("done".equals(workcase.getStatus())) { process.setAction(WorkcaseProcessAction.FINISH.getName()); process.setMessage("工单完成"); - workcaseChatService.summaryChat(existing.getWorkcaseId()); } else if ("cancelled".equals(workcase.getStatus())) { process.setAction(WorkcaseProcessAction.REPEAL.getName()); process.setMessage("工单撤销"); - workcaseChatService.summaryChat(existing.getWorkcaseId()); } else { process.setAction(WorkcaseProcessAction.INFO.getName()); process.setMessage("状态变更: " + oldStatus + " -> " + workcase.getStatus()); @@ -309,13 +302,11 @@ public class WorkcaseServiceImpl implements WorkcaseService { workcase.setWorkcaseId(workcaseProcess.getWorkcaseId()); workcase.setStatus("done"); workcaseMapper.updateWorkcase(workcase); - workcaseChatService.summaryChat(workcaseProcess.getWorkcaseId()); } else if (WorkcaseProcessAction.REPEAL.getName().equals(action)) { TbWorkcaseDTO workcase = new TbWorkcaseDTO(); workcase.setWorkcaseId(workcaseProcess.getWorkcaseId()); workcase.setStatus("cancelled"); workcaseMapper.updateWorkcase(workcase); - workcaseChatService.summaryChat(workcaseProcess.getWorkcaseId()); } int rows = workcaseProcessMapper.insertWorkcaseProcess(workcaseProcess); diff --git a/urbanLifelineWeb/packages/workcase_wechat/api/ai/aiChat.ts b/urbanLifelineWeb/packages/workcase_wechat/api/ai/aiChat.ts new file mode 100644 index 00000000..f6c20de2 --- /dev/null +++ b/urbanLifelineWeb/packages/workcase_wechat/api/ai/aiChat.ts @@ -0,0 +1,194 @@ +import { request } from '../base' +import type { ResultDomain } from '../../types' +import type { + TbChat, + TbChatMessage, + CreateChatParam, + PrepareChatParam, + StopChatParam, + CommentMessageParam, + ChatListParam, + ChatMessageListParam, + SSECallbacks, + SSETask, + SSEMessageData +} from '../../types/ai/aiChat' + +/* eslint-disable @typescript-eslint/no-explicit-any */ +declare const uni: { + getStorageSync: (key: string) => any + request: (options: any) => any +} + +// API 基础配置 +const BASE_URL = 'http://localhost:8180' + +/** + * @description AI对话相关接口(直接调用ai模块) + * @filename aiChat.ts + * @author cascade + * @copyright xyzh + * @since 2025-12-23 + */ +export const aiChatAPI = { + baseUrl: '/urban-lifeline/ai/chat', + + // ====================== AI对话管理 ====================== + + /** + * 创建对话 + * @param param agentId、userId、title 必传 + */ + createChat(param: CreateChatParam): Promise> { + return request({ url: `${this.baseUrl}/conversation`, method: 'POST', data: param }) + }, + + /** + * 更新对话 + * @param chat agentId、userId、title、userType 必传 + */ + updateChat(chat: TbChat): Promise> { + return request({ url: `${this.baseUrl}/conversation`, method: 'PUT', data: chat }) + }, + + /** + * 查询对话列表 + * @param param agentId 必传 + */ + getChatList(param: ChatListParam): Promise> { + return request({ url: `${this.baseUrl}/conversations`, method: 'GET', data: param }) + }, + + /** + * 获取对话消息列表 + * @param param agentId、chatId、userId 必传 + */ + getChatMessageList(param: ChatMessageListParam): Promise> { + return request({ url: `${this.baseUrl}/messages`, method: 'POST', data: param }) + }, + + /** + * 准备流式对话会话 + * @param param agentId、chatId、query、userId 必传 + */ + prepareChatMessageSession(param: PrepareChatParam): Promise> { + return request({ url: `${this.baseUrl}/stream/prepare`, method: 'POST', data: param }) + }, + + /** + * 流式对话(SSE)- 返回EventSource URL + */ + getStreamUrl(sessionId: string): string { + return `${this.baseUrl}/stream?sessionId=${sessionId}` + }, + + /** + * 建立SSE流式对话连接 + * @param sessionId 会话ID(必传) + * @param callbacks 回调函数 + * @returns SSETask 可用于中止请求 + */ + streamChat(sessionId: string, callbacks: SSECallbacks): SSETask { + const url = `${BASE_URL}${this.baseUrl}/stream?sessionId=${sessionId}` + const token = uni.getStorageSync('token') || '' + + const requestTask = uni.request({ + url: url, + method: 'GET', + header: { + 'Accept': 'text/event-stream', + 'Authorization': token ? `Bearer ${token}` : '' + }, + enableChunked: true, + success: (res: any) => { + console.log('SSE请求完成:', res) + // 处理非200状态码 + if (res.statusCode !== 200) { + console.error('SSE请求状态码异常:', res.statusCode) + let errorMsg = '抱歉,服务暂时不可用,请稍后重试。' + if (res.statusCode === 401) { + errorMsg = '登录已过期,请重新登录。' + } else if (res.statusCode === 403) { + errorMsg = '无权限访问,请联系管理员。' + } else if (res.statusCode === 404) { + errorMsg = '会话不存在或已过期,请重新发起对话。' + } else if (res.statusCode >= 500) { + errorMsg = '服务器异常,请稍后重试。' + } + callbacks.onError?.(errorMsg) + } + callbacks.onComplete?.() + }, + fail: (err: any) => { + console.error('SSE请求失败:', err) + callbacks.onError?.('网络连接失败,请稍后重试。') + callbacks.onComplete?.() + } + }) + + // 监听分块数据 + requestTask.onChunkReceived((res: any) => { + try { + const decoder = new TextDecoder('utf-8') + const text = decoder.decode(new Uint8Array(res.data)) + + const lines = text.split('\n') + for (const line of lines) { + if (line.startsWith('data:')) { + const dataStr = line.substring(5).trim() + if (dataStr && dataStr !== '[DONE]') { + try { + const data: SSEMessageData = JSON.parse(dataStr) + const event = data.event + + if (event === 'message' || event === 'agent_message') { + callbacks.onMessage?.(data) + } else if (event === 'message_end') { + callbacks.onEnd?.(data.task_id || '') + } else if (event === 'error' || data.message) { + // 解析错误消息,提取友好提示 + let errorMsg = data.message || '发生错误,请稍后重试。' + // 处理嵌套的 JSON 错误信息 + if (errorMsg.includes('invalid_param')) { + errorMsg = '请求参数错误,请稍后重试。' + } else if (errorMsg.includes('rate_limit')) { + errorMsg = '请求过于频繁,请稍后再试。' + } else if (errorMsg.includes('quota_exceeded')) { + errorMsg = 'AI 服务额度已用完,请联系管理员。' + } + callbacks.onError?.(errorMsg) + } + } catch (e) { + console.log('解析SSE数据失败:', dataStr) + } + } + } + } + } catch (e) { + console.error('处理分块数据失败:', e) + } + }) + + return { + abort: () => { + requestTask.abort() + } + } + }, + + /** + * 停止对话 + * @param param taskId、agentId、userId 必传 + */ + stopChat(param: StopChatParam): Promise> { + return request({ url: `${this.baseUrl}/stop`, method: 'POST', data: param }) + }, + + /** + * 评价对话消息 + * @param param agentId、chatId、messageId、comment、userId 必传 + */ + commentChatMessage(param: CommentMessageParam): Promise> { + return request({ url: `${this.baseUrl}/comment`, method: 'POST', data: param }) + } +} diff --git a/urbanLifelineWeb/packages/workcase_wechat/api/ai/index.ts b/urbanLifelineWeb/packages/workcase_wechat/api/ai/index.ts new file mode 100644 index 00000000..8ae34855 --- /dev/null +++ b/urbanLifelineWeb/packages/workcase_wechat/api/ai/index.ts @@ -0,0 +1 @@ +export { aiChatAPI } from './aiChat' diff --git a/urbanLifelineWeb/packages/workcase_wechat/api/index.ts b/urbanLifelineWeb/packages/workcase_wechat/api/index.ts index 594598bd..80acc5a0 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/api/index.ts +++ b/urbanLifelineWeb/packages/workcase_wechat/api/index.ts @@ -1,3 +1,4 @@ export * from "./base" export * from "./sys" -export * from "./workcase" \ No newline at end of file +export * from "./workcase" +export * from "./ai" \ No newline at end of file diff --git a/urbanLifelineWeb/packages/workcase_wechat/api/workcase/workcaseChat.ts b/urbanLifelineWeb/packages/workcase_wechat/api/workcase/workcaseChat.ts index 8c251de3..fe485699 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/api/workcase/workcaseChat.ts +++ b/urbanLifelineWeb/packages/workcase_wechat/api/workcase/workcaseChat.ts @@ -11,26 +11,30 @@ import type { ChatMemberVO, ChatRoomMessageVO, CustomerServiceVO -} from '../../types/workcase/chatRoom' +} from '../../types/workcase' +import type { + TbChat, + TbChatMessage, + CreateChatParam, + PrepareChatParam, + StopChatParam, + CommentMessageParam, + ChatListParam, + ChatMessageListParam, + SSECallbacks, + SSETask, + SSEMessageData +} from '../../types/ai/aiChat' -// AI对话相关类型(简化版) -interface TbChat { - chatId?: string - userId?: string - title?: string - status?: string -} -interface TbChatMessage { - messageId?: string - chatId?: string - content?: string - role?: string -} -interface ChatPrepareData { - chatId?: string - message?: string +/* eslint-disable @typescript-eslint/no-explicit-any */ +declare const uni: { + getStorageSync: (key: string) => any + request: (options: any) => any } +// API 基础配置 +const BASE_URL = 'http://localhost:8180' + /** * @description 工单对话相关接口 * @filename workcaseChat.ts @@ -45,9 +49,10 @@ export const workcaseChatAPI = { /** * 创建对话 + * @param param agentId和userId必传 */ - createChat(chat: TbChat): Promise> { - return request({ url: this.baseUrl, method: 'POST', data: chat }) + createChat(param: CreateChatParam): Promise> { + return request({ url: this.baseUrl, method: 'POST', data: param }) }, /** @@ -59,23 +64,26 @@ export const workcaseChatAPI = { /** * 查询对话列表 + * @param param userId必传 */ - getChatList(filter: TbChat): Promise> { - return request({ url: `${this.baseUrl}/list`, method: 'POST', data: filter }) + getChatList(param: ChatListParam): Promise> { + return request({ url: `${this.baseUrl}/list`, method: 'POST', data: param }) }, /** * 获取对话消息列表 + * @param param chatId必传 */ - getChatMessageList(filter: TbChat): Promise> { - return request({ url: `${this.baseUrl}/message/list`, method: 'POST', data: filter }) + getChatMessageList(param: ChatMessageListParam): Promise> { + return request({ url: `${this.baseUrl}/message/list`, method: 'POST', data: param }) }, /** - * 准备对话会话 + * 准备流式对话会话 + * @param param chatId和message必传 */ - prepareChatMessageSession(prepareData: ChatPrepareData): Promise> { - return request({ url: `${this.baseUrl}/prepare`, method: 'POST', data: prepareData }) + prepareChatMessageSession(param: PrepareChatParam): Promise> { + return request({ url: `${this.baseUrl}/prepare`, method: 'POST', data: param }) }, /** @@ -86,17 +94,103 @@ export const workcaseChatAPI = { }, /** - * 停止对话 + * 建立SSE流式对话连接 + * @param sessionId 会话ID(必传) + * @param callbacks 回调函数 + * @returns SSETask 可用于中止请求 */ - stopChat(filter: TbChat, taskId: string): Promise> { - return request({ url: `${this.baseUrl}/stop/${taskId}`, method: 'POST', data: filter }) + streamChat(sessionId: string, callbacks: SSECallbacks): SSETask { + const url = `${BASE_URL}${this.baseUrl}/stream/${sessionId}` + const token = uni.getStorageSync('token') || '' + + const requestTask = uni.request({ + url: url, + method: 'GET', + header: { + 'Accept': 'text/event-stream', + 'Authorization': token ? `Bearer ${token}` : '' + }, + enableChunked: true, + success: (res: any) => { + console.log('SSE请求完成:', res) + // 处理非200状态码 + if (res.statusCode !== 200) { + console.error('SSE请求状态码异常:', res.statusCode) + let errorMsg = '抱歉,服务暂时不可用,请稍后重试。' + if (res.statusCode === 401) { + errorMsg = '登录已过期,请重新登录。' + } else if (res.statusCode === 403) { + errorMsg = '无权限访问,请联系管理员。' + } else if (res.statusCode === 404) { + errorMsg = '会话不存在或已过期,请重新发起对话。' + } else if (res.statusCode >= 500) { + errorMsg = '服务器异常,请稍后重试。' + } + callbacks.onError?.(errorMsg) + } + callbacks.onComplete?.() + }, + fail: (err: any) => { + console.error('SSE请求失败:', err) + callbacks.onError?.('网络连接失败,请稍后重试。') + callbacks.onComplete?.() + } + }) + + // 监听分块数据 + requestTask.onChunkReceived((res: any) => { + try { + const decoder = new TextDecoder('utf-8') + const text = decoder.decode(new Uint8Array(res.data)) + + const lines = text.split('\n') + for (const line of lines) { + if (line.startsWith('data:')) { + const dataStr = line.substring(5).trim() + if (dataStr && dataStr !== '[DONE]') { + try { + const data: SSEMessageData = JSON.parse(dataStr) + const event = data.event + + if (event === 'message' || event === 'agent_message') { + callbacks.onMessage?.(data) + } else if (event === 'message_end') { + callbacks.onEnd?.(data.task_id || '') + } else if (event === 'error') { + callbacks.onError?.(data.message || '发生错误,请稍后重试。') + } + } catch (e) { + console.log('解析SSE数据失败:', dataStr) + } + } + } + } + } catch (e) { + console.error('处理分块数据失败:', e) + } + }) + + return { + abort: () => { + requestTask.abort() + } + } }, /** - * 评论对话消息 + * 停止对话 + * @param param taskId, agentId, userId必传 */ - commentChatMessage(filter: TbChat, messageId: string, comment: string): Promise> { - return request({ url: `${this.baseUrl}/comment?messageId=${messageId}&comment=${comment}`, method: 'POST', data: filter }) + stopChat(param: StopChatParam): Promise> { + return request({ url: `${this.baseUrl}/stop/${param.taskId}`, method: 'POST', data: param }) + }, + + /** + * 评价对话消息 + * @param param agentId, chatId, messageId, comment, userId必传 + */ + commentChatMessage(param: CommentMessageParam): Promise> { + return request({ url: `${this.baseUrl}/comment?messageId=${param.messageId}&comment=${param.comment}`, method: 'POST', data: param }) }, // ====================== 对话分析 ====================== diff --git a/urbanLifelineWeb/packages/workcase_wechat/config/index.ts b/urbanLifelineWeb/packages/workcase_wechat/config/index.ts new file mode 100644 index 00000000..131fcdd6 --- /dev/null +++ b/urbanLifelineWeb/packages/workcase_wechat/config/index.ts @@ -0,0 +1 @@ +export const AGENT_ID = '17664699513920001' \ No newline at end of file diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.scss b/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.scss index 901602f4..5fe64cd8 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.scss +++ b/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.scss @@ -308,6 +308,10 @@ background: #FFFFFF; border: 1px solid #E5E5E5; border-radius: 12px; + min-height: 40px; + box-sizing: border-box; + display: flex; + align-items: center; } .user-bubble .message-text { @@ -460,3 +464,43 @@ font-size: 18px; color: #4b87ff; } + +// 打字指示器动画 +.typing-indicator { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + padding: 8px 4px; +} + +.typing-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #999; + animation: typing-bounce 1.4s infinite ease-in-out both; +} + +.typing-dot:nth-child(1) { + animation-delay: -0.32s; +} + +.typing-dot:nth-child(2) { + animation-delay: -0.16s; +} + +.typing-dot:nth-child(3) { + animation-delay: 0s; +} + +@keyframes typing-bounce { + 0%, 80%, 100% { + transform: scale(0.6); + opacity: 0.5; + } + 40% { + transform: scale(1); + opacity: 1; + } +} diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.uvue b/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.uvue index cd96c67d..a1f34557 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.uvue +++ b/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.uvue @@ -65,7 +65,13 @@ AI - {{item.content}} + + + + + + + {{item.content}} {{item.time}} @@ -112,9 +118,9 @@