1078 lines
51 KiB
Markdown
1078 lines
51 KiB
Markdown
# Eternal AI 业务流程文档
|
||
|
||
> 本文档详细描述 Eternal AI 平台的系统架构、数据模型、API 端点清单以及全部业务流程,供新成员快速上手开发与维护。
|
||
>
|
||
> - **项目名称**:EternalAI
|
||
> - **版本**:1.0.0
|
||
> - **描述**:AI 陪伴平台 — 人设创作者设定发布,生成 Hermes agent 配置文件
|
||
> - **作者**:chigulong
|
||
> - **许可证**:MIT
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
- [一、系统架构概述](#一系统架构概述)
|
||
- [1.1 技术栈](#11-技术栈)
|
||
- [1.2 文件结构](#12-文件结构)
|
||
- [1.3 数据流图](#13-数据流图)
|
||
- [二、数据模型](#二数据模型)
|
||
- [2.1 User(用户)](#21-user用户)
|
||
- [2.2 Role(角色)](#22-role角色)
|
||
- [2.3 Order(订单)](#23-order订单)
|
||
- [2.4 模型关系](#24-模型关系)
|
||
- [三、API 端点清单](#三api-端点清单)
|
||
- [四、业务流程](#四业务流程)
|
||
- [4.1 用户注册流程](#41-用户注册流程)
|
||
- [4.2 用户登录流程](#42-用户登录流程)
|
||
- [4.3 登录态持久化流程](#43-登录态持久化流程)
|
||
- [4.4 用户登出流程](#44-用户登出流程)
|
||
- [4.5 角色库浏览流程](#45-角色库浏览流程)
|
||
- [4.6 角色详情查看流程](#46-角色详情查看流程)
|
||
- [4.7 新建角色流程(4 步表单)](#47-新建角色流程4-步表单)
|
||
- [4.8 编辑角色流程](#48-编辑角色流程)
|
||
- [4.9 角色发布数据生成流程](#49-角色发布数据生成流程)
|
||
- [4.10 创作者中心管理流程](#410-创作者中心管理流程)
|
||
- [4.11 设置保存流程](#411-设置保存流程)
|
||
- [4.12 角色付款流程](#412-角色付款流程)
|
||
- [4.13 导航流程](#413-导航流程)
|
||
- [4.14 表单验证流程](#414-表单验证流程)
|
||
- [五、附录](#五附录)
|
||
- [5.1 前端状态结构](#51-前端状态结构)
|
||
- [5.2 localStorage 键名约定](#52-localstorage-键名约定)
|
||
- [5.3 已知限制与待办](#53-已知限制与待办)
|
||
|
||
---
|
||
|
||
## 一、系统架构概述
|
||
|
||
### 1.1 技术栈
|
||
|
||
| 层级 | 技术 | 说明 |
|
||
| --- | --- | --- |
|
||
| 前端 | 原生 HTML / CSS / JavaScript (IIFE) | 单页应用(SPA),无框架,通过 `view` 切换实现路由 |
|
||
| 后端 | Node.js + Express 5.x | RESTful API,提供认证与角色 CRUD |
|
||
| 数据库 | PostgreSQL | 通过 Prisma ORM 访问 |
|
||
| ORM | Prisma 5.22 | Schema 位于 `prisma/schema.prisma` |
|
||
| 认证 | JWT (jsonwebtoken) + bcryptjs | Token 有效期 7 天,密码使用 bcrypt 哈希(salt rounds = 10) |
|
||
| 配置 | dotenv | 通过 `.env` 注入环境变量 |
|
||
| 跨域 | cors | 全局启用 CORS |
|
||
| 测试 | Jest + Playwright 1.54 | `npm test` 运行 Jest |
|
||
|
||
### 1.2 文件结构
|
||
|
||
```
|
||
EternalAI/
|
||
├── index.html # 单页应用 HTML,包含全部视图(view)
|
||
├── app.js # 前端全部逻辑:状态管理、视图路由、API 调用、表单处理
|
||
├── styles.css # 样式表
|
||
├── server.js # Express 服务器入口:中间件、路由挂载、静态文件、启动
|
||
├── package.json # 依赖与脚本
|
||
├── prisma/
|
||
│ └── schema.prisma # 数据库模型定义(User / Role / Order)
|
||
├── src/
|
||
│ ├── lib/
|
||
│ │ ├── auth.js # JWT 与密码工具:hashPassword / verifyPassword / signToken / verifyToken / authMiddleware
|
||
│ │ └── prisma.js # PrismaClient 单例
|
||
│ └── routes/
|
||
│ ├── auth.js # 认证路由:register / login / me / settings
|
||
│ └── roles.js # 角色路由:列表 / 详情 / 我的角色 / 发布 / 编辑 / 完整信息
|
||
└── docs/
|
||
└── business-processes.md # 本文档
|
||
```
|
||
|
||
### 1.3 数据流图
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 浏览器(前端) │
|
||
│ │
|
||
│ index.html (9 个 view) ←─DOM─→ app.js (IIFE) │
|
||
│ │
|
||
│ app.js 内部模块: │
|
||
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │
|
||
│ │ 状态管理 │ │ 视图路由 │ │ API 封装 (api()) │ │
|
||
│ │ (state + │ │ (showView + │ │ fetch + JWT header │ │
|
||
│ │ localStorage)│ │ viewHistory)│ │ │ │
|
||
│ └──────┬───────┘ └──────┬───────┘ └─────────┬──────────┘ │
|
||
│ │ │ │ │
|
||
│ └─────────────────┴─────────────────────┘ │
|
||
│ │ │
|
||
│ localStorage (2 个键) │
|
||
│ eternal_ai_token (JWT) + eternal_ai_state (状态) │
|
||
└───────────────────────────┼─────────────────────────────────────┘
|
||
│ HTTP (fetch, /api/*)
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Express 服务器 (server.js) │
|
||
│ │
|
||
│ 中间件: cors → express.json → 路由 → 静态文件 → 主页 │
|
||
│ │
|
||
│ ┌─────────────────────┐ ┌──────────────────────────┐ │
|
||
│ │ /api/auth │ │ /api/roles │ │
|
||
│ │ (src/routes/auth.js)│ │ (src/routes/roles.js) │ │
|
||
│ │ │ │ │ │
|
||
│ │ POST /register │ │ GET / │ │
|
||
│ │ POST /login │ │ GET /:id │ │
|
||
│ │ GET /me │ │ GET /my/roles │ │
|
||
│ │ PUT /settings │ │ POST / │ │
|
||
│ │ │ │ PUT /:id │ │
|
||
│ │ 依赖: authMiddleware│ │ GET /:id/full │ │
|
||
│ └──────────┬──────────┘ └────────────┬─────────────┘ │
|
||
│ │ │ │
|
||
│ └────────────┬───────────────────┘ │
|
||
│ ▼ │
|
||
│ ┌────────────────────────┐ │
|
||
│ │ PrismaClient 单例 │ │
|
||
│ │ (src/lib/prisma.js) │ │
|
||
│ └────────────┬───────────┘ │
|
||
└───────────────────────────┼─────────────────────────────────────┘
|
||
│ SQL
|
||
▼
|
||
┌──────────────────┐
|
||
│ PostgreSQL │
|
||
│ │
|
||
│ User Role Order│
|
||
└──────────────────┘
|
||
```
|
||
|
||
**关键说明**:
|
||
|
||
- 前端为单页应用,9 个 `view` 通过 `.active` class 切换显示,`viewHistory` 数组维护返回栈。
|
||
- 所有 API 请求经 `api()` 封装,自动注入 `Authorization: Bearer <token>` 头部。
|
||
- JWT Token 与前端状态分别存储在 localStorage 的两个键中(见 [5.2](#52-localstorage-键名约定))。
|
||
- `authMiddleware` 用于需要登录的端点,从 Token 解析 `userId` 挂载到 `req.userId`。
|
||
|
||
---
|
||
|
||
## 二、数据模型
|
||
|
||
数据库 Schema 定义于 `prisma/schema.prisma`,使用 PostgreSQL,包含三个模型。
|
||
|
||
### 2.1 User(用户)
|
||
|
||
| 字段 | 类型 | 约束 | 默认值 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `id` | String | `@id` | `uuid()` | 用户唯一 ID |
|
||
| `account` | String | `@unique` | — | 登录账号(手机号或用户名) |
|
||
| `password` | String | — | — | bcrypt 哈希后的密码 |
|
||
| `isCreator` | Boolean | — | `false` | 是否为创作者 |
|
||
| `creatorName` | String? | 可空 | — | 创作者笔名 |
|
||
| `libraryName` | String? | 可空 | — | 角色库名称(首页「我的 XXX」显示文字) |
|
||
| `boundCreator` | String? | 可空 | — | 绑定的创作者 ID(普通用户通过专属链接绑定) |
|
||
| `createdAt` | DateTime | — | `now()` | 创建时间 |
|
||
| `updatedAt` | DateTime | — | `@updatedAt` | 更新时间 |
|
||
|
||
**关联**:`roles Role[]`(一对多,用户创建的角色)、`orders Order[]`(一对多,用户的订单)。
|
||
|
||
### 2.2 Role(角色)
|
||
|
||
| 字段 | 类型 | 约束 | 默认值 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `id` | String | `@id` | `uuid()` | 角色唯一 ID |
|
||
| `creatorId` | String | 外键 → User.id | — | 创建者 ID |
|
||
| `displayName` | String | — | — | 显示名称 |
|
||
| `gender` | String | — | `"unknown"` | 性别(unknown/female/male/nonbinary) |
|
||
| `age` | String? | 可空 | — | 年龄 |
|
||
| `relationship` | String? | 可空 | — | 与使用者的关系 |
|
||
| `personality` | String | 必填 | — | 性格标签(逗号分隔) |
|
||
| `background` | String | 必填 | — | 背景故事 |
|
||
| `speechStyle` | String | 必填 | — | 说话风格 |
|
||
| `likes` | String? | 可空 | — | 喜好 |
|
||
| `dislikes` | String? | 可空 | — | 厌恶 / 底线 |
|
||
| `memories` | String? | 可空 | — | 共同记忆 / 关键事件 |
|
||
| `secrets` | String? | 可空 | — | 秘密或未说出口的话 |
|
||
| `greeting` | String | 必填 | — | 开场白 |
|
||
| `systemPrompt` | String? | 可空 | — | 系统提示词(可自动生成) |
|
||
| `model` | String | — | `"gpt-4o"` | 模型名称 |
|
||
| `temperature` | Float | — | `0.8` | 温度参数 |
|
||
| `maxTokens` | Int | — | `2048` | 最大 Token 数 |
|
||
| `enableMemory` | Boolean | — | `true` | 是否启用长期记忆 |
|
||
| `enableTools` | Boolean | — | `false` | 是否启用外部工具 |
|
||
| `agentId` | String? | 可空 | — | 角色代号(英文/数字/下划线,用于 config.yaml) |
|
||
| `soulMd` | String? | 可空 | — | 生成的 Soul.md 内容 |
|
||
| `configYaml` | String? | 可空 | — | 生成的 config.yaml 内容 |
|
||
| `avatar` | String? | 可空 | — | 头像 URL |
|
||
| `desc` | String? | 可空 | — | 角色简介 |
|
||
| `price` | Float | — | `0` | 订阅价格(元/月) |
|
||
| `status` | String | — | `"running"` | 状态(running/stopped) |
|
||
| `createdAt` | DateTime | — | `now()` | 创建时间 |
|
||
| `updatedAt` | DateTime | — | `@updatedAt` | 更新时间 |
|
||
|
||
**关联**:`creator User`(多对一)、`orders Order[]`(一对多)。
|
||
|
||
### 2.3 Order(订单)
|
||
|
||
| 字段 | 类型 | 约束 | 默认值 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `id` | String | `@id` | `uuid()` | 订单唯一 ID |
|
||
| `userId` | String | 外键 → User.id | — | 下单用户 ID |
|
||
| `roleId` | String | 外键 → Role.id | — | 订阅角色 ID |
|
||
| `amount` | Float | — | — | 订单金额 |
|
||
| `status` | String | — | `"paid"` | 订单状态 |
|
||
| `createdAt` | DateTime | — | `now()` | 创建时间 |
|
||
|
||
> ⚠️ **注意**:`Order` 模型已在 Schema 中定义,但当前后端路由(`src/routes/*.js`)**未实现任何订单相关端点**。角色付款流程目前为前端 Mock(见 [4.12](#412-角色付款流程))。
|
||
|
||
### 2.4 模型关系
|
||
|
||
```
|
||
User (1) ──────< (N) Role
|
||
│ │
|
||
│ │
|
||
└────< (N) Order >──┘
|
||
```
|
||
|
||
- 一个 User 可创建多个 Role(`creatorId`)。
|
||
- 一个 User 可有多个 Order,一个 Role 可对应多个 Order。
|
||
- Order 同时关联 User(下单者)与 Role(被订阅角色)。
|
||
|
||
---
|
||
|
||
## 三、API 端点清单
|
||
|
||
所有 API 前缀为 `/api`。认证端点挂载于 `/api/auth`,角色端点挂载于 `/api/roles`。
|
||
|
||
### 认证端点(`/api/auth`)
|
||
|
||
| 方法 | 路径 | 鉴权 | 请求体 | 响应 | 说明 |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| POST | `/api/auth/register` | 否 | `{ account: string, password: string }` | `{ token: string, user: { id, account, isCreator, creatorName, libraryName } }` | 注册新用户,返回 JWT |
|
||
| POST | `/api/auth/login` | 否 | `{ account: string, password: string }` | `{ token: string, user: { id, account, isCreator, creatorName, libraryName } }` | 登录,返回 JWT |
|
||
| GET | `/api/auth/me` | 是 | — | `{ user: { id, account, isCreator, creatorName, libraryName, boundCreator } }` | 获取当前登录用户信息 |
|
||
| PUT | `/api/auth/settings` | 是 | `{ creatorName?, libraryName?, isCreator? }` | `{ user: { id, account, isCreator, creatorName, libraryName } }` | 更新用户设置(仅传入字段被更新) |
|
||
|
||
### 角色端点(`/api/roles`)
|
||
|
||
| 方法 | 路径 | 鉴权 | 请求体 / 参数 | 响应 | 说明 |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| GET | `/api/roles` | 否 | — | `{ roles: [{ id, displayName, avatar, desc, price, status }] }` | 获取所有已上架(status=running)角色,按创建时间倒序 |
|
||
| GET | `/api/roles/:id` | 否 | `id`(路径) | `{ role: { id, displayName, avatar, desc, price, status, gender, age, relationship, personality, background, speechStyle, greeting, creatorId } }` | 获取角色详情(公开字段) |
|
||
| GET | `/api/roles/my/roles` | 是 | — | `{ roles: [Role 完整字段] }` | 获取当前用户创建的全部角色 |
|
||
| POST | `/api/roles` | 是 | 角色完整数据(见下) | `{ role: Role }` | 发布新角色 |
|
||
| PUT | `/api/roles/:id` | 是 | `id`(路径)+ 角色数据 | `{ role: Role }` | 编辑角色(仅创建者可编辑) |
|
||
| GET | `/api/roles/:id/full` | 是 | `id`(路径) | `{ role: Role 完整字段 }` | 获取角色完整信息(含 soulMd、configYaml,仅创建者可访问) |
|
||
|
||
**POST `/api/roles` 请求体**(必填字段:`displayName`、`greeting`、`personality`、`background`、`speechStyle`):
|
||
|
||
```json
|
||
{
|
||
"displayName": "云朵",
|
||
"gender": "female",
|
||
"age": "24",
|
||
"relationship": "前任",
|
||
"personality": "温柔, 敏感",
|
||
"background": "你们是如何相遇的...",
|
||
"speechStyle": "常用语气词...",
|
||
"likes": "喜欢的事物",
|
||
"dislikes": "讨厌什么",
|
||
"memories": "共同记忆",
|
||
"secrets": "秘密",
|
||
"greeting": "ta 第一次开口会对你说什么?",
|
||
"systemPrompt": "(留空则自动生成)",
|
||
"model": "hermes-3-llama-3.1-70b",
|
||
"temperature": 0.85,
|
||
"maxTokens": 1024,
|
||
"enableMemory": true,
|
||
"enableTools": false,
|
||
"agentId": "eternal_lover_001",
|
||
"soulMd": "# Soul of ...(生成的 Markdown)",
|
||
"configYaml": "# Hermes Agent Config...(生成的 YAML)",
|
||
"avatar": "https://...",
|
||
"desc": "角色简介",
|
||
"price": 29.9
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 四、业务流程
|
||
|
||
### 4.1 用户注册流程
|
||
|
||
**流程名称**:用户注册流程
|
||
|
||
**参与角色**:未登录访客
|
||
|
||
**前置条件**:
|
||
- 用户处于未登录状态。
|
||
- 数据库可正常访问。
|
||
|
||
**详细步骤**:
|
||
|
||
1. 用户进入首页(`landing` 视图),点击「我的 [XXX]」卡片(`data-action="open-characters"`)。
|
||
2. 前端检测 `state.isLoggedIn === false`,调用 `switchAuthTab('login')` 切换到登录 Tab,并 `showView('auth')` 进入认证视图。
|
||
3. 用户点击「注册」Tab(`data-tab="register"`),切换到注册表单(`#register-form`)。
|
||
4. 用户填写表单:
|
||
- `account`(手机号 / 用户名,必填)
|
||
- `password`(密码,必填,`minlength="6"`)
|
||
- `confirmPassword`(确认密码,必填)
|
||
5. 用户点击「注册」按钮提交表单。
|
||
6. 前端 `submit` 事件触发:
|
||
- 调用 `validatePasswordMatch()` 校验两次密码是否一致,不一致则 `setCustomValidity('两次输入的密码不一致')` 并阻止提交。
|
||
- 通过 `api('/auth/register', { method: 'POST', body: { account, password } })` 发起注册请求。
|
||
7. 后端 `POST /api/auth/register` 处理:
|
||
- 校验 `account` 与 `password` 非空,密码长度 ≥ 6。
|
||
- 查询 `prisma.user.findUnique({ where: { account } })`,若已存在返回 `409 { error: '该账号已注册' }`。
|
||
- 使用 `hashPassword(password)`(bcrypt,salt rounds = 10)哈希密码。
|
||
- `prisma.user.create()` 创建用户,默认 `isCreator = false`、`libraryName = '我的角色库'`。
|
||
- `signToken(user.id)` 生成 JWT(有效期 7 天),返回 `{ token, user }`。
|
||
8. 前端收到响应:
|
||
- `setToken(result.token)` 将 JWT 存入 localStorage。
|
||
- `applyUserData(result.user)` 更新 `state` 并 `saveState()`。
|
||
9. **注册后自动成为创作者**:前端额外发起 `PUT /api/auth/settings`,请求体 `{ isCreator: true, creatorName: data.account }`。
|
||
- 后端更新用户 `isCreator = true`、`creatorName = account`,返回更新后的 user。
|
||
10. 前端更新 `state.isCreator = true`、`state.creatorName = data.account`,`saveState()` + `updateLandingCard()`。
|
||
11. 由于 `state.isCreator` 为 `true`,前端 `showView('creator-center')` 并 `renderCreatorCenter()`,进入创作者管理中心。
|
||
|
||
**后置条件**:
|
||
- 数据库新增一条 User 记录,`isCreator = true`。
|
||
- localStorage 存有 JWT 与用户状态。
|
||
- 用户进入创作者管理中心。
|
||
|
||
**异常处理**:
|
||
- 账号或密码为空 → 后端返回 `400 { error: '账号和密码不能为空' }`,前端 `alert(err.message)`。
|
||
- 密码 < 6 位 → 后端返回 `400 { error: '密码至少 6 位' }`。
|
||
- 账号已注册 → 后端返回 `409 { error: '该账号已注册' }`。
|
||
- 两次密码不一致 → 前端阻止提交并提示。
|
||
- 网络错误 → `api()` 抛出 `无法连接服务器,请检查网络`。
|
||
- 服务器异常 → 后端返回 `500 { error: '注册失败,请稍后重试' }`。
|
||
|
||
**涉及的 API 端点**:
|
||
- `POST /api/auth/register`
|
||
- `PUT /api/auth/settings`(注册成功后自动调用以开启创作者身份)
|
||
|
||
---
|
||
|
||
### 4.2 用户登录流程
|
||
|
||
**流程名称**:用户登录流程
|
||
|
||
**参与角色**:已注册用户
|
||
|
||
**前置条件**:
|
||
- 用户已有账号。
|
||
- 处于未登录状态。
|
||
|
||
**详细步骤**:
|
||
|
||
1. 用户进入首页,点击「我的 [XXX]」卡片(`data-action="open-characters"`)。
|
||
2. 前端检测未登录,`switchAuthTab('login')` + `showView('auth')`。
|
||
3. 用户在登录表单(`#login-form`)填写:
|
||
- `account`(手机号 / 用户名,必填)
|
||
- `password`(密码,必填)
|
||
4. 用户点击「登录」提交。
|
||
5. 前端 `submit` 事件触发:
|
||
- `validatePasswordMatch()`(登录表单无 confirmPassword,直接返回 `true`)。
|
||
- `api('/auth/login', { method: 'POST', body: { account, password } })`。
|
||
6. 后端 `POST /api/auth/login` 处理:
|
||
- 校验 `account` 与 `password` 非空。
|
||
- `prisma.user.findUnique({ where: { account } })` 查询用户。
|
||
- 用户不存在或 `verifyPassword(password, user.password)` 失败 → 返回 `401 { error: '账号或密码错误' }`。
|
||
- 验证通过,`signToken(user.id)` 生成 JWT,返回 `{ token, user }`。
|
||
7. 前端收到响应:
|
||
- `setToken(result.token)` 存 Token。
|
||
- `applyUserData(result.user)` 更新状态。
|
||
8. **根据 `isCreator` 跳转**:
|
||
- 若 `state.isCreator === true` → `showView('creator-center')` + `renderCreatorCenter()`。
|
||
- 否则 → `renderRoleLibrary()` + `showView('role-library')`,进入角色库。
|
||
|
||
**后置条件**:
|
||
- localStorage 存有 JWT 与用户状态。
|
||
- 用户进入创作者管理中心或角色库。
|
||
|
||
**异常处理**:
|
||
- 账号或密码为空 → `400 { error: '账号和密码不能为空' }`。
|
||
- 账号或密码错误 → `401 { error: '账号或密码错误' }`。
|
||
- 服务器异常 → `500 { error: '登录失败,请稍后重试' }`。
|
||
- 所有错误均通过前端 `alert(err.message)` 提示。
|
||
|
||
**涉及的 API 端点**:
|
||
- `POST /api/auth/login`
|
||
|
||
---
|
||
|
||
### 4.3 登录态持久化流程
|
||
|
||
**流程名称**:登录态持久化流程
|
||
|
||
**参与角色**:已登录用户(页面刷新场景)
|
||
|
||
**前置条件**:
|
||
- localStorage 中存在 `eternal_ai_token`(JWT)。
|
||
|
||
**详细步骤**:
|
||
|
||
1. 页面加载时,`app.js` 末尾的 IIFE(立即执行函数)自动执行:
|
||
```
|
||
const token = getToken(); // 读取 localStorage['eternal_ai_token']
|
||
if (!token) return; // 无 Token 则跳过
|
||
```
|
||
2. 若存在 Token,调用 `api('/auth/me')`(GET 请求,自动携带 `Authorization: Bearer <token>`)。
|
||
3. 后端 `GET /api/auth/me` 处理:
|
||
- `authMiddleware` 解析 Token:提取 `userId`,无效则返回 `401`。
|
||
- `prisma.user.findUnique({ where: { id: req.userId } })` 查询用户。
|
||
- 用户不存在 → `404 { error: '用户不存在' }`。
|
||
- 返回 `{ user: { id, account, isCreator, creatorName, libraryName, boundCreator } }`。
|
||
4. 前端收到响应:
|
||
- `applyUserData(user)` 恢复 `state.isLoggedIn = true` 及各项用户字段。
|
||
- `saveState()` 持久化状态。
|
||
- `updateLandingCard()` 更新首页卡片显示。
|
||
5. 同时,页面初始化阶段已执行 `loadState()` 从 localStorage 恢复 `state`(含 `isLoggedIn` 等),保证首屏渲染时即显示登录态。
|
||
|
||
**后置条件**:
|
||
- 用户登录态恢复,首页卡片显示对应内容(创作者显示「进入管理中心」,普通用户显示「进入角色库」)。
|
||
|
||
**异常处理**:
|
||
- Token 过期或无效 → `api('/auth/me')` 抛错,`catch` 块执行 `setToken('')` 清除 Token(用户需重新登录)。
|
||
- 网络错误 → Token 保留,下次刷新重试。
|
||
|
||
**涉及的 API 端点**:
|
||
- `GET /api/auth/me`
|
||
|
||
---
|
||
|
||
### 4.4 用户登出流程
|
||
|
||
**流程名称**:用户登出流程
|
||
|
||
**参与角色**:已登录用户(创作者)
|
||
|
||
**前置条件**:
|
||
- 用户已登录并处于创作者管理中心。
|
||
|
||
**详细步骤**:
|
||
|
||
1. 用户在创作者管理中心点击「我的」Tab(`data-center-tab="settings"`),切换到设置面板。
|
||
2. 用户点击「退出登录」按钮(`data-action="logout"`)。
|
||
3. 前端 `logout()` 函数执行:
|
||
- 重置 `state`:`isLoggedIn = false`、`isCreator = false`、`account = null`、`userId = null`、`boundCreator = null`、`roles = []`、`income = { balance: 0, records: [] }`。
|
||
- `setToken('')` 清除 localStorage 中的 JWT。
|
||
- `saveState()` 持久化重置后的状态。
|
||
- `updateLandingCard()` 更新首页卡片为未登录态(显示「登录 / 注册」)。
|
||
- `showView('landing')` 返回首页。
|
||
|
||
**后置条件**:
|
||
- localStorage 中 JWT 被清除。
|
||
- 前端状态重置为默认值。
|
||
- 用户返回首页,卡片显示「登录 / 注册」。
|
||
|
||
**异常处理**:
|
||
- 此流程为纯前端操作,无 API 调用,无异常风险。
|
||
|
||
**涉及的 API 端点**:
|
||
- 无(纯前端状态清理)
|
||
|
||
---
|
||
|
||
### 4.5 角色库浏览流程
|
||
|
||
**流程名称**:角色库浏览流程
|
||
|
||
**参与角色**:已登录的普通用户(非创作者)
|
||
|
||
**前置条件**:
|
||
- 用户已登录且 `isCreator === false`。
|
||
|
||
**详细步骤**:
|
||
|
||
1. 用户在首页点击「我的 [XXX]」卡片(`data-action="open-characters"`)。
|
||
2. 前端检测 `state.isLoggedIn === true` 且 `state.isCreator === false`,执行:
|
||
- `renderRoleLibrary()` 渲染角色库。
|
||
- `showView('role-library')` 切换视图。
|
||
3. `renderRoleLibrary()` 执行:
|
||
- 设置标题为 `state.libraryName || '我的角色库'`。
|
||
- 列表区域显示「加载中…」。
|
||
- 调用 `api('/roles')` 获取角色列表。
|
||
4. 后端 `GET /api/roles` 处理:
|
||
- `prisma.role.findMany({ where: { status: 'running' }, orderBy: { createdAt: 'desc' }, select: { id, displayName, avatar, desc, price, status } })`。
|
||
- 返回 `{ roles: [...] }`。
|
||
5. 前端渲染:
|
||
- 若 `roles.length === 0` → 清空列表,显示空状态提示(`#library-empty`:「你还没有绑定专属创作者」)。
|
||
- 否则,将每个角色渲染为 `.role-card`,包含头像、名称、简介、价格,卡片带 `data-role-id` 属性。
|
||
6. 用户点击任意角色卡片 → 进入 [角色详情查看流程](#46-角色详情查看流程)。
|
||
|
||
**后置条件**:
|
||
- 角色库视图展示所有上架角色卡片。
|
||
|
||
**异常处理**:
|
||
- API 失败 → 列表区域显示「加载失败:{err.message}」。
|
||
|
||
**涉及的 API 端点**:
|
||
- `GET /api/roles`
|
||
|
||
---
|
||
|
||
### 4.6 角色详情查看流程
|
||
|
||
**流程名称**:角色详情查看流程
|
||
|
||
**参与角色**:已登录的普通用户
|
||
|
||
**前置条件**:
|
||
- 用户处于角色库视图,角色列表已加载。
|
||
|
||
**详细步骤**:
|
||
|
||
1. 用户在角色库点击角色卡片(`.role-card`,含 `data-role-id`)。
|
||
2. 前端事件委托捕获点击:
|
||
- 调用 `renderRoleDetail(roleId)`。
|
||
- `showView('role-detail')` 切换到角色详情视图。
|
||
3. `renderRoleDetail(roleId)` 执行:
|
||
- 调用 `api('/roles/${roleId}')` 获取角色详情。
|
||
4. 后端 `GET /api/roles/:id` 处理:
|
||
- `prisma.role.findUnique({ where: { id }, select: { id, displayName, avatar, desc, price, status, gender, age, relationship, personality, background, speechStyle, greeting, creatorId } })`。
|
||
- 角色不存在 → `404 { error: '角色不存在' }`。
|
||
- 返回 `{ role }`。
|
||
5. 前端渲染详情:
|
||
- `currentRole = role`(保存当前角色,供付款流程使用)。
|
||
- 设置详情头部名称(`#detail-name`)。
|
||
- 设置 hero 背景图(`#detail-hero`,使用 `role.avatar`)。
|
||
- 设置角色名称(`#detail-role-name`)、描述(`#detail-role-desc`,取 `desc || personality`)。
|
||
- 设置价格(`#detail-price`,显示「¥{price} / 月」)。
|
||
- 显示「立即订阅」按钮(`#detail-actions-pre`),隐藏已付款区域(`#detail-paid`)。
|
||
6. 用户可点击「立即订阅」→ 进入 [角色付款流程](#412-角色付款流程)。
|
||
7. 用户可点击返回按钮(`data-action="back-to-library"`)→ `renderRoleLibrary()` + `showView('role-library')` 返回角色库。
|
||
|
||
**后置条件**:
|
||
- 角色详情视图展示角色头像、描述、价格、订阅按钮。
|
||
|
||
**异常处理**:
|
||
- 加载失败 → `alert('加载角色详情失败:' + err.message)` + `goBack()` 返回上一视图。
|
||
|
||
**涉及的 API 端点**:
|
||
- `GET /api/roles/:id`
|
||
|
||
---
|
||
|
||
### 4.7 新建角色流程(4 步表单)
|
||
|
||
**流程名称**:新建角色流程
|
||
|
||
**参与角色**:创作者(`isCreator === true`)
|
||
|
||
**前置条件**:
|
||
- 用户已登录且为创作者。
|
||
- 处于创作者管理中心。
|
||
|
||
**详细步骤**:
|
||
|
||
1. 用户在创作者管理中心「我的角色」Tab,点击「+ 新建角色」按钮(`data-action="new-role"`)。
|
||
2. 前端执行 `resetCreator()`:
|
||
- 重置表单(`form.reset()`)。
|
||
- 显示表单、隐藏结果面板。
|
||
- 清空 `generatedSoul`、`generatedConfig`、`editingRoleId = null`。
|
||
- `updateStep(0)` 回到第一步。
|
||
- `updateSystemPromptPreview()` 刷新系统提示词预览。
|
||
3. `showView('creator')` 切换到角色编辑视图。
|
||
4. **Step 1 — 基础身份**(`data-step="0"`):
|
||
- `agentId`:角色代号(必填,`pattern="[a-zA-Z0-9_]+"`,用于 config.yaml)。
|
||
- `displayName`:显示名称(必填)。
|
||
- `gender`:性别(select,默认 unknown)。
|
||
- `age`:年龄(选填)。
|
||
- 点击「下一步」(`data-action="next"`)→ `validateStep(0)` 校验 → `updateStep(1)`。
|
||
5. **Step 2 — 灵魂设定 Soul.md**(`data-step="1"`):
|
||
- `background`:背景故事(必填,textarea)。
|
||
- `personality`:性格标签(必填,逗号分隔)。
|
||
- `speechStyle`:说话风格(必填,textarea)。
|
||
- `likes`:喜好(选填)。
|
||
- `dislikes`:厌恶 / 底线(选填)。
|
||
- 「上一步」/「下一步」。
|
||
6. **Step 3 — 关系与记忆**(`data-step="2"`):
|
||
- `relationship`:与使用者的关系(选填)。
|
||
- `memories`:共同记忆 / 关键事件(选填,textarea)。
|
||
- `secrets`:秘密或未说出口的话(选填)。
|
||
- `greeting`:开场白(必填,textarea)。
|
||
- 「上一步」/「下一步」。
|
||
7. **Step 4 — 运行配置 config.yaml**(`data-step="3"`):
|
||
- `model`:模型(默认 `hermes-3-llama-3.1-70b`)。
|
||
- `temperature`:温度(number,0~2,默认 0.85)。
|
||
- `maxTokens`:最大 Token(number,默认 1024)。
|
||
- `systemPrompt`:系统提示词(textarea,留空自动生成)。
|
||
- `enableMemory`:启用长期记忆(checkbox,默认勾选)。
|
||
- `enableTools`:启用外部工具(checkbox,默认不勾选)。
|
||
- 点击「生成并发布」(`data-action="publish"`)。
|
||
8. `publish()` 执行(详见 [4.9 角色发布数据生成流程](#49-角色发布数据生成流程)):
|
||
- `validateStep(currentStep)` 校验当前步骤。
|
||
- `getFormData()` 收集表单数据。
|
||
- 若 `systemPrompt` 为空 → `buildSystemPrompt(data)` 自动生成。
|
||
- `generateSoulMd(data)` 生成 Soul.md。
|
||
- `generateConfigYaml(data)` 生成 config.yaml。
|
||
- 构建 payload(补充 `desc`、`price`、`avatar`、`temperature`、`maxTokens` 等)。
|
||
- 由于 `editingRoleId === null`,调用 `api('/roles', { method: 'POST', body: payload })`。
|
||
9. 后端 `POST /api/roles` 处理:
|
||
- 校验必填字段(`displayName`、`greeting`、`personality`、`background`、`speechStyle`)。
|
||
- `prisma.role.create()` 创建角色,`creatorId = req.userId`,`status = 'running'`。
|
||
- 返回 `{ role }`。
|
||
10. 前端收到响应:
|
||
- `form.hidden = true` 隐藏表单。
|
||
- `resultPanel.hidden = false` 显示结果面板。
|
||
- `renderPreview()` 渲染预览代码(默认显示 Soul.md)。
|
||
11. 结果面板展示:
|
||
- 「角色已蒸馏完成」标题。
|
||
- Soul.md 与 config.yaml 下载按钮(`data-download="soul"` / `data-download="config"`)。
|
||
- 预览 Tab 切换(Soul.md / config.yaml)。
|
||
- 「再创建一个」(`data-action="reset"`)与「返回管理中心」(`data-action="back-to-center"`)按钮。
|
||
|
||
**后置条件**:
|
||
- 数据库新增一条 Role 记录,`status = 'running'`,`creatorId` 为当前用户。
|
||
- 前端显示结果面板,可下载 Soul.md 与 config.yaml。
|
||
|
||
**异常处理**:
|
||
- 必填字段缺失 → 后端 `400 { error: '必填字段缺失' }`,前端 `alert('保存失败:' + err.message)`。
|
||
- 步骤校验失败 → `validateStep()` 调用 `input.reportValidity()` 显示浏览器原生校验提示,阻止进入下一步。
|
||
- 服务器异常 → `500 { error: '发布失败' }`。
|
||
|
||
**涉及的 API 端点**:
|
||
- `POST /api/roles`
|
||
|
||
---
|
||
|
||
### 4.8 编辑角色流程
|
||
|
||
**流程名称**:编辑角色流程
|
||
|
||
**参与角色**:创作者(角色创建者)
|
||
|
||
**前置条件**:
|
||
- 用户已登录且为创作者。
|
||
- 创作者管理中心「我的角色」列表已加载。
|
||
|
||
**详细步骤**:
|
||
|
||
1. 用户在创作者管理中心「我的角色」Tab,角色列表由 `renderCreatorRoles()` 渲染(调用 `GET /api/roles/my/roles`)。
|
||
2. 每个角色卡片含「编辑」按钮(`data-action="edit-role"`,`data-role-id="{id}"`)。
|
||
3. 用户点击「编辑」按钮。
|
||
4. 前端事件委托捕获:
|
||
- `await loadRoleForEdit(roleId)`。
|
||
- `showView('creator')`。
|
||
5. `loadRoleForEdit(roleId)` 执行:
|
||
- 调用 `api('/roles/${roleId}/full')` 获取角色完整信息。
|
||
6. 后端 `GET /api/roles/:id/full` 处理:
|
||
- `authMiddleware` 验证登录。
|
||
- `prisma.role.findUnique({ where: { id } })` 查询角色(含全部字段)。
|
||
- 角色不存在 → `404 { error: '角色不存在' }`。
|
||
- `role.creatorId !== req.userId` → `403 { error: '无权查看' }`(仅创建者可访问)。
|
||
- 返回 `{ role }`。
|
||
7. 前端填充表单:
|
||
- `editingRoleId = roleId`(标记为编辑模式)。
|
||
- 显示表单、隐藏结果面板。
|
||
- 逐字段填充:`displayName`、`gender`、`age`、`relationship`、`personality`、`background`、`speechStyle`、`likes`、`dislikes`、`memories`、`secrets`、`greeting`、`systemPrompt`、`model`、`temperature`(转 String)、`maxTokens`(转 String)、`price`(转 String)。
|
||
- 设置 `enableMemory`、`enableTools` checkbox 状态。
|
||
- `updateStep(0)` 回到第一步。
|
||
- `updateSystemPromptPreview()` 刷新预览。
|
||
8. 用户编辑各步骤字段(与新建流程一致的 4 步表单)。
|
||
9. 用户点击「生成并发布」(`data-action="publish"`)。
|
||
10. `publish()` 执行:
|
||
- 由于 `editingRoleId` 不为 `null`,调用 `api('/roles/${editingRoleId}', { method: 'PUT', body: payload })`。
|
||
11. 后端 `PUT /api/roles/:id` 处理:
|
||
- `prisma.role.findUnique({ where: { id } })` 查询角色。
|
||
- 角色不存在 → `404 { error: '角色不存在' }`。
|
||
- `existing.creatorId !== req.userId` → `403 { error: '无权编辑他人角色' }`。
|
||
- `prisma.role.update()` 更新字段(使用 `??` 运算符,未传入字段保留原值)。
|
||
- 返回 `{ role }`。
|
||
12. 前端显示结果面板(同新建流程)。
|
||
|
||
**后置条件**:
|
||
- 数据库中对应 Role 记录被更新。
|
||
- 前端显示结果面板,可下载更新后的 Soul.md 与 config.yaml。
|
||
|
||
**异常处理**:
|
||
- 角色不存在 → `404`。
|
||
- 非创建者编辑 → `403 { error: '无权编辑他人角色' }`。
|
||
- 加载角色数据失败 → `alert('加载角色数据失败:' + err.message)`。
|
||
- 保存失败 → `alert('保存失败:' + err.message)`。
|
||
|
||
**涉及的 API 端点**:
|
||
- `GET /api/roles/:id/full`(加载完整数据)
|
||
- `PUT /api/roles/:id`(提交编辑)
|
||
|
||
---
|
||
|
||
### 4.9 角色发布数据生成流程
|
||
|
||
**流程名称**:角色发布数据生成流程
|
||
|
||
**参与角色**:创作者
|
||
|
||
**前置条件**:
|
||
- 创作者已完成 4 步表单填写并点击「生成并发布」。
|
||
|
||
**详细步骤**:
|
||
|
||
1. `publish()` 被调用,首先 `validateStep(currentStep)` 校验当前步骤。
|
||
2. `getFormData()` 收集表单数据:
|
||
- 使用 `FormData` + `Object.fromEntries()` 获取所有字段。
|
||
- 单独处理 `enableMemory` 与 `enableTools`(checkbox,取 `.checked`)。
|
||
3. 若 `data.systemPrompt.trim()` 为空,调用 `buildSystemPrompt(data)` 自动生成系统提示词:
|
||
- 将 `personality` 按中英文逗号分割为标签数组,用「、」连接。
|
||
- 组装包含基本设定、性格、背景、说话风格、喜好、厌恶、共同记忆、内心秘密的中文提示词。
|
||
- 末尾强调「请始终保持角色一致性……像一个真实、有记忆、有情绪的人一样陪伴对方」。
|
||
4. `generateSoulMd(data)` 生成 Soul.md(Markdown 格式):
|
||
- 标题 `# Soul of {displayName}`。
|
||
- 包含 Identity、Background、Personality、Speech Style、Likes、Dislikes、Shared Memories、Secrets & Inner Voice、First Greeting 等章节。
|
||
- 性格标签用 ` | ` 分隔。
|
||
5. `generateConfigYaml(data)` 生成 config.yaml(YAML 格式):
|
||
- 若 `systemPrompt` 为空,使用 `buildSystemPrompt(data)` 的结果。
|
||
- `escapeYaml()` 对含特殊字符(`:`、`#`、换行、`"`)的值进行转义。
|
||
- 包含 `agent`(id、name、version)、`model`(name、temperature、max_tokens)、`system_prompt`、`memory`(enabled、storage、recall_depth)、`character`(soul_file、greeting)。
|
||
- 若 `enableTools` 为 true,追加 `tools` 块(search_memory、save_memory)。
|
||
6. 构建 payload,补充派生字段:
|
||
- `soulMd = generatedSoul`、`configYaml = generatedConfig`。
|
||
- `desc`:取 `personality` 前两个标签用「,」连接。
|
||
- `price`:`parseFloat(data.price) || 29.9`(表单无 price 字段,默认 29.9)。
|
||
- `avatar`:`data.avatar ||` 自动生成 URL(基于 displayName 的 text_to_image 接口)。
|
||
- `temperature`:`parseFloat(data.temperature) || 0.8`。
|
||
- `maxTokens`:`parseInt(data.maxTokens) || 2048`。
|
||
7. 根据 `editingRoleId` 决定请求方式:
|
||
- `null` → `POST /api/roles`(新建)。
|
||
- 非 `null` → `PUT /api/roles/:id`(编辑)。
|
||
8. 请求成功后:
|
||
- `form.hidden = true`、`resultPanel.hidden = false`。
|
||
- `renderPreview()` 将 `generatedSoul` 或 `generatedConfig` 写入 `<pre><code>` 预览区。
|
||
9. 用户可在结果面板:
|
||
- 切换预览 Tab(Soul.md / config.yaml)。
|
||
- 点击「下载」按钮下载对应文件(`download()` 通过 Blob + `<a download>` 实现)。
|
||
|
||
**后置条件**:
|
||
- Soul.md 与 config.yaml 内容已生成并持久化到数据库(`soulMd`、`configYaml` 字段)。
|
||
- 前端结果面板展示预览,支持下载。
|
||
|
||
**异常处理**:
|
||
- API 失败 → `alert('保存失败:' + err.message)`,表单保持显示,用户可修正后重试。
|
||
|
||
**涉及的 API 端点**:
|
||
- `POST /api/roles`(新建)或 `PUT /api/roles/:id`(编辑)
|
||
|
||
---
|
||
|
||
### 4.10 创作者中心管理流程
|
||
|
||
**流程名称**:创作者中心管理流程
|
||
|
||
**参与角色**:创作者
|
||
|
||
**前置条件**:
|
||
- 用户已登录且 `isCreator === true`。
|
||
|
||
**详细步骤**:
|
||
|
||
1. 用户进入创作者管理中心(登录后自动跳转,或通过首页卡片 / TabBar「我的」进入)。
|
||
2. `renderCreatorCenter()` 被调用,依次执行:
|
||
- `renderCreatorRoles()`:渲染「我的角色」Tab。
|
||
- `renderIncome()`:渲染「收入」Tab。
|
||
- `renderSettings()`:渲染「我的」Tab。
|
||
3. 三个 Tab 通过 `switchCenterTab(tab)` 切换(`data-center-tab` 属性):
|
||
- **roles**(我的角色,默认激活):
|
||
- `renderCreatorRoles()` 调用 `GET /api/roles/my/roles` 获取当前用户创建的全部角色。
|
||
- 渲染角色卡片(含头像、名称、运行状态、编辑按钮)。
|
||
- 列表底部有「+ 新建角色」按钮。
|
||
- 空列表显示「还没有创建角色,点击「新建角色」开始」。
|
||
- **income**(收入):
|
||
- `renderIncome()` 读取 `state.income`,若 `balance === 0` 则使用 `mockIncome`(Mock 数据:余额 ¥1280.50,3 条流水记录)。
|
||
- 显示可提现余额、流水明细列表。
|
||
- 包含「申请提现」表单(收款方式 select + 金额 input),提交时校验金额不超过余额,`alert` 提示「提现申请已提交」(Mock,无 API)。
|
||
- **settings**(我的):
|
||
- `renderSettings()` 将 `state.creatorName` 与 `state.libraryName` 填入设置表单。
|
||
- 设置表单提交 → 进入 [设置保存流程](#411-设置保存流程)。
|
||
- 「退出登录」按钮 → 进入 [用户登出流程](#44-用户登出流程)。
|
||
4. Tab 切换时更新 `#center-tab-label` 文本与 `aria-selected` 属性。
|
||
|
||
**后置条件**:
|
||
- 创作者中心三个 Tab 均可正常切换与展示数据。
|
||
|
||
**异常处理**:
|
||
- `GET /api/roles/my/roles` 失败 → 角色列表区域显示「加载失败:{err.message}」。
|
||
- 提现金额超过余额 → `alert('提现金额超过可提现余额')`。
|
||
|
||
**涉及的 API 端点**:
|
||
- `GET /api/roles/my/roles`(角色列表)
|
||
- `PUT /api/auth/settings`(设置保存,见 4.11)
|
||
|
||
---
|
||
|
||
### 4.11 设置保存流程
|
||
|
||
**流程名称**:设置保存流程
|
||
|
||
**参与角色**:创作者
|
||
|
||
**前置条件**:
|
||
- 用户在创作者管理中心「我的」Tab。
|
||
|
||
**详细步骤**:
|
||
|
||
1. 用户在设置表单(`#settings-form`)填写:
|
||
- `creatorName`:创作者名字(笔名)。
|
||
- `libraryName`:角色库名称(首页「我的 XXX」显示文字)。
|
||
2. 用户点击「保存设置」按钮提交。
|
||
3. 前端 `submit` 事件触发:
|
||
- `FormData` 收集 `creatorName` 与 `libraryName`。
|
||
- `api('/auth/settings', { method: 'PUT', body: { creatorName, libraryName } })`。
|
||
4. 后端 `PUT /api/auth/settings` 处理:
|
||
- `authMiddleware` 验证登录。
|
||
- `prisma.user.update({ where: { id: req.userId }, data: { creatorName, libraryName } })`(仅更新传入字段)。
|
||
- 返回 `{ user: { id, account, isCreator, creatorName, libraryName } }`。
|
||
5. 前端收到响应:
|
||
- 更新 `state.creatorName` 与 `state.libraryName`(若 `libraryName` 为空则回退为 `'我的 [XXX]'`)。
|
||
- `saveState()` 持久化。
|
||
- `updateLandingCard()` 更新首页卡片显示。
|
||
- `alert('设置已保存')` 提示成功。
|
||
|
||
**后置条件**:
|
||
- 数据库 User 记录的 `creatorName`、`libraryName` 字段更新。
|
||
- 前端状态与首页卡片同步更新。
|
||
|
||
**异常处理**:
|
||
- 未登录 → `401 { error: '未登录' }` 或 `401 { error: '登录已过期,请重新登录' }`。
|
||
- 服务器异常 → `500 { error: '更新失败' }`,前端 `alert('保存失败:' + err.message)`。
|
||
|
||
**涉及的 API 端点**:
|
||
- `PUT /api/auth/settings`
|
||
|
||
---
|
||
|
||
### 4.12 角色付款流程
|
||
|
||
**流程名称**:角色付款流程
|
||
|
||
**参与角色**:已登录的普通用户
|
||
|
||
**前置条件**:
|
||
- 用户处于角色详情视图,`currentRole` 已加载。
|
||
|
||
**详细步骤**:
|
||
|
||
1. 用户在角色详情视图点击「立即订阅」按钮(`data-action="pay"`)。
|
||
2. 前端 `payRole()` 执行(**当前为 Mock 实现,无 API 调用、无订单创建**):
|
||
- 隐藏「立即订阅」按钮区域(`#detail-actions-pre`)。
|
||
- 显示已付款区域(`#detail-paid`)。
|
||
- `#detail-qr` 显示二维码占位符「扫码连接 AI 角色」。
|
||
- `#detail-avatar` 设置为 `currentRole.avatar` 背景图。
|
||
3. 已付款区域显示提示文案:「扫码添加后,请将下方头像保存并设置为该联系人的备注头像,获得更完整的体验」。
|
||
4. 用户可点击「下载角色头像」按钮(`data-action="download-avatar"`):
|
||
- 调用 `download(currentRole.name + '_avatar.png', '')`(内容为空)。
|
||
- `window.open(currentRole.avatar, '_blank')` 在新窗口打开头像图片。
|
||
|
||
**后置条件**:
|
||
- 详情视图切换为「已付款」状态,显示二维码占位与头像。
|
||
|
||
**异常处理**:
|
||
- 此流程为纯前端 Mock,无异常处理逻辑。
|
||
|
||
> ⚠️ **注意**:当前付款流程为前端 Mock,未调用后端 API,未创建 Order 记录。`Order` 数据模型已定义但未投入使用。未来需对接真实支付并创建订单。
|
||
|
||
**涉及的 API 端点**:
|
||
- 无(Mock 实现)
|
||
|
||
---
|
||
|
||
### 4.13 导航流程
|
||
|
||
**流程名称**:导航流程
|
||
|
||
**参与角色**:所有用户
|
||
|
||
**前置条件**:
|
||
- 应用已加载。
|
||
|
||
**详细步骤**:
|
||
|
||
**A. 视图(View)系统**
|
||
|
||
应用包含 9 个视图,通过 `.active` class 控制显示:
|
||
|
||
| 视图 ID | 标签 | 说明 |
|
||
| --- | --- | --- |
|
||
| `landing` | 首页 | 落地页,含两张卡片与底部链接 |
|
||
| `auth` | 登录 / 注册 | 认证视图,含登录与注册两个 Tab |
|
||
| `role-library` | 角色库 | 角色列表 |
|
||
| `role-detail` | 角色详情 | 单个角色详情 |
|
||
| `distill` | 蒸馏前任 | 蒸馏服务介绍页 |
|
||
| `about` | 关于 Eternal AI | 平台介绍与 FAQ |
|
||
| `onboarding` | 创作者入驻 | 创作者合作说明 |
|
||
| `creator-center` | 创作者管理中心 | 三 Tab 管理面板 |
|
||
| `creator` | 角色编辑 | 4 步角色创建/编辑表单 |
|
||
|
||
**B. 视图切换与历史栈**
|
||
|
||
- `showView(name, trackHistory = true)`:
|
||
- 切换所有视图的 `.active` class。
|
||
- 若 `trackHistory` 为 `true` 且新视图与栈顶不同,将 `name` 压入 `viewHistory` 数组。
|
||
- 滚动到顶部。
|
||
- `updateTabBar(name)` 同步底部 TabBar 高亮。
|
||
- 无障碍:将焦点移至新视图,通过 `#sr-announce` live region 播报视图名称。
|
||
- `goBack()`:
|
||
- 弹出 `viewHistory` 栈顶,显示前一个视图(`trackHistory = false` 避免重复压栈)。
|
||
- 若栈中仅剩一个元素,返回 `landing`。
|
||
|
||
**C. 首页卡片导航**(`data-action`)
|
||
|
||
| 动作 | 行为 |
|
||
| --- | --- |
|
||
| `open-characters` | 未登录 → auth 视图;创作者 → creator-center;普通用户 → role-library |
|
||
| `open-distill` | → distill 视图 |
|
||
| `open-about` | → about 视图 |
|
||
| `open-onboarding` | → onboarding 视图 |
|
||
|
||
**D. 底部 TabBar**(`data-tab-action`)
|
||
|
||
| Tab 动作 | 行为 |
|
||
| --- | --- |
|
||
| `tab-home` | → landing 视图 |
|
||
| `tab-distill` | → distill 视图 |
|
||
| `tab-mine` | 未登录 → auth;创作者 → creator-center;普通用户 → role-library |
|
||
|
||
`updateTabBar(viewName)` 根据当前视图映射高亮对应 Tab(landing→home,distill→distill,role-library/creator-center→mine)。
|
||
|
||
**E. 返回按钮**(`data-action`)
|
||
|
||
| 动作 | 行为 |
|
||
| --- | --- |
|
||
| `back` | `goBack()` 返回历史栈上一视图 |
|
||
| `back-to-library` | 重新渲染角色库并切换到 role-library |
|
||
| `back-to-center` | 切换到 creator-center 并重新渲染 |
|
||
|
||
**后置条件**:
|
||
- 视图正确切换,历史栈与 TabBar 状态同步。
|
||
|
||
**异常处理**:
|
||
- 无异常风险(纯前端导航)。
|
||
|
||
**涉及的 API 端点**:
|
||
- 无
|
||
|
||
---
|
||
|
||
### 4.14 表单验证流程
|
||
|
||
**流程名称**:表单验证流程
|
||
|
||
**参与角色**:所有需要填写表单的用户
|
||
|
||
**前置条件**:
|
||
- 用户处于含表单的视图。
|
||
|
||
**详细步骤**:
|
||
|
||
**A. 注册表单验证**(`#register-form`)
|
||
|
||
| 字段 | 验证规则 | 实现方式 |
|
||
| --- | --- | --- |
|
||
| `account` | 必填 | HTML `required` + `checkValidity()` |
|
||
| `password` | 必填,最少 6 位 | HTML `required` + `minlength="6"` |
|
||
| `confirmPassword` | 必填,且与 `password` 一致 | `validatePasswordMatch()`:比较两次值,不一致则 `setCustomValidity('两次输入的密码不一致')` + `reportValidity()` |
|
||
|
||
- 提交时先调用 `validatePasswordMatch(authForm)`,返回 `false` 则阻止提交。
|
||
- 后端二次校验:`account` 与 `password` 非空、`password.length >= 6`。
|
||
|
||
**B. 登录表单验证**(`#login-form`)
|
||
|
||
| 字段 | 验证规则 |
|
||
| --- | --- |
|
||
| `account` | 必填 |
|
||
| `password` | 必填 |
|
||
|
||
- `validatePasswordMatch()` 检测到无 `confirmPassword` 字段,直接返回 `true`。
|
||
- 后端校验 `account` 与 `password` 非空。
|
||
|
||
**C. 角色编辑表单验证**(`#character-form`,4 步)
|
||
|
||
每步通过 `validateStep(index)` 校验:遍历该步骤内所有 `input`、`textarea`、`select`,调用 `input.checkValidity()`,不通过则 `input.reportValidity()` 显示浏览器原生提示。
|
||
|
||
| 步骤 | 字段 | 验证规则 |
|
||
| --- | --- | --- |
|
||
| Step 1 | `agentId` | 必填,`pattern="[a-zA-Z0-9_]+"`(仅英文/数字/下划线) |
|
||
| Step 1 | `displayName` | 必填 |
|
||
| Step 1 | `gender` | select,默认 `unknown`,无需校验 |
|
||
| Step 1 | `age` | 选填 |
|
||
| Step 2 | `background` | 必填 |
|
||
| Step 2 | `personality` | 必填 |
|
||
| Step 2 | `speechStyle` | 必填 |
|
||
| Step 2 | `likes` / `dislikes` | 选填 |
|
||
| Step 3 | `relationship` | 选填 |
|
||
| Step 3 | `memories` / `secrets` | 选填 |
|
||
| Step 3 | `greeting` | 必填 |
|
||
| Step 4 | `model` | 默认值,无需校验 |
|
||
| Step 4 | `temperature` | `type="number"`,`min="0"` `max="2"` `step="0.05"` |
|
||
| Step 4 | `maxTokens` | `type="number"`,`min="1"` |
|
||
| Step 4 | `systemPrompt` | 选填(留空自动生成) |
|
||
|
||
- 「下一步」按钮(`data-action="next"`):先 `validateStep(currentStep)`,通过才 `updateStep(currentStep + 1)`。
|
||
- 「生成并发布」(`data-action="publish"`):先 `validateStep(currentStep)` 校验当前(第 4)步。
|
||
- 后端 `POST /api/roles` 二次校验必填字段:`displayName`、`greeting`、`personality`、`background`、`speechStyle`,缺失返回 `400 { error: '必填字段缺失' }`。
|
||
|
||
**D. 设置表单验证**(`#settings-form`)
|
||
|
||
| 字段 | 验证规则 |
|
||
| --- | --- |
|
||
| `creatorName` | 选填(`type="text"`) |
|
||
| `libraryName` | 选填(`type="text"`) |
|
||
|
||
- 无前端必填校验,直接提交。
|
||
- 后端 `PUT /api/auth/settings` 仅更新传入字段。
|
||
|
||
**E. 提现表单验证**(`#withdraw-form`)
|
||
|
||
| 字段 | 验证规则 |
|
||
| --- | --- |
|
||
| `method` | `required`,select(wechat/alipay) |
|
||
| `amount` | `required`,`type="number"` `min="1"` `step="0.01"` |
|
||
|
||
- 提交时前端额外校验 `amount` 不超过可提现余额(`state.income.balance` 与 `mockIncome.balance` 取较大者比较),超过则 `alert('提现金额超过可提现余额')`。
|
||
|
||
**后置条件**:
|
||
- 校验通过则继续后续流程;不通过则阻止提交并提示用户。
|
||
|
||
**异常处理**:
|
||
- 前端校验失败:浏览器原生 `reportValidity()` 提示或 `alert`。
|
||
- 后端校验失败:返回 4xx + `{ error }`,前端 `alert` 显示错误信息。
|
||
|
||
**涉及的 API 端点**:
|
||
- 所有涉及表单提交的端点(注册、登录、角色发布/编辑、设置保存)。
|
||
|
||
---
|
||
|
||
## 五、附录
|
||
|
||
### 5.1 前端状态结构
|
||
|
||
前端状态 `state` 存储于 `app.js`,默认结构如下:
|
||
|
||
```javascript
|
||
const defaultState = {
|
||
isLoggedIn: false, // 是否已登录
|
||
isCreator: false, // 是否为创作者
|
||
account: null, // 登录账号
|
||
userId: null, // 用户 ID
|
||
boundCreator: null, // 绑定的创作者
|
||
libraryName: '我的 [XXX]', // 角色库名称
|
||
creatorName: '', // 创作者笔名
|
||
roles: [], // 创作者的角色列表
|
||
income: { balance: 0, records: [] }, // 收入数据
|
||
};
|
||
```
|
||
|
||
- `loadState()`:从 localStorage 读取并与 `defaultState` 合并。
|
||
- `saveState()`:将 `state` 序列化写入 localStorage。
|
||
- `applyUserData(user)`:根据后端返回的 user 对象更新 `state` 并 `saveState()`。
|
||
|
||
### 5.2 localStorage 键名约定
|
||
|
||
| 键名 | 内容 | 说明 |
|
||
| --- | --- | --- |
|
||
| `eternal_ai_token` | JWT 字符串 | 登录凭证,由 `setToken()` / `getToken()` 管理 |
|
||
| `eternal_ai_state` | JSON 序列化的 `state` 对象 | 前端状态持久化 |
|
||
|
||
### 5.3 已知限制与待办
|
||
|
||
1. **付款流程为 Mock**:`Order` 模型已定义但未使用,角色付款([4.12](#412-角色付款流程))为纯前端模拟,未对接真实支付、未创建订单记录。
|
||
2. **收入数据为 Mock**:`renderIncome()` 在 `state.income.balance === 0` 时回退到 `mockIncome`(硬编码数据),无后端收入 API。
|
||
3. **提现流程为 Mock**:提现表单提交仅 `alert` 提示,无后端处理。
|
||
4. **角色表单无 `price` 与 `avatar` 输入框**:`publish()` 中 `data.price` 与 `data.avatar` 为 `undefined`,分别回退到默认值 `29.9` 与自动生成的头像 URL。
|
||
5. **`mockRoles` 未使用**:`app.js` 中定义了 `mockRoles` 数组,但 `renderRoleLibrary()` 始终调用 `GET /api/roles` 获取真实数据,该数组未被引用。
|
||
6. **JWT Secret 默认值**:`src/lib/auth.js` 中 `JWT_SECRET` 默认值为 `'eternalai_jwt_secret_2026_change_in_prod'`,生产环境必须通过 `.env` 的 `JWT_SECRET` 覆盖。
|
||
7. **无角色删除功能**:后端未提供 `DELETE /api/roles/:id` 端点,前端无删除入口。
|
||
8. **无角色上下架切换**:`status` 字段支持 `running`/`stopped`,但前端无切换入口,新建角色默认 `running`。
|
||
9. **蒸馏前任服务为展示页**:`distill` 视图为静态介绍页,「立即下单」按钮仅 `alert` 提示,无实际下单流程。
|