docs/developers/daemon/15-channel-adapters.md
packages/channels/ 是 IM 渠道适配器,把聊天平台的入站消息翻成 daemon prompt,把 daemon 的出站事件翻回平台消息。现已落地三个具体渠道:钉钉、微信(Weixin)、Telegram。它们共享 packages/channels/base/ 基座加 DaemonChannelBridge —— 后者做 session 多路复用 + SSE 消费。
每个渠道按可配的 SessionScope(per-sender / per-group 等)把一段会话(或一群)映射到一个 daemon session。适配器委托给 DaemonChannelBridge,bridge 委托给 SDK 的 DaemonSessionClient(见 13-sdk-daemon-client.md)。
DaemonChannelSessionFactory 把 (senderId, groupId?) 解析成 daemon session。ChannelConfig.approvalMode 自动批准。DaemonChannelBridge(共享基座,packages/channels/base/src/DaemonChannelBridge.ts)class DaemonChannelBridge extends EventEmitter {
constructor(opts: {
sessionFactory: DaemonChannelSessionFactory;
config: ChannelConfig;
});
handleInbound(envelope: Envelope): Promise<void>;
shutdown(): Promise<void>;
}
持有 Map<chatId, ActiveSession>,key 是渠道的 chat id(sender / group)。每条记录包括:
DaemonChannelSessionClient(去掉渠道无关方法的 DaemonSessionClient)。发的事件:permission_request、permission_resolved、outbound_message、stream_error、session_died。渠道适配器把它们接到平台原生 API。
ChannelBase(packages/channels/base/src/ChannelBase.ts)每个适配器继承的抽象基:
abstract class ChannelBase {
abstract start(): Promise<void>;
abstract sendOutbound(target, payload): Promise<void>;
handleInbound(envelope: Envelope): Promise<void>; // → bridge.handleInbound
shutdown(): Promise<void>;
}
承担共性:sender / group gating、块流式发送(块大小、节流)、入站去抖。
| 适配器 | 文件 | 传输 | 备注 |
|---|---|---|---|
| 钉钉 | packages/channels/dingtalk/src/DingtalkAdapter.ts | DingTalk Stream SDK WebSocket | 通过 sessionWebhook POST 出站;媒体图片走 DT API 下载,base64 进 envelope |
| 微信(Weixin) | packages/channels/weixin/src/WeixinAdapter.ts | iLink Bot HTTP 长轮询 | 通过专有 sendText / sendImage 出站;带打字指示 |
| Telegram | packages/channels/telegram/src/TelegramAdapter.ts | Telegram Bot API 长轮询(grammy) | 通过 sendMessage 发 HTML 块 |
每个适配器实现:
{ senderId, groupId?, text, media?, raw })。ChannelBase)。| 适配器 | 传输 | 身份 | 权限 UX | 自动批准 |
|---|---|---|---|---|
| 钉钉 | WebSocket 流 | senderStaffId(群里 + conversationId) | 通过 DT markdown 内联按钮 | ChannelConfig.approvalMode = 'auto' | 'prompt' |
| 微信 | HTTP 长轮询 | senderWxid(群里 + groupWxid) | 纯文本提示 + 回复 token | 同上 |
| Telegram | Bot API 长轮询 | from.id(群里 + chat.id) | inline keyboard 按钮 | 同上 |
sequenceDiagram
autonumber
participant CH as Channel platform
participant AD as Channel adapter
participant CB as ChannelBase
participant BR as DaemonChannelBridge
participant SC as DaemonChannelSessionClient
participant D as Daemon
CH-->>AD: inbound message
AD->>AD: build Envelope { senderId, groupId?, text, media? }
AD->>CB: handleInbound(envelope)
CB->>CB: sender / group gating
CB->>BR: handleInbound(envelope)
BR->>BR: resolve chatId → ActiveSession (create-or-attach via factory)
BR->>SC: session.prompt({...})
SC->>D: POST /session/:id/prompt
sequenceDiagram
autonumber
participant D as Daemon
participant SC as DaemonChannelSessionClient
participant BR as DaemonChannelBridge
participant AD as Channel adapter
participant CH as Channel platform
D-->>SC: SSE: session_update (agent_message_chunk)
SC-->>BR: DaemonEvent
BR->>BR: reduce → outbound chunks (block streaming)
BR-->>AD: emit 'outbound_message'
AD->>CH: sendText / sendMessage / sendChunk
sequenceDiagram
autonumber
participant D as Daemon
participant SC as DaemonChannelSessionClient
participant BR as DaemonChannelBridge
participant AD as Channel adapter
D-->>SC: SSE: permission_request
SC-->>BR: DaemonEvent
alt config.approvalMode == 'auto'
BR->>SC: session.respondToPermission({...})
else 'prompt'
BR-->>AD: emit 'permission_request' (renders chat-native UI)
AD->>BR: user picks option → respondToPermission
end
DaemonChannelBridge 与渠道适配器同生命周期;里面的 session 按 chat 维度活。DaemonSessionClient.events() 跟踪 lastSeenEventId,重放正确。shutdown() 关掉所有活 session 和底层传输(渠道的 WebSocket / 长轮询)。timeout 参数。packages/channels/base/ —— ChannelBase、DaemonChannelBridge、types.ts(ChannelConfig、Envelope、SessionScope、ChannelPlugin)。packages/sdk-typescript/src/daemon/ —— DaemonSessionClient 等。@dingtalk/stream(钉钉)、专有 iLink Bot HTTP(微信)、grammy(Telegram)。ChannelConfig(packages/channels/base/src/types.ts):
| 旋钮 | 效果 |
|---|---|
sessionScope | 'per-sender'、'per-group'、'per-thread'(渠道定义) |
approvalMode | 'auto'(自动应答) / 'prompt'(渲染 UI) |
allowlist?: string[] | 允许的 sender id,缺省 = 开放 |
denylist?: string[] | 拒绝的 sender id |
chunkSize、chunkIntervalMs | 出站块流参数 |
daemon: { baseUrl, token?, clientId? } | 传给 DaemonChannelSessionFactory |
每渠道还有自己的 key(钉钉:streamCredentials;微信:ilinkUrl、botId;Telegram:botToken)。
@qwen-code/sdk**。走 ChannelBase → DaemonChannelBridge → DaemonChannelSessionClient(bridge 从 SDK 构造)。这层间接让 bridge 可以换实现(如测试 stub),渠道无感。permission_mediation 策略仍然生效;自动批准只是渠道不问人而已。不要把 auto 与 enforce 级工作流叠加。DaemonChannelBridge 只切块;微信单消息大小、Telegram flood 限制需要适配器处理。packages/channels/base/src/DaemonChannelBridge.tspackages/channels/base/src/ChannelBase.tspackages/channels/base/src/types.tspackages/channels/dingtalk/src/DingtalkAdapter.tspackages/channels/weixin/src/WeixinAdapter.tspackages/channels/telegram/src/TelegramAdapter.tspackages/channels/plugin-example/(reference 插件骨架)../channel-plugins.md。13-sdk-daemon-client.md。