@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 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.4-common
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.4-common
140
+ manyoyo build --iv 1.8.8-common
141
141
 
142
142
  # full 版本
143
- manyoyo build --iv 1.8.4-full
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 configPath = path.join(os.homedir(), '.manyoyo', 'manyoyo.json');
312
- if (fs.existsSync(configPath)) {
313
- try {
314
- const config = JSON5.parse(fs.readFileSync(configPath, 'utf-8'));
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="full"
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 man-db \
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/* ~/.cache ~/.npm ~/go/pkg/mod/cache
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/* ~/.cache ~/.npm ~/go/pkg/mod/cache
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 golang-src gcc
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/* ~/.cache ~/.npm ~/go/pkg/mod/cache
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/* ~/.cache ~/.npm ~/go/pkg/mod/cache
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
+ }
@@ -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
+ };
@@ -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
- const providers = configToml.model_providers;
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:
@@ -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
- configFile: 'container-headless.json',
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: 'contHeadless',
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
- configFile: 'container-headed.json',
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: 'contHeaded',
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
- configFile: 'host-headless.json',
44
- portKey: 'hostHeadless',
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
- configFile: 'host-headed.json',
51
- portKey: 'hostHeaded',
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
- hostListen: '127.0.0.1',
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
- contHeadless: 8931,
281
- contHeaded: 8932,
282
- hostHeadless: 8933,
283
- hostHeaded: 8934,
284
- contHeadedNoVnc: 6080
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.localBinPath('playwright'), 'install', '--with-deps', this.defaultBrowserName(sceneName)], { check: true });
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
- buildSceneConfig(sceneName, options = {}) {
654
- const def = SCENE_DEFS[sceneName];
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/playwright-mcp',
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.contHeadedNoVnc)
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
- spawnHostProcess(mcpBinPath, cfgPath, logFd) {
976
- return spawn(mcpBinPath, ['--config', String(cfgPath)], {
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 = `playwright-mcp.*--config ${cfgPath}`;
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 mcpBinPath = '';
1235
+ let launchCommand = null;
1062
1236
  try {
1063
- mcpBinPath = this.localBinPath('playwright-mcp');
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(mcpBinPath, cfgPath, logFd);
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,
@@ -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.4-common",
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
- "contHeadless": 8931,
66
- "contHeaded": 8932,
67
- "hostHeadless": 8933,
68
- "hostHeaded": 8934,
69
- "contHeadedNoVnc": 6080
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.3.10",
4
- "imageVersion": "1.8.4-common",
3
+ "version": "5.4.6",
4
+ "imageVersion": "1.8.8-common",
5
5
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",
6
6
  "keywords": [
7
7
  "manyoyo",