# 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` 提示，无实际下单流程。
