koishi-plugin-chat-analyse 1.2.0 → 1.2.2

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,25 @@ 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
- if (!words?.length) return "暂无数据可供渲染";
1738
- const wordListJson = JSON.stringify(words);
1733
+ if (!words?.length) return;
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
- 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
- }
1736
+ const weights = words.map((w) => w[1]);
1737
+ const maxWeight = Math.max(...weights, 1);
1738
+ const minWeight = Math.min(...weights);
1739
+ const MAX_FONT_SIZE = 64;
1740
+ const MIN_FONT_SIZE = 4;
1753
1741
  const cardHtml = `
1754
1742
  <div class="container">
1755
1743
  <div class="header">
@@ -1762,16 +1750,20 @@ var Renderer = class {
1762
1750
  <script>
1763
1751
  const palette = ${JSON.stringify(selectedPalette)};
1764
1752
  WordCloud(document.getElementById('wordcloud-container'), {
1765
- list: ${wordListJson},
1753
+ list: ${wordsJson},
1766
1754
  fontFamily: '"Noto Sans CJK SC", "Arial", sans-serif',
1767
- weightFactor: (size) => (Math.log(size) + 1) * ${weightFactor},
1755
+ weightFactor: (size) => {
1756
+ if (${maxWeight} === ${minWeight}) return (${MIN_FONT_SIZE} + ${MAX_FONT_SIZE}) / 2;
1757
+ const normalizedWeight = (size - ${minWeight}) / (${maxWeight} - ${minWeight});
1758
+ return ${MIN_FONT_SIZE} + normalizedWeight * (${MAX_FONT_SIZE} - ${MIN_FONT_SIZE});
1759
+ },
1768
1760
  color: (word, weight, fontSize, distance, theta) => {
1769
1761
  return palette[Math.floor(Math.random() * palette.length)];
1770
1762
  },
1771
1763
  backgroundColor: 'transparent',
1772
1764
  shape: 'square',
1773
1765
  ellipticity: 0.6,
1774
- gridSize: 8,
1766
+ gridSize: 1,
1775
1767
  rotateRatio: 1,
1776
1768
  minRotation: -Math.PI / 4,
1777
1769
  maxRotation: Math.PI / 4,
@@ -1790,7 +1782,7 @@ var Renderer = class {
1790
1782
  </body>
1791
1783
  </html>`;
1792
1784
  const imageBuffer = await this.htmlToImage(fullHtml);
1793
- return imageBuffer ? [imageBuffer] : "图片渲染失败";
1785
+ if (imageBuffer) yield imageBuffer;
1794
1786
  }
1795
1787
  };
1796
1788
 
@@ -1828,12 +1820,9 @@ var Stat = class {
1828
1820
  try {
1829
1821
  const result = await handler(scope, options);
1830
1822
  if (typeof result === "string") return result;
1831
- if (Array.isArray(result) && result.length > 0) {
1832
- for (const buffer of result) await session.sendQueued(import_koishi3.h.image(buffer, "image/png"));
1833
- return;
1834
- }
1823
+ for await (const buffer of result) await session.send(import_koishi3.h.image(buffer, "image/png"));
1835
1824
  } catch (error) {
1836
- this.ctx.logger.error("渲染统计图片失败:", error);
1825
+ this.ctx.logger.error("图片渲染失败:", error);
1837
1826
  return "图片渲染失败";
1838
1827
  }
1839
1828
  };
