OpenClaw微信渠道插件 — 源码深度剖析

从插件注册到消息收发全链路,深入解构核心架构与模块交互逻辑

Posted by iceyao on Monday, March 23, 2026

OpenClaw 微信渠道插件 — 源码深度剖析

从插件注册到消息收发全链路,深入解构 @tencent-weixin/openclaw-weixin 的核心架构、关键算法与模块间交互逻辑

📦 v1.0.2 · 📄 34 个 TypeScript 源文件 · 🔧 Node.js ≥ 22 · 📅 2026-03-22

🔭 一、项目概览与技术栈

@tencent-weixin/openclaw-weixinOpenClaw AI 对话平台的微信渠道插件。它的核心使命是:让 AI Agent 能够通过微信进行对话——接收微信用户的消息(文本、图片、语音、文件、视频),将其投递给 AI 引擎处理,再把 AI 的回复发送回微信用户。

从技术视角看,这是一个典型的**“协议适配层”**——将微信私有的 iLink Bot 协议转换为 OpenClaw 框架统一的消息管线接口。

维度 技术选型
语言 TypeScript(ESM,strict mode)
运行时 Node.js ≥ 22(原生 fetch、ESM)
依赖数量 仅 2 个 runtime 依赖:qrcode-terminal(终端二维码)、zod(配置校验)
通信协议 HTTP JSON API(POST),长轮询(Long Polling)
加密算法 AES-128-ECB + PKCS7(CDN 媒体传输)
音频编解码 SILK → PCM → WAV(可选 silk-wasm)
状态持久化 本地 JSON 文件(~/.openclaw/)

🏗️ 二、整体架构设计

插件的核心架构可以用一张图来全局概览:

▼ 系统架构总览

graph TB
    subgraph 微信侧
        WX[微信用户]
        ILINK[iLink Bot 网关<br/>ilinkai.weixin.qq.com]
        CDN[微信 CDN<br/>novac2c.cdn.weixin.qq.com]
    end

    subgraph OpenClaw 插件["OpenClaw 微信渠道插件"]
        direction TB
        REG[插件注册入口<br/>index.ts]
        CH[Channel 适配器<br/>channel.ts]
        MON[长轮询监听器<br/>monitor.ts]
        PM[消息处理管线<br/>process-message.ts]
        
        subgraph 消息层
            INB[入站标准化<br/>inbound.ts]
            SEND[出站发送<br/>send.ts]
            MEDIA[媒体发送<br/>send-media.ts]
            SLASH[斜杠指令<br/>slash-commands.ts]
        end
        
        subgraph API层
            API[API 客户端<br/>api.ts]
            CACHE[配置缓存<br/>config-cache.ts]
            GUARD[会话守卫<br/>session-guard.ts]
        end
        
        subgraph CDN加密层
            AES[AES-ECB 加解密<br/>aes-ecb.ts]
            UP[CDN 上传<br/>cdn-upload.ts + upload.ts]
            DEC[CDN 下载解密<br/>pic-decrypt.ts]
        end
        
        subgraph 认证层
            AUTH[账号管理<br/>accounts.ts]
            QR[扫码登录<br/>login-qr.ts]
            PAIR[配对鉴权<br/>pairing.ts]
        end
        
        subgraph 存储层
            STATE[状态目录<br/>state-dir.ts]
            SYNC[同步游标<br/>sync-buf.ts]
            DBG[调试模式<br/>debug-mode.ts]
        end
    end

    subgraph OpenClaw 框架
        CORE[OpenClaw Core<br/>路由 · 会话 · AI Agent]
    end

    WX <--> ILINK
    WX --> CDN
    CDN --> WX
    
    ILINK <--> API
    CDN <--> UP
    CDN <--> DEC
    
    REG --> CH
    CH --> MON
    MON --> PM
    PM --> INB
    PM --> SEND
    PM --> MEDIA
    PM --> SLASH
    
    INB --> CORE
    CORE --> SEND
    CORE --> MEDIA
    
    API --> GUARD
    MON --> CACHE
    AUTH --> STATE
    SYNC --> STATE

