magpie-gemini-web 0.4.1 → 0.5.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
@@ -2,11 +2,15 @@
2
2
 
3
3
  [中文](README.md) | English
4
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.
5
+ Use Gemini on the web and Google AI Studio through your Google account in [Magpie](https://usemagpie.ai/). The plugin runs [gemini-web2api-go](https://github.com/zexadev/gemini-web2api-go) and [AIStudio2API](https://github.com/Mag1cFall/AIStudio2API) backends on your machine, turning Google web interfaces into OpenAI-compatible APIs.
6
+
7
+ The plugin provides two independent providers:
8
+ - `gemini-web`: the Gemini web app; see [Getting started](#getting-started-gemini-web).
9
+ - `ai-studio`: Google AI Studio, with native tool calling; see [Google AI Studio](#google-ai-studio-ai-studio).
6
10
 
7
11
  > **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
12
 
9
- ## Getting started
13
+ ## Getting started (Gemini Web)
10
14
 
11
15
  1. Install **magpie-gemini-web** from the Magpie plugin market.
12
16
  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.
@@ -26,7 +30,7 @@ From then on, Magpie starts the backend automatically every time it launches. To
26
30
 
27
31
  **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`.
28
32
 
29
- ## Plugin options
33
+ ## Plugin options (Gemini Web)
30
34
 
31
35
  Tool calls are on by default (coding agents send tools with every request); image input is off. To allow images:
32
36
 
@@ -40,7 +44,7 @@ magpie plugin options magpie-gemini-web '{"allowImages":true}'
40
44
  | `allowImages` | `false` | Allows image input |
41
45
  | `baseURL` | `http://127.0.0.1:8083/v1` | Default backend URL |
42
46
  | `contextLimit` / `outputLimit` | `1000000` / `8192` | Context and output limits reported to clients |
43
- | `requestTimeoutMs` / `modelTimeoutMs` | `240000` / `10000` | Timeouts in ms for requests and for fetching the model list |
47
+ | `requestTimeoutMs` / `modelTimeoutMs` | `240000` / `10000` | Timeouts in ms for requests and for fetching the model list. The request timeout counts each wait on the backend, so a reply that keeps coming is never cut off |
44
48
  | `stripCitations` | `true` | Removes the citation markers Gemini adds to replies, `[cite: 1]` and `[cite_start]`, tool call arguments included. Set it to `false` while Gemini edits code that contains such text itself |
45
49
 
46
50
  ## Thinking effort
@@ -72,11 +76,69 @@ Usage and emails are read the way the backend reaches Google for that account. W
72
76
 
73
77
  So with a proxy pool, an account just added by one-click setup has no email yet. Send one request with it, sign in to it again with **「添加本地后端里已有的 Google 账号」**, then remove the sign-in without an email.
74
78
 
75
- ## Limitations
79
+ ## Limitations (Gemini Web)
76
80
 
77
81
  - Upstream emulates tool calls with prompts, so they aren't always reliable. Flash models often don't call tools.
78
82
  - Only text models whose names start with `gemini-` are exposed. A `cost` of 0 doesn't reflect your actual Google quota.
79
83
 
84
+ ## Google AI Studio (`ai-studio`)
85
+
86
+ Uses Google AI Studio through [AIStudio2API](https://github.com/Mag1cFall/AIStudio2API) (pinned to v0.2.12). Unlike Gemini Web, tool calls use native Function Calling, thinking models offer four levels in Magpie, `minimal` / `low` / `medium` / `high` (`minimal` is the fastest and uses the least quota), and generation needs no browser.
87
+
88
+ ### Getting started
89
+
90
+ 1. Sign in to Google AI Studio and choose **一键配置本地 Google AI Studio(推荐)** (one-click local setup). The plugin downloads AIStudio2API, verifies its SHA-256 and starts it in the background.
91
+ 2. Sign in to your Google account in the separate login window that opens (your usual browser is not affected), then close it. The first time, the login browser Camoufox is downloaded first, so the window can take a few minutes to appear; the login must finish within 15 minutes.
92
+
93
+ To add another account, run the one-click setup again. All accounts share one sign-in card: the local service picks the account for each request, using one until it cools down, then the next.
94
+
95
+ Other sign-in options:
96
+
97
+ - **使用本机已有的 AI Studio 服务** (use the local service already set up): signs in when the machine already has accounts (added from the command line, say), without a login window.
98
+ - **重新登录失效的 AI Studio 账号** (sign an expired account in again): when an account's Google login expires, choose it here to sign in again.
99
+ - **连接已有 AIStudio2API** (connect an existing AIStudio2API): if you run AIStudio2API yourself, enter its address (default `http://127.0.0.1:2048/v1`) and `PROXY_API_KEY`.
100
+
101
+ Usage card: AI Studio reports no remaining quota, so the card shows the pool's accounts and the models they are cooling down on. Each model gets a meter; a full one means no account can use it for now, with the time it comes back.
102
+
103
+ ### Plugin options
104
+
105
+ `allowTools`, `allowImages`, `requestTimeoutMs` and `modelTimeoutMs` apply to both providers; the other AI Studio options go under `studio`. The root `baseURL`, `contextLimit`, `outputLimit` and `stripCitations` apply to Gemini Web only.
106
+
107
+ ```bash
108
+ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:7890"}}'
109
+ ```
110
+
111
+ | Option | Default | Description |
112
+ | :--- | :--- | :--- |
113
+ | `studio.proxy` | none | Proxy for reaching Google (http / https / socks5). The `AI_STUDIO_PROXY` environment variable works too |
114
+ | `studio.baseURL` | `http://127.0.0.1:2048/v1` | Default address for connecting an existing AIStudio2API |
115
+ | `studio.contextLimit` / `studio.outputLimit` | the model catalog's | Limits declared to clients; they can only lower the catalog's |
116
+ | `studio.allowTools` etc. | as at the root | Override the root option of the same name for AI Studio only |
117
+
118
+ AI Studio replies keep their citation marks such as `[cite: 1]`; they can't be stripped.
119
+
120
+ ### Command line
121
+
122
+ When an error from the plugin asks you to run a command, it gives the command with its full path, ready to copy. In the plugin's directory you can also use:
123
+
124
+ | Command | What it does |
125
+ | :--- | :--- |
126
+ | `npm run studio:status` | Show the service and account status |
127
+ | `npm run studio:start` / `studio:stop` | Start / stop the local service |
128
+ | `npm run studio:login` | Add a Google account |
129
+ | `npm run studio:relogin -- [<account>]` | Sign an expired account in again (optional when only one is) |
130
+ | `npm run studio:install -- --archive <file>` | Install offline from a downloaded archive |
131
+ | `node scripts/studio.mjs import <dir>` | Import existing accounts while the service is stopped |
132
+ | `npm run studio:smoke` | Live acceptance run (uses account quota) |
133
+
134
+ ### Security and data
135
+
136
+ - The service listens on `127.0.0.1` only; Magpie keeps a locally generated API key.
137
+ - Data directory: `~/.local/share/magpie-ai-studio` (override with `AI_STUDIO_DATA_DIR`). The log is `backend.log`, started afresh on the next start once it passes 10 MB.
138
+ - A first setup uses port 2048, or another free port when 2048 is taken (by an AIStudio2API of your own, say), saved in the data directory's `config.json`.
139
+ - The downloaded archive stays in the data directory; installing again uses it when its checksum matches.
140
+ - Supported on macOS, Linux (x64 / arm64) and Windows x64.
141
+
80
142
  ## Native Task on macOS (experimental)
81
143
 
82
144
  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.
package/README.md CHANGED
@@ -2,11 +2,15 @@
2
2
 
3
3
  中文 | [English](README.en.md)
4
4
 
5
- 让 [Magpie](https://usemagpie.ai/) 通过你的 Google 账号使用 Gemini 网页版。插件在本机运行 [gemini-web2api-go](https://github.com/zexadev/gemini-web2api-go) 后端,把 Gemini 网页转成 OpenAI 兼容接口。
5
+ 让 [Magpie](https://usemagpie.ai/) 通过你的 Google 账号使用 Gemini 网页版与 Google AI Studio。插件在本机运行 [gemini-web2api-go](https://github.com/zexadev/gemini-web2api-go) 与 [AIStudio2API](https://github.com/Mag1cFall/AIStudio2API) 后端,把 Google 网页转成 OpenAI 兼容接口。
6
+
7
+ 插件提供两个互相独立的 Provider:
8
+ - `gemini-web`:Gemini 网页版,见下面的[开始使用](#开始使用-gemini-web);
9
+ - `ai-studio`:Google AI Studio,原生工具调用,见 [Google AI Studio](#google-ai-studio-ai-studio)。
6
10
 
7
11
  > **2026-10-16 更新**:现在正价支付的 Google AI Pro 可以直接在 Antigravity 中使用 `gemini-4-argon`,同理也可以直接用 Magpie 反代。
8
12
 
9
- ## 开始使用
13
+ ## 开始使用 (Gemini Web)
10
14
 
11
15
  1. 在 Magpie 插件市场安装 **magpie-gemini-web**。
12
16
  2. 登录 Gemini Web,选 **「一键配置本地 Gemini Web(推荐)」**。插件会自动下载本项目发布的优化后端、校验 SHA-256,并在后台启动;不需要安装 Go 或手动打补丁。
@@ -26,7 +30,7 @@
26
30
 
27
31
  **连接已有后端**:如果你自己运行了 gemini-web2api-go,登录时选「连接已有后端(自定义地址 + API key)」,填后端地址(以 `/v1` 结尾)和它的 `API_KEY`。
28
32
 
29
- ## 插件选项
33
+ ## 插件选项 (Gemini Web)
30
34
 
31
35
  工具调用默认开启(Coding Agent 每个请求都带工具),图片输入默认关闭。需要图片输入时:
32
36
 
@@ -40,7 +44,7 @@ magpie plugin options magpie-gemini-web '{"allowImages":true}'
40
44
  | `allowImages` | `false` | 允许图片输入 |
41
45
  | `baseURL` | `http://127.0.0.1:8083/v1` | 默认后端地址 |
42
46
  | `contextLimit` / `outputLimit` | `1000000` / `8192` | 向客户端声明的上下文 / 输出上限 |
43
- | `requestTimeoutMs` / `modelTimeoutMs` | `240000` / `10000` | 请求 / 拉取模型列表的超时(毫秒) |
47
+ | `requestTimeoutMs` / `modelTimeoutMs` | `240000` / `10000` | 请求 / 拉取模型列表的超时(毫秒)。请求超时按每次等待后端计算,回复持续输出时不会被打断 |
44
48
  | `stripCitations` | `true` | 去掉 Gemini 在回复里加的引用标记 `[cite: 1]`、`[cite_start]`(包括工具调用参数)。让 Gemini 编辑本身含有这类文本的代码时,可以设为 `false` |
45
49
 
46
50
  ## 思考档位
@@ -72,11 +76,69 @@ Magpie 的额度卡片会显示 Google 账号的 **5 小时** 和 **每周** 用
72
76
 
73
77
  所以配了代理池时,一键配置刚添加的账号暂时没有邮箱。先用它发一次请求,再用「添加本地后端里已有的 Google 账号」重新登录它,然后删掉没有邮箱的那个登录。
74
78
 
75
- ## 局限
79
+ ## 局限 (Gemini Web)
76
80
 
77
81
  - 工具调用由上游用提示词模拟,不一定可靠,Flash 系列经常不调用工具。
78
82
  - 只开放 `gemini-` 开头的文本模型;`cost` 显示为 0 不代表 Google 的实际配额。
79
83
 
84
+ ## Google AI Studio (`ai-studio`)
85
+
86
+ 通过 [AIStudio2API](https://github.com/Mag1cFall/AIStudio2API)(固定 v0.2.12)使用 Google AI Studio。和 Gemini Web 相比,工具调用走原生 Function Calling,思考模型可以在 Magpie 里切换 `minimal` / `low` / `medium` / `high` 四档(`minimal` 最快、最省额度),日常生成不需要浏览器。
87
+
88
+ ### 开始使用
89
+
90
+ 1. 登录 Google AI Studio,选 **「一键配置本地 Google AI Studio(推荐)」**。插件会自动下载 AIStudio2API、校验 SHA-256 并在后台启动。
91
+ 2. 在弹出的独立登录窗口里登录 Google 账号(不影响你平时用的浏览器),完成后关掉窗口即可。第一次要先下载登录用的浏览器 Camoufox,窗口可能要等几分钟才出现;需要在 15 分钟内完成登录。
92
+
93
+ 想加更多账号,再走一遍一键配置。所有账号共用一张登录卡片:每个请求用哪个账号由本地服务决定,先用一个,冷却了再换下一个。
94
+
95
+ 其他登录选项:
96
+
97
+ - **使用本机已有的 AI Studio 服务**:本机已经有账号(比如用命令行添加的)时直接登录,不打开登录窗口。
98
+ - **重新登录失效的 AI Studio 账号**:账号的 Google 登录过期后,在这里选中它重新登录。
99
+ - **连接已有 AIStudio2API**:你自己运行了 AIStudio2API 时,填它的地址(默认 `http://127.0.0.1:2048/v1`)和 `PROXY_API_KEY`。
100
+
101
+ 额度卡片:AI Studio 不提供剩余额度,卡片显示账号池里的账号和正在冷却的模型。每个模型一条进度条,满格表示暂时没有账号能用它,旁边是恢复时间。
102
+
103
+ ### 插件选项
104
+
105
+ `allowTools`、`allowImages`、`requestTimeoutMs`、`modelTimeoutMs` 两个 Provider 共用;其余 AI Studio 选项放在 `studio` 里,根级别的 `baseURL`、`contextLimit`、`outputLimit`、`stripCitations` 只对 Gemini Web 生效。
106
+
107
+ ```bash
108
+ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:7890"}}'
109
+ ```
110
+
111
+ | 选项 | 默认值 | 说明 |
112
+ | :--- | :--- | :--- |
113
+ | `studio.proxy` | 无 | 访问 Google 用的代理(http / https / socks5)。也可以用环境变量 `AI_STUDIO_PROXY` |
114
+ | `studio.baseURL` | `http://127.0.0.1:2048/v1` | 「连接已有 AIStudio2API」的默认地址 |
115
+ | `studio.contextLimit` / `studio.outputLimit` | 模型目录的值 | 向客户端声明的上限,只会调低 |
116
+ | `studio.allowTools` 等 | 同根级别 | 只对 AI Studio 覆盖根级别的同名选项 |
117
+
118
+ AI Studio 的回复保留引用标记 `[cite: 1]`,不能去掉。
119
+
120
+ ### 命令行
121
+
122
+ 插件报错里需要你运行命令时,会给出带完整路径的命令,复制即可运行。在插件目录里也可以用:
123
+
124
+ | 命令 | 作用 |
125
+ | :--- | :--- |
126
+ | `npm run studio:status` | 查看服务和账号状态 |
127
+ | `npm run studio:start` / `studio:stop` | 启动 / 停止本地服务 |
128
+ | `npm run studio:login` | 添加 Google 账号 |
129
+ | `npm run studio:relogin -- [<账号>]` | 重新登录过期的账号(只有一个时可省略) |
130
+ | `npm run studio:install -- --archive <文件>` | 用下载好的安装包离线安装 |
131
+ | `node scripts/studio.mjs import <目录>` | 服务停止时导入已有账号 |
132
+ | `npm run studio:smoke` | 真实请求验收(会用掉账号额度) |
133
+
134
+ ### 数据与安全
135
+
136
+ - 服务只监听 `127.0.0.1`,Magpie 保存的是本地生成的 API key。
137
+ - 数据目录:`~/.local/share/magpie-ai-studio`(可用 `AI_STUDIO_DATA_DIR` 修改)。日志在 `backend.log`,超过 10 MB 时下次启动换新。
138
+ - 第一次配置使用 2048 端口;被占用时(比如你自己也在跑 AIStudio2API)自动换一个,记在数据目录的 `config.json`。
139
+ - 下载的安装包留在数据目录,重装时校验通过就不再下载。
140
+ - 支持 macOS、Linux(x64 / arm64)和 Windows x64。
141
+
80
142
  ## macOS 原生 Task(实验性)
81
143
 
82
144
  在 macOS 上登录时会多出一个「macOS 原生 Task(实验性)」选项:请求不走 Gemini Web 后端,而是交给本机常驻服务,由 Gemini.app 自己的 Task 模式执行,工具调用走原生的 Function Calling 通道,模型为 `gemini-task-argon`。Windows 和 Linux 上不会出现这个选项,请继续使用上面的一键配置。
package/index.mjs CHANGED
@@ -63,7 +63,11 @@ async function forwardManaged(input, init, getAuth, opts) {
63
63
  }
64
64
 
65
65
  export const GeminiWebPlugin = async (_input, options = {}) => {
66
- const opts = settings(options);
66
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
67
+ settings(options);
68
+ }
69
+ const { studio: _studio, ...webOptions } = options;
70
+ const opts = settings(webOptions);
67
71
 
68
72
  // Bring a backend set up earlier back after Magpie or the computer restarts. Not awaited:
69
73
  // requests that arrive first start it themselves through forwardManaged.
@@ -220,4 +224,5 @@ export const GeminiWebPlugin = async (_input, options = {}) => {
220
224
  };
221
225
  };
222
226
 
227
+ export { AIStudioPlugin } from './lib/studio.mjs';
223
228
  export default GeminiWebPlugin;
package/lib/backend.mjs CHANGED
@@ -30,7 +30,7 @@ const KNOWN_OPTIONS = new Set([
30
30
  'stripCitations'
31
31
  ]);
32
32
 
33
- function makeOpenAIError(status, message, extraHeaders = {}) {
33
+ export function makeOpenAIError(status, message, extraHeaders = {}) {
34
34
  const codeMap = {
35
35
  400: 'invalid_request_error',
36
36
  401: 'unauthorized',
@@ -360,6 +360,47 @@ async function thinkingTwin(body, c, opts) {
360
360
  return cached?.ids.has(twin) ? twin : null;
361
361
  }
362
362
 
363
+ // waitTimer aborts its signal when one wait on the backend, from start() to stop(), outlasts ms.
364
+ function waitTimer(ms) {
365
+ const controller = new AbortController();
366
+ let timeout;
367
+ return {
368
+ signal: controller.signal,
369
+ start() {
370
+ clearTimeout(timeout);
371
+ timeout = setTimeout(() => {
372
+ controller.abort(new DOMException(`The backend sent nothing for ${ms} ms`, 'TimeoutError'));
373
+ }, ms);
374
+ },
375
+ stop() {
376
+ clearTimeout(timeout);
377
+ }
378
+ };
379
+ }
380
+
381
+ // timedBody passes a response body on, timing each read from the backend with timer. Time the
382
+ // reader of the returned stream takes between reads doesn't count.
383
+ function timedBody(body, timer) {
384
+ const reader = body.getReader();
385
+ return new ReadableStream({
386
+ async pull(controller) {
387
+ timer.start();
388
+ let piece;
389
+ try {
390
+ piece = await reader.read();
391
+ } finally {
392
+ timer.stop();
393
+ }
394
+ if (piece.done) controller.close();
395
+ else controller.enqueue(piece.value);
396
+ },
397
+ cancel(reason) {
398
+ timer.stop();
399
+ return reader.cancel(reason);
400
+ }
401
+ });
402
+ }
403
+
363
404
  export async function forward(input, init, getAuth, opts) {
364
405
  const req = new Request(input, init);
365
406
 
@@ -479,17 +520,20 @@ export async function forward(input, init, getAuth, opts) {
479
520
  }
480
521
  }
481
522
 
482
- const twin = await thinkingTwin(body, c, opts);
523
+ const twin = opts.nativeThinking ? null : await thinkingTwin(body, c, opts);
483
524
  if (twin) {
484
525
  body.model = twin;
485
526
  rawBody = JSON.stringify(body);
486
527
  }
487
528
  }
488
529
 
489
- const timeoutSignal = AbortSignal.timeout(opts.requestTimeoutMs);
490
- const signal = AbortSignal.any([req.signal, timeoutSignal]);
530
+ // requestTimeoutMs bounds each wait on the backend, for the response headers and then for each
531
+ // piece of the body, so a long streamed reply that keeps coming is never cut off.
532
+ const timer = waitTimer(opts.requestTimeoutMs);
533
+ const signal = AbortSignal.any([req.signal, timer.signal]);
491
534
 
492
535
  let upstreamRes;
536
+ timer.start();
493
537
  try {
494
538
  upstreamRes = await fetch(incomingUrl, {
495
539
  method: req.method,
@@ -502,13 +546,15 @@ export async function forward(input, init, getAuth, opts) {
502
546
  if (req.signal.aborted) {
503
547
  throw req.signal.reason || new DOMException('The operation was aborted.', 'AbortError');
504
548
  }
505
- if (timeoutSignal.aborted || err?.name === 'TimeoutError') {
549
+ if (timer.signal.aborted || err?.name === 'TimeoutError') {
506
550
  return makeOpenAIError(504, 'Gateway timeout: backend request timed out');
507
551
  }
508
552
  if (err?.name === 'AbortError') {
509
553
  throw err;
510
554
  }
511
555
  return makeOpenAIError(502, 'Bad gateway: failed to reach backend service', { [UNREACHABLE_HEADER]: 'unreachable' });
556
+ } finally {
557
+ timer.stop();
512
558
  }
513
559
 
514
560
  const respHeaders = new Headers(upstreamRes.headers);
@@ -519,11 +565,11 @@ export async function forward(input, init, getAuth, opts) {
519
565
  }
520
566
 
521
567
  // Gemini's citation markers come out of a chat reply, streamed or whole (see cite.mjs).
522
- let respBody = upstreamRes.body;
568
+ let respBody = upstreamRes.body && timedBody(upstreamRes.body, timer);
523
569
  if (opts.stripCitations && incomingUrl === chatUrl && upstreamRes.ok && respBody) {
524
570
  const type = upstreamRes.headers.get('content-type') || '';
525
571
  if (type.includes('text/event-stream')) respBody = citeStream(respBody);
526
- else if (type.includes('json')) respBody = citeJSON(await upstreamRes.text());
572
+ else if (type.includes('json')) respBody = citeJSON(await new Response(respBody).text());
527
573
  }
528
574
 
529
575
  return new Response(respBody, {
package/lib/setup.mjs CHANGED
@@ -78,7 +78,12 @@ export function parseCookieInput(input) {
78
78
  return [...cookies].map(([n, v]) => `${n}=${v}`).join('; ');
79
79
  }
80
80
 
81
- async function adminCall(setup, fetchFn, method, path, body) {
81
+ const ADMIN_TIMEOUT_MS = 60000;
82
+
83
+ // adminCall is one request to the backend's admin API. A request that gets no answer within
84
+ // ADMIN_TIMEOUT_MS fails with timeoutMessage when one is given: the backend was reached, but what
85
+ // it does for the request took too long.
86
+ async function adminCall(setup, fetchFn, method, path, body, timeoutMessage) {
82
87
  let res;
83
88
  try {
84
89
  res = await fetchFn(`${setup.origin}/admin/api/${path}`, {
@@ -89,9 +94,10 @@ async function adminCall(setup, fetchFn, method, path, body) {
89
94
  },
90
95
  body: body ? JSON.stringify(body) : undefined,
91
96
  redirect: 'error',
92
- signal: AbortSignal.timeout(60000)
97
+ signal: AbortSignal.timeout(ADMIN_TIMEOUT_MS)
93
98
  });
94
99
  } catch (err) {
100
+ if (timeoutMessage && err?.name === 'TimeoutError') throw new Error(timeoutMessage);
95
101
  throw new Error(`连不上本地 Gemini Web 后端(${setup.origin}):${err?.message || err}`);
96
102
  }
97
103
  let data = {};
@@ -102,6 +108,22 @@ async function adminCall(setup, fetchFn, method, path, body) {
102
108
  return data;
103
109
  }
104
110
 
111
+ // The check is the backend fetching gemini.google.com with the cookie, directly or through the
112
+ // backend's own proxy pool: neither Magpie's proxy nor HTTP_PROXY reach it.
113
+ function proxyHint(setup) {
114
+ return `后端不走 Magpie 的代理,也不读系统代理(HTTP_PROXY / HTTPS_PROXY):需要代理才能访问 Google 的话,请在管理面板 ${setup.origin}/admin 的代理池里添加(登录口令是数据目录 secrets.json 里的 adminToken),然后重试。`;
115
+ }
116
+
117
+ // checkFailure is the error for a check that came back not ok. The backend words the cookie's
118
+ // own verdicts (signed out, redirected to sign-in, rate limited) itself and prefixes everything
119
+ // else, a failed fetch of gemini.google.com among it, with "检测失败" (explainCookieFailure).
120
+ function checkFailure(setup, detail) {
121
+ if (detail.startsWith('检测失败')) {
122
+ return new Error(`后端没能访问 gemini.google.com 来检测这个 Cookie(${detail})。${proxyHint(setup)}`);
123
+ }
124
+ return new Error(`这个 Cookie 无法登录 Gemini:${detail || '检测未通过'}`);
125
+ }
126
+
105
127
  // importCookie adds the cookie to the backend's account pool and checks it signs in. An
106
128
  // account that fails the check is removed again, and the error says why.
107
129
  export async function importCookie(setup, cookie, options = {}) {
@@ -116,14 +138,15 @@ export async function importCookie(setup, cookie, options = {}) {
116
138
 
117
139
  let check;
118
140
  try {
119
- check = await adminCall(setup, fetchFn, 'POST', `cookies/${added.id}/check`);
141
+ check = await adminCall(setup, fetchFn, 'POST', `cookies/${added.id}/check`, undefined,
142
+ `后端 ${ADMIN_TIMEOUT_MS / 1000} 秒内没检测完这个 Cookie:检测要由后端访问 gemini.google.com,这一步一直没有返回,多半是后端连不上 Google。${proxyHint(setup)}`);
120
143
  } catch (err) {
121
144
  await adminCall(setup, fetchFn, 'DELETE', `cookies/${added.id}`).catch(() => {});
122
145
  throw err;
123
146
  }
124
147
  if (!check.ok) {
125
148
  await adminCall(setup, fetchFn, 'DELETE', `cookies/${added.id}`).catch(() => {});
126
- throw new Error(`这个 Cookie 无法登录 Gemini:${check.detail || '检测未通过'}`);
149
+ throw checkFailure(setup, check.detail || '');
127
150
  }
128
151
  return { id: added.id, detail: check.detail || '' };
129
152
  }