magpie-gemini-web 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -14,10 +14,10 @@ The plugin provides two independent providers that can be used individually or t
14
14
  | :--- | :--- | :--- |
15
15
  | **Best for** | **Coding agents (recommended)** | General chat, web-only models & web quota |
16
16
  | **Tool calling** | **Native Function Calling**, reliable | Prompt-emulated (experimental; Flash often skips tools) |
17
- | **Thinking levels** | `minimal` / `low` / `medium` / `high` (`minimal` is fastest & cheapest) | `low` (base) / `high` (`-thinking` extended thinking) |
17
+ | **Thinking levels** | `minimal` / `low` / `medium` / `high` (`minimal` is fastest & cheapest) | `low` (base) / `medium` (`-thinking-medium`, `gemini-4-argon` only) / `high` (`-thinking` extended thinking) |
18
18
  | **Sign-in method** | Sign in directly in an isolated popup browser window | Copy `cookie` from a private/incognito window |
19
19
  | **Usage cards** | Shows models currently on cooldown and recovery times | Syncs **5-hour** & **weekly** usage and reset times from the web |
20
- | **Local backend** | AIStudio2API (`v0.2.14`) | gemini-web2api-go (`4.22.0-magpie.1` optimized build) |
20
+ | **Local backend** | AIStudio2API (`v0.2.16`) | gemini-web2api-go (`4.26.0-magpie.1` optimized build) |
21
21
 
22
22
  ---
23
23
 
@@ -34,7 +34,7 @@ Install **magpie-gemini-web** from the Magpie plugin market, then sign in to eit
34
34
 
35
35
  ### Option 2: Gemini Web (`gemini-web`)
36
36
 
37
- 1. Sign in to **Gemini Web** in Magpie and choose **「一键配置本地 Gemini Web(推荐)」** (One-click local setup).
37
+ 1. Sign in to **Gemini Web** in Magpie and choose **「一键配置本地 Gemini Web(推荐)」** (One-click local setup). If you need a proxy to reach Google, enter its URL (e.g. `http://127.0.0.1:7890`) when prompted at the start of sign-in, or leave it blank for direct connection / existing proxy pool.
38
38
  2. Copy your cookie using a **private / incognito window** (Firefox recommended):
39
39
  1. Open <https://gemini.google.com/app> in the private window and sign in to your Google account.
40
40
  2. Press `F12` (`⌥⌘I` on macOS), switch to the **Network** tab, and reload the page.
@@ -42,6 +42,8 @@ Install **magpie-gemini-web** from the Magpie plugin market, then sign in to eit
42
42
  4. **Close the private window directly—do not sign out** (signing out invalidates the cookie).
43
43
  3. The plugin verifies the account and completes sign-in. Repeat the steps above to add more accounts.
44
44
 
45
+ > **Backend upgrade required during sign-in?** An older background process may still be running. Exit Magpie, stop the matching `gemini-web2api-go` process (Windows: Task Manager → Details → `gemini-web2api-go.exe`), then run the full installation command shown in the error. It preserves accounts and configuration and selects the plugin's managed backend. Set `GEMINI_WEB_PROXY` first if the download needs a proxy. Reopen Magpie and sign in after installation.
46
+
45
47
  > **Why is a private window required?** The backend periodically refreshes this session to keep it alive. If it shares a session with your daily browser, Google will revoke the session and sign your browser out of all Google services. A private window (or a dedicated secondary Google account) gives the backend an isolated session.
46
48
 
47
49
  ---
@@ -84,6 +86,7 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
84
86
  | `allowImages` | `false` | Shared (override via `studio.allowImages`) | Allow image input |
85
87
  | `requestTimeoutMs` / `modelTimeoutMs` | Web: `240000` / `10000`; Studio: `1800000` / `10000` | Shared (override via `studio.*`) | Timeout (ms) per backend wait or model list fetch; resets on stream data, explicit settings take precedence |
86
88
  | `stripCitations` | `true` | `gemini-web` only | Strip `[cite: 1]` and `[cite_start]` markers from replies and tool arguments (not supported on AI Studio) |
89
+ | `proxy` | none | `gemini-web` only | Proxy for the managed Gemini Web backend to reach Google (`http` / `https` / `socks5` / `socks5h`), or set `GEMINI_WEB_PROXY` env var / enter during sign-in |
87
90
  | `baseURL` | `http://127.0.0.1:8083/v1` | `gemini-web` only | Default Gemini Web backend URL |
88
91
  | `contextLimit` / `outputLimit` | `1000000` / `8192` | `gemini-web` only | Context and output limits reported to clients |
89
92
  | `studio.proxy` | none | `ai-studio` only | Proxy for reaching Google (`http` / `https` / `socks5`), or set `AI_STUDIO_PROXY` env var; when set, it takes precedence over the proxy saved in the admin page. An empty value counts as unset |
@@ -97,7 +100,7 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
97
100
  - **The backend uses neither Magpie's proxies (including the provider proxy set for `ai-studio`) nor `HTTPS_PROXY` and similar variables**: it keeps running after Magpie quits, when a proxy borrowed from Magpie may be gone. Use one of the options above. A backend started by a plugin version from before this fix may still be using a proxy from the environment it was started in (Magpie's, for example); the "代理出口" line marks it and says what a restart changes.
98
101
  - The usage card's "代理出口" (proxy) line, the Google sign-in instructions and failure messages, and `npm run studio:status` show the proxy the backend actually uses, where it comes from, and whether a new setting still needs a restart.
99
102
  - The listen address, API key and admin credentials are managed by the plugin; changing them in the admin page has no effect.
100
- - **Gemini Web:** The backend ignores `HTTP_PROXY`. Add your proxy to the proxy pool in the local admin panel at `http://127.0.0.1:8083/admin` (password is `adminToken` in `secrets.json` inside the data directory).
103
+ - **Gemini Web:** The backend ignores system proxies and `HTTP_PROXY`. Enter your proxy URL when prompted at the start of "One-click local setup", set the `proxy` plugin option or `GEMINI_WEB_PROXY` environment variable, or add it to the proxy pool in the local admin panel at `http://127.0.0.1:8083/admin` (password is `adminToken` in `secrets.json` inside the data directory).
101
104
  - *Note:* When using a proxy pool, send one request with a newly added account so the backend binds it to a proxy, then re-add it via "Add an existing Google account from the local backend" to load its email and usage (SOCKS proxies are not supported for usage reads).
102
105
 
103
106
  **Connecting a self-hosted backend:**
@@ -144,7 +147,7 @@ When a plugin error asks you to run a command, it includes the full ready-to-cop
144
147
  - Concurrency is capped at 1, but Google account rate-limiting or flagging cannot be completely ruled out—use at your own risk.
145
148
 
146
149
  **AI Studio upgrades and model selection:**
