AI Chat  /  项目概述
在线演示
全栈 AI 对话平台 · 高价值AI实战学习项目

从零到上线
AI Chat 完整学习手册

面向初学者的全栈 AI 项目实战教程。涵盖 React + NestJS + SQLite 技术栈,手把手带你完成环境搭建、功能理解、配置调试到生产部署的完整流程。

AI 实战学习小册 在线演示
2
独立应用
6
后端模块
3
AI 对话模式
全栈
TypeScript
React 19 🔴 NestJS 11 🔷 TypeScript 🗄 SQLite 🌊 Tailwind CSS Vite 8
快速开始

5 分钟跑起来

如果你已经有了 Node.js 环境,按照下面步骤可以在 5 分钟内启动项目。

💡

前置条件

需要安装 Node.js ≥ 18npm ≥ 9,建议使用 Node.js 20 LTS 版本。

  1. 克隆项目
    将项目代码下载到本地。
    terminal
    # 如果你有 ZIP 包,解压后进入目录
    # 或者从 git 克隆
    git clone <your-repo-url> ai-chat
    cd ai-chat
  2. 安装全部依赖
    根目录一条命令安装前后端全部依赖(约需 1~3 分钟)。
    terminal
    # 在项目根目录执行
    npm install
    cd client && npm install
    cd ../server && npm install
    cd ..
  3. 配置环境变量
    复制示例配置并修改关键参数(详见配置章节)。
    terminal
    cp server/.env.example server/.env
    # 然后用编辑器打开 server/.env 填写配置
  4. 启动开发服务器
    在根目录运行,前后端同时启动。
    terminal
    npm run dev
    
    # 输出示例:
    [server] NestJS application is running on: http://localhost:3000
    [client] Local: http://localhost:5173
  5. 访问应用
    打开浏览器访问前端地址,即可看到完整的 AI Chat 界面。
    前端 · localhost:5173 后端 API · localhost:3000
环境准备

开发环境搭建

在开始之前,你需要在电脑上安装以下工具。如果已经安装可以跳过对应步骤。

Node.js ≥ 18
JavaScript 运行时,前后端都依赖它。推荐安装 20 LTS 版本。
官网下载 →
Git
版本控制工具,用于克隆代码和管理提交。macOS 通常已预装。
官网下载 →
VS Code(推荐)
主流代码编辑器,安装 TypeScript、ESLint 插件效果最佳。
官网下载 →

验证安装

打开终端(macOS: 按 Cmd+空格,搜索"终端"),运行以下命令:

terminal — 验证工具版本
node --version   # 期望输出: v18.x.x 或 v20.x.x
npm --version    # 期望输出: 9.x.x 或更高
git --version    # 期望输出: git version 2.x.x

macOS 推荐方式

使用 nvm(Node Version Manager)管理 Node 版本更方便:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
安装后运行 nvm install 20 && nvm use 20

项目配置

环境变量配置详解

后端服务使用 .env 文件管理配置。首先复制示例文件,再逐项填写。

server/.env
# ============================
# JWT 认证密钥(必填)
# ============================
JWT_SECRET=your_super_secret_key_change_this_in_production

# ============================
# SSO 单点登录(可选)
# ============================
AIBOOK_APP_ID=your_app_id
AIBOOK_APP_SECRET=your_app_secret

# ============================
# 支付配置(可选,不用支付可留空)
# ============================
JITPAY_APP_KEY=your_jitpay_app_key
JITPAY_APP_SECRET=your_jitpay_app_secret

# ============================
# 应用基础配置
# ============================
PORT=3000
CLIENT_URL=http://localhost:5173
UPLOAD_DIR=./uploads
DATABASE_PATH=./data/aichat.db

# ============================
# 管理员密钥
# ============================
ADMIN_SECRET=admin_secret_key

配置项说明

配置项 必填 说明 示例值
JWT_SECRET 必填 JWT 签名密钥,用于用户 Token 的加密和验证,生产环境请使用随机长字符串 a8f3k9x2...
AIBOOK_APP_ID 可选 第三方 SSO 单点登录的 App ID,不启用 SSO 可留空 app_123
AIBOOK_APP_SECRET 可选 SSO 的 App Secret,配合 APP_ID 使用 secret_abc
JITPAY_APP_KEY 可选 JitPay 支付平台 App Key,不启用支付可留空 pay_key
JITPAY_APP_SECRET 可选 JitPay 支付平台 App Secret pay_secret
PORT 必填 后端服务监听端口,默认 3000 3000
CLIENT_URL 必填 前端地址,用于配置 CORS 跨域白名单 http://localhost:5173
UPLOAD_DIR 必填 文件上传存储目录,相对于 server 目录 ./uploads
DATABASE_PATH 必填 SQLite 数据库文件路径 ./data/aichat.db
ADMIN_SECRET 必填 管理员操作密钥,生产环境务必修改 my_admin_key
⚠️

安全提醒

切勿将 .env 文件提交到 Git 仓库! 项目已在 .gitignore 中排除了 server/.env。生产环境的 JWT_SECRET 和 ADMIN_SECRET 请使用随机生成的强密钥。

前端环境变量(可选)

前端可通过 client/.env.local 设置 API 地址:

client/.env.local
# 开发时指定后端地址(默认已回退到 localhost:3000)
VITE_API_URL=http://localhost:3000

# 生产构建时留空或指向同域(Nginx 会代理)
# VITE_API_URL=
本地开发

本地开发运行

