koishi-plugin-prism 0.1.10 → 0.1.15
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/README.md +3 -4
- package/lib/index.d.ts +0 -1
- package/lib/index.js +114 -79
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
* 🎮 **玩家入场与结算**:通过 `/login` 和 `/logout` 指令开启或结算计费场次,支持 `/入场` 别名;命令回复会引用触发消息,结账账单可私聊通知管理员与指定用户。
|
|
10
10
|
* 💳 **账户钱包与资产管理**:支持查询钱包余额(`/wallet`)和持有的道具资产(`/items`)。
|
|
11
|
-
* 🀄 **麻将桌位集成**:包含 `/mahjong <tableId>` 以及便捷的 `/上桌` / `/下桌` 控制。`上桌` 仅允许已通过 `login`/`入场` 开启默认入场会话的玩家使用。`list`
|
|
11
|
+
* 🀄 **麻将桌位集成**:包含 `/mahjong <tableId>` 以及便捷的 `/上桌` / `/下桌` 控制。`上桌` 仅允许已通过 `login`/`入场` 开启默认入场会话的玩家使用。`list` 按 session 标签分组:有非音乐游戏 session 的玩家归入最新的非音乐标签,纯音乐玩家归入音乐标签;麻将桌额外显示当前人数和容量。已开局桌位会从后端活跃 session 自动恢复,未满桌候座仍由机器人进程暂存,重启后不会保留。
|
|
12
12
|
* 🔌 **硬件设备状态与电源管理**:可直接在聊天中查看设备状态(`/show`)、远程开启/关闭电源(`/on`、`/off`)、远程投币(`/coin`)和模拟刷卡(`/scan`)。
|
|
13
13
|
* 🎟️ **礼物兑换码**:使用 `/redeem <code>` 兑换系统发放的福利礼包。
|
|
14
14
|
* 🛠️ **管理员快捷指令**:允许管理员为指定平台用户增加或扣除余额,并覆盖结账金额后立即结账。
|
|
@@ -31,10 +31,9 @@
|
|
|
31
31
|
| `resolveDisplayName` | `function` | - | 可选。自定义用于获取群内昵称作为玩家注册名的异步逻辑。 |
|
|
32
32
|
| `enableStaffCommands` | `boolean` | `false` | 是否开启管理员快捷指令。 |
|
|
33
33
|
| `staffUserIds` | `string[]` | `[]` | 允许执行管理员快捷指令的平台用户 ID(如 QQ 号)白名单。空列表不授予目标用户操作权限。 |
|
|
34
|
-
| `staffSessionToken` | `string` | - | 管理员写操作所需的 Staff 管理 Token。 |
|
|
35
34
|
| `logoutNotifyUserIds` | `string[]` | `[]` | 结账成功后额外私聊完整账单的平台用户 ID。通知收件人为该列表与 `staffUserIds` 的去重并集。 |
|
|
36
35
|
|
|
37
|
-
管理员快捷指令必须同时配置 `enableStaffCommands: true
|
|
36
|
+
管理员快捷指令必须同时配置 `enableStaffCommands: true` 与 `staffUserIds`。它们使用现有 `integrationToken` 调用受限的余额调整和立即结账接口;目标用户参数使用 Koishi 的 `user` 选择器,只有白名单内的管理员可以操作其他用户。
|
|
38
37
|
|
|
39
38
|
## 📝 机器人指令列表
|
|
40
39
|
|
|
@@ -47,7 +46,7 @@
|
|
|
47
46
|
* `billing` - 预览当前玩家本场计费的消费费用。
|
|
48
47
|
* `wallet` - 查看当前玩家的钱包余额。
|
|
49
48
|
* `items` - 查看当前玩家持有的道具或资产。
|
|
50
|
-
* `list` -
|
|
49
|
+
* `list` - 查看当前在线/在店游玩玩家的列表,按 session 标签分组并对同一玩家去重;存在非音乐游戏 session 时取最新的非音乐标签,麻将桌显示当前人数和容量。已开局桌位由后端 session 恢复;未满桌候座由机器人进程暂存,机器人重启后不会保留。
|
|
51
50
|
* `show [deviceId]` - 查看设备电源与连接状态。
|
|
52
51
|
* `history` - 查看自己的历史游玩记录。
|
|
53
52
|
* `lock` - 发送开门指令。
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -12,7 +12,6 @@ exports.Config = koishi_1.Schema.object({
|
|
|
12
12
|
autoRegister: koishi_1.Schema.boolean().default(true).description("是否自动注册"),
|
|
13
13
|
baseUrl: koishi_1.Schema.string().description("PRiSM 后端 API Base URL"),
|
|
14
14
|
integrationToken: koishi_1.Schema.string().role("secret").description("集成 API Token"),
|
|
15
|
-
staffSessionToken: koishi_1.Schema.string().role("secret").description("Staff 管理 Token (可选)"),
|
|
16
15
|
currencyName: koishi_1.Schema.string().default("猫粮").description("代币名称"),
|
|
17
16
|
defaultDoorDeviceId: koishi_1.Schema.string().default("front-door").description("默认开门设备ID"),
|
|
18
17
|
defaultScanProvider: koishi_1.Schema.string().default("aime").description("默认刷卡提供商"),
|
|
@@ -67,27 +66,27 @@ function applyPrismKoishiPlugin(ctx, config) {
|
|
|
67
66
|
}
|
|
68
67
|
};
|
|
69
68
|
ctx.command("register", "绑定或注册当前平台用户到 PRiSM").action(wrap(async (context) => service.register(await service.sender(context))));
|
|
70
|
-
ctx.command("login [target:user]", "开启玩家计费场次").action(wrap(async (context, target) => service.loginForTarget(await service.sender(context), target)));
|
|
71
|
-
ctx.command("入场 [target:user]", "入场").action(wrap(async (context, target) => service.loginForTarget(await service.sender(context), target)));
|
|
69
|
+
ctx.command("login [target:user]", "开启玩家计费场次").action(wrap(async (context, target) => service.loginForTarget(await service.sender(context), target, context.session?.bot)));
|
|
70
|
+
ctx.command("入场 [target:user]", "入场").action(wrap(async (context, target) => service.loginForTarget(await service.sender(context), target, context.session?.bot)));
|
|
72
71
|
ctx.command("mahjong <tableId>", "加入指定麻将桌").action(wrap(async (context, tableId) => service.mahjongJoin(await service.sender(context), tableId)));
|
|
73
72
|
ctx.command("上桌 <tableId>", "加入指定麻将桌").action(wrap(async (context, tableId) => service.mahjongJoin(await service.sender(context), tableId)));
|
|
74
73
|
ctx.command("下桌 <tableId>", "离开指定麻将桌").action(wrap(async (context, tableId) => service.mahjongLeave(await service.sender(context), tableId)));
|
|
75
|
-
ctx.command("logout [target:user]", "结算玩家计费场次").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.logout(sender, context.session?.bot))));
|
|
76
|
-
ctx.command("billing [target:user]", "预览玩家结账费用").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.billing(sender))));
|
|
77
|
-
ctx.command("wallet [target:user]", "查看玩家钱包").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.wallet(sender))));
|
|
78
|
-
ctx.command("items [target:user]", "查看玩家资产").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.items(sender))));
|
|
74
|
+
ctx.command("logout [target:user]", "结算玩家计费场次").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.logout(sender, context.session?.bot), context.session?.bot)));
|
|
75
|
+
ctx.command("billing [target:user]", "预览玩家结账费用").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.billing(sender), context.session?.bot)));
|
|
76
|
+
ctx.command("wallet [target:user]", "查看玩家钱包").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.wallet(sender), context.session?.bot)));
|
|
77
|
+
ctx.command("items [target:user]", "查看玩家资产").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.items(sender), context.session?.bot)));
|
|
79
78
|
ctx.command("list", "查看当前在线玩家列表").action(wrap(async (context) => service.listActiveSessions(await service.sender(context))));
|
|
80
79
|
ctx.command("show [deviceId]", "查看设备电源状态").action(wrap(async (context, deviceId) => service.listDeviceStates(deviceId)));
|
|
81
|
-
ctx.command("history [target:user]", "查看玩家历史").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.history(sender))));
|
|
80
|
+
ctx.command("history [target:user]", "查看玩家历史").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.history(sender), context.session?.bot)));
|
|
82
81
|
ctx.command("lock", "向默认门锁设备发送开门指令").action(wrap(async (context) => service.lock(await service.sender(context))));
|
|
83
82
|
ctx.command("on <deviceId>", "请求启动指定设备电源").action(wrap(async (context, deviceId) => service.powerOn(await service.sender(context), deviceId)));
|
|
84
83
|
ctx.command("off <deviceId>", "请求关闭指定设备电源").action(wrap(async (context, deviceId) => service.powerOff(await service.sender(context), deviceId)));
|
|
85
84
|
ctx.command("coin <deviceId> [count]", "请求向指定设备投币").action(wrap(async (context, deviceId, count) => service.coin(await service.sender(context), deviceId, count)));
|
|
86
85
|
ctx.command("scan <deviceId> <subject>", "请求指定设备模拟刷卡").action(wrap(async (context, deviceId, subject) => service.scan(await service.sender(context), deviceId, subject)));
|
|
87
86
|
ctx.command("redeem <code>", "兑换 PRiSM 礼物码").action(wrap(async (context, code) => service.redeem(await service.sender(context), code)));
|
|
88
|
-
ctx.command("add <target:user> <amount:number>", "增加玩家余额").action(wrap(async (context, target, amount) => service.adjustTargetBalance(await service.sender(context), target, amount, 1)));
|
|
89
|
-
ctx.command("del <target:user> <amount:number>", "扣除玩家余额").action(wrap(async (context, target, amount) => service.adjustTargetBalance(await service.sender(context), target, amount, -1)));
|
|
90
|
-
ctx.command("overwrite <target:user> <amount:number> [reason:text]", "覆盖结账金额并立即结账").action(wrap(async (context, target, amount, reason) => service.overwriteTargetCheckout(await service.sender(context), target, amount, reason)));
|
|
87
|
+
ctx.command("add <target:user> <amount:number>", "增加玩家余额").action(wrap(async (context, target, amount) => service.adjustTargetBalance(await service.sender(context), target, amount, 1, context.session?.bot)));
|
|
88
|
+
ctx.command("del <target:user> <amount:number>", "扣除玩家余额").action(wrap(async (context, target, amount) => service.adjustTargetBalance(await service.sender(context), target, amount, -1, context.session?.bot)));
|
|
89
|
+
ctx.command("overwrite <target:user> <amount:number> [reason:text]", "覆盖结账金额并立即结账").action(wrap(async (context, target, amount, reason) => service.overwriteTargetCheckout(await service.sender(context), target, amount, reason, context.session?.bot)));
|
|
91
90
|
const intervalMs = (config.powerOffInterval ?? 0) * 1000;
|
|
92
91
|
if (intervalMs > 0 && typeof ctx.setInterval === "function") {
|
|
93
92
|
ctx.setInterval(() => {
|
|
@@ -150,12 +149,6 @@ class PrismApiClient {
|
|
|
150
149
|
throw new PrismBotClientError(error.message || "Network error", "NETWORK_ERROR", 500, {});
|
|
151
150
|
}
|
|
152
151
|
}
|
|
153
|
-
requireStaffSessionToken() {
|
|
154
|
-
if (!this.config.staffSessionToken) {
|
|
155
|
-
throw new PrismBotClientError("Staff session token is required for this Bot shortcut.", "STAFF_TOKEN_REQUIRED", 0, {});
|
|
156
|
-
}
|
|
157
|
-
return this.config.staffSessionToken;
|
|
158
|
-
}
|
|
159
152
|
identityBody(identity) {
|
|
160
153
|
return {
|
|
161
154
|
identity: {
|
|
@@ -273,18 +266,16 @@ class PrismApiClient {
|
|
|
273
266
|
token: this.config.integrationToken,
|
|
274
267
|
});
|
|
275
268
|
}
|
|
276
|
-
async
|
|
277
|
-
return this.request("POST", "/rpc/
|
|
278
|
-
token: this.
|
|
279
|
-
|
|
280
|
-
body: { adjustments },
|
|
269
|
+
async adjustAssetsByIdentity(identity, adjustments) {
|
|
270
|
+
return this.request("POST", "/rpc/integration/players/by-identity/assets/adjustments", {
|
|
271
|
+
token: this.config.integrationToken,
|
|
272
|
+
body: { ...this.identityBody(identity), adjustments },
|
|
281
273
|
});
|
|
282
274
|
}
|
|
283
|
-
async
|
|
284
|
-
return this.request("POST", "/rpc/
|
|
285
|
-
token: this.
|
|
286
|
-
|
|
287
|
-
body: { total, reason },
|
|
275
|
+
async checkoutWithOverrideByIdentity(identity, total, reason) {
|
|
276
|
+
return this.request("POST", "/rpc/integration/players/by-identity/checkout/override", {
|
|
277
|
+
token: this.config.integrationToken,
|
|
278
|
+
body: { ...this.identityBody(identity), total, reason },
|
|
288
279
|
});
|
|
289
280
|
}
|
|
290
281
|
}
|
|
@@ -328,7 +319,6 @@ class PrismKoishiService {
|
|
|
328
319
|
this.client = new PrismApiClient(http, {
|
|
329
320
|
baseUrl: config.baseUrl,
|
|
330
321
|
integrationToken: config.integrationToken,
|
|
331
|
-
staffSessionToken: config.staffSessionToken,
|
|
332
322
|
});
|
|
333
323
|
}
|
|
334
324
|
}
|
|
@@ -352,54 +342,46 @@ class PrismKoishiService {
|
|
|
352
342
|
await this.client.resolveOrRegisterIdentity(this.identity(sender));
|
|
353
343
|
return "注册成功";
|
|
354
344
|
}
|
|
355
|
-
async loginForTarget(actor, targetSubject) {
|
|
345
|
+
async loginForTarget(actor, targetSubject, bot) {
|
|
356
346
|
return this.withTarget(actor, targetSubject, async (sender, isTargeted) => {
|
|
357
347
|
await this.client.startSessionByIdentity(this.identity(sender), this.loginSessionBody());
|
|
358
|
-
return isTargeted ? `✅ 已为用户 ${sender
|
|
359
|
-
});
|
|
348
|
+
return isTargeted ? `✅ 已为用户 ${formatPlayerReference(sender)} 入场成功` : "✅ 入场成功";
|
|
349
|
+
}, bot);
|
|
360
350
|
}
|
|
361
351
|
async login(sender) {
|
|
362
352
|
await this.client.startSessionByIdentity(this.identity(sender), this.loginSessionBody());
|
|
363
353
|
return "✅ 入场成功";
|
|
364
354
|
}
|
|
365
|
-
async withTarget(actor, targetSubject, action) {
|
|
366
|
-
const target = this.targetSender(actor, targetSubject);
|
|
355
|
+
async withTarget(actor, targetSubject, action, bot) {
|
|
356
|
+
const target = await this.targetSender(actor, targetSubject, bot);
|
|
367
357
|
if (typeof target === "string")
|
|
368
358
|
return target;
|
|
369
359
|
return action(target, target !== actor);
|
|
370
360
|
}
|
|
371
|
-
async adjustTargetBalance(actor, targetSubject, rawAmount, direction) {
|
|
361
|
+
async adjustTargetBalance(actor, targetSubject, rawAmount, direction, bot) {
|
|
372
362
|
return this.withTarget(actor, targetSubject, async (sender) => {
|
|
373
363
|
const amount = Number(rawAmount);
|
|
374
364
|
if (!Number.isFinite(amount) || amount <= 0)
|
|
375
365
|
return "金额必须大于 0";
|
|
376
|
-
const player = await this.resolvePlayer(sender);
|
|
377
|
-
const playerId = String(player.id ?? "");
|
|
378
|
-
if (!playerId)
|
|
379
|
-
return "找不到玩家";
|
|
380
366
|
const isAddition = direction === 1;
|
|
381
|
-
await this.client.
|
|
367
|
+
await this.client.adjustAssetsByIdentity(this.identity(sender), [{
|
|
382
368
|
assetType: "currency",
|
|
383
369
|
assetCode: "paid",
|
|
384
370
|
quantityDelta: amount * direction,
|
|
385
371
|
reason: isAddition ? "Koishi 管理员增加余额" : "Koishi 管理员扣除余额",
|
|
386
372
|
}]);
|
|
387
373
|
return `✅ 已为用户 ${sender.id}${isAddition ? "增加" : "扣除"} ${formatNumber(amount)} ${this.config.currencyName}`;
|
|
388
|
-
});
|
|
374
|
+
}, bot);
|
|
389
375
|
}
|
|
390
|
-
async overwriteTargetCheckout(actor, targetSubject, rawAmount, rawReason) {
|
|
376
|
+
async overwriteTargetCheckout(actor, targetSubject, rawAmount, rawReason, bot) {
|
|
391
377
|
return this.withTarget(actor, targetSubject, async (sender) => {
|
|
392
378
|
const total = Number(rawAmount);
|
|
393
379
|
if (!Number.isFinite(total) || total < 0)
|
|
394
380
|
return "金额必须为非负数";
|
|
395
|
-
const player = await this.resolvePlayer(sender);
|
|
396
|
-
const playerId = String(player.id ?? "");
|
|
397
|
-
if (!playerId)
|
|
398
|
-
return "找不到玩家";
|
|
399
381
|
const reason = cleanText(rawReason) || "Koishi 管理员手动调价";
|
|
400
|
-
await this.client.
|
|
401
|
-
return `✅ 已为用户 ${sender
|
|
402
|
-
});
|
|
382
|
+
await this.client.checkoutWithOverrideByIdentity(this.identity(sender), total, reason);
|
|
383
|
+
return `✅ 已为用户 ${formatPlayerReference(sender)} 覆盖结账为 ${formatNumber(total)} ${this.config.currencyName}`;
|
|
384
|
+
}, bot);
|
|
403
385
|
}
|
|
404
386
|
async mahjongJoin(sender, rawTableId) {
|
|
405
387
|
const tableId = cleanText(rawTableId);
|
|
@@ -414,6 +396,7 @@ class PrismKoishiService {
|
|
|
414
396
|
const playerId = String(player.id ?? "");
|
|
415
397
|
const activeResult = (await this.client.listActiveSessions());
|
|
416
398
|
const activeSessions = (activeResult.sessions ?? []);
|
|
399
|
+
this.syncMahjongTableStates(activeSessions);
|
|
417
400
|
if (!this.hasEntrySession(playerId, activeSessions)) {
|
|
418
401
|
return "请先入场后再上桌。";
|
|
419
402
|
}
|
|
@@ -457,6 +440,8 @@ class PrismKoishiService {
|
|
|
457
440
|
const tableConfig = this.mahjongTableConfigs().get(tableId);
|
|
458
441
|
const tableKey = tableConfig?.tableId ?? tableId;
|
|
459
442
|
const tableSubject = tableConfig?.displayName || `${tableId} 桌`;
|
|
443
|
+
const activeResult = (await this.client.listActiveSessions());
|
|
444
|
+
this.syncMahjongTableStates((activeResult.sessions ?? []));
|
|
460
445
|
const state = this.mahjongTables.get(tableKey);
|
|
461
446
|
if (!state)
|
|
462
447
|
return `你不在 ${tableSubject}。`;
|
|
@@ -585,6 +570,7 @@ class PrismKoishiService {
|
|
|
585
570
|
async listActiveSessions(sender) {
|
|
586
571
|
const result = (await this.client.listActiveSessions());
|
|
587
572
|
const sessions = (result?.sessions ?? []);
|
|
573
|
+
this.syncMahjongTableStates(sessions);
|
|
588
574
|
const tableByLabel = new Map(uniqueMahjongConfigs(this.mahjongTableConfigs()).map((table) => [
|
|
589
575
|
mahjongSessionLabel(table, this.config.mahjongLabelPrefix ?? "麻将桌"),
|
|
590
576
|
table,
|
|
@@ -668,41 +654,44 @@ class PrismKoishiService {
|
|
|
668
654
|
|| "未知玩家";
|
|
669
655
|
}
|
|
670
656
|
async buildPlayerGroups(players, tableByLabel) {
|
|
671
|
-
const groups = {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
};
|
|
675
|
-
const groupForTable = new Map(groups.mahjong.map((group) => [group.table.tableId, group]));
|
|
657
|
+
const groups = { groups: [] };
|
|
658
|
+
const groupByLabel = new Map();
|
|
659
|
+
const musicLabel = this.config.loginSessionLabel?.trim() || "音游区间";
|
|
676
660
|
for (const player of players.values()) {
|
|
677
661
|
player.displayName = await this.displayNameForPlayer(player);
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
662
|
+
const nonMusic = player.sessions.filter((session) => Boolean(session.label) && session.label !== musicLabel);
|
|
663
|
+
const source = nonMusic.length > 0 ? nonMusic : player.sessions;
|
|
664
|
+
const selected = source.reduce((latest, session) => !latest || sessionStartedAt(session) > sessionStartedAt(latest) ? session : latest, undefined);
|
|
665
|
+
const label = selected?.label || musicLabel;
|
|
666
|
+
let group = groupByLabel.get(label);
|
|
667
|
+
if (!group) {
|
|
668
|
+
group = { label, table: tableByLabel.get(label), players: [] };
|
|
669
|
+
groupByLabel.set(label, group);
|
|
670
|
+
groups.groups.push(group);
|
|
686
671
|
}
|
|
672
|
+
group.players.push(player);
|
|
687
673
|
}
|
|
688
674
|
return groups;
|
|
689
675
|
}
|
|
690
676
|
mergeWaitingSeats(groups) {
|
|
691
677
|
for (const [tableId, state] of this.mahjongTables) {
|
|
692
|
-
const
|
|
693
|
-
if (!
|
|
678
|
+
const table = this.mahjongTableConfigs().get(tableId);
|
|
679
|
+
if (!table)
|
|
694
680
|
continue;
|
|
681
|
+
const label = mahjongSessionLabel(table, this.config.mahjongLabelPrefix ?? "麻将桌");
|
|
682
|
+
let group = groups.groups.find((candidate) => candidate.label === label);
|
|
683
|
+
if (!group) {
|
|
684
|
+
group = { label, table, players: [] };
|
|
685
|
+
groups.groups.push(group);
|
|
686
|
+
}
|
|
695
687
|
for (const seat of state.waiting) {
|
|
696
688
|
if (group.players.some((player) => player.playerId === seat.playerId))
|
|
697
689
|
continue;
|
|
698
|
-
const existing =
|
|
699
|
-
.flat()
|
|
690
|
+
const existing = groups.groups.flatMap((candidate) => candidate.players)
|
|
700
691
|
.find((player) => player.playerId === seat.playerId);
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (candidate !== group) {
|
|
692
|
+
for (const candidate of groups.groups) {
|
|
693
|
+
if (candidate !== group)
|
|
704
694
|
candidate.players = candidate.players.filter((player) => player.playerId !== seat.playerId);
|
|
705
|
-
}
|
|
706
695
|
}
|
|
707
696
|
group.players.push(existing ?? { playerId: seat.playerId, sessions: [], displayName: seat.displayName });
|
|
708
697
|
}
|
|
@@ -729,6 +718,32 @@ class PrismKoishiService {
|
|
|
729
718
|
mahjongTableConfigs() {
|
|
730
719
|
return parseMahjongTables(this.config.mahjongTables ?? "", this.config.mahjongLabelPrefix ?? "麻将桌");
|
|
731
720
|
}
|
|
721
|
+
syncMahjongTableStates(sessions) {
|
|
722
|
+
const tables = uniqueMahjongConfigs(this.mahjongTableConfigs());
|
|
723
|
+
const tableByLabel = new Map(tables.map((table) => [
|
|
724
|
+
mahjongSessionLabel(table, this.config.mahjongLabelPrefix ?? "麻将桌"),
|
|
725
|
+
table,
|
|
726
|
+
]));
|
|
727
|
+
const activeByTable = new Map();
|
|
728
|
+
for (const session of sessions) {
|
|
729
|
+
const table = tableByLabel.get(session.label ?? "");
|
|
730
|
+
const playerId = String(session.playerId ?? "");
|
|
731
|
+
const sessionId = String(session.id ?? "");
|
|
732
|
+
if (!table || !playerId || !sessionId)
|
|
733
|
+
continue;
|
|
734
|
+
const active = activeByTable.get(table.tableId) ?? {};
|
|
735
|
+
active[playerId] = sessionId;
|
|
736
|
+
activeByTable.set(table.tableId, active);
|
|
737
|
+
}
|
|
738
|
+
for (const table of tables) {
|
|
739
|
+
const state = this.mahjongTables.get(table.tableId) ?? { waiting: [], activeSessions: {} };
|
|
740
|
+
state.activeSessions = activeByTable.get(table.tableId) ?? {};
|
|
741
|
+
state.waiting = state.waiting.filter((seat) => !state.activeSessions[seat.playerId]);
|
|
742
|
+
if (state.waiting.length > 0 || Object.keys(state.activeSessions).length > 0 || this.mahjongTables.has(table.tableId)) {
|
|
743
|
+
this.mahjongTables.set(table.tableId, state);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
732
747
|
async resolvePlayer(sender) {
|
|
733
748
|
return (await this.client.resolveOrRegisterIdentity(this.identity(sender)));
|
|
734
749
|
}
|
|
@@ -744,14 +759,20 @@ class PrismKoishiService {
|
|
|
744
759
|
displayName: sender.name || `${this.config.provider.toUpperCase()} ${sender.id}`,
|
|
745
760
|
};
|
|
746
761
|
}
|
|
747
|
-
targetSender(actor, targetSubject) {
|
|
748
|
-
const subject =
|
|
762
|
+
async targetSender(actor, targetSubject, bot) {
|
|
763
|
+
const subject = normalizeTargetSubject(targetSubject);
|
|
749
764
|
if (!subject)
|
|
750
765
|
return actor;
|
|
751
766
|
const denied = this.targetStaffDenied(actor);
|
|
752
767
|
if (denied)
|
|
753
768
|
return denied;
|
|
754
|
-
|
|
769
|
+
try {
|
|
770
|
+
const user = await bot?.getUser?.(subject);
|
|
771
|
+
return { id: subject, name: user?.name || subject };
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
return { id: subject, name: subject };
|
|
775
|
+
}
|
|
755
776
|
}
|
|
756
777
|
loginSessionBody() {
|
|
757
778
|
const pricingConfigIds = (this.config.loginPricingConfigIds ?? [])
|
|
@@ -966,17 +987,21 @@ function groupSessionsByPlayer(sessions) {
|
|
|
966
987
|
}
|
|
967
988
|
return players;
|
|
968
989
|
}
|
|
990
|
+
function sessionStartedAt(session) {
|
|
991
|
+
const value = Date.parse(session.startedAt ?? "");
|
|
992
|
+
return Number.isFinite(value) ? value : 0;
|
|
993
|
+
}
|
|
969
994
|
function formatPlayerGroups(groups, tableSize, mahjongLabelPrefix) {
|
|
970
|
-
const
|
|
971
|
-
const total =
|
|
995
|
+
const populatedGroups = groups.groups.filter((group) => group.players.length > 0);
|
|
996
|
+
const total = new Set(populatedGroups.flatMap((group) => group.players.map((player) => player.playerId))).size;
|
|
972
997
|
if (total === 0)
|
|
973
998
|
return "🫥 窝里目前没有玩家呢";
|
|
974
999
|
const lines = [`[总计 ${total} 人]`];
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
lines.push(`${
|
|
1000
|
+
for (const group of populatedGroups) {
|
|
1001
|
+
const heading = group.table
|
|
1002
|
+
? `${formatMahjongTableLabel(group.table, mahjongLabelPrefix)} ( ${group.players.length}/${tableSize} )`
|
|
1003
|
+
: `${group.label} ( ${group.players.length}人 )`;
|
|
1004
|
+
lines.push(`${heading}:\n${formatPlayerNames(group.players)}`);
|
|
980
1005
|
}
|
|
981
1006
|
return lines.join("\n");
|
|
982
1007
|
}
|
|
@@ -1012,6 +1037,16 @@ function parsePositiveInt(value, command, label, fallback) {
|
|
|
1012
1037
|
function cleanText(value) {
|
|
1013
1038
|
return value == null ? "" : String(value).trim();
|
|
1014
1039
|
}
|
|
1040
|
+
function normalizeTargetSubject(value) {
|
|
1041
|
+
const subject = cleanText(value);
|
|
1042
|
+
const separator = subject.indexOf(":");
|
|
1043
|
+
return separator > 0 ? subject.slice(separator + 1) : subject;
|
|
1044
|
+
}
|
|
1045
|
+
function formatPlayerReference(sender) {
|
|
1046
|
+
return sender.name && sender.name !== sender.id
|
|
1047
|
+
? `${sender.name}(QQ:${sender.id})`
|
|
1048
|
+
: sender.id;
|
|
1049
|
+
}
|
|
1015
1050
|
function toNumber(value) {
|
|
1016
1051
|
if (value == null)
|
|
1017
1052
|
return 0;
|