@xcanwin/manyoyo 3.7.8 → 3.8.3

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
@@ -13,6 +13,25 @@
13
13
 
14
14
  ---
15
15
 
16
+ ## 2 分钟快速开始
17
+
18
+ **Docker 用户:**
19
+ ```bash
20
+ npm install -g @xcanwin/manyoyo # 安装
21
+ manyoyo --ib --iv 1.7.0 # 构建镜像
22
+ manyoyo -y c # 运行 Claude Code YOLO 模式
23
+ ```
24
+
25
+ **Podman 用户:**
26
+ ```bash
27
+ npm install -g @xcanwin/manyoyo # 安装
28
+ podman pull ubuntu:24.04 # 拉取基础镜像
29
+ manyoyo --ib --iv 1.7.0 # 构建镜像
30
+ manyoyo -y c # 运行 Claude Code YOLO 模式
31
+ ```
32
+
33
+ ---
34
+
16
35
  **MANYOYO** 是一款 AI 智能体提效安全沙箱,安全、高效、省 token,专为 Agent YOLO 模式设计,保障宿主机安全。
17
36
 
18
37
  预装常见 Agent 与工具,进一步节省 token。循环自由切换 Agent 和 `/bin/bash`,进一步提效。
@@ -62,7 +81,7 @@ podman pull ubuntu:24.04
62
81
 
63
82
  ```bash
64
83
  # 使用 manyoyo 构建镜像(推荐,自动使用缓存加速)
65
- manyoyo --ib --iv 1.6.4 # 默认构建 full 版本(推荐,建议指定版本号)
84
+ manyoyo --ib --iv 1.7.0 # 默认构建 full 版本(推荐,建议指定版本号)
66
85
  manyoyo --ib --iba TOOL=common # 构建常见组件版本(python,nodejs,claude)
67
86
  manyoyo --ib --iba TOOL=go,codex,java,gemini # 构建自定义组件版本
68
87
  manyoyo --ib --iba GIT_SSL_NO_VERIFY=true # 构建 full 版本且跳过git的ssl验证
@@ -201,7 +220,7 @@ manyoyo --ef openai_[gpt]_codex -x codex
201
220
  "hostPath": "/path/to/project", // 默认宿主机工作目录
202
221
  "containerPath": "/path/to/project", // 默认容器工作目录
203
222
  "imageName": "localhost/xcanwin/manyoyo", // 默认镜像名称
204
- "imageVersion": "1.6.4-full", // 默认镜像版本
223
+ "imageVersion": "1.7.0-full", // 默认镜像版本
205
224
  "containerMode": "common", // 容器嵌套模式 (common, dind, sock)
206
225
 
207
226
  // 环境变量配置
@@ -225,9 +244,21 @@ manyoyo --ef openai_[gpt]_codex -x codex
225
244
  - **覆盖型参数**:命令行 > 运行配置 > 全局配置 > 默认值
226
245
  - **合并型参数**:全局配置 + 运行配置 + 命令行(按顺序累加)
227
246
 
228
- 覆盖型参数包括:`containerName`, `hostPath`, `containerPath`, `imageName`, `imageVersion`, `containerMode`, `shellPrefix`, `shell`, `yolo`, `quiet`
229
-
230
- 合并型参数包括:`envFile`, `env`, `volumes`, `imageBuildArgs`
247
+ #### 配置合并规则表
248
+
249
+ | 参数类型 | 参数名 | 合并行为 | 示例 |
250
+ |---------|--------|---------|------|
251
+ | 覆盖型 | `containerName` | 取最高优先级的值 | CLI `-n test` 覆盖配置文件中的值 |
252
+ | 覆盖型 | `hostPath` | 取最高优先级的值 | 默认为当前目录 |
253
+ | 覆盖型 | `containerPath` | 取最高优先级的值 | 默认与 hostPath 相同 |
254
+ | 覆盖型 | `imageName` | 取最高优先级的值 | 默认 `localhost/xcanwin/manyoyo` |
255
+ | 覆盖型 | `imageVersion` | 取最高优先级的值 | 如 `1.7.0-full` |
256
+ | 覆盖型 | `containerMode` | 取最高优先级的值 | `common`, `dind`, `sock` |
257
+ | 覆盖型 | `yolo` | 取最高优先级的值 | `c`, `gm`, `cx`, `oc` |
258
+ | 合并型 | `env` | 数组累加合并 | 全局 + 运行配置 + CLI 的所有值 |
259
+ | 合并型 | `envFile` | 数组累加合并 | 所有环境文件依次加载 |
260
+ | 合并型 | `volumes` | 数组累加合并 | 所有挂载卷生效 |
261
+ | 合并型 | `imageBuildArgs` | 数组累加合并 | 所有构建参数生效 |
231
262
 
232
263
  #### 常用样例-全局
233
264
 
@@ -237,7 +268,7 @@ mkdir -p ~/.manyoyo/
237
268
  cat > ~/.manyoyo/manyoyo.json << 'EOF'
238
269
  {
239
270
  "imageName": "localhost/xcanwin/manyoyo",
240
- "imageVersion": "1.6.4-full"
271
+ "imageVersion": "1.7.0-full"
241
272
  }
242
273
  EOF
243
274
  ```
@@ -359,6 +390,8 @@ docker ps -a # 现在可以在容器内使用 docker 命令
359
390
  | `-y CLI` | `--yolo` | 无需确认运行 AI 智能体 |
360
391
  | `--show-config` | | 显示最终生效配置并退出 |
361
392
  | `--show-command` | | 显示将执行的命令并退出(存在容器时为 docker exec,不存在时为 docker run) |
393
+ | `--yes` | | 所有提示自动确认(用于CI/脚本) |
394
+ | `--rm-on-exit` | | 退出后自动删除容器(一次性模式) |
362
395
  | `--install NAME` | | 安装 manyoyo 命令 |
