Node.js SDK 是功能最丰富的实现。77 个测试,零运行时依赖。需要 Node.js ≥ 22。
安装
npm install @wechatbot/wechatbot
快速开始
import { WeChatBot } from '@wechatbot/wechatbot' const bot = new WeChatBot() await bot.login() bot.onMessage(async (msg) => { await bot.sendTyping(msg.userId) await bot.reply(msg, `Echo: ${msg.text}`) }) await bot.start()
配置
const bot = new WeChatBot({ storage: 'file', // 'file' | 'memory' | 自定义 Storage storageDir: '~/.wechatbot', logLevel: 'info', // 'debug' | 'info' | 'warn' | 'error' | 'silent' botAgent: 'MyApp/1.2 (prod)', // 随每个请求发送的 base_info.bot_agent 标识 loginCallbacks: { onQrUrl: (url) => renderQrCode(url), onScanned: () => console.log('已扫码!'), onExpired: () => console.log('已过期...'), }, })
发送消息 — reply() 和 send()
两个方法处理所有发送需求。reply() 回复收到的消息(自动 context_token、自动取消输入状态);send() 按用户 ID 发送。
两者接受相同的 SendContent 类型:
// 文本(字符串简写) await bot.reply(msg, '你好!') // 文本(对象) await bot.reply(msg, { text: '你好!' }) // 图片 + 可选标题 await bot.reply(msg, { image: pngBuffer, caption: '截图' }) // 视频 + 可选标题 await bot.reply(msg, { video: mp4Buffer, caption: '看看这个' }) // 文件 — 按扩展名自动路由: // .png/.jpg/.gif/.webp → 作为图片发送 // .mp4/.mov/.webm → 作为视频发送 // 其他 → 作为文件附件发送 await bot.reply(msg, { file: data, fileName: 'report.pdf' }) await bot.reply(msg, { file: data, fileName: 'photo.png' }) // → 图片! // 从 URL 发送 — 自动下载 + 自动检测类型 await bot.reply(msg, { url: 'https://example.com/photo.jpg' }) await bot.reply(msg, { url: 'https://picsum.photos/400/300', caption: '随机图片!' }) // send() 用法相同,只是用 userId 代替 message await bot.send(userId, '你好!') await bot.send(userId, { image: buffer, caption: '嗨!' })
下载媒体
一个方法下载消息中的任何媒体类型:
bot.onMessage(async (msg) => { const media = await bot.download(msg) if (!media) return // 消息中没有媒体 console.log(media.type) // 'image' | 'file' | 'video' | 'voice' console.log(media.data) // Buffer console.log(media.fileName) // 'report.pdf'(文件) console.log(media.format) // 'wav' | 'silk'(语音) // 保存到磁盘 await writeFile(`/tmp/${media.fileName ?? 'download'}`, media.data) // 回传 if (media.type === 'image') { await bot.reply(msg, { image: media.data, caption: '收到你的图片!' }) } })
优先级:图片 > 文件 > 视频 > 语音。语音自动从 SILK 转码为 WAV(需安装 silk-wasm)。
中间件
Express/Koa 风格的可组合中间件管道,在消息处理器之前运行。
import { WeChatBot, loggingMiddleware, rateLimitMiddleware, typeFilterMiddleware, filterMiddleware, } from '@wechatbot/wechatbot' const bot = new WeChatBot() bot.use(loggingMiddleware(bot.logger)) // 记录每条消息 bot.use(rateLimitMiddleware({ maxMessages: 10, windowMs: 60_000 })) bot.use(typeFilterMiddleware('text', 'image')) // 仅处理文本和图片 bot.use(filterMiddleware(/^\/\w+/)) // 仅处理命令 // 自定义中间件 bot.use(async (ctx, next) => { const start = Date.now() await next() console.log(`处理耗时 ${Date.now() - start}ms`) })
可插拔存储
SDK 通过 Storage 接口持久化登录凭证、消息游标等状态(共 5 个方法:get / set / delete / has / clear,值均为 JSON)。默认是文件存储(~/.wechatbot),也可以换成任何后端:
const bot1 = new WeChatBot({ storage: 'file' }) // 文件存储(默认) const bot2 = new WeChatBot({ storage: 'memory' }) // 内存存储 const bot3 = new WeChatBot({ storage: new RedisStorage() }) // 自定义
自定义存储示例 — PostgreSQL(一张 (tenant_id, key) 主键的 jsonb 表即可):
import type { Storage } from '@wechatbot/wechatbot' import type { Pool } from 'pg' export class PostgresStorage implements Storage { constructor(private pool: Pool, private tenantId: string) {} async get<T>(key: string): Promise<T | undefined> { const { rows } = await this.pool.query( 'SELECT value FROM bot_storage WHERE tenant_id = $1 AND key = $2', [this.tenantId, key], ) return rows[0]?.value as T | undefined } async set<T>(key: string, value: T): Promise<void> { await this.pool.query( `INSERT INTO bot_storage (tenant_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (tenant_id, key) DO UPDATE SET value = $3, updated_at = now()`, [this.tenantId, key, JSON.stringify(value)], ) } async delete(key: string): Promise<void> { await this.pool.query( 'DELETE FROM bot_storage WHERE tenant_id = $1 AND key = $2', [this.tenantId, key], ) } async has(key: string): Promise<boolean> { return (await this.get(key)) !== undefined } async clear(): Promise<void> { await this.pool.query( 'DELETE FROM bot_storage WHERE tenant_id = $1', [this.tenantId], ) } }
凭证(credentials 键)可完全操控对应微信账号,落库前建议做应用层加密(AES-GCM + KMS),不要明文存储。
多租户 / SaaS
每个 WeChatBot 实例完全隔离——独立的 HTTP 客户端、事件、消息轮询器,没有全局状态,也不占用本地端口。一个后端进程可以同时运行多个账号,二维码通过 onQrUrl 回调拿到后推给 Web 前端,让每个用户在自己的页面上扫码:
// 每个租户一个实例,凭证存数据库(见上一节 PostgresStorage) async function startTenantBot(tenantId: string) { const bot = new WeChatBot({ storage: new PostgresStorage(pool, tenantId), }) await bot.login({ callbacks: { onQrUrl: (url) => pushQrToWebUI(tenantId, url), // 推给该用户的页面 onScanned: () => notify(tenantId, '已扫码,等待确认'), onExpired: () => refreshQr(tenantId), }, }) bot.onMessage(async (msg) => { await bot.reply(msg, await agentReply(tenantId, msg)) }) await bot.start() return bot }
注意事项:
- 存储隔离:每个账号必须有独立的
storageDir或存储命名空间(如上例的tenantId),共用会互相覆盖凭证。 - 单账号单实例:同一个账号在全集群只能有一个实例在轮询,否则消息游标互相覆盖、消息漏收或重复。多机部署时用 Postgres advisory lock 或租约表保证每个账号只有一个 owner。
- 重启恢复:凭证已持久化,进程重启后
login()会自动用已有凭证恢复会话,无需重新扫码。
事件
bot.on('login', (creds) => console.log(`已登录: ${creds.accountId}`)) bot.on('session:expired', () => console.log('会话过期')) bot.on('session:restored', (creds) => console.log('已恢复')) bot.on('error', (err) => console.error(err))
进阶:MessageBuilder
对于复杂的多项消息,直接使用构建器:
const payload = bot.createMessage(userId) .text("这是你的报告:") .file({ media: cdnRef, fileName: 'report.pdf', size: 542188 }) .build() await bot.sendRaw(payload)
其他进阶方法:bot.upload(opts) 仅上传到 CDN 不发送,bot.downloadRaw(media, aeskey?) 直接访问 CDN 引用。
API 参考
核心方法
| 方法 | 描述 | |
|---|---|---|
| new WeChatBot(opts?) | 创建实例 | |
| bot.login(opts?) | 二维码登录(已有凭证则跳过) | |
| bot.start() | 启动长轮询 | |
| bot.run(opts?) | login() + start() 合一 | |
| bot.stop() | 优雅停止 | |
| bot.onMessage(handler) | 注册消息处理器 | |
| bot.reply(msg, content) | 回复 — 文本、图片、视频、文件或 URL | |
| bot.send(userId, content) | 发送给用户 — 相同内容类型 | |
| bot.download(msg) | 下载消息中的任何媒体 | |
| bot.sendTyping(userId) | 显示"正在输入" | |
| bot.stopTyping(userId) | 取消输入指示 | |
| bot.use(middleware) | 添加中间件 | |
| bot.sendRaw(payload) | 发送 MessageBuilder 预构建消息 | |
| bot.upload(opts) | 仅上传到 CDN | |
| bot.downloadRaw(media) | 从原始 CDN 引用下载 | |
| bot.createMessage(userId) | 创建 MessageBuilder |