surf-cli 2.5.2 → 2.7.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 +46 -3
- package/dist/content/accessibility-tree.js +8 -8
- package/dist/content/accessibility-tree.js.map +1 -1
- package/dist/content/visual-indicator.js +17 -17
- package/dist/content/visual-indicator.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/service-worker/index.js +27 -27
- package/dist/service-worker/index.js.map +1 -1
- package/native/aistudio-build.cjs +562 -0
- package/native/aistudio-client.cjs +502 -0
- package/native/aistudio-model.cjs +225 -0
- package/native/aistudio-parser.cjs +424 -0
- package/native/aistudio-response.cjs +336 -0
- package/native/cli.cjs +95 -14
- package/native/do-executor.cjs +1 -1
- package/native/gemini-client.cjs +304 -63
- package/native/host-helpers.cjs +29 -0
- package/native/host.cjs +261 -14
- package/native/mcp-server.cjs +1 -1
- package/native/network-store.cjs +3 -1
- package/package.json +10 -9
- package/scripts/install-native-host.cjs +14 -2
- package/scripts/uninstall-native-host.cjs +7 -1
- package/skills/README.md +21 -0
- package/skills/surf/SKILL.md +575 -0
package/native/gemini-client.cjs
CHANGED
|
@@ -143,7 +143,15 @@ function httpsGet(url, headers, opts = {}) {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
function httpsPost(url, headers, body, opts = {}) {
|
|
146
|
-
|
|
146
|
+
return httpsSend("POST", url, headers, body, opts);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function httpsPut(url, headers, body, opts = {}) {
|
|
150
|
+
return httpsSend("PUT", url, headers, body, opts);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function httpsSend(method, url, headers, body, opts = {}) {
|
|
154
|
+
const { timeoutMs = 30000, log = null, label = "httpsSend" } = opts;
|
|
147
155
|
return new Promise((resolve, reject) => {
|
|
148
156
|
const urlObj = new URL(url);
|
|
149
157
|
const bodyBuffer = body == null
|
|
@@ -156,7 +164,7 @@ function httpsPost(url, headers, body, opts = {}) {
|
|
|
156
164
|
hostname: urlObj.hostname,
|
|
157
165
|
port: 443,
|
|
158
166
|
path: urlObj.pathname + urlObj.search,
|
|
159
|
-
method
|
|
167
|
+
method,
|
|
160
168
|
headers: {
|
|
161
169
|
"user-agent": USER_AGENT,
|
|
162
170
|
...headers,
|
|
@@ -224,6 +232,32 @@ async function fetchGeminiAccessToken(cookieMap, opts = {}) {
|
|
|
224
232
|
}
|
|
225
233
|
|
|
226
234
|
function trimGeminiJsonEnvelope(text) {
|
|
235
|
+
// Handle streaming chunk format: )]}\n\n<size>\n<json>\n<size>\n<json>...
|
|
236
|
+
const lines = text.split("\n");
|
|
237
|
+
const chunks = [];
|
|
238
|
+
for (let i = 0; i < lines.length; i++) {
|
|
239
|
+
const line = lines[i].trim();
|
|
240
|
+
if (!line || line === ")]}'" || /^\d+$/.test(line)) continue;
|
|
241
|
+
if (line.startsWith("[")) {
|
|
242
|
+
chunks.push(line);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (chunks.length > 1) {
|
|
247
|
+
const merged = [];
|
|
248
|
+
for (const chunk of chunks) {
|
|
249
|
+
try {
|
|
250
|
+
const parsed = JSON.parse(chunk);
|
|
251
|
+
if (Array.isArray(parsed)) {
|
|
252
|
+
merged.push(...parsed);
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
// ignore
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return JSON.stringify(merged);
|
|
259
|
+
}
|
|
260
|
+
|
|
227
261
|
const start = text.indexOf("[");
|
|
228
262
|
const end = text.lastIndexOf("]");
|
|
229
263
|
if (start === -1 || end === -1 || end <= start) {
|
|
@@ -347,32 +381,48 @@ function parseGeminiStreamGenerateResponse(rawText) {
|
|
|
347
381
|
// File Upload
|
|
348
382
|
// ============================================================================
|
|
349
383
|
|
|
350
|
-
async function uploadGeminiFile(filePath, opts = {}) {
|
|
384
|
+
async function uploadGeminiFile(filePath, cookieMap, opts = {}) {
|
|
351
385
|
const absPath = path.resolve(process.cwd(), filePath);
|
|
352
386
|
const data = fs.readFileSync(absPath);
|
|
353
387
|
const fileName = path.basename(absPath);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
Buffer.from(header, "utf-8"),
|
|
362
|
-
data,
|
|
363
|
-
Buffer.from(footer, "utf-8"),
|
|
364
|
-
]);
|
|
365
|
-
|
|
366
|
-
const res = await httpsPost(GEMINI_UPLOAD_URL, {
|
|
367
|
-
"content-type": `multipart/form-data; boundary=${boundary}`,
|
|
388
|
+
const cookieHeader = buildCookieHeader(cookieMap);
|
|
389
|
+
|
|
390
|
+
// Step 1: Initiate resumable upload
|
|
391
|
+
const initRes = await httpsPut(GEMINI_UPLOAD_URL, {
|
|
392
|
+
"authorization": "Basic c2F2ZXM6cyNMdGhlNmxzd2F2b0RsN3J1d1U=",
|
|
393
|
+
"content-type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
394
|
+
"cookie": cookieHeader,
|
|
368
395
|
"push-id": GEMINI_UPLOAD_PUSH_ID,
|
|
369
|
-
|
|
396
|
+
"referer": "https://gemini.google.com/",
|
|
397
|
+
"x-goog-upload-command": "start",
|
|
398
|
+
"x-goog-upload-header-content-length": String(data.length),
|
|
399
|
+
"x-goog-upload-header-content-type": "application/octet-stream",
|
|
400
|
+
"x-goog-upload-protocol": "resumable",
|
|
401
|
+
"x-tenant-id": "bard-storage",
|
|
402
|
+
}, data, { ...opts, label: opts.label || "geminiUploadInit" });
|
|
403
|
+
|
|
404
|
+
const uploadId = initRes.headers["x-guploader-uploadid"];
|
|
405
|
+
if (!uploadId) {
|
|
406
|
+
throw new Error(`File upload init failed: no upload ID (${initRes.status})`);
|
|
407
|
+
}
|
|
370
408
|
|
|
371
|
-
|
|
372
|
-
|
|
409
|
+
// Step 2: Upload data and finalize
|
|
410
|
+
const uploadUrl = `${GEMINI_UPLOAD_URL}?upload_id=${encodeURIComponent(uploadId)}&upload_protocol=resumable`;
|
|
411
|
+
const finalRes = await httpsPut(uploadUrl, {
|
|
412
|
+
"content-type": "application/octet-stream",
|
|
413
|
+
"cookie": cookieHeader,
|
|
414
|
+
"origin": "https://gemini.google.com",
|
|
415
|
+
"referer": "https://gemini.google.com/",
|
|
416
|
+
"x-goog-upload-command": "upload, finalize",
|
|
417
|
+
"x-goog-upload-offset": "0",
|
|
418
|
+
"x-tenant-id": "bard-storage",
|
|
419
|
+
}, data, { ...opts, label: opts.label || "geminiUpload" });
|
|
420
|
+
|
|
421
|
+
if (finalRes.status < 200 || finalRes.status >= 300) {
|
|
422
|
+
throw new Error(`File upload failed: ${finalRes.status} (${finalRes.text.slice(0, 200)})`);
|
|
373
423
|
}
|
|
374
424
|
|
|
375
|
-
return { id:
|
|
425
|
+
return { id: finalRes.text, name: fileName };
|
|
376
426
|
}
|
|
377
427
|
|
|
378
428
|
// ============================================================================
|
|
@@ -402,18 +452,51 @@ async function downloadGeminiImage(url, cookieMap, outputPath, opts = {}) {
|
|
|
402
452
|
fs.writeFileSync(outputPath, res.buffer);
|
|
403
453
|
}
|
|
404
454
|
|
|
455
|
+
async function downloadGeminiImageViaExtension(url, outputPath, opts = {}) {
|
|
456
|
+
const { fetchUrl, log } = opts;
|
|
457
|
+
const fullUrl = ensureFullSizeImageUrl(url);
|
|
458
|
+
|
|
459
|
+
const result = await fetchUrl(fullUrl);
|
|
460
|
+
if (!result || result.error) throw new Error(`Image download failed: ${result?.error || "no response"}`);
|
|
461
|
+
if (!result.b64) throw new Error("Image download returned no data");
|
|
462
|
+
|
|
463
|
+
const dir = path.dirname(outputPath);
|
|
464
|
+
if (dir && !fs.existsSync(dir)) {
|
|
465
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
fs.writeFileSync(outputPath, Buffer.from(result.b64, "base64"));
|
|
469
|
+
}
|
|
470
|
+
|
|
405
471
|
async function saveFirstGeminiImage(output, cookieMap, outputPath, opts = {}) {
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
472
|
+
const useExtensionDownload = !!opts.fetchUrl;
|
|
473
|
+
const img = output.images?.find(i => i.kind === "generated") ?? output.images?.[0];
|
|
474
|
+
|
|
475
|
+
if (img?.b64) {
|
|
476
|
+
const dir = path.dirname(outputPath);
|
|
477
|
+
if (dir && !fs.existsSync(dir)) {
|
|
478
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
479
|
+
}
|
|
480
|
+
fs.writeFileSync(outputPath, Buffer.from(img.b64, "base64"));
|
|
481
|
+
return { saved: true, imageCount: output.images.length };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (img?.url) {
|
|
485
|
+
if (useExtensionDownload) {
|
|
486
|
+
await downloadGeminiImageViaExtension(img.url, outputPath, opts);
|
|
487
|
+
} else {
|
|
488
|
+
await downloadGeminiImage(img.url, cookieMap, outputPath, opts);
|
|
489
|
+
}
|
|
410
490
|
return { saved: true, imageCount: output.images.length };
|
|
411
491
|
}
|
|
412
492
|
|
|
413
|
-
|
|
414
|
-
const ggdl = extractGgdlUrls(output.rawResponseText);
|
|
493
|
+
const ggdl = extractGgdlUrls(output.rawResponseText || "");
|
|
415
494
|
if (ggdl[0]) {
|
|
416
|
-
|
|
495
|
+
if (useExtensionDownload) {
|
|
496
|
+
await downloadGeminiImageViaExtension(ggdl[0], outputPath, opts);
|
|
497
|
+
} else {
|
|
498
|
+
await downloadGeminiImage(ggdl[0], cookieMap, outputPath, opts);
|
|
499
|
+
}
|
|
417
500
|
return { saved: true, imageCount: ggdl.length };
|
|
418
501
|
}
|
|
419
502
|
|
|
@@ -426,7 +509,7 @@ async function saveFirstGeminiImage(output, cookieMap, outputPath, opts = {}) {
|
|
|
426
509
|
|
|
427
510
|
function buildGeminiFReqPayload(prompt, uploaded, chatMetadata) {
|
|
428
511
|
const promptPayload = uploaded.length > 0
|
|
429
|
-
? [prompt, 0, null, uploaded.map(file => [[file.id, 1]])]
|
|
512
|
+
? [prompt, 0, null, uploaded.map(file => [[file.id, 1], file.name])]
|
|
430
513
|
: [prompt];
|
|
431
514
|
|
|
432
515
|
const innerList = [promptPayload, null, chatMetadata ?? null];
|
|
@@ -443,7 +526,7 @@ async function runGeminiWebOnce(input) {
|
|
|
443
526
|
// 2. Upload files
|
|
444
527
|
const uploaded = [];
|
|
445
528
|
for (const file of files ?? []) {
|
|
446
|
-
uploaded.push(await uploadGeminiFile(file, { timeoutMs, log, label: "geminiUpload" }));
|
|
529
|
+
uploaded.push(await uploadGeminiFile(file, cookieMap, { timeoutMs, log, label: "geminiUpload" }));
|
|
447
530
|
}
|
|
448
531
|
|
|
449
532
|
// 3. Build request
|
|
@@ -519,6 +602,137 @@ async function runGeminiWebWithFallback(input) {
|
|
|
519
602
|
return { ...attempt, effectiveModel: input.model };
|
|
520
603
|
}
|
|
521
604
|
|
|
605
|
+
// ============================================================================
|
|
606
|
+
// In-Page Execution (for image generation)
|
|
607
|
+
// ============================================================================
|
|
608
|
+
|
|
609
|
+
async function runGeminiWebViaPage(input) {
|
|
610
|
+
const { prompt, files, model, timeoutMs = 120000, log = null, createTab, closeTab, jsEval, fetchUrl, uploadFile } = input;
|
|
611
|
+
|
|
612
|
+
if (!createTab || !closeTab || !jsEval) {
|
|
613
|
+
throw new Error("In-page execution requires createTab, closeTab, and jsEval callbacks");
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
let tabId = null;
|
|
617
|
+
try {
|
|
618
|
+
if (log) log("Creating Gemini tab...");
|
|
619
|
+
const tabResult = await createTab();
|
|
620
|
+
tabId = tabResult?.tabId;
|
|
621
|
+
if (!tabId) throw new Error("Failed to create Gemini tab");
|
|
622
|
+
if (log) log(`Gemini tab created: ${tabId}`);
|
|
623
|
+
await new Promise(r => setTimeout(r, 12000));
|
|
624
|
+
|
|
625
|
+
if (files?.length && uploadFile) {
|
|
626
|
+
const absFiles = files.map(f => path.resolve(process.cwd(), f));
|
|
627
|
+
if (log) log(`Uploading ${absFiles.length} file(s) via file chooser...`);
|
|
628
|
+
const result = await uploadFile(tabId, absFiles);
|
|
629
|
+
if (result?.error) throw new Error(`File upload failed: ${result.error}`);
|
|
630
|
+
if (log) log("File uploaded, waiting for processing...");
|
|
631
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const checkJsResult = (result, context) => {
|
|
635
|
+
if (result?.error) throw new Error(`${context}: ${result.error}`);
|
|
636
|
+
if (result?.output === undefined) throw new Error(`${context}: no output`);
|
|
637
|
+
return result.output;
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// Type prompt
|
|
641
|
+
const fullPrompt = prompt.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
642
|
+
if (log) log("Typing prompt...");
|
|
643
|
+
const typeResult = await jsEval(tabId, `
|
|
644
|
+
const editor = document.querySelector('.ql-editor[contenteditable=true]');
|
|
645
|
+
if (!editor) return JSON.stringify({ error: "No editor found on page" });
|
|
646
|
+
editor.focus();
|
|
647
|
+
document.execCommand('selectAll', false, null);
|
|
648
|
+
document.execCommand('insertText', false, '${fullPrompt}');
|
|
649
|
+
return JSON.stringify({ ok: true, len: editor.textContent.length });
|
|
650
|
+
`);
|
|
651
|
+
const typed = JSON.parse(JSON.parse(checkJsResult(typeResult, "Type prompt")));
|
|
652
|
+
if (typed.error) throw new Error(typed.error);
|
|
653
|
+
|
|
654
|
+
const beforeResult = await jsEval(tabId, `
|
|
655
|
+
return String(Array.from(document.querySelectorAll('img[src*="gg-dl"]')).filter(i => i.naturalWidth >= 512).length);
|
|
656
|
+
`);
|
|
657
|
+
const imgCountBefore = parseInt(JSON.parse(checkJsResult(beforeResult, "Count images")) || "0", 10);
|
|
658
|
+
|
|
659
|
+
if (log) log("Submitting...");
|
|
660
|
+
const sendResult = await jsEval(tabId, `
|
|
661
|
+
const btn = document.querySelector('button[aria-label="Send message"]');
|
|
662
|
+
if (!btn) return 'no-btn';
|
|
663
|
+
btn.click();
|
|
664
|
+
return 'sent';
|
|
665
|
+
`);
|
|
666
|
+
const sendVal = JSON.parse(checkJsResult(sendResult, "Click send"));
|
|
667
|
+
if (sendVal === "no-btn") throw new Error("Send button not found on Gemini page");
|
|
668
|
+
|
|
669
|
+
// Poll for response
|
|
670
|
+
if (log) log("Waiting for response...");
|
|
671
|
+
const deadline = Date.now() + timeoutMs;
|
|
672
|
+
let imageUrls = [];
|
|
673
|
+
let responseText = "";
|
|
674
|
+
|
|
675
|
+
while (Date.now() < deadline) {
|
|
676
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
677
|
+
const pollResult = await jsEval(tabId, `
|
|
678
|
+
const ggImgs = Array.from(document.querySelectorAll('img[src*="gg-dl"]'))
|
|
679
|
+
.filter(i => i.naturalWidth >= 512)
|
|
680
|
+
.map(i => i.src);
|
|
681
|
+
const loading = !!document.querySelector('mat-progress-bar, .loading-indicator, message-loading');
|
|
682
|
+
const turns = document.querySelectorAll('message-content');
|
|
683
|
+
const lastTurn = turns.length ? turns[turns.length - 1] : null;
|
|
684
|
+
const text = lastTurn ? lastTurn.textContent?.trim() : "";
|
|
685
|
+
return JSON.stringify({ ggImgs, loading, text, turns: turns.length });
|
|
686
|
+
`);
|
|
687
|
+
const poll = JSON.parse(JSON.parse(checkJsResult(pollResult, "Poll response")));
|
|
688
|
+
const newImgs = poll.ggImgs.slice(imgCountBefore);
|
|
689
|
+
|
|
690
|
+
if (newImgs.length > 0) {
|
|
691
|
+
imageUrls = newImgs;
|
|
692
|
+
responseText = poll.text || "";
|
|
693
|
+
if (log) log(`Found ${newImgs.length} generated image(s)`);
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
if (!poll.loading && poll.text && poll.turns > 0) {
|
|
697
|
+
responseText = poll.text;
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (!imageUrls.length && !responseText) {
|
|
703
|
+
throw new Error("Gemini response timed out");
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Download images via extension
|
|
707
|
+
const images = [];
|
|
708
|
+
if (fetchUrl) {
|
|
709
|
+
for (const url of imageUrls) {
|
|
710
|
+
if (log) log(`Downloading image (${url.slice(0, 60)}...)...`);
|
|
711
|
+
const dlResult = await fetchUrl(url);
|
|
712
|
+
if (dlResult?.b64) {
|
|
713
|
+
images.push({ url, b64: dlResult.b64, type: dlResult.type || "image/png" });
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
for (const url of imageUrls) {
|
|
718
|
+
images.push({ url });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return {
|
|
723
|
+
text: responseText,
|
|
724
|
+
thoughts: null,
|
|
725
|
+
metadata: null,
|
|
726
|
+
images,
|
|
727
|
+
effectiveModel: model,
|
|
728
|
+
_pageTabId: tabId,
|
|
729
|
+
};
|
|
730
|
+
} catch (err) {
|
|
731
|
+
if (tabId) { try { await closeTab(tabId); } catch {} }
|
|
732
|
+
throw err;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
522
736
|
// ============================================================================
|
|
523
737
|
// Main Query Function
|
|
524
738
|
// ============================================================================
|
|
@@ -534,9 +748,15 @@ async function query(options) {
|
|
|
534
748
|
youtube,
|
|
535
749
|
aspectRatio,
|
|
536
750
|
getCookies,
|
|
751
|
+
createTab,
|
|
752
|
+
closeTab,
|
|
753
|
+
jsEval,
|
|
754
|
+
fetchUrl,
|
|
755
|
+
uploadFile,
|
|
537
756
|
timeout = 300000,
|
|
538
757
|
log = () => {},
|
|
539
758
|
} = options;
|
|
759
|
+
const hasPageCallbacks = !!(createTab && closeTab && jsEval);
|
|
540
760
|
|
|
541
761
|
const startTime = Date.now();
|
|
542
762
|
log("Starting Gemini query");
|
|
@@ -579,59 +799,80 @@ async function query(options) {
|
|
|
579
799
|
|
|
580
800
|
try {
|
|
581
801
|
if (editImage) {
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
802
|
+
// Image editing
|
|
803
|
+
if (!hasPageCallbacks) {
|
|
804
|
+
throw new Error("Image editing requires the Chrome extension. Make sure it's loaded.");
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
log("Uploading and editing image...");
|
|
808
|
+
const out = await runGeminiWebViaPage({
|
|
809
|
+
prompt: fullPrompt,
|
|
586
810
|
files: [editImage],
|
|
587
811
|
model: resolvedModel,
|
|
588
|
-
cookieMap,
|
|
589
|
-
chatMetadata: null,
|
|
590
|
-
timeoutMs: timeout,
|
|
591
|
-
log,
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
log("Sending edit request...");
|
|
595
|
-
const editPrompt = `Use image generation tool to ${fullPrompt}`;
|
|
596
|
-
const out = await runGeminiWebWithFallback({
|
|
597
|
-
prompt: editPrompt,
|
|
598
|
-
files,
|
|
599
|
-
model: resolvedModel,
|
|
600
|
-
cookieMap,
|
|
601
|
-
chatMetadata: intro.metadata,
|
|
602
812
|
timeoutMs: timeout,
|
|
603
813
|
log,
|
|
814
|
+
createTab,
|
|
815
|
+
closeTab,
|
|
816
|
+
jsEval,
|
|
817
|
+
fetchUrl,
|
|
818
|
+
uploadFile,
|
|
604
819
|
});
|
|
605
820
|
|
|
606
821
|
response = out;
|
|
607
822
|
|
|
608
823
|
// Save output image
|
|
609
824
|
const outputPath = output || generateImage || "edited.png";
|
|
610
|
-
const
|
|
611
|
-
if (
|
|
612
|
-
|
|
825
|
+
const saveOpts = { timeoutMs: timeout, log };
|
|
826
|
+
if (fetchUrl) saveOpts.fetchUrl = fetchUrl;
|
|
827
|
+
try {
|
|
828
|
+
const imageSave = await saveFirstGeminiImage(out, cookieMap, outputPath, saveOpts);
|
|
829
|
+
if (!imageSave.saved) {
|
|
830
|
+
throw new Error(`No images generated. Response: ${out.text?.slice(0, 200) || "(empty)"}`);
|
|
831
|
+
}
|
|
832
|
+
} finally {
|
|
833
|
+
if (out._pageTabId && closeTab) { try { await closeTab(out._pageTabId); } catch {} }
|
|
613
834
|
}
|
|
614
835
|
imagePath = outputPath;
|
|
615
836
|
|
|
616
837
|
} else if (generateImage) {
|
|
617
838
|
// Image generation
|
|
618
839
|
log("Generating image...");
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
840
|
+
let out;
|
|
841
|
+
if (hasPageCallbacks) {
|
|
842
|
+
out = await runGeminiWebViaPage({
|
|
843
|
+
prompt: fullPrompt,
|
|
844
|
+
model: resolvedModel,
|
|
845
|
+
timeoutMs: timeout,
|
|
846
|
+
log,
|
|
847
|
+
createTab,
|
|
848
|
+
closeTab,
|
|
849
|
+
jsEval,
|
|
850
|
+
fetchUrl,
|
|
851
|
+
});
|
|
852
|
+
} else {
|
|
853
|
+
out = await runGeminiWebWithFallback({
|
|
854
|
+
prompt: fullPrompt,
|
|
855
|
+
files,
|
|
856
|
+
model: resolvedModel,
|
|
857
|
+
cookieMap,
|
|
858
|
+
chatMetadata: null,
|
|
859
|
+
timeoutMs: timeout,
|
|
860
|
+
log,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
628
863
|
|
|
629
864
|
response = out;
|
|
630
865
|
|
|
631
866
|
// Save output image
|
|
632
|
-
const
|
|
633
|
-
if (
|
|
634
|
-
|
|
867
|
+
const saveOpts = { timeoutMs: timeout, log };
|
|
868
|
+
if (fetchUrl) saveOpts.fetchUrl = fetchUrl;
|
|
869
|
+
try {
|
|
870
|
+
const imageSave = await saveFirstGeminiImage(out, cookieMap, generateImage, saveOpts);
|
|
871
|
+
if (!imageSave.saved) {
|
|
872
|
+
throw new Error(`No images generated. Response: ${out.text?.slice(0, 200) || "(empty)"}`);
|
|
873
|
+
}
|
|
874
|
+
} finally {
|
|
875
|
+
if (out._pageTabId && closeTab) { try { await closeTab(out._pageTabId); } catch {} }
|
|
635
876
|
}
|
|
636
877
|
imagePath = generateImage;
|
|
637
878
|
|
package/native/host-helpers.cjs
CHANGED
|
@@ -2,6 +2,10 @@ const fs = require("fs");
|
|
|
2
2
|
const networkFormatters = require("./formatters/network.cjs");
|
|
3
3
|
const networkStore = require("./network-store.cjs");
|
|
4
4
|
|
|
5
|
+
function normalizeModelString(model) {
|
|
6
|
+
return String(model || "").trim().toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
/**
|
|
6
10
|
* Format tool result content for MCP response
|
|
7
11
|
* @param {*} result - The result object from the extension
|
|
@@ -1070,6 +1074,31 @@ function mapToolToMessage(tool, args, tabId) {
|
|
|
1070
1074
|
timeout: a.timeout ? parseInt(a.timeout, 10) * 1000 : 300000,
|
|
1071
1075
|
...baseMsg
|
|
1072
1076
|
};
|
|
1077
|
+
case "aistudio": {
|
|
1078
|
+
if (!a.query) throw new Error("query required");
|
|
1079
|
+
|
|
1080
|
+
return {
|
|
1081
|
+
type: "AISTUDIO_QUERY",
|
|
1082
|
+
query: a.query,
|
|
1083
|
+
model: a.model ? normalizeModelString(a.model) : undefined,
|
|
1084
|
+
withPage: a["with-page"],
|
|
1085
|
+
timeout: a.timeout ? parseInt(a.timeout, 10) * 1000 : 300000,
|
|
1086
|
+
...baseMsg
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
case "aistudio.build": {
|
|
1090
|
+
if (!a.query) throw new Error("query required");
|
|
1091
|
+
|
|
1092
|
+
return {
|
|
1093
|
+
type: "AISTUDIO_BUILD",
|
|
1094
|
+
query: a.query,
|
|
1095
|
+
model: a.model ? normalizeModelString(a.model) : undefined,
|
|
1096
|
+
output: a.output,
|
|
1097
|
+
keepOpen: Boolean(a["keep-open"] || a.keepOpen),
|
|
1098
|
+
timeout: a.timeout ? parseInt(a.timeout, 10) * 1000 : 600000,
|
|
1099
|
+
...baseMsg,
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1073
1102
|
case "window.new":
|
|
1074
1103
|
return {
|
|
1075
1104
|
type: "WINDOW_NEW",
|