2026-03-31 18:52:37 +08:00
|
|
|
import { jsxLocPlugin } from "@builder.io/vite-plugin-jsx-loc";
|
|
|
|
|
import tailwindcss from "@tailwindcss/vite";
|
|
|
|
|
import react from "@vitejs/plugin-react";
|
|
|
|
|
import fs from "node:fs";
|
|
|
|
|
import path from "node:path";
|
|
|
|
|
import { defineConfig, type Plugin, type ViteDevServer } from "vite";
|
|
|
|
|
import { vitePluginManusRuntime } from "vite-plugin-manus-runtime";
|
|
|
|
|
|
|
|
|
|
// =============================================================================
|
|
|
|
|
// Manus Debug Collector - Vite Plugin
|
|
|
|
|
// Writes browser logs directly to files, trimmed when exceeding size limit
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
|
|
|
|
const PROJECT_ROOT = import.meta.dirname;
|
|
|
|
|
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) }));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 15:54:11 +08:00
|
|
|
/** 生产构建时剔除 public 中的 __manus__(仅开发调试使用) */
|
|
|
|
|
function vitePluginExcludeManusFromDist(): Plugin {
|
|
|
|
|
const distDir = path.resolve(import.meta.dirname, "dist");
|
|
|
|
|
return {
|
|
|
|
|
name: "exclude-manus-from-dist",
|
|
|
|
|
closeBundle() {
|
|
|
|
|
if (process.env.NODE_ENV !== "production") return;
|
|
|
|
|
const manusDir = path.join(distDir, "__manus__");
|
|
|
|
|
if (fs.existsSync(manusDir)) {
|
|
|
|
|
fs.rmSync(manusDir, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const plugins = [
|
|
|
|
|
react(),
|
|
|
|
|
tailwindcss(),
|
|
|
|
|
jsxLocPlugin(),
|
|
|
|
|
vitePluginManusRuntime(),
|
2026-05-28 16:26:38 +08:00
|
|
|
// vitePluginManusDebugCollector(), // 已禁用:不再注入 debug-collector.js,消除浏览器网络面板中定时出现的 __manus__/logs 请求
|
2026-05-22 15:54:11 +08:00
|
|
|
vitePluginExcludeManusFromDist(),
|
|
|
|
|
];
|
2026-03-31 18:52:37 +08:00
|
|
|
|
|
|
|
|
export default defineConfig({
|
|
|
|
|
plugins,
|
2026-05-26 19:16:42 +08:00
|
|
|
base: process.env.NODE_ENV === "production" ? "/x9/v2/" : "/",
|
2026-03-31 18:52:37 +08:00
|
|
|
resolve: {
|
|
|
|
|
alias: {
|
|
|
|
|
"@": path.resolve(import.meta.dirname, "client", "src"),
|
|
|
|
|
"@shared": path.resolve(import.meta.dirname, "shared"),
|
|
|
|
|
"@assets": path.resolve(import.meta.dirname, "attached_assets"),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
envDir: path.resolve(import.meta.dirname),
|
|
|
|
|
root: path.resolve(import.meta.dirname, "client"),
|
2026-04-09 17:00:44 +08:00
|
|
|
publicDir: path.resolve(import.meta.dirname, "client/public"),
|
|
|
|
|
// 性能优化
|
2026-03-31 18:52:37 +08:00
|
|
|
build: {
|
2026-04-09 17:00:44 +08:00
|
|
|
target: 'esnext',
|
|
|
|
|
cssTarget: 'esnext',
|
2026-05-22 15:54:11 +08:00
|
|
|
outDir: path.resolve(import.meta.dirname, "dist"),
|
2026-03-31 18:52:37 +08:00
|
|
|
emptyOutDir: true,
|
2026-04-09 17:00:44 +08:00
|
|
|
minify: 'terser',
|
|
|
|
|
terserOptions: {
|
|
|
|
|
compress: {
|
|
|
|
|
drop_console: true,
|
|
|
|
|
drop_debugger: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
rollupOptions: {
|
2026-04-21 15:29:29 +08:00
|
|
|
input: {
|
|
|
|
|
i: path.resolve(import.meta.dirname, "client", "i.html"),
|
|
|
|
|
},
|
2026-04-09 17:00:44 +08:00
|
|
|
output: {
|
2026-04-17 17:57:39 +08:00
|
|
|
manualChunks(id) {
|
|
|
|
|
if (!id.includes("node_modules")) return;
|
|
|
|
|
|
2026-05-26 19:16:42 +08:00
|
|
|
if (
|
|
|
|
|
id.includes("/echarts/") ||
|
|
|
|
|
id.includes("/zrender/") ||
|
|
|
|
|
id.includes("\\echarts\\") ||
|
|
|
|
|
id.includes("\\zrender\\")
|
|
|
|
|
) {
|
|
|
|
|
return "echarts-vendor";
|
2026-04-17 17:57:39 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-26 19:16:42 +08:00
|
|
|
if (id.includes("/react-dom/") || id.includes("/react/")) {
|
|
|
|
|
return "react-vendor";
|
|
|
|
|
}
|
|
|
|
|
if (id.includes("/wouter/")) return "router-vendor";
|
|
|
|
|
if (id.includes("/@radix-ui/")) return "radix-vendor";
|
|
|
|
|
if (id.includes("/framer-motion/")) return "motion-vendor";
|
2026-04-17 17:57:39 +08:00
|
|
|
if (
|
2026-05-26 19:16:42 +08:00
|
|
|
id.includes("/lucide-react/") ||
|
|
|
|
|
id.includes("/recharts/") ||
|
|
|
|
|
id.includes("/class-variance-authority/") ||
|
|
|
|
|
id.includes("/clsx/") ||
|
|
|
|
|
id.includes("/tailwind-merge/")
|
2026-04-17 17:57:39 +08:00
|
|
|
) {
|
|
|
|
|
return "ui-vendor";
|
|
|
|
|
}
|
2026-04-09 17:00:44 +08:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-05-26 19:16:42 +08:00
|
|
|
// echarts full build is ~1.1 MB; only loaded on /eq and /effects (lazy routes).
|
|
|
|
|
chunkSizeWarningLimit: 1200,
|
2026-04-09 17:00:44 +08:00
|
|
|
cssCodeSplit: true,
|
2026-03-31 18:52:37 +08:00
|
|
|
},
|
|
|
|
|
server: {
|
|
|
|
|
port: 3000,
|
|
|
|
|
strictPort: false, // Will find next available port if 3000 is busy
|
|
|
|
|
host: true,
|
2026-04-20 17:51:23 +08:00
|
|
|
// Browser requests with Origin are rejected (403) by the cloud API; proxy in dev avoids CORS/WAF.
|
|
|
|
|
proxy: {
|
|
|
|
|
"/luxsin-audio-api": {
|
|
|
|
|
target: "https://api.luxsin.com.cn",
|
|
|
|
|
changeOrigin: true,
|
|
|
|
|
rewrite: (p) => p.replace(/^\/luxsin-audio-api/, "/audio"),
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-03-31 18:52:37 +08:00
|
|
|
allowedHosts: [
|
|
|
|
|
".manuspre.computer",
|
|
|
|
|
".manus.computer",
|
|
|
|
|
".manus-asia.computer",
|
|
|
|
|
".manuscomputer.ai",
|
|
|
|
|
".manusvm.computer",
|
|
|
|
|
"localhost",
|
|
|
|
|
"127.0.0.1",
|
|
|
|
|
],
|
|
|
|
|
fs: {
|
|
|
|
|
strict: true,
|
|
|
|
|
deny: ["**/.*"],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|