@xcanwin/manyoyo 3.7.7 → 3.8.2

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,6 +3,7 @@
3
3
  <p align="center">
4
4
  <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>
5
5
  <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>
6
+ <a href="https://github.com/xcanwin/manyoyo/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/badge/License-MIT-yellow.svg" /></a>
6
7
  </p>
7
8
 
8
9
  <p align="center">
@@ -12,6 +13,25 @@
12
13
 
13
14
  ---
14
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
+
15
35
  **MANYOYO** 是一款 AI 智能体提效安全沙箱,安全、高效、省 token,专为 Agent YOLO 模式设计,保障宿主机安全。
16
36
 
17
37
  预装常见 Agent 与工具,进一步节省 token。循环自由切换 Agent 和 `/bin/bash`,进一步提效。
@@ -61,7 +81,7 @@ podman pull ubuntu:24.04
61
81
 
62
82
  ```bash
63
83
  # 使用 manyoyo 构建镜像(推荐,自动使用缓存加速)
64
- manyoyo --ib --iv 1.6.4 # 默认构建 full 版本(推荐,建议指定版本号)
84
+ manyoyo --ib --iv 1.7.0 # 默认构建 full 版本(推荐,建议指定版本号)
65
85
  manyoyo --ib --iba TOOL=common # 构建常见组件版本(python,nodejs,claude)
66
86
  manyoyo --ib --iba TOOL=go,codex,java,gemini # 构建自定义组件版本
67
87
  manyoyo --ib --iba GIT_SSL_NO_VERIFY=true # 构建 full 版本且跳过git的ssl验证
@@ -200,7 +220,7 @@ manyoyo --ef openai_[gpt]_codex -x codex
200
220
  "hostPath": "/path/to/project", // 默认宿主机工作目录
201
221
  "containerPath": "/path/to/project", // 默认容器工作目录
202
222
  "imageName": "localhost/xcanwin/manyoyo", // 默认镜像名称
203
- "imageVersion": "1.6.4-full", // 默认镜像版本
223
+ "imageVersion": "1.7.0-full", // 默认镜像版本
204
224
  "containerMode": "common", // 容器嵌套模式 (common, dind, sock)
205
225
 
206
226
  // 环境变量配置
@@ -224,9 +244,21 @@ manyoyo --ef openai_[gpt]_codex -x codex
224
244
  - **覆盖型参数**:命令行 > 运行配置 > 全局配置 > 默认值
225
245
  - **合并型参数**:全局配置 + 运行配置 + 命令行(按顺序累加)
226
246
 
227
- 覆盖型参数包括:`containerName`, `hostPath`, `containerPath`, `imageName`, `imageVersion`, `containerMode`, `shellPrefix`, `shell`, `yolo`, `quiet`
228
-
229
- 合并型参数包括:`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` | 数组累加合并 | 所有构建参数生效 |
230
262
 
231
263
  #### 常用样例-全局
232
264
 
@@ -236,7 +268,7 @@ mkdir -p ~/.manyoyo/
236
268
  cat > ~/.manyoyo/manyoyo.json << 'EOF'
237
269
  {
238
270
  "imageName": "localhost/xcanwin/manyoyo",
239
- "imageVersion": "1.6.4-full"
271
+ "imageVersion": "1.7.0-full"
240
272
  }
241
273
  EOF
242
274
  ```
@@ -358,6 +390,8 @@ docker ps -a # 现在可以在容器内使用 docker 命令
358
390
  | `-y CLI` | `--yolo` | 无需确认运行 AI 智能体 |
359
391
  | `--show-config` | | 显示最终生效配置并退出 |
360
392
  | `--show-command` | | 显示将执行的命令并退出(存在容器时为 docker exec,不存在时为 docker run) |
393
+ | `--yes` | | 所有提示自动确认(用于CI/脚本) |
394
+ | `--rm-on-exit` | | 退出后自动删除容器(一次性模式) |
361
395
  | `--install NAME` | | 安装 manyoyo 命令 |
362
396
  | `-q LIST` | `--quiet` | 静默显示 |
363
397
  | `-r NAME` | `--run` | 加载运行配置(支持 `name` 或 `./path.json`) |
