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

151 lines
4.6 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-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
```json
{
"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"}` |
### 成功响应(两个入口相同)
```json
{
"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
- URL`POST 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`;只保留 `type``group``supergroup`
- `chat.id``json.Number` / raw 转成十进制字符串,禁止 `float64`
群只有 bot **收到过该群的更新**(加群、有人说话等)才会出现。
## Redis
Key 用 `sha256(token)` 的 hex,不把 token 放进 key。
| Key | 类型 | 内容 | TTL |
|-----|------|------|-----|
| `safew:chats:{hash}` | Hash | field=`chat_id`value=`{"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 一条 goroutine`timeout=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 → 只留下 group`id` 为字符串 `"10000778141"`
2. **过滤**`q=测试` 命中 title`q=10000778141` 命中 id。
3. **getUpdates mock**httptest 返回一批 updateoffset 前进。
4. **handler**:非 safew channel → 400;缺 token → 400。
Watcher 用可注入的 poll 函数 + 假 cache,不断言真实 30s 阻塞。
## Success criteria
- 两个接口返回已监控群,`id` 为字符串
- 默认只有群;`q` 可过滤
- 后台 `timeout=30` 持续写入 Redis;列表接口不卡 30 秒
- 仓库与 Redis key 中不出现明文 token