EternalAI/docs/business-processes.md

1078 lines
51 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)`bcryptsalt 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`温度number0~2默认 0.85)。
- `maxTokens`最大 Tokennumber默认 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.mdMarkdown 格式
- 标题 `# Soul of {displayName}`
- 包含 IdentityBackgroundPersonalitySpeech StyleLikesDislikesShared MemoriesSecrets & Inner VoiceFirst Greeting 等章节
- 性格标签用 ` | ` 分隔
5. `generateConfigYaml(data)` 生成 config.yamlYAML 格式
- `systemPrompt` 为空使用 `buildSystemPrompt(data)` 的结果
- `escapeYaml()` 对含特殊字符`:`、`#`、换行、`"`的值进行转义
- 包含 `agent`idnameversion)、`model`nametemperaturemax_tokens)、`system_prompt`、`memory`enabledstoragerecall_depth)、`character`soul_filegreeting)。
- `enableTools` true追加 `tools` search_memorysave_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. 用户可在结果面板
- 切换预览 TabSoul.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.503 条流水记录)。
- 显示可提现余额流水明细列表
- 包含申请提现表单收款方式 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)` 根据当前视图映射高亮对应 Tablandinghomedistilldistillrole-library/creator-centermine)。
**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`selectwechat/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` 提示,无实际下单流程。