mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
fix(chat): preserve refresh history and thinking order
This commit is contained in:
parent
0020f43584
commit
f28ad8d9d4
@ -23,6 +23,7 @@ import vip.mate.approval.PendingApproval;
|
|||||||
import vip.mate.memory.event.ConversationCompletedEvent;
|
import vip.mate.memory.event.ConversationCompletedEvent;
|
||||||
import vip.mate.workspace.conversation.ConversationService;
|
import vip.mate.workspace.conversation.ConversationService;
|
||||||
import vip.mate.workspace.conversation.model.MessageContentPart;
|
import vip.mate.workspace.conversation.model.MessageContentPart;
|
||||||
|
import vip.mate.workspace.conversation.model.MessageEntity;
|
||||||
|
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@ -211,11 +212,13 @@ public class ChatController {
|
|||||||
|
|
||||||
if ("denied".equals(decision)) {
|
if ("denied".equals(decision)) {
|
||||||
String denyMsg = "用户拒绝执行工具 " + pending.getToolName();
|
String denyMsg = "用户拒绝执行工具 " + pending.getToolName();
|
||||||
conversationService.saveMessage(conversationId, "assistant", denyMsg);
|
MessageEntity savedAssistant = conversationService.saveMessage(conversationId, "assistant", denyMsg);
|
||||||
broadcastEvent(conversationId, "message_start", Map.of("role", "assistant"));
|
broadcastEvent(conversationId, "message_start", Map.of("role", "assistant"));
|
||||||
broadcastEvent(conversationId, "content_delta", Map.of("delta", denyMsg));
|
broadcastEvent(conversationId, "content_delta", Map.of("delta", denyMsg));
|
||||||
broadcastEvent(conversationId, "message_complete", Map.of("status", "completed"));
|
broadcastEvent(conversationId, "message_complete", Map.of("status", "completed"));
|
||||||
broadcastEvent(conversationId, "done", Map.of("status", "completed"));
|
broadcastEvent(conversationId, "done", buildDonePayload(
|
||||||
|
conversationId, "completed", savedAssistant, 0, 0, true,
|
||||||
|
conversationService.getMessageCount(conversationId)));
|
||||||
// deny 是正常 turn 终结,用户可能在 awaiting_approval 阶段排了消息
|
// deny 是正常 turn 终结,用户可能在 awaiting_approval 阶段排了消息
|
||||||
ChatStreamTracker.CompletionResult denyCr = streamTracker.completeAndConsumeIfLast(conversationId);
|
ChatStreamTracker.CompletionResult denyCr = streamTracker.completeAndConsumeIfLast(conversationId);
|
||||||
if (denyCr.allDone() && denyCr.queuedInput() != null) {
|
if (denyCr.allDone() && denyCr.queuedInput() != null) {
|
||||||
@ -262,10 +265,11 @@ public class ChatController {
|
|||||||
.doOnComplete(() -> {
|
.doOnComplete(() -> {
|
||||||
if (!finalized.compareAndSet(false, true)) return;
|
if (!finalized.compareAndSet(false, true)) return;
|
||||||
try {
|
try {
|
||||||
|
MessageEntity savedAssistant = null;
|
||||||
List<MessageContentPart> parts = accumulator.toAssistantParts();
|
List<MessageContentPart> parts = accumulator.toAssistantParts();
|
||||||
String text = accumulator.getContent();
|
String text = accumulator.getContent();
|
||||||
if (!text.isBlank() || !parts.isEmpty()) {
|
if (!text.isBlank() || !parts.isEmpty()) {
|
||||||
conversationService.saveMessage(conversationId, "assistant", text, parts,
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant", text, parts,
|
||||||
"completed",
|
"completed",
|
||||||
accumulator.getPromptTokens(),
|
accumulator.getPromptTokens(),
|
||||||
accumulator.getCompletionTokens(),
|
accumulator.getCompletionTokens(),
|
||||||
@ -279,12 +283,8 @@ public class ChatController {
|
|||||||
"hasContent", !text.isBlank()
|
"hasContent", !text.isBlank()
|
||||||
));
|
));
|
||||||
int msgCount = conversationService.getMessageCount(conversationId);
|
int msgCount = conversationService.getMessageCount(conversationId);
|
||||||
broadcastEvent(conversationId, "done", Map.of(
|
broadcastEvent(conversationId, "done", buildDonePayload(
|
||||||
"conversationId", conversationId,
|
conversationId, "completed", savedAssistant, 0, 0, true, msgCount));
|
||||||
"status", "completed",
|
|
||||||
"persisted", true,
|
|
||||||
"messageCount", msgCount
|
|
||||||
));
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("SSE replay complete error: {}", e.getMessage());
|
log.warn("SSE replay complete error: {}", e.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
@ -318,12 +318,13 @@ public class ChatController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
MessageEntity savedAssistant = null;
|
||||||
List<MessageContentPart> replayParts = accumulator.toAssistantParts();
|
List<MessageContentPart> replayParts = accumulator.toAssistantParts();
|
||||||
String replayText = accumulator.getContent();
|
String replayText = accumulator.getContent();
|
||||||
if (!replayText.isBlank() || !replayParts.isEmpty()) {
|
if (!replayText.isBlank() || !replayParts.isEmpty()) {
|
||||||
String savedText = replayText.isBlank() && isUserStop
|
String savedText = replayText.isBlank() && isUserStop
|
||||||
? (replayIsFollowup ? "[已中断]" : "[已停止生成]") : replayText;
|
? (replayIsFollowup ? "[已中断]" : "[已停止生成]") : replayText;
|
||||||
conversationService.saveMessage(conversationId, "assistant", savedText, replayParts,
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant", savedText, replayParts,
|
||||||
errStatus,
|
errStatus,
|
||||||
accumulator.getPromptTokens(),
|
accumulator.getPromptTokens(),
|
||||||
accumulator.getCompletionTokens(),
|
accumulator.getCompletionTokens(),
|
||||||
@ -331,7 +332,7 @@ public class ChatController {
|
|||||||
accumulator.getRuntimeProviderId(),
|
accumulator.getRuntimeProviderId(),
|
||||||
accumulator.toMetadataJson());
|
accumulator.toMetadataJson());
|
||||||
} else if (isUserStop) {
|
} else if (isUserStop) {
|
||||||
conversationService.saveMessage(conversationId, "assistant",
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant",
|
||||||
replayIsFollowup ? "[已中断]" : "[已停止生成]", null, errStatus);
|
replayIsFollowup ? "[已中断]" : "[已停止生成]", null, errStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,15 +353,13 @@ public class ChatController {
|
|||||||
"hasContent", !replayText.isBlank()
|
"hasContent", !replayText.isBlank()
|
||||||
));
|
));
|
||||||
int stoppedMsgCount = conversationService.getMessageCount(conversationId);
|
int stoppedMsgCount = conversationService.getMessageCount(conversationId);
|
||||||
broadcastEvent(conversationId, "done", Map.of(
|
broadcastEvent(conversationId, "done", buildDonePayload(
|
||||||
"conversationId", conversationId,
|
conversationId, "stopped", savedAssistant, 0, 0, true, stoppedMsgCount));
|
||||||
"status", "stopped",
|
|
||||||
"persisted", true,
|
|
||||||
"messageCount", stoppedMsgCount
|
|
||||||
));
|
|
||||||
} else {
|
} else {
|
||||||
broadcastEvent(conversationId, "error", Map.of("message",
|
broadcastEvent(conversationId, "error", buildErrorPayload(
|
||||||
e.getMessage() != null ? e.getMessage() : "replay error"));
|
conversationId,
|
||||||
|
e.getMessage() != null ? e.getMessage() : "replay error",
|
||||||
|
savedAssistant));
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.warn("SSE replay error finalize failed: {}", ex.getMessage());
|
log.warn("SSE replay error finalize failed: {}", ex.getMessage());
|
||||||
@ -446,12 +445,13 @@ public class ChatController {
|
|||||||
persistStatus = isInterruptFollowup ? "interrupted" : "stopped";
|
persistStatus = isInterruptFollowup ? "interrupted" : "stopped";
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
MessageEntity savedAssistant = null;
|
||||||
List<MessageContentPart> assistantParts = accumulator.toAssistantParts();
|
List<MessageContentPart> assistantParts = accumulator.toAssistantParts();
|
||||||
String assistantText = accumulator.getContent();
|
String assistantText = accumulator.getContent();
|
||||||
if (!assistantText.isBlank() || !assistantParts.isEmpty()) {
|
if (!assistantText.isBlank() || !assistantParts.isEmpty()) {
|
||||||
String savedText = assistantText.isBlank() && wasStopped
|
String savedText = assistantText.isBlank() && wasStopped
|
||||||
? (isInterruptFollowup ? "[已中断]" : "[已停止生成]") : assistantText;
|
? (isInterruptFollowup ? "[已中断]" : "[已停止生成]") : assistantText;
|
||||||
conversationService.saveMessage(conversationId, "assistant", savedText, assistantParts,
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant", savedText, assistantParts,
|
||||||
persistStatus,
|
persistStatus,
|
||||||
accumulator.getPromptTokens(),
|
accumulator.getPromptTokens(),
|
||||||
accumulator.getCompletionTokens(),
|
accumulator.getCompletionTokens(),
|
||||||
@ -459,7 +459,7 @@ public class ChatController {
|
|||||||
accumulator.getRuntimeProviderId(),
|
accumulator.getRuntimeProviderId(),
|
||||||
accumulator.toMetadataJson());
|
accumulator.toMetadataJson());
|
||||||
} else if (wasStopped) {
|
} else if (wasStopped) {
|
||||||
conversationService.saveMessage(conversationId, "assistant",
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant",
|
||||||
isInterruptFollowup ? "[已中断]" : "[已停止生成]", null, persistStatus);
|
isInterruptFollowup ? "[已中断]" : "[已停止生成]", null, persistStatus);
|
||||||
}
|
}
|
||||||
// 发布对话完成事件(仅正常完成时,停止/中断不触发记忆提取)
|
// 发布对话完成事件(仅正常完成时,停止/中断不触发记忆提取)
|
||||||
@ -490,14 +490,9 @@ public class ChatController {
|
|||||||
"hasContent", !assistantText.isBlank()
|
"hasContent", !assistantText.isBlank()
|
||||||
));
|
));
|
||||||
int msgCount = conversationService.getMessageCount(conversationId);
|
int msgCount = conversationService.getMessageCount(conversationId);
|
||||||
broadcastEvent(conversationId, "done", Map.of(
|
broadcastEvent(conversationId, "done", buildDonePayload(
|
||||||
"conversationId", conversationId,
|
conversationId, persistStatus, savedAssistant,
|
||||||
"status", persistStatus,
|
accumulator.getPromptTokens(), accumulator.getCompletionTokens(), true, msgCount));
|
||||||
"promptTokens", accumulator.getPromptTokens(),
|
|
||||||
"completionTokens", accumulator.getCompletionTokens(),
|
|
||||||
"persisted", true,
|
|
||||||
"messageCount", msgCount
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("SSE complete error: {}", e.getMessage());
|
log.warn("SSE complete error: {}", e.getMessage());
|
||||||
@ -534,12 +529,13 @@ public class ChatController {
|
|||||||
|
|
||||||
log.info("SSE stream cancelled ({}): conversationId={}", status, conversationId);
|
log.info("SSE stream cancelled ({}): conversationId={}", status, conversationId);
|
||||||
try {
|
try {
|
||||||
|
MessageEntity savedAssistant = null;
|
||||||
List<MessageContentPart> assistantParts = accumulator.toAssistantParts();
|
List<MessageContentPart> assistantParts = accumulator.toAssistantParts();
|
||||||
String assistantText = accumulator.getContent();
|
String assistantText = accumulator.getContent();
|
||||||
if (!assistantText.isBlank() || !assistantParts.isEmpty()) {
|
if (!assistantText.isBlank() || !assistantParts.isEmpty()) {
|
||||||
String savedText = assistantText.isBlank()
|
String savedText = assistantText.isBlank()
|
||||||
? (isInterruptFollowup ? "[已中断]" : "[已停止生成]") : assistantText;
|
? (isInterruptFollowup ? "[已中断]" : "[已停止生成]") : assistantText;
|
||||||
conversationService.saveMessage(conversationId, "assistant", savedText, assistantParts,
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant", savedText, assistantParts,
|
||||||
status,
|
status,
|
||||||
accumulator.getPromptTokens(),
|
accumulator.getPromptTokens(),
|
||||||
accumulator.getCompletionTokens(),
|
accumulator.getCompletionTokens(),
|
||||||
@ -547,7 +543,7 @@ public class ChatController {
|
|||||||
accumulator.getRuntimeProviderId(),
|
accumulator.getRuntimeProviderId(),
|
||||||
accumulator.toMetadataJson());
|
accumulator.toMetadataJson());
|
||||||
} else {
|
} else {
|
||||||
conversationService.saveMessage(conversationId, "assistant",
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant",
|
||||||
isInterruptFollowup ? "[已中断]" : "[已停止生成]", null, status);
|
isInterruptFollowup ? "[已中断]" : "[已停止生成]", null, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -568,12 +564,8 @@ public class ChatController {
|
|||||||
"hasContent", !assistantText.isBlank()
|
"hasContent", !assistantText.isBlank()
|
||||||
));
|
));
|
||||||
int stoppedMsgCount = conversationService.getMessageCount(conversationId);
|
int stoppedMsgCount = conversationService.getMessageCount(conversationId);
|
||||||
broadcastEvent(conversationId, "done", Map.of(
|
broadcastEvent(conversationId, "done", buildDonePayload(
|
||||||
"conversationId", conversationId,
|
conversationId, "stopped", savedAssistant, 0, 0, true, stoppedMsgCount));
|
||||||
"status", "stopped",
|
|
||||||
"persisted", true,
|
|
||||||
"messageCount", stoppedMsgCount
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("SSE stop finalize error: {}", e.getMessage());
|
log.warn("SSE stop finalize error: {}", e.getMessage());
|
||||||
@ -623,10 +615,11 @@ public class ChatController {
|
|||||||
log.info("SSE doOnError saving: conversationId={}, status={}, textLen={}, partsCount={}",
|
log.info("SSE doOnError saving: conversationId={}, status={}, textLen={}, partsCount={}",
|
||||||
conversationId, status, assistantText.length(), assistantParts.size());
|
conversationId, status, assistantText.length(), assistantParts.size());
|
||||||
String errorMsg = e.getMessage() != null ? e.getMessage() : "unknown error";
|
String errorMsg = e.getMessage() != null ? e.getMessage() : "unknown error";
|
||||||
|
MessageEntity savedAssistant = null;
|
||||||
if (!assistantText.isBlank() || !assistantParts.isEmpty()) {
|
if (!assistantText.isBlank() || !assistantParts.isEmpty()) {
|
||||||
String savedText = assistantText.isBlank() && isUserStop
|
String savedText = assistantText.isBlank() && isUserStop
|
||||||
? (isInterruptFollowup ? "[已中断]" : "[已停止生成]") : assistantText;
|
? (isInterruptFollowup ? "[已中断]" : "[已停止生成]") : assistantText;
|
||||||
conversationService.saveMessage(conversationId, "assistant", savedText, assistantParts,
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant", savedText, assistantParts,
|
||||||
status,
|
status,
|
||||||
accumulator.getPromptTokens(),
|
accumulator.getPromptTokens(),
|
||||||
accumulator.getCompletionTokens(),
|
accumulator.getCompletionTokens(),
|
||||||
@ -634,10 +627,10 @@ public class ChatController {
|
|||||||
accumulator.getRuntimeProviderId(),
|
accumulator.getRuntimeProviderId(),
|
||||||
accumulator.toMetadataJson());
|
accumulator.toMetadataJson());
|
||||||
} else if (isUserStop) {
|
} else if (isUserStop) {
|
||||||
conversationService.saveMessage(conversationId, "assistant",
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant",
|
||||||
isInterruptFollowup ? "[已中断]" : "[已停止生成]", null, status);
|
isInterruptFollowup ? "[已中断]" : "[已停止生成]", null, status);
|
||||||
} else {
|
} else {
|
||||||
conversationService.saveMessage(conversationId, "assistant", "[错误] " + errorMsg, null, "failed");
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant", "[错误] " + errorMsg, null, "failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInterruptFollowup) {
|
if (isInterruptFollowup) {
|
||||||
@ -657,17 +650,10 @@ public class ChatController {
|
|||||||
"hasContent", !assistantText.isBlank()
|
"hasContent", !assistantText.isBlank()
|
||||||
));
|
));
|
||||||
int stoppedMsgCount = conversationService.getMessageCount(conversationId);
|
int stoppedMsgCount = conversationService.getMessageCount(conversationId);
|
||||||
broadcastEvent(conversationId, "done", Map.of(
|
broadcastEvent(conversationId, "done", buildDonePayload(
|
||||||
"conversationId", conversationId,
|
conversationId, "stopped", savedAssistant, 0, 0, true, stoppedMsgCount));
|
||||||
"status", "stopped",
|
|
||||||
"persisted", true,
|
|
||||||
"messageCount", stoppedMsgCount
|
|
||||||
));
|
|
||||||
} else {
|
} else {
|
||||||
broadcastEvent(conversationId, "error", Map.of(
|
broadcastEvent(conversationId, "error", buildErrorPayload(conversationId, errorMsg, savedAssistant));
|
||||||
"message", errorMsg,
|
|
||||||
"conversationId", conversationId
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
} catch (Exception ioException) {
|
} catch (Exception ioException) {
|
||||||
log.error("SSE doOnError save/broadcast failed: conversationId={}, error={}",
|
log.error("SSE doOnError save/broadcast failed: conversationId={}, error={}",
|
||||||
@ -1011,10 +997,11 @@ public class ChatController {
|
|||||||
.doOnComplete(() -> {
|
.doOnComplete(() -> {
|
||||||
if (!finalized.compareAndSet(false, true)) return;
|
if (!finalized.compareAndSet(false, true)) return;
|
||||||
try {
|
try {
|
||||||
|
MessageEntity savedAssistant = null;
|
||||||
List<MessageContentPart> parts = accumulator.toAssistantParts();
|
List<MessageContentPart> parts = accumulator.toAssistantParts();
|
||||||
String text = accumulator.getContent();
|
String text = accumulator.getContent();
|
||||||
if (!text.isBlank() || !parts.isEmpty()) {
|
if (!text.isBlank() || !parts.isEmpty()) {
|
||||||
conversationService.saveMessage(conversationId, "assistant", text, parts,
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant", text, parts,
|
||||||
"completed",
|
"completed",
|
||||||
accumulator.getPromptTokens(),
|
accumulator.getPromptTokens(),
|
||||||
accumulator.getCompletionTokens(),
|
accumulator.getCompletionTokens(),
|
||||||
@ -1027,11 +1014,10 @@ public class ChatController {
|
|||||||
"hasThinking", !accumulator.getThinking().isBlank(),
|
"hasThinking", !accumulator.getThinking().isBlank(),
|
||||||
"hasContent", !text.isBlank()
|
"hasContent", !text.isBlank()
|
||||||
));
|
));
|
||||||
broadcastEvent(conversationId, "done", Map.of(
|
broadcastEvent(conversationId, "done", buildDonePayload(
|
||||||
"status", "completed",
|
conversationId, "completed", savedAssistant,
|
||||||
"promptTokens", accumulator.getPromptTokens(),
|
accumulator.getPromptTokens(), accumulator.getCompletionTokens(), true,
|
||||||
"completionTokens", accumulator.getCompletionTokens()
|
conversationService.getMessageCount(conversationId)));
|
||||||
));
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("SSE queued complete error: {}", e.getMessage());
|
log.warn("SSE queued complete error: {}", e.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
@ -1055,10 +1041,11 @@ public class ChatController {
|
|||||||
log.error("SSE queued stream error: conversationId={}, cause={}", conversationId, e.getMessage());
|
log.error("SSE queued stream error: conversationId={}, cause={}", conversationId, e.getMessage());
|
||||||
// 持久化已累积的 assistant 消息(修复:原逻辑未保存导致回答丢失)
|
// 持久化已累积的 assistant 消息(修复:原逻辑未保存导致回答丢失)
|
||||||
try {
|
try {
|
||||||
|
MessageEntity savedAssistant = null;
|
||||||
List<MessageContentPart> parts = accumulator.toAssistantParts();
|
List<MessageContentPart> parts = accumulator.toAssistantParts();
|
||||||
String text = accumulator.getContent();
|
String text = accumulator.getContent();
|
||||||
if (!text.isBlank() || !parts.isEmpty()) {
|
if (!text.isBlank() || !parts.isEmpty()) {
|
||||||
conversationService.saveMessage(conversationId, "assistant", text, parts,
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant", text, parts,
|
||||||
"failed",
|
"failed",
|
||||||
accumulator.getPromptTokens(),
|
accumulator.getPromptTokens(),
|
||||||
accumulator.getCompletionTokens(),
|
accumulator.getCompletionTokens(),
|
||||||
@ -1067,14 +1054,16 @@ public class ChatController {
|
|||||||
accumulator.toMetadataJson());
|
accumulator.toMetadataJson());
|
||||||
} else {
|
} else {
|
||||||
String errorMsg = e.getMessage() != null ? e.getMessage() : "queued stream error";
|
String errorMsg = e.getMessage() != null ? e.getMessage() : "queued stream error";
|
||||||
conversationService.saveMessage(conversationId, "assistant",
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant",
|
||||||
"[错误] " + errorMsg, null, "failed");
|
"[错误] " + errorMsg, null, "failed");
|
||||||
}
|
}
|
||||||
|
broadcastEvent(conversationId, "error", buildErrorPayload(
|
||||||
|
conversationId,
|
||||||
|
e.getMessage() != null ? e.getMessage() : "queued stream error",
|
||||||
|
savedAssistant));
|
||||||
} catch (Exception saveEx) {
|
} catch (Exception saveEx) {
|
||||||
log.error("SSE queued doOnError save failed: {}", saveEx.getMessage());
|
log.error("SSE queued doOnError save failed: {}", saveEx.getMessage());
|
||||||
}
|
}
|
||||||
broadcastEvent(conversationId, "error", Map.of(
|
|
||||||
"message", e.getMessage() != null ? e.getMessage() : "queued stream error"));
|
|
||||||
ChatStreamTracker.CompletionResult cr = streamTracker.completeAndConsumeIfLast(conversationId);
|
ChatStreamTracker.CompletionResult cr = streamTracker.completeAndConsumeIfLast(conversationId);
|
||||||
if (cr.allDone()) {
|
if (cr.allDone()) {
|
||||||
if (cr.queuedInput() != null) {
|
if (cr.queuedInput() != null) {
|
||||||
@ -1109,6 +1098,32 @@ public class ChatController {
|
|||||||
streamTracker.broadcast(conversationId, name, payload);
|
streamTracker.broadcast(conversationId, name, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildDonePayload(String conversationId, String status, MessageEntity savedAssistant,
|
||||||
|
int promptTokens, int completionTokens,
|
||||||
|
boolean persisted, Integer messageCount) {
|
||||||
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
if (conversationId != null && !conversationId.isBlank()) payload.put("conversationId", conversationId);
|
||||||
|
payload.put("status", status);
|
||||||
|
if (savedAssistant != null && savedAssistant.getId() != null) {
|
||||||
|
payload.put("assistantMessageId", savedAssistant.getId());
|
||||||
|
}
|
||||||
|
if (promptTokens > 0) payload.put("promptTokens", promptTokens);
|
||||||
|
if (completionTokens > 0) payload.put("completionTokens", completionTokens);
|
||||||
|
payload.put("persisted", persisted);
|
||||||
|
if (messageCount != null) payload.put("messageCount", messageCount);
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildErrorPayload(String conversationId, String message, MessageEntity savedAssistant) {
|
||||||
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
payload.put("message", message);
|
||||||
|
if (conversationId != null && !conversationId.isBlank()) payload.put("conversationId", conversationId);
|
||||||
|
if (savedAssistant != null && savedAssistant.getId() != null) {
|
||||||
|
payload.put("assistantMessageId", savedAssistant.getId());
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
private List<MessageContentPart> normalizeRequestParts(ChatStreamRequest request) {
|
private List<MessageContentPart> normalizeRequestParts(ChatStreamRequest request) {
|
||||||
if (request.getContentParts() != null && !request.getContentParts().isEmpty()) {
|
if (request.getContentParts() != null && !request.getContentParts().isEmpty()) {
|
||||||
return request.getContentParts();
|
return request.getContentParts();
|
||||||
@ -1232,12 +1247,20 @@ public class ChatController {
|
|||||||
private final List<Map<String, Object>> toolCalls = new ArrayList<>();
|
private final List<Map<String, Object>> toolCalls = new ArrayList<>();
|
||||||
/** 有序事件时间线 — 前端分段渲染的权威数据源 */
|
/** 有序事件时间线 — 前端分段渲染的权威数据源 */
|
||||||
private final List<Map<String, Object>> segments = new ArrayList<>();
|
private final List<Map<String, Object>> segments = new ArrayList<>();
|
||||||
|
private final List<Map<String, Object>> browserActions = new ArrayList<>();
|
||||||
|
private final List<String> warnings = new ArrayList<>();
|
||||||
|
private final List<Map<String, Object>> planStepResults = new ArrayList<>();
|
||||||
private int segCounter = 0;
|
private int segCounter = 0;
|
||||||
private int promptTokens = 0;
|
private int promptTokens = 0;
|
||||||
private int completionTokens = 0;
|
private int completionTokens = 0;
|
||||||
private String runtimeModelName = "";
|
private String runtimeModelName = "";
|
||||||
private String runtimeProviderId = "";
|
private String runtimeProviderId = "";
|
||||||
private boolean awaitingApproval = false;
|
private boolean awaitingApproval = false;
|
||||||
|
private String currentPhase = "";
|
||||||
|
private Long planId = null;
|
||||||
|
private List<String> planSteps = List.of();
|
||||||
|
private Integer currentPlanStep = null;
|
||||||
|
private Map<String, Object> pendingApproval = null;
|
||||||
|
|
||||||
synchronized void accept(AgentService.StreamDelta delta, String conversationId) {
|
synchronized void accept(AgentService.StreamDelta delta, String conversationId) {
|
||||||
if (delta == null) return;
|
if (delta == null) return;
|
||||||
@ -1254,6 +1277,7 @@ public class ChatController {
|
|||||||
if ("phase".equals(delta.eventType())) {
|
if ("phase".equals(delta.eventType())) {
|
||||||
String phase = String.valueOf(delta.eventData().getOrDefault("phase", ""));
|
String phase = String.valueOf(delta.eventData().getOrDefault("phase", ""));
|
||||||
if (!phase.isBlank()) {
|
if (!phase.isBlank()) {
|
||||||
|
currentPhase = phase;
|
||||||
streamTracker.updatePhase(conversationId, phase);
|
streamTracker.updatePhase(conversationId, phase);
|
||||||
// phase 切换时关闭 running 的 content/thinking segment,保留边界
|
// phase 切换时关闭 running 的 content/thinking segment,保留边界
|
||||||
finalizeRunningSegments("content", "thinking");
|
finalizeRunningSegments("content", "thinking");
|
||||||
@ -1309,7 +1333,62 @@ public class ChatController {
|
|||||||
private void accumulateToolEvent(String eventType, Map<String, Object> data, String conversationId) {
|
private void accumulateToolEvent(String eventType, Map<String, Object> data, String conversationId) {
|
||||||
if ("tool_approval_requested".equals(eventType)) {
|
if ("tool_approval_requested".equals(eventType)) {
|
||||||
awaitingApproval = true;
|
awaitingApproval = true;
|
||||||
|
currentPhase = "awaiting_approval";
|
||||||
|
pendingApproval = new LinkedHashMap<>();
|
||||||
|
pendingApproval.put("pendingId", data.getOrDefault("pendingId", ""));
|
||||||
|
pendingApproval.put("toolName", data.getOrDefault("toolName", ""));
|
||||||
|
pendingApproval.put("arguments", data.getOrDefault("arguments", ""));
|
||||||
|
pendingApproval.put("reason", data.getOrDefault("reason", ""));
|
||||||
|
pendingApproval.put("status", "pending_approval");
|
||||||
|
if (data.containsKey("findings")) pendingApproval.put("findings", data.get("findings"));
|
||||||
|
if (data.containsKey("maxSeverity")) pendingApproval.put("maxSeverity", data.get("maxSeverity"));
|
||||||
|
if (data.containsKey("summary")) pendingApproval.put("summary", data.get("summary"));
|
||||||
streamTracker.updatePhase(conversationId, "awaiting_approval");
|
streamTracker.updatePhase(conversationId, "awaiting_approval");
|
||||||
|
} else if ("tool_approval_resolved".equals(eventType)) {
|
||||||
|
if (pendingApproval != null) {
|
||||||
|
pendingApproval.put("status",
|
||||||
|
"approved".equals(String.valueOf(data.getOrDefault("decision", ""))) ? "approved" : "denied");
|
||||||
|
}
|
||||||
|
} else if ("plan_created".equals(eventType)) {
|
||||||
|
Object rawPlanId = data.get("planId");
|
||||||
|
if (rawPlanId instanceof Number n) {
|
||||||
|
planId = n.longValue();
|
||||||
|
} else if (rawPlanId != null) {
|
||||||
|
try { planId = Long.valueOf(String.valueOf(rawPlanId)); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
Object steps = data.get("steps");
|
||||||
|
if (steps instanceof List<?> list) {
|
||||||
|
planSteps = list.stream().map(String::valueOf).toList();
|
||||||
|
planStepResults.clear();
|
||||||
|
for (int i = 0; i < planSteps.size(); i++) {
|
||||||
|
planStepResults.add(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentPlanStep = 0;
|
||||||
|
} else if ("plan_step_started".equals(eventType)) {
|
||||||
|
Object idx = data.get("index");
|
||||||
|
if (idx instanceof Number n) {
|
||||||
|
currentPlanStep = n.intValue();
|
||||||
|
}
|
||||||
|
} else if ("plan_step_completed".equals(eventType)) {
|
||||||
|
Object idx = data.get("index");
|
||||||
|
if (idx instanceof Number n) {
|
||||||
|
int index = n.intValue();
|
||||||
|
currentPlanStep = index;
|
||||||
|
ensurePlanStepCapacity(index + 1);
|
||||||
|
Map<String, Object> stepResult = new LinkedHashMap<>();
|
||||||
|
stepResult.put("result", data.getOrDefault("result", ""));
|
||||||
|
stepResult.put("status", "completed");
|
||||||
|
planStepResults.set(index, stepResult);
|
||||||
|
}
|
||||||
|
} else if ("browser_action".equals(eventType)) {
|
||||||
|
browserActions.add(new LinkedHashMap<>(data));
|
||||||
|
} else if ("warning".equals(eventType)) {
|
||||||
|
String warning = String.valueOf(data.getOrDefault("message",
|
||||||
|
data.getOrDefault("delta", "")));
|
||||||
|
if (!warning.isBlank()) {
|
||||||
|
warnings.add(warning);
|
||||||
|
}
|
||||||
} else if ("tool_call_started".equals(eventType)) {
|
} else if ("tool_call_started".equals(eventType)) {
|
||||||
// toolCalls(兼容)
|
// toolCalls(兼容)
|
||||||
Map<String, Object> tc = new LinkedHashMap<>();
|
Map<String, Object> tc = new LinkedHashMap<>();
|
||||||
@ -1349,6 +1428,12 @@ public class ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ensurePlanStepCapacity(int size) {
|
||||||
|
while (planStepResults.size() < size) {
|
||||||
|
planStepResults.add(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Segment helpers ====================
|
// ==================== Segment helpers ====================
|
||||||
|
|
||||||
private Map<String, Object> newSegment(String type) {
|
private Map<String, Object> newSegment(String type) {
|
||||||
@ -1430,6 +1515,28 @@ public class ChatController {
|
|||||||
if (!segments.isEmpty()) {
|
if (!segments.isEmpty()) {
|
||||||
metadata.put("segments", segments);
|
metadata.put("segments", segments);
|
||||||
}
|
}
|
||||||
|
if (!currentPhase.isBlank()) {
|
||||||
|
metadata.put("currentPhase", currentPhase);
|
||||||
|
}
|
||||||
|
if (planId != null || !planSteps.isEmpty() || currentPlanStep != null) {
|
||||||
|
Map<String, Object> plan = new LinkedHashMap<>();
|
||||||
|
if (planId != null) plan.put("planId", planId);
|
||||||
|
if (!planSteps.isEmpty()) plan.put("steps", planSteps);
|
||||||
|
if (currentPlanStep != null) plan.put("currentStep", currentPlanStep);
|
||||||
|
if (planStepResults.stream().anyMatch(java.util.Objects::nonNull)) {
|
||||||
|
plan.put("stepResults", planStepResults);
|
||||||
|
}
|
||||||
|
metadata.put("plan", plan);
|
||||||
|
}
|
||||||
|
if (pendingApproval != null && !pendingApproval.isEmpty()) {
|
||||||
|
metadata.put("pendingApproval", pendingApproval);
|
||||||
|
}
|
||||||
|
if (!browserActions.isEmpty()) {
|
||||||
|
metadata.put("browserActions", browserActions);
|
||||||
|
}
|
||||||
|
if (!warnings.isEmpty()) {
|
||||||
|
metadata.put("warnings", warnings);
|
||||||
|
}
|
||||||
return objectMapper.writeValueAsString(metadata);
|
return objectMapper.writeValueAsString(metadata);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Failed to serialize metadata: {}", e.getMessage());
|
log.warn("Failed to serialize metadata: {}", e.getMessage());
|
||||||
|
|||||||
@ -594,19 +594,12 @@ const segments = computed<MessageSegment[]>(() => {
|
|||||||
if (props.message.role !== 'assistant') return []
|
if (props.message.role !== 'assistant') return []
|
||||||
const meta = parsedMetadata.value
|
const meta = parsedMetadata.value
|
||||||
|
|
||||||
// DEBUG: 临时日志,验证后删除
|
|
||||||
if (meta?.segments) {
|
|
||||||
console.debug('[MessageBubble] segments found:', meta.segments.length, 'first:', meta.segments[0]?.type)
|
|
||||||
} else {
|
|
||||||
console.debug('[MessageBubble] NO segments in metadata, keys:', Object.keys(meta || {}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优先:使用 metadata.segments(流式时由前端写入,历史时由后端持久化)
|
// 优先:使用 metadata.segments(流式时由前端写入,历史时由后端持久化)
|
||||||
// 这是按事件顺序记录的完整时间线,保留了 thinking→tools→content 的真实交错
|
|
||||||
if (meta?.segments && Array.isArray(meta.segments) && meta.segments.length > 0
|
if (meta?.segments && Array.isArray(meta.segments) && meta.segments.length > 0
|
||||||
&& typeof meta.segments[0] === 'object' && meta.segments[0]?.type) {
|
&& typeof meta.segments[0] === 'object' && meta.segments[0]?.type) {
|
||||||
// 补充:如果后端 segments 没有 thinking 但 contentParts 有(非原生 thinking 模型)
|
|
||||||
const segs = [...meta.segments] as MessageSegment[]
|
const segs = [...meta.segments] as MessageSegment[]
|
||||||
|
|
||||||
|
// 补充:如果后端 segments 没有 thinking 但 contentParts 有(非原生 thinking 模型)
|
||||||
const hasThinking = segs.some(s => s.type === 'thinking')
|
const hasThinking = segs.some(s => s.type === 'thinking')
|
||||||
if (!hasThinking) {
|
if (!hasThinking) {
|
||||||
const thinkingPart = props.message.contentParts?.find(p => p.type === 'thinking')
|
const thinkingPart = props.message.contentParts?.find(p => p.type === 'thinking')
|
||||||
@ -614,6 +607,18 @@ const segments = computed<MessageSegment[]>(() => {
|
|||||||
segs.unshift({ id: 'th-fb', type: 'thinking', status: 'completed', thinkingText: thinkingPart.text })
|
segs.unshift({ id: 'th-fb', type: 'thinking', status: 'completed', thinkingText: thinkingPart.text })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 修复历史消息顺序:如果 thinking 被落在 content 后面,提到首个 content 前
|
||||||
|
// 只处理单个 thinking 段的常见场景,避免破坏复杂交错时间线
|
||||||
|
const thinkingIndices = segs
|
||||||
|
.map((seg, index) => seg.type === 'thinking' ? index : -1)
|
||||||
|
.filter(index => index >= 0)
|
||||||
|
const firstNonThinkingIdx = segs.findIndex((seg: MessageSegment) => seg.type !== 'thinking')
|
||||||
|
if (thinkingIndices.length === 1 && firstNonThinkingIdx >= 0 && thinkingIndices[0] > firstNonThinkingIdx) {
|
||||||
|
const [thinkingSeg] = segs.splice(thinkingIndices[0], 1)
|
||||||
|
segs.splice(0, 0, thinkingSeg)
|
||||||
|
}
|
||||||
|
|
||||||
return segs
|
return segs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user