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
7 changes: 4 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ data/

# 模块启停状态:本地生成(init / fetch-module / enable|disable),勿提交
/modules/module-lock.json
# 误放仓根的 lock(权威路径是 modules/module-lock.json)
/module-lock.json
# 遗留聚合 manifest 目录(已由各包 sapi/manifest.json 取代)
/modules/_manifests/

Expand All @@ -54,6 +56,5 @@ test
!.vscode/settings.json
remote-controller/dist/index.js

# 测试环境下产物

/packs/
# 测试环境下产物 / 收件箱与构建产物
/packs/
15 changes: 10 additions & 5 deletions bds-tools/src/world-packs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,12 @@ export async function disableInstalledPack(opts: {
});
}

export type UninstallPackAction = "trashed" | "deleted" | "missing";
export type UninstallPackResult =
| { action: "trashed"; dest: string }
| { action: "deleted" }
| { action: "missing" };

export type UninstallPackAction = UninstallPackResult["action"];

/**
* 卸载已安装世界包:先 disable enable-list,再移入 trashDir 或直接删除目录。
Expand All @@ -390,7 +395,7 @@ export async function uninstallInstalledPack(opts: {
pack: InstalledWorldPack;
/** 提供则移入该目录;null/省略则直接删除 */
trashDir?: string | null;
}): Promise<{ ok: boolean; action: UninstallPackAction; dest?: string; reason?: string }> {
}): Promise<UninstallPackResult> {
await disableInstalledPack({
bdsRoot: opts.bdsRoot,
levelName: opts.levelName,
Expand All @@ -400,7 +405,7 @@ export async function uninstallInstalledPack(opts: {
});

if (!fs.existsSync(opts.pack.dir)) {
return { ok: true, action: "missing" };
return { action: "missing" };
}

const trashDir = opts.trashDir?.trim() || null;
Expand All @@ -417,11 +422,11 @@ export async function uninstallInstalledPack(opts: {
await copyDirAsync(opts.pack.dir, dest);
await fs.promises.rm(opts.pack.dir, { recursive: true, force: true });
}
return { ok: true, action: "trashed", dest };
return { action: "trashed", dest };
}

await fs.promises.rm(opts.pack.dir, { recursive: true, force: true });
return { ok: true, action: "deleted" };
return { action: "deleted" };
}

export function worldPackParentDir(
Expand Down
75 changes: 75 additions & 0 deletions bds-tools/world-packs.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,79 @@ describe("world-packs primitives", () => {
const still = JSON.parse(fs.readFileSync(path.join(existingDir, "manifest.json"), "utf8"));
assert.equal(still.header.uuid, "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
});

it("uninstallInstalledPack:回收站 / purge / 目录缺失", async () => {
const { uninstallInstalledPack, listInstalledWorldPacks } = await import("./dist/world-packs.js");
const bds = path.join(tmp, "uninstall-bds");
const level = "Bedrock level";
const folder = "[BP] Gone";
const uuid = "dddddddd-dddd-dddd-dddd-dddddddddddd";
const packDir = path.join(bds, "worlds", level, "behavior_packs", folder);
writeManifest(packDir, {
name: "Gone",
uuid,
version: [1, 0, 0],
type: "data",
});
fs.mkdirSync(path.join(bds, "worlds", level), { recursive: true });
fs.writeFileSync(
path.join(bds, "worlds", level, "world_behavior_packs.json"),
JSON.stringify([{ pack_id: uuid, version: [1, 0, 0] }])
);

const packs = listInstalledWorldPacks(bds, level);
assert.equal(packs.length, 1);
const trash = path.join(tmp, "trash-bin");
const trashed = await uninstallInstalledPack({
bdsRoot: bds,
levelName: level,
pack: packs[0],
trashDir: trash,
});
assert.equal(trashed.action, "trashed");
assert.ok(trashed.dest && fs.existsSync(trashed.dest));
assert.equal(fs.existsSync(packDir), false);
const enable = JSON.parse(
fs.readFileSync(path.join(bds, "worlds", level, "world_behavior_packs.json"), "utf8")
);
assert.equal(enable.length, 0);

/* 目录已不在 list 中;构造 missing 场景 */
const phantom = {
...packs[0],
dir: path.join(bds, "worlds", level, "behavior_packs", "no-such"),
enabled: false,
};
const missing = await uninstallInstalledPack({
bdsRoot: bds,
levelName: level,
pack: phantom,
trashDir: trash,
});
assert.equal(missing.action, "missing");

/* purge:再装一个直接删 */
const folder2 = "[BP] PurgeMe";
const uuid2 = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee";
const packDir2 = path.join(bds, "worlds", level, "behavior_packs", folder2);
writeManifest(packDir2, {
name: "PurgeMe",
uuid: uuid2,
version: [1, 0, 0],
type: "data",
});
fs.writeFileSync(
path.join(bds, "worlds", level, "world_behavior_packs.json"),
JSON.stringify([{ pack_id: uuid2, version: [1, 0, 0] }])
);
const packs2 = listInstalledWorldPacks(bds, level);
const purged = await uninstallInstalledPack({
bdsRoot: bds,
levelName: level,
pack: packs2.find((p) => p.uuid === uuid2),
trashDir: null,
});
assert.equal(purged.action, "deleted");
assert.equal(fs.existsSync(packDir2), false);
});
});
2 changes: 1 addition & 1 deletion db-server/src/routes/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ interface Deps {
listModules?: () => Array<Record<string, unknown>>;
/**
* 注入模块 HMAC token 表(仅 loopback 可达;供 SAPI host-bootstrap 注入
* set*ModuleContext,因 SAPI 不能读 data/module-tokens.json)。
* set*ModuleContext,因 SAPI 不能直接读与 DB 同目录的 module-tokens.json)。
*/
getModuleTokens?: () => Record<string, string>;
}
Expand Down
4 changes: 0 additions & 4 deletions module-lock.json

This file was deleted.

12 changes: 11 additions & 1 deletion sfmc/pack-update-config.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,24 @@ describe("pack-update config + provider resolve", () => {
}
});

it("defaultBindingEnabled 默认 false,且出现在类型化配置上", () => {
it("defaultBindingEnabled 默认 false,且出现在类型化配置上", async () => {
const cfgPath = path.join(tmpRoot, "configs", "pack-update.json");
if (fs.existsSync(cfgPath)) fs.unlinkSync(cfgPath);
const cfg = loadPackUpdateConfig();
assert.equal(cfg.defaultBindingEnabled, false);
assert.equal(typeof cfg.match.nameMinScore, "number");
assert.equal(cfg.uninstall.recycleBin, true);
assert.equal(cfg.uninstall.trashRelativeDir, "packs/_trash");

const { DEFAULT_PACK_UNINSTALL } = await import(
pathToFileURL(path.resolve("dist/pack-update/types.js")).href
);
const { resolveUninstallTrashDir } = await import(
pathToFileURL(path.resolve("dist/pack-update/config.js")).href
);
assert.equal(cfg.uninstall.trashRelativeDir, DEFAULT_PACK_UNINSTALL.trashRelativeDir);
assert.equal(resolveUninstallTrashDir({ purge: true }), null);
assert.ok(String(resolveUninstallTrashDir({})).includes(DEFAULT_PACK_UNINSTALL.trashRelativeDir));
});

it("旧版 providers.curseforge.match 提升到顶层 match", () => {
Expand Down
24 changes: 18 additions & 6 deletions sfmc/src/pack-update/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import fs from "node:fs";
import path from "node:path";
import { ROOT } from "../runtime.js";
import type { CurseForgeProviderConfig, PackUpdateConfig, PackUpdateMatchConfig } from "./types.js";
import { DEFAULT_PACK_UNINSTALL } from "./types.js";

const DEFAULT_MATCH: PackUpdateMatchConfig = {
nameMinScore: 0.6,
Expand Down Expand Up @@ -46,10 +47,7 @@ const DEFAULTS: PackUpdateConfig = {
skipDisabledBindings: true,
failMode: "continue",
},
uninstall: {
recycleBin: true,
trashRelativeDir: "packs/_trash",
},
uninstall: { ...DEFAULT_PACK_UNINSTALL },
};

function deepMerge<T extends Record<string, unknown>>(base: T, over: Partial<T>): T {
Expand Down Expand Up @@ -136,10 +134,11 @@ export function getPackMatchConfig(cfg: PackUpdateConfig): PackUpdateMatchConfig
return cfg.match;
}

/** 卸载回收站绝对路径(相对 SFMC_ROOT) */
/** 卸载回收站绝对路径(相对 SFMC_ROOT;缺省走 DEFAULT_PACK_UNINSTALL) */
export function resolvePackTrashDir(cfg?: PackUpdateConfig): string {
const c = cfg ?? loadPackUpdateConfig();
const rel = (c.uninstall?.trashRelativeDir || "packs/_trash").trim() || "packs/_trash";
const fallback = DEFAULT_PACK_UNINSTALL.trashRelativeDir;
const rel = (c.uninstall?.trashRelativeDir || fallback).trim() || fallback;
return path.isAbsolute(rel) ? rel : path.join(ROOT, rel);
}

Expand All @@ -149,6 +148,19 @@ export function isPackUninstallRecycleBin(cfg?: PackUpdateConfig): boolean {
return c.uninstall?.recycleBin !== false;
}

/**
* 解析本次卸载的 trash 目标(DRY:purge + recycleBin 只在一处组合)。
* 返回 null 表示直接删除。
*/
export function resolveUninstallTrashDir(opts?: {
purge?: boolean;
cfg?: PackUpdateConfig;
}): string | null {
const cfg = opts?.cfg ?? loadPackUpdateConfig();
if (opts?.purge || !isPackUninstallRecycleBin(cfg)) return null;
return resolvePackTrashDir(cfg);
}

/**
* 确保 configs/pack-update.json 存在;缺失则写入内置 DEFAULTS + $schema。
*/
Expand Down
6 changes: 6 additions & 0 deletions sfmc/src/pack-update/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ export interface PackUninstallConfig {
trashRelativeDir: string;
}

/** 卸载策略内置默认(DEFAULTS / resolve / schema 文案的唯一权威) */
export const DEFAULT_PACK_UNINSTALL: PackUninstallConfig = {
recycleBin: true,
trashRelativeDir: "packs/_trash",
};

export interface PackUpdateConfig {
enabled: boolean;
checkOnBdsStart: boolean;
Expand Down
Loading