@lizardon386/core-abi 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ export * from './module-api.js';
2
2
  export * from './client.js';
3
3
  export * from './seats.js';
4
4
  export * from './manifest.js';
5
+ export * from './reference.js';
6
+ export * from './rule-config.js';
5
7
  export * from './codes.js';
6
8
  export * from './hash.js';
7
9
  export * from './version.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@ export * from './module-api.js';
2
2
  export * from './client.js';
3
3
  export * from './seats.js';
4
4
  export * from './manifest.js';
5
+ export * from './reference.js';
6
+ export * from './rule-config.js';
5
7
  export * from './codes.js';
6
8
  export * from './hash.js';
7
9
  export * from './version.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC"}
@@ -3,6 +3,58 @@ export interface GameUiOptions {
3
3
  /** 游戏内聊天由平台聊天卡提供,还是由游戏自己的界面提供。 */
4
4
  chat?: 'platform' | 'game';
5
5
  }
6
+ type RuleConfigFieldBase = {
7
+ key: string;
8
+ label: string;
9
+ description?: string;
10
+ };
11
+ /** 建房时房主需要填的一项规则;只声明表单项,值仍走 roomCreate.config。 */
12
+ export type RuleConfigField = (RuleConfigFieldBase & {
13
+ type: 'boolean';
14
+ default: boolean;
15
+ }) | (RuleConfigFieldBase & {
16
+ type: 'enum';
17
+ default: string;
18
+ options: Array<{
19
+ value: string;
20
+ label: string;
21
+ description?: string;
22
+ }>;
23
+ }) | (RuleConfigFieldBase & {
24
+ type: 'integer';
25
+ default: number;
26
+ min: number;
27
+ max: number;
28
+ step?: number;
29
+ });
30
+ /** 游戏的规则表单声明;平台据此渲染开关,规范化与校验仍由游戏和服务端负责。 */
31
+ export interface RuleConfigSpec {
32
+ fields: RuleConfigField[];
33
+ }
34
+ /** 游戏提供的静态资料入口;只给包内相对路径,内容懒加载。 */
35
+ export interface GameReferenceSpec {
36
+ entry: string;
37
+ }
38
+ export interface GameReferenceDocument {
39
+ version: 1;
40
+ title?: string;
41
+ groups: GameReferenceGroup[];
42
+ }
43
+ export interface GameReferenceGroup {
44
+ id: string;
45
+ title: string;
46
+ items: GameReferenceItem[];
47
+ }
48
+ export interface GameReferenceItem {
49
+ title?: string;
50
+ text: string;
51
+ images?: GameReferenceImage[];
52
+ }
53
+ export interface GameReferenceImage {
54
+ path: string;
55
+ alt: string;
56
+ caption?: string;
57
+ }
6
58
  /** 游戏模块对外声明的自己:房主建房、客户端校验、模块下发三处都要用它。 */
7
59
  export interface GameManifest {
8
60
  id: string;
@@ -26,10 +78,15 @@ export interface GameManifest {
26
78
  assets?: GameAsset[];
27
79
  /** 可选 UI 能力;缺省时按平台默认(聊天卡由平台显示)。 */
28
80
  ui?: GameUiOptions;
81
+ /** 建房时的规则表单;缺省表示这个游戏没有可配置项。 */
82
+ ruleConfig?: RuleConfigSpec;
83
+ /** 静态资料入口;只声明路径,文档由平台按需取。 */
84
+ reference?: GameReferenceSpec;
29
85
  }
30
86
  export interface GameAsset {
31
87
  path: string;
32
88
  hash: string;
33
89
  bytes?: number;
34
90
  }
91
+ export {};
35
92
  //# sourceMappingURL=manifest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,WAAW,aAAa;IAC5B,kCAAkC;IAClC,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;CAC5B;AAED,yCAAyC;AACzC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,aAAa,EAAE,OAAO,CAAC;IACvB,oCAAoC;IACpC,QAAQ,EAAE,OAAO,CAAC;IAClB,8BAA8B;IAC9B,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,mCAAmC;IACnC,EAAE,CAAC,EAAE,aAAa,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import type { NoticeCode, PlatformCode } from './codes.js';
2
- import type { GameUiOptions } from './manifest.js';
2
+ import type { GameReferenceSpec, GameUiOptions, RuleConfigSpec } from './manifest.js';
3
3
  import type { RoomPhase, SeatRole } from './seats.js';
4
4
  /**
5
5
  * 宿主与游戏模块之间的 ABI。
@@ -99,5 +99,9 @@ export interface GameModule {
99
99
  acceptsSnapshot?(snapshot: unknown): boolean;
100
100
  /** 可选 UI 声明;例如让游戏自己的聊天面板替代平台聊天卡。 */
101
101
  ui?: GameUiOptions;
102
+ /** 建房规则表单;只声明可配置项,值仍由 normalizeConfig 收口。 */
103
+ ruleConfig?: RuleConfigSpec;
104
+ /** 静态资料入口;manifest 只带路径,文档由平台懒加载。 */
105
+ reference?: GameReferenceSpec;
102
106
  }
103
107
  //# sourceMappingURL=module-api.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"module-api.d.ts","sourceRoot":"","sources":["../src/module-api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"module-api.d.ts","sourceRoot":"","sources":["../src/module-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACtF,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;GAKG;AAEH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,IAAI,CAAC;AAErC,MAAM,MAAM,MAAM,GACd;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GACnE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtD,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,MAAM,OAAO,GAAG,MAAM,EAAE,GAAG,MAAM,CAAC;AAExC,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAExD;AAED,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,MAAM,CAEnE;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,4BAA4B;IAC5B,GAAG,IAAI,MAAM,CAAC;IACd,MAAM,IAAI,MAAM,CAAC;IACjB,KAAK,IAAI,YAAY,EAAE,CAAC;IACxB,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;CAChD;AAED,MAAM,WAAW,YAAY;IAC3B,+BAA+B;IAC/B,SAAS,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC;IACrC,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAChD,oCAAoC;IACpC,KAAK,IAAI,OAAO,CAAC;IACjB,QAAQ,IAAI,OAAO,CAAC;IACpB,mCAAmC;IACnC,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC7B,UAAU,CAAC,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,EAAE,CAAC;IAC1C,WAAW,CAAC,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,EAAE,CAAC;IAC7D,4BAA4B;IAC5B,SAAS,CAAC,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,EAAE,CAAC;IACzC,OAAO,CAAC,IAAI,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,iDAAiD;IACjD,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnF,oCAAoC;IACpC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC;IACrD,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,WAAW,GAAG,YAAY,CAAC;IACxE,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,GAAG,YAAY,CAAC;IAC1D,wCAAwC;IACxC,eAAe,CAAC,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC;IAC7C,oCAAoC;IACpC,EAAE,CAAC,EAAE,aAAa,CAAC;IACnB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,qCAAqC;IACrC,SAAS,CAAC,EAAE,iBAAiB,CAAC;CAC/B"}
