bd8a9ff96d
通过 getUpdates 将群写入 Redis,供创建/编辑渠道时选择 chat_id,避免前端重复传递 token。
4.6 KiB
4.6 KiB
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
- 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。
- 解析:update 含 group + private → 只留下 group;
id为字符串"10000778141"。 - 过滤:
q=测试命中 title;q=10000778141命中 id。 - getUpdates mock:httptest 返回一批 update,offset 前进。
- handler:非 safew channel → 400;缺 token → 400。
Watcher 用可注入的 poll 函数 + 假 cache,不断言真实 30s 阻塞。
Success criteria
- 两个接口返回已监控群,
id为字符串 - 默认只有群;
q可过滤 - 后台
timeout=30持续写入 Redis;列表接口不卡 30 秒 - 仓库与 Redis key 中不出现明文 token