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
16 changes: 3 additions & 13 deletions .github/workflows/ootb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,14 @@ jobs:
shell: pwsh
run: npm run build --workspaces --if-present

- name: Ensure configs via db-server defaults
- name: Seed configs via SDK ensure
shell: pwsh
env:
SFMC_ROOT: ${{ github.workspace }}
run: |
# 各服务 ensure 写入 configs/;此处预跑 db-server 一次生成骨架后退出
# 直接走 @sfmc-bds/sdk/node/config.ensureCoreConfigs,不启 db-server HTTP
New-Item -ItemType Directory -Force -Path configs | Out-Null
$p = Start-Process -FilePath node -ArgumentList "db-server/dist/index.js" -WorkingDirectory $PWD -WindowStyle Hidden -PassThru -RedirectStandardOutput "db-server/_seed.out" -RedirectStandardError "db-server/_seed.err"
$ok = $false
for ($i = 0; $i -lt 40; $i++) {
Start-Sleep -Milliseconds 250
try {
$r = Invoke-WebRequest -Uri "http://127.0.0.1:3001/api/health" -UseBasicParsing -TimeoutSec 1
if ($r.StatusCode -eq 200) { $ok = $true; break }
} catch {}
}
if ($p -and !$p.HasExited) { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue }
if (-not $ok) { Write-Host "WARN: db-server seed health not ready; configs may be partial" }
node tools/seed-configs.mjs
Get-ChildItem configs -Filter *.json | ForEach-Object { Write-Host "seeded $($_.Name)" }

