170 lines
7.6 KiB
Markdown
170 lines
7.6 KiB
Markdown
# 04 — Bridge(转接器)架构设计指南
|
||
|
||
> 目标读者:要把微信 ClawBot 接到自己已有 AI 项目的开发者。
|
||
> 本文档不绑定具体语言,给出模块划分、状态模型、对接模式和可靠性设计。
|
||
|
||
## 1. Bridge 的职责边界
|
||
|
||
Bridge 是一个**薄适配层**,职责只有:
|
||
|
||
```
|
||
iLink 长轮询 ──▶ 消息规范化 ──▶ 调用你的 AI 后端 ──▶ 回复格式化 ──▶ sendmessage
|
||
```
|
||
|
||
Bridge **不负责**:
|
||
|
||
- 对话历史/记忆管理(那是 AI 后端的事,除非后端无状态)
|
||
- 业务逻辑、工具调用编排
|
||
- 用户体系、计费
|
||
|
||
保持薄的好处:协议变了只改 Bridge;AI 后端可以独立演进、独立测试。
|
||
|
||
## 2. 推荐模块划分
|
||
|
||
```
|
||
bridge/
|
||
├── ilink/
|
||
│ ├── client.* # HTTP 封装:请求头、鉴权、X-WECHAT-UIN、超时
|
||
│ ├── auth.* # 扫码登录流程(get_bot_qrcode / get_qrcode_status)
|
||
│ ├── poller.* # 长轮询主循环、游标管理、退避重试
|
||
│ ├── sender.* # sendmessage / sendtyping / 分片发送
|
||
│ └── media.* # getuploadurl、CDN 上传下载、AES-128-ECB
|
||
├── store/
|
||
│ ├── account.* # bot_token / ilink_bot_id / baseurl 持久化
|
||
│ ├── cursor.* # get_updates_buf 持久化(每次更新立即落盘)
|
||
│ └── context.* # peer_id → context_token 映射持久化
|
||
├── normalize.* # WeixinMessage → 内部统一消息模型
|
||
├── backend.* # 你的 AI 项目的调用封装(HTTP / SDK / 进程)
|
||
└── main.* # 装配 + 启动 + 优雅退出
|
||
```
|
||
|
||
参考:官方包 `@tencent-weixin/openclaw-weixin` 的目录结构(`auth/ api/ cdn/ messaging/ monitor/ config/ storage/`)几乎就是这个划分;Java SDK 的门面模式(`OpenClawWeixinSdk` 统一封装 `QrLoginFlowService / WeixinApiClient / WeixinLongPollMonitor / MediaUploadService`)也值得对照。
|
||
|
||
## 3. 必须持久化的三类状态
|
||
|
||
| 状态 | 内容 | 更新时机 | 丢失后果 |
|
||
|---|---|---|---|
|
||
| 账号凭据 | `bot_token`、`ilink_bot_id`、`ilink_user_id`、`baseurl` | 扫码成功时 | 需重新扫码 |
|
||
| 同步游标 | `get_updates_buf` | **每次 getupdates 响应后立即** | 重复收到历史消息 |
|
||
| 上下文令牌 | `peer_id → context_token` | 每条入站消息 | 无法回复/主动消息送达不到 |
|
||
|
||
实现建议:
|
||
|
||
- 单机用 JSON 文件或 SQLite 就够(官方包就是文件存储,Java SDK 用 `FileAccountStore / FileContextTokenStore / FileSyncCursorStore`)
|
||
- 游标落盘要**同步写**(或至少 flush),崩溃丢游标 = 重启后消息轰炸
|
||
- 文件加个简易锁,防止多开实例写坏状态
|
||
|
||
## 4. 与 AI 后端的对接模式
|
||
|
||
按你的 AI 项目形态选一种:
|
||
|
||
### 模式 A:HTTP API 后端(最常见)
|
||
|
||
```
|
||
poller.onMessage(msg):
|
||
reply = http_post("http://127.0.0.1:8000/chat", {
|
||
"session_id": msg.peer_id, # 用微信用户 ID 当会话 ID
|
||
"text": msg.text,
|
||
"attachments": msg.media_paths
|
||
})
|
||
sender.send_text(msg.peer_id, reply.text, msg.context_token)
|
||
```
|
||
|
||
- 会话 ID 直接映射微信 `from_user_id`,天然一对一私聊
|
||
- 后端无状态的话,Bridge 也不存历史;有状态则后端自己管
|
||
|
||
### 模式 B:本地 SDK / 库调用
|
||
|
||
Bridge 与 AI 项目同进程(或同语言直接 import),把消息对象传给 Agent 函数,拿回字符串。参考 [cc-weixin](https://github.com/hao-ji-xing/cc-weixin) 把 Claude Agent SDK 的 `query()` 包一层。
|
||
|
||
### 模式 C:异步任务队列
|
||
|
||
AI 处理慢(深度研究、图像生成)时:
|
||
|
||
```
|
||
1. 收到消息 → 立即回复「收到,处理中」(可选:sendtyping)
|
||
2. 任务入队,worker 处理
|
||
3. 完成后用保存的最新 context_token 主动 sendmessage
|
||
```
|
||
|
||
⚠️ 注意 24h 窗口限制(见 06 文档):任务完成后主动推送必须在窗口内才送得到。慢任务一定要设计超时兜底文案。
|
||
|
||
### 模式 D:多通道网关(进阶)
|
||
|
||
如果以后还要接 Telegram/飞书/钉钉,把「规范化消息模型 + 统一网关」抽出来,iLink 只是其中一个 adapter。直接参考 [OpenAkita 的架构](https://github.com/openakita/openakita/blob/main/docs/im-channels.md):`Adapter → UnifiedMessage → Gateway → Agent`,发送路径反向。第一版不用做,但内部消息模型(见 03 文档第 7 节)建议一开始就用。
|
||
|
||
## 5. 可靠性设计
|
||
|
||
### 5.1 长轮询主循环
|
||
|
||
伪代码:
|
||
|
||
```
|
||
loop:
|
||
try:
|
||
resp = getupdates(cursor, timeout=server_timeout + 10s)
|
||
if resp.ret == 0:
|
||
if resp.get_updates_buf: cursor.save(resp.get_updates_buf)
|
||
for msg in resp.msgs: dispatch(msg)
|
||
backoff.reset()
|
||
elif resp.errcode == -14:
|
||
pause_account(1h); notify("需重新扫码")
|
||
else:
|
||
backoff.retry() # 2s 快速重试,连续 3 次后 30s
|
||
except HttpTimeout:
|
||
continue # 长轮询正常超时
|
||
except NetworkError:
|
||
backoff.retry()
|
||
```
|
||
|
||
### 5.2 消息去重
|
||
|
||
三层兜底:
|
||
|
||
1. 游标正确持久化(治本)
|
||
2. 应用层按消息 ID 去重(`client_id` / 服务端消息 ID,存最近 N 条的 set)
|
||
3. 发送侧带唯一 `client_id`(防自己重发)
|
||
|
||
### 5.3 回复超时
|
||
|
||
- AI 后端设硬超时(如 60s),超时回「处理超时,请重试」
|
||
- 有资料称 24h 窗口外消息送达不到——对「用户发问 → bot 回复」场景这不是问题(立即回复),只影响主动推送
|
||
|
||
### 5.4 优雅退出
|
||
|
||
- 捕获 SIGTERM/SIGINT:停 poller → 等正在处理的消息发完 → flush 状态 → 退出
|
||
- 不要在 dispatch 中途杀进程,游标已推进但回复没发 = 用户消息被吞
|
||
|
||
### 5.5 可观测性
|
||
|
||
- 结构化日志:收消息(peer、类型、长度)、发消息、游标推进、错误码、退避事件
|
||
- 关键指标:轮询错误率、-14 出现次数、AI 后端耗时分布
|
||
- token、context_token 不要打进日志(打 hash 前缀即可)
|
||
|
||
## 6. 安全注意
|
||
|
||
- `bot_token` 等于这个微信 bot 的全部权限:**不入 git、不进日志、文件权限 600**
|
||
- 谁扫码谁就是这个 bot 的「主人」,但任何拿到 token 的人都能收发消息——服务器上注意文件权限
|
||
- 入站消息内容视为不可信输入:如果 AI 后端有工具调用能力(执行命令、读写文件),提示词注入风险要自己评估
|
||
- 建议加白名单:只响应指定 `from_user_id`(比如就你自己),防止别人给 bot 发消息白嫖你的模型 API 额度
|
||
|
||
## 7. 容量与限制设计
|
||
|
||
- 微信侧不支持 Markdown:发送前转纯文本
|
||
- 长回复分片:按段落或固定字数(如 1500 字)切多条,注意保持顺序
|
||
- 媒体大小:入站大图/大文件设上限,超限回复提示而不是硬下载
|
||
- AI 后端并发:一个微信号只有一个 bot,但用户可能连发多条——dispatch 建议串行 per-peer(或按 peer 加锁),避免回复乱序
|
||
|
||
## 8. 测试策略
|
||
|
||
- 协议层:把 iLink client 抽象成接口,测试时用 mock server 回放抓包 JSON
|
||
- 规范化层:构造各类 WeixinMessage JSON(文本/图片/语音/图文混排/多 item)做单元测试
|
||
- 端到端:真机扫码,用另一个微信号发消息验证
|
||
- 回归重点:游标持久化(重启不重复收)、context_token 传递、-14 恢复流程
|
||
|
||
## 参考资料
|
||
|
||
- [openclaw-weixin-sdk 架构图与失败恢复策略](https://github.com/openclaw-java/openclaw-weixin-sdk)
|
||
- [OpenAkita 多通道网关架构](https://github.com/openakita/openakita/blob/main/docs/im-channels.md)
|
||
- [cc-weixin — 接入 Claude Code Agent 的完整范例](https://github.com/hao-ji-xing/cc-weixin)
|