@koishi-ce/plugin-analytics 1.0.1 → 1.1.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/dist/{echarts-D6RpzsTY.js → echarts-BMfAiWrm.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/style.css +1 -1
- package/lib/assets/de-DE-C35VOgt9.yml +6 -0
- package/lib/assets/en-US-C8mb89z7.yml +6 -0
- package/lib/assets/fr-FR-WljZXOoA.yml +6 -0
- package/lib/assets/ja-JP-DP_l11TV.yml +6 -0
- package/lib/assets/ru-RU-DWF7MRwv.yml +6 -0
- package/lib/assets/zh-CN-Cof4p_74.yml +6 -0
- package/lib/assets/zh-TW-_jfudK1s.yml +6 -0
- package/lib/index.d.ts +2 -2
- package/lib/index.mjs +15 -0
- package/package.json +4 -4
- package/src/index.test.ts +68 -23
- package/src/index.ts +126 -44
package/src/index.ts
CHANGED
|
@@ -28,6 +28,13 @@ import {
|
|
|
28
28
|
Time,
|
|
29
29
|
type Universal,
|
|
30
30
|
} from "@koishi-ce/koishi";
|
|
31
|
+
import deDE from "../locales/de-DE.yml";
|
|
32
|
+
import enUS from "../locales/en-US.yml";
|
|
33
|
+
import frFR from "../locales/fr-FR.yml";
|
|
34
|
+
import jaJP from "../locales/ja-JP.yml";
|
|
35
|
+
import ruRU from "../locales/ru-RU.yml";
|
|
36
|
+
import zhCN from "../locales/zh-CN.yml";
|
|
37
|
+
import zhTW from "../locales/zh-TW.yml";
|
|
31
38
|
|
|
32
39
|
declare module "@koishi-ce/koishi" {
|
|
33
40
|
interface Tables {
|
|
@@ -65,6 +72,14 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
65
72
|
recentDayCount: Schema.natural()
|
|
66
73
|
.description("统计最近几天的数据。")
|
|
67
74
|
.default(7),
|
|
75
|
+
}).i18n({
|
|
76
|
+
"de-DE": deDE,
|
|
77
|
+
"en-US": enUS,
|
|
78
|
+
"fr-FR": frFR,
|
|
79
|
+
"ja-JP": jaJP,
|
|
80
|
+
"ru-RU": ruRU,
|
|
81
|
+
"zh-CN": zhCN,
|
|
82
|
+
"zh-TW": zhTW,
|
|
68
83
|
});
|
|
69
84
|
|
|
70
85
|
// 基类 Service 声明了 config: unknown;此处收敛为必填并在构造时归一化
|
|
@@ -89,7 +104,8 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
89
104
|
super(ctx, "analytics");
|
|
90
105
|
|
|
91
106
|
this.config = {
|
|
92
|
-
statsInternal:
|
|
107
|
+
statsInternal:
|
|
108
|
+
config.statsInternal ?? Time.minute * 10,
|
|
93
109
|
recentDayCount: config.recentDayCount ?? 7,
|
|
94
110
|
};
|
|
95
111
|
|
|
@@ -105,7 +121,13 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
105
121
|
count: "integer",
|
|
106
122
|
},
|
|
107
123
|
{
|
|
108
|
-
primary: [
|
|
124
|
+
primary: [
|
|
125
|
+
"date",
|
|
126
|
+
"hour",
|
|
127
|
+
"type",
|
|
128
|
+
"selfId",
|
|
129
|
+
"platform",
|
|
130
|
+
],
|
|
109
131
|
},
|
|
110
132
|
);
|
|
111
133
|
|
|
@@ -157,20 +179,23 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
157
179
|
this.upload();
|
|
158
180
|
});
|
|
159
181
|
|
|
160
|
-
ctx
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
182
|
+
ctx
|
|
183
|
+
.any()
|
|
184
|
+
.before("command/execute", ({ command, session }) => {
|
|
185
|
+
if (!command || !session) return;
|
|
186
|
+
// 观察字段在类型上不可索引,按宽泛视图读取(与 createIndex 同理)
|
|
187
|
+
// biome-ignore lint/suspicious/noExplicitAny: Session<never,...> 的 user
|
|
188
|
+
const user = (session as Session<any, any, any>)
|
|
189
|
+
.user;
|
|
190
|
+
this.addAudit(this.commands, {
|
|
191
|
+
...this.createIndex(session),
|
|
192
|
+
name: command.name,
|
|
193
|
+
// 库表列声明为 integer,而 user.id 为字符串,这里按数值归一
|
|
194
|
+
userId: +(user?.["id"] || 0),
|
|
195
|
+
channelId: session.channelId ?? "",
|
|
196
|
+
});
|
|
197
|
+
this.upload();
|
|
171
198
|
});
|
|
172
|
-
this.upload();
|
|
173
|
-
});
|
|
174
199
|
|
|
175
200
|
ctx.console.addEntry({
|
|
176
201
|
dev: resolve(__dirname, "../client/index.ts"),
|
|
@@ -179,8 +204,10 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
179
204
|
}
|
|
180
205
|
|
|
181
206
|
// 不协变,各事件回调处的具体泛型互不相同,内部工具方法统一放宽
|
|
182
|
-
|
|
183
|
-
|
|
207
|
+
private createIndex(
|
|
208
|
+
// biome-ignore lint/suspicious/noExplicitAny: Session 泛型在 user 观察字段上
|
|
209
|
+
session: Session<any, any, any>,
|
|
210
|
+
): Analytics.Index {
|
|
184
211
|
return {
|
|
185
212
|
selfId: session.selfId,
|
|
186
213
|
platform: session.platform,
|
|
@@ -198,7 +225,10 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
198
225
|
index: Omit<T, "count">,
|
|
199
226
|
) {
|
|
200
227
|
const audit = buffer.find((data) =>
|
|
201
|
-
deepEqual(
|
|
228
|
+
deepEqual(
|
|
229
|
+
pick(data, Object.keys(index) as (keyof T)[]),
|
|
230
|
+
index,
|
|
231
|
+
),
|
|
202
232
|
);
|
|
203
233
|
if (audit) {
|
|
204
234
|
audit.count += 1;
|
|
@@ -236,14 +266,21 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
236
266
|
const dateHour = date.getHours();
|
|
237
267
|
if (
|
|
238
268
|
forced ||
|
|
239
|
-
+date - +this.lastUpdate >
|
|
269
|
+
+date - +this.lastUpdate >
|
|
270
|
+
this.config.statsInternal ||
|
|
240
271
|
dateHour !== this.updateHour
|
|
241
272
|
) {
|
|
242
273
|
this.lastUpdate = date;
|
|
243
274
|
this.updateHour = dateHour;
|
|
244
275
|
await Promise.all([
|
|
245
|
-
this.uploadAudit(
|
|
246
|
-
|
|
276
|
+
this.uploadAudit(
|
|
277
|
+
"analytics.message",
|
|
278
|
+
this.messages,
|
|
279
|
+
),
|
|
280
|
+
this.uploadAudit(
|
|
281
|
+
"analytics.command",
|
|
282
|
+
this.commands,
|
|
283
|
+
),
|
|
247
284
|
]);
|
|
248
285
|
logger.debug("analytics updated");
|
|
249
286
|
}
|
|
@@ -252,7 +289,8 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
252
289
|
/** 最近 N 天(不含今天)的日期号查询区间,供各聚合查询复用。 */
|
|
253
290
|
private queryRecent(): Query.FieldExpr<number> {
|
|
254
291
|
return {
|
|
255
|
-
$gte:
|
|
292
|
+
$gte:
|
|
293
|
+
Time.getDateNumber() - this.config.recentDayCount,
|
|
256
294
|
$lt: Time.getDateNumber(),
|
|
257
295
|
};
|
|
258
296
|
}
|
|
@@ -263,7 +301,9 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
263
301
|
*
|
|
264
302
|
* @param lengthTask 参与平均的天数(见 download 中的计算)
|
|
265
303
|
*/
|
|
266
|
-
private async getCommandRate(
|
|
304
|
+
private async getCommandRate(
|
|
305
|
+
lengthTask: Promise<number>,
|
|
306
|
+
) {
|
|
267
307
|
const data = await this.ctx.database
|
|
268
308
|
.select("analytics.command", {
|
|
269
309
|
date: this.queryRecent(),
|
|
@@ -288,14 +328,20 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
288
328
|
private async getDauHistory() {
|
|
289
329
|
const data = await this.ctx.database
|
|
290
330
|
.select("analytics.command", {
|
|
291
|
-
date: {
|
|
331
|
+
date: {
|
|
332
|
+
$gte:
|
|
333
|
+
Time.getDateNumber() -
|
|
334
|
+
this.config.recentDayCount,
|
|
335
|
+
},
|
|
292
336
|
userId: { $gt: 0 },
|
|
293
337
|
})
|
|
294
338
|
.groupBy(["date"], {
|
|
295
339
|
count: (row) => $.count(row.userId),
|
|
296
340
|
})
|
|
297
341
|
.execute();
|
|
298
|
-
const result: number[] = new Array(
|
|
342
|
+
const result: number[] = new Array(
|
|
343
|
+
this.config.recentDayCount + 1,
|
|
344
|
+
).fill(0);
|
|
299
345
|
const today = Time.getDateNumber();
|
|
300
346
|
data.forEach((stat) => {
|
|
301
347
|
result[today - stat.date] = stat.count;
|
|
@@ -310,7 +356,9 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
310
356
|
*
|
|
311
357
|
* @param lengthTask 参与平均的天数(见 download 中的计算)
|
|
312
358
|
*/
|
|
313
|
-
private async getMessageByBot(
|
|
359
|
+
private async getMessageByBot(
|
|
360
|
+
lengthTask: Promise<number>,
|
|
361
|
+
) {
|
|
314
362
|
const data = await this.ctx.database
|
|
315
363
|
.select("analytics.message", {
|
|
316
364
|
date: this.queryRecent(),
|
|
@@ -321,16 +369,22 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
321
369
|
.execute();
|
|
322
370
|
const length = await lengthTask;
|
|
323
371
|
// 机器人资料(bot.user)运行时可能缺席,按 Partial 记录
|
|
324
|
-
const result = {} as Dict<
|
|
372
|
+
const result = {} as Dict<
|
|
373
|
+
Dict<MessageStats & Partial<Universal.User>>
|
|
374
|
+
>;
|
|
325
375
|
data.forEach((stat) => {
|
|
326
|
-
const bot =
|
|
327
|
-
|
|
376
|
+
const bot =
|
|
377
|
+
this.ctx.bots[`${stat.platform}:${stat.selfId}`];
|
|
378
|
+
const entry = ((result[stat.platform] ||= {})[
|
|
379
|
+
stat.selfId
|
|
380
|
+
] ||= {
|
|
328
381
|
...(bot?.user ?? {}),
|
|
329
382
|
send: 0,
|
|
330
383
|
receive: 0,
|
|
331
384
|
});
|
|
332
385
|
// type 列的取值集合由写入端约定为 send / receive 两种
|
|
333
|
-
entry[stat.type as "send" | "receive"] =
|
|
386
|
+
entry[stat.type as "send" | "receive"] =
|
|
387
|
+
stat.count / length;
|
|
334
388
|
});
|
|
335
389
|
return result;
|
|
336
390
|
}
|
|
@@ -353,7 +407,10 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
353
407
|
const today = Time.getDateNumber();
|
|
354
408
|
const result: MessageStats[] = [];
|
|
355
409
|
data.forEach((stat) => {
|
|
356
|
-
const entry = (result[today - stat.date] ||= {
|
|
410
|
+
const entry = (result[today - stat.date] ||= {
|
|
411
|
+
send: 0,
|
|
412
|
+
receive: 0,
|
|
413
|
+
});
|
|
357
414
|
entry[stat.type as "send" | "receive"] = stat.count;
|
|
358
415
|
});
|
|
359
416
|
for (let i = 0; i < result.length; i++) {
|
|
@@ -368,7 +425,9 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
368
425
|
*
|
|
369
426
|
* @param lengthTask 参与平均的天数(见 download 中的计算)
|
|
370
427
|
*/
|
|
371
|
-
private async getMessageByHour(
|
|
428
|
+
private async getMessageByHour(
|
|
429
|
+
lengthTask: Promise<number>,
|
|
430
|
+
) {
|
|
372
431
|
const data = await this.ctx.database
|
|
373
432
|
.select("analytics.message", {
|
|
374
433
|
date: this.queryRecent(),
|
|
@@ -384,7 +443,8 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
384
443
|
data.forEach((stat) => {
|
|
385
444
|
const entry = result[stat.hour];
|
|
386
445
|
if (!entry) return;
|
|
387
|
-
entry[stat.type as "send" | "receive"] =
|
|
446
|
+
entry[stat.type as "send" | "receive"] =
|
|
447
|
+
stat.count / length;
|
|
388
448
|
});
|
|
389
449
|
return result;
|
|
390
450
|
}
|
|
@@ -400,7 +460,10 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
400
460
|
async download(): Promise<Analytics.Payload> {
|
|
401
461
|
const messageByDateTask = this.getMessageByDate();
|
|
402
462
|
const lengthTask = messageByDateTask.then((data) => {
|
|
403
|
-
return Math.min(
|
|
463
|
+
return Math.min(
|
|
464
|
+
Math.max(data.length - 1, 1),
|
|
465
|
+
this.config.recentDayCount,
|
|
466
|
+
);
|
|
404
467
|
});
|
|
405
468
|
const [
|
|
406
469
|
userCount,
|
|
@@ -414,14 +477,22 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
414
477
|
messageByHour,
|
|
415
478
|
] = await Promise.all([
|
|
416
479
|
// 用户总数
|
|
417
|
-
this.ctx.database.eval("user", (row) =>
|
|
480
|
+
this.ctx.database.eval("user", (row) =>
|
|
481
|
+
$.count(row.id),
|
|
482
|
+
),
|
|
418
483
|
// 昨日新增用户数(createdAt 落在昨天一整天)
|
|
419
|
-
this.ctx.database.eval(
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
484
|
+
this.ctx.database.eval(
|
|
485
|
+
"user",
|
|
486
|
+
(row) => $.count(row.id),
|
|
487
|
+
{
|
|
488
|
+
createdAt: {
|
|
489
|
+
$gte: Time.fromDateNumber(
|
|
490
|
+
Time.getDateNumber() - 1,
|
|
491
|
+
),
|
|
492
|
+
$lt: Time.fromDateNumber(Time.getDateNumber()),
|
|
493
|
+
},
|
|
423
494
|
},
|
|
424
|
-
|
|
495
|
+
),
|
|
425
496
|
// 群组总数:channel 表中 id === guildId 的行即群本身(而非普通子频道)
|
|
426
497
|
this.ctx.database.eval(
|
|
427
498
|
"channel",
|
|
@@ -435,8 +506,14 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
435
506
|
(row) =>
|
|
436
507
|
$.and(
|
|
437
508
|
$.eq(row.id, row.guildId),
|
|
438
|
-
$.gte(
|
|
439
|
-
|
|
509
|
+
$.gte(
|
|
510
|
+
row.createdAt,
|
|
511
|
+
Time.fromDateNumber(Time.getDateNumber() - 1),
|
|
512
|
+
),
|
|
513
|
+
$.lt(
|
|
514
|
+
row.createdAt,
|
|
515
|
+
Time.fromDateNumber(Time.getDateNumber()),
|
|
516
|
+
),
|
|
440
517
|
),
|
|
441
518
|
),
|
|
442
519
|
this.getCommandRate(lengthTask),
|
|
@@ -465,7 +542,10 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
465
542
|
*/
|
|
466
543
|
override async get() {
|
|
467
544
|
const date = new Date();
|
|
468
|
-
const dateNumber = Time.getDateNumber(
|
|
545
|
+
const dateNumber = Time.getDateNumber(
|
|
546
|
+
date,
|
|
547
|
+
date.getTimezoneOffset(),
|
|
548
|
+
);
|
|
469
549
|
if (dateNumber !== this.cachedDate) {
|
|
470
550
|
this.cachedData = this.download();
|
|
471
551
|
this.cachedDate = dateNumber;
|
|
@@ -511,7 +591,9 @@ namespace Analytics {
|
|
|
511
591
|
guildIncrement: number;
|
|
512
592
|
dauHistory: number[];
|
|
513
593
|
commandRate: Dict<number>;
|
|
514
|
-
messageByBot: Dict<
|
|
594
|
+
messageByBot: Dict<
|
|
595
|
+
Dict<MessageStats & Partial<Universal.User>>
|
|
596
|
+
>;
|
|
515
597
|
messageByDate: MessageStats[];
|
|
516
598
|
messageByHour: MessageStats[];
|
|
517
599
|
}
|