surf-cli 2.5.0 → 2.5.2
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/native/cli.cjs +1 -1
- package/native/gemini-client.cjs +74 -29
- package/package.json +1 -1
package/native/cli.cjs
CHANGED
package/native/gemini-client.cjs
CHANGED
|
@@ -91,15 +91,12 @@ function hasRequiredCookies(cookieMap) {
|
|
|
91
91
|
return REQUIRED_COOKIES.every(name => Boolean(cookieMap[name]));
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
function delay(ms) {
|
|
95
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
94
|
// ============================================================================
|
|
99
95
|
// HTTP Helpers
|
|
100
96
|
// ============================================================================
|
|
101
97
|
|
|
102
|
-
function httpsGet(url, headers,
|
|
98
|
+
function httpsGet(url, headers, opts = {}) {
|
|
99
|
+
const { binary = false, timeoutMs = 30000, log = null, label = "httpsGet" } = opts;
|
|
103
100
|
return new Promise((resolve, reject) => {
|
|
104
101
|
const urlObj = new URL(url);
|
|
105
102
|
const options = {
|
|
@@ -111,9 +108,12 @@ function httpsGet(url, headers, binary = false) {
|
|
|
111
108
|
"user-agent": USER_AGENT,
|
|
112
109
|
...headers,
|
|
113
110
|
},
|
|
111
|
+
rejectUnauthorized: false, // Required for Gemini API
|
|
112
|
+
timeout: timeoutMs,
|
|
114
113
|
};
|
|
115
114
|
|
|
116
115
|
const req = https.request(options, (res) => {
|
|
116
|
+
if (log) log(`${label}: response ${res.statusCode} ${urlObj.hostname}${urlObj.pathname}`);
|
|
117
117
|
const chunks = [];
|
|
118
118
|
res.on("data", chunk => chunks.push(chunk));
|
|
119
119
|
res.on("end", () => {
|
|
@@ -125,16 +125,33 @@ function httpsGet(url, headers, binary = false) {
|
|
|
125
125
|
buffer: binary ? buffer : null,
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
|
+
res.on("error", (err) => {
|
|
129
|
+
if (log) log(`${label}: response error ${err.message}`);
|
|
130
|
+
reject(err);
|
|
131
|
+
});
|
|
128
132
|
});
|
|
129
133
|
|
|
130
|
-
req.on("
|
|
134
|
+
req.on("timeout", () => {
|
|
135
|
+
req.destroy(new Error(`${label}: request timeout after ${timeoutMs}ms`));
|
|
136
|
+
});
|
|
137
|
+
req.on("error", (err) => {
|
|
138
|
+
if (log) log(`${label}: request error ${err.message}`);
|
|
139
|
+
reject(err);
|
|
140
|
+
});
|
|
131
141
|
req.end();
|
|
132
142
|
});
|
|
133
143
|
}
|
|
134
144
|
|
|
135
|
-
function httpsPost(url, headers, body) {
|
|
145
|
+
function httpsPost(url, headers, body, opts = {}) {
|
|
146
|
+
const { timeoutMs = 30000, log = null, label = "httpsPost" } = opts;
|
|
136
147
|
return new Promise((resolve, reject) => {
|
|
137
148
|
const urlObj = new URL(url);
|
|
149
|
+
const bodyBuffer = body == null
|
|
150
|
+
? null
|
|
151
|
+
: Buffer.isBuffer(body)
|
|
152
|
+
? body
|
|
153
|
+
: Buffer.from(String(body), "utf-8");
|
|
154
|
+
const hasLength = Object.keys(headers || {}).some((key) => key.toLowerCase() === "content-length");
|
|
138
155
|
const options = {
|
|
139
156
|
hostname: urlObj.hostname,
|
|
140
157
|
port: 443,
|
|
@@ -143,25 +160,39 @@ function httpsPost(url, headers, body) {
|
|
|
143
160
|
headers: {
|
|
144
161
|
"user-agent": USER_AGENT,
|
|
145
162
|
...headers,
|
|
163
|
+
...(bodyBuffer && !hasLength ? { "content-length": String(bodyBuffer.length) } : {}),
|
|
146
164
|
},
|
|
165
|
+
rejectUnauthorized: false, // Required for Gemini API
|
|
166
|
+
timeout: timeoutMs,
|
|
147
167
|
};
|
|
148
168
|
|
|
149
169
|
const req = https.request(options, (res) => {
|
|
170
|
+
if (log) log(`${label}: response ${res.statusCode} ${urlObj.hostname}${urlObj.pathname}`);
|
|
150
171
|
let data = "";
|
|
151
172
|
res.on("data", chunk => data += chunk);
|
|
152
173
|
res.on("end", () => resolve({ status: res.statusCode, headers: res.headers, text: data }));
|
|
174
|
+
res.on("error", (err) => {
|
|
175
|
+
if (log) log(`${label}: response error ${err.message}`);
|
|
176
|
+
reject(err);
|
|
177
|
+
});
|
|
153
178
|
});
|
|
154
179
|
|
|
155
|
-
req.on("
|
|
156
|
-
|
|
180
|
+
req.on("timeout", () => {
|
|
181
|
+
req.destroy(new Error(`${label}: request timeout after ${timeoutMs}ms`));
|
|
182
|
+
});
|
|
183
|
+
req.on("error", (err) => {
|
|
184
|
+
if (log) log(`${label}: request error ${err.message}`);
|
|
185
|
+
reject(err);
|
|
186
|
+
});
|
|
187
|
+
if (bodyBuffer) req.write(bodyBuffer);
|
|
157
188
|
req.end();
|
|
158
189
|
});
|
|
159
190
|
}
|
|
160
191
|
|
|
161
|
-
async function fetchWithRedirects(url, headers, maxRedirects = 10, binary = false) {
|
|
192
|
+
async function fetchWithRedirects(url, headers, maxRedirects = 10, binary = false, opts = {}) {
|
|
162
193
|
let current = url;
|
|
163
194
|
for (let i = 0; i <= maxRedirects; i++) {
|
|
164
|
-
const res = await httpsGet(current, headers, binary);
|
|
195
|
+
const res = await httpsGet(current, headers, { ...opts, binary, label: opts.label || "httpsGet" });
|
|
165
196
|
if (res.status >= 300 && res.status < 400 && res.headers.location) {
|
|
166
197
|
current = new URL(res.headers.location, current).toString();
|
|
167
198
|
continue;
|
|
@@ -175,9 +206,12 @@ async function fetchWithRedirects(url, headers, maxRedirects = 10, binary = fals
|
|
|
175
206
|
// Gemini API Functions
|
|
176
207
|
// ============================================================================
|
|
177
208
|
|
|
178
|
-
async function fetchGeminiAccessToken(cookieMap) {
|
|
209
|
+
async function fetchGeminiAccessToken(cookieMap, opts = {}) {
|
|
179
210
|
const cookieHeader = buildCookieHeader(cookieMap);
|
|
180
|
-
const res = await fetchWithRedirects(GEMINI_APP_URL, { cookie: cookieHeader }
|
|
211
|
+
const res = await fetchWithRedirects(GEMINI_APP_URL, { cookie: cookieHeader }, 10, false, {
|
|
212
|
+
...opts,
|
|
213
|
+
label: opts.label || "geminiAccessToken",
|
|
214
|
+
});
|
|
181
215
|
const html = res.text;
|
|
182
216
|
|
|
183
217
|
const tokens = ["SNlM0e", "thykhd"];
|
|
@@ -220,8 +254,7 @@ function extractGgdlUrls(rawText) {
|
|
|
220
254
|
}
|
|
221
255
|
|
|
222
256
|
function ensureFullSizeImageUrl(url) {
|
|
223
|
-
if (url.includes("=
|
|
224
|
-
if (url.includes("=s")) return url;
|
|
257
|
+
if (url.includes("=s")) return url; // Already has size parameter
|
|
225
258
|
return `${url}=s2048`;
|
|
226
259
|
}
|
|
227
260
|
|
|
@@ -314,7 +347,7 @@ function parseGeminiStreamGenerateResponse(rawText) {
|
|
|
314
347
|
// File Upload
|
|
315
348
|
// ============================================================================
|
|
316
349
|
|
|
317
|
-
async function uploadGeminiFile(filePath) {
|
|
350
|
+
async function uploadGeminiFile(filePath, opts = {}) {
|
|
318
351
|
const absPath = path.resolve(process.cwd(), filePath);
|
|
319
352
|
const data = fs.readFileSync(absPath);
|
|
320
353
|
const fileName = path.basename(absPath);
|
|
@@ -333,7 +366,7 @@ async function uploadGeminiFile(filePath) {
|
|
|
333
366
|
const res = await httpsPost(GEMINI_UPLOAD_URL, {
|
|
334
367
|
"content-type": `multipart/form-data; boundary=${boundary}`,
|
|
335
368
|
"push-id": GEMINI_UPLOAD_PUSH_ID,
|
|
336
|
-
}, body);
|
|
369
|
+
}, body, { ...opts, label: opts.label || "geminiUpload" });
|
|
337
370
|
|
|
338
371
|
if (res.status < 200 || res.status >= 300) {
|
|
339
372
|
throw new Error(`File upload failed: ${res.status} (${res.text.slice(0, 200)})`);
|
|
@@ -346,12 +379,15 @@ async function uploadGeminiFile(filePath) {
|
|
|
346
379
|
// Image Download
|
|
347
380
|
// ============================================================================
|
|
348
381
|
|
|
349
|
-
async function downloadGeminiImage(url, cookieMap, outputPath) {
|
|
382
|
+
async function downloadGeminiImage(url, cookieMap, outputPath, opts = {}) {
|
|
350
383
|
const cookieHeader = buildCookieHeader(cookieMap);
|
|
351
384
|
const fullUrl = ensureFullSizeImageUrl(url);
|
|
352
385
|
|
|
353
386
|
// Use binary mode for image download
|
|
354
|
-
const res = await fetchWithRedirects(fullUrl, { cookie: cookieHeader }, 10, true
|
|
387
|
+
const res = await fetchWithRedirects(fullUrl, { cookie: cookieHeader }, 10, true, {
|
|
388
|
+
...opts,
|
|
389
|
+
label: opts.label || "geminiImageDownload",
|
|
390
|
+
});
|
|
355
391
|
|
|
356
392
|
if (res.status < 200 || res.status >= 300) {
|
|
357
393
|
throw new Error(`Failed to download image: ${res.status}`);
|
|
@@ -366,18 +402,18 @@ async function downloadGeminiImage(url, cookieMap, outputPath) {
|
|
|
366
402
|
fs.writeFileSync(outputPath, res.buffer);
|
|
367
403
|
}
|
|
368
404
|
|
|
369
|
-
async function saveFirstGeminiImage(output, cookieMap, outputPath) {
|
|
405
|
+
async function saveFirstGeminiImage(output, cookieMap, outputPath, opts = {}) {
|
|
370
406
|
// Try generated or web images first
|
|
371
407
|
const genOrWeb = output.images.find(img => img.kind === "generated") ?? output.images[0];
|
|
372
408
|
if (genOrWeb?.url) {
|
|
373
|
-
await downloadGeminiImage(genOrWeb.url, cookieMap, outputPath);
|
|
409
|
+
await downloadGeminiImage(genOrWeb.url, cookieMap, outputPath, opts);
|
|
374
410
|
return { saved: true, imageCount: output.images.length };
|
|
375
411
|
}
|
|
376
412
|
|
|
377
413
|
// Fall back to gg-dl URLs in raw response
|
|
378
414
|
const ggdl = extractGgdlUrls(output.rawResponseText);
|
|
379
415
|
if (ggdl[0]) {
|
|
380
|
-
await downloadGeminiImage(ggdl[0], cookieMap, outputPath);
|
|
416
|
+
await downloadGeminiImage(ggdl[0], cookieMap, outputPath, opts);
|
|
381
417
|
return { saved: true, imageCount: ggdl.length };
|
|
382
418
|
}
|
|
383
419
|
|
|
@@ -398,16 +434,16 @@ function buildGeminiFReqPayload(prompt, uploaded, chatMetadata) {
|
|
|
398
434
|
}
|
|
399
435
|
|
|
400
436
|
async function runGeminiWebOnce(input) {
|
|
401
|
-
const { prompt, files, model, cookieMap, chatMetadata } = input;
|
|
437
|
+
const { prompt, files, model, cookieMap, chatMetadata, timeoutMs = 30000, log = null } = input;
|
|
402
438
|
const cookieHeader = buildCookieHeader(cookieMap);
|
|
403
439
|
|
|
404
440
|
// 1. Get access token
|
|
405
|
-
const at = await fetchGeminiAccessToken(cookieMap);
|
|
441
|
+
const at = await fetchGeminiAccessToken(cookieMap, { timeoutMs, log, label: "geminiAccessToken" });
|
|
406
442
|
|
|
407
443
|
// 2. Upload files
|
|
408
444
|
const uploaded = [];
|
|
409
445
|
for (const file of files ?? []) {
|
|
410
|
-
uploaded.push(await uploadGeminiFile(file));
|
|
446
|
+
uploaded.push(await uploadGeminiFile(file, { timeoutMs, log, label: "geminiUpload" }));
|
|
411
447
|
}
|
|
412
448
|
|
|
413
449
|
// 3. Build request
|
|
@@ -419,12 +455,13 @@ async function runGeminiWebOnce(input) {
|
|
|
419
455
|
// 4. Send request
|
|
420
456
|
const res = await httpsPost(GEMINI_STREAM_GENERATE_URL, {
|
|
421
457
|
"content-type": "application/x-www-form-urlencoded;charset=utf-8",
|
|
458
|
+
"host": "gemini.google.com",
|
|
422
459
|
"origin": "https://gemini.google.com",
|
|
423
460
|
"referer": "https://gemini.google.com/",
|
|
424
461
|
"x-same-domain": "1",
|
|
425
462
|
"cookie": cookieHeader,
|
|
426
463
|
[MODEL_HEADER_NAME]: MODEL_HEADERS[model] || MODEL_HEADERS["gemini-3-pro"],
|
|
427
|
-
}, params.toString());
|
|
464
|
+
}, params.toString(), { timeoutMs, log, label: "geminiStreamGenerate" });
|
|
428
465
|
|
|
429
466
|
const rawResponseText = res.text;
|
|
430
467
|
|
|
@@ -496,8 +533,8 @@ async function query(options) {
|
|
|
496
533
|
output,
|
|
497
534
|
youtube,
|
|
498
535
|
aspectRatio,
|
|
499
|
-
timeout = 300000,
|
|
500
536
|
getCookies,
|
|
537
|
+
timeout = 300000,
|
|
501
538
|
log = () => {},
|
|
502
539
|
} = options;
|
|
503
540
|
|
|
@@ -550,6 +587,8 @@ async function query(options) {
|
|
|
550
587
|
model: resolvedModel,
|
|
551
588
|
cookieMap,
|
|
552
589
|
chatMetadata: null,
|
|
590
|
+
timeoutMs: timeout,
|
|
591
|
+
log,
|
|
553
592
|
});
|
|
554
593
|
|
|
555
594
|
log("Sending edit request...");
|
|
@@ -560,13 +599,15 @@ async function query(options) {
|
|
|
560
599
|
model: resolvedModel,
|
|
561
600
|
cookieMap,
|
|
562
601
|
chatMetadata: intro.metadata,
|
|
602
|
+
timeoutMs: timeout,
|
|
603
|
+
log,
|
|
563
604
|
});
|
|
564
605
|
|
|
565
606
|
response = out;
|
|
566
607
|
|
|
567
608
|
// Save output image
|
|
568
609
|
const outputPath = output || generateImage || "edited.png";
|
|
569
|
-
const imageSave = await saveFirstGeminiImage(out, cookieMap, outputPath);
|
|
610
|
+
const imageSave = await saveFirstGeminiImage(out, cookieMap, outputPath, { timeoutMs: timeout, log });
|
|
570
611
|
if (!imageSave.saved) {
|
|
571
612
|
throw new Error(`No images generated. Response: ${out.text?.slice(0, 200) || "(empty)"}`);
|
|
572
613
|
}
|
|
@@ -581,12 +622,14 @@ async function query(options) {
|
|
|
581
622
|
model: resolvedModel,
|
|
582
623
|
cookieMap,
|
|
583
624
|
chatMetadata: null,
|
|
625
|
+
timeoutMs: timeout,
|
|
626
|
+
log,
|
|
584
627
|
});
|
|
585
628
|
|
|
586
629
|
response = out;
|
|
587
630
|
|
|
588
631
|
// Save output image
|
|
589
|
-
const imageSave = await saveFirstGeminiImage(out, cookieMap, generateImage);
|
|
632
|
+
const imageSave = await saveFirstGeminiImage(out, cookieMap, generateImage, { timeoutMs: timeout, log });
|
|
590
633
|
if (!imageSave.saved) {
|
|
591
634
|
throw new Error(`No images generated. Response: ${out.text?.slice(0, 200) || "(empty)"}`);
|
|
592
635
|
}
|
|
@@ -601,6 +644,8 @@ async function query(options) {
|
|
|
601
644
|
model: resolvedModel,
|
|
602
645
|
cookieMap,
|
|
603
646
|
chatMetadata: null,
|
|
647
|
+
timeoutMs: timeout,
|
|
648
|
+
log,
|
|
604
649
|
});
|
|
605
650
|
|
|
606
651
|
response = out;
|