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.

251 lines
6.8 KiB

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'
const app = express()
app.use(cors({
origin: 'http://localhost:5173',
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type'],
exposedHeaders: ['Content-Type']
}))
app.use(express.json())
// 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()