koishi-plugin-chat-analyse 1.4.8 → 1.4.10

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
@@ -30,7 +30,6 @@ export interface LineChartData {
30
30
  */
31
31
  export declare class Renderer {
32
32
  private ctx;
33
- private readonly COLOR_PALETTES;
34
33
  private readonly COMMON_STYLE;
35
34
  /**
36
35
  * @constructor
package/lib/index.d.ts CHANGED
@@ -22,15 +22,13 @@ export interface Config {
22
22
  cacheRetentionDays: number;
23
23
  enableSimilarActivity: boolean;
24
24
  enableAutoBackup: boolean;
25
+ color: string;
26
+ shape: string;
25
27
  ellipticity: number;
26
- rotateRatio: number;
27
28
  minRotation: number;
28
29
  maxRotation: number;
29
- minFontSize: number;
30
- maxFontSize: number;
31
- gridSize: number;
30
+ rotationSteps: number;
32
31
  fontFamily: string;
33
- shape: 'square' | 'circle' | 'cardioid' | 'diamond' | 'triangle-forward' | 'triangle' | 'pentagon' | 'star';
34
32
  maskImage: string;
35
33
  }
36
34
  /** @description 插件的配置项定义 */
package/lib/index.js CHANGED
@@ -1493,26 +1493,6 @@ var Renderer = class {
1493
1493
  static {
1494
1494
  __name(this, "Renderer");
1495
1495
  }
1496
- COLOR_PALETTES = [
1497
- // --- 4组近似色 ---
1498
- // 1. Oceanic Blues: 更深邃、专业的蓝色系
1499
- ["#A9D6E5", "#89C2D9", "#61A5C2", "#2A6F97", "#012A4A"],
1500
- // 2. Forest Greens: 丰富、饱和的绿色系
1501
- ["#ADDDBC", "#80C9A7", "#52B69A", "#34A0A4", "#168AAD"],
1502
- // 3. Royal Purples: 优雅、浓郁的紫色系
1503
- ["#C792DF", "#AB69C6", "#9040AD", "#7B2CBF", "#5A189A"],
1504
- // 4. Sunset Glow: 温暖、明亮的日落色系
1505
- ["#FFDD77", "#FFC94A", "#FFB703", "#F8961E", "#E85D04"],
1506
- // --- 4组缤纷色 ---
1507
- // 5. Vivid Candy: 鲜艳的糖果色
1508
- ["#E63946", "#588157", "#A8DADC", "#457B9D", "#1D3557"],
1509
- // 6. Retro Groove: 复古风格
1510
- ["#264653", "#2A9D8F", "#F0C151", "#F4A261", "#E76F51"],
1511
- // 7. Neon Pop: 高对比度的现代色彩组合
1512
- ["#EF476F", "#FFD166", "#06D6A0", "#118AB2", "#073B4C"],
1513
- // 8. Bold Impact: 大胆且冲击力强的撞色
1514
- ["#D90429", "#F95738", "#F2C57C", "#0C7C59", "#003E1F"]
1515
- ];
1516
1496
  COMMON_STYLE = `
1517
1497
  :root {
1518
1498
  --card-bg: #fff; --text-color: #111827; --header-color: #111827;
@@ -1529,7 +1509,7 @@ var Renderer = class {
1529
1509
  .container {
1530
1510
  display: inline-block; background: var(--card-bg); border-radius: 12px;
1531
1511
  padding: 0; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,.05);
1532
- width: 600px;
1512
+ min-width: 480px; max-width: 640px;
1533
1513
  }
1534
1514
  .header {
1535
1515
  padding: 12px 16px;
@@ -1540,7 +1520,7 @@ var Renderer = class {
1540
1520
  }
1541
1521
  .title-text {
1542
1522
  font-size: 16px; font-weight: 600; color: var(--header-color);
1543
- margin: 0; text-align: center;
1523
+ margin: 0 8px; text-align: center;
1544
1524
  }
1545
1525
  .stat-chip, .time-label {
1546
1526
  display: inline-flex; align-items: baseline; padding: 4px 8px;
@@ -1582,7 +1562,7 @@ var Renderer = class {
1582
1562
  async htmlToImage(fullHtmlContent) {
1583
1563
  const page = await this.ctx.puppeteer.page();
1584
1564
  try {
1585
- await page.setViewport({ width: 1080, height: 720, deviceScaleFactor: 1 });
1565
+ await page.setViewport({ width: 800, height: 600, deviceScaleFactor: 1 });
1586
1566
  await page.setContent(fullHtmlContent, { waitUntil: "networkidle0" });
1587
1567
  const { width, height } = await page.evaluate(() => ({
1588
1568
  width: document.body.scrollWidth,
@@ -1702,71 +1682,71 @@ var Renderer = class {
1702
1682
  */
1703
1683
  async *renderLineChart(data) {
1704
1684
  const { title, time, series, labels } = data;
1705
- const colorfulPalettes = this.COLOR_PALETTES.slice(4);
1706
- const selectedPalette = colorfulPalettes[Math.floor(Math.random() * colorfulPalettes.length)];
1707
- const shuffledColors = [...selectedPalette].sort(() => 0.5 - Math.random());
1708
- const seriesColors = series.map((_, index) => shuffledColors[index % shuffledColors.length]);
1709
- const width = 600, height = 320;
1710
- const padding = { top: 10, right: 20, bottom: 70, left: 20 };
1711
- const chartWidth = width - padding.left - padding.right;
1712
- const chartHeight = height - padding.top - padding.bottom;
1685
+ const seriesColors = series.map(() => {
1686
+ const hue = Math.floor(Math.random() * 360);
1687
+ const saturation = Math.floor(Math.random() * 30 + 70);
1688
+ const lightness = Math.floor(Math.random() * 20 + 50);
1689
+ return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
1690
+ });
1713
1691
  const maxVal = Math.max(1, ...series.flatMap((s) => s.data));
1714
1692
  const yTickCount = 5;
1715
1693
  const yTickValue = Math.ceil(maxVal / yTickCount);
1716
1694
  const yAxisMax = yTickValue * yTickCount;
1717
1695
  const getX = /* @__PURE__ */ __name((index) => {
1718
- if (labels.length <= 1) return padding.left + chartWidth / 2;
1719
- return padding.left + index / (labels.length - 1) * chartWidth;
1696
+ if (labels.length <= 1) return 320;
1697
+ return 40 + index / (labels.length - 1) * 540;
1720
1698
  }, "getX");
1721
- const getY = /* @__PURE__ */ __name((value) => padding.top + chartHeight - value / yAxisMax * chartHeight, "getY");
1699
+ const getY = /* @__PURE__ */ __name((value) => 250 - value / yAxisMax * 240, "getY");
1722
1700
  let svgElements = "";
1723
1701
  for (let i = 0; i <= yTickCount; i++) {
1724
1702
  const y = getY(i * yTickValue);
1725
1703
  const value = i * yTickValue;
1726
- svgElements += `<line x1="${padding.left}" y1="${y}" x2="${width - padding.right}" y2="${y}" stroke="var(--border-color)" stroke-width="1"/>`;
1727
- svgElements += `<text x="${padding.left - 8}" y="${y + 4}" font-size="10" fill="var(--sub-text-color)" text-anchor="end">${value}</text>`;
1704
+ svgElements += `<line x1="40" y1="${y}" x2="580" y2="${y}" stroke="var(--border-color)" stroke-width="1"/>`;
1705
+ svgElements += `<text x="32" y="${y + 4}" font-size="10" fill="var(--sub-text-color)" text-anchor="end">${value}</text>`;
1728
1706
  }
1729
1707
  labels.forEach((label, index) => {
1730
- if (index % Math.ceil(labels.length / 12) === 0) {
1708
+ if (labels.length > 1 && index % Math.ceil(labels.length / 12) === 0) {
1731
1709
  const x = getX(index);
1732
- svgElements += `<text x="${x}" y="${height - padding.bottom + 20}" font-size="10" fill="var(--sub-text-color)" text-anchor="middle">${label}</text>`;
1710
+ svgElements += `<text x="${x}" y="270" font-size="10" fill="var(--sub-text-color)" text-anchor="middle">${label}</text>`;
1733
1711
  }
1734
1712
  });
1735
1713
  series.forEach((s, seriesIndex) => {
1736
- const color = seriesColors[seriesIndex];
1714
+ const color = seriesColors[seriesIndex % seriesColors.length];
1737
1715
  const points = s.data.map((value, index) => `${getX(index)},${getY(value)}`).join(" ");
1738
1716
  svgElements += `<polyline points="${points}" fill="none" stroke="${color}" stroke-width="2"/>`;
1739
1717
  });
1718
+ let legendHeight = 0;
1740
1719
  if (series.length > 1) {
1741
- const ITEMS_PER_ROW = 3;
1742
- const ROW_HEIGHT = 20;
1743
- const LEGEND_START_Y = height - padding.bottom + 45;
1744
- const columnWidth = chartWidth / ITEMS_PER_ROW;
1720
+ const legendRows = Math.ceil(series.length / 3);
1721
+ legendHeight = 15 + legendRows * 20;
1722
+ const LEGEND_START_Y = 285;
1723
+ const columnWidth = 560 / 3;
1745
1724
  series.forEach((s, seriesIndex) => {
1746
- const rowIndex = Math.floor(seriesIndex / ITEMS_PER_ROW);
1747
- const colIndex = seriesIndex % ITEMS_PER_ROW;
1748
- const legendX = padding.left + colIndex * columnWidth;
1749
- const legendY = LEGEND_START_Y + rowIndex * ROW_HEIGHT;
1750
- const color = seriesColors[seriesIndex];
1725
+ const rowIndex = Math.floor(seriesIndex / 3);
1726
+ const colIndex = seriesIndex % 3;
1727
+ const legendX = 40 + colIndex * columnWidth;
1728
+ const legendY = LEGEND_START_Y + rowIndex * 20;
1729
+ const color = seriesColors[seriesIndex % seriesColors.length];
1751
1730
  svgElements += `<rect x="${legendX}" y="${legendY - 8}" width="12" height="8" fill="${color}" rx="2"/>`;
1752
1731
  svgElements += `<text x="${legendX + 18}" y="${legendY}" font-size="12" fill="var(--text-color)">${s.name}</text>`;
1753
1732
  });
1754
1733
  }
1734
+ const svgHeight = 280 + legendHeight;
1755
1735
  const totalMessages = series.reduce((sum, s) => sum + s.data.reduce((a, b) => a + b, 0), 0);
1756
1736
  const cardHtml = `
1757
- <div class="container">
1737
+ <div class="container" style="width: 600px;">
1758
1738
  <div class="header">
1759
1739
  <div class="stat-chip">总计: <span>${totalMessages.toLocaleString()}</span></div>
1760
1740
  <h1 class="title-text">${title}</h1>
1761
1741
  <div class="time-label">${time.toLocaleString("zh-CN", { hour12: false })}</div>
1762
1742
  </div>
1763
1743
  <div class="chart-wrapper">
1764
- <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
1744
+ <svg width="600" height="${svgHeight}" xmlns="http://www.w3.org/2000/svg">
1765
1745
  ${svgElements}
1766
1746
  </svg>
1767
1747
  </div>
1768
1748
  </div>`;
1769
- const chartStyles = ` .chart-wrapper { padding: 10px; } `;
1749
+ const chartStyles = ` .chart-wrapper { padding: 10px; box-sizing: border-box; } `;
1770
1750
  const fullHtml = this.generateFullHtml(cardHtml, chartStyles);
1771
1751
  const imageBuffer = await this.htmlToImage(fullHtml);
1772
1752
  if (imageBuffer) yield imageBuffer;
@@ -1783,44 +1763,45 @@ var Renderer = class {
1783
1763
  const { title, time, words } = data;
1784
1764
  if (!words?.length) return;
1785
1765
  const wordsJson = JSON.stringify(words);
1786
- const selectedPalette = this.COLOR_PALETTES[Math.floor(Math.random() * this.COLOR_PALETTES.length)];
1787
1766
  const weights = words.map((w) => w[1]);
1788
1767
  const maxWeight = Math.max(...weights, 1);
1789
1768
  const minWeight = Math.min(...weights);
1769
+ const wordCount = words.length;
1770
+ const maxFontSize = Math.max(20, Math.round(400 / Math.log1p(wordCount)));
1771
+ const minFontSize = Math.max(4, Math.round(maxFontSize / 12));
1790
1772
  const cardHtml = `
1791
- <div class="container">
1773
+ <div class="container" style="width: 600px;">
1792
1774
  <div class="header">
1793
1775
  <div class="stat-chip">词数: <span>${words.length}</span></div>
1794
1776
  <h1 class="title-text">${title}</h1>
1795
1777
  <div class="time-label">${time.toLocaleString("zh-CN", { hour12: false })}</div>
1796
1778
  </div>
1797
- <div style="width: 600px; height: 600px; margin: auto;">
1779
+ <div style="width: 600px; height: 600px; margin: auto; padding: 10px 0;">
1798
1780
  <canvas id="wordcloud-container" width="600" height="600"></canvas>
1799
1781
  </div>
1800
1782
  <script>${wordCloudScript}</script>
1801
1783
  <script>
1802
1784
  const canvas = document.getElementById('wordcloud-container');
1803
- const palette = ${JSON.stringify(selectedPalette)};
1804
1785
  const options = {
1805
1786
  list: ${wordsJson},
1806
1787
  fontFamily: ${JSON.stringify(config.fontFamily)},
1807
1788
  weightFactor: (size) => {
1808
- if (${maxWeight} === ${minWeight}) return (${config.minFontSize} + ${config.maxFontSize}) / 2;
1789
+ if (${maxWeight} === ${minWeight}) return (${minFontSize} + ${maxFontSize}) / 2;
1809
1790
  const normalizedWeight = (size - ${minWeight}) / (${maxWeight} - ${minWeight});
1810
- return ${config.minFontSize} + normalizedWeight * (${config.maxFontSize} - ${config.minFontSize});
1811
- },
1812
- color: (word, weight, fontSize, distance, theta) => {
1813
- return palette[Math.floor(Math.random() * palette.length)];
1791
+ return ${minFontSize} + normalizedWeight * (${maxFontSize} - ${minFontSize});
1814
1792
  },
1793
+ color: ${JSON.stringify(config.color)},
1815
1794
  shape: ${JSON.stringify(config.shape)},
1816
- gridSize: ${config.gridSize},
1817
1795
  ellipticity: ${config.ellipticity},
1818
- rotateRatio: ${config.rotateRatio},
1819
1796
  minRotation: ${config.minRotation},
1820
1797
  maxRotation: ${config.maxRotation},
1798
+ rotationSteps: ${config.rotationSteps},
1821
1799
  backgroundColor: 'transparent',
1822
1800
  clearCanvas: true,
1801
+ shrinkToFit: true,
1802
+ rotateRatio: 1,
1823
1803
  shuffle: true,
1804
+ gridSize: 1,
1824
1805
  };
1825
1806
 
1826
1807
  const maskImageUrl = ${JSON.stringify(config.maskImage)};
@@ -2547,15 +2528,13 @@ var Config3 = import_koishi7.Schema.intersect([
2547
2528
  }).description("高级分析配置"),
2548
2529
  import_koishi7.Schema.object({
2549
2530
  ellipticity: import_koishi7.Schema.number().min(0).max(1).default(1).description("长宽比"),
2550
- rotateRatio: import_koishi7.Schema.number().min(0).max(1).default(0.5).description("旋转比"),
2551
- minRotation: import_koishi7.Schema.number().default(Math.PI / 2).description("最小旋转角"),
2531
+ rotationSteps: import_koishi7.Schema.number().min(0).default(3).description("旋转步数"),
2532
+ minRotation: import_koishi7.Schema.number().default(-Math.PI / 2).description("最小旋转角"),
2552
2533
  maxRotation: import_koishi7.Schema.number().default(Math.PI / 2).description("最大旋转角"),
2553
- minFontSize: import_koishi7.Schema.number().min(1).default(4).description("最小字号"),
2554
- maxFontSize: import_koishi7.Schema.number().min(1).default(64).description("最大字号"),
2555
- gridSize: import_koishi7.Schema.number().min(0).default(1).description("词云间距"),
2556
- fontFamily: import_koishi7.Schema.string().default('"Noto Sans CJK SC", "Arial", sans-serif').description("词云字体"),
2557
- shape: import_koishi7.Schema.union(["square", "circle", "cardioid", "diamond", "triangle-forward", "triangle", "pentagon", "star"]).default("square").description("词云形状"),
2558
- maskImage: import_koishi7.Schema.string().role("link").description("词云蒙版 (一个图片的URL,会覆盖形状设置)")
2534
+ color: import_koishi7.Schema.string().default("random-light").description("词云颜色"),
2535
+ shape: import_koishi7.Schema.string().default("square").description("词云形状"),
2536
+ fontFamily: import_koishi7.Schema.string().default('"Noto Sans CJK SC", Arial, sans-serif').description("词云字体"),
2537
+ maskImage: import_koishi7.Schema.string().role("link").description("蒙版图片")
2559
2538
  }).description("词云生成配置")
2560
2539
  ]);
2561
2540
  async function parseQueryScope(ctx, session, options) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-chat-analyse",
3
3
  "description": "强大而全面的聊天数据分析插件。支持多维度统计(命令、发言、消息类型、活跃度),可生成发言排行、词云图,并提供完善的数据管理。",
4
- "version": "1.4.8",
4
+ "version": "1.4.10",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],
package/readme.md CHANGED
@@ -142,14 +142,14 @@
142
142
  `enableMsgStat`: **启用消息统计**。 (默认: `true`)
