@sfmc-bds/db-server 0.5.2 → 0.6.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.
Files changed (55) hide show
  1. package/dist/domain/bind-providers.d.ts +59 -0
  2. package/dist/domain/bind-providers.d.ts.map +1 -0
  3. package/dist/domain/bind-providers.js +33 -0
  4. package/dist/domain/bind-providers.js.map +7 -0
  5. package/dist/domain/metrics.d.ts +5 -0
  6. package/dist/domain/metrics.d.ts.map +1 -1
  7. package/dist/domain/metrics.js +28 -2
  8. package/dist/domain/metrics.js.map +2 -2
  9. package/dist/domain/player-level.d.ts +29 -0
  10. package/dist/domain/player-level.d.ts.map +1 -0
  11. package/dist/domain/player-level.js +30 -0
  12. package/dist/domain/player-level.js.map +7 -0
  13. package/dist/domain/process-resources.d.ts +12 -0
  14. package/dist/domain/process-resources.d.ts.map +1 -1
  15. package/dist/domain/process-resources.js +72 -1
  16. package/dist/domain/process-resources.js.map +2 -2
  17. package/dist/domain/qq-bind.d.ts +21 -0
  18. package/dist/domain/qq-bind.d.ts.map +1 -0
  19. package/dist/domain/qq-bind.js +54 -0
  20. package/dist/domain/qq-bind.js.map +7 -0
  21. package/dist/domain/schema.d.ts +1 -0
  22. package/dist/domain/schema.d.ts.map +1 -1
  23. package/dist/domain/schema.js +26 -0
  24. package/dist/domain/schema.js.map +2 -2
  25. package/dist/domain/tracked-processes.d.ts +30 -0
  26. package/dist/domain/tracked-processes.d.ts.map +1 -0
  27. package/dist/domain/tracked-processes.js +78 -0
  28. package/dist/domain/tracked-processes.js.map +7 -0
  29. package/dist/domain/wechat-bind.d.ts +56 -0
  30. package/dist/domain/wechat-bind.d.ts.map +1 -0
  31. package/dist/domain/wechat-bind.js +84 -0
  32. package/dist/domain/wechat-bind.js.map +7 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +37 -7
  35. package/dist/index.js.map +2 -2
  36. package/dist/lib/sqlite.d.ts +10 -0
  37. package/dist/lib/sqlite.d.ts.map +1 -1
  38. package/dist/lib/sqlite.js +17 -2
  39. package/dist/lib/sqlite.js.map +2 -2
  40. package/dist/routes/bind-confirm.d.ts +30 -0
  41. package/dist/routes/bind-confirm.d.ts.map +1 -0
  42. package/dist/routes/bind-confirm.js +27 -0
  43. package/dist/routes/bind-confirm.js.map +7 -0
  44. package/dist/routes/qq-bind.d.ts +3 -1
  45. package/dist/routes/qq-bind.d.ts.map +1 -1
  46. package/dist/routes/qq-bind.js +8 -64
  47. package/dist/routes/qq-bind.js.map +2 -2
  48. package/dist/routes/status.d.ts.map +1 -1
  49. package/dist/routes/status.js +10 -2
  50. package/dist/routes/status.js.map +2 -2
  51. package/dist/routes/wechat-bind.d.ts +39 -0
  52. package/dist/routes/wechat-bind.d.ts.map +1 -0
  53. package/dist/routes/wechat-bind.js +56 -0
  54. package/dist/routes/wechat-bind.js.map +7 -0
  55. package/package.json +5 -4
@@ -0,0 +1,59 @@
1
+ /**
2
+ * domain/bind-providers.ts — 外部账号 ↔ 游戏账号绑定的提供方抽象与短码生成
3
+ *
4
+ * 使用场景:QQ 与微信两种绑定共用游戏内同一个 `/c:bind` 流程。
5
+ * - routes/bind-confirm.ts 按注册顺序询问每个提供方“这个短码是不是你的”,交给对应提供方确认(OCP:新增渠道只需注册新提供方);
6
+ * - 各渠道申请短码时用 generateBindCode 生成,在所有提供方的待确认表里都不重复,保证统一确认入口不会认错渠道。
7
+ */
8
+ /** 绑定短码有效期(与 QQ 原有流程一致) */
9
+ export declare const BIND_CODE_TTL_MS: number;
10
+ /** 绑定短码位数(与 QQ 原有流程一致;游戏侧接受 4–8 位数字) */
11
+ export declare const BIND_CODE_LEN = 6;
12
+ /** 游戏侧提交的确认请求 */
13
+ export interface BindConfirmInput {
14
+ code: string;
15
+ xuid: string;
16
+ name: string;
17
+ now: number;
18
+ }
19
+ /** 确认结果:成功带绑定记录;失败带 HTTP 状态与错误码(错误码与 QQ 原接口一致,游戏侧据此提示) */
20
+ export type BindConfirmResult = {
21
+ ok: true;
22
+ binding: Record<string, unknown>;
23
+ } | {
24
+ ok: false;
25
+ status: number;
26
+ error: string;
27
+ hint?: string;
28
+ };
29
+ /**
30
+ * 绑定提供方契约。
31
+ * 使用场景:QQ(domain/qq-bind.ts)与微信(domain/wechat-bind.ts)各实现一份,由 index.ts 组装成列表注入统一确认路由(LSP:确认语义一致)。
32
+ */
33
+ export interface BindProvider {
34
+ /** 渠道标识,返回给游戏侧用于提示文案(qq / wechat) */
35
+ readonly id: string;
36
+ /** 待确认表里是否存在该短码(不判断过期,过期交给 confirm 返回 code_expired) */
37
+ hasPendingCode(code: string): boolean;
38
+ /** 用短码完成绑定 */
39
+ confirm(input: BindConfirmInput): BindConfirmResult;
40
+ }
41
+ /**
42
+ * 生成在所有提供方中都未被占用的短码;多次碰撞后返回空串,由调用方回 code_gen_failed。
43
+ *
44
+ * @param isTaken 判断短码是否已被任一提供方占用。
45
+ */
46
+ export declare function generateBindCode(isTaken: (code: string) => boolean, length?: number): string;
47
+ /**
48
+ * 由提供方列表构造“短码是否已占用”判断。
49
+ * 使用场景:index.ts 组装后注入 QQ / 微信申请短码的路由,两边共用同一个判断(DRY)。
50
+ */
51
+ export declare function codeTakenBy(providers: () => readonly BindProvider[]): (code: string) => boolean;
52
+ /**
53
+ * 统一确认:找到持有该短码的提供方并确认;都没有时返回 invalid_code。
54
+ * 使用场景:routes/bind-confirm.ts。返回值附带 provider,游戏侧据此决定提示文案和是否解除访客限制。
55
+ */
56
+ export declare function confirmWithProviders(providers: readonly BindProvider[], input: BindConfirmInput): (BindConfirmResult & {
57
+ provider?: string;
58
+ });
59
+ //# sourceMappingURL=bind-providers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind-providers.d.ts","sourceRoot":"","sources":["../../src/domain/bind-providers.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,2BAA2B;AAC3B,eAAO,MAAM,gBAAgB,QAAgB,CAAC;AAC9C,wCAAwC;AACxC,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B,iBAAiB;AACjB,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,0DAA0D;AAC1D,MAAM,MAAM,iBAAiB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAC9C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhE;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,qCAAqC;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACtC,cAAc;IACd,OAAO,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;CACrD;AASD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,EAAE,MAAM,SAAgB,GAAG,MAAM,CAMnG;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,SAAS,YAAY,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAE/F;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,SAAS,YAAY,EAAE,EAClC,KAAK,EAAE,gBAAgB,GACtB,CAAC,iBAAiB,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAM7C"}
@@ -0,0 +1,33 @@
1
+ import { randomInt } from "node:crypto";
2
+ const BIND_CODE_TTL_MS = 5 * 60 * 1e3;
3
+ const BIND_CODE_LEN = 6;
4
+ function randomCode(length) {
5
+ let s = "";
6
+ for (let i = 0; i < length; i++) s += String(randomInt(0, 10));
7
+ return s;
8
+ }
9
+ function generateBindCode(isTaken, length = BIND_CODE_LEN) {
10
+ for (let attempt = 0; attempt < 8; attempt++) {
11
+ const candidate = randomCode(length);
12
+ if (!isTaken(candidate)) return candidate;
13
+ }
14
+ return "";
15
+ }
16
+ function codeTakenBy(providers) {
17
+ return (code) => providers().some((provider) => provider.hasPendingCode(code));
18
+ }
19
+ function confirmWithProviders(providers, input) {
20
+ for (const provider of providers) {
21
+ if (!provider.hasPendingCode(input.code)) continue;
22
+ return { ...provider.confirm(input), provider: provider.id };
23
+ }
24
+ return { ok: false, status: 400, error: "invalid_code" };
25
+ }
26
+ export {
27
+ BIND_CODE_LEN,
28
+ BIND_CODE_TTL_MS,
29
+ codeTakenBy,
30
+ confirmWithProviders,
31
+ generateBindCode
32
+ };
33
+ //# sourceMappingURL=bind-providers.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/domain/bind-providers.ts"],
4
+ "sourcesContent": ["/**\n * domain/bind-providers.ts \u2014 \u5916\u90E8\u8D26\u53F7 \u2194 \u6E38\u620F\u8D26\u53F7\u7ED1\u5B9A\u7684\u63D0\u4F9B\u65B9\u62BD\u8C61\u4E0E\u77ED\u7801\u751F\u6210\n *\n * \u4F7F\u7528\u573A\u666F\uFF1AQQ \u4E0E\u5FAE\u4FE1\u4E24\u79CD\u7ED1\u5B9A\u5171\u7528\u6E38\u620F\u5185\u540C\u4E00\u4E2A `/c:bind` \u6D41\u7A0B\u3002\n * - routes/bind-confirm.ts \u6309\u6CE8\u518C\u987A\u5E8F\u8BE2\u95EE\u6BCF\u4E2A\u63D0\u4F9B\u65B9\u201C\u8FD9\u4E2A\u77ED\u7801\u662F\u4E0D\u662F\u4F60\u7684\u201D\uFF0C\u4EA4\u7ED9\u5BF9\u5E94\u63D0\u4F9B\u65B9\u786E\u8BA4\uFF08OCP\uFF1A\u65B0\u589E\u6E20\u9053\u53EA\u9700\u6CE8\u518C\u65B0\u63D0\u4F9B\u65B9\uFF09\uFF1B\n * - \u5404\u6E20\u9053\u7533\u8BF7\u77ED\u7801\u65F6\u7528 generateBindCode \u751F\u6210\uFF0C\u5728\u6240\u6709\u63D0\u4F9B\u65B9\u7684\u5F85\u786E\u8BA4\u8868\u91CC\u90FD\u4E0D\u91CD\u590D\uFF0C\u4FDD\u8BC1\u7EDF\u4E00\u786E\u8BA4\u5165\u53E3\u4E0D\u4F1A\u8BA4\u9519\u6E20\u9053\u3002\n */\n\nimport { randomInt } from \"node:crypto\";\n\n/** \u7ED1\u5B9A\u77ED\u7801\u6709\u6548\u671F\uFF08\u4E0E QQ \u539F\u6709\u6D41\u7A0B\u4E00\u81F4\uFF09 */\nexport const BIND_CODE_TTL_MS = 5 * 60 * 1000;\n/** \u7ED1\u5B9A\u77ED\u7801\u4F4D\u6570\uFF08\u4E0E QQ \u539F\u6709\u6D41\u7A0B\u4E00\u81F4\uFF1B\u6E38\u620F\u4FA7\u63A5\u53D7 4\u20138 \u4F4D\u6570\u5B57\uFF09 */\nexport const BIND_CODE_LEN = 6;\n\n/** \u6E38\u620F\u4FA7\u63D0\u4EA4\u7684\u786E\u8BA4\u8BF7\u6C42 */\nexport interface BindConfirmInput {\n code: string;\n xuid: string;\n name: string;\n now: number;\n}\n\n/** \u786E\u8BA4\u7ED3\u679C\uFF1A\u6210\u529F\u5E26\u7ED1\u5B9A\u8BB0\u5F55\uFF1B\u5931\u8D25\u5E26 HTTP \u72B6\u6001\u4E0E\u9519\u8BEF\u7801\uFF08\u9519\u8BEF\u7801\u4E0E QQ \u539F\u63A5\u53E3\u4E00\u81F4\uFF0C\u6E38\u620F\u4FA7\u636E\u6B64\u63D0\u793A\uFF09 */\nexport type BindConfirmResult =\n | { ok: true; binding: Record<string, unknown> }\n | { ok: false; status: number; error: string; hint?: string };\n\n/**\n * \u7ED1\u5B9A\u63D0\u4F9B\u65B9\u5951\u7EA6\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1AQQ\uFF08domain/qq-bind.ts\uFF09\u4E0E\u5FAE\u4FE1\uFF08domain/wechat-bind.ts\uFF09\u5404\u5B9E\u73B0\u4E00\u4EFD\uFF0C\u7531 index.ts \u7EC4\u88C5\u6210\u5217\u8868\u6CE8\u5165\u7EDF\u4E00\u786E\u8BA4\u8DEF\u7531\uFF08LSP\uFF1A\u786E\u8BA4\u8BED\u4E49\u4E00\u81F4\uFF09\u3002\n */\nexport interface BindProvider {\n /** \u6E20\u9053\u6807\u8BC6\uFF0C\u8FD4\u56DE\u7ED9\u6E38\u620F\u4FA7\u7528\u4E8E\u63D0\u793A\u6587\u6848\uFF08qq / wechat\uFF09 */\n readonly id: string;\n /** \u5F85\u786E\u8BA4\u8868\u91CC\u662F\u5426\u5B58\u5728\u8BE5\u77ED\u7801\uFF08\u4E0D\u5224\u65AD\u8FC7\u671F\uFF0C\u8FC7\u671F\u4EA4\u7ED9 confirm \u8FD4\u56DE code_expired\uFF09 */\n hasPendingCode(code: string): boolean;\n /** \u7528\u77ED\u7801\u5B8C\u6210\u7ED1\u5B9A */\n confirm(input: BindConfirmInput): BindConfirmResult;\n}\n\n/** \u751F\u6210\u4E00\u4E2A\u7EAF\u6570\u5B57\u77ED\u7801 */\nfunction randomCode(length: number): string {\n let s = \"\";\n for (let i = 0; i < length; i++) s += String(randomInt(0, 10));\n return s;\n}\n\n/**\n * \u751F\u6210\u5728\u6240\u6709\u63D0\u4F9B\u65B9\u4E2D\u90FD\u672A\u88AB\u5360\u7528\u7684\u77ED\u7801\uFF1B\u591A\u6B21\u78B0\u649E\u540E\u8FD4\u56DE\u7A7A\u4E32\uFF0C\u7531\u8C03\u7528\u65B9\u56DE code_gen_failed\u3002\n *\n * @param isTaken \u5224\u65AD\u77ED\u7801\u662F\u5426\u5DF2\u88AB\u4EFB\u4E00\u63D0\u4F9B\u65B9\u5360\u7528\u3002\n */\nexport function generateBindCode(isTaken: (code: string) => boolean, length = BIND_CODE_LEN): string {\n for (let attempt = 0; attempt < 8; attempt++) {\n const candidate = randomCode(length);\n if (!isTaken(candidate)) return candidate;\n }\n return \"\";\n}\n\n/**\n * \u7531\u63D0\u4F9B\u65B9\u5217\u8868\u6784\u9020\u201C\u77ED\u7801\u662F\u5426\u5DF2\u5360\u7528\u201D\u5224\u65AD\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1Aindex.ts \u7EC4\u88C5\u540E\u6CE8\u5165 QQ / \u5FAE\u4FE1\u7533\u8BF7\u77ED\u7801\u7684\u8DEF\u7531\uFF0C\u4E24\u8FB9\u5171\u7528\u540C\u4E00\u4E2A\u5224\u65AD\uFF08DRY\uFF09\u3002\n */\nexport function codeTakenBy(providers: () => readonly BindProvider[]): (code: string) => boolean {\n return (code) => providers().some((provider) => provider.hasPendingCode(code));\n}\n\n/**\n * \u7EDF\u4E00\u786E\u8BA4\uFF1A\u627E\u5230\u6301\u6709\u8BE5\u77ED\u7801\u7684\u63D0\u4F9B\u65B9\u5E76\u786E\u8BA4\uFF1B\u90FD\u6CA1\u6709\u65F6\u8FD4\u56DE invalid_code\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1Aroutes/bind-confirm.ts\u3002\u8FD4\u56DE\u503C\u9644\u5E26 provider\uFF0C\u6E38\u620F\u4FA7\u636E\u6B64\u51B3\u5B9A\u63D0\u793A\u6587\u6848\u548C\u662F\u5426\u89E3\u9664\u8BBF\u5BA2\u9650\u5236\u3002\n */\nexport function confirmWithProviders(\n providers: readonly BindProvider[],\n input: BindConfirmInput\n): (BindConfirmResult & { provider?: string }) {\n for (const provider of providers) {\n if (!provider.hasPendingCode(input.code)) continue;\n return { ...provider.confirm(input), provider: provider.id };\n }\n return { ok: false, status: 400, error: \"invalid_code\" };\n}\n"],
5
+ "mappings": "AAQA,SAAS,iBAAiB;AAGnB,MAAM,mBAAmB,IAAI,KAAK;AAElC,MAAM,gBAAgB;AA6B7B,SAAS,WAAW,QAAwB;AAC1C,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,QAAQ,IAAK,MAAK,OAAO,UAAU,GAAG,EAAE,CAAC;AAC7D,SAAO;AACT;AAOO,SAAS,iBAAiB,SAAoC,SAAS,eAAuB;AACnG,WAAS,UAAU,GAAG,UAAU,GAAG,WAAW;AAC5C,UAAM,YAAY,WAAW,MAAM;AACnC,QAAI,CAAC,QAAQ,SAAS,EAAG,QAAO;AAAA,EAClC;AACA,SAAO;AACT;AAMO,SAAS,YAAY,WAAqE;AAC/F,SAAO,CAAC,SAAS,UAAU,EAAE,KAAK,CAAC,aAAa,SAAS,eAAe,IAAI,CAAC;AAC/E;AAMO,SAAS,qBACd,WACA,OAC6C;AAC7C,aAAW,YAAY,WAAW;AAChC,QAAI,CAAC,SAAS,eAAe,MAAM,IAAI,EAAG;AAC1C,WAAO,EAAE,GAAG,SAAS,QAAQ,KAAK,GAAG,UAAU,SAAS,GAAG;AAAA,EAC7D;AACA,SAAO,EAAE,IAAI,OAAO,QAAQ,KAAK,OAAO,eAAe;AACzD;",
6
+ "names": []
7
+ }
@@ -27,6 +27,11 @@ export interface ResourceSample {
27
27
  hostUsedMb: number | null;
28
28
  dbMb: number | null;
29
29
  bdsMb: number | null;
30
+ /**
31
+ * 其余已登记进程的内存(MB)。
32
+ * 使用场景:守护进程、QQ 桥、隧道、LLBot、附属进程。旧采样没有该字段;键缺省表示当时没采到。
33
+ */
34
+ partsMb?: Record<string, number>;
30
35
  }
