From 56ccb7b76ad772d54ff38fb224466b3e9d218c64 Mon Sep 17 00:00:00 2001 From: eafonyang Date: Wed, 10 Jun 2026 19:59:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/public/__manus__/debug-collector.js | 821 --------------------- vite.config.ts | 161 +--- 2 files changed, 1 insertion(+), 981 deletions(-) delete mode 100644 client/public/__manus__/debug-collector.js diff --git a/client/public/__manus__/debug-collector.js b/client/public/__manus__/debug-collector.js deleted file mode 100644 index 0504555..0000000 --- a/client/public/__manus__/debug-collector.js +++ /dev/null @@ -1,821 +0,0 @@ -/** - * Manus Debug Collector (agent-friendly) - * - * Captures: - * 1) Console logs - * 2) Network requests (fetch + XHR) - * 3) User interactions (semantic uiEvents: click/type/submit/nav/scroll/etc.) - * - * Data is periodically sent to /__manus__/logs - * Note: uiEvents are mirrored to sessionEvents for sessionReplay.log - */ -(function () { - "use strict"; - - // Prevent double initialization - if (window.__MANUS_DEBUG_COLLECTOR__) return; - - // ========================================================================== - // Configuration - // ========================================================================== - const CONFIG = { - reportEndpoint: "/__manus__/logs", - bufferSize: { - console: 500, - network: 200, - // semantic, agent-friendly UI events - ui: 500, - }, - reportInterval: 2000, - sensitiveFields: [ - "password", - "token", - "secret", - "key", - "authorization", - "cookie", - "session", - ], - maxBodyLength: 10240, - // UI event logging privacy policy: - // - inputs matching sensitiveFields or type=password are masked by default - // - non-sensitive inputs log up to 200 chars - uiInputMaxLen: 200, - uiTextMaxLen: 80, - // Scroll throttling: minimum ms between scroll events - scrollThrottleMs: 500, - }; - - // ========================================================================== - // Storage - // ========================================================================== - const store = { - consoleLogs: [], - networkRequests: [], - uiEvents: [], - lastReportTime: Date.now(), - lastScrollTime: 0, - }; - - // ========================================================================== - // Utility Functions - // ========================================================================== - - function sanitizeValue(value, depth) { - if (depth === void 0) depth = 0; - if (depth > 5) return "[Max Depth]"; - if (value === null) return null; - if (value === undefined) return undefined; - - if (typeof value === "string") { - return value.length > 1000 ? value.slice(0, 1000) + "...[truncated]" : value; - } - - if (typeof value !== "object") return value; - - if (Array.isArray(value)) { - return value.slice(0, 100).map(function (v) { - return sanitizeValue(v, depth + 1); - }); - } - - var sanitized = {}; - for (var k in value) { - if (Object.prototype.hasOwnProperty.call(value, k)) { - var isSensitive = CONFIG.sensitiveFields.some(function (f) { - return k.toLowerCase().indexOf(f) !== -1; - }); - if (isSensitive) { - sanitized[k] = "[REDACTED]"; - } else { - sanitized[k] = sanitizeValue(value[k], depth + 1); - } - } - } - return sanitized; - } - - function formatArg(arg) { - try { - if (arg instanceof Error) { - return { type: "Error", message: arg.message, stack: arg.stack }; - } - if (typeof arg === "object") return sanitizeValue(arg); - return String(arg); - } catch (e) { - return "[Unserializable]"; - } - } - - function formatArgs(args) { - var result = []; - for (var i = 0; i < args.length; i++) result.push(formatArg(args[i])); - return result; - } - - function pruneBuffer(buffer, maxSize) { - if (buffer.length > maxSize) buffer.splice(0, buffer.length - maxSize); - } - - function tryParseJson(str) { - if (typeof str !== "string") return str; - try { - return JSON.parse(str); - } catch (e) { - return str; - } - } - - // ========================================================================== - // Semantic UI Event Logging (agent-friendly) - // ========================================================================== - - function shouldIgnoreTarget(target) { - try { - if (!target || !(target instanceof Element)) return false; - return !!target.closest(".manus-no-record"); - } catch (e) { - return false; - } - } - - function compactText(s, maxLen) { - try { - var t = (s || "").trim().replace(/\s+/g, " "); - if (!t) return ""; - return t.length > maxLen ? t.slice(0, maxLen) + "…" : t; - } catch (e) { - return ""; - } - } - - function elText(el) { - try { - var t = el.innerText || el.textContent || ""; - return compactText(t, CONFIG.uiTextMaxLen); - } catch (e) { - return ""; - } - } - - function describeElement(el) { - if (!el || !(el instanceof Element)) return null; - - var getAttr = function (name) { - return el.getAttribute(name); - }; - - var tag = el.tagName ? el.tagName.toLowerCase() : null; - var id = el.id || null; - var name = getAttr("name") || null; - var role = getAttr("role") || null; - var ariaLabel = getAttr("aria-label") || null; - - var dataLoc = getAttr("data-loc") || null; - var testId = - getAttr("data-testid") || - getAttr("data-test-id") || - getAttr("data-test") || - null; - - var type = tag === "input" ? (getAttr("type") || "text") : null; - var href = tag === "a" ? getAttr("href") || null : null; - - // a small, stable hint for agents (avoid building full CSS paths) - var selectorHint = null; - if (testId) selectorHint = '[data-testid="' + testId + '"]'; - else if (dataLoc) selectorHint = '[data-loc="' + dataLoc + '"]'; - else if (id) selectorHint = "#" + id; - else selectorHint = tag || "unknown"; - - return { - tag: tag, - id: id, - name: name, - type: type, - role: role, - ariaLabel: ariaLabel, - testId: testId, - dataLoc: dataLoc, - href: href, - text: elText(el), - selectorHint: selectorHint, - }; - } - - function isSensitiveField(el) { - if (!el || !(el instanceof Element)) return false; - var tag = el.tagName ? el.tagName.toLowerCase() : ""; - if (tag !== "input" && tag !== "textarea") return false; - - var type = (el.getAttribute("type") || "").toLowerCase(); - if (type === "password") return true; - - var name = (el.getAttribute("name") || "").toLowerCase(); - var id = (el.id || "").toLowerCase(); - - return CONFIG.sensitiveFields.some(function (f) { - return name.indexOf(f) !== -1 || id.indexOf(f) !== -1; - }); - } - - function getInputValueSafe(el) { - if (!el || !(el instanceof Element)) return null; - var tag = el.tagName ? el.tagName.toLowerCase() : ""; - if (tag !== "input" && tag !== "textarea" && tag !== "select") return null; - - var v = ""; - try { - v = el.value != null ? String(el.value) : ""; - } catch (e) { - v = ""; - } - - if (isSensitiveField(el)) return { masked: true, length: v.length }; - - if (v.length > CONFIG.uiInputMaxLen) v = v.slice(0, CONFIG.uiInputMaxLen) + "…"; - return v; - } - - function logUiEvent(kind, payload) { - var entry = { - timestamp: Date.now(), - kind: kind, - url: location.href, - viewport: { width: window.innerWidth, height: window.innerHeight }, - payload: sanitizeValue(payload), - }; - store.uiEvents.push(entry); - pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui); - } - - function installUiEventListeners() { - // Clicks - document.addEventListener( - "click", - function (e) { - var t = e.target; - if (shouldIgnoreTarget(t)) return; - logUiEvent("click", { - target: describeElement(t), - x: e.clientX, - y: e.clientY, - }); - }, - true - ); - - // Typing "commit" events - document.addEventListener( - "change", - function (e) { - var t = e.target; - if (shouldIgnoreTarget(t)) return; - logUiEvent("change", { - target: describeElement(t), - value: getInputValueSafe(t), - }); - }, - true - ); - - document.addEventListener( - "focusin", - function (e) { - var t = e.target; - if (shouldIgnoreTarget(t)) return; - logUiEvent("focusin", { target: describeElement(t) }); - }, - true - ); - - document.addEventListener( - "focusout", - function (e) { - var t = e.target; - if (shouldIgnoreTarget(t)) return; - logUiEvent("focusout", { - target: describeElement(t), - value: getInputValueSafe(t), - }); - }, - true - ); - - // Enter/Escape are useful for form flows & modals - document.addEventListener( - "keydown", - function (e) { - if (e.key !== "Enter" && e.key !== "Escape") return; - var t = e.target; - if (shouldIgnoreTarget(t)) return; - logUiEvent("keydown", { key: e.key, target: describeElement(t) }); - }, - true - ); - - // Form submissions - document.addEventListener( - "submit", - function (e) { - var t = e.target; - if (shouldIgnoreTarget(t)) return; - logUiEvent("submit", { target: describeElement(t) }); - }, - true - ); - - // Throttled scroll events - window.addEventListener( - "scroll", - function () { - var now = Date.now(); - if (now - store.lastScrollTime < CONFIG.scrollThrottleMs) return; - store.lastScrollTime = now; - - logUiEvent("scroll", { - scrollX: window.scrollX, - scrollY: window.scrollY, - documentHeight: document.documentElement.scrollHeight, - viewportHeight: window.innerHeight, - }); - }, - { passive: true } - ); - - // Navigation tracking for SPAs - function nav(reason) { - logUiEvent("navigate", { reason: reason }); - } - - var origPush = history.pushState; - history.pushState = function () { - origPush.apply(this, arguments); - nav("pushState"); - }; - - var origReplace = history.replaceState; - history.replaceState = function () { - origReplace.apply(this, arguments); - nav("replaceState"); - }; - - window.addEventListener("popstate", function () { - nav("popstate"); - }); - window.addEventListener("hashchange", function () { - nav("hashchange"); - }); - } - - // ========================================================================== - // Console Interception - // ========================================================================== - - var originalConsole = { - log: console.log.bind(console), - debug: console.debug.bind(console), - info: console.info.bind(console), - warn: console.warn.bind(console), - error: console.error.bind(console), - }; - - ["log", "debug", "info", "warn", "error"].forEach(function (method) { - console[method] = function () { - var args = Array.prototype.slice.call(arguments); - - var entry = { - timestamp: Date.now(), - level: method.toUpperCase(), - args: formatArgs(args), - stack: method === "error" ? new Error().stack : null, - }; - - store.consoleLogs.push(entry); - pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); - - originalConsole[method].apply(console, args); - }; - }); - - window.addEventListener("error", function (event) { - store.consoleLogs.push({ - timestamp: Date.now(), - level: "ERROR", - args: [ - { - type: "UncaughtError", - message: event.message, - filename: event.filename, - lineno: event.lineno, - colno: event.colno, - stack: event.error ? event.error.stack : null, - }, - ], - stack: event.error ? event.error.stack : null, - }); - pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); - - // Mark an error moment in UI event stream for agents - logUiEvent("error", { - message: event.message, - filename: event.filename, - lineno: event.lineno, - colno: event.colno, - }); - }); - - window.addEventListener("unhandledrejection", function (event) { - var reason = event.reason; - store.consoleLogs.push({ - timestamp: Date.now(), - level: "ERROR", - args: [ - { - type: "UnhandledRejection", - reason: reason && reason.message ? reason.message : String(reason), - stack: reason && reason.stack ? reason.stack : null, - }, - ], - stack: reason && reason.stack ? reason.stack : null, - }); - pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); - - logUiEvent("unhandledrejection", { - reason: reason && reason.message ? reason.message : String(reason), - }); - }); - - // ========================================================================== - // Fetch Interception - // ========================================================================== - - var originalFetch = window.fetch.bind(window); - - window.fetch = function (input, init) { - init = init || {}; - var startTime = Date.now(); - // Handle string, Request object, or URL object - var url = typeof input === "string" - ? input - : (input && (input.url || input.href || String(input))) || ""; - var method = init.method || (input && input.method) || "GET"; - - // Don't intercept internal requests - if (url.indexOf("/__manus__/") === 0) { - return originalFetch(input, init); - } - - // Safely parse headers (avoid breaking if headers format is invalid) - var requestHeaders = {}; - try { - if (init.headers) { - requestHeaders = Object.fromEntries(new Headers(init.headers).entries()); - } - } catch (e) { - requestHeaders = { _parseError: true }; - } - - var entry = { - timestamp: startTime, - type: "fetch", - method: method.toUpperCase(), - url: url, - request: { - headers: requestHeaders, - body: init.body ? sanitizeValue(tryParseJson(init.body)) : null, - }, - response: null, - duration: null, - error: null, - }; - - return originalFetch(input, init) - .then(function (response) { - entry.duration = Date.now() - startTime; - - var contentType = (response.headers.get("content-type") || "").toLowerCase(); - var contentLength = response.headers.get("content-length"); - - entry.response = { - status: response.status, - statusText: response.statusText, - headers: Object.fromEntries(response.headers.entries()), - body: null, - }; - - // Semantic network hint for agents on failures (sync, no need to wait for body) - if (response.status >= 400) { - logUiEvent("network_error", { - kind: "fetch", - method: entry.method, - url: entry.url, - status: response.status, - statusText: response.statusText, - }); - } - - // Skip body capture for streaming responses (SSE, etc.) to avoid memory leaks - var isStreaming = contentType.indexOf("text/event-stream") !== -1 || - contentType.indexOf("application/stream") !== -1 || - contentType.indexOf("application/x-ndjson") !== -1; - if (isStreaming) { - entry.response.body = "[Streaming response - not captured]"; - store.networkRequests.push(entry); - pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); - return response; - } - - // Skip body capture for large responses to avoid memory issues - if (contentLength && parseInt(contentLength, 10) > CONFIG.maxBodyLength) { - entry.response.body = "[Response too large: " + contentLength + " bytes]"; - store.networkRequests.push(entry); - pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); - return response; - } - - // Skip body capture for binary content types - var isBinary = contentType.indexOf("image/") !== -1 || - contentType.indexOf("video/") !== -1 || - contentType.indexOf("audio/") !== -1 || - contentType.indexOf("application/octet-stream") !== -1 || - contentType.indexOf("application/pdf") !== -1 || - contentType.indexOf("application/zip") !== -1; - if (isBinary) { - entry.response.body = "[Binary content: " + contentType + "]"; - store.networkRequests.push(entry); - pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); - return response; - } - - // For text responses, clone and read body in background - var clonedResponse = response.clone(); - - // Async: read body in background, don't block the response - clonedResponse - .text() - .then(function (text) { - if (text.length <= CONFIG.maxBodyLength) { - entry.response.body = sanitizeValue(tryParseJson(text)); - } else { - entry.response.body = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]"; - } - }) - .catch(function () { - entry.response.body = "[Unable to read body]"; - }) - .finally(function () { - store.networkRequests.push(entry); - pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); - }); - - // Return response immediately, don't wait for body reading - return response; - }) - .catch(function (error) { - entry.duration = Date.now() - startTime; - entry.error = { message: error.message, stack: error.stack }; - - store.networkRequests.push(entry); - pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); - - logUiEvent("network_error", { - kind: "fetch", - method: entry.method, - url: entry.url, - message: error.message, - }); - - throw error; - }); - }; - - // ========================================================================== - // XHR Interception - // ========================================================================== - - var originalXHROpen = XMLHttpRequest.prototype.open; - var originalXHRSend = XMLHttpRequest.prototype.send; - - XMLHttpRequest.prototype.open = function (method, url) { - this._manusData = { - method: (method || "GET").toUpperCase(), - url: url, - startTime: null, - }; - return originalXHROpen.apply(this, arguments); - }; - - XMLHttpRequest.prototype.send = function (body) { - var xhr = this; - - if ( - xhr._manusData && - xhr._manusData.url && - xhr._manusData.url.indexOf("/__manus__/") !== 0 - ) { - xhr._manusData.startTime = Date.now(); - xhr._manusData.requestBody = body ? sanitizeValue(tryParseJson(body)) : null; - - xhr.addEventListener("load", function () { - var contentType = (xhr.getResponseHeader("content-type") || "").toLowerCase(); - var responseBody = null; - - // Skip body capture for streaming responses - var isStreaming = contentType.indexOf("text/event-stream") !== -1 || - contentType.indexOf("application/stream") !== -1 || - contentType.indexOf("application/x-ndjson") !== -1; - - // Skip body capture for binary content types - var isBinary = contentType.indexOf("image/") !== -1 || - contentType.indexOf("video/") !== -1 || - contentType.indexOf("audio/") !== -1 || - contentType.indexOf("application/octet-stream") !== -1 || - contentType.indexOf("application/pdf") !== -1 || - contentType.indexOf("application/zip") !== -1; - - if (isStreaming) { - responseBody = "[Streaming response - not captured]"; - } else if (isBinary) { - responseBody = "[Binary content: " + contentType + "]"; - } else { - // Safe to read responseText for text responses - try { - var text = xhr.responseText || ""; - if (text.length > CONFIG.maxBodyLength) { - responseBody = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]"; - } else { - responseBody = sanitizeValue(tryParseJson(text)); - } - } catch (e) { - // responseText may throw for non-text responses - responseBody = "[Unable to read response: " + e.message + "]"; - } - } - - var entry = { - timestamp: xhr._manusData.startTime, - type: "xhr", - method: xhr._manusData.method, - url: xhr._manusData.url, - request: { body: xhr._manusData.requestBody }, - response: { - status: xhr.status, - statusText: xhr.statusText, - body: responseBody, - }, - duration: Date.now() - xhr._manusData.startTime, - error: null, - }; - - store.networkRequests.push(entry); - pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); - - if (entry.response && entry.response.status >= 400) { - logUiEvent("network_error", { - kind: "xhr", - method: entry.method, - url: entry.url, - status: entry.response.status, - statusText: entry.response.statusText, - }); - } - }); - - xhr.addEventListener("error", function () { - var entry = { - timestamp: xhr._manusData.startTime, - type: "xhr", - method: xhr._manusData.method, - url: xhr._manusData.url, - request: { body: xhr._manusData.requestBody }, - response: null, - duration: Date.now() - xhr._manusData.startTime, - error: { message: "Network error" }, - }; - - store.networkRequests.push(entry); - pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); - - logUiEvent("network_error", { - kind: "xhr", - method: entry.method, - url: entry.url, - message: "Network error", - }); - }); - } - - return originalXHRSend.apply(this, arguments); - }; - - // ========================================================================== - // Data Reporting - // ========================================================================== - - function reportLogs() { - var consoleLogs = store.consoleLogs.splice(0); - var networkRequests = store.networkRequests.splice(0); - var uiEvents = store.uiEvents.splice(0); - - // Skip if no new data - if ( - consoleLogs.length === 0 && - networkRequests.length === 0 && - uiEvents.length === 0 - ) { - return Promise.resolve(); - } - - var payload = { - timestamp: Date.now(), - consoleLogs: consoleLogs, - networkRequests: networkRequests, - // Mirror uiEvents to sessionEvents for sessionReplay.log - sessionEvents: uiEvents, - // agent-friendly semantic events - uiEvents: uiEvents, - }; - - return originalFetch(CONFIG.reportEndpoint, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }).catch(function () { - // Put data back on failure (but respect limits) - store.consoleLogs = consoleLogs.concat(store.consoleLogs); - store.networkRequests = networkRequests.concat(store.networkRequests); - store.uiEvents = uiEvents.concat(store.uiEvents); - - pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); - pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); - pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui); - }); - } - - // Periodic reporting - setInterval(reportLogs, CONFIG.reportInterval); - - // Report on page unload - window.addEventListener("beforeunload", function () { - var consoleLogs = store.consoleLogs; - var networkRequests = store.networkRequests; - var uiEvents = store.uiEvents; - - if ( - consoleLogs.length === 0 && - networkRequests.length === 0 && - uiEvents.length === 0 - ) { - return; - } - - var payload = { - timestamp: Date.now(), - consoleLogs: consoleLogs, - networkRequests: networkRequests, - // Mirror uiEvents to sessionEvents for sessionReplay.log - sessionEvents: uiEvents, - uiEvents: uiEvents, - }; - - if (navigator.sendBeacon) { - var payloadStr = JSON.stringify(payload); - // sendBeacon has ~64KB limit, truncate if too large - var MAX_BEACON_SIZE = 60000; // Leave some margin - if (payloadStr.length > MAX_BEACON_SIZE) { - // Prioritize: keep recent events, drop older logs - var truncatedPayload = { - timestamp: Date.now(), - consoleLogs: consoleLogs.slice(-50), - networkRequests: networkRequests.slice(-20), - sessionEvents: uiEvents.slice(-100), - uiEvents: uiEvents.slice(-100), - _truncated: true, - }; - payloadStr = JSON.stringify(truncatedPayload); - } - navigator.sendBeacon(CONFIG.reportEndpoint, payloadStr); - } - }); - - // ========================================================================== - // Initialization - // ========================================================================== - - // Install semantic UI listeners ASAP - try { - installUiEventListeners(); - } catch (e) { - console.warn("[Manus] Failed to install UI listeners:", e); - } - - // Mark as initialized - window.__MANUS_DEBUG_COLLECTOR__ = { - version: "2.0-no-rrweb", - store: store, - forceReport: reportLogs, - }; - - console.debug("[Manus] Debug collector initialized (no rrweb, UI events only)"); -})(); diff --git a/vite.config.ts b/vite.config.ts index a6182ec..2325ee6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,167 +3,10 @@ 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 { defineConfig, type Plugin } 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) })); - } - }); - }); - }, - }; -} - -/** 生产构建时剔除 public 中的 __manus__(仅开发调试使用) */ -function vitePluginExcludeManusFromDist(): Plugin { - const distDir = path.resolve(import.meta.dirname, "dist/modern"); - 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 }); - } - }, - }; -} /** Dev: / → i.modern.html; /i.html → UA router entry */ function vitePluginModernDevEntry(): Plugin { @@ -203,8 +46,6 @@ const plugins = [ tailwindcss(), jsxLocPlugin(), vitePluginManusRuntime(), - // vitePluginManusDebugCollector(), // 已禁用:不再注入 debug-collector.js,消除浏览器网络面板中定时出现的 __manus__/logs 请求 - vitePluginExcludeManusFromDist(), vitePluginCopyRouterEntry(), ];