From fc53fa2e58446aa42251c4a498410cef34614e7c Mon Sep 17 00:00:00 2001 From: chiguyong Date: Sat, 20 Jun 2026 23:50:53 +0800 Subject: [PATCH] ci: add CI/CD deployment scripts with PM2, Nginx, and auto-setup - Add ecosystem.config.js for PM2 process management - Add deploy/setup-server.sh for one-shot server initialization (auto-detects OS, installs Node.js 20/PostgreSQL 15/PM2/Nginx) - Add deploy/deploy.sh for repeatable deployments (pull -> install -> migrate -> reload -> health check) - Add deploy/nginx.conf reverse proxy template with security headers - Rewrite .gitea/workflows/deploy.yml with full CI/CD pipeline (checkout -> build -> migrate -> deploy -> health check) - Add .env.example template with DATABASE_URL/JWT_SECRET/PORT/ALLOWED_ORIGINS - Add docs/deployment.md (full deployment guide) and docs/business-processes.md - Update package.json scripts (db:generate, test:e2e, deploy) - Add logs/ to .gitignore --- .env.example | 18 + .gitea/workflows/deploy.yml | 69 ++- .gitignore | 1 + deploy/deploy.sh | 99 ++++ deploy/nginx.conf | 87 +++ deploy/setup-server.sh | 197 +++++++ docs/business-processes.md | 1077 +++++++++++++++++++++++++++++++++++ docs/deployment.md | 332 +++++++++++ ecosystem.config.js | 23 + package.json | 5 +- 10 files changed, 1897 insertions(+), 11 deletions(-) create mode 100644 .env.example create mode 100644 deploy/deploy.sh create mode 100644 deploy/nginx.conf create mode 100644 deploy/setup-server.sh create mode 100644 docs/business-processes.md create mode 100644 docs/deployment.md create mode 100644 ecosystem.config.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a39c623 --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# ===== EternalAI 环境变量配置 ===== +# 复制此文件为 .env 并填写真实值 +# cp .env.example .env + +# 数据库连接字符串 +# 格式: postgresql://用户名:密码@主机:端口/数据库名 +DATABASE_URL="postgresql://eternalai:YOUR_PASSWORD@localhost:5432/eternalai" + +# JWT 密钥 — 生产环境必须修改为随机长字符串(至少 32 字节) +# 生成方法: openssl rand -base64 48 +JWT_SECRET="PLEASE_CHANGE_THIS_TO_A_RANDOM_SECRET" + +# 服务端口 +PORT=3001 + +# CORS 允许的来源(逗号分隔,生产环境应限制为实际域名) +# 留空则允许所有来源(不推荐) +ALLOWED_ORIGINS="" diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 69a9ce2..ccadb87 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,23 +1,72 @@ name: Deploy EternalAI + on: push: branches: - master + workflow_dispatch: jobs: deploy: runs-on: self-hosted steps: - - name: Checkout code - uses: actions/checkout@v2 + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v2 - with: - node-version: '18' + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' - - name: Install dependencies - run: npm install + - name: Install dependencies + run: npm install - - name: Start server - run: npm start + - name: Generate Prisma Client + run: npx prisma generate + + - name: Push database schema + run: npx prisma db push + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + + - name: Create logs directory + run: mkdir -p logs + + - name: Restart PM2 process + run: | + if pm2 describe eternalai &>/dev/null; then + pm2 reload ecosystem.config.js --update-env + echo "PM2 process reloaded" + else + pm2 start ecosystem.config.js + echo "PM2 process started" + fi + pm2 save + + - name: Health check + run: | + sleep 3 + for i in $(seq 1 15); do + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3001 || echo "000") + if [ "$HTTP_CODE" = "200" ]; then + echo "Health check passed (HTTP $HTTP_CODE)" + exit 0 + fi + echo "Waiting for server... (attempt $i, HTTP $HTTP_CODE)" + sleep 2 + done + echo "Health check failed" + pm2 logs eternalai --lines 30 --nostream + exit 1 + + - name: Reload Nginx + run: sudo nginx -t && sudo nginx -s reload || echo "Nginx reload skipped" + + - name: Deploy summary + run: | + echo "============================================" + echo " Deploy completed: $(git rev-parse --short HEAD)" + echo " $(git log -1 --format='%s')" + echo "============================================" diff --git a/.gitignore b/.gitignore index 0db2bd1..327ba8f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules/ .env test-results/ playwright-report/ +logs/ diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100644 index 0000000..42a8b05 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ===== EternalAI 部署脚本 ===== +# 每次部署时运行,拉取最新代码、安装依赖、迁移数据库、重启服务 +# 用法: bash deploy/deploy.sh + +# ---- 颜色输出 ---- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# ---- 配置 ---- +APP_DIR="$(cd "$(dirname "$0")/.." && pwd)" +HEALTH_URL="http://localhost:3001" +HEALTH_TIMEOUT=30 + +cd "$APP_DIR" +info "应用目录: $APP_DIR" + +# ---- 前置检查 ---- +if [[ ! -f .env ]]; then + error ".env 文件不存在,请先运行 deploy/setup-server.sh" + exit 1 +fi + +if ! command -v pm2 &>/dev/null; then + error "PM2 未安装,请先运行 deploy/setup-server.sh" + exit 1 +fi + +# ---- 拉取最新代码 ---- +info "拉取最新代码..." +git fetch --all +git reset --hard origin/master +info "当前版本: $(git rev-parse --short HEAD)" + +# ---- 安装依赖 ---- +info "安装 npm 依赖..." +npm install --production=false + +# ---- 生成 Prisma Client ---- +info "生成 Prisma Client..." +npx prisma generate + +# ---- 数据库迁移 ---- +info "推送数据库 Schema..." +npx prisma db push + +# ---- 创建日志目录 ---- +mkdir -p logs + +# ---- 重启 PM2 进程 ---- +info "重启 PM2 进程..." +if pm2 describe eternalai &>/dev/null; then + pm2 reload ecosystem.config.js --update-env + info "PM2 进程已 reload" +else + pm2 start ecosystem.config.js + info "PM2 进程已启动" +fi +pm2 save + +# ---- 健康检查 ---- +info "健康检查 (${HEALTH_TIMEOUT}s 超时)..." +ELAPSED=0 +while [[ $ELAPSED -lt $HEALTH_TIMEOUT ]]; do + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL" 2>/dev/null || echo "000") + if [[ "$HTTP_CODE" == "200" ]]; then + info "健康检查通过 (HTTP $HTTP_CODE)" + break + fi + sleep 2 + ELAPSED=$((ELAPSED + 2)) + echo -n "." +done +echo "" + +if [[ "$HTTP_CODE" != "200" ]]; then + error "健康检查失败 (HTTP $HTTP_CODE),请检查日志: pm2 logs eternalai" + exit 1 +fi + +# ---- 完成 ---- +echo "" +info "============================================" +info " 部署完成!" +info "============================================" +echo "" +echo "版本: $(git rev-parse --short HEAD)" +echo "分支: $(git branch --show-current)" +echo "状态: pm2 status" +echo "日志: pm2 logs eternalai" +echo "" diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..599570e --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,87 @@ +# ===== EternalAI Nginx 反向代理配置 ===== +# +# 使用方法: +# 1. 复制到 Nginx 配置目录: +# sudo cp deploy/nginx.conf /etc/nginx/sites-available/eternalai +# sudo ln -s /etc/nginx/sites-available/eternalai /etc/nginx/sites-enabled/ +# 2. 替换 YOUR_DOMAIN 为实际域名 +# 3. 测试配置: sudo nginx -t +# 4. 重载: sudo nginx -s reload +# +# HTTPS 配置(推荐): +# sudo certbot --nginx -d YOUR_DOMAIN + +# 替换 YOUR_DOMAIN 为你的实际域名 +upstream eternalai_backend { + server 127.0.0.1:3001; + keepalive 32; +} + +server { + listen 80; + server_name YOUR_DOMAIN; + + # 安全头 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # 请求体大小限制 + client_max_body_size 10m; + + # API 请求代理到 Node.js + location /api/ { + proxy_pass http://eternalai_backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + } + + # 静态文件缓存 + location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + proxy_pass http://eternalai_backend; + proxy_cache_bypass $http_upgrade; + expires 1d; + add_header Cache-Control "public, immutable"; + } + + # 主页和其他路由 + location / { + proxy_pass http://eternalai_backend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # 禁止访问敏感文件 + location ~ /\. { + deny all; + } + location ~ /\.(env|git) { + deny all; + } + location ~ /node_modules/ { + deny all; + } + location ~ /prisma/ { + deny all; + } + location ~ /e2e/ { + deny all; + } + location ~ /deploy/ { + deny all; + } + + access_log /var/log/nginx/eternalai_access.log; + error_log /var/log/nginx/eternalai_error.log; +} diff --git a/deploy/setup-server.sh b/deploy/setup-server.sh new file mode 100644 index 0000000..3dd993f --- /dev/null +++ b/deploy/setup-server.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ===== EternalAI 服务器初始化脚本 ===== +# 首次部署时在服务器上运行,自动检测并安装所有依赖 +# 用法: bash deploy/setup-server.sh +# +# 可通过环境变量自定义: +# DB_NAME=eternalai 数据库名 +# DB_USER=eternalai 数据库用户 +# DB_PASS=xxx 数据库密码(必填,否则自动生成) + +# ---- 颜色输出 ---- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# ---- 配置 ---- +DB_NAME="${DB_NAME:-eternalai}" +DB_USER="${DB_USER:-eternalai}" +DB_PASS="${DB_PASS:-$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)}" +APP_DIR="$(cd "$(dirname "$0")/.." && pwd)" +NODE_VERSION="20" + +info "应用目录: $APP_DIR" +info "数据库: $DB_NAME (用户: $DB_USER)" + +# ---- 检测 OS ---- +if [[ -f /etc/os-release ]]; then + . /etc/os-release + OS_ID="$ID" + info "检测到操作系统: $PRETTY_NAME" +else + error "无法检测操作系统,仅支持 Ubuntu/Debian/CentOS" + exit 1 +fi + +# ---- 安装 Node.js ---- +if ! command -v node &>/dev/null; then + info "安装 Node.js $NODE_VERSION LTS..." + if [[ "$OS_ID" == "ubuntu" || "$OS_ID" == "debian" ]]; then + curl -fsSL "https://deb.nodesource.com/setup_${NODE_VERSION}.x" | sudo -E bash - + sudo apt-get install -y nodejs + elif [[ "$OS_ID" == "centos" || "$OS_ID" == "rhel" || "$OS_ID" == "rocky" ]]; then + curl -fsSL "https://rpm.nodesource.com/setup_${NODE_VERSION}.x" | sudo -E bash - + sudo yum install -y nodejs + else + error "不支持的操作系统: $OS_ID,请手动安装 Node.js $NODE_VERSION" + exit 1 + fi + info "Node.js 安装完成: $(node -v)" +else + info "Node.js 已安装: $(node -v)" + NODE_MAJOR=$(node -v | sed 's/v\([0-9]*\).*/\1/') + if [[ "$NODE_MAJOR" -lt 18 ]]; then + warn "Node.js 版本过低 (当前 $(node -v)),建议升级到 $NODE_VERSION LTS" + fi +fi + +# ---- 安装 PM2 ---- +if ! command -v pm2 &>/dev/null; then + info "安装 PM2..." + sudo npm install -g pm2 + info "PM2 安装完成: $(pm2 -v)" +else + info "PM2 已安装: $(pm2 -v)" +fi + +# ---- 安装 PostgreSQL ---- +if ! command -v psql &>/dev/null; then + info "安装 PostgreSQL 15..." + if [[ "$OS_ID" == "ubuntu" || "$OS_ID" == "debian" ]]; then + sudo apt-get install -y ca-certificates curl gnupg lsb-release + sudo install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg + echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list + sudo apt-get update + sudo apt-get install -y postgresql-15 + elif [[ "$OS_ID" == "centos" || "$OS_ID" == "rhel" || "$OS_ID" == "rocky" ]]; then + sudo yum install -y "https://download.postgresql.org/pub/repos/yum/reporpms/EL-$(rpm -E %{rhel})-x86_64/pgdg-redhat-repo-latest.noarch.rpm" + sudo yum install -y postgresql15-server postgresql15 + sudo /usr/pgsql-15/bin/postgresql-15-setup initdb + else + error "不支持的操作系统: $OS_ID,请手动安装 PostgreSQL 15" + exit 1 + fi + info "PostgreSQL 安装完成" +else + info "PostgreSQL 已安装: $(psql --version)" +fi + +# ---- 启动 PostgreSQL ---- +if ! sudo systemctl is-active --quiet postgresql; then + info "启动 PostgreSQL 服务..." + sudo systemctl enable postgresql + sudo systemctl start postgresql +fi + +# ---- 创建数据库和用户 ---- +info "创建数据库 $DB_NAME 和用户 $DB_USER..." +sudo -u postgres psql -tc "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'" | grep -q 1 || { + sudo -u postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';" + info "数据库用户 $DB_USER 已创建" +} +sudo -u postgres psql -tc "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'" | grep -q 1 || { + sudo -u postgres psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;" + info "数据库 $DB_NAME 已创建" +} +sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;" >/dev/null + +# 确保 PostgreSQL 对 pg_hba.conf 允许密码认证 +PG_HBA=$(sudo -u postgres psql -t -c "SHOW hba_file" | xargs) +if [[ -f "$PG_HBA" ]]; then + if ! sudo grep -q "$DB_USER" "$PG_HBA" 2>/dev/null; then + warn "如果连接数据库失败,请检查 $PG_HBA 是否允许 md5/scram-sha-256 认证" + warn "添加: host $DB_NAME $DB_USER 127.0.0.1/32 scram-sha-256" + fi +fi + +# ---- 安装 Nginx ---- +if ! command -v nginx &>/dev/null; then + info "安装 Nginx..." + if [[ "$OS_ID" == "ubuntu" || "$OS_ID" == "debian" ]]; then + sudo apt-get install -y nginx + elif [[ "$OS_ID" == "centos" || "$OS_ID" == "rhel" || "$OS_ID" == "rocky" ]]; then + sudo yum install -y nginx + fi + info "Nginx 安装完成: $(nginx -v 2>&1)" +else + info "Nginx 已安装: $(nginx -v 2>&1)" +fi + +if ! sudo systemctl is-active --quiet nginx; then + sudo systemctl enable nginx + sudo systemctl start nginx +fi + +# ---- 创建 .env 文件 ---- +ENV_FILE="$APP_DIR/.env" +if [[ ! -f "$ENV_FILE" ]]; then + info "创建 .env 文件..." + JWT_SECRET=$(openssl rand -base64 48) + cat > "$ENV_FILE" </dev/null | grep "sudo" | bash || true + +# ---- 完成 ---- +echo "" +info "============================================" +info " 服务器初始化完成!" +info "============================================" +echo "" +echo "下一步:" +echo " 1. 配置 Nginx 反向代理(参考 deploy/nginx.conf)" +echo " 2. 检查应用状态: pm2 status" +echo " 3. 查看日志: pm2 logs eternalai" +echo " 4. 访问应用: http://localhost:3001" +echo "" diff --git a/docs/business-processes.md b/docs/business-processes.md new file mode 100644 index 0000000..d7d9555 --- /dev/null +++ b/docs/business-processes.md @@ -0,0 +1,1077 @@ +# 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 ` 头部。 +- 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 `)。 +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` 写入 `
` 预览区。
+9. 用户可在结果面板:
+   - 切换预览 Tab(Soul.md / config.yaml)。
+   - 点击「下载」按钮下载对应文件(`download()` 通过 Blob + `` 实现)。
+
+**后置条件**:
+- 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` 提示,无实际下单流程。
diff --git a/docs/deployment.md b/docs/deployment.md
new file mode 100644
index 0000000..da63aff
--- /dev/null
+++ b/docs/deployment.md
@@ -0,0 +1,332 @@
+# EternalAI 部署指南
+
+## 架构概览
+
+```
+用户浏览器 → Nginx (80/443) → Node.js Express (3001) → PostgreSQL (5432)
+                                    ↓
+                              PM2 进程管理
+```
+
+## 文件清单
+
+| 文件 | 用途 |
+|------|------|
+| `ecosystem.config.js` | PM2 进程配置 |
+| `.env.example` | 环境变量模板 |
+| `deploy/setup-server.sh` | 首次服务器初始化脚本 |
+| `deploy/deploy.sh` | 每次部署脚本 |
+| `deploy/nginx.conf` | Nginx 反向代理配置模板 |
+| `.gitea/workflows/deploy.yml` | Gitea Actions CI/CD 工作流 |
+
+---
+
+## 一、首次部署(全量部署)
+
+### 1.1 准备服务器
+
+在目标服务器上执行以下操作。
+
+```bash
+# 克隆代码仓库
+cd /opt  # 或你选择的部署目录
+git clone http://gitea.fischerai.cn/chigulong/eternalai.git
+cd eternalai
+```
+
+### 1.2 运行服务器初始化脚本
+
+```bash
+bash deploy/setup-server.sh
+```
+
+此脚本会自动完成:
+- 检测并安装 Node.js 20 LTS(如缺失)
+- 检测并安装 PostgreSQL 15(如缺失)
+- 检测并安装 PM2(如缺失)
+- 检测并安装 Nginx(如缺失)
+- 创建数据库 `eternalai` 和用户
+- 自动生成 `.env` 文件(含随机 JWT 密钥和数据库密码)
+- 安装 npm 依赖
+- 生成 Prisma Client
+- 推送数据库 Schema
+- 启动 PM2 进程并配置开机自启
+
+**自定义数据库配置**(可选):
+```bash
+DB_NAME=mydb DB_USER=myuser DB_PASS=mypassword bash deploy/setup-server.sh
+```
+
+脚本完成后会输出数据库密码,请妥善保存。
+
+### 1.3 配置 Nginx 反向代理
+
+```bash
+# 复制 Nginx 配置
+sudo cp deploy/nginx.conf /etc/nginx/sites-available/eternalai
+sudo ln -s /etc/nginx/sites-available/eternalai /etc/nginx/sites-enabled/
+
+# 编辑配置,替换 YOUR_DOMAIN 为实际域名
+sudo nano /etc/nginx/sites-enabled/eternalai
+# 将所有 YOUR_DOMAIN 替换为你的域名,如 eternalai.example.com
+
+# 测试配置
+sudo nginx -t
+
+# 重载 Nginx
+sudo nginx -s reload
+```
+
+### 1.4 配置 HTTPS(推荐)
+
+```bash
+# 安装 Certbot
+sudo apt-get install -y certbot python3-certbot-nginx  # Ubuntu/Debian
+# 或
+sudo yum install -y certbot python3-certbot-nginx      # CentOS/RHEL
+
+# 自动获取并配置 SSL 证书
+sudo certbot --nginx -d YOUR_DOMAIN
+
+# 证书自动续期(Certbot 会自动配置 cron)
+sudo certbot renew --dry-run
+```
+
+### 1.5 验证部署
+
+```bash
+# 检查 PM2 进程状态
+pm2 status
+
+# 查看应用日志
+pm2 logs eternalai
+
+# 测试本地访问
+curl http://localhost:3001
+
+# 测试 Nginx 代理访问
+curl http://YOUR_DOMAIN
+```
+
+---
+
+## 二、CI/CD 自动部署(推送即部署)
+
+### 2.1 配置 Gitea Secrets
+
+在 Gitea 仓库设置中添加 Secret:
+
+1. 进入仓库 → Settings → Actions → Secrets
+2. 添加 `DATABASE_URL`,值为 `.env` 文件中的 `DATABASE_URL`
+
+### 2.2 配置 Gitea Runner
+
+确保已注册 self-hosted runner:
+
+```bash
+# 在服务器上注册 runner(如尚未注册)
+# 参考 Gitea 官方文档: https://docs.gitea.com/usage/actions/quickstart
+```
+
+### 2.3 自动部署流程
+
+每次推送代码到 `master` 分支时,Gitea Actions 会自动:
+
+1. 拉取最新代码
+2. 安装 npm 依赖
+3. 生成 Prisma Client
+4. 推送数据库 Schema(`prisma db push`)
+5. 重启 PM2 进程(`pm2 reload`)
+6. 健康检查(等待 HTTP 200)
+7. 重载 Nginx
+
+```bash
+# 推送代码触发自动部署
+git push origin master
+```
+
+### 2.4 手动触发部署
+
+在 Gitea 仓库 → Actions 页面,可手动触发 `Deploy EternalAI` 工作流。
+
+---
+
+## 三、手动部署(不使用 CI/CD)
+
+```bash
+# SSH 登录服务器
+ssh user@your-server
+
+# 进入应用目录
+cd /opt/eternalai
+
+# 运行部署脚本
+bash deploy/deploy.sh
+```
+
+部署脚本会自动完成:拉取代码 → 安装依赖 → 数据库迁移 → 重启 PM2 → 健康检查。
+
+---
+
+## 四、常用运维命令
+
+### PM2 进程管理
+
+```bash
+pm2 status                    # 查看进程状态
+pm2 logs eternalai            # 实时查看日志
+pm2 logs eternalai --lines 100  # 查看最近 100 行日志
+pm2 restart eternalai         # 重启进程
+pm2 reload eternalai          # 零停机重载
+pm2 stop eternalai            # 停止进程
+pm2 delete eternalai          # 删除进程
+pm2 monit                     # 监控面板
+```
+
+### 数据库操作
+
+```bash
+# 推送 Schema 变更
+npx prisma db push
+
+# 打开 Prisma Studio(数据库可视化管理)
+npx prisma studio
+
+# 连接 PostgreSQL
+psql -U eternalai -d eternalai -h localhost
+```
+
+### Nginx 操作
+
+```bash
+sudo nginx -t                 # 测试配置
+sudo nginx -s reload          # 重载配置
+sudo systemctl status nginx   # 查看状态
+sudo tail -f /var/log/nginx/eternalai_error.log  # 查看错误日志
+```
+
+### 日志查看
+
+```bash
+# 应用日志
+tail -f logs/out.log          # 标准输出
+tail -f logs/err.log          # 错误输出
+
+# Nginx 日志
+sudo tail -f /var/log/nginx/eternalai_access.log
+sudo tail -f /var/log/nginx/eternalai_error.log
+
+# PostgreSQL 日志
+sudo tail -f /var/log/postgresql/postgresql-15-main.log
+```
+
+---
+
+## 五、更新部署
+
+### 5.1 日常更新(代码变更)
+
+```bash
+# 方法一:推送代码触发 CI/CD(推荐)
+git push origin master
+
+# 方法二:手动部署
+bash deploy/deploy.sh
+```
+
+### 5.2 数据库 Schema 变更
+
+修改 `prisma/schema.prisma` 后:
+
+```bash
+# 推送到 master,CI/CD 会自动执行 prisma db push
+git add prisma/schema.prisma
+git commit -m "feat: update schema"
+git push origin master
+
+# 或手动执行
+npx prisma db push
+pm2 reload eternalai
+```
+
+### 5.3 环境变量变更
+
+```bash
+# 编辑 .env 文件
+nano .env
+
+# 重启应用使配置生效
+pm2 reload eternalai --update-env
+```
+
+---
+
+## 六、回滚
+
+```bash
+# 查看部署历史
+git log --oneline -10
+
+# 回滚到指定版本
+git checkout 
+npm install
+npx prisma db push
+pm2 reload eternalai
+
+# 或回滚到上一个版本
+git checkout HEAD~1
+npm install
+npx prisma db push
+pm2 reload eternalai
+```
+
+---
+
+## 七、故障排查
+
+### 应用无法启动
+
+```bash
+# 查看错误日志
+pm2 logs eternalai --err --lines 50
+
+# 常见原因:
+# 1. .env 文件缺失 → 运行 deploy/setup-server.sh
+# 2. 数据库连接失败 → 检查 DATABASE_URL 和 PostgreSQL 服务
+# 3. 端口被占用 → 检查 PORT 环境变量
+```
+
+### 数据库连接失败
+
+```bash
+# 检查 PostgreSQL 服务
+sudo systemctl status postgresql
+
+# 测试连接
+psql -U eternalai -d eternalai -h localhost -W
+
+# 检查 pg_hba.conf 认证配置
+sudo cat /etc/postgresql/15/main/pg_hba.conf | grep -v '^#'
+```
+
+### Nginx 502 Bad Gateway
+
+```bash
+# 检查 Node.js 进程是否运行
+pm2 status
+
+# 检查端口
+curl http://localhost:3001
+
+# 检查 Nginx 错误日志
+sudo tail -f /var/log/nginx/eternalai_error.log
+```
+
+### PM2 进程未开机自启
+
+```bash
+# 重新配置开机自启
+pm2 startup systemd
+# 执行输出的 sudo 命令
+pm2 save
+```
diff --git a/ecosystem.config.js b/ecosystem.config.js
new file mode 100644
index 0000000..3772c36
--- /dev/null
+++ b/ecosystem.config.js
@@ -0,0 +1,23 @@
+module.exports = {
+  apps: [
+    {
+      name: 'eternalai',
+      script: 'server.js',
+      cwd: __dirname,
+      instances: 1,
+      exec_mode: 'fork',
+      autorestart: true,
+      max_restarts: 10,
+      restart_delay: 3000,
+      max_memory_restart: '512M',
+      env: {
+        NODE_ENV: 'production',
+      },
+      error_file: './logs/err.log',
+      out_file: './logs/out.log',
+      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
+      merge_logs: true,
+      time: true,
+    },
+  ],
+};
diff --git a/package.json b/package.json
index e6c0a95..feeef8a 100644
--- a/package.json
+++ b/package.json
@@ -7,8 +7,11 @@
     "start": "node server.js",
     "dev": "node --watch server.js",
     "db:push": "prisma db push",
+    "db:generate": "prisma generate",
     "db:studio": "prisma studio",
-    "test": "jest"
+    "test": "playwright test",
+    "test:e2e": "playwright test",
+    "deploy": "bash deploy/deploy.sh"
   },
   "keywords": [
     "ai",