mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
refactor(memory): unify ConversationCompletedEvent publish
This commit is contained in:
parent
2e15369465
commit
869e0c47e6
@ -10,8 +10,7 @@ import vip.mate.channel.model.ChannelEntity;
|
|||||||
import vip.mate.channel.notification.ApprovalNotificationService;
|
import vip.mate.channel.notification.ApprovalNotificationService;
|
||||||
import vip.mate.channel.service.ChannelService;
|
import vip.mate.channel.service.ChannelService;
|
||||||
import vip.mate.channel.web.ChatStreamTracker;
|
import vip.mate.channel.web.ChatStreamTracker;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import vip.mate.memory.event.ConversationCompletionPublisher;
|
||||||
import vip.mate.memory.event.ConversationCompletedEvent;
|
|
||||||
import vip.mate.tts.TtsService;
|
import vip.mate.tts.TtsService;
|
||||||
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;
|
||||||
@ -50,7 +49,7 @@ public class ChannelMessageRouter {
|
|||||||
private final ChannelSessionStore channelSessionStore;
|
private final ChannelSessionStore channelSessionStore;
|
||||||
private final ApprovalService approvalService;
|
private final ApprovalService approvalService;
|
||||||
private final ApprovalNotificationService approvalNotificationService;
|
private final ApprovalNotificationService approvalNotificationService;
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
private final ConversationCompletionPublisher completionPublisher;
|
||||||
private final TtsService ttsService;
|
private final TtsService ttsService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final ChatStreamTracker streamTracker;
|
private final ChatStreamTracker streamTracker;
|
||||||
@ -95,7 +94,7 @@ public class ChannelMessageRouter {
|
|||||||
ChannelSessionStore channelSessionStore,
|
ChannelSessionStore channelSessionStore,
|
||||||
ApprovalService approvalService,
|
ApprovalService approvalService,
|
||||||
ApprovalNotificationService approvalNotificationService,
|
ApprovalNotificationService approvalNotificationService,
|
||||||
ApplicationEventPublisher eventPublisher,
|
ConversationCompletionPublisher completionPublisher,
|
||||||
TtsService ttsService,
|
TtsService ttsService,
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
ChatStreamTracker streamTracker) {
|
ChatStreamTracker streamTracker) {
|
||||||
@ -105,7 +104,7 @@ public class ChannelMessageRouter {
|
|||||||
this.channelSessionStore = channelSessionStore;
|
this.channelSessionStore = channelSessionStore;
|
||||||
this.approvalService = approvalService;
|
this.approvalService = approvalService;
|
||||||
this.approvalNotificationService = approvalNotificationService;
|
this.approvalNotificationService = approvalNotificationService;
|
||||||
this.eventPublisher = eventPublisher;
|
this.completionPublisher = completionPublisher;
|
||||||
this.ttsService = ttsService;
|
this.ttsService = ttsService;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.streamTracker = streamTracker;
|
this.streamTracker = streamTracker;
|
||||||
@ -612,17 +611,13 @@ public class ChannelMessageRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发布对话完成事件(触发异步记忆提取),失败不影响正常流程
|
* Publish the conversation-completed event (triggers async memory extraction).
|
||||||
|
* Delegates to {@link ConversationCompletionPublisher} so the try/catch and
|
||||||
|
* messageCount lookup no longer live here.
|
||||||
*/
|
*/
|
||||||
private void publishConversationCompletedEvent(Long agentId, String conversationId,
|
private void publishConversationCompletedEvent(Long agentId, String conversationId,
|
||||||
String userMessage, String assistantReply) {
|
String userMessage, String assistantReply) {
|
||||||
try {
|
completionPublisher.publish(agentId, conversationId, userMessage, assistantReply, "channel");
|
||||||
int msgCount = conversationService.getMessageCount(conversationId);
|
|
||||||
eventPublisher.publishEvent(new ConversationCompletedEvent(
|
|
||||||
agentId, conversationId, userMessage, assistantReply, msgCount, "channel"));
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.debug("[Memory] Failed to publish ConversationCompletedEvent: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 流式处理(Web 渠道专用,不走队列) ====================
|
// ==================== 流式处理(Web 渠道专用,不走队列) ====================
|
||||||
|
|||||||
@ -14,13 +14,12 @@ import org.springframework.security.core.Authentication;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
|
||||||
import vip.mate.common.result.R;
|
import vip.mate.common.result.R;
|
||||||
import vip.mate.agent.AgentService;
|
import vip.mate.agent.AgentService;
|
||||||
import vip.mate.agent.model.AgentEntity;
|
import vip.mate.agent.model.AgentEntity;
|
||||||
import vip.mate.approval.ApprovalService;
|
import vip.mate.approval.ApprovalService;
|
||||||
import vip.mate.approval.PendingApproval;
|
import vip.mate.approval.PendingApproval;
|
||||||
import vip.mate.memory.event.ConversationCompletedEvent;
|
import vip.mate.memory.event.ConversationCompletionPublisher;
|
||||||
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 vip.mate.workspace.conversation.model.MessageEntity;
|
||||||
@ -59,7 +58,7 @@ public class ChatController {
|
|||||||
private final ApprovalService approvalService;
|
private final ApprovalService approvalService;
|
||||||
private final ChatStreamTracker streamTracker;
|
private final ChatStreamTracker streamTracker;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
private final ConversationCompletionPublisher completionPublisher;
|
||||||
private final Path uploadRoot = Paths.get("data", "chat-uploads");
|
private final Path uploadRoot = Paths.get("data", "chat-uploads");
|
||||||
|
|
||||||
// 使用虚拟线程池处理 SSE(Java 17+ 兼容,Java 21 可用 Executors.newVirtualThreadPerTaskExecutor())
|
// 使用虚拟线程池处理 SSE(Java 17+ 兼容,Java 21 可用 Executors.newVirtualThreadPerTaskExecutor())
|
||||||
@ -464,13 +463,7 @@ public class ChatController {
|
|||||||
}
|
}
|
||||||
// 发布对话完成事件(仅正常完成时,停止/中断不触发记忆提取)
|
// 发布对话完成事件(仅正常完成时,停止/中断不触发记忆提取)
|
||||||
if (!wasStopped) {
|
if (!wasStopped) {
|
||||||
try {
|
completionPublisher.publish(agentId, conversationId, message, assistantText, "web");
|
||||||
int msgCount = conversationService.getMessageCount(conversationId);
|
|
||||||
eventPublisher.publishEvent(new ConversationCompletedEvent(
|
|
||||||
agentId, conversationId, message, assistantText, msgCount, "web"));
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.debug("[Memory] Failed to publish ConversationCompletedEvent: {}", ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInterruptFollowup) {
|
if (isInterruptFollowup) {
|
||||||
@ -806,14 +799,7 @@ public class ChatController {
|
|||||||
String promptText = buildPromptText(request.getMessage(), request.getContentParts());
|
String promptText = buildPromptText(request.getMessage(), request.getContentParts());
|
||||||
String response = agentService.chat(agentId, promptText, request.getConversationId());
|
String response = agentService.chat(agentId, promptText, request.getConversationId());
|
||||||
conversationService.saveMessage(request.getConversationId(), "assistant", response);
|
conversationService.saveMessage(request.getConversationId(), "assistant", response);
|
||||||
// 发布对话完成事件
|
completionPublisher.publish(agentId, request.getConversationId(), request.getMessage(), response, "web");
|
||||||
try {
|
|
||||||
int msgCount = conversationService.getMessageCount(request.getConversationId());
|
|
||||||
eventPublisher.publishEvent(new ConversationCompletedEvent(
|
|
||||||
agentId, request.getConversationId(), request.getMessage(), response, msgCount, "web"));
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.debug("[Memory] Failed to publish ConversationCompletedEvent: {}", ex.getMessage());
|
|
||||||
}
|
|
||||||
return R.ok(response);
|
return R.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import org.springframework.web.socket.TextMessage;
|
|||||||
import org.springframework.web.socket.WebSocketSession;
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
|
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
|
||||||
import vip.mate.agent.AgentService;
|
import vip.mate.agent.AgentService;
|
||||||
|
import vip.mate.memory.event.ConversationCompletionPublisher;
|
||||||
import vip.mate.stt.SttService;
|
import vip.mate.stt.SttService;
|
||||||
import vip.mate.tts.TtsService;
|
import vip.mate.tts.TtsService;
|
||||||
import vip.mate.workspace.conversation.ConversationService;
|
import vip.mate.workspace.conversation.ConversationService;
|
||||||
@ -48,6 +49,7 @@ public class TalkModeWebSocketHandler extends AbstractWebSocketHandler {
|
|||||||
private final TtsService ttsService;
|
private final TtsService ttsService;
|
||||||
private final AgentService agentService;
|
private final AgentService agentService;
|
||||||
private final ConversationService conversationService;
|
private final ConversationService conversationService;
|
||||||
|
private final ConversationCompletionPublisher completionPublisher;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||||
@ -148,6 +150,10 @@ public class TalkModeWebSocketHandler extends AbstractWebSocketHandler {
|
|||||||
// 6. 保存助手回复
|
// 6. 保存助手回复
|
||||||
conversationService.saveMessage(talkSession.conversationId, "assistant", reply, List.of());
|
conversationService.saveMessage(talkSession.conversationId, "assistant", reply, List.of());
|
||||||
|
|
||||||
|
// Publish conversation-completed event so memory extraction runs for voice turns too.
|
||||||
|
completionPublisher.publish(talkSession.agentId, talkSession.conversationId,
|
||||||
|
transcript, reply, "talk");
|
||||||
|
|
||||||
// 7. 推送文字回复
|
// 7. 推送文字回复
|
||||||
sendJson(session, Map.of("type", "reply", "text", reply));
|
sendJson(session, Map.of("type", "reply", "text", reply));
|
||||||
|
|
||||||
|
|||||||
@ -18,9 +18,8 @@ import vip.mate.agent.repository.AgentMapper;
|
|||||||
import vip.mate.cron.model.CronJobDTO;
|
import vip.mate.cron.model.CronJobDTO;
|
||||||
import vip.mate.cron.model.CronJobEntity;
|
import vip.mate.cron.model.CronJobEntity;
|
||||||
import vip.mate.cron.repository.CronJobMapper;
|
import vip.mate.cron.repository.CronJobMapper;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
|
||||||
import vip.mate.exception.MateClawException;
|
import vip.mate.exception.MateClawException;
|
||||||
import vip.mate.memory.event.ConversationCompletedEvent;
|
import vip.mate.memory.event.ConversationCompletionPublisher;
|
||||||
import vip.mate.workspace.conversation.ConversationService;
|
import vip.mate.workspace.conversation.ConversationService;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@ -48,7 +47,7 @@ public class CronJobService implements ApplicationRunner {
|
|||||||
private final AgentMapper agentMapper;
|
private final AgentMapper agentMapper;
|
||||||
private final AgentService agentService;
|
private final AgentService agentService;
|
||||||
private final ConversationService conversationService;
|
private final ConversationService conversationService;
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
private final ConversationCompletionPublisher completionPublisher;
|
||||||
|
|
||||||
private final ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
private final ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||||
private final ConcurrentHashMap<Long, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<Long, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
|
||||||
@ -279,13 +278,7 @@ public class CronJobService implements ApplicationRunner {
|
|||||||
conversationService.saveMessage(conversationId, "assistant", result);
|
conversationService.saveMessage(conversationId, "assistant", result);
|
||||||
|
|
||||||
// 发布对话完成事件
|
// 发布对话完成事件
|
||||||
try {
|
completionPublisher.publish(job.getAgentId(), conversationId, userMessage, result, "cron");
|
||||||
int msgCount = conversationService.getMessageCount(conversationId);
|
|
||||||
eventPublisher.publishEvent(new ConversationCompletedEvent(
|
|
||||||
job.getAgentId(), conversationId, userMessage, result, msgCount, "cron"));
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.debug("[Memory] Failed to publish ConversationCompletedEvent: {}", ex.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 合并更新 lastRunTime + nextRunTime,单次 DB 写入
|
// 合并更新 lastRunTime + nextRunTime,单次 DB 写入
|
||||||
updateRunTimes(job.getId(), job.getCronExpression(), job.getTimezone());
|
updateRunTimes(job.getId(), job.getCronExpression(), job.getTimezone());
|
||||||
|
|||||||
@ -0,0 +1,71 @@
|
|||||||
|
package vip.mate.memory.event;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import vip.mate.workspace.conversation.ConversationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single source of truth for publishing {@link ConversationCompletedEvent}.
|
||||||
|
* <p>
|
||||||
|
* Callers (chat controllers, channel routers, cron executor, talk-mode handler …)
|
||||||
|
* should invoke {@link #publish(Long, String, String, String, String)} once per
|
||||||
|
* fully completed turn — i.e. after both the user and assistant messages have
|
||||||
|
* been persisted via {@link ConversationService}.
|
||||||
|
* <p>
|
||||||
|
* This helper hides the boilerplate of looking up message count, building the
|
||||||
|
* event record, and swallowing publish-time exceptions so that a memory hook
|
||||||
|
* failure never surfaces to the user.
|
||||||
|
* <p>
|
||||||
|
* Introduced to close the P0 gap in RFC-040 §6.1: three historical call sites
|
||||||
|
* hand-rolled the same try/catch, and several real entry points forgot to
|
||||||
|
* publish the event at all. Routing every site through this bean makes the
|
||||||
|
* memory production pipeline observable and uniform.
|
||||||
|
*
|
||||||
|
* @author MateClaw Team
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ConversationCompletionPublisher {
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
private final ConversationService conversationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish a {@link ConversationCompletedEvent} for the given turn.
|
||||||
|
* Must be called <em>after</em> the assistant message is persisted, because
|
||||||
|
* {@link PostConversationMemoryListener} uses {@code messageCount} as a
|
||||||
|
* summarization threshold and downstream summarizer reads the DB.
|
||||||
|
*
|
||||||
|
* @param agentId agent id the turn ran under
|
||||||
|
* @param conversationId conversation/session id
|
||||||
|
* @param userMessage last user message (for summarizer heuristics)
|
||||||
|
* @param assistantReply agent's final reply (may be empty for replay-only turns)
|
||||||
|
* @param source trigger source label: {@code web}, {@code channel},
|
||||||
|
* {@code cron}, {@code talk}, {@code replay}, …
|
||||||
|
*/
|
||||||
|
public void publish(Long agentId,
|
||||||
|
String conversationId,
|
||||||
|
String userMessage,
|
||||||
|
String assistantReply,
|
||||||
|
String source) {
|
||||||
|
if (agentId == null || conversationId == null || conversationId.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int messageCount = conversationService.getMessageCount(conversationId);
|
||||||
|
eventPublisher.publishEvent(new ConversationCompletedEvent(
|
||||||
|
agentId,
|
||||||
|
conversationId,
|
||||||
|
userMessage != null ? userMessage : "",
|
||||||
|
assistantReply != null ? assistantReply : "",
|
||||||
|
messageCount,
|
||||||
|
source != null ? source : "unknown"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("[Memory] Failed to publish ConversationCompletedEvent (source={}, conv={}): {}",
|
||||||
|
source, conversationId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user