@zhin.js/plugin-group-suite 0.1.7 → 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.
@@ -0,0 +1,101 @@
1
+ export interface StatsRankEntry {
2
+ rank: number;
3
+ userId: string;
4
+ name: string;
5
+ count: number;
6
+ percent: number;
7
+ }
8
+
9
+ export interface StatsRankReportData {
10
+ title: string;
11
+ entries: StatsRankEntry[];
12
+ participantCount: number;
13
+ totalMessages: number;
14
+ myRank?: number;
15
+ myCount?: number;
16
+ empty?: boolean;
17
+ }
18
+
19
+ export interface MyStatsReportData {
20
+ userName: string;
21
+ scope: string;
22
+ todayCount: number;
23
+ weekCount: number;
24
+ monthCount: number;
25
+ totalCount: number;
26
+ activeDays: number;
27
+ }
28
+
29
+ export function buildStatsRankReportData(
30
+ stats: Map<string, { name: string; count: number }>,
31
+ title: string,
32
+ size: number,
33
+ myUserId?: string,
34
+ ): StatsRankReportData {
35
+ const sorted = [...stats.entries()].sort((a, b) => b[1].count - a[1].count);
36
+ const total = sorted.reduce((s, [, v]) => s + v.count, 0);
37
+ const topCount = sorted[0]?.[1].count || 1;
38
+ const top = sorted.slice(0, size);
39
+
40
+ if (top.length === 0) {
41
+ return {
42
+ title,
43
+ entries: [],
44
+ participantCount: 0,
45
+ totalMessages: 0,
46
+ empty: true,
47
+ };
48
+ }
49
+
50
+ const entries: StatsRankEntry[] = top.map(([userId, v], i) => ({
51
+ rank: i + 1,
52
+ userId,
53
+ name: v.name,
54
+ count: v.count,
55
+ percent: Math.max(4, Math.round((v.count / topCount) * 100)),
56
+ }));
57
+
58
+ let myRank: number | undefined;
59
+ let myCount: number | undefined;
60
+ if (myUserId) {
61
+ const idx = sorted.findIndex(([uid]) => uid === myUserId);
62
+ if (idx >= 0) {
63
+ myRank = idx + 1;
64
+ myCount = sorted[idx][1].count;
65
+ }
66
+ }
67
+
68
+ return {
69
+ title,
70
+ entries,
71
+ participantCount: stats.size,
72
+ totalMessages: total,
73
+ myRank,
74
+ myCount,
75
+ };
76
+ }
77
+
78
+ export function formatRankText(data: StatsRankReportData): string {
79
+ if (data.empty) return `${data.title}\n暂无数据`;
80
+ const medals = ["🥇", "🥈", "🥉"];
81
+ const lines = data.entries.map((entry) => {
82
+ const prefix = entry.rank <= 3 ? medals[entry.rank - 1] : `${entry.rank}.`;
83
+ return `${prefix} ${entry.name} — ${entry.count}条`;
84
+ });
85
+ let footer = `\n共 ${data.participantCount} 人, ${data.totalMessages} 条消息`;
86
+ if (data.myRank != null && data.myCount != null) {
87
+ footer += ` | 你第${data.myRank}名 (${data.myCount}条)`;
88
+ }
89
+ return `${data.title}\n${lines.join("\n")}${footer}`;
90
+ }
91
+
92
+ export function formatMyStatsText(data: MyStatsReportData): string {
93
+ return [
94
+ `${data.userName} 的${data.scope}消息统计`,
95
+ `今日: ${data.todayCount} 条`,
96
+ `本周: ${data.weekCount} 条`,
97
+ `本月: ${data.monthCount} 条`,
98
+ `总计: ${data.totalCount} 条`,
99
+ `活跃天数: ${data.activeDays} 天`,
100
+ ].join("\n");
101
+ }
package/src/stats.ts CHANGED
@@ -1,6 +1,8 @@
1
- import { formatCompact, MessageCommand, type Plugin } from "zhin.js";
1
+ import { formatCompact, MessageCommand, segment, type Plugin } from "zhin.js";
2
2
  import type { GroupSuiteConfig } from "./config.js";
3
- import { getMessageContextKey, todayStr, ts } from "./shared.js";
3
+ import { todayStr, ts } from "./shared.js";
4
+ import { buildStatsRankReportData, type MyStatsReportData } from "./stats-data.js";
5
+ import { buildMyStatsHtml, buildStatsRankHtml, STATS_REPORT_CANVAS } from "./stats-card.js";
4
6
 
