magpie-gemini-web 0.11.0 → 0.12.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
@@ -21,7 +21,7 @@ The plugin provides two independent providers that can be used individually or t
21
21
  | **Image generation** | Nano Banana models, for Magpie's image generation (Magpie v0.1.2075+, see below) | Not supported |
22
22
  | **Sign-in method** | Sign in directly in an isolated popup browser window | Copy `cookie` from a private/incognito window |
23
23
  | **Usage cards** | Shows models currently on cooldown and recovery times | Syncs **5-hour** & **weekly** usage and reset times from the web |
24
- | **Local backend** | AIStudio2API (`v0.3.2`) | gemini-web2api-go (`4.27.1-magpie.1` optimized build) |
24
+ | **Local backend** | AIStudio2API (`v0.3.4`) | gemini-web2api-go (`4.28.0-magpie.1` optimized build) |
25
25
 
26
26
  ---
27
27
 
@@ -56,7 +56,7 @@ Install **magpie-gemini-web** from the Magpie plugin market, then sign in to eit
56
56
 
57
57
  - **One card per account:** Each Google account gets its own usage card titled with its email address.
58
58
  - *Migrating older Gemini Web accounts:* For accounts added in earlier versions, add a Gemini Web account in Magpie and choose **「添加本地后端里已有的 Google 账号」** (Add an existing Google account from the local backend) to create individual cards, then remove the old pooled `API key …` card.
59
- - **Automatic account rotation:** The local backend selects which Google account serves each request (Gemini Web rotates across the pool; AI Studio uses one account until cooldown and switches to the next). Switching cards in Magpie does not change which account the backend picks.
59
+ - **Automatic account rotation:** The local backend selects which Google account serves each request (Gemini Web rotates across the pool; AI Studio uses one account until cooldown and switches to the next, with optional account priority order and per-model account bindings configurable in the admin page or via `studio.accountOrder` and `studio.modelAccounts`). Switching cards in Magpie does not change which account the backend picks.
60
60
  - **Usage reporting:**
61
61
  - `gemini-web`: Available only with the one-click local backend. Usage is read from Google again whenever Magpie asks for it (at most once every 30 seconds per account), so pressing refresh shows the current allowance, including usage from the Gemini app, the website or other devices. On its own, Magpie asks about once a minute while the Usage page or the menu bar's allowances panel is open, whenever it updates allowances shown in the menu bar (every 3 minutes by default), and at most once a minute while it routes requests among accounts; otherwise it doesn't ask. A failed read isn't retried on its own for 10 minutes; pressing refresh in that time retries it at once (press it again if the first press doesn't).
62
62
  - `ai-studio`: AI Studio does not report remaining total quota; cards display models currently on cooldown and their recovery times, or prompt when re-authentication is needed.
@@ -114,6 +114,8 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
114
114
  | `baseURL` | `http://127.0.0.1:8083/v1` | `gemini-web` only | Default Gemini Web backend URL |
115
115
  | `contextLimit` / `outputLimit` | `1000000` / `8192` | `gemini-web` only | Context and output limits reported to clients |
116
116
  | `studio.proxy` | none | `ai-studio` only | Proxy for reaching Google (`http` / `https` / `socks5`), or set `AI_STUDIO_PROXY` env var; when set, it takes precedence over the proxy saved in the admin page. An empty value counts as unset |
117
+ | `studio.accountOrder` | none | `ai-studio` only | Account priority order (comma-separated string or string array, e.g. `["b@gmail.com","a@gmail.com"]`); seeds `ACCOUNT_ORDER` in `.env` when unset, while preserving any non-empty value saved in the admin page |
118
+ | `studio.modelAccounts` | none | `ai-studio` only | Per-model account bindings (`"model:acc1|acc2"` string or `{ "gemini-3.1-pro-preview": ["acc1", "acc2"] }` object); seeds `MODEL_ACCOUNTS` in `.env` when unset, while preserving any non-empty value saved in the admin page |
117
119
  | `studio.baseURL` | `http://127.0.0.1:2048/v1` | `ai-studio` only | Default URL when connecting an existing AIStudio2API instance |
118
120
  | `studio.contextLimit` / `studio.outputLimit` | Model catalog defaults | `ai-studio` only | Limits reported to clients (can only lower the catalog defaults) |
119
121
  | `studio.imageModels` | `true` | `ai-studio` only | List the Nano Banana image models, for Magpie's image generation; `false` lists text models only |
@@ -176,8 +178,8 @@ When a plugin error asks you to run a command, it includes the full ready-to-cop
176
178
  - Gemini Web takes at most 2 requests at once per sign-in (AI Studio 1), adjustable in Magpie's provider settings. Google account rate-limiting or flagging cannot be completely ruled out—use at your own risk.
177
179
 
178
180
  **AI Studio upgrades and model selection:**
179
- - The plugin pins SHA-256-verified `v0.3.2-magpie.1`, based on upstream `v0.3.2`. Upgrade the plugin to update the backend. Official releases at or below that upstream version, and managed backends an older plugin installed, are upgraded on the next managed startup, preserving accounts, port and sign-in credentials.
180
- - Account bindings and hedge model selections from 0.2.19 are preserved. Settings saved in the admin page, including upstream defaults, survive plugin restarts. Hedge requests remain off by default; enable them and select models in the admin page if needed.
181
+ - The plugin pins SHA-256-verified `v0.3.4-magpie.1`, based on upstream `v0.3.4`. Upgrade the plugin to update the backend. Official releases at or below that upstream version, and managed backends an older plugin installed, are upgraded on the next managed startup, preserving accounts, port and sign-in credentials.
182
+ - Account priority ordering (`ACCOUNT_ORDER`) and per-model account bindings (`MODEL_ACCOUNTS`) from 0.3.3, as well as API key account bindings and hedge model selections from 0.2.19, are preserved. Settings saved in the admin page, including upstream defaults, survive plugin restarts. Hedge requests remain off by default; enable them and select models in the admin page if needed.
181
183
  - For users upgrading from 0.2.18 with `auto` enabled, hedging now applies only to `FIRST_EVENT_HEDGE_MODELS`, defaulting to Argon when that list is absent. Saving in the admin page fixes the current defaults in the file; later upstream default changes require manual adjustment. When taking over an official 0.2.19 sparse configuration, missing settings follow the plugin's initialization rules (for example, hedging becomes `off`). See the [upgrade compatibility notes](docs/studio-release.md#0219-升级兼容).
182
184
  - **Online updates are disabled in the managed backend on macOS, Linux, and Windows.** The admin page says updates are managed by magpie-gemini-web; the update endpoint returns HTTP 409 and update checks do not contact GitHub. Independently managed external servers are unaffected. See [docs/studio-release.md](docs/studio-release.md) for how the managed build is made and reproduced.
183
185
  - Managed Unix installs still attempt to set `package/` to `0555` as additional file protection. Permission failures do not block service, and disabling online updates does not depend on permissions. To reset by manually deleting the data directory, stop the service first, then run `chmod -R u+w "<data-dir>/package"` before deleting it; replace `<data-dir>` with the actual path.