@@ -384,6 +418,53 @@ docker ps -a # 现在可以在容器内使用 docker 命令
384
418
  npm uninstall -g @xcanwin/manyoyo
385
419
  ```
386
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
+
387
468
  ## 许可证
388
469
 
389
470
  MIT
package/bin/manyoyo.js CHANGED
@@ -23,6 +23,17 @@ function formatDate() {
23
23
  return `${month}${day}-${hour}${minute}`;
24
24
  }
25
25
 
26
+ // ==============================================================================
27
+ // Configuration Constants
28
+ // ==============================================================================
29
+
30
+ const CONFIG = {
31
+ CACHE_TTL_DAYS: 2, // 缓存过期天数
32
+ CONTAINER_READY_MAX_RETRIES: 30, // 容器就绪最大重试次数
33
+ CONTAINER_READY_INITIAL_DELAY: 100, // 容器就绪初始延迟(ms)
34
+ CONTAINER_READY_MAX_DELAY: 2000, // 容器就绪最大延迟(ms)
35
+ };
36
+
26
37
  // Default configuration
27
38
  let CONTAINER_NAME = `myy-${formatDate()}`;
28
39
  let HOST_PATH = process.cwd();
@@ -40,8 +51,11 @@ let CONTAINER_ENVS = [];
40
51
  let CONTAINER_VOLUMES = [];
41
52
  let MANYOYO_NAME = "manyoyo";
42
53
  let CONT_MODE = "";
54
+ let CONT_MODE_ARGS = [];
43
55
  let QUIET = {};
44
56
  let SHOW_COMMAND = false;
57
+ let YES_MODE = false;
58
+ let RM_ON_EXIT = false;
45
59
 
46
60
  // Color definitions using ANSI codes
47
61
  const RED = '\x1b[0;31m';
@@ -62,10 +76,73 @@ function sleep(ms) {
62
76
  return new Promise(resolve => setTimeout(resolve, ms));
63
77
  }
64
78
 
79
+ /**
80
+ * 敏感信息脱敏(用于 --show-config 输出)
81
+ * @param {Object} obj - 配置对象
82
+ * @returns {Object} 脱敏后的配置对象
83
+ */
84
+ function sanitizeSensitiveData(obj) {
85
+ const sensitiveKeys = ['KEY', 'TOKEN', 'SECRET', 'PASSWORD', 'AUTH', 'CREDENTIAL'];
86
+
87
+ function sanitizeValue(key, value) {
88
+ if (typeof value !== 'string') return value;
89
+ const upperKey = key.toUpperCase();
90
+ if (sensitiveKeys.some(k => upperKey.includes(k))) {
91
+ if (value.length <= 8) return '****';
92
+ return value.slice(0, 4) + '****' + value.slice(-4);
93
+ }
94
+ return value;
95
+ }
96
+
97
+ function sanitizeArray(arr) {
98
+ return arr.map(item => {
99
+ if (typeof item === 'string' && item.includes('=')) {
100
+ const idx = item.indexOf('=');
101
+ const key = item.slice(0, idx);
102
+ const value = item.slice(idx + 1);
103
+ return `${key}=${sanitizeValue(key, value)}`;
104
+ }
105
+ return item;
106
+ });
107
+ }
108
+
109
+ const result = {};
110
+ for (const [key, value] of Object.entries(obj)) {
111
+ if (Array.isArray(value)) {
112
+ result[key] = sanitizeArray(value);
113
+ } else if (typeof value === 'object' && value !== null) {
114
+ result[key] = sanitizeSensitiveData(value);
115
+ } else {
116
+ result[key] = sanitizeValue(key, value);
117
+ }
118
+ }
119
+ return result;
120
+ }
121
+
65
122
  // ==============================================================================
66
123
  // Configuration File Functions
67
124
  // ==============================================================================
68
125
 
126
+ /**
127
+ * @typedef {Object} Config
128
+ * @property {string} [containerName] - 容器名称
129
+ * @property {string} [hostPath] - 宿主机路径
130
+ * @property {string} [containerPath] - 容器路径
131
+ * @property {string} [imageName] - 镜像名称
132
+ * @property {string} [imageVersion] - 镜像版本
133
+ * @property {string[]} [env] - 环境变量数组
134
+ * @property {string[]} [envFile] - 环境文件数组
135
+ * @property {string[]} [volumes] - 挂载卷数组
136
+ * @property {string} [yolo] - YOLO 模式
137
+ * @property {string} [containerMode] - 容器模式
138
+ * @property {number} [cacheTTL] - 缓存过期天数
139
+ * @property {string} [nodeMirror] - Node.js 镜像源
140
+ */
141
+
142
+ /**
143
+ * 加载全局配置文件
144
+ * @returns {Config} 配置对象
145
+ */
69
146
  function loadConfig() {
70
147
  const configPath = path.join(os.homedir(), '.manyoyo', 'manyoyo.json');
71
148
  if (fs.existsSync(configPath)) {
@@ -187,6 +264,10 @@ async function askQuestion(prompt) {
187
264
  // Configuration Functions
188
265
  // ==============================================================================
189
266
 
267
+ /**
268
+ * 添加环境变量
269
+ * @param {string} env - 环境变量字符串 (KEY=VALUE)
270
+ */
190
271
  function addEnv(env) {
191
272
  const idx = env.indexOf('=');
192
273
  if (idx <= 0) {
@@ -276,7 +357,7 @@ function setYolo(cli) {
276
357
  break;
277
358
  case 'opencode':
278
359
  case 'oc':
279
- EXEC_COMMAND = "OPENCODE_PERMISSION='\"allow\"' opencode";
360
+ EXEC_COMMAND = "OPENCODE_PERMISSION='{\"*\":\"allow\"}' opencode";
280
361
  break;
281
362
  default:
282
363
  console.log(`${RED}⚠️ 未知LLM CLI: ${cli}${NC}`);
@@ -284,21 +365,33 @@ function setYolo(cli) {
284
365
  }
285
366
  }
286
367
 
368
+ /**
369
+ * 设置容器嵌套模式
370
+ * @param {string} mode - 模式名称 (common, dind, sock)
371
+ */
287
372
  function setContMode(mode) {
288
373
  switch (mode) {
289
374
  case 'common':
290
375
  CONT_MODE = "";
376
+ CONT_MODE_ARGS = [];
291
377
  break;
292
378
  case 'docker-in-docker':
293
379
  case 'dind':
294
380
  case 'd':
295
381
  CONT_MODE = "--privileged";
382
+ CONT_MODE_ARGS = ['--privileged'];
296
383
  console.log(`${GREEN}✅ 开启安全的容器嵌套容器模式, 手动在容器内启动服务: nohup dockerd &${NC}`);
297
384
  break;
298
385
  case 'mount-docker-socket':
299
386
  case 'sock':
300
387
  case 's':
301
388
  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";
389
+ CONT_MODE_ARGS = [
390
+ '--privileged',
391
+ '--volume', '/var/run/docker.sock:/var/run/docker.sock',
392
+ '--env', 'DOCKER_HOST=unix:///var/run/docker.sock',
393
+ '--env', 'CONTAINER_HOST=unix:///var/run/docker.sock'
394
+ ];
302
395
  console.log(`${RED}⚠️ 开启危险的容器嵌套容器模式, 危害: 容器可访问宿主机文件${NC}`);
303
396
  break;
304
397
  default:
@@ -322,6 +415,19 @@ function dockerExec(cmd, options = {}) {
322
415
  }
323
416
  }
324
417
 
418
+ function showImagePullHint(err) {
419
+ const stderr = err && err.stderr ? err.stderr.toString() : '';
420
+ const stdout = err && err.stdout ? err.stdout.toString() : '';
421
+ const message = err && err.message ? err.message : '';
422
+ const combined = `${message}\n${stderr}\n${stdout}`;
423
+ if (!/localhost\/v2|pinging container registry localhost|connection refused|dial tcp .*:443/i.test(combined)) {
424
+ return;
425
+ }
426
+ const image = `${IMAGE_NAME}:${IMAGE_VERSION}`;
427
+ console.log(`${YELLOW}💡 提示: 本地未找到镜像 ${image},并且从 localhost 注册表拉取失败。${NC}`);
428
+ console.log(`${YELLOW} 你可以: 1) 更新 ~/.manyoyo/manyoyo.json 的 imageVersion 2) 或先执行 manyoyo --ib --iv <version> 构建镜像。${NC}`);
429
+ }
430
+
325
431
  function runCmd(cmd, args, options = {}) {
326
432
  const result = spawnSync(cmd, args, { encoding: 'utf-8', ...options });
327
433
  if (result.error) {
@@ -430,10 +536,24 @@ function pruneDanglingImages() {
430
536
  console.log(`${GREEN}✅ 清理完成${NC}`);
431
537
  }
432
538
 
539
+ /**
540
+ * 准备构建缓存(Node.js、JDT LSP、gopls)
541
+ * @param {string} imageTool - 构建工具类型
542
+ */
433
543
  async function prepareBuildCache(imageTool) {
434
544
  const cacheDir = path.join(__dirname, '../docker/cache');
435
545
  const timestampFile = path.join(cacheDir, '.timestamps.json');
436
- const cacheTTLDays = 2;
546
+
547
+ // 从配置文件读取 TTL,默认 2 天
548
+ const config = loadConfig();
549
+ const cacheTTLDays = config.cacheTTL || CONFIG.CACHE_TTL_DAYS;
550
+
551
+ // 镜像源优先级:用户配置 > 腾讯云 > 官方
552
+ const nodeMirrors = [
553
+ config.nodeMirror,
554
+ 'https://mirrors.tencent.com/nodejs-release',
555
+ 'https://nodejs.org/dist'
556
+ ].filter(Boolean);
437
557
 
438
558
  console.log(`\n${CYAN}准备构建缓存...${NC}`);
439
559
 
@@ -476,18 +596,46 @@ async function prepareBuildCache(imageTool) {
476
596
  const hasNodeCache = fs.existsSync(nodeCacheDir) && fs.readdirSync(nodeCacheDir).some(f => f.startsWith('node-') && f.includes(`linux-${archNode}`));
477
597
  if (!hasNodeCache || isExpired(nodeKey)) {
478
598
  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;
599
+
600
+ // 尝试多个镜像源
601
+ let downloadSuccess = false;
602
+ for (const mirror of nodeMirrors) {
603
+ try {
604
+ console.log(`${BLUE}尝试镜像源: ${mirror}${NC}`);
605
+ const shasumUrl = `${mirror}/latest-v${nodeVersion}.x/SHASUMS256.txt`;
606
+ const shasumContent = execSync(`curl -sL ${shasumUrl}`, { encoding: 'utf-8' });
607
+ const shasumLine = shasumContent.split('\n').find(line => line.includes(`linux-${archNode}.tar.gz`));
608
+ if (!shasumLine) continue;
609
+
610
+ const [expectedHash, fileName] = shasumLine.trim().split(/\s+/);
611
+ const nodeUrl = `${mirror}/latest-v${nodeVersion}.x/${fileName}`;
612
+ const nodeTargetPath = path.join(nodeCacheDir, fileName);
613
+
614
+ // 下载文件
615
+ runCmd('curl', ['-fsSL', nodeUrl, '-o', nodeTargetPath], { stdio: 'inherit' });
616
+
617
+ // SHA256 校验
618
+ const actualHash = execSync(`sha256sum "${nodeTargetPath}" | awk '{print $1}'`, { encoding: 'utf-8' }).trim();
619
+ if (actualHash !== expectedHash) {
620
+ console.log(`${RED}SHA256 校验失败,删除文件${NC}`);
621
+ fs.unlinkSync(nodeTargetPath);
622
+ continue;
623
+ }
624
+
625
+ console.log(`${GREEN}✓ SHA256 校验通过${NC}`);
626
+ timestamps[nodeKey] = now.toISOString();
627
+ fs.writeFileSync(timestampFile, JSON.stringify(timestamps, null, 4));
628
+ console.log(`${GREEN}✓ Node.js 下载完成${NC}`);
629
+ downloadSuccess = true;
630
+ break;
631
+ } catch (e) {
632
+ console.log(`${YELLOW}镜像源 ${mirror} 失败,尝试下一个...${NC}`);
633
+ }
634
+ }
635
+
636
+ if (!downloadSuccess) {
637
+ console.error(`${RED}错误: Node.js 下载失败(所有镜像源均不可用)${NC}`);
638
+ throw new Error('Node.js download failed');
491
639
  }
492
640
  } else {
493
641
  console.log(`${GREEN}✓ Node.js 缓存已存在${NC}`);
@@ -625,8 +773,10 @@ async function buildImage(IMAGE_BUILD_ARGS, imageName, imageVersion) {
625
773
  console.log(`${BLUE}准备执行命令:${NC}`);
626
774
  console.log(`${buildCmd}\n`);
627
775
 
628
- const reply = await askQuestion(`❔ 是否继续构建? [ 直接回车=继续, ctrl+c=取消 ]: `);
629
- console.log("");
776
+ if (!YES_MODE) {
777
+ await askQuestion(`❔ 是否继续构建? [ 直接回车=继续, ctrl+c=取消 ]: `);
778
+ console.log("");
779
+ }
630
780
 
631
781
  try {
632
782
  execSync(buildCmd, { stdio: 'inherit' });
@@ -702,6 +852,8 @@ function setupCommander() {
702
852
  .option('--install <name>', '安装manyoyo命令 (docker-cli-plugin)')
703
853
  .option('--show-config', '显示最终生效配置并退出')
704
854
  .option('--show-command', '显示将执行的 docker run 命令并退出')
855
+ .option('--yes', '所有提示自动确认 (用于CI/脚本)')
856
+ .option('--rm-on-exit', '退出后自动删除容器 (一次性模式)')
705
857
  .option('-q, --quiet <item>', '静默显示 (可多次使用: cnew,crm,tip,cmd,full)', (value, previous) => [...(previous || []), value], []);
706
858
 
707
859
  // Docker CLI plugin metadata check
@@ -801,6 +953,14 @@ function setupCommander() {
801
953
  }
802
954
  }
803
955
 
956
+ if (options.yes) {
957
+ YES_MODE = true;
958
+ }
959
+
960
+ if (options.rmOnExit) {
961
+ RM_ON_EXIT = true;
962
+ }
963
+
804
964
  if (options.showConfig) {
805
965
  const finalConfig = {
806
966
  hostPath: HOST_PATH,
@@ -824,7 +984,9 @@ function setupCommander() {
824
984
  suffix: EXEC_COMMAND_SUFFIX
825
985
  }
826
986
  };
827
- console.log(JSON.stringify(finalConfig, null, 4));
987
+ // 敏感信息脱敏
988
+ const sanitizedConfig = sanitizeSensitiveData(finalConfig);
989
+ console.log(JSON.stringify(sanitizedConfig, null, 4));
828
990
  process.exit(0);
829
991
  }
830
992
 
@@ -869,15 +1031,20 @@ function validateHostPath() {
869
1031
  }
870
1032
  }
871
1033
 
1034
+ /**
1035
+ * 等待容器就绪(使用指数退避算法)
1036
+ * @param {string} containerName - 容器名称
1037
+ */
872
1038
  async function waitForContainerReady(containerName) {
873
- const MAX_RETRIES = 50;
874
- let count = 0;
875
- while (true) {
1039
+ const MAX_RETRIES = CONFIG.CONTAINER_READY_MAX_RETRIES;
1040
+ let retryDelay = CONFIG.CONTAINER_READY_INITIAL_DELAY;
1041
+
1042
+ for (let count = 0; count < MAX_RETRIES; count++) {
876
1043
  try {
877
1044
  const status = getContainerStatus(containerName);
878
1045
 
879
1046
  if (status === 'running') {
880
- break;
1047
+ return;
881
1048
  }
882
1049
 
883
1050
  if (status === 'exited') {
@@ -886,39 +1053,41 @@ async function waitForContainerReady(containerName) {
886
1053
  process.exit(1);
887
1054
  }
888
1055
 
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
- }
1056
+ await sleep(retryDelay);
1057
+ retryDelay = Math.min(retryDelay * 2, CONFIG.CONTAINER_READY_MAX_DELAY);
897
1058
  } catch (e) {
898
- await sleep(100);
899
- count++;
900
- if (count >= MAX_RETRIES) {
901
- console.log(`${RED}⚠️ 错误: 容器启动超时。${NC}`);
902
- process.exit(1);
903
- }
1059
+ await sleep(retryDelay);
1060
+ retryDelay = Math.min(retryDelay * 2, CONFIG.CONTAINER_READY_MAX_DELAY);
904
1061
  }
905
1062
  }
1063
+
1064
+ console.log(`${RED}⚠️ 错误: 容器启动超时。${NC}`);
1065
+ process.exit(1);
906
1066
  }
907
1067
 
1068
+ /**
1069
+ * 创建新容器
1070
+ * @returns {Promise<string>} 默认命令
1071
+ */
908
1072
  async function createNewContainer() {
909
1073
  if ( !(QUIET.cnew || QUIET.full) ) console.log(`${CYAN}📦 manyoyo by xcanwin 正在创建新容器: ${YELLOW}${CONTAINER_NAME}${NC}`);
910
1074
 
911
1075
  EXEC_COMMAND = `${EXEC_COMMAND_PREFIX}${EXEC_COMMAND}${EXEC_COMMAND_SUFFIX}`;
912
1076
  const defaultCommand = EXEC_COMMAND;
913
1077
 
914
- const dockerRunCmd = buildDockerRunCmd();
915
-
916
1078
  if (SHOW_COMMAND) {
917
- console.log(dockerRunCmd);
1079
+ console.log(buildDockerRunCmd());
918
1080
  process.exit(0);
919
1081
  }
920
1082
 
921
- dockerExec(dockerRunCmd, { stdio: 'pipe' });
1083
+ // 使用数组参数执行命令(安全方式)
1084
+ try {
1085
+ const args = buildDockerRunArgs();
1086
+ dockerExecArgs(args, { stdio: 'pipe' });
1087
+ } catch (e) {
1088
+ showImagePullHint(e);
1089
+ throw e;
1090
+ }
922
1091
 
923
1092
  // Wait for container to be ready
924
1093
  await waitForContainerReady(CONTAINER_NAME);
@@ -926,17 +1095,45 @@ async function createNewContainer() {
926
1095
  return defaultCommand;
927
1096
  }
928
1097
 
929
- function buildDockerRunCmd() {
930
- // Build docker run command
1098
+ /**
1099
+ * 构建 Docker run 命令参数数组(安全方式,避免命令注入)
1100
+ * @returns {string[]} 命令参数数组
1101
+ */
1102
+ function buildDockerRunArgs() {
931
1103
  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`;
