gemini-reverse 1.0.4 → 1.0.6
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/client.js +27 -3
- package/components/gemMixin.js +2 -1
- package/index.d.ts +0 -1
- package/package.json +1 -1
- package/types/candidate.js +16 -2
- package/types/image.js +116 -29
- package/types/video.js +4 -0
- package/utils/upload.js +17 -6
package/client.js
CHANGED
|
@@ -341,13 +341,25 @@ class GeminiClient {
|
|
|
341
341
|
temporary = false,
|
|
342
342
|
deep_research = false,
|
|
343
343
|
} = {}) {
|
|
344
|
+
if (!this._running) {
|
|
345
|
+
await this.init({
|
|
346
|
+
timeout: this.timeout,
|
|
347
|
+
autoClose: this.autoClose,
|
|
348
|
+
closeDelay: this.closeDelay,
|
|
349
|
+
autoRefresh: this.autoRefresh,
|
|
350
|
+
refreshInterval: this.refreshInterval,
|
|
351
|
+
verbose: this.verbose,
|
|
352
|
+
watchdogTimeout: this.watchdogTimeout,
|
|
353
|
+
});
|
|
354
|
+
if (!this._running) throw new APIError(`Invalid function call: GeminiClient.generateContent. Client initialization failed.`);
|
|
355
|
+
}
|
|
344
356
|
if (this.autoClose) this._resetCloseTask();
|
|
345
357
|
|
|
346
358
|
let fileData = null;
|
|
347
359
|
if (files && files.length) {
|
|
348
360
|
await this._sendBardActivity();
|
|
349
361
|
const uploaded = await Promise.all(
|
|
350
|
-
files.map(f => uploadFile(f, this.proxy, this.pushId)),
|
|
362
|
+
files.map(f => uploadFile(f, this.proxy, this.pushId, this.cookies)),
|
|
351
363
|
);
|
|
352
364
|
fileData = uploaded.map((url, i) => [[url], parseFileName(files[i])]);
|
|
353
365
|
}
|
|
@@ -375,13 +387,25 @@ class GeminiClient {
|
|
|
375
387
|
temporary = false,
|
|
376
388
|
deep_research = false,
|
|
377
389
|
} = {}) {
|
|
390
|
+
if (!this._running) {
|
|
391
|
+
await this.init({
|
|
392
|
+
timeout: this.timeout,
|
|
393
|
+
autoClose: this.autoClose,
|
|
394
|
+
closeDelay: this.closeDelay,
|
|
395
|
+
autoRefresh: this.autoRefresh,
|
|
396
|
+
refreshInterval: this.refreshInterval,
|
|
397
|
+
verbose: this.verbose,
|
|
398
|
+
watchdogTimeout: this.watchdogTimeout,
|
|
399
|
+
});
|
|
400
|
+
if (!this._running) throw new APIError(`Invalid function call: GeminiClient.generateContentStream. Client initialization failed.`);
|
|
401
|
+
}
|
|
378
402
|
if (this.autoClose) this._resetCloseTask();
|
|
379
403
|
|
|
380
404
|
let fileData = null;
|
|
381
405
|
if (files && files.length) {
|
|
382
406
|
await this._sendBardActivity();
|
|
383
407
|
const uploaded = await Promise.all(
|
|
384
|
-
files.map(f => uploadFile(f, this.proxy, this.pushId)),
|
|
408
|
+
files.map(f => uploadFile(f, this.proxy, this.pushId, this.cookies)),
|
|
385
409
|
);
|
|
386
410
|
fileData = uploaded.map((url, i) => [[url], parseFileName(files[i])]);
|
|
387
411
|
}
|
|
@@ -536,7 +560,7 @@ class GeminiClient {
|
|
|
536
560
|
case ErrorCode.TEMPORARY_ERROR_1013:
|
|
537
561
|
throw new APIError('Temporary error (1013). Retrying...');
|
|
538
562
|
default:
|
|
539
|
-
throw new APIError(`Unknown API error code: ${ec}.`);
|
|
563
|
+
throw new APIError(`Failed to generate contents. Unknown API error code: ${ec}. This might be a content policy rejection or a temporary Google service issue.`);
|
|
540
564
|
}
|
|
541
565
|
}
|
|
542
566
|
|
package/components/gemMixin.js
CHANGED
|
@@ -16,7 +16,8 @@ class GemMixin {
|
|
|
16
16
|
return this._gems;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
async fetchGems({ includeHidden = false
|
|
19
|
+
async fetchGems({ includeHidden = false } = {}) {
|
|
20
|
+
const language = this.language || 'en';
|
|
20
21
|
const response = await this._batchExecute([
|
|
21
22
|
new RPCData({ rpcid: GRPC.LIST_GEMS, payload: includeHidden ? `[4,['${language}'],0]` : `[3,['${language}'],0]`, identifier: 'system' }),
|
|
22
23
|
new RPCData({ rpcid: GRPC.LIST_GEMS, payload: `[2,['${language}'],0]`, identifier: 'custom' }),
|
package/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gemini-reverse",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Unofficial Node.js client for gemini.google.com — inspired by Gemini-API (Python). Supports streaming, chat sessions, gems, file uploads, and TypeScript.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
package/types/candidate.js
CHANGED
|
@@ -5,12 +5,26 @@ const { WebImage, GeneratedImage } = require('./image');
|
|
|
5
5
|
function decodeHtml(str) {
|
|
6
6
|
if (!str) return str;
|
|
7
7
|
return str
|
|
8
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(parseInt(n, 10)))
|
|
9
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, h) => String.fromCodePoint(parseInt(h, 16)))
|
|
8
10
|
.replace(/&/g, '&')
|
|
9
11
|
.replace(/</g, '<')
|
|
10
12
|
.replace(/>/g, '>')
|
|
11
13
|
.replace(/"/g, '"')
|
|
12
|
-
.replace(
|
|
13
|
-
.replace(/&
|
|
14
|
+
.replace(/'/g, "'")
|
|
15
|
+
.replace(/ /g, ' ')
|
|
16
|
+
.replace(/©/g, '©')
|
|
17
|
+
.replace(/®/g, '®')
|
|
18
|
+
.replace(/™/g, '™')
|
|
19
|
+
.replace(/—/g, '—')
|
|
20
|
+
.replace(/–/g, '–')
|
|
21
|
+
.replace(/…/g, '…')
|
|
22
|
+
.replace(/«/g, '«')
|
|
23
|
+
.replace(/»/g, '»')
|
|
24
|
+
.replace(/“/g, '“')
|
|
25
|
+
.replace(/”/g, '”')
|
|
26
|
+
.replace(/‘/g, '‘')
|
|
27
|
+
.replace(/’/g, '’');
|
|
14
28
|
}
|
|
15
29
|
|
|
16
30
|
class Candidate {
|
package/types/image.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const axios = require('axios');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
6
7
|
|
|
7
8
|
class Image {
|
|
8
9
|
constructor({ url, title = '[Image]', alt = '', proxy = null } = {}) {
|
|
@@ -12,43 +13,76 @@ class Image {
|
|
|
12
13
|
this.proxy = proxy;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
_getUrlForHash() {
|
|
17
|
+
return this.url;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
toString() {
|
|
16
21
|
const short = this.url.length <= 20 ? this.url : this.url.slice(0, 8) + '...' + this.url.slice(-12);
|
|
17
22
|
return `Image(title='${this.title}', alt='${this.alt}', url='${short}')`;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
async save({ path: savePath = 'temp', filename = null,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (verbose) console.warn(`Invalid filename: ${fname}`);
|
|
27
|
-
if (skipInvalidFilename) return null;
|
|
25
|
+
async save({ path: savePath = 'temp', filename = null, verbose = false } = {}) {
|
|
26
|
+
if (!filename || !path.extname(filename)) {
|
|
27
|
+
const timestamp = new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14);
|
|
28
|
+
const urlHash = crypto.createHash('sha256').update(this._getUrlForHash()).digest('hex').slice(0, 10);
|
|
29
|
+
const baseName = filename ? path.parse(filename).name : 'image';
|
|
30
|
+
filename = `${timestamp}_${urlHash}_${baseName}`;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
fs.mkdirSync(savePath, { recursive: true });
|
|
34
|
+
return await this._performSave(savePath, filename, verbose);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async _performSave(savePath, filename, verbose) {
|
|
38
|
+
const imgUrl = this.url;
|
|
39
|
+
const proxyConfig = this._parseProxy(this.proxy);
|
|
40
|
+
const clientRef = this.client_ref || null;
|
|
41
|
+
const cookies = clientRef ? clientRef.cookies : null;
|
|
31
42
|
|
|
32
|
-
const res = await axios.get(
|
|
43
|
+
const res = await axios.get(imgUrl, {
|
|
33
44
|
responseType: 'arraybuffer',
|
|
34
|
-
headers:
|
|
45
|
+
headers: {
|
|
46
|
+
'Origin': 'https://gemini.google.com',
|
|
47
|
+
'Referer': 'https://gemini.google.com/',
|
|
48
|
+
...(cookies ? { 'Cookie': Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ') } : {}),
|
|
49
|
+
},
|
|
35
50
|
maxRedirects: 5,
|
|
36
51
|
...(proxyConfig ? { proxy: proxyConfig } : {}),
|
|
37
52
|
});
|
|
38
53
|
|
|
39
|
-
if (
|
|
54
|
+
if (verbose) console.debug(`HTTP Request: GET ${imgUrl} [${res.status}]`);
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
console.warn(`Content type of ${fname} is not image, but ${contentType}.`);
|
|
56
|
+
if (res.status !== 200) {
|
|
57
|
+
throw new Error(`Error downloading image: ${res.status}`);
|
|
44
58
|
}
|
|
45
59
|
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
const contentType = (res.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
|
|
61
|
+
let ext = path.extname(filename);
|
|
62
|
+
if (!ext) {
|
|
63
|
+
if (contentType.includes('jpeg') || contentType.includes('jpg')) ext = '.jpg';
|
|
64
|
+
else if (contentType.includes('png')) ext = '.png';
|
|
65
|
+
else if (contentType.includes('webp')) ext = '.webp';
|
|
66
|
+
else if (contentType.includes('gif')) ext = '.gif';
|
|
67
|
+
else ext = '.png';
|
|
68
|
+
filename = filename + ext;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const dest = path.join(savePath, filename);
|
|
48
72
|
fs.writeFileSync(dest, Buffer.from(res.data));
|
|
49
|
-
if (verbose) console.
|
|
73
|
+
if (verbose) console.info(`Image saved as ${path.resolve(dest)}`);
|
|
50
74
|
return path.resolve(dest);
|
|
51
75
|
}
|
|
76
|
+
|
|
77
|
+
_parseProxy(proxyStr) {
|
|
78
|
+
if (!proxyStr) return undefined;
|
|
79
|
+
try {
|
|
80
|
+
const u = new URL(proxyStr);
|
|
81
|
+
return { protocol: u.protocol.replace(':', ''), host: u.hostname, port: parseInt(u.port) };
|
|
82
|
+
} catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
52
86
|
}
|
|
53
87
|
|
|
54
88
|
class WebImage extends Image {
|
|
@@ -56,19 +90,72 @@ class WebImage extends Image {
|
|
|
56
90
|
}
|
|
57
91
|
|
|
58
92
|
class GeneratedImage extends Image {
|
|
59
|
-
constructor({
|
|
60
|
-
super(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
this.
|
|
93
|
+
constructor({ url, title = '[Image]', alt = '', proxy = null, client_ref = null, cid = '', rid = '', rcid = '', image_id = '' } = {}) {
|
|
94
|
+
super({ url, title, alt, proxy });
|
|
95
|
+
this.client_ref = client_ref;
|
|
96
|
+
this.cid = cid;
|
|
97
|
+
this.rid = rid;
|
|
98
|
+
this.rcid = rcid;
|
|
99
|
+
this.image_id = image_id;
|
|
64
100
|
}
|
|
65
101
|
|
|
66
|
-
async
|
|
67
|
-
if (fullSize)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
102
|
+
async _performSave(savePath, filename, verbose, fullSize = true) {
|
|
103
|
+
if (fullSize) {
|
|
104
|
+
if (this.client_ref && this.cid && this.rid && this.rcid && this.image_id) {
|
|
105
|
+
try {
|
|
106
|
+
const originalUrl = await this.client_ref._getFullSizeImage(
|
|
107
|
+
this.cid, this.rid, this.rcid, this.image_id,
|
|
108
|
+
);
|
|
109
|
+
if (originalUrl) {
|
|
110
|
+
const proxyConfig = this._parseProxy(this.proxy);
|
|
111
|
+
const cookies = this.client_ref ? this.client_ref.cookies : null;
|
|
112
|
+
const headers = {
|
|
113
|
+
'Origin': 'https://gemini.google.com',
|
|
114
|
+
'Referer': 'https://gemini.google.com/',
|
|
115
|
+
...(cookies ? { 'Cookie': Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ') } : {}),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const r1 = await axios.get(`${originalUrl}=d-I?alr=yes`, {
|
|
119
|
+
headers, maxRedirects: 5, ...(proxyConfig ? { proxy: proxyConfig } : {}),
|
|
120
|
+
});
|
|
121
|
+
const r2 = await axios.get(r1.data.trim(), {
|
|
122
|
+
headers, maxRedirects: 5, ...(proxyConfig ? { proxy: proxyConfig } : {}),
|
|
123
|
+
});
|
|
124
|
+
this.url = r2.data.trim();
|
|
125
|
+
return await super._performSave(savePath, filename, verbose);
|
|
126
|
+
}
|
|
127
|
+
} catch (e) {
|
|
128
|
+
if (verbose) console.debug(`Failed to fetch full size image via RPC: ${e.message}, falling back.`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (this.url.includes('=s1024-rj')) {
|
|
133
|
+
this.url = this.url.replace('=s1024-rj', '=s2048-rj');
|
|
134
|
+
} else if (!this.url.includes('=s2048-rj')) {
|
|
135
|
+
this.url += '=s2048-rj';
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
if (this.url.includes('=s2048-rj')) {
|
|
139
|
+
this.url = this.url.replace('=s2048-rj', '=s1024-rj');
|
|
140
|
+
} else if (!this.url.includes('=s1024-rj')) {
|
|
141
|
+
this.url += '=s1024-rj';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return await super._performSave(savePath, filename, verbose);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async save({ path: savePath = 'temp', filename = null, verbose = false, fullSize = true } = {}) {
|
|
149
|
+
if (!filename || !path.extname(filename)) {
|
|
150
|
+
const timestamp = new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14);
|
|
151
|
+
const urlHash = crypto.createHash('sha256').update(this._getUrlForHash()).digest('hex').slice(0, 10);
|
|
152
|
+
const baseName = filename ? path.parse(filename).name : 'image';
|
|
153
|
+
filename = `${timestamp}_${urlHash}_${baseName}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fs.mkdirSync(savePath, { recursive: true });
|
|
157
|
+
return await this._performSave(savePath, filename, verbose, fullSize);
|
|
71
158
|
}
|
|
72
159
|
}
|
|
73
160
|
|
|
74
|
-
module.exports = { Image, WebImage, GeneratedImage };
|
|
161
|
+
module.exports = { Image, WebImage, GeneratedImage };
|
package/types/video.js
CHANGED
|
@@ -45,6 +45,10 @@ class Video {
|
|
|
45
45
|
const proxyConfig = this.proxy ? this._parseProxy(this.proxy) : undefined;
|
|
46
46
|
const res = await axios.get(url, {
|
|
47
47
|
responseType: 'arraybuffer',
|
|
48
|
+
headers: {
|
|
49
|
+
'Origin': 'https://gemini.google.com',
|
|
50
|
+
'Referer': 'https://gemini.google.com/',
|
|
51
|
+
},
|
|
48
52
|
...(proxyConfig ? { proxy: proxyConfig } : {}),
|
|
49
53
|
validateStatus: null,
|
|
50
54
|
});
|
package/utils/upload.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const axios = require('axios');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const mime = require('mime-types');
|
|
6
7
|
const FormData = require('form-data');
|
|
7
8
|
const { Endpoint, Headers } = require('../constants');
|
|
8
9
|
const { parseProxy } = require('./accessToken');
|
|
@@ -21,26 +22,36 @@ function parseFileName(file) {
|
|
|
21
22
|
return generateRandomName();
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
async function uploadFile(file, proxy = null,
|
|
25
|
+
async function uploadFile(file, proxy = null, pushId = '', cookies = {}) {
|
|
25
26
|
let content, fname;
|
|
26
27
|
|
|
27
28
|
if (typeof file === 'string') {
|
|
28
29
|
const fp = path.resolve(file);
|
|
29
30
|
if (!fs.existsSync(fp)) throw new Error(`${fp} is not a valid file.`);
|
|
30
|
-
fname =
|
|
31
|
+
fname = path.basename(fp);
|
|
31
32
|
content = fs.readFileSync(fp);
|
|
32
33
|
} else if (Buffer.isBuffer(file)) {
|
|
33
34
|
content = file;
|
|
34
|
-
fname =
|
|
35
|
+
fname = generateRandomName();
|
|
35
36
|
} else {
|
|
36
37
|
throw new Error(`Unsupported file type: ${typeof file}`);
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
const contentType = mime.lookup(fname) || 'application/octet-stream';
|
|
41
|
+
|
|
39
42
|
const form = new FormData();
|
|
40
|
-
form.append('file', content, { filename: fname });
|
|
43
|
+
form.append('file', content, { filename: fname, contentType });
|
|
44
|
+
|
|
45
|
+
const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
41
46
|
|
|
42
47
|
const res = await axios.post(Endpoint.UPLOAD, form, {
|
|
43
|
-
headers: {
|
|
48
|
+
headers: {
|
|
49
|
+
...Headers.REFERER,
|
|
50
|
+
...Headers.UPLOAD,
|
|
51
|
+
...form.getHeaders(),
|
|
52
|
+
'Push-ID': pushId,
|
|
53
|
+
...(cookieStr ? { 'Cookie': cookieStr } : {}),
|
|
54
|
+
},
|
|
44
55
|
maxRedirects: 5,
|
|
45
56
|
...(proxy ? { proxy: parseProxy(proxy) } : {}),
|
|
46
57
|
});
|
|
@@ -48,4 +59,4 @@ async function uploadFile(file, proxy = null, filename = null) {
|
|
|
48
59
|
return res.data;
|
|
49
60
|
}
|
|
50
61
|
|
|
51
|
-
module.exports = { uploadFile, parseFileName, generateRandomName };
|
|
62
|
+
module.exports = { uploadFile, parseFileName, generateRandomName };
|