51 KiB
Eternal AI 业务流程文档
本文档详细描述 Eternal AI 平台的系统架构、数据模型、API 端点清单以及全部业务流程,供新成员快速上手开发与维护。
- 项目名称:EternalAI
- 版本:1.0.0
- 描述:AI 陪伴平台 — 人设创作者设定发布,生成 Hermes agent 配置文件
- 作者:chigulong
- 许可证:MIT
目录
一、系统架构概述
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通过.activeclass 切换显示,viewHistory数组维护返回栈。 - 所有 API 请求经
api()封装,自动注入Authorization: Bearer <token>头部。 - JWT Token 与前端状态分别存储在 localStorage 的两个键中(见 5.2)。
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)。
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):
{
"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 用户注册流程
流程名称:用户注册流程
参与角色:未登录访客
前置条件:
- 用户处于未登录状态。
- 数据库可正常访问。
详细步骤:
- 用户进入首页(
landing视图),点击「我的 [XXX]」卡片(data-action="open-characters")。 - 前端检测
state.isLoggedIn === false,调用switchAuthTab('login')切换到登录 Tab,并showView('auth')进入认证视图。 - 用户点击「注册」Tab(
data-tab="register"),切换到注册表单(#register-form)。 - 用户填写表单:
account(手机号 / 用户名,必填)password(密码,必填,minlength="6")confirmPassword(确认密码,必填)
- 用户点击「注册」按钮提交表单。
- 前端
submit事件触发:- 调用
validatePasswordMatch()校验两次密码是否一致,不一致则setCustomValidity('两次输入的密码不一致')并阻止提交。 - 通过
api('/auth/register', { method: 'POST', body: { account, password } })发起注册请求。
- 调用
- 后端
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 }。
- 校验
- 前端收到响应:
setToken(result.token)将 JWT 存入 localStorage。applyUserData(result.user)更新state并saveState()。
- 注册后自动成为创作者:前端额外发起
PUT /api/auth/settings,请求体{ isCreator: true, creatorName: data.account }。- 后端更新用户
isCreator = true、creatorName = account,返回更新后的 user。
- 后端更新用户
- 前端更新
state.isCreator = true、state.creatorName = data.account,saveState()+updateLandingCard()。 - 由于
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/registerPUT /api/auth/settings(注册成功后自动调用以开启创作者身份)
4.2 用户登录流程
流程名称:用户登录流程
参与角色:已注册用户
前置条件:
- 用户已有账号。
- 处于未登录状态。
详细步骤:
- 用户进入首页,点击「我的 [XXX]」卡片(
data-action="open-characters")。 - 前端检测未登录,
switchAuthTab('login')+showView('auth')。 - 用户在登录表单(
#login-form)填写:account(手机号 / 用户名,必填)password(密码,必填)
- 用户点击「登录」提交。
- 前端
submit事件触发:validatePasswordMatch()(登录表单无 confirmPassword,直接返回true)。api('/auth/login', { method: 'POST', body: { account, password } })。
- 后端
POST /api/auth/login处理:- 校验
account与password非空。 prisma.user.findUnique({ where: { account } })查询用户。- 用户不存在或
verifyPassword(password, user.password)失败 → 返回401 { error: '账号或密码错误' }。 - 验证通过,
signToken(user.id)生成 JWT,返回{ token, user }。
- 校验
- 前端收到响应:
setToken(result.token)存 Token。applyUserData(result.user)更新状态。
- 根据
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)。
详细步骤:
- 页面加载时,
app.js末尾的 IIFE(立即执行函数)自动执行:const token = getToken(); // 读取 localStorage['eternal_ai_token'] if (!token) return; // 无 Token 则跳过 - 若存在 Token,调用
api('/auth/me')(GET 请求,自动携带Authorization: Bearer <token>)。 - 后端
GET /api/auth/me处理:authMiddleware解析 Token:提取userId,无效则返回401。prisma.user.findUnique({ where: { id: req.userId } })查询用户。- 用户不存在 →
404 { error: '用户不存在' }。 - 返回
{ user: { id, account, isCreator, creatorName, libraryName, boundCreator } }。
- 前端收到响应:
applyUserData(user)恢复state.isLoggedIn = true及各项用户字段。saveState()持久化状态。updateLandingCard()更新首页卡片显示。
- 同时,页面初始化阶段已执行
loadState()从 localStorage 恢复state(含isLoggedIn等),保证首屏渲染时即显示登录态。
后置条件:
- 用户登录态恢复,首页卡片显示对应内容(创作者显示「进入管理中心」,普通用户显示「进入角色库」)。
异常处理:
- Token 过期或无效 →
api('/auth/me')抛错,catch块执行setToken('')清除 Token(用户需重新登录)。 - 网络错误 → Token 保留,下次刷新重试。
涉及的 API 端点:
GET /api/auth/me
4.4 用户登出流程
流程名称:用户登出流程
参与角色:已登录用户(创作者)
前置条件:
- 用户已登录并处于创作者管理中心。
详细步骤:
- 用户在创作者管理中心点击「我的」Tab(
data-center-tab="settings"),切换到设置面板。 - 用户点击「退出登录」按钮(
data-action="logout")。 - 前端
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。
详细步骤:
- 用户在首页点击「我的 [XXX]」卡片(
data-action="open-characters")。 - 前端检测
state.isLoggedIn === true且state.isCreator === false,执行:renderRoleLibrary()渲染角色库。showView('role-library')切换视图。
renderRoleLibrary()执行:- 设置标题为
state.libraryName || '我的角色库'。 - 列表区域显示「加载中…」。
- 调用
api('/roles')获取角色列表。
- 设置标题为
- 后端
GET /api/roles处理:prisma.role.findMany({ where: { status: 'running' }, orderBy: { createdAt: 'desc' }, select: { id, displayName, avatar, desc, price, status } })。- 返回
{ roles: [...] }。
- 前端渲染:
- 若
roles.length === 0→ 清空列表,显示空状态提示(#library-empty:「你还没有绑定专属创作者」)。 - 否则,将每个角色渲染为
.role-card,包含头像、名称、简介、价格,卡片带data-role-id属性。
- 若
- 用户点击任意角色卡片 → 进入 角色详情查看流程。
后置条件:
- 角色库视图展示所有上架角色卡片。
异常处理:
- API 失败 → 列表区域显示「加载失败:{err.message}」。
涉及的 API 端点:
GET /api/roles
4.6 角色详情查看流程
流程名称:角色详情查看流程
参与角色:已登录的普通用户
前置条件:
- 用户处于角色库视图,角色列表已加载。
详细步骤:
- 用户在角色库点击角色卡片(
.role-card,含data-role-id)。 - 前端事件委托捕获点击:
- 调用
renderRoleDetail(roleId)。 showView('role-detail')切换到角色详情视图。
- 调用
renderRoleDetail(roleId)执行:- 调用
api('/roles/${roleId}')获取角色详情。
- 调用
- 后端
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 }。
- 前端渲染详情:
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)。
- 用户可点击「立即订阅」→ 进入 角色付款流程。
- 用户可点击返回按钮(
data-action="back-to-library")→renderRoleLibrary()+showView('role-library')返回角色库。
后置条件:
- 角色详情视图展示角色头像、描述、价格、订阅按钮。
异常处理:
- 加载失败 →
alert('加载角色详情失败:' + err.message)+goBack()返回上一视图。
涉及的 API 端点:
GET /api/roles/:id
4.7 新建角色流程(4 步表单)
流程名称:新建角色流程
参与角色:创作者(isCreator === true)
前置条件:
- 用户已登录且为创作者。
- 处于创作者管理中心。
详细步骤:
- 用户在创作者管理中心「我的角色」Tab,点击「+ 新建角色」按钮(
data-action="new-role")。 - 前端执行
resetCreator():- 重置表单(
form.reset())。 - 显示表单、隐藏结果面板。
- 清空
generatedSoul、generatedConfig、editingRoleId = null。 updateStep(0)回到第一步。updateSystemPromptPreview()刷新系统提示词预览。
- 重置表单(
showView('creator')切换到角色编辑视图。- 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)。
- Step 2 — 灵魂设定 Soul.md(
data-step="1"):background:背景故事(必填,textarea)。personality:性格标签(必填,逗号分隔)。speechStyle:说话风格(必填,textarea)。likes:喜好(选填)。dislikes:厌恶 / 底线(选填)。- 「上一步」/「下一步」。
- Step 3 — 关系与记忆(
data-step="2"):relationship:与使用者的关系(选填)。memories:共同记忆 / 关键事件(选填,textarea)。secrets:秘密或未说出口的话(选填)。greeting:开场白(必填,textarea)。- 「上一步」/「下一步」。
- 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")。
publish()执行(详见 4.9 角色发布数据生成流程):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 })。
- 后端
POST /api/roles处理:- 校验必填字段(
displayName、greeting、personality、background、speechStyle)。 prisma.role.create()创建角色,creatorId = req.userId,status = 'running'。- 返回
{ role }。
- 校验必填字段(
- 前端收到响应:
form.hidden = true隐藏表单。resultPanel.hidden = false显示结果面板。renderPreview()渲染预览代码(默认显示 Soul.md)。
- 结果面板展示:
- 「角色已蒸馏完成」标题。
- 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 编辑角色流程
流程名称:编辑角色流程
参与角色:创作者(角色创建者)
前置条件:
- 用户已登录且为创作者。
- 创作者管理中心「我的角色」列表已加载。
详细步骤:
- 用户在创作者管理中心「我的角色」Tab,角色列表由
renderCreatorRoles()渲染(调用GET /api/roles/my/roles)。 - 每个角色卡片含「编辑」按钮(
data-action="edit-role",data-role-id="{id}")。 - 用户点击「编辑」按钮。
- 前端事件委托捕获:
await loadRoleForEdit(roleId)。showView('creator')。
loadRoleForEdit(roleId)执行:- 调用
api('/roles/${roleId}/full')获取角色完整信息。
- 调用
- 后端
GET /api/roles/:id/full处理:authMiddleware验证登录。prisma.role.findUnique({ where: { id } })查询角色(含全部字段)。- 角色不存在 →
404 { error: '角色不存在' }。 role.creatorId !== req.userId→403 { error: '无权查看' }(仅创建者可访问)。- 返回
{ role }。
- 前端填充表单:
editingRoleId = roleId(标记为编辑模式)。- 显示表单、隐藏结果面板。
- 逐字段填充:
displayName、gender、age、relationship、personality、background、speechStyle、likes、dislikes、memories、secrets、greeting、systemPrompt、model、temperature(转 String)、maxTokens(转 String)、price(转 String)。 - 设置
enableMemory、enableToolscheckbox 状态。 updateStep(0)回到第一步。updateSystemPromptPreview()刷新预览。
- 用户编辑各步骤字段(与新建流程一致的 4 步表单)。
- 用户点击「生成并发布」(
data-action="publish")。 publish()执行:- 由于
editingRoleId不为null,调用api('/roles/${editingRoleId}', { method: 'PUT', body: payload })。
- 由于
- 后端
PUT /api/roles/:id处理:prisma.role.findUnique({ where: { id } })查询角色。- 角色不存在 →
404 { error: '角色不存在' }。 existing.creatorId !== req.userId→403 { error: '无权编辑他人角色' }。prisma.role.update()更新字段(使用??运算符,未传入字段保留原值)。- 返回
{ role }。
- 前端显示结果面板(同新建流程)。
后置条件:
- 数据库中对应 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 步表单填写并点击「生成并发布」。
详细步骤:
publish()被调用,首先validateStep(currentStep)校验当前步骤。getFormData()收集表单数据:- 使用
FormData+Object.fromEntries()获取所有字段。 - 单独处理
enableMemory与enableTools(checkbox,取.checked)。
- 使用
- 若
data.systemPrompt.trim()为空,调用buildSystemPrompt(data)自动生成系统提示词:- 将
personality按中英文逗号分割为标签数组,用「、」连接。 - 组装包含基本设定、性格、背景、说话风格、喜好、厌恶、共同记忆、内心秘密的中文提示词。
- 末尾强调「请始终保持角色一致性……像一个真实、有记忆、有情绪的人一样陪伴对方」。
- 将
generateSoulMd(data)生成 Soul.md(Markdown 格式):- 标题
# Soul of {displayName}。 - 包含 Identity、Background、Personality、Speech Style、Likes、Dislikes、Shared Memories、Secrets & Inner Voice、First Greeting 等章节。
- 性格标签用
|分隔。
- 标题
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)。
- 若
- 构建 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。
- 根据
editingRoleId决定请求方式:null→POST /api/roles(新建)。- 非
null→PUT /api/roles/:id(编辑)。
- 请求成功后:
form.hidden = true、resultPanel.hidden = false。renderPreview()将generatedSoul或generatedConfig写入<pre><code>预览区。
- 用户可在结果面板:
- 切换预览 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。
详细步骤:
- 用户进入创作者管理中心(登录后自动跳转,或通过首页卡片 / TabBar「我的」进入)。
renderCreatorCenter()被调用,依次执行:renderCreatorRoles():渲染「我的角色」Tab。renderIncome():渲染「收入」Tab。renderSettings():渲染「我的」Tab。
- 三个 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(我的):
- roles(我的角色,默认激活):
- 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。
详细步骤:
- 用户在设置表单(
#settings-form)填写:creatorName:创作者名字(笔名)。libraryName:角色库名称(首页「我的 XXX」显示文字)。
- 用户点击「保存设置」按钮提交。
- 前端
submit事件触发:FormData收集creatorName与libraryName。api('/auth/settings', { method: 'PUT', body: { creatorName, libraryName } })。
- 后端
PUT /api/auth/settings处理:authMiddleware验证登录。prisma.user.update({ where: { id: req.userId }, data: { creatorName, libraryName } })(仅更新传入字段)。- 返回
{ user: { id, account, isCreator, creatorName, libraryName } }。
- 前端收到响应:
- 更新
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已加载。
详细步骤:
- 用户在角色详情视图点击「立即订阅」按钮(
data-action="pay")。 - 前端
payRole()执行(当前为 Mock 实现,无 API 调用、无订单创建):- 隐藏「立即订阅」按钮区域(
#detail-actions-pre)。 - 显示已付款区域(
#detail-paid)。 #detail-qr显示二维码占位符「扫码连接 AI 角色」。#detail-avatar设置为currentRole.avatar背景图。
- 隐藏「立即订阅」按钮区域(
- 已付款区域显示提示文案:「扫码添加后,请将下方头像保存并设置为该联系人的备注头像,获得更完整的体验」。
- 用户可点击「下载角色头像」按钮(
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):- 切换所有视图的
.activeclass。 - 若
trackHistory为true且新视图与栈顶不同,将name压入viewHistory数组。 - 滚动到顶部。
updateTabBar(name)同步底部 TabBar 高亮。- 无障碍:将焦点移至新视图,通过
#sr-announcelive 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,默认结构如下:
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 已知限制与待办
- 付款流程为 Mock:
Order模型已定义但未使用,角色付款(4.12)为纯前端模拟,未对接真实支付、未创建订单记录。 - 收入数据为 Mock:
renderIncome()在state.income.balance === 0时回退到mockIncome(硬编码数据),无后端收入 API。 - 提现流程为 Mock:提现表单提交仅
alert提示,无后端处理。 - 角色表单无
price与avatar输入框:publish()中data.price与data.avatar为undefined,分别回退到默认值29.9与自动生成的头像 URL。 mockRoles未使用:app.js中定义了mockRoles数组,但renderRoleLibrary()始终调用GET /api/roles获取真实数据,该数组未被引用。- JWT Secret 默认值:
src/lib/auth.js中JWT_SECRET默认值为'eternalai_jwt_secret_2026_change_in_prod',生产环境必须通过.env的JWT_SECRET覆盖。 - 无角色删除功能:后端未提供
DELETE /api/roles/:id端点,前端无删除入口。 - 无角色上下架切换:
status字段支持running/stopped,但前端无切换入口,新建角色默认running。 - 蒸馏前任服务为展示页:
distill视图为静态介绍页,「立即下单」按钮仅alert提示,无实际下单流程。