Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/bds-official-version-source.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@sfmc-bds/bds-tools": patch
"@sfmc-bds/sdk": patch
---

BDS 更新检查改为优先使用 Mojang 官方下载接口获取最新版本,失败再回退到 Bedrock-OSS 版本列表(GitHub raw 与 jsDelivr 镜像)。修复 GitHub 不可达时只能依赖 jsDelivr 旧缓存,新版本发布初期检查结果时新时旧的问题。Bedrock-OSS 尚未收录新版本或不可达时,改用官方下载地址(跳过哈希校验)。自定义了 `version_versions` 或使用 endstone 模式时行为不变。同时修复 HTTP 请求与下载成功(及重定向、4xx)后未清除超时计时器,导致一次性命令在完成后仍空等至超时才退出的问题。
2 changes: 1 addition & 1 deletion modules/sdk/@sfmc-sdk/schemas/bds_updater.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
},
"version_versions": {
"type": "string",
"description": "版本列表 API URL"
"description": "版本列表 API URL(Bedrock-OSS 格式)。不填时先查 Mojang 官方下载接口,失败再用 Bedrock-OSS 列表;填写后只用此地址"
},
"version_versions_mirror": {
"type": "string",
Expand Down
14 changes: 13 additions & 1 deletion packages/bds-tools/src/check-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
isVersionCompatible,
verifyFileHash,
} from "./upstream.js";
import type { VersionDetails } from "./types.js";
import { compareVersions, getCurrentVersionAsync, getCurrentVersionSync, saveVersionCache } from "./version.js";
import { extractZipFileToDir } from "./zipx.js";

