koishi-plugin-prism 0.1.29 → 0.1.31

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
@@ -58,7 +58,7 @@ pricingConfigIds: [pricing-mahjong-a]
58
58
  玩家在未产生任何费用时退场,机器人会简洁显示“本次未产生费用”和当前余额;存在收费或优惠明细时仍显示完整结算账单。
59
59
  结账成功回执中的余额为后端已经完成扣款后的余额;只有 `/billing` 预览会显示当前余额与预计结账后余额。
60
60
  方案内区间封顶直接计入对应计时费用;全局封顶按日期和时段逐条列出,不合并、不截断,并直接形成计费总价,不作为优惠。若旧后端未返回结构化封顶窗口,插件会用封顶历史恢复原价与计入金额,不会显示内部 adjustment 差额。只作用于整次结账的资产优惠直接列在计费总价下方,不显示额外标题或 emoji,也不会误归属到最后一个 session。
61
- 结账成功后,机器人会向 `staffUserIds` 与 `logoutNotifyUserIds` 中的用户私聊同一份账单,账单会明确显示结账玩家身份。
61
+ 在玩家退场或管理员覆盖结账成功后,机器人会向 `staffUserIds` 与 `logoutNotifyUserIds` 中的用户私聊同一份账单,账单会明确显示结账玩家身份。
62
62
  同一玩家在前一次退场结账尚未完成时重复发送 `/logout` 或 `/退场`,机器人会复用同一次结账请求与账单,避免重复扣款。
63
63
  账单和管理员代操作回执均使用“平台昵称(QQ:号码)”称呼玩家;平台暂时无法提供昵称时显示“未知昵称(QQ:号码)”,不会显示内部玩家 ID。
64
64
  管理员 `/add` 增加免费余额;`/del` 按结账相同的顺序从可用免费余额、再从付费余额扣除,余额不足时会返回余额不足提示。
package/lib/index.js CHANGED
@@ -388,9 +388,12 @@ class PrismKoishiService {
388
388
  const total = Number(rawAmount);
389
389
  if (!Number.isFinite(total) || total < 0)
390
390
  return "金额必须为非负数";
391
- const reason = cleanText(rawReason) || "Koishi 管理员手动调价";
392
- await this.client.checkoutWithOverrideByIdentity(this.identity(sender), total, reason);
393
- return `✅ 已为用户 ${formatPlayerReference(sender, this.config.provider)} 覆盖结账为 ${formatNumber(total)} ${this.config.currencyName}`;
391
+ const reason = cleanText(rawReason) || "管理员调价";
392
+ const result = (await this.client.checkoutWithOverrideByIdentity(this.identity(sender), total, reason));
393
+ const playerId = String(result?.playerSettlement?.playerId ?? result?.settlement?.playerId ?? "");
394
+ if (playerId)
395
+ this.removeMahjongPlayer(playerId);
396
+ return this.formatAndNotifyCheckout(result, sender, "✅ 覆盖结账成功 · 结算账单", bot);
394
397
  }, bot);
395
398
  }
396
399
  async mahjongJoin(sender, rawTableId) {
@@ -524,6 +527,12 @@ class PrismKoishiService {
524
527
  }
525
528
  async performLogout(sender, bot) {
526
529
  const result = (await this.client.confirmCheckoutByIdentity(this.identity(sender), false));
530
+ const playerId = String(result?.playerSettlement?.playerId ?? result?.settlement?.playerId ?? "");
531
+ if (playerId)
532
+ this.removeMahjongPlayer(playerId);
533
+ return this.formatAndNotifyCheckout(result, sender, "✅ 退场成功 · 结算账单", bot);
534
+ }
535
+ async formatAndNotifyCheckout(result, sender, title, bot) {
527
536
  const settlement = result?.playerSettlement ?? result?.settlement ?? {};
528
537
  const records = result?.settlements ?? [];
529
538
  const checkoutAdjustments = (result?.checkoutAdjustments ?? []);
@@ -565,10 +574,11 @@ class PrismKoishiService {
565
574
  globalCapWindows: result?.globalCapWindows ?? [],
566
575
  assetHoldings: result?.assetHoldings ?? [],
567
576
  };
568
- const receipt = await this.formatCheckoutPreview(synthetic, sender, "✅ 退场成功 · 结算账单");
577
+ const receipt = await this.formatCheckoutPreview(synthetic, sender, title);
569
578
  const recipients = [...new Set([...(this.config.staffUserIds ?? []), ...(this.config.logoutNotifyUserIds ?? [])])];
570
579
  if (recipients.length > 0 && bot?.broadcast) {
571
- await bot.broadcast(recipients, receipt);
580
+ const channelIds = recipients.map((id) => (id.includes(":") ? id : `private:${id}`));
581
+ await bot.broadcast(channelIds, receipt);
572
582
  }
573
583
  return receipt;
574
584
  }
@@ -808,6 +818,12 @@ class PrismKoishiService {
808
818
  }
809
819
  return null;
810
820
  }
