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
2 changes: 1 addition & 1 deletion .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ jobs:
if: steps.meta.outputs.already_published != 'true' && steps.meta.outputs.workspace != '@sfmc-bds/sdk'
run: npm run build --workspace @sfmc-bds/sdk

# --if-present:纯脚本包(如 @sfmc-bds/tools)无编译步骤,勿强制 build(OCP)
- name: build target workspace
if: steps.meta.outputs.already_published != 'true'
# --if-present: 允许无编译步骤的纯 JS/mjs 包(如 @sfmc-bds/tools)
run: npm run build --workspace "${{ steps.meta.outputs.workspace }}" --if-present

- name: publish to npm
Expand Down
7 changes: 4 additions & 3 deletions db-server/src/routes/_shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,17 @@ export const body = sharedBody;

/**
* v2 失败信封权威形态(DRY + LSP):
* 一律 `{ ok:false, error, code? }`,勿再混用 `{ success:false }`。
* 一律 `{ ok:false, error, code? }`(+ 可选 extra),勿再混用 `{ success:false }`。
* service-routes / db / module-config / index 模块鉴权门应走此助手。
*/
export function jsonV2Fail(
res: ServerResponse,
error: string,
status: number,
code?: string
code?: string,
extra?: Record<string, unknown>
): void {
const payload: Record<string, unknown> = { ok: false, error };
const payload: Record<string, unknown> = { ...(extra ?? {}), ok: false, error };
if (code) payload.code = code;
sharedJson(res, payload, status);
}
Expand Down
16 changes: 8 additions & 8 deletions db-server/src/routes/db-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
const result = deps.schemaRegistry!.define(moduleId, req2);
json(res, { success: true, table: result.table, created: result.created }, 200);
} catch (e) {
json(res, { success: false, error: (e as Error).message }, 400);
jsonV2Fail(res, (e as Error).message, 400);
}
return true;
}
Expand All @@ -89,7 +89,7 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
const status = result.ok ? 200 : 400;
json(res, result as unknown as Record<string, unknown>, status);
} catch (e) {
json(res, { success: false, error: (e as Error).message }, 500);
jsonV2Fail(res, (e as Error).message, 500);
}
return true;
}
Expand All @@ -109,17 +109,17 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
const result = await deps.txRunner!.run({ moduleId, steps: [step] });
if (!result.ok) {
const code = result.code === "permission_denied" ? 403 : 400;
json(res, { success: false, error: result.error, step: result.step, code: result.code }, code);
jsonV2Fail(res, result.error, code, result.code, { step: result.step });
return true;
}
const first = result.results[0];
json(res, { success: true, ...unwrapResult(first, cand.op) }, 200);
} catch (e) {
if (e instanceof PermissionDeniedError) {
json(res, { success: false, error: e.message, code: "permission_denied" }, 403);
jsonV2Fail(res, e.message, 403, "permission_denied");
return true;
}
json(res, { success: false, error: (e as Error).message }, 500);
jsonV2Fail(res, (e as Error).message, 500);
}
return true;
}
Expand All @@ -136,12 +136,12 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
};
const result = await deps.txRunner!.run({ moduleId, steps: [step] });
if (!result.ok) {
json(res, { success: false, error: result.error }, 400);
jsonV2Fail(res, result.error, 400, result.code);
return true;
}
json(res, { success: true });
} catch (e) {
json(res, { success: false, error: (e as Error).message }, 500);
jsonV2Fail(res, (e as Error).message, 500);
}
return true;
}
Expand All @@ -166,7 +166,7 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
json(res, { success: r.ok });
}
} catch (e) {
json(res, { success: false, error: (e as Error).message }, 500);
jsonV2Fail(res, (e as Error).message, 500);
}
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 @@ -76,3 +76,23 @@ test("ServiceRegistry: missing service → no_such_service (not not_in_requires)
(e: unknown) => e instanceof DispatchError && e.code === "no_such_service"
);
});

