ai-chat-ui/server/server.js

352 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

const express = require('express');
const cors = require('cors');
const { createProxyMiddleware } = require('http-proxy-middleware');
const multer = require('multer');
const { v4: uuidv4 } = require('uuid');
const path = require('path');
const fs = require('fs');
const morgan = require('morgan');
require('dotenv').config();
// 引入自定义日志系统
const { logger } = require('./logger');
const app = express();
const PORT = process.env.PORT || 3000;
// 配置全局请求日志中间件
app.use((req, res, next) => {
const startTime = Date.now();
// 保存原始的res.end方法
const originalEnd = res.end;
res.end = function(chunk, encoding) {
// 调用自定义日志记录
logger.http(req, res, startTime);
// 调用原始的res.end
originalEnd.call(this, chunk, encoding);
};
next();
});
// 配置 CORS允许前端项目的跨域请求
app.use(cors());
// --- 1. 流式对话请求代理配置 ---
app.use('/api/chat-ui/chat', (req, res, next) => {
logger.info('Received chat request', {
method: req.method,
url: req.url,
headers: {
'content-type': req.headers['content-type'],
'content-length': req.headers['content-length']
}
});
const apiKey = process.env.ALIYUN_API_KEY;
if (!apiKey) {
logger.error("【错误】发送代理请求前未配置 ALIYUN_API_KEY !");
} else {
req.headers['authorization'] = `Bearer ${apiKey}`;
logger.debug('Added authorization header for chat request');
}
next();
});
// 注意:代理中间件需要在 body-parser (express.json) 之前,不然代理会导致请求体丢失
app.use(
'/api/chat-ui/chat',
createProxyMiddleware({
// 阿里云百炼由于兼容 OpenAI 格式,所以代理到此接口
target: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
changeOrigin: true,
// 去除路径前缀,确保发往阿里云的路径是纯正的 completions 路径
pathRewrite: {
'^/api/chat-ui/chat': '',
},
// 代理日志
onProxyReq: (proxyReq, req, res) => {
logger.info('Proxying chat request to DashScope API', {
target: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
method: req.method,
url: req.url
});
},
onProxyRes: (proxyRes, req, res) => {
logger.info('Received response from DashScope API', {
statusCode: proxyRes.statusCode,
headers: proxyRes.headers
});
},
onError: (err, req, res) => {
logger.error('Error occurred during proxying chat request', {
error: err.message,
method: req.method,
url: req.url
});
res.status(500).json({ error: 'Proxy error: ' + err.message });
}
})
);
// --- 下面的路由专门走 Node.js 业务逻辑,因此需要解析 JSON Body ---
app.use(express.json());
// 内存中暂时存放对话数据用于 Mock
const conversationsDB = {};
// --- 2. 获取模型列表 ---
app.get('/api/chat-ui/models', (req, res) => {
logger.info('Getting models list', {
method: req.method,
url: req.url,
ip: req.ip || req.connection.remoteAddress
});
try {
const models = [
{
id: "qwen-max",
name: "通义千问 Max",
description: "最强大的模型",
maxTokens: 8192,
provider: "Aliyun"
},
{
id: "qwen-plus",
name: "通义千问 Plus",
description: "能力均衡",
maxTokens: 8192,
provider: "Aliyun"
}
];
res.json(models);
logger.info('Successfully returned models list', { count: models.length });
} catch (error) {
logger.error('Error getting models list', { error: error.message });
res.status(500).json({ error: 'Internal server error' });
}
});
// --- 3. 获取所有对话历史 ---
app.get('/api/chat-ui/conversations', (req, res) => {
logger.info('Getting all conversations', {
method: req.method,
url: req.url,
ip: req.ip || req.connection.remoteAddress
});
try {
const conversations = Object.values(conversationsDB);
res.json(conversations);
logger.info('Successfully returned conversations', { count: conversations.length });
} catch (error) {
logger.error('Error getting conversations', { error: error.message });
res.status(500).json({ error: 'Internal server error' });
}
});
// --- 4. 获取单个对话 ---
app.get('/api/chat-ui/conversations/:id', (req, res) => {
const { id } = req.params;
logger.info('Getting conversation by ID', {
method: req.method,
url: req.url,
conversationId: id,
ip: req.ip || req.connection.remoteAddress
});
try {
const conversation = conversationsDB[id];
if (conversation) {
res.json(conversation);
logger.info('Successfully returned conversation', { conversationId: id });
} else {
res.status(404).json({ error: '对话不存在' });
logger.warn('Conversation not found', { conversationId: id });
}
} catch (error) {
logger.error('Error getting conversation by ID', {
conversationId: id,
error: error.message
});
res.status(500).json({ error: 'Internal server error' });
}
});
// --- 5. 保存或更新对话 ---
app.post('/api/chat-ui/conversations', (req, res) => {
logger.info('Creating or updating conversation', {
method: req.method,
url: req.url,
body: {
hasId: !!req.body.id,
title: req.body.title
},
ip: req.ip || req.connection.remoteAddress
});
try {
const data = req.body;
if(!data.id) {
data.id = uuidv4();
logger.debug('Generated new conversation ID', { conversationId: data.id });
}
conversationsDB[data.id] = data;
res.json(data);
logger.info('Successfully saved conversation', { conversationId: data.id });
} catch (error) {
logger.error('Error saving conversation', { error: error.message });
res.status(500).json({ error: 'Internal server error' });
}
});
// --- 6. 删除对话 ---
app.delete('/api/chat-ui/conversations/:id', (req, res) => {
const { id } = req.params;
logger.info('Deleting conversation', {
method: req.method,
url: req.url,
conversationId: id,
ip: req.ip || req.connection.remoteAddress
});
try {
if (conversationsDB[id]) {
delete conversationsDB[id];
res.json({ success: true, message: "删除成功" });
logger.info('Successfully deleted conversation', { conversationId: id });
} else {
res.status(404).json({ error: '对话不存在' });
logger.warn('Attempted to delete non-existent conversation', { conversationId: id });
}
} catch (error) {
logger.error('Error deleting conversation', {
conversationId: id,
error: error.message
});
res.status(500).json({ error: 'Internal server error' });
}
});
// 为了存储上传文件而建立临时目录
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
logger.info('Created uploads directory', { path: uploadDir });
}
// 配置文件上传
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, uploadDir);
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + '-' + file.originalname);
}
});
const upload = multer({ storage: storage });
// 提供静态文件访问支持
app.use('/uploads', express.static(uploadDir));
// --- 7. 上传文件 ---
app.post('/api/chat-ui/upload', upload.single('file'), (req, res) => {
logger.info('Processing file upload', {
method: req.method,
url: req.url,
originalFilename: req.file ? req.file.originalname : 'none',
mimetype: req.file ? req.file.mimetype : 'none',
size: req.file ? req.file.size : 'none',
ip: req.ip || req.connection.remoteAddress
});
if (!req.file) {
const errorMsg = '没有文件上传';
logger.warn(errorMsg, {
method: req.method,
url: req.url
});
return res.status(400).json({ error: errorMsg });
}
try {
// 返回供前端使用和访问的 URL
const response = {
url: `http://localhost:${PORT}/uploads/${req.file.filename}`,
name: req.file.originalname,
size: req.file.size,
mimeType: req.file.mimetype
};
res.json(response);
logger.info('Successfully uploaded file', {
filename: req.file.filename,
originalName: req.file.originalname,
url: response.url,
size: req.file.size
});
} catch (error) {
logger.error('Error processing file upload', {
originalFilename: req.file.originalname,
error: error.message
});
res.status(500).json({ error: 'File upload processing failed' });
}
});
// --- 8. 停止生成 ---
app.post(['/api/chat-ui/stop', '/api/chat-ui/stop/:id'], (req, res) => {
const id = req.params.id;
logger.info('Stop generation request received', {
method: req.method,
url: req.url,
messageId: id,
ip: req.ip || req.connection.remoteAddress
});
try {
res.json({ success: true, message: "已发出停止指令" });
logger.info('Stop generation request processed', { messageId: id });
} catch (error) {
logger.error('Error processing stop generation request', {
messageId: id,
error: error.message
});
res.status(500).json({ error: 'Internal server error' });
}
});
// 其他所有路由返回404
app.use((req, res) => {
logger.warn('Route not found', {
method: req.method,
url: req.url,
ip: req.ip || req.connection.remoteAddress
});
res.status(404).json({ error: 'Endpoint not found' });
});
app.listen(PORT, () => {
logger.info('Server started successfully', {
port: PORT,
timestamp: new Date().toISOString()
});
console.log('====================================');
console.log(`本地代理服务器已启动,监听端口: ${PORT}`);
console.log('====================================');
if (!process.env.ALIYUN_API_KEY) {
console.log('⚠️ 警告: 未在 .env 文件中检测到 ALIYUN_API_KEY!');
console.log('请在 server/.env 中添加您的百炼 API Key。');
} else {
console.log('✅ 检测到了 API Key。');
logger.info('API Key detected in environment');
}
});