magpie-gemini-web 0.10.0 → 0.11.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.0`) | gemini-web2api-go (`4.27.0-magpie.2` optimized build) |
24
+ | **Local backend** | AIStudio2API (`v0.3.2`) | gemini-web2api-go (`4.27.1-magpie.1` optimized build) |
25
25
 
26
26
  ---
27
27
 
@@ -176,7 +176,7 @@ When a plugin error asks you to run a command, it includes the full ready-to-cop
176
176
  - 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
177
 
178
178
  **AI Studio upgrades and model selection:**
179
- - The plugin pins SHA-256-verified `v0.2.19-magpie.1`, based on upstream `v0.2.19`. 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.
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
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
181
  - 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
182
  - **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.
@@ -189,11 +189,11 @@ When a plugin error asks you to run a command, it includes the full ready-to-cop
189
189
  - 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
190
 
191
191
  **Gemini Web optimized backend upgrades & rollback:**
192
- - One-click setup uses [4.27.0-magpie.2](https://github.com/BUKOWSKIREAL/magpie-gemini-web/releases/tag/backend-v4.27.0-magpie.2) by default (built from pinned upstream v4.27.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.
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.
193
193
  - **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
194
  - **Rollback to upstream / restore optimized build** (run in the plugin directory after stopping the backend):
195
195
  ```bash
196
- npm run backend:install:upstream # Roll back and pin to official v4.27.0
196
+ npm run backend:install:upstream # Roll back and pin to official v4.27.1
197
197
  npm run backend:install # Restore default optimized build
198
198
  ```
199
199
  - 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.0`) | gemini-web2api-go (`4.27.0-magpie.2` 优化版) |
24
+ | **本地后端** | AIStudio2API (`v0.3.2`) | gemini-web2api-go (`4.27.1-magpie.1` 优化版) |
25
25
 
26
26
  ---
27
27
 
@@ -176,7 +176,7 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
176
176
  - Gemini Web 每个登录同时最多 2 个请求(AI Studio 为 1),可在 Magpie 的 provider 设置里调整;仍无法完全排除 Google 账号风控风险,请自行评估使用。
177
177
 
178
178
  **AI Studio 升级与模型范围:**
179
- - 插件固定使用经过 SHA-256 校验的 `v0.2.19-magpie.1`(基于上游 `v0.2.19`),通过升级插件更新后端。同基线或更旧的官方版、以及旧版插件装的托管后端,在下次管理启动时自动升级,保留账号、端口与登录凭据。
179
+ - 插件固定使用经过 SHA-256 校验的 `v0.3.2-magpie.1`(基于上游 `v0.3.2`),通过升级插件更新后端。同基线或更旧的官方版、以及旧版插件装的托管后端,在下次管理启动时自动升级,保留账号、端口与登录凭据。
180
180
  - 0.2.19 的账号绑定与备用请求模型设置会保留;管理页保存的默认值不会在重启后被改回插件默认值。备用请求默认关闭,需要时可在管理页开启并选择模型。
