hangwin--mcp-chrome
269 行
9.2 KiB
JavaScript
269 行
9.2 KiB
JavaScript
/* eslint-disable */
|
|
/**
|
|
* Network Capture Helper
|
|
*
|
|
* This script helps replay network requests with the original cookies and headers.
|
|
*/
|
|
|
|
// Prevent duplicate initialization
|
|
if (window.__NETWORK_CAPTURE_HELPER_INITIALIZED__) {
|
|
// Already initialized, skip
|
|
} else {
|
|
window.__NETWORK_CAPTURE_HELPER_INITIALIZED__ = true;
|
|
|
|
/**
|
|
* Replay a network request
|
|
* @param {string} url - The URL to send the request to
|
|
* @param {string} method - The HTTP method to use
|
|
* @param {Object} headers - The headers to include in the request
|
|
* @param {any} body - The body of the request
|
|
* @param {number} timeout - Timeout in milliseconds (default: 30000)
|
|
* @returns {Promise<Object>} - The response data
|
|
*/
|
|
async function replayNetworkRequest(
|
|
url,
|
|
method,
|
|
headers,
|
|
body,
|
|
timeout = 30000,
|
|
formDataDescriptor = null,
|
|
) {
|
|
try {
|
|
// Create fetch options
|
|
const options = {
|
|
method: method,
|
|
headers: headers || {},
|
|
credentials: 'include', // Include cookies
|
|
mode: 'cors',
|
|
cache: 'no-cache',
|
|
};
|
|
|
|
// Helper: convert base64 to Blob
|
|
const base64ToBlob = (base64, contentType = 'application/octet-stream') => {
|
|
try {
|
|
const decodedString = atob(base64);
|
|
const len = decodedString.length;
|
|
const bytes = new Uint8Array(len);
|
|
for (let i = 0; i < len; i++) bytes[i] = decodedString.charCodeAt(i);
|
|
return new Blob([bytes], { type: contentType });
|
|
} catch (e) {
|
|
return new Blob([]);
|
|
}
|
|
};
|
|
|
|
// Helper: request native to read filePath into base64
|
|
const readFileBase64 = (path) =>
|
|
new Promise((resolve) => {
|
|
const requestId = `net-helper-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
const timeoutId = setTimeout(() => {
|
|
cleanup();
|
|
resolve(null);
|
|
}, 30000);
|
|
function onMessage(msg) {
|
|
if (
|
|
msg &&
|
|
msg.type === 'file_operation_response' &&
|
|
msg.responseToRequestId === requestId
|
|
) {
|
|
cleanup();
|
|
const p = msg.payload || {};
|
|
if (p.success && p.base64Data)
|
|
resolve({ base64: p.base64Data, fileName: p.fileName });
|
|
else resolve(null);
|
|
}
|
|
}
|
|
function cleanup() {
|
|
clearTimeout(timeoutId);
|
|
chrome.runtime.onMessage.removeListener(onMessage);
|
|
}
|
|
chrome.runtime.onMessage.addListener(onMessage);
|
|
chrome.runtime
|
|
.sendMessage({
|
|
type: 'forward_to_native',
|
|
message: {
|
|
type: 'file_operation',
|
|
requestId,
|
|
payload: { action: 'readBase64File', filePath: path },
|
|
},
|
|
})
|
|
.catch(() => {
|
|
cleanup();
|
|
resolve(null);
|
|
});
|
|
});
|
|
|
|
// Build multipart/form-data if descriptor is provided
|
|
if (method !== 'GET' && method !== 'HEAD' && formDataDescriptor) {
|
|
const fd = new FormData();
|
|
try {
|
|
if (Array.isArray(formDataDescriptor)) {
|
|
for (const item of formDataDescriptor) {
|
|
if (!Array.isArray(item) || item.length < 2) continue;
|
|
const name = String(item[0] || 'file');
|
|
const spec = String(item[1] || '');
|
|
const filenameHint = item[2] ? String(item[2]) : undefined;
|
|
if (/^(https?:\/\/|url:)/i.test(spec)) {
|
|
const url = spec.replace(/^url:/i, '');
|
|
const resp = await fetch(url);
|
|
const blob = await resp.blob();
|
|
const fn =
|
|
filenameHint || url.split('?')[0].split('#')[0].split('/').pop() || 'file';
|
|
fd.append(name, blob, fn);
|
|
} else if (/^base64:/i.test(spec)) {
|
|
const b64 = spec.replace(/^base64:/i, '');
|
|
const blob = base64ToBlob(b64);
|
|
fd.append(name, blob, filenameHint || 'file');
|
|
} else if (/^file:/i.test(spec)) {
|
|
const p = spec.replace(/^file:/i, '');
|
|
const res = await readFileBase64(p);
|
|
if (res && res.base64) {
|
|
const blob = base64ToBlob(res.base64);
|
|
fd.append(name, blob, filenameHint || res.fileName || 'file');
|
|
}
|
|
} else {
|
|
// treat as string field
|
|
fd.append(name, spec);
|
|
}
|
|
}
|
|
} else if (typeof formDataDescriptor === 'object') {
|
|
const fds = formDataDescriptor;
|
|
const fields = fds.fields || {};
|
|
const files = Array.isArray(fds.files) ? fds.files : [];
|
|
for (const [k, v] of Object.entries(fields)) fd.append(String(k), String(v));
|
|
for (const file of files) {
|
|
const name = String(file.name || 'file');
|
|
if (file.fileUrl) {
|
|
const resp = await fetch(String(file.fileUrl));
|
|
const blob = await resp.blob();
|
|
const fn =
|
|
file.filename ||
|
|
String(file.fileUrl).split('?')[0].split('#')[0].split('/').pop() ||
|
|
'file';
|
|
fd.append(name, blob, fn);
|
|
} else if (file.base64Data) {
|
|
const blob = base64ToBlob(
|
|
String(file.base64Data),
|
|
String(file.contentType || 'application/octet-stream'),
|
|
);
|
|
fd.append(name, blob, file.filename || 'file');
|
|
} else if (file.filePath) {
|
|
const res = await readFileBase64(String(file.filePath));
|
|
if (res && res.base64) {
|
|
const blob = base64ToBlob(
|
|
res.base64,
|
|
String(file.contentType || 'application/octet-stream'),
|
|
);
|
|
fd.append(name, blob, file.filename || res.fileName || 'file');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to construct FormData:', e);
|
|
}
|
|
// Let browser set the correct multipart boundary
|
|
try {
|
|
if (options.headers) {
|
|
delete options.headers['content-type'];
|
|
delete options.headers['Content-Type'];
|
|
}
|
|
} catch {}
|
|
options.body = fd;
|
|
} else if (method !== 'GET' && method !== 'HEAD' && body !== undefined) {
|
|
// Fallback to raw body
|
|
options.body = body;
|
|
}
|
|
|
|
// 创建一个带超时的 fetch
|
|
const fetchWithTimeout = async (url, options, timeout) => {
|
|
const controller = new AbortController();
|
|
const signal = controller.signal;
|
|
|
|
// 设置超时
|
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
|
|
try {
|
|
const response = await fetch(url, { ...options, signal });
|
|
clearTimeout(timeoutId);
|
|
return response;
|
|
} catch (error) {
|
|
clearTimeout(timeoutId);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// 发送带超时的请求
|
|
const response = await fetchWithTimeout(url, options, timeout);
|
|
|
|
// Process response
|
|
const responseData = {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers: {},
|
|
};
|
|
|
|
// Get response headers
|
|
response.headers.forEach((value, key) => {
|
|
responseData.headers[key] = value;
|
|
});
|
|
|
|
// Try to get response body based on content type
|
|
const contentType = response.headers.get('content-type') || '';
|
|
|
|
try {
|
|
if (contentType.includes('application/json')) {
|
|
responseData.body = await response.json();
|
|
} else if (
|
|
contentType.includes('text/') ||
|
|
contentType.includes('application/xml') ||
|
|
contentType.includes('application/javascript')
|
|
) {
|
|
responseData.body = await response.text();
|
|
} else {
|
|
// For binary data, just indicate it was received but not parsed
|
|
responseData.body = '[Binary data not displayed]';
|
|
}
|
|
} catch (error) {
|
|
responseData.body = `[Error parsing response body: ${error.message}]`;
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
response: responseData,
|
|
};
|
|
} catch (error) {
|
|
console.error('Error replaying request:', error);
|
|
return {
|
|
success: false,
|
|
error: `Error replaying request: ${error.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Listen for messages from the extension
|
|
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
|
// Respond to ping message
|
|
if (request.action === 'chrome_network_request_ping') {
|
|
sendResponse({ status: 'pong' });
|
|
return false; // Synchronous response
|
|
} else if (request.action === 'sendPureNetworkRequest') {
|
|
replayNetworkRequest(
|
|
request.url,
|
|
request.method,
|
|
request.headers,
|
|
request.body,
|
|
request.timeout,
|
|
request.formData,
|
|
)
|
|
.then(sendResponse)
|
|
.catch((error) => {
|
|
sendResponse({
|
|
success: false,
|
|
error: `Unexpected error: ${error.message}`,
|
|
});
|
|
});
|
|
return true; // Indicates async response
|
|
}
|
|
});
|
|
}
|