koishi-plugin-chat-analyse 0.5.4 → 0.6.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,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: () => 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 import_koishi6 = require("koishi");
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: 720, height: 10, deviceScaleFactor: 2 });
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((h4) => ["总计发言", "条数", "次数", "数量"].includes(h4)) ?? -1;
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((h4) => `<th>${h4}</th>`).join("")}
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.parseQueryScope(session, options);
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);
@@ -504,12 +554,30 @@ var Stat = class {
504
554
  };
505
555
  }, "createHandler");
506
556
  if (this.config.enableCmdStat) {
507
- cmd.subcommand("cmdstat", "命令统计").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("all", "-a 全局").action(createHandler(async (scope) => {
557
+ cmd.subcommand("cmdstat", "命令统计").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("separate", "-h 分离展示").option("all", "-a 全局").action(createHandler(async (scope, options) => {
508
558
  const stats = await this.ctx.database.select("analyse_cmd").where({ uid: { $in: scope.uids } }).groupBy("command", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"), lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed") }).orderBy("count", "desc").execute();
509
559
  if (stats.length === 0) return "暂无统计数据";
510
- const total = stats.reduce((sum, record) => sum + record.count, 0);
511
- const list = stats.map((item) => [item.command, item.count, item.lastUsed]);
512
- const title = await this.generateTitle(scope.scopeDesc, { main: "命令" });
560
+ let processedStats;
561
+ if (options.separate) {
562
+ processedStats = stats;
563
+ } else {
564
+ const mergedStatsMap = /* @__PURE__ */ new Map();
565
+ for (const stat of stats) {
566
+ const mainCommand = stat.command.split(".")[0];
567
+ const existing = mergedStatsMap.get(mainCommand) || { count: 0, lastUsed: /* @__PURE__ */ new Date(0) };
568
+ existing.count += stat.count;
569
+ if (stat.lastUsed > existing.lastUsed) existing.lastUsed = stat.lastUsed;
570
+ mergedStatsMap.set(mainCommand, existing);
571
+ }
572
+ processedStats = Array.from(mergedStatsMap.entries()).map(([command, data]) => ({
573
+ command,
574
+ count: data.count,
575
+ lastUsed: data.lastUsed
576
+ })).sort((a, b) => b.count - a.count);
577
+ }
578
+ const total = processedStats.reduce((sum, record) => sum + record.count, 0);
579
+ const list = processedStats.map((item) => [item.command, item.count, item.lastUsed]);
580
+ const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "命令" });
513
581
  return this.renderer.renderList({ title, time: /* @__PURE__ */ new Date(), total, list }, ["命令", "次数", "最后使用"]);
514
582
  }));
515
583
  }
@@ -524,7 +592,7 @@ var Stat = class {
524
592
  if (stats.length === 0) return "暂无统计数据";
525
593
  const total = stats.reduce((sum, r) => sum + r.count, 0);
526
594
  const list = stats.map((item) => [userNameMap.get(item.uid) || `UID ${item.uid}`, item.count, item.lastUsed]);
527
- const title = await this.generateTitle(scope.scopeDesc, { main: "发言", subtype: type });
595
+ const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "发言", subtype: type });
528
596
  const headers = type ? ["用户", "条数", "最后发言"] : ["用户", "总计发言", "最后发言"];
529
597
  return this.renderer.renderList({ title, time: /* @__PURE__ */ new Date(), total, list }, headers);
530
598
  }));
@@ -542,7 +610,7 @@ var Stat = class {
542
610
  const total = rankStats.reduce((sum, record) => sum + record.count, 0);
543
611
  const list = rankStats.map((item) => [userNameMap.get(item.uid) || `UID ${item.uid}`, item.count]);
544
612
  const listWithPercentage = list.map((row) => [...row, total > 0 ? `${(row[1] / total * 100).toFixed(2)}%` : "0.00%"]);
545
- const title = await this.generateTitle(scope.scopeDesc, { main: "发言排行", timeRange: hours, subtype: type });
613
+ const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "发言排行", timeRange: hours, subtype: type });
546
614
  return this.renderer.renderList({ title, time: /* @__PURE__ */ new Date(), total, list: listWithPercentage }, ["用户", "总计发言", "占比"]);
