magpie-gemini-web 0.5.2 → 0.5.4
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 +10 -5
- package/README.md +10 -5
- package/docs/studio-release.md +12 -12
- package/lib/studio-native.mjs +285 -43
- package/lib/studio-release.json +19 -19
- package/lib/studio.mjs +72 -18
- package/package.json +1 -1
- package/scripts/studio-managed-smoke.mjs +7 -5
- package/scripts/studio-release.mjs +6 -6
- package/scripts/studio.mjs +17 -26
- package/patches/studio-managed.patch +0 -625
package/README.en.md
CHANGED
|
@@ -17,7 +17,7 @@ The plugin provides two independent providers that can be used individually or t
|
|
|
17
17
|
| **Thinking levels** | `minimal` / `low` / `medium` / `high` (`minimal` is fastest & cheapest) | `low` (base) / `high` (`-thinking` extended thinking) |
|
|
18
18
|
| **Sign-in method** | Sign in directly in an isolated popup browser window | Copy `cookie` from a private/incognito window |
|
|
19
19
|
| **Usage cards** | Shows models currently on cooldown and recovery times | Syncs **5-hour** & **weekly** usage and reset times from the web |
|
|
20
|
-
| **Local backend** | AIStudio2API (`v0.2.
|
|
20
|
+
| **Local backend** | AIStudio2API (`v0.2.14`) | gemini-web2api-go (`4.22.0-magpie.1` optimized build) |
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
@@ -82,12 +82,17 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
|
|
|
82
82
|
| `stripCitations` | `true` | `gemini-web` only | Strip `[cite: 1]` and `[cite_start]` markers from replies and tool arguments (not supported on AI Studio) |
|
|
83
83
|
| `baseURL` | `http://127.0.0.1:8083/v1` | `gemini-web` only | Default Gemini Web backend URL |
|
|
84
84
|
| `contextLimit` / `outputLimit` | `1000000` / `8192` | `gemini-web` only | Context and output limits reported to clients |
|
|
85
|
-
| `studio.proxy` | none | `ai-studio` only | Proxy for reaching Google (`http` / `https` / `socks5`), or set `AI_STUDIO_PROXY` env var |
|
|
85
|
+
| `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 |
|
|
86
86
|
| `studio.baseURL` | `http://127.0.0.1:2048/v1` | `ai-studio` only | Default URL when connecting an existing AIStudio2API instance |
|
|
87
87
|
| `studio.contextLimit` / `studio.outputLimit` | Model catalog defaults | `ai-studio` only | Limits reported to clients (can only lower the catalog defaults) |
|
|
88
88
|
|
|
89
89
|
**Using a proxy to reach Google:**
|
|
90
|
-
- **AI Studio:**
|
|
90
|
+
- **AI Studio:** Preferably set it in the local admin page. Open the address on the usage card's "管理页" (admin page) line (default `http://127.0.0.1:2048`) and sign in as `magpie` with `adminPassword` from `config.json` in the data directory. Save the proxy, then restart the service from the admin page. The plugin keeps the saved value when it restarts the backend.
|
|
91
|
+
- Alternatively, `studio.proxy` or `AI_STUDIO_PROXY` take precedence over the saved proxy. A change applies after the local service restarts (`npm run studio:stop`; the next request starts it again).
|
|
92
|
+
- With neither set, the backend falls back to the `HTTPS_PROXY` environment variable or the system proxy (Windows proxy settings).
|
|
93
|
+
- **The provider proxy set for `ai-studio` in Magpie is not passed to the local backend**; use one of the options above.
|
|
94
|
+
- The usage card's "代理出口" (proxy) line, the Google sign-in instructions and failure messages, and `npm run studio:status` show the proxy the backend actually uses, where it comes from, and whether a new setting still needs a restart.
|
|
95
|
+
- The listen address, API key and admin credentials are managed by the plugin; changing them in the admin page has no effect.
|
|
91
96
|
- **Gemini Web:** The backend ignores `HTTP_PROXY`. Add your proxy to the proxy pool in the local admin panel at `http://127.0.0.1:8083/admin` (password is `adminToken` in `secrets.json` inside the data directory).
|
|
92
97
|
- *Note:* When using a proxy pool, send one request with a newly added account so the backend binds it to a proxy, then re-add it via "Add an existing Google account from the local backend" to load its email and usage (SOCKS proxies are not supported for usage reads).
|
|
93
98
|
|
|
@@ -130,8 +135,8 @@ When a plugin error asks you to run a command, it includes the full ready-to-cop
|
|
|
130
135
|
- Concurrency is capped at 1, but Google account rate-limiting or flagging cannot be completely ruled out—use at your own risk.
|
|
131
136
|
|
|
132
137
|
**AI Studio upgrades and model selection:**
|
|
133
|
-
- The plugin pins SHA-256-verified `v0.2.
|
|
134
|
-
- **Online updates are disabled in the managed backend on macOS, Linux, and Windows.** The admin page
|
|
138
|
+
- The plugin pins SHA-256-verified `v0.2.14-magpie.1`, based on upstream `v0.2.14`. 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.
|
|
139
|
+
- **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.
|
|
135
140
|
- 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.
|
|
136
141
|
- If a newer or custom version is installed, the plugin reports a conflict with `stop`, `install`, and `start` commands carrying the correct `--data-dir`. Stop can recover a changed listener PID and verifies its executable path before signaling it. If HTTP is down but the process is still alive, the plugin refuses to reinstall or launch a second process.
|
|
137
142
|
- When stopped, a replaced executable is restored from the verified pinned archive; failed verification never launches the replacement. Existing account and runtime data files are kept, but **an older backend may not read data migrated by a newer version**. Before manually downgrading, stop the service and back up the entire data directory, including `auth/` and `runtime/requests.db`. If data migration is known to have occurred, update the plugin to match the newer backend.
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
| **思考档位** | `minimal` / `low` / `medium` / `high` 四档(`minimal` 最快最省) | `low`(普通)/ `high`(`-thinking` 扩展思考)两档 |
|
|
18
18
|
| **登录方式** | 弹出独立浏览器窗口直接登录 Google | 用无痕窗口登录并复制 `cookie` |
|
|
19
19
|
| **额度卡片** | 显示冷却中的模型与恢复时间 | 同步网页版 **5 小时** 与 **每周** 用量及重置时间 |
|
|
20
|
-
| **本地后端** | AIStudio2API (`v0.2.
|
|
20
|
+
| **本地后端** | AIStudio2API (`v0.2.14`) | gemini-web2api-go (`4.22.0-magpie.1` 优化版) |
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
@@ -82,12 +82,17 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
|
|
|
82
82
|
| `stripCitations` | `true` | 仅 `gemini-web` | 剥离回复与工具参数中的 `[cite: 1]`、`[cite_start]` 标记(AI Studio 不支持剥离) |
|
|
83
83
|
| `baseURL` | `http://127.0.0.1:8083/v1` | 仅 `gemini-web` | Gemini Web 默认后端地址 |
|
|
84
84
|
| `contextLimit` / `outputLimit` | `1000000` / `8192` | 仅 `gemini-web` | 向客户端声明的上下文 / 输出上限 |
|
|
85
|
-
| `studio.proxy` | 无 | 仅 `ai-studio` | 访问 Google 的代理(`http` / `https` / `socks5`),也可用环境变量 `AI_STUDIO_PROXY
|
|
85
|
+
| `studio.proxy` | 无 | 仅 `ai-studio` | 访问 Google 的代理(`http` / `https` / `socks5`),也可用环境变量 `AI_STUDIO_PROXY`;设置后优先于管理页里保存的代理,留空等于不设置 |
|
|
86
86
|
| `studio.baseURL` | `http://127.0.0.1:2048/v1` | 仅 `ai-studio` | 连接已有 AIStudio2API 时的默认地址 |
|
|
87
87
|
| `studio.contextLimit` / `studio.outputLimit` | 模型目录默认值 | 仅 `ai-studio` | 向客户端声明的上限(仅支持在目录值基础上调低) |
|
|
88
88
|
|
|
89
89
|
**配置代理访问 Google:**
|
|
90
|
-
- **AI Studio
|
|
90
|
+
- **AI Studio**:推荐在本地管理页设置。打开额度卡片「管理页」一行里的地址(默认 `http://127.0.0.1:2048`),用户名 `magpie`,密码是数据目录 `config.json` 里的 `adminPassword`;保存代理后在管理页重启服务即可生效,之后插件重启后端也不会覆盖它。
|
|
91
|
+
- 也可以用上表的 `studio.proxy` 或环境变量 `AI_STUDIO_PROXY`,它们优先于管理页里保存的代理。改了要重启本地服务才生效(`npm run studio:stop`,下一次请求会自动启动)。
|
|
92
|
+
- 都没设置时,后端自动使用 `HTTPS_PROXY` 环境变量或系统代理(Windows 的「代理」设置)。
|
|
93
|
+
- **Magpie 里给 `ai-studio` 设置的供应商代理不会传给本地后端**,请用上面两种方式之一设置。
|
|
94
|
+
- 额度卡片的「代理出口」一行会显示后端当前实际使用的代理、它来自哪里,以及新设置是否还要重启才生效;登录 Google 时的说明、登录失败的提示和 `npm run studio:status` 也会显示。
|
|
95
|
+
- 监听地址、API Key、管理员账号等由插件管理,在管理页修改不会生效。
|
|
91
96
|
- **Gemini Web**:后端不读取 `HTTP_PROXY`,请打开本地管理面板 `http://127.0.0.1:8083/admin` 在代理池中添加(登录口令为数据目录 `secrets.json` 中的 `adminToken`)。
|
|
92
97
|
- *注意*:配置代理池后,新添加的账号需先发送一次请求以绑定代理,再通过「添加本地后端里已有的 Google 账号」重新添加,才能正常读取邮箱和用量(不支持通过 SOCKS 代理读取用量)。
|
|
93
98
|
|
|
@@ -130,8 +135,8 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
|
|
|
130
135
|
- 插件将请求并发限制为 1,但仍无法完全排除 Google 账号风控风险,请自行评估使用。
|
|
131
136
|
|
|
132
137
|
**AI Studio 升级与模型范围:**
|
|
133
|
-
- 插件固定使用经过 SHA-256 校验的 `v0.2.
|
|
134
|
-
- **托管后端已在 macOS、Linux、Windows
|
|
138
|
+
- 插件固定使用经过 SHA-256 校验的 `v0.2.14-magpie.1`(基于上游 `v0.2.14`),通过升级插件更新后端。同基线或更旧的官方版、以及旧版插件装的托管后端,在下次管理启动时自动升级,保留账号、端口与登录凭据。
|
|
139
|
+
- **托管后端已在 macOS、Linux、Windows 统一禁用在线更新**:管理页提示“更新由 magpie-gemini-web 管理”,更新接口返回 HTTP 409,检查更新也不会访问 GitHub。用户自行连接的外部服务不受影响。托管构建方式和可复现构建流程见 [docs/studio-release.md](docs/studio-release.md)。
|
|
135
140
|
- Unix 的 `package/` 仍尽力设置为 `0555`,作为额外的文件保护;权限设置失败不阻断服务,在线更新的禁用也不依赖该权限。若要手动删除数据目录来重置,请先停止服务,再运行 `chmod -R u+w "<数据目录>/package"` 恢复可写权限,然后删除;将 `<数据目录>` 替换为实际路径。
|
|
136
141
|
- 若自行更新到更新版或定制版,插件会提示版本冲突,并提供带正确 `--data-dir` 的 `stop`、`install`、`start` 命令。停止时会找回更新后的监听进程 PID,并核对可执行文件路径;无法核实身份就不会发送终止信号。HTTP 端口关闭但进程尚未退出时,插件不会重装或启动第二个进程。
|
|
137
142
|
- 已停止但文件被替换的后端会从校验通过的固定版本安装包恢复;校验失败不会启动替换文件。安装会保留账号和运行数据文件,但**不保证旧版本能读取新版迁移过的数据**。手动降级前先停止服务并备份整个数据目录(包括 `auth/`、`runtime/requests.db` 等);已知发生数据迁移时应升级插件以匹配新版后端。
|
package/docs/studio-release.md
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
# AI Studio 托管后端构建
|
|
2
2
|
|
|
3
|
-
插件使用 `0.2.
|
|
3
|
+
插件使用 `0.2.14-magpie.1`,基于 AIStudio2API `v0.2.14` 的固定 commit。上游版本、commit、Go 工具链、安装包及二进制 SHA-256 均记录在 `lib/studio-release.json`。MIT 许可见 `LICENSE-AISTUDIO`。
|
|
4
4
|
|
|
5
5
|
## 在线更新策略
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
上游从 v0.2.14 起支持由分发方管理更新,不再需要补丁。构建时通过 `-ldflags` 写入两个值:
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
- `POST /api/update` 返回 HTTP
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- 普通插件升级、账户登录、模型调用及管理功能继续保留。用户自行连接的外部 AIStudio2API
|
|
9
|
+
- `internal/app.version=v0.2.14-magpie.1`:明确的版本标识。插件把同基线或更旧的官方版、以及更旧的托管版(`-magpie.N` 的基线更低,或同基线但 N 更小)视为需要升级;更新的官方版、更新的托管版或不认识的定制版仍提示冲突。
|
|
10
|
+
- `internal/app.managedBy=magpie-gemini-web`:更新模式为 `managed`。`GET /api/update`(包括 `?refresh=1`)只返回本地版本、`mode: managed` 和 `managed_by`,不访问 GitHub;`POST /api/update` 返回 HTTP 409,说明更新由 magpie-gemini-web 管理,不下载、不替换文件、不重启。这两个值在链接时写死,没有环境变量或管理页开关,在所有构建平台相同。
|
|
11
|
+
- 管理页版本面板显示当前版本和"更新由 magpie-gemini-web 管理",不显示检查更新或安装按钮。上游的在线更新代码仍然编译在内,但托管模式下走不到;`studio:managed:smoke` 用真实二进制验证这一点。
|
|
12
|
+
- 构建不带 `desktop` 标签,即上游的命令行版,不包含桌面窗口。
|
|
13
|
+
- 普通插件升级、账户登录、模型调用及管理功能继续保留。用户自行连接的外部 AIStudio2API 服务不受影响。
|
|
14
14
|
|
|
15
15
|
## 复现构建
|
|
16
16
|
|
|
17
17
|
维护者需要 Git、tar、Node.js 26.8.2(构建页面用)、Go 1.27.1。Windows 使用 Git Bash 执行以下命令,安装包运行和解压使用 Windows 自带 PowerShell。最终用户不需要这些构建工具。
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
#
|
|
20
|
+
# 按固定源代码构建所有五个平台,并核对现有 pin。
|
|
21
21
|
npm run studio:release:build
|
|
22
22
|
|
|
23
23
|
# 仅构建当前平台。
|
|
24
24
|
node scripts/studio-release.mjs --target=host --verify
|
|
25
25
|
|
|
26
|
-
#
|
|
26
|
+
# 制作新版本:审查上游源码后重新生成所有平台 pin。
|
|
27
27
|
node scripts/studio-release.mjs --write-pins
|
|
28
28
|
|
|
29
29
|
# 可复用本地上游 git 仓库,但始终导出固定 commit,不使用工作区修改。
|
|
@@ -42,8 +42,8 @@ npm run studio:managed:smoke
|
|
|
42
42
|
AI_STUDIO_TEST_ARCHIVE=/absolute/path/to/archive.tar.gz npm test
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
真实安装验证检查启动、版本、GET 更新状态、POST 更新返回
|
|
45
|
+
真实安装验证检查启动、版本、GET 更新状态、POST 更新返回 409、二进制及 PID 不变、管理页面显示托管提示、停止与状态。`.github/workflows/studio.yml` 在五种系统/架构上重新构建、对比 SHA-256 并运行该验证;本机交叉编译不代替 Windows 真机验证。
|
|
46
46
|
|
|
47
|
-
发布时先审查构建记录和各平台 CI 结果,再将安装包及许可证上传到本仓库 `studio-v0.2.
|
|
47
|
+
发布时先审查构建记录和各平台 CI 结果,再将安装包及许可证上传到本仓库 `studio-v0.2.14-magpie.1` release。该 release 可公开下载后,才能发布引用它的插件版本,否则新安装会遇到 404。构建脚本及 CI 不自动发布 release,也不发布 npm 包。
|
|
48
48
|
|
|
49
|
-
本地试用无需发布:先停止当前托管后端,再执行 `node scripts/studio.mjs install --archive /absolute/path/to/archive` 和 `node scripts/studio.mjs start`。可用 `--data-dir`
|
|
49
|
+
本地试用无需发布:先停止当前托管后端,再执行 `node scripts/studio.mjs install --archive /absolute/path/to/archive` 和 `node scripts/studio.mjs start`。可用 `--data-dir` 指定目录。修改前备份账号和配置;该版本与上游 v0.2.14 使用相同的数据格式。v0.2.14 对数据的改动都是新增的(账号记录的可选字段、新的 `runtime/prompts.db`、`.env` 的新设置),用真实账号验证过:`v0.2.13-magpie.1` 的数据可以直接被它读取,用它运行过之后退回 `v0.2.13-magpie.1` 也能正常使用。
|
package/lib/studio-native.mjs
CHANGED
|
@@ -109,12 +109,36 @@ export function validateProxy(proxy) {
|
|
|
109
109
|
return trimmed;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
112
|
+
// pluginProxy is the proxy this plugin sets for the service over the one saved in its .env: the
|
|
113
|
+
// one given (studio.proxy, or a CLI --proxy), else AI_STUDIO_PROXY, with where it comes from. An
|
|
114
|
+
// empty one names none, so the saved one applies; null when neither names one.
|
|
115
|
+
export function pluginProxy(explicit) {
|
|
116
|
+
const given = validateProxy(explicit);
|
|
117
|
+
if (given) return { proxy: given, source: 'option' };
|
|
118
|
+
const env = validateProxy(process.env.AI_STUDIO_PROXY);
|
|
119
|
+
if (env) return { proxy: env, source: 'env' };
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// downloadProxy is the proxy an install downloads through: the one given (an empty one turns it
|
|
124
|
+
// off), else AI_STUDIO_PROXY, else the service's saved one, else the one an older config.json kept.
|
|
125
|
+
function downloadProxy(dataDir, explicit, savedConfig) {
|
|
115
126
|
if (explicit !== undefined) return validateProxy(explicit);
|
|
116
127
|
if (process.env.AI_STUDIO_PROXY !== undefined) return validateProxy(process.env.AI_STUDIO_PROXY);
|
|
117
|
-
|
|
128
|
+
let saved;
|
|
129
|
+
try {
|
|
130
|
+
saved = readStudioEnv(dataDir).PROXY;
|
|
131
|
+
} catch {}
|
|
132
|
+
return saved !== undefined ? firstProxy(saved) : validateProxy(savedConfig?.proxy);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// firstProxy is the first of a PROXY list (upstream takes several, comma-separated, as a pool).
|
|
136
|
+
function firstProxy(value) {
|
|
137
|
+
return proxyList(value)[0] ?? '';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function proxyList(value) {
|
|
141
|
+
return [...new Set(String(value ?? '').split(',').map(p => p.trim()).filter(Boolean))];
|
|
118
142
|
}
|
|
119
143
|
|
|
120
144
|
export function validateConfig(config) {
|
|
@@ -148,9 +172,34 @@ export function validateConfig(config) {
|
|
|
148
172
|
};
|
|
149
173
|
}
|
|
150
174
|
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
175
|
+
// ENV_KEYS are the settings upstream reads from .env, in the order it writes them (config.configKeys).
|
|
176
|
+
const ENV_KEYS = [
|
|
177
|
+
'AISTUDIO_AUTH_STATES', 'LISTEN_ADDR', 'PROXY_API_KEY', 'PROXY', 'INIT_TIMEOUT', 'REQUEST_TIMEOUT',
|
|
178
|
+
'WARM_WORKER_LIMIT', 'MAX_ACTIVE_WORKERS', 'WARM_STARTUP_CONCURRENCY', 'PER_ACCOUNT_CONCURRENCY',
|
|
179
|
+
'ROUTING_STRATEGY', 'UPSTREAM_CHANNELS', 'MODEL_MAPPING', 'TEMPORARY_CHAT', 'WAA_BACKEND', 'AUTO_START',
|
|
180
|
+
'REQUEST_BODY_LOG', 'LOG_FILE', 'LOG_MAX_TOTAL_MB', 'ADMIN_AUTH_ENABLED', 'ADMIN_USERNAME', 'ADMIN_PASSWORD',
|
|
181
|
+
'BUILD_NATIVE_NONSTREAM', 'MODEL_CHANNELS', 'MEDIA_OUTPUT', 'MEDIA_BASE_URL'
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
// SETTING_DEFAULTS are the settings the admin page owns, as a new setup has them. They are written
|
|
185
|
+
// to .env when it lacks them and left as the admin page saves them after that.
|
|
186
|
+
const SETTING_DEFAULTS = {
|
|
187
|
+
REQUEST_BODY_LOG: 'false',
|
|
188
|
+
LOG_FILE: 'false',
|
|
189
|
+
TEMPORARY_CHAT: 'true',
|
|
190
|
+
PER_ACCOUNT_CONCURRENCY: '1',
|
|
191
|
+
WARM_WORKER_LIMIT: '1',
|
|
192
|
+
MAX_ACTIVE_WORKERS: '1',
|
|
193
|
+
WARM_STARTUP_CONCURRENCY: '1',
|
|
194
|
+
ROUTING_STRATEGY: 'fill-first',
|
|
195
|
+
REQUEST_TIMEOUT: '30m',
|
|
196
|
+
INIT_TIMEOUT: '2m'
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// pluginEnv is the settings this plugin depends on to reach, trust and drive the service. The
|
|
200
|
+
// service is started with them in its environment, which upstream reads over .env, so saving
|
|
201
|
+
// others in the admin page can't break them.
|
|
202
|
+
function pluginEnv(dataDir, config) {
|
|
154
203
|
const authDir = path.resolve(dataDir, 'auth');
|
|
155
204
|
if (/[\r\n]/.test(authDir) || /[\r\n]/.test(config.apiKey) || /[\r\n]/.test(config.adminUsername) || /[\r\n]/.test(config.adminPassword)) {
|
|
156
205
|
throw new Error('Configuration paths or credentials must not contain newlines');
|
|
@@ -163,29 +212,103 @@ function serviceEnv(dataDir, config, options = {}) {
|
|
|
163
212
|
ADMIN_USERNAME: config.adminUsername,
|
|
164
213
|
ADMIN_PASSWORD: config.adminPassword,
|
|
165
214
|
AUTO_START: 'true',
|
|
166
|
-
REQUEST_BODY_LOG: 'false',
|
|
167
|
-
LOG_FILE: 'false',
|
|
168
215
|
WAA_BACKEND: 'go',
|
|
169
216
|
UPSTREAM_CHANNELS: 'playground',
|
|
170
|
-
BUILD_NATIVE_NONSTREAM: 'false'
|
|
171
|
-
TEMPORARY_CHAT: 'true',
|
|
172
|
-
PER_ACCOUNT_CONCURRENCY: '1',
|
|
173
|
-
WARM_WORKER_LIMIT: '1',
|
|
174
|
-
MAX_ACTIVE_WORKERS: '1',
|
|
175
|
-
WARM_STARTUP_CONCURRENCY: '1',
|
|
176
|
-
ROUTING_STRATEGY: 'fill-first',
|
|
177
|
-
REQUEST_TIMEOUT: '30m',
|
|
178
|
-
INIT_TIMEOUT: '2m',
|
|
179
|
-
PROXY: effectiveProxy(options.proxy, config.proxy)
|
|
217
|
+
BUILD_NATIVE_NONSTREAM: 'false'
|
|
180
218
|
};
|
|
181
219
|
}
|
|
182
220
|
|
|
183
|
-
|
|
184
|
-
|
|
221
|
+
// serviceEnv is the environment the service is started with: this plugin's settings, and PROXY
|
|
222
|
+
// only when the plugin sets one (see pluginProxy). Without it the service takes the PROXY saved
|
|
223
|
+
// in .env, which the admin page changes.
|
|
224
|
+
export function serviceEnv(dataDir, config, options = {}) {
|
|
225
|
+
const env = pluginEnv(dataDir, config);
|
|
226
|
+
const owned = pluginProxy(options.proxy);
|
|
227
|
+
if (owned) env.PROXY = owned.proxy;
|
|
228
|
+
return env;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// parseEnvValue and formatEnvValue read and write a .env value as upstream does.
|
|
232
|
+
function parseEnvValue(raw, where) {
|
|
233
|
+
if (!raw) return '';
|
|
234
|
+
if (raw[0] === "'") {
|
|
235
|
+
if (raw.length < 2 || !raw.endsWith("'")) throw new Error(`${where}: unterminated single quote`);
|
|
236
|
+
return raw.slice(1, -1);
|
|
237
|
+
}
|
|
238
|
+
if (raw[0] === '"') {
|
|
239
|
+
try {
|
|
240
|
+
return JSON.parse(raw);
|
|
241
|
+
} catch {
|
|
242
|
+
throw new Error(`${where}: invalid double-quoted value`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const comment = raw.indexOf(' #');
|
|
246
|
+
return comment >= 0 ? raw.slice(0, comment).trim() : raw;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function formatEnvValue(value) {
|
|
250
|
+
return /[ \t\r\n#"']/.test(value) ? JSON.stringify(value) : value;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// readStudioEnv is the settings saved in the service's .env (none when it has no .env).
|
|
254
|
+
export function readStudioEnv(dataDir) {
|
|
255
|
+
const envPath = path.join(dataDir, '.env');
|
|
256
|
+
let text;
|
|
257
|
+
try {
|
|
258
|
+
text = fs.readFileSync(envPath, 'utf8');
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (err.code === 'ENOENT') return {};
|
|
261
|
+
throw err;
|
|
262
|
+
}
|
|
263
|
+
const values = {};
|
|
264
|
+
text.split(/\r?\n/).forEach((line, i) => {
|
|
265
|
+
line = line.trim();
|
|
266
|
+
if (!line || line.startsWith('#')) return;
|
|
267
|
+
const eq = line.indexOf('=');
|
|
268
|
+
if (eq < 0) throw new Error(`${envPath}:${i + 1}: missing "="`);
|
|
269
|
+
const key = line.slice(0, eq).trim();
|
|
270
|
+
if (ENV_KEYS.includes(key)) values[key] = parseEnvValue(line.slice(eq + 1).trim(), `${envPath}:${i + 1}`);
|
|
271
|
+
});
|
|
272
|
+
return values;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function writeStudioEnv(dataDir, values) {
|
|
276
|
+
const envPath = path.join(dataDir, '.env');
|
|
277
|
+
const content = ENV_KEYS.filter(k => values[k] !== undefined).map(k => `${k}=${formatEnvValue(String(values[k]))}\n`).join('');
|
|
278
|
+
let current;
|
|
279
|
+
try {
|
|
280
|
+
current = fs.readFileSync(envPath, 'utf8');
|
|
281
|
+
} catch {}
|
|
282
|
+
if (current !== content) {
|
|
283
|
+
const tmpEnv = `${envPath}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
284
|
+
fs.writeFileSync(tmpEnv, content, { mode: 0o600 });
|
|
285
|
+
fs.renameSync(tmpEnv, envPath);
|
|
286
|
+
}
|
|
287
|
+
fs.chmodSync(envPath, 0o600);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// syncStudioEnv brings .env up to date with config.json, keeping what the admin page saved: this
|
|
291
|
+
// plugin's settings are written over, the others only when missing. A missing PROXY is the one
|
|
292
|
+
// given, else the one an older config.json kept (which nothing reads after that).
|
|
293
|
+
function syncStudioEnv(dataDir, config, seedProxy) {
|
|
294
|
+
const values = readStudioEnv(dataDir);
|
|
295
|
+
Object.assign(values, pluginEnv(dataDir, config));
|
|
296
|
+
for (const [key, value] of Object.entries(SETTING_DEFAULTS)) values[key] ??= value;
|
|
297
|
+
values.PROXY ??= validateProxy(seedProxy) || config.proxy;
|
|
298
|
+
writeStudioEnv(dataDir, values);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// saveStudioProxy saves the service's proxy in its .env, as the admin page does. The service
|
|
302
|
+
// takes it the next time its generation service starts.
|
|
303
|
+
export function saveStudioProxy(dataDir, proxy) {
|
|
304
|
+
const values = readStudioEnv(dataDir);
|
|
305
|
+
values.PROXY = validateProxy(proxy);
|
|
306
|
+
writeStudioEnv(dataDir, values);
|
|
185
307
|
}
|
|
186
308
|
|
|
187
309
|
// ensureStudioConfig reads config.json, writing a new one if there is none: on options.port when
|
|
188
|
-
// given (see pickStudioPort), else on DEFAULT_STUDIO_PORT.
|
|
310
|
+
// given (see pickStudioPort), else on DEFAULT_STUDIO_PORT. It then brings .env up to date (see
|
|
311
|
+
// syncStudioEnv), with explicitProxy as the proxy to save if .env has none yet.
|
|
189
312
|
export function ensureStudioConfig(dataDir, explicitProxy = undefined, options = {}) {
|
|
190
313
|
fs.mkdirSync(dataDir, { recursive: true, mode: 0o700 });
|
|
191
314
|
fs.chmodSync(dataDir, 0o700);
|
|
@@ -204,29 +327,24 @@ export function ensureStudioConfig(dataDir, explicitProxy = undefined, options =
|
|
|
204
327
|
fs.chmodSync(configPath, 0o600);
|
|
205
328
|
} catch (err) {
|
|
206
329
|
if (err.code === 'ENOENT') {
|
|
207
|
-
|
|
330
|
+
const created = {
|
|
208
331
|
version: 1,
|
|
209
332
|
port: options.port ?? DEFAULT_STUDIO_PORT,
|
|
210
333
|
apiKey: 'as_' + crypto.randomBytes(32).toString('hex'),
|
|
211
334
|
adminUsername: 'magpie',
|
|
212
|
-
adminPassword: crypto.randomBytes(32).toString('hex')
|
|
213
|
-
proxy: validateProxy(explicitProxy ?? '')
|
|
335
|
+
adminPassword: crypto.randomBytes(32).toString('hex')
|
|
214
336
|
};
|
|
215
337
|
const tmp = `${configPath}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
216
|
-
fs.writeFileSync(tmp, JSON.stringify(
|
|
338
|
+
fs.writeFileSync(tmp, JSON.stringify(created, null, 2) + '\n', { mode: 0o600 });
|
|
217
339
|
fs.renameSync(tmp, configPath);
|
|
218
340
|
fs.chmodSync(configPath, 0o600);
|
|
341
|
+
config = validateConfig(created);
|
|
219
342
|
} else {
|
|
220
343
|
throw err;
|
|
221
344
|
}
|
|
222
345
|
}
|
|
223
346
|
|
|
224
|
-
|
|
225
|
-
const envContent = generateEnv(dataDir, config, { proxy: explicitProxy });
|
|
226
|
-
const tmpEnv = `${envPath}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
227
|
-
fs.writeFileSync(tmpEnv, envContent, { mode: 0o600 });
|
|
228
|
-
fs.renameSync(tmpEnv, envPath);
|
|
229
|
-
fs.chmodSync(envPath, 0o600);
|
|
347
|
+
syncStudioEnv(dataDir, config, explicitProxy);
|
|
230
348
|
|
|
231
349
|
const authDir = path.join(dataDir, 'auth');
|
|
232
350
|
fs.mkdirSync(authDir, { recursive: true, mode: 0o700 });
|
|
@@ -425,6 +543,114 @@ export async function studioAccounts(options = {}) {
|
|
|
425
543
|
return accounts.filter(a => a.enabled !== false);
|
|
426
544
|
}
|
|
427
545
|
|
|
546
|
+
// PROXY_SOURCES names where the proxy the service goes out through comes from.
|
|
547
|
+
export const PROXY_SOURCES = {
|
|
548
|
+
option: '插件选项 studio.proxy',
|
|
549
|
+
env: '环境变量 AI_STUDIO_PROXY',
|
|
550
|
+
saved: '管理页',
|
|
551
|
+
startup: '服务启动时的设置',
|
|
552
|
+
unknown: '来源待确认',
|
|
553
|
+
system: '自动检测:HTTPS_PROXY 环境变量或系统代理',
|
|
554
|
+
direct: '直连'
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// redactProxy hides the passwords in a PROXY list.
|
|
558
|
+
export function redactProxy(value) {
|
|
559
|
+
return proxyList(value).map(p => {
|
|
560
|
+
try {
|
|
561
|
+
const u = new URL(p);
|
|
562
|
+
if (u.password) u.password = '***';
|
|
563
|
+
return u.toString().replace(/\/$/, '');
|
|
564
|
+
} catch {
|
|
565
|
+
return p;
|
|
566
|
+
}
|
|
567
|
+
}).join(',');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// LAUNCH_RECORD says, for the service process this plugin last started, whether it was started
|
|
571
|
+
// with a PROXY of the plugin's over the saved one, and whose: 'option' or 'env' (see pluginProxy),
|
|
572
|
+
// null for none. A caller that doesn't know the plugin's options (the CLI) reads it from there.
|
|
573
|
+
const LAUNCH_RECORD = 'backend.launch.json';
|
|
574
|
+
|
|
575
|
+
function writeLaunchRecord(dataDir, pid, proxy) {
|
|
576
|
+
const record = { pid, proxy: pluginProxy(proxy)?.source ?? null };
|
|
577
|
+
try {
|
|
578
|
+
fs.writeFileSync(path.join(dataDir, LAUNCH_RECORD), JSON.stringify(record) + '\n', { mode: 0o600 });
|
|
579
|
+
} catch {}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// launchRecord is the record of the running service's start, or null when the running service
|
|
583
|
+
// isn't the one it was written for (or an older plugin started it, which wrote none).
|
|
584
|
+
function launchRecord(dataDir) {
|
|
585
|
+
try {
|
|
586
|
+
const record = JSON.parse(fs.readFileSync(path.join(dataDir, LAUNCH_RECORD), 'utf8'));
|
|
587
|
+
if (record?.pid !== recordedPid(dataDir) || !(record.proxy === null || record.proxy in PROXY_SOURCES)) return null;
|
|
588
|
+
return record;
|
|
589
|
+
} catch {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// studioProxy is how the running service reaches Google:
|
|
595
|
+
// active the proxy it goes out through now ('' for none), and source where that comes from (a
|
|
596
|
+
// PROXY_SOURCES key; 'unknown' when the service can't tell it from a change not yet applied);
|
|
597
|
+
// saved the proxy saved in .env, as the admin page shows it;
|
|
598
|
+
// overridden, when another one is used over a saved one: whose it is ('option' or 'env'), or
|
|
599
|
+
// 'unknown' when the service was started with another one in its environment and
|
|
600
|
+
// nothing tells whose;
|
|
601
|
+
// pending what it takes for the one set (wanted, from wantedSource) to apply: 'restart', the
|
|
602
|
+
// service must start again; 'generation', its generation service must; null, nothing.
|
|
603
|
+
// A standalone caller (the CLI) doesn't know the plugin's options, so it can't tell what the
|
|
604
|
+
// service will be started with next: it reads what it was started with from the launch record.
|
|
605
|
+
// Passwords are hidden.
|
|
606
|
+
export async function studioProxy(local, options = {}) {
|
|
607
|
+
const saved = proxyList(readStudioEnv(local.dataDir).PROXY).join(',');
|
|
608
|
+
const override = options.standalone ? null : pluginProxy(options.proxy);
|
|
609
|
+
const cfg = await studioAdmin(local, 'GET', '/api/config', null, { timeoutMs: 3000, fetchFn: options.fetchFn });
|
|
610
|
+
// What the service reads as its PROXY now: .env, or the environment it was started with over it
|
|
611
|
+
const loaded = proxyList(cfg?.proxy).join(',');
|
|
612
|
+
const active = proxyList(cfg?.active_proxy).join(',');
|
|
613
|
+
|
|
614
|
+
let want = override ? override.proxy : saved;
|
|
615
|
+
let wantedSource = override ? override.source : 'saved';
|
|
616
|
+
let overridden = override && saved && saved !== override.proxy ? override.source : null;
|
|
617
|
+
if (options.standalone && loaded !== saved) {
|
|
618
|
+
// Started with another proxy in its environment: one of the plugin's, which applies as long as
|
|
619
|
+
// it is set, or, with no record of whose, one a restart may or may not set again
|
|
620
|
+
const launched = launchRecord(local.dataDir)?.proxy;
|
|
621
|
+
overridden = launched || 'unknown';
|
|
622
|
+
if (launched) {
|
|
623
|
+
want = loaded;
|
|
624
|
+
wantedSource = launched;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
let pending = null;
|
|
629
|
+
if (loaded !== want) pending = 'restart';
|
|
630
|
+
else if (want && active !== want) pending = 'generation';
|
|
631
|
+
// Saved none, going out through one: the system's, or one saved before that the generation
|
|
632
|
+
// service still has. A change not yet applied leaves which it is open.
|
|
633
|
+
const unsure = !want && active && cfg?.service_restart_required === true;
|
|
634
|
+
if (unsure) pending = 'generation';
|
|
635
|
+
|
|
636
|
+
let source;
|
|
637
|
+
if (!active) source = 'direct';
|
|
638
|
+
else if (unsure) source = 'unknown';
|
|
639
|
+
else if (pending) source = 'startup';
|
|
640
|
+
else if (!loaded) source = 'system';
|
|
641
|
+
else source = wantedSource;
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
active: redactProxy(active),
|
|
645
|
+
source,
|
|
646
|
+
saved: redactProxy(saved),
|
|
647
|
+
overridden,
|
|
648
|
+
wanted: redactProxy(want),
|
|
649
|
+
wantedSource,
|
|
650
|
+
pending
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
428
654
|
export function isConnectRefused(err) {
|
|
429
655
|
if (!err) return false;
|
|
430
656
|
const visited = new Set();
|
|
@@ -442,6 +668,20 @@ export function isConnectRefused(err) {
|
|
|
442
668
|
return false;
|
|
443
669
|
}
|
|
444
670
|
|
|
671
|
+
// olderStudioVersion says whether a version (without its v) is one this plugin may replace: an
|
|
672
|
+
// official X.Y.Z at or below the upstream base, or a managed X.Y.Z-magpie.N on an older base, or
|
|
673
|
+
// on the same base with a lower N.
|
|
674
|
+
function olderStudioVersion(version) {
|
|
675
|
+
const running = /^(\d+)\.(\d+)\.(\d+)(?:-magpie\.(\d+))?$/.exec(version);
|
|
676
|
+
if (!running) return false;
|
|
677
|
+
const ours = /-magpie\.(\d+)$/.exec(release.version);
|
|
678
|
+
const base = running.slice(1, 4).map(Number);
|
|
679
|
+
const expected = release.upstream.version.split('.').map(Number);
|
|
680
|
+
const different = base.findIndex((n, i) => n !== expected[i]);
|
|
681
|
+
if (different !== -1) return base[different] < expected[different];
|
|
682
|
+
return running[4] === undefined || Number(running[4]) < Number(ours[1]);
|
|
683
|
+
}
|
|
684
|
+
|
|
445
685
|
// checkStudioHealth says what answers on the setup's port: 'down' (nothing), 'up' (this setup's
|
|
446
686
|
// service, at the pinned version), 'outdated' (an older official version),
|
|
447
687
|
// 'version_conflict' (a newer or custom version installed outside this plugin),
|
|
@@ -487,14 +727,11 @@ export async function checkStudioHealth(local, fetchFn) {
|
|
|
487
727
|
}
|
|
488
728
|
}
|
|
489
729
|
|
|
490
|
-
// Official releases at or below our upstream base
|
|
491
|
-
// build. Newer/custom installations require an explicit recovery decision.
|
|
730
|
+
// Official releases at or below our upstream base, and managed builds older than this one, can
|
|
731
|
+
// move to this managed build. Newer/custom installations require an explicit recovery decision.
|
|
492
732
|
const runningVersion = String(status.version).replace(/^v/, '');
|
|
493
733
|
if (runningVersion !== release.version) {
|
|
494
|
-
const
|
|
495
|
-
const expected = release.upstream.version.split('.').map(Number);
|
|
496
|
-
const different = parts?.findIndex((n, i) => n !== expected[i]);
|
|
497
|
-
const older = parts && (different === -1 || parts[different] < expected[different]);
|
|
734
|
+
const older = olderStudioVersion(runningVersion);
|
|
498
735
|
return { state: older ? 'outdated' : 'version_conflict', status, reason: `Running version ${status.version}, expected ${STUDIO_VERSION}` };
|
|
499
736
|
}
|
|
500
737
|
|
|
@@ -799,7 +1036,7 @@ export async function installStudio(options = {}) {
|
|
|
799
1036
|
} else {
|
|
800
1037
|
const downloadUrl = `https://github.com/${release.repository}/releases/download/${release.tag}/${pin.asset}`;
|
|
801
1038
|
archivePath = path.join(stageDir, pin.asset);
|
|
802
|
-
const proxy =
|
|
1039
|
+
const proxy = downloadProxy(dataDir, options.proxy, savedConfig);
|
|
803
1040
|
|
|
804
1041
|
if (options.fetchFn && options.fetchFn !== globalThis.fetch) {
|
|
805
1042
|
const res = await options.fetchFn(downloadUrl, { signal: AbortSignal.timeout(300000) });
|
|
@@ -1036,7 +1273,9 @@ function shared(map, key, fn) {
|
|
|
1036
1273
|
}
|
|
1037
1274
|
|
|
1038
1275
|
// ensureStudio makes sure the service is installed and running, and, unless managementOnly,
|
|
1039
|
-
// that it takes generation requests.
|
|
1276
|
+
// that it takes generation requests. options.proxy is a proxy it is started with over the saved
|
|
1277
|
+
// one (studio.proxy); options.savedProxy is one saved as the service's own before it starts, as
|
|
1278
|
+
// the admin page saves one. Callers share one start of the service and one wait for
|
|
1040
1279
|
// it to take requests; a managementOnly caller doesn't wait for the latter.
|
|
1041
1280
|
export async function ensureStudio(options = {}) {
|
|
1042
1281
|
const dataDir = options.dataDir || studioDataDir();
|
|
@@ -1113,10 +1352,11 @@ async function launchStudio(options) {
|
|
|
1113
1352
|
}
|
|
1114
1353
|
|
|
1115
1354
|
if (!manifestValid) {
|
|
1116
|
-
await installStudio({ dataDir, platform, arch, fetchFn, archivePath: options.archivePath, proxy: options.proxy, preferredPort: options.preferredPort });
|
|
1355
|
+
await installStudio({ dataDir, platform, arch, fetchFn, archivePath: options.archivePath, proxy: options.proxy ?? options.savedProxy, preferredPort: options.preferredPort });
|
|
1117
1356
|
}
|
|
1118
1357
|
|
|
1119
1358
|
const config = ensureStudioConfig(dataDir, options.proxy, { port: await newConfigPort(dataDir, options) });
|
|
1359
|
+
if (options.savedProxy !== undefined) saveStudioProxy(dataDir, options.savedProxy);
|
|
1120
1360
|
local = localStudio({ dataDir, platform });
|
|
1121
1361
|
if (!local) throw new Error(`Invalid AI Studio configuration in ${dataDir}`);
|
|
1122
1362
|
|
|
@@ -1128,10 +1368,11 @@ async function launchStudio(options) {
|
|
|
1128
1368
|
assertStudioExited(dataDir, platform);
|
|
1129
1369
|
protectPackage(dataDir, platform);
|
|
1130
1370
|
|
|
1131
|
-
// Clean environment: no
|
|
1371
|
+
// Clean environment: no host AISTUDIO_* or WAA_* vars, nor any of upstream's settings, which
|
|
1372
|
+
// would hide the ones saved in .env
|
|
1132
1373
|
const cleanEnv = {};
|
|
1133
1374
|
for (const [k, v] of Object.entries(process.env)) {
|
|
1134
|
-
if (!k.startsWith('AISTUDIO_') && !k.startsWith('WAA_')) {
|
|
1375
|
+
if (!k.startsWith('AISTUDIO_') && !k.startsWith('WAA_') && !ENV_KEYS.includes(k.toUpperCase())) {
|
|
1135
1376
|
cleanEnv[k] = v;
|
|
1136
1377
|
}
|
|
1137
1378
|
}
|
|
@@ -1161,6 +1402,7 @@ async function launchStudio(options) {
|
|
|
1161
1402
|
});
|
|
1162
1403
|
if (child.pid) {
|
|
1163
1404
|
fs.writeFileSync(path.join(dataDir, 'backend.pid'), `${child.pid}\n`, { mode: 0o600 });
|
|
1405
|
+
writeLaunchRecord(dataDir, child.pid, options.proxy);
|
|
1164
1406
|
}
|
|
1165
1407
|
child.unref?.();
|
|
1166
1408
|
} finally {
|
package/lib/studio-release.json
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.2.
|
|
2
|
+
"version": "0.2.14-magpie.1",
|
|
3
3
|
"repository": "BUKOWSKIREAL/magpie-gemini-web",
|
|
4
|
-
"tag": "studio-v0.2.
|
|
4
|
+
"tag": "studio-v0.2.14-magpie.1",
|
|
5
5
|
"goVersion": "1.27.1",
|
|
6
6
|
"upstream": {
|
|
7
|
-
"version": "0.2.
|
|
7
|
+
"version": "0.2.14",
|
|
8
8
|
"repository": "https://github.com/Mag1cFall/AIStudio2API.git",
|
|
9
|
-
"commit": "
|
|
9
|
+
"commit": "370a313814099b508881807effa2a9d72f2d7e66"
|
|
10
10
|
},
|
|
11
11
|
"binaries": {
|
|
12
12
|
"darwin-arm64": {
|
|
13
|
-
"asset": "aistudio2api-v0.2.
|
|
14
|
-
"sha256": "
|
|
15
|
-
"binarySha256": "
|
|
13
|
+
"asset": "aistudio2api-v0.2.14-magpie.1-darwin-arm64.tar.gz",
|
|
14
|
+
"sha256": "f649c21837741bdb448407f79a3bc998429b446408ef3117a897103b9a47ee72",
|
|
15
|
+
"binarySha256": "999e49572dede3a63f538e3f62ceed5f03349459bdf391fd49fd57784985e13b"
|
|
16
16
|
},
|
|
17
17
|
"darwin-x64": {
|
|
18
|
-
"asset": "aistudio2api-v0.2.
|
|
19
|
-
"sha256": "
|
|
20
|
-
"binarySha256": "
|
|
18
|
+
"asset": "aistudio2api-v0.2.14-magpie.1-darwin-amd64.tar.gz",
|
|
19
|
+
"sha256": "ece680b5dfef57b0b0e96d639b618b74570e6e194f58808d762da85fd41d9b4a",
|
|
20
|
+
"binarySha256": "970df3ac51719721d908162d0233d80711f6c3ee007e193d0a73b6e4e596ca9a"
|
|
21
21
|
},
|
|
22
22
|
"linux-x64": {
|
|
23
|
-
"asset": "aistudio2api-v0.2.
|
|
24
|
-
"sha256": "
|
|
25
|
-
"binarySha256": "
|
|
23
|
+
"asset": "aistudio2api-v0.2.14-magpie.1-linux-amd64.tar.gz",
|
|
24
|
+
"sha256": "f6c7b52a739e4534236f055280fcddd48e64b56fdb954a7844eb453226d5b67f",
|
|
25
|
+
"binarySha256": "316306225bcc343188d6d81635d4bd79e7e82bd5c51b1009cd25361deb3b5e61"
|
|
26
26
|
},
|
|
27
27
|
"linux-arm64": {
|
|
28
|
-
"asset": "aistudio2api-v0.2.
|
|
29
|
-
"sha256": "
|
|
30
|
-
"binarySha256": "
|
|
28
|
+
"asset": "aistudio2api-v0.2.14-magpie.1-linux-arm64.tar.gz",
|
|
29
|
+
"sha256": "a5b838b9b64e78d402372cae6b88e1f3faba406747e598e976728c71da3a75ef",
|
|
30
|
+
"binarySha256": "a5e9573b9edad7fb246178fb4c3d21a0d4905789fd59abf96656d57f26b7526e"
|
|
31
31
|
},
|
|
32
32
|
"win32-x64": {
|
|
33
|
-
"asset": "aistudio2api-v0.2.
|
|
34
|
-
"sha256": "
|
|
35
|
-
"binarySha256": "
|
|
33
|
+
"asset": "aistudio2api-v0.2.14-magpie.1-windows-amd64.zip",
|
|
34
|
+
"sha256": "ccd5b96dabebbc0f1d510959c698beb9d9af09bb65d8b0e021327970b3173384",
|
|
35
|
+
"binarySha256": "ce784c98f1558395a670421ef6cc7d36dde5313f74788f7df5772851c241ae06"
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
}
|