koishi-plugin-prism 0.1.23 → 0.1.28
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 +5 -3
- package/lib/index.js +113 -13
- 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`/`入场`
|
|
11
|
+
* 🀄 **麻将桌位集成**:包含 `/mahjong <tableId>`、`/上桌 <桌号>`、自动识别当前桌位的 `/下桌`,以及用于查看桌名、别名和状态的 `/麻将列表`。`上桌` 仅允许已通过 `login`/`入场` 开启默认入场会话的玩家使用。支持在桌位未满但已开局时进行中途补位(直接开始计费上桌),且玩家下桌时会自动显示剩余游玩人数。`list` 按 session 标签分组:有非音乐游戏 session 的玩家归入最新的非音乐标签,纯音乐玩家归入音乐标签;麻将桌额外显示当前人数和容量。已开局桌位会从后端活跃 session 自动恢复,未满桌候座仍由机器人进程暂存,重启后不会保留。
|
|
12
12
|
* 🔌 **硬件设备状态与电源管理**:可直接在聊天中查看设备状态(`/show`)、远程开启/关闭电源(`/on`、`/off`)、远程投币(`/coin`)和模拟刷卡(`/scan`)。
|
|
13
13
|
* 🎟️ **礼物兑换码**:使用 `/redeem <code>` 兑换系统发放的福利礼包。
|
|
14
14
|
* 🛠️ **管理员快捷指令**:允许管理员为指定平台用户增加或扣除余额,并覆盖结账金额后立即结账。
|
|
@@ -56,6 +56,8 @@ pricingConfigIds: [pricing-mahjong-a]
|
|
|
56
56
|
* `login` / `入场` - 开启当前玩家的计费场次。
|
|
57
57
|
* `logout` - 结算当前玩家的计费场次。
|
|
58
58
|
玩家在未产生任何费用时退场,机器人会简洁显示“本次未产生费用”和当前余额;存在收费或优惠明细时仍显示完整结算账单。
|
|
59
|
+
结账成功回执中的余额为后端已经完成扣款后的余额;只有 `/billing` 预览会显示当前余额与预计结账后余额。
|
|
60
|
+
方案内区间封顶直接计入对应计时费用;全局封顶按日期和时段逐条列出,不合并、不截断,并直接形成计费总价,不作为优惠。若旧后端未返回结构化封顶窗口,插件会用封顶历史恢复原价与计入金额,不会显示内部 adjustment 差额。只作用于整次结账的资产优惠直接列在计费总价下方,不显示额外标题或 emoji,也不会误归属到最后一个 session。
|
|
59
61
|
结账成功后,机器人会向 `staffUserIds` 与 `logoutNotifyUserIds` 中的用户私聊同一份账单,账单会明确显示结账玩家身份。
|
|
60
62
|
同一玩家在前一次退场结账尚未完成时重复发送 `/logout` 或 `/退场`,机器人会复用同一次结账请求与账单,避免重复扣款。
|
|
61
63
|
账单和管理员代操作回执均使用“平台昵称(QQ:号码)”称呼玩家;平台暂时无法提供昵称时显示“未知昵称(QQ:号码)”,不会显示内部玩家 ID。
|
|
@@ -72,8 +74,8 @@ pricingConfigIds: [pricing-mahjong-a]
|
|
|
72
74
|
* `coin <deviceId> [count]` - 请求向指定设备投币指定枚数。
|
|
73
75
|
* `scan <deviceId> <subject>` - 请求向设备发送模拟刷卡。
|
|
74
76
|
* `redeem <code>` - 兑换礼物码。
|
|
75
|
-
* `mahjong <tableId>` / `上桌 [tableId]` - 加入指定麻将桌;`/上桌` 未提供桌号时会引导查看 `/麻将列表`。仅允许已通过 `login`/`入场`
|
|
76
|
-
* `下桌` -
|
|
77
|
+
* `mahjong <tableId>` / `上桌 [tableId]` - 加入指定麻将桌;`/上桌` 未提供桌号时会引导查看 `/麻将列表`。仅允许已通过 `login`/`入场` 开启默认入场会话的玩家使用。支持在桌位未满但已开局时进行中途补位(即直接开始计费上桌)。
|
|
78
|
+
* `下桌` - 自动离开当前所在麻将桌,下桌后会停止计费,并显示该麻将桌的剩余游玩人数。
|
|
77
79
|
* `麻将列表` - 查看已配置机器的桌名、命令别名,以及空闲、等位或游玩中状态。
|
|
78
80
|
* `api测速 [次数]` - 连续查询自己的钱包,显示 Bot 到 PRiSM API 的最小、平均与最大延迟(默认 3 次,最多 10 次)。
|
|
79
81
|
|
package/lib/index.js
CHANGED
|
@@ -415,8 +415,23 @@ class PrismKoishiService {
|
|
|
415
415
|
return `你已经在 ${existing},无需重复上桌。`;
|
|
416
416
|
const state = this.mahjongTables.get(tableKey) ?? { waiting: [], activeSessions: {} };
|
|
417
417
|
this.mahjongTables.set(tableKey, state);
|
|
418
|
-
|
|
419
|
-
|
|
418
|
+
const activeCount = Object.keys(state.activeSessions).length;
|
|
419
|
+
const tableSize = this.config.mahjongTableSize ?? 4;
|
|
420
|
+
if (activeCount >= tableSize) {
|
|
421
|
+
return `「${tableSubject}」正在游玩中(${activeCount}/${tableSize}),请等待本局结束后再上桌。`;
|
|
422
|
+
}
|
|
423
|
+
if (activeCount > 0) {
|
|
424
|
+
const label = tableConfig.displayName || `${this.config.mahjongLabelPrefix ?? "麻将桌"} ${tableKey}`;
|
|
425
|
+
const result = (await this.client.startSessionByIdentity(this.identity(sender), {
|
|
426
|
+
pricingConfigIds: tableConfig.pricingConfigIds,
|
|
427
|
+
label,
|
|
428
|
+
}));
|
|
429
|
+
const session = (result?.session ?? {});
|
|
430
|
+
const sessionId = String(session.id ?? "");
|
|
431
|
+
if (sessionId) {
|
|
432
|
+
state.activeSessions[playerId] = sessionId;
|
|
433
|
+
}
|
|
434
|
+
return `已加入 ${tableSubject},补位成功,麻将计费已开始。当前 ${activeCount + 1}/${tableSize} 人。`;
|
|
420
435
|
}
|
|
421
436
|
state.waiting.push({
|
|
422
437
|
playerId,
|
|
@@ -424,7 +439,6 @@ class PrismKoishiService {
|
|
|
424
439
|
displayName: sender.name || playerId,
|
|
425
440
|
identity: this.identity(sender),
|
|
426
441
|
});
|
|
427
|
-
const tableSize = this.config.mahjongTableSize ?? 4;
|
|
428
442
|
if (state.waiting.length < tableSize) {
|
|
429
443
|
return `已加入 ${tableSubject},当前 ${state.waiting.length}/${tableSize} 人。`;
|
|
430
444
|
}
|
|
@@ -465,8 +479,10 @@ class PrismKoishiService {
|
|
|
465
479
|
if (!sessionId)
|
|
466
480
|
return "你当前未在任何麻将桌上。";
|
|
467
481
|
delete state.activeSessions[playerId];
|
|
482
|
+
const remainingCount = Object.keys(state.activeSessions).length;
|
|
483
|
+
const tableSize = this.config.mahjongTableSize ?? 4;
|
|
468
484
|
await this.client.stopSessionByIdentity(this.identity(sender), sessionId);
|
|
469
|
-
return `已离开 ${tableSubject}
|
|
485
|
+
return `已离开 ${tableSubject},麻将计费已停止。当前还剩 ${remainingCount}/${tableSize} 人。`;
|
|
470
486
|
}
|
|
471
487
|
async listMahjongTables() {
|
|
472
488
|
const activeResult = (await this.client.listActiveSessions());
|
|
@@ -510,22 +526,33 @@ class PrismKoishiService {
|
|
|
510
526
|
const result = (await this.client.confirmCheckoutByIdentity(this.identity(sender), false));
|
|
511
527
|
const settlement = result?.playerSettlement ?? result?.settlement ?? {};
|
|
512
528
|
const records = result?.settlements ?? [];
|
|
529
|
+
const checkoutAdjustments = (result?.checkoutAdjustments ?? []);
|
|
530
|
+
const pricingCapAdjustments = (result?.pricingCapAdjustments ?? []);
|
|
531
|
+
const checkoutAdjustmentKeys = new Set(checkoutAdjustments.map(adjustmentKey));
|
|
532
|
+
const pricingCapAdjustmentKeys = new Set(pricingCapAdjustments.map(adjustmentKey));
|
|
513
533
|
const sessionPreviews = records.map((rec) => {
|
|
514
534
|
const s = rec?.settlement ?? {};
|
|
535
|
+
const sessionAdjustments = (rec?.adjustments ?? []).filter((adjustment) => {
|
|
536
|
+
const key = adjustmentKey(adjustment);
|
|
537
|
+
return !checkoutAdjustmentKeys.has(key) &&
|
|
538
|
+
!pricingCapAdjustmentKeys.has(key) &&
|
|
539
|
+
!isPricingCapAdjustment(adjustment);
|
|
540
|
+
});
|
|
541
|
+
const sessionSubtotal = toNumber(s.subtotal ?? 0);
|
|
515
542
|
return {
|
|
516
543
|
sessionId: s.sessionId,
|
|
517
544
|
label: s.label,
|
|
518
545
|
startedAt: s.startedAt,
|
|
519
546
|
endedAt: s.endedAt ?? s.settledAt,
|
|
520
547
|
status: "closed",
|
|
521
|
-
subtotal:
|
|
522
|
-
total:
|
|
548
|
+
subtotal: sessionSubtotal,
|
|
549
|
+
total: Math.max(0, sessionSubtotal + sessionAdjustments.reduce((sum, adjustment) => sum + toNumber(adjustment?.amount ?? 0), 0)),
|
|
523
550
|
chargeItems: rec?.chargeItems ?? [],
|
|
524
|
-
adjustments:
|
|
551
|
+
adjustments: sessionAdjustments,
|
|
525
552
|
};
|
|
526
553
|
});
|
|
527
554
|
const synthetic = {
|
|
528
|
-
|
|
555
|
+
settlement: {
|
|
529
556
|
playerId: settlement.playerId,
|
|
530
557
|
subtotal: settlement.subtotal ?? 0,
|
|
531
558
|
total: settlement.total ?? 0,
|
|
@@ -533,6 +560,9 @@ class PrismKoishiService {
|
|
|
533
560
|
sessionPreviews,
|
|
534
561
|
chargeItems: result?.chargeItems ?? [],
|
|
535
562
|
adjustments: result?.adjustments ?? [],
|
|
563
|
+
checkoutAdjustments,
|
|
564
|
+
pricingCapAdjustments,
|
|
565
|
+
globalCapWindows: result?.globalCapWindows ?? [],
|
|
536
566
|
assetHoldings: result?.assetHoldings ?? [],
|
|
537
567
|
};
|
|
538
568
|
const receipt = await this.formatCheckoutPreview(synthetic, sender, "✅ 退场成功 · 结算账单");
|
|
@@ -892,7 +922,10 @@ class PrismKoishiService {
|
|
|
892
922
|
},
|
|
893
923
|
];
|
|
894
924
|
}
|
|
895
|
-
const adjustments = result?.adjustments ?? [];
|
|
925
|
+
const adjustments = (result?.adjustments ?? []);
|
|
926
|
+
const pricingCapAdjustments = (result?.pricingCapAdjustments ?? adjustments.filter(isPricingCapAdjustment));
|
|
927
|
+
const sessionAdjustmentKeys = new Set(sessionPreviews.flatMap((session) => (session?.adjustments ?? []).map(adjustmentKey)));
|
|
928
|
+
const checkoutAdjustments = (result?.checkoutAdjustments ?? adjustments.filter((adjustment) => !isPricingCapAdjustment(adjustment) && !sessionAdjustmentKeys.has(adjustmentKey(adjustment))));
|
|
896
929
|
const assetHoldings = result?.assetHoldings ?? [];
|
|
897
930
|
const lines = [];
|
|
898
931
|
lines.push(title);
|
|
@@ -907,7 +940,7 @@ class PrismKoishiService {
|
|
|
907
940
|
}
|
|
908
941
|
}
|
|
909
942
|
const hasNonZeroSessionTotal = sessionPreviews.some((session) => toNumber(session?.total ?? 0) !== 0);
|
|
910
|
-
const hasNonZeroAdjustment = hasAdjustmentEntries(
|
|
943
|
+
const hasNonZeroAdjustment = hasAdjustmentEntries(checkoutAdjustments, sessionPreviews);
|
|
911
944
|
if (!hasNonZeroSessionTotal && !hasNonZeroAdjustment) {
|
|
912
945
|
lines.push("");
|
|
913
946
|
lines.push("本次未产生费用");
|
|
@@ -947,10 +980,58 @@ class PrismKoishiService {
|
|
|
947
980
|
lines.push(` └ ${adjLabel}:${formatNumber(amount)}${currency}`);
|
|
948
981
|
}
|
|
949
982
|
}
|
|
983
|
+
const cappedWindows = (result?.globalCapWindows ?? []).filter((window) => toNumber(window?.currentAmount) !== toNumber(window?.amountApplied));
|
|
984
|
+
const appliedCapAdjustments = pricingCapAdjustments.filter((adjustment) => toNumber(adjustment?.amount ?? 0) !== 0);
|
|
985
|
+
if (cappedWindows.length > 0 || appliedCapAdjustments.length > 0) {
|
|
986
|
+
lines.push("");
|
|
987
|
+
lines.push("封顶:");
|
|
988
|
+
if (cappedWindows.length > 0) {
|
|
989
|
+
for (const window of cappedWindows) {
|
|
990
|
+
const label = firstDefined(window, "ruleLabel", "label", "name") ?? "封顶时段";
|
|
991
|
+
const startedAt = parseDateTime(window?.windowStartedAt);
|
|
992
|
+
const datedLabel = startedAt ? `${formatMD(startedAt)} ${label}` : label;
|
|
993
|
+
const currentAmount = toNumber(window?.currentAmount);
|
|
994
|
+
const amountApplied = toNumber(window?.amountApplied);
|
|
995
|
+
const priceCap = toNumber(window?.priceCap);
|
|
996
|
+
const paidBefore = toNumber(window?.paidBefore);
|
|
997
|
+
const details = [`上限${formatNumber(priceCap)}`];
|
|
998
|
+
if (paidBefore > 0)
|
|
999
|
+
details.push(`已计${formatNumber(paidBefore)}`);
|
|
1000
|
+
lines.push(`- ${datedLabel}:${formatNumber(currentAmount)} → ${formatNumber(amountApplied)}${currency}(${details.join(",")})`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
else {
|
|
1004
|
+
for (const adjustment of appliedCapAdjustments) {
|
|
1005
|
+
const label = firstDefined(adjustment, "label", "name", "source") ?? "封顶时段";
|
|
1006
|
+
const history = adjustment?.pricingCapHistory ?? {};
|
|
1007
|
+
const amountApplied = toNumber(history?.amount);
|
|
1008
|
+
const currentAmount = amountApplied - toNumber(adjustment?.amount ?? 0);
|
|
1009
|
+
const anchorAt = parseDateTime(history?.capAnchorAt);
|
|
1010
|
+
const datedLabel = anchorAt ? `${formatMD(anchorAt)} ${label}` : label;
|
|
1011
|
+
lines.push(amountApplied > 0
|
|
1012
|
+
? `- ${datedLabel}:${formatNumber(currentAmount)} → ${formatNumber(amountApplied)}${currency}`
|
|
1013
|
+
: `- ${datedLabel}:${formatNumber(adjustment?.amount ?? 0)}${currency}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const visibleCheckoutAdjustments = checkoutAdjustments.filter((adjustment) => toNumber(firstDefined(adjustment ?? {}, "amount", "saved", 0)) !== 0);
|
|
950
1018
|
lines.push("");
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1019
|
+
const cappedTotal = Math.max(0, toNumber(subtotal) + pricingCapAdjustments.reduce((sum, adjustment) => sum + toNumber(adjustment?.amount ?? 0), 0));
|
|
1020
|
+
lines.push(`计费总价:${formatNumber(cappedTotal)}${currency}`);
|
|
1021
|
+
const hasManualAdjustment = visibleCheckoutAdjustments.some((adjustment) => cleanText(adjustment?.source).startsWith("staff.override:"));
|
|
1022
|
+
if (visibleCheckoutAdjustments.length > 0) {
|
|
1023
|
+
lines.push("");
|
|
1024
|
+
for (const adjustment of visibleCheckoutAdjustments) {
|
|
1025
|
+
const amount = toNumber(firstDefined(adjustment ?? {}, "amount", "saved", 0));
|
|
1026
|
+
const label = firstDefined(adjustment ?? {}, "label", "name", "source") ?? "优惠";
|
|
1027
|
+
lines.push(`${label}:${formatNumber(amount)}${currency}`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (hasNonZeroAdjustment) {
|
|
1031
|
+
if (visibleCheckoutAdjustments.length > 0)
|
|
1032
|
+
lines.push("");
|
|
1033
|
+
lines.push(`${hasManualAdjustment ? "调整后价格" : "优惠后价格"}:${formatNumber(total)}${currency}`);
|
|
1034
|
+
}
|
|
954
1035
|
if (hasBalance) {
|
|
955
1036
|
const isPreview = result?.settlementPreview != null && result?.settlement == null;
|
|
956
1037
|
if (isPreview) {
|
|
@@ -1137,6 +1218,21 @@ function formatPlayerReference(sender, provider = "qq") {
|
|
|
1137
1218
|
const name = sender.name && sender.name !== sender.id ? sender.name : "未知昵称";
|
|
1138
1219
|
return `${name}(${provider.toUpperCase()}:${sender.id})`;
|
|
1139
1220
|
}
|
|
1221
|
+
function adjustmentKey(adjustment) {
|
|
1222
|
+
const id = cleanText(adjustment?.id);
|
|
1223
|
+
if (id)
|
|
1224
|
+
return `id:${id}`;
|
|
1225
|
+
return JSON.stringify([
|
|
1226
|
+
adjustment?.source ?? "",
|
|
1227
|
+
adjustment?.label ?? "",
|
|
1228
|
+
toNumber(adjustment?.amount ?? 0),
|
|
1229
|
+
]);
|
|
1230
|
+
}
|
|
1231
|
+
function isPricingCapAdjustment(adjustment) {
|
|
1232
|
+
return adjustment?.pricingCapHistory != null ||
|
|
1233
|
+
cleanText(adjustment?.source).startsWith("time.cap:") ||
|
|
1234
|
+
cleanText(adjustment?.id).startsWith("time-cap:");
|
|
1235
|
+
}
|
|
1140
1236
|
function toNumber(value) {
|
|
1141
1237
|
if (value == null)
|
|
1142
1238
|
return 0;
|
|
@@ -1197,6 +1293,10 @@ function formatHM(dt) {
|
|
|
1197
1293
|
const pad = (n) => String(n).padStart(2, "0");
|
|
1198
1294
|
return `${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
|
|
1199
1295
|
}
|
|
1296
|
+
function formatMD(dt) {
|
|
1297
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1298
|
+
return `${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}`;
|
|
1299
|
+
}
|
|
1200
1300
|
function formatDateTime(value) {
|
|
1201
1301
|
const dt = parseDateTime(value);
|
|
1202
1302
|
if (!dt)
|