微信插件 1.0.2 → 1.0.3 源码 Diff 深度分析

> 包名: @tencent-weixin/openclaw-weixin

> 版本对比: 1.0.2 → 1.0.3

> 变更文件: 11 个

> 分析方法: npm pack 下载两个版本 tgz,解压后逐文件 diff -u

一句话总结

1.0.3 的核心改动是让微信插件从"只能回复"变成"能主动发消息"——通过 contextToken 持久化 + 容错降级,解决了 cron 定时任务和 gateway 重启后消息发不出去的问题。

背景:什么是 contextToken?

contextToken 是微信 iLink Bot 协议中的会话凭证。微信的防骚扰机制要求:

这与 Telegram/Discord 完全不同:

平台首次主动发消息对方聊过后定时发
**Telegram**✅ 知道 chat_id 就行
**Discord**✅ 知道 user_id 就行
**QQ Bot**
**微信 iLink**❌ 必须对方先说话✅(1.0.3 持久化 token 后)

变更文件清单

文件改动类型
`package.json`版本号
`src/auth/accounts.ts`🆕 stale 账号清理
`src/auth/login-qr.ts`二维码提示改中文
`src/channel.ts`🆕 多账号发送路由
`src/log-upload.ts`路径跨平台兼容
`src/messaging/error-notice.ts`错误通知容错
`src/messaging/inbound.ts`🔑 token 持久化
`src/messaging/process-message.ts`回复逻辑简化
`src/messaging/send.ts`🔑 发送容错降级
`src/util/logger.ts`路径跨平台兼容
`src/util/redact.ts`🔒 日志脱敏增强

改动一:contextToken 持久化到磁盘(核心)

问题:1.0.2 的 contextToken 只存在内存 Map 中,gateway 重启就全丢了。

1.0.2 实现


// 纯内存,重启归零
const contextTokenStore = new Map<string, string>();

1.0.3 实现:每次收到新 token,同时写一份 JSON 到磁盘:


// 持久化路径: accounts/{accountId}.context-tokens.json
// 内容格式: { "用户A的userId": "token_xxx", "用户B的userId": "token_yyy" }
function persistContextTokens(accountId: string): void {
  const prefix = `${accountId}:`;
  const tokens: Record<string, string> = {};
  for (const [k, v] of contextTokenStore) {
    if (k.startsWith(prefix)) {
      tokens[k.slice(prefix.length)] = v;
    }
  }
  fs.writeFileSync(filePath, JSON.stringify(tokens, null, 0), "utf-8");
}

Gateway 启动时调用 restoreContextTokens() 从磁盘恢复到内存 Map。

新增函数

文件: src/messaging/inbound.ts

改动二:发送时 contextToken 容错降级

问题:1.0.2 在发送消息时如果没有 contextToken,直接 throw Error 拒绝发送。这导致 cron 定时任务、主动推送等场景完全无法工作。

1.0.2 实现(四个发送函数全部如此):


if (!opts.contextToken) {
  logger.error(`sendMessageWeixin: contextToken missing, refusing to send to=${to}`);
  throw new Error("sendMessageWeixin: contextToken is required");  // 💥 直接炸
}

1.0.3 实现


if (!opts.contextToken) {
  logger.warn(`sendMessageWeixin: contextToken missing for to=${to}, sending without context`);
  // ⚠️ 不再 throw,继续尝试发送
}

影响的函数(全部从 throw 改为 warn):

错误通知也同步改了error-notice.ts):


// 1.0.2: 没 token 就不发错误通知
if (!params.contextToken) {
  logger.warn(`no contextToken, cannot notify user`);
  return;  // 静默吞掉
}

// 1.0.3: 没 token 也尝试发
if (!params.contextToken) {
  logger.warn(`no contextToken, sending without context`);
  // 继续执行
}

文件: src/messaging/send.ts, src/messaging/error-notice.ts

改动三:清理重复账号(Stale Account Cleanup)

问题:同一个微信号重复扫码登录会产生多个 account 记录,导致 contextToken 匹配歧义。

1.0.3 新增 clearStaleAccountsForUserId()


export function clearStaleAccountsForUserId(
  currentAccountId: string,
  userId: string,
  onClearContextTokens?: (accountId: string) => void,
): void {
  if (!userId) return;
  const allIds = listIndexedWeixinAccountIds();
  for (const id of allIds) {
    if (id === currentAccountId) continue;
    const data = loadWeixinAccount(id);
    if (data?.userId?.trim() === userId) {
      // 发现同一微信号的旧账号,清除
      onClearContextTokens?.(id);
      clearWeixinAccount(id);
      unregisterWeixinAccountId(id);
    }
  }
}

