diff --git a/urbanLifelineServ/.bin/database/postgres/sql/createTableWorkcase.sql b/urbanLifelineServ/.bin/database/postgres/sql/createTableWorkcase.sql index 825f42ba..cc2d3d57 100644 --- a/urbanLifelineServ/.bin/database/postgres/sql/createTableWorkcase.sql +++ b/urbanLifelineServ/.bin/database/postgres/sql/createTableWorkcase.sql @@ -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; 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) -- 记录聊天室内创建的视频会议 DROP TABLE IF EXISTS workcase.tb_video_meeting CASCADE; diff --git a/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql b/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql index 0b3677ed..b011712b 100644 --- a/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql +++ b/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql @@ -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), -- 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-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), -- 日志与审计 diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java index 53c84a69..8c7c5780 100644 --- a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java @@ -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); + } + } + /** * 停止对话生成 */ diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/WorkflowRunRequest.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/WorkflowRunRequest.java new file mode 100644 index 00000000..a2661d3f --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/WorkflowRunRequest.java @@ -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 inputs = new java.util.HashMap<>(); + + /** + * 响应模式:streaming(流式)、blocking(阻塞) + */ + @JSONField(name = "response_mode") + private String responseMode = "blocking"; + + /** + * 用户标识 + */ + private String user; + +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/WorkflowRunResponse.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/WorkflowRunResponse.java new file mode 100644 index 00000000..b87a6fee --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/WorkflowRunResponse.java @@ -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 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; + } +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentChatServiceImpl.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentChatServiceImpl.java index 70ca191d..f6e77cf2 100644 --- a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentChatServiceImpl.java +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentChatServiceImpl.java @@ -13,6 +13,9 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.xyzh.ai.client.DifyApiClient; import org.xyzh.ai.client.callback.StreamCallback; 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.mapper.TbChatMapper; 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 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; @Autowired @@ -325,11 +329,15 @@ public class AgentChatServiceImpl implements AgentChatService { sessionData.put("service", prepareData.getService()); sessionData.put("isGuest", isGuest); 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); - logger.info("准备对话会话: sessionId={}, agentId={}", sessionId, agentId); + logger.info("准备{}会话: sessionId={}, agentId={}", appType, sessionId, agentId); return ResultDomain.success("准备成功", sessionId); } @@ -479,6 +487,109 @@ public class AgentChatServiceImpl implements AgentChatService { return emitter; } + @Override + public ResultDomain blockingChatMessageWithSession(String sessionId) { + try { + // 1. 从Redis获取会话数据 + String cacheKey = CHAT_SESSION_PREFIX + sessionId; + @SuppressWarnings("unchecked") + Map 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 inputsMap = (Map) sessionData.get("inputsMap"); + + @SuppressWarnings("unchecked") + List filesData = (List) 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 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 public ResultDomain stopChatMessageByTaskId(TbChat filter, String taskId) { // 1. 获取智能体 @@ -547,4 +658,116 @@ public class AgentChatServiceImpl implements AgentChatService { return ResultDomain.failure("评价失败"); } + + @Override + public ResultDomain runWorkflowWithSession(String sessionId) { + try { + // 1. 从Redis获取会话数据(使用workflow前缀) + String cacheKey = WORKFLOW_SESSION_PREFIX + sessionId; + Map 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 inputsMap = (Map) 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 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 runWorkflowBlocking(String agentId, Map inputs, String userId) { + try { + // 1. 获取智能体信息 + ResultDomain 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 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()); + } + } } diff --git a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/ChatPrepareData.java b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/ChatPrepareData.java index 08c2562d..000fd330 100644 --- a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/ChatPrepareData.java +++ b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/ChatPrepareData.java @@ -35,4 +35,7 @@ public class ChatPrepareData implements Serializable { @Schema(description = "智能体输入参数,不同智能体可能需要不同的输入参数") private Map inputsMap; + + @Schema(description = "应用类型(chat=对话应用,workflow=工作流应用),默认为chat") + private String appType = "chat"; } diff --git a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/AgentChatService.java b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/AgentChatService.java index e4b7f7b6..97d0b6f3 100644 --- a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/AgentChatService.java +++ b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/AgentChatService.java @@ -74,6 +74,13 @@ public interface AgentChatService { */ SseEmitter streamChatMessageWithSse(String sessionId); + /** + * 阻塞式对话 - 使用sessionId进行同步调用,等待完整结果返回 + * @param sessionId 会话标识 + * @return ResultDomain 返回AI回复的完整内容 + */ + ResultDomain blockingChatMessageWithSession(String sessionId); + /** * 停止对话生成(通过Dify TaskID) * @param filter 会话过滤条件(包含agentId, userId, userType) @@ -91,5 +98,23 @@ public interface AgentChatService { */ ResultDomain commentChatMessage(TbChat filter, String messageId, String comment); + // ====================== 工作流执行 ====================== + + /** + * 工作流执行(阻塞模式)- 使用sessionId进行同步调用,等待完整结果返回 + * @param sessionId 会话标识(从prepareChatMessageSession返回) + * @return ResultDomain 返回工作流输出结果的JSON字符串 + */ + ResultDomain runWorkflowWithSession(String sessionId); + + /** + * 执行工作流(阻塞模式)- 直接传入inputs执行工作流 + * @param agentId 智能体ID(用于获取API Key) + * @param inputs 工作流输入参数 + * @param userId 用户标识 + * @return ResultDomain 返回工作流输出结果的JSON字符串 + */ + ResultDomain runWorkflowBlocking(String agentId, java.util.Map inputs, String userId); + } diff --git a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/ChatRoomSummaryRequest.java b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/ChatRoomSummaryRequest.java new file mode 100644 index 00000000..8d503720 --- /dev/null +++ b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/ChatRoomSummaryRequest.java @@ -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; +} \ No newline at end of file diff --git a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/ChatRoomSummaryResponse.java b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/ChatRoomSummaryResponse.java new file mode 100644 index 00000000..1fad457d --- /dev/null +++ b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/ChatRoomSummaryResponse.java @@ -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 needs; + + @Schema(description = "解决方案或答案") + private String answer; + + @Schema(description = "词云关键词列表") + private List workcloud; + + @Schema(description = "聊天室ID") + private String roomId; + + @Schema(description = "总结生成时间") + private String summaryTime; + + @Schema(description = "参与总结的消息数量") + private Integer messageCount; +} \ No newline at end of file diff --git a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/TbChatRoomSummaryDTO.java b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/TbChatRoomSummaryDTO.java new file mode 100644 index 00000000..b58377ad --- /dev/null +++ b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/TbChatRoomSummaryDTO.java @@ -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 needs; + + @Schema(description = "解决方案") + private String answer; + + @Schema(description = "词云关键词数组") + private List workcloud; + + @Schema(description = "参与总结的消息数量") + private Integer messageCount; + + @Schema(description = "总结生成时间") + private String summaryTime; + +} diff --git a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/AgentService.java b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/AgentService.java new file mode 100644 index 00000000..8da41d02 --- /dev/null +++ b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/AgentService.java @@ -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 summaryChatRoom(ChatRoomSummaryRequest request); + + /** + * @description 获取聊天室最新的总结 + * @param roomId 聊天室ID + * @return 总结结果 + * @author system + * @since 2026-01-01 + */ + ResultDomain getLatestSummary(String roomId); +} \ No newline at end of file diff --git a/urbanLifelineServ/dify/会话总结.yml b/urbanLifelineServ/dify/会话总结.yml new file mode 100644 index 00000000..d3b472d0 --- /dev/null +++ b/urbanLifelineServ/dify/会话总结.yml @@ -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: [] diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/AiInit.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/AiInit.java index da407be1..175e00d2 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/AiInit.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/AiInit.java @@ -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(); summaryAgent.setIsOuter(true); summaryAgent.setName("工单总结"); diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatController.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatController.java index 2bc724bf..56358d3b 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatController.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatController.java @@ -21,6 +21,9 @@ import org.xyzh.api.workcase.dto.TbChatRoomMessageDTO; import org.xyzh.api.workcase.dto.TbCustomerServiceDTO; import org.xyzh.api.workcase.dto.TbVideoMeetingDTO; 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.WorkcaseChatService; import org.xyzh.api.workcase.vo.ChatMemberVO; @@ -68,6 +71,9 @@ public class WorkcaseChatController { @Autowired private ChatRoomService chatRoomService; + @Autowired + private AgentService agentService; + @Autowired private JwtTokenUtil jwtTokenUtil; @@ -243,6 +249,31 @@ public class WorkcaseChatController { return chatRoomService.deleteMessage(messageId); } + @Operation(summary = "获取聊天室最新总结") + @PreAuthorize("hasAuthority('workcase:room:view')") + @GetMapping("/room/{roomId}/summary") + public ResultDomain getLatestSummary(@PathVariable(value = "roomId") String roomId) { + return agentService.getLatestSummary(roomId); + } + + @Operation(summary = "生成聊天室对话总结") + @PreAuthorize("hasAuthority('workcase:room:view')") + @PostMapping("/room/{roomId}/summary") + public ResultDomain 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 = "添加客服人员") diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/mapper/TbChatRoomSummaryMapper.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/mapper/TbChatRoomSummaryMapper.java new file mode 100644 index 00000000..40e2b20a --- /dev/null +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/mapper/TbChatRoomSummaryMapper.java @@ -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 selectChatRoomSummaryList(@Param("filter") TbChatRoomSummaryDTO filter); + + /** + * 分页查询聊天室总结 + */ + List selectChatRoomSummaryPage(@Param("filter") TbChatRoomSummaryDTO filter, @Param("pageParam") PageParam pageParam); + + /** + * 统计聊天室总结数量 + */ + long countChatRoomSummaries(@Param("filter") TbChatRoomSummaryDTO filter); + + /** + * 删除聊天室总结(逻辑删除) + */ + int deleteChatRoomSummary(@Param("summaryId") String summaryId); + +} diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/AgentServiceImpl.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/AgentServiceImpl.java new file mode 100644 index 00000000..a2139efc --- /dev/null +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/AgentServiceImpl.java @@ -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 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 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 messages = chatMessageMapper.selectChatMessageList(filter); + if (messages == null || messages.isEmpty()) { + return ResultDomain.failure("聊天室没有消息"); + } + + // 4. 过滤消息(根据请求参数) + List> 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 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 inputsMap = new HashMap<>(); + inputsMap.put("chatMessages", chatMessagesJson); // 工作流期望的输入参数名 + prepareData.setInputsMap(inputsMap); + + logger.info("准备工作流会话,输入参数: chatMessages长度={}", chatMessagesJson.length()); + + // 8. 调用准备会话 + ResultDomain 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 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 needs = resultJson.getList("needs", String.class); + response.setNeeds(needs != null ? needs : new ArrayList<>()); + + response.setAnswer(resultJson.getString("answer")); + + // 安全获取workcloud数组 + List 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 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 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 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 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()); + } + } +} \ No newline at end of file diff --git a/urbanLifelineServ/workcase/src/main/resources/mapper/TbChatRoomSummaryMapper.xml b/urbanLifelineServ/workcase/src/main/resources/mapper/TbChatRoomSummaryMapper.xml new file mode 100644 index 00000000..1ff38749 --- /dev/null +++ b/urbanLifelineServ/workcase/src/main/resources/mapper/TbChatRoomSummaryMapper.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + summary_id, room_id, question, needs, answer, workcloud, message_count, summary_time, + optsn, creator, create_time, update_time, delete_time, deleted + + + + INSERT INTO workcase.tb_chat_room_summary ( + optsn, summary_id, room_id, creator + , question + , needs + , answer + , workcloud + , message_count + , summary_time + ) VALUES ( + #{optsn}, #{summaryId}, #{roomId}, #{creator} + , #{question} + , #{needs, jdbcType=ARRAY, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler} + , #{answer} + , #{workcloud, jdbcType=ARRAY, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler} + , #{messageCount} + , #{summaryTime}::timestamptz + ) + + + + UPDATE workcase.tb_chat_room_summary + + question = #{question}, + needs = #{needs, jdbcType=ARRAY, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler}, + answer = #{answer}, + workcloud = #{workcloud, jdbcType=ARRAY, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler}, + message_count = #{messageCount}, + summary_time = #{summaryTime}::timestamptz, + update_time = now() + + WHERE summary_id = #{summaryId} + + + + + + + + + + + + + + UPDATE workcase.tb_chat_room_summary + SET deleted = true, delete_time = now() + WHERE summary_id = #{summaryId} + + + diff --git a/urbanLifelineWeb/packages/workcase/src/api/workcase/workcaseChat.ts b/urbanLifelineWeb/packages/workcase/src/api/workcase/workcaseChat.ts index 65efbe4a..70fc50d9 100644 --- a/urbanLifelineWeb/packages/workcase/src/api/workcase/workcaseChat.ts +++ b/urbanLifelineWeb/packages/workcase/src/api/workcase/workcaseChat.ts @@ -11,7 +11,9 @@ import type { ChatMemberVO, ChatRoomMessageVO, CustomerServiceVO, - VideoMeetingVO + VideoMeetingVO, + ChatRoomSummaryRequest, + ChatRoomSummaryResponse } from '@/types/workcase' /** @@ -286,5 +288,29 @@ export const workcaseChatAPI = { params: { commentLevel } }) return response.data + }, + + // ====================== 聊天室总结管理 ====================== + + /** + * 获取聊天室最新总结 + * @param roomId 聊天室ID + */ + async getLatestSummary(roomId: string): Promise> { + const response = await api.get(`${this.baseUrl}/room/${roomId}/summary`) + return response.data + }, + + /** + * 生成聊天室对话总结 + * @param request 总结请求参数 + */ + async summaryChatRoom(request: ChatRoomSummaryRequest): Promise> { + const { roomId, ...body } = request + const response = await api.post( + `${this.baseUrl}/room/${roomId}/summary`, + Object.keys(body).length > 0 ? body : {} + ) + return response.data } } diff --git a/urbanLifelineWeb/packages/workcase/src/types/workcase/chatRoom.ts b/urbanLifelineWeb/packages/workcase/src/types/workcase/chatRoom.ts index b63298ec..b2870b18 100644 --- a/urbanLifelineWeb/packages/workcase/src/types/workcase/chatRoom.ts +++ b/urbanLifelineWeb/packages/workcase/src/types/workcase/chatRoom.ts @@ -313,4 +313,26 @@ export interface CreateMeetingParam { export interface MarkReadParam { roomId: 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 } \ No newline at end of file diff --git a/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatMessage/ChatMessage.scss b/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatMessage/ChatMessage.scss index df291511..e0ce76ba 100644 --- a/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatMessage/ChatMessage.scss +++ b/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatMessage/ChatMessage.scss @@ -174,26 +174,166 @@ background: #fff; border: 1px solid #e5e7eb; 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 { display: flex; 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-title { font-size: 14px; - font-weight: 700; - color: #111827; - margin-bottom: 8px; + font-weight: 600; + color: #374151; + margin-bottom: 12px; + padding-left: 12px; + border-left: 3px solid #4b87ff; } .summary-text { font-size: 14px; - color: #374151; - line-height: 1.6; + color: #1f2937; + 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); + } } } diff --git a/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatMessage/ChatMessage.vue b/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatMessage/ChatMessage.vue index d87f9460..7b2989c9 100644 --- a/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatMessage/ChatMessage.vue +++ b/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatMessage/ChatMessage.vue @@ -81,19 +81,55 @@
-
-
-
问题概述
+ +
+
+ 正在生成对话总结... +
+ + +
+
+

对话总结

+ +
+ +
+
核心问题
- {{ summary.overview || '暂无概述' }} + {{ summaryData.question }}
-
-
客户诉求
+ +
+
核心诉求
+
    +
  • {{ need }}
  • +
+
+ +
+
解决方案
- {{ summary.demand || '暂无诉求' }} + {{ summaryData.answer }}
+ +
+ + 基于 {{ summaryData.messageCount }} 条消息生成 + + + 生成时间:{{ summaryData.summaryTime }} + +
+
+ + +
+
📝
+
暂无对话总结
+
@@ -101,13 +137,9 @@