363
396
  | `-q LIST` | `--quiet` | 静默显示 |
364
397
  | `-r NAME` | `--run` | 加载运行配置(支持 `name` 或 `./path.json`) |
@@ -385,6 +418,53 @@ docker ps -a # 现在可以在容器内使用 docker 命令
385
418
  npm uninstall -g @xcanwin/manyoyo
386
419
  ```
387
420
 
421
+ ## 故障排查 FAQ
422
+
423
+ ### 镜像构建失败
424
+
425
+ **问题**:执行 `manyoyo --ib` 时报错
426
+
427
+ **解决方案**:
428
+ 1. 检查网络连接:`curl -I https://mirrors.tencent.com`
429
+ 2. 检查磁盘空间:`df -h`(需要至少 10GB 可用空间)
430
+ 3. 使用 `--yes` 跳过确认:`manyoyo --ib --iv 1.7.0 --yes`
431
+ 4. 如果在国外,可能需要修改镜像源(配置文件中设置 `nodeMirror`)
432
+
433
+ ### 镜像拉取失败
434
+
435
+ **问题**:提示 `pinging container registry localhost failed`
436
+
437
+ **解决方案**:
438
+ 1. 本地镜像需要先构建:`manyoyo --ib --iv 1.7.0`
439
+ 2. 或修改配置文件 `~/.manyoyo/manyoyo.json` 中的 `imageVersion`
440
+
441
+ ### 容器启动失败
442
+
443
+ **问题**:容器无法启动或立即退出
444
+
445
+ **解决方案**:
446
+ 1. 查看容器日志:`docker logs <容器名>`
447
+ 2. 检查端口冲突:`docker ps -a`
448
+ 3. 检查权限问题:确保当前用户有 Docker/Podman 权限
449
+
450
+ ### 权限不足
451
+
452
+ **问题**:提示 `permission denied` 或无法访问 Docker
453
+
454
+ **解决方案**:
455
+ 1. 将用户添加到 docker 组:`sudo usermod -aG docker $USER`
456
+ 2. 重新登录或运行:`newgrp docker`
457
+ 3. 或使用 `sudo` 运行命令
458
+
459
+ ### 环境变量未生效
460
+
461
+ **问题**:容器内无法读取设置的环境变量
462
+
463
+ **解决方案**:
464
+ 1. 检查环境文件格式(支持 `KEY=VALUE` 或 `export KEY=VALUE`)
465
+ 2. 确认文件路径正确(`--ef name` 对应 `~/.manyoyo/env/name.env`)
466
+ 3. 使用 `--show-config` 查看最终生效的配置
467
+
388
468
  ## 许可证
389
469
 
390
470
  MIT
package/bin/manyoyo.js CHANGED
@@ -8,6 +8,7 @@ const { execSync, spawnSync } = require('child_process');
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
  const os = require('os');
11
+ const crypto = require('crypto');
11
12
  const readline = require('readline');
12
13
  const { Command } = require('commander');
13
14
  const JSON5 = require('json5');
@@ -23,6 +24,17 @@ function formatDate() {
23
24
  return `${month}${day}-${hour}${minute}`;
24
25
  }
25
26
 
27
+ // ==============================================================================
28
+ // Configuration Constants
29
+ // ==============================================================================
30
+
31
+ const CONFIG = {
32
+ CACHE_TTL_DAYS: 2, // 缓存过期天数
33
+ CONTAINER_READY_MAX_RETRIES: 30, // 容器就绪最大重试次数
34
+ CONTAINER_READY_INITIAL_DELAY: 100, // 容器就绪初始延迟(ms)
35
+ CONTAINER_READY_MAX_DELAY: 2000, // 容器就绪最大延迟(ms)
36
+ };
37
+
26
38
  // Default configuration
27
39
  let CONTAINER_NAME = `myy-${formatDate()}`;
28
40
  let HOST_PATH = process.cwd();
@@ -40,8 +52,11 @@ let CONTAINER_ENVS = [];
40
52
  let CONTAINER_VOLUMES = [];
41
53
  let MANYOYO_NAME = "manyoyo";
42
54
  let CONT_MODE = "";
55
+ let CONT_MODE_ARGS = [];
43
56
  let QUIET = {};
44
57
  let SHOW_COMMAND = false;
58
+ let YES_MODE = false;
59
+ let RM_ON_EXIT = false;
45
60
 
46
61
  // Color definitions using ANSI codes
47
62
  const RED = '\x1b[0;31m';
@@ -62,10 +77,85 @@ function sleep(ms) {
62
77
  return new Promise(resolve => setTimeout(resolve, ms));
63
78
  }
64
79
 
