@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 +1 -1
- package/README.md +1 -1
- package/bin/manyoyo.js +70 -164
- package/docker/manyoyo.Dockerfile +16 -4
- package/docker/res/playwright/browser.json +28 -0
- package/docker/res/playwright/playwright-cli.sh +26 -0
- package/docker/res/playwright/stealth.init.js +14 -0
- package/lib/global-config.js +8 -33
- package/lib/init-config.js +11 -2
- package/lib/json5-text-edit.js +6 -3
- package/lib/plugin/fingerprint.js +176 -0
- package/lib/plugin/index.js +62 -47
- package/lib/plugin/playwright-assets/vnc-entry.sh +21 -0
- package/lib/plugin/playwright-extensions.js +299 -0
- package/lib/plugin/playwright-probe.js +53 -0
- package/lib/plugin/playwright-relay.js +88 -0
- package/lib/plugin/playwright-server.js +23 -0
- package/lib/plugin/playwright.js +707 -1853
- package/lib/web/server.js +27 -3
- package/manyoyo.example.json +16 -28
- package/package.json +4 -5
- package/docker/res/playwright/cli-cont-headless.init.js +0 -12
- package/docker/res/playwright/cli-cont-headless.json +0 -37
- package/lib/plugin/playwright-assets/compose-headed.yaml +0 -19
- package/lib/plugin/playwright-assets/compose-headless.yaml +0 -11
- package/lib/plugin/playwright-assets/headed.Dockerfile +0 -15
- package/lib/plugin/playwright-assets/init-headed.sh +0 -17
package/README.en.md
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.
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
|
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
|
-
|
|
1461
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
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
|
-
|
|
1475
|
-
|
|
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', '
|
|
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 === '
|
|
1490
|
+
if (selectedAction === 'playwright') {
|
|
1583
1491
|
const runConfig = options.run ? loadRunConfig(options.run, config) : {};
|
|
1584
1492
|
return {
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2590
|
-
const exitCode = await
|
|
2591
|
-
globalConfig: modeState.
|
|
2592
|
-
runConfig: modeState.
|
|
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
|
-
|
|
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
|
+
})();
|
package/lib/global-config.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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,
|
package/lib/init-config.js
CHANGED
|
@@ -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
|
-
|
|
454
|
-
|
|
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
|
|
package/lib/json5-text-edit.js
CHANGED
|
@@ -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
|
|
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
|
}
|