ai对话
This commit is contained in:
@@ -17,9 +17,10 @@ import org.xyzh.api.ai.dto.DifyFileInfo;
|
||||
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);
|
||||
if(NonUtils.isNotEmpty(token)){
|
||||
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
|
||||
if (NonUtils.isNotEmpty(loginDomain)) {
|
||||
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
|
||||
chat.setUserType(true);
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ public class ChatController {
|
||||
chat.setUserType(false);
|
||||
if(NonUtils.isNotEmpty(token)){
|
||||
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
|
||||
if (NonUtils.isNotEmpty(loginDomain)) {
|
||||
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
|
||||
chat.setUserType(true);
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ public class ChatController {
|
||||
chat.setUserType(false);
|
||||
if(NonUtils.isNotEmpty(token)){
|
||||
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
|
||||
if (NonUtils.isNotEmpty(loginDomain)) {
|
||||
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
|
||||
chat.setUserType(true);
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ public class ChatController {
|
||||
filter.setUserType(false);
|
||||
if(NonUtils.isNotEmpty(token)){
|
||||
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
|
||||
if (NonUtils.isNotEmpty(loginDomain)) {
|
||||
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
|
||||
filter.setUserType(true);
|
||||
}
|
||||
}
|
||||
@@ -161,7 +161,7 @@ public class ChatController {
|
||||
filter.setUserType(false);
|
||||
if(NonUtils.isNotEmpty(token)){
|
||||
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
|
||||
if (NonUtils.isNotEmpty(loginDomain)) {
|
||||
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
|
||||
filter.setUserType(true);
|
||||
}
|
||||
}
|
||||
@@ -192,7 +192,7 @@ public class ChatController {
|
||||
chatPrepareData.setUserType(false);
|
||||
if(NonUtils.isNotEmpty(token)){
|
||||
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
|
||||
if (NonUtils.isNotEmpty(loginDomain)) {
|
||||
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
|
||||
chatPrepareData.setUserType(true);
|
||||
}
|
||||
}
|
||||
@@ -245,7 +245,7 @@ public class ChatController {
|
||||
filter.setUserType(false);
|
||||
if(NonUtils.isNotEmpty(token)){
|
||||
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
|
||||
if (NonUtils.isNotEmpty(loginDomain)) {
|
||||
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
|
||||
filter.setUserType(true);
|
||||
}
|
||||
}
|
||||
@@ -278,7 +278,7 @@ public class ChatController {
|
||||
filter.setUserType(false);
|
||||
if(NonUtils.isNotEmpty(token)){
|
||||
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
|
||||
if (NonUtils.isNotEmpty(loginDomain)) {
|
||||
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
|
||||
filter.setUserType(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,6 +326,7 @@ public class AgentChatServiceImpl implements AgentChatService {
|
||||
chatRequest.setQuery(query);
|
||||
chatRequest.setUser(userId);
|
||||
chatRequest.setResponseMode("streaming");
|
||||
chatRequest.setInputs(new HashMap<>()); // Dify API 要求 inputs 必传
|
||||
|
||||
if (filesData != null && !filesData.isEmpty()) {
|
||||
chatRequest.setFiles(filesData);
|
||||
|
||||
@@ -17,7 +17,7 @@ auth:
|
||||
- /error
|
||||
- /actuator/health
|
||||
- /actuator/info
|
||||
- /ai/chat/* # AI对话,有非系统用户对话的接口,无登录状态
|
||||
- /ai/chat/** # AI对话,有非系统用户对话的接口,无登录状态
|
||||
|
||||
security:
|
||||
aes:
|
||||
|
||||
@@ -17,7 +17,7 @@ auth:
|
||||
- /error
|
||||
- /actuator/health
|
||||
- /actuator/info
|
||||
- /ai/chat/* # AI对话,有非系统用户对话的接口,无登录状态
|
||||
- /ai/chat/** # AI对话,有非系统用户对话的接口,无登录状态
|
||||
|
||||
security:
|
||||
aes:
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
package org.xyzh.api.workcase.service;
|
||||
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import org.xyzh.api.ai.dto.ChatPrepareData;
|
||||
import org.xyzh.api.ai.dto.TbChat;
|
||||
import org.xyzh.api.ai.dto.TbChatMessage;
|
||||
import org.xyzh.api.workcase.dto.TbWordCloudDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.core.page.PageRequest;
|
||||
|
||||
@@ -18,85 +13,6 @@ import org.xyzh.common.core.page.PageRequest;
|
||||
*/
|
||||
public interface WorkcaseChatService {
|
||||
|
||||
// ========================= 聊天管理 ==========================
|
||||
/**
|
||||
* @description 来客创建聊天对话
|
||||
* @param
|
||||
* @author yslg
|
||||
* @since 2025-12-18
|
||||
*/
|
||||
ResultDomain<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. 来客不存在,创建新来客
|
||||
if (guest == null) {
|
||||
TbGuestDTO newGuest = new TbGuestDTO();
|
||||
newGuest.setOptsn(IdUtil.getOptsn());
|
||||
newGuest.setUserId(IdUtil.generateID());
|
||||
newGuest.setWechatId(loginParam.getWechatId());
|
||||
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.dto.sys.TbGuestDTO;
|
||||
import org.xyzh.common.dto.sys.TbSysUserRoleDTO;
|
||||
import org.xyzh.common.utils.id.IdUtil;
|
||||
import org.xyzh.system.mapper.user.TbGuestMapper;
|
||||
import org.xyzh.system.mapper.user.TbSysUserRoleMapper;
|
||||
|
||||
@@ -48,6 +49,7 @@ public class GuestServiceImpl implements GuestService{
|
||||
|
||||
// 绑定访客角色(role_guest)
|
||||
TbSysUserRoleDTO userRole = new TbSysUserRoleDTO();
|
||||
userRole.setOptsn(IdUtil.getOptsn());
|
||||
userRole.setUserId(guest.getUserId());
|
||||
userRole.setRoleId("role_guest");
|
||||
userRole.setDeptId("dept_root");
|
||||
|
||||
@@ -224,12 +224,15 @@ public class SysUserServiceImpl implements SysUserService {
|
||||
|
||||
@Override
|
||||
public ResultDomain<SysUserVO> getLoginUser(SysUserVO filter) {
|
||||
// 登录查询语义与 getUser 相同(可根据用户名/手机号/邮箱查询)
|
||||
// 登录查询语义与 getUser 相同(可根据用户名/手机号/邮箱/wechatId查询)
|
||||
if(NonUtils.isNotNull(filter.getPhone())){
|
||||
filter.setPhone(filter.getPhone());
|
||||
}
|
||||
SysUserVO userVO = userMapper.getUserByFilter(filter).get(0);
|
||||
return ResultDomain.success("查询成功", userVO);
|
||||
List<SysUserVO> list = userMapper.getUserByFilter(filter);
|
||||
if (list == null || list.isEmpty()) {
|
||||
return ResultDomain.failure("用户不存在");
|
||||
}
|
||||
return ResultDomain.success("查询成功", list.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -172,6 +172,9 @@
|
||||
<if test="filter.username !=null and filter.username !=''">
|
||||
AND ui.username = #{filter.username}
|
||||
</if>
|
||||
<if test="filter.wechatId !=null and filter.wechatId !=''">
|
||||
AND u.wechat_id = #{filter.wechatId}
|
||||
</if>
|
||||
<!-- username / userType / deptPath 在表中不存在,按 SQL 为准移除相关条件 -->
|
||||
AND (u.deleted IS NULL OR u.deleted = false)
|
||||
</where>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<!-- 用户角色关系字段 -->
|
||||
<id column="user_id" property="userId" 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="creator" property="creator" jdbcType="VARCHAR"/>
|
||||
@@ -62,7 +63,7 @@
|
||||
|
||||
<!-- 基础列 -->
|
||||
<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
|
||||
</sql>
|
||||
|
||||
@@ -73,6 +74,7 @@
|
||||
<!-- 必填字段:user_id, role_id, optsn -->
|
||||
user_id,
|
||||
role_id,
|
||||
dept_id,
|
||||
optsn,
|
||||
<!-- 可选字段:基础字段按是否有值动态拼接 -->
|
||||
<if test="creator != null and creator != ''">creator,</if>
|
||||
@@ -88,6 +90,7 @@
|
||||
<!-- 必填字段值 -->
|
||||
#{userId},
|
||||
#{roleId},
|
||||
#{deptId},
|
||||
#{optsn},
|
||||
<!-- 可选字段值 -->
|
||||
<if test="creator != null and creator != ''">#{creator},</if>
|
||||
|
||||
@@ -9,7 +9,9 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.xyzh.api.ai.dto.TbAgent;
|
||||
import org.xyzh.api.ai.dto.TbKnowledge;
|
||||
import org.xyzh.api.ai.service.AgentService;
|
||||
import org.xyzh.api.ai.service.KnowledgeService;
|
||||
import org.xyzh.api.system.service.SysConfigService;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
@@ -23,8 +25,8 @@ import org.xyzh.common.utils.id.IdUtil;
|
||||
* @since 2025-12-18
|
||||
*/
|
||||
@Configuration
|
||||
public class KnowledgeInit {
|
||||
private static final Logger logger = LoggerFactory.getLogger(KnowledgeInit.class);
|
||||
public class AiInit {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AiInit.class);
|
||||
|
||||
private static final String SERVICE_WORKCASE = "workcase";
|
||||
private static final String CATEGORY_INTERNAL = "internal";
|
||||
@@ -36,6 +38,9 @@ public class KnowledgeInit {
|
||||
@DubboReference(version = "1.0.0", group = "system", timeout = 30000, retries = 0)
|
||||
private SysConfigService sysConfigService;
|
||||
|
||||
@DubboReference(version = "1.0.0", group = "ai", timeout = 30000, retries = 0)
|
||||
private AgentService agentService;
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner knowledgeInitRunner() {
|
||||
return args -> {
|
||||
@@ -58,6 +63,31 @@ public class KnowledgeInit {
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner agentInitRunner(){
|
||||
return args -> {
|
||||
logger.info("开始初始化客服系统智能体...");
|
||||
TbAgent agent = new TbAgent();
|
||||
agent.setIsOuter(true);
|
||||
agent.setName("泰豪小电");
|
||||
ResultDomain<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个知识库配置
|
||||
*/
|
||||
@@ -2,6 +2,7 @@ package org.xyzh.workcase.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -11,16 +12,11 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import org.xyzh.api.ai.dto.ChatPrepareData;
|
||||
import org.xyzh.api.ai.dto.TbChat;
|
||||
import org.xyzh.api.ai.dto.TbChatMessage;
|
||||
import org.xyzh.api.workcase.dto.TbChatRoomDTO;
|
||||
import org.xyzh.api.workcase.dto.TbChatRoomMemberDTO;
|
||||
import org.xyzh.api.workcase.dto.TbChatRoomMessageDTO;
|
||||
import org.xyzh.api.workcase.dto.TbCustomerServiceDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWordCloudDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
|
||||
import org.xyzh.api.workcase.service.ChatRoomService;
|
||||
import org.xyzh.api.workcase.service.WorkcaseChatService;
|
||||
import org.xyzh.api.workcase.vo.ChatMemberVO;
|
||||
@@ -49,6 +45,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
@Tag(name = "工单对话")
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/workcase/chat")
|
||||
public class WorkcaseChatContorller {
|
||||
@@ -59,106 +56,6 @@ public class WorkcaseChatContorller {
|
||||
@Autowired
|
||||
private ChatRoomService chatRoomService;
|
||||
|
||||
// ========================= AI对话管理 =========================
|
||||
|
||||
@Operation(summary = "创建对话")
|
||||
@PreAuthorize("hasAuthority('workcase:chat:create')")
|
||||
@PostMapping
|
||||
public ResultDomain<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) =========================
|
||||
|
||||
@Operation(summary = "创建聊天室(转人工时调用)")
|
||||
|
||||
@@ -4,24 +4,16 @@ import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.dubbo.config.annotation.DubboReference;
|
||||
import org.apache.dubbo.config.annotation.DubboService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import org.xyzh.api.ai.dto.ChatPrepareData;
|
||||
import org.xyzh.api.ai.dto.TbChat;
|
||||
import org.xyzh.api.ai.dto.TbChatMessage;
|
||||
import org.xyzh.api.ai.service.AgentChatService;
|
||||
import org.xyzh.api.workcase.dto.TbWordCloudDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
|
||||
import org.xyzh.api.workcase.service.WorkcaseChatService;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.core.page.PageDomain;
|
||||
import org.xyzh.common.core.page.PageParam;
|
||||
import org.xyzh.common.core.page.PageRequest;
|
||||
import org.xyzh.common.redis.service.RedisService;
|
||||
import org.xyzh.common.utils.id.IdUtil;
|
||||
import org.xyzh.workcase.mapper.TbWordCloudMapper;
|
||||
|
||||
@@ -30,201 +22,9 @@ public class WorkcaseChatServiceImpl implements WorkcaseChatService{
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WorkcaseChatServiceImpl.class);
|
||||
|
||||
private static final String CHAT_COUNT_KEY_PREFIX = "workcase:chat:count:";
|
||||
private static final int TRANSFER_HUMAN_THRESHOLD = 3;
|
||||
|
||||
@DubboReference(version = "1.0.0", group = "ai", check = false, retries = 0)
|
||||
private AgentChatService agentChatService;
|
||||
|
||||
@Autowired
|
||||
private TbWordCloudMapper wordCloudMapper;
|
||||
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
|
||||
// ========================= 聊天管理 ==========================
|
||||
|
||||
@Override
|
||||
public ResultDomain<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
|
||||
|
||||
@@ -9,7 +9,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseDeviceDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseProcessDTO;
|
||||
import org.xyzh.api.workcase.service.WorkcaseChatService;
|
||||
import org.xyzh.api.workcase.service.WorkcaseService;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.core.page.PageDomain;
|
||||
@@ -21,7 +20,6 @@ import org.xyzh.workcase.mapper.TbWorkcaseDeviceMapper;
|
||||
import org.xyzh.workcase.mapper.TbWorkcaseMapper;
|
||||
import org.xyzh.workcase.mapper.TbWorkcaseProcessMapper;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
@DubboService(version = "1.0.0",group = "workcase",timeout = 30000,retries = 0)
|
||||
@@ -37,9 +35,6 @@ public class WorkcaseServiceImpl implements WorkcaseService {
|
||||
@Autowired
|
||||
private TbWorkcaseDeviceMapper workcaseDeviceMapper;
|
||||
|
||||
@Autowired
|
||||
private WorkcaseChatService workcaseChatService;
|
||||
|
||||
// ====================== 工单管理 ======================
|
||||
|
||||
@Override
|
||||
@@ -103,11 +98,9 @@ public class WorkcaseServiceImpl implements WorkcaseService {
|
||||
if ("done".equals(workcase.getStatus())) {
|
||||
process.setAction(WorkcaseProcessAction.FINISH.getName());
|
||||
process.setMessage("工单完成");
|
||||
workcaseChatService.summaryChat(existing.getWorkcaseId());
|
||||
} else if ("cancelled".equals(workcase.getStatus())) {
|
||||
process.setAction(WorkcaseProcessAction.REPEAL.getName());
|
||||
process.setMessage("工单撤销");
|
||||
workcaseChatService.summaryChat(existing.getWorkcaseId());
|
||||
} else {
|
||||
process.setAction(WorkcaseProcessAction.INFO.getName());
|
||||
process.setMessage("状态变更: " + oldStatus + " -> " + workcase.getStatus());
|
||||
@@ -309,13 +302,11 @@ public class WorkcaseServiceImpl implements WorkcaseService {
|
||||
workcase.setWorkcaseId(workcaseProcess.getWorkcaseId());
|
||||
workcase.setStatus("done");
|
||||
workcaseMapper.updateWorkcase(workcase);
|
||||
workcaseChatService.summaryChat(workcaseProcess.getWorkcaseId());
|
||||
} else if (WorkcaseProcessAction.REPEAL.getName().equals(action)) {
|
||||
TbWorkcaseDTO workcase = new TbWorkcaseDTO();
|
||||
workcase.setWorkcaseId(workcaseProcess.getWorkcaseId());
|
||||
workcase.setStatus("cancelled");
|
||||
workcaseMapper.updateWorkcase(workcase);
|
||||
workcaseChatService.summaryChat(workcaseProcess.getWorkcaseId());
|
||||
}
|
||||
|
||||
int rows = workcaseProcessMapper.insertWorkcaseProcess(workcaseProcess);
|
||||
|
||||
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 "./sys"
|
||||
export * from "./workcase"
|
||||
export * from "./ai"
|
||||
@@ -11,26 +11,30 @@ import type {
|
||||
ChatMemberVO,
|
||||
ChatRoomMessageVO,
|
||||
CustomerServiceVO
|
||||
} from '../../types/workcase/chatRoom'
|
||||
} from '../../types/workcase'
|
||||
import type {
|
||||
TbChat,
|
||||
TbChatMessage,
|
||||
CreateChatParam,
|
||||
PrepareChatParam,
|
||||
StopChatParam,
|
||||
CommentMessageParam,
|
||||
ChatListParam,
|
||||
ChatMessageListParam,
|
||||
SSECallbacks,
|
||||
SSETask,
|
||||
SSEMessageData
|
||||
} from '../../types/ai/aiChat'
|
||||
|
||||
// AI对话相关类型(简化版)
|
||||
interface TbChat {
|
||||
chatId?: string
|
||||
userId?: string
|
||||
title?: string
|
||||
status?: string
|
||||
}
|
||||
interface TbChatMessage {
|
||||
messageId?: string
|
||||
chatId?: string
|
||||
content?: string
|
||||
role?: string
|
||||
}
|
||||
interface ChatPrepareData {
|
||||
chatId?: string
|
||||
message?: string
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare const uni: {
|
||||
getStorageSync: (key: string) => any
|
||||
request: (options: any) => any
|
||||
}
|
||||
|
||||
// API 基础配置
|
||||
const BASE_URL = 'http://localhost:8180'
|
||||
|
||||
/**
|
||||
* @description 工单对话相关接口
|
||||
* @filename workcaseChat.ts
|
||||
@@ -45,9 +49,10 @@ export const workcaseChatAPI = {
|
||||
|
||||
/**
|
||||
* 创建对话
|
||||
* @param param agentId和userId必传
|
||||
*/
|
||||
createChat(chat: TbChat): Promise<ResultDomain<TbChat>> {
|
||||
return request<TbChat>({ url: this.baseUrl, method: 'POST', data: chat })
|
||||
createChat(param: CreateChatParam): Promise<ResultDomain<TbChat>> {
|
||||
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>> {
|
||||
return request<TbChat>({ url: `${this.baseUrl}/list`, method: 'POST', data: filter })
|
||||
getChatList(param: ChatListParam): Promise<ResultDomain<TbChat[]>> {
|
||||
return request<TbChat[]>({ url: `${this.baseUrl}/list`, method: 'POST', data: param })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取对话消息列表
|
||||
* @param param chatId必传
|
||||
*/
|
||||
getChatMessageList(filter: TbChat): Promise<ResultDomain<TbChatMessage>> {
|
||||
return request<TbChatMessage>({ url: `${this.baseUrl}/message/list`, method: 'POST', data: filter })
|
||||
getChatMessageList(param: ChatMessageListParam): Promise<ResultDomain<TbChatMessage[]>> {
|
||||
return request<TbChatMessage[]>({ url: `${this.baseUrl}/message/list`, method: 'POST', data: param })
|
||||
},
|
||||
|
||||
/**
|
||||
* 准备对话会话
|
||||
* 准备流式对话会话
|
||||
* @param param chatId和message必传
|
||||
*/
|
||||
prepareChatMessageSession(prepareData: ChatPrepareData): Promise<ResultDomain<string>> {
|
||||
return request<string>({ url: `${this.baseUrl}/prepare`, method: 'POST', data: prepareData })
|
||||
prepareChatMessageSession(param: PrepareChatParam): Promise<ResultDomain<string>> {
|
||||
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>> {
|
||||
return request<boolean>({ url: `${this.baseUrl}/stop/${taskId}`, method: 'POST', data: filter })
|
||||
streamChat(sessionId: string, callbacks: SSECallbacks): SSETask {
|
||||
const url = `${BASE_URL}${this.baseUrl}/stream/${sessionId}`
|
||||
const token = uni.getStorageSync('token') || ''
|
||||
|
||||
const requestTask = uni.request({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
header: {
|
||||
'Accept': 'text/event-stream',
|
||||
'Authorization': token ? `Bearer ${token}` : ''
|
||||
},
|
||||
enableChunked: true,
|
||||
success: (res: any) => {
|
||||
console.log('SSE请求完成:', res)
|
||||
// 处理非200状态码
|
||||
if (res.statusCode !== 200) {
|
||||
console.error('SSE请求状态码异常:', res.statusCode)
|
||||
let errorMsg = '抱歉,服务暂时不可用,请稍后重试。'
|
||||
if (res.statusCode === 401) {
|
||||
errorMsg = '登录已过期,请重新登录。'
|
||||
} else if (res.statusCode === 403) {
|
||||
errorMsg = '无权限访问,请联系管理员。'
|
||||
} else if (res.statusCode === 404) {
|
||||
errorMsg = '会话不存在或已过期,请重新发起对话。'
|
||||
} else if (res.statusCode >= 500) {
|
||||
errorMsg = '服务器异常,请稍后重试。'
|
||||
}
|
||||
callbacks.onError?.(errorMsg)
|
||||
}
|
||||
callbacks.onComplete?.()
|
||||
},
|
||||
fail: (err: any) => {
|
||||
console.error('SSE请求失败:', err)
|
||||
callbacks.onError?.('网络连接失败,请稍后重试。')
|
||||
callbacks.onComplete?.()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听分块数据
|
||||
requestTask.onChunkReceived((res: any) => {
|
||||
try {
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
const text = decoder.decode(new Uint8Array(res.data))
|
||||
|
||||
const lines = text.split('\n')
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data:')) {
|
||||
const dataStr = line.substring(5).trim()
|
||||
if (dataStr && dataStr !== '[DONE]') {
|
||||
try {
|
||||
const data: SSEMessageData = JSON.parse(dataStr)
|
||||
const event = data.event
|
||||
|
||||
if (event === 'message' || event === 'agent_message') {
|
||||
callbacks.onMessage?.(data)
|
||||
} else if (event === 'message_end') {
|
||||
callbacks.onEnd?.(data.task_id || '')
|
||||
} else if (event === 'error') {
|
||||
callbacks.onError?.(data.message || '发生错误,请稍后重试。')
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('解析SSE数据失败:', dataStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('处理分块数据失败:', e)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
abort: () => {
|
||||
requestTask.abort()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 评论对话消息
|
||||
* 停止对话
|
||||
* @param param taskId, agentId, userId必传
|
||||
*/
|
||||
commentChatMessage(filter: TbChat, messageId: string, comment: string): Promise<ResultDomain<boolean>> {
|
||||
return request<boolean>({ url: `${this.baseUrl}/comment?messageId=${messageId}&comment=${comment}`, method: 'POST', data: filter })
|
||||
stopChat(param: StopChatParam): Promise<ResultDomain<boolean>> {
|
||||
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;
|
||||
border: 1px solid #E5E5E5;
|
||||
border-radius: 12px;
|
||||
min-height: 40px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-bubble .message-text {
|
||||
@@ -460,3 +464,43 @@
|
||||
font-size: 18px;
|
||||
color: #4b87ff;
|
||||
}
|
||||
|
||||
// 打字指示器动画
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
.typing-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #999;
|
||||
animation: typing-bounce 1.4s infinite ease-in-out both;
|
||||
}
|
||||
|
||||
.typing-dot:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.typing-dot:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
.typing-dot:nth-child(3) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
@keyframes typing-bounce {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,13 @@
|
||||
<text class="avatar-text">AI</text>
|
||||
</view>
|
||||
<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>
|
||||
<text class="message-time">{{item.time}}</text>
|
||||
@@ -112,9 +118,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, onMounted } from 'vue'
|
||||
import WorkcaseCreator from '@/components/WorkcaseCreator/WorkcaseCreator.uvue'
|
||||
import { guestAPI, workcaseChatAPI } from '@/api'
|
||||
import { guestAPI, aiChatAPI } from '@/api'
|
||||
import type { TbWorkcaseDTO } from '@/types'
|
||||
|
||||
import { AGENT_ID } from '@/config'
|
||||
// 前端消息展示类型
|
||||
interface ChatMessageItem {
|
||||
type: 'user' | 'bot'
|
||||
@@ -122,7 +128,7 @@
|
||||
time: string
|
||||
actions?: string[] | null
|
||||
}
|
||||
|
||||
const agentId = AGENT_ID
|
||||
// 响应式数据
|
||||
const messages = ref<ChatMessageItem[]>([])
|
||||
const inputText = ref<string>('')
|
||||
@@ -141,7 +147,7 @@
|
||||
userId: ''
|
||||
})
|
||||
const isMockMode = ref(true) // 开发环境mock模式
|
||||
|
||||
const userType = ref(false)
|
||||
// AI 对话相关
|
||||
const chatId = ref<string>('') // 当前会话ID
|
||||
const currentTaskId = ref<string>('') // 当前任务ID(用于停止)
|
||||
@@ -161,9 +167,9 @@
|
||||
// 开发环境:使用mock数据
|
||||
if (isMockMode.value) {
|
||||
userInfo.value = {
|
||||
wechatId: '17857100375',
|
||||
username: '测试用户',
|
||||
phone: '17857100375',
|
||||
wechatId: '17857100377',
|
||||
username: '访客用户',
|
||||
phone: '17857100377',
|
||||
userId: ''
|
||||
}
|
||||
await doIdentify()
|
||||
@@ -173,12 +179,12 @@
|
||||
// 切换mock用户(开发调试用)
|
||||
function switchMockUser() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['员工 (17857100375)', '访客 (17857100376)'],
|
||||
itemList: ['员工 (17857100375)', '访客 (17857100377)'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
userInfo.value = { wechatId: '17857100375', username: '员工用户', phone: '17857100375', userId: '' }
|
||||
} else {
|
||||
userInfo.value = { wechatId: '17857100376', username: '访客用户', phone: '17857100376', userId: '' }
|
||||
userInfo.value = { wechatId: '17857100377', username: '访客用户', phone: '17857100377', userId: '' }
|
||||
}
|
||||
doIdentify()
|
||||
}
|
||||
@@ -198,10 +204,16 @@
|
||||
const loginDomain = res.data
|
||||
uni.setStorageSync('token', loginDomain.token || '')
|
||||
uni.setStorageSync('userInfo', JSON.stringify(loginDomain.user))
|
||||
uni.setStorageSync('loginDomain', JSON.stringify(loginDomain))
|
||||
uni.setStorageSync('wechatId', userInfo.value.wechatId)
|
||||
userInfo.value.userId = loginDomain.user?.userId || ''
|
||||
console.log('identify成功:', loginDomain)
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
if(loginDomain.user.status == 'guest') {
|
||||
userType.value = false
|
||||
} else {
|
||||
userType.value = true
|
||||
}
|
||||
} else {
|
||||
console.error('identify失败:', res.message)
|
||||
}
|
||||
@@ -275,9 +287,11 @@
|
||||
try {
|
||||
// 如果没有会话ID,先创建会话
|
||||
if (!chatId.value) {
|
||||
const createRes = await workcaseChatAPI.createChat({
|
||||
const createRes = await aiChatAPI.createChat({
|
||||
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) {
|
||||
chatId.value = createRes.data.chatId || ''
|
||||
@@ -288,22 +302,24 @@
|
||||
}
|
||||
|
||||
// 准备流式对话
|
||||
const prepareRes = await workcaseChatAPI.prepareChatMessageSession({
|
||||
const prepareRes = await aiChatAPI.prepareChatMessageSession({
|
||||
chatId: chatId.value,
|
||||
message: query
|
||||
query: query,
|
||||
agentId: agentId,
|
||||
userType: userType.value,
|
||||
userId: userInfo.value.userId
|
||||
})
|
||||
if (!prepareRes.success || !prepareRes.data) {
|
||||
throw new Error(prepareRes.message || '准备对话失败')
|
||||
}
|
||||
const sessionId = prepareRes.data
|
||||
console.log('准备流式对话成功:', sessionId)
|
||||
|
||||
// 添加空的AI消息占位
|
||||
const messageIndex = messages.value.length
|
||||
addMessage('bot', '')
|
||||
|
||||
// 建立SSE连接
|
||||
streamChat(sessionId, messageIndex)
|
||||
startStreamChat(sessionId, messageIndex)
|
||||
} catch (error : any) {
|
||||
console.error('AI聊天失败:', error)
|
||||
isTyping.value = false
|
||||
@@ -312,66 +328,30 @@
|
||||
}
|
||||
|
||||
// SSE 流式对话
|
||||
function streamChat(sessionId : string, messageIndex : number) {
|
||||
const url = `http://localhost:8180${workcaseChatAPI.getStreamUrl(sessionId)}`
|
||||
console.log('建立SSE连接:', url)
|
||||
function startStreamChat(sessionId : string, messageIndex : number) {
|
||||
console.log('建立SSE连接, sessionId:', sessionId)
|
||||
|
||||
const requestTask = uni.request({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
header: { 'Accept': 'text/event-stream' },
|
||||
enableChunked: true,
|
||||
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') {
|
||||
aiChatAPI.streamChat(sessionId, {
|
||||
onMessage: (data) => {
|
||||
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)
|
||||
}
|
||||
nextTick(() => scrollToBottom())
|
||||
}
|
||||
},
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -491,7 +471,11 @@
|
||||
|
||||
// 滚动到底部
|
||||
function scrollToBottom() {
|
||||
// 先重置再设置大值,确保值变化触发滚动
|
||||
scrollTop.value = scrollTop.value + 1
|
||||
nextTick(() => {
|
||||
scrollTop.value = 999999
|
||||
})
|
||||
}
|
||||
|
||||
// 联系人工客服
|
||||
|
||||
@@ -108,3 +108,121 @@ export interface CommentMessageParams {
|
||||
comment: 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