5 分钟跑起来
如果你已经有了 Node.js 环境,按照下面步骤可以在 5 分钟内启动项目。
前置条件
需要安装 Node.js ≥ 18 和 npm ≥ 9,建议使用 Node.js 20 LTS 版本。
-
克隆项目将项目代码下载到本地。terminal
# 如果你有 ZIP 包,解压后进入目录 # 或者从 git 克隆 git clone <your-repo-url> ai-chat cd ai-chat
-
安装全部依赖根目录一条命令安装前后端全部依赖(约需 1~3 分钟)。terminal
# 在项目根目录执行 npm install cd client && npm install cd ../server && npm install cd ..
-
配置环境变量复制示例配置并修改关键参数(详见配置章节)。terminal
cp server/.env.example server/.env # 然后用编辑器打开 server/.env 填写配置 -
启动开发服务器在根目录运行,前后端同时启动。terminal
npm run dev # 输出示例: [server] NestJS application is running on: http://localhost:3000 [client] Local: http://localhost:5173
-
访问应用打开浏览器访问前端地址,即可看到完整的 AI Chat 界面。
开发环境搭建
在开始之前,你需要在电脑上安装以下工具。如果已经安装可以跳过对应步骤。
验证安装
打开终端(macOS: 按 Cmd+空格,搜索"终端"),运行以下命令:
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 文件管理配置。首先复制示例文件,再逐项填写。
# ============================ # 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 地址:
# 开发时指定后端地址(默认已回退到 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/ 目录中。
根目录结构
后端目录结构
前端目录结构
后端核心模块解析
后端采用 NestJS 模块化架构,每个功能域对应一个独立模块,各模块职责清晰、相互解耦。
负责用户注册、登录、JWT Token 签发与验证。基于 Passport.js + JWT 策略实现无状态认证,支持第三方 SSO 单点登录。
管理用户信息、Token 消耗记录、套餐状态。支持管理员后台操作。
核心业务模块。管理对话会话、消息记录,通过 SSE 流式返回 AI 响应。支持三种对话模式。
基于 Multer 处理文件上传,支持 TXT、Markdown、Word (docx) 等格式。文件内容可作为上下文注入到 AI 对话。
集成 JitPay 支付平台,处理套餐购买、支付回调,为用户充値 Token 额度。提供体验券和源码套餐。
封装不同 AI 模型的调用逻辑,通过统一接口支持豆包、Kimi、DeepSeek 等多种模型,模式切换对上层透明。
前端技术架构详解
前端基于 React 19 + Vite 8 + TypeScript 构建,使用 Zustand 轻量状态管理,TailwindCSS 实现样式。
技术栈组成
AI 对话模式
快速响应,适合日常问答。使用响应速度最快的模型配置。
深度推理,展示思维过程。适合复杂逻辑和分析任务。
专业回答,包含技术细节。适合需要精准专业知识的场景。
AI 模式配置位置
在 client/src/constants.ts 的 CHAT_MODES 数组中配置,可自行添加或修改模式。
后端技术架构详解
后端采用 NestJS 11 + SQLite + TypeScript,数据库使用轻量的 SQLite(better-sqlite3),无需额外安装数据库服务。
系统分层架构
为什么选择 SQLite?
SQLite 是一个文件型数据库,无需安装任何服务,数据直接存储在 server/data/aichat.db 文件中。非常适合学习项目和中小型应用,生产环境如需更高并发可迁移到 PostgreSQL/MySQL。
生产环境部署
将项目部署到云服务器(Linux),推荐使用腾讯云、阿里云或任意 Linux VPS。
-
服务器要求Linux(Ubuntu 20.04/22.04 推荐),最低配置 1 核 2G 内存。1数2G最低配置Ubuntu 22.04推荐系统80 / 443开放端口
-
服务器安装 Node.js + PM2SSH 登录服务器后执行。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
-
上传代码并构建本地用 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 # 填写生产配置
-
使用 PM2 启动后端PM2 确保后端和系统一起启动,崩溃后自动重启。ssh terminal
pm2 start dist/main.js --name ai-chat-server pm2 save && pm2 startup # 开机自启 pm2 status # 查看运行状态
Nginx 反向代理配置
Nginx 同时承担前端静态文件服务和后端 API 反向代理,并可配置 HTTPS。
# 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 接口速览
后端提供 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/stream | SSE 流式发送消息并获取 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 Chat 的 AI 能力模块采用「提供商适配器模式」,支持多家大模型厂商无缝接入,配合 SSE 流式输出,实现连续对话、深度思考、网页设计生成、AI 画图等多种功能。
1AI 模块整体架构
2提供商适配器模式 — 多厂商支持原理
项目设计了 ProviderAdapter 接口,每个厂商实现两个方法:构建请求体和解析响应块。添加新厂商只需写一个对象,主流程不需修改。
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;
}
buildRequestBody 和 parseChunk; 然后在 getAdapter() 的 map 里加一行 key→适配器映射即可。3SSE 流式输出完整实现
{ content: "你好" }{ content: "分析..." }{ fullText, tokensUsed }{ message: "..." }// ⚠️ 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 通道正式建立
// 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(); },
});
// 用 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);
}
}
EventSource 仅支持 GET 请求,无法携带请求体和 Authorization 头。项目采用 fetch + ReadableStream 方案:完整支持 POST Body 和 JWT 鉴权,并用 buf.split('\n\n') 正确处理跨 chunk 的 SSE 分隔符拼接问题。4三种对话模式详解
mode:'fast'| temperature | 0.7 |
| max_tokens | 2,048 |
| DeepSeek 模型 | deepseek-chat |
mode:'think'| temperature | 0.3 |
| max_tokens | 8,192 |
| DeepSeek 模型 | deepseek-reasoner |
mode:'expert'| temperature | 0.5 |
| max_tokens | 16,384 |
| 强制模式 | webdesign 自动切 |
5特殊功能:网页设计生成 & 职业顾问
content_type:'webdesign'- 自动加载
design-md/风格知识库 - 强制 expert 模式 (max_tokens=16384)
- 60+ 世界顶级设计风格可选
- 要求输出完整 HTML 代码块
content_type:'career'- 内置小夕资深职业顾问提示词
- 7大核心能力:简历 / 面试 / 谈判
- STAR 框架回答评估
- ATS 关键词匹配分析
// 内容类型分流:注入不同系统提示词
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 内联解决跨域问题。
// 限额配置:免费=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,降低泄露风险。
// 用户传入 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';api_key 为空时自动从环境变量读取 KEY。开发阶段可仅设置环境变量,无需登录后台配置。🚀动手实践:新增一个 AI 厂商(以 Moonshot/Kimi 为例)
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, // ← 加这一行
};name: Kimi (Moonshot)
provider: moonshot
model_id: moonshot-v1-8k
base_url: https://api.moonshot.cn/v1
api_key: sk-xxxxxxxx 或设 MOONSHOT_API_KEY 环境变量server/.env 中添加坤底 KeyMOONSHOT_API_KEY=sk-xxxxxxxxxxxxxxxx⭐隐藏技术亮点挖掘
除了主流适配器模式,源码中还藏着几个值得仔细研读的工程化设计。
design-md/ 目录,将 60+ 设计風格名号动态注入 System Prompt。新增風格只需在目录加一个文件夹,无需改代码。```html 代码块,禁止任何解释文字。前端直接提取代码块 iframe 预览,实现一句话生成完整网页。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}`;
}
部分 AI 厂商(如豆包)在请求参数有误时不报错,而是建立连接后静默无响应。项目用《滑动超时》区分“AI 在慢速思考”和“AI 已静止不动”:每收到数据就重置计时器,90s 内完全无数据才主动终止并提示用户。
// 滑动超时:每收到数据就重置,区分“慢思考”和“真的挂了”
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);
}
});
messages.slice(-20)const msg = attachment_content
? `${message}\n\n[附件]\n${attachment_content}`
: message;if (!regenerate && messages.length <= 1)
chatService.autoGenerateTitle(conv_id);if (regenerate)
chatService.deleteLastAssistantMessage(conv_id);多模态富媒体生成 — 3-Agent 协作架构
多模态模块采用三个 AI 异步编排噢:Agent 1 规划文档结构和配图方案,Agent 2 流式写作富文本并插入占位符,Agent 3 并行生成所有配图并实时推送到前端。
10多模态 3-Agent 协作架构
11多模态 SSE 事件协议
{ title, summary, sections, imagePrompts }{ content: "文字" }{ index, prompt }{ index, url }{ fullContent }{ message }// 扫描 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
? ``
: `> \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,项目采用三层防御策略彻底干涁。
mermaid.initialize({} as any) 和 (mermaid as any).parseError = () => {} 阻止默认错误渲染[id^="mermaid-"]、.mermaid-error-icon 等污染 DOM 元素// 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');
}
genRef.current++ 取消过期异步渲染。renderChain promise 链保证全局串行执行。重新生成按钮触发 genRef.current++ 并重置渲染链。情绪识别与工单系统 — 自动派单与人工接入
在每次 AI 对话结束时,服务端自动分析用户最后一条消息的情绪倾向,对负面情绪或人工需求自动生成工单,并向用户推送微信客服二维码。
13情绪识别关键词匹配算法
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 (
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
)| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/admin/tickets | Admin | 所有工单列表 |
| PATCH | /api/admin/tickets/:id | Admin | 更新状态/备注 |
| GET | /api/tickets/mine | User | 我的工单 |
15情绪识别全链路整合图
// 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 });
// 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} />; // 微信二维码 + 关闭按钒
}
常见问题 FAQ
汇总初学者最常遇到的问题,先查这里再提问效率更高。