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
77 changes: 37 additions & 40 deletions db-server/src/routes/db-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,53 +92,50 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
}

// 交互式事务会话 — 供 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<string, unknown>, 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<string, unknown>;
const sessionOps: Array<{
path: string;
requireTxId?: boolean;
requireStep?: boolean;
run: (args: { txId: string; step?: TxStep | undefined }) => Promise<SessionReply> | 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<SessionReply>,
},
{
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<string, unknown>, 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<string, unknown>, 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<string, unknown>, result.ok ? 200 : 400);
const result = await op.run(step !== undefined ? { txId, step } : { txId });
json(res, result as Record<string, unknown>, result.ok ? 200 : 400);
} catch (e) {
jsonV2Fail(res, (e as Error).message, 500);
}
Expand Down
14 changes: 9 additions & 5 deletions db-server/src/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
33 changes: 8 additions & 25 deletions db-server/src/services/builtin-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 注册内置插件,返回已注册插件数 */
Expand All @@ -51,30 +37,27 @@ export function registerEnabledBuiltinServices(
return n;
}

/** 热启用:只注册单个内置插件(若尚未注册) */
/** 热启用:只注册单个内置插件(若尚未注册) — 以 registry.moduleId 为权威,勿硬编码 service 名(DRY) */
export function registerBuiltinPluginForModule(
registry: ServiceRegistry,
deps: BuiltinServiceDeps,
moduleId: string
): 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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion sfmc/src/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading