From 1c51f8c46e71f2d36eca9f9310789d15b26ef08a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Jul 2026 17:15:02 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20BDS=20=E8=B7=AF=E5=BE=84=20helpers=20?= =?UTF-8?q?=E6=94=B6=E6=95=9B=20Demeter/DRY=EF=BC=88=E5=AE=A1=E6=9F=A5=20#?= =?UTF-8?q?57=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #57 已让 pack-lifecycle 直连 pack-manager-lib,但调用方仍硬拼 worlds/config/server.properties 布局。抽出 bdsWorldsDir / worldPackListFile / configPermissionPath 等权威入口, readLevelName 委托 Sync,删掉未使用的 listEnabled* 双轨。 Co-authored-by: Shiroha --- bds-tools/pack-manager-lifecycle.test.mjs | 31 ++++++++++ bds-tools/src/cli-pack-manager.ts | 10 ++-- bds-tools/src/pack-manager.ts | 68 +++++++++++++++------- bds-tools/src/server-properties.ts | 4 +- bds-tools/src/world-packs.ts | 16 ++--- sfmc/src/pack-lifecycle.ts | 71 +++++++---------------- sfmc/src/world-packs.ts | 5 +- 7 files changed, 116 insertions(+), 89 deletions(-) diff --git a/bds-tools/pack-manager-lifecycle.test.mjs b/bds-tools/pack-manager-lifecycle.test.mjs index 7ef5d484..cb232b19 100644 --- a/bds-tools/pack-manager-lifecycle.test.mjs +++ b/bds-tools/pack-manager-lifecycle.test.mjs @@ -156,6 +156,37 @@ describe("pack-manager CLI extensions", () => { assert.equal(await readLevelName(bds), "My World"); }); + it("BDS 路径 helpers 单一权威(Demeter/DRY)", async () => { + const { + bdsWorldsDir, + bdsWorldLevelDir, + worldPackListFile, + configPermissionPath, + hasConfigPermission, + serverPropertiesPath, + ensureConfigPermission, + } = await import("./dist/pack-manager.js"); + const bds = path.join(tmp, "bds-paths"); + fs.mkdirSync(bds, { recursive: true }); + assert.equal(bdsWorldsDir(bds), path.join(bds, "worlds")); + assert.equal(bdsWorldLevelDir(bds, "L1"), path.join(bds, "worlds", "L1")); + assert.equal( + worldPackListFile(bdsWorldsDir(bds), "L1", "behavior"), + path.join(bds, "worlds", "L1", "world_behavior_packs.json") + ); + assert.equal( + worldPackListFile(bdsWorldsDir(bds), "L1", "resource"), + path.join(bds, "worlds", "L1", "world_resource_packs.json") + ); + assert.equal(serverPropertiesPath(bds), path.join(bds, "server.properties")); + const uuid = "00000000-0000-4000-8000-000000000099"; + assert.equal(configPermissionPath(bds, uuid), path.join(bds, "config", uuid, "permission.json")); + assert.equal(hasConfigPermission(bds, uuid), false); + assert.equal(await ensureConfigPermission(bds, uuid), true); + assert.equal(hasConfigPermission(bds, uuid), true); + assert.equal(await ensureConfigPermission(bds, uuid), false); + }); + it("readWorldPackListResult 区分缺失与 JSON 损坏(doctor parseFail)", async () => { const { readWorldPackListResult } = await import("./dist/pack-manager.js"); const worldsDir = path.join(tmp, "worlds-parse"); diff --git a/bds-tools/src/cli-pack-manager.ts b/bds-tools/src/cli-pack-manager.ts index 2252e81f..48e69bbd 100644 --- a/bds-tools/src/cli-pack-manager.ts +++ b/bds-tools/src/cli-pack-manager.ts @@ -132,7 +132,7 @@ async function main(): Promise { ...(rpName ? { rpName } : {}), ...(clearRp ? { clearResourcePack: true } : {}), }); - process.stdout.write(`[pack-manager] deployed to ${path.join(bdsRoot, "worlds", level)}\n`); + process.stdout.write(`[pack-manager] deployed to ${mod.bdsWorldLevelDir(path.resolve(bdsRoot), level)}\n`); return; } case "enable-pack": { @@ -168,11 +168,11 @@ async function main(): Promise { case "ensure-permission": { const bdsRoot = need(args, "bds-root"); const packId = need(args, "pack-id"); - const wrote = await mod.ensureConfigPermission(path.resolve(bdsRoot), packId); + const root = path.resolve(bdsRoot); + const wrote = await mod.ensureConfigPermission(root, packId); + const rel = path.relative(root, mod.configPermissionPath(root, packId)); process.stdout.write( - wrote - ? `[pack-manager] wrote config/${packId}/permission.json\n` - : `[pack-manager] config/${packId}/permission.json already exists — skipped\n` + wrote ? `[pack-manager] wrote ${rel}\n` : `[pack-manager] ${rel} already exists — skipped\n` ); return; } diff --git a/bds-tools/src/pack-manager.ts b/bds-tools/src/pack-manager.ts index 39401024..63ca0f91 100644 --- a/bds-tools/src/pack-manager.ts +++ b/bds-tools/src/pack-manager.ts @@ -111,6 +111,41 @@ export const SFMC_PERMISSIONS = [ "@minecraft/diagnostics", ] as const; +/** `/server.properties` — level-name / telemetry 等共用(DRY) */ +export function serverPropertiesPath(bdsRoot: string): string { + return path.join(bdsRoot, "server.properties"); +} + +/** `/worlds` — enable-list / 部署调用方勿再硬拼(Demeter) */ +export function bdsWorldsDir(bdsRoot: string): string { + return path.join(bdsRoot, "worlds"); +} + +/** `/worlds/` */ +export function bdsWorldLevelDir(bdsRoot: string, levelName: string): string { + return path.join(bdsWorldsDir(bdsRoot), levelName); +} + +/** 世界 enable-list JSON 绝对路径(单一权威,供读写两侧) */ +export function worldPackListFile( + worldsDir: string, + levelName: string, + kind: "behavior" | "resource" +): string { + const name = kind === "behavior" ? "world_behavior_packs.json" : "world_resource_packs.json"; + return path.join(worldsDir, levelName, name); +} + +/** `/config//permission.json` — Script API 权限文件权威路径 */ +export function configPermissionPath(bdsRoot: string, bpUuid: string): string { + return path.join(bdsRoot, "config", bpUuid, "permission.json"); +} + +/** 是否已有 Script API permission.json(只读,供 preflight/status) */ +export function hasConfigPermission(bdsRoot: string, bpUuid: string): boolean { + return fs.existsSync(configPermissionPath(bdsRoot, bpUuid)); +} + /** 随机生成 BP/RP manifest.json header.uuid (RFC 4122 v4) */ export function randomUuid(): string { /* crypto.randomUUID 是 Node 19+ 内置,SEA 走 Node 22+,这里直接用 */ @@ -193,16 +228,14 @@ function parseLevelNameFromProperties(text: string): string { /** 同步读 level-name(供 sfmc resolveBdsContext 等同进程调用方,DRY)。 */ export function readLevelNameSync(bdsRoot: string): string { - const file = path.join(bdsRoot, "server.properties"); + const file = serverPropertiesPath(bdsRoot); if (!fs.existsSync(file)) return "Bedrock level"; return parseLevelNameFromProperties(fs.readFileSync(file, "utf8")); } +/** 与 sync 同契约;CLI / async 调用方走此入口(LSP) */ export async function readLevelName(bdsRoot: string): Promise { - const file = path.join(bdsRoot, "server.properties"); - if (!fs.existsSync(file)) return "Bedrock level"; - const text = await fs.promises.readFile(file, "utf8"); - return parseLevelNameFromProperties(text); + return readLevelNameSync(bdsRoot); } /** @@ -213,10 +246,11 @@ export async function readLevelName(bdsRoot: string): Promise { * must invoke after restart, because BDS only reads that file at startup. */ export async function deployToBDS(opts: DeployOpts): Promise { - const worldsDir = path.join(opts.bdsRoot, "worlds", opts.levelName); - const bpDst = path.join(worldsDir, "behavior_packs", opts.bpName); - const rpDst = path.join(worldsDir, "resource_packs", opts.rpName ?? `${opts.bpName}-rp`); - await fs.promises.mkdir(worldsDir, { recursive: true }); + /* levelDir = worlds/;与 EnablePackOpts.worldsDir(=worlds/) 语义不同 — 勿混用(LSP) */ + const levelDir = bdsWorldLevelDir(opts.bdsRoot, opts.levelName); + const bpDst = path.join(levelDir, "behavior_packs", opts.bpName); + const rpDst = path.join(levelDir, "resource_packs", opts.rpName ?? `${opts.bpName}-rp`); + await fs.promises.mkdir(levelDir, { recursive: true }); await fs.promises.rm(bpDst, { recursive: true, force: true }); await copyDirAsync(opts.behaviorPackSrc, bpDst); await writePermissionsJson(bpDst); @@ -238,9 +272,9 @@ export async function deployToBDS(opts: DeployOpts): Promise { * @returns true = 新写入; false = 已存在跳过 */ export async function ensureConfigPermission(bdsRoot: string, bpUuid: string): Promise { - const dir = path.join(bdsRoot, "config", bpUuid); - const file = path.join(dir, "permission.json"); + const file = configPermissionPath(bdsRoot, bpUuid); if (fs.existsSync(file)) return false; + const dir = path.dirname(file); await fs.promises.mkdir(dir, { recursive: true }); const payload = { allowed_modules: [...SFMC_PERMISSIONS] }; const tmp = path.join(dir, `.permission.${process.pid}.tmp`); @@ -306,11 +340,7 @@ async function editWorldPackList( opts: EnablePackOpts, mode: "enable" | "disable" ): Promise { - const file = path.join( - opts.worldsDir, - opts.levelName, - opts.kind === "behavior" ? "world_behavior_packs.json" : "world_resource_packs.json" - ); + const file = worldPackListFile(opts.worldsDir, opts.levelName, opts.kind); /* 与 readWorldPackList 同源解析,再写回(DRY);异步写路径保留原语义 */ let entries: WorldPackEntry[] = readWorldPackList(opts.worldsDir, opts.levelName, opts.kind); const idx = entries.findIndex((e) => e.pack_id === opts.packUuid); @@ -459,11 +489,7 @@ export function readWorldPackListResult( levelName: string, kind: "behavior" | "resource" ): WorldPackListReadResult { - const file = path.join( - worldsDir, - levelName, - kind === "behavior" ? "world_behavior_packs.json" : "world_resource_packs.json" - ); + const file = worldPackListFile(worldsDir, levelName, kind); if (!fs.existsSync(file)) return { entries: [] }; try { const arr = JSON.parse(fs.readFileSync(file, "utf8")) as unknown; diff --git a/bds-tools/src/server-properties.ts b/bds-tools/src/server-properties.ts index ae396142..b9d105fe 100644 --- a/bds-tools/src/server-properties.ts +++ b/bds-tools/src/server-properties.ts @@ -2,7 +2,7 @@ * server.properties 辅助:安装/启动时确保 emit-server-telemetry=true */ import fs from "node:fs"; -import path from "node:path"; +import { serverPropertiesPath } from "./pack-manager.js"; export const EMIT_SERVER_TELEMETRY_KEY = "emit-server-telemetry"; export const EMIT_SERVER_TELEMETRY_LINE = "emit-server-telemetry=true"; @@ -20,7 +20,7 @@ export function ensureEmitServerTelemetry( bdsRoot: string, logger?: ServerPropertiesLogger ): boolean { - const file = path.join(bdsRoot, "server.properties"); + const file = serverPropertiesPath(bdsRoot); if (!fs.existsSync(file)) return false; let text = fs.readFileSync(file, "utf8"); diff --git a/bds-tools/src/world-packs.ts b/bds-tools/src/world-packs.ts index c933cf79..f130272d 100644 --- a/bds-tools/src/world-packs.ts +++ b/bds-tools/src/world-packs.ts @@ -6,6 +6,8 @@ import os from "node:os"; import path from "node:path"; import { copyDirAsync } from "./fsx.js"; import { + bdsWorldLevelDir, + bdsWorldsDir, disablePackInWorld, enablePackInWorld, readPackManifestHeader, @@ -144,8 +146,8 @@ function listPackDirsIn(parent: string): string[] { /** 扫描世界内已安装 BP/RP 目录 */ export function listInstalledWorldPacks(bdsRoot: string, levelName: string): InstalledWorldPack[] { - const worldRoot = path.join(bdsRoot, "worlds", levelName); - const worldsDir = path.join(bdsRoot, "worlds"); + const worldRoot = bdsWorldLevelDir(bdsRoot, levelName); + const worldsDir = bdsWorldsDir(bdsRoot); const result: InstalledWorldPack[] = []; for (const kind of ["behavior", "resource"] as const) { @@ -294,7 +296,7 @@ export async function enableInstalledPack(opts: { info: PackManifestInfo; }): Promise { await enablePackInWorld({ - worldsDir: path.join(opts.bdsRoot, "worlds"), + worldsDir: bdsWorldsDir(opts.bdsRoot), levelName: opts.levelName, kind: opts.info.kind, packUuid: opts.info.uuid, @@ -311,7 +313,7 @@ export async function disableInstalledPack(opts: { version: [number, number, number]; }): Promise { await disablePackInWorld({ - worldsDir: path.join(opts.bdsRoot, "worlds"), + worldsDir: bdsWorldsDir(opts.bdsRoot), levelName: opts.levelName, kind: opts.kind, packUuid: opts.packUuid, @@ -325,9 +327,7 @@ export function worldPackParentDir( kind: WorldPackKind ): string { return path.join( - bdsRoot, - "worlds", - levelName, + bdsWorldLevelDir(bdsRoot, levelName), kind === "behavior" ? "behavior_packs" : "resource_packs" ); } @@ -347,7 +347,7 @@ export function listWorldEnableListResult( levelName: string, kind: WorldPackKind ): WorldPackListReadResult { - return readWorldPackListResult(path.join(bdsRoot, "worlds"), levelName, kind); + return readWorldPackListResult(bdsWorldsDir(bdsRoot), levelName, kind); } export function findInstalledPackById( diff --git a/sfmc/src/pack-lifecycle.ts b/sfmc/src/pack-lifecycle.ts index 41cc58d3..4d943f6d 100644 --- a/sfmc/src/pack-lifecycle.ts +++ b/sfmc/src/pack-lifecycle.ts @@ -27,10 +27,14 @@ import { t } from "./i18n/index.js"; import { assembleBehaviorPack, assembleResourcePack, + bdsWorldLevelDir, + bdsWorldsDir, + configPermissionPath, deployToBDS, disablePackInWorld, enablePackInWorld, ensureConfigPermission, + hasConfigPermission, readLevelNameSync, readPackManifestHeader, readWorldPackList, @@ -160,24 +164,17 @@ export function resolveBdsContext(): { bdsRoot: string; levelName: string } { } export function deployedBpDir(bdsRoot: string, levelName: string): string { - return path.join(bdsRoot, "worlds", levelName, "behavior_packs", BP_NAME); + return path.join(bdsWorldLevelDir(bdsRoot, levelName), "behavior_packs", BP_NAME); } export function deployedRpDir(bdsRoot: string, levelName: string): string { - return path.join(bdsRoot, "worlds", levelName, "resource_packs", RP_NAME); + return path.join(bdsWorldLevelDir(bdsRoot, levelName), "resource_packs", RP_NAME); } export function deployedCatalogPath(bdsRoot: string, levelName: string): string { return path.join(deployedBpDir(bdsRoot, levelName), DEPLOY_CATALOG_NAME); } -/** 读 BP/RP manifest header — 直连 pack-manager-lib(与 CLI read-manifest 同一权威,DRY/DIP) */ -function readManifestHeader( - packDir: string -): { uuid: string; version: [number, number, number]; moduleUuid?: string } | null { - return readPackManifestHeader(packDir); -} - /** 解析启用状态:lock 优先,否则 catalog.enabledByDefault(缺省 true↔!==false),未收录模块 false */ function isModuleEnabled(logicalId: string, lock: ModuleLock, catalogDefaults: Map): boolean { const st = lock.modules?.[logicalId]; @@ -198,8 +195,8 @@ function collectDeployedPackUuids( const cat = readDeployedCatalog(bdsRoot, levelName); if (cat?.bpUuid) bp.add(cat.bpUuid); if (cat?.rpUuid) rp.add(cat.rpUuid); - const liveBp = readManifestHeader(deployedBpDir(bdsRoot, levelName)); - const liveRp = readManifestHeader(deployedRpDir(bdsRoot, levelName)); + const liveBp = readPackManifestHeader(deployedBpDir(bdsRoot, levelName)); + const liveRp = readPackManifestHeader(deployedRpDir(bdsRoot, levelName)); if (liveBp?.uuid) bp.add(liveBp.uuid); if (liveRp?.uuid) rp.add(liveRp.uuid); return { bp, rp }; @@ -284,25 +281,7 @@ export async function scanLocalModules(): Promise< return out; } -/** 仅启用且有 SAPI 入口的路径 — 供 esbuild */ -export async function listEnabledSapiEntries(): Promise { - const mods = await scanLocalModules(); - return mods.filter((m) => m.enabled && m.entryPath).map((m) => m.entryPath!); -} - -/** 启用且带 resource_pack 的模块目录 map */ -export async function listEnabledResourcePackDirs(): Promise> { - const mods = await scanLocalModules(); - const out: Record = {}; - for (const m of mods) { - if (!m.enabled || !m.hasResourcePack) continue; - out[m.folderId] = path.join(packagesDir(), m.folderId, "resource_pack"); - } - return out; -} - -/** - * 从本机状态 + 已有 UUID 合成 desired catalog。 +/** 从本机状态 + 已有 UUID 合成 desired catalog。 * UUID 优先:deployed catalog → 本地 build manifest → 新建随机。 */ export async function computeDesiredCatalog(opts?: { @@ -328,8 +307,8 @@ export async function computeDesiredCatalog(opts?: { if (m.enabled && m.hasResourcePack) anyRp = true; } - const localBp = readManifestHeader(bpOut()); - const localRp = readManifestHeader(rpOut()); + const localBp = readPackManifestHeader(bpOut()); + const localRp = readPackManifestHeader(rpOut()); const bpUuid = opts?.bpUuid ?? localBp?.uuid ?? crypto.randomUUID(); const bpVersion = opts?.bpVersion ?? localBp?.version ?? DEFAULT_PACK_VERSION; const bpModuleUuid = opts?.bpModuleUuid ?? localBp?.moduleUuid; @@ -394,7 +373,7 @@ export function catalogsEqual(a: DeployCatalog, b: DeployCatalog): boolean { /** 世界 enable-list 查询 — 直连 pack-manager-lib(DRY/DIP,与 CLI has-pack 同契约) */ function worldPackListHas(bdsRoot: string, levelName: string, kind: "behavior" | "resource", uuid: string): boolean { - return pmWorldPackListHas(path.join(bdsRoot, "worlds"), levelName, kind, uuid); + return pmWorldPackListHas(bdsWorldsDir(bdsRoot), levelName, kind, uuid); } /** esbuild 聚合启用模块 → assemble BP + RP(若有) */ @@ -519,7 +498,7 @@ export async function deployPacks(catalog: DeployCatalog): Promise { /* 确保 catalog 在部署后的 BP 内(deploy 会拷贝整个目录) */ writeJson(path.join(deployedBpDir(bdsRoot, levelName), DEPLOY_CATALOG_NAME), catalog); - const worldsDir = path.join(bdsRoot, "worlds"); + const worldsDir = bdsWorldsDir(bdsRoot); try { await enablePackInWorld({ worldsDir, @@ -584,11 +563,8 @@ export async function deployPacks(catalog: DeployCatalog): Promise { try { const wrote = await ensureConfigPermission(bdsRoot, catalog.bpUuid); - packLog( - wrote - ? `wrote config/${catalog.bpUuid}/permission.json` - : `config/${catalog.bpUuid}/permission.json already exists — skipped` - ); + const permRel = path.relative(bdsRoot, configPermissionPath(bdsRoot, catalog.bpUuid)); + packLog(wrote ? `wrote ${permRel}` : `${permRel} already exists — skipped`); } catch (e) { throw new Error(`ensure-permission failed: ${(e as Error).message}`); } @@ -666,15 +642,12 @@ export async function ensurePacksReady(): Promise { if (deployed.rpUuid && !worldPackListHas(bdsRoot, levelName, "resource", deployed.rpUuid)) { needRebuild = true; } - if (!existsSync(path.join(bdsRoot, "config", deployed.bpUuid, "permission.json"))) { + if (!hasConfigPermission(bdsRoot, deployed.bpUuid)) { /* permission 缺失只补写,不强制整包重编 */ try { const wrote = await ensureConfigPermission(bdsRoot, deployed.bpUuid); - packLog( - wrote - ? `wrote config/${deployed.bpUuid}/permission.json` - : `config/${deployed.bpUuid}/permission.json already exists — skipped` - ); + const permRel = path.relative(bdsRoot, configPermissionPath(bdsRoot, deployed.bpUuid)); + packLog(wrote ? `wrote ${permRel}` : `${permRel} already exists — skipped`); } catch (e) { throw new Error(`ensure-permission failed: ${(e as Error).message}`); } @@ -739,9 +712,7 @@ export async function cmdPackStatus(_args: string[]): Promise { const deployed = readDeployedCatalog(bdsRoot, levelName); const desired = await computeDesiredCatalog(deployed ? reusePackIds(deployed) : undefined); const match = deployed ? catalogsEqual(desired, deployed) : false; - const permPath = deployed - ? path.join(bdsRoot, "config", deployed.bpUuid, "permission.json") - : null; + const permPath = deployed ? configPermissionPath(bdsRoot, deployed.bpUuid) : null; const lines = [ c.bold("\nPack status"), ` bds : ${bdsRoot}`, @@ -781,7 +752,7 @@ export async function cmdPackList(_args: string[]): Promise { ` ${mark} ${m.folderId.padEnd(24)} ${m.logicalId.padEnd(24)} ${m.enabled ? "on " : "off"} ${m.hasResourcePack ? "+rp" : " "} ${m.version}` ); } - const worldsDir = path.join(bdsRoot, "worlds"); + const worldsDir = bdsWorldsDir(bdsRoot); lines.push(c.bold("\nWorld enable lists")); for (const kind of ["behavior", "resource"] as const) { const arr = readWorldPackList(worldsDir, levelName, kind); @@ -805,7 +776,7 @@ export async function cmdPackEnableDisable(action: "enable" | "disable", args: s const uuid = kind === "behavior" ? deployed.bpUuid : deployed.rpUuid; const version = kind === "behavior" ? deployed.bpVersion : deployed.rpVersion; if (!uuid || !version) return c.red(t("pack.noUuid", { kind })); - const worldsDir = path.join(bdsRoot, "worlds"); + const worldsDir = bdsWorldsDir(bdsRoot); try { const opts = { worldsDir, levelName, kind, packUuid: uuid, version }; if (action === "enable") await enablePackInWorld(opts); diff --git a/sfmc/src/world-packs.ts b/sfmc/src/world-packs.ts index 34cad69f..26946557 100644 --- a/sfmc/src/world-packs.ts +++ b/sfmc/src/world-packs.ts @@ -677,12 +677,11 @@ export async function dispatchPacksCommand(sub: string | undefined, args: string case "path": { ensureInboxLayout(); const { bdsRoot, levelName } = resolveBdsContext(); - const world = path.join(bdsRoot, "worlds", levelName); return [ `bdsRoot: ${bdsRoot}`, `level: ${levelName}`, - `behavior: ${path.join(world, "behavior_packs")}`, - `resource: ${path.join(world, "resource_packs")}`, + `behavior: ${worldPackParentDir(bdsRoot, levelName, "behavior")}`, + `resource: ${worldPackParentDir(bdsRoot, levelName, "resource")}`, `inbox: ${packsInboxDir()}`, ].join("\n"); }