chrome-ai-bridge 1.0.8 → 1.0.9

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,69 @@ 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 for instant detection
193
192
  const startTime = Date.now();
194
193
  const maxWaitTime = 180000; // 3 minutes
195
- let imageFound = false;
196
- 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,
194
+ const imageFound = await page.evaluate((maxWait) => {
195
+ return new Promise(resolve => {
196
+ // Check if image already exists (immediate return)
197
+ const checkCompletion = () => {
198
+ // Check for generated image
199
+ const images = document.querySelectorAll('img[src*="blob:"], img[src*="generated"]');
200
+ // Check for download button with various detection methods
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
+ if (images.length > 0 || hasDownload) {
219
+ return true;
220
+ }
221
+ return false;
215
222
  };
223
+ // Initial check
224
+ if (checkCompletion()) {
225
+ resolve(true);
226
+ return;
227
+ }
228
+ // Set up MutationObserver for instant detection
229
+ const observer = new MutationObserver(() => {
230
+ if (checkCompletion()) {
231
+ observer.disconnect();
232
+ clearTimeout(timeoutId);
233
+ resolve(true);
234
+ }
235
+ });
236
+ observer.observe(document.body, {
237
+ childList: true,
238
+ subtree: true,
239
+ attributes: true,
240
+ attributeFilter: ['src', 'aria-label', 'aria-describedby'],
241
+ });
242
+ // Timeout fallback
243
+ const timeoutId = setTimeout(() => {
244
+ observer.disconnect();
245
+ resolve(false);
246
+ }, maxWait);
216
247
  });
217
- if (status.imageCount > 0 || status.hasDownload) {
218
- imageFound = true;
219
- response.appendResponseLine(`✅ 画像生成完了 (${Math.floor((Date.now() - startTime) / 1000)}秒)`);
220
- break;
221
- }
222
- if (!status.isGenerating && Date.now() - startTime > 30000) {
223
- // Not generating and no image after 30s - might have failed
224
- response.appendResponseLine('⚠️ 生成中インジケータが消えました...');
225
- }
226
- }
248
+ }, maxWaitTime);
227
249
  if (!imageFound) {
228
250
  response.appendResponseLine('❌ 画像生成タイムアウト (3分)');
229
251
  return;
230
252
  }
253
+ response.appendResponseLine(`✅ 画像生成完了 (${Math.floor((Date.now() - startTime) / 1000)}秒)`);
231
254
  // Try to download the image using CDP-based download manager
232
255
  response.appendResponseLine('📥 画像をダウンロード中...');
233
256
  // Set up download manager with CDP events
@@ -235,10 +258,18 @@ export const askGeminiImage = defineTool({
235
258
  const downloadManager = new DownloadManager(page, userDownloadsDir);
236
259
  try {
237
260
  await downloadManager.startMonitoring();
238
- // Listen for progress updates
261
+ // Track reported progress to ensure threshold-based reporting
262
+ let lastReportedThreshold = 0;
263
+ // Listen for progress updates with threshold-based reporting
264
+ // This ensures 25%, 50%, 75%, 100% are always reported even if progress jumps
239
265
  downloadManager.on('progress', (percent, filename) => {
240
- if (percent % 25 === 0) {
241
- response.appendResponseLine(`📥 ダウンロード中... ${percent}% (${filename})`);
266
+ // Calculate the next threshold to report (25, 50, 75, 100)
267
+ const thresholds = [25, 50, 75, 100];
268
+ for (const threshold of thresholds) {
269
+ if (percent >= threshold && lastReportedThreshold < threshold) {
270
+ response.appendResponseLine(`📥 ダウンロード中... ${threshold}% (${filename})`);
271
+ lastReportedThreshold = threshold;
272
+ }
242
273
  }
243
274
  });
244
275
  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.9",
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",