@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 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 API_BASE = 'https://api.github.com';
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(匿名 API)。返回 { version, tag, assets: { name: url } }。
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, apiBase = API_BASE, now = Date.now } = {}) {
109
- const response = await request(fetchImpl, `${apiBase}/repos/${REPO}/releases/latest`);
110
- if (response.status === 403 || response.status === 429) {
111
- const remaining = response.headers.get('x-ratelimit-remaining');
112
- if (response.status === 429 || remaining === '0') {
113
- const reset = Number(response.headers.get('x-ratelimit-reset'));
114
- const when = reset > 0 ? new Date(reset * 1000).toLocaleTimeString('zh-CN', { hour12: false }) : '';
115
- throw new UpdateError('RATE_LIMIT', `GitHub 对匿名请求做了频率限制,${when ? `请在 ${when} 之后重试` : '请稍后重试'}。`);
116
- }
117
- }
118
- if (response.status === 404) throw new UpdateError('NO_RELEASE', '仓库还没有发布的 Release,暂时无法升级。');
119
- if (!response.ok) throw new UpdateError('HTTP', `查询最新版本失败(HTTP ${response.status})。`);
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 asset of Array.isArray(release.assets) ? release.assets : []) {
132
- if (asset && typeof asset.name === 'string' && typeof asset.browser_download_url === 'string' && asset.browser_download_url.startsWith(allowedPrefix)) assets[asset.name] = asset.browser_download_url;
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: release.tag_name, assets };
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];
@@ -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
  }