koishi-plugin-class-score-system 1.0.0
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/commands/menu.d.ts +2 -0
- package/lib/index.js +1922 -0
- package/lib/utils/constants.d.ts +48 -0
- package/lib/utils/image.d.ts +10 -0
- package/package.json +27 -0
- package/src/commands/adjust-score.ts +68 -0
- package/src/commands/admin-manager.ts +340 -0
- package/src/commands/bind-qq.ts +112 -0
- package/src/commands/bind-server.ts +92 -0
- package/src/commands/menu.ts +243 -0
- package/src/commands/query-score.ts +136 -0
- package/src/commands/ranking.ts +142 -0
- package/src/commands/statistics.ts +158 -0
- package/src/index.ts +41 -0
- package/src/locales/zh-CN.yml +58 -0
- package/src/models/database.ts +84 -0
- package/src/services/api.ts +335 -0
- package/src/services/binding.ts +72 -0
- package/src/services/group-admin.ts +89 -0
- package/src/services/server.ts +93 -0
- package/src/utils/constants.ts +51 -0
- package/src/utils/formatter.ts +172 -0
- package/src/utils/image.ts +114 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/validator.ts +71 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1922 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
import { Schema } from "koishi";
|
|
6
|
+
|
|
7
|
+
// src/models/database.ts
|
|
8
|
+
function extendDatabase(ctx) {
|
|
9
|
+
ctx.model.extend("server_configs", {
|
|
10
|
+
id: "integer",
|
|
11
|
+
guildId: "string",
|
|
12
|
+
name: "string",
|
|
13
|
+
address: "string",
|
|
14
|
+
username: "string",
|
|
15
|
+
token: "string",
|
|
16
|
+
createdAt: "timestamp",
|
|
17
|
+
updatedAt: "timestamp"
|
|
18
|
+
}, {
|
|
19
|
+
primary: "id",
|
|
20
|
+
autoInc: true
|
|
21
|
+
});
|
|
22
|
+
ctx.model.extend("qq_bindings", {
|
|
23
|
+
id: "integer",
|
|
24
|
+
guildId: "string",
|
|
25
|
+
userId: "integer",
|
|
26
|
+
username: "string",
|
|
27
|
+
openId: "string",
|
|
28
|
+
serverId: "integer",
|
|
29
|
+
createdAt: "timestamp",
|
|
30
|
+
updatedAt: "timestamp"
|
|
31
|
+
}, {
|
|
32
|
+
primary: "id",
|
|
33
|
+
autoInc: true,
|
|
34
|
+
unique: ["openId", "guildId"]
|
|
35
|
+
});
|
|
36
|
+
ctx.model.extend("group_admins", {
|
|
37
|
+
id: "integer",
|
|
38
|
+
guildId: "string",
|
|
39
|
+
openId: "string",
|
|
40
|
+
remark: "string",
|
|
41
|
+
verify: "integer",
|
|
42
|
+
createdAt: "timestamp"
|
|
43
|
+
}, {
|
|
44
|
+
primary: "id",
|
|
45
|
+
autoInc: true,
|
|
46
|
+
unique: ["openId", "guildId"]
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
__name(extendDatabase, "extendDatabase");
|
|
50
|
+
|
|
51
|
+
// src/services/api.ts
|
|
52
|
+
var CsmsApiService = class {
|
|
53
|
+
static {
|
|
54
|
+
__name(this, "CsmsApiService");
|
|
55
|
+
}
|
|
56
|
+
baseUrl;
|
|
57
|
+
token;
|
|
58
|
+
ctx;
|
|
59
|
+
constructor(ctx, baseUrl, token) {
|
|
60
|
+
this.ctx = ctx;
|
|
61
|
+
this.baseUrl = baseUrl.replace(/\/api\/global_api\.php$/, "").replace(/\/$/, "");
|
|
62
|
+
this.token = token;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 发送 API 请求
|
|
66
|
+
* 按照 global_api.md 文档规范实现
|
|
67
|
+
*/
|
|
68
|
+
async request(params) {
|
|
69
|
+
try {
|
|
70
|
+
const url = `${this.baseUrl}/api/global_api.php`;
|
|
71
|
+
const cleanParams = {};
|
|
72
|
+
for (const [key, value] of Object.entries(params)) {
|
|
73
|
+
if (value !== void 0 && value !== null) {
|
|
74
|
+
cleanParams[key] = String(value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
this.ctx.logger.debug(`API请求: ${url}`, cleanParams);
|
|
78
|
+
const response = await this.ctx.http.get(url, {
|
|
79
|
+
params: cleanParams,
|
|
80
|
+
headers: {
|
|
81
|
+
"Authorization": this.token
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
this.ctx.logger.debug(`API响应:`, response);
|
|
85
|
+
return response;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
this.ctx.logger.error("CSMS API 请求失败:", error);
|
|
88
|
+
return { error: error.message || "API请求失败" };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 验证 Token - 使用自定义的 add_score 接口测试
|
|
93
|
+
* 注意: 文档未提供专门的 validate_token 接口,用此方法间接验证
|
|
94
|
+
*/
|
|
95
|
+
async validateToken() {
|
|
96
|
+
try {
|
|
97
|
+
const url = `${this.baseUrl}/api/global_api.php`;
|
|
98
|
+
const params = {
|
|
99
|
+
action: "add_score",
|
|
100
|
+
data: JSON.stringify({
|
|
101
|
+
username: "__validate__",
|
|
102
|
+
score_change: 0,
|
|
103
|
+
description: "__token_validation__"
|
|
104
|
+
}),
|
|
105
|
+
token: this.token
|
|
106
|
+
};
|
|
107
|
+
this.ctx.logger.info(`验证Token中...`);
|
|
108
|
+
const response = await this.ctx.http.get(url, { params });
|
|
109
|
+
if (response.error) {
|
|
110
|
+
const errorLower = response.error.toLowerCase();
|
|
111
|
+
if (errorLower.includes("未授权") || errorLower.includes("token") || errorLower.includes("无效")) {
|
|
112
|
+
this.ctx.logger.warn(`Token验证失败: ${response.error}`);
|
|
113
|
+
return { valid: false };
|
|
114
|
+
}
|
|
115
|
+
this.ctx.logger.info(`Token验证成功`);
|
|
116
|
+
return { valid: true };
|
|
117
|
+
}
|
|
118
|
+
return { valid: true };
|
|
119
|
+
} catch (error) {
|
|
120
|
+
this.ctx.logger.error(`Token验证异常:`, error);
|
|
121
|
+
return { valid: false };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 查询用户列表
|
|
126
|
+
* GET /api/global_api.php?users&order_by=xxx&order=DESC&limit=20&offset=0
|
|
127
|
+
*/
|
|
128
|
+
async getUsers(options) {
|
|
129
|
+
const params = { users: "" };
|
|
130
|
+
if (options?.where) {
|
|
131
|
+
params.where = JSON.stringify(options.where);
|
|
132
|
+
}
|
|
133
|
+
if (options?.order_by) {
|
|
134
|
+
params.order_by = options.order_by;
|
|
135
|
+
}
|
|
136
|
+
if (options?.order) {
|
|
137
|
+
params.order = options.order;
|
|
138
|
+
}
|
|
139
|
+
if (options?.limit !== void 0) {
|
|
140
|
+
params.limit = options.limit.toString();
|
|
141
|
+
}
|
|
142
|
+
if (options?.offset !== void 0) {
|
|
143
|
+
params.offset = options.offset.toString();
|
|
144
|
+
}
|
|
145
|
+
if (options?.search) {
|
|
146
|
+
params.search = options.search;
|
|
147
|
+
}
|
|
148
|
+
return this.request(params);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 查询单个用户
|
|
152
|
+
* GET /api/global_api.php?users&id=xxx
|
|
153
|
+
*/
|
|
154
|
+
async getUserById(id) {
|
|
155
|
+
return this.request({ users: "", id: id.toString() });
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 按用户名查询用户
|
|
159
|
+
* GET /api/global_api.php?users&where={"username":"xxx"}
|
|
160
|
+
*/
|
|
161
|
+
async getUserByUsername(username) {
|
|
162
|
+
return this.request({
|
|
163
|
+
users: "",
|
|
164
|
+
where: JSON.stringify({ username })
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* 按QQ号查询用户
|
|
169
|
+
* GET /api/global_api.php?users&where={"qq_number":"xxx"}
|
|
170
|
+
*/
|
|
171
|
+
async getUserByQq(qqNumber) {
|
|
172
|
+
return this.request({
|
|
173
|
+
users: "",
|
|
174
|
+
where: JSON.stringify({ qq_number: qqNumber })
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 获取排行榜(按 total_score 降序)
|
|
179
|
+
* GET /api/global_api.php?users&order_by=total_score&order=DESC&limit=20
|
|
180
|
+
*/
|
|
181
|
+
async getRanking(limit = 20, offset = 0) {
|
|
182
|
+
return this.request({
|
|
183
|
+
users: "",
|
|
184
|
+
order_by: "total_score",
|
|
185
|
+
order: "DESC",
|
|
186
|
+
limit: limit.toString(),
|
|
187
|
+
offset: offset.toString()
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 查询积分记录
|
|
192
|
+
* GET /api/global_api.php?score_logs&where={"user_id":xxx}&order_by=created_at&order=DESC&limit=50
|
|
193
|
+
*/
|
|
194
|
+
async getScoreLogs(userId, limit = 50) {
|
|
195
|
+
return this.request({
|
|
196
|
+
score_logs: "",
|
|
197
|
+
where: JSON.stringify({ user_id: userId }),
|
|
198
|
+
order_by: "created_at",
|
|
199
|
+
order: "DESC",
|
|
200
|
+
limit: limit.toString()
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 创建新用户
|
|
205
|
+
* GET /api/global_api.php?users&action=create&data={"username":"xxx"}&token=xxx
|
|
206
|
+
*/
|
|
207
|
+
async createUser(username) {
|
|
208
|
+
return this.request({
|
|
209
|
+
users: "",
|
|
210
|
+
action: "create",
|
|
211
|
+
data: JSON.stringify({ username })
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* 更新用户信息(如绑定QQ号)
|
|
216
|
+
* GET /api/global_api.php?users&action=update&id=xxx&data={"qq_number":"xxx"}&token=xxx
|
|
217
|
+
*/
|
|
218
|
+
async updateUser(userId, data) {
|
|
219
|
+
return this.request({
|
|
220
|
+
users: "",
|
|
221
|
+
action: "update",
|
|
222
|
+
id: userId.toString(),
|
|
223
|
+
data: JSON.stringify(data)
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* 绑定用户QQ号
|
|
228
|
+
*/
|
|
229
|
+
async bindQq(userId, qqNumber) {
|
|
230
|
+
return this.updateUser(userId, { qq_number: qqNumber });
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* 批量加减分
|
|
234
|
+
* GET /api/global_api.php?action=add_score&data={"users":[...],"description":"xxx"}&token=xxx
|
|
235
|
+
*/
|
|
236
|
+
async batchAddScore(data) {
|
|
237
|
+
this.ctx.logger.info(`batchAddScore 请求数据:`, JSON.stringify(data));
|
|
238
|
+
const response = await this.request({
|
|
239
|
+
action: "add_score",
|
|
240
|
+
data: JSON.stringify(data)
|
|
241
|
+
});
|
|
242
|
+
this.ctx.logger.info(`batchAddScore API 响应:`, JSON.stringify(response));
|
|
243
|
+
if (response.error) {
|
|
244
|
+
return {
|
|
245
|
+
success: false,
|
|
246
|
+
message: response.error,
|
|
247
|
+
summary: { success_count: 0, failed_count: 0, total_count: 0 },
|
|
248
|
+
details: []
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (response.data && typeof response.data === "object" && "summary" in response.data) {
|
|
252
|
+
return response.data;
|
|
253
|
+
}
|
|
254
|
+
if (response && typeof response === "object" && "summary" in response) {
|
|
255
|
+
return response;
|
|
256
|
+
}
|
|
257
|
+
if (Array.isArray(response.data)) {
|
|
258
|
+
return {
|
|
259
|
+
success: true,
|
|
260
|
+
message: "操作完成",
|
|
261
|
+
summary: { success_count: response.data.length, failed_count: 0, total_count: response.data.length },
|
|
262
|
+
details: response.data
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
this.ctx.logger.error(`batchAddScore 返回数据格式异常,原始响应:`, response);
|
|
266
|
+
return {
|
|
267
|
+
success: false,
|
|
268
|
+
message: "API 返回数据格式异常",
|
|
269
|
+
summary: { success_count: 0, failed_count: 0, total_count: 0 },
|
|
270
|
+
details: []
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 单用户加减分
|
|
275
|
+
*/
|
|
276
|
+
async addScore(username, scoreChange, description) {
|
|
277
|
+
return this.batchAddScore({
|
|
278
|
+
users: [{ username, score_change: scoreChange }],
|
|
279
|
+
description
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// src/services/server.ts
|
|
285
|
+
var ServerConfigService = class {
|
|
286
|
+
static {
|
|
287
|
+
__name(this, "ServerConfigService");
|
|
288
|
+
}
|
|
289
|
+
ctx;
|
|
290
|
+
constructor(ctx) {
|
|
291
|
+
this.ctx = ctx;
|
|
292
|
+
}
|
|
293
|
+
async saveConfig(guildId, name2, address, username, token) {
|
|
294
|
+
const now = /* @__PURE__ */ new Date();
|
|
295
|
+
const existingConfigs = await this.ctx.database.get("server_configs", { guildId });
|
|
296
|
+
const baseUrl = address.replace(/\/api\/global_api\.php$/, "").replace(/\/$/, "");
|
|
297
|
+
if (existingConfigs.length > 0) {
|
|
298
|
+
await this.ctx.database.set("server_configs", { guildId }, {
|
|
299
|
+
name: name2,
|
|
300
|
+
address: baseUrl,
|
|
301
|
+
username,
|
|
302
|
+
token,
|
|
303
|
+
updatedAt: now
|
|
304
|
+
});
|
|
305
|
+
return { ...existingConfigs[0], name: name2, address: baseUrl, username, token, updatedAt: now };
|
|
306
|
+
}
|
|
307
|
+
const config = await this.ctx.database.create("server_configs", {
|
|
308
|
+
guildId,
|
|
309
|
+
name: name2,
|
|
310
|
+
address: baseUrl,
|
|
311
|
+
username,
|
|
312
|
+
token,
|
|
313
|
+
createdAt: now,
|
|
314
|
+
updatedAt: now
|
|
315
|
+
});
|
|
316
|
+
return config;
|
|
317
|
+
}
|
|
318
|
+
async getConfigByGuild(guildId) {
|
|
319
|
+
const configs = await this.ctx.database.get("server_configs", { guildId });
|
|
320
|
+
return configs[0] || null;
|
|
321
|
+
}
|
|
322
|
+
async getAllConfigs() {
|
|
323
|
+
return this.ctx.database.get("server_configs", {});
|
|
324
|
+
}
|
|
325
|
+
async deleteConfig(guildId) {
|
|
326
|
+
await this.ctx.database.remove("server_configs", { guildId });
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
async createApiService(guildId) {
|
|
330
|
+
const config = await this.getConfigByGuild(guildId);
|
|
331
|
+
if (!config) return null;
|
|
332
|
+
return new CsmsApiService(this.ctx, config.address, config.token);
|
|
333
|
+
}
|
|
334
|
+
async validateConfig(address, username, token) {
|
|
335
|
+
try {
|
|
336
|
+
const baseUrl = address.replace(/\/api\/global_api\.php$/, "").replace(/\/$/, "");
|
|
337
|
+
const api = new CsmsApiService(this.ctx, baseUrl, token);
|
|
338
|
+
const result = await api.validateToken();
|
|
339
|
+
if (result.valid) {
|
|
340
|
+
if (result.username && result.username !== username) {
|
|
341
|
+
return {
|
|
342
|
+
valid: true,
|
|
343
|
+
actualUsername: result.username,
|
|
344
|
+
error: `警告:提供的用户名 "${username}" 与实际登录的管理员 "${result.username}" 不一致`
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return { valid: true, actualUsername: result.username };
|
|
348
|
+
}
|
|
349
|
+
return { valid: false, error: "Token 验证失败,请检查 Token 是否正确" };
|
|
350
|
+
} catch (error) {
|
|
351
|
+
this.ctx.logger.error("验证服务器配置失败:", error);
|
|
352
|
+
return { valid: false, error: error.message };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// src/services/binding.ts
|
|
358
|
+
var QqBindingService = class {
|
|
359
|
+
static {
|
|
360
|
+
__name(this, "QqBindingService");
|
|
361
|
+
}
|
|
362
|
+
ctx;
|
|
363
|
+
constructor(ctx) {
|
|
364
|
+
this.ctx = ctx;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* openId 即 session.userId(QQ OpenID,非传统QQ号)
|
|
368
|
+
*/
|
|
369
|
+
async bindQq(guildId, userId, username, openId, serverId) {
|
|
370
|
+
const now = /* @__PURE__ */ new Date();
|
|
371
|
+
const bindings = await this.ctx.database.get("qq_bindings", { openId, guildId });
|
|
372
|
+
if (bindings.length > 0) {
|
|
373
|
+
await this.ctx.database.set("qq_bindings", { openId, guildId }, {
|
|
374
|
+
userId,
|
|
375
|
+
username,
|
|
376
|
+
serverId,
|
|
377
|
+
updatedAt: now
|
|
378
|
+
});
|
|
379
|
+
return { ...bindings[0], userId, username, serverId, updatedAt: now };
|
|
380
|
+
}
|
|
381
|
+
const binding = await this.ctx.database.create("qq_bindings", {
|
|
382
|
+
guildId,
|
|
383
|
+
userId,
|
|
384
|
+
username,
|
|
385
|
+
openId,
|
|
386
|
+
serverId,
|
|
387
|
+
createdAt: now,
|
|
388
|
+
updatedAt: now
|
|
389
|
+
});
|
|
390
|
+
return binding;
|
|
391
|
+
}
|
|
392
|
+
async unbindQq(guildId, openId) {
|
|
393
|
+
await this.ctx.database.remove("qq_bindings", { openId, guildId });
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
async unbindByUserId(guildId, userId) {
|
|
397
|
+
await this.ctx.database.remove("qq_bindings", { userId, guildId });
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
async getUserByOpenId(guildId, openId) {
|
|
401
|
+
const bindings = await this.ctx.database.get("qq_bindings", { openId, guildId });
|
|
402
|
+
return bindings[0] || null;
|
|
403
|
+
}
|
|
404
|
+
async getQqByUserId(guildId, userId) {
|
|
405
|
+
const bindings = await this.ctx.database.get("qq_bindings", { userId, guildId });
|
|
406
|
+
return bindings[0] || null;
|
|
407
|
+
}
|
|
408
|
+
async getAllBindings(guildId) {
|
|
409
|
+
return this.ctx.database.get("qq_bindings", { guildId });
|
|
410
|
+
}
|
|
411
|
+
async getBindingsByServer(guildId, serverId) {
|
|
412
|
+
return this.ctx.database.get("qq_bindings", { serverId, guildId });
|
|
413
|
+
}
|
|
414
|
+
async getBindingsByUsername(guildId, username) {
|
|
415
|
+
return this.ctx.database.get("qq_bindings", { username, guildId });
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// src/services/group-admin.ts
|
|
420
|
+
var GroupAdminService = class {
|
|
421
|
+
static {
|
|
422
|
+
__name(this, "GroupAdminService");
|
|
423
|
+
}
|
|
424
|
+
ctx;
|
|
425
|
+
constructor(ctx) {
|
|
426
|
+
this.ctx = ctx;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* 添加已验证的管理员(verify = 1,用于绑定服务器时自动升级)
|
|
430
|
+
*/
|
|
431
|
+
async addAdmin(guildId, openId, remark) {
|
|
432
|
+
const existing = await this.ctx.database.get("group_admins", { guildId, openId });
|
|
433
|
+
if (existing.length > 0) {
|
|
434
|
+
await this.ctx.database.set("group_admins", { guildId, openId }, { remark, verify: 1 });
|
|
435
|
+
return { ...existing[0], remark, verify: 1 };
|
|
436
|
+
}
|
|
437
|
+
return this.ctx.database.create("group_admins", {
|
|
438
|
+
guildId,
|
|
439
|
+
openId,
|
|
440
|
+
remark,
|
|
441
|
+
verify: 1,
|
|
442
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* 创建待确认的群管记录(Verify = 0)
|
|
447
|
+
*/
|
|
448
|
+
async createPendingAdmin(guildId, openId, remark) {
|
|
449
|
+
const existing = await this.ctx.database.get("group_admins", { guildId, openId });
|
|
450
|
+
if (existing.length > 0) {
|
|
451
|
+
return existing[0];
|
|
452
|
+
}
|
|
453
|
+
return this.ctx.database.create("group_admins", {
|
|
454
|
+
guildId,
|
|
455
|
+
openId,
|
|
456
|
+
remark,
|
|
457
|
+
verify: 0,
|
|
458
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* 确认群管(将 Verify 改为 1)
|
|
463
|
+
*/
|
|
464
|
+
async confirmAdmin(guildId, openId) {
|
|
465
|
+
await this.ctx.database.set("group_admins", { guildId, openId }, { verify: 1 });
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* 移除群管(按备注 查找并删除)
|
|
470
|
+
*/
|
|
471
|
+
async removeAdminByRemark(guildId, remark) {
|
|
472
|
+
await this.ctx.database.remove("group_admins", { guildId, remark });
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* 检查是否为已确认的群管(OpenID + Verify = 1)
|
|
477
|
+
*/
|
|
478
|
+
async isAdmin(guildId, openId) {
|
|
479
|
+
const admins = await this.ctx.database.get("group_admins", { guildId, openId });
|
|
480
|
+
return admins.length > 0 && admins[0].verify === 1;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* 获取所有群管
|
|
484
|
+
*/
|
|
485
|
+
async getAllAdmins(guildId) {
|
|
486
|
+
return this.ctx.database.get("group_admins", { guildId });
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* 检查用户是否有管理权限(按 OpenID + Verify = 1)
|
|
490
|
+
*/
|
|
491
|
+
async hasAdminPermission(session) {
|
|
492
|
+
const openId = session.userId?.toString();
|
|
493
|
+
if (!openId || !session.guildId) return false;
|
|
494
|
+
return await this.isAdmin(session.guildId, openId);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// src/utils/constants.ts
|
|
499
|
+
var CONSTANTS = {
|
|
500
|
+
TOKEN_PATTERN: /^[A-Z0-9]+$/,
|
|
501
|
+
QQ_PATTERN: /^\d{5,12}$/,
|
|
502
|
+
SCORE_MIN: -1e3,
|
|
503
|
+
SCORE_MAX: 1e3,
|
|
504
|
+
RANKING_DEFAULT_LIMIT: 10,
|
|
505
|
+
RANKING_MAX_LIMIT: 50,
|
|
506
|
+
SCORE_LOGS_DEFAULT_LIMIT: 20,
|
|
507
|
+
SCORE_LOGS_MAX_LIMIT: 100,
|
|
508
|
+
USERNAME_MAX_LENGTH: 50,
|
|
509
|
+
DESCRIPTION_MAX_LENGTH: 255
|
|
510
|
+
};
|
|
511
|
+
var ERROR_MESSAGES = {
|
|
512
|
+
NO_SERVER_CONFIG: "未配置CSMS服务器,请先使用 /绑定服务器 命令配置",
|
|
513
|
+
INVALID_TOKEN: "无效的Token格式,Token必须由数字和英文大写字母组成",
|
|
514
|
+
INVALID_QQ: "无效的QQ号码格式",
|
|
515
|
+
INVALID_SCORE: "分数必须在 -1000 到 1000 之间",
|
|
516
|
+
INVALID_USERNAME: "用户名不能为空且长度不能超过50个字符",
|
|
517
|
+
INVALID_DESCRIPTION: "描述信息长度不能超过255个字符",
|
|
518
|
+
INVALID_LIMIT: "数量必须在 1 到 50 之间",
|
|
519
|
+
SERVER_NOT_FOUND: "服务器未找到",
|
|
520
|
+
USER_NOT_FOUND: "用户未找到",
|
|
521
|
+
USER_ALREADY_BOUND: "该QQ号已绑定其他用户",
|
|
522
|
+
USER_NOT_BOUND: "该QQ号未绑定任何用户",
|
|
523
|
+
API_REQUEST_FAILED: "API请求失败",
|
|
524
|
+
INSUFFICIENT_PERMISSION: "权限不足,仅管理员可执行此操作",
|
|
525
|
+
BINDING_FAILED: "绑定失败",
|
|
526
|
+
UNBINDING_FAILED: "解除绑定失败",
|
|
527
|
+
CONFIGURATION_FAILED: "配置保存失败"
|
|
528
|
+
};
|
|
529
|
+
var HELP_MESSAGES = {
|
|
530
|
+
BIND_SERVER: "绑定CSMS服务器\n用法:/绑定服务器 <服务器地址> <管理员用户名> <管理员Token>\n示例:/绑定服务器 https://example.com admin ABC123",
|
|
531
|
+
QUERY_SCORE: "查询积分\n用法:/查询积分 [用户名]\n示例:/查询积分 张三",
|
|
532
|
+
ADJUST_SCORE: "调整积分\n用法:/调整积分 <用户名> <分数> <原因>\n示例:/调整积分 张三 +5 表现优秀",
|
|
533
|
+
BIND_QQ: "绑定QQ\n用法:/绑定QQ <用户名>\n示例:/绑定QQ 张三",
|
|
534
|
+
VIEW_BINDING: "查看绑定\n用法:/查看绑定 [用户名]\n示例:/查看绑定 张三",
|
|
535
|
+
UNBIND_QQ: "解除绑定\n用法:/解除绑定 <QQ号>\n示例:/解除绑定 123456789",
|
|
536
|
+
RANKING: "排行榜\n用法:/排行榜 [数量]\n示例:/排行榜 10",
|
|
537
|
+
STATISTICS: "统计\n用法:/统计 [用户名]\n示例:/统计 张三"
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
// src/utils/validator.ts
|
|
541
|
+
var Validator = class {
|
|
542
|
+
static {
|
|
543
|
+
__name(this, "Validator");
|
|
544
|
+
}
|
|
545
|
+
static validateToken(token) {
|
|
546
|
+
return CONSTANTS.TOKEN_PATTERN.test(token);
|
|
547
|
+
}
|
|
548
|
+
static validateQq(qqNumber) {
|
|
549
|
+
return qqNumber && qqNumber.length > 0 && /^\d+$/.test(qqNumber);
|
|
550
|
+
}
|
|
551
|
+
static validateScore(score) {
|
|
552
|
+
return score >= CONSTANTS.SCORE_MIN && score <= CONSTANTS.SCORE_MAX;
|
|
553
|
+
}
|
|
554
|
+
static validateUsername(username) {
|
|
555
|
+
return username.length > 0 && username.length <= CONSTANTS.USERNAME_MAX_LENGTH;
|
|
556
|
+
}
|
|
557
|
+
static validateDescription(description) {
|
|
558
|
+
return description.length <= CONSTANTS.DESCRIPTION_MAX_LENGTH;
|
|
559
|
+
}
|
|
560
|
+
static validateLimit(limit, max = CONSTANTS.RANKING_MAX_LIMIT) {
|
|
561
|
+
return limit > 0 && limit <= max;
|
|
562
|
+
}
|
|
563
|
+
static validateScoreWithMessage(score) {
|
|
564
|
+
if (!this.validateScore(score)) {
|
|
565
|
+
return ERROR_MESSAGES.INVALID_SCORE;
|
|
566
|
+
}
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
static validateQqWithMessage(qqNumber) {
|
|
570
|
+
if (!this.validateQq(qqNumber)) {
|
|
571
|
+
return ERROR_MESSAGES.INVALID_QQ;
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
static validateTokenWithMessage(token) {
|
|
576
|
+
if (!this.validateToken(token)) {
|
|
577
|
+
return ERROR_MESSAGES.INVALID_TOKEN;
|
|
578
|
+
}
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
static validateUsernameWithMessage(username) {
|
|
582
|
+
if (!this.validateUsername(username)) {
|
|
583
|
+
return ERROR_MESSAGES.INVALID_USERNAME;
|
|
584
|
+
}
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
static validateDescriptionWithMessage(description) {
|
|
588
|
+
if (!this.validateDescription(description)) {
|
|
589
|
+
return ERROR_MESSAGES.INVALID_DESCRIPTION;
|
|
590
|
+
}
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
static validateLimitWithMessage(limit, max = CONSTANTS.RANKING_MAX_LIMIT) {
|
|
594
|
+
if (!this.validateLimit(limit, max)) {
|
|
595
|
+
return `数量必须在 1 到 ${max} 之间`;
|
|
596
|
+
}
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// src/utils/formatter.ts
|
|
602
|
+
var Formatter = class {
|
|
603
|
+
static {
|
|
604
|
+
__name(this, "Formatter");
|
|
605
|
+
}
|
|
606
|
+
// QQ 消息单条最大长度限制(留有余量)
|
|
607
|
+
static MAX_MESSAGE_LENGTH = 1800;
|
|
608
|
+
static formatScoreInfo(user, ranking) {
|
|
609
|
+
return [
|
|
610
|
+
`用户: ${user.username}`,
|
|
611
|
+
`排名: ${ranking || "未知"}`,
|
|
612
|
+
`总积分: ${user.total_score}`,
|
|
613
|
+
`累计加分: ${user.add_score}`,
|
|
614
|
+
`累计扣分: ${user.deduct_score}`,
|
|
615
|
+
`记录数: ${user.score_count}`
|
|
616
|
+
].join("\n");
|
|
617
|
+
}
|
|
618
|
+
static formatRanking(users) {
|
|
619
|
+
if (users.length === 0) {
|
|
620
|
+
return "暂无排名数据";
|
|
621
|
+
}
|
|
622
|
+
const lines = ["【积分排行榜】"];
|
|
623
|
+
const displayUsers = users.slice(0, 20);
|
|
624
|
+
displayUsers.forEach((user, index) => {
|
|
625
|
+
const medal = index < 3 ? ["🥇", "🥈", "🥉"][index] : `${index + 1}.`;
|
|
626
|
+
lines.push(`${medal} ${user.username}: ${user.total_score}分`);
|
|
627
|
+
});
|
|
628
|
+
if (users.length > 20) {
|
|
629
|
+
lines.push(`... 共 ${users.length} 人,显示前20名`);
|
|
630
|
+
}
|
|
631
|
+
return lines.join("\n");
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* 格式化排行榜(带实际排名,用于分页)
|
|
635
|
+
*/
|
|
636
|
+
static formatRankingWithRank(users, startRank) {
|
|
637
|
+
if (users.length === 0) {
|
|
638
|
+
return "暂无排名数据";
|
|
639
|
+
}
|
|
640
|
+
const lines = ["【积分排行榜】"];
|
|
641
|
+
users.forEach((user, index) => {
|
|
642
|
+
const rank = startRank + index;
|
|
643
|
+
const medal = rank <= 3 ? ["🥇", "🥈", "🥉"][rank - 1] : `${rank}.`;
|
|
644
|
+
lines.push(`${medal} ${user.username}: ${user.total_score}分`);
|
|
645
|
+
});
|
|
646
|
+
return lines.join("\n");
|
|
647
|
+
}
|
|
648
|
+
static getMedal(index) {
|
|
649
|
+
const medals = ["🥇", "🥈", "🥉"];
|
|
650
|
+
return medals[index] || `${index + 1}.`;
|
|
651
|
+
}
|
|
652
|
+
static formatScoreLogs(logs) {
|
|
653
|
+
if (logs.length === 0) {
|
|
654
|
+
return "暂无积分记录";
|
|
655
|
+
}
|
|
656
|
+
const lines = ["【积分记录】"];
|
|
657
|
+
const displayLogs = logs.slice(0, 10);
|
|
658
|
+
displayLogs.forEach((log) => {
|
|
659
|
+
const change = log.score_change > 0 ? `+${log.score_change}` : log.score_change;
|
|
660
|
+
lines.push(`${change}分 - ${log.description}`);
|
|
661
|
+
});
|
|
662
|
+
if (logs.length > 10) {
|
|
663
|
+
lines.push(`... 共 ${logs.length} 条记录`);
|
|
664
|
+
}
|
|
665
|
+
return lines.join("\n");
|
|
666
|
+
}
|
|
667
|
+
static formatStatistics(user, logs) {
|
|
668
|
+
const lines = [
|
|
669
|
+
`【${user.username} 的统计信息】`,
|
|
670
|
+
`总积分: ${user.total_score}`,
|
|
671
|
+
`累计加分: ${user.add_score}`,
|
|
672
|
+
`累计扣分: ${user.deduct_score}`,
|
|
673
|
+
`记录数: ${user.score_count}`,
|
|
674
|
+
"最近积分记录:"
|
|
675
|
+
];
|
|
676
|
+
const recentLogs = logs.slice(0, 5);
|
|
677
|
+
if (recentLogs.length > 0) {
|
|
678
|
+
recentLogs.forEach((log) => {
|
|
679
|
+
const change = log.score_change > 0 ? `+${log.score_change}` : log.score_change;
|
|
680
|
+
lines.push(`${change}分 - ${log.description}`);
|
|
681
|
+
});
|
|
682
|
+
} else {
|
|
683
|
+
lines.push("暂无记录");
|
|
684
|
+
}
|
|
685
|
+
return lines.join("\n");
|
|
686
|
+
}
|
|
687
|
+
static formatAddScoreResult(result) {
|
|
688
|
+
const status = result.success ? "积分调整成功" : "积分调整失败";
|
|
689
|
+
const summary = `成功: ${result.summary.success_count} 条,失败: ${result.summary.failed_count} 条`;
|
|
690
|
+
const lines = [status, summary];
|
|
691
|
+
if (!result.success && result.message) {
|
|
692
|
+
lines.push(`原因: ${result.message}`);
|
|
693
|
+
}
|
|
694
|
+
if (result.details && result.details.length > 0) {
|
|
695
|
+
lines.push("详情:");
|
|
696
|
+
result.details.forEach((detail) => {
|
|
697
|
+
if (detail.success) {
|
|
698
|
+
const change = detail.score_change > 0 ? "+" : "";
|
|
699
|
+
lines.push(`[OK] ${detail.username}: ${change}${detail.score_change}分`);
|
|
700
|
+
} else {
|
|
701
|
+
lines.push(`[FAIL] ${detail.username}: ${detail.error || "失败"}`);
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
return lines.join("\n");
|
|
706
|
+
}
|
|
707
|
+
static formatDate(dateString) {
|
|
708
|
+
const date = new Date(dateString);
|
|
709
|
+
return date.toLocaleString("zh-CN", {
|
|
710
|
+
year: "numeric",
|
|
711
|
+
month: "2-digit",
|
|
712
|
+
day: "2-digit",
|
|
713
|
+
hour: "2-digit",
|
|
714
|
+
minute: "2-digit"
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* 安全发送消息,自动分割过长的消息
|
|
719
|
+
* 返回消息片段数组,由调用者处理发送
|
|
720
|
+
*/
|
|
721
|
+
static splitMessage(message) {
|
|
722
|
+
if (message.length <= this.MAX_MESSAGE_LENGTH) {
|
|
723
|
+
return [message];
|
|
724
|
+
}
|
|
725
|
+
const parts = message.split("\n");
|
|
726
|
+
const result = [];
|
|
727
|
+
let currentPart = "";
|
|
728
|
+
for (const line of parts) {
|
|
729
|
+
if ((currentPart + "\n" + line).length > this.MAX_MESSAGE_LENGTH) {
|
|
730
|
+
if (currentPart) {
|
|
731
|
+
result.push(currentPart);
|
|
732
|
+
}
|
|
733
|
+
currentPart = line;
|
|
734
|
+
} else {
|
|
735
|
+
currentPart = currentPart ? currentPart + "\n" + line : line;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (currentPart) {
|
|
739
|
+
result.push(currentPart);
|
|
740
|
+
}
|
|
741
|
+
return result;
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
// src/commands/bind-server.ts
|
|
746
|
+
function registerBindServerCommand(ctx, serverService, adminService) {
|
|
747
|
+
ctx.command("绑定服务器 <地址:string> <用户名:string> <token:string>").alias("bind-server").action(async ({ session }, address, username, token) => {
|
|
748
|
+
if (!address || !username || !token) {
|
|
749
|
+
return HELP_MESSAGES.BIND_SERVER;
|
|
750
|
+
}
|
|
751
|
+
if (!session.guildId) {
|
|
752
|
+
return "此命令只能在群聊中使用";
|
|
753
|
+
}
|
|
754
|
+
const openId = session.userId?.toString();
|
|
755
|
+
if (!openId) {
|
|
756
|
+
return "无法获取您的用户ID";
|
|
757
|
+
}
|
|
758
|
+
const existingConfig = await serverService.getConfigByGuild(session.guildId);
|
|
759
|
+
if (existingConfig) {
|
|
760
|
+
return "❌ 该群聊已绑定服务器,如需更换请先使用 /服务器解绑";
|
|
761
|
+
}
|
|
762
|
+
const tokenError = Validator.validateTokenWithMessage(token);
|
|
763
|
+
if (tokenError) {
|
|
764
|
+
ctx.logger.info(`Token 格式验证失败: ${tokenError}`);
|
|
765
|
+
return `Token 格式验证失败`;
|
|
766
|
+
}
|
|
767
|
+
try {
|
|
768
|
+
ctx.logger.info(`正在验证服务器配置...`);
|
|
769
|
+
const result = await serverService.validateConfig(address, username, token);
|
|
770
|
+
if (!result.valid) {
|
|
771
|
+
const errorMsg = result.error || "服务器配置验证失败";
|
|
772
|
+
ctx.logger.info(`服务器配置验证失败: ${errorMsg}`);
|
|
773
|
+
return `验证失败: ${errorMsg}`;
|
|
774
|
+
}
|
|
775
|
+
const actualUsername = result.actualUsername || username;
|
|
776
|
+
const config = await serverService.saveConfig(session.guildId, "CSMS服务器", address, actualUsername, token);
|
|
777
|
+
await adminService.addAdmin(session.guildId, openId, "管理员");
|
|
778
|
+
ctx.logger.info(`群聊 ${session.guildId} 的首个绑定者 ${openId} 被自动添加为管理员`);
|
|
779
|
+
const domain = config.address.replace(/^https?:\/\//, "").split("/")[0];
|
|
780
|
+
ctx.logger.info(`服务器配置成功,domain=${domain},username=${actualUsername}`);
|
|
781
|
+
return `✅ 服务器配置成功`;
|
|
782
|
+
} catch (error) {
|
|
783
|
+
ctx.logger.error("保存服务器配置失败:", error);
|
|
784
|
+
return `配置保存失败: ${error.message}`;
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
ctx.command("服务器解绑").alias("unbind-server").action(async ({ session }) => {
|
|
788
|
+
if (!session.guildId) {
|
|
789
|
+
return "此命令只能在群聊中使用";
|
|
790
|
+
}
|
|
791
|
+
const hasPermission = await adminService.hasAdminPermission(session);
|
|
792
|
+
if (!hasPermission) {
|
|
793
|
+
return "❌ 此命令仅限管理员使用";
|
|
794
|
+
}
|
|
795
|
+
const config = await serverService.getConfigByGuild(session.guildId);
|
|
796
|
+
if (!config) {
|
|
797
|
+
return "❌ 该群聊尚未绑定任何服务器";
|
|
798
|
+
}
|
|
799
|
+
await session.send("⚠️ 确定要解除服务器绑定吗?解绑后其他人可重新绑定。请在 30 秒内输入「确认」来完成操作");
|
|
800
|
+
const reply = await session.prompt(3e4);
|
|
801
|
+
if (reply?.trim() !== "确认") {
|
|
802
|
+
return "✅ 已取消解绑操作";
|
|
803
|
+
}
|
|
804
|
+
await serverService.deleteConfig(session.guildId);
|
|
805
|
+
ctx.logger.info(`群聊 ${session.guildId} 解除了服务器绑定`);
|
|
806
|
+
return `✅ 已解除服务器绑定`;
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
__name(registerBindServerCommand, "registerBindServerCommand");
|
|
810
|
+
|
|
811
|
+
// src/utils/image.ts
|
|
812
|
+
import { h } from "koishi";
|
|
813
|
+
async function generateImage(ctx, html, options = {}) {
|
|
814
|
+
const width = options.width || 450;
|
|
815
|
+
const height = options.height || 800;
|
|
816
|
+
try {
|
|
817
|
+
const page = await ctx.puppeteer.page();
|
|
818
|
+
await page.setViewport({
|
|
819
|
+
width,
|
|
820
|
+
height,
|
|
821
|
+
deviceScaleFactor: 2
|
|
822
|
+
});
|
|
823
|
+
await page.setContent(html, { waitUntil: "networkidle0" });
|
|
824
|
+
await page.evaluateHandle("document.fonts.ready");
|
|
825
|
+
await page.evaluate(() => {
|
|
826
|
+
return new Promise((resolve) => {
|
|
827
|
+
const img = document.querySelector("body");
|
|
828
|
+
if (img && img.style.backgroundImage) {
|
|
829
|
+
const bgImg = new Image();
|
|
830
|
+
bgImg.onload = () => resolve();
|
|
831
|
+
bgImg.onerror = () => resolve();
|
|
832
|
+
bgImg.src = img.style.backgroundImage.replace(/url\(['"]?(.+?)['"]?\)/, "$1");
|
|
833
|
+
} else {
|
|
834
|
+
resolve();
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
const screenshot = await page.screenshot({
|
|
839
|
+
type: "png",
|
|
840
|
+
clip: { x: 0, y: 0, width, height }
|
|
841
|
+
});
|
|
842
|
+
return screenshot;
|
|
843
|
+
} catch (error) {
|
|
844
|
+
ctx.logger.error("生成图片失败:", error);
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
__name(generateImage, "generateImage");
|
|
849
|
+
async function sendImageOrText(ctx, session, html, fallbackText, options = {}) {
|
|
850
|
+
const image = await generateImage(ctx, html, options);
|
|
851
|
+
if (image) {
|
|
852
|
+
await session.send(h.image(image, "image/png"));
|
|
853
|
+
} else {
|
|
854
|
+
await session.send(fallbackText);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
__name(sendImageOrText, "sendImageOrText");
|
|
858
|
+
function getBaseStyles() {
|
|
859
|
+
return `
|
|
860
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
861
|
+
body {
|
|
862
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
863
|
+
background: url('https://api.yppp.net/pe.php') center/cover no-repeat;
|
|
864
|
+
width: 450px;
|
|
865
|
+
min-height: 800px;
|
|
866
|
+
display: flex;
|
|
867
|
+
justify-content: center;
|
|
868
|
+
align-items: flex-start;
|
|
869
|
+
padding: 40px 0;
|
|
870
|
+
}
|
|
871
|
+
.container {
|
|
872
|
+
background: rgba(255, 255, 255, 0.5);
|
|
873
|
+
border-radius: 24px;
|
|
874
|
+
padding: 28px 24px;
|
|
875
|
+
width: 380px;
|
|
876
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
|
877
|
+
margin: 0 auto;
|
|
878
|
+
}
|
|
879
|
+
.title {
|
|
880
|
+
text-align: center;
|
|
881
|
+
font-size: 22px;
|
|
882
|
+
font-weight: bold;
|
|
883
|
+
color: #2c3e50;
|
|
884
|
+
margin-bottom: 20px;
|
|
885
|
+
padding-bottom: 12px;
|
|
886
|
+
border-bottom: 2px solid #3498db;
|
|
887
|
+
}
|
|
888
|
+
.content {
|
|
889
|
+
color: #555;
|
|
890
|
+
font-size: 14px;
|
|
891
|
+
line-height: 1.8;
|
|
892
|
+
white-space: pre-wrap;
|
|
893
|
+
word-break: break-all;
|
|
894
|
+
}
|
|
895
|
+
.footer {
|
|
896
|
+
text-align: center;
|
|
897
|
+
margin-top: 16px;
|
|
898
|
+
padding-top: 12px;
|
|
899
|
+
border-top: 1px solid rgba(0,0,0,0.1);
|
|
900
|
+
color: #95a5a6;
|
|
901
|
+
font-size: 10px;
|
|
902
|
+
}
|
|
903
|
+
`;
|
|
904
|
+
}
|
|
905
|
+
__name(getBaseStyles, "getBaseStyles");
|
|
906
|
+
|
|
907
|
+
// src/commands/query-score.ts
|
|
908
|
+
function registerQueryScoreCommand(ctx, serverService, bindingService) {
|
|
909
|
+
ctx.command("查询积分 [用户名:string]").alias("query-score").action(async ({ session }, username) => {
|
|
910
|
+
if (!session.guildId) {
|
|
911
|
+
return "此命令只能在群聊中使用";
|
|
912
|
+
}
|
|
913
|
+
const api = await serverService.createApiService(session.guildId);
|
|
914
|
+
if (!api) {
|
|
915
|
+
return ERROR_MESSAGES.NO_SERVER_CONFIG;
|
|
916
|
+
}
|
|
917
|
+
try {
|
|
918
|
+
let targetUsername = username;
|
|
919
|
+
if (!username) {
|
|
920
|
+
const openId = session.userId?.toString();
|
|
921
|
+
if (!openId) {
|
|
922
|
+
return "无法获取您的用户ID";
|
|
923
|
+
}
|
|
924
|
+
const binding = await bindingService.getUserByOpenId(session.guildId, openId);
|
|
925
|
+
if (!binding) {
|
|
926
|
+
return `${ERROR_MESSAGES.USER_NOT_BOUND}。请先使用 /绑定QQ <用户名> 命令绑定您的账号`;
|
|
927
|
+
}
|
|
928
|
+
targetUsername = binding.username;
|
|
929
|
+
}
|
|
930
|
+
const response = await api.getUserByUsername(targetUsername);
|
|
931
|
+
if (response.error || !response.data || response.data.length === 0) {
|
|
932
|
+
return ERROR_MESSAGES.USER_NOT_FOUND;
|
|
933
|
+
}
|
|
934
|
+
const user = response.data[0];
|
|
935
|
+
const rankingResponse = await api.getRanking(CONSTANTS.RANKING_MAX_LIMIT, 0);
|
|
936
|
+
let ranking = 0;
|
|
937
|
+
if (!rankingResponse.error && rankingResponse.data) {
|
|
938
|
+
const index = rankingResponse.data.findIndex((u) => u.id === user.id);
|
|
939
|
+
ranking = index >= 0 ? index + 1 : 0;
|
|
940
|
+
}
|
|
941
|
+
const textResult = Formatter.formatScoreInfo(user, ranking);
|
|
942
|
+
const html = `
|
|
943
|
+
<!DOCTYPE html>
|
|
944
|
+
<html>
|
|
945
|
+
<head>
|
|
946
|
+
<meta charset="utf-8">
|
|
947
|
+
<style>
|
|
948
|
+
${getBaseStyles()}
|
|
949
|
+
.info-grid {
|
|
950
|
+
display: grid;
|
|
951
|
+
grid-template-columns: 1fr 1fr;
|
|
952
|
+
gap: 12px;
|
|
953
|
+
margin-top: 16px;
|
|
954
|
+
}
|
|
955
|
+
.info-item {
|
|
956
|
+
background: rgba(52, 152, 219, 0.1);
|
|
957
|
+
border-radius: 12px;
|
|
958
|
+
padding: 16px;
|
|
959
|
+
text-align: center;
|
|
960
|
+
}
|
|
961
|
+
.info-label {
|
|
962
|
+
color: #7f8c8d;
|
|
963
|
+
font-size: 12px;
|
|
964
|
+
margin-bottom: 8px;
|
|
965
|
+
}
|
|
966
|
+
.info-value {
|
|
967
|
+
color: #2c3e50;
|
|
968
|
+
font-size: 24px;
|
|
969
|
+
font-weight: bold;
|
|
970
|
+
}
|
|
971
|
+
.info-value.score {
|
|
972
|
+
color: #3498db;
|
|
973
|
+
}
|
|
974
|
+
.info-value.rank {
|
|
975
|
+
color: #9b59b6;
|
|
976
|
+
}
|
|
977
|
+
.username {
|
|
978
|
+
text-align: center;
|
|
979
|
+
font-size: 20px;
|
|
980
|
+
font-weight: bold;
|
|
981
|
+
color: #2c3e50;
|
|
982
|
+
margin-bottom: 4px;
|
|
983
|
+
}
|
|
984
|
+
.subtitle {
|
|
985
|
+
text-align: center;
|
|
986
|
+
color: #95a5a6;
|
|
987
|
+
font-size: 12px;
|
|
988
|
+
margin-bottom: 20px;
|
|
989
|
+
}
|
|
990
|
+
</style>
|
|
991
|
+
</head>
|
|
992
|
+
<body>
|
|
993
|
+
<div class="container">
|
|
994
|
+
<div class="title">📊 积分查询结果</div>
|
|
995
|
+
<div class="username">${user.username}</div>
|
|
996
|
+
<div class="subtitle">用户ID: ${user.id}</div>
|
|
997
|
+
<div class="info-grid">
|
|
998
|
+
<div class="info-item">
|
|
999
|
+
<div class="info-label">当前积分</div>
|
|
1000
|
+
<div class="info-value score">${user.total_score}</div>
|
|
1001
|
+
</div>
|
|
1002
|
+
<div class="info-item">
|
|
1003
|
+
<div class="info-label">排行榜</div>
|
|
1004
|
+
<div class="info-value rank">#${ranking}</div>
|
|
1005
|
+
</div>
|
|
1006
|
+
</div>
|
|
1007
|
+
<div class="footer">班级操行分管理系统 v1.0</div>
|
|
1008
|
+
</div>
|
|
1009
|
+
</body>
|
|
1010
|
+
</html>`;
|
|
1011
|
+
await sendImageOrText(ctx, session, html, textResult, { height: 600 });
|
|
1012
|
+
return "";
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
ctx.logger.error("查询积分失败:", error);
|
|
1015
|
+
return ERROR_MESSAGES.API_REQUEST_FAILED;
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
__name(registerQueryScoreCommand, "registerQueryScoreCommand");
|
|
1020
|
+
|
|
1021
|
+
// src/commands/bind-qq.ts
|
|
1022
|
+
function registerBindQqCommand(ctx, serverService, bindingService) {
|
|
1023
|
+
ctx.command("绑定QQ <用户名:string>").alias("bind-qq").action(async ({ session }, username) => {
|
|
1024
|
+
if (!username) {
|
|
1025
|
+
return HELP_MESSAGES.BIND_QQ;
|
|
1026
|
+
}
|
|
1027
|
+
if (!session.guildId) {
|
|
1028
|
+
return "此命令只能在群聊中使用";
|
|
1029
|
+
}
|
|
1030
|
+
const usernameError = Validator.validateUsernameWithMessage(username);
|
|
1031
|
+
if (usernameError) {
|
|
1032
|
+
return usernameError;
|
|
1033
|
+
}
|
|
1034
|
+
const openId = session.userId?.toString();
|
|
1035
|
+
if (!openId) {
|
|
1036
|
+
return "无法获取您的用户ID";
|
|
1037
|
+
}
|
|
1038
|
+
ctx.logger.info(`获取到的用户 OpenID: ${openId}`);
|
|
1039
|
+
const config = await serverService.getConfigByGuild(session.guildId);
|
|
1040
|
+
if (!config) {
|
|
1041
|
+
return ERROR_MESSAGES.NO_SERVER_CONFIG;
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
const existingBinding = await bindingService.getUserByOpenId(session.guildId, openId);
|
|
1045
|
+
if (existingBinding) {
|
|
1046
|
+
return `❌ 您已绑定「${existingBinding.username}」,如需更换账号请先使用 /解除绑定 解绑后重新绑定`;
|
|
1047
|
+
}
|
|
1048
|
+
const existingBindings = await bindingService.getBindingsByUsername(session.guildId, username);
|
|
1049
|
+
if (existingBindings.length > 0) {
|
|
1050
|
+
return `❌ 用户名「${username}」已被其他QQ账号绑定,每个用户名只能绑定一个QQ`;
|
|
1051
|
+
}
|
|
1052
|
+
const api = await serverService.createApiService(session.guildId);
|
|
1053
|
+
if (!api) {
|
|
1054
|
+
return ERROR_MESSAGES.NO_SERVER_CONFIG;
|
|
1055
|
+
}
|
|
1056
|
+
const response = await api.getUserByUsername(username);
|
|
1057
|
+
if (response.error || !response.data || response.data.length === 0) {
|
|
1058
|
+
return ERROR_MESSAGES.USER_NOT_FOUND;
|
|
1059
|
+
}
|
|
1060
|
+
const user = response.data[0];
|
|
1061
|
+
await bindingService.bindQq(session.guildId, user.id, username, openId, config.id);
|
|
1062
|
+
return `✅ 绑定成功
|
|
1063
|
+
用户名: ${username}`;
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
ctx.logger.error("绑定QQ失败:", error);
|
|
1066
|
+
return ERROR_MESSAGES.BINDING_FAILED;
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
ctx.command("解除绑定").alias("unbind").action(async ({ session }, username) => {
|
|
1070
|
+
if (!session.guildId) {
|
|
1071
|
+
return "此命令只能在群聊中使用";
|
|
1072
|
+
}
|
|
1073
|
+
const openId = session.userId?.toString();
|
|
1074
|
+
if (!openId) {
|
|
1075
|
+
return "无法获取您的用户ID";
|
|
1076
|
+
}
|
|
1077
|
+
let binding = null;
|
|
1078
|
+
if (username) {
|
|
1079
|
+
const bindings = await bindingService.getBindingsByUsername(session.guildId, username);
|
|
1080
|
+
binding = bindings.find((b) => b.openId === openId);
|
|
1081
|
+
} else {
|
|
1082
|
+
binding = await bindingService.getUserByOpenId(session.guildId, openId);
|
|
1083
|
+
}
|
|
1084
|
+
if (!binding) {
|
|
1085
|
+
return `❌ 您尚未绑定任何账号${username ? `(用户:${username})` : ""}`;
|
|
1086
|
+
}
|
|
1087
|
+
await session.send(`⚠️ 确定要解除「${binding.username}」的绑定吗?请在 30 秒内输入「确认」来完成操作`);
|
|
1088
|
+
const reply = await session.prompt(3e4);
|
|
1089
|
+
if (reply?.trim() !== "确认") {
|
|
1090
|
+
return "✅ 已取消解绑操作";
|
|
1091
|
+
}
|
|
1092
|
+
await bindingService.unbindQq(session.guildId, openId);
|
|
1093
|
+
ctx.logger.info(`用户 ${binding.username}(OpenID: ${openId})解除了绑定`);
|
|
1094
|
+
return `✅ 已解除「${binding.username}」的绑定`;
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
__name(registerBindQqCommand, "registerBindQqCommand");
|
|
1098
|
+
|
|
1099
|
+
// src/commands/adjust-score.ts
|
|
1100
|
+
function registerAdjustScoreCommand(ctx, serverService, adminService) {
|
|
1101
|
+
ctx.command("调整积分 <用户名:string> <分数:number> <原因:text>").alias("adjust-score").action(async ({ session }, username, score, reason) => {
|
|
1102
|
+
if (!username || score === void 0 || !reason) {
|
|
1103
|
+
return HELP_MESSAGES.ADJUST_SCORE;
|
|
1104
|
+
}
|
|
1105
|
+
if (!session.guildId) {
|
|
1106
|
+
return "此命令只能在群聊中使用";
|
|
1107
|
+
}
|
|
1108
|
+
const hasPermission = await adminService.hasAdminPermission(session);
|
|
1109
|
+
if (!hasPermission) {
|
|
1110
|
+
return ERROR_MESSAGES.INSUFFICIENT_PERMISSION;
|
|
1111
|
+
}
|
|
1112
|
+
const usernameError = Validator.validateUsernameWithMessage(username);
|
|
1113
|
+
if (usernameError) {
|
|
1114
|
+
return usernameError;
|
|
1115
|
+
}
|
|
1116
|
+
const scoreError = Validator.validateScoreWithMessage(score);
|
|
1117
|
+
if (scoreError) {
|
|
1118
|
+
return scoreError;
|
|
1119
|
+
}
|
|
1120
|
+
const descriptionError = Validator.validateDescriptionWithMessage(reason);
|
|
1121
|
+
if (descriptionError) {
|
|
1122
|
+
return descriptionError;
|
|
1123
|
+
}
|
|
1124
|
+
const api = await serverService.createApiService(session.guildId);
|
|
1125
|
+
if (!api) {
|
|
1126
|
+
return ERROR_MESSAGES.NO_SERVER_CONFIG;
|
|
1127
|
+
}
|
|
1128
|
+
try {
|
|
1129
|
+
const data = {
|
|
1130
|
+
users: [
|
|
1131
|
+
{
|
|
1132
|
+
username,
|
|
1133
|
+
score_change: score
|
|
1134
|
+
}
|
|
1135
|
+
],
|
|
1136
|
+
description: reason
|
|
1137
|
+
};
|
|
1138
|
+
ctx.logger.info(`调整积分请求: 用户=${username}, 分数=${score}, 原因=${reason}`);
|
|
1139
|
+
const result = await api.batchAddScore(data);
|
|
1140
|
+
ctx.logger.info(`调整积分结果:`, JSON.stringify(result));
|
|
1141
|
+
const message = Formatter.formatAddScoreResult(result);
|
|
1142
|
+
return Formatter.splitMessage(message);
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
ctx.logger.error("调整积分失败:", error);
|
|
1145
|
+
return ERROR_MESSAGES.API_REQUEST_FAILED;
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
__name(registerAdjustScoreCommand, "registerAdjustScoreCommand");
|
|
1150
|
+
|
|
1151
|
+
// src/commands/ranking.ts
|
|
1152
|
+
var PAGE_SIZE = 10;
|
|
1153
|
+
function registerRankingCommand(ctx, serverService) {
|
|
1154
|
+
ctx.command("排行榜 [页码:number]").alias("ranking").action(async ({ session }, page = 1) => {
|
|
1155
|
+
if (!session.guildId) {
|
|
1156
|
+
return "此命令只能在群聊中使用";
|
|
1157
|
+
}
|
|
1158
|
+
const api = await serverService.createApiService(session.guildId);
|
|
1159
|
+
if (!api) {
|
|
1160
|
+
return ERROR_MESSAGES.NO_SERVER_CONFIG;
|
|
1161
|
+
}
|
|
1162
|
+
try {
|
|
1163
|
+
const allResponse = await api.getRanking(1e4, 0);
|
|
1164
|
+
if (allResponse.error || !allResponse.data) {
|
|
1165
|
+
return ERROR_MESSAGES.API_REQUEST_FAILED;
|
|
1166
|
+
}
|
|
1167
|
+
const totalUsers = allResponse.data.length;
|
|
1168
|
+
const totalPages = Math.ceil(totalUsers / PAGE_SIZE);
|
|
1169
|
+
if (page < 1) page = 1;
|
|
1170
|
+
if (page > totalPages && totalPages > 0) page = totalPages;
|
|
1171
|
+
const offset = (page - 1) * PAGE_SIZE;
|
|
1172
|
+
const response = await api.getRanking(PAGE_SIZE, offset);
|
|
1173
|
+
if (response.error || !response.data) {
|
|
1174
|
+
return ERROR_MESSAGES.API_REQUEST_FAILED;
|
|
1175
|
+
}
|
|
1176
|
+
if (response.data.length === 0) {
|
|
1177
|
+
return `暂无排行数据`;
|
|
1178
|
+
}
|
|
1179
|
+
const startRank = offset + 1;
|
|
1180
|
+
const message = Formatter.formatRankingWithRank(response.data, startRank);
|
|
1181
|
+
const pageInfo = totalPages > 1 ? `第 ${page}/${totalPages} 页,共 ${totalUsers} 人` : `共 ${totalUsers} 人`;
|
|
1182
|
+
const rankItems = response.data.map((user, index) => {
|
|
1183
|
+
const rank = startRank + index;
|
|
1184
|
+
const medal = rank === 1 ? "🥇" : rank === 2 ? "🥈" : rank === 3 ? "🥉" : `${rank}.`;
|
|
1185
|
+
const scoreColor = user.total_score >= 0 ? "#27ae60" : "#e74c3c";
|
|
1186
|
+
return `
|
|
1187
|
+
<div class="rank-item">
|
|
1188
|
+
<span class="rank-num">${medal}</span>
|
|
1189
|
+
<span class="rank-name">${user.username}</span>
|
|
1190
|
+
<span class="rank-score" style="color: ${scoreColor}">${user.total_score >= 0 ? "+" : ""}${user.total_score}</span>
|
|
1191
|
+
</div>`;
|
|
1192
|
+
}).join("");
|
|
1193
|
+
const html = `
|
|
1194
|
+
<!DOCTYPE html>
|
|
1195
|
+
<html>
|
|
1196
|
+
<head>
|
|
1197
|
+
<meta charset="utf-8">
|
|
1198
|
+
<style>
|
|
1199
|
+
${getBaseStyles()}
|
|
1200
|
+
.rank-list {
|
|
1201
|
+
max-height: 550px;
|
|
1202
|
+
overflow-y: auto;
|
|
1203
|
+
}
|
|
1204
|
+
.rank-item {
|
|
1205
|
+
display: flex;
|
|
1206
|
+
align-items: center;
|
|
1207
|
+
padding: 10px 12px;
|
|
1208
|
+
margin-bottom: 8px;
|
|
1209
|
+
background: rgba(52, 152, 219, 0.08);
|
|
1210
|
+
border-radius: 10px;
|
|
1211
|
+
}
|
|
1212
|
+
.rank-num {
|
|
1213
|
+
width: 40px;
|
|
1214
|
+
font-size: 16px;
|
|
1215
|
+
font-weight: bold;
|
|
1216
|
+
color: #3498db;
|
|
1217
|
+
}
|
|
1218
|
+
.rank-name {
|
|
1219
|
+
flex: 1;
|
|
1220
|
+
color: #2c3e50;
|
|
1221
|
+
font-size: 14px;
|
|
1222
|
+
overflow: hidden;
|
|
1223
|
+
text-overflow: ellipsis;
|
|
1224
|
+
white-space: nowrap;
|
|
1225
|
+
}
|
|
1226
|
+
.rank-score {
|
|
1227
|
+
font-size: 14px;
|
|
1228
|
+
font-weight: bold;
|
|
1229
|
+
margin-left: 12px;
|
|
1230
|
+
}
|
|
1231
|
+
.page-info {
|
|
1232
|
+
text-align: center;
|
|
1233
|
+
color: #95a5a6;
|
|
1234
|
+
font-size: 12px;
|
|
1235
|
+
margin-top: 16px;
|
|
1236
|
+
padding-top: 12px;
|
|
1237
|
+
border-top: 1px dashed #eee;
|
|
1238
|
+
}
|
|
1239
|
+
.title-icon {
|
|
1240
|
+
font-size: 28px;
|
|
1241
|
+
display: block;
|
|
1242
|
+
margin-bottom: 8px;
|
|
1243
|
+
}
|
|
1244
|
+
</style>
|
|
1245
|
+
</head>
|
|
1246
|
+
<body>
|
|
1247
|
+
<div class="container">
|
|
1248
|
+
<div class="title"><span class="title-icon">🏆</span>积分排行榜</div>
|
|
1249
|
+
<div class="rank-list">
|
|
1250
|
+
${rankItems}
|
|
1251
|
+
</div>
|
|
1252
|
+
<div class="page-info">${pageInfo}</div>
|
|
1253
|
+
<div class="footer">班级操行分管理系统 v1.0</div>
|
|
1254
|
+
</div>
|
|
1255
|
+
</body>
|
|
1256
|
+
</html>`;
|
|
1257
|
+
const textResult = message + (totalPages > 1 ? `
|
|
1258
|
+
|
|
1259
|
+
📄 ${pageInfo}` : "");
|
|
1260
|
+
await sendImageOrText(ctx, session, html, textResult, { height: 820 });
|
|
1261
|
+
return "";
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
ctx.logger.error("获取排行榜失败:", error);
|
|
1264
|
+
return ERROR_MESSAGES.API_REQUEST_FAILED;
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
__name(registerRankingCommand, "registerRankingCommand");
|
|
1269
|
+
|
|
1270
|
+
// src/commands/statistics.ts
|
|
1271
|
+
function registerStatisticsCommand(ctx, serverService, bindingService) {
|
|
1272
|
+
ctx.command("统计 [用户名:string]").alias("statistics").action(async ({ session }, username) => {
|
|
1273
|
+
if (!session.guildId) {
|
|
1274
|
+
return "此命令只能在群聊中使用";
|
|
1275
|
+
}
|
|
1276
|
+
const api = await serverService.createApiService(session.guildId);
|
|
1277
|
+
if (!api) {
|
|
1278
|
+
return ERROR_MESSAGES.NO_SERVER_CONFIG;
|
|
1279
|
+
}
|
|
1280
|
+
try {
|
|
1281
|
+
let targetUsername = username;
|
|
1282
|
+
if (!username) {
|
|
1283
|
+
const openId = session.userId?.toString();
|
|
1284
|
+
if (!openId) {
|
|
1285
|
+
return "无法获取您的用户ID";
|
|
1286
|
+
}
|
|
1287
|
+
const binding = await bindingService.getUserByOpenId(session.guildId, openId);
|
|
1288
|
+
if (!binding) {
|
|
1289
|
+
return `${ERROR_MESSAGES.USER_NOT_BOUND}
|
|
1290
|
+
|
|
1291
|
+
请先使用 /绑定QQ <用户名> 命令绑定您的账号`;
|
|
1292
|
+
}
|
|
1293
|
+
targetUsername = binding.username;
|
|
1294
|
+
}
|
|
1295
|
+
const userResponse = await api.getUserByUsername(targetUsername);
|
|
1296
|
+
if (userResponse.error || !userResponse.data || userResponse.data.length === 0) {
|
|
1297
|
+
return ERROR_MESSAGES.USER_NOT_FOUND;
|
|
1298
|
+
}
|
|
1299
|
+
const user = userResponse.data[0];
|
|
1300
|
+
const totalScore = Number(user.total_score) || 0;
|
|
1301
|
+
const addScore = Number(user.add_score) || 0;
|
|
1302
|
+
const deductScore = Math.abs(Number(user.deduct_score)) || 0;
|
|
1303
|
+
const scoreCount = Number(user.score_count) || 0;
|
|
1304
|
+
const html = `
|
|
1305
|
+
<!DOCTYPE html>
|
|
1306
|
+
<html>
|
|
1307
|
+
<head>
|
|
1308
|
+
<meta charset="utf-8">
|
|
1309
|
+
<style>
|
|
1310
|
+
${getBaseStyles()}
|
|
1311
|
+
.user-info {
|
|
1312
|
+
text-align: center;
|
|
1313
|
+
margin-bottom: 20px;
|
|
1314
|
+
}
|
|
1315
|
+
.username {
|
|
1316
|
+
font-size: 20px;
|
|
1317
|
+
font-weight: bold;
|
|
1318
|
+
color: #2c3e50;
|
|
1319
|
+
}
|
|
1320
|
+
.user-id {
|
|
1321
|
+
color: #95a5a6;
|
|
1322
|
+
font-size: 11px;
|
|
1323
|
+
margin-top: 4px;
|
|
1324
|
+
}
|
|
1325
|
+
.stats-grid {
|
|
1326
|
+
display: grid;
|
|
1327
|
+
grid-template-columns: 1fr 1fr;
|
|
1328
|
+
gap: 12px;
|
|
1329
|
+
margin-top: 16px;
|
|
1330
|
+
}
|
|
1331
|
+
.stat-card {
|
|
1332
|
+
background: rgba(52, 152, 219, 0.1);
|
|
1333
|
+
border-radius: 12px;
|
|
1334
|
+
padding: 14px;
|
|
1335
|
+
text-align: center;
|
|
1336
|
+
}
|
|
1337
|
+
.stat-card.positive {
|
|
1338
|
+
background: rgba(39, 174, 96, 0.1);
|
|
1339
|
+
}
|
|
1340
|
+
.stat-card.negative {
|
|
1341
|
+
background: rgba(231, 76, 60, 0.1);
|
|
1342
|
+
}
|
|
1343
|
+
.stat-card.total {
|
|
1344
|
+
background: rgba(155, 89, 182, 0.1);
|
|
1345
|
+
}
|
|
1346
|
+
.stat-label {
|
|
1347
|
+
color: #7f8c8d;
|
|
1348
|
+
font-size: 11px;
|
|
1349
|
+
margin-bottom: 6px;
|
|
1350
|
+
}
|
|
1351
|
+
.stat-value {
|
|
1352
|
+
font-size: 20px;
|
|
1353
|
+
font-weight: bold;
|
|
1354
|
+
color: #2c3e50;
|
|
1355
|
+
}
|
|
1356
|
+
.stat-value.positive { color: #27ae60; }
|
|
1357
|
+
.stat-value.negative { color: #e74c3c; }
|
|
1358
|
+
.stat-value.total { color: #9b59b6; }
|
|
1359
|
+
</style>
|
|
1360
|
+
</head>
|
|
1361
|
+
<body>
|
|
1362
|
+
<div class="container">
|
|
1363
|
+
<div class="title">📊 积分统计</div>
|
|
1364
|
+
<div class="user-info">
|
|
1365
|
+
<div class="username">${user.username}</div>
|
|
1366
|
+
<div class="user-id">用户ID: ${user.id} | 当前积分: ${user.total_score}</div>
|
|
1367
|
+
</div>
|
|
1368
|
+
<div class="stats-grid">
|
|
1369
|
+
<div class="stat-card positive">
|
|
1370
|
+
<div class="stat-label">累计加分</div>
|
|
1371
|
+
<div class="stat-value positive">+${addScore}</div>
|
|
1372
|
+
</div>
|
|
1373
|
+
<div class="stat-card negative">
|
|
1374
|
+
<div class="stat-label">累计扣分</div>
|
|
1375
|
+
<div class="stat-value negative">-${deductScore}</div>
|
|
1376
|
+
</div>
|
|
1377
|
+
<div class="stat-card total">
|
|
1378
|
+
<div class="stat-label">总积分</div>
|
|
1379
|
+
<div class="stat-value total">${totalScore}</div>
|
|
1380
|
+
</div>
|
|
1381
|
+
<div class="stat-card">
|
|
1382
|
+
<div class="stat-label">操作次数</div>
|
|
1383
|
+
<div class="stat-value">${scoreCount}</div>
|
|
1384
|
+
</div>
|
|
1385
|
+
</div>
|
|
1386
|
+
<div class="footer">班级操行分管理系统 v1.0</div>
|
|
1387
|
+
</div>
|
|
1388
|
+
</body>
|
|
1389
|
+
</html>`;
|
|
1390
|
+
const textResult = [
|
|
1391
|
+
`【${user.username} 的统计信息】`,
|
|
1392
|
+
`总积分: ${user.total_score}`,
|
|
1393
|
+
`累计加分: +${addScore}`,
|
|
1394
|
+
`累计扣分: -${deductScore}`,
|
|
1395
|
+
`操作次数: ${scoreCount}`
|
|
1396
|
+
].join("\n");
|
|
1397
|
+
await sendImageOrText(ctx, session, html, textResult, { height: 550 });
|
|
1398
|
+
return "";
|
|
1399
|
+
} catch (error) {
|
|
1400
|
+
ctx.logger.error("获取统计信息失败:", error);
|
|
1401
|
+
return ERROR_MESSAGES.API_REQUEST_FAILED;
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
__name(registerStatisticsCommand, "registerStatisticsCommand");
|
|
1406
|
+
|
|
1407
|
+
// src/commands/admin-manager.ts
|
|
1408
|
+
import { h as h2 } from "koishi";
|
|
1409
|
+
var pendingRequests = /* @__PURE__ */ new Map();
|
|
1410
|
+
function generateCode() {
|
|
1411
|
+
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
1412
|
+
let code = "";
|
|
1413
|
+
for (let i = 0; i < 6; i++) {
|
|
1414
|
+
code += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
1415
|
+
}
|
|
1416
|
+
return code;
|
|
1417
|
+
}
|
|
1418
|
+
__name(generateCode, "generateCode");
|
|
1419
|
+
function cleanupExpiredRequests() {
|
|
1420
|
+
const now = Date.now();
|
|
1421
|
+
for (const [code, req] of pendingRequests) {
|
|
1422
|
+
if (now - req.createdAt > 12e4) {
|
|
1423
|
+
pendingRequests.delete(code);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
__name(cleanupExpiredRequests, "cleanupExpiredRequests");
|
|
1428
|
+
setInterval(cleanupExpiredRequests, 3e4);
|
|
1429
|
+
function registerGroupAdminCommand(ctx, adminService) {
|
|
1430
|
+
function getAtId(session) {
|
|
1431
|
+
if (session.elements) {
|
|
1432
|
+
const atElements = h2.select(session.elements, "at");
|
|
1433
|
+
if (atElements.length > 0) {
|
|
1434
|
+
return String(atElements[0].attrs?.id || "");
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
return "";
|
|
1438
|
+
}
|
|
1439
|
+
__name(getAtId, "getAtId");
|
|
1440
|
+
ctx.command("加管 [备注:string]").alias("add-admin").action(async ({ session }, remark) => {
|
|
1441
|
+
if (!session.guildId) {
|
|
1442
|
+
return "此命令只能在群聊中使用";
|
|
1443
|
+
}
|
|
1444
|
+
const hasPermission = await adminService.hasAdminPermission(session);
|
|
1445
|
+
if (!hasPermission) {
|
|
1446
|
+
return "❌ 此命令仅限管理员使用";
|
|
1447
|
+
}
|
|
1448
|
+
const targetId = getAtId(session);
|
|
1449
|
+
if (!targetId) {
|
|
1450
|
+
return "请用 @ 指定要添加为群管的用户:/加管 @某人";
|
|
1451
|
+
}
|
|
1452
|
+
cleanupExpiredRequests();
|
|
1453
|
+
for (const [, req] of pendingRequests) {
|
|
1454
|
+
if (req.guildId === session.guildId) {
|
|
1455
|
+
const admins = await adminService.getAllAdmins(session.guildId);
|
|
1456
|
+
const existing = admins.find((a) => a.verify === 0);
|
|
1457
|
+
if (existing) {
|
|
1458
|
+
return `❌ 该用户已有正在等待确认的加管请求`;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const adminRemark = remark || "未命名";
|
|
1463
|
+
const code = generateCode();
|
|
1464
|
+
pendingRequests.set(code, {
|
|
1465
|
+
guildId: session.guildId,
|
|
1466
|
+
remark: adminRemark,
|
|
1467
|
+
createdAt: Date.now()
|
|
1468
|
+
});
|
|
1469
|
+
await session.send(
|
|
1470
|
+
`✅ 已发起加管请求
|
|
1471
|
+
` + h2.at(targetId) + ` 请发送 /确认 ${code} 同意加管
|
|
1472
|
+
备注:${adminRemark}
|
|
1473
|
+
(120秒内有效)`
|
|
1474
|
+
);
|
|
1475
|
+
return "";
|
|
1476
|
+
});
|
|
1477
|
+
ctx.command("确认 [确认码:string]").alias("confirm").action(async ({ session }, code) => {
|
|
1478
|
+
if (!session.guildId) {
|
|
1479
|
+
return "此命令只能在群聊中使用";
|
|
1480
|
+
}
|
|
1481
|
+
const openId = session.userId?.toString();
|
|
1482
|
+
if (!openId) {
|
|
1483
|
+
return "无法获取您的用户ID";
|
|
1484
|
+
}
|
|
1485
|
+
if (!code) {
|
|
1486
|
+
return "请提供确认码:/确认 [确认码]";
|
|
1487
|
+
}
|
|
1488
|
+
cleanupExpiredRequests();
|
|
1489
|
+
const req = pendingRequests.get(code);
|
|
1490
|
+
if (!req) {
|
|
1491
|
+
return "❌ 无效的确认码或已过期";
|
|
1492
|
+
}
|
|
1493
|
+
if (req.guildId !== session.guildId) {
|
|
1494
|
+
return "❌ 请在发起加管请求的群聊中确认";
|
|
1495
|
+
}
|
|
1496
|
+
const remark = req.remark;
|
|
1497
|
+
pendingRequests.delete(code);
|
|
1498
|
+
await adminService.createPendingAdmin(session.guildId, openId, remark);
|
|
1499
|
+
ctx.logger.info(`群聊 ${session.guildId} 用户同意加管: OpenID=${openId}, 备注=${remark}, Verify=0`);
|
|
1500
|
+
await session.send(
|
|
1501
|
+
`✅ 您已同意加管
|
|
1502
|
+
您的 OpenID:${openId}
|
|
1503
|
+
备注:${remark}
|
|
1504
|
+
等待管理员执行 /加管确认 完成加管
|
|
1505
|
+
(120秒内有效)`
|
|
1506
|
+
);
|
|
1507
|
+
return "";
|
|
1508
|
+
});
|
|
1509
|
+
ctx.command("加管确认 [确认码:string]").alias("confirm-add-admin").action(async ({ session }, code) => {
|
|
1510
|
+
if (!session.guildId) {
|
|
1511
|
+
return "此命令只能在群聊中使用";
|
|
1512
|
+
}
|
|
1513
|
+
const hasPermission = await adminService.hasAdminPermission(session);
|
|
1514
|
+
if (!hasPermission) {
|
|
1515
|
+
return "❌ 此命令仅限管理员使用";
|
|
1516
|
+
}
|
|
1517
|
+
if (!code) {
|
|
1518
|
+
return "请提供确认码:/加管确认 [确认码]";
|
|
1519
|
+
}
|
|
1520
|
+
const admins = await adminService.getAllAdmins(session.guildId);
|
|
1521
|
+
const pendingAdmin = admins.find((a) => a.verify === 0);
|
|
1522
|
+
if (!pendingAdmin) {
|
|
1523
|
+
return "❌ 没有待确认的加管请求";
|
|
1524
|
+
}
|
|
1525
|
+
await adminService.confirmAdmin(session.guildId, pendingAdmin.openId);
|
|
1526
|
+
ctx.logger.info(`群聊 ${session.guildId} 添加群管完成: OpenID=${pendingAdmin.openId}, 备注=${pendingAdmin.remark}, Verify=1`);
|
|
1527
|
+
return `✅ 加管成功
|
|
1528
|
+
备注:${pendingAdmin.remark}
|
|
1529
|
+
OpenID:${pendingAdmin.openId}`;
|
|
1530
|
+
});
|
|
1531
|
+
ctx.command("减管 [备注:string]").alias("remove-admin").action(async ({ session }, remark) => {
|
|
1532
|
+
if (!session.guildId) {
|
|
1533
|
+
return "此命令只能在群聊中使用";
|
|
1534
|
+
}
|
|
1535
|
+
const hasPermission = await adminService.hasAdminPermission(session);
|
|
1536
|
+
if (!hasPermission) {
|
|
1537
|
+
return "❌ 此命令仅限管理员使用";
|
|
1538
|
+
}
|
|
1539
|
+
if (!remark) {
|
|
1540
|
+
return "请提供要移除的群管备注:/减管 [备注]";
|
|
1541
|
+
}
|
|
1542
|
+
await adminService.removeAdminByRemark(session.guildId, remark);
|
|
1543
|
+
ctx.logger.info(`群聊 ${session.guildId} 移除群管: 备注=${remark}`);
|
|
1544
|
+
return `✅ 已移除群管
|
|
1545
|
+
备注:${remark}`;
|
|
1546
|
+
});
|
|
1547
|
+
ctx.command("群管列表").alias("admin-list").action(async ({ session }) => {
|
|
1548
|
+
if (!session.guildId) {
|
|
1549
|
+
return "此命令只能在群聊中使用";
|
|
1550
|
+
}
|
|
1551
|
+
const admins = await adminService.getAllAdmins(session.guildId);
|
|
1552
|
+
if (admins.length === 0) {
|
|
1553
|
+
return "当前群聊没有管理员";
|
|
1554
|
+
}
|
|
1555
|
+
const list = admins.map(
|
|
1556
|
+
(admin, index) => `${index + 1}. [${admin.remark}] ${admin.openId} ${admin.verify === 1 ? "✅" : "⏳"}`
|
|
1557
|
+
).join("\n");
|
|
1558
|
+
const adminItems = admins.map((admin, index) => {
|
|
1559
|
+
const status = admin.verify === 1 ? "✅ 已验证" : "⏳ 待确认";
|
|
1560
|
+
const statusColor = admin.verify === 1 ? "#27ae60" : "#f39c12";
|
|
1561
|
+
return `
|
|
1562
|
+
<div class="admin-item">
|
|
1563
|
+
<div class="admin-left">
|
|
1564
|
+
<span class="admin-num">${index + 1}</span>
|
|
1565
|
+
<span class="admin-remark">${admin.remark}</span>
|
|
1566
|
+
</div>
|
|
1567
|
+
<div class="admin-right">
|
|
1568
|
+
<span class="admin-status" style="color: ${statusColor}">${status}</span>
|
|
1569
|
+
</div>
|
|
1570
|
+
</div>
|
|
1571
|
+
<div class="admin-id">${admin.openId}</div>`;
|
|
1572
|
+
}).join("");
|
|
1573
|
+
const html = `
|
|
1574
|
+
<!DOCTYPE html>
|
|
1575
|
+
<html>
|
|
1576
|
+
<head>
|
|
1577
|
+
<meta charset="utf-8">
|
|
1578
|
+
<style>
|
|
1579
|
+
${getBaseStyles()}
|
|
1580
|
+
.admin-list {
|
|
1581
|
+
max-height: 500px;
|
|
1582
|
+
overflow-y: auto;
|
|
1583
|
+
}
|
|
1584
|
+
.admin-item {
|
|
1585
|
+
display: flex;
|
|
1586
|
+
justify-content: space-between;
|
|
1587
|
+
align-items: center;
|
|
1588
|
+
padding: 12px 14px;
|
|
1589
|
+
background: rgba(52, 152, 219, 0.08);
|
|
1590
|
+
border-radius: 10px;
|
|
1591
|
+
margin-bottom: 8px;
|
|
1592
|
+
}
|
|
1593
|
+
.admin-left {
|
|
1594
|
+
display: flex;
|
|
1595
|
+
align-items: center;
|
|
1596
|
+
gap: 10px;
|
|
1597
|
+
}
|
|
1598
|
+
.admin-num {
|
|
1599
|
+
width: 24px;
|
|
1600
|
+
height: 24px;
|
|
1601
|
+
background: #3498db;
|
|
1602
|
+
color: white;
|
|
1603
|
+
border-radius: 50%;
|
|
1604
|
+
display: flex;
|
|
1605
|
+
align-items: center;
|
|
1606
|
+
justify-content: center;
|
|
1607
|
+
font-size: 12px;
|
|
1608
|
+
font-weight: bold;
|
|
1609
|
+
}
|
|
1610
|
+
.admin-remark {
|
|
1611
|
+
color: #2c3e50;
|
|
1612
|
+
font-size: 14px;
|
|
1613
|
+
font-weight: 500;
|
|
1614
|
+
}
|
|
1615
|
+
.admin-status {
|
|
1616
|
+
font-size: 12px;
|
|
1617
|
+
font-weight: bold;
|
|
1618
|
+
}
|
|
1619
|
+
.admin-id {
|
|
1620
|
+
font-size: 10px;
|
|
1621
|
+
color: #bdc3c7;
|
|
1622
|
+
margin-top: -4px;
|
|
1623
|
+
margin-bottom: 8px;
|
|
1624
|
+
padding-left: 48px;
|
|
1625
|
+
word-break: break-all;
|
|
1626
|
+
}
|
|
1627
|
+
.count-badge {
|
|
1628
|
+
text-align: center;
|
|
1629
|
+
background: rgba(155, 89, 182, 0.1);
|
|
1630
|
+
border-radius: 20px;
|
|
1631
|
+
padding: 8px;
|
|
1632
|
+
margin-bottom: 16px;
|
|
1633
|
+
color: #9b59b6;
|
|
1634
|
+
font-size: 13px;
|
|
1635
|
+
}
|
|
1636
|
+
</style>
|
|
1637
|
+
</head>
|
|
1638
|
+
<body>
|
|
1639
|
+
<div class="container">
|
|
1640
|
+
<div class="title">👥 群管列表</div>
|
|
1641
|
+
<div class="count-badge">共 ${admins.length} 位管理员</div>
|
|
1642
|
+
<div class="admin-list">
|
|
1643
|
+
${adminItems}
|
|
1644
|
+
</div>
|
|
1645
|
+
<div class="footer">班级操行分管理系统 v1.0</div>
|
|
1646
|
+
</div>
|
|
1647
|
+
</body>
|
|
1648
|
+
</html>`;
|
|
1649
|
+
await sendImageOrText(ctx, session, html, `当前群聊的管理员:
|
|
1650
|
+
${list}`, { height: 600 });
|
|
1651
|
+
return "";
|
|
1652
|
+
});
|
|
1653
|
+
ctx.command("我的ID").alias("my-id").action(async ({ session }) => {
|
|
1654
|
+
if (!session.guildId) {
|
|
1655
|
+
return "此命令只能在群聊中使用";
|
|
1656
|
+
}
|
|
1657
|
+
const openId = session.userId?.toString();
|
|
1658
|
+
if (!openId) {
|
|
1659
|
+
return "无法获取您的用户ID";
|
|
1660
|
+
}
|
|
1661
|
+
return `您的 OpenID:
|
|
1662
|
+
${openId}`;
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
__name(registerGroupAdminCommand, "registerGroupAdminCommand");
|
|
1666
|
+
|
|
1667
|
+
// src/commands/menu.ts
|
|
1668
|
+
import { h as h3 } from "koishi";
|
|
1669
|
+
var MENU_HTML = `
|
|
1670
|
+
<!DOCTYPE html>
|
|
1671
|
+
<html>
|
|
1672
|
+
<head>
|
|
1673
|
+
<meta charset="utf-8">
|
|
1674
|
+
<style>
|
|
1675
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1676
|
+
body {
|
|
1677
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
1678
|
+
background: url('https://api.yppp.net/pe.php') center/cover no-repeat;
|
|
1679
|
+
width: 450px;
|
|
1680
|
+
min-height: 800px;
|
|
1681
|
+
display: flex;
|
|
1682
|
+
justify-content: center;
|
|
1683
|
+
align-items: flex-start;
|
|
1684
|
+
padding: 40px 0;
|
|
1685
|
+
}
|
|
1686
|
+
.container {
|
|
1687
|
+
background: rgba(255, 255, 255, 0.5);
|
|
1688
|
+
border-radius: 24px;
|
|
1689
|
+
padding: 28px 24px;
|
|
1690
|
+
width: 380px;
|
|
1691
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
|
1692
|
+
margin: 0 auto;
|
|
1693
|
+
}
|
|
1694
|
+
.title {
|
|
1695
|
+
text-align: center;
|
|
1696
|
+
font-size: 22px;
|
|
1697
|
+
font-weight: bold;
|
|
1698
|
+
color: #2c3e50;
|
|
1699
|
+
margin-bottom: 20px;
|
|
1700
|
+
padding-bottom: 12px;
|
|
1701
|
+
border-bottom: 2px solid #3498db;
|
|
1702
|
+
}
|
|
1703
|
+
.section {
|
|
1704
|
+
margin-bottom: 16px;
|
|
1705
|
+
}
|
|
1706
|
+
.section-title {
|
|
1707
|
+
font-size: 14px;
|
|
1708
|
+
font-weight: bold;
|
|
1709
|
+
color: #3498db;
|
|
1710
|
+
margin-bottom: 10px;
|
|
1711
|
+
display: flex;
|
|
1712
|
+
align-items: center;
|
|
1713
|
+
gap: 6px;
|
|
1714
|
+
}
|
|
1715
|
+
.section-title::before {
|
|
1716
|
+
content: '';
|
|
1717
|
+
display: inline-block;
|
|
1718
|
+
width: 3px;
|
|
1719
|
+
height: 16px;
|
|
1720
|
+
background: linear-gradient(180deg, #3498db, #9b59b6);
|
|
1721
|
+
border-radius: 2px;
|
|
1722
|
+
}
|
|
1723
|
+
.command-list {
|
|
1724
|
+
list-style: none;
|
|
1725
|
+
padding-left: 8px;
|
|
1726
|
+
}
|
|
1727
|
+
.command-item {
|
|
1728
|
+
padding: 6px 0;
|
|
1729
|
+
color: #555;
|
|
1730
|
+
font-size: 12px;
|
|
1731
|
+
border-bottom: 1px dashed #eee;
|
|
1732
|
+
display: flex;
|
|
1733
|
+
align-items: flex-start;
|
|
1734
|
+
}
|
|
1735
|
+
.command-item:last-child {
|
|
1736
|
+
border-bottom: none;
|
|
1737
|
+
}
|
|
1738
|
+
.command-item::before {
|
|
1739
|
+
content: '•';
|
|
1740
|
+
color: #9b59b6;
|
|
1741
|
+
font-weight: bold;
|
|
1742
|
+
margin-right: 6px;
|
|
1743
|
+
flex-shrink: 0;
|
|
1744
|
+
}
|
|
1745
|
+
.command-name {
|
|
1746
|
+
color: #2c3e50;
|
|
1747
|
+
font-weight: 600;
|
|
1748
|
+
background: #ecf0f1;
|
|
1749
|
+
padding: 1px 5px;
|
|
1750
|
+
border-radius: 3px;
|
|
1751
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
1752
|
+
font-size: 11px;
|
|
1753
|
+
white-space: nowrap;
|
|
1754
|
+
}
|
|
1755
|
+
.command-desc {
|
|
1756
|
+
color: #7f8c8d;
|
|
1757
|
+
font-size: 11px;
|
|
1758
|
+
margin-left: 4px;
|
|
1759
|
+
}
|
|
1760
|
+
.footer {
|
|
1761
|
+
text-align: center;
|
|
1762
|
+
margin-top: 16px;
|
|
1763
|
+
padding-top: 12px;
|
|
1764
|
+
border-top: 1px solid #eee;
|
|
1765
|
+
color: #95a5a6;
|
|
1766
|
+
font-size: 10px;
|
|
1767
|
+
}
|
|
1768
|
+
</style>
|
|
1769
|
+
</head>
|
|
1770
|
+
<body>
|
|
1771
|
+
<div class="container">
|
|
1772
|
+
<div class="title">📋 班级操行分管理系统菜单</div>
|
|
1773
|
+
|
|
1774
|
+
<div class="section">
|
|
1775
|
+
<div class="section-title">🛠️ 基础指令</div>
|
|
1776
|
+
<ul class="command-list">
|
|
1777
|
+
<li class="command-item">
|
|
1778
|
+
<span class="command-name">/查询积分</span>
|
|
1779
|
+
<span class="command-desc">[用户名]</span>
|
|
1780
|
+
</li>
|
|
1781
|
+
<li class="command-item">
|
|
1782
|
+
<span class="command-name">/绑定QQ</span>
|
|
1783
|
+
<span class="command-desc"><用户名></span>
|
|
1784
|
+
</li>
|
|
1785
|
+
<li class="command-item">
|
|
1786
|
+
<span class="command-name">/排行榜</span>
|
|
1787
|
+
<span class="command-desc">[数量]</span>
|
|
1788
|
+
</li>
|
|
1789
|
+
<li class="command-item">
|
|
1790
|
+
<span class="command-name">/统计</span>
|
|
1791
|
+
<span class="command-desc">[用户名]</span>
|
|
1792
|
+
</li>
|
|
1793
|
+
</ul>
|
|
1794
|
+
</div>
|
|
1795
|
+
|
|
1796
|
+
<div class="section">
|
|
1797
|
+
<div class="section-title">⚙️ 管理员指令</div>
|
|
1798
|
+
<ul class="command-list">
|
|
1799
|
+
<li class="command-item">
|
|
1800
|
+
<span class="command-name">/绑定服务器</span>
|
|
1801
|
+
<span class="command-desc"><地址> <用户> <token></span>
|
|
1802
|
+
</li>
|
|
1803
|
+
<li class="command-item">
|
|
1804
|
+
<span class="command-name">/服务器解绑</span>
|
|
1805
|
+
</li>
|
|
1806
|
+
<li class="command-item">
|
|
1807
|
+
<span class="command-name">/调整积分</span>
|
|
1808
|
+
<span class="command-desc"><用户> <分数> <原因></span>
|
|
1809
|
+
</li>
|
|
1810
|
+
</ul>
|
|
1811
|
+
</div>
|
|
1812
|
+
|
|
1813
|
+
<div class="section">
|
|
1814
|
+
<div class="section-title">👤 群管指令</div>
|
|
1815
|
+
<ul class="command-list">
|
|
1816
|
+
<li class="command-item">
|
|
1817
|
+
<span class="command-name">/加管</span>
|
|
1818
|
+
<span class="command-desc">@某人 <备注></span>
|
|
1819
|
+
</li>
|
|
1820
|
+
<li class="command-item">
|
|
1821
|
+
<span class="command-name">/确认</span>
|
|
1822
|
+
<span class="command-desc"><验证码></span>
|
|
1823
|
+
</li>
|
|
1824
|
+
<li class="command-item">
|
|
1825
|
+
<span class="command-name">/加管确认</span>
|
|
1826
|
+
</li>
|
|
1827
|
+
<li class="command-item">
|
|
1828
|
+
<span class="command-name">/减管</span>
|
|
1829
|
+
<span class="command-desc"><备注></span>
|
|
1830
|
+
</li>
|
|
1831
|
+
<li class="command-item">
|
|
1832
|
+
<span class="command-name">/群管列表</span>
|
|
1833
|
+
</li>
|
|
1834
|
+
</ul>
|
|
1835
|
+
</div>
|
|
1836
|
+
|
|
1837
|
+
<div class="footer">班级操行分管理系统 v1.0</div>
|
|
1838
|
+
</div>
|
|
1839
|
+
</body>
|
|
1840
|
+
</html>
|
|
1841
|
+
`;
|
|
1842
|
+
function registerMenuCommand(ctx) {
|
|
1843
|
+
ctx.command("菜单").alias("menu").action(async ({ session }) => {
|
|
1844
|
+
if (!session.guildId) {
|
|
1845
|
+
return "此命令只能在群聊中使用";
|
|
1846
|
+
}
|
|
1847
|
+
try {
|
|
1848
|
+
const page = await ctx.puppeteer.page();
|
|
1849
|
+
await page.setViewport({
|
|
1850
|
+
width: 450,
|
|
1851
|
+
height: 800,
|
|
1852
|
+
deviceScaleFactor: 2
|
|
1853
|
+
});
|
|
1854
|
+
await page.setContent(MENU_HTML, { waitUntil: "networkidle0" });
|
|
1855
|
+
await page.evaluateHandle("document.fonts.ready");
|
|
1856
|
+
const screenshot = await page.screenshot({
|
|
1857
|
+
type: "png",
|
|
1858
|
+
clip: { x: 0, y: 0, width: 450, height: 800 }
|
|
1859
|
+
});
|
|
1860
|
+
await session.send(h3.image(screenshot, "image/png"));
|
|
1861
|
+
return "";
|
|
1862
|
+
} catch (error) {
|
|
1863
|
+
ctx.logger.error("生成菜单图片失败:", error);
|
|
1864
|
+
return `📋 **班级操行分管理系统菜单**
|
|
1865
|
+
|
|
1866
|
+
━━━━━━━━━━━━━━━━━━━━━━
|
|
1867
|
+
🛠️ **基础指令**
|
|
1868
|
+
━━━━━━━━━━━━━━━━━━━━━━
|
|
1869
|
+
• /查询积分 [用户名] - 查询积分和排名
|
|
1870
|
+
• /绑定QQ <用户名> - 绑定QQ号与系统账号
|
|
1871
|
+
• /排行榜 [数量] - 显示积分排行榜
|
|
1872
|
+
• /统计 [用户名] - 显示积分统计信息
|
|
1873
|
+
|
|
1874
|
+
━━━━━━━━━━━━━━━━━━━━━━
|
|
1875
|
+
⚙️ **管理员指令**
|
|
1876
|
+
━━━━━━━━━━━━━━━━━━━━━━
|
|
1877
|
+
• /绑定服务器 <地址> <用户名> <token> - 绑定CSMS服务器
|
|
1878
|
+
• /服务器解绑 - 解除服务器绑定
|
|
1879
|
+
• /调整积分 <用户名> <分数> <原因> - 调整积分
|
|
1880
|
+
|
|
1881
|
+
━━━━━━━━━━━━━━━━━━━━━━
|
|
1882
|
+
👤 **群管指令**
|
|
1883
|
+
━━━━━━━━━━━━━━━━━━━━━━
|
|
1884
|
+
• /加管 @某人 <备注> - 添加管理员
|
|
1885
|
+
• /确认 <验证码> - 确认加管
|
|
1886
|
+
• /加管确认 - 完成加管流程
|
|
1887
|
+
• /减管 <备注> - 移除管理员
|
|
1888
|
+
• /群管列表 - 显示管理员列表
|
|
1889
|
+
|
|
1890
|
+
━━━━━━━━━━━━━━━━━━━━━━`;
|
|
1891
|
+
}
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
__name(registerMenuCommand, "registerMenuCommand");
|
|
1895
|
+
|
|
1896
|
+
// src/index.ts
|
|
1897
|
+
var name = "class-score-system";
|
|
1898
|
+
var inject = ["database", "puppeteer"];
|
|
1899
|
+
var Config = Schema.object({});
|
|
1900
|
+
function apply(ctx) {
|
|
1901
|
+
ctx.logger.info("班级操行分管理系统插件正在加载...");
|
|
1902
|
+
extendDatabase(ctx);
|
|
1903
|
+
const serverService = new ServerConfigService(ctx);
|
|
1904
|
+
const bindingService = new QqBindingService(ctx);
|
|
1905
|
+
const adminService = new GroupAdminService(ctx);
|
|
1906
|
+
registerGroupAdminCommand(ctx, adminService);
|
|
1907
|
+
registerBindServerCommand(ctx, serverService, adminService);
|
|
1908
|
+
registerQueryScoreCommand(ctx, serverService, bindingService);
|
|
1909
|
+
registerBindQqCommand(ctx, serverService, bindingService);
|
|
1910
|
+
registerAdjustScoreCommand(ctx, serverService, adminService);
|
|
1911
|
+
registerRankingCommand(ctx, serverService);
|
|
1912
|
+
registerStatisticsCommand(ctx, serverService, bindingService);
|
|
1913
|
+
registerMenuCommand(ctx);
|
|
1914
|
+
ctx.logger.info("班级操行分管理系统插件加载完成");
|
|
1915
|
+
}
|
|
1916
|
+
__name(apply, "apply");
|
|
1917
|
+
export {
|
|
1918
|
+
Config,
|
|
1919
|
+
apply,
|
|
1920
|
+
inject,
|
|
1921
|
+
name
|
|
1922
|
+
};
|