OneBot Channel 设计
状态:设计草案 日期:2026-05-28 目标:为 nahida-bot 增加 OneBot 协议支持,统一对接 NapCat、Lagrange、LLOneBot 等 QQ 端实现,以及可能的非 QQ OneBot 实现。 相关文档:
1. 结论
推荐实现 OneBot v11 + v12 双版本支持,通过内部统一中间表示消除版本差异。通信方式按使用广度优先支持 正向 WebSocket(bot 作为 client 连接 OneBot 实现端),其次 WebHook(OneBot 实现端 HTTP POST 推送事件 + 反向 HTTP API 调用),暂不优先支持反向 WebSocket(bot 作为 server)。
关键取舍:
- v11 和 v12 共用一个插件、一份配置:内部用
OneBotProtocol抽象层归一化两个版本的事件和 API,外层 plugin、config、message converter 不感知版本差异。 - 正向 WS 优先于 WebHook:绝大多数 OneBot 实现端(NapCat、Lagrange、LLOneBot)默认提供正向 WS server,bot 主动连接是最简单的部署拓扑,不需要 bot 侧暴露公网端口。
- 不实现完整 OneBot API 覆盖:首版只实现收发消息所需的最小 action 集(
send_msg、get_msg、get_login_info、get_group_info、get_group_list等约 10 个)。扩展 action 按需添加。 - Segment 消息模型直接对接现有
InboundMessage/OutboundMessage:OneBot 的 array-based message segment([{type, data}, ...])归一化为 nahida-bot 的文本 + 附件模型;CQ 码文本模式仅做降级兼容,不作为主解析路径。 - 不引入 nb2 或 nonebot 作为依赖:OneBot 插件是本项目自己的
Plugin + ChannelService实现,参考但不依赖 nonebot2 生态。
Cross-cutting 与 Milky 的差异:
| 维度 | Milky | OneBot |
|---|---|---|
| 连接方向 | bot → WS server 收事件;bot → HTTP 发 API | 正向 WS:bot → WS server 收发双工(v11)/ 仅事件(v12);反向 WS / WebHook 也支持 |
| API 协议 | HTTP POST /api/:api | v11:WS 内 JSON-RPC-style envelope;v12:HTTP POST / + action body |
| 事件协议 | WS JSON frame | v11:WS JSON frame;v12:WS/HTTP POST |
| 消息格式 | Milky segment dict | OneBot message segment array |
| 文件上传 | 独立 file API | 内嵌于 send_msg 的 segment |
| 单 WS 双工 RPC | 否——/event 只推送事件,API 走 HTTP | v11 是;v12 分离 |
2. OneBot v11 vs v12 差异分析
2.1 协议分层
v11 是单协议版:所有通信(事件接收 + API 调用)可以在同一条 WebSocket 上完成(正向 WS),也可以拆成 WebHook(事件推送) + HTTP(API 调用)。
v12 拆分为两个独立规范:
- OneBot Connect:通信方式规范(HTTP、WebSocket、WebHook 三种模式)
- OneBot Standard:事件和 action 的语义定义
2.2 连接模式对比
| 模式 | v11 | v12 | nahida-bot 首版 |
|---|---|---|---|
| 正向 WS(bot 连 impl) | ✅ 双工:同一连接收发事件和 API | ✅ 仅推送事件;API 走 HTTP | 优先实现 |
| 反向 WS(impl 连 bot) | ✅ 双工 | ✅ 仅推送事件 | 暂缓 |
| WebHook(事件推送) | ✅ HTTP POST 事件 + 反向 HTTP API | ✅ HTTP POST 事件 + 反向 HTTP API | 其次实现 |
| HTTP API | ✅ 配合 WebHook 用 | ✅ 配合所有模式用 | 随 WebHook 一起实现 |
正向 WS 是大多数用户的首选:NapCat 默认在 ws://127.0.0.1:3001 提供正向 WS;Lagrange 同样提供;LLOneBot 也支持。这个模式下 bot 不需要暴露端口。
2.3 消息格式差异
v11:
[
{"type": "text", "data": {"text": "你好"}},
{"type": "image", "data": {"file": "http://...", "url": "http://..."}},
{"type": "at", "data": {"qq": "123456"}}
]- 同时支持 message segment array 和 CQ 码 string 两种表示
post_type区分message、notice、request、meta_eventmessage_type区分private、group
v12:
[
{"type": "text", "data": {"text": "你好"}},
{"type": "image", "data": {"file_id": "...", "url": "..."}},
{"type": "mention", "data": {"user_id": "123456"}}
]- 只有 message segment array,不保留 CQ 码
- event
type使用message.private、message.group等 namespaced 格式 alt_message字段提供纯文本降级
2.4 Action/API 差异
| 方面 | v11 | v12 |
|---|---|---|
| 调用格式 | {action, params, echo} over WS | {action, params, echo, self} over HTTP |
| 响应格式 | {status, retcode, data, echo} | {status, retcode, data, echo} |
| 错误码 | retcode=0 为成功 | retcode=0 为成功 |
| action 命名 | send_msg、get_login_info | send_message、get_self_info |
| action 参数 | 各 action 独立 | 更标准化,有 type/param schema |
v12 的 action 普遍更规范,例如 send_message 替代 send_msg,get_self_info 替代 get_login_info。但语义差异不大,可以在协议适配层统一。
2.5 事件差异
| 方面 | v11 | v12 |
|---|---|---|
| 事件标识 | post_type + 各子字段 | type(如 message.private) |
| 消息事件 | message_type: private/group | message.private / message.group |
| 通知事件 | notice_type + sub 字段 | 各自独立 type(如 friend.increase) |
| 自身信息 | self_id、self_tiny_id | self object({platform, user_id}) |
| 消息 ID | message_id (int) | message_id (string) |
| 时间戳 | time (int, unix) | time (float, unix) |
3. 架构设计
3.1 分层架构
┌─────────────────────────────────────────────────────┐
│ OneBotPlugin (ChannelService) │
│ plugin.py — 生命周期、注册 channel │
│ config.py — 版本无关的配置模型 │
├─────────────────────────────────────────────────────┤
│ OneBotAdapter │
│ adapter.py — 协议版本检测,统一事件/action接口 │
├──────────────────────┬──────────────────────────────┤
│ OneBotV11Protocol │ OneBotV12Protocol │
│ v11/event.py │ v12/event.py │
│ v11/action.py │ v12/action.py │
│ v11/connect.py │ v12/connect.py │
├──────────────────────┴──────────────────────────────┤
│ Transport Layer │
│ transport/ws.py — 正向 WS client │
│ transport/webhook.py — WebHook HTTP server │
│ transport/http_api.py — HTTP API client (v12 / │
│ WebHook 模式) │
└─────────────────────────────────────────────────────┘
│ Message Converter │
│ message_converter.py — segment ↔ Inbound/Outbound │
└─────────────────────────────────────────────────────┘3.2 协议适配层
核心接口:
class OneBotProtocol(Protocol):
"""Normalize v11/v12 differences behind a single interface."""
@property
def version(self) -> str: ...
"""Return "v11" or "v12"."""
def detect_event_type(self, raw: dict[str, Any]) -> str | None: ...
"""Return normalized event type string or None if not an event frame."""
def normalize_event(self, raw: dict[str, Any]) -> dict[str, Any]: ...
"""Convert a v11 or v12 event dict into a stable intermediate format."""
def encode_action(self, action: str, params: dict[str, Any]) -> dict[str, Any]: ...
"""Build the protocol-specific action payload."""
def decode_response(self, raw: dict[str, Any]) -> OneBotResponse: ...
"""Parse an action response into a stable result object."""中间事件格式(版本无关):
@dataclass
class NormalizedEvent:
type: str # "message.private" | "message.group" | ...
sub_type: str # "friend" | "group" | "normal" | ...
message_id: str
user_id: str
group_id: str | None
self: OneBotSelf # {platform, user_id}
message: list[dict] # normalized segments
alt_message: str
time: float
raw: dict[str, Any]版本检测策略:
- 正向 WS 连接成功后,v11 impl 会立即推送
meta_event.lifecycle或meta_event.heartbeat;v12 在 connect 握手后推送事件 - 通过事件 field 检测:有
post_type且值为message|notice|request|meta_event→ v11;有type且值为message.*|notice.*|meta.*→ v12 - 也支持配置显式指定
protocol_version: "v11" | "v12" | "auto"(默认auto)
3.3 连接模式适配
正向 WebSocket(首版优先)
nahida-bot OneBot 实现端 (NapCat/Lagrange/LLOneBot)
│ │
│── WS connect ─────────────────→│
│ │
│←── lifecycle event (v11 only)──│
│ │
│ [v11: 同一条 WS 上收发 action] │
│── {action, params, echo} ─────→│ (v11 only)
│←── {status, data, echo} ──────│ (v11 only)
│ │
│←── event JSON frame ──────────│
│ │
│ [v12: API 通过 HTTP 发送] │
│── HTTP POST / {action} ───────→│ (v12 only)
│←── HTTP 200 {status, data} ───│ (v12 only)正向 WS 实现要点:
- v11:
OneBotV11WSConnection管理一条 WS 连接,同时作为事件源和 API transport。事件帧和 action 响应帧通过echo字段区分路由。连接断开时自动重连。 - v12:
OneBotV12WSConnection管理 WS 仅作为事件源;API 调用走独立OneBotHTTPClient(复用同一base_url的 HTTP 端点)。 - 重连策略:初始延迟 1s,指数退避至最大 30s,无限重试(用户停止插件时退出)。
WebHook(其次实现)
nahida-bot OneBot 实现端
│ │
│ [事件] │
│←── HTTP POST /onebot/event ────│
│── HTTP 204 ──────────────────→│
│ │
│ [API 调用] │
│── HTTP POST {impl_url}/ ──────→│
│←── HTTP 200 ──────────────────│WebHook 实现要点:
- bot 侧在 Gateway(FastAPI)注册
/onebot/event端点接收事件 X-Self-IDheader 区分多账号X-Signature/ token 校验防止伪造事件(复用 OneBot 实现的 secret/token 机制)- API 调用方向:bot → impl 的 HTTP API(主动出站),通过配置的
impl_base_url或事件中携带的 impl 地址
3.4 消息转换
v11 CQ 码兼容策略
v11 同时支持 message array 和 CQ 码 string。转换策略:
- 优先使用
message字段(array),这是 OneBot 规范的标准格式 - 如果
message为空或缺失,fallback 到raw_message(CQ 码 string) - CQ 码解析作为降级兼容路径,不完全覆盖所有 CQ 码类型,只覆盖常见类型(
[CQ:text]、[CQ:image]、[CQ:at]、[CQ:face]、[CQ:reply]、[CQ:record]、[CQ:video]、[CQ:file]) - 无法解析的 CQ 码保留原文不丢弃
Segment → InboundMessage 映射:
SEGMENT_TO_INBOUND = {
"text": → InboundMessage.text 追加
"image": → InboundAttachment(kind="image", platform_id=file_id, url=url)
"record": → InboundAttachment(kind="audio", platform_id=file_id, url=url)
"video": → InboundAttachment(kind="video", platform_id=file_id, url=url)
"file": → InboundAttachment(kind="file", platform_id=file_id, url=url)
"at": → InboundMessage.text 追加 "@display_name";填充 mentioned_user_ids
"mention": → (v12) 同 at
"reply": → InboundMessage.reply_to = message_id
"face": → InboundMessage.text 追加 "[表情: {id}]"
"forward": → InboundMessage.text 追加 "[合并转发: {id}]"
}OutboundMessage → Segment 映射:
OutboundMessage.text → {"type": "text", "data": {"text": ...}}
OutboundMessage.reply_to → {"type": "reply", "data": {"id": ...}}
OutboundMessage.attachment(kind="image") → {"type": "image", "data": {"file": ...}}
OutboundMessage.attachment(kind="audio") → {"type": "record", "data": {"file": ...}}
OutboundMessage.attachment(kind="video") → {"type": "video", "data": {"file": ...}}3.5 群聊触发策略
复用 Milky 的 GroupInteractionPolicy 模式:
GroupTriggerMode = Literal["mention", "command", "always"]| 模式 | 行为 |
|---|---|
mention | 仅当 @bot 或 @全体成员时触发 |
command | 仅当消息以 command_prefix 开头或 @bot 时触发 |
always | 所有群消息都触发 |
@bot 检测逻辑:检查 message segment 中的 at/mention 是否包含 self_id,或在 v11 CQ 码检测 [CQ:at,qq={self_id}]。
3.6 ChatAddress 模型
OneBot 消息天然有明确的 channel/chat_type/chat_id 三元组:
# 私聊
ChatAddress(channel="onebot", target_type="private", target_id="u_{user_id}")
# 群聊
ChatAddress(channel="onebot", target_type="group", target_id="{group_id}")
# 支持多账号场景:用 self_id 区分不同 bot 账号的同一群/用户
# 通过 session metadata 存储 self_id4. 配置模型
class OneBotPluginConfig(BaseModel):
"""OneBot channel plugin configuration, protocol-version agnostic."""
# --- 协议版本 ---
protocol_version: Literal["v11", "v12", "auto"] = "auto"
# --- 正向 WS 模式 ---
ws_url: str = ""
"""正向 WebSocket 地址,例如 ws://127.0.0.1:3001。空字符串表示不启用正向 WS。"""
ws_access_token: str = ""
"""正向 WS 鉴权 token,在 v11 中可作为 connect 参数或 header,v12 中作为 Authorization header。"""
# --- WebHook 模式 ---
webhook_enabled: bool = False
webhook_host: str = "127.0.0.1"
webhook_port: int = 6186
webhook_path: str = "/onebot/event"
webhook_secret: str = ""
"""WebHook 签名密钥,用于验证事件来源。"""
# --- HTTP API(v12 或 WebHook 模式下的出站 API 调用)---
impl_base_url: str = ""
"""OneBot 实现端的 HTTP API 地址,例如 http://127.0.0.1:3001。正向 WS v12 和 WebHook 模式都需要。"""
impl_access_token: str = ""
"""HTTP API 调用的 access token。"""
# --- 通用配置 ---
command_prefix: str = "/"
group_trigger_mode: GroupTriggerMode = "mention"
group_context_capture: bool = False
reply_to_inbound: bool | None = None
allowed_friends: list[str] = Field(default_factory=list)
allowed_groups: list[str] = Field(default_factory=list)
# --- 重连策略(正向 WS)---
reconnect_initial_delay: float = 1.0
reconnect_max_delay: float = 30.0
# --- 消息 ---
max_text_length: int = 4000
# --- 媒体 ---
media_download_dir: str = "./data/temp/onebot"
enable_media_download_tool: bool = True
cache_media_on_receive: bool = True
# --- 分段发送 ---
split_long_text: bool = True
"""超长文本是否自动分段发送(群聊场景常见限制)。"""
@model_validator(mode="after")
def _validate_connectivity(self) -> "OneBotPluginConfig":
if not self.ws_url and not self.webhook_enabled:
raise ValueError(
"At least one connectivity mode must be configured: "
"ws_url or webhook_enabled"
)
if self.protocol_version in ("v12", "auto") and not self.impl_base_url:
if self.ws_url:
# v12 模式需要 HTTP API 地址;从 ws_url 推导
pass # 由 plugin on_load 推导
elif self.webhook_enabled and not self.impl_base_url:
raise ValueError(
"impl_base_url is required for v12 HTTP API calls"
)
return self
@property
def derived_http_base_url(self) -> str:
"""Derive HTTP API base from ws_url if not explicitly configured."""
if self.impl_base_url:
return self.impl_base_url.rstrip("/")
if self.ws_url:
return self.ws_url.replace("ws://", "http://").replace("wss://", "https://").rstrip("/")
return ""5. 目录结构
nahida_bot/channels/onebot/
__init__.py # 导出 OneBotPlugin
plugin.yaml # manifest
plugin.py # OneBotPlugin (Plugin + ChannelService)
config.py # OneBotPluginConfig
protocol.py # OneBotProtocol abstract + NormalizedEvent + OneBotResponse
adapter.py # OneBotAdapter — version detection + dispatch
v11/
__init__.py
event.py # v11 → NormalizedEvent
action.py # Normalized action → v11 payload
connect.py # v11 WS connection (双工: 事件 + API)
v12/
__init__.py
event.py # v12 → NormalizedEvent
action.py # Normalized action → v12 payload
connect.py # v12 WS connection (仅事件)
transport/
__init__.py
ws.py # 正向 WS client(版本感知)
webhook.py # WebHook HTTP endpoint handler
http_api.py # 出站 HTTP API client (v12 / WebHook mode)
message_converter.py # segment ↔ InboundMessage / OutboundMessage
segment_models.py # OneBot segment dataclasses
cq_code.py # v11 CQ 码降级解析器6. Action 覆盖计划
首版实现的最小 action 集:
| Action | v11 名称 | v12 名称 | 用途 |
|---|---|---|---|
| 发送消息 | send_msg | send_message | 发送私聊/群聊消息 |
| 获取消息 | get_msg | get_message | 按 message_id 获取消息详情 |
| 获取自身信息 | get_login_info | get_self_info | 获取 bot 自己的 QQ 号和昵称 |
| 获取群信息 | get_group_info | get_group_info | 获取群名称、成员数等 |
| 获取群列表 | get_group_list | get_group_list | 获取 bot 加入的所有群 |
| 获取好友列表 | get_friend_list | get_friend_list | 获取 bot 的所有好友 |
| 获取群成员信息 | get_group_member_info | get_group_member_info | 获取群成员的群名片等 |
| 获取群成员列表 | get_group_member_list | get_group_member_list | 获取群成员列表 |
| 撤回消息 | delete_msg | delete_message | 撤回已发送的消息 |
| 获取文件 URL | get_file_url (ext) | get_file | 获取文件下载 URL |
优先级排序:
- P0:
send_msg/message、get_login_info/self_info、get_msg/message——收发消息闭环 - P1:
get_group_info、get_group_list——群管理基础 - P2:
get_friend_list、delete_msg、群成员查询 - P3:扩展 action(
set_group_card、set_group_ban等)
不在首版范围的 action(如 send_like、set_restart、clean_cache 等)返回 protocol-level "unsupported action" 错误。
7. 事件覆盖计划
7.1 消息事件
| v11 post_type | v12 type | 处理方式 |
|---|---|---|
message.private | message.private | 转换为 MessageReceived |
message.group | message.group | 转换为 MessageReceived + GroupPolicy 判断 |
7.2 通知事件(可观测性,首版不触发 agent)
| v11 notice_type | v12 type | 处理方式 |
|---|---|---|
friend_add | friend.increase | 日志记录 |
group_increase | group.increase | 日志记录,可选欢迎消息 |
group_decrease | group.decrease | 日志记录 |
group_ban | — (v12 暂无) | 日志记录 |
friend_recall | — | 日志记录 |
group_recall | — | 日志记录 |
7.3 请求事件
| v11 request_type | v12 type | 处理方式 |
|---|---|---|
friend | request.friend | 日志记录;可配置自动同意 |
group.invite | request.group.invite | 日志记录;可配置自动同意 |
7.4 元事件
| v11 | v12 | 处理方式 |
|---|---|---|
meta_event.lifecycle | meta.connect | 更新内部状态(self_id、连接时间) |
meta_event.heartbeat | meta.heartbeat | 日志 debug 级别,超时检测 |
8. 实施阶段
Phase 0:基础设施
Phase 1:v11 正向 WS 最小闭环
Phase 2:v12 正向 WS
Phase 3:WebHook 模式
Phase 4:群聊增强与可观测性
Phase 5:扩展 action 与优化
9. 风险与对策
| 风险 | 对策 |
|---|---|
| v11/v12 行为差异导致适配层膨胀 | 统一 NormalizedEvent/OneBotResponse 有限字段;差异只在 protocol 子类内部消化 |
| OneBot 实现端行为不一致(NapCat vs Lagrange vs LLOneBot) | 优先在 NapCat + Lagrange 两个最常用实现上测试;adapter 层对字段缺失做 defensive 处理 |
| CQ 码格式各异,降级解析覆盖率不足 | CQ 码只作为 fallback;主路径始终是 segment array;解析失败时保留原文 |
| 正向 WS 连接断开后消息丢失 | 正向 WS 模式下事件推送是实时的,断线期间消息不可恢复,这是 OneBot 协议本身的约束;在配置文档中说明,并建议关键场景使用 WebHook |
| v12 生态尚不成熟,多数用户仍用 v11 | 首版即支持双版本,降低用户迁移压力;v11 做完整闭环,v12 保持兼容跟进 |
| 多 bot 账号场景(多个 OneBot 实现端)共享一个 nahida-bot | 首版只支持单个 OneBot 连接;多账号通过 Channel 多实例化(加载多个 OneBot plugin 副本)解决,Phase 4 再简化配置 |
| OneBot 实现端 API 调用频率限制 | send_msg 做有限重试(仅网络错误和 429),不在插件层做全局 rate limit |
| 与 Milky 功能重叠(都是 QQ 接入) | 两者是平行的 channel plugin,用户根据部署环境二选一;不互相依赖,各自独立维护 |
10. 与 Milky 的共存策略
nahida-bot 可能同时拥有 OneBot 和 Milky 两个 QQ Channel。共存时需要注意:
- Session 隔离:
ChatAddress使用不同channel值(onebotvsmilky),session 完全隔离。 - 同一 QQ 群/用户:即使同一个 QQ 群同时出现在 Milky 和 OneBot 中,session 也不互通。这是预期行为——channel 是消息来源的权威标识。
- 跨 channel 消息转发:如果用户想让 OneBot 收到的消息触发 Milky 回复(或反之),通过
message工具显式跨 channel 发送,不做隐式路由。 - 配置文档:明确告知用户不要同时为同一个 bot QQ 号启用两个 channel,避免群友收到重复回复。
11. 参考资料
- OneBot v11 标准:https://github.com/botuniverse/onebot-11
- OneBot v12 标准:https://12.onebot.dev/
- NapCat 实现:https://github.com/NapNeko/NapCatQQ
- Lagrange.OneBot 实现:https://github.com/LagrangeDev/Lagrange.Core
- LLOneBot 实现:https://github.com/LLOneBot/LLOneBot
- nonebot2 adapter-onebot:https://github.com/nonebot/adapter-onebot
- AstrBot OneBot 平台接入:https://github.com/Soulter/AstrBot
- Milky 协议参考(非 OneBot):https://github.com/ProtocolScience/milky-python-sdk