import { defineConfig } 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 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"; // [已移除] dev 期自己签 NLS token 的中间件(`/nls-token/`)。 // 2026-09-24 起 token 改由**后端网关**提供:`GET http://121.43.55.7:10011/proxy/verify/getALYToken` // → `{code:200, content:""}`,前端走同源 `/api/` 代理拿(2026-09-29 前叫 `/aly-api/`)。 // 于是前端侧**不再需要 AccessKeySecret**,vite 也不必持凭据。 // AK/SK 自签的能力仍留在 harness/tools/nls-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(() => { return { plugins: [ vue(), Components({ resolvers: [AntDesignVueResolver()], }), svgLoader(), basicSsl(), ], server: { proxy: { // ── 代理总原则(2026-09-29 用户指定)──────────────────────────────── // 前缀**尽量与服务真实路径一致**,只有服务端本身不带前缀时才剥: // /oauth/** → 121.43.55.7:10086,**直通**(真实路径就带 /oauth) // /dms/** → 121.43.55.7:10081,**直通**(真实路径就带 /dms) // /api/** → 121.43.55.7:10011,**剥掉 /api**(网关真实路径是 /proxy/verify/…, // 实测直连 /api/proxy/… 是 404) // /chat-api/** → 聊天后端,剥前缀(后端真实路径是 /api/chat,与前端前缀不同名) // 三台后端都只有 HTTP,而 dev server 是 HTTPS ⇒ 直连会被浏览器按混合内容拦掉, // 必须由服务端这一跳(dev 是 vite 代理,生产是 nginx)代劳。 // 2026-09-29 之前是 /dms-api(补 /dms) + /ip-api + /aly-api(两条打同一台) —— 现在合并。 // // ⚠️ **target 直接写在这里,不从环境变量读**(用户 2026-09-29 明确要求 // 「vite.config.ts 里面不要加环境变量」)—— 所以本文件不再 `loadEnv`。 // 换地址就改这四处;`.env.*` 只管**产物里**用到的值(VITE_DMS_API 等同源前缀)。 // 后端网关 121.43.55.7:10011:语音识别 token(/proxy/verify/getALYToken) // 与取本机 IP(/proxy/verify/getIPByBrowser)**在同一台**,所以共用这一条。 "/api/": { target: "http://121.43.55.7:10011", changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ""), }, // DMS 登录服务 121.43.55.7:10086(真实路径带 /oauth,所以直通、不改写)。 // 前端目前不直接调它(token 是 node 侧换的);留着是为了本地能自测登录接口。 "/oauth/": { target: "http://121.43.55.7:10086", changeOrigin: true, }, // DMS 数据管理服务(会话/问答记录/反馈)——真实路由都在 /dms/ 下,前缀同名,直通。 // token(DMS 的鉴权头名就叫 token,不是 Authorization): // ① **前端自己带**:未登录访客用服务账号现换一个(`src/network/api/dms/service-token.ts`, // 2026-09-29 用户要求改成这套)—— 这种请求**代理不覆盖**它的 token; // ② 请求没带时才由代理兜底注入:读仓库根 `.dms-token`(gitignored)或进程环境变量 // DMS_TOKEN(`readDmsToken()` 自己会读,见 harness/tools/dms-token.mjs) // —— 命令行走代理排查(curl / verify-dms-*.mjs)靠的就是这条。 // 现读而非静态 headers:换 token 不用重启 dev server。 "/dms/": { target: "http://121.43.55.7:10081", changeOrigin: true, configure: (proxy) => { proxy.on("proxyReq", (proxyReq) => { // 前端已经带了 token 就别覆盖(否则前端的刷新逻辑白做) const fromClient = typeof proxyReq.getHeader === "function" ? proxyReq.getHeader("token") : undefined; if (fromClient) return; const token = readDmsToken(); if (token) { proxyReq.setHeader("token", token); } warnDmsTokenIfNeeded(token); }); }, }, // 新版聊天后端(POST /api/chat,SSE):后端真实路径是 /api/chat,与前端前缀不同名 ⇒ 剥前缀。 // 对应 .env.development 中 VITE_CHAT_API="/chat-api"。 "/chat-api/": { target: "http://192.168.2.23:8000", changeOrigin: true, rewrite: (path) => path.replace(/^\/chat-api/, ""), }, // [已移除] "/stream/" 代理(数字人视频流):src/index.html 里没有任何代码往它发请求, // 相关 env(VITE_STREAM_SERVER / VITE_STREAM_TOKEN / VITE_HUMAN_IDLE_MP4_URL)也已删除。 // [已移除] "/asr/" 代理(https://human-screen-v3.metamaker.cn):旧厂商 ASR 的退路, // 2026-09-24 按用户要求把旧实现连同 env(VITE_ASR / VITE_XF_ASR)一起删除; // 现在的语音识别走阿里云 NLS(见 harness/docs/reference/asr-nls.md)。 }, }, 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", }, }, }, }; });