短信验证码转发场景中,常用的通知渠道有 Bark、飞书、企业微信、钉钉。但这些要么需要额外 App,要么依赖企业账号体系。一个更直觉的需求是——直接转发到微信个人号。微信近期开放了 ilinkai Bot 接口,理论上支持通过 HTTP API 向个人微信发送消息。
项目基于 Cloudflare Worker 构建,是一个无服务器 SMS 转发网关:
手机收到短信 → HTTP POST → Cloudflare Worker(鉴权 → 去重 → 限流)→ 并行推送
↓
┌────────┬────────┬────────┬────────┬────────┐
│ Bark │ 飞书 │企业微信│ 钉钉 │ 微信 │
│ Push │Webhook │Webhook │Webhook │ilinkai │
└────────┴────────┴────────┴────────┴────────┘
↓
┌──────────────────┐
│ Armbian 服务器 │
│ 保活脚本(systemd)│
│ 长轮询保持在线 │
└──────────────────┘
微信 ilinkai 作为第五个通知渠道接入,与其他渠道完全并行。但由于 ilinkai 的 token 机制,需要一个额外的常驻进程来保活——这是本方案与其他渠道最大的区别。
ilinkai 是微信提供的 Bot API,核心接口:
| 接口方法 | 用途 |
|---|---|
/ilink/bot/get_bot_qrcode |
GET 获取登录二维码 |
/ilink/bot/get_qrcode_status |
GET 轮询扫码状态,获取 token |
/ilink/bot/getupdates |
POST 长轮询接收消息(服务端 hold 35 秒) |
/ilink/bot/sendmessage |
POST 发送消息 |
每次请求携带以下 Headers:
Content-Type: application/json
AuthorizationType: ilink_bot_token
X-WECHAT-UIN: <base64(random_uint32)>
Authorization: Bearer <bot_token>
API 文档中 qrcode_img_content 字段名容易让人以为是 base64 编码的图片数据。实际返回的是一个 URL 链接:
{
"qrcode": "683bfcab...",
"qrcode_img_content": "https://liteapp.weixin.qq.com/q/xxxx?qrcode=xxx&bot_type=3"
}
直接在浏览器打开这个 URL 扫码即可。
ilinkai 的 sendmessage 接口有一个反直觉的行为:即使 token 过期或参数错误,也可能返回 HTTP 200。
{} — 可能成功,也可能失败{"ret":-2} — token 过期{"ret":-14} — session 过期不能仅依赖 HTTP 状态码判断,必须检查响应体中的 ret 字段。
这是最大的坑。扫码获取的 bot_token 在短时间不活跃后就会失效。
根因:ilinkai 的设计是为对话式 Bot 服务的,客户端需要通过 getupdates 长轮询持续保持连接。如果没有持续的轮询请求,服务端会判定 session 离线,token 随即失效。
而 Cloudflare Worker 是请求驱动的无状态服务,两次 SMS 推送之间可能间隔数小时,期间没有任何请求维持会话。
这是调试时间最长的问题。消息发送后 HTTP 200 返回 {},看起来成功了,但微信上始终收不到消息。
通过反向工程微信官方的 OpenClaw 插件(@tencent-weixin/openclaw-weixin),发现 sendmessage 有几个文档中未明确说明但实际必需的字段和 Headers:
必需的消息体字段:
{
"msg": {
"from_user_id": "",
"to_user_id": "xxx@im.wechat",
"client_id": "unique-id-per-message",
"message_type": 2,
"message_state": 2,
"item_list": [...]
},
"base_info": {
"channel_version": "2.1.1"
}
}
from_user_id: ""——必须存在,即使为空字符串client_id——每条消息的唯一标识base_info——请求元数据必需的额外 Headers:
iLink-App-Id: bot
iLink-App-ClientVersion: 131329
缺少以上任何一项,接口都会静默返回 200 + {} 但不实际投递消息。没有任何错误提示。
直接 npm pack 下载 OpenClaw 插件的 npm 包,解包后阅读 TypeScript 源码:
npm pack @tencent-weixin/openclaw-weixin
tar xzf tencent-weixin-openclaw-weixin-2.1.1.tgz
关键文件:
- src/messaging/send.ts——发送消息的完整实现
- src/api/api.ts——HTTP 请求封装,包含所有 Headers
- src/api/types.ts——消息类型定义
- src/monitor/monitor.ts——长轮询保活循环
- src/api/session-guard.ts——session 过期处理
export async function sendWeixinNotification(env, title, content, device, code) {
const botToken = env.WEIXIN_BOT_TOKEN;
const targetUser = env.WEIXIN_TARGET_USER;
if (!botToken || !targetUser) {
return { success: false, error: 'Not configured' };
}
const text = buildWeixinText(title, content, device, code);
const uin = btoa(String(Math.floor(Math.random() * 4294967296)));
const clientId = `sms-forwarder-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
const payload = {
msg: {
from_user_id: '',
to_user_id: targetUser,
client_id: clientId,
message_type: 2, // BOT 消息
message_state: 2, // FINISH 状态
item_list: [{ type: 1, text_item: { text } }],
},
base_info: { channel_version: '2.1.1' },
};
const baseUrl = env.WEIXIN_BASE_URL || 'https://ilinkai.weixin.qq.com';
const response = await fetch(`${baseUrl}/ilink/bot/sendmessage`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'AuthorizationType': 'ilink_bot_token',
'X-WECHAT-UIN': uin,
'Authorization': `Bearer ${botToken}`,
'iLink-App-Id': 'bot',
'iLink-App-ClientVersion': '131329',
},
body: JSON.stringify(payload),
});
const result = await safeJson(response);
if (response.ok && (result.ret === undefined || result.ret === 0)) {
return { success: true };
}
return { success: false, error: `ret=${result.ret}` };
}
由于 Cloudflare Worker 无法运行常驻进程,需要在一台服务器上部署保活脚本。脚本逻辑很简单——无限循环调用 getupdates,模拟客户端持续在线:
// weixin-keepalive.mjs(核心逻辑简化版)
import crypto from 'node:crypto';
const BOT_TOKEN = process.env.WEIXIN_BOT_TOKEN;
const BASE_URL = 'https://ilinkai.weixin.qq.com';
while (true) {
try {
const res = await fetch(`${BASE_URL}/ilink/bot/getupdates`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'AuthorizationType': 'ilink_bot_token',
'X-WECHAT-UIN': randomUin(),
'Authorization': `Bearer ${BOT_TOKEN}`,
},
body: JSON.stringify({
get_updates_buf: cursor,
base_info: { channel_version: '1.0.2' },
}),
signal: AbortSignal.timeout(40000),
});
const data = await res.json();
// 更新游标
if (data.get_updates_buf) cursor = data.get_updates_buf;
// session 过期处理
if (data.errcode === -14 || data.ret === -14) {
console.error('Session 过期,需要重新扫码');
await sleep(3600000); // 暂停 1 小时
}
} catch (err) {
if (err.name === 'AbortError') continue; // 长轮询超时是正常的
await sleep(5000); // 网络错误重试
}
}
完整脚本还包括:游标持久化到文件(重启不丢失)、连续失败退避、优雅退出信号处理、运行统计等。
运行配置脚本:
node scripts/weixin-setup.mjs
脚本会输出一个 URL,在浏览器中打开用微信扫码,然后用目标微信号给 Bot 发一条消息。脚本自动获取:
WEIXIN_BOT_TOKEN——Bot 登录凭证WEIXIN_TARGET_USER——接收通知的微信用户 ID(格式 xxx@im.wechat)将 scripts/weixin-keepalive.mjs 上传到服务器:
mkdir -p /opt/weixin-keepalive
scp scripts/weixin-keepalive.mjs user@server:/opt/weixin-keepalive/
创建 systemd 服务:
# /etc/systemd/system/weixin-keepalive.service
[Unit]
Description=WeChat ilinkai Bot Keepalive
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
Environment=WEIXIN_BOT_TOKEN=<your_token>
Environment=WEIXIN_STATE_FILE=/opt/weixin-keepalive/state.json
ExecStart=/usr/bin/node /opt/weixin-keepalive/weixin-keepalive.mjs
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
启动并设置开机自启:
systemctl daemon-reload
systemctl enable weixin-keepalive
systemctl start weixin-keepalive
查看运行状态:
systemctl status weixin-keepalive
journalctl -u weixin-keepalive -f
正常运行时日志类似:
微信保活脚本启动
BASE_URL: https://ilinkai.weixin.qq.com
TOKEN: ...
运行正常 | 轮询: 100 | 错误: 0
在 Cloudflare 控制台 → Workers & Pages → 你的 Worker → Settings → Variables and Secrets,添加:
| 类型 | 变量名 | 值 |
|---|---|---|
| Secret | WEIXIN_BOT_TOKEN | 第一步获取的 token |
| Secret | WEIXIN_TARGET_USER | 第一步获取的 user ID |
保存后 Worker 自动重新部署。
curl -X POST https://your-worker.workers.dev/api/sms/forward \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your_api_token>" \
-d '{"content": "您的验证码是 123456,10分钟内有效", "device": "test"}'
预期返回:
{
"success": true,
"message": "forwarded",
"code": "123456",
"feishu": true,
"bark": 1,
"weixin": true
}
同时微信收到推送通知。
Token 是扫码登录时微信服务端生成的,存储在 Worker secrets 和 systemd 环境变量中。服务器重启后 systemd 自动拉起保活脚本,从 state.json 恢复游标继续轮询。
重新扫码后需要同时更新服务器的 systemd 环境变量和 Cloudflare 的 Worker Secret。
脚本非常轻量:
适合跑在树莓派、Armbian 盒子等低功耗设备上。
项目状态:
- 接口集成已完成
- 单向推送验证通过
- Worker 部署正常运行
- Token 保活已部署,运行稳定
- 长期稳定性观察中
微信个人号接收 SMS 转发在技术上完全可行,但实现过程远比其他渠道复杂。核心难点不在于接口调用本身,而在于:
如果你的通知需求已经被 Bark/飞书等渠道满足,没有必要折腾微信渠道。但如果你确实需要推送到微信——希望这篇文章能帮你少走弯路。