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.
@@ -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);
@@ -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.generateTitle(scope.scopeDesc, { main: "命令" });
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.generateTitle(scope.scopeDesc, { main: "发言", subtype: type });
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.generateTitle(scope.scopeDesc, { main: "发言排行", timeRange: hours, subtype: type });
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.generateTitle(scope.scopeDesc, { main: "活跃" });
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 Config = import_koishi6.Schema.intersect([
844
- import_koishi6.Schema.object({
845
- enableListener: import_koishi6.Schema.boolean().default(true).description("启用消息监听"),
846
- enableDataIO: import_koishi6.Schema.boolean().default(true).description("启用数据管理")
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
- import_koishi6.Schema.object({
849
- enableCmdStat: import_koishi6.Schema.boolean().default(true).description("启用命令统计"),
850
- enableMsgStat: import_koishi6.Schema.boolean().default(true).description("启用消息统计"),
851
- enableActivity: import_koishi6.Schema.boolean().default(true).description("启用活跃统计"),
852
- enableRankStat: import_koishi6.Schema.boolean().default(true).description("启用发言排行"),
853
- rankRetentionDays: import_koishi6.Schema.number().min(0).default(31).description("排行保留天数"),
854
- enableWhoAt: import_koishi6.Schema.boolean().default(true).description("启用提及记录"),
855
- atRetentionDays: import_koishi6.Schema.number().min(0).default(7).description("提及保留天数")
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
- import_koishi6.Schema.object({
858
- enableOriRecord: import_koishi6.Schema.boolean().default(true).description("启用原始记录")
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.5.5",
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-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": "^4.26.1",
43
+ "@nlpjs/lang-zh": "^4.26.1"
40
44
  }
41
45
  }