@@ -0,0 +1,36 @@
1
+ import type { GameReferenceDocument } from './manifest.js';
2
+ /**
3
+ * 游戏静态资料:平台只认纯文本 + 图片,不解析 HTML / Markdown。
4
+ * 这一份解析器由平台客户端和 sdk-tools 共用,校验口径只有一处。
5
+ */
6
+ export declare const GAME_REFERENCE_LIMITS: {
7
+ readonly documentBytes: number;
8
+ readonly groups: 20;
9
+ readonly items: 120;
10
+ readonly groupId: 64;
11
+ readonly documentTitle: 40;
12
+ readonly groupTitle: 40;
13
+ readonly itemTitle: 60;
14
+ readonly text: 2000;
15
+ readonly imagesPerItem: 6;
16
+ readonly entryLength: 256;
17
+ readonly alt: 120;
18
+ readonly caption: 120;
19
+ };
20
+ export type GameReferenceParseResult = {
21
+ ok: true;
22
+ doc: GameReferenceDocument;
23
+ } | {
24
+ ok: false;
25
+ error: string;
26
+ };
27
+ /** 包内相对路径:不接受绝对路径、反斜杠、`..` 段和空段。 */
28
+ export declare function isValidPackRelativePath(value: unknown): value is string;
29
+ /** reference.entry 指向包内 JSON 文档。 */
30
+ export declare function isValidGameReferenceEntry(value: unknown): value is string;
31
+ /**
32
+ * 校验并规范化资料文档。输入可以是已解析的对象,也可以是原始 JSON 文本;
33
+ * 失败时返回一句可以直接显示给开发者的中文原因。
34
+ */
35
+ export declare function parseGameReference(value: unknown): GameReferenceParseResult;
36
+ //# sourceMappingURL=reference.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reference.d.ts","sourceRoot":"","sources":["../src/reference.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EAItB,MAAM,eAAe,CAAC;AAEvB;;;GAGG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;CAaxB,CAAC;AAIX,MAAM,MAAM,wBAAwB,GAChC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,qBAAqB,CAAA;CAAE,GACxC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAoBjC,oCAAoC;AACpC,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAMvE;AAED,oCAAoC;AACpC,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEzE;AA+DD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,wBAAwB,CA2B3E"}
@@ -0,0 +1,161 @@
1
+ /**
2
+ * 游戏静态资料:平台只认纯文本 + 图片,不解析 HTML / Markdown。
3
+ * 这一份解析器由平台客户端和 sdk-tools 共用,校验口径只有一处。
4
+ */
5
+ export const GAME_REFERENCE_LIMITS = {
6
+ documentBytes: 64 * 1024,
7
+ groups: 20,
8
+ items: 120,
9
+ groupId: 64,
10
+ documentTitle: 40,
11
+ groupTitle: 40,
12
+ itemTitle: 60,
13
+ text: 2000,
14
+ imagesPerItem: 6,
15
+ entryLength: 256,
16
+ alt: 120,
17
+ caption: 120,
18
+ };
19
+ const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'webp', 'svg', 'gif'];
20
+ class ReferenceCheckError extends Error {
21
+ }
22
+ function fail(path, message) {
23
+ throw new ReferenceCheckError(path ? `${path} ${message}` : message);
24
+ }
25
+ function chars(value) {
26
+ return [...value].length;
27
+ }
28
+ function utf8Bytes(value) {
29
+ return new TextEncoder().encode(value).length;
30
+ }
31
+ function isObject(value) {
32
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
33
+ }
34
+ /** 包内相对路径:不接受绝对路径、反斜杠、`..` 段和空段。 */
35
+ export function isValidPackRelativePath(value) {
36
+ if (typeof value !== 'string' || value.length === 0)
37
+ return false;
38
+ if (value.length > GAME_REFERENCE_LIMITS.entryLength)
39
+ return false;
40
+ if (value.includes('\\') || value.startsWith('/') || /^[A-Za-z]:/.test(value))
41
+ return false;
42
+ if (/[\u0000-\u001f]/.test(value))
43
+ return false;
44
+ return value.split('/').every((part) => part !== '' && part !== '.' && part !== '..');
45
+ }
46
+ /** reference.entry 指向包内 JSON 文档。 */
47
+ export function isValidGameReferenceEntry(value) {
48
+ return isValidPackRelativePath(value) && /\.json$/i.test(value);
49
+ }
50
+ function readText(value, path, limit, required) {
51
+ if (value === undefined && !required)
52
+ return undefined;
53
+ if (typeof value !== 'string')
54
+ fail(path, '必须是字符串');
55
+ if (value.length === 0)
56
+ fail(path, '不能为空');
57
+ if (chars(value) > limit)
58
+ fail(path, `超过 ${limit} 字`);
59
+ return value;
60
+ }
61
+ function readImage(value, path) {
62
+ if (!isObject(value))
63
+ fail(path, '必须是对象');
64
+ const imagePath = value.path;
65
+ if (!isValidPackRelativePath(imagePath))
66
+ fail(`${path}.path`, '必须是合法的包内相对路径');
67
+ if (!IMAGE_EXTENSIONS.some((ext) => imagePath.toLowerCase().endsWith(`.${ext}`))) {
68
+ fail(`${path}.path`, `扩展名只允许 ${IMAGE_EXTENSIONS.join(' / ')}`);
69
+ }
70
+ const alt = readText(value.alt, `${path}.alt`, GAME_REFERENCE_LIMITS.alt, true);
71
+ const caption = readText(value.caption, `${path}.caption`, GAME_REFERENCE_LIMITS.caption, false);
72
+ return { path: imagePath, alt, ...(caption === undefined ? {} : { caption }) };
73
+ }
74
+ function readItem(value, path) {
75
+ if (!isObject(value))
76
+ fail(path, '必须是对象');
77
+ const title = readText(value.title, `${path}.title`, GAME_REFERENCE_LIMITS.itemTitle, false);
78
+ const text = readText(value.text, `${path}.text`, GAME_REFERENCE_LIMITS.text, true);
79
+ let images;
80
+ if (value.images !== undefined) {
81
+ if (!Array.isArray(value.images))
82
+ fail(`${path}.images`, '必须是数组');
83
+ if (value.images.length > GAME_REFERENCE_LIMITS.imagesPerItem) {
84
+ fail(`${path}.images`, `每项最多 ${GAME_REFERENCE_LIMITS.imagesPerItem} 张图`);
85
+ }
86
+ images = value.images.map((image, index) => readImage(image, `${path}.images[${index}]`));
87
+ }
88
+ return { text, ...(title === undefined ? {} : { title }), ...(images === undefined ? {} : { images }) };
89
+ }
90
+ function readGroup(value, path, seenIds) {
91
+ if (!isObject(value))
92
+ fail(path, '必须是对象');
93
+ if (typeof value.id !== 'string' || value.id.length === 0)
94
+ fail(`${path}.id`, '必须是非空字符串');
95
+ if (chars(value.id) > GAME_REFERENCE_LIMITS.groupId)
96
+ fail(`${path}.id`, `超过 ${GAME_REFERENCE_LIMITS.groupId} 字`);
97
+ if (seenIds.has(value.id))
98
+ fail(`${path}.id`, '与前面的分组重复');
99
+ seenIds.add(value.id);
100
+ const title = readText(value.title, `${path}.title`, GAME_REFERENCE_LIMITS.groupTitle, true);
101
+ if (!Array.isArray(value.items))
102
+ fail(`${path}.items`, '必须是数组');
103
+ const items = value.items.map((item, index) => readItem(item, `${path}.items[${index}]`));
104
+ return { id: value.id, title, items };
105
+ }
106
+ function parseDocument(value) {
107
+ if (!isObject(value))
108
+ fail('', '资料文档必须是对象');
109
+ if (value.version !== 1)
110
+ fail('version', '当前只支持 1');
111
+ const title = readText(value.title, 'title', GAME_REFERENCE_LIMITS.documentTitle, false);
112
+ if (!Array.isArray(value.groups))
113
+ fail('groups', '必须是数组');
114
+ if (value.groups.length === 0)
115
+ fail('groups', '至少要有一个分组');
116
+ if (value.groups.length > GAME_REFERENCE_LIMITS.groups)
117
+ fail('groups', `最多 ${GAME_REFERENCE_LIMITS.groups} 组`);
118
+ const seenIds = new Set();
119
+ const groups = value.groups.map((group, index) => readGroup(group, `groups[${index}]`, seenIds));
120
+ const totalItems = groups.reduce((sum, group) => sum + group.items.length, 0);
121
+ if (totalItems > GAME_REFERENCE_LIMITS.items)
122
+ fail('groups', `条数合计最多 ${GAME_REFERENCE_LIMITS.items} 条`);
123
+ return { version: 1, ...(title === undefined ? {} : { title }), groups };
124
+ }
125
+ /**
126
+ * 校验并规范化资料文档。输入可以是已解析的对象,也可以是原始 JSON 文本;
127
+ * 失败时返回一句可以直接显示给开发者的中文原因。
128
+ */
129
+ export function parseGameReference(value) {
130
+ try {
131
+ let raw = value;
132
+ if (typeof value === 'string') {
133
+ if (utf8Bytes(value) > GAME_REFERENCE_LIMITS.documentBytes) {
134
+ fail('', `文档超过 ${GAME_REFERENCE_LIMITS.documentBytes} 字节`);
135
+ }
136
+ try {
137
+ raw = JSON.parse(value);
138
+ }
139
+ catch {
140
+ fail('', '不是合法的 JSON');
141
+ }
142
+ }
143
+ let bytes;
144
+ try {
145
+ bytes = utf8Bytes(JSON.stringify(raw) ?? '');
146
+ }
147
+ catch {
148
+ fail('', '文档无法序列化');
149
+ }
150
+ if (bytes > GAME_REFERENCE_LIMITS.documentBytes) {
151
+ fail('', `文档超过 ${GAME_REFERENCE_LIMITS.documentBytes} 字节`);
152
+ }
153
+ return { ok: true, doc: parseDocument(raw) };
154
+ }
155
+ catch (error) {
156
+ if (error instanceof ReferenceCheckError)
157
+ return { ok: false, error: error.message };
158
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
159
+ }
160
+ }
161
+ //# sourceMappingURL=reference.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reference.js","sourceRoot":"","sources":["../src/reference.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { RuleConfigSpec } from './manifest.js';
2
+ /**
3
+ * 建房规则表单的校验口径。声明的是表单项,不是值;
4
+ * 值仍由 roomCreate.config 传入,由游戏 normalizeConfig 规范化、服务端校验。
5
+ */
6
+ export declare const RULE_CONFIG_LIMITS: {
7
+ readonly fields: 24;
8
+ readonly keyLength: 32;
9
+ readonly label: 40;
10
+ readonly description: 200;
11
+ readonly enumOptions: 8;
12
+ readonly enumValueLength: 32;
13
+ readonly enumLabel: 24;
14
+ readonly integerSpan: 1000;
15
+ };
16
+ export type RuleConfigParseResult = {
17
+ ok: true;
18
+ spec: RuleConfigSpec;
19
+ } | {
20
+ ok: false;
21
+ error: string;
22
+ };
23
+ /** 校验并规范化规则表单声明;失败时返回一句可以直接显示给开发者的中文原因。 */
24
+ export declare function parseRuleConfig(value: unknown): RuleConfigParseResult;
25
+ //# sourceMappingURL=rule-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule-config.d.ts","sourceRoot":"","sources":["../src/rule-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,cAAc,EAAE,MAAM,eAAe,CAAC;AAErE;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;CASrB,CAAC;AAKX,MAAM,MAAM,qBAAqB,GAC7B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,cAAc,CAAA;CAAE,GAClC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAsGjC,2CAA2C;AAC3C,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,CAarE"}
@@ -0,0 +1,147 @@
1
+ /**
2
+ * 建房规则表单的校验口径。声明的是表单项,不是值;
3
+ * 值仍由 roomCreate.config 传入,由游戏 normalizeConfig 规范化、服务端校验。
4
+ */
5
+ export const RULE_CONFIG_LIMITS = {
6
+ fields: 24,
7
+ keyLength: 32,
8
+ label: 40,
9
+ description: 200,
10
+ enumOptions: 8,
11
+ enumValueLength: 32,
12
+ enumLabel: 24,
13
+ integerSpan: 1000,
14
+ };
15
+ const KEY_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
16
+ const ENUM_VALUE_PATTERN = /^[a-z0-9-]{1,32}$/;
17
+ class RuleConfigCheckError extends Error {
18
+ }
19
+ function fail(path, message) {
20
+ throw new RuleConfigCheckError(path ? `${path} ${message}` : message);
21
+ }
22
+ function chars(value) {
23
+ return [...value].length;
24
+ }
25
+ function isObject(value) {
26
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
27
+ }
28
+ function readLabel(value, path, limit) {
29
+ if (typeof value !== 'string')
30
+ fail(path, '必须是字符串');
31
+ if (value.length === 0)
32
+ fail(path, '不能为空');
33
+ if (chars(value) > limit)
34
+ fail(path, `超过 ${limit} 字`);
35
+ return value;
36
+ }
37
+ function readDescription(value, path) {
38
+ if (value === undefined)
39
+ return undefined;
40
+ if (typeof value !== 'string')
41
+ fail(path, '必须是字符串');
42
+ if (chars(value) > RULE_CONFIG_LIMITS.description)
43
+ fail(path, `超过 ${RULE_CONFIG_LIMITS.description} 字`);
44
+ return value;
45
+ }
46
+ function readField(value, path, seenKeys) {
47
+ if (!isObject(value))
48
+ fail(path, '必须是对象');
49
+ if (typeof value.key !== 'string' || !KEY_PATTERN.test(value.key)) {
50
+ fail(`${path}.key`, '必须匹配 ^[a-z][a-z0-9-]{0,31}$');
51
+ }
52
+ if (seenKeys.has(value.key))
53
+ fail(`${path}.key`, '与前面的字段重复');
54
+ seenKeys.add(value.key);
55
+ const label = readLabel(value.label, `${path}.label`, RULE_CONFIG_LIMITS.label);
56
+ const description = readDescription(value.description, `${path}.description`);
57
+ if (value.type === 'boolean') {
58
+ if (typeof value.default !== 'boolean')
59
+ fail(`${path}.default`, 'boolean 字段的默认值必须是布尔值');
60
+ return { key: value.key, label, type: 'boolean', default: value.default, ...(description === undefined ? {} : { description }) };
61
+ }
62
+ if (value.type === 'enum') {
63
+ if (!Array.isArray(value.options))
64
+ fail(`${path}.options`, '必须是数组');
65
+ if (value.options.length === 0)
66
+ fail(`${path}.options`, '至少要有一个选项');
67
+ if (value.options.length > RULE_CONFIG_LIMITS.enumOptions) {
68
+ fail(`${path}.options`, `最多 ${RULE_CONFIG_LIMITS.enumOptions} 个选项`);
69
+ }
70
+ const seenValues = new Set();
71
+ const options = value.options.map((option, index) => {
72
+ const optionPath = `${path}.options[${index}]`;
73
+ if (!isObject(option))
74
+ fail(optionPath, '必须是对象');
75
+ if (typeof option.value !== 'string' || !ENUM_VALUE_PATTERN.test(option.value)) {
76
+ fail(`${optionPath}.value`, '必须匹配 ^[a-z0-9-]{1,32}$');
77
+ }
78
+ if (seenValues.has(option.value))
79
+ fail(`${optionPath}.value`, '与前面的选项重复');
80
+ seenValues.add(option.value);
81
+ const optionLabel = readLabel(option.label, `${optionPath}.label`, RULE_CONFIG_LIMITS.enumLabel);
82
+ const optionDescription = readDescription(option.description, `${optionPath}.description`);
83
+ return { value: option.value, label: optionLabel, ...(optionDescription === undefined ? {} : { description: optionDescription }) };
84
+ });
85
+ if (typeof value.default !== 'string')
86
+ fail(`${path}.default`, 'enum 字段的默认值必须是字符串');
87
+ if (!seenValues.has(value.default))
88
+ fail(`${path}.default`, '必须是 options 里的某个 value');
89
+ return { key: value.key, label, type: 'enum', default: value.default, options, ...(description === undefined ? {} : { description }) };
90
+ }
91
+ if (value.type === 'integer') {
92
+ if (typeof value.default !== 'number' || !Number.isSafeInteger(value.default)) {
93
+ fail(`${path}.default`, 'integer 字段的默认值必须是整数');
94
+ }
95
+ if (typeof value.min !== 'number' || !Number.isSafeInteger(value.min))
96
+ fail(`${path}.min`, '必须是整数');
97
+ if (typeof value.max !== 'number' || !Number.isSafeInteger(value.max))
98
+ fail(`${path}.max`, '必须是整数');
99
+ if (value.min > value.default)
100
+ fail(`${path}.min`, '不能大于 default');
101
+ if (value.default > value.max)
102
+ fail(`${path}.max`, '不能小于 default');
103
+ if (value.max - value.min > RULE_CONFIG_LIMITS.integerSpan) {
104
+ fail(`${path}.max`, `与 min 的跨度最多 ${RULE_CONFIG_LIMITS.integerSpan}`);
105
+ }
106
+ let step;
107
+ if (value.step !== undefined) {
108
+ if (typeof value.step !== 'number' || !Number.isSafeInteger(value.step) || value.step <= 0) {
109
+ fail(`${path}.step`, '必须是正整数');
110
+ }
111
+ step = value.step;
112
+ }
113
+ return {
114
+ key: value.key,
115
+ label,
116
+ type: 'integer',
117
+ default: value.default,
118
+ min: value.min,
119
+ max: value.max,
120
+ ...(step === undefined ? {} : { step }),
121
+ ...(description === undefined ? {} : { description }),
122
+ };
123
+ }
124
+ return fail(`${path}.type`, '只支持 boolean / enum / integer');
125
+ }
126
+ /** 校验并规范化规则表单声明;失败时返回一句可以直接显示给开发者的中文原因。 */
127
+ export function parseRuleConfig(value) {
128
+ try {
129
+ if (!isObject(value))
130
+ fail('', '规则表单声明必须是对象');
131
+ if (!Array.isArray(value.fields))
132
+ fail('fields', '必须是数组');
133
+ if (value.fields.length === 0)
134
+ fail('fields', '至少要有一项');
135
+ if (value.fields.length > RULE_CONFIG_LIMITS.fields)
136
+ fail('fields', `最多 ${RULE_CONFIG_LIMITS.fields} 项`);
137
+ const seenKeys = new Set();
138
+ const fields = value.fields.map((field, index) => readField(field, `fields[${index}]`, seenKeys));
139
+ return { ok: true, spec: { fields } };
140
+ }
141
+ catch (error) {
142
+ if (error instanceof RuleConfigCheckError)
143
+ return { ok: false, error: error.message };
144
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
145
+ }
146
+ }
147
+ //# sourceMappingURL=rule-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule-config.js","sourceRoot":"","sources":["../src/rule-config.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,MAAM,EAAE,EAAE;IACV,SAAS,EAAE,EAAE;IACb,KAAK,EAAE,EAAE;IACT,WAAW,EAAE,GAAG;IAChB,WAAW,EAAE,CAAC;IACd,eAAe,EAAE,EAAE;IACnB,SAAS,EAAE,EAAE;IACb,WAAW,EAAE,IAAI;CACT,CAAC;AAEX,MAAM,WAAW,GAAG,wBAAwB,CAAC;AAC7C,MAAM,kBAAkB,GAAG,mBAAmB,CAAC;AAM/C,MAAM,oBAAqB,SAAQ,KAAK;CAAG;AAE3C,SAAS,IAAI,CAAC,IAAY,EAAE,OAAe;IACzC,MAAM,IAAI,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;AACxE,CAAC;AAED,SAAS,KAAK,CAAC,KAAa;IAC1B,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC;AAC3B,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,SAAS,CAAC,KAAc,EAAE,IAAY,EAAE,KAAa;IAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACpD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC3C,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,KAAK;QAAE,IAAI,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC,CAAC;IACtD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,eAAe,CAAC,KAAc,EAAE,IAAY;IACnD,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACpD,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,kBAAkB,CAAC,WAAW;QAAE,IAAI,CAAC,IAAI,EAAE,MAAM,kBAAkB,CAAC,WAAW,IAAI,CAAC,CAAC;IACxG,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,SAAS,CAAC,KAAc,EAAE,IAAY,EAAE,QAAqB;IACpE,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC1C,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAClE,IAAI,CAAC,GAAG,IAAI,MAAM,EAAE,6BAA6B,CAAC,CAAC;IACrD,CAAC;IACD,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC;QAAE,IAAI,CAAC,GAAG,IAAI,MAAM,EAAE,UAAU,CAAC,CAAC;IAC7D,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACxB,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,IAAI,QAAQ,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAChF,MAAM,WAAW,GAAG,eAAe,CAAC,KAAK,CAAC,WAAW,EAAE,GAAG,IAAI,cAAc,CAAC,CAAC;IAE9E,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC7B,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,SAAS;YAAE,IAAI,CAAC,GAAG,IAAI,UAAU,EAAE,sBAAsB,CAAC,CAAC;QACxF,OAAO,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IACnI,CAAC;IAED,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC1B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC;YAAE,IAAI,CAAC,GAAG,IAAI,UAAU,EAAE,OAAO,CAAC,CAAC;QACpE,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,IAAI,CAAC,GAAG,IAAI,UAAU,EAAE,UAAU,CAAC,CAAC;QACpE,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,kBAAkB,CAAC,WAAW,EAAE,CAAC;YAC1D,IAAI,CAAC,GAAG,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAC,WAAW,MAAM,CAAC,CAAC;QACtE,CAAC;QACD,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;QACrC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;YAClD,MAAM,UAAU,GAAG,GAAG,IAAI,YAAY,KAAK,GAAG,CAAC;YAC/C,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAAE,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YACjD,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC/E,IAAI,CAAC,GAAG,UAAU,QAAQ,EAAE,wBAAwB,CAAC,CAAC;YACxD,CAAC;YACD,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC;gBAAE,IAAI,CAAC,GAAG,UAAU,QAAQ,EAAE,UAAU,CAAC,CAAC;YAC1E,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC7B,MAAM,WAAW,GAAG,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,UAAU,QAAQ,EAAE,kBAAkB,CAAC,SAAS,CAAC,CAAC;YACjG,MAAM,iBAAiB,GAAG,eAAe,CAAC,MAAM,CAAC,WAAW,EAAE,GAAG,UAAU,cAAc,CAAC,CAAC;YAC3F,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;QACrI,CAAC,CAAC,CAAC;QACH,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ;YAAE,IAAI,CAAC,GAAG,IAAI,UAAU,EAAE,mBAAmB,CAAC,CAAC;QACpF,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC;YAAE,IAAI,CAAC,GAAG,IAAI,UAAU,EAAE,wBAAwB,CAAC,CAAC;QACtF,OAAO,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IACzI,CAAC;IAED,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC7B,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9E,IAAI,CAAC,GAAG,IAAI,UAAU,EAAE,qBAAqB,CAAC,CAAC;QACjD,CAAC;QACD,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,GAAG,IAAI,MAAM,EAAE,OAAO,CAAC,CAAC;QACpG,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,GAAG,IAAI,MAAM,EAAE,OAAO,CAAC,CAAC;QACpG,IAAI,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC,OAAO;YAAE,IAAI,CAAC,GAAG,IAAI,MAAM,EAAE,cAAc,CAAC,CAAC;QACnE,IAAI,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,GAAG;YAAE,IAAI,CAAC,GAAG,IAAI,MAAM,EAAE,cAAc,CAAC,CAAC;QACnE,IAAI,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC,GAAG,GAAG,kBAAkB,CAAC,WAAW,EAAE,CAAC;YAC3D,IAAI,CAAC,GAAG,IAAI,MAAM,EAAE,eAAe,kBAAkB,CAAC,WAAW,EAAE,CAAC,CAAC;QACvE,CAAC;QACD,IAAI,IAAwB,CAAC;QAC7B,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC7B,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC;gBAC3F,IAAI,CAAC,GAAG,IAAI,OAAO,EAAE,QAAQ,CAAC,CAAC;YACjC,CAAC;YACD,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACpB,CAAC;QACD,OAAO;YACL,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,KAAK;YACL,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;YACvC,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;SACtD,CAAC;IACJ,CAAC;IAED,OAAO,IAAI,CAAC,GAAG,IAAI,OAAO,EAAE,8BAA8B,CAAC,CAAC;AAC9D,CAAC;AAED,2CAA2C;AAC3C,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,IAAI,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,IAAI,CAAC,EAAE,EAAE,aAAa,CAAC,CAAC;QAC9C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC;YAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC1D,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QACxD,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,kBAAkB,CAAC,MAAM;YAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,kBAAkB,CAAC,MAAM,IAAI,CAAC,CAAC;QACzG,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;QACnC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,UAAU,KAAK,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC;QAClG,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC;IACxC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,oBAAoB;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;QACtF,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;IACtF,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lizardon386/core-abi",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "3178 游戏接口层:GameModule / GameInstance / Effect 等宿主与游戏共同遵守的 ABI 和数据类型",
5
5
  "license": "MIT",
6
6
  "author": "3178-battle-platform contributors",
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@ export * from './module-api.js';
2
2
  export * from './client.js';
3
3
  export * from './seats.js';
4
4
  export * from './manifest.js';
5
+ export * from './reference.js';
6
+ export * from './rule-config.js';
5
7
  export * from './codes.js';
6
8
  export * from './hash.js';
7
9
  export * from './version.js';
package/src/manifest.ts CHANGED
@@ -5,6 +5,56 @@ export interface GameUiOptions {
5
5
  chat?: 'platform' | 'game';
6
6
  }
7
7
 
8
+ type RuleConfigFieldBase = {
9
+ key: string;
10
+ label: string;
11
+ description?: string;
12
+ };
13
+
14
+ /** 建房时房主需要填的一项规则;只声明表单项,值仍走 roomCreate.config。 */
15
+ export type RuleConfigField =
16
+ | (RuleConfigFieldBase & { type: 'boolean'; default: boolean })
17
+ | (RuleConfigFieldBase & {
18
+ type: 'enum';
19
+ default: string;
20
+ options: Array<{ value: string; label: string; description?: string }>;
21
+ })
22
+ | (RuleConfigFieldBase & { type: 'integer'; default: number; min: number; max: number; step?: number });
23
+
24
+ /** 游戏的规则表单声明;平台据此渲染开关,规范化与校验仍由游戏和服务端负责。 */
25
+ export interface RuleConfigSpec {
26
+ fields: RuleConfigField[];
27
+ }
28
+
29
+ /** 游戏提供的静态资料入口;只给包内相对路径,内容懒加载。 */
30
+ export interface GameReferenceSpec {
31
+ entry: string;
32
+ }
33
+
34
+ export interface GameReferenceDocument {
35
+ version: 1;
36
+ title?: string;
37
+ groups: GameReferenceGroup[];
38
+ }
39
+
40
+ export interface GameReferenceGroup {
41
+ id: string;
42
+ title: string;
43
+ items: GameReferenceItem[];
44
+ }
45
+
46
+ export interface GameReferenceItem {
47
+ title?: string;
48
+ text: string;
49
+ images?: GameReferenceImage[];
50
+ }
51
+
52
+ export interface GameReferenceImage {
53
+ path: string;
54
+ alt: string;
55
+ caption?: string;
56
+ }
57
+
8
58
  /** 游戏模块对外声明的自己:房主建房、客户端校验、模块下发三处都要用它。 */