@@ -1983,7 +1972,7 @@ var WhoAt = class {
1983
1972
  * @param cmd - 主命令实例。
1984
1973
  */
1985
1974
  registerCommand(cmd) {
1986
- cmd.subcommand("whoatme", "谁提及我").usage("查看最近提及我的消息,不分群组。").action(async ({ session }) => {
1975
+ cmd.subcommand("whoatme", "谁提及我").usage("查看最近提及我的消息,查看后自动删除。").action(async ({ session }) => {
1987
1976
  if (!session.userId) return "无法获取用户信息";
1988
1977
  try {
1989
1978
  const records = await this.ctx.database.get("analyse_at", { target: session.userId }, {
@@ -1999,6 +1988,8 @@ var WhoAt = class {
1999
1988
  const author = (0, import_koishi4.h)("author", { id: senderInfo.id, name: senderInfo.name });
2000
1989
  return (0, import_koishi4.h)("message", {}, [author, import_koishi4.h.text(record.content)]);
2001
1990
  });
1991
+ const recordIdsToDelete = records.map((r) => r.id);
1992
+ await this.ctx.database.remove("analyse_at", { id: { $in: recordIdsToDelete } });
2002
1993
  return (0, import_koishi4.h)("message", { forward: true }, messageElements);
2003
1994
  } catch (error) {
2004
1995
  this.ctx.logger.error("查询提及记录失败:", error);
@@ -2183,30 +2174,33 @@ var Analyse = class {
2183
2174
  registerCommands(cmd) {
2184
2175
  if (this.config.enableWordCloud) {
2185
2176
  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 }) => {
2186
- if (!this.jieba) return "Jieba 分词服务未就绪";
2187
- const scope = await parseQueryScope(this.ctx, session, options);
2188
- if (scope.error) return scope.error;
2189
- scope.uids ??= (await this.ctx.database.get("analyse_user", {}, ["uid"])).map((u) => u.uid);
2190
- if (!scope.uids?.length) return "暂无用户数据";
2191
- const since = new Date(Date.now() - options.hours * import_koishi6.Time.hour);
2192
- const records = await this.ctx.database.get("analyse_cache", { uid: { $in: scope.uids }, timestamp: { $gte: since } }, ["content"]);
2193
- if (!records.length) return "暂无统计数据";
2194
- const exclusionRegex = /\[(face|file|forward|img|gif|audio|video|json|rps|markdown|dice|at:.*?)\]/g;
2195
- const allText = records.map((r) => r.content.replace(exclusionRegex, "")).join(" ");
2196
- const words = this.jieba.cut(allText).filter((w) => {
2197
- if (w.trim().length <= 1) return false;
2198
- if (/^\d+$/.test(w)) return false;
2199
- return true;
2200
- });
2201
- if (!words.length) return "暂无有效词语";
2202
- const wordCounts = words.reduce((map, word) => map.set(word, (map.get(word) || 0) + 1), /* @__PURE__ */ new Map());
2203
- const wordList = Array.from(wordCounts.entries()).sort((a, b) => b[1] - a[1]);
2204
- session.send(`正在生成词云:${wordList.slice(0, 10)}`);
2205
- const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "词云" });
2206
- const result = await this.renderer.renderWordCloud({ title, time: /* @__PURE__ */ new Date(), words: wordList });
2207
- if (typeof result === "string") return result;
2208
- if (Array.isArray(result) && result.length > 0) {
2209
- for (const buffer of result) await session.sendQueued(import_koishi6.h.image(buffer, "image/png"));
2177
+ try {
2178
+ if (!this.jieba) return "Jieba 分词服务未就绪";
2179
+ const scope = await parseQueryScope(this.ctx, session, options);
2180
+ if (scope.error) return scope.error;
2181
+ scope.uids ??= (await this.ctx.database.get("analyse_user", {}, ["uid"])).map((u) => u.uid);
2182
+ if (!scope.uids?.length) return "暂无用户数据";
2183
+ const since = new Date(Date.now() - options.hours * import_koishi6.Time.hour);
2184
+ const records = await this.ctx.database.get("analyse_cache", { uid: { $in: scope.uids }, timestamp: { $gte: since } }, ["content"]);
2185
+ if (!records.length) return "暂无统计数据";
2186
+ const exclusionRegex = /\[(face|file|forward|img|gif|audio|video|json|rps|markdown|dice|at:.*?)\]/g;
2187
+ const allText = records.map((r) => r.content.replace(exclusionRegex, "")).join(" ");
2188
+ const words = this.jieba.cut(allText).filter((w) => {
2189
+ if (w.trim().length <= 1) return false;
2190
+ if (/^\d+$/.test(w)) return false;
2191
+ return true;
2192
+ });
2193
+ if (!words.length) return "暂无有效词语";
2194
+ const wordCounts = words.reduce((map, word) => map.set(word, (map.get(word) || 0) + 1), /* @__PURE__ */ new Map());
2195
+ const wordList = Array.from(wordCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 512);
2196
+ const topWordsPreview = wordList.slice(0, 10).map((item) => item[0]).join(", ");
2197
+ session.send(`正在生成词云,热门词汇:${topWordsPreview}...`);
2198
+ const title = await generateTitle(this.ctx, scope.scopeDesc, { main: "词云" });
2199
+ const imageGenerator = this.renderer.renderWordCloud({ title, time: /* @__PURE__ */ new Date(), words: wordList });
2200
+ for await (const buffer of imageGenerator) await session.send(import_koishi6.h.image(buffer, "image/png"));
2201
+ } catch (error) {
2202
+ this.ctx.logger.error("生成词云图片失败:", error);
2203
+ return "图片渲染失败";
2210
2204
  }
2211
2205
  });
2212
2206
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-chat-analyse",
3
3
  "description": "强大而全面的聊天数据分析,支持统计命令,发言,消息类型,活跃度,支持发言排行和生成词云",
4
- "version": "1.2.0",
4
+ "version": "1.2.2",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],