Files
qwen-code/packages/chrome-qwen-bridge/extension/background/service-worker.js
2025-12-20 00:58:41 +08:00

366 lines
10 KiB
JavaScript

/**
* 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;
// 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) => {
console.log('Native message received:', message);
handleNativeMessage(message);
});
nativePort.onDisconnect.addListener(() => {
const error = chrome.runtime.lastError;
console.log('Native host disconnected');
if (error) {
console.error('Disconnect error:', error);
}
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;
}
qwenCliStatus = message.qwenStatus || 'connected';
// Notify popup of connection
chrome.runtime.sendMessage({
type: 'STATUS_UPDATE',
status: qwenCliStatus,
capabilities: message.capabilities
}).catch(() => {});
} 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);
}
} 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
setTimeout(() => {
if (pendingRequests.has(id)) {
pendingRequests.delete(id);
reject(new Error('Request timeout'));
}
}, 30000); // 30 second timeout
});
}
// Handle events from Qwen CLI
function handleQwenEvent(event) {
console.log('Qwen event:', event);
// Forward event to content scripts and popup
chrome.tabs.query({}, (tabs) => {
tabs.forEach(tab => {
chrome.tabs.sendMessage(tab.id, {
type: 'QWEN_EVENT',
event: event.data
}).catch(() => {}); // Ignore errors for tabs without content script
});
});
chrome.runtime.sendMessage({
type: 'QWEN_EVENT',
event: event.data
}).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 });
})
.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
});
return false;
}
if (request.type === 'EXTRACT_PAGE_DATA') {
// Request page data from content script
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]) {
chrome.tabs.sendMessage(tabs[0].id, {
type: 'EXTRACT_DATA'
}, (response) => {
if (chrome.runtime.lastError) {
sendResponse({
success: false,
error: chrome.runtime.lastError.message
});
} 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
sendToNativeHost({
type: 'qwen_request',
action: request.action,
data: request.data
})
.then(response => {
sendResponse({ success: true, data: response });
})
.catch(error => {
sendResponse({ success: false, error: error.message });
});
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
}
});
// 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
}
});
// Open side panel when extension icon is clicked
chrome.action.onClicked.addListener((tab) => {
chrome.sidePanel.open({ windowId: tab.windowId });
});
// Export for testing
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
connectToNativeHost,
sendToNativeHost
};
}