magpie-gemini-web 0.2.0 → 0.3.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 +85 -0
- package/README.md +49 -156
- package/index.mjs +80 -7
- package/lib/usage.mjs +193 -0
- package/package.json +2 -1
package/README.en.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# magpie-gemini-web
|
|
2
|
+
|
|
3
|
+
[中文](README.md) | English
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
## Getting started
|
|
8
|
+
|
|
9
|
+
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.
|
|
11
|
+
3. Copy your cookie when prompted. **Use a private / incognito window** (Firefox recommended):
|
|
12
|
+
1. In the private window, open <https://gemini.google.com/app> and sign in to the Google account you want to use.
|
|
13
|
+
2. Press F12 (⌥⌘I on macOS), switch to **Network** and reload the page.
|
|
14
|
+
3. Click any `gemini.google.com` request and go to **Headers** → **Request Headers**.
|
|
15
|
+
4. Copy the value of the `cookie` line and paste it into Magpie.
|
|
16
|
+
5. Close the private window. **Don't sign out**, because signing out invalidates the cookie.
|
|
17
|
+
4. The plugin checks that the account works, then finishes signing in.
|
|
18
|
+
|
|
19
|
+
> **Why a private window?** The backend refreshes this sign-in periodically to keep it alive. If it shares a session with the browser you normally use, Google revokes the session and your browser gets signed out of every Google service. A private window gives the backend a session of its own. Using a separate Google account for the backend is safer still.
|
|
20
|
+
|
|
21
|
+
From then on, Magpie starts the backend automatically every time it launches. To add another account, run the one-click setup again.
|
|
22
|
+
|
|
23
|
+
> Need a proxy to reach Google? The backend ignores `HTTP_PROXY`. Add the proxy in the proxy pool of the admin panel at `http://127.0.0.1:8083/admin`. The admin password is `adminToken` in `secrets.json` in the data directory.
|
|
24
|
+
|
|
25
|
+
**Connecting an existing backend:** if you already run gemini-web2api-go yourself, choose 连接已有后端 (connect an existing backend with a custom URL and API key). Enter its URL (ending in `/v1`) and its `API_KEY`.
|
|
26
|
+
|
|
27
|
+
## Plugin options
|
|
28
|
+
|
|
29
|
+
Tool calls and image input are off by default. Turn them on if a coding agent needs tools:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
magpie plugin options magpie-gemini-web '{"allowTools":true,"allowImages":true}'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Option | Default | Description |
|
|
36
|
+
| :--- | :--- | :--- |
|
|
37
|
+
| `allowTools` | `false` | Allows tool calls (experimental). When off, requests with tools get a 400 |
|
|
38
|
+
| `allowImages` | `false` | Allows image input |
|
|
39
|
+
| `baseURL` | `http://127.0.0.1:8083/v1` | Default backend URL |
|
|
40
|
+
| `contextLimit` / `outputLimit` | `32768` / `8192` | Context and output limits reported to clients |
|
|
41
|
+
| `requestTimeoutMs` / `modelTimeoutMs` | `240000` / `10000` | Timeouts in ms for requests and for fetching the model list |
|
|
42
|
+
|
|
43
|
+
## Thinking effort
|
|
44
|
+
|
|
45
|
+
You can switch a model between `low` and `high` effort in Magpie if it has a `-thinking` version. `low` uses the base model and `high` uses the `-thinking` model with extended thinking. After upgrading the plugin, run `magpie provider models gemini-web` to refresh the model list. Argon's medium tier will be added once upstream supports it ([#5](https://github.com/BUKOWSKIREAL/magpie-gemini-web/issues/5)).
|
|
46
|
+
|
|
47
|
+
## Security and data
|
|
48
|
+
|
|
49
|
+
- 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.
|
|
50
|
+
- 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.
|
|
51
|
+
- 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.
|
|
52
|
+
- 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.
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
Magpie's usage cards show each Google account's **5-hour** and **weekly** usage and reset times, as <https://gemini.google.com/usage> does. Each Google account gets a card of its own, named by its email:
|
|
57
|
+
|
|
58
|
+
- An account added by one-click setup becomes its own card.
|
|
59
|
+
- For an account already in the backend (one added by an older version, say), add another Gemini Web account in Magpie and pick **「添加本地后端里已有的 Google 账号」** (add an account already in the local backend), then choose it from the list. The old `API key …` sign-in can then go: it shows the whole pool as one card (each window at the account that has used the most).
|
|
60
|
+
|
|
61
|
+
Note that the backend picks the Google account for each request from its pool in turn: how Magpie spreads requests over the cards doesn't decide which account serves them. Only the one-click local backend has usage; a backend you connect to yourself keeps its cookies out of the plugin's reach.
|
|
62
|
+
|
|
63
|
+
## Limitations
|
|
64
|
+
|
|
65
|
+
- Upstream emulates tool calls with prompts, so they aren't always reliable. Flash models often don't call tools.
|
|
66
|
+
- Only text models whose names start with `gemini-` are exposed. A `cost` of 0 doesn't reflect your actual Google quota.
|
|
67
|
+
|
|
68
|
+
## Argon patched build (optional)
|
|
69
|
+
|
|
70
|
+
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. Building it requires Go 1.26+:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/BUKOWSKIREAL/magpie-gemini-web.git && cd magpie-gemini-web
|
|
74
|
+
git clone https://github.com/zexadev/gemini-web2api-go.git backend-source
|
|
75
|
+
git -C backend-source checkout a65b1d1a5ac614c1b455857334b4f1c73ab8fcfb
|
|
76
|
+
git -C backend-source apply ../patches/argon.patch
|
|
77
|
+
npm run backend:build:argon
|
|
78
|
+
npm run backend:install:argon
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT. The upstream gemini-web2api-go project is also MIT (see `LICENSE-UPSTREAM`).
|
package/README.md
CHANGED
|
@@ -1,192 +1,85 @@
|
|
|
1
1
|
# magpie-gemini-web
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
中文 | [English](README.en.md)
|
|
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
|
-
##
|
|
7
|
+
## 开始使用
|
|
8
8
|
|
|
9
|
-
1. 在 Magpie
|
|
10
|
-
2.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
3. 插件把 Cookie 导入本地后端并检测能否登录 Gemini;检测通过即完成登录,检测失败会删掉这条账号并提示原因(例如缺少哪些 Cookie、账号未登录)。全程不需要手动填写 API key。
|
|
9
|
+
1. 在 Magpie 插件市场安装 **magpie-gemini-web**。
|
|
10
|
+
2. 登录 Gemini Web,选 **「一键配置本地 Gemini Web(推荐)」**。插件会自动下载并校验后端,在后台启动它。
|
|
11
|
+
3. 按提示复制 Cookie,**一定要用无痕 / 隐私窗口**(推荐 Firefox):
|
|
12
|
+
1. 在无痕窗口打开 <https://gemini.google.com/app>,登录要使用的 Google 账号;
|
|
13
|
+
2. 按 F12(macOS 按 ⌥⌘I),切到 **Network(网络)**,刷新页面;
|
|
14
|
+
3. 点任意一个 `gemini.google.com` 请求 → **Headers(标头)** → **Request Headers(请求标头)**;
|
|
15
|
+
4. 复制 `cookie` 那一行的值,粘贴回 Magpie;
|
|
16
|
+
5. 直接关掉无痕窗口,**不要点退出登录**(退出会让这份 Cookie 失效)。
|
|
17
|
+
4. 插件检测账号可用后自动完成登录。
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
> **为什么要用无痕窗口?** 后端会定时刷新这份登录来保活。如果它和你平时用的浏览器是同一个登录会话,Google 会把会话作废,你的浏览器就会退出所有 Google 服务。无痕窗口能给后端一份独立的会话;更稳妥的做法是给后端单独用一个 Google 小号。
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
>
|
|
24
|
-
> Cookie 只发给本机的后端,保存在它的本地 SQLite 数据库里;Magpie 保存的是后端的本地 API key,而不是 Cookie。
|
|
21
|
+
之后 Magpie 每次启动都会自动拉起后端,不用再管。想加更多账号,再走一遍一键配置即可。
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
magpie plugin options magpie-gemini-web '{"allowTools":true,"allowImages":true}'
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
**支持的平台**:一键配置为以下平台锁定了官方二进制及其 SHA-256:macOS(Apple Silicon / Intel)、Linux(x64 / arm64)、Windows(x64 / arm64)。其他平台请用第 3 节的「连接已有后端」。
|
|
23
|
+
> 需要代理才能访问 Google?后端不读 `HTTP_PROXY`,请在管理面板 `http://127.0.0.1:8083/admin` 的代理池里添加(登录口令是数据目录 `secrets.json` 里的 `adminToken`)。
|
|
32
24
|
|
|
33
|
-
|
|
25
|
+
**连接已有后端**:如果你自己运行了 gemini-web2api-go,登录时选「连接已有后端(自定义地址 + API key)」,填后端地址(以 `/v1` 结尾)和它的 `API_KEY`。
|
|
34
26
|
|
|
35
|
-
##
|
|
27
|
+
## 插件选项
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
- **Magpie 插件层**(本仓库):纯 JavaScript (ESM),无第三方依赖。负责认证与一键配置、动态模型目录 (`models`)、请求转发与输入过滤 (`forward`),以及管理本地后端进程。
|
|
39
|
-
- **原生后端层**(`gemini-web2api-go`):上游独立 Go 进程,维护 Google 账号 Cookie 池并提供 OpenAI 兼容接口。
|
|
40
|
-
2. **凭证隔离**:一键配置时插件只把你粘贴的 Cookie 交给本机后端的管理接口,自身不保存、不转发到其他地方;之后插件与 Magpie 仅凭本地随机 `apiKey` 访问后端。
|
|
41
|
-
3. **本地回环**:上游默认监听 `0.0.0.0`;本插件生成的配置**锁定为 `127.0.0.1:8083`**,启动前还会再次校验 host 与二进制哈希。若 8083 已被别的服务占用,一键配置会报错而不会覆盖,可修改数据目录下 `config.json` 的 `port`。
|
|
42
|
-
4. **请求过滤**:转发前剥离调用方的 `Cookie`、`Authorization`、`x-api-key`、`anthropic-*` 等头部。
|
|
43
|
-
5. **并发抑制**:插件声明 `maxConcurrency: 1`,降低请求频率,但**不能保证账号不被 Google 风控或封禁**;请知悉使用网页逆向服务的风险。
|
|
44
|
-
6. **开源协议**:上游项目与本插件均为 MIT 许可证。
|
|
29
|
+
工具调用和图片输入默认关闭,Coding Agent 需要工具时请开启:
|
|
45
30
|
|
|
46
|
-
**数据目录**:默认 `~/.local/share/magpie-gemini-web`(可用环境变量 `GEMINI_WEB_DATA_DIR` 自定义),包含:
|
|
47
|
-
|
|
48
|
-
| 文件 | 内容 |
|
|
49
|
-
| :--- | :--- |
|
|
50
|
-
| `bin/gemini-web2api-go` | 后端二进制(权限 `0700`) |
|
|
51
|
-
| `config.json` | 后端配置,host 固定 `127.0.0.1`(权限 `0600`) |
|
|
52
|
-
| `secrets.json` | 本地随机生成的 `apiKey`(`gw_` 开头,插件访问后端用)和 `adminToken`(管理面板 `http://127.0.0.1:8083/admin` 的口令),均与 Google 账号无关(权限 `0600`) |
|
|
53
|
-
| `gemini.db` | 后端的账号与请求记录 |
|
|
54
|
-
| `backend.log` / `backend.pid` | 插件在后台启动的后端的日志与进程号 |
|
|
55
|
-
|
|
56
|
-
只要不删除 `secrets.json`,重新配置不会更换这两个值。要查看或管理已导入的账号,可打开管理面板,用 `adminToken` 登录;把它复制到剪贴板(macOS,不在终端显示):
|
|
57
31
|
```bash
|
|
58
|
-
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## 3. 连接已有后端(自定义地址 + API key)
|
|
64
|
-
|
|
65
|
-
已经自己运行了 gemini-web2api-go(其他机器、反向代理或自定义端口)时,登录时选 **「连接已有后端(自定义地址 + API key)」**:
|
|
66
|
-
|
|
67
|
-
- **Backend OpenAI base URL**:留空使用默认的 `http://127.0.0.1:8083/v1`,或填入你的地址(必须以 `/v1` 结尾;本地只允许 loopback HTTP,远程须 HTTPS)。
|
|
68
|
-
- **API key**:后端的 `API_KEY`(不是 Google Cookie)。
|
|
69
|
-
|
|
70
|
-
这种方式下插件不会安装或启动任何后端,Cookie 也需要你自己在后端管理面板导入(关键项包括 `SID`、`HSID`、`SSID`、`APISID`、`SAPISID`、`__Secure-1PSID`、`__Secure-1PSIDTS`,仅有两个 `__Secure-*` 值未必足够)。
|
|
71
|
-
|
|
72
|
-
> 重新登录(例如更换 API key 或 base URL)后,正在运行的 Magpie 不会重新读取 provider 地址,需要执行 `magpie plugin off magpie-gemini-web && magpie plugin on magpie-gemini-web` 或重启 Magpie App 才会生效。
|
|
73
|
-
>
|
|
74
|
-
> 0.1.0 登录时若留空 base URL,会把空字符串存进登录信息;0.1.1 起改为存入补全后的默认地址。若遇到网关返回 502「Gemini Web has no endpoint configured」([#3](https://github.com/BUKOWSKIREAL/magpie-gemini-web/issues/3)),升级后重新登录一次,或登录时显式填写 `http://127.0.0.1:8083/v1`。
|
|
75
|
-
|
|
76
|
-
### 命令行管理脚本(开发者)
|
|
77
|
-
|
|
78
|
-
从源码仓库也可以手动管理同一个数据目录里的后端(需要 Node.js >= 22):
|
|
79
|
-
```bash
|
|
80
|
-
git clone https://github.com/BUKOWSKIREAL/magpie-gemini-web.git
|
|
81
|
-
cd magpie-gemini-web
|
|
82
|
-
npm run backend:install # 下载并校验 v4.22.0 二进制,创建 config.json / secrets.json
|
|
83
|
-
npm run backend:start # 前台运行后端
|
|
84
|
-
npm run backend:status # 检查后端是否响应
|
|
32
|
+
magpie plugin options magpie-gemini-web '{"allowTools":true,"allowImages":true}'
|
|
85
33
|
```
|
|
86
|
-
用这些脚本装好的后端,一键配置会直接复用(同一套密钥),不会重复下载。
|
|
87
|
-
|
|
88
|
-
> 后端成功启动并列出基础模型目录,只代表本地服务通路正常,不代表全部能力可用。2026-10-14 已通过真实 Magpie 网关验证匿名 Flash Lite 普通对话;工具调用在非流式、流式和直连后端对照中均未成功,`tool_choice` 未得到满足。上游未返回缓存 token 统计,Magpie 显示的 0 不代表 Google 的实际缓存命中情况。登录态模型需要有效 Cookie。后续 Argon 补丁已验证真实 Magpie 普通对话、SSE 和首轮工具调用;工具结果回传含正确值但未严格遵循仅回显要求,图片和 Argon 缓存未验证。本地验收记录不随公开仓库或 npm 包分发。
|
|
89
|
-
|
|
90
|
-
---
|
|
91
34
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
35
|
+
| 选项 | 默认值 | 说明 |
|
|
36
|
+
| :--- | :--- | :--- |
|
|
37
|
+
| `allowTools` | `false` | 允许工具调用(实验性)。关闭时带工具的请求会返回 400 |
|
|
38
|
+
| `allowImages` | `false` | 允许图片输入 |
|
|
39
|
+
| `baseURL` | `http://127.0.0.1:8083/v1` | 默认后端地址 |
|
|
40
|
+
| `contextLimit` / `outputLimit` | `32768` / `8192` | 向客户端声明的上下文 / 输出上限 |
|
|
41
|
+
| `requestTimeoutMs` / `modelTimeoutMs` | `240000` / `10000` | 请求 / 拉取模型列表的超时(毫秒) |
|
|
95
42
|
|
|
96
|
-
|
|
97
|
-
| :--- | :--- | :--- | :--- |
|
|
98
|
-
| `baseURL` | string | `http://127.0.0.1:8083/v1` | 后端 OpenAI 兼容 Base URL(必须以 `/v1` 结尾,仅允许本地 loopback HTTP 或远程 HTTPS) |
|
|
99
|
-
| `allowTools` | boolean | `false` | 是否开启实验性工具调用。**默认为 false**,Coding Agent 若使用工具需设为 true |
|
|
100
|
-
| `allowImages` | boolean | `false` | 是否允许图片输入。**默认为 false** |
|
|
101
|
-
| `contextLimit` | integer | `32768` | 客户端声明的上下文上限预算 |
|
|
102
|
-
| `outputLimit` | integer | `8192` | 客户端声明的最大输出 Token 预算(需 $\le$ `contextLimit`) |
|
|
103
|
-
| `requestTimeoutMs`| integer | `240000` | 转发单次请求的超时时间(毫秒,默认 4 分钟) |
|
|
104
|
-
| `modelTimeoutMs` | integer | `10000` | 获取模型列表的超时时间(毫秒,默认 10 秒) |
|
|
43
|
+
## 思考档位
|
|
105
44
|
|
|
106
|
-
|
|
45
|
+
有 `-thinking` 版本的模型可以在 Magpie 里切换 `low` / `high`:`low` 用普通模型,`high` 用 `-thinking` 模型(扩展思考)。升级插件后执行 `magpie provider models gemini-web` 刷新模型列表。Argon 的「中」档等上游支持([#5](https://github.com/BUKOWSKIREAL/magpie-gemini-web/issues/5))。
|
|
107
46
|
|
|
108
|
-
|
|
47
|
+
## 安全与数据
|
|
109
48
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
49
|
+
- 后端只监听 `127.0.0.1`;Cookie 只交给本机后端,Magpie 保存的是本地生成的 API key。
|
|
50
|
+
- 后端二进制锁定官方 v4.22.0,下载和每次启动都校验 SHA-256。支持 macOS、Linux、Windows(x64 / arm64)。
|
|
51
|
+
- 数据目录:`~/.local/share/magpie-gemini-web`(可用 `GEMINI_WEB_DATA_DIR` 修改),日志在 `backend.log`。
|
|
52
|
+
- 插件限制并发为 1,但无法保证账号不被 Google 风控,请自行承担风险。
|
|
114
53
|
|
|
115
|
-
|
|
116
|
-
- 直接选 `-thinking` 模型照常可用,档位设置对它不生效。
|
|
117
|
-
- 网页端 Argon 的「中」档上游尚未支持(见 [#5](https://github.com/BUKOWSKIREAL/magpie-gemini-web/issues/5))。
|
|
118
|
-
- 升级插件后,执行 `magpie provider models gemini-web` 重新拉取模型列表,档位才会出现。
|
|
54
|
+
## 用量
|
|
119
55
|
|
|
120
|
-
|
|
56
|
+
Magpie 的额度卡片会显示 Google 账号的 **5 小时** 和 **每周** 用量与重置时间,和 <https://gemini.google.com/usage> 页面一致。每个 Google 账号单独一张卡片,标题是它的邮箱:
|
|
121
57
|
|
|
122
|
-
|
|
58
|
+
- 一键配置添加的账号会自动成为单独的一张卡片;
|
|
59
|
+
- 后端里已有的账号(比如旧版本添加的),在 Magpie 里给 Gemini Web 再添加一个账号,选 **「添加本地后端里已有的 Google 账号」**,从列表里选择即可。之后可以删掉旧的 `API key …` 登录,它显示的是整个账号池合在一起的用量(每个窗口取用得最多的那个账号)。
|
|
123
60
|
|
|
124
|
-
|
|
61
|
+
注意:请求最终由后端在账号池里轮流挑选 Google 账号,Magpie 在几张卡片之间怎么分配请求,不决定实际用的是哪个账号。只有一键配置的本地后端能显示用量;「连接已有后端」模式下读不到账号的 Cookie。
|
|
125
62
|
|
|
126
|
-
|
|
127
|
-
- **模型标识**(模型定义来自上游 v4.22.0):均为 HexID `95a4c0abcba24364`、Mode 8。
|
|
63
|
+
## 局限
|
|
128
64
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
| `gemini-4-argon` | 低 | `4 Argon` |
|
|
132
|
-
| `gemini-4-argon-thinking` | 高(带 `reasoning_content`) | `4 Argon High` |
|
|
65
|
+
- 工具调用由上游用提示词模拟,不一定可靠,Flash 系列经常不调用工具。
|
|
66
|
+
- 只开放 `gemini-` 开头的文本模型;`cost` 显示为 0 不代表 Google 的实际配额。
|
|
133
67
|
|
|
134
|
-
|
|
135
|
-
- **账号权限要求**:必须在后端管理面板配置拥有 Argon 访问权限的 Google 登录 Cookie;无 Cookie 时后端自动隐藏这两个模型。
|
|
136
|
-
- **结构化元数据校验**:由于 Google Web 对无权限、Cookie 失效或配额耗尽的请求会静默降级,后端在底层响应解析层(`argon.go`)对 Argon 请求实行严格的结构化元数据身份校验(解析 `wrb.fr` payload 中的模型 hex 与显示名,而非依赖模型自述文本),并按请求的思考档位只接受对应的显示名(普通档 `4 Argon`、扩展思考 `4 Argon High`),档位被静默替换同样视为不符。流式请求逐帧校验:确认 Argon 身份之前的帧先暂存、不发给客户端,确认后放行并继续增量转发(实测身份与首段正文同帧到达)。身份缺失或首次出现即不符时请求报错,不发送任何未验证内容;若放行之后上游又报告了其他模型,后端立即停止读取并发送流错误,但已发出的内容无法撤回。非流式请求仍完整读取后再校验。
|
|
137
|
-
- **协议验证状态**:`4.22.0-argon.1` 已通过真实 Magpie 网关验证两档的普通对话和 SSE:`gemini-4-argon` 后端记录实际模型 `4 Argon`,`gemini-4-argon-thinking` 记录 `4 Argon High` 并返回 `reasoning_content`。工具调用沿用 `4.21.0-argon.2` 的结论:首轮工具调用名称与参数正确,第二轮含正确测试值但附带额外说明,不代表工具兼容性已完全解决。本地验收记录不随公开仓库或 npm 包分发。
|
|
138
|
-
- **延迟优化**(自 `4.21.0-argon.2` 起;2026-10-14 直连后端实测,单次样本、上游波动大,仅供参考):
|
|
139
|
-
- XSRF token 按账号身份缓存,不再因每次响应刷新 SIDCC 而失效,登录态请求的前置开销由约 0.9–1.3 秒降至约 10 毫秒;Cookie 轮转后在后台重取 token。
|
|
140
|
-
- Argon 流式首字约 5.4 秒(同次总耗时 9.8 秒),不再等待整段生成结束。
|
|
141
|
-
- 工具调用块闭合且函数名有效时即结束读取上游并返回 `tool_calls`,Argon 首轮工具调用约 6.1 秒。
|
|
142
|
-
- 超过 `max_prompt_bytes` 的请求转附件时,工具定义(紧凑 JSON)与末尾格式提醒保留在正文,附件只含对话历史;158KB 带工具请求实测可正确发起工具调用并回传结果。
|
|
143
|
-
- 客户端断开或超时后,后端中止上游读取、上传与重试等待,并在请求记录中记为 499。
|
|
68
|
+
## Argon 增强版(可选)
|
|
144
69
|
|
|
145
|
-
|
|
70
|
+
官方后端已支持 `gemini-4-argon`(需付费 / Ultra 账号)。本仓库的补丁版额外校验返回的确实是 Argon(防止被静默降级),并降低延迟。需要 Go 1.26+:
|
|
146
71
|
|
|
147
|
-
需要 upstream `go.mod` 声明的 Go 版本(Go 1.26+)与 Git。从插件根目录执行:
|
|
148
72
|
```bash
|
|
73
|
+
git clone https://github.com/BUKOWSKIREAL/magpie-gemini-web.git && cd magpie-gemini-web
|
|
149
74
|
git clone https://github.com/zexadev/gemini-web2api-go.git backend-source
|
|
150
75
|
git -C backend-source checkout a65b1d1a5ac614c1b455857334b4f1c73ab8fcfb
|
|
151
76
|
git -C backend-source apply ../patches/argon.patch
|
|
77
|
+
npm run backend:build:argon
|
|
78
|
+
npm run backend:install:argon
|
|
152
79
|
```
|
|
153
80
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
> **从 `4.21.0-argon.*` 升级**:旧补丁新增的文件会与新补丁冲突,请删除 `backend-source/` 后按上面三行重新获取源码,再从下方第 1 步开始。
|
|
157
|
-
|
|
158
|
-
1. **构建 Argon 增强版后端**:
|
|
159
|
-
```bash
|
|
160
|
-
npm run backend:build:argon
|
|
161
|
-
```
|
|
162
|
-
该命令将在 `backend-source/` 目录执行 Go 编译,并在 `backend-build/` 生成校验清单 `manifest.json` 与二进制(版本 `4.22.0-argon.1`)。
|
|
163
|
-
|
|
164
|
-
2. **安装到本地数据目录**:
|
|
165
|
-
```bash
|
|
166
|
-
npm run backend:install:argon
|
|
167
|
-
```
|
|
168
|
-
安全地将构建好的 Argon 二进制安装到 `~/.local/share/magpie-gemini-web/bin/`,并同步更新编译清单,保留已有配置与密钥。
|
|
169
|
-
|
|
170
|
-
3. **启动 Argon 后端**:
|
|
171
|
-
```bash
|
|
172
|
-
npm run backend:start:argon
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
装好后也可以不手动启动:插件认可已安装 Argon 版的哈希,Magpie 启动时会像官方版一样在后台拉起它。若插件已在后台运行旧版后端,先结束它(`kill $(cat ~/.local/share/magpie-gemini-web/backend.pid)`)再启动,否则端口会被占用。
|
|
176
|
-
|
|
177
|
-
> **版本切换提示**:如果重新执行官方标准的 `npm run backend:install`,将自动下载官方原版二进制并覆盖当前版本:Argon 模型仍可用,但不再有身份校验与延迟优化;重新执行 `npm run backend:install:argon` 即可切回 Argon 版本。
|
|
178
|
-
|
|
179
|
-
---
|
|
81
|
+
安装后重启 Magpie 即可,插件会在后台启动 Argon 版(如有旧后端在运行,先 `kill $(cat ~/.local/share/magpie-gemini-web/backend.pid)`)。运行 `npm run backend:install` 可切回官方版。
|
|
180
82
|
|
|
181
|
-
##
|
|
83
|
+
## 许可证
|
|
182
84
|
|
|
183
|
-
|
|
184
|
-
- Gemini 网页版无原生 OpenAI 格式 Function Calling 协议,上游通过系统提示词模拟实现。
|
|
185
|
-
- 工具调用块需要完整解析后才以 `tool_calls` 返回(块外正文仍实时流式输出),准确度受模型指令遵循能力影响;实测 Flash 系列经常拒绝调用模拟工具。
|
|
186
|
-
- 默认配置下(`allowTools: false`),若请求包含 `tools` 或 `functions`,插件会直接拒绝并返回 400 提示,绝不静默剥离工具参数。
|
|
187
|
-
2. **媒体输入类型限制**:
|
|
188
|
-
- 即使开启 `allowImages: true`,插件也仅允许 `text` 与包含有效 URL 的 `image_url`,其他媒体类型(如 `input_audio` 等)将被严格拒绝。
|
|
189
|
-
3. **计费与配额说明**:
|
|
190
|
-
- 动态获取的模型目录中 `cost` 统一声明为 `0`,表示本插件不向客户端计收商业 API 费用,**不代表 Google 网页端的实际使用限额或剩余配额**。Token 计数由后端估算。
|
|
191
|
-
4. **模型范围过滤**:
|
|
192
|
-
- 插件仅暴露以 `gemini-` 开头且非多媒体专用模型(自动过滤 `image`、`video`、`music`、`canvas`、`research` 等变体)。
|
|
85
|
+
MIT。上游 gemini-web2api-go 同为 MIT(见 `LICENSE-UPSTREAM`)。
|
package/index.mjs
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
import { normalizeBaseURL, settings, credentials, listModels, forward, UNREACHABLE_HEADER } from './lib/backend.mjs';
|
|
2
2
|
import { ensureBackend, localBackend } from './lib/native.mjs';
|
|
3
3
|
import { parseCookieInput, importCookie } from './lib/setup.mjs';
|
|
4
|
+
import { usage, poolChoices, accountEmail } from './lib/usage.mjs';
|
|
4
5
|
|
|
5
6
|
const ID = 'gemini-web';
|
|
6
7
|
const GEMINI_URL = 'https://gemini.google.com/app';
|
|
7
8
|
|
|
9
|
+
// The backend keeps the session alive by rotating its cookies, which signs the browser it came
|
|
10
|
+
// from out of Google. A private window that is closed afterwards gives the backend a session of
|
|
11
|
+
// its own.
|
|
8
12
|
const COOKIE_STEPS = [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
'2.
|
|
12
|
-
'3.
|
|
13
|
+
'请用浏览器的无痕 / 隐私窗口(推荐 Firefox),不要用你平时登录着 Google 的窗口:后端会定时刷新这份登录,和浏览器共用会被 Google 踢出登录。',
|
|
14
|
+
`1. 在无痕窗口打开 ${GEMINI_URL} 并登录要使用的 Google 账号`,
|
|
15
|
+
'2. 按 F12(macOS 按 ⌥⌘I)打开开发者工具,切到 Network(网络)面板,刷新页面',
|
|
16
|
+
'3. 点列表里任意一个 gemini.google.com 的请求 → Headers(标头)→ Request Headers(请求标头)',
|
|
17
|
+
'4. 找到 cookie 那一行,复制它的值,粘贴到下面',
|
|
18
|
+
'5. 直接关掉无痕窗口,不要点「退出登录」(退出会让这份 Cookie 失效)',
|
|
13
19
|
'Cookie 只保存在本机的 Gemini Web 后端里,不会发到别处。'
|
|
14
20
|
].join('\n');
|
|
15
21
|
|
|
22
|
+
// signInFor is the Magpie sign-in for one account of the local backend's pool: the backend's
|
|
23
|
+
// key, with the account's id for its usage card and its Google email, which names the card and
|
|
24
|
+
// keeps each Google account a sign-in of its own (Magpie takes two sign-ins with one email for
|
|
25
|
+
// one account, and two with neither for one when their key is the same).
|
|
26
|
+
async function signInFor(local, account) {
|
|
27
|
+
const email = account.email || await accountEmail(account).catch(() => undefined);
|
|
28
|
+
return {
|
|
29
|
+
type: 'success',
|
|
30
|
+
key: local.apiKey,
|
|
31
|
+
metadata: { baseURL: local.baseURL, account: account.id, ...(email ? { email } : {}) }
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
// managedBackend is the local backend this plugin set up, when baseURL points at it.
|
|
17
36
|
function managedBackend(baseURL) {
|
|
18
37
|
const local = localBackend();
|
|
@@ -61,7 +80,8 @@ export const GeminiWebPlugin = async (_input, options = {}) => {
|
|
|
61
80
|
type: 'oauth',
|
|
62
81
|
label: '一键配置本地 Gemini Web(推荐)',
|
|
63
82
|
// Installs and starts the pinned backend, then takes the Gemini cookie the user pastes.
|
|
64
|
-
// The result carries the backend's own API key, so Magpie saves an API-key sign-in
|
|
83
|
+
// The result carries the backend's own API key, so Magpie saves an API-key sign-in,
|
|
84
|
+
// one for each Google account (see signInFor).
|
|
65
85
|
async authorize() {
|
|
66
86
|
const local = await ensureBackend();
|
|
67
87
|
return {
|
|
@@ -69,12 +89,14 @@ export const GeminiWebPlugin = async (_input, options = {}) => {
|
|
|
69
89
|
method: 'code',
|
|
70
90
|
instructions: COOKIE_STEPS,
|
|
71
91
|
async callback(code) {
|
|
92
|
+
let cookie, added;
|
|
72
93
|
try {
|
|
73
|
-
|
|
94
|
+
cookie = parseCookieInput(code);
|
|
95
|
+
added = await importCookie(local, cookie);
|
|
74
96
|
} catch (err) {
|
|
75
97
|
return { type: 'failed', error: err.message };
|
|
76
98
|
}
|
|
77
|
-
return
|
|
99
|
+
return signInFor(local, { id: added.id, cookie });
|
|
78
100
|
}
|
|
79
101
|
};
|
|
80
102
|
}
|
|
@@ -101,6 +123,38 @@ export const GeminiWebPlugin = async (_input, options = {}) => {
|
|
|
101
123
|
type: 'success',
|
|
102
124
|
metadata: { baseURL: normalizeBaseURL(inputs?.baseURL || opts.baseURL) }
|
|
103
125
|
})
|
|
126
|
+
}, {
|
|
127
|
+
type: 'oauth',
|
|
128
|
+
label: '添加本地后端里已有的 Google 账号',
|
|
129
|
+
// Magpie asks through ask(), listing the pool as it is now; prompts holds the question
|
|
130
|
+
// for OpenCode, and makes Magpie hand authorize the answer.
|
|
131
|
+
prompts: [{ type: 'select', key: 'account', message: '选择 Google 账号', options: [] }],
|
|
132
|
+
async ask(inputs) {
|
|
133
|
+
if (inputs?.account) return null;
|
|
134
|
+
if (!localBackend()) throw new Error('还没有本地后端:请先用「一键配置本地 Gemini Web」');
|
|
135
|
+
const choices = await poolChoices();
|
|
136
|
+
if (!choices.length) throw new Error('本地后端里没有启用的 Google 账号');
|
|
137
|
+
return {
|
|
138
|
+
type: 'select',
|
|
139
|
+
key: 'account',
|
|
140
|
+
message: '选择 Google 账号',
|
|
141
|
+
placeholder: '',
|
|
142
|
+
options: choices.map(c => ({ label: c.label, value: String(c.id), hint: c.hint }))
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
async authorize(inputs) {
|
|
146
|
+
const local = localBackend();
|
|
147
|
+
if (!local) throw new Error('还没有本地后端:请先用「一键配置本地 Gemini Web」');
|
|
148
|
+
const id = Number(inputs?.account);
|
|
149
|
+
const chosen = (await poolChoices()).find(c => c.id === id);
|
|
150
|
+
if (!chosen) throw new Error(`本地后端里没有启用的账号 #${inputs?.account}`);
|
|
151
|
+
return {
|
|
152
|
+
url: '',
|
|
153
|
+
method: 'auto',
|
|
154
|
+
instructions: '',
|
|
155
|
+
callback: async () => signInFor(local, chosen)
|
|
156
|
+
};
|
|
157
|
+
}
|
|
104
158
|
}],
|
|
105
159
|
async loader(getAuth) {
|
|
106
160
|
const auth = await getAuth();
|
|
@@ -111,6 +165,25 @@ export const GeminiWebPlugin = async (_input, options = {}) => {
|
|
|
111
165
|
baseURL: c.baseURL,
|
|
112
166
|
fetch: (input, init) => forwardManaged(input, init, getAuth, opts)
|
|
113
167
|
};
|
|
168
|
+
},
|
|
169
|
+
// Magpie's usage card: the 5-hour and weekly limits of each Google account in the local
|
|
170
|
+
// backend's pool, as gemini.google.com/usage shows them. A backend this plugin doesn't
|
|
171
|
+
// manage keeps its accounts out of reach. No read marks the sign-in: the Magpie key
|
|
172
|
+
// belongs to the backend, not to Google.
|
|
173
|
+
async usage(getAuth) {
|
|
174
|
+
const auth = await getAuth();
|
|
175
|
+
if (auth?.type !== 'api') return { error: 'Gemini Web 还没有登录', windows: [], signIn: 'kept' };
|
|
176
|
+
let c;
|
|
177
|
+
try {
|
|
178
|
+
c = credentials(auth, opts);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
return { error: err.message, windows: [], signIn: 'kept' };
|
|
181
|
+
}
|
|
182
|
+
if (!managedBackend(c.baseURL)) {
|
|
183
|
+
return { error: '只能读取插件自己管理的本地后端的额度(连接已有后端时读不到)', windows: [], signIn: 'kept' };
|
|
184
|
+
}
|
|
185
|
+
const account = auth.metadata?.account;
|
|
186
|
+
return { ...(await usage(Number.isInteger(account) ? { account } : {})), signIn: 'kept' };
|
|
114
187
|
}
|
|
115
188
|
},
|
|
116
189
|
provider: {
|
package/lib/usage.mjs
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Reads how much of each Gemini account's usage limits is gone, the way the Usage page at
|
|
2
|
+
// gemini.google.com/usage does: a persisted GraphQL query whose getUsageInfo lists a 5-hour and
|
|
3
|
+
// a weekly pool. The cookies come from the local backend's account pool, which also writes back
|
|
4
|
+
// the cookies Google rotates, so the copy there is always the live one.
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { getDataDir } from './native.mjs';
|
|
7
|
+
|
|
8
|
+
const EXECUTE_QUERY = 'https://gemini.google.com/_/BardChatUi/graphql/schemas/GEMINI_WEB_GRAPHQL/executeQuery';
|
|
9
|
+
// UsagePageQuery, and the variables the Usage page sends with it.
|
|
10
|
+
const USAGE_QUERY = '2/dWbCvkVB0X16r9NPnxifeOAAqdCqMf0xeWvau9yPy7g=';
|
|
11
|
+
const USAGE_VARIABLES = {
|
|
12
|
+
g1Request: {
|
|
13
|
+
entrypoints: [
|
|
14
|
+
'GEMINI_USAGE_SETTINGS_UPGRADE_BANNER',
|
|
15
|
+
'GEMINI_USAGE_SETTINGS_AI_CREDITS_UPSELL_CARD',
|
|
16
|
+
'GEMINI_USAGE_SETTINGS_UPGRADE_AND_CREDITS_BANNER'
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
workspaceRequest: {
|
|
20
|
+
recommendationsForSurfaceRequests: [{
|
|
21
|
+
recommendationSurface: { rpOnramp: 'GEMINI_CHAT_USAGE_LIMITS_UPSELL', version: '1' },
|
|
22
|
+
maxRecommendationCount: 1
|
|
23
|
+
}]
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';
|
|
27
|
+
|
|
28
|
+
// The pools the Usage page shows, under the names Magpie gives the same windows elsewhere.
|
|
29
|
+
const POOLS = [
|
|
30
|
+
{ type: 'GXU_FIVE_HOURLY', name: '5 hours', span: 5 * 3600 },
|
|
31
|
+
{ type: 'GXU_WEEKLY', name: '7 days', span: 7 * 24 * 3600 }
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// openReadOnly opens the backend's SQLite database without writing to it: bun:sqlite under
|
|
35
|
+
// Magpie's Bun, node:sqlite under Node.
|
|
36
|
+
async function openReadOnly(file) {
|
|
37
|
+
if (globalThis.Bun) {
|
|
38
|
+
const { Database } = await import('bun:sqlite');
|
|
39
|
+
const db = new Database(file, { readonly: true });
|
|
40
|
+
return { all: sql => db.query(sql).all(), close: () => db.close() };
|
|
41
|
+
}
|
|
42
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
43
|
+
const db = new DatabaseSync(file, { readOnly: true });
|
|
44
|
+
return { all: sql => db.prepare(sql).all(), close: () => db.close() };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// poolAccounts lists the enabled accounts in the backend's pool, each with the proxy it is
|
|
48
|
+
// bound to, if any.
|
|
49
|
+
export async function poolAccounts(dataDir = getDataDir()) {
|
|
50
|
+
const db = await openReadOnly(path.join(dataDir, 'gemini.db'));
|
|
51
|
+
try {
|
|
52
|
+
return db.all(`SELECT a.id, a.label, a.cookie, p.url AS proxy
|
|
53
|
+
FROM accounts a LEFT JOIN proxies p ON p.id = a.proxy_id AND p.enabled = 1
|
|
54
|
+
WHERE a.status = 'enabled' ORDER BY a.id`);
|
|
55
|
+
} finally {
|
|
56
|
+
db.close();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// parseUsage takes the pools out of an executeQuery reply: a JSON array of payloads, which
|
|
61
|
+
// may start with the )]}' guard.
|
|
62
|
+
export function parseUsage(text) {
|
|
63
|
+
const start = text.indexOf('[');
|
|
64
|
+
if (start < 0) throw new Error('Gemini 返回了无法识别的用量数据');
|
|
65
|
+
const payloads = JSON.parse(text.slice(start));
|
|
66
|
+
for (const p of Array.isArray(payloads) ? payloads : []) {
|
|
67
|
+
const info = p?.data?.viewer?.rpcs?.getUsageInfo;
|
|
68
|
+
if (info) return { tier: info.subscriptionTier, pools: Array.isArray(info.pools) ? info.pools : [] };
|
|
69
|
+
if (p?.errors?.length) throw new Error(`Gemini 拒绝了用量查询:${p.errors[0]?.message || 'unknown error'}`);
|
|
70
|
+
}
|
|
71
|
+
throw new Error('Gemini 的回复里没有用量数据');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// geminiRequest is one signed-in request to gemini.google.com as the account, through the
|
|
75
|
+
// proxy the backend bound it to. A redirect is Google sending a dead cookie to sign-in.
|
|
76
|
+
async function geminiRequest(url, account, headers, options) {
|
|
77
|
+
const fetchFn = options.fetchFn || globalThis.fetch;
|
|
78
|
+
const init = {
|
|
79
|
+
headers: { ...headers, Cookie: account.cookie, 'User-Agent': USER_AGENT },
|
|
80
|
+
redirect: 'manual',
|
|
81
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 15000)
|
|
82
|
+
};
|
|
83
|
+
// Bun's fetch takes an HTTP proxy; an account bound to one reaches Google only through it.
|
|
84
|
+
if (/^https?:\/\//i.test(account.proxy || '')) init.proxy = account.proxy;
|
|
85
|
+
const res = await fetchFn(url, init);
|
|
86
|
+
if (res.status >= 300 && res.status < 400 || res.status === 401 || res.status === 403) {
|
|
87
|
+
throw new Error('Cookie 已失效,请重新走一遍一键配置');
|
|
88
|
+
}
|
|
89
|
+
if (!res.ok) throw new Error(`Gemini 返回 HTTP ${res.status}`);
|
|
90
|
+
return res.text();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// accountUsage asks Gemini for one account's usage pools.
|
|
94
|
+
export async function accountUsage(account, options = {}) {
|
|
95
|
+
const url = `${EXECUTE_QUERY}?${new URLSearchParams({ query_signature: USAGE_QUERY, variables: JSON.stringify(USAGE_VARIABLES) })}`;
|
|
96
|
+
return parseUsage(await geminiRequest(url, account, {
|
|
97
|
+
Accept: 'application/json',
|
|
98
|
+
Origin: 'https://gemini.google.com',
|
|
99
|
+
Referer: 'https://gemini.google.com/usage'
|
|
100
|
+
}, options));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The Google account's email, from the signed-in app page's bootstrap data (WIZ_global_data's
|
|
104
|
+
// oPEP7c), kept per backend account: the page is large and the email doesn't change.
|
|
105
|
+
const emails = new Map();
|
|
106
|
+
const EMAIL_FIELD = /"oPEP7c":"([^"@\s]+@[^"\s]+)"/;
|
|
107
|
+
|
|
108
|
+
export async function accountEmail(account, options = {}) {
|
|
109
|
+
const known = emails.get(account.id);
|
|
110
|
+
if (known) return known;
|
|
111
|
+
const page = await geminiRequest('https://gemini.google.com/app', account, { Accept: 'text/html' }, options);
|
|
112
|
+
const email = EMAIL_FIELD.exec(page)?.[1];
|
|
113
|
+
if (email) emails.set(account.id, email);
|
|
114
|
+
return email;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// windowsOf turns an account's pools into Magpie usage windows, 5 hours then 7 days.
|
|
118
|
+
export function windowsOf(pools) {
|
|
119
|
+
const out = [];
|
|
120
|
+
for (const { type, name, span } of POOLS) {
|
|
121
|
+
const p = pools.find(x => x?.poolType === type);
|
|
122
|
+
if (typeof p?.percentageUsed !== 'number' || !(p.percentageUsed >= 0)) continue;
|
|
123
|
+
const w = { name, used: Math.min(100, Math.round(p.percentageUsed * 100)) };
|
|
124
|
+
const at = Date.parse(p.gxu?.windowRefreshesAt ?? '');
|
|
125
|
+
if (Number.isFinite(at)) w.resetsAt = new Date(at).toISOString();
|
|
126
|
+
w.span = span;
|
|
127
|
+
out.push(w);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const shortName = who => who.replace(/@gmail\.com$/i, '');
|
|
133
|
+
const soonest = w => (w.resetsAt ? Date.parse(w.resetsAt) : Infinity);
|
|
134
|
+
|
|
135
|
+
// poolWindows makes one card of several accounts. The backend takes them in turn, so the pool
|
|
136
|
+
// runs short once any one does: each window is its tightest account's, the most used and of
|
|
137
|
+
// those the soonest to start again, as Magpie's own card reads a family of model windows. The
|
|
138
|
+
// accounts' own figures, and any account that couldn't be read, go in its tooltip.
|
|
139
|
+
export function poolWindows(read) {
|
|
140
|
+
const out = [];
|
|
141
|
+
for (const { name } of POOLS) {
|
|
142
|
+
const each = read.flatMap(r => (r.windows || []).filter(w => w.name === name).map(w => ({ ...w, who: r.who })));
|
|
143
|
+
if (!each.length) continue;
|
|
144
|
+
const top = each.reduce((a, b) => (b.used > a.used || (b.used === a.used && soonest(b) < soonest(a)) ? b : a));
|
|
145
|
+
const { who, ...w } = top;
|
|
146
|
+
const parts = each.map(x => `${shortName(x.who)} ${x.used}%`);
|
|
147
|
+
for (const r of read) if (r.error) parts.push(`${shortName(r.who)} 读取失败`);
|
|
148
|
+
out.push({ ...w, display: `各账号已用 ${parts.join(',')}` });
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// usage is the Magpie usage card: one account's 5-hour and weekly windows, as
|
|
154
|
+
// gemini.google.com/usage shows them, when the sign-in names its backend account; else the
|
|
155
|
+
// whole pool's, as one card. Accounts go by their Google emails (#id until that is known).
|
|
156
|
+
// Magpie hides a card's windows behind an error, so a pool account that can't be read only
|
|
157
|
+
// says so in the tooltip while the others still show.
|
|
158
|
+
export async function usage(options = {}) {
|
|
159
|
+
let accounts;
|
|
160
|
+
try {
|
|
161
|
+
accounts = await (options.poolAccounts || poolAccounts)(options.dataDir);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
return { error: `读不到本地后端的账号池:${err?.message || err}`, windows: [] };
|
|
164
|
+
}
|
|
165
|
+
if (options.account !== undefined) {
|
|
166
|
+
accounts = accounts.filter(a => a.id === options.account);
|
|
167
|
+
if (!accounts.length) return { error: `本地后端里已经没有这个账号(#${options.account}),可能被停用或删除了`, windows: [] };
|
|
168
|
+
}
|
|
169
|
+
if (!accounts.length) return { error: '本地后端里没有启用的 Google 账号', windows: [] };
|
|
170
|
+
|
|
171
|
+
const read = await Promise.all(accounts.map(async a => {
|
|
172
|
+
const [u, email] = await Promise.allSettled([accountUsage(a, options), accountEmail(a, options)]);
|
|
173
|
+
const who = (email.status === 'fulfilled' && email.value) || `#${a.id}`;
|
|
174
|
+
return u.status === 'fulfilled' ? { who, windows: windowsOf(u.value.pools) } : { who, error: u.reason?.message || String(u.reason) };
|
|
175
|
+
}));
|
|
176
|
+
const user = read.length > 1 ? `${read[0].who} +${read.length - 1}` : read[0].who;
|
|
177
|
+
const several = read.length > 1;
|
|
178
|
+
const windows = several ? poolWindows(read) : read[0].windows || [];
|
|
179
|
+
if (!windows.length) {
|
|
180
|
+
const error = read.filter(r => r.error).map(r => (several ? `${r.who}:` : '') + r.error).join(';');
|
|
181
|
+
return { user, error: error || 'Gemini 没有返回用量窗口', windows: [] };
|
|
182
|
+
}
|
|
183
|
+
return { user, windows };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// poolChoices lists the pool's accounts for a sign-in to pick one, each by its email.
|
|
187
|
+
export async function poolChoices(options = {}) {
|
|
188
|
+
const accounts = await (options.poolAccounts || poolAccounts)(options.dataDir);
|
|
189
|
+
return Promise.all(accounts.map(async a => {
|
|
190
|
+
const email = await accountEmail(a, options).catch(() => undefined);
|
|
191
|
+
return { id: a.id, email, label: email || `#${a.id}`, hint: email ? `#${a.id}` : a.label };
|
|
192
|
+
}));
|
|
193
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magpie-gemini-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Gemini Web provider for Magpie, backed by gemini-web2api-go",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"scripts/",
|
|
24
24
|
"patches/",
|
|
25
25
|
"README.md",
|
|
26
|
+
"README.en.md",
|
|
26
27
|
"LICENSE",
|
|
27
28
|
"LICENSE-UPSTREAM"
|
|
28
29
|
],
|