koishi-plugin-chat-analyse 1.1.4 → 1.2.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/Renderer.d.ts CHANGED
@@ -63,26 +63,26 @@ export declare class Renderer {
63
63
  /**
64
64
  * @public
65
65
  * @method renderList
66
- * @description 将表格型数据渲染成一个或多个列表形式的图片。如果数据过多,会自动进行分页渲染。
66
+ * @description 将表格型数据渲染成列表形式的图片。如果数据过多,会通过异步生成器逐个产出图片。
67
67
  * @param {ListRenderData} data - 包含标题、时间、总计和列表数据的对象。
68
68
  * @param {string[]} [headers] - (可选)列表的表头数组。
69
- * @returns {Promise<string | Buffer[]>} - 成功时返回包含图片 Buffer 的数组,失败或无数据时返回提示字符串。
69
+ * @returns {AsyncGenerator<Buffer>} - 一个异步生成器,每次迭代产出一张图片的 Buffer
70
70
  */
71
- renderList(data: ListRenderData, headers?: string[]): Promise<string | Buffer[]>;
71
+ renderList(data: ListRenderData, headers?: string[]): AsyncGenerator<Buffer>;
72
72
  /**
73
73
  * @public
74
74
  * @method renderCircadianChart
75
- * @description 将 24 小时制的活跃度数据渲染成一张柱状图图片。
75
+ * @description 将 24 小时制的活跃度数据渲染成一张柱状图图片。通过异步生成器产出图片。
76
76
  * @param {CircadianChartData} data - 包含标题、时间、总计和 24 小时数据数组的对象。
77
- * @returns {Promise<string | Buffer[]>} - 成功时返回包含图片 Buffer 的数组,失败或无数据时返回提示字符串。
77
+ * @returns {AsyncGenerator<Buffer>} - 一个异步生成器,产出渲染后的图片 Buffer
78
78
  */
79
- renderCircadianChart(data: CircadianChartData): Promise<string | Buffer[]>;
79
+ renderCircadianChart(data: CircadianChartData): AsyncGenerator<Buffer>;
80
80
  /**
81
81
  * @public
82
82
  * @method renderWordCloud
83
83
  * @description 将词频数据渲染成一张词云图片,使用 Puppeteer 和 wordcloud2.js。
84
84
  * @param {WordCloudData} data - 包含标题、时间和词汇列表的对象。
85
- * @returns {Promise<string | Buffer[]>} - 成功时返回图片 Buffer 数组,否则返回提示。
85
+ * @returns {AsyncGenerator<Buffer>} - 一个异步生成器,产出渲染后的图片 Buffer
86
86
  */
87
- renderWordCloud(data: WordCloudData): Promise<string | Buffer[]>;
87
+ renderWordCloud(data: WordCloudData): AsyncGenerator<Buffer>;
88
88
  }
package/lib/index.js CHANGED
@@ -1487,20 +1487,20 @@ var Renderer = class {
1487
1487
  __name(this, "Renderer");
1488
1488
  }
