@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/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: config.statsInternal ?? Time.minute * 10,
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: ["date", "hour", "type", "selfId", "platform"],
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.any().before("command/execute", ({ command, session }) => {
161
- if (!command || !session) return;
162
- // 观察字段在类型上不可索引,按宽泛视图读取( createIndex 同理)
163
- // biome-ignore lint/suspicious/noExplicitAny: Session<never,...> 的 user
164
- const user = (session as Session<any, any, any>).user;
165
- this.addAudit(this.commands, {
166
- ...this.createIndex(session),
167
- name: command.name,
168
- // 库表列声明为 integer,而 user.id 为字符串,这里按数值归一
169
- userId: +(user?.["id"] || 0),
170
- channelId: session.channelId ?? "",
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
- // biome-ignore lint/suspicious/noExplicitAny: Session 泛型在 user 观察字段上
183
- private createIndex(session: Session<any, any, any>): Analytics.Index {
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: session.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(pick(data, Object.keys(index) as (keyof T)[]), index),
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 > this.config.statsInternal ||
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("analytics.message", this.messages),
246
- this.uploadAudit("analytics.command", this.commands),
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: Time.getDateNumber() - this.config.recentDayCount,
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(lengthTask: Promise<number>) {
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: { $gte: Time.getDateNumber() - this.config.recentDayCount },
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(this.config.recentDayCount + 1).fill(0);
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(lengthTask: Promise<number>) {
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<Dict<MessageStats & Partial<Universal.User>>>;
375
+ const result = {} as Dict<
376
+ Dict<MessageStats & Partial<Universal.User>>
377
+ >;
325
378
  data.forEach((stat) => {
326
- const bot = this.ctx.bots[`${stat.platform}:${stat.selfId}`];
327
- const entry = ((result[stat.platform] ||= {})[stat.selfId] ||= {
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"] = stat.count / length;
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] ||= { send: 0, receive: 0 });
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(lengthTask: Promise<number>) {
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"] = stat.count / length;
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(Math.max(data.length - 1, 1), this.config.recentDayCount);
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) => $.count(row.id)),
483
+ this.ctx.database.eval("user", (row) =>
484
+ $.count(row.id),
485
+ ),
418
486
  // 昨日新增用户数(createdAt 落在昨天一整天)
419
- this.ctx.database.eval("user", (row) => $.count(row.id), {
420
- createdAt: {
421
- $gte: Time.fromDateNumber(Time.getDateNumber() - 1),
422
- $lt: Time.fromDateNumber(Time.getDateNumber()),
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(row.createdAt, Time.fromDateNumber(Time.getDateNumber() - 1)),
439
- $.lt(row.createdAt, Time.fromDateNumber(Time.getDateNumber())),
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(date, date.getTimezoneOffset());
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<Dict<MessageStats & Partial<Universal.User>>>;
597
+ messageByBot: Dict<
598
+ Dict<MessageStats & Partial<Universal.User>>
599
+ >;
515
600
  messageByDate: MessageStats[];
516
601
  messageByHour: MessageStats[];
517
602
  }