# 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 前缀);换图会更新 hash,CDN 用 `?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 job;Worker 进程内 `BroadcastRunner` 限速发送(`BROADCAST_INTERVAL_MS`,默认 200ms;生产可调到 50–100ms 加速,注意 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` | 应被轮询的 bot(active + 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% 等价。 - **范围** 0–500,默认 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:`,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___`),权重不会跟随到新 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)。