/** * Background Service Worker for Qwen CLI Bridge * Handles communication between extension components and native host */ // Native messaging host name const NATIVE_HOST_NAME = 'com.qwen.cli.bridge'; // Connection state let nativePort = null; let isConnected = false; let qwenCliStatus = 'disconnected'; let pendingRequests = new Map(); let requestId = 0; // Cache the latest available commands so late listeners (e.g. sidepanel opened later) // can fetch them via GET_STATUS. let lastAvailableCommands = []; // Cache latest MCP tools list (from notifications/tools/list_changed) let lastMcpTools = []; // Static list of internal Chrome browser MCP tools exposed by this extension const INTERNAL_MCP_TOOLS = [ { name: 'browser_read_page', description: 'Read content of the current active tab (url, title, text, links, images).', }, { name: 'browser_capture_screenshot', description: 'Capture a screenshot (PNG) of the current visible tab (base64).', }, { name: 'browser_get_network_logs', description: 'Get recent Network.* events from Chrome debugger for the active tab.', }, { name: 'browser_get_console_logs', description: 'Get recent console logs from the active tab.', }, ]; // Heuristic: detect if user intent asks to read current page function shouldTriggerReadPage(text) { if (!text) return false; const t = String(text).toLowerCase(); const keywords = [ 'read this page', 'read the page', 'read current page', 'read page', '读取当前页面', '读取页面', '读取网页', '读这个页面', ]; return keywords.some((k) => t.includes(k)); } // Heuristic: detect if user intent asks for console logs function shouldTriggerConsoleLogs(text) { if (!text) return false; const t = String(text).toLowerCase(); const keywords = [ 'console log', 'console logs', 'get console', 'show console', 'browser console', 'console 日志', 'console日志', '控制台日志', '获取控制台', '查看控制台', '读取日志', '日志信息', '获取日志', '查看日志', 'log信息', '错误日志', 'error log', ]; return keywords.some((k) => t.includes(k)); } // Heuristic: detect if user intent asks for screenshot function shouldTriggerScreenshot(text) { if (!text) return false; const t = String(text).toLowerCase(); const keywords = [ 'screenshot', 'capture screen', 'take screenshot', 'screen capture', '截图', '截屏', '屏幕截图', '页面截图', ]; return keywords.some((k) => t.includes(k)); } // Heuristic: detect if user intent asks for network logs function shouldTriggerNetworkLogs(text) { if (!text) return false; const t = String(text).toLowerCase(); const keywords = [ 'network log', 'network logs', 'network request', 'api call', 'http request', '网络日志', '网络请求', 'api请求', '请求日志', '接口请求', '接口日志', 'xhr', 'fetch', '请求记录', '网络记录', ]; return keywords.some((k) => t.includes(k)); } // Connection management function connectToNativeHost() { if (nativePort) { return Promise.resolve(nativePort); } return new Promise((resolve, reject) => { try { console.log('Attempting to connect to Native Host:', NATIVE_HOST_NAME); nativePort = chrome.runtime.connectNative(NATIVE_HOST_NAME); // Check for immediate errors if (chrome.runtime.lastError) { console.error('Chrome runtime error:', chrome.runtime.lastError); reject(new Error(chrome.runtime.lastError.message)); return; } nativePort.onMessage.addListener((message) => { // 简化日志输出,直接显示 data 内容 if (message.type === 'event' && message.data) { console.log( '[Native Event]', message.data.type, message.data.update || message.data, ); } else if (message.type === 'response') { console.log( '[Native Response]', 'id:', message.id, message.success ? '✓' : '✗', message.data || message.error, ); } else { console.log( '[Native Message]', message.type, message.data || message, ); } handleNativeMessage(message); }); nativePort.onDisconnect.addListener(() => { const error = chrome.runtime.lastError; console.log('Native host disconnected'); if (error) { console.error('Disconnect error:', error); console.error('Disconnect error message:', error.message); } nativePort = null; isConnected = false; qwenCliStatus = 'disconnected'; // Reject all pending requests for (const [id, handler] of pendingRequests) { handler.reject(new Error('Native host disconnected')); } pendingRequests.clear(); // Notify popup of disconnection chrome.runtime .sendMessage({ type: 'STATUS_UPDATE', status: 'disconnected', }) .catch(() => {}); // Ignore errors if popup is closed }); // Send initial handshake console.log('Sending handshake...'); nativePort.postMessage({ type: 'handshake', version: '1.0.0' }); // Set timeout for handshake response const handshakeTimeout = setTimeout(() => { console.error('Handshake timeout - no response from Native Host'); if (nativePort) { nativePort.disconnect(); } reject(new Error('Handshake timeout')); }, 5000); // Store timeout so we can clear it when we get response nativePort._handshakeTimeout = handshakeTimeout; isConnected = true; qwenCliStatus = 'connected'; resolve(nativePort); } catch (error) { console.error('Failed to connect to native host:', error); reject(error); } }); } // Handle messages from native host function handleNativeMessage(message) { if (message.type === 'handshake_response') { console.log('Handshake successful:', message); // Clear handshake timeout if (nativePort && nativePort._handshakeTimeout) { clearTimeout(nativePort._handshakeTimeout); delete nativePort._handshakeTimeout; } // Native host is connected, but Qwen CLI might not be running yet // 'disconnected' from host means Qwen CLI is not running, but we ARE connected to native host const hostQwenStatus = message.qwenStatus || 'disconnected'; // Set our status to 'connected' (to native host), or 'running' if Qwen CLI is already running qwenCliStatus = hostQwenStatus === 'running' ? 'running' : 'connected'; // Notify popup of connection chrome.runtime .sendMessage({ type: 'STATUS_UPDATE', status: qwenCliStatus, capabilities: message.capabilities, qwenInstalled: message.qwenInstalled, qwenVersion: message.qwenVersion, }) .catch(() => {}); } else if (message.type === 'browser_request') { // Handle browser requests from Qwen CLI via Native Host handleBrowserRequest(message); } else if (message.type === 'permission_request') { // Forward permission request from Native Host to UI console.log('[Permission Request]', message); broadcastToUI({ type: 'permissionRequest', data: { requestId: message.requestId, sessionId: message.sessionId, toolCall: message.toolCall, options: message.options, }, }); } else if (message.type === 'response' && message.id !== undefined) { // Handle response to a specific request const handler = pendingRequests.get(message.id); if (handler) { if (message.error) { handler.reject(new Error(message.error)); } else { handler.resolve(message.data); } pendingRequests.delete(message.id); } // Heuristic: when a prompt completes, native returns a response with a stop reason. // We use that as the end-of-stream signal so the UI can finalize the assistant message. try { if ( message?.data && (message.data.stopReason || message.data.stop_reason || message.data.status === 'done') ) { broadcastToUI({ type: 'streamEnd' }); } } catch (_) { // ignore } } else if (message.type === 'event') { // Handle events from Qwen CLI handleQwenEvent(message); } } // Send request to native host async function sendToNativeHost(message) { if (!nativePort || !isConnected) { await connectToNativeHost(); } return new Promise((resolve, reject) => { const id = ++requestId; pendingRequests.set(id, { resolve, reject }); nativePort.postMessage({ ...message, id, }); // Set timeout for request // Default 30s, but ACP session creation and MCP discovery can take longer let timeoutMs = 30000; if (message && message.type === 'start_qwen') timeoutMs = 180000; // 3 minutes for startup + MCP discovery if ( message && (message.type === 'qwen_prompt' || message.type === 'qwen_request') ) timeoutMs = 180000; setTimeout(() => { if (pendingRequests.has(id)) { pendingRequests.delete(id); reject(new Error('Request timeout')); } }, timeoutMs); }); } // Handle browser requests from Qwen CLI (via Native Host) async function handleBrowserRequest(message) { const { browserRequestId, requestType, params } = message; console.log('Browser request:', requestType, params); try { // Notify UI tool start try { broadcastToUI({ type: 'toolProgress', data: { name: requestType, stage: 'start' }, }); } catch (_) {} let data; switch (requestType) { case 'read_page': data = await getBrowserPageContent(); break; case 'capture_screenshot': data = await getBrowserScreenshot(); break; case 'get_network_logs': data = await getBrowserNetworkLogs(); break; case 'get_console_logs': data = await getBrowserConsoleLogs(); break; default: throw new Error(`Unknown browser request type: ${requestType}`); } // Send response back to native host nativePort.postMessage({ type: 'browser_response', browserRequestId, data, }); // Notify UI tool end (success) try { broadcastToUI({ type: 'toolProgress', data: { name: requestType, stage: 'end', ok: true }, }); } catch (_) {} } catch (error) { console.error('Browser request error:', error); nativePort.postMessage({ type: 'browser_response', browserRequestId, error: error.message, }); // Notify UI tool end (failure) try { broadcastToUI({ type: 'toolProgress', data: { name: requestType, stage: 'end', ok: false, error: String(error?.message || error), }, }); } catch (_) {} } } // Get current page content async function getBrowserPageContent() { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); const tab = tabs[0]; if (!tab) { throw new Error('No active tab found'); } // Check if we can access this page if ( tab.url && (tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://') || tab.url.startsWith('edge://') || tab.url.startsWith('about:')) ) { throw new Error('Cannot access browser internal page'); } // Try to inject content script try { await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content/content-script.js'], }); } catch (injectError) { console.log('Script injection skipped:', injectError.message); } // Request page data from content script return new Promise((resolve, reject) => { chrome.tabs.sendMessage(tab.id, { type: 'EXTRACT_DATA' }, (response) => { if (chrome.runtime.lastError) { reject( new Error( chrome.runtime.lastError.message + '. Try refreshing the page.', ), ); } else if (response && response.success) { resolve({ url: tab.url, title: tab.title, content: response.data?.content || { text: '', markdown: '' }, links: response.data?.links || [], images: response.data?.images || [], }); } else { reject(new Error(response?.error || 'Failed to extract page data')); } }); }); } // Capture screenshot of current tab async function getBrowserScreenshot() { return new Promise((resolve, reject) => { chrome.tabs.captureVisibleTab(null, { format: 'png' }, (dataUrl) => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); } else { resolve({ dataUrl }); } }); }); } // Get network logs async function getBrowserNetworkLogs() { // Use the existing getNetworkLogs function const logs = await getNetworkLogs(null); return { logs }; } // Get console logs (requires content script) async function getBrowserConsoleLogs() { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); const tab = tabs[0]; if (!tab) { throw new Error('No active tab found'); } // Check if we can access this page if ( tab.url && (tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://') || tab.url.startsWith('edge://') || tab.url.startsWith('about:')) ) { throw new Error('Cannot access browser internal page'); } // Try to inject content script try { await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content/content-script.js'], }); } catch (injectError) { console.log('Script injection skipped:', injectError.message); } // Request console logs from content script return new Promise((resolve, reject) => { chrome.tabs.sendMessage( tab.id, { type: 'GET_CONSOLE_LOGS' }, (response) => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); } else if (response && response.success) { resolve({ logs: response.data || [] }); } else { reject(new Error(response?.error || 'Failed to get console logs')); } }, ); }); } // Handle events from Qwen CLI (ACP events) function handleQwenEvent(event) { const eventData = event.data; // 简化日志:显示事件类型和关键信息 if (eventData?.type === 'session_update') { const update = eventData.update; console.log( '[Qwen]', update?.sessionUpdate, update?.content?.text?.slice(0, 50) || update, ); } else { console.log('[Qwen]', eventData?.type, eventData); } // Map ACP events to UI-compatible messages if (eventData?.type === 'session_update') { const update = eventData.update; if (update?.sessionUpdate === 'agent_message_chunk') { // Stream chunk broadcastToUI({ type: 'streamChunk', data: { chunk: update.content?.text || '' }, }); } else if (update?.sessionUpdate === 'available_commands_update') { // Cache and forward available commands list to UI for visibility/debugging lastAvailableCommands = Array.isArray(update.availableCommands) ? update.availableCommands : []; broadcastToUI({ type: 'availableCommands', data: { availableCommands: lastAvailableCommands }, }); } else if (update?.sessionUpdate === 'user_message_chunk') { // Ignore echo of the user's own message to avoid duplicates in UI. // The sidepanel already appends the user message on submit. // If needed in the future, we can gate this by a feature flag. return; } else if ( update?.sessionUpdate === 'tool_call' || update?.sessionUpdate === 'tool_call_update' ) { // Tool call broadcastToUI({ type: 'toolCall', data: update, }); } else if (update?.sessionUpdate === 'plan') { // Plan update broadcastToUI({ type: 'plan', data: { entries: update.entries }, }); } } else if (eventData?.type === 'qwen_stopped') { qwenCliStatus = 'stopped'; broadcastToUI({ type: 'STATUS_UPDATE', status: 'stopped', }); } else if (eventData?.type === 'auth_update') { const authUri = eventData.authUri; // Forward auth update to UI and try to open auth URL broadcastToUI({ type: 'authUpdate', data: { authUri } }); if (authUri) { try { chrome.tabs.create({ url: authUri }); } catch (_) {} } } else if (eventData?.type === 'tools_list_changed') { // Forward MCP tools list to UI and cache it lastMcpTools = Array.isArray(eventData.tools) ? eventData.tools : []; broadcastToUI({ type: 'mcpTools', data: { tools: lastMcpTools } }); } else if (eventData?.type === 'host_info') { console.log('[Host] Info', eventData); broadcastToUI({ type: 'hostInfo', data: eventData }); } else if (eventData?.type === 'cli_stderr') { console.log('[Qwen STDERR]', eventData.line); broadcastToUI({ type: 'hostLog', data: { line: eventData.line } }); } // Also forward raw event for compatibility chrome.tabs.query({}, (tabs) => { tabs.forEach((tab) => { chrome.tabs .sendMessage(tab.id, { type: 'QWEN_EVENT', event: eventData, }) .catch(() => {}); }); }); } // Broadcast message to all UI components (side panel, popup, etc.) function broadcastToUI(message) { chrome.runtime.sendMessage(message).catch(() => {}); } // Message handlers from extension components chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { console.log('Message received:', request, 'from:', sender); if (request.type === 'CONNECT') { // Connect to native host connectToNativeHost() .then(() => { sendResponse({ success: true, status: qwenCliStatus, internalTools: INTERNAL_MCP_TOOLS, }); // Broadcast internal tools so UI can render tools panel try { broadcastToUI({ type: 'internalMcpTools', data: { tools: INTERNAL_MCP_TOOLS }, }); } catch (_) {} }) .catch((error) => { sendResponse({ success: false, error: error.message }); }); return true; // Will respond asynchronously } if (request.type === 'GET_STATUS') { // Get current connection status sendResponse({ connected: isConnected, status: qwenCliStatus, availableCommands: lastAvailableCommands, mcpTools: lastMcpTools, internalTools: INTERNAL_MCP_TOOLS, }); return false; } // Handle sendMessage from side panel (for chat) if (request.type === 'sendMessage') { const text = request.data?.text; if (!text) { sendResponse({ success: false, error: 'No text provided' }); return false; } // First ensure Qwen CLI is started const startAndSend = async () => { try { // Check if connected if (!isConnected) { await connectToNativeHost(); } // Start Qwen CLI if not running if (qwenCliStatus !== 'running') { try { await sendToNativeHost({ type: 'start_qwen', cwd: request.data?.cwd || '/', }); qwenCliStatus = 'running'; } catch (startError) { // If CLI is already running (but session might still be initializing), // treat it as running and continue if ( startError.message && startError.message.includes('already running') ) { console.log('Qwen CLI already running, continuing...'); qwenCliStatus = 'running'; } else { throw startError; } } } // Fallback: if user intent asks to read page (and MCP might not be available), // directly read the page via content script and send to Qwen for analysis. try { if (shouldTriggerReadPage(text)) { broadcastToUI({ type: 'toolProgress', data: { name: 'read_page', stage: 'start' }, }); const data = await getBrowserPageContent(); // start stream for qwen_request path broadcastToUI({ type: 'streamStart' }); await sendToNativeHost({ type: 'qwen_request', action: 'analyze_page', data: data, userPrompt: text, // Include user's full request for context }); // do not send original prompt to avoid duplication broadcastToUI({ type: 'toolProgress', data: { name: 'read_page', stage: 'end', ok: true }, }); sendResponse({ success: true }); return; } } catch (e) { console.warn('Fallback read_page failed:', e); broadcastToUI({ type: 'toolProgress', data: { name: 'read_page', stage: 'end', ok: false, error: String((e && e.message) || e), }, }); // continue to send prompt normally } // Fallback: get console logs try { const shouldGetConsole = shouldTriggerConsoleLogs(text); console.log( '[Fallback] shouldTriggerConsoleLogs:', shouldGetConsole, 'text:', text, ); if (shouldGetConsole) { console.log('[Fallback] Triggering console logs...'); broadcastToUI({ type: 'toolProgress', data: { name: 'console_logs', stage: 'start' }, }); const data = await getBrowserConsoleLogs(); console.log('[Fallback] Console logs data:', data); const logs = data.logs || []; const formatted = logs .slice(-50) .map((log) => `[${log.type}] ${log.message}`) .join('\n'); broadcastToUI({ type: 'streamStart' }); await sendToNativeHost({ type: 'qwen_request', action: 'process_text', data: { text: `Console logs (last ${Math.min(logs.length, 50)} entries):\n${formatted || '(no logs captured)'}`, context: 'console logs from browser', }, userPrompt: text, // Include user's full request }); broadcastToUI({ type: 'toolProgress', data: { name: 'console_logs', stage: 'end', ok: true }, }); sendResponse({ success: true }); return; } } catch (e) { console.error('[Fallback] Console logs failed:', e); broadcastToUI({ type: 'toolProgress', data: { name: 'console_logs', stage: 'end', ok: false, error: String((e && e.message) || e), }, }); } // Fallback: capture screenshot try { if (shouldTriggerScreenshot(text)) { broadcastToUI({ type: 'toolProgress', data: { name: 'screenshot', stage: 'start' }, }); const screenshot = await getBrowserScreenshot(); const tabs = await chrome.tabs.query({ active: true, currentWindow: true, }); broadcastToUI({ type: 'streamStart' }); await sendToNativeHost({ type: 'qwen_request', action: 'analyze_screenshot', data: { dataUrl: screenshot.dataUrl, url: tabs[0]?.url || 'unknown', }, userPrompt: text, // Include user's full request }); broadcastToUI({ type: 'toolProgress', data: { name: 'screenshot', stage: 'end', ok: true }, }); sendResponse({ success: true }); return; } } catch (e) { console.warn('Fallback screenshot failed:', e); broadcastToUI({ type: 'toolProgress', data: { name: 'screenshot', stage: 'end', ok: false, error: String((e && e.message) || e), }, }); } // Fallback: get network logs try { const shouldGetNetwork = shouldTriggerNetworkLogs(text); console.log( '[Fallback] shouldTriggerNetworkLogs:', shouldGetNetwork, 'text:', text, ); if (shouldGetNetwork) { console.log('[Fallback] Triggering network logs...'); broadcastToUI({ type: 'toolProgress', data: { name: 'network_logs', stage: 'start' }, }); const logs = await getNetworkLogs(null); console.log('[Fallback] Network logs count:', logs?.length); const summary = logs.slice(-50).map((log) => ({ method: log.method, url: log.params?.request?.url || log.params?.documentURL, status: log.params?.response?.status, timestamp: log.timestamp, })); broadcastToUI({ type: 'streamStart' }); await sendToNativeHost({ type: 'qwen_request', action: 'process_text', data: { text: `Network logs (last ${summary.length} entries):\n${JSON.stringify(summary, null, 2)}`, context: 'network request logs from browser', }, userPrompt: text, // Include user's full request }); broadcastToUI({ type: 'toolProgress', data: { name: 'network_logs', stage: 'end', ok: true }, }); sendResponse({ success: true }); return; } } catch (e) { console.error('[Fallback] Network logs failed:', e); broadcastToUI({ type: 'toolProgress', data: { name: 'network_logs', stage: 'end', ok: false, error: String((e && e.message) || e), }, }); } // Send the prompt with retry logic for session initialization // Notify UI that a new stream is starting right before sending prompt broadcastToUI({ type: 'streamStart' }); // Helper to send prompt with retries (session might still be initializing) const sendPromptWithRetry = async (maxRetries = 3, delayMs = 2000) => { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { await sendToNativeHost({ type: 'qwen_prompt', text: text, }); return; // Success } catch (err) { const isSessionError = err.message && (err.message.includes('No active session') || err.message.includes('session')); if (isSessionError && attempt < maxRetries) { console.log( `Session not ready, retry ${attempt}/${maxRetries} in ${delayMs}ms...`, ); await new Promise((r) => setTimeout(r, delayMs)); } else { throw err; } } } }; await sendPromptWithRetry(); sendResponse({ success: true }); } catch (error) { console.error('sendMessage error:', error); broadcastToUI({ type: 'error', data: { message: error.message }, }); sendResponse({ success: false, error: error.message }); } }; startAndSend(); return true; // Will respond asynchronously } // Handle cancel streaming if (request.type === 'cancelStreaming') { sendToNativeHost({ type: 'qwen_cancel' }) .then(() => { broadcastToUI({ type: 'streamEnd' }); sendResponse({ success: true }); }) .catch((error) => { sendResponse({ success: false, error: error.message }); }); return true; } // Handle permission response if (request.type === 'permissionResponse') { sendToNativeHost({ type: 'permission_response', requestId: request.data?.requestId, optionId: request.data?.optionId, }) .then(() => sendResponse({ success: true })) .catch((error) => sendResponse({ success: false, error: error.message })); return true; } if (request.type === 'EXTRACT_PAGE_DATA') { // Request page data from content script chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { if (tabs[0]) { const tab = tabs[0]; // Check if we can inject content script (skip chrome:// and other protected pages) if ( tab.url && (tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://') || tab.url.startsWith('edge://') || tab.url.startsWith('about:')) ) { sendResponse({ success: false, error: 'Cannot access this page (browser internal page)', }); return; } // Try to inject content script first in case it's not loaded try { await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content/content-script.js'], }); } catch (injectError) { // Script might already be injected or page doesn't allow injection console.log('Script injection skipped:', injectError.message); } chrome.tabs.sendMessage( tab.id, { type: 'EXTRACT_DATA', }, (response) => { if (chrome.runtime.lastError) { sendResponse({ success: false, error: chrome.runtime.lastError.message + '. Try refreshing the page.', }); } else { sendResponse(response); } }, ); } else { sendResponse({ success: false, error: 'No active tab found', }); } }); return true; // Will respond asynchronously } if (request.type === 'SEND_TO_QWEN') { // Send data to Qwen CLI via native host const send = async () => { try { // Ensure native host connection if (!isConnected) { await connectToNativeHost(); } // Ensure CLI is running if (qwenCliStatus !== 'running') { await sendToNativeHost({ type: 'start_qwen', cwd: request.data?.cwd || '/', }); qwenCliStatus = 'running'; } // Inform UI that a stream is starting try { broadcastToUI({ type: 'streamStart' }); } catch (_) {} const response = await sendToNativeHost({ type: 'qwen_request', action: request.action, data: request.data, }); sendResponse({ success: true, data: response }); } catch (error) { try { broadcastToUI({ type: 'toolProgress', data: { name: request.action || 'request', stage: 'end', ok: false, error: String(error?.message || error), }, }); broadcastToUI({ type: 'error', data: { message: String(error?.message || error) }, }); } catch (_) {} const errMsg = error && error && error.message ? error && error.message : String(error); sendResponse({ success: false, error: errMsg }); } }; send(); return true; // Will respond asynchronously } if (request.type === 'START_QWEN_CLI') { // Request native host to start Qwen CLI sendToNativeHost({ type: 'start_qwen', config: request.config || {}, }) .then((response) => { qwenCliStatus = 'running'; sendResponse({ success: true, data: response }); }) .catch((error) => { sendResponse({ success: false, error: error.message }); }); return true; // Will respond asynchronously } if (request.type === 'STOP_QWEN_CLI') { // Request native host to stop Qwen CLI sendToNativeHost({ type: 'stop_qwen', }) .then((response) => { qwenCliStatus = 'stopped'; sendResponse({ success: true, data: response }); }) .catch((error) => { sendResponse({ success: false, error: error.message }); }); return true; // Will respond asynchronously } if (request.type === 'CAPTURE_SCREENSHOT') { // Capture screenshot of active tab chrome.tabs.captureVisibleTab(null, { format: 'png' }, (dataUrl) => { if (chrome.runtime.lastError) { sendResponse({ success: false, error: chrome.runtime.lastError.message, }); } else { sendResponse({ success: true, data: dataUrl, }); } }); return true; // Will respond asynchronously } if (request.type === 'GET_NETWORK_LOGS') { // Get network logs (requires debugger API) getNetworkLogs(sender.tab?.id) .then((logs) => { sendResponse({ success: true, data: logs }); }) .catch((error) => { sendResponse({ success: false, error: error.message }); }); return true; // Will respond asynchronously } if (request.type === 'GET_CONSOLE_LOGS') { // Get console logs via content script getBrowserConsoleLogs() .then((res) => { sendResponse({ success: true, data: res.logs || [] }); }) .catch((error) => { sendResponse({ success: false, error: error.message }); }); return true; // Will respond asynchronously } }); // Network logging using debugger API const debuggerTargets = new Map(); async function getNetworkLogs(tabId) { if (!tabId) { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); tabId = tabs[0]?.id; if (!tabId) throw new Error('No active tab found'); } // Check if debugger is already attached if (!debuggerTargets.has(tabId)) { await chrome.debugger.attach({ tabId }, '1.3'); await chrome.debugger.sendCommand({ tabId }, 'Network.enable'); // Store network logs debuggerTargets.set(tabId, { logs: [] }); // Listen for network events chrome.debugger.onEvent.addListener((source, method, params) => { if (source.tabId === tabId) { const target = debuggerTargets.get(tabId); if (target && method.startsWith('Network.')) { target.logs.push({ method, params, timestamp: Date.now(), }); } } }); } const target = debuggerTargets.get(tabId); return target?.logs || []; } // Clean up debugger on tab close chrome.tabs.onRemoved.addListener((tabId) => { if (debuggerTargets.has(tabId)) { chrome.debugger.detach({ tabId }); debuggerTargets.delete(tabId); } }); // Listen for extension installation or update chrome.runtime.onInstalled.addListener((details) => { console.log('Extension installed/updated:', details); if (details.reason === 'install') { // Just log the installation, don't auto-open options console.log('Extension installed for the first time'); // Users can access options from popup menu } // Ensure clicking the action icon opens the side panel (Chrome API) try { if ( chrome.sidePanel && typeof chrome.sidePanel.setPanelBehavior === 'function' ) { // Open side panel when the action icon is clicked // This is the recommended way in recent Chrome versions chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); } } catch (e) { console.warn('Failed to set side panel behavior:', e); } }); // Open side panel when extension icon is clicked chrome.action.onClicked.addListener((tab) => { try { const openForWindow = (winId) => { try { chrome.sidePanel.open({ windowId: winId }); } catch (e) { console.error('Failed to open side panel:', e); } }; if (tab && typeof tab.windowId === 'number') { openForWindow(tab.windowId); } else { // Fallback: get current window and open chrome.windows.getCurrent({}, (win) => { if (win && typeof win.id === 'number') { openForWindow(win.id); } else { console.error('No active window to open side panel'); } }); } } catch (e) { console.error('onClicked handler error:', e); } }); // Also configure side panel behavior on startup (in addition to onInstalled) try { if ( chrome.sidePanel && typeof chrome.sidePanel.setPanelBehavior === 'function' ) { chrome.runtime.onStartup.addListener(() => { try { chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); } catch (e) { console.warn('setPanelBehavior onStartup failed:', e); } }); } } catch (_) {} // Export for testing if (typeof module !== 'undefined' && module.exports) { module.exports = { connectToNativeHost, sendToNativeHost, }; }