koishi-plugin-prism 0.1.22 → 0.1.27
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 -0
- package/lib/index.js +108 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,6 +56,8 @@ pricingConfigIds: [pricing-mahjong-a]
|
|
|
56
56
|
* `login` / `入场` - 开启当前玩家的计费场次。
|
|
57
57
|
* `logout` - 结算当前玩家的计费场次。
|
|
58
58
|
玩家在未产生任何费用时退场,机器人会简洁显示“本次未产生费用”和当前余额;存在收费或优惠明细时仍显示完整结算账单。
|
|
59
|
+
结账成功回执中的余额为后端已经完成扣款后的余额;只有 `/billing` 预览会显示当前余额与预计结账后余额。
|
|
60
|
+
方案内区间封顶直接计入对应计时费用;全局封顶按日期和时段逐条列出,不合并、不截断,并直接形成计费总价,不作为优惠。只作用于整次结账的资产优惠直接列在计费总价下方,不显示额外标题或 emoji,也不会误归属到最后一个 session。
|
|
59
61
|
结账成功后,机器人会向 `staffUserIds` 与 `logoutNotifyUserIds` 中的用户私聊同一份账单,账单会明确显示结账玩家身份。
|
|
60
62
|
同一玩家在前一次退场结账尚未完成时重复发送 `/logout` 或 `/退场`,机器人会复用同一次结账请求与账单,避免重复扣款。
|
|
61
63
|
账单和管理员代操作回执均使用“平台昵称(QQ:号码)”称呼玩家;平台暂时无法提供昵称时显示“未知昵称(QQ:号码)”,不会显示内部玩家 ID。
|
|
@@ -75,6 +77,7 @@ pricingConfigIds: [pricing-mahjong-a]
|
|
|
75
77
|
* `mahjong <tableId>` / `上桌 [tableId]` - 加入指定麻将桌;`/上桌` 未提供桌号时会引导查看 `/麻将列表`。仅允许已通过 `login`/`入场` 开启默认入场会话的玩家使用。
|
|
76
78
|
* `下桌` - 自动离开当前所在麻将桌。
|
|
77
79
|
* `麻将列表` - 查看已配置机器的桌名、命令别名,以及空闲、等位或游玩中状态。
|
|
80
|
+
* `api测速 [次数]` - 连续查询自己的钱包,显示 Bot 到 PRiSM API 的最小、平均与最大延迟(默认 3 次,最多 10 次)。
|
|
78
81
|
|
|
79
82
|
### 管理员快捷指令
|
|
80
83
|
启用 `enableStaffCommands`、配置 `staffUserIds` 白名单与 `staffSessionToken` 后可使用:
|
package/lib/index.js
CHANGED
|
@@ -52,6 +52,7 @@ const USAGE = {
|
|
|
52
52
|
mahjong_join: "/上桌 <桌号>",
|
|
53
53
|
mahjong_leave: "/下桌",
|
|
54
54
|
mahjong_list: "/麻将列表",
|
|
55
|
+
api_benchmark: "/api测速 [次数]",
|
|
55
56
|
prism_on: "/prism on <设备ID>",
|
|
56
57
|
prism_off: "/prism off <设备ID|all>",
|
|
57
58
|
prism_coin: "/prism coin <设备ID> [数量]",
|
|
@@ -82,6 +83,7 @@ function applyPrismKoishiPlugin(ctx, config) {
|
|
|
82
83
|
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)));
|
|
83
84
|
ctx.command("billing [target:user]", "预览玩家结账费用").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.billing(sender), context.session?.bot)));
|
|
84
85
|
ctx.command("wallet [target:user]", "查看玩家钱包").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.wallet(sender), context.session?.bot)));
|
|
86
|
+
ctx.command("api测速 [count:number]", "测试 Bot 到 PRiSM API 的钱包查询延迟").action(wrap(async (context, count) => service.benchmarkApi(await service.sender(context), count)));
|
|
85
87
|
ctx.command("items [target:user]", "查看玩家资产").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.items(sender), context.session?.bot)));
|
|
86
88
|
ctx.command("list", "查看当前在线玩家列表").action(wrap(async (context) => service.listActiveSessions(await service.sender(context))));
|
|
87
89
|
ctx.command("show [deviceId]", "查看设备电源状态").action(wrap(async (context, deviceId) => service.listDeviceStates(deviceId)));
|
|
@@ -508,22 +510,33 @@ class PrismKoishiService {
|
|
|
508
510
|
const result = (await this.client.confirmCheckoutByIdentity(this.identity(sender), false));
|
|
509
511
|
const settlement = result?.playerSettlement ?? result?.settlement ?? {};
|
|
510
512
|
const records = result?.settlements ?? [];
|
|
513
|
+
const checkoutAdjustments = (result?.checkoutAdjustments ?? []);
|
|
514
|
+
const pricingCapAdjustments = (result?.pricingCapAdjustments ?? []);
|
|
515
|
+
const checkoutAdjustmentKeys = new Set(checkoutAdjustments.map(adjustmentKey));
|
|
516
|
+
const pricingCapAdjustmentKeys = new Set(pricingCapAdjustments.map(adjustmentKey));
|
|
511
517
|
const sessionPreviews = records.map((rec) => {
|
|
512
518
|
const s = rec?.settlement ?? {};
|
|
519
|
+
const sessionAdjustments = (rec?.adjustments ?? []).filter((adjustment) => {
|
|
520
|
+
const key = adjustmentKey(adjustment);
|
|
521
|
+
return !checkoutAdjustmentKeys.has(key) &&
|
|
522
|
+
!pricingCapAdjustmentKeys.has(key) &&
|
|
523
|
+
!isPricingCapAdjustment(adjustment);
|
|
524
|
+
});
|
|
525
|
+
const sessionSubtotal = toNumber(s.subtotal ?? 0);
|
|
513
526
|
return {
|
|
514
527
|
sessionId: s.sessionId,
|
|
515
528
|
label: s.label,
|
|
516
529
|
startedAt: s.startedAt,
|
|
517
530
|
endedAt: s.endedAt ?? s.settledAt,
|
|
518
531
|
status: "closed",
|
|
519
|
-
subtotal:
|
|
520
|
-
total:
|
|
532
|
+
subtotal: sessionSubtotal,
|
|
533
|
+
total: Math.max(0, sessionSubtotal + sessionAdjustments.reduce((sum, adjustment) => sum + toNumber(adjustment?.amount ?? 0), 0)),
|
|
521
534
|
chargeItems: rec?.chargeItems ?? [],
|
|
522
|
-
adjustments:
|
|
535
|
+
adjustments: sessionAdjustments,
|
|
523
536
|
};
|
|
524
537
|
});
|
|
525
538
|
const synthetic = {
|
|
526
|
-
|
|
539
|
+
settlement: {
|
|
527
540
|
playerId: settlement.playerId,
|
|
528
541
|
subtotal: settlement.subtotal ?? 0,
|
|
529
542
|
total: settlement.total ?? 0,
|
|
@@ -531,6 +544,9 @@ class PrismKoishiService {
|
|
|
531
544
|
sessionPreviews,
|
|
532
545
|
chargeItems: result?.chargeItems ?? [],
|
|
533
546
|
adjustments: result?.adjustments ?? [],
|
|
547
|
+
checkoutAdjustments,
|
|
548
|
+
pricingCapAdjustments,
|
|
549
|
+
globalCapWindows: result?.globalCapWindows ?? [],
|
|
534
550
|
assetHoldings: result?.assetHoldings ?? [],
|
|
535
551
|
};
|
|
536
552
|
const receipt = await this.formatCheckoutPreview(synthetic, sender, "✅ 退场成功 · 结算账单");
|
|
@@ -544,6 +560,26 @@ class PrismKoishiService {
|
|
|
544
560
|
const result = (await this.client.getWalletByIdentity(this.identity(sender)));
|
|
545
561
|
return formatWallet(result, this.config.currencyName);
|
|
546
562
|
}
|
|
563
|
+
async benchmarkApi(sender, rawCount) {
|
|
564
|
+
const count = rawCount == null || rawCount === "" ? 3 : Number(rawCount);
|
|
565
|
+
if (!Number.isInteger(count) || count < 1 || count > 10)
|
|
566
|
+
return "次数须为 1 到 10 的整数。";
|
|
567
|
+
const samples = [];
|
|
568
|
+
for (let index = 0; index < count; index++) {
|
|
569
|
+
const startedAt = performance.now();
|
|
570
|
+
await this.client.getWalletByIdentity(this.identity(sender));
|
|
571
|
+
samples.push(performance.now() - startedAt);
|
|
572
|
+
}
|
|
573
|
+
const min = Math.min(...samples);
|
|
574
|
+
const max = Math.max(...samples);
|
|
575
|
+
const average = samples.reduce((sum, sample) => sum + sample, 0) / samples.length;
|
|
576
|
+
return [
|
|
577
|
+
`📡 PRiSM API 测速(钱包查询,${count} 次)`,
|
|
578
|
+
`最小:${formatNumber(min)} ms`,
|
|
579
|
+
`平均:${formatNumber(average)} ms`,
|
|
580
|
+
`最大:${formatNumber(max)} ms`,
|
|
581
|
+
].join("\n");
|
|
582
|
+
}
|
|
547
583
|
async items(sender) {
|
|
548
584
|
const holdings = extractRows((await this.client.getAssetsByIdentity(this.identity(sender))));
|
|
549
585
|
if (holdings.length === 0)
|
|
@@ -870,7 +906,10 @@ class PrismKoishiService {
|
|
|
870
906
|
},
|
|
871
907
|
];
|
|
872
908
|
}
|
|
873
|
-
const adjustments = result?.adjustments ?? [];
|
|
909
|
+
const adjustments = (result?.adjustments ?? []);
|
|
910
|
+
const pricingCapAdjustments = (result?.pricingCapAdjustments ?? adjustments.filter(isPricingCapAdjustment));
|
|
911
|
+
const sessionAdjustmentKeys = new Set(sessionPreviews.flatMap((session) => (session?.adjustments ?? []).map(adjustmentKey)));
|
|
912
|
+
const checkoutAdjustments = (result?.checkoutAdjustments ?? adjustments.filter((adjustment) => !isPricingCapAdjustment(adjustment) && !sessionAdjustmentKeys.has(adjustmentKey(adjustment))));
|
|
874
913
|
const assetHoldings = result?.assetHoldings ?? [];
|
|
875
914
|
const lines = [];
|
|
876
915
|
lines.push(title);
|
|
@@ -885,7 +924,7 @@ class PrismKoishiService {
|
|
|
885
924
|
}
|
|
886
925
|
}
|
|
887
926
|
const hasNonZeroSessionTotal = sessionPreviews.some((session) => toNumber(session?.total ?? 0) !== 0);
|
|
888
|
-
const hasNonZeroAdjustment = hasAdjustmentEntries(
|
|
927
|
+
const hasNonZeroAdjustment = hasAdjustmentEntries(checkoutAdjustments, sessionPreviews);
|
|
889
928
|
if (!hasNonZeroSessionTotal && !hasNonZeroAdjustment) {
|
|
890
929
|
lines.push("");
|
|
891
930
|
lines.push("本次未产生费用");
|
|
@@ -925,10 +964,51 @@ class PrismKoishiService {
|
|
|
925
964
|
lines.push(` └ ${adjLabel}:${formatNumber(amount)}${currency}`);
|
|
926
965
|
}
|
|
927
966
|
}
|
|
967
|
+
const cappedWindows = (result?.globalCapWindows ?? []).filter((window) => toNumber(window?.currentAmount) !== toNumber(window?.amountApplied));
|
|
968
|
+
const appliedCapAdjustments = pricingCapAdjustments.filter((adjustment) => toNumber(adjustment?.amount ?? 0) !== 0);
|
|
969
|
+
if (cappedWindows.length > 0 || appliedCapAdjustments.length > 0) {
|
|
970
|
+
lines.push("");
|
|
971
|
+
lines.push("封顶:");
|
|
972
|
+
if (cappedWindows.length > 0) {
|
|
973
|
+
for (const window of cappedWindows) {
|
|
974
|
+
const label = firstDefined(window, "ruleLabel", "label", "name") ?? "封顶时段";
|
|
975
|
+
const startedAt = parseDateTime(window?.windowStartedAt);
|
|
976
|
+
const datedLabel = startedAt ? `${formatMD(startedAt)} ${label}` : label;
|
|
977
|
+
const currentAmount = toNumber(window?.currentAmount);
|
|
978
|
+
const amountApplied = toNumber(window?.amountApplied);
|
|
979
|
+
const priceCap = toNumber(window?.priceCap);
|
|
980
|
+
const paidBefore = toNumber(window?.paidBefore);
|
|
981
|
+
const details = [`上限${formatNumber(priceCap)}`];
|
|
982
|
+
if (paidBefore > 0)
|
|
983
|
+
details.push(`已计${formatNumber(paidBefore)}`);
|
|
984
|
+
lines.push(`- ${datedLabel}:${formatNumber(currentAmount)} → ${formatNumber(amountApplied)}${currency}(${details.join(",")})`);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
for (const adjustment of appliedCapAdjustments) {
|
|
989
|
+
const label = firstDefined(adjustment, "label", "name", "source") ?? "封顶时段";
|
|
990
|
+
lines.push(`- ${label}:计费调整 ${formatNumber(adjustment?.amount ?? 0)}${currency}`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
const visibleCheckoutAdjustments = checkoutAdjustments.filter((adjustment) => toNumber(firstDefined(adjustment ?? {}, "amount", "saved", 0)) !== 0);
|
|
928
995
|
lines.push("");
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
996
|
+
const cappedTotal = Math.max(0, toNumber(subtotal) + pricingCapAdjustments.reduce((sum, adjustment) => sum + toNumber(adjustment?.amount ?? 0), 0));
|
|
997
|
+
lines.push(`计费总价:${formatNumber(cappedTotal)}${currency}`);
|
|
998
|
+
const hasManualAdjustment = visibleCheckoutAdjustments.some((adjustment) => cleanText(adjustment?.source).startsWith("staff.override:"));
|
|
999
|
+
if (visibleCheckoutAdjustments.length > 0) {
|
|
1000
|
+
lines.push("");
|
|
1001
|
+
for (const adjustment of visibleCheckoutAdjustments) {
|
|
1002
|
+
const amount = toNumber(firstDefined(adjustment ?? {}, "amount", "saved", 0));
|
|
1003
|
+
const label = firstDefined(adjustment ?? {}, "label", "name", "source") ?? "优惠";
|
|
1004
|
+
lines.push(`${label}:${formatNumber(amount)}${currency}`);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (hasNonZeroAdjustment) {
|
|
1008
|
+
if (visibleCheckoutAdjustments.length > 0)
|
|
1009
|
+
lines.push("");
|
|
1010
|
+
lines.push(`${hasManualAdjustment ? "调整后价格" : "优惠后价格"}:${formatNumber(total)}${currency}`);
|
|
1011
|
+
}
|
|
932
1012
|
if (hasBalance) {
|
|
933
1013
|
const isPreview = result?.settlementPreview != null && result?.settlement == null;
|
|
934
1014
|
if (isPreview) {
|
|
@@ -1115,6 +1195,21 @@ function formatPlayerReference(sender, provider = "qq") {
|
|
|
1115
1195
|
const name = sender.name && sender.name !== sender.id ? sender.name : "未知昵称";
|
|
1116
1196
|
return `${name}(${provider.toUpperCase()}:${sender.id})`;
|
|
1117
1197
|
}
|
|
1198
|
+
function adjustmentKey(adjustment) {
|
|
1199
|
+
const id = cleanText(adjustment?.id);
|
|
1200
|
+
if (id)
|
|
1201
|
+
return `id:${id}`;
|
|
1202
|
+
return JSON.stringify([
|
|
1203
|
+
adjustment?.source ?? "",
|
|
1204
|
+
adjustment?.label ?? "",
|
|
1205
|
+
toNumber(adjustment?.amount ?? 0),
|
|
1206
|
+
]);
|
|
1207
|
+
}
|
|
1208
|
+
function isPricingCapAdjustment(adjustment) {
|
|
1209
|
+
return adjustment?.pricingCapHistory != null ||
|
|
1210
|
+
cleanText(adjustment?.source).startsWith("time.cap:") ||
|
|
1211
|
+
cleanText(adjustment?.id).startsWith("time-cap:");
|
|
1212
|
+
}
|
|
1118
1213
|
function toNumber(value) {
|
|
1119
1214
|
if (value == null)
|
|
1120
1215
|
return 0;
|
|
@@ -1175,6 +1270,10 @@ function formatHM(dt) {
|
|
|
1175
1270
|
const pad = (n) => String(n).padStart(2, "0");
|
|
1176
1271
|
return `${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
|
|
1177
1272
|
}
|
|
1273
|
+
function formatMD(dt) {
|
|
1274
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1275
|
+
return `${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}`;
|
|
1276
|
+
}
|
|
1178
1277
|
function formatDateTime(value) {
|
|
1179
1278
|
const dt = parseDateTime(value);
|
|
1180
1279
|
if (!dt)
|