💡 架构核心理念

整个插件采用**“洋葱模型”分层设计:最外层是协议适配**(API/CDN 加解密),中间层是消息管线(标准化、路由、分发),最内层对接 OpenClaw 框架核心。每一层都有明确的职责边界和接口契约。


📁 三、源码目录结构解析

plugin/package/
├── index.ts              ← 插件入口,注册 Channel + CLI
├── package.json          ← 依赖 & 元数据
├── openclaw.plugin.json  ← 插件声明(ID、频道列表、配置 Schema)
└── src/
    ├── channel.ts        ← ChannelPlugin 主适配器(380 行核心)
    ├── runtime.ts        ← 全局运行时管理(异步等待初始化)
    ├── log-upload.ts     ← CLI 子命令:日志上传
    ├── vendor.d.ts       ← 第三方类型声明
    ├── api/              ← 微信 iLink API 通信层
    │   ├── api.ts          HTTP 请求封装(5 个端点)
    │   ├── types.ts        协议类型定义
    │   ├── config-cache.ts 配置缓存(TTL + 指数退避)
    │   └── session-guard.ts 会话过期保护
    ├── auth/             ← 认证 & 账号管理
    │   ├── accounts.ts     多账号 CRUD
    │   ├── login-qr.ts     扫码登录状态机
    │   └── pairing.ts      配对鉴权(allowFrom 管理)
    ├── cdn/              ← CDN 媒体加密传输
    │   ├── aes-ecb.ts      AES-128-ECB 加解密原语
    │   ├── cdn-upload.ts   CDN 上传(含重试)
    │   ├── cdn-url.ts      CDN URL 构造器
    │   ├── pic-decrypt.ts  CDN 下载 + 解密
    │   └── upload.ts       统一上传管线(hash→encrypt→upload)
    ├── media/            ← 媒体处理
    │   ├── media-download.ts 多类型媒体下载分发
    │   ├── mime.ts         MIME 类型 ↔ 扩展名映射
    │   └── silk-transcode.ts SILK 语音转 WAV
    ├── messaging/        ← 消息处理核心
    │   ├── process-message.ts 单消息全链路处理(482 行)
    │   ├── inbound.ts      入站消息标准化 & contextToken 缓存
    │   ├── send.ts         出站发送(文本/图片/视频/文件)
    │   ├── send-media.ts   媒体发送路由(按 MIME 分发)
    │   ├── slash-commands.ts 斜杠指令处理器
    │   ├── debug-mode.ts   调试模式开关
    │   └── error-notice.ts 错误通知(fire-and-forget)
    ├── storage/          ← 本地持久化
    │   ├── state-dir.ts    状态目录解析
    │   └── sync-buf.ts     长轮询同步游标持久化
    ├── util/             ← 通用工具
    │   ├── logger.ts       结构化 JSON 日志
    │   ├── random.ts       唯一 ID 生成
    │   └── redact.ts       敏感信息脱敏
    └── config/
        └── config-schema.ts Zod 配置校验 Schema

🔌 四、插件注册与生命周期

插件的生命始于 index.ts,这是 OpenClaw 框架加载插件时的唯一入口点:

▼ 插件注册时序图

sequenceDiagram
    participant F as OpenClaw Framework
    participant P as Plugin (index.ts)
    participant R as Runtime Manager
    participant C as Channel Adapter
    participant CLI as CLI Registry

    F->>P: import default plugin
    F->>P: plugin.register(api)
    P->>R: setWeixinRuntime(api.runtime)
    R-->>R: 存储全局 pluginRuntime
    P->>C: api.registerChannel({ plugin: weixinPlugin })
    Note over C: 注册消息出/入站适配器<br/>登录逻辑、网关启动函数
    P->>CLI: api.registerCli(registerWeixinCli)
    Note over CLI: 注册 logs-upload 子命令
    F-->>F: 插件就绪,开始启动账号

注册过程体现了三个关键设计

4.1 Runtime 注入模式