1104
+ const safeLabelCmd = EXEC_COMMAND.replace(/[\r\n]/g, ' ');
1105
+
1106
+ const args = [
1107
+ 'run', '-d',
1108
+ '--name', CONTAINER_NAME,
1109
+ '--entrypoint', '',
1110
+ ...CONT_MODE_ARGS,
1111
+ ...CONTAINER_ENVS,
1112
+ ...CONTAINER_VOLUMES,
1113
+ '--volume', `${HOST_PATH}:${CONTAINER_PATH}`,
1114
+ '--workdir', CONTAINER_PATH,
1115
+ '--label', `manyoyo.default_cmd=${safeLabelCmd}`,
1116
+ fullImage,
1117
+ 'tail', '-f', '/dev/null'
1118
+ ];
1119
+
1120
+ return args;
1121
+ }
938
1122
 
939
- return dockerRunCmd;
1123
+ /**
1124
+ * 构建 Docker run 命令字符串(用于显示)
1125
+ * @returns {string} 命令字符串
1126
+ */
1127
+ function buildDockerRunCmd() {
1128
+ const args = buildDockerRunArgs();
1129
+ // 对包含空格或特殊字符的参数加引号
1130
+ const quotedArgs = args.map(arg => {
1131
+ if (arg.includes(' ') || arg.includes('"') || arg.includes('=')) {
1132
+ return `"${arg.replace(/"/g, '\\"')}"`;
1133
+ }
1134
+ return arg;
1135
+ });
1136
+ return `${DOCKER_CMD} ${quotedArgs.join(' ')}`;
940
1137
  }
