koishi-plugin-prism 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/lib/index.js +114 -22
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  * 🎮 **玩家入场与结算**:通过 `/login` 和 `/logout` 指令开启或结算计费场次,支持 `/入场` 别名。
10
10
  * 💳 **账户钱包与资产管理**:支持查询钱包余额(`/wallet`)和持有的道具资产(`/items`)。
11
- * 🀄 **麻将桌位集成**:包含 `/mahjong <tableId>` 以及便捷的 `/上桌` / `/下桌` 控制。
11
+ * 🀄 **麻将桌位集成**:包含 `/mahjong <tableId>` 以及便捷的 `/上桌` / `/下桌` 控制。`上桌` 仅允许已通过 `login`/`入场` 开启默认入场会话的玩家使用。`list` 会按音乐游戏和麻将桌分组,并对同一玩家的多个计时会话去重;麻将桌显示当前人数和容量。未满桌候座由机器人进程暂存,机器人重启后不会保留。
12
12
  * 🔌 **硬件设备状态与电源管理**:可直接在聊天中查看设备状态(`/show`)、远程开启/关闭电源(`/on`、`/off`)、远程投币(`/coin`)和模拟刷卡(`/scan`)。
13
13
  * 🎟️ **礼物兑换码**:使用 `/redeem <code>` 兑换系统发放的福利礼包。
14
14
  * 🛠️ **管理员高级指令**:允许管理员在聊天中查看玩家(`/admin.players`)、创建新玩家账户(`/admin.create-player`)、充值余额(`/admin.grant-balance`)、手动结账(`/admin.checkout`)以及制作礼包兑换码(`/admin.redeem-code`)。
@@ -41,7 +41,7 @@
41
41
  * `billing` - 预览当前玩家本场计费的消费费用。
42
42
  * `wallet` - 查看当前玩家的钱包余额。
43
43
  * `items` - 查看当前玩家持有的道具或资产。
44
- * `list` - 查看当前在线/在店游玩玩家的列表。
44
+ * `list` - 查看当前在线/在店游玩玩家的列表,按音乐游戏和麻将桌分组并对同一玩家的多个计时会话去重;麻将桌显示当前人数和容量。未满桌候座由机器人进程暂存,机器人重启后不会保留。
45
45
  * `show [deviceId]` - 查看设备电源与连接状态。
46
46
  * `history` - 查看自己的历史游玩记录。
47
47
  * `lock` - 发送开门指令。
@@ -50,7 +50,7 @@
50
50
  * `coin <deviceId> [count]` - 请求向指定设备投币指定枚数。
51
51
  * `scan <deviceId> <subject>` - 请求向设备发送模拟刷卡。
52
52
  * `redeem <code>` - 兑换礼物码。
53
- * `mahjong <tableId>` / `上桌 <tableId>` - 加入指定麻将桌。
53
+ * `mahjong <tableId>` / `上桌 <tableId>` - 加入指定麻将桌;仅允许已通过 `login`/`入场` 开启默认入场会话的玩家使用。
54
54
  * `下桌 <tableId>` - 离开指定麻将桌。
55
55
 
56
56
  ### 管理员指令 (需在白名单内并开启)
