@xcanwin/manyoyo 8.4.0 → 8.5.1

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 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>MANYOYO – open-source sandbox for running AI coding agents (Claude Code / Codex / Gemini) safely in Docker/Podman</b></p>
7
- <p align="center">开源 AI Agent 沙箱:让 Claude Code / Codex / Gemini / OpenCode 在容器里放心跑 YOLO 模式,AI 随便折腾,伤不到你的电脑。</p>
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-MIT-yellow.svg" /></a>
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="README.md"><b>中文</b></a> |
16
- <a href="README.en.md">English</a>
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
- ### macOS / Linux(推荐)
23
+ ## 安装(二选一即可)
24
+
25
+ ### 1. macOS / Linux(推荐)
27
26
 
28
27
  ```bash
29
- curl -fsSL https://github.com/xcanwin/manyoyo/raw/main/scripts/install.sh | sh
28
+ curl -fsSL https://xcanwin.github.io/manyoyo/install.sh | sh
30
29
  ```
31
30
 
32
31
  装完按屏幕提示完成配置。macOS 不需要预装任何东西。Linux:缺少容器环境时,安装器会说明原因,征得你同意后用 sudo 自动安装。
33
32
 
34
- ### 已有 Node.js 和 Docker/Podman
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
- - 中文:<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/))
80
- - English: <https://xcanwin.github.io/manyoyo/en/>
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
- MIT
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
- 欢迎提交 [Issue](https://github.com/xcanwin/manyoyo/issues) 和 Pull Request。
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
@@ -9,6 +9,7 @@ const net = require('net');
9
9
  const readline = require('readline');
10
10
  const { Command, Help } = require('commander');
11
11
  const { startWebServer } = require('../lib/web/server');
12
+ const { isSidecarName } = require('../lib/egress-sidecar');
12
13
  const { buildContainerRunArgs, buildContainerRunCommand, runWithEnvFile, hasNetworkArg } = require('../lib/container-run');
13
14
  const { getManyoyoConfigPath, readManyoyoConfig, syncGlobalImageVersion } = require('../lib/global-config');
14
15
  const { resolveUpdateImageVersion } = require('../lib/image-version-policy');
@@ -25,6 +26,7 @@ const { listServeInstances, formatServeTable } = require('../lib/serve-instances
25
26
  const { pruneDanglingImages: pruneDanglingImagesSafely } = require('../lib/image-prune');
26
27
  const { runUninstall, readPid, defaultIsManyoyoServe, defaultKill } = require('../lib/uninstall');
27
28
  const appUpdate = require('../lib/app-update');
29
+ const { getGithubFetch } = require('../lib/net-proxy');
28
30
  const { createUpdateChecker } = require('../lib/update-check');
29
31
  const { getLoginTokenDir, issueLoginToken } = require('../lib/login-token');
30
32
  const { launchApp, getAppStatePath, openBrowser } = require('../lib/app-launcher');
@@ -835,6 +837,18 @@ async function prepareRuntimeForUpdate(configuredRuntime) {
835
837
  return true;
836
838
  }
837
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
+
838
852
  async function updateOfflineInstall(mode, options, globalConfig) {
839
853
  const { appRoot } = mode;
840
854
  if (options.rollback) {
@@ -846,14 +860,33 @@ async function updateOfflineInstall(mode, options, globalConfig) {
846
860
 
847
861
  const installed = appUpdate.currentVersion(appRoot) || mode.version;
848
862
  console.log(`${CYAN}🔄 当前版本: ${installed}${NC}`);
849
- console.log(`${CYAN}🔄 正在查询最新版本...${NC}`);
850
- const release = await appUpdate.fetchLatestRelease();
851
- if (appUpdate.compareVersions(release.version, installed) <= 0) {
852
- console.log(`${GREEN}✅ 已是最新版本 ${installed}${NC}`);
853
- return;
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
+ }
854
889
  }
855
- console.log(`${CYAN}⬇️ 发现新版本 ${release.version},只下载 manyoyo 本体(数十 MB)${NC}`);
856
- const result = await appUpdate.installAppUpdate({ appRoot, release, targetOs: appUpdate.platformOs(), log: line => console.log(` ${line}`) });
857
890
  console.log(`${GREEN}✅ 更新完成: ${installed} → ${result.version}(上一版本 ${result.previous || '无'} 已保留,可用 ${MANYOYO_NAME} update --rollback 回滚)${NC}`);
858
891
 
859
892
  // Podman / VM 磁盘有变化只提示(需要新的完整包)
@@ -921,6 +954,10 @@ async function updateManyoyo(options = {}, globalConfig = {}) {
921
954
  console.error(`${RED}❌ --rollback 只适用于离线包安装;npm 安装的版本请用 npm install -g @xcanwin/manyoyo@<版本> 回退。${NC}`);
922
955
  process.exit(1);
923
956
  }
957
+ if (options.file) {
958
+ console.error(`${RED}❌ --file 只适用于离线包安装;npm 安装请用 npm install -g @xcanwin/manyoyo 升级。${NC}`);
959
+ process.exit(1);
960
+ }
924
961
 
925
962
  let isLocalFileInstall = false;
926
963
  let currentVersion = 'unknown';
@@ -994,6 +1031,7 @@ function getContList() {
994
1031
  const image = cols[4] || '';
995
1032
  // include manyoyo runtime containers (image match)
996
1033
  // and plugin containers (both legacy manyoyo-* and new my-* prefixes)
1034
+ if (isSidecarName(name)) return false; // 域名白名单的过滤代理容器不是用户的会话容器
997
1035
  return image.includes('manyoyo') || name.startsWith('manyoyo-') || name.startsWith('my-');
998
1036
  });
999
1037
 
@@ -1185,7 +1223,8 @@ https://github.com/xcanwin/manyoyo
1185
1223
  .helpGroup('日常:')
1186
1224
  .description('升级到最新版本')
1187
1225
  .option('--rollback', '回到上一版本')
1188
- .action(options => selectAction('update', { update: true, rollback: Boolean(options.rollback) }));
1226
+ .option('--file <path>', '安装手动下载的升级包(manyoyo-<版本>-<系统>-<架构>-app.tar.gz,同目录需有 SHA256SUMS)')
1227
+ .action(options => selectAction('update', { update: true, rollback: Boolean(options.rollback), file: options.file || '' }));
1189
1228
 
1190
1229
  program.command('uninstall')
1191
1230
  .helpGroup('日常:')
@@ -2432,7 +2471,7 @@ async function runWebServerMode(runtime) {
2432
2471
  currentVersion: require('../package.json').version,
2433
2472
  installMode: appUpdate.detectInstallMode({ scriptPath: __filename }).mode,
2434
2473
  enabled: UPDATE_CHECK_ENABLED,
2435
- fetchLatest: () => appUpdate.fetchLatestRelease()
2474
+ fetchLatest: () => appUpdate.fetchLatestRelease({ fetchImpl: getGithubFetch().fetch })
2436
2475
  });
2437
2476
 
2438
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 new UpdateError('NETWORK', `无法连接 GitHub(${error && error.cause && error.cause.code || error.name || '网络错误'})。请检查网络或代理后重试。`);
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(`校验清单: ${names.sums}`);
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 new UpdateError('NETWORK', `下载升级包失败:${error.message}`);
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
- const previous = currentVersion(appRoot);
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,
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const { domainAllowed } = require('./network-policy');
4
+
5
+ // 容器里的 Chrome 启动后会自己连一些谷歌的后台服务(更新、账号同步、字体 / 资源)。白名单下它们必然被拦,
6
+ // 对用户没有意义:网络分区的“最近被拦截”把它们折叠成“浏览器后台请求”,对话里也不为它们提示。
7
+ // 数据只写在这一处。
8
+ const BROWSER_BACKGROUND_DOMAINS = [
9
+ 'accounts.google.com',
10
+ 'update.googleapis.com',
11
+ 'www.google.com',
12
+ 'www.gstatic.com',
13
+ 'dl.google.com',
14
+ 'clients2.google.com',
15
+ 'android.clients.google.com',
16
+ 'content-autofill.googleapis.com'
17
+ ];
18
+
19
+ function isBrowserBackground(host) {
20
+ return domainAllowed(BROWSER_BACKGROUND_DOMAINS, host);
21
+ }
22
+
23
+ module.exports = { BROWSER_BACKGROUND_DOMAINS, isBrowserBackground };
@@ -0,0 +1,34 @@
1
+ # manyoyo 容器内的 env 加载:init.sh 启动自启动前加载,终端里的 `reload-env` 也用它。
2
+ # 由宿主机以只读目录挂到 /run/manyoyo-sys/env.sh,容器内不可改。
3
+ # 与宿主机侧(lib/env-text.js)同一语法:一行 KEY=VALUE,# 注释与空行忽略,可选 `export ` 前缀与 `=` 两侧空白,
4
+ # 值两端成对的引号去掉;不展开变量、不执行命令;key 必须合法
5
+ load_env() {
6
+ local file=$1 line key val
7
+ [ -f "$file" ] || return 0
8
+ while IFS= read -r line || [ -n "$line" ]; do
9
+ line=${line%$'\r'}
10
+ line="${line#"${line%%[![:space:]]*}"}"
11
+ line="${line%"${line##*[![:space:]]}"}"
12
+ case $line in ''|'#'*) continue ;; esac
13
+ if [[ $line =~ ^export[[:space:]]+ ]]; then line=${line#"${BASH_REMATCH[0]}"}; fi
14
+ [[ $line =~ ^([^=[:space:]]+)[[:space:]]*=[[:space:]]*(.*)$ ]] || continue
15
+ key=${BASH_REMATCH[1]}
16
+ val=${BASH_REMATCH[2]}
17
+ [[ $key =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
18
+ if [ "${#val}" -ge 2 ]; then
19
+ case $val in
20
+ \"*\") val=${val:1:${#val}-2} ;;
21
+ \'*\') val=${val:1:${#val}-2} ;;
22
+ esac
23
+ fi
24
+ export "$key=$val"
25
+ done < "$file"
26
+ }
27
+
28
+ # 已打开的终端里让最新的环境变量生效(优先级与新开终端一致:managed < 环境变量文件 < 用户 env)
29
+ reload-env() {
30
+ local sys=${MANYOYO_SYS_DIR:-/run/manyoyo-sys} box=${MANYOYO_BOX_DIR:-/run/manyoyo}
31
+ load_env "$sys/managed.env"
32
+ load_env "$sys/files.env"
33
+ load_env "$box/env"
34
+ }
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ // 容器 id:随机 16 位十六进制,状态目录、过滤代理的映射与拒绝记录都按它索引。
4
+ // 单独成文件:过滤代理 sidecar 里只带得动这几个无依赖的小文件。
5
+ const ID_RE = /^[0-9a-f]{16}$/;
6
+
7
+ function isValidId(id) {
8
+ return typeof id === 'string' && ID_RE.test(id);
9
+ }
10
+
11
+ module.exports = { ID_RE, isValidId };
@@ -10,30 +10,7 @@ log() { printf '%s %s\n' "$(date '+%F %T')" "$*" >> "$LOG" 2>/dev/null; }
10
10
 
11
11
  trap 'kill -TERM -1 2>/dev/null; exit 0' TERM INT
12
12
 
13
- # 与宿主机侧(lib/env-text.js)同一语法:一行 KEY=VALUE,# 注释与空行忽略,可选 `export ` 前缀与 `=` 两侧空白,
14
- # 值两端成对的引号去掉;不展开变量、不执行命令;key 必须合法
15
- load_env() {
16
- local file=$1 line key val
17
- [ -f "$file" ] || return 0
18
- while IFS= read -r line || [ -n "$line" ]; do
19
- line=${line%$'\r'}
20
- line="${line#"${line%%[![:space:]]*}"}"
21
- line="${line%"${line##*[![:space:]]}"}"
22
- case $line in ''|'#'*) continue ;; esac
23
- if [[ $line =~ ^export[[:space:]]+ ]]; then line=${line#"${BASH_REMATCH[0]}"}; fi
24
- [[ $line =~ ^([^=[:space:]]+)[[:space:]]*=[[:space:]]*(.*)$ ]] || continue
25
- key=${BASH_REMATCH[1]}
26
- val=${BASH_REMATCH[2]}
27
- [[ $key =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
28
- if [ "${#val}" -ge 2 ]; then
29
- case $val in
30
- \"*\") val=${val:1:${#val}-2} ;;
31
- \'*\') val=${val:1:${#val}-2} ;;
32
- esac
33
- fi
34
- export "$key=$val"
35
- done < "$file"
36
- }
13
+ . "$SYS/env.sh"
37
14
 
38
15
  # 日志超过 256 KiB 时只保留尾部 128 KiB
39
16
  if [ -f "$LOG" ] && [ "$(stat -c %s "$LOG" 2>/dev/null || echo 0)" -gt 262144 ]; then
@@ -44,8 +21,12 @@ run_autostart() {
44
21
  if [ -e "$SYS/net-required" ]; then
45
22
  log "等待 manyoyo 下发网络规则"
46
23
  while [ ! -e "$GATE/ready" ]; do sleep 0.5; done
24
+ log "网络规则已就绪"
25
+ fi
26
+ if [ ! -s "$BOX/autostart.sh" ]; then
27
+ log "没有自启动命令"
28
+ exit 0
47
29
  fi
48
- [ -s "$BOX/autostart.sh" ] || exit 0
49
30
  load_env "$SYS/managed.env"
50
31
  load_env "$SYS/files.env"
51
32
  load_env "$BOX/env"