import express from 'express' import { spawn } from 'child_process' import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const CONFIG_DIR = path.join(__dirname, '.config') const CONFIG_FILE = path.join(CONFIG_DIR, 'ai-config.json') // Ensure config directory exists if (!fs.existsSync(CONFIG_DIR)) { fs.mkdirSync(CONFIG_DIR, { recursive: true }) } // Load config from file function loadConfig() { try { if (fs.existsSync(CONFIG_FILE)) { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')) } } catch (e) { console.error('Failed to load config:', e) } return { provider: 'aliyun', apiKey: '', model: 'qwen3.6-plus', baseUrl: 'https://coding.dashscope.aliyuncs.com/v1', temperature: 0.7, maxTokens: 8000 } } // Save config to file function saveConfig(config) { try { fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8') return true } catch (e) { console.error('Failed to save config:', e) return false } } import cors from 'cors' // 导入路由 import authRoutes from './src/routes/auth.js' import plansRoutes from './src/routes/plans.js' import statsRoutes from './src/routes/stats.js' const app = express() app.use(cors({ origin: 'http://localhost:5173', methods: ['GET', 'POST', 'OPTIONS', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], exposedHeaders: ['Content-Type'] })) app.use(express.json()) // 注册路由 app.use('/api/auth', authRoutes) app.use('/api/plans', plansRoutes) app.use('/api/stats', statsRoutes) // API: Get config app.get('/api/config', (req, res) => { const config = loadConfig() // Don't expose full API key in GET request res.json({ ...config, apiKey: config.apiKey ? config.apiKey.slice(0, 8) + '********' : '' }) }) // API: Check if API key is configured app.get('/api/config/check', (req, res) => { const config = loadConfig() res.json({ configured: !!config.apiKey }) }) // API: Update config app.post('/api/config', (req, res) => { const currentConfig = loadConfig() const updates = req.body // If updating API key, use the new value; otherwise keep existing const newConfig = { ...currentConfig, ...updates, apiKey: updates.apiKey || currentConfig.apiKey } if (saveConfig(newConfig)) { res.json({ success: true }) } else { res.status(500).json({ success: false, message: 'Failed to save config' }) } }) // API: Test connection app.post('/api/config/test', async (req, res) => { const { apiKey, model, baseUrl } = req.body const config = loadConfig() const testApiKey = apiKey || config.apiKey const testModel = model || config.model const testBaseUrl = baseUrl || config.baseUrl if (!testApiKey) { return res.json({ success: false, message: '请先输入 API Key' }) } try { const response = await fetch(`${testBaseUrl}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${testApiKey}` }, body: JSON.stringify({ model: testModel, messages: [{ role: 'user', content: '你好,请回复OK' }], max_tokens: 10 }) }) const data = await response.json() if (response.ok && data.choices) { res.json({ success: true, message: `连接成功!模型: ${data.model || testModel}` }) } else { res.json({ success: false, message: data.error?.message || '连接失败' }) } } catch (error) { res.json({ success: false, message: `网络错误: ${error.message}` }) } }) // API: Proxy AI chat request (keeps API key on server) app.post('/api/chat', async (req, res) => { console.log('[api/chat] POST received, body:', JSON.stringify(req.body).substring(0, 100)) const config = loadConfig() if (!config.apiKey) { return res.status(401).json({ error: { message: '请先配置 AI API Key(访问 /settings)' } }) } // Handle client disconnect let clientDisconnected = false req.socket.on('close', () => { clientDisconnected = true console.log('[api/chat] Client socket closed') }) try { const response = await fetch(`${config.baseUrl}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.apiKey}` }, body: JSON.stringify({ ...req.body, model: req.body.model || config.model, temperature: req.body.temperature ?? config.temperature, max_tokens: req.body.max_tokens || config.maxTokens }) }) if (!response.ok) { const error = await response.json().catch(() => ({})) return res.status(response.status).json(error) } console.log('[api/chat] AI response status:', response.status) // Stream the response to client res.setHeader('Content-Type', 'application/x-ndjson') res.setHeader('Cache-Control', 'no-cache') res.setHeader('Connection', 'keep-alive') res.setHeader('X-Accel-Buffering', 'no') console.log('[api/chat] Streaming started') const reader = response.body.getReader() const decoder = new TextDecoder() let totalBytes = 0 let chunkCount = 0 while (true) { if (clientDisconnected) { reader.cancel() return } const { done, value } = await reader.read() if (done) break const chunk = decoder.decode(value, { stream: true }) totalBytes += chunk.length chunkCount++ res.write(chunk) if (chunkCount % 10 === 0) { console.log('[api/chat] Chunk count:', chunkCount, 'bytes:', totalBytes) } } console.log('[api/chat] Streaming done, total chunks:', chunkCount, 'bytes:', totalBytes) res.end() } catch (error) { if (!clientDisconnected) { res.status(500).json({ error: { message: `请求失败: ${error.message}` } }) } } }) // Catch-all for debugging app.all('/api/*', (req, res) => { console.log('[DEBUG CATCH-ALL]', req.method, req.path) res.status(404).json({ error: 'Route not found' }) }) async function start() { const isDev = process.env.NODE_ENV !== 'production' if (isDev) { console.log('[Server] Starting Vite dev server...') // Run Vite dev server on port 5173 // API routes are handled by Express on port 3001 // We use a simple approach: start both servers const viteProcess = spawn('npx', ['vite'], { stdio: 'inherit', shell: true, env: { ...process.env, NODE_ENV: 'development' } }) viteProcess.on('error', (err) => { console.error('[Server] Failed to start Vite:', err) }) // Start Express API server on port 3001 const apiPort = 3001 app.listen(apiPort, () => { console.log(`[Server] API server running at http://localhost:${apiPort}`) console.log(`[Server] API Key stored in: ${CONFIG_FILE}`) }) } else { // Production: serve static files app.use(express.static(path.join(__dirname, 'dist'))) const port = process.env.PORT || 3000 app.listen(port, () => { console.log(`Server running on port ${port}`) }) } } start()