Files
tianshu-engine/vite.config.ts

179 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, number>()
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) { res.writeHead(400); res.end(JSON.stringify({ error: 'missing fields' })); return }
const dedupKey = `${mode}_${sessionId || ''}_${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 args = ['opencode', 'run', '--model', 'deepseek/deepseek-v4-pro', '--format', 'json']
if (sessionId) args.splice(2, 0, '--session', sessionId)
args.push(fullMessage)
const child = spawn('npx', args, {
env: { ...process.env, DEEPSEEK_API_KEY: apiKey || process.env.DEEPSEEK_API_KEY || '' },
timeout: 15000,
})
let stdout = ''
let stderr = ''
let responded = false
child.stdout.on('data', (d: Buffer) => stdout += d.toString())
child.stderr.on('data', (d: Buffer) => stderr += d.toString())
child.on('error', () => {
if (responded) return
responded = true
res.writeHead(503)
res.end(JSON.stringify({ error: 'opencode 未安装,请运行 npm install' }))
})
child.on('close', async (code) => {
if (responded) return
responded = true
if (code !== 0) {
res.writeHead(500)
res.end(JSON.stringify({ error: 'opencode exited with code ' + code, stderr }))
return
}
let resolvedSessionId = sessionId
if (!resolvedSessionId) {
try {
const listChild = spawn('npx', ['opencode', 'session', 'list', '--format', 'json', '--max-count', '1'], {
timeout: 5000,
env: { ...process.env, DEEPSEEK_API_KEY: apiKey || process.env.DEEPSEEK_API_KEY || '' },
})
let listOut = ''
listChild.stdout.on('data', (d: Buffer) => listOut += d.toString())
listChild.on('error', () => {})
await new Promise<void>((resolveList) => listChild.on('close', () => {
try {
const sessions = JSON.parse(listOut)
if (Array.isArray(sessions) && sessions.length > 0) {
resolvedSessionId = sessions[0].id
}
} catch {}
resolveList()
}))
} catch {}
}
if (mode === 'json') {
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, sessionId: resolvedSessionId || '' }))
} else {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ result: stdout || 'done', sessionId: resolvedSessionId || '' }))
}
})
} catch (e: any) {
res.writeHead(400)
res.end(JSON.stringify({ error: e.message }))
}
})
})
server.middlewares.use('/api/ai/sessions', (req: any, res: any) => {
if (req.method !== 'GET') { res.writeHead(405); res.end(); return }
try {
const child = spawn('npx', ['opencode', 'session', 'list', '--format', 'json'], { timeout: 5000 })
let stdout = ''
let responded = false
child.stdout.on('data', (d: Buffer) => stdout += d.toString())
child.on('error', () => {
if (responded) return
responded = true
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end('[]')
})
child.on('close', () => {
if (responded) return
responded = true
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'),
},
},
},
})