magpie-gemini-web 0.5.3 → 0.5.5
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 +15 -6
- package/README.md +15 -6
- package/docs/studio-release.md +12 -12
- package/lib/studio-native.mjs +192 -29
- package/lib/studio-page.mjs +173 -0
- package/lib/studio-release.json +20 -19
- package/lib/studio.mjs +183 -35
- package/package.json +1 -1
- package/scripts/studio-managed-smoke.mjs +7 -5
- package/scripts/studio-release.mjs +13 -6
- 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
|
|
|
@@ -29,7 +29,7 @@ Install **magpie-gemini-web** from the Magpie plugin market, then sign in to eit
|
|
|
29
29
|
|
|
30
30
|
1. Sign in to **Google AI Studio** in Magpie and choose **「用 Google 账号登录(推荐)」** (Sign in with a Google account).
|
|
31
31
|
2. Sign in to your Google account in the separate login window that opens (your regular browser is unaffected), then close the window.
|
|
32
|
-
> On first run, the login browser Camoufox (~300 MB)
|
|
32
|
+
> On first run, the local service (~25 MB) and the login browser Camoufox (~300 MB) are downloaded first, so the window may take a few minutes to appear. While they download, Magpie opens a progress page in your browser. Magpie waits at most 10 minutes, downloads included, so finish signing in promptly; if GitHub is unreachable, set `studio.proxy` first (see plugin options and proxy settings below).
|
|
33
33
|
3. **Managing accounts:** Choose "Sign in with a Google account" again at any time to **add a new account**, create a card for an existing local account, or **re-authenticate** an expired account.
|
|
34
34
|
|
|
35
35
|
### Option 2: Gemini Web (`gemini-web`)
|
|
@@ -74,6 +74,10 @@ magpie plugin options magpie-gemini-web '{"allowImages":true}'
|
|
|
74
74
|
magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:7890"}}'
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
> **Windows PowerShell**: `magpie` runs the `magpie.cmd` wrapper, which loses the double quotes inside the JSON when it passes the arguments on. Call the program behind it directly instead: it is in the same folder as `magpie.cmd` (which `where.exe magpie` finds), named something like `magpie-windows-amd64.exe`, and Magpie's Settings > Command line shows it too. How to write it depends on the PowerShell version (`$PSVersionTable.PSVersion` shows it):
|
|
78
|
+
> - PowerShell 7.3 or later, default settings: `& "<full path to the program>" plugin options magpie-gemini-web '{"allowImages":true}'`
|
|
79
|
+
> - Windows PowerShell 5.1 (built into Windows): it doesn't escape the double quotes in an argument for you, so write them as `\"`: `& "<full path to the program>" plugin options magpie-gemini-web '{\"allowImages\":true}'`
|
|
80
|
+
|
|
77
81
|
| Option | Default | Scope | Description |
|
|
78
82
|
| :--- | :--- | :--- | :--- |
|
|
79
83
|
| `allowTools` | `true` | Shared (override via `studio.allowTools`) | Allow tool calls. When `false`, requests with tools return 400 |
|
|
@@ -89,8 +93,8 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
|
|
|
89
93
|
**Using a proxy to reach Google:**
|
|
90
94
|
- **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
95
|
- 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
|
|
93
|
-
- **The provider proxy set for `ai-studio`
|
|
96
|
+
- With none set, the backend goes out directly, or on Windows through the system proxy (Windows proxy settings).
|
|
97
|
+
- **The backend uses neither Magpie's proxies (including the provider proxy set for `ai-studio`) nor `HTTPS_PROXY` and similar variables**: it keeps running after Magpie quits, when a proxy borrowed from Magpie may be gone. Use one of the options above. A backend started by a plugin version from before this fix may still be using a proxy from the environment it was started in (Magpie's, for example); the "代理出口" line marks it and says what a restart changes.
|
|
94
98
|
- 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
99
|
- The listen address, API key and admin credentials are managed by the plugin; changing them in the admin page has no effect.
|
|
96
100
|
- **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).
|
|
@@ -109,6 +113,11 @@ After upgrading the plugin, run `magpie provider models gemini-web` to refresh a
|
|
|
109
113
|
|
|
110
114
|
<br>
|
|
111
115
|
|
|
116
|
+
**When the local service starts and stops:**
|
|
117
|
+
- It starts in the background when the plugin loads (when Magpie opens, for example); after that, a request or a usage card refresh starts it again if it isn't running.
|
|
118
|
+
- It is a process of its own: **quitting Magpie doesn't stop it**, and Magpie picks it up again the next time it opens.
|
|
119
|
+
- To stop it, use `studio:stop` below (or the full command the card or an error gives). A stop lasts until the next request, which starts it again.
|
|
120
|
+
|
|
112
121
|
When a plugin error asks you to run a command, it includes the full ready-to-copy path. From the plugin directory, you can also run:
|
|
113
122
|
|
|
114
123
|
| Command | Description |
|
|
@@ -135,8 +144,8 @@ When a plugin error asks you to run a command, it includes the full ready-to-cop
|
|
|
135
144
|
- Concurrency is capped at 1, but Google account rate-limiting or flagging cannot be completely ruled out—use at your own risk.
|
|
136
145
|
|
|
137
146
|
**AI Studio upgrades and model selection:**
|
|
138
|
-
- The plugin pins SHA-256-verified `v0.2.
|
|
139
|
-
- **Online updates are disabled in the managed backend on macOS, Linux, and Windows.** The admin page
|
|
147
|
+
- 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.
|
|
148
|
+
- **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.
|
|
140
149
|
- 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.
|
|
141
150
|
- 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.
|
|
142
151
|
- 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
|
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
1. 在 Magpie 中登录 **Google AI Studio**,选择 **「用 Google 账号登录(推荐)」**。
|
|
31
31
|
2. 在弹出的独立登录窗口中登录 Google 账号(不影响日常浏览器),完成后直接关闭窗口。
|
|
32
|
-
>
|
|
32
|
+
> 首次使用要先下载本地服务(约 25 MB)和登录浏览器 Camoufox(约 300 MB),弹窗可能需要等几分钟才出现。需要下载时,Magpie 会在浏览器里打开一个进度页,显示下载进度。Magpie 最多等 10 分钟(下载的时间也算在内),请尽快完成登录;连不上 GitHub 时,先按下面「插件选项与代理配置」设置 `studio.proxy`。
|
|
33
33
|
3. **后续管理**:再次点击「用 Google 账号登录」,可选择**添加新账号**、为本机已有账号建卡,或为过期账号**重新登录**。
|
|
34
34
|
|
|
35
35
|
### 方式二:Gemini Web (`gemini-web`)
|
|
@@ -74,6 +74,10 @@ magpie plugin options magpie-gemini-web '{"allowImages":true}'
|
|
|
74
74
|
magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:7890"}}'
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
> **Windows PowerShell**:`magpie` 实际运行的是 `magpie.cmd` 包装器,它转发参数时会弄丢 JSON 里的双引号。请直接调用它背后的可执行文件:这个文件和 `magpie.cmd` 在同一个文件夹(`where.exe magpie` 可以找到),文件名可能是 `magpie-windows-amd64.exe` 之类,Magpie 的「设置 → 命令行」里也会显示。写法取决于 PowerShell 版本(用 `$PSVersionTable.PSVersion` 查看):
|
|
78
|
+
> - PowerShell 7.3 及以上,默认设置:`& "<可执行文件的完整路径>" plugin options magpie-gemini-web '{"allowImages":true}'`
|
|
79
|
+
> - Windows PowerShell 5.1(系统自带):它不会替你转义参数里的双引号,要写成 `\"`:`& "<可执行文件的完整路径>" plugin options magpie-gemini-web '{\"allowImages\":true}'`
|
|
80
|
+
|
|
77
81
|
| 选项 | 默认值 | 适用范围 | 说明 |
|
|
78
82
|
| :--- | :--- | :--- | :--- |
|
|
79
83
|
| `allowTools` | `true` | 共用(可用 `studio.allowTools` 单独覆盖) | 允许工具调用。设为 `false` 时带工具的请求返回 400 |
|
|
@@ -89,8 +93,8 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
|
|
|
89
93
|
**配置代理访问 Google:**
|
|
90
94
|
- **AI Studio**:推荐在本地管理页设置。打开额度卡片「管理页」一行里的地址(默认 `http://127.0.0.1:2048`),用户名 `magpie`,密码是数据目录 `config.json` 里的 `adminPassword`;保存代理后在管理页重启服务即可生效,之后插件重启后端也不会覆盖它。
|
|
91
95
|
- 也可以用上表的 `studio.proxy` 或环境变量 `AI_STUDIO_PROXY`,它们优先于管理页里保存的代理。改了要重启本地服务才生效(`npm run studio:stop`,下一次请求会自动启动)。
|
|
92
|
-
-
|
|
93
|
-
-
|
|
96
|
+
- 都没设置时,后端直连;Windows 上会使用系统代理(Windows 的「代理」设置)。
|
|
97
|
+
- **后端不使用 Magpie 的代理(包括给 `ai-studio` 设置的供应商代理),也不读取 `HTTPS_PROXY` 等环境变量**:它在 Magpie 退出后仍会运行,借用 Magpie 的代理到时可能已经失效。请用上面两种方式之一设置。加入这项修复之前的插件版本启动的后端,可能还在用启动时环境变量里的代理(比如 Magpie 的);「代理出口」一行会标出来,并说明重启后会怎样。
|
|
94
98
|
- 额度卡片的「代理出口」一行会显示后端当前实际使用的代理、它来自哪里,以及新设置是否还要重启才生效;登录 Google 时的说明、登录失败的提示和 `npm run studio:status` 也会显示。
|
|
95
99
|
- 监听地址、API Key、管理员账号等由插件管理,在管理页修改不会生效。
|
|
96
100
|
- **Gemini Web**:后端不读取 `HTTP_PROXY`,请打开本地管理面板 `http://127.0.0.1:8083/admin` 在代理池中添加(登录口令为数据目录 `secrets.json` 中的 `adminToken`)。
|
|
@@ -109,6 +113,11 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
|
|
|
109
113
|
|
|
110
114
|
<br>
|
|
111
115
|
|
|
116
|
+
**本地服务什么时候启动、停止:**
|
|
117
|
+
- 插件加载时(例如打开 Magpie)会在后台启动本地服务;之后发请求、刷新额度卡片时,服务没在运行也会自动启动。
|
|
118
|
+
- 本地服务是独立进程,**退出 Magpie 不会停止它**,下次打开 Magpie 直接接着用。
|
|
119
|
+
- 停止服务用下表的 `studio:stop`(或卡片、报错里给出的完整命令)。停止只是一次性的:下一次请求会再次启动它。
|
|
120
|
+
|
|
112
121
|
当插件报错提示运行命令时,直接复制报错中的完整命令执行即可。在插件目录下也可使用:
|
|
113
122
|
|
|
114
123
|
| 命令 | 作用 |
|
|
@@ -135,8 +144,8 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
|
|
|
135
144
|
- 插件将请求并发限制为 1,但仍无法完全排除 Google 账号风控风险,请自行评估使用。
|
|
136
145
|
|
|
137
146
|
**AI Studio 升级与模型范围:**
|
|
138
|
-
- 插件固定使用经过 SHA-256 校验的 `v0.2.
|
|
139
|
-
- **托管后端已在 macOS、Linux、Windows
|
|
147
|
+
- 插件固定使用经过 SHA-256 校验的 `v0.2.14-magpie.1`(基于上游 `v0.2.14`),通过升级插件更新后端。同基线或更旧的官方版、以及旧版插件装的托管后端,在下次管理启动时自动升级,保留账号、端口与登录凭据。
|
|
148
|
+
- **托管后端已在 macOS、Linux、Windows 统一禁用在线更新**:管理页提示“更新由 magpie-gemini-web 管理”,更新接口返回 HTTP 409,检查更新也不会访问 GitHub。用户自行连接的外部服务不受影响。托管构建方式和可复现构建流程见 [docs/studio-release.md](docs/studio-release.md)。
|
|
140
149
|
- Unix 的 `package/` 仍尽力设置为 `0555`,作为额外的文件保护;权限设置失败不阻断服务,在线更新的禁用也不依赖该权限。若要手动删除数据目录来重置,请先停止服务,再运行 `chmod -R u+w "<数据目录>/package"` 恢复可写权限,然后删除;将 `<数据目录>` 替换为实际路径。
|
|
141
150
|
- 若自行更新到更新版或定制版,插件会提示版本冲突,并提供带正确 `--data-dir` 的 `stop`、`install`、`start` 命令。停止时会找回更新后的监听进程 PID,并核对可执行文件路径;无法核实身份就不会发送终止信号。HTTP 端口关闭但进程尚未退出时,插件不会重装或启动第二个进程。
|
|
142
151
|
- 已停止但文件被替换的后端会从校验通过的固定版本安装包恢复;校验失败不会启动替换文件。安装会保留账号和运行数据文件,但**不保证旧版本能读取新版迁移过的数据**。手动降级前先停止服务并备份整个数据目录(包括 `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`。`upstream.camoufox` 是这个 commit 要求的 Camoufox 版本(上游 `internal/camoufoxnative/download.go` 里的 `camoufoxRelease`),插件靠它判断登录时要不要重新下载 Camoufox;更新上游 commit 时要一起改,构建脚本发现不一致会报错。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
|
@@ -5,8 +5,13 @@ import crypto from 'node:crypto';
|
|
|
5
5
|
import net from 'node:net';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { spawn, execFile, execFileSync } from 'node:child_process';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
8
9
|
import release from './studio-release.json' with { type: 'json' };
|
|
9
10
|
|
|
11
|
+
// execFileAsync is execFile awaited: unpacking a download this way leaves the plugin host free to answer
|
|
12
|
+
// meanwhile, a sign-in's progress page among others.
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
|
|
10
15
|
export const STUDIO_VERSION = `v${release.version}`;
|
|
11
16
|
export const PINNED_ARCHIVES = release.binaries;
|
|
12
17
|
// The port a new setup tries first: AIStudio2API's own default.
|
|
@@ -17,6 +22,16 @@ const LOG_ROTATE_BYTES = 10 * 1024 * 1024;
|
|
|
17
22
|
const adminSessions = new Map();
|
|
18
23
|
const inFlightStarts = new Map();
|
|
19
24
|
const inFlightReady = new Map();
|
|
25
|
+
// setupSteps is where a start of the service stands, by data dir, while one is in flight:
|
|
26
|
+
// { step: 'download', received, total } (bytes; total 0 while unknown), { step: 'install' } or
|
|
27
|
+
// { step: 'start' }. A sign-in's progress page reads it (setupStep).
|
|
28
|
+
const setupSteps = new Map();
|
|
29
|
+
|
|
30
|
+
// setupStep is where the start of the service in dataDir stands, null when none is in flight or
|
|
31
|
+
// it hasn't got to a step yet (as while it waits for another's lock).
|
|
32
|
+
export function setupStep(dataDir) {
|
|
33
|
+
return setupSteps.get(dataDir) ?? null;
|
|
34
|
+
}
|
|
20
35
|
|
|
21
36
|
export function studioDataDir(env = process.env) {
|
|
22
37
|
return env.AI_STUDIO_DATA_DIR || path.join(os.homedir(), '.local', 'share', 'magpie-ai-studio');
|
|
@@ -122,7 +137,7 @@ export function pluginProxy(explicit) {
|
|
|
122
137
|
|
|
123
138
|
// downloadProxy is the proxy an install downloads through: the one given (an empty one turns it
|
|
124
139
|
// 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) {
|
|
140
|
+
export function downloadProxy(dataDir, explicit, savedConfig) {
|
|
126
141
|
if (explicit !== undefined) return validateProxy(explicit);
|
|
127
142
|
if (process.env.AI_STUDIO_PROXY !== undefined) return validateProxy(process.env.AI_STUDIO_PROXY);
|
|
128
143
|
let saved;
|
|
@@ -178,9 +193,12 @@ const ENV_KEYS = [
|
|
|
178
193
|
'WARM_WORKER_LIMIT', 'MAX_ACTIVE_WORKERS', 'WARM_STARTUP_CONCURRENCY', 'PER_ACCOUNT_CONCURRENCY',
|
|
179
194
|
'ROUTING_STRATEGY', 'UPSTREAM_CHANNELS', 'MODEL_MAPPING', 'TEMPORARY_CHAT', 'WAA_BACKEND', 'AUTO_START',
|
|
180
195
|
'REQUEST_BODY_LOG', 'LOG_FILE', 'LOG_MAX_TOTAL_MB', 'ADMIN_AUTH_ENABLED', 'ADMIN_USERNAME', 'ADMIN_PASSWORD',
|
|
181
|
-
'BUILD_NATIVE_NONSTREAM'
|
|
196
|
+
'BUILD_NATIVE_NONSTREAM', 'MODEL_CHANNELS', 'MEDIA_OUTPUT', 'MEDIA_BASE_URL'
|
|
182
197
|
];
|
|
183
198
|
|
|
199
|
+
// PROXY_ENV are the proxy variables the service would fall back to with no PROXY of its own.
|
|
200
|
+
const PROXY_ENV = ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY'];
|
|
201
|
+
|
|
184
202
|
// SETTING_DEFAULTS are the settings the admin page owns, as a new setup has them. They are written
|
|
185
203
|
// to .env when it lacks them and left as the admin page saves them after that.
|
|
186
204
|
const SETTING_DEFAULTS = {
|
|
@@ -550,7 +568,8 @@ export const PROXY_SOURCES = {
|
|
|
550
568
|
saved: '管理页',
|
|
551
569
|
startup: '服务启动时的设置',
|
|
552
570
|
unknown: '来源待确认',
|
|
553
|
-
system: '
|
|
571
|
+
system: '自动检测:系统代理设置',
|
|
572
|
+
legacy: '旧版插件启动的服务自动检测到的,可能来自启动时的环境变量或系统代理设置',
|
|
554
573
|
direct: '直连'
|
|
555
574
|
};
|
|
556
575
|
|
|
@@ -570,10 +589,13 @@ export function redactProxy(value) {
|
|
|
570
589
|
// LAUNCH_RECORD says, for the service process this plugin last started, whether it was started
|
|
571
590
|
// with a PROXY of the plugin's over the saved one, and whose: 'option' or 'env' (see pluginProxy),
|
|
572
591
|
// null for none. A caller that doesn't know the plugin's options (the CLI) reads it from there.
|
|
592
|
+
// proxyEnv 'none' says it was started with no *_PROXY in its environment (see launchStudio); a
|
|
593
|
+
// record without it is an older plugin's, whose service may have had one: Magpie's, a shell's,
|
|
594
|
+
// or none, which nothing kept tells apart.
|
|
573
595
|
const LAUNCH_RECORD = 'backend.launch.json';
|
|
574
596
|
|
|
575
597
|
function writeLaunchRecord(dataDir, pid, proxy) {
|
|
576
|
-
const record = { pid, proxy: pluginProxy(proxy)?.source ?? null };
|
|
598
|
+
const record = { pid, proxy: pluginProxy(proxy)?.source ?? null, proxyEnv: 'none' };
|
|
577
599
|
try {
|
|
578
600
|
fs.writeFileSync(path.join(dataDir, LAUNCH_RECORD), JSON.stringify(record) + '\n', { mode: 0o600 });
|
|
579
601
|
} catch {}
|
|
@@ -637,8 +659,12 @@ export async function studioProxy(local, options = {}) {
|
|
|
637
659
|
if (!active) source = 'direct';
|
|
638
660
|
else if (unsure) source = 'unknown';
|
|
639
661
|
else if (pending) source = 'startup';
|
|
640
|
-
else if (!loaded)
|
|
641
|
-
|
|
662
|
+
else if (!loaded) {
|
|
663
|
+
// An older plugin started it, maybe with a proxy in its environment, which the system's can't
|
|
664
|
+
// be told from
|
|
665
|
+
const record = launchRecord(local.dataDir);
|
|
666
|
+
source = record && record.proxyEnv !== 'none' ? 'legacy' : 'system';
|
|
667
|
+
} else source = wantedSource;
|
|
642
668
|
|
|
643
669
|
return {
|
|
644
670
|
active: redactProxy(active),
|
|
@@ -668,6 +694,20 @@ export function isConnectRefused(err) {
|
|
|
668
694
|
return false;
|
|
669
695
|
}
|
|
670
696
|
|
|
697
|
+
// olderStudioVersion says whether a version (without its v) is one this plugin may replace: an
|
|
698
|
+
// official X.Y.Z at or below the upstream base, or a managed X.Y.Z-magpie.N on an older base, or
|
|
699
|
+
// on the same base with a lower N.
|
|
700
|
+
function olderStudioVersion(version) {
|
|
701
|
+
const running = /^(\d+)\.(\d+)\.(\d+)(?:-magpie\.(\d+))?$/.exec(version);
|
|
702
|
+
if (!running) return false;
|
|
703
|
+
const ours = /-magpie\.(\d+)$/.exec(release.version);
|
|
704
|
+
const base = running.slice(1, 4).map(Number);
|
|
705
|
+
const expected = release.upstream.version.split('.').map(Number);
|
|
706
|
+
const different = base.findIndex((n, i) => n !== expected[i]);
|
|
707
|
+
if (different !== -1) return base[different] < expected[different];
|
|
708
|
+
return running[4] === undefined || Number(running[4]) < Number(ours[1]);
|
|
709
|
+
}
|
|
710
|
+
|
|
671
711
|
// checkStudioHealth says what answers on the setup's port: 'down' (nothing), 'up' (this setup's
|
|
672
712
|
// service, at the pinned version), 'outdated' (an older official version),
|
|
673
713
|
// 'version_conflict' (a newer or custom version installed outside this plugin),
|
|
@@ -713,20 +753,113 @@ export async function checkStudioHealth(local, fetchFn) {
|
|
|
713
753
|
}
|
|
714
754
|
}
|
|
715
755
|
|
|
716
|
-
// Official releases at or below our upstream base
|
|
717
|
-
// build. Newer/custom installations require an explicit recovery decision.
|
|
756
|
+
// Official releases at or below our upstream base, and managed builds older than this one, can
|
|
757
|
+
// move to this managed build. Newer/custom installations require an explicit recovery decision.
|
|
718
758
|
const runningVersion = String(status.version).replace(/^v/, '');
|
|
719
759
|
if (runningVersion !== release.version) {
|
|
720
|
-
const
|
|
721
|
-
const expected = release.upstream.version.split('.').map(Number);
|
|
722
|
-
const different = parts?.findIndex((n, i) => n !== expected[i]);
|
|
723
|
-
const older = parts && (different === -1 || parts[different] < expected[different]);
|
|
760
|
+
const older = olderStudioVersion(runningVersion);
|
|
724
761
|
return { state: older ? 'outdated' : 'version_conflict', status, reason: `Running version ${status.version}, expected ${STUDIO_VERSION}` };
|
|
725
762
|
}
|
|
726
763
|
|
|
727
764
|
return { state: 'up', status };
|
|
728
765
|
}
|
|
729
766
|
|
|
767
|
+
// CAMOUFOX_EXECUTABLES are where upstream's Camoufox, the browser it signs Google accounts in
|
|
768
|
+
// with, has its executable in its folder, by platform (camoufoxnative.camoufoxExecutablePath).
|
|
769
|
+
const CAMOUFOX_EXECUTABLES = {
|
|
770
|
+
win32: 'camoufox.exe',
|
|
771
|
+
linux: 'camoufox-bin',
|
|
772
|
+
darwin: path.join('Camoufox.app', 'Contents', 'MacOS', 'camoufox')
|
|
773
|
+
};
|
|
774
|
+
// How much of the end of backend.log camoufoxState reads for the download's size
|
|
775
|
+
const LOG_TAIL_BYTES = 256 * 1024;
|
|
776
|
+
// CAMOUFOX_VERSION is the Camoufox the pinned upstream takes (camoufoxnative.camoufoxRelease): it
|
|
777
|
+
// downloads that one over any other it finds.
|
|
778
|
+
export const CAMOUFOX_VERSION = release.upstream.camoufox;
|
|
779
|
+
|
|
780
|
+
// camoufoxState says whether the service in dataDir has Camoufox, which it downloads (from GitHub,
|
|
781
|
+
// some 300 MB) when an account first signs in and whenever the one it has isn't CAMOUFOX_VERSION:
|
|
782
|
+
// { ready: true }, or how far that download has got, as upstream's camoufoxnative leaves it in
|
|
783
|
+
// runtime/ under the service's working directory (dataDir): the bytes received of the archive's
|
|
784
|
+
// total (0 while not known), and whether it is unpacking. One named by CAMOUFOX_PATH is the only
|
|
785
|
+
// one upstream takes, and never downloads over.
|
|
786
|
+
export function camoufoxState(dataDir, platform = process.platform, env = process.env) {
|
|
787
|
+
const exe = CAMOUFOX_EXECUTABLES[platform];
|
|
788
|
+
if (!exe || env.CAMOUFOX_PATH?.trim()) return { ready: true };
|
|
789
|
+
const runtime = path.join(dataDir, 'runtime');
|
|
790
|
+
const candidates = [path.join(runtime, 'camoufox', exe), path.join(dataDir, 'package', 'runtime', 'camoufox', exe)];
|
|
791
|
+
if (platform === 'win32' && env.LOCALAPPDATA) candidates.push(path.join(env.LOCALAPPDATA, 'camoufox', 'camoufox', 'Cache', exe));
|
|
792
|
+
if (candidates.some(camoufoxPinned)) return { ready: true };
|
|
793
|
+
|
|
794
|
+
let names = [];
|
|
795
|
+
try { names = fs.readdirSync(runtime); } catch {}
|
|
796
|
+
const part = names.find(n => /^camoufox-.+\.zip\.part$/.test(n));
|
|
797
|
+
let received = 0;
|
|
798
|
+
try { if (part) received = fs.statSync(path.join(runtime, part)).size; } catch {}
|
|
799
|
+
const total = camoufoxSize(path.join(dataDir, 'backend.log'));
|
|
800
|
+
return {
|
|
801
|
+
ready: false,
|
|
802
|
+
received,
|
|
803
|
+
// the log rounds the size down to a MiB
|
|
804
|
+
total: total && Math.max(total, received),
|
|
805
|
+
unpacking: names.some(n => n.startsWith('.camoufox-stage-'))
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// camoufoxPinned says whether exe is a Camoufox upstream takes as it is: the version its
|
|
810
|
+
// application.ini gives, beside it or in the app's Resources (macOS), is CAMOUFOX_VERSION
|
|
811
|
+
// (camoufoxnative.validateCamoufoxExecutable).
|
|
812
|
+
function camoufoxPinned(exe) {
|
|
813
|
+
try {
|
|
814
|
+
if (fs.statSync(exe).isDirectory()) return false;
|
|
815
|
+
} catch {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
const dir = path.dirname(exe);
|
|
819
|
+
for (const ini of [path.join(dir, 'application.ini'), path.join(path.dirname(dir), 'Resources', 'application.ini')]) {
|
|
820
|
+
let text;
|
|
821
|
+
try {
|
|
822
|
+
text = fs.readFileSync(ini, 'utf8');
|
|
823
|
+
} catch (err) {
|
|
824
|
+
if (err.code === 'ENOENT') continue;
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
for (const line of text.split('\n')) {
|
|
828
|
+
const version = line.trim().match(/^Version=(.*)$/);
|
|
829
|
+
if (version) return version[1].trim() === CAMOUFOX_VERSION;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// camoufoxSize is the size of the Camoufox archive the service last began downloading, as its log
|
|
836
|
+
// says it ("Camoufox 下载已开始", in whole MiB), 0 when it doesn't.
|
|
837
|
+
function camoufoxSize(logPath) {
|
|
838
|
+
let tail;
|
|
839
|
+
try {
|
|
840
|
+
const fd = fs.openSync(logPath, 'r');
|
|
841
|
+
try {
|
|
842
|
+
const size = fs.fstatSync(fd).size;
|
|
843
|
+
const length = Math.min(size, LOG_TAIL_BYTES);
|
|
844
|
+
tail = Buffer.alloc(length);
|
|
845
|
+
fs.readSync(fd, tail, 0, length, size - length);
|
|
846
|
+
} finally {
|
|
847
|
+
fs.closeSync(fd);
|
|
848
|
+
}
|
|
849
|
+
} catch {
|
|
850
|
+
return 0;
|
|
851
|
+
}
|
|
852
|
+
const lines = tail.toString('utf8').split('\n');
|
|
853
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
854
|
+
if (!lines[i].includes('Camoufox 下载已开始')) continue;
|
|
855
|
+
try {
|
|
856
|
+
const mib = JSON.parse(lines[i]).size_mib;
|
|
857
|
+
if (Number.isFinite(mib) && mib > 0) return mib * 1024 * 1024;
|
|
858
|
+
} catch {}
|
|
859
|
+
}
|
|
860
|
+
return 0;
|
|
861
|
+
}
|
|
862
|
+
|
|
730
863
|
export async function studioStatus(options = {}) {
|
|
731
864
|
const dataDir = options.dataDir || studioDataDir();
|
|
732
865
|
const platform = options.platform || process.platform;
|
|
@@ -963,6 +1096,20 @@ async function withLock(dataDir, fn, timeoutMs = 600000) {
|
|
|
963
1096
|
}
|
|
964
1097
|
}
|
|
965
1098
|
|
|
1099
|
+
// downloadProgress is a download's step as curl writes it: the bytes in file so far, of the
|
|
1100
|
+
// Content-Length the last response in headersFile gave (the archive's, after GitHub's redirects),
|
|
1101
|
+
// 0 while that isn't known.
|
|
1102
|
+
export function downloadProgress(file, headersFile) {
|
|
1103
|
+
let received = 0;
|
|
1104
|
+
let total = 0;
|
|
1105
|
+
try { received = fs.statSync(file).size; } catch {}
|
|
1106
|
+
try {
|
|
1107
|
+
const lengths = [...fs.readFileSync(headersFile, 'latin1').matchAll(/^content-length:\s*(\d+)/gim)];
|
|
1108
|
+
if (lengths.length) total = Number(lengths.at(-1)[1]);
|
|
1109
|
+
} catch {}
|
|
1110
|
+
return { step: 'download', received, total: total >= received ? total : 0 };
|
|
1111
|
+
}
|
|
1112
|
+
|
|
966
1113
|
export async function installStudio(options = {}) {
|
|
967
1114
|
const platform = options.platform || process.platform;
|
|
968
1115
|
const arch = options.arch || process.arch;
|
|
@@ -1026,6 +1173,7 @@ export async function installStudio(options = {}) {
|
|
|
1026
1173
|
const downloadUrl = `https://github.com/${release.repository}/releases/download/${release.tag}/${pin.asset}`;
|
|
1027
1174
|
archivePath = path.join(stageDir, pin.asset);
|
|
1028
1175
|
const proxy = downloadProxy(dataDir, options.proxy, savedConfig);
|
|
1176
|
+
setupSteps.set(dataDir, { step: 'download', received: 0, total: 0 });
|
|
1029
1177
|
|
|
1030
1178
|
if (options.fetchFn && options.fetchFn !== globalThis.fetch) {
|
|
1031
1179
|
const res = await options.fetchFn(downloadUrl, { signal: AbortSignal.timeout(300000) });
|
|
@@ -1044,18 +1192,26 @@ export async function installStudio(options = {}) {
|
|
|
1044
1192
|
if (proxy) {
|
|
1045
1193
|
curlArgs.push('--proxy', proxy);
|
|
1046
1194
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1195
|
+
// The headers curl dumps as they come say the archive's size, and the file it writes says
|
|
1196
|
+
// how much has come: together they show the download's progress.
|
|
1197
|
+
const headersPath = path.join(stageDir, 'headers.txt');
|
|
1198
|
+
curlArgs.push('-D', headersPath, '-o', archivePath, downloadUrl);
|
|
1199
|
+
const watch = setInterval(() => setupSteps.set(dataDir, downloadProgress(archivePath, headersPath)), 500);
|
|
1200
|
+
try {
|
|
1201
|
+
await new Promise((resolve, reject) => {
|
|
1202
|
+
execFile('curl', curlArgs, (err, _stdout, stderr) => {
|
|
1203
|
+
if (err) {
|
|
1204
|
+
const status = err.code || err.status || 'failed';
|
|
1205
|
+
const reason = String(stderr || '').trim().slice(0, 500);
|
|
1206
|
+
reject(new Error(`Failed to download AIStudio2API archive via curl (status: ${status})${reason ? `: ${reason}` : ''}`));
|
|
1207
|
+
} else {
|
|
1208
|
+
resolve();
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1057
1211
|
});
|
|
1058
|
-
}
|
|
1212
|
+
} finally {
|
|
1213
|
+
clearInterval(watch);
|
|
1214
|
+
}
|
|
1059
1215
|
const downloadedHash = computeFileHash(archivePath);
|
|
1060
1216
|
if (downloadedHash !== expectedHash) {
|
|
1061
1217
|
throw new Error(`SHA256 checksum mismatch for downloaded archive: expected ${expectedHash}, got ${downloadedHash}`);
|
|
@@ -1068,11 +1224,12 @@ export async function installStudio(options = {}) {
|
|
|
1068
1224
|
fs.chmodSync(retainedArchive, 0o600);
|
|
1069
1225
|
}
|
|
1070
1226
|
|
|
1227
|
+
setupSteps.set(dataDir, { step: 'install' });
|
|
1071
1228
|
const extractDir = path.join(stageDir, 'extracted');
|
|
1072
1229
|
fs.mkdirSync(extractDir, { recursive: true, mode: 0o700 });
|
|
1073
1230
|
|
|
1074
1231
|
if (platform === 'win32') {
|
|
1075
|
-
|
|
1232
|
+
await execFileAsync('powershell', [
|
|
1076
1233
|
'-NoProfile',
|
|
1077
1234
|
'-NonInteractive',
|
|
1078
1235
|
'-Command',
|
|
@@ -1082,7 +1239,7 @@ export async function installStudio(options = {}) {
|
|
|
1082
1239
|
windowsHide: true
|
|
1083
1240
|
});
|
|
1084
1241
|
} else {
|
|
1085
|
-
const listOutput =
|
|
1242
|
+
const listOutput = (await execFileAsync('tar', ['-tvf', archivePath], { encoding: 'utf8' })).stdout.split('\n').filter(Boolean);
|
|
1086
1243
|
for (const line of listOutput) {
|
|
1087
1244
|
const parts = line.split(/\s+/);
|
|
1088
1245
|
const mode = parts[0];
|
|
@@ -1090,14 +1247,14 @@ export async function installStudio(options = {}) {
|
|
|
1090
1247
|
throw new Error(`Symlinks or special files in archive rejected: ${line}`);
|
|
1091
1248
|
}
|
|
1092
1249
|
}
|
|
1093
|
-
const listing =
|
|
1250
|
+
const listing = (await execFileAsync('tar', ['-tf', archivePath], { encoding: 'utf8' })).stdout.split('\n').filter(Boolean);
|
|
1094
1251
|
for (const entry of listing) {
|
|
1095
1252
|
const entryParts = entry.replace(/\\/g, '/').split('/');
|
|
1096
1253
|
if (entryParts.includes('..') || entry.startsWith('/') || entry.startsWith('\\') || /^[a-zA-Z]:/.test(entry)) {
|
|
1097
1254
|
throw new Error(`Dangerous member in archive rejected: ${entry}`);
|
|
1098
1255
|
}
|
|
1099
1256
|
}
|
|
1100
|
-
|
|
1257
|
+
await execFileAsync('tar', ['-xzf', archivePath, '-C', extractDir]);
|
|
1101
1258
|
}
|
|
1102
1259
|
|
|
1103
1260
|
let topDir = extractDir;
|
|
@@ -1268,7 +1425,8 @@ function shared(map, key, fn) {
|
|
|
1268
1425
|
// it to take requests; a managementOnly caller doesn't wait for the latter.
|
|
1269
1426
|
export async function ensureStudio(options = {}) {
|
|
1270
1427
|
const dataDir = options.dataDir || studioDataDir();
|
|
1271
|
-
const { local, status } = await shared(inFlightStarts, dataDir,
|
|
1428
|
+
const { local, status } = await shared(inFlightStarts, dataDir,
|
|
1429
|
+
() => launchStudio({ ...options, dataDir }).finally(() => setupSteps.delete(dataDir)));
|
|
1272
1430
|
if (options.managementOnly) return local;
|
|
1273
1431
|
return shared(inFlightReady, dataDir, () => readyStudio(local, status, options));
|
|
1274
1432
|
}
|
|
@@ -1361,12 +1519,17 @@ async function launchStudio(options) {
|
|
|
1361
1519
|
// would hide the ones saved in .env
|
|
1362
1520
|
const cleanEnv = {};
|
|
1363
1521
|
for (const [k, v] of Object.entries(process.env)) {
|
|
1364
|
-
if (!k.startsWith('AISTUDIO_') && !k.startsWith('WAA_') && !ENV_KEYS.includes(k.toUpperCase())) {
|
|
1522
|
+
if (!k.startsWith('AISTUDIO_') && !k.startsWith('WAA_') && !ENV_KEYS.includes(k.toUpperCase()) && !PROXY_ENV.includes(k.toUpperCase())) {
|
|
1365
1523
|
cleanEnv[k] = v;
|
|
1366
1524
|
}
|
|
1367
1525
|
}
|
|
1526
|
+
// Nor a proxy of the starter's: the service outlives Magpie, whose plugin host gives a program it
|
|
1527
|
+
// starts with none of these the proxy of the request it starts in, or Magpie's own, which may be
|
|
1528
|
+
// gone by the time the service uses it. Empty ones keep the host from adding its own.
|
|
1529
|
+
for (const k of PROXY_ENV) cleanEnv[k] = '';
|
|
1368
1530
|
Object.assign(cleanEnv, serviceEnv(dataDir, config, { proxy: options.proxy }));
|
|
1369
1531
|
|
|
1532
|
+
setupSteps.set(dataDir, { step: 'start' });
|
|
1370
1533
|
const logPath = path.join(dataDir, 'backend.log');
|
|
1371
1534
|
rotateLog(logPath);
|
|
1372
1535
|
const logFd = fs.openSync(logPath, 'a', 0o600);
|