项目采用 Monorepo 结构,根目录的 npm scripts 可以同时启动前后端,也可以分开启动调试。

同时启动(推荐)
前后端并行运行
根目录
npm run dev
分开启动(调试)
两个终端窗口
两个终端
# 终端1 - 后端
npm run dev:server

# 终端2 - 前端
npm run dev:client

npm scripts 说明

命令执行位置说明
npm run dev根目录同时启动前后端开发服务器(热重载)
npm run dev:server根目录仅启动后端,等同于进入 server 目录运行 nest start --watch
npm run dev:client根目录仅启动前端 Vite 开发服务器
npm run build根目录构建前后端生产版本
npm run build:server根目录仅构建后端(输出到 server/dist)
npm run build:client根目录仅构建前端(输出到 client/dist)
ℹ️

热重载说明

前端(Vite):修改任何 .tsx/.ts/.css 文件,浏览器会立即自动刷新。
后端(NestJS):修改任何 .ts 文件,后端服务会自动重新编译并重启(--watch 模式)。

目录结构

项目目录详解

了解每个目录和文件的作用,是读懂项目的第一步。整个项目是一个 Monorepo,前后端分别在 client/server/ 目录中。

根目录结构

ai-chat/ ├── client/ # 前端 React 应用 ├── server/ # 后端 NestJS 应用 ├── deploy/ # 部署脚本 ├── tech/ # 技术文档 ├── package.json # 根包(并行启动脚本) └── .gitignore # Git 忽略配置

后端目录结构

server/src/ ├── database/ # 数据库初始化 ├── modules/ # 功能模块 │ ├── auth/ # 认证模块(JWT+Passport) │ ├── user/ # 用户模块 │ ├── chat/ # 聊天模块(SSE 流式) │ ├── file/ # 文件上传模块 │ ├── payment/ # 支付模块 │ └── ai/ # AI 大模型接入层 ├── app.module.ts # 根模块 └── main.ts # 入口文件

前端目录结构

client/src/ ├── assets/ # 静态资源(图片、SVG) ├── components/ # 复用组件(chat/layout/payment/ui) ├── pages/ # 页面组件(auth/chat/settings) ├── services/ # API 请求服务层(axios 封装) ├── stores/ # Zustand 状态管理(auth/chat/ui) ├── constants.ts # 全局常量(套餐、AI模式等) ├── App.tsx # 路由配置入口 └── main.tsx # React 挂载入口
核心模块

后端核心模块解析

后端采用 NestJS 模块化架构,每个功能域对应一个独立模块,各模块职责清晰、相互解耦。

Auth 模块
认证 & 授权

负责用户注册、登录、JWT Token 签发与验证。基于 Passport.js + JWT 策略实现无状态认证,支持第三方 SSO 单点登录。

JWT Passport SSO
User 模块
用户管理

管理用户信息、Token 消耗记录、套餐状态。支持管理员后台操作。

用户信息 Token 统计 套餐管理
Chat 模块
对话管理 & AI 调用

核心业务模块。管理对话会话、消息记录,通过 SSE 流式返回 AI 响应。支持三种对话模式。

SSE 流式 多模型 上下文
File 模块
文件上传 & 管理

基于 Multer 处理文件上传,支持 TXT、Markdown、Word (docx) 等格式。文件内容可作为上下文注入到 AI 对话。

Multer docx 解析 静态服务
Payment 模块
支付 & 套餐

集成 JitPay 支付平台,处理套餐购买、支付回调,为用户充値 Token 额度。提供体验券和源码套餐。

JitPay Token 充値 订单管理
AI 模块
大模型接入层

封装不同 AI 模型的调用逻辑,通过统一接口支持豆包、Kimi、DeepSeek 等多种模型,模式切换对上层透明。

豆包 Kimi DeepSeek
前端架构

前端技术架构详解

前端基于 React 19 + Vite 8 + TypeScript 构建,使用 Zustand 轻量状态管理,TailwindCSS 实现样式。

技术栈组成

React 19
UI 渲染框架,支持并发特性
Vite 8
极速构建工具,毫秒级 HMR
🌊
Tailwind CSS 3
原子化 CSS,快速构建 UI
🐻
Zustand 5
轻量状态管理,替代 Redux
🔗
React Router 7
客户端路由管理

AI 对话模式

⚡ 极速模式 (fast)

快速响应,适合日常问答。使用响应速度最快的模型配置。

🧠 思考模式 (think)

深度推理,展示思维过程。适合复杂逻辑和分析任务。

🎓 专家模式 (expert)

专业回答,包含技术细节。适合需要精准专业知识的场景。

📌

AI 模式配置位置

client/src/constants.tsCHAT_MODES 数组中配置,可自行添加或修改模式。

后端架构

后端技术架构详解

后端采用 NestJS 11 + SQLite + TypeScript,数据库使用轻量的 SQLite(better-sqlite3),无需额外安装数据库服务

系统分层架构

接入层
React 前端 :5173
HTTP / SSE
JWT Bearer Token
应用层 (Controller)
AuthController
UserController
ChatController
FileController
PaymentController
服务层 (Service)
AuthService
UserService
ChatService
FileService
PaymentService
AiService
数据层 (SQLite)
users 表
conversations 表
messages 表
payments 表
files 表
💡

为什么选择 SQLite?

SQLite 是一个文件型数据库,无需安装任何服务,数据直接存储在 server/data/aichat.db 文件中。非常适合学习项目和中小型应用,生产环境如需更高并发可迁移到 PostgreSQL/MySQL。

