@sfmc-bds/db-server 0.7.0 → 0.8.1

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 (38) hide show
  1. package/dist/domain/cpu-usage.d.ts +43 -0
  2. package/dist/domain/cpu-usage.d.ts.map +1 -0
  3. package/dist/domain/cpu-usage.js +71 -0
  4. package/dist/domain/cpu-usage.js.map +7 -0
  5. package/dist/domain/metrics.d.ts +8 -0
  6. package/dist/domain/metrics.d.ts.map +1 -1
  7. package/dist/domain/metrics.js +16 -4
  8. package/dist/domain/metrics.js.map +2 -2
  9. package/dist/domain/process-resources.d.ts +2 -0
  10. package/dist/domain/process-resources.d.ts.map +1 -1
  11. package/dist/domain/process-resources.js.map +2 -2
  12. package/dist/domain/scoreboard.d.ts +122 -0
  13. package/dist/domain/scoreboard.d.ts.map +1 -0
  14. package/dist/domain/scoreboard.js +134 -0
  15. package/dist/domain/scoreboard.js.map +7 -0
  16. package/dist/domain/tracked-processes.d.ts +2 -0
  17. package/dist/domain/tracked-processes.d.ts.map +1 -1
  18. package/dist/domain/tracked-processes.js.map +2 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -0
  21. package/dist/index.js.map +2 -2
  22. package/dist/routes/db-routes.d.ts +1 -0
  23. package/dist/routes/db-routes.d.ts.map +1 -1
  24. package/dist/routes/db-routes.js +5 -0
  25. package/dist/routes/db-routes.js.map +2 -2
  26. package/dist/routes/scoreboard.d.ts +20 -0
  27. package/dist/routes/scoreboard.d.ts.map +1 -0
  28. package/dist/routes/scoreboard.js +30 -0
  29. package/dist/routes/scoreboard.js.map +7 -0
  30. package/dist/routes/status.d.ts +6 -1
  31. package/dist/routes/status.d.ts.map +1 -1
  32. package/dist/routes/status.js +33 -8
  33. package/dist/routes/status.js.map +2 -2
  34. package/dist/tx-runner.d.ts +10 -0
  35. package/dist/tx-runner.d.ts.map +1 -1
  36. package/dist/tx-runner.js +17 -0
  37. package/dist/tx-runner.js.map +2 -2
  38. package/package.json +9 -4
