@xcanwin/manyoyo 8.5.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
@@ -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');
@@ -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
- console.log(`${CYAN}🔄 正在查询最新版本...${NC}`);
851
- const release = await appUpdate.fetchLatestRelease();
852
- if (appUpdate.compareVersions(release.version, installed) <= 0) {
853
- console.log(`${GREEN}✅ 已是最新版本 ${installed}${NC}`);
854
- 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
+ }
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
- .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 || '' }));
1191
1228
 
1192
1229
  program.command('uninstall')
1193
1230
  .helpGroup('日常:')
@@ -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 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,183 @@
1
+ 'use strict';
2
+
3
+ // 访问 GitHub 的请求读取代理:环境变量(https_proxy 等)或 macOS 系统代理。
4
+ // Node 内置 fetch 不读代理变量,所以这里按请求构造带代理的 fetch,绝不修改全局(serve 进程里还有访问本机/容器的请求)。
5
+
6
+ const http = require('http');
7
+ const https = require('https');
8
+ const { Readable } = require('stream');
9
+ const { spawnSync } = require('child_process');
10
+
11
+ const MAX_REDIRECTS = 5;
12
+ const IDLE_TIMEOUT_MS = 60000;
13
+
14
+ function pick(env, names) {
15
+ for (const name of names) {
16
+ const value = String(env[name] || env[name.toUpperCase()] || '').trim();
17
+ if (value) return value;
18
+ }
19
+ return '';
20
+ }
21
+
22
+ // 去掉账号密码,只留 scheme://host:port,用于打印
23
+ function displayProxy(url) {
24
+ try {
25
+ const parsed = new URL(url);
26
+ return `${parsed.protocol}//${parsed.host}`;
27
+ } catch (error) {
28
+ return '';
29
+ }
30
+ }
31
+
32
+ function normalizeProxyUrl(value) {
33
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `http://${value}`;
34
+ }
35
+
36
+ /**
37
+ * 解析 macOS `scutil --proxy` 的输出。返回 { url, noProxy } 或 { unsupported } 或 null。
38
+ * 只认 HTTPS 代理(没有就退到 HTTP 代理);PAC / 自动代理不解析。
39
+ */
40
+ function parseScutilProxy(text) {
41
+ const values = {};
42
+ const exceptions = [];
43
+ let inExceptions = false;
44
+ for (const rawLine of String(text || '').split('\n')) {
45
+ const line = rawLine.trim();
46
+ if (/^ExceptionsList\s*:/.test(line)) { inExceptions = true; continue; }
47
+ if (inExceptions) {
48
+ const item = line.match(/^\d+\s*:\s*(.+)$/);
49
+ if (item) { exceptions.push(item[1].trim()); continue; }
50
+ inExceptions = false;
51
+ }
52
+ const pair = line.match(/^(\w+)\s*:\s*(.+)$/);
53
+ if (pair) values[pair[1]] = pair[2].trim();
54
+ }
55
+ const noProxy = exceptions.map(item => item.replace(/^\*\./, '.')).join(',');
56
+ for (const prefix of ['HTTPS', 'HTTP']) {
57
+ if (values[`${prefix}Enable`] === '1' && values[`${prefix}Proxy`] && /^\d+$/.test(values[`${prefix}Port`] || '')) {
58
+ return { url: `http://${values[`${prefix}Proxy`]}:${values[`${prefix}Port`]}`, noProxy };
59
+ }
60
+ }
61
+ if (values.ProxyAutoConfigEnable === '1' || values.SOCKSEnable === '1') return { unsupported: true };
62
+ return null;
63
+ }
64
+
65
+ function readScutilProxy() {
66
+ const result = spawnSync('/usr/sbin/scutil', ['--proxy'], { encoding: 'utf-8', timeout: 3000 });
67
+ return result.status === 0 ? result.stdout : '';
68
+ }
69
+
70
+ /**
71
+ * 找出当前该用的代理。优先级:环境变量 > macOS 系统代理 > 无。
72
+ * 返回 { proxyEnv, source: 'env'|'system'|null, display, notice };notice 是需要告诉用户的一句话(如暂不支持 socks)。
73
+ */
74
+ function resolveProxy({ env = process.env, platform = process.platform, readSystemProxy = readScutilProxy } = {}) {
75
+ const noProxy = pick(env, ['no_proxy']);
76
+ const candidates = [pick(env, ['https_proxy']), pick(env, ['http_proxy']), pick(env, ['all_proxy'])].filter(Boolean);
77
+ let notice = '';
78
+ for (const candidate of candidates) {
79
+ const url = normalizeProxyUrl(candidate);
80
+ if (/^socks/i.test(url)) {
81
+ notice = '暂不支持 socks 代理,请改用 http 代理端口。';
82
+ continue;
83
+ }
84
+ if (!/^https?:\/\//i.test(url) || !displayProxy(url)) continue;
85
+ return { proxyEnv: { HTTPS_PROXY: url, HTTP_PROXY: url, ...(noProxy ? { NO_PROXY: noProxy } : {}) }, source: 'env', display: displayProxy(url), notice };
86
+ }
87
+ if (platform === 'darwin') {
88
+ let parsed = null;
89
+ try {
90
+ parsed = parseScutilProxy(readSystemProxy());
91
+ } catch (error) {
92
+ parsed = null;
93
+ }
94
+ if (parsed && parsed.url) {
95
+ const merged = [noProxy, parsed.noProxy].filter(Boolean).join(',');
96
+ return { proxyEnv: { HTTPS_PROXY: parsed.url, HTTP_PROXY: parsed.url, ...(merged ? { NO_PROXY: merged } : {}) }, source: 'system', display: displayProxy(parsed.url), notice };
97
+ }
98
+ if (parsed && parsed.unsupported) notice = '系统代理用了自动配置或 socks,暂不支持,请在终端设置 https_proxy 指向 http 代理端口。';
99
+ }
100
+ return { proxyEnv: null, source: null, display: '', notice };
101
+ }
102
+
103
+ // Agent 的 proxyEnv 选项:Node 22.21+ / 24.5+ 才有
104
+ function supportsProxyEnv(version = process.versions.node) {
105
+ const [major, minor] = String(version).split('.').map(Number);
106
+ if (major >= 25) return true;
107
+ if (major === 24) return minor >= 5;
108
+ if (major === 22) return minor >= 21;
109
+ return false;
110
+ }
111
+
112
+ function requestOnce(url, options, agents) {
113
+ const target = new URL(url);
114
+ const client = target.protocol === 'http:' ? http : https;
115
+ const agent = target.protocol === 'http:' ? agents.http : agents.https;
116
+ return new Promise((resolve, reject) => {
117
+ const req = client.request(target, { method: options.method || 'GET', headers: options.headers || {}, agent, signal: options.signal }, resolve);
118
+ // 连接或下载中途卡住(代理停止转发等)时不能永远挂着:空闲超过 60 秒按超时处理
119
+ req.setTimeout(IDLE_TIMEOUT_MS, () => req.destroy(Object.assign(new Error('空闲超时'), { code: 'ETIMEDOUT' })));
120
+ req.on('error', reject);
121
+ req.end();
122
+ });
123
+ }
124
+
125
+ /**
126
+ * 带代理的最小 fetch 兼容实现(status / ok / headers.get / json / text / body)。
127
+ * 手动跟随最多 5 次重定向,每一跳都由 Agent 按 NO_PROXY 重新判断是否走代理;HTTPS 目标走 CONNECT 隧道,证书照常校验。
128
+ */
129
+ function createProxyFetch(proxyEnv, agentOptions = {}) {
130
+ const agents = {
131
+ http: new http.Agent({ proxyEnv, keepAlive: false }),
132
+ https: new https.Agent({ proxyEnv, keepAlive: false, ...agentOptions })
133
+ };
134
+ return async function proxyFetch(url, options = {}) {
135
+ let current = String(url);
136
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {
137
+ const res = await requestOnce(current, options, agents);
138
+ const location = res.headers.location;
139
+ if (res.statusCode >= 300 && res.statusCode < 400 && location && options.redirect !== 'manual') {
140
+ res.resume();
141
+ const next = new URL(location, current);
142
+ // 跨站跳转不带凭据类请求头
143
+ if (next.host !== new URL(current).host && options.headers) {
144
+ options = { ...options, headers: Object.fromEntries(Object.entries(options.headers).filter(([key]) => !/^(authorization|cookie)$/i.test(key))) };
145
+ }
146
+ current = next.toString();
147
+ continue;
148
+ }
149
+ const headers = { get: name => { const value = res.headers[String(name).toLowerCase()]; return Array.isArray(value) ? value.join(', ') : value === undefined ? null : value; } };
150
+ const readText = async () => {
151
+ const chunks = [];
152
+ for await (const chunk of res) chunks.push(chunk);
153
+ return Buffer.concat(chunks).toString('utf-8');
154
+ };
155
+ return {
156
+ status: res.statusCode,
157
+ ok: res.statusCode >= 200 && res.statusCode < 300,
158
+ headers,
159
+ url: current,
160
+ body: Readable.toWeb(res),
161
+ text: readText,
162
+ json: async () => JSON.parse(await readText())
163
+ };
164
+ }
165
+ throw Object.assign(new Error('重定向次数过多'), { cause: { code: 'TOO_MANY_REDIRECTS' } });
166
+ };
167
+ }
168
+
169
+ /**
170
+ * 访问 GitHub 用的 fetch:有可用代理时返回带代理的版本,否则返回内置 fetch。
171
+ * 返回 { fetch, proxy, notice };notice 是要打印给用户的提示(使用了哪个代理 / 为什么没用)。
172
+ */
173
+ function getGithubFetch({ env = process.env, platform = process.platform, readSystemProxy, nodeVersion = process.versions.node, baseFetch = globalThis.fetch } = {}) {
174
+ const proxy = resolveProxy({ env, platform, readSystemProxy });
175
+ if (!proxy.proxyEnv) return { fetch: baseFetch, proxy, notice: proxy.notice };
176
+ if (!supportsProxyEnv(nodeVersion)) {
177
+ return { fetch: baseFetch, proxy, notice: '当前 Node 版本不支持代理,请升级到 Node 22.21+ 或 24+。' };
178
+ }
179
+ const label = proxy.source === 'system' ? '使用系统代理' : '使用代理';
180
+ return { fetch: createProxyFetch(proxy.proxyEnv), proxy, notice: `${label}: ${proxy.display}` };
181
+ }
182
+
183
+ module.exports = { resolveProxy, parseScutilProxy, displayProxy, supportsProxyEnv, createProxyFetch, getGithubFetch };
@@ -612,7 +612,7 @@ class PlaywrightPlugin {
612
612
  async startChrome() {
613
613
  const { port } = this.config;
614
614
  const candidates = this.devtoolsCandidates();
615
- const enableHint = '请在 Chrome 地址栏进入 chrome://inspect/#remote-debugging,勾选“Allow remote debugging”(Chrome 没开就先打开它),然后重试。\n'
615
+ const enableHint = '请在 Chrome 地址栏进入:\nchrome://inspect/#remote-debugging\n勾选“Allow remote debugging”(Chrome 没开就先打开它),然后重试。\n'
616
616
  + '(自定义用户目录时,用 plugins.playwright.devtoolsActivePortPath 指定 DevToolsActivePort 文件。)';
617
617
  const target = findDevToolsActivePort(candidates);
618
618
  if (!target) {
@@ -821,7 +821,9 @@ class PlaywrightPlugin {
821
821
  if (!result.ok) {
822
822
  if (mode === 'chrome') {
823
823
  this.err('[up] chrome 失败: 连不上 Chrome 的调试接口。');
824
- this.err('请确认已在 chrome://inspect/#remote-debugging 开启远程调试,并在 Chrome 弹出的“要允许远程调试吗?”里点“允许”(60 秒内),然后重试。');
824
+ this.err('请确认已在 Chrome 地址栏进入:');
825
+ this.err('chrome://inspect/#remote-debugging');
826
+ this.err('开启远程调试,并在 Chrome 弹出的“要允许远程调试吗?”里点“允许”(60 秒内),然后重试。');
825
827
  } else {
826
828
  this.err(`[up] ${mode} 失败: 浏览器服务已启动,但打不开页面(${result.error})。`);
827
829
  this.err(tailText(this.logPath(), 10));
package/package.json CHANGED
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "8.5.0",
3
+ "version": "8.5.1",
4
4
  "imageVersion": "2.2.0-common",
5
5
  "playwrightCliVersion": "0.1.19",
6
6
  "patchrightCoreVersion": "1.63.0",
7
7
  "patchrightCoreIntegrity": "sha512-nuRJVTMVKoOFzY8vmwFqJZR3F0trUCKiPzDSgCOPq/yHHTefm9czILV23dXJsi5mvS7sCy7U1CqG7edA/21DtQ==",
8
- "description": "MANYOYO – open-source AI agent sandbox: run AI coding agents (Claude Code / Codex / Gemini) safely in Docker/Podman",
8
+ "description": "MANYOYO(慢悠悠)– open-source AI coding agent sandbox: run Claude Code, Codex, Gemini CLI and OpenCode in YOLO mode safely inside Docker/Podman",
9
9
  "keywords": [
10
10
  "manyoyo",
11
11
  "ai-agent",
12
+ "ai-sandbox",
13
+ "coding-agent",
12
14
  "agent-cli",
13
15
  "security-sandbox",
14
16
  "claude-code",
@@ -23,7 +25,7 @@
23
25
  "type": "git",
24
26
  "url": "git+https://github.com/xcanwin/manyoyo.git"
25
27
  },
26
- "license": "MIT",
28
+ "license": "Apache-2.0",
27
29
  "author": "xcanwin",
28
30
  "type": "commonjs",
29
31
  "main": "bin/manyoyo.js",
package/README.en.md DELETED
@@ -1,88 +0,0 @@
1
- <p align="center">
2
- <img src="./assets/manyoyo-logo-09-cyberpunk-terminal.svg" alt="MANYOYO logo" width="560" />
3
- </p>
4
-
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">Run Claude Code / Codex / Gemini / OpenCode in YOLO mode inside a container: let the AI go wild without hurting your computer.</p>
8
- <p align="center">
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
- <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>
12
- </p>
13
-
14
- <p align="center">
15
- <a href="README.md">中文</a> |
16
- <a href="README.en.md"><b>English</b></a>
17
- </p>
18
- <p align="center">
19
- Docs: <a href="https://xcanwin.github.io/manyoyo/en/">https://xcanwin.github.io/manyoyo/en/</a>
20
- </p>
21
-
22
- ---
23
-
24
- ## Install
25
-
26
- ### macOS / Linux (recommended)
27
-
28
- ```bash
29
- curl -fsSL https://github.com/xcanwin/manyoyo/raw/main/scripts/install.sh | sh
30
- ```
31
-
32
- 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.
33
-
34
- ### Already have Node.js and Docker/Podman
35
-
36
- ```bash
37
- npm install -g @xcanwin/manyoyo
38
- manyoyo
39
- ```
40
-
41
- ## Daily use
42
-
43
- ```bash
44
- manyoyo # open the web UI
45
- manyoyo update # upgrade
46
- manyoyo uninstall # uninstall (config and data are kept by default)
47
- ```
48
-
49
- ## Command line (optional)
50
-
51
- ```bash
52
- manyoyo init all # import the agent configs already on this machine
53
- manyoyo run -y c # start Claude Code in YOLO mode inside the sandbox
54
- ```
55
-
56
- The default image `ghcr.io/xcanwin/manyoyo` is pulled automatically on first use; run `manyoyo build --iv 2.1.1-common` only if you need a custom image. More commands: [CLI reference](https://xcanwin.github.io/manyoyo/en/reference/cli-options).
57
-
58
- ## What it does
59
-
60
- - **Multiple agents**: `claude`, `codex`, `gemini`, `opencode` behind one entry point
61
- - **Container isolation**: Docker / Podman; the agent only sees the work directory you give it
62
- - **Web UI**: chat, browse files and open a terminal in the browser, phone friendly
63
- - **One config**: run profiles, environment variables, mounts and image arguments in `~/.manyoyo/manyoyo.json`
64
- - **One command** to install, upgrade and uninstall
65
-
66
- | | Running the agent CLI directly | MANYOYO |
67
- | --- | --- | --- |
68
- | Host exposure | High | Lower |
69
- | Run boundary | Scattered | Centralised in the container and config |
70
- | Reproducible environment | Weak | Strong (image + config) |
71
- | High-risk mode warnings | Left to the tool | Explicit YOLO / SOLO / sock warnings |
72
-
73
- ## Security notes
74
-
75
- 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).
76
-
77
- ## Docs
78
-
79
- - 中文: <https://xcanwin.github.io/manyoyo/>
80
- - English: <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/))
81
-
82
- ## License
83
-
84
- MIT
85
-
86
- ## Contributing
87
-
88
- [Issues](https://github.com/xcanwin/manyoyo/issues) and pull requests are welcome.