新增 Channel
对应源码
| 扩展环节 | 源码 |
|---|---|
| Channel 基类契约 | nanobot/channels/base.py::BaseChannel |
| 发现和插件加载 | nanobot/channels/registry.py、nanobot/channels/manager.py |
| 入站消息、安全和 Pairing | nanobot/channels/manager.py、nanobot/pairing/store.py |
| 出站、stream、reasoning 和 media | nanobot/bus/events.py、nanobot/channels/manager.py、具体 Channel |
| 配置接入 | nanobot/config/schema.py、nanobot/cli/commands.py::onboard |
| 测试 | tests/channels/、tests/pairing/ |
学习目标
读完后,你应当能够:
- 用
BaseChannel实现最小入站/出站适配; - 通过
nanobot.channelsentry point 发布外部 plugin; - 正确处理 allowlist、pairing、stream、reasoning、media 和关闭;
- 用 fake platform client 测试,而不连接真实聊天平台。
前置知识
- 理解 Channel 与 Gateway;
- 能编写长运行异步任务;
- 知道平台 user id、chat id 和 message id 不一定相同。
1. Channel 的最小职责
Channel 只做平台适配:
flowchart LR A["平台事件"] --> B["解析 sender/chat/content/media"] B --> C["BaseChannel._handle_message"] C --> D["InboundMessage"] D --> E["MessageBus"] E --> F["Agent core"] F --> G["OutboundMessage"] G --> H["Channel.send / send_delta"] H --> I["平台 API"]
它不应复制 AgentLoop、Session 或 Tool 调用逻辑。
2. BaseChannel 必需方法
子类必须实现:
async start()async stop()async send(msg)
start() 必须保持运行,直到 stop() 或 task 取消。如果建立连接后立即 return,ChannelManager 会认为该 Channel 已结束。
类属性至少定义:
name = "webhook"
display_name = "Webhook"3. 入站必须经过 _handle_message
平台 callback 中应调用:
await self._handle_message(
sender_id=sender,
chat_id=chat,
content=text,
media=media,
metadata=metadata,
session_key=optional_override,
is_dm=is_direct_message,
)BaseChannel._handle_message() 会:
- 检查
allowFrom或 pairing store; - 未授权私聊可生成 pairing code;
- 根据 streaming 能力设置
_wants_stream; - 构造
InboundMessage; - 发布到 MessageBus。
直接 bus.publish_inbound() 会绕过这些共用行为,除非你有经过审查的特殊原因。
4. 身份建模
必须明确:
sender_id:谁发的;chat_id:回复到哪里;session_key:哪些消息共享会话;message_id:引用/去重/线程;- group/thread/reply id:平台自己的层级。
常见错误是把 group chat id 当 user allowlist id,或让不同 thread 意外共享一个 Session。
用字符串保留平台原始稳定 id,不要把可能超出 JavaScript 安全整数的 id 转为 number。
5. allowlist 与 pairing
BaseChannel.is_allowed() 当前顺序:
allowFrom中有"*";- sender exact match;
- pairing store 已批准;
- 拒绝。
allowFrom 条目是 opaque token,必须精确匹配。空列表会拒绝所有未配对用户;字段省略时,ChannelManager 记录 pairing-only 提示。
新增 Channel 应复用这层逻辑,并用平台真正的用户稳定 ID。
6. 出站与重试
send() 接收:
msg.contentmsg.mediamsg.chat_idmsg.metadata
投递失败应抛异常。ChannelManager._send_with_retry() 统一使用 1、2、4 秒退避,而不是要求每个 Channel 再套一层相同重试。
取消必须向上传播,保证 Gateway 可以正常关闭。
如果平台 API 自身已经有重试,要避免两个层级叠加导致重复消息。
7. Streaming
Channel 只有同时满足以下条件才宣告 streaming:
- config 的
streaming为真; - 子类覆盖
send_delta()。
处理 metadata:
_stream_delta_stream_end_stream_id_resuming
有状态 buffer 必须按 _stream_id,不能只按 chat_id。同一 chat 可能并发多个 segment、恢复或多标签页镜像。
平台若不适合频繁编辑,可不开 streaming,直接使用 send()。
8. Progress、Tool hint、Reasoning 和文件编辑
可选表面:
send_progresssend_tool_hintssend_reasoning_delta()send_reasoning_end()send_file_edit_events()
这些是低优先级观察信息。平台不支持时可安全忽略,不应把 reasoning 当普通最终回复发送。
ChannelManager 还会合并连续 text delta,减少平台 API 调用。
9. Media
Channel media 代码需要单独审查:
- 上传大小和类型;
- 临时文件清理;
- 远程 URL 下载的 SSRF;
- 文件名路径穿越;
- MIME 与扩展名不一致;
- 平台下载 token;
- 出站本地文件是否在允许 media root;
- Markdown 到平台格式的转义。
参考现有专项测试,而不要只复制 happy path。
10. 外部 plugin 优先
官方推荐独立 package:
[project.entry-points."nanobot.channels"]
webhook = "nanobot_channel_webhook:WebhookChannel"ChannelsConfig 允许 extra section;ChannelManager 从内置模块名和配置 extra keys 中找 enabled Channel。
发现规则:
- 内置 Channel 用
pkgutil扫描; - plugin 用
nanobot.channelsentry points; - 只 import enabled 内置模块,减少第三方 SDK 启动成本;
- 同名时内置 Channel 优先,外部 plugin 不能 shadow。
default_config() 可让 nanobot onboard 生成初始配置。
11. 最小测试集
- registry 能发现 entry point;
- disabled config 不实例化;
start()启动与stop()清理;- allowed sender 产生正确
InboundMessage; - denied/pairing 行为;
send()目标和格式;- delivery failure 由 manager 重试;
- streaming delta/end 与
_stream_id; - media 安全;
- cancellation 传播。
测试参考:
tests/channels/test_base_channel.pytests/channels/test_channel_plugins.pytests/channels/test_channel_manager_delta_coalescing.pytests/channels/test_channel_manager_reasoning.py- 一个协议相近的现有 Channel test。
官方完整示例:
12. 最小修改范围
外部 plugin:
- 独立 package 与 config model;
BaseChannel子类;- entry point;
- tests/readme。
内置 Channel 还可能需要:
- optional dependency;
- 用户配置文档;
- onboarding;
- WebUI settings;
- 平台专项安全测试。
不要仅为了实验就修改 ChannelsConfig 的固定字段;当前 extra config 已为 plugin 留出边界。
自检
- 为什么平台 callback 应调用
_handle_message()? sender_id与chat_id混用会造成什么问题?- Channel 的
start()为什么不能在连接成功后 return? - 外部 plugin 与内置 Channel 同名时谁优先?