EternalAI/docs/plans/2026-06-21-001-feat-admin-r...

518 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.

# Plan: 管理员审核 + Hermes 同步工作流
**Status:** active
**Created:** 2026-06-21
**Origin:** docs/brainstorms/2026-06-20-hermes-cross-machine-deploy-requirements.md (演进)
**Plan depth:** Standard
---
## Summary
实现「管理员审核 + 管理员发起 Hermes 同步」的新工作流。角色创建后进入待审核状态管理员后台审核通过后填写同步参数profile 名、主 model key/服务商、多媒体 model key/服务商、定时任务开关EternalAI 用一次性 sync_token 向 Hermes 发起同步请求Hermes 回调拉取文件并创建 profile返回绑定二维码。测试范围限定为鉴权和文件拉取无真实 Hermes用 mock 端点验证)。
---
## Problem Frame
当前流程:创作者创建角色后直接上架,用户自行用 API Key + curl 拉取配置到 Hermes。存在以下问题
1. 缺少内容审核环节,任何人设都可直接发布
2. 同步由用户手动操作,无法集中管控
3. Hermes 配置参数model key、服务商等分散在用户侧不一致
新流程:管理员集中审核 + 管理员发起同步 + Hermes 回调拉取 + 二维码绑定。
---
## Requirements
### 功能需求
- **R1**: 角色创建后状态为 `pending_review`,需管理员审核才能上架
- **R2**: 独立 Admin 表,管理员有独立登录入口
- **R3**: 管理员后台可查看待审核列表、角色详情,通过/驳回
- **R4**: 审核通过后管理员填写同步参数并发起同步
- **R5**: EternalAI 用一次性 sync_tokenHMAC SHA-256 对称密钥)向 Hermes POST 同步请求
- **R6**: Hermes 用 sync_token 回调 EternalAI 拉取 SOUL.md 和 config.yaml
- **R7**: Hermes 返回二维码 URLEternalAI 存储并展示给管理员和创作者
- **R8**: Hermes webhook URL 全局配置,同步时可覆盖
- **R9**: 测试范围鉴权admin 登录、sync_token 验证)+ 文件拉取mock Hermes 回调)
### 非功能需求
- sync_token 5 分钟过期,一次性消费
- Hermes 回调拉取端点同时支持 sync_token 和现有 API Key 认证
- 现有用户侧 API Key 拉取流程保留不变
---
## Key Technical Decisions
### KTD1: 独立 Admin 表
Admin 与 User 分离,独立登录接口,不混用 JWT。
**理由**: 用户选择。管理员权限边界清晰,避免 User 表 isAdmin 字段的权限提升风险。
### KTD2: sync_token 用 HMAC SHA-256 对称密钥
EternalAI 和 Hermes 预共享 `SYNC_SECRET`sync_token 是 JWTHS256payload 含 `roleId`、`adminId`、`iat`、`exp`5 分钟)。
**理由**: 用户选择。对称密钥实现简单,双方预共享即可。
### KTD3: Hermes webhook URL 全局配置 + 可覆盖
系统设置表存储 `HERMES_WEBHOOK_URL`,管理员发起同步时可在表单中覆盖。
**理由**: 用户选择。大多数情况用全局配置,特殊场景可覆盖。
### KTD4: 二维码由 Hermes 生成返回
Hermes 创建 profile 后生成二维码 URL 返回给 EternalAIEternalAI 存储到 Role 记录并展示。
**理由**: 用户选择。二维码内容(微信绑定链接)由 Hermes 侧定义。
### KTD5: 角色审核状态机
```
pending_review → approved → syncing → synced
pending_review → rejected
syncing → failed
```
Role 模型新增 `reviewStatus` 字段(默认 `pending_review`),新增 `qrCodeUrl`、`syncedAt` 字段。
### KTD6: Mock Hermes 测试端点
在 EternalAI 内部创建 `/api/mock-hermes/*` 端点模拟 Hermes 行为:接收同步请求、用 sync_token 回调拉取文件、返回 mock 二维码。仅测试环境启用。
---
## High-Level Technical Design
### 同步流程时序图
```mermaid
sequenceDiagram
participant Admin as 管理员
participant EAI as EternalAI
participant Hermes as Hermes (Mock)
Admin->>EAI: POST /api/admin/sync/:roleId
Note over EAI: 生成 sync_token (JWT, 5min)
EAI->>Hermes: POST {webhook_url} /api/sync
Note over EAI: Body: { profileName, modelKey,<br/>provider, multimediaModelKey,<br/>multimediaProvider, enableSchedule,<br/>sync_token, file_pull_base_url }
Hermes->>Hermes: 验证 sync_token 签名
Hermes->>EAI: GET /api/hermes/roles/:id/SOUL.md
Note over Hermes: Header: X-Sync-Token
EAI->>EAI: 验证 sync_token (签名+过期+未消费)
EAI-->>Hermes: SOUL.md content
Hermes->>EAI: GET /api/hermes/roles/:id/config.yaml
Note over Hermes: Header: X-Sync-Token
EAI-->>Hermes: config.yaml content
Note over Hermes: 创建 profile, 生成二维码
Hermes-->>EAI: 200 { qrCodeUrl, profileId }
Note over EAI: 存储 qrCodeUrl, 更新 reviewStatus=synced
EAI-->>Admin: 200 { qrCodeUrl, reviewStatus }
```
### 角色审核状态机
```mermaid
stateDiagram-v2
[*] --> pending_review : 创建角色
pending_review --> approved : 管理员通过
pending_review --> rejected : 管理员驳回
approved --> syncing : 管理员发起同步
syncing --> synced : Hermes 返回成功
syncing --> failed : 同步失败/超时
failed --> syncing : 重新同步
```
---
## Implementation Units
### U1. Admin 数据模型与认证
**Goal:** 创建独立 Admin 表,实现管理员注册/登录/中间件。
**Requirements:** R2
**Dependencies:**
**Files:**
- `prisma/schema.prisma` — 新增 Admin 模型
- `src/lib/auth.js` — 新增 `adminSignToken`、`adminVerifyToken`、`adminAuthMiddleware`
- `src/routes/admin-auth.js` — 新建,管理员登录路由
- `server.js` — 注册 `/api/admin-auth` 路由
- `e2e/admin-auth.spec.js` — 新建,管理员认证测试
**Approach:**
- Admin 模型:`id`、`account`(唯一)、`password`bcrypt、`createdAt`
- Admin JWT 与用户 JWT 使用不同 secret`ADMIN_JWT_SECRET`),防止跨角色伪造
- `adminAuthMiddleware` 验证 Admin JWT设置 `req.adminId`
- 管理员账号通过 `prisma db seed` 或 CLI 脚本创建,不开放注册 API
**Test scenarios:**
- 管理员登录成功,返回 admin JWT
- 管理员登录密码错误,返回 401
- 无 token 访问管理员接口,返回 401
- 用户 JWT 访问管理员接口,返回 403secret 不同,验证失败)
- 管理员 JWT 访问用户接口,返回 401用户中间件不识别 admin token
**Verification:** 管理员可登录admin JWT 可访问管理员接口,用户 JWT 不可访问管理员接口。
---
### U2. 角色审核状态机与审核 API
**Goal:** Role 模型新增审核状态字段,实现审核 API。
**Requirements:** R1, R3
**Dependencies:** U1
**Files:**
- `prisma/schema.prisma` — Role 模型新增 `reviewStatus`、`qrCodeUrl`、`syncedAt`、`reviewNote` 字段
- `src/routes/admin.js` — 新建,管理员审核路由
- `server.js` — 注册 `/api/admin` 路由
- `e2e/admin-review.spec.js` — 新建,审核流程测试
**Approach:**
- `reviewStatus` 枚举值:`pending_review`(默认)、`approved`、`rejected`、`syncing`、`synced`、`failed`
- 现有 `POST /api/roles` 创建角色时自动设置 `reviewStatus = 'pending_review'`
- `GET /api/admin/reviews` — 待审核列表(分页,按 createdAt desc
- `GET /api/admin/reviews/:roleId` — 角色详情(含所有字段)
- `POST /api/admin/reviews/:roleId/approve` — 通过审核,状态 → `approved`
- `POST /api/admin/reviews/:roleId/reject` — 驳回,状态 → `rejected`body 含 `reviewNote`
- 角色库 `GET /api/roles` 只返回 `reviewStatus = 'synced'` 的角色(已同步完成才上架)
**Test scenarios:**
- 创建角色后 reviewStatus 为 pending_review
- 管理员获取待审核列表,包含 pending_review 角色
- 管理员通过审核,状态变为 approved
- 管理员驳回审核,状态变为 rejectedreviewNote 有值
- 非管理员调用审核接口,返回 401
- 角色库不显示 pending_review / approved / rejected 状态的角色
**Verification:** 审核状态流转正确,非管理员无法操作。
---
### U3. sync_token 机制与系统配置
**Goal:** 实现 sync_token 生成/验证,系统配置存储 Hermes webhook URL。
**Requirements:** R5, R8
**Dependencies:** U1
**Files:**
- `prisma/schema.prisma` — 新增 SystemConfig 模型key-value 存储)
- `src/lib/sync-token.js` — 新建sync_token 生成与验证
- `src/routes/admin-config.js` — 新建,系统配置管理路由
- `server.js` — 注册 `/api/admin/config` 路由
- `e2e/sync-token.spec.js` — 新建sync_token 测试
**Approach:**
- SystemConfig 模型:`key`(唯一)、`value`、`updatedAt`
- 预置配置项:`HERMES_WEBHOOK_URL`、`SYNC_SECRET`
- `sync-token.js`:
- `generateSyncToken(roleId, adminId)` — 生成 JWTHS256payload `{ roleId, adminId, iat, exp }`5 分钟过期
- `verifySyncToken(token)` — 验证签名 + 过期,返回 payload 或 null
- 使用 `SYNC_SECRET` 从 SystemConfig 读取(首次启动自动生成)
- `PUT /api/admin/config/:key` — 更新配置项(仅管理员)
- `GET /api/admin/config` — 获取所有配置(仅管理员,敏感值脱敏)
**Test scenarios:**
- 生成 sync_token验证签名通过payload 正确
- 过期 token>5min验证失败
- 篡改 payload 后验证失败(签名不匹配)
- 管理员更新 HERMES_WEBHOOK_URL再次读取值正确
- 非管理员访问配置接口,返回 401
**Verification:** sync_token 生成/验证正确,系统配置可读写。
---
### U4. 同步发起 API
**Goal:** 管理员发起同步EternalAI 向 Hermes POST 请求。
**Requirements:** R4, R5, R6
**Dependencies:** U2, U3
**Files:**
- `src/routes/admin-sync.js` — 新建,同步发起路由
- `src/lib/hermes-client.js` — 新建Hermes HTTP 客户端
- `server.js` — 注册 `/api/admin/sync` 路由
- `e2e/admin-sync.spec.js` — 新建,同步发起测试
**Approach:**
- `POST /api/admin/sync/:roleId` — 发起同步
- Body: `{ profileName, modelKey, provider, multimediaModelKey, multimediaProvider, enableSchedule, webhookUrl? }`
- `webhookUrl` 可选,未提供则用 SystemConfig 中的 `HERMES_WEBHOOK_URL`
- 前置检查:`reviewStatus` 必须为 `approved``failed`
- 生成 sync_token更新 `reviewStatus = 'syncing'`
- 调用 `hermes-client.js``postSync(webhookUrl, payload)` 向 Hermes POST
- Hermes 返回 `{ qrCodeUrl, profileId }` → 存储 `qrCodeUrl`,更新 `reviewStatus = 'synced'`,记录 `syncedAt`
- Hermes 返回错误 → 更新 `reviewStatus = 'failed'`
- 请求超时30s→ 更新 `reviewStatus = 'failed'`
- `hermes-client.js`:
- `postSync(webhookUrl, payload)` — 用 `fetch` POST 到 Hermespayload 含 sync_token 和 file_pull_base_url
- `file_pull_base_url` = EternalAI 自身的基础 URL从 SystemConfig 读取 `ETERNALAI_BASE_URL`
**Test scenarios:**
- 管理员对 approved 角色发起同步reviewStatus 变为 syncing
- Mock Hermes 返回成功reviewStatus 变为 syncedqrCodeUrl 有值
- Mock Hermes 返回错误reviewStatus 变为 failed
- 对 pending_review 角色发起同步,返回 400
- 对 syncing 角色发起同步,返回 409重复同步
- 非管理员发起同步,返回 401
**Verification:** 同步发起后状态流转正确,成功时存储二维码 URL。
---
### U5. Hermes 回调拉取端点改造
**Goal:** 改造现有 `/api/hermes/` 端点,支持 sync_token 认证。
**Requirements:** R6
**Dependencies:** U3
**Files:**
- `src/routes/hermes.js` — 改造,新增 sync_token 认证路径
- `src/lib/auth.js` — 新增 `syncTokenMiddleware`
- `e2e/hermes-callback.spec.js` — 新建,回调拉取测试
**Approach:**
- 新增 `syncTokenMiddleware`:
- 读取 `X-Sync-Token` header
- 验证 sync_token 签名 + 过期
- 从 payload 提取 `roleId`,与 URL 中的 `:id` 比对,不一致返回 403
- 验证通过后设置 `req.userId`(通过 Role.creatorId 反查)和 `req.syncTokenPayload`
- 改造 `apiKeyMiddleware` 逻辑:
- 优先检查 `X-Sync-Token` header → 走 sync_token 路径
- 否则检查 `Authorization: Bearer eak_` → 走 API Key 路径
- 否则检查 `Authorization: Bearer <jwt>` → 走 JWT 路径
- sync_token 消费后标记为已使用(内存 Set5 分钟后自动清理;或用 token jti + 短期缓存)
- SOUL.md 和 config.yaml 端点保持 text/plain 响应不变
**Test scenarios:**
- 用有效 sync_token 拉取 SOUL.md返回 200 + 文件内容
- 用有效 sync_token 拉取 config.yaml返回 200 + 文件内容
- sync_token 中的 roleId 与 URL :id 不匹配,返回 403
- 过期 sync_token返回 401
- 已消费的 sync_token 再次使用,返回 401
- 无 token 访问,返回 401
- 用 API Keyeak_访问仍正常工作向后兼容
**Verification:** sync_token 可拉取文件,向后兼容 API Key 认证。
---
### U6. 二维码存储与状态展示
**Goal:** 存储 Hermes 返回的二维码 URL展示给管理员和创作者。
**Requirements:** R7
**Dependencies:** U4
**Files:**
- `src/routes/roles.js` — 改造,`GET /api/roles/my/roles` 返回 reviewStatus 和 qrCodeUrl
- `src/routes/admin.js` — 改造,管理员可查看所有角色的同步状态
- `app.js` — 改造,创作者角色卡片显示审核状态和二维码
- `e2e/qr-display.spec.js` — 新建,二维码展示测试
**Approach:**
- `GET /api/roles/my/roles` 返回字段新增 `reviewStatus`、`qrCodeUrl`、`syncedAt`
- 创作者角色卡片根据 reviewStatus 显示状态标签:
- `pending_review` → "待审核"(灰色)
- `approved` → "已通过,等待同步"(蓝色)
- `syncing` → "同步中"(黄色)
- `synced` → "已同步"(绿色)+ 显示二维码
- `failed` → "同步失败"(红色)
- `rejected` → "已驳回"(红色)
- synced 状态的角色卡片显示二维码图片(`qrCodeUrl`)和"转发二维码"按钮
- 管理员后台有"同步状态"页面,显示所有角色的审核+同步状态
**Test scenarios:**
- 创作者查看自己的角色列表pending_review 角色显示"待审核"标签
- 同步成功后,创作者角色卡片显示二维码图片
- 驳回角色显示"已驳回"标签
- 管理员查看同步状态列表,包含所有角色的 reviewStatus
**Verification:** 创作者和管理员都能看到正确的审核状态和二维码。
---
### U7. 管理员后台 UI
**Goal:** 管理员登录页 + 审核列表 + 角色详情审核页 + 同步表单。
**Requirements:** R2, R3, R4
**Dependencies:** U1, U2, U4
**Files:**
- `index.html` — 新增管理员视图admin-login、admin-reviews、admin-sync
- `app.js` — 新增管理员路由、审核交互、同步表单
- `styles.css` — 管理员后台样式
- `e2e/admin-ui.spec.js` — 新建,管理员 UI 测试
**Approach:**
- 管理员入口:首页底部隐藏链接 `/admin`,或直接访问 `#admin-login`
- 管理员登录页:账号 + 密码,登录后跳转 `#admin-reviews`
- 审核列表页:表格显示角色名、创作者、创建时间、状态;点击进入详情
- 角色详情审核页:显示所有角色字段,"通过"和"驳回"按钮
- 同步表单(审核通过后显示):
- profile 名字text默认角色名
- 主 model 服务商selectopenrouter / together / local
- 主 model keypassword input
- 多媒体 model 服务商select
- 多媒体 model keypassword input
- 是否开启定时任务checkbox
- Hermes webhook URLtext默认全局配置值可覆盖
- "发起同步"按钮
- 同步成功后显示二维码图片和"复制链接"按钮
**Test scenarios:**
- 管理员登录后跳转审核列表
- 审核列表显示待审核角色
- 点击角色进入详情,显示完整信息
- 通过审核后显示同步表单
- 填写同步参数并提交,显示同步中状态
- 同步成功后显示二维码
**Verification:** 管理员可完成登录→审核→同步的完整 UI 流程。
---
### U8. Mock Hermes 测试端点
**Goal:** 在 EternalAI 内部创建 mock Hermes 端点,用于测试同步流程。
**Requirements:** R9
**Dependencies:** U3, U5
**Files:**
- `src/routes/mock-hermes.js` — 新建mock Hermes 端点
- `server.js` — 注册 `/api/mock-hermes` 路由(仅非 production 环境)
- `e2e/mock-hermes.spec.js` — 新建mock Hermes 集成测试
**Approach:**
- `POST /api/mock-hermes/sync` — 模拟 Hermes 接收同步请求
- 验证 sync_token 签名(用同一 SYNC_SECRET
- 用 sync_token 回调 EternalAI 拉取 SOUL.md 和 config.yaml
- 返回 mock 二维码 URL`https://mock.hermes.local/qr/<roleId>`
- 返回 mock profileId`mock-profile-<roleId>`
- Mock 端点用 `fetch` 回调 EternalAI 自身的 `/api/hermes/roles/:id/SOUL.md``/api/hermes/roles/:id/config.yaml`
- 回调时携带 `X-Sync-Token` header
- 仅在 `NODE_ENV !== 'production'` 时注册路由
**Test scenarios:**
- POST /api/mock-hermes/sync 收到请求后,回调拉取 SOUL.md 成功
- POST /api/mock-hermes/sync 回调拉取 config.yaml 成功
- 返回 mock 二维码 URL 和 profileId
- 无效 sync_tokenmock Hermes 返回 401
- production 环境下 /api/mock-hermes 路由不存在404
**Verification:** Mock Hermes 完整模拟同步流程,可用于 E2E 测试。
---
### U9. E2E 集成测试:完整审核+同步流程
**Goal:** 端到端测试从创建角色到同步成功的完整流程。
**Requirements:** R1-R9
**Dependencies:** U1-U8
**Files:**
- `e2e/full-sync-flow.spec.js` — 新建,完整流程测试
**Approach:**
测试完整流程:
1. 创作者注册 → 登录 → 创建角色(状态 pending_review
2. 管理员登录 → 查看待审核列表 → 查看详情 → 通过审核
3. 管理员填写同步参数 → 发起同步
4. Mock Hermes 接收请求 → 回调拉取文件 → 返回二维码
5. 创作者查看角色列表 → 看到已同步状态 + 二维码
6. 管理员查看同步状态 → 看到已同步
**Test scenarios:**
- 完整流程:创建 → 审核 → 同步 → 二维码展示
- 驳回流程:创建 → 审核 → 驳回 → 创作者看到"已驳回"
- 同步失败流程:创建 → 审核 → 同步mock 返回错误)→ 状态 failed → 重新同步
- 向后兼容:现有 API Key 拉取流程仍正常工作
**Verification:** E2E 测试覆盖所有核心路径,全部通过。
---
## Scope Boundaries
### In Scope
- Admin 表与认证
- 角色审核状态机
- sync_token 生成/验证/消费
- 同步发起 API
- Hermes 回调拉取端点改造sync_token 认证)
- 二维码存储与展示
- 管理员后台 UI
- Mock Hermes 测试端点
- E2E 测试(鉴权 + 文件拉取)
### Out of Scope
- 真实 Hermes 服务器对接(用 mock 代替)
- 微信公众号/小程序绑定实现(二维码内容由 Hermes 侧定义)
- 管理员注册 UI通过 seed 脚本创建)
- 速率限制(已有 P2 待修复,不在本次范围)
- 现有 API Key 拉取流程的改造(保持向后兼容)
### Deferred to Follow-Up Work
- 真实 Hermes 服务器对接(需 Hermes 侧实现 `/api/sync` 端点)
- 微信绑定流程实现
- 同步重试机制(指数退避)
- 同步日志审计
- 多 Hermes 实例支持
---
## Risks & Dependencies
| 风险 | 影响 | 缓解 |
|------|------|------|
| sync_token 内存消费记录在多实例部署下失效 | 重复消费风险 | 后续可改用 Redis当前单实例够用 |
| Mock Hermes 端点误暴露到生产环境 | 安全风险 | 仅 `NODE_ENV !== 'production'` 注册路由 |
| Hermes 回调时 EternalAI 不可达 | 同步失败 | 同步状态设为 failed管理员可重试 |
| sync_token 在传输中被截获 | 5 分钟内可滥用 | HTTPS + 一次性消费 + 短过期 |
---
## Open Questions
- **OQ1**: sync_token 一次性消费用内存 Set 还是数据库表?(计划用内存 Set单实例部署够用多实例时改 Redis
- **OQ2**: 管理员账号初始创建方式?(计划用 `prisma db seed` 脚本,账号密码从环境变量读取)
- **OQ3**: EternalAI 自身的基础 URL`ETERNALAI_BASE_URL`)如何获取?(计划从 SystemConfig 读取,首次部署时配置)
---
## System-Wide Impact
- **数据库**: 新增 Admin、SystemConfig 表Role 表新增 4 个字段
- **API**: 新增 `/api/admin-auth`、`/api/admin`、`/api/admin/config`、`/api/admin/sync`、`/api/mock-hermes` 路由组
- **前端**: 新增 3 个管理员视图,改造创作者角色卡片
- **认证**: 新增 Admin JWT独立 secret和 sync_tokenHMAC SHA-256两套机制
- **向后兼容**: 现有用户 API Key 拉取流程不变