surf-cli 2.8.0 → 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 +98 -4
- 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 +2 -2
- package/native/chatgpt-client.cjs +47 -31
- package/native/cli.cjs +300 -204
- package/native/client-transport.cjs +168 -0
- package/native/do-executor.cjs +25 -44
- package/native/doctor.cjs +55 -5
- package/native/endpoint.cjs +174 -0
- package/native/file-transfer.cjs +734 -0
- package/native/gemini-client.cjs +156 -71
- package/native/grok-client.cjs +98 -89
- package/native/host-helpers.cjs +37 -12
- package/native/host-sessions.cjs +283 -0
- package/native/host.cjs +800 -620
- package/native/listener.cjs +20 -0
- package/native/mcp-server.cjs +60 -65
- 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 +1 -1
- package/package.json +8 -6
- package/scripts/install-native-host.cjs +36 -5
- package/skills/README.md +11 -5
- package/skills/deep-x-research/SKILL.md +106 -0
- package/skills/surf/SKILL.md +31 -4
- 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,20 +690,20 @@ 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, `
|
|
706
|
+
const beforeResult = await jsEval(tabId, `(() => {
|
|
655
707
|
const imageKey = (img) => {
|
|
656
708
|
const url = img.currentSrc || img.src || "";
|
|
657
709
|
return url + "|" + img.naturalWidth + "x" + img.naturalHeight;
|
|
@@ -665,16 +717,16 @@ async function runGeminiWebViaPage(input) {
|
|
|
665
717
|
})
|
|
666
718
|
.map(imageKey);
|
|
667
719
|
return JSON.stringify(baselineKeys);
|
|
668
|
-
`);
|
|
720
|
+
})()`);
|
|
669
721
|
const baselineImageKeys = JSON.parse(JSON.parse(checkJsResult(beforeResult, "Count images")) || "[]");
|
|
670
722
|
|
|
671
723
|
if (log) log("Submitting...");
|
|
672
|
-
const sendResult = await jsEval(tabId, `
|
|
724
|
+
const sendResult = await jsEval(tabId, `(() => {
|
|
673
725
|
const btn = document.querySelector('button[aria-label="Send message"]');
|
|
674
726
|
if (!btn) return 'no-btn';
|
|
675
727
|
btn.click();
|
|
676
728
|
return 'sent';
|
|
677
|
-
`);
|
|
729
|
+
})()`);
|
|
678
730
|
const sendVal = JSON.parse(checkJsResult(sendResult, "Click send"));
|
|
679
731
|
if (sendVal === "no-btn") throw new Error("Send button not found on Gemini page");
|
|
680
732
|
|
|
@@ -685,8 +737,8 @@ async function runGeminiWebViaPage(input) {
|
|
|
685
737
|
let responseText = "";
|
|
686
738
|
|
|
687
739
|
while (Date.now() < deadline) {
|
|
688
|
-
await
|
|
689
|
-
const pollResult = await jsEval(tabId, `
|
|
740
|
+
await abortableDelay(2000, signal);
|
|
741
|
+
const pollResult = await jsEval(tabId, `(async () => {
|
|
690
742
|
const baselineKeys = new Set(${JSON.stringify(baselineImageKeys)});
|
|
691
743
|
const imageKey = (img) => {
|
|
692
744
|
const url = img.currentSrc || img.src || "";
|
|
@@ -729,7 +781,7 @@ async function runGeminiWebViaPage(input) {
|
|
|
729
781
|
const lastTurn = turns.length ? turns[turns.length - 1] : null;
|
|
730
782
|
const text = lastTurn ? lastTurn.textContent?.trim() : "";
|
|
731
783
|
return JSON.stringify({ images, loading, text, turns: turns.length });
|
|
732
|
-
`);
|
|
784
|
+
})()`);
|
|
733
785
|
const poll = JSON.parse(JSON.parse(checkJsResult(pollResult, "Poll response")));
|
|
734
786
|
const newImgs = poll.images || [];
|
|
735
787
|
|
|
@@ -758,7 +810,7 @@ async function runGeminiWebViaPage(input) {
|
|
|
758
810
|
const chunkSize = 40000;
|
|
759
811
|
let type = img.type || "image/png";
|
|
760
812
|
while (true) {
|
|
761
|
-
const chunkResult = await jsEval(tabId, `
|
|
813
|
+
const chunkResult = await jsEval(tabId, `(() => {
|
|
762
814
|
const item = window.__surfGeminiBlobImages?.[${img.blobIndex}];
|
|
763
815
|
if (!item) return JSON.stringify({ error: "Blob image not found" });
|
|
764
816
|
return JSON.stringify({
|
|
@@ -767,7 +819,7 @@ async function runGeminiWebViaPage(input) {
|
|
|
767
819
|
type: item.type || "image/png",
|
|
768
820
|
url: item.url,
|
|
769
821
|
});
|
|
770
|
-
`);
|
|
822
|
+
})()`);
|
|
771
823
|
const chunk = JSON.parse(JSON.parse(checkJsResult(chunkResult, "Read blob image chunk")));
|
|
772
824
|
if (chunk.error) throw new Error(chunk.error);
|
|
773
825
|
b64 += chunk.chunk || "";
|
|
@@ -780,7 +832,7 @@ async function runGeminiWebViaPage(input) {
|
|
|
780
832
|
}
|
|
781
833
|
if (img?.url && fetchUrl) {
|
|
782
834
|
if (log) log(`Downloading image (${img.url.slice(0, 60)}...)...`);
|
|
783
|
-
const dlResult = await
|
|
835
|
+
const dlResult = await guardedFetchUrl(img.url);
|
|
784
836
|
if (dlResult?.b64) {
|
|
785
837
|
images.push({ url: img.url, b64: dlResult.b64, type: dlResult.type || "image/png" });
|
|
786
838
|
}
|
|
@@ -798,7 +850,13 @@ async function runGeminiWebViaPage(input) {
|
|
|
798
850
|
_pageTabId: tabId,
|
|
799
851
|
};
|
|
800
852
|
} catch (err) {
|
|
801
|
-
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
|
+
}
|
|
802
860
|
throw err;
|
|
803
861
|
}
|
|
804
862
|
}
|
|
@@ -810,7 +868,7 @@ async function runGeminiWebViaPage(input) {
|
|
|
810
868
|
async function query(options) {
|
|
811
869
|
const {
|
|
812
870
|
prompt,
|
|
813
|
-
model = "gemini-3-pro",
|
|
871
|
+
model = "gemini-3.1-pro",
|
|
814
872
|
file,
|
|
815
873
|
generateImage,
|
|
816
874
|
editImage,
|
|
@@ -825,14 +883,17 @@ async function query(options) {
|
|
|
825
883
|
uploadFile,
|
|
826
884
|
timeout = 300000,
|
|
827
885
|
log = () => {},
|
|
886
|
+
signal,
|
|
828
887
|
} = options;
|
|
888
|
+
throwIfAborted(signal);
|
|
829
889
|
const hasPageCallbacks = !!(createTab && closeTab && jsEval);
|
|
890
|
+
const guardedJsEval = (...args) => raceAbort(() => jsEval(...args), signal);
|
|
830
891
|
|
|
831
892
|
const startTime = Date.now();
|
|
832
893
|
log("Starting Gemini query");
|
|
833
894
|
|
|
834
895
|
// 1. Get cookies from Chrome
|
|
835
|
-
const cookieResponse = await getCookies
|
|
896
|
+
const cookieResponse = await raceAbort(getCookies, signal);
|
|
836
897
|
const cookies = cookieResponse?.cookies;
|
|
837
898
|
if (!Array.isArray(cookies)) {
|
|
838
899
|
throw new Error("Failed to get cookies from Chrome. Make sure the extension is loaded and Chrome is running.");
|
|
@@ -846,7 +907,13 @@ async function query(options) {
|
|
|
846
907
|
log(`Got ${Object.keys(cookieMap).length} Gemini cookies`);
|
|
847
908
|
|
|
848
909
|
// 2. Resolve model
|
|
849
|
-
|
|
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
|
+
}
|
|
850
917
|
|
|
851
918
|
// 3. Build prompt
|
|
852
919
|
let fullPrompt = prompt || "";
|
|
@@ -883,16 +950,17 @@ async function query(options) {
|
|
|
883
950
|
log,
|
|
884
951
|
createTab,
|
|
885
952
|
closeTab,
|
|
886
|
-
jsEval,
|
|
953
|
+
jsEval: guardedJsEval,
|
|
887
954
|
fetchUrl,
|
|
888
955
|
uploadFile,
|
|
956
|
+
signal,
|
|
889
957
|
});
|
|
890
958
|
|
|
891
959
|
response = out;
|
|
892
960
|
|
|
893
961
|
// Save output image
|
|
894
962
|
const outputPath = output || generateImage || "edited.png";
|
|
895
|
-
const saveOpts = { timeoutMs: timeout, log };
|
|
963
|
+
const saveOpts = { timeoutMs: timeout, log, signal };
|
|
896
964
|
if (fetchUrl) saveOpts.fetchUrl = fetchUrl;
|
|
897
965
|
try {
|
|
898
966
|
const imageSave = await saveFirstGeminiImage(out, cookieMap, outputPath, saveOpts);
|
|
@@ -900,7 +968,13 @@ async function query(options) {
|
|
|
900
968
|
throw new Error(`No images generated. Response: ${out.text?.slice(0, 200) || "(empty)"}`);
|
|
901
969
|
}
|
|
902
970
|
} finally {
|
|
903
|
-
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
|
+
}
|
|
904
978
|
}
|
|
905
979
|
imagePath = outputPath;
|
|
906
980
|
|
|
@@ -916,8 +990,9 @@ async function query(options) {
|
|
|
916
990
|
log,
|
|
917
991
|
createTab,
|
|
918
992
|
closeTab,
|
|
919
|
-
jsEval,
|
|
993
|
+
jsEval: guardedJsEval,
|
|
920
994
|
fetchUrl,
|
|
995
|
+
signal,
|
|
921
996
|
});
|
|
922
997
|
} else {
|
|
923
998
|
out = await runGeminiWebWithFallback({
|
|
@@ -928,13 +1003,14 @@ async function query(options) {
|
|
|
928
1003
|
chatMetadata: null,
|
|
929
1004
|
timeoutMs: timeout,
|
|
930
1005
|
log,
|
|
1006
|
+
signal,
|
|
931
1007
|
});
|
|
932
1008
|
}
|
|
933
1009
|
|
|
934
1010
|
response = out;
|
|
935
1011
|
|
|
936
1012
|
// Save output image
|
|
937
|
-
const saveOpts = { timeoutMs: timeout, log };
|
|
1013
|
+
const saveOpts = { timeoutMs: timeout, log, signal };
|
|
938
1014
|
if (fetchUrl) saveOpts.fetchUrl = fetchUrl;
|
|
939
1015
|
try {
|
|
940
1016
|
const imageSave = await saveFirstGeminiImage(out, cookieMap, generateImage, saveOpts);
|
|
@@ -942,7 +1018,13 @@ async function query(options) {
|
|
|
942
1018
|
throw new Error(`No images generated. Response: ${out.text?.slice(0, 200) || "(empty)"}`);
|
|
943
1019
|
}
|
|
944
1020
|
} finally {
|
|
945
|
-
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
|
+
}
|
|
946
1028
|
}
|
|
947
1029
|
imagePath = generateImage;
|
|
948
1030
|
|
|
@@ -957,11 +1039,13 @@ async function query(options) {
|
|
|
957
1039
|
chatMetadata: null,
|
|
958
1040
|
timeoutMs: timeout,
|
|
959
1041
|
log,
|
|
1042
|
+
signal,
|
|
960
1043
|
});
|
|
961
1044
|
|
|
962
1045
|
response = out;
|
|
963
1046
|
}
|
|
964
1047
|
} catch (error) {
|
|
1048
|
+
if (error?.code === "SURF_REQUEST_ABORTED") throw error;
|
|
965
1049
|
throw new Error(`Gemini request failed: ${error.message}`);
|
|
966
1050
|
}
|
|
967
1051
|
|
|
@@ -983,6 +1067,7 @@ async function query(options) {
|
|
|
983
1067
|
// ============================================================================
|
|
984
1068
|
|
|
985
1069
|
module.exports = {
|
|
1070
|
+
httpsGet,
|
|
986
1071
|
query,
|
|
987
1072
|
hasRequiredCookies,
|
|
988
1073
|
buildCookieMap,
|