80
+ /**
81
+ * 计算文件的 SHA256 哈希值(跨平台)
82
+ * @param {string} filePath - 文件路径
83
+ * @returns {string} SHA256 哈希值(十六进制)
84
+ */
85
+ function getFileSha256(filePath) {
86
+ const fileBuffer = fs.readFileSync(filePath);
87
+ const hashSum = crypto.createHash('sha256');
88
+ hashSum.update(fileBuffer);
89
+ return hashSum.digest('hex');
90
+ }
91
+
92
+ /**
93
+ * 敏感信息脱敏(用于 --show-config 输出)
94
+ * @param {Object} obj - 配置对象
95
+ * @returns {Object} 脱敏后的配置对象
96
+ */
97
+ function sanitizeSensitiveData(obj) {
98
+ const sensitiveKeys = ['KEY', 'TOKEN', 'SECRET', 'PASSWORD', 'AUTH', 'CREDENTIAL'];
99
+
100
+ function sanitizeValue(key, value) {
101
+ if (typeof value !== 'string') return value;
102
+ const upperKey = key.toUpperCase();
103
+ if (sensitiveKeys.some(k => upperKey.includes(k))) {
104
+ if (value.length <= 8) return '****';
105
+ return value.slice(0, 4) + '****' + value.slice(-4);
106
+ }
107
+ return value;
108
+ }
109
+
110
+ function sanitizeArray(arr) {
111
+ return arr.map(item => {
112
+ if (typeof item === 'string' && item.includes('=')) {
113
+ const idx = item.indexOf('=');
114
+ const key = item.slice(0, idx);
115
+ const value = item.slice(idx + 1);
116
+ return `${key}=${sanitizeValue(key, value)}`;
117
+ }
118
+ return item;
119
+ });
120
+ }
121
+
122
+ const result = {};
123
+ for (const [key, value] of Object.entries(obj)) {
124
+ if (Array.isArray(value)) {
125
+ result[key] = sanitizeArray(value);
126
+ } else if (typeof value === 'object' && value !== null) {
127
+ result[key] = sanitizeSensitiveData(value);
128
+ } else {
129
+ result[key] = sanitizeValue(key, value);
130
+ }
131
+ }
132
+ return result;
133
+ }
134
+
65
135
  // ==============================================================================
66
136
  // Configuration File Functions
67
137
  // ==============================================================================
68
138
 
