会话总结工作流接入、前后端处理
This commit is contained in:
@@ -113,6 +113,30 @@ CREATE INDEX idx_chat_msg_sender ON workcase.tb_chat_room_message(sender_id, sen
|
|||||||
CREATE INDEX idx_chat_msg_ai ON workcase.tb_chat_room_message(ai_message_id) WHERE ai_message_id IS NOT NULL;
|
CREATE INDEX idx_chat_msg_ai ON workcase.tb_chat_room_message(ai_message_id) WHERE ai_message_id IS NOT NULL;
|
||||||
COMMENT ON TABLE workcase.tb_chat_room_message IS 'IM聊天消息表,包含AI对话和人工客服消息';
|
COMMENT ON TABLE workcase.tb_chat_room_message IS 'IM聊天消息表,包含AI对话和人工客服消息';
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS workcase.tb_chat_room_summary CASCADE;
|
||||||
|
CREATE TABLE workcase.tb_chat_room_summary (
|
||||||
|
optsn VARCHAR(50) NOT NULL, -- 流水号
|
||||||
|
summary_id VARCHAR(50) NOT NULL, -- 总结ID
|
||||||
|
room_id VARCHAR(50) NOT NULL, -- 聊天室ID
|
||||||
|
question TEXT DEFAULT NULL, -- 核心问题
|
||||||
|
needs VARCHAR(500)[] DEFAULT '{}', -- 核心诉求数组
|
||||||
|
answer TEXT DEFAULT NULL, -- 解决方案
|
||||||
|
workcloud VARCHAR(500)[] DEFAULT '{}', -- 词云关键词数组
|
||||||
|
message_count INTEGER DEFAULT 0, -- 参与总结的消息数量
|
||||||
|
summary_time TIMESTAMPTZ DEFAULT NULL, -- 总结生成时间
|
||||||
|
creator VARCHAR(50) NOT NULL, -- 创建人
|
||||||
|
create_time TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
|
||||||
|
update_time TIMESTAMPTZ DEFAULT NULL, -- 更新时间
|
||||||
|
delete_time TIMESTAMPTZ DEFAULT NULL, -- 删除时间
|
||||||
|
deleted BOOLEAN NOT NULL DEFAULT false, -- 是否删除
|
||||||
|
PRIMARY KEY (summary_id),
|
||||||
|
UNIQUE (optsn)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_chat_room_summary_room ON workcase.tb_chat_room_summary(room_id, summary_time DESC);
|
||||||
|
CREATE INDEX idx_chat_room_summary_time ON workcase.tb_chat_room_summary(summary_time DESC);
|
||||||
|
COMMENT ON TABLE workcase.tb_chat_room_summary IS '聊天室总结表,保存AI生成的聊天总结分析';
|
||||||
|
|
||||||
|
|
||||||
-- 4. 视频会议表(Jitsi Meet)
|
-- 4. 视频会议表(Jitsi Meet)
|
||||||
-- 记录聊天室内创建的视频会议
|
-- 记录聊天室内创建的视频会议
|
||||||
DROP TABLE IF EXISTS workcase.tb_video_meeting CASCADE;
|
DROP TABLE IF EXISTS workcase.tb_video_meeting CASCADE;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ INSERT INTO config.tb_sys_config (
|
|||||||
('CFG-0477', 'cfg_dify_retrieval_threshold', 'dify.knowledge.retrieval.score.threshold','相似度阈值', '0.5', 'DOUBLE', 'input', '检索相似度阈值', NULL, NULL, 'dify', 'mod_agent', 170, 1, '低于此阈值的文档将被过滤(0.0-1.0)', 'system', NULL, NULL, now(), NULL, NULL, false),
|
('CFG-0477', 'cfg_dify_retrieval_threshold', 'dify.knowledge.retrieval.score.threshold','相似度阈值', '0.5', 'DOUBLE', 'input', '检索相似度阈值', NULL, NULL, 'dify', 'mod_agent', 170, 1, '低于此阈值的文档将被过滤(0.0-1.0)', 'system', NULL, NULL, now(), NULL, NULL, false),
|
||||||
-- Dify workcase相关智能体配置
|
-- Dify workcase相关智能体配置
|
||||||
('CFG-0478', 'cfg_dify_workcase_chat', 'dify.workcase.agent.chat','泰豪小电AgentApiKey', 'app-CDKy0wYkPnl6dA6G7eu113Vw', 'String', 'input', '泰豪小电AgentApiKey', NULL, NULL, 'dify', 'mod_agent', 160, 1, '泰豪小电AgentApiKey', 'system', NULL, NULL, now(), NULL, NULL, false),
|
('CFG-0478', 'cfg_dify_workcase_chat', 'dify.workcase.agent.chat','泰豪小电AgentApiKey', 'app-CDKy0wYkPnl6dA6G7eu113Vw', 'String', 'input', '泰豪小电AgentApiKey', NULL, NULL, 'dify', 'mod_agent', 160, 1, '泰豪小电AgentApiKey', 'system', NULL, NULL, now(), NULL, NULL, false),
|
||||||
('CFG-0479', 'cfg_dify_workcase_summary', 'dify.workcase.agent.summary','工单总结AgentApikey', 'app-YMlj2B0m21KpYZPv3YdObi7r', 'String', 'input', '工单总结AgentApikey', NULL, NULL, 'dify', 'mod_agent', 170, 1, '工单总结AgentApikey', 'system', NULL, NULL, now(), NULL, NULL, false),
|
('CFG-0479', 'cfg_dify_workcase_summary', 'dify.workcase.workflow.summary','工单总结AgentApikey', 'app-YMlj2B0m21KpYZPv3YdObi7r', 'String', 'input', '工单总结AgentApikey', NULL, NULL, 'dify', 'mod_agent', 170, 1, '工单总结AgentApikey', 'system', NULL, NULL, now(), NULL, NULL, false),
|
||||||
|
|
||||||
|
|
||||||
-- 日志与审计
|
-- 日志与审计
|
||||||
|
|||||||
@@ -572,6 +572,45 @@ public class DifyApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================== 工作流 API =====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行工作流(阻塞模式)
|
||||||
|
*/
|
||||||
|
public WorkflowRunResponse runWorkflowBlocking(WorkflowRunRequest request, String apiKey) {
|
||||||
|
String url = difyConfig.getFullApiUrl("/workflows/run");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 设置为阻塞模式
|
||||||
|
request.setResponseMode("blocking");
|
||||||
|
|
||||||
|
String jsonBody = JSON.toJSONString(request);
|
||||||
|
logger.info("调用Dify工作流接口: url={}, request={}", url, jsonBody);
|
||||||
|
|
||||||
|
Request httpRequest = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.header("Authorization", "Bearer " + getApiKey(apiKey))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.post(RequestBody.create(jsonBody, MediaType.parse("application/json")))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (Response response = httpClient.newCall(httpRequest).execute()) {
|
||||||
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
logger.error("工作流执行失败: {} - {}", response.code(), responseBody);
|
||||||
|
throw new DifyException("工作流执行失败: " + responseBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("工作流执行成功: {}", responseBody);
|
||||||
|
return JSON.parseObject(responseBody, WorkflowRunResponse.class);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("工作流执行异常", e);
|
||||||
|
throw new DifyException("工作流执行异常: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停止对话生成
|
* 停止对话生成
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package org.xyzh.ai.client.dto;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.annotation.JSONField;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 工作流执行请求
|
||||||
|
* @filename WorkflowRunRequest.java
|
||||||
|
* @author system
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2026-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WorkflowRunRequest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入变量(Dify 工作流 API 必需字段)
|
||||||
|
*/
|
||||||
|
@JSONField(serializeFeatures = com.alibaba.fastjson2.JSONWriter.Feature.WriteMapNullValue)
|
||||||
|
private Map<String, Object> inputs = new java.util.HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应模式:streaming(流式)、blocking(阻塞)
|
||||||
|
*/
|
||||||
|
@JSONField(name = "response_mode")
|
||||||
|
private String responseMode = "blocking";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户标识
|
||||||
|
*/
|
||||||
|
private String user;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package org.xyzh.ai.client.dto;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.annotation.JSONField;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 工作流执行响应(阻塞模式)
|
||||||
|
* @filename WorkflowRunResponse.java
|
||||||
|
* @author system
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2026-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WorkflowRunResponse {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流执行ID
|
||||||
|
*/
|
||||||
|
@JSONField(name = "workflow_run_id")
|
||||||
|
private String workflowRunId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务ID
|
||||||
|
*/
|
||||||
|
@JSONField(name = "task_id")
|
||||||
|
private String taskId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流执行数据
|
||||||
|
*/
|
||||||
|
private WorkflowData data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class WorkflowData {
|
||||||
|
/**
|
||||||
|
* 执行ID
|
||||||
|
*/
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流ID
|
||||||
|
*/
|
||||||
|
@JSONField(name = "workflow_id")
|
||||||
|
private String workflowId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行状态:running、succeeded、failed、stopped
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流输出结果
|
||||||
|
*/
|
||||||
|
private Map<String, Object> outputs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误信息
|
||||||
|
*/
|
||||||
|
private String error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行耗时(秒)
|
||||||
|
*/
|
||||||
|
@JSONField(name = "elapsed_time")
|
||||||
|
private Double elapsedTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总Token数
|
||||||
|
*/
|
||||||
|
@JSONField(name = "total_tokens")
|
||||||
|
private Integer totalTokens;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@JSONField(name = "created_at")
|
||||||
|
private Long createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成时间
|
||||||
|
*/
|
||||||
|
@JSONField(name = "finished_at")
|
||||||
|
private Long finishedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,9 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|||||||
import org.xyzh.ai.client.DifyApiClient;
|
import org.xyzh.ai.client.DifyApiClient;
|
||||||
import org.xyzh.ai.client.callback.StreamCallback;
|
import org.xyzh.ai.client.callback.StreamCallback;
|
||||||
import org.xyzh.ai.client.dto.ChatRequest;
|
import org.xyzh.ai.client.dto.ChatRequest;
|
||||||
|
import org.xyzh.ai.client.dto.ChatResponse;
|
||||||
|
import org.xyzh.ai.client.dto.WorkflowRunRequest;
|
||||||
|
import org.xyzh.ai.client.dto.WorkflowRunResponse;
|
||||||
import org.xyzh.ai.config.DifyConfig;
|
import org.xyzh.ai.config.DifyConfig;
|
||||||
import org.xyzh.ai.mapper.TbChatMapper;
|
import org.xyzh.ai.mapper.TbChatMapper;
|
||||||
import org.xyzh.ai.mapper.TbChatMessageMapper;
|
import org.xyzh.ai.mapper.TbChatMessageMapper;
|
||||||
@@ -60,6 +63,7 @@ public class AgentChatServiceImpl implements AgentChatService {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(AgentChatServiceImpl.class);
|
private static final Logger logger = LoggerFactory.getLogger(AgentChatServiceImpl.class);
|
||||||
|
|
||||||
private static final String CHAT_SESSION_PREFIX = "ai:chat:session:";
|
private static final String CHAT_SESSION_PREFIX = "ai:chat:session:";
|
||||||
|
private static final String WORKFLOW_SESSION_PREFIX = "ai:workflow:session:";
|
||||||
private static final long SESSION_TTL = 5 * 60;
|
private static final long SESSION_TTL = 5 * 60;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -325,11 +329,15 @@ public class AgentChatServiceImpl implements AgentChatService {
|
|||||||
sessionData.put("service", prepareData.getService());
|
sessionData.put("service", prepareData.getService());
|
||||||
sessionData.put("isGuest", isGuest);
|
sessionData.put("isGuest", isGuest);
|
||||||
sessionData.put("inputsMap", inputsMap); // 存储处理好的 inputs
|
sessionData.put("inputsMap", inputsMap); // 存储处理好的 inputs
|
||||||
|
sessionData.put("appType", prepareData.getAppType()); // 存储应用类型
|
||||||
|
|
||||||
String cacheKey = CHAT_SESSION_PREFIX + sessionId;
|
// 根据应用类型选择不同的Redis key前缀
|
||||||
|
String appType = prepareData.getAppType();
|
||||||
|
String prefix = "workflow".equals(appType) ? WORKFLOW_SESSION_PREFIX : CHAT_SESSION_PREFIX;
|
||||||
|
String cacheKey = prefix + sessionId;
|
||||||
redisService.set(cacheKey, sessionData, SESSION_TTL, TimeUnit.SECONDS);
|
redisService.set(cacheKey, sessionData, SESSION_TTL, TimeUnit.SECONDS);
|
||||||
|
|
||||||
logger.info("准备对话会话: sessionId={}, agentId={}", sessionId, agentId);
|
logger.info("准备{}会话: sessionId={}, agentId={}", appType, sessionId, agentId);
|
||||||
return ResultDomain.success("准备成功", sessionId);
|
return ResultDomain.success("准备成功", sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,6 +487,109 @@ public class AgentChatServiceImpl implements AgentChatService {
|
|||||||
return emitter;
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<String> blockingChatMessageWithSession(String sessionId) {
|
||||||
|
try {
|
||||||
|
// 1. 从Redis获取会话数据
|
||||||
|
String cacheKey = CHAT_SESSION_PREFIX + sessionId;
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> sessionData = redisService.get(cacheKey, Map.class);
|
||||||
|
|
||||||
|
if (sessionData == null) {
|
||||||
|
return ResultDomain.failure("会话已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 解析会话数据
|
||||||
|
String agentId = (String) sessionData.get("agentId");
|
||||||
|
String chatId = (String) sessionData.get("chatId");
|
||||||
|
String query = (String) sessionData.get("query");
|
||||||
|
String userId = (String) sessionData.get("userId");
|
||||||
|
String apiKey = (String) sessionData.get("apiKey");
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> inputsMap = (Map<String, Object>) sessionData.get("inputsMap");
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<DifyFileInfo> filesData = (List<DifyFileInfo>) sessionData.get("filesData");
|
||||||
|
|
||||||
|
// 3. 删除已使用的会话数据
|
||||||
|
redisService.delete(cacheKey);
|
||||||
|
|
||||||
|
// 4. 保存用户消息(如果有 chatId 的话)
|
||||||
|
if (StringUtils.hasText(chatId)) {
|
||||||
|
String userMessageId = IdUtil.getSnowflakeId();
|
||||||
|
TbChatMessage userMessage = new TbChatMessage();
|
||||||
|
userMessage.setOptsn(IdUtil.getOptsn());
|
||||||
|
userMessage.setMessageId(userMessageId);
|
||||||
|
userMessage.setChatId(chatId);
|
||||||
|
userMessage.setRole("user");
|
||||||
|
userMessage.setContent(query);
|
||||||
|
|
||||||
|
// 提取系统文件ID列表保存到消息中
|
||||||
|
if (filesData != null && !filesData.isEmpty()) {
|
||||||
|
List<String> sysFileIds = filesData.stream()
|
||||||
|
.map(DifyFileInfo::getSysFileId)
|
||||||
|
.filter(StringUtils::hasText)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
if (!sysFileIds.isEmpty()) {
|
||||||
|
userMessage.setFiles(sysFileIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chatMessageMapper.insertChatMessage(userMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 构建Dify请求
|
||||||
|
ChatRequest chatRequest = new ChatRequest();
|
||||||
|
chatRequest.setQuery(query);
|
||||||
|
chatRequest.setUser(userId);
|
||||||
|
chatRequest.setResponseMode("blocking");
|
||||||
|
|
||||||
|
// 使用从Redis获取的inputsMap,如果为空则创建新的
|
||||||
|
if (inputsMap == null) {
|
||||||
|
inputsMap = new HashMap<>();
|
||||||
|
}
|
||||||
|
chatRequest.setInputs(inputsMap); // Dify API 要求 inputs 必传
|
||||||
|
|
||||||
|
if (filesData != null && !filesData.isEmpty()) {
|
||||||
|
chatRequest.setFiles(filesData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 调用Dify阻塞式接口
|
||||||
|
logger.info("调用Dify阻塞式接口: agentId={}, userId={}", agentId, userId);
|
||||||
|
ChatResponse chatResponse = difyApiClient.blockingChat(chatRequest, apiKey);
|
||||||
|
|
||||||
|
if (chatResponse == null || chatResponse.getAnswer() == null) {
|
||||||
|
return ResultDomain.failure("工作流返回结果为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
String answer = chatResponse.getAnswer();
|
||||||
|
|
||||||
|
// 7. 保存AI回复消息(如果有 chatId 的话)
|
||||||
|
if (StringUtils.hasText(chatId)) {
|
||||||
|
String aiMessageId = IdUtil.getSnowflakeId();
|
||||||
|
TbChatMessage aiMessage = new TbChatMessage();
|
||||||
|
aiMessage.setOptsn(IdUtil.getOptsn());
|
||||||
|
aiMessage.setMessageId(aiMessageId);
|
||||||
|
aiMessage.setDifyMessageId(chatResponse.getMessageId());
|
||||||
|
aiMessage.setChatId(chatId);
|
||||||
|
aiMessage.setRole("ai");
|
||||||
|
aiMessage.setContent(answer);
|
||||||
|
chatMessageMapper.insertChatMessage(aiMessage);
|
||||||
|
|
||||||
|
logger.info("阻塞式对话完成: chatId={}, aiMessageId={}", chatId, aiMessageId);
|
||||||
|
} else {
|
||||||
|
logger.info("阻塞式对话完成(无chatId): userId={}", userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResultDomain.success("对话成功", answer);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("阻塞式对话异常: sessionId={}", sessionId, e);
|
||||||
|
return ResultDomain.failure("对话失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResultDomain<Boolean> stopChatMessageByTaskId(TbChat filter, String taskId) {
|
public ResultDomain<Boolean> stopChatMessageByTaskId(TbChat filter, String taskId) {
|
||||||
// 1. 获取智能体
|
// 1. 获取智能体
|
||||||
@@ -547,4 +658,116 @@ public class AgentChatServiceImpl implements AgentChatService {
|
|||||||
|
|
||||||
return ResultDomain.failure("评价失败");
|
return ResultDomain.failure("评价失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<String> runWorkflowWithSession(String sessionId) {
|
||||||
|
try {
|
||||||
|
// 1. 从Redis获取会话数据(使用workflow前缀)
|
||||||
|
String cacheKey = WORKFLOW_SESSION_PREFIX + sessionId;
|
||||||
|
Map<String, Object> sessionData = redisService.get(cacheKey, Map.class);
|
||||||
|
|
||||||
|
if (sessionData == null) {
|
||||||
|
return ResultDomain.failure("会话已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 解析会话数据
|
||||||
|
String agentId = (String) sessionData.get("agentId");
|
||||||
|
String userId = (String) sessionData.get("userId");
|
||||||
|
String apiKey = (String) sessionData.get("apiKey");
|
||||||
|
Map<String, Object> inputsMap = (Map<String, Object>) sessionData.get("inputsMap");
|
||||||
|
|
||||||
|
// 3. 删除已使用的会话数据
|
||||||
|
redisService.delete(cacheKey);
|
||||||
|
|
||||||
|
// 4. 构建工作流请求
|
||||||
|
WorkflowRunRequest workflowRequest = new WorkflowRunRequest();
|
||||||
|
workflowRequest.setInputs(inputsMap != null ? inputsMap : new HashMap<>());
|
||||||
|
workflowRequest.setResponseMode("blocking");
|
||||||
|
workflowRequest.setUser(userId);
|
||||||
|
|
||||||
|
logger.info("执行工作流: agentId={}, userId={}, sessionId={}", agentId, userId, sessionId);
|
||||||
|
|
||||||
|
// 5. 调用Dify工作流接口
|
||||||
|
WorkflowRunResponse workflowResponse = difyApiClient.runWorkflowBlocking(workflowRequest, apiKey);
|
||||||
|
|
||||||
|
if (workflowResponse == null || workflowResponse.getData() == null) {
|
||||||
|
return ResultDomain.failure("工作流执行失败:返回结果为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 检查工作流执行状态
|
||||||
|
String status = workflowResponse.getData().getStatus();
|
||||||
|
if (!"succeeded".equals(status)) {
|
||||||
|
String error = workflowResponse.getData().getError();
|
||||||
|
logger.error("工作流执行失败: status={}, error={}", status, error);
|
||||||
|
return ResultDomain.failure("工作流执行失败: " + (error != null ? error : status));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 提取outputs
|
||||||
|
Map<String, Object> outputs = workflowResponse.getData().getOutputs();
|
||||||
|
if (outputs == null) {
|
||||||
|
return ResultDomain.failure("工作流执行失败:outputs为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 将outputs转为JSON字符串返回
|
||||||
|
String outputsJson = JSON.toJSONString(outputs);
|
||||||
|
logger.info("工作流执行成功: agentId={}, workflowRunId={}", agentId, workflowResponse.getWorkflowRunId());
|
||||||
|
|
||||||
|
return ResultDomain.success("工作流执行成功", outputsJson);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("工作流执行异常: sessionId={}", sessionId, e);
|
||||||
|
return ResultDomain.failure("工作流执行异常: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<String> runWorkflowBlocking(String agentId, Map<String, Object> inputs, String userId) {
|
||||||
|
try {
|
||||||
|
// 1. 获取智能体信息
|
||||||
|
ResultDomain<TbAgent> agentResult = agentService.selectAgentById(agentId);
|
||||||
|
if (!agentResult.getSuccess() || agentResult.getData() == null) {
|
||||||
|
return ResultDomain.failure("智能体不存在");
|
||||||
|
}
|
||||||
|
TbAgent agent = agentResult.getData();
|
||||||
|
|
||||||
|
// 2. 构建工作流请求
|
||||||
|
WorkflowRunRequest workflowRequest = new WorkflowRunRequest();
|
||||||
|
workflowRequest.setInputs(inputs);
|
||||||
|
workflowRequest.setResponseMode("blocking");
|
||||||
|
workflowRequest.setUser(userId);
|
||||||
|
|
||||||
|
logger.info("执行工作流: agentId={}, userId={}, inputs={}", agentId, userId, JSON.toJSONString(inputs));
|
||||||
|
|
||||||
|
// 3. 调用Dify工作流接口
|
||||||
|
WorkflowRunResponse workflowResponse = difyApiClient.runWorkflowBlocking(workflowRequest, agent.getApiKey());
|
||||||
|
|
||||||
|
if (workflowResponse == null || workflowResponse.getData() == null) {
|
||||||
|
return ResultDomain.failure("工作流执行失败:返回结果为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 检查工作流执行状态
|
||||||
|
String status = workflowResponse.getData().getStatus();
|
||||||
|
if (!"succeeded".equals(status)) {
|
||||||
|
String error = workflowResponse.getData().getError();
|
||||||
|
logger.error("工作流执行失败: status={}, error={}", status, error);
|
||||||
|
return ResultDomain.failure("工作流执行失败: " + (error != null ? error : status));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 提取outputs
|
||||||
|
Map<String, Object> outputs = workflowResponse.getData().getOutputs();
|
||||||
|
if (outputs == null) {
|
||||||
|
return ResultDomain.failure("工作流执行失败:outputs为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 将outputs转为JSON字符串返回
|
||||||
|
String outputsJson = JSON.toJSONString(outputs);
|
||||||
|
logger.info("工作流执行成功: agentId={}, workflowRunId={}", agentId, workflowResponse.getWorkflowRunId());
|
||||||
|
|
||||||
|
return ResultDomain.success("工作流执行成功", outputsJson);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("工作流执行异常: agentId={}, userId={}", agentId, userId, e);
|
||||||
|
return ResultDomain.failure("工作流执行异常: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,4 +35,7 @@ public class ChatPrepareData implements Serializable {
|
|||||||
|
|
||||||
@Schema(description = "智能体输入参数,不同智能体可能需要不同的输入参数")
|
@Schema(description = "智能体输入参数,不同智能体可能需要不同的输入参数")
|
||||||
private Map<String, Object> inputsMap;
|
private Map<String, Object> inputsMap;
|
||||||
|
|
||||||
|
@Schema(description = "应用类型(chat=对话应用,workflow=工作流应用),默认为chat")
|
||||||
|
private String appType = "chat";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,13 @@ public interface AgentChatService {
|
|||||||
*/
|
*/
|
||||||
SseEmitter streamChatMessageWithSse(String sessionId);
|
SseEmitter streamChatMessageWithSse(String sessionId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阻塞式对话 - 使用sessionId进行同步调用,等待完整结果返回
|
||||||
|
* @param sessionId 会话标识
|
||||||
|
* @return ResultDomain<String> 返回AI回复的完整内容
|
||||||
|
*/
|
||||||
|
ResultDomain<String> blockingChatMessageWithSession(String sessionId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停止对话生成(通过Dify TaskID)
|
* 停止对话生成(通过Dify TaskID)
|
||||||
* @param filter 会话过滤条件(包含agentId, userId, userType)
|
* @param filter 会话过滤条件(包含agentId, userId, userType)
|
||||||
@@ -91,5 +98,23 @@ public interface AgentChatService {
|
|||||||
*/
|
*/
|
||||||
ResultDomain<Boolean> commentChatMessage(TbChat filter, String messageId, String comment);
|
ResultDomain<Boolean> commentChatMessage(TbChat filter, String messageId, String comment);
|
||||||
|
|
||||||
|
// ====================== 工作流执行 ======================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流执行(阻塞模式)- 使用sessionId进行同步调用,等待完整结果返回
|
||||||
|
* @param sessionId 会话标识(从prepareChatMessageSession返回)
|
||||||
|
* @return ResultDomain<String> 返回工作流输出结果的JSON字符串
|
||||||
|
*/
|
||||||
|
ResultDomain<String> runWorkflowWithSession(String sessionId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行工作流(阻塞模式)- 直接传入inputs执行工作流
|
||||||
|
* @param agentId 智能体ID(用于获取API Key)
|
||||||
|
* @param inputs 工作流输入参数
|
||||||
|
* @param userId 用户标识
|
||||||
|
* @return ResultDomain<String> 返回工作流输出结果的JSON字符串
|
||||||
|
*/
|
||||||
|
ResultDomain<String> runWorkflowBlocking(String agentId, java.util.Map<String, Object> inputs, String userId);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.xyzh.api.workcase.dto;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 聊天室总结请求参数
|
||||||
|
* @filename ChatRoomSummaryRequest.java
|
||||||
|
* @author system
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2026-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "聊天室总结请求参数")
|
||||||
|
public class ChatRoomSummaryRequest implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "聊天室ID", required = true)
|
||||||
|
private String roomId;
|
||||||
|
|
||||||
|
@Schema(description = "是否包含系统消息", defaultValue = "false")
|
||||||
|
private Boolean includeSystemMessages = false;
|
||||||
|
|
||||||
|
@Schema(description = "是否包含会议消息", defaultValue = "false")
|
||||||
|
private Boolean includeMeetingMessages = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package org.xyzh.api.workcase.dto;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 聊天室总结响应结果
|
||||||
|
* @filename ChatRoomSummaryResponse.java
|
||||||
|
* @author system
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2026-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "聊天室总结响应结果")
|
||||||
|
public class ChatRoomSummaryResponse implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "用户提出的核心问题")
|
||||||
|
private String question;
|
||||||
|
|
||||||
|
@Schema(description = "用户的核心诉求列表")
|
||||||
|
private List<String> needs;
|
||||||
|
|
||||||
|
@Schema(description = "解决方案或答案")
|
||||||
|
private String answer;
|
||||||
|
|
||||||
|
@Schema(description = "词云关键词列表")
|
||||||
|
private List<String> workcloud;
|
||||||
|
|
||||||
|
@Schema(description = "聊天室ID")
|
||||||
|
private String roomId;
|
||||||
|
|
||||||
|
@Schema(description = "总结生成时间")
|
||||||
|
private String summaryTime;
|
||||||
|
|
||||||
|
@Schema(description = "参与总结的消息数量")
|
||||||
|
private Integer messageCount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package org.xyzh.api.workcase.dto;
|
||||||
|
|
||||||
|
import org.xyzh.common.dto.BaseDTO;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 聊天室总结表对象
|
||||||
|
* @filename TbChatRoomSummaryDTO.java
|
||||||
|
* @author system
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2026-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "聊天室总结表对象")
|
||||||
|
public class TbChatRoomSummaryDTO extends BaseDTO {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "总结ID")
|
||||||
|
private String summaryId;
|
||||||
|
|
||||||
|
@Schema(description = "聊天室ID")
|
||||||
|
private String roomId;
|
||||||
|
|
||||||
|
@Schema(description = "核心问题")
|
||||||
|
private String question;
|
||||||
|
|
||||||
|
@Schema(description = "核心诉求数组")
|
||||||
|
private List<String> needs;
|
||||||
|
|
||||||
|
@Schema(description = "解决方案")
|
||||||
|
private String answer;
|
||||||
|
|
||||||
|
@Schema(description = "词云关键词数组")
|
||||||
|
private List<String> workcloud;
|
||||||
|
|
||||||
|
@Schema(description = "参与总结的消息数量")
|
||||||
|
private Integer messageCount;
|
||||||
|
|
||||||
|
@Schema(description = "总结生成时间")
|
||||||
|
private String summaryTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package org.xyzh.api.workcase.service;
|
||||||
|
|
||||||
|
import org.xyzh.api.workcase.dto.ChatRoomSummaryRequest;
|
||||||
|
import org.xyzh.api.workcase.dto.ChatRoomSummaryResponse;
|
||||||
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 智能体服务接口,提供AI相关的业务功能
|
||||||
|
* @filename AgentService.java
|
||||||
|
* @author system
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2026-01-01
|
||||||
|
*/
|
||||||
|
public interface AgentService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 总结聊天室对话内容
|
||||||
|
* @param request 聊天室总结请求参数
|
||||||
|
* @return 总结结果
|
||||||
|
* @author system
|
||||||
|
* @since 2026-01-01
|
||||||
|
*/
|
||||||
|
ResultDomain<ChatRoomSummaryResponse> summaryChatRoom(ChatRoomSummaryRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取聊天室最新的总结
|
||||||
|
* @param roomId 聊天室ID
|
||||||
|
* @return 总结结果
|
||||||
|
* @author system
|
||||||
|
* @since 2026-01-01
|
||||||
|
*/
|
||||||
|
ResultDomain<ChatRoomSummaryResponse> getLatestSummary(String roomId);
|
||||||
|
}
|
||||||
268
urbanLifelineServ/dify/会话总结.yml
Normal file
268
urbanLifelineServ/dify/会话总结.yml
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
app:
|
||||||
|
description: ''
|
||||||
|
icon: 🤖
|
||||||
|
icon_background: '#FFEAD5'
|
||||||
|
mode: workflow
|
||||||
|
name: 会话总结
|
||||||
|
use_icon_as_answer_icon: false
|
||||||
|
dependencies:
|
||||||
|
- current_identifier: null
|
||||||
|
type: marketplace
|
||||||
|
value:
|
||||||
|
marketplace_plugin_unique_identifier: langgenius/siliconflow:0.0.38@4795747d4fca05fee9daf34b1bcc110ffbbfcd9112f5f9e914f90b8b5dd549e5
|
||||||
|
version: null
|
||||||
|
kind: app
|
||||||
|
version: 0.5.0
|
||||||
|
workflow:
|
||||||
|
conversation_variables: []
|
||||||
|
environment_variables: []
|
||||||
|
features:
|
||||||
|
file_upload:
|
||||||
|
allowed_file_extensions:
|
||||||
|
- .JPG
|
||||||
|
- .JPEG
|
||||||
|
- .PNG
|
||||||
|
- .GIF
|
||||||
|
- .WEBP
|
||||||
|
- .SVG
|
||||||
|
allowed_file_types:
|
||||||
|
- image
|
||||||
|
allowed_file_upload_methods:
|
||||||
|
- local_file
|
||||||
|
- remote_url
|
||||||
|
enabled: false
|
||||||
|
fileUploadConfig:
|
||||||
|
audio_file_size_limit: 50
|
||||||
|
batch_count_limit: 5
|
||||||
|
file_size_limit: 500
|
||||||
|
image_file_batch_limit: 10
|
||||||
|
image_file_size_limit: 10
|
||||||
|
single_chunk_attachment_limit: 10
|
||||||
|
video_file_size_limit: 100
|
||||||
|
workflow_file_upload_limit: 10
|
||||||
|
image:
|
||||||
|
enabled: false
|
||||||
|
number_limits: 3
|
||||||
|
transfer_methods:
|
||||||
|
- local_file
|
||||||
|
- remote_url
|
||||||
|
number_limits: 3
|
||||||
|
opening_statement: ''
|
||||||
|
retriever_resource:
|
||||||
|
enabled: true
|
||||||
|
sensitive_word_avoidance:
|
||||||
|
enabled: false
|
||||||
|
speech_to_text:
|
||||||
|
enabled: false
|
||||||
|
suggested_questions: []
|
||||||
|
suggested_questions_after_answer:
|
||||||
|
enabled: false
|
||||||
|
text_to_speech:
|
||||||
|
enabled: false
|
||||||
|
language: ''
|
||||||
|
voice: ''
|
||||||
|
graph:
|
||||||
|
edges:
|
||||||
|
- data:
|
||||||
|
isInIteration: false
|
||||||
|
isInLoop: false
|
||||||
|
sourceType: start
|
||||||
|
targetType: code
|
||||||
|
id: 1767170626348-source-1767170986690-target
|
||||||
|
source: '1767170626348'
|
||||||
|
sourceHandle: source
|
||||||
|
target: '1767170986690'
|
||||||
|
targetHandle: target
|
||||||
|
type: custom
|
||||||
|
zIndex: 0
|
||||||
|
- data:
|
||||||
|
isInLoop: false
|
||||||
|
sourceType: code
|
||||||
|
targetType: llm
|
||||||
|
id: 1767170986690-source-1767170825066-target
|
||||||
|
source: '1767170986690'
|
||||||
|
sourceHandle: source
|
||||||
|
target: '1767170825066'
|
||||||
|
targetHandle: target
|
||||||
|
type: custom
|
||||||
|
zIndex: 0
|
||||||
|
- data:
|
||||||
|
isInIteration: false
|
||||||
|
isInLoop: false
|
||||||
|
sourceType: llm
|
||||||
|
targetType: end
|
||||||
|
id: 1767170825066-source-1767173247791-target
|
||||||
|
source: '1767170825066'
|
||||||
|
sourceHandle: source
|
||||||
|
target: '1767173247791'
|
||||||
|
targetHandle: target
|
||||||
|
type: custom
|
||||||
|
zIndex: 0
|
||||||
|
nodes:
|
||||||
|
- data:
|
||||||
|
selected: false
|
||||||
|
title: 用户输入
|
||||||
|
type: start
|
||||||
|
variables:
|
||||||
|
- default: ''
|
||||||
|
hint: ''
|
||||||
|
label: 聊天室对话数据
|
||||||
|
max_length: 99999
|
||||||
|
options: []
|
||||||
|
placeholder: ''
|
||||||
|
required: true
|
||||||
|
type: paragraph
|
||||||
|
variable: chatMessages
|
||||||
|
height: 109
|
||||||
|
id: '1767170626348'
|
||||||
|
position:
|
||||||
|
x: -40
|
||||||
|
y: 267
|
||||||
|
positionAbsolute:
|
||||||
|
x: -40
|
||||||
|
y: 267
|
||||||
|
selected: false
|
||||||
|
sourcePosition: right
|
||||||
|
targetPosition: left
|
||||||
|
type: custom
|
||||||
|
width: 242
|
||||||
|
- data:
|
||||||
|
context:
|
||||||
|
enabled: true
|
||||||
|
variable_selector:
|
||||||
|
- '1767170986690'
|
||||||
|
- result
|
||||||
|
model:
|
||||||
|
completion_params:
|
||||||
|
temperature: 0.7
|
||||||
|
mode: chat
|
||||||
|
name: Qwen/Qwen2.5-VL-72B-Instruct
|
||||||
|
provider: langgenius/siliconflow/siliconflow
|
||||||
|
prompt_template:
|
||||||
|
- id: fd7bffa7-97b7-4a7e-a47f-04dfbd15eca6
|
||||||
|
role: system
|
||||||
|
text: '# 角色定义
|
||||||
|
|
||||||
|
你是一个专业的聊天室总结助手,严格按照要求输出指定格式的JSON内容,不输出任何多余文字、注释、换行。
|
||||||
|
|
||||||
|
聊天室角色说明:guest=用户、ai=智能助手、agent=人工客服,聊天消息已按send_time时间正序排列。
|
||||||
|
|
||||||
|
|
||||||
|
# 输出规则(必须严格遵守,违反则任务失败)
|
||||||
|
|
||||||
|
1. 必须输出标准JSON字符串,仅包含 {"question":"","needs":[""],"answer":""} 三个字段,无其他字段、无多余内容;
|
||||||
|
|
||||||
|
2. question:提炼用户(guest)的核心问题,无业务问题则填"用户无明确业务问题,仅进行友好问候";
|
||||||
|
|
||||||
|
3. needs:提取用户的核心诉求,格式为数组,无诉求则填空数组[],仅保留业务相关诉求,过滤问候语;
|
||||||
|
|
||||||
|
4. answer:整理有效解答(优先ai/agent回复),无有效解答则填"暂无有效解答,需用户补充更具体的问题或背景信息";
|
||||||
|
|
||||||
|
5. JSON中禁止出现换行、多余空格,content中的特殊字符/引号自动转义,确保JSON语法合规。
|
||||||
|
|
||||||
|
|
||||||
|
# 输出格式(唯一合法格式,必须原样输出)
|
||||||
|
|
||||||
|
{"question":"用户提出的问题描述", "needs":["客户诉求1","客户诉求2"],"answer":"解决方案"}
|
||||||
|
|
||||||
|
|
||||||
|
# 聊天上下文(完整带角色,已排序)
|
||||||
|
|
||||||
|
{{#context#}}'
|
||||||
|
selected: false
|
||||||
|
structured_output:
|
||||||
|
schema:
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
answer:
|
||||||
|
type: string
|
||||||
|
needs:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
question:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- question
|
||||||
|
- needs
|
||||||
|
- answer
|
||||||
|
type: object
|
||||||
|
structured_output_enabled: false
|
||||||
|
title: LLM
|
||||||
|
type: llm
|
||||||
|
vision:
|
||||||
|
enabled: false
|
||||||
|
height: 88
|
||||||
|
id: '1767170825066'
|
||||||
|
position:
|
||||||
|
x: 886
|
||||||
|
y: 294
|
||||||
|
positionAbsolute:
|
||||||
|
x: 886
|
||||||
|
y: 294
|
||||||
|
selected: false
|
||||||
|
sourcePosition: right
|
||||||
|
targetPosition: left
|
||||||
|
type: custom
|
||||||
|
width: 242
|
||||||
|
- data:
|
||||||
|
code: "import json\n\ndef main(chatMessages: str):\n # 核心:JSON字符串 转 Python对象/对象数组\n\
|
||||||
|
\ obj_array = json.loads(chatMessages)\n # 返回转换后的对象数组,key自定义为你后续要用的名称(示例用result)\n\
|
||||||
|
\ # {\"senderType\":\"ai\\guest\\staff\",\"content\":\"xxx\",\"send_time\"\
|
||||||
|
:\"xxx\"}\n obj_array_sorted = sorted(obj_array, key=lambda x: x[\"send_time\"\
|
||||||
|
])\n return {\n \"result\": obj_array_sorted\n }"
|
||||||
|
code_language: python3
|
||||||
|
outputs:
|
||||||
|
result:
|
||||||
|
children: null
|
||||||
|
type: array[object]
|
||||||
|
selected: false
|
||||||
|
title: jsonstring转对象数组
|
||||||
|
type: code
|
||||||
|
variables:
|
||||||
|
- value_selector:
|
||||||
|
- '1767170626348'
|
||||||
|
- chatMessages
|
||||||
|
value_type: string
|
||||||
|
variable: chatMessages
|
||||||
|
height: 52
|
||||||
|
id: '1767170986690'
|
||||||
|
position:
|
||||||
|
x: 301
|
||||||
|
y: 308
|
||||||
|
positionAbsolute:
|
||||||
|
x: 301
|
||||||
|
y: 308
|
||||||
|
selected: false
|
||||||
|
sourcePosition: right
|
||||||
|
targetPosition: left
|
||||||
|
type: custom
|
||||||
|
width: 242
|
||||||
|
- data:
|
||||||
|
outputs:
|
||||||
|
- value_selector:
|
||||||
|
- '1767170825066'
|
||||||
|
- text
|
||||||
|
value_type: string
|
||||||
|
variable: text
|
||||||
|
selected: false
|
||||||
|
title: 输出
|
||||||
|
type: end
|
||||||
|
height: 88
|
||||||
|
id: '1767173247791'
|
||||||
|
position:
|
||||||
|
x: 1192
|
||||||
|
y: 294
|
||||||
|
positionAbsolute:
|
||||||
|
x: 1192
|
||||||
|
y: 294
|
||||||
|
selected: true
|
||||||
|
sourcePosition: right
|
||||||
|
targetPosition: left
|
||||||
|
type: custom
|
||||||
|
width: 242
|
||||||
|
viewport:
|
||||||
|
x: -661
|
||||||
|
y: 93.5
|
||||||
|
zoom: 1
|
||||||
|
rag_pipeline_variables: []
|
||||||
@@ -86,7 +86,7 @@ public class AiInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String summaryAgentApiKey = sysConfigService.getStringConfig("dify.workcase.agent.summary");
|
String summaryAgentApiKey = sysConfigService.getStringConfig("dify.workcase.workflow.summary");
|
||||||
TbAgent summaryAgent = new TbAgent();
|
TbAgent summaryAgent = new TbAgent();
|
||||||
summaryAgent.setIsOuter(true);
|
summaryAgent.setIsOuter(true);
|
||||||
summaryAgent.setName("工单总结");
|
summaryAgent.setName("工单总结");
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ 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.TbVideoMeetingDTO;
|
import org.xyzh.api.workcase.dto.TbVideoMeetingDTO;
|
||||||
import org.xyzh.api.workcase.dto.TbWordCloudDTO;
|
import org.xyzh.api.workcase.dto.TbWordCloudDTO;
|
||||||
|
import org.xyzh.api.workcase.dto.ChatRoomSummaryRequest;
|
||||||
|
import org.xyzh.api.workcase.dto.ChatRoomSummaryResponse;
|
||||||
|
import org.xyzh.api.workcase.service.AgentService;
|
||||||
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;
|
||||||
@@ -68,6 +71,9 @@ public class WorkcaseChatController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ChatRoomService chatRoomService;
|
private ChatRoomService chatRoomService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AgentService agentService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private JwtTokenUtil jwtTokenUtil;
|
private JwtTokenUtil jwtTokenUtil;
|
||||||
|
|
||||||
@@ -243,6 +249,31 @@ public class WorkcaseChatController {
|
|||||||
return chatRoomService.deleteMessage(messageId);
|
return chatRoomService.deleteMessage(messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取聊天室最新总结")
|
||||||
|
@PreAuthorize("hasAuthority('workcase:room:view')")
|
||||||
|
@GetMapping("/room/{roomId}/summary")
|
||||||
|
public ResultDomain<ChatRoomSummaryResponse> getLatestSummary(@PathVariable(value = "roomId") String roomId) {
|
||||||
|
return agentService.getLatestSummary(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "生成聊天室对话总结")
|
||||||
|
@PreAuthorize("hasAuthority('workcase:room:view')")
|
||||||
|
@PostMapping("/room/{roomId}/summary")
|
||||||
|
public ResultDomain<ChatRoomSummaryResponse> summaryChatRoom(
|
||||||
|
@PathVariable(value = "roomId") String roomId,
|
||||||
|
@RequestBody(required = false) ChatRoomSummaryRequest request) {
|
||||||
|
// 如果请求体为空,创建一个默认的请求对象
|
||||||
|
if (request == null) {
|
||||||
|
request = new ChatRoomSummaryRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置聊天室ID
|
||||||
|
request.setRoomId(roomId);
|
||||||
|
|
||||||
|
// 调用服务层进行总结
|
||||||
|
return agentService.summaryChatRoom(request);
|
||||||
|
}
|
||||||
|
|
||||||
// ========================= 客服人员管理 =========================
|
// ========================= 客服人员管理 =========================
|
||||||
|
|
||||||
@Operation(summary = "添加客服人员")
|
@Operation(summary = "添加客服人员")
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package org.xyzh.workcase.mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.xyzh.api.workcase.dto.TbChatRoomSummaryDTO;
|
||||||
|
import org.xyzh.common.core.page.PageParam;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 聊天室总结数据访问层
|
||||||
|
* @filename TbChatRoomSummaryMapper.java
|
||||||
|
* @author system
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2026-01-01
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface TbChatRoomSummaryMapper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入聊天室总结
|
||||||
|
*/
|
||||||
|
int insertChatRoomSummary(TbChatRoomSummaryDTO summary);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新聊天室总结(只更新非null字段)
|
||||||
|
*/
|
||||||
|
int updateChatRoomSummary(TbChatRoomSummaryDTO summary);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询聊天室总结
|
||||||
|
*/
|
||||||
|
TbChatRoomSummaryDTO selectChatRoomSummaryById(@Param("summaryId") String summaryId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据聊天室ID查询最新一条总结
|
||||||
|
*/
|
||||||
|
TbChatRoomSummaryDTO selectLatestSummaryByRoomId(@Param("roomId") String roomId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询聊天室总结列表
|
||||||
|
*/
|
||||||
|
List<TbChatRoomSummaryDTO> selectChatRoomSummaryList(@Param("filter") TbChatRoomSummaryDTO filter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询聊天室总结
|
||||||
|
*/
|
||||||
|
List<TbChatRoomSummaryDTO> selectChatRoomSummaryPage(@Param("filter") TbChatRoomSummaryDTO filter, @Param("pageParam") PageParam pageParam);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计聊天室总结数量
|
||||||
|
*/
|
||||||
|
long countChatRoomSummaries(@Param("filter") TbChatRoomSummaryDTO filter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除聊天室总结(逻辑删除)
|
||||||
|
*/
|
||||||
|
int deleteChatRoomSummary(@Param("summaryId") String summaryId);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
package org.xyzh.workcase.service;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
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 com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
|
||||||
|
import org.xyzh.api.ai.dto.ChatPrepareData;
|
||||||
|
import org.xyzh.api.ai.dto.TbAgent;
|
||||||
|
import org.xyzh.api.ai.service.AgentChatService;
|
||||||
|
import org.xyzh.api.system.service.SysConfigService;
|
||||||
|
import org.xyzh.api.workcase.dto.ChatRoomSummaryRequest;
|
||||||
|
import org.xyzh.api.workcase.dto.ChatRoomSummaryResponse;
|
||||||
|
import org.xyzh.api.workcase.dto.TbChatRoomMessageDTO;
|
||||||
|
import org.xyzh.api.workcase.dto.TbChatRoomSummaryDTO;
|
||||||
|
import org.xyzh.api.workcase.dto.TbWordCloudDTO;
|
||||||
|
import org.xyzh.api.workcase.service.AgentService;
|
||||||
|
import org.xyzh.api.workcase.vo.ChatRoomMessageVO;
|
||||||
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
|
import org.xyzh.common.utils.id.IdUtil;
|
||||||
|
import org.xyzh.workcase.mapper.TbChatMessageMapper;
|
||||||
|
import org.xyzh.workcase.mapper.TbChatRoomSummaryMapper;
|
||||||
|
import org.xyzh.workcase.mapper.TbWordCloudMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 智能体服务实现类,提供AI相关的业务功能
|
||||||
|
* @filename AgentServiceImpl.java
|
||||||
|
* @author system
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2026-01-01
|
||||||
|
*/
|
||||||
|
@DubboService(version = "1.0.0", group = "workcase", timeout = 30000, retries = 0)
|
||||||
|
public class AgentServiceImpl implements AgentService {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AgentServiceImpl.class);
|
||||||
|
|
||||||
|
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
// 系统配置键名
|
||||||
|
private static final String CONFIG_KEY_SUMMARY_API = "dify.workcase.workflow.summary";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TbChatMessageMapper chatMessageMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TbWordCloudMapper wordCloudMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TbChatRoomSummaryMapper chatRoomSummaryMapper;
|
||||||
|
|
||||||
|
@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 org.xyzh.api.ai.service.AgentService aiAgentService;
|
||||||
|
|
||||||
|
@DubboReference(version = "1.0.0", group = "ai", timeout = 60000, retries = 0)
|
||||||
|
private AgentChatService agentChatService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<ChatRoomSummaryResponse> summaryChatRoom(ChatRoomSummaryRequest request) {
|
||||||
|
try {
|
||||||
|
logger.info("开始总结聊天室: roomId={}", request.getRoomId());
|
||||||
|
|
||||||
|
// 1. 从系统配置获取API Key
|
||||||
|
String apiKey = sysConfigService.getStringConfig(CONFIG_KEY_SUMMARY_API);
|
||||||
|
if (apiKey == null || apiKey.isEmpty()) {
|
||||||
|
logger.error("未配置聊天室总结工作流的API Key: {}", CONFIG_KEY_SUMMARY_API);
|
||||||
|
return ResultDomain.failure("系统未配置聊天室总结功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 根据API Key查询对应的Agent
|
||||||
|
TbAgent agentFilter = new TbAgent();
|
||||||
|
agentFilter.setApiKey(apiKey);
|
||||||
|
ResultDomain<TbAgent> agentResult = aiAgentService.getAgentList(agentFilter);
|
||||||
|
if (!agentResult.getSuccess() || agentResult.getDataList() == null || agentResult.getDataList().isEmpty()) {
|
||||||
|
logger.error("未找到API Key对应的智能体: {}", apiKey);
|
||||||
|
return ResultDomain.failure("未找到对应的智能体配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
TbAgent agent = agentResult.getDataList().get(0);
|
||||||
|
logger.info("找到智能体: agentId={}, name={}", agent.getAgentId(), agent.getName());
|
||||||
|
|
||||||
|
// 3. 获取聊天室的所有消息
|
||||||
|
TbChatRoomMessageDTO filter = new TbChatRoomMessageDTO();
|
||||||
|
filter.setRoomId(request.getRoomId());
|
||||||
|
|
||||||
|
List<ChatRoomMessageVO> messages = chatMessageMapper.selectChatMessageList(filter);
|
||||||
|
if (messages == null || messages.isEmpty()) {
|
||||||
|
return ResultDomain.failure("聊天室没有消息");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 过滤消息(根据请求参数)
|
||||||
|
List<Map<String, Object>> filteredMessages = new ArrayList<>();
|
||||||
|
for (ChatRoomMessageVO message : messages) {
|
||||||
|
String messageType = message.getMessageType();
|
||||||
|
|
||||||
|
// 根据请求参数决定是否包含系统消息和会议消息
|
||||||
|
boolean shouldInclude = true;
|
||||||
|
if ("system".equals(messageType) && !Boolean.TRUE.equals(request.getIncludeSystemMessages())) {
|
||||||
|
shouldInclude = false;
|
||||||
|
}
|
||||||
|
if ("meeting".equals(messageType) && !Boolean.TRUE.equals(request.getIncludeMeetingMessages())) {
|
||||||
|
shouldInclude = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldInclude) {
|
||||||
|
Map<String, Object> msgMap = new HashMap<>();
|
||||||
|
msgMap.put("senderType", message.getSenderType()); // guest/ai/agent
|
||||||
|
msgMap.put("content", message.getContent());
|
||||||
|
msgMap.put("send_time", DATE_FORMAT.format(message.getSendTime()));
|
||||||
|
filteredMessages.add(msgMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredMessages.isEmpty()) {
|
||||||
|
return ResultDomain.failure("聊天室没有有效的对话消息");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("聊天室 {} 共有 {} 条有效消息", request.getRoomId(), filteredMessages.size());
|
||||||
|
|
||||||
|
// 5. 将消息列表转换为JSON字符串
|
||||||
|
String chatMessagesJson = JSON.toJSONString(filteredMessages);
|
||||||
|
|
||||||
|
// 6. 准备调用工作流的参数
|
||||||
|
ChatPrepareData prepareData = new ChatPrepareData();
|
||||||
|
prepareData.setAgentId(agent.getAgentId());
|
||||||
|
prepareData.setQuery("总结聊天内容");
|
||||||
|
prepareData.setUserId("system_summary");
|
||||||
|
prepareData.setUserType(true); // 作为员工身份调用
|
||||||
|
prepareData.setAppType("workflow"); // 设置为workflow类型
|
||||||
|
|
||||||
|
// 7. 设置工作流的输入参数
|
||||||
|
Map<String, Object> inputsMap = new HashMap<>();
|
||||||
|
inputsMap.put("chatMessages", chatMessagesJson); // 工作流期望的输入参数名
|
||||||
|
prepareData.setInputsMap(inputsMap);
|
||||||
|
|
||||||
|
logger.info("准备工作流会话,输入参数: chatMessages长度={}", chatMessagesJson.length());
|
||||||
|
|
||||||
|
// 8. 调用准备会话
|
||||||
|
ResultDomain<String> prepareResult = agentChatService.prepareChatMessageSession(prepareData);
|
||||||
|
if (!prepareResult.getSuccess()) {
|
||||||
|
logger.error("准备工作流会话失败: {}", prepareResult.getMessage());
|
||||||
|
return ResultDomain.failure("准备会话失败: " + prepareResult.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
String sessionId = prepareResult.getData();
|
||||||
|
logger.info("工作流会话准备成功: sessionId={}", sessionId);
|
||||||
|
|
||||||
|
// 9. 调用工作流执行,获取完整结果
|
||||||
|
ResultDomain<String> workflowResult = agentChatService.runWorkflowWithSession(sessionId);
|
||||||
|
if (!workflowResult.getSuccess()) {
|
||||||
|
logger.error("工作流执行失败: {}", workflowResult.getMessage());
|
||||||
|
return ResultDomain.failure("总结失败: " + workflowResult.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
String outputsJson = workflowResult.getData();
|
||||||
|
logger.debug("工作流返回结果: {}", outputsJson);
|
||||||
|
|
||||||
|
// 10. 解析工作流返回的outputs(JSON格式)
|
||||||
|
JSONObject outputsObject;
|
||||||
|
try {
|
||||||
|
outputsObject = JSON.parseObject(outputsJson);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("解析工作流输出失败: {}", outputsJson, e);
|
||||||
|
return ResultDomain.failure("工作流输出格式错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. 从outputs中获取text字段(工作流的输出节点)
|
||||||
|
String text = outputsObject.getString("text");
|
||||||
|
if (text == null || text.isEmpty()) {
|
||||||
|
logger.error("工作流输出中没有text字段: {}", outputsJson);
|
||||||
|
return ResultDomain.failure("工作流输出缺少text字段");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. 解析text中的JSON结果
|
||||||
|
JSONObject resultJson;
|
||||||
|
try {
|
||||||
|
resultJson = JSON.parseObject(text);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("解析工作流text字段失败: {}", text, e);
|
||||||
|
return ResultDomain.failure("工作流返回结果格式错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 13. 构建响应对象
|
||||||
|
ChatRoomSummaryResponse response = new ChatRoomSummaryResponse();
|
||||||
|
response.setRoomId(request.getRoomId());
|
||||||
|
response.setQuestion(resultJson.getString("question"));
|
||||||
|
|
||||||
|
// 安全获取needs数组
|
||||||
|
List<String> needs = resultJson.getList("needs", String.class);
|
||||||
|
response.setNeeds(needs != null ? needs : new ArrayList<>());
|
||||||
|
|
||||||
|
response.setAnswer(resultJson.getString("answer"));
|
||||||
|
|
||||||
|
// 安全获取workcloud数组
|
||||||
|
List<String> workcloud = resultJson.getList("workcloud", String.class);
|
||||||
|
response.setWorkcloud(workcloud != null ? workcloud : new ArrayList<>());
|
||||||
|
|
||||||
|
response.setSummaryTime(DATE_FORMAT.format(new java.util.Date()));
|
||||||
|
response.setMessageCount(filteredMessages.size());
|
||||||
|
|
||||||
|
// 14. 保存词云数据到数据库
|
||||||
|
if (workcloud != null && !workcloud.isEmpty()) {
|
||||||
|
saveWordCloudData(request.getRoomId(), workcloud);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 15. 保存总结数据到数据库
|
||||||
|
saveChatRoomSummary(request.getRoomId(), resultJson, filteredMessages.size());
|
||||||
|
|
||||||
|
logger.info("聊天室总结完成: roomId={}, question={}, wordcloud数量={}",
|
||||||
|
request.getRoomId(), response.getQuestion(), workcloud != null ? workcloud.size() : 0);
|
||||||
|
|
||||||
|
return ResultDomain.success("总结成功", response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("总结聊天室异常: roomId={}", request.getRoomId(), e);
|
||||||
|
return ResultDomain.failure("总结失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存词云数据到数据库
|
||||||
|
* @param roomId 聊天室ID
|
||||||
|
* @param wordList 词云关键词列表
|
||||||
|
*/
|
||||||
|
private void saveWordCloudData(String roomId, List<String> wordList) {
|
||||||
|
try {
|
||||||
|
String today = new SimpleDateFormat("yyyy-MM-dd").format(new java.util.Date());
|
||||||
|
|
||||||
|
for (String word : wordList) {
|
||||||
|
if (word == null || word.trim().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询是否已存在该词条(同一天、同一分类)
|
||||||
|
TbWordCloudDTO filter = new TbWordCloudDTO();
|
||||||
|
filter.setWord(word.trim());
|
||||||
|
filter.setCategory("chatroom_summary"); // 分类:聊天室总结
|
||||||
|
filter.setStatDate(today);
|
||||||
|
|
||||||
|
TbWordCloudDTO existing = wordCloudMapper.selectWordCloudOne(filter);
|
||||||
|
|
||||||
|
if (existing != null) {
|
||||||
|
// 已存在,增加词频
|
||||||
|
wordCloudMapper.incrementFrequency(existing.getWordId(), 1);
|
||||||
|
logger.debug("词云词频递增: word={}, wordId={}", word, existing.getWordId());
|
||||||
|
} else {
|
||||||
|
// 不存在,插入新词条
|
||||||
|
TbWordCloudDTO newWord = new TbWordCloudDTO();
|
||||||
|
newWord.setWordId(IdUtil.getSnowflakeId());
|
||||||
|
newWord.setOptsn(IdUtil.getOptsn());
|
||||||
|
newWord.setWord(word.trim());
|
||||||
|
newWord.setFrequency("1");
|
||||||
|
newWord.setCategory("chatroom_summary");
|
||||||
|
newWord.setStatDate(today);
|
||||||
|
|
||||||
|
wordCloudMapper.insertWordCloud(newWord);
|
||||||
|
logger.debug("插入新词云: word={}, wordId={}", word, newWord.getWordId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("词云数据保存完成: roomId={}, 词条数量={}", roomId, wordList.size());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("保存词云数据失败: roomId={}", roomId, e);
|
||||||
|
// 词云保存失败不影响总结流程,仅记录日志
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存聊天室总结数据到数据库
|
||||||
|
* @param roomId 聊天室ID
|
||||||
|
* @param resultJson 工作流返回的JSON结果
|
||||||
|
* @param messageCount 参与总结的消息数量
|
||||||
|
*/
|
||||||
|
private void saveChatRoomSummary(String roomId, JSONObject resultJson, int messageCount) {
|
||||||
|
try {
|
||||||
|
TbChatRoomSummaryDTO summary = new TbChatRoomSummaryDTO();
|
||||||
|
summary.setSummaryId(IdUtil.getSnowflakeId());
|
||||||
|
summary.setOptsn(IdUtil.getOptsn());
|
||||||
|
summary.setRoomId(roomId);
|
||||||
|
summary.setQuestion(resultJson.getString("question"));
|
||||||
|
|
||||||
|
// 获取needs数组并转换为String[]
|
||||||
|
List<String> needsList = resultJson.getList("needs", String.class);
|
||||||
|
if (needsList != null && !needsList.isEmpty()) {
|
||||||
|
summary.setNeeds(needsList);
|
||||||
|
} else {
|
||||||
|
summary.setNeeds(new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.setAnswer(resultJson.getString("answer"));
|
||||||
|
|
||||||
|
// 获取workcloud数组并转换为String[]
|
||||||
|
List<String> workcloudList = resultJson.getList("workcloud", String.class);
|
||||||
|
if (workcloudList != null && !workcloudList.isEmpty()) {
|
||||||
|
summary.setWorkcloud(workcloudList);
|
||||||
|
} else {
|
||||||
|
summary.setWorkcloud(new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.setMessageCount(messageCount);
|
||||||
|
summary.setSummaryTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()));
|
||||||
|
summary.setCreator("system");
|
||||||
|
|
||||||
|
chatRoomSummaryMapper.insertChatRoomSummary(summary);
|
||||||
|
|
||||||
|
logger.info("聊天室总结数据保存成功: roomId={}, summaryId={}", roomId, summary.getSummaryId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("保存聊天室总结数据失败: roomId={}", roomId, e);
|
||||||
|
// 总结数据保存失败不影响主流程,仅记录日志
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<ChatRoomSummaryResponse> getLatestSummary(String roomId) {
|
||||||
|
try {
|
||||||
|
logger.info("查询聊天室最新总结: roomId={}", roomId);
|
||||||
|
|
||||||
|
// 查询最新的总结记录
|
||||||
|
TbChatRoomSummaryDTO summary = chatRoomSummaryMapper.selectLatestSummaryByRoomId(roomId);
|
||||||
|
|
||||||
|
if (summary == null) {
|
||||||
|
logger.info("未找到聊天室总结: roomId={}", roomId);
|
||||||
|
return ResultDomain.failure("暂无总结数据");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建响应对象
|
||||||
|
ChatRoomSummaryResponse response = new ChatRoomSummaryResponse();
|
||||||
|
response.setRoomId(summary.getRoomId());
|
||||||
|
response.setQuestion(summary.getQuestion());
|
||||||
|
response.setNeeds(summary.getNeeds());
|
||||||
|
response.setAnswer(summary.getAnswer());
|
||||||
|
response.setWorkcloud(summary.getWorkcloud());
|
||||||
|
response.setSummaryTime(summary.getSummaryTime());
|
||||||
|
response.setMessageCount(summary.getMessageCount());
|
||||||
|
|
||||||
|
logger.info("查询聊天室总结成功: roomId={}, summaryId={}", roomId, summary.getSummaryId());
|
||||||
|
return ResultDomain.success("查询成功", response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("查询聊天室总结异常: roomId={}", roomId, e);
|
||||||
|
return ResultDomain.failure("查询失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="org.xyzh.workcase.mapper.TbChatRoomSummaryMapper">
|
||||||
|
|
||||||
|
<resultMap id="BaseResultMap" type="org.xyzh.api.workcase.dto.TbChatRoomSummaryDTO">
|
||||||
|
<id column="summary_id" property="summaryId" jdbcType="VARCHAR"/>
|
||||||
|
<result column="room_id" property="roomId" jdbcType="VARCHAR"/>
|
||||||
|
<result column="question" property="question" jdbcType="VARCHAR"/>
|
||||||
|
<result column="needs" property="needs" jdbcType="ARRAY" typeHandler="org.xyzh.common.jdbc.handler.StringArrayTypeHandler"/>
|
||||||
|
<result column="answer" property="answer" jdbcType="VARCHAR"/>
|
||||||
|
<result column="workcloud" property="workcloud" jdbcType="ARRAY" typeHandler="org.xyzh.common.jdbc.handler.StringArrayTypeHandler"/>
|
||||||
|
<result column="message_count" property="messageCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="summary_time" property="summaryTime" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="optsn" property="optsn" jdbcType="VARCHAR"/>
|
||||||
|
<result column="creator" property="creator" jdbcType="VARCHAR"/>
|
||||||
|
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="delete_time" property="deleteTime" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="deleted" property="deleted" jdbcType="BOOLEAN"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<sql id="Base_Column_List">
|
||||||
|
summary_id, room_id, question, needs, answer, workcloud, message_count, summary_time,
|
||||||
|
optsn, creator, create_time, update_time, delete_time, deleted
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<insert id="insertChatRoomSummary" parameterType="org.xyzh.api.workcase.dto.TbChatRoomSummaryDTO">
|
||||||
|
INSERT INTO workcase.tb_chat_room_summary (
|
||||||
|
optsn, summary_id, room_id, creator
|
||||||
|
<if test="question != null">, question</if>
|
||||||
|
<if test="needs != null">, needs</if>
|
||||||
|
<if test="answer != null">, answer</if>
|
||||||
|
<if test="workcloud != null">, workcloud</if>
|
||||||
|
<if test="messageCount != null">, message_count</if>
|
||||||
|
<if test="summaryTime != null">, summary_time</if>
|
||||||
|
) VALUES (
|
||||||
|
#{optsn}, #{summaryId}, #{roomId}, #{creator}
|
||||||
|
<if test="question != null">, #{question}</if>
|
||||||
|
<if test="needs != null">, #{needs, jdbcType=ARRAY, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler}</if>
|
||||||
|
<if test="answer != null">, #{answer}</if>
|
||||||
|
<if test="workcloud != null">, #{workcloud, jdbcType=ARRAY, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler}</if>
|
||||||
|
<if test="messageCount != null">, #{messageCount}</if>
|
||||||
|
<if test="summaryTime != null">, #{summaryTime}::timestamptz</if>
|
||||||
|
)
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="updateChatRoomSummary" parameterType="org.xyzh.api.workcase.dto.TbChatRoomSummaryDTO">
|
||||||
|
UPDATE workcase.tb_chat_room_summary
|
||||||
|
<set>
|
||||||
|
<if test="question != null">question = #{question},</if>
|
||||||
|
<if test="needs != null">needs = #{needs, jdbcType=ARRAY, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler},</if>
|
||||||
|
<if test="answer != null">answer = #{answer},</if>
|
||||||
|
<if test="workcloud != null">workcloud = #{workcloud, jdbcType=ARRAY, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler},</if>
|
||||||
|
<if test="messageCount != null">message_count = #{messageCount},</if>
|
||||||
|
<if test="summaryTime != null">summary_time = #{summaryTime}::timestamptz,</if>
|
||||||
|
update_time = now()
|
||||||
|
</set>
|
||||||
|
WHERE summary_id = #{summaryId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<select id="selectChatRoomSummaryById" resultMap="BaseResultMap">
|
||||||
|
SELECT <include refid="Base_Column_List"/>
|
||||||
|
FROM workcase.tb_chat_room_summary
|
||||||
|
WHERE summary_id = #{summaryId} AND deleted = false
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectLatestSummaryByRoomId" resultMap="BaseResultMap">
|
||||||
|
SELECT <include refid="Base_Column_List"/>
|
||||||
|
FROM workcase.tb_chat_room_summary
|
||||||
|
WHERE room_id = #{roomId} AND deleted = false
|
||||||
|
ORDER BY summary_time DESC
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectChatRoomSummaryList" resultMap="BaseResultMap">
|
||||||
|
SELECT <include refid="Base_Column_List"/>
|
||||||
|
FROM workcase.tb_chat_room_summary
|
||||||
|
<where>
|
||||||
|
deleted = false
|
||||||
|
<if test="filter.summaryId != null and filter.summaryId != ''">
|
||||||
|
AND summary_id = #{filter.summaryId}
|
||||||
|
</if>
|
||||||
|
<if test="filter.roomId != null and filter.roomId != ''">
|
||||||
|
AND room_id = #{filter.roomId}
|
||||||
|
</if>
|
||||||
|
<if test="filter.question != null and filter.question != ''">
|
||||||
|
AND question LIKE CONCAT('%', #{filter.question}, '%')
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY summary_time DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectChatRoomSummaryPage" resultMap="BaseResultMap">
|
||||||
|
SELECT <include refid="Base_Column_List"/>
|
||||||
|
FROM workcase.tb_chat_room_summary
|
||||||
|
<where>
|
||||||
|
deleted = false
|
||||||
|
<if test="filter.summaryId != null and filter.summaryId != ''">
|
||||||
|
AND summary_id = #{filter.summaryId}
|
||||||
|
</if>
|
||||||
|
<if test="filter.roomId != null and filter.roomId != ''">
|
||||||
|
AND room_id = #{filter.roomId}
|
||||||
|
</if>
|
||||||
|
<if test="filter.question != null and filter.question != ''">
|
||||||
|
AND question LIKE CONCAT('%', #{filter.question}, '%')
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY summary_time DESC
|
||||||
|
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="countChatRoomSummaries" resultType="long">
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM workcase.tb_chat_room_summary
|
||||||
|
<where>
|
||||||
|
deleted = false
|
||||||
|
<if test="filter.summaryId != null and filter.summaryId != ''">
|
||||||
|
AND summary_id = #{filter.summaryId}
|
||||||
|
</if>
|
||||||
|
<if test="filter.roomId != null and filter.roomId != ''">
|
||||||
|
AND room_id = #{filter.roomId}
|
||||||
|
</if>
|
||||||
|
<if test="filter.question != null and filter.question != ''">
|
||||||
|
AND question LIKE CONCAT('%', #{filter.question}, '%')
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<update id="deleteChatRoomSummary">
|
||||||
|
UPDATE workcase.tb_chat_room_summary
|
||||||
|
SET deleted = true, delete_time = now()
|
||||||
|
WHERE summary_id = #{summaryId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -11,7 +11,9 @@ import type {
|
|||||||
ChatMemberVO,
|
ChatMemberVO,
|
||||||
ChatRoomMessageVO,
|
ChatRoomMessageVO,
|
||||||
CustomerServiceVO,
|
CustomerServiceVO,
|
||||||
VideoMeetingVO
|
VideoMeetingVO,
|
||||||
|
ChatRoomSummaryRequest,
|
||||||
|
ChatRoomSummaryResponse
|
||||||
} from '@/types/workcase'
|
} from '@/types/workcase'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -286,5 +288,29 @@ export const workcaseChatAPI = {
|
|||||||
params: { commentLevel }
|
params: { commentLevel }
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
|
},
|
||||||
|
|
||||||
|
// ====================== 聊天室总结管理 ======================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取聊天室最新总结
|
||||||
|
* @param roomId 聊天室ID
|
||||||
|
*/
|
||||||
|
async getLatestSummary(roomId: string): Promise<ResultDomain<ChatRoomSummaryResponse>> {
|
||||||
|
const response = await api.get<ChatRoomSummaryResponse>(`${this.baseUrl}/room/${roomId}/summary`)
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成聊天室对话总结
|
||||||
|
* @param request 总结请求参数
|
||||||
|
*/
|
||||||
|
async summaryChatRoom(request: ChatRoomSummaryRequest): Promise<ResultDomain<ChatRoomSummaryResponse>> {
|
||||||
|
const { roomId, ...body } = request
|
||||||
|
const response = await api.post<ChatRoomSummaryResponse>(
|
||||||
|
`${this.baseUrl}/room/${roomId}/summary`,
|
||||||
|
Object.keys(body).length > 0 ? body : {}
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -313,4 +313,26 @@ export interface CreateMeetingParam {
|
|||||||
export interface MarkReadParam {
|
export interface MarkReadParam {
|
||||||
roomId: string
|
roomId: string
|
||||||
messageIds?: string[]
|
messageIds?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天室总结请求参数
|
||||||
|
*/
|
||||||
|
export interface ChatRoomSummaryRequest {
|
||||||
|
roomId: string
|
||||||
|
includeSystemMessages?: boolean
|
||||||
|
includeMeetingMessages?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天室总结响应结果
|
||||||
|
*/
|
||||||
|
export interface ChatRoomSummaryResponse {
|
||||||
|
question?: string
|
||||||
|
needs?: string[]
|
||||||
|
answer?: string
|
||||||
|
workcloud?: string[]
|
||||||
|
roomId?: string
|
||||||
|
summaryTime?: string
|
||||||
|
messageCount?: number
|
||||||
}
|
}
|
||||||
@@ -174,26 +174,166 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
|
min-height: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 400px;
|
||||||
|
gap: 16px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 4px solid #e5e7eb;
|
||||||
|
border-top-color: #4b87ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-content {
|
.summary-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 2px solid #e5e7eb;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.regenerate-btn {
|
||||||
|
padding: 6px 16px;
|
||||||
|
background: #fff;
|
||||||
|
color: #4b87ff;
|
||||||
|
border: 1px solid #4b87ff;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #4b87ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-section {
|
.summary-section {
|
||||||
.summary-title {
|
.summary-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: #111827;
|
color: #374151;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 12px;
|
||||||
|
padding-left: 12px;
|
||||||
|
border-left: 3px solid #4b87ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-text {
|
.summary-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #374151;
|
color: #1f2937;
|
||||||
line-height: 1.6;
|
line-height: 1.8;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 24px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1f2937;
|
||||||
|
line-height: 2;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::marker {
|
||||||
|
color: #4b87ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fef3c7;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #92400e;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
&:not(:last-child)::after {
|
||||||
|
content: '•';
|
||||||
|
margin-left: 16px;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 400px;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn {
|
||||||
|
padding: 10px 24px;
|
||||||
|
background: #4b87ff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #3b77ef;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(75, 135, 255, 0.3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,19 +81,55 @@
|
|||||||
|
|
||||||
<!-- 对话纪要 -->
|
<!-- 对话纪要 -->
|
||||||
<div v-else class="summary-container">
|
<div v-else class="summary-container">
|
||||||
<div class="summary-content">
|
<!-- 加载状态 -->
|
||||||
<div class="summary-section">
|
<div v-if="loadingSummary" class="summary-loading">
|
||||||
<div class="summary-title">问题概述</div>
|
<div class="loading-spinner"></div>
|
||||||
|
<span>正在生成对话总结...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 总结内容 -->
|
||||||
|
<div v-else-if="summaryData" class="summary-content">
|
||||||
|
<div class="summary-header">
|
||||||
|
<h3>对话总结</h3>
|
||||||
|
<button class="regenerate-btn" @click="generateSummary">重新生成</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="summaryData.question" class="summary-section">
|
||||||
|
<div class="summary-title">核心问题</div>
|
||||||
<div class="summary-text">
|
<div class="summary-text">
|
||||||
{{ summary.overview || '暂无概述' }}
|
{{ summaryData.question }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-section">
|
|
||||||
<div class="summary-title">客户诉求</div>
|
<div v-if="summaryData.needs && summaryData.needs.length > 0" class="summary-section">
|
||||||
|
<div class="summary-title">核心诉求</div>
|
||||||
|
<ul class="summary-list">
|
||||||
|
<li v-for="(need, index) in summaryData.needs" :key="index">{{ need }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="summaryData.answer" class="summary-section">
|
||||||
|
<div class="summary-title">解决方案</div>
|
||||||
<div class="summary-text">
|
<div class="summary-text">
|
||||||
{{ summary.demand || '暂无诉求' }}
|
{{ summaryData.answer }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-meta">
|
||||||
|
<span v-if="summaryData.messageCount">
|
||||||
|
基于 {{ summaryData.messageCount }} 条消息生成
|
||||||
|
</span>
|
||||||
|
<span v-if="summaryData.summaryTime">
|
||||||
|
生成时间:{{ summaryData.summaryTime }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-else class="summary-empty">
|
||||||
|
<div class="empty-icon">📝</div>
|
||||||
|
<div class="empty-text">暂无对话总结</div>
|
||||||
|
<button class="generate-btn" @click="generateSummary">生成总结</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -101,13 +137,9 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch, computed, nextTick } from 'vue'
|
import { ref, onMounted, watch, computed, nextTick } from 'vue'
|
||||||
import type { ChatRoomVO, ChatRoomMessageVO } from '@/types/workcase/chatRoom'
|
import type { ChatRoomVO, ChatRoomMessageVO, ChatRoomSummaryResponse } from '@/types/workcase/chatRoom'
|
||||||
import { workcaseChatAPI } from '@/api/workcase/workcaseChat'
|
import { workcaseChatAPI } from '@/api/workcase/workcaseChat'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
interface Summary {
|
|
||||||
overview?: string
|
|
||||||
demand?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
chatRoom?: ChatRoomVO
|
chatRoom?: ChatRoomVO
|
||||||
@@ -123,10 +155,8 @@ const activeTab = ref<'record' | 'summary'>('record')
|
|||||||
const messages = ref<ChatRoomMessageVO[]>([])
|
const messages = ref<ChatRoomMessageVO[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const loadingMore = ref(false)
|
const loadingMore = ref(false)
|
||||||
const summary = ref<Summary>({
|
const summaryData = ref<ChatRoomSummaryResponse | null>(null)
|
||||||
overview: '',
|
const loadingSummary = ref(false)
|
||||||
demand: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// 分页相关
|
// 分页相关
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
@@ -241,34 +271,82 @@ const handleScroll = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成对话纪要(可以后续接入AI生成)
|
// 查询最新的对话总结
|
||||||
const generateSummary = () => {
|
const loadLatestSummary = async () => {
|
||||||
if (messages.value.length === 0) return
|
if (!targetRoomId.value) {
|
||||||
|
return
|
||||||
// 简单提取第一条和最后一条消息作为概述
|
}
|
||||||
const firstMsg = messages.value[0]
|
|
||||||
const lastMsg = messages.value[messages.value.length - 1]
|
loadingSummary.value = true
|
||||||
|
summaryData.value = null
|
||||||
summary.value = {
|
|
||||||
overview: `客户反馈:${firstMsg.content?.substring(0, 50) || ''}`,
|
try {
|
||||||
demand: lastMsg.content?.substring(0, 100) || ''
|
const res = await workcaseChatAPI.getLatestSummary(targetRoomId.value)
|
||||||
|
|
||||||
|
if (res.success && res.data) {
|
||||||
|
summaryData.value = res.data
|
||||||
|
} else {
|
||||||
|
// 查询失败,表示还没有总结
|
||||||
|
summaryData.value = null
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('查询总结失败:', error)
|
||||||
|
summaryData.value = null
|
||||||
|
} finally {
|
||||||
|
loadingSummary.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成对话纪要(调用后端AI接口)
|
||||||
|
const generateSummary = async () => {
|
||||||
|
if (!targetRoomId.value) {
|
||||||
|
ElMessage.warning('无效的聊天室ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingSummary.value = true
|
||||||
|
summaryData.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await workcaseChatAPI.summaryChatRoom({
|
||||||
|
roomId: targetRoomId.value,
|
||||||
|
includeSystemMessages: false,
|
||||||
|
includeMeetingMessages: false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.success && res.data) {
|
||||||
|
summaryData.value = res.data
|
||||||
|
ElMessage.success('总结生成成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '生成总结失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成总结失败:', error)
|
||||||
|
ElMessage.error('生成总结失败')
|
||||||
|
} finally {
|
||||||
|
loadingSummary.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadMessages()
|
await loadMessages()
|
||||||
generateSummary()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听 roomId 变化,重新加载数据
|
// 监听 roomId 变化,重新加载数据
|
||||||
watch(targetRoomId, async (newVal) => {
|
watch(targetRoomId, async (newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
messages.value = []
|
messages.value = []
|
||||||
summary.value = { overview: '', demand: '' }
|
summaryData.value = null
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
total.value = 0
|
total.value = 0
|
||||||
await loadMessages()
|
await loadMessages()
|
||||||
generateSummary()
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听切换到对话纪要标签,自动查询总结
|
||||||
|
watch(activeTab, async (newVal) => {
|
||||||
|
if (newVal === 'summary' && !summaryData.value && !loadingSummary.value) {
|
||||||
|
await loadLatestSummary()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user