547
615
  }));
548
616
  }
@@ -556,57 +624,11 @@ var Stat = class {
556
624
  hourlyCounts[stat.timestamp.getHours()] += stat.count;
557
625
  totalMessages += stat.count;
558
626
  });
559
- const title = await this.generateTitle(scope.scopeDesc, { main: "活跃" });
627
+ const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "活跃" });
560
628
  return this.renderer.renderCircadianChart({ title, time: /* @__PURE__ */ new Date(), total: totalMessages, data: hourlyCounts });
561
629
  }));
562
630
  }
563
631
  }
564
- /**
565
- * @private @method parseQueryScope
566
- * @description 解析命令选项,转换为包含 UIDs 和描述性信息的统一查询范围对象。
567
- * @param session - 当前会话对象。
568
- * @param options - 命令选项。
569
- * @returns 包含 uids、错误或范围描述的查询范围对象。
570
- */
571
- async parseQueryScope(session, options) {
572
- const scopeDesc = { guildId: options.guild, userId: void 0 };
573
- if (options.user) scopeDesc.userId = import_koishi3.h.select(options.user, "at")[0]?.attrs.id ?? options.user.trim();
574
- if (!options.all && !scopeDesc.guildId && !scopeDesc.userId) scopeDesc.guildId = session.guildId;
575
- if (!options.all && !scopeDesc.guildId && !scopeDesc.userId) return { error: "请指定查询范围", scopeDesc };
576
- const query = {};
577
- if (scopeDesc.guildId) query.channelId = scopeDesc.guildId;
578
- if (scopeDesc.userId) query.userId = scopeDesc.userId;
579
- if (Object.keys(query).length === 0) return { uids: void 0, scopeDesc };
580
- const users = await this.ctx.database.get("analyse_user", query, ["uid"]);
581
- if (users.length === 0) return { error: "暂无统计数据", scopeDesc };
582
- return { uids: users.map((u) => u.uid), scopeDesc };
583
- }
584
- /**
585
- * @private @method generateTitle
586
- * @description 根据查询范围和类型动态生成易于理解的图片标题。
587
- * @returns 生成的标题字符串。
588
- */
589
- async generateTitle(scopeDesc, options) {
590
- let guildName = "", userName = "", scopeText = "全局";
591
- if (scopeDesc.guildId) {
592
- const [guild] = await this.ctx.database.get("analyse_user", { channelId: scopeDesc.guildId }, ["channelName"]);
593
- guildName = guild?.channelName || scopeDesc.guildId;
594
- }
595
- if (scopeDesc.userId) {
596
- const [user] = await this.ctx.database.get("analyse_user", { userId: scopeDesc.userId }, ["userName"]);
597
- userName = user?.userName || scopeDesc.userId;
598
- }
599
- const typeText = options.subtype ? `“${options.subtype}”` : "";
600
- const mainText = options.main;
601
- if (mainText.includes("排行")) {
602
- scopeText = guildName || "全局";
603
- return `${options.timeRange}小时${scopeText}${typeText}${mainText}`;
604
- }
605
- if (userName && guildName) scopeText = `${guildName} ${userName}`;
606
- else if (userName) scopeText = userName;
607
- else if (guildName) scopeText = guildName;
608
- return `${scopeText}${typeText}${mainText}统计`;
609
- }
610
632
  };
611
633
 
612
634
  // src/WhoAt.ts
@@ -799,13 +821,120 @@ var Data = class {
799
821
  ]);
800
822
  const uniqueChannels = [...new Map(allChannelInfo.map((item) => [item.channelId, item])).values()];