147
- - The plugin pins SHA-256-verified `v0.2.14-magpie.1`, based on upstream `v0.2.14`. Upgrade the plugin to update the backend. Official releases at or below that upstream version, and managed backends an older plugin installed, are upgraded on the next managed startup, preserving accounts, port and sign-in credentials.
150
+ - The plugin pins SHA-256-verified `v0.2.16-magpie.1`, based on upstream `v0.2.16`. Upgrade the plugin to update the backend. Official releases at or below that upstream version, and managed backends an older plugin installed, are upgraded on the next managed startup, preserving accounts, port and sign-in credentials.
148
151
  - **Online updates are disabled in the managed backend on macOS, Linux, and Windows.** The admin page says updates are managed by magpie-gemini-web; the update endpoint returns HTTP 409 and update checks do not contact GitHub. Independently managed external servers are unaffected. See [docs/studio-release.md](docs/studio-release.md) for how the managed build is made and reproduced.
149
152
  - Managed Unix installs still attempt to set `package/` to `0555` as additional file protection. Permission failures do not block service, and disabling online updates does not depend on permissions. To reset by manually deleting the data directory, stop the service first, then run `chmod -R u+w "<data-dir>/package"` before deleting it; replace `<data-dir>` with the actual path.
150
153
  - If a newer or custom version is installed, the plugin reports a conflict with `stop`, `install`, and `start` commands carrying the correct `--data-dir`. Stop can recover a changed listener PID and verifies its executable path before signaling it. If HTTP is down but the process is still alive, the plugin refuses to reinstall or launch a second process.
@@ -154,11 +157,11 @@ When a plugin error asks you to run a command, it includes the full ready-to-cop
154
157
  - Models with `-build` or `assistant_route` are filtered out: they inject the Build coding assistant's own system prompt and are not offered as ordinary Agent models. Refresh with `magpie provider models ai-studio` after upgrading.
155
158
 
156
159
  **Gemini Web optimized backend upgrades & rollback:**
