Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • Channel 系统架构

Channel 系统架构

Moltbot 的多渠道消息系统,支持插件化扩展和统一路由

概述

Channel 系统是 Moltbot 的消息层,负责从各种聊天平台(WhatsApp、Telegram、Discord、Slack 等)接收消息,规范化后路由到 Agent,并将响应发送回原渠道。

设计目标

  • 插件化: 每个渠道作为独立插件实现
  • 统一路由: 所有渠道共享相同的路由和会话管理逻辑
  • 消息规范化: 将不同渠道的消息格式统一为内部表示
  • 安全策略: DM 配对、允许列表、命令权限控制
  • 轻量核心: 通过 Channel Dock 共享代码,避免依赖插件实现

Channel Plugin 接口

核心定义

文件: src/channels/plugins/types.plugin.ts

export type ChannelPlugin<ResolvedAccount = any> = {
  id: ChannelId;
  meta: ChannelMeta;
  capabilities: ChannelCapabilities;
  defaults?: {
    queue?: { debounceMs?: number; };
  };
  reload?: { configPrefixes: string[]; noopPrefixes?: string[]; };

  // 生命周期和配置适配器
  onboarding?: ChannelOnboardingAdapter;
  config: ChannelConfigAdapter<ResolvedAccount>;
  configSchema?: ChannelConfigSchema;
  setup?: ChannelSetupAdapter;

  // 安全和配对
  pairing?: ChannelPairingAdapter;
  security?: ChannelSecurityAdapter<ResolvedAccount>;

  // 消息和路由
  groups?: ChannelGroupAdapter;
  mentions?: ChannelMentionAdapter;
  outbound?: ChannelOutboundAdapter;
  commands?: ChannelCommandAdapter;

  // 状态和监控
  status?: ChannelStatusAdapter<ResolvedAccount>;
  gatewayMethods?: string[];
  gateway?: ChannelGatewayAdapter<ResolvedAccount>;
  auth?: ChannelAuthAdapter;

  // 扩展功能
  elevated?: ChannelElevatedAdapter;
  streaming?: ChannelStreamingAdapter;
  threading?: ChannelThreadingAdapter;
  messaging?: ChannelMessagingAdapter;
  agentPrompt?: ChannelAgentPromptAdapter;
  directory?: ChannelDirectoryAdapter;
  resolver?: ChannelResolverAdapter;
  actions?: ChannelMessageActionAdapter;
  heartbeat?: ChannelHeartbeatAdapter;

  // Agent 工具
  agentTools?: ChannelAgentToolFactory | ChannelAgentTool[];
};

ChannelCapabilities

export type ChannelCapabilities = {
  chatTypes: ("direct" | "group" | "channel" | "thread")[];
  nativeCommands?: boolean;    // 支持 / 命令
  blockStreaming?: boolean;      // 阻塞流式输出
  threaded?: boolean;            // 支持线程化回复
  reactions?: boolean;          // 支持 emoji 反应
  threads?: boolean;             // 支持线程
  mentions?: boolean;            // 支持 @mention
  mediaUpload?: boolean;         // 支持媒体上传
  typingIndicator?: boolean;     // 支持输入指示器
  readReceipt?: boolean;         // 支持已读回执
  presence?: boolean;            // 支持在线状态
};

Channel Dock 机制

设计原则

Channel Dock 是轻量级的通道元数据和行为定义,用于共享代码路径:

  • 保持轻量: 不包含 monitors, probes, puppeteer/web login
  • 允许: 配置读取器, allowFrom 格式化, mention 剥离模式, threading 默认值
  • 共享代码: 应该从这里导入,而不是从插件注册表导入

Dock 接口

文件: src/channels/dock.ts

export type ChannelDock = {
  id: ChannelId;
  capabilities: ChannelCapabilities;
  commands?: ChannelCommandAdapter;
  outbound?: {
    textChunkLimit?: number;
  };
  streaming?: ChannelDockStreaming;
  elevated?: ChannelElevatedAdapter;
  config?: {
    resolveAllowFrom?: (params: {
      cfg: MoltbotConfig;
      accountId?: string | null;
    }) => Array<string | number> | undefined;
    formatAllowFrom?: (params: {
      cfg: MoltbotConfig;
      accountId?: string | null;
      allowFrom: Array<string | number>;
    }) => string[];
  };
  groups?: ChannelGroupAdapter;
  mentions?: ChannelMentionAdapter;
  threading?: ChannelThreadingAdapter;
  agentPrompt?: ChannelAgentPromptAdapter;
};

