diff --git a/.eslintrc.json b/.eslintrc.json index 59ca3019..b9087767 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,10 +1,23 @@ { "env": { "browser": true, - "es2021": true + "es2021": true, + "webextensions": true }, "extends": ["eslint:recommended", "plugin:react/recommended"], - "overrides": [], + "overrides": [ + { + "files": [ + "build.mjs", + ".github/workflows/scripts/*.mjs", + "scripts/**/*.js", + "scripts/**/*.mjs" + ], + "env": { + "node": true + } + } + ], "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" diff --git a/package.json b/package.json index 8bf2f924..fee45a15 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "build:safari": "bash ./safari/build.sh", "dev": "node build.mjs --development", "analyze": "node build.mjs --analyze", - "lint": "eslint --ext .js,.mjs,.jsx .", - "lint:fix": "eslint --ext .js,.mjs,.jsx . --fix", + "lint": "npx eslint --ext .js,.mjs,.jsx .", + "lint:fix": "npx eslint --ext .js,.mjs,.jsx . --fix", "pretty": "prettier --write ./**/*.{js,mjs,jsx,json,css,scss}", "stage": "run-script-os", "stage:default": "git add $(git diff --name-only --cached --diff-filter=d)", diff --git a/src/background/index.mjs b/src/background/index.mjs index 6fde2d48..e17093f8 100644 --- a/src/background/index.mjs +++ b/src/background/index.mjs @@ -51,197 +51,514 @@ import { generateAnswersWithMoonshotCompletionApi } from '../services/apis/moons import { generateAnswersWithMoonshotWebApi } from '../services/apis/moonshot-web.mjs' import { isUsingModelName } from '../utils/model-name-convert.mjs' -function setPortProxy(port, proxyTabId) { - port.proxy = Browser.tabs.connect(proxyTabId) - const proxyOnMessage = (msg) => { - port.postMessage(msg) +const RECONNECT_CONFIG = { + MAX_ATTEMPTS: 5, + BASE_DELAY_MS: 1000, + BACKOFF_MULTIPLIER: 2, +}; + +const SENSITIVE_KEYWORDS = [ + 'apikey', + 'token', + 'secret', + 'password', + 'kimimoonshotrefreshtoken', + 'auth', + 'key', + 'credential', + 'jwt', + 'session', + 'access', + 'private', + 'oauth', +]; + +function redactSensitiveFields(obj, recursionDepth = 0, maxDepth = 5, seen = new WeakSet()) { + if (recursionDepth > maxDepth) { + return 'REDACTED_TOO_DEEP'; } - const portOnMessage = (msg) => { - port.proxy.postMessage(msg) + if (obj === null || typeof obj !== 'object') { + return obj; } - const proxyOnDisconnect = () => { - port.proxy = Browser.tabs.connect(proxyTabId) + + if (seen.has(obj)) { + return 'REDACTED_CIRCULAR_REFERENCE'; } - const portOnDisconnect = () => { - port.proxy.onMessage.removeListener(proxyOnMessage) - port.onMessage.removeListener(portOnMessage) - port.proxy.onDisconnect.removeListener(proxyOnDisconnect) - port.onDisconnect.removeListener(portOnDisconnect) + seen.add(obj); + + if (Array.isArray(obj)) { + const redactedArray = []; + for (let i = 0; i < obj.length; i++) { + const item = obj[i]; + if (item !== null && typeof item === 'object') { + redactedArray[i] = redactSensitiveFields(item, recursionDepth + 1, maxDepth, seen); + } else if (typeof item === 'string') { + let isItemSensitive = false; + const lowerItem = item.toLowerCase(); + for (const keyword of SENSITIVE_KEYWORDS) { + if (lowerItem.includes(keyword)) { + isItemSensitive = true; + break; + } + } + redactedArray[i] = isItemSensitive ? 'REDACTED' : item; + } else { + redactedArray[i] = item; + } + } + return redactedArray; + } else { + const redactedObj = {}; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const lowerKey = key.toLowerCase(); + let isKeySensitive = false; + for (const keyword of SENSITIVE_KEYWORDS) { + if (lowerKey.includes(keyword)) { + isKeySensitive = true; + break; + } + } + if (isKeySensitive) { + redactedObj[key] = 'REDACTED'; + } else if (obj[key] !== null && typeof obj[key] === 'object') { + redactedObj[key] = redactSensitiveFields(obj[key], recursionDepth + 1, maxDepth, seen); + } else { + redactedObj[key] = obj[key]; + } + } + } + return redactedObj; + } +} + +function setPortProxy(port, proxyTabId) { + try { + console.debug(`[background] Attempting to connect to proxy tab: ${proxyTabId}`) + + if (port.proxy) { + try { + if (port._proxyOnMessage) port.proxy.onMessage.removeListener(port._proxyOnMessage); + if (port._proxyOnDisconnect) port.proxy.onDisconnect.removeListener(port._proxyOnDisconnect); + } catch(e) { + console.warn('[background] Error removing old listeners from previous port.proxy instance:', e); + } + } + if (port._portOnMessage) port.onMessage.removeListener(port._portOnMessage); + if (port._portOnDisconnect) port.onDisconnect.removeListener(port._portOnDisconnect); + + port.proxy = Browser.tabs.connect(proxyTabId, { name: 'background-to-content-script-proxy' }) + console.debug(`[background] Successfully connected to proxy tab: ${proxyTabId}`) + port._reconnectAttempts = 0 + + port._proxyOnMessage = (msg) => { + console.debug('[background] Message from proxy tab:', msg) + port.postMessage(msg) + } + port._portOnMessage = (msg) => { + console.debug('[background] Message to proxy tab:', msg) + if (port.proxy) { + try { + port.proxy.postMessage(msg) + } catch (e) { + console.error('[background] Error posting message to proxy tab in _portOnMessage:', e, msg); + try { + port.postMessage({ error: 'Failed to forward message to target tab. Tab might be closed or an extension error occurred.' }); + } catch (notifyError) { + console.error('[background] Error sending forwarding failure notification back to original sender:', notifyError); + } + } + } else { + console.warn('[background] Port proxy not available to send message:', msg) + } + } + + port._proxyOnDisconnect = () => { + console.warn(`[background] Proxy tab ${proxyTabId} disconnected.`) + + const proxyRef = port.proxy; + port.proxy = null + + if (proxyRef) { + if (port._proxyOnMessage) { + try { proxyRef.onMessage.removeListener(port._proxyOnMessage); } + catch(e) { console.warn("[background] Error removing _proxyOnMessage from disconnected proxyRef:", e); } + } + if (port._proxyOnDisconnect) { + try { proxyRef.onDisconnect.removeListener(port._proxyOnDisconnect); } + catch(e) { console.warn("[background] Error removing _proxyOnDisconnect from disconnected proxyRef:", e); } + } + } + + port._reconnectAttempts = (port._reconnectAttempts || 0) + 1; + if (port._reconnectAttempts > RECONNECT_CONFIG.MAX_ATTEMPTS) { + console.error(`[background] Max reconnect attempts (${RECONNECT_CONFIG.MAX_ATTEMPTS}) reached for tab ${proxyTabId}. Giving up.`); + if (port._portOnMessage) { + try { port.onMessage.removeListener(port._portOnMessage); } + catch(e) { console.warn("[background] Error removing _portOnMessage on max retries:", e); } + } + if (port._portOnDisconnect) { + try { port.onDisconnect.removeListener(port._portOnDisconnect); } + catch(e) { console.warn("[background] Error removing _portOnDisconnect from main port on max retries:", e); } + } + try { + port.postMessage({ error: `Connection to ChatGPT tab lost after ${RECONNECT_CONFIG.MAX_ATTEMPTS} attempts. Please refresh the page.` }); + } catch(e) { + console.warn("[background] Error sending final error message on max retries:", e); + } + return; + } + + const delay = Math.pow(RECONNECT_CONFIG.BACKOFF_MULTIPLIER, port._reconnectAttempts - 1) * RECONNECT_CONFIG.BASE_DELAY_MS; + console.log(`[background] Attempting reconnect #${port._reconnectAttempts} in ${delay / 1000}s for tab ${proxyTabId}.`) + + setTimeout(() => { + console.debug(`[background] Retrying connection to tab ${proxyTabId}, attempt ${port._reconnectAttempts}.`); + setPortProxy(port, proxyTabId); + }, delay); + } + + port._portOnDisconnect = () => { + console.log('[background] Main port disconnected (e.g. popup/sidebar closed). Cleaning up proxy connections and listeners.'); + if (port._portOnMessage) { + try { port.onMessage.removeListener(port._portOnMessage); } + catch(e) { console.warn("[background] Error removing _portOnMessage on main port disconnect:", e); } + } + const proxyRef = port.proxy; + if (proxyRef) { + if (port._proxyOnMessage) { + try { proxyRef.onMessage.removeListener(port._proxyOnMessage); } + catch(e) { console.warn("[background] Error removing _proxyOnMessage from proxyRef on main port disconnect:", e); } + } + if (port._proxyOnDisconnect) { + try { proxyRef.onDisconnect.removeListener(port._proxyOnDisconnect); } + catch(e) { console.warn("[background] Error removing _proxyOnDisconnect from proxyRef on main port disconnect:", e); } + } + try { + proxyRef.disconnect(); + } catch(e) { + console.warn('[background] Error disconnecting proxyRef on main port disconnect:', e); + } + port.proxy = null; + } + if (port._portOnDisconnect) { + try { port.onDisconnect.removeListener(port._portOnDisconnect); } + catch(e) { console.warn("[background] Error removing _portOnDisconnect on main port disconnect:", e); } + } + port._reconnectAttempts = 0; + } + + port.proxy.onMessage.addListener(port._proxyOnMessage) + port.onMessage.addListener(port._portOnMessage) + port.proxy.onDisconnect.addListener(port._proxyOnDisconnect) + port.onDisconnect.addListener(port._portOnDisconnect) + + } catch (error) { + console.error(`[background] Error in setPortProxy for tab ${proxyTabId}:`, error) } - port.proxy.onMessage.addListener(proxyOnMessage) - port.onMessage.addListener(portOnMessage) - port.proxy.onDisconnect.addListener(proxyOnDisconnect) - port.onDisconnect.addListener(portOnDisconnect) } async function executeApi(session, port, config) { - console.debug('modelName', session.modelName) - console.debug('apiMode', session.apiMode) - if (isUsingCustomModel(session)) { - if (!session.apiMode) - await generateAnswersWithCustomApi( - port, - session.question, - session, - config.customModelApiUrl.trim() || 'http://localhost:8000/v1/chat/completions', - config.customApiKey, - config.customModelName, - ) - else - await generateAnswersWithCustomApi( + console.log( + `[background] executeApi called for model: ${session.modelName}, apiMode: ${session.apiMode}`, + ) + console.debug('[background] Full session details (redacted):', redactSensitiveFields(session)) + console.debug('[background] Full config details (redacted):', redactSensitiveFields(config)) + if (session.apiMode) { + console.debug('[background] Session apiMode details (redacted):', redactSensitiveFields(session.apiMode)) + } + + try { + if (isUsingCustomModel(session)) { + console.debug('[background] Using Custom Model API') + if (!session.apiMode) + await generateAnswersWithCustomApi( + port, + session.question, + session, + config.customModelApiUrl.trim() || 'http://localhost:8000/v1/chat/completions', + config.customApiKey, + config.customModelName, + ) + else + await generateAnswersWithCustomApi( + port, + session.question, + session, + session.apiMode.customUrl.trim() || + config.customModelApiUrl.trim() || + 'http://localhost:8000/v1/chat/completions', + session.apiMode.apiKey.trim() || config.customApiKey, + session.apiMode.customName, + ) + } else if (isUsingChatgptWebModel(session)) { + console.debug('[background] Using ChatGPT Web Model') + let tabId + if ( + config.chatgptTabId && + config.customChatGptWebApiUrl === defaultConfig.customChatGptWebApiUrl + ) { + try { + const tab = await Browser.tabs.get(config.chatgptTabId) + if (tab) tabId = tab.id + } catch (e) { + console.warn( + `[background] Failed to get ChatGPT tab with ID ${config.chatgptTabId}:`, + e.message, + ) + } + } + if (tabId) { + console.debug(`[background] ChatGPT Tab ID ${tabId} found.`) + if (!port.proxy) { + console.debug('[background] port.proxy not found, calling setPortProxy.') + setPortProxy(port, tabId) + } + if (port.proxy) { + console.debug('[background] Posting message to proxy tab:', { session }) + try { + port.proxy.postMessage({ session }) + } catch (e) { + console.warn('[background] Error posting message to existing proxy tab in executeApi (ChatGPT Web Model):', e, '. Attempting to reconnect.', { session }); + setPortProxy(port, tabId); // Attempt to re-establish the connection + if (port.proxy) { + console.debug('[background] Proxy re-established. Attempting to post message again.'); + try { + port.proxy.postMessage({ session }); + console.info('[background] Successfully posted session after proxy reconnection.'); + } catch (e2) { + console.error('[background] Error posting message even after proxy reconnection:', e2, { session }); + try { + port.postMessage({ error: 'Failed to communicate with ChatGPT tab after reconnection attempt. Try refreshing the page.' }); + } catch (notifyError) { + console.error('[background] Error sending final communication failure notification back:', notifyError); + } + } + } else { + console.error('[background] Failed to re-establish proxy connection. Cannot send session.'); + try { + port.postMessage({ error: 'Could not re-establish connection to ChatGPT tab. Try refreshing the page.' }); + } catch (notifyError) { + console.error('[background] Error sending re-establishment failure notification back:', notifyError); + } + } + } + } else { + console.error( + '[background] Failed to send message: port.proxy is still not available after initial setPortProxy attempt.', + ); + try { + port.postMessage({ error: 'Failed to initialize connection to ChatGPT tab. Try refreshing the page.' }); + } catch (notifyError) { + console.error('[background] Error sending initial connection failure notification back:', notifyError); + } + } + } else { + console.debug('[background] No valid ChatGPT Tab ID found. Using direct API call.') + const accessToken = await getChatGptAccessToken() + await generateAnswersWithChatgptWebApi(port, session.question, session, accessToken) + } + } else if (isUsingClaudeWebModel(session)) { + console.debug('[background] Using Claude Web Model') + const sessionKey = await getClaudeSessionKey() + await generateAnswersWithClaudeWebApi(port, session.question, session, sessionKey) + } else if (isUsingMoonshotWebModel(session)) { + console.debug('[background] Using Moonshot Web Model') + await generateAnswersWithMoonshotWebApi(port, session.question, session, config) + } else if (isUsingBingWebModel(session)) { + console.debug('[background] Using Bing Web Model') + const accessToken = await getBingAccessToken() + if (isUsingModelName('bingFreeSydney', session)) { + console.debug('[background] Using Bing Free Sydney model') + await generateAnswersWithBingWebApi(port, session.question, session, accessToken, true) + } else { + await generateAnswersWithBingWebApi(port, session.question, session, accessToken) + } + } else if (isUsingGeminiWebModel(session)) { + console.debug('[background] Using Gemini Web Model') + const cookies = await getBardCookies() + await generateAnswersWithBardWebApi(port, session.question, session, cookies) + } else if (isUsingChatgptApiModel(session)) { + console.debug('[background] Using ChatGPT API Model') + await generateAnswersWithChatgptApi(port, session.question, session, config.apiKey) + } else if (isUsingClaudeApiModel(session)) { + console.debug('[background] Using Claude API Model') + await generateAnswersWithClaudeApi(port, session.question, session) + } else if (isUsingMoonshotApiModel(session)) { + console.debug('[background] Using Moonshot API Model') + await generateAnswersWithMoonshotCompletionApi( port, session.question, session, - session.apiMode.customUrl.trim() || - config.customModelApiUrl.trim() || - 'http://localhost:8000/v1/chat/completions', - session.apiMode.apiKey.trim() || config.customApiKey, - session.apiMode.customName, + config.moonshotApiKey, ) - } else if (isUsingChatgptWebModel(session)) { - let tabId - if ( - config.chatgptTabId && - config.customChatGptWebApiUrl === defaultConfig.customChatGptWebApiUrl - ) { - const tab = await Browser.tabs.get(config.chatgptTabId).catch(() => {}) - if (tab) tabId = tab.id - } - if (tabId) { - if (!port.proxy) { - setPortProxy(port, tabId) - port.proxy.postMessage({ session }) - } + } else if (isUsingChatGLMApiModel(session)) { + console.debug('[background] Using ChatGLM API Model') + await generateAnswersWithChatGLMApi(port, session.question, session) + } else if (isUsingOllamaApiModel(session)) { + console.debug('[background] Using Ollama API Model') + await generateAnswersWithOllamaApi(port, session.question, session) + } else if (isUsingAzureOpenAiApiModel(session)) { + console.debug('[background] Using Azure OpenAI API Model') + await generateAnswersWithAzureOpenaiApi(port, session.question, session) + } else if (isUsingGptCompletionApiModel(session)) { + console.debug('[background] Using GPT Completion API Model') + await generateAnswersWithGptCompletionApi(port, session.question, session, config.apiKey) + } else if (isUsingGithubThirdPartyApiModel(session)) { + console.debug('[background] Using Github Third Party API Model') + await generateAnswersWithWaylaidwandererApi(port, session.question, session) } else { - const accessToken = await getChatGptAccessToken() - await generateAnswersWithChatgptWebApi(port, session.question, session, accessToken) + console.warn('[background] Unknown model or session configuration:', session) + port.postMessage({ error: 'Unknown model configuration' }) } - } else if (isUsingClaudeWebModel(session)) { - const sessionKey = await getClaudeSessionKey() - await generateAnswersWithClaudeWebApi(port, session.question, session, sessionKey) - } else if (isUsingMoonshotWebModel(session)) { - await generateAnswersWithMoonshotWebApi(port, session.question, session, config) - } else if (isUsingBingWebModel(session)) { - const accessToken = await getBingAccessToken() - if (isUsingModelName('bingFreeSydney', session)) - await generateAnswersWithBingWebApi(port, session.question, session, accessToken, true) - else await generateAnswersWithBingWebApi(port, session.question, session, accessToken) - } else if (isUsingGeminiWebModel(session)) { - const cookies = await getBardCookies() - await generateAnswersWithBardWebApi(port, session.question, session, cookies) - } else if (isUsingChatgptApiModel(session)) { - await generateAnswersWithChatgptApi(port, session.question, session, config.apiKey) - } else if (isUsingClaudeApiModel(session)) { - await generateAnswersWithClaudeApi(port, session.question, session) - } else if (isUsingMoonshotApiModel(session)) { - await generateAnswersWithMoonshotCompletionApi( - port, - session.question, - session, - config.moonshotApiKey, - ) - } else if (isUsingChatGLMApiModel(session)) { - await generateAnswersWithChatGLMApi(port, session.question, session) - } else if (isUsingOllamaApiModel(session)) { - await generateAnswersWithOllamaApi(port, session.question, session) - } else if (isUsingAzureOpenAiApiModel(session)) { - await generateAnswersWithAzureOpenaiApi(port, session.question, session) - } else if (isUsingGptCompletionApiModel(session)) { - await generateAnswersWithGptCompletionApi(port, session.question, session, config.apiKey) - } else if (isUsingGithubThirdPartyApiModel(session)) { - await generateAnswersWithWaylaidwandererApi(port, session.question, session) + } catch (error) { + console.error(`[background] Error in executeApi for model ${session.modelName}:`, error) + port.postMessage({ error: error.message || 'An unexpected error occurred in executeApi' }) } } Browser.runtime.onMessage.addListener(async (message, sender) => { - switch (message.type) { - case 'FEEDBACK': { - const token = await getChatGptAccessToken() - await sendMessageFeedback(token, message.data) - break - } - case 'DELETE_CONVERSATION': { - const token = await getChatGptAccessToken() - await deleteConversation(token, message.data.conversationId) - break - } - case 'NEW_URL': { - await Browser.tabs.create({ - url: message.data.url, - pinned: message.data.pinned, - }) - if (message.data.jumpBack) { - await setUserConfig({ - notificationJumpBackTabId: sender.tab.id, + console.debug('[background] Received message:', message, 'from sender:', sender) + try { + switch (message.type) { + case 'FEEDBACK': { + console.log('[background] Processing FEEDBACK message') + const token = await getChatGptAccessToken() + await sendMessageFeedback(token, message.data) + break + } + case 'DELETE_CONVERSATION': { + console.log('[background] Processing DELETE_CONVERSATION message') + const token = await getChatGptAccessToken() + await deleteConversation(token, message.data.conversationId) + break + } + case 'NEW_URL': { + console.log('[background] Processing NEW_URL message:', message.data) + await Browser.tabs.create({ + url: message.data.url, + pinned: message.data.pinned, }) + if (message.data.jumpBack) { + console.debug('[background] Setting jumpBackTabId:', sender.tab?.id) + await setUserConfig({ + notificationJumpBackTabId: sender.tab?.id, + }) + } + break } - break - } - case 'SET_CHATGPT_TAB': { - await setUserConfig({ - chatgptTabId: sender.tab.id, - }) - break - } - case 'ACTIVATE_URL': - await Browser.tabs.update(message.data.tabId, { active: true }) - break - case 'OPEN_URL': - openUrl(message.data.url) - break - case 'OPEN_CHAT_WINDOW': { - const config = await getUserConfig() - const url = Browser.runtime.getURL('IndependentPanel.html') - const tabs = await Browser.tabs.query({ url: url, windowType: 'popup' }) - if (!config.alwaysCreateNewConversationWindow && tabs.length > 0) - await Browser.windows.update(tabs[0].windowId, { focused: true }) - else - await Browser.windows.create({ - url: url, - type: 'popup', - width: 500, - height: 650, + case 'SET_CHATGPT_TAB': { + console.log('[background] Processing SET_CHATGPT_TAB message. Tab ID:', sender.tab?.id) + await setUserConfig({ + chatgptTabId: sender.tab?.id, }) - break - } - case 'REFRESH_MENU': - refreshMenu() - break - case 'PIN_TAB': { - let tabId - if (message.data.tabId) tabId = message.data.tabId - else tabId = sender.tab.id - - await Browser.tabs.update(tabId, { pinned: true }) - if (message.data.saveAsChatgptConfig) { - await setUserConfig({ chatgptTabId: tabId }) + break } - break - } - case 'FETCH': { - if (message.data.input.includes('bing.com')) { - const accessToken = await getBingAccessToken() - await setUserConfig({ bingAccessToken: accessToken }) + case 'ACTIVATE_URL': + console.log('[background] Processing ACTIVATE_URL message:', message.data) + await Browser.tabs.update(message.data.tabId, { active: true }) + break + case 'OPEN_URL': + console.log('[background] Processing OPEN_URL message:', message.data) + openUrl(message.data.url) + break + case 'OPEN_CHAT_WINDOW': { + console.log('[background] Processing OPEN_CHAT_WINDOW message') + const config = await getUserConfig() + const url = Browser.runtime.getURL('IndependentPanel.html') + const tabs = await Browser.tabs.query({ url: url, windowType: 'popup' }) + if (!config.alwaysCreateNewConversationWindow && tabs.length > 0) { + console.debug('[background] Focusing existing chat window:', tabs[0].windowId) + await Browser.windows.update(tabs[0].windowId, { focused: true }) + } else { + console.debug('[background] Creating new chat window.') + await Browser.windows.create({ + url: url, + type: 'popup', + width: 500, + height: 650, + }) + } + break + } + case 'REFRESH_MENU': + console.log('[background] Processing REFRESH_MENU message') + refreshMenu() + break + case 'PIN_TAB': { + console.log('[background] Processing PIN_TAB message:', message.data) + let tabId = message.data.tabId || sender.tab?.id + if (tabId) { + await Browser.tabs.update(tabId, { pinned: true }) + if (message.data.saveAsChatgptConfig) { + console.debug('[background] Saving pinned tab as ChatGPT config tab:', tabId) + await setUserConfig({ chatgptTabId: tabId }) + } + } else { + console.warn('[background] No tabId found for PIN_TAB message.') + } + break } + case 'FETCH': { + console.log('[background] Processing FETCH message for URL:', message.data.input) + if (message.data.input.includes('bing.com')) { + console.debug('[background] Fetching Bing access token for FETCH message.') + const accessToken = await getBingAccessToken() + await setUserConfig({ bingAccessToken: accessToken }) + } - try { - const response = await fetch(message.data.input, message.data.init) - const text = await response.text() - return [ - { + try { + const response = await fetch(message.data.input, message.data.init) + const text = await response.text() + const responseObject = { // Defined for clarity before conditional error property body: text, + ok: response.ok, status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers), - }, - null, - ] - } catch (error) { - return [null, error] + }; + if (!response.ok) { + responseObject.error = `HTTP error ${response.status}: ${response.statusText}`; + console.warn(`[background] FETCH received error status: ${response.status} for ${message.data.input}`); + } + console.debug( + `[background] FETCH successful for ${message.data.input}, status: ${response.status}`, + ) + return [responseObject, null]; + } catch (error) { + console.error(`[background] FETCH error for ${message.data.input}:`, error) + return [null, { message: error.message, stack: error.stack }] + } + } + case 'GET_COOKIE': { + console.log('[background] Processing GET_COOKIE message:', message.data) + try { + const cookie = await Browser.cookies.get({ + url: message.data.url, + name: message.data.name, + }) + console.debug('[background] Cookie found:', cookie) + return cookie?.value + } catch (error) { + console.error( + `[background] Error getting cookie ${message.data.name} for ${message.data.url}:`, + error, + ) + return null + } } + default: + console.warn('[background] Unknown message type received:', message.type) } - case 'GET_COOKIE': { - return (await Browser.cookies.get({ url: message.data.url, name: message.data.name }))?.value + } catch (error) { + console.error( + `[background] Error processing message type ${message.type}:`, + error, + 'Original message:', + message, + ) + if (message.type === 'FETCH') { + return [null, { message: error.message, stack: error.stack }] } } }) @@ -249,22 +566,38 @@ Browser.runtime.onMessage.addListener(async (message, sender) => { try { Browser.webRequest.onBeforeRequest.addListener( (details) => { - if ( - details.url.includes('/public_key') && - !details.url.includes(defaultConfig.chatgptArkoseReqParams) - ) { - let formData = new URLSearchParams() - for (const k in details.requestBody.formData) { - formData.append(k, details.requestBody.formData[k]) - } - setUserConfig({ - chatgptArkoseReqUrl: details.url, - chatgptArkoseReqForm: + try { + console.debug('[background] onBeforeRequest triggered for URL:', details.url) + if ( + details.url.includes('/public_key') && + !details.url.includes(defaultConfig.chatgptArkoseReqParams) + ) { + console.log('[background] Capturing Arkose public_key request:', details.url) + let formData = new URLSearchParams() + if (details.requestBody?.formData) { + for (const k in details.requestBody.formData) { + formData.append(k, details.requestBody.formData[k]) + } + } + const formString = formData.toString() || - new TextDecoder('utf-8').decode(new Uint8Array(details.requestBody.raw[0].bytes)), - }).then(() => { - console.log('Arkose req url and form saved') - }) + (details.requestBody?.raw?.[0]?.bytes + ? new TextDecoder('utf-8').decode(new Uint8Array(details.requestBody.raw[0].bytes)) + : '') + + setUserConfig({ + chatgptArkoseReqUrl: details.url, + chatgptArkoseReqForm: formString, + }) + .then(() => { + console.log('[background] Arkose req url and form saved successfully.') + }) + .catch((e) => + console.error('[background] Error saving Arkose req url and form:', e), + ) + } + } catch (error) { + console.error('[background] Error in onBeforeRequest listener callback:', error, details) } }, { @@ -276,36 +609,132 @@ try { Browser.webRequest.onBeforeSendHeaders.addListener( (details) => { - const headers = details.requestHeaders - for (let i = 0; i < headers.length; i++) { - if (headers[i].name === 'Origin') { - headers[i].value = 'https://www.bing.com' - } else if (headers[i].name === 'Referer') { - headers[i].value = 'https://www.bing.com/search?q=Bing+AI&showconv=1&FORM=hpcodx' + try { + console.debug('[background] onBeforeSendHeaders triggered for URL:', details.url) + const headers = details.requestHeaders + let modified = false + for (let i = 0; i < headers.length; i++) { + if (headers[i]) { + const headerNameLower = headers[i].name?.toLowerCase(); + if (headerNameLower === 'origin') { + headers[i].value = 'https://www.bing.com' + modified = true + } else if (headerNameLower === 'referer') { + headers[i].value = 'https://www.bing.com/search?q=Bing+AI&showconv=1&FORM=hpcodx' + modified = true + } + } + } + if (modified) { + console.debug('[background] Modified headers for Bing:', headers) } + return { requestHeaders: headers } + } catch (error) { + console.error( + '[background] Error in onBeforeSendHeaders listener callback:', + error, + details, + ) + return { requestHeaders: details.requestHeaders } } - return { requestHeaders: headers } }, { urls: ['wss://sydney.bing.com/*', 'https://www.bing.com/*'], types: ['xmlhttprequest', 'websocket'], }, - ['requestHeaders'], + ['requestHeaders', ...(Browser.runtime.getManifest().manifest_version < 3 ? ['blocking'] : [])], ) Browser.tabs.onUpdated.addListener(async (tabId, info, tab) => { - if (!tab.url) return - // eslint-disable-next-line no-undef - await chrome.sidePanel.setOptions({ - tabId, - path: 'IndependentPanel.html', - enabled: true, - }) + const outerTryCatchError = (error) => { + console.error('[background] Error in tabs.onUpdated listener callback (outer):', error, tabId, info); + }; + try { + if (!tab.url || (info.status && info.status !== 'complete')) { + console.debug( + `[background] Skipping side panel update for tabId: ${tabId}. Tab URL: ${tab.url}, Info Status: ${info.status}`, + ); + return; + } + console.debug( + `[background] tabs.onUpdated event for tabId: ${tabId}, status: ${info.status}, url: ${tab.url}. Proceeding with side panel update.`, + ); + + let sidePanelSet = false; + try { + if (Browser.sidePanel && typeof Browser.sidePanel.setOptions === 'function') { + await Browser.sidePanel.setOptions({ + tabId, + path: 'IndependentPanel.html', + enabled: true, + }); + console.debug(`[background] Side panel options set for tab ${tabId} using Browser.sidePanel`); + sidePanelSet = true; + } + } catch (browserError) { + console.warn('[background] Browser.sidePanel.setOptions failed:', browserError.message); + } + + if (!sidePanelSet) { + // eslint-disable-next-line no-undef + if (typeof chrome !== 'undefined' && chrome.sidePanel && typeof chrome.sidePanel.setOptions === 'function') { + console.debug('[background] Attempting chrome.sidePanel.setOptions as fallback.'); + try { + // eslint-disable-next-line no-undef + await chrome.sidePanel.setOptions({ + tabId, + path: 'IndependentPanel.html', + enabled: true, + }); + console.debug(`[background] Side panel options set for tab ${tabId} using chrome.sidePanel`); + sidePanelSet = true; + } catch (chromeError) { + console.error('[background] chrome.sidePanel.setOptions also failed:', chromeError.message); + } + } + } + + if (!sidePanelSet) { + console.warn('[background] SidePanel API (Browser.sidePanel or chrome.sidePanel) not available or setOptions failed in this browser. Side panel options not set for tab:', tabId); + } + } catch (error) { + outerTryCatchError(error); + } + }); +} catch (error) { + console.error('[background] Error setting up webRequest or tabs listeners:', error) +} + +try { + registerPortListener(async (session, port, config) => { + console.debug( + `[background] Port listener triggered for session: ${session.modelName}, port: ${port.name}`, + ) + try { + await executeApi(session, port, config) + } catch (e) { + console.error( + `[background] Error in port listener callback executing API for session ${session.modelName}:`, + e, + ) + port.postMessage({ error: e.message || 'An unexpected error occurred in port listener' }) + } }) + console.log('[background] Port listener registered successfully.') +} catch (error) { + console.error('[background] Error registering port listener:', error) +} + +try { + registerCommands() + console.log('[background] Commands registered successfully.') } catch (error) { - console.log(error) + console.error('[background] Error registering commands:', error) } -registerPortListener(async (session, port, config) => await executeApi(session, port, config)) -registerCommands() -refreshMenu() +try { + refreshMenu() + console.log('[background] Menu refreshed successfully.') +} catch (error) { + console.error('[background] Error refreshing menu:', error) +} diff --git a/src/content-script/index.jsx b/src/content-script/index.jsx index 591881e4..2df7e515 100644 --- a/src/content-script/index.jsx +++ b/src/content-script/index.jsx @@ -35,461 +35,922 @@ import WebJumpBackNotification from '../components/WebJumpBackNotification' * @param {SiteConfig} siteConfig */ async function mountComponent(siteConfig) { - const userConfig = await getUserConfig() - - if (!userConfig.alwaysFloatingSidebar) { - const retry = 10 - let oldUrl = location.href - for (let i = 1; i <= retry; i++) { - if (location.href !== oldUrl) { - console.log(`SiteAdapters Retry ${i}/${retry}: stop`) + console.debug('[content] mountComponent called with siteConfig:', siteConfig) + try { + const userConfig = await getUserConfig() + console.debug('[content] User config in mountComponent:', userConfig) + + if (!userConfig.alwaysFloatingSidebar) { + const retry = 10 + let oldUrl = location.href + let elementFound = false + for (let i = 1; i <= retry; i++) { + console.debug(`[content] mountComponent retry ${i}/${retry} for element detection.`) + if (location.href !== oldUrl) { + console.log('[content] URL changed during retry, stopping mountComponent.') + return + } + const e = + (siteConfig && + (getPossibleElementByQuerySelector(siteConfig.sidebarContainerQuery) || + getPossibleElementByQuerySelector(siteConfig.appendContainerQuery) || + getPossibleElementByQuerySelector(siteConfig.resultsContainerQuery))) || + getPossibleElementByQuerySelector([userConfig.prependQuery]) || + getPossibleElementByQuerySelector([userConfig.appendQuery]) + if (e) { + console.log('[content] Element found for mounting component:', e) + elementFound = true + break + } else { + console.debug(`[content] Element not found on retry ${i}.`) + if (i === retry) { + console.warn('[content] Element not found after all retries for mountComponent.') + return + } + await new Promise((r) => setTimeout(r, 500)) + } + } + if (!elementFound) { + console.warn( + '[content] No suitable element found for non-floating sidebar after retries.', + ) return } - const e = - (siteConfig && - (getPossibleElementByQuerySelector(siteConfig.sidebarContainerQuery) || - getPossibleElementByQuerySelector(siteConfig.appendContainerQuery) || - getPossibleElementByQuerySelector(siteConfig.resultsContainerQuery))) || - getPossibleElementByQuerySelector([userConfig.prependQuery]) || - getPossibleElementByQuerySelector([userConfig.appendQuery]) - if (e) { - console.log(`SiteAdapters Retry ${i}/${retry}: found`) - console.log(e) - break - } else { - console.log(`SiteAdapters Retry ${i}/${retry}: not found`) - if (i === retry) return - else await new Promise((r) => setTimeout(r, 500)) + } + + document.querySelectorAll('.chatgptbox-container,#chatgptbox-container').forEach((e) => { + try { + unmountComponentAtNode(e) + e.remove() + } catch (err) { + console.error('[content] Error removing existing chatgptbox container:', err) } + }) + + let question + if (userConfig.inputQuery) { + console.debug('[content] Getting input from userConfig.inputQuery') + question = await getInput([userConfig.inputQuery]) } - } - document.querySelectorAll('.chatgptbox-container,#chatgptbox-container').forEach((e) => { - unmountComponentAtNode(e) - e.remove() - }) + if (!question && siteConfig) { + console.debug('[content] Getting input from siteConfig.inputQuery') + question = await getInput(siteConfig.inputQuery) + } + console.debug('[content] Question for component:', question) + + // Ensure cleanup again in case getInput took time and new elements were added + document.querySelectorAll('.chatgptbox-container,#chatgptbox-container').forEach((e) => { + try { + unmountComponentAtNode(e) + e.remove() + } catch (err) { + console.error('[content] Error removing existing chatgptbox container post getInput:', err) + } + }) - let question - if (userConfig.inputQuery) question = await getInput([userConfig.inputQuery]) - if (!question && siteConfig) question = await getInput(siteConfig.inputQuery) + if (userConfig.alwaysFloatingSidebar && question) { + console.log('[content] Rendering floating sidebar.') + const position = { + x: window.innerWidth - 300 - Math.floor((20 / 100) * window.innerWidth), + y: window.innerHeight / 2 - 200, + } + const toolbarContainer = createElementAtPosition(position.x, position.y) + toolbarContainer.className = 'chatgptbox-toolbar-container-not-queryable' - document.querySelectorAll('.chatgptbox-container,#chatgptbox-container').forEach((e) => { - unmountComponentAtNode(e) - e.remove() - }) + let triggered = false + if (userConfig.triggerMode === 'always') triggered = true + else if (userConfig.triggerMode === 'questionMark' && question && endsWithQuestionMark(question.trim())) + triggered = true + console.debug('[content] Floating sidebar triggered:', triggered) - if (userConfig.alwaysFloatingSidebar && question) { - const position = { - x: window.innerWidth - 300 - Math.floor((20 / 100) * window.innerWidth), - y: window.innerHeight / 2 - 200, + render( + , + toolbarContainer, + ) + console.log('[content] Floating sidebar rendered.') + return } - const toolbarContainer = createElementAtPosition(position.x, position.y) - toolbarContainer.className = 'chatgptbox-toolbar-container-not-queryable' - let triggered = false - if (userConfig.triggerMode === 'always') triggered = true - else if (userConfig.triggerMode === 'questionMark' && endsWithQuestionMark(question.trim())) - triggered = true + if (!question && !userConfig.alwaysFloatingSidebar) { + console.log('[content] No question found and not alwaysFloatingSidebar, skipping DecisionCard render.') + return + } + console.log('[content] Rendering DecisionCard.') + const container = document.createElement('div') + container.id = 'chatgptbox-container' render( - , - toolbarContainer, + container, ) - return + console.log('[content] DecisionCard rendered.') + } catch (error) { + console.error('[content] Error in mountComponent:', error) } - - const container = document.createElement('div') - container.id = 'chatgptbox-container' - render( - , - container, - ) } -/** - * @param {string[]|function} inputQuery - * @returns {Promise} - */ async function getInput(inputQuery) { - let input - if (typeof inputQuery === 'function') { - input = await inputQuery() - const replyPromptBelow = `Reply in ${await getPreferredLanguage()}. Regardless of the language of content I provide below. !!This is very important!!` - const replyPromptAbove = `Reply in ${await getPreferredLanguage()}. Regardless of the language of content I provide above. !!This is very important!!` - if (input) return `${replyPromptBelow}\n\n` + input + `\n\n${replyPromptAbove}` - return input - } - const searchInput = getPossibleElementByQuerySelector(inputQuery) - if (searchInput) { - if (searchInput.value) input = searchInput.value - else if (searchInput.textContent) input = searchInput.textContent - if (input) - return ( - `Reply in ${await getPreferredLanguage()}.\nThe following is a search input in a search engine, ` + - `giving useful content or solutions and as much information as you can related to it, ` + - `use markdown syntax to make your answer more readable, such as code blocks, bold, list:\n` + - input - ) + console.debug('[content] getInput called with query:', inputQuery) + try { + let input + if (typeof inputQuery === 'function') { + console.debug('[content] Input query is a function.') + input = await inputQuery() + if (input) { + const preferredLanguage = await getPreferredLanguage() + const replyPromptBelow = `Reply in ${preferredLanguage}. Regardless of the language of content I provide below. !!This is very important!!` + const replyPromptAbove = `Reply in ${preferredLanguage}. Regardless of the language of content I provide above. !!This is very important!!` + const result = `${replyPromptBelow}\n\n${input}\n\n${replyPromptAbove}` + console.debug('[content] getInput from function result:', result) + return result + } + console.debug('[content] getInput from function returned no input.') + return input + } + console.debug('[content] Input query is a selector.') + const searchInput = getPossibleElementByQuerySelector(inputQuery) + if (searchInput) { + console.debug('[content] Found search input element:', searchInput) + if (searchInput.value) input = searchInput.value + else if (searchInput.textContent) input = searchInput.textContent + if (input) { + const preferredLanguage = await getPreferredLanguage() + const result = + `Reply in ${preferredLanguage}.\nThe following is a search input in a search engine, ` + + `giving useful content or solutions and as much information as you can related to it, ` + + `use markdown syntax to make your answer more readable, such as code blocks, bold, list:\n` + + input + console.debug('[content] getInput from selector result:', result) + return result + } + } + console.debug('[content] No input found from selector or element empty.') + return undefined + } catch (error) { + console.error('[content] Error in getInput:', error) + return undefined } } let toolbarContainer const deleteToolbar = () => { - if (toolbarContainer && toolbarContainer.className === 'chatgptbox-toolbar-container') - toolbarContainer.remove() + try { + if (toolbarContainer && toolbarContainer.className === 'chatgptbox-toolbar-container') { + console.debug('[content] Deleting toolbar:', toolbarContainer) + toolbarContainer.remove() + toolbarContainer = null + } + } catch (error) { + console.error('[content] Error in deleteToolbar:', error) + } } -const createSelectionTools = async (toolbarContainer, selection) => { - toolbarContainer.className = 'chatgptbox-toolbar-container' - const userConfig = await getUserConfig() - render( - , - toolbarContainer, +const createSelectionTools = async (toolbarContainerElement, selection) => { + console.debug( + '[content] createSelectionTools called with selection:', + selection, + 'and container:', + toolbarContainerElement, ) + try { + toolbarContainerElement.className = 'chatgptbox-toolbar-container' + const userConfig = await getUserConfig() + render( + , + toolbarContainerElement, + ) + console.log('[content] Selection tools rendered.') + } catch (error) { + console.error('[content] Error in createSelectionTools:', error) + } } async function prepareForSelectionTools() { + console.log('[content] Initializing selection tools.') document.addEventListener('mouseup', (e) => { - if (toolbarContainer && toolbarContainer.contains(e.target)) return - const selectionElement = - window.getSelection()?.rangeCount > 0 && - window.getSelection()?.getRangeAt(0).endContainer.parentElement - if (toolbarContainer && selectionElement && toolbarContainer.contains(selectionElement)) return - - deleteToolbar() - setTimeout(async () => { - const selection = window - .getSelection() - ?.toString() - .trim() - .replace(/^-+|-+$/g, '') - if (selection) { - let position - - const config = await getUserConfig() - if (!config.selectionToolsNextToInputBox) position = { x: e.pageX + 20, y: e.pageY + 20 } - else { - const inputElement = selectionElement.querySelector('input, textarea') - if (inputElement) { - position = getClientPosition(inputElement) - position = { - x: position.x + window.scrollX + inputElement.offsetWidth + 50, - y: e.pageY + 30, + try { + if (toolbarContainer?.contains(e.target)) { + console.debug('[content] Mouseup inside toolbar, ignoring.') + return + } + const selectionElement = + window.getSelection()?.rangeCount > 0 && + window.getSelection()?.getRangeAt(0).endContainer.parentElement + if (selectionElement && toolbarContainer?.contains(selectionElement)) { + console.debug('[content] Mouseup selection is inside toolbar, ignoring.') + return + } + + deleteToolbar() + setTimeout(async () => { + try { + const selection = window + .getSelection() + ?.toString() + .trim() + .replace(/^-+|-+$/g, '') + if (selection) { + console.debug('[content] Text selected:', selection) + let position + + const config = await getUserConfig() + if (!config.selectionToolsNextToInputBox) { + position = { x: e.pageX + 20, y: e.pageY + 20 } + } else { + const activeElement = document.activeElement + const inputElement = + selectionElement?.querySelector('input, textarea') || + (activeElement?.matches('input, textarea') ? activeElement : null) + + if (inputElement) { + console.debug('[content] Input element found for positioning toolbar:', inputElement) + const clientRect = getClientPosition(inputElement) + position = { + x: clientRect.x + window.scrollX + inputElement.offsetWidth + 50, + y: e.pageY + 30, + } + } else { + position = { x: e.pageX + 20, y: e.pageY + 20 } + } } + console.debug('[content] Toolbar position:', position) + toolbarContainer = createElementAtPosition(position.x, position.y) + await createSelectionTools(toolbarContainer, selection) } else { - position = { x: e.pageX + 20, y: e.pageY + 20 } + console.debug('[content] No text selected on mouseup.') } + } catch (err) { + console.error('[content] Error in mouseup setTimeout callback for selection tools:', err) } - toolbarContainer = createElementAtPosition(position.x, position.y) - await createSelectionTools(toolbarContainer, selection) - } - }) + }, 0) + } catch (error) { + console.error('[content] Error in mouseup listener for selection tools:', error) + } }) - document.addEventListener('mousedown', (e) => { - if (toolbarContainer && toolbarContainer.contains(e.target)) return - document.querySelectorAll('.chatgptbox-toolbar-container').forEach((e) => e.remove()) + document.addEventListener('mousedown', (e) => { + try { + if (toolbarContainer?.contains(e.target)) { + console.debug('[content] Mousedown inside toolbar, ignoring.') + return + } + console.debug('[content] Mousedown outside toolbar, removing existing toolbars.') + document.querySelectorAll('.chatgptbox-toolbar-container').forEach((el) => el.remove()) + toolbarContainer = null + } catch (error) { + console.error('[content] Error in mousedown listener for selection tools:', error) + } }) + document.addEventListener('keydown', (e) => { - if ( - toolbarContainer && - !toolbarContainer.contains(e.target) && - (e.target.nodeName === 'INPUT' || e.target.nodeName === 'TEXTAREA') - ) { - setTimeout(() => { - if (!window.getSelection()?.toString().trim()) deleteToolbar() - }) + try { + if ( + toolbarContainer && + !toolbarContainer.contains(e.target) && + (e.target.nodeName === 'INPUT' || e.target.nodeName === 'TEXTAREA') + ) { + console.debug('[content] Keydown in input/textarea outside toolbar.') + setTimeout(() => { + try { + if (!window.getSelection()?.toString().trim()) { + console.debug('[content] No selection after keydown, deleting toolbar.') + deleteToolbar() + } + } catch (err_inner) { + console.error('[content] Error in keydown setTimeout callback:', err_inner) + } + }, 0) + } + } catch (error) { + console.error('[content] Error in keydown listener for selection tools:', error) } }) } async function prepareForSelectionToolsTouch() { + console.log('[content] Initializing touch selection tools.') document.addEventListener('touchend', (e) => { - if (toolbarContainer && toolbarContainer.contains(e.target)) return - if ( - toolbarContainer && - window.getSelection()?.rangeCount > 0 && - toolbarContainer.contains(window.getSelection()?.getRangeAt(0).endContainer.parentElement) - ) - return - - deleteToolbar() - setTimeout(() => { - const selection = window - .getSelection() - ?.toString() - .trim() - .replace(/^-+|-+$/g, '') - if (selection) { - toolbarContainer = createElementAtPosition( - e.changedTouches[0].pageX + 20, - e.changedTouches[0].pageY + 20, - ) - createSelectionTools(toolbarContainer, selection) + try { + if (toolbarContainer?.contains(e.target)) { + console.debug('[content] Touchend inside toolbar, ignoring.') + return } - }) + if ( + window.getSelection()?.rangeCount > 0 && + toolbarContainer?.contains(window.getSelection()?.getRangeAt(0).endContainer.parentElement) + ) { + console.debug('[content] Touchend selection is inside toolbar, ignoring.') + return + } + + deleteToolbar() + setTimeout(async () => { + try { + const selection = window + .getSelection() + ?.toString() + .trim() + .replace(/^-+|-+$/g, '') + if (selection) { + console.debug('[content] Text selected via touch:', selection) + const touch = e.changedTouches[0] + toolbarContainer = createElementAtPosition(touch.pageX + 20, touch.pageY + 20) + await createSelectionTools(toolbarContainer, selection) + } else { + console.debug('[content] No text selected on touchend.') + } + } catch (err) { + console.error( + '[content] Error in touchend setTimeout callback for touch selection tools:', + err, + ) + } + }, 0) + } catch (error) { + console.error('[content] Error in touchend listener for touch selection tools:', error) + } }) - document.addEventListener('touchstart', (e) => { - if (toolbarContainer && toolbarContainer.contains(e.target)) return - document.querySelectorAll('.chatgptbox-toolbar-container').forEach((e) => e.remove()) + document.addEventListener('touchstart', (e) => { + try { + if (toolbarContainer?.contains(e.target)) { + console.debug('[content] Touchstart inside toolbar, ignoring.') + return + } + console.debug('[content] Touchstart outside toolbar, removing existing toolbars.') + document.querySelectorAll('.chatgptbox-toolbar-container').forEach((el) => el.remove()) + toolbarContainer = null + } catch (error) { + console.error('[content] Error in touchstart listener for touch selection tools:', error) + } }) } let menuX, menuY async function prepareForRightClickMenu() { + console.log('[content] Initializing right-click menu handler.') document.addEventListener('contextmenu', (e) => { menuX = e.clientX menuY = e.clientY + console.debug(`[content] Context menu opened at X: ${menuX}, Y: ${menuY}`) }) Browser.runtime.onMessage.addListener(async (message) => { if (message.type === 'CREATE_CHAT') { - const data = message.data - let prompt = '' - if (data.itemId in toolsConfig) { - prompt = await toolsConfig[data.itemId].genPrompt(data.selectionText) - } else if (data.itemId in menuConfig) { - const menuItem = menuConfig[data.itemId] - if (!menuItem.genPrompt) return - else prompt = await menuItem.genPrompt() - if (prompt) prompt = await cropText(`Reply in ${await getPreferredLanguage()}.\n` + prompt) + console.log('[content] Received CREATE_CHAT message:', message) + try { + const data = message.data + let prompt = '' + if (data.itemId in toolsConfig) { + console.debug('[content] Generating prompt from toolsConfig for item:', data.itemId) + prompt = await toolsConfig[data.itemId].genPrompt(data.selectionText) + } else if (data.itemId in menuConfig) { + console.debug('[content] Generating prompt from menuConfig for item:', data.itemId) + const menuItem = menuConfig[data.itemId] + if (!menuItem.genPrompt) { + console.warn('[content] No genPrompt for menu item:', data.itemId) + return + } + prompt = await menuItem.genPrompt() + if (prompt) { + const preferredLanguage = await getPreferredLanguage() + prompt = await cropText(`Reply in ${preferredLanguage}.\n` + prompt) + } + } else { + console.warn('[content] Unknown itemId for CREATE_CHAT:', data.itemId) + return + } + console.debug('[content] Generated prompt:', prompt) + + const position = data.useMenuPosition + ? { x: menuX, y: menuY } + : { x: window.innerWidth / 2 - 300, y: window.innerHeight / 2 - 200 } + console.debug('[content] Toolbar position for CREATE_CHAT:', position) + const container = createElementAtPosition(position.x, position.y) + container.className = 'chatgptbox-toolbar-container-not-queryable' + const userConfig = await getUserConfig() + render( + , + container, + ) + console.log('[content] CREATE_CHAT toolbar rendered.') + } catch (error) { + console.error('[content] Error processing CREATE_CHAT message:', error, message) } - - const position = data.useMenuPosition - ? { x: menuX, y: menuY } - : { x: window.innerWidth / 2 - 300, y: window.innerHeight / 2 - 200 } - const container = createElementAtPosition(position.x, position.y) - container.className = 'chatgptbox-toolbar-container-not-queryable' - const userConfig = await getUserConfig() - render( - , - container, - ) } }) } async function prepareForStaticCard() { - const userConfig = await getUserConfig() - let siteRegex - if (userConfig.useSiteRegexOnly) siteRegex = userConfig.siteRegex - else - siteRegex = new RegExp( - (userConfig.siteRegex && userConfig.siteRegex + '|') + Object.keys(siteConfig).join('|'), - ) - - const matches = location.hostname.match(siteRegex) - if (matches) { - const siteName = matches[0] + console.log('[content] Initializing static card.') + try { + const userConfig = await getUserConfig() + let siteRegexPattern + if (userConfig.useSiteRegexOnly) { + siteRegexPattern = userConfig.siteRegex + } else { + siteRegexPattern = + (userConfig.siteRegex ? userConfig.siteRegex + '|' : '') + + Object.keys(siteConfig) + .filter((k) => k) + .join('|') + } - if ( - userConfig.siteAdapters.includes(siteName) && - !userConfig.activeSiteAdapters.includes(siteName) - ) + if (!siteRegexPattern) { + console.debug('[content] No site regex pattern defined for static card.') return + } + const siteRegex = new RegExp(siteRegexPattern) + console.debug('[content] Static card site regex:', siteRegex) + + const matches = location.hostname.match(siteRegex) + if (matches) { + const siteName = matches[0] + console.log(`[content] Static card matched site: ${siteName}`) + + if ( + userConfig.siteAdapters.includes(siteName) && + !userConfig.activeSiteAdapters.includes(siteName) + ) { + console.log( + `[content] Site adapter for ${siteName} is installed but not active. Skipping static card.`, + ) + return + } - let initSuccess = true - if (siteName in siteConfig) { - const siteAction = siteConfig[siteName].action - if (siteAction && siteAction.init) { - initSuccess = await siteAction.init(location.hostname, userConfig, getInput, mountComponent) + let initSuccess = true + if (siteName in siteConfig) { + const siteAdapterAction = siteConfig[siteName].action + if (siteAdapterAction?.init) { + console.debug(`[content] Initializing site adapter action for ${siteName}.`) + initSuccess = await siteAdapterAction.init( + location.hostname, + userConfig, + getInput, + mountComponent, + ) + console.debug(`[content] Site adapter init success for ${siteName}: ${initSuccess}`) + } } - } - if (initSuccess) mountComponent(siteConfig[siteName]) + if (initSuccess) { + console.log(`[content] Mounting static card for site: ${siteName}`) + await mountComponent(siteConfig[siteName]) + } else { + console.warn(`[content] Static card init failed for site: ${siteName}`) + } + } else { + console.debug('[content] No static card match for current site:', location.hostname) + } + } catch (error) { + console.error('[content] Error in prepareForStaticCard:', error) } } async function overwriteAccessToken() { - if (location.hostname !== 'chatgpt.com') { + console.debug('[content] overwriteAccessToken called for hostname:', location.hostname) + try { if (location.hostname === 'kimi.moonshot.cn') { - setUserConfig({ - kimiMoonShotRefreshToken: window.localStorage.refresh_token, - }) + console.log('[content] On kimi.moonshot.cn, attempting to save refresh token.') + const refreshToken = window.localStorage.refresh_token + if (refreshToken) { + await setUserConfig({ kimiMoonShotRefreshToken: refreshToken }) + console.log('[content] Kimi Moonshot refresh token saved.') + } else { + console.warn('[content] Kimi Moonshot refresh token not found in localStorage.') + } + return } - return - } - let data - if (location.pathname === '/api/auth/session') { - const response = document.querySelector('pre').textContent - try { - data = JSON.parse(response) - } catch (error) { - console.error('json error', error) + if (location.hostname !== 'chatgpt.com') { + console.debug('[content] Not on chatgpt.com, skipping access token overwrite.') + return } - } else { - const resp = await fetch('https://chatgpt.com/api/auth/session') - data = await resp.json().catch(() => ({})) - } - if (data && data.accessToken) { - await setAccessToken(data.accessToken) - console.log(data.accessToken) - } -} - -async function prepareForForegroundRequests() { - if (location.hostname !== 'chatgpt.com' || location.pathname === '/auth/login') return - const userConfig = await getUserConfig() - - if ( - !chatgptWebModelKeys.some((model) => - getApiModesStringArrayFromConfig(userConfig, true).includes(model), - ) - ) - return - - if (location.pathname === '/') { - const input = document.querySelector('#prompt-textarea') - if (input) { - input.textContent = ' ' - input.dispatchEvent(new Event('input', { bubbles: true })) - setTimeout(() => { - input.textContent = '' - input.dispatchEvent(new Event('input', { bubbles: true })) - }, 300) + console.log('[content] On chatgpt.com, attempting to overwrite access token.') + let data + if (location.pathname === '/api/auth/session') { + console.debug('[content] On /api/auth/session page.') + const preElement = document.querySelector('pre') + if (preElement?.textContent) { + const response = preElement.textContent + try { + data = JSON.parse(response) + console.debug('[content] Parsed access token data from
 tag.')
+        } catch (error) {
+          console.error('[content] Failed to parse JSON from 
 tag for access token:', error)
+        }
+      } else {
+        console.warn('[content] 
 tag not found or empty for access token on /api/auth/session.')
+      }
+    } else {
+      console.debug('[content] Not on /api/auth/session page, fetching token from API endpoint.')
+      try {
+        const resp = await fetch('https://chatgpt.com/api/auth/session')
+        if (resp.ok) {
+          data = await resp.json()
+          console.debug('[content] Fetched access token data from API endpoint.')
+        } else {
+          console.warn(
+            `[content] Failed to fetch access token, status: ${resp.status}`,
+            await resp.text(),
+          )
+        }
+      } catch (error) {
+        console.error('[content] Error fetching access token from API:', error)
+      }
     }
