Files
tianshu-engine/vite.config.ts

176 lines
6.6 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) {
const opencodeBin = resolve(__dirname, 'node_modules', 'opencode-ai', 'bin', 'opencode.exe')
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(opencodeBin, ['session', 'list', '--format', 'json'], { timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'] })
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'
? `当前项目根目录: ${__dirname}\n代码模式直接修改 src/ 下的源码文件并保存。需求:`
: `当前项目根目录: ${__dirname}\nJSON模式只返回修改后的 JSON 文本,不要写任何文件。需求:`
const fullMessage = modePrefix + userMessage
const args = ['run', '--model', 'deepseek/deepseek-v4-pro', '--format', 'json']
if (sessionId) args.push('--session', sessionId)
args.push(fullMessage)
const child = spawn(opencodeBin, args, {
env: { ...process.env, DEEPSEEK_API_KEY: apiKey || process.env.DEEPSEEK_API_KEY || '' },
timeout: 60000,
stdio: ['ignore', 'pipe', 'pipe'],
})
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 rawLine of stdout.split(/\r?\n/)) {
const line = rawLine.trim()
if (!line) continue
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 codeBlock = aiText.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/)
let jsonStr = codeBlock ? codeBlock[1] : aiText
try {
JSON.parse(jsonStr)
} catch {
const bareMatch = aiText.match(/(\{[\s\S]*\}|\[[\s\S]*\])/)
jsonStr = bareMatch ? bareMatch[0] : aiText
}
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: 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'),
},
},
},
})