@xcanwin/manyoyo 5.1.0 → 5.1.3
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/bin/manyoyo.js +30 -10
- package/config.example.json +2 -0
- package/lib/{services → plugin}/index.js +3 -1
- package/lib/{services → plugin}/playwright.js +420 -27
- package/package.json +1 -1
- /package/lib/{services → plugin}/playwright-assets/compose-headed.yaml +0 -0
- /package/lib/{services → plugin}/playwright-assets/compose-headless.yaml +0 -0
- /package/lib/{services → plugin}/playwright-assets/headed.Dockerfile +0 -0
- /package/lib/{services → plugin}/playwright-assets/init-headed.sh +0 -0
package/bin/manyoyo.js
CHANGED
|
@@ -14,7 +14,7 @@ const { buildContainerRunArgs, buildContainerRunCommand } = require('../lib/cont
|
|
|
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/
|
|
17
|
+
const { runPluginCommand } = require('../lib/plugin');
|
|
18
18
|
const { version: BIN_VERSION, imageVersion: IMAGE_VERSION_DEFAULT } = require('../package.json');
|
|
19
19
|
const IMAGE_VERSION_BASE = String(IMAGE_VERSION_DEFAULT || '1.0.0').split('-')[0];
|
|
20
20
|
const IMAGE_VERSION_HELP_EXAMPLE = IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
|
|
@@ -873,7 +873,9 @@ async function setupCommander() {
|
|
|
873
873
|
pluginAction: params.action || 'ls',
|
|
874
874
|
pluginName: params.pluginName || 'playwright',
|
|
875
875
|
pluginScene: params.scene || 'host-headless',
|
|
876
|
-
pluginHost: params.host || ''
|
|
876
|
+
pluginHost: params.host || '',
|
|
877
|
+
pluginExtensions: Array.isArray(params.extensions) ? params.extensions : [],
|
|
878
|
+
pluginProdversion: params.prodversion || ''
|
|
877
879
|
});
|
|
878
880
|
};
|
|
879
881
|
|
|
@@ -889,14 +891,20 @@ async function setupCommander() {
|
|
|
889
891
|
|
|
890
892
|
const actions = ['up', 'down', 'status', 'health', 'logs'];
|
|
891
893
|
actions.forEach(action => {
|
|
892
|
-
command.command(`${action} [scene]`)
|
|
894
|
+
const sceneCommand = command.command(`${action} [scene]`)
|
|
893
895
|
.description(`${action} playwright 场景,scene 默认 host-headless`)
|
|
894
|
-
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
896
|
+
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)');
|
|
897
|
+
|
|
898
|
+
if (action === 'up') {
|
|
899
|
+
appendArrayOption(sceneCommand, '--ext <path>', '追加浏览器扩展目录(可多次传入;目录需包含 manifest.json)');
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
sceneCommand.action((scene, options) => selectPluginAction({
|
|
903
|
+
action,
|
|
904
|
+
pluginName: 'playwright',
|
|
905
|
+
scene: scene || 'host-headless',
|
|
906
|
+
extensions: action === 'up' ? (options.ext || []) : []
|
|
907
|
+
}, options));
|
|
900
908
|
});
|
|
901
909
|
|
|
902
910
|
command.command('mcp-add')
|
|
@@ -909,6 +917,16 @@ async function setupCommander() {
|
|
|
909
917
|
scene: 'all',
|
|
910
918
|
host: options.host || ''
|
|
911
919
|
}, options));
|
|
920
|
+
|
|
921
|
+
command.command('ext-download')
|
|
922
|
+
.description('下载并解压 Playwright 扩展到 ~/.manyoyo/plugin/playwright/extensions/')
|
|
923
|
+
.option('--prodversion <ver>', 'CRX 下载使用的 Chrome 版本号 (默认 132.0.0.0)')
|
|
924
|
+
.action(options => selectPluginAction({
|
|
925
|
+
action: 'ext-download',
|
|
926
|
+
pluginName: 'playwright',
|
|
927
|
+
scene: 'all',
|
|
928
|
+
prodversion: options.prodversion || ''
|
|
929
|
+
}, options));
|
|
912
930
|
};
|
|
913
931
|
|
|
914
932
|
program
|
|
@@ -1095,7 +1113,9 @@ async function setupCommander() {
|
|
|
1095
1113
|
action: options.pluginAction,
|
|
1096
1114
|
pluginName: options.pluginName,
|
|
1097
1115
|
scene: options.pluginScene || 'host-headless',
|
|
1098
|
-
host: options.pluginHost || ''
|
|
1116
|
+
host: options.pluginHost || '',
|
|
1117
|
+
extensions: Array.isArray(options.pluginExtensions) ? options.pluginExtensions : [],
|
|
1118
|
+
prodversion: options.pluginProdversion || ''
|
|
1099
1119
|
},
|
|
1100
1120
|
pluginGlobalConfig: config,
|
|
1101
1121
|
pluginRunConfig: runConfig
|
package/config.example.json
CHANGED
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
"mcpDefaultHost": "host.docker.internal",
|
|
37
37
|
// cont-headed 场景读取的密码环境变量名(默认 VNC_PASSWORD)
|
|
38
38
|
"vncPasswordEnvKey": "VNC_PASSWORD",
|
|
39
|
+
// playwright ext-download 的 CRX prodversion 参数
|
|
40
|
+
"extensionProdversion": "132.0.0.0",
|
|
39
41
|
"ports": {
|
|
40
42
|
"contHeadless": 8931,
|
|
41
43
|
"contHeaded": 8932,
|
|
@@ -6,6 +6,14 @@ const os = require('os');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { spawn, spawnSync } = require('child_process');
|
|
8
8
|
|
|
9
|
+
const EXTENSIONS = [
|
|
10
|
+
['ublock-origin-lite', 'ddkjiahejlhfcafbddmgiahcphecmpfh'],
|
|
11
|
+
['adguard', 'bgnkhhnnamicmpeenaelnjfhikgbkllg'],
|
|
12
|
+
['privacy-badger', 'pkehgijcmpdhfbdbbnkijodmdjhbjlgp'],
|
|
13
|
+
['webrtc-leak-shield', 'bppamachkoflopbagkdoflbgfjflfnfl'],
|
|
14
|
+
['webgl-fingerprint-defender', 'olnbjpaejebpnokblkepbphhembdicik']
|
|
15
|
+
];
|
|
16
|
+
|
|
9
17
|
const SCENE_ORDER = ['cont-headless', 'cont-headed', 'host-headless', 'host-headed'];
|
|
10
18
|
|
|
11
19
|
const SCENE_DEFS = {
|
|
@@ -47,6 +55,7 @@ const SCENE_DEFS = {
|
|
|
47
55
|
|
|
48
56
|
const VALID_RUNTIME = new Set(['container', 'host', 'mixed']);
|
|
49
57
|
const VALID_ACTIONS = new Set(['up', 'down', 'status', 'health', 'logs']);
|
|
58
|
+
const CONTAINER_EXTENSION_ROOT = '/app/extensions';
|
|
50
59
|
|
|
51
60
|
function sleep(ms) {
|
|
52
61
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
@@ -76,6 +85,129 @@ function asStringArray(value, fallback) {
|
|
|
76
85
|
.filter(Boolean);
|
|
77
86
|
}
|
|
78
87
|
|
|
88
|
+
function isHostPermission(value) {
|
|
89
|
+
if (value === '<all_urls>') {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return /^(?:\*|http|https|file|ftp):\/\//.test(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function scriptSourcesFromHtml(htmlFile) {
|
|
96
|
+
const content = fs.readFileSync(htmlFile, { encoding: 'utf8' });
|
|
97
|
+
const scripts = [...content.matchAll(/<script[^>]+src=["']([^"']+)["']/gi)].map(m => m[1]);
|
|
98
|
+
return scripts.filter(src => !/^(?:https?:)?\/\//.test(src));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function convertManifestV2ToV3(extDir) {
|
|
102
|
+
const manifestFile = path.join(extDir, 'manifest.json');
|
|
103
|
+
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
104
|
+
if (manifest.manifest_version !== 2) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
manifest.manifest_version = 3;
|
|
109
|
+
|
|
110
|
+
if (manifest.browser_action && !manifest.action) {
|
|
111
|
+
manifest.action = manifest.browser_action;
|
|
112
|
+
delete manifest.browser_action;
|
|
113
|
+
}
|
|
114
|
+
if (manifest.page_action && !manifest.action) {
|
|
115
|
+
manifest.action = manifest.page_action;
|
|
116
|
+
delete manifest.page_action;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const background = manifest.background;
|
|
120
|
+
if (background && typeof background === 'object' && !Array.isArray(background)) {
|
|
121
|
+
let scripts = [];
|
|
122
|
+
if (Array.isArray(background.scripts)) {
|
|
123
|
+
scripts = background.scripts.filter(s => typeof s === 'string');
|
|
124
|
+
} else if (typeof background.page === 'string') {
|
|
125
|
+
const pagePath = path.join(extDir, background.page);
|
|
126
|
+
if (fs.existsSync(pagePath)) {
|
|
127
|
+
scripts = scriptSourcesFromHtml(pagePath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (scripts.length > 0) {
|
|
132
|
+
const swName = 'generated_background_sw.js';
|
|
133
|
+
const swFile = path.join(extDir, swName);
|
|
134
|
+
const swLines = [
|
|
135
|
+
'// Auto-generated by manyoyo playwright ext-download for MV3.',
|
|
136
|
+
`importScripts(${scripts.map(s => JSON.stringify(s)).join(', ')});`,
|
|
137
|
+
''
|
|
138
|
+
];
|
|
139
|
+
fs.writeFileSync(swFile, swLines.join('\n'), 'utf8');
|
|
140
|
+
manifest.background = { service_worker: swName };
|
|
141
|
+
} else {
|
|
142
|
+
delete manifest.background;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (typeof manifest.content_security_policy === 'string') {
|
|
147
|
+
manifest.content_security_policy = { extension_pages: manifest.content_security_policy };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (Array.isArray(manifest.permissions)) {
|
|
151
|
+
const hostPermissions = Array.isArray(manifest.host_permissions) ? [...manifest.host_permissions] : [];
|
|
152
|
+
const keptPermissions = [];
|
|
153
|
+
|
|
154
|
+
for (const perm of manifest.permissions) {
|
|
155
|
+
if (typeof perm === 'string' && isHostPermission(perm)) {
|
|
156
|
+
if (!hostPermissions.includes(perm)) {
|
|
157
|
+
hostPermissions.push(perm);
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
keptPermissions.push(perm);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
manifest.permissions = keptPermissions;
|
|
165
|
+
if (hostPermissions.length > 0) {
|
|
166
|
+
manifest.host_permissions = hostPermissions;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const war = manifest.web_accessible_resources;
|
|
171
|
+
if (Array.isArray(war) && war.length > 0 && war.every(v => typeof v === 'string')) {
|
|
172
|
+
manifest.web_accessible_resources = [
|
|
173
|
+
{
|
|
174
|
+
resources: war,
|
|
175
|
+
matches: ['<all_urls>']
|
|
176
|
+
}
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fs.writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildCrxUrl(extId, prodversion) {
|
|
185
|
+
return (
|
|
186
|
+
'https://clients2.google.com/service/update2/crx' +
|
|
187
|
+
`?response=redirect&prodversion=${prodversion}` +
|
|
188
|
+
'&acceptformat=crx2,crx3' +
|
|
189
|
+
`&x=id%3D${extId}%26installsource%3Dondemand%26uc`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function crxZipOffset(data) {
|
|
194
|
+
if (data.subarray(0, 4).toString('ascii') !== 'Cr24') {
|
|
195
|
+
throw new Error('not a CRX file');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const version = data.readUInt32LE(4);
|
|
199
|
+
if (version === 2) {
|
|
200
|
+
const pubLen = data.readUInt32LE(8);
|
|
201
|
+
const sigLen = data.readUInt32LE(12);
|
|
202
|
+
return 16 + pubLen + sigLen;
|
|
203
|
+
}
|
|
204
|
+
if (version === 3) {
|
|
205
|
+
const headerLen = data.readUInt32LE(8);
|
|
206
|
+
return 12 + headerLen;
|
|
207
|
+
}
|
|
208
|
+
throw new Error(`unsupported CRX version: ${version}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
79
211
|
class PlaywrightPlugin {
|
|
80
212
|
constructor(options = {}) {
|
|
81
213
|
this.projectRoot = options.projectRoot || path.join(__dirname, '..', '..');
|
|
@@ -88,6 +220,7 @@ class PlaywrightPlugin {
|
|
|
88
220
|
|
|
89
221
|
resolveConfig() {
|
|
90
222
|
const homeDir = os.homedir();
|
|
223
|
+
const pluginRootDir = path.join(homeDir, '.manyoyo', 'plugin', 'playwright');
|
|
91
224
|
const defaultConfig = {
|
|
92
225
|
runtime: 'mixed',
|
|
93
226
|
enabledScenes: [...SCENE_ORDER],
|
|
@@ -98,8 +231,9 @@ class PlaywrightPlugin {
|
|
|
98
231
|
containerRuntime: 'podman',
|
|
99
232
|
vncPasswordEnvKey: 'VNC_PASSWORD',
|
|
100
233
|
headedImage: 'localhost/xcanwin/manyoyo-playwright-headed',
|
|
101
|
-
configDir: path.join(
|
|
102
|
-
runDir: path.join(
|
|
234
|
+
configDir: path.join(pluginRootDir, 'config'),
|
|
235
|
+
runDir: path.join(pluginRootDir, 'run'),
|
|
236
|
+
extensionProdversion: '132.0.0.0',
|
|
103
237
|
composeDir: path.join(__dirname, 'playwright-assets'),
|
|
104
238
|
ports: {
|
|
105
239
|
contHeadless: 8931,
|
|
@@ -184,8 +318,15 @@ class PlaywrightPlugin {
|
|
|
184
318
|
}
|
|
185
319
|
|
|
186
320
|
ensureCommandAvailable(command) {
|
|
187
|
-
const
|
|
188
|
-
|
|
321
|
+
const name = String(command || '').trim();
|
|
322
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name)) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
const result = spawnSync('sh', ['-c', `command -v ${name}`], {
|
|
326
|
+
encoding: 'utf8',
|
|
327
|
+
stdio: ['ignore', 'ignore', 'ignore']
|
|
328
|
+
});
|
|
329
|
+
return result.status === 0;
|
|
189
330
|
}
|
|
190
331
|
|
|
191
332
|
scenePort(sceneName) {
|
|
@@ -206,6 +347,14 @@ class PlaywrightPlugin {
|
|
|
206
347
|
return path.join(this.config.runDir, `${sceneName}.log`);
|
|
207
348
|
}
|
|
208
349
|
|
|
350
|
+
extensionDirPath() {
|
|
351
|
+
return path.join(os.homedir(), '.manyoyo', 'plugin', 'playwright', 'extensions');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
extensionTmpDirPath() {
|
|
355
|
+
return path.join(os.homedir(), '.manyoyo', 'plugin', 'playwright', 'tmp-crx');
|
|
356
|
+
}
|
|
357
|
+
|
|
209
358
|
resolveTargets(sceneName = 'all') {
|
|
210
359
|
const requested = String(sceneName || 'all').trim();
|
|
211
360
|
const enabledSet = new Set(this.config.enabledScenes);
|
|
@@ -237,9 +386,94 @@ class PlaywrightPlugin {
|
|
|
237
386
|
.filter(scene => isAllowedByRuntime(scene));
|
|
238
387
|
}
|
|
239
388
|
|
|
240
|
-
|
|
389
|
+
resolveExtensionPaths(extensionArgs = []) {
|
|
390
|
+
const inputs = asStringArray(extensionArgs, []);
|
|
391
|
+
const uniquePaths = [];
|
|
392
|
+
const seen = new Set();
|
|
393
|
+
|
|
394
|
+
for (const item of inputs) {
|
|
395
|
+
const absPath = path.resolve(item);
|
|
396
|
+
if (!fs.existsSync(absPath)) {
|
|
397
|
+
throw new Error(`扩展路径不存在: ${absPath}`);
|
|
398
|
+
}
|
|
399
|
+
const stat = fs.statSync(absPath);
|
|
400
|
+
if (!stat.isDirectory()) {
|
|
401
|
+
throw new Error(`扩展路径必须是目录: ${absPath}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const manifestPath = path.join(absPath, 'manifest.json');
|
|
405
|
+
if (fs.existsSync(manifestPath)) {
|
|
406
|
+
if (!seen.has(absPath)) {
|
|
407
|
+
seen.add(absPath);
|
|
408
|
+
uniquePaths.push(absPath);
|
|
409
|
+
}
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const children = fs.readdirSync(absPath, { withFileTypes: true })
|
|
414
|
+
.filter(dirent => dirent.isDirectory())
|
|
415
|
+
.map(dirent => path.join(absPath, dirent.name))
|
|
416
|
+
.filter(child => fs.existsSync(path.join(child, 'manifest.json')));
|
|
417
|
+
|
|
418
|
+
if (children.length === 0) {
|
|
419
|
+
throw new Error(`目录下未找到扩展(manifest.json): ${absPath}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
for (const childPath of children) {
|
|
423
|
+
if (!seen.has(childPath)) {
|
|
424
|
+
seen.add(childPath);
|
|
425
|
+
uniquePaths.push(childPath);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return uniquePaths;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
buildExtensionLaunchArgs(extensionPaths) {
|
|
434
|
+
const joined = extensionPaths.join(',');
|
|
435
|
+
return [
|
|
436
|
+
`--disable-extensions-except=${joined}`,
|
|
437
|
+
`--load-extension=${joined}`
|
|
438
|
+
];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
sanitizeExtensionMountName(value) {
|
|
442
|
+
const sanitized = String(value || '')
|
|
443
|
+
.trim()
|
|
444
|
+
.replace(/[^A-Za-z0-9._-]/g, '-')
|
|
445
|
+
.replace(/-+/g, '-')
|
|
446
|
+
.replace(/^-|-$/g, '');
|
|
447
|
+
return sanitized || 'ext';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
buildContainerExtensionMounts(extensionPaths = []) {
|
|
451
|
+
const hostPaths = asStringArray(extensionPaths, []);
|
|
452
|
+
const containerPaths = [];
|
|
453
|
+
const volumeMounts = [];
|
|
454
|
+
|
|
455
|
+
hostPaths.forEach((hostPath, idx) => {
|
|
456
|
+
const safeName = this.sanitizeExtensionMountName(path.basename(hostPath));
|
|
457
|
+
const containerPath = path.posix.join(CONTAINER_EXTENSION_ROOT, `ext-${idx + 1}-${safeName}`);
|
|
458
|
+
containerPaths.push(containerPath);
|
|
459
|
+
volumeMounts.push(`${hostPath}:${containerPath}:ro`);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
return { containerPaths, volumeMounts };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
buildSceneConfig(sceneName, options = {}) {
|
|
241
466
|
const def = SCENE_DEFS[sceneName];
|
|
242
467
|
const port = this.scenePort(sceneName);
|
|
468
|
+
const extensionPaths = asStringArray(options.extensionPaths, []);
|
|
469
|
+
const launchOptions = {
|
|
470
|
+
channel: 'chromium',
|
|
471
|
+
headless: def.headless
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
if (extensionPaths.length > 0) {
|
|
475
|
+
launchOptions.args = this.buildExtensionLaunchArgs(extensionPaths);
|
|
476
|
+
}
|
|
243
477
|
|
|
244
478
|
return {
|
|
245
479
|
server: {
|
|
@@ -255,10 +489,7 @@ class PlaywrightPlugin {
|
|
|
255
489
|
browser: {
|
|
256
490
|
chromiumSandbox: true,
|
|
257
491
|
browserName: 'chromium',
|
|
258
|
-
launchOptions
|
|
259
|
-
channel: 'chromium',
|
|
260
|
-
headless: def.headless
|
|
261
|
-
},
|
|
492
|
+
launchOptions,
|
|
262
493
|
contextOptions: {
|
|
263
494
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36'
|
|
264
495
|
}
|
|
@@ -266,9 +497,9 @@ class PlaywrightPlugin {
|
|
|
266
497
|
};
|
|
267
498
|
}
|
|
268
499
|
|
|
269
|
-
ensureSceneConfig(sceneName) {
|
|
500
|
+
ensureSceneConfig(sceneName, options = {}) {
|
|
270
501
|
fs.mkdirSync(this.config.configDir, { recursive: true });
|
|
271
|
-
const payload = this.buildSceneConfig(sceneName);
|
|
502
|
+
const payload = this.buildSceneConfig(sceneName, options);
|
|
272
503
|
const filePath = this.sceneConfigPath(sceneName);
|
|
273
504
|
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
|
|
274
505
|
return filePath;
|
|
@@ -339,28 +570,68 @@ class PlaywrightPlugin {
|
|
|
339
570
|
return path.join(this.config.composeDir, def.composeFile);
|
|
340
571
|
}
|
|
341
572
|
|
|
342
|
-
|
|
573
|
+
sceneComposeOverridePath(sceneName) {
|
|
574
|
+
return path.join(this.config.runDir, `${sceneName}.compose.override.yaml`);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
ensureContainerComposeOverride(sceneName, volumeMounts = []) {
|
|
578
|
+
const overridePath = this.sceneComposeOverridePath(sceneName);
|
|
579
|
+
if (!Array.isArray(volumeMounts) || volumeMounts.length === 0) {
|
|
580
|
+
fs.rmSync(overridePath, { force: true });
|
|
581
|
+
return '';
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
fs.mkdirSync(this.config.runDir, { recursive: true });
|
|
585
|
+
const lines = [
|
|
586
|
+
'services:',
|
|
587
|
+
' playwright:',
|
|
588
|
+
' volumes:'
|
|
589
|
+
];
|
|
590
|
+
volumeMounts.forEach(item => {
|
|
591
|
+
lines.push(` - ${JSON.stringify(String(item))}`);
|
|
592
|
+
});
|
|
593
|
+
fs.writeFileSync(overridePath, `${lines.join('\n')}\n`, 'utf8');
|
|
594
|
+
return overridePath;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async startContainer(sceneName, options = {}) {
|
|
343
598
|
const runtime = this.config.containerRuntime;
|
|
344
599
|
if (!this.ensureCommandAvailable(runtime)) {
|
|
345
600
|
this.writeStderr(`[up] ${sceneName} failed: ${runtime} command not found.`);
|
|
346
601
|
return 1;
|
|
347
602
|
}
|
|
348
603
|
|
|
349
|
-
const
|
|
604
|
+
const incomingExtensionPaths = asStringArray(options.extensionPaths, []);
|
|
605
|
+
let configOptions = { ...options, extensionPaths: incomingExtensionPaths };
|
|
606
|
+
const composeFiles = [this.containerComposePath(sceneName)];
|
|
607
|
+
|
|
608
|
+
if (incomingExtensionPaths.length > 0) {
|
|
609
|
+
const mapped = this.buildContainerExtensionMounts(incomingExtensionPaths);
|
|
610
|
+
const overridePath = this.ensureContainerComposeOverride(sceneName, mapped.volumeMounts);
|
|
611
|
+
if (overridePath) {
|
|
612
|
+
composeFiles.push(overridePath);
|
|
613
|
+
}
|
|
614
|
+
configOptions = { ...options, extensionPaths: mapped.containerPaths };
|
|
615
|
+
} else {
|
|
616
|
+
this.ensureContainerComposeOverride(sceneName, []);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const cfgPath = this.ensureSceneConfig(sceneName, configOptions);
|
|
350
620
|
const env = this.containerEnv(sceneName, cfgPath, { requireVncPassword: true });
|
|
351
621
|
const def = SCENE_DEFS[sceneName];
|
|
352
622
|
|
|
353
623
|
try {
|
|
354
|
-
|
|
624
|
+
const args = [
|
|
355
625
|
runtime,
|
|
356
626
|
'compose',
|
|
357
627
|
'-p',
|
|
358
|
-
def.projectName
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
'
|
|
362
|
-
|
|
363
|
-
|
|
628
|
+
def.projectName
|
|
629
|
+
];
|
|
630
|
+
composeFiles.forEach(filePath => {
|
|
631
|
+
args.push('-f', filePath);
|
|
632
|
+
});
|
|
633
|
+
args.push('up', '-d');
|
|
634
|
+
this.runCmd(args, { env, check: true });
|
|
364
635
|
} catch (error) {
|
|
365
636
|
return error.returncode || 1;
|
|
366
637
|
}
|
|
@@ -382,6 +653,7 @@ class PlaywrightPlugin {
|
|
|
382
653
|
return 1;
|
|
383
654
|
}
|
|
384
655
|
|
|
656
|
+
this.ensureContainerComposeOverride(sceneName, []);
|
|
385
657
|
const cfgPath = this.sceneConfigPath(sceneName);
|
|
386
658
|
const env = this.containerEnv(sceneName, cfgPath);
|
|
387
659
|
const def = SCENE_DEFS[sceneName];
|
|
@@ -520,14 +792,14 @@ class PlaywrightPlugin {
|
|
|
520
792
|
return [];
|
|
521
793
|
}
|
|
522
794
|
|
|
523
|
-
async startHost(sceneName) {
|
|
795
|
+
async startHost(sceneName, options = {}) {
|
|
524
796
|
if (!this.ensureCommandAvailable('npx')) {
|
|
525
797
|
this.writeStderr(`[up] ${sceneName} failed: npx command not found.`);
|
|
526
798
|
return 1;
|
|
527
799
|
}
|
|
528
800
|
|
|
529
801
|
fs.mkdirSync(this.config.runDir, { recursive: true });
|
|
530
|
-
const cfgPath = this.ensureSceneConfig(sceneName);
|
|
802
|
+
const cfgPath = this.ensureSceneConfig(sceneName, options);
|
|
531
803
|
const pidFile = this.scenePidFile(sceneName);
|
|
532
804
|
const logFile = this.sceneLogFile(sceneName);
|
|
533
805
|
const port = this.scenePort(sceneName);
|
|
@@ -665,6 +937,118 @@ class PlaywrightPlugin {
|
|
|
665
937
|
return 0;
|
|
666
938
|
}
|
|
667
939
|
|
|
940
|
+
async downloadFile(url, output, retries = 3, timeoutMs = 60_000) {
|
|
941
|
+
const timeoutSec = Math.max(1, Math.ceil(timeoutMs / 1000));
|
|
942
|
+
if (!this.ensureCommandAvailable('curl')) {
|
|
943
|
+
throw new Error('curl command not found');
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
let lastError = null;
|
|
947
|
+
for (let i = 1; i <= retries; i += 1) {
|
|
948
|
+
try {
|
|
949
|
+
const result = this.runCmd([
|
|
950
|
+
'curl',
|
|
951
|
+
'--fail',
|
|
952
|
+
'--location',
|
|
953
|
+
'--silent',
|
|
954
|
+
'--show-error',
|
|
955
|
+
'--connect-timeout',
|
|
956
|
+
String(timeoutSec),
|
|
957
|
+
'--max-time',
|
|
958
|
+
String(timeoutSec),
|
|
959
|
+
'--output',
|
|
960
|
+
output,
|
|
961
|
+
url
|
|
962
|
+
], { captureOutput: true, check: false });
|
|
963
|
+
if (result.returncode !== 0) {
|
|
964
|
+
throw new Error(result.stderr || `curl failed with exit code ${result.returncode}`);
|
|
965
|
+
}
|
|
966
|
+
return;
|
|
967
|
+
} catch (error) {
|
|
968
|
+
lastError = error;
|
|
969
|
+
if (i < retries) {
|
|
970
|
+
// eslint-disable-next-line no-await-in-loop
|
|
971
|
+
await sleep(1000);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
throw new Error(`download failed after ${retries} attempts: ${url}; ${String(lastError)}`);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
extractZipBuffer(zipBuffer, outDir) {
|
|
980
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
981
|
+
const tempZip = path.join(os.tmpdir(), `manyoyo-playwright-ext-${process.pid}-${Date.now()}.zip`);
|
|
982
|
+
fs.writeFileSync(tempZip, zipBuffer);
|
|
983
|
+
|
|
984
|
+
const result = spawnSync('unzip', ['-oq', tempZip, '-d', outDir], { encoding: 'utf8' });
|
|
985
|
+
fs.rmSync(tempZip, { force: true });
|
|
986
|
+
|
|
987
|
+
if (result.error) {
|
|
988
|
+
throw result.error;
|
|
989
|
+
}
|
|
990
|
+
if (result.status !== 0) {
|
|
991
|
+
throw new Error(result.stderr || `unzip failed with exit code ${result.status}`);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
extractCrx(crxFile, outDir) {
|
|
996
|
+
const data = fs.readFileSync(crxFile);
|
|
997
|
+
const offset = crxZipOffset(data);
|
|
998
|
+
const zipBuffer = data.subarray(offset);
|
|
999
|
+
|
|
1000
|
+
this.extractZipBuffer(zipBuffer, outDir);
|
|
1001
|
+
|
|
1002
|
+
const manifest = path.join(outDir, 'manifest.json');
|
|
1003
|
+
if (!fs.existsSync(manifest)) {
|
|
1004
|
+
throw new Error(`${crxFile} extracted but manifest.json missing`);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (convertManifestV2ToV3(outDir)) {
|
|
1008
|
+
this.writeStdout(`[manifest] upgraded to MV3: ${path.basename(outDir)}`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async downloadExtensions(options = {}) {
|
|
1013
|
+
if (!this.ensureCommandAvailable('unzip')) {
|
|
1014
|
+
this.writeStderr('[ext-download] failed: unzip command not found.');
|
|
1015
|
+
return 1;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const prodversion = String(options.prodversion || this.config.extensionProdversion || '132.0.0.0').trim();
|
|
1019
|
+
const extDir = path.resolve(this.extensionDirPath());
|
|
1020
|
+
const tmpDir = path.resolve(this.extensionTmpDirPath());
|
|
1021
|
+
|
|
1022
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1023
|
+
fs.mkdirSync(extDir, { recursive: true });
|
|
1024
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1025
|
+
|
|
1026
|
+
try {
|
|
1027
|
+
this.writeStdout(`[info] ext dir: ${extDir}`);
|
|
1028
|
+
this.writeStdout(`[info] tmp dir: ${tmpDir}`);
|
|
1029
|
+
|
|
1030
|
+
for (const [name, extId] of EXTENSIONS) {
|
|
1031
|
+
const url = buildCrxUrl(extId, prodversion);
|
|
1032
|
+
const crxFile = path.join(tmpDir, `${name}.crx`);
|
|
1033
|
+
const outDir = path.join(extDir, name);
|
|
1034
|
+
|
|
1035
|
+
this.writeStdout(`[download] ${name}`);
|
|
1036
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1037
|
+
await this.downloadFile(url, crxFile);
|
|
1038
|
+
|
|
1039
|
+
this.writeStdout(`[extract] ${name}`);
|
|
1040
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
1041
|
+
this.extractCrx(crxFile, outDir);
|
|
1042
|
+
}
|
|
1043
|
+
} finally {
|
|
1044
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1045
|
+
this.writeStdout(`[cleanup] removed ${tmpDir}`);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
this.writeStdout(`[done] all extensions are ready: ${extDir}`);
|
|
1049
|
+
return 0;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
668
1052
|
detectCurrentIPv4() {
|
|
669
1053
|
const interfaces = os.networkInterfaces();
|
|
670
1054
|
for (const values of Object.values(interfaces)) {
|
|
@@ -724,12 +1108,12 @@ class PlaywrightPlugin {
|
|
|
724
1108
|
return 0;
|
|
725
1109
|
}
|
|
726
1110
|
|
|
727
|
-
async runOnScene(action, sceneName) {
|
|
1111
|
+
async runOnScene(action, sceneName, options = {}) {
|
|
728
1112
|
const def = SCENE_DEFS[sceneName];
|
|
729
1113
|
if (action === 'up') {
|
|
730
1114
|
return def.type === 'container'
|
|
731
|
-
? await this.startContainer(sceneName)
|
|
732
|
-
: await this.startHost(sceneName);
|
|
1115
|
+
? await this.startContainer(sceneName, options)
|
|
1116
|
+
: await this.startHost(sceneName, options);
|
|
733
1117
|
}
|
|
734
1118
|
if (action === 'down') {
|
|
735
1119
|
return def.type === 'container'
|
|
@@ -753,7 +1137,7 @@ class PlaywrightPlugin {
|
|
|
753
1137
|
return 1;
|
|
754
1138
|
}
|
|
755
1139
|
|
|
756
|
-
async run({ action, scene = 'host-headless', host = '' }) {
|
|
1140
|
+
async run({ action, scene = 'host-headless', host = '', extensions = [], prodversion = '' }) {
|
|
757
1141
|
if (action === 'ls') {
|
|
758
1142
|
return this.printSummary();
|
|
759
1143
|
}
|
|
@@ -762,6 +1146,10 @@ class PlaywrightPlugin {
|
|
|
762
1146
|
return this.printMcpAdd(host);
|
|
763
1147
|
}
|
|
764
1148
|
|
|
1149
|
+
if (action === 'ext-download') {
|
|
1150
|
+
return await this.downloadExtensions({ prodversion });
|
|
1151
|
+
}
|
|
1152
|
+
|
|
765
1153
|
if (!VALID_ACTIONS.has(action)) {
|
|
766
1154
|
throw new Error(`未知 plugin 动作: ${action}`);
|
|
767
1155
|
}
|
|
@@ -772,10 +1160,14 @@ class PlaywrightPlugin {
|
|
|
772
1160
|
return 1;
|
|
773
1161
|
}
|
|
774
1162
|
|
|
1163
|
+
const extensionPaths = action === 'up'
|
|
1164
|
+
? this.resolveExtensionPaths(extensions)
|
|
1165
|
+
: [];
|
|
1166
|
+
|
|
775
1167
|
let rc = 0;
|
|
776
1168
|
for (const sceneName of targets) {
|
|
777
1169
|
// eslint-disable-next-line no-await-in-loop
|
|
778
|
-
const code = await this.runOnScene(action, sceneName);
|
|
1170
|
+
const code = await this.runOnScene(action, sceneName, { extensionPaths });
|
|
779
1171
|
if (code !== 0) {
|
|
780
1172
|
rc = 1;
|
|
781
1173
|
}
|
|
@@ -786,6 +1178,7 @@ class PlaywrightPlugin {
|
|
|
786
1178
|
}
|
|
787
1179
|
|
|
788
1180
|
module.exports = {
|
|
1181
|
+
EXTENSIONS,
|
|
789
1182
|
SCENE_ORDER,
|
|
790
1183
|
SCENE_DEFS,
|
|
791
1184
|
PlaywrightPlugin
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|