test("jsonV2Fail: ok 方言 + extra(step) 合并(LSP/DRY)", async () => {
const { jsonV2Fail } = 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;

jsonV2Fail(res, "boom", 400, "bad_step", { step: 2 });
const payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
equal(statusCode, 400);
deepEqual(payload, { ok: false, error: "boom", code: "bad_step", step: 2 });
});
22 changes: 22 additions & 0 deletions modules/sdk/@sfmc-sdk/src/module-loader/http-data-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* HttpDB → ConfigManager.DataAdapter 适配器(DIP)。
* ConfigManager 只依赖 DataAdapter 抽象;本文件是 module-loader 侧唯一 HttpDB 装配点。
*/

import { HttpDB } from "../sapi/runtime/httpdb.js";
import type { DataAdapter } from "./internal/config-manager.js";

/** 用 HttpDB 实现 DataAdapter;可选覆盖 db-server 基址。 */
export function createHttpDataAdapter(opts?: { baseUrl?: string }): DataAdapter {
if (opts?.baseUrl) HttpDB.configure({ baseUrl: opts.baseUrl });
return {
checkHealth: async () => {
await HttpDB.checkHealth();
},
getAllConfigs: async () => HttpDB.get("/api/sfmc/configs/all"),
getModules: async () => HttpDB.get("/api/sfmc/modules"),
setAuthToken: (token: string) => {
HttpDB.setAuthToken(token);
},
};
}
1 change: 1 addition & 0 deletions modules/sdk/@sfmc-sdk/src/module-loader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export type { ModuleLifecycle, ModuleDescriptor } from "./runtime.js";
export { guardEvent, announceLoaded } from "./runtime.js";
export { installHostBootstrap } from "./install.js";
export type { InstallOptions, HostBackend, ModuleSurface } from "./install.js";
export { createHttpDataAdapter } from "./http-data-adapter.js";
export const SFMC_MODULE_LOADER_VERSION = "0.1.0" as const;
45 changes: 21 additions & 24 deletions modules/sdk/@sfmc-sdk/src/module-loader/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,19 @@
* 1) system.beforeEvents.startup.subscribe:ConfigManager.init() + bootAll + snapshot
* 2) world.afterEvents.worldLoad.subscribe:bootAfterWorldLoad
* 3) system.beforeEvents.shutdown.subscribe:teardown
* 4) bindDataAdapter():注入 db-server HTTP 适配器
* 4) bindDataAdapter():注入 db-server HTTP 适配器(DIP:经 DataAdapter)
* 5) 注册 setModuleGuard 给 Command.trigger 使用
*
* 本批 (Stage A+B):占位实现,host adapters (Command/Permission/HttpDB 等) 在 Stage F
* (core-* 模块迁移)完成后实装。
*/

import { system, world } from "@minecraft/server";
import { createHttpDataAdapter } from "./http-data-adapter.js";
import type { DataAdapter } from "./internal/config-manager.js";
import { ConfigManager } from "./internal/config-manager.js";
import { ModuleRegistry } from "./runtime.js";

export interface HostBackend {
/** 注入 db-server 数据适配器 */
bindDataAdapter(adapter: unknown): void;
bindDataAdapter(adapter: DataAdapter): void;
/** 关闭 db-server HTTP 客户端 */
dispose(): void;
}
Expand All @@ -38,23 +37,28 @@ export interface InstallOptions {
dbServerUrl?: string;
/** 可选注入自定义 HostBackend(测试用) */
hostBackend?: HostBackend;
/**
* 测试/离线可注入自定义 DataAdapter;默认走 HttpDB 实现。
* 高层只依赖 DataAdapter 抽象(DIP),不直接依赖 HttpDB。
*/
dataAdapter?: DataAdapter;
/** 模块 id 列表(默认 undefined = 全部装载,从 catalog 读取) */
enabledModuleIds?: readonly string[];
}

