352 lines
10 KiB
JavaScript
352 lines
10 KiB
JavaScript
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');
|
||
}
|
||
});
|