@@ -0,0 +1,43 @@
1
+ /**
2
+ * cpu-usage.ts — 把累计 CPU 时间换算成占用率。
3
+ *
4
+ * 使用场景:运行指标的主机 CPU 与各进程 CPU 曲线(GET /api/sfmc/metrics 与资源历史落库)。
5
+ * 占用率口径与任务管理器一致:以整机全部逻辑核为 100%,各进程之和不超过主机。
6
+ * 两次读数间隔太短时沿用上一次的结果,避免 Linux ps 秒级精度在短窗口里跳成 0% / 100%。
7
+ */
8
+ import os from "node:os";
9
+ /** 两次读数的最短间隔(毫秒)。间隔不足时不更新基线,直接返回上一次的占用率。 */
10
+ export declare const CPU_MIN_WINDOW_MS = 5000;
11
+ /**
12
+ * 主机整机 CPU 占用率。
13
+ * 使用场景:status 路由每次采集资源时调用一次;第一次没有基线,返回 null,不拿开机以来的平均值冒充当前值。
14
+ */
15
+ export declare class HostCpuMeter {
16
+ private readCpus;
17
+ private minWindowMs;
18
+ private last;
19
+ /** @param readCpus 读取各核累计时间,默认 os.cpus();单独传入便于替换数据源。 */
20
+ constructor(readCpus?: () => os.CpuInfo[], minWindowMs?: number);
21
+ measure(now?: number): number | null;
22
+ }
23
+ /** 一个待换算的进程读数。key 是角色键(db、bds、daemon、sidecar:<id> 等),同一角色换了 PID 视为新进程。 */
24
+ export interface CpuReading {
25
+ key: string;
26
+ pid: number;
27
+ running: boolean;
28
+ cpuSeconds: number | null;
29
+ }
30
+ /**
31
+ * 各进程 CPU 占用率。
32
+ * 使用场景:status 路由把 BDS、数据服务和已登记进程的累计 CPU 秒数一并交给 measure,按角色键取回占用率。
33
+ * 本轮没出现的角色会被丢弃,附属进程反复启停时基线表不会无限增长。
34
+ */
35
+ export declare class ProcessCpuMeter {
36
+ private cores;
37
+ private minWindowMs;
38
+ private last;
39
+ /** @param cores 逻辑核数,默认取本机可用并行度。 */
40
+ constructor(cores?: number, minWindowMs?: number);
41
+ measure(rows: CpuReading[], now?: number): Map<string, number | null>;
42
+ }
43
+ //# sourceMappingURL=cpu-usage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cpu-usage.d.ts","sourceRoot":"","sources":["../../src/domain/cpu-usage.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB,4CAA4C;AAC5C,eAAO,MAAM,iBAAiB,OAAQ,CAAC;AAOvC;;;GAGG;AACH,qBAAa,YAAY;IAIrB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,WAAW;IAJrB,OAAO,CAAC,IAAI,CAAoF;IAChG,yDAAyD;IACzD,YACU,QAAQ,GAAE,MAAM,EAAE,CAAC,OAAO,EAAY,EACtC,WAAW,SAAoB,EACrC;IAEJ,OAAO,CAAC,GAAG,SAAa,GAAG,MAAM,GAAG,IAAI,CAcvC;CACF;AAED,0EAA0E;AAC1E,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;GAIG;AACH,qBAAa,eAAe;IAIxB,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,WAAW;IAJrB,OAAO,CAAC,IAAI,CAA8F;IAC1G,oCAAoC;IACpC,YACU,KAAK,SAAyC,EAC9C,WAAW,SAAoB,EACrC;IAEJ,OAAO,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,GAAG,SAAa,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAyBxE;CACF"}
@@ -0,0 +1,71 @@
1
+ import os from "node:os";
2
+ const CPU_MIN_WINDOW_MS = 5e3;
3
+ function toPercent(ratio) {
4
+ return Math.round(Math.min(100, Math.max(0, ratio * 100)) * 10) / 10;
5
+ }
6
+ class HostCpuMeter {
7
+ /** @param readCpus 读取各核累计时间,默认 os.cpus();单独传入便于替换数据源。 */
8
+ constructor(readCpus = os.cpus, minWindowMs = CPU_MIN_WINDOW_MS) {
9
+ this.readCpus = readCpus;
10
+ this.minWindowMs = minWindowMs;
11
+ }
12
+ readCpus;
13
+ minWindowMs;
14
+ last = null;
15
+ measure(now = Date.now()) {
16
+ if (this.last && now - this.last.at < this.minWindowMs) return this.last.percent;
17
+ let idle = 0;
18
+ let total = 0;
19
+ for (const cpu of this.readCpus()) {
20
+ const times = cpu.times;
21
+ idle += times.idle;
22
+ total += times.user + times.nice + times.sys + times.idle + times.irq;
23
+ }
24
+ const previous = this.last;
25
+ const deltaTotal = previous ? total - previous.total : 0;
26
+ const percent = previous && deltaTotal > 0 ? toPercent(1 - (idle - previous.idle) / deltaTotal) : null;
27
+ this.last = { idle, total, at: now, percent };
28
+ return percent;
29
+ }
30
+ }
31
+ class ProcessCpuMeter {
32
+ /** @param cores 逻辑核数,默认取本机可用并行度。 */
33
+ constructor(cores = Math.max(1, os.availableParallelism()), minWindowMs = CPU_MIN_WINDOW_MS) {
34
+ this.cores = cores;
35
+ this.minWindowMs = minWindowMs;
36
+ }
37
+ cores;
38
+ minWindowMs;
39
+ last = /* @__PURE__ */ new Map();
40
+ measure(rows, now = Date.now()) {
41
+ const next = /* @__PURE__ */ new Map();
42
+ const result = /* @__PURE__ */ new Map();
43
+ for (const row of rows) {
44
+ if (!row.running || row.pid <= 0 || row.cpuSeconds === null) {
45
+ result.set(row.key, null);
46
+ continue;
47
+ }
48
+ const previous = this.last.get(row.key);
49
+ const samePid = previous?.pid === row.pid;
50
+ if (previous && samePid && now - previous.at < this.minWindowMs) {
51
+ next.set(row.key, previous);
52
+ result.set(row.key, previous.percent);
53
+ continue;
54
+ }
55
+ const deltaCpu = samePid && previous ? row.cpuSeconds - previous.cpuSeconds : NaN;
56
+ const deltaWall = samePid && previous ? (now - previous.at) / 1e3 : 0;
57
+ const percent = deltaWall > 0 && deltaCpu >= 0 ? toPercent(deltaCpu / (deltaWall * this.cores)) : null;
58
+ const entry = { pid: row.pid, cpuSeconds: row.cpuSeconds, at: now, percent };
59
+ next.set(row.key, entry);
60
+ result.set(row.key, percent);
61
+ }
62
+ this.last = next;
63
+ return result;
64
+ }
65
+ }
66
+ export {
67
+ CPU_MIN_WINDOW_MS,
68
+ HostCpuMeter,
69
+ ProcessCpuMeter
70
+ };
71
+ //# sourceMappingURL=cpu-usage.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/domain/cpu-usage.ts"],
4
+ "sourcesContent": ["/**\n * cpu-usage.ts \u2014 \u628A\u7D2F\u8BA1 CPU \u65F6\u95F4\u6362\u7B97\u6210\u5360\u7528\u7387\u3002\n *\n * \u4F7F\u7528\u573A\u666F\uFF1A\u8FD0\u884C\u6307\u6807\u7684\u4E3B\u673A CPU \u4E0E\u5404\u8FDB\u7A0B CPU \u66F2\u7EBF\uFF08GET /api/sfmc/metrics \u4E0E\u8D44\u6E90\u5386\u53F2\u843D\u5E93\uFF09\u3002\n * \u5360\u7528\u7387\u53E3\u5F84\u4E0E\u4EFB\u52A1\u7BA1\u7406\u5668\u4E00\u81F4\uFF1A\u4EE5\u6574\u673A\u5168\u90E8\u903B\u8F91\u6838\u4E3A 100%\uFF0C\u5404\u8FDB\u7A0B\u4E4B\u548C\u4E0D\u8D85\u8FC7\u4E3B\u673A\u3002\n * \u4E24\u6B21\u8BFB\u6570\u95F4\u9694\u592A\u77ED\u65F6\u6CBF\u7528\u4E0A\u4E00\u6B21\u7684\u7ED3\u679C\uFF0C\u907F\u514D Linux ps \u79D2\u7EA7\u7CBE\u5EA6\u5728\u77ED\u7A97\u53E3\u91CC\u8DF3\u6210 0% / 100%\u3002\n */\nimport os from \"node:os\";\n\n/** \u4E24\u6B21\u8BFB\u6570\u7684\u6700\u77ED\u95F4\u9694\uFF08\u6BEB\u79D2\uFF09\u3002\u95F4\u9694\u4E0D\u8DB3\u65F6\u4E0D\u66F4\u65B0\u57FA\u7EBF\uFF0C\u76F4\u63A5\u8FD4\u56DE\u4E0A\u4E00\u6B21\u7684\u5360\u7528\u7387\u3002 */\nexport const CPU_MIN_WINDOW_MS = 5_000;\n\n/** \u5360\u7528\u7387\u4FDD\u7559\u4E00\u4F4D\u5C0F\u6570\uFF0C\u5E76\u6536\u5728 0\u2013100 \u4E4B\u95F4\u3002 */\nfunction toPercent(ratio: number): number {\n return Math.round(Math.min(100, Math.max(0, ratio * 100)) * 10) / 10;\n}\n\n/**\n * \u4E3B\u673A\u6574\u673A CPU \u5360\u7528\u7387\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1Astatus \u8DEF\u7531\u6BCF\u6B21\u91C7\u96C6\u8D44\u6E90\u65F6\u8C03\u7528\u4E00\u6B21\uFF1B\u7B2C\u4E00\u6B21\u6CA1\u6709\u57FA\u7EBF\uFF0C\u8FD4\u56DE null\uFF0C\u4E0D\u62FF\u5F00\u673A\u4EE5\u6765\u7684\u5E73\u5747\u503C\u5192\u5145\u5F53\u524D\u503C\u3002\n */\nexport class HostCpuMeter {\n private last: { idle: number; total: number; at: number; percent: number | null } | null = null;\n /** @param readCpus \u8BFB\u53D6\u5404\u6838\u7D2F\u8BA1\u65F6\u95F4\uFF0C\u9ED8\u8BA4 os.cpus()\uFF1B\u5355\u72EC\u4F20\u5165\u4FBF\u4E8E\u66FF\u6362\u6570\u636E\u6E90\u3002 */\n constructor(\n private readCpus: () => os.CpuInfo[] = os.cpus,\n private minWindowMs = CPU_MIN_WINDOW_MS,\n ) {}\n\n measure(now = Date.now()): number | null {\n if (this.last && now - this.last.at < this.minWindowMs) return this.last.percent;\n let idle = 0;\n let total = 0;\n for (const cpu of this.readCpus()) {\n const times = cpu.times;\n idle += times.idle;\n total += times.user + times.nice + times.sys + times.idle + times.irq;\n }\n const previous = this.last;\n const deltaTotal = previous ? total - previous.total : 0;\n const percent = previous && deltaTotal > 0 ? toPercent(1 - (idle - previous.idle) / deltaTotal) : null;\n this.last = { idle, total, at: now, percent };\n return percent;\n }\n}\n\n/** \u4E00\u4E2A\u5F85\u6362\u7B97\u7684\u8FDB\u7A0B\u8BFB\u6570\u3002key \u662F\u89D2\u8272\u952E\uFF08db\u3001bds\u3001daemon\u3001sidecar:<id> \u7B49\uFF09\uFF0C\u540C\u4E00\u89D2\u8272\u6362\u4E86 PID \u89C6\u4E3A\u65B0\u8FDB\u7A0B\u3002 */\nexport interface CpuReading {\n key: string;\n pid: number;\n running: boolean;\n cpuSeconds: number | null;\n}\n\n/**\n * \u5404\u8FDB\u7A0B CPU \u5360\u7528\u7387\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1Astatus \u8DEF\u7531\u628A BDS\u3001\u6570\u636E\u670D\u52A1\u548C\u5DF2\u767B\u8BB0\u8FDB\u7A0B\u7684\u7D2F\u8BA1 CPU \u79D2\u6570\u4E00\u5E76\u4EA4\u7ED9 measure\uFF0C\u6309\u89D2\u8272\u952E\u53D6\u56DE\u5360\u7528\u7387\u3002\n * \u672C\u8F6E\u6CA1\u51FA\u73B0\u7684\u89D2\u8272\u4F1A\u88AB\u4E22\u5F03\uFF0C\u9644\u5C5E\u8FDB\u7A0B\u53CD\u590D\u542F\u505C\u65F6\u57FA\u7EBF\u8868\u4E0D\u4F1A\u65E0\u9650\u589E\u957F\u3002\n */\nexport class ProcessCpuMeter {\n private last = new Map<string, { pid: number; cpuSeconds: number; at: number; percent: number | null }>();\n /** @param cores \u903B\u8F91\u6838\u6570\uFF0C\u9ED8\u8BA4\u53D6\u672C\u673A\u53EF\u7528\u5E76\u884C\u5EA6\u3002 */\n constructor(\n private cores = Math.max(1, os.availableParallelism()),\n private minWindowMs = CPU_MIN_WINDOW_MS,\n ) {}\n\n measure(rows: CpuReading[], now = Date.now()): Map<string, number | null> {\n const next = new Map<string, { pid: number; cpuSeconds: number; at: number; percent: number | null }>();\n const result = new Map<string, number | null>();\n for (const row of rows) {\n if (!row.running || row.pid <= 0 || row.cpuSeconds === null) {\n result.set(row.key, null);\n continue;\n }\n const previous = this.last.get(row.key);\n const samePid = previous?.pid === row.pid;\n if (previous && samePid && now - previous.at < this.minWindowMs) {\n next.set(row.key, previous);\n result.set(row.key, previous.percent);\n continue;\n }\n const deltaCpu = samePid && previous ? row.cpuSeconds - previous.cpuSeconds : NaN;\n const deltaWall = samePid && previous ? (now - previous.at) / 1000 : 0;\n // \u7D2F\u8BA1\u503C\u56DE\u9000\u8BF4\u660E PID \u88AB\u590D\u7528\u6216\u8BFB\u6570\u5F02\u5E38\uFF0C\u8FD9\u4E00\u8F6E\u53EA\u91CD\u5EFA\u57FA\u7EBF\u3002\n const percent = deltaWall > 0 && deltaCpu >= 0 ? toPercent(deltaCpu / (deltaWall * this.cores)) : null;\n const entry = { pid: row.pid, cpuSeconds: row.cpuSeconds, at: now, percent };\n next.set(row.key, entry);\n result.set(row.key, percent);\n }\n this.last = next;\n return result;\n }\n}\n"],
5
+ "mappings": "AAOA,OAAO,QAAQ;AAGR,MAAM,oBAAoB;AAGjC,SAAS,UAAU,OAAuB;AACxC,SAAO,KAAK,MAAM,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,QAAQ,GAAG,CAAC,IAAI,EAAE,IAAI;AACpE;AAMO,MAAM,aAAa;AAAA;AAAA,EAGxB,YACU,WAA+B,GAAG,MAClC,cAAc,mBACtB;AAFQ;AACA;AAAA,EACP;AAAA,EAFO;AAAA,EACA;AAAA,EAJF,OAAmF;AAAA,EAO3F,QAAQ,MAAM,KAAK,IAAI,GAAkB;AACvC,QAAI,KAAK,QAAQ,MAAM,KAAK,KAAK,KAAK,KAAK,YAAa,QAAO,KAAK,KAAK;AACzE,QAAI,OAAO;AACX,QAAI,QAAQ;AACZ,eAAW,OAAO,KAAK,SAAS,GAAG;AACjC,YAAM,QAAQ,IAAI;AAClB,cAAQ,MAAM;AACd,eAAS,MAAM,OAAO,MAAM,OAAO,MAAM,MAAM,MAAM,OAAO,MAAM;AAAA,IACpE;AACA,UAAM,WAAW,KAAK;AACtB,UAAM,aAAa,WAAW,QAAQ,SAAS,QAAQ;AACvD,UAAM,UAAU,YAAY,aAAa,IAAI,UAAU,KAAK,OAAO,SAAS,QAAQ,UAAU,IAAI;AAClG,SAAK,OAAO,EAAE,MAAM,OAAO,IAAI,KAAK,QAAQ;AAC5C,WAAO;AAAA,EACT;AACF;AAeO,MAAM,gBAAgB;AAAA;AAAA,EAG3B,YACU,QAAQ,KAAK,IAAI,GAAG,GAAG,qBAAqB,CAAC,GAC7C,cAAc,mBACtB;AAFQ;AACA;AAAA,EACP;AAAA,EAFO;AAAA,EACA;AAAA,EAJF,OAAO,oBAAI,IAAqF;AAAA,EAOxG,QAAQ,MAAoB,MAAM,KAAK,IAAI,GAA+B;AACxE,UAAM,OAAO,oBAAI,IAAqF;AACtG,UAAM,SAAS,oBAAI,IAA2B;AAC9C,eAAW,OAAO,MAAM;AACtB,UAAI,CAAC,IAAI,WAAW,IAAI,OAAO,KAAK,IAAI,eAAe,MAAM;AAC3D,eAAO,IAAI,IAAI,KAAK,IAAI;AACxB;AAAA,MACF;AACA,YAAM,WAAW,KAAK,KAAK,IAAI,IAAI,GAAG;AACtC,YAAM,UAAU,UAAU,QAAQ,IAAI;AACtC,UAAI,YAAY,WAAW,MAAM,SAAS,KAAK,KAAK,aAAa;AAC/D,aAAK,IAAI,IAAI,KAAK,QAAQ;AAC1B,eAAO,IAAI,IAAI,KAAK,SAAS,OAAO;AACpC;AAAA,MACF;AACA,YAAM,WAAW,WAAW,WAAW,IAAI,aAAa,SAAS,aAAa;AAC9E,YAAM,YAAY,WAAW,YAAY,MAAM,SAAS,MAAM,MAAO;AAErE,YAAM,UAAU,YAAY,KAAK,YAAY,IAAI,UAAU,YAAY,YAAY,KAAK,MAAM,IAAI;AAClG,YAAM,QAAQ,EAAE,KAAK,IAAI,KAAK,YAAY,IAAI,YAAY,IAAI,KAAK,QAAQ;AAC3E,WAAK,IAAI,IAAI,KAAK,KAAK;AACvB,aAAO,IAAI,IAAI,KAAK,OAAO;AAAA,IAC7B;AACA,SAAK,OAAO;AACZ,WAAO;AAAA,EACT;AACF;",
6
+ "names": []
7
+ }
@@ -32,6 +32,14 @@ export interface ResourceSample {
32
32
  * 使用场景:守护进程、QQ 桥、隧道、LLBot、附属进程。旧采样没有该字段;键缺省表示当时没采到。
33
33
  */
34
34
  partsMb?: Record<string, number>;
35
+ /**
36
+ * 整机 CPU 占用率(0–100)与各进程占用率,以整机全部逻辑核为 100%。
37
+ * 使用场景:CPU 曲线。旧采样没有这些字段;null 或键缺省表示当时没采到,不是 0。
38
+ */
39
+ hostCpuPercent?: number | null;
40
+ dbCpuPercent?: number | null;
41
+ bdsCpuPercent?: number | null;
42
+ partsCpuPercent?: Record<string, number>;
35
43
  }
