koishi-plugin-chat-analyse 0.6.4 → 0.7.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.
package/lib/Analyse.d.ts CHANGED
@@ -5,18 +5,20 @@ export interface WordCloudData {
5
5
  time: Date;
6
6
  words: [string, number][];
7
7
  }
8
+ /**
9
+ * @class Analyse
10
+ * @description 提供文本分析功能,如生成词云。
11
+ */
8
12
  export declare class Analyse {
9
13
  private ctx;
10
14
  private config;
11
15
  private renderer;
12
- private nlp;
13
- private isNlpReady;
16
+ private readonly jieba;
14
17
  constructor(ctx: Context, config: Config);
15
18
  /**
16
- * @private
17
- * @method initializeNlp
18
- * @description 异步加载并训练 NLP 模型。
19
+ * @public @method registerCommands
20
+ * @description 在主命令下注册子命令。
21
+ * @param cmd - 主命令实例。
19
22
  */
20
- private initializeNlp;
21
23
  registerCommands(cmd: Command): void;
22
24
  }
package/lib/index.d.ts CHANGED
@@ -19,7 +19,6 @@ export interface Config {
19
19
  atRetentionDays: number;
20
20
  rankRetentionDays: number;
21
21
  enableWordCloud: boolean;
22
- enableVocabulary: boolean;
23
22
  }
24
23
  /** @description 插件的配置项定义 */
25
24
  export declare const Config: Schema<Config>;
package/lib/index.js CHANGED
@@ -553,7 +553,7 @@ var Stat = class {
553
553
  };
554
554
  }, "createHandler");