941
1138
 
942
1139
  async function connectExistingContainer() {
@@ -996,7 +1193,17 @@ function executeInContainer(defaultCommand) {
996
1193
  }
997
1194
  }
998
1195
 
1196
+ /**
1197
+ * 处理会话退出后的交互
1198
+ * @param {string} defaultCommand - 默认命令
1199
+ */
999
1200
  async function handlePostExit(defaultCommand) {
1201
+ // --rm-on-exit 模式:自动删除容器
1202
+ if (RM_ON_EXIT) {
1203
+ removeContainer(CONTAINER_NAME);
1204
+ return;
1205
+ }
1206
+
1000
1207
  getHelloTip(CONTAINER_NAME, defaultCommand);
1001
1208
 
1002
1209
  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 \
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
@@ -3,15 +3,35 @@
3
3
  <p align="center">
4
4
  <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>
5
5
  <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>
6
+ <a href="https://github.com/xcanwin/manyoyo/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/badge/License-MIT-yellow.svg" /></a>
6
7
  </p>
7
8
 
8
9
  <p align="center">
9
- <a href="README.md">中文</a> |
10
- <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>
11
12
  </p>
12
13
 
13
14
  ---
14
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
+
15
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.
16
36
 
17
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.
@@ -61,7 +81,7 @@ Only one of the following commands needs to be executed:
61
81
 
62
82
  ```bash
63
83
  # Build using manyoyo (Recommended, auto-cache enabled)
64
- 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)
65
85
  manyoyo --ib --iba TOOL=common # Build common version (python,nodejs,claude)
66
86
  manyoyo --ib --iba TOOL=go,codex,java,gemini # Build custom combination
67
87
  manyoyo --ib --iba GIT_SSL_NO_VERIFY=true # Build the full version and skip Git SSL verification
@@ -200,7 +220,7 @@ Refer to `config.example.json` for all available options:
200
220
  "hostPath": "/path/to/project", // Default host working directory
201
221
  "containerPath": "/path/to/project", // Default container working directory
202
222
  "imageName": "localhost/xcanwin/manyoyo", // Default image name
203
- "imageVersion": "1.6.4-full", // Default image version
223
+ "imageVersion": "1.7.0-full", // Default image version
204
224
  "containerMode": "common", // Container nesting mode (common, dind, sock)
205
225
 
206
226
  // Environment variable configuration
@@ -224,9 +244,21 @@ Refer to `config.example.json` for all available options:
224
244
  - **Override parameters**: Command line > Run config > Global config > Defaults
225
245
  - **Merge parameters**: Global config + Run config + Command line (concatenated in order)
226
246
 
227
- Override parameters include: `containerName`, `hostPath`, `containerPath`, `imageName`, `imageVersion`, `containerMode`, `shellPrefix`, `shell`, `yolo`, `quiet`
228
-
229
- 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 |
230
262
 
231
263
  #### Common Examples-Global
232
264
 
@@ -236,7 +268,7 @@ mkdir -p ~/.manyoyo/
236
268
  cat > ~/.manyoyo/manyoyo.json << 'EOF'
237
269
  {
238
270
  "imageName": "localhost/xcanwin/manyoyo",
239
- "imageVersion": "1.6.4-full"
271
+ "imageVersion": "1.7.0-full"
240
272
  }
241
273
  EOF
242
274
  ```
@@ -358,6 +390,8 @@ docker ps -a # Now you can use docker commands inside the container
358
390
  | `-y CLI` | `--yolo` | Run AI agent without confirmation |
359
391
  | `--show-config` | | Print final effective config and exit |
360
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) |
361
395
  | `--install NAME` | | Install manyoyo command |
362
396
  | `-q LIST` | `--quiet` | Quiet output |
363
397
  | `-r NAME` | `--run` | Load run configuration (supports `name` or `./path.json`) |
@@ -384,6 +418,53 @@ docker ps -a # Now you can use docker commands inside the container
384
418
  npm uninstall -g @xcanwin/manyoyo
385
419
  ```
386
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
+
387
468
  ## License
388
469
 
389
470
  MIT
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "3.7.7",
4
- "imageVersion": "1.6.5",
3
+ "version": "3.8.2",
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
  }