@xcanwin/manyoyo 8.5.0 → 8.6.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.md +66 -25
- package/bin/manyoyo.js +50 -13
- package/lib/app-update.js +93 -35
- package/lib/container-network.js +100 -48
- package/lib/container-run.js +1 -1
- package/lib/egress-proxy.js +146 -64
- package/lib/egress-sidecar-main.js +23 -5
- package/lib/egress-sidecar.js +22 -12
- package/lib/net-proxy.js +183 -0
- package/lib/network-endpoints.js +18 -19
- package/lib/network-policy.js +296 -77
- package/lib/plugin/playwright.js +4 -2
- package/lib/web/AGENTS.md +1 -1
- package/lib/web/container-manage.js +68 -30
- package/lib/web/index.html +80 -81
- package/lib/web/server.js +12 -18
- package/manyoyo.example.json +9 -6
- package/package.json +5 -3
- package/README.en.md +0 -88
package/README.md
CHANGED
|
@@ -3,35 +3,36 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# <p align="center"><a href="https://github.com/xcanwin/manyoyo">MANYOYO(慢悠悠)</a></p>
|
|
6
|
-
<p align="center"><b>
|
|
7
|
-
<p align="center">开源 AI
|
|
6
|
+
<p align="center"><b>Open-source sandbox for AI coding agents: run Claude Code, Codex, Gemini CLI and OpenCode in YOLO mode safely inside Docker/Podman</b></p>
|
|
7
|
+
<p align="center">开源 AI 编程智能体安全沙箱:让 Claude Code / Codex / Gemini CLI / OpenCode 在 Docker/Podman 容器里放心跑 YOLO 模式,AI 随便折腾,伤不到你的电脑。</p>
|
|
8
8
|
<p align="center">
|
|
9
9
|
<a href="https://www.npmjs.com/package/@xcanwin/manyoyo"><img alt="npm" src="https://img.shields.io/npm/v/@xcanwin/manyoyo?style=flat-square" /></a>
|
|
10
10
|
<a href="https://github.com/xcanwin/manyoyo/actions/workflows/npm-publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/xcanwin/manyoyo/npm-publish.yml?style=flat-square" /></a>
|
|
11
|
-
<a href="https://github.com/xcanwin/manyoyo/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/badge/License-
|
|
11
|
+
<a href="https://github.com/xcanwin/manyoyo/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" /></a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
15
|
-
<a href="
|
|
16
|
-
<a href="
|
|
17
|
-
</p>
|
|
18
|
-
<p align="center">
|
|
19
|
-
文档:<a href="https://xcanwin.github.io/manyoyo/">https://xcanwin.github.io/manyoyo/</a>
|
|
15
|
+
<a href="#中文"><b>中文</b></a> |
|
|
16
|
+
<a href="#english">English</a>
|
|
20
17
|
</p>
|
|
21
18
|
|
|
22
19
|
---
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
<a name="中文"></a>
|
|
25
22
|
|
|
26
|
-
|
|
23
|
+
## 安装(二选一即可)
|
|
24
|
+
|
|
25
|
+
### 1. macOS / Linux(推荐)
|
|
27
26
|
|
|
28
27
|
```bash
|
|
29
|
-
curl -fsSL https://github.
|
|
28
|
+
curl -fsSL https://xcanwin.github.io/manyoyo/install.sh | sh
|
|
30
29
|
```
|
|
31
30
|
|
|
32
31
|
装完按屏幕提示完成配置。macOS 不需要预装任何东西。Linux:缺少容器环境时,安装器会说明原因,征得你同意后用 sudo 自动安装。
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
下载慢或打不开 GitHub?先在终端设置代理(`export https_proxy=http://127.0.0.1:7890`,macOS 的系统代理会自动识别),或见[安装文档](https://xcanwin.github.io/manyoyo/guide/quick-start)里的手动下载办法。
|
|
34
|
+
|
|
35
|
+
### 2. 已有 Node.js 和 Docker/Podman
|
|
35
36
|
|
|
36
37
|
```bash
|
|
37
38
|
npm install -g @xcanwin/manyoyo
|
|
@@ -63,26 +64,66 @@ manyoyo run -y c # 在沙箱里以 YOLO 模式启动 Claude Code
|
|
|
63
64
|
- **统一配置**:运行配置、环境变量、挂载与镜像参数集中在 `~/.manyoyo/manyoyo.json`
|
|
64
65
|
- **一条命令**安装、升级、卸载
|
|
65
66
|
|
|
66
|
-
| 对比项 | 裸跑 Agent CLI | MANYOYO |
|
|
67
|
-
| --- | --- | --- |
|
|
68
|
-
| 宿主机暴露面 | 高 | 更低 |
|
|
69
|
-
| 运行边界 | 分散 | 集中到容器与配置 |
|
|
70
|
-
| 环境复现 | 弱 | 强(镜像 + 配置) |
|
|
71
|
-
| 高风险模式说明 | 通常依赖工具自身 | 明确提示 YOLO / SOLO / sock 风险 |
|
|
72
|
-
|
|
73
67
|
## 安全须知
|
|
74
68
|
|
|
75
69
|
MANYOYO 降低风险,但不是“绝对安全”:主要隔离手段是容器而不是虚拟机;`YOLO / SOLO` 仍可能执行危险命令;`sock` 模式会暴露宿主机 Docker socket;对外监听必须设强密码。详见[安全须知](https://xcanwin.github.io/manyoyo/guide/security)。
|
|
76
70
|
|
|
77
71
|
## 文档
|
|
78
72
|
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
<https://xcanwin.github.io/manyoyo/>:[安装](https://xcanwin.github.io/manyoyo/guide/quick-start)、[命令速查](https://xcanwin.github.io/manyoyo/reference/cli-options)、[配置](https://xcanwin.github.io/manyoyo/configuration/)、[故障排查](https://xcanwin.github.io/manyoyo/troubleshooting/)
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
<a name="english"></a>
|
|
78
|
+
|
|
79
|
+
## Install (pick one)
|
|
80
|
+
|
|
81
|
+
### 1. macOS / Linux (recommended)
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
curl -fsSL https://xcanwin.github.io/manyoyo/install.sh | sh
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
When it finishes, follow the on-screen prompts to finish setup. macOS needs nothing installed first. Linux: if the container runtime is missing, the installer explains why and, with your consent, installs it with sudo.
|
|
88
|
+
|
|
89
|
+
Slow or cannot reach GitHub? Set a proxy in the terminal first (`export https_proxy=http://127.0.0.1:7890`; the macOS system proxy is detected automatically), or see the manual download steps in the [install guide](https://xcanwin.github.io/manyoyo/en/guide/quick-start).
|
|
90
|
+
|
|
91
|
+
### 2. Already have Node.js and Docker/Podman
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm install -g @xcanwin/manyoyo
|
|
95
|
+
manyoyo
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Daily use
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
manyoyo # open the web UI
|
|
102
|
+
manyoyo update # upgrade
|
|
103
|
+
manyoyo uninstall # uninstall (config and data are kept by default)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Command line (optional)
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
manyoyo init all # import the agent configs already on this machine
|
|
110
|
+
manyoyo run -y c # start Claude Code in YOLO mode inside the sandbox
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The default image `ghcr.io/xcanwin/manyoyo` is pulled automatically on first use; run `manyoyo build --iv 2.2.0-common` only if you need a custom image. More commands: [CLI reference](https://xcanwin.github.io/manyoyo/en/reference/cli-options).
|
|
114
|
+
|
|
115
|
+
## What it does
|
|
116
|
+
|
|
117
|
+
- **Multiple agents**: `claude`, `codex`, `gemini`, `opencode` behind one entry point
|
|
118
|
+
- **Container isolation**: Docker / Podman; the agent only sees the work directory you give it
|
|
119
|
+
- **Web UI**: chat, browse files and open a terminal in the browser, phone friendly
|
|
120
|
+
- **One config**: run profiles, environment variables, mounts and image arguments in `~/.manyoyo/manyoyo.json`
|
|
121
|
+
- **One command** to install, upgrade and uninstall
|
|
81
122
|
|
|
82
|
-
##
|
|
123
|
+
## Security notes
|
|
83
124
|
|
|
84
|
-
|
|
125
|
+
MANYOYO reduces risk but is not "absolutely safe": the main isolation is a container, not a virtual machine; `YOLO / SOLO` can still run dangerous commands; `sock` mode exposes the host Docker socket; set a strong password before listening on a public address. See [Security notes](https://xcanwin.github.io/manyoyo/en/guide/security).
|
|
85
126
|
|
|
86
|
-
##
|
|
127
|
+
## Docs
|
|
87
128
|
|
|
88
|
-
|
|
129
|
+
<https://xcanwin.github.io/manyoyo/en/>: [Install](https://xcanwin.github.io/manyoyo/en/guide/quick-start), [CLI reference](https://xcanwin.github.io/manyoyo/en/reference/cli-options), [Configuration](https://xcanwin.github.io/manyoyo/en/configuration/), [FAQ](https://xcanwin.github.io/manyoyo/en/troubleshooting/)
|
package/bin/manyoyo.js
CHANGED
|
@@ -26,6 +26,7 @@ const { listServeInstances, formatServeTable } = require('../lib/serve-instances
|
|
|
26
26
|
const { pruneDanglingImages: pruneDanglingImagesSafely } = require('../lib/image-prune');
|
|
27
27
|
const { runUninstall, readPid, defaultIsManyoyoServe, defaultKill } = require('../lib/uninstall');
|
|
28
28
|
const appUpdate = require('../lib/app-update');
|
|
29
|
+
const { getGithubFetch } = require('../lib/net-proxy');
|
|
29
30
|
const { createUpdateChecker } = require('../lib/update-check');
|
|
30
31
|
const { getLoginTokenDir, issueLoginToken } = require('../lib/login-token');
|
|
31
32
|
const { launchApp, getAppStatePath, openBrowser } = require('../lib/app-launcher');
|
|
@@ -48,7 +49,7 @@ const { applyAptMirror } = require('../lib/mirrors');
|
|
|
48
49
|
const containerState = require('../lib/container-state');
|
|
49
50
|
const { buildExecArgs, resolveContainerId } = require('../lib/container-exec');
|
|
50
51
|
const { createNetworkManager, NETWORK_NAME } = require('../lib/container-network');
|
|
51
|
-
const { normalizePolicy } = require('../lib/network-policy');
|
|
52
|
+
const { normalizePolicy, isUnrestricted: isUnrestrictedPolicy } = require('../lib/network-policy');
|
|
52
53
|
const { withEnvEndpoints } = require('../lib/network-endpoints');
|
|
53
54
|
const { resolveManageOptions } = require('../lib/container-manage-options');
|
|
54
55
|
const {
|
|
@@ -353,7 +354,7 @@ function installServeProcessDiagnostics(logger) {
|
|
|
353
354
|
* @property {string} [containerRuntime] - 容器运行时(auto/docker/podman,默认 auto;仅全局配置生效)
|
|
354
355
|
* @property {string} [autostart] - 容器每次启动时由容器内 init 执行的 bash 脚本(新建容器时写入状态目录)
|
|
355
356
|
* @property {boolean} [autostartOnServe] - serve 启动时自动拉起此容器
|
|
356
|
-
* @property {object} [network] - 网络策略(preset /
|
|
357
|
+
* @property {object} [network] - 网络策略(preset / outbound / inbound / expose,见 lib/network-policy.js),不写为默认收紧
|
|
357
358
|
* @property {{apt?: string, npm?: string, pip?: string}} [mirrors] - 容器内 apt/npm/pip 软件源(http/https URL,空/缺省为官方默认;仅全局配置生效)
|
|
358
359
|
* @property {boolean} [updateCheck] - serve 是否每天检查一次新版本(默认 true;仅全局配置生效,请求不附带任何本机信息)
|
|
359
360
|
* @property {number} [cacheTTL] - 缓存过期天数
|
|
@@ -836,6 +837,18 @@ async function prepareRuntimeForUpdate(configuredRuntime) {
|
|
|
836
837
|
return true;
|
|
837
838
|
}
|
|
838
839
|
|
|
840
|
+
// 网络失败后的“下一步”:先说代理,再给手动下载升级包的办法(地址写完整,能直接复制)
|
|
841
|
+
function updateNetworkNextSteps(proxy, release) {
|
|
842
|
+
const lines = [proxy && proxy.proxyEnv
|
|
843
|
+
? `当前代理: ${proxy.display},请确认代理可用`
|
|
844
|
+
: '有代理的话先在终端设置: export https_proxy=http://127.0.0.1:7890'];
|
|
845
|
+
const names = appUpdate.releaseAssetNames(release ? release.version : '<版本>', appUpdate.arch(), appUpdate.platformOs());
|
|
846
|
+
lines.push(`也可以手动下载这两个文件到同一目录,再执行: ${MANYOYO_NAME} update --file <下载的 ${names.app}>`);
|
|
847
|
+
const base = release ? `https://github.com/xcanwin/manyoyo/releases/download/v${release.version}` : 'https://github.com/xcanwin/manyoyo/releases/latest';
|
|
848
|
+
lines.push(release ? `${base}/${names.app}` : base, release ? `${base}/${names.sums}` : '');
|
|
849
|
+
return `\n ${lines.filter(Boolean).join('\n ')}`;
|
|
850
|
+
}
|
|
851
|
+
|
|
839
852
|
async function updateOfflineInstall(mode, options, globalConfig) {
|
|
840
853
|
const { appRoot } = mode;
|
|
841
854
|
if (options.rollback) {
|
|
@@ -847,14 +860,33 @@ async function updateOfflineInstall(mode, options, globalConfig) {
|
|
|
847
860
|
|
|
848
861
|
const installed = appUpdate.currentVersion(appRoot) || mode.version;
|
|
849
862
|
console.log(`${CYAN}🔄 当前版本: ${installed}${NC}`);
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
863
|
+
let result;
|
|
864
|
+
if (options.file) {
|
|
865
|
+
const info = appUpdate.parseAppFileName(options.file);
|
|
866
|
+
if (info && appUpdate.compareVersions(info.version, installed) <= 0) {
|
|
867
|
+
console.log(`${GREEN}✅ 已是最新版本 ${installed}${NC}`);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
console.log(`${CYAN}⬇️ 安装本地升级包 ${path.basename(options.file)}${NC}`);
|
|
871
|
+
result = await appUpdate.installAppFile({ appRoot, file: path.resolve(options.file), targetOs: appUpdate.platformOs() });
|
|
872
|
+
} else {
|
|
873
|
+
const github = getGithubFetch();
|
|
874
|
+
if (github.notice) console.log(`${CYAN}🌐 ${github.notice}${NC}`);
|
|
875
|
+
let release = null;
|
|
876
|
+
try {
|
|
877
|
+
console.log(`${CYAN}🔄 正在查询最新版本...${NC}`);
|
|
878
|
+
release = await appUpdate.fetchLatestRelease({ fetchImpl: github.fetch });
|
|
879
|
+
if (appUpdate.compareVersions(release.version, installed) <= 0) {
|
|
880
|
+
console.log(`${GREEN}✅ 已是最新版本 ${installed}${NC}`);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
console.log(`${CYAN}⬇️ 发现新版本 ${release.version},只下载 manyoyo 本体(数十 MB)${NC}`);
|
|
884
|
+
result = await appUpdate.installAppUpdate({ appRoot, release, fetchImpl: github.fetch, targetOs: appUpdate.platformOs(), log: line => console.log(` ${line}`) });
|
|
885
|
+
} catch (error) {
|
|
886
|
+
if (error instanceof appUpdate.UpdateError && error.code === 'NETWORK') error.message += updateNetworkNextSteps(github.proxy, release);
|
|
887
|
+
throw error;
|
|
888
|
+
}
|
|
855
889
|
}
|
|
856
|
-
console.log(`${CYAN}⬇️ 发现新版本 ${release.version},只下载 manyoyo 本体(数十 MB)${NC}`);
|
|
857
|
-
const result = await appUpdate.installAppUpdate({ appRoot, release, targetOs: appUpdate.platformOs(), log: line => console.log(` ${line}`) });
|
|
858
890
|
console.log(`${GREEN}✅ 更新完成: ${installed} → ${result.version}(上一版本 ${result.previous || '无'} 已保留,可用 ${MANYOYO_NAME} update --rollback 回滚)${NC}`);
|
|
859
891
|
|
|
860
892
|
// Podman / VM 磁盘有变化只提示(需要新的完整包)
|
|
@@ -922,6 +954,10 @@ async function updateManyoyo(options = {}, globalConfig = {}) {
|
|
|
922
954
|
console.error(`${RED}❌ --rollback 只适用于离线包安装;npm 安装的版本请用 npm install -g @xcanwin/manyoyo@<版本> 回退。${NC}`);
|
|
923
955
|
process.exit(1);
|
|
924
956
|
}
|
|
957
|
+
if (options.file) {
|
|
958
|
+
console.error(`${RED}❌ --file 只适用于离线包安装;npm 安装请用 npm install -g @xcanwin/manyoyo 升级。${NC}`);
|
|
959
|
+
process.exit(1);
|
|
960
|
+
}
|
|
925
961
|
|
|
926
962
|
let isLocalFileInstall = false;
|
|
927
963
|
let currentVersion = 'unknown';
|
|
@@ -1187,7 +1223,8 @@ https://github.com/xcanwin/manyoyo
|
|
|
1187
1223
|
.helpGroup('日常:')
|
|
1188
1224
|
.description('升级到最新版本')
|
|
1189
1225
|
.option('--rollback', '回到上一版本')
|
|
1190
|
-
.
|
|
1226
|
+
.option('--file <path>', '安装手动下载的升级包(manyoyo-<版本>-<系统>-<架构>-app.tar.gz,同目录需有 SHA256SUMS)')
|
|
1227
|
+
.action(options => selectAction('update', { update: true, rollback: Boolean(options.rollback), file: options.file || '' }));
|
|
1191
1228
|
|
|
1192
1229
|
program.command('uninstall')
|
|
1193
1230
|
.helpGroup('日常:')
|
|
@@ -2104,8 +2141,8 @@ async function createNewContainer(runtime) {
|
|
|
2104
2141
|
|
|
2105
2142
|
await ensureRunImage(runtime);
|
|
2106
2143
|
|
|
2107
|
-
const networkPolicy = await withEnvEndpoints(normalizePolicy(runtime.network), [...userEnvLines, ...containerState.readEnvFilePaths(runtime.envFiles || []).flatMap(file => file.entries.map(entry => `${entry.key}=${entry.value}`))]);
|
|
2108
2144
|
const networkManager = createCliNetworkManager(runtime);
|
|
2145
|
+
const networkPolicy = await withEnvEndpoints(await networkManager.resolveRefs(normalizePolicy(runtime.network)), [...userEnvLines, ...containerState.readEnvFilePaths(runtime.envFiles || []).flatMap(file => file.entries.map(entry => `${entry.key}=${entry.value}`))]);
|
|
2109
2146
|
if (!hasNetworkArg([...(runtime.contModeArgs || []), ...(runtime.containerExtraArgs || [])])) {
|
|
2110
2147
|
try {
|
|
2111
2148
|
await networkManager.ensureBridgeNetwork();
|
|
@@ -2119,7 +2156,7 @@ async function createNewContainer(runtime) {
|
|
|
2119
2156
|
envFiles: runtime.envFiles,
|
|
2120
2157
|
autostart: runtime.autostart,
|
|
2121
2158
|
network: networkPolicy,
|
|
2122
|
-
netRequired: networkPolicy
|
|
2159
|
+
netRequired: !isUnrestrictedPolicy(networkPolicy),
|
|
2123
2160
|
meta: { name: runtime.containerName }
|
|
2124
2161
|
});
|
|
2125
2162
|
|
|
@@ -2434,7 +2471,7 @@ async function runWebServerMode(runtime) {
|
|
|
2434
2471
|
currentVersion: require('../package.json').version,
|
|
2435
2472
|
installMode: appUpdate.detectInstallMode({ scriptPath: __filename }).mode,
|
|
2436
2473
|
enabled: UPDATE_CHECK_ENABLED,
|
|
2437
|
-
fetchLatest: () => appUpdate.fetchLatestRelease()
|
|
2474
|
+
fetchLatest: () => appUpdate.fetchLatestRelease({ fetchImpl: getGithubFetch().fetch })
|
|
2438
2475
|
});
|
|
2439
2476
|
|
|
2440
2477
|
// 启动时检查 Playwright 当前模式是否还活着,失效就回退到默认模式(只警告,不阻塞启动)
|
package/lib/app-update.js
CHANGED
|
@@ -18,12 +18,30 @@ const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
|
18
18
|
const FETCH_TIMEOUT_MS = 15000;
|
|
19
19
|
|
|
20
20
|
class UpdateError extends Error {
|
|
21
|
-
constructor(code, message) {
|
|
21
|
+
constructor(code, message, extra = {}) {
|
|
22
22
|
super(message);
|
|
23
23
|
this.code = code;
|
|
24
|
+
Object.assign(this, extra);
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
// 网络错误码 → 中文原因;没见过的显示原始 code
|
|
29
|
+
function describeNetworkReason(error) {
|
|
30
|
+
const code = error && (error.cause && error.cause.code || error.code) || '';
|
|
31
|
+
const name = error && (error.cause && error.cause.name || error.name) || '';
|
|
32
|
+
if (/TIMEOUT|ETIMEDOUT|ABORT/i.test(code) || /TimeoutError|AbortError/.test(name)) return '连接超时';
|
|
33
|
+
if (code === 'ECONNRESET') return '连接被重置';
|
|
34
|
+
if (code === 'ECONNREFUSED') return '连接被拒绝';
|
|
35
|
+
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN') return '域名解析失败';
|
|
36
|
+
if (/CERT|SELF_SIGNED|UNABLE_TO_VERIFY|ERR_TLS/i.test(code)) return '证书校验失败';
|
|
37
|
+
return code || name || '网络错误';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function networkError(url, error, prefix = '连不上') {
|
|
41
|
+
const host = (() => { try { return new URL(url).host; } catch (e) { return String(url); } })();
|
|
42
|
+
return new UpdateError('NETWORK', `${prefix} ${host}(${describeNetworkReason(error)})\n 地址: ${url}`, { url });
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
function parseVersion(text) {
|
|
28
46
|
const match = String(text || '').trim().replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
29
47
|
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
@@ -80,7 +98,7 @@ async function request(fetchImpl, url, options = {}) {
|
|
|
80
98
|
headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'manyoyo-update', ...(options.headers || {}) }
|
|
81
99
|
});
|
|
82
100
|
} catch (error) {
|
|
83
|
-
throw
|
|
101
|
+
throw networkError(url, error);
|
|
84
102
|
}
|
|
85
103
|
}
|
|
86
104
|
|
|
@@ -135,7 +153,7 @@ function releaseAssetNames(version, targetArch, targetOs = 'macos') {
|
|
|
135
153
|
|
|
136
154
|
async function fetchText(fetchImpl, url) {
|
|
137
155
|
const response = await request(fetchImpl, url);
|
|
138
|
-
if (!response.ok) throw new UpdateError('HTTP', `下载 ${url.split('/').pop()} 失败(HTTP ${response.status}
|
|
156
|
+
if (!response.ok) throw new UpdateError('HTTP', `下载 ${url.split('/').pop()} 失败(HTTP ${response.status})。\n 地址: ${url}`, { url });
|
|
139
157
|
return response.text();
|
|
140
158
|
}
|
|
141
159
|
|
|
@@ -183,6 +201,44 @@ function pruneApps(appRoot, keep) {
|
|
|
183
201
|
}
|
|
184
202
|
}
|
|
185
203
|
|
|
204
|
+
/**
|
|
205
|
+
* 解开已校验过的 -app.tar.gz 并切换 current(网络下载与 --file 共用)。包不完整/内容不符都在切换前中止。
|
|
206
|
+
*/
|
|
207
|
+
async function installFromArchive({ appRoot, archive, version, targetArch, targetOs, run = defaultRun }) {
|
|
208
|
+
const staging = path.join(appRoot, `.tmp-${version}-${process.pid}`);
|
|
209
|
+
try {
|
|
210
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
211
|
+
fs.mkdirSync(staging, { recursive: true });
|
|
212
|
+
run(TAR_COMMAND, ['-xzf', archive, '-C', staging]);
|
|
213
|
+
|
|
214
|
+
for (const required of ['node/bin/node', 'manyoyo/bin/manyoyo.js', 'manifest.json']) {
|
|
215
|
+
if (!fs.existsSync(path.join(staging, required))) throw new UpdateError('BAD_PACKAGE', `升级包不完整(缺少 ${required}),已中止升级。`);
|
|
216
|
+
}
|
|
217
|
+
let manifest;
|
|
218
|
+
try {
|
|
219
|
+
manifest = JSON.parse(fs.readFileSync(path.join(staging, 'manifest.json'), 'utf-8'));
|
|
220
|
+
} catch (error) {
|
|
221
|
+
throw new UpdateError('BAD_PACKAGE', '升级包的 manifest.json 无法解析,已中止升级。');
|
|
222
|
+
}
|
|
223
|
+
if (manifest.version !== version || manifest.kind !== 'app' || manifest.arch !== targetArch || (manifest.os && manifest.os !== targetOs)) {
|
|
224
|
+
throw new UpdateError('BAD_PACKAGE', `升级包内容与预期不符(版本 ${manifest.version}、类型 ${manifest.kind}、架构 ${manifest.arch}、系统 ${manifest.os || targetOs}),已中止升级。`);
|
|
225
|
+
}
|
|
226
|
+
fs.writeFileSync(path.join(staging, '.installed'), await sha256File(path.join(staging, 'manifest.json')));
|
|
227
|
+
|
|
228
|
+
const previous = currentVersion(appRoot);
|
|
229
|
+
const target = path.join(appRoot, version);
|
|
230
|
+
if (version !== previous) fs.rmSync(target, { recursive: true, force: true });
|
|
231
|
+
if (version !== previous) fs.renameSync(staging, target);
|
|
232
|
+
switchCurrent(appRoot, version);
|
|
233
|
+
writeUpdateRecord(appRoot, { previous: previous && previous !== version ? previous : readUpdateRecord(appRoot).previous || '', current: version, updatedAt: new Date().toISOString() });
|
|
234
|
+
const record = readUpdateRecord(appRoot);
|
|
235
|
+
pruneApps(appRoot, [version, record.previous].filter(Boolean));
|
|
236
|
+
return { version, previous: record.previous || '', manifest };
|
|
237
|
+
} finally {
|
|
238
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
186
242
|
/**
|
|
187
243
|
* 下载并安装 -app.tar.gz,成功后切换 current。校验失败/包不完整都在切换前中止,不留半成品。
|
|
188
244
|
*/
|
|
@@ -194,57 +250,56 @@ async function installAppUpdate({ appRoot, release, fetchImpl = fetch, targetArc
|
|
|
194
250
|
throw new UpdateError('NO_ASSET', `Release ${release.version} 里没有适合这台机器(${targetOs}-${targetArch})的升级包(需要 ${names.app} 与 ${names.sums})。`);
|
|
195
251
|
}
|
|
196
252
|
|
|
197
|
-
log(`校验清单: ${
|
|
253
|
+
log(`校验清单: ${sumsUrl}`);
|
|
198
254
|
const sums = parseSha256Sums(await fetchText(fetchImpl, sumsUrl));
|
|
199
255
|
const expected = sums[names.app];
|
|
200
256
|
if (!expected) throw new UpdateError('CHECKSUM', `校验清单里没有 ${names.app},已中止升级。`);
|
|
201
257
|
|
|
202
258
|
const workDir = fs.mkdtempSync(path.join(tmpRoot, 'manyoyo-update-'));
|
|
203
|
-
const staging = path.join(appRoot, `.tmp-${release.version}-${process.pid}`);
|
|
204
259
|
try {
|
|
205
|
-
log(`下载升级包: ${names.app}`);
|
|
206
260
|
const archive = path.join(workDir, names.app);
|
|
207
261
|
try {
|
|
208
262
|
await downloadVerified({ url: appUrl, sha256: expected, dest: archive, fetchImpl: (url, options) => request(fetchImpl, url, { ...options, noTimeout: true }), log });
|
|
209
263
|
} catch (error) {
|
|
210
264
|
if (/SHA256 校验失败/.test(error.message)) throw new UpdateError('CHECKSUM', `升级包 SHA256 校验失败,已中止升级,当前版本不受影响。`);
|
|
211
265
|
if (error instanceof UpdateError) throw error;
|
|
212
|
-
throw
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
fs.rmSync(staging, { recursive: true, force: true });
|
|
216
|
-
fs.mkdirSync(staging, { recursive: true });
|
|
217
|
-
run(TAR_COMMAND, ['-xzf', archive, '-C', staging]);
|
|
218
|
-
|
|
219
|
-
for (const required of ['node/bin/node', 'manyoyo/bin/manyoyo.js', 'manifest.json']) {
|
|
220
|
-
if (!fs.existsSync(path.join(staging, required))) throw new UpdateError('BAD_PACKAGE', `升级包不完整(缺少 ${required}),已中止升级。`);
|
|
221
|
-
}
|
|
222
|
-
let manifest;
|
|
223
|
-
try {
|
|
224
|
-
manifest = JSON.parse(fs.readFileSync(path.join(staging, 'manifest.json'), 'utf-8'));
|
|
225
|
-
} catch (error) {
|
|
226
|
-
throw new UpdateError('BAD_PACKAGE', '升级包的 manifest.json 无法解析,已中止升级。');
|
|
266
|
+
throw networkError(appUrl, error, '下载中断,连不上');
|
|
227
267
|
}
|
|
228
|
-
if (manifest.version !== release.version || manifest.kind !== 'app' || manifest.arch !== targetArch || (manifest.os && manifest.os !== targetOs)) {
|
|
229
|
-
throw new UpdateError('BAD_PACKAGE', `升级包内容与预期不符(版本 ${manifest.version}、类型 ${manifest.kind}、架构 ${manifest.arch}、系统 ${manifest.os || targetOs}),已中止升级。`);
|
|
230
|
-
}
|
|
231
|
-
fs.writeFileSync(path.join(staging, '.installed'), await sha256File(path.join(staging, 'manifest.json')));
|
|
232
268
|
|
|
233
|
-
|
|
234
|
-
const target = path.join(appRoot, release.version);
|
|
235
|
-
if (release.version !== previous) fs.rmSync(target, { recursive: true, force: true });
|
|
236
|
-
if (release.version !== previous) fs.renameSync(staging, target);
|
|
237
|
-
switchCurrent(appRoot, release.version);
|
|
238
|
-
writeUpdateRecord(appRoot, { previous: previous && previous !== release.version ? previous : readUpdateRecord(appRoot).previous || '', current: release.version, updatedAt: new Date().toISOString() });
|
|
239
|
-
const record = readUpdateRecord(appRoot);
|
|
240
|
-
pruneApps(appRoot, [release.version, record.previous].filter(Boolean));
|
|
241
|
-
return { version: release.version, previous: record.previous || '', manifest };
|
|
269
|
+
return await installFromArchive({ appRoot, archive, version: release.version, targetArch, targetOs, run });
|
|
242
270
|
} finally {
|
|
243
271
|
fs.rmSync(workDir, { recursive: true, force: true });
|
|
244
|
-
fs.rmSync(staging, { recursive: true, force: true });
|
|
245
272
|
}
|
|
246
273
|
}
|
|
247
274
|
|
|
275
|
+
const APP_FILE_PATTERN = /^manyoyo-(\d+\.\d+\.\d+)-(macos|linux)-(arm64|x64)-app\.tar\.gz$/;
|
|
276
|
+
|
|
277
|
+
/** 从升级包文件名解析版本 / 系统 / 架构,不符合命名就返回 null。 */
|
|
278
|
+
function parseAppFileName(file) {
|
|
279
|
+
const match = path.basename(String(file)).match(APP_FILE_PATTERN);
|
|
280
|
+
return match ? { version: match[1], os: match[2], arch: match[3] } : null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* 安装手动下载的 -app.tar.gz:同目录的 SHA256SUMS 必须列出它且哈希一致(没有跳过校验的办法),之后与网络升级走同一段安装逻辑。
|
|
285
|
+
*/
|
|
286
|
+
async function installAppFile({ appRoot, file, targetArch = arch(), targetOs = 'macos', run = defaultRun }) {
|
|
287
|
+
const info = parseAppFileName(file);
|
|
288
|
+
if (!info) throw new UpdateError('BAD_FILE', `文件名不符合升级包命名(manyoyo-<版本>-<系统>-<架构>-app.tar.gz): ${path.basename(String(file))}`);
|
|
289
|
+
if (info.os !== targetOs || info.arch !== targetArch) {
|
|
290
|
+
throw new UpdateError('BAD_FILE', `这个升级包是 ${info.os}-${info.arch} 的,这台机器需要 ${targetOs}-${targetArch}。`);
|
|
291
|
+
}
|
|
292
|
+
if (!fs.existsSync(file)) throw new UpdateError('BAD_FILE', `找不到文件: ${file}`);
|
|
293
|
+
const sumsFile = path.join(path.dirname(path.resolve(file)), 'SHA256SUMS');
|
|
294
|
+
if (!fs.existsSync(sumsFile)) {
|
|
295
|
+
throw new UpdateError('CHECKSUM', `同目录下没有 SHA256SUMS,无法校验。\n 请从 https://github.com/${REPO}/releases/download/v${info.version}/SHA256SUMS 下载,放到升级包旁边再试。`);
|
|
296
|
+
}
|
|
297
|
+
const expected = parseSha256Sums(fs.readFileSync(sumsFile, 'utf-8'))[path.basename(file)];
|
|
298
|
+
if (!expected) throw new UpdateError('CHECKSUM', `SHA256SUMS 里没有 ${path.basename(file)},已中止升级。`);
|
|
299
|
+
if (await sha256File(file) !== expected) throw new UpdateError('CHECKSUM', '升级包 SHA256 校验失败,已中止升级,当前版本不受影响。');
|
|
300
|
+
return installFromArchive({ appRoot, archive: file, version: info.version, targetArch, targetOs, run });
|
|
301
|
+
}
|
|
302
|
+
|
|
248
303
|
function rollbackApp({ appRoot }) {
|
|
249
304
|
const current = currentVersion(appRoot);
|
|
250
305
|
const record = readUpdateRecord(appRoot);
|
|
@@ -334,6 +389,9 @@ module.exports = {
|
|
|
334
389
|
parseSha256Sums,
|
|
335
390
|
releaseAssetNames,
|
|
336
391
|
installAppUpdate,
|
|
392
|
+
installAppFile,
|
|
393
|
+
parseAppFileName,
|
|
394
|
+
networkError,
|
|
337
395
|
rollbackApp,
|
|
338
396
|
currentVersion,
|
|
339
397
|
describeRuntimeChange,
|