logo
0
0
WeChat Login
feat: 增加入站消息兼容性元数据支持

weixin-go

cnb.cool/zishuo/wxclaw 是基于当前 openclaw-weixin TypeScript 实现移植出来的独立 Go 微信消息网关。

HTTP 管理面使用 github.com/go-chi/chi/v5,并由同一服务直接提供内嵌 HTML 测试控制台。

文档导航

能力

  • Weixin CGI API client
  • QR 登录与账号落盘
  • getUpdates 长轮询监控
  • context_token 进程内缓存与出站复用
  • getConfig typing ticket 缓存
  • -14 session pause 熔断
  • CDN 上传/下载与 AES-128-ECB 加解密
  • HTTP 管理 API
  • 入站事件 webhook 投递
  • 文本/媒体发送
  • 日志上传 CLI(logs-upload

快速启动

  1. 准备配置文件:
cp examples/config.yaml ./config.yaml

如果直接启动时找不到 config.yaml,网关也会自动在目标路径生成同款推荐模板。

  1. 修改 webhook、存储目录、账号配置。

  2. 启动:

cd weixin-go go run ./cmd/weixin-gateway -config ./config.yaml

或构建二进制:

cd weixin-go go build -o bin/weixin-gateway ./cmd/weixin-gateway ./bin/weixin-gateway -config ./config.yaml
  1. 打开浏览器测试台:
http://127.0.0.1:8080/

或:

http://127.0.0.1:8080/console

网关 HTTP 层已切到 github.com/go-chi/chi/v5,根路径会直接返回内嵌 HTML 控制台,方便做登录、绑定、发消息与 webhook 联调。

更详细的操作说明见 USAGE_GUIDE.zh-CN.md

若要按接口字段、请求示例和返回结构逐项接入,直接看 API_REFERENCE.zh-CN.md

若要理解模块结构、生命周期、存储与设计取舍,直接看 DESIGN.md

CLI 子命令

上传日志

对齐 TS 版本 logs-upload 行为,支持 YYYYMMDDYYYYMMDDHH、完整文件名或绝对路径。上传 URL 优先级:

  1. --url
  2. WEIXIN_GATEWAY_LOG_UPLOAD_URL
  3. --config 指向配置中的 weixin.log_upload_url(兼容读取 channels.openclaw-weixin.logUploadUrl

示例:

cd weixin-go go run ./cmd/weixin-gateway logs-upload \ --url https://example.com/upload \ --file 20260322 \ --log-dir /tmp/openclaw

也可用环境变量:

export WEIXIN_GATEWAY_LOG_UPLOAD_URL=https://example.com/upload export WEIXIN_GATEWAY_LOG_DIR=/tmp/openclaw ./bin/weixin-gateway logs-upload --file openclaw-2026-03-22.log

默认上传字段名为 file。若响应头包含 fileid 会直接打印;若没有,会输出全部响应头用于排障,并打印响应体。

HTTP API

  • GET /
  • GET /console
  • GET /health
  • GET /status
  • GET /api/v1/accounts
  • POST /api/v1/qr/start
  • POST /api/v1/qr/wait
  • POST /api/v1/send/text
  • POST /api/v1/send/media
  • POST /api/v1/send/typing

生命周期管理 API

用于生产环境中的账号/设备绑定状态维护(账号启停、allowFrom 管理、debug 开关)。推荐通过这些接口完成绑定/解绑交互与运行态操作。

  • POST /api/v1/accounts/:account_id/start
  • POST /api/v1/accounts/:account_id/stop
  • GET /api/v1/accounts/:account_id/allow-from
  • POST /api/v1/accounts/:account_id/allow-from
  • DELETE /api/v1/accounts/:account_id/allow-from
  • GET /api/v1/accounts/:account_id/devices
  • POST /api/v1/accounts/:account_id/devices/bind
  • POST /api/v1/accounts/:account_id/devices/unbind
  • GET /api/v1/accounts/:account_id/debug
  • POST /api/v1/accounts/:account_id/debug
  • POST /api/v1/accounts/:account_id/debug/toggle

示例:

curl -X POST http://127.0.0.1:8080/api/v1/accounts/bot-a-im-bot/start
curl -X POST http://127.0.0.1:8080/api/v1/accounts/bot-a-im-bot/allow-from \ -H 'content-type: application/json' \ -d '{"user_id":"wxid_xxx@im.wechat"}'
curl -X DELETE http://127.0.0.1:8080/api/v1/accounts/bot-a-im-bot/allow-from \ -H 'content-type: application/json' \ -d '{"user_id":"wxid_xxx@im.wechat"}'
curl -X POST http://127.0.0.1:8080/api/v1/accounts/bot-a-im-bot/devices/bind \ -H 'content-type: application/json' \ -d '{"user_id":"wxid_xxx@im.wechat"}'
curl -X POST http://127.0.0.1:8080/api/v1/send/typing \ -H 'content-type: application/json' \ -d '{"account_id":"bot-a-im-bot","to":"wxid_xxx@im.wechat","context_token":"optional"}'

allow-from 返回的是“显式授权集合”;devices 返回的是“当前有效授权集合”。当前网关语义如下:

  • allowFrom 文件非空时,以文件里的显式绑定用户为准
  • 若文件不存在,但 accounts.<id>.allow_from 在配置里有值,则该配置列表会作为显式授权集合
  • allowFrom 文件为空时,fallback 到扫码登录保存的 userId
  • bind(U2) 只会基于当前显式授权集合写回;若当前仍处于 fallback owner 模式,首次 bind 不会把 owner 自动固化进文件
  • unbind(U2) 若移除后文件为空,后续 inbound 会重新 fallback 到扫码 owner
  • file_present=true 表示该账号已经存在独立的 allowFrom 文件,即使文件内容当前为空

POST /api/v1/qr/start

{ "account_id": "bot-a", "force": false, "base_url": "https://ilinkai.weixin.qq.com", "bot_type": "3" }

POST /api/v1/qr/wait

{ "account_id": "bot-a", "session_key": "bot-a" }

说明:

  • session_key 可留空;当它为空时,网关会回退使用 account_id 等待已有 QR 会话。
  • 成功响应会返回原始 account_id,并额外带 normalized_account_id 供网关侧后续 API 使用。

POST /api/v1/send/text

context_token 可显式传入;若不传,则网关会尝试使用内存缓存的最近 token。

{ "account_id": "bot-a-im-bot", "to": "wxid_xxx@im.wechat", "text": "hello", "context_token": "optional" }

POST /api/v1/send/media

支持:

  • media_url
  • media_path
  • multipart file
{ "account_id": "bot-a-im-bot", "to": "wxid_xxx@im.wechat", "text": "附件如下", "media_path": "/data/demo/report.pdf" }

POST /api/v1/send/typing

可直接传 typing_ticket,也可只传 context_token,让网关先走 getConfig 补 ticket。

{ "account_id": "bot-a-im-bot", "to": "wxid_xxx@im.wechat", "context_token": "optional", "typing_ticket": "optional", "status": 1 }

Webhook 入站事件

若配置了 webhook.url,每条入站消息都会 POST 到该地址:

{ "channel": "openclaw-weixin", "account_id": "bot-a-im-bot", "from_user_id": "wxid_xxx@im.wechat", "to_user_id": "wxid_xxx@im.wechat", "body": "你好", "context_token": "....", "timestamp_ms": 1710000000000, "typing_ticket": "....", "media": { "media_path": "/abs/path/to/file", "media_type": "image/jpeg" }, "compat_context": { "Body": "你好", "From": "wxid_xxx@im.wechat", "AccountId": "bot-a-im-bot" }, "command": { "raw_body": "你好", "is_slash": false }, "authorization": { "sender_allowed": true, "mode": "pairing_allow_from" }, "lifecycle": { "source": "get_updates", "message_type_name": "user" }, "compatibility": { "version": "src-monitor-compat/v1", "mode": "explicit" }, "raw": { "...": "原始 WeixinMessage" } }

当前 webhook 是单向投递,不直接消费 webhook 返回体生成自动回复。自动回复可由业务侧收到 webhook 后调用 /api/v1/send/text/api/v1/send/media/api/v1/send/typing 完成。消费端应允许 compat_context / command / authorization / lifecycle / compatibility 这类增强字段存在。

浏览器测试台

根路径 //console 都会返回内嵌 HTML 控制台,可直接在网页端完成以下操作:

  • 健康检查、账号状态、账号列表
  • QR 登录 start/wait
  • 账号监控 start/stop
  • allow-from 查询、写入、移除
  • devices 查询、bind/unbind
  • debug 查询、开启、关闭、切换
  • 文本发送、媒体发送、typing 发送

这套页面适合做网关联调与人工回归;真正的 AI 自动回复仍应由外部业务服务消费 webhook 后再回调网关发送接口。

生产注意事项

  • context_token 仍保持与 TS 版一致,只存内存,不落盘;进程重启后不能做无上下文主动回复。
  • 图片/视频上传按 TS 真实实现移植,统一 no_need_thumb=true,不走缩略图。
  • account 维度 route_tag 已在 Go 版中生效;若不配置则回落到全局 weixin.route_tag
  • 入站媒体会落到 storage.data_dir/openclaw-weixin/media/inbound/
  • 浏览器测试台只负责调网关 API,不内置 LLM;AI 服务仍需监听 webhook.url 并主动调用发送接口。
  • webhook.secret 目前仍是保留配置字段,真实投递时不会自动签名;若要鉴权,请通过 webhook.headers 自行注入 Header。