生产部署

生产环境部署

将项目部署到云服务器(Linux),推荐使用腾讯云、阿里云或任意 Linux VPS。

1
准备服务器
2
安装依赖
3
上传代码
4
构建项目
5
配置 Nginx
6
上线运行
  1. 服务器要求
    Linux(Ubuntu 20.04/22.04 推荐),最低配置 1 核 2G 内存。
    1数2G
    最低配置
    Ubuntu 22.04
    推荐系统
    80 / 443
    开放端口
  2. 服务器安装 Node.js + PM2
    SSH 登录服务器后执行。
    ssh terminal
    # 安装 nvm
    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
    source ~/.bashrc
    
    # 安装 Node 20 LTS
    nvm install 20 && nvm use 20 && nvm alias default 20
    
    # 安装 Nginx + PM2
    apt update && apt install -y nginx
    npm install -g pm2
  3. 上传代码并构建
    本地用 rsync 上传,服务器上安装依赖并构建。
    本地终端 → 服务器
    # 本地执行:上传项目
    rsync -avz --exclude='node_modules' --exclude='.git' \
      ./  root@your-ip:/var/www/ai-chat/
    
    # SSH 到服务器上:
    cd /var/www/ai-chat
    npm install
    cd server && npm install && npm run build
    cd ../client && npm install && npm run build
    cd ../server
    cp .env.example .env && nano .env  # 填写生产配置
  4. 使用 PM2 启动后端
    PM2 确保后端和系统一起启动,崩溃后自动重启。
    ssh terminal
    pm2 start dist/main.js --name ai-chat-server
    pm2 save && pm2 startup   # 开机自启
    pm2 status                # 查看运行状态
Nginx 配置

Nginx 反向代理配置

Nginx 同时承担前端静态文件服务和后端 API 反向代理,并可配置 HTTPS。