Dock 示例:Telegram

const TELEGRAM_DOCK: ChannelDock = {
  id: "telegram",
  capabilities: {
    chatTypes: ["direct", "group", "channel", "thread"],
    nativeCommands: true,
    blockStreaming: true,
  },
  outbound: { textChunkLimit: 4000 },
  config: {
    resolveAllowFrom: ({ cfg, accountId }) =>
      (resolveTelegramAccount({ cfg, accountId }).config.allowFrom ?? [])
        .map((entry) => String(entry)),
    formatAllowFrom: ({ allowFrom }) =>
      allowFrom
        .map((entry) => String(entry).trim())
        .filter(Boolean)
        .map((entry) => entry.replace(/^(telegram|tg):/i, ""))
        .map((entry) => entry.toLowerCase()),
  },
  groups: {
    resolveRequireMention: resolveTelegramGroupGroupRequireMention,
    resolveToolPolicy: resolveTelegramGroupToolPolicy,
  },
  threading: {
    resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "first",
    buildToolContext: ({ context, hasRepliedRef }) => {
      const threadId = context.MessageThreadId ?? context.ReplyToId;
      return {
        currentChannelId: context.To?.trim() || undefined,
        currentThreadTs: threadId != null ? String(threadId) : undefined,
        hasRepliedRef,
      };
    },
  },
};

消息路由与会话

路由解析流程

Session Key 构建

文件: src/routing/session-key.ts

// Agent 主会话键
export function buildAgentMainSessionKey(params: {
  agentId: string;
  mainKey?: string | undefined;
}): string {
  const agentId = normalizeAgentId(params.agentId);
  const mainKey = normalizeMainKey(params.mainKey);
  return `agent:${agentId}:${mainKey}`;
}

// Agent 对等会话键 (DM/Group)
export function buildAgentPeerSessionKey(params: {
  agentId: string;
  mainKey?: string | undefined;
  channel: string;
  peerKind?: "dm" | "group" | "channel" | null;
  peerId?: string | null;
  identityLinks?: Record<string, string[]>;
  dmScope?: "main" | "per-peer" | "per-channel-peer";
}): string {
  const peerKind = params.peerKind ?? "dm";
  if (peerKind === "dm") {
    const dmScope = params.dmScope ?? "main";
    let peerId = (params.peerId ?? "").trim();

    // 处理 identity links
    const linkedPeerId = dmScope === "main" ? null
      : resolveLinkedPeerId({
          identityLinks: params.identityLinks,
          channel: params.channel,
          peerId,
        });
    if (linkedPeerId) peerId = linkedPeerId;

    peerId = peerId.toLowerCase();

    // per-channel-peer scope
    if (dmScope === "per-channel-peer" && peerId) {
      const channel = (params.channel ?? "").trim().toLowerCase() || "unknown";
      return `agent:${normalizeAgentId(params.agentId)}:${channel}:dm:${peerId}`;
    }

    // per-peer scope
    if (dmScope === "per-peer" && peerId) {
      return `agent:${normalizeAgentId(params.agentId)}:dm:${peerId}`;
    }

    // main scope
    return buildAgentMainSessionKey({
      agentId: params.agentId,
      mainKey: params.mainKey,
    });
  }

  // group/channel scope
  const channel = (params.channel ?? "").trim().toLowerCase.toLowerCase() || "unknown";
  const peerId = ((params.peerId ?? "").trim() || "unknown").toLowerCase();
  return `agent:${normalizeAgentId(params.agentId)}:${channel}:${peerKind}:${peerId}`;
}

路由匹配优先级

文件: src/routing/resolve-route.ts

  1. peer 绑定: 匹配特定用户/频道
  2. guild 绑定: 匹配 Discord guild
  3. team 绑定: 匹配 Microsoft Teams team
  4. account 绑定: 匹配特定账号(非通配符)
  5. 通配 account 绑定: 匹配所有账号(accountId: "*")
  6. 默认 Agent: 回退到配置的默认 Agent

内置通道实现

WhatsApp

文件: src/whatsapp/

消息规范化

