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
|
@@ -146,8 +146,15 @@ export class NetworkInterceptor {
|
|
|
146
146
|
const isSSE = contentType.includes('text/event-stream');
|
|
147
147
|
if (isResponseUrl(url) || isSSE) {
|
|
148
148
|
this.pendingBodies.add(requestId);
|
|
149
|
-
this.fetchResponseBody(requestId, url).catch(() => {
|
|
150
|
-
|
|
149
|
+
this.fetchResponseBody(requestId, url).catch((err) => {
|
|
150
|
+
console.error(`[NetworkInterceptor] fetchResponseBody error for ${url.slice(0, 80)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else if (!url) {
|
|
154
|
+
// Speculative capture: requestWillBeSent was missed (tab reuse scenario)
|
|
155
|
+
// Try fetching body and check if it looks like ChatGPT SSE or Gemini response
|
|
156
|
+
this.speculativeFetchBody(requestId).catch(() => {
|
|
157
|
+
// Best-effort; silent failure expected
|
|
151
158
|
});
|
|
152
159
|
}
|
|
153
160
|
});
|
|
@@ -163,40 +170,87 @@ export class NetworkInterceptor {
|
|
|
163
170
|
this.client.on(event, wrappedHandler);
|
|
164
171
|
}
|
|
165
172
|
async fetchResponseBody(requestId, url) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
+
const maxRetries = 2;
|
|
174
|
+
const retryDelayMs = 500;
|
|
175
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
176
|
+
try {
|
|
177
|
+
const result = await this.client.send('Network.getResponseBody', { requestId });
|
|
178
|
+
if (result?.body) {
|
|
179
|
+
let data;
|
|
180
|
+
if (result.base64Encoded) {
|
|
181
|
+
try {
|
|
182
|
+
data = Buffer.from(result.body, 'base64').toString('utf-8');
|
|
183
|
+
}
|
|
184
|
+
catch (decodeErr) {
|
|
185
|
+
console.error(`[NetworkInterceptor] Base64 decode failed for ${url.slice(0, 80)}: ${decodeErr instanceof Error ? decodeErr.message : String(decodeErr)}`);
|
|
186
|
+
this.pendingBodies.delete(requestId);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
173
189
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return;
|
|
190
|
+
else {
|
|
191
|
+
data = result.body;
|
|
177
192
|
}
|
|
193
|
+
this.frames.push({
|
|
194
|
+
timestamp: Date.now() / 1000,
|
|
195
|
+
type: 'fetch-body',
|
|
196
|
+
requestId,
|
|
197
|
+
url,
|
|
198
|
+
data,
|
|
199
|
+
});
|
|
200
|
+
console.error(`[NetworkInterceptor] Body captured: ${url.slice(0, 80)} (${data.length} bytes)`);
|
|
201
|
+
}
|
|
202
|
+
this.pendingBodies.delete(requestId);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
207
|
+
const isRetryable = msg.includes('No resource') || msg.includes('No data found');
|
|
208
|
+
if (isRetryable && attempt < maxRetries) {
|
|
209
|
+
console.error(`[NetworkInterceptor] getResponseBody retry ${attempt + 1}/${maxRetries} for ${url.slice(0, 80)}`);
|
|
210
|
+
await new Promise(r => setTimeout(r, retryDelayMs));
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (!isRetryable) {
|
|
214
|
+
console.error(`[NetworkInterceptor] getResponseBody failed for ${url.slice(0, 80)}: ${msg}`);
|
|
178
215
|
}
|
|
179
216
|
else {
|
|
180
|
-
|
|
217
|
+
console.error(`[NetworkInterceptor] getResponseBody failed after ${maxRetries} retries for ${url.slice(0, 80)}: ${msg}`);
|
|
181
218
|
}
|
|
219
|
+
this.pendingBodies.delete(requestId);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
this.pendingBodies.delete(requestId);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Speculative body fetch for requests where requestWillBeSent was missed
|
|
227
|
+
* (e.g., tab reuse). Checks if the body looks like a ChatGPT or Gemini response.
|
|
228
|
+
*/
|
|
229
|
+
async speculativeFetchBody(requestId) {
|
|
230
|
+
try {
|
|
231
|
+
const result = await this.client.send('Network.getResponseBody', { requestId });
|
|
232
|
+
if (!result?.body)
|
|
233
|
+
return;
|
|
234
|
+
const data = result.base64Encoded
|
|
235
|
+
? Buffer.from(result.body, 'base64').toString('utf-8')
|
|
236
|
+
: result.body;
|
|
237
|
+
// Check if body looks like ChatGPT SSE (contains "data: " lines and [DONE])
|
|
238
|
+
const isChatGPTSSE = data.includes('data: ') && data.includes('[DONE]');
|
|
239
|
+
// Check if body looks like Gemini response (starts with )]}')
|
|
240
|
+
const isGemini = data.startsWith(")]}'");
|
|
241
|
+
if (isChatGPTSSE || isGemini) {
|
|
182
242
|
this.frames.push({
|
|
183
243
|
timestamp: Date.now() / 1000,
|
|
184
244
|
type: 'fetch-body',
|
|
185
245
|
requestId,
|
|
186
|
-
url,
|
|
246
|
+
url: '<speculative>',
|
|
187
247
|
data,
|
|
188
248
|
});
|
|
249
|
+
console.error(`[NetworkInterceptor] Speculative capture hit: ${isChatGPTSSE ? 'ChatGPT SSE' : 'Gemini'} (${data.length} bytes)`);
|
|
189
250
|
}
|
|
190
251
|
}
|
|
191
|
-
catch
|
|
192
|
-
//
|
|
193
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
194
|
-
if (!msg.includes('No resource') && !msg.includes('No data found')) {
|
|
195
|
-
console.error(`[NetworkInterceptor] getResponseBody failed for ${url.slice(0, 80)}: ${msg}`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
finally {
|
|
199
|
-
this.pendingBodies.delete(requestId);
|
|
252
|
+
catch {
|
|
253
|
+
// Silent: speculative capture is best-effort
|
|
200
254
|
}
|
|
201
255
|
}
|
|
202
256
|
stopCapture() {
|
|
@@ -212,14 +266,30 @@ export class NetworkInterceptor {
|
|
|
212
266
|
}
|
|
213
267
|
/**
|
|
214
268
|
* Wait for all pending response body fetches, then stop capture.
|
|
269
|
+
* IMPORTANT: capturing remains true during the wait so that late-arriving
|
|
270
|
+
* loadingFinished events are still processed (this was the root cause of
|
|
271
|
+
* textLength=0 — setting capturing=false first dropped those events).
|
|
215
272
|
*/
|
|
216
|
-
async stopCaptureAndWait(timeoutMs =
|
|
217
|
-
|
|
218
|
-
// Wait for pending body fetches
|
|
273
|
+
async stopCaptureAndWait(timeoutMs = 15000) {
|
|
274
|
+
// Phase 1: Wait for pending body fetches (capturing stays true)
|
|
219
275
|
const deadline = Date.now() + timeoutMs;
|
|
220
276
|
while (this.pendingBodies.size > 0 && Date.now() < deadline) {
|
|
221
277
|
await new Promise(r => setTimeout(r, 100));
|
|
222
278
|
}
|
|
279
|
+
// Phase 2: Grace period — if no frames captured yet, wait for late loadingFinished
|
|
280
|
+
if (this.frames.length === 0) {
|
|
281
|
+
const graceDeadline = Math.min(Date.now() + 3000, deadline);
|
|
282
|
+
console.error('[NetworkInterceptor] No frames yet, waiting grace period for late events...');
|
|
283
|
+
while (this.frames.length === 0 && Date.now() < graceDeadline) {
|
|
284
|
+
await new Promise(r => setTimeout(r, 100));
|
|
285
|
+
// Also wait for any new pending bodies that appeared during grace
|
|
286
|
+
while (this.pendingBodies.size > 0 && Date.now() < graceDeadline) {
|
|
287
|
+
await new Promise(r => setTimeout(r, 100));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Phase 3: Now stop capturing
|
|
292
|
+
this.capturing = false;
|
|
223
293
|
// Warn if pending bodies remain after timeout
|
|
224
294
|
if (this.pendingBodies.size > 0) {
|
|
225
295
|
console.warn(`[NetworkInterceptor] Timeout: ${this.pendingBodies.size} pending bodies abandoned (requestIds: ${[...this.pendingBodies].slice(0, 3).join(', ')}${this.pendingBodies.size > 3 ? '...' : ''})`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-ai-bridge",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "MCP server bridging Chrome extension and AI assistants (ChatGPT, Gemini). Extension-only mode - no Puppeteer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./scripts/cli.mjs",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"files": [
|
|
48
48
|
"build/src",
|
|
49
|
+
"build/extension",
|
|
49
50
|
"scripts/cli.mjs",
|
|
50
51
|
"scripts/browser-globals-mock.mjs",
|
|
51
52
|
"README.md",
|