chrome-ai-bridge 1.0.9 → 1.0.11

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.
@@ -188,64 +188,71 @@ export const askGeminiImage = defineTool({
188
188
  response.appendResponseLine('⚠️ 送信ボタンが見つかりません (Enterキーを試行)');
189
189
  }
190
190
  response.appendResponseLine('🎨 画像生成中... (1-2分かかることがあります)');
191
- // Wait for image generation using MutationObserver for instant detection
191
+ // Wait for image generation using MutationObserver + polling hybrid approach
192
+ // MutationObserver provides instant detection, polling ensures we don't miss state
192
193
  const startTime = Date.now();
193
194
  const maxWaitTime = 180000; // 3 minutes
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;
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);
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 || '';
234
210
  }
211
+ return (text.includes('ダウンロード') ||
212
+ text.includes('Download') ||
213
+ text.includes('フルサイズ') ||
214
+ ariaLabel.toLowerCase().includes('download') ||
215
+ desc.includes('フルサイズ') ||
216
+ desc.includes('ダウンロード'));
235
217
  });
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(() => {
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;
244
231
  observer.disconnect();
245
- resolve(false);
246
- }, maxWait);
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)
242
+ let imageFound = false;
243
+ while (Date.now() - startTime < maxWaitTime) {
244
+ // Check if MutationObserver detected image
245
+ const found = await page.evaluate(() => {
246
+ // @ts-expect-error - window property
247
+ return window.__geminiImageFound === true;
247
248
  });
248
- }, maxWaitTime);
249
+ if (found) {
250
+ imageFound = true;
251
+ break;
252
+ }
253
+ // Short wait before next check (500ms for responsiveness)
254
+ await new Promise(resolve => setTimeout(resolve, 500));
255
+ }
249
256
  if (!imageFound) {
250
257
  response.appendResponseLine('❌ 画像生成タイムアウト (3分)');
251
258
  return;
@@ -275,6 +282,9 @@ export const askGeminiImage = defineTool({
275
282
  downloadManager.on('started', (filename) => {
276
283
  response.appendResponseLine(`📥 ダウンロード開始: ${filename}`);
277
284
  });
285
+ // Get existing Gemini images BEFORE clicking download button
286
+ const existingFiles = await fs.promises.readdir(userDownloadsDir);
287
+ const existingGeminiImages = new Set(existingFiles.filter(f => f.startsWith('Gemini_Generated_Image_') && f.endsWith('.png')));
278
288
  // Click download button - Gemini uses "フルサイズの画像をダウンロード" button
279
289
  // Improved selector: prioritize aria-describedby for more reliable detection
280
290
  const downloadClicked = await page.evaluate(() => {
@@ -318,25 +328,41 @@ export const askGeminiImage = defineTool({
318
328
  response.appendResponseLine('ヒント: ブラウザで画像を右クリックして保存してください');
319
329
  return;
320
330
  }
321
- // Wait for download to complete using CDP events (more reliable than file polling)
322
- response.appendResponseLine('⏳ CDP経由でダウンロード完了を待機中...');
323
- let downloadedPath;
324
- try {
325
- downloadedPath = await downloadManager.waitForDownload(60000); // 60 seconds
326
- response.appendResponseLine(`✅ ダウンロード完了: ${path.basename(downloadedPath)}`);
327
- }
328
- catch (downloadError) {
329
- const errMsg = downloadError instanceof Error ? downloadError.message : String(downloadError);
330
- if (errMsg.includes('timeout')) {
331
- response.appendResponseLine('❌ ダウンロードタイムアウト (60秒)');
332
- response.appendResponseLine('💡 ヒント: ブラウザで画像を右クリックして「画像を保存」してください');
333
- }
334
- else if (errMsg.includes('canceled')) {
335
- response.appendResponseLine('❌ ダウンロードがキャンセルされました');
331
+ // Wait for download to complete using hybrid approach:
332
+ // 1. Try CDP events first (reliable for standard downloads)
333
+ // 2. Fall back to filesystem monitoring (for blob/JS downloads like Gemini)
334
+ response.appendResponseLine('⏳ ダウンロード完了を待機中...');
335
+ let downloadedPath = null;
336
+ const downloadStartTime = Date.now();
337
+ const downloadTimeout = 60000; // 60 seconds
338
+ // Try CDP-based detection with filesystem fallback
339
+ while (Date.now() - downloadStartTime < downloadTimeout) {
340
+ // Check for new Gemini image files (filesystem fallback)
341
+ const currentFiles = await fs.promises.readdir(userDownloadsDir);
342
+ const newGeminiImages = currentFiles.filter(f => f.startsWith('Gemini_Generated_Image_') &&
343
+ f.endsWith('.png') &&
344
+ !existingGeminiImages.has(f));
345
+ if (newGeminiImages.length > 0) {
346
+ // Found new image file
347
+ const newestImage = newGeminiImages.sort().pop();
348
+ downloadedPath = path.join(userDownloadsDir, newestImage);
349
+ // Wait a bit for file to be fully written
350
+ await new Promise(resolve => setTimeout(resolve, 500));
351
+ response.appendResponseLine(`✅ ダウンロード完了: ${newestImage}`);
352
+ break;
336
353
  }
337
- else {
338
- response.appendResponseLine(`❌ ダウンロードエラー: ${errMsg}`);
354
+ // Also check CDP events (for standard downloads)
355
+ const completedDownloads = Array.from(downloadManager.getPendingDownloads()).filter((d) => d.state === 'completed');
356
+ if (completedDownloads.length > 0) {
357
+ // CDP detected completion - but Gemini uses blob downloads so this rarely fires
358
+ break;
339
359
  }
360
+ // Short wait before next check
361
+ await new Promise(resolve => setTimeout(resolve, 500));
362
+ }
363
+ if (!downloadedPath) {
364
+ response.appendResponseLine('❌ ダウンロードタイムアウト (60秒)');
365
+ response.appendResponseLine('💡 ヒント: ブラウザで画像を右クリックして「画像を保存」してください');
340
366
  return;
341
367
  }
342
368
  // Ensure output directory exists
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-ai-bridge",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
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",