Tauri v2 + React 19 desktop app. AI summary with pause/resume and checkpoint resume, book-chat with RAG and chat sessions, unified data persistence, mind-map download/export, and a Tauri build pipeline. Co-Authored-By: Claude Code <noreply@anthropic.com>
176 lines
5.5 KiB
TypeScript
176 lines
5.5 KiB
TypeScript
import { defineConfig, type Plugin } from 'vite'
|
|
import path from 'path'
|
|
import react from '@vitejs/plugin-react'
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
import wasm from 'vite-plugin-wasm'
|
|
import topLevelAwait from 'vite-plugin-top-level-await'
|
|
import http from 'http'
|
|
import https from 'https'
|
|
|
|
const host = process.env.TAURI_DEV_HOST
|
|
|
|
/**
|
|
* Vite 插件:为 AI 请求提供 CORS 代理
|
|
* 拦截 /api/ai/* 请求,根据 X-AI-Target-Url 头动态转发到目标 AI 服务
|
|
*/
|
|
function aiCorsProxy(): Plugin {
|
|
return {
|
|
name: 'ai-cors-proxy',
|
|
configureServer(server) {
|
|
server.middlewares.use('/api/ai', async (req, res) => {
|
|
const targetUrl = req.headers['x-ai-target-url'] as string
|
|
if (!targetUrl) {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
res.end(JSON.stringify({ error: 'Missing X-AI-Target-Url header' }))
|
|
return
|
|
}
|
|
|
|
console.log(`[AI Proxy] ${req.method} ${targetUrl}`)
|
|
|
|
try {
|
|
const url = new URL(targetUrl)
|
|
const isHttps = url.protocol === 'https:'
|
|
|
|
// 读取请求体
|
|
const chunks: Buffer[] = []
|
|
for await (const chunk of req) {
|
|
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
|
|
}
|
|
const bodyBuffer = Buffer.concat(chunks)
|
|
|
|
// 构建转发请求头,去除代理专用头
|
|
const forwardHeaders: Record<string, string | string[] | undefined> = {}
|
|
for (const [key, val] of Object.entries(req.headers)) {
|
|
if (
|
|
key.toLowerCase() === 'host' ||
|
|
key.toLowerCase() === 'x-ai-target-url' ||
|
|
key.toLowerCase() === 'origin' ||
|
|
key.toLowerCase() === 'referer'
|
|
) {
|
|
continue
|
|
}
|
|
forwardHeaders[key] = val
|
|
}
|
|
forwardHeaders['host'] = url.host
|
|
forwardHeaders['origin'] = url.origin
|
|
forwardHeaders['referer'] = url.origin
|
|
|
|
const proxyRes = await new Promise<http.IncomingMessage>(
|
|
(resolve, reject) => {
|
|
const lib = isHttps ? https : http
|
|
const proxyReq = lib.request(
|
|
{
|
|
hostname: url.hostname,
|
|
port: url.port || (isHttps ? 443 : 80),
|
|
path: url.pathname + url.search,
|
|
method: req.method,
|
|
headers: forwardHeaders,
|
|
rejectUnauthorized: false, // 允许自签名证书(开发环境)
|
|
},
|
|
resolve
|
|
)
|
|
|
|
proxyReq.on('error', (err: NodeJS.ErrnoException) => {
|
|
console.error('[AI Proxy] Request error:', err.code, err.message)
|
|
reject(err)
|
|
})
|
|
|
|
proxyReq.setTimeout(120000, () => {
|
|
proxyReq.destroy()
|
|
reject(new Error('Proxy timeout after 120s'))
|
|
})
|
|
|
|
proxyReq.write(bodyBuffer)
|
|
proxyReq.end()
|
|
}
|
|
)
|
|
|
|
console.log(`[AI Proxy] Response: ${proxyRes.statusCode} from ${targetUrl}`)
|
|
|
|
// 转发响应头
|
|
const responseHeaders: Record<string, string | string[] | undefined> = {}
|
|
for (const [key, val] of Object.entries(proxyRes.headers)) {
|
|
responseHeaders[key] = val
|
|
}
|
|
res.writeHead(proxyRes.statusCode || 500, responseHeaders)
|
|
|
|
// 流式转发响应体
|
|
proxyRes.pipe(res)
|
|
|
|
proxyRes.on('error', (err) => {
|
|
console.error('[AI Proxy] Response stream error:', err.message)
|
|
if (!res.writableEnded) {
|
|
res.end()
|
|
}
|
|
})
|
|
} catch (error) {
|
|
const errMsg = error instanceof Error ? error.message : String(error)
|
|
console.error('[AI Proxy] Error:', errMsg)
|
|
if (!res.headersSent) {
|
|
res.writeHead(502, { 'Content-Type': 'application/json' })
|
|
}
|
|
if (!res.writableEnded) {
|
|
res.end(JSON.stringify({ error: 'Proxy error', message: errMsg }))
|
|
}
|
|
}
|
|
})
|
|
},
|
|
}
|
|
}
|
|
|
|
// https://vite.dev/config/
|
|
export default defineConfig({
|
|
// Prevent Vite from clearing the terminal so Rust errors stay visible (Tauri)
|
|
clearScreen: false,
|
|
plugins: [
|
|
react({
|
|
babel: {
|
|
plugins: ['babel-plugin-react-compiler'],
|
|
},
|
|
}),
|
|
tailwindcss(),
|
|
wasm(),
|
|
topLevelAwait(),
|
|
aiCorsProxy(),
|
|
],
|
|
resolve: {
|
|
alias: {
|
|
'@': path.resolve(__dirname, './src'),
|
|
},
|
|
},
|
|
// Env variables starting with these prefixes are exposed to the frontend
|
|
envPrefix: ['VITE_', 'TAURI_ENV_*'],
|
|
server: {
|
|
// Tauri expects a fixed port; fail if unavailable
|
|
port: 5173,
|
|
strictPort: true,
|
|
host: host || false,
|
|
hmr: host
|
|
? {
|
|
protocol: 'ws',
|
|
host,
|
|
port: 1421,
|
|
}
|
|
: undefined,
|
|
watch: {
|
|
// Ignore Rust project to avoid unnecessary reloads
|
|
ignored: ['**/src-tauri/**'],
|
|
},
|
|
},
|
|
build: {
|
|
// Only apply Tauri-specific targets when building via `tauri build`
|
|
...(process.env.TAURI_ENV_PLATFORM
|
|
? {
|
|
// Tauri uses Chromium on Windows and WebKit on macOS/Linux
|
|
target:
|
|
process.env.TAURI_ENV_PLATFORM === 'windows'
|
|
? 'chrome105'
|
|
: 'safari13',
|
|
// Don't minify for debug builds
|
|
minify: !process.env.TAURI_ENV_DEBUG ? 'esbuild' : false,
|
|
// Produce sourcemaps for debug builds
|
|
sourcemap: !!process.env.TAURI_ENV_DEBUG,
|
|
}
|
|
: {}),
|
|
},
|
|
}) |