Node 适合做微信机器人网关:回调是 HTTP,发送也是 HTTP,事件驱动和队列都顺手。个人号没有官方 Node SDK 时,自己包一层 client 即可。这篇用 Express 给出可落地的接入方式。

结构

src/
  channel.js    // 登录状态、发文本
  webhook.js    // Express 路由
  bot.js        // 规则
  store.js      // 去重与会话
  worker.js     // 消费队列

用 TypeScript 更好,但教程用普通 JS,方便复制。生产建议把 event 类型写死,避免 msg.xxx 满天飞。

Webhook:读原始 body

验签如果按原始字节计算,必须在 express.json() 之前保留 raw。常见写法:

app.use('/hooks/wechat/message', express.raw({ type: '*/*' }));

app.post('/hooks/wechat/message', async (req, res) => {
  const token = req.get('x-token');
  if (token !== process.env.CALLBACK_TOKEN) {
    return res.status(401).end();
  }
  const raw = req.body.toString('utf8');
  const event = parseEvent(raw);
  if (!event || store.seen(event.accountId, event.msgId)) {
    return res.status(200).json({ ok: true });
  }
  await store.saveRaw(event.accountId, event.msgId, raw);
  await queue.add(event);
  return res.status(200).json({ ok: true });
});

立刻 200。queue.add 用 BullMQ / Redis,单机 demo 可以用内存数组,进程一重启会丢,只适合本地。

发送封装

async function sendText({ accountId, to, text, requestId }) {
  const online = await channel.isOnline(accountId);
  if (!online) {
    return { ok: false, code: 'offline' };
  }
  const resp = await fetch(`${process.env.CHANNEL_BASE_URL}/message/text`, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      authorization: `Bearer ${process.env.CHANNEL_TOKEN}`,
    },
    body: JSON.stringify({ accountId, to, content: text, requestId }),
  });
  const data = await resp.json().catch(() => ({}));
  return { ok: resp.ok, status: resp.status, data };
}

URL 为示意。错误码要映射成你们自己的:offlinenot_friendrate_limitbad_params。业务层不要只拿到 false

规则与会话锁

function reply(event, session) {
  if (session.humanTaken) return null;
  if (event.isGroup) return null;
  const text = (event.text || '').trim();
  if (text === '人工') {
    session.takeHuman();
    return '已转接,请稍候';
  }
  if (text.includes('营业时间')) {
    return '每天 10:00–21:00,以门店当日安排为准';
  }
  return '回复「营业时间」或「人工」';
}

Node 里特别容易把会话存在内存 Map 里。多进程 / 多副本会串。会话和去重从第一天就进 Redis 或数据库。

Worker

async function handle(event) {
  const session = await store.getSession(event.accountId, event.from);
  const text = reply(event, session);
  if (!text) return;
  await sendText({
    accountId: event.accountId,
    to: event.from,
    text,
    requestId: `out-${event.msgId}`,
  });
}

同一好友的出站用队列按 key 串行(例如 BullMQ jobId + group),比在代码里 setTimeout 可靠。Serverless(云函数)尤其不要 sleep 频控,函数一结就没了。

本地开发注意

  • 回调要公网 HTTPS。本地用内网穿透,域名固定,不要每次换 URL 还去通道后台改

  • Node 默认不要把未捕获异常吞掉,worker 里 try/catch 并打 msgid

  • fetch 记得设超时(AbortController),通道卡住时不要占满事件循环

联调顺序:在线 → 打 webhook → 去重 → 发出文本 → 点“人工”后不再回。

小结

Node 微信机器人开发的关键,是 raw 回调、快速应答、Redis 会话、按账号串行发送。Express 只是入口。先把私聊文本和转人工做稳,图片与群消息作为事件类型扩展,而不是另起一个服务。通道字段以实际对接文档为准,Node 侧用一层 client 隔开即可。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