@xcanwin/manyoyo 8.6.4 → 8.6.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/bin/manyoyo.js +1 -0
- package/lib/download-verified.js +30 -7
- package/lib/plugin/playwright.js +54 -1
- package/lib/web/index.html +88 -88
- package/package.json +1 -1
package/bin/manyoyo.js
CHANGED
|
@@ -1395,6 +1395,7 @@ https://github.com/xcanwin/manyoyo
|
|
|
1395
1395
|
[
|
|
1396
1396
|
['down', '停止当前模式,回到默认模式'],
|
|
1397
1397
|
['status', '查看当前模式并真实探测浏览器是否可用(不可用时退出码非 0)'],
|
|
1398
|
+
['probe', '真实探测当前模式(不切换模式、失败也不回退;默认模式会起一次性容器;失败退出码非 0)'],
|
|
1398
1399
|
['logs', '查看当前模式的浏览器服务日志'],
|
|
1399
1400
|
['mcp-add', '输出在容器内注册 Playwright MCP(stdio,随当前模式自动切换)的命令']
|
|
1400
1401
|
].forEach(([action, description]) => {
|
package/lib/download-verified.js
CHANGED
|
@@ -16,11 +16,16 @@ function sha256File(filePath) {
|
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
const RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
|
|
20
|
+
|
|
21
|
+
class RetryableError extends Error {}
|
|
22
|
+
|
|
19
23
|
/**
|
|
20
24
|
* 下载并校验 SHA256。缓存命中(文件存在且哈希一致)直接复用;
|
|
21
25
|
* 哈希不符一律删除并报错,绝不保留不可信文件。
|
|
26
|
+
* 5xx / 429 / 网络错误(含下载中途断开)按 1s、2s、4s 退避,最多重试 retries 次;4xx 与哈希不符不重试。
|
|
22
27
|
*/
|
|
23
|
-
async function downloadVerified({ url, sha256, dest, fetchImpl = fetch, log = () => {} }) {
|
|
28
|
+
async function downloadVerified({ url, sha256, dest, fetchImpl = fetch, log = () => {}, retries = 3, retryDelayMs = 1000 }) {
|
|
24
29
|
if (!/^[0-9a-f]{64}$/.test(sha256 || '')) {
|
|
25
30
|
throw new Error(`缺少有效的 SHA256: ${url}`);
|
|
26
31
|
}
|
|
@@ -29,13 +34,20 @@ async function downloadVerified({ url, sha256, dest, fetchImpl = fetch, log = ()
|
|
|
29
34
|
log(`缓存命中: ${path.basename(dest)}`);
|
|
30
35
|
return dest;
|
|
31
36
|
}
|
|
32
|
-
log(`下载: ${url}`);
|
|
33
|
-
const response = await fetchImpl(url, { redirect: 'follow' });
|
|
34
|
-
if (!response.ok || !response.body) {
|
|
35
|
-
throw new Error(`下载失败 (${response.status}): ${url}`);
|
|
36
|
-
}
|
|
37
37
|
const partial = `${dest}.partial`;
|
|
38
|
-
|
|
38
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
39
|
+
try {
|
|
40
|
+
await fetchOnce({ url, partial, fetchImpl, log });
|
|
41
|
+
break;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
fs.rmSync(partial, { force: true });
|
|
44
|
+
const retryable = error instanceof RetryableError || !(error && error.status);
|
|
45
|
+
if (!retryable || attempt >= retries) throw error instanceof RetryableError ? error.cause : error;
|
|
46
|
+
const wait = retryDelayMs * (2 ** attempt);
|
|
47
|
+
log(`下载失败,${wait / 1000} 秒后重试(${attempt + 1}/${retries}): ${error.message}`);
|
|
48
|
+
await new Promise(resolve => setTimeout(resolve, wait));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
39
51
|
const actual = await sha256File(partial);
|
|
40
52
|
if (actual !== sha256) {
|
|
41
53
|
fs.rmSync(partial, { force: true });
|
|
@@ -45,4 +57,15 @@ async function downloadVerified({ url, sha256, dest, fetchImpl = fetch, log = ()
|
|
|
45
57
|
return dest;
|
|
46
58
|
}
|
|
47
59
|
|
|
60
|
+
async function fetchOnce({ url, partial, fetchImpl, log }) {
|
|
61
|
+
log(`下载: ${url}`);
|
|
62
|
+
const response = await fetchImpl(url, { redirect: 'follow' });
|
|
63
|
+
if (!response.ok || !response.body) {
|
|
64
|
+
const error = Object.assign(new Error(`下载失败 (${response.status}): ${url}`), { status: response.status });
|
|
65
|
+
if (RETRYABLE_STATUS.has(response.status)) throw Object.assign(new RetryableError(error.message), { cause: error });
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(partial));
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
module.exports = { sha256File, downloadVerified };
|
package/lib/plugin/playwright.js
CHANGED
|
@@ -35,6 +35,15 @@ const SERVER_START_TIMEOUT_MS = 30000;
|
|
|
35
35
|
const PROBE_TIMEOUT_MS = 5000;
|
|
36
36
|
// chrome 首次连接要等用户在 Chrome 弹出的“要允许远程调试吗?”里点允许
|
|
37
37
|
const CHROME_APPROVAL_TIMEOUT_MS = 60000;
|
|
38
|
+
// 默认模式的探测:一次性容器里用 playwright-cli 打开 data: 页面,再读回标题(读到 manyoyo-ok 才算通过)
|
|
39
|
+
const CONTAINER_PROBE_TIMEOUT_MS = 180000;
|
|
40
|
+
const CONTAINER_PROBE_SCRIPT = [
|
|
41
|
+
'playwright-cli -s=manyoyo-probe open "data:text/html,<title>manyoyo-ok</title>" >/dev/null 2>&1',
|
|
42
|
+
'out=$(playwright-cli -s=manyoyo-probe eval document.title 2>&1)',
|
|
43
|
+
'playwright-cli -s=manyoyo-probe close >/dev/null 2>&1',
|
|
44
|
+
'echo "$out"',
|
|
45
|
+
'case "$out" in *manyoyo-ok*) exit 0;; *) exit 1;; esac'
|
|
46
|
+
].join('; ');
|
|
38
47
|
|
|
39
48
|
function asObject(value) {
|
|
40
49
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
@@ -399,10 +408,11 @@ class PlaywrightPlugin {
|
|
|
399
408
|
return this.runtime;
|
|
400
409
|
}
|
|
401
410
|
|
|
402
|
-
runtimeCall(args, { capture = true } = {}) {
|
|
411
|
+
runtimeCall(args, { capture = true, timeout } = {}) {
|
|
403
412
|
const runtime = this.resolveRuntime();
|
|
404
413
|
const result = spawnSync(runtime.command, args, {
|
|
405
414
|
encoding: 'utf8',
|
|
415
|
+
...(timeout ? { timeout } : {}),
|
|
406
416
|
env: { ...process.env, ...runtime.env },
|
|
407
417
|
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit'
|
|
408
418
|
});
|
|
@@ -899,6 +909,46 @@ class PlaywrightPlugin {
|
|
|
899
909
|
return 1;
|
|
900
910
|
}
|
|
901
911
|
|
|
912
|
+
// 真实探测当前模式(不切换模式、失败也不回退;发布流水线用它:status 在失败时会把模式回退成默认;默认模式会按需重建 current/ 里的配置文件)。
|
|
913
|
+
// 宿主机上的模式连上浏览器服务开页面;默认模式(容器内浏览器)起一个一次性容器,真的打开页面并读回标题
|
|
914
|
+
async probe() {
|
|
915
|
+
const state = this.readState();
|
|
916
|
+
if (state.mode !== 'default') {
|
|
917
|
+
const alive = await this.modeAlive(state);
|
|
918
|
+
const result = alive ? await this.probeState(state) : { ok: false, error: state.mode === 'vnc' ? 'vnc 容器没有在运行' : '浏览器服务进程已不在' };
|
|
919
|
+
if (result.ok) {
|
|
920
|
+
this.out(`真实探测: 通过(${state.mode},${result.ms}ms)`);
|
|
921
|
+
return 0;
|
|
922
|
+
}
|
|
923
|
+
this.err(`真实探测: 失败(${state.mode}:${result.error})`);
|
|
924
|
+
return 1;
|
|
925
|
+
}
|
|
926
|
+
let image;
|
|
927
|
+
try {
|
|
928
|
+
image = this.ensureBaseImage();
|
|
929
|
+
} catch (error) {
|
|
930
|
+
this.err(`真实探测: 失败(${error.message})`);
|
|
931
|
+
return 1;
|
|
932
|
+
}
|
|
933
|
+
const runtimeCommand = this.runtimeCommand();
|
|
934
|
+
const integration = await this.buildContainerIntegration({ runtimeCommand });
|
|
935
|
+
const name = `manyoyo-probe-${process.pid}`;
|
|
936
|
+
const args = ['run', '--rm', '--init', '--name', name, '--shm-size=1g', ...integration.extraArgs];
|
|
937
|
+
for (const volume of integration.volumes) args.push('--volume', volume);
|
|
938
|
+
for (const entry of integration.env) args.push('--env', entry);
|
|
939
|
+
args.push(image, 'sh', '-c', CONTAINER_PROBE_SCRIPT);
|
|
940
|
+
const started = Date.now();
|
|
941
|
+
const result = this.runtimeCall(args, { timeout: CONTAINER_PROBE_TIMEOUT_MS });
|
|
942
|
+
if (result.status !== 0) this.runtimeCall(['rm', '-f', name]); // 超时时容器客户端被杀,容器本身可能还在
|
|
943
|
+
if (result.status === 0 && /manyoyo-ok/.test(result.stdout)) {
|
|
944
|
+
this.out(`真实探测: 通过(default,${Date.now() - started}ms)`);
|
|
945
|
+
return 0;
|
|
946
|
+
}
|
|
947
|
+
const reason = `${result.stdout}${result.stderr}`.trim().split('\n').filter(Boolean).slice(-1)[0] || `退出码 ${result.status}`;
|
|
948
|
+
this.err(`真实探测: 失败(default:容器里没能打开页面,${reason})`);
|
|
949
|
+
return 1;
|
|
950
|
+
}
|
|
951
|
+
|
|
902
952
|
logs() {
|
|
903
953
|
const state = this.readState();
|
|
904
954
|
if (state.mode === 'vnc') {
|
|
@@ -933,6 +983,9 @@ class PlaywrightPlugin {
|
|
|
933
983
|
if (action === 'status') {
|
|
934
984
|
return await this.status();
|
|
935
985
|
}
|
|
986
|
+
if (action === 'probe') {
|
|
987
|
+
return await this.probe();
|
|
988
|
+
}
|
|
936
989
|
if (action === 'logs') {
|
|
937
990
|
return this.logs();
|
|
938
991
|
}
|