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

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