Expand Down Expand Up @@ -185,9 +186,12 @@ export async function runUpdate(): Promise<number> {

// 2. 获取最新版本
let latestVer: string;
/** 版本号来自 Mojang 官方下载接口时的官方下载地址;Bedrock-OSS 版本详情不可用时作为下载来源 */
let officialUrl = "";
try {
const info = await getVersionInfo(cfg, channel);
latestVer = info.version;
officialUrl = info.downloadUrl ?? "";
log.info(`最新版本: ${latestVer}`);
const cmp = compareVersions(latestVer, currentVer);
if (cmp <= 0 && !force) {
Expand Down Expand Up @@ -220,7 +224,15 @@ export async function runUpdate(): Promise<number> {
let downloadUrls: string[];
let hash: { sha1: string; sha256: string };
try {
const details = await fetchVersionDetails(cfg, channel, latestVer);
let details: VersionDetails;
try {
details = await fetchVersionDetails(cfg, channel, latestVer);
} catch (e) {
// 官方刚发布时 Bedrock-OSS 可能尚未收录该版本,或其源在当前网络不可达:改用官方下载地址,无哈希可校验
if (!officialUrl) throw e;
log.warn(`版本详情不可用: ${(e as Error).message},改用官方下载地址(跳过哈希校验)`);
details = { downloadUrl: officialUrl, sha1: "", sha256: "", size: 0 };
}
downloadUrls = buildDownloadUrls(cfg, channel, latestVer, details);
hash = { sha1: details.sha1, sha256: details.sha256 };
} catch (e) {
Expand Down
26 changes: 21 additions & 5 deletions packages/bds-tools/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export async function httpRequest(
const chunks: Buffer[] = [];
res.on("data", (c: Buffer) => chunks.push(c));
res.on("end", () => {
clearTimers();
const body = Buffer.concat(chunks);
const status = res.statusCode ?? 0;
// 3xx → 跟随 Location
Expand All @@ -87,11 +88,15 @@ export async function httpRequest(
const connectTimer = setTimeout(() => {
req.destroy(new Error(`HTTP 连接超时 ${connectTimeoutMs}ms: ${url}`));
}, connectTimeoutMs);
/** 清除本次请求的两个超时计时器;响应读完(含成功、重定向、4xx)与请求出错时调用,避免计时器让进程空等到超时才退出。 */
const clearTimers = (): void => {
clearTimeout(totalTimer);
clearTimeout(connectTimer);
};

req.on("socket", () => clearTimeout(connectTimer));
req.on("error", (err) => {
clearTimeout(totalTimer);
clearTimeout(connectTimer);
clearTimers();
reject(err);
});

Expand Down Expand Up @@ -204,6 +209,8 @@ export async function httpDownload(
(res) => {
const status = res.statusCode ?? 0;
if (status >= 300 && status < 400 && redirects < MAX_REDIRECTS) {
// 重定向后由新请求自带计时器,本请求的计时器需清除,否则旧计时器到期会销毁已结束的请求并误报错误
clearTimers();
const loc = res.headers.location;
if (!loc) return reject(new Error(`HTTP ${status} 缺少 Location`));
redirects++;
Expand All @@ -214,7 +221,10 @@ export async function httpDownload(
.catch(reject);
return;
}
if (status >= 400) return reject(new Error(`HTTP ${status} for ${url}`));
if (status >= 400) {
clearTimers();
return reject(new Error(`HTTP ${status} for ${url}`));
}

const total = parseInt(String(res.headers["content-length"] ?? 0), 10) || 0;
const file = createWriteStream(destPath);
Expand All @@ -228,6 +238,7 @@ export async function httpDownload(
const handleError = (e: Error): void => {
if (failed) return;
failed = true;
clearTimers();
file.close();
// 清理 partial 文件
import("node:fs").then((fs) => {
Expand Down Expand Up @@ -259,6 +270,7 @@ export async function httpDownload(
// 不能直接监听 file 'finish' 因为我们在错误时手动 handleError
file.on("finish", () => {
if (failed) return;
clearTimers();
try {
const finalBytes = statSync(destPath).size;
// 收尾:确保最后一次回调让进度条走到 100%(即便最近
Expand All @@ -281,11 +293,15 @@ export async function httpDownload(
const connectTimer = setTimeout(() => {
req.destroy(new Error(`下载连接超时 ${connectTimeoutMs}ms: ${url}`));
}, connectTimeoutMs);
/** 清除本次下载的两个超时计时器;下载完成、重定向、4xx 与任一出错路径调用,避免计时器在下载结束后继续存活。 */
const clearTimers = (): void => {
clearTimeout(totalTimer);
clearTimeout(connectTimer);
};

req.on("socket", () => clearTimeout(connectTimer));
req.on("error", (e) => {
clearTimeout(totalTimer);
clearTimeout(connectTimer);
clearTimers();
log.warn(`HTTP 错误 ${url}: ${(e as Error).message}`);
reject(e);
});
Expand Down
2 changes: 2 additions & 0 deletions packages/bds-tools/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface BdsUpdaterConfig {
export interface VersionInfo {
version: string;
cdnRoot: string;
/** 版本号取自 Mojang 官方下载接口时,该版本的官方下载地址;Bedrock-OSS 版本列表来源时为空 */
downloadUrl?: string;
}

export interface VersionDetails {
Expand Down
69 changes: 67 additions & 2 deletions packages/bds-tools/src/upstream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,70 @@
import crypto from "node:crypto";
import fs from "node:fs";
import { bdsCdnZipSuffix, bdsDetailsPlatformKey, bdsHostOs, type BdsHostOs } from "./host-platform.js";
import { fetchJsonWithFallback } from "./http.js";
import { fetchJsonWithFallback, httpGetJson } from "./http.js";
import { log } from "./log.js";
import type { BdsUpdaterConfig, VersionDetails, VersionInfo } from "./types.js";
import { toVer3 } from "./version.js";

const DEFAULT_VERSIONS_API = "https://raw.githubusercontent.com/Bedrock-OSS/BDS-Versions/main/versions.json";
const DEFAULT_VERSIONS_MIRROR = "https://cdn.jsdelivr.net/gh/Bedrock-OSS/BDS-Versions@main/versions.json";

/**
* Mojang 官方下载链接接口:返回各平台当前正式版 / 预览版 BDS 的下载地址(文件名带版本号)。
* 使用场景:getVersionInfo 的首选版本源。GitHub raw 在部分网络下不可达,jsDelivr 镜像在新版本发布初期
* 会返回旧缓存,导致检查结果时新时旧;官方接口无此问题。
*/
const OFFICIAL_LINKS_API = "https://net-secondary.web.minecraft-services.net/api/v1.0/download/links";
const OFFICIAL_LINKS_TIMEOUT_MS = 15_000;

/** 官方接口解析结果:当前通道与平台的 BDS 版本号及其官方下载地址。 */
export interface OfficialServerLink {
version: string;
downloadUrl: string;
}

/**
* 官方接口中当前通道与平台对应的 downloadType,例如 serverBedrockWindows、serverBedrockPreviewLinux。
* 使用场景:parseOfficialServerLink 从 links 列表中挑选条目。
*/
function officialDownloadType(channel: string, hostOs: BdsHostOs): string {
const preview = channel === "preview" ? "Preview" : "";
const os = hostOs === "windows" ? "Windows" : "Linux";
return `serverBedrock${preview}${os}`;
}

/**
* 从官方接口响应中解析当前通道与平台的 BDS 版本号与下载地址;结构不符或地址中没有版本号时返回 null。
* 使用场景:fetchOfficialServerLink;单独导出便于离线校验解析规则。
*/
export function parseOfficialServerLink(json: unknown, channel: string, hostOs: BdsHostOs): OfficialServerLink | null {
const links = (json as { result?: { links?: unknown } } | null)?.result?.links;
if (!Array.isArray(links)) return null;
const type = officialDownloadType(channel, hostOs);
const entry = links.find((l: { downloadType?: unknown }) => l?.downloadType === type) as
| { downloadUrl?: unknown }
| undefined;
const url = typeof entry?.downloadUrl === "string" ? entry.downloadUrl : "";
const m = /bedrock-server-(\d+(?:\.\d+){2,3})(?:-preview)?\.zip$/i.exec(url);
return m ? { version: m[1]!, downloadUrl: url } : null;
}

/**
* 请求官方接口并解析当前通道的 BDS 版本;请求失败或解析不到时记录警告并返回 null,由调用方回退到 Bedrock-OSS 版本列表。
* 使用场景:getVersionInfo 在 bedrock-oss 模式且未自定义版本列表地址时优先调用。
*/
async function fetchOfficialServerLink(channel: string, hostOs: BdsHostOs): Promise<OfficialServerLink | null> {
try {
const json = await httpGetJson<unknown>(OFFICIAL_LINKS_API, { totalTimeoutMs: OFFICIAL_LINKS_TIMEOUT_MS });
const link = parseOfficialServerLink(json, channel, hostOs);
if (!link) log.warn("官方下载接口中没有当前通道的 BDS 下载地址,改用 Bedrock-OSS 版本列表");
return link;
} catch (e) {
log.warn(`官方下载接口不可用: ${(e as Error).message},改用 Bedrock-OSS 版本列表`);
return null;
}
}

function resolveTemplate(tpl: string, vars: Record<string, string>): string {
let s = tpl;
for (const [k, v] of Object.entries(vars)) {
Expand All @@ -29,6 +85,8 @@ function resolveTemplate(tpl: string, vars: Record<string, string>): string {

/**
* 获取 BDS 最新可用版本信息(支持重试与指数退避)。
* bedrock-oss 模式且未自定义版本列表地址(version_versions / BDS_VERSIONS_API)时,先查 Mojang 官方下载接口,
* 失败再用 Bedrock-OSS 版本列表(GitHub raw 与 jsDelivr 镜像)。
*
* @param cfg BDS 更新器配置。
* @param channel 更新通道(如 "release" 或 "preview")。
Expand All @@ -41,11 +99,18 @@ export async function getVersionInfo(
channel: string,
hostOs: BdsHostOs = bdsHostOs()
): Promise<VersionInfo> {
const api = cfg.version_versions || process.env["BDS_VERSIONS_API"] || DEFAULT_VERSIONS_API;
const customApi = cfg.version_versions || process.env["BDS_VERSIONS_API"];
const api = customApi || DEFAULT_VERSIONS_API;
const mirror = cfg.version_versions_mirror || process.env["BDS_VERSIONS_MIRROR"] || DEFAULT_VERSIONS_MIRROR;
const sources = [api, mirror].filter(Boolean);
const versionMode = cfg.version_mode || "bedrock-oss";

// 自定义了版本列表地址或使用 endstone 时尊重原配置,不查官方接口
if (versionMode === "bedrock-oss" && !customApi) {
const official = await fetchOfficialServerLink(channel, hostOs);
if (official) return { version: official.version, cdnRoot: "", downloadUrl: official.downloadUrl };
}

let lastErr: unknown;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
Expand Down
Loading