import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path' import fs from 'fs' import { spawn } from 'child_process' function apiSavePlugin() { return { name: 'api-save', configureServer(server: any) { server.middlewares.use('/api/save', (req: any, res: any) => { if (req.method !== 'POST') { res.writeHead(405); res.end(); return } let body = '' req.on('data', (c: string) => body += c) req.on('end', () => { try { const { path, data } = JSON.parse(body) if (!path || typeof path !== 'string' || !path.startsWith('/scenes/')) { res.writeHead(400) res.end(JSON.stringify({ error: 'invalid path' })) return } const safePath = resolve(__dirname, 'public', '.' + path) fs.writeFileSync(safePath, JSON.stringify(data, null, 2)) res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ ok: true })) } catch (e: any) { res.writeHead(400) res.end(JSON.stringify({ error: e.message })) } }) }) const dedupMap = new Map() server.middlewares.use('/api/ai', (req: any, res: any) => { if (req.method !== 'POST') { res.writeHead(405); res.end(); return } let body = '' req.on('data', (c: string) => body += c) req.on('end', () => { try { const { sessionId, userMessage, apiKey, mode } = JSON.parse(body) if (!userMessage || !sessionId) { res.writeHead(400); res.end(JSON.stringify({ error: 'missing fields' })); return } const dedupKey = `${mode}_${userMessage}` const last = dedupMap.get(dedupKey) || 0 if (Date.now() - last < 3000) { res.writeHead(429) res.end(JSON.stringify({ error: 'duplicate request' })) return } dedupMap.set(dedupKey, Date.now()) const modePrefix = mode === 'code' ? '代码模式:直接修改 src/ 下的源码文件并保存。需求:' : 'JSON模式:只返回修改后的 JSON 文本,不要写任何文件。需求:' const fullMessage = modePrefix + userMessage const opencodeBin = resolve(__dirname, 'node_modules', '.bin', 'opencode') const child = spawn(opencodeBin, ['run', '--session', sessionId, '--model', 'deepseek', '--format', 'json', fullMessage], { env: { ...process.env, DEEPSEEK_API_KEY: apiKey || process.env.DEEPSEEK_API_KEY || '' }, timeout: 15000, }) let stdout = '' let stderr = '' child.stdout.on('data', (d: Buffer) => stdout += d.toString()) child.stderr.on('data', (d: Buffer) => stderr += d.toString()) child.on('close', (code) => { if (code !== 0) { res.writeHead(500) res.end(JSON.stringify({ error: 'opencode exited with code ' + code, stderr })) return } if (mode === 'json') { // try to extract JSON block from response const match = stdout.match(/```json\n?([\s\S]*?)\n?```|\{[\s\S]*\}/) const jsonStr = match ? (match[1] || match[0]) : stdout try { JSON.parse(jsonStr) } catch { res.writeHead(500) res.end(JSON.stringify({ error: 'invalid JSON returned', raw: stdout })) return } res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ result: jsonStr })) } else { res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ result: stdout || 'done' })) } }) } catch (e: any) { res.writeHead(400) res.end(JSON.stringify({ error: e.message })) } }) }) server.middlewares.use('/api/ai/sessions', (req: any, res: any) => { try { const opencodeBin = resolve(__dirname, 'node_modules', '.bin', 'opencode') const child = spawn(opencodeBin, ['session', 'list'], { timeout: 5000 }) let stdout = '' child.stdout.on('data', (d: Buffer) => stdout += d.toString()) child.on('close', () => { res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(stdout || '[]') }) } catch { res.writeHead(200, { 'Content-Type': 'application/json' }) res.end('[]') } }) }, } } export default defineConfig({ plugins: [vue(), apiSavePlugin()], resolve: { alias: { '@': resolve(__dirname, 'src'), '@engine': resolve(__dirname, 'engine'), }, }, build: { rollupOptions: { input: { main: resolve(__dirname, 'index.html'), editor: resolve(__dirname, 'editor/index.html'), }, }, }, })