mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
fix(chat): persist empty assistant turns
This commit is contained in:
parent
32fe582b82
commit
e9a11f8db4
@ -251,7 +251,8 @@ public class ChatController {
|
|||||||
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", buildDonePayload(
|
broadcastEvent(conversationId, "done", buildDonePayload(
|
||||||
conversationId, "completed", savedAssistant, 0, 0, true,
|
conversationId, "completed", savedAssistant, 0, 0,
|
||||||
|
isAssistantPersisted(savedAssistant),
|
||||||
conversationService.getMessageCount(conversationId)));
|
conversationService.getMessageCount(conversationId)));
|
||||||
// deny 是正常 turn 终结,用户可能在 awaiting_approval 阶段排了消息
|
// deny 是正常 turn 终结,用户可能在 awaiting_approval 阶段排了消息
|
||||||
ChatStreamTracker.CompletionResult denyCr = streamTracker.completeAndConsumeIfLast(conversationId);
|
ChatStreamTracker.CompletionResult denyCr = streamTracker.completeAndConsumeIfLast(conversationId);
|
||||||
@ -346,6 +347,13 @@ public class ChatController {
|
|||||||
accumulator.getRuntimeModelName(),
|
accumulator.getRuntimeModelName(),
|
||||||
accumulator.getRuntimeProviderId(),
|
accumulator.getRuntimeProviderId(),
|
||||||
accumulator.toMetadataJson()); // includes toolCalls metadata
|
accumulator.toMetadataJson()); // includes toolCalls metadata
|
||||||
|
} else if (replayWasStopped) {
|
||||||
|
boolean replayIsFollowup = replayInterrupt == ChatStreamTracker.InterruptType.USER_INTERRUPT_WITH_FOLLOWUP;
|
||||||
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant",
|
||||||
|
replayIsFollowup ? "[已中断]" : "[已停止生成]", null, persistStatus);
|
||||||
|
} else {
|
||||||
|
savedAssistant = saveEmptyAssistantPlaceholder(
|
||||||
|
conversationId, persistStatus, accumulator, "SSE replay doOnComplete");
|
||||||
}
|
}
|
||||||
broadcastEvent(conversationId, "message_complete", Map.of(
|
broadcastEvent(conversationId, "message_complete", Map.of(
|
||||||
"status", persistStatus,
|
"status", persistStatus,
|
||||||
@ -354,7 +362,8 @@ public class ChatController {
|
|||||||
));
|
));
|
||||||
int msgCount = conversationService.getMessageCount(conversationId);
|
int msgCount = conversationService.getMessageCount(conversationId);
|
||||||
broadcastEvent(conversationId, "done", buildDonePayload(
|
broadcastEvent(conversationId, "done", buildDonePayload(
|
||||||
conversationId, persistStatus, savedAssistant, 0, 0, true, msgCount));
|
conversationId, persistStatus, savedAssistant, 0, 0,
|
||||||
|
isAssistantPersisted(savedAssistant), 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 {
|
||||||
@ -420,6 +429,10 @@ public class ChatController {
|
|||||||
} else if (isUserStop) {
|
} else if (isUserStop) {
|
||||||
savedAssistant = conversationService.saveMessage(conversationId, "assistant",
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant",
|
||||||
replayIsFollowup ? "[已中断]" : "[已停止生成]", null, errStatus);
|
replayIsFollowup ? "[已中断]" : "[已停止生成]", null, errStatus);
|
||||||
|
} else {
|
||||||
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant",
|
||||||
|
"[错误] " + (e.getMessage() != null ? e.getMessage() : "replay error"),
|
||||||
|
null, "failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (replayIsFollowup) {
|
if (replayIsFollowup) {
|
||||||
@ -440,7 +453,8 @@ public class ChatController {
|
|||||||
));
|
));
|
||||||
int stoppedMsgCount = conversationService.getMessageCount(conversationId);
|
int stoppedMsgCount = conversationService.getMessageCount(conversationId);
|
||||||
broadcastEvent(conversationId, "done", buildDonePayload(
|
broadcastEvent(conversationId, "done", buildDonePayload(
|
||||||
conversationId, "stopped", savedAssistant, 0, 0, true, stoppedMsgCount));
|
conversationId, "stopped", savedAssistant, 0, 0,
|
||||||
|
isAssistantPersisted(savedAssistant), stoppedMsgCount));
|
||||||
} else {
|
} else {
|
||||||
broadcastEvent(conversationId, "error", buildErrorPayload(
|
broadcastEvent(conversationId, "error", buildErrorPayload(
|
||||||
conversationId,
|
conversationId,
|
||||||
@ -467,6 +481,8 @@ public class ChatController {
|
|||||||
() -> log.debug("SSE replay subscription completed: conversationId={}", conversationId));
|
() -> log.debug("SSE replay subscription completed: conversationId={}", conversationId));
|
||||||
|
|
||||||
streamTracker.setDisposable(conversationId, disposable);
|
streamTracker.setDisposable(conversationId, disposable);
|
||||||
|
streamTracker.setEmergencySaveCallback(conversationId,
|
||||||
|
() -> emergencySaveAccumulator(conversationId, accumulator));
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("SSE approval replay setup error: {}", e.getMessage());
|
log.error("SSE approval replay setup error: {}", e.getMessage());
|
||||||
@ -600,6 +616,9 @@ public class ChatController {
|
|||||||
} else if (wasStopped) {
|
} else if (wasStopped) {
|
||||||
savedAssistant = conversationService.saveMessage(conversationId, "assistant",
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant",
|
||||||
isInterruptFollowup ? "[已中断]" : "[已停止生成]", null, persistStatus);
|
isInterruptFollowup ? "[已中断]" : "[已停止生成]", null, persistStatus);
|
||||||
|
} else {
|
||||||
|
savedAssistant = saveEmptyAssistantPlaceholder(
|
||||||
|
conversationId, persistStatus, accumulator, "SSE doOnComplete");
|
||||||
}
|
}
|
||||||
// 发布对话完成事件(仅正常完成时;停止/中断/错误均不触发记忆提取)
|
// 发布对话完成事件(仅正常完成时;停止/中断/错误均不触发记忆提取)
|
||||||
// RFC-049 follow-up: also skip on isError — error turns persist
|
// RFC-049 follow-up: also skip on isError — error turns persist
|
||||||
@ -628,7 +647,8 @@ public class ChatController {
|
|||||||
int msgCount = conversationService.getMessageCount(conversationId);
|
int msgCount = conversationService.getMessageCount(conversationId);
|
||||||
broadcastEvent(conversationId, "done", buildDonePayload(
|
broadcastEvent(conversationId, "done", buildDonePayload(
|
||||||
conversationId, persistStatus, savedAssistant,
|
conversationId, persistStatus, savedAssistant,
|
||||||
accumulator.getPromptTokens(), accumulator.getCompletionTokens(), true, msgCount));
|
accumulator.getPromptTokens(), accumulator.getCompletionTokens(),
|
||||||
|
isAssistantPersisted(savedAssistant), msgCount));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("SSE complete error: {}", e.getMessage());
|
log.warn("SSE complete error: {}", e.getMessage());
|
||||||
@ -729,7 +749,8 @@ public class ChatController {
|
|||||||
));
|
));
|
||||||
int stoppedMsgCount = conversationService.getMessageCount(conversationId);
|
int stoppedMsgCount = conversationService.getMessageCount(conversationId);
|
||||||
broadcastEvent(conversationId, "done", buildDonePayload(
|
broadcastEvent(conversationId, "done", buildDonePayload(
|
||||||
conversationId, "stopped", savedAssistant, 0, 0, true, stoppedMsgCount));
|
conversationId, "stopped", savedAssistant, 0, 0,
|
||||||
|
isAssistantPersisted(savedAssistant), stoppedMsgCount));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("SSE stop finalize error: {}", e.getMessage());
|
log.warn("SSE stop finalize error: {}", e.getMessage());
|
||||||
@ -832,7 +853,8 @@ public class ChatController {
|
|||||||
));
|
));
|
||||||
int stoppedMsgCount = conversationService.getMessageCount(conversationId);
|
int stoppedMsgCount = conversationService.getMessageCount(conversationId);
|
||||||
broadcastEvent(conversationId, "done", buildDonePayload(
|
broadcastEvent(conversationId, "done", buildDonePayload(
|
||||||
conversationId, "stopped", savedAssistant, 0, 0, true, stoppedMsgCount));
|
conversationId, "stopped", savedAssistant, 0, 0,
|
||||||
|
isAssistantPersisted(savedAssistant), stoppedMsgCount));
|
||||||
} else {
|
} else {
|
||||||
broadcastEvent(conversationId, "error", buildErrorPayload(conversationId, errorMsg, savedAssistant));
|
broadcastEvent(conversationId, "error", buildErrorPayload(conversationId, errorMsg, savedAssistant));
|
||||||
}
|
}
|
||||||
@ -1234,6 +1256,13 @@ public class ChatController {
|
|||||||
accumulator.getRuntimeModelName(),
|
accumulator.getRuntimeModelName(),
|
||||||
accumulator.getRuntimeProviderId(),
|
accumulator.getRuntimeProviderId(),
|
||||||
accumulator.toMetadataJson());
|
accumulator.toMetadataJson());
|
||||||
|
} else if (queuedWasStopped) {
|
||||||
|
boolean queuedIsFollowup = queuedInterrupt == ChatStreamTracker.InterruptType.USER_INTERRUPT_WITH_FOLLOWUP;
|
||||||
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant",
|
||||||
|
queuedIsFollowup ? "[已中断]" : "[已停止生成]", null, persistStatus);
|
||||||
|
} else {
|
||||||
|
savedAssistant = saveEmptyAssistantPlaceholder(
|
||||||
|
conversationId, persistStatus, accumulator, "SSE queued doOnComplete");
|
||||||
}
|
}
|
||||||
broadcastEvent(conversationId, "message_complete", Map.of(
|
broadcastEvent(conversationId, "message_complete", Map.of(
|
||||||
"status", persistStatus,
|
"status", persistStatus,
|
||||||
@ -1242,7 +1271,8 @@ public class ChatController {
|
|||||||
));
|
));
|
||||||
broadcastEvent(conversationId, "done", buildDonePayload(
|
broadcastEvent(conversationId, "done", buildDonePayload(
|
||||||
conversationId, persistStatus, savedAssistant,
|
conversationId, persistStatus, savedAssistant,
|
||||||
accumulator.getPromptTokens(), accumulator.getCompletionTokens(), true,
|
accumulator.getPromptTokens(), accumulator.getCompletionTokens(),
|
||||||
|
isAssistantPersisted(savedAssistant),
|
||||||
conversationService.getMessageCount(conversationId)));
|
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());
|
||||||
@ -1355,6 +1385,29 @@ public class ChatController {
|
|||||||
? "interrupted" : "stopped";
|
? "interrupted" : "stopped";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String emptyAssistantPlaceholder(String status) {
|
||||||
|
if ("awaiting_approval".equals(status)) return "[等待审批]";
|
||||||
|
return "[本次没有输出]";
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isAssistantPersisted(MessageEntity savedAssistant) {
|
||||||
|
return savedAssistant != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MessageEntity saveEmptyAssistantPlaceholder(String conversationId, String status,
|
||||||
|
StreamAccumulator accumulator, String source) {
|
||||||
|
log.warn("{} with empty accumulator: conversationId={}, status={}, finishReason={}, phase={}, hasSegments={}",
|
||||||
|
source, conversationId, status, accumulator.getFinishReason(),
|
||||||
|
accumulator.getCurrentPhase(), !accumulator.segmentsEmpty());
|
||||||
|
return conversationService.saveMessage(conversationId, "assistant",
|
||||||
|
emptyAssistantPlaceholder(status), null, status,
|
||||||
|
accumulator.getPromptTokens(),
|
||||||
|
accumulator.getCompletionTokens(),
|
||||||
|
accumulator.getRuntimeModelName(),
|
||||||
|
accumulator.getRuntimeProviderId(),
|
||||||
|
accumulator.toMetadataJson());
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, Object> buildDonePayload(String conversationId, String status, MessageEntity savedAssistant,
|
private Map<String, Object> buildDonePayload(String conversationId, String status, MessageEntity savedAssistant,
|
||||||
int promptTokens, int completionTokens,
|
int promptTokens, int completionTokens,
|
||||||
boolean persisted, Integer messageCount) {
|
boolean persisted, Integer messageCount) {
|
||||||
@ -1406,6 +1459,9 @@ public class ChatController {
|
|||||||
String text = accumulator.getContent();
|
String text = accumulator.getContent();
|
||||||
List<MessageContentPart> parts = accumulator.toAssistantParts();
|
List<MessageContentPart> parts = accumulator.toAssistantParts();
|
||||||
if (text.isBlank() && parts.isEmpty()) {
|
if (text.isBlank() && parts.isEmpty()) {
|
||||||
|
log.warn("[ChatController] Emergency save skipped (empty accumulator): conversationId={}, finishReason={}, phase={}, hasSegments={}",
|
||||||
|
conversationId, accumulator.getFinishReason(), accumulator.getCurrentPhase(),
|
||||||
|
!accumulator.segmentsEmpty());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boolean awaitingApproval = accumulator.isAwaitingApproval();
|
boolean awaitingApproval = accumulator.isAwaitingApproval();
|
||||||
@ -1817,6 +1873,9 @@ public class ChatController {
|
|||||||
int getCompletionTokens() { return completionTokens; }
|
int getCompletionTokens() { return completionTokens; }
|
||||||
String getRuntimeModelName() { return runtimeModelName; }
|
String getRuntimeModelName() { return runtimeModelName; }
|
||||||
String getRuntimeProviderId() { return runtimeProviderId; }
|
String getRuntimeProviderId() { return runtimeProviderId; }
|
||||||
|
String getCurrentPhase() { return currentPhase; }
|
||||||
|
String getFinishReason() { return finishReason; }
|
||||||
|
boolean segmentsEmpty() { return segments.isEmpty(); }
|
||||||
|
|
||||||
synchronized List<MessageContentPart> toAssistantParts() {
|
synchronized List<MessageContentPart> toAssistantParts() {
|
||||||
List<MessageContentPart> parts = new ArrayList<>();
|
List<MessageContentPart> parts = new ArrayList<>();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user