package/lib/index.js CHANGED
@@ -389,6 +389,11 @@ class PrismKoishiService {
389
389
  const tableSubject = tableConfig.displayName || `${tableKey} 桌`;
390
390
  const player = await this.resolvePlayer(sender);
391
391
  const playerId = String(player.id ?? "");
392
+ const activeResult = (await this.client.listActiveSessions());
393
+ const activeSessions = (activeResult.sessions ?? []);
394
+ if (!this.hasEntrySession(playerId, activeSessions)) {
395
+ return "请先入场后再上桌。";
396
+ }
392
397
  const existing = this.mahjongTableForPlayer(playerId);
393
398
  if (existing)
394
399
  return `你已经在 ${existing} 桌了。`;
@@ -552,28 +557,14 @@ class PrismKoishiService {
552
557
  async listActiveSessions(sender) {
553
558
  const result = (await this.client.listActiveSessions());
554
559
  const sessions = (result?.sessions ?? []);
555
- if (sessions.length === 0)
556
- return "🫥 窝里目前没有玩家呢";
557
- const lines = [`👥 窝里目前共有 ${sessions.length} 人`];
558
- for (const session of sessions) {
559
- const identitySubject = findSubjectForSession(session, this.config.provider);
560
- let display;
561
- if (identitySubject) {
562
- const platformName = await this.resolvePlatformName(identitySubject);
563
- if (platformName) {
564
- display = `${platformName} ( ${identitySubject} )`;
565
- }
566
- else {
567
- display = `${session.playerDisplayName || identitySubject} ( ${identitySubject} )`;
568
- }
569
- }
570
- else {
571
- display = session.playerDisplayName || session.playerId || "未知玩家";
572
- }
573
- const timeStr = formatDateTime(session.startedAt);
574
- lines.push(`玩家: ${display}\n入场时间: ${timeStr}`);
575
- }
576
- return lines.join("\n\n");
560
+ const tableByLabel = new Map(uniqueMahjongConfigs(this.mahjongTableConfigs()).map((table) => [
561
+ mahjongSessionLabel(table, this.config.mahjongLabelPrefix ?? "麻将桌"),
562
+ table,
563
+ ]));
564
+ const players = groupSessionsByPlayer(sessions);
565
+ const groups = await this.buildPlayerGroups(players, tableByLabel);
566
+ this.mergeWaitingSeats(groups);
567
+ return formatPlayerGroups(groups, this.config.mahjongTableSize ?? 4, this.config.mahjongLabelPrefix ?? "麻将桌");
577
568
  }
578
569
  async listDeviceStates(rawAlias) {
579
570
  const alias = cleanText(rawAlias);
@@ -712,6 +703,64 @@ class PrismKoishiService {
712
703
  return null;
713
704
  }
714
705
  }
706
+ async displayNameForPlayer(player) {
707
+ let identitySubject;
708
+ for (const session of player.sessions) {
709
+ const subject = findSubjectForSession(session, this.config.provider);
710
+ if (!subject)
711
+ continue;
712
+ identitySubject ??= subject;
713
+ const platformName = await this.resolvePlatformName(subject);
714
+ if (platformName)
715
+ return platformName;
716
+ }
717
+ return player.sessions.find((session) => session.playerDisplayName)?.playerDisplayName
718
+ || identitySubject
719
+ || player.displayName
720
+ || player.playerId
721
+ || "未知玩家";
722
+ }
723
+ async buildPlayerGroups(players, tableByLabel) {
724
+ const groups = {
725
+ music: [],
726
+ mahjong: uniqueMahjongConfigs(this.mahjongTableConfigs()).map((table) => ({ table, players: [] })),
727
+ };
728
+ const groupForTable = new Map(groups.mahjong.map((group) => [group.table.tableId, group]));
729
+ for (const player of players.values()) {
730
+ player.displayName = await this.displayNameForPlayer(player);
731
+ const table = player.sessions
732
+ .map((session) => tableByLabel.get(session.label ?? ""))
733
+ .find((value) => Boolean(value));
734
+ if (table) {
735
+ groupForTable.get(table.tableId)?.players.push(player);
736
+ }
737
+ else {
738
+ groups.music.push(player);
739
+ }
740
+ }
741
+ return groups;
742
+ }
743
+ mergeWaitingSeats(groups) {
744
+ for (const [tableId, state] of this.mahjongTables) {
745
+ const group = groups.mahjong.find((candidate) => candidate.table.tableId === tableId);
746
+ if (!group)
747
+ continue;
748
+ for (const seat of state.waiting) {
749
+ if (group.players.some((player) => player.playerId === seat.playerId))
750
+ continue;
751
+ const existing = [groups.music, ...groups.mahjong.map((candidate) => candidate.players)]
752
+ .flat()
753
+ .find((player) => player.playerId === seat.playerId);
754
+ groups.music = groups.music.filter((player) => player.playerId !== seat.playerId);
755
+ for (const candidate of groups.mahjong) {
756
+ if (candidate !== group) {
757
+ candidate.players = candidate.players.filter((player) => player.playerId !== seat.playerId);
758
+ }
759
+ }
760
+ group.players.push(existing ?? { playerId: seat.playerId, sessions: [], displayName: seat.displayName });
761
+ }
762
+ }
763
+ }
715
764
  async resolvePlayerDisplay(sender, playerId) {
716
765
  if (!sender)
717
766
  return playerId || "未知玩家";
@@ -736,6 +785,10 @@ class PrismKoishiService {
736
785
  async resolvePlayer(sender) {
737
786
  return (await this.client.resolveOrRegisterIdentity(this.identity(sender)));
738
787
  }
788
+ hasEntrySession(playerId, sessions) {
789
+ const label = this.config.loginSessionLabel?.trim();
790
+ return sessions.some((session) => session.playerId === playerId && (label ? session.label === label : true));
791
+ }
739
792
  identity(sender) {
740
793
  return {
741
794
  provider: this.config.provider,
@@ -924,6 +977,45 @@ function parseMahjongTables(value, labelPrefix) {
924
977
  }
925
978
  return tables;
926
979
  }
980
+ function uniqueMahjongConfigs(tables) {
981
+ return [...new Map([...tables.values()].map((table) => [table.tableId, table])).values()];
982
+ }
983
+ function mahjongSessionLabel(table, labelPrefix) {
984
+ return table.displayName || `${labelPrefix} ${table.tableId}`;
985
+ }
986
+ function groupSessionsByPlayer(sessions) {
987
+ const players = new Map();
988
+ for (const session of sessions) {
989
+ const playerId = session.playerId || findSubjectForSession(session, "") || "";
990
+ if (!playerId)
991
+ continue;
992
+ const player = players.get(playerId) ?? { playerId, sessions: [] };
993
+ player.sessions.push(session);
994
+ players.set(playerId, player);
995
+ }
996
+ return players;
997
+ }
998
+ function formatPlayerGroups(groups, tableSize, mahjongLabelPrefix) {
999
+ const populatedMahjongGroups = groups.mahjong.filter((group) => group.players.length > 0);
1000
+ const total = groups.music.length + populatedMahjongGroups.reduce((sum, group) => sum + group.players.length, 0);
1001
+ if (total === 0)
1002
+ return "🫥 窝里目前没有玩家呢";
1003
+ const lines = [`[总计 ${total} 人]`];
1004
+ if (groups.music.length > 0) {
1005
+ lines.push(`🎵 音乐游戏 ( ${groups.music.length}人 ):\n${formatPlayerNames(groups.music)}`);
1006
+ }
1007
+ for (const group of populatedMahjongGroups) {
1008
+ lines.push(`${formatMahjongTableLabel(group.table, mahjongLabelPrefix)} ( ${group.players.length}/${tableSize} ):\n${formatPlayerNames(group.players)}`);
1009
+ }
1010
+ return lines.join("\n");
1011
+ }
1012
+ function formatMahjongTableLabel(table, labelPrefix) {
1013
+ const label = mahjongSessionLabel(table, labelPrefix);
1014
+ return table.displayName ? label : `🀄️ ${label}`;
1015
+ }
1016
+ function formatPlayerNames(players) {
1017
+ return players.map((player) => `- ${player.displayName || player.playerId || "未知玩家"}`).join(", ");
1018
+ }
927
1019
  function splitOnce(text, sep) {
928
1020
  const idx = text.indexOf(sep);
929
1021
  if (idx === -1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-prism",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "PRiSM Next 计费与设备管理系统的 Koishi 机器人集成插件",
5
5
  "main": "./lib/index.js",
6
6
  "exports": {
@@ -48,4 +48,4 @@
48
48
  "test": "bun test test",
49
49
  "typecheck": "tsc -p tsconfig.json"
50
50
  }
51
- }
51
+ }