chrome-ai-bridge 1.0.8 → 1.0.10

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.
@@ -16,8 +16,62 @@ export class DownloadManager extends EventEmitter {
16
16
  this.page = page;
17
17
  this.downloadDir = downloadDir;
18
18
  }
19
+ /**
20
+ * Handle downloadWillBegin event
21
+ */
22
+ handleDownloadWillBegin = (event) => {
23
+ const info = {
24
+ guid: event.guid,
25
+ url: event.url,
26
+ suggestedFilename: event.suggestedFilename,
27
+ state: 'inProgress',
28
+ receivedBytes: 0,
29
+ totalBytes: 0,
30
+ };
31
+ this.pendingDownloads.set(event.guid, info);
32
+ this.emit('started', event.suggestedFilename);
33
+ };
34
+ /**
35
+ * Handle downloadProgress event
36
+ */
37
+ handleDownloadProgress = (event) => {
38
+ const info = this.pendingDownloads.get(event.guid);
39
+ if (!info)
40
+ return;
41
+ info.receivedBytes = event.receivedBytes;
42
+ info.totalBytes = event.totalBytes;
43
+ info.state = event.state;
44
+ // Emit progress
45
+ if (event.totalBytes > 0) {
46
+ const percent = Math.round((event.receivedBytes / event.totalBytes) * 100);
47
+ this.emit('progress', percent, info.suggestedFilename);
48
+ }
49
+ // Handle completion
50
+ if (event.state === 'completed') {
51
+ // Construct the final path
52
+ const finalPath = `${this.downloadDir}/${info.suggestedFilename}`;
53
+ info.resolvedPath = finalPath;
54
+ this.emit('completed', finalPath);
55
+ // Resolve any waiting promises
56
+ const resolver = this.downloadPromiseResolvers.get(event.guid);
57
+ if (resolver) {
58
+ resolver.resolve(finalPath);
59
+ this.downloadPromiseResolvers.delete(event.guid);
60
+ }
61
+ }
62
+ else if (event.state === 'canceled') {
63
+ this.emit('canceled', info.suggestedFilename);
64
+ const resolver = this.downloadPromiseResolvers.get(event.guid);
65
+ if (resolver) {
66
+ resolver.reject(new Error('Download canceled'));
67
+ this.downloadPromiseResolvers.delete(event.guid);
68
+ }
69
+ }
70
+ };
19
71
  /**
20
72
  * Start monitoring downloads using CDP events
73
+ * Event handlers are registered immediately after CDP session creation
74
+ * to prevent race conditions
21
75
  */
