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.
|
|
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,69 @@ export const askGeminiImage = defineTool({
|
|
|
188
188
|
response.appendResponseLine('⚠️ 送信ボタンが見つかりません (Enterキーを試行)');
|
|
189
189
|
}
|
|
190
190
|
response.appendResponseLine('🎨 画像生成中... (1-2分かかることがあります)');
|
|
191
|
-
// Wait for image generation
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
241
|
-
|
|
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