143
143
  `enableActivity`: **启用活跃统计**。 (默认: `true`)
144
144
  `enableRankStat`: **启用发言排行**。 (默认: `true`)
145
- `rankRetentionDays`: **排行保留天数**。发言排行数据的保留时长(天),`0` 为永久保留。 (默认: `180`)
145
+ `rankRetentionDays`: **排行保留天数**。发言排行数据的保留时长(天),`0` 为永久保留。 (默认: `365`)
146
146
  `enableWhoAt`: **启用提及记录**。 (默认: `true`)
147
147
  `atRetentionDays`: **提及保留天数**。`whoatme` 数据的保留时长(天),`0` 为永久保留。 (默认: `3`)
148
148
 
149
149
  ### 高级分析配置
150
150
 
151
151
  `enableOriRecord`: **启用原始记录**。是否记录原始消息内容。这是 `.view` 和 `wordcloud` 功能的基础。 (默认: `true`)
152
- `cacheRetentionDays`: **原始记录保留天数**。原始消息记录的保留时长(天),`0` 为永久保留。 (默认: `30`)
152
+ `cacheRetentionDays`: **原始记录保留天数**。原始消息记录的保留时长(天),`0` 为永久保留。 (默认: `31`)
153
153
  `enableAutoBackup`: **启用自动备份记录**。是否开启每月自动备份原始消息记录。 (默认: `false`)