9
59
  export interface GameManifest {
10
60
  id: string;
@@ -25,6 +75,10 @@ export interface GameManifest {
25
75
  assets?: GameAsset[];
26
76
  /** 可选 UI 能力;缺省时按平台默认(聊天卡由平台显示)。 */
27
77
  ui?: GameUiOptions;
78
+ /** 建房时的规则表单;缺省表示这个游戏没有可配置项。 */
79
+ ruleConfig?: RuleConfigSpec;
80
+ /** 静态资料入口;只声明路径,文档由平台按需取。 */
81
+ reference?: GameReferenceSpec;
28
82
  }
29
83
 
30
84
  export interface GameAsset {
package/src/module-api.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { NoticeCode, PlatformCode } from './codes.js';
2
- import type { GameUiOptions } from './manifest.js';
2
+ import type { GameReferenceSpec, GameUiOptions, RuleConfigSpec } from './manifest.js';
3
3
  import type { RoomPhase, SeatRole } from './seats.js';
4
4
 
5
5
  /**
@@ -95,4 +95,8 @@ export interface GameModule {
95
95
  acceptsSnapshot?(snapshot: unknown): boolean;
96
96
  /** 可选 UI 声明;例如让游戏自己的聊天面板替代平台聊天卡。 */
97
97
  ui?: GameUiOptions;
98
+ /** 建房规则表单;只声明可配置项,值仍由 normalizeConfig 收口。 */
99
+ ruleConfig?: RuleConfigSpec;
100
+ /** 静态资料入口;manifest 只带路径,文档由平台懒加载。 */
101
+ reference?: GameReferenceSpec;
98
102
  }
@@ -0,0 +1,157 @@
1
+ import type {
2
+ GameReferenceDocument,
3
+ GameReferenceGroup,
4
+ GameReferenceImage,
5
+ GameReferenceItem,
6
+ } from './manifest.js';
7
+
8
+ /**
9
+ * 游戏静态资料:平台只认纯文本 + 图片,不解析 HTML / Markdown。
10
+ * 这一份解析器由平台客户端和 sdk-tools 共用,校验口径只有一处。
11
+ */
12
+ export const GAME_REFERENCE_LIMITS = {
13
+ documentBytes: 64 * 1024,
14
+ groups: 20,
15
+ items: 120,
16
+ groupId: 64,
17
+ documentTitle: 40,
18
+ groupTitle: 40,
19
+ itemTitle: 60,
20
+ text: 2000,
21
+ imagesPerItem: 6,
22
+ entryLength: 256,
23
+ alt: 120,
24
+ caption: 120,
25
+ } as const;
26
+
27
+ const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'webp', 'svg', 'gif'];
28
+
29
+ export type GameReferenceParseResult =
30
+ | { ok: true; doc: GameReferenceDocument }
31
+ | { ok: false; error: string };
32
+
33
+ class ReferenceCheckError extends Error {}
34
+
35
+ function fail(path: string, message: string): never {
36
+ throw new ReferenceCheckError(path ? `${path} ${message}` : message);
37
+ }
38
+
39
+ function chars(value: string): number {
40
+ return [...value].length;
41
+ }
42
+
43
+ function utf8Bytes(value: string): number {
44
+ return new TextEncoder().encode(value).length;
45
+ }
46
+
47
+ function isObject(value: unknown): value is Record<string, unknown> {
48
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
49
+ }
50
+
51
+ /** 包内相对路径:不接受绝对路径、反斜杠、`..` 段和空段。 */
52
+ export function isValidPackRelativePath(value: unknown): value is string {
53
+ if (typeof value !== 'string' || value.length === 0) return false;
54
+ if (value.length > GAME_REFERENCE_LIMITS.entryLength) return false;
55
+ if (value.includes('\\') || value.startsWith('/') || /^[A-Za-z]:/.test(value)) return false;
56
+ if (/[\u0000-\u001f]/.test(value)) return false;
57
+ return value.split('/').every((part) => part !== '' && part !== '.' && part !== '..');
58
+ }
59
+
60
+ /** reference.entry 指向包内 JSON 文档。 */
61
+ export function isValidGameReferenceEntry(value: unknown): value is string {
62
+ return isValidPackRelativePath(value) && /\.json$/i.test(value);
63
+ }
64
+
65
+ function readText(value: unknown, path: string, limit: number, required: boolean): string | undefined {
66
+ if (value === undefined && !required) return undefined;
67
+ if (typeof value !== 'string') fail(path, '必须是字符串');
68
+ if (value.length === 0) fail(path, '不能为空');
69
+ if (chars(value) > limit) fail(path, `超过 ${limit} 字`);
70
+ return value;
71
+ }
72
+
73
+ function readImage(value: unknown, path: string): GameReferenceImage {
74
+ if (!isObject(value)) fail(path, '必须是对象');
75
+ const imagePath = value.path;
76
+ if (!isValidPackRelativePath(imagePath)) fail(`${path}.path`, '必须是合法的包内相对路径');
77
+ if (!IMAGE_EXTENSIONS.some((ext) => imagePath.toLowerCase().endsWith(`.${ext}`))) {
78
+ fail(`${path}.path`, `扩展名只允许 ${IMAGE_EXTENSIONS.join(' / ')}`);
79
+ }
80
+ const alt = readText(value.alt, `${path}.alt`, GAME_REFERENCE_LIMITS.alt, true) as string;
81
+ const caption = readText(value.caption, `${path}.caption`, GAME_REFERENCE_LIMITS.caption, false);
82
+ return { path: imagePath, alt, ...(caption === undefined ? {} : { caption }) };
83
+ }
84
+
85
+ function readItem(value: unknown, path: string): GameReferenceItem {
86
+ if (!isObject(value)) fail(path, '必须是对象');
87
+ const title = readText(value.title, `${path}.title`, GAME_REFERENCE_LIMITS.itemTitle, false);
88
+ const text = readText(value.text, `${path}.text`, GAME_REFERENCE_LIMITS.text, true) as string;
89
+ let images: GameReferenceImage[] | undefined;
90
+ if (value.images !== undefined) {
91
+ if (!Array.isArray(value.images)) fail(`${path}.images`, '必须是数组');
92
+ if (value.images.length > GAME_REFERENCE_LIMITS.imagesPerItem) {
93
+ fail(`${path}.images`, `每项最多 ${GAME_REFERENCE_LIMITS.imagesPerItem} 张图`);
94
+ }
95
+ images = value.images.map((image, index) => readImage(image, `${path}.images[${index}]`));
96
+ }
97
+ return { text, ...(title === undefined ? {} : { title }), ...(images === undefined ? {} : { images }) };
98
+ }
99
+
100
+ function readGroup(value: unknown, path: string, seenIds: Set<string>): GameReferenceGroup {
101
+ if (!isObject(value)) fail(path, '必须是对象');
102
+ if (typeof value.id !== 'string' || value.id.length === 0) fail(`${path}.id`, '必须是非空字符串');
103
+ if (chars(value.id) > GAME_REFERENCE_LIMITS.groupId) fail(`${path}.id`, `超过 ${GAME_REFERENCE_LIMITS.groupId} 字`);
104
+ if (seenIds.has(value.id)) fail(`${path}.id`, '与前面的分组重复');
105
+ seenIds.add(value.id);
106
+ const title = readText(value.title, `${path}.title`, GAME_REFERENCE_LIMITS.groupTitle, true) as string;
107
+ if (!Array.isArray(value.items)) fail(`${path}.items`, '必须是数组');
108
+ const items = value.items.map((item, index) => readItem(item, `${path}.items[${index}]`));
109
+ return { id: value.id, title, items };
110
+ }
111
+
112
+ function parseDocument(value: unknown): GameReferenceDocument {
113
+ if (!isObject(value)) fail('', '资料文档必须是对象');
114
+ if (value.version !== 1) fail('version', '当前只支持 1');
115
+ const title = readText(value.title, 'title', GAME_REFERENCE_LIMITS.documentTitle, false);
116
+ if (!Array.isArray(value.groups)) fail('groups', '必须是数组');
117
+ if (value.groups.length === 0) fail('groups', '至少要有一个分组');
118
+ if (value.groups.length > GAME_REFERENCE_LIMITS.groups) fail('groups', `最多 ${GAME_REFERENCE_LIMITS.groups} 组`);
119
+ const seenIds = new Set<string>();
120
+ const groups = value.groups.map((group, index) => readGroup(group, `groups[${index}]`, seenIds));
121
+ const totalItems = groups.reduce((sum, group) => sum + group.items.length, 0);
122
+ if (totalItems > GAME_REFERENCE_LIMITS.items) fail('groups', `条数合计最多 ${GAME_REFERENCE_LIMITS.items} 条`);
123
+ return { version: 1, ...(title === undefined ? {} : { title }), groups };
124
+ }
125
+
126
+ /**
127
+ * 校验并规范化资料文档。输入可以是已解析的对象,也可以是原始 JSON 文本;
128
+ * 失败时返回一句可以直接显示给开发者的中文原因。
129
+ */
130
+ export function parseGameReference(value: unknown): GameReferenceParseResult {
131
+ try {
132
+ let raw = value;
133
+ if (typeof value === 'string') {
134
+ if (utf8Bytes(value) > GAME_REFERENCE_LIMITS.documentBytes) {
135
+ fail('', `文档超过 ${GAME_REFERENCE_LIMITS.documentBytes} 字节`);
136
+ }
137
+ try {
138
+ raw = JSON.parse(value) as unknown;
139
+ } catch {
140
+ fail('', '不是合法的 JSON');
141
+ }
142
+ }
143
+ let bytes: number;
144
+ try {
145
+ bytes = utf8Bytes(JSON.stringify(raw) ?? '');
146
+ } catch {
147
+ fail('', '文档无法序列化');
148
+ }
149
+ if (bytes > GAME_REFERENCE_LIMITS.documentBytes) {
150
+ fail('', `文档超过 ${GAME_REFERENCE_LIMITS.documentBytes} 字节`);
151
+ }
152
+ return { ok: true, doc: parseDocument(raw) };
153
+ } catch (error) {
154
+ if (error instanceof ReferenceCheckError) return { ok: false, error: error.message };
155
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
156
+ }
157
+ }
@@ -0,0 +1,139 @@
1
+ import type { RuleConfigField, RuleConfigSpec } from './manifest.js';
2
+
3
+ /**
4
+ * 建房规则表单的校验口径。声明的是表单项,不是值;
5
+ * 值仍由 roomCreate.config 传入,由游戏 normalizeConfig 规范化、服务端校验。
6
+ */
7
+ export const RULE_CONFIG_LIMITS = {
8
+ fields: 24,
9
+ keyLength: 32,
10
+ label: 40,
11
+ description: 200,
12
+ enumOptions: 8,
13
+ enumValueLength: 32,
14
+ enumLabel: 24,
15
+ integerSpan: 1000,
16
+ } as const;
17
+
18
+ const KEY_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
19
+ const ENUM_VALUE_PATTERN = /^[a-z0-9-]{1,32}$/;
20
+
21
+ export type RuleConfigParseResult =
22
+ | { ok: true; spec: RuleConfigSpec }
23
+ | { ok: false; error: string };
24
+
25
+ class RuleConfigCheckError extends Error {}
26
+
27
+ function fail(path: string, message: string): never {
28
+ throw new RuleConfigCheckError(path ? `${path} ${message}` : message);
29
+ }
30
+
31
+ function chars(value: string): number {
32
+ return [...value].length;
33
+ }
34
+
35
+ function isObject(value: unknown): value is Record<string, unknown> {
36
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
37
+ }
38
+
39
+ function readLabel(value: unknown, path: string, limit: number): string {
40
+ if (typeof value !== 'string') fail(path, '必须是字符串');
41
+ if (value.length === 0) fail(path, '不能为空');
42
+ if (chars(value) > limit) fail(path, `超过 ${limit} 字`);
43
+ return value;
44
+ }
45
+
46
+ function readDescription(value: unknown, path: string): string | undefined {
47
+ if (value === undefined) return undefined;
48
+ if (typeof value !== 'string') fail(path, '必须是字符串');
49
+ if (chars(value) > RULE_CONFIG_LIMITS.description) fail(path, `超过 ${RULE_CONFIG_LIMITS.description} 字`);
50
+ return value;
51
+ }
52
+
53
+ function readField(value: unknown, path: string, seenKeys: Set<string>): RuleConfigField {
54
+ if (!isObject(value)) fail(path, '必须是对象');
55
+ if (typeof value.key !== 'string' || !KEY_PATTERN.test(value.key)) {
56
+ fail(`${path}.key`, '必须匹配 ^[a-z][a-z0-9-]{0,31}$');
57
+ }
58
+ if (seenKeys.has(value.key)) fail(`${path}.key`, '与前面的字段重复');
59
+ seenKeys.add(value.key);
60
+ const label = readLabel(value.label, `${path}.label`, RULE_CONFIG_LIMITS.label);
61
+ const description = readDescription(value.description, `${path}.description`);
62
+
63
+ if (value.type === 'boolean') {
64
+ if (typeof value.default !== 'boolean') fail(`${path}.default`, 'boolean 字段的默认值必须是布尔值');
65
+ return { key: value.key, label, type: 'boolean', default: value.default, ...(description === undefined ? {} : { description }) };
66
+ }
67
+
68
+ if (value.type === 'enum') {
69
+ if (!Array.isArray(value.options)) fail(`${path}.options`, '必须是数组');
70
+ if (value.options.length === 0) fail(`${path}.options`, '至少要有一个选项');
71
+ if (value.options.length > RULE_CONFIG_LIMITS.enumOptions) {
72
+ fail(`${path}.options`, `最多 ${RULE_CONFIG_LIMITS.enumOptions} 个选项`);
73
+ }
74
+ const seenValues = new Set<string>();
75
+ const options = value.options.map((option, index) => {
76
+ const optionPath = `${path}.options[${index}]`;
77
+ if (!isObject(option)) fail(optionPath, '必须是对象');
78
+ if (typeof option.value !== 'string' || !ENUM_VALUE_PATTERN.test(option.value)) {
79
+ fail(`${optionPath}.value`, '必须匹配 ^[a-z0-9-]{1,32}$');
80
+ }
81
+ if (seenValues.has(option.value)) fail(`${optionPath}.value`, '与前面的选项重复');
82
+ seenValues.add(option.value);
83
+ const optionLabel = readLabel(option.label, `${optionPath}.label`, RULE_CONFIG_LIMITS.enumLabel);
84
+ const optionDescription = readDescription(option.description, `${optionPath}.description`);
85
+ return { value: option.value, label: optionLabel, ...(optionDescription === undefined ? {} : { description: optionDescription }) };
86
+ });
87
+ if (typeof value.default !== 'string') fail(`${path}.default`, 'enum 字段的默认值必须是字符串');
88
+ if (!seenValues.has(value.default)) fail(`${path}.default`, '必须是 options 里的某个 value');
89
+ return { key: value.key, label, type: 'enum', default: value.default, options, ...(description === undefined ? {} : { description }) };
90
+ }
91
+
92
+ if (value.type === 'integer') {
93
+ if (typeof value.default !== 'number' || !Number.isSafeInteger(value.default)) {
94
+ fail(`${path}.default`, 'integer 字段的默认值必须是整数');
95
+ }
96
+ if (typeof value.min !== 'number' || !Number.isSafeInteger(value.min)) fail(`${path}.min`, '必须是整数');
97
+ if (typeof value.max !== 'number' || !Number.isSafeInteger(value.max)) fail(`${path}.max`, '必须是整数');
98
+ if (value.min > value.default) fail(`${path}.min`, '不能大于 default');
99
+ if (value.default > value.max) fail(`${path}.max`, '不能小于 default');
100
+ if (value.max - value.min > RULE_CONFIG_LIMITS.integerSpan) {
101
+ fail(`${path}.max`, `与 min 的跨度最多 ${RULE_CONFIG_LIMITS.integerSpan}`);
102
+ }
103
+ let step: number | undefined;
104
+ if (value.step !== undefined) {
105
+ if (typeof value.step !== 'number' || !Number.isSafeInteger(value.step) || value.step <= 0) {
106
+ fail(`${path}.step`, '必须是正整数');
107
+ }
108
+ step = value.step;
109
+ }
110
+ return {
111
+ key: value.key,
112
+ label,
113
+ type: 'integer',
114
+ default: value.default,
115
+ min: value.min,
116
+ max: value.max,
117
+ ...(step === undefined ? {} : { step }),
118
+ ...(description === undefined ? {} : { description }),
119
+ };
120
+ }
121
+
122
+ return fail(`${path}.type`, '只支持 boolean / enum / integer');
123
+ }
124
+
125
+ /** 校验并规范化规则表单声明;失败时返回一句可以直接显示给开发者的中文原因。 */
126
+ export function parseRuleConfig(value: unknown): RuleConfigParseResult {
127
+ try {
128
+ if (!isObject(value)) fail('', '规则表单声明必须是对象');
129
+ if (!Array.isArray(value.fields)) fail('fields', '必须是数组');
130
+ if (value.fields.length === 0) fail('fields', '至少要有一项');
131
+ if (value.fields.length > RULE_CONFIG_LIMITS.fields) fail('fields', `最多 ${RULE_CONFIG_LIMITS.fields} 项`);
132
+ const seenKeys = new Set<string>();
133
+ const fields = value.fields.map((field, index) => readField(field, `fields[${index}]`, seenKeys));
134
+ return { ok: true, spec: { fields } };
135
+ } catch (error) {
136
+ if (error instanceof RuleConfigCheckError) return { ok: false, error: error.message };
137
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
138
+ }
139
+ }