koishi-plugin-prism 0.1.6 → 0.1.10
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 +18 -13
- package/lib/index.d.ts +3 -0
- package/lib/index.js +226 -160
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
## 🌟 功能特性
|
|
8
8
|
|
|
9
|
-
* 🎮 **玩家入场与结算**:通过 `/login` 和 `/logout` 指令开启或结算计费场次,支持 `/入场`
|
|
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
|
+
* 🛠️ **管理员快捷指令**:允许管理员为指定平台用户增加或扣除余额,并覆盖结账金额后立即结账。
|
|
15
15
|
|
|
16
16
|
## ⚙️ 配置说明
|
|
17
17
|
|
|
@@ -29,8 +29,12 @@
|
|
|
29
29
|
| `defaultScanProvider` | `string` | `"aime"` | 默认模拟刷卡时的读卡器协议提供商(如 `"aime"`)。 |
|
|
30
30
|
| `currencyName` | `string` | `"金币"` | 账户货币在显示时的自定义单位名称。 |
|
|
31
31
|
| `resolveDisplayName` | `function` | - | 可选。自定义用于获取群内昵称作为玩家注册名的异步逻辑。 |
|
|
32
|
-
| `enableStaffCommands` | `boolean` | `false` |
|
|
33
|
-
| `staffUserIds` | `string[]` | `[]` |
|
|
32
|
+
| `enableStaffCommands` | `boolean` | `false` | 是否开启管理员快捷指令。 |
|
|
33
|
+
| `staffUserIds` | `string[]` | `[]` | 允许执行管理员快捷指令的平台用户 ID(如 QQ 号)白名单。空列表不授予目标用户操作权限。 |
|
|
34
|
+
| `staffSessionToken` | `string` | - | 管理员写操作所需的 Staff 管理 Token。 |
|
|
35
|
+
| `logoutNotifyUserIds` | `string[]` | `[]` | 结账成功后额外私聊完整账单的平台用户 ID。通知收件人为该列表与 `staffUserIds` 的去重并集。 |
|
|
36
|
+
|
|
37
|
+
管理员快捷指令必须同时配置 `enableStaffCommands: true`、`staffUserIds` 和 `staffSessionToken`。目标用户参数使用 Koishi 的 `user` 选择器;只有白名单内的管理员可以操作其他用户。
|
|
34
38
|
|
|
35
39
|
## 📝 机器人指令列表
|
|
36
40
|
|
|
@@ -38,10 +42,12 @@
|
|
|
38
42
|
* `register` - 绑定或注册当前平台账号到 PRiSM 账户。
|
|
39
43
|
* `login` / `入场` - 开启当前玩家的计费场次。
|
|
40
44
|
* `logout` - 结算当前玩家的计费场次。
|
|
45
|
+
玩家在未产生任何费用时退场,机器人会简洁显示“本次未产生费用”和当前余额;存在收费或优惠明细时仍显示完整结算账单。
|
|
46
|
+
结账成功后,机器人会向 `staffUserIds` 与 `logoutNotifyUserIds` 中的用户私聊同一份账单,账单会明确显示结账玩家身份。
|
|
41
47
|
* `billing` - 预览当前玩家本场计费的消费费用。
|
|
42
48
|
* `wallet` - 查看当前玩家的钱包余额。
|
|
43
49
|
* `items` - 查看当前玩家持有的道具或资产。
|
|
44
|
-
* `list` -
|
|
50
|
+
* `list` - 查看当前在线/在店游玩玩家的列表,按音乐游戏和麻将桌分组并对同一玩家的多个计时会话去重;麻将桌显示当前人数和容量。未满桌候座由机器人进程暂存,机器人重启后不会保留。
|
|
45
51
|
* `show [deviceId]` - 查看设备电源与连接状态。
|
|
46
52
|
* `history` - 查看自己的历史游玩记录。
|
|
47
53
|
* `lock` - 发送开门指令。
|
|
@@ -50,15 +56,14 @@
|
|
|
50
56
|
* `coin <deviceId> [count]` - 请求向指定设备投币指定枚数。
|
|
51
57
|
* `scan <deviceId> <subject>` - 请求向设备发送模拟刷卡。
|
|
52
58
|
* `redeem <code>` - 兑换礼物码。
|
|
53
|
-
* `mahjong <tableId>` / `上桌 <tableId>` -
|
|
59
|
+
* `mahjong <tableId>` / `上桌 <tableId>` - 加入指定麻将桌;仅允许已通过 `login`/`入场` 开启默认入场会话的玩家使用。
|
|
54
60
|
* `下桌 <tableId>` - 离开指定麻将桌。
|
|
55
61
|
|
|
56
|
-
###
|
|
57
|
-
|
|
58
|
-
* `
|
|
59
|
-
* `
|
|
60
|
-
* `
|
|
61
|
-
* `admin.checkout <playerId>` - 代替并强制为指定玩家结账。
|
|
62
|
+
### 管理员快捷指令
|
|
63
|
+
启用 `enableStaffCommands`、配置 `staffUserIds` 白名单与 `staffSessionToken` 后可使用:
|
|
64
|
+
* `add <target:user> <amount:number>` - 为目标用户增加余额。
|
|
65
|
+
* `del <target:user> <amount:number>` - 从目标用户扣除余额。
|
|
66
|
+
* `overwrite <target:user> <amount:number> [reason:text]` - 覆盖目标用户本次结账金额,并立即执行结账;未填写原因时使用默认管理员调价原因。
|
|
62
67
|
|
|
63
68
|
## 🛠️ 本地开发与构建
|
|
64
69
|
|
package/lib/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export type PrismKoishiPluginConfig = {
|
|
|
24
24
|
currencyName: string;
|
|
25
25
|
enableStaffCommands?: boolean;
|
|
26
26
|
staffUserIds?: string[];
|
|
27
|
+
logoutNotifyUserIds?: string[];
|
|
27
28
|
mahjongTables?: string;
|
|
28
29
|
mahjongTableSize?: number;
|
|
29
30
|
mahjongLabelPrefix?: string;
|
|
@@ -56,6 +57,7 @@ export type KoishiLikeContext = {
|
|
|
56
57
|
export type KoishiActionContext = {
|
|
57
58
|
session: {
|
|
58
59
|
userId: string;
|
|
60
|
+
messageId?: string;
|
|
59
61
|
senderId?: string;
|
|
60
62
|
senderName?: string;
|
|
61
63
|
username?: string;
|
|
@@ -63,6 +65,7 @@ export type KoishiActionContext = {
|
|
|
63
65
|
getUser?(id: string): Promise<{
|
|
64
66
|
name?: string;
|
|
65
67
|
}>;
|
|
68
|
+
broadcast?(userIds: string[], content: string): Promise<void>;
|
|
66
69
|
};
|
|
67
70
|
};
|
|
68
71
|
};
|
package/lib/index.js
CHANGED
|
@@ -20,6 +20,7 @@ exports.Config = koishi_1.Schema.object({
|
|
|
20
20
|
loginSessionLabel: koishi_1.Schema.string().default("音游区间").description("默认入场场次标签 (防重复入场)"),
|
|
21
21
|
enableStaffCommands: koishi_1.Schema.boolean().default(false).description("是否启用管理员指令"),
|
|
22
22
|
staffUserIds: koishi_1.Schema.array(koishi_1.Schema.string()).default([]).description("允许执行管理员指令的平台用户ID列表"),
|
|
23
|
+
logoutNotifyUserIds: koishi_1.Schema.array(koishi_1.Schema.string()).default([]).description("结账账单私聊通知的平台用户ID列表"),
|
|
23
24
|
powerOffInterval: koishi_1.Schema.number().default(0).description("无人自动关机等待秒数 (0为禁用)"),
|
|
24
25
|
mahjongTables: koishi_1.Schema.string().description("麻将桌配置"),
|
|
25
26
|
mahjongTableSize: koishi_1.Schema.number().default(4).description("麻将桌人数限制"),
|
|
@@ -52,47 +53,41 @@ const USAGE = {
|
|
|
52
53
|
prism_redeem: "/prism redeem <兑换码>",
|
|
53
54
|
list: "/list",
|
|
54
55
|
show: "/show [设备ID]",
|
|
55
|
-
staff_create_player: "/prism.admin.create-player <玩家昵称>",
|
|
56
|
-
staff_grant_balance: "/prism.admin.grant-balance <玩家ID> <金额>",
|
|
57
|
-
staff_redeem_code: "/prism.admin.redeem-code <兑换码> <礼物ID>",
|
|
58
|
-
staff_checkout: "/prism.admin.checkout <玩家ID>",
|
|
59
56
|
};
|
|
60
57
|
function applyPrismKoishiPlugin(ctx, config) {
|
|
61
58
|
const service = new PrismKoishiService(ctx, config);
|
|
62
59
|
const wrap = (handler) => async (context, ...args) => {
|
|
63
60
|
try {
|
|
64
|
-
|
|
61
|
+
const message = await handler(context, ...args);
|
|
62
|
+
return context.session?.messageId ? `${(0, koishi_1.h)("quote", { id: context.session.messageId })}${message}` : message;
|
|
65
63
|
}
|
|
66
64
|
catch (error) {
|
|
67
|
-
|
|
65
|
+
const message = service.handleCommandError(error);
|
|
66
|
+
return context.session?.messageId ? `${(0, koishi_1.h)("quote", { id: context.session.messageId })}${message}` : message;
|
|
68
67
|
}
|
|
69
68
|
};
|
|
70
69
|
ctx.command("register", "绑定或注册当前平台用户到 PRiSM").action(wrap(async (context) => service.register(await service.sender(context))));
|
|
71
|
-
ctx.command("login", "
|
|
72
|
-
ctx.command("入场", "入场
|
|
70
|
+
ctx.command("login [target:user]", "开启玩家计费场次").action(wrap(async (context, target) => service.loginForTarget(await service.sender(context), target)));
|
|
71
|
+
ctx.command("入场 [target:user]", "入场").action(wrap(async (context, target) => service.loginForTarget(await service.sender(context), target)));
|
|
73
72
|
ctx.command("mahjong <tableId>", "加入指定麻将桌").action(wrap(async (context, tableId) => service.mahjongJoin(await service.sender(context), tableId)));
|
|
74
73
|
ctx.command("上桌 <tableId>", "加入指定麻将桌").action(wrap(async (context, tableId) => service.mahjongJoin(await service.sender(context), tableId)));
|
|
75
74
|
ctx.command("下桌 <tableId>", "离开指定麻将桌").action(wrap(async (context, tableId) => service.mahjongLeave(await service.sender(context), tableId)));
|
|
76
|
-
ctx.command("logout", "
|
|
77
|
-
ctx.command("billing", "
|
|
78
|
-
ctx.command("wallet", "
|
|
79
|
-
ctx.command("items", "
|
|
75
|
+
ctx.command("logout [target:user]", "结算玩家计费场次").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.logout(sender, context.session?.bot))));
|
|
76
|
+
ctx.command("billing [target:user]", "预览玩家结账费用").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.billing(sender))));
|
|
77
|
+
ctx.command("wallet [target:user]", "查看玩家钱包").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.wallet(sender))));
|
|
78
|
+
ctx.command("items [target:user]", "查看玩家资产").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.items(sender))));
|
|
80
79
|
ctx.command("list", "查看当前在线玩家列表").action(wrap(async (context) => service.listActiveSessions(await service.sender(context))));
|
|
81
80
|
ctx.command("show [deviceId]", "查看设备电源状态").action(wrap(async (context, deviceId) => service.listDeviceStates(deviceId)));
|
|
82
|
-
ctx.command("history", "
|
|
81
|
+
ctx.command("history [target:user]", "查看玩家历史").action(wrap(async (context, target) => service.withTarget(await service.sender(context), target, (sender) => service.history(sender))));
|
|
83
82
|
ctx.command("lock", "向默认门锁设备发送开门指令").action(wrap(async (context) => service.lock(await service.sender(context))));
|
|
84
83
|
ctx.command("on <deviceId>", "请求启动指定设备电源").action(wrap(async (context, deviceId) => service.powerOn(await service.sender(context), deviceId)));
|
|
85
84
|
ctx.command("off <deviceId>", "请求关闭指定设备电源").action(wrap(async (context, deviceId) => service.powerOff(await service.sender(context), deviceId)));
|
|
86
85
|
ctx.command("coin <deviceId> [count]", "请求向指定设备投币").action(wrap(async (context, deviceId, count) => service.coin(await service.sender(context), deviceId, count)));
|
|
87
86
|
ctx.command("scan <deviceId> <subject>", "请求指定设备模拟刷卡").action(wrap(async (context, deviceId, subject) => service.scan(await service.sender(context), deviceId, subject)));
|
|
88
87
|
ctx.command("redeem <code>", "兑换 PRiSM 礼物码").action(wrap(async (context, code) => service.redeem(await service.sender(context), code)));
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
ctx.command("admin.grant-balance <playerId> <amount>", "给指定玩家发放充值余额").action(wrap(async (context, playerId, amount) => service.staffGrantBalance(await service.sender(context), playerId, amount)));
|
|
93
|
-
ctx.command("admin.redeem-code <code> <presentId>", "创建单次使用兑换码").action(wrap(async (context, code, presentId) => service.staffRedeemCode(await service.sender(context), code, presentId)));
|
|
94
|
-
ctx.command("admin.checkout <playerId>", "替指定玩家结账").action(wrap(async (context, playerId) => service.staffCheckout(await service.sender(context), playerId)));
|
|
95
|
-
}
|
|
88
|
+
ctx.command("add <target:user> <amount:number>", "增加玩家余额").action(wrap(async (context, target, amount) => service.adjustTargetBalance(await service.sender(context), target, amount, 1)));
|
|
89
|
+
ctx.command("del <target:user> <amount:number>", "扣除玩家余额").action(wrap(async (context, target, amount) => service.adjustTargetBalance(await service.sender(context), target, amount, -1)));
|
|
90
|
+
ctx.command("overwrite <target:user> <amount:number> [reason:text]", "覆盖结账金额并立即结账").action(wrap(async (context, target, amount, reason) => service.overwriteTargetCheckout(await service.sender(context), target, amount, reason)));
|
|
96
91
|
const intervalMs = (config.powerOffInterval ?? 0) * 1000;
|
|
97
92
|
if (intervalMs > 0 && typeof ctx.setInterval === "function") {
|
|
98
93
|
ctx.setInterval(() => {
|
|
@@ -278,35 +273,18 @@ class PrismApiClient {
|
|
|
278
273
|
token: this.config.integrationToken,
|
|
279
274
|
});
|
|
280
275
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return this.request("GET", "/rpc/staff/players", {
|
|
284
|
-
token: this.requireStaffSessionToken(),
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
async createStaffPlayer(displayName) {
|
|
288
|
-
return this.request("POST", "/rpc/staff/players", {
|
|
289
|
-
token: this.requireStaffSessionToken(),
|
|
290
|
-
body: { displayName },
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
async grantStaffAssets(playerId, assets) {
|
|
294
|
-
return this.request("POST", "/rpc/staff/players/:playerId/adjustments/assets", {
|
|
276
|
+
async adjustStaffAssets(playerId, adjustments) {
|
|
277
|
+
return this.request("POST", "/rpc/staff/players/:playerId/assets/adjustments", {
|
|
295
278
|
token: this.requireStaffSessionToken(),
|
|
296
279
|
params: { playerId },
|
|
297
|
-
body: {
|
|
280
|
+
body: { adjustments },
|
|
298
281
|
});
|
|
299
282
|
}
|
|
300
|
-
async
|
|
301
|
-
return this.request("POST", "/rpc/staff/
|
|
302
|
-
token: this.requireStaffSessionToken(),
|
|
303
|
-
body: input,
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
async staffCheckout(playerId) {
|
|
307
|
-
return this.request("POST", "/rpc/staff/players/:playerId/settlements/checkout", {
|
|
283
|
+
async checkoutWithOverride(playerId, total, reason) {
|
|
284
|
+
return this.request("POST", "/rpc/staff/players/:playerId/checkout/override", {
|
|
308
285
|
token: this.requireStaffSessionToken(),
|
|
309
286
|
params: { playerId },
|
|
287
|
+
body: { total, reason },
|
|
310
288
|
});
|
|
311
289
|
}
|
|
312
290
|
}
|
|
@@ -374,10 +352,55 @@ class PrismKoishiService {
|
|
|
374
352
|
await this.client.resolveOrRegisterIdentity(this.identity(sender));
|
|
375
353
|
return "注册成功";
|
|
376
354
|
}
|
|
355
|
+
async loginForTarget(actor, targetSubject) {
|
|
356
|
+
return this.withTarget(actor, targetSubject, async (sender, isTargeted) => {
|
|
357
|
+
await this.client.startSessionByIdentity(this.identity(sender), this.loginSessionBody());
|
|
358
|
+
return isTargeted ? `✅ 已为用户 ${sender.id} 入场成功` : "✅ 入场成功";
|
|
359
|
+
});
|
|
360
|
+
}
|
|
377
361
|
async login(sender) {
|
|
378
362
|
await this.client.startSessionByIdentity(this.identity(sender), this.loginSessionBody());
|
|
379
363
|
return "✅ 入场成功";
|
|
380
364
|
}
|
|
365
|
+
async withTarget(actor, targetSubject, action) {
|
|
366
|
+
const target = this.targetSender(actor, targetSubject);
|
|
367
|
+
if (typeof target === "string")
|
|
368
|
+
return target;
|
|
369
|
+
return action(target, target !== actor);
|
|
370
|
+
}
|
|
371
|
+
async adjustTargetBalance(actor, targetSubject, rawAmount, direction) {
|
|
372
|
+
return this.withTarget(actor, targetSubject, async (sender) => {
|
|
373
|
+
const amount = Number(rawAmount);
|
|
374
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
375
|
+
return "金额必须大于 0";
|
|
376
|
+
const player = await this.resolvePlayer(sender);
|
|
377
|
+
const playerId = String(player.id ?? "");
|
|
378
|
+
if (!playerId)
|
|
379
|
+
return "找不到玩家";
|
|
380
|
+
const isAddition = direction === 1;
|
|
381
|
+
await this.client.adjustStaffAssets(playerId, [{
|
|
382
|
+
assetType: "currency",
|
|
383
|
+
assetCode: "paid",
|
|
384
|
+
quantityDelta: amount * direction,
|
|
385
|
+
reason: isAddition ? "Koishi 管理员增加余额" : "Koishi 管理员扣除余额",
|
|
386
|
+
}]);
|
|
387
|
+
return `✅ 已为用户 ${sender.id}${isAddition ? "增加" : "扣除"} ${formatNumber(amount)} ${this.config.currencyName}`;
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
async overwriteTargetCheckout(actor, targetSubject, rawAmount, rawReason) {
|
|
391
|
+
return this.withTarget(actor, targetSubject, async (sender) => {
|
|
392
|
+
const total = Number(rawAmount);
|
|
393
|
+
if (!Number.isFinite(total) || total < 0)
|
|
394
|
+
return "金额必须为非负数";
|
|
395
|
+
const player = await this.resolvePlayer(sender);
|
|
396
|
+
const playerId = String(player.id ?? "");
|
|
397
|
+
if (!playerId)
|
|
398
|
+
return "找不到玩家";
|
|
399
|
+
const reason = cleanText(rawReason) || "Koishi 管理员手动调价";
|
|
400
|
+
await this.client.checkoutWithOverride(playerId, total, reason);
|
|
401
|
+
return `✅ 已为用户 ${sender.id} 覆盖结账为 ${formatNumber(total)} ${this.config.currencyName}`;
|
|
402
|
+
});
|
|
403
|
+
}
|
|
381
404
|
async mahjongJoin(sender, rawTableId) {
|
|
382
405
|
const tableId = cleanText(rawTableId);
|
|
383
406
|
if (!tableId)
|
|
@@ -389,6 +412,11 @@ class PrismKoishiService {
|
|
|
389
412
|
const tableSubject = tableConfig.displayName || `${tableKey} 桌`;
|
|
390
413
|
const player = await this.resolvePlayer(sender);
|
|
391
414
|
const playerId = String(player.id ?? "");
|
|
415
|
+
const activeResult = (await this.client.listActiveSessions());
|
|
416
|
+
const activeSessions = (activeResult.sessions ?? []);
|
|
417
|
+
if (!this.hasEntrySession(playerId, activeSessions)) {
|
|
418
|
+
return "请先入场后再上桌。";
|
|
419
|
+
}
|
|
392
420
|
const existing = this.mahjongTableForPlayer(playerId);
|
|
393
421
|
if (existing)
|
|
394
422
|
return `你已经在 ${existing} 桌了。`;
|
|
@@ -450,7 +478,7 @@ class PrismKoishiService {
|
|
|
450
478
|
const result = (await this.client.previewCheckoutByIdentity(this.identity(sender)));
|
|
451
479
|
return this.formatCheckoutPreview(result, sender);
|
|
452
480
|
}
|
|
453
|
-
async logout(sender) {
|
|
481
|
+
async logout(sender, bot) {
|
|
454
482
|
const result = (await this.client.confirmCheckoutByIdentity(this.identity(sender)));
|
|
455
483
|
const settlement = result?.playerSettlement ?? result?.settlement ?? {};
|
|
456
484
|
const records = result?.settlements ?? [];
|
|
@@ -479,7 +507,12 @@ class PrismKoishiService {
|
|
|
479
507
|
adjustments: result?.adjustments ?? [],
|
|
480
508
|
assetHoldings: result?.assetHoldings ?? [],
|
|
481
509
|
};
|
|
482
|
-
|
|
510
|
+
const receipt = await this.formatCheckoutPreview(synthetic, sender, "✅ 退场成功 · 结算账单");
|
|
511
|
+
const recipients = [...new Set([...(this.config.staffUserIds ?? []), ...(this.config.logoutNotifyUserIds ?? [])])];
|
|
512
|
+
if (recipients.length > 0 && bot?.broadcast) {
|
|
513
|
+
await bot.broadcast(recipients, receipt);
|
|
514
|
+
}
|
|
515
|
+
return receipt;
|
|
483
516
|
}
|
|
484
517
|
async wallet(sender) {
|
|
485
518
|
const result = (await this.client.getWalletByIdentity(this.identity(sender)));
|
|
@@ -552,28 +585,14 @@ class PrismKoishiService {
|
|
|
552
585
|
async listActiveSessions(sender) {
|
|
553
586
|
const result = (await this.client.listActiveSessions());
|
|
554
587
|
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");
|
|
588
|
+
const tableByLabel = new Map(uniqueMahjongConfigs(this.mahjongTableConfigs()).map((table) => [
|
|
589
|
+
mahjongSessionLabel(table, this.config.mahjongLabelPrefix ?? "麻将桌"),
|
|
590
|
+
table,
|
|
591
|
+
]));
|
|
592
|
+
const players = groupSessionsByPlayer(sessions);
|
|
593
|
+
const groups = await this.buildPlayerGroups(players, tableByLabel);
|
|
594
|
+
this.mergeWaitingSeats(groups);
|
|
595
|
+
return formatPlayerGroups(groups, this.config.mahjongTableSize ?? 4, this.config.mahjongLabelPrefix ?? "麻将桌");
|
|
577
596
|
}
|
|
578
597
|
async listDeviceStates(rawAlias) {
|
|
579
598
|
const alias = cleanText(rawAlias);
|
|
@@ -592,87 +611,6 @@ class PrismKoishiService {
|
|
|
592
611
|
.map((d) => `${d.label || d.deviceId}: ${d.state?.state ?? "unknown"}`)
|
|
593
612
|
.join("\n");
|
|
594
613
|
}
|
|
595
|
-
async staffPlayers(sender) {
|
|
596
|
-
const denied = this.staffDenied(sender);
|
|
597
|
-
if (denied)
|
|
598
|
-
return denied;
|
|
599
|
-
const result = (await this.client.listStaffPlayers());
|
|
600
|
-
const players = (result?.players ?? []);
|
|
601
|
-
if (players.length === 0)
|
|
602
|
-
return "🫥 窝里目前没有玩家呢";
|
|
603
|
-
return [
|
|
604
|
-
`👥 窝里目前共有 ${players.length} 人`,
|
|
605
|
-
"",
|
|
606
|
-
...players.map((p) => {
|
|
607
|
-
const lines = [`玩家: ${p?.displayName ?? p?.id ?? ""} (${p?.id ?? ""})`];
|
|
608
|
-
if (p?.status)
|
|
609
|
-
lines.push(`状态: ${p.status}`);
|
|
610
|
-
if (p?.walletTotal != null)
|
|
611
|
-
lines.push(`余额: ${p.walletTotal} ${this.config.currencyName}`);
|
|
612
|
-
return lines.join("\n");
|
|
613
|
-
}),
|
|
614
|
-
].join("\n");
|
|
615
|
-
}
|
|
616
|
-
async staffCreatePlayer(sender, rawDisplayName) {
|
|
617
|
-
const displayName = cleanText(rawDisplayName);
|
|
618
|
-
if (!displayName)
|
|
619
|
-
return commandUsage("staff_create_player");
|
|
620
|
-
const denied = this.staffDenied(sender);
|
|
621
|
-
if (denied)
|
|
622
|
-
return denied;
|
|
623
|
-
const result = (await this.client.createStaffPlayer?.(displayName));
|
|
624
|
-
const player = result?.player ?? {};
|
|
625
|
-
return `创建成功\n玩家: ${player?.displayName ?? displayName}\nID: ${player?.id ?? ""}`;
|
|
626
|
-
}
|
|
627
|
-
async staffGrantBalance(sender, rawPlayerId, rawAmount) {
|
|
628
|
-
const playerId = cleanText(rawPlayerId);
|
|
629
|
-
const amount = cleanText(rawAmount);
|
|
630
|
-
if (!playerId || !amount)
|
|
631
|
-
return commandUsage("staff_grant_balance");
|
|
632
|
-
const denied = this.staffDenied(sender);
|
|
633
|
-
if (denied)
|
|
634
|
-
return denied;
|
|
635
|
-
await this.client.grantStaffAssets?.(playerId, [
|
|
636
|
-
{
|
|
637
|
-
assetType: "currency",
|
|
638
|
-
assetCode: "paid",
|
|
639
|
-
amount: Number(amount),
|
|
640
|
-
mergeStrategy: "stack",
|
|
641
|
-
activeAt: null,
|
|
642
|
-
expiresAt: null,
|
|
643
|
-
},
|
|
644
|
-
]);
|
|
645
|
-
return `✅ 已为玩家 ${playerId} 发放 ${amount} ${this.config.currencyName}`;
|
|
646
|
-
}
|
|
647
|
-
async staffRedeemCode(sender, rawCode, rawPresentId) {
|
|
648
|
-
const code = cleanText(rawCode);
|
|
649
|
-
const presentId = cleanText(rawPresentId);
|
|
650
|
-
if (!code || !presentId)
|
|
651
|
-
return commandUsage("staff_redeem_code");
|
|
652
|
-
const denied = this.staffDenied(sender);
|
|
653
|
-
if (denied)
|
|
654
|
-
return denied;
|
|
655
|
-
const result = (await this.client.createStaffRedeemCode?.({
|
|
656
|
-
code,
|
|
657
|
-
presentId,
|
|
658
|
-
activeAt: null,
|
|
659
|
-
expiresAt: null,
|
|
660
|
-
maxUseCount: 1,
|
|
661
|
-
}));
|
|
662
|
-
const redeemCode = result?.redeemCode?.code ?? code;
|
|
663
|
-
return `成功生成 1 个兑换码:\n${redeemCode}`;
|
|
664
|
-
}
|
|
665
|
-
async staffCheckout(sender, rawPlayerId) {
|
|
666
|
-
const playerId = cleanText(rawPlayerId);
|
|
667
|
-
if (!playerId)
|
|
668
|
-
return commandUsage("staff_checkout");
|
|
669
|
-
const denied = this.staffDenied(sender);
|
|
670
|
-
if (denied)
|
|
671
|
-
return denied;
|
|
672
|
-
const result = (await this.client.staffCheckout?.(playerId));
|
|
673
|
-
const settlement = result?.settlement ?? {};
|
|
674
|
-
return `\n✅ 已为用户 ${playerId} 退场\n消费: ${formatNumber(settlement?.total ?? 0)} ${this.config.currencyName}`;
|
|
675
|
-
}
|
|
676
614
|
async autoPowerOffLoop() {
|
|
677
615
|
const interval = this.config.powerOffInterval ?? 0;
|
|
678
616
|
if (interval <= 0)
|
|
@@ -712,6 +650,64 @@ class PrismKoishiService {
|
|
|
712
650
|
return null;
|
|
713
651
|
}
|
|
714
652
|
}
|
|
653
|
+
async displayNameForPlayer(player) {
|
|
654
|
+
let identitySubject;
|
|
655
|
+
for (const session of player.sessions) {
|
|
656
|
+
const subject = findSubjectForSession(session, this.config.provider);
|
|
657
|
+
if (!subject)
|
|
658
|
+
continue;
|
|
659
|
+
identitySubject ??= subject;
|
|
660
|
+
const platformName = await this.resolvePlatformName(subject);
|
|
661
|
+
if (platformName)
|
|
662
|
+
return platformName;
|
|
663
|
+
}
|
|
664
|
+
return player.sessions.find((session) => session.playerDisplayName)?.playerDisplayName
|
|
665
|
+
|| identitySubject
|
|
666
|
+
|| player.displayName
|
|
667
|
+
|| player.playerId
|
|
668
|
+
|| "未知玩家";
|
|
669
|
+
}
|
|
670
|
+
async buildPlayerGroups(players, tableByLabel) {
|
|
671
|
+
const groups = {
|
|
672
|
+
music: [],
|
|
673
|
+
mahjong: uniqueMahjongConfigs(this.mahjongTableConfigs()).map((table) => ({ table, players: [] })),
|
|
674
|
+
};
|
|
675
|
+
const groupForTable = new Map(groups.mahjong.map((group) => [group.table.tableId, group]));
|
|
676
|
+
for (const player of players.values()) {
|
|
677
|
+
player.displayName = await this.displayNameForPlayer(player);
|
|
678
|
+
const table = player.sessions
|
|
679
|
+
.map((session) => tableByLabel.get(session.label ?? ""))
|
|
680
|
+
.find((value) => Boolean(value));
|
|
681
|
+
if (table) {
|
|
682
|
+
groupForTable.get(table.tableId)?.players.push(player);
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
groups.music.push(player);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return groups;
|
|
689
|
+
}
|
|
690
|
+
mergeWaitingSeats(groups) {
|
|
691
|
+
for (const [tableId, state] of this.mahjongTables) {
|
|
692
|
+
const group = groups.mahjong.find((candidate) => candidate.table.tableId === tableId);
|
|
693
|
+
if (!group)
|
|
694
|
+
continue;
|
|
695
|
+
for (const seat of state.waiting) {
|
|
696
|
+
if (group.players.some((player) => player.playerId === seat.playerId))
|
|
697
|
+
continue;
|
|
698
|
+
const existing = [groups.music, ...groups.mahjong.map((candidate) => candidate.players)]
|
|
699
|
+
.flat()
|
|
700
|
+
.find((player) => player.playerId === seat.playerId);
|
|
701
|
+
groups.music = groups.music.filter((player) => player.playerId !== seat.playerId);
|
|
702
|
+
for (const candidate of groups.mahjong) {
|
|
703
|
+
if (candidate !== group) {
|
|
704
|
+
candidate.players = candidate.players.filter((player) => player.playerId !== seat.playerId);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
group.players.push(existing ?? { playerId: seat.playerId, sessions: [], displayName: seat.displayName });
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
715
711
|
async resolvePlayerDisplay(sender, playerId) {
|
|
716
712
|
if (!sender)
|
|
717
713
|
return playerId || "未知玩家";
|
|
@@ -736,6 +732,10 @@ class PrismKoishiService {
|
|
|
736
732
|
async resolvePlayer(sender) {
|
|
737
733
|
return (await this.client.resolveOrRegisterIdentity(this.identity(sender)));
|
|
738
734
|
}
|
|
735
|
+
hasEntrySession(playerId, sessions) {
|
|
736
|
+
const label = this.config.loginSessionLabel?.trim();
|
|
737
|
+
return sessions.some((session) => session.playerId === playerId && (label ? session.label === label : true));
|
|
738
|
+
}
|
|
739
739
|
identity(sender) {
|
|
740
740
|
return {
|
|
741
741
|
provider: this.config.provider,
|
|
@@ -744,6 +744,15 @@ class PrismKoishiService {
|
|
|
744
744
|
displayName: sender.name || `${this.config.provider.toUpperCase()} ${sender.id}`,
|
|
745
745
|
};
|
|
746
746
|
}
|
|
747
|
+
targetSender(actor, targetSubject) {
|
|
748
|
+
const subject = cleanText(targetSubject);
|
|
749
|
+
if (!subject)
|
|
750
|
+
return actor;
|
|
751
|
+
const denied = this.targetStaffDenied(actor);
|
|
752
|
+
if (denied)
|
|
753
|
+
return denied;
|
|
754
|
+
return { id: subject, name: subject };
|
|
755
|
+
}
|
|
747
756
|
loginSessionBody() {
|
|
748
757
|
const pricingConfigIds = (this.config.loginPricingConfigIds ?? [])
|
|
749
758
|
.map((id) => id.trim())
|
|
@@ -764,6 +773,14 @@ class PrismKoishiService {
|
|
|
764
773
|
return "权限不足";
|
|
765
774
|
return null;
|
|
766
775
|
}
|
|
776
|
+
targetStaffDenied(sender) {
|
|
777
|
+
if (!this.config.enableStaffCommands)
|
|
778
|
+
return "员工命令未启用";
|
|
779
|
+
const allowed = this.config.staffUserIds ?? [];
|
|
780
|
+
if (!allowed.includes(sender.id))
|
|
781
|
+
return "权限不足";
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
767
784
|
async formatCheckoutPreview(result, sender, title = "【结算账单】") {
|
|
768
785
|
if (result?.billing && result?.session) {
|
|
769
786
|
return formatLegacyBilling(result, this.config.currencyName);
|
|
@@ -796,6 +813,24 @@ class PrismKoishiService {
|
|
|
796
813
|
const lines = [];
|
|
797
814
|
lines.push(title);
|
|
798
815
|
lines.push(await this.resolvePlayerDisplay(sender, playerId));
|
|
816
|
+
let balance = 0;
|
|
817
|
+
let hasBalance = false;
|
|
818
|
+
for (const holding of assetHoldings) {
|
|
819
|
+
const code = String(holding?.assetCode ?? "").toLowerCase();
|
|
820
|
+
if (code.includes("paid") || code.includes("free") || code.includes("currency")) {
|
|
821
|
+
balance += toNumber(holding?.quantity ?? 0);
|
|
822
|
+
hasBalance = true;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const hasNonZeroSessionTotal = sessionPreviews.some((session) => toNumber(session?.total ?? 0) !== 0);
|
|
826
|
+
const hasNonZeroAdjustment = hasAdjustmentEntries(adjustments, sessionPreviews);
|
|
827
|
+
if (!hasNonZeroSessionTotal && !hasNonZeroAdjustment) {
|
|
828
|
+
lines.push("");
|
|
829
|
+
lines.push("本次未产生费用");
|
|
830
|
+
if (hasBalance)
|
|
831
|
+
lines.push(`余额:${formatNumber(balance)}${currency}`);
|
|
832
|
+
return lines.join("\n");
|
|
833
|
+
}
|
|
799
834
|
const validStarts = sessionPreviews.map((s) => parseDateTime(s?.startedAt)).filter(Boolean);
|
|
800
835
|
const validEnds = sessionPreviews.map((s) => sessionDisplayEnd(s, previewedAt)).filter(Boolean);
|
|
801
836
|
if (validStarts.length > 0) {
|
|
@@ -828,20 +863,9 @@ class PrismKoishiService {
|
|
|
828
863
|
lines.push(` └ ${adjLabel}:${formatNumber(amount)}${currency}`);
|
|
829
864
|
}
|
|
830
865
|
}
|
|
831
|
-
let balance = 0;
|
|
832
|
-
let hasBalance = false;
|
|
833
|
-
for (const holding of assetHoldings) {
|
|
834
|
-
const code = String(holding?.assetCode ?? "").toLowerCase();
|
|
835
|
-
if (code.includes("paid") || code.includes("free") || code.includes("currency")) {
|
|
836
|
-
balance += toNumber(holding?.quantity ?? 0);
|
|
837
|
-
hasBalance = true;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
866
|
lines.push("");
|
|
841
867
|
lines.push(`计费总价:${formatNumber(subtotal)}${currency}`);
|
|
842
|
-
|
|
843
|
-
|| sessionPreviews.some((sp) => (sp?.adjustments ?? []).some((adj) => toNumber(firstDefined(adj ?? {}, "amount", "saved", 0)) !== 0));
|
|
844
|
-
if (hasDiscount)
|
|
868
|
+
if (hasNonZeroAdjustment)
|
|
845
869
|
lines.push(`优惠后价格:${formatNumber(total)}${currency}`);
|
|
846
870
|
if (hasBalance)
|
|
847
871
|
lines.push(`扣款后余额:${formatNumber(balance)}${currency}`);
|
|
@@ -924,6 +948,45 @@ function parseMahjongTables(value, labelPrefix) {
|
|
|
924
948
|
}
|
|
925
949
|
return tables;
|
|
926
950
|
}
|
|
951
|
+
function uniqueMahjongConfigs(tables) {
|
|
952
|
+
return [...new Map([...tables.values()].map((table) => [table.tableId, table])).values()];
|
|
953
|
+
}
|
|
954
|
+
function mahjongSessionLabel(table, labelPrefix) {
|
|
955
|
+
return table.displayName || `${labelPrefix} ${table.tableId}`;
|
|
956
|
+
}
|
|
957
|
+
function groupSessionsByPlayer(sessions) {
|
|
958
|
+
const players = new Map();
|
|
959
|
+
for (const session of sessions) {
|
|
960
|
+
const playerId = session.playerId || findSubjectForSession(session, "") || "";
|
|
961
|
+
if (!playerId)
|
|
962
|
+
continue;
|
|
963
|
+
const player = players.get(playerId) ?? { playerId, sessions: [] };
|
|
964
|
+
player.sessions.push(session);
|
|
965
|
+
players.set(playerId, player);
|
|
966
|
+
}
|
|
967
|
+
return players;
|
|
968
|
+
}
|
|
969
|
+
function formatPlayerGroups(groups, tableSize, mahjongLabelPrefix) {
|
|
970
|
+
const populatedMahjongGroups = groups.mahjong.filter((group) => group.players.length > 0);
|
|
971
|
+
const total = groups.music.length + populatedMahjongGroups.reduce((sum, group) => sum + group.players.length, 0);
|
|
972
|
+
if (total === 0)
|
|
973
|
+
return "🫥 窝里目前没有玩家呢";
|
|
974
|
+
const lines = [`[总计 ${total} 人]`];
|
|
975
|
+
if (groups.music.length > 0) {
|
|
976
|
+
lines.push(`🎵 音乐游戏 ( ${groups.music.length}人 ):\n${formatPlayerNames(groups.music)}`);
|
|
977
|
+
}
|
|
978
|
+
for (const group of populatedMahjongGroups) {
|
|
979
|
+
lines.push(`${formatMahjongTableLabel(group.table, mahjongLabelPrefix)} ( ${group.players.length}/${tableSize} ):\n${formatPlayerNames(group.players)}`);
|
|
980
|
+
}
|
|
981
|
+
return lines.join("\n");
|
|
982
|
+
}
|
|
983
|
+
function formatMahjongTableLabel(table, labelPrefix) {
|
|
984
|
+
const label = mahjongSessionLabel(table, labelPrefix);
|
|
985
|
+
return table.displayName ? label : `🀄️ ${label}`;
|
|
986
|
+
}
|
|
987
|
+
function formatPlayerNames(players) {
|
|
988
|
+
return players.map((player) => `- ${player.displayName || player.playerId || "未知玩家"}`).join(", ");
|
|
989
|
+
}
|
|
927
990
|
function splitOnce(text, sep) {
|
|
928
991
|
const idx = text.indexOf(sep);
|
|
929
992
|
if (idx === -1)
|
|
@@ -967,6 +1030,9 @@ function toNumber(value) {
|
|
|
967
1030
|
return asFloat;
|
|
968
1031
|
return 0;
|
|
969
1032
|
}
|
|
1033
|
+
function hasAdjustmentEntries(adjustments, sessionPreviews) {
|
|
1034
|
+
return adjustments.some((adjustment) => toNumber(firstDefined(adjustment ?? {}, "amount", "saved", 0)) !== 0) || sessionPreviews.some((session) => (session?.adjustments ?? []).some((adjustment) => toNumber(firstDefined(adjustment ?? {}, "amount", "saved", 0)) !== 0));
|
|
1035
|
+
}
|
|
970
1036
|
function formatNumber(value) {
|
|
971
1037
|
if (typeof value === "boolean")
|
|
972
1038
|
return value ? "1" : "0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-prism",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "PRiSM Next 计费与设备管理系统的 Koishi 机器人集成插件",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -48,4 +48,4 @@
|
|
|
48
48
|
"test": "bun test test",
|
|
49
49
|
"typecheck": "tsc -p tsconfig.json"
|
|
50
50
|
}
|
|
51
|
-
}
|
|
51
|
+
}
|