setWeixinRuntime() 将框架提供的 PluginRuntime 存入模块级全局变量。这使得后续所有模块都能通过 getWeixinRuntime() 访问框架能力(路由、会话、媒体存储等),而不需要在函数调用链中层层传递:

// runtime.ts — 异步等待模式
export async function waitForWeixinRuntime(timeoutMs = 10_000): Promise<PluginRuntime> {
  const start = Date.now();
  while (!pluginRuntime) {
    if (Date.now() - start > timeoutMs) throw new Error("timeout");
    await new Promise(r => setTimeout(r, 100));  // 100ms 轮询间隔
  }
  return pluginRuntime;
}

💡 设计考量

为什么不直接传参?因为 monitorWeixinProvider 在独立的异步循环中运行,可能在 register() 完成之前就被调用。waitForWeixinRuntime() 的轮询等待机制优雅地解决了这个竞态问题。

4.2 ChannelPlugin 契约

weixinPlugin 对象实现了 OpenClaw 的 ChannelPlugin 接口,这是一个全面的适配契约,包含 8 个主要模块:

模块 说明
meta 渠道元信息(ID、标签、文档路径、排序权重)
capabilities 声明支持的能力:direct 聊天、媒体传输
config 账号列表、解析、状态描述
outbound 出站消息发送(文本 + 媒体)
auth 扫码登录流程编排
gateway 网关启动(启动长轮询)+ QR 登录
status 运行时状态快照 & 健康检查
agentPrompt 给 AI Agent 的工具使用提示

📱 五、扫码登录认证流程

登录是用户使用插件的第一步,也是最复杂的交互流程之一。核心实现在 login-qr.ts 中,采用两阶段异步状态机设计:

▼ 扫码登录全流程

sequenceDiagram
    actor U as 用户
    participant CLI as OpenClaw CLI
    participant QR as login-qr.ts
    participant ILINK as iLink 网关
    participant ACC as accounts.ts

    U->>CLI: openclaw channels login
    CLI->>QR: startWeixinLoginWithQr()
    QR->>QR: purgeExpiredLogins()
    QR->>ILINK: GET /ilink/bot/get_bot_qrcode?bot_type=3
    ILINK-->>QR: { qrcode, qrcode_img_content }
    QR->>QR: 存入 activeLogins Map
    QR-->>CLI: { qrcodeUrl, sessionKey }
    CLI->>U: 终端显示二维码 📱

    U->>U: 手机扫码

    CLI->>QR: waitForWeixinLogin(sessionKey)

    loop 轮询状态(最多 8 分钟)
        QR->>ILINK: GET /get_qrcode_status?qrcode=xxx
        alt status = "wait"
            ILINK-->>QR: { status: "wait" }
            Note over QR: 继续轮询
        else status = "scaned"
            ILINK-->>QR: { status: "scaned" }
            QR-->>U: 👀 已扫码,请在手机上确认...
        else status = "expired"
            ILINK-->>QR: { status: "expired" }
            QR->>ILINK: 重新获取二维码(最多3次)
            QR-->>U: ⏳ 新二维码已生成
        else status = "confirmed"
            ILINK-->>QR: { status: "confirmed", bot_token, ilink_bot_id, ilink_user_id }
        end
    end

    QR->>ACC: saveWeixinAccount(normalizedId, { token, baseUrl, userId })
    QR->>ACC: registerWeixinAccountId(normalizedId)
    ACC->>ACC: 写入 accounts.json 索引
    ACC->>ACC: 写入 {accountId}.json 凭证
    QR-->>U: ✅ 与微信连接成功!

5.1 关键算法:二维码自动刷新

二维码有有效期限制,插件实现了自动刷新机制:最多连续刷新 3 次,每次过期后自动向 iLink 网关重新申请二维码并在终端重新渲染。这避免了用户因超时而需要重新执行整个登录命令。

5.2 账号 ID 规范化

微信返回的原始 bot ID 格式为 hex@im.bot,包含 @. 等文件系统不安全字符。插件通过框架提供的 normalizeAccountId() 将其转换为安全格式(如 hex-im-bot),同时保留 deriveRawAccountId() 反向推导能力,确保旧版数据兼容。


