@8-/gemini-web-api 1.0.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.
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: js_review
3
+ description: JavaScript 代码审查
4
+ ---
5
+
6
+ # JS / Bun 代码规范与审查指南
7
+
8
+ ## 1. 运行时与现代语法规范
9
+
10
+ - 执行环境:优先使用 Bun 现代运行时,脚本文件顶部统一使用 `#!/usr/bin/env -S bun`。
11
+ - 路径与元数据:必须使用 `import.meta.dirname` 获取当前目录路径,禁止使用传统 `__dirname`、`__filename` 或冗长的 `fileURLToPath(import.meta.url)`。
12
+ - 极速文件 I/O:在 Bun 环境下,优先使用原生 `Bun.file(path).text()` / `Bun.file(path).json()` / `Bun.write(path, data)` 进行极速异步 I/O。
13
+ - 现代语法:充分利用 ES2024+ 现代特性,如 `replaceAll()`、空值合并操作符(`??`)、可选链(`?.`,禁止过度防御式编程)。
14
+
15
+ ## 2. 代码风格与设计哲学
16
+
17
+ - 简洁优雅:接口设计低耦合、高内聚,拆分过长函数为单一职责纯函数。
18
+ - 纯函数优先:严禁定义 `class`,全部使用纯函数与数据流管道。
19
+ - 箭头函数:统一使用箭头函数 `const funcName = () => {}`,不使用 `function` 关键字(生成器除外);如可用 `.bind` 绑定参数则避免多层包装。
20
+ - 变量声明:连续声明必须合并为一个 `const` 语句(例如 `const a = 1, b = 2, c = 3;`),减少语句冗余。
21
+ - 异步处理:统一使用 `async/await`,严禁使用 `.then()` 链式调用。
22
+ - 异常处理:不盲目自动生成 `try...catch`(由人工按需维护,已有 `try catch` 保留)。
23
+ - 对象与解构:优先使用解构赋值提取需要的属性,避免在循环或内部深层反复使用点号访问。
24
+ - 参数与多值返回:
25
+ - 函数参数扁平化,写 `a, b, c` 而非单一对象 `{ a, b, c }`;如可选参数多,采用 `[[配置项数字, 配置项值], ...]` 范式,配置项用数字常量定义。
26
+ - 多值返回统一使用数组 `[a, b, c]`;多返回值时使用数值常量定义位置语义。
27
+ - 状态表示:严禁使用魔法字符串表示状态,统一用常量/数字枚举定义。
28
+ - 字符串拼接:普通拼接使用 `+`,`import` 导入语句除外(方便 Vite / 打包器静态分析)。
29
+ - 循环与列表:
30
+ - 数组多用 `map`、`forEach`、`filter`、`find`;
31
+ - `for` 循环如需序号统一使用 `++i` 而非 `i++`;
32
+ - 列表变量名不使用复数形式,统一以 `_li` 结尾(例如 `user_li`, `cmd_li`)。
33
+
34
+ ## 3. 命名规范
35
+
36
+ - 极简语义:使用简短明确的动词/名词(例如用 `rm` 代替 `remove`/`delete`),禁止无意义单个字母或过度缩写。
37
+ - 文件名与函数:名词在前、动词在后(如 `profileSet.js` 而非 `setProfile.js`)。函数命名尽量精炼动词,不带无意义的 `get` 前缀(如 `cookieByHeader` 而非 `getCookie`)。
38
+ - 风格约定:
39
+ - 普通变量名:蛇形命名 `snake_case`(如 `user_auth_token`);若变量为函数则使用小写驼峰 `camelCase`。
40
+ - 函数名:小写驼峰 `camelCase`。
41
+ - 回调函数参数:小写驼峰(如 `onChange`)。
42
+ - 模块级/全局常量:全大写下划线 `UPPER_SNAKE_CASE`(如 `DEFAULT_TIMEOUT`, `CODE_TO_ID`)。
43
+
44
+ ## 4. 模块化机制
45
+
46
+ - 精准按需导入:严禁 `import * as x` 或直接导入庞大对象。
47
+ - 导出规范:
48
+ - 禁止导出单一大对象,以函数、变量为粒度导出。
49
+ - 可变全局状态(如语言、用户信息)使用 `export let` 导出。
50
+ - 其余函数与常量合并使用单一 `export const` + 逗号声明。
51
+ - 单一功能文件使用 `export default`。
52
+
53
+ ## 5. 错误处理与浏览器兼容
54
+
55
+ - 错误码常量化:避免使用字符串描述错误,统一用 `const` 声明数值错误码。
56
+ - 结构化错误:需附带数据信息时使用 `[错误码, 数据项1, 数据项2]` 数组范式。
57
+ - Web 标准兼容 API:
58
+ - 加解密强制使用原生 Web Crypto API (`crypto.subtle`)。
59
+ - 二进制处理统一使用 `Uint8Array`。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 x-at-01
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,307 @@
1
+ <div align="center">
2
+
3
+ # Gemini Web API
4
+
5
+ **OpenAI-compatible Gemini Web API reverse proxy running on Bun & Node.js.**
6
+ **Automatically detects Chrome session cookies. Zero configuration required.**
7
+
8
+ **兼容 OpenAI 格式的 Gemini Web 反向代理服务,支持 Bun 与 Node.js 双运行时。**
9
+ **全自动探测本地 Chrome 凭据,无需任何手动配置,开箱即用。**
10
+
11
+ <p>
12
+ <a href="#english">English</a> | <a href="#简体中文">简体中文</a>
13
+ </p>
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ <a id="english"></a>
20
+
21
+ ## English
22
+
23
+ ### Overview
24
+
25
+ **`@8-/gemini-web-api`** is an ultra-lightweight reverse proxy that turns your Google Gemini Web session into a standard OpenAI-compatible API.
26
+
27
+ **No configuration is needed**: It automatically discovers your existing Chrome / Brave / Edge browser session, retrieves the encryption key from the macOS Keychain, decrypts your Gemini session cookies, and starts serving OpenAI-compatible endpoints (`/v1/chat/completions`, `/v1/models`) instantly.
28
+
29
+ ### Key Highlights
30
+
31
+ - **Zero Configuration**: As long as you are logged into [gemini.google.com](https://gemini.google.com) in Chrome (or Brave / Edge), the server automatically extracts and decrypts your tokens. No manual cookie copying or `.env` required!
32
+ - **Dual Runtime Support**: Native ultra-high performance on **Bun** (`Bun.serve`) and seamless native support on **Node.js** (>= 22.5.0, via `node:sqlite` and Web Streams).
33
+ - **Zero External Dependencies**: 100% built on standard Web APIs (`fetch`, `ReadableStream`, `crypto.subtle`) and built-in Node/Bun modules.
34
+ - **OpenAI Drop-in Replacement**: Fully compatible with `/v1/chat/completions` and `/v1/models`. Works seamlessly with ChatGPT-Next-Web, LobeChat, Cherry Studio, OpenAI SDKs, etc.
35
+ - **Reasoning & Thinking Models**: Full streaming support for `<think>` thoughts and `reasoning_content` deltas (compatible with DeepSeek/Gemini thinking UI renderers).
36
+ - **Dynamic Model Discovery**: Automatically discovers available Gemini models (Flash, Pro, etc.) and capacity tiers directly from your web session.
37
+ - **Image Proxy**: Built-in `/gemini-proxy/image` endpoint to safely render generated card images without CORS or anti-hotlinking issues.
38
+
39
+ ---
40
+
41
+ ### Installation
42
+
43
+ Install globally using **Bun** or **npm**:
44
+
45
+ ```bash
46
+ # Using Bun
47
+ bun i -g @8-/gemini-web-api
48
+
49
+ # Using npm
50
+ npm i -g @8-/gemini-web-api
51
+ ```
52
+
53
+ Or run instantly without installing:
54
+
55
+ ```bash
56
+ # Using bunx
57
+ bunx @8-/gemini-web-api
58
+
59
+ # Using npx
60
+ npx @8-/gemini-web-api
61
+ ```
62
+
63
+ ---
64
+
65
+ ### Usage
66
+
67
+ #### 1. Start the Server
68
+
69
+ Simply run:
70
+
71
+ ```bash
72
+ gemini-web-api
73
+ ```
74
+
75
+ The server will automatically:
76
+ 1. Detect your local Chrome / Brave / Edge cookies database.
77
+ 2. Read the encryption key from macOS Keychain.
78
+ 3. Decrypt your `__Secure-1PSID` and `__Secure-1PSIDTS` cookies.
79
+ 4. Discover available models from Gemini.
80
+ 5. Start listening on `http://0.0.0.0:7860`.
81
+
82
+ #### 2. Optional Environment Variables
83
+
84
+ If you want to customize the port, add an authentication key, or enable thinking mode, set environment variables inline or in a `.env` file:
85
+
86
+ ```bash
87
+ # Example: Custom port, API key, and thinking mode
88
+ PORT=8000 API_KEY=sk-mysecret ENABLE_THINKING=true gemini-web-api
89
+ ```
90
+
91
+ | Variable | Default | Description |
92
+ | :--- | :--- | :--- |
93
+ | `PORT` | `7860` | Server listening port |
94
+ | `HOST` | `0.0.0.0` | Server listening host |
95
+ | `API_KEY` | *(empty)* | Optional Bearer API key for authorization |
96
+ | `ENABLE_THINKING` | `false` | Enable reasoning thoughts (`<think>` blocks & `reasoning_content`) |
97
+ | `SECURE_1PSID` | *(auto-detected)* | Manual override for `__Secure-1PSID` cookie (only needed if no browser is logged in) |
98
+ | `SECURE_1PSIDTS` | *(auto-detected)* | Manual override for `__Secure-1PSIDTS` cookie |
99
+
100
+ ---
101
+
102
+ ### API Calling Examples
103
+
104
+ #### Test with cURL (Streaming)
105
+
106
+ ```bash
107
+ curl -N http://127.0.0.1:7860/v1/chat/completions \
108
+ -H "Content-Type: application/json" \
109
+ -d '{
110
+ "model": "gemini-flash",
111
+ "messages": [
112
+ {"role": "user", "content": "Explain quantum computing in one sentence."}
113
+ ],
114
+ "stream": true
115
+ }'
116
+ ```
117
+
118
+ #### Use with OpenAI Python SDK
119
+
120
+ ```python
121
+ from openai import OpenAI
122
+
123
+ client = OpenAI(
124
+ base_url="http://127.0.0.1:7860/v1",
125
+ api_key="none" # Any string if API_KEY is not set
126
+ )
127
+
128
+ response = client.chat.completions.create(
129
+ model="gemini-flash",
130
+ messages=[{"role": "user", "content": "Hello Gemini!"}],
131
+ stream=True
132
+ )
133
+
134
+ for chunk in response:
135
+ content = chunk.choices[0].delta.content or ""
136
+ print(content, end="", flush=True)
137
+ ```
138
+
139
+ #### Use with OpenAI Node.js SDK
140
+
141
+ ```javascript
142
+ import OpenAI from "openai";
143
+
144
+ const openai = new OpenAI({
145
+ baseURL: "http://127.0.0.1:7860/v1",
146
+ apiKey: "none",
147
+ });
148
+
149
+ const stream = await openai.chat.completions.create({
150
+ model: "gemini-flash",
151
+ messages: [{ role: "user", content: "Tell me a joke." }],
152
+ stream: true,
153
+ });
154
+
155
+ for await (const chunk of stream) {
156
+ process.stdout.write(chunk.choices[0]?.delta?.content || "");
157
+ }
158
+ ```
159
+
160
+ ---
161
+
162
+ <a id="简体中文"></a>
163
+
164
+ ## 简体中文
165
+
166
+ ### 项目简介
167
+
168
+ **`@8-/gemini-web-api`** 是一个极简的 Google Gemini Web 转 OpenAI 标准接口反向代理服务。
169
+
170
+ **无需繁琐配置**:只需在日常使用的 Chrome(或 Brave、Edge)浏览器中登录过 [gemini.google.com](https://gemini.google.com),程序启动时即可全自动探测浏览器 Cookie 数据库,通过 macOS Keychain 提取密钥并自动解密登录凭据,立即开始提供兼容 OpenAI 的 `/v1/chat/completions` 与 `/v1/models` 接口!
171
+
172
+ ### 核心亮点
173
+
174
+ - **开箱即用,零手动配置**:自动探测 Chrome / Brave / Edge 浏览器会话,全自动解密并获取登录令牌,彻底告别手动抓包复制 Cookie 的烦恼!
175
+ - **Bun 与 Node.js 双运行时支持**:在 **Bun** 下使用原生 `Bun.serve` 极速响应;在 **Node.js** (>= 22.5.0) 下利用内置 `node:sqlite` 与原生 Web Streams 同样流畅运行。
176
+ - **零第三方外部依赖**:纯基于 Web 标准接口(`fetch`、`ReadableStream`、`crypto.subtle`)与运行时内置标准模块构建。
177
+ - **无缝替代 OpenAI 接口**:标准 `/v1/chat/completions` 与 `/v1/models` 路由,直接兼容 ChatGPT-Next-Web、LobeChat、Cherry Studio 以及各类 OpenAI SDK。
178
+ - **深度思考与推理模式**:完整支持 `<think>` 思考标签与 SSE `reasoning_content` 流式字段传输,兼容各类思考过程前端渲染。
179
+ - **动态模型列表发现**:启动时自动从 Gemini 网页端动态拉取已解锁的可用模型及配额层级。
180
+ - **图片安全代理**:内置 `/gemini-proxy/image` 接口,解决 Google 生成图片防盗链和跨域展示问题。
181
+
182
+ ---
183
+
184
+ ### 安装方式
185
+
186
+ 通过 **Bun** 或 **npm** 全局安装:
187
+
188
+ ```bash
189
+ # 使用 Bun 全局安装
190
+ bun i -g @8-/gemini-web-api
191
+
192
+ # 使用 npm 全局安装
193
+ npm i -g @8-/gemini-web-api
194
+ ```
195
+
196
+ 或者无需安装,直接使用 `bunx` / `npx` 即开即用:
197
+
198
+ ```bash
199
+ # 使用 bunx
200
+ bunx @8-/gemini-web-api
201
+
202
+ # 使用 npx
203
+ npx @8-/gemini-web-api
204
+ ```
205
+
206
+ ---
207
+
208
+ ### 使用指南
209
+
210
+ #### 1. 启动服务
211
+
212
+ 安装后直接在终端运行:
213
+
214
+ ```bash
215
+ gemini-web-api
216
+ ```
217
+
218
+ 程序将全自动执行:
219
+ 1. 自动探测本地 Chrome、Brave、Edge 浏览器的 Cookie 存储路径;
220
+ 2. 自动从 macOS Keychain 安全读取加密密码并推导密钥;
221
+ 3. 解密 `__Secure-1PSID` 与 `__Secure-1PSIDTS` 凭据;
222
+ 4. 动态同步 Gemini 网页端模型配置;
223
+ 5. 在 `http://0.0.0.0:7860` 启动 OpenAI 兼容接口。
224
+
225
+ #### 2. 可选环境变量配置
226
+
227
+ 如需自定义端口、设置访问密码或启用思考模式,可在命令行直接传入或在当前目录下创建 `.env` 文件:
228
+
229
+ ```bash
230
+ # 示例:自定义端口 8000、密码及开启思考模式
231
+ PORT=8000 API_KEY=sk-mysecret ENABLE_THINKING=true gemini-web-api
232
+ ```
233
+
234
+ | 环境变量 | 默认值 | 说明 |
235
+ | :--- | :--- | :--- |
236
+ | `PORT` | `7860` | 服务的监听端口 |
237
+ | `HOST` | `0.0.0.0` | 监听的主机地址 |
238
+ | `API_KEY` | *(留空)* | 可选的 Bearer 访问密码鉴权 |
239
+ | `ENABLE_THINKING` | `false` | 是否开启思考模式(输出 `<think>` 与 `reasoning_content`) |
240
+ | `SECURE_1PSID` | *(自动探测)* | 手动指定 `__Secure-1PSID`(仅在未登录浏览器时需要) |
241
+ | `SECURE_1PSIDTS` | *(自动探测)* | 手动指定 `__Secure-1PSIDTS` |
242
+
243
+ ---
244
+
245
+ ### 接口调用示例
246
+
247
+ #### cURL 流式对话调用
248
+
249
+ ```bash
250
+ curl -N http://127.0.0.1:7860/v1/chat/completions \
251
+ -H "Content-Type: application/json" \
252
+ -d '{
253
+ "model": "gemini-flash",
254
+ "messages": [
255
+ {"role": "user", "content": "用一句话解释相对论"}
256
+ ],
257
+ "stream": true
258
+ }'
259
+ ```
260
+
261
+ #### Python (OpenAI SDK)
262
+
263
+ ```python
264
+ from openai import OpenAI
265
+
266
+ client = OpenAI(
267
+ base_url="http://127.0.0.1:7860/v1",
268
+ api_key="none" # 未设置 API_KEY 时可填任意字符串
269
+ )
270
+
271
+ response = client.chat.completions.create(
272
+ model="gemini-flash",
273
+ messages=[{"role": "user", "content": "你好,Gemini!"}],
274
+ stream=True
275
+ )
276
+
277
+ for chunk in response:
278
+ content = chunk.choices[0].delta.content or ""
279
+ print(content, end="", flush=True)
280
+ ```
281
+
282
+ #### Node.js (OpenAI SDK)
283
+
284
+ ```javascript
285
+ import OpenAI from "openai";
286
+
287
+ const openai = new OpenAI({
288
+ baseURL: "http://127.0.0.1:7860/v1",
289
+ apiKey: "none",
290
+ });
291
+
292
+ const stream = await openai.chat.completions.create({
293
+ model: "gemini-flash",
294
+ messages: [{ role: "user", content: "讲个笑话吧" }],
295
+ stream: true,
296
+ });
297
+
298
+ for await (const chunk of stream) {
299
+ process.stdout.write(chunk.choices[0]?.delta?.content || "");
300
+ }
301
+ ```
302
+
303
+ ---
304
+
305
+ ### 开源协议
306
+
307
+ 本项目采用 [MIT License](LICENSE) 授权。
package/index.js ADDED
@@ -0,0 +1,891 @@
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
+ import { join } from "node:path";
7
+ import { DatabaseSync } from "node:sqlite";
8
+ import { Readable } from "node:stream";
9
+
10
+ try {
11
+ process.loadEnvFile(join(import.meta.dirname, ".env"));
12
+ } catch (err) {
13
+ if (err.code !== "ENOENT") throw err;
14
+ }
15
+
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
+ };
886
+
887
+ await sessionInit();
888
+
889
+ const server = serverStart();
890
+
891
+ export default server;
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@8-/gemini-web-api",
3
+ "version": "1.0.0",
4
+ "description": "OpenAI-compatible Gemini Web API reverse proxy running seamlessly on both Bun and Node.js with zero external dependencies.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "gemini-web-api": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "bun index.js",
12
+ "start:node": "node index.js"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/x-at-01/gemini-web-api.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/x-at-01/gemini-web-api/issues"
20
+ },
21
+ "homepage": "https://github.com/x-at-01/gemini-web-api#readme",
22
+ "keywords": [
23
+ "gemini",
24
+ "gemini-web-api",
25
+ "openai",
26
+ "openai-compatible",
27
+ "reverse-proxy",
28
+ "bun",
29
+ "node",
30
+ "llm",
31
+ "sse",
32
+ "streaming"
33
+ ],
34
+ "license": "MIT"
35
+ }