chat-relay/extension/background.js
2025-07-24 00:23:41 -07:00

1215 lines
67 KiB
JavaScript

/*
* Chat Relay: Relay for AI Chat Interfaces
* Copyright (C) 2025 Jamison Moore
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
// AI Chat Relay - Background Script
// Default settings
const DEFAULT_SETTINGS = {
serverHost: 'localhost',
serverPort: 3003,
serverProtocol: 'ws'
};
let relaySocket = null;
let reconnectInterval = 5000;
let reconnectTimer = null;
let activeTabId = null;
let serverUrl = '';
let lastRequestId = null; // User's global lastRequestId
let processingRequest = false; // User's global processing flag
let pendingRequests = []; // User's command queue
let lastSuccessfullyProcessedMessageText = null; // Text of the last message successfully processed (AI response or duplicate handled)
const pendingRequestDetails = new Map(); // Stores { text: string } for active requests, keyed by requestId
// Supported domains for chat interfaces
const supportedDomains = ['gemini.google.com', 'aistudio.google.com', 'chatgpt.com', 'claude.ai'];
// ===== DEBUGGER RELATED GLOBALS =====
const BG_LOG_PREFIX = '[BG DEBUGGER]';
let debuggerAttachedTabs = new Map(); // tabId -> { providerName, patterns, isFetchEnabled, isAttached, lastKnownRequestId }
// Load settings and connect to the relay server
function loadSettingsAndConnect() {
console.log("BACKGROUND: Loading settings and connecting to relay server");
chrome.storage.sync.get(DEFAULT_SETTINGS, (items) => {
serverUrl = `${items.serverProtocol}://${items.serverHost}:${items.serverPort}`;
console.log("BACKGROUND: Using server URL:", serverUrl);
connectToRelayServer();
});
}
// Connect to the relay server
function connectToRelayServer() {
if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
console.log("BACKGROUND: Relay WS: Already connected.");
return;
}
if (!navigator.onLine) {
console.warn("BACKGROUND: Network offline. Deferring connection attempt.");
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(connectToRelayServer, reconnectInterval);
return;
}
const healthCheckUrl = serverUrl.replace(/^ws/, 'http') + '/health';
console.log("BACKGROUND: Performing HTTP pre-check to", healthCheckUrl);
fetch(healthCheckUrl)
.then(response => {
if (!response.ok) {
// Server responded, but not with a 2xx status (e.g., 404, 500)
console.warn(`BACKGROUND: HTTP pre-check to ${healthCheckUrl} received non-OK status: ${response.status}. Server might be having issues. Deferring WebSocket attempt.`);
return Promise.reject(new Error(`Server responded with ${response.status}`));
}
return response.json(); // Attempt to parse JSON
})
.then(healthData => {
console.log(`BACKGROUND: HTTP pre-check to ${healthCheckUrl} successful. Server status: ${healthData.status}, Active Connections: ${healthData.activeBrowserConnections}. Proceeding with WebSocket connection.`);
attemptWebSocketConnection();
})
.catch(fetchError => {
// This catches network errors (server down) or errors from the .then() chain (non-OK response, JSON parse error)
console.warn(`BACKGROUND: HTTP pre-check to ${healthCheckUrl} failed: ${fetchError.message}. Server is likely down, unreachable, or health endpoint is misbehaving. Deferring WebSocket attempt.`);
relaySocket = null;
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(connectToRelayServer, reconnectInterval);
});
}
function attemptWebSocketConnection() {
console.log("BACKGROUND: Relay WS: Attempting to connect to", serverUrl);
try {
relaySocket = new WebSocket(serverUrl);
relaySocket.onopen = () => {
console.log("BACKGROUND: Relay WS: Connection established with relay server.");
reconnectInterval = 5000; // Reset reconnect interval on successful connection
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = null;
// Notify the server that this extension instance is ready
if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
const readyMessage = { type: 'EXTENSION_READY' };
relaySocket.send(JSON.stringify(readyMessage));
console.log("BACKGROUND: Relay WS: Sent EXTENSION_READY to server.");
}
};
relaySocket.onmessage = (event) => {
console.log("BACKGROUND: Relay WS: Message received from relay server:", event.data);
try {
const command = JSON.parse(event.data);
if (command.type === 'SEND_CHAT_MESSAGE') {
console.log("BACKGROUND: Received SEND_CHAT_MESSAGE command with requestId:", command.requestId);
// Store details for this new request
pendingRequestDetails.set(command.requestId, { messageContent: command.message }); // Changed key 'text' to 'messageContent'
let messagePreview = "";
const messageValue = command.message;
if (typeof messageValue === 'string') {
messagePreview = `String: "${messageValue.substring(0, 50)}..."`;
} else if (messageValue instanceof ArrayBuffer) {
messagePreview = `ArrayBuffer data (size: ${messageValue.byteLength} bytes)`;
} else if (messageValue instanceof Blob) {
messagePreview = `Blob data (size: ${messageValue.size} bytes, type: ${messageValue.type})`;
} else if (messageValue && typeof messageValue === 'object' && messageValue !== null) {
messagePreview = `Object data (type: ${Object.prototype.toString.call(messageValue)})`;
} else {
messagePreview = `Data type: ${typeof messageValue}, Value: ${String(messageValue).substring(0,50)}`;
}
console.log(`BACKGROUND: Stored details for requestId: ${command.requestId}, message: ${messagePreview}`);
// Add to the queue
pendingRequests.push(command);
console.log(`BACKGROUND: Added command with requestId: ${command.requestId} to queue. Queue length: ${pendingRequests.length}`);
// Attempt to process the next request in the queue
processNextRequest();
}
} catch (error) {
console.error("BACKGROUND: Relay WS: Error processing message from relay server:", error);
}
};
relaySocket.onerror = (errorEvent) => {
console.warn("BACKGROUND: Relay WS: WebSocket connection error (event):", errorEvent);
// onclose will typically follow and handle reconnection logic
};
relaySocket.onclose = (closeEvent) => {
console.log(`BACKGROUND: Relay WS: Connection closed (event). Code: ${closeEvent.code}, Reason: '${closeEvent.reason || 'N/A'}', Cleanly: ${closeEvent.wasClean}. Will attempt reconnect (via connectToRelayServer) in ${reconnectInterval / 1000}s.`);
relaySocket = null;
if (reconnectTimer) clearTimeout(reconnectTimer);
// Retry the entire connectToRelayServer process, which includes the HTTP pre-check
reconnectTimer = setTimeout(connectToRelayServer, reconnectInterval);
};
} catch (instantiationError) {
console.error("BACKGROUND: Relay WS: Error instantiating WebSocket:", instantiationError);
relaySocket = null;
if (reconnectTimer) clearTimeout(reconnectTimer);
console.log(`BACKGROUND: Relay WS: Instantiation failed. Will attempt reconnect (via connectToRelayServer) in ${reconnectInterval / 1000}s.`);
// Retry the entire connectToRelayServer process
reconnectTimer = setTimeout(connectToRelayServer, reconnectInterval);
}
}
// Forward commands to content script
async function forwardCommandToContentScript(command) { // command will include original requestId
try {
console.log("BACKGROUND: Forwarding command to content script:", command);
let targetTabIdForCommand = null;
if (activeTabId) {
try {
console.log(`BACKGROUND: Attempting to use stored activeTabId: ${activeTabId}`);
// Test send to ensure tab is still valid for this command before associating requestId
await new Promise((resolve, reject) => {
chrome.tabs.sendMessage(activeTabId, { type: "PING_TAB" }, response => { // Ping before associating
if (chrome.runtime.lastError || !response || !response.success) {
console.warn(`BACKGROUND: Ping to stored tab ${activeTabId} failed or no ack:`, chrome.runtime.lastError ? chrome.runtime.lastError.message : "No response/success false");
activeTabId = null; // Invalidate activeTabId
reject(new Error("Ping failed"));
} else {
console.log(`BACKGROUND: Ping to stored tab ${activeTabId} successful.`);
targetTabIdForCommand = activeTabId;
resolve();
}
});
});
} catch (error) {
// Fall through to findAndSendToSuitableTab if ping fails
console.warn(`BACKGROUND: Error using stored activeTabId ${activeTabId}, will find new tab:`, error);
}
}
if (!targetTabIdForCommand) {
targetTabIdForCommand = await findAndSendToSuitableTab(command, true); // Pass true to only find, not send yet
}
if (targetTabIdForCommand) {
const tabInfo = debuggerAttachedTabs.get(targetTabIdForCommand);
if (tabInfo) {
tabInfo.lastKnownRequestId = command.requestId; // Store command's requestId for this specific tab
console.log(BG_LOG_PREFIX, `Associated requestId ${command.requestId} with tab ${targetTabIdForCommand} for debugger.`);
} else {
console.warn(BG_LOG_PREFIX, `Tab ${targetTabIdForCommand} is not being debugged. Cannot associate requestId for debugger.`);
}
// Now actually send the command
const MAX_SEND_RETRIES = 3;
const SEND_RETRY_DELAY = 500;
let sendAttempt = 0;
function sendMessageWithRetry() {
if (sendAttempt >= MAX_SEND_RETRIES) {
const errorMessage = `Failed to send message to content script in tab ${targetTabIdForCommand} after ${MAX_SEND_RETRIES} attempts.`;
console.error(`BACKGROUND: ${errorMessage}`);
if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
relaySocket.send(JSON.stringify({
type: "CHAT_RESPONSE_ERROR",
requestId: command.requestId,
error: errorMessage
}));
}
if (lastRequestId === command.requestId) {
processingRequest = false;
}
processNextRequest();
return;
}
sendAttempt++;
chrome.tabs.sendMessage(targetTabIdForCommand, command, (response) => {
if (chrome.runtime.lastError) {
console.warn(`BACKGROUND: Attempt ${sendAttempt} to send message to tab ${targetTabIdForCommand} failed: ${chrome.runtime.lastError.message}. Retrying in ${SEND_RETRY_DELAY}ms...`);
setTimeout(sendMessageWithRetry, SEND_RETRY_DELAY);
} else {
console.log(`BACKGROUND: Content script in tab ${targetTabIdForCommand} acknowledged command on attempt ${sendAttempt}:`, response);
}
});
}
sendMessageWithRetry();
} else {
const errorMsg = "Could not find any suitable tab for command.";
console.error(`BACKGROUND: ${errorMsg} for requestId: ${command.requestId}.`);
if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
relaySocket.send(JSON.stringify({
type: "CHAT_RESPONSE_ERROR",
requestId: command.requestId,
error: errorMsg
}));
console.log(`BACKGROUND: Sent CHAT_RESPONSE_ERROR to server for requestId: ${command.requestId} (no suitable tab).`);
} else {
console.error(`BACKGROUND: Relay WS not OPEN, cannot send CHAT_RESPONSE_ERROR for requestId: ${command.requestId} (no suitable tab).`);
}
if (lastRequestId === command.requestId) {
processingRequest = false;
console.log(`BACKGROUND: Reset processingRequest for requestId: ${command.requestId} (no suitable tab).`);
}
// Ensure processNextRequest is called to handle any queued items,
// even if this one failed.
processNextRequest();
}
} catch (error) {
console.error("BACKGROUND: Error in forwardCommandToContentScript for requestId:", command.requestId, error);
// Send an error back to the server if an unexpected error occurs during forwarding
if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
relaySocket.send(JSON.stringify({
type: "CHAT_RESPONSE_ERROR",
requestId: command.requestId,
error: `Internal error in background script while forwarding command: ${error.message}`
}));
console.log(`BACKGROUND: Sent CHAT_RESPONSE_ERROR to server for requestId: ${command.requestId} (exception).`);
} else {
console.error(`BACKGROUND: Relay WS not OPEN, cannot send CHAT_RESPONSE_ERROR for requestId: ${command.requestId} (exception).`);
}
if (lastRequestId === command.requestId) {
processingRequest = false;
console.log(`BACKGROUND: Reset processingRequest for requestId: ${command.requestId} (exception).`);
}
}
}
// Helper function to find a suitable tab and send the command
async function findAndSendToSuitableTab(command, justFinding = false) {
try {
console.log("BACKGROUND: Finding suitable tab for command:", command);
const allTabs = await chrome.tabs.query({});
const matchingTabs = allTabs.filter(tab => {
if (!tab.url) return false;
return supportedDomains.some(domain => tab.url.includes(domain));
});
console.log(`BACKGROUND: Found ${matchingTabs.length} tabs matching supported domains`);
if (matchingTabs.length > 0) {
const activeMatchingTabs = matchingTabs.filter(tab => tab.active);
const targetTab = activeMatchingTabs.length > 0 ? activeMatchingTabs[0] : matchingTabs[0];
console.log(`BACKGROUND: Selected tab ${targetTab.id} (${targetTab.url})`);
activeTabId = targetTab.id; // Update global activeTabId
if (justFinding) {
return targetTab.id;
}
console.warn("BACKGROUND: findAndSendToSuitableTab called with justFinding=false. Sending is now handled by caller.");
return targetTab.id;
} else {
console.error("BACKGROUND: Could not find any tabs matching supported domains.");
return null;
}
} catch (error) {
console.error("BACKGROUND: Error finding suitable tab:", error);
return null;
}
}
// Process the next request in the queue
function processNextRequest() {
console.log("BACKGROUND: Processing next request, queue length:", pendingRequests.length);
if (processingRequest && pendingRequests.length > 0) {
console.log("BACKGROUND: Still processing a request, deferring processNextRequest call.");
return;
}
if (pendingRequests.length > 0) {
const nextCommand = pendingRequests.shift();
console.log("BACKGROUND: Processing next command from queue:", nextCommand);
// Ensure details are stored if this came from the pendingRequests queue
// (though ideally they are stored when initially received from server)
if (!pendingRequestDetails.has(nextCommand.requestId) && nextCommand.message !== undefined) {
pendingRequestDetails.set(nextCommand.requestId, { messageContent: nextCommand.message }); // Use messageContent
let preview = typeof nextCommand.message === 'string' ? `"${nextCommand.message.substring(0,30)}..."` : `Type: ${typeof nextCommand.message}`;
console.log(`BACKGROUND: Stored details (messageContent) for queued requestId: ${nextCommand.requestId} (Message: ${preview}) while processing queue.`);
}
processingRequest = true;
lastRequestId = nextCommand.requestId;
// Add a delay before forwarding the command
setTimeout(() => {
forwardCommandToContentScript({
action: "SEND_CHAT_MESSAGE",
requestId: nextCommand.requestId,
messageContent: nextCommand.message,
settings: nextCommand.settings,
lastProcessedText: lastSuccessfullyProcessedMessageText // Pass the text of the last successfully processed message
});
}, 500); // 500ms delay
} else {
console.log("BACKGROUND: No pending requests to process.");
}
}
// Helper function to check if a URL is supported by a given provider
// This might need to be more sophisticated if provider domains are complex
function isUrlSupportedByProvider(url, providerName) {
// This function would need access to the provider definitions or a shared config
// For AIStudioProvider:
if (providerName === "AIStudioProvider") {
return url.includes("aistudio.google.com");
}
// For GeminiProvider:
if (providerName === "GeminiProvider") {
return url.includes("gemini.google.com");
}
// For ChatGPTProvider:
if (providerName === "ChatGptProvider") { // Match the casing used by the provider's .name property
return url.includes("chatgpt.com");
}
// For ClaudeProvider:
if (providerName === "ClaudeProvider") { // Match the casing used by the provider's .name property
return url.includes("claude.ai");
}
// Add other providers if necessary
console.warn(BG_LOG_PREFIX, `isUrlSupportedByProvider: Unknown providerName '${providerName}'`);
return false;
}
// Listen for tab updates
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url) {
const isSupportedDomain = supportedDomains.some(domain => tab.url.includes(domain));
if (isSupportedDomain) {
console.log(`BACKGROUND: A supported tab ${tabId} (${tab.url}) was updated. Checking if it should be the active tab.`);
// Potentially update activeTabId, but be careful if multiple supported tabs are open.
// The existing logic for activeTabId update via messages from content script might be more reliable.
// For now, let's ensure it's set if it's the *only* active one or becomes active.
if (tab.active || !activeTabId) {
// Check if this tab is actually one of the supported types before making it active
// This is a bit redundant with supportedDomains check but good for clarity
const currentProvider = providerUtils.getProviderForUrl(tab.url); // Assuming providerUtils is accessible or we have a similar utility
if (currentProvider) {
activeTabId = tabId;
console.log(`BACKGROUND: Set ${tabId} (${tab.url}) as the active tab.`);
}
}
}
}
// Handle debugger re-attachment on URL changes for already debugged tabs
const attachmentDetails = debuggerAttachedTabs.get(tabId);
if (attachmentDetails && attachmentDetails.isAttached && changeInfo.url && tab && tab.url) {
// changeInfo.url is the old URL, tab.url is the new one
console.log(BG_LOG_PREFIX, `Tab ${tabId} updated. Old URL: ${changeInfo.url}, New URL: ${tab.url}. Checking debugger status.`);
const providerStillValidForNewUrl = isUrlSupportedByProvider(tab.url, attachmentDetails.providerName);
if (providerStillValidForNewUrl) {
console.log(BG_LOG_PREFIX, `Tab ${tabId} URL changed to ${tab.url}. Provider ${attachmentDetails.providerName} still valid. Re-initiating debugger attachment.`);
const oldProviderName = attachmentDetails.providerName;
const oldPatterns = attachmentDetails.patterns; // These patterns were from the content script for the *domain*
// Detach first to ensure a clean state, then re-attach.
// The 'isAttached' flag in attachmentDetails will be set to false by detachDebugger.
await detachDebugger(tabId);
// Check if tab still exists (it should, as we are in its onUpdated event)
try {
const updatedTabInfo = await chrome.tabs.get(tabId);
if (updatedTabInfo) {
console.log(BG_LOG_PREFIX, `Proactively re-attaching debugger to ${tabId} (${updatedTabInfo.url}) with provider ${oldProviderName}.`);
// Content script should send SET_DEBUGGER_TARGETS on its re-initialization.
// However, a proactive re-attachment can be beneficial.
// The patterns might need to be re-fetched if they are URL-specific beyond the domain.
// For now, using oldPatterns, assuming they are domain-level.
await attachDebuggerAndEnableFetch(tabId, oldProviderName, oldPatterns);
}
} catch (error) {
console.warn(BG_LOG_PREFIX, `Error getting tab info for ${tabId} during re-attachment attempt:`, error.message);
}
} else {
console.log(BG_LOG_PREFIX, `Tab ${tabId} URL changed to ${tab.url}. Provider ${attachmentDetails.providerName} no longer valid or URL not supported by provider. Detaching debugger.`);
await detachDebugger(tabId);
}
} else if (attachmentDetails && attachmentDetails.isAttached && changeInfo.status === 'loading' && tab && tab.url && !changeInfo.url) {
// Sometimes URL change is only visible when status is 'loading' and tab.url is the new one.
// This is a more aggressive check.
const newUrl = tab.url;
console.log(BG_LOG_PREFIX, `Tab ${tabId} is loading new URL: ${newUrl}. Checking debugger status.`);
const providerStillValidForNewUrl = isUrlSupportedByProvider(newUrl, attachmentDetails.providerName);
if (!providerStillValidForNewUrl) {
console.log(BG_LOG_PREFIX, `Tab ${tabId} loading new URL ${newUrl}. Provider ${attachmentDetails.providerName} may no longer be valid. Detaching.`);
await detachDebugger(tabId);
}
// If provider is still valid, we'll let the 'complete' status handler above deal with re-attachment if needed,
// or rely on content script sending SET_DEBUGGER_TARGETS.
}
});
// Listen for tab updates to inject the WebSocket proxy if needed
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'loading' && tab.url && tab.url.includes('chatgpt.com')) {
chrome.scripting.executeScript({
target: { tabId: tabId },
files: ['providers/websocket-proxy.js'],
world: 'MAIN'
});
}
});
// Listen for messages from Content Scripts and Popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log("BACKGROUND: Received message:", message.type || message.action, "from tabId:", sender.tab ? sender.tab.id : 'popup/unknown');
if (message.type === "CONTENT_SCRIPT_LOADED") {
console.log("BACKGROUND: Content script loaded successfully on:", message.url, "hostname:", message.hostname);
sendResponse({ success: true, message: "Content script load acknowledged" });
return true;
}
if (sender.tab && sender.tab.id) {
activeTabId = sender.tab.id; // User's original logic for activeTabId
console.log(`BACKGROUND: Updated activeTabId to ${activeTabId} from sender`);
}
if (message.type === "SET_DEBUGGER_TARGETS") {
if (sender.tab && sender.tab.id) {
const tabId = sender.tab.id;
console.log(BG_LOG_PREFIX, `SET_DEBUGGER_TARGETS for tab ${tabId}, provider: ${message.providerName}, patterns:`, message.patterns);
attachDebuggerAndEnableFetch(tabId, message.providerName, message.patterns);
sendResponse({ status: "Debugger attachment initiated" });
} else {
console.error(BG_LOG_PREFIX, "SET_DEBUGGER_TARGETS message received without valid sender.tab.id");
sendResponse({ status: "Error: Missing tabId" });
}
return true;
}
else if (message.type === "CHAT_RELAY_READY") {
console.log(`BACKGROUND: Content script ready in ${message.chatInterface} on tab ${sender.tab ? sender.tab.id : 'unknown'}`);
if (sender.tab && sender.tab.id) activeTabId = sender.tab.id;
sendResponse({ success: true });
return true; // Indicate that sendResponse might be used (even if synchronously here)
} else if (message.action === "RESPONSE_CAPTURED") {
console.log(`BACKGROUND: Received captured response (OLD DOM METHOD) from content script on tab ${sender.tab ? sender.tab.id : 'unknown'} Request ID: ${message.requestId}`);
if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
console.log("BACKGROUND: Forwarding (OLD DOM) response to relay server:", message.response);
relaySocket.send(JSON.stringify({
type: "CHAT_RESPONSE",
requestId: message.requestId,
response: message.response,
isFinal: true
}));
sendResponse({ success: true });
if (lastRequestId === message.requestId) {
processingRequest = false;
console.log("BACKGROUND: Reset processingRequest after (OLD DOM) RESPONSE_CAPTURED.");
processNextRequest();
}
} else {
console.error("BACKGROUND: Relay WS not connected, cannot forward (OLD DOM) response");
sendResponse({ success: false, error: "Relay WebSocket not connected" });
if (lastRequestId === message.requestId) {
processingRequest = false;
}
}
return true;
} else if (message.action === "GET_CONNECTION_STATUS") {
const isConnected = relaySocket && relaySocket.readyState === WebSocket.OPEN;
sendResponse({ connected: isConnected });
return true; // Indicate that sendResponse might be used
} else if (message.type === "CHAT_RESPONSE_FROM_DOM") {
console.log(`BACKGROUND: Received CHAT_RESPONSE_FROM_DOM from tab ${sender.tab ? sender.tab.id : 'unknown'} for requestId ${message.requestId}`);
const tabId = sender.tab ? sender.tab.id : null;
const tabInfo = tabId ? debuggerAttachedTabs.get(tabId) : null;
if (tabInfo && tabInfo.lastKnownRequestId === message.requestId && processingRequest) {
if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
relaySocket.send(JSON.stringify({
type: "CHAT_RESPONSE_CHUNK",
requestId: message.requestId,
chunk: message.text,
isFinal: message.isFinal !== undefined ? message.isFinal : true
}));
relaySocket.send(JSON.stringify({
type: "CHAT_RESPONSE_STREAM_ENDED",
requestId: message.requestId
}));
console.log(`BACKGROUND: Sent CHAT_RESPONSE_CHUNK (from DOM) and _STREAM_ENDED for app requestId: ${message.requestId}`);
sendResponse({ success: true, message: "DOM Response forwarded to relay." });
} else {
console.error(`BACKGROUND: Relay WS not connected, cannot send DOM-captured response for requestId: ${message.requestId}`);
sendResponse({ success: false, error: "Relay WebSocket not connected." });
}
// Finalize this request processing
processingRequest = false;
if (tabInfo) tabInfo.lastKnownRequestId = null; // Clear for this specific tab op
console.log(`BACKGROUND: Reset processingRequest. Cleared lastKnownRequestId for tab ${tabId} after DOM response.`);
processNextRequest();
} else {
console.warn(`BACKGROUND: Mismatched requestId or not processing for CHAT_RESPONSE_FROM_DOM. Current lastKnownRequestId: ${tabInfo ? tabInfo.lastKnownRequestId : 'N/A'}, processingRequest: ${processingRequest}, msg RequestId: ${message.requestId}`);
sendResponse({ success: false, error: "Mismatched requestId or not processing." });
}
return true;
} else if (message.type === "CHAT_RESPONSE_FROM_DOM_FAILED") {
console.error(`BACKGROUND: Received CHAT_RESPONSE_FROM_DOM_FAILED from tab ${sender.tab ? sender.tab.id : 'unknown'} for requestId ${message.requestId}: ${message.error}`);
const tabId = sender.tab ? sender.tab.id : null;
const tabInfo = tabId ? debuggerAttachedTabs.get(tabId) : null;
if (tabInfo && tabInfo.lastKnownRequestId === message.requestId && processingRequest) {
if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
relaySocket.send(JSON.stringify({
type: "CHAT_RESPONSE_ERROR",
requestId: message.requestId,
error: `Failed to capture response from DOM on tab ${tabId}: ${message.error}`
}));
}
sendResponse({ success: true, message: "DOM failure noted and error sent to relay." });
// Finalize this request processing
processingRequest = false;
if (tabInfo) tabInfo.lastKnownRequestId = null;
console.log(`BACKGROUND: Reset processingRequest. Cleared lastKnownRequestId for tab ${tabId} after DOM failure.`);
processNextRequest();
} else {
console.warn(`BACKGROUND: Mismatched requestId or not processing for CHAT_RESPONSE_FROM_DOM_FAILED. Current lastKnownRequestId: ${tabInfo ? tabInfo.lastKnownRequestId : 'N/A'}, processingRequest: ${processingRequest}, msg RequestId: ${message.requestId}`);
sendResponse({ success: false, error: "Mismatched requestId or not processing for DOM failure." });
}
return true;
} else if (message.type === "FINAL_RESPONSE_TO_RELAY") {
console.log(BG_LOG_PREFIX, `[REQ-${message.requestId}] RECEIVED FINAL_RESPONSE_TO_RELAY. FromTab: ${sender.tab ? sender.tab.id : 'N/A'}. HasError: ${!!message.error}. TextLength: ${message.text ? String(message.text).length : 'N/A'}. IsFinal: ${message.isFinal}. FullMsg:`, JSON.stringify(message).substring(0,500));
const tabId = sender.tab ? sender.tab.id : null;
const tabInfo = tabId ? debuggerAttachedTabs.get(tabId) : null;
// Update lastSuccessfullyProcessedMessageText regardless of current processing state,
// as this confirms a message text was fully processed by the AI.
const details = pendingRequestDetails.get(message.requestId);
if (details) {
if (typeof details.messageContent === 'string') {
lastSuccessfullyProcessedMessageText = details.messageContent;
console.log(`BACKGROUND: Updated lastSuccessfullyProcessedMessageText to: "${lastSuccessfullyProcessedMessageText.substring(0,50)}..." for completed requestId ${message.requestId}`);
} else {
console.log(`BACKGROUND: RequestId ${message.requestId} (messageContent type: ${typeof details.messageContent}) completed. lastSuccessfullyProcessedMessageText not updated with non-string content.`);
}
pendingRequestDetails.delete(message.requestId);
} else {
console.warn(`BACKGROUND: Received FINAL_RESPONSE_TO_RELAY for unknown requestId ${message.requestId} (not in pendingRequestDetails). Cannot update lastSuccessfullyProcessedMessageText accurately.`);
}
// Check if this is the request we are currently processing for state reset
if (processingRequest && lastRequestId === message.requestId) {
if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
if (message.error) { // Check if content.js sent an error (e.g., response too large)
console.error(BG_LOG_PREFIX, `Content script reported an error for requestId ${message.requestId}: ${message.error}`);
try {
relaySocket.send(JSON.stringify({
type: "CHAT_RESPONSE_ERROR",
requestId: message.requestId,
error: message.error
}));
console.log(BG_LOG_PREFIX, `Sent CHAT_RESPONSE_ERROR to server for requestId ${message.requestId} due to content script error.`);
sendResponse({ success: true, message: "Error reported by content script sent to relay." });
} catch (e) {
console.error(BG_LOG_PREFIX, `Error sending CHAT_RESPONSE_ERROR to relay for requestId ${message.requestId}:`, e);
sendResponse({ success: false, error: `Error sending CHAT_RESPONSE_ERROR to relay: ${e.message}` });
}
} else { // No error from content.js, proceed to send data
try {
let responseText = message.text || "";
// Decode text if it was encoded by content script
if (message.encoded) {
responseText = decodeURIComponent(responseText);
}
console.log(BG_LOG_PREFIX, `Attempting to send FINAL CHAT_RESPONSE_CHUNK for requestId ${message.requestId}. Data length: ${responseText.length}`);
relaySocket.send(JSON.stringify({
type: "CHAT_RESPONSE_CHUNK",
requestId: message.requestId,
chunk: responseText,
isFinal: true
}));
console.log(BG_LOG_PREFIX, `Attempting to send CHAT_RESPONSE_STREAM_ENDED for requestId ${message.requestId}`);
relaySocket.send(JSON.stringify({
type: "CHAT_RESPONSE_STREAM_ENDED",
requestId: message.requestId
}));
console.log(BG_LOG_PREFIX, `Successfully sent FINAL CHAT_RESPONSE_CHUNK and _STREAM_ENDED for app requestId: ${message.requestId} to relaySocket.`);
sendResponse({ success: true, message: "Final response sent to relay." });
} catch (e) {
console.error(BG_LOG_PREFIX, `Error during relaySocket.send() for FINAL response (requestId ${message.requestId}):`, e);
sendResponse({ success: false, error: `Error sending final response to relay: ${e.message}` });
}
}
} else {
console.error(BG_LOG_PREFIX, `Relay WS not OPEN (state: ${relaySocket ? relaySocket.readyState : 'null'}), cannot send final response/error for app requestId: ${message.requestId}`);
sendResponse({ success: false, error: "Relay WebSocket not connected." });
}
// Finalize this request processing
console.log(BG_LOG_PREFIX, `Processing complete for command with app requestId: ${message.requestId} on tab ${tabId}`);
processingRequest = false;
if (tabInfo) tabInfo.lastKnownRequestId = null;
console.log(BG_LOG_PREFIX, `Reset processingRequest. Cleared lastKnownRequestId for tab ${tabId}.`);
processNextRequest();
} else {
console.warn(`BACKGROUND: Received FINAL_RESPONSE_TO_RELAY for requestId ${message.requestId}, but not currently processing it (current: ${lastRequestId}, processing: ${processingRequest}). Ignoring.`);
sendResponse({ success: false, error: "Request ID mismatch or not processing." });
}
return true; // Indicate async response potentially
} else if (message.type === "DUPLICATE_MESSAGE_HANDLED") {
console.log(`BACKGROUND: Content script handled requestId ${message.requestId} as a duplicate of text: "${message.originalText ? message.originalText.substring(0,50) : 'N/A'}..."`);
// Update last successfully processed text because this text was confirmed as a duplicate of it.
lastSuccessfullyProcessedMessageText = message.originalText;
pendingRequestDetails.delete(message.requestId); // Clean up details map
console.log(`BACKGROUND: Updated lastSuccessfullyProcessedMessageText (due to duplicate) to: "${lastSuccessfullyProcessedMessageText ? lastSuccessfullyProcessedMessageText.substring(0,50) : 'N/A'}..."`);
if (processingRequest && lastRequestId === message.requestId) {
if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
relaySocket.send(JSON.stringify({
type: "CHAT_RESPONSE",
requestId: message.requestId,
response: `[ChatRelay Extension] Request to send duplicate message ("${message.originalText ? message.originalText.substring(0,100) : 'N/A'}") was detected and cleared from input. No message sent to AI.`,
isFinal: true
}));
console.log(`BACKGROUND: Sent CHAT_RESPONSE (for duplicate) to server for requestId: ${message.requestId}.`);
} else {
console.error(`BACKGROUND: Relay WS not OPEN, cannot send CHAT_RESPONSE (for duplicate) for requestId: ${message.requestId}.`);
}
processingRequest = false;
// lastRequestId remains, it's the ID of the last command *received*
// currentRequestText (if used) would be nulled here.
const tabInfo = sender.tab ? debuggerAttachedTabs.get(sender.tab.id) : null;
if (tabInfo && tabInfo.lastKnownRequestId === message.requestId) {
tabInfo.lastKnownRequestId = null;
}
console.log(`BACKGROUND: Reset processingRequest after DUPLICATE_MESSAGE_HANDLED for requestId: ${message.requestId}.`);
processNextRequest();
} else {
console.warn(`BACKGROUND: Received DUPLICATE_MESSAGE_HANDLED for requestId ${message.requestId}, but not currently processing it or ID mismatch. Current lastRequestId: ${lastRequestId}, processing: ${processingRequest}. Still updated LSPMT.`);
// If it was an older request, its details are cleaned, LSPMT updated. Server informed if possible.
if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
relaySocket.send(JSON.stringify({
type: "CHAT_RESPONSE",
requestId: message.requestId,
response: `[ChatRelay Extension] An older/superseded request (ID: ${message.requestId}, Text: "${message.originalText ? message.originalText.substring(0,100) : 'N/A'}") was handled as a duplicate.`,
isFinal: true
}));
}
}
sendResponse({ success: true, message: "Duplicate handling acknowledged by background." });
return true;
} else if (message.type === "USER_STOP_REQUEST") {
const requestIdToStop = message.requestId;
console.log(`BACKGROUND: Received USER_STOP_REQUEST for requestId: ${requestIdToStop}`);
let responseSent = false; // To ensure sendResponse is called once
// Case 1: The request to stop is the currently processing one.
if (processingRequest && lastRequestId === requestIdToStop) {
console.log(`BACKGROUND: Initiating stop for currently processing request: ${lastRequestId}. Content script will send FINAL_RESPONSE_TO_RELAY.`);
if (activeTabId) {
chrome.tabs.sendMessage(activeTabId, {
action: "STOP_STREAMING",
requestId: lastRequestId
}, response => {
if (chrome.runtime.lastError) {
console.error(`BACKGROUND: Error sending STOP_STREAMING to tab ${activeTabId} for requestId ${lastRequestId}:`, chrome.runtime.lastError.message);
} else {
console.log(`BACKGROUND: Sent STOP_STREAMING to tab ${activeTabId} for requestId ${lastRequestId}. Content script ack:`, response);
}
});
} else {
console.warn(`BACKGROUND: Cannot send STOP_STREAMING for currently processing requestId ${lastRequestId}, activeTabId is null. This request might not be properly finalized by the provider.`);
// If no active tab, we can't tell content.js to stop.
// We should still inform the relay and clean up what we can,
// though the provider state might remain for this request.
if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
relaySocket.send(JSON.stringify({
type: "CHAT_RESPONSE_ERROR",
requestId: lastRequestId,
error: "Request cancelled by user (no active tab to signal provider)."
}));
}
// Since we can't rely on FINAL_RESPONSE_TO_RELAY, we have to clean up here.
processingRequest = false;
pendingRequestDetails.delete(lastRequestId);
// lastSuccessfullyProcessedMessageText = null; // Consider if this should be reset
console.log(`BACKGROUND: Forcefully reset processingRequest for ${lastRequestId} due to USER_STOP_REQUEST with no active tab.`);
processNextRequest(); // Attempt to process next
}
// Inform relay server about cancellation (can be done early)
if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
relaySocket.send(JSON.stringify({
type: "CHAT_RESPONSE_ERROR", // Or a new type like "USER_CANCELLED_REQUEST"
requestId: lastRequestId, // Use lastRequestId as it's the one being processed
error: "Request cancelled by user."
}));
console.log(`BACKGROUND: Sent CHAT_RESPONSE_ERROR (user cancelled) to server for currently processing requestId: ${lastRequestId}.`);
}
// IMPORTANT: Do NOT set processingRequest = false or clear lastRequestId details here.
// Let the FINAL_RESPONSE_TO_RELAY (triggered by provider.stopStreaming) handle the final state cleanup.
sendResponse({ success: true, message: `Stop initiated for currently processing request ${lastRequestId}. Waiting for finalization from content script.` });
responseSent = true;
// Case 2: The request to stop is in the pending queue (not actively processing).
} else {
const initialQueueLength = pendingRequests.length;
pendingRequests = pendingRequests.filter(req => req.requestId !== requestIdToStop);
if (pendingRequests.length < initialQueueLength) {
console.log(`BACKGROUND: Removed requestId ${requestIdToStop} from pendingRequests queue.`);
pendingRequestDetails.delete(requestIdToStop); // Clean up details for the queued item
if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
relaySocket.send(JSON.stringify({
type: "CHAT_RESPONSE_ERROR",
requestId: requestIdToStop,
error: `Request ${requestIdToStop} cancelled by user while in queue.`
}));
console.log(`BACKGROUND: Sent CHAT_RESPONSE_ERROR (user cancelled in queue) to server for requestId: ${requestIdToStop}.`);
}
if (!responseSent) sendResponse({ success: true, message: `Request ${requestIdToStop} removed from queue.` });
} else if (message.type === 'WEBSOCKET_MESSAGE') {
if (activeTabId) {
chrome.tabs.sendMessage(activeTabId, {
action: 'HANDLE_WEBSOCKET_DATA',
requestId: lastRequestId,
data: message.data
});
}
responseSent = true;
}
}
if (!responseSent) {
console.warn(`BACKGROUND: USER_STOP_REQUEST for ${requestIdToStop}, but it was not actively processing nor found in the pending queue. Current active: ${lastRequestId}, processing: ${processingRequest}`);
sendResponse({ success: false, error: "Request not found processing or in queue." });
}
return true;
}
// IMPORTANT: Add other top-level else if (message.action === "SAVE_SETTINGS") etc. here if they exist outside this snippet
});
// Listen for storage changes to update the server URL
chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace === 'sync') {
let needsReconnect = false;
if (changes.serverHost || changes.serverPort || changes.serverProtocol) {
needsReconnect = true;
}
if (needsReconnect) {
console.log("BACKGROUND: Server settings changed, reconnecting...");
if (relaySocket) {
relaySocket.close();
} else {
loadSettingsAndConnect();
}
}
}
});
// Initial setup
loadSettingsAndConnect();
// Placeholder for providerUtils if it's not globally available from another script.
// In a real extension, this would likely be imported or part of a shared module.
const providerUtils = {
_providers: {}, // providerName -> { instance, domains }
registerProvider: function(name, domains, instance) {
this._providers[name] = { instance, domains };
// console.log(BG_LOG_PREFIX, `Provider registered in background (simulated): ${name}`);
},
getProviderForUrl: function(url) {
for (const name in this._providers) {
if (this._providers[name].domains.some(domain => url.includes(domain))) {
return this._providers[name].instance;
}
}
return null;
},
// Simulate AIStudioProvider registration for isUrlSupportedByProvider
// This would normally happen if provider-utils.js was also loaded in background context
// or if this info was passed/stored differently.
_initializeSimulatedProviders: function() {
this.registerProvider("AIStudioProvider", ["aistudio.google.com"], { name: "AIStudioProvider" });
this.registerProvider("GeminiProvider", ["gemini.google.com"], { name: "GeminiProvider" });
this.registerProvider("ChatGPTProvider", ["chatgpt.com"], { name: "ChatGPTProvider" });
this.registerProvider("ClaudeProvider", ["claude.ai"], { name: "ClaudeProvider" });
}
};
providerUtils._initializeSimulatedProviders(); // Call to populate for the helper
console.log("BACKGROUND: AI Chat Relay: Background Service Worker started.");
// ===== DEBUGGER LOGIC =====
async function attachDebuggerAndEnableFetch(tabId, providerName, patterns) {
if (!tabId || !patterns || patterns.length === 0) {
console.error(BG_LOG_PREFIX, `attachDebuggerAndEnableFetch: Invalid parameters for tab ${tabId}. Patterns:`, patterns);
return;
}
const debuggee = { tabId: tabId };
const requiredVersion = "1.3";
try {
const existingAttachment = debuggerAttachedTabs.get(tabId);
if (existingAttachment && existingAttachment.isAttached) {
console.log(BG_LOG_PREFIX, `Already attached to tab ${tabId}. Ensuring Fetch is enabled with latest patterns.`);
} else {
console.log(BG_LOG_PREFIX, `Attempting to attach debugger to tab ${tabId}`);
await new Promise((resolve, reject) => {
chrome.debugger.attach(debuggee, requiredVersion, () => {
if (chrome.runtime.lastError) {
console.error(BG_LOG_PREFIX, `Error attaching debugger to tab ${tabId}:`, chrome.runtime.lastError.message);
debuggerAttachedTabs.delete(tabId);
return reject(chrome.runtime.lastError);
}
console.log(BG_LOG_PREFIX, `Successfully attached debugger to tab ${tabId}`);
debuggerAttachedTabs.set(tabId, {
providerName: providerName,
patterns: patterns,
isFetchEnabled: false,
isAttached: true,
lastKnownRequestId: null
});
resolve();
});
});
}
const currentTabDataForPatterns = debuggerAttachedTabs.get(tabId);
if (currentTabDataForPatterns) {
currentTabDataForPatterns.patterns = patterns;
currentTabDataForPatterns.providerName = providerName;
}
// Explicitly disable Fetch first, in case it's in a weird state
try {
console.log(BG_LOG_PREFIX, `Attempting to disable Fetch domain for tab ${tabId} before re-enabling.`);
await new Promise((resolve, reject) => {
chrome.debugger.sendCommand(debuggee, "Fetch.disable", {}, (disableResponse) => {
if (chrome.runtime.lastError) {
console.warn(BG_LOG_PREFIX, `Warning/Error disabling Fetch for tab ${tabId}:`, chrome.runtime.lastError.message);
// Don't reject, just log, as we want to proceed to enable anyway
} else {
console.log(BG_LOG_PREFIX, `Successfully disabled Fetch for tab ${tabId}. Response:`, disableResponse);
}
resolve();
});
});
} catch (e) {
console.warn(BG_LOG_PREFIX, `Exception during explicit Fetch.disable for tab ${tabId}:`, e);
}
console.log(BG_LOG_PREFIX, `Enabling Fetch domain for tab ${tabId} with patterns:`, patterns);
await new Promise((resolve, reject) => {
const fetchEnableParams = { patterns: patterns };
console.log(BG_LOG_PREFIX, `Preparing to call Fetch.enable for tab ${tabId} with params:`, JSON.stringify(fetchEnableParams));
chrome.debugger.sendCommand(debuggee, "Fetch.enable", fetchEnableParams, (response) => {
if (chrome.runtime.lastError) {
console.error(BG_LOG_PREFIX, `Error enabling Fetch for tab ${tabId}:`, chrome.runtime.lastError.message);
return reject(chrome.runtime.lastError);
}
console.log(BG_LOG_PREFIX, `Successfully enabled Fetch for tab ${tabId}. Response from Fetch.enable:`, response);
const currentTabData = debuggerAttachedTabs.get(tabId);
if (currentTabData) {
currentTabData.isFetchEnabled = true;
}
resolve();
});
});
} catch (error) {
console.error(BG_LOG_PREFIX, `Error in attachDebuggerAndEnableFetch for tab ${tabId}:`, error);
// Ensure flags are reset on error
const currentTabDataOnError = debuggerAttachedTabs.get(tabId);
if (currentTabDataOnError) {
currentTabDataOnError.isFetchEnabled = false; // Ensure this is false
// if (!currentTabDataOnError.isAttached) { // Commenting out: if attach failed earlier, it's already deleted. If attach succeeded but enable failed, we want to keep attachment info.
// debuggerAttachedTabs.delete(tabId);
// }
}
}
}
async function detachDebugger(tabId) {
if (!tabId) return;
const attachmentDetails = debuggerAttachedTabs.get(tabId);
if (attachmentDetails && attachmentDetails.isAttached) {
console.log(BG_LOG_PREFIX, `Attempting to detach debugger from tab ${tabId}`);
try {
await new Promise((resolve, reject) => {
chrome.debugger.detach({ tabId: tabId }, () => {
if (chrome.runtime.lastError) {
console.error(BG_LOG_PREFIX, `Error detaching debugger from tab ${tabId}:`, chrome.runtime.lastError.message);
} else {
console.log(BG_LOG_PREFIX, `Successfully detached debugger from tab ${tabId}`);
}
debuggerAttachedTabs.delete(tabId);
resolve(); // Resolve even if detach had an error, as we've cleaned up map
});
});
} catch (error) { // Catch errors from the Promise constructor itself or unhandled rejections
console.error(BG_LOG_PREFIX, `Exception during detach for tab ${tabId}:`, error);
debuggerAttachedTabs.delete(tabId); // Ensure cleanup
}
} else {
// If not attached or no details, still ensure it's not in the map
debuggerAttachedTabs.delete(tabId);
}
}
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
console.log(BG_LOG_PREFIX, `Tab ${tabId} removed. Detaching debugger if attached.`);
detachDebugger(tabId);
});
chrome.runtime.onSuspend.addListener(() => {
console.log(BG_LOG_PREFIX, "Extension suspending. Detaching all debuggers.");
for (const tabId of debuggerAttachedTabs.keys()) {
detachDebugger(tabId);
}
});
chrome.debugger.onDetach.addListener((source, reason) => {
if (source.tabId) {
console.log(BG_LOG_PREFIX, `Debugger detached from tab ${source.tabId} (e.g. DevTools closed) due to: ${reason}. Cleaning up.`);
debuggerAttachedTabs.delete(source.tabId);
}
});
chrome.debugger.onEvent.addListener((debuggeeId, message, params) => {
if (!debuggeeId.tabId) return;
const tabId = debuggeeId.tabId;
const tabInfo = debuggerAttachedTabs.get(tabId);
// DEVELOPER ACTION: This parsing function needs to be robustly implemented
// based on consistent observation of the AI Studio response structure.
function parseAiStudioResponse(jsonString) {
try {
const parsed = JSON.parse(jsonString);
let combinedText = "";
if (Array.isArray(parsed)) {
for (const topLevelEntry of parsed) {
if (topLevelEntry && topLevelEntry[0] && topLevelEntry[0][2] && Array.isArray(topLevelEntry[0][2])) {
for (const candidateBlock of topLevelEntry[0][2]) {
if (candidateBlock && candidateBlock[0] && candidateBlock[0][0] && candidateBlock[0][0][0] && Array.isArray(candidateBlock[0][0][0][0])) {
for (const innerMostArray of candidateBlock[0][0][0][0]) {
if (Array.isArray(innerMostArray) && innerMostArray.length > 1 && typeof innerMostArray[1] === 'string') {
const textSegment = innerMostArray[1];
// Basic heuristic to filter out "thought process" or similar meta-commentary.
// This will need refinement based on actual response variations.
if (!textSegment.toLowerCase().includes("thinking process") &&
!textSegment.toLowerCase().includes("thought process") &&
!textSegment.startsWith("1.") && // Avoid numbered list from thoughts
!textSegment.startsWith("2.") &&
!textSegment.startsWith("3.") &&
!textSegment.startsWith("4.") &&
!textSegment.startsWith("5.") &&
!textSegment.startsWith("6.") &&
textSegment.trim() !== "**") {
combinedText += textSegment; // Concatenate, newlines are part of the text
}
}
}
}
}
}
}
}
// Cleanup common markdown/formatting that might not be desired for relay
let cleanedMessage = combinedText.replace(/\*\*/g, "").replace(/\\n/g, "\n").replace(/\n\s*\n/g, '\n').trim();
if (cleanedMessage) {
console.log(BG_LOG_PREFIX, "Parsed AI Studio response to (first 100 chars):", cleanedMessage.substring(0, 100));
return cleanedMessage;
} else {
console.warn(BG_LOG_PREFIX, "Parsing AI Studio response yielded empty text. Original (first 200 chars):", jsonString.substring(0,200));
}
} catch (e) {
console.error(BG_LOG_PREFIX, "Error parsing AI Studio response JSON:", e, "Original string (first 200 chars):", jsonString.substring(0, 200));
}
console.warn(BG_LOG_PREFIX, "Failed to extract clean text from AI Studio response, returning raw data.");
return jsonString;
}
if (message === "Fetch.requestPaused") {
// Log immediately upon entering Fetch.requestPaused
console.log(BG_LOG_PREFIX, `ENTERED Fetch.requestPaused for tab ${tabId}, URL: ${params.request.url}, Debugger NetworkRequestId: ${params.requestId}, Stage: ${params.responseErrorReason || params.requestStage}, Headers:`, params.request.headers);
if (!tabInfo) {
console.warn(BG_LOG_PREFIX, `Fetch.requestPaused for tab ${tabId} but NO tabInfo found in debuggerAttachedTabs. Continuing request.`);
chrome.debugger.sendCommand(debuggeeId, "Fetch.continueRequest", { requestId: params.requestId });
return;
}
if (!tabInfo.isFetchEnabled) {
console.log(BG_LOG_PREFIX, `Tab ${tabId} not actively monitored or Fetch not enabled (tabInfo.isFetchEnabled is false). Continuing request.`);
chrome.debugger.sendCommand(debuggeeId, "Fetch.continueRequest", { requestId: params.requestId });
return;
}
// This is the application's requestId (e.g., 0, 1, 2...)
// It's CRITICAL that tabInfo.lastKnownRequestId is correctly set when the command was initially forwarded.
const currentOperationRequestId = tabInfo.lastKnownRequestId;
if (currentOperationRequestId === null || currentOperationRequestId === undefined) {
console.warn(BG_LOG_PREFIX, `Fetch.requestPaused for tab ${tabId} (URL: ${params.request.url}) but tabInfo.lastKnownRequestId is null/undefined. Cannot associate with an operation. Continuing request.`);
chrome.debugger.sendCommand(debuggeeId, "Fetch.continueRequest", { requestId: params.requestId });
return;
}
// Check if the URL matches any of the patterns for this tab
const matchesPattern = tabInfo.patterns.some(p => {
try {
// Ensure the pattern is treated as a string and properly escaped for regex construction.
// Basic wildcard to regex: replace * with .*? (non-greedy)
const patternRegex = new RegExp(String(p.urlPattern).replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\*/g, '.*?'));
return patternRegex.test(params.request.url);
} catch (e) {
console.error(BG_LOG_PREFIX, `Error creating regex from pattern '${p.urlPattern}':`, e);
return false;
}
});
if (!matchesPattern) {
// console.log(BG_LOG_PREFIX, `Fetch.requestPaused for URL ${params.request.url} did NOT match stored patterns for tab ${tabId}. Continuing request. Patterns:`, tabInfo.patterns);
chrome.debugger.sendCommand(debuggeeId, "Fetch.continueRequest", { requestId: params.requestId });
return;
}
console.log(BG_LOG_PREFIX, `Fetch.requestPaused for URL ${params.request.url} MATCHED pattern. App RequestId: ${currentOperationRequestId}. Proceeding...`);
console.log(BG_LOG_PREFIX, `[DEBUG_STAGE_STATUS] For matched URL ${params.request.url}, appReqId: ${currentOperationRequestId}, debugger netReqId: ${params.requestId}:`);
console.log(BG_LOG_PREFIX, ` - params.requestStage: ${params.requestStage}`);
console.log(BG_LOG_PREFIX, ` - params.responseStatusCode: ${params.responseStatusCode}`);
console.log(BG_LOG_PREFIX, ` - params.responseErrorReason: ${params.responseErrorReason}`);
// Additional pre-condition logging
console.log(BG_LOG_PREFIX, `[PRE-CONDITION CHECK] For AppReqId: ${currentOperationRequestId}, Debugger netReqId: ${params.requestId}`);
console.log(BG_LOG_PREFIX, ` - tabInfo (raw):`, tabInfo); // Log the raw tabInfo object
console.log(BG_LOG_PREFIX, ` - tabInfo.patterns:`, tabInfo ? JSON.stringify(tabInfo.patterns) : "tabInfo is null");
console.log(BG_LOG_PREFIX, ` - tabInfo.lastKnownRequestId: ${tabInfo ? tabInfo.lastKnownRequestId : "tabInfo is null"} (should be currentOperationRequestId)`);
console.log(BG_LOG_PREFIX, ` - currentOperationRequestId (derived from tabInfo.lastKnownRequestId): ${currentOperationRequestId}`);
// Scenario 1: Network error before even getting a response status (e.g., DNS failure)
if (params.responseErrorReason) {
console.error(BG_LOG_PREFIX, `Response error for ${params.request.url} (debugger netReqId ${params.requestId}): ${params.responseErrorReason}. AppReqId: ${currentOperationRequestId}.`);
const messageToContent = {
type: "PROVIDER_DEBUGGER_EVENT", // Ensure this type is handled by content.js
detail: {
requestId: currentOperationRequestId,
networkRequestId: params.requestId,
error: `Network error: ${params.responseErrorReason}`,
isFinal: true
}
};
chrome.tabs.sendMessage(tabId, messageToContent, response => {
if (chrome.runtime.lastError) console.error(BG_LOG_PREFIX, `Error sending debugger error event (responseErrorReason) to content script for tab ${tabId}:`, chrome.runtime.lastError.message);
else console.log(BG_LOG_PREFIX, `Sent debugger error event (responseErrorReason) to content script for tab ${tabId}, appReqId: ${currentOperationRequestId}, ack:`, response);
});
chrome.debugger.sendCommand(debuggeeId, "Fetch.continueRequest", { requestId: params.requestId });
return;
}
// Scenario 2: We have response headers (indicated by params.responseStatusCode being present).
if (params.responseStatusCode) {
if (params.responseStatusCode >= 200 && params.responseStatusCode < 300) {
// SUCCESS: We have a 2xx status, attempt to get the response body.
console.log(BG_LOG_PREFIX, `Attempting Fetch.getResponseBody for ${params.request.url}, appReqId: ${currentOperationRequestId}, debugger netReqId: ${params.requestId} (Stage: ${params.requestStage}, Status: ${params.responseStatusCode})`);
chrome.debugger.sendCommand(debuggeeId, "Fetch.getResponseBody", { requestId: params.requestId }, (responseBodyData) => {
let errorMessageForContent = null;
if (chrome.runtime.lastError) {
errorMessageForContent = `Error calling Fetch.getResponseBody: ${chrome.runtime.lastError.message}`;
console.error(BG_LOG_PREFIX, `${errorMessageForContent} for tab ${tabId}, appReqId: ${currentOperationRequestId}, debugger netReqId: ${params.requestId}`);
}
console.log(BG_LOG_PREFIX, `[getResponseBody CB] appReqId: ${currentOperationRequestId}, netReqId: ${params.requestId}. responseBodyData raw:`, responseBodyData);
if (responseBodyData) {
console.log(BG_LOG_PREFIX, `[getResponseBody CB] responseBodyData.body (first 100): ${responseBodyData.body ? String(responseBodyData.body).substring(0,100) : 'N/A'}, .base64Encoded: ${responseBodyData.base64Encoded}`);
}
console.log(BG_LOG_PREFIX, `[getResponseBody CB] errorMessageForContent before check: '${errorMessageForContent}'`);
if (!responseBodyData && !errorMessageForContent) {
errorMessageForContent = "No response body data and no explicit error from getResponseBody.";
console.warn(BG_LOG_PREFIX, `${errorMessageForContent} for tab ${tabId}, appReqId: ${currentOperationRequestId}, debugger netReqId: ${params.requestId}`);
}
console.log(BG_LOG_PREFIX, `[getResponseBody CB] errorMessageForContent AFTER check: '${errorMessageForContent}'`);
// Text encoding fix: properly decode base64-encoded response bodies
let processedData = null;
if (responseBodyData && responseBodyData.body) {
if (responseBodyData.base64Encoded) {
try {
processedData = new TextDecoder('utf-8').decode(Uint8Array.from(atob(responseBodyData.body), c => c.charCodeAt(0)));
} catch (error) {
console.error(BG_LOG_PREFIX, `Error decoding base64 response body for requestId ${params.requestId}:`, error);
processedData = responseBodyData.body; // Fallback to original
}
} else {
processedData = responseBodyData.body;
}
}
const messageToContent = {
type: "PROVIDER_DEBUGGER_EVENT",
detail: {
requestId: currentOperationRequestId,
networkRequestId: params.requestId,
data: processedData,
base64Encoded: false, // Always false since we've decoded it
error: errorMessageForContent,
isFinal: true
}
};
console.log(BG_LOG_PREFIX, `Sending PROVIDER_DEBUGGER_EVENT (body/error) to content script for tab ${tabId}, appReqId: ${currentOperationRequestId}. Error: ${errorMessageForContent}, Data (first 100): ${messageToContent.detail.data ? String(messageToContent.detail.data).substring(0,100) + "..." : "null"}`);
chrome.tabs.sendMessage(tabId, messageToContent, response => {
if (chrome.runtime.lastError) console.error(BG_LOG_PREFIX, `Error sending/acking debugger event (body/error) to content script for tab ${tabId}:`, chrome.runtime.lastError.message);
else console.log(BG_LOG_PREFIX, `Sent debugger event (body/error) to content script for tab ${tabId}, appReqId: ${currentOperationRequestId}, ack:`, response);
});
});
} else { // Non-2xx status code
const httpErrorMessage = `HTTP error ${params.responseStatusCode} for ${params.request.url}`;
console.error(BG_LOG_PREFIX, `${httpErrorMessage}. AppReqId: ${currentOperationRequestId}, Debugger netReqId: ${params.requestId}.`);
const messageToContent = {
type: "PROVIDER_DEBUGGER_EVENT",
detail: {
requestId: currentOperationRequestId,
networkRequestId: params.requestId,
error: httpErrorMessage,
isFinal: true
}
};
chrome.tabs.sendMessage(tabId, messageToContent, response => {
if (chrome.runtime.lastError) console.error(BG_LOG_PREFIX, `Error sending debugger HTTP error event to content script for tab ${tabId}:`, chrome.runtime.lastError.message);
else console.log(BG_LOG_PREFIX, `Sent debugger HTTP error event to content script for tab ${tabId}, appReqId: ${currentOperationRequestId}, ack:`, response);
});
chrome.debugger.sendCommand(debuggeeId, "Fetch.continueRequest", { requestId: params.requestId });
}
} else {
// No response headers yet (no params.responseStatusCode), and no responseErrorReason.
// This could be an earlier stage of the request or one not relevant for body capture.
console.log(BG_LOG_PREFIX, `Request for ${params.request.url} (debugger netReqId: ${params.requestId}, appReqId: ${currentOperationRequestId}) does not have responseStatusCode. Stage: ${params.requestStage}. Continuing request.`);
chrome.debugger.sendCommand(debuggeeId, "Fetch.continueRequest", { requestId: params.requestId });
}
}
});