-  }
 
-  await Browser.runtime.sendMessage({
-    type: 'SET_CHATGPT_TAB',
-    data: {},
-  })
-
-  registerPortListener(async (session, port) => {
-    if (isUsingChatgptWebModel(session)) {
-      const accessToken = await getChatGptAccessToken()
-      await generateAnswersWithChatgptWebApi(port, session.question, session, accessToken)
+    if (data?.accessToken) {
+      await setAccessToken(data.accessToken)
+      console.log('[content] ChatGPT Access token has been set successfully from page data.')
+    } else {
+      console.warn('[content] No access token found in page data or fetch response.')
     }
-  })
+  } catch (error) {
+    console.error('[content] Error in overwriteAccessToken:', error)
+  }
 }
 
 async function getClaudeSessionKey() {
-  return Browser.runtime.sendMessage({
-    type: 'GET_COOKIE',
-    data: { url: 'https://claude.ai/', name: 'sessionKey' },
-  })
+  console.debug('[content] getClaudeSessionKey called.')
+  try {
+    const sessionKey = await Browser.runtime.sendMessage({
+      type: 'GET_COOKIE',
+      data: { url: 'https://claude.ai/', name: 'sessionKey' },
+    })
+    console.debug('[content] Claude session key from background:', sessionKey ? 'found' : 'not found')
+    return sessionKey
+  } catch (error) {
+    console.error('[content] Error in getClaudeSessionKey sending message:', error)
+    return null
+  }
 }
 
 async function prepareForJumpBackNotification() {
-  if (
-    location.hostname === 'chatgpt.com' &&
-    document.querySelector('button[data-testid=login-button]')
-  ) {
-    console.log('chatgpt not logged in')
-    return
-  }
-
-  const url = new URL(window.location.href)
-  if (url.searchParams.has('chatgptbox_notification')) {
-    if (location.hostname === 'claude.ai' && !(await getClaudeSessionKey())) {
-      console.log('claude not logged in')
-
-      await new Promise((resolve) => {
-        const timer = setInterval(async () => {
-          const token = await getClaudeSessionKey()
-          if (token) {
-            clearInterval(timer)
-            resolve()
-          }
-        }, 500)
-      })
+  console.log('[content] Initializing jump back notification.')
+  try {
+    if (
+      location.hostname === 'chatgpt.com' &&
+      document.querySelector('button[data-testid=login-button]')
+    ) {
+      console.log('[content] ChatGPT login button found, user not logged in. Skipping jump back.')
+      return
     }
 
-    if (location.hostname === 'kimi.moonshot.cn' && !window.localStorage.refresh_token) {
-      console.log('kimi not logged in')
-      setTimeout(() => {
-        document.querySelectorAll('button').forEach((button) => {
-          if (button.textContent === '立即登录') {
-            button.click()
+    const url = new URL(window.location.href)
+    if (url.searchParams.has('chatgptbox_notification')) {
+      console.log('[content] chatgptbox_notification param found in URL.')
+
+      if (location.hostname === 'claude.ai') {
+        console.debug('[content] On claude.ai, checking login status.')
+        let claudeSession = await getClaudeSessionKey()
+        if (!claudeSession) {
+          console.log('[content] Claude session key not found, waiting for it...')
+          let promiseSettled = false
+          let timerId = null
+          let timeoutId = null
+          const cleanup = () => {
+            if (timerId) clearInterval(timerId)
+            if (timeoutId) clearTimeout(timeoutId)
           }
-        })
-      }, 1000)
-
-      await new Promise((resolve) => {
-        const timer = setInterval(() => {
-          const token = window.localStorage.refresh_token
-          if (token) {
-            setUserConfig({
-              kimiMoonShotRefreshToken: token,
-            })
-            clearInterval(timer)
-            resolve()
+
+          await new Promise((resolve, reject) => {
+            timerId = setInterval(async () => {
+              if (promiseSettled) {
+                console.warn('[content] Promise already settled but Claude interval still running. This indicates a potential cleanup issue.');
+                cleanup()
+                return
+              }
+              try {
+                claudeSession = await getClaudeSessionKey()
+                if (claudeSession) {
+                  if (!promiseSettled) {
+                    promiseSettled = true
+                    cleanup()
+                    console.log('[content] Claude session key found after waiting.')
+                    resolve()
+                  }
+                }
+              } catch (err) {
+                console.error('[content] Error polling for Claude session key:', err)
+                const errMsg = err.message.toLowerCase();
+                if ((errMsg.includes('network') || errMsg.includes('permission')) && !promiseSettled) {
+                  promiseSettled = true;
+                  cleanup();
+                  reject(new Error(`Failed to get Claude session key due to: ${err.message}`));
+                }
+              }
+            }, 500)
+
+            timeoutId = setTimeout(() => {
+              if (!promiseSettled) {
+                promiseSettled = true
+                cleanup()
+                console.warn('[content] Timed out waiting for Claude session key.')
+                reject(new Error('Timed out waiting for Claude session key.'))
+              }
+            }, 30000)
+          }).catch((err) => {
+            console.error('[content] Failed to get Claude session key for jump back notification:', err)
+            return
+          })
+        } else {
+          console.log('[content] Claude session key found immediately.')
+        }
+      }
+
+      if (location.hostname === 'kimi.moonshot.cn') {
+        console.debug('[content] On kimi.moonshot.cn, checking login status.')
+        if (!window.localStorage.refresh_token) {
+          console.log('[content] Kimi refresh token not found, attempting to trigger login.')
+          setTimeout(() => {
+            try {
+              document.querySelectorAll('button').forEach((button) => {
+                if (button.textContent === '立即登录') {
+                  console.log('[content] Clicking Kimi login button.')
+                  button.click()
+                }
+              })
+            } catch (err_click) {
+              console.error('[content] Error clicking Kimi login button:', err_click)
+            }
+          }, 1000)
+
+          let promiseSettled = false
+          let timerId = null
+          let timeoutId = null
+          const cleanup = () => {
+            if (timerId) clearInterval(timerId)
+            if (timeoutId) clearTimeout(timeoutId)
           }
-        }, 500)
-      })
-    }
 
-    const div = document.createElement('div')
-    document.body.append(div)
-    render(
-      ,
-      div,
-    )
+          await new Promise((resolve, reject) => {
+            timerId = setInterval(async () => {
+              if (promiseSettled) {
+                console.warn('[content] Promise already settled but Kimi interval still running. This indicates a potential cleanup issue.');
+                cleanup()
+                return
+              }
+              try {
+                const token = window.localStorage.refresh_token
+                if (token) {
+                  if (!promiseSettled) {
+                    promiseSettled = true
+                    cleanup()
+                    console.log('[content] Kimi refresh token found after waiting.')
+                    await setUserConfig({ kimiMoonShotRefreshToken: token })
+                    console.log('[content] Kimi refresh token saved to config.')
+                    resolve()
+                  }
+                }
+              } catch (err_set) {
+                console.error('[content] Error setting Kimi refresh token from polling:', err_set)
+                const errMsg = err_set.message.toLowerCase();
+                if ((errMsg.includes('network') || errMsg.includes('storage')) && !promiseSettled) { // Example error check
+                  promiseSettled = true;
+                  cleanup();
+                  reject(new Error(`Failed to process Kimi token: ${err_set.message}`));
+                }
+              }
+            }, 500)
+
+            timeoutId = setTimeout(() => {
+              if (!promiseSettled) {
+                promiseSettled = true
+                cleanup()
+                console.warn('[content] Timed out waiting for Kimi refresh token.')
+                reject(new Error('Timed out waiting for Kimi refresh token.'))
+              }
+            }, 30000)
+          }).catch((err) => {
+            console.error('[content] Failed to get Kimi refresh token for jump back notification:', err)
+            return
+          })
+        } else {
+          console.log('[content] Kimi refresh token found in localStorage.')
+          await setUserConfig({ kimiMoonShotRefreshToken: window.localStorage.refresh_token })
+        }
+      }
+
+      console.log('[content] Rendering WebJumpBackNotification.')
+      const div = document.createElement('div')
+      document.body.append(div)
+      render(
+        ,
+        div,
+      )
+      console.log('[content] WebJumpBackNotification rendered.')
+    } else {
+      console.debug('[content] No chatgptbox_notification param in URL.')
+    }
+  } catch (error) {
+    console.error('[content] Error in prepareForJumpBackNotification:', error)
   }
 }
 
 async function run() {
-  await getPreferredLanguageKey().then((lang) => {
-    changeLanguage(lang)
-  })
-  Browser.runtime.onMessage.addListener(async (message) => {
-    if (message.type === 'CHANGE_LANG') {
-      const data = message.data
-      changeLanguage(data.lang)
+  console.log('[content] Script run started.')
+  try {
+    await getPreferredLanguageKey().then((lang) => {
+      console.log(`[content] Setting language to: ${lang}`)
+      changeLanguage(lang)
+    }).catch(err => console.error('[content] Error setting preferred language:', err))
+
+    Browser.runtime.onMessage.addListener(async (message) => {
+      console.debug('[content] Received runtime message:', message)
+      try {
+        if (message.type === 'CHANGE_LANG') {
+          console.log('[content] Processing CHANGE_LANG message:', message.data)
+          changeLanguage(message.data.lang)
+        }
+      } catch (error) {
+        console.error('[content] Error in global runtime.onMessage listener:', error, message)
+      }
+    })
+
+    await overwriteAccessToken()
+    await manageChatGptTabState()
+
+    Browser.storage.onChanged.addListener(async (changes, areaName) => {
+      console.debug('[content] Storage changed:', changes, 'in area:', areaName)
+      try {
+        if (areaName === 'local' && (changes.userConfig || changes.config)) {
+          console.log(
+            '[content] User config changed in storage, re-evaluating ChatGPT tab state.',
+          )
+          await manageChatGptTabState()
+        }
+      } catch (error) {
+        console.error('[content] Error in storage.onChanged listener:', error)
+      }
+    })
+
+    await prepareForSelectionTools()
+    await prepareForSelectionToolsTouch()
+    await prepareForStaticCard()
+    await prepareForRightClickMenu()
+    await prepareForJumpBackNotification()
+
+    console.log('[content] Script run completed successfully.')
+  } catch (error) {
+    console.error('[content] Error in run function:', error)
+  }
+}
+
+async function manageChatGptTabState() {
+  console.debug('[content] manageChatGptTabState called. Current location:', location.href)
+  try {
+    if (location.hostname !== 'chatgpt.com' || location.pathname === '/auth/login') {
+      console.debug(
+        '[content] Not on main chatgpt.com page, skipping manageChatGptTabState logic.',
+      )
+      return
     }
-  })
 
-  await overwriteAccessToken()
-  await prepareForForegroundRequests()
+    const userConfig = await getUserConfig()
+    console.debug('[content] User config in manageChatGptTabState:', userConfig)
+    const isThisTabDesignatedForChatGptWeb = chatgptWebModelKeys.some((model) =>
+      getApiModesStringArrayFromConfig(userConfig, true).includes(model),
+    )
+    console.debug(
+      '[content] Is this tab designated for ChatGPT Web:',
+      isThisTabDesignatedForChatGptWeb,
+    )
+
+    if (isThisTabDesignatedForChatGptWeb) {
+      if (location.pathname === '/') {
+        console.debug('[content] On chatgpt.com root path.')
+        const input = document.querySelector('#prompt-textarea')
+        if (input && input.value === '') {
+          console.log('[content] Manipulating #prompt-textarea for focus.')
+          input.value = ' '
+          input.dispatchEvent(new Event('input', { bubbles: true }))
+          setTimeout(() => {
+            if (input && input.value === ' ') {
+              input.value = ''
+              input.dispatchEvent(new Event('input', { bubbles: true }))
+              console.debug('[content] #prompt-textarea manipulation complete.')
+            } else if (!input) {
+              console.warn('[content] #prompt-textarea no longer available in setTimeout callback.');
+            }
+          }, 300)
+        } else {
+          console.debug(
+            '[content] #prompt-textarea not found, not empty (value: "'+ input?.value +'"), or not on root path for manipulation.',
+          )
+        }
+      }
 
-  prepareForSelectionTools()
-  prepareForSelectionToolsTouch()
-  prepareForStaticCard()
-  prepareForRightClickMenu()
-  prepareForJumpBackNotification()
+      console.log('[content] Sending SET_CHATGPT_TAB message.')
+      await Browser.runtime.sendMessage({
+        type: 'SET_CHATGPT_TAB',
+        data: {},
+      })
+      console.log('[content] SET_CHATGPT_TAB message sent successfully.')
+    } else {
+      console.log('[content] This tab is NOT configured for ChatGPT Web model processing.')
+    }
+  } catch (error) {
+    console.error('[content] Error in manageChatGptTabState:', error)
+  }
+}
+
+if (!window.__chatGPTBoxPortListenerRegistered) {
+  try {
+    if (location.hostname === 'chatgpt.com' && location.pathname !== '/auth/login') {
+      console.log('[content] Attempting to register port listener for chatgpt.com.')
+      registerPortListener(async (session, port) => {
+        console.debug(
+          `[content] Port listener callback triggered. Session:`,
+          session,
+          `Port:`,
+          port.name,
+        )
+        try {
+          if (isUsingChatgptWebModel(session)) {
+            console.log(
+              '[content] Session is for ChatGPT Web Model, processing request for question:',
+              session.question,
+            )
+            const accessToken = await getChatGptAccessToken()
+            if (!accessToken) {
+              console.warn('[content] No ChatGPT access token available for web API call.')
+              port.postMessage({ error: 'Missing ChatGPT access token.' })
+              return
+            }
+            await generateAnswersWithChatgptWebApi(port, session.question, session, accessToken)
+            console.log('[content] generateAnswersWithChatgptWebApi call completed.')
+          } else {
+            console.debug(
+              '[content] Session is not for ChatGPT Web Model, skipping processing in this listener.',
+            )
+          }
+        } catch (e) {
+          console.error('[content] Error in port listener callback:', e, 'Session:', session)
+          try {
+            port.postMessage({ error: e.message || 'An unexpected error occurred in content script port listener.' })
+          } catch (postError) {
+            console.error('[content] Error sending error message back via port:', postError)
+          }
+        }
+      })
+      console.log('[content] Generic port listener registered successfully for chatgpt.com pages.')
+      window.__chatGPTBoxPortListenerRegistered = true
+    } else {
+      console.debug(
+        '[content] Not on chatgpt.com or on login page, skipping port listener registration.',
+      )
+    }
+  } catch (error) {
+    console.error('[content] Error registering global port listener:', error)
+  }
+} else {
+  console.log('[content] Port listener already registered, skipping.')
 }
 
 run()