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 +8 -8
- package/lib/index.js +71 -77
- package/package.json +1 -1
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 {
|
|
69
|
+
* @returns {AsyncGenerator<Buffer>} - 一个异步生成器,每次迭代产出一张图片的 Buffer。
|
|
70
70
|
*/
|
|
71
|
-
renderList(data: ListRenderData, headers?: string[]):
|
|
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 {
|
|
77
|
+
* @returns {AsyncGenerator<Buffer>} - 一个异步生成器,产出渲染后的图片 Buffer。
|
|
78
78
|
*/
|
|
79
|
-
renderCircadianChart(data: CircadianChartData):
|
|
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 {
|
|
85
|
+
* @returns {AsyncGenerator<Buffer>} - 一个异步生成器,产出渲染后的图片 Buffer。
|
|
86
86
|
*/
|
|
87
|
-
renderWordCloud(data: WordCloudData):
|
|
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
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
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 {
|
|
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)
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
1741
|
-
const
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
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: ${
|
|
1753
|
+
list: ${wordsJson},
|
|
1766
1754
|
fontFamily: '"Noto Sans CJK SC", "Arial", sans-serif',
|
|
1767
|
-
weightFactor: (size) =>
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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("
|
|
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
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
for (const buffer of
|
|
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
|
}
|