export function normalizeWhatsAppTarget(value: string): string | null {
  const candidate = stripWhatsAppTargetPrefixes(value);
  if (!candidate) return null;

  // 处理群组 JID
  if (isWhatsAppGroupJid(candidate)) {
    const localPart = candidate.slice(0, candidate.length - "@g.us".length);
    return `${localPart}@g.us`;
  }

  // 处理用户 JID (e.g. "41796666864:0@s.whatsapp.net")
  if (isWhatsAppUserTarget(candidate)) {
    const phone = extractUserJidPhone(candidate);
    if (!phone) return null;
    const normalized = normalizeE164(phone);
    return normalized.length > 1 ? normalized : null;
  }

  // 处理普通手机号
  if (candidate.includes("@")) return null;
  const normalized = normalizeE164(candidate);
  return normalized.length > 1 ? normalized : null;
}

Telegram

文件: src/telegram/

消息规范化

export function normalizeTelegramMessagingTarget(raw: string): string | undefined {
  const trimmed = raw.trim();
  if (!trimmed) return undefined;
  let normalized = trimmed;

  // 移除前缀
  if (normalized.startsWith("telegram:")) {
    normalized = normalized.slice("telegram:".length).trim();
  } else if (normalized.startsWith("tg:")) {
    normalized = normalized.slice("tg:".length).trim();
  }

  // 解析 t.me 链接
  const tmeMatch =
    /^https?:\/\/t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized) ??
    /^t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized);
  if (tmeMatch?.[1]) normalized = `@${tmeMatch[1]}`;

  if (!normalized) return undefined;
  return `telegram:${normalized}`.toLowerCase();
}

Discord

文件: src/discord/

消息规范化

export function normalizeDiscordMessagingTarget(raw: string): string | undefined {
  // 默认裸 ID 到 channels 以便路由在工具操作中稳定
  const target = parseDiscordTarget(raw, { defaultKind: "channel" });
  return target?.normalized;
}

安全和认证机制

Allowlist 匹配

文件: src/channels/allowlist-match.ts

export type AllowlistMatchSource =
  | "wildcard"
  | "id"
  | "name"
  | "tag"
  | "username"
  | "prefixed-id"
  | "prefixed-user"
  | "prefixed-name"
  | "prefixed-tag";

export type AllowlistMatch = {
  matched: boolean;
  source?: AllowlistMatchSource;
  value?: string;
};

export function matchAllowlist(params: {
  cfg: MoltbotConfig;
  channel: string;
  accountId?: string | null;
  targetId: string;
  targetName?: string | null;
  targetTag?: string | null;
  targetUsername?: string | null;
}): AllowlistMatch {
  // 1. 检查通配符 "*"
  if (allowlist.includes("*")) {
    return { matched: true, source: "wildcard", value: "*" };
  }

  // 2. 检查裸 ID
  if (allowlist.includes(targetId)) {
    return { matched: true, source: "id", value: targetId };
  }

  // 3. 检查前缀 ID (e.g., "telegram:123456")
  const prefixedId = `${channel}:${targetId}`;
  if (allowlist.includes(prefixedId)) {
    return { matched: true, source: "prefixed-id", value: prefixedId };
  }

  // 4. 检查名称
  if (targetName && allowlist.includes(targetName)) {
    return { matched: true, source: "name", value: targetName };
  }

  // 5. 其他匹配逻辑...
  return { matched: false };
}

DM 配对策略

文件: src/channels/dock.ts

export type DmPolicy = "open" | "pairing" | "closed";

// "open": 所有 DM 都接受
// "pairing": 未知发送者需要配对码
// "closed": 拒绝所有 DM

配对流程:

  1. 未知发送者发送消息
  2. Gateway 生成 6 位配对码
  3. 发送配对消息给发送者
  4. 用户使用 moltbot pairing approve <channel> <code> 批准
  5. 发送者加入允许列表

插件加载机制

文件: src/channels/plugins/index.ts

export function listChannelPlugins(): ChannelPlugin[] {
  const registry = requireActivePluginRegistry();
  return registry.channels.map((entry) => entry.plugin);
}

export function getChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
  const resolvedId = String(id).trim();
  if (!resolvedId) return undefined;
  return listChannelPlugins().find((plugin) => plugin.id === resolvedId);
}

代码路径引用

功能文件路径
Channel Plugin 接口src/channels/plugins/types.plugin.ts
Channel Docksrc/channels/dock.ts
消息规范化src/channels/plugins/normalize/
路由解析src/routing/resolve-route.ts
Session Key 构建src/routing/session-key.ts
Allowlist 匹配src/channels/allowlist-match.ts
WhatsApp 实现src/whatsapp/
Telegram 实现src/telegram/
Discord 实现src/discord/
Slack 实现src/slack/