- name: Run check-ootb
Expand Down
16 changes: 5 additions & 11 deletions bds-tools/src/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,8 @@
import {
configPath,
DEFAULT_BDS_UPDATER_CONFIG,
ensureJsonConfig,
isConfigMetaKey,
loadEnsuredConfig,
resolveRuntimeRoot,
stripConfigMeta,
withConfigSchema,
type BdsUpdaterConfig,
} from "@sfmc-bds/sdk/node/config";
import fs from "node:fs";
Expand All @@ -32,15 +29,12 @@ export const ROLLBACK_MARKER: string = path.join(SCRIPT_DIR, ".last_update_rollb
/** 加载并解析 bds_updater.json。
* 启动时 ensure 文件存在,避免 wizard 没跑时 bds_updater.json 缺失导致 loadConfig 抛错。 */
export function loadConfig(): BdsUpdaterConfig {
const raw = ensureJsonConfig<Record<string, unknown>>(
return loadEnsuredConfig(
ROOT_DIR,
"bds_updater.json",
withConfigSchema({ ...DEFAULT_BDS_UPDATER_CONFIG } as Record<string, unknown>, "bds_updater")
);
for (const k of Object.keys(raw)) {
if (isConfigMetaKey(k)) delete raw[k];
}
return stripConfigMeta(raw) as BdsUpdaterConfig;
"bds_updater",
{ ...DEFAULT_BDS_UPDATER_CONFIG } as Record<string, unknown>
) as BdsUpdaterConfig;
}

/** 解析后的 BDS 路径 / 备份目录 */
Expand Down
29 changes: 12 additions & 17 deletions db-server/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,12 @@
*/

import {
configPath,
DEFAULT_DB_CONFIG,
DEFAULT_PERMISSIONS,
DEFAULT_QQ_CONFIG,
ensureJson,
ensureJsonConfig,
isConfigMetaKey,
ensureCoreConfigs,
loadEnsuredConfig,
modulePath,
resolveRuntimeRoot,
withConfigSchema,
DEFAULT_DB_CONFIG,
DEFAULT_QQ_CONFIG,
} from "@sfmc-bds/sdk/node/config";
import { isAbsolute, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
Expand Down Expand Up @@ -46,32 +42,31 @@ export interface EnvConfig {

export function loadEnv(): EnvConfig {
const PROJECT_ROOT = resolveRuntimeRoot(resolve(__dirname, "..", ".."));
/* 启动时确认 db/qq config 存在;不存在就写带默认值的骨架。
/* 启动时确认 db/qq/permissions 存在;不存在就写带默认值的骨架。
* 不依赖 wizard:wizard 只填字段,骨架由服务自己 ensure。 */
const dbconfig = ensureJsonConfig<Record<string, unknown>>(
ensureCoreConfigs(PROJECT_ROOT, ["db_config", "qq_config", "permissions"]);
const dbconfig = loadEnsuredConfig(
PROJECT_ROOT,
"db_config.json",
withConfigSchema({ ...DEFAULT_DB_CONFIG } as Record<string, unknown>, "db_config")
"db_config",
{ ...DEFAULT_DB_CONFIG } as Record<string, unknown>
);
const qqconfig = ensureJsonConfig<Record<string, unknown>>(
const qqconfig = loadEnsuredConfig(
PROJECT_ROOT,
"qq_config.json",
withConfigSchema({ ...DEFAULT_QQ_CONFIG } as Record<string, unknown>, "qq_config")
"qq_config",
{ ...DEFAULT_QQ_CONFIG } as Record<string, unknown>
);
ensureJson(configPath(PROJECT_ROOT, "permissions.json"), DEFAULT_PERMISSIONS);

// ── 优先级:JSON > 系统环境变量 > 默认值 ───────────────────────
const envBaseline = { ...process.env };

// 元数据键:`$schema`、`_` 前缀(如 _comment)不写 env、不打日志
for (const [k, v] of Object.entries(dbconfig)) {
if (isConfigMetaKey(k)) continue;
const envKey = k.replace(/([A-Z])/g, "_$1").toUpperCase();
process.env[envKey] = String(v);
log.info(`db_config::${k} -> process.env.${envKey} = ${String(v)}`);
}
for (const [k, v] of Object.entries(qqconfig)) {
if (isConfigMetaKey(k)) continue;
const envKey = k.replace(/([A-Z])/g, "_$1").toUpperCase();
if (process.env[envKey] === undefined) {
process.env[envKey] = String(v);
Expand Down
99 changes: 85 additions & 14 deletions modules/sdk/@sfmc-sdk/src/node/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ export interface DBConfig {
[key: string]: unknown;
}

export interface PermissionsConfig {
permissions?: Array<{ player_name?: string; level?: number; [key: string]: unknown }>;
/** permissions.json 根为数组(与 permissions.schema.json 一致;勿写成 { permissions: [] }) */
export type PermissionsConfig = Array<{
player_name: string;
level: number;
[key: string]: unknown;
}
}>;

export interface RuntimeConfig {
runtime_root?: string;
Expand Down Expand Up @@ -137,20 +139,15 @@ export type ConfigSchemaId =
| "module_catalog";

/**
* 生成文件内 `$schema` 相对路径(相对 configs/ 或 packs/)。
* IDE 用;运行时加载器应忽略该键。
* 生成文件内 `$schema` 相对路径。
* configs/、packs/、modules/ 均在仓顶下一层,相对 node_modules 深度相同;
* `from` 仅作调用点语义标注,便于日后若布局分叉再按位置扩展(OCP)。
*/
export function configSchemaRef(
schemaId: ConfigSchemaId,
from: "configs" | "packs" | "modules" = "configs"
_from: "configs" | "packs" | "modules" = "configs"
): string {
const rel =
from === "configs"
? "../node_modules/@sfmc-bds/sdk/schemas"
: from === "packs"
? "../node_modules/@sfmc-bds/sdk/schemas"
: "../node_modules/@sfmc-bds/sdk/schemas";
return `${rel}/${schemaId}.schema.json`;
return `../node_modules/@sfmc-bds/sdk/schemas/${schemaId}.schema.json`;
}

/** 在对象根写入 `$schema`(不覆盖已有);数组根勿调用。 */
Expand Down Expand Up @@ -215,7 +212,7 @@ export const DEFAULT_BDS_UPDATER_CONFIG: BdsUpdaterConfig = {
};

/** permissions.json 根为数组;默认空表 */
export const DEFAULT_PERMISSIONS: Array<{ player_name: string; level: number }> = [];
export const DEFAULT_PERMISSIONS: PermissionsConfig = [];

/** remote.json 骨架(enroll 前) */
export const DEFAULT_REMOTE_CONFIG: RemoteConfig = {
Expand Down Expand Up @@ -341,4 +338,78 @@ export function ensureJson<T>(filePath: string, seed: T = {} as T): T {
*/
export function ensureJsonConfig<T>(root: string, name: ConfigName, seed: T = {} as T): T {
return ensureJson<T>(configPath(root, name), seed);
}

/**
* ensure 带 `$schema` 的配置对象(DRY:禁止各服务手写 ensureJsonConfig+withConfigSchema)。
* 返回磁盘内容(可能含元数据键);运行时业务请用 {@link loadEnsuredConfig}。
*/
export function ensureSchemaConfig<T extends Record<string, unknown>>(
root: string,
name: ConfigName,
schemaId: ConfigSchemaId,
defaults: T,
from: "configs" | "packs" | "modules" = "configs"
): T & { $schema?: string } {
return ensureJsonConfig(
root,
name,
withConfigSchema({ ...defaults } as Record<string, unknown>, schemaId, from)
) as T & { $schema?: string };
}

/**
* ensure 后剥离元数据,返回可交给业务逻辑的纯配置(LSP:与无 `$schema` 的契约一致)。
*/
export function loadEnsuredConfig<T extends Record<string, unknown>>(
root: string,
name: ConfigName,
schemaId: ConfigSchemaId,
defaults: T,
from: "configs" | "packs" | "modules" = "configs"
): T {
return stripConfigMeta(
ensureSchemaConfig(root, name, schemaId, defaults, from) as Record<string, unknown>
) as T;
}

/** 平台核心配置种子集合(扩展新文件时只改此处 + switch) */
export type CoreConfigKind = "db_config" | "qq_config" | "bds_updater" | "permissions" | "remote";

/**
* 按需 ensure 平台核心配置。新种类加到 CoreConfigKind + switch 即可(OCP)。
* permissions 根为数组,不能走 withConfigSchema。
*/
export function ensureCoreConfigs(root: string, kinds: readonly CoreConfigKind[]): void {
for (const kind of kinds) {
switch (kind) {
case "db_config":
ensureSchemaConfig(root, "db_config.json", "db_config", {
...DEFAULT_DB_CONFIG,
} as Record<string, unknown>);
break;
case "qq_config":
ensureSchemaConfig(root, "qq_config.json", "qq_config", {
...DEFAULT_QQ_CONFIG,
} as Record<string, unknown>);
break;
case "bds_updater":
ensureSchemaConfig(root, "bds_updater.json", "bds_updater", {
...DEFAULT_BDS_UPDATER_CONFIG,
} as Record<string, unknown>);
break;
case "permissions":
ensureJson(configPath(root, "permissions.json"), DEFAULT_PERMISSIONS);
break;
case "remote":
ensureSchemaConfig(root, "remote.json", "remote", {
...DEFAULT_REMOTE_CONFIG,
} as Record<string, unknown>);
break;
default: {
const _exhaustive: never = kind;
void _exhaustive;
}
}
}
}
36 changes: 18 additions & 18 deletions qq-bridge/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@
import {
configPath,
DEFAULT_QQ_CONFIG,
ensureJsonConfig,
readJson,
loadEnsuredConfig,
resolveRuntimeRoot,
withConfigSchema,
stripConfigMeta,
} from "@sfmc-bds/sdk/node/config";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
Expand All @@ -28,28 +27,29 @@ export const ROOT_DIR: string = resolveRuntimeRoot(resolve(__dirname, "..", ".."
export const CFG_PATH: string = configPath(ROOT_DIR, "qq_config.json");

function applyDefaults(raw: Partial<QQBridgeConfig>): QQBridgeConfig {
/* 以 SDK DEFAULT_QQ_CONFIG 为唯一缺省权威(DRY/LSP),再叠运行时派生字段 */
const merged = { ...DEFAULT_QQ_CONFIG, ...raw } as Partial<QQBridgeConfig> & Record<string, unknown>;
const stripped = stripConfigMeta(merged);
return {
qq_enabled: raw.qq_enabled !== false,
qq_ws_port: parseInt(String(raw.qq_ws_port ?? "3002"), 10),
qq_group_id: String(raw.qq_group_id ?? ""),
bridge_channel_id: String(raw.bridge_channel_id ?? ""),
db_host: String(raw.db_host ?? "127.0.0.1"),
db_port: parseInt(String(raw.db_port ?? "3001"), 10),
mctoqq_prefix: String(raw.mctoqq_prefix ?? "[MC]"),
...raw,
qq_enabled: stripped.qq_enabled !== false,
qq_ws_port: parseInt(String(stripped.qq_ws_port ?? DEFAULT_QQ_CONFIG.qq_ws_port ?? 3002), 10),
qq_group_id: String(stripped.qq_group_id ?? DEFAULT_QQ_CONFIG.qq_group_id ?? "0"),
bridge_channel_id: String(stripped.bridge_channel_id ?? DEFAULT_QQ_CONFIG.bridge_channel_id ?? ""),
db_host: String(stripped.db_host ?? "127.0.0.1"),
db_port: parseInt(String(stripped.db_port ?? "3001"), 10),
mctoqq_prefix: String(stripped.mctoqq_prefix ?? DEFAULT_QQ_CONFIG.mctoqq_prefix ?? "[MC]"),
...stripped,
};
}

function readFromDisk(): QQBridgeConfig {
ensureJsonConfig(
const raw = loadEnsuredConfig(
ROOT_DIR,
"qq_config.json",
withConfigSchema({ ...DEFAULT_QQ_CONFIG } as Record<string, unknown>, "qq_config")
"qq_config",
{ ...DEFAULT_QQ_CONFIG } as Record<string, unknown>
);
const raw = readJson<Partial<QQBridgeConfig>>(CFG_PATH);
if (!raw) throw new Error(`配置文件不存在或无法解析: ${CFG_PATH}`);
const { $schema: _s, ...rest } = raw as Partial<QQBridgeConfig> & { $schema?: string };
return applyDefaults(rest);
return applyDefaults(raw as Partial<QQBridgeConfig>);
}

/** 进程启动时加载一次。失败直接退出,与旧实现一致。 */
Expand All @@ -74,4 +74,4 @@ export function reloadInto(cfg: QQBridgeConfig): void {
// reload 失败不抛,旧实现也是只 log
log.error(`重载配置失败: ${(e as Error).message}`);
}
}
}
14 changes: 8 additions & 6 deletions sfmc/src/remote-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { WebSocket } from "ws";
import {
configPath,
DEFAULT_REMOTE_CONFIG,
ensureJson,
loadEnsuredConfig,
writeJson,
withConfigSchema,
type RemoteConfig,
Expand Down Expand Up @@ -38,12 +38,14 @@ let stopping = false;

const HEARTBEAT_INTERVAL_MS = 20_000;

/** 启动时确保 remote.json 存在,返回现有或空配置。 */
/** 启动时确保 remote.json 存在,返回现有或空配置(已剥离 $schema)。 */
function loadConfig(): RemoteConfig {
return ensureJson<RemoteConfig>(
configPath(ROOT, "remote.json"),
withConfigSchema({ ...DEFAULT_REMOTE_CONFIG } as Record<string, unknown>, "remote") as RemoteConfig
);
return loadEnsuredConfig(
ROOT,
"remote.json",
"remote",
{ ...DEFAULT_REMOTE_CONFIG } as Record<string, unknown>
) as RemoteConfig;
}

function writeConfig(config: RemoteConfig): void {
Expand Down
Loading