Files
notice/README.md
T

6.7 KiB
Raw Blame History

notice

notice 是一个 Go 多渠道消息通知 package,统一接入飞书、钉钉、企业微信和通用 Webhook。调用方只需要构造统一的 Message,渠道差异、签名和响应业务码均由 package 内部处理。

HTTP 请求统一使用 git.zhangdeman.cn/zhangdeman/network,每次发送只调用一次目标 Webhook,不在 package 内自动重试。

安装

go get git.zhangdeman.cn/zhangdeman/notice

当前版本依赖 Go 1.25 及以上版本。

支持能力

消息类型 飞书 钉钉 企业微信 通用 Webhook
普通文本 Text Text Text 统一 JSON
带颜色文本 彩色卡片文本 Markdown 语义前缀 Markdown 内置颜色 统一 JSON
Markdown 交互卡片 Markdown Markdown 统一 JSON
卡片 Card JSON 2.0 ActionCard Template Card 统一 JSON
卡片主题 支持 保留内容 保留内容 原样传递

快速开始

创建 Factory

package main

import (
	"context"
	"os"
	"time"

	"git.zhangdeman.cn/zhangdeman/notice"
)

func main() {
	factory := notice.NewFactory(map[notice.Channel]notice.Config{
		notice.ChannelFeishu: {
			Webhook: os.Getenv("FEISHU_WEBHOOK"),
			Security: &notice.SecurityConfig{
				SignEnabled: true,
				SignSecret:  os.Getenv("FEISHU_SIGN_SECRET"),
			},
			Timeout: 5 * time.Second,
		},
		notice.ChannelDingTalk: {
			Webhook: os.Getenv("DINGTALK_WEBHOOK"),
			Security: &notice.SecurityConfig{
				SignEnabled: true,
				SignSecret:  os.Getenv("DINGTALK_SIGN_SECRET"),
			},
			Timeout: 5 * time.Second,
		},
		notice.ChannelWeCom: {
			Webhook: os.Getenv("WECOM_WEBHOOK"),
			Timeout: 5 * time.Second,
		},
	})

	sender, err := factory.Get(notice.ChannelFeishu)
	if err != nil {
		panic(err)
	}
	if err = sender.Send(context.Background(), notice.Text("服务启动完成")); err != nil {
		panic(err)
	}
}

Factory.Get(channel) 会缓存初始化成功的 Sender。同一 Factory 内再次获取相同渠道时直接返回已有实例,并发调用也只会初始化一个实例。

也可以绕过 Factory,直接创建单个渠道实例:

sender, err := notice.New(notice.ChannelWebhook, notice.Config{
	Webhook: "https://example.com/webhook",
})

Webhook 地址必须为 HTTPSTimeout 为空时默认 5 秒。

独立签名校验

每个机器人配置拥有独立的安全校验开关和密钥:

Security: &notice.SecurityConfig{
	SignEnabled: true,
	SignSecret:  os.Getenv("ROBOT_SIGN_SECRET"),
},
  • 飞书:在请求体中加入秒级 timestampsign
  • 钉钉:在 Webhook 查询参数中加入毫秒级 timestampsign
  • SignEnabledfalse 或未设置 Security 时不生成签名。
  • 启用签名后 SignSecret 不能为空。
  • 企业微信群机器人和通用 Webhook 不启用该签名配置。

SecurityConfig 属于单个渠道的 Config,因此不同机器人可以分别启用签名并使用不同密钥。

消息格式

普通文本

err := sender.Send(ctx, notice.Text("数据同步任务已完成"))

带颜色文本

message := notice.ColorText(
	notice.TextSegment{Text: "发布状态:"},
	notice.TextSegment{
		Text:  "失败",
		Color: notice.ColorDanger,
		Bold:  true,
	},
)

err := sender.Send(ctx, message)

支持 defaultinfosuccesswarningdangermuted 六种语义颜色。各平台会转换为最接近的原生显示效果。

Markdown

err := sender.Send(ctx, notice.Markdown(
	"订单服务告警",
	"**错误率超过阈值**\n\n当前值:3.2%",
))

卡片消息

message := notice.Card(notice.CardContent{
	Title:    "发布结果",
	Theme:    notice.CardThemeGreen,
	Markdown: "服务已成功发布到生产环境。",
	Fields: []notice.CardField{
		{Name: "服务", Value: "order-service"},
		{Name: "版本", Value: "v1.8.0"},
	},
	Actions: []notice.CardAction{
		{Text: "查看发布详情", URL: "https://example.com/releases/1001"},
	},
})

err := sender.Send(ctx, message)

飞书支持 defaultbluewathetturquoisegreenyelloworangeredcarminevioletpurpleindigogrey 卡片主题。卡片操作地址必须使用 HTTPS。

同步与异步发送

同步发送直接返回结果:

if err := sender.Send(ctx, message); err != nil {
	return err
}

异步发送返回容量为 1 的只读错误通道:

result := sender.SendAsync(ctx, message)

go func() {
	if err := <-result; err != nil {
		log.Printf("send notice failed: %v", err)
	}
}()

异步任务运行在当前进程中。调用方需要保证进程和 Context 在发送完成前仍然有效。

包装 Zap Logger

WrapZap 在原 Logger Core 之外附加通知 Core,不改变原有控制台或文件日志输出。

base, err := zap.NewProduction()
if err != nil {
	return err
}

logger, err := factory.WrapZap(base, notice.ZapConfig{
	Channels: []notice.Channel{
		notice.ChannelFeishu,
		notice.ChannelDingTalk,
	},
	OnError: func(err error) {
		// 使用独立的处理方式,避免写回 logger 形成递归通知。
		fmt.Printf("send log notice failed: %v\n", err)
	},
})
if err != nil {
	return err
}
defer logger.Sync()

logger.Info("服务启动完成")
logger.Warn("订单延迟升高", zap.Int("delay_ms", 2500))
logger.Error("订单创建失败", zap.String("order_id", "O1001"))

Levels 为空时,仅精确匹配 WarnLevelErrorLevel。自定义等级示例:

logger, err := factory.WrapZap(base, notice.ZapConfig{
	Channels: []notice.Channel{notice.ChannelWeCom},
	Levels: []zapcore.Level{
		zapcore.ErrorLevel,
		zapcore.DPanicLevel,
	},
})

通知异步发送到指定渠道。logger.Sync() 会等待已触发的通知结束,适合在程序退出前调用。日志消息会转换为卡片,包含等级、Logger 名称、时间、调用位置以及 Zap 字段。

错误处理

  • 参数错误可通过 errors.Is 判断 ErrUnsupportedChannelErrChannelNotConfiguredErrInvalidConfigErrInvalidMessageErrInvalidZapConfig
  • 非 2xx 响应返回 *notice.HTTPError
  • 平台业务码失败返回 *notice.PlatformError
  • SendAsync 的错误通过返回通道读取;Zap 通知错误通过 ZapConfig.OnError 接收。
var platformErr *notice.PlatformError
if errors.As(err, &platformErr) {
	log.Printf("channel=%s code=%s message=%s", platformErr.Channel, platformErr.Code, platformErr.Message)
}

配置中包含 Webhook 和签名密钥,业务代码不要将完整配置写入日志。

开发验证

go test -race ./...

详细的协议映射与实现说明见多渠道消息通知平台设计文档