配套新增 unregisterWeixinAccountId() 从索引文件中移除旧账号。

文件: src/auth/accounts.ts

改动四:多账号发送路由

问题:cron 发消息时没有指定 accountId(因为 cron 不知道用哪个微信账号发),1.0.2 无法处理这种情况。

1.0.3 新增 resolveOutboundAccountId()


发送消息(无 accountId)
  ↓
单账号?→ 直接用它
  ↓
多账号?→ 通过 contextToken 反查:哪个账号跟这个收件人聊过天?→ 用那个
  ↓
都匹配不到?→ 抛出描述性错误

文件: src/channel.ts

改动五:日志敏感信息脱敏

问题:1.0.2 打日志时,HTTP body 中的 token 等敏感信息是明文的:


{"context_token":"sk-abc123xyz真实token","bot_token":"secret_456"}

1.0.3 改进:用正则匹配敏感字段名,自动替换值为


const SENSITIVE_FIELDS = /\b(context_token|bot_token|token|authorization|Authorization)\b/;

export function redactBody(body: string | undefined, maxLen = DEFAULT_BODY_MAX_LEN): string {
  if (!body) return "(empty)";
  const redacted = body.replace(
    /"(context_token|bot_token|token|authorization|Authorization)"\s*:\s*"[^"]*"/g,
    '"$1":"<redacted>"',
  );
  if (redacted.length <= maxLen) return redacted;
  return `${redacted.slice(0, maxLen)}…(truncated, totalLen=${redacted.length})`;
}

效果:


{"context_token":"<redacted>","bot_token":"<redacted>"}

为什么重要:日志文件经常被收集到监控系统(Datadog、Loki 等),或排查问题时直接分享给他人。明文 token 躺在日志里 = 把钥匙贴在公告栏上。

文件: src/util/redact.ts

改动六:其他小改

路径跨平台兼容

/tmp/openclaw 硬编码路径全部改为 resolvePreferredOpenClawTmpDir(),适配不同操作系统。

影响文件: src/util/logger.ts, src/log-upload.ts, src/channel.ts

二维码登录提示改中文


// 1.0.2
process.stdout.write(`QR Code URL: ${qrResponse.qrcode_img_content}\n`);

// 1.0.3
process.stdout.write(`二维码未加载成功,请用浏览器打开以下链接扫码:\n`);
process.stdout.write(`${qrResponse.qrcode_img_content}\n`);

文件: src/auth/login-qr.ts

发送成功日志提级

sendImageMessageWeixinsendVideoMessageWeixin 的成功日志从 debug 提升为 info,方便排查。

回复流式设置

process-message.ts 中回复时显式设置 disableBlockStreaming: false

错误处理简化

process-message.ts 移除了 contextToken is required 的特殊分支(因为 send 层不再 throw 这个错误了)。

组合效果:完整流程


1. 用户跟 Bot 聊天
   → 微信附带 contextToken
   → 存内存 Map + 写磁盘 JSON

2. 三天后 cron 定时任务触发
   → resolveOutboundAccountId() 找到正确账号
   → 从磁盘读到该用户的 contextToken
   → 正常发送 ✅

3. Gateway 重启
   → restoreContextTokens() 从磁盘恢复所有 token
   → 不影响后续发送 ✅

4. 万一 token 过期或丢失
   → 不 crash,降级为 warn
   → 尝试发送,能发就发 ✅

5. 同一微信号重新扫码登录
   → clearStaleAccountsForUserId() 清除旧账号
   → 不会出现 token 匹配歧义 ✅

升级建议

建议升级。1.0.3 解决的都是实际使用中会遇到的问题:

升级命令:


cd ~/.openclaw/extensions/openclaw-weixin
npm install @tencent-weixin/[email protected]
openclaw gateway restart

评分

维度评分(/10)
问题修复价值9.0 — contextToken 持久化解决了真实痛点
代码质量8.0 — 改动清晰,向后兼容,无 breaking change
安全改进7.5 — 日志脱敏是该做的基本功
文档/提示6.0 — 二维码中文化不错,但没有 CHANGELOG
**综合****8.0**

报告基于 npm pack 下载的 1.0.2 与 1.0.3 tgz 包逐文件 diff 生成 | 2026-03-24

来源: npm @tencent-weixin/openclaw-weixin