|
@@ -2,32 +2,49 @@ package com.storlead.knowledge.api;
|
|
|
|
|
|
|
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
|
import com.fasterxml.jackson.core.type.TypeReference;
|
|
import com.fasterxml.jackson.core.type.TypeReference;
|
|
|
|
|
+import com.fasterxml.jackson.databind.JsonNode;
|
|
|
import com.storlead.framework.common.result.Result;
|
|
import com.storlead.framework.common.result.Result;
|
|
|
import com.storlead.knowledge.config.DifyProperties;
|
|
import com.storlead.knowledge.config.DifyProperties;
|
|
|
|
|
+import com.storlead.knowledge.entity.AiMessageEntity;
|
|
|
|
|
+import com.storlead.knowledge.entity.AiMessageReviewEntity;
|
|
|
import com.storlead.knowledge.pojo.dto.ChatDTO;
|
|
import com.storlead.knowledge.pojo.dto.ChatDTO;
|
|
|
-import com.storlead.knowledge.pojo.dto.TagDTO;
|
|
|
|
|
|
|
+import com.storlead.knowledge.pojo.dto.QueryPageDTO;
|
|
|
|
|
+import com.storlead.knowledge.service.AiMessageReviewService;
|
|
|
|
|
+import com.storlead.knowledge.service.AiMessageService;
|
|
|
import com.storlead.knowledge.utils.HttpService;
|
|
import com.storlead.knowledge.utils.HttpService;
|
|
|
import com.storlead.knowledge.utils.JacksonHolder;
|
|
import com.storlead.knowledge.utils.JacksonHolder;
|
|
|
import io.swagger.annotations.Api;
|
|
import io.swagger.annotations.Api;
|
|
|
import io.swagger.annotations.ApiOperation;
|
|
import io.swagger.annotations.ApiOperation;
|
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
import org.springframework.http.MediaType;
|
|
import org.springframework.http.MediaType;
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
import javax.annotation.Resource;
|
|
|
import java.io.IOException;
|
|
import java.io.IOException;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
|
|
|
|
|
|
|
|
@RestController
|
|
@RestController
|
|
|
@RequestMapping("/chat/")
|
|
@RequestMapping("/chat/")
|
|
|
@Api(tags = "知识库: 对话及流程")
|
|
@Api(tags = "知识库: 对话及流程")
|
|
|
public class ChatController {
|
|
public class ChatController {
|
|
|
|
|
+
|
|
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(ChatController.class);
|
|
|
|
|
+
|
|
|
@Resource
|
|
@Resource
|
|
|
private HttpService httpService;
|
|
private HttpService httpService;
|
|
|
|
|
|
|
|
@Resource
|
|
@Resource
|
|
|
private DifyProperties difyProperties;
|
|
private DifyProperties difyProperties;
|
|
|
|
|
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AiMessageService aiMessageService;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AiMessageReviewService aiMessageReviewService;
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 创建聊天 blocking模式
|
|
* 创建聊天 blocking模式
|
|
|
* @param chatDTO
|
|
* @param chatDTO
|
|
@@ -39,7 +56,18 @@ public class ChatController {
|
|
|
public Result<Object> chat(@RequestBody ChatDTO chatDTO) throws JsonProcessingException {
|
|
public Result<Object> chat(@RequestBody ChatDTO chatDTO) throws JsonProcessingException {
|
|
|
String url = difyProperties.getBaseUrl() + "chat-messages";
|
|
String url = difyProperties.getBaseUrl() + "chat-messages";
|
|
|
String body = JacksonHolder.OBJECT_MAPPER.writeValueAsString(chatDTO);
|
|
String body = JacksonHolder.OBJECT_MAPPER.writeValueAsString(chatDTO);
|
|
|
- return httpService.post(url, null, "Bearer "+difyProperties.getAppKeys().get(chatDTO.getAppId()), body, new TypeReference<>() {});
|
|
|
|
|
|
|
+ Result<Object> result = httpService.post(url, null, "Bearer "+chatDTO.getAppId(), body, new TypeReference<>() {});
|
|
|
|
|
+
|
|
|
|
|
+ // 异步保存聊天记录(不影响主流程)
|
|
|
|
|
+ if (result.isSuccess() && result.getResult() != null) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ saveBlockingMessage(chatDTO, result.getResult());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("保存聊天记录失败", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -49,26 +77,163 @@ public class ChatController {
|
|
|
* @return
|
|
* @return
|
|
|
* @throws IOException
|
|
* @throws IOException
|
|
|
*/
|
|
*/
|
|
|
- @PostMapping(value = "/chatStreaming", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
|
|
|
|
|
|
+ @PostMapping(value = "chatStreaming", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
|
|
|
|
+ @ApiOperation("创建聊天 流式返回")
|
|
|
public SseEmitter chatStreaming(@RequestBody ChatDTO chatDTO) throws IOException {
|
|
public SseEmitter chatStreaming(@RequestBody ChatDTO chatDTO) throws IOException {
|
|
|
|
|
|
|
|
SseEmitter emitter = new SseEmitter(0L); // 0 = 不超时
|
|
SseEmitter emitter = new SseEmitter(0L); // 0 = 不超时
|
|
|
|
|
|
|
|
String url = difyProperties.getBaseUrl() + "chat-messages";
|
|
String url = difyProperties.getBaseUrl() + "chat-messages";
|
|
|
|
|
|
|
|
- // 强制开启流式
|
|
|
|
|
- //chatDTO.setStream(true);
|
|
|
|
|
-
|
|
|
|
|
String body = JacksonHolder.OBJECT_MAPPER.writeValueAsString(chatDTO);
|
|
String body = JacksonHolder.OBJECT_MAPPER.writeValueAsString(chatDTO);
|
|
|
-
|
|
|
|
|
httpService.postStream(
|
|
httpService.postStream(
|
|
|
url,
|
|
url,
|
|
|
- "Bearer " + difyProperties.getAppKeys().get(chatDTO.getAppId()),
|
|
|
|
|
|
|
+ "Bearer " +chatDTO.getAppId(),
|
|
|
body,
|
|
body,
|
|
|
- emitter
|
|
|
|
|
|
|
+ emitter,
|
|
|
|
|
+ (lastData) -> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ saveStreamingMessage(chatDTO, lastData);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("保存流式聊天记录失败", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
return emitter;
|
|
return emitter;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ @GetMapping("messages")
|
|
|
|
|
+ @ApiOperation("获取聊天记录(支持关键字搜索和分页,含好评/差评总数)")
|
|
|
|
|
+ public Result<Object> listMessages(QueryPageDTO page) {
|
|
|
|
|
+ return Result.ok(aiMessageService.pageQueryWithReview(page));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @GetMapping("message/{id}")
|
|
|
|
|
+ @ApiOperation("根据ID获取单条聊天记录")
|
|
|
|
|
+ public Result<Object> getMessage(@PathVariable Long id) {
|
|
|
|
|
+ AiMessageEntity entity = aiMessageService.getById(id);
|
|
|
|
|
+ if (entity == null) {
|
|
|
|
|
+ return Result.error("聊天记录不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+ return Result.ok(entity);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @PutMapping("message")
|
|
|
|
|
+ @ApiOperation("更新单条聊天记录")
|
|
|
|
|
+ public Result<Object> updateMessage(@RequestBody AiMessageEntity entity) {
|
|
|
|
|
+ if (entity.getId() == null) {
|
|
|
|
|
+ return Result.error("ID不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ boolean success = aiMessageService.update(entity);
|
|
|
|
|
+ if (!success) {
|
|
|
|
|
+ return Result.error("更新失败,聊天记录不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+ return Result.ok("更新成功");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @GetMapping("message/{id}/review")
|
|
|
|
|
+ @ApiOperation("根据聊天消息ID获取评价记录")
|
|
|
|
|
+ public Result<Object> getReview(@PathVariable Long id) {
|
|
|
|
|
+ AiMessageReviewEntity entity = aiMessageReviewService.getByAiMessageId(id);
|
|
|
|
|
+ if (entity == null) {
|
|
|
|
|
+ return Result.ok(new AiMessageReviewEntity().setAiMessageId(id).setAgree(0).setDisagree(0));
|
|
|
|
|
+ }
|
|
|
|
|
+ return Result.ok(entity);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @PostMapping("message/review")
|
|
|
|
|
+ @ApiOperation("添加或更新评价记录(点赞/差评)")
|
|
|
|
|
+ public Result<Object> saveOrUpdateReview(@RequestBody AiMessageReviewEntity entity) {
|
|
|
|
|
+ if (entity.getAiMessageId() == null) {
|
|
|
|
|
+ return Result.error("aiMessageId不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ // 确保 agree 和 disagree 不为 null
|
|
|
|
|
+ if (entity.getAgree() == null) {
|
|
|
|
|
+ entity.setAgree(0);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (entity.getDisagree() == null) {
|
|
|
|
|
+ entity.setDisagree(0);
|
|
|
|
|
+ }
|
|
|
|
|
+ boolean success = aiMessageReviewService.saveOrUpdateReview(entity);
|
|
|
|
|
+ if (!success) {
|
|
|
|
|
+ return Result.error("操作失败");
|
|
|
|
|
+ }
|
|
|
|
|
+ return Result.ok("操作成功");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @GetMapping("history")
|
|
|
|
|
+ @ApiOperation("获取应用的对话历史")
|
|
|
|
|
+ public Result<Object> list(ChatDTO chatDTO) {
|
|
|
|
|
+ String url = difyProperties.getBaseUrl() + "conversations";
|
|
|
|
|
+ return httpService.get(
|
|
|
|
|
+ url,
|
|
|
|
|
+ Map.of("user", chatDTO.getUser()),
|
|
|
|
|
+ "Bearer "+difyProperties.getAppKeys().get(chatDTO.getAppId()),
|
|
|
|
|
+ new TypeReference<>() {}
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 保存 blocking 模式的聊天记录
|
|
|
|
|
+ */
|
|
|
|
|
+ private void saveBlockingMessage(ChatDTO chatDTO, Object responseData) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ String json = JacksonHolder.OBJECT_MAPPER.writeValueAsString(responseData);
|
|
|
|
|
+ JsonNode root = JacksonHolder.OBJECT_MAPPER.readTree(json);
|
|
|
|
|
+ System.out.println(root);
|
|
|
|
|
+ AiMessageEntity entity = new AiMessageEntity();
|
|
|
|
|
+ entity.setTaskId(getJsonText(root, "task_id"));
|
|
|
|
|
+ entity.setMessageId(getJsonText(root, "message_id"));
|
|
|
|
|
+ entity.setConversationId(getJsonText(root, "conversation_id"));
|
|
|
|
|
+ entity.setAppId(chatDTO.getAppId());
|
|
|
|
|
+ entity.setInputs(chatDTO.getInputs() != null ? JacksonHolder.OBJECT_MAPPER.writeValueAsString(chatDTO.getInputs()) : null);
|
|
|
|
|
+ entity.setQuery(chatDTO.getQuery());
|
|
|
|
|
+ entity.setResponseMode(chatDTO.getResponse_mode());
|
|
|
|
|
+ entity.setUserId(chatDTO.getUser());
|
|
|
|
|
+ entity.setAnswer(getJsonText(root, "answer"));
|
|
|
|
|
+ entity.setCreatedAt(Integer.parseInt(getJsonText(root, "created_at")));
|
|
|
|
|
+
|
|
|
|
|
+ aiMessageService.save(entity);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("解析并保存blocking聊天记录失败", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 保存 streaming 模式的聊天记录(从 message_end 事件中解析)
|
|
|
|
|
+ */
|
|
|
|
|
+ private void saveStreamingMessage(ChatDTO chatDTO, String lastData) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // lastData 格式: {"event":"message_end","task_id":"...","message_id":"...","conversation_id":"...","metadata":{...}}
|
|
|
|
|
+ JsonNode root = JacksonHolder.OBJECT_MAPPER.readTree(lastData);
|
|
|
|
|
+
|
|
|
|
|
+ AiMessageEntity entity = new AiMessageEntity();
|
|
|
|
|
+ entity.setTaskId(getJsonText(root, "task_id"));
|
|
|
|
|
+ entity.setMessageId(getJsonText(root, "message_id"));
|
|
|
|
|
+ entity.setConversationId(getJsonText(root, "conversation_id"));
|
|
|
|
|
+ entity.setAppId(chatDTO.getAppId());
|
|
|
|
|
+ entity.setInputs(chatDTO.getInputs() != null ? JacksonHolder.OBJECT_MAPPER.writeValueAsString(chatDTO.getInputs()) : null);
|
|
|
|
|
+ entity.setQuery(chatDTO.getQuery());
|
|
|
|
|
+ entity.setResponseMode(chatDTO.getResponse_mode());
|
|
|
|
|
+ entity.setUserId(chatDTO.getUser());
|
|
|
|
|
+ // streaming 模式下 answer 在 metadata 中逐句返回,message_end 事件本身不含完整 answer,此处留空
|
|
|
|
|
+ entity.setAnswer(getJsonText(root, "answer"));
|
|
|
|
|
+ entity.setCreatedAt(Integer.parseInt(getJsonText(root, "created_at")));
|
|
|
|
|
+
|
|
|
|
|
+ aiMessageService.save(entity);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("解析并保存streaming聊天记录失败", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 安全获取 JSON 节点文本值
|
|
|
|
|
+ */
|
|
|
|
|
+ private String getJsonText(JsonNode node, String field) {
|
|
|
|
|
+ JsonNode value = node.get(field);
|
|
|
|
|
+ return value != null && !value.isNull() ? value.asText() : null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
}
|
|
}
|
|
|
|
|
+
|