157
- - One-click setup uses [4.22.0-magpie.1](https://github.com/BUKOWSKIREAL/magpie-gemini-web/releases/tag/backend-v4.22.0-magpie.1) by default (built from pinned upstream v4.22.0 with XSRF caching, request cancellation propagation, early tool-call completion, and Argon model verification).
160
+ - One-click setup uses [4.26.0-magpie.1](https://github.com/BUKOWSKIREAL/magpie-gemini-web/releases/tag/backend-v4.26.0-magpie.1) by default (built from pinned upstream v4.26.0 with XSRF caching, request cancellation propagation, early tool-call completion, and Argon model verification).
158
161
  - **Upgrading:** Running backends are not interrupted automatically. Stop the existing process (PID in `backend.pid` inside the data directory); the next Magpie launch or request will upgrade the binary while preserving config and cookies.
159
162
  - **Rollback to upstream / restore optimized build** (run in the plugin directory after stopping the backend):
160
163
  ```bash
161
- npm run backend:install:upstream # Roll back and pin to official v4.22.0
164
+ npm run backend:install:upstream # Roll back and pin to official v4.26.0
162
165
  npm run backend:install # Restore default optimized build
163
166
  ```
164
167
  - For reproducible builds and release procedures, see [docs/backend-release.md](docs/backend-release.md).
package/README.md CHANGED
@@ -14,10 +14,10 @@
14
14
  | :--- | :--- | :--- |
15
15
  | **适用场景** | **推荐 Coding Agent 使用** | 日常对话、使用网页版专属模型与额度 |
16
16
  | **工具调用** | **原生 Function Calling**,稳定可靠 | 提示词模拟(实验性,Flash 系列常不调工具) |
17
- | **思考档位** | `minimal` / `low` / `medium` / `high` 四档(`minimal` 最快最省) | `low`(普通)/ `high`(`-thinking` 扩展思考)两档 |
17
+ | **思考档位** | `minimal` / `low` / `medium` / `high` 四档(`minimal` 最快最省) | `low`(普通)/ `medium`(`-thinking-medium`,仅 `gemini-4-argon`)/ `high`(`-thinking` 扩展思考) |
18
18
  | **登录方式** | 弹出独立浏览器窗口直接登录 Google | 用无痕窗口登录并复制 `cookie` |
19
19
  | **额度卡片** | 显示冷却中的模型与恢复时间 | 同步网页版 **5 小时** 与 **每周** 用量及重置时间 |
20
- | **本地后端** | AIStudio2API (`v0.2.14`) | gemini-web2api-go (`4.22.0-magpie.1` 优化版) |
20
+ | **本地后端** | AIStudio2API (`v0.2.16`) | gemini-web2api-go (`4.26.0-magpie.1` 优化版) |
21
21
 
22
22
  ---
23
23
 
@@ -34,7 +34,7 @@
34
34
 
35
35
  ### 方式二:Gemini Web (`gemini-web`)
36
36
 
37
- 1. 在 Magpie 中登录 **Gemini Web**,选择 **「一键配置本地 Gemini Web(推荐)」**。
37
+ 1. 在 Magpie 中登录 **Gemini Web**,选择 **「一键配置本地 Gemini Web(推荐)」**(若访问 Google 需要代理,可在第一步提示中直接填写代理地址,例如 `http://127.0.0.1:7890`,无需代理或已配置代理池可留空)。
38
38
  2. 打开浏览器**无痕 / 隐私窗口**(推荐 Firefox)获取 Cookie:
39
39
  1. 访问 <https://gemini.google.com/app> 并登录 Google 账号;
40
40
  2. 按 `F12`(macOS 按 `⌥⌘I`)切到 **Network(网络)**,刷新页面;
@@ -42,6 +42,8 @@
42
42
  4. **直接关闭无痕窗口,不要点退出登录**(点退出会使 Cookie 失效)。
43
43
  3. 插件验证账号可用后即完成登录。想添加更多账号,重复上述步骤即可。
44
44
 
45
+ > **升级后提示后端不支持登录时绑定代理?** 旧后台进程可能仍在运行。请按提示先退出 Magpie,停止对应的 `gemini-web2api-go` 进程(Windows:任务管理器 → 详细信息 → `gemini-web2api-go.exe`),再运行提示中的完整安装命令。它会保留账号和配置,并切换到插件的托管后端;如下载需要代理,先设置 `GEMINI_WEB_PROXY`。完成后重新打开 Magpie 登录。
46
+
45
47
  > **为什么必须用无痕窗口?** 后端会定时刷新会话来保活。若与日常浏览器共用同一登录会话,Google 会将整个会话作废,导致你的浏览器退出所有 Google 服务。使用无痕窗口(或单独准备 Google 小号)可提供独立会话。
46
48
 
47
49
  ---
@@ -84,6 +86,7 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
84
86
  | `allowImages` | `false` | 共用(可用 `studio.allowImages` 单独覆盖) | 允许图片输入 |
85
87
  | `requestTimeoutMs` / `modelTimeoutMs` | Web:`240000` / `10000`;Studio:`1800000` / `10000` | 共用(`studio.*` 可覆盖) | 单次等待后端 / 拉取模型列表的超时(毫秒);收到流数据后重新计时,显式配置优先 |
86
88
  | `stripCitations` | `true` | 仅 `gemini-web` | 剥离回复与工具参数中的 `[cite: 1]`、`[cite_start]` 标记(AI Studio 不支持剥离) |
89
+ | `proxy` | 无 | 仅 `gemini-web` | 一键配置后端访问 Google 的代理(`http` / `https` / `socks5` / `socks5h`),也可用环境变量 `GEMINI_WEB_PROXY` 或在登录第一步填写 |
87
90
  | `baseURL` | `http://127.0.0.1:8083/v1` | 仅 `gemini-web` | Gemini Web 默认后端地址 |
88
91
  | `contextLimit` / `outputLimit` | `1000000` / `8192` | 仅 `gemini-web` | 向客户端声明的上下文 / 输出上限 |
89
92
  | `studio.proxy` | 无 | 仅 `ai-studio` | 访问 Google 的代理(`http` / `https` / `socks5`),也可用环境变量 `AI_STUDIO_PROXY`;设置后优先于管理页里保存的代理,留空等于不设置 |
@@ -97,7 +100,7 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
97
100
  - **后端不使用 Magpie 的代理(包括给 `ai-studio` 设置的供应商代理),也不读取 `HTTPS_PROXY` 等环境变量**:它在 Magpie 退出后仍会运行,借用 Magpie 的代理到时可能已经失效。请用上面两种方式之一设置。加入这项修复之前的插件版本启动的后端,可能还在用启动时环境变量里的代理(比如 Magpie 的);「代理出口」一行会标出来,并说明重启后会怎样。
98
101
  - 额度卡片的「代理出口」一行会显示后端当前实际使用的代理、它来自哪里,以及新设置是否还要重启才生效;登录 Google 时的说明、登录失败的提示和 `npm run studio:status` 也会显示。
99
102
  - 监听地址、API Key、管理员账号等由插件管理,在管理页修改不会生效。
100
- - **Gemini Web**:后端不读取 `HTTP_PROXY`,请打开本地管理面板 `http://127.0.0.1:8083/admin` 在代理池中添加(登录口令为数据目录 `secrets.json` 中的 `adminToken`)。
103
+ - **Gemini Web**:后端不读取系统代理或 `HTTP_PROXY`。可在「一键配置本地 Gemini Web」登录第一步直接填写代理地址,或设置插件选项 `proxy` / 环境变量 `GEMINI_WEB_PROXY`,也可以打开本地管理面板 `http://127.0.0.1:8083/admin` 在代理池中添加(登录口令为数据目录 `secrets.json` 中的 `adminToken`)。
101
104
  - *注意*:配置代理池后,新添加的账号需先发送一次请求以绑定代理,再通过「添加本地后端里已有的 Google 账号」重新添加,才能正常读取邮箱和用量(不支持通过 SOCKS 代理读取用量)。
102
105
 
103
106
  **连接自建后端:**
@@ -144,7 +147,7 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
144
147
  - 插件将请求并发限制为 1,但仍无法完全排除 Google 账号风控风险,请自行评估使用。
145
148
 
146
149
  **AI Studio 升级与模型范围:**
147
- - 插件固定使用经过 SHA-256 校验的 `v0.2.14-magpie.1`(基于上游 `v0.2.14`),通过升级插件更新后端。同基线或更旧的官方版、以及旧版插件装的托管后端,在下次管理启动时自动升级,保留账号、端口与登录凭据。
150
+ - 插件固定使用经过 SHA-256 校验的 `v0.2.16-magpie.1`(基于上游 `v0.2.16`),通过升级插件更新后端。同基线或更旧的官方版、以及旧版插件装的托管后端,在下次管理启动时自动升级,保留账号、端口与登录凭据。
148
151
  - **托管后端已在 macOS、Linux、Windows 统一禁用在线更新**:管理页提示“更新由 magpie-gemini-web 管理”,更新接口返回 HTTP 409,检查更新也不会访问 GitHub。用户自行连接的外部服务不受影响。托管构建方式和可复现构建流程见 [docs/studio-release.md](docs/studio-release.md)。
149
152
  - Unix 的 `package/` 仍尽力设置为 `0555`,作为额外的文件保护;权限设置失败不阻断服务,在线更新的禁用也不依赖该权限。若要手动删除数据目录来重置,请先停止服务,再运行 `chmod -R u+w "<数据目录>/package"` 恢复可写权限,然后删除;将 `<数据目录>` 替换为实际路径。
150
153
  - 若自行更新到更新版或定制版,插件会提示版本冲突,并提供带正确 `--data-dir` 的 `stop`、`install`、`start` 命令。停止时会找回更新后的监听进程 PID,并核对可执行文件路径;无法核实身份就不会发送终止信号。HTTP 端口关闭但进程尚未退出时,插件不会重装或启动第二个进程。
@@ -154,11 +157,11 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
154
157
  - 模型列表过滤 `-build` / `assistant_route` 模型:这类模型附带 Build 写代码助手的系统提示词,不作为普通 Agent 模型提供。升级后可执行 `magpie provider models ai-studio` 刷新列表。
155
158
 
156
159
  **Gemini Web 优化后端升级与回退:**
157
- - 插件默认使用本仓库发布的 [4.22.0-magpie.1](https://github.com/BUKOWSKIREAL/magpie-gemini-web/releases/tag/backend-v4.22.0-magpie.1)(基于上游 v4.22.0 集成 XSRF 缓存、请求取消传播、工具调用提前收尾与 Argon 校验补丁)。
160
+ - 插件默认使用本仓库发布的 [4.26.0-magpie.1](https://github.com/BUKOWSKIREAL/magpie-gemini-web/releases/tag/backend-v4.26.0-magpie.1)(基于上游 v4.26.0 集成 XSRF 缓存、请求取消传播、工具调用提前收尾与 Argon 校验补丁)。
158
161
  - **旧版升级**:插件不会打断运行中的后端。先停止旧进程(PID 见数据目录 `backend.pid`),下次启动 Magpie 或发请求时会自动更新二进制并保留原有配置与账号。
159
162
  - **回退官方版 / 恢复优化版**(停止后端后在插件目录执行):
160
163
  ```bash
161
- npm run backend:install:upstream # 回退并固定使用官方 v4.22.0
164
+ npm run backend:install:upstream # 回退并固定使用官方 v4.26.0
162
165
  npm run backend:install # 恢复默认优化版
163
166
  ```
164
167
  - 源码复现构建与发布流程见 [docs/backend-release.md](docs/backend-release.md)。
@@ -28,4 +28,6 @@ The metadata records `CGO_ENABLED=0`, the exact Go toolchain, upstream commit, p
28
28
 
29
29
  Existing running processes are left alone to avoid interrupting requests. Once an old official backend is stopped, automatic startup installs the optimized build while preserving configuration, secrets and the database. A download or checksum failure keeps the old file and never starts an unverified replacement.
30
30
 
31
- An explicit `npm run backend:install:upstream` installs the pinned upstream v4.22.0 and persists `upstream` in `backend-channel.json`. Automatic startup respects that choice. `npm run backend:install` returns to the optimized channel. Stop a running backend before replacing it, particularly on Windows. Explicit local Argon builds remain trusted through their existing build manifest.
31
+ New cookie imports require the running backend to advertise `capabilities.account_proxy_binding: true` at `/`. The plugin checks this before changing the proxy pool and again before submitting a cookie, so a reused older or upstream backend cannot silently ignore `proxy_id`. Unsupported builds receive instructions to stop the old process and run the installed plugin's CLI with `install --data-dir=<path>`. This explicitly selects the optimized channel, including for previous upstream or custom-build installations. The check does not interrupt existing conversations. Release smoke verifies the capability and actual proxy selection; version strings alone do not establish support.
32
+
33
+ An explicit `npm run backend:install:upstream` installs the pinned upstream v4.26.0 and persists `upstream` in `backend-channel.json`. Automatic startup respects that choice. `npm run backend:install` returns to the optimized channel. Stop a running backend before replacing it, particularly on Windows. Explicit local Argon builds remain trusted through their existing build manifest.
@@ -1,12 +1,12 @@
1
1
  # AI Studio 托管后端构建
2
2
 
3
- 插件使用 `0.2.14-magpie.1`,基于 AIStudio2API `v0.2.14` 的固定 commit。上游版本、commit、Go 工具链、安装包及二进制 SHA-256 均记录在 `lib/studio-release.json`。`upstream.camoufox` 是这个 commit 要求的 Camoufox 版本(上游 `internal/camoufoxnative/download.go` 里的 `camoufoxRelease`),插件靠它判断登录时要不要重新下载 Camoufox;更新上游 commit 时要一起改,构建脚本发现不一致会报错。MIT 许可见 `LICENSE-AISTUDIO`。
3
+ 插件使用 `0.2.16-magpie.1`,基于 AIStudio2API `v0.2.16` 的固定 commit。上游版本、commit、Go 工具链、安装包及二进制 SHA-256 均记录在 `lib/studio-release.json`。`upstream.camoufox` 是这个 commit 要求的 Camoufox 版本(上游 `internal/camoufoxnative/download.go` 里的 `camoufoxRelease`),插件靠它判断登录时要不要重新下载 Camoufox;更新上游 commit 时要一起改,构建脚本发现不一致会报错。MIT 许可见 `LICENSE-AISTUDIO`。
4
4
 
5
5
  ## 在线更新策略
6
6
 
7
7
  上游从 v0.2.14 起支持由分发方管理更新,不再需要补丁。构建时通过 `-ldflags` 写入两个值:
8
8
 
9
- - `internal/app.version=v0.2.14-magpie.1`:明确的版本标识。插件把同基线或更旧的官方版、以及更旧的托管版(`-magpie.N` 的基线更低,或同基线但 N 更小)视为需要升级;更新的官方版、更新的托管版或不认识的定制版仍提示冲突。
9
+ - `internal/app.version=v0.2.16-magpie.1`:明确的版本标识。插件把同基线或更旧的官方版、以及更旧的托管版(`-magpie.N` 的基线更低,或同基线但 N 更小)视为需要升级;更新的官方版、更新的托管版或不认识的定制版仍提示冲突。
10
10
  - `internal/app.managedBy=magpie-gemini-web`:更新模式为 `managed`。`GET /api/update`(包括 `?refresh=1`)只返回本地版本、`mode: managed` 和 `managed_by`,不访问 GitHub;`POST /api/update` 返回 HTTP 409,说明更新由 magpie-gemini-web 管理,不下载、不替换文件、不重启。这两个值在链接时写死,没有环境变量或管理页开关,在所有构建平台相同。
11
11
  - 管理页版本面板显示当前版本和"更新由 magpie-gemini-web 管理",不显示检查更新或安装按钮。上游的在线更新代码仍然编译在内,但托管模式下走不到;`studio:managed:smoke` 用真实二进制验证这一点。
12
12
  - 构建不带 `desktop` 标签,即上游的命令行版,不包含桌面窗口。
@@ -30,7 +30,7 @@ node scripts/studio-release.mjs --write-pins
30
30
  node scripts/studio-release.mjs --source=/path/to/AIStudio2API --verify
31
31
  ```
32
32
 
33
- 输出在 `studio-build/release/`,不进入 npm 包。包含 macOS x64/arm64、Linux x64/arm64、Windows x64 安装包,以及构建记录、SHA256SUMS 和上游许可证。每个安装包携带许可证和托管升级说明。Go 的标准 tar/zip/gzip 写入器固定文件顺序、权限与时间;编译使用 `-trimpath -buildvcs=false`。
33
+ 输出在 `studio-build/release/`,不进入 npm 包。包含 macOS x64/arm64、Linux x64/arm64、Windows x64 安装包,以及构建记录、SHA256SUMS 和上游许可证。每个安装包携带许可证和托管升级说明。Go 的标准 tar/zip/gzip 写入器固定文件顺序、权限与时间;编译使用 `-trimpath -buildvcs=false`。上游前端构建用 Node 的 zlib 预压缩网页资源(`.gz`),其输出随 CPU 和系统不同,而这些资源嵌入二进制;构建脚本在前端构建后用 `scripts/studio-regzip.go` 以 Go 的 gzip 重新压缩,使各平台构建结果一致。
34
34
 
35
35
  ## 验证与发布
36
36
 
@@ -44,6 +44,6 @@ AI_STUDIO_TEST_ARCHIVE=/absolute/path/to/archive.tar.gz npm test
44
44
 
45
45
  真实安装验证检查启动、版本、GET 更新状态、POST 更新返回 409、二进制及 PID 不变、管理页面显示托管提示、停止与状态。`.github/workflows/studio.yml` 在五种系统/架构上重新构建、对比 SHA-256 并运行该验证;本机交叉编译不代替 Windows 真机验证。
46
46
 
47
- 发布时先审查构建记录和各平台 CI 结果,再将安装包及许可证上传到本仓库 `studio-v0.2.14-magpie.1` release。该 release 可公开下载后,才能发布引用它的插件版本,否则新安装会遇到 404。构建脚本及 CI 不自动发布 release,也不发布 npm 包。
47
+ 发布时先审查构建记录和各平台 CI 结果,再将安装包及许可证上传到本仓库 `studio-v0.2.16-magpie.1` release。该 release 可公开下载后,才能发布引用它的插件版本,否则新安装会遇到 404。构建脚本及 CI 不自动发布 release,也不发布 npm 包。
48
48
 
49
- 本地试用无需发布:先停止当前托管后端,再执行 `node scripts/studio.mjs install --archive /absolute/path/to/archive` 和 `node scripts/studio.mjs start`。可用 `--data-dir` 指定目录。修改前备份账号和配置;该版本与上游 v0.2.14 使用相同的数据格式。v0.2.14 对数据的改动都是新增的(账号记录的可选字段、新的 `runtime/prompts.db`、`.env` 的新设置),用真实账号验证过:`v0.2.13-magpie.1` 的数据可以直接被它读取,用它运行过之后退回 `v0.2.13-magpie.1` 也能正常使用。
49
+ 本地试用无需发布:先停止当前托管后端,再执行 `node scripts/studio.mjs install --archive /absolute/path/to/archive` 和 `node scripts/studio.mjs start`。可用 `--data-dir` 指定目录。修改前备份账号和配置;该版本与上游 v0.2.16 使用相同的数据格式。
package/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { normalizeBaseURL, settings, credentials, listModels, forward, UNREACHABLE_HEADER } from './lib/backend.mjs';
2
2
  import { ensureBackend, localBackend } from './lib/native.mjs';
3
- import { parseCookieInput, importCookie } from './lib/setup.mjs';
3
+ import { parseCookieInput, importCookie, ensureProxy, validateProxyURL, requireLoginBackend } from './lib/setup.mjs';
4
4
  import { usage, poolChoices, poolAccounts, accountEmail } from './lib/usage.mjs';
5
5
  import { taskSupported, taskMethod, taskLoader, taskModels, isTaskAuth } from './lib/task.mjs';
6
6
 
@@ -55,7 +55,7 @@ async function forwardManaged(input, init, getAuth, opts) {
55
55
  }
56
56
  if (!managedBackend(c.baseURL)) return res;
57
57
  try {
58
- await ensureBackend();
58
+ await ensureBackend({ proxy: opts.proxy });
59
59
  } catch {
60
60
  return res;
61
61
  }
@@ -68,11 +68,30 @@ export const GeminiWebPlugin = async (_input, options = {}) => {
68
68
  }
69
69
  const { studio: _studio, ...webOptions } = options;
70
70
  const opts = settings(webOptions);
71
+ const proxyPrompt = {
72
+ type: 'text',
73
+ key: 'proxy',
74
+ message: '访问 Google 的代理地址(无需代理或已在代理池配置可留空)',
75
+ placeholder: opts.proxy || 'http://127.0.0.1:7890',
76
+ validate: value => {
77
+ try {
78
+ validateProxyURL(value);
79
+ } catch (e) {
80
+ return e.message;
81
+ }
82
+ }
83
+ };
71
84
 
72
85
  // Bring a backend set up earlier back after Magpie or the computer restarts. Not awaited:
73
86
  // requests that arrive first start it themselves through forwardManaged.
74
87
  if (localBackend()) {
75
- ensureBackend().catch(err => console.error(`[gemini-web] could not start the local backend: ${err.message}`));
88
+ ensureBackend({ proxy: opts.proxy })
89
+ .then(async local => {
90
+ if (!opts.proxy) return;
91
+ await requireLoginBackend(local);
92
+ await ensureProxy(local, opts.proxy);
93
+ })
94
+ .catch(err => console.error(`[gemini-web] could not start the local backend: ${err.message}`));
76
95
  }
77
96
 
78
97
  return {
@@ -86,20 +105,29 @@ export const GeminiWebPlugin = async (_input, options = {}) => {
86
105
  methods: [{
87
106
  type: 'oauth',
88
107
  label: '一键配置本地 Gemini Web(推荐)',
89
- // Installs and starts the pinned backend, then takes the Gemini cookie the user pastes.
108
+ prompts: [proxyPrompt],
109
+ ask(inputs) {
110
+ return inputs && 'proxy' in inputs ? null : proxyPrompt;
111
+ },
112
+ // Installs and starts the pinned backend while the user copies the Gemini cookie; the
113
+ // callback then waits for it, configures its proxy pool when a proxy URL is given, and
114
+ // imports the cookie (importCookie checks the backend can bind the account to a proxy).
90
115
  // The result carries the backend's own API key, so Magpie saves an API-key sign-in,
91
116
  // one for each Google account (see signInFor).
92
- async authorize() {
93
- const local = await ensureBackend();
117
+ async authorize(inputs) {
118
+ const proxy = validateProxyURL(inputs?.proxy || opts.proxy || '');
119
+ const ready = ensureBackend({ proxy });
120
+ ready.catch(() => {});
94
121
  return {
95
122
  url: GEMINI_URL,
96
123
  method: 'code',
97
124
  instructions: COOKIE_STEPS,
98
125
  async callback(code) {
99
- let cookie, added;
126
+ let local, cookie, added;
100
127
  try {
101
128
  cookie = parseCookieInput(code);
102
- added = await importCookie(local, cookie);
129
+ local = await ready;
130
+ added = await importCookie(local, cookie, proxy ? { proxy } : {});
103
131
  } catch (err) {
104
132
  return { type: 'failed', error: err.message };
105
133
  }
@@ -206,7 +234,7 @@ export const GeminiWebPlugin = async (_input, options = {}) => {
206
234
  return await listModels(c, opts);
207
235
  } catch (err) {
208
236
  if (!err.unreachable || !managedBackend(c.baseURL)) throw err;
209
- await ensureBackend();
237
+ await ensureBackend({ proxy: opts.proxy });
210
238
  return listModels(c, opts);
211
239
  }
212
240
  }
@@ -1,36 +1,36 @@
1
1
  {
2
- "version": "4.22.0-magpie.1",
2
+ "version": "4.26.0-magpie.1",
3
3
  "repository": "BUKOWSKIREAL/magpie-gemini-web",
4
- "tag": "backend-v4.22.0-magpie.1",
4
+ "tag": "backend-v4.26.0-magpie.1",
5
5
  "upstream": {
6
6
  "repository": "https://github.com/zexadev/gemini-web2api-go.git",
7
- "commit": "a65b1d1a5ac614c1b455857334b4f1c73ab8fcfb"
7
+ "commit": "7ce02d89512058b2bd147fb2dc4e40187f5135ad"
8
8
  },
9
9
  "goVersion": "1.27.1",
10
10
  "binaries": {
11
11
  "darwin-arm64": {
12
- "asset": "gemini-web2api-go_v4.22.0-magpie.1_darwin_arm64",
13
- "sha256": "833dd54e7c0df927afc1d4e0f6443bcc03b7f0a4d04d24cdfde47d5dd27349c5"
12
+ "asset": "gemini-web2api-go_v4.26.0-magpie.1_darwin_arm64",
13
+ "sha256": "ae2581dfba30d7566c2c55cd53b0b65c56aab5e5f592ccac983a65c4444f069e"
14
14
  },
15
15
  "darwin-x64": {
16
- "asset": "gemini-web2api-go_v4.22.0-magpie.1_darwin_amd64",
17
- "sha256": "d65b053a0e5f8e1065ea35835b3a7d12f783d97964c02bef49679df7ead92fd4"
16
+ "asset": "gemini-web2api-go_v4.26.0-magpie.1_darwin_amd64",
17
+ "sha256": "e220a5c53284c0c17ca9bc1a3e454448c95b84155fc460c17e0c8fcbae3b9d88"
18
18
  },
19
19
  "linux-x64": {
20
- "asset": "gemini-web2api-go_v4.22.0-magpie.1_linux_amd64",
21
- "sha256": "94f082e666b34051f191959de3f2cd601078d117b1d3ededcdcd6b2201c48ef9"
20
+ "asset": "gemini-web2api-go_v4.26.0-magpie.1_linux_amd64",
21
+ "sha256": "cbbb9784823301f4f90e44d33a087d4d745770f4f35d77735fbae370087a7a43"
22
22
  },
23
23
  "linux-arm64": {
24
- "asset": "gemini-web2api-go_v4.22.0-magpie.1_linux_arm64",
25
- "sha256": "4a7cbca2771acc9e836a5a986e11b3b247baf77e9900262cb588b515a1161aaa"
24
+ "asset": "gemini-web2api-go_v4.26.0-magpie.1_linux_arm64",
25
+ "sha256": "cf503f30ed3cfd358de926cf58b455b9ce7951dfa51b24584bd11559e9cfe7c0"
26
26
  },
27
27
  "win32-x64": {
28
- "asset": "gemini-web2api-go_v4.22.0-magpie.1_windows_amd64.exe",
29
- "sha256": "61d279d50c6c09ffe81ed059a557077900948d0ae598e4c4b3a5d5c213ad5e5c"
28
+ "asset": "gemini-web2api-go_v4.26.0-magpie.1_windows_amd64.exe",
29
+ "sha256": "e0d0bc52acd1c0c7dc7d1bdc8eb3da09dee2be8ed9a36173ab3be1bf4c58f667"
30
30
  },
31
31
  "win32-arm64": {
32
- "asset": "gemini-web2api-go_v4.22.0-magpie.1_windows_arm64.exe",
33
- "sha256": "20bad088b114d3dc989830ff35f7e56c1e0026caf9721b739fe982f5fd3d1880"
32
+ "asset": "gemini-web2api-go_v4.26.0-magpie.1_windows_arm64.exe",
33
+ "sha256": "d038049af66365f0a6ab634664ca3d3715dd26e42d67f4c7f0b4f2ccba64f143"
34
34
  }
35
35
  }
36
36
  }
package/lib/backend.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { citeStream, citeJSON } from './cite.mjs';
2
+ import { validateProxyURL } from './setup.mjs';
2
3
 
3
4
  export const DEFAULT_BASE = 'http://127.0.0.1:8083/v1';
4
5
 
@@ -6,11 +7,13 @@ const MEDIA_SPECIAL = /(?:^|[-_])(image|music|canvas|video|research)(?:[-_]|$)/i
6
7
  const CONTROL_CHARS = /[\x00-\x1f\x7f]/;
7
8
  const NEWLINE_CHARS = /[\r\n]/;
8
9
 
9
- // The backend has no effort parameter: extended thinking is a separate "<id>-thinking" model.
10
- // A base model with such a twin offers low (itself) and high (the twin), and the twin isn't
11
- // listed on its own; a request that names it still goes through. Magpie maps any other level to
12
- // the nearest declared one before it reaches us; direct callers get the same rule.
10
+ // The backend has no effort parameter: extended thinking uses separate "<id>-thinking-medium"
11
+ // and "<id>-thinking" models. A base model with such twins offers low (itself), medium (when
12
+ // "<id>-thinking-medium" exists), and high ("<id>-thinking"), while the twins aren't listed on
13
+ // their own; a request that names one directly still goes through. Magpie maps any other level
14
+ // to the nearest declared one before it reaches us; direct callers get the same rule.
13
15
  const THINKING_SUFFIX = '-thinking';
16
+ const MEDIUM_THINKING_SUFFIX = '-thinking-medium';
14
17
  const LOW_EFFORTS = new Set(['none', 'minimal', 'low']);
15
18
  const MODEL_CACHE_MS = 10 * 60 * 1000;
16
19
  const knownModels = new Map();
@@ -21,6 +24,7 @@ export const UNREACHABLE_HEADER = 'X-Gemini-Web-Backend';
21
24
 
22
25
  const KNOWN_OPTIONS = new Set([
23
26
  'baseURL',
27
+ 'proxy',
24
28
  'allowTools',
25
29
  'allowImages',
26
30
  'contextLimit',
@@ -115,6 +119,13 @@ export function settings(options = {}) {
115
119
  baseURL = normalizeBaseURL(options.baseURL);
116
120
  }
117
121
 
122
+ let proxy = '';
123
+ if (options.proxy !== undefined) {
124
+ proxy = validateProxyURL(options.proxy);
125
+ } else if (process.env.GEMINI_WEB_PROXY) {
126
+ proxy = validateProxyURL(process.env.GEMINI_WEB_PROXY);
127
+ }
128
+
118
129
  // On by default: coding agents send tools with every request and don't read toolcall,
119
130
  // so turning this off only means every one of their requests gets a 400.
120
131
  let allowTools = true;
@@ -181,6 +192,7 @@ export function settings(options = {}) {
181
192
 
182
193
  return {
183
194
  baseURL,
195
+ proxy,
184
196
  allowTools,
185
197
  allowImages,
186
198
  contextLimit,
@@ -289,7 +301,16 @@ export async function listModels(c, opts) {
289
301
  const result = Object.create(null);
290
302
  for (const id of validIds) {
291
303
  if (id.endsWith(THINKING_SUFFIX) && ids.has(id.slice(0, -THINKING_SUFFIX.length))) continue;
292
- const hasTwin = !id.endsWith(THINKING_SUFFIX) && ids.has(id + THINKING_SUFFIX);
304
+ if (id.endsWith(MEDIUM_THINKING_SUFFIX) && ids.has(id.slice(0, -MEDIUM_THINKING_SUFFIX.length))) continue;
305
+ const isTwin = id.endsWith(THINKING_SUFFIX) || id.endsWith(MEDIUM_THINKING_SUFFIX);
306
+ const hasMedium = !isTwin && ids.has(id + MEDIUM_THINKING_SUFFIX);
307
+ const hasHigh = !isTwin && ids.has(id + THINKING_SUFFIX);
308
+ const variants = {};
309
+ if (hasMedium || hasHigh) {
310
+ variants.low = {};
311
+ if (hasMedium) variants.medium = {};
312
+ if (hasHigh) variants.high = {};
313
+ }
293
314
  result[id] = {
294
315
  id,
295
316
  providerID: 'gemini-web',
@@ -326,7 +347,7 @@ export async function listModels(c, opts) {
326
347
  },
327
348
  options: {},
328
349
  headers: {},
329
- variants: hasTwin ? { low: {}, high: {} } : {},
350
+ variants,
330
351
  status: 'active'
331
352
  };
332
353
  }
@@ -345,7 +366,7 @@ async function thinkingTwin(body, c, opts) {
345
366
  const effort = body.reasoning_effort;
346
367
  const model = body.model;
347
368
  if (typeof effort !== 'string' || LOW_EFFORTS.has(effort)) return null;
348
- if (typeof model !== 'string' || model.endsWith(THINKING_SUFFIX)) return null;
369
+ if (typeof model !== 'string' || model.endsWith(THINKING_SUFFIX) || model.endsWith(MEDIUM_THINKING_SUFFIX)) return null;
349
370
 
350
371
  let cached = knownModels.get(modelCacheKey(c));
351
372
  if (!cached || Date.now() - cached.at > MODEL_CACHE_MS) {
@@ -356,6 +377,9 @@ async function thinkingTwin(body, c, opts) {
356
377
  }
357
378
  cached = knownModels.get(modelCacheKey(c));
358
379
  }
380
+ if (effort === 'medium' && cached?.ids.has(model + MEDIUM_THINKING_SUFFIX)) {
381
+ return model + MEDIUM_THINKING_SUFFIX;
382
+ }
359
383
  const twin = model + THINKING_SUFFIX;
360
384
  return cached?.ids.has(twin) ? twin : null;
361
385
  }
@@ -535,12 +559,14 @@ export async function forward(input, init, getAuth, opts) {
535
559
  let upstreamRes;
536
560
  timer.start();
537
561
  try {
562
+ // Bun's fetch drops a connection idle for 300 s; the timer sets the deadline instead.
538
563
  upstreamRes = await fetch(incomingUrl, {
539
564
  method: req.method,
540
565
  headers,
541
566
  body: rawBody,
542
567
  signal,
543
- redirect: 'error'
568
+ redirect: 'error',
569
+ timeout: false
544
570
  });
545
571
  } catch (err) {
546
572
  if (req.signal.aborted) {
package/lib/native.mjs CHANGED
@@ -2,23 +2,24 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  import crypto from 'node:crypto';
5
- import { spawn } from 'node:child_process';
5
+ import { spawn, execFile } from 'node:child_process';
6
+ import { validateProxyURL } from './setup.mjs';
6
7
  import release from './backend-release.json' with { type: 'json' };
7
8
 
8
9
  export const BACKEND_VERSION = `v${release.version}`;
9
10
  const RELEASE_BASE = `https://github.com/${release.repository}/releases/download/${release.tag}/`;
10
11
  export const PINNED_BINARIES = release.binaries;
11
12
 
12
- const UPSTREAM_VERSION = 'v4.22.0';
13
+ const UPSTREAM_VERSION = 'v4.26.0';
13
14
  const UPSTREAM_BASE = `https://github.com/zexadev/gemini-web2api-go/releases/download/${UPSTREAM_VERSION}/`;
14
15
  // Retained for an explicit, checksum-verified rollback to the upstream release.
15
16
  export const UPSTREAM_BINARIES = {
16
- 'darwin-arm64': { asset: 'gemini-web2api-go_v4.22.0_darwin_arm64', sha256: 'c69ca75cf43017cf2a62dbf5c12b07f6bde66f403f714c32209b1ad350ed24c9' },
17
- 'darwin-x64': { asset: 'gemini-web2api-go_v4.22.0_darwin_amd64', sha256: '384e617bcc30aacac6b7c4e4d0490e88d282b13b29f7a0a20d645b69514cd405' },
18
- 'linux-x64': { asset: 'gemini-web2api-go_v4.22.0_linux_amd64', sha256: '21fcc81e0ad8ce0921e6b6c2cafce1089852530142b37e0289355c6e72df5299' },
19
- 'linux-arm64': { asset: 'gemini-web2api-go_v4.22.0_linux_arm64', sha256: 'f2822fa0f1a7d2b37c2bc9cbb30a95c2e8028462453a110389511408bdfd4fd7' },
20
- 'win32-x64': { asset: 'gemini-web2api-go_v4.22.0_windows_amd64.exe', sha256: '17e37f7e5c813b2a8d90c9a0265e6cde3bcfa4e814822af999fe8c4178f02729' },
21
- 'win32-arm64': { asset: 'gemini-web2api-go_v4.22.0_windows_arm64.exe', sha256: 'b42e951bfc08d7fe8bdb436dca87fc6a1b7948bb8d10e110653bcf13a790756c' }
17
+ 'darwin-arm64': { asset: 'gemini-web2api-go_v4.26.0_darwin_arm64', sha256: 'cb1afabb5c5db57f99dc67b7cdf8c862a6e7aa33883cbe5355da6f696f02bc41' },
18
+ 'darwin-x64': { asset: 'gemini-web2api-go_v4.26.0_darwin_amd64', sha256: 'de4e519a65caa168b49d906c73e9012a0b8f09ec80978dc8d3efcb674bb28c8f' },
19
+ 'linux-x64': { asset: 'gemini-web2api-go_v4.26.0_linux_amd64', sha256: '2c0c4cb73989a34ca76a9e75c79e0e8684c6d7abc58231c65ef1eca4c411be81' },
20
+ 'linux-arm64': { asset: 'gemini-web2api-go_v4.26.0_linux_arm64', sha256: '5518a5f055e025b118d327b361f8c9f81df319132ed5e064eea191bbe129a50a' },
21
+ 'win32-x64': { asset: 'gemini-web2api-go_v4.26.0_windows_amd64.exe', sha256: '32717fed3ed02b5c82950a01ab3a49e6724de21be8b31c7bb83e8e2e8e454128' },
22
+ 'win32-arm64': { asset: 'gemini-web2api-go_v4.26.0_windows_arm64.exe', sha256: 'ed5a4f0b6f6b0078ff2173f9b3128ed0836748e42e35ea78ff1bc6e1f456e37d' }
22
23
  };
23
24
 
24
25
  export function pinnedBinary(platform = process.platform, arch = process.arch, channel = 'optimized') {
@@ -76,6 +77,7 @@ export async function installBackend(options = {}) {
76
77
  const expectedHash = options.expectedHash || pin.sha256;
77
78
  const downloadUrl = options.url || pin.url;
78
79
  const fetchFn = options.fetchFn || globalThis.fetch;
80
+ const proxy = validateProxyURL(options.proxy ?? process.env.GEMINI_WEB_PROXY ?? '');
79
81
 
80
82
  const dataDir = options.dataDir || getDataDir();
81
83
  fs.mkdirSync(dataDir, { recursive: true, mode: 0o700 });
@@ -98,24 +100,57 @@ export async function installBackend(options = {}) {
98
100
 
99
101
  if (!binaryReady) {
100
102
  console.log(`Downloading backend binary from ${downloadUrl}...`);
101
- const res = await fetchFn(downloadUrl, {
102
- signal: AbortSignal.timeout(300000)
103
- });
104
- if (!res.ok) {
105
- throw new Error(`Failed to download binary: HTTP ${res.status}`);
106
- }
107
-
108
- const arrayBuffer = await res.arrayBuffer();
109
- const buffer = Buffer.from(arrayBuffer);
110
- const downloadedHash = crypto.createHash('sha256').update(buffer).digest('hex');
111
-
112
- if (downloadedHash !== expectedHash) {
113
- throw new Error(`SHA256 checksum mismatch: expected ${expectedHash}, got ${downloadedHash}`);
114
- }
115
-
116
103
  const tmpPath = path.join(binDir, `gemini-web2api-go.tmp.${Date.now()}.${crypto.randomBytes(4).toString('hex')}`);
117
104
  try {
118
- fs.writeFileSync(tmpPath, buffer, { mode: 0o700 });
105
+ // A slow download goes on while data keeps coming; a minute without any gives it up.
106
+ if (proxy && (!options.fetchFn || options.fetchFn === globalThis.fetch)) {
107
+ const curlArgs = [
108
+ '--fail', '--location', '--silent', '--show-error',
109
+ '--connect-timeout', '15', '--speed-limit', '1', '--speed-time', '60',
110
+ '--proxy', proxy,
111
+ '-o', tmpPath, downloadUrl
112
+ ];
113
+ await new Promise((resolve, reject) => {
114
+ execFile('curl', curlArgs, (err, _stdout, stderr) => {
115
+ if (!err) return resolve();
116
+ const status = err.code || err.status || 'failed';
117
+ const reason = String(stderr || '').trim().slice(0, 500);
118
+ reject(new Error(`Failed to download binary via curl (status: ${status})${reason ? `: ${reason}` : ''}`));
119
+ });
120
+ });
121
+ fs.chmodSync(tmpPath, 0o700);
122
+ } else {
123
+ const stall = new AbortController();
124
+ let timer;
125
+ const rearm = () => {
126
+ clearTimeout(timer);
127
+ timer = setTimeout(() => stall.abort(new Error('Failed to download binary: no data for 60 s')), 60000);
128
+ };
129
+ const chunks = [];
130
+ try {
131
+ rearm();
132
+ const res = await fetchFn(downloadUrl, {
133
+ ...(proxy ? { proxy } : {}),
134
+ signal: stall.signal
135
+ });
136
+ if (!res.ok) {
137
+ throw new Error(`Failed to download binary: HTTP ${res.status}`);
138
+ }
139
+ for await (const chunk of res.body) {
140
+ chunks.push(chunk);
141
+ rearm();
142
+ }
143
+ } finally {
144
+ clearTimeout(timer);
145
+ }
146
+ fs.writeFileSync(tmpPath, Buffer.concat(chunks), { mode: 0o700 });
147
+ }
148
+
149
+ const downloadedHash = computeFileHash(tmpPath);
150
+ if (downloadedHash !== expectedHash) {
151
+ throw new Error(`SHA256 checksum mismatch: expected ${expectedHash}, got ${downloadedHash}`);
152
+ }
153
+
119
154
  fs.renameSync(tmpPath, binPath);
120
155
  fs.chmodSync(binPath, 0o700);
121
156
  console.log(`Backend binary installed: ${binPath}`);
@@ -341,9 +376,19 @@ async function withLock(dataDir, fn, timeoutMs) {
341
376
  await sleep(300);
342
377
  }
343
378
  }
379
+ // A download has no overall deadline, so the holder keeps the lock fresh while it works;
380
+ // only a lock left by a process that is gone goes stale.
381
+ const keepFresh = setInterval(() => {
382
+ try {
383
+ const now = new Date();
384
+ fs.utimesSync(lockPath, now, now);
385
+ } catch {}
386
+ }, Math.max(1000, Math.floor(timeoutMs / 4)));
387
+ keepFresh.unref?.();
344
388
  try {
345
389
  return await fn();
346
390
  } finally {
391
+ clearInterval(keepFresh);
347
392
  fs.rmSync(lockPath, { force: true });
348
393
  }
349
394
  }
@@ -376,7 +421,7 @@ export async function ensureBackend(options = {}) {
376
421
  const trusted = trustedHashes(dataDir, platform, arch);
377
422
  const binPath = binaryPath(dataDir, platform);
378
423
  if (!fs.existsSync(binPath) || !trusted.includes(computeFileHash(binPath))) {
379
- await installBackend({ dataDir, platform, arch, fetchFn, channel: backendChannel(dataDir) });
424
+ await installBackend({ dataDir, platform, arch, fetchFn, channel: backendChannel(dataDir), proxy: options.proxy });
380
425
  } else {
381
426
  ensureDataFiles(dataDir);
382
427
  }
@@ -389,8 +434,9 @@ export async function ensureBackend(options = {}) {
389
434
  const launch = prepareLaunch(dataDir, trusted, platform);
390
435
  const logPath = path.join(dataDir, 'backend.log');
391
436
  const log = fs.openSync(logPath, 'a', 0o600);
437
+ let child;
392
438
  try {
393
- const child = spawnFn(launch.binPath, launch.args, {
439
+ child = spawnFn(launch.binPath, launch.args, {
394
440
  env: launch.env,
395
441
  detached: true,
396
442
  stdio: ['ignore', log, log],
@@ -408,6 +454,11 @@ export async function ensureBackend(options = {}) {
408
454
  if (await backendState(setup, fetchFn) === 'up') return setup;
409
455
  await sleep(300);
410
456
  }
457
+ try { child?.kill?.('SIGTERM'); } catch {}
458
+ if (child?.pid && typeof child?.kill !== 'function') {
459
+ try { process.kill(child.pid, 'SIGTERM'); } catch {}
460
+ }
461
+ fs.rmSync(path.join(dataDir, 'backend.pid'), { force: true });
411
462
  throw new Error(`Gemini Web backend did not start within ${Math.round(startTimeoutMs / 1000)}s; see ${logPath}`);
412
463
  }, 600000);
413
464
  }