const http = require('http'); const https = require('https'); const fs = require('fs'); const path = require('path'); const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB // 读取配置文件 function loadConfig() { const configPath = path.join(__dirname, 'config.json'); if (!fs.existsSync(configPath)) { console.error('❌ 未找到 config.json,请创建配置文件'); process.exit(1); } const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); const apiKey = config.api_key; if (!apiKey || apiKey === 'sk-你的API密钥') { console.warn('⚠️ 请在 config.json 中配置真实的 API Key(当前为占位值)'); console.warn('⚠️ API 请求将会失败,但界面可以正常访问\n'); } return { apiKey: apiKey || '', baseUrl: config.base_url || 'https://yibuapi.com/v1' }; } const CONFIG = loadConfig(); console.log(`✅ API Key 已加载: ${CONFIG.apiKey.slice(0, 8)}...`); console.log(`🌐 API Base URL: ${CONFIG.baseUrl}`); // MIME 类型映射 const MIME_TYPES = { '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.png': 'image/png', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', }; const PORT = 8080; const TARGET_URL = new URL(CONFIG.baseUrl); // 读取请求体,带大小限制 function readBody(req, limit, callback) { const chunks = []; let size = 0; req.on('data', chunk => { size += chunk.length; if (size > limit) { req.destroy(); return callback(new Error('请求体超过大小限制')); } chunks.push(chunk); }); req.on('end', () => callback(null, Buffer.concat(chunks))); req.on('error', err => callback(err)); } // 代理 API 请求 function proxyRequest(req, res) { readBody(req, MAX_BODY_SIZE, (err, body) => { if (err) { res.writeHead(413, { 'Content-Type': 'application/json; charset=utf-8' }); return res.end(JSON.stringify({ error: { message: err.message } })); } const options = { hostname: TARGET_URL.hostname, port: TARGET_URL.port || 443, path: req.url, method: req.method, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CONFIG.apiKey}`, 'Content-Length': Buffer.byteLength(body), }, }; const proxyReq = https.request(options, proxyRes => { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); proxyRes.on('error', err => { console.error('[代理] 响应流错误:', err.message); if (!res.writableEnded) res.end(); }); }); proxyReq.on('error', err => { console.error('[代理] 请求失败:', err.message); if (!res.headersSent) { res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' }); } res.end(JSON.stringify({ error: { message: `API 请求失败: ${err.message}` } })); }); proxyReq.setTimeout(180000, () => { proxyReq.destroy(); if (!res.headersSent) { res.writeHead(504, { 'Content-Type': 'application/json; charset=utf-8' }); } res.end(JSON.stringify({ error: { message: '请求超时,请重试' } })); }); res.on('close', () => { if (!proxyReq.destroyed) proxyReq.destroy(); }); proxyReq.write(body); proxyReq.end(); }); } // 对话数据存储 const DATA_DIR = path.join(__dirname, 'data'); const CONV_FILE = path.join(DATA_DIR, 'conversations.json'); const CONV_TMP = CONV_FILE + '.tmp'; function loadConversations() { try { if (fs.existsSync(CONV_FILE)) { return JSON.parse(fs.readFileSync(CONV_FILE, 'utf-8')); } } catch(e) { console.error('读取对话数据失败:', e.message); } return []; } // 原子写:先写 .tmp 再 rename,避免写盘崩溃导致文件损坏 function saveConversations(data) { if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); fs.writeFileSync(CONV_TMP, JSON.stringify(data), 'utf-8'); fs.renameSync(CONV_TMP, CONV_FILE); } // 静态文件服务 function serveStatic(req, res) { let filePath = req.url === '/' ? '/index.html' : req.url; filePath = path.join(__dirname, filePath); // 安全检查:防止目录遍历(__dirname 追加 path.sep 避免前缀匹配绕过) const root = __dirname + path.sep; if (!path.resolve(filePath).startsWith(root)) { res.writeHead(403); res.end('403 Forbidden'); return; } const ext = path.extname(filePath); const contentType = MIME_TYPES[ext] || 'application/octet-stream'; fs.readFile(filePath, (err, data) => { if (err) { if (err.code === 'ENOENT') { res.writeHead(404); res.end('404 Not Found'); } else { res.writeHead(500); res.end('500 Internal Server Error'); } return; } res.writeHead(200, { 'Content-Type': contentType }); res.end(data); }); } // 创建服务器 const server = http.createServer((req, res) => { // 使用 WHATWG URL API(url.parse 在 Node 24 已弃用) const parsed = new URL(req.url, 'http://localhost'); // API 代理 if (parsed.pathname.startsWith('/v1/')) { return proxyRequest(req, res); } // 对话数据接口 if (parsed.pathname === '/api/conversations') { if (req.method === 'GET') { const data = loadConversations(); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); return res.end(JSON.stringify(data)); } if (req.method === 'POST') { readBody(req, MAX_BODY_SIZE, (err, raw) => { if (err) { res.writeHead(413, { 'Content-Type': 'application/json; charset=utf-8' }); return res.end(JSON.stringify({ ok: false, error: err.message })); } try { const data = JSON.parse(raw.toString()); saveConversations(data); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ ok: true })); } catch(e) { res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ ok: false, error: e.message })); } }); return; } } // 静态文件 serveStatic(req, res); }); server.listen(PORT, () => { const convs = loadConversations(); console.log(`💬 已加载 ${convs.length} 个对话`); console.log(`🚀 聊天工具已启动: http://localhost:${PORT}`); console.log('📋 按 Ctrl+C 停止服务\n'); });