let _installed = false;

export function installHostBootstrap(options: InstallOptions = {}): HostBackend {
if (_installed) return _bootstrapStubBackend();
if (_installed) return _bootstrapBackend();
_installed = true;

// 1) 占位 data adapter:Stage F 之前 ConsoleData 把 readAll 抛 NOT_IMPLEMENTED;
// installHostBootstrap 仍然注册 hooks,只有 ConfigManager.init() 会失败 —
// 这是允许的中间形态。modules/src/index.ts 还可以装运行。
ConfigManager.bindDataAdapter(stubDataAdapter());
if (options.hostBackend) options.hostBackend.bindDataAdapter(undefined);
// DIP:ConfigManager ← DataAdapter;默认 HttpDB,可被 options 替换(测试/自定义 host)
const adapter =
options.dataAdapter ?? createHttpDataAdapter(options.dbServerUrl ? { baseUrl: options.dbServerUrl } : undefined);
ConfigManager.bindDataAdapter(adapter);
if (options.hostBackend) options.hostBackend.bindDataAdapter(adapter);

// 2) 装配 system.events
// 装配 system.events
system.beforeEvents.startup.subscribe(async () => {
try {
await ConfigManager.init();
Expand All @@ -77,25 +81,18 @@ export function installHostBootstrap(options: InstallOptions = {}): HostBackend
} catch {}
});

return _bootstrapStubBackend();
return _bootstrapBackend();
}

