mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-22 17:57:46 +00:00
chore(chrome-qwen-bridge): connect & them
This commit is contained in:
28
package-lock.json
generated
28
package-lock.json
generated
@@ -3142,6 +3142,10 @@
|
|||||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/@qwen-code/chrome-bridge": {
|
||||||
|
"resolved": "packages/chrome-qwen-bridge",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@qwen-code/qwen-code": {
|
"node_modules/@qwen-code/qwen-code": {
|
||||||
"resolved": "packages/cli",
|
"resolved": "packages/cli",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -13864,7 +13868,6 @@
|
|||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
@@ -17492,6 +17495,29 @@
|
|||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/chrome-qwen-bridge": {
|
||||||
|
"name": "@qwen-code/chrome-bridge",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
|
"@types/react": "^19.1.8",
|
||||||
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"autoprefixer": "^10.4.22",
|
||||||
|
"esbuild": "^0.25.3",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^3.4.18",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@qwen-code/qwen-code",
|
"name": "@qwen-code/qwen-code",
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
|
|||||||
@@ -32,7 +32,14 @@ function connectToNativeHost() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nativePort.onMessage.addListener((message) => {
|
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);
|
handleNativeMessage(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,6 +48,7 @@ function connectToNativeHost() {
|
|||||||
console.log('Native host disconnected');
|
console.log('Native host disconnected');
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Disconnect error:', error);
|
console.error('Disconnect error:', error);
|
||||||
|
console.error('Disconnect error message:', error.message);
|
||||||
}
|
}
|
||||||
nativePort = null;
|
nativePort = null;
|
||||||
isConnected = false;
|
isConnected = false;
|
||||||
@@ -97,14 +105,35 @@ function handleNativeMessage(message) {
|
|||||||
delete nativePort._handshakeTimeout;
|
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
|
// Notify popup of connection
|
||||||
chrome.runtime.sendMessage({
|
chrome.runtime.sendMessage({
|
||||||
type: 'STATUS_UPDATE',
|
type: 'STATUS_UPDATE',
|
||||||
status: qwenCliStatus,
|
status: qwenCliStatus,
|
||||||
capabilities: message.capabilities
|
capabilities: message.capabilities,
|
||||||
|
qwenInstalled: message.qwenInstalled,
|
||||||
|
qwenVersion: message.qwenVersion
|
||||||
}).catch(() => {});
|
}).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) {
|
} else if (message.type === 'response' && message.id !== undefined) {
|
||||||
// Handle response to a specific request
|
// Handle response to a specific request
|
||||||
const handler = pendingRequests.get(message.id);
|
const handler = pendingRequests.get(message.id);
|
||||||
@@ -147,24 +176,226 @@ async function sendToNativeHost(message) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle events from Qwen CLI
|
// Handle browser requests from Qwen CLI (via Native Host)
|
||||||
function handleQwenEvent(event) {
|
async function handleBrowserRequest(message) {
|
||||||
console.log('Qwen event:', event);
|
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) => {
|
chrome.tabs.query({}, (tabs) => {
|
||||||
tabs.forEach(tab => {
|
tabs.forEach(tab => {
|
||||||
chrome.tabs.sendMessage(tab.id, {
|
chrome.tabs.sendMessage(tab.id, {
|
||||||
type: 'QWEN_EVENT',
|
type: 'QWEN_EVENT',
|
||||||
event: event.data
|
event: eventData
|
||||||
}).catch(() => {}); // Ignore errors for tabs without content script
|
}).catch(() => {});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
chrome.runtime.sendMessage({
|
// Broadcast message to all UI components (side panel, popup, etc.)
|
||||||
type: 'QWEN_EVENT',
|
function broadcastToUI(message) {
|
||||||
event: event.data
|
chrome.runtime.sendMessage(message).catch(() => {});
|
||||||
}).catch(() => {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message handlers from extension components
|
// Message handlers from extension components
|
||||||
@@ -192,17 +423,114 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
return false;
|
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') {
|
if (request.type === 'EXTRACT_PAGE_DATA') {
|
||||||
// Request page data from content script
|
// 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]) {
|
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'
|
type: 'EXTRACT_DATA'
|
||||||
}, (response) => {
|
}, (response) => {
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
sendResponse({
|
sendResponse({
|
||||||
success: false,
|
success: false,
|
||||||
error: chrome.runtime.lastError.message
|
error: chrome.runtime.lastError.message + '. Try refreshing the page.'
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
sendResponse(response);
|
sendResponse(response);
|
||||||
|
|||||||
@@ -389,6 +389,14 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'GET_CONSOLE_LOGS':
|
||||||
|
// Get captured console logs
|
||||||
|
sendResponse({
|
||||||
|
success: true,
|
||||||
|
data: consoleLogs.slice() // Return a copy
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
case 'GET_SELECTED_TEXT':
|
case 'GET_SELECTED_TEXT':
|
||||||
// Get currently selected text
|
// Get currently selected text
|
||||||
sendResponse({
|
sendResponse({
|
||||||
|
|||||||
@@ -318,10 +318,33 @@ getSelectedBtn.addEventListener('click', async () => {
|
|||||||
throw new Error('No active tab found');
|
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
|
// Get selected text from content script
|
||||||
const response = await chrome.tabs.sendMessage(tab.id, {
|
let response;
|
||||||
type: 'GET_SELECTED_TEXT'
|
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) {
|
if (response.success && response.data) {
|
||||||
// Send to Qwen CLI
|
// Send to Qwen CLI
|
||||||
@@ -379,10 +402,33 @@ consoleLogsBtn.addEventListener('click', async () => {
|
|||||||
throw new Error('No active tab found');
|
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
|
// Get console logs from content script
|
||||||
const response = await chrome.tabs.sendMessage(tab.id, {
|
let response;
|
||||||
type: 'EXTRACT_DATA'
|
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) {
|
if (response.success) {
|
||||||
const consoleLogs = response.data.consoleLogs || [];
|
const consoleLogs = response.data.consoleLogs || [];
|
||||||
|
|||||||
@@ -3,138 +3,57 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Qwen CLI Bridge</title>
|
<title>Qwen Code</title>
|
||||||
<link rel="stylesheet" href="sidepanel.css">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div id="root">
|
||||||
<!-- Header -->
|
<div class="loading-container">
|
||||||
<header class="header">
|
<div class="loading-spinner"></div>
|
||||||
<div class="logo">
|
<div class="loading-text">Loading Qwen Code...</div>
|
||||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
</div>
|
||||||
<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>
|
</div>
|
||||||
|
<script src="sidepanel-app.js"></script>
|
||||||
<script src="sidepanel.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const { spawn } = require('child_process');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Logging
|
// Logging
|
||||||
@@ -36,12 +37,14 @@ function logDebug(message) {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
function sendMessageToExtension(message) {
|
function sendMessageToExtension(message) {
|
||||||
|
log(`Sending to extension: ${JSON.stringify(message).slice(0, 100)}`);
|
||||||
const buffer = Buffer.from(JSON.stringify(message));
|
const buffer = Buffer.from(JSON.stringify(message));
|
||||||
const length = Buffer.allocUnsafe(4);
|
const length = Buffer.allocUnsafe(4);
|
||||||
length.writeUInt32LE(buffer.length, 0);
|
length.writeUInt32LE(buffer.length, 0);
|
||||||
|
|
||||||
process.stdout.write(length);
|
process.stdout.write(length);
|
||||||
process.stdout.write(buffer);
|
process.stdout.write(buffer);
|
||||||
|
log('Message sent successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
function readMessagesFromExtension() {
|
function readMessagesFromExtension() {
|
||||||
@@ -126,12 +129,30 @@ class AcpConnection {
|
|||||||
try {
|
try {
|
||||||
log(`Starting Qwen CLI with ACP mode in ${cwd}`);
|
log(`Starting Qwen CLI with ACP mode in ${cwd}`);
|
||||||
|
|
||||||
this.process = spawn('qwen', ['--experimental-acp'], {
|
// Chrome 环境没有用户 PATH,需要手动设置
|
||||||
cwd,
|
const env = {
|
||||||
shell: true,
|
...process.env,
|
||||||
windowsHide: true,
|
PATH:
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
'/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:' +
|
||||||
});
|
(process.env.PATH || ''),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.process = spawn(
|
||||||
|
'/Users/yiliang/.npm-global/bin/qwen',
|
||||||
|
[
|
||||||
|
'--experimental-acp',
|
||||||
|
'--allowed-mcp-server-names',
|
||||||
|
'chrome-browser',
|
||||||
|
'--debug',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
cwd,
|
||||||
|
env,
|
||||||
|
shell: true,
|
||||||
|
windowsHide: true,
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!this.process || !this.process.pid) {
|
if (!this.process || !this.process.pid) {
|
||||||
this.process = null;
|
this.process = null;
|
||||||
@@ -169,7 +190,7 @@ class AcpConnection {
|
|||||||
|
|
||||||
sendMessageToExtension({
|
sendMessageToExtension({
|
||||||
type: 'event',
|
type: 'event',
|
||||||
data: { type: 'qwen_stopped', code }
|
data: { type: 'qwen_stopped', code },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -199,8 +220,8 @@ class AcpConnection {
|
|||||||
status: 'running',
|
status: 'running',
|
||||||
pid: this.process.pid,
|
pid: this.process.pid,
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
agentInfo: initResult.data.agentInfo
|
agentInfo: initResult.data.agentInfo,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(`Failed to start Qwen CLI: ${error.message}`);
|
logError(`Failed to start Qwen CLI: ${error.message}`);
|
||||||
@@ -265,8 +286,8 @@ class AcpConnection {
|
|||||||
data: {
|
data: {
|
||||||
type: 'session_update',
|
type: 'session_update',
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
update: params.update
|
update: params.update,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -275,8 +296,8 @@ class AcpConnection {
|
|||||||
type: 'event',
|
type: 'event',
|
||||||
data: {
|
data: {
|
||||||
type: 'auth_update',
|
type: 'auth_update',
|
||||||
authUri: params._meta?.authUri
|
authUri: params._meta?.authUri,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -294,7 +315,7 @@ class AcpConnection {
|
|||||||
requestId: id,
|
requestId: id,
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
toolCall: params.toolCall,
|
toolCall: params.toolCall,
|
||||||
options: params.options
|
options: params.options,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -308,9 +329,32 @@ class AcpConnection {
|
|||||||
this.handleFileWriteRequest(id, params);
|
this.handleFileWriteRequest(id, params);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// Browser MCP Tools
|
||||||
|
case 'browser/read_page':
|
||||||
|
// Get current page content from browser
|
||||||
|
this.handleBrowserReadPage(id, params);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'browser/capture_screenshot':
|
||||||
|
// Capture screenshot of current tab
|
||||||
|
this.handleBrowserCaptureScreenshot(id, params);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'browser/get_network_logs':
|
||||||
|
// Get network logs from browser
|
||||||
|
this.handleBrowserGetNetworkLogs(id, params);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'browser/get_console_logs':
|
||||||
|
// Get console logs from browser
|
||||||
|
this.handleBrowserGetConsoleLogs(id, params);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
log(`Unknown ACP request: ${method}`);
|
log(`Unknown ACP request: ${method}`);
|
||||||
this.sendAcpResponse(id, { error: { code: -32601, message: 'Method not found' } });
|
this.sendAcpResponse(id, {
|
||||||
|
error: { code: -32601, message: 'Method not found' },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,7 +364,7 @@ class AcpConnection {
|
|||||||
this.sendAcpResponse(id, { result: { content } });
|
this.sendAcpResponse(id, { result: { content } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.sendAcpResponse(id, {
|
this.sendAcpResponse(id, {
|
||||||
error: { code: -32000, message: `Failed to read file: ${err.message}` }
|
error: { code: -32000, message: `Failed to read file: ${err.message}` },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,7 +375,85 @@ class AcpConnection {
|
|||||||
this.sendAcpResponse(id, { result: null });
|
this.sendAcpResponse(id, { result: null });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.sendAcpResponse(id, {
|
this.sendAcpResponse(id, {
|
||||||
error: { code: -32000, message: `Failed to write file: ${err.message}` }
|
error: {
|
||||||
|
code: -32000,
|
||||||
|
message: `Failed to write file: ${err.message}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browser request handlers
|
||||||
|
async handleBrowserReadPage(id, params) {
|
||||||
|
try {
|
||||||
|
const data = await sendBrowserRequest('read_page', params);
|
||||||
|
this.sendAcpResponse(id, {
|
||||||
|
result: {
|
||||||
|
url: data.url,
|
||||||
|
title: data.title,
|
||||||
|
content: data.content,
|
||||||
|
links: data.links,
|
||||||
|
images: data.images,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.sendAcpResponse(id, {
|
||||||
|
error: { code: -32000, message: `Failed to read page: ${err.message}` },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleBrowserCaptureScreenshot(id, params) {
|
||||||
|
try {
|
||||||
|
const data = await sendBrowserRequest('capture_screenshot', params);
|
||||||
|
this.sendAcpResponse(id, {
|
||||||
|
result: {
|
||||||
|
dataUrl: data.dataUrl,
|
||||||
|
format: 'png',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.sendAcpResponse(id, {
|
||||||
|
error: {
|
||||||
|
code: -32000,
|
||||||
|
message: `Failed to capture screenshot: ${err.message}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleBrowserGetNetworkLogs(id, params) {
|
||||||
|
try {
|
||||||
|
const data = await sendBrowserRequest('get_network_logs', params);
|
||||||
|
this.sendAcpResponse(id, {
|
||||||
|
result: {
|
||||||
|
logs: data.logs || [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.sendAcpResponse(id, {
|
||||||
|
error: {
|
||||||
|
code: -32000,
|
||||||
|
message: `Failed to get network logs: ${err.message}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleBrowserGetConsoleLogs(id, params) {
|
||||||
|
try {
|
||||||
|
const data = await sendBrowserRequest('get_console_logs', params);
|
||||||
|
this.sendAcpResponse(id, {
|
||||||
|
result: {
|
||||||
|
logs: data.logs || [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.sendAcpResponse(id, {
|
||||||
|
error: {
|
||||||
|
code: -32000,
|
||||||
|
message: `Failed to get console logs: ${err.message}`,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,7 +478,7 @@ class AcpConnection {
|
|||||||
jsonrpc: '2.0',
|
jsonrpc: '2.0',
|
||||||
id,
|
id,
|
||||||
method,
|
method,
|
||||||
params
|
params,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.pendingRequests.delete(id);
|
this.pendingRequests.delete(id);
|
||||||
@@ -377,7 +499,7 @@ class AcpConnection {
|
|||||||
this.sendAcpMessage({
|
this.sendAcpMessage({
|
||||||
jsonrpc: '2.0',
|
jsonrpc: '2.0',
|
||||||
id,
|
id,
|
||||||
...response
|
...response,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +507,7 @@ class AcpConnection {
|
|||||||
this.sendAcpMessage({
|
this.sendAcpMessage({
|
||||||
jsonrpc: '2.0',
|
jsonrpc: '2.0',
|
||||||
method,
|
method,
|
||||||
params
|
params,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,9 +518,15 @@ class AcpConnection {
|
|||||||
clientCapabilities: {
|
clientCapabilities: {
|
||||||
fs: {
|
fs: {
|
||||||
readTextFile: true,
|
readTextFile: true,
|
||||||
writeTextFile: true
|
writeTextFile: true,
|
||||||
}
|
},
|
||||||
}
|
browser: {
|
||||||
|
readPage: true,
|
||||||
|
captureScreenshot: true,
|
||||||
|
getNetworkLogs: true,
|
||||||
|
getConsoleLogs: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
log(`Qwen CLI initialized: ${JSON.stringify(result)}`);
|
log(`Qwen CLI initialized: ${JSON.stringify(result)}`);
|
||||||
@@ -411,9 +539,28 @@ class AcpConnection {
|
|||||||
|
|
||||||
async createSession(cwd) {
|
async createSession(cwd) {
|
||||||
try {
|
try {
|
||||||
|
// Get the path to browser-mcp-server.js
|
||||||
|
const browserMcpServerPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
'browser-mcp-server.js',
|
||||||
|
);
|
||||||
|
|
||||||
|
log(`Creating session with MCP server: ${browserMcpServerPath}`);
|
||||||
|
|
||||||
|
const mcpServersConfig = [
|
||||||
|
{
|
||||||
|
name: 'chrome-browser',
|
||||||
|
command: '/usr/local/bin/node',
|
||||||
|
args: [browserMcpServerPath],
|
||||||
|
env: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
log(`MCP servers config: ${JSON.stringify(mcpServersConfig)}`);
|
||||||
|
|
||||||
const result = await this.sendAcpRequest('session/new', {
|
const result = await this.sendAcpRequest('session/new', {
|
||||||
cwd,
|
cwd,
|
||||||
mcpServers: []
|
mcpServers: mcpServersConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sessionId = result.sessionId;
|
this.sessionId = result.sessionId;
|
||||||
@@ -433,7 +580,7 @@ class AcpConnection {
|
|||||||
try {
|
try {
|
||||||
const result = await this.sendAcpRequest('session/prompt', {
|
const result = await this.sendAcpRequest('session/prompt', {
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
prompt: [{ type: 'text', text }]
|
prompt: [{ type: 'text', text }],
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: true, data: result };
|
return { success: true, data: result };
|
||||||
@@ -450,7 +597,7 @@ class AcpConnection {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.sendAcpNotification('session/cancel', {
|
this.sendAcpNotification('session/cancel', {
|
||||||
sessionId: this.sessionId
|
sessionId: this.sessionId,
|
||||||
});
|
});
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -461,8 +608,10 @@ class AcpConnection {
|
|||||||
respondToPermission(requestId, optionId) {
|
respondToPermission(requestId, optionId) {
|
||||||
this.sendAcpResponse(requestId, {
|
this.sendAcpResponse(requestId, {
|
||||||
result: {
|
result: {
|
||||||
outcome: optionId ? { outcome: 'selected', optionId } : { outcome: 'cancelled' }
|
outcome: optionId
|
||||||
}
|
? { outcome: 'selected', optionId }
|
||||||
|
: { outcome: 'cancelled' },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,11 +636,59 @@ class AcpConnection {
|
|||||||
return {
|
return {
|
||||||
status: this.status,
|
status: this.status,
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
pid: this.process?.pid || null
|
pid: this.process?.pid || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Browser Request Bridge (Native Host <-> Chrome Extension)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Pending browser requests from Qwen CLI that need Chrome Extension responses
|
||||||
|
const pendingBrowserRequests = new Map();
|
||||||
|
let browserRequestId = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a request to Chrome Extension and wait for response
|
||||||
|
*/
|
||||||
|
function sendBrowserRequest(requestType, params) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = ++browserRequestId;
|
||||||
|
pendingBrowserRequests.set(id, { resolve, reject });
|
||||||
|
|
||||||
|
sendMessageToExtension({
|
||||||
|
type: 'browser_request',
|
||||||
|
browserRequestId: id,
|
||||||
|
requestType,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeout after 30 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pendingBrowserRequests.has(id)) {
|
||||||
|
pendingBrowserRequests.delete(id);
|
||||||
|
reject(new Error(`Browser request ${requestType} timed out`));
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle browser response from Chrome Extension
|
||||||
|
*/
|
||||||
|
function handleBrowserResponse(message) {
|
||||||
|
const pending = pendingBrowserRequests.get(message.browserRequestId);
|
||||||
|
if (pending) {
|
||||||
|
pendingBrowserRequests.delete(message.browserRequestId);
|
||||||
|
if (message.error) {
|
||||||
|
pending.reject(new Error(message.error));
|
||||||
|
} else {
|
||||||
|
pending.resolve(message.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Global State
|
// Global State
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -502,10 +699,14 @@ const acpConnection = new AcpConnection();
|
|||||||
async function checkQwenInstallation() {
|
async function checkQwenInstallation() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
const checkProcess = spawn('qwen', ['--version'], {
|
const checkProcess = spawn(
|
||||||
shell: true,
|
'/Users/yiliang/.npm-global/bin/qwen',
|
||||||
windowsHide: true
|
['--version'],
|
||||||
});
|
{
|
||||||
|
shell: true,
|
||||||
|
windowsHide: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let output = '';
|
let output = '';
|
||||||
checkProcess.stdout.on('data', (data) => {
|
checkProcess.stdout.on('data', (data) => {
|
||||||
@@ -538,19 +739,52 @@ async function checkQwenInstallation() {
|
|||||||
// Message Handlers
|
// Message Handlers
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a prompt string from action and data
|
||||||
|
*/
|
||||||
|
function buildPromptFromAction(action, data) {
|
||||||
|
switch (action) {
|
||||||
|
case 'analyze_page':
|
||||||
|
return `Please analyze the following webpage data and provide insights:\n\nURL: ${data.url}\nTitle: ${data.title}\n\nContent:\n${data.content?.text || data.content?.markdown || 'No content available'}\n\nPlease provide a summary and any notable observations.`;
|
||||||
|
|
||||||
|
case 'analyze_screenshot':
|
||||||
|
return `Please analyze the screenshot from this URL: ${data.url}\n\n[Screenshot data provided as base64 image]`;
|
||||||
|
|
||||||
|
case 'ai_analyze':
|
||||||
|
return (
|
||||||
|
data.prompt ||
|
||||||
|
`Please analyze the following webpage:\n\nURL: ${data.pageData?.url}\nTitle: ${data.pageData?.title}\n\nContent:\n${data.pageData?.content?.text || 'No content available'}`
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'process_text':
|
||||||
|
return `Please process the following ${data.context || 'text'}:\n\n${data.text}`;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// For unknown actions, just stringify the data
|
||||||
|
return `Action: ${action}\nData: ${JSON.stringify(data, null, 2)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleExtensionMessage(message) {
|
async function handleExtensionMessage(message) {
|
||||||
log(`Received from extension: ${JSON.stringify(message)}`);
|
log(`Received from extension: ${JSON.stringify(message)}`);
|
||||||
|
|
||||||
|
// Handle browser response (async response from extension for browser requests)
|
||||||
|
if (message.type === 'browser_response') {
|
||||||
|
handleBrowserResponse(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'handshake':
|
case 'handshake':
|
||||||
const installInfo = await checkQwenInstallation();
|
// 立即响应,不等待 qwen 版本检查
|
||||||
response = {
|
response = {
|
||||||
type: 'handshake_response',
|
type: 'handshake_response',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
qwenInstalled: installInfo.installed,
|
qwenInstalled: true, // 假设已安装,后续会验证
|
||||||
qwenVersion: installInfo.version,
|
qwenVersion: 'checking...',
|
||||||
qwenStatus: acpConnection.getStatus().status
|
qwenStatus: acpConnection.getStatus().status,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -560,7 +794,7 @@ async function handleExtensionMessage(message) {
|
|||||||
response = {
|
response = {
|
||||||
type: 'response',
|
type: 'response',
|
||||||
id: message.id,
|
id: message.id,
|
||||||
...startResult
|
...startResult,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -569,7 +803,7 @@ async function handleExtensionMessage(message) {
|
|||||||
response = {
|
response = {
|
||||||
type: 'response',
|
type: 'response',
|
||||||
id: message.id,
|
id: message.id,
|
||||||
...stopResult
|
...stopResult,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -578,7 +812,7 @@ async function handleExtensionMessage(message) {
|
|||||||
response = {
|
response = {
|
||||||
type: 'response',
|
type: 'response',
|
||||||
id: message.id,
|
id: message.id,
|
||||||
...promptResult
|
...promptResult,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -587,7 +821,7 @@ async function handleExtensionMessage(message) {
|
|||||||
response = {
|
response = {
|
||||||
type: 'response',
|
type: 'response',
|
||||||
id: message.id,
|
id: message.id,
|
||||||
...cancelResult
|
...cancelResult,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -596,10 +830,31 @@ async function handleExtensionMessage(message) {
|
|||||||
response = {
|
response = {
|
||||||
type: 'response',
|
type: 'response',
|
||||||
id: message.id,
|
id: message.id,
|
||||||
success: true
|
success: true,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'qwen_request':
|
||||||
|
// Handle generic requests from extension (analyze_page, analyze_screenshot, etc.)
|
||||||
|
// Convert action + data to a prompt for Qwen CLI
|
||||||
|
const promptText = buildPromptFromAction(message.action, message.data);
|
||||||
|
if (acpConnection.status !== 'running') {
|
||||||
|
response = {
|
||||||
|
type: 'response',
|
||||||
|
id: message.id,
|
||||||
|
success: false,
|
||||||
|
error: 'Qwen CLI is not running. Please start it first.',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const actionResult = await acpConnection.prompt(promptText);
|
||||||
|
response = {
|
||||||
|
type: 'response',
|
||||||
|
id: message.id,
|
||||||
|
...actionResult,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'get_status':
|
case 'get_status':
|
||||||
const status = acpConnection.getStatus();
|
const status = acpConnection.getStatus();
|
||||||
const installStatus = await checkQwenInstallation();
|
const installStatus = await checkQwenInstallation();
|
||||||
@@ -609,8 +864,8 @@ async function handleExtensionMessage(message) {
|
|||||||
data: {
|
data: {
|
||||||
...status,
|
...status,
|
||||||
qwenInstalled: installStatus.installed,
|
qwenInstalled: installStatus.installed,
|
||||||
qwenVersion: installStatus.version
|
qwenVersion: installStatus.version,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -618,13 +873,96 @@ async function handleExtensionMessage(message) {
|
|||||||
response = {
|
response = {
|
||||||
type: 'response',
|
type: 'response',
|
||||||
id: message.id,
|
id: message.id,
|
||||||
error: `Unknown message type: ${message.type}`
|
error: `Unknown message type: ${message.type}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessageToExtension(response);
|
sendMessageToExtension(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// HTTP Bridge Server (for browser-mcp-server.js to call)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const HTTP_PORT = 18765;
|
||||||
|
let httpServer = null;
|
||||||
|
|
||||||
|
function startHttpBridgeServer() {
|
||||||
|
if (httpServer) return;
|
||||||
|
|
||||||
|
httpServer = http.createServer(async (req, res) => {
|
||||||
|
// Set CORS headers
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
res.writeHead(405);
|
||||||
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = '';
|
||||||
|
req.on('data', (chunk) => (body += chunk));
|
||||||
|
req.on('end', async () => {
|
||||||
|
try {
|
||||||
|
const request = JSON.parse(body);
|
||||||
|
log(`HTTP Bridge request: ${request.method}`);
|
||||||
|
|
||||||
|
let result;
|
||||||
|
switch (request.method) {
|
||||||
|
case 'read_page':
|
||||||
|
result = await sendBrowserRequest(
|
||||||
|
'read_page',
|
||||||
|
request.params || {},
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'capture_screenshot':
|
||||||
|
result = await sendBrowserRequest(
|
||||||
|
'capture_screenshot',
|
||||||
|
request.params || {},
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'get_network_logs':
|
||||||
|
result = await sendBrowserRequest(
|
||||||
|
'get_network_logs',
|
||||||
|
request.params || {},
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'get_console_logs':
|
||||||
|
result = await sendBrowserRequest(
|
||||||
|
'get_console_logs',
|
||||||
|
request.params || {},
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown method: ${request.method}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify({ success: true, data: result }));
|
||||||
|
} catch (err) {
|
||||||
|
log(`HTTP Bridge error: ${err.message}`);
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end(JSON.stringify({ success: false, error: err.message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
httpServer.listen(HTTP_PORT, '127.0.0.1', () => {
|
||||||
|
log(`HTTP Bridge server started on port ${HTTP_PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
httpServer.on('error', (err) => {
|
||||||
|
logError(`HTTP Bridge server error: ${err.message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Cleanup
|
// Cleanup
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -632,6 +970,10 @@ async function handleExtensionMessage(message) {
|
|||||||
function cleanup() {
|
function cleanup() {
|
||||||
log('Cleaning up...');
|
log('Cleaning up...');
|
||||||
acpConnection.stop();
|
acpConnection.stop();
|
||||||
|
if (httpServer) {
|
||||||
|
httpServer.close();
|
||||||
|
httpServer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
@@ -649,4 +991,5 @@ process.on('SIGTERM', () => {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
log('Native host started (ACP mode)');
|
log('Native host started (ACP mode)');
|
||||||
|
startHttpBridgeServer();
|
||||||
readMessagesFromExtension();
|
readMessagesFromExtension();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
],
|
],
|
||||||
"author": "Qwen Team",
|
"author": "Qwen Team",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"extension/",
|
"extension/",
|
||||||
"native-host/",
|
"native-host/",
|
||||||
@@ -26,18 +27,33 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "./debug.sh",
|
"dev": "./debug.sh",
|
||||||
|
"build:ui": "node esbuild.config.js",
|
||||||
|
"build:ui:watch": "node esbuild.config.js --watch",
|
||||||
|
"build": "npm run build:ui",
|
||||||
"install:extension": "./first-install.sh",
|
"install:extension": "./first-install.sh",
|
||||||
"install:host": "cd native-host && ./smart-install.sh",
|
"install:host": "cd native-host && ./smart-install.sh",
|
||||||
"install:all": "./first-install.sh",
|
"install:all": "./first-install.sh",
|
||||||
"dev:chrome": "open -a 'Google Chrome' --args --load-extension=$PWD/extension --auto-open-devtools-for-tabs",
|
"dev:chrome": "open -a 'Google Chrome' --args --load-extension=$PWD/extension --auto-open-devtools-for-tabs",
|
||||||
"dev:server": "qwen server --port 8080",
|
|
||||||
"build": "./build.sh",
|
|
||||||
"package": "zip -r chrome-qwen-bridge.zip extension/",
|
"package": "zip -r chrome-qwen-bridge.zip extension/",
|
||||||
"clean": "rm -rf dist *.zip /tmp/qwen-bridge-host.log /tmp/qwen-server.log .extension-id",
|
"clean": "rm -rf dist *.zip /tmp/qwen-bridge-host.log /tmp/qwen-server.log .extension-id",
|
||||||
"logs": "tail -f /tmp/qwen-bridge-host.log",
|
"logs": "tail -f /tmp/qwen-bridge-host.log"
|
||||||
"logs:qwen": "tail -f /tmp/qwen-server.log"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
|
"@types/react": "^19.1.8",
|
||||||
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"autoprefixer": "^10.4.22",
|
||||||
|
"esbuild": "^0.25.3",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^3.4.18",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist",
|
"target": "ES2020",
|
||||||
"rootDir": "./",
|
"module": "ESNext",
|
||||||
"composite": true
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": false,
|
||||||
|
"declarationMap": false,
|
||||||
|
"noEmit": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*"],
|
||||||
"extension/**/*",
|
"exclude": ["node_modules", "dist", "extension"]
|
||||||
"native-host/**/*"
|
}
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"dist"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user