Files
aiaa-notification-server/docs/superpowers/specs/2026-08-15-safew-chats-list-design.md
ryan bd8a9ff96d feat: 增加 SafeW 已监控群列表接口
通过 getUpdates 将群写入 Redis,供创建/编辑渠道时选择 chat_id,避免前端重复传递 token。
2026-08-15 00:40:40 +08:00

4.6 KiB
Raw Permalink Blame History

SafeW 群列表接口设计

Date: 2026-08-15
Status: Approved

Goal

为 safew 渠道提供两个管理接口,返回该 bot token 已经监控到的群。SafeW 没有 getChats;用 getUpdates 收集群聊,写入 Redis,列表接口读 Redis。

Non-goals

  • 列出 private / channel
  • 在请求路径上 timeout=30 阻塞 UI
  • 把 bot token 明文写入 Redis key 或提交到仓库
  • 远程 e2e 真实 SafeW 轮询
  • 新增 MySQL 表

Endpoints

Admin Bearer 鉴权,与现有 /api/v1/channels 相同。

POST /api/v1/channels/safew/chats

创建渠道时选群。Body

{
  "token": "<bot token>",
  "q": "测试AI"
}
字段 必填 说明
token SafeW Bot Token
q 按群名 / username / chat_id 子串过滤(不区分大小写)

路由必须注册在 GET /channels/:id 之前,避免 safew 被当成 id。

GET /api/v1/channels/:id/chats?q=

编辑已有渠道。用库里 channel.config.token,前端不再传 token。

情况 响应
channel 不存在 404 {"error":"channel not found"}
type != safew 400 {"error":"channel is not safew"}
config 无 token 400 {"error":"safew token is required"}

成功响应(两个入口相同)

{
  "data": [
    {
      "id": "10000778141",
      "type": "group",
      "title": "测试AI",
      "username": null
    }
  ],
  "total": 1
}
  • id 永远是 JSON 字符串(SafeW 的数字 id 可能超过 JS 安全整数)
  • username 没有则为 null
  • 默认只含 group / supergroup
  • 空列表:200{"data":[],"total":0}bot 尚未在任何群收到更新时正常)

Token 无效:401{"error":"<SafeW description>"}

Architecture

POST/GET chats
  → 解析 token
  → Watcher.Ensure(token)     // 懒启动该 token 的后台长轮询
  → Poll getUpdates timeout=0 // 抽干积压,不阻塞
  → 合并群到 Redis
  → 按 q 过滤,返回 data/total

Watcher (per token, in-process)
  loop:
    getUpdates timeout=30
    合并群到 Redis
    更新 offset

同一 token 对 SafeW getUpdates 必须串行(Redis 锁),避免并发把 offset 冲掉。

SafeW getUpdates

  • URLPOST https://api.safew.bot/bot{token}/getUpdates
  • Body{"timeout":<0|30>,"offset":<last+1>,"limit":100}
  • 处理完一批后:offset = max(update_id)+1 写入 Redis
  • 从每条 update 的 message / edited_message / my_chat_member / chat_member / channel_post 等字段里取 chat;只保留 typegroupsupergroup
  • chat.idjson.Number / raw 转成十进制字符串,禁止 float64

群只有 bot 收到过该群的更新(加群、有人说话等)才会出现。

Redis

Key 用 sha256(token) 的 hex,不把 token 放进 key。

Key 类型 内容 TTL
safew:chats:{hash} Hash field=chat_idvalue={"id","type","title","username"}
safew:offset:{hash} String 已确认的最大 update_id
safew:poll:{hash} String getUpdates 互斥锁,短 TTL(约 35s

进程重启后群记录仍在;Watcher 从存着的 offset 继续。

Components

文件 职责
internal/adapter/safew.go getUpdates HTTP、解析 chat、id 转字符串
internal/cache/safew_chats.go chats / offset / lock
internal/safew/watcher.go 每 token 一条 goroutinetimeout=30
internal/handler/channel.go 两个 HTTP handler
cmd/server/main.go 注册路由、注入 Watcher、shutdown 时 Stop
README.md / docs/httpie/curls.md 接口文档

Error handling

情况 行为
SafeW 401 / token 无效 列表接口 401;Watcher 打日志,该轮结束,下次再试
网络错误、getUpdates 5xx 列表仍返回 Redis 已有群;Watcher sleep 后重试
抢不到 poll 锁 跳过本次拉取,直接返回 Redis
JSON 非法 / 缺 token 400

Tests

不引入真实 token。

  1. 解析update 含 group + private → 只留下 groupid 为字符串 "10000778141"
  2. 过滤q=测试 命中 titleq=10000778141 命中 id。
  3. getUpdates mockhttptest 返回一批 updateoffset 前进。
  4. handler:非 safew channel → 400;缺 token → 400。

Watcher 用可注入的 poll 函数 + 假 cache,不断言真实 30s 阻塞。

Success criteria

  • 两个接口返回已监控群,id 为字符串
  • 默认只有群;q 可过滤
  • 后台 timeout=30 持续写入 Redis;列表接口不卡 30 秒
  • 仓库与 Redis key 中不出现明文 token