koishi-plugin-chat-analyse 0.6.2 → 0.6.4

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/Renderer.d.ts CHANGED
@@ -19,6 +19,7 @@ export interface CircadianChartData {
19
19
  time: Date;
20
20
  total: string | number;
21
21
  data: number[];
22
+ labels?: string[];
22
23
  }
23
24
  /**
24
25
  * @class Renderer
package/lib/index.d.ts CHANGED
@@ -54,6 +54,7 @@ export declare function generateTitle(ctx: Context, scopeDesc: {
54
54
  main: string;
55
55
  subtype?: string;
56
56
  timeRange?: number;
57
+ timeUnit?: '小时' | '天';
57
58
  }): Promise<string>;
58
59
  /**
59
60
  * @function apply
package/lib/index.js CHANGED
@@ -425,16 +425,15 @@ var Renderer = class {
425
425
  * @returns {Promise<string | Buffer[]>} - 成功时返回包含图片 Buffer 的数组,失败或无数据时返回提示字符串。
426
426
  */
427
427
  async renderCircadianChart(data) {
428
- const { title, time, total, data: hourlyCounts } = data;
428
+ const { title, time, total, data: hourlyCounts, labels } = data;
429
429
  if (!hourlyCounts || hourlyCounts.every((c) => c === 0)) return "暂无数据可供渲染";
430
430
  const maxCount = Math.max(...hourlyCounts, 1);
431
431
  const chartStyles = `
432
432
  .chart-container { display: flex; align-items: flex-end; gap: 4px; height: 180px; padding: 30px 15px 10px; }
433
433
  .bar-wrapper { flex: 1; text-align: center; display: flex; flex-direction: column; justify-content: flex-end; height: 100%; }
434
- .bar-value { font-size: 11px; color: var(--sub-text-color); height: 16px; line-height: 16px; font-weight: 500; visibility: ${maxCount > 50 ? "hidden" : "visible"}; }
434
+ .bar-value { font-size: 11px; color: var(--sub-text-color); height: 16px; line-height: 16px; font-weight: 500; }
435
435
  .bar-container { flex-grow: 1; display: flex; align-items: flex-end; width: 100%; }
436
436
  .bar { width: 100%; background-color: var(--accent-color); opacity: .7; border-radius: 3px 3px 0 0; transition: height .3s ease-out; }
437
- .bar.peak { opacity: 1; background-color: var(--gold); }
438
437
  .bar-label { font-size: 10px; color: var(--sub-text-color); margin-top: 4px; height: 12px; }
439
438
  `;
440
439
  const cardHtml = `
@@ -449,9 +448,9 @@ var Renderer = class {
449
448
  <div class="bar-wrapper">
450
449
  <div class="bar-value">${count > 0 ? count : ""}</div>
451
450
  <div class="bar-container">
452
- <div class="bar ${count === maxCount ? "peak" : ""}" style="height: ${count / maxCount * 100}%;"></div>
451
+ <div class="bar" style="height: ${count / maxCount * 100}%;"></div>
453
452
  </div>
454
- <div class="bar-label">${hour}</div>
453
+ <div class="bar-label">${labels ? labels[hour] : hour}</div>
455
454
  </div>`).join("")}
456
455
  </div>
