From fe180ad366aaca3238efa1125f2998cab64941ac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 11:18:15 +0000 Subject: [PATCH] =?UTF-8?q?fix(pack-update):=20=E9=A1=B6=E5=B1=82=20match?= =?UTF-8?q?=20+=20=E6=8C=89=20provider=20id=20=E5=88=86=E6=B4=BE=EF=BC=88?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5=20#61=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 合入 #61 后残余 SOLID 债: - match 从 providers.curseforge 提升到 PackUpdateConfig 顶层(OCP/DIP/Demeter),兼容旧嵌套字段 - createPackSourceProvider(cfg, id) switch 分派;prepareCheck 按 binding.provider(OCP/LSP) - CheckResult.archivePath 更名为 stagingDir;去掉无意义 getProvider 包装 - 移除已无 runtime 依赖的 @types/cli-progress Co-authored-by: Shiroha --- configs-default/pack-update.json | 12 +++--- docs/guide/pack-update.md | 14 +++---- package-lock.json | 11 ----- package.json | 1 - sfmc/src/pack-update/config.ts | 55 +++++++++++++++++++------ sfmc/src/pack-update/providers/index.ts | 19 ++++++--- sfmc/src/pack-update/service.ts | 34 +++++++-------- sfmc/src/pack-update/types.ts | 7 +++- 8 files changed, 92 insertions(+), 61 deletions(-) diff --git a/configs-default/pack-update.json b/configs-default/pack-update.json index 085ce0b9..13480143 100644 --- a/configs-default/pack-update.json +++ b/configs-default/pack-update.json @@ -1,10 +1,14 @@ { - "_comment": "世界包更新。apiKey 来自 https://console.curseforge.com/(x-api-key)。Bedrock gameId=78022,Addons classId=4984。官方 /mods/search 若 403 会自动改用 searchBaseUrl(api.curse.tools)。也可用环境变量 CURSEFORGE_API_KEY。", + "_comment": "世界包更新。apiKey 来自 https://console.curseforge.com/(x-api-key)。Bedrock gameId=78022,Addons classId=4984。官方 /mods/search 若 403 会自动改用 searchBaseUrl(api.curse.tools)。也可用环境变量 CURSEFORGE_API_KEY。match 为源无关策略,置于顶层。", "enabled": true, "checkOnBdsStart": true, "applyOnBdsStart": true, "askConfirmOnBind": true, "probeSourceAfterInstall": true, + "match": { + "nameMinScore": 0.6, + "stripFolderTags": true + }, "providers": { "curseforge": { "enabled": true, @@ -14,11 +18,7 @@ "gameId": 78022, "classId": 4984, "pageSize": 10, - "preferredReleaseTypes": ["release", "beta", "alpha"], - "match": { - "nameMinScore": 0.6, - "stripFolderTags": true - } + "preferredReleaseTypes": ["release", "beta", "alpha"] } }, "versionPolicy": { diff --git a/docs/guide/pack-update.md b/docs/guide/pack-update.md index 5604aa5c..0b2e675d 100644 --- a/docs/guide/pack-update.md +++ b/docs/guide/pack-update.md @@ -88,6 +88,10 @@ flowchart TD "applyOnBdsStart": true, "askConfirmOnBind": true, "probeSourceAfterInstall": true, + "match": { + "nameMinScore": 0.6, + "stripFolderTags": true + }, "providers": { "curseforge": { "enabled": true, @@ -97,11 +101,7 @@ flowchart TD "gameId": 78022, "classId": 4984, "pageSize": 10, - "preferredReleaseTypes": ["release", "beta", "alpha"], - "match": { - "nameMinScore": 0.6, - "stripFolderTags": true - } + "preferredReleaseTypes": ["release", "beta", "alpha"] } }, "versionPolicy": { @@ -122,12 +122,12 @@ flowchart TD | 字段 | 含义 | |------|------| +| `match.nameMinScore` | 安装后自动绑定的最低相似度阈值(源无关,顶层)。 | +| `match.stripFolderTags` | 清洗时是否去掉方括号标签(如 `[BP]`/`[玩法]`)。 | | `gameId` | **Minecraft Bedrock = `78022`**。历史误用 `459` 无效,加载时会纠正。Java Minecraft 是 `432`,不要混用。 | | `classId` | Bedrock **Addons = `4984`**;`null` 时用 `/v1/categories?classesOnly=true` 解析「Addons」。 | | `baseUrl` | 官方 Core API:getMod / files / download-url。 | | `searchBaseUrl` | 搜索镜像;官方 search 403 时回退。 | -| `nameMinScore` | 安装后自动绑定的最低相似度阈值。 | -| `stripFolderTags` | 清洗时是否去掉方括号标签(如 `[BP]`/`[玩法]`)。 | ### 3.4 绑定文件示例 diff --git a/package-lock.json b/package-lock.json index e0844fc5..da90ef9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,6 @@ "@minecraft/server-net": "1.0.0-beta.11940b24", "@minecraft/server-ui": "2.2.0-beta.1.26.40-preview.30", "@minecraft/vanilla-data": "1.26.40-preview.30", - "@types/cli-progress": "^3.11.6", "@types/lodash": "^4.17.24", "@types/node": "^26.1.1", "@types/ws": "^8.18.1", @@ -843,16 +842,6 @@ "resolved": "tools", "link": true }, - "node_modules/@types/cli-progress": { - "version": "3.11.6", - "resolved": "https://registry.npmmirror.com/@types/cli-progress/-/cli-progress-3.11.6.tgz", - "integrity": "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmmirror.com/@types/esrecurse/-/esrecurse-4.3.1.tgz", diff --git a/package.json b/package.json index 63bc1488..d342cc8d 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "@minecraft/server-net": "1.0.0-beta.11940b24", "@minecraft/server-ui": "2.2.0-beta.1.26.40-preview.30", "@minecraft/vanilla-data": "1.26.40-preview.30", - "@types/cli-progress": "^3.11.6", "@types/lodash": "^4.17.24", "@types/node": "^26.1.1", "@types/ws": "^8.18.1", diff --git a/sfmc/src/pack-update/config.ts b/sfmc/src/pack-update/config.ts index e9189b43..da0cbaee 100644 --- a/sfmc/src/pack-update/config.ts +++ b/sfmc/src/pack-update/config.ts @@ -4,7 +4,12 @@ import fs from "node:fs"; import path from "node:path"; import { ROOT, resolveDefaultsDir, seedMissingConfigsFromDefaults } from "../runtime.js"; -import type { PackUpdateConfig, PackUpdateMatchConfig } from "./types.js"; +import type { CurseForgeProviderConfig, PackUpdateConfig, PackUpdateMatchConfig } from "./types.js"; + +const DEFAULT_MATCH: PackUpdateMatchConfig = { + nameMinScore: 0.6, + stripFolderTags: true, +}; const DEFAULTS: PackUpdateConfig = { enabled: true, @@ -12,6 +17,7 @@ const DEFAULTS: PackUpdateConfig = { applyOnBdsStart: true, askConfirmOnBind: true, probeSourceAfterInstall: true, + match: { ...DEFAULT_MATCH }, providers: { curseforge: { enabled: true, @@ -22,10 +28,6 @@ const DEFAULTS: PackUpdateConfig = { classId: 4984, pageSize: 10, preferredReleaseTypes: ["release", "beta", "alpha"], - match: { - nameMinScore: 0.6, - stripFolderTags: true, - }, }, }, versionPolicy: { @@ -62,12 +64,34 @@ export function packUpdateConfigPath(): string { return path.join(ROOT, "configs", "pack-update.json"); } +/** + * 将旧版挂在 providers.curseforge.match 的字段提升到顶层 match(兼容一版)。 + * 顶层显式配置优先于嵌套遗留。 + */ +function hoistLegacyMatch(raw: Record): PackUpdateMatchConfig | undefined { + const top = raw.match; + const providers = raw.providers as Record | undefined; + const cf = providers?.curseforge as Record | undefined; + const nested = cf?.match; + const pick = + top && typeof top === "object" && !Array.isArray(top) + ? (top as Partial) + : nested && typeof nested === "object" && !Array.isArray(nested) + ? (nested as Partial) + : undefined; + if (!pick) return undefined; + return deepMerge( + DEFAULT_MATCH as unknown as Record, + pick as Record + ) as unknown as PackUpdateMatchConfig; +} + export function loadPackUpdateConfig(): PackUpdateConfig { const cfgPath = packUpdateConfigPath(); - let raw: Partial = {}; + let raw: Record = {}; if (fs.existsSync(cfgPath)) { try { - raw = JSON.parse(fs.readFileSync(cfgPath, "utf8")) as Partial; + raw = JSON.parse(fs.readFileSync(cfgPath, "utf8")) as Record; } catch { raw = {}; } @@ -76,34 +100,41 @@ export function loadPackUpdateConfig(): PackUpdateConfig { const bundled = defaultsDir ? path.join(defaultsDir, "pack-update.json") : ""; if (bundled && fs.existsSync(bundled)) { try { - raw = JSON.parse(fs.readFileSync(bundled, "utf8")) as Partial; + raw = JSON.parse(fs.readFileSync(bundled, "utf8")) as Record; } catch { raw = {}; } } } + const legacyMatch = hoistLegacyMatch(raw); const merged = deepMerge( DEFAULTS as unknown as Record, raw as Record ) as unknown as PackUpdateConfig; + if (legacyMatch) { + merged.match = legacyMatch; + } + const envKey = process.env.CURSEFORGE_API_KEY?.trim(); if (envKey) { merged.providers.curseforge.apiKey = envKey; } - /* 兼容旧配置里未接线的 byUuidInArchive/byName,避免 deepMerge 残留脏字段影响契约 */ - const match = merged.providers.curseforge.match as PackUpdateMatchConfig & Record; + /* 剥离曾挂在 CF 下的 match / 未接线死字段,避免脏配置渗入 Provider(Demeter/契约) */ + const cf = merged.providers.curseforge as CurseForgeProviderConfig & Record; + delete cf.match; + const match = merged.match as PackUpdateMatchConfig & Record; delete match.byUuidInArchive; delete match.byName; return merged; } -/** 匹配策略访问器:编排层勿直接挖 providers.curseforge.match(Demeter) */ +/** 匹配策略访问器:编排层只读顶层 match,勿挖 providers.*(Demeter) */ export function getPackMatchConfig(cfg: PackUpdateConfig): PackUpdateMatchConfig { - return cfg.providers.curseforge.match; + return cfg.match; } /** diff --git a/sfmc/src/pack-update/providers/index.ts b/sfmc/src/pack-update/providers/index.ts index 3764da4e..0f837ab9 100644 --- a/sfmc/src/pack-update/providers/index.ts +++ b/sfmc/src/pack-update/providers/index.ts @@ -1,12 +1,21 @@ /** - * 源提供者注册表:按配置构造 PackSourceProvider(OCP:新源加分支/注册,不改编排层)。 + * 源提供者注册表:按 id 构造 PackSourceProvider(OCP:新源加 case,不改编排层)。 */ -import type { PackSourceProvider, PackUpdateConfig } from "../types.js"; +import type { PackProviderId, PackSourceProvider, PackUpdateConfig } from "../types.js"; import { CurseForgeBedrockProvider } from "./curseforge.js"; -export function createPackSourceProvider(cfg: PackUpdateConfig): PackSourceProvider { - /* 当前仅 curseforge;后续源在此扩展,service 只依赖 PackSourceProvider */ - return new CurseForgeBedrockProvider(cfg.providers.curseforge); +export function createPackSourceProvider( + cfg: PackUpdateConfig, + id: PackProviderId = "curseforge" +): PackSourceProvider { + switch (id) { + case "curseforge": + return new CurseForgeBedrockProvider(cfg.providers.curseforge); + default: { + const _exhaustive: never = id; + throw new Error(`未知 pack 源提供者: ${String(_exhaustive)}`); + } + } } export { CurseForgeBedrockProvider }; diff --git a/sfmc/src/pack-update/service.ts b/sfmc/src/pack-update/service.ts index 370484de..c1e933dd 100644 --- a/sfmc/src/pack-update/service.ts +++ b/sfmc/src/pack-update/service.ts @@ -50,10 +50,6 @@ function logPack(text: string, level: "info" | "warn" | "error" | "success" = "i pushLog(text, "pack", level); } -function getProvider(cfg: PackUpdateConfig): PackSourceProvider { - return createPackSourceProvider(cfg); -} - function fmtVer(v: SemVer3): string { return v.join("."); } @@ -117,7 +113,7 @@ export async function probeSourceAfterInstall(opts: { if (opts.info.kind !== "behavior") return; ensurePackUpdateConfigFile(); - const provider = getProvider(cfg); + const provider = createPackSourceProvider(cfg, "curseforge"); if (!provider.isConfigured()) { logPack(t("packUpdate.needKey", { path: packUpdateConfigPath() }), "warn"); return; @@ -223,7 +219,7 @@ export async function probeSourceAfterInstall(opts: { export async function searchRemote(query: string): Promise { const cfg = loadPackUpdateConfig(); - const provider = getProvider(cfg); + const provider = createPackSourceProvider(cfg, "curseforge"); if (!provider.isConfigured()) { return c.yellow(t("packUpdate.needKey", { path: packUpdateConfigPath() })); } @@ -241,7 +237,7 @@ export async function searchRemote(query: string): Promise { export async function bindPackSource(packId: string, ref: string): Promise { const cfg = loadPackUpdateConfig(); - const provider = getProvider(cfg); + const provider = createPackSourceProvider(cfg, "curseforge"); if (!provider.isConfigured()) { return c.yellow(t("packUpdate.needKey", { path: packUpdateConfigPath() })); } @@ -297,7 +293,8 @@ interface CheckResult { remoteBpInfo?: PackManifestInfo; remoteRoots?: string[]; tempDir?: string; - archivePath?: string; + /** 下载归档所在的临时 staging 目录(非 zip 文件路径) */ + stagingDir?: string; } function withLocalBp(base: Omit, localBp: InstalledWorldPack | undefined): CheckResult { @@ -335,7 +332,7 @@ async function prepareCheck( const localBp = packs.find((p) => p.uuid.toLowerCase() === bpUuid.toLowerCase() && p.kind === "behavior"); const name = localBp?.name ?? bpUuid; const localVer: SemVer3 = localBp?.version ?? [0, 0, 0]; - const provider = getProvider(cfg); + const provider = createPackSourceProvider(cfg, binding.provider); binding.lastCheckedAt = new Date().toISOString(); setBinding(bpUuid, binding); @@ -370,16 +367,16 @@ async function prepareCheck( ); } - const staging = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-pack-upd-")); - const archivePath = path.join(staging, file.fileName || "pack.zip"); - await provider.download(file, archivePath); - const tempDir = await extractArchiveToTemp(archivePath); + const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-pack-upd-")); + const archiveFile = path.join(stagingDir, file.fileName || "pack.zip"); + await provider.download(file, archiveFile); + const tempDir = await extractArchiveToTemp(archiveFile); const roots = discoverPackRoots(tempDir, { maxDepth: 3 }); const bpRoot = roots.find((r) => readPackManifestInfo(r)?.kind === "behavior"); const remoteBpInfo = bpRoot ? readPackManifestInfo(bpRoot) : null; if (!remoteBpInfo) { try { - fs.rmSync(staging, { recursive: true, force: true }); + fs.rmSync(stagingDir, { recursive: true, force: true }); fs.rmSync(tempDir, { recursive: true, force: true }); } catch { /* ignore */ @@ -414,14 +411,14 @@ async function prepareCheck( remoteBpInfo, remoteRoots: roots, tempDir, - archivePath: staging, + stagingDir, }, localBp ); } function cleanupCheck(r: CheckResult): void { - for (const d of [r.tempDir, r.archivePath]) { + for (const d of [r.tempDir, r.stagingDir]) { if (!d) continue; try { fs.rmSync(d, { recursive: true, force: true }); @@ -543,8 +540,9 @@ async function applyUpdate(r: CheckResult, cfg: PackUpdateConfig): Promise { const cfg = loadPackUpdateConfig(); if (!cfg.enabled) return c.dim(t("packUpdate.disabled")); - const provider = getProvider(cfg); - if (!provider.isConfigured()) { + /* 入口预检:当前仅 curseforge;逐 binding 时仍按 binding.provider 分派(LSP) */ + const gate = createPackSourceProvider(cfg, "curseforge"); + if (!gate.isConfigured()) { return c.yellow(t("packUpdate.needKey", { path: packUpdateConfigPath() })); } diff --git a/sfmc/src/pack-update/types.ts b/sfmc/src/pack-update/types.ts index 59ecc269..ebc7e5d2 100644 --- a/sfmc/src/pack-update/types.ts +++ b/sfmc/src/pack-update/types.ts @@ -16,6 +16,7 @@ export interface PackUpdateMatchConfig { stripFolderTags: boolean; } +/** CF 源专属配置(不含通用 match;match 在 PackUpdateConfig 顶层) */ export interface CurseForgeProviderConfig { enabled: boolean; apiKey: string; @@ -32,7 +33,6 @@ export interface CurseForgeProviderConfig { classId: number | null; pageSize: number; preferredReleaseTypes: PackReleaseType[]; - match: PackUpdateMatchConfig; } export interface VersionPolicyConfig { @@ -56,6 +56,11 @@ export interface PackUpdateConfig { applyOnBdsStart: boolean; askConfirmOnBind: boolean; probeSourceAfterInstall: boolean; + /** + * 名称/slug 匹配策略(源无关)。 + * 勿再塞进 providers.*(曾挂在 curseforge 下,违反 DIP/OCP)。 + */ + match: PackUpdateMatchConfig; providers: { curseforge: CurseForgeProviderConfig; };