前言
微信作为国内用户基数最大的即时通讯平台,天然具备做消息自动化的土壤:客服自动回复、群管理机器人、消息转发提醒……这类需求在企业和个人开发者中非常普遍。相比 Python,Node.js 凭借事件驱动的异步模型和 Express/Fastify 等轻量 Web 框架,在处理 Webhook 回调时有天然优势——一个进程既能主动发消息,又能实时监听入站回调,代码结构也更自然。
本文面向有一定 Node.js 基础的开发者,完整演示如何搭建一个"发消息 + 收回调"的微信机器人:从环境准备、扫码登录,到主动发送文本/图片,再到配置回调服务器处理接收到的消息,每个环节都会给出可运行的示例代码,并附上防封频率控制的实践建议。
一、整体架构与工作流程
在动手写代码之前,先梳理清楚机器人的运行逻辑,能减少很多调试时间。
1.1 核心流程
扫码登录 → 获取 appId
↓
主动发消息(调 HTTP 接口)
↓
平台把别人发来的消息 POST 到你的回调地址
↓
Node.js 回调服务器解析 → 业务逻辑处理
整个链路分两条线:下行(你主动发消息,走 HTTP POST 到接口平台)和上行(别人发消息给你,平台 Webhook 回调到你的服务器)。两条线相互独立,但共用同一个 appId。
1.2 技术选型
| 职责 | 推荐方案 |
|---|---|
| HTTP 请求(发消息) | axios |
| 回调服务器 | express |
| 进程守护 | pm2 |
| 环境变量管理 | dotenv |
依赖都很轻量,整个项目不需要复杂的构建工具。
二、环境准备
2.1 初始化项目
bashmkdir wechat-bot && cd wechat-bot
npm init -y
npm install axios express dotenv
npm install -D nodemon
2.2 目录结构
wechat-bot/
├── .env # 配置(不提交 git)
├── config.js # 统一读取配置
├── api.js # 封装接口调用
├── server.js # 回调服务器
├── bot.js # 业务逻辑入口
└── package.json
2.3 配置文件
.env:
iniBASE_URL=https://你的接口域名 # 注册后在官方文档获取
TOKEN=你的Token
APP_ID=你的appId # 登录后获取
CALLBACK_PORT=3000
config.js:
javascriptrequire('dotenv').config();
module.exports = {
BASE_URL: process.env.BASE_URL,
TOKEN: process.env.TOKEN,
APP_ID: process.env.APP_ID,
PORT: parseInt(process.env.CALLBACK_PORT) || 3000,
};
代码为示例,具体接口地址、字段名称以官方文档为准。
注意事项:.env 文件包含 Token 等敏感信息,一定要加入 .gitignore,不要提交到版本控制系统。Token 泄露会导致账号被滥用,建议定期在后台重置 Token。
三、扫码登录获取 appId
appId 是整个机器人的身份标识,每次登录后获得,后续所有接口都要带上它。
3.1 获取登录二维码
api.js(部分):
javascriptconst axios = require('axios');
const { BASE_URL, TOKEN } = require('./config');
const http = axios.create({
baseURL: BASE_URL,
headers: { token: TOKEN }, // 鉴权字段名以官方文档为准
timeout: 10000,
});
/**
* 获取登录二维码
* @returns {{ qrUrl: string, uuid: string }}
*/
async function getLoginQrCode() {
const res = await http.post('/login/getLoginQrCode');
if (res.data.ret !== 200) throw new Error(res.data.msg);
return res.data.data; // { qrUrl, uuid }
}
/**
* 轮询检查登录状态
* @param {string} uuid
*/
async function checkLogin(uuid) {
const res = await http.post('/login/checkLogin', { uuid });
if (res.data.ret !== 200) throw new Error(res.data.msg);
return res.data.data; // { loginState, appId, ... }
}
module.exports = { http, getLoginQrCode, checkLogin };
3.2 登录流程脚本
javascript// login.js —— 一次性运行,把 appId 记录到 .env
const qrcode = require('qrcode-terminal');
const { getLoginQrCode, checkLogin } = require('./api');
(async () => {
console.log('正在获取二维码...');
const { qrUrl, uuid } = await getLoginQrCode();
// 在终端打印可扫描的二维码
qrcode.generate(qrUrl, { small: true });
console.log('请用微信扫码,等待登录...');
// 每 3 秒轮询一次
const timer = setInterval(async () => {
try {
const result = await checkLogin(uuid);
if (result.loginState === 1) {
console.log('登录成功,appId:', result.appId);
console.log('请将 appId 写入 .env 的 APP_ID 字段');
clearInterval(timer);
}
} catch (e) {
console.error('检查登录状态出错:', e.message);
}
}, 3000);
})();
运行 node login.js,扫码后终端打印 appId,手动写入 .env。
注意事项:登录二维码一般有有效期(通常 2~5 分钟),过期需要重新获取。如果网络条件较差,可以适当把轮询间隔从 3 秒调整到 5 秒,避免因频繁请求被接口限流。扫码完成后不要立即大量调用发消息接口,新账号建议先正常挂机使用 2~3 天,让账号"暖机"后再接入自动化逻辑。
四、主动发消息
登录后就可以调接口发消息了。常见的消息类型:文本、图片、文件、链接卡片。
4.1 封装发消息方法
继续在 api.js 中添加:
javascriptconst { APP_ID } = require('./config');
/**
* 发送文本消息
* @param {string} toWxid 收件人微信 ID(或群 ID)
* @param {string} content 消息内容
* @param {string[]} [ats] 群聊 @ 列表(可选)
*/
async function sendText(toWxid, content, ats = []) {
const res = await http.post('/message/postText', {
appId: APP_ID,
toWxid,
content,
ats,
});
if (res.data.ret !== 200) throw new Error(res.data.msg);
return res.data.data;
}
/**
* 发送图片消息
* @param {string} toWxid
* @param {string} imgUrl 公网可访问的图片地址
*/
async function sendImage(toWxid, imgUrl) {
const res = await http.post('/message/postImage', {
appId: APP_ID,
toWxid,
imgUrl,
});
if (res.data.ret !== 200) throw new Error(res.data.msg);
return res.data.data;
}
/**
* 发送链接卡片
*/
async function sendLink(toWxid, { title, desc, linkUrl, thumbUrl }) {
const res = await http.post('/message/postLink', {
appId: APP_ID,
toWxid,
title,
desc,
linkUrl,
thumbUrl,
});
if (res.data.ret !== 200) throw new Error(res.data.msg);
return res.data.data;
}
module.exports = { http, getLoginQrCode, checkLogin, sendText, sendImage, sendLink };
4.2 使用示例
javascript// demo-send.js
const { sendText, sendImage } = require('./api');
(async () => {
// 给指定微信号发文本
await sendText('friend_wxid_xxxxx', '你好,这是机器人发出的消息');
// 发图片
await sendImage('friend_wxid_xxxxx', 'https://example.com/photo.jpg');
console.log('发送完成');
})();
4.3 频率控制(必读)
批量发消息是最容易触发风控的操作,务必遵守以下节奏:
| 场景 | 建议频率 |
|---|---|
| 加好友 | 24 小时内 5–15 个,每 2 小时不超过 5 个 |
| 主动给好友发消息 | 加随机间隔(建议 3–10 秒) |
| 群发消息 | 每条间隔 5–15 秒,避免连续相同内容 |
| 新账号 | 扫码登录后在线至少 3 天再调接口 |
用 delay 函数控制节奏:
javascriptconst delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function batchSend(targets, content) {
for (const wxid of targets) {
await sendText(wxid, content);
// 随机等待 3~8 秒
const wait = 3000 + Math.random() * 5000;
await delay(wait);
}
}
注意事项:发送内容上也要注意多样性,避免完全相同的文案反复群发,容易被识别为营销行为。可以在消息中插入动态变量(如称呼、时间等),让每条消息在文字层面有差异。此外,发送失败(ret 非 200)时不要立即重试,建议记录失败队列,等待一段时间后再统一补发。
五、收消息:配置回调服务器
收消息是机器人的"耳朵"。平台会把别人发来的消息、好友请求等事件,以 HTTP POST 的形式推送到你预先设置好的回调地址。
5.1 启动 Express 回调服务器
server.js:
javascriptconst express = require('express');
const { PORT } = require('./config');
const app = express();
app.use(express.json());
// 挂载回调路由(模块化分离)
const callbackRouter = require('./callback');
app.use('/wechat/callback', callbackRouter);
app.listen(PORT, () => {
console.log(`回调服务器已启动,监听端口 ${PORT}`);
});
module.exports = app;
5.2 处理回调消息
callback.js:
javascriptconst express = require('express');
const router = express.Router();
const { sendText } = require('./api');
/**
* 平台推送的消息结构(示例,以官方文档为准):
* {
* appId: "你的appId",
* fromWxid: "发件人 wxid",
* toWxid: "收件人 wxid(即机器人自己)",
* type: 1, // 1=文本 3=图片 43=视频 49=链接 ...
* content: "消息内容",
* msgId: "消息ID",
* createTime: 1700000000
* }
*/
router.post('/', async (req, res) => {
// 必须立即返回 200,否则平台会重复推送
res.status(200).json({ ret: 200, msg: 'ok' });
const msg = req.body;
console.log('收到消息:', JSON.stringify(msg));
try {
await handleMessage(msg);
} catch (err) {
console.error('处理消息出错:', err.message);
}
});
async function handleMessage(msg) {
const { type, fromWxid, content } = msg;
// 只处理文本消息
if (type !== 1) return;
// 关键词自动回复
if (content.includes('帮助') || content === '?') {
await sendText(fromWxid, '可用指令:\n帮助 —— 查看此菜单\n时间 —— 查询当前时间');
return;
}
if (content === '时间') {
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
await sendText(fromWxid, `当前北京时间:${now}`);
return;
}
// 默认回复(可注释掉避免无谓回复)
// await sendText(fromWxid, `收到你的消息:${content}`);
}
module.exports = router;
关键点:必须在处理业务逻辑之前先响应 200,否则平台会超时重试,导致同一条消息被处理多次。
5.3 设置回调地址
回调服务器必须部署在公网可访问的地址(本地调试可用 ngrok/frp 做内网穿透)。
javascript// set-callback.js —— 运行一次即可
const { http } = require('./api');
const { APP_ID } = require('./config');
(async () => {
const res = await http.post('/login/setCallback', {
appId: APP_ID,
callbackUrl: 'https://your-server.com/wechat/callback', // 替换为你的公网地址
});
console.log(res.data.ret === 200 ? '回调地址设置成功' : res.data.msg);
})();
注意事项:回调地址必须是 HTTPS,很多平台不接受 HTTP 回调。如果使用 ngrok 调试,每次重启 ngrok 都会产生新的域名,需要重新调用 set-callback.js 更新。生产环境建议使用固定域名并配置 SSL 证书,可以用 Let's Encrypt 免费申请。另外,回调服务器应当对请求来源做基本校验(比如验证请求头中的签名字段),防止外部伪造回调请求触发业务逻辑。
六、把回调与发消息整合到一起
有了发消息和收消息两个模块后,把它们组合成一个完整的机器人进程:
bot.js:
javascriptrequire('./server'); // 启动回调服务器
const { checkOnline } = require('./api');
const { APP_ID } = require('./config');
// 定期检查在线状态,断线告警
setInterval(async () => {
try {
const res = await checkOnline(APP_ID);
if (!res.isOnline) {
console.warn('[警告] 账号已掉线,请重新扫码登录');
}
} catch (e) {
console.error('心跳检测失败:', e.message);
}
}, 60 * 1000); // 每分钟检查一次
console.log('微信机器人已启动');
用 pm2 守护进程:
bashnpm install -g pm2
pm2 start bot.js --name wechat-bot
pm2 save
pm2 startup # 设置开机自启
整合注意事项:心跳检测发现账号掉线后,当前代码只会打印警告,实际生产场景建议接入钉钉/邮件告警,以便及时感知异常。pm2 的日志默认存放在 ~/.pm2/logs/ 目录,可以通过 pm2 logs wechat-bot 实时查看,出现问题时优先检查日志里的报错信息。
七、消息类型扩展
文本只是最基础的消息类型,实际场景中还需要处理图片、文件、语音等。下面列出常见类型的处理思路:
7.1 接收图片并下载
javascript// 在 handleMessage 中
if (type === 3) {
// type=3 为图片消息,content 里通常包含图片信息
// 需要调用下载接口获取图片内容,以官方文档为准
console.log('收到图片消息,msgId:', msg.msgId);
// 建议用队列处理,不要在回调里同步下载
}
7.2 群聊 @ 识别
javascript// 群消息中,fromWxid 通常是"群ID@chatroom"
function isGroupMsg(fromWxid) {
return fromWxid.endsWith('@chatroom');
}
// 判断是否 @ 了机器人(content 里通常含 @昵称)
function isMentioned(content, botNickname) {
return content.includes(`@${botNickname}`);
}
7.3 消息去重
网络不稳定时,平台可能重发同一条回调:
javascriptconst processedMsgIds = new Set();
async function handleMessage(msg) {
if (processedMsgIds.has(msg.msgId)) return; // 已处理
processedMsgIds.add(msg.msgId);
// 防止 Set 无限增长:超过 1000 条清空(简单方案)
if (processedMsgIds.size > 1000) processedMsgIds.clear();
// ... 正常业务处理
}
扩展注意事项:消息去重的 Set 是内存结构,重启进程后会清空,重启瞬间如果平台补发了旧消息可能出现重复处理。对于幂等性要求较高的业务(比如转账通知、工单创建),建议把 msgId 持久化到 Redis 或数据库,并设置合理的过期时间(如 24 小时)。图片、文件类消息的内容通常不直接放在回调体里,而是以媒体 ID 或 CDN 链接形式下发,下载时注意链接的有效期,及时转存到自己的存储服务。
八、常见问题排查
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 收不到回调消息 | 回调地址不可公网访问 | 检查服务器防火墙/端口,或用 ngrok 穿透 |
| 收不到回调消息 | setCallback 未调用或失败 | 重新运行 set-callback.js 确认返回 200 |
| 发消息失败 ret 非 200 | appId 过期或账号掉线 | 调用 checkOnline 确认在线状态 |
| 消息重复处理 | 回调响应超时导致重试 | 确保第一行就 res.status(200).json(...) |
| 接口频率报错 | 调用过于密集 | 加 delay 控制节奏,参考第四节频率表 |
| 新账号发消息失败 | 在线天数不足 | 扫码登录后保持在线至少 3 天再批量调用 |
对于需要托管 HTTP 接口、不想自己维护底层通信的场景,WechatApi 提供扫码登录、消息收发、好友与群管理等 REST 接口,HTTP 调用即可,可查阅其接口文档和 appId 获取流程。
总结
本文从项目初始化开始,完整实现了一个基于 Node.js 的微信机器人:用 axios 调 HTTP 接口主动发送文本和图片,用 Express 搭建回调服务器处理平台推送的消息,并整合成一个统一的机器人进程用 pm2 守护运行。频率控制、消息去重、群聊 @ 识别这些工程细节也都有覆盖。
开发过程中几个容易踩坑的地方值得再次强调:回调接口必须先返回 200 再执行业务逻辑、.env 文件不要提交 git、新账号先暖机再调接口、批量发送时务必加随机间隔。这些细节处理得好,机器人才能稳定长期运行而不触发风控。
实际落地时,建议先在测试号上充分验证业务逻辑,再对接生产账号,稳健操作比追求速度更重要。接口字段和行为细节以官方文档为准。
