// import { jsxLocPlugin } from "@builder.io/vite-plugin-jsx-loc"; // 禁用:与 R3F 冲突 import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; import fs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { defineConfig, type Plugin, type ViteDevServer } from "vite"; // ============================================================================= // Manus Debug Collector - Vite Plugin // Writes browser logs directly to files, trimmed when exceeding size limit // ============================================================================= const PROJECT_ROOT = path.dirname(fileURLToPath(import.meta.url)); /** 从仓库根解析依赖(Vite root 在 client/ 时,部分环境下需显式指向根目录 node_modules) */ const requireFromRoot = createRequire(import.meta.url); const LOG_DIR = path.join(PROJECT_ROOT, ".manus-logs"); const MAX_LOG_SIZE_BYTES = 1 * 1024 * 1024; // 1MB per log file const TRIM_TARGET_BYTES = Math.floor(MAX_LOG_SIZE_BYTES * 0.6); // Trim to 60% to avoid constant re-trimming type LogSource = "browserConsole" | "networkRequests" | "sessionReplay"; function ensureLogDir() { if (!fs.existsSync(LOG_DIR)) { fs.mkdirSync(LOG_DIR, { recursive: true }); } } function trimLogFile(logPath: string, maxSize: number) { try { if (!fs.existsSync(logPath) || fs.statSync(logPath).size <= maxSize) { return; } const lines = fs.readFileSync(logPath, "utf-8").split("\n"); const keptLines: string[] = []; let keptBytes = 0; // Keep newest lines (from end) that fit within 60% of maxSize const targetSize = TRIM_TARGET_BYTES; for (let i = lines.length - 1; i >= 0; i--) { const lineBytes = Buffer.byteLength(`${lines[i]}\n`, "utf-8"); if (keptBytes + lineBytes > targetSize) break; keptLines.unshift(lines[i]); keptBytes += lineBytes; } fs.writeFileSync(logPath, keptLines.join("\n"), "utf-8"); } catch { /* ignore trim errors */ } } function writeToLogFile(source: LogSource, entries: unknown[]) { if (entries.length === 0) return; ensureLogDir(); const logPath = path.join(LOG_DIR, `${source}.log`); // Format entries with timestamps const lines = entries.map((entry) => { const ts = new Date().toISOString(); return `[${ts}] ${JSON.stringify(entry)}`; }); // Append to log file fs.appendFileSync(logPath, `${lines.join("\n")}\n`, "utf-8"); // Trim if exceeds max size trimLogFile(logPath, MAX_LOG_SIZE_BYTES); } /** * Vite plugin to collect browser debug logs * - POST /__manus__/logs: Browser sends logs, written directly to files * - Files: browserConsole.log, networkRequests.log, sessionReplay.log * - Auto-trimmed when exceeding 1MB (keeps newest entries) */ function vitePluginManusDebugCollector(): Plugin { return { name: "manus-debug-collector", transformIndexHtml(html) { if (process.env.NODE_ENV === "production") { return html; } return { html, tags: [ { tag: "script", attrs: { src: "/__manus__/debug-collector.js", defer: true, }, injectTo: "head", }, ], }; }, configureServer(server: ViteDevServer) { // POST /__manus__/logs: Browser sends logs (written directly to files) server.middlewares.use("/__manus__/logs", (req, res, next) => { if (req.method !== "POST") { return next(); } const handlePayload = (payload: any) => { // Write logs directly to files if (payload.consoleLogs?.length > 0) { writeToLogFile("browserConsole", payload.consoleLogs); } if (payload.networkRequests?.length > 0) { writeToLogFile("networkRequests", payload.networkRequests); } if (payload.sessionEvents?.length > 0) { writeToLogFile("sessionReplay", payload.sessionEvents); } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: true })); }; const reqBody = (req as { body?: unknown }).body; if (reqBody && typeof reqBody === "object") { try { handlePayload(reqBody); } catch (e) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: String(e) })); } return; } let body = ""; req.on("data", (chunk) => { body += chunk.toString(); }); req.on("end", () => { try { const payload = JSON.parse(body); handlePayload(payload); } catch (e) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: String(e) })); } }); }); }, }; } /** * 自定义插件:禁用完整页面刷新,保留 HMR * 当模块无法热更新时,不会自动刷新页面 */ function vitePluginNoFullReload(): Plugin { return { name: 'no-full-reload', // 修改 HMR 客户端代码,阻止自动刷新 transformIndexHtml(html) { if (process.env.NODE_ENV === 'production') { return html; } return { html, tags: [ { tag: 'script', children: ` // 拦截并阻止 Vite HMR 的完整页面刷新 (function() { const originalReload = window.location.reload; let reloadBlocked = false; window.location.reload = function() { if (!reloadBlocked) { console.warn('[HMR] Page reload blocked. Manual refresh required if needed.'); reloadBlocked = true; setTimeout(() => { reloadBlocked = false; }, 1000); } // 不执行实际刷新 }; })(); `, injectTo: 'head', }, ], }; }, }; } // 注意:jsxLocPlugin 与 React Three Fiber 不兼容,已禁用 const plugins = [ react(), tailwindcss(), // jsxLocPlugin(), // 禁用:与 R3F 冲突,会导致 "Cannot set data-loc" 错误 vitePluginManusDebugCollector(), vitePluginNoFullReload(), // 禁用自动刷新 ]; export default defineConfig({ plugins, resolve: { alias: { "@": path.resolve(PROJECT_ROOT, "client", "src"), "@shared": path.resolve(PROJECT_ROOT, "shared"), "@assets": path.resolve(PROJECT_ROOT, "attached_assets"), delaunator: requireFromRoot.resolve("delaunator"), "robust-predicates": requireFromRoot.resolve("robust-predicates"), }, }, envDir: path.resolve(PROJECT_ROOT), root: path.resolve(PROJECT_ROOT, "client"), publicDir: path.resolve(PROJECT_ROOT, "client", "public"), build: { outDir: path.resolve(PROJECT_ROOT, "dist/public"), emptyOutDir: true, }, server: { host: true, allowedHosts: [ ".manuspre.computer", ".manus.computer", ".manus-asia.computer", ".manuscomputer.ai", ".manusvm.computer", "localhost", "127.0.0.1", "npsx.i-llusion.cn", ], fs: { strict: true, deny: ["**/.*"], allow: [PROJECT_ROOT, path.join(PROJECT_ROOT, "node_modules")], }, hmr: false, watch: null, }, });