From 4d35895752026b1615acc4d0102c40228a8f2069 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Jul 2026 16:11:27 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20fetch-module=E2=86=92zipx=20DRY=20+=20do?= =?UTF-8?q?ctor=20parseFail=20+=20=E5=8E=BB=20stripOsc=20=E4=B8=AD?= =?UTF-8?q?=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审查 main@e95d81e ootb 红(#51+#52 合并残留)后 #54 已修 TS2300; 本补丁收敛遗留 SOLID 债: - MAJOR(DRY): tools/fetch-module 解压委托 @sfmc-bds/bds-tools/zipx - MAJOR(LSP): readWorldPackListResult 恢复 doctor parseFail 信号 - MINOR(DRY): cmdUpdate 直接 stripTaskbarOsc,去掉无用 stripOsc 别名 Co-authored-by: Shiroha --- bds-tools/pack-manager-lifecycle.test.mjs | 22 +++++++++++++++++ bds-tools/src/pack-manager.ts | 29 +++++++++++++++++------ bds-tools/src/world-packs.ts | 13 +++++++++- bds-tools/src/zipx.ts | 2 +- package-lock.json | 4 +++- package.json | 1 - sfmc/src/commands.ts | 8 ++----- sfmc/src/world-packs.ts | 11 +++++---- tools/fetch-module.mjs | 21 ++++------------ tools/package.json | 3 +++ 10 files changed, 76 insertions(+), 38 deletions(-) diff --git a/bds-tools/pack-manager-lifecycle.test.mjs b/bds-tools/pack-manager-lifecycle.test.mjs index 8a442a02..1cb44d2b 100644 --- a/bds-tools/pack-manager-lifecycle.test.mjs +++ b/bds-tools/pack-manager-lifecycle.test.mjs @@ -145,6 +145,28 @@ describe("pack-manager CLI extensions", () => { assert.deepEqual(readWorldPackList(worldsDir, "L1", "behavior"), listed); }); + it("readWorldPackListResult 区分缺失与 JSON 损坏(doctor parseFail)", async () => { + const { readWorldPackListResult } = await import("./dist/pack-manager.js"); + const worldsDir = path.join(tmp, "worlds-parse"); + const levelDir = path.join(worldsDir, "Lbad"); + fs.mkdirSync(levelDir, { recursive: true }); + + const missing = readWorldPackListResult(worldsDir, "Lbad", "behavior"); + assert.deepEqual(missing.entries, []); + assert.equal(missing.parseFailedFile, undefined); + + const badFile = path.join(levelDir, "world_behavior_packs.json"); + fs.writeFileSync(badFile, "{not-json", "utf8"); + const bad = readWorldPackListResult(worldsDir, "Lbad", "behavior"); + assert.deepEqual(bad.entries, []); + assert.equal(bad.parseFailedFile, badFile); + + fs.writeFileSync(badFile, '{"not":"array"}', "utf8"); + const notArr = readWorldPackListResult(worldsDir, "Lbad", "behavior"); + assert.deepEqual(notArr.entries, []); + assert.equal(notArr.parseFailedFile, badFile); + }); + it("无 deploy-catalog 时仍可凭磁盘 RP manifest 卸世界清单(BLOCKER 回归)", async () => { const { assembleResourcePack, diff --git a/bds-tools/src/pack-manager.ts b/bds-tools/src/pack-manager.ts index 1b0b0056..70ccbe65 100644 --- a/bds-tools/src/pack-manager.ts +++ b/bds-tools/src/pack-manager.ts @@ -437,22 +437,28 @@ export function loadModuleResourcePackMap(jsonPath: string): Record; + /** 文件存在但解析失败时为该 JSON 绝对路径 */ + parseFailedFile?: string; +}; + +export function readWorldPackListResult( worldsDir: string, levelName: string, kind: "behavior" | "resource" -): Array<{ pack_id: string; version: [number, number, number] }> { +): WorldPackListReadResult { const file = path.join( worldsDir, levelName, kind === "behavior" ? "world_behavior_packs.json" : "world_resource_packs.json" ); - if (!fs.existsSync(file)) return []; + if (!fs.existsSync(file)) return { entries: [] }; try { const arr = JSON.parse(fs.readFileSync(file, "utf8")) as unknown; - if (!Array.isArray(arr)) return []; + if (!Array.isArray(arr)) return { entries: [], parseFailedFile: file }; const out: Array<{ pack_id: string; version: [number, number, number] }> = []; for (const e of arr) { if (!e || typeof e !== "object") continue; @@ -465,12 +471,21 @@ export function readWorldPackList( : [1, 0, 0]; out.push({ pack_id: packId, version }); } - return out; + return { entries: out }; } catch { - return []; + return { entries: [], parseFailedFile: file }; } } +/** 兼容读侧:只要 entries(损坏时与缺失同为 [])。详细结果见 readWorldPackListResult。 */ +export function readWorldPackList( + worldsDir: string, + levelName: string, + kind: "behavior" | "resource" +): Array<{ pack_id: string; version: [number, number, number] }> { + return readWorldPackListResult(worldsDir, levelName, kind).entries; +} + /** 世界 enable-list 是否已含指定 pack_id(只读,供 preflight 复用)。 */ export function worldPackListHas( worldsDir: string, diff --git a/bds-tools/src/world-packs.ts b/bds-tools/src/world-packs.ts index 4da7ae90..c933cf79 100644 --- a/bds-tools/src/world-packs.ts +++ b/bds-tools/src/world-packs.ts @@ -10,6 +10,8 @@ import { enablePackInWorld, readPackManifestHeader, readWorldPackList, + readWorldPackListResult, + type WorldPackListReadResult, } from "./pack-manager.js"; import { extractZipFileToDir } from "./zipx.js"; @@ -336,7 +338,16 @@ export function listWorldEnableEntries( levelName: string, kind: WorldPackKind ): Array<{ pack_id: string; version: [number, number, number] }> { - return readWorldPackList(path.join(bdsRoot, "worlds"), levelName, kind); + return listWorldEnableListResult(bdsRoot, levelName, kind).entries; +} + +/** 含 parseFail 信号的 enable-list 快照(doctor 用;不暴露 JSON 路径构造细节) */ +export function listWorldEnableListResult( + bdsRoot: string, + levelName: string, + kind: WorldPackKind +): WorldPackListReadResult { + return readWorldPackListResult(path.join(bdsRoot, "worlds"), levelName, kind); } export function findInstalledPackById( diff --git a/bds-tools/src/zipx.ts b/bds-tools/src/zipx.ts index ab2aae42..98651da6 100644 --- a/bds-tools/src/zipx.ts +++ b/bds-tools/src/zipx.ts @@ -1,6 +1,6 @@ /** * zipx.ts — JSZip 安全解压(防 zip-slip / 绝对路径 / Windows `\`) - * 单一权威,供 world-packs / check-update 复用(DRY)。 + * 单一权威,供 world-packs / check-update / wizard / fetch-module 复用(DRY)。 */ import fs from "node:fs"; import path from "node:path"; diff --git a/package-lock.json b/package-lock.json index 24d1c6ff..34d00bea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,6 @@ "@typescript-eslint/utils": "^8.65.0", "esbuild": "^0.28.1", "eslint": "^10.7.0", - "jszip": "^3.10.1", "lodash": "^4.18.1", "nodemon": "^3.1.14", "npm-run-all2": "^9.0.2", @@ -3589,6 +3588,9 @@ "name": "@sfmc-bds/tools", "version": "0.1.1", "license": "AGPL-3.0-only", + "dependencies": { + "@sfmc-bds/bds-tools": "^0.1.0" + }, "bin": { "sfmc-catalog-sync": "catalog-sync.mjs", "sfmc-check-modules": "check-modules.mjs", diff --git a/package.json b/package.json index b7753615..7ce9c777 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "@typescript-eslint/utils": "^8.65.0", "esbuild": "^0.28.1", "eslint": "^10.7.0", - "jszip": "^3.10.1", "lodash": "^4.18.1", "nodemon": "^3.1.14", "npm-run-all2": "^9.0.2", diff --git a/sfmc/src/commands.ts b/sfmc/src/commands.ts index e032fe77..9dd7f704 100644 --- a/sfmc/src/commands.ts +++ b/sfmc/src/commands.ts @@ -163,11 +163,6 @@ export async function cmdStopAll(): Promise { return c.dim(t("svc.allStopped")); } -/** 剥离 Windows Terminal 任务栏 OSC,避免 pipe 日志出现空白行(委托 bds-tools/taskbar) */ -function stripOsc(s: string): string { - return stripTaskbarOsc(s); -} - /** * BDS 更新:子进程始终 --no-start,由 sfmc 监督器接管启停与日志。 * (updater 内 detached 自启会导致 REPL 丢 PID / 无 stdout) @@ -190,7 +185,8 @@ export async function cmdUpdate(args: string[] = []): Promise { }); let out = ""; const pushChunk = (raw: string, level: "info" | "error"): void => { - const s = stripOsc(raw); + // 剥离 Windows Terminal 任务栏 OSC,避免 pipe 日志空白行(权威:bds-tools/taskbar) + const s = stripTaskbarOsc(raw); out += s; for (const line of s .split(/\r?\n/) diff --git a/sfmc/src/world-packs.ts b/sfmc/src/world-packs.ts index 069f7e93..34cad69f 100644 --- a/sfmc/src/world-packs.ts +++ b/sfmc/src/world-packs.ts @@ -16,7 +16,7 @@ import { installPackDirectory, isPackArchive, listInstalledWorldPacks, - listWorldEnableEntries, + listWorldEnableListResult, readPackManifestInfo, worldPackParentDir, type InstalledWorldPack, @@ -454,10 +454,13 @@ async function cmdDoctor(): Promise { const issues: string[] = []; for (const kind of ["behavior", "resource"] as const) { - // 经 listWorldEnableEntries → pack-manager.readWorldPackList(DRY/Demeter,不硬编码 JSON 文件名) - const entries = listWorldEnableEntries(bdsRoot, levelName, kind); + // 经 listWorldEnableListResult → pack-manager(DRY/Demeter;保留 parseFail 信号 — LSP) + const snap = listWorldEnableListResult(bdsRoot, levelName, kind); + if (snap.parseFailedFile) { + issues.push(t("packs.doctor.parseFail", { file: path.basename(snap.parseFailedFile) })); + } const byUuid = new Map(packs.filter((p) => p.kind === kind).map((p) => [p.uuid, p])); - for (const e of entries) { + for (const e of snap.entries) { const p = byUuid.get(e.pack_id); if (!p) { issues.push(t("packs.doctor.missingDir", { kind, uuid: e.pack_id })); diff --git a/tools/fetch-module.mjs b/tools/fetch-module.mjs index e1705158..28d336d4 100755 --- a/tools/fetch-module.mjs +++ b/tools/fetch-module.mjs @@ -25,6 +25,7 @@ import { pipeline } from "node:stream/promises"; import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; +import { extractZipFileToDir } from "@sfmc-bds/bds-tools/zipx"; import { upsertCatalogEntry, removeCatalogEntry } from "./lib/catalog.mjs"; import { setModuleLockEnabled, removeModuleLock } from "./lib/lock.mjs"; import { PACKAGES_DIR, ROOT } from "./lib/paths.mjs"; @@ -411,24 +412,10 @@ async function listGithub(source) { } } +/** 解压模块包 — 委托 bds-tools/zipx(DRY;防 zip-slip / `\` / 绝对路径) */ async function unzip(zipPath, dstDir) { - const JSZip = (await import("jszip")).default; - const data = await fsp.readFile(zipPath); - const zip = await JSZip.loadAsync(data); - for (const e of Object.values(zip.files)) { - // Windows zip 常带 `\`;必须归一成 `/`,否则 Linux 会写出字面量 `sapi\manifest.json` - const rel = String(e.name).replace(/\\/g, "/").replace(/^\/+/, ""); - if (!rel || rel.includes("..")) continue; - const parts = rel.replace(/\/$/, "").split("/").filter(Boolean); - if (parts.length === 0) continue; - const out = path.join(dstDir, ...parts); - if (e.dir || rel.endsWith("/")) { - await fsp.mkdir(out, { recursive: true }); - continue; - } - await fsp.mkdir(path.dirname(out), { recursive: true }); - await fsp.writeFile(out, await e.async("nodebuffer")); - } + await fsp.mkdir(dstDir, { recursive: true }); + await extractZipFileToDir(zipPath, dstDir); } async function copyDir(src, dst) { diff --git a/tools/package.json b/tools/package.json index 6e161434..68d50230 100644 --- a/tools/package.json +++ b/tools/package.json @@ -38,6 +38,9 @@ "scripts": { "build": "node -e \"process.exit(0)\"" }, + "dependencies": { + "@sfmc-bds/bds-tools": "^0.1.0" + }, "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/"