@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 +88 -0
- package/README.md +5 -4
- package/bin/manyoyo.js +97 -171
- package/docker/manyoyo.Dockerfile +16 -4
- package/docker/res/claude/settings.json +5 -0
- package/docker/res/playwright/browser.json +28 -0
- package/docker/res/playwright/playwright-cli.sh +26 -0
- package/docker/res/playwright/stealth.init.js +14 -0
- package/lib/container-run.js +36 -0
- package/lib/doctor.js +36 -4
- package/lib/error-hints.js +10 -2
- package/lib/global-config.js +8 -33
- package/lib/init-config.js +11 -2
- package/lib/json5-text-edit.js +22 -4
- package/lib/plugin/fingerprint.js +176 -0
- package/lib/plugin/index.js +62 -47
- package/lib/plugin/playwright-assets/vnc-entry.sh +21 -0
- package/lib/plugin/playwright-extensions.js +299 -0
- package/lib/plugin/playwright-probe.js +53 -0
- package/lib/plugin/playwright-relay.js +88 -0
- package/lib/plugin/playwright-server.js +23 -0
- package/lib/plugin/playwright.js +707 -1853
- package/lib/rootless-network.js +68 -0
- package/lib/serve-log.js +11 -0
- package/lib/web/server.js +27 -3
- package/manyoyo.example.json +16 -28
- package/package.json +5 -6
- package/docker/res/playwright/cli-cont-headless.init.js +0 -12
- package/docker/res/playwright/cli-cont-headless.json +0 -37
- package/lib/plugin/playwright-assets/compose-headed.yaml +0 -19
- package/lib/plugin/playwright-assets/compose-headless.yaml +0 -11
- package/lib/plugin/playwright-assets/headed.Dockerfile +0 -15
- package/lib/plugin/playwright-assets/init-headed.sh +0 -17
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"
|
|
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="
|
|
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
|
-
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
|
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
|
-
|
|
1448
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
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
|
-
|
|
1462
|
-
|
|
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', '
|
|
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 === '
|
|
1490
|
+
if (selectedAction === 'playwright') {
|
|
1567
1491
|
const runConfig = options.run ? loadRunConfig(options.run, config) : {};
|
|
1568
1492
|
return {
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2098
|
+
runWithEnvFile(args, path.join(os.homedir(), '.manyoyo', 'tmp'), finalArgs => dockerExecArgs(finalArgs, { stdio: 'pipe' }));
|
|
2179
2099
|
} catch (e) {
|
|
2180
2100
|
showImagePullHint(e);
|
|
2181
|
-
|
|
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.
|
|
2570
|
-
const exitCode = await
|
|
2571
|
-
globalConfig: modeState.
|
|
2572
|
-
runConfig: modeState.
|
|
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
|
-
|
|
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
|
|
|
@@ -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" "$@"
|