🔄 六、长轮询消息监听机制

插件的"心脏"是 monitor.ts 中的长轮询循环。它是连接微信消息流和 AI 管线的桥梁

▼ 长轮询监听主循环

flowchart TD
    START([gateway.startAccount]) --> INIT[初始化]
    INIT --> |等待 runtime| RUNTIME{runtime 就绪?}
    RUNTIME --> |否| WAIT_RT[轮询等待 100ms]
    WAIT_RT --> RUNTIME
    RUNTIME --> |是| LOAD[加载同步游标<br/>syncBuf]
    LOAD --> POLL

    POLL[getUpdates 长轮询<br/>timeout=35s] --> RESP{响应结果?}

    RESP --> |超时/空| SAVE_BUF[保存同步游标]
    SAVE_BUF --> POLL

    RESP --> |API 错误| ERR_CHECK{errcode=-14?<br/>会话过期}
    ERR_CHECK --> |是| PAUSE[pauseSession<br/>暂停 1 小时]
    PAUSE --> SLEEP_PAUSE[sleep 1h]
    SLEEP_PAUSE --> POLL
    ERR_CHECK --> |否| FAIL_INC[consecutiveFailures++]
    FAIL_INC --> FAIL_CHECK{连续失败 ≥ 3?}
    FAIL_CHECK --> |是| BACKOFF[退避 30s]
    BACKOFF --> POLL
    FAIL_CHECK --> |否| RETRY[重试 2s]
    RETRY --> POLL

    RESP --> |成功+有消息| RESET[重置失败计数]
    RESET --> UPDATE_BUF[更新同步游标]
    UPDATE_BUF --> LOOP[遍历消息列表]
    LOOP --> EACH[对每条消息]
    EACH --> CONFIG[获取用户配置<br/>typingTicket]
    CONFIG --> PROCESS[processOneMessage]
    PROCESS --> NEXT{还有消息?}
    NEXT --> |是| EACH
    NEXT --> |否| POLL

    RESP --> |AbortSignal| STOP([监听结束])

6.1 同步游标(Sync Buffer)机制

这是长轮询的核心状态管理,类似于消息队列中的 offset 概念:

  1. 首次请求get_updates_buf 传空字符串,服务端返回当前时间点之后的新消息
  2. 后续请求:将上次响应的 get_updates_buf 回传,服务端据此判断客户端已消费到哪里
  3. 持久化:游标写入 ~/.openclaw/openclaw-weixin/accounts/{id}.sync.json,重启后可断点续传
  4. 兼容性:依次尝试规范化 ID、原始 ID、单账号旧格式路径,覆盖三代存储方案

6.2 三级容错策略

场景 策略 参数
客户端超时(AbortError) 视为正常,立即重试
API 错误(ret≠0) 累计失败计数,2s 后重试 MAX = 3 次
连续 3 次失败 指数退避 30s 冷却
会话过期(errcode=-14) 全局暂停 1 小时

6.3 服务端自适应超时

服务端可以通过响应中的 longpolling_timeout_ms 字段动态调整客户端的下次轮询超时时间。这是一种服务端驱动的流量控制机制——当服务端负载高时可以增大超时值减少请求频率。


⚡ 七、消息处理管线

process-message.ts 是整个插件最复杂的模块(482 行),编排了从收到原始消息到 AI 回复送达的完整链路

▼ 单消息处理全流程

