@sfmc-bds/db-server 0.5.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/domain/activity-log.d.ts +129 -0
- package/dist/domain/activity-log.d.ts.map +1 -0
- package/dist/domain/activity-log.js +186 -0
- package/dist/domain/activity-log.js.map +7 -0
- package/dist/domain/bind-providers.d.ts +59 -0
- package/dist/domain/bind-providers.d.ts.map +1 -0
- package/dist/domain/bind-providers.js +33 -0
- package/dist/domain/bind-providers.js.map +7 -0
- package/dist/domain/metrics.d.ts +5 -0
- package/dist/domain/metrics.d.ts.map +1 -1
- package/dist/domain/metrics.js +28 -2
- package/dist/domain/metrics.js.map +2 -2
- package/dist/domain/player-level.d.ts +29 -0
- package/dist/domain/player-level.d.ts.map +1 -0
- package/dist/domain/player-level.js +30 -0
- package/dist/domain/player-level.js.map +7 -0
- package/dist/domain/process-resources.d.ts +12 -0
- package/dist/domain/process-resources.d.ts.map +1 -1
- package/dist/domain/process-resources.js +72 -1
- package/dist/domain/process-resources.js.map +2 -2
- package/dist/domain/qq-bind.d.ts +21 -0
- package/dist/domain/qq-bind.d.ts.map +1 -0
- package/dist/domain/qq-bind.js +54 -0
- package/dist/domain/qq-bind.js.map +7 -0
- package/dist/domain/schema.d.ts +1 -0
- package/dist/domain/schema.d.ts.map +1 -1
- package/dist/domain/schema.js +26 -0
- package/dist/domain/schema.js.map +2 -2
- package/dist/domain/tracked-processes.d.ts +30 -0
- package/dist/domain/tracked-processes.d.ts.map +1 -0
- package/dist/domain/tracked-processes.js +78 -0
- package/dist/domain/tracked-processes.js.map +7 -0
- package/dist/domain/wechat-bind.d.ts +56 -0
- package/dist/domain/wechat-bind.d.ts.map +1 -0
- package/dist/domain/wechat-bind.js +84 -0
- package/dist/domain/wechat-bind.js.map +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -7
- package/dist/index.js.map +2 -2
- package/dist/lib/sqlite.d.ts +10 -0
- package/dist/lib/sqlite.d.ts.map +1 -1
- package/dist/lib/sqlite.js +17 -2
- package/dist/lib/sqlite.js.map +2 -2
- package/dist/routes/bind-confirm.d.ts +30 -0
- package/dist/routes/bind-confirm.d.ts.map +1 -0
- package/dist/routes/bind-confirm.js +27 -0
- package/dist/routes/bind-confirm.js.map +7 -0
- package/dist/routes/qq-bind.d.ts +3 -1
- package/dist/routes/qq-bind.d.ts.map +1 -1
- package/dist/routes/qq-bind.js +8 -64
- package/dist/routes/qq-bind.js.map +2 -2
- package/dist/routes/status.d.ts.map +1 -1
- package/dist/routes/status.js +10 -2
- package/dist/routes/status.js.map +2 -2
- package/dist/routes/wechat-activity.d.ts +32 -0
- package/dist/routes/wechat-activity.d.ts.map +1 -0
- package/dist/routes/wechat-activity.js +40 -0
- package/dist/routes/wechat-activity.js.map +7 -0
- package/dist/routes/wechat-bind.d.ts +39 -0
- package/dist/routes/wechat-bind.d.ts.map +1 -0
- package/dist/routes/wechat-bind.js +56 -0
- package/dist/routes/wechat-bind.js.map +7 -0
- package/package.json +10 -4
|
@@ -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": "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;
|
|
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": "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",
|
|
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
|
+
}
|
package/dist/domain/schema.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/domain/schema.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/domain/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAuKjD"}
|
package/dist/domain/schema.js
CHANGED
|
@@ -160,6 +160,32 @@ function initSchema(db) {
|
|
|
160
160
|
`
|
|
161
161
|
CREATE INDEX IF NOT EXISTS idx_qq_admin_actions_status ON sfmc_qq_admin_actions(status, created_at)`
|
|
162
162
|
);
|
|
163
|
+
db.exec(
|
|
164
|
+
/* sql */
|
|
165
|
+
`
|
|
166
|
+
CREATE TABLE IF NOT EXISTS sfmc_wechat_bind_pending (
|
|
167
|
+
code TEXT PRIMARY KEY NOT NULL,
|
|
168
|
+
wechat_openid TEXT NOT NULL,
|
|
169
|
+
created_at INTEGER NOT NULL,
|
|
170
|
+
expires_at INTEGER NOT NULL
|
|
171
|
+
)`
|
|
172
|
+
);
|
|
173
|
+
db.exec(
|
|
174
|
+
/* sql */
|
|
175
|
+
`
|
|
176
|
+
CREATE TABLE IF NOT EXISTS sfmc_wechat_bindings (
|
|
177
|
+
wechat_openid TEXT PRIMARY KEY NOT NULL,
|
|
178
|
+
player_xuid TEXT NOT NULL UNIQUE,
|
|
179
|
+
player_name TEXT NOT NULL DEFAULT '',
|
|
180
|
+
bound_at INTEGER NOT NULL
|
|
181
|
+
)`
|
|
182
|
+
);
|
|
183
|
+
db.exec(
|
|
184
|
+
/* sql */
|
|
185
|
+
`
|
|
186
|
+
CREATE INDEX IF NOT EXISTS idx_wechat_bind_pending_openid ON sfmc_wechat_bind_pending(wechat_openid);
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_wechat_bindings_xuid ON sfmc_wechat_bindings(player_xuid)`
|
|
188
|
+
);
|
|
163
189
|
db.exec(
|
|
164
190
|
/* sql */
|
|
165
191
|
`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/domain/schema.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * domain/schema.ts \u2014 \u6570\u636E\u5E93 Schema \u521D\u59CB\u5316\u5B9A\u4E49\uFF08\u5E73\u53F0\u6838\u5FC3\u5E95\u5EA7\u8868\uFF09\n *\n * \u96C6\u4E2D\u7BA1\u7406\u5E73\u53F0\u6838\u5FC3\u6570\u636E\u8868\u7684 CREATE TABLE IF NOT EXISTS \u4E0E CREATE INDEX IF NOT EXISTS DDL \u8BED\u53E5\u3002\n * \u8C03\u7528\u65B9\u5F0F\uFF1AinitSchema(db) \u2014\u2014 \u5728 openDatabase \u4E4B\u540E\u3001\u4E1A\u52A1\u67E5\u8BE2\u542F\u52A8\u524D\u6267\u884C\u3002\n *\n * \u67B6\u6784\u7EA6\u5B9A\uFF1A\n * \u4E1A\u52A1\u6A21\u5757\uFF08activity-log, coop, land, chat, economy \u7B49\uFF09\u79C1\u6709\u6570\u636E\u8868\u5747\u7531\u5404\u81EA\u6A21\u5757\u5728 SAPI \u8FD0\u884C\u671F\u901A\u8FC7\n * db.defineTable(...) \u81EA\u4E3B\u58F0\u660E\u4E0E\u7EF4\u62A4\uFF0C\u4E0D\u5728\u6B64\u5904\u9759\u6001\u786C\u7F16\u7801\uFF0C\u907F\u514D\u5217\u5B9A\u4E49\u4E0E\u7D22\u5F15\u51B2\u7A81\u3002\n *\n * \u6B64\u5904\u4EC5\u5B9A\u4E49\u5E73\u53F0\u6838\u5FC3\u8FD0\u884C\u3001\u8DE8\u670D\u52A1\u5171\u4EAB\uFF08qq-bridge / HTTP \u7AEF\u70B9 / status \u63A2\u6D3B\uFF09\u7684\u57FA\u7840\u5E95\u5EA7\u8868\uFF1A\n * 1. \u4E16\u754C\u4E0E\u73A9\u5BB6\u8FD0\u884C\u671F\u72B6\u6001\u5FEB\u7167\uFF08sfmc_world, sfmc_players\uFF09\n * 2. \u8DE8\u8FDB\u7A0B\u804A\u5929\u6D88\u606F\u805A\u5408\uFF08sfmc_chat_messages\uFF09\n * 3. QQ \u8EAB\u4EFD\u7ED1\u5B9A\u4E0E\u5BA1\u6279\u961F\u5217\uFF08sfmc_qq_*\uFF09\n */\n\nimport type { DatabaseSync } from \"node:sqlite\";\n\n/**\n * \u521D\u59CB\u5316\u6570\u636E\u5E93\u7ED3\u6784\uFF0C\u521B\u5EFA\u5E73\u53F0\u6838\u5FC3\u5E95\u5EA7\u8868\u4E0E\u7D22\u5F15\u3002\n *\n * @param db SQLite \u6570\u636E\u5E93\u8FDE\u63A5\u5B9E\u4F8B\u3002\n */\nexport function initSchema(db: DatabaseSync): void {\n // (1) \u4E16\u754C\u8FD0\u884C\u671F\u72B6\u6001\uFF08\u4F9B status \u8DEF\u7531\u4E0E qq-bridge \u8BFB\u53D6\uFF0C\u5B57\u6BB5\u4E0E data-backup \u517C\u5BB9\uFF09\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_world (\n id TEXT PRIMARY KEY,\n seed TEXT DEFAULT '',\n difficulty TEXT DEFAULT '',\n spawn_x INTEGER DEFAULT 0,\n spawn_y INTEGER DEFAULT 0,\n spawn_z INTEGER DEFAULT 0,\n gamerules TEXT DEFAULT '{}',\n day INTEGER DEFAULT 0,\n absolute_time INTEGER DEFAULT 0,\n moon_phase INTEGER DEFAULT 0,\n updated_at INTEGER DEFAULT 0\n )`);\n\n // (2) \u73A9\u5BB6\u8FD0\u884C\u671F\u72B6\u6001\uFF08\u4F9B status \u8DEF\u7531\u4E0E qq-bridge \u8BFB\u53D6\uFF0C\u5B57\u6BB5\u4E0E data-backup \u517C\u5BB9\uFF09\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_players (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL DEFAULT '',\n xuid TEXT DEFAULT '',\n last_online INTEGER DEFAULT 0,\n level INTEGER DEFAULT 0,\n total_xp INTEGER DEFAULT 0,\n tags TEXT DEFAULT '',\n spawn_dimension TEXT DEFAULT '',\n spawn_x REAL,\n spawn_y REAL,\n spawn_z REAL,\n dimension TEXT DEFAULT '',\n x REAL,\n y REAL,\n z REAL,\n game_mode TEXT DEFAULT '',\n snapshot_hash TEXT DEFAULT '',\n active_channel TEXT DEFAULT '',\n subscribed_channels TEXT DEFAULT '[]',\n updated_at INTEGER DEFAULT 0\n )`);\n const playerColumns = new Map(\n (db.prepare(`PRAGMA table_info(\"sfmc_players\")`).all() as Array<{ name: string }>).map((column) => [\n column.name,\n true,\n ])\n );\n const missingPlayerColumns: Array<[string, string]> = [\n [\"spawn_dimension\", \"TEXT DEFAULT ''\"],\n [\"spawn_x\", \"REAL\"],\n [\"spawn_y\", \"REAL\"],\n [\"spawn_z\", \"REAL\"],\n [\"dimension\", \"TEXT DEFAULT ''\"],\n [\"x\", \"REAL\"],\n [\"y\", \"REAL\"],\n [\"z\", \"REAL\"],\n [\"game_mode\", \"TEXT DEFAULT ''\"],\n [\"snapshot_hash\", \"TEXT DEFAULT ''\"],\n [\"active_channel\", \"TEXT DEFAULT ''\"],\n [\"subscribed_channels\", \"TEXT DEFAULT '[]'\"],\n ];\n for (const [name, definition] of missingPlayerColumns) {\n if (!playerColumns.has(name)) db.exec(`ALTER TABLE sfmc_players ADD COLUMN \"${name}\" ${definition}`);\n }\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_players_name ON sfmc_players(name)`);\n\n // (3) \u804A\u5929\u6D88\u606F\u805A\u5408\uFF08\u4F9B routes/messages \u4E0E qq-bridge \u53CC\u5411\u540C\u6B65\uFF09\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_chat_messages (\n id TEXT PRIMARY KEY NOT NULL,\n channel_id TEXT NOT NULL DEFAULT '',\n from_id TEXT NOT NULL DEFAULT '',\n from_name TEXT NOT NULL DEFAULT '',\n type TEXT NOT NULL DEFAULT 'text',\n content TEXT NOT NULL DEFAULT '',\n attachment TEXT DEFAULT NULL,\n show_timestamp INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_chat_messages_channel_time ON sfmc_chat_messages(channel_id, created_at ASC);\n CREATE INDEX IF NOT EXISTS idx_chat_messages_created_at ON sfmc_chat_messages(created_at ASC)`);\n\n // (4) QQ\u2194MC \u8EAB\u4EFD\u7ED1\u5B9A\uFF08\u5E73\u53F0\u8868\uFF1Bqq-bridge / \u6E38\u620F\u6A21\u5757\u5171\u7528 HTTP\uFF0C\u975E\u6A21\u5757\u79C1\u6709\u8868\uFF09\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_bind_pending (\n code TEXT PRIMARY KEY NOT NULL,\n qq_user_openid TEXT NOT NULL,\n qq_backend TEXT NOT NULL DEFAULT 'official',\n created_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL\n )`);\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_bindings (\n qq_user_openid TEXT PRIMARY KEY NOT NULL,\n player_xuid TEXT NOT NULL UNIQUE,\n player_name TEXT NOT NULL DEFAULT '',\n qq_backend TEXT NOT NULL DEFAULT 'official',\n bound_at INTEGER NOT NULL\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_qq_bind_pending_openid ON sfmc_qq_bind_pending(qq_user_openid);\n CREATE INDEX IF NOT EXISTS idx_qq_bindings_xuid ON sfmc_qq_bindings(player_xuid)`);\n\n // (5) QQ \u5165\u670D\u5BA1\u6279\u3002\u65E7\u8E22\u4EBA\u961F\u5217\u8868\u4FDD\u7559\uFF0C\u907F\u514D\u5DF2\u6709\u5E93\u7F3A\u8868\uFF1B\u5E73\u53F0\u4E0D\u518D\u5199\u5165\u6216\u6D88\u8D39\u3002\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_join_requests (\n id TEXT PRIMARY KEY NOT NULL,\n applicant_openid TEXT NOT NULL,\n player_name TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n qq_backend TEXT NOT NULL DEFAULT 'official',\n created_at INTEGER NOT NULL,\n decided_at INTEGER,\n decided_by TEXT,\n applied_at INTEGER,\n apply_error TEXT\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_qq_join_status ON sfmc_qq_join_requests(status, created_at)`);\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_admin_actions (\n id TEXT PRIMARY KEY NOT NULL,\n kind TEXT NOT NULL,\n target_name TEXT NOT NULL,\n reason TEXT NOT NULL DEFAULT '',\n requested_by TEXT NOT NULL DEFAULT '',\n status TEXT NOT NULL DEFAULT 'pending',\n created_at INTEGER NOT NULL,\n applied_at INTEGER,\n apply_error TEXT\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_qq_admin_actions_status ON sfmc_qq_admin_actions(status, created_at)`);\n\n // QQ \u8D26\u53F7\u9875\u5FEB\u7167\uFF1Aqq-link \u8C03\u7528\u7ECF\u6D4E / \u5728\u7EBF\u65F6\u957F\u670D\u52A1\u540E\u5199\u5165\uFF0C\u4F9B\u300C\u6211\u7684\u8D26\u53F7\u300D\u8BFB\u6700\u65B0\u6570\u5B57\u3002\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_account_profile (\n player_xuid TEXT PRIMARY KEY NOT NULL,\n player_name TEXT NOT NULL DEFAULT '',\n balance INTEGER,\n today_seconds INTEGER,\n month_seconds INTEGER,\n total_seconds INTEGER,\n updated_at INTEGER NOT NULL\n )`);\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * domain/schema.ts \u2014 \u6570\u636E\u5E93 Schema \u521D\u59CB\u5316\u5B9A\u4E49\uFF08\u5E73\u53F0\u6838\u5FC3\u5E95\u5EA7\u8868\uFF09\n *\n * \u96C6\u4E2D\u7BA1\u7406\u5E73\u53F0\u6838\u5FC3\u6570\u636E\u8868\u7684 CREATE TABLE IF NOT EXISTS \u4E0E CREATE INDEX IF NOT EXISTS DDL \u8BED\u53E5\u3002\n * \u8C03\u7528\u65B9\u5F0F\uFF1AinitSchema(db) \u2014\u2014 \u5728 openDatabase \u4E4B\u540E\u3001\u4E1A\u52A1\u67E5\u8BE2\u542F\u52A8\u524D\u6267\u884C\u3002\n *\n * \u67B6\u6784\u7EA6\u5B9A\uFF1A\n * \u4E1A\u52A1\u6A21\u5757\uFF08activity-log, coop, land, chat, economy \u7B49\uFF09\u79C1\u6709\u6570\u636E\u8868\u5747\u7531\u5404\u81EA\u6A21\u5757\u5728 SAPI \u8FD0\u884C\u671F\u901A\u8FC7\n * db.defineTable(...) \u81EA\u4E3B\u58F0\u660E\u4E0E\u7EF4\u62A4\uFF0C\u4E0D\u5728\u6B64\u5904\u9759\u6001\u786C\u7F16\u7801\uFF0C\u907F\u514D\u5217\u5B9A\u4E49\u4E0E\u7D22\u5F15\u51B2\u7A81\u3002\n *\n * \u6B64\u5904\u4EC5\u5B9A\u4E49\u5E73\u53F0\u6838\u5FC3\u8FD0\u884C\u3001\u8DE8\u670D\u52A1\u5171\u4EAB\uFF08qq-bridge / HTTP \u7AEF\u70B9 / status \u63A2\u6D3B\uFF09\u7684\u57FA\u7840\u5E95\u5EA7\u8868\uFF1A\n * 1. \u4E16\u754C\u4E0E\u73A9\u5BB6\u8FD0\u884C\u671F\u72B6\u6001\u5FEB\u7167\uFF08sfmc_world, sfmc_players\uFF09\n * 2. \u8DE8\u8FDB\u7A0B\u804A\u5929\u6D88\u606F\u805A\u5408\uFF08sfmc_chat_messages\uFF09\n * 3. QQ \u8EAB\u4EFD\u7ED1\u5B9A\u4E0E\u5BA1\u6279\u961F\u5217\uFF08sfmc_qq_*\uFF09\n * 4. \u5FAE\u4FE1\u8EAB\u4EFD\u7ED1\u5B9A\uFF08sfmc_wechat_*\uFF0C\u5C0F\u7A0B\u5E8F\u73A9\u5BB6\u7AEF\u4F7F\u7528\uFF09\n */\n\nimport type { DatabaseSync } from \"node:sqlite\";\n\n/**\n * \u521D\u59CB\u5316\u6570\u636E\u5E93\u7ED3\u6784\uFF0C\u521B\u5EFA\u5E73\u53F0\u6838\u5FC3\u5E95\u5EA7\u8868\u4E0E\u7D22\u5F15\u3002\n *\n * @param db SQLite \u6570\u636E\u5E93\u8FDE\u63A5\u5B9E\u4F8B\u3002\n */\nexport function initSchema(db: DatabaseSync): void {\n // (1) \u4E16\u754C\u8FD0\u884C\u671F\u72B6\u6001\uFF08\u4F9B status \u8DEF\u7531\u4E0E qq-bridge \u8BFB\u53D6\uFF0C\u5B57\u6BB5\u4E0E data-backup \u517C\u5BB9\uFF09\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_world (\n id TEXT PRIMARY KEY,\n seed TEXT DEFAULT '',\n difficulty TEXT DEFAULT '',\n spawn_x INTEGER DEFAULT 0,\n spawn_y INTEGER DEFAULT 0,\n spawn_z INTEGER DEFAULT 0,\n gamerules TEXT DEFAULT '{}',\n day INTEGER DEFAULT 0,\n absolute_time INTEGER DEFAULT 0,\n moon_phase INTEGER DEFAULT 0,\n updated_at INTEGER DEFAULT 0\n )`);\n\n // (2) \u73A9\u5BB6\u8FD0\u884C\u671F\u72B6\u6001\uFF08\u4F9B status \u8DEF\u7531\u4E0E qq-bridge \u8BFB\u53D6\uFF0C\u5B57\u6BB5\u4E0E data-backup \u517C\u5BB9\uFF09\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_players (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL DEFAULT '',\n xuid TEXT DEFAULT '',\n last_online INTEGER DEFAULT 0,\n level INTEGER DEFAULT 0,\n total_xp INTEGER DEFAULT 0,\n tags TEXT DEFAULT '',\n spawn_dimension TEXT DEFAULT '',\n spawn_x REAL,\n spawn_y REAL,\n spawn_z REAL,\n dimension TEXT DEFAULT '',\n x REAL,\n y REAL,\n z REAL,\n game_mode TEXT DEFAULT '',\n snapshot_hash TEXT DEFAULT '',\n active_channel TEXT DEFAULT '',\n subscribed_channels TEXT DEFAULT '[]',\n updated_at INTEGER DEFAULT 0\n )`);\n const playerColumns = new Map(\n (db.prepare(`PRAGMA table_info(\"sfmc_players\")`).all() as Array<{ name: string }>).map((column) => [\n column.name,\n true,\n ])\n );\n const missingPlayerColumns: Array<[string, string]> = [\n [\"spawn_dimension\", \"TEXT DEFAULT ''\"],\n [\"spawn_x\", \"REAL\"],\n [\"spawn_y\", \"REAL\"],\n [\"spawn_z\", \"REAL\"],\n [\"dimension\", \"TEXT DEFAULT ''\"],\n [\"x\", \"REAL\"],\n [\"y\", \"REAL\"],\n [\"z\", \"REAL\"],\n [\"game_mode\", \"TEXT DEFAULT ''\"],\n [\"snapshot_hash\", \"TEXT DEFAULT ''\"],\n [\"active_channel\", \"TEXT DEFAULT ''\"],\n [\"subscribed_channels\", \"TEXT DEFAULT '[]'\"],\n ];\n for (const [name, definition] of missingPlayerColumns) {\n if (!playerColumns.has(name)) db.exec(`ALTER TABLE sfmc_players ADD COLUMN \"${name}\" ${definition}`);\n }\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_players_name ON sfmc_players(name)`);\n\n // (3) \u804A\u5929\u6D88\u606F\u805A\u5408\uFF08\u4F9B routes/messages \u4E0E qq-bridge \u53CC\u5411\u540C\u6B65\uFF09\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_chat_messages (\n id TEXT PRIMARY KEY NOT NULL,\n channel_id TEXT NOT NULL DEFAULT '',\n from_id TEXT NOT NULL DEFAULT '',\n from_name TEXT NOT NULL DEFAULT '',\n type TEXT NOT NULL DEFAULT 'text',\n content TEXT NOT NULL DEFAULT '',\n attachment TEXT DEFAULT NULL,\n show_timestamp INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_chat_messages_channel_time ON sfmc_chat_messages(channel_id, created_at ASC);\n CREATE INDEX IF NOT EXISTS idx_chat_messages_created_at ON sfmc_chat_messages(created_at ASC)`);\n\n // (4) QQ\u2194MC \u8EAB\u4EFD\u7ED1\u5B9A\uFF08\u5E73\u53F0\u8868\uFF1Bqq-bridge / \u6E38\u620F\u6A21\u5757\u5171\u7528 HTTP\uFF0C\u975E\u6A21\u5757\u79C1\u6709\u8868\uFF09\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_bind_pending (\n code TEXT PRIMARY KEY NOT NULL,\n qq_user_openid TEXT NOT NULL,\n qq_backend TEXT NOT NULL DEFAULT 'official',\n created_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL\n )`);\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_bindings (\n qq_user_openid TEXT PRIMARY KEY NOT NULL,\n player_xuid TEXT NOT NULL UNIQUE,\n player_name TEXT NOT NULL DEFAULT '',\n qq_backend TEXT NOT NULL DEFAULT 'official',\n bound_at INTEGER NOT NULL\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_qq_bind_pending_openid ON sfmc_qq_bind_pending(qq_user_openid);\n CREATE INDEX IF NOT EXISTS idx_qq_bindings_xuid ON sfmc_qq_bindings(player_xuid)`);\n\n // (5) QQ \u5165\u670D\u5BA1\u6279\u3002\u65E7\u8E22\u4EBA\u961F\u5217\u8868\u4FDD\u7559\uFF0C\u907F\u514D\u5DF2\u6709\u5E93\u7F3A\u8868\uFF1B\u5E73\u53F0\u4E0D\u518D\u5199\u5165\u6216\u6D88\u8D39\u3002\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_join_requests (\n id TEXT PRIMARY KEY NOT NULL,\n applicant_openid TEXT NOT NULL,\n player_name TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n qq_backend TEXT NOT NULL DEFAULT 'official',\n created_at INTEGER NOT NULL,\n decided_at INTEGER,\n decided_by TEXT,\n applied_at INTEGER,\n apply_error TEXT\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_qq_join_status ON sfmc_qq_join_requests(status, created_at)`);\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_admin_actions (\n id TEXT PRIMARY KEY NOT NULL,\n kind TEXT NOT NULL,\n target_name TEXT NOT NULL,\n reason TEXT NOT NULL DEFAULT '',\n requested_by TEXT NOT NULL DEFAULT '',\n status TEXT NOT NULL DEFAULT 'pending',\n created_at INTEGER NOT NULL,\n applied_at INTEGER,\n apply_error TEXT\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_qq_admin_actions_status ON sfmc_qq_admin_actions(status, created_at)`);\n\n // (6) \u5FAE\u4FE1\u2194MC \u8EAB\u4EFD\u7ED1\u5B9A\uFF08\u5C0F\u7A0B\u5E8F\u73A9\u5BB6\u7AEF\u7ECF mp-gateway \u7533\u8BF7\u77ED\u7801\uFF0C\u6E38\u620F\u5185 /c:bind \u786E\u8BA4\uFF09\u3002\n // \u4E0E QQ \u7ED1\u5B9A\u72EC\u7ACB\uFF1A\u540C\u4E00\u6E38\u620F\u8D26\u53F7\u53EF\u540C\u65F6\u7ED1\u5B9A QQ \u4E0E\u5FAE\u4FE1\uFF0CQQ \u8868\u4E0E\u6D41\u7A0B\u4E0D\u53D7\u5F71\u54CD\u3002\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_wechat_bind_pending (\n code TEXT PRIMARY KEY NOT NULL,\n wechat_openid TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL\n )`);\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_wechat_bindings (\n wechat_openid TEXT PRIMARY KEY NOT NULL,\n player_xuid TEXT NOT NULL UNIQUE,\n player_name TEXT NOT NULL DEFAULT '',\n bound_at INTEGER NOT NULL\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_wechat_bind_pending_openid ON sfmc_wechat_bind_pending(wechat_openid);\n CREATE INDEX IF NOT EXISTS idx_wechat_bindings_xuid ON sfmc_wechat_bindings(player_xuid)`);\n\n // QQ \u8D26\u53F7\u9875\u5FEB\u7167\uFF1Aqq-link \u8C03\u7528\u7ECF\u6D4E / \u5728\u7EBF\u65F6\u957F\u670D\u52A1\u540E\u5199\u5165\uFF0C\u4F9B\u300C\u6211\u7684\u8D26\u53F7\u300D\u8BFB\u6700\u65B0\u6570\u5B57\u3002\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_account_profile (\n player_xuid TEXT PRIMARY KEY NOT NULL,\n player_name TEXT NOT NULL DEFAULT '',\n balance INTEGER,\n today_seconds INTEGER,\n month_seconds INTEGER,\n total_seconds INTEGER,\n updated_at INTEGER NOT NULL\n )`);\n}\n"],
|
|
5
|
+
"mappings": "AAwBO,SAAS,WAAW,IAAwB;AAEjD,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAad;AAGJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBd;AACJ,QAAM,gBAAgB,IAAI;AAAA,IACvB,GAAG,QAAQ,mCAAmC,EAAE,IAAI,EAA8B,IAAI,CAAC,WAAW;AAAA,MACjG,OAAO;AAAA,MACP;AAAA,IACF,CAAC;AAAA,EACH;AACA,QAAM,uBAAgD;AAAA,IACpD,CAAC,mBAAmB,iBAAiB;AAAA,IACrC,CAAC,WAAW,MAAM;AAAA,IAClB,CAAC,WAAW,MAAM;AAAA,IAClB,CAAC,WAAW,MAAM;AAAA,IAClB,CAAC,aAAa,iBAAiB;AAAA,IAC/B,CAAC,KAAK,MAAM;AAAA,IACZ,CAAC,KAAK,MAAM;AAAA,IACZ,CAAC,KAAK,MAAM;AAAA,IACZ,CAAC,aAAa,iBAAiB;AAAA,IAC/B,CAAC,iBAAiB,iBAAiB;AAAA,IACnC,CAAC,kBAAkB,iBAAiB;AAAA,IACpC,CAAC,uBAAuB,mBAAmB;AAAA,EAC7C;AACA,aAAW,CAAC,MAAM,UAAU,KAAK,sBAAsB;AACrD,QAAI,CAAC,cAAc,IAAI,IAAI,EAAG,IAAG,KAAK,wCAAwC,IAAI,KAAK,UAAU,EAAE;AAAA,EACrG;AACA,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA,EACkD;AAGpE,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA,EAE8E;AAGhG,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA,EAEiE;AAGnF,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA,EAC2E;AAC7F,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA,EACoF;AAItG,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA,EAEyE;AAG3F,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASd;AACN;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tracked-processes.ts — 采集本实例已登记的其它 SFMC 进程内存。
|
|
3
|
+
*
|
|
4
|
+
* 使用场景:运行指标除 BDS、数据服务外,还要列出守护进程、QQ 桥、隧道、LLBot 和模块附属进程。
|
|
5
|
+
* 只读这些进程自己写下的运行态文件,不按进程名全机扫描,避免把同机另一个程序算进来。
|
|
6
|
+
* 界面名称与桌面端 metric-series.ts 的 sfmcPartLabel 一致。
|
|
7
|
+
*/
|
|
8
|
+
/** 一条已确认仍在运行的进程。内存采不到的不返回,避免把过期 PID 显示成「资源未知」。 */
|
|
9
|
+
export interface TrackedProcess {
|
|
10
|
+
/** daemon、qq、tunnel、llbot,或 sidecar:<模块 id> */
|
|
11
|
+
key: string;
|
|
12
|
+
/** 侧栏和曲线用的名称 */
|
|
13
|
+
label: string;
|
|
14
|
+
pid: number;
|
|
15
|
+
running: true;
|
|
16
|
+
memoryMb: number;
|
|
17
|
+
cpuSeconds: number | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 从运行态 JSON 里取出 pid。
|
|
21
|
+
* 使用场景:daemon.json、qq.runtime.json 等只借 pid 字段,其余内容(例如鉴权 token)不读取、不返回。
|
|
22
|
+
*/
|
|
23
|
+
export declare function readRecordedPid(text: string): number;
|
|
24
|
+
/**
|
|
25
|
+
* 采集仍在运行的已登记进程。
|
|
26
|
+
* @param root 实例根目录。
|
|
27
|
+
* @param skip 已经单独统计过的 PID(数据服务、本实例 BDS),这里不再重复计入。
|
|
28
|
+
*/
|
|
29
|
+
export declare function collectTrackedProcesses(root: string, skip: ReadonlySet<number>): Promise<TrackedProcess[]>;
|
|
30
|
+
//# sourceMappingURL=tracked-processes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracked-processes.d.ts","sourceRoot":"","sources":["../../src/domain/tracked-processes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,kDAAkD;AAClD,MAAM,WAAW,cAAc;IAC7B,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,IAAI,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAUD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOpD;AA6BD;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CA8BhH"}
|