import { defineConfig, loadEnv } from "vite"; // import { darkTheme } from "./src/theme.js"; import vue from "@vitejs/plugin-vue"; import Components from "unplugin-vue-components/vite"; import { AntDesignVueResolver } from "unplugin-vue-components/resolvers"; import { visualizer } from "rollup-plugin-visualizer"; import svgLoader from "vite-svg-loader"; import path from "path"; import basicSsl from "@vitejs/plugin-basic-ssl"; // DMS token 的读取口径收在 harness/tools/dms-token.mjs(与本仓库的 DMS 脚本共用一份): // **显式参数 → 环境变量 → 本机文件 `.dms-token`**。 // 关键是「每个请求现读」而不是启动时读一次 —— token 约 24 小时过期,以前换一次就要重启 dev server。 import { DMS_TOKEN_FILE, readDmsToken, parseJwtPayload, describeDmsToken } from "./harness/tools/dms-token.mjs"; /** 同一个状态只提醒一次,避免每个请求都刷屏 */ let dmsTokenWarnedFor = ""; /** * token 有问题时给出**明确**提示 —— 以前 token 过期是**静默失败**(DMS 写入不生效、 * 只有一条埋在别处控制台里的 warn),排查「为什么数据没存上」要花很久。 */ const warnDmsTokenIfNeeded = (token: string): void => { if (!token) { if (dmsTokenWarnedFor === "missing") return; dmsTokenWarnedFor = "missing"; console.warn( `\n[dms] ⚠️ 没有 DMS token,DMS 读写(会话/问答记录/反馈)会失败,聊天本身不受影响。\n` + `[dms] 最省事:在浏览器里复制 token,然后跑 \`npm run dms:token\`(从剪贴板读)。\n` + `[dms] 文件位置:${DMS_TOKEN_FILE}\n` ); return; } const exp = Number(parseJwtPayload(token)?.exp); if (!Number.isFinite(exp)) return; const leftMs = exp * 1000 - Date.now(); const at = new Date(exp * 1000).toLocaleString("zh-CN"); if (leftMs <= 0) { if (dmsTokenWarnedFor === `expired:${exp}`) return; dmsTokenWarnedFor = `expired:${exp}`; console.warn( `\n[dms] ⚠️ DMS token 已于 ${at} 过期(DMS 读写会失败)。复制新 token 后跑 \`npm run dms:token\`。\n` ); return; } if (leftMs < 2 * 3600 * 1000) { const key = `soon:${exp}`; if (dmsTokenWarnedFor === key) return; dmsTokenWarnedFor = key; console.warn(`\n[dms] ⏳ DMS token 将于 ${at} 过期(还剩 ${(leftMs / 3600000).toFixed(1)} 小时)。\n`); } }; // https://vitejs.dev/config/ export default defineConfig(({ mode }) => { // 第三个参数传空串:不加前缀过滤,才能读到 DMS_TOKEN 这类非 VITE_ 前缀的变量 // (DMS_TOKEN 只写在 .env.development.local,该文件按 .gitignore 不入库) const env = loadEnv(mode, process.cwd(), ""); return { plugins: [ vue(), Components({ resolvers: [AntDesignVueResolver()], }), visualizer({ open: false, gzipSize: true, file: "./dist/stats.html", brotliSize: true, }), svgLoader(), basicSsl(), ], server: { proxy: { //配置本地代理 "/api/": { //匹配的路径 target: "http://aixq.shqp.gov.cn", //目标url changeOrigin: true, //跨域 rewrite: (path) => path.replace(/^\/api/, ""), //重写路径 }, // 新版聊天后端代理(POST /api/chat,SSE) // 开发服务器是 HTTPS 而后端是 HTTP,浏览器会拦截直连(混合内容), // 且后端未实现 CORS,因此开发环境统一走同源代理。 // 对应 .env.development 中 VITE_CHAT_API="/chat-api" // target 取 VITE_CHAT_TARGET(地址收在 env 里,不再写死在配置文件中) "/chat-api/": { target: env.VITE_CHAT_TARGET || "http://192.168.2.23:8000", changeOrigin: true, rewrite: (path) => path.replace(/^\/chat-api/, ""), }, // DMS 数据管理服务(会话/问答记录/反馈) // 两件事都在代理侧做,浏览器拿不到: // ① 补回 /dms 前缀(DMS 的真实路由都在 /dms/ 下) // ② 注入 token 头(DMS 的鉴权头名就叫 token,不是 Authorization) // token 来源:.env.development.local 的 DMS_TOKEN(不入库) "/dms-api/": { target: env.VITE_DMS_TARGET || "http://121.43.55.7:10081", changeOrigin: true, rewrite: (path) => path.replace(/^\/dms-api/, "/dms"), // token 在**每个请求**现读(见 readDmsToken 的说明):换 token 不用重启 dev server。 // 用 configure 而不是静态 headers —— 后者的值在启动时就定死了。 configure: (proxy) => { proxy.on("proxyReq", (proxyReq) => { // 现读:换 token 后不需要重启 dev server const token = readDmsToken(env.DMS_TOKEN); if (token) { // DMS 的鉴权头名就叫 token(不是 Authorization) proxyReq.setHeader("token", token); } warnDmsTokenIfNeeded(token); }); }, }, "/asr/": { //匹配的路径 target: "https://human-screen-v3.metamaker.cn", //目标url changeOrigin: true, //跨域 rewrite: (path) => path.replace(/^\/asr/, ""), //重写路径 ws: true, }, // [已移除] "/stream/" 代理(数字人视频流):src/index.html 里没有任何代码往它发请求, // 相关 env(VITE_STREAM_SERVER / VITE_STREAM_TOKEN / VITE_HUMAN_IDLE_MP4_URL)也已删除。 // 注意 "/asr/" 代理**保留** —— 它是切到讯飞 ASR 分支(VITE_XF_ASR="/asr")的退路。 }, }, build: { rollupOptions: { input: { main: path.resolve(__dirname, "index.html"), redirect: path.resolve(__dirname, "redirect.html"), }, output: { manualChunks: (id) => { console.log(id); if (id.includes("node_modules/three")) { return "three"; } else if (id.includes("node_modules")) { return "vendor"; } }, }, }, assetsInlineLimit: 20480, }, resolve: { alias: { "@": path.resolve("./src/"), three$: path.resolve(__dirname, "./node_modules/three/build/three.cjs"), }, }, base: "./", css: { preprocessorOptions: { scss: { api: "modern-compiler", }, }, }, }; });