31
36
  export declare class ResourceHistory {
32
37
  private query;
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/domain/metrics.ts"],"names":[],"mappings":"AAGA,OAAO,EAAe,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAE7D,oDAAoD;AACpD,eAAO,MAAM,iBAAiB,OAAQ,CAAC;AACvC,kBAAkB;AAClB,eAAO,MAAM,kBAAkB,OAAQ,CAAC;AACxC,6BAA6B;AAC7B,eAAO,MAAM,qBAAqB,UAAY,CAAC;AAC/C,2CAA2C;AAC3C,eAAO,MAAM,oBAAoB,QAA6B,CAAC;AAO/D;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAIvD;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAKD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAoC7F;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAID,qBAAa,eAAe;IAGd,OAAO,CAAC,KAAK;IAFzB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,SAAS,CAAK;IACtB,YAAoB,KAAK,EAAE,OAAO,EAEjC;IACD,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAgBnC;IACD,IAAI,CAAC,GAAG,SAAa,EAAE,MAAM,SAAwB,GAAG,cAAc,EAAE,CAEvE;CACF;AAED,qBAAa,cAAc;IAGb,OAAO,CAAC,KAAK;IAFzB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,SAAS,CAAK;IACtB,YAAoB,KAAK,EAAE,OAAO,EAEjC;IACD,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAYlC;IACD,OAAO,CAAC,KAAK;IAMb,IAAI,CAAC,GAAG,SAAa,EAAE,MAAM,SAAwB,GAAG,aAAa,EAAE,CAEtE;CACF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,aAAa,EAAE,CAAC;IAAC,eAAe,EAAE,cAAc,EAAE,CAAA;CAAE,GAAG,IAAI,CAiB1J"}
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/domain/metrics.ts"],"names":[],"mappings":"AAGA,OAAO,EAAe,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAE7D,oDAAoD;AACpD,eAAO,MAAM,iBAAiB,OAAQ,CAAC;AACvC,kBAAkB;AAClB,eAAO,MAAM,kBAAkB,OAAQ,CAAC;AACxC,6BAA6B;AAC7B,eAAO,MAAM,qBAAqB,UAAY,CAAC;AAC/C,2CAA2C;AAC3C,eAAO,MAAM,oBAAoB,QAA6B,CAAC;AAO/D;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAIvD;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAKD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAoC7F;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAsBD,qBAAa,eAAe;IAGd,OAAO,CAAC,KAAK;IAFzB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,SAAS,CAAK;IACtB,YAAoB,KAAK,EAAE,OAAO,EAEjC;IACD,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAyBnC;IACD,IAAI,CAAC,GAAG,SAAa,EAAE,MAAM,SAAwB,GAAG,cAAc,EAAE,CAEvE;CACF;AAED,qBAAa,cAAc;IAGb,OAAO,CAAC,KAAK;IAFzB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,SAAS,CAAK;IACtB,YAAoB,KAAK,EAAE,OAAO,EAEjC;IACD,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAYlC;IACD,OAAO,CAAC,KAAK;IAMb,IAAI,CAAC,GAAG,SAAa,EAAE,MAAM,SAAwB,GAAG,aAAa,EAAE,CAEtE;CACF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,aAAa,EAAE,CAAC;IAAC,eAAe,EAAE,cAAc,EAAE,CAAA;CAAE,GAAG,IAAI,CAiB1J"}
@@ -34,6 +34,17 @@ function parseMetrics(data, now) {
34
34
  };
35
35
  }
36
36
  const memoryMb = (value) => value === null || typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 8388608;
