从openclaw-weixin逆向搓CC桥接

序章:微信怎么指挥Claude Code远程干活
微信官方只发布了 OpenClaw 的 ClawBot 插件,专门面向 OpenClaw 这个 AI Gateway 框架。Claude Code 本身没有官方的微信插件。
可用的方案
方案一:通过 OpenClaw 间接接入(最稳)
OpenClaw 可以把 Claude Code 作为后端模型,再通过官方微信 ClawBot 插件收发消息。本质是:微信 → OpenClaw → Claude Code。这是目前最合规、最稳定的路径。
方案二:cc-connect(开源,直连)
cc-connect 是一个开源项目,把本地 Claude Code 直接接入微信/飞书/钉钉等平台。配好之后,直接在微信里发消息,Claude Code 就会在电脑上干活,然后把结果发回来。
它不是一个新 AI,就是个消息桥——微信发消息过去,cc-connect 转一道手,调你电脑上的 Claude Code,结果再传回微信。说白了给 Claude Code 装了个微信遥控器,消耗的还是你自己的额度。
GitHub 地址:https://github.com/chenhg5/cc-connect

方案三:cli-wechat-bridge(npm 包)
另一个方案 cli-wechat-bridge,要求 Node.js ≥ 24.0.0、已安装 Claude Code、手机微信保持登录,安装命令:
npm install -g cli-wechat-bridge@latest
然后扫码登录即可。
方案四:Weixin-Agent-SDK(更底层)
开发者基于微信 ClawBot 官方插件推出了 Weixin-Agent-SDK,借助这个 SDK 可以将任意 AI 智能体连接到微信,理论上支持 Claude Code、Codex 等所有支持 ACP 协议的 Agent。
不想瞎折腾的建议
如果你只是想随时随地用手机微信给 Claude Code 下任务,cc-connect 或 cli-wechat-bridge 是最直接的方案。如果你已经在用 OpenClaw,走官方 ClawBot 插件更稳,不用担心封号风险。
下面就是我这种喜欢折腾的人干的事,供大家参考
“
本文档记录如何基于 OpenClaw 官方微信渠道插件(TypeScript)的逆向分析结论,用 Rust 从零实现一个独立运行的微信-Claude 桥接服务。 目标:让微信个人账号能够收发消息、调用 Claude 模型、支持多账号与持久化。
1. 为什么要手搓一个 Rust 版
OpenClaw 的 openclaw-weixin 是一个 TypeScript 写的 ChannelPlugin,必须跑在 OpenClaw gateway 里。它的核心逻辑可以抽象成几条清晰的边界:
-
上游:腾讯 ilink 微信 HTTP API(二维码登录、拉消息、发消息、CDN 上传下载)。 -
下游:Claude Messages API。 -
中间:消息路由、会话上下文、账号调度、鉴权、媒体编解码。
用 Rust 重写的收益:
-
独立部署:不依赖 OpenClaw 宿主,可作为 systemd/Docker 服务长期运行。 -
完全控制:系统提示、工具调用、多轮对话、流式回复都可以自己决定。 -
类型安全:协议类型、状态机、错误处理在编译期就能卡住。 -
资源效率:单个二进制,静态链接,内存占用低。
2. 整体架构
用户微信发消息 │ ▼腾讯 ilink 网关 │ ▼getupdates 长轮询 ◄── sync_buf(游标)持久化 │ ▼processOneMessage ├── 媒体下载 / AES 解密 ├── 鉴权(allowFrom / pairing) ├── 路由 resolveAgentRoute └── 通过 channelRuntime 进入 OpenClaw AI pipeline │ ▼ OpenClaw 调度 Claude/模型生成回复 │ ▼ deliver(payload) → sendmessage / CDN upload │ ▼ 微信用户收到回复
Rust 版采用 Cargo workspace,拆成 6 个 crate:
|
|
|
|---|---|
weixin-core |
|
weixin-auth |
|
weixin-media |
|
claude-api |
|
bridge |
|
cli |
clap
|
3. 关键技术点与核心代码
3.1 二维码登录:从 ilink 拿 bot_token
微信 bot 登录不走 OAuth,而是走 ilink 的二维码流程:
-
POST ilink/bot/get_bot_qrcode拿到二维码内容。 -
终端用 qrcodecrate 渲染成 Unicode 二维码。 -
GET ilink/bot/get_qrcode_status长轮询扫码状态。 -
状态到 confirmed后获得bot_token、ilink_bot_id、baseurl。
关键状态机:
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "snake_case")]pubenumQrStatus {#[default] Wait, Scanned, Confirmed, Expired, NeedVerifyCode, VerifyCodeBlocked, ScannedButRedirect, BindedRedirect, ScanedButRedirect,}
二维码过期后会自动刷新,最多 MAX_QR_REFRESH_COUNT 次。登录成功后把 bot_token 和 base_url 按账号 ID 存到本地,文件权限设为 0o600。
3.2 HTTP 客户端:固定请求头是核心
ilink 接口有严格的请求头约定,少了任何一个都会返回鉴权失败:
const H_AUTHORIZATION_TYPE: HeaderName = HeaderName::from_static("authorizationtype");const H_X_WECHAT_UIN: HeaderName = HeaderName::from_static("x-wechat-uin");const H_ILINK_APP_ID: HeaderName = HeaderName::from_static("ilink-app-id");const H_ILINK_APP_CLIENT_VERSION: HeaderName = HeaderName::from_static("ilink-appclientversion");fnbuild_headers(&self) -> HeaderMap {letmut headers = HeaderMap::new(); headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); headers.insert(H_AUTHORIZATION_TYPE, HeaderValue::from_static("ilink_bot_token")); headers.insert(H_X_WECHAT_UIN, random_wechat_uin()); headers.insert(H_ILINK_APP_ID, ilink_app_id()); headers.insert(H_ILINK_APP_CLIENT_VERSION, ilink_app_client_version());ifletSome(token) = &self.token {let value = format!("Bearer {token}");ifletOk(hv) = HeaderValue::from_str(&value) { headers.insert(AUTHORIZATION, hv); } } headers}
注意点:
-
AuthorizationType是ilink_bot_token,不是Bearer。 -
Authorization头是Bearer <bot_token>。 -
X-WECHAT-UIN是随机 uint32 的 base64。 -
iLink-App-Id和iLink-App-ClientVersion需要和 openclaw-weixin 保持一致。
3.3 消息长轮询与 sync_buf 持久化
POST ilink/bot/getupdates 是长轮询接口,请求体里带 get_updates_buf,服务端会 hold 住连接直到有新消息或超时。
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]#[serde(default, rename_all = "snake_case")]pubstructGetUpdatesReq {#[serde(skip_serializing_if = "Option::is_none")]pub get_updates_buf: Option<String>,#[serde(skip_serializing_if = "Option::is_none")]pub limit: Option<i32>,}
每次成功拉取消息后,必须把服务端返回的最新 get_updates_buf 写盘。这样重启后才能从断点续传,避免消息丢失或重复。
Monitor 循环的结构:
loop {select! { _ = cancel.cancelled() => break, result = client.get_updates(req, cancellation_token) => {match result {Ok(resp) => { session_guard.reset_failures(); sync_buf.save(&resp.get_updates_buf)?;for msg in resp.msg_list { on_message(msg).await; } }Err(WeixinError::Api { errcode: -14, .. }) => { session_guard.pause(Duration::from_secs(3600)); }Err(_) => {if session_guard.record_failure() >= 3 { sleep(Duration::from_secs(30)).await; } } } } }}
关键点:
-
每个账号一个 tokio::task,一个CancellationToken子节点。 -
服务端返回 errcode=-14时暂停该账号 1 小时。 -
连续失败 3 次后休眠 30 秒。
3.4 媒体消息:AES-128-ECB + CDN
微信 CDN 上传要求先加密再上传,下载后解密。协议使用 AES-128-ECB + PKCS7 padding。
pubfnencrypt_aes_ecb(plaintext: &[u8], key: &[u8; 16]) -> Result<Vec<u8>, MediaError> {let cipher = Aes128::new_from_slice(key).map_err(|_| MediaError::Aes)?;let padded_size = aes_ecb_padded_size(plaintext.len());letmut buf = vec![0u8; padded_size]; buf[..plaintext.len()].copy_from_slice(plaintext);let pad_len = padded_size - plaintext.len();if pad_len == 0 {let block_start = padded_size - 16;for byte in &mut buf[block_start..] { *byte = 16; } } else {for byte in &mut buf[plaintext.len()..] { *byte = pad_len asu8; } }for chunk in buf.chunks_exact_mut(16) {let block = GenericArray::from_mut_slice(chunk); cipher.encrypt_block(block); }Ok(buf)}
上传流程:
-
计算明文 MD5 和大小。 -
生成随机 16 字节 AES key。 -
调用 POST ilink/bot/getuploadurl拿到 CDN 预签名上传参数。 -
用 AES-128-ECB 加密文件。 -
PUT/POST到 CDN。 -
服务端返回 x-encrypted-param,后续发送消息时携带。
下载流程反过来:从 encrypt_query_param 或 full_url 拉密文 → AES 解密 → 保存到媒体目录。
3.5 消息转换:WeChat → Claude
微信消息类型很多(文本、图片、语音、文件、视频),但 Claude Messages API 只认 text 和 image content block。所以要做一层转换:
-
文本 body 直接作为 textblock。 -
图片文件读取后 base64 编码成 imageblock。 -
文本类文件(CSV、TXT、JSON、Markdown 等)直接读取内容内联为 textblock,限制 256KB。 -
Office 文档(DOCX、XLSX、PDF)通过 weixin-media的document模块提取文本后再内联: -
DOCX 提取段落; -
XLSX 每个 sheet 转成 Markdown 表格; -
PDF 提取文本(暂不支持扫描版 OCR)。 -
其他不支持的文件只发一行 [Attached file: {file_name}]。
核心入口:
pubfninbound_to_claude_message(msg: &InboundMessage) -> Message {// ... 文本 body ...ifletSome(ref media) = msg.media {let path = &media.media_path;if media.media_type.as_str().starts_with("image/") {// base64 image block } elseif is_text_media_type(...) || is_text_file_extension(path) || is_document_file(path) {// 文本/文档内联let extracted = if is_document_file(path) { weixin_media::extract_document_text(path).ok() } else { None };// 读取文件, extracted 优先,受 256KB 限制 } else {// [Attached file: name] } }}
weixin-media/src/document.rs 提供统一入口:
pubfnextract_document_text(path: &Path) -> Result<String, DocumentError> {match ext.as_str() {"docx" => extract_docx(path),"xlsx" => extract_xlsx(path),"pdf" => extract_pdf(path), other => Err(DocumentError::UnsupportedFormat(other.to_string())), }}
依赖:docx-rs、calamine、pdf-extract。解析失败时 bridge 会回退到文件附件提示。
3.6 会话上下文管理
微信没有原生的“会话 ID”,但是每条入站消息会带一个 context_token,回复时必须原样回传。我们用 account_id:user_id 作为内部会话 key,维护历史消息列表。
pubstructSessionStore { inner: Arc<RwLock<HashMap<String, Vec<Message>>>>, config: SessionConfig,}
每次收到消息:
-
把 InboundMessage转成 ClaudeMessage。 -
从 SessionStore读出历史。 -
截断到 max_messages/max_input_tokens预算内。 -
调用 Claude API。 -
把 assistant 回复写回会话。
context_token 也要持久化,因为服务重启后第一条入站消息会带它,否则回复会找不到对应账号。
3.7 鉴权:allowFrom 白名单
默认拒绝所有未授权用户。用户可发送 /allow 申请,管理员用 CLI 命令授权:
claude-weixin allow <ACCOUNT_ID> <USER_ID>
授权列表按账号存成 JSON,例如 ~/.local/share/claude-weixin/openclaw-weixin-accounts/{accountId}-allowFrom.json。
Bridge 里收到消息时先走鉴权:
let authorized = resolve_authorization( &AuthorizationInput { from: msg.from.clone(), account_user_id: account.user_id.clone(), runtime_allow_list, configured_allow_list, mode: auth_mode, },);match authorized { AuthorizationOutcome::Allowed => { /* 走 LLM 流程 */ } AuthorizationOutcome::Denied => { send_text_reply("您尚未被授权,请发送 /allow 申请,或联系管理员。").await; } AuthorizationOutcome::Pairing => { /* 通知管理员审批 */ }}
3.8 Claude API 客户端:Anthropic + OpenAI 兼容
claude-api crate 同时支持 Anthropic Messages API 和 OpenAI 兼容接口。关键是在请求构造和 SSE 解析时做分支:
let response = matchself.api_format { ApiFormat::Anthropic => self.create_message_anthropic(req).await, ApiFormat::OpenAiCompatible => self.create_message_openai(req).await,};
OpenAI 模式把 MessageRequest 转成 /v1/chat/completions 的 JSON,把 SSE data: {...} 解析成统一的文本增量事件。这样上层 bridge 不需要关心底层是 Kimi、MiniMax 还是 Claude。
3.9 /code 斜杠命令:调用本地 Claude Code CLI
这是一个额外功能:让授权用户直接在微信里调用本地 claude -p。

配置:
[claude_code]enabled = trueclaude_path = "claude"timeout_seconds = 300permission_mode = "bypassPermissions" # 默认自动放行allowed_tools = "default"
执行时把 prompt 通过 stdin 传给 claude -p,在工作目录 ~/.local/share/claude-weixin/workspaces/{account_id}/ 下运行,捕获 stdout 返回:
letmut child = cmd.spawn().map_err(ClaudeCodeError::Spawn)?;ifletSome(mut stdin) = child.stdin.take() { stdin.write_all(prompt.as_bytes()).await.map_err(ClaudeCodeError::Spawn)?;drop(stdin);}let result = timeout(duration, child.wait_with_output()).await;
注意两点:
-
Claude Code CLI 在 --print模式下要求输入通过 stdin 或 prompt 参数提供,直接传位置参数会被忽略并报错。 -
微信桥接无法做交互确认。 claude -p默认的permission_mode = "auto"在遇到文件写入、代码编辑等操作时仍会请求人工确认,但微信消息是单向文本通道,用户没法像终端里一样按Y/N。因此生产环境推荐permission_mode = "bypassPermissions",让 Claude Code 自动放行执行;如果需要限制风险,可以在allowed_tools里收窄允许的工具集。
4. 从零开始的实现顺序
如果你想完全自己手搓,建议按这个顺序:
-
协议类型:把 openclaw-weixin 的 src/api/types.ts镜像成 Rust struct(weixin-core/src/types.rs)。 -
HTTP 客户端:实现带固定请求头的 WeixinClient(api_post/api_get)。 -
二维码登录:实现 QrLoginClient,能在终端展示二维码并拿到bot_token。 -
账号持久化: accounts.json索引 +{accountId}.json凭证,权限0o600。 -
长轮询: Monitor循环 +sync_buf持久化 + 取消机制。 -
鉴权: allowFrom读取/写入、/allow斜杠命令。 -
文本收发: sendmessage发送文本、markdown 过滤、长文本分块。 -
Claude API:先实现 Anthropic 模式,再扩展 OpenAI 兼容模式。 -
媒体:AES-128-ECB、CDN 上传下载、缩略图、SILK→WAV。 -
会话与上下文: SessionStore、ContextTokenStore、多账号隔离。 -
桥接编排: Bridge把上面所有模块串起来。 -
CLI 与部署: clap子命令、systemd unit、Dockerfile。
5. 容易踩的坑
-
请求头大小写:HTTP/2 会把 header 名转小写,但 authorizationtype这类自定义头必须显式指定。 -
AES key 格式:下载接口返回的 aes_key是 base64 字符串,解密前要解码成 16 字节。 -
sync_buf为空:首次登录后get_updates_buf可能是空字符串,必须作为None或""发送,不能缺失。 -
context_token回传:回复消息时一定要把入站消息的context_token原样塞回sendmessage的请求体,否则微信侧会找不到会话。 -
长文本分块:微信单条消息有长度限制,超过后要拆成多条 sendmessage,每条都带context_token。 -
Claude Code CLI 的 stdin: claude -p读 prompt 推荐用 stdin,位置参数在--allowedTools之后会被误判。 -
微信桥接无法交互确认: /code默认应使用permission_mode = "bypassPermissions",否则claude -p在写入文件/编辑代码时会等待终端确认,微信用户无法响应,导致超时失败。 -
并发锁:不要在 await之间持有std::sync::Mutex,要用tokio::sync::Mutex或RwLock。 -
文档解析限制:DOCX/XLSX/PDF 通过纯 Rust 库解析,复杂格式或扫描版 PDF 可能提取不完整,失败时只会提示 [Attached file: name]。
6. 参考
-
OpenClaw 官方微信插件: openclaw-weixin(版本 2.4.4) -
协议文档: openclaw-weixin/README.zh_CN.md中“后端 API 协议”章节 -
Anthropic Messages API: https://docs.anthropic.com/en/api/messages
7. 两个简易任务

比如在微信上用 /code 让claude写个hello_claude.py文件。

又比如在微信上用 /code 让claude给我当前市场什么信号,它会自动调用jctrader这个MCP服务,给我一个报告。
夜雨聆风