/etc/nginx/sites-available/ai-chat
# HTTP 转 HTTPS 重定向
server {
    listen 80;
    server_name your-domain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name your-domain.com;
    ssl_certificate     /etc/ssl/certs/your-domain.pem;
    ssl_certificate_key /etc/ssl/private/your-domain.key;

    # 前端静态文件
    location / {
        root /var/www/ai-chat/client/dist;
        index index.html;
        try_files $uri $uri/ /index.html; # SPA 路由支持
    }

    # 后端 API 代理
    location /api/ {
        proxy_pass http://localhost:3000/;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # SSE 流式接口特殊配置(AI 对话必须)
    location /api/chat/stream {
        proxy_pass http://localhost:3000/chat/stream;
        proxy_buffering off;          # 禁用缓冲,实时传输
        proxy_cache off;
        proxy_http_version 1.1;
        proxy_read_timeout 300s;
        chunked_transfer_encoding on;
    }

    client_max_body_size 50m; # 文件上传大小限制
}
启用配置
ln -s /etc/nginx/sites-available/ai-chat /etc/nginx/sites-enabled/
nginx -t          # 测试配置是否正确
systemctl reload nginx

免费 HTTPS 证书

使用 Let's Encrypt + Certbot 免费获取并自动续期 SSL 证书:
apt install certbot python3-certbot-nginx && certbot --nginx -d your-domain.com

API 接口

API 接口速览

后端提供 RESTful API(部分使用 SSE 流式)。开发时可通过 Postman 或 curl 调试。

AUTH 认证接口

方法路径说明认证
POST/auth/register注册新用户
POST/auth/login登录,返回 JWT Token
POST/auth/sso/callback第三方 SSO 回调

CHAT 对话接口

方法路径说明认证
GET/chat/conversations获取对话列表Bearer
POST/chat/conversations创建新对话Bearer
GET/chat/conversations/:id/messages获取消息记录Bearer
POST/chat/streamSSE 流式发送消息并获取 AI 回复Bearer
DELETE/chat/conversations/:id删除对话Bearer

FILE 文件接口

方法路径说明认证
POST/file/upload上传文件(multipart/form-data)Bearer
GET/file/list获取文件列表Bearer
DELETE/file/:id删除文件Bearer
🔐

如何携带 Token 调用接口

登录成功后返回 access_token,后续请求在 Header 中携带:
Authorization: Bearer eyJhbGci...

AI 能力系统

AI 能力系统 — 架构设计与核心实现

AI Chat 的 AI 能力模块采用「提供商适配器模式」,支持多家大模型厂商无缝接入,配合 SSE 流式输出,实现连续对话、深度思考、网页设计生成、AI 画图等多种功能。

多提供商适配
支持 DeepSeek、MiniMax、千问、豆包、Moonshot 等,统一适配器接口
📡
SSE 流式输出
Server-Sent Events 实时流式传输,配合 reasoning 思考过程同步展示
🎨
AI 画图生成
对接豆包 SeedDream 文生图,自动 base64 转码内联预览
📝
三模式切换
极速 / 深思 / 专家三档位,动态调整温度和 max_tokens
🌐
网页设计生成
内置设计风格知识库,注入强力系统提示词自动生成 HTML
💼
职业顾问模式
内置小夕资深职业顾问系统提示词,支持简历诊断 / 模拟面试

1AI 模块整体架构

架构设计思路:Controller 接收请求 → JWT 验证 → ChatService 管理消息 → content_type 分流 → AiService 选择适配器 → Axios 流式请求 → SSE 实时输出。全程异步,不阻塞主线程。
AI 模块层次架构图
HTTP POST
/api/chat/completions
ChatController
权限验证
JWT Guard
userId 解析 + Token 余额检查
消息管理
ChatService
历史消息读写 / 标题生成
提示词注入
content_type 分流
webdesign / career 特殊模式
核心服务
AiService.streamChat()
选择适配器 → 构建请求体 → Axios 流式请求 → 解析 SSE 块
超时护置
Inactivity Timer
90s 无数据自动断开
提供商适配器层
DeepSeekMiniMaxQwen豆包OpenAI兼容

2提供商适配器模式 — 多厂商支持原理

项目设计了 ProviderAdapter 接口,每个厂商实现两个方法:构建请求体和解析响应块。添加新厂商只需写一个对象,主流程不需修改。

ai.service.ts — 适配器接口与温度映射
interface ProviderAdapter {
    buildRequestBody(opts: BuildRequestOptions): Record<string, any>;
    parseChunk(parsed: any): ParsedChunk;
  }
  
  // 不同模式的温度 & max_tokens
  function modeParams(mode: ChatMode) {
    return {
      temperature: mode === 'fast' ? 0.7 : mode === 'think' ? 0.3 : 0.5,
      max_tokens:  mode === 'fast' ? 2048 : mode === 'think' ? 8192 : 16384,
    };
  }
  
  // DeepSeek 适配器 —— 思考模式自动切换 deepseek-reasoner
  const deepseekAdapter: ProviderAdapter = {
    buildRequestBody({ messages, mode, model, userModelId }) {
      const finalModelId = userModelId || (mode==='think' ? 'deepseek-reasoner' : model.model_id);
      return { model: finalModelId, messages, stream: true, ...modeParams(mode) };
    },
    parseChunk(parsed) {
      const delta = parsed.choices?.[0]?.delta || {};
      return { content: delta.content, reasoning: delta.reasoning_content, tokens: parsed.usage?.total_tokens };
    },
  };
  
  // 通用 OpenAI 兼容适配器(Moonshot / 豆包 / Qwen...)
  function getAdapter(provider: string): ProviderAdapter {
    const map = { deepseek: deepseekAdapter, minimax: minimaxAdapter, qwen: qwenAdapter, doubao: doubaoAdapter };
    return map[provider] ?? defaultAdapter;
  }
💡 添加新厂商的步骤:仿照已有适配器写一个新对象,实现 buildRequestBodyparseChunk; 然后在 getAdapter() 的 map 里加一行 key→适配器映射即可。

3SSE 流式输出完整实现

📱 前端 (React)
① fetch POST /api/chat
② 获得 ReadableStream
③ 实时解析 SSE 事件
④ setReply / setThinking
SSE
⚙️ NestJS 控制器
① JWT 鉴权 + System Prompt
② setHeader + flushHeaders()
③ aiService.streamChat()
④ onDone → 写 DB + 扣 Token
Axios stream
🤖 AI 厂商 API
OpenAI 兼容 SSE 格式
data: {delta} × N 次
data: [DONE]
适配器层统一解析
event: chunk
AI 输出文字片段
{ content: "你好" }
event: reasoning
思考链 (R1 模型)
{ content: "分析..." }
event: done
流结束 + 写 DB
{ fullText, tokensUsed }
event: error
错误描述
{ message: "..." }
chat.controller.ts — ① 服务端设置 SSE 响应头
// ⚠️ flushHeaders() 必须在首次 write() 之前调用,否则响应头会混入 body
res.setHeader('Content-Type',     'text/event-stream');
res.setHeader('Cache-Control',     'no-cache');
res.setHeader('Connection',        'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // 必须!关闭 Nginx 缓冲,否则所有内容攒到最后才一次性返回
res.flushHeaders(); // 立即将响应头推送给客户端,SSE 通道正式建立
chat.controller.ts — ② SSE 事件写入 & 4 种回调完整注册
// SSE 协议规范:event: xxx\ndata: JSON\n\n(末尾两个换行是必须的分隔符)
const writeEvent = (event: string, data: any) =>
  res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);

await aiService.streamChat({
  modelId, messages, mode,
  // 每收到一个文字 delta 就立即推给客户端,用户实时看到字符
  onChunk:     (chunk)     => writeEvent('chunk',     { content: chunk }),
  // DeepSeek R1 / think 模式的推理过程,前端单独展示为折叠面板
  onReasoning: (reasoning) => writeEvent('reasoning', { content: reasoning }),
  // 流结束:写 DB、扣 Token、关闭连接(顺序不能颠倒)
  onDone: (fullText, reasoningText, tokensUsed) => {
    chatService.addMessage(conversation_id, 'assistant', fullText, {
      reasoning_content: reasoningText || undefined,
      tokens_used: tokensUsed,
    });
    userService.deductTokens(userId, tokensUsed);
    writeEvent('done', { fullText, reasoningText, tokensUsed });
    res.end();
  },
  onError: (err) => { writeEvent('error', { message: err.message }); res.end(); },
});
chatService.ts — ③ 前端消费 SSE(fetch + ReadableStream)
// 用 fetch 而非 EventSource:支持 POST + 自定义 Header(Bearer Token)
const res = await fetch('/api/chat/completions', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
  body: JSON.stringify({ conversation_id, model_id, mode, message }),
});

