Files
ryan 0d0cd0c510 feat: 新增 SafeW 出站通知渠道
通过 Bot API sendMessage 投递渲染后的通知,MarkdownV2 自动转义标题与正文。

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

3.7 KiB
Raw Permalink Blame History

SafeW 渠道设计

Date: 2026-08-14
Status: Approved

Goal

新增出站渠道 safew:规则命中并渲染模板后,通过 SafeW Bot API sendMessage 把通知发到指定会话。

Non-goals

  • 接收 SafeW webhook / 入站更新
  • 全局 bot tokenconfig.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 时:

{
  "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 里数字 ID123)或字符串("123" / "@name")都接受,sender 归一成字符串再发给 SafeW

token 或空 chat_id:立即返回 error,不发 HTTP。token / chat_id 读写前 strings.TrimSpace

Send contract

  • URLPOST https://api.safew.bot/bot{token}/sendMessage
  • HeaderContent-Type: application/json
  • Body
{
  "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.gohttptest.Server 模拟 SafeW):

  1. 转义:hello_world.hello\_world\.;标题包在 *...*
  2. 成功请求:URL 含 /bot{token}/sendMessagebody 含 chat_idparse_mode=MarkdownV2、转义后的 text
  3. ok: false 或 HTTP 4xx → error,且含 description(若有)
  4. token / chat_id → 失败且不发 HTTP
  5. chat_id 为 JSON number123456789)时仍能发出,请求里是字符串 "123456789"

不修改 test/e2e/。手工验证用 docs/httpie/curls.md 里的创建示例。

Success criteria

  • 可创建 type=safew 的 channel,并绑到规则
  • 命中规则后异步调用 SafeW sendMessage
  • 含 MarkdownV2 特殊字符的标题/正文能发出(已转义)
  • SafeW 业务失败(ok: false)记入失败日志,而不是当成成功