From 4510e6464ab05a345282a4051b5edd14163d4a66 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 22 Jul 2026 14:18:22 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(solid):=20orderBy=20field/col=20?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=20+=20npm-publish=20workspaces=20=E9=97=A8?= =?UTF-8?q?=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SDK OrderBy 用 field,tx-runner 只认 col,带 orderBy 的 query 会炸。 抽取 normalizeOrderBy 兼收 field/col 与数组。 清单包必须落在 root workspaces,否则 npm -w 再现 ba65eb9 tools 失败; resolve-npm-publish-tag 与 check-ootb 共用断言(DRY)。 Co-authored-by: Shiroha --- db-server/src/lib/order-by.ts | 34 +++++++++++++++++++ db-server/src/runtime.test.ts | 14 ++++++++ db-server/src/tx-runner.ts | 20 ++++++++---- tools/check-ootb.mjs | 16 +++++++++ tools/lib/npm-publish-packages.mjs | 52 ++++++++++++++++++++++++++++++ tools/resolve-npm-publish-tag.mjs | 14 +++++++- 6 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 db-server/src/lib/order-by.ts diff --git a/db-server/src/lib/order-by.ts b/db-server/src/lib/order-by.ts new file mode 100644 index 00000000..7df5bcd7 --- /dev/null +++ b/db-server/src/lib/order-by.ts @@ -0,0 +1,34 @@ +/** + * order-by.ts — 规范化 QueryOptions.orderBy(DRY + LSP) + * + * SDK 权威形态是 `{ field, dir? }`(见 @sfmc-bds/sdk/db OrderBy); + * tx-runner 早期用过 `{ col, dir? }`。两端必须互通,否则 + * `ORDER BY "undefined"` / `orderBy bad column` 静默炸。 + */ + +export type NormalizedOrder = { col: string; dir: "asc" | "desc" }; + +/** + * 把 SDK `field` / 遗留 `col`、单值 / 数组 统一成 `{col,dir}[]`。 + * @throws 缺列名或类型非法时 + */ +export function normalizeOrderBy(orderBy: unknown): NormalizedOrder[] { + if (orderBy == null) return []; + const list = Array.isArray(orderBy) ? orderBy : [orderBy]; + const out: NormalizedOrder[] = []; + for (const item of list) { + if (item == null || typeof item !== "object") { + throw new Error("[tx] orderBy 项必须是对象"); + } + const rec = item as Record; + const col = rec.col ?? rec.field; + if (typeof col !== "string" || !col) { + throw new Error("[tx] orderBy 缺少 field/col"); + } + out.push({ + col, + dir: rec.dir === "desc" ? "desc" : "asc", + }); + } + return out; +} diff --git a/db-server/src/runtime.test.ts b/db-server/src/runtime.test.ts index 3d3c3fac..cf165026 100644 --- a/db-server/src/runtime.test.ts +++ b/db-server/src/runtime.test.ts @@ -96,3 +96,17 @@ test("jsonV2Fail: ok 方言 + extra(step) 合并(LSP/DRY)", async () => { equal(statusCode, 400); deepEqual(payload, { ok: false, error: "boom", code: "bad_step", step: 2 }); }); + +test("normalizeOrderBy: SDK field 与遗留 col / 数组互通(LSP)", async () => { + const { normalizeOrderBy } = await import("./lib/order-by.js"); + deepEqual(normalizeOrderBy(undefined), []); + deepEqual(normalizeOrderBy({ field: "created_at", dir: "desc" }), [ + { col: "created_at", dir: "desc" }, + ]); + deepEqual(normalizeOrderBy({ col: "id" }), [{ col: "id", dir: "asc" }]); + deepEqual(normalizeOrderBy([{ field: "a" }, { col: "b", dir: "desc" }]), [ + { col: "a", dir: "asc" }, + { col: "b", dir: "desc" }, + ]); + throws(() => normalizeOrderBy({ dir: "asc" }), /field\/col/); +}); diff --git a/db-server/src/tx-runner.ts b/db-server/src/tx-runner.ts index 1227836a..20d4312e 100644 --- a/db-server/src/tx-runner.ts +++ b/db-server/src/tx-runner.ts @@ -39,6 +39,7 @@ import { Perm, assertModulePermission, } from "./permission-gate.js"; +import { normalizeOrderBy } from "./lib/order-by.js"; const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/; @@ -51,7 +52,10 @@ export interface TxStepQuery { table: string; opts?: { where?: WhereExpr; - orderBy?: { col: string; dir?: "asc" | "desc" }; + /** SDK 用 field;历史 col 仍接受(normalizeOrderBy) */ + orderBy?: + | { field?: string; col?: string; dir?: "asc" | "desc" } + | Array<{ field?: string; col?: string; dir?: "asc" | "desc" }>; limit?: number; offset?: number; }; @@ -261,17 +265,21 @@ export class TxRunner { private doQuery(_mid: string, mod: ModuleManifestV2, step: TxStepQuery): TxStepResult { this.requireTableRead(mod, step.table); const where = compile(step.opts?.where); - const orderBy = step.opts?.orderBy; + // LSP:与 SDK QueryOptions.orderBy({field}|[]) 对齐,兼收遗留 {col} + const orders = normalizeOrderBy(step.opts?.orderBy); const limit = typeof step.opts?.limit === "number" ? step.opts?.limit : undefined; const offset = typeof step.opts?.offset === "number" ? step.opts?.offset : undefined; const whereClause = where.sql; const params = [...where.values]; let extra = ""; - if (orderBy) { - if (!IDENT.test(orderBy.col)) throw new Error(`[tx] orderBy bad column "${orderBy.col}"`); - const dir = orderBy.dir === "desc" ? "DESC" : "ASC"; - extra += ` ORDER BY "${orderBy.col}" ${dir}`; + if (orders.length > 0) { + const parts: string[] = []; + for (const o of orders) { + if (!IDENT.test(o.col)) throw new Error(`[tx] orderBy bad column "${o.col}"`); + parts.push(`"${o.col}" ${o.dir === "desc" ? "DESC" : "ASC"}`); + } + extra += ` ORDER BY ${parts.join(", ")}`; } if (typeof limit === "number") { extra += " LIMIT " + Math.max(0, Math.floor(limit)); diff --git a/tools/check-ootb.mjs b/tools/check-ootb.mjs index 7fd3f42a..a04ad526 100755 --- a/tools/check-ootb.mjs +++ b/tools/check-ootb.mjs @@ -20,6 +20,10 @@ import { import { exists } from "./lib/io.mjs"; import { requestJson, waitHealth } from "./lib/http.mjs"; import { killProc, runSync } from "./lib/proc.mjs"; +import { + NPM_PUBLISH_PACKAGES, + assertPublishPackageInWorkspaces, +} from "./lib/npm-publish-packages.mjs"; const errors = []; const passed = []; @@ -63,6 +67,18 @@ async function main() { else fail("必备仓库文件齐全", "缺失: " + missing.join(", ")); } + // 1b) npm-publish 清单 ⊆ root workspaces(DRY;防 ba65eb9 tools 发包失败再现) + { + try { + for (const name of Object.keys(NPM_PUBLISH_PACKAGES)) { + assertPublishPackageInWorkspaces(name, ROOT); + } + pass("npm-publish 包均在 workspaces"); + } catch (e) { + fail("npm-publish 包均在 workspaces", e?.message || String(e)); + } + } + // 2) configs: configs/ 或 configs-default/ { const need = ["db_config.json", "bds_updater.json", "qq_config.json"]; diff --git a/tools/lib/npm-publish-packages.mjs b/tools/lib/npm-publish-packages.mjs index 8a6dbf19..121991bb 100644 --- a/tools/lib/npm-publish-packages.mjs +++ b/tools/lib/npm-publish-packages.mjs @@ -3,6 +3,9 @@ * workflow / docs / pack:verify 应对齐本表,勿在 yaml 里再抄一份 case/map。 */ +import fs from "node:fs"; +import path from "node:path"; + /** npm 包名 → 相对仓库根的 package.json 路径 */ export const NPM_PUBLISH_PACKAGES = { "@sfmc-bds/sdk": "modules/sdk/@sfmc-sdk/package.json", @@ -20,3 +23,52 @@ export function resolvePublishPackage(pkg) { } return null; } + +/** + * 判断相对仓根的目录是否落在 root workspaces 声明内。 + * 支持精确项与末尾 `/*` 一层通配(与 npm workspaces 常见写法一致)。 + * @param {string} dirPosix 正斜杠相对路径,如 "tools" / "modules/packages/afk" + * @param {string[]} workspaces + */ +export function workspaceIncludesDir(dirPosix, workspaces) { + const dir = dirPosix.replace(/\\/g, "/").replace(/\/+$/, ""); + for (const raw of workspaces) { + const w = String(raw).replace(/\\/g, "/").replace(/\/+$/, ""); + if (w === dir) return true; + if (w.endsWith("/*")) { + const prefix = w.slice(0, -1); // "modules/packages/" + if (!dir.startsWith(prefix)) continue; + const rest = dir.slice(prefix.length); + if (rest && !rest.includes("/")) return true; + } + } + return false; +} + +/** + * 发包前断言:清单里的包必须在 root workspaces 中,否则 + * `npm run build -w` / `npm publish -w` 会报 No workspaces found + * (见 @sfmc-bds/tools@v0.1.0 on ba65eb9)。 + * @param {string} pkgName + * @param {string} [repoRoot=process.cwd()] + * @returns {{ workspaceDir: string, workspaces: string[] }} + */ +export function assertPublishPackageInWorkspaces(pkgName, repoRoot = process.cwd()) { + const resolved = resolvePublishPackage(pkgName); + if (!resolved) { + throw new Error(`Unknown publish package: ${pkgName}`); + } + const pkgPath = NPM_PUBLISH_PACKAGES[resolved]; + const workspaceDir = path.posix.dirname(pkgPath.replace(/\\/g, "/")); + const rootPkgPath = path.join(repoRoot, "package.json"); + const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf8")); + const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : []; + if (!workspaceIncludesDir(workspaceDir, workspaces)) { + throw new Error( + `${resolved} 路径 ${workspaceDir} 不在 root workspaces 中` + + ` (当前: ${JSON.stringify(workspaces)});` + + ` npm -w 会失败。请把该目录写入 package.json#workspaces(DRY)。` + ); + } + return { workspaceDir, workspaces }; +} diff --git a/tools/resolve-npm-publish-tag.mjs b/tools/resolve-npm-publish-tag.mjs index 72f46e06..2a982a7d 100644 --- a/tools/resolve-npm-publish-tag.mjs +++ b/tools/resolve-npm-publish-tag.mjs @@ -11,7 +11,11 @@ * 避免 workflow_dispatch 重跑对已发布版本 E403(与 publish 步骤幂等)。 */ import fs from "node:fs"; -import { NPM_PUBLISH_PACKAGES, resolvePublishPackage } from "./lib/npm-publish-packages.mjs"; +import { + NPM_PUBLISH_PACKAGES, + resolvePublishPackage, + assertPublishPackageInWorkspaces, +} from "./lib/npm-publish-packages.mjs"; const tag = process.env.PUBLISH_TAG || process.env.GITHUB_REF_NAME || ""; const outFile = process.env.GITHUB_OUTPUT; @@ -36,6 +40,14 @@ if (!resolved) { process.exit(1); } +// DRY:清单登记的包必须在 root workspaces,否则后续 npm -w 必挂 +try { + assertPublishPackageInWorkspaces(resolved); +} catch (e) { + console.error(e?.message || e); + process.exit(1); +} + const pkgPath = NPM_PUBLISH_PACKAGES[resolved]; const actual = JSON.parse(fs.readFileSync(pkgPath, "utf8")).version; if (actual !== ver) { From 84fa265fb972173fbc5ff0f2b0197717b5650b51 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 22 Jul 2026 14:18:22 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(sdk):=20HttpDB=20=E6=8C=89=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E4=BC=A0=20token=20+=20db.tx=20=E7=A6=81=E6=AD=A2?= =?UTF-8?q?=E5=81=87=E8=AF=BB=E5=9B=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 模块 db/config/service 不再写进程级 authToken,避免与 ConfigManager 互相覆盖(DIP)。db.tx 录制期 query/get 显式抛错,不再返回 []/null stub(LSP); call 仍可入队但返回值不可用。 Co-authored-by: Shiroha --- .../sdk/@sfmc-sdk/src/sapi/config/client.ts | 11 ++-- modules/sdk/@sfmc-sdk/src/sapi/db/client.ts | 50 +++++++++++-------- modules/sdk/@sfmc-sdk/src/sapi/db/index.ts | 3 +- .../sdk/@sfmc-sdk/src/sapi/runtime/httpdb.ts | 47 +++++++++++------ .../sdk/@sfmc-sdk/src/sapi/service/client.ts | 15 +++--- 5 files changed, 79 insertions(+), 47 deletions(-) diff --git a/modules/sdk/@sfmc-sdk/src/sapi/config/client.ts b/modules/sdk/@sfmc-sdk/src/sapi/config/client.ts index 3abcf84e..1de788bb 100644 --- a/modules/sdk/@sfmc-sdk/src/sapi/config/client.ts +++ b/modules/sdk/@sfmc-sdk/src/sapi/config/client.ts @@ -16,23 +16,24 @@ import { HttpRequestMethod } from "@minecraft/server-net"; let _moduleId = ""; let _configKey = ""; +let _authToken = ""; const _cache = new Map(); let _loadPromise: Promise | null = null; export function setConfigModuleContext(moduleId: string, configKey: string, token: string): void { _moduleId = moduleId; _configKey = configKey; + _authToken = token; _cache.clear(); _loadPromise = null; - HttpDB.setAuthToken(token); } export function clearConfigModuleContext(): void { _moduleId = ""; _configKey = ""; + _authToken = ""; _cache.clear(); _loadPromise = null; - HttpDB.setAuthToken(""); } /** @@ -53,7 +54,8 @@ async function ensureLoaded(): Promise { const res = await HttpDB.typedRequest<{ config: Record }>( HttpRequestMethod.GET, withModuleId(`/api/sfmc/configs/${encodeURIComponent(_configKey)}`), - undefined + undefined, + { authToken: _authToken } ); if (res.ok && res.data) { for (const [k, v] of Object.entries(res.data.config ?? {})) { @@ -79,7 +81,8 @@ export const config = { const res = await HttpDB.typedRequest<{ ok: true }>( HttpRequestMethod.POST, withModuleId(`/api/sfmc/configs/${encodeURIComponent(_configKey)}/set`), - { key, value } + { key, value }, + { authToken: _authToken } ); if (res.ok) { _cache.set(key, value); diff --git a/modules/sdk/@sfmc-sdk/src/sapi/db/client.ts b/modules/sdk/@sfmc-sdk/src/sapi/db/client.ts index 1a335ed5..e3a5e4ff 100644 --- a/modules/sdk/@sfmc-sdk/src/sapi/db/client.ts +++ b/modules/sdk/@sfmc-sdk/src/sapi/db/client.ts @@ -6,8 +6,8 @@ * * 鉴权: * - X-SFMC-Module-Id 通过 URL query string 传(?moduleId=...) - * - X-SFMC-Module-Token 通过 HttpDB.setAuthToken 走 Authorization: Bearer - * (设置时由 installHostBootstrap 在 db 上下文 setup 时调用) + * - Bearer token 按请求传入(HttpDB typedRequest opts),不写进程级 static + * (避免与 ConfigManager / 其它模块互相覆盖 — DIP) */ import { HttpDB } from "../runtime/httpdb.js"; @@ -26,17 +26,18 @@ import type { /* ── 模块身份(由 installHostBootstrap 注入) ─────────────────────── */ let _moduleId = ""; +let _authToken = ""; let _currentTxId: string | null = null; export function setDbModuleContext(moduleId: string, token: string): void { _moduleId = moduleId; - HttpDB.setAuthToken(token); + _authToken = token; } export function clearDbModuleContext(): void { _moduleId = ""; + _authToken = ""; _currentTxId = null; - HttpDB.setAuthToken(""); } /* ── HTTP 辅助 ──────────────────────────────────────────────────── */ @@ -56,13 +57,28 @@ function withModuleId(path: string): string { } async function post(path: string, body: unknown): Promise { - const res = await HttpDB.typedRequest(HttpRequestMethod.POST, withModuleId(path), body as Record); + const res = await HttpDB.typedRequest( + HttpRequestMethod.POST, + withModuleId(path), + body as Record, + { authToken: _authToken } + ); if (!res.ok) { throw new DbError(res.error ?? "db_server_error", "internal", res.status); } return res.data as T; } +/** 事务录制期不支持 query/get 读回:返回 []/null stub 会破坏 LSP(误导业务分支)。 */ +function txReadNotSupported(op: string): never { + throw new DbError( + `db.tx 内暂不支持 ${op} 读回结果(录制后一次性提交,无交互协议);` + + `请在事务外先 db.${op},再在 tx 内做写操作`, + "tx_interactive_required", + 0 + ); +} + /* ── 公开 API ──────────────────────────────────────────────────── */ export const db = { @@ -119,7 +135,11 @@ export const db = { }); }, - /** 事务:边界在 db-server 进程。失败自动回滚,成功提交。 */ + /** + * 事务:边界在 db-server 进程。失败自动回滚,成功提交。 + * 录制期:写操作(insert/update/delete/audit/call)可入队;query/get 显式抛错, + * 避免返回 []/null 假数据破坏 LSP。call 可入队但返回值不可用(两阶段协议另议)。 + */ async tx(fn: (tx: TxContext) => Promise): Promise { if (_currentTxId) throw new DbError("嵌套事务暂不支持", "nested_tx", 0); const steps: TxStep[] = []; @@ -132,21 +152,8 @@ export const db = { }; const recorder: TxContext = { - query: async = Record>( - table: string, - opts?: QueryOptions - ): Promise => { - if (opts) push({ op: "query", table, opts }); - else push({ op: "query", table }); - return []; - }, - get: async = Record>( - table: string, - id: string | number - ): Promise => { - push({ op: "get", table, id: String(id) }); - return null; - }, + query: async () => txReadNotSupported("query"), + get: async () => txReadNotSupported("get"), insert: async >(table: string, row: U): Promise => { push({ op: "insert", table, row }); return row; @@ -168,6 +175,7 @@ export const db = { }, call: async (name: string, input: Record): Promise => { push({ op: "service", name, input }); + // 录制期无服务端 result 可回放;勿依赖返回值 return undefined as unknown as U; }, }; diff --git a/modules/sdk/@sfmc-sdk/src/sapi/db/index.ts b/modules/sdk/@sfmc-sdk/src/sapi/db/index.ts index 3bd64587..7e296763 100644 --- a/modules/sdk/@sfmc-sdk/src/sapi/db/index.ts +++ b/modules/sdk/@sfmc-sdk/src/sapi/db/index.ts @@ -6,7 +6,8 @@ * * 设计: * - db.query / get / insert / update / delete / audit / idempotent:单 RPC - * - db.tx(fn):把 fn 内的 step 录下来,一次性发 /api/sfmc/db/tx,server 端事务跑 + * - db.tx(fn):把 fn 内写 step 录下来,一次性发 /api/sfmc/db/tx,server 端事务跑 + * (query/get 在录制期不可用 — 避免 stub 假数据;交互读回需两阶段协议) * - 不允许原始 SQL;只能传 WhereExpr 表达式树,平台翻译 * - 模块不能 require("fs");只能走这里 */ diff --git a/modules/sdk/@sfmc-sdk/src/sapi/runtime/httpdb.ts b/modules/sdk/@sfmc-sdk/src/sapi/runtime/httpdb.ts index 330d3a12..1cdaeff6 100644 --- a/modules/sdk/@sfmc-sdk/src/sapi/runtime/httpdb.ts +++ b/modules/sdk/@sfmc-sdk/src/sapi/runtime/httpdb.ts @@ -15,9 +15,13 @@ import { isSuccessfulHttpEnvelope } from "./http-envelope.js"; let baseUrl = "http://127.0.0.1:3001"; const TIMEOUT = 3; +/** 单次请求可选覆盖;模块客户端应传自己的 token,勿抢进程级默认值(DIP)。 */ +export type HttpRequestAuthOpts = { authToken?: string }; + export class HttpDB { private static available = true; private static _lastErrorLog = 0; + /** 仅 ConfigManager / DataAdapter 默认通道;模块 db/config/service 走 per-request。 */ private static authToken = ""; /** 注入 db-server 基址(如 http://127.0.0.1:4000),去掉末尾 /。 */ @@ -96,7 +100,8 @@ export class HttpDB { private static async request( method: HttpRequestMethod, path: string, - bodyData?: Record + bodyData?: Record, + opts?: HttpRequestAuthOpts ): Promise<{ status: number; body: string }> { try { const req = new HttpRequest(`${baseUrl}${path}`); @@ -107,7 +112,9 @@ export class HttpDB { req.body = JSON.stringify(bodyData); req.addHeader("Content-Type", "application/json"); } - if (this.authToken) req.addHeader("Authorization", `Bearer ${this.authToken}`); + // 请求级 token 优先,否则回落 DataAdapter 默认(ConfigManager) + const token = (opts?.authToken ?? this.authToken).trim(); + if (token) req.addHeader("Authorization", `Bearer ${token}`); const res = await http.request(req); this.available = true; @@ -124,17 +131,19 @@ export class HttpDB { static async requestJSON( method: HttpRequestMethod, path: string, - bodyData?: Record + bodyData?: Record, + opts?: HttpRequestAuthOpts ): Promise<{ status: number; body: string }> { - return this.request(method, path, bodyData); + return this.request(method, path, bodyData, opts); } static async typedRequest( method: HttpRequestMethod, path: string, - bodyData?: Record + bodyData?: Record, + opts?: HttpRequestAuthOpts ): Promise<{ ok: boolean; data?: T; error?: string; status: number }> { - const { status, body } = await this.request(method, path, bodyData); + const { status, body } = await this.request(method, path, bodyData, opts); if (!body) return { ok: false, error: "network_error", status }; try { const parsed = JSON.parse(body); @@ -148,27 +157,35 @@ export class HttpDB { } } - static async get(path: string): Promise { - const { status, body } = await this.request(HttpRequestMethod.GET, path); + static async get(path: string, opts?: HttpRequestAuthOpts): Promise { + const { status, body } = await this.request(HttpRequestMethod.GET, path, undefined, opts); if (status !== 200) console.info(`[HttpDB] GET ${path} → ${status}`); return status === 200 ? body : null; } - static async post(path: string, bodyData: Record): Promise { - const { status } = await this.request(HttpRequestMethod.POST, path, bodyData); + static async post( + path: string, + bodyData: Record, + opts?: HttpRequestAuthOpts + ): Promise { + const { status } = await this.request(HttpRequestMethod.POST, path, bodyData, opts); if (status !== 200) console.info(`[HttpDB] POST ${path} → ${status}`); return status === 200; } - static async put(path: string, bodyData: Record): Promise { - const { status } = await this.request(HttpRequestMethod.PUT, path, bodyData); + static async put( + path: string, + bodyData: Record, + opts?: HttpRequestAuthOpts + ): Promise { + const { status } = await this.request(HttpRequestMethod.PUT, path, bodyData, opts); if (status !== 200) console.info(`[HttpDB] PUT ${path} → ${status}`); return status === 200; } - static async del(path: string): Promise { - const { status } = await this.request(HttpRequestMethod.DELETE, path); + static async del(path: string, opts?: HttpRequestAuthOpts): Promise { + const { status } = await this.request(HttpRequestMethod.DELETE, path, undefined, opts); if (status !== 200) console.info(`[HttpDB] DELETE ${path} → ${status}`); return status === 200; } -} \ No newline at end of file +} diff --git a/modules/sdk/@sfmc-sdk/src/sapi/service/client.ts b/modules/sdk/@sfmc-sdk/src/sapi/service/client.ts index ee89468b..31208681 100644 --- a/modules/sdk/@sfmc-sdk/src/sapi/service/client.ts +++ b/modules/sdk/@sfmc-sdk/src/sapi/service/client.ts @@ -1,8 +1,8 @@ /** * client.ts — 跨模块 service registry 的 SAPI 侧客户端 * - * 鉴权同 db:moduleId 走 URL ?moduleId=,token 走 Authorization Bearer - * (由 setServiceModuleContext 调 HttpDB.setAuthToken) + * 鉴权同 db:moduleId 走 URL ?moduleId=,token 按请求 Bearer 传入 + * (不写 HttpDB 进程级 static,避免与其它客户端争用 — DIP) * * 设计: * - service.get(name, input):发 GET /api/sfmc/services/:name?input=... @@ -14,18 +14,19 @@ import { HttpDB } from "../runtime/httpdb.js"; import { HttpRequestMethod } from "@minecraft/server-net"; let _moduleId = ""; +let _authToken = ""; let _isInTx: () => boolean = () => false; export function setServiceModuleContext(moduleId: string, token: string, inTx: () => boolean): void { _moduleId = moduleId; + _authToken = token; _isInTx = inTx; - HttpDB.setAuthToken(token); } export function clearServiceModuleContext(): void { _moduleId = ""; + _authToken = ""; _isInTx = () => false; - HttpDB.setAuthToken(""); } export interface ServiceInfo { @@ -60,7 +61,8 @@ export const service = { const res = await HttpDB.typedRequest<{ ok: true; result: T }>( HttpRequestMethod.GET, withModuleId(`/api/sfmc/services/${encodeURIComponent(name)}?${qs}`), - undefined + undefined, + { authToken: _authToken } ); if (!res.ok) { throw new ServiceError(res.error ?? "service_error", "internal", res.status); @@ -72,7 +74,8 @@ export const service = { const res = await HttpDB.typedRequest<{ services: ServiceInfo[] }>( HttpRequestMethod.GET, withModuleId("/api/sfmc/services"), - undefined + undefined, + { authToken: _authToken } ); if (res.ok && res.data) return res.data.services; return [];