821
+ removeMahjongPlayer(playerId) {
822
+ for (const state of this.mahjongTables.values()) {
823
+ delete state.activeSessions[playerId];
824
+ state.waiting = state.waiting.filter((seat) => seat.playerId !== playerId);
825
+ }
826
+ }
811
827
  mahjongTableConfigs() {
812
828
  return resolveMahjongTableConfigs(this.config.mahjongTableConfigs ?? [], this.config.mahjongTables ?? "", this.config.mahjongLabelPrefix ?? "麻将桌");
813
829
  }
@@ -817,6 +833,9 @@ class PrismKoishiService {
817
833
  mahjongSessionLabel(table, this.config.mahjongLabelPrefix ?? "麻将桌"),
818
834
  table,
819
835
  ]));
836
+ // All playerIds that still have at least one active session in the backend.
837
+ // Used to evict waiting seats for players who have fully checked out externally.
838
+ const allActivePlayerIds = new Set(sessions.map((s) => String(s.playerId ?? "")).filter(Boolean));
820
839
  const activeByTable = new Map();
821
840
  for (const session of sessions) {
822
841
  const table = tableByLabel.get(session.label ?? "");
@@ -831,7 +850,9 @@ class PrismKoishiService {
831
850
  for (const table of tables) {
832
851
  const state = this.mahjongTables.get(table.tableId) ?? { waiting: [], activeSessions: {} };
833
852
  state.activeSessions = activeByTable.get(table.tableId) ?? {};
834
- state.waiting = state.waiting.filter((seat) => !state.activeSessions[seat.playerId]);
853
+ // Remove seats that have been promoted to active, or whose players have
854
+ // fully left the backend (no entry session remaining — orphan eviction).
855
+ state.waiting = state.waiting.filter((seat) => !state.activeSessions[seat.playerId] && allActivePlayerIds.has(seat.playerId));
835
856
  if (state.waiting.length > 0 || Object.keys(state.activeSessions).length > 0 || this.mahjongTables.has(table.tableId)) {
836
857
  this.mahjongTables.set(table.tableId, state);
837
858
  }
@@ -1014,11 +1035,15 @@ class PrismKoishiService {
1014
1035
  }
1015
1036
  }
1016
1037
  }
1017
- const visibleCheckoutAdjustments = checkoutAdjustments.filter((adjustment) => toNumber(firstDefined(adjustment ?? {}, "amount", "saved", 0)) !== 0);
1038
+ const visibleCheckoutAdjustments = checkoutAdjustments.filter((adjustment) => {
1039
+ const amount = toNumber(firstDefined(adjustment ?? {}, "amount", "saved", 0));
1040
+ const isOverride = cleanText(adjustment?.source).startsWith("staff.override:");
1041
+ return amount !== 0 && !isOverride;
1042
+ });
1018
1043
  lines.push("");
1019
1044
  const cappedTotal = Math.max(0, toNumber(subtotal) + pricingCapAdjustments.reduce((sum, adjustment) => sum + toNumber(adjustment?.amount ?? 0), 0));
1020
1045
  lines.push(`计费总价:${formatNumber(cappedTotal)}${currency}`);
1021
- const hasManualAdjustment = visibleCheckoutAdjustments.some((adjustment) => cleanText(adjustment?.source).startsWith("staff.override:"));
1046
+ const hasManualAdjustment = checkoutAdjustments.some((adjustment) => cleanText(adjustment?.source).startsWith("staff.override:"));
1022
1047
  if (visibleCheckoutAdjustments.length > 0) {
1023
1048
  lines.push("");
1024
1049
  for (const adjustment of visibleCheckoutAdjustments) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-prism",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "description": "PRiSM Next 计费与设备管理系统的 Koishi 机器人集成插件",
5
5
  "main": "./lib/index.js",
6
6
  "exports": {