Files
aiaa-notification-server/docs/superpowers/specs/2026-08-14-safew-channel-design.md
T
ryan 0d0cd0c510 feat: 新增 SafeW 出站通知渠道
通过 Bot API sendMessage 投递渲染后的通知,MarkdownV2 自动转义标题与正文。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 00:58:44 +08:00

123 lines
3.7 KiB
Markdown
Raw 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.
# SafeW 渠道设计
**Date:** 2026-08-14
**Status:** Approved
## Goal
新增出站渠道 `safew`:规则命中并渲染模板后,通过 SafeW Bot API `sendMessage` 把通知发到指定会话。
## Non-goals
- 接收 SafeW webhook / 入站更新
- 全局 bot token`config.yaml` 不加 safew 段)
- 自定义 API base URL
- 钉钉式按分钟限流排队
- 远程 e2e 真实发送(无现成测试 bot 凭据)
- 管理 API 对 channel `type` 做白名单校验(现有渠道也没有)
## Architecture
沿用现有扩展点:实现 `adapter.ChannelSender`,在 `adapter.NewSender` 注册 `"safew"`
不改表结构。`channel.type` 为字符串,`channel.config` 为 JSON。
发送仍走:
```
POST /api/v1/notify
→ 匹配规则、渲染模板
→ Router 按 ch.Type 取 sender
→ 异步 Send(title, content, config)
→ 失败重试 1s / 5s / 30s
→ 写入 message_log
```
`title` 沿用现有值:`{source.name}: {event}``content` 为模板渲染结果。
## Channel config
创建 channel 时:
```json
{
"name": "safew-ops",
"type": "safew",
"config": {
"token": "<bot token>",
"chat_id": "123456789"
},
"status": 1
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `token` | string | 是 | SafeW Bot Token,每个 channel 独立 |
| `chat_id` | string 或 number | 是 | 目标会话。JSON 里数字 ID(`123`)或字符串(`"123"` / `"@name"`)都接受,sender 归一成字符串再发给 SafeW |
`token` 或空 `chat_id`:立即返回 error,不发 HTTP。`token` / `chat_id` 读写前 `strings.TrimSpace`
## Send contract
- URL`POST https://api.safew.bot/bot{token}/sendMessage`
- Header`Content-Type: application/json`
- Body
```json
{
"chat_id": "<chat_id>",
"text": "*<escaped title>*\n<escaped content>",
"parse_mode": "MarkdownV2"
}
```
### MarkdownV2 转义
标题和正文都自动转义后再组装。模板里的 markdown 会变成字面量,避免特殊字符导致 400。
转义字符:`_ * [ ] ( ) ~ \` > # + - = | { } . !`
规则:每个字符前加 `\`。
组装:`text = "*" + escape(title) + "*\n" + escape(content)`
## Error handling
| 情况 | 行为 |
|------|------|
| config JSON 非法、缺 `token` / `chat_id` | 返回 error(router 仍会按现有策略重试,每次同样失败) |
| 网络错误、HTTP status ≥ 400 | 返回 error,触发重试 |
| HTTP 2xx 但 body `{"ok":false}` | 视为失败;error 带上 SafeW 的 `description` |
| HTTP 2xx 且 `ok: true` | 成功 |
不单独处理 429。不引入新的限流器。
## Components
| 文件 | 改动 |
|------|------|
| `internal/adapter/safew.go` | 新增 `SafeWSender` |
| `internal/adapter/safew_test.go` | httptest mock 单元测试 |
| `internal/adapter/adapter.go` | `NewSender` 增加 `case "safew"` |
| `README.md` | 渠道列表与创建示例 |
| `docs/httpie/curls.md` | 创建 SafeW channel 的 curl |
## Tests
`internal/adapter/safew_test.go``httptest.Server` 模拟 SafeW):
1. 转义:`hello_world.``hello\_world\.`;标题包在 `*...*`
2. 成功请求:URL 含 `/bot{token}/sendMessage`body 含 `chat_id``parse_mode=MarkdownV2`、转义后的 `text`
3. `ok: false` 或 HTTP 4xx → error,且含 `description`(若有)
4.`token` / `chat_id` → 失败且不发 HTTP
5. `chat_id` 为 JSON number`123456789`)时仍能发出,请求里是字符串 `"123456789"`
不修改 `test/e2e/`。手工验证用 `docs/httpie/curls.md` 里的创建示例。
## Success criteria
- 可创建 `type=safew` 的 channel,并绑到规则
- 命中规则后异步调用 SafeW `sendMessage`
- 含 MarkdownV2 特殊字符的标题/正文能发出(已转义)
- SafeW 业务失败(`ok: false`)记入失败日志,而不是当成成功