801
823
  const channelOutput = uniqueChannels.length ? "频道列表:\n" + uniqueChannels.map((c) => `[${c.channelId}] ${c.channelName}`).join("\n") : "暂无频道记录";
802
- const commandOutput = commands.length ? "命令列表:\n" + commands.join(", ") : "暂无命令记录";
824
+ const commandNames = commands.map((c) => c.command);
825
+ const commandOutput = commandNames.length ? "命令列表:\n" + commandNames.join(", ") : "暂无命令记录";
803
826
  return `${channelOutput}
804
827
  ${commandOutput}`;
805
828
  });
806
829
  }
807
830
  };
808
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", "生成词云", { checkArgCount: false }).usage("基于指定范围内的聊天记录生成词云图。").option("guild", "-g <guildId:string> 指定群组").option("user", "-u <user:string> 指定用户").option("all", "-a 全局").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 records = await this.ctx.database.select("analyse_cache").where({ uid: { $in: scope.uids } }).project(["content"]).execute();
869
+ if (records.length === 0) return "暂无统计数据";
870
+ const allText = records.map((r) => r.content).join(" ");
871
+ const result = await this.nlp.process("zh", allText);
872
+ const words = result.stems.filter((stem) => stem.length > 1);
873
+ const wordCounts = words.reduce((map, word) => {
874
+ map.set(word, (map.get(word) || 0) + 1);
875
+ return map;
876
+ }, /* @__PURE__ */ new Map());
877
+ if (wordCounts.size === 0) return "暂无有效词语";
878
+ const wordList = Array.from(wordCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 150);
879
+ const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "词云" });
880
+ const renderResult = await this.renderer.renderWordCloud({ title, time: /* @__PURE__ */ new Date(), words: wordList });
881
+ if (typeof renderResult === "string") return renderResult;
882
+ if (Array.isArray(renderResult) && renderResult.length > 0) {
883
+ for (const buffer of renderResult) await session.sendQueued(import_koishi6.h.image(buffer, "image/png"));
884
+ }
885
+ });
886
+ }
887
+ if (this.config.enableVocabulary) {
888
+ cmd.subcommand(".vocabulary", "词汇排行", { checkArgCount: false }).usage("根据不重复词汇量占总词汇量的比例进行排行。").option("guild", "-g <guildId:string> 指定群组").option("all", "-a 全局").action(async ({ session, options }) => {
889
+ if (!this.isNlpReady) return "文本分析尚未就绪,请稍后再试";
890
+ const scope = await parseQueryScope(this.ctx, session, options);
891
+ if (scope.error) return scope.error;
892
+ const users = await this.ctx.database.get("analyse_user", { uid: { $in: scope.uids } }, ["uid", "userName"]);
893
+ const userNameMap = new Map(users.map((u) => [u.uid, u.userName]));
894
+ const allRecords = await this.ctx.database.get("analyse_cache", { uid: { $in: scope.uids } }, ["uid", "content"]);
895
+ if (allRecords.length === 0) return "暂无统计数据";
896
+ const messagesByUid = /* @__PURE__ */ new Map();
897
+ for (const record of allRecords) {
898
+ if (!messagesByUid.has(record.uid)) messagesByUid.set(record.uid, []);
899
+ messagesByUid.get(record.uid).push(record.content);
900
+ }
901
+ const richnessData = [];
902
+ for (const [uid, messages] of messagesByUid.entries()) {
903
+ const allText = messages.join(" ");
904
+ const result = await this.nlp.process("zh", allText);
905
+ const words = result.stems.filter((stem) => stem.length > 1);
906
+ if (words.length < 50) continue;
907
+ const uniqueWords = new Set(words);
908
+ const richness = uniqueWords.size / words.length;
909
+ richnessData.push({
910
+ name: userNameMap.get(uid) || `UID ${uid}`,
911
+ total: words.length,
912
+ unique: uniqueWords.size,
913
+ richness
914
+ });
915
+ }
916
+ if (richnessData.length === 0) return "暂无有效词语";
917
+ richnessData.sort((a, b) => b.richness - a.richness);
918
+ const list = richnessData.map((item) => [
919
+ item.name,
920
+ item.unique,
921
+ item.total,
922
+ `${(item.richness * 100).toFixed(2)}%`
923
+ ]);
924
+ const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "词汇排行" });
925
+ const renderResult = await this.renderer.renderList(
926
+ { title, time: /* @__PURE__ */ new Date(), total: richnessData.length, list },
927
+ ["用户", "不重复词数", "总词数", "丰富度"]
928
+ );
929
+ if (typeof renderResult === "string") return renderResult;
930
+ if (Array.isArray(renderResult) && renderResult.length > 0) {
931
+ for (const buffer of renderResult) await session.sendQueued(import_koishi6.h.image(buffer, "image/png"));
932
+ }
933
+ });
934
+ }
935
+ }
936
+ };
937
+
809
938
  // src/index.ts
