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.
|
|
3
|
-
"lastUpdated": "2025-
|
|
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
|
|
192
|
-
//
|
|
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
|
-
|
|
198
|
-
const
|
|
199
|
-
//
|
|
200
|
-
|
|
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 (
|
|
249
|
+
if (found) {
|
|
218
250
|
imageFound = true;
|
|
219
|
-
response.appendResponseLine(`✅ 画像生成完了 (${Math.floor((Date.now() - startTime) / 1000)}秒)`);
|
|
220
251
|
break;
|
|
221
252
|
}
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
241
|
-
|
|
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