@koishi-ce/plugin-analytics 1.0.2 → 1.1.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/dist/echarts-B6DtxUFq.js +19 -0
- 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 +16 -1
- package/package.json +4 -3
- package/src/index.test.ts +92 -25
- package/src/index.ts +130 -45
- package/dist/echarts-BMfAiWrm.js +0 -19
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,10 +204,15 @@ 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
|
-
selfId
|
|
212
|
+
// selfId 为联合主键之一且列非空,沙盒等无 selfId 的会话落
|
|
213
|
+
// 空串行,避免整批 upsert 因非空约束失败丢统计
|
|
214
|
+
// upstream: koishijs/koishi#1501
|
|
215
|
+
selfId: session.selfId ?? "",
|
|
186
216
|
platform: session.platform,
|
|
187
217
|
date: Time.getDateNumber(),
|
|
188
218
|
hour: new Date().getHours(),
|
|
@@ -198,7 +228,10 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
198
228
|
index: Omit<T, "count">,
|
|
199
229
|
) {
|
|
200
230
|
const audit = buffer.find((data) =>
|
|
201
|
-
deepEqual(
|
|
231
|
+
deepEqual(
|
|
232
|
+
pick(data, Object.keys(index) as (keyof T)[]),
|
|
233
|
+
index,
|
|
234
|
+
),
|
|
202
235
|
);
|
|
203
236
|
if (audit) {
|
|
204
237
|
audit.count += 1;
|
|
@@ -236,14 +269,21 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
236
269
|
const dateHour = date.getHours();
|
|
237
270
|
if (
|
|
238
271
|
forced ||
|
|
239
|
-
+date - +this.lastUpdate >
|
|
272
|
+
+date - +this.lastUpdate >
|
|
273
|
+
this.config.statsInternal ||
|
|
240
274
|
dateHour !== this.updateHour
|
|
241
275
|
) {
|
|
242
276
|
this.lastUpdate = date;
|
|
243
277
|
this.updateHour = dateHour;
|
|
244
278
|
await Promise.all([
|
|
245
|
-
this.uploadAudit(
|
|
246
|
-
|
|
279
|
+
this.uploadAudit(
|
|
280
|
+
"analytics.message",
|
|
281
|
+
this.messages,
|
|
282
|
+
),
|
|
283
|
+
this.uploadAudit(
|
|
284
|
+
"analytics.command",
|
|
285
|
+
this.commands,
|
|
286
|
+
),
|
|
247
287
|
]);
|
|
248
288
|
logger.debug("analytics updated");
|
|
249
289
|
}
|
|
@@ -252,7 +292,8 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
252
292
|
/** 最近 N 天(不含今天)的日期号查询区间,供各聚合查询复用。 */
|
|
253
293
|
private queryRecent(): Query.FieldExpr<number> {
|
|
254
294
|
return {
|
|
255
|
-
$gte:
|
|
295
|
+
$gte:
|
|
296
|
+
Time.getDateNumber() - this.config.recentDayCount,
|
|
256
297
|
$lt: Time.getDateNumber(),
|
|
257
298
|
};
|
|
258
299
|
}
|
|
@@ -263,7 +304,9 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
263
304
|
*
|
|
264
305
|
* @param lengthTask 参与平均的天数(见 download 中的计算)
|
|
265
306
|
*/
|
|
266
|
-
private async getCommandRate(
|
|
307
|
+
private async getCommandRate(
|
|
308
|
+
lengthTask: Promise<number>,
|
|
309
|
+
) {
|
|
267
310
|
const data = await this.ctx.database
|
|
268
311
|
.select("analytics.command", {
|
|
269
312
|
date: this.queryRecent(),
|
|
@@ -288,14 +331,20 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
288
331
|
private async getDauHistory() {
|
|
289
332
|
const data = await this.ctx.database
|
|
290
333
|
.select("analytics.command", {
|
|
291
|
-
date: {
|
|
334
|
+
date: {
|
|
335
|
+
$gte:
|
|
336
|
+
Time.getDateNumber() -
|
|
337
|
+
this.config.recentDayCount,
|
|
338
|
+
},
|
|
292
339
|
userId: { $gt: 0 },
|
|
293
340
|
})
|
|
294
341
|
.groupBy(["date"], {
|
|
295
342
|
count: (row) => $.count(row.userId),
|
|
296
343
|
})
|
|
297
344
|
.execute();
|
|
298
|
-
const result: number[] = new Array(
|
|
345
|
+
const result: number[] = new Array(
|
|
346
|
+
this.config.recentDayCount + 1,
|
|
347
|
+
).fill(0);
|
|
299
348
|
const today = Time.getDateNumber();
|
|
300
349
|
data.forEach((stat) => {
|
|
301
350
|
result[today - stat.date] = stat.count;
|
|
@@ -310,7 +359,9 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
310
359
|
*
|
|
311
360
|
* @param lengthTask 参与平均的天数(见 download 中的计算)
|
|
312
361
|
*/
|
|
313
|
-
private async getMessageByBot(
|
|
362
|
+
private async getMessageByBot(
|
|
363
|
+
lengthTask: Promise<number>,
|
|
364
|
+
) {
|
|
314
365
|
const data = await this.ctx.database
|
|
315
366
|
.select("analytics.message", {
|
|
316
367
|
date: this.queryRecent(),
|
|
@@ -321,16 +372,22 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
321
372
|
.execute();
|
|
322
373
|
const length = await lengthTask;
|
|
323
374
|
// 机器人资料(bot.user)运行时可能缺席,按 Partial 记录
|
|
324
|
-
const result = {} as Dict<
|
|
375
|
+
const result = {} as Dict<
|
|
376
|
+
Dict<MessageStats & Partial<Universal.User>>
|
|
377
|
+
>;
|
|
325
378
|
data.forEach((stat) => {
|
|
326
|
-
const bot =
|
|
327
|
-
|
|
379
|
+
const bot =
|
|
380
|
+
this.ctx.bots[`${stat.platform}:${stat.selfId}`];
|
|
381
|
+
const entry = ((result[stat.platform] ||= {})[
|
|
382
|
+
stat.selfId
|
|
383
|
+
] ||= {
|
|
328
384
|
...(bot?.user ?? {}),
|
|
329
385
|
send: 0,
|
|
330
386
|
receive: 0,
|
|
331
387
|
});
|
|
332
388
|
// type 列的取值集合由写入端约定为 send / receive 两种
|
|
333
|
-
entry[stat.type as "send" | "receive"] =
|
|
389
|
+
entry[stat.type as "send" | "receive"] =
|
|
390
|
+
stat.count / length;
|
|
334
391
|
});
|
|
335
392
|
return result;
|
|
336
393
|
}
|
|
@@ -353,7 +410,10 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
353
410
|
const today = Time.getDateNumber();
|
|
354
411
|
const result: MessageStats[] = [];
|
|
355
412
|
data.forEach((stat) => {
|
|
356
|
-
const entry = (result[today - stat.date] ||= {
|
|
413
|
+
const entry = (result[today - stat.date] ||= {
|
|
414
|
+
send: 0,
|
|
415
|
+
receive: 0,
|
|
416
|
+
});
|
|
357
417
|
entry[stat.type as "send" | "receive"] = stat.count;
|
|
358
418
|
});
|
|
359
419
|
for (let i = 0; i < result.length; i++) {
|
|
@@ -368,7 +428,9 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
368
428
|
*
|
|
369
429
|
* @param lengthTask 参与平均的天数(见 download 中的计算)
|
|
370
430
|
*/
|
|
371
|
-
private async getMessageByHour(
|
|
431
|
+
private async getMessageByHour(
|
|
432
|
+
lengthTask: Promise<number>,
|
|
433
|
+
) {
|
|
372
434
|
const data = await this.ctx.database
|
|
373
435
|
.select("analytics.message", {
|
|
374
436
|
date: this.queryRecent(),
|
|
@@ -384,7 +446,8 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
384
446
|
data.forEach((stat) => {
|
|
385
447
|
const entry = result[stat.hour];
|
|
386
448
|
if (!entry) return;
|
|
387
|
-
entry[stat.type as "send" | "receive"] =
|
|
449
|
+
entry[stat.type as "send" | "receive"] =
|
|
450
|
+
stat.count / length;
|
|
388
451
|
});
|
|
389
452
|
return result;
|
|
390
453
|
}
|
|
@@ -400,7 +463,10 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
400
463
|
async download(): Promise<Analytics.Payload> {
|
|
401
464
|
const messageByDateTask = this.getMessageByDate();
|
|
402
465
|
const lengthTask = messageByDateTask.then((data) => {
|
|
403
|
-
return Math.min(
|
|
466
|
+
return Math.min(
|
|
467
|
+
Math.max(data.length - 1, 1),
|
|
468
|
+
this.config.recentDayCount,
|
|
469
|
+
);
|
|
404
470
|
});
|
|
405
471
|
const [
|
|
406
472
|
userCount,
|
|
@@ -414,14 +480,22 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
414
480
|
messageByHour,
|
|
415
481
|
] = await Promise.all([
|
|
416
482
|
// 用户总数
|
|
417
|
-
this.ctx.database.eval("user", (row) =>
|
|
483
|
+
this.ctx.database.eval("user", (row) =>
|
|
484
|
+
$.count(row.id),
|
|
485
|
+
),
|
|
418
486
|
// 昨日新增用户数(createdAt 落在昨天一整天)
|
|
419
|
-
this.ctx.database.eval(
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
487
|
+
this.ctx.database.eval(
|
|
488
|
+
"user",
|
|
489
|
+
(row) => $.count(row.id),
|
|
490
|
+
{
|
|
491
|
+
createdAt: {
|
|
492
|
+
$gte: Time.fromDateNumber(
|
|
493
|
+
Time.getDateNumber() - 1,
|
|
494
|
+
),
|
|
495
|
+
$lt: Time.fromDateNumber(Time.getDateNumber()),
|
|
496
|
+
},
|
|
423
497
|
},
|
|
424
|
-
|
|
498
|
+
),
|
|
425
499
|
// 群组总数:channel 表中 id === guildId 的行即群本身(而非普通子频道)
|
|
426
500
|
this.ctx.database.eval(
|
|
427
501
|
"channel",
|
|
@@ -435,8 +509,14 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
435
509
|
(row) =>
|
|
436
510
|
$.and(
|
|
437
511
|
$.eq(row.id, row.guildId),
|
|
438
|
-
$.gte(
|
|
439
|
-
|
|
512
|
+
$.gte(
|
|
513
|
+
row.createdAt,
|
|
514
|
+
Time.fromDateNumber(Time.getDateNumber() - 1),
|
|
515
|
+
),
|
|
516
|
+
$.lt(
|
|
517
|
+
row.createdAt,
|
|
518
|
+
Time.fromDateNumber(Time.getDateNumber()),
|
|
519
|
+
),
|
|
440
520
|
),
|
|
441
521
|
),
|
|
442
522
|
this.getCommandRate(lengthTask),
|
|
@@ -465,7 +545,10 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
465
545
|
*/
|
|
466
546
|
override async get() {
|
|
467
547
|
const date = new Date();
|
|
468
|
-
const dateNumber = Time.getDateNumber(
|
|
548
|
+
const dateNumber = Time.getDateNumber(
|
|
549
|
+
date,
|
|
550
|
+
date.getTimezoneOffset(),
|
|
551
|
+
);
|
|
469
552
|
if (dateNumber !== this.cachedDate) {
|
|
470
553
|
this.cachedData = this.download();
|
|
471
554
|
this.cachedDate = dateNumber;
|
|
@@ -511,7 +594,9 @@ namespace Analytics {
|
|
|
511
594
|
guildIncrement: number;
|
|
512
595
|
dauHistory: number[];
|
|
513
596
|
commandRate: Dict<number>;
|
|
514
|
-
messageByBot: Dict<
|
|
597
|
+
messageByBot: Dict<
|
|
598
|
+
Dict<MessageStats & Partial<Universal.User>>
|
|
599
|
+
>;
|
|
515
600
|
messageByDate: MessageStats[];
|
|
516
601
|
messageByHour: MessageStats[];
|
|
517
602
|
}
|