|
|
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'
|
|
|
import sharesRoutes from './src/routes/shares.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)
|
|
|
app.use('/api/shares', sharesRoutes)
|
|
|
|
|
|
// 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')))
|
|
|
|
|
|
// SPA catch-all: serve index.html for all non-API routes
|
|
|
app.get('*', (req, res) => {
|
|
|
res.sendFile(path.join(__dirname, 'dist', 'index.html'))
|
|
|
})
|
|
|
|
|
|
const port = process.env.PORT || 3000
|
|
|
app.listen(port, () => {
|
|
|
console.log(`Server running on port ${port}`)
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
|
|
|
start()
|