surf-cli 2.7.2 → 2.9.0
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.
- package/README.md +208 -13
- package/dist/content/index.js +116 -0
- package/dist/content/index.js.map +1 -0
- package/dist/manifest.json +2 -11
- package/dist/options/options.js +3 -3
- package/dist/options/options.js.map +1 -1
- package/dist/service-worker/index.js +261 -61
- package/dist/service-worker/index.js.map +1 -1
- package/native/abort.cjs +65 -0
- package/native/ai-queue.cjs +64 -0
- package/native/aistudio-build.cjs +21 -13
- package/native/aistudio-client.cjs +40 -20
- package/native/browser-lock.cjs +169 -0
- package/native/chatgpt-client.cjs +63 -30
- package/native/cli.cjs +947 -460
- package/native/client-transport.cjs +168 -0
- package/native/config.cjs +2 -2
- package/native/do-executor.cjs +25 -51
- package/native/do-parser.cjs +12 -0
- package/native/doctor.cjs +633 -0
- package/native/endpoint.cjs +174 -0
- package/native/file-transfer.cjs +734 -0
- package/native/gemini-client.cjs +244 -88
- package/native/grok-client.cjs +321 -212
- package/native/host-helpers.cjs +88 -16
- package/native/host-sessions.cjs +283 -0
- package/native/host.cjs +811 -616
- package/native/listener.cjs +20 -0
- package/native/mcp-server.cjs +60 -62
- package/native/network-export.cjs +113 -0
- package/native/perplexity-client.cjs +46 -17
- package/native/remote-auth.cjs +279 -0
- package/native/remote-transport.cjs +337 -0
- package/native/request-pending.cjs +148 -0
- package/native/socket-path.cjs +46 -0
- package/package.json +11 -9
- package/scripts/install-native-host.cjs +184 -51
- package/scripts/uninstall-native-host.cjs +93 -15
- package/skills/README.md +11 -5
- package/skills/deep-x-research/SKILL.md +106 -0
- package/skills/surf/SKILL.md +77 -22
- package/dist/content/accessibility-tree.js +0 -11
- package/dist/content/accessibility-tree.js.map +0 -1
- package/dist/content/visual-indicator.js +0 -111
- package/dist/content/visual-indicator.js.map +0 -1
package/native/gemini-client.cjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const https = require("https");
|
|
9
|
+
const { abortError, abortableDelay, raceAbort, throwIfAborted } = require("./abort.cjs");
|
|
9
10
|
const fs = require("fs");
|
|
10
11
|
const path = require("path");
|
|
11
12
|
|
|
@@ -22,9 +23,9 @@ const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
|
|
|
22
23
|
|
|
23
24
|
const MODEL_HEADER_NAME = "x-goog-ext-525001261-jspb";
|
|
24
25
|
const MODEL_HEADERS = {
|
|
25
|
-
"gemini-3-pro": '[1,null,null,null,"
|
|
26
|
-
"gemini-
|
|
27
|
-
"gemini-
|
|
26
|
+
"gemini-3.1-pro": '[1,null,null,null,"e6fa609c3fa255c0",null,null,0,[4]]',
|
|
27
|
+
"gemini-3.5-flash": '[1,null,null,null,"56fdd199312815e2",null,null,0,[4]]',
|
|
28
|
+
"gemini-3.1-flash-lite": '[1,null,null,null,"8c46e95b1a07cecc",null,null,0,[4]]',
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
const REQUIRED_COOKIES = ["__Secure-1PSID", "__Secure-1PSIDTS"];
|
|
@@ -96,7 +97,8 @@ function hasRequiredCookies(cookieMap) {
|
|
|
96
97
|
// ============================================================================
|
|
97
98
|
|
|
98
99
|
function httpsGet(url, headers, opts = {}) {
|
|
99
|
-
const { binary = false, timeoutMs = 30000, log = null, label = "httpsGet" } = opts;
|
|
100
|
+
const { binary = false, timeoutMs = 30000, log = null, label = "httpsGet", signal } = opts;
|
|
101
|
+
throwIfAborted(signal);
|
|
100
102
|
return new Promise((resolve, reject) => {
|
|
101
103
|
const urlObj = new URL(url);
|
|
102
104
|
const options = {
|
|
@@ -131,13 +133,17 @@ function httpsGet(url, headers, opts = {}) {
|
|
|
131
133
|
});
|
|
132
134
|
});
|
|
133
135
|
|
|
136
|
+
const onAbort = () => req.destroy(abortError(signal, "Request cancelled"));
|
|
137
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
134
138
|
req.on("timeout", () => {
|
|
135
139
|
req.destroy(new Error(`${label}: request timeout after ${timeoutMs}ms`));
|
|
136
140
|
});
|
|
137
141
|
req.on("error", (err) => {
|
|
142
|
+
signal?.removeEventListener("abort", onAbort);
|
|
138
143
|
if (log) log(`${label}: request error ${err.message}`);
|
|
139
144
|
reject(err);
|
|
140
145
|
});
|
|
146
|
+
req.on("close", () => signal?.removeEventListener("abort", onAbort));
|
|
141
147
|
req.end();
|
|
142
148
|
});
|
|
143
149
|
}
|
|
@@ -151,7 +157,8 @@ function httpsPut(url, headers, body, opts = {}) {
|
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
function httpsSend(method, url, headers, body, opts = {}) {
|
|
154
|
-
const { timeoutMs = 30000, log = null, label = "httpsSend" } = opts;
|
|
160
|
+
const { timeoutMs = 30000, log = null, label = "httpsSend", signal } = opts;
|
|
161
|
+
throwIfAborted(signal);
|
|
155
162
|
return new Promise((resolve, reject) => {
|
|
156
163
|
const urlObj = new URL(url);
|
|
157
164
|
const bodyBuffer = body == null
|
|
@@ -185,13 +192,17 @@ function httpsSend(method, url, headers, body, opts = {}) {
|
|
|
185
192
|
});
|
|
186
193
|
});
|
|
187
194
|
|
|
195
|
+
const onAbort = () => req.destroy(abortError(signal, "Request cancelled"));
|
|
196
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
188
197
|
req.on("timeout", () => {
|
|
189
198
|
req.destroy(new Error(`${label}: request timeout after ${timeoutMs}ms`));
|
|
190
199
|
});
|
|
191
200
|
req.on("error", (err) => {
|
|
201
|
+
signal?.removeEventListener("abort", onAbort);
|
|
192
202
|
if (log) log(`${label}: request error ${err.message}`);
|
|
193
203
|
reject(err);
|
|
194
204
|
});
|
|
205
|
+
req.on("close", () => signal?.removeEventListener("abort", onAbort));
|
|
195
206
|
if (bodyBuffer) req.write(bodyBuffer);
|
|
196
207
|
req.end();
|
|
197
208
|
});
|
|
@@ -292,14 +303,49 @@ function ensureFullSizeImageUrl(url) {
|
|
|
292
303
|
return `${url}=s2048`;
|
|
293
304
|
}
|
|
294
305
|
|
|
306
|
+
function getFirstStringAtPaths(value, paths) {
|
|
307
|
+
for (const pathParts of paths) {
|
|
308
|
+
const found = getNestedValue(value, pathParts, "");
|
|
309
|
+
if (typeof found === "string" && found.trim()) return found;
|
|
310
|
+
}
|
|
311
|
+
return "";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const GEMINI_TEXT_PATHS = [
|
|
315
|
+
[1, 0],
|
|
316
|
+
[1, 0, 0],
|
|
317
|
+
[1, 0, 1],
|
|
318
|
+
[1, 1, 0],
|
|
319
|
+
[2, 0],
|
|
320
|
+
[22, 0],
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
const GEMINI_CARD_CONTENT_RE = /^http:\/\/googleusercontent\.com\/card_content\/\d+/;
|
|
324
|
+
const GEMINI_CARD_ALT_PATHS = [[22, 0], [1, 1, 0], [2, 0]];
|
|
325
|
+
|
|
326
|
+
function resolveCandidateText(candidate) {
|
|
327
|
+
const textRaw = getFirstStringAtPaths(candidate, GEMINI_TEXT_PATHS);
|
|
328
|
+
if (GEMINI_CARD_CONTENT_RE.test(textRaw)) {
|
|
329
|
+
return getFirstStringAtPaths(candidate, GEMINI_CARD_ALT_PATHS) || textRaw;
|
|
330
|
+
}
|
|
331
|
+
return textRaw;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// An unresolved card_content placeholder URL is not answer text; score it zero.
|
|
335
|
+
function candidateTextScore(candidate) {
|
|
336
|
+
const resolved = resolveCandidateText(candidate);
|
|
337
|
+
return GEMINI_CARD_CONTENT_RE.test(resolved) ? 0 : resolved.length;
|
|
338
|
+
}
|
|
339
|
+
|
|
295
340
|
function parseGeminiStreamGenerateResponse(rawText) {
|
|
296
341
|
const responseJson = JSON.parse(trimGeminiJsonEnvelope(rawText));
|
|
297
342
|
const errorCode = extractErrorCode(responseJson);
|
|
298
343
|
|
|
299
344
|
const parts = Array.isArray(responseJson) ? responseJson : [];
|
|
300
|
-
let bodyIndex = 0;
|
|
301
345
|
let body = null;
|
|
302
|
-
|
|
346
|
+
let bestTextLength = -1;
|
|
347
|
+
|
|
348
|
+
// Stream chunks are cumulative; the longest text is the most complete answer.
|
|
303
349
|
for (let i = 0; i < parts.length; i++) {
|
|
304
350
|
const partBody = getNestedValue(parts[i], [2], null);
|
|
305
351
|
if (!partBody) continue;
|
|
@@ -307,9 +353,14 @@ function parseGeminiStreamGenerateResponse(rawText) {
|
|
|
307
353
|
const parsed = JSON.parse(partBody);
|
|
308
354
|
const candidateList = getNestedValue(parsed, [4], []);
|
|
309
355
|
if (Array.isArray(candidateList) && candidateList.length > 0) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
356
|
+
if (!body) {
|
|
357
|
+
body = parsed;
|
|
358
|
+
}
|
|
359
|
+
const score = candidateTextScore(candidateList[0]);
|
|
360
|
+
if (score > bestTextLength) {
|
|
361
|
+
bestTextLength = score;
|
|
362
|
+
body = parsed;
|
|
363
|
+
}
|
|
313
364
|
}
|
|
314
365
|
} catch {
|
|
315
366
|
// ignore
|
|
@@ -318,11 +369,7 @@ function parseGeminiStreamGenerateResponse(rawText) {
|
|
|
318
369
|
|
|
319
370
|
const candidateList = getNestedValue(body, [4], []);
|
|
320
371
|
const firstCandidate = candidateList[0];
|
|
321
|
-
const
|
|
322
|
-
const cardContent = /^http:\/\/googleusercontent\.com\/card_content\/\d+/.test(textRaw);
|
|
323
|
-
const text = cardContent
|
|
324
|
-
? (getNestedValue(firstCandidate, [22, 0], null) ?? textRaw)
|
|
325
|
-
: textRaw;
|
|
372
|
+
const text = resolveCandidateText(firstCandidate);
|
|
326
373
|
const thoughts = getNestedValue(firstCandidate, [37, 0, 0], null);
|
|
327
374
|
const metadata = getNestedValue(body, [1], []);
|
|
328
375
|
|
|
@@ -341,27 +388,23 @@ function parseGeminiStreamGenerateResponse(rawText) {
|
|
|
341
388
|
});
|
|
342
389
|
}
|
|
343
390
|
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (candidateImages != null) {
|
|
355
|
-
imgBody = parsed;
|
|
356
|
-
break;
|
|
357
|
-
}
|
|
358
|
-
} catch {
|
|
359
|
-
// ignore
|
|
391
|
+
// Keep the last chunk with a non-empty image list; a trailing empty must not erase it.
|
|
392
|
+
let imgBody = null;
|
|
393
|
+
for (let i = 0; i < parts.length; i++) {
|
|
394
|
+
const partBody = getNestedValue(parts[i], [2], null);
|
|
395
|
+
if (!partBody) continue;
|
|
396
|
+
try {
|
|
397
|
+
const parsed = JSON.parse(partBody);
|
|
398
|
+
const imgs = getNestedValue(parsed, [4, 0, 12, 7, 0], null);
|
|
399
|
+
if (Array.isArray(imgs) && imgs.length > 0) {
|
|
400
|
+
imgBody = parsed;
|
|
360
401
|
}
|
|
402
|
+
} catch {
|
|
403
|
+
// ignore
|
|
361
404
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const generated = getNestedValue(
|
|
405
|
+
}
|
|
406
|
+
if (imgBody) {
|
|
407
|
+
const generated = getNestedValue(imgBody, [4, 0, 12, 7, 0], []);
|
|
365
408
|
for (const genImage of generated) {
|
|
366
409
|
const url = getNestedValue(genImage, [0, 3, 3], null);
|
|
367
410
|
if (!url) continue;
|
|
@@ -517,16 +560,17 @@ function buildGeminiFReqPayload(prompt, uploaded, chatMetadata) {
|
|
|
517
560
|
}
|
|
518
561
|
|
|
519
562
|
async function runGeminiWebOnce(input) {
|
|
520
|
-
const { prompt, files, model, cookieMap, chatMetadata, timeoutMs = 30000, log = null } = input;
|
|
563
|
+
const { prompt, files, model, cookieMap, chatMetadata, timeoutMs = 30000, log = null, signal } = input;
|
|
564
|
+
throwIfAborted(signal);
|
|
521
565
|
const cookieHeader = buildCookieHeader(cookieMap);
|
|
522
566
|
|
|
523
567
|
// 1. Get access token
|
|
524
|
-
const at = await fetchGeminiAccessToken(cookieMap, { timeoutMs, log, label: "geminiAccessToken" });
|
|
568
|
+
const at = await fetchGeminiAccessToken(cookieMap, { timeoutMs, log, label: "geminiAccessToken", signal });
|
|
525
569
|
|
|
526
570
|
// 2. Upload files
|
|
527
571
|
const uploaded = [];
|
|
528
572
|
for (const file of files ?? []) {
|
|
529
|
-
uploaded.push(await uploadGeminiFile(file, cookieMap, { timeoutMs, log, label: "geminiUpload" }));
|
|
573
|
+
uploaded.push(await uploadGeminiFile(file, cookieMap, { timeoutMs, log, label: "geminiUpload", signal }));
|
|
530
574
|
}
|
|
531
575
|
|
|
532
576
|
// 3. Build request
|
|
@@ -543,8 +587,8 @@ async function runGeminiWebOnce(input) {
|
|
|
543
587
|
"referer": "https://gemini.google.com/",
|
|
544
588
|
"x-same-domain": "1",
|
|
545
589
|
"cookie": cookieHeader,
|
|
546
|
-
[MODEL_HEADER_NAME]: MODEL_HEADERS[model] || MODEL_HEADERS["gemini-3-pro"],
|
|
547
|
-
}, params.toString(), { timeoutMs, log, label: "geminiStreamGenerate" });
|
|
590
|
+
[MODEL_HEADER_NAME]: MODEL_HEADERS[model] || MODEL_HEADERS["gemini-3.1-pro"],
|
|
591
|
+
}, params.toString(), { timeoutMs, log, label: "geminiStreamGenerate", signal });
|
|
548
592
|
|
|
549
593
|
const rawResponseText = res.text;
|
|
550
594
|
|
|
@@ -591,12 +635,13 @@ async function runGeminiWebOnce(input) {
|
|
|
591
635
|
}
|
|
592
636
|
|
|
593
637
|
async function runGeminiWebWithFallback(input) {
|
|
638
|
+
throwIfAborted(input.signal);
|
|
594
639
|
const attempt = await runGeminiWebOnce(input);
|
|
595
640
|
|
|
596
641
|
// Auto-fallback to flash if model unavailable
|
|
597
|
-
if (isModelUnavailable(attempt.errorCode) && input.model !== "gemini-
|
|
598
|
-
const fallback = await runGeminiWebOnce({ ...input, model: "gemini-
|
|
599
|
-
return { ...fallback, effectiveModel: "gemini-
|
|
642
|
+
if (isModelUnavailable(attempt.errorCode) && input.model !== "gemini-3.5-flash") {
|
|
643
|
+
const fallback = await runGeminiWebOnce({ ...input, model: "gemini-3.5-flash" });
|
|
644
|
+
return { ...fallback, effectiveModel: "gemini-3.5-flash" };
|
|
600
645
|
}
|
|
601
646
|
|
|
602
647
|
return { ...attempt, effectiveModel: input.model };
|
|
@@ -607,7 +652,14 @@ async function runGeminiWebWithFallback(input) {
|
|
|
607
652
|
// ============================================================================
|
|
608
653
|
|
|
609
654
|
async function runGeminiWebViaPage(input) {
|
|
610
|
-
const { prompt, files, model, timeoutMs = 120000, log = null, createTab, closeTab, jsEval, fetchUrl, uploadFile } = input;
|
|
655
|
+
const { prompt, files, model, timeoutMs = 120000, log = null, createTab, closeTab, jsEval, fetchUrl, uploadFile, signal } = input;
|
|
656
|
+
throwIfAborted(signal);
|
|
657
|
+
const guardedUploadFile = uploadFile
|
|
658
|
+
? (...args) => raceAbort(() => uploadFile(...args), signal)
|
|
659
|
+
: uploadFile;
|
|
660
|
+
const guardedFetchUrl = fetchUrl
|
|
661
|
+
? (...args) => raceAbort(() => fetchUrl(...args), signal)
|
|
662
|
+
: fetchUrl;
|
|
611
663
|
|
|
612
664
|
if (!createTab || !closeTab || !jsEval) {
|
|
613
665
|
throw new Error("In-page execution requires createTab, closeTab, and jsEval callbacks");
|
|
@@ -616,19 +668,19 @@ async function runGeminiWebViaPage(input) {
|
|
|
616
668
|
let tabId = null;
|
|
617
669
|
try {
|
|
618
670
|
if (log) log("Creating Gemini tab...");
|
|
619
|
-
const tabResult = await createTab
|
|
671
|
+
const tabResult = await raceAbort(createTab, signal);
|
|
620
672
|
tabId = tabResult?.tabId;
|
|
621
673
|
if (!tabId) throw new Error("Failed to create Gemini tab");
|
|
622
674
|
if (log) log(`Gemini tab created: ${tabId}`);
|
|
623
|
-
await
|
|
675
|
+
await abortableDelay(12000, signal);
|
|
624
676
|
|
|
625
677
|
if (files?.length && uploadFile) {
|
|
626
678
|
const absFiles = files.map(f => path.resolve(process.cwd(), f));
|
|
627
679
|
if (log) log(`Uploading ${absFiles.length} file(s) via file chooser...`);
|
|
628
|
-
const result = await
|
|
680
|
+
const result = await guardedUploadFile(tabId, absFiles);
|
|
629
681
|
if (result?.error) throw new Error(`File upload failed: ${result.error}`);
|
|
630
682
|
if (log) log("File uploaded, waiting for processing...");
|
|
631
|
-
await
|
|
683
|
+
await abortableDelay(3000, signal);
|
|
632
684
|
}
|
|
633
685
|
|
|
634
686
|
const checkJsResult = (result, context) => {
|
|
@@ -638,57 +690,103 @@ async function runGeminiWebViaPage(input) {
|
|
|
638
690
|
};
|
|
639
691
|
|
|
640
692
|
// Type prompt
|
|
641
|
-
const
|
|
693
|
+
const promptJson = JSON.stringify(prompt);
|
|
642
694
|
if (log) log("Typing prompt...");
|
|
643
|
-
const typeResult = await jsEval(tabId, `
|
|
695
|
+
const typeResult = await jsEval(tabId, `(() => {
|
|
644
696
|
const editor = document.querySelector('.ql-editor[contenteditable=true]');
|
|
645
697
|
if (!editor) return JSON.stringify({ error: "No editor found on page" });
|
|
646
698
|
editor.focus();
|
|
647
699
|
document.execCommand('selectAll', false, null);
|
|
648
|
-
document.execCommand('insertText', false,
|
|
700
|
+
document.execCommand('insertText', false, ${promptJson});
|
|
649
701
|
return JSON.stringify({ ok: true, len: editor.textContent.length });
|
|
650
|
-
`);
|
|
702
|
+
})()`);
|
|
651
703
|
const typed = JSON.parse(JSON.parse(checkJsResult(typeResult, "Type prompt")));
|
|
652
704
|
if (typed.error) throw new Error(typed.error);
|
|
653
705
|
|
|
654
|
-
const beforeResult = await jsEval(tabId, `
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
706
|
+
const beforeResult = await jsEval(tabId, `(() => {
|
|
707
|
+
const imageKey = (img) => {
|
|
708
|
+
const url = img.currentSrc || img.src || "";
|
|
709
|
+
return url + "|" + img.naturalWidth + "x" + img.naturalHeight;
|
|
710
|
+
};
|
|
711
|
+
const baselineKeys = Array.from(document.images)
|
|
712
|
+
.filter((img) => {
|
|
713
|
+
const url = img.currentSrc || img.src || "";
|
|
714
|
+
return img.naturalWidth >= 512
|
|
715
|
+
&& img.naturalHeight >= 512
|
|
716
|
+
&& (url.includes("gg-dl") || url.startsWith("blob:"));
|
|
717
|
+
})
|
|
718
|
+
.map(imageKey);
|
|
719
|
+
return JSON.stringify(baselineKeys);
|
|
720
|
+
})()`);
|
|
721
|
+
const baselineImageKeys = JSON.parse(JSON.parse(checkJsResult(beforeResult, "Count images")) || "[]");
|
|
658
722
|
|
|
659
723
|
if (log) log("Submitting...");
|
|
660
|
-
const sendResult = await jsEval(tabId, `
|
|
724
|
+
const sendResult = await jsEval(tabId, `(() => {
|
|
661
725
|
const btn = document.querySelector('button[aria-label="Send message"]');
|
|
662
726
|
if (!btn) return 'no-btn';
|
|
663
727
|
btn.click();
|
|
664
728
|
return 'sent';
|
|
665
|
-
`);
|
|
729
|
+
})()`);
|
|
666
730
|
const sendVal = JSON.parse(checkJsResult(sendResult, "Click send"));
|
|
667
731
|
if (sendVal === "no-btn") throw new Error("Send button not found on Gemini page");
|
|
668
732
|
|
|
669
733
|
// Poll for response
|
|
670
734
|
if (log) log("Waiting for response...");
|
|
671
735
|
const deadline = Date.now() + timeoutMs;
|
|
672
|
-
let
|
|
736
|
+
let imageEntries = [];
|
|
673
737
|
let responseText = "";
|
|
674
738
|
|
|
675
739
|
while (Date.now() < deadline) {
|
|
676
|
-
await
|
|
677
|
-
const pollResult = await jsEval(tabId, `
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
.
|
|
740
|
+
await abortableDelay(2000, signal);
|
|
741
|
+
const pollResult = await jsEval(tabId, `(async () => {
|
|
742
|
+
const baselineKeys = new Set(${JSON.stringify(baselineImageKeys)});
|
|
743
|
+
const imageKey = (img) => {
|
|
744
|
+
const url = img.currentSrc || img.src || "";
|
|
745
|
+
return url + "|" + img.naturalWidth + "x" + img.naturalHeight;
|
|
746
|
+
};
|
|
747
|
+
const generatedImgs = Array.from(document.images)
|
|
748
|
+
.filter((img) => {
|
|
749
|
+
const url = img.currentSrc || img.src || "";
|
|
750
|
+
return img.naturalWidth >= 512
|
|
751
|
+
&& img.naturalHeight >= 512
|
|
752
|
+
&& (url.includes("gg-dl") || url.startsWith("blob:"));
|
|
753
|
+
})
|
|
754
|
+
.filter((img) => !baselineKeys.has(imageKey(img)));
|
|
755
|
+
window.__surfGeminiBlobImages = window.__surfGeminiBlobImages || [];
|
|
756
|
+
window.__surfGeminiBlobImageIndexes = window.__surfGeminiBlobImageIndexes || Object.create(null);
|
|
757
|
+
const images = await Promise.all(generatedImgs.map(async (img) => {
|
|
758
|
+
const url = img.currentSrc || img.src || "";
|
|
759
|
+
if (!url.startsWith("blob:")) return { url };
|
|
760
|
+
const key = imageKey(img);
|
|
761
|
+
if (Number.isInteger(window.__surfGeminiBlobImageIndexes[key])) {
|
|
762
|
+
return { url, blobIndex: window.__surfGeminiBlobImageIndexes[key], type: "image/png" };
|
|
763
|
+
}
|
|
764
|
+
const canvas = document.createElement("canvas");
|
|
765
|
+
canvas.width = img.naturalWidth;
|
|
766
|
+
canvas.height = img.naturalHeight;
|
|
767
|
+
const ctx = canvas.getContext("2d");
|
|
768
|
+
if (!ctx) throw new Error("Canvas context unavailable");
|
|
769
|
+
ctx.drawImage(img, 0, 0);
|
|
770
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
771
|
+
const blobIndex = window.__surfGeminiBlobImages.push({
|
|
772
|
+
url,
|
|
773
|
+
b64: dataUrl.split(",")[1],
|
|
774
|
+
type: "image/png",
|
|
775
|
+
}) - 1;
|
|
776
|
+
window.__surfGeminiBlobImageIndexes[key] = blobIndex;
|
|
777
|
+
return { url, blobIndex, type: "image/png" };
|
|
778
|
+
}));
|
|
681
779
|
const loading = !!document.querySelector('mat-progress-bar, .loading-indicator, message-loading');
|
|
682
780
|
const turns = document.querySelectorAll('message-content');
|
|
683
781
|
const lastTurn = turns.length ? turns[turns.length - 1] : null;
|
|
684
782
|
const text = lastTurn ? lastTurn.textContent?.trim() : "";
|
|
685
|
-
return JSON.stringify({
|
|
686
|
-
`);
|
|
783
|
+
return JSON.stringify({ images, loading, text, turns: turns.length });
|
|
784
|
+
})()`);
|
|
687
785
|
const poll = JSON.parse(JSON.parse(checkJsResult(pollResult, "Poll response")));
|
|
688
|
-
const newImgs = poll.
|
|
786
|
+
const newImgs = poll.images || [];
|
|
689
787
|
|
|
690
788
|
if (newImgs.length > 0) {
|
|
691
|
-
|
|
789
|
+
imageEntries = newImgs;
|
|
692
790
|
responseText = poll.text || "";
|
|
693
791
|
if (log) log(`Found ${newImgs.length} generated image(s)`);
|
|
694
792
|
break;
|
|
@@ -699,23 +797,47 @@ async function runGeminiWebViaPage(input) {
|
|
|
699
797
|
}
|
|
700
798
|
}
|
|
701
799
|
|
|
702
|
-
if (!
|
|
800
|
+
if (!imageEntries.length && !responseText) {
|
|
703
801
|
throw new Error("Gemini response timed out");
|
|
704
802
|
}
|
|
705
803
|
|
|
706
|
-
// Download images via extension
|
|
804
|
+
// Download URL-backed images via extension; read blob images from the page in chunks.
|
|
707
805
|
const images = [];
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
806
|
+
for (const img of imageEntries) {
|
|
807
|
+
if (Number.isInteger(img?.blobIndex)) {
|
|
808
|
+
let b64 = "";
|
|
809
|
+
let offset = 0;
|
|
810
|
+
const chunkSize = 40000;
|
|
811
|
+
let type = img.type || "image/png";
|
|
812
|
+
while (true) {
|
|
813
|
+
const chunkResult = await jsEval(tabId, `(() => {
|
|
814
|
+
const item = window.__surfGeminiBlobImages?.[${img.blobIndex}];
|
|
815
|
+
if (!item) return JSON.stringify({ error: "Blob image not found" });
|
|
816
|
+
return JSON.stringify({
|
|
817
|
+
chunk: item.b64.slice(${offset}, ${offset + chunkSize}),
|
|
818
|
+
done: ${offset + chunkSize} >= item.b64.length,
|
|
819
|
+
type: item.type || "image/png",
|
|
820
|
+
url: item.url,
|
|
821
|
+
});
|
|
822
|
+
})()`);
|
|
823
|
+
const chunk = JSON.parse(JSON.parse(checkJsResult(chunkResult, "Read blob image chunk")));
|
|
824
|
+
if (chunk.error) throw new Error(chunk.error);
|
|
825
|
+
b64 += chunk.chunk || "";
|
|
826
|
+
type = chunk.type || type;
|
|
827
|
+
if (chunk.done) break;
|
|
828
|
+
offset += chunkSize;
|
|
714
829
|
}
|
|
830
|
+
images.push({ url: img.url, b64, type });
|
|
831
|
+
continue;
|
|
715
832
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
833
|
+
if (img?.url && fetchUrl) {
|
|
834
|
+
if (log) log(`Downloading image (${img.url.slice(0, 60)}...)...`);
|
|
835
|
+
const dlResult = await guardedFetchUrl(img.url);
|
|
836
|
+
if (dlResult?.b64) {
|
|
837
|
+
images.push({ url: img.url, b64: dlResult.b64, type: dlResult.type || "image/png" });
|
|
838
|
+
}
|
|
839
|
+
} else if (img?.url) {
|
|
840
|
+
images.push({ url: img.url });
|
|
719
841
|
}
|
|
720
842
|
}
|
|
721
843
|
|
|
@@ -728,7 +850,13 @@ async function runGeminiWebViaPage(input) {
|
|
|
728
850
|
_pageTabId: tabId,
|
|
729
851
|
};
|
|
730
852
|
} catch (err) {
|
|
731
|
-
if (tabId) {
|
|
853
|
+
if (tabId) {
|
|
854
|
+
try {
|
|
855
|
+
await closeTab(tabId);
|
|
856
|
+
} catch (closeError) {
|
|
857
|
+
log?.(`Failed to close Gemini tab ${tabId}: ${closeError?.message || closeError}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
732
860
|
throw err;
|
|
733
861
|
}
|
|
734
862
|
}
|
|
@@ -740,7 +868,7 @@ async function runGeminiWebViaPage(input) {
|
|
|
740
868
|
async function query(options) {
|
|
741
869
|
const {
|
|
742
870
|
prompt,
|
|
743
|
-
model = "gemini-3-pro",
|
|
871
|
+
model = "gemini-3.1-pro",
|
|
744
872
|
file,
|
|
745
873
|
generateImage,
|
|
746
874
|
editImage,
|
|
@@ -755,14 +883,17 @@ async function query(options) {
|
|
|
755
883
|
uploadFile,
|
|
756
884
|
timeout = 300000,
|
|
757
885
|
log = () => {},
|
|
886
|
+
signal,
|
|
758
887
|
} = options;
|
|
888
|
+
throwIfAborted(signal);
|
|
759
889
|
const hasPageCallbacks = !!(createTab && closeTab && jsEval);
|
|
890
|
+
const guardedJsEval = (...args) => raceAbort(() => jsEval(...args), signal);
|
|
760
891
|
|
|
761
892
|
const startTime = Date.now();
|
|
762
893
|
log("Starting Gemini query");
|
|
763
894
|
|
|
764
895
|
// 1. Get cookies from Chrome
|
|
765
|
-
const cookieResponse = await getCookies
|
|
896
|
+
const cookieResponse = await raceAbort(getCookies, signal);
|
|
766
897
|
const cookies = cookieResponse?.cookies;
|
|
767
898
|
if (!Array.isArray(cookies)) {
|
|
768
899
|
throw new Error("Failed to get cookies from Chrome. Make sure the extension is loaded and Chrome is running.");
|
|
@@ -776,7 +907,13 @@ async function query(options) {
|
|
|
776
907
|
log(`Got ${Object.keys(cookieMap).length} Gemini cookies`);
|
|
777
908
|
|
|
778
909
|
// 2. Resolve model
|
|
779
|
-
|
|
910
|
+
let resolvedModel;
|
|
911
|
+
if (MODEL_HEADERS[model]) {
|
|
912
|
+
resolvedModel = model;
|
|
913
|
+
} else {
|
|
914
|
+
resolvedModel = "gemini-3.1-pro";
|
|
915
|
+
log(`Unknown Gemini model "${model}"; using "${resolvedModel}"`);
|
|
916
|
+
}
|
|
780
917
|
|
|
781
918
|
// 3. Build prompt
|
|
782
919
|
let fullPrompt = prompt || "";
|
|
@@ -813,16 +950,17 @@ async function query(options) {
|
|
|
813
950
|
log,
|
|
814
951
|
createTab,
|
|
815
952
|
closeTab,
|
|
816
|
-
jsEval,
|
|
953
|
+
jsEval: guardedJsEval,
|
|
817
954
|
fetchUrl,
|
|
818
955
|
uploadFile,
|
|
956
|
+
signal,
|
|
819
957
|
});
|
|
820
958
|
|
|
821
959
|
response = out;
|
|
822
960
|
|
|
823
961
|
// Save output image
|
|
824
962
|
const outputPath = output || generateImage || "edited.png";
|
|
825
|
-
const saveOpts = { timeoutMs: timeout, log };
|
|
963
|
+
const saveOpts = { timeoutMs: timeout, log, signal };
|
|
826
964
|
if (fetchUrl) saveOpts.fetchUrl = fetchUrl;
|
|
827
965
|
try {
|
|
828
966
|
const imageSave = await saveFirstGeminiImage(out, cookieMap, outputPath, saveOpts);
|
|
@@ -830,7 +968,13 @@ async function query(options) {
|
|
|
830
968
|
throw new Error(`No images generated. Response: ${out.text?.slice(0, 200) || "(empty)"}`);
|
|
831
969
|
}
|
|
832
970
|
} finally {
|
|
833
|
-
if (out._pageTabId && closeTab) {
|
|
971
|
+
if (out._pageTabId && closeTab) {
|
|
972
|
+
try {
|
|
973
|
+
await closeTab(out._pageTabId);
|
|
974
|
+
} catch (closeError) {
|
|
975
|
+
log(`Failed to close Gemini tab ${out._pageTabId}: ${closeError?.message || closeError}`);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
834
978
|
}
|
|
835
979
|
imagePath = outputPath;
|
|
836
980
|
|
|
@@ -846,8 +990,9 @@ async function query(options) {
|
|
|
846
990
|
log,
|
|
847
991
|
createTab,
|
|
848
992
|
closeTab,
|
|
849
|
-
jsEval,
|
|
993
|
+
jsEval: guardedJsEval,
|
|
850
994
|
fetchUrl,
|
|
995
|
+
signal,
|
|
851
996
|
});
|
|
852
997
|
} else {
|
|
853
998
|
out = await runGeminiWebWithFallback({
|
|
@@ -858,13 +1003,14 @@ async function query(options) {
|
|
|
858
1003
|
chatMetadata: null,
|
|
859
1004
|
timeoutMs: timeout,
|
|
860
1005
|
log,
|
|
1006
|
+
signal,
|
|
861
1007
|
});
|
|
862
1008
|
}
|
|
863
1009
|
|
|
864
1010
|
response = out;
|
|
865
1011
|
|
|
866
1012
|
// Save output image
|
|
867
|
-
const saveOpts = { timeoutMs: timeout, log };
|
|
1013
|
+
const saveOpts = { timeoutMs: timeout, log, signal };
|
|
868
1014
|
if (fetchUrl) saveOpts.fetchUrl = fetchUrl;
|
|
869
1015
|
try {
|
|
870
1016
|
const imageSave = await saveFirstGeminiImage(out, cookieMap, generateImage, saveOpts);
|
|
@@ -872,7 +1018,13 @@ async function query(options) {
|
|
|
872
1018
|
throw new Error(`No images generated. Response: ${out.text?.slice(0, 200) || "(empty)"}`);
|
|
873
1019
|
}
|
|
874
1020
|
} finally {
|
|
875
|
-
if (out._pageTabId && closeTab) {
|
|
1021
|
+
if (out._pageTabId && closeTab) {
|
|
1022
|
+
try {
|
|
1023
|
+
await closeTab(out._pageTabId);
|
|
1024
|
+
} catch (closeError) {
|
|
1025
|
+
log(`Failed to close Gemini tab ${out._pageTabId}: ${closeError?.message || closeError}`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
876
1028
|
}
|
|
877
1029
|
imagePath = generateImage;
|
|
878
1030
|
|
|
@@ -887,11 +1039,13 @@ async function query(options) {
|
|
|
887
1039
|
chatMetadata: null,
|
|
888
1040
|
timeoutMs: timeout,
|
|
889
1041
|
log,
|
|
1042
|
+
signal,
|
|
890
1043
|
});
|
|
891
1044
|
|
|
892
1045
|
response = out;
|
|
893
1046
|
}
|
|
894
1047
|
} catch (error) {
|
|
1048
|
+
if (error?.code === "SURF_REQUEST_ABORTED") throw error;
|
|
895
1049
|
throw new Error(`Gemini request failed: ${error.message}`);
|
|
896
1050
|
}
|
|
897
1051
|
|
|
@@ -913,10 +1067,12 @@ async function query(options) {
|
|
|
913
1067
|
// ============================================================================
|
|
914
1068
|
|
|
915
1069
|
module.exports = {
|
|
1070
|
+
httpsGet,
|
|
916
1071
|
query,
|
|
917
1072
|
hasRequiredCookies,
|
|
918
1073
|
buildCookieMap,
|
|
919
1074
|
parseGeminiStreamGenerateResponse,
|
|
1075
|
+
runGeminiWebViaPage,
|
|
920
1076
|
REQUIRED_COOKIES,
|
|
921
1077
|
ALL_COOKIE_NAMES,
|
|
922
1078
|
GEMINI_APP_URL,
|