chore(chrome-qwen-bridge): connect & them

This commit is contained in:
yiliang114
2025-12-20 18:51:49 +08:00
parent a1f893f0c6
commit cc3cfb5d65
8 changed files with 910 additions and 217 deletions

View File

@@ -32,7 +32,14 @@ function connectToNativeHost() {
}
nativePort.onMessage.addListener((message) => {
console.log('Native message received:', 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);
});
@@ -41,6 +48,7 @@ function connectToNativeHost() {
console.log('Native host disconnected');
if (error) {
console.error('Disconnect error:', error);
console.error('Disconnect error message:', error.message);
}
nativePort = null;
isConnected = false;
@@ -97,14 +105,35 @@ function handleNativeMessage(message) {
delete nativePort._handshakeTimeout;
}
qwenCliStatus = message.qwenStatus || 'connected';
// 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
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);
@@ -147,24 +176,226 @@ async function sendToNativeHost(message) {
});
}
// Handle events from Qwen CLI
function handleQwenEvent(event) {
console.log('Qwen event:', event);
// Handle browser requests from Qwen CLI (via Native Host)
async function handleBrowserRequest(message) {
const { browserRequestId, requestType, params } = message;
console.log('Browser request:', requestType, params);
// Forward event to content scripts and popup
try {
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
});
} catch (error) {
console.error('Browser request error:', error);
nativePort.postMessage({
type: 'browser_response',
browserRequestId,
error: error.message
});
}
}
// 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 === 'user_message_chunk') {
// User message (usually echo)
broadcastToUI({
type: 'message',
data: {
role: 'user',
content: update.content?.text || '',
timestamp: Date.now()
}
});
} 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'
});
}
// Also forward raw event for compatibility
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
event: eventData
}).catch(() => {});
});
});
}
chrome.runtime.sendMessage({
type: 'QWEN_EVENT',
event: event.data
}).catch(() => {});
// Broadcast message to all UI components (side panel, popup, etc.)
function broadcastToUI(message) {
chrome.runtime.sendMessage(message).catch(() => {});
}
// Message handlers from extension components
@@ -192,17 +423,114 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
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') {
broadcastToUI({ type: 'streamStart' });
await sendToNativeHost({
type: 'start_qwen',
cwd: request.data?.cwd || '/'
});
qwenCliStatus = 'running';
}
// Send the prompt
await sendToNativeHost({
type: 'qwen_prompt',
text: text
});
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 }, (tabs) => {
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
if (tabs[0]) {
chrome.tabs.sendMessage(tabs[0].id, {
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
error: chrome.runtime.lastError.message + '. Try refreshing the page.'
});
} else {
sendResponse(response);

View File

@@ -389,6 +389,14 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
});
break;
case 'GET_CONSOLE_LOGS':
// Get captured console logs
sendResponse({
success: true,
data: consoleLogs.slice() // Return a copy
});
break;
case 'GET_SELECTED_TEXT':
// Get currently selected text
sendResponse({

View File

@@ -318,10 +318,33 @@ getSelectedBtn.addEventListener('click', async () => {
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 this page (browser internal page)');
}
// Try to inject content script first
try {
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content/content-script.js']
});
} catch (injectError) {
console.log('Script injection skipped:', injectError.message);
}
// Get selected text from content script
const response = await chrome.tabs.sendMessage(tab.id, {
type: 'GET_SELECTED_TEXT'
});
let response;
try {
response = await chrome.tabs.sendMessage(tab.id, {
type: 'GET_SELECTED_TEXT'
});
} catch (msgError) {
throw new Error('Cannot connect to page. Please refresh the page and try again.');
}
if (response.success && response.data) {
// Send to Qwen CLI
@@ -379,10 +402,33 @@ consoleLogsBtn.addEventListener('click', async () => {
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 this page (browser internal page)');
}
// Try to inject content script first
try {
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content/content-script.js']
});
} catch (injectError) {
console.log('Script injection skipped:', injectError.message);
}
// Get console logs from content script
const response = await chrome.tabs.sendMessage(tab.id, {
type: 'EXTRACT_DATA'
});
let response;
try {
response = await chrome.tabs.sendMessage(tab.id, {
type: 'EXTRACT_DATA'
});
} catch (msgError) {
throw new Error('Cannot connect to page. Please refresh the page and try again.');
}
if (response.success) {
const consoleLogs = response.data.consoleLogs || [];

View File

@@ -3,138 +3,57 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Qwen CLI Bridge</title>
<link rel="stylesheet" href="sidepanel.css">
<title>Qwen Code</title>
<style>
/* Base reset and full-height container */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #root {
width: 100%;
height: 100%;
overflow: hidden;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: #1e1e1e;
color: #e0e0e0;
}
/* Loading state */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 16px;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #333;
border-top-color: #615fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
color: #888;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<div class="logo">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<h1>Qwen CLI Bridge</h1>
</div>
<div class="status-indicator" id="statusIndicator">
<span class="status-dot"></span>
<span class="status-text">Disconnected</span>
</div>
</header>
<!-- Connection Section -->
<section class="section connection-section">
<h2>Connection</h2>
<div class="connection-controls">
<button id="connectBtn" class="btn btn-primary">
Connect to Qwen CLI
</button>
<button id="startQwenBtn" class="btn btn-secondary" disabled>
Start Qwen CLI
</button>
</div>
<div id="connectionError" class="error-message" style="display: none;"></div>
</section>
<!-- Actions Section -->
<section class="section actions-section">
<h2>Quick Actions</h2>
<div class="action-buttons">
<button id="extractDataBtn" class="action-btn" disabled>
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Extract Page Data
</button>
<button id="captureScreenBtn" class="action-btn" disabled>
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Capture Screenshot
</button>
<button id="analyzePageBtn" class="action-btn" disabled>
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
Analyze with AI
</button>
<button id="getSelectedBtn" class="action-btn" disabled>
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
Send Selected Text
</button>
<button id="networkLogsBtn" class="action-btn" disabled>
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
</svg>
Network Logs
</button>
<button id="consoleLogsBtn" class="action-btn" disabled>
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Console Logs
</button>
</div>
</section>
<!-- Response Section -->
<section class="section response-section" id="responseSection" style="display: none;">
<h2>Response</h2>
<div class="response-container">
<div class="response-header">
<span id="responseType" class="response-type"></span>
<button id="copyResponseBtn" class="btn-icon" title="Copy to clipboard">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</div>
<pre id="responseContent" class="response-content"></pre>
</div>
</section>
<!-- Settings Section -->
<section class="section settings-section">
<details>
<summary>Advanced Settings</summary>
<div class="settings-content">
<div class="setting-item">
<label for="mcpServers">MCP Servers:</label>
<input type="text" id="mcpServers" placeholder="chrome-devtools,playwright" />
</div>
<div class="setting-item">
<label for="httpPort">HTTP Port:</label>
<input type="number" id="httpPort" placeholder="8080" value="8080" />
</div>
<div class="setting-item">
<label for="autoConnect">
<input type="checkbox" id="autoConnect" />
Auto-connect on startup
</label>
</div>
<button id="saveSettingsBtn" class="btn btn-small">Save Settings</button>
</div>
</details>
</section>
<!-- Footer -->
<footer class="footer">
<a href="#" id="openOptionsBtn">Options</a>
<span></span>
<a href="#" id="helpBtn">Help</a>
<span></span>
<span class="version">v1.0.0</span>
</footer>
<div id="root">
<div class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">Loading Qwen Code...</div>
</div>
</div>
<script src="sidepanel.js"></script>
<script src="sidepanel-app.js"></script>
</body>
</html>