const reader  = res.body!.getReader();
const decoder = new TextDecoder();
let buf = '';

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buf += decoder.decode(value, { stream: true });
  // 按 SSE 协议分隔符 \n\n 切割;最后一段可能不完整,留到下次循环拼接
  const parts = buf.split('\n\n');
  buf = parts.pop() ?? '';
  for (const part of parts) {
    const evt  = part.match(/^event: (\w+)/m)?.[1];
    const data = part.match(/^data: (.+)/m)?.[1];
    if (!evt || !data) continue;
    const { content } = JSON.parse(data);
    if (evt === 'chunk')     setReply(r     => r + content);
    if (evt === 'reasoning') setThinking(r  => r + content);
    if (evt === 'done')      setLoading(false);
    if (evt === 'error')     toast.error(content);
  }
}
💡 为什么用 fetch 而非 EventSource?  原生 EventSource 仅支持 GET 请求,无法携带请求体和 Authorization 头。项目采用 fetch + ReadableStream 方案:完整支持 POST Body 和 JWT 鉴权,并用 buf.split('\n\n') 正确处理跨 chunk 的 SSE 分隔符拼接问题。

4三种对话模式详解

⚡ 极速mode:'fast'
temperature0.7
max_tokens2,048
DeepSeek 模型deepseek-chat
适合:日常闲天、快速查询
🧠 深思mode:'think'
temperature0.3
max_tokens8,192
DeepSeek 模型deepseek-reasoner
适合:数学题、逐步分析推理
👨‍💻 专家mode:'expert'
temperature0.5
max_tokens16,384
强制模式webdesign 自动切
适合:代码威调、完整 HTML

5特殊功能:网页设计生成 & 职业顾问

🎨
网页设计模式
content_type:'webdesign'
  • 自动加载 design-md/ 风格知识库
  • 强制 expert 模式 (max_tokens=16384)
  • 60+ 世界顶级设计风格可选
  • 要求输出完整 HTML 代码块
💼
职业顾问模式
content_type:'career'
  • 内置小夕资深职业顾问提示词
  • 7大核心能力:简历 / 面试 / 谈判
  • STAR 框架回答评估
  • ATS 关键词匹配分析
chat.controller.ts — content_type 分流逻辑
// 内容类型分流:注入不同系统提示词
if (content_type === 'webdesign') {
  historyMessages.unshift({ role: 'system', content: getWebDesignSystemPrompt() });
} else if (content_type === 'career') {
  historyMessages.unshift({ role: 'system', content: getCareerSystemPrompt() });
}
// 网页设计模式强制 expert(max_tokens=16384,避免 HTML 被截断)
const effectiveMode = content_type === 'webdesign' ? 'expert' : mode;

6AI 画图生成实现详解

对接豆包火山引擎 SeedDream 文生图,内置按套餐限额(免费用户 0 张、体验券 2 张、源码版 4 张),返回的图片 URL 自动代理转为 Base64 内联解决跨域问题。

📝
权限频率控制
images_used < planLimits[套餐]
📡
豆包 SeedDream API
2048x2048,防止像素不足报错
🗃️
代理转 Base64
URL 下载 → Buffer → Data URI
计数回写
images_used +1 并写入 DB
chat.controller.ts — 图片生成限额逻辑
// 限额配置:免费=0张、体验券=2张、源码版=4张
const planLimits: Record<string, number> = { experience: 2, source: 4 };
const imageLimit = planLimits[user.plan_type] ?? 0;

if (imageLimit === 0) {
  return { error: '\u56fe\u7247\u751f\u6210\u4e3a\u4ed8\u8d39\u529f\u80fd', need_upgrade: true };
}
// 生成成功后计数回写
const imageUrl = await aiService.generateImage(prompt);
userService.incrementImagesUsed(userId);
return { imageUrl, images_used: imagesUsed + 1 };

7用户自定义 API Key 机制

用户可在左侧设置第三方 Key,请求时透传 user_api_key / user_base_url / user_model_id 三字段覆盖系统配置。平台不存储用户 Key,降低泄露风险。

ai.service.ts — 凭证优先级:用户 > DB > 环境变量
// 用户传入 Key 时,跳过 DB 读取,直接使用
if (!userApiKey) {
  model = getModelById(modelId);
  if (!model.api_key) {
    // DB 为空时,从环境变量坤底(DEEPSEEK_API_KEY 等)
    const envKey = getEnvApiKey(model.provider);
    if (envKey) model = { ...model, api_key: envKey };
  }
}
const apiKey  = userApiKey  || model?.api_key;
const baseUrl = userBaseUrl || model?.base_url || 'https://api.openai.com/v1';
🔑 配置小技巧:DB 中 api_key 为空时自动从环境变量读取 KEY。开发阶段可仅设置环境变量,无需登录后台配置。

🚀动手实践:新增一个 AI 厂商(以 Moonshot/Kimi 为例)

1
ai.service.ts 添加适配器并注册
ai.service.ts
// Step1: 新建适配器
const moonshotAdapter: ProviderAdapter = {
  buildRequestBody({ messages, mode, model, userModelId }) {
    return { model: userModelId || model.model_id, messages,
      stream: true, stream_options: { include_usage: true }, ...modeParams(mode) };
  },
  parseChunk(parsed) {
    const delta = parsed.choices?.[0]?.delta || {};
    return { content: delta.content, tokens: parsed.usage?.total_tokens };
  },
};

