chrome-ai-bridge 2.3.9 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/extension/README.md +181 -0
- package/build/extension/background.mjs +1318 -0
- package/build/extension/debug-logger.mjs +148 -0
- package/build/extension/icons/icon-128.png +0 -0
- package/build/extension/icons/icon-16.png +0 -0
- package/build/extension/icons/icon-32.png +0 -0
- package/build/extension/icons/icon-48.png +0 -0
- package/build/extension/icons/icon.svg +19 -0
- package/build/extension/manifest.json +28 -0
- package/build/extension/relay-server.ts +539 -0
- package/build/extension/ui/connect.html +429 -0
- package/build/extension/ui/connect.js +491 -0
- package/build/src/extension/relay-server.js +27 -5
- package/build/src/fast-cdp/extension-raw.js +4 -1
- package/build/src/fast-cdp/fast-chat.js +13 -6
- package/build/src/fast-cdp/network-interceptor.js +96 -26
- package/package.json +2 -1
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chrome-ai-bridge Connect UI
|
|
3
|
+
* Extension2-style simple flow:
|
|
4
|
+
* 1. MCP server opens connect.html?mcpRelayUrl=ws://...
|
|
5
|
+
* 2. Tab list is displayed
|
|
6
|
+
* 3. User selects tab -> Click "Connect"
|
|
7
|
+
* 4. Done
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
class ConnectUI {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.mcpRelayUrl = null;
|
|
13
|
+
this.sessionId = null;
|
|
14
|
+
this.allowTabTakeover = false;
|
|
15
|
+
this.autoMode = false;
|
|
16
|
+
this.autoTabId = null;
|
|
17
|
+
this.autoTabUrl = null;
|
|
18
|
+
this.autoNewTab = false;
|
|
19
|
+
this.debugPanelVisible = false;
|
|
20
|
+
this.autoRefreshInterval = null;
|
|
21
|
+
|
|
22
|
+
// DOM elements
|
|
23
|
+
this.statusEl = document.getElementById('status');
|
|
24
|
+
this.statusTextEl = document.getElementById('status-text');
|
|
25
|
+
this.statusIconEl = this.statusEl.querySelector('.status-icon');
|
|
26
|
+
this.tabSelectionEl = document.getElementById('tab-selection');
|
|
27
|
+
this.tabsListEl = document.getElementById('tabs-list');
|
|
28
|
+
this.connectedViewEl = document.getElementById('connected-view');
|
|
29
|
+
this.connectedTabTitleEl = document.getElementById('connected-tab-title');
|
|
30
|
+
this.connectedTabIdEl = document.getElementById('connected-tab-id');
|
|
31
|
+
this.disconnectBtnEl = document.getElementById('disconnect-btn');
|
|
32
|
+
this.errorViewEl = document.getElementById('error-view');
|
|
33
|
+
this.errorMessageEl = document.getElementById('error-message');
|
|
34
|
+
|
|
35
|
+
// Debug panel elements
|
|
36
|
+
this.debugToggleEl = document.getElementById('debug-toggle');
|
|
37
|
+
this.debugPanelEl = document.getElementById('debug-panel');
|
|
38
|
+
this.logFilterEl = document.getElementById('log-filter');
|
|
39
|
+
this.refreshLogsEl = document.getElementById('refresh-logs');
|
|
40
|
+
this.clearLogsEl = document.getElementById('clear-logs');
|
|
41
|
+
this.exportLogsEl = document.getElementById('export-logs');
|
|
42
|
+
this.debugStatsEl = document.getElementById('debug-stats');
|
|
43
|
+
this.logOutputEl = document.getElementById('log-output');
|
|
44
|
+
|
|
45
|
+
this.init();
|
|
46
|
+
this.initDebugPanel();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async init() {
|
|
50
|
+
try {
|
|
51
|
+
// Parse URL parameters (Extension2 style: parameters are always provided)
|
|
52
|
+
const params = new URLSearchParams(window.location.search);
|
|
53
|
+
this.mcpRelayUrl = params.get('mcpRelayUrl');
|
|
54
|
+
this.sessionId = params.get('sessionId');
|
|
55
|
+
this.allowTabTakeover = params.get('allowTabTakeover') === 'true';
|
|
56
|
+
this.autoMode = params.get('auto') === 'true';
|
|
57
|
+
this.autoTabUrl = params.get('tabUrl');
|
|
58
|
+
this.autoNewTab = params.get('newTab') === 'true';
|
|
59
|
+
const rawTabId = params.get('tabId');
|
|
60
|
+
this.autoTabId =
|
|
61
|
+
rawTabId && /^\d+$/.test(rawTabId) ? Number(rawTabId) : null;
|
|
62
|
+
|
|
63
|
+
// Validate relay URL
|
|
64
|
+
if (!this.mcpRelayUrl) {
|
|
65
|
+
this.showError('Missing mcpRelayUrl parameter. Make sure the MCP server is running.');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!this.validateRelayUrl()) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (this.autoMode) {
|
|
74
|
+
const connected = await this.tryAutoConnect();
|
|
75
|
+
if (connected) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Show tab selection UI
|
|
81
|
+
await this.loadTabs();
|
|
82
|
+
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.showError(`Initialization failed: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
validateRelayUrl() {
|
|
89
|
+
try {
|
|
90
|
+
const url = new URL(this.mcpRelayUrl);
|
|
91
|
+
if (!['127.0.0.1', 'localhost', '::1'].includes(url.hostname)) {
|
|
92
|
+
this.showError('Invalid relay URL: must be loopback address (127.0.0.1)');
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
} catch {
|
|
97
|
+
this.showError('Invalid relay URL format');
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async tryAutoConnect() {
|
|
103
|
+
if (!this.autoTabUrl && !this.autoTabId) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
this.showStatus('Auto-connecting...', 'info', '⚡');
|
|
109
|
+
|
|
110
|
+
const relayResponse = await chrome.runtime.sendMessage({
|
|
111
|
+
type: 'connectToRelay',
|
|
112
|
+
mcpRelayUrl: this.mcpRelayUrl,
|
|
113
|
+
sessionId: this.sessionId,
|
|
114
|
+
});
|
|
115
|
+
if (!relayResponse || !relayResponse.success) {
|
|
116
|
+
throw new Error(relayResponse?.error || 'Relay connection failed');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const connectResponse = await chrome.runtime.sendMessage({
|
|
120
|
+
type: 'connectToTab',
|
|
121
|
+
mcpRelayUrl: this.mcpRelayUrl,
|
|
122
|
+
tabId: this.autoTabId,
|
|
123
|
+
tabUrl: this.autoTabUrl,
|
|
124
|
+
newTab: this.autoNewTab,
|
|
125
|
+
sessionId: this.sessionId,
|
|
126
|
+
allowTabTakeover: this.allowTabTakeover,
|
|
127
|
+
});
|
|
128
|
+
if (!connectResponse || !connectResponse.success) {
|
|
129
|
+
throw new Error(connectResponse?.error || 'Tab connection failed');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.showStatus('Connected. Closing...', 'success', '✅');
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
window.close();
|
|
135
|
+
}, 300);
|
|
136
|
+
return true;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const message =
|
|
139
|
+
error && typeof error === 'object' && 'message' in error
|
|
140
|
+
? error.message
|
|
141
|
+
: String(error);
|
|
142
|
+
this.showStatus(
|
|
143
|
+
`Auto-connect failed (${message}). Select tab manually.`,
|
|
144
|
+
'error',
|
|
145
|
+
'⚠️',
|
|
146
|
+
);
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async loadTabs() {
|
|
152
|
+
try {
|
|
153
|
+
const tabs = await chrome.tabs.query({});
|
|
154
|
+
|
|
155
|
+
// Filter out extension pages and chrome:// URLs
|
|
156
|
+
const filteredTabs = tabs.filter(tab => {
|
|
157
|
+
if (!tab.url) return false;
|
|
158
|
+
if (tab.url.startsWith('chrome://')) return false;
|
|
159
|
+
if (tab.url.startsWith('chrome-extension://')) return false;
|
|
160
|
+
if (tab.url.startsWith('devtools://')) return false;
|
|
161
|
+
return true;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (filteredTabs.length === 0) {
|
|
165
|
+
this.showError('No tabs available. Open a web page first.');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.renderTabs(filteredTabs);
|
|
170
|
+
this.showStatus('Select a tab to connect', 'info', '📋');
|
|
171
|
+
this.tabSelectionEl.classList.remove('hidden');
|
|
172
|
+
|
|
173
|
+
} catch (error) {
|
|
174
|
+
this.showError(`Failed to load tabs: ${error.message}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
renderTabs(tabs) {
|
|
179
|
+
this.tabsListEl.innerHTML = '';
|
|
180
|
+
|
|
181
|
+
// Sort tabs: active tab first, then by window/tab order
|
|
182
|
+
const sortedTabs = [...tabs].sort((a, b) => {
|
|
183
|
+
if (a.active && !b.active) return -1;
|
|
184
|
+
if (!a.active && b.active) return 1;
|
|
185
|
+
if (a.windowId !== b.windowId) return a.windowId - b.windowId;
|
|
186
|
+
return a.index - b.index;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
sortedTabs.forEach(tab => {
|
|
190
|
+
const tabItem = document.createElement('div');
|
|
191
|
+
tabItem.className = `tab-item${tab.active ? ' active' : ''}`;
|
|
192
|
+
tabItem.dataset.tabId = tab.id;
|
|
193
|
+
|
|
194
|
+
// Favicon
|
|
195
|
+
const faviconEl = document.createElement('div');
|
|
196
|
+
faviconEl.className = 'tab-favicon';
|
|
197
|
+
if (tab.favIconUrl && !tab.favIconUrl.startsWith('chrome://')) {
|
|
198
|
+
const img = document.createElement('img');
|
|
199
|
+
img.src = tab.favIconUrl;
|
|
200
|
+
img.onerror = () => { faviconEl.textContent = '🌐'; };
|
|
201
|
+
faviconEl.appendChild(img);
|
|
202
|
+
} else {
|
|
203
|
+
faviconEl.textContent = '🌐';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Tab info
|
|
207
|
+
const infoEl = document.createElement('div');
|
|
208
|
+
infoEl.className = 'tab-info';
|
|
209
|
+
|
|
210
|
+
const titleRow = document.createElement('div');
|
|
211
|
+
titleRow.className = 'tab-title-row';
|
|
212
|
+
|
|
213
|
+
const titleEl = document.createElement('span');
|
|
214
|
+
titleEl.className = 'tab-title';
|
|
215
|
+
titleEl.textContent = tab.title || 'Untitled';
|
|
216
|
+
titleRow.appendChild(titleEl);
|
|
217
|
+
|
|
218
|
+
if (tab.active) {
|
|
219
|
+
const badge = document.createElement('span');
|
|
220
|
+
badge.className = 'tab-active-badge';
|
|
221
|
+
badge.textContent = 'Active';
|
|
222
|
+
titleRow.appendChild(badge);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const urlEl = document.createElement('div');
|
|
226
|
+
urlEl.className = 'tab-url';
|
|
227
|
+
urlEl.textContent = tab.url || '';
|
|
228
|
+
|
|
229
|
+
const idEl = document.createElement('div');
|
|
230
|
+
idEl.className = 'tab-id';
|
|
231
|
+
idEl.textContent = `Tab ID: ${tab.id}`;
|
|
232
|
+
|
|
233
|
+
infoEl.appendChild(titleRow);
|
|
234
|
+
infoEl.appendChild(urlEl);
|
|
235
|
+
infoEl.appendChild(idEl);
|
|
236
|
+
|
|
237
|
+
// Connect button
|
|
238
|
+
const connectBtn = document.createElement('button');
|
|
239
|
+
connectBtn.className = 'tab-connect-btn';
|
|
240
|
+
connectBtn.textContent = 'Connect';
|
|
241
|
+
connectBtn.addEventListener('click', (e) => {
|
|
242
|
+
e.stopPropagation();
|
|
243
|
+
this.connectToTab(tab);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
tabItem.appendChild(faviconEl);
|
|
247
|
+
tabItem.appendChild(infoEl);
|
|
248
|
+
tabItem.appendChild(connectBtn);
|
|
249
|
+
|
|
250
|
+
// Also connect on row click
|
|
251
|
+
tabItem.addEventListener('click', () => {
|
|
252
|
+
this.connectToTab(tab);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
this.tabsListEl.appendChild(tabItem);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async connectToTab(tab) {
|
|
260
|
+
try {
|
|
261
|
+
this.showStatus(`Connecting to "${tab.title}"...`, 'info', '⏳');
|
|
262
|
+
|
|
263
|
+
// Step 1: Connect to relay
|
|
264
|
+
const relayResponse = await chrome.runtime.sendMessage({
|
|
265
|
+
type: 'connectToRelay',
|
|
266
|
+
mcpRelayUrl: this.mcpRelayUrl,
|
|
267
|
+
sessionId: this.sessionId,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (!relayResponse || !relayResponse.success) {
|
|
271
|
+
throw new Error(relayResponse?.error || 'Relay connection failed');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Step 2: Connect to tab
|
|
275
|
+
const connectResponse = await chrome.runtime.sendMessage({
|
|
276
|
+
type: 'connectToTab',
|
|
277
|
+
mcpRelayUrl: this.mcpRelayUrl,
|
|
278
|
+
tabId: tab.id,
|
|
279
|
+
windowId: tab.windowId,
|
|
280
|
+
sessionId: this.sessionId,
|
|
281
|
+
allowTabTakeover: this.allowTabTakeover,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (!connectResponse || !connectResponse.success) {
|
|
285
|
+
throw new Error(connectResponse?.error || 'Tab connection failed');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Show connected view
|
|
289
|
+
this.tabSelectionEl.classList.add('hidden');
|
|
290
|
+
this.connectedViewEl.classList.remove('hidden');
|
|
291
|
+
this.connectedTabTitleEl.textContent = tab.title || 'Untitled';
|
|
292
|
+
this.connectedTabIdEl.textContent = tab.id;
|
|
293
|
+
this.showStatus('Connected', 'success', '✅');
|
|
294
|
+
|
|
295
|
+
// Set up disconnect button
|
|
296
|
+
this.disconnectBtnEl.onclick = () => this.disconnect(tab.id);
|
|
297
|
+
|
|
298
|
+
} catch (error) {
|
|
299
|
+
this.showError(`Connection failed: ${error.message}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async disconnect(tabId) {
|
|
304
|
+
try {
|
|
305
|
+
await chrome.runtime.sendMessage({
|
|
306
|
+
type: 'disconnect',
|
|
307
|
+
tabId: tabId
|
|
308
|
+
});
|
|
309
|
+
} catch {
|
|
310
|
+
// Ignore disconnect errors
|
|
311
|
+
}
|
|
312
|
+
this.reset();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
reset() {
|
|
316
|
+
this.connectedViewEl.classList.add('hidden');
|
|
317
|
+
this.errorViewEl.classList.add('hidden');
|
|
318
|
+
this.loadTabs();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
showStatus(message, type = 'info', icon = '⏳') {
|
|
322
|
+
this.statusTextEl.textContent = message;
|
|
323
|
+
this.statusIconEl.textContent = icon;
|
|
324
|
+
this.statusEl.className = `status ${type}`;
|
|
325
|
+
this.statusEl.classList.remove('hidden');
|
|
326
|
+
this.errorViewEl.classList.add('hidden');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
showError(message) {
|
|
330
|
+
this.showStatus(message, 'error', '❌');
|
|
331
|
+
this.tabSelectionEl.classList.add('hidden');
|
|
332
|
+
this.connectedViewEl.classList.add('hidden');
|
|
333
|
+
this.errorViewEl.classList.remove('hidden');
|
|
334
|
+
this.errorMessageEl.textContent = message;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
escapeHtml(text) {
|
|
338
|
+
const div = document.createElement('div');
|
|
339
|
+
div.textContent = text;
|
|
340
|
+
return div.innerHTML;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ========== Debug Panel Methods ==========
|
|
344
|
+
|
|
345
|
+
initDebugPanel() {
|
|
346
|
+
this.debugToggleEl.addEventListener('click', () => {
|
|
347
|
+
this.toggleDebugPanel();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
this.logFilterEl.addEventListener('change', () => {
|
|
351
|
+
this.refreshLogs();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
this.refreshLogsEl.addEventListener('click', () => {
|
|
355
|
+
this.refreshLogs();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
this.clearLogsEl.addEventListener('click', () => {
|
|
359
|
+
this.clearLogs();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
this.exportLogsEl.addEventListener('click', () => {
|
|
363
|
+
this.exportLogs();
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
toggleDebugPanel() {
|
|
368
|
+
this.debugPanelVisible = !this.debugPanelVisible;
|
|
369
|
+
if (this.debugPanelVisible) {
|
|
370
|
+
this.debugPanelEl.classList.remove('hidden');
|
|
371
|
+
this.debugToggleEl.textContent = 'Hide Debug Logs';
|
|
372
|
+
this.refreshLogs();
|
|
373
|
+
this.autoRefreshInterval = setInterval(() => this.refreshLogs(), 2000);
|
|
374
|
+
} else {
|
|
375
|
+
this.debugPanelEl.classList.add('hidden');
|
|
376
|
+
this.debugToggleEl.textContent = 'Show Debug Logs';
|
|
377
|
+
if (this.autoRefreshInterval) {
|
|
378
|
+
clearInterval(this.autoRefreshInterval);
|
|
379
|
+
this.autoRefreshInterval = null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async refreshLogs() {
|
|
385
|
+
try {
|
|
386
|
+
const filter = this.logFilterEl.value || null;
|
|
387
|
+
const response = await chrome.runtime.sendMessage({
|
|
388
|
+
type: 'getDebugLogs',
|
|
389
|
+
filter: filter,
|
|
390
|
+
limit: 100
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
if (!response || !response.success) {
|
|
394
|
+
this.logOutputEl.textContent = 'Failed to fetch logs';
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Update stats
|
|
399
|
+
const stats = response.stats;
|
|
400
|
+
const state = response.state;
|
|
401
|
+
this.debugStatsEl.innerHTML = `
|
|
402
|
+
<strong>Total Logs:</strong> ${stats.total} |
|
|
403
|
+
<strong>Active Connections:</strong> ${state.activeConnections?.length || 0} |
|
|
404
|
+
<strong>Pending:</strong> ${state.pendingTabSelection?.length || 0}
|
|
405
|
+
<br>
|
|
406
|
+
<strong>By Category:</strong>
|
|
407
|
+
${Object.entries(stats.byCategory || {}).map(([cat, count]) => `${cat}: ${count}`).join(', ') || 'none'}
|
|
408
|
+
`;
|
|
409
|
+
|
|
410
|
+
// Render logs
|
|
411
|
+
const logs = response.logs || [];
|
|
412
|
+
if (logs.length === 0) {
|
|
413
|
+
this.logOutputEl.innerHTML = '<span style="color: #888;">No logs yet</span>';
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const html = logs.map(log => this.formatLogEntry(log)).join('');
|
|
418
|
+
this.logOutputEl.innerHTML = html;
|
|
419
|
+
|
|
420
|
+
// Scroll to bottom
|
|
421
|
+
this.logOutputEl.scrollTop = this.logOutputEl.scrollHeight;
|
|
422
|
+
} catch (error) {
|
|
423
|
+
this.logOutputEl.textContent = `Error: ${error.message}`;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
formatLogEntry(log) {
|
|
428
|
+
const ts = log.ts ? log.ts.split('T')[1].split('.')[0] : '';
|
|
429
|
+
const cat = log.category || 'unknown';
|
|
430
|
+
const catClass = `cat-${cat}`;
|
|
431
|
+
const msg = this.escapeHtml(log.message || '');
|
|
432
|
+
const data = log.data ? this.escapeHtml(JSON.stringify(log.data)) : '';
|
|
433
|
+
|
|
434
|
+
return `<div class="log-entry">` +
|
|
435
|
+
`<span class="ts">${ts}</span> ` +
|
|
436
|
+
`<span class="${catClass}">[${cat.toUpperCase()}]</span> ` +
|
|
437
|
+
`<span class="msg">${msg}</span>` +
|
|
438
|
+
(data ? `<span class="data">${data}</span>` : '') +
|
|
439
|
+
`</div>`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async clearLogs() {
|
|
443
|
+
try {
|
|
444
|
+
await chrome.runtime.sendMessage({ type: 'clearDebugLogs' });
|
|
445
|
+
this.refreshLogs();
|
|
446
|
+
} catch (error) {
|
|
447
|
+
this.showError(`Failed to clear logs: ${error.message}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async exportLogs() {
|
|
452
|
+
try {
|
|
453
|
+
const response = await chrome.runtime.sendMessage({
|
|
454
|
+
type: 'getDebugLogs',
|
|
455
|
+
filter: null,
|
|
456
|
+
limit: 500
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
if (!response || !response.success) {
|
|
460
|
+
this.showError('Failed to export logs');
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const exportData = {
|
|
465
|
+
timestamp: new Date().toISOString(),
|
|
466
|
+
stats: response.stats,
|
|
467
|
+
state: response.state,
|
|
468
|
+
logs: response.logs
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
|
472
|
+
const url = URL.createObjectURL(blob);
|
|
473
|
+
const a = document.createElement('a');
|
|
474
|
+
a.href = url;
|
|
475
|
+
a.download = `chrome-ai-bridge-debug-${Date.now()}.json`;
|
|
476
|
+
a.click();
|
|
477
|
+
URL.revokeObjectURL(url);
|
|
478
|
+
} catch (error) {
|
|
479
|
+
this.showError(`Failed to export logs: ${error.message}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Initialize when DOM is ready
|
|
485
|
+
if (document.readyState === 'loading') {
|
|
486
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
487
|
+
new ConnectUI();
|
|
488
|
+
});
|
|
489
|
+
} else {
|
|
490
|
+
new ConnectUI();
|
|
491
|
+
}
|
|
@@ -29,6 +29,7 @@ export class RelayServer extends EventEmitter {
|
|
|
29
29
|
discoveryServer = null;
|
|
30
30
|
discoveryPort = null;
|
|
31
31
|
keepAliveTimer = null;
|
|
32
|
+
_lastDiscoveryOptions = {};
|
|
32
33
|
constructor(options = {}) {
|
|
33
34
|
super();
|
|
34
35
|
this.host = options.host || '127.0.0.1';
|
|
@@ -172,6 +173,9 @@ export class RelayServer extends EventEmitter {
|
|
|
172
173
|
this.ready = true;
|
|
173
174
|
debugLog(`[RelayServer] Connection ready for tab ${this.tabId}`);
|
|
174
175
|
this.emit('ready', this.tabId);
|
|
176
|
+
// Release discovery port after WebSocket is established.
|
|
177
|
+
// 1-second grace period lets Extension finish processing the response.
|
|
178
|
+
setTimeout(() => this.stopDiscoveryServer(), 1000);
|
|
175
179
|
break;
|
|
176
180
|
case 'pong':
|
|
177
181
|
debugLog('[RelayServer] Received keep-alive pong');
|
|
@@ -269,6 +273,7 @@ export class RelayServer extends EventEmitter {
|
|
|
269
273
|
* Extension polls this endpoint when user clicks the extension icon.
|
|
270
274
|
*/
|
|
271
275
|
async startDiscoveryServer(options = {}) {
|
|
276
|
+
this._lastDiscoveryOptions = options;
|
|
272
277
|
const ports = [38765, 38766, 38767, 38768, 38769, 38770, 38771, 38772, 38773, 38774, 38775];
|
|
273
278
|
const wsUrl = this.getConnectionURL();
|
|
274
279
|
for (const port of ports) {
|
|
@@ -332,6 +337,27 @@ export class RelayServer extends EventEmitter {
|
|
|
332
337
|
debugLog('[RelayServer] Could not start discovery server on any port');
|
|
333
338
|
return null;
|
|
334
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Release the discovery HTTP server (port).
|
|
342
|
+
* Called automatically after the Extension WebSocket connects (ready event).
|
|
343
|
+
* The port becomes available for other sessions.
|
|
344
|
+
*/
|
|
345
|
+
stopDiscoveryServer() {
|
|
346
|
+
if (this.discoveryServer) {
|
|
347
|
+
this.discoveryServer.close();
|
|
348
|
+
debugLog(`[RelayServer] Discovery server released (port ${this.discoveryPort})`);
|
|
349
|
+
this.discoveryServer = null;
|
|
350
|
+
this.discoveryPort = null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Re-acquire a discovery port (e.g. after WebSocket disconnect for reconnection).
|
|
355
|
+
* Uses the same options as the last startDiscoveryServer() call.
|
|
356
|
+
*/
|
|
357
|
+
async restartDiscoveryServer(options) {
|
|
358
|
+
this.stopDiscoveryServer();
|
|
359
|
+
return this.startDiscoveryServer(options || this._lastDiscoveryOptions);
|
|
360
|
+
}
|
|
335
361
|
/**
|
|
336
362
|
* Stop server
|
|
337
363
|
*/
|
|
@@ -349,11 +375,7 @@ export class RelayServer extends EventEmitter {
|
|
|
349
375
|
this.ready = false;
|
|
350
376
|
this.tabId = null;
|
|
351
377
|
this.rejectPendingRequests(new Error('RELAY_STOPPED: Relay stopped before request completion'));
|
|
352
|
-
|
|
353
|
-
this.discoveryServer.close();
|
|
354
|
-
this.discoveryServer = null;
|
|
355
|
-
this.discoveryPort = null;
|
|
356
|
-
}
|
|
378
|
+
this.stopDiscoveryServer();
|
|
357
379
|
if (this.wss) {
|
|
358
380
|
return new Promise((resolve) => {
|
|
359
381
|
this.wss.close(() => {
|
|
@@ -5,7 +5,10 @@ import { RelayServer } from '../extension/relay-server.js';
|
|
|
5
5
|
import { logRelay, logExtension, logInfo, logError } from './mcp-logger.js';
|
|
6
6
|
// Stable extension ID (from manifest.json key)
|
|
7
7
|
const EXTENSION_ID = 'ibjplbopgmcacpmfpnaeoloepdhenlbm';
|
|
8
|
-
|
|
8
|
+
// Wake connect page disabled by default — it opens a chrome-extension:// URL
|
|
9
|
+
// that gets ERR_BLOCKED_BY_CLIENT and annoys users. Discovery polling is the
|
|
10
|
+
// primary mechanism; wake is only useful in rare edge cases.
|
|
11
|
+
const ENABLE_WAKE_CONNECT_PAGE = process.env.CAI_ENABLE_WAKE_CONNECT_PAGE === '1';
|
|
9
12
|
/**
|
|
10
13
|
* Get Chrome executable path for current platform
|
|
11
14
|
*/
|
|
@@ -2032,16 +2032,20 @@ async function askChatGPTFastInternal(question, debug) {
|
|
|
2032
2032
|
summary: interceptor.getSummary(),
|
|
2033
2033
|
});
|
|
2034
2034
|
// Hybrid: prefer network text (primary), DOM as fallback
|
|
2035
|
+
// Use network if it captured anything and is at least 50% of DOM length
|
|
2036
|
+
// (avoids using truncated network text when DOM has the full answer)
|
|
2035
2037
|
let hybridAnswer = finalAnswer;
|
|
2036
2038
|
let answerSource = 'dom';
|
|
2037
|
-
|
|
2039
|
+
const netLen = networkResult.text.length;
|
|
2040
|
+
const domLen = finalAnswer.length;
|
|
2041
|
+
if (netLen > 0 && (domLen === 0 || netLen >= domLen * 0.5)) {
|
|
2038
2042
|
hybridAnswer = networkResult.text;
|
|
2039
2043
|
answerSource = 'network';
|
|
2040
2044
|
}
|
|
2041
2045
|
logInfo('chatgpt', 'Answer source selected', {
|
|
2042
2046
|
source: answerSource,
|
|
2043
|
-
networkLen:
|
|
2044
|
-
domLen
|
|
2047
|
+
networkLen: netLen,
|
|
2048
|
+
domLen,
|
|
2045
2049
|
});
|
|
2046
2050
|
return { answer: hybridAnswer, timings: fullTimings, debug: debugInfo };
|
|
2047
2051
|
}
|
|
@@ -3030,17 +3034,20 @@ async function askGeminiFastInternal(question, debug) {
|
|
|
3030
3034
|
});
|
|
3031
3035
|
// Hybrid: prefer network text (primary), DOM as fallback
|
|
3032
3036
|
// Normalize network text with same Gemini-specific cleanup as DOM text
|
|
3037
|
+
// Use network if it captured anything and is at least 50% of DOM length
|
|
3033
3038
|
const networkNormalized = normalizeGeminiResponse(networkResult.text, question);
|
|
3034
3039
|
let hybridAnswer = normalized;
|
|
3035
3040
|
let answerSource = 'dom';
|
|
3036
|
-
|
|
3041
|
+
const netLen = networkNormalized.length;
|
|
3042
|
+
const domLen = normalized.length;
|
|
3043
|
+
if (netLen > 0 && (domLen === 0 || netLen >= domLen * 0.5)) {
|
|
3037
3044
|
hybridAnswer = networkNormalized;
|
|
3038
3045
|
answerSource = 'network';
|
|
3039
3046
|
}
|
|
3040
3047
|
logInfo('gemini', 'Answer source selected', {
|
|
3041
3048
|
source: answerSource,
|
|
3042
|
-
networkLen:
|
|
3043
|
-
domLen
|
|
3049
|
+
networkLen: netLen,
|
|
3050
|
+
domLen,
|
|
3044
3051
|
});
|
|
3045
3052
|
return { answer: hybridAnswer, timings: fullTimings, debug: debugInfo };
|
|
3046
3053
|
}
|