Files

251 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# notice
`notice` 是一个 Go 多渠道消息通知 package,统一接入飞书、钉钉、企业微信和通用 Webhook。调用方只需要构造统一的 `Message`,渠道差异、签名和响应业务码均由 package 内部处理。
HTTP 请求统一使用 [`git.zhangdeman.cn/zhangdeman/network`](https://git.zhangdeman.cn/zhangdeman/network),每次发送只调用一次目标 Webhook,不在 package 内自动重试。
## 安装
```bash
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
```go
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,直接创建单个渠道实例:
```go
sender, err := notice.New(notice.ChannelWebhook, notice.Config{
Webhook: "https://example.com/webhook",
})
```
Webhook 地址必须为 HTTPS`Timeout` 为空时默认 5 秒。
### 独立签名校验
每个机器人配置拥有独立的安全校验开关和密钥:
```go
Security: &notice.SecurityConfig{
SignEnabled: true,
SignSecret: os.Getenv("ROBOT_SIGN_SECRET"),
},
```
- 飞书:在请求体中加入秒级 `timestamp``sign`
- 钉钉:在 Webhook 查询参数中加入毫秒级 `timestamp``sign`
- `SignEnabled``false` 或未设置 `Security` 时不生成签名。
- 启用签名后 `SignSecret` 不能为空。
- 企业微信群机器人和通用 Webhook 不启用该签名配置。
`SecurityConfig` 属于单个渠道的 `Config`,因此不同机器人可以分别启用签名并使用不同密钥。
## 消息格式
### 普通文本
```go
err := sender.Send(ctx, notice.Text("数据同步任务已完成"))
```
### 带颜色文本
```go
message := notice.ColorText(
notice.TextSegment{Text: "发布状态:"},
notice.TextSegment{
Text: "失败",
Color: notice.ColorDanger,
Bold: true,
},
)
err := sender.Send(ctx, message)
```
支持 `default``info``success``warning``danger``muted` 六种语义颜色。各平台会转换为最接近的原生显示效果。
### Markdown
```go
err := sender.Send(ctx, notice.Markdown(
"订单服务告警",
"**错误率超过阈值**\n\n当前值:3.2%",
))
```
### 卡片消息
```go
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)
```
飞书支持 `default``blue``wathet``turquoise``green``yellow``orange``red``carmine``violet``purple``indigo``grey` 卡片主题。卡片操作地址必须使用 HTTPS。
## 同步与异步发送
同步发送直接返回结果:
```go
if err := sender.Send(ctx, message); err != nil {
return err
}
```
异步发送返回容量为 1 的只读错误通道:
```go
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,不改变原有控制台或文件日志输出。
```go
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` 为空时,仅精确匹配 `WarnLevel``ErrorLevel`。自定义等级示例:
```go
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` 判断 `ErrUnsupportedChannel``ErrChannelNotConfigured``ErrInvalidConfig``ErrInvalidMessage``ErrInvalidZapConfig`
- 非 2xx 响应返回 `*notice.HTTPError`
- 平台业务码失败返回 `*notice.PlatformError`
- `SendAsync` 的错误通过返回通道读取;Zap 通知错误通过 `ZapConfig.OnError` 接收。
```go
var platformErr *notice.PlatformError
if errors.As(err, &platformErr) {
log.Printf("channel=%s code=%s message=%s", platformErr.Channel, platformErr.Code, platformErr.Message)
}
```
配置中包含 Webhook 和签名密钥,业务代码不要将完整配置写入日志。
## 开发验证
```bash
go test -race ./...
```
详细的协议映射与实现说明见[多渠道消息通知平台设计文档](./多渠道消息通知平台设计文档.md)。