555
555
  if (this.config.enableCmdStat) {
556
- cmd.subcommand("cmdstat", "命令统计").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("separate", "-h 分离展示").option("all", "-a 全局").action(createHandler(async (scope, options) => {
556
+ cmd.subcommand("cmdstat", "命令统计").usage("查询命令统计,可指定查询范围,默认当前群组。").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("separate", "-h 分离展示").option("all", "-a 全局").action(createHandler(async (scope, options) => {
557
557
  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();
558
558
  if (stats.length === 0) return "暂无统计数据";
559
559
  let processedStats;
@@ -581,7 +581,7 @@ var Stat = class {
581
581
  }));
582
582
  }
583
583
  if (this.config.enableMsgStat) {
584
- cmd.subcommand("msgstat", "发言统计").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("type", "-t <type:string> 指定类型").option("all", "-a 全局").action(createHandler(async (scope, options) => {
584
+ cmd.subcommand("msgstat", "发言统计").usage("查询发言统计,可指定查询范围,默认当前群组。").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("type", "-t <type:string> 指定类型").option("all", "-a 全局").action(createHandler(async (scope, options) => {
585
585
  const { type } = options;
586
586
  const query = { uid: { $in: scope.uids } };
587
587
  if (type) query.type = type;
@@ -597,7 +597,7 @@ var Stat = class {
597
597
  }));
598
598
  }
599
599
  if (this.config.enableRankStat) {
600
- cmd.subcommand("rankstat", "发言排行").option("guild", "-g <guildId:string> 指定群组").option("type", "-t <type:string> 指定类型").option("hours", "-h <hours:number> 指定时长", { fallback: 24 }).option("all", "-a 全局").action(createHandler(async (scope, options) => {
600
+ cmd.subcommand("rankstat", "发言排行").usage("查询发言排行,可指定查询范围,默认当前群组。").option("guild", "-g <guildId:string> 指定群组").option("type", "-t <type:string> 指定类型").option("hours", "-h <hours:number> 指定时长", { fallback: 24 }).option("all", "-a 全局").action(createHandler(async (scope, options) => {
601
601
  const { hours, type } = options;
602
602
  const since = new Date(Date.now() - hours * import_koishi3.Time.hour);
603
603
  const query = { uid: { $in: scope.uids }, timestamp: { $gte: since } };
@@ -614,7 +614,7 @@ var Stat = class {
614
614
  }));
615
615
  }
616
616
  if (this.config.enableActivity) {
617
- cmd.subcommand("activity", "活跃统计").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("hours", "-h <hours:number> 指定偏移时长").option("all", "-a 全局").option("days", "-d 切换至天数").action(createHandler(async (scope, options) => {
617
+ cmd.subcommand("activity", "活跃统计").usage("查询活跃统计,可指定查询范围,默认当前群组。").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("hours", "-h <hours:number> 指定偏移时长").option("all", "-a 全局").option("days", "-d 切换至天数").action(createHandler(async (scope, options) => {
618
618
  const { days, hours } = options;
619
619
  if (days) {
620
620
  const timeRangeInDays = 24;
@@ -798,7 +798,7 @@ var Data = class {
798
798
  return "数据恢复失败";
799
799
  }
800
800
  });
801
- cmd.subcommand(".clear", "清除数据", { authority: 4 }).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 清除全部").usage(`根据指定条件清理统计数据,可以组合多个选项以精确控制清除范围。`).action(async ({ options }) => {
801
+ 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 }) => {
802
802
  if (Object.keys(options).length === 0) return "请指定清除条件";
803
803
  if (options.table && !ALL_TABLES.includes(options.table)) return `表名 ${options.table} 无效`;
804
804
  try {
@@ -871,107 +871,64 @@ ${commandOutput}`;
871
871
 
872
872
  // src/Analyse.ts
873
873
  var import_koishi6 = require("koishi");
874
- var import_basic = require("@nlpjs/basic");
875
- var import_lang_zh = require("@nlpjs/lang-zh");
874
+ var import_jieba = require("@node-rs/jieba");
875
+ var import_dict = require("@node-rs/jieba/dict");
876
876
  var Analyse = class {
877
877
  constructor(ctx, config) {
878
878
  this.ctx = ctx;
879
879
  this.config = config;
880
880
  this.renderer = new Renderer(ctx);
881
- this.nlp = new import_basic.Nlp({ languages: ["zh"], nlu: { log: false } });
882
- this.nlp.settings.autoSave = false;
883
- this.nlp.container.register("extract-lang-zh", new import_lang_zh.LangZh());
884
- this.initializeNlp().catch((err) => {
885
- this.ctx.logger.error("NLP 语言模型加载失败:", err);
886
- });
881
+ if (config.enableWordCloud) this.jieba = import_jieba.Jieba.withDict(import_dict.dict);
887
882
  }
888
883
  static {
889
884
  __name(this, "Analyse");
890
885
  }
891
886
  renderer;
892
- nlp;
893
- isNlpReady = false;
887
+ jieba = null;
894
888
  /**
895
- * @private
896
- * @method initializeNlp
897
- * @description 异步加载并训练 NLP 模型。
889
+ * @public @method registerCommands
890
+ * @description 在主命令下注册子命令。
891
+ * @param cmd - 主命令实例。
898
892
  */
899
- async initializeNlp() {
900
- await this.nlp.train();
901
- this.isNlpReady = true;
902
- }
903
893
  registerCommands(cmd) {
904
894
  if (this.config.enableWordCloud) {
905
- 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 }) => {
906
- if (!this.isNlpReady) return "文本分析尚未就绪,请稍后再试";
895
+ cmd.subcommand("wordcloud", "生成词云").usage("基于聊天记录生成词云图,可指定范围,默认当前群组。").option("guild", "-g <guildId:string> 指定群组").option("user", "-u <user:string> 指定用户").option("hours", "-h <hours:number> 指定时长", { fallback: 24 }).option("all", "-a 全局").action(async ({ session, options }) => {
896
+ if (!this.jieba) return "Jieba 分词服务未就绪";
907
897
  const scope = await parseQueryScope(this.ctx, session, options);
908
898
  if (scope.error) return scope.error;
899
+ scope.uids ??= (await this.ctx.database.get("analyse_user", {}, ["uid"])).map((u) => u.uid);
900
+ if (!scope.uids?.length) return "暂无用户数据";
909
901
  const since = new Date(Date.now() - options.hours * import_koishi6.Time.hour);
910
- const records = await this.ctx.database.select("analyse_cache").where({ uid: { $in: scope.uids }, timestamp: { $gte: since } }).project(["content"]).execute();
911
- if (records.length === 0) return "暂无统计数据";
902
+ const records = await this.ctx.database.get("analyse_cache", { uid: { $in: scope.uids }, timestamp: { $gte: since } }, ["content"]);
903
+ if (!records.length) return "暂无统计数据";
912
904
  const allText = records.map((r) => r.content).join(" ");
913
- const result = await this.nlp.process("zh", allText);
914
- const words = (result.stems || []).filter((stem) => stem.length > 1);
915
- const wordCounts = words.reduce((map, word) => {
916
- map.set(word, (map.get(word) || 0) + 1);
917
- return map;
918
- }, /* @__PURE__ */ new Map());
919
- if (wordCounts.size === 0) return "暂无有效词语";
905
+ const exclusionSet = /* @__PURE__ */ new Set([
906
+ "[face]",
907
+ "[file]",
908
+ "[forward]",
909
+ "[img]",
910
+ "[audio]",
911
+ "[video]",
912
+ "[json]",
913
+ "[rps]",
914
+ "[markdown]",
915
+ "[dice]"
916
+ ]);
917
+ const words = this.jieba.cut(allText).filter((w) => {
918
+ if (w.length <= 1) return false;
919
+ if (/^\d+$/.test(w)) return false;
920
+ if (exclusionSet.has(w)) return false;
921
+ if (/^\[at:.*?\]$/.test(w)) return false;
922
+ return true;
923
+ });
924
+ if (!words.length) return "暂无有效词语";
925
+ const wordCounts = words.reduce((map, word) => map.set(word, (map.get(word) || 0) + 1), /* @__PURE__ */ new Map());
920
926
  const wordList = Array.from(wordCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 150);
921
927
  const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "词云" });
922
- const renderResult = await this.renderer.renderWordCloud({ title, time: /* @__PURE__ */ new Date(), words: wordList });
923
- if (typeof renderResult === "string") return renderResult;
924
- if (Array.isArray(renderResult) && renderResult.length > 0) {
925
- for (const buffer of renderResult) await session.sendQueued(import_koishi6.h.image(buffer, "image/png"));
926
- }
927
- });
928
- }
929
- if (this.config.enableVocabulary) {
930
- cmd.subcommand(".vocabulary", "词汇排行").usage("根据不重复词汇量占总词汇量的比例进行排行。").option("guild", "-g <guildId:string> 指定群组").option("all", "-a 全局").option("hours", "-h <hours:number> 指定时长", { fallback: 24 }).action(async ({ session, options }) => {
931
- if (!this.isNlpReady) return "文本分析尚未就绪,请稍后再试";
932
- const scope = await parseQueryScope(this.ctx, session, options);
933
- if (scope.error) return scope.error;
934
- const users = await this.ctx.database.get("analyse_user", { uid: { $in: scope.uids } }, ["uid", "userName"]);
935
- const userNameMap = new Map(users.map((u) => [u.uid, u.userName]));
936
- const since = new Date(Date.now() - options.hours * import_koishi6.Time.hour);
937
- const allRecords = await this.ctx.database.get("analyse_cache", { uid: { $in: scope.uids }, timestamp: { $gte: since } }, ["uid", "content"]);
938
- if (allRecords.length === 0) return "暂无统计数据";
939
- const messagesByUid = /* @__PURE__ */ new Map();
940
- for (const record of allRecords) {
941
- if (!messagesByUid.has(record.uid)) messagesByUid.set(record.uid, []);
942
- messagesByUid.get(record.uid).push(record.content);
943
- }
944
- const richnessData = [];
945
- for (const [uid, messages] of messagesByUid.entries()) {
946
- const allText = messages.join(" ");
947
- const result = await this.nlp.process("zh", allText);
948
- const words = (result.stems || []).filter((stem) => stem.length > 1);
949
- if (words.length < 50) continue;
950
- const uniqueWords = new Set(words);
951
- const richness = uniqueWords.size / words.length;
952
- richnessData.push({
953
- name: userNameMap.get(uid) || `UID ${uid}`,
954
- total: words.length,
955
- unique: uniqueWords.size,
956
- richness
957
- });
958
- }
959
- if (richnessData.length === 0) return "暂无有效词语";
960
- richnessData.sort((a, b) => b.richness - a.richness);
961
- const list = richnessData.map((item) => [
962
- item.name,
963
- item.unique,
964
- item.total,
965
- `${(item.richness * 100).toFixed(2)}%`
966
- ]);
967
- const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "词汇排行" });
968
- const renderResult = await this.renderer.renderList(
969
- { title, time: /* @__PURE__ */ new Date(), total: richnessData.length, list },
970
- ["用户", "不重复词数", "总词数", "丰富度"]
971
- );
972
- if (typeof renderResult === "string") return renderResult;
973
- if (Array.isArray(renderResult) && renderResult.length > 0) {
974
- for (const buffer of renderResult) await session.sendQueued(import_koishi6.h.image(buffer, "image/png"));
928
+ const result = await this.renderer.renderWordCloud({ title, time: /* @__PURE__ */ new Date(), words: wordList });
929
+ if (typeof result === "string") return result;
930
+ if (Array.isArray(result) && result.length > 0) {
931
+ for (const buffer of result) await session.sendQueued(import_koishi6.h.image(buffer, "image/png"));
975
932
  }
976
933
  });
977
934
  }
@@ -1009,8 +966,7 @@ var Config3 = import_koishi7.Schema.intersect([
1009
966
  }).description("基础分析配置"),
1010
967
  import_koishi7.Schema.object({
1011
968
  enableOriRecord: import_koishi7.Schema.boolean().default(true).description("启用原始记录"),
1012
- enableWordCloud: import_koishi7.Schema.boolean().default(true).description("启用词云生成"),
1013
- enableVocabulary: import_koishi7.Schema.boolean().default(true).description("启用词汇排行")
969
+ enableWordCloud: import_koishi7.Schema.boolean().default(true).description("启用词云生成")
1014
970
  }).description("高级分析配置")
1015
971
  ]);
1016
972
  async function parseQueryScope(ctx, session, options) {
@@ -1057,7 +1013,7 @@ function apply(ctx, config) {
1057
1013
  new Stat(ctx, config).registerCommands(analyse);
1058
1014
  if (config.enableWhoAt) new WhoAt(ctx, config).registerCommand(analyse);
1059
1015
  if (config.enableDataIO) new Data(ctx).registerCommands(analyse);
1060
- if (config.enableOriRecord && (config.enableWordCloud || config.enableVocabulary)) new Analyse(ctx, config).registerCommands(analyse);
1016
+ if (config.enableOriRecord && config.enableWordCloud) new Analyse(ctx, config).registerCommands(analyse);
1061
1017
  }
1062
1018
  __name(apply, "apply");
1063
1019
  // Annotate the CommonJS export names for ESM import in node:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-chat-analyse",
3
3
  "description": "聊天记录分析",
4
- "version": "0.6.4",
4
+ "version": "0.7.0",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],
@@ -39,7 +39,6 @@
39
39
  "koishi": "4.18.8"
40
40
  },
41
41
  "dependencies": {
42
- "@nlpjs/basic": "^4.26.1",
43
- "@nlpjs/lang-zh": "^4.26.1"
42
+ "@node-rs/jieba": "^2.0.1"
44
43
  }
45
44
  }