magpie-gemini-web 0.4.0 → 0.4.1

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
@@ -4,10 +4,12 @@
4
4
 
5
5
  Use Gemini on the web through your Google account in [Magpie](https://usemagpie.ai/). The plugin runs the [gemini-web2api-go](https://github.com/zexadev/gemini-web2api-go) backend on your machine. The backend turns the Gemini web app into an OpenAI-compatible API.
6
6
 
7
+ > **2026-10-16 update:** Full-price Google AI Pro accounts can now use `gemini-4-argon` directly in Antigravity, and can likewise be proxied directly with Magpie.
8
+
7
9
  ## Getting started
8
10
 
9
11
  1. Install **magpie-gemini-web** from the Magpie plugin market.
10
- 2. Sign in to Gemini Web and choose **一键配置本地 Gemini Web (one-click local setup, recommended)**. The plugin downloads the backend, verifies it and starts it in the background.
12
+ 2. Sign in to Gemini Web and choose **一键配置本地 Gemini Web (one-click local setup, recommended)**. The plugin downloads this project's optimized backend, verifies its SHA-256 and starts it in the background. No Go installation or manual patching is needed.
11
13
  3. Copy your cookie when prompted. **Use a private / incognito window** (Firefox recommended):
12
14
  1. In the private window, open <https://gemini.google.com/app> and sign in to the Google account you want to use.
13
15
  2. Press F12 (⌥⌘I on macOS), switch to **Network** and reload the page.
@@ -48,7 +50,7 @@ You can switch a model between `low` and `high` effort in Magpie if it has a `-t
48
50
  ## Security and data
49
51
 
50
52
  - The backend listens on `127.0.0.1` only. Your cookie goes only to that local backend, and Magpie stores a locally generated API key instead.
51
- - The plugin pins the official v4.22.0 backend binary and checks its SHA-256 on download and on every start. Builds are available for macOS, Linux and Windows on x64 and arm64.
53
+ - The default backend is this project's **4.22.0-magpie.1**, built from pinned upstream v4.22.0 sources with this repository's patch. SHA-256 is checked on download and before launching a new process. Builds cover macOS, Linux and Windows on x64 and arm64; the release includes sources, build metadata and checksums.
52
54
  - The data directory is `~/.local/share/magpie-gemini-web` (set `GEMINI_WEB_DATA_DIR` to change it). The backend writes its log to `backend.log` there.
53
55
  - The plugin sends one request at a time, but this can't guarantee that Google won't flag your account. Use it at your own risk.
54
56
 
@@ -79,32 +81,36 @@ So with a proxy pool, an account just added by one-click setup has no email yet.
79
81
 
80
82
  On macOS, sign-in offers one more option, "macOS 原生 Task(实验性)": requests skip the Gemini Web backend and go to a local daemon that runs them in Gemini.app's own Task mode, with tool calls on its native Function Calling channel, as the model `gemini-task-argon`. Windows and Linux don't offer it; keep using one-click setup there.
81
83
 
82
- This is an alpha: it supports only the verified Gemini.app build, needs a local setup first (Gemini.app signed in, plus session templates), hasn't passed an end-to-end check against real Argon yet, and shows no usage. See [native-task/README.md](native-task/README.md) for setup, session requirements and limitations.
84
+ This is an alpha: it supports only the verified Gemini.app build, needs a local setup first (Gemini.app signed in, plus session templates), has passed only one live tool round trip against real Argon, and shows no usage; if an agent is interrupted before it returns a tool result, that call holds the only Task slot for up to about 4 minutes. See [native-task/README.md](native-task/README.md) for setup, session requirements and limitations.
85
+
86
+ ## Default optimized backend and rollback
87
+
88
+ Starting with plugin **0.4.1**, one-click setup uses [4.22.0-magpie.1](https://github.com/BUKOWSKIREAL/magpie-gemini-web/releases/tag/backend-v4.22.0-magpie.1). It includes the changes that previously required a manual build: stable XSRF caching, cancellation propagation for the main generation and upload requests, early completion after complete tool calls, browser headers, and verification of the returned Argon model. Your Google account still needs access to the requested model.
89
+
90
+ **Upgrading an existing installation:** the plugin leaves a running backend alone. Stop the existing backend process (its process ID is in `backend.pid` in the data directory), then start Magpie or send a request. A stopped old official backend is upgraded automatically, preserving configuration, the cookie database and API keys. Explicitly installed local Argon builds are kept; to replace one with the published build, stop it and run `backend:install` below.
83
91
 
84
- ## Argon patched build (optional)
92
+ To roll back, stop the backend and run this from the plugin directory:
85
93
 
86
- The official backend already supports `gemini-4-argon`, which needs a paid or Ultra account. This repo's patched build also verifies that responses really come from Argon, so a silent downgrade is caught, and it lowers latency. It also makes the backend's requests look more like a browser's: on a direct connection every request carries the full set of Chrome headers (the official build's direct chat requests say `Go-http-client/2.0`), and refreshing the cookies to keep a session alive no longer fetches the `/app` page right away. Building it requires Go 1.26+:
94
+ ```bash
95
+ npm run backend:install:upstream
96
+ ```
97
+
98
+ This installs the checksum-verified official v4.22.0 and saves the selection, so automatic starts keep using upstream. To restore the default optimized build:
87
99
 
88
100
  ```bash
89
- git clone https://github.com/BUKOWSKIREAL/magpie-gemini-web.git && cd magpie-gemini-web
90
- git clone https://github.com/zexadev/gemini-web2api-go.git backend-source
91
- git -C backend-source checkout a65b1d1a5ac614c1b455857334b4f1c73ab8fcfb
92
- git -C backend-source apply ../patches/argon.patch
93
- npm run backend:build:argon
94
- npm run backend:install:argon
101
+ npm run backend:install
95
102
  ```
96
103
 
97
- Restart Magpie when the install finishes, and the plugin starts the Argon build in the background. If an older backend is still running, stop it first with `kill $(cat ~/.local/share/magpie-gemini-web/backend.pid)`. Run `npm run backend:install` to switch back to the official build.
104
+ A failed download or checksum verification keeps the previous binary. On Windows, stop the old process before replacing its executable.
98
105
 
99
- When `patches/argon.patch` changes (after you pull a new version of this repo, say), apply it again to clean sources and rebuild. Once the old backend is stopped, the plugin starts the new one on the next request:
106
+ The backend release includes source and build metadata. Developers with Git, Node 22+ and Go 1.27.1 can reproduce all six builds and compare the pinned hashes from this repository:
100
107
 
101
108
  ```bash
102
- git -C backend-source checkout -- . && git -C backend-source clean -fdq
103
- git -C backend-source apply ../patches/argon.patch
104
- npm run backend:build:argon && npm run backend:install:argon
105
- kill $(cat ~/.local/share/magpie-gemini-web/backend.pid)
109
+ npm run backend:release:build
106
110
  ```
107
111
 
112
+ The script extracts clean sources from the pinned upstream commit and applies `patches/argon.patch`; it never changes the developer's `backend-source` working tree. See [docs/backend-release.md](docs/backend-release.md) for the release procedure.
113
+
108
114
  ## License
109
115
 
110
116
  MIT. The upstream gemini-web2api-go project is also MIT (see `LICENSE-UPSTREAM`).
package/README.md CHANGED
@@ -4,10 +4,12 @@
4
4
 
5
5
  让 [Magpie](https://usemagpie.ai/) 通过你的 Google 账号使用 Gemini 网页版。插件在本机运行 [gemini-web2api-go](https://github.com/zexadev/gemini-web2api-go) 后端,把 Gemini 网页转成 OpenAI 兼容接口。
6
6
 
7
+ > **2026-10-16 更新**:现在正价支付的 Google AI Pro 可以直接在 Antigravity 中使用 `gemini-4-argon`,同理也可以直接用 Magpie 反代。
8
+
7
9
  ## 开始使用
8
10
 
9
11
  1. 在 Magpie 插件市场安装 **magpie-gemini-web**。
10
- 2. 登录 Gemini Web,选 **「一键配置本地 Gemini Web(推荐)」**。插件会自动下载并校验后端,在后台启动它。
12
+ 2. 登录 Gemini Web,选 **「一键配置本地 Gemini Web(推荐)」**。插件会自动下载本项目发布的优化后端、校验 SHA-256,并在后台启动;不需要安装 Go 或手动打补丁。
11
13
  3. 按提示复制 Cookie,**一定要用无痕 / 隐私窗口**(推荐 Firefox):
12
14
  1. 在无痕窗口打开 <https://gemini.google.com/app>,登录要使用的 Google 账号;
13
15
  2. 按 F12(macOS 按 ⌥⌘I),切到 **Network(网络)**,刷新页面;
@@ -48,7 +50,7 @@ magpie plugin options magpie-gemini-web '{"allowImages":true}'
48
50
  ## 安全与数据
49
51
 
50
52
  - 后端只监听 `127.0.0.1`;Cookie 只交给本机后端,Magpie 保存的是本地生成的 API key。
51
- - 后端二进制锁定官方 v4.22.0,下载和每次启动都校验 SHA-256。支持 macOS、Linux、Windows(x64 / arm64)。
53
+ - 默认后端为本项目发布的 **4.22.0-magpie.1**,基于固定的上游 v4.22.0 源码应用本仓库补丁。下载和启动新进程前校验 SHA-256。支持 macOS、Linux、Windows(x64 / arm64),发布页提供源码、构建记录和校验和。
52
54
  - 数据目录:`~/.local/share/magpie-gemini-web`(可用 `GEMINI_WEB_DATA_DIR` 修改),日志在 `backend.log`。
53
55
  - 插件限制并发为 1,但无法保证账号不被 Google 风控,请自行承担风险。
54
56
 
@@ -79,32 +81,36 @@ Magpie 的额度卡片会显示 Google 账号的 **5 小时** 和 **每周** 用
79
81
 
80
82
  在 macOS 上登录时会多出一个「macOS 原生 Task(实验性)」选项:请求不走 Gemini Web 后端,而是交给本机常驻服务,由 Gemini.app 自己的 Task 模式执行,工具调用走原生的 Function Calling 通道,模型为 `gemini-task-argon`。Windows 和 Linux 上不会出现这个选项,请继续使用上面的一键配置。
81
83
 
82
- 当前为 Alpha:仅支持已验证的 Gemini.app 版本,选这个选项之前需要先在本机完成 setup(需要 Gemini.app 已登录,以及会话模板),尚未完成真实 Argon 的端到端验收,读不到额度。配置步骤、会话要求和限制见 [native-task/README.md](native-task/README.md)。
84
+ 当前为 Alpha:仅支持已验证的 Gemini.app 版本,选这个选项之前需要先在本机完成 setup(需要 Gemini.app 已登录,以及会话模板),只完成过一次真实 Argon 的工具往返验证,读不到额度;Agent 中途打断时,未回传结果的工具调用会占住唯一的 Task 名额最长约 4 分钟。配置步骤、会话要求和限制见 [native-task/README.md](native-task/README.md)。
85
+
86
+ ## 默认优化后端与回退
87
+
88
+ 从插件 **0.4.1** 起,一键配置默认使用 [4.22.0-magpie.1](https://github.com/BUKOWSKIREAL/magpie-gemini-web/releases/tag/backend-v4.22.0-magpie.1)。它包含原先需要手动编译的优化:稳定的 XSRF 缓存、主要生成与上传请求的取消传播、完整工具调用后的提前收尾、浏览器请求头,以及 Argon 返回模型校验。Google 账号仍需具备对应模型权限。
89
+
90
+ **已有用户升级**:插件不会中断正在运行的后端。先停止现有后端进程(进程 ID 在数据目录的 `backend.pid`),再启动 Magpie 或发送请求;停止状态的旧官方后端会自动更新,配置、Cookie 数据库和 API key 保留。已经手动安装的本地 Argon 构建会继续复用;要切到发布版,可在停止后端后运行下面的 `backend:install`。
83
91
 
84
- ## Argon 增强版(可选)
92
+ 需要回退时,在插件目录停止后端后运行:
85
93
 
86
- 官方后端已支持 `gemini-4-argon`(需付费 / Ultra 账号)。本仓库的补丁版额外校验返回的确实是 Argon(防止被静默降级),并降低延迟。它也让后端的请求更像浏览器:直连时每个请求都带齐 Chrome 请求头(官方版直连时,对话请求的 UA 是 `Go-http-client/2.0`),保活刷新 Cookie 后也不再立即重新抓取 `/app` 页面。需要 Go 1.26+:
94
+ ```bash
95
+ npm run backend:install:upstream
96
+ ```
97
+
98
+ 这会安装经过 SHA-256 校验的官方 v4.22.0,并记住回退选择,之后自动启动也使用官方版。恢复默认优化版:
87
99
 
88
100
  ```bash
89
- git clone https://github.com/BUKOWSKIREAL/magpie-gemini-web.git && cd magpie-gemini-web
90
- git clone https://github.com/zexadev/gemini-web2api-go.git backend-source
91
- git -C backend-source checkout a65b1d1a5ac614c1b455857334b4f1c73ab8fcfb
92
- git -C backend-source apply ../patches/argon.patch
93
- npm run backend:build:argon
94
- npm run backend:install:argon
101
+ npm run backend:install
95
102
  ```
96
103
 
97
- 安装后重启 Magpie 即可,插件会在后台启动 Argon 版(如有旧后端在运行,先 `kill $(cat ~/.local/share/magpie-gemini-web/backend.pid)`)。运行 `npm run backend:install` 可切回官方版。
104
+ 下载失败或校验不通过时保留旧二进制。Windows 上必须先停止旧进程,才能替换正在使用的可执行文件。
98
105
 
99
- `patches/argon.patch` 更新后(比如拉取了本仓库的新版本),需要在干净的源码上重新应用补丁并重新构建。停掉旧后端后,插件会在下一个请求时启动新版:
106
+ 源码与构建记录随后端 Release 提供。开发者使用 Git、Node 22+、Go 1.27.1,可从本仓库复现全部六个平台的构建并核对哈希:
100
107
 
101
108
  ```bash
102
- git -C backend-source checkout -- . && git -C backend-source clean -fdq
103
- git -C backend-source apply ../patches/argon.patch
104
- npm run backend:build:argon && npm run backend:install:argon
105
- kill $(cat ~/.local/share/magpie-gemini-web/backend.pid)
109
+ npm run backend:release:build
106
110
  ```
107
111
 
112
+ 构建脚本从固定的上游提交提取干净源码、应用 `patches/argon.patch`,不会修改本地 `backend-source` 的工作内容。发布流程见 [docs/backend-release.md](docs/backend-release.md)。
113
+
108
114
  ## 友链
109
115
 
110
116
  [LINUX DO](https://linux.do/) — 真诚、友善、团结、专业的技术社区。
@@ -0,0 +1,31 @@
1
+ # Backend releases
2
+
3
+ The plugin's default backend is built from the upstream commit and toolchain recorded in `lib/backend-release.json`, with `patches/argon.patch` applied. The builder overrides the backend's displayed version with the release version. Six binaries are pinned by SHA-256 in the same JSON file. The build does not include the developer's uncommitted backend sources, local data or credentials.
4
+
5
+ For a new backend release:
6
+
7
+ 1. Choose a new backend version and tag in `lib/backend-release.json`. Never replace files under an already published backend tag: older plugins pin those hashes. Update the upstream commit or patch when needed.
8
+ 2. Run `node scripts/backend-release.mjs --write-pins`. It extracts a clean upstream tree, applies the patch, runs Go tests, cross-compiles all six targets and writes the resulting pins. It uses a local `backend-source` Git object database if available; otherwise it fetches the pinned public commit in a temporary directory.
9
+ 3. Run `npm test` and `node scripts/backend-smoke.mjs`. The smoke test uses a temporary data directory, checks the actual executable's version and hash, starts it on a spare loopback port, checks authentication and model discovery, and verifies reuse. It uses no Google account and takes the tokenizer's offline fallback.
10
+ 4. Commit the release metadata, installer changes, tests and documentation. Run the **Verify backend release** GitHub Actions workflow at that commit. It rebuilds on native macOS, Linux and Windows runners for both architectures, compares the pinned hashes, and runs the installation/startup smoke test. Wait for all jobs to pass.
11
+ 5. Create a private draft under the pinned backend tag. Attach all six binaries, `SHA256SUMS.txt`, `build.json`, the patched source archive and `LICENSE-UPSTREAM` from `backend-build/release`. If local binary uploads are unreliable, run **Upload verified backend release** with the successful verification run ID: it downloads the six CI artifacts, checks every committed hash, and uploads missing executables to the draft. Existing assets must match exactly; published releases cannot be modified by this workflow. Include the upstream commit and patch SHA-256 in the release notes, then publish the complete draft. Keep backend-only releases from replacing the plugin release as GitHub's latest release.
12
+ 6. Verify public asset downloads against the committed pins. Run `node scripts/backend-smoke.mjs --download` to test the actual default download URL on the host platform.
13
+ 7. Bump and publish the plugin package only after the backend assets are publicly available. Inspect `npm pack --dry-run`; the npm package contains release metadata and patch/build scripts, but no executables, local databases or credentials. Attach the exact published npm tarball to the plugin's GitHub release as well.
14
+
15
+ Reproduce a release without changing its pins:
16
+
17
+ ```bash
18
+ npm run backend:release:build
19
+ ```
20
+
21
+ Build just the host target and compare its hash:
22
+
23
+ ```bash
24
+ node scripts/backend-release.mjs --target=host --verify
25
+ ```
26
+
27
+ The metadata records `CGO_ENABLED=0`, the exact Go toolchain, upstream commit, patch hash and build flags. The binary hashes are reproducible; the source archive is supplied for inspection and is not promised to have a reproducible gzip timestamp. The source archive includes upstream's license, and `LICENSE-UPSTREAM` is also supplied as a separate asset.
28
+
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
+
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.
@@ -0,0 +1,36 @@
1
+ {
2
+ "version": "4.22.0-magpie.1",
3
+ "repository": "BUKOWSKIREAL/magpie-gemini-web",
4
+ "tag": "backend-v4.22.0-magpie.1",
5
+ "upstream": {
6
+ "repository": "https://github.com/zexadev/gemini-web2api-go.git",
7
+ "commit": "a65b1d1a5ac614c1b455857334b4f1c73ab8fcfb"
8
+ },
9
+ "goVersion": "1.27.1",
10
+ "binaries": {
11
+ "darwin-arm64": {
12
+ "asset": "gemini-web2api-go_v4.22.0-magpie.1_darwin_arm64",
13
+ "sha256": "833dd54e7c0df927afc1d4e0f6443bcc03b7f0a4d04d24cdfde47d5dd27349c5"
14
+ },
15
+ "darwin-x64": {
16
+ "asset": "gemini-web2api-go_v4.22.0-magpie.1_darwin_amd64",
17
+ "sha256": "d65b053a0e5f8e1065ea35835b3a7d12f783d97964c02bef49679df7ead92fd4"
18
+ },
19
+ "linux-x64": {
20
+ "asset": "gemini-web2api-go_v4.22.0-magpie.1_linux_amd64",
21
+ "sha256": "94f082e666b34051f191959de3f2cd601078d117b1d3ededcdcd6b2201c48ef9"
22
+ },
23
+ "linux-arm64": {
24
+ "asset": "gemini-web2api-go_v4.22.0-magpie.1_linux_arm64",
25
+ "sha256": "4a7cbca2771acc9e836a5a986e11b3b247baf77e9900262cb588b515a1161aaa"
26
+ },
27
+ "win32-x64": {
28
+ "asset": "gemini-web2api-go_v4.22.0-magpie.1_windows_amd64.exe",
29
+ "sha256": "61d279d50c6c09ffe81ed059a557077900948d0ae598e4c4b3a5d5c213ad5e5c"
30
+ },
31
+ "win32-arm64": {
32
+ "asset": "gemini-web2api-go_v4.22.0-magpie.1_windows_arm64.exe",
33
+ "sha256": "20bad088b114d3dc989830ff35f7e56c1e0026caf9721b739fe982f5fd3d1880"
34
+ }
35
+ }
36
+ }
package/lib/native.mjs CHANGED
@@ -3,12 +3,16 @@ import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  import crypto from 'node:crypto';
5
5
  import { spawn } from 'node:child_process';
6
+ import release from './backend-release.json' with { type: 'json' };
6
7
 
7
- export const BACKEND_VERSION = 'v4.22.0';
8
- const RELEASE_BASE = `https://github.com/zexadev/gemini-web2api-go/releases/download/${BACKEND_VERSION}/`;
8
+ export const BACKEND_VERSION = `v${release.version}`;
9
+ const RELEASE_BASE = `https://github.com/${release.repository}/releases/download/${release.tag}/`;
10
+ export const PINNED_BINARIES = release.binaries;
9
11
 
10
- // Upstream release assets by `${process.platform}-${process.arch}`, SHA-256 from the release's SHA256SUMS.txt.
11
- export const PINNED_BINARIES = {
12
+ const UPSTREAM_VERSION = 'v4.22.0';
13
+ const UPSTREAM_BASE = `https://github.com/zexadev/gemini-web2api-go/releases/download/${UPSTREAM_VERSION}/`;
14
+ // Retained for an explicit, checksum-verified rollback to the upstream release.
15
+ export const UPSTREAM_BINARIES = {
12
16
  'darwin-arm64': { asset: 'gemini-web2api-go_v4.22.0_darwin_arm64', sha256: 'c69ca75cf43017cf2a62dbf5c12b07f6bde66f403f714c32209b1ad350ed24c9' },
13
17
  'darwin-x64': { asset: 'gemini-web2api-go_v4.22.0_darwin_amd64', sha256: '384e617bcc30aacac6b7c4e4d0490e88d282b13b29f7a0a20d645b69514cd405' },
14
18
  'linux-x64': { asset: 'gemini-web2api-go_v4.22.0_linux_amd64', sha256: '21fcc81e0ad8ce0921e6b6c2cafce1089852530142b37e0289355c6e72df5299' },
@@ -17,12 +21,38 @@ export const PINNED_BINARIES = {
17
21
  'win32-arm64': { asset: 'gemini-web2api-go_v4.22.0_windows_arm64.exe', sha256: 'b42e951bfc08d7fe8bdb436dca87fc6a1b7948bb8d10e110653bcf13a790756c' }
18
22
  };
19
23
 
20
- export function pinnedBinary(platform = process.platform, arch = process.arch) {
21
- const pin = PINNED_BINARIES[`${platform}-${arch}`];
24
+ export function pinnedBinary(platform = process.platform, arch = process.arch, channel = 'optimized') {
25
+ if (channel !== 'optimized' && channel !== 'upstream') throw new Error(`Invalid backend channel: ${channel}`);
26
+ const pins = channel === 'upstream' ? UPSTREAM_BINARIES : PINNED_BINARIES;
27
+ const base = channel === 'upstream' ? UPSTREAM_BASE : RELEASE_BASE;
28
+ const version = channel === 'upstream' ? UPSTREAM_VERSION : BACKEND_VERSION;
29
+ const pin = pins[`${platform}-${arch}`];
22
30
  if (!pin) {
23
- throw new Error(`No gemini-web2api-go ${BACKEND_VERSION} build for ${platform}/${arch}. Download one manually from https://github.com/zexadev/gemini-web2api-go/releases/tag/${BACKEND_VERSION}`);
31
+ throw new Error(`No gemini-web2api-go ${version} build for ${platform}/${arch}. See ${base.slice(0, -1).replace('/download/', '/tag/')}`);
32
+ }
33
+ return { url: base + pin.asset, sha256: pin.sha256 };
34
+ }
35
+
36
+ export function backendChannel(dataDir = getDataDir()) {
37
+ try {
38
+ const { channel } = JSON.parse(fs.readFileSync(path.join(dataDir, 'backend-channel.json'), 'utf8'));
39
+ if (channel !== 'optimized' && channel !== 'upstream') throw new Error('Expected optimized or upstream');
40
+ return channel;
41
+ } catch (error) {
42
+ if (error.code === 'ENOENT') return 'optimized';
43
+ throw new Error(`Invalid backend-channel.json: ${error.message}`);
44
+ }
45
+ }
46
+
47
+ function saveBackendChannel(dataDir, channel) {
48
+ const file = path.join(dataDir, 'backend-channel.json');
49
+ const temp = `${file}.${crypto.randomBytes(4).toString('hex')}.tmp`;
50
+ try {
51
+ fs.writeFileSync(temp, JSON.stringify({ channel }) + '\n', { mode: 0o600 });
52
+ fs.renameSync(temp, file);
53
+ } finally {
54
+ fs.rmSync(temp, { force: true });
24
55
  }
25
- return { url: RELEASE_BASE + pin.asset, sha256: pin.sha256 };
26
56
  }
27
57
 
28
58
  export function getDataDir(env = process.env) {
@@ -41,7 +71,8 @@ export function computeFileHash(filePath) {
41
71
  export async function installBackend(options = {}) {
42
72
  const platform = options.platform || process.platform;
43
73
  const arch = options.arch || process.arch;
44
- const pin = options.expectedHash && options.url ? null : pinnedBinary(platform, arch);
74
+ const channel = options.channel ?? 'optimized';
75
+ const pin = options.expectedHash && options.url ? null : pinnedBinary(platform, arch, channel);
45
76
  const expectedHash = options.expectedHash || pin.sha256;
46
77
  const downloadUrl = options.url || pin.url;
47
78
  const fetchFn = options.fetchFn || globalThis.fetch;
@@ -62,8 +93,6 @@ export async function installBackend(options = {}) {
62
93
  if (existingHash === expectedHash) {
63
94
  binaryReady = true;
64
95
  console.log(`Backend binary verified: ${binPath}`);
65
- } else {
66
- fs.unlinkSync(binPath);
67
96
  }
68
97
  }
69
98
 
@@ -97,7 +126,11 @@ export async function installBackend(options = {}) {
97
126
  }
98
127
  }
99
128
 
100
- return { binPath, ...ensureDataFiles(dataDir) };
129
+ const files = ensureDataFiles(dataDir);
130
+ // A local custom build keeps the user's channel. Official installs record their
131
+ // selection only after verification and replacement succeeded.
132
+ if ((!options.expectedHash && !options.url) || options.channel !== undefined) saveBackendChannel(dataDir, channel);
133
+ return { binPath, ...files };
101
134
  }
102
135
 
103
136
  // ensureDataFiles creates config.json and secrets.json when missing; existing ones are kept.
@@ -136,7 +169,7 @@ function ensureDataFiles(dataDir) {
136
169
 
137
170
  export function startBackend(options = {}) {
138
171
  const dataDir = options.dataDir || getDataDir();
139
- const expectedHash = options.expectedHash || pinnedBinary().sha256;
172
+ const expectedHash = options.expectedHash || pinnedBinary(process.platform, process.arch, backendChannel(dataDir)).sha256;
140
173
  const spawnFn = options.spawnFn || spawn;
141
174
  const { binPath, args, env } = prepareLaunch(dataDir, [expectedHash]);
142
175
 
@@ -236,12 +269,11 @@ function prepareLaunch(dataDir, trustedHashes, platform = process.platform) {
236
269
  return { binPath, args, env, port: config.port, secrets };
237
270
  }
238
271
 
239
- // trustedHashes is what a backend binary in dataDir may hash to: the pinned upstream build for
240
- // this platform, plus a local Argon build installed by `backend:install:argon`.
272
+ // trustedHashes accepts the selected release channel and any explicitly installed local build.
241
273
  function trustedHashes(dataDir, platform, arch) {
242
274
  const hashes = [];
243
275
  try {
244
- hashes.push(pinnedBinary(platform, arch).sha256);
276
+ hashes.push(pinnedBinary(platform, arch, backendChannel(dataDir)).sha256);
245
277
  } catch {}
246
278
  try {
247
279
  const argon = JSON.parse(fs.readFileSync(path.join(dataDir, 'argon-build.json'), 'utf8'));
@@ -318,7 +350,7 @@ async function withLock(dataDir, fn, timeoutMs) {
318
350
 
319
351
  // ensureBackend makes sure the local backend is installed and running, and returns how to reach
320
352
  // it. A backend already answering with this setup's key is left alone. Otherwise a missing or
321
- // untrusted binary is (re)installed from the pinned upstream release, config and secrets are
353
+ // untrusted binary is (re)installed from the selected release channel, config and secrets are
322
354
  // created when missing, and the backend is started in the background (logging to backend.log)
323
355
  // so it outlives the caller.
324
356
  export async function ensureBackend(options = {}) {
@@ -344,7 +376,7 @@ export async function ensureBackend(options = {}) {
344
376
  const trusted = trustedHashes(dataDir, platform, arch);
345
377
  const binPath = binaryPath(dataDir, platform);
346
378
  if (!fs.existsSync(binPath) || !trusted.includes(computeFileHash(binPath))) {
347
- await installBackend({ dataDir, platform, arch, fetchFn });
379
+ await installBackend({ dataDir, platform, arch, fetchFn, channel: backendChannel(dataDir) });
348
380
  } else {
349
381
  ensureDataFiles(dataDir);
350
382
  }
@@ -29,7 +29,9 @@
29
29
 
30
30
  2026-10-15:已执行通过 41 项 Python 测试和 12 项 Node 测试;真实 Magpie `0.1.1244` 的隔离环境完成了插件安装、本地连接、模型发现及 8 个 HTTP 请求的工具往返测试。该网关测试使用模拟原生上游,包含两次串行工具、最终答案重试、错误会话/调用 ID 拒绝、旧工具阶段拒绝和下一轮用户消息。
31
31
 
32
- 当前实现已使用现有登录完成 OAuth 续期及保存轮次读取,但真实 Task 请求被 Google 返回的 `Remy quota exhausted`(gRPC 8)拒绝,尚未完成本版真实 Argon 工具端到端验收。服务将该错误映射为 `native_quota_exhausted`,不会自动重发 Task。
32
+ 2026-10-15:通过 `magpie-gemini-web` 0.4.0 的 Task 登录方式(直接加载插件,未经 Magpie 网关)完成了一次真实 Argon 工具往返:模型返回 `Read` 工具调用(19.2 秒),回传工具结果后得到正确的最终答案(13.7 秒)。此前真实 Task 请求曾被 Google 以 `Remy quota exhausted`(gRPC 8)拒绝,服务将该错误映射为 `native_quota_exhausted`,不会自动重发 Task。
33
+
34
+ 已知问题:客户端收到工具调用后若不回传结果(例如 agent 中途被打断),该 Task 会一直占用唯一的名额,直到工具等待和任务超时到期(最长约 4 分钟),期间其他请求返回 409 `concurrency_limit`。另外曾出现过一次首个健康检查超时(2 秒),未能复现。
33
35
 
34
36
  ## Magpie 插件安装与登录
35
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magpie-gemini-web",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Gemini Web provider for Magpie, backed by gemini-web2api-go",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
@@ -22,6 +22,7 @@
22
22
  "lib/",
23
23
  "scripts/backend.mjs",
24
24
  "scripts/backend-argon.mjs",
25
+ "scripts/backend-release.mjs",
25
26
  "patches/",
26
27
  "native-task/lib/",
27
28
  "native-task/bridge/",
@@ -32,6 +33,7 @@
32
33
  "native-task/LICENSE",
33
34
  "README.md",
34
35
  "README.en.md",
36
+ "docs/backend-release.md",
35
37
  "LICENSE",
36
38
  "LICENSE-UPSTREAM"
37
39
  ],
@@ -50,6 +52,8 @@
50
52
  "scripts": {
51
53
  "test": "node --test test/*.test.mjs native-task/test/*.test.mjs",
52
54
  "backend:install": "node scripts/backend.mjs install",
55
+ "backend:install:upstream": "node scripts/backend.mjs install-upstream",
56
+ "backend:release:build": "node scripts/backend-release.mjs --verify",
53
57
  "backend:start": "node scripts/backend.mjs start",
54
58
  "backend:status": "node scripts/backend.mjs status",
55
59
  "backend:build:argon": "node scripts/backend-argon.mjs build",
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ // Build from a clean, pinned upstream tree plus the checked-in patch. The developer's
3
+ // backend-source working tree is never changed or included in release artifacts.
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import crypto from 'node:crypto';
8
+ import { execFileSync } from 'node:child_process';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const root = fileURLToPath(new URL('..', import.meta.url));
12
+ const pinPath = path.join(root, 'lib/backend-release.json');
13
+ const release = JSON.parse(fs.readFileSync(pinPath, 'utf8'));
14
+ const args = process.argv.slice(2);
15
+ const writePins = args.includes('--write-pins');
16
+ const verify = args.includes('--verify');
17
+ const requested = args.find(arg => arg.startsWith('--target='))?.slice('--target='.length);
18
+ for (const arg of args) {
19
+ if (!['--write-pins', '--verify'].includes(arg) && !arg.startsWith('--target=')) throw new Error(`Unknown argument: ${arg}`);
20
+ }
21
+ const platforms = {
22
+ 'darwin-arm64': ['darwin', 'arm64'],
23
+ 'darwin-x64': ['darwin', 'amd64'],
24
+ 'linux-x64': ['linux', 'amd64'],
25
+ 'linux-arm64': ['linux', 'arm64'],
26
+ 'win32-x64': ['windows', 'amd64'],
27
+ 'win32-arm64': ['windows', 'arm64']
28
+ };
29
+ const target = requested === 'host' ? `${process.platform}-${process.arch}` : requested;
30
+ if (target && !platforms[target]) throw new Error(`Unsupported target: ${target}`);
31
+ if (writePins && (target || verify)) throw new Error('--write-pins requires a full build without --verify');
32
+ const targets = target ? [target] : Object.keys(platforms);
33
+ const output = path.join(root, 'backend-build', 'release');
34
+ fs.mkdirSync(output, { recursive: true });
35
+ const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'magpie-backend-build-'));
36
+ const source = path.join(scratch, 'source');
37
+ fs.mkdirSync(source);
38
+ const env = { ...process.env, CGO_ENABLED: '0', GOFLAGS: '', GOEXPERIMENT: '',
39
+ GOTOOLCHAIN: `go${release.goVersion}`, GOAMD64: 'v1', GOARM64: 'v8.0' };
40
+ for (const name of ['ARGON_PROBE_COOKIE', 'GW2A_UPLOAD_PROBE', 'SQL_DSN', 'GOOS', 'GOARCH']) delete env[name];
41
+ const go = (argv, extraEnv = {}) => execFileSync('go', argv, { cwd: source, env: { ...env, ...extraEnv }, stdio: 'inherit' });
42
+ const hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
43
+ const patchPath = path.join(root, 'patches/argon.patch');
44
+ const flags = ['-trimpath', '-buildvcs=false', '-ldflags=-s -w'];
45
+
46
+ try {
47
+ let upstream = path.join(root, 'backend-source');
48
+ try {
49
+ execFileSync('git', ['-C', upstream, 'cat-file', '-e', `${release.upstream.commit}^{commit}`], { stdio: 'pipe' });
50
+ } catch {
51
+ upstream = path.join(scratch, 'upstream');
52
+ execFileSync('git', ['init', '-q', upstream]);
53
+ execFileSync('git', ['-C', upstream, 'fetch', '--depth=1', release.upstream.repository, release.upstream.commit], { stdio: 'inherit' });
54
+ }
55
+ const archive = path.join(scratch, 'upstream.tar');
56
+ execFileSync('git', ['-C', upstream, 'archive', '--format=tar', `--output=${archive}`, release.upstream.commit]);
57
+ // Relative archive paths also work with Git for Windows' tar, which otherwise
58
+ // treats the drive letter in C:\\... as a remote archive host.
59
+ execFileSync('tar', ['-xf', 'upstream.tar', '-C', 'source'], { cwd: scratch });
60
+ execFileSync('git', ['apply', '--check', patchPath], { cwd: source, stdio: 'inherit' });
61
+ execFileSync('git', ['apply', patchPath], { cwd: source, stdio: 'inherit' });
62
+ const versionPath = path.join(source, 'internal/app/server.go');
63
+ const server = fs.readFileSync(versionPath, 'utf8');
64
+ const versionLine = /^const Version = "[^"]+"$/m;
65
+ if (!versionLine.test(server)) throw new Error('Backend version declaration not found');
66
+ fs.writeFileSync(versionPath, server.replace(versionLine, `const Version = ${JSON.stringify(release.version)}`));
67
+ console.log(`Testing ${release.version} from ${release.upstream.commit} with patches/argon.patch`);
68
+ go(['version']);
69
+ go(['test', './...', '-count=1']);
70
+
71
+ const binaries = {};
72
+ for (const key of targets) {
73
+ const [goos, goarch] = platforms[key];
74
+ const asset = `gemini-web2api-go_v${release.version}_${goos}_${goarch}${goos === 'windows' ? '.exe' : ''}`;
75
+ const file = path.join(output, asset);
76
+ console.log(`Building ${key}: ${asset}`);
77
+ go(['build', ...flags, '-o', file, '.'], { GOOS: goos, GOARCH: goarch });
78
+ const bytes = fs.readFileSync(file);
79
+ const sha256 = hash(bytes);
80
+ if (verify && (release.binaries[key]?.sha256 !== sha256 || release.binaries[key]?.asset !== asset)) {
81
+ throw new Error(`Release pin mismatch for ${key}: built ${sha256}`);
82
+ }
83
+ binaries[key] = { asset, sha256 };
84
+ if (key === `${process.platform}-${process.arch}`) {
85
+ const version = execFileSync(file, ['--version'], { encoding: 'utf8' }).trim();
86
+ if (version !== `gemini-web2api-go ${release.version}`) throw new Error(`Unexpected binary version: ${version}`);
87
+ console.log(version);
88
+ }
89
+ }
90
+
91
+ if (!target) {
92
+ const sourceName = `gemini-web2api-go_v${release.version}_source.tar.gz`;
93
+ const sourceArchive = fs.openSync(path.join(output, sourceName), 'w');
94
+ try {
95
+ execFileSync('tar', ['-czf', '-', '.'], { cwd: source, stdio: ['ignore', sourceArchive, 'inherit'] });
96
+ } finally {
97
+ fs.closeSync(sourceArchive);
98
+ }
99
+ fs.copyFileSync(path.join(root, 'LICENSE-UPSTREAM'), path.join(output, 'LICENSE-UPSTREAM'));
100
+ const build = { version: release.version, upstream: release.upstream, goVersion: release.goVersion,
101
+ patch: 'patches/argon.patch', patchSha256: hash(fs.readFileSync(patchPath)),
102
+ versionOverride: release.version, cgoEnabled: false, flags, binaries };
103
+ fs.writeFileSync(path.join(output, 'build.json'), `${JSON.stringify(build, null, 2)}\n`);
104
+ const assets = [...Object.values(binaries).map(b => b.asset), sourceName, 'LICENSE-UPSTREAM', 'build.json'].sort();
105
+ fs.writeFileSync(path.join(output, 'SHA256SUMS.txt'), assets.map(name => `${hash(fs.readFileSync(path.join(output, name)))} ${name}\n`).join(''));
106
+ if (writePins) fs.writeFileSync(pinPath, `${JSON.stringify({ ...release, binaries }, null, 2)}\n`);
107
+ }
108
+ console.log(`Release artifacts ready: ${output}`);
109
+ } finally {
110
+ fs.rmSync(scratch, { recursive: true, force: true });
111
+ }
@@ -8,6 +8,9 @@ async function main() {
8
8
  case 'install':
9
9
  await installBackend();
10
10
  break;
11
+ case 'install-upstream':
12
+ await installBackend({ channel: 'upstream' });
13
+ break;
11
14
  case 'start':
12
15
  startBackend();
13
16
  break;
@@ -15,7 +18,7 @@ async function main() {
15
18
  await statusBackend();
16
19
  break;
17
20
  default:
18
- console.error('Usage: node scripts/backend.mjs <install|start|status>');
21
+ console.error('Usage: node scripts/backend.mjs <install|install-upstream|start|status>');
19
22
  process.exit(1);
20
23
  }
21
24
  }