@agile-team/robot-cli 3.0.0 → 3.0.2
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/CHANGELOG.md +22 -0
- package/dist/index.js +107 -41
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [3.0.2] - 2026-03-29
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **根治 ZIP 下载内容校验问题** — 新增两层防护:
|
|
13
|
+
1. `Content-Type` 检测: 若服务器返回 `text/html`(登录页/CAPTCHA/跳转页)立即报错,`tryDownload` 自动切换到下一个源(codeload CDN → GitHub API → github.com)
|
|
14
|
+
2. ZIP 魔术字节校验 (`PK 50 4B`): 写入磁盘前检查 buffer 头部,若不是合法 ZIP 抛出含内容预览的可读错误,而非让 `extract-zip` 报 `end of central directory record signature not found`
|
|
15
|
+
|
|
16
|
+
## [3.0.1] - 2026-03-28
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **下载策略全面重写** — 参考 giget (unjs/giget, 306k+ 项目使用) 的下载方式:
|
|
21
|
+
- Gitee 备用源提升为首选 (国内用户最快)
|
|
22
|
+
- 使用 `codeload.github.com` 直连 CDN 替代 `github.com/archive/` (跳过 302 重定向)
|
|
23
|
+
- 使用 `api.github.com` REST API 作为二级备选 (与 giget 一致)
|
|
24
|
+
- `github.com` 原始链接降为最后兜底
|
|
25
|
+
- **移除不稳定第三方镜像** — 移除 `hub.gitmirror.com` 和 `ghproxy.net` (不可控第三方代理)
|
|
26
|
+
- **超时优化** — 快速源 (Gitee/codeload) 30s, 慢速源 (API/github.com) 60s, 不再需要 120s
|
|
27
|
+
- **重试缩减** — 每源 2 次重试 (原 3 次), 退避时间 1s/2s (原 2s/4s), 快速失败切换下一源
|
|
28
|
+
- **错误信息增强** — 失败时列出每个源的具体错误原因
|
|
29
|
+
|
|
8
30
|
## [3.0.0] - 2026-03-28
|
|
9
31
|
|
|
10
32
|
### ⚠️ BREAKING CHANGES
|
package/dist/index.js
CHANGED
|
@@ -266,12 +266,29 @@ var START_COMMAND_MAP = {
|
|
|
266
266
|
// src/download.ts
|
|
267
267
|
var CACHE_DIR = path.join(os.homedir(), ".robot-cli", CACHE_DIR_NAME);
|
|
268
268
|
var CACHE_INDEX_PATH = path.join(CACHE_DIR, "index.json");
|
|
269
|
+
function parseGitHubRepo(repoUrl) {
|
|
270
|
+
try {
|
|
271
|
+
const url = new URL(repoUrl);
|
|
272
|
+
if (url.hostname !== "github.com") return null;
|
|
273
|
+
const parts = url.pathname.replace(/^\/|\/$/g, "").split("/");
|
|
274
|
+
if (parts.length >= 2) return { owner: parts[0], repo: parts[1] };
|
|
275
|
+
return null;
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
269
280
|
function buildDownloadUrl(repoUrl, branch = "main") {
|
|
270
281
|
try {
|
|
271
282
|
const url = new URL(repoUrl);
|
|
272
283
|
const host = url.hostname;
|
|
273
284
|
const cleanUrl = repoUrl.replace(/\/+$/, "");
|
|
274
|
-
if (host === "github.com")
|
|
285
|
+
if (host === "github.com") {
|
|
286
|
+
const gh = parseGitHubRepo(cleanUrl);
|
|
287
|
+
if (gh) return `https://codeload.github.com/${gh.owner}/${gh.repo}/zip/refs/heads/${branch}`;
|
|
288
|
+
return `${cleanUrl}/archive/refs/heads/${branch}.zip`;
|
|
289
|
+
}
|
|
290
|
+
if (host === "codeload.github.com") return `${cleanUrl}/zip/refs/heads/${branch}`;
|
|
291
|
+
if (host === "api.github.com") return cleanUrl;
|
|
275
292
|
if (host === "gitee.com")
|
|
276
293
|
return `${cleanUrl}/repository/archive/${branch}.zip`;
|
|
277
294
|
if (host === "gitlab.com") {
|
|
@@ -343,76 +360,123 @@ async function getCacheStats() {
|
|
|
343
360
|
async function clearCache() {
|
|
344
361
|
await fs.remove(CACHE_DIR);
|
|
345
362
|
}
|
|
346
|
-
var
|
|
347
|
-
var
|
|
348
|
-
var MAX_RETRIES =
|
|
349
|
-
function
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
363
|
+
var TIMEOUT_FAST = 3e4;
|
|
364
|
+
var TIMEOUT_SLOW = 6e4;
|
|
365
|
+
var MAX_RETRIES = 2;
|
|
366
|
+
function buildDownloadSources(repoUrl, branch, giteeUrl) {
|
|
367
|
+
const sources = [];
|
|
368
|
+
const gh = parseGitHubRepo(repoUrl);
|
|
369
|
+
if (giteeUrl) {
|
|
370
|
+
sources.push({
|
|
371
|
+
url: giteeUrl,
|
|
372
|
+
name: "gitee.com",
|
|
373
|
+
timeout: TIMEOUT_FAST
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
if (gh) {
|
|
377
|
+
sources.push({
|
|
378
|
+
url: `https://codeload.github.com/${gh.owner}/${gh.repo}/zip/refs/heads/${branch}`,
|
|
379
|
+
name: "codeload.github.com",
|
|
380
|
+
timeout: TIMEOUT_FAST,
|
|
381
|
+
isDirect: true
|
|
382
|
+
});
|
|
383
|
+
sources.push({
|
|
384
|
+
url: `https://api.github.com/repos/${gh.owner}/${gh.repo}/zipball/${branch}`,
|
|
385
|
+
name: "api.github.com",
|
|
386
|
+
timeout: TIMEOUT_SLOW,
|
|
387
|
+
isDirect: true,
|
|
388
|
+
headers: {
|
|
389
|
+
Accept: "application/vnd.github+json",
|
|
390
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
sources.push({
|
|
394
|
+
url: repoUrl,
|
|
395
|
+
name: "github.com",
|
|
396
|
+
timeout: TIMEOUT_SLOW
|
|
397
|
+
});
|
|
398
|
+
} else {
|
|
399
|
+
sources.push({
|
|
400
|
+
url: repoUrl,
|
|
401
|
+
name: new URL(repoUrl).hostname,
|
|
402
|
+
timeout: TIMEOUT_SLOW
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
return sources;
|
|
360
406
|
}
|
|
361
|
-
async function fetchWithRetry(downloadUrl, timeout, retries) {
|
|
407
|
+
async function fetchWithRetry(downloadUrl, timeout, retries, extraHeaders) {
|
|
362
408
|
let lastError;
|
|
363
409
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
364
410
|
try {
|
|
365
411
|
const response = await fetch(downloadUrl, {
|
|
366
412
|
signal: AbortSignal.timeout(timeout),
|
|
367
|
-
|
|
413
|
+
redirect: "follow",
|
|
414
|
+
headers: {
|
|
415
|
+
"User-Agent": "Robot-CLI/3.0",
|
|
416
|
+
...extraHeaders
|
|
417
|
+
}
|
|
368
418
|
});
|
|
369
419
|
if (!response.ok) {
|
|
370
420
|
if (response.status === 404) throw new Error(`\u4ED3\u5E93\u4E0D\u5B58\u5728 (404)`);
|
|
371
421
|
throw new Error(`HTTP ${response.status}`);
|
|
372
422
|
}
|
|
423
|
+
const ct = response.headers.get("content-type") ?? "";
|
|
424
|
+
if (ct.includes("text/html")) {
|
|
425
|
+
throw new Error(`\u6536\u5230 HTML \u54CD\u5E94\u800C\u975E ZIP \u6587\u4EF6\uFF08\u8BE5\u6E90\u53EF\u80FD\u9700\u8981\u8BA4\u8BC1\uFF09`);
|
|
426
|
+
}
|
|
373
427
|
return response;
|
|
374
428
|
} catch (error) {
|
|
375
429
|
lastError = error;
|
|
376
430
|
if (lastError.message.includes("404")) throw lastError;
|
|
377
431
|
if (attempt < retries) {
|
|
378
|
-
await new Promise((r) => setTimeout(r,
|
|
432
|
+
await new Promise((r) => setTimeout(r, 1e3 * attempt));
|
|
379
433
|
}
|
|
380
434
|
}
|
|
381
435
|
}
|
|
382
436
|
throw lastError;
|
|
383
437
|
}
|
|
384
438
|
async function tryDownload(repoUrl, branch = "main", spinner, giteeUrl) {
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const current = mirrors[i];
|
|
390
|
-
const isOriginal = i === 0;
|
|
391
|
-
let sourceName;
|
|
392
|
-
try {
|
|
393
|
-
sourceName = isOriginal ? host : new URL(current.replace(/^(https?:\/\/[^/]+)\/.*/, "$1")).hostname;
|
|
394
|
-
} catch {
|
|
395
|
-
sourceName = `\u955C\u50CF ${i}`;
|
|
396
|
-
}
|
|
439
|
+
const sources = buildDownloadSources(repoUrl, branch, giteeUrl);
|
|
440
|
+
const errors = [];
|
|
441
|
+
for (let i = 0; i < sources.length; i++) {
|
|
442
|
+
const source = sources[i];
|
|
397
443
|
try {
|
|
398
|
-
if (spinner) spinner.text = `\u8FDE\u63A5 ${
|
|
399
|
-
const downloadUrl =
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
444
|
+
if (spinner) spinner.text = `\u8FDE\u63A5 ${source.name} ...`;
|
|
445
|
+
const downloadUrl = source.isDirect ? source.url : buildDownloadUrl(source.url, branch);
|
|
446
|
+
if (spinner) spinner.text = `\u4ECE ${source.name} \u4E0B\u8F7D\u6A21\u677F...`;
|
|
447
|
+
const response = await fetchWithRetry(
|
|
448
|
+
downloadUrl,
|
|
449
|
+
source.timeout,
|
|
450
|
+
MAX_RETRIES,
|
|
451
|
+
source.headers
|
|
452
|
+
);
|
|
403
453
|
if (spinner) {
|
|
404
454
|
const len = response.headers.get("content-length");
|
|
405
455
|
const sizeInfo = len ? `${(parseInt(len) / 1024 / 1024).toFixed(1)}MB ` : "";
|
|
406
|
-
spinner.text = `\u4E0B\u8F7D\u4E2D ${sizeInfo}(${
|
|
456
|
+
spinner.text = `\u4E0B\u8F7D\u4E2D ${sizeInfo}(${source.name})`;
|
|
407
457
|
}
|
|
408
|
-
return { response, sourceName };
|
|
458
|
+
return { response, sourceName: source.name };
|
|
409
459
|
} catch (error) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
460
|
+
const errMsg = error.message || String(error);
|
|
461
|
+
errors.push(`${source.name}: ${errMsg}`);
|
|
462
|
+
if (i < sources.length - 1 && spinner) {
|
|
463
|
+
spinner.text = `${source.name} \u4E0D\u53EF\u7528\uFF0C\u5207\u6362\u5230 ${sources[i + 1].name}...`;
|
|
464
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
465
|
+
}
|
|
413
466
|
}
|
|
414
467
|
}
|
|
415
|
-
throw new Error(
|
|
468
|
+
throw new Error(
|
|
469
|
+
`\u6240\u6709\u4E0B\u8F7D\u6E90\u5747\u4E0D\u53EF\u7528:
|
|
470
|
+
${errors.map((e) => ` - ${e}`).join("\n")}`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
function assertZipBuffer(buffer, sourceName) {
|
|
474
|
+
if (buffer.length < 4 || buffer[0] !== 80 || buffer[1] !== 75) {
|
|
475
|
+
const preview = buffer.slice(0, 100).toString("utf8").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\xff]/g, "\xB7").replace(/\s+/g, " ").slice(0, 80);
|
|
476
|
+
throw new Error(
|
|
477
|
+
`${sourceName} \u8FD4\u56DE\u7684\u4E0D\u662F\u6709\u6548 ZIP \u6587\u4EF6 (\u5185\u5BB9: "${preview}")`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
416
480
|
}
|
|
417
481
|
async function downloadTemplate(template, options = {}) {
|
|
418
482
|
const { spinner, noCache, giteeUrl: optGiteeUrl } = options;
|
|
@@ -452,9 +516,11 @@ async function downloadTemplate(template, options = {}) {
|
|
|
452
516
|
spinner.text = `\u4E0B\u8F7D\u4E2D [${bar}] ${pct}% ${sizeMB}MB/${totalMB}MB (${sourceName})`;
|
|
453
517
|
}
|
|
454
518
|
const buffer = Buffer.concat(chunks);
|
|
519
|
+
assertZipBuffer(buffer, sourceName);
|
|
455
520
|
await fs.writeFile(tempZip, buffer);
|
|
456
521
|
} else {
|
|
457
522
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
523
|
+
assertZipBuffer(buffer, sourceName);
|
|
458
524
|
await fs.writeFile(tempZip, buffer);
|
|
459
525
|
}
|
|
460
526
|
if (spinner) spinner.text = "\u89E3\u538B\u6A21\u677F\u6587\u4EF6...";
|