457
456
  </div>`;
@@ -615,17 +614,58 @@ var Stat = class {
615
614
  }));
616
615
  }
617
616
  if (this.config.enableActivity) {
618
- cmd.subcommand("activity", "活跃统计").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("all", "-a 全局").action(createHandler(async (scope) => {
619
- const hourlyStats = await this.ctx.database.select("analyse_rank").where({ uid: { $in: scope.uids } }).groupBy(["timestamp"], { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count") }).execute();
620
- if (hourlyStats.length === 0) return "暂无统计数据";
621
- const hourlyCounts = Array(24).fill(0);
622
- let totalMessages = 0;
623
- hourlyStats.forEach((stat) => {
624
- hourlyCounts[stat.timestamp.getHours()] += stat.count;
625
- totalMessages += stat.count;
626
- });
627
- const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "活跃" });
628
- return this.renderer.renderCircadianChart({ title, time: /* @__PURE__ */ new Date(), total: totalMessages, data: hourlyCounts });
617
+ cmd.subcommand("activity", "活跃统计").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("hours", "-h <hours:number> 指定偏移时长").option("all", "-a 全局").option("days", "-d 切换至天数").action(createHandler(async (scope, options) => {
618
+ const { days, hours } = options;
619
+ if (days) {
620
+ const timeRangeInDays = 24;
621
+ const since = new Date(Date.now() - timeRangeInDays * import_koishi3.Time.day);
622
+ const stats = await this.ctx.database.select("analyse_rank").where({ uid: { $in: scope.uids }, timestamp: { $gte: since } }).project(["timestamp", "count"]).execute();
623
+ if (stats.length === 0) return "暂无统计数据";
624
+ const startOfToday = /* @__PURE__ */ new Date();
625
+ startOfToday.setHours(0, 0, 0, 0);
626
+ const dailyCounts = Array(timeRangeInDays).fill(0);
627
+ const dayLabels = Array(timeRangeInDays).fill("");
628
+ for (let i = 0; i < timeRangeInDays; i++) {
629
+ const d = new Date(startOfToday.getTime() - i * import_koishi3.Time.day);
630
+ dayLabels[timeRangeInDays - 1 - i] = String(d.getDate());
631
+ }
632
+ stats.forEach((stat) => {
633
+ const statDayStart = new Date(stat.timestamp);
634
+ statDayStart.setHours(0, 0, 0, 0);
635
+ const daysAgo = Math.round((startOfToday.getTime() - statDayStart.getTime()) / import_koishi3.Time.day);
636
+ if (daysAgo >= 0 && daysAgo < timeRangeInDays) {
637
+ const index = timeRangeInDays - 1 - daysAgo;
638
+ dailyCounts[index] += stat.count;
639
+ }
640
+ });
641
+ const totalMessages = dailyCounts.reduce((a, b) => a + b, 0);
642
+ const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "活跃", timeRange: timeRangeInDays, timeUnit: "天" });
643
+ return this.renderer.renderCircadianChart({ title, time: /* @__PURE__ */ new Date(), total: totalMessages, data: dailyCounts, labels: dayLabels });
644
+ } else {
645
+ const timeWindowHours = 24;
646
+ const offsetHours = typeof hours === "number" ? hours : 0;
647
+ const now = /* @__PURE__ */ new Date();
648
+ const until = new Date(now.getTime() - offsetHours * import_koishi3.Time.hour);
649
+ const since = new Date(until.getTime() - timeWindowHours * import_koishi3.Time.hour);
650
+ const hourlyStats = await this.ctx.database.select("analyse_rank").where({ uid: { $in: scope.uids }, timestamp: { $gte: since, $lt: until } }).groupBy("timestamp", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count") }).execute();
651
+ if (hourlyStats.length === 0) return "暂无统计数据";
652
+ const processedCounts = Array(timeWindowHours).fill(0);
653
+ const hourLabels = Array(timeWindowHours).fill("");
654
+ for (let i = 0; i < timeWindowHours; i++) {
655
+ const d = new Date(until.getTime() - (i + 1) * import_koishi3.Time.hour);
656
+ hourLabels[timeWindowHours - 1 - i] = String(d.getHours());
657
+ }
658
+ hourlyStats.forEach((stat) => {
659
+ const hoursBeforeUntil = Math.floor((until.getTime() - stat.timestamp.getTime()) / import_koishi3.Time.hour);
660
+ if (hoursBeforeUntil >= 0 && hoursBeforeUntil < timeWindowHours) {
661
+ const index = timeWindowHours - 1 - hoursBeforeUntil;
662
+ processedCounts[index] += stat.count;
663
+ }
664
+ });
665
+ const totalMessages = processedCounts.reduce((a, b) => a + b, 0);
666
+ const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "活跃", timeRange: timeWindowHours, timeUnit: "小时" });
667
+ return this.renderer.renderCircadianChart({ title, time: /* @__PURE__ */ new Date(), total: totalMessages, data: processedCounts, labels: hourLabels });
668
+ }
629
669
  }));
630
670
  }
631
671
  }
@@ -871,7 +911,7 @@ var Analyse = class {
871
911
  if (records.length === 0) return "暂无统计数据";
872
912
  const allText = records.map((r) => r.content).join(" ");
873
913
  const result = await this.nlp.process("zh", allText);
874
- const words = result.stems.filter((stem) => stem.length > 1);
914
+ const words = (result.stems || []).filter((stem) => stem.length > 1);
875
915
  const wordCounts = words.reduce((map, word) => {
876
916
  map.set(word, (map.get(word) || 0) + 1);
877
917
  return map;
@@ -905,7 +945,7 @@ var Analyse = class {
905
945
  for (const [uid, messages] of messagesByUid.entries()) {
906
946
  const allText = messages.join(" ");
907
947
  const result = await this.nlp.process("zh", allText);
908
- const words = result.stems.filter((stem) => stem.length > 1);
948
+ const words = (result.stems || []).filter((stem) => stem.length > 1);
909
949
  if (words.length < 50) continue;
910
950
  const uniqueWords = new Set(words);
911
951
  const richness = uniqueWords.size / words.length;
@@ -997,16 +1037,18 @@ async function generateTitle(ctx, scopeDesc, options) {
997
1037
  const [user] = await ctx.database.get("analyse_user", { userId: scopeDesc.userId }, ["userName"]);
998
1038
  userName = user?.userName || scopeDesc.userId;
999
1039
  }
1040
+ const timeText = options.timeRange ? `${options.timeRange}${options.timeUnit || "小时"}` : "";
1000
1041
  const typeText = options.subtype ? `“${options.subtype}”` : "";
1001
1042
  const mainText = options.main;
1002
- if (mainText.includes("排行")) {
1043
+ if (mainText.includes("排行") || mainText.includes("活跃")) {
1003
1044
  scopeText = guildName || "全局";
1004
- return `${options.timeRange}小时${scopeText}${typeText}${mainText}`;
1045
+ } else {
1046
+ if (userName && guildName) scopeText = `${guildName} ${userName}`;
1047
+ else if (userName) scopeText = userName;
1048
+ else if (guildName) scopeText = guildName;
1005
1049
  }
1006
- if (userName && guildName) scopeText = `${guildName} ${userName}`;
1007
- else if (userName) scopeText = userName;
1008
- else if (guildName) scopeText = guildName;
1009
- return `${scopeText}${typeText}${mainText}统计`;
1050
+ const suffix = mainText.includes("排行") ? "" : "统计";
1051
+ return `${timeText}${scopeText}${typeText}${mainText}${suffix}`;
1010
1052
  }
1011
1053
  __name(generateTitle, "generateTitle");
1012
1054
  function apply(ctx, config) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-chat-analyse",
3
3
  "description": "聊天记录分析",
4
- "version": "0.6.2",
4
+ "version": "0.6.4",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],