@xcanwin/manyoyo 8.2.1 → 8.3.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.en.md ADDED
@@ -0,0 +1,88 @@
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.
package/README.md CHANGED
@@ -3,7 +3,8 @@
3
3
  </p>
4
4
 
5
5
  # <p align="center"><a href="https://github.com/xcanwin/manyoyo">MANYOYO(慢悠悠)</a></p>
6
- <p align="center">让 Claude Code / Codex / Gemini / OpenCode 在容器里放心跑 YOLO 模式:AI 随便折腾,伤不到你的电脑。</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>
7
8
  <p align="center">
8
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>
9
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>
@@ -12,7 +13,7 @@
12
13
 
13
14
  <p align="center">
14
15
  <a href="README.md"><b>中文</b></a> |
15
- <a href="https://xcanwin.github.io/manyoyo/en/">English</a>
16
+ <a href="README.en.md">English</a>
16
17
  </p>
17
18
  <p align="center">
18
19
  文档:<a href="https://xcanwin.github.io/manyoyo/">https://xcanwin.github.io/manyoyo/</a>
@@ -28,7 +29,7 @@
28
29
  curl -fsSL https://github.com/xcanwin/manyoyo/raw/main/scripts/install.sh | sh
29
30
  ```
30
31
 
31
- 装完浏览器自动打开,按向导选 Agent、填 Key 就能开始对话。macOS 不需要预装任何东西;Linux 需要已有 Docker 或 Podman。
32
+ 装完按屏幕提示完成配置。macOS 不需要预装任何东西。Linux:缺少容器环境时,安装器会说明原因,征得你同意后用 sudo 自动安装。
32
33
 
33
34
  ### 已有 Node.js 和 Docker/Podman
34
35
 
@@ -52,7 +53,7 @@ manyoyo init all # 把本机已有的 Agent 配置搬进来
52
53
  manyoyo run -y c # 在沙箱里以 YOLO 模式启动 Claude Code
53
54
  ```
54
55
 
