From 01cd9cf6ea673549468344e4cd267ea35f17c2b1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 22 Jul 2026 17:12:34 +0000 Subject: [PATCH 1/4] =?UTF-8?q?fix(cli):=20=E6=B6=88=E9=99=A4=20MODULE=5FH?= =?UTF-8?q?ELP=5FLABEL=20=E6=9C=AA=E4=BD=BF=E7=94=A8=E8=87=B4=20TS6133=20?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #32/#33 合并后 HELP 改用 paintModuleCmdAlias,遗留 MODULE_HELP_LABEL 触发 noUnusedLocals,ootb 在 Build 阶段红。改为一次染色常量复用, 并删除未使用的 MODULE_CMD_ALIAS_LABEL(DRY)。 Co-authored-by: Shiroha --- sfmc/src/module-commands.ts | 4 +--- sfmc/src/repl.ts | 18 +++++++++--------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/sfmc/src/module-commands.ts b/sfmc/src/module-commands.ts index 778f4612..6a8ea6e5 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); @@ -56,6 +53,7 @@ export const MODULE_SUBCOMMANDS = [ "disable", ] as const; +/** Usage 行主名|别名(与 MODULE_CMD_NAMES 同源,避免与 HELP 漂移)。 */ export const MODULE_USAGE = `Usage: sfmc ${MODULE_CMD_NAMES.join("|")} <${MODULE_SUBCOMMANDS.join("|")}> [args]`; diff --git a/sfmc/src/repl.ts b/sfmc/src/repl.ts index dffc1c7d..e602f302 100644 --- a/sfmc/src/repl.ts +++ b/sfmc/src/repl.ts @@ -21,8 +21,8 @@ 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("/"); +/** HELP 行首:染色后的 module 别名标签(权威来源 paintModuleCmdAlias / MODULE_CMD_NAMES)。 */ +const MODULE_HELP_LABEL = paintModuleCmdAlias(c.green); const welcome = `\n ${c.text(`⠪⡁⡯⠁`)} @@ -53,19 +53,19 @@ ${c.bold("Commands")} ${c.green("remote enroll")} [name] Enroll this supervisor with a controller ${c.green("remote disable")} Disable + disconnect remote agent - ${paintModuleCmdAlias(c.green)} list + ${MODULE_HELP_LABEL} list List installed modules - ${paintModuleCmdAlias(c.green)} search [id] + ${MODULE_HELP_LABEL} search [id] Fetch registry list / show one module's registry info - ${paintModuleCmdAlias(c.green)} install [--from ] + ${MODULE_HELP_LABEL} install [--from ] Fetch + install a module - ${paintModuleCmdAlias(c.green)} uninstall + ${MODULE_HELP_LABEL} uninstall Remove an installed module - ${paintModuleCmdAlias(c.green)} verify [id] + ${MODULE_HELP_LABEL} verify [id] Verify installed modules (SHA-256) - ${paintModuleCmdAlias(c.green)} info + ${MODULE_HELP_LABEL} info Show one installed module's details - ${paintModuleCmdAlias(c.green)} enable|disable + ${MODULE_HELP_LABEL} enable|disable Toggle module (needs db-server) ${c.green("version")} Show version ${c.green("help")} Show this From 76e492121d4885048535652c622d79ea7399b67d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 22 Jul 2026 17:12:40 +0000 Subject: [PATCH 2/4] =?UTF-8?q?fix(solid):=20enable=20=E7=83=AD=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=20token/enabledSet=20+=20banned=5Fitems/canDisable=20?= =?UTF-8?q?LSP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MAJOR DIP: setModuleEnabled 后同步 enabledSet、enabledManifests、 moduleAuth.tokens 与 builtin handlers,使「只重启 BDS」可用新 token。 MAJOR LSP: configs/all.banned_items 改为 string[](与 ConfigManager 契约对齐); resolveModuleByKey 省略 canDisable 视为可禁用(与 list 同源)。 Co-authored-by: Shiroha --- db-server/src/index.ts | 23 +++++- db-server/src/module-runtime-sync.ts | 111 +++++++++++++++++++++++++++ db-server/src/routes/config.ts | 3 +- db-server/src/runtime.test.ts | 65 ++++++++++++++++ 4 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 db-server/src/module-runtime-sync.ts diff --git a/db-server/src/index.ts b/db-server/src/index.ts index d050cfb1..abd7e5c3 100644 --- a/db-server/src/index.ts +++ b/db-server/src/index.ts @@ -35,6 +35,7 @@ import { SchemaRegistry } from "./schema-registry.js"; import { ServiceRegistry } from "./service-registry.js"; import { TxRunner } from "./tx-runner.js"; import { registerEnabledBuiltinServices } from "./services/builtin-handlers.js"; +import { syncModuleRuntimeState } from "./module-runtime-sync.js"; import { readJson } from "@sfmc-bds/sdk/node/config"; @@ -186,11 +187,17 @@ function buildModuleList() { 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; + /* LSP:与 buildModuleList.can_disable 同源 — 省略字段视为可禁用 */ + return { id, configKey, canDisable: raw.canDisable !== false }; } function setModuleEnabled(mod: { id: string; canDisable: boolean }, enabled: boolean) { @@ -199,6 +206,18 @@ function setModuleEnabled(mod: { id: string; canDisable: boolean }, enabled: boo updateModuleState(lockFile, mod.id, { enabled: !!enabled }); // DRY:与 loadModuleLock 对称走 saveModuleLock,勿散落 writeJson saveModuleLock(env.MODULE_LOCK_PATH, lockFile); + // DIP:同步 enabledSet / tokens / builtin handlers,使「只重启 BDS」可用新 token + syncModuleRuntimeState({ + moduleId: mod.id, + enabled: !!enabled, + projectRoot: env.PROJECT_ROOT, + enabledSet, + enabledManifests, + loadedManifest, + moduleAuth, + serviceRegistry, + builtinDeps: { query, db }, + }); } // ── 平台路由(非模块业务) ─────────────────────────────────── diff --git a/db-server/src/module-runtime-sync.ts b/db-server/src/module-runtime-sync.ts new file mode 100644 index 00000000..3d336657 --- /dev/null +++ b/db-server/src/module-runtime-sync.ts @@ -0,0 +1,111 @@ +/** + * module-runtime-sync.ts — enable/disable 时同步进程内运行态 + * + * lock 落盘后,鉴态(enabledSet / enabledManifests / moduleAuth.tokens / + * builtin service handlers)必须与之一致,否则「只重启 BDS」拿不到新 token + * (DIP:路由只依赖注入的集合,不感知启停实现)。 + */ + +import { writeJson } from "@sfmc-bds/sdk/node/config"; +import { join } from "node:path"; +import type { ModuleManifestV2 } from "./manifest-loader.js"; +import { deriveToken, type ModuleAuthMap } from "./module-auth.js"; +import { + BUILTIN_SERVICE_PLUGINS, + type BuiltinServiceDeps, +} from "./services/builtin-handlers.js"; +import type { ServiceRegistry } from "./service-registry.js"; +import { log } from "./lib/log.js"; + +export type LoadedModules = { + modules: Record; +}; + +export type SyncModuleRuntimeOpts = { + moduleId: string; + enabled: boolean; + projectRoot: string; + enabledSet: Set; + enabledManifests: Map; + loadedManifest: LoadedModules; + moduleAuth: ModuleAuthMap; + serviceRegistry: ServiceRegistry; + builtinDeps: BuiltinServiceDeps; +}; + +/** 把当前 moduleAuth.tokens 写回 data/module-tokens.json(不轮换 secret)。 */ +export function persistModuleAuthTokens(projectRoot: string, auth: ModuleAuthMap): void { + const outFile = join(projectRoot, "data", "module-tokens.json"); + writeJson(outFile, { + tokens: auth.tokens, + secret: auth.secret, + generatedAt: new Date().toISOString(), + secretGenerated: false, + }); +} + +/** 卸掉某模块在 ServiceRegistry 上的全部 handler。 */ +export function unregisterHandlersForModule(registry: ServiceRegistry, moduleId: string): number { + let n = 0; + for (const h of registry.list()) { + if (h.moduleId !== moduleId) continue; + registry.unregisterHandler(h.name); + n += 1; + } + return n; +} + +/** 若该 moduleId 有内置插件且尚未注册,则注册。 */ +export function registerBuiltinForModule( + registry: ServiceRegistry, + deps: BuiltinServiceDeps, + moduleId: string +): boolean { + const plugin = BUILTIN_SERVICE_PLUGINS.find((p) => p.moduleId === moduleId); + if (!plugin) return false; + const already = registry.list().some((h) => h.moduleId === moduleId); + if (already) return false; + plugin.register(registry, deps); + return true; +} + +/** + * 同步 enable/disable 后的进程内集合与 token 文件。 + * 调用方负责先写 lock;本函数不碰磁盘 lock。 + */ +export function syncModuleRuntimeState(opts: SyncModuleRuntimeOpts): void { + const { + moduleId, + enabled, + projectRoot, + enabledSet, + enabledManifests, + loadedManifest, + moduleAuth, + serviceRegistry, + builtinDeps, + } = opts; + + if (enabled) { + enabledSet.add(moduleId); + const manifest = loadedManifest.modules[moduleId]; + if (manifest) enabledManifests.set(moduleId, manifest); + moduleAuth.tokens[moduleId] = deriveToken(moduleId, moduleAuth.secret); + persistModuleAuthTokens(projectRoot, moduleAuth); + if (registerBuiltinForModule(serviceRegistry, builtinDeps, moduleId)) { + log.info(`[service] runtime-enable: registered builtin handlers for ${moduleId}`); + } + log.info(`[modules] runtime-enable ${moduleId} (token+enabledSet synced)`); + return; + } + + enabledSet.delete(moduleId); + enabledManifests.delete(moduleId); + delete moduleAuth.tokens[moduleId]; + persistModuleAuthTokens(projectRoot, moduleAuth); + const removed = unregisterHandlersForModule(serviceRegistry, moduleId); + if (removed > 0) { + log.info(`[service] runtime-disable: unregistered ${removed} handler(s) for ${moduleId}`); + } + log.info(`[modules] runtime-disable ${moduleId} (token+enabledSet synced)`); +} diff --git a/db-server/src/routes/config.ts b/db-server/src/routes/config.ts index 792b74e5..2deec66a 100644 --- a/db-server/src/routes/config.ts +++ b/db-server/src/routes/config.ts @@ -86,8 +86,7 @@ function createConfigRoutes({ json, projectRoot, listModules, getModuleTokens }: .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 })), + .filter((i) => typeof i === "string" && i && !i.startsWith("_")), clean: stripMetaDeep(readJson(configPath(projectRoot, "clean.json")) ?? {}), grids: (arrayOrEmpty(readJson(configPath(projectRoot, "grids.json"))) as Array>) .filter((r) => r && r.name) diff --git a/db-server/src/runtime.test.ts b/db-server/src/runtime.test.ts index 54315c78..4d6c85e1 100644 --- a/db-server/src/runtime.test.ts +++ b/db-server/src/runtime.test.ts @@ -130,3 +130,68 @@ test("normalizeOrderBy: SDK field 与遗留 col / 数组互通(LSP)", async () = ]); throws(() => normalizeOrderBy({ dir: "asc" }), /field\/col/); }); + +test("syncModuleRuntimeState: enable/disable 热更新 token+enabledSet(DIP)", async () => { + const { mkdtempSync, rmSync, readFileSync } = await import("node:fs"); + const { join } = await import("node:path"); + const { tmpdir } = await import("node:os"); + const { ServiceRegistry } = await import("./service-registry.js"); + const { + syncModuleRuntimeState, + unregisterHandlersForModule, + } = await import("./module-runtime-sync.js"); + const { deriveToken } = await import("./module-auth.js"); + + const root = mkdtempSync(join(tmpdir(), "sfmc-runtime-sync-")); + try { + const enabledSet = new Set(["feature-a"]); + const enabledManifests = new Map(); + const moduleAuth = { tokens: { "feature-a": "old" }, secret: "test-secret" }; + const registry = new ServiceRegistry(); + const fakeManifest = { + id: "feature-b", + version: "1.0.0", + permissions: [] as string[], + services: { provides: [], requires: [] }, + db: { tables: [] }, + config: { key: "b" }, + } as unknown as import("./manifest-loader.js").ModuleManifestV2; + + syncModuleRuntimeState({ + moduleId: "feature-b", + enabled: true, + projectRoot: root, + enabledSet, + enabledManifests, + loadedManifest: { modules: { "feature-b": fakeManifest } }, + moduleAuth, + serviceRegistry: registry, + builtinDeps: { query: (() => []) as never, db: {} as never }, + }); + + equal(enabledSet.has("feature-b"), true); + equal(enabledManifests.has("feature-b"), true); + equal(moduleAuth.tokens["feature-b"], deriveToken("feature-b", "test-secret")); + const store = JSON.parse(readFileSync(join(root, "data", "module-tokens.json"), "utf8")); + equal(store.tokens["feature-b"], moduleAuth.tokens["feature-b"]); + + syncModuleRuntimeState({ + moduleId: "feature-b", + enabled: false, + projectRoot: root, + enabledSet, + enabledManifests, + loadedManifest: { modules: { "feature-b": fakeManifest } }, + moduleAuth, + serviceRegistry: registry, + builtinDeps: { query: (() => []) as never, db: {} as never }, + }); + + equal(enabledSet.has("feature-b"), false); + equal(enabledManifests.has("feature-b"), false); + equal(moduleAuth.tokens["feature-b"], undefined); + equal(unregisterHandlersForModule(registry, "feature-b"), 0); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); From 14132ef47fa17d0bc9c6f165d1841f10aef3134c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 22 Jul 2026 17:12:55 +0000 Subject: [PATCH 3/4] =?UTF-8?q?test(db-server):=20=E6=94=BE=E5=AE=BD=20syn?= =?UTF-8?q?cModuleRuntimeState=20=E7=94=A8=E4=BE=8B=20tokens=20=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record 允许动态增删 feature-b token,避免 tsc 窄化报错。 Co-authored-by: Shiroha --- db-server/src/runtime.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db-server/src/runtime.test.ts b/db-server/src/runtime.test.ts index 4d6c85e1..c7450082 100644 --- a/db-server/src/runtime.test.ts +++ b/db-server/src/runtime.test.ts @@ -146,7 +146,7 @@ test("syncModuleRuntimeState: enable/disable 热更新 token+enabledSet(DIP)", a try { const enabledSet = new Set(["feature-a"]); const enabledManifests = new Map(); - const moduleAuth = { tokens: { "feature-a": "old" }, secret: "test-secret" }; + const moduleAuth = { tokens: { "feature-a": "old" } as Record, secret: "test-secret" }; const registry = new ServiceRegistry(); const fakeManifest = { id: "feature-b", From acb46e3aa5c83cee3a0f5ed339f4f4a02ef87b94 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 22 Jul 2026 17:14:58 +0000 Subject: [PATCH 4/4] =?UTF-8?q?fix(solid):=20=E5=90=B8=E6=94=B6=20#35=20?= =?UTF-8?q?=E2=80=94=20moduleCanDisable/getAllConfigs=20DRY=20+=20service?= =?UTF-8?q?=20code=20LSP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 与并行 #34/#35 对齐并超集: - moduleCanDisable 供 list/disable 同源 - getAllConfigs 委托单资源 helpers;banned_items 权威 string[] - ConfigManager 兼容历史 {item_id};ootb 锁形状 - service.get 保留服务端 code(勿一律 internal) Co-authored-by: Shiroha --- db-server/src/index.ts | 9 ++- db-server/src/routes/config.ts | 61 ++++++------------- .../module-loader/internal/config-manager.ts | 8 ++- .../sdk/@sfmc-sdk/src/sapi/service/client.ts | 4 +- tools/check-ootb.mjs | 7 +++ 5 files changed, 41 insertions(+), 48 deletions(-) diff --git a/db-server/src/index.ts b/db-server/src/index.ts index abd7e5c3..514f9f2a 100644 --- a/db-server/src/index.ts +++ b/db-server/src/index.ts @@ -160,7 +160,7 @@ 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, + can_disable: moduleCanDisable(raw as Record), // ConfigManager 认 installed!==false;已装包默认 true installed: true, requires: Array.isArray((raw as Record).requires) @@ -184,6 +184,11 @@ 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(); @@ -197,7 +202,7 @@ function resolveModuleByKey(key: string) { const configKey = String(raw.configKey || raw.config_key || "").trim(); if (!id || !configKey) return null; /* LSP:与 buildModuleList.can_disable 同源 — 省略字段视为可禁用 */ - return { id, configKey, canDisable: raw.canDisable !== false }; + 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 2deec66a..95d93fad 100644 --- a/db-server/src/routes/config.ts +++ b/db-server/src/routes/config.ts @@ -71,49 +71,24 @@ function createConfigRoutes({ json, projectRoot, listModules, getModuleTokens }: function getAllConfigs(): Record { const modules = typeof listModules === "function" ? listModules() : []; const module_tokens = typeof getModuleTokens === "function" ? getModuleTokens() : {}; + /** + * SAPI ConfigManager 契约:banned_items 为 string[](与 GET /banned_items 同源)。 + * 勿再映射成 {item_id} — 否则 filter(typeof s === "string") 会得到空缓存(LSP)。 + * 其余资源复用单资源 helpers,避免 getAllConfigs 与专用路由双写(DRY)。 + */ 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("_")), - 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 ?? [], - }; - }), + settings: stripMeta(readCfg("settings.json") as Record | null), + areas: getAreas(), + permissions: getPermissions(), + banned_items: getBannedItems(), + clean: getClean(), + grids: getGrids(), + peace_filters: getPeaceFilters(), + questions: getQA(), }; } @@ -152,10 +127,10 @@ function createConfigRoutes({ json, projectRoot, listModules, getModuleTokens }: .map((r) => stripMetaDeep(r)); } - function getBannedItems(): Array<{ item_id: string }> { - return (arrayOrEmpty(readCfg("banned_items.json")) as Array) - .filter((i) => typeof i === "string" && i && !i.startsWith("_")) - .map((id) => ({ item_id: id })); + function getBannedItems(): string[] { + return (arrayOrEmpty(readCfg("banned_items.json")) as Array).filter( + (i) => typeof i === "string" && i && !i.startsWith("_") + ); } function getClean(): { item_max: number; poll_interval: number } { @@ -247,7 +222,7 @@ function createConfigRoutes({ json, projectRoot, listModules, getModuleTokens }: } if (requestPath === "/api/sfmc/banned_items") { if (method === "GET") { - json(res, { items: getBannedItems().map((x) => x.item_id) }); + json(res, { items: getBannedItems() }); return true; } } 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/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);