flowchart TD
    MSG([收到 WeixinMessage]) --> SLASH{以 / 开头?}
    SLASH --> |是| CMD[handleSlashCommand]
    CMD --> CMD_R{指令已处理?}
    CMD_R --> |是| END_CMD([结束,跳过 AI])
    CMD_R --> |否| MEDIA_FIND
    SLASH --> |否| MEDIA_FIND

    MEDIA_FIND[查找媒体附件<br/>优先级: 图片 > 视频 > 文件 > 语音]
    MEDIA_FIND --> HAS_MEDIA{有媒体?}
    HAS_MEDIA --> |是| DOWNLOAD[CDN 下载 + AES 解密]
    DOWNLOAD --> TRANSCODE{是语音?}
    TRANSCODE --> |是| SILK[SILK → WAV 转码]
    SILK --> NORMALIZE
    TRANSCODE --> |否| NORMALIZE
    HAS_MEDIA --> |否| NORMALIZE

    NORMALIZE[weixinMessageToMsgContext<br/>标准化为 MsgContext]

    NORMALIZE --> AUTH[鉴权管线]
    AUTH --> AUTH_R{通过?}
    AUTH_R --> |disabled/unauthorized| DROP([丢弃消息])
    AUTH_R --> |通过| ROUTE

    ROUTE[resolveAgentRoute<br/>确定 AI Agent 路由]
    ROUTE --> SESSION[recordInboundSession<br/>记录入站会话]
    SESSION --> TOKEN[缓存 contextToken]
    TOKEN --> TYPING[创建 Typing 回调<br/>5s keepalive]

    TYPING --> DISPATCH[dispatchReplyFromConfig<br/>调用 AI Agent 生成回复]

    DISPATCH --> DELIVER[deliver 回调]
    DELIVER --> MD2TXT[markdownToPlainText<br/>去除 Markdown 语法]
    MD2TXT --> HAS_MEDIA2{有媒体 URL?}
    HAS_MEDIA2 --> |是| RESOLVE_MEDIA{本地路径<br/>还是远程 URL?}
    RESOLVE_MEDIA --> |本地| UPLOAD_SEND[上传 CDN + 发送]
    RESOLVE_MEDIA --> |远程| DL_UPLOAD[下载 → 上传 CDN → 发送]
    HAS_MEDIA2 --> |否| SEND_TEXT[sendMessageWeixin 发送文本]
    UPLOAD_SEND --> DONE
    DL_UPLOAD --> DONE
    SEND_TEXT --> DONE

    DONE([回复送达]) --> DEBUG{debug 模式?}
    DEBUG --> |是| TIMING[追加全链路耗时报告]
    DEBUG --> |否| FINISH([处理完成])
    TIMING --> FINISH

7.1 入站消息标准化

weixinMessageToMsgContext() 将微信私有格式转为框架统一的 MsgContext

// 关键转换逻辑
const ctx: WeixinMsgContext = {
  Body: bodyFromItemList(msg.item_list),  // 提取文本 + 引用消息
  From: from_user_id,                      // 发送者
  To: from_user_id,                        // 回复目标(直聊=同一人)
  AccountId: accountId,                    // 当前 bot 账号
  Provider: "openclaw-weixin",             // 渠道标识
  ChatType: "direct",                      // 聊天类型
  MediaPath: opts?.decryptedPicPath,       // 已解密的本地媒体路径
};

引用消息处理

bodyFromItemList() 对引用消息有精巧处理:如果引用的是媒体(图片等),只传递当前文本作为 Body;如果引用的是文字,则将引用内容以 [引用: xxx] 格式拼接到正文前面,让 AI 获得完整的上下文。

7.2 Context Token 缓存

微信 API 要求每次发送回复都必须携带入站消息中的 context_token。插件使用一个进程内 Map 来缓存这个映射关系:

// key 格式: "{accountId}:{userId}"
const contextTokenStore = new Map<string, string>();

// 入站时写入
setContextToken(accountId, userId, token);

// 出站时读取
const token = getContextToken(accountId, userId);

这是一个无 TTL 的简单缓存——token 在每次收到新消息时更新,确保始终使用最新值。

7.3 鉴权管线

消息在进入 AI 管线前必须经过两道鉴权:

  1. 命令授权resolveSenderCommandAuthorizationWithRuntime):检查发送者是否在 allowFrom 列表中
  2. DM 策略resolveDirectDmAuthorizationOutcome):根据 pairing 策略判断是 disabled / unauthorized / authorized

allowFrom 列表的读取有两个来源(优先级递减):

  • 框架级 {accountId}-allowFrom.json 文件(配对流程写入)
  • 账号数据中的 userId(QR 登录时保存,兼容旧安装)

7.4 Typing 输入指示器