181
181
  - 从 0.2.18 升级且已开启 `auto` 时,备用请求从所有模型缩为 `FIRST_EVENT_HEDGE_MODELS` 中的模型,未配置该列表时仅有 Argon。管理页完整保存会固定当时的默认值,后续上游默认值变化需要手动调整。接管官方 0.2.19 的稀疏配置时,缺失项会按插件规则补齐(例如备用请求变为 `off`);详见[升级兼容说明](docs/studio-release.md#0219-升级兼容)。
182
182
  - **托管后端已在 macOS、Linux、Windows 统一禁用在线更新**:管理页提示“更新由 magpie-gemini-web 管理”,更新接口返回 HTTP 409,检查更新也不会访问 GitHub。用户自行连接的外部服务不受影响。托管构建方式和可复现构建流程见 [docs/studio-release.md](docs/studio-release.md)。
@@ -189,11 +189,11 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
189
189
  - 模型列表过滤 `-build` / `assistant_route` 模型:这类模型附带 Build 写代码助手的系统提示词,不作为普通 Agent 模型提供。升级后可执行 `magpie provider models ai-studio` 刷新列表。
190
190
 
191
191
  **Gemini Web 优化后端升级与回退:**
192
- - 插件默认使用本仓库发布的 [4.27.0-magpie.2](https://github.com/BUKOWSKIREAL/magpie-gemini-web/releases/tag/backend-v4.27.0-magpie.2)(基于上游 v4.27.0 集成 XSRF 缓存、请求取消传播、工具调用提前收尾与 Argon 校验补丁)。对话请求按上游沉默计时:Google 让请求排队时每 60 秒发一次心跳,后端会一直等到出字;连续 `request_timeout_sec`(默认 180 秒)没有任何数据才放弃这一次并重试,还在出字的长回复也不会被截断。
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 秒)没有任何数据才放弃这一次并重试,还在出字的长回复也不会被截断。
193
193
  - **旧版升级**:插件不会打断运行中的后端。先停止旧进程(PID 见数据目录 `backend.pid`),下次启动 Magpie 或发请求时会自动更新二进制并保留原有配置与账号。
194
194
  - **回退官方版 / 恢复优化版**(停止后端后在插件目录执行):
195
195
  ```bash
196
- npm run backend:install:upstream # 回退并固定使用官方 v4.27.0
196
+ npm run backend:install:upstream # 回退并固定使用官方 v4.27.1
197
197
  npm run backend:install # 恢复默认优化版
198
198
  ```
199
199
  - 源码复现构建与发布流程见 [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.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.
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.
@@ -1,8 +1,21 @@
1
1
  # AI Studio 托管后端构建
2
2
 
3
- 插件使用 `0.3.0-magpie.1`,基于 AIStudio2API `v0.3.0` 及其后续修复 commit `48f75fa722b25d22a493e75f6d4e5f9d5035d7e3`。上游版本、commit、Go 版本、C/C++ 工具链与 SpiderMonkey 静态库(`toolchains`)、补丁列表、安装包及二进制 SHA-256 均记录在 `lib/studio-release.json`。`upstream.camoufox` 是这个 commit 要求的 Camoufox 版本(上游 `internal/camoufoxnative/download.go` 里的 `camoufoxRelease`),更新上游 commit 时要一起核对,构建脚本发现不一致会报错。MIT 许可见 `LICENSE-AISTUDIO`。
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`。
4
4
 
5
- ## 0.3.0 升级兼容
5
+ ## 0.3.2 升级兼容
6
+
7
+ - 基线为上游 `v0.3.2`(`4c44cb9765aa9fd497b91db1f5c1573504b9b85f`)。包含安全/拒答原因透传与重试、Build 模型工具调用续读、每日额度冷却回退、Omni 内联图片与视频、附件预览和视频抽帧设置,以及长文本、Worker 冷启动和 Camoufox 界面运行性能修复。Camoufox、SpiderMonkey、libwebp 和固定编译器工具链沿用上一版。
8
+ - 新增 `HIDE_DOCK_ICON`:插件保留管理页保存的值,并隔离宿主环境同名变量;该设置只影响上游桌面版,托管构建仍不带 `desktop` 标签。
9
+ - 请求正文迁移补丁与托管配置补丁均原样适用于 `v0.3.2`;真实安装冒烟增加 Dock 图标设置保存与重启持久化检查。
10
+
11
+ ## 0.3.1 升级兼容(历史记录)
12
+
13
+ - 基线为上游 `v0.3.1`(`80847a7665c442a238918c957963217773f36291`)。包含 AVX2/NEON 汇编图片与音频编码加速、libwebp 1.6.0 有损 WebP 编码、多 choice/图片生成/Gemini 生成结果复制指向已释放栈的崩溃修复、预热登录续签与模型目录同步互等修复、10 秒流式心跳与 `X-Accel-Buffering: no`、YouTube 链接误识别与代码文本附件修复、账户时区不可见字符清理,以及到期资源与媒体目录上限回收。Camoufox、SpiderMonkey 静态库和固定编译器工具链沿用上一版。
14
+ - 新增 `toolchains.libwebp`(`Mag1cFall/libwebp` 的 `v1.6.0-aistudio2api.1` release),与 SpiderMonkey 一样按目标下载到 `studio-build/toolchains/` 并校验 SHA-256;头文件统一经 `CGO_CPPFLAGS` 传入以保留 `-O2`,与上游 `.github/actions/static-libraries` 一致。
15
+ - `patches/studio-0.2.17-preserve-request-bodies.patch` 适配上游 `sqlitedb.Open(path, schema)` 的表结构同步:在建表后、清理未定义旧表前执行 `migrateLegacyBodies`,确保从 0.2.16 及更早版本升级时先完成 `bodies` 事务迁移再删除旧表,迁移失败仍原子回滚。
16
+ - `patches/studio-0.2.19-managed-config.patch` 适配上游 `v0.3.1` 的 `WAA_BACKEND` 回写与回退分支:托管构建(`internal/config.managedBy=magpie-gemini-web`)在 `Load` 时不根据旧账户目录自动切换为 `camoufox` 或向 `.env` 追加键,在 `Save` 时继续完整写出全部非环境变量控制项。
17
+
18
+ ## 0.3.0 升级兼容(历史记录)
6
19
 
7
20
  - 基线为上游 `v0.3.0` 加 PR #109(`48f75fa722b25d22a493e75f6d4e5f9d5035d7e3`)。包含账号异步任务流、本机 Chrome / Cookie / CDP 浏览器账号导入、超过 63 字符的工具函数名自动压缩还原、`STRIP_THOUGHT_SIGNATURES`、Responses / Interactions / 音频状态持久化(`runtime/states.db`)、`gemini-live-2.5-flash` 与 `gemini-2.5-pro-tts` 等模型更新,以及上游默认 `WAA_BACKEND=spidermonkey`。Camoufox、SpiderMonkey 静态库和固定工具链沿用上一版。
8
21
  - 后台账户任务适配:上游 `POST /api/accounts` 与 `POST /api/accounts/{id}/login` 改为启动不随 HTTP 请求取消的后台任务(返回 `202 Accepted` 与 `{ task }`)。插件与 CLI 通过 `studioLogin` 轮询 `GET /api/accounts/tasks` 等待完成,再从 `GET /api/accounts` 读取账号,并在结束或超时退出时调用 `DELETE /api/accounts/tasks/{id}` 清理或取消后台任务(超时会自动关闭弹出的 Camoufox 登录窗口)。
@@ -36,7 +49,7 @@
36
49
 
37
50
  上游从 v0.2.14 起原生支持分发方管理更新。构建时通过 `-ldflags` 写入以下值:
38
51
 
39
- - `internal/app.version=v0.3.0-magpie.1`:明确的版本标识。同基线或更旧的官方版、以及更旧的托管版需要升级;更新的官方版、更新的托管版或不认识的定制版仍提示冲突。
52
+ - `internal/app.version=v0.3.2-magpie.1`:明确的版本标识。同基线或更旧的官方版、以及更旧的托管版需要升级;更新的官方版、更新的托管版或不认识的定制版仍提示冲突。
40
53
  - `internal/app.managedBy=magpie-gemini-web`:`GET /api/update`(包括 `?refresh=1`)只返回本地版本、`mode: managed` 和 `managed_by`,不访问 GitHub;`POST /api/update` 返回 HTTP 409,不下载、不替换文件、不重启。管理页显示“更新由 magpie-gemini-web 管理”。
41
54
 
42
55
  `internal/config.managedBy=magpie-gemini-web` 同时启用上面的配置兼容补丁。
@@ -113,7 +126,17 @@ AI_STUDIO_TEST_ARCHIVE=/absolute/path/to/archive.tar.gz npm test
113
126
 
114
127
  真实安装验证检查旧版请求正文迁移及重启后可读、管理页保存的备用请求设置、启动、版本、GET 更新状态、POST 更新返回 409、二进制及 PID 不变、管理页面显示托管提示、停止与状态。`.github/workflows/studio.yml` 在五种系统/架构的原生 runner 上重新构建、对比 SHA-256 并运行该验证;本机交叉编译不能代替 Linux/Windows 真机验证。
115
128
 
116
- ## 0.3.0 升级验证(2026-11-05)
129
+ ## 0.3.2 升级验证(2026-11-08)
130
+
131
+ - 五个平台的候选构建([run 42443570134](https://github.com/BUKOWSKIREAL/magpie-gemini-web/actions/runs/42443570134))通过上游测试、真实安装冒烟验证(含旧版 `bodies` 表迁移、Dock 图标设置保存与重启持久化、SpiderMonkey 与 libwebp 静态链接、管理页配置持久化)及插件测试;安装包和二进制 pin 来自该次构建。
132
+ - 随后的严格验证构建([run 42444479121](https://github.com/BUKOWSKIREAL/magpie-gemini-web/actions/runs/42444479121))在五个平台的全新 runner 上重新构建,安装包与可执行文件哈希均和 pin 完全一致,真实安装冒烟验证与插件测试全部通过。
133
+
134
+ ## 0.3.1 升级验证(2026-11-07,历史记录)
135
+
136
+ - 五个平台的候选构建([run 42255835006](https://github.com/BUKOWSKIREAL/magpie-gemini-web/actions/runs/42255835006))通过上游测试、真实安装冒烟验证(含旧版 `bodies` 表迁移、SpiderMonkey 与 libwebp 静态链接、管理页配置持久化)及插件测试;安装包和二进制 pin(含在固定 Xcode 26.3 `17C529` 下生成的 macOS 产物)来自该次构建。
137
+ - 随后的严格验证构建([run 42258671953](https://github.com/BUKOWSKIREAL/magpie-gemini-web/actions/runs/42258671953))在五个平台的全新 runner 上重新构建,哈希与 pin 完全一致,真实安装冒烟验证与插件测试全部通过。
138
+
139
+ ## 0.3.0 升级验证(2026-11-05,历史记录)
117
140
 
118
141
  - 五个平台的候选构建([run 41976798432](https://github.com/BUKOWSKIREAL/magpie-gemini-web/actions/runs/41976798432))一次性通过上游测试、真实安装冒烟验证(含 `strip_thought_signatures=false`、`/api/accounts/tasks` 接口与管理页配置持久化)及插件测试;安装包和二进制 pin 来自该次构建。
119
142
  - 随后的严格验证构建([run 41977975873](https://github.com/BUKOWSKIREAL/magpie-gemini-web/actions/runs/41977975873))在五个平台的全新 runner 上重新构建,哈希与 pin 完全一致,真实安装冒烟验证与插件测试全部通过。
@@ -149,14 +172,14 @@ AI_STUDIO_TEST_ARCHIVE=/absolute/path/to/archive.tar.gz npm test
149
172
  - 新版 Google 登录/生成端到端验证尚未完成。
150
173
  - 此分支未发布后端 release 或 npm 包。本机日常后端仍使用 `0.2.16-magpie.1`,新版仅在临时数据目录运行。
151
174
 
152
- 发布前必须审查各平台 CI 与构建记录,再将安装包及许可证上传到本仓库 `studio-v0.3.0-magpie.1` release。该 release 的安装包可公开下载后,才能发布对应 npm 版本,否则新安装会遇到 404。合并 PR 不代表已经满足发布条件;构建脚本及验证 CI 不自动发布 release 或 npm 包。
175
+ 发布前必须审查各平台 CI 与构建记录,再将安装包及许可证上传到本仓库 `studio-v0.3.2-magpie.1` release。该 release 的安装包可公开下载后,才能发布对应 npm 版本,否则新安装会遇到 404。合并 PR 不代表已经满足发布条件;构建脚本及验证 CI 不自动发布 release 或 npm 包。
153
176
 
154
177
  本地试用建议使用独立数据目录:
155
178
 
156
179
  ```bash
157
- node scripts/studio.mjs install --data-dir /path/to/studio-030-trial --archive /absolute/path/to/archive
158
- node scripts/studio.mjs start --data-dir /path/to/studio-030-trial
159
- node scripts/studio.mjs stop --data-dir /path/to/studio-030-trial
180
+ node scripts/studio.mjs install --data-dir /path/to/studio-031-trial --archive /absolute/path/to/archive
181
+ node scripts/studio.mjs start --data-dir /path/to/studio-031-trial
182
+ node scripts/studio.mjs stop --data-dir /path/to/studio-031-trial
160
183
  ```
161
184
 
162
185
  使用现有账号数据测试前,先停止旧服务并备份;不要让两个进程同时写同一个数据目录。
@@ -1,36 +1,36 @@
1
1
  {
2
- "version": "4.27.0-magpie.2",
2
+ "version": "4.27.1-magpie.1",
3
3
  "repository": "BUKOWSKIREAL/magpie-gemini-web",
4
- "tag": "backend-v4.27.0-magpie.2",
4
+ "tag": "backend-v4.27.1-magpie.1",
5
5
  "upstream": {
6
6
  "repository": "https://github.com/zexadev/gemini-web2api-go.git",
7
- "commit": "e97cfccc7e68e356ac504ba220e0b004382029a8"
7
+ "commit": "c8a4ac25768b668fa94d293fcb97059c4419b602"
8
8
  },
9
9
  "goVersion": "1.27.1",
10
10
  "binaries": {
11
11
  "darwin-arm64": {
12
- "asset": "gemini-web2api-go_v4.27.0-magpie.2_darwin_arm64",
13
- "sha256": "45c01d9a99142957eadf30b75ee17da13ea468ca9b4b0d342b9e7b2b440a9072"
12
+ "asset": "gemini-web2api-go_v4.27.1-magpie.1_darwin_arm64",
13
+ "sha256": "a0828624c3301e79a36461bf1197d87ad0836d26262bea69fd9255d018eccac2"
14
14
  },
15
15
  "darwin-x64": {
16
- "asset": "gemini-web2api-go_v4.27.0-magpie.2_darwin_amd64",
17
- "sha256": "4739f96c703f08ac8c21a7b699564bb26f80298a0ea8c30b126de501fc61d451"
16
+ "asset": "gemini-web2api-go_v4.27.1-magpie.1_darwin_amd64",
17
+ "sha256": "4b2a37dceefe7ed7f055f4c92f1038ccd3671c28bd612a4251a339a1c12dd975"
18
18
  },
19
19
  "linux-x64": {
20
- "asset": "gemini-web2api-go_v4.27.0-magpie.2_linux_amd64",
21
- "sha256": "837d5416507daf628984dd7e32aba69380f54d3385e8fd0b478af9b5a0c3dc1b"
20
+ "asset": "gemini-web2api-go_v4.27.1-magpie.1_linux_amd64",
21
+ "sha256": "bbded68869c0221868dcd81b96bdd05dce4afea0f4073e9ef4ed2b77a95f66d0"
22
22
  },
23
23
  "linux-arm64": {
24
- "asset": "gemini-web2api-go_v4.27.0-magpie.2_linux_arm64",
25
- "sha256": "8c070298efb4db79839b214c113cc486a4693cc3d144fc17c30daa4b96d9a99b"
24
+ "asset": "gemini-web2api-go_v4.27.1-magpie.1_linux_arm64",
25
+ "sha256": "95e9ca87c5bc64c59a54d86be31425afa775b4e6ba367fc2eec652e096151bfa"
26
26
  },
27
27
  "win32-x64": {
28
- "asset": "gemini-web2api-go_v4.27.0-magpie.2_windows_amd64.exe",
29
- "sha256": "ec9550617a73aa9e17847dd511edc9b64425fa980a5024ea385d6fd28ccb7d2d"
28
+ "asset": "gemini-web2api-go_v4.27.1-magpie.1_windows_amd64.exe",
29
+ "sha256": "4da25f05694e7d70d001b1d9665024ec2c98dd8714e310823a5d09d513fe8e02"
30
30
  },
31
31
  "win32-arm64": {
32
- "asset": "gemini-web2api-go_v4.27.0-magpie.2_windows_arm64.exe",
33
- "sha256": "7cb9d2a47fd2106da4db8e5e4983547b962d3e03dd3ed5f0ac529b2eab31f97a"
32
+ "asset": "gemini-web2api-go_v4.27.1-magpie.1_windows_arm64.exe",
33
+ "sha256": "782891140d65f7f364472ba2263e8498cf0b86872881246b3e5612ee74d06447"
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.0';
14
+ const UPSTREAM_VERSION = 'v4.27.1';
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.0_darwin_arm64', sha256: '0cc9b5628e24624aa6d6c6c2fadbbc4702632f9c2387b9271e0f596d6d2a44af' },
19
- 'darwin-x64': { asset: 'gemini-web2api-go_v4.27.0_darwin_amd64', sha256: '424e2c7c91922941a9e1beef72d1936da595aa74719f50132998fb2d2e57f20b' },
20
- 'linux-x64': { asset: 'gemini-web2api-go_v4.27.0_linux_amd64', sha256: '5320ff87c6158b648c074b9ae6c700a75b672c8f7119b62e6f22b034e970cfca' },
21
- 'linux-arm64': { asset: 'gemini-web2api-go_v4.27.0_linux_arm64', sha256: '66392aa8c67151ead0f5e2baa1f686e97754f8cc54b856e3605ae1e26633af01' },
22
- 'win32-x64': { asset: 'gemini-web2api-go_v4.27.0_windows_amd64.exe', sha256: 'd8e051abbb657bcaa2edf68faa5a077be881ba4de26facc87fc1f6b8fde04b81' },
23
- 'win32-arm64': { asset: 'gemini-web2api-go_v4.27.0_windows_arm64.exe', sha256: 'faae38bb2d88e583a0c7ef96f7dceb687b2ecf167675a20f0d75e011884d74a4' }
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' }
24
24
  };
25
25
 
26
26
  export function pinnedBinary(platform = process.platform, arch = process.arch, channel = 'optimized') {
@@ -195,7 +195,7 @@ 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
197
  'ROUTING_STRATEGY', 'UPSTREAM_CHANNELS', 'MODEL_MAPPING', 'MODEL_CHANNELS', 'WAA_BACKEND',
198
- 'MEDIA_OUTPUT', 'MEDIA_BASE_URL', 'AUTO_START', 'REQUEST_BODY_LOG', 'LOG_FILE', 'LOG_MAX_TOTAL_MB',
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'
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.3.0-magpie.1",
2
+ "version": "0.3.2-magpie.1",
3
3
  "repository": "BUKOWSKIREAL/magpie-gemini-web",
4
- "tag": "studio-v0.3.0-magpie.1",
4
+ "tag": "studio-v0.3.2-magpie.1",
5
5
  "goVersion": "1.27.1",
6
6
  "toolchains": {
7
7
  "zig": "0.16.0",
@@ -52,12 +52,38 @@
52
52
  "sha256": "942a3b57b202f899d799cac678b7471d01d0b34714070f0810c18fca37ffd721"
53
53
  }
54
54
  }
55
+ },
56
+ "libwebp": {
57
+ "release": "v1.6.0-aistudio2api.1",
58
+ "repository": "Mag1cFall/libwebp",
59
+ "targets": {
60
+ "darwin-arm64": {
61
+ "asset": "libwebp-v1.6.0-aistudio2api.1-darwin-arm64.tar.gz",
62
+ "sha256": "66394cee457fe48408718c2dafd34808077f446a25678d54b4c504f711e579a6"
63
+ },
64
+ "darwin-x64": {
65
+ "asset": "libwebp-v1.6.0-aistudio2api.1-darwin-amd64.tar.gz",
66
+ "sha256": "d526ba604c0d2937122587eee4d13a01edd7c6c3e7e6bfdc72ab453ab18ae89f"
67
+ },
68
+ "linux-x64": {
69
+ "asset": "libwebp-v1.6.0-aistudio2api.1-linux-amd64.tar.gz",
70
+ "sha256": "d5047d17eace8b9639a08786b750e46ee83a1f251b7b961cd0f049371974ab78"
71
+ },
72
+ "linux-arm64": {
73
+ "asset": "libwebp-v1.6.0-aistudio2api.1-linux-arm64.tar.gz",
74
+ "sha256": "299028ab369a7f8b34feefd41f68cebdc6118c1306d37a57731da9503cd64084"
75
+ },
76
+ "win32-x64": {
77
+ "asset": "libwebp-v1.6.0-aistudio2api.1-windows-amd64.tar.gz",
78
+ "sha256": "ca38ca4cfae9bfe5808dbcfa11707d81efd534d723d2b7622cd32ef1114dea1d"
79
+ }
80
+ }
55
81
  }
56
82
  },
57
83
  "upstream": {
58
- "version": "0.3.0",
84
+ "version": "0.3.2",
59
85
  "repository": "https://github.com/Mag1cFall/AIStudio2API.git",
60
- "commit": "48f75fa722b25d22a493e75f6d4e5f9d5035d7e3",
86
+ "commit": "4c44cb9765aa9fd497b91db1f5c1573504b9b85f",
61
87
  "camoufox": "152.0.4-beta.29"
62
88
  },
63
89
  "patches": [
@@ -66,29 +92,29 @@
66
92
  ],
67
93
  "binaries": {
68
94
  "darwin-arm64": {
69
- "asset": "aistudio2api-v0.3.0-magpie.1-darwin-arm64.tar.gz",
70
- "sha256": "fa2be9428645977c5fd1f88b373f2531d81f86dc0da1cb367918639ec2cc6ea0",
71
- "binarySha256": "ef368372a3c6d9b90a9ac50743b9e01526fb8370355606b2a47117f6d22071e0"
95
+ "asset": "aistudio2api-v0.3.2-magpie.1-darwin-arm64.tar.gz",
96
+ "sha256": "a3b10971d3b30d6c2621fc0ca179a041efd0b9992ed2fdb4117a689012dc86f3",
97
+ "binarySha256": "f0f2234862b4955a2adc4f24940db631aed3b62148e3732f4a9cb614e4d0c507"
72
98
  },
73
99
  "darwin-x64": {
74
- "asset": "aistudio2api-v0.3.0-magpie.1-darwin-amd64.tar.gz",
75
- "sha256": "2865c869800a211e57dd8362e5a0cad95b79c14228c03ed56e63477dc99317d2",
76
- "binarySha256": "85d59752c453c77d6d6c8289f50627029ead22d429c0064d3545d198d90d8963"
100
+ "asset": "aistudio2api-v0.3.2-magpie.1-darwin-amd64.tar.gz",
101
+ "sha256": "1b613b228054417904a8f8cebcb50446bbdd438ce19e104ae293053673b0816a",
102
+ "binarySha256": "a8b56b4f411929742482ef18538ce5b5cc4a0e690d50f049b8c8aa95d3513b3a"
77
103
  },
78
104
  "linux-x64": {
79
- "asset": "aistudio2api-v0.3.0-magpie.1-linux-amd64.tar.gz",
80
- "sha256": "432b4e7650f2d86f82965bdec221522a4d7e0ad2d49e1c58c3a3f0cb7fc1304b",
81
- "binarySha256": "cbce67249646dfe4006ec9f7e50ac141136755b0d26a04c435700ca352dd0129"
105
+ "asset": "aistudio2api-v0.3.2-magpie.1-linux-amd64.tar.gz",
106
+ "sha256": "fe1c2c65a3dd4a2971b40432070c438b372cd1680724f96c07638e1855a4cbfd",
107
+ "binarySha256": "01e78fd13fd5ff12c30f71e24501b9aa01faa6a5c67ecae526f8e52d34d8c7f3"
82
108
  },
83
109
  "linux-arm64": {
84
- "asset": "aistudio2api-v0.3.0-magpie.1-linux-arm64.tar.gz",
85
- "sha256": "5eb213627e48384d3e50c26bc1f056ddcd9eedce617847b1e8d477c529ecc5f8",
86
- "binarySha256": "10b8b30de5412f5855fabd314f25fa01a539d464a683b1b6e78608a549aac2cf"
110
+ "asset": "aistudio2api-v0.3.2-magpie.1-linux-arm64.tar.gz",
111
+ "sha256": "155fbae81c8e188d5770d4ad31e9feb74dc8e87aec92367d8d8069000441f99c",
112
+ "binarySha256": "3dbc76102beffe9a97dda612b9684f08b114176fae127763d5d1379cb3c08cdd"
87
113
  },
88
114
  "win32-x64": {
89
- "asset": "aistudio2api-v0.3.0-magpie.1-windows-amd64.zip",
90
- "sha256": "f426a010fb40505acb1499de523328a7afb386f551ab68781b4c124dbc2c9ba2",
91
- "binarySha256": "cb3cd70312f2df0e2f400621abc268dde7ba780979ccd7e95f2acebb8e9349cc"
115
+ "asset": "aistudio2api-v0.3.2-magpie.1-windows-amd64.zip",
116
+ "sha256": "054e9aac804a7116b626750a5a2adbcb8ad8d465e428b292530fdebc4f2b2cd5",
117
+ "binarySha256": "e7720ced68f82a2aa5afa16c17614c067a57ef2afa59399b720e9d215e7eb42a"
92
118
  }
93
119
  }
94
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magpie-gemini-web",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Gemini Web and Google AI Studio provider for Magpie, backed by gemini-web2api-go and AIStudio2API",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
@@ -1,5 +1,5 @@
1
1
  diff --git a/go.mod b/go.mod
2
- index c6a75e2..db2c8f5 100644
2
+ index c6a75e272d59110a5f83e71bfa89b4588622905d..db2c8f5f60618bad14c38e013b469132d2c0c546 100644
3
3
  --- a/go.mod
4
4
  +++ b/go.mod
5
5
  @@ -9,6 +9,7 @@ require (
@@ -19,7 +19,7 @@ index c6a75e2..db2c8f5 100644
19
19
  golang.org/x/text v0.42.0 // indirect
20
20
  modernc.org/libc v1.72.3 // indirect
21
21
  diff --git a/internal/app/admin_cookies.go b/internal/app/admin_cookies.go
22
- index 78236c6..a5109b0 100644
22
+ index 78236c6760f6509fcdce801ba853d9ecfafe16ca..a5109b0785d71c0ed1081b7316c2b3319223b160 100644
23
23
  --- a/internal/app/admin_cookies.go
24
24
  +++ b/internal/app/admin_cookies.go
25
25
  @@ -87,9 +87,10 @@ func handleAdminCookies(w http.ResponseWriter, r *http.Request) {
@@ -65,7 +65,7 @@ index 78236c6..a5109b0 100644
65
65
  // 只改传了的字段:没传的沿用当前值。
66
66
  var curLabel, curNote string
67
67
  diff --git a/internal/app/admin_ui/index.html b/internal/app/admin_ui/index.html
68
- index 4aedd27..2364e2b 100644
68
+ index 4aedd2788f396f98cb4267baa0b3674e7a8ba7af..2364e2baec4c0c3c7678b78d3bb5f7e118e4beed 100644
69
69
  --- a/internal/app/admin_ui/index.html
70
70
  +++ b/internal/app/admin_ui/index.html
71
71
  @@ -1020,7 +1020,8 @@ const RT_GROUPS = [
@@ -89,7 +89,7 @@ index 4aedd27..2364e2b 100644
89
89
  {
90
90
  diff --git a/internal/app/argon.go b/internal/app/argon.go
91
91
  new file mode 100644
92
- index 0000000..a2a5e77
92
+ index 0000000000000000000000000000000000000000..a2a5e774e8547afb125de6161c624bbfe572fe7e
93
93
  --- /dev/null
94
94
  +++ b/internal/app/argon.go
95
95
  @@ -0,0 +1,172 @@
@@ -267,7 +267,7 @@ index 0000000..a2a5e77
267
267
  +}
268
268
  diff --git a/internal/app/argon_probe_test.go b/internal/app/argon_probe_test.go
269
269
  new file mode 100644
270
- index 0000000..bbc0041
270
+ index 0000000000000000000000000000000000000000..bbc0041bb2d8769657d195f9b2b997f4c49da181
271
271
  --- /dev/null
272
272
  +++ b/internal/app/argon_probe_test.go
273
273
  @@ -0,0 +1,53 @@
@@ -326,7 +326,7 @@ index 0000000..bbc0041
326
326
  +}
327
327
  diff --git a/internal/app/argon_test.go b/internal/app/argon_test.go
328
328
  new file mode 100644
329
- index 0000000..2f0b8cd
329
+ index 0000000000000000000000000000000000000000..2f0b8cdc8f97bd303aa35bac1dda9d60611428a9
330
330
  --- /dev/null
331
331
  +++ b/internal/app/argon_test.go
332
332
  @@ -0,0 +1,453 @@
@@ -785,7 +785,7 @@ index 0000000..2f0b8cd
785
785
  +}
786
786
  diff --git a/internal/app/argon_timing_probe_test.go b/internal/app/argon_timing_probe_test.go
787
787
  new file mode 100644
788
- index 0000000..fb1ccec
788
+ index 0000000000000000000000000000000000000000..fb1ccec78a19896b0bf4ec1ffc3f97e957beb4f4
789
789
  --- /dev/null
790
790
  +++ b/internal/app/argon_timing_probe_test.go
791
791
  @@ -0,0 +1,74 @@
@@ -865,7 +865,7 @@ index 0000000..fb1ccec
865
865
  +}
866
866
  diff --git a/internal/app/cancel_test.go b/internal/app/cancel_test.go
867
867
  new file mode 100644
868
- index 0000000..f36c700
868
+ index 0000000000000000000000000000000000000000..f36c70005e6da66df26dd934271c0d9646faa9f2
869
869
  --- /dev/null
870
870
  +++ b/internal/app/cancel_test.go
871
871
  @@ -0,0 +1,106 @@
@@ -976,7 +976,7 @@ index 0000000..f36c700
976
976
  + t.Fatal("没有记录这次请求")
977
977
  +}
978
978
  diff --git a/internal/app/client.go b/internal/app/client.go
979
- index c63b3a9..da8ccb8 100644
979
+ index c63b3a9fabbf1fe2f141a05def3f9947dff32ca3..da8ccb8ff683903907e7159bea42268c299e2793 100644
980
980
  --- a/internal/app/client.go
981
981
  +++ b/internal/app/client.go
982
982
  @@ -1,9 +1,11 @@
@@ -1196,7 +1196,7 @@ index c63b3a9..da8ccb8 100644
1196
1196
  + setChromeHeaders(req.Header)
1197
1197
  }
1198
1198
  diff --git a/internal/app/client_timeout_test.go b/internal/app/client_timeout_test.go
1199
- index 94c0b70..2a4b697 100644
1199
+ index 94c0b7097137d057c85cc98df7fbebbf16207b46..2a4b697e943f58c55c33e2af48033853cbcc516a 100644
1200
1200
  --- a/internal/app/client_timeout_test.go
1201
1201
  +++ b/internal/app/client_timeout_test.go
1202
1202
  @@ -1,6 +1,12 @@
@@ -1309,7 +1309,7 @@ index 94c0b70..2a4b697 100644
1309
1309
  + }
1310
1310
  +}
1311
1311
  diff --git a/internal/app/context_file.go b/internal/app/context_file.go
1312
- index 97995a2..ba03140 100644
1312
+ index 97995a2f0ebb7626512590a23da1bc3f6fc97e3b..ba031401e77e479ebb33cc3a7868c904c7cb42ea 100644
1313
1313
  --- a/internal/app/context_file.go
1314
1314
  +++ b/internal/app/context_file.go
1315
1315
  @@ -1,6 +1,7 @@
@@ -1388,7 +1388,7 @@ index 97995a2..ba03140 100644
1388
1388
  + return promptParts{body: parts}.join()
1389
1389
  }
1390
1390
  diff --git a/internal/app/conversation.go b/internal/app/conversation.go
1391
- index 529cfad..c48b692 100644
1391
+ index e961a1b6af302999fe46d017feb3e4ed2c3301c1..39adf8893fab0abf1e999e73f3a9d604c72cf40e 100644
1392
1392
  --- a/internal/app/conversation.go
1393
1393
  +++ b/internal/app/conversation.go
1394
1394
  @@ -1,6 +1,7 @@
@@ -1428,7 +1428,7 @@ index 529cfad..c48b692 100644
1428
1428
  p, slotOK, slotErr := acquireSlot(conv.proxyID)
1429
1429
  if !slotOK {
1430
1430
  return &StreamResult{}, slotErr
1431
- @@ -371,12 +371,19 @@ func streamGenerateConv(prompt string, mc ModelConfig, conv *convState,
1431
+ @@ -375,12 +375,19 @@ func streamGenerateConv(prompt string, mc ModelConfig, conv *convState,
1432
1432
  t0 := time.Now()
1433
1433
  var lastErr error
1434
1434
  for attempt := 0; attempt < rtCfg().RetryAttempts; attempt++ {
@@ -1449,7 +1449,7 @@ index 529cfad..c48b692 100644
1449
1449
  // 降级:同一个号重试还是降级,直接交给上层(见 gemini.go 同一处)。
1450
1450
  var mde *ModelDowngradedError
1451
1451
  if errors.As(err, &mde) {
1452
- @@ -385,6 +392,9 @@ func streamGenerateConv(prompt string, mc ModelConfig, conv *convState,
1452
+ @@ -389,6 +396,9 @@ func streamGenerateConv(prompt string, mc ModelConfig, conv *convState,
1453
1453
  Emitted: tracker.emitted, EmittedReasoning: rtracker.emitted}, err
1454
1454
  }
1455
1455
  if err != nil || statusCode != 200 || !hasContentFrame(string(raw)) {
@@ -1459,10 +1459,21 @@ index 529cfad..c48b692 100644
1459
1459
  if err == nil && statusCode == 200 {
1460
1460
  lastErr = fmt.Errorf("续接无内容帧(会话可能已失效,raw %d 字节)", len(raw))
1461
1461
  } else if err != nil {
1462
- @@ -399,7 +409,9 @@ func streamGenerateConv(prompt string, mc ModelConfig, conv *convState,
1463
- break
1462
+ @@ -407,7 +417,9 @@ func streamGenerateConv(prompt string, mc ModelConfig, conv *convState,
1463
+ if errors.Is(err, errUpstreamFailureReply) {
1464
+ logf("retry %d/%d: %v", attempt+1, rtCfg().RetryAttempts, err)
1465
+ }
1466
+ - time.Sleep(time.Duration(rtCfg().RetryDelaySec) * time.Second)
1467
+ + if werr := sleepCtx(ctx, time.Duration(rtCfg().RetryDelaySec)*time.Second); werr != nil {
1468
+ + return &StreamResult{ProxyID: p.ID, ProxyName: p.Name, AccountID: conv.accountID}, werr
1469
+ + }
1470
+ }
1471
+ continue
1472
+ }
1473
+ @@ -419,7 +431,9 @@ func streamGenerateConv(prompt string, mc ModelConfig, conv *convState,
1464
1474
  }
1465
1475
  if attempt < rtCfg().RetryAttempts-1 {
1476
+ logf("retry %d/%d: %v", attempt+1, rtCfg().RetryAttempts, lastErr)
1466
1477
  - time.Sleep(time.Duration(rtCfg().RetryDelaySec) * time.Second)
1467
1478
  + if werr := sleepCtx(ctx, time.Duration(rtCfg().RetryDelaySec)*time.Second); werr != nil {
1468
1479
  + return &StreamResult{ProxyID: p.ID, ProxyName: p.Name, AccountID: conv.accountID}, werr
@@ -1470,7 +1481,7 @@ index 529cfad..c48b692 100644
1470
1481
  }
1471
1482
  continue
1472
1483
  }
1473
- @@ -440,7 +452,7 @@ func streamGenerateConv(prompt string, mc ModelConfig, conv *convState,
1484
+ @@ -460,7 +474,7 @@ func streamGenerateConv(prompt string, mc ModelConfig, conv *convState,
1474
1485
  // - 没命中 → 新建会话,首轮发全量拼接的历史(含 tools 指令,把当前上下文交给服务端)。
1475
1486
  //
1476
1487
  // 支持 tools(续接里也解析 ```tool_call``` 围栏);图片走原单轮路径(callGemini)。
@@ -1479,7 +1490,7 @@ index 529cfad..c48b692 100644
1479
1490
  tools []map[string]interface{}, toolChoice interface{},
1480
1491
  onDelta, onReasoning func(string)) (string, []ToolCall, *StreamResult, error) {
1481
1492
  parentKey := convParentKey(messages)
1482
- @@ -491,7 +503,7 @@ func callGeminiConv(messages []map[string]interface{}, mc ModelConfig,
1493
+ @@ -511,7 +525,7 @@ func callGeminiConv(messages []map[string]interface{}, mc ModelConfig,
1483
1494
  }
1484
1495
  }
1485
1496
  }
@@ -1489,7 +1500,7 @@ index 529cfad..c48b692 100644
1489
1500
  break
1490
1501
  }
1491
1502
  diff --git a/internal/app/cookie_pool.go b/internal/app/cookie_pool.go
1492
- index 60fe35f..14e5a44 100644
1503
+ index 60fe35fe0664c2feeb9d9d811be01c6d3323799c..14e5a44734cdd50be933af1cf8387a65500ab3aa 100644
1493
1504
  --- a/internal/app/cookie_pool.go
1494
1505
  +++ b/internal/app/cookie_pool.go
1495
1506
  @@ -565,6 +565,9 @@ func checkAccountCookie(a CookieAccount) CookieCheck {
@@ -1503,7 +1514,7 @@ index 60fe35f..14e5a44 100644
1503
1514
  if extractSAPISID(a.Cookie) == "" {
1504
1515
  detail += ",但缺 SAPISID(算不出授权头,部分接口会被当匿名)"
1505
1516
  diff --git a/internal/app/gemini.go b/internal/app/gemini.go
1506
- index 7572da2..d52d582 100644
1517
+ index e07759d3b1fa17d6e81afab0e42e355925d6cbc9..8b9ad100a2fcd5e7b53e1a9855f28eee98d110d0 100644
1507
1518
  --- a/internal/app/gemini.go
1508
1519
  +++ b/internal/app/gemini.go
1509
1520
  @@ -3,6 +3,7 @@ package app
@@ -1613,7 +1624,7 @@ index 7572da2..d52d582 100644
1613
1624
  return attrib(ferr)
1614
1625
  } else if used {
1615
1626
  prompt = p
1616
- @@ -726,45 +748,27 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1627
+ @@ -726,49 +748,27 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1617
1628
  xsrfRetried := false // XSRF 自愈只做一次,避免死循环
1618
1629
  t0 := time.Now()
1619
1630
 
@@ -1628,6 +1639,10 @@ index 7572da2..d52d582 100644
1628
1639
  - return err
1629
1640
  - }
1630
1641
  - }
1642
+ - // 出错话术在吐给客户端之前拦下,中断读取走下面的重试。
1643
+ - if failureReplyInLine(line) {
1644
+ - return errUpstreamFailureReply
1645
+ - }
1631
1646
  - // 思考链先推完才轮到正文,所以先处理它,客户端拿到的顺序才对。
1632
1647
  - if onReasoning != nil {
1633
1648
  - if r := reasoningInLine(line); r != "" {
@@ -1639,7 +1654,7 @@ index 7572da2..d52d582 100644
1639
1654
  - if onDelta == nil {
1640
1655
  - return nil
1641
1656
  - }
1642
- - for _, t := range textsInLine(line) {
1657
+ - for _, t := range renderedTextsInLine(line, false) {
1643
1658
  - if d := tracker.Push(t); d != "" {
1644
1659
  - onDelta(d)
1645
1660
  - }
@@ -1671,7 +1686,7 @@ index 7572da2..d52d582 100644
1671
1686
  var statusCode int
1672
1687
  var raw []byte
1673
1688
  var ttfb int64
1674
- @@ -772,9 +776,9 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1689
+ @@ -776,9 +776,9 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1675
1690
  var err error
1676
1691
  if useWC {
1677
1692
  wcReq.Cookie = cookieStr
@@ -1683,7 +1698,7 @@ index 7572da2..d52d582 100644
1683
1698
  }
1684
1699
  if len(setCookie) > 0 && cookieID > 0 {
1685
1700
  if merged := mergeSetCookie(cookieStr, setCookie); merged != cookieStr {
1686
- @@ -782,6 +786,10 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1701
+ @@ -786,6 +786,10 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1687
1702
  updateAccountCookie(cookieID, merged)
1688
1703
  }
1689
1704
  }
@@ -1694,7 +1709,7 @@ index 7572da2..d52d582 100644
1694
1709
  // 降级不是出口或 cookie 的错,也不该重试:同一个号再发一次还是降级。
1695
1710
  var mde *ModelDowngradedError
1696
1711
  if errors.As(err, &mde) {
1697
- @@ -793,6 +801,9 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1712
+ @@ -797,6 +801,9 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1698
1713
  return res, err
1699
1714
  }
1700
1715
  if err != nil {
@@ -1702,9 +1717,9 @@ index 7572da2..d52d582 100644
1702
1717
  + return attrib(cerr) // 客户端断开,不是出口的错
1703
1718
  + }
1704
1719
  lastErr = err
1705
- if pickedOK {
1706
- recordProxyResult(picked.ID, false, err.Error())
1707
- @@ -803,7 +814,9 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1720
+ // 出错话术不记进代理健康度:匿名直连也一样出,跟出口无关,算进去会误熔断。
1721
+ if pickedOK && !errors.Is(err, errUpstreamFailureReply) {
1722
+ @@ -808,7 +815,9 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1708
1723
  }
1709
1724
  if attempt < rtCfg().RetryAttempts-1 {
1710
1725
  logf("retry %d/%d: %v", attempt+1, rtCfg().RetryAttempts, err)
@@ -1715,7 +1730,7 @@ index 7572da2..d52d582 100644
1715
1730
  }
1716
1731
  continue
1717
1732
  }
1718
- @@ -832,7 +845,9 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1733
+ @@ -837,7 +846,9 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1719
1734
  break
1720
1735
  }
1721
1736
  if attempt < rtCfg().RetryAttempts-1 {
@@ -1726,7 +1741,7 @@ index 7572da2..d52d582 100644
1726
1741
  }
1727
1742
  continue
1728
1743
  }
1729
- @@ -860,7 +875,9 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1744
+ @@ -865,7 +876,9 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1730
1745
  }
1731
1746
  if attempt < rtCfg().RetryAttempts-1 {
1732
1747
  logf("retry %d/%d: 空响应(无内容帧,%d 字节)", attempt+1, rtCfg().RetryAttempts, len(raw))
@@ -1737,7 +1752,18 @@ index 7572da2..d52d582 100644
1737
1752
  }
1738
1753
  continue
1739
1754
  }
1740
- @@ -926,6 +943,78 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1755
+ @@ -877,7 +890,9 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1756
+ }
1757
+ if attempt < rtCfg().RetryAttempts-1 {
1758
+ logf("retry %d/%d: %v", attempt+1, rtCfg().RetryAttempts, lastErr)
1759
+ - time.Sleep(time.Duration(rtCfg().RetryDelaySec) * time.Second)
1760
+ + if werr := retryWait(); werr != nil {
1761
+ + return attrib(werr)
1762
+ + }
1763
+ }
1764
+ continue
1765
+ }
1766
+ @@ -943,6 +958,82 @@ func streamGenerateOnce(prompt, latest string, mc ModelConfig, pending []pending
1741
1767
  return attrib(lastErr)
1742
1768
  }
1743
1769
 
@@ -1767,6 +1793,10 @@ index 7572da2..d52d582 100644
1767
1793
  + return err
1768
1794
  + }
1769
1795
  + }
1796
+ + // 出错话术在吐给客户端之前拦下,中断读取走重试。
1797
+ + if failureReplyInLine(line) {
1798
+ + return errUpstreamFailureReply
1799
+ + }
1770
1800
  + // 思考链先推完才轮到正文,所以先处理它,客户端拿到的顺序才对。
1771
1801
  + if onReasoning != nil {
1772
1802
  + if r := reasoningInLine(line); r != "" {
@@ -1779,7 +1809,7 @@ index 7572da2..d52d582 100644
1779
1809
  + return nil
1780
1810
  + }
1781
1811
  + grew := false
1782
- + for _, t := range textsInLine(line) {
1812
+ + for _, t := range renderedTextsInLine(line, false) {
1783
1813
  + if d := watch.Push(t); d != "" {
1784
1814
  + grew = true
1785
1815
  + if onDelta != nil {
@@ -1816,7 +1846,7 @@ index 7572da2..d52d582 100644
1816
1846
  // upstreamModelRe 匹配响应帧里服务端自报的模型显示名(帧的 [42] 位)。
1817
1847
  // 形如 ...,"fbb127bbb056c959",null,null,"3.6 Flash",true,...
1818
1848
  var upstreamModelRe = regexp.MustCompile(`\\"[0-9a-f]{16}\\",null,null,\\"([^"\\]{1,40})\\"`)
