koishi-plugin-chat-analyse 1.4.7 → 1.4.9

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,17 @@ 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
28
  rotateRatio: number;
27
29
  minRotation: number;
28
30
  maxRotation: number;
31
+ rotationSteps: number;
29
32
  minFontSize: number;
30
33
  maxFontSize: number;
31
34
  gridSize: number;
32
35
  fontFamily: string;
33
- shape: 'square' | 'circle' | 'cardioid' | 'diamond' | 'triangle-forward' | 'triangle' | 'pentagon' | 'star';
34
36
  maskImage: string;
35
37
  }
36
38
  /** @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;
@@ -1582,13 +1562,13 @@ 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: 720, height: 1080, deviceScaleFactor: 2 });
1565
+ await page.setViewport({ width: 1080, height: 720, deviceScaleFactor: 1 });
1586
1566
  await page.setContent(fullHtmlContent, { waitUntil: "networkidle0" });
1587
1567
  const { width, height } = await page.evaluate(() => ({
1588
1568
  width: document.body.scrollWidth,
1589
1569
  height: document.body.scrollHeight
1590
1570
  }));
1591
- await page.setViewport({ width, height, deviceScaleFactor: 2 });
1571
+ await page.setViewport({ width, height, deviceScaleFactor: 1 });
1592
1572
  return await page.screenshot({ type: "png", omitBackground: true });
1593
1573
  } catch (error) {
1594
1574
  this.ctx.logger.error("图片渲染失败:", error);
@@ -1702,34 +1682,32 @@ 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 300;
1697
+ return 20 + index / (labels.length - 1) * 560;
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="${20}" y1="${y}" x2="580" y2="${y}" stroke="var(--border-color)" stroke-width="1"/>`;
1705
+ svgElements += `<text x="${20 - 8}" 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
1708
  if (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) => {
@@ -1738,15 +1716,13 @@ var Renderer = class {
1738
1716
  svgElements += `<polyline points="${points}" fill="none" stroke="${color}" stroke-width="2"/>`;
1739
1717
  });
1740
1718
  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;
1719
+ const LEGEND_START_Y = 295;
1720
+ const columnWidth = 560 / 3;
1745
1721
  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;
1722
+ const rowIndex = Math.floor(seriesIndex / 3);
1723
+ const colIndex = seriesIndex % 3;
1724
+ const legendX = 20 + colIndex * columnWidth;
1725
+ const legendY = LEGEND_START_Y + rowIndex * 20;
1750
1726
  const color = seriesColors[seriesIndex];
1751
1727
  svgElements += `<rect x="${legendX}" y="${legendY - 8}" width="12" height="8" fill="${color}" rx="2"/>`;
1752
1728
  svgElements += `<text x="${legendX + 18}" y="${legendY}" font-size="12" fill="var(--text-color)">${s.name}</text>`;
@@ -1761,7 +1737,7 @@ var Renderer = class {
1761
1737
  <div class="time-label">${time.toLocaleString("zh-CN", { hour12: false })}</div>
1762
1738
  </div>
1763
1739
  <div class="chart-wrapper">
1764
- <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
1740
+ <svg width="600" height="320" xmlns="http://www.w3.org/2000/svg">
1765
1741
  ${svgElements}
1766
1742
  </svg>
1767
1743
  </div>
@@ -1783,7 +1759,6 @@ var Renderer = class {
1783
1759
  const { title, time, words } = data;
1784
1760
  if (!words?.length) return;
1785
1761
  const wordsJson = JSON.stringify(words);
1786
- const selectedPalette = this.COLOR_PALETTES[Math.floor(Math.random() * this.COLOR_PALETTES.length)];
1787
1762
  const weights = words.map((w) => w[1]);
1788
1763
  const maxWeight = Math.max(...weights, 1);
1789
1764
  const minWeight = Math.min(...weights);
@@ -1794,13 +1769,12 @@ var Renderer = class {
1794
1769
  <h1 class="title-text">${title}</h1>
1795
1770
  <div class="time-label">${time.toLocaleString("zh-CN", { hour12: false })}</div>
1796
1771
  </div>
1797
- <div style="width: 512px; height: 512px; margin: auto;">
1798
- <canvas id="wordcloud-container" width="512" height="512"></canvas>
1772
+ <div style="width: 600px; height: 600px; margin: auto;">
1773
+ <canvas id="wordcloud-container" width="600" height="600"></canvas>
1799
1774
  </div>
1800
1775
  <script>${wordCloudScript}</script>
1801
1776
  <script>
1802
1777
  const canvas = document.getElementById('wordcloud-container');
1803
- const palette = ${JSON.stringify(selectedPalette)};
1804
1778
  const options = {
1805
1779
  list: ${wordsJson},
1806
1780
  fontFamily: ${JSON.stringify(config.fontFamily)},
@@ -1809,17 +1783,17 @@ var Renderer = class {
1809
1783
  const normalizedWeight = (size - ${minWeight}) / (${maxWeight} - ${minWeight});
1810
1784
  return ${config.minFontSize} + normalizedWeight * (${config.maxFontSize} - ${config.minFontSize});
1811
1785
  },
1812
- color: (word, weight, fontSize, distance, theta) => {
1813
- return palette[Math.floor(Math.random() * palette.length)];
1814
- },
1786
+ color: ${JSON.stringify(config.color)},
1815
1787
  shape: ${JSON.stringify(config.shape)},
1816
1788
  gridSize: ${config.gridSize},
1817
1789
  ellipticity: ${config.ellipticity},
1818
1790
  rotateRatio: ${config.rotateRatio},
1819
1791
  minRotation: ${config.minRotation},
1820
1792
  maxRotation: ${config.maxRotation},
1793
+ rotationSteps: ${config.rotationSteps},
1821
1794
  backgroundColor: 'transparent',
1822
1795
  clearCanvas: true,
1796
+ shrinkToFit: true,
1823
1797
  shuffle: true,
1824
1798
  };
1825
1799
 
@@ -2548,14 +2522,16 @@ var Config3 = import_koishi7.Schema.intersect([
2548
2522
  import_koishi7.Schema.object({
2549
2523
  ellipticity: import_koishi7.Schema.number().min(0).max(1).default(1).description("长宽比"),
2550
2524
  rotateRatio: import_koishi7.Schema.number().min(0).max(1).default(0.5).description("旋转比"),
2551
- minRotation: import_koishi7.Schema.number().default(Math.PI / 2).description("最小旋转角"),
2525
+ rotationSteps: import_koishi7.Schema.number().min(0).default(3).description("旋转步数"),
2526
+ minRotation: import_koishi7.Schema.number().default(0).description("最小旋转角"),
2552
2527
  maxRotation: import_koishi7.Schema.number().default(Math.PI / 2).description("最大旋转角"),
2553
2528
  minFontSize: import_koishi7.Schema.number().min(1).default(4).description("最小字号"),
2554
2529
  maxFontSize: import_koishi7.Schema.number().min(1).default(64).description("最大字号"),
2555
- gridSize: import_koishi7.Schema.number().min(0).default(1).description("词云间距"),
2530
+ gridSize: import_koishi7.Schema.number().min(0).default(1).description("词语间距"),
2531
+ color: import_koishi7.Schema.string().default("random-light").description("词云颜色"),
2532
+ shape: import_koishi7.Schema.string().default("square").description("词云形状"),
2556
2533
  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
+ maskImage: import_koishi7.Schema.string().role("link").description("蒙版图片")
2559
2535
  }).description("词云生成配置")
2560
2536
  ]);
2561
2537
  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.7",
4
+ "version": "1.4.9",
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,18 @@
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
+ | `gridSize` | **词语间距**:用于分隔单词的网格大小(px)。值越大,单词间距越大。 | `1` |
170
+ | `ellipticity` | **长宽比**:形状的扁平程度(0-1),仅对非方形形状有效。 | `1` |
171
+ | `rotateRatio` | **旋转比**:随机旋转的单词所占的比例(0-1)。 | `0.5` |
172
+ | `minRotation` | **最小旋转角**:单词随机旋转的最小角度(弧度)。 | `0` |
173
+ | `maxRotation` | **最大旋转角**:单词随机旋转的最大角度(弧度)。 | `1.570796` (π/2) |
174
+ | `rotationSteps`| **旋转步数**:旋转角度的选择方式。0表示随机,2表示只在最小/最大角度中二选一。 | `3` |
173
175
 
174
176
  ## 📌 注意事项
175
177