@xcanwin/manyoyo 5.3.10 → 5.4.6
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 +3 -3
- package/bin/manyoyo.js +89 -15
- package/docker/manyoyo.Dockerfile +39 -19
- package/docker/res/playwright/cli-cont-headless.init.js +12 -0
- package/docker/res/playwright/cli-cont-headless.json +37 -0
- package/lib/container-run.js +1 -0
- package/lib/global-config.js +88 -0
- package/lib/init-config.js +1 -12
- package/lib/plugin/playwright-assets/compose-headed.yaml +1 -1
- package/lib/plugin/playwright-assets/compose-headless.yaml +1 -1
- package/lib/plugin/playwright.js +235 -52
- package/lib/web/server.js +4 -0
- package/manyoyo.example.json +12 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ AI Agent CLI 往往需要:
|
|
|
53
53
|
```bash
|
|
54
54
|
npm install -g @xcanwin/manyoyo
|
|
55
55
|
podman pull ubuntu:24.04 # 仅 Podman 需要
|
|
56
|
-
manyoyo build --iv 1.8.
|
|
56
|
+
manyoyo build --iv 1.8.8-common
|
|
57
57
|
manyoyo init all
|
|
58
58
|
manyoyo run -r claude
|
|
59
59
|
```
|
|
@@ -137,10 +137,10 @@ manyoyo config command
|
|
|
137
137
|
|
|
138
138
|
```bash
|
|
139
139
|
# common 版本
|
|
140
|
-
manyoyo build --iv 1.8.
|
|
140
|
+
manyoyo build --iv 1.8.8-common
|
|
141
141
|
|
|
142
142
|
# full 版本
|
|
143
|
-
manyoyo build --iv 1.8.
|
|
143
|
+
manyoyo build --iv 1.8.8-full
|
|
144
144
|
|
|
145
145
|
# 自定义工具集
|
|
146
146
|
manyoyo build --iba TOOL=go,codex,java,gemini
|
package/bin/manyoyo.js
CHANGED
|
@@ -8,13 +8,13 @@ const crypto = require('crypto');
|
|
|
8
8
|
const net = require('net');
|
|
9
9
|
const readline = require('readline');
|
|
10
10
|
const { Command } = require('commander');
|
|
11
|
-
const JSON5 = require('json5');
|
|
12
11
|
const { startWebServer } = require('../lib/web/server');
|
|
13
12
|
const { buildContainerRunArgs, buildContainerRunCommand } = require('../lib/container-run');
|
|
13
|
+
const { getManyoyoConfigPath, readManyoyoConfig, syncGlobalImageVersion } = require('../lib/global-config');
|
|
14
14
|
const { initAgentConfigs } = require('../lib/init-config');
|
|
15
15
|
const { buildImage } = require('../lib/image-build');
|
|
16
16
|
const { resolveAgentResumeArg, buildAgentResumeCommand } = require('../lib/agent-resume');
|
|
17
|
-
const { runPluginCommand } = require('../lib/plugin');
|
|
17
|
+
const { runPluginCommand, createPlugin } = require('../lib/plugin');
|
|
18
18
|
const { buildManyoyoLogPath } = require('../lib/log-path');
|
|
19
19
|
const {
|
|
20
20
|
sanitizeSensitiveData,
|
|
@@ -71,6 +71,7 @@ let CONTAINER_ENVS = [];
|
|
|
71
71
|
let FIRST_CONTAINER_ENVS = [];
|
|
72
72
|
let CONTAINER_VOLUMES = [];
|
|
73
73
|
let CONTAINER_PORTS = [];
|
|
74
|
+
let CONTAINER_EXTRA_ARGS = [];
|
|
74
75
|
const MANYOYO_NAME = detectCommandName();
|
|
75
76
|
let CONT_MODE_ARGS = [];
|
|
76
77
|
let QUIET = {};
|
|
@@ -308,19 +309,29 @@ function installServeProcessDiagnostics(logger) {
|
|
|
308
309
|
* @returns {Config} 配置对象
|
|
309
310
|
*/
|
|
310
311
|
function loadConfig() {
|
|
311
|
-
const
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return config;
|
|
316
|
-
} catch (e) {
|
|
317
|
-
console.error(`${YELLOW}⚠️ 配置文件格式错误: ${configPath}${NC}`);
|
|
312
|
+
const result = readManyoyoConfig();
|
|
313
|
+
if (result.exists) {
|
|
314
|
+
if (result.parseError) {
|
|
315
|
+
console.error(`${YELLOW}⚠️ 配置文件格式错误: ${result.path}${NC}`);
|
|
318
316
|
return {};
|
|
319
317
|
}
|
|
318
|
+
return result.config;
|
|
320
319
|
}
|
|
321
320
|
return {};
|
|
322
321
|
}
|
|
323
322
|
|
|
323
|
+
function syncBuiltImageVersionToGlobalConfig(imageVersion) {
|
|
324
|
+
const syncResult = syncGlobalImageVersion(imageVersion);
|
|
325
|
+
if (syncResult.updated) {
|
|
326
|
+
console.log(`${GREEN}✅ 已同步 ${path.basename(getManyoyoConfigPath())} 的 imageVersion: ${imageVersion}${NC}`);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (syncResult.reason === 'unchanged') {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
console.log(`${YELLOW}⚠️ 镜像构建成功,但未更新 imageVersion: ${syncResult.path}${NC}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
324
335
|
function loadRunConfig(name, config) {
|
|
325
336
|
const runName = String(name || '').trim();
|
|
326
337
|
if (!runName) {
|
|
@@ -559,6 +570,63 @@ function addEnvFile(envFile) {
|
|
|
559
570
|
return addEnvFileTo(CONTAINER_ENVS, envFile);
|
|
560
571
|
}
|
|
561
572
|
|
|
573
|
+
function hasEnvKey(targetEnvs, key) {
|
|
574
|
+
for (let i = 0; i < targetEnvs.length; i += 2) {
|
|
575
|
+
if (targetEnvs[i] !== '--env') {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const text = String(targetEnvs[i + 1] || '');
|
|
579
|
+
const idx = text.indexOf('=');
|
|
580
|
+
if (idx > 0 && text.slice(0, idx) === key) {
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function appendUniqueArgs(targetArgs, extraArgs) {
|
|
588
|
+
const joinedExisting = new Set();
|
|
589
|
+
for (let i = 0; i < targetArgs.length; i += 2) {
|
|
590
|
+
const head = String(targetArgs[i] || '');
|
|
591
|
+
const value = String(targetArgs[i + 1] || '');
|
|
592
|
+
if (head.startsWith('--')) {
|
|
593
|
+
joinedExisting.add(`${head}\u0000${value}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
for (let i = 0; i < extraArgs.length; i += 2) {
|
|
598
|
+
const head = String(extraArgs[i] || '');
|
|
599
|
+
const value = String(extraArgs[i + 1] || '');
|
|
600
|
+
const signature = `${head}\u0000${value}`;
|
|
601
|
+
if (!joinedExisting.has(signature)) {
|
|
602
|
+
joinedExisting.add(signature);
|
|
603
|
+
targetArgs.push(head, value);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function applyPlaywrightCliSessionIntegration(config, runConfig) {
|
|
609
|
+
try {
|
|
610
|
+
const plugin = createPlugin('playwright', {
|
|
611
|
+
globalConfig: config,
|
|
612
|
+
runConfig,
|
|
613
|
+
projectRoot: path.join(__dirname, '..')
|
|
614
|
+
});
|
|
615
|
+
const integration = plugin.buildCliSessionIntegration(DOCKER_CMD);
|
|
616
|
+
for (const entry of integration.envEntries) {
|
|
617
|
+
const parsed = parseEnvEntry(entry);
|
|
618
|
+
if (!hasEnvKey(CONTAINER_ENVS, parsed.key)) {
|
|
619
|
+
addEnv(`${parsed.key}=${parsed.value}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
appendUniqueArgs(CONTAINER_EXTRA_ARGS, integration.extraArgs);
|
|
623
|
+
appendUniqueArgs(CONTAINER_VOLUMES, integration.volumeEntries || []);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
console.error(`${RED}⚠️ 错误: Playwright CLI 会话注入失败: ${error.message || String(error)}${NC}`);
|
|
626
|
+
process.exit(1);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
562
630
|
function addVolume(volume) {
|
|
563
631
|
CONTAINER_VOLUMES.push("--volume", volume);
|
|
564
632
|
}
|
|
@@ -954,7 +1022,7 @@ async function setupCommander() {
|
|
|
954
1022
|
...options,
|
|
955
1023
|
pluginAction: params.action || 'ls',
|
|
956
1024
|
pluginName: params.pluginName || 'playwright',
|
|
957
|
-
pluginScene: params.scene || 'host-headless',
|
|
1025
|
+
pluginScene: params.scene || 'mcp-host-headless',
|
|
958
1026
|
pluginHost: params.host || '',
|
|
959
1027
|
pluginExtensionPaths: Array.isArray(params.extensionPaths) ? params.extensionPaths : [],
|
|
960
1028
|
pluginExtensionNames: Array.isArray(params.extensionNames) ? params.extensionNames : [],
|
|
@@ -975,7 +1043,7 @@ async function setupCommander() {
|
|
|
975
1043
|
const actions = ['up', 'down', 'status', 'health', 'logs'];
|
|
976
1044
|
actions.forEach(action => {
|
|
977
1045
|
const sceneCommand = command.command(`${action} [scene]`)
|
|
978
|
-
.description(`执行 playwright ${action} 场景(scene 默认 host-headless)`)
|
|
1046
|
+
.description(`执行 playwright ${action} 场景(scene 默认 mcp-host-headless)`)
|
|
979
1047
|
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)');
|
|
980
1048
|
|
|
981
1049
|
if (action === 'up') {
|
|
@@ -986,7 +1054,7 @@ async function setupCommander() {
|
|
|
986
1054
|
sceneCommand.action((scene, options) => selectPluginAction({
|
|
987
1055
|
action,
|
|
988
1056
|
pluginName: 'playwright',
|
|
989
|
-
scene: scene || 'host-headless',
|
|
1057
|
+
scene: scene || 'mcp-host-headless',
|
|
990
1058
|
extensionPaths: action === 'up' ? (options.extPath || []) : [],
|
|
991
1059
|
extensionNames: action === 'up' ? (options.extName || []) : []
|
|
992
1060
|
}, options));
|
|
@@ -1041,8 +1109,8 @@ async function setupCommander() {
|
|
|
1041
1109
|
${MANYOYO_NAME} serve 127.0.0.1:3000 启动本机网页服务
|
|
1042
1110
|
${MANYOYO_NAME} serve 127.0.0.1:3000 -d 后台启动;未设密码时会打印本次随机密码
|
|
1043
1111
|
${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P 123 -d 后台启动并监听全部网卡
|
|
1044
|
-
${MANYOYO_NAME} playwright up host-headless 启动 playwright 默认场景(推荐)
|
|
1045
|
-
${MANYOYO_NAME} plugin playwright up host-headless 通过 plugin 命名空间启动
|
|
1112
|
+
${MANYOYO_NAME} playwright up mcp-host-headless 启动 playwright 默认场景(推荐)
|
|
1113
|
+
${MANYOYO_NAME} plugin playwright up mcp-host-headless 通过 plugin 命名空间启动
|
|
1046
1114
|
${MANYOYO_NAME} run -n test -q tip -q cmd 多次使用静默选项
|
|
1047
1115
|
`);
|
|
1048
1116
|
|
|
@@ -1208,7 +1276,7 @@ Notes:
|
|
|
1208
1276
|
pluginRequest: {
|
|
1209
1277
|
action: options.pluginAction,
|
|
1210
1278
|
pluginName: options.pluginName,
|
|
1211
|
-
scene: options.pluginScene || 'host-headless',
|
|
1279
|
+
scene: options.pluginScene || 'mcp-host-headless',
|
|
1212
1280
|
host: options.pluginHost || '',
|
|
1213
1281
|
extensionPaths: Array.isArray(options.pluginExtensionPaths) ? options.pluginExtensionPaths : [],
|
|
1214
1282
|
extensionNames: Array.isArray(options.pluginExtensionNames) ? options.pluginExtensionNames : [],
|
|
@@ -1302,6 +1370,8 @@ Notes:
|
|
|
1302
1370
|
};
|
|
1303
1371
|
Object.entries(firstEnvMap).forEach(([key, value]) => addEnvTo(FIRST_CONTAINER_ENVS, `${key}=${value}`));
|
|
1304
1372
|
|
|
1373
|
+
applyPlaywrightCliSessionIntegration(config, runConfig);
|
|
1374
|
+
|
|
1305
1375
|
const volumeList = mergeArrayConfig(config.volumes, runConfig.volumes, options.volume);
|
|
1306
1376
|
volumeList.forEach(v => addVolume(v));
|
|
1307
1377
|
|
|
@@ -1438,6 +1508,7 @@ function createRuntimeContext(modeState = {}) {
|
|
|
1438
1508
|
firstExecCommandPrefix: FIRST_EXEC_COMMAND_PREFIX,
|
|
1439
1509
|
firstExecCommandSuffix: FIRST_EXEC_COMMAND_SUFFIX,
|
|
1440
1510
|
contModeArgs: CONT_MODE_ARGS,
|
|
1511
|
+
containerExtraArgs: CONTAINER_EXTRA_ARGS,
|
|
1441
1512
|
containerEnvs: CONTAINER_ENVS,
|
|
1442
1513
|
firstContainerEnvs: FIRST_CONTAINER_ENVS,
|
|
1443
1514
|
containerVolumes: CONTAINER_VOLUMES,
|
|
@@ -1662,6 +1733,7 @@ function buildDockerRunArgs(runtime) {
|
|
|
1662
1733
|
imageName: runtime.imageName,
|
|
1663
1734
|
imageVersion: runtime.imageVersion,
|
|
1664
1735
|
contModeArgs: runtime.contModeArgs,
|
|
1736
|
+
containerExtraArgs: runtime.containerExtraArgs,
|
|
1665
1737
|
containerEnvs: runtime.containerEnvs,
|
|
1666
1738
|
containerVolumes: runtime.containerVolumes,
|
|
1667
1739
|
containerPorts: runtime.containerPorts,
|
|
@@ -1826,6 +1898,7 @@ async function runWebServerMode(runtime) {
|
|
|
1826
1898
|
execCommand: runtime.execCommand,
|
|
1827
1899
|
execCommandSuffix: runtime.execCommandSuffix,
|
|
1828
1900
|
contModeArgs: runtime.contModeArgs,
|
|
1901
|
+
containerExtraArgs: runtime.containerExtraArgs,
|
|
1829
1902
|
containerEnvs: runtime.containerEnvs,
|
|
1830
1903
|
containerVolumes: runtime.containerVolumes,
|
|
1831
1904
|
containerPorts: runtime.containerPorts,
|
|
@@ -1908,6 +1981,7 @@ async function main() {
|
|
|
1908
1981
|
pruneDanglingImages,
|
|
1909
1982
|
colors: { RED, GREEN, YELLOW, BLUE, CYAN, NC }
|
|
1910
1983
|
});
|
|
1984
|
+
syncBuiltImageVersionToGlobalConfig(runtime.imageVersion);
|
|
1911
1985
|
process.exit(0);
|
|
1912
1986
|
}
|
|
1913
1987
|
|
|
@@ -64,7 +64,7 @@ FROM ubuntu:24.04
|
|
|
64
64
|
|
|
65
65
|
ARG TARGETARCH
|
|
66
66
|
ARG NODE_VERSION=24
|
|
67
|
-
ARG TOOL="
|
|
67
|
+
ARG TOOL="common"
|
|
68
68
|
|
|
69
69
|
# 镜像源参数化(默认使用阿里云,可按需覆盖)
|
|
70
70
|
ARG APT_MIRROR=https://mirrors.aliyun.com
|
|
@@ -74,9 +74,11 @@ ARG PIP_INDEX_URL=https://mirrors.tencent.com/pypi/simple
|
|
|
74
74
|
ARG PY_TEXT_PIP_PACKAGES="PyYAML python-dotenv tomlkit pyjson5 jsonschema"
|
|
75
75
|
ARG PY_TEXT_EXTRA_PIP_PACKAGES=""
|
|
76
76
|
ENV LANG=C.UTF-8 \
|
|
77
|
-
LC_ALL=C.UTF-8
|
|
77
|
+
LC_ALL=C.UTF-8 \
|
|
78
|
+
PIP_ROOT_USER_ACTION=ignore \
|
|
79
|
+
PLAYWRIGHT_MCP_CONFIG=/app/config/cli-cont-headless.json
|
|
78
80
|
|
|
79
|
-
#
|
|
81
|
+
# 合并系统依赖与 Python 安装为单层,减少镜像体积
|
|
80
82
|
RUN <<EOX
|
|
81
83
|
# 配置 APT 镜像源
|
|
82
84
|
sed -i "s|http://[^/]*\.ubuntu\.com|${APT_MIRROR}|g" /etc/apt/sources.list.d/ubuntu.sources
|
|
@@ -87,12 +89,14 @@ RUN <<EOX
|
|
|
87
89
|
# 开发与构建
|
|
88
90
|
# 系统管理
|
|
89
91
|
# 通用工具
|
|
92
|
+
# Python
|
|
90
93
|
apt-get -o Acquire::https::Verify-Peer=false update -y
|
|
91
94
|
apt-get -o Acquire::https::Verify-Peer=false install -y --no-install-recommends \
|
|
92
95
|
ca-certificates openssl curl wget net-tools iputils-ping dnsutils socat ncat ssh \
|
|
93
96
|
git gh g++ make sqlite3 \
|
|
94
|
-
procps psmisc lsof supervisor
|
|
95
|
-
nano jq file tree ripgrep less bc xxd tar zip unzip gzip
|
|
97
|
+
procps psmisc lsof supervisor \
|
|
98
|
+
nano jq file tree ripgrep less bc xxd tar zip unzip gzip \
|
|
99
|
+
python3.12 python3.12-dev python3.12-venv python3-pip
|
|
96
100
|
|
|
97
101
|
# 更新 CA 证书
|
|
98
102
|
update-ca-certificates
|
|
@@ -107,15 +111,7 @@ RUN <<EOX
|
|
|
107
111
|
apt-get install -y --no-install-recommends docker.io
|
|
108
112
|
;; esac
|
|
109
113
|
|
|
110
|
-
#
|
|
111
|
-
apt-get clean
|
|
112
|
-
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.cache ~/.npm ~/go/pkg/mod/cache
|
|
113
|
-
EOX
|
|
114
|
-
|
|
115
|
-
RUN <<EOX
|
|
116
|
-
# 安装 python
|
|
117
|
-
apt-get update -y
|
|
118
|
-
apt-get install -y --no-install-recommends python3.12 python3.12-dev python3.12-venv python3-pip
|
|
114
|
+
# 配置 python
|
|
119
115
|
ln -sf /usr/bin/python3 /usr/bin/python
|
|
120
116
|
ln -sf /usr/bin/pip3 /usr/bin/pip
|
|
121
117
|
pip config set global.index-url "${PIP_INDEX_URL}"
|
|
@@ -131,6 +127,8 @@ EOX
|
|
|
131
127
|
|
|
132
128
|
# 从 cache-stage 复制 Node.js(缓存或下载)
|
|
133
129
|
COPY --from=cache-stage /opt/node /usr/local
|
|
130
|
+
COPY ./docker/res/playwright/cli-cont-headless.init.js /app/config/cli-cont-headless.init.js
|
|
131
|
+
COPY ./docker/res/playwright/cli-cont-headless.json /app/config/cli-cont-headless.json
|
|
134
132
|
COPY ./docker/res/ /tmp/docker-res/
|
|
135
133
|
ARG GIT_SSL_NO_VERIFY=false
|
|
136
134
|
|
|
@@ -203,9 +201,31 @@ RUN <<EOX
|
|
|
203
201
|
cp /tmp/docker-res/opencode/opencode.json ~/.config/opencode/opencode.json
|
|
204
202
|
;; esac
|
|
205
203
|
|
|
204
|
+
# 安装 Playwright CLI skills(不在镜像构建阶段下载浏览器)
|
|
205
|
+
npm install -g @playwright/cli@latest
|
|
206
|
+
PLAYWRIGHT_CLI_INSTALL_DIR=/tmp/playwright-cli-install
|
|
207
|
+
mkdir -p "${PLAYWRIGHT_CLI_INSTALL_DIR}/.playwright"
|
|
208
|
+
cat > "${PLAYWRIGHT_CLI_INSTALL_DIR}/.playwright/cli.config.json" <<'EOF'
|
|
209
|
+
{
|
|
210
|
+
"browser": {
|
|
211
|
+
"browserName": "chromium",
|
|
212
|
+
"launchOptions": {
|
|
213
|
+
"channel": "chrome"
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
EOF
|
|
218
|
+
cd "${PLAYWRIGHT_CLI_INSTALL_DIR}"
|
|
219
|
+
playwright-cli install --skills
|
|
220
|
+
mkdir -p "$HOME/.codex/skills/playwright-cli" ~/.gemini/skills/playwright-cli
|
|
221
|
+
cp -R "${PLAYWRIGHT_CLI_INSTALL_DIR}/.claude/skills/playwright-cli/." "$HOME/.codex/skills/playwright-cli/"
|
|
222
|
+
cp -R "${PLAYWRIGHT_CLI_INSTALL_DIR}/.claude/skills/playwright-cli/." "$HOME/.gemini/skills/playwright-cli/"
|
|
223
|
+
cd $OLDPWD
|
|
224
|
+
rm -rf "${PLAYWRIGHT_CLI_INSTALL_DIR}"
|
|
225
|
+
|
|
206
226
|
# 清理
|
|
207
227
|
npm cache clean --force
|
|
208
|
-
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.
|
|
228
|
+
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.npm ~/go/pkg/mod/cache
|
|
209
229
|
EOX
|
|
210
230
|
|
|
211
231
|
# 从 cache-stage 复制 JDT LSP(缓存或下载)
|
|
@@ -224,7 +244,7 @@ RUN <<EOX
|
|
|
224
244
|
|
|
225
245
|
# 清理
|
|
226
246
|
apt-get clean
|
|
227
|
-
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.
|
|
247
|
+
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.npm ~/go/pkg/mod/cache
|
|
228
248
|
;; esac
|
|
229
249
|
rm -rf /tmp/jdtls-cache
|
|
230
250
|
EOX
|
|
@@ -236,7 +256,7 @@ RUN <<EOX
|
|
|
236
256
|
# 安装 go
|
|
237
257
|
case ",$TOOL," in *,full,*|*,go,*)
|
|
238
258
|
apt-get update -y
|
|
239
|
-
apt-get install -y --no-install-recommends golang
|
|
259
|
+
apt-get install -y --no-install-recommends golang gcc
|
|
240
260
|
go env -w GOPROXY=https://mirrors.tencent.com/go
|
|
241
261
|
|
|
242
262
|
# 安装 LSP服务(go)
|
|
@@ -252,7 +272,7 @@ RUN <<EOX
|
|
|
252
272
|
# 清理
|
|
253
273
|
apt-get clean
|
|
254
274
|
go clean -modcache -cache
|
|
255
|
-
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.
|
|
275
|
+
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.npm ~/go/pkg/mod/cache
|
|
256
276
|
;; esac
|
|
257
277
|
rm -rf /tmp/gopls-cache
|
|
258
278
|
EOX
|
|
@@ -262,7 +282,7 @@ COPY ./docker/res/supervisor/s.conf /etc/supervisor/conf.d/s.conf
|
|
|
262
282
|
|
|
263
283
|
RUN <<EOX
|
|
264
284
|
# 清理
|
|
265
|
-
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.
|
|
285
|
+
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.npm ~/go/pkg/mod/cache
|
|
266
286
|
EOX
|
|
267
287
|
|
|
268
288
|
WORKDIR /tmp
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
(function () {
|
|
4
|
+
const platformValue = 'MacIntel';
|
|
5
|
+
try {
|
|
6
|
+
const navProto = Object.getPrototypeOf(navigator);
|
|
7
|
+
Object.defineProperty(navProto, 'platform', {
|
|
8
|
+
configurable: true,
|
|
9
|
+
get: () => platformValue
|
|
10
|
+
});
|
|
11
|
+
} catch (_) {}
|
|
12
|
+
})();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"outputDir": "/tmp/.playwright-cli",
|
|
3
|
+
"browser": {
|
|
4
|
+
"chromiumSandbox": true,
|
|
5
|
+
"browserName": "chromium",
|
|
6
|
+
"initScript": [
|
|
7
|
+
"/app/config/cli-cont-headless.init.js"
|
|
8
|
+
],
|
|
9
|
+
"launchOptions": {
|
|
10
|
+
"channel": "chromium",
|
|
11
|
+
"headless": true,
|
|
12
|
+
"args": [
|
|
13
|
+
"--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
|
14
|
+
"--lang=zh-CN",
|
|
15
|
+
"--window-size=1366,768",
|
|
16
|
+
"--disable-blink-features=AutomationControlled",
|
|
17
|
+
"--force-webrtc-ip-handling-policy=disable_non_proxied_udp"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"contextOptions": {
|
|
21
|
+
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
|
22
|
+
"locale": "zh-CN",
|
|
23
|
+
"timezoneId": "Asia/Shanghai",
|
|
24
|
+
"viewport": {
|
|
25
|
+
"width": 1366,
|
|
26
|
+
"height": 768
|
|
27
|
+
},
|
|
28
|
+
"screen": {
|
|
29
|
+
"width": 1366,
|
|
30
|
+
"height": 768
|
|
31
|
+
},
|
|
32
|
+
"extraHTTPHeaders": {
|
|
33
|
+
"Accept-Language": "zh-CN,zh;q=0.9"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
package/lib/container-run.js
CHANGED
|
@@ -10,6 +10,7 @@ function buildContainerRunArgs(options) {
|
|
|
10
10
|
'--name', options.containerName,
|
|
11
11
|
'--entrypoint', '',
|
|
12
12
|
...(options.contModeArgs || []),
|
|
13
|
+
...(options.containerExtraArgs || []),
|
|
13
14
|
...(options.containerEnvs || []),
|
|
14
15
|
...(options.containerVolumes || []),
|
|
15
16
|
...(options.containerPorts || []),
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const JSON5 = require('json5');
|
|
7
|
+
|
|
8
|
+
function getManyoyoConfigPath(homeDir = os.homedir()) {
|
|
9
|
+
return path.join(homeDir, '.manyoyo', 'manyoyo.json');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function readManyoyoConfig(homeDir = os.homedir()) {
|
|
13
|
+
const configPath = getManyoyoConfigPath(homeDir);
|
|
14
|
+
if (!fs.existsSync(configPath)) {
|
|
15
|
+
return {
|
|
16
|
+
path: configPath,
|
|
17
|
+
exists: false,
|
|
18
|
+
config: {}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const config = JSON5.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
24
|
+
return {
|
|
25
|
+
path: configPath,
|
|
26
|
+
exists: true,
|
|
27
|
+
config
|
|
28
|
+
};
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return {
|
|
31
|
+
path: configPath,
|
|
32
|
+
exists: true,
|
|
33
|
+
config: {},
|
|
34
|
+
parseError: error
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function syncGlobalImageVersion(imageVersion, options = {}) {
|
|
40
|
+
const homeDir = options.homeDir || os.homedir();
|
|
41
|
+
const result = readManyoyoConfig(homeDir);
|
|
42
|
+
const configPath = result.path;
|
|
43
|
+
|
|
44
|
+
if (result.parseError) {
|
|
45
|
+
return {
|
|
46
|
+
updated: false,
|
|
47
|
+
path: configPath,
|
|
48
|
+
reason: 'parse-error'
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const currentConfig = result.config;
|
|
53
|
+
if (typeof currentConfig !== 'object' || currentConfig === null || Array.isArray(currentConfig)) {
|
|
54
|
+
return {
|
|
55
|
+
updated: false,
|
|
56
|
+
path: configPath,
|
|
57
|
+
reason: 'invalid-root'
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (currentConfig.imageVersion === imageVersion) {
|
|
62
|
+
return {
|
|
63
|
+
updated: false,
|
|
64
|
+
path: configPath,
|
|
65
|
+
reason: 'unchanged'
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const nextConfig = {
|
|
70
|
+
...currentConfig,
|
|
71
|
+
imageVersion
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
75
|
+
fs.writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 4)}\n`);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
updated: true,
|
|
79
|
+
path: configPath,
|
|
80
|
+
reason: result.exists ? 'updated' : 'created'
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
getManyoyoConfigPath,
|
|
86
|
+
readManyoyoConfig,
|
|
87
|
+
syncGlobalImageVersion
|
|
88
|
+
};
|
package/lib/init-config.js
CHANGED
|
@@ -193,18 +193,7 @@ function collectCodexInitData(homeDir, ctx) {
|
|
|
193
193
|
|
|
194
194
|
if (configToml && typeof configToml === 'object') {
|
|
195
195
|
setInitValue(values, 'OPENAI_MODEL', configToml.model);
|
|
196
|
-
|
|
197
|
-
let providerConfig = null;
|
|
198
|
-
if (providers && typeof providers === 'object') {
|
|
199
|
-
const selectedProviderName =
|
|
200
|
-
(typeof configToml.model_provider === 'string' && providers[configToml.model_provider])
|
|
201
|
-
? configToml.model_provider
|
|
202
|
-
: Object.keys(providers)[0];
|
|
203
|
-
providerConfig = selectedProviderName ? providers[selectedProviderName] : null;
|
|
204
|
-
}
|
|
205
|
-
if (providerConfig && typeof providerConfig === 'object') {
|
|
206
|
-
setInitValue(values, 'OPENAI_BASE_URL', providerConfig.base_url);
|
|
207
|
-
}
|
|
196
|
+
setInitValue(values, 'OPENAI_BASE_URL', configToml.openai_base_url);
|
|
208
197
|
}
|
|
209
198
|
|
|
210
199
|
if (fs.existsSync(codexDir)) {
|
|
@@ -6,7 +6,7 @@ services:
|
|
|
6
6
|
args:
|
|
7
7
|
PLAYWRIGHT_MCP_BASE_IMAGE: mcr.microsoft.com/playwright/mcp:${PLAYWRIGHT_MCP_DOCKER_TAG:-latest}
|
|
8
8
|
image: ${PLAYWRIGHT_MCP_IMAGE:-localhost/xcanwin/manyoyo-playwright-headed}
|
|
9
|
-
container_name: ${PLAYWRIGHT_MCP_CONTAINER_NAME:-my-playwright-cont-headed}
|
|
9
|
+
container_name: ${PLAYWRIGHT_MCP_CONTAINER_NAME:-my-playwright-mcp-cont-headed}
|
|
10
10
|
init: true
|
|
11
11
|
stdin_open: true
|
|
12
12
|
ports:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
services:
|
|
2
2
|
playwright:
|
|
3
3
|
image: mcr.microsoft.com/playwright/mcp:${PLAYWRIGHT_MCP_DOCKER_TAG:-latest}
|
|
4
|
-
container_name: ${PLAYWRIGHT_MCP_CONTAINER_NAME:-my-playwright-cont-headless}
|
|
4
|
+
container_name: ${PLAYWRIGHT_MCP_CONTAINER_NAME:-my-playwright-mcp-cont-headless}
|
|
5
5
|
init: true
|
|
6
6
|
stdin_open: true
|
|
7
7
|
ports:
|
package/lib/plugin/playwright.js
CHANGED
|
@@ -15,42 +15,62 @@ const EXTENSIONS = [
|
|
|
15
15
|
['webgl-fingerprint-defender', 'olnbjpaejebpnokblkepbphhembdicik']
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
-
const SCENE_ORDER = ['cont-headless', 'cont-headed', 'host-headless', 'host-headed'];
|
|
18
|
+
const SCENE_ORDER = ['mcp-cont-headless', 'mcp-cont-headed', 'mcp-host-headless', 'mcp-host-headed', 'cli-host-headless', 'cli-host-headed'];
|
|
19
19
|
|
|
20
20
|
const SCENE_DEFS = {
|
|
21
|
-
'cont-headless': {
|
|
21
|
+
'mcp-cont-headless': {
|
|
22
22
|
type: 'container',
|
|
23
|
-
|
|
23
|
+
engine: 'mcp',
|
|
24
|
+
configFile: 'mcp-cont-headless.json',
|
|
24
25
|
composeFile: 'compose-headless.yaml',
|
|
25
|
-
projectName: 'my-playwright-cont-headless',
|
|
26
|
-
containerName: 'my-playwright-cont-headless',
|
|
27
|
-
portKey: '
|
|
26
|
+
projectName: 'my-playwright-mcp-cont-headless',
|
|
27
|
+
containerName: 'my-playwright-mcp-cont-headless',
|
|
28
|
+
portKey: 'mcpContHeadless',
|
|
28
29
|
headless: true,
|
|
29
30
|
listenHost: '0.0.0.0'
|
|
30
31
|
},
|
|
31
|
-
'cont-headed': {
|
|
32
|
+
'mcp-cont-headed': {
|
|
32
33
|
type: 'container',
|
|
33
|
-
|
|
34
|
+
engine: 'mcp',
|
|
35
|
+
configFile: 'mcp-cont-headed.json',
|
|
34
36
|
composeFile: 'compose-headed.yaml',
|
|
35
|
-
projectName: 'my-playwright-cont-headed',
|
|
36
|
-
containerName: 'my-playwright-cont-headed',
|
|
37
|
-
portKey: '
|
|
37
|
+
projectName: 'my-playwright-mcp-cont-headed',
|
|
38
|
+
containerName: 'my-playwright-mcp-cont-headed',
|
|
39
|
+
portKey: 'mcpContHeaded',
|
|
38
40
|
headless: false,
|
|
39
41
|
listenHost: '0.0.0.0'
|
|
40
42
|
},
|
|
41
|
-
'host-headless': {
|
|
43
|
+
'mcp-host-headless': {
|
|
42
44
|
type: 'host',
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
engine: 'mcp',
|
|
46
|
+
configFile: 'mcp-host-headless.json',
|
|
47
|
+
portKey: 'mcpHostHeadless',
|
|
45
48
|
headless: true,
|
|
46
49
|
listenHost: '127.0.0.1'
|
|
47
50
|
},
|
|
48
|
-
'host-headed': {
|
|
51
|
+
'mcp-host-headed': {
|
|
49
52
|
type: 'host',
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
engine: 'mcp',
|
|
54
|
+
configFile: 'mcp-host-headed.json',
|
|
55
|
+
portKey: 'mcpHostHeaded',
|
|
52
56
|
headless: false,
|
|
53
57
|
listenHost: '127.0.0.1'
|
|
58
|
+
},
|
|
59
|
+
'cli-host-headless': {
|
|
60
|
+
type: 'host',
|
|
61
|
+
engine: 'cli',
|
|
62
|
+
configFile: 'cli-host-headless.json',
|
|
63
|
+
portKey: 'cliHostHeadless',
|
|
64
|
+
headless: true,
|
|
65
|
+
listenHost: '0.0.0.0'
|
|
66
|
+
},
|
|
67
|
+
'cli-host-headed': {
|
|
68
|
+
type: 'host',
|
|
69
|
+
engine: 'cli',
|
|
70
|
+
configFile: 'cli-host-headed.json',
|
|
71
|
+
portKey: 'cliHostHeaded',
|
|
72
|
+
headless: false,
|
|
73
|
+
listenHost: '0.0.0.0'
|
|
54
74
|
}
|
|
55
75
|
};
|
|
56
76
|
|
|
@@ -67,6 +87,14 @@ const DEFAULT_FINGERPRINT_PROFILE = {
|
|
|
67
87
|
};
|
|
68
88
|
const DISABLE_WEBRTC_LAUNCH_ARGS = ['--disable-webrtc'];
|
|
69
89
|
|
|
90
|
+
function isMcpScene(sceneName) {
|
|
91
|
+
return Boolean(SCENE_DEFS[sceneName] && SCENE_DEFS[sceneName].engine === 'mcp');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isCliScene(sceneName) {
|
|
95
|
+
return Boolean(SCENE_DEFS[sceneName] && SCENE_DEFS[sceneName].engine === 'cli');
|
|
96
|
+
}
|
|
97
|
+
|
|
70
98
|
function platformFromUserAgent(userAgent) {
|
|
71
99
|
const ua = String(userAgent || '').toLowerCase();
|
|
72
100
|
if (ua.includes('macintosh') || ua.includes('mac os x')) {
|
|
@@ -264,7 +292,7 @@ class PlaywrightPlugin {
|
|
|
264
292
|
const defaultConfig = {
|
|
265
293
|
runtime: 'mixed',
|
|
266
294
|
enabledScenes: [...SCENE_ORDER],
|
|
267
|
-
|
|
295
|
+
cliSessionScene: 'cli-host-headless',
|
|
268
296
|
mcpDefaultHost: 'host.docker.internal',
|
|
269
297
|
dockerTag: process.env.PLAYWRIGHT_MCP_DOCKER_TAG || 'latest',
|
|
270
298
|
containerRuntime: '',
|
|
@@ -277,11 +305,13 @@ class PlaywrightPlugin {
|
|
|
277
305
|
disableWebRTC: false,
|
|
278
306
|
composeDir: path.join(__dirname, 'playwright-assets'),
|
|
279
307
|
ports: {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
308
|
+
mcpContHeadless: 8931,
|
|
309
|
+
mcpContHeaded: 8932,
|
|
310
|
+
mcpHostHeadless: 8933,
|
|
311
|
+
mcpHostHeaded: 8934,
|
|
312
|
+
cliHostHeadless: 8935,
|
|
313
|
+
cliHostHeaded: 8936,
|
|
314
|
+
mcpContHeadedNoVnc: 6080
|
|
285
315
|
}
|
|
286
316
|
};
|
|
287
317
|
|
|
@@ -306,6 +336,7 @@ class PlaywrightPlugin {
|
|
|
306
336
|
asStringArray(this.globalConfig.enabledScenes, [...defaultConfig.enabledScenes])
|
|
307
337
|
);
|
|
308
338
|
merged.containerRuntime = this.resolveContainerRuntime(merged.containerRuntime);
|
|
339
|
+
merged.cliSessionScene = String(merged.cliSessionScene || defaultConfig.cliSessionScene).trim();
|
|
309
340
|
merged.navigatorPlatform = String(merged.navigatorPlatform || defaultConfig.navigatorPlatform).trim() || defaultConfig.navigatorPlatform;
|
|
310
341
|
merged.disableWebRTC = asBoolean(merged.disableWebRTC, defaultConfig.disableWebRTC);
|
|
311
342
|
|
|
@@ -317,6 +348,9 @@ class PlaywrightPlugin {
|
|
|
317
348
|
if (invalidScene) {
|
|
318
349
|
throw new Error(`playwright.enabledScenes 包含未知场景: ${invalidScene}`);
|
|
319
350
|
}
|
|
351
|
+
if (merged.cliSessionScene && !isCliScene(merged.cliSessionScene)) {
|
|
352
|
+
throw new Error(`playwright.cliSessionScene 无效: ${merged.cliSessionScene}`);
|
|
353
|
+
}
|
|
320
354
|
|
|
321
355
|
return merged;
|
|
322
356
|
}
|
|
@@ -412,15 +446,49 @@ class PlaywrightPlugin {
|
|
|
412
446
|
return !fs.existsSync(this.sceneConfigPath(sceneName));
|
|
413
447
|
}
|
|
414
448
|
|
|
449
|
+
sceneEndpointPath(sceneName) {
|
|
450
|
+
return path.join(this.config.runDir, `${sceneName}.endpoint.json`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
readSceneEndpoint(sceneName) {
|
|
454
|
+
const filePath = this.sceneEndpointPath(sceneName);
|
|
455
|
+
if (!fs.existsSync(filePath)) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
460
|
+
} catch {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
writeSceneEndpoint(sceneName, payload) {
|
|
466
|
+
fs.mkdirSync(this.config.runDir, { recursive: true });
|
|
467
|
+
fs.writeFileSync(this.sceneEndpointPath(sceneName), `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
removeSceneEndpoint(sceneName) {
|
|
471
|
+
fs.rmSync(this.sceneEndpointPath(sceneName), { force: true });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
sceneCliAttachConfigPath(sceneName) {
|
|
475
|
+
return path.join(this.config.runDir, `${sceneName}.cli-attach.json`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
writeSceneCliAttachConfig(sceneName, payload) {
|
|
479
|
+
fs.mkdirSync(this.config.runDir, { recursive: true });
|
|
480
|
+
fs.writeFileSync(this.sceneCliAttachConfigPath(sceneName), `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
removeSceneCliAttachConfig(sceneName) {
|
|
484
|
+
fs.rmSync(this.sceneCliAttachConfigPath(sceneName), { force: true });
|
|
485
|
+
}
|
|
486
|
+
|
|
415
487
|
sceneInitScriptPath(sceneName) {
|
|
416
488
|
const configFile = path.basename(this.sceneConfigPath(sceneName), '.json');
|
|
417
489
|
return path.join(this.config.configDir, `${configFile}.init.js`);
|
|
418
490
|
}
|
|
419
491
|
|
|
420
|
-
legacySceneInitScriptPath(sceneName) {
|
|
421
|
-
return path.join(this.config.configDir, `${sceneName}.init.js`);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
492
|
buildInitScriptContent() {
|
|
425
493
|
const lines = [
|
|
426
494
|
"'use strict';",
|
|
@@ -471,14 +539,13 @@ class PlaywrightPlugin {
|
|
|
471
539
|
const filePath = this.sceneInitScriptPath(sceneName);
|
|
472
540
|
const content = this.buildInitScriptContent();
|
|
473
541
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
474
|
-
const legacyFilePath = this.legacySceneInitScriptPath(sceneName);
|
|
475
|
-
if (legacyFilePath !== filePath) {
|
|
476
|
-
fs.rmSync(legacyFilePath, { force: true });
|
|
477
|
-
}
|
|
478
542
|
return filePath;
|
|
479
543
|
}
|
|
480
544
|
|
|
481
545
|
defaultBrowserName(sceneName) {
|
|
546
|
+
if (isCliScene(sceneName)) {
|
|
547
|
+
return 'chromium';
|
|
548
|
+
}
|
|
482
549
|
const cfg = this.buildSceneConfig(sceneName);
|
|
483
550
|
const browserName = cfg && cfg.browser && cfg.browser.browserName;
|
|
484
551
|
return String(browserName || 'chromium');
|
|
@@ -494,10 +561,10 @@ class PlaywrightPlugin {
|
|
|
494
561
|
}
|
|
495
562
|
|
|
496
563
|
ensureHostScenePrerequisites(sceneName) {
|
|
497
|
-
if (!this.sceneConfigMissing(sceneName)) {
|
|
564
|
+
if (!isCliScene(sceneName) && !this.sceneConfigMissing(sceneName)) {
|
|
498
565
|
return;
|
|
499
566
|
}
|
|
500
|
-
this.runCmd([this.
|
|
567
|
+
this.runCmd([this.playwrightBinPath(sceneName), 'install', '--with-deps', this.defaultBrowserName(sceneName)], { check: true });
|
|
501
568
|
}
|
|
502
569
|
|
|
503
570
|
scenePidFile(sceneName) {
|
|
@@ -517,6 +584,26 @@ class PlaywrightPlugin {
|
|
|
517
584
|
return binPath;
|
|
518
585
|
}
|
|
519
586
|
|
|
587
|
+
playwrightBinPath(sceneName) {
|
|
588
|
+
if (!isCliScene(sceneName)) {
|
|
589
|
+
return this.localBinPath('playwright');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const filename = process.platform === 'win32' ? 'playwright.cmd' : 'playwright';
|
|
593
|
+
const candidates = [
|
|
594
|
+
path.join(this.projectRoot, 'node_modules', '@playwright', 'mcp', 'node_modules', '.bin', filename),
|
|
595
|
+
path.join(this.projectRoot, 'node_modules', '.bin', filename)
|
|
596
|
+
];
|
|
597
|
+
|
|
598
|
+
for (const candidate of candidates) {
|
|
599
|
+
if (fs.existsSync(candidate)) {
|
|
600
|
+
return candidate;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
throw new Error(`local binary not found for ${sceneName}. Run npm install first.`);
|
|
605
|
+
}
|
|
606
|
+
|
|
520
607
|
extensionDirPath() {
|
|
521
608
|
return path.join(os.homedir(), '.manyoyo', 'plugin', 'playwright', 'extensions');
|
|
522
609
|
}
|
|
@@ -650,22 +737,25 @@ class PlaywrightPlugin {
|
|
|
650
737
|
return { containerPaths, volumeMounts };
|
|
651
738
|
}
|
|
652
739
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const port = this.scenePort(sceneName);
|
|
656
|
-
const extensionPaths = asStringArray(options.extensionPaths, []);
|
|
657
|
-
const initScript = asStringArray(options.initScript, []);
|
|
658
|
-
const baseLaunchArgs = [
|
|
740
|
+
baseLaunchArgs() {
|
|
741
|
+
return [
|
|
659
742
|
`--user-agent=${DEFAULT_FINGERPRINT_PROFILE.userAgent}`,
|
|
660
743
|
`--lang=${DEFAULT_FINGERPRINT_PROFILE.locale}`,
|
|
661
744
|
`--window-size=${DEFAULT_FINGERPRINT_PROFILE.width},${DEFAULT_FINGERPRINT_PROFILE.height}`,
|
|
662
745
|
'--disable-blink-features=AutomationControlled',
|
|
663
746
|
'--force-webrtc-ip-handling-policy=disable_non_proxied_udp'
|
|
664
747
|
];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
buildMcpSceneConfig(sceneName, options = {}) {
|
|
751
|
+
const def = SCENE_DEFS[sceneName];
|
|
752
|
+
const port = this.scenePort(sceneName);
|
|
753
|
+
const extensionPaths = asStringArray(options.extensionPaths, []);
|
|
754
|
+
const initScript = asStringArray(options.initScript, []);
|
|
665
755
|
const launchOptions = {
|
|
666
756
|
channel: 'chromium',
|
|
667
757
|
headless: def.headless,
|
|
668
|
-
args: [...baseLaunchArgs]
|
|
758
|
+
args: [...this.baseLaunchArgs()]
|
|
669
759
|
};
|
|
670
760
|
|
|
671
761
|
if (extensionPaths.length > 0) {
|
|
@@ -676,7 +766,7 @@ class PlaywrightPlugin {
|
|
|
676
766
|
}
|
|
677
767
|
|
|
678
768
|
return {
|
|
679
|
-
outputDir: '/tmp
|
|
769
|
+
outputDir: '/tmp/.playwright-mcp',
|
|
680
770
|
server: {
|
|
681
771
|
host: def.listenHost,
|
|
682
772
|
port,
|
|
@@ -712,8 +802,44 @@ class PlaywrightPlugin {
|
|
|
712
802
|
};
|
|
713
803
|
}
|
|
714
804
|
|
|
805
|
+
buildCliSceneConfig(sceneName, options = {}) {
|
|
806
|
+
const def = SCENE_DEFS[sceneName];
|
|
807
|
+
const extensionPaths = asStringArray(options.extensionPaths, []);
|
|
808
|
+
const payload = {
|
|
809
|
+
host: def.listenHost,
|
|
810
|
+
port: this.scenePort(sceneName),
|
|
811
|
+
wsPath: `/${sceneName}-${crypto.randomBytes(8).toString('hex')}`,
|
|
812
|
+
headless: def.headless,
|
|
813
|
+
channel: 'chromium',
|
|
814
|
+
chromiumSandbox: true,
|
|
815
|
+
args: [...this.baseLaunchArgs()]
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
if (extensionPaths.length > 0) {
|
|
819
|
+
payload.args.push(...this.buildExtensionLaunchArgs(extensionPaths));
|
|
820
|
+
}
|
|
821
|
+
if (this.config.disableWebRTC) {
|
|
822
|
+
payload.args.push(...DISABLE_WEBRTC_LAUNCH_ARGS);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return payload;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
buildSceneConfig(sceneName, options = {}) {
|
|
829
|
+
if (isCliScene(sceneName)) {
|
|
830
|
+
return this.buildCliSceneConfig(sceneName, options);
|
|
831
|
+
}
|
|
832
|
+
return this.buildMcpSceneConfig(sceneName, options);
|
|
833
|
+
}
|
|
834
|
+
|
|
715
835
|
ensureSceneConfig(sceneName, options = {}) {
|
|
716
836
|
fs.mkdirSync(this.config.configDir, { recursive: true });
|
|
837
|
+
if (isCliScene(sceneName)) {
|
|
838
|
+
const payload = this.buildCliSceneConfig(sceneName, options);
|
|
839
|
+
const filePath = this.sceneConfigPath(sceneName);
|
|
840
|
+
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
|
|
841
|
+
return filePath;
|
|
842
|
+
}
|
|
717
843
|
const initScriptPath = this.ensureSceneInitScript(sceneName);
|
|
718
844
|
const configuredInitScript = asStringArray(options.initScript, []);
|
|
719
845
|
const initScript = configuredInitScript.length > 0 ? configuredInitScript : [initScriptPath];
|
|
@@ -769,16 +895,16 @@ class PlaywrightPlugin {
|
|
|
769
895
|
PLAYWRIGHT_MCP_CONFIG_PATH: cfgPath,
|
|
770
896
|
PLAYWRIGHT_MCP_CONTAINER_NAME: def.containerName,
|
|
771
897
|
PLAYWRIGHT_MCP_IMAGE: this.config.headedImage,
|
|
772
|
-
PLAYWRIGHT_MCP_NOVNC_PORT: String(this.config.ports.
|
|
898
|
+
PLAYWRIGHT_MCP_NOVNC_PORT: String(this.config.ports.mcpContHeadedNoVnc)
|
|
773
899
|
};
|
|
774
900
|
|
|
775
|
-
if (sceneName === 'cont-headed') {
|
|
901
|
+
if (sceneName === 'mcp-cont-headed') {
|
|
776
902
|
const envKey = this.config.vncPasswordEnvKey;
|
|
777
903
|
let password = process.env[envKey];
|
|
778
904
|
if (!password) {
|
|
779
905
|
password = this.randomAlnum(16);
|
|
780
906
|
if (requireVncPassword) {
|
|
781
|
-
this.writeStdout(`[up] cont-headed ${envKey} not set; generated random 16-char password: ${password}`);
|
|
907
|
+
this.writeStdout(`[up] mcp-cont-headed ${envKey} not set; generated random 16-char password: ${password}`);
|
|
782
908
|
}
|
|
783
909
|
}
|
|
784
910
|
env.VNC_PASSWORD = password;
|
|
@@ -816,6 +942,37 @@ class PlaywrightPlugin {
|
|
|
816
942
|
return overridePath;
|
|
817
943
|
}
|
|
818
944
|
|
|
945
|
+
buildCliSessionIntegration(dockerCmd) {
|
|
946
|
+
const sceneName = this.config.cliSessionScene;
|
|
947
|
+
if (!sceneName) {
|
|
948
|
+
return { envEntries: [], extraArgs: [], volumeEntries: [] };
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const endpoint = this.readSceneEndpoint(sceneName);
|
|
952
|
+
if (!endpoint || !Number.isInteger(endpoint.port) || endpoint.port <= 0 || typeof endpoint.wsPath !== 'string' || !endpoint.wsPath) {
|
|
953
|
+
return { envEntries: [], extraArgs: [], volumeEntries: [] };
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const normalizedDockerCmd = String(dockerCmd || '').trim().toLowerCase();
|
|
957
|
+
const connectHost = normalizedDockerCmd === 'podman' ? 'host.containers.internal' : 'host.docker.internal';
|
|
958
|
+
const remoteEndpoint = `ws://${connectHost}:${endpoint.port}${endpoint.wsPath}`;
|
|
959
|
+
const hostConfigPath = this.sceneCliAttachConfigPath(sceneName);
|
|
960
|
+
const containerConfigPath = `/tmp/manyoyo-playwright/${sceneName}.cli-attach.json`;
|
|
961
|
+
this.writeSceneCliAttachConfig(sceneName, {
|
|
962
|
+
browser: {
|
|
963
|
+
remoteEndpoint
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
const envEntries = [
|
|
967
|
+
`PLAYWRIGHT_MCP_CONFIG=${containerConfigPath}`
|
|
968
|
+
];
|
|
969
|
+
const extraArgs = normalizedDockerCmd === 'docker'
|
|
970
|
+
? ['--add-host', 'host.docker.internal:host-gateway']
|
|
971
|
+
: [];
|
|
972
|
+
const volumeEntries = ['--volume', `${hostConfigPath}:${containerConfigPath}:ro`];
|
|
973
|
+
return { envEntries, extraArgs, volumeEntries };
|
|
974
|
+
}
|
|
975
|
+
|
|
819
976
|
async startContainer(sceneName, options = {}) {
|
|
820
977
|
const runtime = this.config.containerRuntime;
|
|
821
978
|
if (!this.ensureCommandAvailable(runtime)) {
|
|
@@ -972,8 +1129,21 @@ class PlaywrightPlugin {
|
|
|
972
1129
|
return cp.returncode === 0 ? 0 : 1;
|
|
973
1130
|
}
|
|
974
1131
|
|
|
975
|
-
|
|
976
|
-
|
|
1132
|
+
hostLaunchCommand(sceneName, cfgPath) {
|
|
1133
|
+
if (isCliScene(sceneName)) {
|
|
1134
|
+
return {
|
|
1135
|
+
command: this.playwrightBinPath(sceneName),
|
|
1136
|
+
args: ['launch-server', '--browser', this.defaultBrowserName(sceneName), '--config', String(cfgPath)]
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
return {
|
|
1140
|
+
command: this.localBinPath('playwright-mcp'),
|
|
1141
|
+
args: ['--config', String(cfgPath)]
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
spawnHostProcess(command, args, logFd) {
|
|
1146
|
+
return spawn(command, args, {
|
|
977
1147
|
detached: true,
|
|
978
1148
|
stdio: ['ignore', logFd, logFd]
|
|
979
1149
|
});
|
|
@@ -998,7 +1168,9 @@ class PlaywrightPlugin {
|
|
|
998
1168
|
|
|
999
1169
|
hostScenePids(sceneName) {
|
|
1000
1170
|
const cfgPath = this.sceneConfigPath(sceneName);
|
|
1001
|
-
const pattern =
|
|
1171
|
+
const pattern = isCliScene(sceneName)
|
|
1172
|
+
? `playwright.*launch-server.*--config ${cfgPath}`
|
|
1173
|
+
: `playwright-mcp.*--config ${cfgPath}`;
|
|
1002
1174
|
const cp = this.runCmd(['pgrep', '-f', pattern], { captureOutput: true, check: false });
|
|
1003
1175
|
|
|
1004
1176
|
if (cp.returncode !== 0 || !cp.stdout.trim()) {
|
|
@@ -1043,6 +1215,8 @@ class PlaywrightPlugin {
|
|
|
1043
1215
|
const pidFile = this.scenePidFile(sceneName);
|
|
1044
1216
|
const logFile = this.sceneLogFile(sceneName);
|
|
1045
1217
|
const port = this.scenePort(sceneName);
|
|
1218
|
+
this.removeSceneEndpoint(sceneName);
|
|
1219
|
+
this.removeSceneCliAttachConfig(sceneName);
|
|
1046
1220
|
|
|
1047
1221
|
let managedPids = this.hostScenePids(sceneName);
|
|
1048
1222
|
if (managedPids.length > 0 && (await this.portReady(port))) {
|
|
@@ -1058,22 +1232,29 @@ class PlaywrightPlugin {
|
|
|
1058
1232
|
|
|
1059
1233
|
fs.rmSync(pidFile, { force: true });
|
|
1060
1234
|
const logFd = fs.openSync(logFile, 'a');
|
|
1061
|
-
let
|
|
1235
|
+
let launchCommand = null;
|
|
1062
1236
|
try {
|
|
1063
|
-
|
|
1237
|
+
launchCommand = this.hostLaunchCommand(sceneName, cfgPath);
|
|
1064
1238
|
} catch (error) {
|
|
1065
1239
|
fs.closeSync(logFd);
|
|
1066
1240
|
this.writeStderr(`[up] ${sceneName} failed: ${error.message || String(error)}`);
|
|
1067
1241
|
return 1;
|
|
1068
1242
|
}
|
|
1069
1243
|
|
|
1070
|
-
const starter = this.spawnHostProcess(
|
|
1244
|
+
const starter = this.spawnHostProcess(launchCommand.command, launchCommand.args, logFd);
|
|
1071
1245
|
fs.closeSync(logFd);
|
|
1072
1246
|
if (typeof starter.unref === 'function') {
|
|
1073
1247
|
starter.unref();
|
|
1074
1248
|
}
|
|
1075
1249
|
|
|
1076
1250
|
if (await this.waitForPort(port)) {
|
|
1251
|
+
if (isCliScene(sceneName)) {
|
|
1252
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
1253
|
+
this.writeSceneEndpoint(sceneName, {
|
|
1254
|
+
port,
|
|
1255
|
+
wsPath: String(cfg.wsPath || '')
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1077
1258
|
managedPids = await this.waitForHostPids(sceneName, starter.pid);
|
|
1078
1259
|
if (managedPids.length > 0) {
|
|
1079
1260
|
fs.writeFileSync(pidFile, `${managedPids[0]}`, 'utf8');
|
|
@@ -1099,6 +1280,8 @@ class PlaywrightPlugin {
|
|
|
1099
1280
|
const pidFile = this.scenePidFile(sceneName);
|
|
1100
1281
|
const port = this.scenePort(sceneName);
|
|
1101
1282
|
const managedPids = this.hostScenePids(sceneName);
|
|
1283
|
+
this.removeSceneEndpoint(sceneName);
|
|
1284
|
+
this.removeSceneCliAttachConfig(sceneName);
|
|
1102
1285
|
|
|
1103
1286
|
for (const pid of managedPids) {
|
|
1104
1287
|
try {
|
|
@@ -1337,7 +1520,7 @@ class PlaywrightPlugin {
|
|
|
1337
1520
|
return 1;
|
|
1338
1521
|
}
|
|
1339
1522
|
|
|
1340
|
-
const scenes = this.resolveTargets('all');
|
|
1523
|
+
const scenes = this.resolveTargets('all').filter(sceneName => isMcpScene(sceneName));
|
|
1341
1524
|
for (const sceneName of scenes) {
|
|
1342
1525
|
const url = `http://${host}:${this.scenePort(sceneName)}/mcp`;
|
|
1343
1526
|
this.writeStdout(`claude mcp add -t http -s user playwright-${sceneName} ${url}`);
|
|
@@ -1391,7 +1574,7 @@ class PlaywrightPlugin {
|
|
|
1391
1574
|
return 1;
|
|
1392
1575
|
}
|
|
1393
1576
|
|
|
1394
|
-
async run({ action, scene = 'host-headless', host = '', extensionPaths = [], extensionNames = [], prodversion = '' }) {
|
|
1577
|
+
async run({ action, scene = 'mcp-host-headless', host = '', extensionPaths = [], extensionNames = [], prodversion = '' }) {
|
|
1395
1578
|
if (action === 'ls') {
|
|
1396
1579
|
return this.printSummary();
|
|
1397
1580
|
}
|
package/lib/web/server.js
CHANGED
|
@@ -778,6 +778,7 @@ function buildStaticContainerRuntime(ctx, containerName) {
|
|
|
778
778
|
imageName: ctx.imageName,
|
|
779
779
|
imageVersion: ctx.imageVersion,
|
|
780
780
|
contModeArgs: Array.isArray(ctx.contModeArgs) ? ctx.contModeArgs.slice() : [],
|
|
781
|
+
containerExtraArgs: Array.isArray(ctx.containerExtraArgs) ? ctx.containerExtraArgs.slice() : [],
|
|
781
782
|
containerEnvs: Array.isArray(ctx.containerEnvs) ? ctx.containerEnvs.slice() : [],
|
|
782
783
|
containerVolumes: Array.isArray(ctx.containerVolumes) ? ctx.containerVolumes.slice() : [],
|
|
783
784
|
containerPorts: Array.isArray(ctx.containerPorts) ? ctx.containerPorts.slice() : [],
|
|
@@ -918,6 +919,7 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
918
919
|
imageName,
|
|
919
920
|
imageVersion,
|
|
920
921
|
contModeArgs,
|
|
922
|
+
containerExtraArgs: Array.isArray(ctx.containerExtraArgs) ? ctx.containerExtraArgs.slice() : [],
|
|
921
923
|
containerEnvs,
|
|
922
924
|
containerVolumes,
|
|
923
925
|
containerPorts,
|
|
@@ -1019,6 +1021,7 @@ async function ensureWebContainer(ctx, state, containerInput) {
|
|
|
1019
1021
|
imageName: runtime.imageName,
|
|
1020
1022
|
imageVersion: runtime.imageVersion,
|
|
1021
1023
|
contModeArgs: runtime.contModeArgs,
|
|
1024
|
+
containerExtraArgs: runtime.containerExtraArgs,
|
|
1022
1025
|
containerEnvs: runtime.containerEnvs,
|
|
1023
1026
|
containerVolumes: runtime.containerVolumes,
|
|
1024
1027
|
containerPorts: runtime.containerPorts,
|
|
@@ -1770,6 +1773,7 @@ async function startWebServer(options) {
|
|
|
1770
1773
|
execCommand: options.execCommand,
|
|
1771
1774
|
execCommandSuffix: options.execCommandSuffix,
|
|
1772
1775
|
contModeArgs: options.contModeArgs,
|
|
1776
|
+
containerExtraArgs: options.containerExtraArgs,
|
|
1773
1777
|
containerEnvs: options.containerEnvs,
|
|
1774
1778
|
containerVolumes: options.containerVolumes,
|
|
1775
1779
|
containerPorts: options.containerPorts,
|
package/manyoyo.example.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hostPath": "/path/to/your/project",
|
|
7
7
|
"containerPath": "/path/to/your/project",
|
|
8
8
|
"imageName": "localhost/xcanwin/manyoyo",
|
|
9
|
-
"imageVersion": "1.8.
|
|
9
|
+
"imageVersion": "1.8.8-common",
|
|
10
10
|
"containerMode": "common",
|
|
11
11
|
|
|
12
12
|
// 容器启动环境(env 按 key 合并覆盖;其余数组参数会累加)
|
|
@@ -50,10 +50,12 @@
|
|
|
50
50
|
// mixed: 支持容器+宿主机;container: 仅容器;host: 仅宿主机
|
|
51
51
|
"runtime": "mixed",
|
|
52
52
|
// 启用场景(可按需裁剪)
|
|
53
|
-
"enabledScenes": ["cont-headless", "cont-headed", "host-headless", "host-headed"],
|
|
53
|
+
"enabledScenes": ["mcp-cont-headless", "mcp-cont-headed", "mcp-host-headless", "mcp-host-headed", "cli-host-headless", "cli-host-headed"],
|
|
54
|
+
// my run 默认注入的 playwright-cli 宿主场景
|
|
55
|
+
"cliSessionScene": "cli-host-headless",
|
|
54
56
|
// mcp-add 默认 host(可改为 localhost / 127.0.0.1)
|
|
55
57
|
"mcpDefaultHost": "host.docker.internal",
|
|
56
|
-
// cont-headed 场景读取的密码环境变量名(默认 VNC_PASSWORD)
|
|
58
|
+
// mcp-cont-headed 场景读取的密码环境变量名(默认 VNC_PASSWORD)
|
|
57
59
|
"vncPasswordEnvKey": "VNC_PASSWORD",
|
|
58
60
|
// playwright ext-download 的 CRX prodversion 参数
|
|
59
61
|
"extensionProdversion": "132.0.0.0",
|
|
@@ -62,11 +64,13 @@
|
|
|
62
64
|
// 是否禁用 WebRTC(默认 false)
|
|
63
65
|
"disableWebRTC": false,
|
|
64
66
|
"ports": {
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
67
|
+
"mcpContHeadless": 8931,
|
|
68
|
+
"mcpContHeaded": 8932,
|
|
69
|
+
"mcpHostHeadless": 8933,
|
|
70
|
+
"mcpHostHeaded": 8934,
|
|
71
|
+
"cliHostHeadless": 8935,
|
|
72
|
+
"cliHostHeaded": 8936,
|
|
73
|
+
"mcpContHeadedNoVnc": 6080
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
},
|
package/package.json
CHANGED