乐于分享
好东西不私藏

从openclaw-weixin逆向搓CC桥接

从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 重写的收益:

  1. 独立部署:不依赖 OpenClaw 宿主,可作为 systemd/Docker 服务长期运行。
  2. 完全控制:系统提示、工具调用、多轮对话、流式回复都可以自己决定。
  3. 类型安全:协议类型、状态机、错误处理在编译期就能卡住。
  4. 资源效率:单个二进制,静态链接,内存占用低。

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:

Crate
职责
weixin-core
ilink 协议类型 + HTTP 客户端 + 长轮询 Monitor
weixin-auth
二维码登录、账号持久化、allowFrom 授权列表
weixin-media
AES-128-ECB、CDN 上传下载、缩略图
claude-api
Anthropic Messages API + OpenAI 兼容模式
bridge
消息路由、会话管理、回复编排、斜杠命令
cli clap

 命令行入口

3. 关键技术点与核心代码

3.1 二维码登录:从 ilink 拿 bot_token

微信 bot 登录不走 OAuth,而是走 ilink 的二维码流程:

  1. POST ilink/bot/get_bot_qrcode 拿到二维码内容。
  2. 终端用 qrcode crate 渲染成 Unicode 二维码。
  3. GET ilink/bot/get_qrcode_status 长轮询扫码状态。
  4. 状态到 confirmed 后获得 bot_tokenilink_bot_idbaseurl

关键状态机:

#[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: &[u816]) -> 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)}

上传流程:

  1. 计算明文 MD5 和大小。
  2. 生成随机 16 字节 AES key。
  3. 调用 POST ilink/bot/getuploadurl 拿到 CDN 预签名上传参数。
  4. 用 AES-128-ECB 加密文件。
  5. PUT/POST 到 CDN。
  6. 服务端返回 x-encrypted-param,后续发送消息时携带。

下载流程反过来:从 encrypt_query_param 或 full_url 拉密文 → AES 解密 → 保存到媒体目录。

3.5 消息转换:WeChat → Claude

微信消息类型很多(文本、图片、语音、文件、视频),但 Claude Messages API 只认 text 和 image content block。所以要做一层转换:

  • 文本 body 直接作为 text block。
  • 图片文件读取后 base64 编码成 image block。
  • 文本类文件(CSV、TXT、JSON、Markdown 等)直接读取内容内联为 text block,限制 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-rscalaminepdf-extract。解析失败时 bridge 会回退到文件附件提示。

3.6 会话上下文管理

微信没有原生的“会话 ID”,但是每条入站消息会带一个 context_token,回复时必须原样回传。我们用 account_id:user_id 作为内部会话 key,维护历史消息列表。

pubstructSessionStore {    inner: Arc<RwLock<HashMap<StringVec<Message>>>>,    config: SessionConfig,}

每次收到消息:

  1. 把 InboundMessage 转成 Claude Message
  2. 从 SessionStore 读出历史。
  3. 截断到 max_messages / max_input_tokens 预算内。
  4. 调用 Claude API。
  5. 把 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;

注意两点:

  1. Claude Code CLI 在 --print 模式下要求输入通过 stdin 或 prompt 参数提供,直接传位置参数会被忽略并报错。
  2. 微信桥接无法做交互确认claude -p 默认的 permission_mode = "auto" 在遇到文件写入、代码编辑等操作时仍会请求人工确认,但微信消息是单向文本通道,用户没法像终端里一样按 Y/N。因此生产环境推荐 permission_mode = "bypassPermissions",让 Claude Code 自动放行执行;如果需要限制风险,可以在 allowed_tools 里收窄允许的工具集。

4. 从零开始的实现顺序

如果你想完全自己手搓,建议按这个顺序:

  1. 协议类型:把 openclaw-weixin 的 src/api/types.ts 镜像成 Rust struct(weixin-core/src/types.rs)。
  2. HTTP 客户端:实现带固定请求头的 WeixinClientapi_post / api_get)。
  3. 二维码登录:实现 QrLoginClient,能在终端展示二维码并拿到 bot_token
  4. 账号持久化accounts.json 索引 + {accountId}.json 凭证,权限 0o600
  5. 长轮询Monitor 循环 + sync_buf 持久化 + 取消机制。
  6. 鉴权allowFrom 读取/写入、/allow 斜杠命令。
  7. 文本收发sendmessage 发送文本、markdown 过滤、长文本分块。
  8. Claude API:先实现 Anthropic 模式,再扩展 OpenAI 兼容模式。
  9. 媒体:AES-128-ECB、CDN 上传下载、缩略图、SILK→WAV。
  10. 会话与上下文SessionStoreContextTokenStore、多账号隔离。
  11. 桥接编排Bridge 把上面所有模块串起来。
  12. CLI 与部署clap 子命令、systemd unit、Dockerfile。

5. 容易踩的坑

  1. 请求头大小写:HTTP/2 会把 header 名转小写,但 authorizationtype 这类自定义头必须显式指定。
  2. AES key 格式:下载接口返回的 aes_key 是 base64 字符串,解密前要解码成 16 字节。
  3. sync_buf 为空:首次登录后 get_updates_buf 可能是空字符串,必须作为 None 或 "" 发送,不能缺失。
  4. context_token 回传:回复消息时一定要把入站消息的 context_token 原样塞回 sendmessage 的请求体,否则微信侧会找不到会话。
  5. 长文本分块:微信单条消息有长度限制,超过后要拆成多条 sendmessage,每条都带 context_token
  6. Claude Code CLI 的 stdinclaude -p 读 prompt 推荐用 stdin,位置参数在 --allowedTools 之后会被误判。
  7. 微信桥接无法交互确认/code 默认应使用 permission_mode = "bypassPermissions",否则 claude -p 在写入文件/编辑代码时会等待终端确认,微信用户无法响应,导致超时失败。
  8. 并发锁:不要在 await 之间持有 std::sync::Mutex,要用 tokio::sync::Mutex 或 RwLock
  9. 文档解析限制: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服务,给我一个报告。