server: 平台抽象层+bilibili、服务端加密凭据库、角色中间件;web: 精简页面/路由、macOS 客户端(Swift)与多份方案文档;移除误入库的编译产物

This commit is contained in:
Qiufeng
2026-08-21 10:53:32 +08:00
parent 0daa9782c9
commit 3585c39bab
103 changed files with 5842 additions and 3197 deletions
+34 -140
View File
@@ -1,161 +1,55 @@
# EveryPublish 服务端接口文档(v1)
# EveryPublish Web API(Web-only V1)
> 基地址:`/api/v1` · 统一响应:`{"code":0,"message":"ok","data":{...}}`,code!=0 为业务错误。
> 认证:`Authorization: Bearer <accessToken>`(15min,前端自动用 refresh 轮换)。
Base URL:`http://127.0.0.1:8090/api/v1`。成功响应为 `{code:0,message:"ok",data:...}`,失败响应包含业务 `code/message`。
## 业务错误码
| code | 含义 |
|---|---|
| 1001 | 参数不合法 |
| 1002 | 未登录/令牌无效 |
| 1003 | 无权限 |
| 1004 | 资源不存在 |
| 2001 | 邮箱已注册 |
| 2002 | 邮箱或密码错误 |
| 2003 | 刷新令牌无效 |
| 2004 | 已是成员 |
| 2005 | 邀请链接无效或过期 |
| 1006 | 需要两步验证(2FA,默认关) |
| 3001-3005 | 业务冲突(账号不可删/已绑定/挑战已结束/链接过期/任务状态不允许) |
## 认证 /auth
## 认证
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /auth/register | 注册 {email,password,nickname} → 返回令牌对+用户+默认工作空间 |
| POST | /auth/login | 登录 {email,password} → 令牌对+用户 |
| POST | /auth/refresh | {refreshToken} → 轮换新令牌对(旧 refresh 立即吊销) |
| POST | /auth/logout | 登出(吊销 refresh) |
| GET | /auth/me | 当前用户+工作空间+角色 |
| POST | `/auth/register` | 注册并返回 token |
| POST | `/auth/login` | 登录并返回 access/refresh |
| POST | `/auth/refresh` | refresh 轮换 |
| POST | `/auth/logout` | 登出 |
| POST | `/auth/password` | 修改当前用户密码 |
| GET | `/auth/me` | 当前用户 |
| POST | `/auth/switch-workspace/:id` | 切换到本人所属工作空间并签发新 token |
## 工作空间 /workspaces(登录)
## 核心业务
| 方法 | 路径 | 说明 |
| 模块 | 路径 | 说明 |
|---|---|---|
| POST | /workspaces | 创建 {name}(创建者为 owner) |
| GET | /workspaces | 我所在的工作空间列表 |
| PUT | /workspaces/:id | 改名(owner/admin) |
| 工作区/成员 | `/workspaces`、`/members` | 工作区与角色权限 |
| 账号 | `/accounts`、`/accounts/:id/bind`、`/accounts/:id/credentials`、`/accounts/:id/check`、`/accounts/:id/unbind` | 网页本机账号、扫码登录、凭据导入和登录检查 |
| 素材 | `/materials`、`/materials/:id/url`、`/files/:token` | multipart、SHA-256 去重、一次性下载直链和 `?inline=1` 预览 |
| 任务 | `/tasks`、`/tasks/:id/events`、`submit`、`approve`、`reject`、`retry`、`cancel` | 状态机、时间线和审核 |
| 挑战 | `/challenges`、`/challenges/:id/solve`、`resend`、`suspend` | QR/验证码/APP 确认 |
| 通知 | `/notifications`、`/notifications/:id/read`、`read-all` | 站内通知 |
| 审计 | `/audit-logs` | 业务操作记录 |
## 成员 /members(登录)
### 账号绑定返回值
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /members | 成员列表(含 email/nickname) |
| POST | /members/invite | 邀请 {email,role} → {token,link}(owner/admin;role: admin/operator/reviewer/viewer) |
| POST | /members/join | 接受邀请 {token}(须与登录邮箱一致) |
| PUT | /members/:id/role | 改角色 {role}(不可改 owner) |
| DELETE | /members/:id | 移除(不可移除 owner) |
`POST /accounts/:id/bind` 对同一账号的已有 active challenge 幂等,返回 `challengeId、qrUrl、qrToken、prompt、expiresAt`。网页显示 `qrUrl`,服务端后台轮询平台状态;用户使用目标平台手机客户端扫码,不生成 EveryPublish 客户端配对码。真实二维码由适配器确认,不能调用 `/challenges/:id/solve` 伪造完成;只有显式 `mock://` 联调挑战允许使用 `value` 解决。
## 账号台账 /accounts(登录)
`POST /accounts/:id/credentials` 接收 `{ "cookies": "..." }`,仅在 `EXECUTOR_MODE=web` 且平台适配器已注册时可用。成功后只返回账号状态;Cookie 不进入响应、审计详情或 WebSocket payload。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /accounts?platform=&status=&page=&size= | 台账列表(状态:unbound/binding/active/suspended/expired) |
| POST | /accounts | 新增 {platform(douyin/kuaishou/xiaohongshu/bilibili),accountName,avatarUrl?,ipProfile?} |
| PUT | /accounts/:id | 改资料(名称/头像/IP 画像) |
| DELETE | /accounts/:id | 删除(仅 unbound) |
| POST | /accounts/:id/bind | 发起绑定 → 创建挑战记录(status→binding)→ {challengeId} |
`POST /accounts/:id/check` 调用平台适配器的凭据检查;有效时更新 `active/lastActiveAt`,失效时更新 `expired/lastError`,不会返回 Cookie。
## 素材 /materials(登录)
写操作按 workspace member role 限制:账号资料、素材和任务由 owner/admin/operator 处理;解绑、删除和工作区成员管理需要 owner/admin;审核由 reviewer/admin 处理;viewer 只读。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /materials?kind=&group=&page=&size= | 素材列表 |
| POST | /materials | multipart 上传:file 字段 + 可选 group/tags;流式 sha256,同工作区去重(dedup:true 返回已有素材) |
| GET | /materials/:id/url | 生成签名直链(10 分钟、一次性)→ {url,expiresAt} |
| DELETE | /materials/:id | 删除素材及文件 |
| GET | /files/:token | 凭 token 下载文件(无鉴权,token 即凭证;一次性) |
## Browser WebSocket
## 任务 /tasks(登录;状态机)
`GET /ws/browser?token=<accessToken>`,浏览器连接后接收:
```mermaid
flowchart LR
D[draft] -->|submit| P[pending_review]
P -->|approve| Q[queued] -->|dispatch| DP[dispatched] -->|start| R[running]
R -->|success| S[success]
R -->|fail| F[failed] -->|retry| Q
R -->|suspend| SU[suspended] -->|resume| Q
P -->|reject| RJ[rejected] -->|resubmit| P
D -->|cancel| C[cancelled]
Q -->|cancel| C
DP -->|cancel| C
R -->|cancel| C
SU -->|cancel| C
```
- `task.status`:任务状态/结果/错误。
- `challenge.status`:挑战创建、解决、过期。
- `notification.new`:新通知。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /tasks?status=&schedule=&page=&size= | 任务列表 |
| POST | /tasks | 新建草稿 {title,content?,tags?,accountIds[],materialIds[],scheduleAt?(unix ms),priority?(1-10)} |
| GET | /tasks/:id | 详情 |
| PUT | /tasks/:id | 编辑(仅 draft/rejected) |
| DELETE | /tasks/:id | 删除(仅 draft) |
| POST | /tasks/:id/submit | 提交审核 |
| POST | /tasks/:id/approve | 审核通过 → queued(通知创建人) |
| POST | /tasks/:id/reject | 驳回 {note}(通知创建人) |
| POST | /tasks/:id/resubmit | 驳回后重提 |
| POST | /tasks/:id/retry | 失败重试 → queued |
| POST | /tasks/:id/cancel | 取消 |
WebSocket 不是首屏数据源;页面必须先 REST 拉取,断线时通过 REST 轮询兜底。
## 挑战 /challenges(登录)
## 本地 mock 执行
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /challenges?status=&accountId= | 挑战列表(过期自动置 expired) |
| POST | /challenges/:id/solve | 人工完成 {value?}(验证码/APP 确认;扫码由 Agent 完成) |
| POST | /challenges/:id/resend | 一键重发(expired/suspended → active,重置 30min) |
| POST | /challenges/:id/suspend | 挂起 |
设置 `EXECUTOR_MODE=mock`(默认)后,审核通过的任务由 Go server 本机执行,状态依次为 `queued → dispatched → running → success`,结果 URL 使用 `mock://` 协议,仅用于自动化联调。
## 通知 /notifications(登录)
设置 `EXECUTOR_MODE=web` 后,已注册的平台适配器在 Go server 所在设备执行。当前已接入 B 站:二维码轮询、workspace/account 隔离加密凭据、UPOS 分片上传和投稿;其它未接入平台保留明确的 mock fallback,不能把 mock URL 当作真实平台结果。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /notifications?kind= | 我的通知 + unread 计数 |
| POST | /notifications/:id/read | 标记已读 |
| POST | /notifications/read-all | 全部已读 |
## 审计 /audit-logs(登录)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /audit-logs?userId=&action=&page=&size= | 审计日志(中间件自动埋点:登录/登出/增删改/审核/挑战等) |
## 设备配对 /agent
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| POST | /agent/pair-code | Bearer | 生成配对码(6 位,5 分钟一次性) |
| POST | /agent/pair | 无(凭码) | 配对 {code,deviceName,os,version,publicKey(Ed25519 base64)} → {deviceId} |
| GET | /agent/devices | Bearer | 设备列表(在线状态) |
## WSS 双通道
### Agent 通道 `/ws/agent`(Ed25519 设备签名,无 JWT)
| 方向 | 消息 | 说明 |
|---|---|---|
| C→S | `hello` | {deviceId,nonce,ts,sig(base64, 签名原文 nonce+`\|`+ts),version} |
| S→C | `hello.ack` | {serverNonce,sessionId,serverTs,sig} |
| C→S | `heartbeat`(10-30s) / S→C `heartbeat.ack` | 保活 |
| S→C | `task.push` | 任务下发 {taskId,platform,accountId,title,content,tags,materialUrls(一次性签名直链),priority} |
| C→S | `task.ack` | {taskId,accept,reason?}(accept→running) |
| C→S | `task.result` | {taskId,status(success/failed),publishedUrl,receipts,error,finishedAt} |
| S→C | `challenge.new` | 绑定扫码挑战 {challengeId,accountId,platform,kind,qrToken,qrUrl,prompt,expiresAt} |
| C→S | `challenge.ack` / `challenge.solve` | 应答 / 完成(solve 后账号→active) |
### 浏览器通道 `/ws/browser?token=<JWT>`(只读订阅)
| 事件 | 说明 |
|---|---|
| `task.status` | {taskId,status,errorMessage,publishedUrls}(下发/执行/成功/失败实时推送) |
| `agent.status` | {deviceId,online} |
| `challenge.status` | {challengeId,status,accountId} |
> 实测:任务下发延迟(approve→task.push)≈ 28.7ms(本机,硬指标 <300ms)。
## 健康检查
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /health | 服务健康(db/redis 状态) |
B 站 adapter 的 `BILIBILI_MEMBER_BASE`、`BILIBILI_PASSPORT_BASE` 和 `BILIBILI_UPOS_SCHEME` 可在本地 stub 联调时覆盖;生产环境保持官方地址并遵守平台规则。