22
76
  async startMonitoring() {
23
77
  if (this.isMonitoring) {
@@ -25,59 +79,16 @@ export class DownloadManager extends EventEmitter {
25
79
  }
26
80
  // Create CDP session
27
81
  this.cdpSession = await this.page.createCDPSession();
82
+ // Register event handlers IMMEDIATELY after CDP session creation
83
+ // This prevents race condition where events could be missed
84
+ this.cdpSession.on('Browser.downloadWillBegin', this.handleDownloadWillBegin);
85
+ this.cdpSession.on('Browser.downloadProgress', this.handleDownloadProgress);
28
86
  // Enable download events
29
87
  await this.cdpSession.send('Browser.setDownloadBehavior', {
30
88
  behavior: 'allowAndName',
31
89
  downloadPath: this.downloadDir,
32
90
  eventsEnabled: true,
33
91
  });
34
- // Listen for download events
35
- this.cdpSession.on('Browser.downloadWillBegin', (event) => {
36
- const info = {
37
- guid: event.guid,
38
- url: event.url,
39
- suggestedFilename: event.suggestedFilename,
40
- state: 'inProgress',
41
- receivedBytes: 0,
42
- totalBytes: 0,
43
- };
44
- this.pendingDownloads.set(event.guid, info);
45
- this.emit('started', event.suggestedFilename);
46
- });
47
- this.cdpSession.on('Browser.downloadProgress', (event) => {
48
- const info = this.pendingDownloads.get(event.guid);
49
- if (!info)
50
- return;
51
- info.receivedBytes = event.receivedBytes;
52
- info.totalBytes = event.totalBytes;
53
- info.state = event.state;
54
- // Emit progress
55
- if (event.totalBytes > 0) {
56
- const percent = Math.round((event.receivedBytes / event.totalBytes) * 100);
57
- this.emit('progress', percent, info.suggestedFilename);
58
- }
59
- // Handle completion
60
- if (event.state === 'completed') {
61
- // Construct the final path
62
- const finalPath = `${this.downloadDir}/${info.suggestedFilename}`;
63
- info.resolvedPath = finalPath;
64
- this.emit('completed', finalPath);
65
- // Resolve any waiting promises
66
- const resolver = this.downloadPromiseResolvers.get(event.guid);
67
- if (resolver) {
68
- resolver.resolve(finalPath);
69
- this.downloadPromiseResolvers.delete(event.guid);
70
- }
71
- }
72
- else if (event.state === 'canceled') {
73
- this.emit('canceled', info.suggestedFilename);
74
- const resolver = this.downloadPromiseResolvers.get(event.guid);
75
- if (resolver) {
76
- resolver.reject(new Error('Download canceled'));
77
- this.downloadPromiseResolvers.delete(event.guid);
78
- }
79
- }
80
- });
81
92
  this.isMonitoring = true;
82
93
  }
83
94
  /**
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.0.0",
3
- "lastUpdated": "2025-11-27",
2
+ "version": "1.1.0",
3
+ "lastUpdated": "2025-01-24",
4
4
  "description": "Selectors for Gemini web interface",
5
5
  "elements": {
6
6
  "editor": {
@@ -16,6 +16,17 @@
16
16
  "response": {
17
17
  "css": "model-response",
18
18
  "description": "Response container"
19
+ },
20
+ "downloadButton": {
21
+ "ax": {
22
+ "ariaDescribedByContains": ["フルサイズ", "ダウンロード", "Download"]
23
+ },
24
+ "text": ["フルサイズの画像をダウンロード", "Download full-size image"],
25
+ "description": "Download button for generated images"
26
+ },
27
+ "generatedImage": {
28
+ "css": "img[src*='blob:'], img[src*='generated']",
29
+ "description": "Generated image element"
19
30
  }
20
31
  },
21
32
  "placeholders": {
@@ -188,46 +188,76 @@ export const askGeminiImage = defineTool({
188
188
  response.appendResponseLine('⚠️ 送信ボタンが見つかりません (Enterキーを試行)');
189
189
  }
190
190
  response.appendResponseLine('🎨 画像生成中... (1-2分かかることがあります)');
191
- // Wait for image generation to complete
192
- // Look for generated image or download button
191
+ // Wait for image generation using MutationObserver + polling hybrid approach
192
+ // MutationObserver provides instant detection, polling ensures we don't miss state
193
193
  const startTime = Date.now();
194
194
  const maxWaitTime = 180000; // 3 minutes
195
+ // Set up MutationObserver in the page (stores result in window object)
196
+ await page.evaluate(() => {
197
+ // @ts-expect-error - window property
198
+ window.__geminiImageFound = false;
199
+ const checkCompletion = () => {
200
+ const images = document.querySelectorAll('img[src*="blob:"], img[src*="generated"]');
201
+ const buttons = Array.from(document.querySelectorAll('button, [role="menuitem"]'));
202
+ const hasDownload = buttons.some(b => {
203
+ const text = b.textContent || '';
204
+ const ariaLabel = b.getAttribute('aria-label') || '';
205
+ const describedBy = b.getAttribute('aria-describedby');
206
+ let desc = '';
207
+ if (describedBy) {
208
+ const descEl = document.getElementById(describedBy);
209
+ desc = descEl?.textContent || '';
210
+ }
211
+ return (text.includes('ダウンロード') ||
212
+ text.includes('Download') ||
213
+ text.includes('フルサイズ') ||
214
+ ariaLabel.toLowerCase().includes('download') ||
215
+ desc.includes('フルサイズ') ||
216
+ desc.includes('ダウンロード'));
217
+ });
218
+ return images.length > 0 || hasDownload;
219
+ };
220
+ // Initial check
221
+ if (checkCompletion()) {
222
+ // @ts-expect-error - window property
223
+ window.__geminiImageFound = true;
224
+ return;
225
+ }
226
+ // Set up MutationObserver
227
+ const observer = new MutationObserver(() => {
228
+ if (checkCompletion()) {
229
+ // @ts-expect-error - window property
230
+ window.__geminiImageFound = true;
231
+ observer.disconnect();
232
+ }
233
+ });
234
+ observer.observe(document.body, {
235
+ childList: true,
236
+ subtree: true,
237
+ attributes: true,
238
+ attributeFilter: ['src', 'aria-label', 'aria-describedby'],
239
+ });
240
+ });
241
+ // Poll for the result (short intervals to minimize latency)
195
242
  let imageFound = false;
196
243
  while (Date.now() - startTime < maxWaitTime) {
197
- await new Promise(resolve => setTimeout(resolve, 2000));
198
- const status = await page.evaluate(() => {
199
- // Check for generated image
200
- const images = document.querySelectorAll('img[src*="blob:"], img[src*="generated"]');
201
- // Check for download button or menu
202
- const downloadButtons = Array.from(document.querySelectorAll('button, [role="menuitem"]'));
203
- const hasDownload = downloadButtons.some(b => b.textContent?.includes('ダウンロード') ||
204
- b.textContent?.includes('Download') ||
205
- b.getAttribute('aria-label')?.includes('download') ||
206
- b.getAttribute('aria-label')?.includes('ダウンロード'));
207
- // Check if still generating
208
- const isGenerating = document.body.innerText.includes('生成中') ||
209
- document.body.innerText.includes('Generating') ||
210
- document.querySelector('[role="progressbar"]') !== null;
211
- return {
212
- imageCount: images.length,
213
- hasDownload,
214
- isGenerating,
215
- };
244
+ // Check if MutationObserver detected image
245
+ const found = await page.evaluate(() => {
246
+ // @ts-expect-error - window property
247
+ return window.__geminiImageFound === true;
216
248
  });
217
- if (status.imageCount > 0 || status.hasDownload) {
249
+ if (found) {
218
250
  imageFound = true;
219
- response.appendResponseLine(`✅ 画像生成完了 (${Math.floor((Date.now() - startTime) / 1000)}秒)`);
220
251
  break;
221
252
  }
222
- if (!status.isGenerating && Date.now() - startTime > 30000) {
223
- // Not generating and no image after 30s - might have failed
224
- response.appendResponseLine('⚠️ 生成中インジケータが消えました...');
225
- }
253
+ // Short wait before next check (500ms for responsiveness)
254
+ await new Promise(resolve => setTimeout(resolve, 500));
226
255
  }
227
256
  if (!imageFound) {
228
257
  response.appendResponseLine('❌ 画像生成タイムアウト (3分)');
229
258
  return;
230
259
  }
260
+ response.appendResponseLine(`✅ 画像生成完了 (${Math.floor((Date.now() - startTime) / 1000)}秒)`);
231
261
  // Try to download the image using CDP-based download manager
232
262
  response.appendResponseLine('📥 画像をダウンロード中...');
233
263
  // Set up download manager with CDP events
@@ -235,10 +265,18 @@ export const askGeminiImage = defineTool({
235
265
  const downloadManager = new DownloadManager(page, userDownloadsDir);
236
266
  try {
237
267
  await downloadManager.startMonitoring();
238
- // Listen for progress updates
268
+ // Track reported progress to ensure threshold-based reporting
269
+ let lastReportedThreshold = 0;
270
+ // Listen for progress updates with threshold-based reporting
271
+ // This ensures 25%, 50%, 75%, 100% are always reported even if progress jumps
239
272
  downloadManager.on('progress', (percent, filename) => {
240
- if (percent % 25 === 0) {
241
- response.appendResponseLine(`📥 ダウンロード中... ${percent}% (${filename})`);
273
+ // Calculate the next threshold to report (25, 50, 75, 100)
274
+ const thresholds = [25, 50, 75, 100];
275
+ for (const threshold of thresholds) {
276
+ if (percent >= threshold && lastReportedThreshold < threshold) {
277
+ response.appendResponseLine(`📥 ダウンロード中... ${threshold}% (${filename})`);
278
+ lastReportedThreshold = threshold;
279
+ }
242
280
  }
243
281
  });
244
282
  downloadManager.on('started', (filename) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-ai-bridge",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "MCP server bridging Chrome browser and AI assistants (ChatGPT, Gemini). Browser automation + AI consultation.",
5
5
  "type": "module",
6
6
  "bin": "./scripts/cli.mjs",