// Step2: 在 getAdapter() 的 map 里加一行
const map = {
  deepseek: deepseekAdapter, minimax: minimaxAdapter,
  qwen: qwenAdapter, doubao: doubaoAdapter,
  moonshot: moonshotAdapter,  // ← 加这一行
};
2
管理后台新增模型记录
name:     Kimi (Moonshot)
provider: moonshot
model_id: moonshot-v1-8k
base_url: https://api.moonshot.cn/v1
api_key:  sk-xxxxxxxx  或设 MOONSHOT_API_KEY 环境变量
3
可选:在 server/.env 中添加坤底 Key
MOONSHOT_API_KEY=sk-xxxxxxxxxxxxxxxx
4
重启后端,前端就能选择 Kimi 模型对话 🎉
工作完成!无需修改任何已有业务逻辑。适配器模式的核心价値在于:扩展开放、改动最小化

隐藏技术亮点挖掘

除了主流适配器模式,源码中还藏着几个值得仔细研读的工程化设计。

🎨
One-shot 网页设计 Agent 实现原理
design-prompts.ts — 动态提示词工程 + 强制输出格式
① 运行时动态扫描風格库
启动时扫描 design-md/ 目录,将 60+ 设计風格名号动态注入 System Prompt。新增風格只需在目录加一个文件夹,无需改代码。
② 强制输出格式
Prompt 要求 AI 仅输出一个 ```html 代码块,禁止任何解释文字。前端直接提取代码块 iframe 预览,实现一句话生成完整网页。
③ 配套工程优化
自动切 expert 模式(16384 tokens)防止长 HTML 被截断;允许内联 Tailwind CDN 和 Iconify,大幅减少 CSS 代码量。
design-prompts.ts — 运行时扫描目录构建風格列表
export function getWebDesignSystemPrompt(): string {
  // 运行时动态扫描 design-md/ 目录,获取所有可用風格名
  const designMdPath = path.join(process.cwd(), 'design-md');
  let styles: string[] = [];
  try {
    styles = fs.readdirSync(designMdPath)
      .filter(d => fs.statSync(path.join(designMdPath, d)).isDirectory());
  } catch { /* 目录不存在时降级用内置列表 */ }

  // 風格列表注入 Prompt,AI 自主选择最匹配的風格
  const styleList = styles.length > 0
    ? styles.join(', ')     // e.g. notion, stripe, apple, figma, linear, vercel...
    : 'notion, stripe, apple, figma, linear, vercel, cursor, claude';

  return `You are an expert web designer...
3. Generate a COMPLETE, standalone HTML:
   - Has all CSS inside <style>; may use Tailwind CDN + Iconify
   - Is responsive, mobile-friendly, with micro-interactions
4. Wrap ENTIRE output in ONE \`\`\`html code block.
   Output ONLY the code block. NO explanations whatsoever.
Available styles: ${styleList}`;
}
⏱️
90s 滑动不活跃超时机制
ai.service.ts — 防止 AI 厂商静默无响应导致连接挂死

部分 AI 厂商(如豆包)在请求参数有误时不报错,而是建立连接后静默无响应。项目用《滑动超时》区分“AI 在慢速思考”和“AI 已静止不动”:每收到数据就重置计时器,90s 内完全无数据才主动终止并提示用户。

ai.service.ts — 滑动超时 + 空响应检测
// 滑动超时:每收到数据就重置,区分“慢思考”和“真的挂了”
let inactivityTimer: ReturnType<typeof setTimeout> | null = null;
const INACTIVITY_TIMEOUT = 90_000; // 90s
const resetTimer = () => {
  if (inactivityTimer) clearTimeout(inactivityTimer);
  inactivityTimer = setTimeout(() => {
    response.data.destroy(); // 强制关闭 Axios stream
    onError(new Error('AI 服务超时(90s 无数据),请重试'));
  }, INACTIVITY_TIMEOUT);
};
resetTimer(); // 连接建立就启动计时

response.data.on('data', (chunk: Buffer) => {
  resetTimer(); // ← 每收到一块数据就重置,证明 AI 还在“活着”
  // ... 解析 SSE chunk ...
});

response.data.on('end', () => {
  if (inactivityTimer) clearTimeout(inactivityTimer);
  // 流结束但内容为空 → 空响应检测,主动报错而非展示空气泡
  if (!fullText && !reasoningText) {
    onError(new Error('AI 模型返回了空响应,请重试'));
  } else {
    onDone(fullText, reasoningText, tokensUsed);
  }
});
🧠
chat.controller.ts 隐藏的 4 个工程化细节
对话上下文管理中的实用设计
📏 20 条滑动语境窗口
取最近 20 条消息作为上下文,防止 Token 超限。实现仅一行:
messages.slice(-20)
📎 附件内容拼接
文件内容直接追加到用户消息中,实现“文件问答”:
const msg = attachment_content
  ? `${message}\n\n[附件]\n${attachment_content}`
  : message;
✏️ 自动生成标题
第一条消息后自动根据内容生成对话标题:
if (!regenerate && messages.length <= 1)
  chatService.autoGenerateTitle(conv_id);
🔁 Regenerate 重新生成
删除上一条 AI 回复并重新生成,保持历史干净:
if (regenerate)
  chatService.deleteLastAssistantMessage(conv_id);
多模态富媒体

多模态富媒体生成 — 3-Agent 协作架构

多模态模块采用三个 AI 异步编排噢:Agent 1 规划文档结构和配图方案,Agent 2 流式写作富文本并插入占位符,Agent 3 并行生成所有配图并实时推送到前端。