当 AI 在生成回复时,插件会向用户发送"正在输入"的状态指示。实现使用了 5 秒 keepalive 模式:

const typingCallbacks = createTypingCallbacks({
  start: () => sendTyping({ status: TypingStatus.TYPING }),
  stop:  () => sendTyping({ status: TypingStatus.CANCEL }),
  keepaliveIntervalMs: 5000,  // 每 5 秒重发一次
});

📤 八、消息发送与媒体上传

出站消息分为纯文本媒体两条路径,由 send-media.ts 按 MIME 类型统一路由:

▼ 媒体发送路由逻辑

flowchart LR
    INPUT[本地文件路径] --> MIME[getMimeFromFilename<br/>检测 MIME]
    MIME --> R{MIME 类型}
    R --> |video/*| VUP[uploadVideoToWeixin]
    R --> |image/*| IUP[uploadFileToWeixin]
    R --> |其他| FUP[uploadFileAttachmentToWeixin]
    VUP --> VS[sendVideoMessageWeixin]
    IUP --> IS[sendImageMessageWeixin]
    FUP --> FS[sendFileMessageWeixin]

8.1 Markdown → 纯文本转换

AI 模型的回复通常包含 Markdown 格式,但微信不支持 Markdown 渲染。markdownToPlainText() 实现了智能格式剥离

// 代码块:去除围栏,保留内容
result = result.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_, code) => code.trim());
// 图片:完全移除
result = result.replace(/!\[[^\]]*\]\([^)]*\)/g, "");
// 链接:只保留文字
result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
// 表格:去除分隔行,管道符转空格
result = result.replace(/^\|[\s:|-]+\|$/gm, "");

8.2 消息拆分策略

插件配置了 textChunkLimit: 4000,框架会自动将超长回复拆分为多个 4000 字符的分段依次发送。同时,媒体和文本分开发送——每个 item_list 只包含一个条目,避免消息组合的兼容性问题。


🔐 九、CDN 加密传输体系

这是插件技术含量最高的部分。微信的所有媒体文件都通过 CDN 传输,并使用 AES-128-ECB 对称加密。

▼ CDN 上传加密流程

flowchart TD
    FILE[本地文件] --> READ[读取文件内容<br/>Buffer]
    READ --> HASH[计算 MD5 + 大小]
    HASH --> KEY[生成随机 AES-128 密钥<br/>crypto.randomBytes 16]
    KEY --> PADSIZE[计算密文大小<br/>ceil(plain+1 / 16)× 16]
    PADSIZE --> API_CALL[调用 getUploadUrl API]
    API_CALL --> |获取 upload_param| ENC[AES-128-ECB 加密<br/>PKCS7 填充]
    ENC --> URL[构造 CDN URL<br/>cdnBaseUrl/upload?encrypted_query_param=...&filekey=...]
    URL --> POST[POST 密文到 CDN]
    POST --> |Header: x-encrypted-param| PARAM[获取 downloadEncryptedQueryParam]
    PARAM --> BUILD[构造 CDNMedia 引用]
    BUILD --> MSG[嵌入 MessageItem 发送]

9.1 AES-128-ECB 加解密

加密原语极简但高效,仅 22 行代码覆盖三个核心功能:

// 加密
export function encryptAesEcb(plaintext: Buffer, key: Buffer): Buffer {
  const cipher = createCipheriv("aes-128-ecb", key, null);
  return Buffer.concat([cipher.update(plaintext), cipher.final()]);
}

// 解密
export function decryptAesEcb(ciphertext: Buffer, key: Buffer): Buffer {
  const decipher = createDecipheriv("aes-128-ecb", key, null);
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
}

// 预测密文大小(PKCS7 填充)
export function aesEcbPaddedSize(plaintextSize: number): number {
  return Math.ceil((plaintextSize + 1) / 16) * 16;
}

⚠️ 为什么使用 ECB 模式?

ECB 模式在密码学上不推荐用于大数据加密(因为相同的明文块产生相同密文),但在微信 CDN 场景中,每个文件使用独立的随机密钥,且文件完整性由 CDN 层保证,因此 ECB 模式的简单性(无需 IV)是合理的工程权衡。

9.2 AES 密钥的双重编码问题

pic-decrypt.ts 中的 parseAesKey() 是一个有趣的兼容性处理——微信不同消息类型中 AES 密钥的编码方式不一致:

消息类型 编码格式 解码路径
图片 base64(16 字节原始密钥) base64 → 直接使用
文件/语音/视频 base64(32 字符 hex 字符串) base64 → ASCII → hex decode

parseAesKey() 通过检查 base64 解码后的长度(16 vs 32)和内容模式(是否全为 hex 字符)来自动判断编码方式。

9.3 CDN 上传重试机制

cdn-upload.ts 实现了区分客户端/服务端错误的重试策略:

  • 🔵 4xx 客户端错误 → 立即失败(请求本身有问题,重试无意义)
  • 🟠 5xx 服务端错误 → 最多重试 3 次
  • 🟢 200 成功 → 从响应头 x-encrypted-param 获取下载参数

👥 十、多账号管理与状态持久化

插件支持多个微信账号同时在线,账号数据分散在多个文件中,采用**“索引 + 数据"分离**的存储架构:

▼ 多账号存储架构

graph LR
    subgraph 状态目录["~/.openclaw/openclaw-weixin/"]
        INDEX[accounts.json<br/>账号 ID 索引]
        subgraph accounts["accounts/"]
            A1["{id-1}.json<br/>token + baseUrl + userId"]
            A2["{id-2}.json<br/>token + baseUrl + userId"]
            S1["{id-1}.sync.json<br/>同步游标"]
            S2["{id-2}.sync.json<br/>同步游标"]
        end
        DBG_F["debug-mode.json<br/>调试状态"]
    end

    subgraph credentials["~/.openclaw/credentials/"]
        AF1["openclaw-weixin-{id-1}-allowFrom.json<br/>配对授权列表"]
        AF2["openclaw-weixin-{id-2}-allowFrom.json<br/>配对授权列表"]
    end

    INDEX --> A1
    INDEX --> A2
    A1 --- S1
    A2 --- S2

10.1 三代兼容性适配

由于产品迭代,存在三种不同的数据存储格式:

版本 凭证路径 同步游标路径
第一代(单账号) credentials/openclaw-weixin/credentials.json agents/default/sessions/.openclaw-weixin-sync/default.json
第二代(原始 ID) accounts/{raw@id}.json accounts/{raw@id}.sync.json
第三代(规范化 ID) accounts/{normalized-id}.json accounts/{normalized-id}.sync.json

loadWeixinAccount()loadGetUpdatesBuf() 按优先级依次尝试这三种路径,新数据始终写入第三代格式,实现了无缝的向后兼容迁移。

10.2 凭证安全

保存凭证时,插件尝试将文件权限设为 0o600(仅所有者可读写),防止其他用户读取 bot token:

fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
try {
  fs.chmodSync(filePath, 0o600);  // 最佳努力
} catch { /* 某些文件系统可能不支持 */ }

