@xcanwin/manyoyo 8.6.3 → 8.6.5
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/app-update.js +29 -27
- 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/app-update.js
CHANGED
|
@@ -11,7 +11,7 @@ const { spawnSync } = require('child_process');
|
|
|
11
11
|
const { downloadVerified, sha256File } = require('./download-verified');
|
|
12
12
|
|
|
13
13
|
const REPO = 'xcanwin/manyoyo';
|
|
14
|
-
const
|
|
14
|
+
const WEB_BASE = 'https://github.com';
|
|
15
15
|
// 不依赖 PATH 查找 tar(macOS 与常见 Linux 都在 /usr/bin)
|
|
16
16
|
const TAR_COMMAND = fs.existsSync('/usr/bin/tar') ? '/usr/bin/tar' : 'tar';
|
|
17
17
|
const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
@@ -103,35 +103,32 @@ async function request(fetchImpl, url, options = {}) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
|
-
* 查询最新 Release
|
|
106
|
+
* 查询最新 Release。返回 { version, tag, assets: { name: url } }。
|
|
107
|
+
* 不调 GitHub API:匿名 API 按 IP 每小时 60 次,共享出口(公司网络、代理、CI runner)经常被限流。
|
|
108
|
+
* 改为跟随 github.com 的 releases/latest 跳转取版本(与 scripts/install.sh 一致),
|
|
109
|
+
* 升级包与 SHA256SUMS 的命名是固定的,下载地址直接拼在本仓库的 Release 下载路径下。
|
|
107
110
|
*/
|
|
108
|
-
async function fetchLatestRelease({ fetchImpl = fetch,
|
|
109
|
-
const response = await request(fetchImpl, `${
|
|
110
|
-
if (response.status ===
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
let release;
|
|
121
|
-
try {
|
|
122
|
-
release = await response.json();
|
|
123
|
-
} catch (error) {
|
|
124
|
-
throw new UpdateError('HTTP', '查询最新版本失败(返回内容不是有效的 JSON)。');
|
|
125
|
-
}
|
|
126
|
-
const version = String(release.tag_name || '').replace(/^v/, '');
|
|
127
|
-
if (!VERSION_PATTERN.test(version)) throw new UpdateError('HTTP', `最新 Release 的版本号无法识别: ${release.tag_name}`);
|
|
128
|
-
// 默认 API 下,下载地址必须在本仓库的 Release 下载路径内(测试注入自定义 apiBase 时不限制)
|
|
129
|
-
const allowedPrefix = apiBase === API_BASE ? `https://github.com/${REPO}/releases/download/` : '';
|
|
111
|
+
async function fetchLatestRelease({ fetchImpl = fetch, webBase = WEB_BASE } = {}) {
|
|
112
|
+
const response = await request(fetchImpl, `${webBase}/${REPO}/releases/latest`, { redirect: 'manual', headers: { Accept: 'text/html' } });
|
|
113
|
+
if (response.status === 429) throw new UpdateError('RATE_LIMIT', 'GitHub 对访问做了频率限制,请稍后重试。');
|
|
114
|
+
const location = response.status >= 300 && response.status < 400 ? String(response.headers.get('location') || '') : '';
|
|
115
|
+
if (!location && !response.ok && response.status !== 404) throw new UpdateError('HTTP', `查询最新版本失败(HTTP ${response.status})。`);
|
|
116
|
+
// 没有任何 Release 时 latest 跳到 Release 列表页(或 404)
|
|
117
|
+
const match = /\/releases\/tag\/([^/?#]+)$/.exec(location);
|
|
118
|
+
if (!match) throw new UpdateError('NO_RELEASE', '仓库还没有发布的 Release,暂时无法升级。');
|
|
119
|
+
const tag = decodeURIComponent(match[1]);
|
|
120
|
+
const version = tag.replace(/^v/, '');
|
|
121
|
+
if (!VERSION_PATTERN.test(version)) throw new UpdateError('HTTP', `最新 Release 的版本号无法识别: ${tag}`);
|
|
122
|
+
const download = `${webBase}/${REPO}/releases/download/${encodeURIComponent(tag)}/`;
|
|
130
123
|
const assets = {};
|
|
131
|
-
for (const
|
|
132
|
-
|
|
124
|
+
for (const targetOs of ['macos', 'linux']) {
|
|
125
|
+
for (const targetArch of ['arm64', 'x64']) {
|
|
126
|
+
const names = releaseAssetNames(version, targetArch, targetOs);
|
|
127
|
+
assets[names.app] = `${download}${names.app}`;
|
|
128
|
+
assets[names.sums] = `${download}${names.sums}`;
|
|
129
|
+
}
|
|
133
130
|
}
|
|
134
|
-
return { version, tag
|
|
131
|
+
return { version, tag, assets };
|
|
135
132
|
}
|
|
136
133
|
|
|
137
134
|
function parseSha256Sums(text) {
|
|
@@ -250,6 +247,11 @@ async function installAppUpdate({ appRoot, release, fetchImpl = fetch, targetArc
|
|
|
250
247
|
throw new UpdateError('NO_ASSET', `Release ${release.version} 里没有适合这台机器(${targetOs}-${targetArch})的升级包(需要 ${names.app} 与 ${names.sums})。`);
|
|
251
248
|
}
|
|
252
249
|
|
|
250
|
+
// 下载地址是按固定命名拼出来的:先确认这个 Release 里确实有本机的升级包(404 就是没有)
|
|
251
|
+
const probe = await request(fetchImpl, appUrl, { method: 'HEAD' });
|
|
252
|
+
if (probe.status === 404) {
|
|
253
|
+
throw new UpdateError('NO_ASSET', `Release ${release.version} 里没有适合这台机器(${targetOs}-${targetArch})的升级包(需要 ${names.app} 与 ${names.sums})。`);
|
|
254
|
+
}
|
|
253
255
|
log(`校验清单: ${sumsUrl}`);
|
|
254
256
|
const sums = parseSha256Sums(await fetchText(fetchImpl, sumsUrl));
|
|
255
257
|
const expected = sums[names.app];
|
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
|
}
|