📋
Agent 1 — 规划师
AI 分析请求,返回 JSON:标题、流程概要、章节列表、配图关键词
✍️
Agent 2 — 写作家
SSE 流式写作 MD 内容,在合适位置插入 [IMAGE_N] 占位符
🎨
Agent 3 — 绘画师
扫描 fullContent 中实际占位符,并行调调豆包 SeedDream 生成配图
💾
存库前替换
用真实 URL 替换占位符,周刷页面不再出现 [IMAGE_N] 残留

10多模态 3-Agent 协作架构

设计思路:Agent 1 首先规划 → Agent 2 流式写作(SSE 实时推送)→ Agent 3 扫描占位符并并行生图 → 存库前用真实 URL 替换占位符。管理员享有最高权限,不受额度限制。
3-Agent 多模态流程图
HTTP POST
/api/multimodal/generate
MultimodalController — JWT + isAdmin 验证
Agent 1 — 规划师 (JSON 输出)
titlesummarysections[]imagePrompts[]mermaidCharts[]
Agent 2 — 写作家 (SSE 流式,推送 plan/chunk/done 事件)
⚡ 实时流式 MD 内容 · 在当前小节末插入 [IMAGE_N] 占位符 · 接收 Agent1 sections 和 mermaid 指尊
Agent 3 — 绘画师 (扫描 fullContent 中实际 [IMAGE_N] 并行生成)
regex.exec 循环整个内容 → Promise.all 并行调用 SeedDream API → SSE image_start/image 事件实时推送到前端
存库前占位符替换
savedContent = fullContent.替换([IMAGE_N] → ![img](url))
清除未能替换的残留占位符 · 写入 SQLite

11多模态 SSE 事件协议

event: plan
Agent1 规划结果
{ title, summary, sections, imagePrompts }
event: chunk
Agent2 内容片段
{ content: "文字" }
event: image_start
Agent3 开始生图
{ index, prompt }
event: image
Agent3 图片就绪
{ index, url }
event: done
全流结束 + 写 DB
{ fullContent }
event: error
任意阶段报错
{ message }
multimodal.controller.ts — Agent3 扫描占位符并并行生图
// 扫描 fullContent 中所有 [IMAGE_N] 占位符(不依赖 Agent1 的规划)
const foundPlaceholders = new Set<number>();
const placeholderRe = /\[IMAGE_(\d+)\]/g;
let phMatch: RegExpExecArray | null;
while ((phMatch = placeholderRe.exec(fullContent)) !== null) {
  foundPlaceholders.add(parseInt(phMatch[1], 10));
}

// 并行生成所有图片
await Promise.all(sortedIndices.map(async (index) => {
  const prompt = effectiveImagePrompts[index] ?? `high quality illustration for: ${plan.summary}`;
  if (canGenerateImages) {
    writeEvent('image_start', { index, prompt });
    const url = await multimodalService.generateImage(prompt);
    imageResults.set(index, url);
    writeEvent('image', { index, url });
  }
}));

// 存库前替换占位符,周刷不再浮现占位符
let savedContent = fullContent;
imageResults.forEach((url, index) => {
  const replacement = url
    ? `![\u751f\u6210\u56fe\u7247${index + 1}](${url})`
    : `> \u26A0\uFE0F \u56fe\u7247${index + 1}\u751f\u6210\u5931\u8d25`;
  savedContent = savedContent.replace(`[IMAGE_${index}]`, replacement);
});
// 清除没有生成器对应的残留占位符
savedContent = savedContent.replace(/\[IMAGE_\d+\]/g, '');
chatService.addMessage(conversation_id, 'assistant', savedContent, { content_type: 'multimodal' });
💡 管理员权限设计:管理员(is_admin=1)检测到 canGenerateImages = true,跳过所有限额检查,始终能创建最高质量的多模态文档。普通用户按套餐类型限制生图数量。

12Mermaid 图表防辡渲染与 DOM 防污染

mermaid v10/v11 在解析失败时会向 document.body 注入错误 SVG/div,项目采用三层防御策略彻底干涁。

① suppressErrors + parseError 覆盖
mermaid.initialize({} as any)(mermaid as any).parseError = () => {} 阻止默认错误渲染
② cleanMermaidArtifacts()
catch 块中删除 [id^="mermaid-"].mermaid-error-icon 等污染 DOM 元素
③ 错误信息优化
过滤掉 “mermaid version” 行,将 “Syntax error in text” 替换为 「图表语法错误」
MermaidChart.tsx — 三层防御清理逻辑
// cleanMermaidArtifacts — 清除 DOM 污染
function cleanMermaidArtifacts(id?: string) {
  if (id) document.getElementById(id)?.remove();
  document.querySelectorAll('[id^="mermaid-"], .mermaid-error-icon, .mermaid > svg.error-icon')
    .forEach((el) => el.remove());
}

// 覆盖 parseError 防止默认渲染
try { (mermaid as any).parseError = () => {}; } catch { /* ignore */ }

// catch 块中清理并优化错误提示
} catch (err: any) {
  if (gen !== genRef.current) return;
  cleanMermaidArtifacts(id); // 删除 DOM 污染
  const raw = typeof err === 'string' ? err : (err?.message ?? String(err));
  const msg = raw.split('\n')
    .filter((l: string) => !/mermaid version/i.test(l)) // 过滤版本行
    .join(' ')
    .replace(/Syntax error in text/gi, '\u56fe\u8868\u8bed\u6cd5\u9519\u8bef')
    .slice(0, 120);
  setError(msg || 'Mermaid \u56fe\u8868\u6e32\u67d3\u5931\u8d25');
}
💡 Generation Counter 模式:genRef.current++ 取消过期异步渲染。renderChain promise 链保证全局串行执行。重新生成按钮触发 genRef.current++ 并重置渲染链。
情绪识别与工单

