@hieuzest/koishi-plugin-mahjongpub 0.1.22 → 0.2.1
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/lib/api.d.ts +95 -17
- package/lib/index.d.ts +7 -4
- package/lib/index.js +324 -26
- package/lib/mahjongpub.d.ts +87 -0
- package/lib/manager.d.ts +47 -0
- package/package.json +6 -2
package/lib/api.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Context, Dict, Quester } from 'koishi';
|
|
2
2
|
export interface Team {
|
|
3
3
|
pw?: string;
|
|
4
|
-
tid:
|
|
5
|
-
cid?:
|
|
4
|
+
tid: number;
|
|
5
|
+
cid?: number;
|
|
6
6
|
name: string;
|
|
7
7
|
players: string[];
|
|
8
8
|
qq?: string[];
|
|
@@ -12,8 +12,8 @@ export declare class TeamAdmin implements Team {
|
|
|
12
12
|
options: {
|
|
13
13
|
endpoint: string;
|
|
14
14
|
};
|
|
15
|
-
tid:
|
|
16
|
-
cid:
|
|
15
|
+
tid: number;
|
|
16
|
+
cid: number;
|
|
17
17
|
name: string;
|
|
18
18
|
description: string;
|
|
19
19
|
players: string[];
|
|
@@ -28,16 +28,16 @@ export declare class TeamAdmin implements Team {
|
|
|
28
28
|
}
|
|
29
29
|
declare namespace TeamAdmin {
|
|
30
30
|
interface Payload {
|
|
31
|
-
tid:
|
|
31
|
+
tid: number;
|
|
32
32
|
t_name: string;
|
|
33
33
|
t_player?: string;
|
|
34
34
|
t_sub?: string;
|
|
35
35
|
t_pw: string;
|
|
36
36
|
t_join_time?: string;
|
|
37
37
|
t_join_ip?: string;
|
|
38
|
-
t_type?:
|
|
38
|
+
t_type?: number;
|
|
39
39
|
t_ps?: string;
|
|
40
|
-
cid:
|
|
40
|
+
cid: number;
|
|
41
41
|
qq?: string;
|
|
42
42
|
ls_ip?: string;
|
|
43
43
|
ls_time?: string;
|
|
@@ -45,19 +45,22 @@ declare namespace TeamAdmin {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
export declare class ContestAdmin {
|
|
48
|
-
cid:
|
|
48
|
+
cid: number;
|
|
49
49
|
pw: string;
|
|
50
50
|
options: {
|
|
51
51
|
endpoint: string;
|
|
52
52
|
};
|
|
53
53
|
name: string;
|
|
54
54
|
teams: Dict<Team>;
|
|
55
|
+
round: number;
|
|
55
56
|
http: Quester;
|
|
56
57
|
raw?: ContestAdmin.Payload;
|
|
57
|
-
constructor(ctx: Context, cid:
|
|
58
|
+
constructor(ctx: Context, cid: number, pw: string, options: {
|
|
58
59
|
endpoint: string;
|
|
59
60
|
});
|
|
60
61
|
read(force?: boolean): Promise<void>;
|
|
62
|
+
getRounds(): Promise<Record<number, Record<number, Contest.Round>>>;
|
|
63
|
+
getRecords(round: number): Promise<Contest.Records>;
|
|
61
64
|
getRoundRecord(round: number, cls: number): Promise<ContestAdmin.MatchRoundRecord>;
|
|
62
65
|
updateRoundRecord(record: ContestAdmin.MatchRoundRecord): Promise<string>;
|
|
63
66
|
}
|
|
@@ -67,13 +70,13 @@ declare namespace ContestAdmin {
|
|
|
67
70
|
c_name: string;
|
|
68
71
|
c_gonggao: string;
|
|
69
72
|
c_pad: string;
|
|
70
|
-
c_round:
|
|
71
|
-
c_s_po:
|
|
72
|
-
c_sub_t:
|
|
73
|
-
cid:
|
|
74
|
-
r_type:
|
|
75
|
-
t_max:
|
|
76
|
-
t_sub:
|
|
73
|
+
c_round: number;
|
|
74
|
+
c_s_po: number;
|
|
75
|
+
c_sub_t: number;
|
|
76
|
+
cid: number;
|
|
77
|
+
r_type: number;
|
|
78
|
+
t_max: number;
|
|
79
|
+
t_sub: number;
|
|
77
80
|
t_type: string;
|
|
78
81
|
};
|
|
79
82
|
c_team: TeamAdmin.Payload[];
|
|
@@ -88,10 +91,85 @@ declare namespace ContestAdmin {
|
|
|
88
91
|
logurl: string;
|
|
89
92
|
}
|
|
90
93
|
interface MatchRoundRecord {
|
|
91
|
-
cid:
|
|
94
|
+
cid: number;
|
|
92
95
|
cls: number;
|
|
93
96
|
rnd: number;
|
|
94
97
|
matches: MatchRecord[];
|
|
95
98
|
}
|
|
99
|
+
interface RoundPayload {
|
|
100
|
+
clsmark: string;
|
|
101
|
+
code: string;
|
|
102
|
+
rid: number;
|
|
103
|
+
round: number;
|
|
104
|
+
t_class: number;
|
|
105
|
+
tid1: number;
|
|
106
|
+
tid2: number;
|
|
107
|
+
tid3: number;
|
|
108
|
+
tid4: number;
|
|
109
|
+
}
|
|
110
|
+
interface Round {
|
|
111
|
+
rid: number;
|
|
112
|
+
round: number;
|
|
113
|
+
cls: number;
|
|
114
|
+
code: string;
|
|
115
|
+
tids: number[];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
declare namespace Contest {
|
|
119
|
+
interface RoundPayload {
|
|
120
|
+
clsmark: string;
|
|
121
|
+
code: string;
|
|
122
|
+
rid: number;
|
|
123
|
+
round: number;
|
|
124
|
+
t_class: number;
|
|
125
|
+
tid1: number;
|
|
126
|
+
tid2: number;
|
|
127
|
+
tid3: number;
|
|
128
|
+
tid4: number;
|
|
129
|
+
}
|
|
130
|
+
interface Round {
|
|
131
|
+
rid: number;
|
|
132
|
+
round: number;
|
|
133
|
+
cls: number;
|
|
134
|
+
code: string;
|
|
135
|
+
tids: number[];
|
|
136
|
+
}
|
|
137
|
+
interface MatchPayload {
|
|
138
|
+
inx: number;
|
|
139
|
+
cid: number;
|
|
140
|
+
rowi: number;
|
|
141
|
+
name1: string;
|
|
142
|
+
num1: number;
|
|
143
|
+
name2: string;
|
|
144
|
+
num2: number;
|
|
145
|
+
name3: string;
|
|
146
|
+
num3: number;
|
|
147
|
+
name4: string;
|
|
148
|
+
num4: number;
|
|
149
|
+
url: string;
|
|
150
|
+
pint1: number;
|
|
151
|
+
pint2: number;
|
|
152
|
+
pint3: number;
|
|
153
|
+
pint4: number;
|
|
154
|
+
rid: number;
|
|
155
|
+
round: number;
|
|
156
|
+
i_time: string;
|
|
157
|
+
}
|
|
158
|
+
interface Match {
|
|
159
|
+
inx: number;
|
|
160
|
+
cid: number;
|
|
161
|
+
rowi: number;
|
|
162
|
+
players: {
|
|
163
|
+
name: string;
|
|
164
|
+
num: number;
|
|
165
|
+
point: number;
|
|
166
|
+
}[];
|
|
167
|
+
url: string;
|
|
168
|
+
rid: number;
|
|
169
|
+
round: number;
|
|
170
|
+
time: string;
|
|
171
|
+
}
|
|
172
|
+
type RecordsPayload = Record<number, MatchPayload[]>;
|
|
173
|
+
type Records = Record<number, Match[]>;
|
|
96
174
|
}
|
|
97
175
|
export {};
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { Context, Dict, Schema } from 'koishi';
|
|
1
|
+
import { Context, Dict, Schema, Service } from 'koishi';
|
|
2
2
|
import { ContestAdmin, TeamAdmin } from './api';
|
|
3
3
|
declare module 'koishi' {
|
|
4
|
+
interface Context {
|
|
5
|
+
mahjongpub: MahjongPub;
|
|
6
|
+
}
|
|
4
7
|
interface User {
|
|
5
8
|
'mahjongpub/bind-team': string;
|
|
6
9
|
'mahjongpub/bind-teams': Dict<string>;
|
|
@@ -17,9 +20,9 @@ declare module '@koishijs/cache' {
|
|
|
17
20
|
teams: TeamAdmin;
|
|
18
21
|
}
|
|
19
22
|
}
|
|
20
|
-
export declare class MahjongPub {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
export declare class MahjongPub extends Service {
|
|
24
|
+
protected ctx: Context;
|
|
25
|
+
config: MahjongPub.Config;
|
|
23
26
|
teams: Dict<TeamAdmin>;
|
|
24
27
|
contests: Dict<ContestAdmin>;
|
|
25
28
|
constructor(ctx: Context, config: MahjongPub.Config);
|
package/lib/index.js
CHANGED
|
@@ -23,18 +23,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
23
23
|
// src/locales/zh-CN.yml
|
|
24
24
|
var require_zh_CN = __commonJS({
|
|
25
25
|
"src/locales/zh-CN.yml"(exports2, module2) {
|
|
26
|
-
module2.exports = { commands: { "mahjongpub.team.bind": { description: "绑定队伍", messages: { failed: "绑定失败", success: "成功: [{cid}:{tid}] {name}" } }, "mahjongpub.team.unbind": { description: "解除绑定", messages: { notbind: "未绑定", success: "成功" } }, "mahjongpub.team.password": { description: "查看密码", messages: { notbind: "未绑定", output: "密码: {0}" } }, "mahjongpub.team.stats": { description: "查看队伍信息", messages: { notbind: "未绑定", failed: "失败", output: "- [{cid}:{tid}] {name}\n{players.map((p, i) => '' + (i+1) + ': ' + p).join('\\n')}\n" } }, "mahjongpub.team.desc": { description: "队伍简介", messages: { notbind: "未绑定", failed: "失败", output: "- [{cid}:{tid}] {name}\n{description}\n" } }, "mahjongpub.team.logo": { description: "队伍头像", messages: { notbind: "未绑定", failed: "失败" } }, "mahjongpub.team.add": { description: "添加队员", messages: { notbind: "未绑定", failed: "失败", success: "- [{cid}:{tid}] {name}\n{players.map((p, i) => '' + (i+1) + ': ' + p).join('\\n')}\n" } }, "mahjongpub.team.remove": { description: "删除队员", messages: { notbind: "未绑定", failed: "失败", success: "- [{cid}:{tid}] {name}\n{players.map((p, i) => '' + (i+1) + ': ' + p).join('\\n')}\n" } }, "mahjongpub.team.swap": { description: "交换队员", messages: { notbind: "未绑定", failed: "失败", invalid: "参数错误", "wide-range": "范围过大", success: "- [{cid}:{tid}] {name}\n{players.map((p, i) => '' + (i+1) + ': ' + p).join('\\n')}\n" } }, "mahjongpub.contest.bind": { description: "绑定比赛", messages: { failed: "绑定失败", success: "成功: [{cid}] {name}" } }, "mahjongpub.contest.record.get": { description: "查询比赛记录", messages: { failed: "查询失败" } }, "mahjongpub.contest.record.update": { description: "修改比赛记录", messages: { failed: "修改失败" } }, "mahjongpub.database.record.get": { description: "查询比赛记录", messages: { failed: "查询失败" } }, "mahjongpub.database.record.update": { description: "修改比赛记录", messages: { success: "修改成功", failed: "修改失败", unmodified: "未修改" } } } };
|
|
26
|
+
module2.exports = { commands: { "mahjongpub.team.bind": { description: "绑定队伍", messages: { failed: "绑定失败", success: "成功: [{cid}:{tid}] {name}" } }, "mahjongpub.team.unbind": { description: "解除绑定", messages: { notbind: "未绑定", success: "成功" } }, "mahjongpub.team.password": { description: "查看密码", messages: { notbind: "未绑定", output: "密码: {0}" } }, "mahjongpub.team.stats": { description: "查看队伍信息", messages: { notbind: "未绑定", failed: "失败", output: "- [{cid}:{tid}] {name}\n{players.map((p, i) => '' + (i+1) + ': ' + p).join('\\n')}\n" } }, "mahjongpub.team.desc": { description: "队伍简介", messages: { notbind: "未绑定", failed: "失败", output: "- [{cid}:{tid}] {name}\n{description}\n" } }, "mahjongpub.team.logo": { description: "队伍头像", messages: { notbind: "未绑定", failed: "失败" } }, "mahjongpub.team.add": { description: "添加队员", messages: { notbind: "未绑定", failed: "失败", success: "- [{cid}:{tid}] {name}\n{players.map((p, i) => '' + (i+1) + ': ' + p).join('\\n')}\n" } }, "mahjongpub.team.remove": { description: "删除队员", messages: { notbind: "未绑定", failed: "失败", success: "- [{cid}:{tid}] {name}\n{players.map((p, i) => '' + (i+1) + ': ' + p).join('\\n')}\n" } }, "mahjongpub.team.swap": { description: "交换队员", messages: { notbind: "未绑定", failed: "失败", invalid: "参数错误", "wide-range": "范围过大", success: "- [{cid}:{tid}] {name}\n{players.map((p, i) => '' + (i+1) + ': ' + p).join('\\n')}\n" } }, "mahjongpub.contest.bind": { description: "绑定比赛", messages: { failed: "绑定失败", success: "成功: [{cid}] {name}" } }, "mahjongpub.contest.password": { description: "查看密码", messages: { failed: "未绑定", success: "[{0}] {1}\n" } }, "mahjongpub.contest.record.get": { description: "查询比赛记录", messages: { failed: "查询失败" } }, "mahjongpub.contest.record.update": { description: "修改比赛记录", messages: { failed: "修改失败" } }, "mahjongpub.database.record.get": { description: "查询比赛记录", messages: { failed: "查询失败" } }, "mahjongpub.database.record.update": { description: "修改比赛记录", messages: { success: "修改成功", failed: "修改失败", unmodified: "未修改" } } } };
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
// src/index.ts
|
|
31
|
-
var
|
|
32
|
-
__export(
|
|
33
|
-
MahjongPub: () =>
|
|
34
|
-
default: () =>
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
MahjongPub: () => MahjongPub2,
|
|
34
|
+
default: () => index_default
|
|
35
35
|
});
|
|
36
|
-
module.exports = __toCommonJS(
|
|
37
|
-
var
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
var import_koishi3 = require("koishi");
|
|
38
38
|
|
|
39
39
|
// src/api.ts
|
|
40
40
|
var TeamAdmin = class {
|
|
@@ -79,7 +79,7 @@ var TeamAdmin = class {
|
|
|
79
79
|
posttype: "update",
|
|
80
80
|
t_pw: this.pw,
|
|
81
81
|
t_ps: this.description,
|
|
82
|
-
cid: this.cid,
|
|
82
|
+
cid: String(this.cid),
|
|
83
83
|
linkurl: this.img,
|
|
84
84
|
team_type: this.ready ? "1" : "0",
|
|
85
85
|
postdata: this.players.join("\n")
|
|
@@ -103,13 +103,14 @@ var ContestAdmin = class {
|
|
|
103
103
|
}
|
|
104
104
|
name;
|
|
105
105
|
teams;
|
|
106
|
+
round;
|
|
106
107
|
http;
|
|
107
108
|
raw;
|
|
108
109
|
async read(force = false) {
|
|
109
110
|
if (this.teams && !force) return;
|
|
110
111
|
const payload = {
|
|
111
112
|
posttype: "login",
|
|
112
|
-
cid: this.cid,
|
|
113
|
+
cid: String(this.cid),
|
|
113
114
|
c_pw: this.pw
|
|
114
115
|
};
|
|
115
116
|
const form = new FormData();
|
|
@@ -120,6 +121,7 @@ var ContestAdmin = class {
|
|
|
120
121
|
if (!data?.c_admin?.cid) throw new Error("failed to read contest info");
|
|
121
122
|
this.raw = data;
|
|
122
123
|
this.name = data.c_admin.c_name;
|
|
124
|
+
this.round = data.c_admin.c_round;
|
|
123
125
|
this.teams = Object.fromEntries(data.c_team.map((team) => [team.tid, {
|
|
124
126
|
tid: team.tid,
|
|
125
127
|
cid: this.cid,
|
|
@@ -129,10 +131,58 @@ var ContestAdmin = class {
|
|
|
129
131
|
pw: team.t_pw
|
|
130
132
|
}]));
|
|
131
133
|
}
|
|
134
|
+
async getRounds() {
|
|
135
|
+
const data = await this.http.get("/api/data.php", {
|
|
136
|
+
params: {
|
|
137
|
+
t: "class",
|
|
138
|
+
cid: this.cid
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
const res = {};
|
|
142
|
+
data.forEach((round) => {
|
|
143
|
+
(res[round.round] ??= {})[round.t_class] = {
|
|
144
|
+
rid: round.rid,
|
|
145
|
+
round: round.round,
|
|
146
|
+
cls: round.t_class,
|
|
147
|
+
code: round.code,
|
|
148
|
+
tids: [round.tid1, round.tid2, round.tid3, round.tid4].filter(Boolean)
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
return res;
|
|
152
|
+
}
|
|
153
|
+
async getRecords(round) {
|
|
154
|
+
const data = await this.http.get("/api/data.php", {
|
|
155
|
+
params: {
|
|
156
|
+
t: "c_data",
|
|
157
|
+
cid: this.cid,
|
|
158
|
+
r: round
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
const res = {};
|
|
162
|
+
Object.entries(data).forEach(([rid, rs]) => {
|
|
163
|
+
if (!rs.length) return;
|
|
164
|
+
res[rid] = rs.map((x) => ({
|
|
165
|
+
inx: x.inx,
|
|
166
|
+
cid: x.cid,
|
|
167
|
+
rowi: x.rowi,
|
|
168
|
+
players: [
|
|
169
|
+
{ name: x.name1, num: x.num1, point: x.pint1 },
|
|
170
|
+
{ name: x.name2, num: x.num2, point: x.pint2 },
|
|
171
|
+
{ name: x.name3, num: x.num3, point: x.pint3 },
|
|
172
|
+
{ name: x.name4, num: x.num4, point: x.pint4 }
|
|
173
|
+
],
|
|
174
|
+
url: x.url,
|
|
175
|
+
rid: x.rid,
|
|
176
|
+
round: x.round,
|
|
177
|
+
time: x.i_time
|
|
178
|
+
}));
|
|
179
|
+
});
|
|
180
|
+
return res;
|
|
181
|
+
}
|
|
132
182
|
async getRoundRecord(round, cls) {
|
|
133
183
|
const payload = {
|
|
134
184
|
type: "login",
|
|
135
|
-
cid: this.cid,
|
|
185
|
+
cid: String(this.cid),
|
|
136
186
|
cls: String(cls),
|
|
137
187
|
rnd: String(round)
|
|
138
188
|
};
|
|
@@ -166,7 +216,7 @@ var ContestAdmin = class {
|
|
|
166
216
|
async updateRoundRecord(record) {
|
|
167
217
|
const payload = {
|
|
168
218
|
type: "update",
|
|
169
|
-
cid: this.cid,
|
|
219
|
+
cid: String(this.cid),
|
|
170
220
|
cls: String(record.cls),
|
|
171
221
|
rnd: String(record.rnd),
|
|
172
222
|
pw: this.pw
|
|
@@ -189,6 +239,222 @@ var ContestAdmin = class {
|
|
|
189
239
|
}
|
|
190
240
|
};
|
|
191
241
|
|
|
242
|
+
// src/manager.ts
|
|
243
|
+
var import_koishi2 = require("koishi");
|
|
244
|
+
var import_lobby = require("@hieuzest/koishi-plugin-riichi-city/lobby");
|
|
245
|
+
|
|
246
|
+
// src/mahjongpub.ts
|
|
247
|
+
var import_koishi = require("koishi");
|
|
248
|
+
var import_cachetools = require("@hieuzest/cachetools");
|
|
249
|
+
var MahjongPub = class _MahjongPub {
|
|
250
|
+
constructor(ctx) {
|
|
251
|
+
this.ctx = ctx;
|
|
252
|
+
this.http = ctx.http.extend({});
|
|
253
|
+
}
|
|
254
|
+
static {
|
|
255
|
+
__name(this, "MahjongPub");
|
|
256
|
+
}
|
|
257
|
+
serverUri = "https://cdn.r-mj.com";
|
|
258
|
+
http;
|
|
259
|
+
async getContest(cid) {
|
|
260
|
+
return await this.http.get(`${this.serverUri}/api/data.php`, {
|
|
261
|
+
params: {
|
|
262
|
+
t: "admin",
|
|
263
|
+
cid
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
async getTeams(cid) {
|
|
268
|
+
return this.http.get(`${this.serverUri}/api/data.php`, {
|
|
269
|
+
params: {
|
|
270
|
+
t: "team",
|
|
271
|
+
cid
|
|
272
|
+
}
|
|
273
|
+
}).then((teams) => (0, import_koishi.mapValues)(teams, (team) => ({
|
|
274
|
+
...team,
|
|
275
|
+
players: [...team.t_player?.split(/\s+/) ?? [], ...team.t_sub?.split(/\s+/) ?? []]
|
|
276
|
+
})));
|
|
277
|
+
}
|
|
278
|
+
async getRounds(cid) {
|
|
279
|
+
return Object.fromEntries(await this.http.get(`${this.serverUri}/api/data.php`, {
|
|
280
|
+
params: {
|
|
281
|
+
t: "class",
|
|
282
|
+
cid
|
|
283
|
+
}
|
|
284
|
+
}).then((rounds) => rounds.map((round) => [round.rid, {
|
|
285
|
+
...round,
|
|
286
|
+
tids: [round.tid1, round.tid2, round.tid3, round.tid4]
|
|
287
|
+
}])));
|
|
288
|
+
}
|
|
289
|
+
async getMatches(cid, round) {
|
|
290
|
+
return await this.http.get(`${this.serverUri}/api/data.php`, {
|
|
291
|
+
params: {
|
|
292
|
+
t: "c_data",
|
|
293
|
+
cid,
|
|
294
|
+
r: round
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
async getAllMatches(cid) {
|
|
299
|
+
const contest = await this.getContest(cid);
|
|
300
|
+
return await this.http.get(`${this.serverUri}/api/data.php`, {
|
|
301
|
+
params: {
|
|
302
|
+
t: "multi_log",
|
|
303
|
+
cid,
|
|
304
|
+
r: [...Array(contest.c_round).keys()].map((x) => x + 1).map((x) => x.toString()).join(",")
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
static cacheOptions = {
|
|
309
|
+
"getContest": {
|
|
310
|
+
maxAge: 120
|
|
311
|
+
},
|
|
312
|
+
"getRounds": {
|
|
313
|
+
maxAge: 60
|
|
314
|
+
},
|
|
315
|
+
"getTeams": {
|
|
316
|
+
maxAge: 30
|
|
317
|
+
},
|
|
318
|
+
"getMatches": {
|
|
319
|
+
maxAge: 120
|
|
320
|
+
},
|
|
321
|
+
"getAllMatches": {
|
|
322
|
+
maxAge: 300
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
static new(ctx) {
|
|
326
|
+
return (0, import_cachetools.ttlcachedClass)(new _MahjongPub(ctx), _MahjongPub.cacheOptions);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// src/manager.ts
|
|
331
|
+
var ContestExtra = class {
|
|
332
|
+
constructor(ctx) {
|
|
333
|
+
this.ctx = ctx;
|
|
334
|
+
this.subscribers = /* @__PURE__ */ new Set();
|
|
335
|
+
this.stopCls = /* @__PURE__ */ new Set();
|
|
336
|
+
}
|
|
337
|
+
static {
|
|
338
|
+
__name(this, "ContestExtra");
|
|
339
|
+
}
|
|
340
|
+
subscribers;
|
|
341
|
+
stopCls;
|
|
342
|
+
async broadcast(msg) {
|
|
343
|
+
this.subscribers.forEach((channel) => this.ctx.sendMessage(channel, msg).catch(import_koishi2.noop));
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
var ContestManager = class {
|
|
347
|
+
constructor(ctx, config) {
|
|
348
|
+
this.ctx = ctx;
|
|
349
|
+
this.config = config;
|
|
350
|
+
this.mahjongpub = MahjongPub.new(ctx);
|
|
351
|
+
ctx.command("contest.manager", { authority: 3 }).action(import_koishi2.noop);
|
|
352
|
+
ctx.command("contest.manager.init").channelFields(["mahjongpub/bind-contest"]).option("clear", "-c").action(async ({ session, options }) => {
|
|
353
|
+
const cid = +(session.channel["mahjongpub/bind-contest"] || 0);
|
|
354
|
+
if (!cid) return "Unauthorized.";
|
|
355
|
+
this.extra[cid] ??= new ContestExtra(ctx);
|
|
356
|
+
this.extra[cid].subscribers.add(session.cid);
|
|
357
|
+
if (options.clear) this.extra[cid].stopCls.clear();
|
|
358
|
+
return "Finished.";
|
|
359
|
+
});
|
|
360
|
+
ctx.command("contest.manager.start [cls:number]").channelFields(["mahjongpub/bind-contest"]).action(async ({ session }, cls) => {
|
|
361
|
+
const cid = +(session.channel["mahjongpub/bind-contest"] || 0);
|
|
362
|
+
if (!cid) return "Unauthorized.";
|
|
363
|
+
this.extra[cid] ??= new ContestExtra(ctx);
|
|
364
|
+
this.extra[cid].subscribers.add(session.cid);
|
|
365
|
+
if (cls) {
|
|
366
|
+
this.extra[cid].stopCls.delete(cls);
|
|
367
|
+
await this.startMatch(cid, cls);
|
|
368
|
+
} else {
|
|
369
|
+
this.extra[cid].stopCls.clear();
|
|
370
|
+
const contest = await this.mahjongpub.getContest(cid);
|
|
371
|
+
const rounds = await this.mahjongpub.getRounds(cid);
|
|
372
|
+
for (const r of Object.values(rounds)) {
|
|
373
|
+
if (r.round !== contest.c_round) continue;
|
|
374
|
+
await this.startMatch(cid, r.t_class);
|
|
375
|
+
await (0, import_koishi2.sleep)(1e3);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
ctx.command("contest.manager.stop [cls:number]").channelFields(["mahjongpub/bind-contest"]).action(async ({ session }, cls) => {
|
|
380
|
+
const cid = +(session.channel["mahjongpub/bind-contest"] || 0);
|
|
381
|
+
if (!cid) return "Unauthorized/";
|
|
382
|
+
this.extra[cid] ??= new ContestExtra(ctx);
|
|
383
|
+
if (cls) {
|
|
384
|
+
this.extra[cid].stopCls.add(cls);
|
|
385
|
+
} else {
|
|
386
|
+
const contest = await this.mahjongpub.getContest(cid);
|
|
387
|
+
const rounds = await this.mahjongpub.getRounds(cid);
|
|
388
|
+
for (const r of Object.values(rounds)) {
|
|
389
|
+
if (r.round !== contest.c_round) continue;
|
|
390
|
+
this.extra[cid].stopCls.add(r.t_class);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return "Finished.";
|
|
394
|
+
});
|
|
395
|
+
ctx.server.post("/contest-manager/finished", async (kCtx) => {
|
|
396
|
+
if (config.token && kCtx.request.headers.authorization !== `Bearer ${config.token}`) {
|
|
397
|
+
kCtx.status = 403;
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
this.onMatchFinished(kCtx.request.body);
|
|
401
|
+
kCtx.status = 200;
|
|
402
|
+
return true;
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
static {
|
|
406
|
+
__name(this, "ContestManager");
|
|
407
|
+
}
|
|
408
|
+
static inject = ["server", "mahjong", "mahjong.database", "mahjongpub", "sendMessage"];
|
|
409
|
+
mahjongpub;
|
|
410
|
+
extra = /* @__PURE__ */ Object.create(null);
|
|
411
|
+
async startMatch(cid, cls, timeout, tag) {
|
|
412
|
+
tag ||= `${cls}组`;
|
|
413
|
+
if (this.extra[cid]?.stopCls.has(cls)) {
|
|
414
|
+
this.ctx.logger.info(`try starting match ${cid}-${cls}, but stopped`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
this.ctx.logger.info(`try starting match ${cid}-${cls}, timeout ${timeout}`);
|
|
418
|
+
const contest = await this.mahjongpub.getContest(cid);
|
|
419
|
+
const rounds = await this.mahjongpub.getRounds(cid);
|
|
420
|
+
const teams = await this.mahjongpub.getTeams(cid);
|
|
421
|
+
const [lastRecord] = await this.ctx.mahjong.database.db("scoreboard").collection("matches").find({
|
|
422
|
+
cid: `${cid}`,
|
|
423
|
+
round: +contest.c_round,
|
|
424
|
+
cls
|
|
425
|
+
}).sort({ rowi: -1 }).limit(1).toArray();
|
|
426
|
+
if (lastRecord && !lastRecord._finished) {
|
|
427
|
+
this.extra[cid]?.broadcast(`[${tag}] 发现进行中的比赛,已忽略`);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const rowi = lastRecord ? lastRecord.rowi + 1 : 0;
|
|
431
|
+
const round = Object.values(rounds).find((x) => x.round === contest.c_round && x.t_class === cls);
|
|
432
|
+
const players = round.tids.map((tid) => teams[tid].players[rowi]);
|
|
433
|
+
try {
|
|
434
|
+
await this.ctx["zx-dhs"].startMatch(+round.code, players.map((x, i) => import_lobby.Player.fromPattern(x, lastRecord ? lastRecord.results?.[i].num : 1e5)));
|
|
435
|
+
this.extra[cid]?.broadcast(`[${tag}] 成功:` + players.join(","));
|
|
436
|
+
} catch (e) {
|
|
437
|
+
if (e instanceof import_lobby.PlayerNotFoundError) {
|
|
438
|
+
this.extra[cid]?.broadcast(`[${tag}] ` + e.message + ` <timeout=${timeout}>`);
|
|
439
|
+
if (timeout < 600) this.ctx.setTimeout(() => this.startMatch(cid, cls, timeout + 30), 30 * 1e3);
|
|
440
|
+
} else {
|
|
441
|
+
throw e;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async onMatchFinished(record) {
|
|
446
|
+
this.extra[record.cid]?.broadcast(`对局完成:
|
|
447
|
+
` + record.results.map((x) => `${x.name} ${x.num}`).join("\n"));
|
|
448
|
+
await (0, import_koishi2.sleep)(2e3);
|
|
449
|
+
return this.startMatch(+record.cid, record.cls);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
((ContestManager2) => {
|
|
453
|
+
ContestManager2.Config = import_koishi2.Schema.object({
|
|
454
|
+
token: import_koishi2.Schema.string()
|
|
455
|
+
});
|
|
456
|
+
})(ContestManager || (ContestManager = {}));
|
|
457
|
+
|
|
192
458
|
// src/index.ts
|
|
193
459
|
function parsePlatform(target) {
|
|
194
460
|
const index = target.startsWith("sandbox:") ? target.lastIndexOf(":") : target.indexOf(":");
|
|
@@ -197,10 +463,12 @@ function parsePlatform(target) {
|
|
|
197
463
|
return [platform, id];
|
|
198
464
|
}
|
|
199
465
|
__name(parsePlatform, "parsePlatform");
|
|
200
|
-
var
|
|
466
|
+
var MahjongPub2 = class extends import_koishi3.Service {
|
|
201
467
|
constructor(ctx, config) {
|
|
468
|
+
super(ctx, "mahjongpub", true);
|
|
202
469
|
this.ctx = ctx;
|
|
203
470
|
this.config = config;
|
|
471
|
+
ctx.plugin(ContestManager);
|
|
204
472
|
ctx.i18n.define("zh-CN", require_zh_CN());
|
|
205
473
|
ctx.model.extend("user", {
|
|
206
474
|
"mahjongpub/bind-team": "string",
|
|
@@ -277,7 +545,7 @@ var MahjongPub = class {
|
|
|
277
545
|
const team = await this.getTeam(pw);
|
|
278
546
|
try {
|
|
279
547
|
await team.read();
|
|
280
|
-
if (!img) return
|
|
548
|
+
if (!img) return import_koishi3.h.image(team.img);
|
|
281
549
|
const url = await ctx.assets.upload("https://koishi.chat/logo.png", `${session.id}-${session.timestamp}.png`);
|
|
282
550
|
team.img = url;
|
|
283
551
|
return await team.write();
|
|
@@ -376,7 +644,17 @@ var MahjongPub = class {
|
|
|
376
644
|
return session.text(".failed");
|
|
377
645
|
}
|
|
378
646
|
});
|
|
379
|
-
ctx.command("mahjongpub.contest.bind <cid:string>
|
|
647
|
+
ctx.command("mahjongpub.contest.bind <cid:string> [cpw:string]", { authority: 4 }).option("channel", "-c <channel:channel>").option("only", "-o").channelFields(["mahjongpub/bind-contest", "mahjongpub/bind-contestpw"]).action(async ({ session, options }, cid, cpw) => {
|
|
648
|
+
if (options.only) {
|
|
649
|
+
if (options.channel) {
|
|
650
|
+
ctx.database.setChannel(...parsePlatform(options.channel), {
|
|
651
|
+
"mahjongpub/bind-contest": cid
|
|
652
|
+
});
|
|
653
|
+
} else {
|
|
654
|
+
session.channel["mahjongpub/bind-contest"] = cid;
|
|
655
|
+
}
|
|
656
|
+
return "Finished.";
|
|
657
|
+
}
|
|
380
658
|
const contest = await this.getContest(cid, cpw);
|
|
381
659
|
try {
|
|
382
660
|
await contest.read();
|
|
@@ -395,12 +673,32 @@ var MahjongPub = class {
|
|
|
395
673
|
}
|
|
396
674
|
return session.text(".success", contest);
|
|
397
675
|
});
|
|
676
|
+
ctx.command("mahjongpub.contest.password", { authority: 4 }).option("channel", "-c <channel:channel>").channelFields(["mahjongpub/bind-contest", "mahjongpub/bind-contestpw"]).action(async ({ session }) => {
|
|
677
|
+
const [cid, cpw] = [session.channel["mahjongpub/bind-contest"], session.channel["mahjongpub/bind-contestpw"]];
|
|
678
|
+
if (cid) return session.text(".success", [cid, cpw]);
|
|
679
|
+
else return session.text(".failed");
|
|
680
|
+
});
|
|
681
|
+
ctx.command("mahjongpub.contest.round [round:natural]", { authority: 3 }).channelFields(["mahjongpub/bind-contest", "mahjongpub/bind-contestpw"]).action(async ({ session }, round) => {
|
|
682
|
+
const [cid, cpw] = [session.channel["mahjongpub/bind-contest"], session.channel["mahjongpub/bind-contestpw"]];
|
|
683
|
+
const contest = await this.getContest(cid, cpw);
|
|
684
|
+
try {
|
|
685
|
+
const rounds = await contest.getRounds();
|
|
686
|
+
const clss = rounds[round || contest.round];
|
|
687
|
+
return Object.entries(clss).map(([i, r]) => [
|
|
688
|
+
`[${i}]`,
|
|
689
|
+
r.tids.map((tid) => `${contest.teams[tid].name}`).join(" / ")
|
|
690
|
+
].join(" ")).join("\n");
|
|
691
|
+
} catch (e) {
|
|
692
|
+
ctx.logger.warn(e);
|
|
693
|
+
return session.text(".failed");
|
|
694
|
+
}
|
|
695
|
+
});
|
|
398
696
|
ctx.command("mahjongpub.contest.record.get <round:natural> <cls:natural>", { authority: 3 }).option("rowi", "-i <rowi:integer>").channelFields(["mahjongpub/bind-contest", "mahjongpub/bind-contestpw"]).action(async ({ session, options }, round, cls) => {
|
|
399
697
|
const [cid, cpw] = [session.channel["mahjongpub/bind-contest"], session.channel["mahjongpub/bind-contestpw"]];
|
|
400
698
|
const contest = await this.getContest(cid, cpw);
|
|
401
699
|
try {
|
|
402
700
|
const record = await contest.getRoundRecord(round, cls);
|
|
403
|
-
if ((0,
|
|
701
|
+
if ((0, import_koishi3.isNullable)(options.rowi)) {
|
|
404
702
|
return record.matches.map((match, i) => `[${i}] ` + match.players.map((x) => `${x.name} ${x.point}`).join(" / ")).join("\n");
|
|
405
703
|
} else if (record.matches.at(options.rowi ?? -1)) {
|
|
406
704
|
return [
|
|
@@ -523,29 +821,29 @@ var MahjongPub = class {
|
|
|
523
821
|
if (this.ctx.get("cache")) {
|
|
524
822
|
let res2 = await this.ctx.cache.get("contests", cid);
|
|
525
823
|
if (res2) return res2;
|
|
526
|
-
res2 = new ContestAdmin(this.ctx, cid, pw, this.config);
|
|
824
|
+
res2 = new ContestAdmin(this.ctx, +cid, pw, this.config);
|
|
527
825
|
await res2.read();
|
|
528
826
|
await this.ctx.cache.set("contests", cid, res2, 1e3 * 600);
|
|
529
827
|
return res2;
|
|
530
828
|
}
|
|
531
|
-
const res = new ContestAdmin(this.ctx, cid, pw, this.config);
|
|
829
|
+
const res = new ContestAdmin(this.ctx, +cid, pw, this.config);
|
|
532
830
|
await res.read();
|
|
533
831
|
return res;
|
|
534
832
|
}
|
|
535
833
|
};
|
|
536
|
-
((
|
|
537
|
-
|
|
834
|
+
((MahjongPub3) => {
|
|
835
|
+
MahjongPub3.inject = {
|
|
538
836
|
required: ["database"],
|
|
539
837
|
optional: ["assets", "cache", "mahjong", "mahjong.database"]
|
|
540
838
|
};
|
|
541
|
-
|
|
542
|
-
informNotbind:
|
|
543
|
-
endpoint:
|
|
544
|
-
batchInterval:
|
|
545
|
-
batchCount:
|
|
839
|
+
MahjongPub3.Config = import_koishi3.Schema.object({
|
|
840
|
+
informNotbind: import_koishi3.Schema.boolean().default(false),
|
|
841
|
+
endpoint: import_koishi3.Schema.string().default("https://cdn.r-mj.com/"),
|
|
842
|
+
batchInterval: import_koishi3.Schema.natural().default(300),
|
|
843
|
+
batchCount: import_koishi3.Schema.natural().default(10)
|
|
546
844
|
});
|
|
547
|
-
})(
|
|
548
|
-
var
|
|
845
|
+
})(MahjongPub2 || (MahjongPub2 = {}));
|
|
846
|
+
var index_default = MahjongPub2;
|
|
549
847
|
// Annotate the CommonJS export names for ESM import in node:
|
|
550
848
|
0 && (module.exports = {
|
|
551
849
|
MahjongPub
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Context, Dict } from 'koishi';
|
|
2
|
+
export declare class MahjongPub {
|
|
3
|
+
ctx: Context;
|
|
4
|
+
private serverUri;
|
|
5
|
+
private http;
|
|
6
|
+
constructor(ctx: Context);
|
|
7
|
+
getContest(cid: number): Promise<MahjongPub.Contest>;
|
|
8
|
+
getTeams(cid: number): Promise<Dict<MahjongPub.Team>>;
|
|
9
|
+
getRounds(cid: number): Promise<Dict<MahjongPub.Round>>;
|
|
10
|
+
getMatches(cid: number, round: number): Promise<Dict<MahjongPub.Match[]>>;
|
|
11
|
+
getAllMatches(cid: number): Promise<Dict<MahjongPub.Match[]>>;
|
|
12
|
+
static cacheOptions: {
|
|
13
|
+
readonly getContest: {
|
|
14
|
+
readonly maxAge: 120;
|
|
15
|
+
};
|
|
16
|
+
readonly getRounds: {
|
|
17
|
+
readonly maxAge: 60;
|
|
18
|
+
};
|
|
19
|
+
readonly getTeams: {
|
|
20
|
+
readonly maxAge: 30;
|
|
21
|
+
};
|
|
22
|
+
readonly getMatches: {
|
|
23
|
+
readonly maxAge: 120;
|
|
24
|
+
};
|
|
25
|
+
readonly getAllMatches: {
|
|
26
|
+
readonly maxAge: 300;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
static new(ctx: Context): import("@hieuzest/cachetools").CachedClass<MahjongPub, "getContest" | "getRounds" | "getTeams" | "getMatches" | "getAllMatches", "_">;
|
|
30
|
+
}
|
|
31
|
+
export declare namespace MahjongPub {
|
|
32
|
+
export interface Team {
|
|
33
|
+
tid: number;
|
|
34
|
+
cid: string;
|
|
35
|
+
t_name: string;
|
|
36
|
+
t_ps: string;
|
|
37
|
+
t_player?: string;
|
|
38
|
+
t_sub?: string;
|
|
39
|
+
img: string;
|
|
40
|
+
t_type: number;
|
|
41
|
+
players: string[];
|
|
42
|
+
}
|
|
43
|
+
export interface Contest {
|
|
44
|
+
cid: number;
|
|
45
|
+
c_name: string;
|
|
46
|
+
c_round: number;
|
|
47
|
+
c_gonggao: string;
|
|
48
|
+
c_pad: string;
|
|
49
|
+
t_type: string;
|
|
50
|
+
}
|
|
51
|
+
export interface Round {
|
|
52
|
+
rid: number;
|
|
53
|
+
round: number;
|
|
54
|
+
t_class: number;
|
|
55
|
+
code: string;
|
|
56
|
+
tid1: number;
|
|
57
|
+
tid2: number;
|
|
58
|
+
tid3: number;
|
|
59
|
+
tid4: number;
|
|
60
|
+
tids: number[];
|
|
61
|
+
}
|
|
62
|
+
export interface MatchBrief {
|
|
63
|
+
inx: number;
|
|
64
|
+
cid: number;
|
|
65
|
+
rid: number;
|
|
66
|
+
round: number;
|
|
67
|
+
rowi: number;
|
|
68
|
+
i_time: string;
|
|
69
|
+
url: string;
|
|
70
|
+
}
|
|
71
|
+
type Stringiable = string | number;
|
|
72
|
+
type Suffixed<K, P extends Stringiable> = K extends string ? `${K}${P}` : never;
|
|
73
|
+
type SuffixRemoved<PK, P extends Stringiable> = PK extends Suffixed<infer K, P> ? K : '';
|
|
74
|
+
type SuffixedValue<T extends object, PK extends string, P extends Stringiable> = T extends {
|
|
75
|
+
[K in SuffixRemoved<PK, P>]: infer TValue;
|
|
76
|
+
} ? TValue : never;
|
|
77
|
+
type SuffixedObject<T extends object, P extends Stringiable> = {
|
|
78
|
+
[K in Suffixed<keyof T, P>]: SuffixedValue<T, K, P>;
|
|
79
|
+
};
|
|
80
|
+
interface MatchPlayerTemplate {
|
|
81
|
+
name: string;
|
|
82
|
+
num: number;
|
|
83
|
+
}
|
|
84
|
+
type MatchPlayer<P extends Stringiable> = SuffixedObject<MatchPlayerTemplate, P>;
|
|
85
|
+
export type Match = MatchBrief & MatchPlayer<1> & MatchPlayer<2> & MatchPlayer<3> & MatchPlayer<4>;
|
|
86
|
+
export {};
|
|
87
|
+
}
|
package/lib/manager.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Context, Dict, Schema } from 'koishi';
|
|
2
|
+
import { MahjongPub } from './mahjongpub';
|
|
3
|
+
interface PlayerResult {
|
|
4
|
+
name: string;
|
|
5
|
+
num: number;
|
|
6
|
+
pint: number;
|
|
7
|
+
}
|
|
8
|
+
interface MatchRecord {
|
|
9
|
+
_id: string;
|
|
10
|
+
inx?: string;
|
|
11
|
+
cid: string;
|
|
12
|
+
rid: string;
|
|
13
|
+
round: number;
|
|
14
|
+
rowi: number;
|
|
15
|
+
time?: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
cls: number;
|
|
18
|
+
tids: number[];
|
|
19
|
+
players: string[];
|
|
20
|
+
ver?: number;
|
|
21
|
+
_finished?: boolean;
|
|
22
|
+
results?: PlayerResult[];
|
|
23
|
+
}
|
|
24
|
+
declare class ContestExtra {
|
|
25
|
+
ctx: Context;
|
|
26
|
+
subscribers: Set<string>;
|
|
27
|
+
stopCls: Set<number>;
|
|
28
|
+
constructor(ctx: Context);
|
|
29
|
+
broadcast(msg: string): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export declare class ContestManager {
|
|
32
|
+
protected ctx: Context;
|
|
33
|
+
config: ContestManager.Config;
|
|
34
|
+
static inject: string[];
|
|
35
|
+
mahjongpub: MahjongPub;
|
|
36
|
+
extra: Dict<ContestExtra>;
|
|
37
|
+
constructor(ctx: Context, config: ContestManager.Config);
|
|
38
|
+
startMatch(cid: number, cls: number, timeout?: number, tag?: string): Promise<void>;
|
|
39
|
+
onMatchFinished(record: MatchRecord): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export declare namespace ContestManager {
|
|
42
|
+
interface Config {
|
|
43
|
+
token?: string;
|
|
44
|
+
}
|
|
45
|
+
const Config: Schema<Config>;
|
|
46
|
+
}
|
|
47
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hieuzest/koishi-plugin-mahjongpub",
|
|
3
3
|
"description": "Mahjong.pub API",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
"plugin"
|
|
16
16
|
],
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"koishi": "^4.17.1"
|
|
18
|
+
"koishi": "^4.17.1",
|
|
19
|
+
"@hieuzest/koishi-plugin-riichi-city": "^0.5.2"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@hieuzest/cachetools": "^0.1.5"
|
|
19
23
|
}
|
|
20
24
|
}
|