1489
1489
  COLOR_PALETTES = [
1490
- ["#2c5b7a", "#3a8fb7", "#64b9cc", "#97d8c4", "#ccece6", "#7f8c8d"],
1491
- // Blue/Teal
1492
- ["#d946ef", "#a21caf", "#86198f", "#fdf4ff", "#faf5ff", "#f5d0fe"],
1493
- // Fuchsia/Purple
1494
- ["#4f46e5", "#6366f1", "#a5b4fc", "#e0e7ff", "#eef2ff", "#3730a3"],
1495
- // Indigo
1496
- ["#d97706", "#f59e0b", "#fcd34d", "#fefce8", "#fffbeb", "#b45309"],
1497
- // Amber/Yellow
1498
- ["#059669", "#10b981", "#6ee7b7", "#ecfdf5", "#d1fae5", "#047857"],
1499
- // Emerald/Green
1500
- ["#db2777", "#ec4899", "#f9a8d4", "#fdf2f8", "#fce7f3", "#be185d"],
1501
- // Pink
1502
- ["#e11d48", "#f43f5e", "#fb7185", "#fff1f2", "#ffe4e6", "#be123c"]
1503
- // Rose/Red
1490
+ // 1. Oceanic: 深邃的海洋蓝与青色系,沉稳专业
1491
+ ["#003f5c", "#2f4b7c", "#0077b6", "#023e8a", "#2a6f97", "#0096c7"],
1492
+ // 2. Sunset: 充满活力的日落色系,温暖而醒目
1493
+ ["#c1121f", "#d9501e", "#e36414", "#9a031e", "#5f0f40", "#fb8500"],
1494
+ // 3. Forest: 茂密森林的绿色系,自然且舒适
1495
+ ["#1b4332", "#2d6a4f", "#40916c", "#52b788", "#283618", "#081c15"],
1496
+ // 4. Grape: 浓郁的葡萄与浆果色系,高贵而神秘
1497
+ ["#4a0072", "#6a00a8", "#810099", "#c71585", "#58004f", "#3d0c4c"],
1498
+ // 5. Candy: 甜美的糖果色系,活泼有趣
1499
+ ["#e63946", "#f77f00", "#2a9d8f", "#457b9d", "#8d99ae", "#d62828"],
1500
+ // 6. Retro: 复古风格色盘,兼具沉稳与活力
1501
+ ["#264653", "#2a9d8f", "#e76f51", "#f4a261", "#bc6c25", "#a56c03"],
1502
+ // 7. Midnight: 深邃的午夜色系,搭配亮色点缀,对比强烈
1503
+ ["#03045e", "#0077b6", "#00b4d8", "#d00000", "#e85d04", "#212529"]
1504
1504
  ];
1505
1505
  COMMON_STYLE = `
1506
1506
  :root {
@@ -1606,16 +1606,14 @@ var Renderer = class {
1606
1606
  /**
1607
1607
  * @public
1608
1608
  * @method renderList
1609
- * @description 将表格型数据渲染成一个或多个列表形式的图片。如果数据过多,会自动进行分页渲染。
1609
+ * @description 将表格型数据渲染成列表形式的图片。如果数据过多,会通过异步生成器逐个产出图片。
1610
1610
  * @param {ListRenderData} data - 包含标题、时间、总计和列表数据的对象。
1611
1611
  * @param {string[]} [headers] - (可选)列表的表头数组。
1612
- * @returns {Promise<string | Buffer[]>} - 成功时返回包含图片 Buffer 的数组,失败或无数据时返回提示字符串。
1612
+ * @returns {AsyncGenerator<Buffer>} - 一个异步生成器,每次迭代产出一张图片的 Buffer
1613
1613
  */
1614
- async renderList(data, headers) {
1614
+ async *renderList(data, headers) {
1615
1615
  const { title, time, list } = data;
1616
- if (!list?.length) return "暂无数据可供渲染";
1617
1616
  const CHUNK_SIZE = 100;
1618
- const imageBuffers = [];
1619
1617
  const totalItems = list.length;
1620
1618
  const countHeaderIndex = headers?.findIndex((h6) => ["总计发言", "条数", "次数", "数量"].includes(h6)) ?? -1;
1621
1619
  const totalCount = data.total || (countHeaderIndex > -1 ? list.reduce((sum, row) => sum + (Number(row[countHeaderIndex]) || 0), 0) : totalItems);
@@ -1680,20 +1678,18 @@ var Renderer = class {
1680
1678
  </div>`;
1681
1679
  const fullHtml = this.generateFullHtml(cardHtml, listStyles);
1682
1680
  const imageBuffer = await this.htmlToImage(fullHtml);
1683
- if (imageBuffer) imageBuffers.push(imageBuffer);
1681
+ if (imageBuffer) yield imageBuffer;
1684
1682
  }
1685
- return imageBuffers.length > 0 ? imageBuffers : "图片渲染失败";
1686
1683
  }
