package vip.mate.channel; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import vip.mate.channel.health.ChannelHealth; import vip.mate.channel.model.ChannelEntity; import java.time.Instant; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; /** * 渠道适配器抽象基类 *
* 渠道适配器抽象基类设计:
* - 统一的生命周期管理(start/stop/isRunning)
* - Bot 前缀过滤(群消息中只响应 @bot 或指定前缀的消息)
* - 配置解析(从 ChannelEntity.configJson 读取渠道特有配置)
* - 消息路由(通过 ChannelMessageRouter 转发到 Agent)
*
* @author MateClaw Team
*/
@Slf4j
public abstract class AbstractChannelAdapter implements ChannelAdapter {
protected final ChannelEntity channelEntity;
protected final ChannelMessageRouter messageRouter;
protected final ObjectMapper objectMapper;
protected final AtomicBoolean running = new AtomicBoolean(false);
/** 解析后的渠道配置 */
protected Map
* 子类在检测到连接断开时调用此方法。方法内部会:
* 1. 检查是否已超过最大重试次数
* 2. 计算下一次重试延迟
* 3. 通过 ScheduledExecutorService 调度 {@link #doReconnect()}
*/
protected void scheduleReconnect() {
if (!running.get()) {
log.debug("[{}] Not running, skipping reconnect", getChannelType());
return;
}
if (backoff.isExhausted()) {
connectionState.set(ConnectionState.ERROR);
lastError = "Max reconnect attempts (" + backoff.getMaxAttempts() + ") exhausted";
log.error("[{}] {}: {}", getChannelType(), channelEntity.getName(), lastError);
return;
}
connectionState.set(ConnectionState.RECONNECTING);
long delayMs = backoff.nextDelayMs();
log.info("[{}] Scheduling reconnect for {} in {}ms (attempt #{})",
getChannelType(), channelEntity.getName(), delayMs, backoff.getAttempts());
reconnectFuture = ensureReconnectScheduler().schedule(() -> {
if (!running.get()) return;
try {
doReconnect();
onReconnectSuccess();
} catch (Exception e) {
onReconnectFailed(e);
}
}, delayMs, TimeUnit.MILLISECONDS);
}
/**
* 实际重连逻辑(子类覆写)
*
* 默认实现调用 doStop() + doStart(),子类可覆写以实现更细粒度的重连。
*/
protected void doReconnect() {
log.info("[{}] Reconnecting: {}", getChannelType(), channelEntity.getName());
try {
doStop();
} catch (Exception e) {
log.debug("[{}] doStop during reconnect: {}", getChannelType(), e.getMessage());
}
doStart();
}
/**
* 连接断开时调用(子类调用此方法触发重连流程)
*/
protected void onDisconnected(String reason) {
if (!running.get()) return;
lastError = reason;
log.warn("[{}] Disconnected: {} - {}", getChannelType(), channelEntity.getName(), reason);
scheduleReconnect();
}
/**
* 重连成功回调
*/
protected void onReconnectSuccess() {
backoff.reset();
connectionState.set(ConnectionState.CONNECTED);
lastEventTimeMs.set(System.currentTimeMillis());
lastError = null;
log.info("[{}] Reconnected successfully: {} (backoff reset)",
getChannelType(), channelEntity.getName());
}
/**
* 重连失败回调
*/
protected void onReconnectFailed(Exception e) {
lastError = e.getMessage();
log.warn("[{}] Reconnect failed for {}: {} (attempt #{})",
getChannelType(), channelEntity.getName(), e.getMessage(), backoff.getAttempts());
scheduleReconnect();
}
@Override
public void start() {
if (running.compareAndSet(false, true)) {
log.info("[{}] Starting channel: {}", getChannelType(), channelEntity.getName());
try {
doStart();
connectionState.set(ConnectionState.CONNECTED);
lastEventTimeMs.set(System.currentTimeMillis());
lastError = null;
backoff.reset();
log.info("[{}] Channel started successfully: {}", getChannelType(), channelEntity.getName());
} catch (Exception e) {
running.set(false);
connectionState.set(ConnectionState.ERROR);
lastError = e.getMessage();
log.error("[{}] Failed to start channel {}: {}", getChannelType(), channelEntity.getName(), e.getMessage(), e);
throw new RuntimeException("Channel start failed: " + e.getMessage(), e);
}
}
}
@Override
public void stop() {
if (running.compareAndSet(true, false)) {
log.info("[{}] Stopping channel: {}", getChannelType(), channelEntity.getName());
// 取消挂起的重连任务
if (reconnectFuture != null) {
reconnectFuture.cancel(false);
reconnectFuture = null;
}
if (reconnectScheduler != null && !reconnectScheduler.isShutdown()) {
reconnectScheduler.shutdownNow();
reconnectScheduler = null;
}
try {
doStop();
log.info("[{}] Channel stopped: {}", getChannelType(), channelEntity.getName());
} catch (Exception e) {
log.error("[{}] Error stopping channel {}: {}", getChannelType(), channelEntity.getName(), e.getMessage(), e);
}
connectionState.set(ConnectionState.DISCONNECTED);
}
}
@Override
public boolean isRunning() {
return running.get();
}
/**
* Map the existing {@code ConnectionState} machine to a typed
* {@link ChannelHealth} snapshot. The "UP" status requires
* {@code running=true} AND {@code state==CONNECTED} — this is what
* makes the green dot honest: "stopped admins" and "started but
* disconnected" both report something other than UP.
*/
@Override
public ChannelHealth health() {
Long id = channelEntity != null ? channelEntity.getId() : null;
String type = getChannelType();
if (!running.get()) {
return ChannelHealth.outOfService(type, id);
}
Instant lastEvent = Instant.ofEpochMilli(lastEventTimeMs.get());
ConnectionState s = connectionState.get();
return switch (s) {
case CONNECTED -> ChannelHealth.up(type, id, lastEvent);
case RECONNECTING -> ChannelHealth.reconnecting(type, id,
lastError != null ? lastError : "reconnecting", lastEvent);
case ERROR -> ChannelHealth.down(type, id,
lastError != null ? lastError : "channel error", lastEvent);
case DISCONNECTED -> ChannelHealth.down(type, id,
"disconnected", lastEvent);
};
}
/**
* RFC-024 Change 1:刷新"活跃时间"的标准入口。
*
* 任何代表"连接仍然有效"的事件都应调用本方法:
*
*
* 这样 {@code ChannelHealthMonitor} 才能准确区分"连接僵尸但用户没发消息" vs
* "连接真的死了",不再依赖消息密度这个稀疏信号。
* 实现 require_mention / bot_prefix 过滤机制: * - 私聊(chatId == null 或等于 senderId):始终处理 * - 群聊:如果设置了 botPrefix,只处理以该前缀开头的消息 */ protected boolean shouldProcess(ChannelMessage message) { String botPrefix = channelEntity.getBotPrefix(); if (botPrefix == null || botPrefix.isBlank()) { return true; // 未设置前缀,处理所有消息 } // 私聊始终处理 if (isDirectMessage(message)) { return true; } // 群聊检查前缀 String content = message.getContent(); return content != null && content.trim().startsWith(botPrefix.trim()); } /** * 清理消息中的 bot 前缀 */ protected String cleanBotPrefix(String content) { if (content == null) return ""; String botPrefix = channelEntity.getBotPrefix(); if (botPrefix != null && !botPrefix.isBlank() && content.trim().startsWith(botPrefix.trim())) { return content.trim().substring(botPrefix.trim().length()).trim(); } return content.trim(); } /** * 检查消息是否包含非文本内容部分(图片、文件、视频等) */ private boolean hasContentParts(ChannelMessage message) { if (message.getContentParts() == null || message.getContentParts().isEmpty()) { return false; } return message.getContentParts().stream() .anyMatch(p -> p != null && !"text".equals(p.getType())); } /** * 判断是否为私聊消息 */ protected boolean isDirectMessage(ChannelMessage message) { return message.getChatId() == null || message.getChatId().equals(message.getSenderId()); } // ==================== 访问控制 ==================== /** * 检查消息发送者是否有权访问此渠道 *
* 基于策略的访问控制设计:
* - dm_policy / group_policy:控制私聊/群聊是否开放
* - allow_from:用户白名单
* - deny_message:拒绝时的提示消息
* - require_mention:群聊中是否需要 @机器人
*/
protected boolean checkAccess(ChannelMessage message) {
boolean isDM = isDirectMessage(message);
// 1. 检查私聊/群聊策略
String policy = isDM
? getConfigString("dm_policy", "open")
: getConfigString("group_policy", "open");
if ("closed".equals(policy)) {
log.info("[{}] {} blocked by {} policy=closed, sender={}",
getChannelType(), isDM ? "DM" : "Group", isDM ? "dm" : "group", message.getSenderId());
sendDenyMessage(message);
return false;
}
// 2. 群聊中检查 require_mention(需要 @机器人才响应)
// 注:如果已设置 botPrefix,shouldProcess() 已处理;此处处理 configJson 中的 require_mention
if (!isDM && getConfigBoolean("require_mention", false)) {
String botPrefix = channelEntity.getBotPrefix();
if (botPrefix == null || botPrefix.isBlank()) {
// 设置了 require_mention 但没有 botPrefix,无法判断 mention,放行
log.debug("[{}] require_mention=true but no botPrefix configured, allowing", getChannelType());
}
// 如果有 botPrefix,shouldProcess() 已经过滤过非 mention 消息,此处放行
}
// 3. 检查 allow_from 白名单
List