5
7
  export function weekStartStr(): string {
6
8
  const d = new Date();
@@ -105,29 +107,22 @@ export async function queryStats(
105
107
  return result;
106
108
  }
107
109
 
108
- function formatRank(
109
- stats: Map<string, { name: string; count: number }>,
110
- title: string,
111
- size: number,
112
- myUserId?: string,
113
- ): string {
114
- const sorted = [...stats.entries()].sort((a, b) => b[1].count - a[1].count);
115
- const total = sorted.reduce((s, [, v]) => s + v.count, 0);
116
- const top = sorted.slice(0, size);
117
- if (top.length === 0) return `${title}\n暂无数据`;
118
- const medals = ["🥇", "🥈", "🥉"];
119
- const lines = top.map(([, v], i) => {
120
- const prefix = i < 3 ? medals[i] : `${i + 1}.`;
121
- return `${prefix} ${v.name} — ${v.count}条`;
110
+ function statsRankReply(data: ReturnType<typeof buildStatsRankReportData>) {
111
+ return segment.html({
112
+ html: buildStatsRankHtml(data),
113
+ width: 540,
114
+ backgroundColor: STATS_REPORT_CANVAS,
115
+ fileName: "message-stats-rank.png",
116
+ });
117
+ }
118
+
119
+ function myStatsReply(data: MyStatsReportData) {
120
+ return segment.html({
121
+ html: buildMyStatsHtml(data),
122
+ width: 540,
123
+ backgroundColor: STATS_REPORT_CANVAS,
124
+ fileName: "my-message-stats.png",
122
125
  });
123
- let footer = `\n共 ${stats.size} 人, ${total} 条消息`;
124
- if (myUserId) {
125
- const myRank = sorted.findIndex(([uid]) => uid === myUserId);
126
- if (myRank >= 0) {
127
- footer += ` | 你第${myRank + 1}名 (${sorted[myRank][1].count}条)`;
128
- }
129
- }
130
- return `${title}\n${lines.join("\n")}${footer}`;
131
126
  }
132
127
 
133
128
  export function registerStats(plugin: Plugin, cfg: GroupSuiteConfig): void {
@@ -181,12 +176,13 @@ export function registerStats(plugin: Plugin, cfg: GroupSuiteConfig): void {
181
176
  const groupId = getGroupId(message);
182
177
  const stats = await queryStats(groupId, todayStr());
183
178
  const title = groupId ? "今日本群消息统计" : "今日全局消息统计";
184
- return formatRank(
179
+ const data = buildStatsRankReportData(
185
180
  stats,
186
181
  title,
187
182
  cfg.rankSize,
188
183
  String(message.$sender?.id || ""),
189
184
  );
185
+ return statsRankReply(data);
190
186
  }),
191
187
  );
192
188
 
@@ -198,12 +194,13 @@ export function registerStats(plugin: Plugin, cfg: GroupSuiteConfig): void {
198
194
  const groupId = getGroupId(message);
199
195
  const stats = await queryStats(groupId, weekStartStr());
200
196
  const title = groupId ? "本周本群消息统计" : "本周全局消息统计";
201
- return formatRank(
197
+ const data = buildStatsRankReportData(
202
198
  stats,
203
199
  title,
204
200
  cfg.rankSize,
205
201
  String(message.$sender?.id || ""),
206
202
  );
203
+ return statsRankReply(data);
207
204
  }),
208
205
  );
209
206
 
@@ -215,12 +212,13 @@ export function registerStats(plugin: Plugin, cfg: GroupSuiteConfig): void {
215
212
  const groupId = getGroupId(message);
216
213
  const stats = await queryStats(groupId, monthStartStr());
217
214
  const title = groupId ? "本月话唠排行" : "全局话唠排行";
218
- return formatRank(
215
+ const data = buildStatsRankReportData(
219
216
  stats,
220
217
  title,
221
218
  cfg.rankSize,
222
219
  String(message.$sender?.id || ""),
223
220
  );
221
+ return statsRankReply(data);
224
222
  }),
225
223
  );
226
224
 
@@ -253,14 +251,16 @@ export function registerStats(plugin: Plugin, cfg: GroupSuiteConfig): void {
253
251
  if (row.date === today) todayCount += c;
254
252
  }
255
253
  const scope = groupId ? "本群" : "全局";
256
- return [
257
- `${message.$sender?.name || "你"} 的${scope}消息统计`,
258
- `今日: ${todayCount} 条`,
259
- `本周: ${weekCount} 条`,
260
- `本月: ${monthCount} 条`,
261
- `总计: ${totalCount} 条`,
262
- `活跃天数: ${rows.length} 天`,
263
- ].join("\n");
254
+ const data: MyStatsReportData = {
255
+ userName: String(message.$sender?.name || "你"),
256
+ scope,
257
+ todayCount,
258
+ weekCount,
259
+ monthCount,
260
+ totalCount,
261
+ activeDays: rows.length,
262
+ };
263
+ return myStatsReply(data);
264
264
  }),
265
265
  );
266
266