Files
gavindiazandClaude Code 31ca07ea7a feat: ebook-to-mindmap desktop app with AI summary, book chat and mind-map export
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>
2026-09-25 23:00:24 +08:00

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,
}
: {}),
},
})