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
18 changes: 14 additions & 4 deletions db-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { ServiceRegistry } from "./service-registry.js";
import { TxRunner } from "./tx-runner.js";
import { registerEnabledBuiltinServices } from "./services/builtin-handlers.js";

import { readJson, writeJson } from "@sfmc-bds/sdk/node/config";
import { readJson } from "@sfmc-bds/sdk/node/config";

import { createModuleConfigRoutes } from "./routes/module-config-routes.js";
import { createDbRoutes } from "./routes/db-routes.js";
Expand All @@ -49,7 +49,7 @@ import { createMessagesRoutes } from "./routes/messages.js";
import { createModuleRoutes } from "./routes/modules.js";

import { forwardToQQBridge, makeLLBotConfig } from "./domain/bridge.js";
import { isEnabled, loadModuleLock, updateModuleState } from "./lib/module-state.js";
import { isEnabled, loadModuleLock, saveModuleLock, updateModuleState } from "./lib/module-state.js";
import { body as sharedBody, json as sharedJson } from "./lib/http.js";

if (!assertNodeVersion(22, 13)) {
Expand Down Expand Up @@ -153,12 +153,15 @@ function buildModuleList() {
id,
module_id: id,
name: configKey,
configKey,
config_key: configKey,
display_name: String((raw as Record<string, unknown>).name || configKey),
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,
// ConfigManager 认 installed!==false;已装包默认 true
installed: true,
requires: Array.isArray((raw as Record<string, unknown>).requires)
? ((raw as Record<string, unknown>).requires as unknown[]).filter(Boolean).map(String)
: [],
Expand Down Expand Up @@ -194,7 +197,8 @@ function setModuleEnabled(mod: { id: string; canDisable: boolean }, enabled: boo
// 直接更新启动时缓存的 lockFile(而非另读一份新副本),
// 否则 buildModuleList() 读的仍是旧缓存,导致启停后 enabled 状态不翻转。
updateModuleState(lockFile, mod.id, { enabled: !!enabled });
writeJson(env.MODULE_LOCK_PATH, lockFile);
// DRY:与 loadModuleLock 对称走 saveModuleLock,勿散落 writeJson
saveModuleLock(env.MODULE_LOCK_PATH, lockFile);
}

// ── 平台路由(非模块业务) ───────────────────────────────────
Expand All @@ -217,7 +221,13 @@ const messagesRoutes = createMessagesRoutes({
fromId
),
});
const configRoutes = createConfigRoutes({ json, projectRoot: env.PROJECT_ROOT });
const configRoutes = createConfigRoutes({
json,
projectRoot: env.PROJECT_ROOT,
// DIP:路由不读 lock/catalog/token 文件,由入口注入与 /modules 同源数据
listModules: () => buildModuleList() as Array<Record<string, unknown>>,
getModuleTokens: () => ({ ...moduleAuth.tokens }),
});
const moduleRoutesInstance = createModuleRoutes({
loadModuleCatalog,
buildModuleList: buildModuleList as unknown as () => Array<Record<string, unknown>>,
Expand Down
12 changes: 12 additions & 0 deletions db-server/src/routes/_shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,17 @@ export function jsonV2Fail(
sharedJson(res, payload, status);
}

/**
* v2 成功信封权威形态(DRY + LSP):
* 一律 `{ ok:true, ...fields }`,与 jsonV2Fail 对称;客户端已双认 ok/success。
*/
export function jsonV2Ok(
res: ServerResponse,
fields: Record<string, unknown> = {},
status = 200
): void {
sharedJson(res, { ...fields, ok: true }, status);
}

export {};

19 changes: 17 additions & 2 deletions db-server/src/routes/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ import { configPath, readJson, type ConfigName } from "@sfmc-bds/sdk/node/config
interface Deps {
json: (res: import("http").ServerResponse, data: Record<string, unknown>, status?: number) => void;
projectRoot: string;
/**
* 注入模块列表(与 GET /api/sfmc/modules 同源 — DRY)。
* 勿在本路由再读 catalog/lock(DIP:高层装配,路由只消费抽象)。
*/
listModules?: () => Array<Record<string, unknown>>;
/**
* 注入模块 HMAC token 表(仅 loopback 可达;供 SAPI host-bootstrap 注入
* set*ModuleContext,因 SAPI 不能读 data/module-tokens.json)。
*/
getModuleTokens?: () => Record<string, string>;
}

function isMetaKey(k: string): boolean {
Expand Down Expand Up @@ -54,13 +64,18 @@ function arrayOrEmpty(v: unknown): unknown[] {
return Array.isArray(v) ? v : [];
}

function createConfigRoutes({ json, projectRoot }: Deps) {
function createConfigRoutes({ json, projectRoot, listModules, getModuleTokens }: Deps) {
/** 仓顶服务 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 {
modules: [],
// 与 /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)
Expand Down
11 changes: 6 additions & 5 deletions db-server/src/routes/db-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import type { IncomingMessage, ServerResponse } from "node:http";
import { json as defaultJson, type Method } from "../lib/http.js";
import { jsonV2Fail, type ModuleAuth } from "./_shared.js";
import { jsonV2Fail, jsonV2Ok, type ModuleAuth } from "./_shared.js";
import type {
DefineTableRequest,
SchemaRegistry,
Expand Down Expand Up @@ -71,7 +71,7 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
const req2 = body as unknown as DefineTableRequest & { moduleId?: string };
delete (req2 as { moduleId?: string }).moduleId;
const result = deps.schemaRegistry!.define(moduleId, req2);
json(res, { success: true, table: result.table, created: result.created }, 200);
jsonV2Ok(res, { table: result.table, created: result.created });
} catch (e) {
jsonV2Fail(res, (e as Error).message, 400);
}
Expand All @@ -87,6 +87,7 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
const steps = Array.isArray(txReq.steps) ? txReq.steps : [];
const result = await deps.txRunner!.run({ moduleId, steps });
const status = result.ok ? 200 : 400;
// TxResponse/TxError 自身已带 ok 字段;原样回传保持契约
json(res, result as unknown as Record<string, unknown>, status);
} catch (e) {
jsonV2Fail(res, (e as Error).message, 500);
Expand All @@ -113,7 +114,7 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
return true;
}
const first = result.results[0];
json(res, { success: true, ...unwrapResult(first, cand.op) }, 200);
jsonV2Ok(res, unwrapResult(first, cand.op) as Record<string, unknown>);
} catch (e) {
if (e instanceof PermissionDeniedError) {
jsonV2Fail(res, e.message, 403, "permission_denied");
Expand All @@ -139,7 +140,7 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
jsonV2Fail(res, result.error, 400, result.code);
return true;
}
json(res, { success: true });
jsonV2Ok(res);
} catch (e) {
jsonV2Fail(res, (e as Error).message, 500);
}
Expand All @@ -163,7 +164,7 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
String(body.key),
body.value
);
json(res, { success: r.ok });
jsonV2Ok(res, { committed: r.ok });
}
} catch (e) {
jsonV2Fail(res, (e as Error).message, 500);
Expand Down
14 changes: 3 additions & 11 deletions db-server/src/routes/service-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,7 @@ export function createServiceRoutes(depsIn: Partial<ServiceRoutesDeps>) {
try {
payload = JSON.parse(rawInput);
} catch (e) {
json(
res,
{ ok: false, error: `invalid input json: ${(e as Error).message}`, code: "bad_request" },
400
);
jsonV2Fail(res, `invalid input json: ${(e as Error).message}`, 400, "bad_request");
return true;
}
}
Expand All @@ -80,15 +76,11 @@ export function createServiceRoutes(depsIn: Partial<ServiceRoutesDeps>) {
json(res, { ok: true, result: out.result });
} catch (e) {
if (e instanceof PermissionDeniedError) {
json(res, { ok: false, error: e.message, code: "permission_denied" }, 403);
jsonV2Fail(res, e.message, 403, "permission_denied");
return true;
}
const err = e as { status?: number; code?: string; message: string };
json(
res,
{ ok: false, error: err.message, code: err.code ?? "internal" },
err.status ?? 500
);
jsonV2Fail(res, err.message, err.status ?? 500, err.code ?? "internal");
}
return true;
}
Expand Down
20 changes: 20 additions & 0 deletions db-server/src/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,26 @@ test("jsonV2Fail: ok 方言 + extra(step) 合并(LSP/DRY)", async () => {
deepEqual(payload, { ok: false, error: "boom", code: "bad_step", step: 2 });
});

test("jsonV2Ok: 与 Fail 对称 ok 方言(LSP/DRY)", async () => {
const { jsonV2Ok } = await import("./routes/_shared.js");
const chunks: Buffer[] = [];
let statusCode = 0;
const res = {
writeHead(code: number) {
statusCode = code;
},
end(body?: string) {
if (body) chunks.push(Buffer.from(body));
},
setHeader() {},
} as unknown as import("node:http").ServerResponse;

jsonV2Ok(res, { rows: [1] });
const payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
equal(statusCode, 200);
deepEqual(payload, { ok: true, rows: [1] });
});

test("normalizeOrderBy: SDK field 与遗留 col / 数组互通(LSP)", async () => {
const { normalizeOrderBy } = await import("./lib/order-by.js");
deepEqual(normalizeOrderBy(undefined), []);
Expand Down
12 changes: 8 additions & 4 deletions modules/packages/afk/sapi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,17 @@ ModuleRegistry.register({
Permission.register("afk.clear.other", Permission.OP);
},
async init() {
const cfg = await config.get<AfkConfig>("afk");
const safe: AfkConfig = cfg ?? { afk_time: 120, step_time: 15 };
if (!cfg) {
// getAll 取整份 afk.json(afk_time/step_time);勿用 get("afk")(文件无此键)
const cfg = await config.getAll<AfkConfig>();
const hasTime = typeof cfg?.afk_time === "number";
const safe: AfkConfig = hasTime
? { afk_time: cfg.afk_time, step_time: typeof cfg.step_time === "number" ? cfg.step_time : 15 }
: { afk_time: 120, step_time: 15 };
if (!hasTime) {
debug.e("AFK", "configs/afk.json missing — using built-in defaults {afk_time:120, step_time:15}");
}

config.onChange("afk", (key, value) => {
config.onChange((key, value) => {
debug.i("AFK", `config.<${key}> changed: ${JSON.stringify(value)}`);
});

Expand Down
66 changes: 61 additions & 5 deletions modules/sdk/@sfmc-sdk/src/module-loader/internal/config-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ export type ConfigKey =
| "questions";

type ConfigCache = {
/** 启停态:同时按 catalog id 与 configKey 索引(OCP/LSP) */
modules: Map<string, boolean>;
/** catalog id → configKey */
moduleConfigKeys: Map<string, string>;
/** catalog id → HMAC module token(来自 configs/all.module_tokens) */
moduleTokens: Map<string, string>;
settings: Map<string, string>;
areas: any[];
permissions: Record<string, number>;
Expand All @@ -47,7 +52,17 @@ type ConfigCache = {
};

type AllConfigs = {
modules: Array<{ config_key?: string; configKey?: string; name?: string; enabled?: boolean; installed?: boolean }>;
modules: Array<{
id?: string;
module_id?: string;
config_key?: string;
configKey?: string;
name?: string;
enabled?: boolean;
installed?: boolean;
}>;
/** loopback 下发的模块 HMAC token;SAPI 无 fs,靠此注入身份(DIP) */
module_tokens?: Record<string, string>;
settings: Record<string, any>;
areas: Array<{
name?: string;
Expand Down Expand Up @@ -78,6 +93,8 @@ type AllConfigs = {
export class ConfigManager {
private static cache: ConfigCache = {
modules: new Map(),
moduleConfigKeys: new Map(),
moduleTokens: new Map(),
settings: new Map(),
areas: [],
permissions: {},
Expand Down Expand Up @@ -119,11 +136,25 @@ export class ConfigManager {
return ConfigManager._ready;
}

/**
* 模块是否启用。key 可为 catalog id(feature-afk)或 configKey(afk);
* populate 时双写索引,避免 ModuleRegistry / 旧 Modules 枚举键不一致(LSP)。
*/
static isEnabled(module: string): boolean {
if (!ConfigManager._ready) return false;
return ConfigManager.cache.modules.get(module) ?? false;
}

/** 取模块 HMAC token(来自 configs/all.module_tokens)。 */
static getModuleToken(moduleId: string): string {
return ConfigManager.cache.moduleTokens.get(moduleId) ?? "";
}

/** 取模块 configKey;无则空串。 */
static getModuleConfigKey(moduleId: string): string {
return ConfigManager.cache.moduleConfigKeys.get(moduleId) ?? "";
}

static getSetting<T>(key: string, defaultVal?: T): T {
const val = ConfigManager.cache.settings.get(key);
if (val === undefined) return defaultVal as T;
Expand Down Expand Up @@ -182,9 +213,9 @@ export class ConfigManager {
try {
const { modules } = JSON.parse(body);
ConfigManager.cache.modules.clear();
ConfigManager.cache.moduleConfigKeys.clear();
for (const m of modules) {
const key = m.config_key || m.configKey || m.name;
if (key) ConfigManager.cache.modules.set(key, !!m.enabled && m.installed !== false);
ConfigManager._indexModuleEntry(m);
}
ConfigManager._notifyModuleChanges();
} catch (e) {
Expand All @@ -194,11 +225,36 @@ export class ConfigManager {

// ── Internal ──

private static _indexModuleEntry(m: {
id?: string;
module_id?: string;
config_key?: string;
configKey?: string;
name?: string;
enabled?: boolean;
installed?: boolean;
}): void {
const id = String(m.id || m.module_id || "").trim();
const key = String(m.config_key || m.configKey || m.name || "").trim();
const enabled = !!m.enabled && m.installed !== false;
if (id) {
ConfigManager.cache.modules.set(id, enabled);
if (key) ConfigManager.cache.moduleConfigKeys.set(id, key);
}
if (key) ConfigManager.cache.modules.set(key, enabled);
}

private static populate(all: AllConfigs): void {
ConfigManager.cache.modules.clear();
ConfigManager.cache.moduleConfigKeys.clear();
ConfigManager.cache.moduleTokens.clear();
for (const m of all.modules || []) {
const key = m.config_key || m.configKey || m.name;
if (key) ConfigManager.cache.modules.set(key, !!m.enabled && m.installed !== false);
ConfigManager._indexModuleEntry(m);
}
for (const [id, token] of Object.entries(all.module_tokens || {})) {
if (id && typeof token === "string" && token) {
ConfigManager.cache.moduleTokens.set(id, token);
}
}

ConfigManager.cache.settings.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,8 @@ export const Modules = {
} as const;

export type ModuleKey = (typeof Modules)[keyof typeof Modules];
export type ModuleId = keyof typeof Modules;
/**
* OCP:模块 id 为开放字符串(catalog/manifest id,如 feature-afk);
* Modules 枚举仅作旧别名,新模块不必改此表。
*/
export type ModuleId = string;
Loading
Loading