Files
sub2api/frontend/vite.config.ts
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

177 lines
4.9 KiB
TypeScript

import { defineConfig, loadEnv, Plugin } from 'vite'
import vue from '@vitejs/plugin-vue'
import checker from 'vite-plugin-checker'
import { resolve } from 'path'
function escapeHtml(value: string): string {
return value.replace(/[&<>"']/g, (character) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
})[character] || character)
}
function isSafeImageUrl(value: string): boolean {
const trimmed = value.trim()
if ((trimmed.startsWith('/') && !trimmed.startsWith('//')) || /^data:image\//i.test(trimmed)) {
return true
}
try {
const parsed = new URL(trimmed)
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
} catch {
return false
}
}
function injectBranding(html: string, config: { site_name?: string; site_logo?: string }): string {
let brandedHtml = html
const siteName = config.site_name?.trim()
if (siteName) {
brandedHtml = brandedHtml.replace(
/<title>[^<]*<\/title>/i,
`<title>${escapeHtml(siteName)} - AI API Gateway</title>`,
)
}
const siteLogo = config.site_logo?.trim()
if (siteLogo && isSafeImageUrl(siteLogo)) {
brandedHtml = brandedHtml.replace(
/<link\s+rel=["']icon["'][^>]*>/i,
`<link rel="icon" href="${escapeHtml(siteLogo)}" />`,
)
}
return brandedHtml
}
/**
* Vite 插件:开发模式下注入公开配置到 index.html
* 与生产模式的后端注入行为保持一致,消除闪烁
*/
function injectPublicSettings(backendUrl: string): Plugin {
return {
name: 'inject-public-settings',
apply: 'serve',
transformIndexHtml: {
order: 'pre',
async handler(html) {
try {
const response = await fetch(`${backendUrl}/api/v1/settings/public`, {
signal: AbortSignal.timeout(2000)
})
if (response.ok) {
const data = await response.json()
if (data.code === 0 && data.data) {
const script = `<script>window.__APP_CONFIG__=${JSON.stringify(data.data)};</script>`
return injectBranding(html, data.data).replace('</head>', `${script}\n</head>`)
}
}
} catch (e) {
console.warn('[vite] 无法获取公开配置,将回退到 API 调用:', (e as Error).message)
}
return html
}
}
}
}
export default defineConfig(({ mode }) => {
// 加载环境变量
const env = loadEnv(mode, process.cwd(), '')
const backendUrl = env.VITE_DEV_PROXY_TARGET || 'http://localhost:8080'
const devPort = Number(env.VITE_DEV_PORT || 3000)
return {
plugins: [
vue(),
checker({
vueTsc: true
}),
injectPublicSettings(backendUrl)
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
// 使用 vue-i18n 运行时版本,避免 CSP unsafe-eval 问题
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
}
},
define: {
// 启用 vue-i18n JIT 编译,在 CSP 环境下处理消息插值
// JIT 编译器生成 AST 对象而非 JS 代码,无需 unsafe-eval
__INTLIFY_JIT_COMPILATION__: true
},
build: {
outDir: '../backend/internal/web/dist',
emptyOutDir: true,
rollupOptions: {
output: {
/**
* 手动分包配置
* 分离第三方库并按功能合并应用代码,避免循环依赖
*/
manualChunks(id: string) {
if (id.includes('node_modules')) {
// Vue 核心库
if (
id.includes('/vue/') ||
id.includes('/vue-router/') ||
id.includes('/pinia/') ||
id.includes('/@vue/')
) {
return 'vendor-vue'
}
// UI 工具库(较大,单独分离)
if (id.includes('/@vueuse/') || id.includes('/xlsx/')) {
return 'vendor-ui'
}
// 图表库
if (id.includes('/chart.js/') || id.includes('/vue-chartjs/')) {
return 'vendor-chart'
}
// 国际化
if (id.includes('/vue-i18n/') || id.includes('/@intlify/')) {
return 'vendor-i18n'
}
// Stripe 仅在支付流程中按需加载,避免进入首页公共依赖。
if (id.includes('/@stripe/stripe-js/')) {
return 'vendor-stripe'
}
// 其他小型第三方库合并
return 'vendor-misc'
}
// 应用代码:按入口点自动分包,不手动干预
// 这样可以避免循环依赖,同时保持合理的 chunk 数量
}
}
}
},
server: {
host: '0.0.0.0',
port: devPort,
proxy: {
'/api': {
target: backendUrl,
changeOrigin: true
},
'/v1': {
target: backendUrl,
changeOrigin: true
},
'/setup': {
target: backendUrl,
changeOrigin: true
}
}
}
}
})