Skip to content

Commit d2cff75

Browse files
committed
2 parents 525e69f + 5250c43 commit d2cff75

12 files changed

Lines changed: 359 additions & 162 deletions

File tree

.github/workflows/ootb.yml

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,14 @@ jobs:
3131
shell: pwsh
3232
run: npm run build --workspaces --if-present
3333

34-
- name: Ensure configs via db-server defaults
34+
- name: Seed configs via SDK ensure
3535
shell: pwsh
3636
env:
3737
SFMC_ROOT: ${{ github.workspace }}
3838
run: |
39-
# 各服务 ensure 写入 configs/;此处预跑 db-server 一次生成骨架后退出
39+
# 直接走 @sfmc-bds/sdk/node/config.ensureCoreConfigs,不启 db-server HTTP
4040
New-Item -ItemType Directory -Force -Path configs | Out-Null
41-
$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"
42-
$ok = $false
43-
for ($i = 0; $i -lt 40; $i++) {
44-
Start-Sleep -Milliseconds 250
45-
try {
46-
$r = Invoke-WebRequest -Uri "http://127.0.0.1:3001/api/health" -UseBasicParsing -TimeoutSec 1
47-
if ($r.StatusCode -eq 200) { $ok = $true; break }
48-
} catch {}
49-
}
50-
if ($p -and !$p.HasExited) { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue }
51-
if (-not $ok) { Write-Host "WARN: db-server seed health not ready; configs may be partial" }
41+
node tools/seed-configs.mjs
5242
Get-ChildItem configs -Filter *.json | ForEach-Object { Write-Host "seeded $($_.Name)" }
5343
5444
- name: Run check-ootb

