@8-/gemini-web-api 1.0.0 → 1.0.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/index.js CHANGED
@@ -1,11 +1,5 @@
1
1
  #!/usr/bin/env -S bun
2
- import { spawnSync } from "node:child_process";
3
- import { existsSync } from "node:fs";
4
- import { createServer } from "node:http";
5
- import { homedir } from "node:os";
6
2
  import { join } from "node:path";
7
- import { DatabaseSync } from "node:sqlite";
8
- import { Readable } from "node:stream";
9
3
 
10
4
  try {
11
5
  process.loadEnvFile(join(import.meta.dirname, ".env"));
@@ -13,876 +7,68 @@ try {
13
7
  if (err.code !== "ENOENT") throw err;
14
8
  }
15
9
 
16
- export const HOST = process.env.HOST ?? "0.0.0.0",
17
- PORT = parseInt(process.env.PORT ?? "7860", 10),
18
- API_KEY = process.env.API_KEY ?? "",
19
- ENABLE_THINKING = process.env.ENABLE_THINKING === "true",
20
- INIT_URL = "https://gemini.google.com/app",
21
- GEN_URL =
22
- "https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate",
23
- BATCH_URL = "https://gemini.google.com/_/BardChatUi/data/batchexecute",
24
- USER_AGENT =
25
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
26
- SALT = new TextEncoder().encode("saltysalt"),
27
- IV = new Uint8Array(16).fill(32),
28
- CORS_HEADERS = {
29
- "Access-Control-Allow-Origin": "*",
30
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
31
- "Access-Control-Allow-Headers": "*",
32
- },
33
- STATUS_OK = 200,
34
- STATUS_NO_CONTENT = 204,
35
- STATUS_BAD_REQUEST = 400,
36
- STATUS_UNAUTHORIZED = 401,
37
- STATUS_NOT_FOUND = 404,
38
- STATUS_SERVER_ERR = 500,
39
- ERR_MISSING_URL = 4001,
40
- ERR_UNAUTHORIZED = 4011,
41
- ERR_NOT_FOUND = 4041,
42
- THINKING_LEVEL_DISABLED = 1,
43
- THINKING_LEVEL_ENABLED = 2,
44
- ROLE_ASSISTANT = "assistant",
45
- STOP_REASON_STOP = "stop",
46
- PAYLOAD_INNER_REQ_IDX = 0,
47
- PAYLOAD_MODEL_HEADER_IDX = 1,
48
- PAYLOAD_UUID_IDX = 2,
49
- BROWSER_PATH_LI = [
50
- {
51
- db_path: join(homedir(), "Library/Application Support/Google/Chrome/Default/Cookies"),
52
- account: "Chrome",
53
- service: "Chrome Safe Storage",
54
- },
55
- {
56
- db_path: join(homedir(), "Library/Application Support/Google/Chrome/Profile 1/Cookies"),
57
- account: "Chrome",
58
- service: "Chrome Safe Storage",
59
- },
60
- {
61
- db_path: join(
62
- homedir(),
63
- "Library/Application Support/BraveSoftware/Brave-Browser/Default/Cookies",
64
- ),
65
- account: "Brave",
66
- service: "Brave Safe Storage",
67
- },
68
- {
69
- db_path: join(homedir(), "Library/Application Support/Microsoft Edge/Default/Cookies"),
70
- account: "Microsoft Edge",
71
- service: "Microsoft Edge Safe Storage",
72
- },
73
- ],
74
- DEFAULT_MODEL_LI = [
75
- {
76
- id: "gemini-flash",
77
- model_id: "56fdd199312815e2",
78
- disp: "Flash",
79
- cat: "Flash",
80
- desc: "Default Flash",
81
- num: 1,
82
- capacity: 1,
83
- alias_li: ["gemini-flash", "flash", "default"],
84
- },
85
- {
86
- id: "gemini-pro",
87
- model_id: "797f3d0293f288ad",
88
- disp: "Pro",
89
- cat: "Pro",
90
- desc: "Default Pro",
91
- num: 3,
92
- capacity: 1,
93
- alias_li: ["gemini-pro", "pro"],
94
- },
95
- ];
96
-
97
- export let session_state = {
98
- cookie_header: "",
99
- at: "",
100
- bl: "",
101
- fsid: "",
102
- req_id: Math.floor(Math.random() * 90000) + 10000,
103
- model_li: [],
104
- default_model: null,
105
- };
106
-
107
- export const keychainPwdRead = (account, service) => {
108
- const proc = spawnSync(
109
- "/usr/bin/security",
110
- ["-q", "find-generic-password", "-w", "-a", account, "-s", service],
111
- { encoding: "utf8" },
112
- );
113
- return (proc.stdout ?? "").trim();
114
- },
115
-
116
- valDecrypt = async (enc_data, aes_key, has_integrity) => {
117
- const data = enc_data.slice(3),
118
- dec = await crypto.subtle.decrypt({ name: "AES-CBC", iv: IV }, aes_key, data);
119
- let byte_li = new Uint8Array(dec);
120
- if (has_integrity) byte_li = byte_li.slice(32);
121
- return new TextDecoder().decode(byte_li);
122
- },
123
-
124
- cookieRead = async () => {
125
- if (process.env.SECURE_1PSID && process.env.SECURE_1PSIDTS) {
126
- return (
127
- "__Secure-1PSID=" +
128
- process.env.SECURE_1PSID +
129
- "; __Secure-1PSIDTS=" +
130
- process.env.SECURE_1PSIDTS
131
- );
132
- }
133
- const target = BROWSER_PATH_LI.find((browser_cfg) => existsSync(browser_cfg.db_path));
134
- if (!target) return "";
135
- const pwd = keychainPwdRead(target.account, target.service),
136
- key_mat = await crypto.subtle.importKey("raw", new TextEncoder().encode(pwd), "PBKDF2", false, [
137
- "deriveKey",
138
- ]),
139
- aes_key = await crypto.subtle.deriveKey(
140
- { name: "PBKDF2", salt: SALT, iterations: 1003, hash: "SHA-1" },
141
- key_mat,
142
- { name: "AES-CBC", length: 128 },
143
- false,
144
- ["decrypt"],
145
- ),
146
- db = new DatabaseSync(target.db_path, { readOnly: true }),
147
- version_row = db.prepare("SELECT value FROM meta WHERE key = 'version'").get(),
148
- has_integrity = parseInt(version_row?.value ?? "0", 10) >= 24,
149
- row_li = db
150
- .prepare(
151
- "SELECT name, encrypted_value FROM cookies WHERE host_key LIKE '%google.com%' AND name IN ('__Secure-1PSID', '__Secure-1PSIDTS')",
152
- )
153
- .all(),
154
- cookie_map = {};
155
-
156
- for (const { name, encrypted_value } of row_li) {
157
- if (encrypted_value && encrypted_value.length > 3) {
158
- const val = await valDecrypt(encrypted_value, aes_key, has_integrity);
159
- cookie_map[name] = val;
160
- }
161
- }
162
- db.close();
163
- return Object.entries(cookie_map)
164
- .map(([cookie_name, cookie_val]) => cookie_name + "=" + cookie_val)
165
- .join("; ");
166
- },
167
-
168
- versionCompare = (a_ver, b_ver) => {
169
- const a_match = (a_ver ?? "").match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/),
170
- b_match = (b_ver ?? "").match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/),
171
- a_maj = a_match ? parseInt(a_match[1], 10) : 0,
172
- a_min = a_match && a_match[2] ? parseInt(a_match[2], 10) : 0,
173
- a_pat = a_match && a_match[3] ? parseInt(a_match[3], 10) : 0,
174
- b_maj = b_match ? parseInt(b_match[1], 10) : 0,
175
- b_min = b_match && b_match[2] ? parseInt(b_match[2], 10) : 0,
176
- b_pat = b_match && b_match[3] ? parseInt(b_match[3], 10) : 0;
177
- if (a_maj !== b_maj) return a_maj - b_maj;
178
- if (a_min !== b_min) return a_min - b_min;
179
- return a_pat - b_pat;
180
- },
181
-
182
- modelFetch = async (at, bl, fsid, cookie_header) => {
183
- session_state.req_id += 1;
184
- const session_id = crypto.randomUUID().toUpperCase(),
185
- batch_headers = {
186
- "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
187
- Origin: "https://gemini.google.com",
188
- Referer: "https://gemini.google.com/",
189
- "X-Same-Domain": "1",
190
- "User-Agent": USER_AGENT,
191
- Cookie: cookie_header,
192
- "x-goog-ext-525001261-jspb": JSON.stringify([
193
- 1,
194
- null,
195
- null,
196
- null,
197
- null,
198
- null,
199
- null,
200
- null,
201
- [4, 5, 6, 8],
202
- null,
203
- null,
204
- null,
205
- null,
206
- null,
207
- null,
208
- null,
209
- session_id,
210
- ]),
211
- "x-goog-ext-73010989-jspb": "[0]",
212
- },
213
- payload = [[["otAQ7b", "[]", null, "generic"]]],
214
- form_data = new URLSearchParams(),
215
- batch_url = new URL(BATCH_URL),
216
- discovered_li = [];
217
-
218
- form_data.set("at", at);
219
- form_data.set("f.req", JSON.stringify(payload));
220
-
221
- batch_url.searchParams.set("rpcids", "otAQ7b");
222
- batch_url.searchParams.set("hl", "en");
223
- batch_url.searchParams.set("_reqid", String(session_state.req_id));
224
- batch_url.searchParams.set("rt", "c");
225
- batch_url.searchParams.set("source-path", "/app");
226
- if (bl) batch_url.searchParams.set("bl", bl);
227
- if (fsid) batch_url.searchParams.set("f.sid", fsid);
228
-
229
- const res = await fetch(batch_url.toString(), {
230
- method: "POST",
231
- headers: batch_headers,
232
- body: form_data.toString(),
233
- });
234
-
235
- const res_text = await res.text();
236
- let clean_text = res_text;
237
- if (clean_text.startsWith(")]}\x27")) clean_text = clean_text.slice(4).trimStart();
238
- const match_res = clean_text.match(/^(\d+)\n/);
239
- if (match_res) {
240
- const chunk_len = parseInt(match_res[1], 10),
241
- start_idx = match_res[1].length,
242
- chunk_str = clean_text.slice(start_idx, start_idx + chunk_len).trim();
243
- if (chunk_str.startsWith("[")) {
244
- const part_li = JSON.parse(chunk_str);
245
- for (const part of part_li) {
246
- if (part[1] === "otAQ7b" && part[2]) {
247
- const part_body = JSON.parse(part[2]),
248
- model_li = part_body[15],
249
- tier_flag_li = part_body[16] ?? [],
250
- capability_flag_li = part_body[17] ?? [];
251
- let cap = 1;
252
- if (capability_flag_li.includes(115)) cap = 4;
253
- else if (tier_flag_li.includes(16) || capability_flag_li.includes(106)) cap = 3;
254
- else if (tier_flag_li.includes(8) || capability_flag_li.includes(19)) cap = 2;
255
-
256
- for (const item of model_li ?? []) {
257
- const id = item[0],
258
- cat = String(item[1] ?? item[10] ?? ""),
259
- disp = String(item[11] ?? item[19] ?? item[1] ?? ""),
260
- desc = String(item[12] ?? item[2] ?? ""),
261
- num =
262
- typeof item[17] === "number" ? item[17] : typeof item[9] === "number" ? item[9] : 1,
263
- disp_slug = disp.toLowerCase().replaceAll(/\s+/g, "-"),
264
- cat_slug = cat.toLowerCase().replaceAll(/\s+/g, "-"),
265
- primary_id = "gemini-" + disp_slug,
266
- alias_li = [id.toLowerCase(), primary_id, "gemini-" + cat_slug, cat_slug, disp_slug];
267
- discovered_li.push({
268
- id: primary_id,
269
- model_id: id,
270
- disp,
271
- cat,
272
- desc,
273
- num,
274
- capacity: cap,
275
- alias_li,
276
- });
277
- }
278
- }
279
- }
280
- }
281
- }
282
-
283
- const non_lite_li = discovered_li.filter(
284
- (model_item) =>
285
- !model_item.disp.toLowerCase().includes("lite") &&
286
- !model_item.cat.toLowerCase().includes("lite") &&
287
- !model_item.id.toLowerCase().includes("lite"),
288
- );
289
- non_lite_li.sort((a, b) => versionCompare(b.disp, a.disp));
290
- session_state.default_model = non_lite_li[0] ?? discovered_li[0] ?? DEFAULT_MODEL_LI[0];
291
- console.log(
292
- "默认模型: " + session_state.default_model.disp + " (" + session_state.default_model.id + ")",
293
- );
294
-
295
- return discovered_li;
296
- },
297
-
298
- sessionInit = async () => {
299
- try {
300
- if (!session_state.cookie_header) {
301
- session_state.cookie_header = await cookieRead();
302
- }
303
- const res = await fetch(INIT_URL, {
304
- headers: {
305
- "User-Agent": USER_AGENT,
306
- Cookie: session_state.cookie_header,
307
- },
308
- redirect: "manual",
309
- });
310
-
311
- let html = "";
312
- if (res.status === STATUS_OK) {
313
- html = await res.text();
314
- } else if (res.status >= 300 && res.status < 400) {
315
- const location = res.headers.get("location") ?? "";
316
- if (location.includes("/sorry/")) {
317
- console.warn("Gemini 会话初始化提示: 触发了 Google 验证码重定向 (sorry/index)");
318
- } else if (location) {
319
- const redirect_res = await fetch(location, {
320
- headers: {
321
- "User-Agent": USER_AGENT,
322
- Cookie: session_state.cookie_header,
323
- },
324
- });
325
- if (redirect_res.ok) html = await redirect_res.text();
326
- }
327
- }
328
-
329
- const at_match = html.match(/"SNlM0e":\s*"([^"]+)"/),
330
- bl_match = html.match(/"cfb2h":\s*"([^"]+)"/),
331
- fsid_match = html.match(/"FdrFJe":\s*"([^"]+)"/);
332
- session_state.at = at_match ? at_match[1] : "";
333
- session_state.bl = bl_match ? bl_match[1] : "";
334
- session_state.fsid = fsid_match ? fsid_match[1] : "";
335
- if (session_state.at) {
336
- session_state.model_li = await modelFetch(
337
- session_state.at,
338
- session_state.bl,
339
- session_state.fsid,
340
- session_state.cookie_header,
341
- );
342
- }
343
- } catch (err) {
344
- console.warn("会话初始化异常:", err.message);
345
- }
346
- return session_state;
347
- },
348
-
349
- modelMap = (req_model) => {
350
- const norm = (req_model ?? "").toLowerCase(),
351
- available_li = session_state.model_li.length > 0 ? session_state.model_li : DEFAULT_MODEL_LI,
352
- found = available_li.find(
353
- (model_item) =>
354
- model_item.id === norm ||
355
- model_item.model_id === norm ||
356
- model_item.alias_li.some((alias_item) => norm.includes(alias_item) || alias_item === norm),
357
- );
358
- return found ?? session_state.default_model ?? available_li[0];
359
- },
360
-
361
- conversationFormat = (msg_li) => {
362
- let conversation = "";
363
- for (const { role, content } of msg_li ?? []) {
364
- const text_content = Array.isArray(content)
365
- ? content
366
- .filter((content_item) => content_item.type === "text")
367
- .map((content_item) => content_item.text)
368
- .join("")
369
- : content ?? "";
370
- if (role === "system") {
371
- conversation += "System: " + text_content + "\n\n";
372
- } else if (role === "user") {
373
- conversation += "Human: " + text_content + "\n\n";
374
- } else if (role === "assistant") {
375
- conversation += "Assistant: " + text_content + "\n\n";
376
- }
377
- }
378
- return conversation.trim();
379
- },
380
-
381
- payloadBuild = (prompt, model_info) => {
382
- const session_id = crypto.randomUUID().toUpperCase(),
383
- uuid_val = crypto.randomUUID().toUpperCase(),
384
- thinking_level = ENABLE_THINKING ? THINKING_LEVEL_ENABLED : THINKING_LEVEL_DISABLED,
385
- model_header = [
386
- 1,
387
- null,
388
- null,
389
- null,
390
- model_info.model_id,
391
- null,
392
- null,
393
- 0,
394
- [4, 5, 6, 8],
395
- null,
396
- null,
397
- model_info.capacity ?? 1,
398
- null,
399
- null,
400
- model_info.num,
401
- thinking_level,
402
- session_id,
403
- ],
404
- inner_req = Array.from({ length: 81 }, () => null);
405
- inner_req[0] = [prompt, 0, null, null, null, null, 0];
406
- inner_req[1] = ["en"];
407
- inner_req[2] = ["", "", "", null, null, null, null, null, null, ""];
408
- inner_req[6] = [1];
409
- inner_req[7] = 1;
410
- inner_req[10] = 1;
411
- inner_req[11] = 0;
412
- inner_req[17] = [[0]];
413
- inner_req[18] = 0;
414
- inner_req[27] = 1;
415
- inner_req[30] = [4];
416
- inner_req[41] = [1];
417
- inner_req[53] = 0;
418
- inner_req[59] = uuid_val;
419
- inner_req[61] = [];
420
- inner_req[68] = 1;
421
- inner_req[79] = model_info.num;
422
- inner_req[80] = thinking_level;
423
- return [inner_req, model_header, uuid_val];
424
- },
425
-
426
- streamChunkExtract = (buf) => {
427
- let cur_buf = buf;
428
- if (cur_buf.startsWith(")]}\x27")) {
429
- cur_buf = cur_buf.slice(4).trimStart();
430
- }
431
- const extracted_li = [];
432
- while (true) {
433
- cur_buf = cur_buf.trimStart();
434
- const match_res = cur_buf.match(/^(\d+)\n/);
435
- if (!match_res) break;
436
- const len_str = match_res[1],
437
- chunk_len = parseInt(len_str, 10),
438
- start_idx = len_str.length;
439
- if (cur_buf.length < start_idx + chunk_len) break;
440
- const chunk_str = cur_buf.slice(start_idx, start_idx + chunk_len).trim();
441
- cur_buf = cur_buf.slice(start_idx + chunk_len);
442
-
443
- if (chunk_str.startsWith("[")) {
444
- const item_li = JSON.parse(chunk_str);
445
- for (const item of item_li) {
446
- if (Array.isArray(item) && item[2]) {
447
- const inner = JSON.parse(item[2]),
448
- cand_li = inner[4];
449
- if (Array.isArray(cand_li) && cand_li.length > 0) {
450
- const cand = cand_li[0],
451
- thoughts = cand[37]?.[0]?.[0] ?? "",
452
- raw_text = cand[1]?.[0] ?? "",
453
- card_text = raw_text.startsWith("http://googleusercontent.com/card_content/")
454
- ? (cand[22]?.[0] ?? raw_text)
455
- : raw_text,
456
- text = card_text.replaceAll(
457
- /https?:\/\/googleusercontent\.com\/(?:\w+\/)+\d+\n*/g,
458
- "",
459
- );
460
- extracted_li.push({ thoughts, text });
461
- }
462
- }
463
- }
464
- }
465
- }
466
- return [extracted_li, cur_buf];
467
- },
468
-
469
- sseStreamCreate = (body, completion_id, model, created_time) => {
470
- const enc = new TextEncoder();
471
- return new ReadableStream({
472
- async start(controller) {
473
- const chunkSend = (delta, finish_reason = null) => {
474
- const payload = JSON.stringify({
475
- id: completion_id,
476
- object: "chat.completion.chunk",
477
- created: created_time,
478
- model,
479
- choices: [
480
- {
481
- index: 0,
482
- delta,
483
- finish_reason,
484
- },
485
- ],
486
- });
487
- console.log("<-- [SSE 块]:", JSON.stringify(delta));
488
- controller.enqueue(enc.encode("data: " + payload + "\n\n"));
489
- };
490
-
491
- chunkSend({ role: ROLE_ASSISTANT });
492
-
493
- const reader = body.getReader(),
494
- decoder = new TextDecoder();
495
- let buf = "",
496
- last_text = "",
497
- last_thought = "",
498
- thinking_started = false;
499
-
500
- while (true) {
501
- const read_res = await reader.read();
502
- if (read_res.done) break;
503
- buf += decoder.decode(read_res.value, { stream: true });
504
-
505
- const [extracted_li, next_buf] = streamChunkExtract(buf);
506
- buf = next_buf;
507
-
508
- for (const { thoughts, text } of extracted_li) {
509
- if (ENABLE_THINKING && thoughts) {
510
- if (thoughts.startsWith(last_thought)) {
511
- const thought_delta = thoughts.slice(last_thought.length);
512
- if (thought_delta) {
513
- if (!thinking_started) {
514
- chunkSend({ content: "<think>\n" });
515
- thinking_started = true;
516
- }
517
- chunkSend({
518
- content: thought_delta,
519
- reasoning_content: thought_delta,
520
- });
521
- last_thought = thoughts;
522
- }
523
- } else {
524
- chunkSend({
525
- content: thoughts,
526
- reasoning_content: thoughts,
527
- });
528
- last_thought = thoughts;
529
- }
530
- }
531
-
532
- if (text.startsWith(last_text)) {
533
- const text_delta = text.slice(last_text.length);
534
- if (text_delta) {
535
- if (ENABLE_THINKING && thinking_started && !last_text) {
536
- chunkSend({ content: "</think>\n\n" });
537
- }
538
- chunkSend({ content: text_delta });
539
- last_text = text;
540
- }
541
- } else if (text) {
542
- chunkSend({ content: text });
543
- last_text = text;
544
- }
545
- }
546
- }
547
-
548
- if (ENABLE_THINKING && thinking_started && !last_text) {
549
- chunkSend({ content: "</think>\n\n" });
550
- }
551
-
552
- chunkSend({}, STOP_REASON_STOP);
553
- controller.enqueue(enc.encode("data: [DONE]\n\n"));
554
- controller.close();
555
- console.log(
556
- "<-- [SSE 完成] 完整输出:\n" +
557
- (last_thought ? "<think>\n" + last_thought + "\n</think>\n\n" : "") +
558
- last_text,
559
- );
560
- },
561
- });
562
- },
563
-
564
- fullResponseCollect = async (body) => {
565
- const reader = body.getReader(),
566
- decoder = new TextDecoder();
567
- let buf = "",
568
- final_text = "",
569
- final_thought = "";
570
-
571
- while (true) {
572
- const read_res = await reader.read();
573
- if (read_res.done) break;
574
- buf += decoder.decode(read_res.value, { stream: true });
575
-
576
- const [extracted_li, next_buf] = streamChunkExtract(buf);
577
- buf = next_buf;
578
-
579
- for (const { thoughts, text } of extracted_li) {
580
- if (text) final_text = text;
581
- if (thoughts) final_thought = thoughts;
582
- }
583
- }
584
-
585
- let content = final_text;
586
- if (ENABLE_THINKING && final_thought) {
587
- content = "<think>\n" + final_thought + "\n</think>\n\n" + final_text;
588
- }
589
- return content;
590
- },
591
-
592
- authVerify = (req) => {
593
- if (!API_KEY) return true;
594
- const auth_header = req.headers.get("authorization") ?? "",
595
- key = auth_header.replace(/^Bearer\s+/i, "");
596
- return key === API_KEY;
597
- },
598
-
599
- healthHandle = () => {
600
- const res_body = JSON.stringify({
601
- status: "healthy",
602
- service: "Gemini API 代理 (Node/Bun)",
603
- version: "1.0.0",
604
- endpoints: ["/v1/models", "/v1/chat/completions"],
605
- });
606
- console.log("<-- 响应: [200 OK]", res_body);
607
- return new Response(res_body, {
608
- status: STATUS_OK,
609
- headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
610
- });
611
- },
612
-
613
- modelListHandle = (now) => {
614
- const available_model_li =
615
- session_state.model_li.length > 0 ? session_state.model_li : DEFAULT_MODEL_LI,
616
- model_data_li = available_model_li.map((model_item) => ({
617
- id: model_item.id,
618
- object: "model",
619
- created: now,
620
- owned_by: "google-gemini-web",
621
- })),
622
- res_body = JSON.stringify({ object: "list", data: model_data_li });
623
- console.log("<-- 响应: [200 OK] 模型列表:", res_body);
624
- return new Response(res_body, {
625
- status: STATUS_OK,
626
- headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
627
- });
628
- },
629
-
630
- modelDetailHandle = (req_model_id, now) => {
631
- const target_model = modelMap(req_model_id),
632
- res_body = JSON.stringify({
633
- id: target_model.id,
634
- object: "model",
635
- created: now,
636
- owned_by: "google-gemini-web",
637
- });
638
- console.log("<-- 响应: [200 OK] 模型详情:", res_body);
639
- return new Response(res_body, {
640
- status: STATUS_OK,
641
- headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
642
- });
643
- },
644
-
645
- imageProxyHandle = async (req_url) => {
646
- const target_url = req_url.searchParams.get("url");
647
- if (!target_url) {
648
- return new Response(
649
- JSON.stringify({ code: ERR_MISSING_URL, error: "缺少 url 参数" }),
650
- {
651
- status: STATUS_BAD_REQUEST,
652
- headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
653
- },
654
- );
655
- }
656
- const img_res = await fetch(target_url, {
657
- headers: {
658
- "User-Agent": USER_AGENT,
659
- Referer: "https://gemini.google.com/",
660
- },
661
- }),
662
- img_bytes = new Uint8Array(await img_res.arrayBuffer()),
663
- headers = {
664
- ...CORS_HEADERS,
665
- "Content-Type": img_res.headers.get("content-type") ?? "image/png",
666
- };
667
- return new Response(img_bytes, { status: STATUS_OK, headers });
668
- },
669
-
670
- chatCompletionsHandle = async (req, body) => {
671
- if (!authVerify(req)) {
672
- const res_body = JSON.stringify({ code: ERR_UNAUTHORIZED, error: "未授权" });
673
- console.log("<-- 响应: [401 Unauthorized]", res_body);
674
- return new Response(res_body, {
675
- status: STATUS_UNAUTHORIZED,
676
- headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
677
- });
678
- }
679
-
680
- if (!body.messages && body.prompt) {
681
- body.messages = [{ role: "user", content: body.prompt }];
682
- }
683
-
684
- const model_info = modelMap(body.model),
685
- conversation = conversationFormat(body.messages);
686
-
687
- if (!session_state.at) {
688
- await sessionInit();
689
- }
690
-
691
- session_state.req_id += 1;
692
- const [inner_req, model_header, uuid_val] = payloadBuild(conversation, model_info),
693
- form_data = new URLSearchParams();
694
- form_data.set("at", session_state.at);
695
- form_data.set("f.req", JSON.stringify([null, JSON.stringify(inner_req)]));
696
-
697
- const gen_url = new URL(GEN_URL);
698
- gen_url.searchParams.set("hl", "en");
699
- gen_url.searchParams.set("_reqid", String(session_state.req_id));
700
- gen_url.searchParams.set("rt", "c");
701
- if (session_state.bl) gen_url.searchParams.set("bl", session_state.bl);
702
- if (session_state.fsid) gen_url.searchParams.set("f.sid", session_state.fsid);
703
-
704
- const gen_res = await fetch(gen_url.toString(), {
705
- method: "POST",
706
- headers: {
707
- "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
708
- Origin: "https://gemini.google.com",
709
- Referer: "https://gemini.google.com/",
710
- "X-Same-Domain": "1",
711
- "User-Agent": USER_AGENT,
712
- Cookie: session_state.cookie_header,
713
- "x-goog-ext-525001261-jspb": JSON.stringify(model_header),
714
- "x-goog-ext-73010989-jspb": "[0]",
715
- "x-goog-ext-73010990-jspb": "[0,0,0]",
716
- "x-goog-ext-525005358-jspb": JSON.stringify([uuid_val, 1]),
717
- },
718
- body: form_data.toString(),
719
- }),
720
- completion_id = "chatcmpl-" + crypto.randomUUID(),
721
- created_time = Math.floor(Date.now() / 1000);
722
-
723
- if (body.stream) {
724
- console.log("<-- 响应: [200 OK] 开始流式传输 (SSE)...");
725
- const sse_stream = sseStreamCreate(gen_res.body, completion_id, model_info.id, created_time);
726
- return new Response(sse_stream, {
727
- status: STATUS_OK,
728
- headers: {
729
- ...CORS_HEADERS,
730
- "Content-Type": "text/event-stream",
731
- "Cache-Control": "no-cache",
732
- Connection: "keep-alive",
733
- },
734
- });
735
- }
736
-
737
- const full_content = await fullResponseCollect(gen_res.body),
738
- prompt_tokens = conversation.split(/\s+/).length,
739
- completion_tokens = full_content.split(/\s+/).length,
740
- res_json = {
741
- id: completion_id,
742
- object: "chat.completion",
743
- created: created_time,
744
- model: model_info.id,
745
- choices: [
746
- {
747
- index: 0,
748
- message: {
749
- role: ROLE_ASSISTANT,
750
- content: full_content,
751
- },
752
- finish_reason: STOP_REASON_STOP,
753
- },
754
- ],
755
- usage: {
756
- prompt_tokens,
757
- completion_tokens,
758
- total_tokens: prompt_tokens + completion_tokens,
759
- },
760
- },
761
- res_body = JSON.stringify(res_json);
762
- console.log("<-- 响应: [200 OK]", res_body);
763
- return new Response(res_body, {
764
- status: STATUS_OK,
765
- headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
766
- });
767
- },
768
-
769
- reqHandle = async (req) => {
770
- const req_url = new URL(req.url),
771
- pathname = req_url.pathname,
772
- norm_path = pathname.replace(/\/+$/, "") || "/",
773
- is_body_method = req.method === "POST" || req.method === "PUT" || req.method === "PATCH";
774
- let body = {},
775
- req_text = "";
776
-
777
- if (is_body_method) {
778
- req_text = await req.text();
779
- const trimmed = req_text.trim();
780
- if (trimmed && (trimmed.startsWith("{") || trimmed.startsWith("["))) {
781
- body = JSON.parse(trimmed);
782
- }
783
- }
784
-
785
- console.log("\n--> [" + req.method + "] " + pathname);
786
- console.log("--> 请求头:", JSON.stringify(Object.fromEntries(req.headers.entries())));
787
- if (req_text.trim()) {
788
- console.log("--> 请求体:", req_text);
789
- } else {
790
- console.log("--> 请求体: (无)");
791
- }
792
-
793
- if (req.method === "OPTIONS") {
794
- console.log("<-- 响应: [204 No Content]");
795
- return new Response(null, { status: STATUS_NO_CONTENT, headers: CORS_HEADERS });
796
- }
797
-
798
- if (norm_path === "/" || norm_path === "/v1") {
799
- return healthHandle();
800
- }
801
-
802
- const now = Math.floor(Date.now() / 1000);
803
- if ((norm_path === "/models" || norm_path.endsWith("/models")) && req.method === "GET") {
804
- return modelListHandle(now);
805
- }
806
-
807
- if (norm_path.includes("/models/") && req.method === "GET") {
808
- const req_model_id = norm_path.split("/").pop();
809
- return modelDetailHandle(req_model_id, now);
810
- }
811
-
812
- if (norm_path.endsWith("/gemini-proxy/image") && req.method === "GET") {
813
- console.log("<-- 响应: [200 OK] 代理图片");
814
- return imageProxyHandle(req_url);
815
- }
816
-
817
- if (
818
- (norm_path === "/chat/completions" ||
819
- norm_path.endsWith("/chat/completions") ||
820
- norm_path === "/completions" ||
821
- norm_path.endsWith("/completions")) &&
822
- req.method === "POST"
823
- ) {
824
- return chatCompletionsHandle(req, body);
825
- }
826
-
827
- const res_body = JSON.stringify({
828
- code: ERR_NOT_FOUND,
829
- error: "未找到接口",
830
- path: pathname,
831
- method: req.method,
832
- });
833
- console.log("<-- 响应: [404 Not Found] 未找到接口: " + req.method + " " + pathname);
834
- return new Response(res_body, {
835
- status: STATUS_NOT_FOUND,
836
- headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
837
- });
838
- },
839
-
840
- serverStart = () => {
841
- if (typeof Bun !== "undefined" && Bun.serve) {
842
- const bun_server = Bun.serve({
843
- port: PORT,
844
- hostname: HOST,
845
- fetch: reqHandle,
846
- });
847
- console.log("服务运行在 (Bun) http://" + HOST + ":" + PORT);
848
- return bun_server;
849
- }
850
-
851
- const node_server = createServer(async (node_req, node_res) => {
852
- try {
853
- const url = "http://" + (node_req.headers.host ?? (HOST + ":" + PORT)) + node_req.url,
854
- has_body = node_req.method !== "GET" && node_req.method !== "HEAD",
855
- web_req = new Request(url, {
856
- method: node_req.method,
857
- headers: node_req.headers,
858
- body: has_body ? Readable.toWeb(node_req) : null,
859
- duplex: "half",
860
- }),
861
- web_res = await reqHandle(web_req);
862
-
863
- node_res.statusCode = web_res.status;
864
- for (const [header_key, header_val] of web_res.headers.entries()) {
865
- node_res.setHeader(header_key, header_val);
866
- }
867
- if (web_res.body) {
868
- Readable.fromWeb(web_res.body).pipe(node_res);
869
- } else {
870
- node_res.end();
871
- }
872
- } catch (err) {
873
- if (!node_res.headersSent) {
874
- node_res.statusCode = STATUS_SERVER_ERR;
875
- node_res.end(JSON.stringify({ code: STATUS_SERVER_ERR, error: "Internal Server Error" }));
876
- }
877
- console.error("服务器异常:", err);
878
- }
879
- });
880
-
881
- node_server.listen(PORT, HOST, () => {
882
- console.log("服务运行在 (Node) http://" + HOST + ":" + PORT);
883
- });
884
- return node_server;
885
- };
10
+ import { sessionInit } from "./src/sessionState.js";
11
+ import { serverStart } from "./src/serverStart.js";
12
+
13
+ export {
14
+ API_KEY,
15
+ BROWSER_PATH_LI,
16
+ CORS_HEADERS,
17
+ DEFAULT_MODEL_LI,
18
+ ENABLE_THINKING,
19
+ ERR_MISSING_URL,
20
+ ERR_NOT_FOUND,
21
+ ERR_UNAUTHORIZED,
22
+ HOST,
23
+ INIT_URL,
24
+ PORT,
25
+ ROLE_ASSISTANT,
26
+ ROLE_SYSTEM,
27
+ ROLE_TOOL,
28
+ ROLE_USER,
29
+ STATUS_BAD_REQUEST,
30
+ STATUS_NO_CONTENT,
31
+ STATUS_NOT_FOUND,
32
+ STATUS_OK,
33
+ STATUS_SERVER_ERR,
34
+ STOP_REASON_STOP,
35
+ STOP_REASON_TOOL_CALLS,
36
+ TOOL_FENCE_CLOSE,
37
+ TOOL_FENCE_OPEN,
38
+ USER_AGENT,
39
+ } from "./src/constant.js";
40
+
41
+ export { cookieRead, keychainPwdRead, valDecrypt } from "./src/cookieRead.js";
42
+ export { sessionInit, session_state } from "./src/sessionState.js";
43
+ export {
44
+ modelFetch,
45
+ modelListFormat,
46
+ modelListPrint,
47
+ modelMap,
48
+ stringPadEnd,
49
+ stringVisualWidth,
50
+ versionCompare,
51
+ } from "./src/modelDiscover.js";
52
+ export {
53
+ jsonFormat,
54
+ toolCallExtract,
55
+ toolFenceGateCreate,
56
+ toolPromptBuild,
57
+ toolResultFormat,
58
+ } from "./src/toolHandle.js";
59
+ export { conversationFormat, payloadBuild } from "./src/payloadBuild.js";
60
+ export { fullResponseCollect, streamChunkExtract } from "./src/streamExtract.js";
61
+ export { sseStreamCreate } from "./src/sseResponse.js";
62
+ export {
63
+ authVerify,
64
+ chatCompletionsHandle,
65
+ healthHandle,
66
+ imageProxyHandle,
67
+ modelDetailHandle,
68
+ modelListHandle,
69
+ reqHandle,
70
+ } from "./src/router.js";
71
+ export { serverStart } from "./src/serverStart.js";
886
72
 
887
73
  await sessionInit();
888
74