在日常研发和运维场景中,系统经常需要把关键事件及时通知到团队,例如:
如果通知只停留在日志或后台页面里,响应链路会比较长。企业微信机器人提供了一种简单直接的方式:在群聊中添加机器人后,拿到一个 Webhook 地址,业务系统通过 HTTP POST 请求把消息推送到群里。
本文以 NestJS 为技术栈,整理一个可复用的企业微信机器人通知模块,重点关注:
企业微信自定义机器人本质上是一个群聊 Webhook。
配置流程通常如下:
示例 Webhook 形态如下:
https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
其中 key 是机器人调用凭证,必须当作敏感信息处理。不要写死在代码里,也不要提交到 Git 仓库、博客、截图或公开文档中。
企业微信机器人支持多种消息类型,常见包括:
text:文本消息markdown:Markdown 消息image:图片消息news:图文消息file:文件消息voice:语音消息template_card:模板卡片消息在业务通知场景中,最常用的是 text 和 markdown。
请求体示例:
{
"msgtype": "text",
"text": {
"content": "服务启动成功",
"mentioned_list": ["@all"],
"mentioned_mobile_list": []
}
}
说明:
msgtype 固定为 texttext.content 是文本内容mentioned_list 可以通过 userid 提醒群成员@all 可以提醒所有人,但生产环境要慎用请求体示例:
{
"msgtype": "markdown",
"markdown": {
"content": "### 服务告警\n> 服务:<font color=\"warning\">order-service</font>\n> 状态:接口超时\n> 时间:2026-08-19 10:30:00"
}
}
说明:
msgtype 固定为 markdownmarkdown.content 是 Markdown 内容info、comment、warning企业微信机器人支持的是 Markdown 子集,不是完整 GitHub Flavored Markdown。比如复杂表格、HTML 块、多行代码块等效果不一定符合预期。
在 NestJS 中实现企业微信机器人通知,建议封装成一个独立模块,例如 WecomRobotModule。
推荐结构:
src/
wecom-robot/
dto/
send-markdown.dto.ts
send-text.dto.ts
interfaces/
wecom-robot-message.interface.ts
wecom-robot.module.ts
wecom-robot.service.ts
app.module.ts
核心职责划分:
WecomRobotModule:注册 HTTP 能力和服务提供者WecomRobotService:负责组装消息、发送请求、处理响应NestJS 官方推荐通过模块组织应用结构,通过 Provider 承载业务逻辑,并使用依赖注入组合能力。HTTP 请求可以使用 @nestjs/axios 提供的 HttpModule 和 HttpService。
npm install @nestjs/axios axios
npm install @nestjs/config
如果需要环境变量校验,可以额外安装:
npm install joi
创建 .env:
WECOM_ROBOT_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx
WECOM_ROBOT_ENABLED=true
建议:
.env 文件不要提交到代码仓库在 AppModule 中启用配置模块:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
import { WecomRobotModule } from './wecom-robot/wecom-robot.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
WECOM_ROBOT_WEBHOOK: Joi.string().uri().required(),
WECOM_ROBOT_ENABLED: Joi.boolean().default(true),
}),
}),
WecomRobotModule,
],
})
export class AppModule {}
这里使用 isGlobal: true 后,其他模块可以直接注入 ConfigService,不需要重复导入 ConfigModule。
创建 src/wecom-robot/interfaces/wecom-robot-message.interface.ts:
export interface WecomTextMessage {
msgtype: 'text';
text: {
content: string;
mentioned_list?: string[];
mentioned_mobile_list?: string[];
};
}
export interface WecomMarkdownMessage {
msgtype: 'markdown';
markdown: {
content: string;
};
}
export type WecomRobotMessage = WecomTextMessage | WecomMarkdownMessage;
export interface WecomRobotResponse {
errcode: number;
errmsg: string;
}
这样做的好处是:
msgtype创建 src/wecom-robot/wecom-robot.module.ts:
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { WecomRobotService } from './wecom-robot.service';
@Module({
imports: [
HttpModule.register({
timeout: 5000,
maxRedirects: 0,
}),
],
providers: [WecomRobotService],
exports: [WecomRobotService],
})
export class WecomRobotModule {}
这里把 WecomRobotService 导出,方便其他业务模块注入使用。
HttpModule.register() 中配置了:
timeout: 5000:避免外部接口长时间阻塞业务线程maxRedirects: 0:Webhook 场景通常不需要跟随重定向创建 src/wecom-robot/wecom-robot.service.ts:
import { HttpService } from '@nestjs/axios';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { firstValueFrom } from 'rxjs';
import {
WecomMarkdownMessage,
WecomRobotMessage,
WecomRobotResponse,
WecomTextMessage,
} from './interfaces/wecom-robot-message.interface';
@Injectable()
export class WecomRobotService {
private readonly logger = new Logger(WecomRobotService.name);
constructor(
private readonly httpService: HttpService,
private readonly configService: ConfigService,
) {}
async sendText(
content: string,
options?: {
mentionedList?: string[];
mentionedMobileList?: string[];
},
): Promise<void> {
const message: WecomTextMessage = {
msgtype: 'text',
text: {
content,
mentioned_list: options?.mentionedList,
mentioned_mobile_list: options?.mentionedMobileList,
},
};
await this.send(message);
}
async sendMarkdown(content: string): Promise<void> {
const message: WecomMarkdownMessage = {
msgtype: 'markdown',
markdown: {
content,
},
};
await this.send(message);
}
private async send(message: WecomRobotMessage): Promise<void> {
const enabled = this.configService.get<boolean>('WECOM_ROBOT_ENABLED', true);
if (!enabled) {
this.logger.debug('WeCom robot notification is disabled.');
return;
}
const webhook = this.configService.get<string>('WECOM_ROBOT_WEBHOOK');
if (!webhook) {
this.logger.warn('WECOM_ROBOT_WEBHOOK is not configured.');
return;
}
try {
const response = await firstValueFrom(
this.httpService.post<WecomRobotResponse>(webhook, message, {
headers: {
'Content-Type': 'application/json',
},
}),
);
const data = response.data;
if (data.errcode !== 0) {
this.logger.warn(
`Failed to send WeCom robot message: errcode=${data.errcode}, errmsg=${data.errmsg}`,
);
return;
}
this.logger.debug('WeCom robot message sent successfully.');
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
this.logger.error(`Failed to request WeCom robot webhook: ${reason}`);
}
}
}
HttpService.post() 返回的是 RxJS Observable,因此这里使用 firstValueFrom() 转成 Promise,更适合在 async/await 代码中使用。
通知失败时没有直接抛出异常,是因为机器人通知通常是辅助链路,不应该轻易影响主业务流程。例如订单创建成功后,群通知失败不应该导致订单创建回滚。
当然,如果你的业务要求通知必须成功,可以调整策略:
假设有一个订单服务,需要在订单创建后发送通知。
import { Injectable } from '@nestjs/common';
import { WecomRobotService } from '../wecom-robot/wecom-robot.service';
@Injectable()
export class OrderService {
constructor(private readonly wecomRobotService: WecomRobotService) {}
async createOrder() {
const order = {
id: 'ORDER_202608190001',
amount: 299,
buyer: '张三',
};
await this.wecomRobotService.sendMarkdown(
[
'### 新订单通知',
`> 订单号:<font color="info">${order.id}</font>`,
`> 买家:${order.buyer}`,
`> 金额:<font color="warning">¥${order.amount}</font>`,
`> 时间:${new Date().toLocaleString('zh-CN')}`,
].join('\n'),
);
return order;
}
}
通知效果大致如下:
### 新订单通知
> 订单号:<font color="info">ORDER_202608190001</font>
> 买家:张三
> 金额:<font color="warning">¥299</font>
> 时间:2026/8/19 10:30:00
如果项目中到处手写 Markdown 字符串,很容易出现格式不统一、字段缺失、颜色滥用等问题。推荐进一步封装通知模板。
例如创建 src/wecom-robot/wecom-robot-template.ts:
export class WecomRobotTemplate {
static orderCreated(params: {
orderId: string;
buyer: string;
amount: number;
createdAt: Date;
}): string {
return [
'### 新订单通知',
`> 订单号:<font color="info">${params.orderId}</font>`,
`> 买家:${params.buyer}`,
`> 金额:<font color="warning">¥${params.amount}</font>`,
`> 时间:${params.createdAt.toLocaleString('zh-CN')}`,
].join('\n');
}
static jobFailed(params: {
jobName: string;
reason: string;
happenedAt: Date;
}): string {
return [
'### 定时任务失败',
`> 任务:<font color="warning">${params.jobName}</font>`,
`> 原因:${params.reason}`,
`> 时间:${params.happenedAt.toLocaleString('zh-CN')}`,
].join('\n');
}
}
业务中使用:
await this.wecomRobotService.sendMarkdown(
WecomRobotTemplate.jobFailed({
jobName: 'sync-order-status',
reason: '第三方接口超时',
happenedAt: new Date(),
}),
);
这样可以把“通知内容设计”和“通知发送能力”拆开,后期维护会轻松很多。
企业微信机器人对消息长度有限制:
注意这里是字节,不是字符。中文、emoji、特殊符号都可能占用多个字节。
可以写一个工具函数:
export function getUtf8ByteLength(value: string): number {
return Buffer.byteLength(value, 'utf8');
}
export function truncateUtf8(value: string, maxBytes: number): string {
let result = '';
for (const char of value) {
const next = result + char;
if (Buffer.byteLength(next, 'utf8') > maxBytes) {
return result;
}
result = next;
}
return result;
}
在发送前处理:
const content = truncateUtf8(markdownContent, 4096);
await this.wecomRobotService.sendMarkdown(content);
更好的实践是:重要通知不要塞太多内容。群消息只放摘要和链接,详情放到后台页面、日志平台或监控平台中。
企业微信机器人有发送频率限制:每个机器人发送的消息不能超过 20 条/分钟。
真实项目里,最容易出现刷屏的场景是:
推荐处理方式:
不要每条失败都发一条群消息,可以按时间窗口聚合。
示例:
过去 5 分钟订单同步失败 132 次
影响店铺 8 个
主要错误:第三方接口超时
同一类错误短时间内只通知一次。
可以用 Redis 设置短期 key:
wecom:notify:dedupe:job-failed:sync-order-status
设置 5 分钟过期。如果 key 存在,就不重复发送。
不同等级采用不同策略:
| 等级 | 示例 | 策略 |
|---|---|---|
| info | 发布成功 | 可以发送,但不 @ 人 |
| warning | 任务失败一次 | 发送摘要 |
| error | 核心链路失败 | @ 负责人 |
| critical | 大面积不可用 | @all,但必须谨慎 |
Webhook 地址就是机器人凭证,泄漏后别人可以直接向群里发消息。
必须注意:
.env日志中如果必须输出,可以脱敏:
export function maskWebhook(webhook: string): string {
return webhook.replace(/key=([^&]+)/, 'key=****');
}
发送机器人消息可能失败,常见原因包括:
可以定义一个更清晰的结果类型:
export interface SendWecomMessageResult {
success: boolean;
errcode?: number;
errmsg?: string;
reason?: string;
}
然后让发送方法返回结果:
async sendMarkdown(content: string): Promise<SendWecomMessageResult> {
const message: WecomMarkdownMessage = {
msgtype: 'markdown',
markdown: { content },
};
return this.send(message);
}
这样业务侧可以决定如何处理:
const result = await this.wecomRobotService.sendMarkdown(content);
if (!result.success) {
// 写入审计表、进入补偿队列,或仅记录日志
}
机器人通知是否重试,要看业务性质。
适合重试:
不适合盲目重试:
简单策略:
如果项目中已经使用 BullMQ、RabbitMQ 或 Kafka,可以把通知作为异步任务处理。
不要在业务服务中直接写:
axios.post('https://qyapi.weixin.qq.com/xxx', body);
应该封装成:
await this.wecomRobotService.sendMarkdown(content);
这样可以统一处理:
推荐格式:
### 告警标题
> 服务:order-service
> 环境:production
> 等级:warning
> 原因:第三方接口超时
> 时间:2026-08-19 10:30:00
> 链接:[查看详情](https://example.com)
避免:
报错了,快看
好的通知应该让接收者快速判断:
建议在通知中明确环境:
> 环境:<font color="warning">production</font>
否则开发环境的一条测试告警,很容易造成误判。
@all 应该只用于真正需要所有人立即关注的事件,例如:
普通日志、成功通知、低优先级告警不建议 @all。
WecomRobotService 可以通过 mock HttpService 测试。
测试重点:
sendText() 是否组装正确请求体sendMarkdown() 是否组装正确请求体errcode !== 0 时是否记录失败示例伪代码:
it('should send markdown message', async () => {
httpService.post.mockReturnValue(
of({
data: {
errcode: 0,
errmsg: 'ok',
},
}),
);
await service.sendMarkdown('### hello');
expect(httpService.post).toHaveBeenCalledWith(
expect.stringContaining('https://qyapi.weixin.qq.com'),
{
msgtype: 'markdown',
markdown: {
content: '### hello',
},
},
expect.any(Object),
);
});
企业微信机器人通知实现起来并不复杂,本质就是一次 HTTPS POST 请求。但在真实项目中,真正需要注意的是工程化细节:
基于 NestJS 的模块化和依赖注入机制,我们可以把企业微信机器人封装成一个干净的基础设施能力。业务模块只关心“发送什么通知”,而不用关心“怎么调用企业微信接口”。这也是后端项目中非常典型的一类工程实践:把外部系统集成能力沉淀为可复用模块。