koishi-plugin-group-analysis-mx 1.1.0 → 1.2.0
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/config.js +9 -1
- package/lib/service/analysis.js +18 -0
- package/lib/service/renderer.js +189 -18
- package/lib/skins/anime.js +2 -2
- package/lib/skins/art.js +2 -2
- package/lib/skins/md3.js +2 -2
- package/lib/skins/newspaper.js +1 -1
- package/lib/skins/scrapbook.js +1 -1
- package/lib/utils.js +4 -0
- package/package.json +3 -2
- package/resources/original/atri/activity_chart.html +53 -0
- package/resources/original/atri/chat_quality_item.html +155 -0
- package/resources/original/atri/html_template.html +3101 -0
- package/resources/original/atri/image_template.html +1928 -0
- package/resources/original/atri/quote_item.html +33 -0
- package/resources/original/atri/topic_item.html +44 -0
- package/resources/original/atri/user_title_item.html +43 -0
- package/resources/original/bluearchive/activity_chart.html +40 -0
- package/resources/original/bluearchive/chat_quality_item.html +62 -0
- package/resources/original/bluearchive/html_template.html +1279 -0
- package/resources/original/bluearchive/image_template.html +1279 -0
- package/resources/original/bluearchive/quote_item.html +34 -0
- package/resources/original/bluearchive/topic_item.html +20 -0
- package/resources/original/bluearchive/user_title_item.html +40 -0
- package/resources/original/format/activity_chart.html +14 -0
- package/resources/original/format/chat_quality_item.html +131 -0
- package/resources/original/format/html_template.html +546 -0
- package/resources/original/format/image_template.html +546 -0
- package/resources/original/format/quote_item.html +12 -0
- package/resources/original/format/topic_item.html +17 -0
- package/resources/original/format/user_title_item.html +30 -0
- package/resources/original/hack/activity_chart.html +13 -0
- package/resources/original/hack/chat_quality_item.html +188 -0
- package/resources/original/hack/html_template.html +129 -0
- package/resources/original/hack/image_template.html +129 -0
- package/resources/original/hack/quote_item.html +24 -0
- package/resources/original/hack/shared_styles.html +782 -0
- package/resources/original/hack/topic_item.html +11 -0
- package/resources/original/hack/user_title_item.html +30 -0
- package/resources/original/hatsunemiku/activity_chart.html +46 -0
- package/resources/original/hatsunemiku/chat_quality_item.html +43 -0
- package/resources/original/hatsunemiku/html_template.html +1811 -0
- package/resources/original/hatsunemiku/image_template.html +1629 -0
- package/resources/original/hatsunemiku/inline_assets.html +7 -0
- package/resources/original/hatsunemiku/quote_item.html +30 -0
- package/resources/original/hatsunemiku/topic_item.html +37 -0
- package/resources/original/hatsunemiku/user_title_item.html +38 -0
- package/resources/original/retrofuturism/activity_chart.html +15 -0
- package/resources/original/retrofuturism/chat_quality_item.html +177 -0
- package/resources/original/retrofuturism/html_template.html +846 -0
- package/resources/original/retrofuturism/image_template.html +846 -0
- package/resources/original/retrofuturism/quote_item.html +20 -0
- package/resources/original/retrofuturism/topic_item.html +12 -0
- package/resources/original/retrofuturism/user_title_item.html +28 -0
- package/resources/original/simple/activity_chart.html +11 -0
- package/resources/original/simple/chat_quality_item.html +121 -0
- package/resources/original/simple/html_template.html +69 -0
- package/resources/original/simple/image_template.html +69 -0
- package/resources/original/simple/quote_item.html +16 -0
- package/resources/original/simple/topic_item.html +12 -0
- package/resources/original/simple/user_title_item.html +16 -0
- package/resources/original/springfestival/activity_chart.html +44 -0
- package/resources/original/springfestival/chat_quality_item.html +153 -0
- package/resources/original/springfestival/html_template.html +693 -0
- package/resources/original/springfestival/image_template.html +692 -0
- package/resources/original/springfestival/quote_item.html +24 -0
- package/resources/original/springfestival/topic_item.html +14 -0
- package/resources/original/springfestival/user_title_item.html +30 -0
package/lib/config.js
CHANGED
|
@@ -95,7 +95,15 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
95
95
|
koishi_1.Schema.const('anime').description('二次元风格'),
|
|
96
96
|
koishi_1.Schema.const('newspaper').description('报纸风格'),
|
|
97
97
|
koishi_1.Schema.const('art').description('艺术风格'),
|
|
98
|
-
koishi_1.Schema.const('scrapbook').description('手账风格')
|
|
98
|
+
koishi_1.Schema.const('scrapbook').description('手账风格'),
|
|
99
|
+
koishi_1.Schema.const('atri').description('ATRI(原项目皮肤)'),
|
|
100
|
+
koishi_1.Schema.const('hatsunemiku').description('初音未来(原项目皮肤)'),
|
|
101
|
+
koishi_1.Schema.const('bluearchive').description('蔚蓝档案(原项目皮肤)'),
|
|
102
|
+
koishi_1.Schema.const('springfestival').description('新春(原项目皮肤)'),
|
|
103
|
+
koishi_1.Schema.const('retrofuturism').description('复古未来(原项目皮肤)'),
|
|
104
|
+
koishi_1.Schema.const('hack').description('黑客终端(原项目皮肤)'),
|
|
105
|
+
koishi_1.Schema.const('format').description('简约排版(原项目皮肤)'),
|
|
106
|
+
koishi_1.Schema.const('simple').description('极简(原项目皮肤)')
|
|
99
107
|
])
|
|
100
108
|
.description('渲染界面皮肤。')
|
|
101
109
|
.default('md3')
|
package/lib/service/analysis.js
CHANGED
|
@@ -806,6 +806,24 @@ class AnalysisService extends koishi_1.Service {
|
|
|
806
806
|
});
|
|
807
807
|
// Final statistics
|
|
808
808
|
const sortedUsers = users.sort((a, b) => b.messageCount - a.messageCount);
|
|
809
|
+
// 为用户补全真实头像:优先消息记录中的头像,其次按平台生成头像 URL。
|
|
810
|
+
// 官方 QQ 机器人等平台下 userId 为 openid,不能直接使用 QQ 号头像接口。
|
|
811
|
+
const botForAvatar = this._getBot(selfId);
|
|
812
|
+
const avatarMap = new Map();
|
|
813
|
+
for (const user of sortedUsers) {
|
|
814
|
+
if (!user.avatar) {
|
|
815
|
+
user.avatar = (0, utils_1.resolveUserAvatar)(botForAvatar, user.userId) || undefined;
|
|
816
|
+
}
|
|
817
|
+
avatarMap.set(String(user.userId), user.avatar || '');
|
|
818
|
+
}
|
|
819
|
+
// 用户称号列表同样补全头像(LLM 返回的 id 可能是数字类型)
|
|
820
|
+
if (Array.isArray(userTitles)) {
|
|
821
|
+
userTitles.forEach((title) => {
|
|
822
|
+
if (title && !title.avatar && avatarMap.has(String(title.id))) {
|
|
823
|
+
title.avatar = avatarMap.get(String(title.id));
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
}
|
|
809
827
|
const overallActiveHours = users.reduce((acc, user) => {
|
|
810
828
|
for (const hour in user.activeHours) {
|
|
811
829
|
acc[hour] = (acc[hour] || 0) + user.activeHours[hour];
|
package/lib/service/renderer.js
CHANGED
|
@@ -7,8 +7,11 @@ exports.RendererService = void 0;
|
|
|
7
7
|
const koishi_1 = require("koishi");
|
|
8
8
|
const fs_1 = require("fs");
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const nunjucks = require("nunjucks");
|
|
10
11
|
const utils_1 = require("../utils");
|
|
11
12
|
const skins_1 = require("../skins");
|
|
13
|
+
/** 来自原项目 astrbot_plugin_qq_group_daily_analysis 的皮肤(Jinja2 模板,用 nunjucks 渲染) */
|
|
14
|
+
const ORIGINAL_SKINS = ['atri', 'bluearchive', 'format', 'hack', 'hatsunemiku', 'retrofuturism', 'simple', 'springfestival'];
|
|
12
15
|
class RendererService extends koishi_1.Service {
|
|
13
16
|
config;
|
|
14
17
|
static inject = ['puppeteer'];
|
|
@@ -51,23 +54,31 @@ class RendererService extends koishi_1.Service {
|
|
|
51
54
|
const resourcesDir = __dirname + '/../../resources';
|
|
52
55
|
const templateDir = this.templateDir;
|
|
53
56
|
const skin = this.config.skin || 'md3';
|
|
54
|
-
//
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
await fs_1.promises.
|
|
57
|
+
// 拷贝原项目皮肤模板(nunjucks 渲染用)
|
|
58
|
+
const originalSourceDir = path_1.default.resolve(resourcesDir, 'original');
|
|
59
|
+
const originalDestDir = path_1.default.resolve(templateDir, 'original');
|
|
60
|
+
await fs_1.promises.mkdir(originalDestDir, { recursive: true });
|
|
61
|
+
await fs_1.promises.cp(originalSourceDir, originalDestDir, { recursive: true });
|
|
62
|
+
// 原项目皮肤模板直接使用 original 目录,无需拷贝到独立目录
|
|
63
|
+
if (!ORIGINAL_SKINS.includes(skin)) {
|
|
64
|
+
// Source: resources/md3 (or other skin)
|
|
65
|
+
const skinSourceDir = path_1.default.resolve(resourcesDir, skin);
|
|
66
|
+
// Destination: data/group_analysis/md3
|
|
67
|
+
const skinDestDir = path_1.default.resolve(templateDir, skin);
|
|
68
|
+
/* try {
|
|
69
|
+
await fs.access(skinDestDir)
|
|
70
|
+
} catch (error) { */
|
|
71
|
+
await fs_1.promises.mkdir(skinDestDir, { recursive: true });
|
|
72
|
+
// Copy only the configured skin directory
|
|
73
|
+
await fs_1.promises.cp(skinSourceDir, skinDestDir, { recursive: true });
|
|
74
|
+
/* } */
|
|
75
|
+
const tempHtmlFiles = await fs_1.promises
|
|
76
|
+
.readdir(skinDestDir)
|
|
77
|
+
.then((files) => files.filter((file) => file.endsWith('.html') && !file.startsWith('template')))
|
|
78
|
+
.catch(() => []);
|
|
79
|
+
for (const file of tempHtmlFiles) {
|
|
80
|
+
await fs_1.promises.unlink(path_1.default.resolve(skinDestDir, file));
|
|
81
|
+
}
|
|
71
82
|
}
|
|
72
83
|
const page = await this.ctx.puppeteer.page();
|
|
73
84
|
try {
|
|
@@ -105,9 +116,18 @@ class RendererService extends koishi_1.Service {
|
|
|
105
116
|
const hour = new Date().getHours();
|
|
106
117
|
theme = hour >= 19 || hour < 6 ? 'dark' : 'light';
|
|
107
118
|
}
|
|
119
|
+
const skin = config.skin || 'md3';
|
|
120
|
+
if (ORIGINAL_SKINS.includes(skin)) {
|
|
121
|
+
// 原项目皮肤:nunjucks 渲染完整页面后整页截图
|
|
122
|
+
const page = await this._renderOriginalSkinGroup(data, theme);
|
|
123
|
+
const imageBuffer = await page.screenshot({ fullPage: true });
|
|
124
|
+
await page.close();
|
|
125
|
+
this.ctx.logger.info(`图片渲染成功(原项目皮肤 ${skin})!`);
|
|
126
|
+
return imageBuffer;
|
|
127
|
+
}
|
|
108
128
|
const page = await this._renderGroupAnalysis(data, theme);
|
|
109
129
|
// 找到页面中的 container 元素
|
|
110
|
-
const renderer = skins_1.skinRegistry.getSafe(
|
|
130
|
+
const renderer = skins_1.skinRegistry.getSafe(skin);
|
|
111
131
|
const selector = renderer.containerSelector;
|
|
112
132
|
const element = await page.$(selector);
|
|
113
133
|
if (!element) {
|
|
@@ -183,6 +203,157 @@ class RendererService extends koishi_1.Service {
|
|
|
183
203
|
}, 3 * 60 * 1000); // 3 分钟
|
|
184
204
|
return page;
|
|
185
205
|
}
|
|
206
|
+
async _renderOriginalSkinGroup(data, theme) {
|
|
207
|
+
if (!this.ctx.puppeteer) {
|
|
208
|
+
throw new Error('Puppeteer service is not available.');
|
|
209
|
+
}
|
|
210
|
+
const skin = this.config.skin || 'md3';
|
|
211
|
+
const skinDir = path_1.default.resolve(this.templateDir, 'original', skin);
|
|
212
|
+
const templatePath = path_1.default.resolve(skinDir, 'html_template.html');
|
|
213
|
+
if (!fs_1.existsSync(templatePath)) {
|
|
214
|
+
throw new Error(`原项目皮肤模板不存在: ${templatePath}`);
|
|
215
|
+
}
|
|
216
|
+
const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(skinDir), {
|
|
217
|
+
autoescape: false,
|
|
218
|
+
throwOnUndefined: false
|
|
219
|
+
});
|
|
220
|
+
// 注册 Python 风格的 format 过滤器(模板中使用了 "%02d" | format(...) 等)
|
|
221
|
+
env.addFilter('format', (fmt, ...args) => {
|
|
222
|
+
let index = 0;
|
|
223
|
+
return String(fmt).replace(/%[-+0-9.]*[sdif]/g, (match) => {
|
|
224
|
+
const arg = args[index++];
|
|
225
|
+
if (match.endsWith('s'))
|
|
226
|
+
return String(arg ?? '');
|
|
227
|
+
if (match.endsWith('d')) {
|
|
228
|
+
const width = parseInt(match.match(/\d+/)?.[0] || '0', 10);
|
|
229
|
+
let value = String(Math.trunc(Number(arg) || 0));
|
|
230
|
+
if (match.includes('0'))
|
|
231
|
+
value = value.padStart(width, '0');
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
if (match.endsWith('f')) {
|
|
235
|
+
const digits = parseInt(match.match(/\.(\d+)/)?.[1] || '0', 10);
|
|
236
|
+
return (Number(arg) || 0).toFixed(digits);
|
|
237
|
+
}
|
|
238
|
+
return String(arg ?? '');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
const context = await this.buildOriginalSkinContext(data, env);
|
|
242
|
+
const html = env.render('html_template.html', context);
|
|
243
|
+
const randomId = Math.random().toString(36).substring(2, 15);
|
|
244
|
+
const outTemplateHtmlPath = path_1.default.resolve(skinDir, `${randomId}.html`);
|
|
245
|
+
await fs_1.promises.writeFile(outTemplateHtmlPath, html);
|
|
246
|
+
this.ctx.logger.info(`原项目皮肤 ${skin} HTML 生成完成,正在调用 Puppeteer 渲染...`);
|
|
247
|
+
const page = await this.ctx.puppeteer.page();
|
|
248
|
+
await page.setViewport({ width: 1080, height: 1920, deviceScaleFactor: 2 });
|
|
249
|
+
await page.goto('file://' + outTemplateHtmlPath, {
|
|
250
|
+
waitUntil: 'networkidle0',
|
|
251
|
+
timeout: 60000
|
|
252
|
+
}).catch(() => page.goto('file://' + outTemplateHtmlPath, {
|
|
253
|
+
waitUntil: 'domcontentloaded',
|
|
254
|
+
timeout: 60000
|
|
255
|
+
}));
|
|
256
|
+
try {
|
|
257
|
+
await page.evaluate(() => document.fonts.ready);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
this.ctx.logger.debug(`等待字体加载超时: ${err}`);
|
|
261
|
+
}
|
|
262
|
+
// 等待图片加载完成(头像等)
|
|
263
|
+
await page.evaluate(() => Promise.all(Array.from(document.images)
|
|
264
|
+
.filter((img) => !img.complete)
|
|
265
|
+
.map((img) => new Promise((resolve) => {
|
|
266
|
+
img.onload = img.onerror = resolve;
|
|
267
|
+
})))).catch(() => { });
|
|
268
|
+
this.ctx.setTimeout(async () => {
|
|
269
|
+
try {
|
|
270
|
+
await fs_1.promises.unlink(outTemplateHtmlPath);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
this.ctx.logger.debug(`删除临时文件失败: ${error}`);
|
|
274
|
+
}
|
|
275
|
+
}, 3 * 60 * 1000);
|
|
276
|
+
return page;
|
|
277
|
+
}
|
|
278
|
+
async buildOriginalSkinContext(data, env) {
|
|
279
|
+
// 活跃度图表数据:24 小时数组
|
|
280
|
+
const activeHoursData = data.activeHoursData || {};
|
|
281
|
+
const values = Object.values(activeHoursData);
|
|
282
|
+
const maxCount = values.length > 0 ? Math.max(...values) : 0;
|
|
283
|
+
const chartData = Array.from({ length: 24 }, (_, hour) => {
|
|
284
|
+
const count = activeHoursData[hour] || 0;
|
|
285
|
+
return {
|
|
286
|
+
hour,
|
|
287
|
+
count,
|
|
288
|
+
percentage: maxCount > 0 ? Math.round((count / maxCount) * 100) : 0
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
// 话题:原项目子模板使用 topic.index / topic.topic.topic / topic.detail / topic.contributors
|
|
292
|
+
const topics = (data.topics || []).map((t, index) => ({
|
|
293
|
+
index: index + 1,
|
|
294
|
+
topic: { topic: t.topic || '' },
|
|
295
|
+
detail: t.detail || '',
|
|
296
|
+
contributors: Array.isArray(t.contributors)
|
|
297
|
+
? t.contributors.join('、')
|
|
298
|
+
: (t.contributors || '')
|
|
299
|
+
}));
|
|
300
|
+
// 用户称号:原项目子模板使用 title.avatar_data / title.name / title.title / title.mbti / title.reason
|
|
301
|
+
const titles = (data.userTitles || []).map((t) => ({
|
|
302
|
+
name: t.name || '',
|
|
303
|
+
title: t.title || '',
|
|
304
|
+
mbti: t.mbti || '',
|
|
305
|
+
reason: t.reason || '',
|
|
306
|
+
avatar_data: t.avatar || ''
|
|
307
|
+
}));
|
|
308
|
+
// 金句:原项目子模板使用 quote.avatar_url / quote.sender / quote.content / quote.reason
|
|
309
|
+
const quotes = (data.goldenQuotes || []).map((q) => ({
|
|
310
|
+
sender: q.sender || '',
|
|
311
|
+
content: q.content || '',
|
|
312
|
+
reason: q.reason || '',
|
|
313
|
+
avatar_url: q.avatar || ''
|
|
314
|
+
}));
|
|
315
|
+
// 渲染子模板为 HTML 字符串
|
|
316
|
+
let topicsHtml = '';
|
|
317
|
+
let titlesHtml = '';
|
|
318
|
+
let quotesHtml = '';
|
|
319
|
+
let hourlyChartHtml = '';
|
|
320
|
+
try {
|
|
321
|
+
if (topics.length)
|
|
322
|
+
topicsHtml = env.render('topic_item.html', { topics });
|
|
323
|
+
if (titles.length)
|
|
324
|
+
titlesHtml = env.render('user_title_item.html', { titles });
|
|
325
|
+
if (quotes.length)
|
|
326
|
+
quotesHtml = env.render('quote_item.html', { quotes });
|
|
327
|
+
hourlyChartHtml = env.render('activity_chart.html', { chart_data: chartData });
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
this.ctx.logger.warn('渲染原项目皮肤子模板时发生错误,将使用空内容:', error);
|
|
331
|
+
}
|
|
332
|
+
const now = new Date();
|
|
333
|
+
return {
|
|
334
|
+
current_date: data.analysisDate || now.toLocaleDateString('zh-CN'),
|
|
335
|
+
current_datetime: now.toLocaleString('zh-CN', { hour12: false }),
|
|
336
|
+
message_count: data.totalMessages,
|
|
337
|
+
participant_count: data.totalParticipants,
|
|
338
|
+
total_characters: data.totalChars,
|
|
339
|
+
emoji_count: data.emojiCount || 0,
|
|
340
|
+
most_active_period: data.mostActivePeriod || 'N/A',
|
|
341
|
+
// 字体与渲染环境(与原项目默认值保持一致)
|
|
342
|
+
t2i_font_source: 'Mainland',
|
|
343
|
+
t2i_google_fonts_mirror: 'https://fonts.googleapis.com',
|
|
344
|
+
t2i_gstatic_mirror: 'https://fonts.gstatic.com',
|
|
345
|
+
// 子模板 HTML
|
|
346
|
+
topics_html: topicsHtml,
|
|
347
|
+
titles_html: titlesHtml,
|
|
348
|
+
quotes_html: quotesHtml,
|
|
349
|
+
hourly_chart_html: hourlyChartHtml,
|
|
350
|
+
chat_quality_html: '',
|
|
351
|
+
// Token 统计(Koishi 版不统计,填 0)
|
|
352
|
+
total_tokens: 0,
|
|
353
|
+
prompt_tokens: 0,
|
|
354
|
+
completion_tokens: 0
|
|
355
|
+
};
|
|
356
|
+
}
|
|
186
357
|
async renderUserPersona(data, username, avatar, config) {
|
|
187
358
|
try {
|
|
188
359
|
let theme = config.theme;
|
package/lib/skins/anime.js
CHANGED
|
@@ -17,7 +17,7 @@ class AnimeSkinRenderer {
|
|
|
17
17
|
return userStats
|
|
18
18
|
.map((user) => `
|
|
19
19
|
<div class="char-box">
|
|
20
|
-
<img src="${(0, types_1.getAvatarUrl)(user.userId)}" alt="avatar" class="char-avatar">
|
|
20
|
+
<img src="${user.avatar || (0, types_1.getAvatarUrl)(user.userId)}" alt="avatar" class="char-avatar">
|
|
21
21
|
<div class="char-content">
|
|
22
22
|
<div class="char-name">${user.nickname}</div>
|
|
23
23
|
<div class="char-detail">
|
|
@@ -53,7 +53,7 @@ class AnimeSkinRenderer {
|
|
|
53
53
|
return userTitles
|
|
54
54
|
.map((title) => `
|
|
55
55
|
<div class="char-box">
|
|
56
|
-
<img src="${(0, types_1.getAvatarUrl)(title.id)}" alt="avatar" class="char-avatar">
|
|
56
|
+
<img src="${title.avatar || (0, types_1.getAvatarUrl)(title.id)}" alt="avatar" class="char-avatar">
|
|
57
57
|
<div class="char-content">
|
|
58
58
|
<div class="char-name">${title.name}</div>
|
|
59
59
|
<div style="margin-bottom: 6px;">
|
package/lib/skins/art.js
CHANGED
|
@@ -17,7 +17,7 @@ class ArtSkinRenderer {
|
|
|
17
17
|
return userStats
|
|
18
18
|
.map((user) => `
|
|
19
19
|
<div class="user-stat-card">
|
|
20
|
-
<img src="${(0, types_1.getAvatarUrl)(user.userId)}" alt="avatar" class="avatar">
|
|
20
|
+
<img src="${user.avatar || (0, types_1.getAvatarUrl)(user.userId)}" alt="avatar" class="avatar">
|
|
21
21
|
<div class="user-details">
|
|
22
22
|
<div class="nickname">${user.nickname}</div>
|
|
23
23
|
<div class="stats-grid">
|
|
@@ -54,7 +54,7 @@ class ArtSkinRenderer {
|
|
|
54
54
|
return userTitles
|
|
55
55
|
.map((title) => `
|
|
56
56
|
<div class="title-card">
|
|
57
|
-
<img src="${(0, types_1.getAvatarUrl)(title.id)}" alt="avatar" class="avatar">
|
|
57
|
+
<img src="${title.avatar || (0, types_1.getAvatarUrl)(title.id)}" alt="avatar" class="avatar">
|
|
58
58
|
<div class="title-details">
|
|
59
59
|
<div class="nickname">${title.name}</div>
|
|
60
60
|
<div class="title-badge">${title.mbti && title.mbti !== 'N/A' ? `${title.title} | ${title.mbti}` : title.title}</div>
|
package/lib/skins/md3.js
CHANGED
|
@@ -17,7 +17,7 @@ class Md3SkinRenderer {
|
|
|
17
17
|
return userStats
|
|
18
18
|
.map((user) => `
|
|
19
19
|
<div class="user-stat-card">
|
|
20
|
-
<img src="${(0, types_1.getAvatarUrl)(user.userId)}" alt="avatar" class="avatar">
|
|
20
|
+
<img src="${user.avatar || (0, types_1.getAvatarUrl)(user.userId)}" alt="avatar" class="avatar">
|
|
21
21
|
<div class="user-details">
|
|
22
22
|
<div class="nickname">${user.nickname}</div>
|
|
23
23
|
<div class="stats-grid">
|
|
@@ -54,7 +54,7 @@ class Md3SkinRenderer {
|
|
|
54
54
|
return userTitles
|
|
55
55
|
.map((title) => `
|
|
56
56
|
<div class="title-card">
|
|
57
|
-
<img src="${(0, types_1.getAvatarUrl)(title.id)}" alt="avatar" class="avatar">
|
|
57
|
+
<img src="${title.avatar || (0, types_1.getAvatarUrl)(title.id)}" alt="avatar" class="avatar">
|
|
58
58
|
<div class="title-details">
|
|
59
59
|
<div class="nickname">${title.name}</div>
|
|
60
60
|
<div class="title-badge">${title.mbti && title.mbti !== 'N/A' ? `${title.title} | ${title.mbti}` : title.title}</div>
|
package/lib/skins/newspaper.js
CHANGED
|
@@ -19,7 +19,7 @@ class NewspaperSkinRenderer {
|
|
|
19
19
|
<div class="news-profile">
|
|
20
20
|
<div class="profile-rank">NO.${index + 1}</div>
|
|
21
21
|
<div class="profile-main">
|
|
22
|
-
<img src="${(0, types_1.getAvatarUrl)(user.userId)}" alt="avatar" class="profile-avatar">
|
|
22
|
+
<img src="${user.avatar || (0, types_1.getAvatarUrl)(user.userId)}" alt="avatar" class="profile-avatar">
|
|
23
23
|
<div class="profile-info">
|
|
24
24
|
<div class="profile-name">${user.nickname}</div>
|
|
25
25
|
<div class="profile-data">
|
package/lib/skins/scrapbook.js
CHANGED
|
@@ -66,7 +66,7 @@ class ScrapbookSkinRenderer {
|
|
|
66
66
|
<div class="card-tape"></div>
|
|
67
67
|
<div class="user-header">
|
|
68
68
|
<div class="u-avatar">
|
|
69
|
-
<img src="${(0, types_1.getAvatarUrl)(title.id)}" alt="头像" style="width: 100%; height: 100%; object-fit: cover;">
|
|
69
|
+
<img src="${title.avatar || (0, types_1.getAvatarUrl)(title.id)}" alt="头像" style="width: 100%; height: 100%; object-fit: cover;">
|
|
70
70
|
</div>
|
|
71
71
|
<div class="u-info">
|
|
72
72
|
<div class="u-name">${title.name}</div>
|
package/lib/utils.js
CHANGED
|
@@ -41,6 +41,10 @@ function calculateBasicStats(messages) {
|
|
|
41
41
|
userStats[userId] = getInitialUserStats(msg);
|
|
42
42
|
}
|
|
43
43
|
const stat = userStats[userId];
|
|
44
|
+
// 持续更新头像:使用该用户最近一条带头像的消息
|
|
45
|
+
if (msg.avatarUrl) {
|
|
46
|
+
stat.avatar = msg.avatarUrl;
|
|
47
|
+
}
|
|
44
48
|
stat.messageCount++;
|
|
45
49
|
stat.lastActive = new Date(Math.max(stat.lastActive.getTime(), msg.timestamp.getTime()));
|
|
46
50
|
const hour = msg.timestamp.getHours();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-group-analysis-mx",
|
|
3
3
|
"description": "群聊分析插件 - 生成精美群聊统计报告,支持 AI 话题总结(独立版,不依赖 ChatLuna)",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/index.mjs",
|
|
7
7
|
"typings": "lib/index.d.ts",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"chrono-node": "^2.9.0",
|
|
26
26
|
"js-yaml": "^4.1.0",
|
|
27
27
|
"zod": "^3.25.0",
|
|
28
|
-
"cron-parser": "^5.5.0"
|
|
28
|
+
"cron-parser": "^5.5.0",
|
|
29
|
+
"nunjucks": "^3.2.4"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"koishi": "^4.18.0",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<div class="chart-container">
|
|
2
|
+
<div style="display: flex; align-items: stretch; height: 220px; gap: 5px; padding: 20px 0 10px;">
|
|
3
|
+
{% for hour_data in chart_data %}
|
|
4
|
+
{% set ratio = loop.index0 / ((chart_data|length - 1) if (chart_data|length - 1) > 0 else 1) %}
|
|
5
|
+
{% if ratio < 0.3333 %}
|
|
6
|
+
{% set local_ratio = ratio / 0.3333 %}
|
|
7
|
+
{% set top_red = (210 + local_ratio * 45)|round|int %}
|
|
8
|
+
{% set top_green = (225 + local_ratio * 15)|round|int %}
|
|
9
|
+
{% set top_blue = (255 - local_ratio * 25)|round|int %}
|
|
10
|
+
{% set bottom_red = (170 + local_ratio * 85)|round|int %}
|
|
11
|
+
{% set bottom_green = (190 - local_ratio * 5)|round|int %}
|
|
12
|
+
{% set bottom_blue = (255 - local_ratio * 55)|round|int %}
|
|
13
|
+
{% elif ratio < 0.6666 %}
|
|
14
|
+
{% set local_ratio = (ratio - 0.3333) / 0.3333 %}
|
|
15
|
+
{% set top_red = (255 - local_ratio * 20)|round|int %}
|
|
16
|
+
{% set top_green = (240 + local_ratio * 15)|round|int %}
|
|
17
|
+
{% set top_blue = (230 + local_ratio * 10)|round|int %}
|
|
18
|
+
{% set bottom_red = (255 - local_ratio * 105)|round|int %}
|
|
19
|
+
{% set bottom_green = (185 + local_ratio * 45)|round|int %}
|
|
20
|
+
{% set bottom_blue = 200 %}
|
|
21
|
+
{% else %}
|
|
22
|
+
{% set local_ratio = (ratio - 0.6666) / 0.3334 %}
|
|
23
|
+
{% set top_red = (235 - local_ratio * 25)|round|int %}
|
|
24
|
+
{% set top_green = (255 - local_ratio * 30)|round|int %}
|
|
25
|
+
{% set top_blue = (240 + local_ratio * 15)|round|int %}
|
|
26
|
+
{% set bottom_red = (150 + local_ratio * 20)|round|int %}
|
|
27
|
+
{% set bottom_green = (230 - local_ratio * 40)|round|int %}
|
|
28
|
+
{% set bottom_blue = (200 + local_ratio * 55)|round|int %}
|
|
29
|
+
{% endif %}
|
|
30
|
+
{% set count_red = (bottom_red * 0.7)|round|int %}
|
|
31
|
+
{% set count_green = (bottom_green * 0.7)|round|int %}
|
|
32
|
+
{% set count_blue = (bottom_blue * 0.7)|round|int %} <div
|
|
33
|
+
style="flex: 1; display: flex; flex-direction: column; align-items: center; min-width: 0; height: 100%;">
|
|
34
|
+
<div
|
|
35
|
+
style="flex: 1; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: end;">
|
|
36
|
+
<span
|
|
37
|
+
style="font-size: 10px; font-weight: 700; color: rgb({{ count_red }}, {{ count_green }}, {{ count_blue }}); margin-bottom: 4px; line-height: 1; white-space: nowrap; text-shadow: 0 1px 0 rgba(255,255,255,0.65);">{{hour_data.count}}</span>
|
|
38
|
+
<div style="
|
|
39
|
+
background: linear-gradient(180deg, rgb({{ top_red }}, {{ top_green }}, {{ top_blue }}) 0%, rgb({{ bottom_red }}, {{ bottom_green }}, {{ bottom_blue }}) 100%);
|
|
40
|
+
width: calc(100% - 4px);
|
|
41
|
+
height: {{hour_data.percentage}}%;
|
|
42
|
+
border-radius: 10px 10px 0 0;
|
|
43
|
+
transition: all 0.3s ease;
|
|
44
|
+
min-height: 5px;
|
|
45
|
+
box-shadow: 0 6px 14px rgba({{ bottom_red }}, {{ bottom_green }}, {{ bottom_blue }}, 0.14);
|
|
46
|
+
border: none;
|
|
47
|
+
" title="{{hour_data.hour}}时 - {{hour_data.count}}条消息"></div>
|
|
48
|
+
</div>
|
|
49
|
+
<span style="font-size: 10px; color: #9a8aa3; margin-top: 8px; line-height: 1;">{{hour_data.hour}}</span>
|
|
50
|
+
</div>
|
|
51
|
+
{% endfor %}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.quality-card {
|
|
3
|
+
position: relative;
|
|
4
|
+
padding: 24px 24px 20px;
|
|
5
|
+
border-radius: 24px;
|
|
6
|
+
background: linear-gradient(90deg, rgba(255,240,230,0.95) 0%, rgba(255,255,255,0.98) 100%) !important;
|
|
7
|
+
border: 1px solid rgba(112, 189, 236, 0.22);
|
|
8
|
+
box-shadow: 0 18px 40px rgba(86, 161, 205, 0.14);
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.quality-card::before {
|
|
13
|
+
content: "锐评";
|
|
14
|
+
position: absolute;
|
|
15
|
+
top: 14px;
|
|
16
|
+
right: 18px;
|
|
17
|
+
font-size: 46px;
|
|
18
|
+
font-weight: 700;
|
|
19
|
+
color: rgba(83, 171, 219, 0.08);
|
|
20
|
+
letter-spacing: 4px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.quality-head {
|
|
24
|
+
display: flex;
|
|
25
|
+
justify-content: space-between;
|
|
26
|
+
align-items: flex-end;
|
|
27
|
+
gap: 16px;
|
|
28
|
+
margin-bottom: 18px;
|
|
29
|
+
position: relative;
|
|
30
|
+
z-index: 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.quality-title {
|
|
34
|
+
font-size: 28px;
|
|
35
|
+
font-weight: 700;
|
|
36
|
+
color: #2a6587;
|
|
37
|
+
margin-bottom: 4px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.quality-subtitle {
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
padding: 7px 12px;
|
|
44
|
+
border-radius: 999px;
|
|
45
|
+
background: rgba(117, 203, 244, 0.16);
|
|
46
|
+
color: #5492b7;
|
|
47
|
+
font-size: 14px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.quality-bars {
|
|
51
|
+
display: grid;
|
|
52
|
+
gap: 12px;
|
|
53
|
+
margin-bottom: 16px;
|
|
54
|
+
position: relative;
|
|
55
|
+
z-index: 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.quality-bar-row {
|
|
59
|
+
display: grid;
|
|
60
|
+
grid-template-columns: 94px 1fr 54px;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 12px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.quality-bar-label {
|
|
66
|
+
font-size: 14px;
|
|
67
|
+
color: #52768d;
|
|
68
|
+
font-weight: 600;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.quality-bar-track {
|
|
72
|
+
height: 12px;
|
|
73
|
+
border-radius: 999px;
|
|
74
|
+
background: rgba(126, 199, 235, 0.16);
|
|
75
|
+
overflow: hidden;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.quality-bar-fill {
|
|
79
|
+
height: 100%;
|
|
80
|
+
border-radius: 999px;
|
|
81
|
+
box-shadow: 0 6px 12px rgba(100, 167, 209, 0.2);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.quality-bar-value {
|
|
85
|
+
text-align: right;
|
|
86
|
+
font-size: 13px;
|
|
87
|
+
color: #5d88a3;
|
|
88
|
+
font-weight: 700;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.quality-comment-grid {
|
|
92
|
+
display: grid;
|
|
93
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
94
|
+
gap: 12px;
|
|
95
|
+
margin-bottom: 16px;
|
|
96
|
+
position: relative;
|
|
97
|
+
z-index: 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.quality-comment {
|
|
101
|
+
padding: 14px 14px 12px;
|
|
102
|
+
border-radius: 16px;
|
|
103
|
+
background: rgba(255,255,255,0.74);
|
|
104
|
+
border: 1px dashed rgba(113, 191, 237, 0.34);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.quality-comment strong {
|
|
108
|
+
display: inline-block;
|
|
109
|
+
margin-bottom: 6px;
|
|
110
|
+
color: #3f84ad;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.quality-summary {
|
|
114
|
+
position: relative;
|
|
115
|
+
z-index: 1;
|
|
116
|
+
padding: 14px 16px;
|
|
117
|
+
border-radius: 18px;
|
|
118
|
+
background: rgba(255, 255, 255, 0.65);
|
|
119
|
+
color: #4f7389;
|
|
120
|
+
line-height: 1.8;
|
|
121
|
+
border: 1px solid rgba(112, 189, 236, 0.2);
|
|
122
|
+
}
|
|
123
|
+
</style>
|
|
124
|
+
|
|
125
|
+
<div class="quality-card">
|
|
126
|
+
<div class="quality-head">
|
|
127
|
+
<div>
|
|
128
|
+
<div class="quality-title">{{ title }}</div>
|
|
129
|
+
<div class="quality-subtitle">{{ subtitle }}</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div class="quality-bars">
|
|
134
|
+
{% for dim in dimensions %}
|
|
135
|
+
<div class="quality-bar-row">
|
|
136
|
+
<div class="quality-bar-label">{{ dim.name }}</div>
|
|
137
|
+
<div class="quality-bar-track">
|
|
138
|
+
<div class="quality-bar-fill" style="width: {{ dim.percentage }}%; background: linear-gradient(90deg, {{ dim.color }}, color-mix(in srgb, {{ dim.color }} 65%, white));"></div>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="quality-bar-value">{{ dim.percentage }}%</div>
|
|
141
|
+
</div>
|
|
142
|
+
{% endfor %}
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div class="quality-comment-grid">
|
|
146
|
+
{% for dim in dimensions %}
|
|
147
|
+
<div class="quality-comment">
|
|
148
|
+
<strong>{{ dim.name }}</strong>
|
|
149
|
+
<div>{{ dim.comment }}</div>
|
|
150
|
+
</div>
|
|
151
|
+
{% endfor %}
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div class="quality-summary">{{ summary }}</div>
|
|
155
|
+
</div>
|