微信ClawBot Cron定时任务消息,无法推送到微信的解决方案
- 内容介绍
- 文章标签
- 相关推荐
有人用微信ClawBot使用cron任务成功的吗? 在web里可以看到回复内容,但是微信没有收到消息 [image] 我尝试用openclaw主动发送消息给微信,告诉我openclaw-weixin主动发消息需要contextToken,这个 token 是微信对话上下文里的,必须要我先用微信发送消息给openclaw,openclaw才能回复我。 那这不能实现cron,不是瞎扯么
有佬提到是可以的,大概原因应该是我重启了gateway
ST(@hst)佬 说: 一个context_token只能回复10条消息
试了一下,确实是…所以只能临时解决重启gateway后收不到cron消息的问题…
IMG_28651320×2868 497 KB
零、自己提问,自己解决。
在折腾 OpenClaw 的 openclaw-weixin 通道时,
- 在控制台里主动给微信发消息
- 用 cron 定时推送到微信
- gateway 重启后继续给上次聊过的人发消息
报错:
sendWeixinOutbound: contextToken is required
我这边把问题摸清、修好并跑通了。
根因不是
to写错,也不只是accountId没带,而是 Weixin 的主动发送依赖会话contextToken。这个 token 原本只在内存里,重启就丢,所以需要做一层持久化。
一、问题现象
- 刚跟机器人聊过,主动发消息有时是成功的
- 一重启 gateway,突然又不能发了
- cron 配置看起来没毛病,但投递依然失败
常见报错:
sendWeixinOutbound: contextToken is required
如果你也碰到这个,很大概率就是同一个问题。
二、根因到底是什么
openclaw-weixin 在发送微信消息时,不是只靠:
toaccountId
就能发出去。
它还需要一个很关键的参数:
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;
}
方式二、patch
openclaw-weixin-contexttoken-persistence.zip (1.4 KB)
使用方式
在 openclaw-weixin 插件源码目录 里应用它。
比如你的插件源码如果在:
~/.openclaw/extensions/openclaw-weixin
那就进入这个目录再打 patch。
用法 1:git apply
推荐用这个:
cd ~/.openclaw/extensions/openclaw-weixin
git apply ~/openclaw-weixin-contexttoken-persistence.patch
如果没有报错,说明 patch 已经成功应用。
用法 2:patch 命令
如果你不是 git 工作树,也可以试:
cd ~/.openclaw/extensions/openclaw-weixin
patch -p1 < ./openclaw-weixin-contexttoken-persistence.patch
打完patch
1、重启 gateway
openclaw gateway restart
2、微信openclaw随便发一条消息
这一步很关键。
因为重启后,插件要先收到一条新的微信入站消息,才能把新的 contextToken 写进磁盘缓存。
比如你在微信里给机器人发一句:
测试消息
生成出来的缓存文件类似这样
{
"XXXXXXX88888888XXXXXXXXXXXXX@im.wechat": "AABBAAAFAAABAAAA..."
}
六、后续
看哪的文档好像写说token有效期是24小时,大概就是这个。不知道有什么办法可以续时,各位佬再研究研究。。。
网友解答:--【壹】--:
谢谢大佬分享,暂时没用微信,观望下
--【贰】--:
我的是非常奇怪,无论如何定时任务都无法发到微信插件,即使我正跟微信插件在聊天中
按楼主文改了后,能生成了缓存,还是不行,不知道哪里出问题
--【叁】--:
感谢大佬了。
有人用微信ClawBot使用cron任务成功的吗? 在web里可以看到回复内容,但是微信没有收到消息 [image] 我尝试用openclaw主动发送消息给微信,告诉我openclaw-weixin主动发消息需要contextToken,这个 token 是微信对话上下文里的,必须要我先用微信发送消息给openclaw,openclaw才能回复我。 那这不能实现cron,不是瞎扯么
有佬提到是可以的,大概原因应该是我重启了gateway
ST(@hst)佬 说: 一个context_token只能回复10条消息
试了一下,确实是…所以只能临时解决重启gateway后收不到cron消息的问题…
IMG_28651320×2868 497 KB
零、自己提问,自己解决。
在折腾 OpenClaw 的 openclaw-weixin 通道时,
- 在控制台里主动给微信发消息
- 用 cron 定时推送到微信
- gateway 重启后继续给上次聊过的人发消息
报错:
sendWeixinOutbound: contextToken is required
我这边把问题摸清、修好并跑通了。
根因不是
to写错,也不只是accountId没带,而是 Weixin 的主动发送依赖会话contextToken。这个 token 原本只在内存里,重启就丢,所以需要做一层持久化。
一、问题现象
- 刚跟机器人聊过,主动发消息有时是成功的
- 一重启 gateway,突然又不能发了
- cron 配置看起来没毛病,但投递依然失败
常见报错:
sendWeixinOutbound: contextToken is required
如果你也碰到这个,很大概率就是同一个问题。
二、根因到底是什么
openclaw-weixin 在发送微信消息时,不是只靠:
toaccountId
就能发出去。
它还需要一个很关键的参数:
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;
}
方式二、patch
openclaw-weixin-contexttoken-persistence.zip (1.4 KB)
使用方式
在 openclaw-weixin 插件源码目录 里应用它。
比如你的插件源码如果在:
~/.openclaw/extensions/openclaw-weixin
那就进入这个目录再打 patch。
用法 1:git apply
推荐用这个:
cd ~/.openclaw/extensions/openclaw-weixin
git apply ~/openclaw-weixin-contexttoken-persistence.patch
如果没有报错,说明 patch 已经成功应用。
用法 2:patch 命令
如果你不是 git 工作树,也可以试:
cd ~/.openclaw/extensions/openclaw-weixin
patch -p1 < ./openclaw-weixin-contexttoken-persistence.patch
打完patch
1、重启 gateway
openclaw gateway restart
2、微信openclaw随便发一条消息
这一步很关键。
因为重启后,插件要先收到一条新的微信入站消息,才能把新的 contextToken 写进磁盘缓存。
比如你在微信里给机器人发一句:
测试消息
生成出来的缓存文件类似这样
{
"XXXXXXX88888888XXXXXXXXXXXXX@im.wechat": "AABBAAAFAAABAAAA..."
}
六、后续
看哪的文档好像写说token有效期是24小时,大概就是这个。不知道有什么办法可以续时,各位佬再研究研究。。。
网友解答:--【壹】--:
谢谢大佬分享,暂时没用微信,观望下
--【贰】--:
我的是非常奇怪,无论如何定时任务都无法发到微信插件,即使我正跟微信插件在聊天中
按楼主文改了后,能生成了缓存,还是不行,不知道哪里出问题
--【叁】--:
感谢大佬了。

