koishi-plugin-prism 0.1.20 → 0.1.23

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 CHANGED
@@ -57,6 +57,7 @@ pricingConfigIds: [pricing-mahjong-a]
57
57
  * `logout` - 结算当前玩家的计费场次。
58
58
  玩家在未产生任何费用时退场,机器人会简洁显示“本次未产生费用”和当前余额;存在收费或优惠明细时仍显示完整结算账单。
59
59
  结账成功后,机器人会向 `staffUserIds` 与 `logoutNotifyUserIds` 中的用户私聊同一份账单,账单会明确显示结账玩家身份。
60
+ 同一玩家在前一次退场结账尚未完成时重复发送 `/logout` 或 `/退场`,机器人会复用同一次结账请求与账单,避免重复扣款。
60
61
  账单和管理员代操作回执均使用“平台昵称(QQ:号码)”称呼玩家;平台暂时无法提供昵称时显示“未知昵称(QQ:号码)”,不会显示内部玩家 ID。
61
62
  管理员 `/add` 增加免费余额;`/del` 按结账相同的顺序从可用免费余额、再从付费余额扣除,余额不足时会返回余额不足提示。
62
63
  * `billing` - 预览当前玩家本场计费的消费费用。
@@ -74,6 +75,7 @@ pricingConfigIds: [pricing-mahjong-a]
74
75
  * `mahjong <tableId>` / `上桌 [tableId]` - 加入指定麻将桌;`/上桌` 未提供桌号时会引导查看 `/麻将列表`。仅允许已通过 `login`/`入场` 开启默认入场会话的玩家使用。
75
76
  * `下桌` - 自动离开当前所在麻将桌。
76
77
  * `麻将列表` - 查看已配置机器的桌名、命令别名,以及空闲、等位或游玩中状态。
78
+ * `api测速 [次数]` - 连续查询自己的钱包,显示 Bot 到 PRiSM API 的最小、平均与最大延迟(默认 3 次,最多 10 次)。
77
79
 
78
80
  ### 管理员快捷指令
79
81
  启用 `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)));
@@ -217,10 +219,10 @@ class PrismApiClient {
217
219
  body: this.identityBody(identity),
218
220
  });
219
221
  }
220
- async confirmCheckoutByIdentity(identity) {
222
+ async confirmCheckoutByIdentity(identity, closeSessionsBeforeBalanceCheck = true) {
221
223
  return this.request("POST", "/rpc/integration/players/by-identity/checkout/confirm", {
222
224
  token: this.config.integrationToken,
223
- body: this.identityBody(identity),
225
+ body: { ...this.identityBody(identity), closeSessionsBeforeBalanceCheck },
224
226
  });
225
227
  }
226
228
  async redeemCodeByIdentity(identity, code) {
@@ -297,6 +299,7 @@ class PrismApiClient {
297
299
  class PrismKoishiService {
298
300
  config;
299
301
  mahjongTables = new Map();
302
+ logoutInFlight = new Map();
300
303
  client;
301
304
  constructor(ctx, config) {
302
305
  this.config = config;
@@ -490,7 +493,21 @@ class PrismKoishiService {
490
493
  return this.formatCheckoutPreview(result, sender);
491
494
  }
492
495
  async logout(sender, bot) {
493
- const result = (await this.client.confirmCheckoutByIdentity(this.identity(sender)));
496
+ const existing = this.logoutInFlight.get(sender.id);
497
+ if (existing)
498
+ return existing;
499
+ const task = this.performLogout(sender, bot);
500
+ this.logoutInFlight.set(sender.id, task);
501
+ try {
502
+ return await task;
503
+ }
504
+ finally {
505
+ if (this.logoutInFlight.get(sender.id) === task)
506
+ this.logoutInFlight.delete(sender.id);
507
+ }
508
+ }
509
+ async performLogout(sender, bot) {
510
+ const result = (await this.client.confirmCheckoutByIdentity(this.identity(sender), false));
494
511
  const settlement = result?.playerSettlement ?? result?.settlement ?? {};
495
512
  const records = result?.settlements ?? [];
496
513
  const sessionPreviews = records.map((rec) => {
@@ -529,6 +546,26 @@ class PrismKoishiService {
529
546
  const result = (await this.client.getWalletByIdentity(this.identity(sender)));
530
547
  return formatWallet(result, this.config.currencyName);
531
548
  }
549
+ async benchmarkApi(sender, rawCount) {
550
+ const count = rawCount == null || rawCount === "" ? 3 : Number(rawCount);
551
+ if (!Number.isInteger(count) || count < 1 || count > 10)
552
+ return "次数须为 1 到 10 的整数。";
553
+ const samples = [];
554
+ for (let index = 0; index < count; index++) {
555
+ const startedAt = performance.now();
556
+ await this.client.getWalletByIdentity(this.identity(sender));
557
+ samples.push(performance.now() - startedAt);
558
+ }
559
+ const min = Math.min(...samples);
560
+ const max = Math.max(...samples);
561
+ const average = samples.reduce((sum, sample) => sum + sample, 0) / samples.length;
562
+ return [
563
+ `📡 PRiSM API 测速(钱包查询,${count} 次)`,
564
+ `最小:${formatNumber(min)} ms`,
565
+ `平均:${formatNumber(average)} ms`,
566
+ `最大:${formatNumber(max)} ms`,
567
+ ].join("\n");
568
+ }
532
569
  async items(sender) {
533
570
  const holdings = extractRows((await this.client.getAssetsByIdentity(this.identity(sender))));
534
571
  if (holdings.length === 0)
@@ -914,8 +951,19 @@ class PrismKoishiService {
914
951
  lines.push(`计费总价:${formatNumber(subtotal)}${currency}`);
915
952
  if (hasNonZeroAdjustment)
916
953
  lines.push(`优惠后价格:${formatNumber(total)}${currency}`);
917
- if (hasBalance)
918
- lines.push(`扣款后余额:${formatNumber(balance)}${currency}`);
954
+ if (hasBalance) {
955
+ const isPreview = result?.settlementPreview != null && result?.settlement == null;
956
+ if (isPreview) {
957
+ const projectedBalance = balance - toNumber(total);
958
+ lines.push(`当前余额:${formatNumber(balance)}${currency}`);
959
+ lines.push(projectedBalance >= 0
960
+ ? `预计结账后余额:${formatNumber(projectedBalance)}${currency}`
961
+ : `预计结账后余额:余额不足(还差 ${formatNumber(-projectedBalance)}${currency})`);
962
+ }
963
+ else {
964
+ lines.push(`扣款后余额:${formatNumber(balance)}${currency}`);
965
+ }
966
+ }
919
967
  return lines.join("\n");
920
968
  }
921
969
  handleCommandError(error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-prism",
3
- "version": "0.1.20",
3
+ "version": "0.1.23",
4
4
  "description": "PRiSM Next 计费与设备管理系统的 Koishi 机器人集成插件",
5
5
  "main": "./lib/index.js",
6
6
  "exports": {