# 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": "", "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": "", "text": "**\n", "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`)记入失败日志,而不是当成成功