diff --git a/db-server/src/index.ts b/db-server/src/index.ts index d050cfb1..2344ad68 100644 --- a/db-server/src/index.ts +++ b/db-server/src/index.ts @@ -159,7 +159,8 @@ function buildModuleList() { type: String((raw as Record).type || "feature"), description: String((raw as Record).description || ""), default_enabled: (raw as Record).enabledByDefault !== false, - can_disable: (raw as Record).canDisable !== false, + // 与 resolveModuleByKey 共用 moduleCanDisable,避免 list/disable 默认值漂移(LSP/DRY) + can_disable: moduleCanDisable(raw as Record), // ConfigManager 认 installed!==false;已装包默认 true installed: true, requires: Array.isArray((raw as Record).requires) @@ -183,14 +184,25 @@ function buildModuleList() { .filter(Boolean); } +/** catalog 省略 canDisable 时默认允许禁用(与 buildModuleList.can_disable 同源)。 */ +function moduleCanDisable(raw: Record): boolean { + return raw.canDisable !== false; +} + function resolveModuleByKey(key: string) { const k = String(key || "").trim(); const catalog = loadModuleCatalog(); - return catalog.find( + const raw = catalog.find( (m) => String((m as Record).id || "") === k || String((m as Record).configKey || (m as Record).config_key || "") === k - ) as { id: string; configKey: string; canDisable: boolean } | null; + ) as Record | undefined; + if (!raw) return null; + const id = String(raw.id || "").trim(); + const configKey = String(raw.configKey || raw.config_key || "").trim(); + if (!id || !configKey) return null; + // 归一化 canDisable:裸 catalog 条目缺省字段时 undefined 会被 !canDisable 误判为不可禁用(LSP) + return { id, configKey, canDisable: moduleCanDisable(raw) }; } function setModuleEnabled(mod: { id: string; canDisable: boolean }, enabled: boolean) { diff --git a/db-server/src/routes/config.ts b/db-server/src/routes/config.ts index 792b74e5..22f260c4 100644 --- a/db-server/src/routes/config.ts +++ b/db-server/src/routes/config.ts @@ -68,56 +68,6 @@ function createConfigRoutes({ json, projectRoot, listModules, getModuleTokens }: /** 仓顶服务 config 读取统一走 SDK;闭包捕获 projectRoot。 */ const readCfg = (name: ConfigName): unknown => readJson(configPath(projectRoot, name)); - function getAllConfigs(): Record { - const modules = typeof listModules === "function" ? listModules() : []; - const module_tokens = typeof getModuleTokens === "function" ? getModuleTokens() : {}; - return { - // 与 /api/sfmc/modules 同源;ConfigManager.init 一次拉齐启停态(DRY) - modules, - // loopback-only 下发;SAPI 无 fs,靠此注入模块身份(DIP) - module_tokens, - settings: stripMeta(readJson(configPath(projectRoot, "settings.json")) as Record | null), - areas: (arrayOrEmpty(readJson(configPath(projectRoot, "areas.json"))) as Array>) - .filter((r) => r && r.module && r.dimension != null) - .map((r) => stripMetaDeep(r)), - permissions: ( - arrayOrEmpty(readJson(configPath(projectRoot, "permissions.json"))) as Array> - ) - .filter((r) => r && r.player_name) - .map((r) => stripMetaDeep(r)), - banned_items: (arrayOrEmpty(readJson(configPath(projectRoot, "banned_items.json"))) as Array) - .filter((i) => typeof i === "string" && i && !i.startsWith("_")) - .map((id) => ({ item_id: id })), - clean: stripMetaDeep(readJson(configPath(projectRoot, "clean.json")) ?? {}), - grids: (arrayOrEmpty(readJson(configPath(projectRoot, "grids.json"))) as Array>) - .filter((r) => r && r.name) - .map((r) => stripMetaDeep(r)), - peace_filters: ( - arrayOrEmpty(readJson(configPath(projectRoot, "peace_filters.json"))) as Array> - ) - .filter((r) => r && r.family) - .map((r) => stripMetaDeep(r)), - questions: (arrayOrEmpty(readJson(configPath(projectRoot, "questions.json"))) as Array>) - .filter((r) => r && r.question) - .map((r, idx: number) => { - const clean = stripMetaDeep(r) as Record; - return { - id: idx + 1, - weight: clean.weight ?? 1, - question: clean.question, - answers: clean.answers ?? [], - msg_right: clean.msg_right ?? "", - msg_wrong: clean.msg_wrong ?? "", - explanation: clean.explanation ?? "", - min_rank: clean.min_rank ?? null, - max_rank: clean.max_rank ?? null, - rewards: clean.rewards ?? [], - punishments: clean.punishments ?? [], - }; - }), - }; - } - function getSettingsFlat(): Array<{ key: string; value: string }> { const obj = stripMeta(readJson(configPath(projectRoot, "settings.json")) as Record | null); return Object.entries(obj).map(([key, value]) => ({ key, value: jsonValue(value) })); @@ -197,6 +147,30 @@ function createConfigRoutes({ json, projectRoot, listModules, getModuleTokens }: }); } + /** + * SAPI ConfigManager 契约:banned_items 为 string[](与 GET /banned_items 同源)。 + * 勿再映射成 {item_id} — 否则 filter(typeof s === "string") 会得到空缓存(LSP)。 + * 其余资源复用单资源 helpers,避免 getAllConfigs 与专用路由双写(DRY)。 + */ + function getAllConfigs(): Record { + const modules = typeof listModules === "function" ? listModules() : []; + const module_tokens = typeof getModuleTokens === "function" ? getModuleTokens() : {}; + return { + // 与 /api/sfmc/modules 同源;ConfigManager.init 一次拉齐启停态(DRY) + modules, + // loopback-only 下发;SAPI 无 fs,靠此注入模块身份(DIP) + module_tokens, + settings: stripMeta(readCfg("settings.json") as Record | null), + areas: getAreas(), + permissions: getPermissions(), + banned_items: getBannedItems().map((x) => x.item_id), + clean: getClean(), + grids: getGrids(), + peace_filters: getPeaceFilters(), + questions: getQA(), + }; + } + return async function handleConfigRoute({ path: requestPath, method, diff --git a/modules/sdk/@sfmc-sdk/src/module-loader/internal/config-manager.ts b/modules/sdk/@sfmc-sdk/src/module-loader/internal/config-manager.ts index 9aa1b79a..7b340e0d 100644 --- a/modules/sdk/@sfmc-sdk/src/module-loader/internal/config-manager.ts +++ b/modules/sdk/@sfmc-sdk/src/module-loader/internal/config-manager.ts @@ -74,7 +74,8 @@ type AllConfigs = { end_z: number; }>; permissions: Array<{ player_name: string; level: number }>; - banned_items: string[]; + /** 权威为 string[];兼容历史 {item_id} */ + banned_items: Array; clean: { item_max?: number; poll_interval?: number }; grids: Array<{ name: string; @@ -275,7 +276,10 @@ export class ConfigManager { ConfigManager.cache.permissions[p.player_name] = p.level; } - ConfigManager.cache.bannedItems = (all.banned_items || []).filter((s) => typeof s === "string"); + // 权威契约为 string[];兼容历史 {item_id} 以免旧服务端把缓存清空(LSP 防御) + ConfigManager.cache.bannedItems = (all.banned_items || []) + .map((s) => (typeof s === "string" ? s : s && typeof s === "object" ? String((s as { item_id?: unknown }).item_id || "") : "")) + .filter((id) => !!id); if (all.clean) { ConfigManager.cache.clean = { diff --git a/modules/sdk/@sfmc-sdk/src/sapi/service/client.ts b/modules/sdk/@sfmc-sdk/src/sapi/service/client.ts index f852d2d4..acaff6d5 100644 --- a/modules/sdk/@sfmc-sdk/src/sapi/service/client.ts +++ b/modules/sdk/@sfmc-sdk/src/sapi/service/client.ts @@ -81,7 +81,9 @@ export const service = { authOpts() ); if (!res.ok) { - throw new ServiceError(res.error ?? "service_error", "internal", res.status); + // LSP:与 db 客户端一致,保留服务端 code,勿一律打成 internal + const data = res.data as { error?: string; code?: string } | undefined; + throw new ServiceError(data?.error ?? res.error ?? "service_error", data?.code || "internal", res.status); } return (res.data as { ok: true; result: T }).result; }, diff --git a/sfmc/src/module-commands.ts b/sfmc/src/module-commands.ts index 778f4612..48672e3a 100644 --- a/sfmc/src/module-commands.ts +++ b/sfmc/src/module-commands.ts @@ -30,9 +30,6 @@ export const MODULE_CMD_NAMES = ["module", "mod"] as const; export type ModuleCmdName = (typeof MODULE_CMD_NAMES)[number]; -/** HELP 中主名+别名展示串(权威来源 MODULE_CMD_NAMES),如 "module/mod"。 */ -export const MODULE_CMD_ALIAS_LABEL = MODULE_CMD_NAMES.join("/"); - /** 判断是否为 module 顶层命令(含别名);避免 main/repl 再硬编码 case。 */ export function isModuleCommand(cmd: string | undefined): cmd is ModuleCmdName { return !!cmd && (MODULE_CMD_NAMES as readonly string[]).includes(cmd); diff --git a/sfmc/src/repl.ts b/sfmc/src/repl.ts index dffc1c7d..e0044ba4 100644 --- a/sfmc/src/repl.ts +++ b/sfmc/src/repl.ts @@ -21,9 +21,6 @@ function setRaw(v: boolean): void { } catch {} } -/** HELP 行首:把 MODULE_CMD_NAMES 着色后用 / 拼接(如 module/mod)。 */ -const MODULE_HELP_LABEL = MODULE_CMD_NAMES.map((n) => c.green(n)).join("/"); - const welcome = `\n ${c.text(`⠪⡁⡯⠁`)} ${c.text(`⠒⠁⠃`)}${c.purple(`⠄`)} diff --git a/tools/check-ootb.mjs b/tools/check-ootb.mjs index 05589522..7cbba2dc 100755 --- a/tools/check-ootb.mjs +++ b/tools/check-ootb.mjs @@ -136,6 +136,13 @@ async function main() { if (!all.body.module_tokens || typeof all.body.module_tokens !== "object") { throw new Error("configs/all 缺少 module_tokens"); } + // LSP:configs/all.banned_items 须为 string[](与 /banned_items、ConfigManager 同源) + if (Array.isArray(all.body.banned_items)) { + const bad = all.body.banned_items.find((x) => typeof x !== "string"); + if (bad !== undefined) { + throw new Error(`configs/all.banned_items 须为 string[],收到 ${typeof bad}`); + } + } pass(`db-server 启动 + 平台 API (modules=${mods.body.modules.length})`); } catch (e) { fail("db-server 启动 + 平台 API", e.message);