From 099913c0aeeb3cd0a73fa02b6a148fff3348333d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 12:02:29 +0000 Subject: [PATCH] =?UTF-8?q?fix(pack-update):=20=E4=BF=AE=E5=A4=8D=20defaul?= =?UTF-8?q?tBindingEnabled=20=E5=A5=91=E7=BA=A6=E5=B9=B6=E9=A1=B6=E5=B1=82?= =?UTF-8?q?=E5=8C=96=20match?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ootb@b52e487 在 cli 声明发出阶段失败:DEFAULTS 写了 defaultBindingEnabled 但 PackUpdateConfig 未声明(TS2353)。同时补齐绑定写入接线,并合入 审查 #62 的 match 顶层化与按 binding.provider 分派。 Co-authored-by: Shiroha --- configs-default/pack-update.json | 12 ++--- docs/guide/pack-update.md | 16 ++++--- package-lock.json | 18 ------- package.json | 1 - sfmc/src/pack-update/config.ts | 55 ++++++++++++++++----- sfmc/src/pack-update/providers/index.ts | 19 ++++++-- sfmc/src/pack-update/service.ts | 63 ++++++++++++++++--------- sfmc/src/pack-update/types.ts | 12 ++++- 8 files changed, 123 insertions(+), 73 deletions(-) diff --git a/configs-default/pack-update.json b/configs-default/pack-update.json index c8117ce6..c147536c 100644 --- a/configs-default/pack-update.json +++ b/configs-default/pack-update.json @@ -1,11 +1,15 @@ { - "_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。defaultBindingEnabled=false 时新建绑定默认关闭,需手改 enabled 或改此默认后才会自动检查/更新。", + "_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 为源无关策略,置于顶层。defaultBindingEnabled=false 时新建绑定默认关闭,需手改 enabled 或改此默认后才会自动检查/更新。", "enabled": true, "checkOnBdsStart": true, "applyOnBdsStart": true, "askConfirmOnBind": true, "probeSourceAfterInstall": true, "defaultBindingEnabled": false, + "match": { + "nameMinScore": 0.6, + "stripFolderTags": true + }, "providers": { "curseforge": { "enabled": true, @@ -15,11 +19,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..de605abe 100644 --- a/docs/guide/pack-update.md +++ b/docs/guide/pack-update.md @@ -88,6 +88,11 @@ flowchart TD "applyOnBdsStart": true, "askConfirmOnBind": true, "probeSourceAfterInstall": true, + "defaultBindingEnabled": false, + "match": { + "nameMinScore": 0.6, + "stripFolderTags": true + }, "providers": { "curseforge": { "enabled": true, @@ -97,11 +102,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 +123,13 @@ flowchart TD | 字段 | 含义 | |------|------| +| `defaultBindingEnabled` | 新建绑定的默认 `enabled`;`false` 时仍写入 `pack-sources.json`,但不会自动检查/更新。 | +| `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..106c6a15 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", @@ -2778,12 +2767,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "license": "MIT" }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "license": "MIT" - }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", @@ -3684,7 +3667,6 @@ "@sfmc-bds/bds-tools": "^0.1.0", "@sfmc-bds/sdk": "^0.1.0", "chalk": "^5.4.1", - "jsonc-parser": "^3.3.1", "ws": "^8.21.1" }, "bin": { diff --git a/package.json b/package.json index bda0b5e5..0beb0209 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 722386e0..c15d0e63 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, @@ -14,6 +19,7 @@ const DEFAULTS: PackUpdateConfig = { probeSourceAfterInstall: true, /* 探测绑定默认关闭,避免误更新;可在 pack-update.json 改为 true */ defaultBindingEnabled: false, + match: { ...DEFAULT_MATCH }, providers: { curseforge: { enabled: true, @@ -24,10 +30,6 @@ const DEFAULTS: PackUpdateConfig = { classId: 4984, pageSize: 10, preferredReleaseTypes: ["release", "beta", "alpha"], - match: { - nameMinScore: 0.6, - stripFolderTags: true, - }, }, }, versionPolicy: { @@ -64,12 +66,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 = {}; } @@ -78,34 +102,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..72b280d9 100644 --- a/sfmc/src/pack-update/service.ts +++ b/sfmc/src/pack-update/service.ts @@ -50,14 +50,14 @@ 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("."); } +function bindingEnabledLabel(enabled: boolean): string { + return enabled ? "on" : "off"; +} + function providerShortLabel(id: PackProviderId): string { return id === "curseforge" ? "cf" : id; } @@ -84,10 +84,12 @@ async function searchAndRankHits( function makeBindingFromHit( hit: SourceSearchHit, pairedResourceUuid: string | null, - prev?: PackSourceBinding | null + opts?: { prev?: PackSourceBinding | null; defaultEnabled: boolean } ): PackSourceBinding { + const prev = opts?.prev; return { - enabled: true, + /* 重绑保留原 enabled;新建走配置契约 defaultBindingEnabled(LSP) */ + enabled: prev?.enabled ?? opts?.defaultEnabled ?? false, provider: hit.provider, projectId: hit.projectId, slug: hit.slug, @@ -117,7 +119,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; @@ -210,11 +212,13 @@ export async function probeSourceAfterInstall(opts: { } const paired = (opts.packDir ? pairedRpUuidFromBpDir(opts.packDir) : null) ?? null; - setBinding(opts.info.uuid, makeBindingFromHit(best.hit, paired)); + const binding = makeBindingFromHit(best.hit, paired, { defaultEnabled: cfg.defaultBindingEnabled }); + setBinding(opts.info.uuid, binding); logPack( t("packUpdate.bindOk", { uuid: opts.info.uuid, slug: best.hit.slug, + enabled: bindingEnabledLabel(binding.enabled), path: packSourcesPath(), }), "success" @@ -223,7 +227,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 +245,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() })); } @@ -255,8 +259,19 @@ export async function bindPackSource(packId: string, ref: string): Promise, localBp: InstalledWorldPack | undefined): CheckResult { @@ -335,7 +351,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 +386,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 +430,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 +559,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..1d141d24 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,16 @@ export interface PackUpdateConfig { applyOnBdsStart: boolean; askConfirmOnBind: boolean; probeSourceAfterInstall: boolean; + /** + * 新建绑定的默认 enabled。 + * false 时探测/手动 bind 仍写入 pack-sources,但不会自动检查/更新,需手改或改此默认。 + */ + defaultBindingEnabled: boolean; + /** + * 名称/slug 匹配策略(源无关)。 + * 勿再塞进 providers.*(曾挂在 curseforge 下,违反 DIP/OCP)。 + */ + match: PackUpdateMatchConfig; providers: { curseforge: CurseForgeProviderConfig; };