koishi-plugin-group-analysis-mx 1.2.3 → 1.2.5
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/service/analysis.js +59 -15
- package/lib/service/renderer.js +41 -12
- package/package.json +1 -1
package/lib/service/analysis.js
CHANGED
|
@@ -15,6 +15,8 @@ class AnalysisService extends koishi_1.Service {
|
|
|
15
15
|
personaCache = new Map();
|
|
16
16
|
personaProcessing = new Set();
|
|
17
17
|
groupAnalysisCache = new Map();
|
|
18
|
+
// 群昵称缓存:官方 bot 消息作者名是个人昵称,群名片需通过 getGuildMember 获取
|
|
19
|
+
_groupNicknameCache = new Map();
|
|
18
20
|
constructor(ctx, config) {
|
|
19
21
|
super(ctx, 'group_analysis', true);
|
|
20
22
|
this.config = config;
|
|
@@ -702,6 +704,31 @@ class AnalysisService extends koishi_1.Service {
|
|
|
702
704
|
}
|
|
703
705
|
await session.send(message);
|
|
704
706
|
}
|
|
707
|
+
async resolveGroupNickname(bot, guildId, userId) {
|
|
708
|
+
if (!bot?.getGuildMember || !guildId || !userId)
|
|
709
|
+
return null;
|
|
710
|
+
const key = `${bot.selfId}:${guildId}:${userId}`;
|
|
711
|
+
const cached = this._groupNicknameCache.get(key);
|
|
712
|
+
if (cached) {
|
|
713
|
+
if (cached.expireAt > Date.now())
|
|
714
|
+
return cached.name;
|
|
715
|
+
this._groupNicknameCache.delete(key);
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
const info = await bot.getGuildMember(guildId, userId);
|
|
719
|
+
const name = (info?.name || info?.user?.name || '').trim();
|
|
720
|
+
this._groupNicknameCache.set(key, {
|
|
721
|
+
name: name || null,
|
|
722
|
+
expireAt: Date.now() + (name ? 3600_000 : 60_000)
|
|
723
|
+
});
|
|
724
|
+
return name || null;
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
// 无权限或调用失败,缓存失败状态 60 秒,避免反复请求
|
|
728
|
+
this._groupNicknameCache.set(key, { name: null, expireAt: Date.now() + 60_000 });
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
705
732
|
async lookupUserProfileFromMessages(platform, selfId, userId) {
|
|
706
733
|
try {
|
|
707
734
|
const records = await this.ctx.database
|
|
@@ -806,9 +833,23 @@ class AnalysisService extends koishi_1.Service {
|
|
|
806
833
|
});
|
|
807
834
|
// Final statistics
|
|
808
835
|
const sortedUsers = users.sort((a, b) => b.messageCount - a.messageCount);
|
|
836
|
+
// 群名片解析:官方 QQ bot 消息作者名是"个人昵称",群名片需通过 getGuildMember 获取。
|
|
837
|
+
// 只对活跃的 top 用户调用,且带缓存避免重复请求。
|
|
838
|
+
const botForAvatar = this._getBot(selfId);
|
|
839
|
+
const guildIdForNick = target?.guildId;
|
|
840
|
+
const groupNicknameMap = new Map();
|
|
841
|
+
if (botForAvatar?.getGuildMember && guildIdForNick) {
|
|
842
|
+
const topUsers = sortedUsers.slice(0, Math.max(this.config.maxUsersInReport * 2, 20));
|
|
843
|
+
await Promise.all(topUsers.map(async (u) => {
|
|
844
|
+
const gname = await this.resolveGroupNickname(botForAvatar, guildIdForNick, String(u.userId));
|
|
845
|
+
if (gname)
|
|
846
|
+
groupNicknameMap.set(String(u.userId), gname);
|
|
847
|
+
}));
|
|
848
|
+
if (groupNicknameMap.size)
|
|
849
|
+
this.ctx.logger.info(`已获取 ${groupNicknameMap.size} 个群成员的群名片。`);
|
|
850
|
+
}
|
|
809
851
|
// 为用户补全真实头像:优先消息记录中的头像,其次按平台生成头像 URL。
|
|
810
852
|
// 官方 QQ 机器人等平台下 userId 为 openid,不能直接使用 QQ 号头像接口。
|
|
811
|
-
const botForAvatar = this._getBot(selfId);
|
|
812
853
|
const avatarMap = new Map();
|
|
813
854
|
for (const user of sortedUsers) {
|
|
814
855
|
if (!user.avatar) {
|
|
@@ -816,23 +857,26 @@ class AnalysisService extends koishi_1.Service {
|
|
|
816
857
|
}
|
|
817
858
|
avatarMap.set(String(user.userId), user.avatar || '');
|
|
818
859
|
}
|
|
819
|
-
//
|
|
820
|
-
//
|
|
860
|
+
// 昵称映射:统一用群名片作为显示名。LLM 生成的 sender/name 很可能是"个人昵称",
|
|
861
|
+
// 这里把个人昵称(含/不含 emoji)归一化后映射到群名片。
|
|
821
862
|
const nicknameAvatarMap = new Map();
|
|
822
863
|
const nicknameMap = new Map();
|
|
823
864
|
for (const user of sortedUsers) {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
865
|
+
const rawName = user.nickname;
|
|
866
|
+
const displayName = groupNicknameMap.get(String(user.userId)) || rawName;
|
|
867
|
+
if (rawName) {
|
|
868
|
+
const normRaw = (0, utils_1.normalizeNickname)(rawName);
|
|
869
|
+
nicknameMap.set(normRaw || rawName, displayName);
|
|
870
|
+
nicknameMap.set((0, utils_1.normalizeNickname)(displayName) || displayName, displayName);
|
|
871
|
+
}
|
|
872
|
+
if (user.avatar) {
|
|
873
|
+
nicknameAvatarMap.set(rawName, user.avatar);
|
|
874
|
+
const normRaw = (0, utils_1.normalizeNickname)(rawName);
|
|
875
|
+
if (normRaw)
|
|
876
|
+
nicknameAvatarMap.set(normRaw, user.avatar);
|
|
877
|
+
}
|
|
878
|
+
// 排行榜显示名使用群名片
|
|
879
|
+
user.nickname = displayName;
|
|
836
880
|
}
|
|
837
881
|
// 用户称号:用真实昵称替换 LLM 生成的 name(保留 emoji),并补全头像
|
|
838
882
|
if (Array.isArray(userTitles)) {
|
package/lib/service/renderer.js
CHANGED
|
@@ -29,6 +29,10 @@ class RendererService extends koishi_1.Service {
|
|
|
29
29
|
return path_1.default.resolve(this.templateDir, skin, filename);
|
|
30
30
|
}
|
|
31
31
|
async imageToBase64(url) {
|
|
32
|
+
// 已内联的 data URI 直接返回,无需再下载
|
|
33
|
+
if (typeof url === 'string' && url.startsWith('data:')) {
|
|
34
|
+
return url;
|
|
35
|
+
}
|
|
32
36
|
try {
|
|
33
37
|
this.ctx.logger.debug(`转换图片为 Base64: ${url}`);
|
|
34
38
|
const response = await fetch(url);
|
|
@@ -191,6 +195,15 @@ class RendererService extends koishi_1.Service {
|
|
|
191
195
|
// 等待字体加载完成
|
|
192
196
|
await page.evaluate(() => document.fonts.ready);
|
|
193
197
|
this.ctx.logger.info('字体加载完成。');
|
|
198
|
+
// 等待页面内图片加载完成(避免截图像素缺失/头像空白),最多等待 8 秒
|
|
199
|
+
await Promise.race([
|
|
200
|
+
page.evaluate(() => Promise.all(Array.from(document.images)
|
|
201
|
+
.filter((img) => !img.complete)
|
|
202
|
+
.map((img) => new Promise((resolve) => {
|
|
203
|
+
img.onload = img.onerror = resolve;
|
|
204
|
+
})))),
|
|
205
|
+
new Promise((resolve) => setTimeout(resolve, 8000))
|
|
206
|
+
]).catch(() => { });
|
|
194
207
|
// 设置 3 分钟后自动删除临时文件
|
|
195
208
|
this.ctx.setTimeout(async () => {
|
|
196
209
|
try {
|
|
@@ -259,12 +272,15 @@ class RendererService extends koishi_1.Service {
|
|
|
259
272
|
catch (err) {
|
|
260
273
|
this.ctx.logger.debug(`等待字体加载超时: ${err}`);
|
|
261
274
|
}
|
|
262
|
-
//
|
|
263
|
-
await
|
|
264
|
-
.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
275
|
+
// 等待图片加载完成(头像等),最多等待 8 秒
|
|
276
|
+
await Promise.race([
|
|
277
|
+
page.evaluate(() => Promise.all(Array.from(document.images)
|
|
278
|
+
.filter((img) => !img.complete)
|
|
279
|
+
.map((img) => new Promise((resolve) => {
|
|
280
|
+
img.onload = img.onerror = resolve;
|
|
281
|
+
})))),
|
|
282
|
+
new Promise((resolve) => setTimeout(resolve, 8000))
|
|
283
|
+
]).catch(() => { });
|
|
268
284
|
this.ctx.setTimeout(async () => {
|
|
269
285
|
try {
|
|
270
286
|
await fs_1.promises.unlink(outTemplateHtmlPath);
|
|
@@ -298,20 +314,33 @@ class RendererService extends koishi_1.Service {
|
|
|
298
314
|
: (t.contributors || '')
|
|
299
315
|
}));
|
|
300
316
|
// 用户称号:原项目子模板使用 title.avatar_data / title.name / title.title / title.mbti / title.reason
|
|
301
|
-
|
|
317
|
+
// 头像统一转成 base64 内联,避免外网加载失败导致头像空白
|
|
318
|
+
const avatarBase64Cache = new Map();
|
|
319
|
+
const toBase64 = async (url) => {
|
|
320
|
+
if (!url)
|
|
321
|
+
return '';
|
|
322
|
+
if (url.startsWith('data:'))
|
|
323
|
+
return url;
|
|
324
|
+
if (avatarBase64Cache.has(url))
|
|
325
|
+
return avatarBase64Cache.get(url);
|
|
326
|
+
const b64 = await this.imageToBase64(url);
|
|
327
|
+
avatarBase64Cache.set(url, b64);
|
|
328
|
+
return b64;
|
|
329
|
+
};
|
|
330
|
+
const titles = await Promise.all((data.userTitles || []).map(async (t) => ({
|
|
302
331
|
name: t.name || '',
|
|
303
332
|
title: t.title || '',
|
|
304
333
|
mbti: t.mbti || '',
|
|
305
334
|
reason: t.reason || '',
|
|
306
|
-
avatar_data: t.avatar
|
|
307
|
-
}));
|
|
335
|
+
avatar_data: t.avatar ? await toBase64(t.avatar) : ''
|
|
336
|
+
})));
|
|
308
337
|
// 金句:原项目子模板使用 quote.avatar_url / quote.sender / quote.content / quote.reason
|
|
309
|
-
const quotes = (data.goldenQuotes || []).map((q) => ({
|
|
338
|
+
const quotes = await Promise.all((data.goldenQuotes || []).map(async (q) => ({
|
|
310
339
|
sender: q.sender || '',
|
|
311
340
|
content: q.content || '',
|
|
312
341
|
reason: q.reason || '',
|
|
313
|
-
avatar_url: q.avatar
|
|
314
|
-
}));
|
|
342
|
+
avatar_url: q.avatar ? await toBase64(q.avatar) : ''
|
|
343
|
+
})));
|
|
315
344
|
// 渲染子模板为 HTML 字符串
|
|
316
345
|
let topicsHtml = '';
|
|
317
346
|
let titlesHtml = '';
|
package/package.json
CHANGED