|
|
@@ -1,11 +1,14 @@
|
|
1
|
1
|
package com.huimv.employment.service.conversation;
|
|
2
|
2
|
|
|
|
3
|
+import com.fasterxml.jackson.databind.JsonNode;
|
|
3
|
4
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
4
|
5
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
|
5
|
6
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
6
|
7
|
import com.huimv.employment.dao.entity.FeConversation;
|
|
7
|
8
|
import com.huimv.employment.integration.kb.ConsoleChatCollectResult;
|
|
|
9
|
+import com.huimv.employment.integration.kb.ConsoleChatSseAggregator;
|
|
8
|
10
|
import com.huimv.employment.integration.kb.KbOpenAiProxyService;
|
|
|
11
|
+import com.huimv.employment.integration.kb.SseStreamSupport;
|
|
9
|
12
|
import com.huimv.employment.service.conversation.dto.ConversationChatRequest;
|
|
10
|
13
|
import com.huimv.employment.service.conversation.dto.ParsedAssistantReply;
|
|
11
|
14
|
import org.slf4j.Logger;
|
|
|
@@ -17,10 +20,7 @@ import java.io.IOException;
|
|
17
|
20
|
import java.io.OutputStream;
|
|
18
|
21
|
|
|
19
|
22
|
/**
|
|
20
|
|
- * 会话内 AI 对话:组装上游请求、流式转发并在结束后落库。
|
|
21
|
|
- * <p>
|
|
22
|
|
- * 卡片字段由服务端在 SSE complete 后根据草稿状态写入,不依赖智能体返回 card JSON。
|
|
23
|
|
- * </p>
|
|
|
23
|
+ * 会话内 AI 对话:流式转发;pending 且 missingFields 为空时在 SSE 最后一条消息注入卡片字段后落库。
|
|
24
|
24
|
*/
|
|
25
|
25
|
@Service
|
|
26
|
26
|
public class ConversationChatService {
|
|
|
@@ -48,9 +48,6 @@ public class ConversationChatService {
|
|
48
|
48
|
this.objectMapper = objectMapper;
|
|
49
|
49
|
}
|
|
50
|
50
|
|
|
51
|
|
- /**
|
|
52
|
|
- * 在指定会话内发起一轮 AI 对话,SSE 流结束后写入 user/assistant 消息。
|
|
53
|
|
- */
|
|
54
|
51
|
public void chat(Long userId, Long conversationId, ConversationChatRequest request, String agentId,
|
|
55
|
52
|
OutputStream output) throws IOException {
|
|
56
|
53
|
FeConversation conversation = conversationService.requireOwnedActive(userId, conversationId);
|
|
|
@@ -66,21 +63,92 @@ public class ConversationChatService {
|
|
66
|
63
|
|
|
67
|
64
|
ConsoleChatCollectResult collectResult = kbOpenAiProxyService.streamConsoleChatCollecting(
|
|
68
|
65
|
upstreamBody, agentId, output, objectMapper);
|
|
69
|
|
- // SSE complete(或流正常关闭)后:服务端按 ready 草稿写入卡片,忽略智能体卡片 JSON
|
|
|
66
|
+
|
|
70
|
67
|
ParsedAssistantReply parsed = conversationDraftCardEnricher.attachCardOnComplete(
|
|
71
|
68
|
conversation.getId(), collectResult.getAssistantText());
|
|
72
|
69
|
|
|
|
70
|
+ // 把卡片字段写入「最后一条」SSE 消息本体后再发 [DONE]
|
|
|
71
|
+ writeLastSseMessageWithCard(output, collectResult.getHeldTerminalLine(), parsed);
|
|
|
72
|
+ SseStreamSupport.writeDoneEvent(output);
|
|
|
73
|
+
|
|
73
|
74
|
if (!StringUtils.hasText(parsed.getDisplayText()) && !parsed.hasCard()) {
|
|
74
|
|
- log.warn("assistant 回复为空,未写入 fe_conversation_message conversationId={} conversationNo={}",
|
|
75
|
|
- conversation.getId(), conversation.getConversationNo());
|
|
|
75
|
+ log.warn("assistant 回复为空,未写入 fe_conversation_message conversationId={}",
|
|
|
76
|
+ conversation.getId());
|
|
76
|
77
|
} else if (parsed.hasCard()) {
|
|
77
|
|
- log.info("SSE complete 落库卡片 conversationId={} completed={} contentType={} relatedDraftId={}",
|
|
78
|
|
- conversation.getId(), collectResult.isCompleted(),
|
|
79
|
|
- parsed.getContentType(), parsed.getRelatedDraftId());
|
|
|
78
|
+ log.info("SSE 最后消息已注入卡片 conversationId={} relatedDraftId={}",
|
|
|
79
|
+ conversation.getId(), parsed.getRelatedDraftId());
|
|
80
|
80
|
}
|
|
81
|
81
|
conversationMessageService.saveAssistantMessage(conversation.getId(), parsed);
|
|
82
|
82
|
}
|
|
83
|
83
|
|
|
|
84
|
+ /**
|
|
|
85
|
+ * 在最后一条 SSE 消息 JSON 上追加 content_type / next_action / card_payload。
|
|
|
86
|
+ */
|
|
|
87
|
+ private void writeLastSseMessageWithCard(OutputStream output,
|
|
|
88
|
+ String heldTerminalLine,
|
|
|
89
|
+ ParsedAssistantReply parsed) throws IOException {
|
|
|
90
|
+ if (output == null) {
|
|
|
91
|
+ return;
|
|
|
92
|
+ }
|
|
|
93
|
+ if (!parsed.hasCard()) {
|
|
|
94
|
+ // 无卡片也要把 hold 的最后消息发出,避免客户端永远收不到完结消息
|
|
|
95
|
+ if (StringUtils.hasText(heldTerminalLine)) {
|
|
|
96
|
+ SseStreamSupport.writeRawSseLine(output, heldTerminalLine);
|
|
|
97
|
+ }
|
|
|
98
|
+ return;
|
|
|
99
|
+ }
|
|
|
100
|
+
|
|
|
101
|
+ ObjectNode msg = buildEnrichedLastMessage(heldTerminalLine, parsed);
|
|
|
102
|
+ String json = objectMapper.writeValueAsString(msg);
|
|
|
103
|
+ SseStreamSupport.writeDataEvent(output, json);
|
|
|
104
|
+ log.info("SSE 最后消息已写出 next_action={} content_type={} draftId={}",
|
|
|
105
|
+ msg.path("next_action").asText(),
|
|
|
106
|
+ msg.path("content_type").asText(),
|
|
|
107
|
+ parsed.getRelatedDraftId());
|
|
|
108
|
+ }
|
|
|
109
|
+
|
|
|
110
|
+ private ObjectNode buildEnrichedLastMessage(String heldTerminalLine, ParsedAssistantReply parsed)
|
|
|
111
|
+ throws IOException {
|
|
|
112
|
+ ObjectNode msg;
|
|
|
113
|
+ if (StringUtils.hasText(heldTerminalLine)) {
|
|
|
114
|
+ String payload = ConsoleChatSseAggregator.normalizeDataPayload(heldTerminalLine.trim());
|
|
|
115
|
+ JsonNode node = objectMapper.readTree(payload);
|
|
|
116
|
+ if (node != null && node.isObject()) {
|
|
|
117
|
+ msg = (ObjectNode) node;
|
|
|
118
|
+ } else {
|
|
|
119
|
+ msg = objectMapper.createObjectNode();
|
|
|
120
|
+ msg.put("object", "message");
|
|
|
121
|
+ msg.put("type", "message");
|
|
|
122
|
+ msg.put("role", "assistant");
|
|
|
123
|
+ msg.put("status", "completed");
|
|
|
124
|
+ }
|
|
|
125
|
+ } else {
|
|
|
126
|
+ msg = objectMapper.createObjectNode();
|
|
|
127
|
+ msg.put("object", "message");
|
|
|
128
|
+ msg.put("type", "message");
|
|
|
129
|
+ msg.put("role", "assistant");
|
|
|
130
|
+ msg.put("status", "completed");
|
|
|
131
|
+ ArrayNode content = msg.putArray("content");
|
|
|
132
|
+ ObjectNode textPart = content.addObject();
|
|
|
133
|
+ textPart.put("type", "text");
|
|
|
134
|
+ textPart.put("text", parsed.getDisplayText() != null ? parsed.getDisplayText() : "");
|
|
|
135
|
+ }
|
|
|
136
|
+
|
|
|
137
|
+ // 强制覆盖/追加卡片字段(不用智能体返回)
|
|
|
138
|
+ msg.put("content_type", MessageCardConstants.CONTENT_TYPE_CARD_DRAFT);
|
|
|
139
|
+ msg.put("next_action", MessageCardConstants.NEXT_ACTION_SHOW_EMPLOYMENT_DRAFT);
|
|
|
140
|
+ if (parsed.getRelatedDraftId() != null) {
|
|
|
141
|
+ msg.put("related_draft_id", parsed.getRelatedDraftId());
|
|
|
142
|
+ }
|
|
|
143
|
+ if (StringUtils.hasText(parsed.getCardPayload())) {
|
|
|
144
|
+ msg.set("card_payload", objectMapper.readTree(parsed.getCardPayload()));
|
|
|
145
|
+ }
|
|
|
146
|
+ if (!msg.has("status") || msg.get("status").asText("").isEmpty()) {
|
|
|
147
|
+ msg.put("status", "completed");
|
|
|
148
|
+ }
|
|
|
149
|
+ return msg;
|
|
|
150
|
+ }
|
|
|
151
|
+
|
|
84
|
152
|
private byte[] buildUpstreamBody(FeConversation conversation, Long userId, String userText) throws IOException {
|
|
85
|
153
|
ObjectNode root = objectMapper.createObjectNode();
|
|
86
|
154
|
ArrayNode input = objectMapper.createArrayNode();
|