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.

220 lines
5.7 KiB

import express from 'express'
import { createServer as createViteServer } from 'vite'
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
}
}
const app = express()
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) => {
const config = loadConfig()
if (!config.apiKey) {
return res.status(401).json({ error: { message: '请先配置 AI API Key访问 /settings' } })
}
// Handle client disconnect
let clientDisconnected = false
req.on('close', () => { clientDisconnected = true })
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,
response_format: { type: 'json_object' }
})
})
if (!response.ok) {
const error = await response.json().catch(() => ({}))
return res.status(response.status).json(error)
}
// Stream the response to client
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
res.setHeader('X-Accel-Buffering', 'no')
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
if (clientDisconnected) {
reader.cancel()
return
}
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
try {
res.write(chunk)
} catch (e) {
// Client disconnected, stop writing
reader.cancel()
return
}
}
res.end()
} catch (error) {
if (!clientDisconnected) {
res.status(500).json({ error: { message: `请求失败: ${error.message}` } })
}
}
})
async function start() {
const isDev = process.env.NODE_ENV !== 'production'
if (isDev) {
// Development: use Vite middleware
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'spa'
})
app.use(vite.middlewares)
const port = 5173
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`)
console.log(`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()