@xcanwin/manyoyo 8.3.0 → 8.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -53,7 +53,7 @@ manyoyo init all # import the agent configs already on this machine
53
53
  manyoyo run -y c # start Claude Code in YOLO mode inside the sandbox
54
54
  ```
55
55
 
56
- The default image `ghcr.io/xcanwin/manyoyo` is pulled automatically on first use; run `manyoyo build --iv 2.1.0-common` only if you need a custom image. More commands: [CLI reference](https://xcanwin.github.io/manyoyo/en/reference/cli-options).
56
+ The default image `ghcr.io/xcanwin/manyoyo` is pulled automatically on first use; run `manyoyo build --iv 2.1.1-common` only if you need a custom image. More commands: [CLI reference](https://xcanwin.github.io/manyoyo/en/reference/cli-options).
57
57
 
58
58
  ## What it does
59
59
 
package/README.md CHANGED
@@ -53,7 +53,7 @@ manyoyo init all # 把本机已有的 Agent 配置搬进来
53
53
  manyoyo run -y c # 在沙箱里以 YOLO 模式启动 Claude Code
54
54
  ```
55
55
 
56
- 默认镜像 `ghcr.io/xcanwin/manyoyo` 首次使用时自动拉取;需要自定义镜像时再执行 `manyoyo build --iv 2.1.0-common`。更多命令见[命令速查](https://xcanwin.github.io/manyoyo/reference/cli-options)。
56
+ 默认镜像 `ghcr.io/xcanwin/manyoyo` 首次使用时自动拉取;需要自定义镜像时再执行 `manyoyo build --iv 2.1.1-common`。更多命令见[命令速查](https://xcanwin.github.io/manyoyo/reference/cli-options)。
57
57
 
58
58
  ## 它能做什么
59
59
 
package/bin/manyoyo.js CHANGED
@@ -34,7 +34,7 @@ const { resolveAgentResumeArg, buildAgentResumeCommand } = require('../lib/agent
34
34
  const { resolveYoloCommand } = require('../lib/agent-adapters');
35
35
  const { runDoctorChecks, applyDoctorFixes } = require('../lib/doctor');
36
36
  const { resolveContainerMode } = require('../lib/container-modes');
37
- const { runPluginCommand, createPlugin } = require('../lib/plugin');
37
+ const { runPlaywrightCommand, buildContainerIntegration, mergeIntegration } = require('../lib/plugin');
38
38
  const { buildManyoyoLogPath } = require('../lib/log-path');
39
39
  const { resolveRuntimeConfig } = require('../lib/runtime-resolver');
40
40
  const { resolveWorktreeSupport } = require('../lib/worktrees');
@@ -123,6 +123,7 @@ const IMAGE_VERSION_TAG_PATTERN = /^(\d+\.\d+\.\d+)-([A-Za-z0-9][A-Za-z0-9_.-]*)
123
123
 
124
124
  // Docker command (will be set by ensure_docker)
125
125
  let DOCKER_CMD = 'docker';
126
+ let PLAYWRIGHT_PLUGIN_CONFIG = { globalConfig: {}, runConfig: {} };
126
127
  // 仅运行时子进程使用的完整 env(私有 Podman 才有值),不要写回 process.env
127
128
  let DOCKER_ENV;
128
129
  let CONTAINER_RUNTIME = null;
@@ -603,61 +604,21 @@ function addEnvFile(envFile) {
603
604
  return addEnvFileTo(CONTAINER_ENVS, envFile);
604
605
  }
605
606
 
606
- function hasEnvKey(targetEnvs, key) {
607
- for (let i = 0; i < targetEnvs.length; i += 2) {
608
- if (targetEnvs[i] !== '--env') {
609
- continue;
610
- }
611
- const text = String(targetEnvs[i + 1] || '');
612
- const idx = text.indexOf('=');
613
- if (idx > 0 && text.slice(0, idx) === key) {
614
- return true;
615
- }
616
- }
617
- return false;
618
- }
619
-
620
- function appendUniqueArgs(targetArgs, extraArgs) {
621
- const joinedExisting = new Set();
622
- for (let i = 0; i < targetArgs.length; i += 2) {
623
- const head = String(targetArgs[i] || '');
624
- const value = String(targetArgs[i + 1] || '');
625
- if (head.startsWith('--')) {
626
- joinedExisting.add(`${head}\u0000${value}`);
627
- }
628
- }
629
-
630
- for (let i = 0; i < extraArgs.length; i += 2) {
631
- const head = String(extraArgs[i] || '');
632
- const value = String(extraArgs[i + 1] || '');
633
- const signature = `${head}\u0000${value}`;
634
- if (!joinedExisting.has(signature)) {
635
- joinedExisting.add(signature);
636
- targetArgs.push(head, value);
637
- }
638
- }
639
- }
640
-
641
- function applyPlaywrightCliSessionIntegration(config, runConfig) {
642
- try {
643
- const plugin = createPlugin('playwright', {
644
- globalConfig: config,
645
- runConfig,
646
- projectRoot: path.join(__dirname, '..')
647
- });
648
- const integration = plugin.buildCliSessionIntegration(DOCKER_CMD);
649
- for (const entry of integration.envEntries) {
650
- const parsed = parseEnvEntry(entry);
651
- if (!hasEnvKey(CONTAINER_ENVS, parsed.key)) {
652
- addEnv(`${parsed.key}=${parsed.value}`);
653
- }
654
- }
655
- appendUniqueArgs(CONTAINER_EXTRA_ARGS, integration.extraArgs);
656
- appendUniqueArgs(CONTAINER_VOLUMES, integration.volumeEntries || []);
657
- } catch (error) {
658
- console.error(`${RED}⚠️ 错误: Playwright CLI 会话注入失败: ${error.message || String(error)}${NC}`);
659
- process.exit(1);
607
+ // Playwright 浏览器模式的容器参数;在容器创建时(运行时已确定)才计算,失败只警告,不影响 run
608
+ async function applyPlaywrightIntegration(runtime) {
609
+ const integration = await buildContainerIntegration({
610
+ ...PLAYWRIGHT_PLUGIN_CONFIG,
611
+ dryRun: runtime.showCommand,
612
+ runtimeCommand: DOCKER_CMD,
613
+ envEntries: runtime.containerEnvs.filter((_, i) => i % 2 === 1)
614
+ });
615
+ if (integration.warning) {
616
+ console.warn(`${YELLOW}⚠️ ${integration.warning}${NC}`);
660
617
  }
618
+ const merged = mergeIntegration(runtime, integration);
619
+ runtime.containerEnvs = merged.containerEnvs;
620
+ runtime.containerVolumes = merged.containerVolumes;
621
+ runtime.containerExtraArgs = merged.containerExtraArgs;
661
622
  }
662
623
 
663
624
  function addVolume(volume) {
@@ -1201,79 +1162,6 @@ async function setupCommander() {
1201
1162
  selectedAction = action;
1202
1163
  selectedOptions = options;
1203
1164
  };
1204
- const selectPluginAction = (params = {}, options = {}) => {
1205
- selectAction('plugin', {
1206
- ...options,
1207
- pluginAction: params.action || 'ls',
1208
- pluginName: params.pluginName || 'playwright',
1209
- pluginScene: params.scene || 'mcp-host-headless',
1210
- pluginHost: params.host || '',
1211
- pluginExtensionPaths: Array.isArray(params.extensionPaths) ? params.extensionPaths : [],
1212
- pluginExtensionNames: Array.isArray(params.extensionNames) ? params.extensionNames : [],
1213
- pluginProdversion: params.prodversion || ''
1214
- });
1215
- };
1216
-
1217
- const registerPlaywrightAliasCommands = (command) => {
1218
- command.command('ls')
1219
- .description('列出 playwright 启用场景')
1220
- .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1221
- .action(options => selectPluginAction({
1222
- action: 'ls',
1223
- pluginName: 'playwright',
1224
- scene: 'all'
1225
- }, options));
1226
-
1227
- const actions = ['up', 'down', 'status', 'health', 'logs'];
1228
- actions.forEach(action => {
1229
- const sceneCommand = command.command(`${action} [scene]`)
1230
- .description(`执行 playwright ${action} 场景(scene 默认 mcp-host-headless)`)
1231
- .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)');
1232
-
1233
- if (action === 'up') {
1234
- appendArrayOption(sceneCommand, '--ext-path <path>', '追加浏览器扩展目录(可多次传入;目录需包含 manifest.json)');
1235
- appendArrayOption(sceneCommand, '--ext-name <name>', '追加 ~/.manyoyo/plugin/playwright/extensions/ 下的扩展目录名(可多次传入)');
1236
- }
1237
-
1238
- sceneCommand.action((scene, options) => selectPluginAction({
1239
- action,
1240
- pluginName: 'playwright',
1241
- scene: scene || 'mcp-host-headless',
1242
- extensionPaths: action === 'up' ? (options.extPath || []) : [],
1243
- extensionNames: action === 'up' ? (options.extName || []) : []
1244
- }, options));
1245
- });
1246
-
1247
- command.command('mcp-add')
1248
- .description('输出 playwright 的 MCP 接入命令')
1249
- .option('--host <host>', 'MCP URL 使用的主机名或IP (默认 host.docker.internal)')
1250
- .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1251
- .action(options => selectPluginAction({
1252
- action: 'mcp-add',
1253
- pluginName: 'playwright',
1254
- scene: 'all',
1255
- host: options.host || ''
1256
- }, options));
1257
-
1258
- command.command('cli-add')
1259
- .description('输出 playwright-cli skill 安装命令')
1260
- .action(() => selectPluginAction({
1261
- action: 'cli-add',
1262
- pluginName: 'playwright',
1263
- scene: 'all'
1264
- }));
1265
-
1266
- command.command('ext-download')
1267
- .description('下载并解压 Playwright 扩展到 ~/.manyoyo/plugin/playwright/extensions/')
1268
- .option('--prodversion <ver>', 'CRX 下载使用的 Chrome 版本号 (默认 132.0.0.0)')
1269
- .action(options => selectPluginAction({
1270
- action: 'ext-download',
1271
- pluginName: 'playwright',
1272
- scene: 'all',
1273
- prodversion: options.prodversion || ''
1274
- }, options));
1275
- };
1276
-
1277
1165
  program
1278
1166
  .name(MANYOYO_NAME)
1279
1167
  .optionsGroup('选项:')
@@ -1454,25 +1342,45 @@ https://github.com/xcanwin/manyoyo
1454
1342
  });
1455
1343
  });
1456
1344
 
1457
- const playwrightCommand = program.command('playwright').helpGroup('网页服务与插件:').description('管理 Playwright 插件服务');
1345
+ const selectPlaywright = (action, params = {}, options = {}) => {
1346
+ selectAction('playwright', { ...options, playwrightRequest: { action, ...params } });
1347
+ };
1348
+ const playwrightCommand = program.command('playwright').helpGroup('网页服务与插件:')
1349
+ .description('管理容器内浏览器的模式(默认无需任何命令)')
1350
+ .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1351
+ .action(options => selectPlaywright('overview', {}, options));
1458
1352
  playwrightCommand.addHelpText('after', `
1459
- 示例:
1460
- ${MANYOYO_NAME} playwright up mcp-host-headless 启动 MCP 宿主场景(默认/推荐)
1461
- ${MANYOYO_NAME} playwright up cli-host-headless 启动 CLI 宿主场景(供容器内 playwright-cli 附着)
1353
+ 模式(同一时间只有一个):
1354
+ (默认) 容器内虚拟屏里的有头浏览器,什么都不用做
1355
+ headed 宿主机上有窗口的浏览器:${MANYOYO_NAME} playwright up headed
1356
+ chrome 你正在用的 Chrome(沿用登录状态,有风险):${MANYOYO_NAME} playwright up chrome
1357
+ vnc 独立容器里的浏览器,用 noVNC 网页观看:${MANYOYO_NAME} playwright up vnc
1462
1358
  `);
1463
- registerPlaywrightAliasCommands(playwrightCommand);
1464
-
1465
- const pluginCommand = program.command('plugin', { hidden: true }).description('管理 manyoyo 插件');
1466
- pluginCommand.command('ls')
1467
- .description('列出可用插件与启用场景')
1359
+ playwrightCommand.command('up [mode]')
1360
+ .description('切换到 headed / chrome / vnc 模式(不带参数列出全部模式)')
1468
1361
  .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1469
- .action(options => selectPluginAction({
1470
- action: 'ls',
1471
- pluginName: 'playwright',
1472
- scene: 'all'
1362
+ .option('--ext-path <path>', '追加浏览器扩展目录(可多次传入;目录需包含 manifest.json;仅 headed、vnc)', (value, previous = []) => [...previous, value])
1363
+ .option('--ext-name <name>', '追加 ~/.manyoyo/plugin/playwright/extensions/ 下的扩展目录名(可多次传入;仅 headed、vnc)', (value, previous = []) => [...previous, value])
1364
+ .action((mode, options) => selectPlaywright('up', {
1365
+ mode: mode || '',
1366
+ extensionPaths: options.extPath || [],
1367
+ extensionNames: options.extName || []
1473
1368
  }, options));
1474
- const pluginPlaywrightCommand = pluginCommand.command('playwright').description('管理 playwright 插件服务');
1475
- registerPlaywrightAliasCommands(pluginPlaywrightCommand);
1369
+ [
1370
+ ['down', '停止当前模式,回到默认模式'],
1371
+ ['status', '查看当前模式并真实探测浏览器是否可用(不可用时退出码非 0)'],
1372
+ ['logs', '查看当前模式的浏览器服务日志'],
1373
+ ['mcp-add', '输出在容器内注册 Playwright MCP(stdio,随当前模式自动切换)的命令']
1374
+ ].forEach(([action, description]) => {
1375
+ playwrightCommand.command(action)
1376
+ .description(description)
1377
+ .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1378
+ .action(options => selectPlaywright(action, {}, options));
1379
+ });
1380
+ playwrightCommand.command('ext-download')
1381
+ .description('下载并解压 Playwright 扩展到 ~/.manyoyo/plugin/playwright/extensions/')
1382
+ .option('--prodversion <ver>', 'CRX 下载使用的 Chrome 版本号 (默认 132.0.0.0)')
1383
+ .action(options => selectPlaywright('ext-download', { prodversion: options.prodversion || '' }, options));
1476
1384
 
1477
1385
  // 安装包装完后调用的隐藏入口(不在帮助里):按有头 / 无头决定下一步
1478
1386
  if (process.argv[2] === '--post-install') {
@@ -1525,7 +1433,7 @@ https://github.com/xcanwin/manyoyo
1525
1433
 
1526
1434
  UPDATE_CHECK_ENABLED = config.updateCheck !== false;
1527
1435
  MIRRORS = normalizeMirrors(config.mirrors);
1528
- const noDockerActions = new Set(['init', 'update', 'config-show', 'plugin', 'doctor', 'uninstall', 'podman', 'setup']);
1436
+ const noDockerActions = new Set(['init', 'update', 'config-show', 'playwright', 'doctor', 'uninstall', 'podman', 'setup']);
1529
1437
  if (isServerStopMode || isServerListMode) {
1530
1438
  noDockerActions.add('serve');
1531
1439
  }
@@ -1579,21 +1487,13 @@ https://github.com/xcanwin/manyoyo
1579
1487
  process.exit(0);
1580
1488
  }
1581
1489
 
1582
- if (selectedAction === 'plugin') {
1490
+ if (selectedAction === 'playwright') {
1583
1491
  const runConfig = options.run ? loadRunConfig(options.run, config) : {};
1584
1492
  return {
1585
- isPluginMode: true,
1586
- pluginRequest: {
1587
- action: options.pluginAction,
1588
- pluginName: options.pluginName,
1589
- scene: options.pluginScene || 'mcp-host-headless',
1590
- host: options.pluginHost || '',
1591
- extensionPaths: Array.isArray(options.pluginExtensionPaths) ? options.pluginExtensionPaths : [],
1592
- extensionNames: Array.isArray(options.pluginExtensionNames) ? options.pluginExtensionNames : [],
1593
- prodversion: options.pluginProdversion || ''
1594
- },
1595
- pluginGlobalConfig: config,
1596
- pluginRunConfig: runConfig
1493
+ isPlaywrightMode: true,
1494
+ playwrightRequest: options.playwrightRequest,
1495
+ playwrightGlobalConfig: config,
1496
+ playwrightRunConfig: runConfig
1597
1497
  };
1598
1498
  }
1599
1499
 
@@ -1662,7 +1562,7 @@ https://github.com/xcanwin/manyoyo
1662
1562
  const firstEnvMap = resolvedRuntime.first.env;
1663
1563
  Object.entries(firstEnvMap).forEach(([key, value]) => addEnvTo(FIRST_CONTAINER_ENVS, `${key}=${value}`));
1664
1564
 
1665
- applyPlaywrightCliSessionIntegration(config, runConfig);
1565
+ PLAYWRIGHT_PLUGIN_CONFIG = { globalConfig: config, runConfig };
1666
1566
 
1667
1567
  const volumeList = resolvedRuntime.volumes;
1668
1568
  volumeList.forEach(v => addVolume(v));
@@ -1832,7 +1732,7 @@ https://github.com/xcanwin/manyoyo
1832
1732
  isServerDetach: Boolean(selectedAction === 'serve' && options.detach),
1833
1733
  isServerListenSpecified: Boolean(isServerMode && options.server !== true),
1834
1734
  updateAgents: Boolean(options.updateAgents),
1835
- isPluginMode: false
1735
+ isPlaywrightMode: false
1836
1736
  };
1837
1737
  }
1838
1738
 
@@ -2183,6 +2083,8 @@ async function createNewContainer(runtime) {
2183
2083
  );
2184
2084
  const defaultCommand = runtime.execCommand;
2185
2085
 
2086
+ await applyPlaywrightIntegration(runtime);
2087
+
2186
2088
  if (runtime.showCommand) {
2187
2089
  console.log(buildDockerRunCmd(runtime));
2188
2090
  process.exit(0);
@@ -2473,6 +2375,11 @@ async function runWebServerMode(runtime) {
2473
2375
  fetchLatest: () => appUpdate.fetchLatestRelease()
2474
2376
  });
2475
2377
 
2378
+ // 启动时检查 Playwright 当前模式是否还活着,失效就回退到默认模式(只警告,不阻塞启动)
2379
+ buildContainerIntegration({ ...PLAYWRIGHT_PLUGIN_CONFIG, runtimeCommand: DOCKER_CMD })
2380
+ .then(integration => { if (integration.warning) console.warn(`${YELLOW}⚠️ ${integration.warning}${NC}`); })
2381
+ .catch(() => {});
2382
+
2476
2383
  const needRuntimeHeal = !isRuntimeProven();
2477
2384
  if (needRuntimeHeal) {
2478
2385
  RUNTIME_STATE.status = 'starting';
@@ -2586,11 +2493,10 @@ async function main() {
2586
2493
  // 1. Setup commander and parse arguments
2587
2494
  const modeState = await setupCommander();
2588
2495
 
2589
- if (modeState.isPluginMode) {
2590
- const exitCode = await runPluginCommand(modeState.pluginRequest, {
2591
- globalConfig: modeState.pluginGlobalConfig,
2592
- runConfig: modeState.pluginRunConfig,
2593
- projectRoot: path.join(__dirname, '..'),
2496
+ if (modeState.isPlaywrightMode) {
2497
+ const exitCode = await runPlaywrightCommand(modeState.playwrightRequest, {
2498
+ globalConfig: modeState.playwrightGlobalConfig,
2499
+ runConfig: modeState.playwrightRunConfig,
2594
2500
  stdout: process.stdout,
2595
2501
  stderr: process.stderr
2596
2502
  });
@@ -95,7 +95,9 @@ ARG PY_TEXT_EXTRA_PIP_PACKAGES=""
95
95
  ENV LANG=C.UTF-8 \
96
96
  LC_ALL=C.UTF-8 \
97
97
  PIP_ROOT_USER_ACTION=ignore \
98
- PLAYWRIGHT_MCP_CONFIG=/app/config/cli-cont-headless.json
98
+ NO_UPDATE_NOTIFIER=1 \
99
+ DISPLAY=:99 \
100
+ PLAYWRIGHT_MCP_CONFIG=/run/manyoyo-playwright/config.json
99
101
 
100
102
  # 合并系统依赖与 Python 安装为单层,减少镜像体积
101
103
  RUN <<EOX
@@ -152,8 +154,6 @@ EOX
152
154
  # 从 cache-stage 复制 Node.js(缓存或下载)
153
155
  COPY --from=cache-stage /opt/node /usr/local
154
156
  COPY ./package.json /tmp/manyoyo-package.json
155
- COPY ./docker/res/playwright/cli-cont-headless.init.js /app/config/cli-cont-headless.init.js
156
- COPY ./docker/res/playwright/cli-cont-headless.json /app/config/cli-cont-headless.json
157
157
  COPY ./docker/res/ /tmp/docker-res/
158
158
  ARG GIT_SSL_NO_VERIFY=false
159
159
 
@@ -230,12 +230,18 @@ RUN <<EOX
230
230
  cp /tmp/docker-res/opencode/opencode.json ~/.config/opencode/opencode.json
231
231
  ;; esac
232
232
 
233
- # 安装 Playwright CLI skills(不在镜像构建阶段下载浏览器)
233
+ # 安装 Playwright CLI skills 与 chromium 浏览器
234
234
  PLAYWRIGHT_CLI_INSTALL_DIR=/tmp/playwright-cli-install
235
235
  mkdir -p "$PLAYWRIGHT_CLI_INSTALL_DIR/.playwright"
236
236
  cd "$PLAYWRIGHT_CLI_INSTALL_DIR"
237
237
  PLAYWRIGHT_CLI_VERSION=$(node -p "const pkg = require('/tmp/manyoyo-package.json'); const value = String(pkg.playwrightCliVersion || '').trim(); if (!value) { throw new Error('package.json.playwrightCliVersion is required'); } value")
238
238
  npm install -g "@playwright/cli@${PLAYWRIGHT_CLI_VERSION}"
239
+ # 浏览器与系统依赖都用 @playwright/cli 自带的 playwright-core 安装;
240
+ # 默认在 Xvfb 虚拟屏(配轻量窗口管理器)里跑有头浏览器,所以装 xvfb、fluxbox 与常见字体;x11vnc / noVNC 供 playwright vnc 模式使用
241
+ PLAYWRIGHT_CORE_CLI="$(npm root -g)/@playwright/cli/node_modules/playwright-core/cli.js"
242
+ node "$PLAYWRIGHT_CORE_CLI" install-deps chromium
243
+ apt-get install -y --no-install-recommends xvfb fluxbox x11vnc novnc websockify fonts-noto-cjk fonts-noto-color-emoji fonts-liberation
244
+ node "$PLAYWRIGHT_CORE_CLI" install --no-shell chromium
239
245
  playwright-cli install --skills
240
246
  PLAYWRIGHT_CLI_SKILL_SOURCE="$PLAYWRIGHT_CLI_INSTALL_DIR/.claude/skills/playwright-cli"
241
247
  for target in ~/.claude/skills/playwright-cli ~/.codex/skills/playwright-cli ~/.gemini/skills/playwright-cli; do
@@ -252,6 +258,12 @@ RUN <<EOX
252
258
  rm -f /var/log/dpkg.log /var/log/bootstrap.log /var/lib/dpkg/status-old /var/cache/debconf/templates.dat-old
253
259
  EOX
254
260
 
261
+ # Playwright 默认配置(挂载宿主机目录时会被覆盖,路径固定)与按需启动 Xvfb 的 playwright-cli 包装
262
+ COPY ./docker/res/playwright/browser.json /run/manyoyo-playwright/config.json
263
+ COPY ./docker/res/playwright/stealth.init.js /run/manyoyo-playwright/stealth.init.js
264
+ COPY --chmod=755 ./docker/res/playwright/playwright-cli.sh /usr/local/sbin/playwright-cli
265
+ RUN ln -s playwright-cli /usr/local/sbin/playwright-mcp
266
+
255
267
  # 从 cache-stage 复制 JDT LSP 到最终位置,避免中转层残留
256
268
  COPY --from=cache-stage /opt/jdtls /root/.local/share/jdtls
257
269
 
@@ -0,0 +1,28 @@
1
+ {
2
+ "outputDir": "/tmp/.playwright-cli",
3
+ "browser": {
4
+ "chromiumSandbox": false,
5
+ "browserName": "chromium",
6
+ "initScript": [
7
+ "/run/manyoyo-playwright/stealth.init.js"
8
+ ],
9
+ "contextOptions": {
10
+ "locale": "en-US",
11
+ "timezoneId": "UTC",
12
+ "viewport": null,
13
+ "extraHTTPHeaders": {
14
+ "Accept-Language": "en-US,en;q=0.9"
15
+ }
16
+ },
17
+ "launchOptions": {
18
+ "channel": "chromium",
19
+ "headless": false,
20
+ "args": [
21
+ "--lang=en-US",
22
+ "--window-size=1920,1080",
23
+ "--disable-blink-features=AutomationControlled",
24
+ "--force-webrtc-ip-handling-policy=disable_non_proxied_udp"
25
+ ]
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,26 @@
1
+ #!/bin/sh
2
+ # 按需启动 Xvfb 与轻量窗口管理器(多个会话共用 :99),浏览器以有头模式跑在虚拟屏里。
3
+ # 以 playwright-cli 名字调用时交给 @playwright/cli;以 playwright-mcp 名字调用时启动 stdio MCP(读同一份 $PLAYWRIGHT_MCP_CONFIG)。
4
+ export DISPLAY=:99
5
+ CONFIG="${PLAYWRIGHT_MCP_CONFIG:-/run/manyoyo-playwright/config.json}"
6
+ # 浏览器在容器外(headed / chrome / vnc)时不需要虚拟屏
7
+ if ! grep -qE '"(remoteEndpoint|cdpEndpoint)"' "$CONFIG" 2>/dev/null; then
8
+ # 以锁文件里的进程是否活着为准;容器被 kill 后遗留的 socket 与锁文件要清掉
9
+ if ! kill -0 "$(cat /tmp/.X99-lock 2>/dev/null)" 2>/dev/null; then
10
+ rm -f /tmp/.X99-lock /tmp/.X11-unix/X99
11
+ setsid Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp >/dev/null 2>&1 &
12
+ i=0
13
+ while [ ! -S /tmp/.X11-unix/X99 ] && [ "$i" -lt 50 ]; do
14
+ sleep 0.1
15
+ i=$((i + 1))
16
+ done
17
+ # 窗口管理器让窗口有真实的外框与工作区(screen.availHeight < height),不再是裸 X 屏
18
+ setsid fluxbox >/dev/null 2>&1 &
19
+ sleep 0.5
20
+ fi
21
+ fi
22
+ PACKAGE=/usr/local/lib/node_modules/@playwright/cli
23
+ if [ "$(basename "$0")" = "playwright-mcp" ]; then
24
+ exec node "$PACKAGE/node_modules/playwright-core/cli.js" mcp --config "$CONFIG" "$@"
25
+ fi
26
+ exec node "$PACKAGE/playwright-cli.js" "$@"
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+ (function () {
3
+ try {
4
+ const toolbar = 87;
5
+ const patch = (name, fallback, getter) => {
6
+ const original = Object.getOwnPropertyDescriptor(window, name);
7
+ if (!original || !original.get) return;
8
+ const real = original.get;
9
+ Object.defineProperty(window, name, { ...original, get: getter(function () { const value = real.call(this); return value === 0 ? fallback() : value; }) });
10
+ };
11
+ patch('outerWidth', () => window.innerWidth, fn => Object.getOwnPropertyDescriptor({ get outerWidth() { return fn.call(this); } }, 'outerWidth').get);
12
+ patch('outerHeight', () => window.innerHeight + toolbar, fn => Object.getOwnPropertyDescriptor({ get outerHeight() { return fn.call(this); } }, 'outerHeight').get);
13
+ } catch (_) {}
14
+ })();
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
  const JSON5 = require('json5');
7
- const { findTopLevelPropertyValueRange } = require('./json5-text-edit');
7
+ const { upsertValueByPath } = require('./json5-text-edit');
8
8
  const { writeConfigFileSecure } = require('./secure-file');
9
9
 
10
10
  function getManyoyoConfigPath(homeDir = os.homedir()) {
@@ -38,23 +38,8 @@ function readManyoyoConfig(homeDir = os.homedir()) {
38
38
  }
39
39
  }
40
40
 
41
- function insertTopLevelImageVersion(text, imageVersion) {
42
- const openBraceIndex = text.indexOf('{');
43
- if (openBraceIndex === -1) {
44
- return null;
45
- }
46
-
47
- const newlineIndex = text.indexOf('\n', openBraceIndex);
48
- const insertIndex = newlineIndex === -1 ? openBraceIndex + 1 : newlineIndex + 1;
49
- return `${text.slice(0, insertIndex)} imageVersion: ${JSON.stringify(imageVersion)},\n${text.slice(insertIndex)}`;
50
- }
51
-
52
41
  function updateImageVersionText(text, imageVersion) {
53
- const range = findTopLevelPropertyValueRange(text, 'imageVersion');
54
- if (range) {
55
- return `${text.slice(0, range.start)}${JSON.stringify(imageVersion)}${text.slice(range.end)}`;
56
- }
57
- return insertTopLevelImageVersion(text, imageVersion);
42
+ return upsertValueByPath(text, ['imageVersion'], JSON.stringify(imageVersion));
58
43
  }
59
44
 
60
45
  function syncGlobalImageVersion(imageVersion, options = {}) {
@@ -87,22 +72,12 @@ function syncGlobalImageVersion(imageVersion, options = {}) {
87
72
  };
88
73
  }
89
74
 
90
- const nextConfig = {
91
- ...currentConfig,
92
- imageVersion
93
- };
94
-
95
- if (result.exists) {
96
- const currentText = fs.readFileSync(configPath, 'utf-8');
97
- const updatedText = updateImageVersionText(currentText, imageVersion);
98
- if (updatedText) {
99
- writeConfigFileSecure(configPath, updatedText.endsWith('\n') ? updatedText : `${updatedText}\n`);
100
- } else {
101
- writeConfigFileSecure(configPath, `${JSON.stringify(nextConfig, null, 4)}\n`);
102
- }
103
- } else {
104
- writeConfigFileSecure(configPath, `${JSON.stringify(nextConfig, null, 4)}\n`);
105
- }
75
+ // 只在原文上局部替换,保留注释与格式;文件不存在或为空时写标准 JSON
76
+ const currentText = result.exists ? fs.readFileSync(configPath, 'utf-8') : '';
77
+ const updatedText = currentText.trim()
78
+ ? updateImageVersionText(currentText, imageVersion)
79
+ : `${JSON.stringify({ imageVersion }, null, 4)}\n`;
80
+ writeConfigFileSecure(configPath, updatedText.endsWith('\n') ? updatedText : `${updatedText}\n`);
106
81
 
107
82
  return {
108
83
  updated: true,
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
  const { writeConfigFileSecure } = require('./secure-file');
7
+ const { upsertValueByPath } = require('./json5-text-edit');
7
8
 
8
9
  // 每个 Agent 需要哪些环境变量:CLI init 与 Web 首次向导共用的单一数据源。
9
10
  // requiredAnyOf:其中任意一个非空即视为该 Agent 可用。
@@ -414,6 +415,9 @@ async function initAgentConfigs(rawAgents, options = {}) {
414
415
  }
415
416
  runsMap = { ...manyoyoConfig.runs };
416
417
  }
418
+ // 只在原文上局部写入各 runs.<agent>,保留用户的注释与格式
419
+ const originalRaw = fs.existsSync(manyoyoConfigPath) ? fs.readFileSync(manyoyoConfigPath, 'utf-8') : '';
420
+ let nextRaw = originalRaw;
417
421
 
418
422
  let hasConfigChanged = false;
419
423
  ctx.log(`${CYAN}🧭 正在初始化 MANYOYO 配置: ${agents.join(', ')}${NC}`);
@@ -427,6 +431,7 @@ async function initAgentConfigs(rawAgents, options = {}) {
427
431
  if (shouldWriteRun) {
428
432
  const buildResult = buildInitRunProfile(agent, spec.yolo, data.volumes, data.keys, data.values);
429
433
  runsMap[agent] = buildResult.runProfile;
434
+ if (originalRaw.trim()) nextRaw = upsertValueByPath(nextRaw, ['runs', agent], JSON.stringify(buildResult.runProfile, null, 4));
430
435
  writeResult = { missingKeys: buildResult.missingKeys, unsafeKeys: buildResult.unsafeKeys };
431
436
  hasConfigChanged = true;
432
437
  }
@@ -450,8 +455,12 @@ async function initAgentConfigs(rawAgents, options = {}) {
450
455
  }
451
456
 
452
457
  if (hasConfigChanged || !fs.existsSync(manyoyoConfigPath)) {
453
- manyoyoConfig.runs = runsMap;
454
- writeConfigFileSecure(manyoyoConfigPath, `${JSON.stringify(manyoyoConfig, null, 4)}\n`);
458
+ if (originalRaw.trim()) {
459
+ writeConfigFileSecure(manyoyoConfigPath, nextRaw);
460
+ } else {
461
+ // 新文件没有可保留的注释,写标准 JSON
462
+ writeConfigFileSecure(manyoyoConfigPath, `${JSON.stringify({ ...manyoyoConfig, runs: runsMap }, null, 4)}\n`);
463
+ }
455
464
  }
456
465
  }
457
466
 
@@ -252,10 +252,13 @@ function insertObjectProperty(text, objectStartIndex, key, valueText) {
252
252
  const objectIndent = lineIndentAt(text, objectStartIndex);
253
253
  const childIndent = `${objectIndent} `;
254
254
  const prefix = `\n${childIndent}${formatPropertyKey(key)}: `;
255
- // 空对象:闭合的 } 另起一行,不要贴在逗号后面
256
- const closesRightAfter = text[skipTrivia(text, objectStartIndex + 1)] === '}';
255
+ // '{' 同一行后面只有空白或 // 注释时,插到该行末尾,注释留在 '{' 那一行,不会被挪到新属性后面
256
+ const lineEnd = text.indexOf('\n', objectStartIndex + 1);
257
+ const tailIsTrivia = lineEnd !== -1 && /^\s*(\/\/.*)?$/.test(text.slice(objectStartIndex + 1, lineEnd));
258
+ // 空对象且 } 紧跟在同一行:闭合的 } 另起一行;} 已在后面的行上就不再补,避免多出空行
259
+ const closesRightAfter = !tailIsTrivia && text[skipTrivia(text, objectStartIndex + 1)] === '}';
257
260
  const insertion = `${prefix}${reindent(valueText, childIndent)},${closesRightAfter ? `\n${objectIndent}` : ''}`;
258
- const insertPos = objectStartIndex + 1;
261
+ const insertPos = tailIsTrivia ? lineEnd : objectStartIndex + 1;
259
262
  const nextText = `${text.slice(0, insertPos)}${insertion}${text.slice(insertPos)}`;
260
263
  return { text: nextText, valueStart: insertPos + prefix.length };
261
264
  }