810
939
  var usage = `
811
940
  <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);">
@@ -821,37 +950,78 @@ var usage = `
821
950
  `;
822
951
  var name = "chat-analyse";
823
952
  var using = ["database", "puppeteer", "cron"];
824
- var Config = import_koishi6.Schema.intersect([
825
- import_koishi6.Schema.object({
826
- enableListener: import_koishi6.Schema.boolean().default(true).description("启用消息监听"),
827
- enableDataIO: import_koishi6.Schema.boolean().default(true).description("启用数据管理")
953
+ var Config3 = import_koishi7.Schema.intersect([
954
+ import_koishi7.Schema.object({
955
+ enableListener: import_koishi7.Schema.boolean().default(true).description("启用消息监听"),
956
+ enableDataIO: import_koishi7.Schema.boolean().default(true).description("启用数据管理")
828
957
  }).description("杂项配置"),
829
- import_koishi6.Schema.object({
830
- enableCmdStat: import_koishi6.Schema.boolean().default(true).description("启用命令统计"),
831
- enableMsgStat: import_koishi6.Schema.boolean().default(true).description("启用消息统计"),
832
- enableActivity: import_koishi6.Schema.boolean().default(true).description("启用活跃统计"),
833
- enableRankStat: import_koishi6.Schema.boolean().default(true).description("启用发言排行"),
834
- rankRetentionDays: import_koishi6.Schema.number().min(0).default(31).description("排行保留天数"),
835
- enableWhoAt: import_koishi6.Schema.boolean().default(true).description("启用提及记录"),
836
- atRetentionDays: import_koishi6.Schema.number().min(0).default(7).description("提及保留天数")
958
+ import_koishi7.Schema.object({
959
+ enableCmdStat: import_koishi7.Schema.boolean().default(true).description("启用命令统计"),
960
+ enableMsgStat: import_koishi7.Schema.boolean().default(true).description("启用消息统计"),
961
+ enableActivity: import_koishi7.Schema.boolean().default(true).description("启用活跃统计"),
962
+ enableRankStat: import_koishi7.Schema.boolean().default(true).description("启用发言排行"),
963
+ rankRetentionDays: import_koishi7.Schema.number().min(0).default(31).description("排行保留天数"),
964
+ enableWhoAt: import_koishi7.Schema.boolean().default(true).description("启用提及记录"),
965
+ atRetentionDays: import_koishi7.Schema.number().min(0).default(7).description("提及保留天数")
837
966
  }).description("基础分析配置"),
838
- import_koishi6.Schema.object({
839
- enableOriRecord: import_koishi6.Schema.boolean().default(true).description("启用原始记录")
967
+ import_koishi7.Schema.object({
968
+ enableOriRecord: import_koishi7.Schema.boolean().default(true).description("启用原始记录"),
969
+ enableWordCloud: import_koishi7.Schema.boolean().default(true).description("启用词云生成"),
970
+ enableVocabulary: import_koishi7.Schema.boolean().default(true).description("启用词汇排行")
840
971
  }).description("高级分析配置")
841
972
  ]);
973
+ async function parseQueryScope(ctx, session, options) {
974
+ const scopeDesc = { guildId: options.guild, userId: void 0 };
975
+ if (options.user) scopeDesc.userId = import_koishi7.h.select(options.user, "at")[0]?.attrs.id ?? options.user.trim();
976
+ if (!options.all && !scopeDesc.guildId && !scopeDesc.userId) scopeDesc.guildId = session.guildId;
977
+ if (!options.all && !scopeDesc.guildId && !scopeDesc.userId) return { error: "请指定查询范围", scopeDesc };
978
+ const query = {};
979
+ if (scopeDesc.guildId) query.channelId = scopeDesc.guildId;
980
+ if (scopeDesc.userId) query.userId = scopeDesc.userId;
981
+ if (Object.keys(query).length === 0) return { uids: void 0, scopeDesc };
982
+ const users = await ctx.database.get("analyse_user", query, ["uid"]);
983
+ if (users.length === 0) return { error: "暂无统计数据", scopeDesc };
984
+ return { uids: users.map((u) => u.uid), scopeDesc };
985
+ }
986
+ __name(parseQueryScope, "parseQueryScope");
987
+ async function generateTitle(ctx, scopeDesc, options) {
988
+ let guildName = "", userName = "", scopeText = "全局";
989
+ if (scopeDesc.guildId) {
990
+ const [guild] = await ctx.database.get("analyse_user", { channelId: scopeDesc.guildId }, ["channelName"]);
991
+ guildName = guild?.channelName || scopeDesc.guildId;
992
+ }
993
+ if (scopeDesc.userId) {
994
+ const [user] = await ctx.database.get("analyse_user", { userId: scopeDesc.userId }, ["userName"]);
995
+ userName = user?.userName || scopeDesc.userId;
996
+ }
997
+ const typeText = options.subtype ? `“${options.subtype}”` : "";
998
+ const mainText = options.main;
999
+ if (mainText.includes("排行")) {
1000
+ scopeText = guildName || "全局";
1001
+ return `${options.timeRange}小时${scopeText}${typeText}${mainText}`;
1002
+ }
1003
+ if (userName && guildName) scopeText = `${guildName} ${userName}`;
1004
+ else if (userName) scopeText = userName;
1005
+ else if (guildName) scopeText = guildName;
1006
+ return `${scopeText}${typeText}${mainText}统计`;
1007
+ }
1008
+ __name(generateTitle, "generateTitle");
842
1009
  function apply(ctx, config) {
843
1010
  if (config.enableListener) new Collector(ctx, config);
844
1011
  const analyse = ctx.command("analyse", "数据分析");
845
1012
  new Stat(ctx, config).registerCommands(analyse);
846
1013
  if (config.enableWhoAt) new WhoAt(ctx, config).registerCommand(analyse);
847
1014
  if (config.enableDataIO) new Data(ctx).registerCommands(analyse);
1015
+ if (config.enableOriRecord && (config.enableWordCloud || config.enableVocabulary)) new Analyse(ctx, config).registerCommands(analyse);
848
1016
  }
849
1017
  __name(apply, "apply");
850
1018
  // Annotate the CommonJS export names for ESM import in node:
851
1019
  0 && (module.exports = {
852
1020
  Config,
853
1021
  apply,
1022
+ generateTitle,
854
1023
  name,
1024
+ parseQueryScope,
855
1025
  usage,
856
1026
  using
857
1027
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-chat-analyse",
3
3
  "description": "聊天记录分析",
4
- "version": "0.5.4",
4
+ "version": "0.6.0",
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-puppeteer": "^3.5.2",
36
- "koishi-plugin-cron": "^3.0.0"
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": "^5.0.0-alpha.5",
43
+ "@nlpjs/lang-zh": "^5.0.0-alpha.5"
40
44
  }
41
45
  }