mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(channel): add clear magic command
This commit is contained in:
parent
ae62faee4e
commit
d10ed9dd06
@ -0,0 +1,47 @@
|
||||
package vip.mate.channel;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* User-typed channel control commands that should be handled by the platform
|
||||
* instead of being sent to the agent as normal prompt text.
|
||||
*/
|
||||
final class ChannelMagicCommand {
|
||||
|
||||
private static final Set<String> CLEAR_COMMANDS = Set.of(
|
||||
"clear",
|
||||
"/clear",
|
||||
"reset",
|
||||
"/reset",
|
||||
"清空",
|
||||
"/清空",
|
||||
"清空上下文",
|
||||
"/清空上下文",
|
||||
"清理上下文",
|
||||
"/清理上下文",
|
||||
"清除上下文",
|
||||
"/清除上下文",
|
||||
"重置上下文",
|
||||
"/重置上下文"
|
||||
);
|
||||
|
||||
private ChannelMagicCommand() {
|
||||
}
|
||||
|
||||
static boolean isClearCommand(String text) {
|
||||
String normalized = normalize(text);
|
||||
return !normalized.isEmpty() && CLEAR_COMMANDS.contains(normalized);
|
||||
}
|
||||
|
||||
static String clearConfirmation() {
|
||||
return "✅ 上下文已清理,后续消息会从新的上下文开始。";
|
||||
}
|
||||
|
||||
private static String normalize(String text) {
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
return text.trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@ -271,6 +271,11 @@ public class ChannelMessageRouter {
|
||||
}
|
||||
channelEntity = fresh;
|
||||
|
||||
String conversationId = buildConversationId(message);
|
||||
if (handleMagicCommand(message, adapter, conversationId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fan out to the trigger pipeline FIRST — channel_message and
|
||||
// content_match triggers fire on every received message regardless
|
||||
// of whether the channel has an agent attached. If we returned
|
||||
@ -292,7 +297,6 @@ public class ChannelMessageRouter {
|
||||
}
|
||||
|
||||
String channelType = adapter.getChannelType();
|
||||
String conversationId = buildConversationId(message);
|
||||
|
||||
log.info("[{}] Enqueuing message: sender={}, conversationId={}, agentId={}",
|
||||
channelType, message.getSenderId(), conversationId, agentId);
|
||||
@ -601,6 +605,10 @@ public class ChannelMessageRouter {
|
||||
adapter.getChannelType(), message.getSenderId(), conversationId, agentId);
|
||||
|
||||
try {
|
||||
if (handleMagicCommand(message, adapter, conversationId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ======= 审批拦截层 =======
|
||||
String userText = message.getContent() != null ? message.getContent().trim() : "";
|
||||
PendingApproval pending = approvalService.findPendingByConversation(conversationId);
|
||||
@ -910,6 +918,38 @@ public class ChannelMessageRouter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle channel-native control commands before the message is persisted
|
||||
* or forwarded to the agent. Mirrors QwenPaw's control-command dispatch:
|
||||
* a recognized command is terminal for this inbound message.
|
||||
*/
|
||||
private boolean handleMagicCommand(ChannelMessage message, ChannelAdapter adapter,
|
||||
String conversationId) {
|
||||
String userText = message != null ? message.getContent() : null;
|
||||
if (!ChannelMagicCommand.isClearCommand(userText)) {
|
||||
return false;
|
||||
}
|
||||
cancelPending(conversationId);
|
||||
conversationService.clearMessages(conversationId);
|
||||
String replyTarget = resolveReplyTarget(message);
|
||||
if (replyTarget != null) {
|
||||
adapter.sendMessage(replyTarget, ChannelMagicCommand.clearConfirmation());
|
||||
}
|
||||
log.info("[{}] Magic command handled: clear conversationId={}, sender={}",
|
||||
adapter.getChannelType(), conversationId, message != null ? message.getSenderId() : null);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void cancelPending(String conversationId) {
|
||||
PendingMessage pending;
|
||||
synchronized (pendingMessages) {
|
||||
pending = pendingMessages.remove(conversationId);
|
||||
}
|
||||
if (pending != null && pending.timer != null) {
|
||||
pending.timer.cancel(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式处理路径(渠道无关)
|
||||
* <p>
|
||||
|
||||
@ -0,0 +1,80 @@
|
||||
package vip.mate.channel;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import vip.mate.agent.AgentService;
|
||||
import vip.mate.agent.context.ChatOrigin;
|
||||
import vip.mate.approval.ApprovalWorkflowService;
|
||||
import vip.mate.channel.model.ChannelEntity;
|
||||
import vip.mate.channel.notification.ApprovalNotificationService;
|
||||
import vip.mate.channel.service.ChannelService;
|
||||
import vip.mate.channel.web.ChatStreamTracker;
|
||||
import vip.mate.memory.event.ConversationCompletionPublisher;
|
||||
import vip.mate.tts.TtsService;
|
||||
import vip.mate.workspace.conversation.ConversationService;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class ChannelMagicCommandTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("clear aliases are recognized as channel magic commands")
|
||||
void clearAliasesAreRecognized() {
|
||||
assertTrue(ChannelMagicCommand.isClearCommand("clear"));
|
||||
assertTrue(ChannelMagicCommand.isClearCommand("/clear"));
|
||||
assertTrue(ChannelMagicCommand.isClearCommand(" 清空上下文 "));
|
||||
assertTrue(ChannelMagicCommand.isClearCommand("清理上下文"));
|
||||
assertTrue(ChannelMagicCommand.isClearCommand("/reset"));
|
||||
|
||||
assertFalse(ChannelMagicCommand.isClearCommand("clear 一下北京天气"));
|
||||
assertFalse(ChannelMagicCommand.isClearCommand("请清理上下文后继续"));
|
||||
assertFalse(ChannelMagicCommand.isClearCommand("/approval approve abc123"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("clear command wipes current channel conversation and does not call agent")
|
||||
void clearCommandClearsConversationWithoutCallingAgent() throws Exception {
|
||||
AgentService agentService = mock(AgentService.class);
|
||||
ConversationService conversationService = mock(ConversationService.class);
|
||||
ChannelService channelService = mock(ChannelService.class);
|
||||
ChannelSessionStore channelSessionStore = mock(ChannelSessionStore.class);
|
||||
ApprovalWorkflowService approvalService = mock(ApprovalWorkflowService.class);
|
||||
ApprovalNotificationService approvalNotificationService = mock(ApprovalNotificationService.class);
|
||||
ConversationCompletionPublisher completionPublisher = mock(ConversationCompletionPublisher.class);
|
||||
TtsService ttsService = mock(TtsService.class);
|
||||
ChatStreamTracker streamTracker = mock(ChatStreamTracker.class);
|
||||
ChannelChatOriginFactory chatOriginFactory = mock(ChannelChatOriginFactory.class);
|
||||
ChannelErrorClassifier errorClassifier = mock(ChannelErrorClassifier.class);
|
||||
ChannelMessageRouter router = new ChannelMessageRouter(agentService, conversationService,
|
||||
channelService, channelSessionStore, approvalService, approvalNotificationService,
|
||||
completionPublisher, ttsService, new ObjectMapper(), streamTracker,
|
||||
chatOriginFactory, errorClassifier);
|
||||
|
||||
ChannelAdapter adapter = mock(ChannelAdapter.class);
|
||||
when(adapter.getChannelType()).thenReturn("wecom");
|
||||
ChannelEntity channel = new ChannelEntity();
|
||||
channel.setAgentId(100L);
|
||||
ChannelMessage message = ChannelMessage.builder()
|
||||
.senderId("alice")
|
||||
.replyToken("reply-1")
|
||||
.content("/clear")
|
||||
.build();
|
||||
|
||||
Method process = ChannelMessageRouter.class.getDeclaredMethod(
|
||||
"processMessage", ChannelMessage.class, ChannelAdapter.class, ChannelEntity.class, String.class);
|
||||
process.setAccessible(true);
|
||||
process.invoke(router, message, adapter, channel, "wecom:alice");
|
||||
|
||||
verify(conversationService).clearMessages("wecom:alice");
|
||||
verify(adapter).sendMessage(eq("reply-1"), contains("上下文已清理"));
|
||||
verify(conversationService, never()).saveMessage(anyString(), anyString(), anyString(), any(), anyString());
|
||||
verify(agentService, never()).chatStructuredStream(
|
||||
anyLong(), anyString(), anyString(), anyString(), any(ChatOrigin.class));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user