Files
tianshu-engine/vite.config.ts

177 lines
7.1 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
console.log(`\n[AI] ${mode === 'json' ? 'JSON' : '代码'}模式 | session=${sessionId || '新会话'} | prompt: ${userMessage.substring(0, 120)}${userMessage.length > 120 ? '...' : ''}`)
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) {
console.log(`[AI] 错误: ${code === null ? '超时' : 'exit code ' + code}${stderr ? ' stderr: ' + stderr.substring(0, 200) : ''}`)
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 === 'step_start') {
console.log(`[AI] 思考中...`)
} else if (event.type === 'text' && event.part?.text) {
aiText += event.part.text
} else if (event.type === 'step_finish') {
const tokens = event.part?.tokens
if (tokens) console.log(`[AI] tokens: in=${tokens.input} out=${tokens.output} cost=$${tokens.cost?.toFixed(6)}`)
}
} catch { continue }
}
console.log(`[AI] 响应 | sessions=${resolvedSessionId || ''} | ${aiText.length} chars | ${aiText.substring(0, 300)}${aiText.length > 300 ? '...' : ''}`)
if (mode === 'json') {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ result: aiText || 'done', 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'),
},
},
},
})