# 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,
provider, multimediaModelKey,
multimediaProvider, enableSchedule,
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 路径 - 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/` - 返回 mock profileId:`mock-profile-` - 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 拉取流程不变