feat: 增加 SafeW 已监控群列表接口
通过 getUpdates 将群写入 Redis,供创建/编辑渠道时选择 chat_id,避免前端重复传递 token。
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,150 @@
|
||||
# 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 返回一批 update,offset 前进。
|
||||
4. **handler**:非 safew channel → 400;缺 token → 400。
|
||||
|
||||
Watcher 用可注入的 poll 函数 + 假 cache,不断言真实 30s 阻塞。
|
||||
|
||||
## Success criteria
|
||||
|
||||
- 两个接口返回已监控群,`id` 为字符串
|
||||
- 默认只有群;`q` 可过滤
|
||||
- 后台 `timeout=30` 持续写入 Redis;列表接口不卡 30 秒
|
||||
- 仓库与 Redis key 中不出现明文 token
|
||||
Reference in New Issue
Block a user