bds-tools/src/paths.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,8 @@
88
import {
99
configPath,
1010
DEFAULT_BDS_UPDATER_CONFIG,
11-
ensureJsonConfig,
12-
isConfigMetaKey,
11+
loadEnsuredConfig,
1312
resolveRuntimeRoot,
14-
stripConfigMeta,
15-
withConfigSchema,
1613
type BdsUpdaterConfig,
1714
} from "@sfmc-bds/sdk/node/config";
1815
import fs from "node:fs";
@@ -32,15 +29,12 @@ export const ROLLBACK_MARKER: string = path.join(SCRIPT_DIR, ".last_update_rollb
3229
/** 加载并解析 bds_updater.json。
3330
* 启动时 ensure 文件存在,避免 wizard 没跑时 bds_updater.json 缺失导致 loadConfig 抛错。 */
3431
export function loadConfig(): BdsUpdaterConfig {
35-
const raw = ensureJsonConfig<Record<string, unknown>>(
32+
return loadEnsuredConfig(
3633
ROOT_DIR,
3734
"bds_updater.json",
38-
withConfigSchema({ ...DEFAULT_BDS_UPDATER_CONFIG } as Record<string, unknown>, "bds_updater")
39-
);
40-
for (const k of Object.keys(raw)) {
41-
if (isConfigMetaKey(k)) delete raw[k];
42-
}
43-
return stripConfigMeta(raw) as BdsUpdaterConfig;
35+
"bds_updater",
36+
{ ...DEFAULT_BDS_UPDATER_CONFIG } as Record<string, unknown>
37+
) as BdsUpdaterConfig;
4438
}
4539

4640
/** 解析后的 BDS 路径 / 备份目录 */

db-server/src/env.ts

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,12 @@
33
*/
44

55
import {
6-
configPath,
7-
DEFAULT_DB_CONFIG,
8-
DEFAULT_PERMISSIONS,
9-
DEFAULT_QQ_CONFIG,
10-
ensureJson,
11-
ensureJsonConfig,
12-
isConfigMetaKey,
6+
ensureCoreConfigs,
7+
loadEnsuredConfig,
138
modulePath,
149
resolveRuntimeRoot,
15-
withConfigSchema,
10+
DEFAULT_DB_CONFIG,
11+
DEFAULT_QQ_CONFIG,
1612
} from "@sfmc-bds/sdk/node/config";
1713
import { isAbsolute, join, resolve } from "node:path";
1814
import { fileURLToPath } from "node:url";
@@ -46,32 +42,31 @@ export interface EnvConfig {
4642

4743
export function loadEnv(): EnvConfig {
4844
const PROJECT_ROOT = resolveRuntimeRoot(resolve(__dirname, "..", ".."));
49-
/* 启动时确认 db/qq config 存在;不存在就写带默认值的骨架。
45+
/* 启动时确认 db/qq/permissions 存在;不存在就写带默认值的骨架。
5046
* 不依赖 wizard:wizard 只填字段,骨架由服务自己 ensure。 */
51-
const dbconfig = ensureJsonConfig<Record<string, unknown>>(
47+
ensureCoreConfigs(PROJECT_ROOT, ["db_config", "qq_config", "permissions"]);
48+
const dbconfig = loadEnsuredConfig(
5249
PROJECT_ROOT,
5350
"db_config.json",
54-
withConfigSchema({ ...DEFAULT_DB_CONFIG } as Record<string, unknown>, "db_config")
51+
"db_config",
52+
{ ...DEFAULT_DB_CONFIG } as Record<string, unknown>
5553
);
56-
const qqconfig = ensureJsonConfig<Record<string, unknown>>(
54+
const qqconfig = loadEnsuredConfig(
5755
PROJECT_ROOT,
5856
"qq_config.json",
59-
withConfigSchema({ ...DEFAULT_QQ_CONFIG } as Record<string, unknown>, "qq_config")
57+
"qq_config",
58+
{ ...DEFAULT_QQ_CONFIG } as Record<string, unknown>
6059
);
61-
ensureJson(configPath(PROJECT_ROOT, "permissions.json"), DEFAULT_PERMISSIONS);
6260

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

66-
// 元数据键:`$schema`、`_` 前缀(如 _comment)不写 env、不打日志
6764
for (const [k, v] of Object.entries(dbconfig)) {
68-
if (isConfigMetaKey(k)) continue;
6965
const envKey = k.replace(/([A-Z])/g, "_$1").toUpperCase();
7066
process.env[envKey] = String(v);
7167
log.info(`db_config::${k} -> process.env.${envKey} = ${String(v)}`);
7268
}
7369
for (const [k, v] of Object.entries(qqconfig)) {
74-
if (isConfigMetaKey(k)) continue;
7570
const envKey = k.replace(/([A-Z])/g, "_$1").toUpperCase();
7671
if (process.env[envKey] === undefined) {
7772
process.env[envKey] = String(v);

modules/sdk/@sfmc-sdk/src/logs/terminal-progress.ts

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ export interface TerminalProgressOptions {
2626
export interface ProgressHandle {
2727
start(total: number, startValue?: number, payload?: Record<string, string>): void;
2828
update(value: number, payload?: Record<string, string>): void;
29+
/**
30+
* 修正总量刻度(未知→已知 Content-Length / finish 补总量)。
31+
* LSP:勿用重 start 冒充改 total;实现须保留当前 value,并允许随后 update 按新刻度出帧。
32+
*/
33+
setTotal(total: number): void;
2934
stop(): void;
3035
/** 当前是否已 start 且未 stop */
3136
readonly active: boolean;
@@ -115,7 +120,7 @@ export interface DownloadProgressBinderOptions {
115120
* BDS 更新与 CF 包下载共用,避免两处复制 lastTime/lastLoaded 环路(DRY)。
116121
*
117122
* LSP:与 httpDownload 契约对齐——中途 total=0(无 Content-Length)仍可回调;
118-
* 总量一旦变为已知(含 finish 用 finalBytes 补总量),须重设 bar 刻度,避免一直卡在占位 1MB。
123+
* 总量一旦变为已知(含 finish 用 finalBytes 补总量),经 setTotal 修正刻度,避免卡在占位 1MB。
119124
*/
120125
export function bindByteProgressToBar(
121126
bar: ProgressHandle,
@@ -138,16 +143,17 @@ export function bindByteProgressToBar(
138143
const speedText = formatDownloadSpeed(speed);
139144

140145
if (!started) {
141-
bar.start(totalMb, 0, { speed: "0 KB/s" });
146+
/* 首帧 startValue=已下载量,避免非 TTY 先打出虚假 0%(LSP) */
147+
bar.start(totalMb, dlMb, { speed: speedText });
142148
started = true;
143149
knownTotal = hasTotal;
144150
} else if (hasTotal && !knownTotal) {
145-
/* 未知→已知:重 start 以修正 total(ProgressHandle 无独立 setTotal) */
146-
bar.start(totalMb, dlMb, { speed: speedText });
151+
/* 未知→已知:setTotal 修正刻度,勿重 start(OCP/LSP) */
152+
bar.setTotal(totalMb);
147153
knownTotal = true;
148-
} else {
149-
bar.update(dlMb, { speed: speedText });
150154
}
155+
/* 始终 update:TTY 重绘 / 非 TTY 步进日志与 payload 对齐 */
156+
bar.update(dlMb, { speed: speedText });
151157

152158
if (speedSampleMs <= 0 || now - lastTime >= speedSampleMs) {
153159
lastTime = now;
@@ -250,11 +256,21 @@ export function createTerminalProgress(opts: TerminalProgressOptions = {}): Prog
250256
if (useBar) {
251257
if (!id) register();
252258
draw();
253-
} else if (opts.logger) {
254-
opts.logger(`进度 0% (0/${total})`);
255-
lastLoggedPct = 0;
259+
} else {
260+
/* 尊重 startValue(LSP);与 update 共用 emitNonTty(DRY) */
261+
emitNonTty(true);
256262
}
257263
},
264+
setTotal(t) {
265+
if (!started || stopped) return;
266+
total = t > 0 ? t : 1;
267+
/* 刻度变更后允许按新百分比再出帧(含从错误占位%回落到真实%) */
268+
lastLoggedPct = -1;
269+
if (useBar) {
270+
draw();
271+
}
272+
/* 非 TTY:留给随后 update 打一条正确刻度日志,避免 start+update 双打 */
273+
},
258274
update(v, p) {
259275
if (!started || stopped) return;
260276
value = v;
@@ -263,16 +279,7 @@ export function createTerminalProgress(opts: TerminalProgressOptions = {}): Prog
263279
draw();
264280
return;
265281
}
266-
if (!opts.logger) return;
267-
const pct = total > 0 ? (value / total) * 100 : 0;
268-
const stepped = Math.floor(pct / stepPercent) * stepPercent;
269-
if (stepped !== lastLoggedPct && (stepped > lastLoggedPct || pct >= 100)) {
270-
lastLoggedPct = stepped;
271-
const speed = payload.speed ? ` ${payload.speed}` : "";
272-
opts.logger(
273-
`进度 ${Math.min(100, Math.round(pct))}% (${value.toFixed(1)}/${total.toFixed(1)})${speed}`
274-
);
275-
}
282+
emitNonTty(false);
276283
},
277284
stop() {
278285
if (stopped) return;
@@ -286,6 +293,24 @@ export function createTerminalProgress(opts: TerminalProgressOptions = {}): Prog
286293
},
287294
};
288295

296+
/** 非 TTY 百分比日志唯一出口(start / update 共用 — DRY) */
297+
function emitNonTty(force: boolean): void {
298+
if (!opts.logger) return;
299+
const pct = total > 0 ? (value / total) * 100 : 0;
300+
const stepped = Math.floor(pct / stepPercent) * stepPercent;
301+
if (
302+
!force &&
303+
!(stepped !== lastLoggedPct && (stepped > lastLoggedPct || pct >= 100))
304+
) {
305+
return;
306+
}
307+
lastLoggedPct = stepped;
308+
const speed = payload.speed ? ` ${payload.speed}` : "";
309+
opts.logger(
310+
`进度 ${Math.min(100, Math.round(pct))}% (${value.toFixed(1)}/${total.toFixed(1)})${speed}`
311+
);
312+
}
313+
289314
return handle;
290315
}
291316

modules/sdk/@sfmc-sdk/src/node/config/index.ts

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,12 @@ export interface DBConfig {
5353
[key: string]: unknown;
5454
}
5555

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

6163
export interface RuntimeConfig {
6264
runtime_root?: string;
@@ -137,20 +139,15 @@ export type ConfigSchemaId =
137139
| "module_catalog";
138140

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

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

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

220217
/** remote.json 骨架(enroll 前) */
221218
export const DEFAULT_REMOTE_CONFIG: RemoteConfig = {
@@ -341,4 +338,78 @@ export function ensureJson<T>(filePath: string, seed: T = {} as T): T {
341338
*/
342339
export function ensureJsonConfig<T>(root: string, name: ConfigName, seed: T = {} as T): T {
343340
return ensureJson<T>(configPath(root, name), seed);
341+
}
342+
343+
/**
344+
* ensure 带 `$schema` 的配置对象(DRY:禁止各服务手写 ensureJsonConfig+withConfigSchema)。
345+
* 返回磁盘内容(可能含元数据键);运行时业务请用 {@link loadEnsuredConfig}。
346+
*/
347+
export function ensureSchemaConfig<T extends Record<string, unknown>>(
348+
root: string,
349+
name: ConfigName,
350+
schemaId: ConfigSchemaId,
351+
defaults: T,
352+
from: "configs" | "packs" | "modules" = "configs"
353+
): T & { $schema?: string } {
354+
return ensureJsonConfig(
355+
root,
356+
name,
357+
withConfigSchema({ ...defaults } as Record<string, unknown>, schemaId, from)
358+
) as T & { $schema?: string };
359+
}
360+
361+
/**
362+
* ensure 后剥离元数据,返回可交给业务逻辑的纯配置(LSP:与无 `$schema` 的契约一致)。
363+
*/
364+
export function loadEnsuredConfig<T extends Record<string, unknown>>(
365+
root: string,
366+
name: ConfigName,
367+
schemaId: ConfigSchemaId,
368+
defaults: T,
369+
from: "configs" | "packs" | "modules" = "configs"
370+
): T {
371+
return stripConfigMeta(
372+
ensureSchemaConfig(root, name, schemaId, defaults, from) as Record<string, unknown>
373+
) as T;
374+
}
375+
376+
/** 平台核心配置种子集合(扩展新文件时只改此处 + switch) */
377+
export type CoreConfigKind = "db_config" | "qq_config" | "bds_updater" | "permissions" | "remote";
378+
379+
/**
380+
* 按需 ensure 平台核心配置。新种类加到 CoreConfigKind + switch 即可(OCP)。
381+
* permissions 根为数组,不能走 withConfigSchema。
382+
*/
383+
export function ensureCoreConfigs(root: string, kinds: readonly CoreConfigKind[]): void {
384+
for (const kind of kinds) {
385+
switch (kind) {
386+
case "db_config":
387+
ensureSchemaConfig(root, "db_config.json", "db_config", {
388+
...DEFAULT_DB_CONFIG,
389+
} as Record<string, unknown>);
390+
break;
391+
case "qq_config":
392+
ensureSchemaConfig(root, "qq_config.json", "qq_config", {
393+
...DEFAULT_QQ_CONFIG,
394+
} as Record<string, unknown>);
395+
break;
396+
case "bds_updater":
397+
ensureSchemaConfig(root, "bds_updater.json", "bds_updater", {
398+
...DEFAULT_BDS_UPDATER_CONFIG,
399+
} as Record<string, unknown>);
400+
break;
401+
case "permissions":
402+
ensureJson(configPath(root, "permissions.json"), DEFAULT_PERMISSIONS);
403+
break;
404+
case "remote":
405+
ensureSchemaConfig(root, "remote.json", "remote", {
406+
...DEFAULT_REMOTE_CONFIG,
407+
} as Record<string, unknown>);
408+
break;
409+
default: {
410+
const _exhaustive: never = kind;
411+
void _exhaustive;
412+
}
413+
}
414+
}
344415
}

0 commit comments

Comments
 (0)