🛡️ 十一、容错与可观测性设计

11.1 会话守卫(Session Guard)

当检测到 errcode=-14(会话过期)时,session-guard.ts 会将整个账号暂停 1 小时,阻止所有入站和出站 API 调用

// 每次 API 调用前必须检查
export function assertSessionActive(accountId: string): void {
  if (isSessionPaused(accountId)) {
    const remainingMin = Math.ceil(getRemainingPauseMs(accountId) / 60_000);
    throw new Error(
      `session paused, ${remainingMin} min remaining`
    );
  }
}

这防止了在会话失效后持续向微信服务器发送无效请求,避免被限流或封禁。

11.2 配置缓存(Config Cache)

WeixinConfigManagergetConfig API 实现了随机化 TTL + 指数退避的缓存策略:

▼ 配置缓存刷新策略

stateDiagram-v2
    [*] --> 无缓存
    无缓存 --> 获取中: 首次访问
    获取中 --> 已缓存: 成功(ret=0)
    获取中 --> 重试中: 失败
    已缓存 --> 获取中: 随机 TTL 到期(0~24h)
    重试中 --> 获取中: 退避后重试
    重试中 --> 重试中: 继续失败(延迟×2)

    note right of 已缓存
      nextFetchAt = now + random() × 24h
      抖动避免雷群效应
    end note

    note right of 重试中
      初始: 2s → 4s → 8s → ... → 最大 1h
      指数退避 + 上界
    end note

