wechatbot
δΈ­ζ–‡

The Node.js SDK is the most feature-rich implementation. 77 tests, zero runtime dependencies. Requires Node.js β‰₯ 22.

Install

npm install @wechatbot/wechatbot

Quick start

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()

Configuration

const bot = new WeChatBot({ storage: 'file', // 'file' | 'memory' | custom Storage storageDir: '~/.wechatbot', logLevel: 'info', // 'debug' | 'info' | 'warn' | 'error' | 'silent' botAgent: 'MyApp/1.2 (prod)', // base_info.bot_agent identifier sent on every request loginCallbacks: { onQrUrl: (url) => renderQrCode(url), onScanned: () => console.log('Scanned!'), onExpired: () => console.log('Expired...'), }, })

Sending β€” reply() and send()

Two methods handle all outgoing messages. reply() replies to an incoming message (auto context_token, auto cancel typing). send() sends to a user by ID.

Both accept the same SendContent type:

// Text (string shorthand) await bot.reply(msg, 'Hello!') // Text (object) await bot.reply(msg, { text: 'Hello!' }) // Image with optional caption await bot.reply(msg, { image: pngBuffer, caption: 'Screenshot' }) // Video with optional caption await bot.reply(msg, { video: mp4Buffer, caption: 'Check this out' }) // File β€” auto-routes by extension: // .png/.jpg/.gif/.webp β†’ sent as image // .mp4/.mov/.webm β†’ sent as video // everything else β†’ sent as file attachment await bot.reply(msg, { file: data, fileName: 'report.pdf' }) await bot.reply(msg, { file: data, fileName: 'photo.png' }) // β†’ image! // From URL β€” auto-download + auto-detect type from Content-Type / extension await bot.reply(msg, { url: 'https://example.com/photo.jpg' }) await bot.reply(msg, { url: 'https://picsum.photos/400/300', caption: 'Random!' }) // send() works the same way, just takes userId instead of message await bot.send(userId, 'Hello!') await bot.send(userId, { image: buffer, caption: 'Hi!' })

Downloading media

One method downloads any media type from an incoming message:

bot.onMessage(async (msg) => { const media = await bot.download(msg) if (!media) return // no media in this message console.log(media.type) // 'image' | 'file' | 'video' | 'voice' console.log(media.data) // Buffer console.log(media.fileName) // 'report.pdf' (for files) console.log(media.format) // 'wav' | 'silk' (for voice) // Save to disk await writeFile(`/tmp/${media.fileName ?? 'download'}`, media.data) // Echo back if (media.type === 'image') { await bot.reply(msg, { image: media.data, caption: 'Got your image!' }) } })

Priority: image > file > video > voice. Voice is auto-transcoded from SILK to WAV (if silk-wasm is installed).

Middleware

Express/Koa-style composable middleware pipeline. Runs before message handlers.

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+/)) // Custom middleware bot.use(async (ctx, next) => { const start = Date.now() await next() console.log(`Processed in ${Date.now() - start}ms`) })

Middleware that doesn't call next() stops the chain β€” perfect for auth, filtering, routing.

Pluggable storage

The SDK persists credentials, the message cursor, and other state through the Storage interface β€” five methods (get / set / delete / has / clear), JSON values. File storage (~/.wechatbot) is the default; swap in any backend:

// File storage (default) β€” survives restarts const bot1 = new WeChatBot({ storage: 'file' }) // Memory storage β€” fast, ephemeral const bot2 = new WeChatBot({ storage: 'memory' }) // Custom β€” implement 5 methods const bot3 = new WeChatBot({ storage: new RedisStorage() })

Custom storage example β€” PostgreSQL (one jsonb table keyed by (tenant_id, key)):

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], ) } }

The credentials key grants full control of the WeChat account β€” encrypt it at the application layer (AES-GCM + KMS) before it hits the database.

Multi-tenant / SaaS

Every WeChatBot instance is fully isolated β€” its own HTTP client, event emitter, and message poller, with no global state and no local ports. One backend process can run many accounts side by side. The QR code arrives via the onQrUrl callback, so you can push it to a web frontend and let each user scan from their own page:

// One instance per tenant, credentials in the database (see PostgresStorage above) async function startTenantBot(tenantId: string) { const bot = new WeChatBot({ storage: new PostgresStorage(pool, tenantId), }) await bot.login({ callbacks: { onQrUrl: (url) => pushQrToWebUI(tenantId, url), // render on that user's page onScanned: () => notify(tenantId, 'Scanned β€” waiting for confirmation'), onExpired: () => refreshQr(tenantId), }, }) bot.onMessage(async (msg) => { await bot.reply(msg, await agentReply(tenantId, msg)) }) await bot.start() return bot }

Things to watch:

  • Storage isolation β€” each account needs its own storageDir or storage namespace (the tenantId above); sharing one overwrites credentials.
  • One live instance per account β€” only one poller per account across your whole fleet, or message cursors overwrite each other and messages get lost or duplicated. In multi-machine deployments, use a Postgres advisory lock or a lease table to elect a single owner per account.
  • Restart recovery β€” credentials are persisted, so after a restart login() restores the session without a new QR scan.

Events

bot.on('login', (creds) => console.log(`Logged in: ${creds.accountId}`)) bot.on('session:expired', () => console.log('Session expired')) bot.on('session:restored', (creds) => console.log('Restored')) bot.on('error', (err) => console.error(err)) bot.on('poll:start', () => { }) bot.on('poll:stop', () => { }) bot.on('close', () => { })

Advanced: MessageBuilder

For complex multi-item messages, use the builder directly:

const payload = bot.createMessage(userId) .text("Here's your report:") .file({ media: cdnRef, fileName: 'report.pdf', size: 542188 }) .build() await bot.sendRaw(payload)

Other advanced methods: bot.upload(opts) for CDN upload without sending, bot.downloadRaw(media, aeskey?) for raw CDN references.

API reference

Core methods
MethodDescription
new WeChatBot(opts?)Create instance
bot.login(opts?)QR login (skips if credentials exist)
bot.start()Start long-poll loop
bot.run(opts?)login() + start() in one call
bot.stop()Stop gracefully
bot.onMessage(handler)Register message handler
bot.reply(msg, content)Reply β€” text, image, video, file, or URL
bot.send(userId, content)Send to user β€” same content types
bot.download(msg)Download any media from message
bot.sendTyping(userId)Show "typing..." indicator
bot.stopTyping(userId)Cancel typing indicator
bot.use(middleware)Add middleware to pipeline
bot.sendRaw(payload)Send pre-built MessageBuilder payload
bot.upload(opts)Upload to CDN without sending
bot.downloadRaw(media)Download from raw CDN reference
bot.createMessage(userId)Create MessageBuilder for user
Node.js SDK β€” @wechatbot/wechatbot