You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

261 lines
7.1 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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()