Files
tianshu-engine/vite.config.ts

173 lines
6.5 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/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, stdio: ['ignore', 'pipe', 'pipe'], shell: process.platform === 'win32' })
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('[]')
}
})
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: 60000,
stdio: ['ignore', 'pipe', 'pipe'],
shell: process.platform === 'win32',
})
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', (err: any) => {
if (responded) return
responded = true
res.writeHead(503)
res.end(JSON.stringify({ error: err?.message || 'spawn failed' }))
})
child.on('close', async (code) => {
if (responded) return
responded = true
if (code !== 0) {
res.writeHead(500)
res.end(JSON.stringify({ error: code === null ? 'opencode 超时,请重试或简化需求' : 'opencode exited with code ' + code, stderr }))
return
}
let resolvedSessionId = sessionId
let aiText = ''
for (const line of stdout.trim().split('\n')) {
try {
const event = JSON.parse(line)
if (!resolvedSessionId && event.sessionID) resolvedSessionId = event.sessionID
if (event.type === 'text' && event.part?.text) {
aiText = event.part.text
}
} catch { continue }
}
if (mode === 'json') {
const jsonMatch = aiText.match(/```json\n?([\s\S]*?)\n?```/)
const jsonStr = jsonMatch ? jsonMatch[1] : aiText
try {
JSON.parse(jsonStr)
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ result: jsonStr, sessionId: resolvedSessionId || '' }))
} catch {
res.writeHead(500)
res.end(JSON.stringify({ error: 'invalid JSON returned', raw: stdout }))
}
} else {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ result: aiText || 'done', sessionId: resolvedSessionId || '' }))
}
})
} catch (e: any) {
res.writeHead(400)
res.end(JSON.stringify({ error: e.message }))
}
})
})
},
}
}
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'),
},
},
},
})