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
- peer 绑定: 匹配特定用户/频道
- guild 绑定: 匹配 Discord guild
- team 绑定: 匹配 Microsoft Teams team
- account 绑定: 匹配特定账号(非通配符)
- 通配 account 绑定: 匹配所有账号(
accountId: "*") - 默认 Agent: 回退到配置的默认 Agent
内置通道实现
文件: 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
配对流程:
- 未知发送者发送消息
- Gateway 生成 6 位配对码
- 发送配对消息给发送者
- 用户使用
moltbot pairing approve <channel> <code>批准 - 发送者加入允许列表
插件加载机制
文件: 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 Dock | src/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/ |