1687
1684
  /**
1688
1685
  * @public
1689
1686
  * @method renderCircadianChart
1690
- * @description 将 24 小时制的活跃度数据渲染成一张柱状图图片。
1687
+ * @description 将 24 小时制的活跃度数据渲染成一张柱状图图片。通过异步生成器产出图片。
1691
1688
  * @param {CircadianChartData} data - 包含标题、时间、总计和 24 小时数据数组的对象。
1692
- * @returns {Promise<string | Buffer[]>} - 成功时返回包含图片 Buffer 的数组,失败或无数据时返回提示字符串。
1689
+ * @returns {AsyncGenerator<Buffer>} - 一个异步生成器,产出渲染后的图片 Buffer
1693
1690
  */
1694
- async renderCircadianChart(data) {
1691
+ async *renderCircadianChart(data) {
1695
1692
  const { title, time, total, data: hourlyCounts, labels } = data;
1696
- if (!hourlyCounts || hourlyCounts.every((c) => c === 0)) return "暂无数据可供渲染";
1697
1693
  const maxCount = Math.max(...hourlyCounts, 1);
1698
1694
  const chartStyles = `
1699
1695
  .chart-container { display: flex; align-items: flex-end; gap: 4px; height: 180px; padding: 30px 15px 10px; }
@@ -1723,33 +1719,28 @@ var Renderer = class {
1723
1719
  </div>`;
1724
1720
  const fullHtml = this.generateFullHtml(cardHtml, chartStyles);
1725
1721
  const imageBuffer = await this.htmlToImage(fullHtml);
1726
- return imageBuffer ? [imageBuffer] : "图片渲染失败";
1722
+ if (imageBuffer) yield imageBuffer;
1727
1723
  }
1728
1724
  /**
1729
1725
  * @public
1730
1726
  * @method renderWordCloud
1731
1727
  * @description 将词频数据渲染成一张词云图片,使用 Puppeteer 和 wordcloud2.js。
1732
1728
  * @param {WordCloudData} data - 包含标题、时间和词汇列表的对象。
1733
- * @returns {Promise<string | Buffer[]>} - 成功时返回图片 Buffer 数组,否则返回提示。
1729
+ * @returns {AsyncGenerator<Buffer>} - 一个异步生成器,产出渲染后的图片 Buffer
1734
1730
  */
1735
- async renderWordCloud(data) {
1731
+ async *renderWordCloud(data) {
1736
1732
  const { title, time, words } = data;
1737
1733
  if (!words?.length) return "暂无数据可供渲染";
1738
- const wordListJson = JSON.stringify(words);
1734
+ const wordsJson = JSON.stringify(words);
1739
1735
  const selectedPalette = this.COLOR_PALETTES[Math.floor(Math.random() * this.COLOR_PALETTES.length)];
1740
- let weightFactor = 64;
1741
1736
  const count = words.length;
1742
- if (count <= 32) {
1743
- weightFactor = 64;
1744
- } else if (count <= 64) {
1745
- weightFactor = 48;
1746
- } else if (count <= 128) {
1747
- weightFactor = 32;
1748
- } else if (count <= 256) {
1749
- weightFactor = 24;
1750
- } else {
1751
- weightFactor = 12;
1752
- }
1737
+ const minWords = 64;
1738
+ const maxWords = 512;
1739
+ const maxWeight = 32;
1740
+ const minWeight = 4;
1741
+ const progress = (count - minWords) / (maxWords - minWords);
1742
+ const clampedProgress = Math.max(0, Math.min(1, progress));
1743
+ const dynamicWeightFactor = maxWeight - (maxWeight - minWeight) * clampedProgress;
1753
1744
  const cardHtml = `
1754
1745
  <div class="container">
1755
1746
  <div class="header">
@@ -1762,14 +1753,16 @@ var Renderer = class {
1762
1753
  <script>
1763
1754
  const palette = ${JSON.stringify(selectedPalette)};
1764
1755
  WordCloud(document.getElementById('wordcloud-container'), {
1765
- list: ${wordListJson},
1766
- fontFamily: '"Noto Sans CJK SC", "Helvetica Neue", "Arial", sans-serif',
1767
- weightFactor: (size) => (Math.log(size) + 1) * ${weightFactor},
1756
+ list: ${wordsJson},
1757
+ fontFamily: '"Noto Sans CJK SC", "Arial", sans-serif',
1758
+ weightFactor: (size) => Math.max((Math.log(size) + 1) * ${dynamicWeightFactor}, 8),
1768
1759
  color: (word, weight, fontSize, distance, theta) => {
1769
1760
  return palette[Math.floor(Math.random() * palette.length)];
1770
1761
  },
1771
1762
  backgroundColor: 'transparent',
1772
- gridSize: 8,
1763
+ shape: 'square',
1764
+ ellipticity: 0.6,
1765
+ gridSize: 2,
1773
1766
  rotateRatio: 1,
1774
1767
  minRotation: -Math.PI / 4,
1775
1768
  maxRotation: Math.PI / 4,
@@ -1788,7 +1781,7 @@ var Renderer = class {
1788
1781
  </body>
1789
1782
  </html>`;
1790
1783
  const imageBuffer = await this.htmlToImage(fullHtml);
1791
- return imageBuffer ? [imageBuffer] : "图片渲染失败";
1784
+ if (imageBuffer) yield imageBuffer;
1792
1785
  }
1793
1786
  };
1794
1787
 
@@ -1826,12 +1819,9 @@ var Stat = class {
1826
1819
  try {
1827
1820
  const result = await handler(scope, options);
1828
1821
  if (typeof result === "string") return result;
1829
- if (Array.isArray(result) && result.length > 0) {
1830
- for (const buffer of result) await session.sendQueued(import_koishi3.h.image(buffer, "image/png"));
1831
- return;
1832
- }
1822
+ for await (const buffer of result) await session.send(import_koishi3.h.image(buffer, "image/png"));
1833
1823
  } catch (error) {
1834
- this.ctx.logger.error("渲染统计图片失败:", error);
1824
+ this.ctx.logger.error("图片渲染失败:", error);
1835
1825
  return "图片渲染失败";
1836
1826
  }
1837
1827
  };
@@ -1981,7 +1971,7 @@ var WhoAt = class {
1981
1971
  * @param cmd - 主命令实例。
1982
1972
  */
1983
1973
  registerCommand(cmd) {
1984
- cmd.subcommand("whoatme", "谁提及我").usage("查看最近提及我的消息,不分群组。").action(async ({ session }) => {
1974
+ cmd.subcommand("whoatme", "谁提及我").usage("查看最近提及我的消息,查看后自动删除。").action(async ({ session }) => {
1985
1975
  if (!session.userId) return "无法获取用户信息";
1986
1976
  try {
1987
1977
  const records = await this.ctx.database.get("analyse_at", { target: session.userId }, {
@@ -1997,6 +1987,8 @@ var WhoAt = class {
1997
1987
  const author = (0, import_koishi4.h)("author", { id: senderInfo.id, name: senderInfo.name });
1998
1988
  return (0, import_koishi4.h)("message", {}, [author, import_koishi4.h.text(record.content)]);
1999
1989
  });
1990
+ const recordIdsToDelete = records.map((r) => r.id);
1991
+ await this.ctx.database.remove("analyse_at", { id: { $in: recordIdsToDelete } });
2000
1992
  return (0, import_koishi4.h)("message", { forward: true }, messageElements);
2001
1993
  } catch (error) {
2002
1994
  this.ctx.logger.error("查询提及记录失败:", error);
@@ -2181,30 +2173,33 @@ var Analyse = class {
2181
2173
  registerCommands(cmd) {
2182
2174
  if (this.config.enableWordCloud) {
2183
2175
  cmd.subcommand("wordcloud", "生成词云").usage("基于聊天记录生成词云图,可指定范围,默认当前群组。").option("guild", "-g <guildId:string> 指定群组").option("user", "-u <user:string> 指定用户").option("hours", "-t <hours:number> 指定时长", { fallback: 24 }).option("all", "-a 全局").action(async ({ session, options }) => {
2184
- if (!this.jieba) return "Jieba 分词服务未就绪";
2185
- const scope = await parseQueryScope(this.ctx, session, options);
2186
- if (scope.error) return scope.error;
2187
- scope.uids ??= (await this.ctx.database.get("analyse_user", {}, ["uid"])).map((u) => u.uid);
2188
- if (!scope.uids?.length) return "暂无用户数据";
2189
- const since = new Date(Date.now() - options.hours * import_koishi6.Time.hour);
2190
- const records = await this.ctx.database.get("analyse_cache", { uid: { $in: scope.uids }, timestamp: { $gte: since } }, ["content"]);
2191
- if (!records.length) return "暂无统计数据";
2192
- const exclusionRegex = /\[(face|file|forward|img|gif|audio|video|json|rps|markdown|dice|at:.*?)\]/g;
2193
- const allText = records.map((r) => r.content.replace(exclusionRegex, "")).join(" ");
2194
- const words = this.jieba.cut(allText).filter((w) => {
2195
- if (w.trim().length <= 1) return false;
2196
- if (/^\d+$/.test(w)) return false;
2197
- return true;
2198
- });
2199
- if (!words.length) return "暂无有效词语";
2200
- const wordCounts = words.reduce((map, word) => map.set(word, (map.get(word) || 0) + 1), /* @__PURE__ */ new Map());
2201
- const wordList = Array.from(wordCounts.entries()).sort((a, b) => b[1] - a[1]);
2202
- session.send(`正在生成词云:${wordList.slice(0, 10)}`);
2203
- const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "词云" });
2204
- const result = await this.renderer.renderWordCloud({ title, time: /* @__PURE__ */ new Date(), words: wordList });
2205
- if (typeof result === "string") return result;
2206
- if (Array.isArray(result) && result.length > 0) {
2207
- for (const buffer of result) await session.sendQueued(import_koishi6.h.image(buffer, "image/png"));
2176
+ try {
2177
+ if (!this.jieba) return "Jieba 分词服务未就绪";
2178
+ const scope = await parseQueryScope(this.ctx, session, options);
2179
+ if (scope.error) return scope.error;
2180
+ scope.uids ??= (await this.ctx.database.get("analyse_user", {}, ["uid"])).map((u) => u.uid);
2181
+ if (!scope.uids?.length) return "暂无用户数据";
2182
+ const since = new Date(Date.now() - options.hours * import_koishi6.Time.hour);
2183
+ const records = await this.ctx.database.get("analyse_cache", { uid: { $in: scope.uids }, timestamp: { $gte: since } }, ["content"]);
2184
+ if (!records.length) return "暂无统计数据";
2185
+ const exclusionRegex = /\[(face|file|forward|img|gif|audio|video|json|rps|markdown|dice|at:.*?)\]/g;
2186
+ const allText = records.map((r) => r.content.replace(exclusionRegex, "")).join(" ");
2187
+ const words = this.jieba.cut(allText).filter((w) => {
2188
+ if (w.trim().length <= 1) return false;
2189
+ if (/^\d+$/.test(w)) return false;
2190
+ return true;
2191
+ });
2192
+ if (!words.length) return "暂无有效词语";
2193
+ const wordCounts = words.reduce((map, word) => map.set(word, (map.get(word) || 0) + 1), /* @__PURE__ */ new Map());
2194
+ const wordList = Array.from(wordCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 512);
2195
+ const topWordsPreview = wordList.slice(0, 10).map((item) => item[0]).join(", ");
2196
+ session.send(`正在生成词云,热门词汇:${topWordsPreview}...`);
2197
+ const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "词云" });
2198
+ const imageGenerator = this.renderer.renderWordCloud({ title, time: /* @__PURE__ */ new Date(), words: wordList });
2199
+ for await (const buffer of imageGenerator) await session.send(import_koishi6.h.image(buffer, "image/png"));
2200
+ } catch (error) {
2201
+ this.ctx.logger.error("生成词云图片失败:", error);
2202
+ return "图片渲染失败";
2208
2203
  }
2209
2204
  });
2210
2205
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-chat-analyse",
3
3
  "description": "强大而全面的聊天数据分析,支持统计命令,发言,消息类型,活跃度,支持发言排行和生成词云",
4
- "version": "1.1.4",
4
+ "version": "1.2.1",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],