1. ClawBot无法推送到微信的解决方案


作者ClawBot创建时间

问题

有人用微信ClawBot使用cron任务成功的吗? 在web里可以看到回复内容,但是微信没有收到消息。我尝试用openclaw主动发送消息给微信,告诉我openclaw-weixin主动发消息需要contextToken,这个 token 是微信对话上下文里的,必须要我先用微信发送消息给openclaw,openclaw才能回复我。 那这不能实现cron,不是瞎扯么

一、问题现象

刚跟机器人聊过,主动发消息有时是成功的 一重启 gateway,突然又不能发了 cron 配置看起来没毛病,但投递依然失败 常见报错:

sendWeixinOutbound: contextToken is required 如果你也碰到这个,很大概率就是同一个问题。

二、根因到底是什么

openclaw-weixin 在发送微信消息时,不是只靠:

  • to
  • accountId

就能发出去。

它还需要一个很关键的参数:

  • contextToken

这个 contextToken 是微信在入站消息里带过来的“当前会话上下文令牌”。回复消息时,需要把它原样带回去。

原来的问题 插件原本的逻辑大致是:

  • 收到微信消息
  • 从入站消息里拿到 context_token
  • 放进一个内存 Map
  • 后续发送时,再从这个 Map 里取出来用

问题就在第 3 步:

它只存在内存里,不落盘。

所以:

  • 进程活着时,可能还能发
  • gateway 一重启,内存清空
  • 之后主动发消息、cron 推送,就拿不到 token 了

这就是为什么它会表现成:

  • “有时候能发”
  • “重启后又不行”
  • “cron 像玄学一样”

三、修复思路

思路其实不复杂:

写入时

收到微信入站消息时:

  • 继续存内存
  • 顺手存一份到磁盘

读取时

发送微信消息时:

  • 先查内存
  • 内存没有就回退查磁盘缓存

这样就能覆盖三种情况:

  • 实时对话回复:走内存
  • gateway 重启后继续主动发:走磁盘
  • cron 定时推送:走磁盘或内存

四、我实际做的改动

1)新增一个持久化文件 新增:

src/storage/context-token.ts 负责把 token 存到: ~/.openclaw/openclaw-weixin/context-tokens/<accountId>.json 也就是说,每个账号一个 token 缓存文件,里面按用户 ID 存最近一次可用的 contextToken。

2)修改 src/messaging/inbound.ts 把 contextToken 的逻辑改成:

  • setContextToken():写内存 + 落盘
  • getContextToken():先查内存,查不到再查磁盘

这样就把“只存在进程内”的临时状态,变成了“可恢复”的状态。

五、修改方式

方式一、新增一个文件,修改一个文件 新增文件:

.openclaw/extensions/openclaw-weixin/src/storage/context-token.ts

import fs from "node:fs";
import path from "node:path";

import { resolveStateDir } from "./state-dir.js";

function resolveContextTokenDir(): string {
return path.join(resolveStateDir(), "openclaw-weixin", "context-tokens");
}

function resolveContextTokenPath(accountId: string): string {
return path.join(resolveContextTokenDir(), `${accountId}.json`);
}

type ContextTokenMap = Record<string, string>;

function readTokenMap(filePath: string): ContextTokenMap {
try {
if (!fs.existsSync(filePath)) return {};
const raw = fs.readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw) as Record<string, unknown>;
const out: ContextTokenMap = {};
for (const [k, v] of Object.entries(parsed)) {
if (typeof k === "string" && k && typeof v === "string" && v) out[k] = v;
}
return out;
} catch {
return {};
}
}

export function loadPersistedContextToken(accountId: string, userId: string): string | undefined {
const filePath = resolveContextTokenPath(accountId);
const map = readTokenMap(filePath);
return map[userId];
}

export function savePersistedContextToken(accountId: string, userId: string, token: string): void {
const dir = resolveContextTokenDir();
fs.mkdirSync(dir, { recursive: true });

const filePath = resolveContextTokenPath(accountId);
const current = readTokenMap(filePath);
current[userId] = token;

fs.writeFileSync(filePath, JSON.stringify(current, null, 2), "utf-8");
try {
fs.chmodSync(filePath, 0o600);
} catch {
// best-effort
}
}

修改文件: .openclaw/extensions/openclaw-weixin/src/messaging/inbound.ts

改动一:顶部增加引用:

import { loadPersistedContextToken, savePersistedContextToken } from "../storage/context-token.js";

改动二:修改 setContextToken()

export function setContextToken(accountId: string, userId: string, token: string): void {
const k = contextTokenKey(accountId, userId);
logger.debug(`setContextToken: key=${k}`);
contextTokenStore.set(k, token);
savePersistedContextToken(accountId, userId, token);
}

改动三:修改 getContextToken()

export function getContextToken(accountId: string, userId: string): string | undefined {
const k = contextTokenKey(accountId, userId);
const val = contextTokenStore.get(k);
if (val !== undefined) {
logger.debug(
`getContextToken: key=${k} found=true source=memory storeSize=${contextTokenStore.size}`,
);
return val;
}

const persisted = loadPersistedContextToken(accountId, userId);
if (persisted !== undefined) {
contextTokenStore.set(k, persisted);
logger.debug(
`getContextToken: key=${k} found=true source=disk storeSize=${contextTokenStore.size}`,
);
return persisted;
}

logger.debug(
`getContextToken: key=${k} found=false storeSize=${contextTokenStore.size}`,
);
return undefined;
}

六、后续

看哪的文档好像写说token有效期是24小时,大概就是这个。不知道有什么办法可以续时,各位佬再研究研究。。。