/** * CLI Web Vite 构建配置 * * 用于构建 CLI Web 的 SPA 前端(不含 Electron 依赖)。 * 输出到 dist-cli-web/,由 clb web 托管。 * * 与 Electron renderer 构建的关键区别: * - __IS_ELECTRON__ = false(使用 FetchAdapter 而非 window.chatApi) * - 不包含 apps/desktop/preload 和 apps/desktop/main * - 输出目录独立(dist-cli-web/ vs out/renderer/) */ import { resolve } from 'path' import { readFileSync } from 'fs' import { spawn, type ChildProcess } from 'child_process' import * as net from 'net' import { defineConfig, type Plugin } from 'vite' import vue from '@vitejs/plugin-vue' import ui from '@nuxt/ui/vite' import { DEFAULT_API_PORT } from './packages/config/src/schema' import { createChatlabStartCommand, terminateChatlabStartProcess } from './scripts/dev-server-command.mjs' import { chatlabIconBundle } from './vite.icon-bundle.config' const BACKEND_PORT = DEFAULT_API_PORT function isPortInUse(port: number): Promise { return new Promise((resolve, reject) => { const server = net.createServer() server.once('error', (error: NodeJS.ErrnoException) => { if (error.code === 'EADDRINUSE') { resolve(true) return } reject(error) }) server.once('listening', () => { server.close() resolve(false) }) server.listen(port, '127.0.0.1') }) } async function isChatlabBackendResponsive(port: number, timeoutMs = 800): Promise { try { const responses = await Promise.all( ['/_web/sessions', '/_web/ai/global-chats'].map((path) => fetch(`http://127.0.0.1:${port}${path}`, { signal: AbortSignal.timeout(timeoutMs), }) ) ) return responses.every((response) => [200, 401, 403].includes(response.status)) } catch { return false } } /** * 自动启动 clb web 后端的插件 * 仅在 CHATLAB_AUTO_SERVE=1 时生效(由 dev:cli-web 脚本设置) */ function chatlabServePlugin(): Plugin { let serverProcess: ChildProcess | null = null let processCleanupRegistered = false function unregisterProcessCleanup() { if (!processCleanupRegistered) return process.off('exit', stopServerProcess) process.off('SIGINT', handleSigint) process.off('SIGTERM', handleSigterm) processCleanupRegistered = false } function stopServerProcess() { if (!serverProcess) return terminateChatlabStartProcess(serverProcess) serverProcess = null unregisterProcessCleanup() } function handleSigint() { stopServerProcess() process.exit(130) } function handleSigterm() { stopServerProcess() process.exit(143) } function registerProcessCleanup() { if (processCleanupRegistered) return process.once('exit', stopServerProcess) process.once('SIGINT', handleSigint) process.once('SIGTERM', handleSigterm) processCleanupRegistered = true } return { name: 'chatlab-start', async configureServer(server) { if (process.env.CHATLAB_AUTO_SERVE !== '1') return const inUse = await isPortInUse(BACKEND_PORT) if (inUse) { const responsive = await isChatlabBackendResponsive(BACKEND_PORT) if (responsive) { console.log(`[clb web] Port ${BACKEND_PORT} already has a responsive ChatLab API, skipping`) return } throw new Error( `[clb web] Port ${BACKEND_PORT} is in use, but the ChatLab API is stale or incomplete. Stop the stale process and restart dev:cli-web.` ) } const serverDir = resolve(__dirname, 'apps/cli') const startCommand = createChatlabStartCommand({ serverDir, backendPort: BACKEND_PORT, }) serverProcess = spawn(startCommand.command, startCommand.args, startCommand.options) serverProcess.stdout?.on('data', (data: Buffer) => { const line = data.toString().trim() if (line) console.log(`[clb web] ${line}`) }) serverProcess.stderr?.on('data', (data: Buffer) => { const line = data.toString().trim() if (line) console.error(`[clb web] ${line}`) }) serverProcess.on('exit', (code) => { if (code !== null && code !== 0) { console.error(`[clb web] exited with code ${code}`) } serverProcess = null unregisterProcessCleanup() }) registerProcessCleanup() }, // Vite closes and rebuilds its HTTP server during config hot reloads. // Cleanup stays bound to the Vite process lifecycle so a reload cannot stop the CLI backend. } } export default defineConfig({ root: 'src/', base: '/', resolve: { alias: { '@': resolve(__dirname, 'src/'), '~': resolve(__dirname, 'src/'), '@openchatlab': resolve(__dirname, 'packages'), '@electron/shared': resolve(__dirname, 'apps/desktop/shared'), '@electron/preload': resolve(__dirname, 'apps/desktop/preload'), }, }, define: { __IS_ELECTRON__: JSON.stringify(false), __IS_WEB_WASM__: JSON.stringify(false), __APP_VERSION__: JSON.stringify(JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8')).version), }, plugins: [ vue(), ui({ ui: { colors: { primary: 'pink', neutral: 'zinc', }, }, }), chatlabIconBundle(__dirname), chatlabServePlugin(), ], build: { outDir: resolve(__dirname, 'dist-cli-web'), emptyOutDir: true, sourcemap: false, rollupOptions: { input: { index: resolve(__dirname, 'src/index.html'), }, output: { manualChunks(id) { if (id.includes('node_modules/echarts-wordcloud')) return 'vendor-echarts-wordcloud' if (id.includes('node_modules/zrender')) return 'vendor-zrender' if (id.includes('node_modules/echarts')) return 'vendor-echarts' if (id.includes('node_modules/@zumer/snapdom')) return 'vendor-snapdom' return undefined }, }, }, }, server: { host: '127.0.0.1', port: 3100, proxy: { '/_web': `http://127.0.0.1:${BACKEND_PORT}`, '/api': `http://127.0.0.1:${BACKEND_PORT}`, '/_proxy/chatlab.fun': { target: 'https://chatlab.fun', changeOrigin: true, rewrite: (p: string) => p.replace(/^\/_proxy\/chatlab\.fun/, ''), }, }, }, })