37
+ const partKey = (key) => /^[a-z0-9][a-z0-9_:-]{0,80}$/i.test(key);
38
+ const memoryNumber = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 8388608;
39
+ function sanitizeParts(value) {
40
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
41
+ const parts = {};
42
+ for (const [key, item] of Object.entries(value)) {
43
+ if (!partKey(key) || !memoryNumber(item)) continue;
44
+ parts[key] = item;
45
+ }
46
+ return Object.keys(parts).length ? parts : void 0;
47
+ }
37
48
  class ResourceHistory {
38
49
  constructor(query) {
39
50
  this.query = query;
@@ -44,8 +55,16 @@ class ResourceHistory {
44
55
  lastPrune = 0;
45
56
  append(sample) {
46
57
  if (sample.recordedAt - this.lastWrite < RESOURCE_SAMPLE_MS) return;
47
- if (sample.hostUsedMb === null && sample.dbMb === null && sample.bdsMb === null) return;
58
+ const partsMb = sanitizeParts(sample.partsMb);
59
+ if (sample.hostUsedMb === null && sample.dbMb === null && sample.bdsMb === null && !partsMb) return;
48
60
  if (!memoryMb(sample.hostUsedMb) || !memoryMb(sample.dbMb) || !memoryMb(sample.bdsMb)) return;
61
+ sample = {
62
+ recordedAt: sample.recordedAt,
63
+ hostUsedMb: sample.hostUsedMb,
64
+ dbMb: sample.dbMb,
65
+ bdsMb: sample.bdsMb,
66
+ ...partsMb ? { partsMb } : {}
67
+ };
49
68
  this.query("INSERT OR REPLACE INTO sfmc_resource_metrics (recorded_at, sample) VALUES (?, ?)", [
50
69
  sample.recordedAt,
51
70
  JSON.stringify(sample)
@@ -149,7 +168,14 @@ function parseResourceRows(rows) {
149
168
  try {
150
169
  const sample = JSON.parse(row.sample);
151
170
  if (!memoryMb(sample.hostUsedMb) || !memoryMb(sample.dbMb) || !memoryMb(sample.bdsMb)) return [];
152
- return [sample];
171
+ const partsMb = sanitizeParts(sample.partsMb);
172
+ return [{
173
+ recordedAt: sample.recordedAt,
174
+ hostUsedMb: sample.hostUsedMb,
175
+ dbMb: sample.dbMb,
176
+ bdsMb: sample.bdsMb,
177
+ ...partsMb ? { partsMb } : {}
178
+ }];
153
179
  } catch {
154
180
  return [];
155
181
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/domain/metrics.ts"],
4
- "sourcesContent": ["/** \u8FD0\u884C\u6307\u6807\u7684\u53D7\u63A7\u6821\u9A8C\u4E0E\u6709\u754C\u5386\u53F2\uFF1B\u53EA\u8BB0\u5F55\u5E73\u53F0\u4E0A\u62A5\uFF0C\u4E0D\u63A5\u53D7\u4EFB\u610F\u8868\u540D\u6216 SQL\u3002 */\nimport { existsSync } from \"node:fs\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport { createQuery, type QueryFn } from \"../lib/sqlite.js\";\n\n/** \u8FD0\u884C\u6307\u6807\u5199\u5165\u53BB\u91CD\u95F4\u9694\u3002\u8BFB\u5386\u53F2\u65F6\u4E00\u5C0F\u65F6\u5185\u7684\u70B9\u6570\u6309\u8FD9\u4E2A\u95F4\u9694\u5C01\u9876\uFF0C\u907F\u514D\u518D\u88AB\u56FA\u5B9A 720 \u6761\u622A\u65AD\u3002 */\nexport const RUNTIME_SAMPLE_MS = 3_000;\n/** \u8D44\u6E90\u6307\u6807\u5199\u5165\u53BB\u91CD\u95F4\u9694\u3002 */\nexport const RESOURCE_SAMPLE_MS = 8_000;\n/** \u4E0D\u62BD\u7A00\u7684\u6700\u957F\u7A97\u53E3\uFF0C\u4E0E\u539F\u5148\u300C\u6700\u8FD1\u4E00\u5C0F\u65F6\u300D\u4E00\u81F4\u3002 */\nexport const METRICS_RAW_WINDOW_MS = 3_600_000;\n/** \u843D\u5E93\u4FDD\u7559\u65F6\u957F\uFF0C\u4E0E prune \u5220\u9664\u6761\u4EF6\u4E00\u81F4\u3002\u8BF7\u6C42\u66F4\u957F\u7684\u8303\u56F4\u4F1A\u88AB\u622A\u5230\u8FD9\u91CC\u3002 */\nexport const METRICS_RETENTION_MS = 72 * METRICS_RAW_WINDOW_MS;\n\nconst METRIC_TABLES = {\n runtime: \"sfmc_runtime_metrics\",\n resource: \"sfmc_resource_metrics\",\n} as const;\n\n/**\n * \u628A\u8C03\u7528\u65B9\u7ED9\u51FA\u7684\u7A97\u53E3\u6536\u6210\u843D\u5E93\u80FD\u8986\u76D6\u7684\u8303\u56F4\u3002\n * \u7F3A\u7701\u4ECD\u662F\u4E00\u5C0F\u65F6\uFF0C\u65E7\u5BA2\u6237\u7AEF\u4E0D\u4F20 spanMs \u65F6\u884C\u4E3A\u4E0D\u53D8\u3002\n */\nexport function clampMetricsSpan(value: unknown): number {\n if (value === undefined) return METRICS_RAW_WINDOW_MS;\n if (typeof value !== \"number\" || !Number.isSafeInteger(value) || value <= 0) throw new Error(\"\u975E\u6CD5\u65F6\u95F4\u8303\u56F4\");\n return Math.min(value, METRICS_RETENTION_MS);\n}\n\nexport interface MetricsSample {\n recordedAt: number;\n bootId: string;\n tps: number | null;\n onlineCount: number;\n entities: Record<string, number | null>;\n entitiesUpdatedAt: number | null;\n chunkEstimate: number | null;\n}\nconst DIMENSIONS = [\"minecraft:overworld\", \"minecraft:nether\", \"minecraft:the_end\"];\nconst count = (value: unknown) =>\n value === null || (typeof value === \"number\" && Number.isSafeInteger(value) && value >= 0 && value <= 100_000_000);\n\nexport function parseMetrics(data: Record<string, unknown>, now: number): MetricsSample | null {\n if (!Array.isArray(data.players) || data.players.length > 1000) return null;\n if (\n typeof data.bootId !== \"string\" ||\n !/^[a-zA-Z0-9-]{1,96}$/.test(data.bootId) ||\n !(\n data.tps === null ||\n (typeof data.tps === \"number\" && Number.isFinite(data.tps) && data.tps >= 0 && data.tps <= 20)\n ) ||\n !count(data.chunkEstimate) ||\n typeof data.onlineCount !== \"number\" ||\n data.onlineCount !== (data.players as unknown[]).length ||\n !data.entities ||\n typeof data.entities !== \"object\" ||\n Array.isArray(data.entities) ||\n !(\n data.entitiesUpdatedAt === null ||\n (typeof data.entitiesUpdatedAt === \"number\" &&\n Number.isSafeInteger(data.entitiesUpdatedAt) &&\n data.entitiesUpdatedAt > 0 &&\n data.entitiesUpdatedAt <= now + 2000)\n )\n )\n return null;\n const raw = data.entities as Record<string, unknown>;\n if (Object.keys(raw).some((key) => !DIMENSIONS.includes(key)) || DIMENSIONS.some((key) => !count(raw[key])))\n return null;\n return {\n recordedAt: now,\n bootId: data.bootId,\n tps: data.tps as number | null,\n onlineCount: data.onlineCount,\n entities: { ...raw } as Record<string, number | null>,\n entitiesUpdatedAt: data.entitiesUpdatedAt as number | null,\n chunkEstimate: data.chunkEstimate as number | null,\n };\n}\n\nexport interface ResourceSample {\n recordedAt: number;\n hostUsedMb: number | null;\n dbMb: number | null;\n bdsMb: number | null;\n}\nconst memoryMb = (value: unknown) =>\n value === null || (typeof value === \"number\" && Number.isFinite(value) && value >= 0 && value <= 8_388_608);\n\nexport class ResourceHistory {\n private lastWrite = 0;\n private lastPrune = 0;\n constructor(private query: QueryFn) {\n query(\"CREATE TABLE IF NOT EXISTS sfmc_resource_metrics (recorded_at INTEGER PRIMARY KEY, sample TEXT NOT NULL)\");\n }\n append(sample: ResourceSample): void {\n if (sample.recordedAt - this.lastWrite < RESOURCE_SAMPLE_MS) return;\n if (sample.hostUsedMb === null && sample.dbMb === null && sample.bdsMb === null) return;\n if (!memoryMb(sample.hostUsedMb) || !memoryMb(sample.dbMb) || !memoryMb(sample.bdsMb)) return;\n this.query(\"INSERT OR REPLACE INTO sfmc_resource_metrics (recorded_at, sample) VALUES (?, ?)\", [\n sample.recordedAt,\n JSON.stringify(sample),\n ]);\n if (sample.recordedAt - this.lastPrune > 60_000) {\n this.query(\"DELETE FROM sfmc_resource_metrics WHERE recorded_at < ?\", [sample.recordedAt - METRICS_RETENTION_MS]);\n this.query(\n \"DELETE FROM sfmc_resource_metrics WHERE recorded_at NOT IN (SELECT recorded_at FROM sfmc_resource_metrics ORDER BY recorded_at DESC LIMIT 60000)\"\n );\n this.lastPrune = sample.recordedAt;\n }\n this.lastWrite = sample.recordedAt;\n }\n read(now = Date.now(), spanMs = METRICS_RAW_WINDOW_MS): ResourceSample[] {\n return parseResourceRows(readMetricRows(this.query, \"resource\", now, spanMs));\n }\n}\n\nexport class MetricsHistory {\n private lastWrite = 0;\n private lastPrune = 0;\n constructor(private query: QueryFn) {\n query(\"CREATE TABLE IF NOT EXISTS sfmc_runtime_metrics (recorded_at INTEGER PRIMARY KEY, sample TEXT NOT NULL)\");\n }\n append(sample: MetricsSample): void {\n // \u540C\u4E00\u91C7\u6837\u7A97\u53E3\u53BB\u91CD\uFF0C\u9632\u6B62\u5F02\u5E38\u91CD\u8BD5\u9020\u6210\u65E0\u754C\u843D\u5E93\u3002\n if (sample.recordedAt - this.lastWrite < RUNTIME_SAMPLE_MS) return;\n this.query(\"INSERT OR REPLACE INTO sfmc_runtime_metrics (recorded_at, sample) VALUES (?, ?)\", [\n sample.recordedAt,\n JSON.stringify(sample),\n ]);\n if (sample.recordedAt - this.lastPrune > 60_000) {\n this.prune(sample.recordedAt);\n this.lastPrune = sample.recordedAt;\n }\n this.lastWrite = sample.recordedAt;\n }\n private prune(now: number): void {\n this.query(\"DELETE FROM sfmc_runtime_metrics WHERE recorded_at < ?\", [now - METRICS_RETENTION_MS]);\n this.query(\n \"DELETE FROM sfmc_runtime_metrics WHERE recorded_at NOT IN (SELECT recorded_at FROM sfmc_runtime_metrics ORDER BY recorded_at DESC LIMIT 60000)\"\n );\n }\n read(now = Date.now(), spanMs = METRICS_RAW_WINDOW_MS): MetricsSample[] {\n return parseRuntimeRows(readMetricRows(this.query, \"runtime\", now, spanMs));\n }\n}\n\n/**\n * \u53EA\u8BFB\u6253\u5F00\u5DF2\u843D\u76D8\u7684\u6307\u6807\u5E93\u3002\u6587\u4EF6\u4E0D\u5B58\u5728\u6216\u4ECD\u88AB\u5360\u7528\u65F6\u8FD4\u56DE null\uFF0C\u4E0D\u521B\u5EFA\u6587\u4EF6\u3001\u4E0D\u5EFA\u8868\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1A\u6570\u636E\u670D\u52A1\u8FDB\u7A0B\u6CA1\u5728\u8DD1\u65F6\uFF0C\u7BA1\u7406\u901A\u9053\u4ECD\u8981\u753B\u51FA\u505C\u670D\u524D\u7684\u66F2\u7EBF\u3002\n */\nexport function readPersistedMetrics(filePath: string, now: number, spanMs: number): { history: MetricsSample[]; resourceHistory: ResourceSample[] } | null {\n if (!existsSync(filePath)) return null;\n let db: DatabaseSync | undefined;\n try {\n db = new DatabaseSync(filePath, { readOnly: true });\n db.exec(\"PRAGMA query_only = ON\");\n db.exec(\"PRAGMA busy_timeout = 5000\");\n const query = createQuery(db);\n return {\n history: parseRuntimeRows(readMetricRows(query, \"runtime\", now, spanMs)),\n resourceHistory: parseResourceRows(readMetricRows(query, \"resource\", now, spanMs)),\n };\n } catch {\n return null;\n } finally {\n try { db?.close(); } catch { /* \u53EA\u8BFB\u8FDE\u63A5\u5173\u95ED\u5931\u8D25\u65F6\u8C03\u7528\u65B9\u4ECD\u6309\u6CA1\u6709\u843D\u76D8\u5904\u7406\u3002 */ }\n }\n}\n\n/**\n * \u7A97\u53E3\u53F3\u7AEF\u5BF9\u9F50\u6700\u540E\u4E00\u6761\u771F\u5B9E\u91C7\u6837\uFF0C\u6240\u4EE5\u505C\u670D\u540E\u8BF7\u6C42\u300C\u6700\u8FD1 15 \u5206\u949F\u300D\u770B\u5230\u7684\u662F\u505C\u670D\u524D\u7684 15 \u5206\u949F\u3002\n * \u4E0D\u8D85\u8FC7\u4E00\u5C0F\u65F6\u65F6\u8FD4\u56DE\u539F\u59CB\u70B9\uFF1B\u66F4\u957F\u5219\u6BCF\u4E2A\u65F6\u95F4\u6876\u4FDD\u7559\u4E00\u6761\u5DF2\u843D\u76D8\u7684\u70B9\uFF0C\u4E0D\u8865\u9020\u91C7\u6837\u3002\n */\nfunction readMetricRows(query: QueryFn, kind: keyof typeof METRIC_TABLES, now: number, spanMs: number): { sample: string }[] {\n const table = METRIC_TABLES[kind];\n const span = clampMetricsSpan(spanMs);\n const intervalMs = kind === \"runtime\" ? RUNTIME_SAMPLE_MS : RESOURCE_SAMPLE_MS;\n const latestRows = query(`SELECT MAX(recorded_at) AS latest FROM ${table}`);\n if (!Array.isArray(latestRows)) return [];\n const latest = (latestRows[0] as { latest: number | null } | undefined)?.latest;\n if (typeof latest !== \"number\") return [];\n const end = Math.min(latest, now);\n const since = end - span;\n const hourPoints = Math.ceil(METRICS_RAW_WINDOW_MS / intervalMs);\n const rows = span <= METRICS_RAW_WINDOW_MS\n ? query(\n `SELECT sample FROM (SELECT sample, recorded_at FROM ${table} WHERE recorded_at >= ? AND recorded_at <= ? ORDER BY recorded_at DESC LIMIT ?) ORDER BY recorded_at ASC`,\n [since, end, Math.ceil(span / intervalMs)]\n )\n : query(\n // \u7ED1\u5B9A\u53C2\u6570\u5728 SQLite \u91CC\u662F\u6D6E\u70B9\uFF0C\u76F4\u63A5\u76F8\u9664\u4E0D\u4F1A\u6309\u6574\u6570\u5206\u6876\uFF0C\u6240\u4EE5\u5148 CAST \u6210\u6574\u6570\u3002\n `SELECT sample FROM (SELECT sample, recorded_at FROM (SELECT sample, recorded_at, ROW_NUMBER() OVER (PARTITION BY recorded_at / CAST(? AS INTEGER) ORDER BY recorded_at DESC) AS rn FROM ${table} WHERE recorded_at >= ? AND recorded_at <= ?) WHERE rn = 1 ORDER BY recorded_at DESC LIMIT ?) ORDER BY recorded_at ASC`,\n [Math.ceil(span / hourPoints), since, end, hourPoints]\n );\n return Array.isArray(rows) ? rows as { sample: string }[] : [];\n}\n\nfunction parseRuntimeRows(rows: { sample: string }[]): MetricsSample[] {\n return rows.flatMap((row) => {\n try {\n return [JSON.parse(row.sample) as MetricsSample];\n } catch {\n return [];\n }\n });\n}\n\nfunction parseResourceRows(rows: { sample: string }[]): ResourceSample[] {\n return rows.flatMap((row) => {\n try {\n const sample = JSON.parse(row.sample) as ResourceSample;\n if (!memoryMb(sample.hostUsedMb) || !memoryMb(sample.dbMb) || !memoryMb(sample.bdsMb)) return [];\n return [sample];\n } catch {\n return [];\n }\n });\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/** \u8FD0\u884C\u6307\u6807\u7684\u53D7\u63A7\u6821\u9A8C\u4E0E\u6709\u754C\u5386\u53F2\uFF1B\u53EA\u8BB0\u5F55\u5E73\u53F0\u4E0A\u62A5\uFF0C\u4E0D\u63A5\u53D7\u4EFB\u610F\u8868\u540D\u6216 SQL\u3002 */\nimport { existsSync } from \"node:fs\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport { createQuery, type QueryFn } from \"../lib/sqlite.js\";\n\n/** \u8FD0\u884C\u6307\u6807\u5199\u5165\u53BB\u91CD\u95F4\u9694\u3002\u8BFB\u5386\u53F2\u65F6\u4E00\u5C0F\u65F6\u5185\u7684\u70B9\u6570\u6309\u8FD9\u4E2A\u95F4\u9694\u5C01\u9876\uFF0C\u907F\u514D\u518D\u88AB\u56FA\u5B9A 720 \u6761\u622A\u65AD\u3002 */\nexport const RUNTIME_SAMPLE_MS = 3_000;\n/** \u8D44\u6E90\u6307\u6807\u5199\u5165\u53BB\u91CD\u95F4\u9694\u3002 */\nexport const RESOURCE_SAMPLE_MS = 8_000;\n/** \u4E0D\u62BD\u7A00\u7684\u6700\u957F\u7A97\u53E3\uFF0C\u4E0E\u539F\u5148\u300C\u6700\u8FD1\u4E00\u5C0F\u65F6\u300D\u4E00\u81F4\u3002 */\nexport const METRICS_RAW_WINDOW_MS = 3_600_000;\n/** \u843D\u5E93\u4FDD\u7559\u65F6\u957F\uFF0C\u4E0E prune \u5220\u9664\u6761\u4EF6\u4E00\u81F4\u3002\u8BF7\u6C42\u66F4\u957F\u7684\u8303\u56F4\u4F1A\u88AB\u622A\u5230\u8FD9\u91CC\u3002 */\nexport const METRICS_RETENTION_MS = 72 * METRICS_RAW_WINDOW_MS;\n\nconst METRIC_TABLES = {\n runtime: \"sfmc_runtime_metrics\",\n resource: \"sfmc_resource_metrics\",\n} as const;\n\n/**\n * \u628A\u8C03\u7528\u65B9\u7ED9\u51FA\u7684\u7A97\u53E3\u6536\u6210\u843D\u5E93\u80FD\u8986\u76D6\u7684\u8303\u56F4\u3002\n * \u7F3A\u7701\u4ECD\u662F\u4E00\u5C0F\u65F6\uFF0C\u65E7\u5BA2\u6237\u7AEF\u4E0D\u4F20 spanMs \u65F6\u884C\u4E3A\u4E0D\u53D8\u3002\n */\nexport function clampMetricsSpan(value: unknown): number {\n if (value === undefined) return METRICS_RAW_WINDOW_MS;\n if (typeof value !== \"number\" || !Number.isSafeInteger(value) || value <= 0) throw new Error(\"\u975E\u6CD5\u65F6\u95F4\u8303\u56F4\");\n return Math.min(value, METRICS_RETENTION_MS);\n}\n\nexport interface MetricsSample {\n recordedAt: number;\n bootId: string;\n tps: number | null;\n onlineCount: number;\n entities: Record<string, number | null>;\n entitiesUpdatedAt: number | null;\n chunkEstimate: number | null;\n}\nconst DIMENSIONS = [\"minecraft:overworld\", \"minecraft:nether\", \"minecraft:the_end\"];\nconst count = (value: unknown) =>\n value === null || (typeof value === \"number\" && Number.isSafeInteger(value) && value >= 0 && value <= 100_000_000);\n\nexport function parseMetrics(data: Record<string, unknown>, now: number): MetricsSample | null {\n if (!Array.isArray(data.players) || data.players.length > 1000) return null;\n if (\n typeof data.bootId !== \"string\" ||\n !/^[a-zA-Z0-9-]{1,96}$/.test(data.bootId) ||\n !(\n data.tps === null ||\n (typeof data.tps === \"number\" && Number.isFinite(data.tps) && data.tps >= 0 && data.tps <= 20)\n ) ||\n !count(data.chunkEstimate) ||\n typeof data.onlineCount !== \"number\" ||\n data.onlineCount !== (data.players as unknown[]).length ||\n !data.entities ||\n typeof data.entities !== \"object\" ||\n Array.isArray(data.entities) ||\n !(\n data.entitiesUpdatedAt === null ||\n (typeof data.entitiesUpdatedAt === \"number\" &&\n Number.isSafeInteger(data.entitiesUpdatedAt) &&\n data.entitiesUpdatedAt > 0 &&\n data.entitiesUpdatedAt <= now + 2000)\n )\n )\n return null;\n const raw = data.entities as Record<string, unknown>;\n if (Object.keys(raw).some((key) => !DIMENSIONS.includes(key)) || DIMENSIONS.some((key) => !count(raw[key])))\n return null;\n return {\n recordedAt: now,\n bootId: data.bootId,\n tps: data.tps as number | null,\n onlineCount: data.onlineCount,\n entities: { ...raw } as Record<string, number | null>,\n entitiesUpdatedAt: data.entitiesUpdatedAt as number | null,\n chunkEstimate: data.chunkEstimate as number | null,\n };\n}\n\nexport interface ResourceSample {\n recordedAt: number;\n hostUsedMb: number | null;\n dbMb: number | null;\n bdsMb: number | null;\n /**\n * \u5176\u4F59\u5DF2\u767B\u8BB0\u8FDB\u7A0B\u7684\u5185\u5B58\uFF08MB\uFF09\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1A\u5B88\u62A4\u8FDB\u7A0B\u3001QQ \u6865\u3001\u96A7\u9053\u3001LLBot\u3001\u9644\u5C5E\u8FDB\u7A0B\u3002\u65E7\u91C7\u6837\u6CA1\u6709\u8BE5\u5B57\u6BB5\uFF1B\u952E\u7F3A\u7701\u8868\u793A\u5F53\u65F6\u6CA1\u91C7\u5230\u3002\n */\n partsMb?: Record<string, number>;\n}\nconst memoryMb = (value: unknown) =>\n value === null || (typeof value === \"number\" && Number.isFinite(value) && value >= 0 && value <= 8_388_608);\n/** \u8FDB\u7A0B\u89D2\u8272\u952E\u3002\u9644\u5C5E\u8FDB\u7A0B\u7528 sidecar:<\u6A21\u5757 id>\u3002 */\nconst partKey = (key: string) => /^[a-z0-9][a-z0-9_:-]{0,80}$/i.test(key);\nconst memoryNumber = (value: unknown) =>\n typeof value === \"number\" && Number.isFinite(value) && value >= 0 && value <= 8_388_608;\n\n/**\n * \u4E22\u6389\u4E0D\u5408\u6CD5\u7684\u5206\u9879\uFF0C\u4FDD\u7559\u80FD\u7528\u7684\u6570\u5B57\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1A\u8BFB\u5386\u53F2\u65F6\u65E7\u6570\u636E\u6216\u635F\u574F\u5B57\u6BB5\u4E0D\u80FD\u628A\u6574\u6761\u4E3B\u673A/BDS \u91C7\u6837\u4E22\u6389\u3002\n */\nfunction sanitizeParts(value: unknown): Record<string, number> | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const parts: Record<string, number> = {};\n for (const [key, item] of Object.entries(value)) {\n if (!partKey(key) || !memoryNumber(item)) continue;\n parts[key] = item;\n }\n return Object.keys(parts).length ? parts : undefined;\n}\n\nexport class ResourceHistory {\n private lastWrite = 0;\n private lastPrune = 0;\n constructor(private query: QueryFn) {\n query(\"CREATE TABLE IF NOT EXISTS sfmc_resource_metrics (recorded_at INTEGER PRIMARY KEY, sample TEXT NOT NULL)\");\n }\n append(sample: ResourceSample): void {\n if (sample.recordedAt - this.lastWrite < RESOURCE_SAMPLE_MS) return;\n const partsMb = sanitizeParts(sample.partsMb);\n if (sample.hostUsedMb === null && sample.dbMb === null && sample.bdsMb === null && !partsMb) return;\n if (!memoryMb(sample.hostUsedMb) || !memoryMb(sample.dbMb) || !memoryMb(sample.bdsMb)) return;\n // \u5206\u9879\u91CC\u7684\u574F\u952E\u4E22\u6389\uFF0C\u4E3B\u673A\u548C BDS\u3001\u6570\u636E\u670D\u52A1\u4ECD\u6309\u539F\u6765\u7684\u89C4\u5219\u843D\u5E93\u3002\n sample = {\n recordedAt: sample.recordedAt,\n hostUsedMb: sample.hostUsedMb,\n dbMb: sample.dbMb,\n bdsMb: sample.bdsMb,\n ...(partsMb ? { partsMb } : {}),\n };\n this.query(\"INSERT OR REPLACE INTO sfmc_resource_metrics (recorded_at, sample) VALUES (?, ?)\", [\n sample.recordedAt,\n JSON.stringify(sample),\n ]);\n if (sample.recordedAt - this.lastPrune > 60_000) {\n this.query(\"DELETE FROM sfmc_resource_metrics WHERE recorded_at < ?\", [sample.recordedAt - METRICS_RETENTION_MS]);\n this.query(\n \"DELETE FROM sfmc_resource_metrics WHERE recorded_at NOT IN (SELECT recorded_at FROM sfmc_resource_metrics ORDER BY recorded_at DESC LIMIT 60000)\"\n );\n this.lastPrune = sample.recordedAt;\n }\n this.lastWrite = sample.recordedAt;\n }\n read(now = Date.now(), spanMs = METRICS_RAW_WINDOW_MS): ResourceSample[] {\n return parseResourceRows(readMetricRows(this.query, \"resource\", now, spanMs));\n }\n}\n\nexport class MetricsHistory {\n private lastWrite = 0;\n private lastPrune = 0;\n constructor(private query: QueryFn) {\n query(\"CREATE TABLE IF NOT EXISTS sfmc_runtime_metrics (recorded_at INTEGER PRIMARY KEY, sample TEXT NOT NULL)\");\n }\n append(sample: MetricsSample): void {\n // \u540C\u4E00\u91C7\u6837\u7A97\u53E3\u53BB\u91CD\uFF0C\u9632\u6B62\u5F02\u5E38\u91CD\u8BD5\u9020\u6210\u65E0\u754C\u843D\u5E93\u3002\n if (sample.recordedAt - this.lastWrite < RUNTIME_SAMPLE_MS) return;\n this.query(\"INSERT OR REPLACE INTO sfmc_runtime_metrics (recorded_at, sample) VALUES (?, ?)\", [\n sample.recordedAt,\n JSON.stringify(sample),\n ]);\n if (sample.recordedAt - this.lastPrune > 60_000) {\n this.prune(sample.recordedAt);\n this.lastPrune = sample.recordedAt;\n }\n this.lastWrite = sample.recordedAt;\n }\n private prune(now: number): void {\n this.query(\"DELETE FROM sfmc_runtime_metrics WHERE recorded_at < ?\", [now - METRICS_RETENTION_MS]);\n this.query(\n \"DELETE FROM sfmc_runtime_metrics WHERE recorded_at NOT IN (SELECT recorded_at FROM sfmc_runtime_metrics ORDER BY recorded_at DESC LIMIT 60000)\"\n );\n }\n read(now = Date.now(), spanMs = METRICS_RAW_WINDOW_MS): MetricsSample[] {\n return parseRuntimeRows(readMetricRows(this.query, \"runtime\", now, spanMs));\n }\n}\n\n/**\n * \u53EA\u8BFB\u6253\u5F00\u5DF2\u843D\u76D8\u7684\u6307\u6807\u5E93\u3002\u6587\u4EF6\u4E0D\u5B58\u5728\u6216\u4ECD\u88AB\u5360\u7528\u65F6\u8FD4\u56DE null\uFF0C\u4E0D\u521B\u5EFA\u6587\u4EF6\u3001\u4E0D\u5EFA\u8868\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1A\u6570\u636E\u670D\u52A1\u8FDB\u7A0B\u6CA1\u5728\u8DD1\u65F6\uFF0C\u7BA1\u7406\u901A\u9053\u4ECD\u8981\u753B\u51FA\u505C\u670D\u524D\u7684\u66F2\u7EBF\u3002\n */\nexport function readPersistedMetrics(filePath: string, now: number, spanMs: number): { history: MetricsSample[]; resourceHistory: ResourceSample[] } | null {\n if (!existsSync(filePath)) return null;\n let db: DatabaseSync | undefined;\n try {\n db = new DatabaseSync(filePath, { readOnly: true });\n db.exec(\"PRAGMA query_only = ON\");\n db.exec(\"PRAGMA busy_timeout = 5000\");\n const query = createQuery(db);\n return {\n history: parseRuntimeRows(readMetricRows(query, \"runtime\", now, spanMs)),\n resourceHistory: parseResourceRows(readMetricRows(query, \"resource\", now, spanMs)),\n };\n } catch {\n return null;\n } finally {\n try { db?.close(); } catch { /* \u53EA\u8BFB\u8FDE\u63A5\u5173\u95ED\u5931\u8D25\u65F6\u8C03\u7528\u65B9\u4ECD\u6309\u6CA1\u6709\u843D\u76D8\u5904\u7406\u3002 */ }\n }\n}\n\n/**\n * \u7A97\u53E3\u53F3\u7AEF\u5BF9\u9F50\u6700\u540E\u4E00\u6761\u771F\u5B9E\u91C7\u6837\uFF0C\u6240\u4EE5\u505C\u670D\u540E\u8BF7\u6C42\u300C\u6700\u8FD1 15 \u5206\u949F\u300D\u770B\u5230\u7684\u662F\u505C\u670D\u524D\u7684 15 \u5206\u949F\u3002\n * \u4E0D\u8D85\u8FC7\u4E00\u5C0F\u65F6\u65F6\u8FD4\u56DE\u539F\u59CB\u70B9\uFF1B\u66F4\u957F\u5219\u6BCF\u4E2A\u65F6\u95F4\u6876\u4FDD\u7559\u4E00\u6761\u5DF2\u843D\u76D8\u7684\u70B9\uFF0C\u4E0D\u8865\u9020\u91C7\u6837\u3002\n */\nfunction readMetricRows(query: QueryFn, kind: keyof typeof METRIC_TABLES, now: number, spanMs: number): { sample: string }[] {\n const table = METRIC_TABLES[kind];\n const span = clampMetricsSpan(spanMs);\n const intervalMs = kind === \"runtime\" ? RUNTIME_SAMPLE_MS : RESOURCE_SAMPLE_MS;\n const latestRows = query(`SELECT MAX(recorded_at) AS latest FROM ${table}`);\n if (!Array.isArray(latestRows)) return [];\n const latest = (latestRows[0] as { latest: number | null } | undefined)?.latest;\n if (typeof latest !== \"number\") return [];\n const end = Math.min(latest, now);\n const since = end - span;\n const hourPoints = Math.ceil(METRICS_RAW_WINDOW_MS / intervalMs);\n const rows = span <= METRICS_RAW_WINDOW_MS\n ? query(\n `SELECT sample FROM (SELECT sample, recorded_at FROM ${table} WHERE recorded_at >= ? AND recorded_at <= ? ORDER BY recorded_at DESC LIMIT ?) ORDER BY recorded_at ASC`,\n [since, end, Math.ceil(span / intervalMs)]\n )\n : query(\n // \u7ED1\u5B9A\u53C2\u6570\u5728 SQLite \u91CC\u662F\u6D6E\u70B9\uFF0C\u76F4\u63A5\u76F8\u9664\u4E0D\u4F1A\u6309\u6574\u6570\u5206\u6876\uFF0C\u6240\u4EE5\u5148 CAST \u6210\u6574\u6570\u3002\n `SELECT sample FROM (SELECT sample, recorded_at FROM (SELECT sample, recorded_at, ROW_NUMBER() OVER (PARTITION BY recorded_at / CAST(? AS INTEGER) ORDER BY recorded_at DESC) AS rn FROM ${table} WHERE recorded_at >= ? AND recorded_at <= ?) WHERE rn = 1 ORDER BY recorded_at DESC LIMIT ?) ORDER BY recorded_at ASC`,\n [Math.ceil(span / hourPoints), since, end, hourPoints]\n );\n return Array.isArray(rows) ? rows as { sample: string }[] : [];\n}\n\nfunction parseRuntimeRows(rows: { sample: string }[]): MetricsSample[] {\n return rows.flatMap((row) => {\n try {\n return [JSON.parse(row.sample) as MetricsSample];\n } catch {\n return [];\n }\n });\n}\n\nfunction parseResourceRows(rows: { sample: string }[]): ResourceSample[] {\n return rows.flatMap((row) => {\n try {\n const sample = JSON.parse(row.sample) as ResourceSample;\n if (!memoryMb(sample.hostUsedMb) || !memoryMb(sample.dbMb) || !memoryMb(sample.bdsMb)) return [];\n const partsMb = sanitizeParts(sample.partsMb);\n return [{\n recordedAt: sample.recordedAt,\n hostUsedMb: sample.hostUsedMb,\n dbMb: sample.dbMb,\n bdsMb: sample.bdsMb,\n ...(partsMb ? { partsMb } : {}),\n }];\n } catch {\n return [];\n }\n });\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * domain/player-level.ts — 宿主侧计算玩家的 SFMC 有效权限等级
3
+ *
4
+ * 使用场景:GET /api/sfmc/wechat/bind/me 返回绑定玩家的等级,mp-gateway 据此判断微信用户是否为管理员(等级 3)。
5
+ * 游戏内由 Permission.getPermission 计算,宿主侧没有 Player 对象,这里改从两份文件取输入,
6
+ * 合成规则共用 @sfmc-bds/sdk/contracts 的 effectivePermissionLevel(DRY)。
7
+ * 每次调用都现读文件,管理员在桌面端或游戏里调整权限后立即生效。
8
+ */
9
+ /**
10
+ * 等级的数据来源(DIP:路由不关心文件位置,由 index.ts 注入读取函数)。
11
+ * 读取失败或文件不存在时返回 null。
12
+ */
13
+ export interface PlayerLevelSources {
14
+ /** configs/permissions.json:数组 `{ player_name, level }` */
15
+ readSfmcPermissions(): unknown;
16
+ /** BDS 根目录 permissions.json:数组 `{ xuid, permission }` */
17
+ readBdsPermissions(): unknown;
18
+ }
19
+ /**
20
+ * 计算玩家有效等级。
21
+ *
22
+ * @param player.xuid 玩家 XUID(查 BDS 原生权限)。
23
+ * @param player.name 玩家名(查 SFMC 覆盖等级)。
24
+ */
25
+ export declare function resolvePlayerLevel(sources: PlayerLevelSources, player: {
26
+ xuid: string;
27
+ name: string;
28
+ }): number;
29
+ //# sourceMappingURL=player-level.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"player-level.d.ts","sourceRoot":"","sources":["../../src/domain/player-level.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,2DAA2D;IAC3D,mBAAmB,IAAI,OAAO,CAAC;IAC/B,yDAAyD;IACzD,kBAAkB,IAAI,OAAO,CAAC;CAC/B;AA0BD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,kBAAkB,EAAE,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAK9G"}
@@ -0,0 +1,30 @@
1
+ import { effectivePermissionLevel, nativeLevelFromBdsPermission } from "@sfmc-bds/sdk/contracts";
2
+ function overrideLevel(raw, name) {
3
+ if (!Array.isArray(raw) || !name) return void 0;
4
+ let found;
5
+ for (const row of raw) {
6
+ if (!row || typeof row !== "object") continue;
7
+ const entry = row;
8
+ if (entry.player_name === name && typeof entry.level === "number") found = entry.level;
9
+ }
10
+ return found;
11
+ }
12
+ function nativeLevel(raw, xuid) {
13
+ if (!Array.isArray(raw) || !xuid) return void 0;
14
+ for (const row of raw) {
15
+ if (!row || typeof row !== "object") continue;
16
+ const entry = row;
17
+ if (String(entry.xuid ?? "") === xuid) return nativeLevelFromBdsPermission(entry.permission);
18
+ }
19
+ return void 0;
20
+ }
21
+ function resolvePlayerLevel(sources, player) {
22
+ return effectivePermissionLevel(
23
+ overrideLevel(sources.readSfmcPermissions(), player.name.trim()),
24
+ nativeLevel(sources.readBdsPermissions(), player.xuid.trim())
25
+ );
26
+ }
27
+ export {
28
+ resolvePlayerLevel
29
+ };
30
+ //# sourceMappingURL=player-level.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/domain/player-level.ts"],
4
+ "sourcesContent": ["/**\n * domain/player-level.ts \u2014 \u5BBF\u4E3B\u4FA7\u8BA1\u7B97\u73A9\u5BB6\u7684 SFMC \u6709\u6548\u6743\u9650\u7B49\u7EA7\n *\n * \u4F7F\u7528\u573A\u666F\uFF1AGET /api/sfmc/wechat/bind/me \u8FD4\u56DE\u7ED1\u5B9A\u73A9\u5BB6\u7684\u7B49\u7EA7\uFF0Cmp-gateway \u636E\u6B64\u5224\u65AD\u5FAE\u4FE1\u7528\u6237\u662F\u5426\u4E3A\u7BA1\u7406\u5458\uFF08\u7B49\u7EA7 3\uFF09\u3002\n * \u6E38\u620F\u5185\u7531 Permission.getPermission \u8BA1\u7B97\uFF0C\u5BBF\u4E3B\u4FA7\u6CA1\u6709 Player \u5BF9\u8C61\uFF0C\u8FD9\u91CC\u6539\u4ECE\u4E24\u4EFD\u6587\u4EF6\u53D6\u8F93\u5165\uFF0C\n * \u5408\u6210\u89C4\u5219\u5171\u7528 @sfmc-bds/sdk/contracts \u7684 effectivePermissionLevel\uFF08DRY\uFF09\u3002\n * \u6BCF\u6B21\u8C03\u7528\u90FD\u73B0\u8BFB\u6587\u4EF6\uFF0C\u7BA1\u7406\u5458\u5728\u684C\u9762\u7AEF\u6216\u6E38\u620F\u91CC\u8C03\u6574\u6743\u9650\u540E\u7ACB\u5373\u751F\u6548\u3002\n */\n\nimport { effectivePermissionLevel, nativeLevelFromBdsPermission } from \"@sfmc-bds/sdk/contracts\";\n\n/**\n * \u7B49\u7EA7\u7684\u6570\u636E\u6765\u6E90\uFF08DIP\uFF1A\u8DEF\u7531\u4E0D\u5173\u5FC3\u6587\u4EF6\u4F4D\u7F6E\uFF0C\u7531 index.ts \u6CE8\u5165\u8BFB\u53D6\u51FD\u6570\uFF09\u3002\n * \u8BFB\u53D6\u5931\u8D25\u6216\u6587\u4EF6\u4E0D\u5B58\u5728\u65F6\u8FD4\u56DE null\u3002\n */\nexport interface PlayerLevelSources {\n /** configs/permissions.json\uFF1A\u6570\u7EC4 `{ player_name, level }` */\n readSfmcPermissions(): unknown;\n /** BDS \u6839\u76EE\u5F55 permissions.json\uFF1A\u6570\u7EC4 `{ xuid, permission }` */\n readBdsPermissions(): unknown;\n}\n\n/** \u5728 configs/permissions.json \u4E2D\u6309\u73A9\u5BB6\u540D\u67E5\u8986\u76D6\u7B49\u7EA7\uFF08\u4E0E ConfigManager.getPermissions \u7684\u89E3\u6790\u53E3\u5F84\u4E00\u81F4\uFF09 */\nfunction overrideLevel(raw: unknown, name: string): number | undefined {\n if (!Array.isArray(raw) || !name) return undefined;\n let found: number | undefined;\n for (const row of raw) {\n if (!row || typeof row !== \"object\") continue;\n const entry = row as { player_name?: unknown; level?: unknown };\n // \u540C\u540D\u591A\u6761\u65F6\u540E\u8005\u8986\u76D6\u524D\u8005\uFF0C\u4E0E ConfigManager \u9010\u6761\u5199\u5165 map \u7684\u7ED3\u679C\u4E00\u81F4\n if (entry.player_name === name && typeof entry.level === \"number\") found = entry.level;\n }\n return found;\n}\n\n/** \u5728 BDS permissions.json \u4E2D\u6309 xuid \u67E5\u539F\u751F\u7B49\u7EA7 */\nfunction nativeLevel(raw: unknown, xuid: string): number | undefined {\n if (!Array.isArray(raw) || !xuid) return undefined;\n for (const row of raw) {\n if (!row || typeof row !== \"object\") continue;\n const entry = row as { xuid?: unknown; permission?: unknown };\n if (String(entry.xuid ?? \"\") === xuid) return nativeLevelFromBdsPermission(entry.permission);\n }\n return undefined;\n}\n\n/**\n * \u8BA1\u7B97\u73A9\u5BB6\u6709\u6548\u7B49\u7EA7\u3002\n *\n * @param player.xuid \u73A9\u5BB6 XUID\uFF08\u67E5 BDS \u539F\u751F\u6743\u9650\uFF09\u3002\n * @param player.name \u73A9\u5BB6\u540D\uFF08\u67E5 SFMC \u8986\u76D6\u7B49\u7EA7\uFF09\u3002\n */\nexport function resolvePlayerLevel(sources: PlayerLevelSources, player: { xuid: string; name: string }): number {\n return effectivePermissionLevel(\n overrideLevel(sources.readSfmcPermissions(), player.name.trim()),\n nativeLevel(sources.readBdsPermissions(), player.xuid.trim())\n );\n}\n"],
5
+ "mappings": "AASA,SAAS,0BAA0B,oCAAoC;AAcvE,SAAS,cAAc,KAAc,MAAkC;AACrE,MAAI,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAM,QAAO;AACzC,MAAI;AACJ,aAAW,OAAO,KAAK;AACrB,QAAI,CAAC,OAAO,OAAO,QAAQ,SAAU;AACrC,UAAM,QAAQ;AAEd,QAAI,MAAM,gBAAgB,QAAQ,OAAO,MAAM,UAAU,SAAU,SAAQ,MAAM;AAAA,EACnF;AACA,SAAO;AACT;AAGA,SAAS,YAAY,KAAc,MAAkC;AACnE,MAAI,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAM,QAAO;AACzC,aAAW,OAAO,KAAK;AACrB,QAAI,CAAC,OAAO,OAAO,QAAQ,SAAU;AACrC,UAAM,QAAQ;AACd,QAAI,OAAO,MAAM,QAAQ,EAAE,MAAM,KAAM,QAAO,6BAA6B,MAAM,UAAU;AAAA,EAC7F;AACA,SAAO;AACT;AAQO,SAAS,mBAAmB,SAA6B,QAAgD;AAC9G,SAAO;AAAA,IACL,cAAc,QAAQ,oBAAoB,GAAG,OAAO,KAAK,KAAK,CAAC;AAAA,IAC/D,YAAY,QAAQ,mBAAmB,GAAG,OAAO,KAAK,KAAK,CAAC;AAAA,EAC9D;AACF;",
6
+ "names": []
7
+ }
@@ -5,4 +5,16 @@ export interface ProcessResources {
5
5
  cpuSeconds: number | null;
6
6
  }
7
7
  export declare function collectProcessResources(pid: number, running: boolean): Promise<ProcessResources>;
8
+ /** 一次系统查询得到的单个进程用量。查不到的 PID 不会出现。 */
9
+ export interface ProcessUsage {
10
+ memoryMb: number;
11
+ cpuSeconds: number | null;
12
+ }
13
+ /**
14
+ * 一次查询多个 PID 的工作集和累计 CPU。
15
+ * 使用场景:内存面板除 BDS、数据服务外还要看守护进程、QQ 桥等,避免每个进程各起一次系统命令。
16
+ * 当前进程仍用 process.memoryUsage,与 collectProcessResources 对自身的算法一致。
17
+ * 命令失败时返回空表,调用方按「这次没采到」处理,不把缺失写成 0。
18
+ */
19
+ export declare function collectProcessResourcesMany(pids: number[]): Promise<Map<number, ProcessUsage>>;
8
20
  //# sourceMappingURL=process-resources.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"process-resources.d.ts","sourceRoot":"","sources":["../../src/domain/process-resources.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAmCtG"}
1
+ {"version":3,"file":"process-resources.d.ts","sourceRoot":"","sources":["../../src/domain/process-resources.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAmCtG;AAED,qCAAqC;AACrC,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAsB,2BAA2B,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CA+CpG"}
@@ -35,7 +35,78 @@ async function collectProcessResources(pid, running) {
35
35
  }
36
36
  return result;
37
37
  }
38
+ async function collectProcessResourcesMany(pids) {
39
+ const unique = [...new Set(pids.filter((pid) => Number.isSafeInteger(pid) && pid > 0))];
40
+ const found = /* @__PURE__ */ new Map();
41
+ if (unique.includes(process.pid)) {
42
+ const cpu = process.cpuUsage();
43
+ found.set(process.pid, {
44
+ memoryMb: Math.round(process.memoryUsage().rss / 1048576),
45
+ cpuSeconds: (cpu.user + cpu.system) / 1e6
46
+ });
47
+ }
48
+ const external = unique.filter((pid) => pid !== process.pid);
49
+ if (!external.length) return found;
50
+ try {
51
+ if (process.platform === "win32") {
52
+ const script = [
53
+ "$ErrorActionPreference = 'SilentlyContinue'",
54
+ `$ids = @(${external.join(",")})`,
55
+ "$rows = foreach ($id in $ids) {",
56
+ " $p = Get-Process -Id $id -ErrorAction SilentlyContinue",
57
+ " if ($null -eq $p) { continue }",
58
+ " $cpu = $p.CPU",
59
+ " [pscustomobject]@{ pid = [int]$p.Id; memoryMb = [int][Math]::Round($p.WorkingSet64/1MB); cpuSeconds = $(if ($null -eq $cpu) { $null } else { [double]$cpu }) }",
60
+ "}",
61
+ "if ($null -eq $rows) { '[]' } else { ConvertTo-Json -InputObject @($rows) -Compress }"
62
+ ].join("\n");
63
+ const { stdout } = await execute("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
64
+ windowsHide: true,
65
+ timeout: 2e3
66
+ });
67
+ for (const row of parseUsageRows(JSON.parse(stdout))) found.set(row.pid, row.usage);
68
+ } else if (process.platform === "linux") {
69
+ const { stdout } = await execute("ps", ["-p", external.join(","), "-o", "pid=", "-o", "rss=", "-o", "time="], {
70
+ timeout: 2e3
71
+ });
72
+ for (const line of stdout.split(/\n/)) {
73
+ const match = line.trim().match(/^(\d+)\s+(\d+)\s+(\S+)$/);
74
+ if (!match) continue;
75
+ const pid = Number(match[1]);
76
+ const rssKb = Number(match[2]);
77
+ if (!Number.isSafeInteger(pid) || pid <= 0 || !Number.isFinite(rssKb)) continue;
78
+ found.set(pid, { memoryMb: Math.round(rssKb / 1024), cpuSeconds: parseCpuClock(match[3] ?? "") });
79
+ }
80
+ }
81
+ } catch {
82
+ }
83
+ return found;
84
+ }
85
+ function parseUsageRows(parsed) {
86
+ const list = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" ? [parsed] : [];
87
+ const rows = [];
88
+ for (const item of list) {
89
+ if (!item || typeof item !== "object") continue;
90
+ const row = item;
91
+ if (typeof row.pid !== "number" || !Number.isSafeInteger(row.pid) || row.pid <= 0) continue;
92
+ if (typeof row.memoryMb !== "number" || !Number.isFinite(row.memoryMb) || row.memoryMb < 0) continue;
93
+ const cpuSeconds = typeof row.cpuSeconds === "number" && Number.isFinite(row.cpuSeconds) && row.cpuSeconds >= 0 ? row.cpuSeconds : null;
94
+ rows.push({ pid: row.pid, usage: { memoryMb: row.memoryMb, cpuSeconds } });
95
+ }
96
+ return rows;
97
+ }
98
+ function parseCpuClock(text) {
99
+ const match = text.trim().match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/);
100
+ if (!match) return null;
101
+ const days = Number(match[1] ?? 0);
102
+ const hours = Number(match[2] ?? 0);
103
+ const minutes = Number(match[3]);
104
+ const seconds = Number(match[4]);
105
+ if (![days, hours, minutes, seconds].every((value) => Number.isFinite(value))) return null;
106
+ return days * 86400 + hours * 3600 + minutes * 60 + seconds;
107
+ }
38
108
  export {
39
- collectProcessResources
109
+ collectProcessResources,
110
+ collectProcessResourcesMany
40
111
  };
41
112
  //# sourceMappingURL=process-resources.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/domain/process-resources.ts"],
4
- "sourcesContent": ["/** \u53D7\u63A7\u8FDB\u7A0B\u8D44\u6E90\u91C7\u96C6\uFF1A\u4EC5\u5141\u8BB8\u5DF2\u6838\u9A8C\u7684\u6570\u5B57 PID\uFF0C\u5931\u8D25\u4FDD\u7559\u672A\u77E5\u3002 */\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nconst execute = promisify(execFile);\nexport interface ProcessResources {\n pid: number;\n running: boolean;\n memoryMb: number | null;\n cpuSeconds: number | null;\n}\n\nexport async function collectProcessResources(pid: number, running: boolean): Promise<ProcessResources> {\n const result: ProcessResources = { pid, running, memoryMb: null, cpuSeconds: null };\n if (!running || !Number.isSafeInteger(pid) || pid <= 0) return result;\n if (pid === process.pid) {\n const cpu = process.cpuUsage();\n return {\n ...result,\n memoryMb: Math.round(process.memoryUsage().rss / 1048576),\n cpuSeconds: (cpu.user + cpu.system) / 1_000_000,\n };\n }\n try {\n if (process.platform === \"win32\") {\n const script = `$p=Get-Process -Id ${pid} -ErrorAction Stop; @{memoryMb=[Math]::Round($p.WorkingSet64/1MB);cpuSeconds=$p.CPU}|ConvertTo-Json -Compress`;\n const { stdout } = await execute(\"powershell.exe\", [\"-NoProfile\", \"-NonInteractive\", \"-Command\", script], {\n windowsHide: true,\n timeout: 2000,\n });\n const data = JSON.parse(stdout) as { memoryMb: number; cpuSeconds: number | null };\n if (Number.isFinite(data.memoryMb) && data.memoryMb >= 0) result.memoryMb = data.memoryMb;\n if (typeof data.cpuSeconds === \"number\" && Number.isFinite(data.cpuSeconds) && data.cpuSeconds >= 0)\n result.cpuSeconds = data.cpuSeconds;\n } else if (process.platform === \"linux\") {\n const { stdout } = await execute(\"ps\", [\"-p\", String(pid), \"-o\", \"rss=\", \"-o\", \"time=\"], { timeout: 2000 });\n const match = stdout.trim().match(/^(\\d+)\\s+(?:(\\d+)-)?(\\d+):(\\d+):(\\d+)$/);\n if (match) {\n result.memoryMb = Math.round(Number(match[1]) / 1024);\n result.cpuSeconds =\n Number(match[2] ?? 0) * 86400 + Number(match[3]) * 3600 + Number(match[4]) * 60 + Number(match[5]);\n }\n }\n } catch {\n /* \u65E0\u6743\u9650\u3001\u8FDB\u7A0B\u9000\u51FA\u3001\u91C7\u96C6\u8D85\u65F6\u5747\u4E0D\u4F2A\u62A5 0\u3002 */\n }\n return result;\n}\n"],
5
- "mappings": "AACA,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAC1B,MAAM,UAAU,UAAU,QAAQ;AAQlC,eAAsB,wBAAwB,KAAa,SAA6C;AACtG,QAAM,SAA2B,EAAE,KAAK,SAAS,UAAU,MAAM,YAAY,KAAK;AAClF,MAAI,CAAC,WAAW,CAAC,OAAO,cAAc,GAAG,KAAK,OAAO,EAAG,QAAO;AAC/D,MAAI,QAAQ,QAAQ,KAAK;AACvB,UAAM,MAAM,QAAQ,SAAS;AAC7B,WAAO;AAAA,MACL,GAAG;AAAA,MACH,UAAU,KAAK,MAAM,QAAQ,YAAY,EAAE,MAAM,OAAO;AAAA,MACxD,aAAa,IAAI,OAAO,IAAI,UAAU;AAAA,IACxC;AAAA,EACF;AACA,MAAI;AACF,QAAI,QAAQ,aAAa,SAAS;AAChC,YAAM,SAAS,sBAAsB,GAAG;AACxC,YAAM,EAAE,OAAO,IAAI,MAAM,QAAQ,kBAAkB,CAAC,cAAc,mBAAmB,YAAY,MAAM,GAAG;AAAA,QACxG,aAAa;AAAA,QACb,SAAS;AAAA,MACX,CAAC;AACD,YAAM,OAAO,KAAK,MAAM,MAAM;AAC9B,UAAI,OAAO,SAAS,KAAK,QAAQ,KAAK,KAAK,YAAY,EAAG,QAAO,WAAW,KAAK;AACjF,UAAI,OAAO,KAAK,eAAe,YAAY,OAAO,SAAS,KAAK,UAAU,KAAK,KAAK,cAAc;AAChG,eAAO,aAAa,KAAK;AAAA,IAC7B,WAAW,QAAQ,aAAa,SAAS;AACvC,YAAM,EAAE,OAAO,IAAI,MAAM,QAAQ,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,MAAM,QAAQ,MAAM,OAAO,GAAG,EAAE,SAAS,IAAK,CAAC;AAC1G,YAAM,QAAQ,OAAO,KAAK,EAAE,MAAM,wCAAwC;AAC1E,UAAI,OAAO;AACT,eAAO,WAAW,KAAK,MAAM,OAAO,MAAM,CAAC,CAAC,IAAI,IAAI;AACpD,eAAO,aACL,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,QAAQ,OAAO,MAAM,CAAC,CAAC,IAAI,OAAO,OAAO,MAAM,CAAC,CAAC,IAAI,KAAK,OAAO,MAAM,CAAC,CAAC;AAAA,MACrG;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;",
4
+ "sourcesContent": ["/** \u53D7\u63A7\u8FDB\u7A0B\u8D44\u6E90\u91C7\u96C6\uFF1A\u4EC5\u5141\u8BB8\u5DF2\u6838\u9A8C\u7684\u6570\u5B57 PID\uFF0C\u5931\u8D25\u4FDD\u7559\u672A\u77E5\u3002 */\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nconst execute = promisify(execFile);\nexport interface ProcessResources {\n pid: number;\n running: boolean;\n memoryMb: number | null;\n cpuSeconds: number | null;\n}\n\nexport async function collectProcessResources(pid: number, running: boolean): Promise<ProcessResources> {\n const result: ProcessResources = { pid, running, memoryMb: null, cpuSeconds: null };\n if (!running || !Number.isSafeInteger(pid) || pid <= 0) return result;\n if (pid === process.pid) {\n const cpu = process.cpuUsage();\n return {\n ...result,\n memoryMb: Math.round(process.memoryUsage().rss / 1048576),\n cpuSeconds: (cpu.user + cpu.system) / 1_000_000,\n };\n }\n try {\n if (process.platform === \"win32\") {\n const script = `$p=Get-Process -Id ${pid} -ErrorAction Stop; @{memoryMb=[Math]::Round($p.WorkingSet64/1MB);cpuSeconds=$p.CPU}|ConvertTo-Json -Compress`;\n const { stdout } = await execute(\"powershell.exe\", [\"-NoProfile\", \"-NonInteractive\", \"-Command\", script], {\n windowsHide: true,\n timeout: 2000,\n });\n const data = JSON.parse(stdout) as { memoryMb: number; cpuSeconds: number | null };\n if (Number.isFinite(data.memoryMb) && data.memoryMb >= 0) result.memoryMb = data.memoryMb;\n if (typeof data.cpuSeconds === \"number\" && Number.isFinite(data.cpuSeconds) && data.cpuSeconds >= 0)\n result.cpuSeconds = data.cpuSeconds;\n } else if (process.platform === \"linux\") {\n const { stdout } = await execute(\"ps\", [\"-p\", String(pid), \"-o\", \"rss=\", \"-o\", \"time=\"], { timeout: 2000 });\n const match = stdout.trim().match(/^(\\d+)\\s+(?:(\\d+)-)?(\\d+):(\\d+):(\\d+)$/);\n if (match) {\n result.memoryMb = Math.round(Number(match[1]) / 1024);\n result.cpuSeconds =\n Number(match[2] ?? 0) * 86400 + Number(match[3]) * 3600 + Number(match[4]) * 60 + Number(match[5]);\n }\n }\n } catch {\n /* \u65E0\u6743\u9650\u3001\u8FDB\u7A0B\u9000\u51FA\u3001\u91C7\u96C6\u8D85\u65F6\u5747\u4E0D\u4F2A\u62A5 0\u3002 */\n }\n return result;\n}\n\n/** \u4E00\u6B21\u7CFB\u7EDF\u67E5\u8BE2\u5F97\u5230\u7684\u5355\u4E2A\u8FDB\u7A0B\u7528\u91CF\u3002\u67E5\u4E0D\u5230\u7684 PID \u4E0D\u4F1A\u51FA\u73B0\u3002 */\nexport interface ProcessUsage {\n memoryMb: number;\n cpuSeconds: number | null;\n}\n\n/**\n * \u4E00\u6B21\u67E5\u8BE2\u591A\u4E2A PID \u7684\u5DE5\u4F5C\u96C6\u548C\u7D2F\u8BA1 CPU\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1A\u5185\u5B58\u9762\u677F\u9664 BDS\u3001\u6570\u636E\u670D\u52A1\u5916\u8FD8\u8981\u770B\u5B88\u62A4\u8FDB\u7A0B\u3001QQ \u6865\u7B49\uFF0C\u907F\u514D\u6BCF\u4E2A\u8FDB\u7A0B\u5404\u8D77\u4E00\u6B21\u7CFB\u7EDF\u547D\u4EE4\u3002\n * \u5F53\u524D\u8FDB\u7A0B\u4ECD\u7528 process.memoryUsage\uFF0C\u4E0E collectProcessResources \u5BF9\u81EA\u8EAB\u7684\u7B97\u6CD5\u4E00\u81F4\u3002\n * \u547D\u4EE4\u5931\u8D25\u65F6\u8FD4\u56DE\u7A7A\u8868\uFF0C\u8C03\u7528\u65B9\u6309\u300C\u8FD9\u6B21\u6CA1\u91C7\u5230\u300D\u5904\u7406\uFF0C\u4E0D\u628A\u7F3A\u5931\u5199\u6210 0\u3002\n */\nexport async function collectProcessResourcesMany(pids: number[]): Promise<Map<number, ProcessUsage>> {\n const unique = [...new Set(pids.filter((pid) => Number.isSafeInteger(pid) && pid > 0))];\n const found = new Map<number, ProcessUsage>();\n if (unique.includes(process.pid)) {\n const cpu = process.cpuUsage();\n found.set(process.pid, {\n memoryMb: Math.round(process.memoryUsage().rss / 1048576),\n cpuSeconds: (cpu.user + cpu.system) / 1_000_000,\n });\n }\n const external = unique.filter((pid) => pid !== process.pid);\n if (!external.length) return found;\n try {\n if (process.platform === \"win32\") {\n const script = [\n \"$ErrorActionPreference = 'SilentlyContinue'\",\n `$ids = @(${external.join(\",\")})`,\n \"$rows = foreach ($id in $ids) {\",\n \" $p = Get-Process -Id $id -ErrorAction SilentlyContinue\",\n \" if ($null -eq $p) { continue }\",\n \" $cpu = $p.CPU\",\n \" [pscustomobject]@{ pid = [int]$p.Id; memoryMb = [int][Math]::Round($p.WorkingSet64/1MB); cpuSeconds = $(if ($null -eq $cpu) { $null } else { [double]$cpu }) }\",\n \"}\",\n \"if ($null -eq $rows) { '[]' } else { ConvertTo-Json -InputObject @($rows) -Compress }\",\n ].join(\"\\n\");\n const { stdout } = await execute(\"powershell.exe\", [\"-NoProfile\", \"-NonInteractive\", \"-Command\", script], {\n windowsHide: true,\n timeout: 2000,\n });\n for (const row of parseUsageRows(JSON.parse(stdout))) found.set(row.pid, row.usage);\n } else if (process.platform === \"linux\") {\n const { stdout } = await execute(\"ps\", [\"-p\", external.join(\",\"), \"-o\", \"pid=\", \"-o\", \"rss=\", \"-o\", \"time=\"], {\n timeout: 2000,\n });\n for (const line of stdout.split(/\\n/)) {\n const match = line.trim().match(/^(\\d+)\\s+(\\d+)\\s+(\\S+)$/);\n if (!match) continue;\n const pid = Number(match[1]);\n const rssKb = Number(match[2]);\n if (!Number.isSafeInteger(pid) || pid <= 0 || !Number.isFinite(rssKb)) continue;\n found.set(pid, { memoryMb: Math.round(rssKb / 1024), cpuSeconds: parseCpuClock(match[3] ?? \"\") });\n }\n }\n } catch {\n /* \u91C7\u96C6\u8D85\u65F6\u6216\u8FDB\u7A0B\u5DF2\u9000\u51FA\u65F6\uFF0C\u5DF2\u6210\u529F\u7684\u81EA\u8EAB\u8BFB\u6570\u4ECD\u4FDD\u7559\uFF0C\u5176\u4F59\u6309\u6CA1\u91C7\u5230\u5904\u7406\u3002 */\n }\n return found;\n}\n\n/** \u628A\u7CFB\u7EDF\u547D\u4EE4\u8FD4\u56DE\u7684\u884C\u6536\u6210\u7528\u91CF\u3002\u975E\u6CD5\u884C\u8DF3\u8FC7\u3002 */\nfunction parseUsageRows(parsed: unknown): Array<{ pid: number; usage: ProcessUsage }> {\n // Windows PowerShell 5 \u4F1A\u628A\u53EA\u6709\u4E00\u884C\u7684\u7ED3\u679C\u6536\u6210\u5BF9\u8C61\uFF0C7 \u5219\u4FDD\u6301\u6570\u7EC4\u3002\n const list = Array.isArray(parsed) ? parsed : parsed && typeof parsed === \"object\" ? [parsed] : [];\n const rows: Array<{ pid: number; usage: ProcessUsage }> = [];\n for (const item of list) {\n if (!item || typeof item !== \"object\") continue;\n const row = item as { pid?: unknown; memoryMb?: unknown; cpuSeconds?: unknown };\n if (typeof row.pid !== \"number\" || !Number.isSafeInteger(row.pid) || row.pid <= 0) continue;\n if (typeof row.memoryMb !== \"number\" || !Number.isFinite(row.memoryMb) || row.memoryMb < 0) continue;\n const cpuSeconds =\n typeof row.cpuSeconds === \"number\" && Number.isFinite(row.cpuSeconds) && row.cpuSeconds >= 0 ? row.cpuSeconds : null;\n rows.push({ pid: row.pid, usage: { memoryMb: row.memoryMb, cpuSeconds } });\n }\n return rows;\n}\n\n/** ps \u7684\u65F6\u95F4\u5217\uFF1Add-hh:mm:ss\u3001hh:mm:ss \u6216 mm:ss\u3002\u89E3\u6790\u4E0D\u4E86\u65F6\u8FD4\u56DE null\u3002 */\nfunction parseCpuClock(text: string): number | null {\n const match = text.trim().match(/^(?:(\\d+)-)?(?:(\\d+):)?(\\d+):(\\d+)$/);\n if (!match) return null;\n const days = Number(match[1] ?? 0);\n const hours = Number(match[2] ?? 0);\n const minutes = Number(match[3]);\n const seconds = Number(match[4]);\n if (![days, hours, minutes, seconds].every((value) => Number.isFinite(value))) return null;\n return days * 86400 + hours * 3600 + minutes * 60 + seconds;\n}\n"],
5
+ "mappings": "AACA,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAC1B,MAAM,UAAU,UAAU,QAAQ;AAQlC,eAAsB,wBAAwB,KAAa,SAA6C;AACtG,QAAM,SAA2B,EAAE,KAAK,SAAS,UAAU,MAAM,YAAY,KAAK;AAClF,MAAI,CAAC,WAAW,CAAC,OAAO,cAAc,GAAG,KAAK,OAAO,EAAG,QAAO;AAC/D,MAAI,QAAQ,QAAQ,KAAK;AACvB,UAAM,MAAM,QAAQ,SAAS;AAC7B,WAAO;AAAA,MACL,GAAG;AAAA,MACH,UAAU,KAAK,MAAM,QAAQ,YAAY,EAAE,MAAM,OAAO;AAAA,MACxD,aAAa,IAAI,OAAO,IAAI,UAAU;AAAA,IACxC;AAAA,EACF;AACA,MAAI;AACF,QAAI,QAAQ,aAAa,SAAS;AAChC,YAAM,SAAS,sBAAsB,GAAG;AACxC,YAAM,EAAE,OAAO,IAAI,MAAM,QAAQ,kBAAkB,CAAC,cAAc,mBAAmB,YAAY,MAAM,GAAG;AAAA,QACxG,aAAa;AAAA,QACb,SAAS;AAAA,MACX,CAAC;AACD,YAAM,OAAO,KAAK,MAAM,MAAM;AAC9B,UAAI,OAAO,SAAS,KAAK,QAAQ,KAAK,KAAK,YAAY,EAAG,QAAO,WAAW,KAAK;AACjF,UAAI,OAAO,KAAK,eAAe,YAAY,OAAO,SAAS,KAAK,UAAU,KAAK,KAAK,cAAc;AAChG,eAAO,aAAa,KAAK;AAAA,IAC7B,WAAW,QAAQ,aAAa,SAAS;AACvC,YAAM,EAAE,OAAO,IAAI,MAAM,QAAQ,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,MAAM,QAAQ,MAAM,OAAO,GAAG,EAAE,SAAS,IAAK,CAAC;AAC1G,YAAM,QAAQ,OAAO,KAAK,EAAE,MAAM,wCAAwC;AAC1E,UAAI,OAAO;AACT,eAAO,WAAW,KAAK,MAAM,OAAO,MAAM,CAAC,CAAC,IAAI,IAAI;AACpD,eAAO,aACL,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,QAAQ,OAAO,MAAM,CAAC,CAAC,IAAI,OAAO,OAAO,MAAM,CAAC,CAAC,IAAI,KAAK,OAAO,MAAM,CAAC,CAAC;AAAA,MACrG;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAcA,eAAsB,4BAA4B,MAAoD;AACpG,QAAM,SAAS,CAAC,GAAG,IAAI,IAAI,KAAK,OAAO,CAAC,QAAQ,OAAO,cAAc,GAAG,KAAK,MAAM,CAAC,CAAC,CAAC;AACtF,QAAM,QAAQ,oBAAI,IAA0B;AAC5C,MAAI,OAAO,SAAS,QAAQ,GAAG,GAAG;AAChC,UAAM,MAAM,QAAQ,SAAS;AAC7B,UAAM,IAAI,QAAQ,KAAK;AAAA,MACrB,UAAU,KAAK,MAAM,QAAQ,YAAY,EAAE,MAAM,OAAO;AAAA,MACxD,aAAa,IAAI,OAAO,IAAI,UAAU;AAAA,IACxC,CAAC;AAAA,EACH;AACA,QAAM,WAAW,OAAO,OAAO,CAAC,QAAQ,QAAQ,QAAQ,GAAG;AAC3D,MAAI,CAAC,SAAS,OAAQ,QAAO;AAC7B,MAAI;AACF,QAAI,QAAQ,aAAa,SAAS;AAChC,YAAM,SAAS;AAAA,QACb;AAAA,QACA,YAAY,SAAS,KAAK,GAAG,CAAC;AAAA,QAC9B;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,IAAI;AACX,YAAM,EAAE,OAAO,IAAI,MAAM,QAAQ,kBAAkB,CAAC,cAAc,mBAAmB,YAAY,MAAM,GAAG;AAAA,QACxG,aAAa;AAAA,QACb,SAAS;AAAA,MACX,CAAC;AACD,iBAAW,OAAO,eAAe,KAAK,MAAM,MAAM,CAAC,EAAG,OAAM,IAAI,IAAI,KAAK,IAAI,KAAK;AAAA,IACpF,WAAW,QAAQ,aAAa,SAAS;AACvC,YAAM,EAAE,OAAO,IAAI,MAAM,QAAQ,MAAM,CAAC,MAAM,SAAS,KAAK,GAAG,GAAG,MAAM,QAAQ,MAAM,QAAQ,MAAM,OAAO,GAAG;AAAA,QAC5G,SAAS;AAAA,MACX,CAAC;AACD,iBAAW,QAAQ,OAAO,MAAM,IAAI,GAAG;AACrC,cAAM,QAAQ,KAAK,KAAK,EAAE,MAAM,yBAAyB;AACzD,YAAI,CAAC,MAAO;AACZ,cAAM,MAAM,OAAO,MAAM,CAAC,CAAC;AAC3B,cAAM,QAAQ,OAAO,MAAM,CAAC,CAAC;AAC7B,YAAI,CAAC,OAAO,cAAc,GAAG,KAAK,OAAO,KAAK,CAAC,OAAO,SAAS,KAAK,EAAG;AACvE,cAAM,IAAI,KAAK,EAAE,UAAU,KAAK,MAAM,QAAQ,IAAI,GAAG,YAAY,cAAc,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC;AAAA,MAClG;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAGA,SAAS,eAAe,QAA8D;AAEpF,QAAM,OAAO,MAAM,QAAQ,MAAM,IAAI,SAAS,UAAU,OAAO,WAAW,WAAW,CAAC,MAAM,IAAI,CAAC;AACjG,QAAM,OAAoD,CAAC;AAC3D,aAAW,QAAQ,MAAM;AACvB,QAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AACvC,UAAM,MAAM;AACZ,QAAI,OAAO,IAAI,QAAQ,YAAY,CAAC,OAAO,cAAc,IAAI,GAAG,KAAK,IAAI,OAAO,EAAG;AACnF,QAAI,OAAO,IAAI,aAAa,YAAY,CAAC,OAAO,SAAS,IAAI,QAAQ,KAAK,IAAI,WAAW,EAAG;AAC5F,UAAM,aACJ,OAAO,IAAI,eAAe,YAAY,OAAO,SAAS,IAAI,UAAU,KAAK,IAAI,cAAc,IAAI,IAAI,aAAa;AAClH,SAAK,KAAK,EAAE,KAAK,IAAI,KAAK,OAAO,EAAE,UAAU,IAAI,UAAU,WAAW,EAAE,CAAC;AAAA,EAC3E;AACA,SAAO;AACT;AAGA,SAAS,cAAc,MAA6B;AAClD,QAAM,QAAQ,KAAK,KAAK,EAAE,MAAM,qCAAqC;AACrE,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,OAAO,MAAM,CAAC,KAAK,CAAC;AACjC,QAAM,QAAQ,OAAO,MAAM,CAAC,KAAK,CAAC;AAClC,QAAM,UAAU,OAAO,MAAM,CAAC,CAAC;AAC/B,QAAM,UAAU,OAAO,MAAM,CAAC,CAAC;AAC/B,MAAI,CAAC,CAAC,MAAM,OAAO,SAAS,OAAO,EAAE,MAAM,CAAC,UAAU,OAAO,SAAS,KAAK,CAAC,EAAG,QAAO;AACtF,SAAO,OAAO,QAAQ,QAAQ,OAAO,UAAU,KAAK;AACtD;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * domain/qq-bind.ts — QQ 绑定的确认逻辑与绑定提供方实现
3
+ *
4
+ * 使用场景:原 routes/qq-bind.ts 的 confirm 逻辑抽到这里,
5
+ * 旧路由 POST /api/sfmc/qq/bind/confirm 与统一入口 POST /api/sfmc/bind/confirm 共用(DRY),行为与原实现一致。
6
+ */
7
+ import type { QueryFn } from "../lib/sqlite.js";
8
+ import type { BindConfirmInput, BindConfirmResult, BindProvider } from "./bind-providers.js";
9
+ /** QQ 待确认表中是否存在该短码 */
10
+ export declare function hasQqPendingCode(query: QueryFn, code: string): boolean;
11
+ /**
12
+ * 用短码确认 QQ 绑定(原 routes/qq-bind.ts confirm 分支的逻辑,未改变判断顺序与错误码)。
13
+ * 绑定记录就是自有白名单:写入后游戏侧允许移动并恢复成员/操作员权限。
14
+ */
15
+ export declare function confirmQqBind(query: QueryFn, { code, xuid, name, now }: BindConfirmInput): BindConfirmResult;
16
+ /**
17
+ * QQ 绑定提供方。
18
+ * 使用场景:index.ts 注册到统一确认入口与跨渠道短码查重。
19
+ */
20
+ export declare function createQqBindProvider(query: QueryFn): BindProvider;
21
+ //# sourceMappingURL=qq-bind.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qq-bind.d.ts","sourceRoot":"","sources":["../../src/domain/qq-bind.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAE7F,sBAAsB;AACtB,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,gBAAgB,GAAG,iBAAiB,CA2C5G;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAMjE"}
@@ -0,0 +1,54 @@
1
+ import { SQL } from "sql-template-strings";
2
+ function hasQqPendingCode(query, code) {
3
+ return query(SQL`SELECT 1 AS ok FROM sfmc_qq_bind_pending WHERE code = ${code} LIMIT 1`).length > 0;
4
+ }
5
+ function confirmQqBind(query, { code, xuid, name, now }) {
6
+ const pending = query(SQL`SELECT * FROM sfmc_qq_bind_pending WHERE code = ${code} LIMIT 1`);
7
+ const row = pending[0];
8
+ if (!row) return { ok: false, status: 400, error: "invalid_code" };
9
+ if (Number(row.expires_at) < now) {
10
+ query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE code = ${code}`);
11
+ return { ok: false, status: 400, error: "code_expired" };
12
+ }
13
+ const byOpenid = query(
14
+ SQL`SELECT player_xuid FROM sfmc_qq_bindings WHERE qq_user_openid = ${row.qq_user_openid} LIMIT 1`
15
+ );
16
+ if (byOpenid[0] && byOpenid[0].player_xuid !== xuid) {
17
+ return { ok: false, status: 409, error: "qq_already_bound", hint: "\u8BF7\u5148\u5728 QQ \u4FA7\u89E3\u7ED1" };
18
+ }
19
+ const byXuid = query(
20
+ SQL`SELECT qq_user_openid FROM sfmc_qq_bindings WHERE player_xuid = ${xuid} LIMIT 1`
21
+ );
22
+ if (byXuid[0] && byXuid[0].qq_user_openid !== row.qq_user_openid) {
23
+ return { ok: false, status: 409, error: "player_already_bound", hint: "\u8BF7\u5148\u89E3\u7ED1\u5F53\u524D QQ" };
24
+ }
25
+ query(SQL`DELETE FROM sfmc_qq_bindings WHERE qq_user_openid = ${row.qq_user_openid} OR player_xuid = ${xuid}`);
26
+ query(
27
+ SQL`INSERT INTO sfmc_qq_bindings (qq_user_openid, player_xuid, player_name, qq_backend, bound_at)
28
+ VALUES (${row.qq_user_openid}, ${xuid}, ${name}, ${row.qq_backend}, ${now})`
29
+ );
30
+ query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE code = ${code}`);
31
+ return {
32
+ ok: true,
33
+ binding: {
34
+ qq_user_openid: row.qq_user_openid,
35
+ player_xuid: xuid,
36
+ player_name: name,
37
+ qq_backend: row.qq_backend,
38
+ bound_at: now
39
+ }
40
+ };
41
+ }
42
+ function createQqBindProvider(query) {
43
+ return {
44
+ id: "qq",
45
+ hasPendingCode: (code) => hasQqPendingCode(query, code),
46
+ confirm: (input) => confirmQqBind(query, input)
47
+ };
48
+ }
49
+ export {
50
+ confirmQqBind,
51
+ createQqBindProvider,
52
+ hasQqPendingCode
53
+ };
54
+ //# sourceMappingURL=qq-bind.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/domain/qq-bind.ts"],
4
+ "sourcesContent": ["/**\n * domain/qq-bind.ts \u2014 QQ \u7ED1\u5B9A\u7684\u786E\u8BA4\u903B\u8F91\u4E0E\u7ED1\u5B9A\u63D0\u4F9B\u65B9\u5B9E\u73B0\n *\n * \u4F7F\u7528\u573A\u666F\uFF1A\u539F routes/qq-bind.ts \u7684 confirm \u903B\u8F91\u62BD\u5230\u8FD9\u91CC\uFF0C\n * \u65E7\u8DEF\u7531 POST /api/sfmc/qq/bind/confirm \u4E0E\u7EDF\u4E00\u5165\u53E3 POST /api/sfmc/bind/confirm \u5171\u7528\uFF08DRY\uFF09\uFF0C\u884C\u4E3A\u4E0E\u539F\u5B9E\u73B0\u4E00\u81F4\u3002\n */\n\nimport { SQL } from \"sql-template-strings\";\nimport type { QueryFn } from \"../lib/sqlite.js\";\nimport type { BindConfirmInput, BindConfirmResult, BindProvider } from \"./bind-providers.js\";\n\n/** QQ \u5F85\u786E\u8BA4\u8868\u4E2D\u662F\u5426\u5B58\u5728\u8BE5\u77ED\u7801 */\nexport function hasQqPendingCode(query: QueryFn, code: string): boolean {\n return (query(SQL`SELECT 1 AS ok FROM sfmc_qq_bind_pending WHERE code = ${code} LIMIT 1`) as unknown[]).length > 0;\n}\n\n/**\n * \u7528\u77ED\u7801\u786E\u8BA4 QQ \u7ED1\u5B9A\uFF08\u539F routes/qq-bind.ts confirm \u5206\u652F\u7684\u903B\u8F91\uFF0C\u672A\u6539\u53D8\u5224\u65AD\u987A\u5E8F\u4E0E\u9519\u8BEF\u7801\uFF09\u3002\n * \u7ED1\u5B9A\u8BB0\u5F55\u5C31\u662F\u81EA\u6709\u767D\u540D\u5355\uFF1A\u5199\u5165\u540E\u6E38\u620F\u4FA7\u5141\u8BB8\u79FB\u52A8\u5E76\u6062\u590D\u6210\u5458/\u64CD\u4F5C\u5458\u6743\u9650\u3002\n */\nexport function confirmQqBind(query: QueryFn, { code, xuid, name, now }: BindConfirmInput): BindConfirmResult {\n const pending = query(SQL`SELECT * FROM sfmc_qq_bind_pending WHERE code = ${code} LIMIT 1`) as Array<{\n code: string;\n qq_user_openid: string;\n qq_backend: string;\n expires_at: number;\n }>;\n const row = pending[0];\n if (!row) return { ok: false, status: 400, error: \"invalid_code\" };\n if (Number(row.expires_at) < now) {\n query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE code = ${code}`);\n return { ok: false, status: 400, error: \"code_expired\" };\n }\n\n const byOpenid = query(\n SQL`SELECT player_xuid FROM sfmc_qq_bindings WHERE qq_user_openid = ${row.qq_user_openid} LIMIT 1`\n ) as Array<{ player_xuid: string }>;\n if (byOpenid[0] && byOpenid[0].player_xuid !== xuid) {\n return { ok: false, status: 409, error: \"qq_already_bound\", hint: \"\u8BF7\u5148\u5728 QQ \u4FA7\u89E3\u7ED1\" };\n }\n const byXuid = query(\n SQL`SELECT qq_user_openid FROM sfmc_qq_bindings WHERE player_xuid = ${xuid} LIMIT 1`\n ) as Array<{ qq_user_openid: string }>;\n if (byXuid[0] && byXuid[0].qq_user_openid !== row.qq_user_openid) {\n return { ok: false, status: 409, error: \"player_already_bound\", hint: \"\u8BF7\u5148\u89E3\u7ED1\u5F53\u524D QQ\" };\n }\n\n query(SQL`DELETE FROM sfmc_qq_bindings WHERE qq_user_openid = ${row.qq_user_openid} OR player_xuid = ${xuid}`);\n query(\n SQL`INSERT INTO sfmc_qq_bindings (qq_user_openid, player_xuid, player_name, qq_backend, bound_at)\n VALUES (${row.qq_user_openid}, ${xuid}, ${name}, ${row.qq_backend}, ${now})`\n );\n query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE code = ${code}`);\n return {\n ok: true,\n binding: {\n qq_user_openid: row.qq_user_openid,\n player_xuid: xuid,\n player_name: name,\n qq_backend: row.qq_backend,\n bound_at: now,\n },\n };\n}\n\n/**\n * QQ \u7ED1\u5B9A\u63D0\u4F9B\u65B9\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1Aindex.ts \u6CE8\u518C\u5230\u7EDF\u4E00\u786E\u8BA4\u5165\u53E3\u4E0E\u8DE8\u6E20\u9053\u77ED\u7801\u67E5\u91CD\u3002\n */\nexport function createQqBindProvider(query: QueryFn): BindProvider {\n return {\n id: \"qq\",\n hasPendingCode: (code) => hasQqPendingCode(query, code),\n confirm: (input) => confirmQqBind(query, input),\n };\n}\n"],
5
+ "mappings": "AAOA,SAAS,WAAW;AAKb,SAAS,iBAAiB,OAAgB,MAAuB;AACtE,SAAQ,MAAM,4DAA4D,IAAI,UAAU,EAAgB,SAAS;AACnH;AAMO,SAAS,cAAc,OAAgB,EAAE,MAAM,MAAM,MAAM,IAAI,GAAwC;AAC5G,QAAM,UAAU,MAAM,sDAAsD,IAAI,UAAU;AAM1F,QAAM,MAAM,QAAQ,CAAC;AACrB,MAAI,CAAC,IAAK,QAAO,EAAE,IAAI,OAAO,QAAQ,KAAK,OAAO,eAAe;AACjE,MAAI,OAAO,IAAI,UAAU,IAAI,KAAK;AAChC,UAAM,oDAAoD,IAAI,EAAE;AAChE,WAAO,EAAE,IAAI,OAAO,QAAQ,KAAK,OAAO,eAAe;AAAA,EACzD;AAEA,QAAM,WAAW;AAAA,IACf,sEAAsE,IAAI,cAAc;AAAA,EAC1F;AACA,MAAI,SAAS,CAAC,KAAK,SAAS,CAAC,EAAE,gBAAgB,MAAM;AACnD,WAAO,EAAE,IAAI,OAAO,QAAQ,KAAK,OAAO,oBAAoB,MAAM,2CAAa;AAAA,EACjF;AACA,QAAM,SAAS;AAAA,IACb,sEAAsE,IAAI;AAAA,EAC5E;AACA,MAAI,OAAO,CAAC,KAAK,OAAO,CAAC,EAAE,mBAAmB,IAAI,gBAAgB;AAChE,WAAO,EAAE,IAAI,OAAO,QAAQ,KAAK,OAAO,wBAAwB,MAAM,0CAAY;AAAA,EACpF;AAEA,QAAM,0DAA0D,IAAI,cAAc,qBAAqB,IAAI,EAAE;AAC7G;AAAA,IACE;AAAA,kBACc,IAAI,cAAc,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,UAAU,KAAK,GAAG;AAAA,EAC/E;AACA,QAAM,oDAAoD,IAAI,EAAE;AAChE,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,MACP,gBAAgB,IAAI;AAAA,MACpB,aAAa;AAAA,MACb,aAAa;AAAA,MACb,YAAY,IAAI;AAAA,MAChB,UAAU;AAAA,IACZ;AAAA,EACF;AACF;AAMO,SAAS,qBAAqB,OAA8B;AACjE,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,gBAAgB,CAAC,SAAS,iBAAiB,OAAO,IAAI;AAAA,IACtD,SAAS,CAAC,UAAU,cAAc,OAAO,KAAK;AAAA,EAChD;AACF;",
6
+ "names": []
7
+ }
@@ -12,6 +12,7 @@
12
12
  * 1. 世界与玩家运行期状态快照(sfmc_world, sfmc_players)
13
13
  * 2. 跨进程聊天消息聚合(sfmc_chat_messages)
14
14
  * 3. QQ 身份绑定与审批队列(sfmc_qq_*)
15
+ * 4. 微信身份绑定(sfmc_wechat_*,小程序玩家端使用)
15
16
  */
16
17
  import type { DatabaseSync } from "node:sqlite";
17
18
  /**