This commit is contained in:
2025-12-23 15:57:11 +08:00
parent 33a16342d3
commit 68daf391af
23 changed files with 608 additions and 523 deletions

View File

@@ -17,9 +17,10 @@ import org.xyzh.api.ai.dto.DifyFileInfo;
public class ChatRequest { public class ChatRequest {
/** /**
* 输入变量 * 输入变量Dify API 必需字段)
*/ */
private Map<String, Object> inputs; @JSONField(serializeFeatures = com.alibaba.fastjson2.JSONWriter.Feature.WriteMapNullValue)
private Map<String, Object> inputs = new java.util.HashMap<>();
/** /**
* 用户问题 * 用户问题

View File

@@ -66,7 +66,7 @@ public class ChatController {
chat.setUserType(false); chat.setUserType(false);
if(NonUtils.isNotEmpty(token)){ if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin(); LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain)) { if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
chat.setUserType(true); chat.setUserType(true);
} }
} }
@@ -95,7 +95,7 @@ public class ChatController {
chat.setUserType(false); chat.setUserType(false);
if(NonUtils.isNotEmpty(token)){ if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin(); LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain)) { if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
chat.setUserType(true); chat.setUserType(true);
} }
} }
@@ -114,7 +114,7 @@ public class ChatController {
chat.setUserType(false); chat.setUserType(false);
if(NonUtils.isNotEmpty(token)){ if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin(); LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain)) { if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
chat.setUserType(true); chat.setUserType(true);
} }
} }
@@ -135,7 +135,7 @@ public class ChatController {
filter.setUserType(false); filter.setUserType(false);
if(NonUtils.isNotEmpty(token)){ if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin(); LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain)) { if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
filter.setUserType(true); filter.setUserType(true);
} }
} }
@@ -161,7 +161,7 @@ public class ChatController {
filter.setUserType(false); filter.setUserType(false);
if(NonUtils.isNotEmpty(token)){ if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin(); LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain)) { if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
filter.setUserType(true); filter.setUserType(true);
} }
} }
@@ -192,7 +192,7 @@ public class ChatController {
chatPrepareData.setUserType(false); chatPrepareData.setUserType(false);
if(NonUtils.isNotEmpty(token)){ if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin(); LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain)) { if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
chatPrepareData.setUserType(true); chatPrepareData.setUserType(true);
} }
} }
@@ -245,7 +245,7 @@ public class ChatController {
filter.setUserType(false); filter.setUserType(false);
if(NonUtils.isNotEmpty(token)){ if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin(); LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain)) { if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
filter.setUserType(true); filter.setUserType(true);
} }
} }
@@ -278,7 +278,7 @@ public class ChatController {
filter.setUserType(false); filter.setUserType(false);
if(NonUtils.isNotEmpty(token)){ if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin(); LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain)) { if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
filter.setUserType(true); filter.setUserType(true);
} }
} }

View File

@@ -326,6 +326,7 @@ public class AgentChatServiceImpl implements AgentChatService {
chatRequest.setQuery(query); chatRequest.setQuery(query);
chatRequest.setUser(userId); chatRequest.setUser(userId);
chatRequest.setResponseMode("streaming"); chatRequest.setResponseMode("streaming");
chatRequest.setInputs(new HashMap<>()); // Dify API 要求 inputs 必传
if (filesData != null && !filesData.isEmpty()) { if (filesData != null && !filesData.isEmpty()) {
chatRequest.setFiles(filesData); chatRequest.setFiles(filesData);

View File

@@ -17,7 +17,7 @@ auth:
- /error - /error
- /actuator/health - /actuator/health
- /actuator/info - /actuator/info
- /ai/chat/* # AI对话有非系统用户对话的接口无登录状态 - /ai/chat/** # AI对话有非系统用户对话的接口无登录状态
security: security:
aes: aes:

View File

@@ -17,7 +17,7 @@ auth:
- /error - /error
- /actuator/health - /actuator/health
- /actuator/info - /actuator/info
- /ai/chat/* # AI对话有非系统用户对话的接口无登录状态 - /ai/chat/** # AI对话有非系统用户对话的接口无登录状态
security: security:
aes: aes:

View File

@@ -1,11 +1,6 @@
package org.xyzh.api.workcase.service; 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.TbWordCloudDTO;
import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageRequest; import org.xyzh.common.core.page.PageRequest;
@@ -18,85 +13,6 @@ import org.xyzh.common.core.page.PageRequest;
*/ */
public interface WorkcaseChatService { public interface WorkcaseChatService {
// ========================= 聊天管理 ==========================
/**
* @description 来客创建聊天对话
* @param
* @author yslg
* @since 2025-12-18
*/
ResultDomain<TbChat> createChat(TbChat chat);
/**
* @description 更新聊天名称
* @param
* @author yslg
* @since 2025-12-18
*/
ResultDomain<TbChat> updateChat(TbChat chat);
/**
* 获取聊天列表
* @param agentId 智能体ID
* @return 聊天列表
*/
ResultDomain<TbChat> getChatList(TbChat filter);
// ========================= 聊天信息管理 ======================
/**
* 获取会话消息列表
* @param filter 会话过滤条件包含agentId, chatId, userId, userType
* @return 会话消息列表
*/
ResultDomain<TbChatMessage> getChatMessageList(TbChat filter);
// 用户转人工后,就不和智能体聊天了,在微信客服里聊天
/**
* 准备聊天数据POST传递复杂参数
* @param prepareData 对话准备数据包含agentId, chatId, query, files, userId, userType
* @return ResultDomain<String> 返回sessionId
*/
ResultDomain<String> 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<Boolean> stopChatMessageByTaskId(TbChat filter, String taskId);
/**
* 评价
* @param filter 会话过滤条件包含agentId, chatId, userId, userType
* @param messageId 消息ID
* @param comment 评价
* @return 评价结果
*/
ResultDomain<Boolean> commentChatMessage(TbChat filter, String messageId, String comment);
// =============================== 对话分析 ==========================
/**
* 对话分析, 提取出工单相关的内容,这里有智能体调用等
* @param chatId 对话ID
* @return 对话分析结果
*/
ResultDomain<TbWorkcaseDTO> analyzeChat(String chatId);
// 对话总结
ResultDomain<TbWorkcaseDTO> summaryChat(String chatId);
// =============================== 对话、工单等词云管理 ========================== // =============================== 对话、工单等词云管理 ==========================
/** /**

View File

@@ -178,6 +178,7 @@ public class GuestController {
// 3. 来客不存在,创建新来客 // 3. 来客不存在,创建新来客
if (guest == null) { if (guest == null) {
TbGuestDTO newGuest = new TbGuestDTO(); TbGuestDTO newGuest = new TbGuestDTO();
newGuest.setOptsn(IdUtil.getOptsn());
newGuest.setUserId(IdUtil.generateID()); newGuest.setUserId(IdUtil.generateID());
newGuest.setWechatId(loginParam.getWechatId()); newGuest.setWechatId(loginParam.getWechatId());
newGuest.setPhone(loginParam.getPhone()); newGuest.setPhone(loginParam.getPhone());

View File

@@ -13,6 +13,7 @@ import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.core.page.PageRequest; import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.dto.sys.TbGuestDTO; import org.xyzh.common.dto.sys.TbGuestDTO;
import org.xyzh.common.dto.sys.TbSysUserRoleDTO; 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.TbGuestMapper;
import org.xyzh.system.mapper.user.TbSysUserRoleMapper; import org.xyzh.system.mapper.user.TbSysUserRoleMapper;
@@ -48,6 +49,7 @@ public class GuestServiceImpl implements GuestService{
// 绑定访客角色(role_guest) // 绑定访客角色(role_guest)
TbSysUserRoleDTO userRole = new TbSysUserRoleDTO(); TbSysUserRoleDTO userRole = new TbSysUserRoleDTO();
userRole.setOptsn(IdUtil.getOptsn());
userRole.setUserId(guest.getUserId()); userRole.setUserId(guest.getUserId());
userRole.setRoleId("role_guest"); userRole.setRoleId("role_guest");
userRole.setDeptId("dept_root"); userRole.setDeptId("dept_root");

View File

@@ -224,12 +224,15 @@ public class SysUserServiceImpl implements SysUserService {
@Override @Override
public ResultDomain<SysUserVO> getLoginUser(SysUserVO filter) { public ResultDomain<SysUserVO> getLoginUser(SysUserVO filter) {
// 登录查询语义与 getUser 相同(可根据用户名/手机号/邮箱查询) // 登录查询语义与 getUser 相同(可根据用户名/手机号/邮箱/wechatId查询)
if(NonUtils.isNotNull(filter.getPhone())){ if(NonUtils.isNotNull(filter.getPhone())){
filter.setPhone(filter.getPhone()); filter.setPhone(filter.getPhone());
} }
SysUserVO userVO = userMapper.getUserByFilter(filter).get(0); List<SysUserVO> list = userMapper.getUserByFilter(filter);
return ResultDomain.success("查询成功", userVO); if (list == null || list.isEmpty()) {
return ResultDomain.failure("用户不存在");
}
return ResultDomain.success("查询成功", list.get(0));
} }
@Override @Override

View File

@@ -172,6 +172,9 @@
<if test="filter.username !=null and filter.username !=''"> <if test="filter.username !=null and filter.username !=''">
AND ui.username = #{filter.username} AND ui.username = #{filter.username}
</if> </if>
<if test="filter.wechatId !=null and filter.wechatId !=''">
AND u.wechat_id = #{filter.wechatId}
</if>
<!-- username / userType / deptPath 在表中不存在,按 SQL 为准移除相关条件 --> <!-- username / userType / deptPath 在表中不存在,按 SQL 为准移除相关条件 -->
AND (u.deleted IS NULL OR u.deleted = false) AND (u.deleted IS NULL OR u.deleted = false)
</where> </where>

View File

@@ -7,6 +7,7 @@
<!-- 用户角色关系字段 --> <!-- 用户角色关系字段 -->
<id column="user_id" property="userId" jdbcType="VARCHAR"/> <id column="user_id" property="userId" jdbcType="VARCHAR"/>
<id column="role_id" property="roleId" jdbcType="VARCHAR"/> <id column="role_id" property="roleId" jdbcType="VARCHAR"/>
<id column="dept_id" property="deptId" jdbcType="VARCHAR"/>
<!-- 基础字段 --> <!-- 基础字段 -->
<result column="optsn" property="optsn" jdbcType="VARCHAR"/> <result column="optsn" property="optsn" jdbcType="VARCHAR"/>
<result column="creator" property="creator" jdbcType="VARCHAR"/> <result column="creator" property="creator" jdbcType="VARCHAR"/>
@@ -62,7 +63,7 @@
<!-- 基础列 --> <!-- 基础列 -->
<sql id="Base_Column_List"> <sql id="Base_Column_List">
user_id, role_id, user_id, role_id, dept_id,
optsn, creator, updater, dept_path, remark, create_time, update_time, delete_time, deleted optsn, creator, updater, dept_path, remark, create_time, update_time, delete_time, deleted
</sql> </sql>
@@ -73,6 +74,7 @@
<!-- 必填字段user_id, role_id, optsn --> <!-- 必填字段user_id, role_id, optsn -->
user_id, user_id,
role_id, role_id,
dept_id,
optsn, optsn,
<!-- 可选字段:基础字段按是否有值动态拼接 --> <!-- 可选字段:基础字段按是否有值动态拼接 -->
<if test="creator != null and creator != ''">creator,</if> <if test="creator != null and creator != ''">creator,</if>
@@ -88,6 +90,7 @@
<!-- 必填字段值 --> <!-- 必填字段值 -->
#{userId}, #{userId},
#{roleId}, #{roleId},
#{deptId},
#{optsn}, #{optsn},
<!-- 可选字段值 --> <!-- 可选字段值 -->
<if test="creator != null and creator != ''">#{creator},</if> <if test="creator != null and creator != ''">#{creator},</if>

View File

@@ -9,7 +9,9 @@ import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner; import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.dto.TbKnowledge;
import org.xyzh.api.ai.service.AgentService;
import org.xyzh.api.ai.service.KnowledgeService; import org.xyzh.api.ai.service.KnowledgeService;
import org.xyzh.api.system.service.SysConfigService; import org.xyzh.api.system.service.SysConfigService;
import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.core.domain.ResultDomain;
@@ -23,8 +25,8 @@ import org.xyzh.common.utils.id.IdUtil;
* @since 2025-12-18 * @since 2025-12-18
*/ */
@Configuration @Configuration
public class KnowledgeInit { public class AiInit {
private static final Logger logger = LoggerFactory.getLogger(KnowledgeInit.class); private static final Logger logger = LoggerFactory.getLogger(AiInit.class);
private static final String SERVICE_WORKCASE = "workcase"; private static final String SERVICE_WORKCASE = "workcase";
private static final String CATEGORY_INTERNAL = "internal"; 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) @DubboReference(version = "1.0.0", group = "system", timeout = 30000, retries = 0)
private SysConfigService sysConfigService; private SysConfigService sysConfigService;
@DubboReference(version = "1.0.0", group = "ai", timeout = 30000, retries = 0)
private AgentService agentService;
@Bean @Bean
public CommandLineRunner knowledgeInitRunner() { public CommandLineRunner knowledgeInitRunner() {
return args -> { 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<TbAgent> 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<TbAgent> resultDomain = agentService.addAgent(agent);
if(resultDomain.getSuccess()){
logger.info("泰豪小电智能体初始化成功");
}else{
logger.error("泰豪小电智能体初始化失败"+resultDomain.getMessage());
}
};
}
/** /**
* 构建8个知识库配置 * 构建8个知识库配置
*/ */

View File

@@ -2,6 +2,7 @@ package org.xyzh.workcase.controller;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize; 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.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; 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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; 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.TbChatRoomDTO;
import org.xyzh.api.workcase.dto.TbChatRoomMemberDTO; import org.xyzh.api.workcase.dto.TbChatRoomMemberDTO;
import org.xyzh.api.workcase.dto.TbChatRoomMessageDTO; import org.xyzh.api.workcase.dto.TbChatRoomMessageDTO;
import org.xyzh.api.workcase.dto.TbCustomerServiceDTO; import org.xyzh.api.workcase.dto.TbCustomerServiceDTO;
import org.xyzh.api.workcase.dto.TbWordCloudDTO; 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.ChatRoomService;
import org.xyzh.api.workcase.service.WorkcaseChatService; import org.xyzh.api.workcase.service.WorkcaseChatService;
import org.xyzh.api.workcase.vo.ChatMemberVO; import org.xyzh.api.workcase.vo.ChatMemberVO;
@@ -49,6 +45,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
* @since 2025-12-19 * @since 2025-12-19
*/ */
@Tag(name = "工单对话") @Tag(name = "工单对话")
@Validated
@RestController @RestController
@RequestMapping("/workcase/chat") @RequestMapping("/workcase/chat")
public class WorkcaseChatContorller { public class WorkcaseChatContorller {
@@ -59,106 +56,6 @@ public class WorkcaseChatContorller {
@Autowired @Autowired
private ChatRoomService chatRoomService; private ChatRoomService chatRoomService;
// ========================= AI对话管理 =========================
@Operation(summary = "创建对话")
@PreAuthorize("hasAuthority('workcase:chat:create')")
@PostMapping
public ResultDomain<TbChat> 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<TbChat> 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<TbChat> getChatList(@RequestBody TbChat filter) {
return workcaseChatService.getChatList(filter);
}
@Operation(summary = "获取对话消息列表")
@PreAuthorize("hasAuthority('workcase:chat:message')")
@PostMapping("/message/list")
public ResultDomain<TbChatMessage> 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<String> 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<Boolean> stopChat(@RequestBody TbChat filter, @PathVariable String taskId) {
return workcaseChatService.stopChatMessageByTaskId(filter, taskId);
}
@Operation(summary = "评论对话消息")
@PreAuthorize("hasAuthority('workcase:chat:message')")
@PostMapping("/comment")
public ResultDomain<Boolean> 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<TbWorkcaseDTO> analyzeChat(@PathVariable String chatId) {
return workcaseChatService.analyzeChat(chatId);
}
@Operation(summary = "总结对话")
@PreAuthorize("hasAuthority('workcase:chat:analyze')")
@PostMapping("/summary/{chatId}")
public ResultDomain<TbWorkcaseDTO> summaryChat(@PathVariable String chatId) {
return workcaseChatService.summaryChat(chatId);
}
// ========================= ChatRoom聊天室管理实时IM ========================= // ========================= ChatRoom聊天室管理实时IM =========================
@Operation(summary = "创建聊天室(转人工时调用)") @Operation(summary = "创建聊天室(转人工时调用)")

View File

@@ -4,24 +4,16 @@ import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.config.annotation.DubboService; import org.apache.dubbo.config.annotation.DubboService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; 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.TbWordCloudDTO;
import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
import org.xyzh.api.workcase.service.WorkcaseChatService; import org.xyzh.api.workcase.service.WorkcaseChatService;
import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageDomain; import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.core.page.PageParam; import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.core.page.PageRequest; import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.redis.service.RedisService;
import org.xyzh.common.utils.id.IdUtil; import org.xyzh.common.utils.id.IdUtil;
import org.xyzh.workcase.mapper.TbWordCloudMapper; 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 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 @Autowired
private TbWordCloudMapper wordCloudMapper; private TbWordCloudMapper wordCloudMapper;
@Autowired
private RedisService redisService;
// ========================= 聊天管理 ==========================
@Override
public ResultDomain<TbChat> createChat(TbChat chat) {
logger.info("创建对话: userId={}", chat.getUserId());
ResultDomain<TbChat> 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<TbChat> updateChat(TbChat chat) {
logger.info("更新对话: chatId={}", chat.getChatId());
return agentChatService.updateChat(chat);
}
@Override
public ResultDomain<TbChat> getChatList(TbChat filter) {
return agentChatService.getChatList(filter);
}
// ========================= 聊天信息管理 ======================
@Override
public ResultDomain<TbChatMessage> getChatMessageList(TbChat filter) {
return agentChatService.getChatMessageList(filter);
}
@Override
public ResultDomain<String> 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<String> 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<Boolean> stopChatMessageByTaskId(TbChat filter, String taskId) {
return agentChatService.stopChatMessageByTaskId(filter, taskId);
}
@Override
public ResultDomain<Boolean> commentChatMessage(TbChat filter, String messageId, String comment) {
return agentChatService.commentChatMessage(filter, messageId, comment);
}
// =============================== 对话分析 ==========================
@Override
public ResultDomain<TbWorkcaseDTO> analyzeChat(String chatId) {
logger.info("分析对话内容,生成工单预填信息: chatId={}", chatId);
TbChat filter = new TbChat();
filter.setChatId(chatId);
ResultDomain<TbChatMessage> msgResult = agentChatService.getChatMessageList(filter);
if (!msgResult.getSuccess() || msgResult.getDataList() == null || msgResult.getDataList().isEmpty()) {
return ResultDomain.failure("获取对话消息失败或消息为空");
}
List<TbChatMessage> messages = msgResult.getDataList();
// ============== 伪代码调用AI分析对话自动生成工单预填信息 ==============
// 步骤5AI根据聊天对话自动生成部分工单信息预填入小程序的工单创建表单
//
// 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<TbChatMessage> 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<TbWorkcaseDTO> summaryChat(String chatId) {
logger.info("总结对话: chatId={}", chatId);
TbChat filter = new TbChat();
filter.setChatId(chatId);
ResultDomain<TbChatMessage> msgResult = agentChatService.getChatMessageList(filter);
if (!msgResult.getSuccess() || msgResult.getDataList() == null) {
return ResultDomain.failure("获取对话消息失败");
}
List<TbChatMessage> messages = msgResult.getDataList();
// TODO: 调用AI进行对话总结提取关键词更新词云
// 伪代码:
// String summary = aiService.summarize(messages);
// List<String> keywords = aiService.extractKeywords(messages);
// updateWordCloud(keywords);
extractAndUpdateWordCloud(messages);
TbWorkcaseDTO summary = new TbWorkcaseDTO();
logger.info("对话总结完成: chatId={}", chatId);
return ResultDomain.success("对话总结完成", summary);
}
private void extractAndUpdateWordCloud(List<TbChatMessage> messages) {
// TODO: 调用AI提取关键词
// 伪代码List<String> 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 @Override

View File

@@ -9,7 +9,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.xyzh.api.workcase.dto.TbWorkcaseDTO; import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
import org.xyzh.api.workcase.dto.TbWorkcaseDeviceDTO; import org.xyzh.api.workcase.dto.TbWorkcaseDeviceDTO;
import org.xyzh.api.workcase.dto.TbWorkcaseProcessDTO; import org.xyzh.api.workcase.dto.TbWorkcaseProcessDTO;
import org.xyzh.api.workcase.service.WorkcaseChatService;
import org.xyzh.api.workcase.service.WorkcaseService; import org.xyzh.api.workcase.service.WorkcaseService;
import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageDomain; 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.TbWorkcaseMapper;
import org.xyzh.workcase.mapper.TbWorkcaseProcessMapper; import org.xyzh.workcase.mapper.TbWorkcaseProcessMapper;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
@DubboService(version = "1.0.0",group = "workcase",timeout = 30000,retries = 0) @DubboService(version = "1.0.0",group = "workcase",timeout = 30000,retries = 0)
@@ -37,9 +35,6 @@ public class WorkcaseServiceImpl implements WorkcaseService {
@Autowired @Autowired
private TbWorkcaseDeviceMapper workcaseDeviceMapper; private TbWorkcaseDeviceMapper workcaseDeviceMapper;
@Autowired
private WorkcaseChatService workcaseChatService;
// ====================== 工单管理 ====================== // ====================== 工单管理 ======================
@Override @Override
@@ -103,11 +98,9 @@ public class WorkcaseServiceImpl implements WorkcaseService {
if ("done".equals(workcase.getStatus())) { if ("done".equals(workcase.getStatus())) {
process.setAction(WorkcaseProcessAction.FINISH.getName()); process.setAction(WorkcaseProcessAction.FINISH.getName());
process.setMessage("工单完成"); process.setMessage("工单完成");
workcaseChatService.summaryChat(existing.getWorkcaseId());
} else if ("cancelled".equals(workcase.getStatus())) { } else if ("cancelled".equals(workcase.getStatus())) {
process.setAction(WorkcaseProcessAction.REPEAL.getName()); process.setAction(WorkcaseProcessAction.REPEAL.getName());
process.setMessage("工单撤销"); process.setMessage("工单撤销");
workcaseChatService.summaryChat(existing.getWorkcaseId());
} else { } else {
process.setAction(WorkcaseProcessAction.INFO.getName()); process.setAction(WorkcaseProcessAction.INFO.getName());
process.setMessage("状态变更: " + oldStatus + " -> " + workcase.getStatus()); process.setMessage("状态变更: " + oldStatus + " -> " + workcase.getStatus());
@@ -309,13 +302,11 @@ public class WorkcaseServiceImpl implements WorkcaseService {
workcase.setWorkcaseId(workcaseProcess.getWorkcaseId()); workcase.setWorkcaseId(workcaseProcess.getWorkcaseId());
workcase.setStatus("done"); workcase.setStatus("done");
workcaseMapper.updateWorkcase(workcase); workcaseMapper.updateWorkcase(workcase);
workcaseChatService.summaryChat(workcaseProcess.getWorkcaseId());
} else if (WorkcaseProcessAction.REPEAL.getName().equals(action)) { } else if (WorkcaseProcessAction.REPEAL.getName().equals(action)) {
TbWorkcaseDTO workcase = new TbWorkcaseDTO(); TbWorkcaseDTO workcase = new TbWorkcaseDTO();
workcase.setWorkcaseId(workcaseProcess.getWorkcaseId()); workcase.setWorkcaseId(workcaseProcess.getWorkcaseId());
workcase.setStatus("cancelled"); workcase.setStatus("cancelled");
workcaseMapper.updateWorkcase(workcase); workcaseMapper.updateWorkcase(workcase);
workcaseChatService.summaryChat(workcaseProcess.getWorkcaseId());
} }
int rows = workcaseProcessMapper.insertWorkcaseProcess(workcaseProcess); int rows = workcaseProcessMapper.insertWorkcaseProcess(workcaseProcess);

View File

@@ -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<ResultDomain<TbChat>> {
return request<TbChat>({ url: `${this.baseUrl}/conversation`, method: 'POST', data: param })
},
/**
* 更新对话
* @param chat agentId、userId、title、userType 必传
*/
updateChat(chat: TbChat): Promise<ResultDomain<TbChat>> {
return request<TbChat>({ url: `${this.baseUrl}/conversation`, method: 'PUT', data: chat })
},
/**
* 查询对话列表
* @param param agentId 必传
*/
getChatList(param: ChatListParam): Promise<ResultDomain<TbChat[]>> {
return request<TbChat[]>({ url: `${this.baseUrl}/conversations`, method: 'GET', data: param })
},
/**
* 获取对话消息列表
* @param param agentId、chatId、userId 必传
*/
getChatMessageList(param: ChatMessageListParam): Promise<ResultDomain<TbChatMessage[]>> {
return request<TbChatMessage[]>({ url: `${this.baseUrl}/messages`, method: 'POST', data: param })
},
/**
* 准备流式对话会话
* @param param agentId、chatId、query、userId 必传
*/
prepareChatMessageSession(param: PrepareChatParam): Promise<ResultDomain<string>> {
return request<string>({ 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<ResultDomain<boolean>> {
return request<boolean>({ url: `${this.baseUrl}/stop`, method: 'POST', data: param })
},
/**
* 评价对话消息
* @param param agentId、chatId、messageId、comment、userId 必传
*/
commentChatMessage(param: CommentMessageParam): Promise<ResultDomain<boolean>> {
return request<boolean>({ url: `${this.baseUrl}/comment`, method: 'POST', data: param })
}
}

View File

@@ -0,0 +1 @@
export { aiChatAPI } from './aiChat'

View File

@@ -1,3 +1,4 @@
export * from "./base" export * from "./base"
export * from "./sys" export * from "./sys"
export * from "./workcase" export * from "./workcase"
export * from "./ai"

View File

@@ -11,26 +11,30 @@ import type {
ChatMemberVO, ChatMemberVO,
ChatRoomMessageVO, ChatRoomMessageVO,
CustomerServiceVO 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对话相关类型简化版 /* eslint-disable @typescript-eslint/no-explicit-any */
interface TbChat { declare const uni: {
chatId?: string getStorageSync: (key: string) => any
userId?: string request: (options: any) => any
title?: string
status?: string
}
interface TbChatMessage {
messageId?: string
chatId?: string
content?: string
role?: string
}
interface ChatPrepareData {
chatId?: string
message?: string
} }
// API 基础配置
const BASE_URL = 'http://localhost:8180'
/** /**
* @description 工单对话相关接口 * @description 工单对话相关接口
* @filename workcaseChat.ts * @filename workcaseChat.ts
@@ -45,9 +49,10 @@ export const workcaseChatAPI = {
/** /**
* 创建对话 * 创建对话
* @param param agentId和userId必传
*/ */
createChat(chat: TbChat): Promise<ResultDomain<TbChat>> { createChat(param: CreateChatParam): Promise<ResultDomain<TbChat>> {
return request<TbChat>({ url: this.baseUrl, method: 'POST', data: chat }) return request<TbChat>({ url: this.baseUrl, method: 'POST', data: param })
}, },
/** /**
@@ -59,23 +64,26 @@ export const workcaseChatAPI = {
/** /**
* 查询对话列表 * 查询对话列表
* @param param userId必传
*/ */
getChatList(filter: TbChat): Promise<ResultDomain<TbChat>> { getChatList(param: ChatListParam): Promise<ResultDomain<TbChat[]>> {
return request<TbChat>({ url: `${this.baseUrl}/list`, method: 'POST', data: filter }) return request<TbChat[]>({ url: `${this.baseUrl}/list`, method: 'POST', data: param })
}, },
/** /**
* 获取对话消息列表 * 获取对话消息列表
* @param param chatId必传
*/ */
getChatMessageList(filter: TbChat): Promise<ResultDomain<TbChatMessage>> { getChatMessageList(param: ChatMessageListParam): Promise<ResultDomain<TbChatMessage[]>> {
return request<TbChatMessage>({ url: `${this.baseUrl}/message/list`, method: 'POST', data: filter }) return request<TbChatMessage[]>({ url: `${this.baseUrl}/message/list`, method: 'POST', data: param })
}, },
/** /**
* 准备对话会话 * 准备流式对话会话
* @param param chatId和message必传
*/ */
prepareChatMessageSession(prepareData: ChatPrepareData): Promise<ResultDomain<string>> { prepareChatMessageSession(param: PrepareChatParam): Promise<ResultDomain<string>> {
return request<string>({ url: `${this.baseUrl}/prepare`, method: 'POST', data: prepareData }) return request<string>({ 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<ResultDomain<boolean>> { streamChat(sessionId: string, callbacks: SSECallbacks): SSETask {
return request<boolean>({ url: `${this.baseUrl}/stop/${taskId}`, method: 'POST', data: filter }) 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<ResultDomain<boolean>> { stopChat(param: StopChatParam): Promise<ResultDomain<boolean>> {
return request<boolean>({ url: `${this.baseUrl}/comment?messageId=${messageId}&comment=${comment}`, method: 'POST', data: filter }) return request<boolean>({ url: `${this.baseUrl}/stop/${param.taskId}`, method: 'POST', data: param })
},
/**
* 评价对话消息
* @param param agentId, chatId, messageId, comment, userId必传
*/
commentChatMessage(param: CommentMessageParam): Promise<ResultDomain<boolean>> {
return request<boolean>({ url: `${this.baseUrl}/comment?messageId=${param.messageId}&comment=${param.comment}`, method: 'POST', data: param })
}, },
// ====================== 对话分析 ====================== // ====================== 对话分析 ======================

View File

@@ -0,0 +1 @@
export const AGENT_ID = '17664699513920001'

View File

@@ -308,6 +308,10 @@
background: #FFFFFF; background: #FFFFFF;
border: 1px solid #E5E5E5; border: 1px solid #E5E5E5;
border-radius: 12px; border-radius: 12px;
min-height: 40px;
box-sizing: border-box;
display: flex;
align-items: center;
} }
.user-bubble .message-text { .user-bubble .message-text {
@@ -460,3 +464,43 @@
font-size: 18px; font-size: 18px;
color: #4b87ff; 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;
}
}

View File

@@ -65,7 +65,13 @@
<text class="avatar-text">AI</text> <text class="avatar-text">AI</text>
</view> </view>
<view class="message-bubble bot-bubble"> <view class="message-bubble bot-bubble">
<text class="message-text">{{item.content}}</text> <!-- 加载动画:内容为空时显示 -->
<view class="typing-indicator" v-if="!item.content && isTyping">
<view class="typing-dot"></view>
<view class="typing-dot"></view>
<view class="typing-dot"></view>
</view>
<text class="message-text" v-else>{{item.content}}</text>
</view> </view>
</view> </view>
<text class="message-time">{{item.time}}</text> <text class="message-time">{{item.time}}</text>
@@ -112,9 +118,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue' import { ref, nextTick, onMounted } from 'vue'
import WorkcaseCreator from '@/components/WorkcaseCreator/WorkcaseCreator.uvue' import WorkcaseCreator from '@/components/WorkcaseCreator/WorkcaseCreator.uvue'
import { guestAPI, workcaseChatAPI } from '@/api' import { guestAPI, aiChatAPI } from '@/api'
import type { TbWorkcaseDTO } from '@/types' import type { TbWorkcaseDTO } from '@/types'
import { AGENT_ID } from '@/config'
// 前端消息展示类型 // 前端消息展示类型
interface ChatMessageItem { interface ChatMessageItem {
type: 'user' | 'bot' type: 'user' | 'bot'
@@ -122,7 +128,7 @@
time: string time: string
actions?: string[] | null actions?: string[] | null
} }
const agentId = AGENT_ID
// 响应式数据 // 响应式数据
const messages = ref<ChatMessageItem[]>([]) const messages = ref<ChatMessageItem[]>([])
const inputText = ref<string>('') const inputText = ref<string>('')
@@ -141,7 +147,7 @@
userId: '' userId: ''
}) })
const isMockMode = ref(true) // 开发环境mock模式 const isMockMode = ref(true) // 开发环境mock模式
const userType = ref(false)
// AI 对话相关 // AI 对话相关
const chatId = ref<string>('') // 当前会话ID const chatId = ref<string>('') // 当前会话ID
const currentTaskId = ref<string>('') // 当前任务ID用于停止 const currentTaskId = ref<string>('') // 当前任务ID用于停止
@@ -161,9 +167,9 @@
// 开发环境使用mock数据 // 开发环境使用mock数据
if (isMockMode.value) { if (isMockMode.value) {
userInfo.value = { userInfo.value = {
wechatId: '17857100375', wechatId: '17857100377',
username: '测试用户', username: '访客用户',
phone: '17857100375', phone: '17857100377',
userId: '' userId: ''
} }
await doIdentify() await doIdentify()
@@ -173,12 +179,12 @@
// 切换mock用户开发调试用 // 切换mock用户开发调试用
function switchMockUser() { function switchMockUser() {
uni.showActionSheet({ uni.showActionSheet({
itemList: ['员工 (17857100375)', '访客 (17857100376)'], itemList: ['员工 (17857100375)', '访客 (17857100377)'],
success: (res) => { success: (res) => {
if (res.tapIndex === 0) { if (res.tapIndex === 0) {
userInfo.value = { wechatId: '17857100375', username: '员工用户', phone: '17857100375', userId: '' } userInfo.value = { wechatId: '17857100375', username: '员工用户', phone: '17857100375', userId: '' }
} else { } else {
userInfo.value = { wechatId: '17857100376', username: '访客用户', phone: '17857100376', userId: '' } userInfo.value = { wechatId: '17857100377', username: '访客用户', phone: '17857100377', userId: '' }
} }
doIdentify() doIdentify()
} }
@@ -198,10 +204,16 @@
const loginDomain = res.data const loginDomain = res.data
uni.setStorageSync('token', loginDomain.token || '') uni.setStorageSync('token', loginDomain.token || '')
uni.setStorageSync('userInfo', JSON.stringify(loginDomain.user)) uni.setStorageSync('userInfo', JSON.stringify(loginDomain.user))
uni.setStorageSync('loginDomain', JSON.stringify(loginDomain))
uni.setStorageSync('wechatId', userInfo.value.wechatId) uni.setStorageSync('wechatId', userInfo.value.wechatId)
userInfo.value.userId = loginDomain.user?.userId || '' userInfo.value.userId = loginDomain.user?.userId || ''
console.log('identify成功:', loginDomain) console.log('identify成功:', loginDomain)
uni.showToast({ title: '登录成功', icon: 'success' }) uni.showToast({ title: '登录成功', icon: 'success' })
if(loginDomain.user.status == 'guest') {
userType.value = false
} else {
userType.value = true
}
} else { } else {
console.error('identify失败:', res.message) console.error('identify失败:', res.message)
} }
@@ -275,9 +287,11 @@
try { try {
// 如果没有会话ID先创建会话 // 如果没有会话ID先创建会话
if (!chatId.value) { if (!chatId.value) {
const createRes = await workcaseChatAPI.createChat({ const createRes = await aiChatAPI.createChat({
title: '智能助手对话', title: '智能助手对话',
userId: userInfo.value.userId || userInfo.value.wechatId userId: userInfo.value.userId || userInfo.value.wechatId,
agentId: agentId,
userType: userType.value
}) })
if (createRes.success && createRes.data) { if (createRes.success && createRes.data) {
chatId.value = createRes.data.chatId || '' chatId.value = createRes.data.chatId || ''
@@ -288,22 +302,24 @@
} }
// 准备流式对话 // 准备流式对话
const prepareRes = await workcaseChatAPI.prepareChatMessageSession({ const prepareRes = await aiChatAPI.prepareChatMessageSession({
chatId: chatId.value, chatId: chatId.value,
message: query query: query,
agentId: agentId,
userType: userType.value,
userId: userInfo.value.userId
}) })
if (!prepareRes.success || !prepareRes.data) { if (!prepareRes.success || !prepareRes.data) {
throw new Error(prepareRes.message || '准备对话失败') throw new Error(prepareRes.message || '准备对话失败')
} }
const sessionId = prepareRes.data const sessionId = prepareRes.data
console.log('准备流式对话成功:', sessionId) console.log('准备流式对话成功:', sessionId)
// 添加空的AI消息占位 // 添加空的AI消息占位
const messageIndex = messages.value.length const messageIndex = messages.value.length
addMessage('bot', '') addMessage('bot', '')
// 建立SSE连接 // 建立SSE连接
streamChat(sessionId, messageIndex) startStreamChat(sessionId, messageIndex)
} catch (error : any) { } catch (error : any) {
console.error('AI聊天失败:', error) console.error('AI聊天失败:', error)
isTyping.value = false isTyping.value = false
@@ -312,66 +328,30 @@
} }
// SSE 流式对话 // SSE 流式对话
function streamChat(sessionId : string, messageIndex : number) { function startStreamChat(sessionId : string, messageIndex : number) {
const url = `http://localhost:8180${workcaseChatAPI.getStreamUrl(sessionId)}` console.log('建立SSE连接, sessionId:', sessionId)
console.log('建立SSE连接:', url)
const requestTask = uni.request({ aiChatAPI.streamChat(sessionId, {
url: url, onMessage: (data) => {
method: 'GET', if (data.answer) {
header: { 'Accept': 'text/event-stream' }, messages.value[messageIndex].content += data.answer
enableChunked: true, nextTick(() => scrollToBottom())
success: (res : any) => {
console.log('SSE请求完成:', res)
isTyping.value = false
},
fail: (err) => {
console.error('SSE请求失败:', err)
isTyping.value = false
messages.value[messageIndex].content = '抱歉,网络连接失败,请稍后重试。'
}
})
// 监听分块数据
requestTask.onChunkReceived((res : any) => {
try {
const decoder = new TextDecoder('utf-8')
const text = decoder.decode(new Uint8Array(res.data))
console.log('收到分块数据:', text)
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 = JSON.parse(dataStr)
const event = data.event
if (event === 'message' || event === 'agent_message') {
if (data.answer) {
messages.value[messageIndex].content += data.answer
}
} else if (event === 'message_end') {
isTyping.value = false
if (data.task_id) {
currentTaskId.value = data.task_id
}
} else if (event === 'error') {
console.error('SSE错误:', data.message)
isTyping.value = false
messages.value[messageIndex].content = data.message || '抱歉,发生错误,请稍后重试。'
}
} catch (e) {
console.log('解析SSE数据失败:', dataStr)
}
}
}
} }
} catch (e) { },
console.error('处理分块数据失败:', e) onEnd: (taskId) => {
isTyping.value = false
if (taskId) {
currentTaskId.value = taskId
}
},
onError: (error) => {
console.error('SSE错误:', error)
isTyping.value = false
messages.value[messageIndex].content = error
},
onComplete: () => {
isTyping.value = false
} }
nextTick(() => scrollToBottom())
}) })
} }
@@ -491,7 +471,11 @@
// 滚动到底部 // 滚动到底部
function scrollToBottom() { function scrollToBottom() {
scrollTop.value = 999999 // 先重置再设置大值,确保值变化触发滚动
scrollTop.value = scrollTop.value + 1
nextTick(() => {
scrollTop.value = 999999
})
} }
// 联系人工客服 // 联系人工客服

View File

@@ -108,3 +108,121 @@ export interface CommentMessageParams {
comment: string comment: string
userId: string userId: string
} }
// ==================== 请求参数类型(必传校验) ====================
/**
* 创建对话参数
*/
export interface CreateChatParam {
/** 智能体ID必传 */
agentId: string
/** 用户ID必传 */
userId: string
/** 用户类型 */
userType: boolean
/** 对话标题 */
title?: string
}
/**
* 准备流式对话参数
*/
export interface PrepareChatParam {
/** 对话ID必传 */
chatId: string
/** 用户问题(必传) */
query: string
/** 智能体ID */
agentId: string
userType: boolean
/** 用户ID */
userId?: string
/** 用户类型 */
/** 文件列表 */
files?: DifyFileInfo[]
}
/**
* 停止对话参数
*/
export interface StopChatParam {
/** 任务ID必传 */
taskId: string
/** 智能体ID必传 */
agentId: string
/** 用户ID必传 */
userId: string
}
/**
* 评价消息参数
*/
export interface CommentMessageParam {
/** 智能体ID必传 */
agentId: string
/** 对话ID必传 */
chatId: string
/** 消息ID必传 */
messageId: string
/** 评价内容(必传) */
comment: string
/** 用户ID必传 */
userId: string
}
/**
* 查询对话列表参数
*/
export interface ChatListParam {
/** 用户ID必传 */
userId: string
/** 智能体ID */
agentId?: string
}
/**
* 查询对话消息列表参数
*/
export interface ChatMessageListParam {
/** 对话ID必传 */
chatId: string
}
// ==================== SSE 流式对话类型 ====================
/**
* SSE 消息事件数据
*/
export interface SSEMessageData {
/** 事件类型 */
event?: string
/** 回答内容 */
answer?: string
/** 任务ID */
task_id?: string
/** 错误消息 */
message?: string
}
/**
* SSE 回调函数
*/
export interface SSECallbacks {
/** 收到消息 */
onMessage?: (data: SSEMessageData) => void
/** 消息结束 */
onEnd?: (taskId: string) => void
/** 发生错误 */
onError?: (error: string) => void
/** 请求完成(无论成功失败) */
onComplete?: () => void
}
/**
* SSE 请求任务对象
*/
export interface SSETask {
/** 停止请求 */
abort: () => void
}