koishi-plugin-chat-analyse 0.5.5 → 0.6.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/lib/Analyse.d.ts +22 -0
- package/lib/Renderer.d.ts +9 -0
- package/lib/Stat.d.ts +0 -14
- package/lib/index.d.ts +35 -1
- package/lib/index.js +223 -70
- package/package.json +7 -3
package/lib/Analyse.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Context, Command } from 'koishi';
|
|
2
|
+
import { Config } from './index';
|
|
3
|
+
export interface WordCloudData {
|
|
4
|
+
title: string;
|
|
5
|
+
time: Date;
|
|
6
|
+
words: [string, number][];
|
|
7
|
+
}
|
|
8
|
+
export declare class Analyse {
|
|
9
|
+
private ctx;
|
|
10
|
+
private config;
|
|
11
|
+
private renderer;
|
|
12
|
+
private nlp;
|
|
13
|
+
private isNlpReady;
|
|
14
|
+
constructor(ctx: Context, config: Config);
|
|
15
|
+
/**
|
|
16
|
+
* @private
|
|
17
|
+
* @method initializeNlp
|
|
18
|
+
* @description 异步加载并训练 NLP 模型。
|
|
19
|
+
*/
|
|
20
|
+
private initializeNlp;
|
|
21
|
+
registerCommands(cmd: Command): void;
|
|
22
|
+
}
|
package/lib/Renderer.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
|
+
import { WordCloudData } from './Analyse';
|
|
2
3
|
/**
|
|
3
4
|
* @interface ListRenderData
|
|
4
5
|
* @description 定义了调用 `renderList` 方法所需的数据结构。
|
|
@@ -74,4 +75,12 @@ export declare class Renderer {
|
|
|
74
75
|
* @returns {Promise<string | Buffer[]>} - 成功时返回包含图片 Buffer 的数组,失败或无数据时返回提示字符串。
|
|
75
76
|
*/
|
|
76
77
|
renderCircadianChart(data: CircadianChartData): Promise<string | Buffer[]>;
|
|
78
|
+
/**
|
|
79
|
+
* @public
|
|
80
|
+
* @method renderWordCloud
|
|
81
|
+
* @description 将词频数据渲染成一张词云图片,使用 Puppeteer 和 wordcloud2.js。
|
|
82
|
+
* @param {WordCloudData} data - 包含标题、时间和词汇列表的对象。
|
|
83
|
+
* @returns {Promise<string | Buffer[]>} - 成功时返回图片 Buffer 数组,否则返回提示。
|
|
84
|
+
*/
|
|
85
|
+
renderWordCloud(data: WordCloudData): Promise<string | Buffer[]>;
|
|
77
86
|
}
|
package/lib/Stat.d.ts
CHANGED
|
@@ -20,18 +20,4 @@ export declare class Stat {
|
|
|
20
20
|
* @param cmd - 主命令实例。
|
|
21
21
|
*/
|
|
22
22
|
registerCommands(cmd: Command): void;
|
|
23
|
-
/**
|
|
24
|
-
* @private @method parseQueryScope
|
|
25
|
-
* @description 解析命令选项,转换为包含 UIDs 和描述性信息的统一查询范围对象。
|
|
26
|
-
* @param session - 当前会话对象。
|
|
27
|
-
* @param options - 命令选项。
|
|
28
|
-
* @returns 包含 uids、错误或范围描述的查询范围对象。
|
|
29
|
-
*/
|
|
30
|
-
private parseQueryScope;
|
|
31
|
-
/**
|
|
32
|
-
* @private @method generateTitle
|
|
33
|
-
* @description 根据查询范围和类型动态生成易于理解的图片标题。
|
|
34
|
-
* @returns 生成的标题字符串。
|
|
35
|
-
*/
|
|
36
|
-
private generateTitle;
|
|
37
23
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Schema } from 'koishi';
|
|
1
|
+
import { Context, Schema, Session } from 'koishi';
|
|
2
2
|
/** @name 插件使用说明 */
|
|
3
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
4
|
export declare const name = "chat-analyse";
|
|
@@ -18,9 +18,43 @@ export interface Config {
|
|
|
18
18
|
enableDataIO: boolean;
|
|
19
19
|
atRetentionDays: number;
|
|
20
20
|
rankRetentionDays: number;
|
|
21
|
+
enableWordCloud: boolean;
|
|
22
|
+
enableVocabulary: boolean;
|
|
21
23
|
}
|
|
22
24
|
/** @description 插件的配置项定义 */
|
|
23
25
|
export declare const Config: Schema<Config>;
|
|
26
|
+
/**
|
|
27
|
+
* @private @method parseQueryScope
|
|
28
|
+
* @description 解析命令选项,转换为包含 UIDs 和描述性信息的统一查询范围对象。
|
|
29
|
+
* @param session - 当前会话对象。
|
|
30
|
+
* @param options - 命令选项。
|
|
31
|
+
* @returns 包含 uids、错误或范围描述的查询范围对象。
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseQueryScope(ctx: Context, session: Session, options: {
|
|
34
|
+
user?: string;
|
|
35
|
+
guild?: string;
|
|
36
|
+
all?: boolean;
|
|
37
|
+
}): Promise<{
|
|
38
|
+
uids?: number[];
|
|
39
|
+
error?: string;
|
|
40
|
+
scopeDesc: {
|
|
41
|
+
guildId?: string;
|
|
42
|
+
userId?: string;
|
|
43
|
+
};
|
|
44
|
+
}>;
|
|
45
|
+
/**
|
|
46
|
+
* @private @method generateTitle
|
|
47
|
+
* @description 根据查询范围和类型动态生成易于理解的图片标题。
|
|
48
|
+
* @returns 生成的标题字符串。
|
|
49
|
+
*/
|
|
50
|
+
export declare function generateTitle(ctx: Context, scopeDesc: {
|
|
51
|
+
guildId?: string;
|
|
52
|
+
userId?: string;
|
|
53
|
+
}, options: {
|
|
54
|
+
main: string;
|
|
55
|
+
subtype?: string;
|
|
56
|
+
timeRange?: number;
|
|
57
|
+
}): Promise<string>;
|
|
24
58
|
/**
|
|
25
59
|
* @function apply
|
|
26
60
|
* @description Koishi 插件的主入口函数,负责初始化和注册所有功能模块。
|
package/lib/index.js
CHANGED
|
@@ -30,14 +30,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
-
Config: () =>
|
|
33
|
+
Config: () => Config3,
|
|
34
34
|
apply: () => apply,
|
|
35
|
+
generateTitle: () => generateTitle,
|
|
35
36
|
name: () => name,
|
|
37
|
+
parseQueryScope: () => parseQueryScope,
|
|
36
38
|
usage: () => usage,
|
|
37
39
|
using: () => using
|
|
38
40
|
});
|
|
39
41
|
module.exports = __toCommonJS(src_exports);
|
|
40
|
-
var
|
|
42
|
+
var import_koishi7 = require("koishi");
|
|
41
43
|
|
|
42
44
|
// src/Collector.ts
|
|
43
45
|
var import_koishi = require("koishi");
|
|
@@ -301,7 +303,7 @@ var Renderer = class {
|
|
|
301
303
|
async htmlToImage(fullHtmlContent) {
|
|
302
304
|
const page = await this.ctx.puppeteer.page();
|
|
303
305
|
try {
|
|
304
|
-
await page.setViewport({ width:
|
|
306
|
+
await page.setViewport({ width: 850, height: 10, deviceScaleFactor: 2 });
|
|
305
307
|
await page.setContent(fullHtmlContent, { waitUntil: "networkidle0" });
|
|
306
308
|
const { width, height } = await page.evaluate(() => ({
|
|
307
309
|
width: document.body.scrollWidth,
|
|
@@ -348,7 +350,7 @@ var Renderer = class {
|
|
|
348
350
|
const CHUNK_SIZE = 100;
|
|
349
351
|
const imageBuffers = [];
|
|
350
352
|
const totalItems = list.length;
|
|
351
|
-
const countHeaderIndex = headers?.findIndex((
|
|
353
|
+
const countHeaderIndex = headers?.findIndex((h6) => ["总计发言", "条数", "次数", "数量"].includes(h6)) ?? -1;
|
|
352
354
|
const totalCount = data.total || (countHeaderIndex > -1 ? list.reduce((sum, row) => sum + (Number(row[countHeaderIndex]) || 0), 0) : totalItems);
|
|
353
355
|
const renderCell = /* @__PURE__ */ __name((cell, i) => {
|
|
354
356
|
const headerText = headers?.[i] || "";
|
|
@@ -396,7 +398,7 @@ var Renderer = class {
|
|
|
396
398
|
<thead>
|
|
397
399
|
<tr>
|
|
398
400
|
<th class="rank-cell">#</th>
|
|
399
|
-
${headers.map((
|
|
401
|
+
${headers.map((h6) => `<th>${h6}</th>`).join("")}
|
|
400
402
|
</tr>
|
|
401
403
|
</thead>` : ""}
|
|
402
404
|
<tbody>
|
|
@@ -457,6 +459,54 @@ var Renderer = class {
|
|
|
457
459
|
const imageBuffer = await this.htmlToImage(fullHtml);
|
|
458
460
|
return imageBuffer ? [imageBuffer] : "图片渲染失败";
|
|
459
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* @public
|
|
464
|
+
* @method renderWordCloud
|
|
465
|
+
* @description 将词频数据渲染成一张词云图片,使用 Puppeteer 和 wordcloud2.js。
|
|
466
|
+
* @param {WordCloudData} data - 包含标题、时间和词汇列表的对象。
|
|
467
|
+
* @returns {Promise<string | Buffer[]>} - 成功时返回图片 Buffer 数组,否则返回提示。
|
|
468
|
+
*/
|
|
469
|
+
async renderWordCloud(data) {
|
|
470
|
+
const { title, time, words } = data;
|
|
471
|
+
if (!words?.length) return "暂无数据可供渲染";
|
|
472
|
+
const wordListJson = JSON.stringify(words);
|
|
473
|
+
const cardHtml = `
|
|
474
|
+
<div class="container">
|
|
475
|
+
<div class="header">
|
|
476
|
+
<div class="stat-chip">词数: <span>${words.length}</span></div>
|
|
477
|
+
<h1 class="title-text">${title}</h1>
|
|
478
|
+
<div class="time-label">${time.toLocaleString("zh-CN", { hour12: false })}</div>
|
|
479
|
+
</div>
|
|
480
|
+
<div id="wordcloud-container" style="width: 800px; height: 600px; margin: auto;"></div>
|
|
481
|
+
<script src="https://cdn.jsdelivr.net/npm/wordcloud@1.2.2/src/wordcloud2.js"></script>
|
|
482
|
+
<script>
|
|
483
|
+
WordCloud(document.getElementById('wordcloud-container'), {
|
|
484
|
+
list: ${wordListJson},
|
|
485
|
+
gridSize: 16,
|
|
486
|
+
weightFactor: (size) => Math.pow(size, 1.2) * 2.5,
|
|
487
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
|
|
488
|
+
color: 'random-dark',
|
|
489
|
+
backgroundColor: 'transparent',
|
|
490
|
+
rotateRatio: 0.5,
|
|
491
|
+
minRotation: -Math.PI / 6,
|
|
492
|
+
maxRotation: Math.PI / 6,
|
|
493
|
+
shuffle: false,
|
|
494
|
+
});
|
|
495
|
+
</script>
|
|
496
|
+
</div>`;
|
|
497
|
+
const fullHtml = `<!DOCTYPE html>
|
|
498
|
+
<html>
|
|
499
|
+
<head>
|
|
500
|
+
<meta charset="UTF-8">
|
|
501
|
+
<style>${this.COMMON_STYLE}</style>
|
|
502
|
+
</head>
|
|
503
|
+
<body>
|
|
504
|
+
${cardHtml}
|
|
505
|
+
</body>
|
|
506
|
+
</html>`;
|
|
507
|
+
const imageBuffer = await this.htmlToImage(fullHtml);
|
|
508
|
+
return imageBuffer ? [imageBuffer] : "图片渲染失败";
|
|
509
|
+
}
|
|
460
510
|
};
|
|
461
511
|
|
|
462
512
|
// src/Stat.ts
|
|
@@ -488,7 +538,7 @@ var Stat = class {
|
|
|
488
538
|
registerCommands(cmd) {
|
|
489
539
|
const createHandler = /* @__PURE__ */ __name((handler) => {
|
|
490
540
|
return async ({ session, options }) => {
|
|
491
|
-
const scope = await this.
|
|
541
|
+
const scope = await parseQueryScope(this.ctx, session, options);
|
|
492
542
|
if (scope.error) return scope.error;
|
|
493
543
|
try {
|
|
494
544
|
const result = await handler(scope, options);
|
|
@@ -527,7 +577,7 @@ var Stat = class {
|
|
|
527
577
|
}
|
|
528
578
|
const total = processedStats.reduce((sum, record) => sum + record.count, 0);
|
|
529
579
|
const list = processedStats.map((item) => [item.command, item.count, item.lastUsed]);
|
|
530
|
-
const title = await this.
|
|
580
|
+
const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "命令" });
|
|
531
581
|
return this.renderer.renderList({ title, time: /* @__PURE__ */ new Date(), total, list }, ["命令", "次数", "最后使用"]);
|
|
532
582
|
}));
|
|
533
583
|
}
|
|
@@ -542,7 +592,7 @@ var Stat = class {
|
|
|
542
592
|
if (stats.length === 0) return "暂无统计数据";
|
|
543
593
|
const total = stats.reduce((sum, r) => sum + r.count, 0);
|
|
544
594
|
const list = stats.map((item) => [userNameMap.get(item.uid) || `UID ${item.uid}`, item.count, item.lastUsed]);
|
|
545
|
-
const title = await this.
|
|
595
|
+
const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "发言", subtype: type });
|
|
546
596
|
const headers = type ? ["用户", "条数", "最后发言"] : ["用户", "总计发言", "最后发言"];
|
|
547
597
|
return this.renderer.renderList({ title, time: /* @__PURE__ */ new Date(), total, list }, headers);
|
|
548
598
|
}));
|
|
@@ -560,7 +610,7 @@ var Stat = class {
|
|
|
560
610
|
const total = rankStats.reduce((sum, record) => sum + record.count, 0);
|
|
561
611
|
const list = rankStats.map((item) => [userNameMap.get(item.uid) || `UID ${item.uid}`, item.count]);
|
|
562
612
|
const listWithPercentage = list.map((row) => [...row, total > 0 ? `${(row[1] / total * 100).toFixed(2)}%` : "0.00%"]);
|
|
563
|
-
const title = await this.
|
|
613
|
+
const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "发言排行", timeRange: hours, subtype: type });
|
|
564
614
|
return this.renderer.renderList({ title, time: /* @__PURE__ */ new Date(), total, list: listWithPercentage }, ["用户", "总计发言", "占比"]);
|
|
565
615
|
}));
|
|
566
616
|
}
|
|
@@ -574,57 +624,11 @@ var Stat = class {
|
|
|
574
624
|
hourlyCounts[stat.timestamp.getHours()] += stat.count;
|
|
575
625
|
totalMessages += stat.count;
|
|
576
626
|
});
|
|
577
|
-
const title = await this.
|
|
627
|
+
const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "活跃" });
|
|
578
628
|
return this.renderer.renderCircadianChart({ title, time: /* @__PURE__ */ new Date(), total: totalMessages, data: hourlyCounts });
|
|
579
629
|
}));
|
|
580
630
|
}
|
|
581
631
|
}
|
|
582
|
-
/**
|
|
583
|
-
* @private @method parseQueryScope
|
|
584
|
-
* @description 解析命令选项,转换为包含 UIDs 和描述性信息的统一查询范围对象。
|
|
585
|
-
* @param session - 当前会话对象。
|
|
586
|
-
* @param options - 命令选项。
|
|
587
|
-
* @returns 包含 uids、错误或范围描述的查询范围对象。
|
|
588
|
-
*/
|
|
589
|
-
async parseQueryScope(session, options) {
|
|
590
|
-
const scopeDesc = { guildId: options.guild, userId: void 0 };
|
|
591
|
-
if (options.user) scopeDesc.userId = import_koishi3.h.select(options.user, "at")[0]?.attrs.id ?? options.user.trim();
|
|
592
|
-
if (!options.all && !scopeDesc.guildId && !scopeDesc.userId) scopeDesc.guildId = session.guildId;
|
|
593
|
-
if (!options.all && !scopeDesc.guildId && !scopeDesc.userId) return { error: "请指定查询范围", scopeDesc };
|
|
594
|
-
const query = {};
|
|
595
|
-
if (scopeDesc.guildId) query.channelId = scopeDesc.guildId;
|
|
596
|
-
if (scopeDesc.userId) query.userId = scopeDesc.userId;
|
|
597
|
-
if (Object.keys(query).length === 0) return { uids: void 0, scopeDesc };
|
|
598
|
-
const users = await this.ctx.database.get("analyse_user", query, ["uid"]);
|
|
599
|
-
if (users.length === 0) return { error: "暂无统计数据", scopeDesc };
|
|
600
|
-
return { uids: users.map((u) => u.uid), scopeDesc };
|
|
601
|
-
}
|
|
602
|
-
/**
|
|
603
|
-
* @private @method generateTitle
|
|
604
|
-
* @description 根据查询范围和类型动态生成易于理解的图片标题。
|
|
605
|
-
* @returns 生成的标题字符串。
|
|
606
|
-
*/
|
|
607
|
-
async generateTitle(scopeDesc, options) {
|
|
608
|
-
let guildName = "", userName = "", scopeText = "全局";
|
|
609
|
-
if (scopeDesc.guildId) {
|
|
610
|
-
const [guild] = await this.ctx.database.get("analyse_user", { channelId: scopeDesc.guildId }, ["channelName"]);
|
|
611
|
-
guildName = guild?.channelName || scopeDesc.guildId;
|
|
612
|
-
}
|
|
613
|
-
if (scopeDesc.userId) {
|
|
614
|
-
const [user] = await this.ctx.database.get("analyse_user", { userId: scopeDesc.userId }, ["userName"]);
|
|
615
|
-
userName = user?.userName || scopeDesc.userId;
|
|
616
|
-
}
|
|
617
|
-
const typeText = options.subtype ? `“${options.subtype}”` : "";
|
|
618
|
-
const mainText = options.main;
|
|
619
|
-
if (mainText.includes("排行")) {
|
|
620
|
-
scopeText = guildName || "全局";
|
|
621
|
-
return `${options.timeRange}小时${scopeText}${typeText}${mainText}`;
|
|
622
|
-
}
|
|
623
|
-
if (userName && guildName) scopeText = `${guildName} ${userName}`;
|
|
624
|
-
else if (userName) scopeText = userName;
|
|
625
|
-
else if (guildName) scopeText = guildName;
|
|
626
|
-
return `${scopeText}${typeText}${mainText}统计`;
|
|
627
|
-
}
|
|
628
632
|
};
|
|
629
633
|
|
|
630
634
|
// src/WhoAt.ts
|
|
@@ -825,6 +829,114 @@ ${commandOutput}`;
|
|
|
825
829
|
}
|
|
826
830
|
};
|
|
827
831
|
|
|
832
|
+
// src/Analyse.ts
|
|
833
|
+
var import_koishi6 = require("koishi");
|
|
834
|
+
var import_basic = require("@nlpjs/basic");
|
|
835
|
+
var import_lang_zh = require("@nlpjs/lang-zh");
|
|
836
|
+
var Analyse = class {
|
|
837
|
+
constructor(ctx, config) {
|
|
838
|
+
this.ctx = ctx;
|
|
839
|
+
this.config = config;
|
|
840
|
+
this.renderer = new Renderer(ctx);
|
|
841
|
+
this.nlp = new import_basic.Nlp({ languages: ["zh"], nlu: { log: false } });
|
|
842
|
+
this.nlp.container.register("extract-lang-zh", new import_lang_zh.LangZh());
|
|
843
|
+
this.initializeNlp().catch((err) => {
|
|
844
|
+
this.ctx.logger.error("NLP 语言模型加载失败:", err);
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
static {
|
|
848
|
+
__name(this, "Analyse");
|
|
849
|
+
}
|
|
850
|
+
renderer;
|
|
851
|
+
nlp;
|
|
852
|
+
isNlpReady = false;
|
|
853
|
+
/**
|
|
854
|
+
* @private
|
|
855
|
+
* @method initializeNlp
|
|
856
|
+
* @description 异步加载并训练 NLP 模型。
|
|
857
|
+
*/
|
|
858
|
+
async initializeNlp() {
|
|
859
|
+
await this.nlp.train();
|
|
860
|
+
this.isNlpReady = true;
|
|
861
|
+
}
|
|
862
|
+
registerCommands(cmd) {
|
|
863
|
+
if (this.config.enableWordCloud) {
|
|
864
|
+
cmd.subcommand(".wordcloud", "生成词云").usage("基于指定范围内的聊天记录生成词云图。").option("guild", "-g <guildId:string> 指定群组").option("user", "-u <user:string> 指定用户").option("all", "-a 全局").option("hours", "-h <hours:number> 指定时长", { fallback: 24 }).action(async ({ session, options }) => {
|
|
865
|
+
if (!this.isNlpReady) return "文本分析尚未就绪,请稍后再试";
|
|
866
|
+
const scope = await parseQueryScope(this.ctx, session, options);
|
|
867
|
+
if (scope.error) return scope.error;
|
|
868
|
+
const since = new Date(Date.now() - options.hours * import_koishi6.Time.hour);
|
|
869
|
+
const records = await this.ctx.database.select("analyse_cache").where({ uid: { $in: scope.uids }, timestamp: { $gte: since } }).project(["content"]).execute();
|
|
870
|
+
if (records.length === 0) return "暂无统计数据";
|
|
871
|
+
const allText = records.map((r) => r.content).join(" ");
|
|
872
|
+
const result = await this.nlp.process("zh", allText);
|
|
873
|
+
const words = result.stems.filter((stem) => stem.length > 1);
|
|
874
|
+
const wordCounts = words.reduce((map, word) => {
|
|
875
|
+
map.set(word, (map.get(word) || 0) + 1);
|
|
876
|
+
return map;
|
|
877
|
+
}, /* @__PURE__ */ new Map());
|
|
878
|
+
if (wordCounts.size === 0) return "暂无有效词语";
|
|
879
|
+
const wordList = Array.from(wordCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 150);
|
|
880
|
+
const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "词云" });
|
|
881
|
+
const renderResult = await this.renderer.renderWordCloud({ title, time: /* @__PURE__ */ new Date(), words: wordList });
|
|
882
|
+
if (typeof renderResult === "string") return renderResult;
|
|
883
|
+
if (Array.isArray(renderResult) && renderResult.length > 0) {
|
|
884
|
+
for (const buffer of renderResult) await session.sendQueued(import_koishi6.h.image(buffer, "image/png"));
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
if (this.config.enableVocabulary) {
|
|
889
|
+
cmd.subcommand(".vocabulary", "词汇排行").usage("根据不重复词汇量占总词汇量的比例进行排行。").option("guild", "-g <guildId:string> 指定群组").option("all", "-a 全局").option("hours", "-h <hours:number> 指定时长", { fallback: 24 }).action(async ({ session, options }) => {
|
|
890
|
+
if (!this.isNlpReady) return "文本分析尚未就绪,请稍后再试";
|
|
891
|
+
const scope = await parseQueryScope(this.ctx, session, options);
|
|
892
|
+
if (scope.error) return scope.error;
|
|
893
|
+
const users = await this.ctx.database.get("analyse_user", { uid: { $in: scope.uids } }, ["uid", "userName"]);
|
|
894
|
+
const userNameMap = new Map(users.map((u) => [u.uid, u.userName]));
|
|
895
|
+
const since = new Date(Date.now() - options.hours * import_koishi6.Time.hour);
|
|
896
|
+
const allRecords = await this.ctx.database.get("analyse_cache", { uid: { $in: scope.uids }, timestamp: { $gte: since } }, ["uid", "content"]);
|
|
897
|
+
if (allRecords.length === 0) return "暂无统计数据";
|
|
898
|
+
const messagesByUid = /* @__PURE__ */ new Map();
|
|
899
|
+
for (const record of allRecords) {
|
|
900
|
+
if (!messagesByUid.has(record.uid)) messagesByUid.set(record.uid, []);
|
|
901
|
+
messagesByUid.get(record.uid).push(record.content);
|
|
902
|
+
}
|
|
903
|
+
const richnessData = [];
|
|
904
|
+
for (const [uid, messages] of messagesByUid.entries()) {
|
|
905
|
+
const allText = messages.join(" ");
|
|
906
|
+
const result = await this.nlp.process("zh", allText);
|
|
907
|
+
const words = result.stems.filter((stem) => stem.length > 1);
|
|
908
|
+
if (words.length < 50) continue;
|
|
909
|
+
const uniqueWords = new Set(words);
|
|
910
|
+
const richness = uniqueWords.size / words.length;
|
|
911
|
+
richnessData.push({
|
|
912
|
+
name: userNameMap.get(uid) || `UID ${uid}`,
|
|
913
|
+
total: words.length,
|
|
914
|
+
unique: uniqueWords.size,
|
|
915
|
+
richness
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
if (richnessData.length === 0) return "暂无有效词语";
|
|
919
|
+
richnessData.sort((a, b) => b.richness - a.richness);
|
|
920
|
+
const list = richnessData.map((item) => [
|
|
921
|
+
item.name,
|
|
922
|
+
item.unique,
|
|
923
|
+
item.total,
|
|
924
|
+
`${(item.richness * 100).toFixed(2)}%`
|
|
925
|
+
]);
|
|
926
|
+
const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "词汇排行" });
|
|
927
|
+
const renderResult = await this.renderer.renderList(
|
|
928
|
+
{ title, time: /* @__PURE__ */ new Date(), total: richnessData.length, list },
|
|
929
|
+
["用户", "不重复词数", "总词数", "丰富度"]
|
|
930
|
+
);
|
|
931
|
+
if (typeof renderResult === "string") return renderResult;
|
|
932
|
+
if (Array.isArray(renderResult) && renderResult.length > 0) {
|
|
933
|
+
for (const buffer of renderResult) await session.sendQueued(import_koishi6.h.image(buffer, "image/png"));
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
|
|
828
940
|
// src/index.ts
|
|
829
941
|
var usage = `
|
|
830
942
|
<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);">
|
|
@@ -840,37 +952,78 @@ var usage = `
|
|
|
840
952
|
`;
|
|
841
953
|
var name = "chat-analyse";
|
|
842
954
|
var using = ["database", "puppeteer", "cron"];
|
|
843
|
-
var
|
|
844
|
-
|
|
845
|
-
enableListener:
|
|
846
|
-
enableDataIO:
|
|
955
|
+
var Config3 = import_koishi7.Schema.intersect([
|
|
956
|
+
import_koishi7.Schema.object({
|
|
957
|
+
enableListener: import_koishi7.Schema.boolean().default(true).description("启用消息监听"),
|
|
958
|
+
enableDataIO: import_koishi7.Schema.boolean().default(true).description("启用数据管理")
|
|
847
959
|
}).description("杂项配置"),
|
|
848
|
-
|
|
849
|
-
enableCmdStat:
|
|
850
|
-
enableMsgStat:
|
|
851
|
-
enableActivity:
|
|
852
|
-
enableRankStat:
|
|
853
|
-
rankRetentionDays:
|
|
854
|
-
enableWhoAt:
|
|
855
|
-
atRetentionDays:
|
|
960
|
+
import_koishi7.Schema.object({
|
|
961
|
+
enableCmdStat: import_koishi7.Schema.boolean().default(true).description("启用命令统计"),
|
|
962
|
+
enableMsgStat: import_koishi7.Schema.boolean().default(true).description("启用消息统计"),
|
|
963
|
+
enableActivity: import_koishi7.Schema.boolean().default(true).description("启用活跃统计"),
|
|
964
|
+
enableRankStat: import_koishi7.Schema.boolean().default(true).description("启用发言排行"),
|
|
965
|
+
rankRetentionDays: import_koishi7.Schema.number().min(0).default(31).description("排行保留天数"),
|
|
966
|
+
enableWhoAt: import_koishi7.Schema.boolean().default(true).description("启用提及记录"),
|
|
967
|
+
atRetentionDays: import_koishi7.Schema.number().min(0).default(7).description("提及保留天数")
|
|
856
968
|
}).description("基础分析配置"),
|
|
857
|
-
|
|
858
|
-
enableOriRecord:
|
|
969
|
+
import_koishi7.Schema.object({
|
|
970
|
+
enableOriRecord: import_koishi7.Schema.boolean().default(true).description("启用原始记录"),
|
|
971
|
+
enableWordCloud: import_koishi7.Schema.boolean().default(true).description("启用词云生成"),
|
|
972
|
+
enableVocabulary: import_koishi7.Schema.boolean().default(true).description("启用词汇排行")
|
|
859
973
|
}).description("高级分析配置")
|
|
860
974
|
]);
|
|
975
|
+
async function parseQueryScope(ctx, session, options) {
|
|
976
|
+
const scopeDesc = { guildId: options.guild, userId: void 0 };
|
|
977
|
+
if (options.user) scopeDesc.userId = import_koishi7.h.select(options.user, "at")[0]?.attrs.id ?? options.user.trim();
|
|
978
|
+
if (!options.all && !scopeDesc.guildId && !scopeDesc.userId) scopeDesc.guildId = session.guildId;
|
|
979
|
+
if (!options.all && !scopeDesc.guildId && !scopeDesc.userId) return { error: "请指定查询范围", scopeDesc };
|
|
980
|
+
const query = {};
|
|
981
|
+
if (scopeDesc.guildId) query.channelId = scopeDesc.guildId;
|
|
982
|
+
if (scopeDesc.userId) query.userId = scopeDesc.userId;
|
|
983
|
+
if (Object.keys(query).length === 0) return { uids: void 0, scopeDesc };
|
|
984
|
+
const users = await ctx.database.get("analyse_user", query, ["uid"]);
|
|
985
|
+
if (users.length === 0) return { error: "暂无统计数据", scopeDesc };
|
|
986
|
+
return { uids: users.map((u) => u.uid), scopeDesc };
|
|
987
|
+
}
|
|
988
|
+
__name(parseQueryScope, "parseQueryScope");
|
|
989
|
+
async function generateTitle(ctx, scopeDesc, options) {
|
|
990
|
+
let guildName = "", userName = "", scopeText = "全局";
|
|
991
|
+
if (scopeDesc.guildId) {
|
|
992
|
+
const [guild] = await ctx.database.get("analyse_user", { channelId: scopeDesc.guildId }, ["channelName"]);
|
|
993
|
+
guildName = guild?.channelName || scopeDesc.guildId;
|
|
994
|
+
}
|
|
995
|
+
if (scopeDesc.userId) {
|
|
996
|
+
const [user] = await ctx.database.get("analyse_user", { userId: scopeDesc.userId }, ["userName"]);
|
|
997
|
+
userName = user?.userName || scopeDesc.userId;
|
|
998
|
+
}
|
|
999
|
+
const typeText = options.subtype ? `“${options.subtype}”` : "";
|
|
1000
|
+
const mainText = options.main;
|
|
1001
|
+
if (mainText.includes("排行")) {
|
|
1002
|
+
scopeText = guildName || "全局";
|
|
1003
|
+
return `${options.timeRange}小时${scopeText}${typeText}${mainText}`;
|
|
1004
|
+
}
|
|
1005
|
+
if (userName && guildName) scopeText = `${guildName} ${userName}`;
|
|
1006
|
+
else if (userName) scopeText = userName;
|
|
1007
|
+
else if (guildName) scopeText = guildName;
|
|
1008
|
+
return `${scopeText}${typeText}${mainText}统计`;
|
|
1009
|
+
}
|
|
1010
|
+
__name(generateTitle, "generateTitle");
|
|
861
1011
|
function apply(ctx, config) {
|
|
862
1012
|
if (config.enableListener) new Collector(ctx, config);
|
|
863
1013
|
const analyse = ctx.command("analyse", "数据分析");
|
|
864
1014
|
new Stat(ctx, config).registerCommands(analyse);
|
|
865
1015
|
if (config.enableWhoAt) new WhoAt(ctx, config).registerCommand(analyse);
|
|
866
1016
|
if (config.enableDataIO) new Data(ctx).registerCommands(analyse);
|
|
1017
|
+
if (config.enableOriRecord && (config.enableWordCloud || config.enableVocabulary)) new Analyse(ctx, config).registerCommands(analyse);
|
|
867
1018
|
}
|
|
868
1019
|
__name(apply, "apply");
|
|
869
1020
|
// Annotate the CommonJS export names for ESM import in node:
|
|
870
1021
|
0 && (module.exports = {
|
|
871
1022
|
Config,
|
|
872
1023
|
apply,
|
|
1024
|
+
generateTitle,
|
|
873
1025
|
name,
|
|
1026
|
+
parseQueryScope,
|
|
874
1027
|
usage,
|
|
875
1028
|
using
|
|
876
1029
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-chat-analyse",
|
|
3
3
|
"description": "聊天记录分析",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.1",
|
|
5
5
|
"contributors": [
|
|
6
6
|
"Yis_Rime <yis_rime@outlook.com>"
|
|
7
7
|
],
|
|
@@ -32,10 +32,14 @@
|
|
|
32
32
|
"platform"
|
|
33
33
|
],
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"koishi-plugin-
|
|
36
|
-
"koishi-plugin-
|
|
35
|
+
"koishi-plugin-cron": "^3.0.0",
|
|
36
|
+
"koishi-plugin-puppeteer": "^3.5.2"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"koishi": "4.18.8"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@nlpjs/basic": "^4.26.1",
|
|
43
|
+
"@nlpjs/lang-zh": "^4.26.1"
|
|
40
44
|
}
|
|
41
45
|
}
|