优化目录
This commit is contained in:
@@ -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)");
|
|
||||||
})();
|
|
||||||
+1
-160
@@ -3,167 +3,10 @@ import tailwindcss from "@tailwindcss/vite";
|
|||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
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";
|
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 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 });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 构建后将统一入口 i.html 复制到 dist/ 根目录 */
|
/** 构建后将统一入口 i.html 复制到 dist/ 根目录 */
|
||||||
function vitePluginCopyRouterEntry(): Plugin {
|
function vitePluginCopyRouterEntry(): Plugin {
|
||||||
@@ -185,8 +28,6 @@ const plugins = [
|
|||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
jsxLocPlugin(),
|
jsxLocPlugin(),
|
||||||
vitePluginManusRuntime(),
|
vitePluginManusRuntime(),
|
||||||
vitePluginManusDebugCollector(),
|
|
||||||
vitePluginExcludeManusFromDist(),
|
|
||||||
vitePluginCopyRouterEntry(),
|
vitePluginCopyRouterEntry(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user