1819
- @@ -1006,10 +1095,122 @@ func setAuthUserHeaders(h map[string]string, authuser int) {
1849
+ @@ -1023,10 +1114,122 @@ func setAuthUserHeaders(h map[string]string, authuser int) {
1820
1850
  // 实测号活一两小时就失效 —— 所以这些必须收下来并写回账号。
1821
1851
  func doGeminiRequest(endpoint, body string, headers map[string]string, proxyURL string,
1822
1852
  onLine func(string) error) (int, []byte, int64, []string, error) {
@@ -1940,7 +1970,7 @@ index 7572da2..d52d582 100644
1940
1970
  if err != nil {
1941
1971
  return 0, nil, 0, nil, err
1942
1972
  }
1943
- @@ -1017,36 +1218,37 @@ func doGeminiRequest(endpoint, body string, headers map[string]string, proxyURL
1973
+ @@ -1034,36 +1237,37 @@ func doGeminiRequest(endpoint, body string, headers map[string]string, proxyURL
1944
1974
  for k, v := range headers {
1945
1975
  req.Header.Set(k, v)
1946
1976
  }
@@ -1987,7 +2017,7 @@ index 7572da2..d52d582 100644
1987
2017
  }
1988
2018
  return resp.StatusCode, raw, ttfb, resp.Header.Values("Set-Cookie"), nil
1989
2019
  }
1990
- @@ -1355,6 +1557,7 @@ func probeGemini(prompt, proxyURL string) ProbeResult {
2020
+ @@ -1421,6 +1625,7 @@ func probeGemini(prompt, proxyURL string) ProbeResult {
1991
2021
  res.Diagnostic = "构建请求失败: " + e.Error()
1992
2022
  return res
1993
2023
  }
@@ -1996,7 +2026,7 @@ index 7572da2..d52d582 100644
1996
2026
  req.Header.Set(k, v)
1997
2027
  }
1998
2028
  diff --git a/internal/app/gemini_api.go b/internal/app/gemini_api.go
1999
- index 3721e44..d5950ab 100644
2029
+ index 3721e4492d70b1e8647242a4ffb3df15a7f8fba1..d5950ab09511f419dfd39aa7328a397b9b2783de 100644
2000
2030
  --- a/internal/app/gemini_api.go
2001
2031
  +++ b/internal/app/gemini_api.go
2002
2032
  @@ -1,6 +1,7 @@
@@ -2072,10 +2102,10 @@ index 3721e44..d5950ab 100644
2072
2102
  if _, ok := err.(*ModelDowngradedError); ok {
2073
2103
  status = 429
2074
2104
  diff --git a/internal/app/gemini_test.go b/internal/app/gemini_test.go
2075
- index 00eb305..2b4c6ca 100644
2105
+ index b6830a418c78b045c21d39cb690e2b32542c7262..eb4e02114acdecb78531ad91e72b66d274a6467e 100644
2076
2106
  --- a/internal/app/gemini_test.go
2077
2107
  +++ b/internal/app/gemini_test.go
2078
- @@ -15,6 +15,7 @@ func TestModelHeader(t *testing.T) {
2108
+ @@ -16,6 +16,7 @@ func TestModelHeader(t *testing.T) {
2079
2109
  {"gemini-3.6-flash", hexFlash36},
2080
2110
  {"gemini-3.5-flash-lite", hexFlashLite},
2081
2111
  {"gemini-3.1-pro", hexPro31},
@@ -2085,7 +2115,7 @@ index 00eb305..2b4c6ca 100644
2085
2115
  name, mc, err := resolveModel(c.name)
2086
2116
  diff --git a/internal/app/headers_test.go b/internal/app/headers_test.go
2087
2117
  new file mode 100644
2088
- index 0000000..ef9b476
2118
+ index 0000000000000000000000000000000000000000..ef9b476da72bae3cc53c9cd85510a2f1db224a7d
2089
2119
  --- /dev/null
2090
2120
  +++ b/internal/app/headers_test.go
2091
2121
  @@ -0,0 +1,74 @@
@@ -2165,7 +2195,7 @@ index 0000000..ef9b476
2165
2195
  +}
2166
2196
  diff --git a/internal/app/latency_test.go b/internal/app/latency_test.go
2167
2197
  new file mode 100644
2168
- index 0000000..fb778f5
2198
+ index 0000000000000000000000000000000000000000..fb778f55c20fe14ce2bac64c1e97ff4182328023
2169
2199
  --- /dev/null
2170
2200
  +++ b/internal/app/latency_test.go
2171
2201
  @@ -0,0 +1,333 @@
@@ -2503,7 +2533,7 @@ index 0000000..fb778f5
2503
2533
  + }
2504
2534
  +}
2505
2535
  diff --git a/internal/app/main_test.go b/internal/app/main_test.go
2506
- index 38e1a44..e3f7636 100644
2536
+ index 38e1a44d97d73524eafc2e67470d17b98a957c3b..e3f7636fd47514cf505abcb765125e48d4e7237d 100644
2507
2537
  --- a/internal/app/main_test.go
2508
2538
  +++ b/internal/app/main_test.go
2509
2539
  @@ -1,9 +1,12 @@
@@ -2562,7 +2592,7 @@ index 38e1a44..e3f7636 100644
2562
2592
  + t.Cleanup(func() { rtMu.Lock(); rtVal.RequestTimeout = saved; rtMu.Unlock() })
2563
2593
  +}
2564
2594
  diff --git a/internal/app/mcp_media.go b/internal/app/mcp_media.go
2565
- index acb1131..a8bab02 100644
2595
+ index acb113196d65f76745f816e2c1a9579884384774..a8bab02191b7fc8ae47afa59fce3744fe11f875b 100644
2566
2596
  --- a/internal/app/mcp_media.go
2567
2597
  +++ b/internal/app/mcp_media.go
2568
2598
  @@ -1,6 +1,7 @@
@@ -2583,7 +2613,7 @@ index acb1131..a8bab02 100644
2583
2613
  return nil, err
2584
2614
  }
2585
2615
  diff --git a/internal/app/media.go b/internal/app/media.go
2586
- index ec3b6e4..07f3164 100644
2616
+ index ec3b6e499aab0bbe4adc1e3ad517e2c9296e9c46..07f316410de88d6c3bae7435344e31fdcfc35157 100644
2587
2617
  --- a/internal/app/media.go
2588
2618
  +++ b/internal/app/media.go
2589
2619
  @@ -762,6 +762,7 @@ func mediaGetOnce(rawURL string, headers map[string]string, proxyURL string) (in
@@ -2595,10 +2625,10 @@ index ec3b6e4..07f3164 100644
2595
2625
  req.Header.Set(k, v)
2596
2626
  }
2597
2627
  diff --git a/internal/app/messages.go b/internal/app/messages.go
2598
- index 81b8900..9d1702d 100644
2628
+ index 1bd5bb4ac780da437deb257d8e9f9f1f33852ede..3d473c4f1a88cd1b40b2c9e75889b66a01740dde 100644
2599
2629
  --- a/internal/app/messages.go
2600
2630
  +++ b/internal/app/messages.go
2601
- @@ -48,8 +48,50 @@ type ToolCallFunction struct {
2631
+ @@ -49,8 +49,50 @@ type ToolCallFunction struct {
2602
2632
  // 出口才知道能不能做,所以判断放在 streamGenerate 里。最新那条消息单独返回,
2603
2633
  // 转附件时用来内联,好让模型不必去文件里找问题。
2604
2634
  func messagesToPrompt(messages []map[string]interface{}, tools []map[string]interface{},
@@ -2651,7 +2681,7 @@ index 81b8900..9d1702d 100644
2651
2681
  }
2652
2682
 
2653
2683
  // latestUserMessage 取最后一条 user 消息的正文,没有则返回空串。
2654
- @@ -67,6 +109,12 @@ func latestUserMessage(messages []map[string]interface{}) string {
2684
+ @@ -68,6 +110,12 @@ func latestUserMessage(messages []map[string]interface{}) string {
2655
2685
 
2656
2686
  func buildPrompt(messages []map[string]interface{}, tools []map[string]interface{},
2657
2687
  toolChoice interface{}) string {
@@ -2664,7 +2694,7 @@ index 81b8900..9d1702d 100644
2664
2694
  var parts []string
2665
2695
 
2666
2696
  toolsInjected := false
2667
- @@ -91,7 +139,7 @@ func buildPrompt(messages []map[string]interface{}, tools []map[string]interface
2697
+ @@ -92,7 +140,7 @@ func buildPrompt(messages []map[string]interface{}, tools []map[string]interface
2668
2698
  })
2669
2699
  }
2670
2700
  if len(defs) > 0 {
@@ -2673,7 +2703,7 @@ index 81b8900..9d1702d 100644
2673
2703
  rule := "Only use tool_call blocks when needed."
2674
2704
  switch {
2675
2705
  case forced != "":
2676
- @@ -104,13 +152,13 @@ func buildPrompt(messages []map[string]interface{}, tools []map[string]interface
2706
+ @@ -105,13 +153,13 @@ func buildPrompt(messages []map[string]interface{}, tools []map[string]interface
2677
2707
  "and nothing else — do not answer the question yourself, even if you " +
2678
2708
  "know the answer."
2679
2709
  }
@@ -2690,7 +2720,7 @@ index 81b8900..9d1702d 100644
2690
2720
  toolsInjected = true
2691
2721
  }
2692
2722
  }
2693
- @@ -167,33 +215,27 @@ func buildPrompt(messages []map[string]interface{}, tools []map[string]interface
2723
+ @@ -168,33 +216,27 @@ func buildPrompt(messages []map[string]interface{}, tools []map[string]interface
2694
2724
  // 工具名(shell_command)0/4 抄假名 —— 示例教的是「动作」不是名字,故不必按客户端
2695
2725
  // 动态生成 schema。
2696
2726
  if toolsInjected {
@@ -2743,8 +2773,8 @@ index 81b8900..9d1702d 100644
2743
2773
  }
2744
2774
 
2745
2775
  func contentToString(content interface{}) string {
2746
- @@ -440,6 +482,48 @@ func parseToolCalls(text string) (string, []ToolCall) {
2747
- return strings.TrimSpace(clean.String()), toolCalls
2776
+ @@ -511,6 +553,45 @@ func argsObject(v interface{}) (string, bool) {
2777
+ return string(b), true
2748
2778
  }
2749
2779
 
2750
2780
  +// toolCallComplete 返回一个判定函数:累积正文里出现了**闭合且能解析**、函数名在
@@ -2778,14 +2808,11 @@ index 81b8900..9d1702d 100644
2778
2808
  + if !ok {
2779
2809
  + return false
2780
2810
  + }
2781
- + var data struct {
2782
- + Name string `json:"name"`
2783
- + Arguments map[string]interface{} `json:"arguments"`
2784
- + }
2785
- + if json.Unmarshal([]byte(strings.TrimSpace(after[:body])), &data) != nil {
2811
+ + var raw map[string]interface{}
2812
+ + if json.Unmarshal([]byte(strings.TrimSpace(after[:body])), &raw) != nil || raw == nil {
2786
2813
  + return false
2787
2814
  + }
2788
- + return names[data.Name]
2815
+ + return names[toolCallName(raw)]
2789
2816
  + }
2790
2817
  +}
2791
2818
  +
@@ -2793,7 +2820,7 @@ index 81b8900..9d1702d 100644
2793
2820
  // 返回 (mode, forcedName):mode ∈ {"auto","none","required"};
2794
2821
  // forcedName 非空表示客户端点名了某个函数。
2795
2822
  diff --git a/internal/app/server.go b/internal/app/server.go
2796
- index 830fa6a..03f9b4b 100644
2823
+ index e29ce3e3ac4372ba65a617ca8c4fdd0aba4da642..b3235675a255057abaa83467cc87b651e7d4d0ac 100644
2797
2824
  --- a/internal/app/server.go
2798
2825
  +++ b/internal/app/server.go
2799
2826
  @@ -1,6 +1,7 @@
@@ -2808,15 +2835,21 @@ index 830fa6a..03f9b4b 100644
2808
2835
  "time"
2809
2836
  )
2810
2837
 
2811
- -const Version = "4.27.0"
2812
- +const Version = "4.27.0-magpie.2"
2838
+ -const Version = "4.27.1"
2839
+ +const Version = "4.27.1-magpie.1"
2813
2840
 
2814
2841
  func writeJSON(w http.ResponseWriter, status int, data interface{}) {
2815
2842
  body, _ := json.Marshal(data)
2816
- @@ -43,6 +44,7 @@ func handleRoot(w http.ResponseWriter, r *http.Request) {
2817
- "status": "ok",
2818
- "version": Version,
2819
- "models": modelNames,
2843
+ @@ -40,9 +41,10 @@ func handleRoot(w http.ResponseWriter, r *http.Request) {
2844
+ modelNames = append(modelNames, n)
2845
+ }
2846
+ writeJSON(w, 200, map[string]interface{}{
2847
+ - "status": "ok",
2848
+ - "version": Version,
2849
+ - "models": modelNames,
2850
+ + "status": "ok",
2851
+ + "version": Version,
2852
+ + "models": modelNames,
2820
2853
  + "capabilities": map[string]bool{"account_proxy_binding": true},
2821
2854
  })
2822
2855
  }
@@ -2909,7 +2942,7 @@ index 830fa6a..03f9b4b 100644
2909
2942
  if downgraded {
2910
2943
  diff --git a/internal/app/stall_test.go b/internal/app/stall_test.go
2911
2944
  new file mode 100644
2912
- index 0000000..cfdd6cf
2945
+ index 0000000000000000000000000000000000000000..cfdd6cf4e739c9fe50992fae42f7892e538916d9
2913
2946
  --- /dev/null
2914
2947
  +++ b/internal/app/stall_test.go
2915
2948
  @@ -0,0 +1,199 @@
@@ -3113,7 +3146,7 @@ index 0000000..cfdd6cf
3113
3146
  + }
3114
3147
  +}
3115
3148
  diff --git a/internal/app/strict_model.go b/internal/app/strict_model.go
3116
- index ecc17ac..88267c5 100644
3149
+ index ecc17aceec6f365649ccb84bc9ca04189b524f03..88267c5b550800765d2b50f474e4ba7670bb6efd 100644
3117
3150
  --- a/internal/app/strict_model.go
3118
3151
  +++ b/internal/app/strict_model.go
3119
3152
  @@ -150,7 +150,8 @@ func strictLineCheck(mc ModelConfig, accountID func() int64) func(line string) e
@@ -3127,7 +3160,7 @@ index ecc17ac..88267c5 100644
3127
3160
 
3128
3161
  // modelDowngraded 判断服务端自报的显示名是不是请求的那个模型。
3129
3162
  diff --git a/internal/app/tokenizer.go b/internal/app/tokenizer.go
3130
- index 634f3b7..399057e 100644
3163
+ index 634f3b76c1ceea03f540e4c2fc5e3a9ca53ccd2b..399057e5ab5e62120094454e2e81da6b12c0d8d8 100644
3131
3164
  --- a/internal/app/tokenizer.go
3132
3165
  +++ b/internal/app/tokenizer.go
3133
3166
  @@ -1,9 +1,17 @@
@@ -3262,7 +3295,7 @@ index 634f3b7..399057e 100644
3262
3295
  if err != nil {
3263
3296
  fmt.Fprintf(os.Stderr, "[tokenizer] init failed (will fall back to chars/4): %v\n", err)
3264
3297
  diff --git a/internal/app/upload.go b/internal/app/upload.go
3265
- index 4feb5d9..a21e8d1 100644
3298
+ index 4feb5d93c088ed058f1f3b57c2e9ca1dc9c6cab1..a21e8d1e671d12d7523648bc3c0948958fe594a3 100644
3266
3299
  --- a/internal/app/upload.go
3267
3300
  +++ b/internal/app/upload.go
3268
3301
  @@ -2,6 +2,7 @@ package app
@@ -3333,7 +3366,7 @@ index 4feb5d9..a21e8d1 100644
3333
3366
  req.Header.Set(k, v)
3334
3367
  }
3335
3368
  diff --git a/internal/app/video_cooldown_test.go b/internal/app/video_cooldown_test.go
3336
- index 8eda49e..cf64828 100644
3369
+ index 8eda49e5f5318f962445c4b08bb33b342f8e2b8f..cf64828c36dcb190fae2e6315573460a9ba4a711 100644
3337
3370
  --- a/internal/app/video_cooldown_test.go
3338
3371
  +++ b/internal/app/video_cooldown_test.go
3339
3372
  @@ -2,6 +2,7 @@ package app
@@ -3354,7 +3387,7 @@ index 8eda49e..cf64828 100644
3354
3387
  return "", nil, res, err
3355
3388
  }
3356
3389
  diff --git a/internal/app/videos.go b/internal/app/videos.go
3357
- index 4ea7ac6..861f72e 100644
3390
+ index 4ea7ac64d3854396b8339b0d2240a96d7a8e9331..861f72e355a62e191c875e7ea72dbe28d51b556f 100644
3358
3391
  --- a/internal/app/videos.go
3359
3392
  +++ b/internal/app/videos.go
3360
3393
  @@ -10,6 +10,7 @@ package app
@@ -3384,7 +3417,7 @@ index 4ea7ac6..861f72e 100644
3384
3417
  return res, nil
3385
3418
  }
3386
3419
  diff --git a/internal/app/vision.go b/internal/app/vision.go
3387
- index b0c9724..b1ca7cb 100644
3420
+ index b0c97245b5e822155b97d5c3b82363dd1cbf27a2..b1ca7cb8d9620d1d295543bd7118f507d32b7980 100644
3388
3421
  --- a/internal/app/vision.go
3389
3422
  +++ b/internal/app/vision.go
3390
3423
  @@ -160,6 +160,7 @@ func fetchImage(src, proxyURL string, idx int) (pendingUpload, error) {
@@ -3396,7 +3429,7 @@ index b0c9724..b1ca7cb 100644
3396
3429
  if err != nil {
3397
3430
  return pendingUpload{}, fmt.Errorf("image %d: fetch failed: %w", idx, err)
3398
3431
  diff --git a/internal/app/xsrf.go b/internal/app/xsrf.go
3399
- index 254d686..e008abb 100644
3432
+ index 254d686a8f91301427d341f31a08363f4ba6c304..e008abbd2e3e901a8ee7e3b4bde6f1c65833f730 100644
3400
3433
  --- a/internal/app/xsrf.go
3401
3434
  +++ b/internal/app/xsrf.go
3402
3435
  @@ -29,15 +29,23 @@ var (
@@ -55,10 +55,10 @@ index 0000000..5569c03
55
55
  +}
56
56
  diff --git a/internal/requestdb/legacy_bodies_test.go b/internal/requestdb/legacy_bodies_test.go
57
57
  new file mode 100644
58
- index 0000000..2761c06
58
+ index 0000000..f815636
59
59
  --- /dev/null
60
60
  +++ b/internal/requestdb/legacy_bodies_test.go
61
- @@ -0,0 +1,77 @@
61
+ @@ -0,0 +1,73 @@
62
62
  +package requestdb
63
63
  +
64
64
  +import (
@@ -71,11 +71,7 @@ index 0000000..2761c06
71
71
  +
72
72
  +func TestLegacyBodiesMigration(t *testing.T) {
73
73
  + file := filepath.Join(t.TempDir(), "requests.db")
74
- + db, err := sqlitedb.Open(file)
75
- + if err != nil {
76
- + t.Fatal(err)
77
- + }
78
- + _, err = db.Exec(`CREATE TABLE bodies (seq INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE, time INTEGER NOT NULL, request TEXT NOT NULL, request_size INTEGER NOT NULL, response TEXT NOT NULL, response_size INTEGER NOT NULL);
74
+ + db, err := sqlitedb.Open(file, `CREATE TABLE bodies (seq INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE, time INTEGER NOT NULL, request TEXT NOT NULL, request_size INTEGER NOT NULL, response TEXT NOT NULL, response_size INTEGER NOT NULL);
79
75
  +INSERT INTO bodies (id,time,request,request_size,response,response_size) VALUES ('old',1234,'旧请求',100,'reply',200);`)
80
76
  + if err != nil {
81
77
  + t.Fatal(err)
@@ -113,12 +109,12 @@ index 0000000..2761c06
113
109
  +}
114
110
  +
115
111
  +func TestLegacyBodiesMigrationRollback(t *testing.T) {
116
- + db, err := sqlitedb.Open(filepath.Join(t.TempDir(), "requests.db"))
112
+ + db, err := sqlitedb.Open(filepath.Join(t.TempDir(), "requests.db"), schema)
117
113
  + if err != nil {
118
114
  + t.Fatal(err)
119
115
  + }
120
116
  + defer db.Close()
121
- + if _, err := db.Exec(schema + `CREATE TABLE bodies (seq INTEGER, id TEXT, time INTEGER, request TEXT, request_size INTEGER, response TEXT, response_size INTEGER);
117
+ + if _, err := db.Exec(`CREATE TABLE bodies (seq INTEGER, id TEXT, time INTEGER, request TEXT, request_size INTEGER, response TEXT, response_size INTEGER);
122
118
  +INSERT INTO bodies VALUES (1,'good',1234,'request',1,'reply',1),(2,'bad',1234,NULL,1,'reply',1);`); err != nil {
123
119
  + t.Fatal(err)
124
120
  + }
@@ -137,24 +133,58 @@ index 0000000..2761c06
137
133
  + }
138
134
  +}
139
135
  diff --git a/internal/requestdb/requestdb.go b/internal/requestdb/requestdb.go
140
- index bff3477..ae92661 100644
136
+ index 0854684..c83be9a 100644
141
137
  --- a/internal/requestdb/requestdb.go
142
138
  +++ b/internal/requestdb/requestdb.go
143
- @@ -58,7 +58,6 @@ CREATE INDEX IF NOT EXISTS requests_account_time ON requests(account, time);
144
- CREATE INDEX IF NOT EXISTS requests_state_time ON requests(state, time);
145
- CREATE TABLE IF NOT EXISTS hours (` + rollupColumns + `) WITHOUT ROWID;
146
- CREATE TABLE IF NOT EXISTS days (` + rollupColumns + `) WITHOUT ROWID;
147
- -DROP TABLE IF EXISTS bodies;
148
- CREATE TABLE IF NOT EXISTS request_bodies (
149
- seq INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE, time INTEGER NOT NULL,
150
- request BLOB NOT NULL, request_size INTEGER NOT NULL, response BLOB NOT NULL, response_size INTEGER NOT NULL);`
151
- @@ -121,6 +120,9 @@ func Open(path string, log Logger) (*Store, error) {
139
+ @@ -112,7 +112,7 @@ func Open(path string, log Logger) (*Store, error) {
140
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
141
+ return nil, err
142
+ }
143
+ - db, err := sqlitedb.Open(path, schema)
144
+ + db, err := sqlitedb.Open(path, schema, migrateLegacyBodies)
145
+ if err != nil {
146
+ return nil, fmt.Errorf("初始化请求账本: %w", err)
147
+ }
148
+ diff --git a/internal/sqlitedb/sqlitedb.go b/internal/sqlitedb/sqlitedb.go
149
+ index d65e9fb..ec78bf3 100644
150
+ --- a/internal/sqlitedb/sqlitedb.go
151
+ +++ b/internal/sqlitedb/sqlitedb.go
152
+ @@ -33,7 +33,7 @@ func init() {
153
+ }
154
+
155
+ // Open 打开或创建数据库,为已有表补上缺少的列、执行 schema 并删除 schema 没有定义的对象,非增量收缩的已有库先整理为增量收缩,最后交还空闲页
156
+ -func Open(path string, schema string) (*sql.DB, error) {
157
+ +func Open(path string, schema string, migrations ...func(*sql.DB) error) (*sql.DB, error) {
158
+ db, err := sql.Open(driverName, "file:"+filepath.ToSlash(path))
159
+ if err != nil {
160
+ return nil, err
161
+ @@ -47,7 +47,7 @@ func Open(path string, schema string) (*sql.DB, error) {
162
+ return nil, errors.Join(fmt.Errorf("整理数据库: %w", err), db.Close())
163
+ }
164
+ }
165
+ - if err := applySchema(db, schema); err != nil {
166
+ + if err := applySchema(db, schema, migrations...); err != nil {
167
+ return nil, errors.Join(fmt.Errorf("同步表结构: %w", err), db.Close())
168
+ }
169
+ if err := Reclaim(context.Background(), db); err != nil {
170
+ @@ -57,7 +57,7 @@ func Open(path string, schema string) (*sql.DB, error) {
171
+ }
172
+
173
+ // applySchema 为已有表补上 schema 定义而缺少的列,执行 schema,再删除 schema 没有定义的表、索引、视图与触发器
174
+ -func applySchema(db *sql.DB, schema string) error {
175
+ +func applySchema(db *sql.DB, schema string, migrations ...func(*sql.DB) error) error {
176
+ defined, err := sql.Open(driverName, ":memory:")
177
+ if err != nil {
178
+ return err
179
+ @@ -84,6 +84,11 @@ func applySchema(db *sql.DB, schema string) error {
152
180
  if _, err := db.Exec(schema); err != nil {
153
- return nil, errors.Join(fmt.Errorf("初始化请求账本: %w", err), db.Close())
181
+ return err
154
182
  }
155
- + if err := migrateLegacyBodies(db); err != nil {
156
- + return nil, errors.Join(fmt.Errorf("迁移请求正文: %w", err), db.Close())
183
+ + for _, migrate := range migrations {
184
+ + if err := migrate(db); err != nil {
185
+ + return err
186
+ + }
157
187
  + }
158
- if err := addCachedTokens(db); err != nil {
159
- return nil, errors.Join(fmt.Errorf("初始化请求账本: %w", err), db.Close())
160
- }
188
+ for name, kind := range have {
189
+ if _, ok := want[name]; !ok {
190
+ if _, err := db.Exec(`DROP ` + strings.ToUpper(kind) + ` IF EXISTS ` + quoteIdentifier(name)); err != nil {
@@ -1,8 +1,8 @@
1
1
  diff --git a/internal/config/config.go b/internal/config/config.go
2
- index de280dd..2843043 100644
2
+ index d65e9fb..2843043 100644
3
3
  --- a/internal/config/config.go
4
4
  +++ b/internal/config/config.go
5
- @@ -67,6 +67,11 @@ var configKeys = [...]string{
5
+ @@ -68,6 +68,11 @@ var configKeys = [...]string{
6
6
  "CONTINUATION_PROMPT",
7
7
  }
8
8
 
@@ -14,7 +14,7 @@ index de280dd..2843043 100644
14
14
  // LogFileMB 是单个日志文件的轮转大小
15
15
  const LogFileMB = 10
16
16
 
17
- @@ -412,7 +417,7 @@ func Load(path string) (Config, error) {
17
+ @@ -424,7 +429,7 @@ func Load(path string) (Config, error) {
18
18
  if err != nil {
19
19
  return Config{}, err
20
20
  }
@@ -23,10 +23,19 @@ index de280dd..2843043 100644
23
23
  if err := writeEnv(path, values); err != nil {
24
24
  return Config{}, err
25
25
  }
26
- @@ -576,8 +581,8 @@ func (c Config) Save(path string) error {
27
- if inFile {
28
- file[key] = saved[key]
26
+ @@ -443,7 +448,7 @@ func Load(path string) (Config, error) {
29
27
  }
28
+ }
29
+ }
30
+ - if _, ok := values["WAA_BACKEND"]; !ok {
31
+ + if _, ok := values["WAA_BACKEND"]; managedBy == "" && !ok {
32
+ if camoufoxOnlyAccounts(cfg.AuthStates) {
33
+ cfg.WAABackend = WAABackendCamoufox
34
+ }
35
+ @@ -612,8 +617,8 @@ func (c Config) Save(path string) error {
36
+ }
37
+ case key == "WAA_BACKEND":
38
+ file[key] = values[key]
30
39
  - case slices.Contains(workerDefaultKeys, key) && !inFile && values[key] == previousDefaults[key]:
31
40
  - case values[key] != defaults[key]:
32
41
  + case managedBy == "" && slices.Contains(workerDefaultKeys, key) && !inFile && values[key] == previousDefaults[key]:
@@ -110,4 +119,3 @@ index 0000000..c5d9991
110
119
  + t.Fatal("managed settings changed across save/load")
111
120
  + }
112
121
  +}
113
-
@@ -9,7 +9,7 @@ import { installBackend, startBackend, getDataDir } from '../lib/native.mjs';
9
9
  const root = fileURLToPath(new URL('..', import.meta.url));
10
10
  const source = path.join(root, 'backend-source');
11
11
  const build = path.join(root, 'backend-build');
12
- const pinned = 'e97cfccc7e68e356ac504ba220e0b004382029a8';
12
+ const pinned = 'c8a4ac25768b668fa94d293fcb97059c4419b602';
13
13
 
14
14
  const command = process.argv[2];
15
15
 
@@ -41,7 +41,7 @@ async function main() {
41
41
  const sha256 = crypto.createHash('sha256').update(bytes).digest('hex');
42
42
 
43
43
  const manifest = {
44
- version: '4.27.0-argon.1',
44
+ version: '4.27.1-argon.1',
45
45
  sourceCommit: pinned,
46
46
  sha256,
47
47
  platform: 'darwin',
@@ -47,11 +47,12 @@ try {
47
47
  assert.equal(config.waa_backend, 'spidermonkey');
48
48
  assert.equal(config.search_model_aliases, false);
49
49
  assert.equal(config.strip_thought_signatures, false);
50
+ assert.equal(config.hide_dock_icon, false);
50
51
  assert.deepEqual(await studioAdmin(local, 'GET', '/api/accounts/tasks'), { tasks: [] });
51
52
  // Save upstream defaults that differ from this plugin's defaults. They must stay explicit
52
53
  // through the backend's Save and the plugin's next config sync, not get silently re-seeded.
53
54
  const choices = {
54
- search_model_aliases: true, strip_thought_signatures: true, routing_strategy: 'round-robin', per_account_concurrency: 2,
55
+ search_model_aliases: true, strip_thought_signatures: true, hide_dock_icon: true, routing_strategy: 'round-robin', per_account_concurrency: 2,
55
56
  first_event_hedge: 'auto', first_event_hedge_models: 'gemini-3.8-flash',
56
57
  api_key_accounts: `${local.apiKey}:smoke@example.com`
57
58
  };
@@ -105,7 +106,7 @@ try {
105
106
  assert.equal(restarted.request_timeout, '5m0s');
106
107
  assert.equal(restarted.waa_backend, 'spidermonkey');
107
108
  assert.deepEqual(await stopStudio({ dataDir }), { stopped: true });
108
- console.log(`PASS ${key}: ${STUDIO_VERSION} (${source}), legacy body migration/restart, saved defaults/hedge/models/key bindings across restart, locked plugin settings, SpiderMonkey WAA, managed status, update HTTP 409, unchanged executable/PID, managed UI, clean stop`);
109
+ console.log(`PASS ${key}: ${STUDIO_VERSION} (${source}), legacy body migration/restart, saved defaults/Dock visibility/hedge/models/key bindings across restart, locked plugin settings, SpiderMonkey WAA, managed status, update HTTP 409, unchanged executable/PID, managed UI, clean stop`);
109
110
  } finally {
110
111
  let stopped = !local;
111
112
  if (local) stopped = (await stopStudio({ dataDir }).catch(() => ({ stopped: false }))).stopped;
@@ -133,6 +133,15 @@ function spidermonkey(key) {
133
133
  return fetchPinned('SpiderMonkey', `https://github.com/${pin.repository}/releases/download/${pin.release}/${archive.asset}`,
134
134
  archive, archive.asset.replace(/\.tar\.gz$/, linux ? '-without-env-interposers' : ''), null, linux ? dropEnvInterposers : null);
135
135
  }
136
+ // libwebp returns the folder with a target's pinned libwebp headers (include) and static
137
+ // libraries (lib), the ones upstream builds its own releases with.
138
+ function libwebp(key) {
139
+ const pin = toolchains.libwebp;
140
+ const archive = pin.targets[key];
141
+ if (!archive) throw new Error(`No pinned libwebp for ${key}`);
142
+ return fetchPinned('libwebp', `https://github.com/${pin.repository}/releases/download/${pin.release}/${archive.asset}`,
143
+ archive, archive.asset.replace(/\.tar\.gz$/, ''), null, null);
144
+ }
136
145
  function xcode() {
137
146
  try {
138
147
  const [version, build] = execFileSync('xcodebuild', ['-version'], { encoding: 'utf8' }).trim().split('\n');
@@ -143,15 +152,16 @@ function xcode() {
143
152
  }
144
153
  // cToolchain returns the C and C++ settings for a target and what they were made with. Only the
145
154
  // targets being built and pinned must use the pinned toolchain; the host's is also used for upstream
146
- // tests. SpiderMonkey's headers and libraries are found through CGO_CXXFLAGS and CGO_LDFLAGS, as
147
- // upstream's own build finds them (.github/actions/spidermonkey); its mozjs package names the libraries.
155
+ // tests. SpiderMonkey's and libwebp's headers and libraries are found through CGO_CPPFLAGS and
156
+ // CGO_LDFLAGS, as upstream's own build finds them (.github/actions/static-libraries).
148
157
  function cToolchain(key, { pin = false } = {}) {
149
158
  const check = pin ? pinned : () => {};
150
159
  const [goos, goarch] = platforms[key];
151
160
  const mozjs = spidermonkey(key);
161
+ const webp = libwebp(key);
152
162
  // Quoted whole, as cgo splits these flags, so a folder with spaces stays one argument.
153
- const include = `"-I${path.join(mozjs, 'include')}"`;
154
- const lib = `"-L${path.join(mozjs, 'lib')}"`;
163
+ const include = `"-I${path.join(mozjs, 'include')}" "-I${path.join(webp, 'include')}"`;
164
+ const lib = `"-L${path.join(mozjs, 'lib')}" "-L${path.join(webp, 'lib')}"`;
155
165
  if (goos === 'darwin') {
156
166
  if (process.platform !== 'darwin') throw new Error('Build macOS archives on macOS with Xcode');
157
167
  const expected = `${toolchains.xcode.version} (${toolchains.xcode.build})`;
@@ -162,7 +172,7 @@ function cToolchain(key, { pin = false } = {}) {
162
172
  clang: execFileSync('clang', ['--version'], { encoding: 'utf8' }).split('\n')[0] };
163
173
  const flags = '-O2 -g -mmacosx-version-min=13.0';
164
174
  return { compiler, env: { CC: 'clang', CXX: 'clang++', MACOSX_DEPLOYMENT_TARGET: '13.0',
165
- CGO_CFLAGS: flags, CGO_CXXFLAGS: `${flags} ${include}`, CGO_LDFLAGS: `${flags} ${lib}` } };
175
+ CGO_CPPFLAGS: include, CGO_CFLAGS: flags, CGO_CXXFLAGS: flags, CGO_LDFLAGS: `${flags} ${lib}` } };
166
176
  }
167
177
  if (goos === 'windows') {
168
178
  const { cc, cxx, libs } = llvmMingw();
@@ -172,7 +182,7 @@ function cToolchain(key, { pin = false } = {}) {
172
182
  check('Windows C compiler', clang, toolchains.llvmMingw.clang,
173
183
  `Unset STUDIO_MINGW_CC to use the pinned llvm-mingw ${toolchains.llvmMingw.version}.`);
174
184
  return { compiler: { llvmMingw: toolchains.llvmMingw.version, clang },
175
- env: { CC: `"${cc}"`, CXX: `"${cxx}"`, CGO_CFLAGS: '-O2 -g', CGO_CXXFLAGS: `-O2 -g ${include}`,
185
+ env: { CC: `"${cc}"`, CXX: `"${cxx}"`, CGO_CPPFLAGS: include, CGO_CFLAGS: '-O2 -g', CGO_CXXFLAGS: '-O2 -g',
176
186
  CGO_LDFLAGS: `-O2 -g "-L${libs}" ${lib}` } };
177
187
  }
178
188
  const version = execFileSync(zig, ['version'], { encoding: 'utf8' }).trim();
@@ -180,7 +190,7 @@ function cToolchain(key, { pin = false } = {}) {
180
190
  const triple = `${goarch === 'amd64' ? 'x86_64' : 'aarch64'}-linux-musl`;
181
191
  return { compiler: { zig: version, target: triple },
182
192
  env: { CC: `"${zig}" cc -target ${triple}`, CXX: `"${zig}" c++ -target ${triple}`,
183
- CGO_CFLAGS: '-O2 -g', CGO_CXXFLAGS: `-O2 -g ${include}`, CGO_LDFLAGS: `-O2 -g ${lib}` } };
193
+ CGO_CPPFLAGS: include, CGO_CFLAGS: '-O2 -g', CGO_CXXFLAGS: '-O2 -g', CGO_LDFLAGS: `-O2 -g ${lib}` } };
184
194
  }
185
195
  const go = (argv, extra = {}) => execFileSync('go', argv, { cwd: source, env: { ...env, ...extra }, stdio: 'inherit' });
186
196
  const npm = argv => execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', argv,