11.3 结构化日志

插件自实现了一个轻量但完备的 JSON Lines 日志系统:

  • 输出位置/tmp/openclaw/openclaw-{YYYY-MM-DD}.log
  • 格式:结构化 JSON,包含时间戳、日志级别、运行时信息、主机名
  • 级别过滤:通过 OPENCLAW_LOG_LEVEL 环境变量控制
  • 敏感信息脱敏redact.ts 工具自动截断 token、URL 参数、请求体
{
  "0": "gateway/channels/openclaw-weixin/abc-im-bot",
  "1": "[abc-im-bot] inbound message: from=user@im.wechat types=1",
  "_meta": {
    "logLevelName": "INFO",
    "date": "2026-03-22T10:30:00.000Z"
  }
}

11.4 Debug 模式

通过 /toggle-debug 斜杠指令可以为单个账号开启调试模式。开启后,每条消息处理完毕后会自动追加一条全链路耗时报告

⏱ Debug 全链路
── 收消息 ──
│ seq=1234 msgId=5678 from=user@im.wechat
│ body="你好" (len=2) itemTypes=[1]
── 鉴权 & 路由 ──
│ auth: cmdAuthorized=true senderAllowed=true
│ route: agent=default session=main
── 回复 ──
│ textLen=150 media=none
│ text="你好!我是 AI 助手..."
── 耗时 ──
├ 平台→插件: 120ms
├ 入站处理(auth+route+media): 45ms (mediaDownload: 0ms)
├ AI生成+回复: 2300ms
├ 总耗时: 2465ms
└ eventTime: 2026-03-22T10:30:00.000Z

11.5 错误通知

error-notice.ts 实现了 fire-and-forget 模式的用户侧错误通知——当发送失败时,尝试给用户发一条友好的错误提示。即使通知发送本身也失败了,也只记日志,绝不抛出异常影响主流程。


🎯 十二、总结与设计亮点

亮点 说明
🏛️ 清晰的分层架构 API 层 → 消息层 → 框架适配层,每层职责单一、接口明确,模块间通过类型契约而非具体实现耦合
🔄 优雅的容错设计 三级重试策略(即时重试→短间隔重试→长退避)覆盖不同级别的故障场景,会话守卫防止无效请求风暴
📦 极简依赖 仅 2 个运行时依赖(qrcode-terminal + zod),充分利用 Node.js 22 原生能力(fetch、crypto、ESM),部署轻量
🔐 端到端加密传输 所有媒体文件通过 AES-128-ECB 加密后上传/下载,密钥随机生成、按消息传递,确保传输安全
🔧 三代兼容迁移 数据加载函数自动适配三种历史存储格式,用户升级无需手动迁移,体现了对向后兼容的工程承诺
👁️ 全链路可观测 结构化 JSON 日志 + 敏感信息自动脱敏 + debug 模式逐条耗时追踪 + 日志上传 CLI,覆盖开发到生产的全场景调试需求

🌟 最终总结

@tencent-weixin/openclaw-weixin 插件展现了一个生产级渠道适配器的完整实现范式:用 ~2500 行 TypeScript 代码,完成了扫码登录、长轮询监听、消息标准化、鉴权路由、AI 调度、媒体加密传输、多账号管理、容错恢复、调试诊断等全套能力。其清晰的模块化设计、对边界情况的细致处理、以及对历史兼容性的尊重,使其成为一个值得深入学习的工程典范。


📝 本文基于 @tencent-weixin/openclaw-weixin@1.0.2 源码分析 | 生成于 2026-03-22

「真诚赞赏,手留余香」

爱折腾的工程师

真诚赞赏,手留余香

使用微信扫描二维码完成支付