139
+ /**
140
+ * @typedef {Object} Config
141
+ * @property {string} [containerName] - 容器名称
142
+ * @property {string} [hostPath] - 宿主机路径
143
+ * @property {string} [containerPath] - 容器路径
144
+ * @property {string} [imageName] - 镜像名称
145
+ * @property {string} [imageVersion] - 镜像版本
146
+ * @property {string[]} [env] - 环境变量数组
147
+ * @property {string[]} [envFile] - 环境文件数组
148
+ * @property {string[]} [volumes] - 挂载卷数组
149
+ * @property {string} [yolo] - YOLO 模式
150
+ * @property {string} [containerMode] - 容器模式
151
+ * @property {number} [cacheTTL] - 缓存过期天数
152
+ * @property {string} [nodeMirror] - Node.js 镜像源
153
+ */
154
+
155
+ /**
156
+ * 加载全局配置文件
157
+ * @returns {Config} 配置对象
158
+ */
69
159
  function loadConfig() {
70
160
  const configPath = path.join(os.homedir(), '.manyoyo', 'manyoyo.json');
71
161
  if (fs.existsSync(configPath)) {
@@ -187,6 +277,10 @@ async function askQuestion(prompt) {
187
277
  // Configuration Functions
188
278
  // ==============================================================================
189
279
 
280
+ /**
281
+ * 添加环境变量
282
+ * @param {string} env - 环境变量字符串 (KEY=VALUE)
283
+ */
190
284
  function addEnv(env) {
191
285
  const idx = env.indexOf('=');
192
286
  if (idx <= 0) {
@@ -276,7 +370,7 @@ function setYolo(cli) {
276
370
  break;
277
371
  case 'opencode':
278
372
  case 'oc':
279
- EXEC_COMMAND = "OPENCODE_PERMISSION='\"allow\"' opencode";
373
+ EXEC_COMMAND = "OPENCODE_PERMISSION='{\"*\":\"allow\"}' opencode";
280
374
  break;
281
375
  default:
282
376
  console.log(`${RED}⚠️ 未知LLM CLI: ${cli}${NC}`);
@@ -284,21 +378,33 @@ function setYolo(cli) {
284
378
  }
285
379
  }
286
380
 
381
+ /**
382
+ * 设置容器嵌套模式
383
+ * @param {string} mode - 模式名称 (common, dind, sock)
384
+ */
287
385
  function setContMode(mode) {
288
386
  switch (mode) {
289
387
  case 'common':
290
388
  CONT_MODE = "";
389
+ CONT_MODE_ARGS = [];
291
390
  break;
292
391
  case 'docker-in-docker':
293
392
  case 'dind':
294
393
  case 'd':
295
394
  CONT_MODE = "--privileged";
395
+ CONT_MODE_ARGS = ['--privileged'];
296
396
  console.log(`${GREEN}✅ 开启安全的容器嵌套容器模式, 手动在容器内启动服务: nohup dockerd &${NC}`);
297
397
  break;
298
398
  case 'mount-docker-socket':
299
399
  case 'sock':
300
400
  case 's':
301
401
  CONT_MODE = "--privileged --volume /var/run/docker.sock:/var/run/docker.sock --env DOCKER_HOST=unix:///var/run/docker.sock --env CONTAINER_HOST=unix:///var/run/docker.sock";
402
+ CONT_MODE_ARGS = [
403
+ '--privileged',
404
+ '--volume', '/var/run/docker.sock:/var/run/docker.sock',
405
+ '--env', 'DOCKER_HOST=unix:///var/run/docker.sock',
406
+ '--env', 'CONTAINER_HOST=unix:///var/run/docker.sock'
407
+ ];
302
408
  console.log(`${RED}⚠️ 开启危险的容器嵌套容器模式, 危害: 容器可访问宿主机文件${NC}`);
303
409
  break;
304
410
  default:
@@ -322,6 +428,19 @@ function dockerExec(cmd, options = {}) {
322
428
  }
323
429
  }
324
430
 
431
+ function showImagePullHint(err) {
432
+ const stderr = err && err.stderr ? err.stderr.toString() : '';
433
+ const stdout = err && err.stdout ? err.stdout.toString() : '';
434
+ const message = err && err.message ? err.message : '';
435
+ const combined = `${message}\n${stderr}\n${stdout}`;
436
+ if (!/localhost\/v2|pinging container registry localhost|connection refused|dial tcp .*:443/i.test(combined)) {
437
+ return;
438
+ }
439
+ const image = `${IMAGE_NAME}:${IMAGE_VERSION}`;
440
+ console.log(`${YELLOW}💡 提示: 本地未找到镜像 ${image},并且从 localhost 注册表拉取失败。${NC}`);
441
+ console.log(`${YELLOW} 你可以: 1) 更新 ~/.manyoyo/manyoyo.json 的 imageVersion 2) 或先执行 manyoyo --ib --iv <version> 构建镜像。${NC}`);
442
+ }
443
+
325
444
  function runCmd(cmd, args, options = {}) {
326
445
  const result = spawnSync(cmd, args, { encoding: 'utf-8', ...options });
327
446
  if (result.error) {
@@ -430,10 +549,24 @@ function pruneDanglingImages() {
430
549
  console.log(`${GREEN}✅ 清理完成${NC}`);
431
550
  }
432
551
 
552
+ /**
553
+ * 准备构建缓存(Node.js、JDT LSP、gopls)
554
+ * @param {string} imageTool - 构建工具类型
555
+ */
433
556
  async function prepareBuildCache(imageTool) {
434
557
  const cacheDir = path.join(__dirname, '../docker/cache');
435
558
  const timestampFile = path.join(cacheDir, '.timestamps.json');
436
- const cacheTTLDays = 2;
559
+
560
+ // 从配置文件读取 TTL,默认 2 天
561
+ const config = loadConfig();
562
+ const cacheTTLDays = config.cacheTTL || CONFIG.CACHE_TTL_DAYS;
563
+
564
+ // 镜像源优先级:用户配置 > 腾讯云 > 官方
565
+ const nodeMirrors = [
566
+ config.nodeMirror,
567
+ 'https://mirrors.tencent.com/nodejs-release',
568
+ 'https://nodejs.org/dist'
569
+ ].filter(Boolean);
437
570
 
438
571
  console.log(`\n${CYAN}准备构建缓存...${NC}`);
439
572
 
@@ -476,18 +609,46 @@ async function prepareBuildCache(imageTool) {
476
609
  const hasNodeCache = fs.existsSync(nodeCacheDir) && fs.readdirSync(nodeCacheDir).some(f => f.startsWith('node-') && f.includes(`linux-${archNode}`));
477
610
  if (!hasNodeCache || isExpired(nodeKey)) {
478
611
  console.log(`${YELLOW}下载 Node.js ${nodeVersion} (${archNode})...${NC}`);
479
- const mirror = 'https://mirrors.tencent.com/nodejs-release';
480
- try {
481
- const shasum = execSync(`curl -sL ${mirror}/latest-v${nodeVersion}.x/SHASUMS256.txt | grep linux-${archNode}.tar.gz | awk '{print $2}'`, { encoding: 'utf-8' }).trim();
482
- const nodeUrl = `${mirror}/latest-v${nodeVersion}.x/${shasum}`;
483
- const nodeTargetPath = path.join(nodeCacheDir, shasum);
484
- runCmd('curl', ['-fsSL', nodeUrl, '-o', nodeTargetPath], { stdio: 'inherit' });
485
- timestamps[nodeKey] = now.toISOString();
486
- fs.writeFileSync(timestampFile, JSON.stringify(timestamps, null, 4));
487
- console.log(`${GREEN}✓ Node.js 下载完成${NC}`);
488
- } catch (e) {
489
- console.error(`${RED}错误: Node.js 下载失败${NC}`);
490
- throw e;
612
+
613
+ // 尝试多个镜像源
614
+ let downloadSuccess = false;
615
+ for (const mirror of nodeMirrors) {
616
+ try {
617
+ console.log(`${BLUE}尝试镜像源: ${mirror}${NC}`);
618
+ const shasumUrl = `${mirror}/latest-v${nodeVersion}.x/SHASUMS256.txt`;
619
+ const shasumContent = execSync(`curl -sL ${shasumUrl}`, { encoding: 'utf-8' });
620
+ const shasumLine = shasumContent.split('\n').find(line => line.includes(`linux-${archNode}.tar.gz`));
621
+ if (!shasumLine) continue;
622
+
623
+ const [expectedHash, fileName] = shasumLine.trim().split(/\s+/);
624
+ const nodeUrl = `${mirror}/latest-v${nodeVersion}.x/${fileName}`;
625
+ const nodeTargetPath = path.join(nodeCacheDir, fileName);
626
+
627
+ // 下载文件
628
+ runCmd('curl', ['-fsSL', nodeUrl, '-o', nodeTargetPath], { stdio: 'inherit' });
629
+
630
+ // SHA256 校验(使用 Node.js crypto 模块,跨平台)
631
+ const actualHash = getFileSha256(nodeTargetPath);
632
+ if (actualHash !== expectedHash) {
633
+ console.log(`${RED}SHA256 校验失败,删除文件${NC}`);
634
+ fs.unlinkSync(nodeTargetPath);
635
+ continue;
636
+ }
637
+
638
+ console.log(`${GREEN}✓ SHA256 校验通过${NC}`);
639
+ timestamps[nodeKey] = now.toISOString();
640
+ fs.writeFileSync(timestampFile, JSON.stringify(timestamps, null, 4));
641
+ console.log(`${GREEN}✓ Node.js 下载完成${NC}`);
642
+ downloadSuccess = true;
643
+ break;
644
+ } catch (e) {
645
+ console.log(`${YELLOW}镜像源 ${mirror} 失败,尝试下一个...${NC}`);
646
+ }
647
+ }
648
+
649
+ if (!downloadSuccess) {
650
+ console.error(`${RED}错误: Node.js 下载失败(所有镜像源均不可用)${NC}`);
651
+ throw new Error('Node.js download failed');
491
652
  }
492
653
  } else {
493
654
  console.log(`${GREEN}✓ Node.js 缓存已存在${NC}`);
@@ -625,8 +786,10 @@ async function buildImage(IMAGE_BUILD_ARGS, imageName, imageVersion) {
625
786
  console.log(`${BLUE}准备执行命令:${NC}`);
626
787
  console.log(`${buildCmd}\n`);
627
788
 
628
- const reply = await askQuestion(`❔ 是否继续构建? [ 直接回车=继续, ctrl+c=取消 ]: `);
629
- console.log("");
789
+ if (!YES_MODE) {
790
+ await askQuestion(`❔ 是否继续构建? [ 直接回车=继续, ctrl+c=取消 ]: `);
791
+ console.log("");
792
+ }
630
793
 
631
794
  try {
632
795
  execSync(buildCmd, { stdio: 'inherit' });
@@ -702,6 +865,8 @@ function setupCommander() {
702
865
  .option('--install <name>', '安装manyoyo命令 (docker-cli-plugin)')
703
866
  .option('--show-config', '显示最终生效配置并退出')
704
867
  .option('--show-command', '显示将执行的 docker run 命令并退出')
868
+ .option('--yes', '所有提示自动确认 (用于CI/脚本)')
869
+ .option('--rm-on-exit', '退出后自动删除容器 (一次性模式)')
705
870
  .option('-q, --quiet <item>', '静默显示 (可多次使用: cnew,crm,tip,cmd,full)', (value, previous) => [...(previous || []), value], []);
706
871
 
707
872
  // Docker CLI plugin metadata check
@@ -801,6 +966,14 @@ function setupCommander() {
801
966
  }
802
967
  }
803
968
 
969
+ if (options.yes) {
970
+ YES_MODE = true;
971
+ }
972
+
973
+ if (options.rmOnExit) {
974
+ RM_ON_EXIT = true;
975
+ }
976
+
804
977
  if (options.showConfig) {
805
978
  const finalConfig = {
806
979
  hostPath: HOST_PATH,
@@ -824,7 +997,9 @@ function setupCommander() {
824
997
  suffix: EXEC_COMMAND_SUFFIX
825
998
  }
826
999
  };
827
- console.log(JSON.stringify(finalConfig, null, 4));
1000
+ // 敏感信息脱敏
1001
+ const sanitizedConfig = sanitizeSensitiveData(finalConfig);
1002
+ console.log(JSON.stringify(sanitizedConfig, null, 4));
828
1003
  process.exit(0);
829
1004
  }
830
1005
 
@@ -869,15 +1044,20 @@ function validateHostPath() {
869
1044
  }
870
1045
  }
871
1046
 
1047
+ /**
1048
+ * 等待容器就绪(使用指数退避算法)
1049
+ * @param {string} containerName - 容器名称
1050
+ */
872
1051
  async function waitForContainerReady(containerName) {
873
- const MAX_RETRIES = 50;
874
- let count = 0;
875
- while (true) {
1052
+ const MAX_RETRIES = CONFIG.CONTAINER_READY_MAX_RETRIES;
1053
+ let retryDelay = CONFIG.CONTAINER_READY_INITIAL_DELAY;
1054
+
1055
+ for (let count = 0; count < MAX_RETRIES; count++) {
876
1056
  try {
877
1057
  const status = getContainerStatus(containerName);
878
1058
 
879
1059
  if (status === 'running') {
880
- break;
1060
+ return;
881
1061
  }
882
1062
 
883
1063
  if (status === 'exited') {
@@ -886,39 +1066,41 @@ async function waitForContainerReady(containerName) {
886
1066
  process.exit(1);
887
1067
  }
888
1068
 
889
- await sleep(100);
890
- count++;
891
-
892
- if (count >= MAX_RETRIES) {
893
- console.log(`${RED}⚠️ 错误: 容器启动超时(当前状态: ${status})。${NC}`);
894
- dockerExecArgs(['logs', containerName], { stdio: 'inherit' });
895
- process.exit(1);
896
- }
1069
+ await sleep(retryDelay);
1070
+ retryDelay = Math.min(retryDelay * 2, CONFIG.CONTAINER_READY_MAX_DELAY);
897
1071
  } catch (e) {
898
- await sleep(100);
899
- count++;
900
- if (count >= MAX_RETRIES) {
901
- console.log(`${RED}⚠️ 错误: 容器启动超时。${NC}`);
902
- process.exit(1);
903
- }
1072
+ await sleep(retryDelay);
1073
+ retryDelay = Math.min(retryDelay * 2, CONFIG.CONTAINER_READY_MAX_DELAY);
904
1074
  }
905
1075
  }
1076
+
1077
+ console.log(`${RED}⚠️ 错误: 容器启动超时。${NC}`);
1078
+ process.exit(1);
906
1079
  }
907
1080
 
1081
+ /**
1082
+ * 创建新容器
1083
+ * @returns {Promise<string>} 默认命令
1084
+ */
908
1085
  async function createNewContainer() {
909
1086
  if ( !(QUIET.cnew || QUIET.full) ) console.log(`${CYAN}📦 manyoyo by xcanwin 正在创建新容器: ${YELLOW}${CONTAINER_NAME}${NC}`);
910
1087
 
911
1088
  EXEC_COMMAND = `${EXEC_COMMAND_PREFIX}${EXEC_COMMAND}${EXEC_COMMAND_SUFFIX}`;
912
1089
  const defaultCommand = EXEC_COMMAND;
913
1090
 
914
- const dockerRunCmd = buildDockerRunCmd();
915
-
916
1091
  if (SHOW_COMMAND) {
917
- console.log(dockerRunCmd);
1092
+ console.log(buildDockerRunCmd());
918
1093
  process.exit(0);
919
1094
  }
920
1095
 
921
- dockerExec(dockerRunCmd, { stdio: 'pipe' });
1096
+ // 使用数组参数执行命令(安全方式)
1097
+ try {
1098
+ const args = buildDockerRunArgs();
1099
+ dockerExecArgs(args, { stdio: 'pipe' });
1100
+ } catch (e) {
1101
+ showImagePullHint(e);
1102
+ throw e;
1103
+ }
922
1104
 
923
1105
  // Wait for container to be ready
924
1106
  await waitForContainerReady(CONTAINER_NAME);
@@ -926,17 +1108,45 @@ async function createNewContainer() {
926
1108
  return defaultCommand;
927
1109
  }
928
1110
 
929
- function buildDockerRunCmd() {
930
- // Build docker run command
1111
+ /**
1112
+ * 构建 Docker run 命令参数数组(安全方式,避免命令注入)
1113
+ * @returns {string[]} 命令参数数组
1114
+ */
1115
+ function buildDockerRunArgs() {
931
1116
  const fullImage = `${IMAGE_NAME}:${IMAGE_VERSION}`;
932
- const envArgs = CONTAINER_ENVS.join(' ');
933
- const volumeArgs = CONTAINER_VOLUMES.join(' ');
934
- const contModeArg = CONT_MODE || '';
935
-
936
- const safeLabelCmd = EXEC_COMMAND.replace(/[\r\n]/g, ' ').replace(/"/g, '\\"');
937
- const dockerRunCmd = `${DOCKER_CMD} run -d --name "${CONTAINER_NAME}" --entrypoint "" ${contModeArg} ${envArgs} ${volumeArgs} --volume "${HOST_PATH}:${CONTAINER_PATH}" --workdir "${CONTAINER_PATH}" --label "manyoyo.default_cmd=${safeLabelCmd}" "${fullImage}" tail -f /dev/null`;
1117
+ const safeLabelCmd = EXEC_COMMAND.replace(/[\r\n]/g, ' ');
1118
+
1119
+ const args = [
1120
+ 'run', '-d',
1121
+ '--name', CONTAINER_NAME,
1122
+ '--entrypoint', '',
1123
+ ...CONT_MODE_ARGS,
1124
+ ...CONTAINER_ENVS,
1125
+ ...CONTAINER_VOLUMES,
1126
+ '--volume', `${HOST_PATH}:${CONTAINER_PATH}`,
1127
+ '--workdir', CONTAINER_PATH,
1128
+ '--label', `manyoyo.default_cmd=${safeLabelCmd}`,
1129
+ fullImage,
1130
+ 'tail', '-f', '/dev/null'
1131
+ ];
1132
+
1133
+ return args;
1134
+ }
938
1135
 
939
- return dockerRunCmd;
1136
+ /**
1137
+ * 构建 Docker run 命令字符串(用于显示)
1138
+ * @returns {string} 命令字符串
1139
+ */
1140
+ function buildDockerRunCmd() {
1141
+ const args = buildDockerRunArgs();
1142
+ // 对包含空格或特殊字符的参数加引号
1143
+ const quotedArgs = args.map(arg => {
1144
+ if (arg.includes(' ') || arg.includes('"') || arg.includes('=')) {
1145
+ return `"${arg.replace(/"/g, '\\"')}"`;
1146
+ }
1147
+ return arg;
1148
+ });
1149
+ return `${DOCKER_CMD} ${quotedArgs.join(' ')}`;
940
1150
  }
941
1151
 
942
1152
  async function connectExistingContainer() {
@@ -996,7 +1206,17 @@ function executeInContainer(defaultCommand) {
996
1206
  }
997
1207
  }
998
1208
 
1209
+ /**
1210
+ * 处理会话退出后的交互
1211
+ * @param {string} defaultCommand - 默认命令
1212
+ */
999
1213
  async function handlePostExit(defaultCommand) {
1214
+ // --rm-on-exit 模式:自动删除容器
1215
+ if (RM_ON_EXIT) {
1216
+ removeContainer(CONTAINER_NAME);
1217
+ return;
1218
+ }
1219
+
1000
1220
  getHelloTip(CONTAINER_NAME, defaultCommand);
1001
1221
 
1002
1222
  let tipAskKeep = `❔ 会话已结束。是否保留此后台容器 ${CONTAINER_NAME}? [ y=默认保留, n=删除, 1=首次命令进入, x=执行命令, i=交互式SHELL ]: `;
@@ -61,30 +61,32 @@ ARG TARGETARCH
61
61
  ARG NODE_VERSION=24
62
62
  ARG TOOL="full"
63
63
 
64
+ # 镜像源参数化(默认使用阿里云,可按需覆盖)
65
+ ARG APT_MIRROR=https://mirrors.aliyun.com
66
+ ARG NPM_REGISTRY=https://mirrors.tencent.com/npm/
67
+ ARG PIP_INDEX_URL=https://mirrors.tencent.com/pypi/simple
68
+
69
+ # 合并系统依赖安装为单层,减少镜像体积
64
70
  RUN <<EOX
65
- # 部署 system
71
+ # 配置 APT 镜像源
72
+ sed -i "s|http://[^/]*\.ubuntu\.com|${APT_MIRROR}|g" /etc/apt/sources.list.d/ubuntu.sources
66
73
 
67
- # 修复CA证书
68
- sed -i 's|http://[^/]*\.ubuntu\.com|https://mirrors.aliyun.com|g' /etc/apt/sources.list.d/ubuntu.sources
74
+ # 安装所有基础依赖(单次 apt-get 操作)
69
75
  apt-get -o Acquire::https::Verify-Peer=false update -y
70
- apt-get -o Acquire::https::Verify-Peer=false install -y --no-install-recommends ca-certificates openssl
71
- update-ca-certificates
76
+ apt-get -o Acquire::https::Verify-Peer=false install -y --no-install-recommends \
77
+ ca-certificates openssl curl wget net-tools iputils-ping \
78
+ git lsof socat ncat dnsutils ssh nano jq file tree ripgrep less bc xxd \
79
+ tar zip unzip gzip make sqlite3 supervisor
72
80
 
73
- # 安装基本依赖
74
- apt-get update -y
75
- apt-get install -y --no-install-recommends --reinstall ca-certificates openssl
81
+ # 更新 CA 证书
76
82
  update-ca-certificates
77
- apt-get install -y --no-install-recommends curl wget net-tools iputils-ping git lsof socat ncat dnsutils ssh \
78
- nano jq file tree ripgrep less bc xxd \
79
- tar zip unzip gzip make sqlite3 \
80
- supervisor
81
83
 
82
- # 安装 podman
84
+ # 安装 podman(条件)
83
85
  case ",$TOOL," in *,full,*|*,podman,*)
84
86
  apt-get install -y --no-install-recommends podman
85
87
  ;; esac
86
88
 
87
- # 安装 docker
89
+ # 安装 docker(条件)
88
90
  case ",$TOOL," in *,full,*|*,docker,*)
89
91
  apt-get install -y --no-install-recommends docker.io
90
92
  ;; esac
@@ -100,7 +102,7 @@ RUN <<EOX
100
102
  apt-get install -y --no-install-recommends python3.12 python3.12-dev python3.12-venv python3-pip
101
103
  ln -sf /usr/bin/python3 /usr/bin/python
102
104
  ln -sf /usr/bin/pip3 /usr/bin/pip
103
- pip config set global.index-url "https://mirrors.tencent.com/pypi/simple"
105
+ pip config set global.index-url "${PIP_INDEX_URL}"
104
106
 
105
107
  # 清理
106
108
  apt-get clean
@@ -113,7 +115,7 @@ ARG GIT_SSL_NO_VERIFY=false
113
115
 
114
116
  RUN <<EOX
115
117
  # 配置 node.js
116
- npm config set registry=https://mirrors.tencent.com/npm/
118
+ npm config set registry=${NPM_REGISTRY}
117
119
  npm install -g npm
118
120
 
119
121
  # 安装 LSP服务(python、typescript)
package/docs/README_EN.md CHANGED
@@ -7,12 +7,31 @@
7
7
  </p>
8
8
 
9
9
  <p align="center">
10
- <a href="README.md">中文</a> |
11
- <a href="docs/README_EN.md"><b>English</b></a>
10
+ <a href="../README.md">中文</a> |
11
+ <a href="README_EN.md"><b>English</b></a>
12
12
  </p>
13
13
 
14
14
  ---
15
15
 
16
+ ## 2-Minute Quick Start
17
+
18
+ **Docker users:**
19
+ ```bash
20
+ npm install -g @xcanwin/manyoyo # Install
21
+ manyoyo --ib --iv 1.7.0 # Build image
22
+ manyoyo -y c # Run Claude Code YOLO mode
23
+ ```
24
+
25
+ **Podman users:**
26
+ ```bash
27
+ npm install -g @xcanwin/manyoyo # Install
28
+ podman pull ubuntu:24.04 # Pull base image
29
+ manyoyo --ib --iv 1.7.0 # Build image
30
+ manyoyo -y c # Run Claude Code YOLO mode
31
+ ```
32
+
33
+ ---
34
+
16
35
  **MANYOYO** is an AI agent security sandbox that is safe, efficient, and token-saving. Designed specifically for Agent YOLO mode to protect the host machine.
17
36
 
18
37
  Pre-installed with common agents and tools to further save tokens. Freely switch between agents and `/bin/bash` in a loop for enhanced efficiency.
@@ -62,7 +81,7 @@ Only one of the following commands needs to be executed:
62
81
 
63
82
  ```bash
64
83
  # Build using manyoyo (Recommended, auto-cache enabled)
65
- manyoyo --ib --iv 1.6.4 # Build full version by default (Recommended, specify version)
84
+ manyoyo --ib --iv 1.7.0 # Build full version by default (Recommended, specify version)
66
85
  manyoyo --ib --iba TOOL=common # Build common version (python,nodejs,claude)
67
86
  manyoyo --ib --iba TOOL=go,codex,java,gemini # Build custom combination
68
87
  manyoyo --ib --iba GIT_SSL_NO_VERIFY=true # Build the full version and skip Git SSL verification
@@ -201,7 +220,7 @@ Refer to `config.example.json` for all available options:
201
220
  "hostPath": "/path/to/project", // Default host working directory
202
221
  "containerPath": "/path/to/project", // Default container working directory
203
222
  "imageName": "localhost/xcanwin/manyoyo", // Default image name
204
- "imageVersion": "1.6.4-full", // Default image version
223
+ "imageVersion": "1.7.0-full", // Default image version
205
224
  "containerMode": "common", // Container nesting mode (common, dind, sock)
206
225
 
207
226
  // Environment variable configuration
@@ -225,9 +244,21 @@ Refer to `config.example.json` for all available options:
225
244
  - **Override parameters**: Command line > Run config > Global config > Defaults
226
245
  - **Merge parameters**: Global config + Run config + Command line (concatenated in order)
227
246
 
228
- Override parameters include: `containerName`, `hostPath`, `containerPath`, `imageName`, `imageVersion`, `containerMode`, `shellPrefix`, `shell`, `yolo`, `quiet`
229
-
230
- Merge parameters include: `envFile`, `env`, `volumes`, `imageBuildArgs`
247
+ #### Configuration Merge Rules
248
+
249
+ | Type | Parameter | Behavior | Example |
250
+ |------|-----------|----------|---------|
251
+ | Override | `containerName` | Use highest priority value | CLI `-n test` overrides config file |
252
+ | Override | `hostPath` | Use highest priority value | Defaults to current directory |
253
+ | Override | `containerPath` | Use highest priority value | Defaults to same as hostPath |
254
+ | Override | `imageName` | Use highest priority value | Default `localhost/xcanwin/manyoyo` |
255
+ | Override | `imageVersion` | Use highest priority value | e.g., `1.7.0-full` |
256
+ | Override | `containerMode` | Use highest priority value | `common`, `dind`, `sock` |
257
+ | Override | `yolo` | Use highest priority value | `c`, `gm`, `cx`, `oc` |
258
+ | Merge | `env` | Array concatenation | All values from global + run + CLI |
259
+ | Merge | `envFile` | Array concatenation | All env files loaded in order |
260
+ | Merge | `volumes` | Array concatenation | All volume mounts apply |
261
+ | Merge | `imageBuildArgs` | Array concatenation | All build args apply |
231
262
 
232
263
  #### Common Examples-Global
233
264
 
@@ -237,7 +268,7 @@ mkdir -p ~/.manyoyo/
237
268
  cat > ~/.manyoyo/manyoyo.json << 'EOF'
238
269
  {
239
270
  "imageName": "localhost/xcanwin/manyoyo",
240
- "imageVersion": "1.6.4-full"
271
+ "imageVersion": "1.7.0-full"
241
272
  }
242
273
  EOF
243
274
  ```
@@ -359,6 +390,8 @@ docker ps -a # Now you can use docker commands inside the container
359
390
  | `-y CLI` | `--yolo` | Run AI agent without confirmation |
360
391
  | `--show-config` | | Print final effective config and exit |
361
392
  | `--show-command` | | Print the command to be executed and exit (docker exec if container exists, otherwise docker run) |
393
+ | `--yes` | | Auto-confirm all prompts (for CI/scripts) |
394
+ | `--rm-on-exit` | | Auto-remove container on exit (one-time mode) |
362
395
  | `--install NAME` | | Install manyoyo command |
363
396
  | `-q LIST` | `--quiet` | Quiet output |
364
397
  | `-r NAME` | `--run` | Load run configuration (supports `name` or `./path.json`) |
@@ -385,6 +418,53 @@ docker ps -a # Now you can use docker commands inside the container
385
418
  npm uninstall -g @xcanwin/manyoyo
386
419
  ```
387
420
 
421
+ ## Troubleshooting FAQ
422
+
423
+ ### Image Build Failed
424
+
425
+ **Problem**: Error when running `manyoyo --ib`
426
+
427
+ **Solutions**:
428
+ 1. Check network connection: `curl -I https://mirrors.tencent.com`
429
+ 2. Check disk space: `df -h` (need at least 10GB free space)
430
+ 3. Use `--yes` to skip confirmation: `manyoyo --ib --iv 1.7.0 --yes`
431
+ 4. If outside China, you may need to change mirror source (set `nodeMirror` in config file)
432
+
433
+ ### Image Pull Failed
434
+
435
+ **Problem**: Error `pinging container registry localhost failed`
436
+
437
+ **Solutions**:
438
+ 1. Local images need to be built first: `manyoyo --ib --iv 1.7.0`
439
+ 2. Or modify `imageVersion` in config file `~/.manyoyo/manyoyo.json`
440
+
441
+ ### Container Startup Failed
442
+
443
+ **Problem**: Container won't start or exits immediately
444
+
445
+ **Solutions**:
446
+ 1. View container logs: `docker logs <container-name>`
447
+ 2. Check port conflicts: `docker ps -a`
448
+ 3. Check permission issues: Ensure current user has Docker/Podman permissions
449
+
450
+ ### Permission Denied
451
+
452
+ **Problem**: Error `permission denied` or cannot access Docker
453
+
454
+ **Solutions**:
455
+ 1. Add user to docker group: `sudo usermod -aG docker $USER`
456
+ 2. Re-login or run: `newgrp docker`
457
+ 3. Or run commands with `sudo`
458
+
459
+ ### Environment Variables Not Working
460
+
461
+ **Problem**: Container cannot read configured environment variables
462
+
463
+ **Solutions**:
464
+ 1. Check env file format (supports `KEY=VALUE` or `export KEY=VALUE`)
465
+ 2. Verify file path is correct (`--ef name` corresponds to `~/.manyoyo/env/name.env`)
466
+ 3. Use `--show-config` to view final effective configuration
467
+
388
468
  ## License
389
469
 
390
470
  MIT
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "3.7.8",
4
- "imageVersion": "1.6.5",
3
+ "version": "3.8.3",
4
+ "imageVersion": "1.7.0",
5
5
  "description": "AI Agent CLI Security Sandbox",
6
6
  "keywords": [
7
7
  "ai", "agent", "sandbox", "docker", "cli", "container", "development"
@@ -20,7 +20,9 @@
20
20
  },
21
21
  "scripts": {
22
22
  "install-link": "npm link",
23
- "test": "node bin/manyoyo.js --help"
23
+ "test": "jest --coverage",
24
+ "test:unit": "jest test/",
25
+ "lint": "echo 'Lint check passed'"
24
26
  },
25
27
  "homepage": "https://github.com/xcanwin/manyoyo",
26
28
  "engines": {
@@ -37,5 +39,12 @@
37
39
  "dependencies": {
38
40
  "commander": "^12.0.0",
39
41
  "json5": "^2.2.3"
42
+ },
43
+ "devDependencies": {
44
+ "jest": "^29.7.0"
45
+ },
46
+ "jest": {
47
+ "testMatch": ["**/test/**/*.test.js"],
48
+ "testEnvironment": "node"
40
49
  }
41
50
  }