154
154
  `enableWordCloud`: **启用词云生成**。
155
155
  > **!** 此功能依赖 **`启用原始记录`**。 (默认: `true`)
@@ -160,16 +160,16 @@
160
160
 
161
161
  | 配置项 | 描述 | 默认值 |
162
162
  | :--- | :--- | :--- |
163
- | `maskImage` | **词云蒙版**:提供一个图片的URL作为词云的形状蒙版。**注意:这会覆盖“基础形状”选项。** | () |
163
+ | `color` | **词云颜色**:可设为 `random-light`、`random-dark`、CSS颜色值(如`#ff0000`)或一个返回颜色的JS函数。 | `random-light` |
164
+ | `shape` | **词云形状**:预设值包括 `circle`, `cardioid`, `diamond`, `square`, `triangle`, `pentagon`, `star`。 | `square` |
165
+ | `maskImage` | **蒙版图片**:提供一个图片的URL作为词云的形状蒙版。**注意:这会覆盖“词云形状”选项。** | (空) |
164
166
  | `fontFamily` | **词云字体**:用于渲染词云的字体列表。 | `"Noto Sans CJK SC", "Arial", sans-serif` |
165
- | `minFontSize` | **最小字号**:权重最小的单词所使用的字号。 | `4` |
166
- | `maxFontSize` | **最大字号**:权重最大的单词所使用的字号。 | `64` |
167
- | `gridSize` | **词云间距**:用于分隔单词的网格大小(像素)。值越大,单词间距越大。 | `1` |
168
- | `shape` | **基础形状**:选择词云的整体轮廓(无蒙版时生效)。 | `square` |
169
- | `ellipticity` | **长宽比**:当形状为“椭圆”时,定义其扁平程度。值越小越扁。 | `1` |
170
- | `rotateRatio` | **旋转比**:随机旋转的单词所占的比例(0 到 1)。 | `0.5` |
171
- | `minRotation` | **最小旋转角**:单词随机旋转的最小角度(弧度)。 | `1.570796` (π/2) |
172
- | `maxRotation` | **最大旋转角**:单词随机旋转的最大角度(弧度)。 | `1.570796` (π/2) |
167
+ | `minFontSize` | **最小字号**:权重最小的单词所使用的字号(px)。 | `4` |
168
+ | `maxFontSize` | **最大字号**:权重最大的单词所使用的字号(px)。 | `64` |
169
+ | `ellipticity` | **长宽比**:形状的扁平程度(0-1),仅对非方形形状有效。 | `1` |
170
+ | `minRotation` | **最小旋转角**:单词随机旋转的最小角度(弧度)。 | `-1.570796` (-π/2) |
171
+ | `maxRotation` | **最大旋转角**:单词随机旋转的最大角度(弧度)。 | `1.570796` (π/2) |
172
+ | `rotationSteps`| **旋转步数**:旋转角度的选择方式。0表示随机,2表示只在最小/最大角度中二选一。 | `3` |
173
173
 
174
174
  ## 📌 注意事项
175
175