@@ -189,11 +191,11 @@ When a plugin error asks you to run a command, it includes the full ready-to-cop
189
191
  - Models with `-build` or `assistant_route` are filtered out: they inject the Build coding assistant's own system prompt and are not offered as ordinary Agent models. Refresh with `magpie provider models ai-studio` after upgrading.
190
192
 
191
193
  **Gemini Web optimized backend upgrades & rollback:**
192
- - One-click setup uses [4.27.1-magpie.1](https://github.com/BUKOWSKIREAL/magpie-gemini-web/releases/tag/backend-v4.27.1-magpie.1) by default (built from pinned upstream v4.27.1 with XSRF caching, request cancellation propagation, early tool-call completion, and Argon model verification). Chat requests are timed by silence: while Google queues a request it sends a heartbeat every 60 s, and the backend waits through it for the first words. A try is given up, and retried, only after Google has sent nothing for `request_timeout_sec` (180 s by default), so a long reply that keeps coming is never cut off either.
194
+ - One-click setup uses [4.28.0-magpie.1](https://github.com/BUKOWSKIREAL/magpie-gemini-web/releases/tag/backend-v4.28.0-magpie.1) by default (built from pinned upstream v4.28.0 with XSRF caching, request cancellation propagation, early tool-call completion, and Argon model verification). Chat requests are timed by silence: while Google queues a request it sends a heartbeat every 60 s, and the backend waits through it for the first words. A try is given up, and retried, only after Google has sent nothing for `request_timeout_sec` (180 s by default), so a long reply that keeps coming is never cut off either.
193
195
  - **Upgrading:** Running backends are not interrupted automatically. Stop the existing process (PID in `backend.pid` inside the data directory); the next Magpie launch or request will upgrade the binary while preserving config and cookies.
194
196
  - **Rollback to upstream / restore optimized build** (run in the plugin directory after stopping the backend):
195
197
  ```bash
196
- npm run backend:install:upstream # Roll back and pin to official v4.27.1
198
+ npm run backend:install:upstream # Roll back and pin to official v4.28.0
197
199
  npm run backend:install # Restore default optimized build
198
200
  ```
199
201
  - For reproducible builds and release procedures, see [docs/backend-release.md](docs/backend-release.md).
package/README.md CHANGED
@@ -21,7 +21,7 @@
21
21
  | **图片生成** | Nano Banana 系列模型,供 Magpie 的图片生成使用(需要 Magpie v0.1.2075+,见下文) | 不支持 |
22
22
  | **登录方式** | 弹出独立浏览器窗口直接登录 Google | 用无痕窗口登录并复制 `cookie` |
23
23
  | **额度卡片** | 显示冷却中的模型与恢复时间 | 同步网页版 **5 小时** 与 **每周** 用量及重置时间 |
24
- | **本地后端** | AIStudio2API (`v0.3.2`) | gemini-web2api-go (`4.27.1-magpie.1` 优化版) |
24
+ | **本地后端** | AIStudio2API (`v0.3.4`) | gemini-web2api-go (`4.28.0-magpie.1` 优化版) |
25
25
 
26
26
  ---
27
27
 
@@ -56,7 +56,7 @@
56
56
 
57
57
  - **一账号一卡片**:每个 Google 账号单独生成一张以邮箱命名的额度卡片。
58
58
  - *旧版迁移(Gemini Web)*:若后端里已有早期版本添加的账号,在登录时选 **「添加本地后端里已有的 Google 账号」** 即可生成独立卡片,随后可删掉旧的 `API key …` 汇总卡片。
59
- - **账号池自动调度**:请求实际使用哪个账号由本地后端决定(Gemini Web 在账号池中轮流挑选;AI Studio 先用一个,进入冷却再切下一个)。在 Magpie 界面切换卡片不会改变后端的账号调度。
59
+ - **账号池自动调度**:请求实际使用哪个账号由本地后端决定(Gemini Web 在账号池中轮流挑选;AI Studio 先用一个,进入冷却再切下一个,可在管理页或通过 `studio.accountOrder`、`studio.modelAccounts` 指定账号优先顺序与模型专属账号池)。在 Magpie 界面切换卡片不会改变后端的账号调度。
60
60
  - **用量刷新说明**:
61
61
  - `gemini-web`:仅一键配置的本地后端可读取用量。Magpie 每次来查都会向 Google 重新读取(同一账号 30 秒内只读一次),点刷新即显示最新额度,包括在 Gemini 官方 App、网页或其他设备上的用量。Magpie 自己来查的频率:用量页或菜单栏的额度面板开着时约每分钟一次;菜单栏显示任何一家额度时随菜单栏更新(默认每 3 分钟);经 Magpie 转发请求时最多每分钟一次;都没有时不查。读取失败后 10 分钟内不自动重试,这期间点刷新会立即重试,若未生效再点一次。
62
62
  - `ai-studio`:AI Studio 不提供剩余总额度,卡片会展示当前账号正在冷却的模型、恢复时间或重新登录提示。
@@ -114,6 +114,8 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
114
114
  | `baseURL` | `http://127.0.0.1:8083/v1` | 仅 `gemini-web` | Gemini Web 默认后端地址 |
115
115
  | `contextLimit` / `outputLimit` | `1000000` / `8192` | 仅 `gemini-web` | 向客户端声明的上下文 / 输出上限 |
116
116
  | `studio.proxy` | 无 | 仅 `ai-studio` | 访问 Google 的代理(`http` / `https` / `socks5`),也可用环境变量 `AI_STUDIO_PROXY`;设置后优先于管理页里保存的代理,留空等于不设置 |
117
+ | `studio.accountOrder` | 无 | 仅 `ai-studio` | 账号优先顺序(逗号分隔字符串或字符串数组,如 `["b@gmail.com","a@gmail.com"]`);当 `.env` 中未保存或为空时作为初始值写入,管理页保存非空值后优先保留管理页设置 |
118
+ | `studio.modelAccounts` | 无 | 仅 `ai-studio` | 模型专属账号绑定(`"model:acc1|acc2"` 字符串或 `{ "gemini-3.1-pro-preview": ["acc1", "acc2"] }` 对象);当 `.env` 中未保存或为空时作为初始值写入,管理页保存非空值后优先保留管理页设置 |
117
119
  | `studio.baseURL` | `http://127.0.0.1:2048/v1` | 仅 `ai-studio` | 连接已有 AIStudio2API 时的默认地址 |
118
120
  | `studio.contextLimit` / `studio.outputLimit` | 模型目录默认值 | 仅 `ai-studio` | 向客户端声明的上限(仅支持在目录值基础上调低) |
119
121
  | `studio.imageModels` | `true` | 仅 `ai-studio` | 列出 Nano Banana 等图像模型,供 Magpie 的图片生成使用;设为 `false` 时只列文本模型 |
@@ -176,8 +178,8 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
176
178
  - Gemini Web 每个登录同时最多 2 个请求(AI Studio 为 1),可在 Magpie 的 provider 设置里调整;仍无法完全排除 Google 账号风控风险,请自行评估使用。
177
179
 
178
180
  **AI Studio 升级与模型范围:**
179
- - 插件固定使用经过 SHA-256 校验的 `v0.3.2-magpie.1`(基于上游 `v0.3.2`),通过升级插件更新后端。同基线或更旧的官方版、以及旧版插件装的托管后端,在下次管理启动时自动升级,保留账号、端口与登录凭据。
180
- - 0.2.19 的账号绑定与备用请求模型设置会保留;管理页保存的默认值不会在重启后被改回插件默认值。备用请求默认关闭,需要时可在管理页开启并选择模型。
181
+ - 插件固定使用经过 SHA-256 校验的 `v0.3.4-magpie.1`(基于上游 `v0.3.4`),通过升级插件更新后端。同基线或更旧的官方版、以及旧版插件装的托管后端,在下次管理启动时自动升级,保留账号、端口与登录凭据。
182
+ - 0.3.3 的账号优先顺序(`ACCOUNT_ORDER`)与模型账号绑定(`MODEL_ACCOUNTS`)、以及 0.2.19 的 API Key 账号绑定与备用请求模型设置均会保留;管理页保存的默认值不会在重启后被改回插件默认值。备用请求默认关闭,需要时可在管理页开启并选择模型。
181
183
  - 从 0.2.18 升级且已开启 `auto` 时,备用请求从所有模型缩为 `FIRST_EVENT_HEDGE_MODELS` 中的模型,未配置该列表时仅有 Argon。管理页完整保存会固定当时的默认值,后续上游默认值变化需要手动调整。接管官方 0.2.19 的稀疏配置时,缺失项会按插件规则补齐(例如备用请求变为 `off`);详见[升级兼容说明](docs/studio-release.md#0219-升级兼容)。
182
184
  - **托管后端已在 macOS、Linux、Windows 统一禁用在线更新**:管理页提示“更新由 magpie-gemini-web 管理”,更新接口返回 HTTP 409,检查更新也不会访问 GitHub。用户自行连接的外部服务不受影响。托管构建方式和可复现构建流程见 [docs/studio-release.md](docs/studio-release.md)。
183
185
  - Unix 的 `package/` 仍尽力设置为 `0555`,作为额外的文件保护;权限设置失败不阻断服务,在线更新的禁用也不依赖该权限。若要手动删除数据目录来重置,请先停止服务,再运行 `chmod -R u+w "<数据目录>/package"` 恢复可写权限,然后删除;将 `<数据目录>` 替换为实际路径。
@@ -189,11 +191,11 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
189
191
  - 模型列表过滤 `-build` / `assistant_route` 模型:这类模型附带 Build 写代码助手的系统提示词,不作为普通 Agent 模型提供。升级后可执行 `magpie provider models ai-studio` 刷新列表。
190
192
 
191
193
  **Gemini Web 优化后端升级与回退:**
192
- - 插件默认使用本仓库发布的 [4.27.1-magpie.1](https://github.com/BUKOWSKIREAL/magpie-gemini-web/releases/tag/backend-v4.27.1-magpie.1)(基于上游 v4.27.1 集成 XSRF 缓存、请求取消传播、工具调用提前收尾与 Argon 校验补丁)。对话请求按上游沉默计时:Google 让请求排队时每 60 秒发一次心跳,后端会一直等到出字;连续 `request_timeout_sec`(默认 180 秒)没有任何数据才放弃这一次并重试,还在出字的长回复也不会被截断。
194
+ - 插件默认使用本仓库发布的 [4.28.0-magpie.1](https://github.com/BUKOWSKIREAL/magpie-gemini-web/releases/tag/backend-v4.28.0-magpie.1)(基于上游 v4.28.0 集成 XSRF 缓存、请求取消传播、工具调用提前收尾与 Argon 校验补丁)。对话请求按上游沉默计时:Google 让请求排队时每 60 秒发一次心跳,后端会一直等到出字;连续 `request_timeout_sec`(默认 180 秒)没有任何数据才放弃这一次并重试,还在出字的长回复也不会被截断。
193
195
  - **旧版升级**:插件不会打断运行中的后端。先停止旧进程(PID 见数据目录 `backend.pid`),下次启动 Magpie 或发请求时会自动更新二进制并保留原有配置与账号。
194
196
  - **回退官方版 / 恢复优化版**(停止后端后在插件目录执行):
195
197
  ```bash
196
- npm run backend:install:upstream # 回退并固定使用官方 v4.27.1
198
+ npm run backend:install:upstream # 回退并固定使用官方 v4.28.0
197
199
  npm run backend:install # 恢复默认优化版
198
200
  ```
199
201
  - 源码复现构建与发布流程见 [docs/backend-release.md](docs/backend-release.md)。
@@ -30,4 +30,4 @@ Existing running processes are left alone to avoid interrupting requests. Once a
30
30
 
31
31
  New cookie imports require the running backend to advertise `capabilities.account_proxy_binding: true` at `/`. The plugin checks this before changing the proxy pool and again before submitting a cookie, so a reused older or upstream backend cannot silently ignore `proxy_id`. Unsupported builds receive instructions to stop the old process and run the installed plugin's CLI with `install --data-dir=<path>`. This explicitly selects the optimized channel, including for previous upstream or custom-build installations. The check does not interrupt existing conversations. Release smoke verifies the capability and actual proxy selection; version strings alone do not establish support.
32
32
 
33
- An explicit `npm run backend:install:upstream` installs the pinned upstream v4.27.1 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.
33
+ An explicit `npm run backend:install:upstream` installs the pinned upstream v4.28.0 and persists `upstream` in `backend-channel.json`. Automatic startup respects that choice. `npm run backend:install` returns to the optimized channel. Stop a running backend before replacing it, particularly on Windows. Explicit local Argon builds remain trusted through their existing build manifest.
@@ -1,8 +1,20 @@
1
1
  # AI Studio 托管后端构建
2
2
 
3
- 插件使用 `0.3.2-magpie.1`,基于 AIStudio2API `v0.3.2`(`4c44cb9765aa9fd497b91db1f5c1573504b9b85f`)。上游版本、commit、Go 版本、C/C++ 工具链、SpiderMonkey 与 libwebp 静态库(`toolchains`)、补丁列表、安装包及二进制 SHA-256 均记录在 `lib/studio-release.json`。`upstream.camoufox` 是这个 commit 要求的 Camoufox 版本(上游 `internal/camoufoxnative/download.go` 里的 `camoufoxRelease`),更新上游 commit 时要一起核对,构建脚本发现不一致会报错。MIT 许可见 `LICENSE-AISTUDIO`。
3
+ 插件使用 `0.3.4-magpie.1`,基于 AIStudio2API `v0.3.4`(`a4092efcdf358775339aa27cbb4db027fdf8f100`)。上游版本、commit、Go 版本、C/C++ 工具链、SpiderMonkey 与 libwebp 静态库(`toolchains`)、补丁列表、安装包及二进制 SHA-256 均记录在 `lib/studio-release.json`。`upstream.camoufox` 是这个 commit 要求的 Camoufox 版本(上游 `internal/camoufoxnative/download.go` 里的 `camoufoxRelease`),更新上游 commit 时要一起核对,构建脚本发现不一致会报错。MIT 许可见 `LICENSE-AISTUDIO`。
4
4
 
5
- ## 0.3.2 升级兼容
5
+ ## 0.3.4 升级兼容
6
+
7
+ - 基线为上游 `v0.3.4`(`a4092efcdf358775339aa27cbb4db027fdf8f100`)。包含 SpiderMonkey Worker 关闭超时崩溃修复、账号启用/禁用即时生效与后台平滑排空、Google AI 欢迎弹窗自动关闭、图片生成 WAA 请求 30 秒超时保护、Responses 流式心跳保活、`-build` 助手配额阶梯式冷却(1/5/15/30/60 分钟),以及管理页界面重构(含接入指南面板新增 Magpie 说明)。Camoufox、SpiderMonkey、libwebp 和固定编译器工具链沿用上一版。
8
+ - 请求正文迁移补丁与托管配置补丁均原样适用于 `v0.3.4`。
9
+
10
+ ## 0.3.3 升级兼容(历史记录)
11
+
12
+ - 基线为上游 `v0.3.3`(`718ea76f1c0d5a981c6368f936ec29c1acc5429e`)。包含 `fill-first` 账号优先顺序(`ACCOUNT_ORDER`)与管理页拖拽排序、按模型绑定专属账号池(`MODEL_ACCOUNTS`)、预热池按账号优先顺序选号、自动续写历史清理(避免重复拼接 `[Continue]` 提示词与残缺尾部)、以及 Google AI Ultra 层级识别。Camoufox、SpiderMonkey、libwebp 和固定编译器工具链沿用上一版。
13
+ - 新增 `ACCOUNT_ORDER` 与 `MODEL_ACCOUNTS`:插件保留管理页保存的值,并隔离宿主环境同名变量;同时支持在插件选项中通过 `studio.accountOrder` 与 `studio.modelAccounts` 配置初始默认值(当 `.env` 中尚未设置或为空时写入,管理页保存非空值后优先保留管理页设置)。
14
+ - 额度卡片、登录账号选择器与 `studio:status` 同步适配 `ACCOUNT_ORDER` 与 `MODEL_ACCOUNTS`:按 `ACCOUNT_ORDER` 排列账号顺序,按 `MODEL_ACCOUNTS` 计算对应模型冷却窗口的分母,并在卡片中展示已配置的账号顺序与模型绑定。
15
+ - 请求正文迁移补丁与托管配置补丁均原样适用于 `v0.3.3`;真实安装冒烟增加 `account_order` 与 `model_accounts` 保存与重启持久化检查。
16
+
17
+ ## 0.3.2 升级兼容(历史记录)
6
18
 
7
19
  - 基线为上游 `v0.3.2`(`4c44cb9765aa9fd497b91db1f5c1573504b9b85f`)。包含安全/拒答原因透传与重试、Build 模型工具调用续读、每日额度冷却回退、Omni 内联图片与视频、附件预览和视频抽帧设置,以及长文本、Worker 冷启动和 Camoufox 界面运行性能修复。Camoufox、SpiderMonkey、libwebp 和固定编译器工具链沿用上一版。
8
20
  - 新增 `HIDE_DOCK_ICON`:插件保留管理页保存的值,并隔离宿主环境同名变量;该设置只影响上游桌面版,托管构建仍不带 `desktop` 标签。
@@ -49,7 +61,7 @@
49
61
 
50
62
  上游从 v0.2.14 起原生支持分发方管理更新。构建时通过 `-ldflags` 写入以下值:
51
63
 
52
- - `internal/app.version=v0.3.2-magpie.1`:明确的版本标识。同基线或更旧的官方版、以及更旧的托管版需要升级;更新的官方版、更新的托管版或不认识的定制版仍提示冲突。
64
+ - `internal/app.version=v0.3.4-magpie.1`:明确的版本标识。同基线或更旧的官方版、以及更旧的托管版需要升级;更新的官方版、更新的托管版或不认识的定制版仍提示冲突。
53
65
  - `internal/app.managedBy=magpie-gemini-web`:`GET /api/update`(包括 `?refresh=1`)只返回本地版本、`mode: managed` 和 `managed_by`,不访问 GitHub;`POST /api/update` 返回 HTTP 409,不下载、不替换文件、不重启。管理页显示“更新由 magpie-gemini-web 管理”。
54
66
 
55
67
  `internal/config.managedBy=magpie-gemini-web` 同时启用上面的配置兼容补丁。
@@ -126,12 +138,22 @@ AI_STUDIO_TEST_ARCHIVE=/absolute/path/to/archive.tar.gz npm test
126
138
 
127
139
  真实安装验证检查旧版请求正文迁移及重启后可读、管理页保存的备用请求设置、启动、版本、GET 更新状态、POST 更新返回 409、二进制及 PID 不变、管理页面显示托管提示、停止与状态。`.github/workflows/studio.yml` 在五种系统/架构的原生 runner 上重新构建、对比 SHA-256 并运行该验证;本机交叉编译不能代替 Linux/Windows 真机验证。
128
140
 
129
- ## 0.3.2 升级验证(2026-11-08)
141
+ ## 0.3.4 升级验证(2026-11-10)
142
+
143
+ - 五个平台的候选构建([run 42744366347](https://github.com/BUKOWSKIREAL/magpie-gemini-web/actions/runs/42744366347))一次性通过上游测试、真实安装冒烟验证(含旧版 `bodies` 表迁移、`account_order` 与 `model_accounts` 设置保存与重启持久化、SpiderMonkey 与 libwebp 静态链接、管理页配置持久化)及插件测试;安装包和二进制 pin 来自该次构建。
144
+ - 随后的严格验证构建([run 42745979214](https://github.com/BUKOWSKIREAL/magpie-gemini-web/actions/runs/42745979214))在五个平台的全新 runner 上重新构建,安装包与可执行文件哈希均和 pin 完全一致,真实安装冒烟验证与插件测试全部通过。
145
+
146
+ ## 0.3.3 升级验证(2026-11-09,历史记录)
147
+
148
+ - 五个平台的候选构建([run 42551469407](https://github.com/BUKOWSKIREAL/magpie-gemini-web/actions/runs/42551469407))通过上游测试、真实安装冒烟验证(含旧版 `bodies` 表迁移、`account_order` 与 `model_accounts` 设置保存与重启持久化、SpiderMonkey 与 libwebp 静态链接、管理页配置持久化)及插件测试;安装包和二进制 pin 来自该次构建。
149
+ - 随后的严格验证构建([run 42552585708](https://github.com/BUKOWSKIREAL/magpie-gemini-web/actions/runs/42552585708))在五个平台的全新 runner 上重新构建,安装包与可执行文件哈希均和 pin 完全一致,真实安装冒烟验证与插件测试全部通过。
150
+
151
+ ## 0.3.2 升级验证(2026-11-08,历史记录)
130
152
 
131
153
  - 五个平台的候选构建([run 42443570134](https://github.com/BUKOWSKIREAL/magpie-gemini-web/actions/runs/42443570134))通过上游测试、真实安装冒烟验证(含旧版 `bodies` 表迁移、Dock 图标设置保存与重启持久化、SpiderMonkey 与 libwebp 静态链接、管理页配置持久化)及插件测试;安装包和二进制 pin 来自该次构建。
132
154
  - 随后的严格验证构建([run 42444479121](https://github.com/BUKOWSKIREAL/magpie-gemini-web/actions/runs/42444479121))在五个平台的全新 runner 上重新构建,安装包与可执行文件哈希均和 pin 完全一致,真实安装冒烟验证与插件测试全部通过。
133
155
 
134
- ## 0.3.1 升级验证(2026-11-07,历史记录)
156
+ ## 0.3.1 升级验证(历史记录,2026-11-07)
135
157
 
136
158
  - 五个平台的候选构建([run 42255835006](https://github.com/BUKOWSKIREAL/magpie-gemini-web/actions/runs/42255835006))通过上游测试、真实安装冒烟验证(含旧版 `bodies` 表迁移、SpiderMonkey 与 libwebp 静态链接、管理页配置持久化)及插件测试;安装包和二进制 pin(含在固定 Xcode 26.3 `17C529` 下生成的 macOS 产物)来自该次构建。
137
159
  - 随后的严格验证构建([run 42258671953](https://github.com/BUKOWSKIREAL/magpie-gemini-web/actions/runs/42258671953))在五个平台的全新 runner 上重新构建,哈希与 pin 完全一致,真实安装冒烟验证与插件测试全部通过。
@@ -172,7 +194,7 @@ AI_STUDIO_TEST_ARCHIVE=/absolute/path/to/archive.tar.gz npm test
172
194
  - 新版 Google 登录/生成端到端验证尚未完成。
173
195
  - 此分支未发布后端 release 或 npm 包。本机日常后端仍使用 `0.2.16-magpie.1`,新版仅在临时数据目录运行。
174
196
 
175
- 发布前必须审查各平台 CI 与构建记录,再将安装包及许可证上传到本仓库 `studio-v0.3.2-magpie.1` release。该 release 的安装包可公开下载后,才能发布对应 npm 版本,否则新安装会遇到 404。合并 PR 不代表已经满足发布条件;构建脚本及验证 CI 不自动发布 release 或 npm 包。
197
+ 发布前必须审查各平台 CI 与构建记录,再将安装包及许可证上传到本仓库 `studio-v0.3.4-magpie.1` release。该 release 的安装包可公开下载后,才能发布对应 npm 版本,否则新安装会遇到 404。合并 PR 不代表已经满足发布条件;构建脚本及验证 CI 不自动发布 release 或 npm 包。
176
198
 
177
199
  本地试用建议使用独立数据目录:
178
200
 
@@ -1,36 +1,36 @@
1
1
  {
2
- "version": "4.27.1-magpie.1",
2
+ "version": "4.28.0-magpie.1",
3
3
  "repository": "BUKOWSKIREAL/magpie-gemini-web",
4
- "tag": "backend-v4.27.1-magpie.1",
4
+ "tag": "backend-v4.28.0-magpie.1",
5
5
  "upstream": {
6
6
  "repository": "https://github.com/zexadev/gemini-web2api-go.git",
7
- "commit": "c8a4ac25768b668fa94d293fcb97059c4419b602"
7
+ "commit": "ed0f5ad8b16af4079fbcaf66255e6645c9893d1f"
8
8
  },
9
9
  "goVersion": "1.27.1",
10
10
  "binaries": {
11
11
  "darwin-arm64": {
12
- "asset": "gemini-web2api-go_v4.27.1-magpie.1_darwin_arm64",
13
- "sha256": "a0828624c3301e79a36461bf1197d87ad0836d26262bea69fd9255d018eccac2"
12
+ "asset": "gemini-web2api-go_v4.28.0-magpie.1_darwin_arm64",
13
+ "sha256": "9c495eb5f6e97fceb9a97fe21b4d33f1127592aa96a8206e3a243244571d9c37"
14
14
  },
15
15
  "darwin-x64": {
16
- "asset": "gemini-web2api-go_v4.27.1-magpie.1_darwin_amd64",
17
- "sha256": "4b2a37dceefe7ed7f055f4c92f1038ccd3671c28bd612a4251a339a1c12dd975"
16
+ "asset": "gemini-web2api-go_v4.28.0-magpie.1_darwin_amd64",
17
+ "sha256": "7f264075c556592f9753a43358c4cf1bd90b3d41343d88836f835d20d9a70f21"
18
18
  },
19
19
  "linux-x64": {
20
- "asset": "gemini-web2api-go_v4.27.1-magpie.1_linux_amd64",
21
- "sha256": "bbded68869c0221868dcd81b96bdd05dce4afea0f4073e9ef4ed2b77a95f66d0"
20
+ "asset": "gemini-web2api-go_v4.28.0-magpie.1_linux_amd64",
21
+ "sha256": "ec3ee723dbd6f99b6e874eaf2aab23b7c8afb3ab5c2a3498b51fea27b1f42ded"
22
22
  },
23
23
  "linux-arm64": {
24
- "asset": "gemini-web2api-go_v4.27.1-magpie.1_linux_arm64",
25
- "sha256": "95e9ca87c5bc64c59a54d86be31425afa775b4e6ba367fc2eec652e096151bfa"
24
+ "asset": "gemini-web2api-go_v4.28.0-magpie.1_linux_arm64",
25
+ "sha256": "bcadd1a679c36ca1eb582835c4a9d01a157612951cd9a2f58c154aa20fc2e893"
26
26
  },
27
27
  "win32-x64": {
28
- "asset": "gemini-web2api-go_v4.27.1-magpie.1_windows_amd64.exe",
29
- "sha256": "4da25f05694e7d70d001b1d9665024ec2c98dd8714e310823a5d09d513fe8e02"
28
+ "asset": "gemini-web2api-go_v4.28.0-magpie.1_windows_amd64.exe",
29
+ "sha256": "8cd8f92133873fe4e15fed6081404ad609c90e8cc7701154d008e23c8b133648"
30
30
  },
31
31
  "win32-arm64": {
32
- "asset": "gemini-web2api-go_v4.27.1-magpie.1_windows_arm64.exe",
33
- "sha256": "782891140d65f7f364472ba2263e8498cf0b86872881246b3e5612ee74d06447"
32
+ "asset": "gemini-web2api-go_v4.28.0-magpie.1_windows_arm64.exe",
33
+ "sha256": "f4d7d46be7fbfe12e6ae26ef3dbd861519611e5dbd364e335ab7a0facecf87ce"
34
34
  }
35
35
  }
36
36
  }
package/lib/native.mjs CHANGED
@@ -11,16 +11,16 @@ export const BACKEND_VERSION = `v${release.version}`;
11
11
  const RELEASE_BASE = `https://github.com/${release.repository}/releases/download/${release.tag}/`;
12
12
  export const PINNED_BINARIES = release.binaries;
13
13
 
14
- const UPSTREAM_VERSION = 'v4.27.1';
14
+ const UPSTREAM_VERSION = 'v4.28.0';
15
15
  const UPSTREAM_BASE = `https://github.com/zexadev/gemini-web2api-go/releases/download/${UPSTREAM_VERSION}/`;
16
16
  // Retained for an explicit, checksum-verified rollback to the upstream release.
17
17
  export const UPSTREAM_BINARIES = {
18
- 'darwin-arm64': { asset: 'gemini-web2api-go_v4.27.1_darwin_arm64', sha256: 'bbb438995debb6e63d1838972477ba478b8a86c038fa8093836b3a492b9178c0' },
19
- 'darwin-x64': { asset: 'gemini-web2api-go_v4.27.1_darwin_amd64', sha256: '6aeb9683c379ed95af1611c48531e4d6464d6061889b97e763e977aa30a05ee3' },
20
- 'linux-x64': { asset: 'gemini-web2api-go_v4.27.1_linux_amd64', sha256: '8e0e3df2de1881e35d8ce2395be95e94e5d0653a1013be5bcab5821fa236b5ac' },
21
- 'linux-arm64': { asset: 'gemini-web2api-go_v4.27.1_linux_arm64', sha256: 'e0ba98effeae4054c3571d9f0df6f0815eac03727ca0bffbc3a1a69c9a896b7a' },
22
- 'win32-x64': { asset: 'gemini-web2api-go_v4.27.1_windows_amd64.exe', sha256: '3d89067af3f68e812be4026d0a9625aea55a3734da154ff0a29276714128025b' },
23
- 'win32-arm64': { asset: 'gemini-web2api-go_v4.27.1_windows_arm64.exe', sha256: 'eeabd5e8de76773140400f5e8dbd234baeb3bc6f5938464ecbdf7cbe09ffaf56' }
18
+ 'darwin-arm64': { asset: 'gemini-web2api-go_v4.28.0_darwin_arm64', sha256: 'a6aaf315d4ea0ab4cc179e75b050c305b55e7347bc654eb5b5980dc82bedeffe' },
19
+ 'darwin-x64': { asset: 'gemini-web2api-go_v4.28.0_darwin_amd64', sha256: '132207df8d9a87ad9c179f71dafc000e7d8f8721244a378e0989d063ecae5bdd' },
20
+ 'linux-x64': { asset: 'gemini-web2api-go_v4.28.0_linux_amd64', sha256: 'e42cd70701a234c06eb81b4af4d925be4fe2b4c77bc6627e99b01750cdfea931' },
21
+ 'linux-arm64': { asset: 'gemini-web2api-go_v4.28.0_linux_arm64', sha256: '404dd0e0e8ebc529f925b9db18541f1adfcb2edd5088827c86430c91b3065ea4' },
22
+ 'win32-x64': { asset: 'gemini-web2api-go_v4.28.0_windows_amd64.exe', sha256: '4a990dbc106334c73ee2a4ae26b917073a2b82f069dc9da1b1073a608576d9a1' },
23
+ 'win32-arm64': { asset: 'gemini-web2api-go_v4.28.0_windows_arm64.exe', sha256: 'ed421bb36a0bf2cd7d0c561ca035e251b01769d0db83ef78834f8578fa545279' }
24
24
  };
25
25
 
26
26
  export function pinnedBinary(platform = process.platform, arch = process.arch, channel = 'optimized') {
@@ -194,13 +194,162 @@ export function validateConfig(config) {
194
194
  const ENV_KEYS = [
195
195
  'AISTUDIO_AUTH_STATES', 'LISTEN_ADDR', 'PREVIEW_LISTEN_ADDR', 'PROXY_API_KEY', 'API_KEY_ACCOUNTS', 'PROXY', 'INIT_TIMEOUT', 'REQUEST_TIMEOUT',
196
196
  'WARM_WORKER_LIMIT', 'MAX_ACTIVE_WORKERS', 'WARM_STARTUP_CONCURRENCY', 'PER_ACCOUNT_CONCURRENCY',
197
- 'ROUTING_STRATEGY', 'UPSTREAM_CHANNELS', 'MODEL_MAPPING', 'MODEL_CHANNELS', 'WAA_BACKEND',
197
+ 'ROUTING_STRATEGY', 'ACCOUNT_ORDER', 'UPSTREAM_CHANNELS', 'MODEL_MAPPING', 'MODEL_CHANNELS', 'MODEL_ACCOUNTS', 'WAA_BACKEND',
198
198
  'MEDIA_OUTPUT', 'MEDIA_BASE_URL', 'AUTO_START', 'HIDE_DOCK_ICON', 'REQUEST_BODY_LOG', 'LOG_FILE', 'LOG_MAX_TOTAL_MB',
199
199
  'ADMIN_AUTH_ENABLED', 'ADMIN_USERNAME', 'ADMIN_PASSWORD', 'BUILD_NATIVE_NONSTREAM',
200
200
  'NONSTREAM_KEEPALIVE', 'FAKE_STREAM', 'YOUTUBE_LINKS', 'STRIP_THOUGHT_SIGNATURES', 'SEARCH_MODEL_ALIASES', 'GOOGLE_SEARCH',
201
201
  'FIRST_EVENT_HEDGE', 'FIRST_EVENT_HEDGE_MODELS', 'CONTINUATION_REWRITE', 'CONTINUATION_PROMPT'
202
202
  ];
203
203
 
204
+ // parseAccountOrder splits ACCOUNT_ORDER into ordered, deduplicated account IDs, matching upstream accountOrder().
205
+ export function parseAccountOrder(value) {
206
+ if (value === undefined || value === null || value === '') return [];
207
+ const items = Array.isArray(value) ? value : String(value).split(/[\s,]+/);
208
+ const out = [];
209
+ const seen = new Set();
210
+ for (const raw of items) {
211
+ const id = String(raw ?? '').trim();
212
+ if (id && !seen.has(id)) {
213
+ seen.add(id);
214
+ out.push(id);
215
+ }
216
+ }
217
+ return out;
218
+ }
219
+
220
+ export function validateAccountOrder(value) {
221
+ if (value === undefined || value === null) return undefined;
222
+ if (typeof value === 'string') {
223
+ if (/[\x00-\x1f\x7f]/.test(value)) {
224
+ throw new Error('Invalid studio.accountOrder: must not contain newlines or control characters');
225
+ }
226
+ const ids = parseAccountOrder(value);
227
+ for (const id of ids) {
228
+ if (/[,|\s\x00-\x1f\x7f]/.test(id)) {
229
+ throw new Error(`Invalid studio.accountOrder: invalid account ID "${id}"`);
230
+ }
231
+ }
232
+ return ids.join(',');
233
+ }
234
+ if (Array.isArray(value)) {
235
+ const ids = [];
236
+ const seen = new Set();
237
+ for (const item of value) {
238
+ if (typeof item !== 'string') {
239
+ throw new Error('Invalid studio.accountOrder: array entries must be strings');
240
+ }
241
+ const id = item.trim();
242
+ if (!id || /[,|\s\x00-\x1f\x7f]/.test(id)) {
243
+ throw new Error('Invalid studio.accountOrder: entries must be non-empty account IDs without commas or whitespace');
244
+ }
245
+ if (!seen.has(id)) {
246
+ seen.add(id);
247
+ ids.push(id);
248
+ }
249
+ }
250
+ return ids.join(',');
251
+ }
252
+ throw new Error('Invalid studio.accountOrder: must be a comma-separated string or an array of account IDs');
253
+ }
254
+
255
+ // parseModelAccounts parses MODEL_ACCOUNTS ("model:acc1|acc2,model2:acc3") into a Map<modelId, accountIds[]>,
256
+ // matching upstream config.ParseModelAccounts.
257
+ export function parseModelAccounts(value, { strict = false } = {}) {
258
+ const accounts = new Map();
259
+ if (value === undefined || value === null || value === '') return accounts;
260
+ if (value instanceof Map) {
261
+ for (const [k, v] of value) {
262
+ const model = String(k ?? '').trim().replace(/^models\//, '');
263
+ const ids = Array.isArray(v) ? v.map(s => String(s ?? '').trim()).filter(Boolean) : parseAccountOrder(String(v ?? '').replaceAll('|', ','));
264
+ if (model && ids.length) accounts.set(model, [...new Set(ids)]);
265
+ }
266
+ return accounts;
267
+ }
268
+ if (typeof value === 'object' && !Array.isArray(value)) {
269
+ if (strict) {
270
+ return parseModelAccounts(validateModelAccounts(value), { strict: true });
271
+ }
272
+ try {
273
+ return parseModelAccounts(validateModelAccounts(value), { strict: false });
274
+ } catch {
275
+ return accounts;
276
+ }
277
+ }
278
+ for (const rawItem of String(value).split(',')) {
279
+ const item = rawItem.trim();
280
+ if (!item) continue;
281
+ const index = item.lastIndexOf(':');
282
+ const model = index >= 0 ? item.slice(0, index).trim().replace(/^models\//, '') : '';
283
+ const ids = index >= 0
284
+ ? [...new Set(item.slice(index + 1).split('|').map(id => id.trim()).filter(Boolean))]
285
+ : [];
286
+ if (index < 0 || !model || !ids.length || /[,|:\s\x00-\x1f\x7f]/.test(model) || ids.some(id => /[:\s\x00-\x1f\x7f]/.test(id))) {
287
+ if (strict) {
288
+ throw new Error(`Invalid studio.modelAccounts entry: must be "model:account|account" (got "${item}")`);
289
+ }
290
+ continue;
291
+ }
292
+ if (accounts.has(model)) {
293
+ if (strict) {
294
+ throw new Error(`Invalid studio.modelAccounts: duplicate model "${model}"`);
295
+ }
296
+ continue;
297
+ }
298
+ accounts.set(model, ids);
299
+ }
300
+ return accounts;
301
+ }
302
+
303
+ export function validateModelAccounts(value) {
304
+ if (value === undefined || value === null) return undefined;
305
+ if (typeof value === 'string') {
306
+ if (/[\x00-\x1f\x7f]/.test(value)) {
307
+ throw new Error('Invalid studio.modelAccounts: must not contain newlines or control characters');
308
+ }
309
+ const parsed = parseModelAccounts(value, { strict: true });
310
+ return [...parsed.entries()].map(([model, ids]) => `${model}:${ids.join('|')}`).join(',');
311
+ }
312
+ if (typeof value === 'object' && !Array.isArray(value)) {
313
+ const parsed = new Map();
314
+ for (const [rawModel, rawAccounts] of Object.entries(value)) {
315
+ const model = String(rawModel).trim().replace(/^models\//, '');
316
+ if (!model || /[,|:\s\x00-\x1f\x7f]/.test(model)) {
317
+ throw new Error(`Invalid studio.modelAccounts: invalid model ID "${rawModel}"`);
318
+ }
319
+ if (parsed.has(model)) {
320
+ throw new Error(`Invalid studio.modelAccounts: duplicate model "${model}"`);
321
+ }
322
+ let list;
323
+ if (typeof rawAccounts === 'string') {
324
+ if (/[\x00-\x1f\x7f]/.test(rawAccounts)) {
325
+ throw new Error(`Invalid studio.modelAccounts: accounts for ${model} must not contain control characters`);
326
+ }
327
+ list = rawAccounts.split(/[|,]/).map(s => s.trim()).filter(Boolean);
328
+ } else if (Array.isArray(rawAccounts)) {
329
+ list = rawAccounts.map(item => {
330
+ if (typeof item !== 'string') {
331
+ throw new Error(`Invalid studio.modelAccounts: account entries for ${model} must be strings`);
332
+ }
333
+ const trimmed = item.trim();
334
+ if (!trimmed || /[,|:\s\x00-\x1f\x7f]/.test(trimmed)) {
335
+ throw new Error(`Invalid studio.modelAccounts: invalid account ID for ${model}: "${item}"`);
336
+ }
337
+ return trimmed;
338
+ });
339
+ } else {
340
+ throw new Error(`Invalid studio.modelAccounts: accounts for ${model} must be an array of account IDs or a "|"-separated string`);
341
+ }
342
+ const ids = [...new Set(list)];
343
+ if (!ids.length || ids.some(id => /[,|:\s\x00-\x1f\x7f]/.test(id))) {
344
+ throw new Error(`Invalid studio.modelAccounts: ${model} must specify at least one valid account ID`);
345
+ }
346
+ parsed.set(model, ids);
347
+ }
348
+ return [...parsed.entries()].map(([model, ids]) => `${model}:${ids.join('|')}`).join(',');
349
+ }
350
+ throw new Error('Invalid studio.modelAccounts: must be a "model:account|account" string or an object mapping model IDs to accounts');
351
+ }
352
+
204
353
  // PROXY_ENV are the proxy variables the service would fall back to with no PROXY of its own.
205
354
  const PROXY_ENV = ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY'];
206
355
 
@@ -330,10 +479,14 @@ function defaultPreviewPort(studioPort) {
330
479
  // syncStudioEnv brings .env up to date with config.json, keeping what the admin page saved: this
331
480
  // plugin's settings are written over, the others only when missing. A missing PROXY is the one
332
481
  // given, else the one an older config.json kept (which nothing reads after that).
333
- function syncStudioEnv(dataDir, config, seedProxy) {
482
+ function syncStudioEnv(dataDir, config, seedProxy, options = {}) {
334
483
  const values = readStudioEnv(dataDir);
335
484
  Object.assign(values, pluginEnv(dataDir, config));
336
485
  for (const [key, value] of Object.entries(SETTING_DEFAULTS)) values[key] ??= value;
486
+ const accountOrder = validateAccountOrder(options.accountOrder);
487
+ if (accountOrder !== undefined && !values.ACCOUNT_ORDER) values.ACCOUNT_ORDER = accountOrder;
488
+ const modelAccounts = validateModelAccounts(options.modelAccounts);
489
+ if (modelAccounts !== undefined && !values.MODEL_ACCOUNTS) values.MODEL_ACCOUNTS = modelAccounts;
337
490
  const preview = previewPort(values.PREVIEW_LISTEN_ADDR);
338
491
  if (!preview || preview === config.port) {
339
492
  values.PREVIEW_LISTEN_ADDR = `127.0.0.1:${defaultPreviewPort(config.port)}`;
@@ -400,7 +553,7 @@ export function ensureStudioConfig(dataDir, explicitProxy = undefined, options =
400
553
  }
401
554
  }
402
555
 
403
- syncStudioEnv(dataDir, config, explicitProxy);
556
+ syncStudioEnv(dataDir, config, explicitProxy, options);
404
557
 
405
558
  const authDir = path.join(dataDir, 'auth');
406
559
  fs.mkdirSync(authDir, { recursive: true, mode: 0o700 });
@@ -1451,7 +1604,7 @@ export async function installStudio(options = {}) {
1451
1604
  fs.renameSync(tmpManifest, manifestPath);
1452
1605
  fs.chmodSync(manifestPath, 0o600);
1453
1606
 
1454
- ensureStudioConfig(dataDir, options.proxy, { port });
1607
+ ensureStudioConfig(dataDir, options.proxy, { port, accountOrder: options.accountOrder, modelAccounts: options.modelAccounts });
1455
1608
  protectPackage(dataDir, platform);
1456
1609
 
1457
1610
  if (hasBackup) {
@@ -1572,6 +1725,54 @@ export async function ensureStudio(options = {}) {
1572
1725
  return shared(inFlightReady, dataDir, () => readyStudio(local, status, options));
1573
1726
  }
1574
1727
 
1728
+ // needsRoutingSeed checks whether options.accountOrder or options.modelAccounts should seed an
1729
+ // unset/empty key in .env, without mutating disk.
1730
+ function needsRoutingSeed(dataDir, options) {
1731
+ const accountOrder = validateAccountOrder(options.accountOrder);
1732
+ const modelAccounts = validateModelAccounts(options.modelAccounts);
1733
+ if (!accountOrder && !modelAccounts) return null;
1734
+ let env = {};
1735
+ try {
1736
+ env = readStudioEnv(dataDir);
1737
+ } catch {
1738
+ return null;
1739
+ }
1740
+ const seedOrder = accountOrder && !env.ACCOUNT_ORDER ? accountOrder : null;
1741
+ const seedModels = modelAccounts && !env.MODEL_ACCOUNTS ? modelAccounts : null;
1742
+ if (!seedOrder && !seedModels) return null;
1743
+ return { seedOrder, seedModels };
1744
+ }
1745
+
1746
+ async function seedRunningStudioRouting(local, dataDir, options, fetchFn) {
1747
+ const seed = needsRoutingSeed(dataDir, options);
1748
+ if (!seed) return;
1749
+ try {
1750
+ const current = await studioAdmin(local, 'GET', '/api/config', null, { fetchFn, timeoutMs: 3000 });
1751
+ if (current && typeof current === 'object') {
1752
+ const next = { ...current };
1753
+ let changed = false;
1754
+ if (seed.seedOrder && !String(current.account_order || '').trim()) {
1755
+ next.account_order = seed.seedOrder;
1756
+ changed = true;
1757
+ }
1758
+ if (seed.seedModels && !String(current.model_accounts || '').trim()) {
1759
+ next.model_accounts = seed.seedModels;
1760
+ changed = true;
1761
+ }
1762
+ if (changed) {
1763
+ await studioAdmin(local, 'PUT', '/api/config', next, { fetchFn, timeoutMs: 3000 });
1764
+ }
1765
+ return;
1766
+ }
1767
+ } catch {
1768
+ // Fall back to .env sync below when the running listener is a test stub without /api/config.
1769
+ }
1770
+ ensureStudioConfig(dataDir, options.proxy, {
1771
+ accountOrder: options.accountOrder,
1772
+ modelAccounts: options.modelAccounts
1773
+ });
1774
+ }
1775
+
1575
1776
  // launchStudio gets the service up, installing it when missing, outdated or untrusted, and
1576
1777
  // returns how to reach it with the status it last reported.
1577
1778
  async function launchStudio(options) {
@@ -1585,7 +1786,7 @@ async function launchStudio(options) {
1585
1786
  let local = localStudio({ dataDir, platform });
1586
1787
  if (local) {
1587
1788
  const health = await checkStudioHealth(local, fetchFn);
1588
- if (health.state === 'up') {
1789
+ if (health.state === 'up' && !needsRoutingSeed(dataDir, options)) {
1589
1790
  protectPackage(dataDir, platform);
1590
1791
  return { local, status: health.status };
1591
1792
  }
@@ -1599,6 +1800,7 @@ async function launchStudio(options) {
1599
1800
  if (local) {
1600
1801
  const health = await settle(local, await checkStudioHealth(local, fetchFn), fetchFn, options.unresponsiveTimeoutMs ?? 10000);
1601
1802
  if (health.state === 'up') {
1803
+ await seedRunningStudioRouting(local, dataDir, options, fetchFn);
1602
1804
  protectPackage(dataDir, platform);
1603
1805
  return { local, status: health.status };
1604
1806
  }
@@ -1640,10 +1842,24 @@ async function launchStudio(options) {
1640
1842
  }
1641
1843
 
1642
1844
  if (!manifestValid) {
1643
- await installStudio({ dataDir, platform, arch, fetchFn, archivePath: options.archivePath, proxy: options.proxy ?? options.savedProxy, preferredPort: options.preferredPort });
1845
+ await installStudio({
1846
+ dataDir,
1847
+ platform,
1848
+ arch,
1849
+ fetchFn,
1850
+ archivePath: options.archivePath,
1851
+ proxy: options.proxy ?? options.savedProxy,
1852
+ preferredPort: options.preferredPort,
1853
+ accountOrder: options.accountOrder,
1854
+ modelAccounts: options.modelAccounts
1855
+ });
1644
1856
  }
1645
1857
 
1646
- const config = ensureStudioConfig(dataDir, options.proxy, { port: await newConfigPort(dataDir, options) });
1858
+ const config = ensureStudioConfig(dataDir, options.proxy, {
1859
+ port: await newConfigPort(dataDir, options),
1860
+ accountOrder: options.accountOrder,
1861
+ modelAccounts: options.modelAccounts
1862
+ });
1647
1863
  if (options.savedProxy !== undefined) saveStudioProxy(dataDir, options.savedProxy);
1648
1864
  local = localStudio({ dataDir, platform });
1649
1865
  if (!local) throw new Error(`Invalid AI Studio configuration in ${dataDir}`);