@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
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* domain/activity-log.ts — 行为日志模块(activity-log)记录的只读查询与统计
|
|
3
|
+
*
|
|
4
|
+
* 使用场景:
|
|
5
|
+
* - CLI 管理通道的 activity.query / activity.stats(桌面端「行为日志」页、小程序管理端)
|
|
6
|
+
* 通过 readActivityPage / readActivityStats 只读打开业务库;
|
|
7
|
+
* - db-server 的微信玩家路由(routes/wechat-activity.ts)用当前连接调用 queryActivityPage,
|
|
8
|
+
* 只查绑定玩家自己的记录。
|
|
9
|
+
* 表结构由 sfmc-module-activity-log 在 init() 中 defineTable("activities") 声明,这里只读、不建表、不写入。
|
|
10
|
+
* 模块从未运行过(表不存在)时返回 installed: false,界面据此提示安装模块。
|
|
11
|
+
*/
|
|
12
|
+
import type { QueryFn } from "../lib/sqlite.js";
|
|
13
|
+
/** 行为日志模块声明的表名(模块 sapi/src/index.ts 的 TABLE) */
|
|
14
|
+
export declare const ACTIVITY_TABLE = "activities";
|
|
15
|
+
/** 单页条数上限;导出时由调用方分页拉取 */
|
|
16
|
+
export declare const ACTIVITY_PAGE_MAX = 500;
|
|
17
|
+
/** 未指定 limit 时的单页条数 */
|
|
18
|
+
export declare const ACTIVITY_PAGE_DEFAULT = 50;
|
|
19
|
+
/** 统计里活跃玩家排行的条数 */
|
|
20
|
+
export declare const ACTIVITY_TOP_ACTORS = 10;
|
|
21
|
+
/** 记录级别,与模块的 Level 一致 */
|
|
22
|
+
export type ActivityLevel = "info" | "warn" | "error";
|
|
23
|
+
/**
|
|
24
|
+
* 查询条件。所有字段可选,多个字段之间是“且”关系。
|
|
25
|
+
* eventType 精确匹配,优先于 eventTypePrefix(如 "block." 表示方块类全部事件)。
|
|
26
|
+
* keyword 在玩家名、目标与附加数据(payload_json,含聊天内容)中模糊匹配。
|
|
27
|
+
*/
|
|
28
|
+
export interface ActivityFilter {
|
|
29
|
+
eventType?: string;
|
|
30
|
+
eventTypePrefix?: string;
|
|
31
|
+
actorId?: string;
|
|
32
|
+
actorName?: string;
|
|
33
|
+
targetId?: string;
|
|
34
|
+
dimension?: string;
|
|
35
|
+
level?: ActivityLevel;
|
|
36
|
+
/** 起始时间(毫秒时间戳,含) */
|
|
37
|
+
from?: number;
|
|
38
|
+
/** 结束时间(毫秒时间戳,含) */
|
|
39
|
+
to?: number;
|
|
40
|
+
keyword?: string;
|
|
41
|
+
}
|
|
42
|
+
/** 分页参数 */
|
|
43
|
+
export interface ActivityPaging {
|
|
44
|
+
limit: number;
|
|
45
|
+
offset: number;
|
|
46
|
+
}
|
|
47
|
+
/** 一条行为记录(空字符串 / null 表示模块未记录该字段) */
|
|
48
|
+
export interface ActivityRecordRow {
|
|
49
|
+
id: string;
|
|
50
|
+
timestamp: number;
|
|
51
|
+
eventType: string;
|
|
52
|
+
actorId: string;
|
|
53
|
+
actorName: string;
|
|
54
|
+
targetId: string;
|
|
55
|
+
dimension: string;
|
|
56
|
+
x: number | null;
|
|
57
|
+
y: number | null;
|
|
58
|
+
z: number | null;
|
|
59
|
+
level: ActivityLevel;
|
|
60
|
+
/** 事件附加数据(聊天内容、伤害来源等);解析失败时为 null */
|
|
61
|
+
payload: Record<string, unknown> | null;
|
|
62
|
+
}
|
|
63
|
+
/** 一页查询结果;total 为满足条件的精确总数 */
|
|
64
|
+
export interface ActivityPageResult {
|
|
65
|
+
installed: boolean;
|
|
66
|
+
records: ActivityRecordRow[];
|
|
67
|
+
total: number;
|
|
68
|
+
limit: number;
|
|
69
|
+
offset: number;
|
|
70
|
+
}
|
|
71
|
+
/** 时间范围内的统计概览 */
|
|
72
|
+
export interface ActivityStatsResult {
|
|
73
|
+
installed: boolean;
|
|
74
|
+
/** 满足条件的记录总数 */
|
|
75
|
+
total: number;
|
|
76
|
+
/** 产生过记录的玩家数 */
|
|
77
|
+
actors: number;
|
|
78
|
+
/** 各事件类型的条数(降序) */
|
|
79
|
+
byType: {
|
|
80
|
+
eventType: string;
|
|
81
|
+
count: number;
|
|
82
|
+
}[];
|
|
83
|
+
/** 各级别的条数 */
|
|
84
|
+
byLevel: {
|
|
85
|
+
level: ActivityLevel;
|
|
86
|
+
count: number;
|
|
87
|
+
}[];
|
|
88
|
+
/** 记录最多的玩家(降序,最多 ACTIVITY_TOP_ACTORS 名) */
|
|
89
|
+
topActors: {
|
|
90
|
+
actorId: string;
|
|
91
|
+
actorName: string;
|
|
92
|
+
count: number;
|
|
93
|
+
}[];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 参数非法时抛出的错误。
|
|
97
|
+
* 使用场景:HTTP 路由据此回 400;CLI 管理通道按普通错误把 message 回给界面。
|
|
98
|
+
*/
|
|
99
|
+
export declare class ActivityQueryError extends Error {
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 把外部传入的参数(管理协议 params / HTTP 查询串)规整为查询条件。
|
|
103
|
+
* 使用场景:CLI activity.query / activity.stats 与微信玩家路由共用,校验规则只有这一处(DRY)。
|
|
104
|
+
*/
|
|
105
|
+
export declare function parseActivityFilter(raw: Record<string, unknown>): ActivityFilter;
|
|
106
|
+
/** 规整分页参数:limit 限制在 1…ACTIVITY_PAGE_MAX,offset 不小于 0 */
|
|
107
|
+
export declare function parseActivityPaging(raw: Record<string, unknown>): ActivityPaging;
|
|
108
|
+
/**
|
|
109
|
+
* 在已有连接上分页查询行为记录(按时间倒序)。
|
|
110
|
+
* 使用场景:db-server 进程内的路由;进程外请用 readActivityPage。
|
|
111
|
+
*/
|
|
112
|
+
export declare function queryActivityPage(query: QueryFn, filter: ActivityFilter, paging: ActivityPaging): ActivityPageResult;
|
|
113
|
+
/**
|
|
114
|
+
* 在已有连接上统计行为记录。
|
|
115
|
+
* 活跃玩家只统计“产生过 player.* 事件”的执行者:模块对爆炸等非玩家来源会把实体类型写进 actor_id,
|
|
116
|
+
* 而 player.* 事件只由真实玩家触发,以此区分,不依赖 ID 格式。
|
|
117
|
+
*/
|
|
118
|
+
export declare function queryActivityStats(query: QueryFn, filter: ActivityFilter): ActivityStatsResult;
|
|
119
|
+
/**
|
|
120
|
+
* 只读打开业务库文件并分页查询。
|
|
121
|
+
* 使用场景:CLI 管理通道 activity.query。数据库文件不存在时按“模块未安装”返回。
|
|
122
|
+
*/
|
|
123
|
+
export declare function readActivityPage(filePath: string, filter: ActivityFilter, paging: ActivityPaging): ActivityPageResult;
|
|
124
|
+
/**
|
|
125
|
+
* 只读打开业务库文件并统计。
|
|
126
|
+
* 使用场景:CLI 管理通道 activity.stats。
|
|
127
|
+
*/
|
|
128
|
+
export declare function readActivityStats(filePath: string, filter: ActivityFilter): ActivityStatsResult;
|
|
129
|
+
//# sourceMappingURL=activity-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activity-log.d.ts","sourceRoot":"","sources":["../../src/domain/activity-log.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { withReadonlyQuery } from "../lib/sqlite.js";
|
|
2
|
+
const ACTIVITY_TABLE = "activities";
|
|
3
|
+
const ACTIVITY_PAGE_MAX = 500;
|
|
4
|
+
const ACTIVITY_PAGE_DEFAULT = 50;
|
|
5
|
+
const ACTIVITY_TOP_ACTORS = 10;
|
|
6
|
+
const FILTER_TEXT_MAX = 128;
|
|
7
|
+
const LEVELS = ["info", "warn", "error"];
|
|
8
|
+
class ActivityQueryError extends Error {
|
|
9
|
+
}
|
|
10
|
+
function optionalText(value, label) {
|
|
11
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
12
|
+
if (typeof value !== "string" || value.length > FILTER_TEXT_MAX || /[\r\n]/.test(value)) {
|
|
13
|
+
throw new ActivityQueryError(`\u975E\u6CD5 ${label}`);
|
|
14
|
+
}
|
|
15
|
+
return value.trim() || void 0;
|
|
16
|
+
}
|
|
17
|
+
function optionalTime(value, label) {
|
|
18
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
19
|
+
const time = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
20
|
+
if (!Number.isFinite(time) || time < 0) throw new ActivityQueryError(`\u975E\u6CD5 ${label}`);
|
|
21
|
+
return Math.floor(time);
|
|
22
|
+
}
|
|
23
|
+
function parseActivityFilter(raw) {
|
|
24
|
+
const filter = {};
|
|
25
|
+
const eventType = optionalText(raw.eventType, "\u4E8B\u4EF6\u7C7B\u578B");
|
|
26
|
+
const eventTypePrefix = optionalText(raw.eventTypePrefix, "\u4E8B\u4EF6\u5206\u7C7B");
|
|
27
|
+
const actorId = optionalText(raw.actorId, "\u73A9\u5BB6 ID");
|
|
28
|
+
const actorName = optionalText(raw.actorName, "\u73A9\u5BB6\u540D");
|
|
29
|
+
const targetId = optionalText(raw.targetId, "\u76EE\u6807");
|
|
30
|
+
const dimension = optionalText(raw.dimension, "\u7EF4\u5EA6");
|
|
31
|
+
const keyword = optionalText(raw.keyword, "\u5173\u952E\u8BCD");
|
|
32
|
+
const levelText = optionalText(raw.level, "\u7EA7\u522B");
|
|
33
|
+
const from = optionalTime(raw.from, "\u5F00\u59CB\u65F6\u95F4");
|
|
34
|
+
const to = optionalTime(raw.to, "\u7ED3\u675F\u65F6\u95F4");
|
|
35
|
+
if (levelText !== void 0 && !LEVELS.includes(levelText)) throw new ActivityQueryError("\u975E\u6CD5 \u7EA7\u522B");
|
|
36
|
+
if (from !== void 0 && to !== void 0 && to < from) throw new ActivityQueryError("\u7ED3\u675F\u65F6\u95F4\u4E0D\u80FD\u65E9\u4E8E\u5F00\u59CB\u65F6\u95F4");
|
|
37
|
+
if (eventType) filter.eventType = eventType;
|
|
38
|
+
else if (eventTypePrefix) filter.eventTypePrefix = eventTypePrefix;
|
|
39
|
+
if (actorId) filter.actorId = actorId;
|
|
40
|
+
if (actorName) filter.actorName = actorName;
|
|
41
|
+
if (targetId) filter.targetId = targetId;
|
|
42
|
+
if (dimension) filter.dimension = dimension;
|
|
43
|
+
if (keyword) filter.keyword = keyword;
|
|
44
|
+
if (levelText) filter.level = levelText;
|
|
45
|
+
if (from !== void 0) filter.from = from;
|
|
46
|
+
if (to !== void 0) filter.to = to;
|
|
47
|
+
return filter;
|
|
48
|
+
}
|
|
49
|
+
function parseActivityPaging(raw) {
|
|
50
|
+
const limit = Number(raw.limit);
|
|
51
|
+
const offset = Number(raw.offset);
|
|
52
|
+
return {
|
|
53
|
+
limit: Number.isFinite(limit) && limit >= 1 ? Math.min(ACTIVITY_PAGE_MAX, Math.floor(limit)) : ACTIVITY_PAGE_DEFAULT,
|
|
54
|
+
offset: Number.isFinite(offset) && offset > 0 ? Math.floor(offset) : 0
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function escapeLike(text2) {
|
|
58
|
+
return text2.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
59
|
+
}
|
|
60
|
+
function tableColumns(query) {
|
|
61
|
+
const rows = query(`PRAGMA table_info("${ACTIVITY_TABLE}")`);
|
|
62
|
+
if (!Array.isArray(rows) || rows.length === 0) return null;
|
|
63
|
+
return new Set(rows.map((row) => String(row.name ?? "")));
|
|
64
|
+
}
|
|
65
|
+
function whereClause(filter, columns, alias = "") {
|
|
66
|
+
const col = (name) => `${alias}"${name}"`;
|
|
67
|
+
const parts = [];
|
|
68
|
+
const values = [];
|
|
69
|
+
if (columns.has("_deleted_at")) parts.push(`${col("_deleted_at")} IS NULL`);
|
|
70
|
+
if (filter.eventType) {
|
|
71
|
+
parts.push(`${col("event_type")} = ?`);
|
|
72
|
+
values.push(filter.eventType);
|
|
73
|
+
} else if (filter.eventTypePrefix) {
|
|
74
|
+
parts.push(`${col("event_type")} LIKE ? ESCAPE '\\'`);
|
|
75
|
+
values.push(`${escapeLike(filter.eventTypePrefix)}%`);
|
|
76
|
+
}
|
|
77
|
+
for (const [key, column] of [["actorId", "actor_id"], ["actorName", "actor_name"], ["targetId", "target_id"], ["dimension", "dimension"], ["level", "level"]]) {
|
|
78
|
+
const value = filter[key];
|
|
79
|
+
if (!value) continue;
|
|
80
|
+
parts.push(`${col(column)} = ?`);
|
|
81
|
+
values.push(value);
|
|
82
|
+
}
|
|
83
|
+
if (filter.from !== void 0) {
|
|
84
|
+
parts.push(`${col("timestamp")} >= ?`);
|
|
85
|
+
values.push(filter.from);
|
|
86
|
+
}
|
|
87
|
+
if (filter.to !== void 0) {
|
|
88
|
+
parts.push(`${col("timestamp")} <= ?`);
|
|
89
|
+
values.push(filter.to);
|
|
90
|
+
}
|
|
91
|
+
if (filter.keyword) {
|
|
92
|
+
const pattern = `%${escapeLike(filter.keyword)}%`;
|
|
93
|
+
parts.push(`(${["actor_name", "target_id", "payload_json"].map((name) => `${col(name)} LIKE ? ESCAPE '\\'`).join(" OR ")})`);
|
|
94
|
+
values.push(pattern, pattern, pattern);
|
|
95
|
+
}
|
|
96
|
+
return { sql: parts.length ? `WHERE ${parts.join(" AND ")}` : "", values };
|
|
97
|
+
}
|
|
98
|
+
const text = (value) => typeof value === "string" ? value : value == null ? "" : String(value);
|
|
99
|
+
const coordinate = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
100
|
+
function toRecord(row) {
|
|
101
|
+
let payload = null;
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(text(row.payload_json) || "{}");
|
|
104
|
+
payload = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
105
|
+
} catch {
|
|
106
|
+
payload = null;
|
|
107
|
+
}
|
|
108
|
+
const level = text(row.level);
|
|
109
|
+
return {
|
|
110
|
+
id: text(row.id),
|
|
111
|
+
timestamp: Number(row.timestamp) || 0,
|
|
112
|
+
eventType: text(row.event_type),
|
|
113
|
+
actorId: text(row.actor_id),
|
|
114
|
+
actorName: text(row.actor_name),
|
|
115
|
+
targetId: text(row.target_id),
|
|
116
|
+
dimension: text(row.dimension),
|
|
117
|
+
x: coordinate(row.x),
|
|
118
|
+
y: coordinate(row.y),
|
|
119
|
+
z: coordinate(row.z),
|
|
120
|
+
level: LEVELS.includes(level) ? level : "info",
|
|
121
|
+
payload
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const count = (value) => Number(value) || 0;
|
|
125
|
+
function queryActivityPage(query, filter, paging) {
|
|
126
|
+
const columns = tableColumns(query);
|
|
127
|
+
if (!columns) return { installed: false, records: [], total: 0, ...paging };
|
|
128
|
+
const where = whereClause(filter, columns);
|
|
129
|
+
const totalRow = query(`SELECT COUNT(*) AS c FROM "${ACTIVITY_TABLE}" ${where.sql}`, where.values)[0];
|
|
130
|
+
const rows = query(
|
|
131
|
+
`SELECT id, timestamp, event_type, actor_id, actor_name, target_id, dimension, x, y, z, level, payload_json
|
|
132
|
+
FROM "${ACTIVITY_TABLE}" ${where.sql}
|
|
133
|
+
ORDER BY timestamp DESC, id DESC
|
|
134
|
+
LIMIT ? OFFSET ?`,
|
|
135
|
+
[...where.values, paging.limit, paging.offset]
|
|
136
|
+
);
|
|
137
|
+
return { installed: true, records: rows.map(toRecord), total: count(totalRow?.c), ...paging };
|
|
138
|
+
}
|
|
139
|
+
function queryActivityStats(query, filter) {
|
|
140
|
+
const columns = tableColumns(query);
|
|
141
|
+
if (!columns) return { installed: false, total: 0, actors: 0, byType: [], byLevel: [], topActors: [] };
|
|
142
|
+
const where = whereClause(filter, columns, "a.");
|
|
143
|
+
const from = `FROM "${ACTIVITY_TABLE}" a ${where.sql}`;
|
|
144
|
+
const playerActor = `a.actor_id <> '' AND EXISTS (SELECT 1 FROM "${ACTIVITY_TABLE}" p WHERE p.actor_id = a.actor_id AND p.event_type LIKE 'player.%')`;
|
|
145
|
+
const and = where.sql ? " AND " : " WHERE ";
|
|
146
|
+
const byType = query(`SELECT a.event_type AS k, COUNT(*) AS c ${from} GROUP BY a.event_type ORDER BY c DESC`, where.values).map((row) => ({ eventType: text(row.k), count: count(row.c) }));
|
|
147
|
+
const byLevel = query(`SELECT a.level AS k, COUNT(*) AS c ${from} GROUP BY a.level`, where.values).filter((row) => LEVELS.includes(text(row.k))).map((row) => ({ level: text(row.k), count: count(row.c) }));
|
|
148
|
+
const actorsRow = query(`SELECT COUNT(DISTINCT a.actor_id) AS c ${from}${and}${playerActor}`, where.values)[0];
|
|
149
|
+
const topActors = query(
|
|
150
|
+
`SELECT a.actor_id AS id, MAX(a.actor_name) AS name, COUNT(*) AS c ${from}${and}${playerActor}
|
|
151
|
+
GROUP BY a.actor_id ORDER BY c DESC LIMIT ?`,
|
|
152
|
+
[...where.values, ACTIVITY_TOP_ACTORS]
|
|
153
|
+
).map((row) => ({ actorId: text(row.id), actorName: text(row.name), count: count(row.c) }));
|
|
154
|
+
return {
|
|
155
|
+
installed: true,
|
|
156
|
+
total: byType.reduce((sum, row) => sum + row.count, 0),
|
|
157
|
+
actors: count(actorsRow?.c),
|
|
158
|
+
byType,
|
|
159
|
+
byLevel,
|
|
160
|
+
topActors
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function readActivityPage(filePath, filter, paging) {
|
|
164
|
+
return withReadonlyQuery(filePath, () => ({ installed: false, records: [], total: 0, ...paging }), (query) => queryActivityPage(query, filter, paging));
|
|
165
|
+
}
|
|
166
|
+
function readActivityStats(filePath, filter) {
|
|
167
|
+
return withReadonlyQuery(
|
|
168
|
+
filePath,
|
|
169
|
+
() => ({ installed: false, total: 0, actors: 0, byType: [], byLevel: [], topActors: [] }),
|
|
170
|
+
(query) => queryActivityStats(query, filter)
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
export {
|
|
174
|
+
ACTIVITY_PAGE_DEFAULT,
|
|
175
|
+
ACTIVITY_PAGE_MAX,
|
|
176
|
+
ACTIVITY_TABLE,
|
|
177
|
+
ACTIVITY_TOP_ACTORS,
|
|
178
|
+
ActivityQueryError,
|
|
179
|
+
parseActivityFilter,
|
|
180
|
+
parseActivityPaging,
|
|
181
|
+
queryActivityPage,
|
|
182
|
+
queryActivityStats,
|
|
183
|
+
readActivityPage,
|
|
184
|
+
readActivityStats
|
|
185
|
+
};
|
|
186
|
+
//# sourceMappingURL=activity-log.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/domain/activity-log.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * domain/activity-log.ts \u2014 \u884C\u4E3A\u65E5\u5FD7\u6A21\u5757\uFF08activity-log\uFF09\u8BB0\u5F55\u7684\u53EA\u8BFB\u67E5\u8BE2\u4E0E\u7EDF\u8BA1\n *\n * \u4F7F\u7528\u573A\u666F\uFF1A\n * - CLI \u7BA1\u7406\u901A\u9053\u7684 activity.query / activity.stats\uFF08\u684C\u9762\u7AEF\u300C\u884C\u4E3A\u65E5\u5FD7\u300D\u9875\u3001\u5C0F\u7A0B\u5E8F\u7BA1\u7406\u7AEF\uFF09\n * \u901A\u8FC7 readActivityPage / readActivityStats \u53EA\u8BFB\u6253\u5F00\u4E1A\u52A1\u5E93\uFF1B\n * - db-server \u7684\u5FAE\u4FE1\u73A9\u5BB6\u8DEF\u7531\uFF08routes/wechat-activity.ts\uFF09\u7528\u5F53\u524D\u8FDE\u63A5\u8C03\u7528 queryActivityPage\uFF0C\n * \u53EA\u67E5\u7ED1\u5B9A\u73A9\u5BB6\u81EA\u5DF1\u7684\u8BB0\u5F55\u3002\n * \u8868\u7ED3\u6784\u7531 sfmc-module-activity-log \u5728 init() \u4E2D defineTable(\"activities\") \u58F0\u660E\uFF0C\u8FD9\u91CC\u53EA\u8BFB\u3001\u4E0D\u5EFA\u8868\u3001\u4E0D\u5199\u5165\u3002\n * \u6A21\u5757\u4ECE\u672A\u8FD0\u884C\u8FC7\uFF08\u8868\u4E0D\u5B58\u5728\uFF09\u65F6\u8FD4\u56DE installed: false\uFF0C\u754C\u9762\u636E\u6B64\u63D0\u793A\u5B89\u88C5\u6A21\u5757\u3002\n */\n\nimport type { QueryFn } from \"../lib/sqlite.js\";\nimport { withReadonlyQuery } from \"../lib/sqlite.js\";\n\n/** \u884C\u4E3A\u65E5\u5FD7\u6A21\u5757\u58F0\u660E\u7684\u8868\u540D\uFF08\u6A21\u5757 sapi/src/index.ts \u7684 TABLE\uFF09 */\nexport const ACTIVITY_TABLE = \"activities\";\n/** \u5355\u9875\u6761\u6570\u4E0A\u9650\uFF1B\u5BFC\u51FA\u65F6\u7531\u8C03\u7528\u65B9\u5206\u9875\u62C9\u53D6 */\nexport const ACTIVITY_PAGE_MAX = 500;\n/** \u672A\u6307\u5B9A limit \u65F6\u7684\u5355\u9875\u6761\u6570 */\nexport const ACTIVITY_PAGE_DEFAULT = 50;\n/** \u7EDF\u8BA1\u91CC\u6D3B\u8DC3\u73A9\u5BB6\u6392\u884C\u7684\u6761\u6570 */\nexport const ACTIVITY_TOP_ACTORS = 10;\n/** \u6587\u672C\u7B5B\u9009\u53C2\u6570\u7684\u957F\u5EA6\u4E0A\u9650\uFF0C\u907F\u514D\u8D85\u957F LIKE \u6A21\u5F0F */\nconst FILTER_TEXT_MAX = 128;\n\n/** \u8BB0\u5F55\u7EA7\u522B\uFF0C\u4E0E\u6A21\u5757\u7684 Level \u4E00\u81F4 */\nexport type ActivityLevel = \"info\" | \"warn\" | \"error\";\nconst LEVELS: readonly ActivityLevel[] = [\"info\", \"warn\", \"error\"];\n\n/**\n * \u67E5\u8BE2\u6761\u4EF6\u3002\u6240\u6709\u5B57\u6BB5\u53EF\u9009\uFF0C\u591A\u4E2A\u5B57\u6BB5\u4E4B\u95F4\u662F\u201C\u4E14\u201D\u5173\u7CFB\u3002\n * eventType \u7CBE\u786E\u5339\u914D\uFF0C\u4F18\u5148\u4E8E eventTypePrefix\uFF08\u5982 \"block.\" \u8868\u793A\u65B9\u5757\u7C7B\u5168\u90E8\u4E8B\u4EF6\uFF09\u3002\n * keyword \u5728\u73A9\u5BB6\u540D\u3001\u76EE\u6807\u4E0E\u9644\u52A0\u6570\u636E\uFF08payload_json\uFF0C\u542B\u804A\u5929\u5185\u5BB9\uFF09\u4E2D\u6A21\u7CCA\u5339\u914D\u3002\n */\nexport interface ActivityFilter {\n eventType?: string;\n eventTypePrefix?: string;\n actorId?: string;\n actorName?: string;\n targetId?: string;\n dimension?: string;\n level?: ActivityLevel;\n /** \u8D77\u59CB\u65F6\u95F4\uFF08\u6BEB\u79D2\u65F6\u95F4\u6233\uFF0C\u542B\uFF09 */\n from?: number;\n /** \u7ED3\u675F\u65F6\u95F4\uFF08\u6BEB\u79D2\u65F6\u95F4\u6233\uFF0C\u542B\uFF09 */\n to?: number;\n keyword?: string;\n}\n\n/** \u5206\u9875\u53C2\u6570 */\nexport interface ActivityPaging {\n limit: number;\n offset: number;\n}\n\n/** \u4E00\u6761\u884C\u4E3A\u8BB0\u5F55\uFF08\u7A7A\u5B57\u7B26\u4E32 / null \u8868\u793A\u6A21\u5757\u672A\u8BB0\u5F55\u8BE5\u5B57\u6BB5\uFF09 */\nexport interface ActivityRecordRow {\n id: string;\n timestamp: number;\n eventType: string;\n actorId: string;\n actorName: string;\n targetId: string;\n dimension: string;\n x: number | null;\n y: number | null;\n z: number | null;\n level: ActivityLevel;\n /** \u4E8B\u4EF6\u9644\u52A0\u6570\u636E\uFF08\u804A\u5929\u5185\u5BB9\u3001\u4F24\u5BB3\u6765\u6E90\u7B49\uFF09\uFF1B\u89E3\u6790\u5931\u8D25\u65F6\u4E3A null */\n payload: Record<string, unknown> | null;\n}\n\n/** \u4E00\u9875\u67E5\u8BE2\u7ED3\u679C\uFF1Btotal \u4E3A\u6EE1\u8DB3\u6761\u4EF6\u7684\u7CBE\u786E\u603B\u6570 */\nexport interface ActivityPageResult {\n installed: boolean;\n records: ActivityRecordRow[];\n total: number;\n limit: number;\n offset: number;\n}\n\n/** \u65F6\u95F4\u8303\u56F4\u5185\u7684\u7EDF\u8BA1\u6982\u89C8 */\nexport interface ActivityStatsResult {\n installed: boolean;\n /** \u6EE1\u8DB3\u6761\u4EF6\u7684\u8BB0\u5F55\u603B\u6570 */\n total: number;\n /** \u4EA7\u751F\u8FC7\u8BB0\u5F55\u7684\u73A9\u5BB6\u6570 */\n actors: number;\n /** \u5404\u4E8B\u4EF6\u7C7B\u578B\u7684\u6761\u6570\uFF08\u964D\u5E8F\uFF09 */\n byType: { eventType: string; count: number }[];\n /** \u5404\u7EA7\u522B\u7684\u6761\u6570 */\n byLevel: { level: ActivityLevel; count: number }[];\n /** \u8BB0\u5F55\u6700\u591A\u7684\u73A9\u5BB6\uFF08\u964D\u5E8F\uFF0C\u6700\u591A ACTIVITY_TOP_ACTORS \u540D\uFF09 */\n topActors: { actorId: string; actorName: string; count: number }[];\n}\n\n/**\n * \u53C2\u6570\u975E\u6CD5\u65F6\u629B\u51FA\u7684\u9519\u8BEF\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1AHTTP \u8DEF\u7531\u636E\u6B64\u56DE 400\uFF1BCLI \u7BA1\u7406\u901A\u9053\u6309\u666E\u901A\u9519\u8BEF\u628A message \u56DE\u7ED9\u754C\u9762\u3002\n */\nexport class ActivityQueryError extends Error {}\n\n/** \u53EF\u9009\u6587\u672C\u53C2\u6570\uFF1A\u7F3A\u7701 / \u7A7A\u4E32\u8FD4\u56DE undefined\uFF0C\u975E\u5B57\u7B26\u4E32\u3001\u8D85\u957F\u6216\u542B\u6362\u884C\u65F6\u62A5\u9519 */\nfunction optionalText(value: unknown, label: string): string | undefined {\n if (value === undefined || value === null || value === \"\") return undefined;\n if (typeof value !== \"string\" || value.length > FILTER_TEXT_MAX || /[\\r\\n]/.test(value)) {\n throw new ActivityQueryError(`\u975E\u6CD5 ${label}`);\n }\n return value.trim() || undefined;\n}\n\n/** \u53EF\u9009\u6BEB\u79D2\u65F6\u95F4\u6233\uFF1A\u63A5\u53D7\u6570\u5B57\u6216\u6570\u5B57\u5B57\u7B26\u4E32\uFF08HTTP \u67E5\u8BE2\u4E32\uFF09 */\nfunction optionalTime(value: unknown, label: string): number | undefined {\n if (value === undefined || value === null || value === \"\") return undefined;\n const time = typeof value === \"number\" ? value : typeof value === \"string\" ? Number(value) : Number.NaN;\n if (!Number.isFinite(time) || time < 0) throw new ActivityQueryError(`\u975E\u6CD5 ${label}`);\n return Math.floor(time);\n}\n\n/**\n * \u628A\u5916\u90E8\u4F20\u5165\u7684\u53C2\u6570\uFF08\u7BA1\u7406\u534F\u8BAE params / HTTP \u67E5\u8BE2\u4E32\uFF09\u89C4\u6574\u4E3A\u67E5\u8BE2\u6761\u4EF6\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1ACLI activity.query / activity.stats \u4E0E\u5FAE\u4FE1\u73A9\u5BB6\u8DEF\u7531\u5171\u7528\uFF0C\u6821\u9A8C\u89C4\u5219\u53EA\u6709\u8FD9\u4E00\u5904\uFF08DRY\uFF09\u3002\n */\nexport function parseActivityFilter(raw: Record<string, unknown>): ActivityFilter {\n const filter: ActivityFilter = {};\n const eventType = optionalText(raw.eventType, \"\u4E8B\u4EF6\u7C7B\u578B\");\n const eventTypePrefix = optionalText(raw.eventTypePrefix, \"\u4E8B\u4EF6\u5206\u7C7B\");\n const actorId = optionalText(raw.actorId, \"\u73A9\u5BB6 ID\");\n const actorName = optionalText(raw.actorName, \"\u73A9\u5BB6\u540D\");\n const targetId = optionalText(raw.targetId, \"\u76EE\u6807\");\n const dimension = optionalText(raw.dimension, \"\u7EF4\u5EA6\");\n const keyword = optionalText(raw.keyword, \"\u5173\u952E\u8BCD\");\n const levelText = optionalText(raw.level, \"\u7EA7\u522B\");\n const from = optionalTime(raw.from, \"\u5F00\u59CB\u65F6\u95F4\");\n const to = optionalTime(raw.to, \"\u7ED3\u675F\u65F6\u95F4\");\n if (levelText !== undefined && !LEVELS.includes(levelText as ActivityLevel)) throw new ActivityQueryError(\"\u975E\u6CD5 \u7EA7\u522B\");\n if (from !== undefined && to !== undefined && to < from) throw new ActivityQueryError(\"\u7ED3\u675F\u65F6\u95F4\u4E0D\u80FD\u65E9\u4E8E\u5F00\u59CB\u65F6\u95F4\");\n if (eventType) filter.eventType = eventType;\n else if (eventTypePrefix) filter.eventTypePrefix = eventTypePrefix;\n if (actorId) filter.actorId = actorId;\n if (actorName) filter.actorName = actorName;\n if (targetId) filter.targetId = targetId;\n if (dimension) filter.dimension = dimension;\n if (keyword) filter.keyword = keyword;\n if (levelText) filter.level = levelText as ActivityLevel;\n if (from !== undefined) filter.from = from;\n if (to !== undefined) filter.to = to;\n return filter;\n}\n\n/** \u89C4\u6574\u5206\u9875\u53C2\u6570\uFF1Alimit \u9650\u5236\u5728 1\u2026ACTIVITY_PAGE_MAX\uFF0Coffset \u4E0D\u5C0F\u4E8E 0 */\nexport function parseActivityPaging(raw: Record<string, unknown>): ActivityPaging {\n const limit = Number(raw.limit);\n const offset = Number(raw.offset);\n return {\n limit: Number.isFinite(limit) && limit >= 1 ? Math.min(ACTIVITY_PAGE_MAX, Math.floor(limit)) : ACTIVITY_PAGE_DEFAULT,\n offset: Number.isFinite(offset) && offset > 0 ? Math.floor(offset) : 0,\n };\n}\n\n/** LIKE \u6A21\u5F0F\u8F6C\u4E49\uFF1A% _ \\ \u6309\u5B57\u9762\u5339\u914D\uFF08\u914D\u5408 ESCAPE '\\'\uFF09 */\nfunction escapeLike(text: string): string {\n return text.replace(/[\\\\%_]/g, (char) => `\\\\${char}`);\n}\n\n/** \u8BFB\u53D6\u8868\u7684\u5217\u540D\uFF1B\u8868\u4E0D\u5B58\u5728\u65F6\u8FD4\u56DE null\uFF08\u6A21\u5757\u5C1A\u672A\u5B89\u88C5\u6216\u4ECE\u672A\u542F\u52A8\uFF09 */\nfunction tableColumns(query: QueryFn): Set<string> | null {\n const rows = query(`PRAGMA table_info(\"${ACTIVITY_TABLE}\")`) as Array<{ name?: unknown }>;\n if (!Array.isArray(rows) || rows.length === 0) return null;\n return new Set(rows.map((row) => String(row.name ?? \"\")));\n}\n\n/**\n * \u6309\u6761\u4EF6\u62FC\u51FA WHERE \u5B50\u53E5\uFF08\u53C2\u6570\u5316\uFF0C\u4E0D\u62FC\u63A5\u7528\u6237\u8F93\u5165\uFF09\u3002\n * \u6A21\u5757\u8868\u9ED8\u8BA4\u542F\u7528\u8F6F\u5220\u9664\uFF08_deleted_at \u5217\uFF09\uFF0C\u5B58\u5728\u8BE5\u5217\u65F6\u6392\u9664\u5DF2\u5220\u9664\u884C\u3002\n */\nfunction whereClause(filter: ActivityFilter, columns: Set<string>, alias = \"\"): { sql: string; values: unknown[] } {\n const col = (name: string) => `${alias}\"${name}\"`;\n const parts: string[] = [];\n const values: unknown[] = [];\n if (columns.has(\"_deleted_at\")) parts.push(`${col(\"_deleted_at\")} IS NULL`);\n if (filter.eventType) {\n parts.push(`${col(\"event_type\")} = ?`);\n values.push(filter.eventType);\n } else if (filter.eventTypePrefix) {\n parts.push(`${col(\"event_type\")} LIKE ? ESCAPE '\\\\'`);\n values.push(`${escapeLike(filter.eventTypePrefix)}%`);\n }\n for (const [key, column] of [[\"actorId\", \"actor_id\"], [\"actorName\", \"actor_name\"], [\"targetId\", \"target_id\"], [\"dimension\", \"dimension\"], [\"level\", \"level\"]] as const) {\n const value = filter[key];\n if (!value) continue;\n parts.push(`${col(column)} = ?`);\n values.push(value);\n }\n if (filter.from !== undefined) {\n parts.push(`${col(\"timestamp\")} >= ?`);\n values.push(filter.from);\n }\n if (filter.to !== undefined) {\n parts.push(`${col(\"timestamp\")} <= ?`);\n values.push(filter.to);\n }\n if (filter.keyword) {\n const pattern = `%${escapeLike(filter.keyword)}%`;\n parts.push(`(${[\"actor_name\", \"target_id\", \"payload_json\"].map((name) => `${col(name)} LIKE ? ESCAPE '\\\\'`).join(\" OR \")})`);\n values.push(pattern, pattern, pattern);\n }\n return { sql: parts.length ? `WHERE ${parts.join(\" AND \")}` : \"\", values };\n}\n\nconst text = (value: unknown) => (typeof value === \"string\" ? value : value == null ? \"\" : String(value));\nconst coordinate = (value: unknown) => (typeof value === \"number\" && Number.isFinite(value) ? value : null);\n\n/** \u6570\u636E\u5E93\u884C \u2192 \u5BF9\u5916\u8BB0\u5F55 */\nfunction toRecord(row: Record<string, unknown>): ActivityRecordRow {\n let payload: Record<string, unknown> | null = null;\n try {\n const parsed: unknown = JSON.parse(text(row.payload_json) || \"{}\");\n payload = parsed && typeof parsed === \"object\" && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : null;\n } catch {\n payload = null;\n }\n const level = text(row.level);\n return {\n id: text(row.id),\n timestamp: Number(row.timestamp) || 0,\n eventType: text(row.event_type),\n actorId: text(row.actor_id),\n actorName: text(row.actor_name),\n targetId: text(row.target_id),\n dimension: text(row.dimension),\n x: coordinate(row.x),\n y: coordinate(row.y),\n z: coordinate(row.z),\n level: LEVELS.includes(level as ActivityLevel) ? (level as ActivityLevel) : \"info\",\n payload,\n };\n}\n\n/** COUNT \u7ED3\u679C\u7EDF\u4E00\u8F6C\u6210\u6570\u5B57\uFF08node:sqlite \u53EF\u80FD\u8FD4\u56DE bigint\uFF09 */\nconst count = (value: unknown) => Number(value) || 0;\n\n/**\n * \u5728\u5DF2\u6709\u8FDE\u63A5\u4E0A\u5206\u9875\u67E5\u8BE2\u884C\u4E3A\u8BB0\u5F55\uFF08\u6309\u65F6\u95F4\u5012\u5E8F\uFF09\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1Adb-server \u8FDB\u7A0B\u5185\u7684\u8DEF\u7531\uFF1B\u8FDB\u7A0B\u5916\u8BF7\u7528 readActivityPage\u3002\n */\nexport function queryActivityPage(query: QueryFn, filter: ActivityFilter, paging: ActivityPaging): ActivityPageResult {\n const columns = tableColumns(query);\n if (!columns) return { installed: false, records: [], total: 0, ...paging };\n const where = whereClause(filter, columns);\n const totalRow = (query(`SELECT COUNT(*) AS c FROM \"${ACTIVITY_TABLE}\" ${where.sql}`, where.values) as Array<{ c?: unknown }>)[0];\n const rows = query(\n `SELECT id, timestamp, event_type, actor_id, actor_name, target_id, dimension, x, y, z, level, payload_json\n FROM \"${ACTIVITY_TABLE}\" ${where.sql}\n ORDER BY timestamp DESC, id DESC\n LIMIT ? OFFSET ?`,\n [...where.values, paging.limit, paging.offset]\n ) as Array<Record<string, unknown>>;\n return { installed: true, records: rows.map(toRecord), total: count(totalRow?.c), ...paging };\n}\n\n/**\n * \u5728\u5DF2\u6709\u8FDE\u63A5\u4E0A\u7EDF\u8BA1\u884C\u4E3A\u8BB0\u5F55\u3002\n * \u6D3B\u8DC3\u73A9\u5BB6\u53EA\u7EDF\u8BA1\u201C\u4EA7\u751F\u8FC7 player.* \u4E8B\u4EF6\u201D\u7684\u6267\u884C\u8005\uFF1A\u6A21\u5757\u5BF9\u7206\u70B8\u7B49\u975E\u73A9\u5BB6\u6765\u6E90\u4F1A\u628A\u5B9E\u4F53\u7C7B\u578B\u5199\u8FDB actor_id\uFF0C\n * \u800C player.* \u4E8B\u4EF6\u53EA\u7531\u771F\u5B9E\u73A9\u5BB6\u89E6\u53D1\uFF0C\u4EE5\u6B64\u533A\u5206\uFF0C\u4E0D\u4F9D\u8D56 ID \u683C\u5F0F\u3002\n */\nexport function queryActivityStats(query: QueryFn, filter: ActivityFilter): ActivityStatsResult {\n const columns = tableColumns(query);\n if (!columns) return { installed: false, total: 0, actors: 0, byType: [], byLevel: [], topActors: [] };\n const where = whereClause(filter, columns, \"a.\");\n const from = `FROM \"${ACTIVITY_TABLE}\" a ${where.sql}`;\n const playerActor = `a.actor_id <> '' AND EXISTS (SELECT 1 FROM \"${ACTIVITY_TABLE}\" p WHERE p.actor_id = a.actor_id AND p.event_type LIKE 'player.%')`;\n const and = where.sql ? \" AND \" : \" WHERE \";\n const byType = (query(`SELECT a.event_type AS k, COUNT(*) AS c ${from} GROUP BY a.event_type ORDER BY c DESC`, where.values) as Array<{ k?: unknown; c?: unknown }>)\n .map((row) => ({ eventType: text(row.k), count: count(row.c) }));\n const byLevel = (query(`SELECT a.level AS k, COUNT(*) AS c ${from} GROUP BY a.level`, where.values) as Array<{ k?: unknown; c?: unknown }>)\n .filter((row) => LEVELS.includes(text(row.k) as ActivityLevel))\n .map((row) => ({ level: text(row.k) as ActivityLevel, count: count(row.c) }));\n const actorsRow = (query(`SELECT COUNT(DISTINCT a.actor_id) AS c ${from}${and}${playerActor}`, where.values) as Array<{ c?: unknown }>)[0];\n const topActors = (query(\n `SELECT a.actor_id AS id, MAX(a.actor_name) AS name, COUNT(*) AS c ${from}${and}${playerActor}\n GROUP BY a.actor_id ORDER BY c DESC LIMIT ?`,\n [...where.values, ACTIVITY_TOP_ACTORS]\n ) as Array<{ id?: unknown; name?: unknown; c?: unknown }>).map((row) => ({ actorId: text(row.id), actorName: text(row.name), count: count(row.c) }));\n return {\n installed: true,\n total: byType.reduce((sum, row) => sum + row.count, 0),\n actors: count(actorsRow?.c),\n byType,\n byLevel,\n topActors,\n };\n}\n\n/**\n * \u53EA\u8BFB\u6253\u5F00\u4E1A\u52A1\u5E93\u6587\u4EF6\u5E76\u5206\u9875\u67E5\u8BE2\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1ACLI \u7BA1\u7406\u901A\u9053 activity.query\u3002\u6570\u636E\u5E93\u6587\u4EF6\u4E0D\u5B58\u5728\u65F6\u6309\u201C\u6A21\u5757\u672A\u5B89\u88C5\u201D\u8FD4\u56DE\u3002\n */\nexport function readActivityPage(filePath: string, filter: ActivityFilter, paging: ActivityPaging): ActivityPageResult {\n return withReadonlyQuery(filePath, () => ({ installed: false, records: [], total: 0, ...paging }), (query) => queryActivityPage(query, filter, paging));\n}\n\n/**\n * \u53EA\u8BFB\u6253\u5F00\u4E1A\u52A1\u5E93\u6587\u4EF6\u5E76\u7EDF\u8BA1\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1ACLI \u7BA1\u7406\u901A\u9053 activity.stats\u3002\n */\nexport function readActivityStats(filePath: string, filter: ActivityFilter): ActivityStatsResult {\n return withReadonlyQuery(\n filePath,\n () => ({ installed: false, total: 0, actors: 0, byType: [], byLevel: [], topActors: [] }),\n (query) => queryActivityStats(query, filter)\n );\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["text"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* domain/bind-providers.ts — 外部账号 ↔ 游戏账号绑定的提供方抽象与短码生成
|
|
3
|
+
*
|
|
4
|
+
* 使用场景:QQ 与微信两种绑定共用游戏内同一个 `/c:bind` 流程。
|
|
5
|
+
* - routes/bind-confirm.ts 按注册顺序询问每个提供方“这个短码是不是你的”,交给对应提供方确认(OCP:新增渠道只需注册新提供方);
|
|
6
|
+
* - 各渠道申请短码时用 generateBindCode 生成,在所有提供方的待确认表里都不重复,保证统一确认入口不会认错渠道。
|
|
7
|
+
*/
|
|
8
|
+
/** 绑定短码有效期(与 QQ 原有流程一致) */
|
|
9
|
+
export declare const BIND_CODE_TTL_MS: number;
|
|
10
|
+
/** 绑定短码位数(与 QQ 原有流程一致;游戏侧接受 4–8 位数字) */
|
|
11
|
+
export declare const BIND_CODE_LEN = 6;
|
|
12
|
+
/** 游戏侧提交的确认请求 */
|
|
13
|
+
export interface BindConfirmInput {
|
|
14
|
+
code: string;
|
|
15
|
+
xuid: string;
|
|
16
|
+
name: string;
|
|
17
|
+
now: number;
|
|
18
|
+
}
|
|
19
|
+
/** 确认结果:成功带绑定记录;失败带 HTTP 状态与错误码(错误码与 QQ 原接口一致,游戏侧据此提示) */
|
|
20
|
+
export type BindConfirmResult = {
|
|
21
|
+
ok: true;
|
|
22
|
+
binding: Record<string, unknown>;
|
|
23
|
+
} | {
|
|
24
|
+
ok: false;
|
|
25
|
+
status: number;
|
|
26
|
+
error: string;
|
|
27
|
+
hint?: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* 绑定提供方契约。
|
|
31
|
+
* 使用场景:QQ(domain/qq-bind.ts)与微信(domain/wechat-bind.ts)各实现一份,由 index.ts 组装成列表注入统一确认路由(LSP:确认语义一致)。
|
|
32
|
+
*/
|
|
33
|
+
export interface BindProvider {
|
|
34
|
+
/** 渠道标识,返回给游戏侧用于提示文案(qq / wechat) */
|
|
35
|
+
readonly id: string;
|
|
36
|
+
/** 待确认表里是否存在该短码(不判断过期,过期交给 confirm 返回 code_expired) */
|
|
37
|
+
hasPendingCode(code: string): boolean;
|
|
38
|
+
/** 用短码完成绑定 */
|
|
39
|
+
confirm(input: BindConfirmInput): BindConfirmResult;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 生成在所有提供方中都未被占用的短码;多次碰撞后返回空串,由调用方回 code_gen_failed。
|
|
43
|
+
*
|
|
44
|
+
* @param isTaken 判断短码是否已被任一提供方占用。
|
|
45
|
+
*/
|
|
46
|
+
export declare function generateBindCode(isTaken: (code: string) => boolean, length?: number): string;
|
|
47
|
+
/**
|
|
48
|
+
* 由提供方列表构造“短码是否已占用”判断。
|
|
49
|
+
* 使用场景:index.ts 组装后注入 QQ / 微信申请短码的路由,两边共用同一个判断(DRY)。
|
|
50
|
+
*/
|
|
51
|
+
export declare function codeTakenBy(providers: () => readonly BindProvider[]): (code: string) => boolean;
|
|
52
|
+
/**
|
|
53
|
+
* 统一确认:找到持有该短码的提供方并确认;都没有时返回 invalid_code。
|
|
54
|
+
* 使用场景:routes/bind-confirm.ts。返回值附带 provider,游戏侧据此决定提示文案和是否解除访客限制。
|
|
55
|
+
*/
|
|
56
|
+
export declare function confirmWithProviders(providers: readonly BindProvider[], input: BindConfirmInput): (BindConfirmResult & {
|
|
57
|
+
provider?: string;
|
|
58
|
+
});
|
|
59
|
+
//# sourceMappingURL=bind-providers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bind-providers.d.ts","sourceRoot":"","sources":["../../src/domain/bind-providers.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,2BAA2B;AAC3B,eAAO,MAAM,gBAAgB,QAAgB,CAAC;AAC9C,wCAAwC;AACxC,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B,iBAAiB;AACjB,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,0DAA0D;AAC1D,MAAM,MAAM,iBAAiB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAC9C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhE;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,qCAAqC;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACtC,cAAc;IACd,OAAO,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;CACrD;AASD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,EAAE,MAAM,SAAgB,GAAG,MAAM,CAMnG;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,SAAS,YAAY,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAE/F;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,SAAS,YAAY,EAAE,EAClC,KAAK,EAAE,gBAAgB,GACtB,CAAC,iBAAiB,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAM7C"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { randomInt } from "node:crypto";
|
|
2
|
+
const BIND_CODE_TTL_MS = 5 * 60 * 1e3;
|
|
3
|
+
const BIND_CODE_LEN = 6;
|
|
4
|
+
function randomCode(length) {
|
|
5
|
+
let s = "";
|
|
6
|
+
for (let i = 0; i < length; i++) s += String(randomInt(0, 10));
|
|
7
|
+
return s;
|
|
8
|
+
}
|
|
9
|
+
function generateBindCode(isTaken, length = BIND_CODE_LEN) {
|
|
10
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
11
|
+
const candidate = randomCode(length);
|
|
12
|
+
if (!isTaken(candidate)) return candidate;
|
|
13
|
+
}
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
function codeTakenBy(providers) {
|
|
17
|
+
return (code) => providers().some((provider) => provider.hasPendingCode(code));
|
|
18
|
+
}
|
|
19
|
+
function confirmWithProviders(providers, input) {
|
|
20
|
+
for (const provider of providers) {
|
|
21
|
+
if (!provider.hasPendingCode(input.code)) continue;
|
|
22
|
+
return { ...provider.confirm(input), provider: provider.id };
|
|
23
|
+
}
|
|
24
|
+
return { ok: false, status: 400, error: "invalid_code" };
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
BIND_CODE_LEN,
|
|
28
|
+
BIND_CODE_TTL_MS,
|
|
29
|
+
codeTakenBy,
|
|
30
|
+
confirmWithProviders,
|
|
31
|
+
generateBindCode
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=bind-providers.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/domain/bind-providers.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * domain/bind-providers.ts \u2014 \u5916\u90E8\u8D26\u53F7 \u2194 \u6E38\u620F\u8D26\u53F7\u7ED1\u5B9A\u7684\u63D0\u4F9B\u65B9\u62BD\u8C61\u4E0E\u77ED\u7801\u751F\u6210\n *\n * \u4F7F\u7528\u573A\u666F\uFF1AQQ \u4E0E\u5FAE\u4FE1\u4E24\u79CD\u7ED1\u5B9A\u5171\u7528\u6E38\u620F\u5185\u540C\u4E00\u4E2A `/c:bind` \u6D41\u7A0B\u3002\n * - routes/bind-confirm.ts \u6309\u6CE8\u518C\u987A\u5E8F\u8BE2\u95EE\u6BCF\u4E2A\u63D0\u4F9B\u65B9\u201C\u8FD9\u4E2A\u77ED\u7801\u662F\u4E0D\u662F\u4F60\u7684\u201D\uFF0C\u4EA4\u7ED9\u5BF9\u5E94\u63D0\u4F9B\u65B9\u786E\u8BA4\uFF08OCP\uFF1A\u65B0\u589E\u6E20\u9053\u53EA\u9700\u6CE8\u518C\u65B0\u63D0\u4F9B\u65B9\uFF09\uFF1B\n * - \u5404\u6E20\u9053\u7533\u8BF7\u77ED\u7801\u65F6\u7528 generateBindCode \u751F\u6210\uFF0C\u5728\u6240\u6709\u63D0\u4F9B\u65B9\u7684\u5F85\u786E\u8BA4\u8868\u91CC\u90FD\u4E0D\u91CD\u590D\uFF0C\u4FDD\u8BC1\u7EDF\u4E00\u786E\u8BA4\u5165\u53E3\u4E0D\u4F1A\u8BA4\u9519\u6E20\u9053\u3002\n */\n\nimport { randomInt } from \"node:crypto\";\n\n/** \u7ED1\u5B9A\u77ED\u7801\u6709\u6548\u671F\uFF08\u4E0E QQ \u539F\u6709\u6D41\u7A0B\u4E00\u81F4\uFF09 */\nexport const BIND_CODE_TTL_MS = 5 * 60 * 1000;\n/** \u7ED1\u5B9A\u77ED\u7801\u4F4D\u6570\uFF08\u4E0E QQ \u539F\u6709\u6D41\u7A0B\u4E00\u81F4\uFF1B\u6E38\u620F\u4FA7\u63A5\u53D7 4\u20138 \u4F4D\u6570\u5B57\uFF09 */\nexport const BIND_CODE_LEN = 6;\n\n/** \u6E38\u620F\u4FA7\u63D0\u4EA4\u7684\u786E\u8BA4\u8BF7\u6C42 */\nexport interface BindConfirmInput {\n code: string;\n xuid: string;\n name: string;\n now: number;\n}\n\n/** \u786E\u8BA4\u7ED3\u679C\uFF1A\u6210\u529F\u5E26\u7ED1\u5B9A\u8BB0\u5F55\uFF1B\u5931\u8D25\u5E26 HTTP \u72B6\u6001\u4E0E\u9519\u8BEF\u7801\uFF08\u9519\u8BEF\u7801\u4E0E QQ \u539F\u63A5\u53E3\u4E00\u81F4\uFF0C\u6E38\u620F\u4FA7\u636E\u6B64\u63D0\u793A\uFF09 */\nexport type BindConfirmResult =\n | { ok: true; binding: Record<string, unknown> }\n | { ok: false; status: number; error: string; hint?: string };\n\n/**\n * \u7ED1\u5B9A\u63D0\u4F9B\u65B9\u5951\u7EA6\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1AQQ\uFF08domain/qq-bind.ts\uFF09\u4E0E\u5FAE\u4FE1\uFF08domain/wechat-bind.ts\uFF09\u5404\u5B9E\u73B0\u4E00\u4EFD\uFF0C\u7531 index.ts \u7EC4\u88C5\u6210\u5217\u8868\u6CE8\u5165\u7EDF\u4E00\u786E\u8BA4\u8DEF\u7531\uFF08LSP\uFF1A\u786E\u8BA4\u8BED\u4E49\u4E00\u81F4\uFF09\u3002\n */\nexport interface BindProvider {\n /** \u6E20\u9053\u6807\u8BC6\uFF0C\u8FD4\u56DE\u7ED9\u6E38\u620F\u4FA7\u7528\u4E8E\u63D0\u793A\u6587\u6848\uFF08qq / wechat\uFF09 */\n readonly id: string;\n /** \u5F85\u786E\u8BA4\u8868\u91CC\u662F\u5426\u5B58\u5728\u8BE5\u77ED\u7801\uFF08\u4E0D\u5224\u65AD\u8FC7\u671F\uFF0C\u8FC7\u671F\u4EA4\u7ED9 confirm \u8FD4\u56DE code_expired\uFF09 */\n hasPendingCode(code: string): boolean;\n /** \u7528\u77ED\u7801\u5B8C\u6210\u7ED1\u5B9A */\n confirm(input: BindConfirmInput): BindConfirmResult;\n}\n\n/** \u751F\u6210\u4E00\u4E2A\u7EAF\u6570\u5B57\u77ED\u7801 */\nfunction randomCode(length: number): string {\n let s = \"\";\n for (let i = 0; i < length; i++) s += String(randomInt(0, 10));\n return s;\n}\n\n/**\n * \u751F\u6210\u5728\u6240\u6709\u63D0\u4F9B\u65B9\u4E2D\u90FD\u672A\u88AB\u5360\u7528\u7684\u77ED\u7801\uFF1B\u591A\u6B21\u78B0\u649E\u540E\u8FD4\u56DE\u7A7A\u4E32\uFF0C\u7531\u8C03\u7528\u65B9\u56DE code_gen_failed\u3002\n *\n * @param isTaken \u5224\u65AD\u77ED\u7801\u662F\u5426\u5DF2\u88AB\u4EFB\u4E00\u63D0\u4F9B\u65B9\u5360\u7528\u3002\n */\nexport function generateBindCode(isTaken: (code: string) => boolean, length = BIND_CODE_LEN): string {\n for (let attempt = 0; attempt < 8; attempt++) {\n const candidate = randomCode(length);\n if (!isTaken(candidate)) return candidate;\n }\n return \"\";\n}\n\n/**\n * \u7531\u63D0\u4F9B\u65B9\u5217\u8868\u6784\u9020\u201C\u77ED\u7801\u662F\u5426\u5DF2\u5360\u7528\u201D\u5224\u65AD\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1Aindex.ts \u7EC4\u88C5\u540E\u6CE8\u5165 QQ / \u5FAE\u4FE1\u7533\u8BF7\u77ED\u7801\u7684\u8DEF\u7531\uFF0C\u4E24\u8FB9\u5171\u7528\u540C\u4E00\u4E2A\u5224\u65AD\uFF08DRY\uFF09\u3002\n */\nexport function codeTakenBy(providers: () => readonly BindProvider[]): (code: string) => boolean {\n return (code) => providers().some((provider) => provider.hasPendingCode(code));\n}\n\n/**\n * \u7EDF\u4E00\u786E\u8BA4\uFF1A\u627E\u5230\u6301\u6709\u8BE5\u77ED\u7801\u7684\u63D0\u4F9B\u65B9\u5E76\u786E\u8BA4\uFF1B\u90FD\u6CA1\u6709\u65F6\u8FD4\u56DE invalid_code\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1Aroutes/bind-confirm.ts\u3002\u8FD4\u56DE\u503C\u9644\u5E26 provider\uFF0C\u6E38\u620F\u4FA7\u636E\u6B64\u51B3\u5B9A\u63D0\u793A\u6587\u6848\u548C\u662F\u5426\u89E3\u9664\u8BBF\u5BA2\u9650\u5236\u3002\n */\nexport function confirmWithProviders(\n providers: readonly BindProvider[],\n input: BindConfirmInput\n): (BindConfirmResult & { provider?: string }) {\n for (const provider of providers) {\n if (!provider.hasPendingCode(input.code)) continue;\n return { ...provider.confirm(input), provider: provider.id };\n }\n return { ok: false, status: 400, error: \"invalid_code\" };\n}\n"],
|
|
5
|
+
"mappings": "AAQA,SAAS,iBAAiB;AAGnB,MAAM,mBAAmB,IAAI,KAAK;AAElC,MAAM,gBAAgB;AA6B7B,SAAS,WAAW,QAAwB;AAC1C,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,QAAQ,IAAK,MAAK,OAAO,UAAU,GAAG,EAAE,CAAC;AAC7D,SAAO;AACT;AAOO,SAAS,iBAAiB,SAAoC,SAAS,eAAuB;AACnG,WAAS,UAAU,GAAG,UAAU,GAAG,WAAW;AAC5C,UAAM,YAAY,WAAW,MAAM;AACnC,QAAI,CAAC,QAAQ,SAAS,EAAG,QAAO;AAAA,EAClC;AACA,SAAO;AACT;AAMO,SAAS,YAAY,WAAqE;AAC/F,SAAO,CAAC,SAAS,UAAU,EAAE,KAAK,CAAC,aAAa,SAAS,eAAe,IAAI,CAAC;AAC/E;AAMO,SAAS,qBACd,WACA,OAC6C;AAC7C,aAAW,YAAY,WAAW;AAChC,QAAI,CAAC,SAAS,eAAe,MAAM,IAAI,EAAG;AAC1C,WAAO,EAAE,GAAG,SAAS,QAAQ,KAAK,GAAG,UAAU,SAAS,GAAG;AAAA,EAC7D;AACA,SAAO,EAAE,IAAI,OAAO,QAAQ,KAAK,OAAO,eAAe;AACzD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/domain/metrics.d.ts
CHANGED
|
@@ -27,6 +27,11 @@ export interface ResourceSample {
|
|
|
27
27
|
hostUsedMb: number | null;
|
|
28
28
|
dbMb: number | null;
|
|
29
29
|
bdsMb: number | null;
|
|
30
|
+
/**
|
|
31
|
+
* 其余已登记进程的内存(MB)。
|
|
32
|
+
* 使用场景:守护进程、QQ 桥、隧道、LLBot、附属进程。旧采样没有该字段;键缺省表示当时没采到。
|
|
33
|
+
*/
|
|
34
|
+
partsMb?: Record<string, number>;
|
|
30
35
|
}
|
|
31
36
|
export declare class ResourceHistory {
|
|
32
37
|
private query;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/domain/metrics.ts"],"names":[],"mappings":"AAGA,OAAO,EAAe,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAE7D,oDAAoD;AACpD,eAAO,MAAM,iBAAiB,OAAQ,CAAC;AACvC,kBAAkB;AAClB,eAAO,MAAM,kBAAkB,OAAQ,CAAC;AACxC,6BAA6B;AAC7B,eAAO,MAAM,qBAAqB,UAAY,CAAC;AAC/C,2CAA2C;AAC3C,eAAO,MAAM,oBAAoB,QAA6B,CAAC;AAO/D;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAIvD;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAKD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAoC7F;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;
|
|
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"}
|
package/dist/domain/metrics.js
CHANGED
|
@@ -34,6 +34,17 @@ function parseMetrics(data, now) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
const memoryMb = (value) => value === null || typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 8388608;
|
|
37
|
+
const partKey = (key) => /^[a-z0-9][a-z0-9_:-]{0,80}$/i.test(key);
|
|
38
|
+
const memoryNumber = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 8388608;
|
|
39
|
+
function sanitizeParts(value) {
|
|
40
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
41
|
+
const parts = {};
|
|
42
|
+
for (const [key, item] of Object.entries(value)) {
|
|
43
|
+
if (!partKey(key) || !memoryNumber(item)) continue;
|
|
44
|
+
parts[key] = item;
|
|
45
|
+
}
|
|
46
|
+
return Object.keys(parts).length ? parts : void 0;
|
|
47
|
+
}
|
|
37
48
|
class ResourceHistory {
|
|
38
49
|
constructor(query) {
|
|
39
50
|
this.query = query;
|
|
@@ -44,8 +55,16 @@ class ResourceHistory {
|
|
|
44
55
|
lastPrune = 0;
|
|
45
56
|
append(sample) {
|
|
46
57
|
if (sample.recordedAt - this.lastWrite < RESOURCE_SAMPLE_MS) return;
|
|
47
|
-
|
|
58
|
+
const partsMb = sanitizeParts(sample.partsMb);
|
|
59
|
+
if (sample.hostUsedMb === null && sample.dbMb === null && sample.bdsMb === null && !partsMb) return;
|
|
48
60
|
if (!memoryMb(sample.hostUsedMb) || !memoryMb(sample.dbMb) || !memoryMb(sample.bdsMb)) return;
|
|
61
|
+
sample = {
|
|
62
|
+
recordedAt: sample.recordedAt,
|
|
63
|
+
hostUsedMb: sample.hostUsedMb,
|
|
64
|
+
dbMb: sample.dbMb,
|
|
65
|
+
bdsMb: sample.bdsMb,
|
|
66
|
+
...partsMb ? { partsMb } : {}
|
|
67
|
+
};
|
|
49
68
|
this.query("INSERT OR REPLACE INTO sfmc_resource_metrics (recorded_at, sample) VALUES (?, ?)", [
|
|
50
69
|
sample.recordedAt,
|
|
51
70
|
JSON.stringify(sample)
|
|
@@ -149,7 +168,14 @@ function parseResourceRows(rows) {
|
|
|
149
168
|
try {
|
|
150
169
|
const sample = JSON.parse(row.sample);
|
|
151
170
|
if (!memoryMb(sample.hostUsedMb) || !memoryMb(sample.dbMb) || !memoryMb(sample.bdsMb)) return [];
|
|
152
|
-
|
|
171
|
+
const partsMb = sanitizeParts(sample.partsMb);
|
|
172
|
+
return [{
|
|
173
|
+
recordedAt: sample.recordedAt,
|
|
174
|
+
hostUsedMb: sample.hostUsedMb,
|
|
175
|
+
dbMb: sample.dbMb,
|
|
176
|
+
bdsMb: sample.bdsMb,
|
|
177
|
+
...partsMb ? { partsMb } : {}
|
|
178
|
+
}];
|
|
153
179
|
} catch {
|
|
154
180
|
return [];
|
|
155
181
|
}
|