@sfmc-bds/db-server 0.6.0 → 0.8.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/scoreboard.d.ts +122 -0
- package/dist/domain/scoreboard.d.ts.map +1 -0
- package/dist/domain/scoreboard.js +134 -0
- package/dist/domain/scoreboard.js.map +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +2 -2
- package/dist/routes/scoreboard.d.ts +20 -0
- package/dist/routes/scoreboard.d.ts.map +1 -0
- package/dist/routes/scoreboard.js +30 -0
- package/dist/routes/scoreboard.js.map +7 -0
- 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/package.json +14 -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,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* domain/scoreboard.ts — 世界计分板快照的接收与读取
|
|
3
|
+
*
|
|
4
|
+
* 使用场景:
|
|
5
|
+
* - 游戏内平台脚本(SDK runtime/scoreboard-sync)每约 2 秒 POST /api/sfmc/scoreboard/live;
|
|
6
|
+
* - CLI 的 scoreboard.read 经 GET /api/sfmc/scoreboard 读取,供桌面端与小程序「计分板」页展示。
|
|
7
|
+
*
|
|
8
|
+
* 遍历全部计分项有开销,所以只在有人查看时同步:每次读取把「关注期」延长 WATCH_MS,
|
|
9
|
+
* 游戏内脚本从上报的响应里得知 watching 后才计算快照;无人查看时只发空闲探询。
|
|
10
|
+
* 快照内容不变时只上报哈希(kind: "same"),数据服务据此确认快照仍然新鲜;
|
|
11
|
+
* 数据服务重启丢失快照、或哈希对不上时,响应 needFull 让脚本下次补发完整快照。
|
|
12
|
+
* 快照只保存在内存中,不落库。
|
|
13
|
+
*/
|
|
14
|
+
/** 参与者类型(与 @sfmc-bds/management 的 ScoreboardParticipantType 一致) */
|
|
15
|
+
export type ScoreboardParticipantType = "player" | "fakePlayer" | "entity";
|
|
16
|
+
/** 显示位(与 @sfmc-bds/management 的 ScoreboardDisplaySlot 一致) */
|
|
17
|
+
export type ScoreboardDisplaySlot = "sidebar" | "list" | "belowName";
|
|
18
|
+
/** 上报中的参与者(各计分项共用,分数按 ID 引用) */
|
|
19
|
+
export interface ScoreboardReportParticipant {
|
|
20
|
+
id: number;
|
|
21
|
+
name: string;
|
|
22
|
+
type: ScoreboardParticipantType;
|
|
23
|
+
online: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** 上报中的计分项;scores 为 [参与者 ID, 分数] 对,避免在每个计分项里重复参与者信息 */
|
|
26
|
+
export interface ScoreboardReportObjective {
|
|
27
|
+
id: string;
|
|
28
|
+
displayName: string;
|
|
29
|
+
scores: Array<[number, number]>;
|
|
30
|
+
}
|
|
31
|
+
/** 上报中的显示位 */
|
|
32
|
+
export interface ScoreboardReportDisplay {
|
|
33
|
+
slot: ScoreboardDisplaySlot;
|
|
34
|
+
objectiveId: string;
|
|
35
|
+
sortOrder: "ascending" | "descending";
|
|
36
|
+
}
|
|
37
|
+
/** 完整快照 */
|
|
38
|
+
export interface ScoreboardSnapshot {
|
|
39
|
+
hash: string;
|
|
40
|
+
/** 游戏内生成快照的时间(毫秒时间戳) */
|
|
41
|
+
generatedAt: number;
|
|
42
|
+
participants: ScoreboardReportParticipant[];
|
|
43
|
+
objectives: ScoreboardReportObjective[];
|
|
44
|
+
displaySlots: ScoreboardReportDisplay[];
|
|
45
|
+
truncated: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** 解析后的上报:空闲探询 / 内容未变 / 完整快照 */
|
|
48
|
+
export type ScoreboardReport = {
|
|
49
|
+
kind: "idle";
|
|
50
|
+
} | {
|
|
51
|
+
kind: "same";
|
|
52
|
+
hash: string;
|
|
53
|
+
generatedAt: number;
|
|
54
|
+
} | ({
|
|
55
|
+
kind: "full";
|
|
56
|
+
} & ScoreboardSnapshot);
|
|
57
|
+
/** 上报的响应:是否有人在看(决定脚本是否计算快照),以及是否需要补发完整快照 */
|
|
58
|
+
export interface ScoreboardReportReply {
|
|
59
|
+
watching: boolean;
|
|
60
|
+
needFull: boolean;
|
|
61
|
+
}
|
|
62
|
+
/** 与 @sfmc-bds/management 的 ScoreboardReadResult 同构;CLI 侧由类型检查保证两边不漂移 */
|
|
63
|
+
export interface ScoreboardReadView {
|
|
64
|
+
available: boolean;
|
|
65
|
+
fresh: boolean;
|
|
66
|
+
updatedAt: number | null;
|
|
67
|
+
objectives: Array<{
|
|
68
|
+
id: string;
|
|
69
|
+
displayName: string;
|
|
70
|
+
participants: Array<ScoreboardReportParticipant & {
|
|
71
|
+
score: number;
|
|
72
|
+
}>;
|
|
73
|
+
}>;
|
|
74
|
+
displaySlots: ScoreboardReportDisplay[];
|
|
75
|
+
truncated: boolean;
|
|
76
|
+
note?: string;
|
|
77
|
+
/**
|
|
78
|
+
* 游戏内脚本最近是否上报过(含空闲探询)。不属于管理协议,CLI 用来判断是否值得等待新快照:
|
|
79
|
+
* 脚本没在上报(BDS 未运行或平台脚本未加载)时直接返回,不空等。
|
|
80
|
+
*/
|
|
81
|
+
scriptConnected: boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 上报体积上限:超过即视为异常请求拒收。游戏内脚本有自己的更小上限(超出时截断并标记 truncated),
|
|
85
|
+
* 这里只防止异常请求占满内存。
|
|
86
|
+
*/
|
|
87
|
+
export declare const SCOREBOARD_REPORT_LIMITS: {
|
|
88
|
+
readonly objectives: 2000;
|
|
89
|
+
readonly participants: 100000;
|
|
90
|
+
readonly scores: 500000;
|
|
91
|
+
readonly name: 512;
|
|
92
|
+
};
|
|
93
|
+
/** 一次读取后保持「有人在看」的时长 */
|
|
94
|
+
export declare const SCOREBOARD_WATCH_MS = 30000;
|
|
95
|
+
/** 快照在最后一次确认后多久内视为新鲜(脚本约 2 秒确认一次,留出几次失败的余量) */
|
|
96
|
+
export declare const SCOREBOARD_FRESH_MS = 10000;
|
|
97
|
+
/**
|
|
98
|
+
* 校验并解析一次上报;格式不对时返回 null。
|
|
99
|
+
* 使用场景:POST /api/sfmc/scoreboard/live 收到的请求体。
|
|
100
|
+
*/
|
|
101
|
+
export declare function parseScoreboardReport(data: Record<string, unknown>, now?: number): ScoreboardReport | null;
|
|
102
|
+
/**
|
|
103
|
+
* 内存中的计分板快照与关注状态。
|
|
104
|
+
* 使用场景:routes/scoreboard.ts 每个数据服务进程持有一个实例。
|
|
105
|
+
*/
|
|
106
|
+
export declare class ScoreboardStore {
|
|
107
|
+
private snapshot;
|
|
108
|
+
/** 脚本最后一次确认快照与世界一致的时间(游戏内时间戳) */
|
|
109
|
+
private verifiedAt;
|
|
110
|
+
/** 数据服务最后一次收到确认的时间(本机时间),用于判断新鲜度 */
|
|
111
|
+
private confirmedAt;
|
|
112
|
+
/** 在此之前视为有人在看 */
|
|
113
|
+
private watchUntil;
|
|
114
|
+
/** 最后一次收到任意上报的时间(本机时间);从未收到时为 null */
|
|
115
|
+
private reportedAt;
|
|
116
|
+
/** 处理一次上报,返回给脚本的指示 */
|
|
117
|
+
report(report: ScoreboardReport, now?: number): ScoreboardReportReply;
|
|
118
|
+
/** 读取快照并延长关注期,使脚本继续同步 */
|
|
119
|
+
read(now?: number): ScoreboardReadView;
|
|
120
|
+
private watching;
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=scoreboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scoreboard.d.ts","sourceRoot":"","sources":["../../src/domain/scoreboard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const SCOREBOARD_REPORT_LIMITS = { objectives: 2e3, participants: 1e5, scores: 5e5, name: 512 };
|
|
2
|
+
const SCOREBOARD_WATCH_MS = 3e4;
|
|
3
|
+
const SCOREBOARD_FRESH_MS = 1e4;
|
|
4
|
+
const PARTICIPANT_TYPES = /* @__PURE__ */ new Set(["player", "fakePlayer", "entity"]);
|
|
5
|
+
const DISPLAY_SLOTS = /* @__PURE__ */ new Set(["sidebar", "list", "belowName"]);
|
|
6
|
+
const isText = (value, allowEmpty = true) => typeof value === "string" && value.length <= SCOREBOARD_REPORT_LIMITS.name && (allowEmpty || value.length > 0);
|
|
7
|
+
const isInt32 = (value) => typeof value === "number" && Number.isInteger(value) && value >= -2147483648 && value <= 2147483647;
|
|
8
|
+
const timestamp = (value, now) => typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : now;
|
|
9
|
+
function parseScoreboardReport(data, now = Date.now()) {
|
|
10
|
+
if (data.kind === "idle") return { kind: "idle" };
|
|
11
|
+
if (!isText(data.hash, false)) return null;
|
|
12
|
+
if (data.kind === "same") return { kind: "same", hash: data.hash, generatedAt: timestamp(data.generatedAt, now) };
|
|
13
|
+
if (data.kind !== "full") return null;
|
|
14
|
+
const rawParticipants = data.participants;
|
|
15
|
+
const rawObjectives = data.objectives;
|
|
16
|
+
const rawDisplay = data.displaySlots ?? [];
|
|
17
|
+
if (!Array.isArray(rawParticipants) || rawParticipants.length > SCOREBOARD_REPORT_LIMITS.participants) return null;
|
|
18
|
+
if (!Array.isArray(rawObjectives) || rawObjectives.length > SCOREBOARD_REPORT_LIMITS.objectives) return null;
|
|
19
|
+
if (!Array.isArray(rawDisplay) || rawDisplay.length > DISPLAY_SLOTS.size) return null;
|
|
20
|
+
const participants = [];
|
|
21
|
+
const known = /* @__PURE__ */ new Set();
|
|
22
|
+
for (const row of rawParticipants) {
|
|
23
|
+
if (!row || typeof row !== "object") return null;
|
|
24
|
+
const { id, name, type, online } = row;
|
|
25
|
+
if (!isInt32(id) || known.has(id) || !isText(name) || typeof type !== "string" || !PARTICIPANT_TYPES.has(type) || typeof online !== "boolean") return null;
|
|
26
|
+
known.add(id);
|
|
27
|
+
participants.push({ id, name, type, online });
|
|
28
|
+
}
|
|
29
|
+
const objectives = [];
|
|
30
|
+
const objectiveIds = /* @__PURE__ */ new Set();
|
|
31
|
+
let scoreCount = 0;
|
|
32
|
+
for (const row of rawObjectives) {
|
|
33
|
+
if (!row || typeof row !== "object") return null;
|
|
34
|
+
const { id, displayName, scores } = row;
|
|
35
|
+
if (!isText(id, false) || objectiveIds.has(id) || !isText(displayName) || !Array.isArray(scores)) return null;
|
|
36
|
+
scoreCount += scores.length;
|
|
37
|
+
if (scoreCount > SCOREBOARD_REPORT_LIMITS.scores) return null;
|
|
38
|
+
const pairs = [];
|
|
39
|
+
for (const pair of scores) {
|
|
40
|
+
if (!Array.isArray(pair) || pair.length !== 2 || !known.has(pair[0]) || !isInt32(pair[1])) return null;
|
|
41
|
+
pairs.push([pair[0], pair[1]]);
|
|
42
|
+
}
|
|
43
|
+
objectiveIds.add(id);
|
|
44
|
+
objectives.push({ id, displayName, scores: pairs });
|
|
45
|
+
}
|
|
46
|
+
const displaySlots = [];
|
|
47
|
+
for (const row of rawDisplay) {
|
|
48
|
+
if (!row || typeof row !== "object") return null;
|
|
49
|
+
const { slot, objectiveId, sortOrder } = row;
|
|
50
|
+
if (typeof slot !== "string" || !DISPLAY_SLOTS.has(slot) || !isText(objectiveId, false)) return null;
|
|
51
|
+
if (sortOrder !== "ascending" && sortOrder !== "descending") return null;
|
|
52
|
+
displaySlots.push({ slot, objectiveId, sortOrder });
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
kind: "full",
|
|
56
|
+
hash: data.hash,
|
|
57
|
+
generatedAt: timestamp(data.generatedAt, now),
|
|
58
|
+
participants,
|
|
59
|
+
objectives,
|
|
60
|
+
displaySlots,
|
|
61
|
+
truncated: data.truncated === true
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
class ScoreboardStore {
|
|
65
|
+
snapshot = null;
|
|
66
|
+
/** 脚本最后一次确认快照与世界一致的时间(游戏内时间戳) */
|
|
67
|
+
verifiedAt = 0;
|
|
68
|
+
/** 数据服务最后一次收到确认的时间(本机时间),用于判断新鲜度 */
|
|
69
|
+
confirmedAt = 0;
|
|
70
|
+
/** 在此之前视为有人在看 */
|
|
71
|
+
watchUntil = 0;
|
|
72
|
+
/** 最后一次收到任意上报的时间(本机时间);从未收到时为 null */
|
|
73
|
+
reportedAt = null;
|
|
74
|
+
/** 处理一次上报,返回给脚本的指示 */
|
|
75
|
+
report(report, now = Date.now()) {
|
|
76
|
+
this.reportedAt = now;
|
|
77
|
+
if (report.kind === "full") {
|
|
78
|
+
const { kind: _kind, ...snapshot } = report;
|
|
79
|
+
this.snapshot = snapshot;
|
|
80
|
+
this.verifiedAt = snapshot.generatedAt;
|
|
81
|
+
this.confirmedAt = now;
|
|
82
|
+
} else if (report.kind === "same") {
|
|
83
|
+
if (!this.snapshot || this.snapshot.hash !== report.hash) return { watching: this.watching(now), needFull: true };
|
|
84
|
+
this.verifiedAt = Math.max(this.verifiedAt, report.generatedAt);
|
|
85
|
+
this.confirmedAt = now;
|
|
86
|
+
}
|
|
87
|
+
return { watching: this.watching(now), needFull: false };
|
|
88
|
+
}
|
|
89
|
+
/** 读取快照并延长关注期,使脚本继续同步 */
|
|
90
|
+
read(now = Date.now()) {
|
|
91
|
+
this.watchUntil = now + SCOREBOARD_WATCH_MS;
|
|
92
|
+
const snapshot = this.snapshot;
|
|
93
|
+
const fresh = snapshot !== null && now - this.confirmedAt <= SCOREBOARD_FRESH_MS;
|
|
94
|
+
const scriptConnected = this.reportedAt !== null && now - this.reportedAt <= SCOREBOARD_FRESH_MS;
|
|
95
|
+
if (!snapshot) {
|
|
96
|
+
return {
|
|
97
|
+
available: false,
|
|
98
|
+
fresh: false,
|
|
99
|
+
updatedAt: null,
|
|
100
|
+
objectives: [],
|
|
101
|
+
displaySlots: [],
|
|
102
|
+
truncated: false,
|
|
103
|
+
note: scriptConnected ? "\u6B63\u5728\u7B49\u5F85\u6E38\u620F\u5185\u540C\u6B65\u8BA1\u5206\u677F" : "\u6E38\u620F\u5185\u5E73\u53F0\u811A\u672C\u672A\u8FDE\u63A5\uFF0C\u8BF7\u786E\u8BA4 BDS \u6B63\u5728\u8FD0\u884C",
|
|
104
|
+
scriptConnected
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const byId = new Map(snapshot.participants.map((row) => [row.id, row]));
|
|
108
|
+
return {
|
|
109
|
+
available: true,
|
|
110
|
+
fresh,
|
|
111
|
+
updatedAt: this.verifiedAt,
|
|
112
|
+
objectives: snapshot.objectives.map((objective) => ({
|
|
113
|
+
id: objective.id,
|
|
114
|
+
displayName: objective.displayName,
|
|
115
|
+
participants: objective.scores.map(([id, score]) => ({ ...byId.get(id), score }))
|
|
116
|
+
})),
|
|
117
|
+
displaySlots: snapshot.displaySlots,
|
|
118
|
+
truncated: snapshot.truncated,
|
|
119
|
+
...fresh ? {} : { note: scriptConnected ? "\u8BA1\u5206\u677F\u5FEB\u7167\u5DF2\u8FC7\u671F\uFF0C\u6B63\u5728\u7B49\u5F85\u6E38\u620F\u5185\u91CD\u65B0\u540C\u6B65" : "\u6E38\u620F\u5185\u5E73\u53F0\u811A\u672C\u672A\u8FDE\u63A5\uFF0C\u663E\u793A\u7684\u662F\u6700\u540E\u4E00\u6B21\u540C\u6B65\u7684\u8BA1\u5206\u677F" },
|
|
120
|
+
scriptConnected
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
watching(now) {
|
|
124
|
+
return now < this.watchUntil;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export {
|
|
128
|
+
SCOREBOARD_FRESH_MS,
|
|
129
|
+
SCOREBOARD_REPORT_LIMITS,
|
|
130
|
+
SCOREBOARD_WATCH_MS,
|
|
131
|
+
ScoreboardStore,
|
|
132
|
+
parseScoreboardReport
|
|
133
|
+
};
|
|
134
|
+
//# sourceMappingURL=scoreboard.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/domain/scoreboard.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * domain/scoreboard.ts \u2014 \u4E16\u754C\u8BA1\u5206\u677F\u5FEB\u7167\u7684\u63A5\u6536\u4E0E\u8BFB\u53D6\n *\n * \u4F7F\u7528\u573A\u666F\uFF1A\n * - \u6E38\u620F\u5185\u5E73\u53F0\u811A\u672C\uFF08SDK runtime/scoreboard-sync\uFF09\u6BCF\u7EA6 2 \u79D2 POST /api/sfmc/scoreboard/live\uFF1B\n * - CLI \u7684 scoreboard.read \u7ECF GET /api/sfmc/scoreboard \u8BFB\u53D6\uFF0C\u4F9B\u684C\u9762\u7AEF\u4E0E\u5C0F\u7A0B\u5E8F\u300C\u8BA1\u5206\u677F\u300D\u9875\u5C55\u793A\u3002\n *\n * \u904D\u5386\u5168\u90E8\u8BA1\u5206\u9879\u6709\u5F00\u9500\uFF0C\u6240\u4EE5\u53EA\u5728\u6709\u4EBA\u67E5\u770B\u65F6\u540C\u6B65\uFF1A\u6BCF\u6B21\u8BFB\u53D6\u628A\u300C\u5173\u6CE8\u671F\u300D\u5EF6\u957F WATCH_MS\uFF0C\n * \u6E38\u620F\u5185\u811A\u672C\u4ECE\u4E0A\u62A5\u7684\u54CD\u5E94\u91CC\u5F97\u77E5 watching \u540E\u624D\u8BA1\u7B97\u5FEB\u7167\uFF1B\u65E0\u4EBA\u67E5\u770B\u65F6\u53EA\u53D1\u7A7A\u95F2\u63A2\u8BE2\u3002\n * \u5FEB\u7167\u5185\u5BB9\u4E0D\u53D8\u65F6\u53EA\u4E0A\u62A5\u54C8\u5E0C\uFF08kind: \"same\"\uFF09\uFF0C\u6570\u636E\u670D\u52A1\u636E\u6B64\u786E\u8BA4\u5FEB\u7167\u4ECD\u7136\u65B0\u9C9C\uFF1B\n * \u6570\u636E\u670D\u52A1\u91CD\u542F\u4E22\u5931\u5FEB\u7167\u3001\u6216\u54C8\u5E0C\u5BF9\u4E0D\u4E0A\u65F6\uFF0C\u54CD\u5E94 needFull \u8BA9\u811A\u672C\u4E0B\u6B21\u8865\u53D1\u5B8C\u6574\u5FEB\u7167\u3002\n * \u5FEB\u7167\u53EA\u4FDD\u5B58\u5728\u5185\u5B58\u4E2D\uFF0C\u4E0D\u843D\u5E93\u3002\n */\n\n/** \u53C2\u4E0E\u8005\u7C7B\u578B\uFF08\u4E0E @sfmc-bds/management \u7684 ScoreboardParticipantType \u4E00\u81F4\uFF09 */\nexport type ScoreboardParticipantType = \"player\" | \"fakePlayer\" | \"entity\";\n/** \u663E\u793A\u4F4D\uFF08\u4E0E @sfmc-bds/management \u7684 ScoreboardDisplaySlot \u4E00\u81F4\uFF09 */\nexport type ScoreboardDisplaySlot = \"sidebar\" | \"list\" | \"belowName\";\n\n/** \u4E0A\u62A5\u4E2D\u7684\u53C2\u4E0E\u8005\uFF08\u5404\u8BA1\u5206\u9879\u5171\u7528\uFF0C\u5206\u6570\u6309 ID \u5F15\u7528\uFF09 */\nexport interface ScoreboardReportParticipant {\n id: number;\n name: string;\n type: ScoreboardParticipantType;\n online: boolean;\n}\n\n/** \u4E0A\u62A5\u4E2D\u7684\u8BA1\u5206\u9879\uFF1Bscores \u4E3A [\u53C2\u4E0E\u8005 ID, \u5206\u6570] \u5BF9\uFF0C\u907F\u514D\u5728\u6BCF\u4E2A\u8BA1\u5206\u9879\u91CC\u91CD\u590D\u53C2\u4E0E\u8005\u4FE1\u606F */\nexport interface ScoreboardReportObjective {\n id: string;\n displayName: string;\n scores: Array<[number, number]>;\n}\n\n/** \u4E0A\u62A5\u4E2D\u7684\u663E\u793A\u4F4D */\nexport interface ScoreboardReportDisplay {\n slot: ScoreboardDisplaySlot;\n objectiveId: string;\n sortOrder: \"ascending\" | \"descending\";\n}\n\n/** \u5B8C\u6574\u5FEB\u7167 */\nexport interface ScoreboardSnapshot {\n hash: string;\n /** \u6E38\u620F\u5185\u751F\u6210\u5FEB\u7167\u7684\u65F6\u95F4\uFF08\u6BEB\u79D2\u65F6\u95F4\u6233\uFF09 */\n generatedAt: number;\n participants: ScoreboardReportParticipant[];\n objectives: ScoreboardReportObjective[];\n displaySlots: ScoreboardReportDisplay[];\n truncated: boolean;\n}\n\n/** \u89E3\u6790\u540E\u7684\u4E0A\u62A5\uFF1A\u7A7A\u95F2\u63A2\u8BE2 / \u5185\u5BB9\u672A\u53D8 / \u5B8C\u6574\u5FEB\u7167 */\nexport type ScoreboardReport =\n | { kind: \"idle\" }\n | { kind: \"same\"; hash: string; generatedAt: number }\n | ({ kind: \"full\" } & ScoreboardSnapshot);\n\n/** \u4E0A\u62A5\u7684\u54CD\u5E94\uFF1A\u662F\u5426\u6709\u4EBA\u5728\u770B\uFF08\u51B3\u5B9A\u811A\u672C\u662F\u5426\u8BA1\u7B97\u5FEB\u7167\uFF09\uFF0C\u4EE5\u53CA\u662F\u5426\u9700\u8981\u8865\u53D1\u5B8C\u6574\u5FEB\u7167 */\nexport interface ScoreboardReportReply {\n watching: boolean;\n needFull: boolean;\n}\n\n/** \u4E0E @sfmc-bds/management \u7684 ScoreboardReadResult \u540C\u6784\uFF1BCLI \u4FA7\u7531\u7C7B\u578B\u68C0\u67E5\u4FDD\u8BC1\u4E24\u8FB9\u4E0D\u6F02\u79FB */\nexport interface ScoreboardReadView {\n available: boolean;\n fresh: boolean;\n updatedAt: number | null;\n objectives: Array<{\n id: string;\n displayName: string;\n participants: Array<ScoreboardReportParticipant & { score: number }>;\n }>;\n displaySlots: ScoreboardReportDisplay[];\n truncated: boolean;\n note?: string;\n /**\n * \u6E38\u620F\u5185\u811A\u672C\u6700\u8FD1\u662F\u5426\u4E0A\u62A5\u8FC7\uFF08\u542B\u7A7A\u95F2\u63A2\u8BE2\uFF09\u3002\u4E0D\u5C5E\u4E8E\u7BA1\u7406\u534F\u8BAE\uFF0CCLI \u7528\u6765\u5224\u65AD\u662F\u5426\u503C\u5F97\u7B49\u5F85\u65B0\u5FEB\u7167\uFF1A\n * \u811A\u672C\u6CA1\u5728\u4E0A\u62A5\uFF08BDS \u672A\u8FD0\u884C\u6216\u5E73\u53F0\u811A\u672C\u672A\u52A0\u8F7D\uFF09\u65F6\u76F4\u63A5\u8FD4\u56DE\uFF0C\u4E0D\u7A7A\u7B49\u3002\n */\n scriptConnected: boolean;\n}\n\n/**\n * \u4E0A\u62A5\u4F53\u79EF\u4E0A\u9650\uFF1A\u8D85\u8FC7\u5373\u89C6\u4E3A\u5F02\u5E38\u8BF7\u6C42\u62D2\u6536\u3002\u6E38\u620F\u5185\u811A\u672C\u6709\u81EA\u5DF1\u7684\u66F4\u5C0F\u4E0A\u9650\uFF08\u8D85\u51FA\u65F6\u622A\u65AD\u5E76\u6807\u8BB0 truncated\uFF09\uFF0C\n * \u8FD9\u91CC\u53EA\u9632\u6B62\u5F02\u5E38\u8BF7\u6C42\u5360\u6EE1\u5185\u5B58\u3002\n */\nexport const SCOREBOARD_REPORT_LIMITS = { objectives: 2000, participants: 100_000, scores: 500_000, name: 512 } as const;\n/** \u4E00\u6B21\u8BFB\u53D6\u540E\u4FDD\u6301\u300C\u6709\u4EBA\u5728\u770B\u300D\u7684\u65F6\u957F */\nexport const SCOREBOARD_WATCH_MS = 30_000;\n/** \u5FEB\u7167\u5728\u6700\u540E\u4E00\u6B21\u786E\u8BA4\u540E\u591A\u4E45\u5185\u89C6\u4E3A\u65B0\u9C9C\uFF08\u811A\u672C\u7EA6 2 \u79D2\u786E\u8BA4\u4E00\u6B21\uFF0C\u7559\u51FA\u51E0\u6B21\u5931\u8D25\u7684\u4F59\u91CF\uFF09 */\nexport const SCOREBOARD_FRESH_MS = 10_000;\n\nconst PARTICIPANT_TYPES = new Set<string>([\"player\", \"fakePlayer\", \"entity\"]);\nconst DISPLAY_SLOTS = new Set<string>([\"sidebar\", \"list\", \"belowName\"]);\n\n/** \u6709\u9650\u957F\u5EA6\u7684\u6587\u672C */\nconst isText = (value: unknown, allowEmpty = true): value is string =>\n typeof value === \"string\" && value.length <= SCOREBOARD_REPORT_LIMITS.name && (allowEmpty || value.length > 0);\n/** 32 \u4F4D\u6709\u7B26\u53F7\u6574\u6570\uFF08\u8BA1\u5206\u677F\u5206\u6570\u4E0E\u8EAB\u4EFD ID \u7684\u53D6\u503C\u8303\u56F4\uFF09 */\nconst isInt32 = (value: unknown): value is number =>\n typeof value === \"number\" && Number.isInteger(value) && value >= -2147483648 && value <= 2147483647;\n/** \u5408\u7406\u7684\u65F6\u95F4\u6233\uFF1B\u7F3A\u7701\u65F6\u7528\u63A5\u6536\u65F6\u95F4 */\nconst timestamp = (value: unknown, now: number): number =>\n typeof value === \"number\" && Number.isSafeInteger(value) && value > 0 ? value : now;\n\n/**\n * \u6821\u9A8C\u5E76\u89E3\u6790\u4E00\u6B21\u4E0A\u62A5\uFF1B\u683C\u5F0F\u4E0D\u5BF9\u65F6\u8FD4\u56DE null\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1APOST /api/sfmc/scoreboard/live \u6536\u5230\u7684\u8BF7\u6C42\u4F53\u3002\n */\nexport function parseScoreboardReport(data: Record<string, unknown>, now = Date.now()): ScoreboardReport | null {\n if (data.kind === \"idle\") return { kind: \"idle\" };\n if (!isText(data.hash, false)) return null;\n if (data.kind === \"same\") return { kind: \"same\", hash: data.hash, generatedAt: timestamp(data.generatedAt, now) };\n if (data.kind !== \"full\") return null;\n\n const rawParticipants = data.participants;\n const rawObjectives = data.objectives;\n const rawDisplay = data.displaySlots ?? [];\n if (!Array.isArray(rawParticipants) || rawParticipants.length > SCOREBOARD_REPORT_LIMITS.participants) return null;\n if (!Array.isArray(rawObjectives) || rawObjectives.length > SCOREBOARD_REPORT_LIMITS.objectives) return null;\n if (!Array.isArray(rawDisplay) || rawDisplay.length > DISPLAY_SLOTS.size) return null;\n\n const participants: ScoreboardReportParticipant[] = [];\n const known = new Set<number>();\n for (const row of rawParticipants) {\n if (!row || typeof row !== \"object\") return null;\n const { id, name, type, online } = row as Record<string, unknown>;\n if (!isInt32(id) || known.has(id) || !isText(name) || typeof type !== \"string\" || !PARTICIPANT_TYPES.has(type) || typeof online !== \"boolean\") return null;\n known.add(id);\n participants.push({ id, name, type: type as ScoreboardParticipantType, online });\n }\n\n const objectives: ScoreboardReportObjective[] = [];\n const objectiveIds = new Set<string>();\n let scoreCount = 0;\n for (const row of rawObjectives) {\n if (!row || typeof row !== \"object\") return null;\n const { id, displayName, scores } = row as Record<string, unknown>;\n if (!isText(id, false) || objectiveIds.has(id) || !isText(displayName) || !Array.isArray(scores)) return null;\n scoreCount += scores.length;\n if (scoreCount > SCOREBOARD_REPORT_LIMITS.scores) return null;\n const pairs: Array<[number, number]> = [];\n for (const pair of scores) {\n if (!Array.isArray(pair) || pair.length !== 2 || !known.has(pair[0]) || !isInt32(pair[1])) return null;\n pairs.push([pair[0], pair[1]]);\n }\n objectiveIds.add(id);\n objectives.push({ id, displayName, scores: pairs });\n }\n\n const displaySlots: ScoreboardReportDisplay[] = [];\n for (const row of rawDisplay) {\n if (!row || typeof row !== \"object\") return null;\n const { slot, objectiveId, sortOrder } = row as Record<string, unknown>;\n if (typeof slot !== \"string\" || !DISPLAY_SLOTS.has(slot) || !isText(objectiveId, false)) return null;\n if (sortOrder !== \"ascending\" && sortOrder !== \"descending\") return null;\n displaySlots.push({ slot: slot as ScoreboardDisplaySlot, objectiveId, sortOrder });\n }\n\n return {\n kind: \"full\",\n hash: data.hash,\n generatedAt: timestamp(data.generatedAt, now),\n participants,\n objectives,\n displaySlots,\n truncated: data.truncated === true,\n };\n}\n\n/**\n * \u5185\u5B58\u4E2D\u7684\u8BA1\u5206\u677F\u5FEB\u7167\u4E0E\u5173\u6CE8\u72B6\u6001\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1Aroutes/scoreboard.ts \u6BCF\u4E2A\u6570\u636E\u670D\u52A1\u8FDB\u7A0B\u6301\u6709\u4E00\u4E2A\u5B9E\u4F8B\u3002\n */\nexport class ScoreboardStore {\n private snapshot: ScoreboardSnapshot | null = null;\n /** \u811A\u672C\u6700\u540E\u4E00\u6B21\u786E\u8BA4\u5FEB\u7167\u4E0E\u4E16\u754C\u4E00\u81F4\u7684\u65F6\u95F4\uFF08\u6E38\u620F\u5185\u65F6\u95F4\u6233\uFF09 */\n private verifiedAt = 0;\n /** \u6570\u636E\u670D\u52A1\u6700\u540E\u4E00\u6B21\u6536\u5230\u786E\u8BA4\u7684\u65F6\u95F4\uFF08\u672C\u673A\u65F6\u95F4\uFF09\uFF0C\u7528\u4E8E\u5224\u65AD\u65B0\u9C9C\u5EA6 */\n private confirmedAt = 0;\n /** \u5728\u6B64\u4E4B\u524D\u89C6\u4E3A\u6709\u4EBA\u5728\u770B */\n private watchUntil = 0;\n /** \u6700\u540E\u4E00\u6B21\u6536\u5230\u4EFB\u610F\u4E0A\u62A5\u7684\u65F6\u95F4\uFF08\u672C\u673A\u65F6\u95F4\uFF09\uFF1B\u4ECE\u672A\u6536\u5230\u65F6\u4E3A null */\n private reportedAt: number | null = null;\n\n /** \u5904\u7406\u4E00\u6B21\u4E0A\u62A5\uFF0C\u8FD4\u56DE\u7ED9\u811A\u672C\u7684\u6307\u793A */\n report(report: ScoreboardReport, now = Date.now()): ScoreboardReportReply {\n this.reportedAt = now;\n if (report.kind === \"full\") {\n const { kind: _kind, ...snapshot } = report;\n this.snapshot = snapshot;\n this.verifiedAt = snapshot.generatedAt;\n this.confirmedAt = now;\n } else if (report.kind === \"same\") {\n if (!this.snapshot || this.snapshot.hash !== report.hash) return { watching: this.watching(now), needFull: true };\n this.verifiedAt = Math.max(this.verifiedAt, report.generatedAt);\n this.confirmedAt = now;\n }\n return { watching: this.watching(now), needFull: false };\n }\n\n /** \u8BFB\u53D6\u5FEB\u7167\u5E76\u5EF6\u957F\u5173\u6CE8\u671F\uFF0C\u4F7F\u811A\u672C\u7EE7\u7EED\u540C\u6B65 */\n read(now = Date.now()): ScoreboardReadView {\n this.watchUntil = now + SCOREBOARD_WATCH_MS;\n const snapshot = this.snapshot;\n const fresh = snapshot !== null && now - this.confirmedAt <= SCOREBOARD_FRESH_MS;\n const scriptConnected = this.reportedAt !== null && now - this.reportedAt <= SCOREBOARD_FRESH_MS;\n if (!snapshot) {\n return {\n available: false,\n fresh: false,\n updatedAt: null,\n objectives: [],\n displaySlots: [],\n truncated: false,\n note: scriptConnected ? \"\u6B63\u5728\u7B49\u5F85\u6E38\u620F\u5185\u540C\u6B65\u8BA1\u5206\u677F\" : \"\u6E38\u620F\u5185\u5E73\u53F0\u811A\u672C\u672A\u8FDE\u63A5\uFF0C\u8BF7\u786E\u8BA4 BDS \u6B63\u5728\u8FD0\u884C\",\n scriptConnected,\n };\n }\n const byId = new Map(snapshot.participants.map((row) => [row.id, row]));\n return {\n available: true,\n fresh,\n updatedAt: this.verifiedAt,\n objectives: snapshot.objectives.map((objective) => ({\n id: objective.id,\n displayName: objective.displayName,\n participants: objective.scores.map(([id, score]) => ({ ...byId.get(id)!, score })),\n })),\n displaySlots: snapshot.displaySlots,\n truncated: snapshot.truncated,\n ...(fresh ? {} : { note: scriptConnected ? \"\u8BA1\u5206\u677F\u5FEB\u7167\u5DF2\u8FC7\u671F\uFF0C\u6B63\u5728\u7B49\u5F85\u6E38\u620F\u5185\u91CD\u65B0\u540C\u6B65\" : \"\u6E38\u620F\u5185\u5E73\u53F0\u811A\u672C\u672A\u8FDE\u63A5\uFF0C\u663E\u793A\u7684\u662F\u6700\u540E\u4E00\u6B21\u540C\u6B65\u7684\u8BA1\u5206\u677F\" }),\n scriptConnected,\n };\n }\n\n private watching(now: number): boolean {\n return now < this.watchUntil;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAiBH,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAiBH,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAqC1C,QAAA,MAAM,GAAG,8BAAY,CAAC;AACtB,QAAA,MAAM,EAAE,oCAA4B,CAAC;AAGrC,QAAA,MAAM,KAAK,mCAAkB,CAAC;AAwB9B,QAAA,MAAM,gBAAgB,8DAA0E,CAAC;AAQjG,QAAA,MAAM,UAAU,0CAId,CAAC;AAQH,QAAA,MAAM,cAAc,gBAAyB,CAAC;AAC9C,QAAA,MAAM,eAAe,iBAAwB,CAAC;AAE9C,QAAA,MAAM,QAAQ,UAMZ,CAAC;AAkiBH,OAAO,EAAE,EAAE,EAAE,gBAAgB,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,QAAQ,EAAE,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,8 @@ import { createWechatBindProvider } from "./domain/wechat-bind.js";
|
|
|
40
40
|
import { createBindConfirmRoutes } from "./routes/bind-confirm.js";
|
|
41
41
|
import { createQqBindRoutes } from "./routes/qq-bind.js";
|
|
42
42
|
import { createWechatBindRoutes } from "./routes/wechat-bind.js";
|
|
43
|
+
import { createWechatActivityRoutes } from "./routes/wechat-activity.js";
|
|
44
|
+
import { createScoreboardRoutes } from "./routes/scoreboard.js";
|
|
43
45
|
import { createQqEventsRoutes } from "./routes/qq-events.js";
|
|
44
46
|
import { createQqJoinRoutes } from "./routes/qq-join.js";
|
|
45
47
|
import { createStatusRoutes } from "./routes/status.js";
|
|
@@ -199,6 +201,7 @@ function setModuleEnabled(mod, enabled) {
|
|
|
199
201
|
const healthRoutes = createHealthRoutes();
|
|
200
202
|
const metricsDb = openDatabase(path.join(path.dirname(env.DB_PATH), "runtime-metrics.sqlite"));
|
|
201
203
|
const statusRoutes = createStatusRoutes({ query: createQuery(metricsDb) });
|
|
204
|
+
const scoreboardRoutes = createScoreboardRoutes();
|
|
202
205
|
for (const name of PLATFORM_MONITORING_SERVICES) serviceRegistry.registerHandler(PLATFORM_SERVICE_OWNER, name, async () => {
|
|
203
206
|
const response = await fetch(`http://127.0.0.1:${env.PORT}/api/sfmc/metrics`, { signal: AbortSignal.timeout(1e4) });
|
|
204
207
|
if (!response.ok) throw new Error("\u5E73\u53F0\u8FD0\u884C\u6307\u6807\u8BFB\u53D6\u5931\u8D25");
|
|
@@ -230,6 +233,7 @@ const wechatBindRoutes = createWechatBindRoutes({
|
|
|
230
233
|
player
|
|
231
234
|
)
|
|
232
235
|
});
|
|
236
|
+
const wechatActivityRoutes = createWechatActivityRoutes({ query, json });
|
|
233
237
|
const QQ_LINK_CONFIG_KEY = "qq_link";
|
|
234
238
|
const QQ_LINK_DEFAULTS = {
|
|
235
239
|
allowlist_enabled: true,
|
|
@@ -478,9 +482,11 @@ async function handle(req, res) {
|
|
|
478
482
|
if (await moduleRoutesInstance(ctxBase)) return;
|
|
479
483
|
if (await healthRoutes(ctxBase)) return;
|
|
480
484
|
if (await statusRoutes(ctxBase)) return;
|
|
485
|
+
if (await scoreboardRoutes(ctxBase)) return;
|
|
481
486
|
if (await qqBindRoutes(ctxBase)) return;
|
|
482
487
|
if (await bindConfirmRoutes(ctxBase)) return;
|
|
483
488
|
if (await wechatBindRoutes(ctxBase)) return;
|
|
489
|
+
if (await wechatActivityRoutes(ctxBase)) return;
|
|
484
490
|
if (await qqJoinRoutes(ctxBase)) return;
|
|
485
491
|
if (await qqEventsRoutes(ctxBase)) return;
|
|
486
492
|
if (await messagesRoutes(ctxBase)) return;
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n *\n * \u5DE5\u4F5C\u6D41:\n * 1. \u52A0\u8F7D env\n * 2. \u6821\u9A8C Node \u7248\u672C\n * 3. openDatabase + createPlatformTables(sfmc__audit / sfmc__idempotent)\n * 4. loadManifestV2() \u2014 \u5931\u8D25 = \u542F\u52A8\u5931\u8D25\n * 5. filterEnabled(loaded, lockFileEnabled)\n * 6. buildModuleAuth({auth_token, enabled_modules}) \u2190 \u4E0E DB \u540C\u76EE\u5F55 module-tokens.json\n * 7. \u5B9E\u4F8B\u5316:SchemaRegistry / ServiceRegistry / IdempotencyStore / TxRunner\n * 8. \u88C5\u914D v2 \u8DEF\u7531 (/api/sfmc/db/* + /api/sfmc/services* + /api/sfmc/configs/:key/*)\n * \u53E6\u4FDD\u7559 messages(qq-bridge) + config/modules/health \u5E73\u53F0\u8DEF\u7531\n * 9. createServer + listen\n *\n * \u9274\u6743(handle \u5C42):\n * - /api/sfmc/db/*, /api/sfmc/services*, /api/sfmc/configs/:key(/set|notify)*\n * \u2192 module token (Authorization: Bearer ...) + ?moduleId=...\n * - \u5176\u4ED6:env.AUTH_TOKEN \u65E7 auth(NEEDS_AUTH)\n */\n\nimport http from \"node:http\";\nimport { PLATFORM_MONITORING_SERVICES, PLATFORM_SERVICE_OWNER } from \"@sfmc-bds/sdk/contracts\";\nimport { SQL } from \"sql-template-strings\";\n\nimport { createPlatformTables } from \"./db-tables.js\";\nimport { syncPlayerSession } from \"./domain/player-session.js\";\nimport { initSchema } from \"./domain/schema.js\";\nimport { loadEnv } from \"./env.js\";\nimport { createIdempotencyStore } from \"./lib/idempotency-store.js\";\nimport { log } from \"./lib/log.js\";\nimport { assertNodeVersion } from \"./lib/runtime.js\";\nimport { createQuery, openDatabase } from \"./lib/sqlite.js\";\nimport { loadManifestV2 } from \"./manifest-loader.js\";\nimport { buildModuleAuth, ensureModuleToken, persistModuleAuth, verifyModuleAuth } from \"./module-auth.js\";\nimport { syncModuleRuntimeState } from \"./module-runtime-sync.js\";\nimport { SchemaRegistry } from \"./schema-registry.js\";\nimport { createServer, startConsole } from \"./server.js\";\nimport { ServiceRegistry } from \"./service-registry.js\";\nimport { registerEnabledBuiltinServices } from \"./services/builtin-handlers.js\";\nimport { TxRunner } from \"./tx-runner.js\";\n\nimport { configPath, ensureJson, patchJson, readJson } from \"@sfmc-bds/sdk/node/config\";\nimport path, { join } from \"node:path\";\n\nimport { jsonV2Fail } from \"./routes/_shared.js\";\nimport { createDbRoutes } from \"./routes/db-routes.js\";\nimport { createModuleConfigRoutes } from \"./routes/module-config-routes.js\";\nimport { createServiceRoutes } from \"./routes/service-routes.js\";\n\nimport { forwardToQQBridge, makeOutboundConfig } from \"./domain/bridge.js\";\nimport { loadContentSnapshot } from \"./domain/installed-content.js\";\nimport { createQqEventsDispatcher, resolveQqEventsConfig, type ResolvedQqEventsConfig } from \"./domain/qq-events.js\";\nimport { body as sharedBody, json as sharedJson } from \"./lib/http.js\";\nimport { isEnabled, loadModuleLock, saveModuleLock, updateModuleState } from \"./lib/module-state.js\";\nimport { createConfigRoutes } from \"./routes/config.js\";\nimport { createContentRoutes } from \"./routes/content.js\";\nimport { createHealthRoutes } from \"./routes/health.js\";\nimport { createMessagesRoutes, gameChatForwardPayload, type ChatChannelQQFlags } from \"./routes/messages.js\";\nimport { createModuleRoutes } from \"./routes/modules.js\";\nimport { codeTakenBy, type BindProvider } from \"./domain/bind-providers.js\";\nimport { resolvePlayerLevel } from \"./domain/player-level.js\";\nimport { createQqBindProvider } from \"./domain/qq-bind.js\";\nimport { createWechatBindProvider } from \"./domain/wechat-bind.js\";\nimport { createBindConfirmRoutes } from \"./routes/bind-confirm.js\";\nimport { createQqBindRoutes } from \"./routes/qq-bind.js\";\nimport { createWechatBindRoutes } from \"./routes/wechat-bind.js\";\nimport { createQqEventsRoutes } from \"./routes/qq-events.js\";\nimport { createQqJoinRoutes, type JoinFeatureFlags } from \"./routes/qq-join.js\";\nimport { createStatusRoutes } from \"./routes/status.js\";\n\nif (!assertNodeVersion(22, 13)) {\n process.exit(2);\n}\n\nconst env = loadEnv();\nconst db = openDatabase(env.DB_PATH);\ncreatePlatformTables(db); // sfmc__audit / sfmc__idempotent\ninitSchema(db); // \u5E73\u53F0\u4E1A\u52A1 bootstrap \u8868(players/world/chat/...)\u2014\u2014qq-bridge \u5728 SAPI defineTable \u4E4B\u524D\u5C31\u9700\u8981\nconst query = createQuery(db);\n\n// \u2500\u2500 v2 manifest \u52A0\u8F7D(\u5931\u8D25 = \u542F\u52A8\u5931\u8D25)\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst loadedManifest = loadManifestV2(); // throws on violation\nlog.success(\n `[manifest v2] loaded ${Object.keys(loadedManifest.modules).length} modules; provides ${loadedManifest.providesMap.size} services`\n);\n\n// \u2500\u2500 enabled \u96C6\u5408(\u4ECE lock file)\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst lockFile = loadModuleLock(env.MODULE_LOCK_PATH);\nconst moduleCatalog = readJson<{ modules?: unknown[] }>(env.MODULE_CATALOG_PATH) ?? { modules: [] };\nconst catalogIds = new Set(\n Array.isArray(moduleCatalog.modules)\n ? (moduleCatalog.modules as Array<{ id?: string }>).map((m) => String(m.id || \"\")).filter((id) => id.length > 0)\n : []\n);\nconst enabledSet = new Set<string>();\nfor (const id of Object.keys(loadedManifest.modules)) {\n if (!catalogIds.has(id)) continue;\n const catalogEntry = (moduleCatalog.modules as Array<Record<string, unknown>>).find((m) => String(m.id) === id);\n if (!catalogEntry) continue;\n const defaultEnabled = catalogEntry.enabledByDefault !== false;\n if (isEnabled(lockFile, id, defaultEnabled)) enabledSet.add(id);\n}\nconst enabledManifests = new Map<string, NonNullable<(typeof loadedManifest.modules)[string]>>();\nfor (const id of enabledSet) {\n const m = loadedManifest.modules[id];\n if (m) enabledManifests.set(id, m);\n}\nlog.info(`[manifest v2] enabled: ${[...enabledSet].sort().join(\", \") || \"(none)\"}`);\n\n// \u2500\u2500 \u6A21\u5757 HMAC token map(\u4E0E DB \u540C\u76EE\u5F55 module-tokens.json)\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst moduleAuth = buildModuleAuth({\n dbPath: env.DB_PATH,\n envAuthToken: env.AUTH_TOKEN,\n enabledModuleIds: [...enabledSet],\n});\n// \u5E73\u53F0\u5BBF\u4E3B\uFF08QQ \u7ED1\u5B9A/\u8D26\u53F7\u5FEB\u7167\uFF09\u9700\u8981\u7A33\u5B9A token\uFF0C\u7ECF configs/all.module_tokens \u4E0B\u53D1\u5230 BDS\nif (ensureModuleToken(moduleAuth, PLATFORM_SERVICE_OWNER)) {\n persistModuleAuth(env.DB_PATH, moduleAuth, env.AUTH_TOKEN);\n log.info(`[auth] \u5DF2\u6D3E\u751F\u5E73\u53F0\u5BBF\u4E3B token: ${PLATFORM_SERVICE_OWNER}`);\n}\n\n// \u2500\u2500 \u4E09\u4EF6\u5957 + \u8DEF\u7531\u5DE5\u5382 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst schemaRegistry = new SchemaRegistry(db);\nconst serviceRegistry = new ServiceRegistry();\nconst idempotent = createIdempotencyStore(db);\nconst txRunner = new TxRunner({\n db,\n query,\n schema: schemaRegistry,\n serviceRegistry,\n enabled: enabledManifests,\n});\n\n// \u2500\u2500 \u8FDB\u7A0B\u5185\u7F6E service handler(\u6269\u5C55\u70B9:BUILTIN_SERVICE_PLUGINS) \u2500\u2500\n{\n const plugins = registerEnabledBuiltinServices(serviceRegistry, { query, db }, enabledSet);\n if (plugins > 0) {\n log.success(`[service] registered ${serviceRegistry.list().length} handlers from ${plugins} builtin plugin(s)`);\n }\n}\n\nconst json = sharedJson;\nconst body = sharedBody;\n\n// \u2500\u2500 \u5DE5\u5177\u51FD\u6570 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction loadModuleCatalog() {\n return Array.isArray(moduleCatalog.modules) ? moduleCatalog.modules : [];\n}\n\nfunction buildModuleList() {\n const catalog = loadModuleCatalog();\n return catalog\n .map((raw) => {\n const entry =\n (raw as Record<string, unknown>).entry && typeof (raw as Record<string, unknown>).entry === \"object\"\n ? ((raw as Record<string, unknown>).entry as Record<string, unknown>)\n : {};\n const id = String((raw as Record<string, unknown>).id || \"\").trim();\n const configKey = String(\n (raw as Record<string, unknown>).configKey || (raw as Record<string, unknown>).config_key || \"\"\n ).trim();\n if (!id || !configKey) return null;\n const state = lockFile.modules[id];\n const enabled = isEnabled(lockFile, id, (raw as Record<string, unknown>).enabledByDefault !== false);\n return {\n id,\n module_id: id,\n name: configKey,\n configKey,\n config_key: configKey,\n display_name: String((raw as Record<string, unknown>).name || configKey),\n description: String((raw as Record<string, unknown>).description || \"\"),\n default_enabled: (raw as Record<string, unknown>).enabledByDefault !== false,\n can_disable: moduleCanDisable(raw as Record<string, unknown>),\n // ConfigManager \u8BA4 installed!==false;\u5DF2\u88C5\u5305\u9ED8\u8BA4 true\n installed: true,\n requires: Array.isArray((raw as Record<string, unknown>).requires)\n ? ((raw as Record<string, unknown>).requires as unknown[]).filter(Boolean).map(String)\n : [],\n optional: Array.isArray((raw as Record<string, unknown>).optional)\n ? ((raw as Record<string, unknown>).optional as unknown[]).filter(Boolean).map(String)\n : [],\n commands: Array.isArray((raw as Record<string, unknown>).commands)\n ? ((raw as Record<string, unknown>).commands as unknown[]).filter(Boolean).map(String)\n : [],\n entry: {\n kind: String(entry.kind || \"\"),\n path: String(entry.path || \"\"),\n init: String(entry.init || \"\"),\n },\n updated_at: state?.updatedAt ?? null,\n enabled: !!enabled,\n };\n })\n .filter(Boolean);\n}\n\n/**\n * QQ\u300C\u6A21\u5757 / \u8D44\u6E90\u5305\u300D\u5FEB\u7167\u3002\u6BCF\u6B21\u8BF7\u6C42\u73B0\u8BFB catalog \u4E0E\u78C1\u76D8\uFF0C\u88C5\u5305\u540E\u4E0D\u7528\u91CD\u542F db-server\u3002\n */\nfunction loadContentSnapshotForRequest() {\n const modules = (buildModuleList() ?? []).flatMap((raw) => {\n if (!raw || typeof raw !== \"object\") return [];\n const row = raw as { id?: unknown; display_name?: unknown; enabled?: unknown };\n const id = String(row.id ?? \"\").trim();\n if (!id) return [];\n return [\n {\n id,\n display_name: String(row.display_name || id),\n enabled: row.enabled !== false,\n },\n ];\n });\n return loadContentSnapshot({\n packagesDir: join(env.MODULES_DIR, \"packages\"),\n modules,\n bdsRoot: currentBdsRoot(),\n });\n}\n\n/**\n * BDS \u6839\u76EE\u5F55\uFF08configs/bds_updater.json \u7684 bds_path\uFF0C\u76F8\u5BF9\u8DEF\u5F84\u6309\u9879\u76EE\u6839\u89E3\u6790\uFF09\uFF1B\u672A\u914D\u7F6E\u65F6\u4E3A null\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1A\u5185\u5BB9\u5FEB\u7167\u8BFB\u53D6\u5DF2\u88C5\u5305\uFF0C\u5FAE\u4FE1\u7ED1\u5B9A\u67E5\u8BE2\u8BFB\u53D6 BDS permissions.json \u8BA1\u7B97\u73A9\u5BB6\u539F\u751F\u7B49\u7EA7\uFF08DRY\uFF1A\u540C\u4E00\u5904\u89E3\u6790\uFF09\u3002\n */\nfunction currentBdsRoot(): string | null {\n const cfg = readJson<{ bds_path?: string }>(configPath(env.PROJECT_ROOT, \"bds_updater.json\"));\n const rawBds = typeof cfg?.bds_path === \"string\" ? cfg.bds_path.trim() : \"\";\n return rawBds ? (path.isAbsolute(rawBds) ? rawBds : path.resolve(env.PROJECT_ROOT, rawBds)) : null;\n}\n\n/** catalog \u7701\u7565 canDisable \u65F6\u9ED8\u8BA4\u5141\u8BB8\u7981\u7528(\u4E0E buildModuleList.can_disable \u540C\u6E90)\u3002 */\nfunction moduleCanDisable(raw: Record<string, unknown>): boolean {\n return raw.canDisable !== false;\n}\n\nfunction resolveModuleByKey(key: string) {\n const k = String(key || \"\").trim();\n const catalog = loadModuleCatalog();\n const raw = catalog.find(\n (m) =>\n String((m as Record<string, unknown>).id || \"\") === k ||\n String((m as Record<string, unknown>).configKey || (m as Record<string, unknown>).config_key || \"\") === k\n ) as Record<string, unknown> | undefined;\n if (!raw) return null;\n const id = String(raw.id || \"\").trim();\n const configKey = String(raw.configKey || raw.config_key || \"\").trim();\n if (!id || !configKey) return null;\n /* LSP:\u4E0E buildModuleList.can_disable \u540C\u6E90 \u2014 \u7701\u7565\u5B57\u6BB5\u89C6\u4E3A\u53EF\u7981\u7528 */\n return { id, configKey, canDisable: moduleCanDisable(raw) };\n}\n\nfunction setModuleEnabled(mod: { id: string; canDisable: boolean }, enabled: boolean) {\n // \u76F4\u63A5\u66F4\u65B0\u542F\u52A8\u65F6\u7F13\u5B58\u7684 lockFile(\u800C\u975E\u53E6\u8BFB\u4E00\u4EFD\u65B0\u526F\u672C),\n // \u5426\u5219 buildModuleList() \u8BFB\u7684\u4ECD\u662F\u65E7\u7F13\u5B58,\u5BFC\u81F4\u542F\u505C\u540E enabled \u72B6\u6001\u4E0D\u7FFB\u8F6C\u3002\n updateModuleState(lockFile, mod.id, { enabled: !!enabled });\n // DRY:\u4E0E loadModuleLock \u5BF9\u79F0\u8D70 saveModuleLock,\u52FF\u6563\u843D writeJson\n saveModuleLock(env.MODULE_LOCK_PATH, lockFile);\n\n // DIP:\u70ED\u540C\u6B65 enabledSet / tokens / manifests / builtin handlers(\u4E0D\u91CD\u542F db-server)\n syncModuleRuntimeState({\n moduleId: mod.id,\n enabled: !!enabled,\n dbPath: env.DB_PATH,\n envAuthToken: env.AUTH_TOKEN,\n enabledSet,\n enabledManifests,\n loadedManifest,\n moduleAuth,\n serviceRegistry,\n builtinDeps: { query, db },\n });\n}\n\n// \u2500\u2500 \u5E73\u53F0\u8DEF\u7531(\u975E\u6A21\u5757\u4E1A\u52A1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst healthRoutes = createHealthRoutes();\n// \u6307\u6807\u72EC\u7ACB\u843D\u5E93\uFF0C\u907F\u514D\u5199\u5165\u4E1A\u52A1\u6A21\u5757\u6B63\u5728\u6301\u6709\u7684\u8DE8\u8BF7\u6C42\u4E8B\u52A1\u3002\nconst metricsDb = openDatabase(path.join(path.dirname(env.DB_PATH), \"runtime-metrics.sqlite\"));\nconst statusRoutes = createStatusRoutes({ query: createQuery(metricsDb) });\nfor (const name of PLATFORM_MONITORING_SERVICES) serviceRegistry.registerHandler(PLATFORM_SERVICE_OWNER, name, async () => {\n const response = await fetch(`http://127.0.0.1:${env.PORT}/api/sfmc/metrics`, { signal: AbortSignal.timeout(10_000) });\n if (!response.ok) throw new Error(\"\u5E73\u53F0\u8FD0\u884C\u6307\u6807\u8BFB\u53D6\u5931\u8D25\");\n const result = await response.json() as { current: { tps: number | null; chunkEstimate: number | null } | null };\n const tps = result.current?.tps ?? null;\n const grade = tps === null ? \"unknown\" : tps >= 19.5 ? \"green\" : tps >= 15 ? \"yellow\" : tps >= 10 ? \"gold\" : \"red\";\n if (name === \"tps.current\") return tps;\n if (name === \"tps.status\") return { tps, grade, text: `\u00A77[TPS] \u00A7f${tps?.toFixed(2) ?? \"\u672A\u77E5\"} \u00A77/ 20.00` };\n return result.current ? { ...result.current, grade, totalLoadedChunks: result.current.chunkEstimate } : null;\n});\n/**\n * \u5DF2\u6CE8\u518C\u7684\u5916\u90E8\u8D26\u53F7\u7ED1\u5B9A\u6E20\u9053\uFF08QQ\u3001\u5FAE\u4FE1\uFF09\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1A\u7EDF\u4E00\u786E\u8BA4\u5165\u53E3\u6309\u987A\u5E8F\u8BE2\u95EE\uFF1B\u4E24\u4E2A\u6E20\u9053\u7533\u8BF7\u77ED\u7801\u65F6\u5171\u7528 isBindCodeTaken \u8DE8\u6E20\u9053\u67E5\u91CD\u3002\u65B0\u589E\u6E20\u9053\u5728\u6B64\u8FFD\u52A0\uFF08OCP\uFF09\u3002\n */\nconst bindProviders: readonly BindProvider[] = [createQqBindProvider(query), createWechatBindProvider(query)];\nconst isBindCodeTaken = codeTakenBy(() => bindProviders);\nconst qqBindRoutes = createQqBindRoutes({ query, body, json, isCodeTaken: isBindCodeTaken });\nconst bindConfirmRoutes = createBindConfirmRoutes({ body, json, providers: () => bindProviders });\nconst wechatBindRoutes = createWechatBindRoutes({\n query,\n body,\n json,\n isCodeTaken: isBindCodeTaken,\n // \u6BCF\u6B21\u73B0\u8BFB\u4E24\u4EFD\u6743\u9650\u6587\u4EF6\uFF1A\u7BA1\u7406\u5458\u8C03\u6574\u6743\u9650\u540E\uFF0C\u5C0F\u7A0B\u5E8F\u7BA1\u7406\u7AEF\u6388\u6743\u7ACB\u5373\u968F\u4E4B\u53D8\u5316\n resolveLevel: (player) =>\n resolvePlayerLevel(\n {\n readSfmcPermissions: () => readJson(configPath(env.PROJECT_ROOT, \"permissions.json\")),\n readBdsPermissions: () => {\n const bdsRoot = currentBdsRoot();\n return bdsRoot ? readJson(join(bdsRoot, \"permissions.json\")) : null;\n },\n },\n player\n ),\n});\n\n/** \u5E73\u53F0 QQ \u7ED1\u5B9A\u76F8\u5173\u914D\u7F6E\uFF08\u975E SDK qq_config\uFF1B\u539F qq-link \u6A21\u5757\u914D\u7F6E\u952E\uFF09 */\nconst QQ_LINK_CONFIG_KEY = \"qq_link\";\nconst QQ_LINK_DEFAULTS = {\n allowlist_enabled: true,\n require_approval: true,\n /** \u662F\u5426\u5C06 QQ \u7FA4\u4E3B/\u7FA4\u7BA1\u89C6\u4F5C SFMC \u7BA1\u7406\u5458\uFF1B\u7F3A\u7701\u5F00\u542F\uFF0C\u4EC5\u6539\u672C\u6587\u4EF6\u53EF\u5173\u95ED\uFF0CAPI/\u7FA4\u804A\u4E0D\u53EF\u5199 */\n treat_group_admins_as_admins: true,\n} as const;\n\nfunction asConfigBool(v: unknown, fallback: boolean): boolean {\n if (v === undefined || v === null) return fallback;\n if (typeof v === \"boolean\") return v;\n const s = String(v).trim().toLowerCase();\n if (s === \"true\" || s === \"1\" || s === \"yes\" || s === \"on\") return true;\n if (s === \"false\" || s === \"0\" || s === \"no\" || s === \"off\") return false;\n return fallback;\n}\n\nfunction qqLinkConfigFile(): string {\n return join(env.PROJECT_ROOT, \"configs\", `${QQ_LINK_CONFIG_KEY}.json`);\n}\n\nfunction readJoinFlags(): JoinFeatureFlags {\n // \u5E73\u53F0\u914D\u7F6E configs/qq_link.json\uFF08\u975E SDK qq_config\uFF09\n const disk = ensureJson<Record<string, unknown>>(qqLinkConfigFile(), { ...QQ_LINK_DEFAULTS });\n return {\n allowlistEnabled: asConfigBool(disk.allowlist_enabled, QQ_LINK_DEFAULTS.allowlist_enabled),\n requireApproval: asConfigBool(disk.require_approval, QQ_LINK_DEFAULTS.require_approval),\n treatGroupAdminsAsAdmins: asConfigBool(\n disk.treat_group_admins_as_admins,\n QQ_LINK_DEFAULTS.treat_group_admins_as_admins\n ),\n };\n}\n\nfunction writeJoinFlags(partial: Partial<JoinFeatureFlags>): JoinFeatureFlags {\n const file = qqLinkConfigFile();\n ensureJson<Record<string, unknown>>(file, { ...QQ_LINK_DEFAULTS });\n const patch: Record<string, boolean> = {};\n if (partial.allowlistEnabled !== undefined) patch.allowlist_enabled = partial.allowlistEnabled;\n if (partial.requireApproval !== undefined) patch.require_approval = partial.requireApproval;\n // \u6545\u610F\u4E0D\u5199 treat_group_admins_as_admins\uFF1A\u4EC5\u4EBA\u5DE5\u6539 configs/qq_link.json\n if (Object.keys(patch).length > 0) {\n patchJson(file, patch);\n }\n return readJoinFlags();\n}\n\n// \u542F\u52A8\u5373\u843D\u76D8\u5E73\u53F0\u9ED8\u8BA4\u914D\u7F6E\uFF0C\u907F\u514D\u300C\u4ECE\u672A\u70B9\u8FC7\u914D\u7F6E/\u7533\u8BF7\u300D\u65F6 configs/qq_link.json \u4E0D\u5B58\u5728\n{\n const created = !readJson(qqLinkConfigFile());\n ensureJson<Record<string, unknown>>(qqLinkConfigFile(), { ...QQ_LINK_DEFAULTS });\n if (created) {\n log.info(`\u5DF2\u5199\u5165\u5E73\u53F0\u914D\u7F6E\u9AA8\u67B6: configs/${QQ_LINK_CONFIG_KEY}.json`);\n }\n}\n\nconst qqJoinRoutes = createQqJoinRoutes({\n query,\n body,\n json,\n getAdminOpenids: () => {\n const raw = (env.qqconfig[\"official\"] as { admin_openids?: unknown } | undefined)?.admin_openids;\n if (!Array.isArray(raw)) return [];\n return raw.map((x) => String(x).trim()).filter(Boolean);\n },\n getJoinFlags: () => readJoinFlags(),\n setJoinFlags: (partial) => writeJoinFlags(partial),\n});\n\nfunction readQqEventsSettings(): ResolvedQqEventsConfig {\n const disk = readJson<Record<string, unknown>>(join(env.PROJECT_ROOT, \"configs\", \"qq_config.json\"));\n if (!disk) throw new Error(\"qq_config.json \u65E0\u6CD5\u8BFB\u53D6\uFF0C\u4E8B\u4EF6\u8BBE\u7F6E\u672A\u4FEE\u6539\");\n return resolveQqEventsConfig(disk.qq_events);\n}\n\nfunction writeQqEventsSettings(patch: Partial<ResolvedQqEventsConfig>): ResolvedQqEventsConfig {\n const file = join(env.PROJECT_ROOT, \"configs\", \"qq_config.json\");\n const settings = { ...readQqEventsSettings(), ...patch };\n patchJson(file, { qq_events: settings });\n return settings;\n}\n\nfunction currentOutbound() {\n return makeOutboundConfig({\n QQ_BACKEND: env.QQ_BACKEND,\n LLBOT_HOST: env.LLBOT_HOST,\n LLBOT_PORT: env.LLBOT_PORT,\n LLBOT_TOKEN: env.LLBOT_TOKEN,\n QQ_GROUP_ID: env.QQ_GROUP_ID,\n QQ_APP_ID: env.QQ_APP_ID,\n QQ_APP_SECRET: env.QQ_APP_SECRET,\n QQ_SANDBOX: env.QQ_SANDBOX,\n QQ_GROUP_OPENID: env.QQ_GROUP_OPENID,\n });\n}\n\nconst qqEventsDispatcher = createQqEventsDispatcher({\n getConfig: readQqEventsSettings,\n getOutbound: () => currentOutbound(),\n});\nconst qqEventsRoutes = createQqEventsRoutes({\n body,\n json,\n dispatcher: qqEventsDispatcher,\n getSettings: readQqEventsSettings,\n setSettings: writeQqEventsSettings,\n isAdmin: (openid, asGroupAdmin) => {\n const admins = (env.qqconfig[\"official\"] as { admin_openids?: unknown } | undefined)?.admin_openids;\n return (\n (Array.isArray(admins) && admins.includes(openid)) || (asGroupAdmin && readJoinFlags().treatGroupAdminsAsAdmins)\n );\n },\n});\n\n/** \u8BFB\u53D6\u5355\u4E2A\u804A\u5929\u9891\u9053\u3002\u8868\u5C1A\u672A\u7531\u804A\u5929\u6A21\u5757\u5EFA\u51FA\u65F6\u8FD4\u56DE null\uFF0C\u4E0D\u8BA9\u8BF7\u6C42 500\u3002 */\nfunction readChatChannel(channelId: string): ChatChannelQQFlags | null {\n try {\n const rows = query(SQL`SELECT * FROM sfmc_chat_channels WHERE id = ${channelId}`);\n return Array.isArray(rows) ? ((rows[0] as ChatChannelQQFlags | undefined) ?? null) : null;\n } catch (err) {\n log.warn(`\u8BFB\u53D6\u804A\u5929\u9891\u9053\u5931\u8D25: ${(err as Error).message}`);\n return null;\n }\n}\n\n/** \u6D88\u606F\u6765\u6E90\u4E3A QQ \u7684\u9891\u9053\u3002QQ \u5165\u7AD9\u53EA\u6295\u5230\u8FD9\u4E9B\u9891\u9053\u3002 */\nfunction listQQSourceChannels(): ChatChannelQQFlags[] {\n try {\n const rows = query(SQL`SELECT * FROM sfmc_chat_channels`);\n if (!Array.isArray(rows)) return [];\n return (rows as ChatChannelQQFlags[]).filter((row) => row.source_qq === 1);\n } catch (err) {\n log.warn(`\u8BFB\u53D6 QQ \u6765\u6E90\u9891\u9053\u5931\u8D25: ${(err as Error).message}`);\n return [];\n }\n}\n\nconst messagesRoutes = createMessagesRoutes({\n query,\n body,\n json,\n getChannelForQQ: readChatChannel,\n listQQSourceChannels,\n forwardToQQBridge: (channelId: string, prefix: string, fromName: string, content: string, fromId: string) =>\n forwardToQQBridge(currentOutbound(), channelId, prefix, fromName, content, fromId),\n});\n\n// \u6E38\u620F\u53D1\u8A00\u8D70 db.tx \u63D2\u5165\uFF0C\u4E0D\u7ECF\u8FC7 /api/sfmc/messages\u3002\u63D0\u4EA4\u540E\u518D\u6309\u9891\u9053\u5F00\u5173\u8F6C\u53D1\u5230 QQ\u3002\ntxRunner.setAfterCommit((inserts) => {\n for (const item of inserts) {\n if (item.table !== \"sfmc_chat_messages\") continue;\n const payload = gameChatForwardPayload(item.row, readChatChannel(String(item.row.channel_id ?? \"\")));\n if (!payload) continue;\n forwardToQQBridge(\n currentOutbound(),\n payload.channelId,\n payload.prefix,\n payload.fromName,\n payload.content,\n payload.fromId,\n );\n }\n});\nconst configRoutes = createConfigRoutes({\n json,\n projectRoot: env.PROJECT_ROOT,\n // DIP:\u8DEF\u7531\u4E0D\u8BFB lock/catalog/token \u6587\u4EF6,\u7531\u5165\u53E3\u6CE8\u5165\u4E0E /modules \u540C\u6E90\u6570\u636E\n listModules: () => buildModuleList() as Array<Record<string, unknown>>,\n getModuleTokens: () => ({ ...moduleAuth.tokens }),\n});\nconst contentRoutes = createContentRoutes({\n json,\n loadSnapshot: loadContentSnapshotForRequest,\n});\nconst moduleRoutesInstance = createModuleRoutes({\n loadModuleCatalog,\n buildModuleList: buildModuleList as unknown as () => Array<Record<string, unknown>>,\n resolveModuleByKey,\n setModuleEnabled,\n body,\n json,\n});\n\n// \u2500\u2500 v2 \u8DEF\u7531\u5DE5\u5382 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// \u7C7B\u578B\u65AD\u8A00\u6210 unknown \u51FD\u6570 \u2014 v2 routes \u7684 ctx \u7C7B\u578B\u4E0E RouteCtx \u4E0D\u517C\u5BB9,\n// \u4F46\u8C03\u7528\u5951\u7EA6({path, method, params, req, res, body?})\u662F\u7A33\u5B9A\u7684\u3002\nconst dbRoutes = createDbRoutes({ schemaRegistry, txRunner, idempotent, json }) as unknown as (\n ctx: Record<string, unknown>\n) => Promise<boolean>;\nconst serviceRoutes = createServiceRoutes({\n serviceRegistry,\n enabled: enabledManifests,\n json,\n}) as unknown as (ctx: Record<string, unknown>) => Promise<boolean>;\nconst moduleConfigRoutes = createModuleConfigRoutes({\n projectRoot: env.PROJECT_ROOT,\n enabled: enabledManifests,\n json,\n}) as unknown as (ctx: Record<string, unknown>) => Promise<boolean>;\n\n// \u2500\u2500 \u4E3B\u8BF7\u6C42\u5904\u7406\u5668 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nasync function handle(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\n const url = new URL(req.url || \"/\", `http://${req.headers.host || \"localhost\"}`);\n const path = url.pathname;\n const method = req.method || \"GET\";\n const params = url.searchParams;\n\n // loopback \u7ED1\u5B9A\n const remote = req.socket.remoteAddress || \"\";\n if (remote && !remote.startsWith(\"127.\") && remote !== \"::1\" && remote !== \"::ffff:127.\") {\n json(res, { success: false, error: \"forbidden\" }, 403);\n return;\n }\n\n // \u2500\u2500 \u9884\u8BFB body(\u6240\u6709\u8DEF\u7531\u5171\u4EAB) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n await body(req);\n\n // \u2500\u2500 v2 \u6A21\u5757\u8EAB\u4EFD\u6821\u9A8C \u2192 \u5199\u5165\u8DEF\u7531 ctx.moduleAuth(LoD:\u4E0D\u6302 req \u79C1\u6709\u5B57\u6BB5) \u2500\u2500\n // \u6CE8\u610F:`/api/sfmc/configs/all` \u662F\u65E7\u7684\u4E00\u6B21\u6027\u914D\u7F6E\u5FEB\u7167\u7AEF\u70B9(SAPI ConfigManager.init\n // \u542F\u52A8\u5FC5\u7528),\u4E0D\u5C5E\u4E8E v2 \u6A21\u5757\u914D\u7F6E\u547D\u540D\u7A7A\u95F4(configs/<\u6A21\u5757 configKey>),\u5FC5\u987B\u8C41\u514D,\n // \u5426\u5219\u4F1A\u88AB\u6A21\u5757\u9274\u6743\u7F51\u5173\u62E6\u6210 401,\u5BFC\u81F4\u63D2\u4EF6\u7AEF\u8D77\u4E0D\u6765\u3002\n const isLegacyConfigAll = path === \"/api/sfmc/configs/all\";\n const needsModuleAuth =\n path.startsWith(\"/api/sfmc/db/\") ||\n path.startsWith(\"/api/sfmc/services\") ||\n (/^\\/api\\/sfmc\\/configs\\/[A-Za-z0-9_-]+(?:\\/(?:set|notify))?$/.test(path) && !isLegacyConfigAll);\n let moduleAuthCtx: { id: string; permissions: string[] } | null = null;\n if (needsModuleAuth) {\n const id = verifyModuleAuth({\n headers: req.headers,\n params,\n auth: moduleAuth,\n enabledModuleIds: enabledSet,\n });\n if (!id) {\n // LSP: v2 \u6A21\u5757\u95E8\u4E0E service-routes \u540C\u7528 ok \u65B9\u8A00\n jsonV2Fail(res, \"unauthorized: module identity invalid\", 401, \"unauthorized\");\n return;\n }\n const manifest = enabledManifests.get(id);\n moduleAuthCtx = {\n id,\n permissions: manifest?.permissions ?? [],\n };\n } else {\n // \u2500\u2500 \u65E7 env token \u9274\u6743(\u53EA\u5BF9 POST/PUT \u751F\u6548) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const PUBLIC_GET =\n path === \"/api/health\" ||\n path === \"/api/sfmc/status\" ||\n path === \"/api/sfmc/qq/join/apply-queue\" ||\n path === \"/api/sfmc/qq/join/settings\" ||\n (method === \"GET\" &&\n (path === \"/api/sfmc/content\" ||\n path === \"/api/sfmc/modules\" ||\n path === \"/api/sfmc/modules/catalog\" ||\n path.startsWith(\"/api/sfmc/modules/\") ||\n path === \"/api/sfmc/qq/bind/me\" ||\n path === \"/api/sfmc/wechat/bind/me\" ||\n path === \"/api/sfmc/qq/join/pending\"));\n // bind/join/events POST \u4E0E messages \u4E00\u6837\u8D70\u65E7 AUTH_TOKEN\uFF08\u82E5\u914D\u7F6E\uFF09\uFF1B\u6E38\u620F/\u6865\u540C\u4E3A loopback\n // \u5FAE\u4FE1\u7ED1\u5B9A\uFF08mp-gateway \u8C03\u7528\uFF09\u4E0E\u7EDF\u4E00\u786E\u8BA4\u5165\u53E3\uFF08\u6E38\u620F\u8C03\u7528\uFF09\u4E0E QQ \u7ED1\u5B9A\u540C\u4E00\u4FE1\u4EFB\u8FB9\u754C\uFF1A\u4EC5\u672C\u673A\n const NEEDS_AUTH =\n !PUBLIC_GET &&\n method !== \"GET\" &&\n !(\n path.startsWith(\"/api/sfmc/qq/bind/\") ||\n path.startsWith(\"/api/sfmc/qq/join/\") ||\n path.startsWith(\"/api/sfmc/qq/events\") ||\n path.startsWith(\"/api/sfmc/wechat/bind/\") ||\n path === \"/api/sfmc/bind/confirm\"\n );\n if (env.AUTH_TOKEN && NEEDS_AUTH) {\n const auth = req.headers[\"authorization\"] || \"\";\n const provided = auth.startsWith(\"Bearer \") ? auth.slice(7) : (req.headers[\"x-db-token\"] as string) || \"\";\n if (provided !== env.AUTH_TOKEN) {\n json(res, { success: false, error: \"unauthorized\" }, 401);\n return;\n }\n }\n }\n\n try {\n if (path === \"/api/sfmc/player-session\" && method === \"POST\") {\n const data = (await body(req)) as Record<string, unknown>;\n const playerName = String(data.playerName ?? \"\").trim();\n const xuid = String(data.xuid ?? \"\").trim();\n if (!playerName || !xuid) {\n json(res, { success: false, error: \"playerName_and_xuid_required\" }, 400);\n return;\n }\n syncPlayerSession(query, { playerName, xuid });\n json(res, { success: true });\n return;\n }\n // \u2500\u2500 v2 \u8DEF\u7531(\u4F18\u5148\u5339\u914D) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (\n moduleAuthCtx &&\n (path.startsWith(\"/api/sfmc/db/\") ||\n path.startsWith(\"/api/sfmc/services\") ||\n (/^\\/api\\/sfmc\\/configs\\/[A-Za-z0-9_-]+/.test(path) && !isLegacyConfigAll))\n ) {\n const ctx: Record<string, unknown> = {\n path,\n method,\n params,\n req,\n res,\n moduleAuth: moduleAuthCtx,\n };\n // body \u5DF2\u5728\u4E0A\u9762 `await body(req)` \u9884\u8BFB\u5E76\u7F13\u5B58\u5230 req._bodyPromise;\n // \u8FD9\u91CC\u590D\u7528\u7F13\u5B58(\u539F\u5B9E\u73B0\u8BFB\u7684 req._body \u4ECE\u672A\u88AB\u8D4B\u503C,\u5BFC\u81F4\u6240\u6709 v2 \u8DEF\u7531 body \u6052\u4E3A\u7A7A)\u3002\n ctx[\"body\"] = await body(req);\n if (path.startsWith(\"/api/sfmc/db/\")) {\n if (await dbRoutes(ctx)) return;\n } else if (path.startsWith(\"/api/sfmc/services\")) {\n if (await serviceRoutes(ctx)) return;\n } else {\n if (await moduleConfigRoutes(ctx)) return;\n }\n }\n\n // \u2500\u2500 \u5E73\u53F0\u8DEF\u7531 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const ctxBase = { path, method, params, req, res } as {\n path: string;\n method: string;\n params: URLSearchParams;\n req: http.IncomingMessage;\n res: http.ServerResponse;\n };\n if (await contentRoutes(ctxBase)) return;\n if (await moduleRoutesInstance(ctxBase)) return;\n if (await healthRoutes(ctxBase)) return;\n if (await statusRoutes(ctxBase)) return;\n if (await qqBindRoutes(ctxBase)) return;\n if (await bindConfirmRoutes(ctxBase)) return;\n if (await wechatBindRoutes(ctxBase)) return;\n if (await qqJoinRoutes(ctxBase)) return;\n if (await qqEventsRoutes(ctxBase)) return;\n if (await messagesRoutes(ctxBase)) return;\n if (await configRoutes(ctxBase)) return;\n\n json(res, { success: false, error: \"not_found\" }, 404);\n } catch (err) {\n log.err(err, \"DogeDB\");\n json(res, { success: false, error: (err as Error).message }, 500);\n }\n}\n\n// \u2500\u2500 \u542F\u52A8 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst server = createServer({\n env: { PORT: env.PORT, HOST: env.HOST, AUTH_TOKEN: env.AUTH_TOKEN },\n handle,\n});\nstartConsole(server, db);\n\nexport { db, enabledManifests, env, moduleAuth, query, schemaRegistry, serviceRegistry, txRunner };\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["/**\n *\n * \u5DE5\u4F5C\u6D41:\n * 1. \u52A0\u8F7D env\n * 2. \u6821\u9A8C Node \u7248\u672C\n * 3. openDatabase + createPlatformTables(sfmc__audit / sfmc__idempotent)\n * 4. loadManifestV2() \u2014 \u5931\u8D25 = \u542F\u52A8\u5931\u8D25\n * 5. filterEnabled(loaded, lockFileEnabled)\n * 6. buildModuleAuth({auth_token, enabled_modules}) \u2190 \u4E0E DB \u540C\u76EE\u5F55 module-tokens.json\n * 7. \u5B9E\u4F8B\u5316:SchemaRegistry / ServiceRegistry / IdempotencyStore / TxRunner\n * 8. \u88C5\u914D v2 \u8DEF\u7531 (/api/sfmc/db/* + /api/sfmc/services* + /api/sfmc/configs/:key/*)\n * \u53E6\u4FDD\u7559 messages(qq-bridge) + config/modules/health \u5E73\u53F0\u8DEF\u7531\n * 9. createServer + listen\n *\n * \u9274\u6743(handle \u5C42):\n * - /api/sfmc/db/*, /api/sfmc/services*, /api/sfmc/configs/:key(/set|notify)*\n * \u2192 module token (Authorization: Bearer ...) + ?moduleId=...\n * - \u5176\u4ED6:env.AUTH_TOKEN \u65E7 auth(NEEDS_AUTH)\n */\n\nimport http from \"node:http\";\nimport { PLATFORM_MONITORING_SERVICES, PLATFORM_SERVICE_OWNER } from \"@sfmc-bds/sdk/contracts\";\nimport { SQL } from \"sql-template-strings\";\n\nimport { createPlatformTables } from \"./db-tables.js\";\nimport { syncPlayerSession } from \"./domain/player-session.js\";\nimport { initSchema } from \"./domain/schema.js\";\nimport { loadEnv } from \"./env.js\";\nimport { createIdempotencyStore } from \"./lib/idempotency-store.js\";\nimport { log } from \"./lib/log.js\";\nimport { assertNodeVersion } from \"./lib/runtime.js\";\nimport { createQuery, openDatabase } from \"./lib/sqlite.js\";\nimport { loadManifestV2 } from \"./manifest-loader.js\";\nimport { buildModuleAuth, ensureModuleToken, persistModuleAuth, verifyModuleAuth } from \"./module-auth.js\";\nimport { syncModuleRuntimeState } from \"./module-runtime-sync.js\";\nimport { SchemaRegistry } from \"./schema-registry.js\";\nimport { createServer, startConsole } from \"./server.js\";\nimport { ServiceRegistry } from \"./service-registry.js\";\nimport { registerEnabledBuiltinServices } from \"./services/builtin-handlers.js\";\nimport { TxRunner } from \"./tx-runner.js\";\n\nimport { configPath, ensureJson, patchJson, readJson } from \"@sfmc-bds/sdk/node/config\";\nimport path, { join } from \"node:path\";\n\nimport { jsonV2Fail } from \"./routes/_shared.js\";\nimport { createDbRoutes } from \"./routes/db-routes.js\";\nimport { createModuleConfigRoutes } from \"./routes/module-config-routes.js\";\nimport { createServiceRoutes } from \"./routes/service-routes.js\";\n\nimport { forwardToQQBridge, makeOutboundConfig } from \"./domain/bridge.js\";\nimport { loadContentSnapshot } from \"./domain/installed-content.js\";\nimport { createQqEventsDispatcher, resolveQqEventsConfig, type ResolvedQqEventsConfig } from \"./domain/qq-events.js\";\nimport { body as sharedBody, json as sharedJson } from \"./lib/http.js\";\nimport { isEnabled, loadModuleLock, saveModuleLock, updateModuleState } from \"./lib/module-state.js\";\nimport { createConfigRoutes } from \"./routes/config.js\";\nimport { createContentRoutes } from \"./routes/content.js\";\nimport { createHealthRoutes } from \"./routes/health.js\";\nimport { createMessagesRoutes, gameChatForwardPayload, type ChatChannelQQFlags } from \"./routes/messages.js\";\nimport { createModuleRoutes } from \"./routes/modules.js\";\nimport { codeTakenBy, type BindProvider } from \"./domain/bind-providers.js\";\nimport { resolvePlayerLevel } from \"./domain/player-level.js\";\nimport { createQqBindProvider } from \"./domain/qq-bind.js\";\nimport { createWechatBindProvider } from \"./domain/wechat-bind.js\";\nimport { createBindConfirmRoutes } from \"./routes/bind-confirm.js\";\nimport { createQqBindRoutes } from \"./routes/qq-bind.js\";\nimport { createWechatBindRoutes } from \"./routes/wechat-bind.js\";\nimport { createWechatActivityRoutes } from \"./routes/wechat-activity.js\";\nimport { createScoreboardRoutes } from \"./routes/scoreboard.js\";\nimport { createQqEventsRoutes } from \"./routes/qq-events.js\";\nimport { createQqJoinRoutes, type JoinFeatureFlags } from \"./routes/qq-join.js\";\nimport { createStatusRoutes } from \"./routes/status.js\";\n\nif (!assertNodeVersion(22, 13)) {\n process.exit(2);\n}\n\nconst env = loadEnv();\nconst db = openDatabase(env.DB_PATH);\ncreatePlatformTables(db); // sfmc__audit / sfmc__idempotent\ninitSchema(db); // \u5E73\u53F0\u4E1A\u52A1 bootstrap \u8868(players/world/chat/...)\u2014\u2014qq-bridge \u5728 SAPI defineTable \u4E4B\u524D\u5C31\u9700\u8981\nconst query = createQuery(db);\n\n// \u2500\u2500 v2 manifest \u52A0\u8F7D(\u5931\u8D25 = \u542F\u52A8\u5931\u8D25)\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst loadedManifest = loadManifestV2(); // throws on violation\nlog.success(\n `[manifest v2] loaded ${Object.keys(loadedManifest.modules).length} modules; provides ${loadedManifest.providesMap.size} services`\n);\n\n// \u2500\u2500 enabled \u96C6\u5408(\u4ECE lock file)\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst lockFile = loadModuleLock(env.MODULE_LOCK_PATH);\nconst moduleCatalog = readJson<{ modules?: unknown[] }>(env.MODULE_CATALOG_PATH) ?? { modules: [] };\nconst catalogIds = new Set(\n Array.isArray(moduleCatalog.modules)\n ? (moduleCatalog.modules as Array<{ id?: string }>).map((m) => String(m.id || \"\")).filter((id) => id.length > 0)\n : []\n);\nconst enabledSet = new Set<string>();\nfor (const id of Object.keys(loadedManifest.modules)) {\n if (!catalogIds.has(id)) continue;\n const catalogEntry = (moduleCatalog.modules as Array<Record<string, unknown>>).find((m) => String(m.id) === id);\n if (!catalogEntry) continue;\n const defaultEnabled = catalogEntry.enabledByDefault !== false;\n if (isEnabled(lockFile, id, defaultEnabled)) enabledSet.add(id);\n}\nconst enabledManifests = new Map<string, NonNullable<(typeof loadedManifest.modules)[string]>>();\nfor (const id of enabledSet) {\n const m = loadedManifest.modules[id];\n if (m) enabledManifests.set(id, m);\n}\nlog.info(`[manifest v2] enabled: ${[...enabledSet].sort().join(\", \") || \"(none)\"}`);\n\n// \u2500\u2500 \u6A21\u5757 HMAC token map(\u4E0E DB \u540C\u76EE\u5F55 module-tokens.json)\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst moduleAuth = buildModuleAuth({\n dbPath: env.DB_PATH,\n envAuthToken: env.AUTH_TOKEN,\n enabledModuleIds: [...enabledSet],\n});\n// \u5E73\u53F0\u5BBF\u4E3B\uFF08QQ \u7ED1\u5B9A/\u8D26\u53F7\u5FEB\u7167\uFF09\u9700\u8981\u7A33\u5B9A token\uFF0C\u7ECF configs/all.module_tokens \u4E0B\u53D1\u5230 BDS\nif (ensureModuleToken(moduleAuth, PLATFORM_SERVICE_OWNER)) {\n persistModuleAuth(env.DB_PATH, moduleAuth, env.AUTH_TOKEN);\n log.info(`[auth] \u5DF2\u6D3E\u751F\u5E73\u53F0\u5BBF\u4E3B token: ${PLATFORM_SERVICE_OWNER}`);\n}\n\n// \u2500\u2500 \u4E09\u4EF6\u5957 + \u8DEF\u7531\u5DE5\u5382 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst schemaRegistry = new SchemaRegistry(db);\nconst serviceRegistry = new ServiceRegistry();\nconst idempotent = createIdempotencyStore(db);\nconst txRunner = new TxRunner({\n db,\n query,\n schema: schemaRegistry,\n serviceRegistry,\n enabled: enabledManifests,\n});\n\n// \u2500\u2500 \u8FDB\u7A0B\u5185\u7F6E service handler(\u6269\u5C55\u70B9:BUILTIN_SERVICE_PLUGINS) \u2500\u2500\n{\n const plugins = registerEnabledBuiltinServices(serviceRegistry, { query, db }, enabledSet);\n if (plugins > 0) {\n log.success(`[service] registered ${serviceRegistry.list().length} handlers from ${plugins} builtin plugin(s)`);\n }\n}\n\nconst json = sharedJson;\nconst body = sharedBody;\n\n// \u2500\u2500 \u5DE5\u5177\u51FD\u6570 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction loadModuleCatalog() {\n return Array.isArray(moduleCatalog.modules) ? moduleCatalog.modules : [];\n}\n\nfunction buildModuleList() {\n const catalog = loadModuleCatalog();\n return catalog\n .map((raw) => {\n const entry =\n (raw as Record<string, unknown>).entry && typeof (raw as Record<string, unknown>).entry === \"object\"\n ? ((raw as Record<string, unknown>).entry as Record<string, unknown>)\n : {};\n const id = String((raw as Record<string, unknown>).id || \"\").trim();\n const configKey = String(\n (raw as Record<string, unknown>).configKey || (raw as Record<string, unknown>).config_key || \"\"\n ).trim();\n if (!id || !configKey) return null;\n const state = lockFile.modules[id];\n const enabled = isEnabled(lockFile, id, (raw as Record<string, unknown>).enabledByDefault !== false);\n return {\n id,\n module_id: id,\n name: configKey,\n configKey,\n config_key: configKey,\n display_name: String((raw as Record<string, unknown>).name || configKey),\n description: String((raw as Record<string, unknown>).description || \"\"),\n default_enabled: (raw as Record<string, unknown>).enabledByDefault !== false,\n can_disable: moduleCanDisable(raw as Record<string, unknown>),\n // ConfigManager \u8BA4 installed!==false;\u5DF2\u88C5\u5305\u9ED8\u8BA4 true\n installed: true,\n requires: Array.isArray((raw as Record<string, unknown>).requires)\n ? ((raw as Record<string, unknown>).requires as unknown[]).filter(Boolean).map(String)\n : [],\n optional: Array.isArray((raw as Record<string, unknown>).optional)\n ? ((raw as Record<string, unknown>).optional as unknown[]).filter(Boolean).map(String)\n : [],\n commands: Array.isArray((raw as Record<string, unknown>).commands)\n ? ((raw as Record<string, unknown>).commands as unknown[]).filter(Boolean).map(String)\n : [],\n entry: {\n kind: String(entry.kind || \"\"),\n path: String(entry.path || \"\"),\n init: String(entry.init || \"\"),\n },\n updated_at: state?.updatedAt ?? null,\n enabled: !!enabled,\n };\n })\n .filter(Boolean);\n}\n\n/**\n * QQ\u300C\u6A21\u5757 / \u8D44\u6E90\u5305\u300D\u5FEB\u7167\u3002\u6BCF\u6B21\u8BF7\u6C42\u73B0\u8BFB catalog \u4E0E\u78C1\u76D8\uFF0C\u88C5\u5305\u540E\u4E0D\u7528\u91CD\u542F db-server\u3002\n */\nfunction loadContentSnapshotForRequest() {\n const modules = (buildModuleList() ?? []).flatMap((raw) => {\n if (!raw || typeof raw !== \"object\") return [];\n const row = raw as { id?: unknown; display_name?: unknown; enabled?: unknown };\n const id = String(row.id ?? \"\").trim();\n if (!id) return [];\n return [\n {\n id,\n display_name: String(row.display_name || id),\n enabled: row.enabled !== false,\n },\n ];\n });\n return loadContentSnapshot({\n packagesDir: join(env.MODULES_DIR, \"packages\"),\n modules,\n bdsRoot: currentBdsRoot(),\n });\n}\n\n/**\n * BDS \u6839\u76EE\u5F55\uFF08configs/bds_updater.json \u7684 bds_path\uFF0C\u76F8\u5BF9\u8DEF\u5F84\u6309\u9879\u76EE\u6839\u89E3\u6790\uFF09\uFF1B\u672A\u914D\u7F6E\u65F6\u4E3A null\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1A\u5185\u5BB9\u5FEB\u7167\u8BFB\u53D6\u5DF2\u88C5\u5305\uFF0C\u5FAE\u4FE1\u7ED1\u5B9A\u67E5\u8BE2\u8BFB\u53D6 BDS permissions.json \u8BA1\u7B97\u73A9\u5BB6\u539F\u751F\u7B49\u7EA7\uFF08DRY\uFF1A\u540C\u4E00\u5904\u89E3\u6790\uFF09\u3002\n */\nfunction currentBdsRoot(): string | null {\n const cfg = readJson<{ bds_path?: string }>(configPath(env.PROJECT_ROOT, \"bds_updater.json\"));\n const rawBds = typeof cfg?.bds_path === \"string\" ? cfg.bds_path.trim() : \"\";\n return rawBds ? (path.isAbsolute(rawBds) ? rawBds : path.resolve(env.PROJECT_ROOT, rawBds)) : null;\n}\n\n/** catalog \u7701\u7565 canDisable \u65F6\u9ED8\u8BA4\u5141\u8BB8\u7981\u7528(\u4E0E buildModuleList.can_disable \u540C\u6E90)\u3002 */\nfunction moduleCanDisable(raw: Record<string, unknown>): boolean {\n return raw.canDisable !== false;\n}\n\nfunction resolveModuleByKey(key: string) {\n const k = String(key || \"\").trim();\n const catalog = loadModuleCatalog();\n const raw = catalog.find(\n (m) =>\n String((m as Record<string, unknown>).id || \"\") === k ||\n String((m as Record<string, unknown>).configKey || (m as Record<string, unknown>).config_key || \"\") === k\n ) as Record<string, unknown> | undefined;\n if (!raw) return null;\n const id = String(raw.id || \"\").trim();\n const configKey = String(raw.configKey || raw.config_key || \"\").trim();\n if (!id || !configKey) return null;\n /* LSP:\u4E0E buildModuleList.can_disable \u540C\u6E90 \u2014 \u7701\u7565\u5B57\u6BB5\u89C6\u4E3A\u53EF\u7981\u7528 */\n return { id, configKey, canDisable: moduleCanDisable(raw) };\n}\n\nfunction setModuleEnabled(mod: { id: string; canDisable: boolean }, enabled: boolean) {\n // \u76F4\u63A5\u66F4\u65B0\u542F\u52A8\u65F6\u7F13\u5B58\u7684 lockFile(\u800C\u975E\u53E6\u8BFB\u4E00\u4EFD\u65B0\u526F\u672C),\n // \u5426\u5219 buildModuleList() \u8BFB\u7684\u4ECD\u662F\u65E7\u7F13\u5B58,\u5BFC\u81F4\u542F\u505C\u540E enabled \u72B6\u6001\u4E0D\u7FFB\u8F6C\u3002\n updateModuleState(lockFile, mod.id, { enabled: !!enabled });\n // DRY:\u4E0E loadModuleLock \u5BF9\u79F0\u8D70 saveModuleLock,\u52FF\u6563\u843D writeJson\n saveModuleLock(env.MODULE_LOCK_PATH, lockFile);\n\n // DIP:\u70ED\u540C\u6B65 enabledSet / tokens / manifests / builtin handlers(\u4E0D\u91CD\u542F db-server)\n syncModuleRuntimeState({\n moduleId: mod.id,\n enabled: !!enabled,\n dbPath: env.DB_PATH,\n envAuthToken: env.AUTH_TOKEN,\n enabledSet,\n enabledManifests,\n loadedManifest,\n moduleAuth,\n serviceRegistry,\n builtinDeps: { query, db },\n });\n}\n\n// \u2500\u2500 \u5E73\u53F0\u8DEF\u7531(\u975E\u6A21\u5757\u4E1A\u52A1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst healthRoutes = createHealthRoutes();\n// \u6307\u6807\u72EC\u7ACB\u843D\u5E93\uFF0C\u907F\u514D\u5199\u5165\u4E1A\u52A1\u6A21\u5757\u6B63\u5728\u6301\u6709\u7684\u8DE8\u8BF7\u6C42\u4E8B\u52A1\u3002\nconst metricsDb = openDatabase(path.join(path.dirname(env.DB_PATH), \"runtime-metrics.sqlite\"));\nconst statusRoutes = createStatusRoutes({ query: createQuery(metricsDb) });\n/** \u4E16\u754C\u8BA1\u5206\u677F\u5FEB\u7167\uFF08\u53EA\u5728\u5185\u5B58\u4E2D\uFF0C\u4F9B\u684C\u9762\u7AEF / \u5C0F\u7A0B\u5E8F\u300C\u8BA1\u5206\u677F\u300D\u9875\u7ECF CLI \u8BFB\u53D6\uFF09 */\nconst scoreboardRoutes = createScoreboardRoutes();\nfor (const name of PLATFORM_MONITORING_SERVICES) serviceRegistry.registerHandler(PLATFORM_SERVICE_OWNER, name, async () => {\n const response = await fetch(`http://127.0.0.1:${env.PORT}/api/sfmc/metrics`, { signal: AbortSignal.timeout(10_000) });\n if (!response.ok) throw new Error(\"\u5E73\u53F0\u8FD0\u884C\u6307\u6807\u8BFB\u53D6\u5931\u8D25\");\n const result = await response.json() as { current: { tps: number | null; chunkEstimate: number | null } | null };\n const tps = result.current?.tps ?? null;\n const grade = tps === null ? \"unknown\" : tps >= 19.5 ? \"green\" : tps >= 15 ? \"yellow\" : tps >= 10 ? \"gold\" : \"red\";\n if (name === \"tps.current\") return tps;\n if (name === \"tps.status\") return { tps, grade, text: `\u00A77[TPS] \u00A7f${tps?.toFixed(2) ?? \"\u672A\u77E5\"} \u00A77/ 20.00` };\n return result.current ? { ...result.current, grade, totalLoadedChunks: result.current.chunkEstimate } : null;\n});\n/**\n * \u5DF2\u6CE8\u518C\u7684\u5916\u90E8\u8D26\u53F7\u7ED1\u5B9A\u6E20\u9053\uFF08QQ\u3001\u5FAE\u4FE1\uFF09\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1A\u7EDF\u4E00\u786E\u8BA4\u5165\u53E3\u6309\u987A\u5E8F\u8BE2\u95EE\uFF1B\u4E24\u4E2A\u6E20\u9053\u7533\u8BF7\u77ED\u7801\u65F6\u5171\u7528 isBindCodeTaken \u8DE8\u6E20\u9053\u67E5\u91CD\u3002\u65B0\u589E\u6E20\u9053\u5728\u6B64\u8FFD\u52A0\uFF08OCP\uFF09\u3002\n */\nconst bindProviders: readonly BindProvider[] = [createQqBindProvider(query), createWechatBindProvider(query)];\nconst isBindCodeTaken = codeTakenBy(() => bindProviders);\nconst qqBindRoutes = createQqBindRoutes({ query, body, json, isCodeTaken: isBindCodeTaken });\nconst bindConfirmRoutes = createBindConfirmRoutes({ body, json, providers: () => bindProviders });\nconst wechatBindRoutes = createWechatBindRoutes({\n query,\n body,\n json,\n isCodeTaken: isBindCodeTaken,\n // \u6BCF\u6B21\u73B0\u8BFB\u4E24\u4EFD\u6743\u9650\u6587\u4EF6\uFF1A\u7BA1\u7406\u5458\u8C03\u6574\u6743\u9650\u540E\uFF0C\u5C0F\u7A0B\u5E8F\u7BA1\u7406\u7AEF\u6388\u6743\u7ACB\u5373\u968F\u4E4B\u53D8\u5316\n resolveLevel: (player) =>\n resolvePlayerLevel(\n {\n readSfmcPermissions: () => readJson(configPath(env.PROJECT_ROOT, \"permissions.json\")),\n readBdsPermissions: () => {\n const bdsRoot = currentBdsRoot();\n return bdsRoot ? readJson(join(bdsRoot, \"permissions.json\")) : null;\n },\n },\n player\n ),\n});\nconst wechatActivityRoutes = createWechatActivityRoutes({ query, json });\n\n/** \u5E73\u53F0 QQ \u7ED1\u5B9A\u76F8\u5173\u914D\u7F6E\uFF08\u975E SDK qq_config\uFF1B\u539F qq-link \u6A21\u5757\u914D\u7F6E\u952E\uFF09 */\nconst QQ_LINK_CONFIG_KEY = \"qq_link\";\nconst QQ_LINK_DEFAULTS = {\n allowlist_enabled: true,\n require_approval: true,\n /** \u662F\u5426\u5C06 QQ \u7FA4\u4E3B/\u7FA4\u7BA1\u89C6\u4F5C SFMC \u7BA1\u7406\u5458\uFF1B\u7F3A\u7701\u5F00\u542F\uFF0C\u4EC5\u6539\u672C\u6587\u4EF6\u53EF\u5173\u95ED\uFF0CAPI/\u7FA4\u804A\u4E0D\u53EF\u5199 */\n treat_group_admins_as_admins: true,\n} as const;\n\nfunction asConfigBool(v: unknown, fallback: boolean): boolean {\n if (v === undefined || v === null) return fallback;\n if (typeof v === \"boolean\") return v;\n const s = String(v).trim().toLowerCase();\n if (s === \"true\" || s === \"1\" || s === \"yes\" || s === \"on\") return true;\n if (s === \"false\" || s === \"0\" || s === \"no\" || s === \"off\") return false;\n return fallback;\n}\n\nfunction qqLinkConfigFile(): string {\n return join(env.PROJECT_ROOT, \"configs\", `${QQ_LINK_CONFIG_KEY}.json`);\n}\n\nfunction readJoinFlags(): JoinFeatureFlags {\n // \u5E73\u53F0\u914D\u7F6E configs/qq_link.json\uFF08\u975E SDK qq_config\uFF09\n const disk = ensureJson<Record<string, unknown>>(qqLinkConfigFile(), { ...QQ_LINK_DEFAULTS });\n return {\n allowlistEnabled: asConfigBool(disk.allowlist_enabled, QQ_LINK_DEFAULTS.allowlist_enabled),\n requireApproval: asConfigBool(disk.require_approval, QQ_LINK_DEFAULTS.require_approval),\n treatGroupAdminsAsAdmins: asConfigBool(\n disk.treat_group_admins_as_admins,\n QQ_LINK_DEFAULTS.treat_group_admins_as_admins\n ),\n };\n}\n\nfunction writeJoinFlags(partial: Partial<JoinFeatureFlags>): JoinFeatureFlags {\n const file = qqLinkConfigFile();\n ensureJson<Record<string, unknown>>(file, { ...QQ_LINK_DEFAULTS });\n const patch: Record<string, boolean> = {};\n if (partial.allowlistEnabled !== undefined) patch.allowlist_enabled = partial.allowlistEnabled;\n if (partial.requireApproval !== undefined) patch.require_approval = partial.requireApproval;\n // \u6545\u610F\u4E0D\u5199 treat_group_admins_as_admins\uFF1A\u4EC5\u4EBA\u5DE5\u6539 configs/qq_link.json\n if (Object.keys(patch).length > 0) {\n patchJson(file, patch);\n }\n return readJoinFlags();\n}\n\n// \u542F\u52A8\u5373\u843D\u76D8\u5E73\u53F0\u9ED8\u8BA4\u914D\u7F6E\uFF0C\u907F\u514D\u300C\u4ECE\u672A\u70B9\u8FC7\u914D\u7F6E/\u7533\u8BF7\u300D\u65F6 configs/qq_link.json \u4E0D\u5B58\u5728\n{\n const created = !readJson(qqLinkConfigFile());\n ensureJson<Record<string, unknown>>(qqLinkConfigFile(), { ...QQ_LINK_DEFAULTS });\n if (created) {\n log.info(`\u5DF2\u5199\u5165\u5E73\u53F0\u914D\u7F6E\u9AA8\u67B6: configs/${QQ_LINK_CONFIG_KEY}.json`);\n }\n}\n\nconst qqJoinRoutes = createQqJoinRoutes({\n query,\n body,\n json,\n getAdminOpenids: () => {\n const raw = (env.qqconfig[\"official\"] as { admin_openids?: unknown } | undefined)?.admin_openids;\n if (!Array.isArray(raw)) return [];\n return raw.map((x) => String(x).trim()).filter(Boolean);\n },\n getJoinFlags: () => readJoinFlags(),\n setJoinFlags: (partial) => writeJoinFlags(partial),\n});\n\nfunction readQqEventsSettings(): ResolvedQqEventsConfig {\n const disk = readJson<Record<string, unknown>>(join(env.PROJECT_ROOT, \"configs\", \"qq_config.json\"));\n if (!disk) throw new Error(\"qq_config.json \u65E0\u6CD5\u8BFB\u53D6\uFF0C\u4E8B\u4EF6\u8BBE\u7F6E\u672A\u4FEE\u6539\");\n return resolveQqEventsConfig(disk.qq_events);\n}\n\nfunction writeQqEventsSettings(patch: Partial<ResolvedQqEventsConfig>): ResolvedQqEventsConfig {\n const file = join(env.PROJECT_ROOT, \"configs\", \"qq_config.json\");\n const settings = { ...readQqEventsSettings(), ...patch };\n patchJson(file, { qq_events: settings });\n return settings;\n}\n\nfunction currentOutbound() {\n return makeOutboundConfig({\n QQ_BACKEND: env.QQ_BACKEND,\n LLBOT_HOST: env.LLBOT_HOST,\n LLBOT_PORT: env.LLBOT_PORT,\n LLBOT_TOKEN: env.LLBOT_TOKEN,\n QQ_GROUP_ID: env.QQ_GROUP_ID,\n QQ_APP_ID: env.QQ_APP_ID,\n QQ_APP_SECRET: env.QQ_APP_SECRET,\n QQ_SANDBOX: env.QQ_SANDBOX,\n QQ_GROUP_OPENID: env.QQ_GROUP_OPENID,\n });\n}\n\nconst qqEventsDispatcher = createQqEventsDispatcher({\n getConfig: readQqEventsSettings,\n getOutbound: () => currentOutbound(),\n});\nconst qqEventsRoutes = createQqEventsRoutes({\n body,\n json,\n dispatcher: qqEventsDispatcher,\n getSettings: readQqEventsSettings,\n setSettings: writeQqEventsSettings,\n isAdmin: (openid, asGroupAdmin) => {\n const admins = (env.qqconfig[\"official\"] as { admin_openids?: unknown } | undefined)?.admin_openids;\n return (\n (Array.isArray(admins) && admins.includes(openid)) || (asGroupAdmin && readJoinFlags().treatGroupAdminsAsAdmins)\n );\n },\n});\n\n/** \u8BFB\u53D6\u5355\u4E2A\u804A\u5929\u9891\u9053\u3002\u8868\u5C1A\u672A\u7531\u804A\u5929\u6A21\u5757\u5EFA\u51FA\u65F6\u8FD4\u56DE null\uFF0C\u4E0D\u8BA9\u8BF7\u6C42 500\u3002 */\nfunction readChatChannel(channelId: string): ChatChannelQQFlags | null {\n try {\n const rows = query(SQL`SELECT * FROM sfmc_chat_channels WHERE id = ${channelId}`);\n return Array.isArray(rows) ? ((rows[0] as ChatChannelQQFlags | undefined) ?? null) : null;\n } catch (err) {\n log.warn(`\u8BFB\u53D6\u804A\u5929\u9891\u9053\u5931\u8D25: ${(err as Error).message}`);\n return null;\n }\n}\n\n/** \u6D88\u606F\u6765\u6E90\u4E3A QQ \u7684\u9891\u9053\u3002QQ \u5165\u7AD9\u53EA\u6295\u5230\u8FD9\u4E9B\u9891\u9053\u3002 */\nfunction listQQSourceChannels(): ChatChannelQQFlags[] {\n try {\n const rows = query(SQL`SELECT * FROM sfmc_chat_channels`);\n if (!Array.isArray(rows)) return [];\n return (rows as ChatChannelQQFlags[]).filter((row) => row.source_qq === 1);\n } catch (err) {\n log.warn(`\u8BFB\u53D6 QQ \u6765\u6E90\u9891\u9053\u5931\u8D25: ${(err as Error).message}`);\n return [];\n }\n}\n\nconst messagesRoutes = createMessagesRoutes({\n query,\n body,\n json,\n getChannelForQQ: readChatChannel,\n listQQSourceChannels,\n forwardToQQBridge: (channelId: string, prefix: string, fromName: string, content: string, fromId: string) =>\n forwardToQQBridge(currentOutbound(), channelId, prefix, fromName, content, fromId),\n});\n\n// \u6E38\u620F\u53D1\u8A00\u8D70 db.tx \u63D2\u5165\uFF0C\u4E0D\u7ECF\u8FC7 /api/sfmc/messages\u3002\u63D0\u4EA4\u540E\u518D\u6309\u9891\u9053\u5F00\u5173\u8F6C\u53D1\u5230 QQ\u3002\ntxRunner.setAfterCommit((inserts) => {\n for (const item of inserts) {\n if (item.table !== \"sfmc_chat_messages\") continue;\n const payload = gameChatForwardPayload(item.row, readChatChannel(String(item.row.channel_id ?? \"\")));\n if (!payload) continue;\n forwardToQQBridge(\n currentOutbound(),\n payload.channelId,\n payload.prefix,\n payload.fromName,\n payload.content,\n payload.fromId,\n );\n }\n});\nconst configRoutes = createConfigRoutes({\n json,\n projectRoot: env.PROJECT_ROOT,\n // DIP:\u8DEF\u7531\u4E0D\u8BFB lock/catalog/token \u6587\u4EF6,\u7531\u5165\u53E3\u6CE8\u5165\u4E0E /modules \u540C\u6E90\u6570\u636E\n listModules: () => buildModuleList() as Array<Record<string, unknown>>,\n getModuleTokens: () => ({ ...moduleAuth.tokens }),\n});\nconst contentRoutes = createContentRoutes({\n json,\n loadSnapshot: loadContentSnapshotForRequest,\n});\nconst moduleRoutesInstance = createModuleRoutes({\n loadModuleCatalog,\n buildModuleList: buildModuleList as unknown as () => Array<Record<string, unknown>>,\n resolveModuleByKey,\n setModuleEnabled,\n body,\n json,\n});\n\n// \u2500\u2500 v2 \u8DEF\u7531\u5DE5\u5382 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// \u7C7B\u578B\u65AD\u8A00\u6210 unknown \u51FD\u6570 \u2014 v2 routes \u7684 ctx \u7C7B\u578B\u4E0E RouteCtx \u4E0D\u517C\u5BB9,\n// \u4F46\u8C03\u7528\u5951\u7EA6({path, method, params, req, res, body?})\u662F\u7A33\u5B9A\u7684\u3002\nconst dbRoutes = createDbRoutes({ schemaRegistry, txRunner, idempotent, json }) as unknown as (\n ctx: Record<string, unknown>\n) => Promise<boolean>;\nconst serviceRoutes = createServiceRoutes({\n serviceRegistry,\n enabled: enabledManifests,\n json,\n}) as unknown as (ctx: Record<string, unknown>) => Promise<boolean>;\nconst moduleConfigRoutes = createModuleConfigRoutes({\n projectRoot: env.PROJECT_ROOT,\n enabled: enabledManifests,\n json,\n}) as unknown as (ctx: Record<string, unknown>) => Promise<boolean>;\n\n// \u2500\u2500 \u4E3B\u8BF7\u6C42\u5904\u7406\u5668 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nasync function handle(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\n const url = new URL(req.url || \"/\", `http://${req.headers.host || \"localhost\"}`);\n const path = url.pathname;\n const method = req.method || \"GET\";\n const params = url.searchParams;\n\n // loopback \u7ED1\u5B9A\n const remote = req.socket.remoteAddress || \"\";\n if (remote && !remote.startsWith(\"127.\") && remote !== \"::1\" && remote !== \"::ffff:127.\") {\n json(res, { success: false, error: \"forbidden\" }, 403);\n return;\n }\n\n // \u2500\u2500 \u9884\u8BFB body(\u6240\u6709\u8DEF\u7531\u5171\u4EAB) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n await body(req);\n\n // \u2500\u2500 v2 \u6A21\u5757\u8EAB\u4EFD\u6821\u9A8C \u2192 \u5199\u5165\u8DEF\u7531 ctx.moduleAuth(LoD:\u4E0D\u6302 req \u79C1\u6709\u5B57\u6BB5) \u2500\u2500\n // \u6CE8\u610F:`/api/sfmc/configs/all` \u662F\u65E7\u7684\u4E00\u6B21\u6027\u914D\u7F6E\u5FEB\u7167\u7AEF\u70B9(SAPI ConfigManager.init\n // \u542F\u52A8\u5FC5\u7528),\u4E0D\u5C5E\u4E8E v2 \u6A21\u5757\u914D\u7F6E\u547D\u540D\u7A7A\u95F4(configs/<\u6A21\u5757 configKey>),\u5FC5\u987B\u8C41\u514D,\n // \u5426\u5219\u4F1A\u88AB\u6A21\u5757\u9274\u6743\u7F51\u5173\u62E6\u6210 401,\u5BFC\u81F4\u63D2\u4EF6\u7AEF\u8D77\u4E0D\u6765\u3002\n const isLegacyConfigAll = path === \"/api/sfmc/configs/all\";\n const needsModuleAuth =\n path.startsWith(\"/api/sfmc/db/\") ||\n path.startsWith(\"/api/sfmc/services\") ||\n (/^\\/api\\/sfmc\\/configs\\/[A-Za-z0-9_-]+(?:\\/(?:set|notify))?$/.test(path) && !isLegacyConfigAll);\n let moduleAuthCtx: { id: string; permissions: string[] } | null = null;\n if (needsModuleAuth) {\n const id = verifyModuleAuth({\n headers: req.headers,\n params,\n auth: moduleAuth,\n enabledModuleIds: enabledSet,\n });\n if (!id) {\n // LSP: v2 \u6A21\u5757\u95E8\u4E0E service-routes \u540C\u7528 ok \u65B9\u8A00\n jsonV2Fail(res, \"unauthorized: module identity invalid\", 401, \"unauthorized\");\n return;\n }\n const manifest = enabledManifests.get(id);\n moduleAuthCtx = {\n id,\n permissions: manifest?.permissions ?? [],\n };\n } else {\n // \u2500\u2500 \u65E7 env token \u9274\u6743(\u53EA\u5BF9 POST/PUT \u751F\u6548) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const PUBLIC_GET =\n path === \"/api/health\" ||\n path === \"/api/sfmc/status\" ||\n path === \"/api/sfmc/qq/join/apply-queue\" ||\n path === \"/api/sfmc/qq/join/settings\" ||\n (method === \"GET\" &&\n (path === \"/api/sfmc/content\" ||\n path === \"/api/sfmc/modules\" ||\n path === \"/api/sfmc/modules/catalog\" ||\n path.startsWith(\"/api/sfmc/modules/\") ||\n path === \"/api/sfmc/qq/bind/me\" ||\n path === \"/api/sfmc/wechat/bind/me\" ||\n path === \"/api/sfmc/qq/join/pending\"));\n // bind/join/events POST \u4E0E messages \u4E00\u6837\u8D70\u65E7 AUTH_TOKEN\uFF08\u82E5\u914D\u7F6E\uFF09\uFF1B\u6E38\u620F/\u6865\u540C\u4E3A loopback\n // \u5FAE\u4FE1\u7ED1\u5B9A\uFF08mp-gateway \u8C03\u7528\uFF09\u4E0E\u7EDF\u4E00\u786E\u8BA4\u5165\u53E3\uFF08\u6E38\u620F\u8C03\u7528\uFF09\u4E0E QQ \u7ED1\u5B9A\u540C\u4E00\u4FE1\u4EFB\u8FB9\u754C\uFF1A\u4EC5\u672C\u673A\n const NEEDS_AUTH =\n !PUBLIC_GET &&\n method !== \"GET\" &&\n !(\n path.startsWith(\"/api/sfmc/qq/bind/\") ||\n path.startsWith(\"/api/sfmc/qq/join/\") ||\n path.startsWith(\"/api/sfmc/qq/events\") ||\n path.startsWith(\"/api/sfmc/wechat/bind/\") ||\n path === \"/api/sfmc/bind/confirm\"\n );\n if (env.AUTH_TOKEN && NEEDS_AUTH) {\n const auth = req.headers[\"authorization\"] || \"\";\n const provided = auth.startsWith(\"Bearer \") ? auth.slice(7) : (req.headers[\"x-db-token\"] as string) || \"\";\n if (provided !== env.AUTH_TOKEN) {\n json(res, { success: false, error: \"unauthorized\" }, 401);\n return;\n }\n }\n }\n\n try {\n if (path === \"/api/sfmc/player-session\" && method === \"POST\") {\n const data = (await body(req)) as Record<string, unknown>;\n const playerName = String(data.playerName ?? \"\").trim();\n const xuid = String(data.xuid ?? \"\").trim();\n if (!playerName || !xuid) {\n json(res, { success: false, error: \"playerName_and_xuid_required\" }, 400);\n return;\n }\n syncPlayerSession(query, { playerName, xuid });\n json(res, { success: true });\n return;\n }\n // \u2500\u2500 v2 \u8DEF\u7531(\u4F18\u5148\u5339\u914D) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (\n moduleAuthCtx &&\n (path.startsWith(\"/api/sfmc/db/\") ||\n path.startsWith(\"/api/sfmc/services\") ||\n (/^\\/api\\/sfmc\\/configs\\/[A-Za-z0-9_-]+/.test(path) && !isLegacyConfigAll))\n ) {\n const ctx: Record<string, unknown> = {\n path,\n method,\n params,\n req,\n res,\n moduleAuth: moduleAuthCtx,\n };\n // body \u5DF2\u5728\u4E0A\u9762 `await body(req)` \u9884\u8BFB\u5E76\u7F13\u5B58\u5230 req._bodyPromise;\n // \u8FD9\u91CC\u590D\u7528\u7F13\u5B58(\u539F\u5B9E\u73B0\u8BFB\u7684 req._body \u4ECE\u672A\u88AB\u8D4B\u503C,\u5BFC\u81F4\u6240\u6709 v2 \u8DEF\u7531 body \u6052\u4E3A\u7A7A)\u3002\n ctx[\"body\"] = await body(req);\n if (path.startsWith(\"/api/sfmc/db/\")) {\n if (await dbRoutes(ctx)) return;\n } else if (path.startsWith(\"/api/sfmc/services\")) {\n if (await serviceRoutes(ctx)) return;\n } else {\n if (await moduleConfigRoutes(ctx)) return;\n }\n }\n\n // \u2500\u2500 \u5E73\u53F0\u8DEF\u7531 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const ctxBase = { path, method, params, req, res } as {\n path: string;\n method: string;\n params: URLSearchParams;\n req: http.IncomingMessage;\n res: http.ServerResponse;\n };\n if (await contentRoutes(ctxBase)) return;\n if (await moduleRoutesInstance(ctxBase)) return;\n if (await healthRoutes(ctxBase)) return;\n if (await statusRoutes(ctxBase)) return;\n if (await scoreboardRoutes(ctxBase)) return;\n if (await qqBindRoutes(ctxBase)) return;\n if (await bindConfirmRoutes(ctxBase)) return;\n if (await wechatBindRoutes(ctxBase)) return;\n if (await wechatActivityRoutes(ctxBase)) return;\n if (await qqJoinRoutes(ctxBase)) return;\n if (await qqEventsRoutes(ctxBase)) return;\n if (await messagesRoutes(ctxBase)) return;\n if (await configRoutes(ctxBase)) return;\n\n json(res, { success: false, error: \"not_found\" }, 404);\n } catch (err) {\n log.err(err, \"DogeDB\");\n json(res, { success: false, error: (err as Error).message }, 500);\n }\n}\n\n// \u2500\u2500 \u542F\u52A8 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst server = createServer({\n env: { PORT: env.PORT, HOST: env.HOST, AUTH_TOKEN: env.AUTH_TOKEN },\n handle,\n});\nstartConsole(server, db);\n\nexport { db, enabledManifests, env, moduleAuth, query, schemaRegistry, serviceRegistry, txRunner };\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["path"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routes/scoreboard.ts — 世界计分板快照
|
|
3
|
+
*
|
|
4
|
+
* 路由列表:
|
|
5
|
+
* POST /api/sfmc/scoreboard/live — 游戏内平台脚本上报快照 / 哈希 / 空闲探询(走旧 token 鉴权)
|
|
6
|
+
* GET /api/sfmc/scoreboard — CLI 的 scoreboard.read 读取快照,并让脚本在关注期内持续同步
|
|
7
|
+
*
|
|
8
|
+
* 规则见 domain/scoreboard.ts。
|
|
9
|
+
*/
|
|
10
|
+
import { ScoreboardStore } from "../domain/scoreboard.js";
|
|
11
|
+
import { type RouteHandler } from "./_shared.js";
|
|
12
|
+
interface Deps {
|
|
13
|
+
/** 测试可注入独立实例;缺省每个路由实例各持有一个 */
|
|
14
|
+
store?: ScoreboardStore;
|
|
15
|
+
now?: () => number;
|
|
16
|
+
}
|
|
17
|
+
/** 创建计分板路由。使用场景:index.ts 平台路由链。 */
|
|
18
|
+
export declare function createScoreboardRoutes({ store, now }?: Deps): RouteHandler;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=scoreboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scoreboard.d.ts","sourceRoot":"","sources":["../../src/routes/scoreboard.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAyB,eAAe,EAAE,MAAM,yBAAyB,CAAC;AACjF,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAE7D,UAAU,IAAI;IACZ,8BAA8B;IAC9B,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,mCAAmC;AACnC,wBAAgB,sBAAsB,CAAC,EAAE,KAA6B,EAAE,GAAc,EAAE,GAAE,IAAS,GAAG,YAAY,CAuBjH"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { parseScoreboardReport, ScoreboardStore } from "../domain/scoreboard.js";
|
|
2
|
+
import { body, json } from "./_shared.js";
|
|
3
|
+
function createScoreboardRoutes({ store = new ScoreboardStore(), now = Date.now } = {}) {
|
|
4
|
+
return async function handle({ path, method, req, res }) {
|
|
5
|
+
if (path === "/api/sfmc/scoreboard/live") {
|
|
6
|
+
if (method !== "POST") {
|
|
7
|
+
json(res, { success: false, error: "not_found" }, 404);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
const report = parseScoreboardReport(await body(req), now());
|
|
11
|
+
if (!report) {
|
|
12
|
+
json(res, { success: false, error: "invalid_scoreboard_report" }, 400);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
json(res, { success: true, ...store.report(report, now()) });
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (path !== "/api/sfmc/scoreboard") return false;
|
|
19
|
+
if (method !== "GET") {
|
|
20
|
+
json(res, { success: false, error: "not_found" }, 404);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
json(res, { success: true, ...store.read(now()) });
|
|
24
|
+
return true;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
createScoreboardRoutes
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=scoreboard.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/routes/scoreboard.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * routes/scoreboard.ts \u2014 \u4E16\u754C\u8BA1\u5206\u677F\u5FEB\u7167\n *\n * \u8DEF\u7531\u5217\u8868\uFF1A\n * POST /api/sfmc/scoreboard/live \u2014 \u6E38\u620F\u5185\u5E73\u53F0\u811A\u672C\u4E0A\u62A5\u5FEB\u7167 / \u54C8\u5E0C / \u7A7A\u95F2\u63A2\u8BE2\uFF08\u8D70\u65E7 token \u9274\u6743\uFF09\n * GET /api/sfmc/scoreboard \u2014 CLI \u7684 scoreboard.read \u8BFB\u53D6\u5FEB\u7167\uFF0C\u5E76\u8BA9\u811A\u672C\u5728\u5173\u6CE8\u671F\u5185\u6301\u7EED\u540C\u6B65\n *\n * \u89C4\u5219\u89C1 domain/scoreboard.ts\u3002\n */\n\nimport { parseScoreboardReport, ScoreboardStore } from \"../domain/scoreboard.js\";\nimport { body, json, type RouteHandler } from \"./_shared.js\";\n\ninterface Deps {\n /** \u6D4B\u8BD5\u53EF\u6CE8\u5165\u72EC\u7ACB\u5B9E\u4F8B\uFF1B\u7F3A\u7701\u6BCF\u4E2A\u8DEF\u7531\u5B9E\u4F8B\u5404\u6301\u6709\u4E00\u4E2A */\n store?: ScoreboardStore;\n now?: () => number;\n}\n\n/** \u521B\u5EFA\u8BA1\u5206\u677F\u8DEF\u7531\u3002\u4F7F\u7528\u573A\u666F\uFF1Aindex.ts \u5E73\u53F0\u8DEF\u7531\u94FE\u3002 */\nexport function createScoreboardRoutes({ store = new ScoreboardStore(), now = Date.now }: Deps = {}): RouteHandler {\n return async function handle({ path, method, req, res }): Promise<boolean> {\n if (path === \"/api/sfmc/scoreboard/live\") {\n if (method !== \"POST\") {\n json(res, { success: false, error: \"not_found\" }, 404);\n return true;\n }\n const report = parseScoreboardReport(await body(req), now());\n if (!report) {\n json(res, { success: false, error: \"invalid_scoreboard_report\" }, 400);\n return true;\n }\n json(res, { success: true, ...store.report(report, now()) });\n return true;\n }\n if (path !== \"/api/sfmc/scoreboard\") return false;\n if (method !== \"GET\") {\n json(res, { success: false, error: \"not_found\" }, 404);\n return true;\n }\n json(res, { success: true, ...store.read(now()) });\n return true;\n };\n}\n"],
|
|
5
|
+
"mappings": "AAUA,SAAS,uBAAuB,uBAAuB;AACvD,SAAS,MAAM,YAA+B;AASvC,SAAS,uBAAuB,EAAE,QAAQ,IAAI,gBAAgB,GAAG,MAAM,KAAK,IAAI,IAAU,CAAC,GAAiB;AACjH,SAAO,eAAe,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,GAAqB;AACzE,QAAI,SAAS,6BAA6B;AACxC,UAAI,WAAW,QAAQ;AACrB,aAAK,KAAK,EAAE,SAAS,OAAO,OAAO,YAAY,GAAG,GAAG;AACrD,eAAO;AAAA,MACT;AACA,YAAM,SAAS,sBAAsB,MAAM,KAAK,GAAG,GAAG,IAAI,CAAC;AAC3D,UAAI,CAAC,QAAQ;AACX,aAAK,KAAK,EAAE,SAAS,OAAO,OAAO,4BAA4B,GAAG,GAAG;AACrE,eAAO;AAAA,MACT;AACA,WAAK,KAAK,EAAE,SAAS,MAAM,GAAG,MAAM,OAAO,QAAQ,IAAI,CAAC,EAAE,CAAC;AAC3D,aAAO;AAAA,IACT;AACA,QAAI,SAAS,uBAAwB,QAAO;AAC5C,QAAI,WAAW,OAAO;AACpB,WAAK,KAAK,EAAE,SAAS,OAAO,OAAO,YAAY,GAAG,GAAG;AACrD,aAAO;AAAA,IACT;AACA,SAAK,KAAK,EAAE,SAAS,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC,EAAE,CAAC;AACjD,WAAO;AAAA,EACT;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routes/wechat-activity.ts — 微信玩家查看自己的行为记录(平台 HTTP,仅本机)
|
|
3
|
+
*
|
|
4
|
+
* GET /api/sfmc/wechat/activity?openid=&limit=&offset=&eventTypePrefix=&from=&to=
|
|
5
|
+
* → { success, bound, installed, records, total, limit, offset }
|
|
6
|
+
*
|
|
7
|
+
* 使用场景:mp-gateway 的玩家端 /player/activity 代小程序「我的行为记录」页调用。
|
|
8
|
+
* 先按 openid 找到绑定的游戏账号,再把执行者强制限定为该账号(绑定表的 player_xuid 即游戏内 player.id,
|
|
9
|
+
* 与行为日志的 actor_id 相同),调用方传入的 actorId / actorName 一律忽略,玩家只能看到自己的记录。
|
|
10
|
+
* openid 由 mp-gateway 通过微信 code2session 取得,信任边界与 routes/wechat-bind.ts 相同。
|
|
11
|
+
*/
|
|
12
|
+
import type http from "node:http";
|
|
13
|
+
import type { QueryFn } from "../lib/sqlite.js";
|
|
14
|
+
interface Deps {
|
|
15
|
+
query: QueryFn;
|
|
16
|
+
json: (res: http.ServerResponse, data: Record<string, unknown>, status?: number) => void;
|
|
17
|
+
}
|
|
18
|
+
/** 路由上下文(与其他平台路由一致) */
|
|
19
|
+
interface Ctx {
|
|
20
|
+
path: string;
|
|
21
|
+
method: string;
|
|
22
|
+
params: URLSearchParams;
|
|
23
|
+
req: http.IncomingMessage;
|
|
24
|
+
res: http.ServerResponse;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 创建微信玩家行为记录路由。
|
|
28
|
+
* 使用场景:index.ts 装配到平台路由链。
|
|
29
|
+
*/
|
|
30
|
+
declare function createWechatActivityRoutes({ query, json }: Deps): ({ path, method, params, res }: Ctx) => Promise<boolean>;
|
|
31
|
+
export { createWechatActivityRoutes };
|
|
32
|
+
//# sourceMappingURL=wechat-activity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wechat-activity.d.ts","sourceRoot":"","sources":["../../src/routes/wechat-activity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAGlC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,UAAU,IAAI;IACZ,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1F;AAED,uBAAuB;AACvB,UAAU,GAAG;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,CAAC;IACxB,GAAG,EAAE,IAAI,CAAC,eAAe,CAAC;IAC1B,GAAG,EAAE,IAAI,CAAC,cAAc,CAAC;CAC1B;AAID;;;GAGG;AACH,iBAAS,0BAA0B,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,IAAI,mCACK,GAAG,KAAG,OAAO,CAAC,OAAO,CAAC,CAgCnF;AAED,OAAO,EAAE,0BAA0B,EAAE,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ActivityQueryError, parseActivityFilter, parseActivityPaging, queryActivityPage } from "../domain/activity-log.js";
|
|
2
|
+
import { findWechatBindingByOpenid } from "../domain/wechat-bind.js";
|
|
3
|
+
const ROUTE = "/api/sfmc/wechat/activity";
|
|
4
|
+
function createWechatActivityRoutes({ query, json }) {
|
|
5
|
+
return async function handle({ path, method, params, res }) {
|
|
6
|
+
if (path !== ROUTE) return false;
|
|
7
|
+
if (method !== "GET") {
|
|
8
|
+
json(res, { success: false, error: "method_not_allowed" }, 405);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
const openid = String(params.get("openid") ?? "").trim();
|
|
12
|
+
if (!openid) {
|
|
13
|
+
json(res, { success: false, error: "openid_required" }, 400);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
const binding = findWechatBindingByOpenid(query, openid);
|
|
17
|
+
if (!binding?.player_xuid) {
|
|
18
|
+
json(res, { success: true, bound: false, installed: false, records: [], total: 0, limit: 0, offset: 0 });
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
const raw = Object.fromEntries(params.entries());
|
|
22
|
+
let page;
|
|
23
|
+
try {
|
|
24
|
+
const { actorId: _actorId, actorName: _actorName, ...filter } = parseActivityFilter(raw);
|
|
25
|
+
page = queryActivityPage(query, { ...filter, actorId: binding.player_xuid }, parseActivityPaging(raw));
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error instanceof ActivityQueryError) {
|
|
28
|
+
json(res, { success: false, error: "invalid_params", message: error.message }, 400);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
json(res, { success: true, bound: true, ...page });
|
|
34
|
+
return true;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
createWechatActivityRoutes
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=wechat-activity.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/routes/wechat-activity.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * routes/wechat-activity.ts \u2014 \u5FAE\u4FE1\u73A9\u5BB6\u67E5\u770B\u81EA\u5DF1\u7684\u884C\u4E3A\u8BB0\u5F55\uFF08\u5E73\u53F0 HTTP\uFF0C\u4EC5\u672C\u673A\uFF09\n *\n * GET /api/sfmc/wechat/activity?openid=&limit=&offset=&eventTypePrefix=&from=&to=\n * \u2192 { success, bound, installed, records, total, limit, offset }\n *\n * \u4F7F\u7528\u573A\u666F\uFF1Amp-gateway \u7684\u73A9\u5BB6\u7AEF /player/activity \u4EE3\u5C0F\u7A0B\u5E8F\u300C\u6211\u7684\u884C\u4E3A\u8BB0\u5F55\u300D\u9875\u8C03\u7528\u3002\n * \u5148\u6309 openid \u627E\u5230\u7ED1\u5B9A\u7684\u6E38\u620F\u8D26\u53F7\uFF0C\u518D\u628A\u6267\u884C\u8005\u5F3A\u5236\u9650\u5B9A\u4E3A\u8BE5\u8D26\u53F7\uFF08\u7ED1\u5B9A\u8868\u7684 player_xuid \u5373\u6E38\u620F\u5185 player.id\uFF0C\n * \u4E0E\u884C\u4E3A\u65E5\u5FD7\u7684 actor_id \u76F8\u540C\uFF09\uFF0C\u8C03\u7528\u65B9\u4F20\u5165\u7684 actorId / actorName \u4E00\u5F8B\u5FFD\u7565\uFF0C\u73A9\u5BB6\u53EA\u80FD\u770B\u5230\u81EA\u5DF1\u7684\u8BB0\u5F55\u3002\n * openid \u7531 mp-gateway \u901A\u8FC7\u5FAE\u4FE1 code2session \u53D6\u5F97\uFF0C\u4FE1\u4EFB\u8FB9\u754C\u4E0E routes/wechat-bind.ts \u76F8\u540C\u3002\n */\n\nimport type http from \"node:http\";\nimport { ActivityQueryError, parseActivityFilter, parseActivityPaging, queryActivityPage } from \"../domain/activity-log.js\";\nimport { findWechatBindingByOpenid } from \"../domain/wechat-bind.js\";\nimport type { QueryFn } from \"../lib/sqlite.js\";\n\ninterface Deps {\n query: QueryFn;\n json: (res: http.ServerResponse, data: Record<string, unknown>, status?: number) => void;\n}\n\n/** \u8DEF\u7531\u4E0A\u4E0B\u6587\uFF08\u4E0E\u5176\u4ED6\u5E73\u53F0\u8DEF\u7531\u4E00\u81F4\uFF09 */\ninterface Ctx {\n path: string;\n method: string;\n params: URLSearchParams;\n req: http.IncomingMessage;\n res: http.ServerResponse;\n}\n\nconst ROUTE = \"/api/sfmc/wechat/activity\";\n\n/**\n * \u521B\u5EFA\u5FAE\u4FE1\u73A9\u5BB6\u884C\u4E3A\u8BB0\u5F55\u8DEF\u7531\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1Aindex.ts \u88C5\u914D\u5230\u5E73\u53F0\u8DEF\u7531\u94FE\u3002\n */\nfunction createWechatActivityRoutes({ query, json }: Deps) {\n return async function handle({ path, method, params, res }: Ctx): Promise<boolean> {\n if (path !== ROUTE) return false;\n if (method !== \"GET\") {\n json(res, { success: false, error: \"method_not_allowed\" }, 405);\n return true;\n }\n const openid = String(params.get(\"openid\") ?? \"\").trim();\n if (!openid) {\n json(res, { success: false, error: \"openid_required\" }, 400);\n return true;\n }\n const binding = findWechatBindingByOpenid(query, openid);\n // \u7A7A player_xuid \u4E0D\u80FD\u4F5C\u4E3A\u6267\u884C\u8005\u6761\u4EF6\uFF08\u7A7A\u503C\u4F1A\u88AB\u7B5B\u9009\u6761\u4EF6\u5FFD\u7565\u800C\u67E5\u51FA\u6240\u6709\u4EBA\u7684\u8BB0\u5F55\uFF09\uFF0C\u6309\u672A\u7ED1\u5B9A\u5904\u7406\n if (!binding?.player_xuid) {\n json(res, { success: true, bound: false, installed: false, records: [], total: 0, limit: 0, offset: 0 });\n return true;\n }\n const raw = Object.fromEntries(params.entries());\n let page;\n try {\n const { actorId: _actorId, actorName: _actorName, ...filter } = parseActivityFilter(raw);\n page = queryActivityPage(query, { ...filter, actorId: binding.player_xuid }, parseActivityPaging(raw));\n } catch (error) {\n if (error instanceof ActivityQueryError) {\n json(res, { success: false, error: \"invalid_params\", message: error.message }, 400);\n return true;\n }\n throw error;\n }\n json(res, { success: true, bound: true, ...page });\n return true;\n };\n}\n\nexport { createWechatActivityRoutes };\n"],
|
|
5
|
+
"mappings": "AAaA,SAAS,oBAAoB,qBAAqB,qBAAqB,yBAAyB;AAChG,SAAS,iCAAiC;AAiB1C,MAAM,QAAQ;AAMd,SAAS,2BAA2B,EAAE,OAAO,KAAK,GAAS;AACzD,SAAO,eAAe,OAAO,EAAE,MAAM,QAAQ,QAAQ,IAAI,GAA0B;AACjF,QAAI,SAAS,MAAO,QAAO;AAC3B,QAAI,WAAW,OAAO;AACpB,WAAK,KAAK,EAAE,SAAS,OAAO,OAAO,qBAAqB,GAAG,GAAG;AAC9D,aAAO;AAAA,IACT;AACA,UAAM,SAAS,OAAO,OAAO,IAAI,QAAQ,KAAK,EAAE,EAAE,KAAK;AACvD,QAAI,CAAC,QAAQ;AACX,WAAK,KAAK,EAAE,SAAS,OAAO,OAAO,kBAAkB,GAAG,GAAG;AAC3D,aAAO;AAAA,IACT;AACA,UAAM,UAAU,0BAA0B,OAAO,MAAM;AAEvD,QAAI,CAAC,SAAS,aAAa;AACzB,WAAK,KAAK,EAAE,SAAS,MAAM,OAAO,OAAO,WAAW,OAAO,SAAS,CAAC,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,EAAE,CAAC;AACvG,aAAO;AAAA,IACT;AACA,UAAM,MAAM,OAAO,YAAY,OAAO,QAAQ,CAAC;AAC/C,QAAI;AACJ,QAAI;AACF,YAAM,EAAE,SAAS,UAAU,WAAW,YAAY,GAAG,OAAO,IAAI,oBAAoB,GAAG;AACvF,aAAO,kBAAkB,OAAO,EAAE,GAAG,QAAQ,SAAS,QAAQ,YAAY,GAAG,oBAAoB,GAAG,CAAC;AAAA,IACvG,SAAS,OAAO;AACd,UAAI,iBAAiB,oBAAoB;AACvC,aAAK,KAAK,EAAE,SAAS,OAAO,OAAO,kBAAkB,SAAS,MAAM,QAAQ,GAAG,GAAG;AAClF,eAAO;AAAA,MACT;AACA,YAAM;AAAA,IACR;AACA,SAAK,KAAK,EAAE,SAAS,MAAM,OAAO,MAAM,GAAG,KAAK,CAAC;AACjD,WAAO;AAAA,EACT;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sfmc-bds/db-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "SFMC SQLite HTTP REST backend — configs, modules, gameplay data",
|
|
5
5
|
"author": "Shiroha7z",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -42,6 +42,16 @@
|
|
|
42
42
|
"import": "./dist/domain/qq-bindings.js",
|
|
43
43
|
"default": "./dist/domain/qq-bindings.js"
|
|
44
44
|
},
|
|
45
|
+
"./activity-log": {
|
|
46
|
+
"types": "./dist/domain/activity-log.d.ts",
|
|
47
|
+
"import": "./dist/domain/activity-log.js",
|
|
48
|
+
"default": "./dist/domain/activity-log.js"
|
|
49
|
+
},
|
|
50
|
+
"./scoreboard": {
|
|
51
|
+
"types": "./dist/domain/scoreboard.d.ts",
|
|
52
|
+
"import": "./dist/domain/scoreboard.js",
|
|
53
|
+
"default": "./dist/domain/scoreboard.js"
|
|
54
|
+
},
|
|
45
55
|
"./package.json": "./package.json"
|
|
46
56
|
},
|
|
47
57
|
"files": [
|
|
@@ -50,11 +60,11 @@
|
|
|
50
60
|
],
|
|
51
61
|
"dependencies": {
|
|
52
62
|
"sql-template-strings": "^2.2.2",
|
|
53
|
-
"@sfmc-bds/
|
|
54
|
-
"@sfmc-bds/
|
|
63
|
+
"@sfmc-bds/sdk": "0.7.0",
|
|
64
|
+
"@sfmc-bds/bds-tools": "0.3.1"
|
|
55
65
|
},
|
|
56
66
|
"devDependencies": {
|
|
57
|
-
"@sfmc-bds/tools": "0.2.
|
|
67
|
+
"@sfmc-bds/tools": "0.2.12"
|
|
58
68
|
},
|
|
59
69
|
"publishConfig": {
|
|
60
70
|
"access": "public",
|