koishi-plugin-prism 0.1.5 → 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.
- package/README.md +3 -3
- package/lib/index.js +147 -46
- 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>` 以及便捷的 `/上桌` / `/下桌`
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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,73 @@ 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
|
+
}
|
|
764
|
+
async resolvePlayerDisplay(sender, playerId) {
|
|
765
|
+
if (!sender)
|
|
766
|
+
return playerId || "未知玩家";
|
|
767
|
+
const platformName = await this.resolvePlatformName(sender.id);
|
|
768
|
+
const name = platformName
|
|
769
|
+
|| (sender.name && sender.name !== sender.id ? sender.name : playerId)
|
|
770
|
+
|| sender.id;
|
|
771
|
+
return `玩家:${name}(${this.config.provider.toUpperCase()}:${sender.id})`;
|
|
772
|
+
}
|
|
715
773
|
mahjongTableForPlayer(playerId) {
|
|
716
774
|
for (const [tableId, state] of this.mahjongTables) {
|
|
717
775
|
if (state.activeSessions[playerId])
|
|
@@ -727,6 +785,10 @@ class PrismKoishiService {
|
|
|
727
785
|
async resolvePlayer(sender) {
|
|
728
786
|
return (await this.client.resolveOrRegisterIdentity(this.identity(sender)));
|
|
729
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
|
+
}
|
|
730
792
|
identity(sender) {
|
|
731
793
|
return {
|
|
732
794
|
provider: this.config.provider,
|
|
@@ -755,7 +817,7 @@ class PrismKoishiService {
|
|
|
755
817
|
return "权限不足";
|
|
756
818
|
return null;
|
|
757
819
|
}
|
|
758
|
-
formatCheckoutPreview(result, sender, title = "【结算账单】") {
|
|
820
|
+
async formatCheckoutPreview(result, sender, title = "【结算账单】") {
|
|
759
821
|
if (result?.billing && result?.session) {
|
|
760
822
|
return formatLegacyBilling(result, this.config.currencyName);
|
|
761
823
|
}
|
|
@@ -785,57 +847,57 @@ class PrismKoishiService {
|
|
|
785
847
|
const adjustments = result?.adjustments ?? [];
|
|
786
848
|
const assetHoldings = result?.assetHoldings ?? [];
|
|
787
849
|
const lines = [];
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
const identitySuffix = sender ? `(${this.config.provider.toUpperCase()}:${sender.id})` : "";
|
|
791
|
-
headerParts.push(`玩家ID:${playerId}${identitySuffix}`);
|
|
792
|
-
}
|
|
793
|
-
else if (sender) {
|
|
794
|
-
headerParts.push(`玩家:${sender.name || sender.id}(${this.config.provider.toUpperCase()}:${sender.id})`);
|
|
795
|
-
}
|
|
796
|
-
lines.push(headerParts.join("\n"));
|
|
850
|
+
lines.push(title);
|
|
851
|
+
lines.push(await this.resolvePlayerDisplay(sender, playerId));
|
|
797
852
|
const validStarts = sessionPreviews.map((s) => parseDateTime(s?.startedAt)).filter(Boolean);
|
|
798
853
|
const validEnds = sessionPreviews.map((s) => sessionDisplayEnd(s, previewedAt)).filter(Boolean);
|
|
799
854
|
if (validStarts.length > 0) {
|
|
800
855
|
const overallStart = minDate(validStarts);
|
|
801
856
|
const overallEnd = validEnds.length > 0 ? maxDate(validEnds) : now(this.config);
|
|
802
|
-
lines.push(
|
|
857
|
+
lines.push(`⏰ 游玩时间:${formatHM(overallStart)}–${formatHM(overallEnd)}`);
|
|
803
858
|
}
|
|
804
|
-
lines.push("");
|
|
805
859
|
for (const sPrev of sessionPreviews) {
|
|
806
860
|
const label = sPrev?.label || "计时区间";
|
|
807
861
|
const startDt = parseDateTime(sPrev?.startedAt);
|
|
808
862
|
const endDt = sessionDisplayEnd(sPrev, previewedAt);
|
|
809
863
|
const status = sPrev?.status ?? "active";
|
|
810
864
|
const sTotal = toNumber(sPrev?.total ?? 0);
|
|
865
|
+
lines.push("");
|
|
811
866
|
lines.push(label);
|
|
812
867
|
if (startDt && endDt) {
|
|
868
|
+
const minutes = Math.floor((endDt.getTime() - startDt.getTime()) / 60_000);
|
|
813
869
|
lines.push(`游玩时段:${formatHM(startDt)}-${formatHM(endDt)}`);
|
|
814
|
-
lines.push(`游玩时长:${formatDurationValue(
|
|
870
|
+
lines.push(`游玩时长:${formatDurationValue(minutes)}|消费:${formatNumber(sTotal)}${currency}`);
|
|
815
871
|
}
|
|
816
872
|
else if (startDt) {
|
|
817
873
|
lines.push(`入场:${formatHM(startDt)} (${status === "active" ? "计费中" : "已关闭"})`);
|
|
818
874
|
}
|
|
819
|
-
|
|
875
|
+
const sessionAdjustments = (sPrev?.adjustments ?? []);
|
|
876
|
+
for (const adj of sessionAdjustments) {
|
|
877
|
+
const amount = toNumber(firstDefined(adj ?? {}, "amount", "saved", 0));
|
|
878
|
+
if (amount === 0)
|
|
879
|
+
continue;
|
|
880
|
+
const adjLabel = firstDefined(adj ?? {}, "label", "name", "source") ?? "优惠";
|
|
881
|
+
lines.push(` └ ${adjLabel}:${formatNumber(amount)}${currency}`);
|
|
882
|
+
}
|
|
820
883
|
}
|
|
821
|
-
|
|
822
|
-
|
|
884
|
+
let balance = 0;
|
|
885
|
+
let hasBalance = false;
|
|
823
886
|
for (const holding of assetHoldings) {
|
|
824
|
-
const qty = toNumber(holding?.quantity ?? 0);
|
|
825
887
|
const code = String(holding?.assetCode ?? "").toLowerCase();
|
|
826
888
|
if (code.includes("paid") || code.includes("free") || code.includes("currency")) {
|
|
827
|
-
|
|
889
|
+
balance += toNumber(holding?.quantity ?? 0);
|
|
890
|
+
hasBalance = true;
|
|
828
891
|
}
|
|
829
892
|
}
|
|
830
|
-
|
|
831
|
-
lines.push(`扣款后余额:${balanceParts.join("+")}`);
|
|
893
|
+
lines.push("");
|
|
832
894
|
lines.push(`计费总价:${formatNumber(subtotal)}${currency}`);
|
|
833
|
-
const hasDiscount = adjustments.some((adj) => {
|
|
834
|
-
|
|
835
|
-
return amount !== 0;
|
|
836
|
-
});
|
|
895
|
+
const hasDiscount = adjustments.some((adj) => toNumber(firstDefined(adj ?? {}, "amount", "saved", 0)) !== 0)
|
|
896
|
+
|| sessionPreviews.some((sp) => (sp?.adjustments ?? []).some((adj) => toNumber(firstDefined(adj ?? {}, "amount", "saved", 0)) !== 0));
|
|
837
897
|
if (hasDiscount)
|
|
838
898
|
lines.push(`优惠后价格:${formatNumber(total)}${currency}`);
|
|
899
|
+
if (hasBalance)
|
|
900
|
+
lines.push(`扣款后余额:${formatNumber(balance)}${currency}`);
|
|
839
901
|
return lines.join("\n");
|
|
840
902
|
}
|
|
841
903
|
handleCommandError(error) {
|
|
@@ -915,6 +977,45 @@ function parseMahjongTables(value, labelPrefix) {
|
|
|
915
977
|
}
|
|
916
978
|
return tables;
|
|
917
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
|
+
}
|
|
918
1019
|
function splitOnce(text, sep) {
|
|
919
1020
|
const idx = text.indexOf(sep);
|
|
920
1021
|
if (idx === -1)
|