36
44
  export declare class ResourceHistory {
37
45
  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;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"}
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;IACjC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1C;AAwCD,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,CA0BnC;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"}
@@ -36,15 +36,25 @@ function parseMetrics(data, now) {
36
36
  const memoryMb = (value) => value === null || typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 8388608;
37
37
  const partKey = (key) => /^[a-z0-9][a-z0-9_:-]{0,80}$/i.test(key);
38
38
  const memoryNumber = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 8388608;
39
- function sanitizeParts(value) {
39
+ const percentNumber = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100;
40
+ function sanitizeParts(value, valid = memoryNumber) {
40
41
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
41
42
  const parts = {};
42
43
  for (const [key, item] of Object.entries(value)) {
43
- if (!partKey(key) || !memoryNumber(item)) continue;
44
+ if (!partKey(key) || !valid(item)) continue;
44
45
  parts[key] = item;
45
46
  }
46
47
  return Object.keys(parts).length ? parts : void 0;
47
48
  }
49
+ function sanitizeCpu(sample) {
50
+ const partsCpuPercent = sanitizeParts(sample.partsCpuPercent, percentNumber);
51
+ return {
52
+ ...percentNumber(sample.hostCpuPercent) ? { hostCpuPercent: sample.hostCpuPercent } : {},
53
+ ...percentNumber(sample.dbCpuPercent) ? { dbCpuPercent: sample.dbCpuPercent } : {},
54
+ ...percentNumber(sample.bdsCpuPercent) ? { bdsCpuPercent: sample.bdsCpuPercent } : {},
55
+ ...partsCpuPercent ? { partsCpuPercent } : {}
56
+ };
57
+ }
48
58
  class ResourceHistory {
49
59
  constructor(query) {
50
60
  this.query = query;
@@ -63,7 +73,8 @@ class ResourceHistory {
63
73
  hostUsedMb: sample.hostUsedMb,
64
74
  dbMb: sample.dbMb,
65
75
  bdsMb: sample.bdsMb,
66
- ...partsMb ? { partsMb } : {}
76
+ ...partsMb ? { partsMb } : {},
77
+ ...sanitizeCpu(sample)
67
78
  };
68
79
  this.query("INSERT OR REPLACE INTO sfmc_resource_metrics (recorded_at, sample) VALUES (?, ?)", [
69
80
  sample.recordedAt,
@@ -174,7 +185,8 @@ function parseResourceRows(rows) {
174
185
  hostUsedMb: sample.hostUsedMb,
175
186
  dbMb: sample.dbMb,
176
187
  bdsMb: sample.bdsMb,
177
- ...partsMb ? { partsMb } : {}
188
+ ...partsMb ? { partsMb } : {},
189
+ ...sanitizeCpu(sample)
178
190
  }];
179
191
  } catch {
180
192
  return [];
@@ -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 /**\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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",
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 /**\n * \u6574\u673A CPU \u5360\u7528\u7387\uFF080\u2013100\uFF09\u4E0E\u5404\u8FDB\u7A0B\u5360\u7528\u7387\uFF0C\u4EE5\u6574\u673A\u5168\u90E8\u903B\u8F91\u6838\u4E3A 100%\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1ACPU \u66F2\u7EBF\u3002\u65E7\u91C7\u6837\u6CA1\u6709\u8FD9\u4E9B\u5B57\u6BB5\uFF1Bnull \u6216\u952E\u7F3A\u7701\u8868\u793A\u5F53\u65F6\u6CA1\u91C7\u5230\uFF0C\u4E0D\u662F 0\u3002\n */\n hostCpuPercent?: number | null;\n dbCpuPercent?: number | null;\n bdsCpuPercent?: number | null;\n partsCpuPercent?: 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/** CPU \u5360\u7528\u7387\u7684\u5408\u6CD5\u6570\u503C\uFF1A0\u2013100 \u7684\u6709\u9650\u6570\u3002 */\nconst percentNumber = (value: unknown): value is number =>\n typeof value === \"number\" && Number.isFinite(value) && value >= 0 && value <= 100;\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 * valid \u9ED8\u8BA4\u6309\u5185\u5B58\u6821\u9A8C\uFF1BCPU \u5206\u9879\u4F20\u5165 percentNumber\u3002\n */\nfunction sanitizeParts(value: unknown, valid: (item: unknown) => boolean = memoryNumber): 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) || !valid(item)) continue;\n parts[key] = item as number;\n }\n return Object.keys(parts).length ? parts : undefined;\n}\n\n/**\n * \u53D6\u51FA\u91C7\u6837\u91CC\u5408\u6CD5\u7684 CPU \u5B57\u6BB5\uFF1B\u4E0D\u5408\u6CD5\u6216\u7F3A\u7701\u7684\u9879\u76F4\u63A5\u7701\u7565\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1AResourceHistory.append \u843D\u5E93\u524D\u4E0E parseResourceRows \u8BFB\u5386\u53F2\u65F6\u5171\u7528\uFF0CCPU \u5B57\u6BB5\u574F\u4E86\u4E0D\u5F71\u54CD\u5185\u5B58\u90E8\u5206\u3002\n */\nfunction sanitizeCpu(sample: ResourceSample): Pick<ResourceSample, \"hostCpuPercent\" | \"dbCpuPercent\" | \"bdsCpuPercent\" | \"partsCpuPercent\"> {\n const partsCpuPercent = sanitizeParts(sample.partsCpuPercent, percentNumber);\n return {\n ...(percentNumber(sample.hostCpuPercent) ? { hostCpuPercent: sample.hostCpuPercent } : {}),\n ...(percentNumber(sample.dbCpuPercent) ? { dbCpuPercent: sample.dbCpuPercent } : {}),\n ...(percentNumber(sample.bdsCpuPercent) ? { bdsCpuPercent: sample.bdsCpuPercent } : {}),\n ...(partsCpuPercent ? { partsCpuPercent } : {}),\n };\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 ...sanitizeCpu(sample),\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 ...sanitizeCpu(sample),\n }];\n } catch {\n return [];\n }\n });\n}\n"],
5
+ "mappings": "AACA,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,mBAAiC;AAGnC,MAAM,oBAAoB;AAE1B,MAAM,qBAAqB;AAE3B,MAAM,wBAAwB;AAE9B,MAAM,uBAAuB,KAAK;AAEzC,MAAM,gBAAgB;AAAA,EACpB,SAAS;AAAA,EACT,UAAU;AACZ;AAMO,SAAS,iBAAiB,OAAwB;AACvD,MAAI,UAAU,OAAW,QAAO;AAChC,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,cAAc,KAAK,KAAK,SAAS,EAAG,OAAM,IAAI,MAAM,sCAAQ;AACrG,SAAO,KAAK,IAAI,OAAO,oBAAoB;AAC7C;AAWA,MAAM,aAAa,CAAC,uBAAuB,oBAAoB,mBAAmB;AAClF,MAAM,QAAQ,CAAC,UACb,UAAU,QAAS,OAAO,UAAU,YAAY,OAAO,cAAc,KAAK,KAAK,SAAS,KAAK,SAAS;AAEjG,SAAS,aAAa,MAA+B,KAAmC;AAC7F,MAAI,CAAC,MAAM,QAAQ,KAAK,OAAO,KAAK,KAAK,QAAQ,SAAS,IAAM,QAAO;AACvE,MACE,OAAO,KAAK,WAAW,YACvB,CAAC,uBAAuB,KAAK,KAAK,MAAM,KACxC,EACE,KAAK,QAAQ,QACZ,OAAO,KAAK,QAAQ,YAAY,OAAO,SAAS,KAAK,GAAG,KAAK,KAAK,OAAO,KAAK,KAAK,OAAO,OAE7F,CAAC,MAAM,KAAK,aAAa,KACzB,OAAO,KAAK,gBAAgB,YAC5B,KAAK,gBAAiB,KAAK,QAAsB,UACjD,CAAC,KAAK,YACN,OAAO,KAAK,aAAa,YACzB,MAAM,QAAQ,KAAK,QAAQ,KAC3B,EACE,KAAK,sBAAsB,QAC1B,OAAO,KAAK,sBAAsB,YACjC,OAAO,cAAc,KAAK,iBAAiB,KAC3C,KAAK,oBAAoB,KACzB,KAAK,qBAAqB,MAAM;AAGpC,WAAO;AACT,QAAM,MAAM,KAAK;AACjB,MAAI,OAAO,KAAK,GAAG,EAAE,KAAK,CAAC,QAAQ,CAAC,WAAW,SAAS,GAAG,CAAC,KAAK,WAAW,KAAK,CAAC,QAAQ,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC;AACxG,WAAO;AACT,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,QAAQ,KAAK;AAAA,IACb,KAAK,KAAK;AAAA,IACV,aAAa,KAAK;AAAA,IAClB,UAAU,EAAE,GAAG,IAAI;AAAA,IACnB,mBAAmB,KAAK;AAAA,IACxB,eAAe,KAAK;AAAA,EACtB;AACF;AAqBA,MAAM,WAAW,CAAC,UAChB,UAAU,QAAS,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,KAAK,SAAS,KAAK,SAAS;AAEnG,MAAM,UAAU,CAAC,QAAgB,+BAA+B,KAAK,GAAG;AACxE,MAAM,eAAe,CAAC,UACpB,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,KAAK,SAAS,KAAK,SAAS;AAEhF,MAAM,gBAAgB,CAAC,UACrB,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,KAAK,SAAS,KAAK,SAAS;AAOhF,SAAS,cAAc,OAAgB,QAAoC,cAAkD;AAC3H,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,EAAG,QAAO;AACxE,QAAM,QAAgC,CAAC;AACvC,aAAW,CAAC,KAAK,IAAI,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC/C,QAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,MAAM,IAAI,EAAG;AACnC,UAAM,GAAG,IAAI;AAAA,EACf;AACA,SAAO,OAAO,KAAK,KAAK,EAAE,SAAS,QAAQ;AAC7C;AAMA,SAAS,YAAY,QAAuH;AAC1I,QAAM,kBAAkB,cAAc,OAAO,iBAAiB,aAAa;AAC3E,SAAO;AAAA,IACL,GAAI,cAAc,OAAO,cAAc,IAAI,EAAE,gBAAgB,OAAO,eAAe,IAAI,CAAC;AAAA,IACxF,GAAI,cAAc,OAAO,YAAY,IAAI,EAAE,cAAc,OAAO,aAAa,IAAI,CAAC;AAAA,IAClF,GAAI,cAAc,OAAO,aAAa,IAAI,EAAE,eAAe,OAAO,cAAc,IAAI,CAAC;AAAA,IACrF,GAAI,kBAAkB,EAAE,gBAAgB,IAAI,CAAC;AAAA,EAC/C;AACF;AAEO,MAAM,gBAAgB;AAAA,EAG3B,YAAoB,OAAgB;AAAhB;AAClB,UAAM,0GAA0G;AAAA,EAClH;AAAA,EAFoB;AAAA,EAFZ,YAAY;AAAA,EACZ,YAAY;AAAA,EAIpB,OAAO,QAA8B;AACnC,QAAI,OAAO,aAAa,KAAK,YAAY,mBAAoB;AAC7D,UAAM,UAAU,cAAc,OAAO,OAAO;AAC5C,QAAI,OAAO,eAAe,QAAQ,OAAO,SAAS,QAAQ,OAAO,UAAU,QAAQ,CAAC,QAAS;AAC7F,QAAI,CAAC,SAAS,OAAO,UAAU,KAAK,CAAC,SAAS,OAAO,IAAI,KAAK,CAAC,SAAS,OAAO,KAAK,EAAG;AAEvF,aAAS;AAAA,MACP,YAAY,OAAO;AAAA,MACnB,YAAY,OAAO;AAAA,MACnB,MAAM,OAAO;AAAA,MACb,OAAO,OAAO;AAAA,MACd,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,MAC7B,GAAG,YAAY,MAAM;AAAA,IACvB;AACA,SAAK,MAAM,oFAAoF;AAAA,MAC7F,OAAO;AAAA,MACP,KAAK,UAAU,MAAM;AAAA,IACvB,CAAC;AACD,QAAI,OAAO,aAAa,KAAK,YAAY,KAAQ;AAC/C,WAAK,MAAM,2DAA2D,CAAC,OAAO,aAAa,oBAAoB,CAAC;AAChH,WAAK;AAAA,QACH;AAAA,MACF;AACA,WAAK,YAAY,OAAO;AAAA,IAC1B;AACA,SAAK,YAAY,OAAO;AAAA,EAC1B;AAAA,EACA,KAAK,MAAM,KAAK,IAAI,GAAG,SAAS,uBAAyC;AACvE,WAAO,kBAAkB,eAAe,KAAK,OAAO,YAAY,KAAK,MAAM,CAAC;AAAA,EAC9E;AACF;AAEO,MAAM,eAAe;AAAA,EAG1B,YAAoB,OAAgB;AAAhB;AAClB,UAAM,yGAAyG;AAAA,EACjH;AAAA,EAFoB;AAAA,EAFZ,YAAY;AAAA,EACZ,YAAY;AAAA,EAIpB,OAAO,QAA6B;AAElC,QAAI,OAAO,aAAa,KAAK,YAAY,kBAAmB;AAC5D,SAAK,MAAM,mFAAmF;AAAA,MAC5F,OAAO;AAAA,MACP,KAAK,UAAU,MAAM;AAAA,IACvB,CAAC;AACD,QAAI,OAAO,aAAa,KAAK,YAAY,KAAQ;AAC/C,WAAK,MAAM,OAAO,UAAU;AAC5B,WAAK,YAAY,OAAO;AAAA,IAC1B;AACA,SAAK,YAAY,OAAO;AAAA,EAC1B;AAAA,EACQ,MAAM,KAAmB;AAC/B,SAAK,MAAM,0DAA0D,CAAC,MAAM,oBAAoB,CAAC;AACjG,SAAK;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA,EACA,KAAK,MAAM,KAAK,IAAI,GAAG,SAAS,uBAAwC;AACtE,WAAO,iBAAiB,eAAe,KAAK,OAAO,WAAW,KAAK,MAAM,CAAC;AAAA,EAC5E;AACF;AAMO,SAAS,qBAAqB,UAAkB,KAAa,QAAwF;AAC1J,MAAI,CAAC,WAAW,QAAQ,EAAG,QAAO;AAClC,MAAI;AACJ,MAAI;AACF,SAAK,IAAI,aAAa,UAAU,EAAE,UAAU,KAAK,CAAC;AAClD,OAAG,KAAK,wBAAwB;AAChC,OAAG,KAAK,4BAA4B;AACpC,UAAM,QAAQ,YAAY,EAAE;AAC5B,WAAO;AAAA,MACL,SAAS,iBAAiB,eAAe,OAAO,WAAW,KAAK,MAAM,CAAC;AAAA,MACvE,iBAAiB,kBAAkB,eAAe,OAAO,YAAY,KAAK,MAAM,CAAC;AAAA,IACnF;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT,UAAE;AACA,QAAI;AAAE,UAAI,MAAM;AAAA,IAAG,QAAQ;AAAA,IAA8B;AAAA,EAC3D;AACF;AAMA,SAAS,eAAe,OAAgB,MAAkC,KAAa,QAAsC;AAC3H,QAAM,QAAQ,cAAc,IAAI;AAChC,QAAM,OAAO,iBAAiB,MAAM;AACpC,QAAM,aAAa,SAAS,YAAY,oBAAoB;AAC5D,QAAM,aAAa,MAAM,0CAA0C,KAAK,EAAE;AAC1E,MAAI,CAAC,MAAM,QAAQ,UAAU,EAAG,QAAO,CAAC;AACxC,QAAM,SAAU,WAAW,CAAC,GAA6C;AACzE,MAAI,OAAO,WAAW,SAAU,QAAO,CAAC;AACxC,QAAM,MAAM,KAAK,IAAI,QAAQ,GAAG;AAChC,QAAM,QAAQ,MAAM;AACpB,QAAM,aAAa,KAAK,KAAK,wBAAwB,UAAU;AAC/D,QAAM,OAAO,QAAQ,wBACjB;AAAA,IACE,uDAAuD,KAAK;AAAA,IAC5D,CAAC,OAAO,KAAK,KAAK,KAAK,OAAO,UAAU,CAAC;AAAA,EAC3C,IACA;AAAA;AAAA,IAEE,2LAA2L,KAAK;AAAA,IAChM,CAAC,KAAK,KAAK,OAAO,UAAU,GAAG,OAAO,KAAK,UAAU;AAAA,EACvD;AACJ,SAAO,MAAM,QAAQ,IAAI,IAAI,OAA+B,CAAC;AAC/D;AAEA,SAAS,iBAAiB,MAA6C;AACrE,SAAO,KAAK,QAAQ,CAAC,QAAQ;AAC3B,QAAI;AACF,aAAO,CAAC,KAAK,MAAM,IAAI,MAAM,CAAkB;AAAA,IACjD,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF,CAAC;AACH;AAEA,SAAS,kBAAkB,MAA8C;AACvE,SAAO,KAAK,QAAQ,CAAC,QAAQ;AAC3B,QAAI;AACF,YAAM,SAAS,KAAK,MAAM,IAAI,MAAM;AACpC,UAAI,CAAC,SAAS,OAAO,UAAU,KAAK,CAAC,SAAS,OAAO,IAAI,KAAK,CAAC,SAAS,OAAO,KAAK,EAAG,QAAO,CAAC;AAC/F,YAAM,UAAU,cAAc,OAAO,OAAO;AAC5C,aAAO,CAAC;AAAA,QACN,YAAY,OAAO;AAAA,QACnB,YAAY,OAAO;AAAA,QACnB,MAAM,OAAO;AAAA,QACb,OAAO,OAAO;AAAA,QACd,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,QAC7B,GAAG,YAAY,MAAM;AAAA,MACvB,CAAC;AAAA,IACH,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -3,6 +3,8 @@ export interface ProcessResources {
3
3
  running: boolean;
4
4
  memoryMb: number | null;
5
5
  cpuSeconds: number | null;
6
+ /** 最近一段时间的 CPU 占用率(整机为 100%)。由 status 路由的 ProcessCpuMeter 补上,采集函数本身不填。 */
7
+ cpuPercent?: number | null;
6
8
  }
7
9
  export declare function collectProcessResources(pid: number, running: boolean): Promise<ProcessResources>;
8
10
  /** 一次系统查询得到的单个进程用量。查不到的 PID 不会出现。 */
@@ -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;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"}
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;IAC1B,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;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"}
@@ -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\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;",
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 /** \u6700\u8FD1\u4E00\u6BB5\u65F6\u95F4\u7684 CPU \u5360\u7528\u7387\uFF08\u6574\u673A\u4E3A 100%\uFF09\u3002\u7531 status \u8DEF\u7531\u7684 ProcessCpuMeter \u8865\u4E0A\uFF0C\u91C7\u96C6\u51FD\u6570\u672C\u8EAB\u4E0D\u586B\u3002 */\n cpuPercent?: 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": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * domain/scoreboard.ts — 世界计分板快照的接收与读取
3
+ *
4
+ * 使用场景:
5
+ * - 游戏内平台脚本(SDK runtime/scoreboard-sync)每约 2 秒 POST /api/sfmc/scoreboard/live;
6
+ * - CLI 的 scoreboard.read 经 GET /api/sfmc/scoreboard 读取,供桌面端与小程序「计分板」页展示。
7
+ *
8
+ * 遍历全部计分项有开销,所以只在有人查看时同步:每次读取把「关注期」延长 WATCH_MS,
9
+ * 游戏内脚本从上报的响应里得知 watching 后才计算快照;无人查看时只发空闲探询。
10
+ * 快照内容不变时只上报哈希(kind: "same"),数据服务据此确认快照仍然新鲜;
11
+ * 数据服务重启丢失快照、或哈希对不上时,响应 needFull 让脚本下次补发完整快照。
12
+ * 快照只保存在内存中,不落库。
13
+ */
14
+ /** 参与者类型(与 @sfmc-bds/management 的 ScoreboardParticipantType 一致) */
15
+ export type ScoreboardParticipantType = "player" | "fakePlayer" | "entity";
16
+ /** 显示位(与 @sfmc-bds/management 的 ScoreboardDisplaySlot 一致) */
17
+ export type ScoreboardDisplaySlot = "sidebar" | "list" | "belowName";
18
+ /** 上报中的参与者(各计分项共用,分数按 ID 引用) */
19
+ export interface ScoreboardReportParticipant {
20
+ id: number;
21
+ name: string;
22
+ type: ScoreboardParticipantType;
23
+ online: boolean;
24
+ }
25
+ /** 上报中的计分项;scores 为 [参与者 ID, 分数] 对,避免在每个计分项里重复参与者信息 */
26
+ export interface ScoreboardReportObjective {
27
+ id: string;
28
+ displayName: string;
29
+ scores: Array<[number, number]>;
30
+ }
31
+ /** 上报中的显示位 */
32
+ export interface ScoreboardReportDisplay {
33
+ slot: ScoreboardDisplaySlot;
34
+ objectiveId: string;
35
+ sortOrder: "ascending" | "descending";
36
+ }
37
+ /** 完整快照 */
38
+ export interface ScoreboardSnapshot {
39
+ hash: string;
40
+ /** 游戏内生成快照的时间(毫秒时间戳) */
41
+ generatedAt: number;
42
+ participants: ScoreboardReportParticipant[];
43
+ objectives: ScoreboardReportObjective[];
44
+ displaySlots: ScoreboardReportDisplay[];
45
+ truncated: boolean;
46
+ }
47
+ /** 解析后的上报:空闲探询 / 内容未变 / 完整快照 */
48
+ export type ScoreboardReport = {
49
+ kind: "idle";
50
+ } | {
51
+ kind: "same";
52
+ hash: string;
53
+ generatedAt: number;
54
+ } | ({
55
+ kind: "full";
56
+ } & ScoreboardSnapshot);
57
+ /** 上报的响应:是否有人在看(决定脚本是否计算快照),以及是否需要补发完整快照 */
58
+ export interface ScoreboardReportReply {
59
+ watching: boolean;
60
+ needFull: boolean;
61
+ }
62
+ /** 与 @sfmc-bds/management 的 ScoreboardReadResult 同构;CLI 侧由类型检查保证两边不漂移 */
63
+ export interface ScoreboardReadView {
64
+ available: boolean;
65
+ fresh: boolean;
66
+ updatedAt: number | null;
67
+ objectives: Array<{
68
+ id: string;
69
+ displayName: string;
70
+ participants: Array<ScoreboardReportParticipant & {
71
+ score: number;
72
+ }>;
73
+ }>;
74
+ displaySlots: ScoreboardReportDisplay[];
75
+ truncated: boolean;
76
+ note?: string;
77
+ /**
78
+ * 游戏内脚本最近是否上报过(含空闲探询)。不属于管理协议,CLI 用来判断是否值得等待新快照:
79
+ * 脚本没在上报(BDS 未运行或平台脚本未加载)时直接返回,不空等。
80
+ */
81
+ scriptConnected: boolean;
82
+ }
83
+ /**
84
+ * 上报体积上限:超过即视为异常请求拒收。游戏内脚本有自己的更小上限(超出时截断并标记 truncated),
85
+ * 这里只防止异常请求占满内存。
86
+ */
87
+ export declare const SCOREBOARD_REPORT_LIMITS: {
88
+ readonly objectives: 2000;
89
+ readonly participants: 100000;
90
+ readonly scores: 500000;
91
+ readonly name: 512;
92
+ };
93
+ /** 一次读取后保持「有人在看」的时长 */
94
+ export declare const SCOREBOARD_WATCH_MS = 30000;
95
+ /** 快照在最后一次确认后多久内视为新鲜(脚本约 2 秒确认一次,留出几次失败的余量) */
96
+ export declare const SCOREBOARD_FRESH_MS = 10000;
97
+ /**
98
+ * 校验并解析一次上报;格式不对时返回 null。
99
+ * 使用场景:POST /api/sfmc/scoreboard/live 收到的请求体。
100
+ */
101
+ export declare function parseScoreboardReport(data: Record<string, unknown>, now?: number): ScoreboardReport | null;
102
+ /**
103
+ * 内存中的计分板快照与关注状态。
104
+ * 使用场景:routes/scoreboard.ts 每个数据服务进程持有一个实例。
105
+ */
106
+ export declare class ScoreboardStore {
107
+ private snapshot;
108
+ /** 脚本最后一次确认快照与世界一致的时间(游戏内时间戳) */
109
+ private verifiedAt;
110
+ /** 数据服务最后一次收到确认的时间(本机时间),用于判断新鲜度 */
111
+ private confirmedAt;
112
+ /** 在此之前视为有人在看 */
113
+ private watchUntil;
114
+ /** 最后一次收到任意上报的时间(本机时间);从未收到时为 null */
115
+ private reportedAt;
116
+ /** 处理一次上报,返回给脚本的指示 */
117
+ report(report: ScoreboardReport, now?: number): ScoreboardReportReply;
118
+ /** 读取快照并延长关注期,使脚本继续同步 */
119
+ read(now?: number): ScoreboardReadView;
120
+ private watching;
121
+ }
122
+ //# sourceMappingURL=scoreboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoreboard.d.ts","sourceRoot":"","sources":["../../src/domain/scoreboard.ts"],"names":[],"mappings":"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"}