mirror of
https://github.com/BinaryBeastMaster/chat-relay.git
synced 2026-04-26 10:50:38 +00:00
1215 lines
67 KiB
JavaScript
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 });
|
|
}
|
|
}
|
|
});
|