ai对话
This commit is contained in:
@@ -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<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户问题
|
* 用户问题
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// =============================== 对话、工单等词云管理 ==========================
|
// =============================== 对话、工单等词云管理 ==========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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个知识库配置
|
||||||
*/
|
*/
|
||||||
@@ -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 = "创建聊天室(转人工时调用)")
|
||||||
|
|||||||
@@ -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分析对话,自动生成工单预填信息 ==============
|
|
||||||
// 步骤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<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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
194
urbanLifelineWeb/packages/workcase_wechat/api/ai/aiChat.ts
Normal file
194
urbanLifelineWeb/packages/workcase_wechat/api/ai/aiChat.ts
Normal 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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { aiChatAPI } from './aiChat'
|
||||||
@@ -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"
|
||||||
@@ -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 })
|
||||||
},
|
},
|
||||||
|
|
||||||
// ====================== 对话分析 ======================
|
// ====================== 对话分析 ======================
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const AGENT_ID = '17664699513920001'
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 联系人工客服
|
// 联系人工客服
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user