情绪识别与工单系统 — 自动派单与人工接入

在每次 AI 对话结束时,服务端自动分析用户最后一条消息的情绪倾向,对负面情绪或人工需求自动生成工单,并向用户推送微信客服二维码。

🔍
轻量关键词匹配
无需调用 AI API,本地关键词匹配,需要人工/负面/中性三种情绪
🎫
工单自动派发
负面情绪或人工需求崇就自动写入 SQLite tickets 表
📱
微信二维码推送
前端 SSE done 事件携带 needsHuman 字段,导入 HumanSupportCard 二维码卡片
📋
管理后台工单流
pending → handling → resolved → closed 四步流转带备注功能

13情绪识别关键词匹配算法

设计思路:紧車尾、无复杂度的本地关键词匹配替代 AI 调用,应对高并发场景。覆盖【投诉/不满意/要人工】三大场景,小税无需计算。
ticket.service.ts — analyzeEmotion() 关键词表
const humanKeywords = [
  '\u4eba\u5de5', '\u5ba2\u670d', '\u4eba\u5de5\u670d\u52a1', '\u8f6c\u4eba\u5de5', '\u4eba\u5de5\u5ba2\u670d',
  '\u8054\u7cfb\u5ba2\u670d', '\u9700\u8981\u4eba\u5de5', '\u627e\u5ba2\u670d', '\u627e\u4eba', '\u5de5\u4f5c\u4eba\u5458',
];
const negativeKeywords = [
  '\u4e0d\u6ee1\u610f', '\u6ca1\u7528', '\u5783\u573e', '\u5dee\u8bc4', '\u5931\u671b', '\u7cd5\u7cd5', '\u6295\u8bc9', '\u4e0d\u5bf9', '\u9519\u4e86',
  '\u8fd9\u4e0d\u662f\u6211\u60f3\u8981\u7684', '\u5b8c\u5168\u4e0d\u5bf9', '\u7b54\u975e\u6240\u95ee',
];

const needsHuman = humanKeywords.some((k) => t.includes(k));
const isNegative = negativeKeywords.some((k) => t.includes(k));
return {
  negative: isNegative || needsHuman,
  needsHuman,
  emotion: needsHuman ? 'needs_human' : isNegative ? 'negative' : 'neutral',
};

14工单数据库设计与 API 接口

🗄️ tickets 表结构
tickets (
  id          INTEGER PK AUTOINCREMENT,
  user_id     INTEGER NOT NULL,
  conversation_id TEXT,
  last_user_message TEXT,
  emotion     TEXT DEFAULT 'neutral',
  needs_human INTEGER DEFAULT 0,
  status      TEXT DEFAULT 'pending',
  -- pending/handling/resolved/closed
  admin_notes TEXT,
  created_at  DATETIME,
  updated_at  DATETIME
)
🌐 Ticket API 接口
MethodPathAuthDescription
GET/api/admin/ticketsAdmin所有工单列表
PATCH/api/admin/tickets/:idAdmin更新状态/备注
GET/api/tickets/mineUser我的工单

15情绪识别全链路整合图

💬 用户对话
用户发送消息
↓ ChatController 处理
AI 回复完成
↓ onDone 回调
分析
🔍 analyzeEmotion()
分析用户最后一条消息
负面 → 工单 + SSE done
人工需求 → needsHuman=true
前端显示微信二维码卡片
chat.controller.ts — onDone 集成情绪分析 + done 事件扩展
// onDone 回调中分析情绪
const allMsgs = chatService.getMessages(conversation_id);
const lastUserMsg = [...allMsgs].reverse().find((m) => m.role === 'user');
const emotion = analyzeEmotion(lastUserMsg?.content || '');

// 负面情绪或人工需求:自动建工单
if (emotion.negative) {
  ticketService.createTicket({
    userId, conversationId: conversation_id,
    lastUserMessage: lastUserMsg?.content?.slice(0, 500),
    emotion: emotion.emotion, needsHuman: emotion.needsHuman,
  });
}

// done 事件携带 needsHuman 字段
writeEvent('done', { fullText, reasoningText, tokensUsed, needsHuman: emotion.needsHuman });
ChatMessages.tsx — HumanSupportCard 微信二维码卡片
// SSE done 事件 needsHuman=true 时插入 human_support 消息
if (evt === 'done' && data.needsHuman) {
  addMessage(conversation_id, {
    id: Date.now(),
    role: 'assistant',
    content: '__human_support__',
    content_type: 'human_support',
  });
}

// ChatMessages.tsx 中识别 special 类型
if (msg.content_type === 'human_support') {
  return <HumanSupportCard key={msg.id} />; // 微信二维码 + 关闭按钒
}
💡 产品设计原则:工单创建不阻断主流程(try-catch 包裹)。管理后台可查看工单、更新状态、添加备注。二维码卡片可由用户主动关闭。
常见问题

常见问题 FAQ

汇总初学者最常遇到的问题,先查这里再提问效率更高。

Qnpm install 安装失败,报权限错误怎么办?
Q后端启动报错 "Cannot find module better-sqlite3"
Q前端访问 API 报 CORS 跨域错误
QAI 对话没有响应,界面一直 loading
Q如何修改套餐价格和 Token 数量?
Q生产环境如何查看后端日志?
还有问题?

加入学员交流群,和同学一起讨论,老师在线答疑。

微信学习群 重新阅读文档