Skip to content
Closed
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
18 changes: 15 additions & 3 deletions db-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ function buildModuleList() {
type: String((raw as Record<string, unknown>).type || "feature"),
description: String((raw as Record<string, unknown>).description || ""),
default_enabled: (raw as Record<string, unknown>).enabledByDefault !== false,
can_disable: (raw as Record<string, unknown>).canDisable !== false,
// 与 resolveModuleByKey 共用 moduleCanDisable,避免 list/disable 默认值漂移(LSP/DRY)
can_disable: moduleCanDisable(raw as Record<string, unknown>),
// ConfigManager 认 installed!==false;已装包默认 true
installed: true,
requires: Array.isArray((raw as Record<string, unknown>).requires)
Expand All @@ -183,14 +184,25 @@ function buildModuleList() {
.filter(Boolean);
}

/** catalog 省略 canDisable 时默认允许禁用(与 buildModuleList.can_disable 同源)。 */
function moduleCanDisable(raw: Record<string, unknown>): 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<string, unknown>).id || "") === k ||
String((m as Record<string, unknown>).configKey || (m as Record<string, unknown>).config_key || "") === k
) as { id: string; configKey: string; canDisable: boolean } | null;
) as Record<string, unknown> | 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) {
Expand Down
74 changes: 24 additions & 50 deletions db-server/src/routes/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> {
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<string, unknown> | null),
areas: (arrayOrEmpty(readJson(configPath(projectRoot, "areas.json"))) as Array<Record<string, unknown>>)
.filter((r) => r && r.module && r.dimension != null)
.map((r) => stripMetaDeep(r)),
permissions: (
arrayOrEmpty(readJson(configPath(projectRoot, "permissions.json"))) as Array<Record<string, unknown>>
)
.filter((r) => r && r.player_name)
.map((r) => stripMetaDeep(r)),
banned_items: (arrayOrEmpty(readJson(configPath(projectRoot, "banned_items.json"))) as Array<string>)
.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<Record<string, unknown>>)
.filter((r) => r && r.name)
.map((r) => stripMetaDeep(r)),
peace_filters: (
arrayOrEmpty(readJson(configPath(projectRoot, "peace_filters.json"))) as Array<Record<string, unknown>>
)
.filter((r) => r && r.family)
.map((r) => stripMetaDeep(r)),
questions: (arrayOrEmpty(readJson(configPath(projectRoot, "questions.json"))) as Array<Record<string, unknown>>)
.filter((r) => r && r.question)
.map((r, idx: number) => {
const clean = stripMetaDeep(r) as Record<string, unknown>;
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<string, unknown> | null);
return Object.entries(obj).map(([key, value]) => ({ key, value: jsonValue(value) }));
Expand Down Expand Up @@ -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<string, unknown> {
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<string, unknown> | 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | { item_id?: string }>;
clean: { item_max?: number; poll_interval?: number };
grids: Array<{
name: string;
Expand Down Expand Up @@ -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 = {
Expand Down
4 changes: 3 additions & 1 deletion modules/sdk/@sfmc-sdk/src/sapi/service/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
Expand Down
3 changes: 0 additions & 3 deletions sfmc/src/module-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 0 additions & 3 deletions sfmc/src/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(`⠄`)}
Expand Down
7 changes: 7 additions & 0 deletions tools/check-ootb.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading