# 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_token（HMAC SHA-256 对称密钥）向 Hermes POST 同步请求
- **R6**: Hermes 用 sync_token 回调 EternalAI 拉取 SOUL.md 和 config.yaml
- **R7**: Hermes 返回二维码 URL，EternalAI 存储并展示给管理员和创作者
- **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 是 JWT（HS256），payload 含 `roleId`、`adminId`、`iat`、`exp`（5 分钟）。

**理由**: 用户选择。对称密钥实现简单，双方预共享即可。

### KTD3: Hermes webhook URL 全局配置 + 可覆盖

系统设置表存储 `HERMES_WEBHOOK_URL`，管理员发起同步时可在表单中覆盖。

**理由**: 用户选择。大多数情况用全局配置，特殊场景可覆盖。

### KTD4: 二维码由 Hermes 生成返回

Hermes 创建 profile 后生成二维码 URL 返回给 EternalAI，EternalAI 存储到 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 访问管理员接口，返回 403（secret 不同，验证失败）
- 管理员 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
- 管理员驳回审核，状态变为 rejected，reviewNote 有值
- 非管理员调用审核接口，返回 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)` — 生成 JWT（HS256），payload `{ 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 到 Hermes，payload 含 sync_token 和 file_pull_base_url
  - `file_pull_base_url` = EternalAI 自身的基础 URL（从 SystemConfig 读取 `ETERNALAI_BASE_URL`）

**Test scenarios:**
- 管理员对 approved 角色发起同步，reviewStatus 变为 syncing
- Mock Hermes 返回成功，reviewStatus 变为 synced，qrCodeUrl 有值
- 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 消费后标记为已使用（内存 Set，5 分钟后自动清理；或用 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 Key（eak_）访问仍正常工作（向后兼容）

**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 服务商（select：openrouter / together / local）
  - 主 model key（password input）
  - 多媒体 model 服务商（select）
  - 多媒体 model key（password input）
  - 是否开启定时任务（checkbox）
  - Hermes webhook URL（text，默认全局配置值，可覆盖）
  - "发起同步"按钮
- 同步成功后显示二维码图片和"复制链接"按钮

**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_token，mock 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_token（HMAC SHA-256）两套机制
- **向后兼容**: 现有用户 API Key 拉取流程不变
