diff --git a/db-server/src/routes/db-routes.ts b/db-server/src/routes/db-routes.ts index dd0b0d62..eff9c735 100644 --- a/db-server/src/routes/db-routes.ts +++ b/db-server/src/routes/db-routes.ts @@ -92,53 +92,50 @@ export function createDbRoutes(depsIn: Partial) { } // 交互式事务会话 — 供 SDK 在回调内读回 query/get/call 结果 - if (path === "/api/sfmc/db/tx/begin") { - try { - const result = deps.txRunner!.beginSession(moduleId); - json(res, result as unknown as Record, result.ok ? 200 : 400); - } catch (e) { - jsonV2Fail(res, (e as Error).message, 500); - } - return true; - } - if (path === "/api/sfmc/db/tx/step") { + // OCP/DRY:新会话动词只往表里加一项,勿再复制 try/catch 四份 + type SessionReply = { ok: boolean } & Record; + const sessionOps: Array<{ + path: string; + requireTxId?: boolean; + requireStep?: boolean; + run: (args: { txId: string; step?: TxStep | undefined }) => Promise | SessionReply; + }> = [ + { + path: "/api/sfmc/db/tx/begin", + run: () => deps.txRunner!.beginSession(moduleId) as SessionReply, + }, + { + path: "/api/sfmc/db/tx/step", + requireTxId: true, + requireStep: true, + run: ({ txId, step }) => deps.txRunner!.stepSession(txId, moduleId, step!) as Promise, + }, + { + path: "/api/sfmc/db/tx/commit", + requireTxId: true, + run: ({ txId }) => deps.txRunner!.commitSession(txId, moduleId) as SessionReply, + }, + { + path: "/api/sfmc/db/tx/rollback", + requireTxId: true, + run: ({ txId }) => deps.txRunner!.rollbackSession(txId, moduleId) as SessionReply, + }, + ]; + for (const op of sessionOps) { + if (path !== op.path) continue; try { const txId = String(body.txId || ""); const step = body.step as TxStep | undefined; - if (!txId || !step || typeof step !== "object" || !("op" in step)) { - jsonV2Fail(res, "tx/step 需要 { txId, step }", 400); + if (op.requireTxId && !txId) { + jsonV2Fail(res, `${op.path.split("/").pop()} 需要 { txId }`, 400); return true; } - const result = await deps.txRunner!.stepSession(txId, moduleId, step); - json(res, result as unknown as Record, result.ok ? 200 : 400); - } catch (e) { - jsonV2Fail(res, (e as Error).message, 500); - } - return true; - } - if (path === "/api/sfmc/db/tx/commit") { - try { - const txId = String(body.txId || ""); - if (!txId) { - jsonV2Fail(res, "tx/commit 需要 { txId }", 400); - return true; - } - const result = deps.txRunner!.commitSession(txId, moduleId); - json(res, result as unknown as Record, result.ok ? 200 : 400); - } catch (e) { - jsonV2Fail(res, (e as Error).message, 500); - } - return true; - } - if (path === "/api/sfmc/db/tx/rollback") { - try { - const txId = String(body.txId || ""); - if (!txId) { - jsonV2Fail(res, "tx/rollback 需要 { txId }", 400); + if (op.requireStep && (!step || typeof step !== "object" || !("op" in step))) { + jsonV2Fail(res, "tx/step 需要 { txId, step }", 400); return true; } - const result = deps.txRunner!.rollbackSession(txId, moduleId); - json(res, result as unknown as Record, result.ok ? 200 : 400); + const result = await op.run(step !== undefined ? { txId, step } : { txId }); + json(res, result as Record, result.ok ? 200 : 400); } catch (e) { jsonV2Fail(res, (e as Error).message, 500); } diff --git a/db-server/src/runtime.test.ts b/db-server/src/runtime.test.ts index 6e9fa3d9..01bf4d5a 100644 --- a/db-server/src/runtime.test.ts +++ b/db-server/src/runtime.test.ts @@ -207,19 +207,23 @@ test("module-auth: ensureModuleToken / revoke 复用 secret(DRY)", async () => { equal(revokeModuleToken(auth, "feature-afk"), false); }); -test("builtin-handlers: 热禁用按 serviceNames 卸载(OCP)", async () => { - const { BUILTIN_SERVICE_PLUGINS, unregisterBuiltinPluginForModule } = await import( +test("builtin-handlers: 热禁用按 moduleId 卸载(DRY/OCP)", async () => { + const { unregisterBuiltinPluginForModule, registerBuiltinPluginForModule } = await import( "./services/builtin-handlers.js" ); - const economy = BUILTIN_SERVICE_PLUGINS.find((p) => p.moduleId === "feature-economy"); - equal(!!economy?.serviceNames?.includes("economy.account.get"), true); const reg = new ServiceRegistry(); reg.registerHandler("feature-economy", "economy.account.get", async () => ({})); reg.registerHandler("feature-economy", "economy.account.debit", async () => ({})); + reg.registerHandler("other-mod", "other.ping", async () => ({})); equal(unregisterBuiltinPluginForModule(reg, "feature-economy"), 2); - equal(reg.list().length, 0); + equal(reg.list().length, 1); + equal(reg.list()[0]?.moduleId, "other-mod"); equal(unregisterBuiltinPluginForModule(reg, "feature-unknown"), 0); + + // 已有同 moduleId handler 时热启用跳过 + equal(registerBuiltinPluginForModule(reg, { query: (() => []) as never, db: {} as never }, "feature-economy"), true); + equal(registerBuiltinPluginForModule(reg, { query: (() => []) as never, db: {} as never }, "feature-economy"), false); }); test("TxRunner 交互会话: step 中途读回 insert/get(PR #31 leftover)", async () => { diff --git a/db-server/src/services/builtin-handlers.ts b/db-server/src/services/builtin-handlers.ts index 260de84d..84f284a8 100644 --- a/db-server/src/services/builtin-handlers.ts +++ b/db-server/src/services/builtin-handlers.ts @@ -15,25 +15,11 @@ export type BuiltinServiceDeps = { query: QueryFn; db: DatabaseSync }; export type BuiltinServicePlugin = { moduleId: string; register: (registry: ServiceRegistry, deps: BuiltinServiceDeps) => void; - /** 与 register 写入的 handler 名对齐,供热禁用 unregister */ - serviceNames?: string[]; }; /** 内置 service 插件清单 — 唯一扩展点 */ export const BUILTIN_SERVICE_PLUGINS: BuiltinServicePlugin[] = [ - { - moduleId: "feature-economy", - register: registerEconomyHandlers, - serviceNames: [ - "economy.account.get", - "economy.account.debit", - "economy.account.credit", - "economy.account.transfer", - "economy.dailyTasks.list", - "economy.dailyTasks.submit", - "economy.stats.monthly", - ], - }, + { moduleId: "feature-economy", register: registerEconomyHandlers }, ]; /** 按 enabledSet 注册内置插件,返回已注册插件数 */ @@ -51,7 +37,7 @@ export function registerEnabledBuiltinServices( return n; } -/** 热启用:只注册单个内置插件(若尚未注册) */ +/** 热启用:只注册单个内置插件(若尚未注册) — 以 registry.moduleId 为权威,勿硬编码 service 名(DRY) */ export function registerBuiltinPluginForModule( registry: ServiceRegistry, deps: BuiltinServiceDeps, @@ -59,22 +45,19 @@ export function registerBuiltinPluginForModule( ): boolean { const plugin = BUILTIN_SERVICE_PLUGINS.find((p) => p.moduleId === moduleId); if (!plugin) return false; - const already = (plugin.serviceNames ?? []).some((n) => registry.list().some((h) => h.name === n)); + const already = registry.list().some((h) => h.moduleId === moduleId); if (already) return false; plugin.register(registry, deps); return true; } -/** 热禁用:卸掉该模块的内置 handler */ +/** 热禁用:按 moduleId 卸掉该模块全部 handler(勿维护 serviceNames 副本 — DRY/OCP) */ export function unregisterBuiltinPluginForModule(registry: ServiceRegistry, moduleId: string): number { - const plugin = BUILTIN_SERVICE_PLUGINS.find((p) => p.moduleId === moduleId); - if (!plugin?.serviceNames) return 0; let n = 0; - for (const name of plugin.serviceNames) { - if (registry.list().some((h) => h.name === name)) { - registry.unregisterHandler(name); - n += 1; - } + for (const h of registry.list()) { + if (h.moduleId !== moduleId) continue; + registry.unregisterHandler(h.name); + n += 1; } return n; } 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 7b340e0d..a86261f5 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 @@ -278,7 +278,13 @@ export class ConfigManager { // 权威契约为 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 || "") : "")) + .map((s) => + typeof s === "string" + ? s + : s && typeof s === "object" + ? String((s as { item_id?: unknown }).item_id || "") + : "" + ) .filter((id) => !!id); if (all.clean) { diff --git a/sfmc/src/repl.ts b/sfmc/src/repl.ts index e602f302..f43a1bf4 100644 --- a/sfmc/src/repl.ts +++ b/sfmc/src/repl.ts @@ -21,7 +21,7 @@ function setRaw(v: boolean): void { } catch {} } -/** HELP 行首:染色后的 module 别名标签(权威来源 paintModuleCmdAlias / MODULE_CMD_NAMES)。 */ +/** HELP 行首:染色后的 module 别名拼接(权威来源 paintModuleCmdAlias / MODULE_CMD_NAMES)。 */ const MODULE_HELP_LABEL = paintModuleCmdAlias(c.green); const welcome = `\n