koishi-plugin-chat-analyse 1.3.1 → 1.3.3
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 +90 -0
- package/lib/index.d.ts +66 -0
- package/lib/index.js +159 -45
- package/package.json +1 -1
- package/readme.md +83 -62
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { WordCloudData } from './Analyse';
|
|
3
|
+
/**
|
|
4
|
+
* @interface ListRenderData
|
|
5
|
+
* @description 定义了调用 `renderList` 方法所需的数据结构。
|
|
6
|
+
*/
|
|
7
|
+
export interface ListRenderData {
|
|
8
|
+
title: string;
|
|
9
|
+
time: Date;
|
|
10
|
+
total?: string | number;
|
|
11
|
+
list: (string | number | Date)[][];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* @interface LineChartData
|
|
15
|
+
* @description 定义了调用 `renderLineChart` 方法所需的数据结构,支持多组数据系列。
|
|
16
|
+
*/
|
|
17
|
+
export interface LineChartData {
|
|
18
|
+
title: string;
|
|
19
|
+
time: Date;
|
|
20
|
+
series: {
|
|
21
|
+
name: string;
|
|
22
|
+
data: number[];
|
|
23
|
+
}[];
|
|
24
|
+
labels: string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* @class Renderer
|
|
28
|
+
* @description 负责将结构化的数据渲染为设计精美的 PNG 图片。
|
|
29
|
+
*/
|
|
30
|
+
export declare class Renderer {
|
|
31
|
+
private ctx;
|
|
32
|
+
private readonly COLOR_PALETTES;
|
|
33
|
+
private readonly COMMON_STYLE;
|
|
34
|
+
/**
|
|
35
|
+
* @constructor
|
|
36
|
+
* @description Renderer 类的构造函数。
|
|
37
|
+
* @param {Context} ctx - Koishi 的插件上下文,用于访问 logger 和 puppeteer 服务。
|
|
38
|
+
*/
|
|
39
|
+
constructor(ctx: Context);
|
|
40
|
+
/**
|
|
41
|
+
* @private
|
|
42
|
+
* @method generateFullHtml
|
|
43
|
+
* @description 将卡片内容和特定样式组合成一个完整的 HTML 文档,以便进行渲染。
|
|
44
|
+
* @param {string} cardContent - 卡片主体部分的 HTML 字符串。
|
|
45
|
+
* @param {string} specificStyles - 针对该卡片类型的特定 CSS 样式字符串。
|
|
46
|
+
* @returns {string} - 一个完整的、可被浏览器渲染的 HTML 字符串。
|
|
47
|
+
*/
|
|
48
|
+
private generateFullHtml;
|
|
49
|
+
/**
|
|
50
|
+
* @private
|
|
51
|
+
* @method htmlToImage
|
|
52
|
+
* @description 使用 puppeteer 将给定的 HTML 字符串内容渲染成 PNG 图片的 Buffer。
|
|
53
|
+
* @param {string} fullHtmlContent - 完整的 HTML 内容字符串。
|
|
54
|
+
* @returns {Promise<Buffer | null>} - 成功时返回包含 PNG 图片数据的 Buffer,失败则返回 null。
|
|
55
|
+
*/
|
|
56
|
+
private htmlToImage;
|
|
57
|
+
/**
|
|
58
|
+
* @private
|
|
59
|
+
* @method formatDate
|
|
60
|
+
* @description 将 Date 对象格式化为易于理解的相对时间字符串(如“刚刚”,“5分钟前”)。
|
|
61
|
+
* @param {Date} date - 需要格式化的日期对象。
|
|
62
|
+
* @returns {string} - 格式化后的时间字符串。
|
|
63
|
+
*/
|
|
64
|
+
private formatDate;
|
|
65
|
+
/**
|
|
66
|
+
* @public
|
|
67
|
+
* @method renderList
|
|
68
|
+
* @description 将表格型数据渲染成列表形式的图片。如果数据过多,会通过异步生成器逐个产出图片。
|
|
69
|
+
* @param {ListRenderData} data - 包含标题、时间、总计和列表数据的对象。
|
|
70
|
+
* @param {string[]} [headers] - (可选)列表的表头数组。
|
|
71
|
+
* @returns {AsyncGenerator<Buffer>} - 一个异步生成器,每次迭代产出一张图片的 Buffer。
|
|
72
|
+
*/
|
|
73
|
+
renderList(data: ListRenderData, headers?: string[]): AsyncGenerator<Buffer>;
|
|
74
|
+
/**
|
|
75
|
+
* @public
|
|
76
|
+
* @method renderLineChart
|
|
77
|
+
* @description 将时间序列数据(如活跃度)渲染成一张基于 SVG 的折线图。支持单组或多组数据进行对比。
|
|
78
|
+
* @param {LineChartData} data - 包含标题、时间、数据系列和标签的对象。
|
|
79
|
+
* @returns {AsyncGenerator<Buffer>} - 一个异步生成器,产出渲染后的图片 Buffer。
|
|
80
|
+
*/
|
|
81
|
+
renderLineChart(data: LineChartData): AsyncGenerator<Buffer>;
|
|
82
|
+
/**
|
|
83
|
+
* @public
|
|
84
|
+
* @method renderWordCloud
|
|
85
|
+
* @description 将词频数据渲染成一张词云图片,使用 Puppeteer 和 wordcloud2.js。
|
|
86
|
+
* @param {WordCloudData} data - 包含标题、时间和词汇列表的对象。
|
|
87
|
+
* @returns {AsyncGenerator<Buffer>} - 一个异步生成器,产出渲染后的图片 Buffer。
|
|
88
|
+
*/
|
|
89
|
+
renderWordCloud(data: WordCloudData): AsyncGenerator<Buffer>;
|
|
90
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Context, Schema, Session } from 'koishi';
|
|
2
|
+
/** @name 插件使用说明 */
|
|
3
|
+
export declare const usage = "\n<div style=\"border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);\">\n <h2 style=\"margin-top: 0; color: #4a6ee0;\">\uD83D\uDCCC \u63D2\u4EF6\u8BF4\u660E</h2>\n <p>\uD83D\uDCD6 <strong>\u4F7F\u7528\u6587\u6863</strong>\uFF1A\u8BF7\u70B9\u51FB\u5DE6\u4E0A\u89D2\u7684 <strong>\u63D2\u4EF6\u4E3B\u9875</strong> \u67E5\u770B\u63D2\u4EF6\u4F7F\u7528\u6587\u6863</p>\n <p>\uD83D\uDD0D <strong>\u66F4\u591A\u63D2\u4EF6</strong>\uFF1A\u53EF\u8BBF\u95EE <a href=\"https://github.com/YisRime\" style=\"color:#4a6ee0;text-decoration:none;\">\u82E1\u6DDE\u7684 GitHub</a> \u67E5\u770B\u672C\u4EBA\u7684\u6240\u6709\u63D2\u4EF6</p>\n</div>\n<div style=\"border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);\">\n <h2 style=\"margin-top: 0; color: #e0574a;\">\u2764\uFE0F \u652F\u6301\u4E0E\u53CD\u9988</h2>\n <p>\uD83C\uDF1F \u559C\u6B22\u8FD9\u4E2A\u63D2\u4EF6\uFF1F\u8BF7\u5728 <a href=\"https://github.com/YisRime\" style=\"color:#e0574a;text-decoration:none;\">GitHub</a> \u4E0A\u7ED9\u6211\u4E00\u4E2A Star\uFF01</p>\n <p>\uD83D\uDC1B \u9047\u5230\u95EE\u9898\uFF1F\u8BF7\u901A\u8FC7 <strong>Issues</strong> \u63D0\u4EA4\u53CD\u9988\uFF0C\u6216\u52A0\u5165 QQ \u7FA4 <a href=\"https://qm.qq.com/q/PdLMx9Jowq\" style=\"color:#e0574a;text-decoration:none;\"><strong>855571375</strong></a> \u8FDB\u884C\u4EA4\u6D41</p>\n</div>\n";
|
|
4
|
+
export declare const name = "chat-analyse";
|
|
5
|
+
export declare const using: string[];
|
|
6
|
+
/**
|
|
7
|
+
* @interface Config
|
|
8
|
+
* @description 定义插件的配置项结构。
|
|
9
|
+
*/
|
|
10
|
+
export interface Config {
|
|
11
|
+
enableListener: boolean;
|
|
12
|
+
enableCmdStat: boolean;
|
|
13
|
+
enableMsgStat: boolean;
|
|
14
|
+
enableRankStat: boolean;
|
|
15
|
+
enableActivity: boolean;
|
|
16
|
+
enableOriRecord: boolean;
|
|
17
|
+
enableWhoAt: boolean;
|
|
18
|
+
enableDataIO: boolean;
|
|
19
|
+
atRetentionDays: number;
|
|
20
|
+
rankRetentionDays: number;
|
|
21
|
+
enableWordCloud: boolean;
|
|
22
|
+
cacheRetentionDays: number;
|
|
23
|
+
enableSimilarActivity: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** @description 插件的配置项定义 */
|
|
26
|
+
export declare const Config: Schema<Config>;
|
|
27
|
+
/**
|
|
28
|
+
* @private @method parseQueryScope
|
|
29
|
+
* @description 解析命令选项,转换为包含 UIDs 和描述性信息的统一查询范围对象。
|
|
30
|
+
* @param session - 当前会话对象。
|
|
31
|
+
* @param options - 命令选项。
|
|
32
|
+
* @returns 包含 uids、错误或范围描述的查询范围对象。
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseQueryScope(ctx: Context, session: Session, options: {
|
|
35
|
+
user?: string;
|
|
36
|
+
guild?: string;
|
|
37
|
+
all?: boolean;
|
|
38
|
+
}): Promise<{
|
|
39
|
+
uids?: number[];
|
|
40
|
+
error?: string;
|
|
41
|
+
scopeDesc: {
|
|
42
|
+
guildId?: string;
|
|
43
|
+
userId?: string;
|
|
44
|
+
};
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* @private @method generateTitle
|
|
48
|
+
* @description 根据查询范围和类型动态生成易于理解的图片标题。
|
|
49
|
+
* @returns 生成的标题字符串。
|
|
50
|
+
*/
|
|
51
|
+
export declare function generateTitle(ctx: Context, scopeDesc: {
|
|
52
|
+
guildId?: string;
|
|
53
|
+
userId?: string;
|
|
54
|
+
}, options: {
|
|
55
|
+
main: string;
|
|
56
|
+
subtype?: string;
|
|
57
|
+
timeRange?: number;
|
|
58
|
+
timeUnit?: '小时' | '天';
|
|
59
|
+
}): Promise<string>;
|
|
60
|
+
/**
|
|
61
|
+
* @function apply
|
|
62
|
+
* @description Koishi 插件的主入口函数,负责初始化和注册所有功能模块。
|
|
63
|
+
* @param ctx - Koishi 的插件上下文。
|
|
64
|
+
* @param config - 用户配置对象。
|
|
65
|
+
*/
|
|
66
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -1518,6 +1518,7 @@ var Renderer = class {
|
|
|
1518
1518
|
.container {
|
|
1519
1519
|
display: inline-block; background: var(--card-bg); border-radius: 12px;
|
|
1520
1520
|
padding: 0; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,.05);
|
|
1521
|
+
width: 600px;
|
|
1521
1522
|
}
|
|
1522
1523
|
.header {
|
|
1523
1524
|
padding: 12px 16px;
|
|
@@ -1527,7 +1528,7 @@ var Renderer = class {
|
|
|
1527
1528
|
border-bottom: 1px solid var(--border-color);
|
|
1528
1529
|
}
|
|
1529
1530
|
.title-text {
|
|
1530
|
-
font-size:
|
|
1531
|
+
font-size: 16px; font-weight: 600; color: var(--header-color);
|
|
1531
1532
|
margin: 0; text-align: center;
|
|
1532
1533
|
}
|
|
1533
1534
|
.stat-chip, .time-label {
|
|
@@ -1631,7 +1632,7 @@ var Renderer = class {
|
|
|
1631
1632
|
.main-table { border-collapse: collapse; width: 100%; }
|
|
1632
1633
|
.main-table th, .main-table td { padding: 9px 16px; vertical-align: middle; text-align: left; }
|
|
1633
1634
|
.main-table thead { border-bottom: 1px solid var(--border-color); }
|
|
1634
|
-
.main-table th { font-size: 12px; font-weight: 500; color: var(--sub-text-color); text-transform: uppercase; }
|
|
1635
|
+
.main-table th { font-size: 12px; font-weight: 500; color: var(--sub-text-color); text-transform: uppercase; white-space: nowrap; }
|
|
1635
1636
|
.main-table td { font-size: 14px; color: var(--text-color); }
|
|
1636
1637
|
.main-table tbody tr:nth-child(even) { background-color: var(--stripe-bg); }
|
|
1637
1638
|
.rank-cell, .count-cell, .date-cell, .percent-cell { text-align: right; white-space: nowrap; width: 1%; font-variant-numeric: tabular-nums; }
|
|
@@ -1683,40 +1684,65 @@ var Renderer = class {
|
|
|
1683
1684
|
}
|
|
1684
1685
|
/**
|
|
1685
1686
|
* @public
|
|
1686
|
-
* @method
|
|
1687
|
-
* @description
|
|
1688
|
-
* @param {
|
|
1687
|
+
* @method renderLineChart
|
|
1688
|
+
* @description 将时间序列数据(如活跃度)渲染成一张基于 SVG 的折线图。支持单组或多组数据进行对比。
|
|
1689
|
+
* @param {LineChartData} data - 包含标题、时间、数据系列和标签的对象。
|
|
1689
1690
|
* @returns {AsyncGenerator<Buffer>} - 一个异步生成器,产出渲染后的图片 Buffer。
|
|
1690
1691
|
*/
|
|
1691
|
-
async *
|
|
1692
|
-
const { title, time,
|
|
1693
|
-
const
|
|
1694
|
-
const
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1692
|
+
async *renderLineChart(data) {
|
|
1693
|
+
const { title, time, series, labels } = data;
|
|
1694
|
+
const width = 600, height = 320;
|
|
1695
|
+
const padding = { top: 20, right: 20, bottom: 60, left: 40 };
|
|
1696
|
+
const chartWidth = width - padding.left - padding.right;
|
|
1697
|
+
const chartHeight = height - padding.top - padding.bottom;
|
|
1698
|
+
const maxVal = Math.max(1, ...series.flatMap((s) => s.data));
|
|
1699
|
+
const yTickCount = 5;
|
|
1700
|
+
const yTickValue = Math.ceil(maxVal / yTickCount);
|
|
1701
|
+
const yAxisMax = yTickValue * yTickCount;
|
|
1702
|
+
const getX = /* @__PURE__ */ __name((index) => padding.left + index / (labels.length - 1) * chartWidth, "getX");
|
|
1703
|
+
const getY = /* @__PURE__ */ __name((value) => padding.top + chartHeight - value / yAxisMax * chartHeight, "getY");
|
|
1704
|
+
let svgElements = "";
|
|
1705
|
+
for (let i = 0; i <= yTickCount; i++) {
|
|
1706
|
+
const y = getY(i * yTickValue);
|
|
1707
|
+
const value = i * yTickValue;
|
|
1708
|
+
svgElements += `<line x1="${padding.left}" y1="${y}" x2="${width - padding.right}" y2="${y}" stroke="var(--border-color)" stroke-width="1"/>`;
|
|
1709
|
+
svgElements += `<text x="${padding.left - 8}" y="${y + 4}" font-size="10" fill="var(--sub-text-color)" text-anchor="end">${value}</text>`;
|
|
1710
|
+
}
|
|
1711
|
+
labels.forEach((label, index) => {
|
|
1712
|
+
if (index % Math.ceil(labels.length / 10) === 0) {
|
|
1713
|
+
const x = getX(index);
|
|
1714
|
+
svgElements += `<text x="${x}" y="${height - padding.bottom + 15}" font-size="10" fill="var(--sub-text-color)" text-anchor="middle">${label}</text>`;
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
series.forEach((s, seriesIndex) => {
|
|
1718
|
+
const color = this.COLOR_PALETTES[0][seriesIndex % this.COLOR_PALETTES[0].length];
|
|
1719
|
+
const points = s.data.map((value, index) => `${getX(index)},${getY(value)}`).join(" ");
|
|
1720
|
+
svgElements += `<polyline points="${points}" fill="none" stroke="${color}" stroke-width="2"/>`;
|
|
1721
|
+
});
|
|
1722
|
+
let legendX = padding.left;
|
|
1723
|
+
const legendY = height - padding.bottom + 40;
|
|
1724
|
+
series.forEach((s, seriesIndex) => {
|
|
1725
|
+
const color = this.COLOR_PALETTES[0][seriesIndex % this.COLOR_PALETTES[0].length];
|
|
1726
|
+
svgElements += `<rect x="${legendX}" y="${legendY - 8}" width="12" height="8" fill="${color}" rx="2"/>`;
|
|
1727
|
+
const textElement = `<text x="${legendX + 18}" y="${legendY}" font-size="12" fill="var(--text-color)">${s.name}</text>`;
|
|
1728
|
+
svgElements += textElement;
|
|
1729
|
+
legendX += 25 + s.name.length * 8;
|
|
1730
|
+
});
|
|
1731
|
+
const totalMessages = series.reduce((sum, s) => sum + s.data.reduce((a, b) => a + b, 0), 0);
|
|
1702
1732
|
const cardHtml = `
|
|
1703
1733
|
<div class="container">
|
|
1704
1734
|
<div class="header">
|
|
1705
|
-
<div class="stat-chip">总计: <span>${
|
|
1735
|
+
<div class="stat-chip">总计: <span>${totalMessages.toLocaleString()}</span></div>
|
|
1706
1736
|
<h1 class="title-text">${title}</h1>
|
|
1707
1737
|
<div class="time-label">${time.toLocaleString("zh-CN", { hour12: false })}</div>
|
|
1708
1738
|
</div>
|
|
1709
|
-
<div class="chart-
|
|
1710
|
-
${
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
<div class="bar-container">
|
|
1714
|
-
<div class="bar" style="height: ${count / maxCount * 100}%;"></div>
|
|
1715
|
-
</div>
|
|
1716
|
-
<div class="bar-label">${labels ? labels[hour] : hour}</div>
|
|
1717
|
-
</div>`).join("")}
|
|
1739
|
+
<div class="chart-wrapper">
|
|
1740
|
+
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
1741
|
+
${svgElements}
|
|
1742
|
+
</svg>
|
|
1718
1743
|
</div>
|
|
1719
1744
|
</div>`;
|
|
1745
|
+
const chartStyles = ` .chart-wrapper { padding: 10px; } `;
|
|
1720
1746
|
const fullHtml = this.generateFullHtml(cardHtml, chartStyles);
|
|
1721
1747
|
const imageBuffer = await this.htmlToImage(fullHtml);
|
|
1722
1748
|
if (imageBuffer) yield imageBuffer;
|
|
@@ -1843,11 +1869,11 @@ var Stat = class {
|
|
|
1843
1869
|
}
|
|
1844
1870
|
}, "handleAction");
|
|
1845
1871
|
if (this.config.enableCmdStat) {
|
|
1846
|
-
cmd.subcommand("cmdstat", "命令统计").usage("查询命令统计,可指定查询范围,默认当前群组。").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("separate", "-s
|
|
1872
|
+
cmd.subcommand("cmdstat", "命令统计").usage("查询命令统计,可指定查询范围,默认当前群组。").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("separate", "-p 分离子命令").option("sortByTime", "-s 以时间排序").option("all", "-a 全局统计").action(({ session, options }) => handleAction(session, (async () => {
|
|
1847
1873
|
const scope = await this.parseScope(session, options);
|
|
1848
1874
|
if (scope.error) return scope.error;
|
|
1849
1875
|
const query = scope.uids ? { uid: { $in: scope.uids } } : {};
|
|
1850
|
-
const stats = await this.ctx.database.select("analyse_cmd").where(query).groupBy("command", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"), lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed") }).
|
|
1876
|
+
const stats = await this.ctx.database.select("analyse_cmd").where(query).groupBy("command", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"), lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed") }).execute();
|
|
1851
1877
|
if (stats.length === 0) return "暂无统计数据";
|
|
1852
1878
|
let processedStats;
|
|
1853
1879
|
if (options.separate) {
|
|
@@ -1861,7 +1887,12 @@ var Stat = class {
|
|
|
1861
1887
|
if (stat.lastUsed > existing.lastUsed) existing.lastUsed = stat.lastUsed;
|
|
1862
1888
|
merged.set(mainCmd, existing);
|
|
1863
1889
|
}
|
|
1864
|
-
processedStats = Array.from(merged.entries()).map(([command, data]) => ({ ...data, command }))
|
|
1890
|
+
processedStats = Array.from(merged.entries()).map(([command, data]) => ({ ...data, command }));
|
|
1891
|
+
}
|
|
1892
|
+
if (options.sortByTime) {
|
|
1893
|
+
processedStats.sort((a, b) => b.lastUsed.getTime() - a.lastUsed.getTime());
|
|
1894
|
+
} else {
|
|
1895
|
+
processedStats.sort((a, b) => b.count - a.count);
|
|
1865
1896
|
}
|
|
1866
1897
|
const total = processedStats.reduce((sum, r) => sum + r.count, 0);
|
|
1867
1898
|
const list = processedStats.map((item) => [item.command, item.count, item.lastUsed]);
|
|
@@ -1870,15 +1901,23 @@ var Stat = class {
|
|
|
1870
1901
|
})()));
|
|
1871
1902
|
}
|
|
1872
1903
|
if (this.config.enableMsgStat) {
|
|
1873
|
-
cmd.subcommand("msgstat", "发言统计").usage("查询发言统计,可指定查询范围,默认当前群组。").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("type", "-t <type:string> 指定类型").option("all", "-a 全局统计").action(({ session, options }) => handleAction(session, (async () => {
|
|
1904
|
+
cmd.subcommand("msgstat", "发言统计").usage("查询发言统计,可指定查询范围,默认当前群组。").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("type", "-t <type:string> 指定类型").option("sortByTime", "-s 以时间排序").option("all", "-a 全局统计").action(({ session, options }) => handleAction(session, (async () => {
|
|
1874
1905
|
const scope = await this.parseScope(session, options);
|
|
1875
1906
|
if (scope.error) return scope.error;
|
|
1876
1907
|
const query = scope.uids ? { uid: { $in: scope.uids } } : {};
|
|
1877
1908
|
if (options.type) query.type = options.type;
|
|
1878
1909
|
const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "发言", subtype: options.type });
|
|
1910
|
+
const applySort = /* @__PURE__ */ __name((stats2) => {
|
|
1911
|
+
if (options.sortByTime) {
|
|
1912
|
+
stats2.sort((a, b) => b.lastUsed.getTime() - a.lastUsed.getTime());
|
|
1913
|
+
} else {
|
|
1914
|
+
stats2.sort((a, b) => b.count - a.count);
|
|
1915
|
+
}
|
|
1916
|
+
}, "applySort");
|
|
1879
1917
|
if (options.user && options.guild) {
|
|
1880
|
-
const stats2 = await this.ctx.database.select("analyse_msg").where(query).groupBy("type", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"), lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed") }).
|
|
1918
|
+
const stats2 = await this.ctx.database.select("analyse_msg").where(query).groupBy("type", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"), lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed") }).execute();
|
|
1881
1919
|
if (stats2.length === 0) return "暂无统计数据";
|
|
1920
|
+
applySort(stats2);
|
|
1882
1921
|
const total2 = stats2.reduce((sum, r) => sum + r.count, 0);
|
|
1883
1922
|
const list2 = stats2.map((item) => [item.type, item.count, item.lastUsed]);
|
|
1884
1923
|
return this.renderer.renderList({ title, time: /* @__PURE__ */ new Date(), total: total2, list: list2 }, ["类型", "条数", "最后发言"]);
|
|
@@ -1886,20 +1925,29 @@ var Stat = class {
|
|
|
1886
1925
|
if (options.user) {
|
|
1887
1926
|
const userRecords = await this.ctx.database.get("analyse_user", { uid: { $in: scope.uids } });
|
|
1888
1927
|
const uidToChannelMap = new Map(userRecords.map((u) => [u.uid, u.channelName || u.channelId]));
|
|
1889
|
-
const stats2 = await this.ctx.database.select("analyse_msg").where(query).groupBy("uid", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"), lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed") }).
|
|
1928
|
+
const stats2 = await this.ctx.database.select("analyse_msg").where(query).groupBy("uid", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"), lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed") }).execute();
|
|
1890
1929
|
if (stats2.length === 0) return "暂无统计数据";
|
|
1930
|
+
applySort(stats2);
|
|
1891
1931
|
const total2 = stats2.reduce((sum, r) => sum + r.count, 0);
|
|
1892
1932
|
const list2 = stats2.map((item) => [uidToChannelMap.get(item.uid) || `未知群组`, item.count, item.lastUsed]);
|
|
1893
1933
|
return this.renderer.renderList({ title, time: /* @__PURE__ */ new Date(), total: total2, list: list2 }, ["群组", "条数", "最后发言"]);
|
|
1894
1934
|
}
|
|
1895
|
-
const stats = await this.ctx.database.select("analyse_msg").where(query).groupBy("uid", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"), lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed") }).
|
|
1935
|
+
const stats = await this.ctx.database.select("analyse_msg").where(query).groupBy("uid", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"), lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed") }).execute();
|
|
1896
1936
|
if (stats.length === 0) return "暂无统计数据";
|
|
1937
|
+
applySort(stats);
|
|
1897
1938
|
const allUids = stats.map((s) => s.uid);
|
|
1898
|
-
const
|
|
1899
|
-
const
|
|
1939
|
+
const userNameMap = /* @__PURE__ */ new Map();
|
|
1940
|
+
const BATCH_SIZE2 = 4096;
|
|
1941
|
+
for (let i = 0; i < allUids.length; i += BATCH_SIZE2) {
|
|
1942
|
+
const batchUids = allUids.slice(i, i + BATCH_SIZE2);
|
|
1943
|
+
const users = await this.ctx.database.get("analyse_user", { uid: { $in: batchUids } }, ["uid", "userName"]);
|
|
1944
|
+
for (const user of users) {
|
|
1945
|
+
userNameMap.set(user.uid, user.userName);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1900
1948
|
const total = stats.reduce((sum, r) => sum + r.count, 0);
|
|
1901
1949
|
const list = stats.map((item) => [userNameMap.get(item.uid) || `UID ${item.uid}`, item.count, item.lastUsed]);
|
|
1902
|
-
return this.renderer.renderList({ title, time: /* @__PURE__ */ new Date(), total, list }, ["用户", "
|
|
1950
|
+
return this.renderer.renderList({ title, time: /* @__PURE__ */ new Date(), total, list }, ["用户", "条数", "最后发言"]);
|
|
1903
1951
|
})()));
|
|
1904
1952
|
}
|
|
1905
1953
|
if (this.config.enableRankStat) {
|
|
@@ -1931,15 +1979,22 @@ var Stat = class {
|
|
|
1931
1979
|
const stats = await this.ctx.database.select("analyse_rank").where(query).groupBy("uid", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count") }).orderBy("count", "desc").execute();
|
|
1932
1980
|
if (stats.length === 0) return "暂无统计数据";
|
|
1933
1981
|
const allUids = stats.map((s) => s.uid);
|
|
1934
|
-
const
|
|
1935
|
-
const
|
|
1982
|
+
const userNameMap = /* @__PURE__ */ new Map();
|
|
1983
|
+
const BATCH_SIZE2 = 4096;
|
|
1984
|
+
for (let i = 0; i < allUids.length; i += BATCH_SIZE2) {
|
|
1985
|
+
const batchUids = allUids.slice(i, i + BATCH_SIZE2);
|
|
1986
|
+
const users = await this.ctx.database.get("analyse_user", { uid: { $in: batchUids } }, ["uid", "userName"]);
|
|
1987
|
+
for (const user of users) {
|
|
1988
|
+
userNameMap.set(user.uid, user.userName);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1936
1991
|
const total = stats.reduce((sum, r) => sum + r.count, 0);
|
|
1937
1992
|
const list = stats.map((r) => [userNameMap.get(r.uid) || `UID ${r.uid}`, r.count, total > 0 ? `${(r.count / total * 100).toFixed(2)}%` : "0.00%"]);
|
|
1938
|
-
return this.renderer.renderList({ title, time: /* @__PURE__ */ new Date(), total, list }, ["用户", "
|
|
1993
|
+
return this.renderer.renderList({ title, time: /* @__PURE__ */ new Date(), total, list }, ["用户", "条数", "占比"]);
|
|
1939
1994
|
})()));
|
|
1940
1995
|
}
|
|
1941
1996
|
if (this.config.enableActivity) {
|
|
1942
|
-
cmd.subcommand("activity", "活跃统计").usage("查询活跃统计,可指定查询范围,默认当前群组。").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("duration", "-n <units:number> 指定时长", { fallback: 24 }).option("offset", "-o <units:number> 指定偏移").option("days", "-d
|
|
1997
|
+
cmd.subcommand("activity", "活跃统计").usage("查询活跃统计,可指定查询范围,默认当前群组。").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("duration", "-n <units:number> 指定时长", { fallback: 24 }).option("offset", "-o <units:number> 指定偏移").option("days", "-d 以天为粒度").option("all", "-a 全局统计").action(({ session, options }) => handleAction(session, (async () => {
|
|
1943
1998
|
const scope = await this.parseScope(session, options);
|
|
1944
1999
|
if (scope.error) return scope.error;
|
|
1945
2000
|
const timeUnit = options.days ? import_koishi3.Time.day : import_koishi3.Time.hour;
|
|
@@ -1966,9 +2021,9 @@ var Stat = class {
|
|
|
1966
2021
|
counts[index] += stat.count;
|
|
1967
2022
|
}
|
|
1968
2023
|
});
|
|
1969
|
-
const total = counts.reduce((a, b) => a + b, 0);
|
|
1970
2024
|
const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "活跃", timeRange: options.duration, timeUnit: timeUnitName });
|
|
1971
|
-
|
|
2025
|
+
const series = [{ name: "活跃度", data: counts }];
|
|
2026
|
+
return this.renderer.renderLineChart({ title, time: /* @__PURE__ */ new Date(), series, labels });
|
|
1972
2027
|
})()));
|
|
1973
2028
|
}
|
|
1974
2029
|
}
|
|
@@ -2101,7 +2156,7 @@ var Data = class {
|
|
|
2101
2156
|
return "数据恢复失败";
|
|
2102
2157
|
}
|
|
2103
2158
|
});
|
|
2104
|
-
cmd.subcommand(".clear", "清除数据", { authority: 4 }).usage(`清除指定统计数据,可精确控制清除范围。`).option("table", "-t <table:string> 指定表名").option("guild", "-g <guildId:string> 指定群组").option("user", "-u <user:string> 指定用户").option("days", "-d <days:number> 指定天数").option("command", "-c <command:string> 指定命令").option("all", "-a
|
|
2159
|
+
cmd.subcommand(".clear", "清除数据", { authority: 4 }).usage(`清除指定统计数据,可精确控制清除范围。`).option("table", "-t <table:string> 指定表名").option("guild", "-g <guildId:string> 指定群组").option("user", "-u <user:string> 指定用户").option("days", "-d <days:number> 指定天数").option("command", "-c <command:string> 指定命令").option("all", "-a 全部清除").action(async ({ options }) => {
|
|
2105
2160
|
if (Object.keys(options).length === 0) return "请指定清除条件";
|
|
2106
2161
|
if (options.table && !ALL_TABLES.includes(options.table)) return `表名 ${options.table} 无效`;
|
|
2107
2162
|
try {
|
|
@@ -2176,6 +2231,21 @@ ${commandOutput}`;
|
|
|
2176
2231
|
var import_koishi6 = require("koishi");
|
|
2177
2232
|
var import_jieba = require("@node-rs/jieba");
|
|
2178
2233
|
var import_dict = require("@node-rs/jieba/dict");
|
|
2234
|
+
function cosineSimilarity(vecA, vecB) {
|
|
2235
|
+
let dotProduct = 0;
|
|
2236
|
+
let magA = 0;
|
|
2237
|
+
let magB = 0;
|
|
2238
|
+
for (let i = 0; i < vecA.length; i++) {
|
|
2239
|
+
dotProduct += (vecA[i] || 0) * (vecB[i] || 0);
|
|
2240
|
+
magA += (vecA[i] || 0) * (vecA[i] || 0);
|
|
2241
|
+
magB += (vecB[i] || 0) * (vecB[i] || 0);
|
|
2242
|
+
}
|
|
2243
|
+
magA = Math.sqrt(magA);
|
|
2244
|
+
magB = Math.sqrt(magB);
|
|
2245
|
+
if (magA === 0 || magB === 0) return 0;
|
|
2246
|
+
return dotProduct / (magA * magB);
|
|
2247
|
+
}
|
|
2248
|
+
__name(cosineSimilarity, "cosineSimilarity");
|
|
2179
2249
|
var Analyse = class {
|
|
2180
2250
|
constructor(ctx, config) {
|
|
2181
2251
|
this.ctx = ctx;
|
|
@@ -2232,6 +2302,49 @@ var Analyse = class {
|
|
|
2232
2302
|
}
|
|
2233
2303
|
});
|
|
2234
2304
|
}
|
|
2305
|
+
if (this.config.enableSimilarActivity) {
|
|
2306
|
+
cmd.subcommand("simiactive", "相似活跃分析").usage("分析你和群友的活跃度,找出谁和你的活跃度最相似。").option("hours", "-n <hours:number> 指定分析时长(小时)", { fallback: 24 }).action(async ({ session, options }) => {
|
|
2307
|
+
if (!session.guildId) return "请在群组中使用此命令";
|
|
2308
|
+
try {
|
|
2309
|
+
const until = /* @__PURE__ */ new Date();
|
|
2310
|
+
const since = new Date(until.getTime() - options.hours * import_koishi6.Time.hour);
|
|
2311
|
+
const points = options.hours;
|
|
2312
|
+
const guildUsers = await this.ctx.database.get("analyse_user", { channelId: session.guildId });
|
|
2313
|
+
if (guildUsers.length < 2) return "暂无用户数据";
|
|
2314
|
+
const selfUser = guildUsers.find((u) => u.userId === session.userId);
|
|
2315
|
+
const guildUserUids = guildUsers.map((u) => u.uid);
|
|
2316
|
+
const uidToNameMap = new Map(guildUsers.map((u) => [u.uid, u.userName]));
|
|
2317
|
+
const records = await this.ctx.database.get("analyse_rank", { uid: { $in: guildUserUids }, timestamp: { $gte: since } });
|
|
2318
|
+
if (records.length === 0) return "暂无统计数据";
|
|
2319
|
+
const activityVectors = /* @__PURE__ */ new Map();
|
|
2320
|
+
guildUserUids.forEach((uid) => activityVectors.set(uid, Array(points).fill(0)));
|
|
2321
|
+
records.forEach((stat) => {
|
|
2322
|
+
const diff = until.getTime() - stat.timestamp.getTime();
|
|
2323
|
+
const index = points - 1 - Math.floor(diff / import_koishi6.Time.hour);
|
|
2324
|
+
if (index >= 0 && index < points) {
|
|
2325
|
+
activityVectors.get(stat.uid)[index] += stat.count;
|
|
2326
|
+
}
|
|
2327
|
+
});
|
|
2328
|
+
const selfVector = activityVectors.get(selfUser.uid);
|
|
2329
|
+
if (!selfVector || selfVector.every((v) => v === 0)) return "暂无统计数据";
|
|
2330
|
+
const similarities = guildUserUids.filter((uid) => uid !== selfUser.uid && !activityVectors.get(uid).every((v) => v === 0)).map((uid) => ({ uid, score: cosineSimilarity(selfVector, activityVectors.get(uid)) })).sort((a, b) => b.score - a.score);
|
|
2331
|
+
if (similarities.length === 0) return "暂无相似用户";
|
|
2332
|
+
const top5 = similarities.slice(0, 5);
|
|
2333
|
+
const series = [{ name: uidToNameMap.get(selfUser.uid) || "您", data: selfVector }];
|
|
2334
|
+
top5.forEach((sim) => {
|
|
2335
|
+
const name2 = uidToNameMap.get(sim.uid) || `UID ${sim.uid}`;
|
|
2336
|
+
series.push({ name: `${name2} (${(sim.score * 100).toFixed(1)}%)`, data: activityVectors.get(sim.uid) });
|
|
2337
|
+
});
|
|
2338
|
+
const labels = Array.from({ length: points }, (_, i) => String(new Date(until.getTime() - (points - 1 - i) * import_koishi6.Time.hour).getHours()));
|
|
2339
|
+
const title = `${options.hours}小时相似活跃分析`;
|
|
2340
|
+
const imageGenerator = this.renderer.renderLineChart({ title, time: /* @__PURE__ */ new Date(), series, labels });
|
|
2341
|
+
for await (const buffer of imageGenerator) await session.send(import_koishi6.h.image(buffer, "image/png"));
|
|
2342
|
+
} catch (error) {
|
|
2343
|
+
this.ctx.logger.error("生成作息分析图片失败:", error);
|
|
2344
|
+
return "图片渲染失败";
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2235
2348
|
}
|
|
2236
2349
|
};
|
|
2237
2350
|
|
|
@@ -2267,7 +2380,8 @@ var Config3 = import_koishi7.Schema.intersect([
|
|
|
2267
2380
|
import_koishi7.Schema.object({
|
|
2268
2381
|
enableOriRecord: import_koishi7.Schema.boolean().default(true).description("启用原始记录"),
|
|
2269
2382
|
cacheRetentionDays: import_koishi7.Schema.number().min(0).default(30).description("记录保留天数"),
|
|
2270
|
-
enableWordCloud: import_koishi7.Schema.boolean().default(true).description("启用词云生成")
|
|
2383
|
+
enableWordCloud: import_koishi7.Schema.boolean().default(true).description("启用词云生成"),
|
|
2384
|
+
enableSimilarActivity: import_koishi7.Schema.boolean().default(true).description("启用相似活跃分析")
|
|
2271
2385
|
}).description("高级分析配置")
|
|
2272
2386
|
]);
|
|
2273
2387
|
async function parseQueryScope(ctx, session, options) {
|
|
@@ -2314,7 +2428,7 @@ function apply(ctx, config) {
|
|
|
2314
2428
|
new Stat(ctx, config).registerCommands(analyse);
|
|
2315
2429
|
if (config.enableWhoAt) new WhoAt(ctx, config).registerCommand(analyse);
|
|
2316
2430
|
if (config.enableDataIO) new Data(ctx).registerCommands(analyse);
|
|
2317
|
-
if (config.
|
|
2431
|
+
if (config.enableWordCloud || config.enableSimilarActivity) new Analyse(ctx, config).registerCommands(analyse);
|
|
2318
2432
|
}
|
|
2319
2433
|
__name(apply, "apply");
|
|
2320
2434
|
// Annotate the CommonJS export names for ESM import in node:
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -8,98 +8,111 @@
|
|
|
8
8
|
|
|
9
9
|
- **高效数据收集**:采用异步、高并发和缓存机制,在不影响机器人性能的前提下,精确高效地收集聊天数据。
|
|
10
10
|
- **多维度统计分析**:
|
|
11
|
-
- **命令统计** (`cmdstat`)
|
|
12
|
-
- **发言统计** (`msgstat`)
|
|
13
|
-
- **发言排行** (`rankstat`)
|
|
14
|
-
- **活跃度分析** (`activity`)
|
|
11
|
+
- **命令统计** (`cmdstat`):追踪指令使用频率,了解用户最常用的功能,支持按次数或时间排序。
|
|
12
|
+
- **发言统计** (`msgstat`):分析用户发言类型与数量,掌握核心用户群体,支持按条数或时间排序。
|
|
13
|
+
- **发言排行** (`rankstat`):生成指定时间范围内的用户发言排行榜,发掘群聊中的“龙王”。
|
|
14
|
+
- **活跃度分析** (`activity`):以小时或天为单位,生成直观的周期性活跃度图表,洞察社群活跃时段。
|
|
15
15
|
- **高级文本分析**:
|
|
16
|
-
- **词云生成** (`wordcloud`):基于聊天记录,利用 Jieba
|
|
17
|
-
-
|
|
16
|
+
- **词云生成** (`wordcloud`):基于聊天记录,利用 Jieba 分词生成热门话题词云图,快速了解近期热点。
|
|
17
|
+
- **相似活跃分析** (`simiactive`): 分析指定时间内,找出与您作息模式最相似的群友,并通过对比图表直观展示。
|
|
18
|
+
- **提及追踪** (`whoatme`):轻松查询谁在什么时候因为什么内容提及了您,不再错过重要信息。
|
|
18
19
|
- **强大的数据管理**:
|
|
19
|
-
- **备份与恢复** (
|
|
20
|
-
- **精确清理** (
|
|
20
|
+
- **备份与恢复** (`.backup`/`.restore`):一键备份所有统计数据至本地,并可随时恢复,保障数据安全。
|
|
21
|
+
- **精确清理** (`.clear`):提供多维度的筛选条件(如按时间、用户、群组、命令),精确清理不再需要的数据。
|
|
21
22
|
- **精美图表渲染**:借助 Puppeteer 服务,将复杂的统计数据渲染成美观、易读的图片,方便在聊天中分享。
|
|
22
23
|
- **高度可配置**:所有功能模块均可独立开关,并可自定义数据保留时长等核心参数,以适应不同场景的需求。
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
## ⚙️ 前置服务
|
|
25
26
|
|
|
26
27
|
本插件依赖以下 Koishi 服务,请确保您已正确安装并启用了它们:
|
|
27
28
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
29
|
+
- **`database`**:用于存储所有统计分析数据。
|
|
30
|
+
- **`puppeteer`**:用于将统计结果渲染成图片。
|
|
31
|
+
- **`cron`**:用于执行数据定时清理任务。
|
|
31
32
|
|
|
32
33
|
## 📝 使用说明
|
|
33
34
|
|
|
34
|
-
所有功能都集成在主指令 `analyse`
|
|
35
|
+
所有功能都集成在主指令 `analyse` 之下。大部分指令不加任何参数时,默认查询**当前群组**的数据。
|
|
35
36
|
|
|
36
37
|
### 指令列表
|
|
37
38
|
|
|
38
39
|
| 指令 | 别名 | 描述 | 选项 |
|
|
39
|
-
|
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `analyse.
|
|
48
|
-
| `analyse.
|
|
49
|
-
| `analyse.
|
|
40
|
+
| :--- | :--- | :--- | :--- |
|
|
41
|
+
| `cmdstat` | 命令统计 | 查询命令使用情况,展示方式根据选项变化 | `-u`, `-g`, `-a`, `-p`, `-s` |
|
|
42
|
+
| `msgstat` | 发言统计 | 查询用户发言统计,展示方式根据选项变化 | `-u`, `-g`, `-a`, `-t`, `-s` |
|
|
43
|
+
| `rankstat` | 发言排行 | 查询指定时间内的发言排行 | `-u`, `-g`, `-a`, `-t`, `-n`, `-o` |
|
|
44
|
+
| `activity` | 活跃统计 | 查询周期性活跃度图表 | `-u`, `-g`, `-a`, `-d`, `-n`, `-o` |
|
|
45
|
+
| `wordcloud` | 生成词云 | 基于聊天记录生成词云图 | `-u`, `-g`, `-t` |
|
|
46
|
+
| `simiactive`| 作息分析 | 分析并找出与您作息相似的群友 | `-n` |
|
|
47
|
+
| `whoatme` | 谁提及我 | 查看最近谁提及了您 | (无) |
|
|
48
|
+
| `analyse.list` | 列出数据 | (管理) 列出已记录的频道和命令 | (无) |
|
|
49
|
+
| `analyse.backup` | 备份数据 | (管理) 将所有数据备份为本地 JSON 文件 | (无) |
|
|
50
|
+
| `analyse.restore` | 恢复数据 | (管理) 从本地 JSON 文件恢复数据 | (无) |
|
|
51
|
+
| `analyse.clear` | 清除数据 | (管理) 根据条件精确清理数据 | `-t`, `-g`, `-u`, `-d`, `-c`, `-a` |
|
|
52
|
+
|
|
53
|
+
**通用选项说明:**
|
|
54
|
+
|
|
55
|
+
- `-u, --user <user>`: 指定用户 (可使用 @ 或 userID)。
|
|
56
|
+
- `-g, --guild <guild>`: 指定群组 (需使用群组ID)。
|
|
57
|
+
- `-a, --all`: 查询全局数据。
|
|
50
58
|
|
|
51
59
|
### 🔎 指令详解
|
|
52
60
|
|
|
53
|
-
#### `
|
|
61
|
+
#### `cmdstat` (命令统计)
|
|
54
62
|
|
|
55
|
-
- **`
|
|
56
|
-
- **`
|
|
57
|
-
- **`
|
|
58
|
-
- **`
|
|
59
|
-
- **`
|
|
60
|
-
- **选项 `-
|
|
63
|
+
- **`cmdstat`**: 查询**当前群组**所有用户的命令统计。
|
|
64
|
+
- **`cmdstat -u @用户`**: 查询**指定用户**在**所有群组**的命令统计。
|
|
65
|
+
- **`cmdstat -g <群组ID>`**: 查询**指定群组**所有用户的命令统计。
|
|
66
|
+
- **`cmdstat -u @用户 -g <群组ID>`**: 查询**指定用户**在**指定群组**的命令统计。
|
|
67
|
+
- **`cmdstat -a`**: 查询**全局**(所有用户+所有群组)的命令统计。
|
|
68
|
+
- **选项 `-p, --separate`**: 分离展示子命令,不合并到主命令。
|
|
69
|
+
- **选项 `-s, --sortByTime`**: 按最后使用时间降序排序(默认按使用次数)。
|
|
61
70
|
|
|
62
|
-
#### `
|
|
71
|
+
#### `msgstat` (发言统计)
|
|
63
72
|
|
|
64
|
-
- **`
|
|
65
|
-
- **`
|
|
66
|
-
- **`
|
|
67
|
-
- **`
|
|
68
|
-
- **`
|
|
69
|
-
- **选项 `-t <类型>`**:
|
|
73
|
+
- **`msgstat`**: 查询**当前群组**的发言统计 (按**用户**展示)。
|
|
74
|
+
- **`msgstat -u @用户`**: 查询**指定用户**的发言统计 (按**群组**展示)。
|
|
75
|
+
- **`msgstat -g <群组ID>`**: 查询**指定群组**的发言统计 (按**用户**展示)。
|
|
76
|
+
- **`msgstat -u @用户 -g <群组ID>`**: 查询**指定用户**在**指定群组**的发言统计 (按**消息类型**展示)。
|
|
77
|
+
- **`msgstat -a`**: 查询**全局**发言统计 (按**用户**展示)。
|
|
78
|
+
- **选项 `-t, --type <类型>`**: 筛选指定消息类型 (`text`, `face`, `image` 等),不改变上述展示逻辑。
|
|
79
|
+
- **选项 `-s, --sortByTime`**: 按最后发言时间降序排序(默认按发言条数)。
|
|
70
80
|
|
|
71
|
-
#### `
|
|
81
|
+
#### `rankstat` (发言排行)
|
|
72
82
|
|
|
73
|
-
- **`
|
|
74
|
-
- **`
|
|
75
|
-
- **`
|
|
76
|
-
- **`
|
|
77
|
-
-
|
|
78
|
-
- **选项 `-
|
|
79
|
-
- **选项 `-
|
|
80
|
-
- **选项 `-t <类型>`**: 筛选指定消息类型,不改变上述展示逻辑。
|
|
83
|
+
- **`rankstat`**: 查询**当前群组**的发言排行 (按**用户**排名)。
|
|
84
|
+
- **`rankstat -u @用户`**: 查询**指定用户**的发言排行 (按**群组**排名)。
|
|
85
|
+
- **`rankstat -u @用户 -g <群组ID>`**: 查询**指定用户**在**指定群组**的发言排行 (按**消息类型**排名)。
|
|
86
|
+
- **`rankstat -a`**: 查询**全局**发言排行 (按**用户**排名)。
|
|
87
|
+
- **选项 `-n, --duration <小时数>`**: 指定查询范围的时长,默认为 `24`。
|
|
88
|
+
- **选项 `-o, --offset <小时数>`**: 指定查询结束时间的偏移量(从现在往前推的小时数),默认为 `0`。
|
|
89
|
+
- **选项 `-t, --type <类型>`**: 筛选指定消息类型。
|
|
81
90
|
|
|
82
|
-
#### `
|
|
91
|
+
#### `activity` (活跃统计)
|
|
83
92
|
|
|
84
|
-
- **`
|
|
85
|
-
- **`
|
|
86
|
-
- **`
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
- **选项 `-n
|
|
90
|
-
- **选项 `-o <数值>`**: 指定查询结束时间的偏移量,默认为 `0`。
|
|
91
|
-
- **选项 `-d`**: 将 `-n` 和 `-o` 的单位从**小时**切换为**天**。
|
|
93
|
+
- **`activity`**: 查询**当前群组**的活跃度。
|
|
94
|
+
- **`activity -a`**: 查询**全局**活跃度。
|
|
95
|
+
- **`activity -u @用户 -g <群组ID>`**: 查询**指定用户**在**指定群组**的活跃度。
|
|
96
|
+
- **选项 `-n, --duration <数值>`**: 指定查询范围的时长,默认为 `24`。
|
|
97
|
+
- **选项 `-o, --offset <数值>`**: 指定查询结束时间的偏移量,默认为 `0`。
|
|
98
|
+
- **选项 `-d, --days`**: 将 `-n` 和 `-o` 的单位从**小时**切换为**天**。
|
|
92
99
|
|
|
93
|
-
|
|
100
|
+
#### `simiactive` (相似活跃分析)
|
|
101
|
+
|
|
102
|
+
- **`simiactive`**: 在**当前群组**中,分析最近 **24 小时**内与您作息最相似的群友。
|
|
103
|
+
- **`simiactive -n 48`**: 指定分析最近 **48 小时**的数据。
|
|
104
|
+
- **选项 `-n, --hours <小时数>`**: 指定查询范围的时长,默认为 `24`。
|
|
105
|
+
|
|
106
|
+
## 🔧 配置项
|
|
94
107
|
|
|
95
108
|
您可以在插件的配置页面中调整以下选项:
|
|
96
109
|
|
|
97
|
-
|
|
110
|
+
### 杂项配置
|
|
98
111
|
|
|
99
112
|
- `enableListener`: **启用消息监听**。总开关,关闭后插件将停止所有数据收集。 (默认: `true`)
|
|
100
|
-
- `enableDataIO`: **启用数据管理**。控制 `.backup`, `.restore`, `.clear`, `.list`
|
|
113
|
+
- `enableDataIO`: **启用数据管理**。控制 `.backup`, `.restore`, `.clear`, `.list` 等管理指令的可用性。 (默认: `true`)
|
|
101
114
|
|
|
102
|
-
|
|
115
|
+
### 基础分析配置
|
|
103
116
|
|
|
104
117
|
- `enableCmdStat`: **启用命令统计**。 (默认: `true`)
|
|
105
118
|
- `enableMsgStat`: **启用消息统计**。 (默认: `true`)
|
|
@@ -109,8 +122,16 @@
|
|
|
109
122
|
- `enableWhoAt`: **启用提及记录**。 (默认: `true`)
|
|
110
123
|
- `atRetentionDays`: **提及保留天数**。`whoatme` 数据的保留时长(天),`0` 为永久保留。 (默认: `3`)
|
|
111
124
|
|
|
112
|
-
|
|
125
|
+
### 高级分析配置
|
|
113
126
|
|
|
114
127
|
- `enableOriRecord`: **启用原始记录**。是否记录原始消息内容以供词云等功能使用。 (默认: `true`)
|
|
115
|
-
- `enableWordCloud`: **启用词云生成**。 (默认: `true`)
|
|
116
128
|
- `cacheRetentionDays`: **原始记录保留天数**。原始消息记录的保留时长(天),`0` 为永久保留。 (默认: `30`)
|
|
129
|
+
- `enableWordCloud`: **启用词云生成** (依赖`原始记录`)。 (默认: `true`)
|
|
130
|
+
- `enablesimiactive`: **启用相似活跃分析** (依赖`发言排行`或`活跃统计`)。 (默认: `true`)
|
|
131
|
+
|
|
132
|
+
## 📌 注意事项
|
|
133
|
+
|
|
134
|
+
1. **Puppeteer 配置**:本插件的图片渲染强依赖 `puppeteer` 服务。请确保您已正确安装并配置了该服务,包括正确设置了可执行文件路径(如有需要)。渲染失败通常与此有关。
|
|
135
|
+
2. **初始数据积累**:插件启用后,需要一段时间来收集数据。因此,在刚安装插件后立即查询可能不会返回任何结果。
|
|
136
|
+
3. **数据清理**:插件会根据您设置的保留天数自动清理过期数据。对于手动清理 (`.clear`),请谨慎操作,特别是 `-a` (清除全部) 选项,该操作不可逆。
|
|
137
|
+
4. **性能考虑**:尽管插件经过优化,但在极大规模的机器人(数千群聊)上,长时间积累的大量数据仍可能对数据库造成压力。建议定期使用 `.clear -d <天数>` 清理过旧的数据。
|