function _bootstrapStubBackend(): HostBackend {
function _bootstrapBackend(): HostBackend {
return {
bindDataAdapter: () => undefined,
bindDataAdapter: (adapter: DataAdapter) => {
ConfigManager.bindDataAdapter(adapter);
},
dispose: () => undefined,
};
}

function stubDataAdapter() {
return {
checkHealth: async () => undefined,
getAllConfigs: async () => null,
getModules: async () => null,
setAuthToken: () => undefined,
};
}

function announceLoaded() {
// runtime.ts 内的同名 export;此处仅为了不让 TS 报 unused 警告
const _ref = announceLoadedExported;
Expand Down
24 changes: 17 additions & 7 deletions modules/sdk/@sfmc-sdk/src/sapi/runtime/httpdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,26 @@ import { system } from "@minecraft/server";
import { http, HttpRequest, HttpRequestMethod } from "@minecraft/server-net";
import { isSuccessfulHttpEnvelope } from "./http-envelope.js";

const HOST = "127.0.0.1";
const PORT = 3001;
const BASE_URL = `http://${HOST}:${PORT}`;
/** 默认连本机 db-server;可通过 configure / InstallOptions.dbServerUrl 覆盖(DIP)。 */
let baseUrl = "http://127.0.0.1:3001";
const TIMEOUT = 3;

export class HttpDB {
private static available = true;
private static _lastErrorLog = 0;
private static authToken = "";

/** 注入 db-server 基址(如 http://127.0.0.1:4000),去掉末尾 /。 */
static configure(opts: { baseUrl?: string }): void {
if (opts.baseUrl) {
baseUrl = opts.baseUrl.replace(/\/+$/, "");
}
}

static getBaseUrl(): string {
return baseUrl;
}

static setAuthToken(token: string): void {
this.authToken = token.trim();
}
Expand Down Expand Up @@ -51,10 +61,10 @@ export class HttpDB {
static async checkHealth(): Promise<boolean> {
for (let i = 0; i < 5; i++) {
try {
const res = await http.get(`${BASE_URL}/api/health`);
const res = await http.get(`${baseUrl}/api/health`);
this.available = res.status === 200;
if (this.available) {
console.info(`[HttpDB] 数据库服务连接成功 (${BASE_URL}/api/health)`);
console.info(`[HttpDB] 数据库服务连接成功 (${baseUrl}/api/health)`);
return true;
}
console.error(`[HttpDB] 数据库服务返回异常状态 ${res.status}`);
Expand All @@ -64,7 +74,7 @@ export class HttpDB {
console.info(`[HttpDB] 连接失败,2s 后重试 (${i + 1}/5)...`);
await system.waitTicks(40);
} else {
console.error(`[HttpDB] 连接失败 (${BASE_URL}): ${err}`);
console.error(`[HttpDB] 连接失败 (${baseUrl}): ${err}`);
}
}
}
Expand All @@ -89,7 +99,7 @@ export class HttpDB {
bodyData?: Record<string, unknown>
): Promise<{ status: number; body: string }> {
try {
const req = new HttpRequest(`${BASE_URL}${path}`);
const req = new HttpRequest(`${baseUrl}${path}`);
req.timeout = TIMEOUT;
req.method = method;

Expand Down
9 changes: 7 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions tools/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
# SFMC dev tools - testing before build

开箱自检、模块安装、冒烟等脚本。面向 monorepo / 已部署的 SFMC 根目录使用。

## 安装

```bash
npm install @sfmc-bds/tools
```

从 npm 安装到非 monorepo `tools/` 路径时,请设置 `SFMC_ROOT` 指向 SFMC 根目录。

## 常用命令

```bash
npx sfmc-check-ootb
npx sfmc-fetch-module search
npx sfmc-catalog-sync
```

或在仓库根:`npm run check-ootb` / `node tools/check-ootb.mjs`。

## 仓库

<https://github.com/DogeLakeDev/ScriptsForMinecraftServer/tree/main/tools>
Empty file modified tools/catalog-sync.mjs
100644 → 100755
Empty file.
Empty file modified tools/check-modules.mjs
100644 → 100755
Empty file.
Empty file modified tools/check-ootb.mjs
100644 → 100755
Empty file.
Empty file modified tools/fetch-module.mjs
100644 → 100755
Empty file.
10 changes: 8 additions & 2 deletions tools/lib/paths.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

/** 主仓根目录 */
export const ROOT = path.resolve(__dirname, "..", "..");
/**
* 主仓根目录。
* 优先 SFMC_ROOT(与 db-server / 冒烟脚本一致);否则按 tools/lib 相对路径回退到 monorepo 根。
* 从 npm 安装到 node_modules 时务必设置 SFMC_ROOT。
*/
export const ROOT = process.env.SFMC_ROOT
? path.resolve(process.env.SFMC_ROOT)
: path.resolve(__dirname, "..", "..");

export const MODULES_DIR = path.join(ROOT, "modules");
export const PACKAGES_DIR = path.join(MODULES_DIR, "packages");
Expand Down
19 changes: 10 additions & 9 deletions tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,22 @@
"tools",
"scriptsforminecraftserver"
],
"main": "check-ootb.mjs",
"main": "./check-ootb.mjs",
"bin": {
"sfmc-check-ootb": "./check-ootb.mjs",
"sfmc-catalog-sync": "./catalog-sync.mjs",
"sfmc-check-modules": "./check-modules.mjs",
"sfmc-fetch-module": "./fetch-module.mjs",
"sfmc-smoke-modules": "./smoke-modules.mjs",
"sfmc-sim-new-user": "./sim-new-user.mjs"
},
"files": [
"*.mjs",
"lib",
"README.md"
],
"scripts": {
"build": "node -e \"process.exit(0)\"",
"check-ootb": "node check-ootb.mjs",
"check-modules": "node check-modules.mjs",
"catalog-sync": "node catalog-sync.mjs",
"smoke-modules": "node smoke-modules.mjs"
},
"engines": {
"node": ">=22.13.0"
"build": "node -e \"process.exit(0)\""
},
"publishConfig": {
"access": "public",
Expand Down
Empty file modified tools/sim-new-user.mjs
100644 → 100755
Empty file.
Empty file modified tools/smoke-modules.mjs
100644 → 100755
Empty file.
Loading