Files
WeChat-AI/docs/admin-api.md
T

281 lines
20 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.
# API 概览(Redis + LINUX DO OAuth
Base: `http://127.0.0.1:8787`
Auth: **Cookie 会话**OAuth 登录后 `wa_session`),`credentials: include`
## 认证
| Method | Path | 说明 |
|--------|------|------|
| GET | `/api/v1/auth/config` | `{ oauthEnabled, provider, localAuthEnabled, inviteRequiredForLocal, passwordMinLength }` |
| GET | `/api/v1/auth/login` | 跳转 LINUX DO OAuth |
| GET | `/api/v1/auth/callback` | OAuth 回调(新 OAuth 用户**不需要**邀请码) |
| POST | `/api/v1/auth/register` | 本地注册:`{ inviteCode, username, password, name? }` → cookie |
| POST | `/api/v1/auth/password-login` | 用户名密码登录:`{ username, password }` → cookie |
| GET | `/api/v1/auth/invite/:code` | 预检邀请码(不消费) |
| POST | `/api/v1/auth/logout` | 退出 |
| GET | `/api/v1/auth/me` | 当前用户(含 `authProvider``isSuperAdmin`,不含密码) |
## 邀请(登录用户)
| Method | Path | 说明 |
|--------|------|------|
| GET | `/api/v1/me/invites` | pending 列表 + 配额 + 邀请链接 |
| POST | `/api/v1/me/invites` | 生成一次性码;受「每 X 小时 N 个」滑动窗口限制 |
| DELETE | `/api/v1/me/invites/:code` | 撤销未使用码 |
邀请链接形态:`{PUBLIC_BASE_URL}/app?invite={CODE}`(打开后自动填入注册表单)。
## 用户
| Method | Path | 说明 |
|--------|------|------|
| GET | `/api/v1/me/bots` | 我的机器人 |
| POST | `/api/v1/me/bots/login/start` | 扫码添加机器人(新建 botId) |
| POST | `/api/v1/me/bots/:botId/relogin/start` | **重新扫码绑定**(更新 token,保留 peers/记忆/分配) |
| GET | `/api/v1/me/bots/login/:sessionId` | 扫码状态 |
| DELETE | `/api/v1/me/bots/:botId` | 删除自己的机器人(含 Redis token |
| GET | `/api/v1/me/peers` | 私聊用户 |
| POST | `/api/v1/me/peers/approve` | 批准 |
| PUT | `/api/v1/me/assignments` | 分配人设(须在库中) |
| GET | `/api/v1/me/personas` | 我的库 + 我创建的 |
| POST | `/api/v1/me/personas/:id/add` | 添加人设到库 |
| DELETE | `/api/v1/me/personas/:id` | 从库移除 |
| GET | `/api/v1/me/memories?botAccountId=&peerId=&personaId?` | 长期记忆;无 personaId 时返回 `{total,groups}` |
| POST | `/api/v1/me/memories/reset` | `{ botAccountId, peerId, personaId? }` 清空记忆 |
| DELETE | `/api/v1/me/memories` | `{ botAccountId, peerId, personaId, memoryId }` 删单条 |
| GET | `/api/v1/me/wechat-bind` | 当前 LINUX DO ↔ 微信绑定状态(`reachable` 表示是否有 context_token |
| POST | `/api/v1/me/wechat-bind/code` | 生成 6 位绑定码(微信 `/绑定 CODE` |
| DELETE | `/api/v1/me/wechat-bind` | 解除绑定并结束相关用户对话 |
| GET | `/api/v1/me/blocks` | 我的黑名单(LINUX DO 用户) |
| POST | `/api/v1/me/blocks` | `{ username }``{ userId }` 拉黑 |
| DELETE | `/api/v1/me/blocks/:userId` | 取消拉黑 |
## 人设广场
| Method | Path | 说明 |
|--------|------|------|
| GET | `/api/v1/square/personas?q=&page=&limit=&sort=heat\|use\|recent\|name` | 搜索公开人设(默认 sort=heat;字段含 useCount / assignCount / forkCount / heatScore / forkedFrom |
| GET | `/api/v1/square/personas/:id` | 详情(含 systemPrompt |
| POST | `/api/v1/square/personas` | 发布(public/private |
| POST | `/api/v1/square/personas/:id/fork` | Fork 为当前用户私有草稿(`PERSONA_FORK_ENABLED`,默认开) |
| PUT | `/api/v1/square/personas/:id` | 作者更新 |
| DELETE | `/api/v1/square/personas/:id` | 作者软删除 |
| GET | `/api/v1/square/stickers?q=&page=&limit=&sort=use\|recent\|name` | 已审核公开表情包(默认 sort=use) |
| GET | `/api/v1/square/stickers/:id` | 详情(`imageUrl` 对公开已审为 CDN 路径) |
| GET | `/api/v1/square/stickers/:id/image` | 预览图(**鉴权**;私有/待审/作者预览) |
| GET | `/cdn/s/:id?v={content_hash}` | **公开 CDN**(无 Cookie):仅 `public`+`approved`+`enabled`;长缓存 immutable |
| POST | `/api/v1/square/stickers` | 投稿(public→待审 / private 自用) |
| PUT | `/api/v1/square/stickers/:id` | 作者更新(公开改动回待审) |
| DELETE | `/api/v1/square/stickers/:id` | 作者软删除 |
| GET | `/api/v1/me/stickers` | 我的库 + 我创建的 |
| POST | `/api/v1/me/stickers/:id/add` | 加入表情库 |
| DELETE | `/api/v1/me/stickers/:id` | 从库移除(对称递减 use_count |
## 网页试聊
不经微信,在 `/app` 内限量体验公开/可用人设。计入用户 Token 用量;会话存 Redis TTL。
| Method | Path | 说明 |
|--------|------|------|
| POST | `/api/v1/try-chat/sessions` | `{ personaId, botName? }``{ sessionId, persona, remainingToday, expiresInSec }` |
| POST | `/api/v1/try-chat/sessions/:sessionId/messages` | `{ text }``{ messages[], remainingToday, remainingSession, usage }` |
| DELETE | `/api/v1/try-chat/sessions/:sessionId` | 结束会话 |
环境变量:`TRY_CHAT_ENABLED``TRY_CHAT_MAX_USER_MSGS_PER_DAY``TRY_CHAT_MAX_USER_MSGS_PER_SESSION``TRY_CHAT_SESSION_TTL_SEC``TRY_CHAT_MAX_HISTORY`
热度公式:`heatScore = use_count*2 + assign_count*5 + fork_count*3`(分配仅在 persona 变化时 +1)。
## 管理员
| Method | Path | 说明 |
|--------|------|------|
| GET | `/api/v1/admin/dashboard` | 仪表盘 + 今日 Token + **全舰队** workerStats + nodes 摘要 + Redis |
| GET | `/api/v1/admin/system` | 系统健康 / doctor / **全舰队** workerStats / nodes 摘要 |
| GET | `/api/v1/admin/nodes` | **仅超管** 部署节点列表(含 fenced) |
| POST | `/api/v1/admin/nodes/:workerId/force-offline` | **仅超管** 强制下线 |
| POST | `/api/v1/admin/nodes/:workerId/clear-fence` | **仅超管** 解除封锁 |
| POST | `/api/v1/admin/nodes/:workerId/weight` | **仅超管** 设负载权重 `{ weight: 0..500 }` |
| DELETE | `/api/v1/admin/nodes/:workerId/weight` | **仅超管** 恢复默认权重(100% |
| POST | `/api/v1/admin/nodes/weights/prune` | **仅超管** 立即清理失效权重(平时会自动过期) |
| POST | `/api/v1/admin/workers/restart-all` | **仅超管** 批量恢复 pollable 并尝试认领(有 token 的 active bot |
| POST | `/api/v1/admin/workers/stop-all` | **仅超管** 暂停全部可轮询 bot(写 pause 标记,不删账号) |
| POST | `/api/v1/admin/system/seed-personas` | 幂等补种官方人设 |
| GET | `/api/v1/admin/memories?botAccountId=&peerId=` | 查看 peer 长期记忆(按人设分组) |
| DELETE | `/api/v1/admin/memories` | `{ botAccountId, peerId, personaId, memoryId }` 删单条 |
| POST | `/api/v1/admin/memories/reset` | `{ botAccountId, peerId, personaId? }` 清记忆 |
| POST | `/api/v1/admin/messages/clear` | `{ botAccountId, peerId }` 清除短期对话历史(保留长期记忆) |
| GET | `/api/v1/admin/peers?status=unapproved\|approved\|all` | 全站私聊 peer(默认待批准) |
| POST | `/api/v1/admin/peers/approve` | `{ botAccountId, peerId }` 管理员代批 |
| POST | `/api/v1/admin/peers/approve-all` | 批准全部待批准 peer |
| GET | `/api/v1/admin/usage?day=` | 按日用量 |
| GET | `/api/v1/admin/usage?days=7` | 近 N 日用量列表 |
| GET | `/api/v1/admin/users` | 用户全量列表(含 botCount、isBanned、authProvider、isSuperAdmin |
| GET | `/api/v1/admin/users/:id` | 用户详情 + 名下机器人 |
| PATCH | `/api/v1/admin/users/:id` | **仅超管** `{ isAdmin }` 授予/撤销管理员(不可撤自己/最后一位;**不可撤超管**) |
| POST | `/api/v1/admin/users/:id/ban` | `{ reason?, cascadeBots? }` 封禁(踢 session;默认级联停用 bot**不可封超管**) |
| POST | `/api/v1/admin/users/:id/unban` | 解封(不自动启 bot |
| DELETE | `/api/v1/admin/users/:id?confirm=username` | 删除用户(级联删 bot/session/索引;**不可删超管** |
> **超管**:仍为管理员的用户中 `created_at` 最早者(系统首位管理员)。对超管的撤销管理 / 封禁 / 删除一律拒绝(`cannot_revoke_super_admin` / `cannot_ban_super_admin` / `cannot_delete_super_admin`)。
| GET | `/api/v1/admin/settings/invites` | 邀请策略(配额窗口小时 / 每窗口上限 / TTL / pending |
| PATCH | `/api/v1/admin/settings/invites` | 更新邀请策略(Redis 覆盖 env 默认) |
| GET | `/api/v1/admin/settings/runtime` | **仅超管** 运行时配置:分组 + 全部项(当前值 / env 默认 / 是否已覆盖 / 是否需重启)+ 警告 |
| PATCH | `/api/v1/admin/settings/runtime` | **仅超管** `{ patch: {key: value}, reset: [key] }` 写 Redis 覆盖 |
| POST | `/api/v1/admin/settings/runtime/reset` | **仅超管** 删除全部覆盖,回到 `.env` |
> 运行时配置详见 [`docs/runtime-settings.md`](./runtime-settings.md)`.env` 是默认值,Redis 存覆盖,各节点 5 秒内同步。密钥字段 GET 只返回掩码;PATCH 传空 = 不改,传 `-` = 清空。
| GET | `/api/v1/admin/bots` | 全部机器人(owner、worker、hasToken、peer 计数;前端分页,后端批量读) |
| GET | `/api/v1/admin/bots/:botId` | 机器人详情 + peers |
| PATCH | `/api/v1/admin/bots/:botId` | 改名 / `{ status: active\|inactive }` 启停 |
| POST | `/api/v1/admin/bots/:botId/stop-worker` | **仅超管** 停止 Worker 轮询 |
| POST | `/api/v1/admin/bots/:botId/start-worker` | **仅超管** 启动/重启 Worker(需已有 Redis token |
| DELETE | `/api/v1/admin/bots/:botId` | 删除机器人 |
| GET | `/api/v1/admin/bots/:botId/send-targets` | **仅超管** 该 bot 的 peers + `hasContextToken`(广播勾选) |
| POST | `/api/v1/admin/broadcast` | **仅超管** 创建广播任务或 `preview:true` 仅预估人数 |
| GET | `/api/v1/admin/broadcast?limit=` | **仅超管** 最近广播任务列表 |
| GET | `/api/v1/admin/broadcast/:id` | **仅超管** 任务详情与进度 |
| POST | `/api/v1/admin/broadcast/:id/cancel` | **仅超管** 取消 pending/running 任务 |
| GET | `/api/v1/admin/personas?q=&includeDisabled=1` | 人设列表 |
| GET | `/api/v1/admin/personas/:id` | 详情(含 prompt |
| POST | `/api/v1/admin/personas` | 创建官方人设(可带 tags / isDefault |
| PUT | `/api/v1/admin/personas/:id` | 更新元信息 / prompt |
| POST | `/api/v1/admin/personas/:id/publish` | 发布新版本 prompt |
| POST | `/api/v1/admin/personas/:id/takedown` | 下架非官方人设 |
| POST | `/api/v1/admin/personas/:id/restore` | 恢复已下架 |
| POST | `/api/v1/admin/personas/:id/set-default` | 设为默认人设 |
| GET | `/api/v1/admin/audit?limit=` | 审计 |
| GET | `/api/v1/admin/stream/recent?limit=&types=&full=` | **仅超管** 活动数据流 backlog(消息 / Worker / LLM`full=1` 消息预览加长;`types=message,redis,worker,llm` |
| GET | `/api/v1/admin/stream?types=&full=&heartbeat=` | **仅超管** SSE 实时数据流(`text/event-stream`;Redis 命令为进程内抽样,不写 Redis backlog |
| GET | `/api/v1/admin/stickers?q=&enabled=` | 表情包列表 |
| GET | `/api/v1/admin/stickers/:id` | 表情详情 |
| GET | `/api/v1/admin/stickers/:id/image` | 预览原图(admin cookie |
| POST | `/api/v1/admin/stickers` | 上传:`{ slug, displayName, description?, tags?, mime?, dataBase64, enabled? }` |
| PUT | `/api/v1/admin/stickers/:id` | 更新元信息 / 可选换图 |
| DELETE | `/api/v1/admin/stickers/:id` | 删除 meta + 本地文件 |
页面:`/` 功能介绍 · `/app` 用户中心 · `/admin` 管理后台 · `/og.jpg` 社交分享图
### 表情包广场 / 回图
- **元数据 + 图片二进制均在 Redis**(`wa:sticker:{id}` / `wa:sticker:{id}:blob`)。
- Meta 含 **`content_hash`**blob sha256 前缀);换图会更新 hashCDN 用 `?v=` 缓存破坏。
- **公开已审图**可走无登录 `GET /cdn/s/:id?v=hash``public, max-age=31536000, immutable`),供 Cloudflare 边缘缓存。
- **公开投稿必须管理员审核**`pending``approve` 后进广场与 CDN);私有仅作者 bot 可用。
- 上传强制安全扫描:禁 SVG、magic/mime 校验、脚本/PHP/polyglot 尾部检测(非杀软)。
- 运行时按 **机器人主人** 的表情库 + 自建可用表情注入 LLM;`{"messages":["文字",{"type":"sticker","slug":"..."}]}`
- Admin`GET /admin/stickers?status=pending``POST .../approve|reject|takedown|restore`
- 环境变量:`STICKER_SEND_ENABLED``MAX_STICKERS_PER_REPLY``STICKER_MAX_BYTES`(默认 2MB)。
- Cloudflare 部署:见 `docs/cloudflare.md`
### Worker 相关配置(与 API 同进程 / 单镜像)
| 环境变量 | 默认 | 说明 |
|----------|------|------|
| `WORKER_ENABLED` | `true` | 是否在本进程跑 iLink 轮询与回复 |
| `MAX_BOTS_PER_WORKER` | `500` | 本进程最多同时 long-poll 的 bot 数 |
| `REPLY_CONCURRENCY` | `16` | 进程内 LLM/发送并发 |
| `INBOX_MAX_LEN` | `20000` | 入站队列深度上限 |
### 管理后台广播(纯文本推送)
- **仅纯文本**;不经 LLM、不写短期对话 / 长期记忆。
- **必须有 `context_token`**(peer 曾给该 bot 发过消息);无 token 的目标在展开时跳过。
- **不按批准过滤**:未批准但可触达的 peer 也会进入队列。
- 异步任务:`POST` 只写 Redis jobWorker 进程内 `BroadcastRunner` 限速发送(`BROADCAST_INTERVAL_MS`,默认 200ms;生产可调到 50100ms 加速,注意 iLink 限流)。
- `scope`
- `all_bots` — 全部机器人下可触达 peer
- `bots``botIds[]` 指定机器人
- `targets` — 显式 `{ botId, peerId }[]`
- `POST` body 带 `preview: true` 时只返回 `{ deliverable, skippedNoToken, missingBots }`,不创建任务。
-`WORKER_ENABLED=true`(默认)才会实际发送;否则任务保持 `pending`
- 环境变量:`BROADCAST_INTERVAL_MS``BROADCAST_MAX_TEXT`(默认 2000)、`BROADCAST_HISTORY`(默认 100)。
### `workerStats` 字段(dashboard / system
**全舰队**租约汇总(`scope: "fleet"`):
| 字段 | 说明 |
|------|------|
| `scope` | `fleet`(管理页)或 `local` |
| `leasedLocal` / `leasedFleet` | 全舰队正在 poll 的 bot 数(fleet 模式下两者相同) |
| `maxBots` | 全舰队容量合计(各节点 maxBots 之和) |
| `pollable` | 应被轮询的 botactive + token + 未 pause |
| `nodesOnline` / `nodesTotal` | 在线 / 注册部署节点数 |
| `atCapacity` | 任一点触顶或舰队已满 |
| `inboxDepth` 等 | **当前应答节点本机** inbox/任务计数(非全舰队加总) |
### 超管(super admin
`created_at` 最早的 `is_admin` 用户。节点管理 API 与后台「节点」页仅超管可用;普通管理员仍可看 Workers 全舰队 bot 列表。
### `nodes` / `GET /admin/nodes`fleet
Redis 心跳注册的**部署进程**(多机时每台一条),**不包含**源站公网 URL(用户统一走主域名;源站 IP 只在 CF Worker `ORIGINS`)。
| 字段 | 说明 |
|------|------|
| `id` | `WORKER_ID` |
| `hostname` / `pid` | 主机与进程 |
| `botCount` / `maxBots` / `leasedCount` / `leasedBotIds` | 容量与租约 |
| `label` / `region` / `version` | 可选运维标签;`version` 为进程 appVersion |
| `online` / `isSelf` | 心跳是否新鲜;是否为当前应答节点 |
| `fenced` / `fenceReason` / `fencedAt` / `fencedBy` | 管理员强制下线封锁(进程可仍在跑 HTTP,但不 poll) |
| `weight` / `weightOverride` / `weightUpdatedAt` / `weightBy` | 负载权重(百分比,默认 100)与是否管理员覆盖 |
| `weightLastSeenAt` | 舰队最后一次确认该节点存活的时间(自动过期计时起点) |
| `targetShare` | 按权重应分配的 bot 数(受 `maxBots` 约束);离线/封锁节点为 `null` |
| `update` | OTA`outdated` / `desiredVersion` / `status` / `progress` / `error` |
| `startedAt` / `updatedAt` | 启动与心跳时间 |
响应另含 `release`(通道当前版本摘要)、`appVersion``otaEnabled``weights``weightTotal`(在线节点权重和)、`weightLimits``weightTtlSec`(权重自动清理宽限期)、`pollableTotal``rebalanceEnabled``rebalanceIntervalSec`
**强制下线说明:** 不停止 Docker/OS 进程;目标 `WORKER_ID` 在 fence 清除前不会 re-register / claim。租约由其他节点接管。从 CF Worker `ORIGINS` 移除源站 IP 是流量侧下线,与本 API 独立。
### 节点负载权重(Worker 负载调节,仅超管)
后台「节点」页 → **负载权重**列 → 滑杆 / 预设 / 手填百分比,保存前可预览各节点调整后的目标 bot 数。
- **相对值**:在线节点按各自权重占比分摊 `wa:bots:pollable`。A=200%、B=100% → 2:1;两个节点都是 200% 与都是 100% 等价。
- **范围** 0500,默认 100。写入 100 等同删除覆盖(`wa:workers:weights` 只保存真正调过的节点)。
- **0% = 腾空**:节点保持在线心跳、继续处理 HTTP,但不再认领 bot,并会把已持有的租约全部释放(此时忽略 `REBALANCE_SLACK`,能真正归零)。与「强制下线」不同:不写 fence,改回非 0 即刻恢复。
- **上限仍然生效**:实际数量取 `min(权重份额, MAX_BOTS_PER_WORKER)`。权重只在容量内重新分配,不能突破单进程上限。
- **单节点例外**:只有一个在线节点时它拿全部(权重是节点之间的比例)。全舰队都设 0% 时退化为均分,避免所有 bot 停止轮询。
- **生效时机**:写入后 publish `wa:worker:wake`,各节点丢弃权重缓存;认领在下一个 `LEASE_RENEW_SEC` 生效,多余租约按 `REBALANCE_INTERVAL_SEC` / `REBALANCE_MAX_PER_TICK` 逐步释放。`REBALANCE_ENABLED=false` 时已认领的租约不迁移,权重只影响后续认领(后台会提示)。
#### 存储与自动清理
存储在 Redis,不进 env,节点重启不丢失。两个 hash 分开写,互不干扰:
| Key | 内容 | 写入方 |
|-----|------|--------|
| `wa:workers:weights` | `workerId` → JSON `{ percent, updatedAt, byUserId, byUsername }` | 仅管理员操作 |
| `wa:workers:weights:seen` | `workerId` → 最后一次确认存活的 ISO 时间 | 仅 GC |
> 拆成两个 hash 是必需的:GC 若为了盖时间戳而回写整条权重记录,会把管理员同一时刻的改动覆盖掉(read-modify-write 丢失更新)。
**节点消失后权重会自动删除**,无需人工干预:
- 判活以心跳 meta`wa:worker:<id>`TTL `WORKER_STALE_SEC`)为准,不看时间戳——所有版本的进程都会写 meta,因此 OTA 混版滚动期间旧版本节点不会被误判为消失。
- 任一在线节点每 5 分钟扫一次(`HSET`/`HDEL` 幂等,多节点并发无害),给还活着的节点刷新时间戳,给已消失且超过宽限期的删除记录,并顺带清掉没有对应权重的孤儿时间戳。权重 hash 为空时完全不产生 Redis 调用。
- 宽限期 `WORKER_WEIGHT_TTL_SEC`(默认 3600 秒,下限 60 秒)必须长于一次重启 / OTA 应用,否则每次发版都会重置调节。
- **已封锁(force-offline)节点永不过期**:封锁是临时的,解除后权重仍在。
- 「立即清理权重」按钮跳过宽限期,立刻删除所有无心跳且未封锁节点的权重(`POST /admin/nodes/weights/prune`)。
> **注意**:未在环境变量固定 `WORKER_ID` 时,进程每次重启都会生成新的随机 ID`w_<host>_<pid>_<rand>`),权重不会跟随到新 ID——这也正是必须自动清理的原因,否则 hash 会随重启次数无限增长。需要权重长期生效,请为每个节点固定 `WORKER_ID`。
### OTA 发布与节点更新(仅超管)
| Method | Path | 说明 |
|--------|------|------|
| GET | `/api/v1/admin/releases/current` | 通道当前 release |
| GET | `/api/v1/admin/releases` | 最近版本列表 |
| POST | `/api/v1/admin/releases` | `mode=blob\|publish\|pack` 上传/注册 |
| POST | `/api/v1/admin/releases/current` | `{ version }` 切换通道版本 |
| POST | `/api/v1/admin/nodes/:workerId/update` | 对该节点下发更新 job |
| POST | `/api/v1/admin/nodes/update-outdated` | 批量更新落后在线节点 |
| GET | `/api/v1/admin/nodes/:workerId/update-status` | 单节点进度 |
CLI 打包:`pnpm release:pack` / `pnpm docker:build`(见 `docs/docker.md`)。
发布通道:`/admin` → 部署节点 → **上传通道包**`files.json`,浏览器超管会话)→ 节点「更新」。
另有公开探活:`GET /health`(进程)、`GET /health/ready`(含 Redis,供 LB)。