55
- 默认镜像 `ghcr.io/xcanwin/manyoyo` 首次使用时自动拉取;需要自定义镜像时再执行 `manyoyo build --iv 2.1.0-common`。更多命令见[命令速查](https://xcanwin.github.io/manyoyo/reference/cli-options)。
56
+ 默认镜像 `ghcr.io/xcanwin/manyoyo` 首次使用时自动拉取;需要自定义镜像时再执行 `manyoyo build --iv 2.1.1-common`。更多命令见[命令速查](https://xcanwin.github.io/manyoyo/reference/cli-options)。
56
57
 
57
58
  ## 它能做什么
58
59
 
package/bin/manyoyo.js CHANGED
@@ -9,7 +9,7 @@ const net = require('net');
9
9
  const readline = require('readline');
10
10
  const { Command, Help } = require('commander');
11
11
  const { startWebServer } = require('../lib/web/server');
12
- const { buildContainerRunArgs, buildContainerRunCommand } = require('../lib/container-run');
12
+ const { buildContainerRunArgs, buildContainerRunCommand, runWithEnvFile } = require('../lib/container-run');
13
13
  const { getManyoyoConfigPath, readManyoyoConfig, syncGlobalImageVersion } = require('../lib/global-config');
14
14
  const { resolveUpdateImageVersion } = require('../lib/image-version-policy');
15
15
  const { selectContainerRuntime, mergeRuntimeEnv } = require('../lib/container-runtime');
@@ -34,7 +34,7 @@ const { resolveAgentResumeArg, buildAgentResumeCommand } = require('../lib/agent
34
34
  const { resolveYoloCommand } = require('../lib/agent-adapters');
35
35
  const { runDoctorChecks, applyDoctorFixes } = require('../lib/doctor');
36
36
  const { resolveContainerMode } = require('../lib/container-modes');
37
- const { runPluginCommand, createPlugin } = require('../lib/plugin');
37
+ const { runPlaywrightCommand, buildContainerIntegration, mergeIntegration } = require('../lib/plugin');
38
38
  const { buildManyoyoLogPath } = require('../lib/log-path');
39
39
  const { resolveRuntimeConfig } = require('../lib/runtime-resolver');
40
40
  const { resolveWorktreeSupport } = require('../lib/worktrees');
@@ -47,6 +47,7 @@ const { applyAptMirror } = require('../lib/mirrors');
47
47
  const {
48
48
  sanitizeSensitiveData,
49
49
  sanitizeServeLogText,
50
+ redactCommandArgs,
50
51
  formatServeLogValue,
51
52
  getServeProcessSnapshot
52
53
  } = require('../lib/serve-log');
@@ -122,6 +123,7 @@ const IMAGE_VERSION_TAG_PATTERN = /^(\d+\.\d+\.\d+)-([A-Za-z0-9][A-Za-z0-9_.-]*)
122
123
 
123
124
  // Docker command (will be set by ensure_docker)
124
125
  let DOCKER_CMD = 'docker';
126
+ let PLAYWRIGHT_PLUGIN_CONFIG = { globalConfig: {}, runConfig: {} };
125
127
  // 仅运行时子进程使用的完整 env(私有 Podman 才有值),不要写回 process.env
126
128
  let DOCKER_ENV;
127
129
  let CONTAINER_RUNTIME = null;
@@ -602,61 +604,21 @@ function addEnvFile(envFile) {
602
604
  return addEnvFileTo(CONTAINER_ENVS, envFile);
603
605
  }
604
606
 
605
- function hasEnvKey(targetEnvs, key) {
606
- for (let i = 0; i < targetEnvs.length; i += 2) {
607
- if (targetEnvs[i] !== '--env') {
608
- continue;
609
- }
610
- const text = String(targetEnvs[i + 1] || '');
611
- const idx = text.indexOf('=');
612
- if (idx > 0 && text.slice(0, idx) === key) {
613
- return true;
614
- }
615
- }
616
- return false;
617
- }
618
-
619
- function appendUniqueArgs(targetArgs, extraArgs) {
620
- const joinedExisting = new Set();
621
- for (let i = 0; i < targetArgs.length; i += 2) {
622
- const head = String(targetArgs[i] || '');
623
- const value = String(targetArgs[i + 1] || '');
624
- if (head.startsWith('--')) {
625
- joinedExisting.add(`${head}\u0000${value}`);
626
- }
627
- }
628
-
629
- for (let i = 0; i < extraArgs.length; i += 2) {
630
- const head = String(extraArgs[i] || '');
631
- const value = String(extraArgs[i + 1] || '');
632
- const signature = `${head}\u0000${value}`;
633
- if (!joinedExisting.has(signature)) {
634
- joinedExisting.add(signature);
635
- targetArgs.push(head, value);
636
- }
637
- }
638
- }
639
-
640
- function applyPlaywrightCliSessionIntegration(config, runConfig) {
641
- try {
642
- const plugin = createPlugin('playwright', {
643
- globalConfig: config,
644
- runConfig,
645
- projectRoot: path.join(__dirname, '..')
646
- });
647
- const integration = plugin.buildCliSessionIntegration(DOCKER_CMD);
648
- for (const entry of integration.envEntries) {
649
- const parsed = parseEnvEntry(entry);
650
- if (!hasEnvKey(CONTAINER_ENVS, parsed.key)) {
651
- addEnv(`${parsed.key}=${parsed.value}`);
652
- }
653
- }
654
- appendUniqueArgs(CONTAINER_EXTRA_ARGS, integration.extraArgs);
655
- appendUniqueArgs(CONTAINER_VOLUMES, integration.volumeEntries || []);
656
- } catch (error) {
657
- console.error(`${RED}⚠️ 错误: Playwright CLI 会话注入失败: ${error.message || String(error)}${NC}`);
658
- process.exit(1);
607
+ // Playwright 浏览器模式的容器参数;在容器创建时(运行时已确定)才计算,失败只警告,不影响 run
608
+ async function applyPlaywrightIntegration(runtime) {
609
+ const integration = await buildContainerIntegration({
610
+ ...PLAYWRIGHT_PLUGIN_CONFIG,
611
+ dryRun: runtime.showCommand,
612
+ runtimeCommand: DOCKER_CMD,
613
+ envEntries: runtime.containerEnvs.filter((_, i) => i % 2 === 1)
614
+ });
615
+ if (integration.warning) {
616
+ console.warn(`${YELLOW}⚠️ ${integration.warning}${NC}`);
659
617
  }
618
+ const merged = mergeIntegration(runtime, integration);
619
+ runtime.containerEnvs = merged.containerEnvs;
620
+ runtime.containerVolumes = merged.containerVolumes;
621
+ runtime.containerExtraArgs = merged.containerExtraArgs;
660
622
  }
661
623
 
662
624
  function addVolume(volume) {
@@ -728,6 +690,18 @@ async function ensureRunImage(runtime) {
728
690
  });
729
691
  }
730
692
 
693
+ // 把 run 失败的 stderr 尾部(已打码)与可执行的修复提示拼进错误信息,而不是只剩一行 Command failed
694
+ function explainCreateFailure(err) {
695
+ const raw = getCommandFailureText(err);
696
+ const tail = sanitizeServeLogText(String(err && err.stderr || '')).trim().split('\n').slice(-8).join('\n');
697
+ const info = describeError(raw, { command: DOCKER_CMD });
698
+ const parts = [err.message];
699
+ if (tail) parts.push(tail);
700
+ if (info && info.code === 'ROOTLESS_NETWORK_MISSING') parts.push(`提示: ${info.reason}\n${info.action}`);
701
+ err.message = parts.join('\n');
702
+ return err;
703
+ }
704
+
731
705
  function showImagePullHint(err) {
732
706
  const info = describeError(getCommandFailureText(err), { imageRef: `${IMAGE_NAME}:${IMAGE_VERSION}` });
733
707
  if (!info || !['IMAGE_PULL_FAILED', 'IMAGE_NOT_FOUND'].includes(info.code)) {
@@ -753,7 +727,7 @@ function runCmd(cmd, args, options = {}) {
753
727
  if (options.ignoreError) {
754
728
  return result.stdout || '';
755
729
  }
756
- const err = new Error(`Command failed: ${cmd} ${args.join(' ')}`);
730
+ const err = new Error(`Command failed: ${cmd} ${redactCommandArgs(args).join(' ')}`);
757
731
  err.stdout = result.stdout;
758
732
  err.stderr = result.stderr;
759
733
  err.status = result.status;
@@ -1188,79 +1162,6 @@ async function setupCommander() {
1188
1162
  selectedAction = action;
1189
1163
  selectedOptions = options;
1190
1164
  };
1191
- const selectPluginAction = (params = {}, options = {}) => {
1192
- selectAction('plugin', {
1193
- ...options,
1194
- pluginAction: params.action || 'ls',
1195
- pluginName: params.pluginName || 'playwright',
1196
- pluginScene: params.scene || 'mcp-host-headless',
1197
- pluginHost: params.host || '',
1198
- pluginExtensionPaths: Array.isArray(params.extensionPaths) ? params.extensionPaths : [],
1199
- pluginExtensionNames: Array.isArray(params.extensionNames) ? params.extensionNames : [],
1200
- pluginProdversion: params.prodversion || ''
1201
- });
1202
- };
1203
-
1204
- const registerPlaywrightAliasCommands = (command) => {
1205
- command.command('ls')
1206
- .description('列出 playwright 启用场景')
1207
- .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1208
- .action(options => selectPluginAction({
1209
- action: 'ls',
1210
- pluginName: 'playwright',
1211
- scene: 'all'
1212
- }, options));
1213
-
1214
- const actions = ['up', 'down', 'status', 'health', 'logs'];
1215
- actions.forEach(action => {
1216
- const sceneCommand = command.command(`${action} [scene]`)
1217
- .description(`执行 playwright ${action} 场景(scene 默认 mcp-host-headless)`)
1218
- .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)');
1219
-
1220
- if (action === 'up') {
1221
- appendArrayOption(sceneCommand, '--ext-path <path>', '追加浏览器扩展目录(可多次传入;目录需包含 manifest.json)');
1222
- appendArrayOption(sceneCommand, '--ext-name <name>', '追加 ~/.manyoyo/plugin/playwright/extensions/ 下的扩展目录名(可多次传入)');
1223
- }
1224
-
1225
- sceneCommand.action((scene, options) => selectPluginAction({
1226
- action,
1227
- pluginName: 'playwright',
1228
- scene: scene || 'mcp-host-headless',
1229
- extensionPaths: action === 'up' ? (options.extPath || []) : [],
1230
- extensionNames: action === 'up' ? (options.extName || []) : []
1231
- }, options));
1232
- });
1233
-
1234
- command.command('mcp-add')
1235
- .description('输出 playwright 的 MCP 接入命令')
1236
- .option('--host <host>', 'MCP URL 使用的主机名或IP (默认 host.docker.internal)')
1237
- .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1238
- .action(options => selectPluginAction({
1239
- action: 'mcp-add',
1240
- pluginName: 'playwright',
1241
- scene: 'all',
1242
- host: options.host || ''
1243
- }, options));
1244
-
1245
- command.command('cli-add')
1246
- .description('输出 playwright-cli skill 安装命令')
1247
- .action(() => selectPluginAction({
1248
- action: 'cli-add',
1249
- pluginName: 'playwright',
1250
- scene: 'all'
1251
- }));
1252
-
1253
- command.command('ext-download')
1254
- .description('下载并解压 Playwright 扩展到 ~/.manyoyo/plugin/playwright/extensions/')
1255
- .option('--prodversion <ver>', 'CRX 下载使用的 Chrome 版本号 (默认 132.0.0.0)')
1256
- .action(options => selectPluginAction({
1257
- action: 'ext-download',
1258
- pluginName: 'playwright',
1259
- scene: 'all',
1260
- prodversion: options.prodversion || ''
1261
- }, options));
1262
- };
1263
-
1264
1165
  program
1265
1166
  .name(MANYOYO_NAME)
1266
1167
  .optionsGroup('选项:')
@@ -1441,25 +1342,45 @@ https://github.com/xcanwin/manyoyo
1441
1342
  });
1442
1343
  });
1443
1344
 
1444
- const playwrightCommand = program.command('playwright').helpGroup('网页服务与插件:').description('管理 Playwright 插件服务');
1345
+ const selectPlaywright = (action, params = {}, options = {}) => {
1346
+ selectAction('playwright', { ...options, playwrightRequest: { action, ...params } });
1347
+ };
1348
+ const playwrightCommand = program.command('playwright').helpGroup('网页服务与插件:')
1349
+ .description('管理容器内浏览器的模式(默认无需任何命令)')
1350
+ .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1351
+ .action(options => selectPlaywright('overview', {}, options));
1445
1352
  playwrightCommand.addHelpText('after', `
1446
- 示例:
1447
- ${MANYOYO_NAME} playwright up mcp-host-headless 启动 MCP 宿主场景(默认/推荐)
1448
- ${MANYOYO_NAME} playwright up cli-host-headless 启动 CLI 宿主场景(供容器内 playwright-cli 附着)
1353
+ 模式(同一时间只有一个):
1354
+ (默认) 容器内虚拟屏里的有头浏览器,什么都不用做
1355
+ headed 宿主机上有窗口的浏览器:${MANYOYO_NAME} playwright up headed
1356
+ chrome 你正在用的 Chrome(沿用登录状态,有风险):${MANYOYO_NAME} playwright up chrome
1357
+ vnc 独立容器里的浏览器,用 noVNC 网页观看:${MANYOYO_NAME} playwright up vnc
1449
1358
  `);
1450
- registerPlaywrightAliasCommands(playwrightCommand);
1451
-
1452
- const pluginCommand = program.command('plugin', { hidden: true }).description('管理 manyoyo 插件');
1453
- pluginCommand.command('ls')
1454
- .description('列出可用插件与启用场景')
1359
+ playwrightCommand.command('up [mode]')
1360
+ .description('切换到 headed / chrome / vnc 模式(不带参数列出全部模式)')
1455
1361
  .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1456
- .action(options => selectPluginAction({
1457
- action: 'ls',
1458
- pluginName: 'playwright',
1459
- scene: 'all'
1362
+ .option('--ext-path <path>', '追加浏览器扩展目录(可多次传入;目录需包含 manifest.json;仅 headed、vnc)', (value, previous = []) => [...previous, value])
1363
+ .option('--ext-name <name>', '追加 ~/.manyoyo/plugin/playwright/extensions/ 下的扩展目录名(可多次传入;仅 headed、vnc)', (value, previous = []) => [...previous, value])
1364
+ .action((mode, options) => selectPlaywright('up', {
1365
+ mode: mode || '',
1366
+ extensionPaths: options.extPath || [],
1367
+ extensionNames: options.extName || []
1460
1368
  }, options));
1461
- const pluginPlaywrightCommand = pluginCommand.command('playwright').description('管理 playwright 插件服务');
1462
- registerPlaywrightAliasCommands(pluginPlaywrightCommand);
1369
+ [
1370
+ ['down', '停止当前模式,回到默认模式'],
1371
+ ['status', '查看当前模式并真实探测浏览器是否可用(不可用时退出码非 0)'],
1372
+ ['logs', '查看当前模式的浏览器服务日志'],
1373
+ ['mcp-add', '输出在容器内注册 Playwright MCP(stdio,随当前模式自动切换)的命令']
1374
+ ].forEach(([action, description]) => {
1375
+ playwrightCommand.command(action)
1376
+ .description(description)
1377
+ .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1378
+ .action(options => selectPlaywright(action, {}, options));
1379
+ });
1380
+ playwrightCommand.command('ext-download')
1381
+ .description('下载并解压 Playwright 扩展到 ~/.manyoyo/plugin/playwright/extensions/')
1382
+ .option('--prodversion <ver>', 'CRX 下载使用的 Chrome 版本号 (默认 132.0.0.0)')
1383
+ .action(options => selectPlaywright('ext-download', { prodversion: options.prodversion || '' }, options));
1463
1384
 
1464
1385
  // 安装包装完后调用的隐藏入口(不在帮助里):按有头 / 无头决定下一步
1465
1386
  if (process.argv[2] === '--post-install') {
@@ -1473,6 +1394,9 @@ https://github.com/xcanwin/manyoyo
1473
1394
  await runAppLauncher({ force: launcherArgs.force });
1474
1395
  }
1475
1396
 
1397
+ // 顶层 -V 与 -v 等价(commander 只允许一个短选项;子命令里的 -v 仍是 --volume,不受影响)
1398
+ if (process.argv.length === 3 && process.argv[2] === '-V') process.argv[2] = '-v';
1399
+
1476
1400
  // Pre-handle -x/--shell-full: treat all following args as a single command
1477
1401
  normalizeShellFullArgv(process.argv);
1478
1402
  normalizeWorktreeArgv(process.argv);
@@ -1509,7 +1433,7 @@ https://github.com/xcanwin/manyoyo
1509
1433
 
1510
1434
  UPDATE_CHECK_ENABLED = config.updateCheck !== false;
1511
1435
  MIRRORS = normalizeMirrors(config.mirrors);
1512
- const noDockerActions = new Set(['init', 'update', 'config-show', 'plugin', 'doctor', 'uninstall', 'podman', 'setup']);
1436
+ const noDockerActions = new Set(['init', 'update', 'config-show', 'playwright', 'doctor', 'uninstall', 'podman', 'setup']);
1513
1437
  if (isServerStopMode || isServerListMode) {
1514
1438
  noDockerActions.add('serve');
1515
1439
  }
@@ -1563,26 +1487,20 @@ https://github.com/xcanwin/manyoyo
1563
1487
  process.exit(0);
1564
1488
  }
1565
1489
 
1566
- if (selectedAction === 'plugin') {
1490
+ if (selectedAction === 'playwright') {
1567
1491
  const runConfig = options.run ? loadRunConfig(options.run, config) : {};
1568
1492
  return {
1569
- isPluginMode: true,
1570
- pluginRequest: {
1571
- action: options.pluginAction,
1572
- pluginName: options.pluginName,
1573
- scene: options.pluginScene || 'mcp-host-headless',
1574
- host: options.pluginHost || '',
1575
- extensionPaths: Array.isArray(options.pluginExtensionPaths) ? options.pluginExtensionPaths : [],
1576
- extensionNames: Array.isArray(options.pluginExtensionNames) ? options.pluginExtensionNames : [],
1577
- prodversion: options.pluginProdversion || ''
1578
- },
1579
- pluginGlobalConfig: config,
1580
- pluginRunConfig: runConfig
1493
+ isPlaywrightMode: true,
1494
+ playwrightRequest: options.playwrightRequest,
1495
+ playwrightGlobalConfig: config,
1496
+ playwrightRunConfig: runConfig
1581
1497
  };
1582
1498
  }
1583
1499
 
1584
- // Load run config if specified
1585
- const runConfig = options.run ? loadRunConfig(options.run, config) : {};
1500
+ // Load run config if specified;doctor 不带 -r 且 runs 里只有一个配置时,按它检查
1501
+ const runsMap = config.runs && typeof config.runs === 'object' && !Array.isArray(config.runs) ? Object.keys(config.runs) : [];
1502
+ const runNameToLoad = options.run || (selectedAction === 'doctor' && runsMap.length === 1 ? runsMap[0] : '');
1503
+ const runConfig = runNameToLoad ? loadRunConfig(runNameToLoad, config) : {};
1586
1504
  const globalFirstConfig = normalizeFirstConfig(config.first, '全局配置');
1587
1505
  const runFirstConfig = normalizeFirstConfig(runConfig.first, '运行配置');
1588
1506
 
@@ -1644,7 +1562,7 @@ https://github.com/xcanwin/manyoyo
1644
1562
  const firstEnvMap = resolvedRuntime.first.env;
1645
1563
  Object.entries(firstEnvMap).forEach(([key, value]) => addEnvTo(FIRST_CONTAINER_ENVS, `${key}=${value}`));
1646
1564
 
1647
- applyPlaywrightCliSessionIntegration(config, runConfig);
1565
+ PLAYWRIGHT_PLUGIN_CONFIG = { globalConfig: config, runConfig };
1648
1566
 
1649
1567
  const volumeList = resolvedRuntime.volumes;
1650
1568
  volumeList.forEach(v => addVolume(v));
@@ -1814,7 +1732,7 @@ https://github.com/xcanwin/manyoyo
1814
1732
  isServerDetach: Boolean(selectedAction === 'serve' && options.detach),
1815
1733
  isServerListenSpecified: Boolean(isServerMode && options.server !== true),
1816
1734
  updateAgents: Boolean(options.updateAgents),
1817
- isPluginMode: false
1735
+ isPlaywrightMode: false
1818
1736
  };
1819
1737
  }
1820
1738
 
@@ -2165,6 +2083,8 @@ async function createNewContainer(runtime) {
2165
2083
  );
2166
2084
  const defaultCommand = runtime.execCommand;
2167
2085
 
2086
+ await applyPlaywrightIntegration(runtime);
2087
+
2168
2088
  if (runtime.showCommand) {
2169
2089
  console.log(buildDockerRunCmd(runtime));
2170
2090
  process.exit(0);
@@ -2175,10 +2095,12 @@ async function createNewContainer(runtime) {
2175
2095
  // 使用数组参数执行命令(安全方式)
2176
2096
  try {
2177
2097
  const args = buildDockerRunArgs(runtime);
2178
- dockerExecArgs(args, { stdio: 'pipe' });
2098
+ runWithEnvFile(args, path.join(os.homedir(), '.manyoyo', 'tmp'), finalArgs => dockerExecArgs(finalArgs, { stdio: 'pipe' }));
2179
2099
  } catch (e) {
2180
2100
  showImagePullHint(e);
2181
- throw e;
2101
+ // 失败的 run 可能留下 Created 状态的容器;这个名字创建前不存在,所以只清理本次留下的这一个
2102
+ try { dockerExecArgs(['rm', '-f', runtime.containerName], { stdio: 'pipe' }); } catch (cleanupError) { /* 尽力清理 */ }
2103
+ throw explainCreateFailure(e);
2182
2104
  }
2183
2105
 
2184
2106
  // Wait for container to be ready
@@ -2223,7 +2145,7 @@ function buildDockerRunArgs(runtime) {
2223
2145
  * @returns {string} 命令字符串
2224
2146
  */
2225
2147
  function buildDockerRunCmd(runtime) {
2226
- const args = buildDockerRunArgs(runtime);
2148
+ const args = redactCommandArgs(buildDockerRunArgs(runtime));
2227
2149
  return buildContainerRunCommand(DOCKER_CMD, args);
2228
2150
  }
2229
2151
 
@@ -2453,6 +2375,11 @@ async function runWebServerMode(runtime) {
2453
2375
  fetchLatest: () => appUpdate.fetchLatestRelease()
2454
2376
  });
2455
2377
 
2378
+ // 启动时检查 Playwright 当前模式是否还活着,失效就回退到默认模式(只警告,不阻塞启动)
2379
+ buildContainerIntegration({ ...PLAYWRIGHT_PLUGIN_CONFIG, runtimeCommand: DOCKER_CMD })
2380
+ .then(integration => { if (integration.warning) console.warn(`${YELLOW}⚠️ ${integration.warning}${NC}`); })
2381
+ .catch(() => {});
2382
+
2456
2383
  const needRuntimeHeal = !isRuntimeProven();
2457
2384
  if (needRuntimeHeal) {
2458
2385
  RUNTIME_STATE.status = 'starting';
@@ -2566,11 +2493,10 @@ async function main() {
2566
2493
  // 1. Setup commander and parse arguments
2567
2494
  const modeState = await setupCommander();
2568
2495
 
2569
- if (modeState.isPluginMode) {
2570
- const exitCode = await runPluginCommand(modeState.pluginRequest, {
2571
- globalConfig: modeState.pluginGlobalConfig,
2572
- runConfig: modeState.pluginRunConfig,
2573
- projectRoot: path.join(__dirname, '..'),
2496
+ if (modeState.isPlaywrightMode) {
2497
+ const exitCode = await runPlaywrightCommand(modeState.playwrightRequest, {
2498
+ globalConfig: modeState.playwrightGlobalConfig,
2499
+ runConfig: modeState.playwrightRunConfig,
2574
2500
  stdout: process.stdout,
2575
2501
  stderr: process.stderr
2576
2502
  });
@@ -95,7 +95,9 @@ ARG PY_TEXT_EXTRA_PIP_PACKAGES=""
95
95
  ENV LANG=C.UTF-8 \
96
96
  LC_ALL=C.UTF-8 \
97
97
  PIP_ROOT_USER_ACTION=ignore \
98
- PLAYWRIGHT_MCP_CONFIG=/app/config/cli-cont-headless.json
98
+ NO_UPDATE_NOTIFIER=1 \
99
+ DISPLAY=:99 \
100
+ PLAYWRIGHT_MCP_CONFIG=/run/manyoyo-playwright/config.json
99
101
 
100
102
  # 合并系统依赖与 Python 安装为单层,减少镜像体积
101
103
  RUN <<EOX
@@ -152,8 +154,6 @@ EOX
152
154
  # 从 cache-stage 复制 Node.js(缓存或下载)
153
155
  COPY --from=cache-stage /opt/node /usr/local
154
156
  COPY ./package.json /tmp/manyoyo-package.json
155
- COPY ./docker/res/playwright/cli-cont-headless.init.js /app/config/cli-cont-headless.init.js
156
- COPY ./docker/res/playwright/cli-cont-headless.json /app/config/cli-cont-headless.json
157
157
  COPY ./docker/res/ /tmp/docker-res/
158
158
  ARG GIT_SSL_NO_VERIFY=false
159
159
 
@@ -230,12 +230,18 @@ RUN <<EOX
230
230
  cp /tmp/docker-res/opencode/opencode.json ~/.config/opencode/opencode.json
231
231
  ;; esac
232
232
 
233
- # 安装 Playwright CLI skills(不在镜像构建阶段下载浏览器)
233
+ # 安装 Playwright CLI skills 与 chromium 浏览器
234
234
  PLAYWRIGHT_CLI_INSTALL_DIR=/tmp/playwright-cli-install
235
235
  mkdir -p "$PLAYWRIGHT_CLI_INSTALL_DIR/.playwright"
236
236
  cd "$PLAYWRIGHT_CLI_INSTALL_DIR"
237
237
  PLAYWRIGHT_CLI_VERSION=$(node -p "const pkg = require('/tmp/manyoyo-package.json'); const value = String(pkg.playwrightCliVersion || '').trim(); if (!value) { throw new Error('package.json.playwrightCliVersion is required'); } value")
238
238
  npm install -g "@playwright/cli@${PLAYWRIGHT_CLI_VERSION}"
239
+ # 浏览器与系统依赖都用 @playwright/cli 自带的 playwright-core 安装;
240
+ # 默认在 Xvfb 虚拟屏(配轻量窗口管理器)里跑有头浏览器,所以装 xvfb、fluxbox 与常见字体;x11vnc / noVNC 供 playwright vnc 模式使用
241
+ PLAYWRIGHT_CORE_CLI="$(npm root -g)/@playwright/cli/node_modules/playwright-core/cli.js"
242
+ node "$PLAYWRIGHT_CORE_CLI" install-deps chromium
243
+ apt-get install -y --no-install-recommends xvfb fluxbox x11vnc novnc websockify fonts-noto-cjk fonts-noto-color-emoji fonts-liberation
244
+ node "$PLAYWRIGHT_CORE_CLI" install --no-shell chromium
239
245
  playwright-cli install --skills
240
246
  PLAYWRIGHT_CLI_SKILL_SOURCE="$PLAYWRIGHT_CLI_INSTALL_DIR/.claude/skills/playwright-cli"
241
247
  for target in ~/.claude/skills/playwright-cli ~/.codex/skills/playwright-cli ~/.gemini/skills/playwright-cli; do
@@ -252,6 +258,12 @@ RUN <<EOX
252
258
  rm -f /var/log/dpkg.log /var/log/bootstrap.log /var/lib/dpkg/status-old /var/cache/debconf/templates.dat-old
253
259
  EOX
254
260
 
261
+ # Playwright 默认配置(挂载宿主机目录时会被覆盖,路径固定)与按需启动 Xvfb 的 playwright-cli 包装
262
+ COPY ./docker/res/playwright/browser.json /run/manyoyo-playwright/config.json
263
+ COPY ./docker/res/playwright/stealth.init.js /run/manyoyo-playwright/stealth.init.js
264
+ COPY --chmod=755 ./docker/res/playwright/playwright-cli.sh /usr/local/sbin/playwright-cli
265
+ RUN ln -s playwright-cli /usr/local/sbin/playwright-mcp
266
+
255
267
  # 从 cache-stage 复制 JDT LSP 到最终位置,避免中转层残留
256
268
  COPY --from=cache-stage /opt/jdtls /root/.local/share/jdtls
257
269
 
@@ -1,4 +1,9 @@
1
1
  {
2
+ "attribution": {
3
+ "commit": "",
4
+ "pr": "",
5
+ "sessionUrl": false
6
+ },
2
7
  "statusLine": {
3
8
  "type": "command",
4
9
  "command": "bash /root/.claude/statusline.sh"
@@ -0,0 +1,28 @@
1
+ {
2
+ "outputDir": "/tmp/.playwright-cli",
3
+ "browser": {
4
+ "chromiumSandbox": false,
5
+ "browserName": "chromium",
6
+ "initScript": [
7
+ "/run/manyoyo-playwright/stealth.init.js"
8
+ ],
9
+ "contextOptions": {
10
+ "locale": "en-US",
11
+ "timezoneId": "UTC",
12
+ "viewport": null,
13
+ "extraHTTPHeaders": {
14
+ "Accept-Language": "en-US,en;q=0.9"
15
+ }
16
+ },
17
+ "launchOptions": {
18
+ "channel": "chromium",
19
+ "headless": false,
20
+ "args": [
21
+ "--lang=en-US",
22
+ "--window-size=1920,1080",
23
+ "--disable-blink-features=AutomationControlled",
24
+ "--force-webrtc-ip-handling-policy=disable_non_proxied_udp"
25
+ ]
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,26 @@
1
+ #!/bin/sh
2
+ # 按需启动 Xvfb 与轻量窗口管理器(多个会话共用 :99),浏览器以有头模式跑在虚拟屏里。
3
+ # 以 playwright-cli 名字调用时交给 @playwright/cli;以 playwright-mcp 名字调用时启动 stdio MCP(读同一份 $PLAYWRIGHT_MCP_CONFIG)。
4
+ export DISPLAY=:99
5
+ CONFIG="${PLAYWRIGHT_MCP_CONFIG:-/run/manyoyo-playwright/config.json}"
6
+ # 浏览器在容器外(headed / chrome / vnc)时不需要虚拟屏
7
+ if ! grep -qE '"(remoteEndpoint|cdpEndpoint)"' "$CONFIG" 2>/dev/null; then
8
+ # 以锁文件里的进程是否活着为准;容器被 kill 后遗留的 socket 与锁文件要清掉
9
+ if ! kill -0 "$(cat /tmp/.X99-lock 2>/dev/null)" 2>/dev/null; then
10
+ rm -f /tmp/.X99-lock /tmp/.X11-unix/X99
11
+ setsid Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp >/dev/null 2>&1 &
12
+ i=0
13
+ while [ ! -S /tmp/.X11-unix/X99 ] && [ "$i" -lt 50 ]; do
14
+ sleep 0.1
15
+ i=$((i + 1))
16
+ done
17
+ # 窗口管理器让窗口有真实的外框与工作区(screen.availHeight < height),不再是裸 X 屏
18
+ setsid fluxbox >/dev/null 2>&1 &
19
+ sleep 0.5
20
+ fi
21
+ fi
22
+ PACKAGE=/usr/local/lib/node_modules/@playwright/cli
23
+ if [ "$(basename "$0")" = "playwright-mcp" ]; then
24
+ exec node "$PACKAGE/node_modules/playwright-core/cli.js" mcp --config "$CONFIG" "$@"
25
+ fi
26
+ exec node "$PACKAGE/playwright-cli.js" "$@"